From c021b936d9fa6bed507c80cc36af7f1ca31b40d2 Mon Sep 17 00:00:00 2001 From: fengruixiang <474182370@qq.com> Date: Wed, 8 Jul 2026 14:43:21 +0800 Subject: [PATCH] Refactor code structure for improved readability and maintainability --- .gitignore | 11 +- .../KYC订阅字段与租用逻辑分析.html | 327 ++++++++++++ docs/justsolutionsWebV2/订单处理流程单.html | 503 ++++++++++++++++++ 3 files changed, 836 insertions(+), 5 deletions(-) create mode 100644 docs/justsolutionsWebV2/KYC订阅字段与租用逻辑分析.html create mode 100644 docs/justsolutionsWebV2/订单处理流程单.html diff --git a/.gitignore b/.gitignore index 389f88a..9223439 100644 --- a/.gitignore +++ b/.gitignore @@ -1,5 +1,6 @@ -docs/AML_Backend/docker-compose-local-dev/AML-local-dev.bak -justsolutionsWebV2/ -AML_Backend/ -AML_Frontend/ -justsolutionsWeb/ +/docs/AML_Backend/docker-compose-local-dev/AML-local-dev.bak +/justsolutionsWebV2/ +/AML_Backend/ +/AML_Frontend/ +/justsolutionsWeb/ + \ No newline at end of file diff --git a/docs/justsolutionsWebV2/KYC订阅字段与租用逻辑分析.html b/docs/justsolutionsWebV2/KYC订阅字段与租用逻辑分析.html new file mode 100644 index 0000000..519f93b --- /dev/null +++ b/docs/justsolutionsWebV2/KYC订阅字段与租用逻辑分析.html @@ -0,0 +1,327 @@ + + + + + +justsolutionsWebV2 · plans-plus KYC 訂閱欄位與租用邏輯分析 + + + + +
+ +
+

plans-plus · KYC 訂閱欄位與租用邏輯分析

+

釐清 plans-plus.html 續費查詢頁「上次訂閱資訊」中的 KYC 欄位到底代表什麼、KYC 功能是否受「有沒有租設備」限制、以及是否每個租戶預設開啟;並記錄本次為此做的文案修正。

+

範圍:AML_BackendiCON.Abp.AMLPortal / iCON.Abp.AML)· AML_Frontend(KYC section)· justsolutionsWebV2(plans-plus 前端文案)

+

結論一句話:「上次訂閱資訊」的 KYC = 上次訂閱是否含「KYC 讀卡設備租用」加購項;租不租設備只影響前端的「機器掃描」入口,不影響上傳/手動錄入 KYC,且非每個租戶預設開啟

+
+ + + + +
+

一、結論速覽

+ + + + + + + + + + + + + + + + + + + + + + +
問題答案說明
「上次訂閱資訊」的 KYC 應該是「是否租用 KYC 設備」嗎?基本是該值 = TenantProperty.EnableKYC,而它由「下單方案是否含 Tag1Code = KYC 的方案行」決定;此方案行在 plans-plus 對應加購項「KYC 讀卡設備租用」。故它實質表示「上次訂閱有無租 KYC 讀卡設備」。
有沒有租 KYC 設備,會影響用戶在系統中使用 KYC 功能嗎?只影響掃描入口enableKYC 在前端只控制「ID Scan/機器掃描」按鈕的顯隱。「上傳」與「手動錄入」兩條路徑始終可用;後端 KYCService/KYCController 完全不校驗 EnableKYC。
「能否使用 KYC 功能」是每個租戶預設開啟的嗎?不是EnableKYC 對一般租戶預設 false,僅在下單方案含 KYC 加購時為 true(續費沿用上次值)。唯一預設 true 的是 Host 宿主租戶。但因前端僅擋掃描按鈕,所有租戶預設仍可用「上傳/錄入」做 KYC。
+
+ + +
+

二、欄位資料鏈路(前端 → 後端)

+ +

plans-plus 續費頁「上次訂閱資訊」卡片的「已購增值 / Add-ons」列,經由這條鏈路取得 KYC 標記:

+ +
plans-plus.js previousAddonText()currentSubscription.addons.kyc → BFF 直通 → CurrentSubscriptionAddonsDto.Kyctp.EnableKYC
+ + + + + + + + + + + + + + + + + + + + + +
內容
前端顯示
plans-plus.js:570-581 / plans-plus.html:347
showTenantInfo()previousAddonText(s) 寫入 #ppTiAddons;當 addons.kyc === true 時,列表加入一個 KYC 標籤。
續費查詢 DTO
RenewableTenantByEmailDto.cs:46-54
CurrentSubscriptionAddonsDto.Kyc,欄位註解明寫「是否已購 KYC(TenantProperty.EnableKYC)」。
後端聚合
OrderService.cs:2460
Addons = new CurrentSubscriptionAddonsDto { Kyc = tp.EnableKYC }——直接取當前生效 TenantProperty.EnableKYC
底層欄位
TenantProperty.cs:69-71
public bool EnableKYC,註解「是否啟用了 KYC 功能」。命名雖是「功能開關」,但賦值來源只綁定「有沒有買 KYC 設備加購」(見下節)。
+ +
+

加購項對應:plans-plus 第 4 步的 KYC 加購項在 plans-plus.html:468 標題為「KYC ID Reader Rental / KYC 身份讀取設備租用」——一台實體讀卡機,按訂閱期租用。這正是產生 Tag1Code = KYC 方案行、進而把 EnableKYC 置 true 的東西。

+
+
+ + +
+

三、EnableKYC 何時被賦值為 true

+ +

EnableKYC 全庫僅有 3 種寫入來源(其餘皆為 EF 遷移快照),沒有任何「按功能開關」的入口

+ + + + + + + + + + + + + + + + + + + + + + + + + +
場景代碼取值
新租戶下單OrderService.cs:763EnableKYC = checkHasEnableKYCInPlan(purchasePlanDetailList)
續費(新開訂單路徑)OrderService.cs:2633EnableKYC = checkHasEnableKYCInPlan(purchasePlanDetailList)
續費(事件隊列生效路徑)OrderService.cs:872EnableKYC = lastTenantProperty.EnableKYC沿用上一份
Host 宿主初始化TenantPropertyDataSeeder.cs:63-79EnableKYC = true僅當 currentTenant.Id == null,即 Host
+ +

判定函式只看方案的 Tag1:

+
// OrderService.cs:2694-2697
+private bool checkHasEnableKYCInPlan(List<PlanDetail> purchasePlanDetailList)
+{
+    return purchasePlanDetailList
+        .Where(a => a.Plan.Tag1Code == PlanTag1Enums.KYC.ToString()).Any();
+}
+ +
+

推論:EnableKYC 語義上等同「該訂閱是否含 KYC 讀卡設備租用」,而不是一個獨立的「KYC 功能總開關」。所以把「上次訂閱資訊」裡的它讀作「是否租用 KYC 設備」是準確的。

+
+
+ + +
+

四、租設備是否影響「使用 KYC 功能」

+ +

4.1 前端:只擋「機器掃描」按鈕

+

AML_Frontend 的 KYC section 讀取當前租戶的 enableKYC

+
// kyc-section.component.ts:147-148
+this.amlTenant.getCurrentTenantProperty$().subscribe(res => {
+  this.enableKYC = res?.enableKYC ?? false
+})
+ +

而它只作用在「ID Scan(機器讀卡掃描)」這一個按鈕上(switchType === 1):

+ + + + + + + +
按鈕switchType受 enableKYC 控制?說明
ID Scan(機器掃描)1是(*ngIf="enableKYC")插實體讀卡機掃描證件;未租設備則隱藏。kyc-section.component.html:10
Upload2(上傳)2否 · 始終顯示上傳證件圖片走 OCR/校驗。kyc-section.component.html:18-24
Input(手動錄入)3否 · 始終顯示手動輸入證件資訊。kyc-section.component.html:26-32
+ +

4.2 後端:無任何門檻

+

後端 KYCService / KYCController 從不讀取 EnableKYC。全庫對該欄位的非遷移讀取僅有兩處:續費沿用(OrderService.cs:872)與續費查詢上報(OrderService.cs:2460)。因此它純粹是前端那顆掃描按鈕的顯隱旗標,不構成服務端鑑權。

+ +
+

結論:沒租設備的租戶照樣能用完整 KYC(上傳 / 手動錄入 → OCR、校驗、檢索),只是看不到「插讀卡機掃證件」這個入口。
「租 KYC 設備」= 解鎖前端硬體掃描按鈕,而非解鎖 KYC 功能本身。

+
+
+ + +
+

五、是否每個租戶預設開啟

+ +
+

一句話:廣義 KYC(上傳/錄入)對所有租戶預設可用;EnableKYC(=是否租了 KYC 讀卡設備)預設關閉,它只額外解鎖前端「機器掃描證件」按鈕。

+
+
+ + +
+

六、本次文案修正

+

原本 plans-plus「上次訂閱資訊」把 KYC 加購項顯示為含糊的「KYC/KYB」(易被誤解成一種核查功能)。已改為與加購項一致的「KYC 設備租用」表述,以準確反映「是否租用 KYC 設備」。後端無改動。

+ + + + + + + + + + + + + + + +
位置修改前修改後
plans-plus.js:575
previousAddonText,「已購增值」列)
parts.push('KYC/KYB')parts.push(tr('ppx_addon_kyc_label'))
plans-plus.html:579
(訂單摘要 #ppSumKyc 靜態回退文字)
KYC/KYB VerificationKYC ID Reader Rental
+ +

改用的 i18n 鍵 ppx_addon_kyc_label 三語皆已是租用表述:繁中「KYC 身份讀取設備租用」/ EN「KYC ID Reader Rental」/ 日「KYC IDリーダー機器レンタル」,故翻譯本體無需改動。訂單摘要 ppx_sum_kyc 的翻譯值原本就已是租用表述,本次僅同步修正其過期的 HTML 靜態回退文字。

+ +
+

驗證:本地以 npm run start:local 啟動 justsolutionsWebV2 即可看到「上次訂閱資訊 → 已購增值」與訂單摘要處的 KYC 顯示為「KYC 設備租用」表述。

+
+
+ + +
+

七、關鍵代碼位置索引

+ + + + + + + + + + + + +
角色檔案 : 行
底層欄位定義AML_Backend/modules/iCON.Abp.AMLPortal/…/Domain/DbEntity/TenantProperty.cs:69-71
賦值:新租戶 / 續費 / 判定函式…/AMLPortal.Application/OrderService.cs:763、2633、872、2694-2697
續費查詢上報 KYC…/AMLPortal.Application/OrderService.cs:2460
續費查詢 DTO…/AMLPortal.Application.Contracts/OrderAppLayer/RenewableTenantByEmailDto.cs:46-54
Host 預設 true…/AMLPortal.Domain/Seeders/TenantPropertyDataSeeder.cs:63-79
前端讀 enableKYC / 掃描按鈕門檻AML_Frontend/…/kyc-section/kyc-section.component.ts:147-148、.html:10
AML DTO 映射…/iCON.Abp.AML.Application/AMLAppAutoMapperProfile.cs:78 · …/TenantConfigLayer/TenantPropertyDto.cs:118
plans-plus 顯示 / 加購項justsolutionsWebV2/public/js/plans-plus.js:570-581 · public/plans-plus.html:468、579
+
+ +
+

本文為程式碼閱讀分析結論,隨代碼演進可能變動;引用行號以撰寫時的倉庫狀態為準。

+ +
+ + diff --git a/docs/justsolutionsWebV2/订单处理流程单.html b/docs/justsolutionsWebV2/订单处理流程单.html new file mode 100644 index 0000000..e53913d --- /dev/null +++ b/docs/justsolutionsWebV2/订单处理流程单.html @@ -0,0 +1,503 @@ + + + + + +plans-plus · 订单处理流程单(下单 → 支付 → 开通) + + + +
+ +
+

plans-plus · 订单处理流程单

+

用户在 plans-plus.html 提交订单后,后端如何处理;支付完成后又如何开通 / 续期 / 执行检测。

+

覆盖四类:新增租户(new) · 续费(renew) · 增值服务(topup) · 即用即付 / 单次查询(single)

+

分层:前端 plans-plus(justsolutionsWebV2/public) → Node BFF(justsolutionsWebV2/server) → 后端 AMLPortal.OrderService / AML.ConsumerPortalService。

+

依据源码撰写;关键实现均标注 文件:行号。基于当前 main 分支代码。

+
+ + + +
+

0 · 架构分层与请求链路

+

所有请求先打到本站 /api/*,由 justsolutionsWebV2 的 Node/Express 服务器按环境分流:test 走本地 mock,其他环境把订阅类接口转成真实后端调用(订阅路由已实现真实对接,单次查询 / 在线支付本轮仍由 mock 兜底)。

+
+ 前端 plans-plus.js + Node BFF(server/routes、server/mock) + 后端同步(OrderService / ConsumerPortalService) + 消息队列 / 支付回调 + 后台 Job(周期 / 异步开通) + 数据 / 状态落库 +
+
+
+
FE
plans-plus 表单
collectPayload() 汇总 type + 方案 + 加值项 + 主体信息
public/js/plans-plus.js:1221
+
+
api.post()/api/*
+
+
BFF
Express 分流
APP_ENV / MOCK:真实订阅路由 vs mock 兜底
server/index.js
+
+
+
+
BE · 订阅
AMLPortal · OrderService
new → CreateOrder;renew/topup → TenantRenewal
amlPortal/Order/portal/*
+
BE · 即用即付
AML · ConsumerPortalService
single → CreateConsumerLinkCreateConsumerOrder
aml/ConsumerPortal/*
+
+
支付成功后(回调 / mock 支付 / 消息队列)
+
+
异步 · 订阅
TenantEventQueueJob
周期跑 ProcessTenantEventQueue → 建/续租户
Basic/Jobs/TenantEventQueueJob.cs
+
异步 · 检测
RabbitMQ 消费者
ProcessConsumerPortalOrder → 按 functionCodes 跑检测
Basic/RabbitMQConsumerService.cs:186
+
+
+

下文每类订单单独给出完整链路。订阅三类(new/renew/topup)共用同一套「订单 → 支付回调 → 租户事件队列 Job」骨架;即用即付走另一套「即时检测链接 → 订单 → RabbitMQ → 检测编排」骨架。

+
+ +
+

1 · 四类订单总览

+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
类型前端 typeBFF 路由后端接口后端服务方法订单枚举支付后动作
新增租户newPOST /api/subscribe
routes/subscribe.js
/api/amlPortal/Order/portal/CreateOrderOrderService.CreateOrder
OrderService.cs:146
OrderType=NewReg新建 ABP 租户 + 按 edition 开关 feature + 建 TenantProperty
续费renewPOST /api/subscribe/api/amlPortal/Order/portal/TenantRenewalOrderService.TenantRenewal
OrderService.cs:2199
OrderType=Renewal作废旧 TenantProperty,建新的(期限往后叠加、额度累加)
增值服务topupPOST /api/subscribe/api/amlPortal/Order/portal/TenantRenewalOrderService.TenantRenewal(PlanList 只含加值项)OrderType=Renewal同续费分支;但无基础方案 → 只叠加额度 / 用户数 / KYC,不延长期限
即用即付singlePOST /api/single-query
当前 mock
目标:/api/aml/ConsumerPortal/CreateConsumerLinkCreateConsumerOrderConsumerPortalService
ConsumerPortalService.cs:964 / 325
ConsumerPortalOrder发 RabbitMQ → 按 functionCodes 逐模块检测 → 邮件发结果
+
前端如何分流 + ppSubmit() plans-plus.js:1342type==='single'submitSingle()(走 /single-query + /payments/create);否则 → POST /subscribe。BFF 的 subscribe.js 再按 payload.type 决定调 CreateOrder(new)还是 TenantRenewal(renew / topup)。 +
+
+ +
+

2 · 新增租户(new)

+
+
FE
提交新购订单
主体信息(公司/个人)+ 行业 edition + 方案 + 加值项 + 推荐人代码。POST /subscribetype='new'
+
+
BFF
subscribe.js 组装后端入参
+
getCatalogBundle(countryCode) 取目录 → buildPlanList() 把方案 + 各加值项映射为 {PlanId, PlanDetailId, PCS}resolveAgentId() 把推荐人代码解析成后端用户 Guid(AgentorId)。KYC 按订阅月数选期限变体。routes/subscribe.js
+
POST /api/amlPortal/Order/portal/CreateOrder
+
BE
OrderService.CreateOrder :146
+
校验:邮箱未占用、Agentor / Plan / PlanDetail 合法、TenantName 未重复、BR/CI 不重复(BR/CI 允许为空)、edition 合法。
Customer(默认密码)→ 建 OrderPenddingPaid / UnPaidOrderType=NewReg)+ OrderDetail(记录期限 / JCount / QCount / 用户上限)。未指定 agent → 挂到顶级 salesAdmin。
+
按金额 / 支付方式分三条支路
+
+
A · 免费单
PlanPrice≤0PendingActive,发「激活邮件」,用户点邮件里的 ActiveFreeOrder 链接后再开通。
+
B · 线下支付
IsOfflinePayment=truePenddingAudit,等管理员 OrderAudit 审核后进入开通。
+
C · 线上支付
MockupPayment() :2916:若 IsMockupPayment=true,读取 mock webhook 数据、内联直接调 OnPaymentSuccess()(等价于「立即支付成功」)。真实支付则等支付方回调。
+
+
支付成功(回调 / mock 支付)
+
BE · 回调
OnPaymentSuccess :1174
+
out_trade_no=OrderCode 找单;写 PaymentInfo(含 InvoiceNo)→ Order.OrderStatus=Paid / PaymentStatus=Paid / 记 PaidAmount / PaymentTime幂等:已有 PaymentInfo 则直接 return。最后 InsertTenantEventQueue(order) 插入一条 EventType=Create / Status=Pending 的租户事件队列 :1371
+
后台 Job 周期扫描队列(异步,非请求内完成)
+
JOB
ProcessTenantEventQueue(NewReg 分支) :528 / 570
+
+ ① 若同名租户已存在 → 只补发通知邮件、队列置 Success(幂等)。
+ ② 否则调 CreateTenant()(ABP SaaS 建租户 + admin)。
+ ③ 关 AMLPortal.Enable,按 edition → feature 映射 启用 / 禁用业务功能(行业决定可用模块)。
+ ④ 建 TenantProperty:期限(结合前端 EffectiveStartTime + 各方案 period 叠加)、JCountLeft/QCountLeft(累加)、UserCountLimit=max(各方案)+附加用户、EnableKYC
+ ⑤ 写 TenantInfo(邮箱 / 国别 / BR/CI / 地址 / 联系人)。
+ ⑥ 发邮件给买家(含改密链接)+ 逐级 agent。
+ ⑦ Order=Completed、队列 Success。失败按 RetryTimes 重试,超限 Failed
+
+
期限计算(NewReg) 若该租户已有生效中的 TenantProperty 且未过期 → 沿用旧的起止时间再往后叠加本次 period(前端选的开始日作废);已过期 / 无历史 → 以前端 EffectiveStartTime(默认今天)为起点叠加。OrderService.cs:692-751
+
+ +
+

3 · 续费(renew)

+

续费前会先查回当前订阅:前端在第 1 步用管理员邮箱 ppLookupTenant() 命中 queryRenewableTenantByEmail,带出租户、当前方案、到期日、额度、推荐人,并默认沿用上次方案。

+
+
FE
提交续费
type='renew',payload 带 tenantId + 管理员邮箱 + 新方案 + 加值项。POST /subscribe
+
+
BFF
subscribe.js(renew 支)
同样 buildPlanList()(含基础方案 + 加值项)→ 组 TenantRenewalParamTargetTenantID + TenantAdminEmail + PlanList)。routes/subscribe.js
+
POST /api/amlPortal/Order/portal/TenantRenewal
+
BE
OrderService.TenantRenewal :2199
+
校验 Plan/PlanDetail、目标租户存在、找到 IsActive 的当前 TenantProperty;校验 admin 邮箱与租户 admin 一致。复用 lastOrder 的公司 / 邮箱 / 行业 / BR/CI 等,建 OrderRenewal / PenddingPaid / UnPaid)+ OrderDetail。基础方案(Tag1=B)的服务起点:当前 EffectiveEndTime 未过期→接续到期日之后;已过期→从今天起。未指定 agent → 沿用上次 agent。
+
线上→ MockupPayment / 线下→ PenddingAudit
+
BE · 回调
OnPaymentSuccess → InsertTenantEventQueue
与新购完全一致:写 PaymentInfo、订单置 Paid、插入 EventType=Create 队列(同一队列,Job 内按 OrderType 区分分支)。
+
+
JOB
ProcessTenantEventQueue(续费分支) :848-989
+
+ 不新建租户。找到当前 IsActive TenantProperty → 置 IsActive=false → 建新一条 TenantProperty:
+ · JCountLeft/QCountLeft = 旧余额 + 本次购买(额度累加,续费不清零)
+ · 期限:未过期→在原到期日基础上叠加本次 period;已过期→从今天起算 period
+ · UserCountLimit = max(旧, 各方案) + 附加用户EnableKYC 沿用
+ · 按 edition 再次启用对应 feature
+ · 发续费邮件给买家 + agent;Order=Completed、队列 Success
+
+
额度语义 续费是「续期 + 叠加额度」,不是重置:JCountLeft/QCountLeft 在旧余额上累加,期限从原到期日往后接续(未过期时)。OrderService.cs:879-880, 888-929
+
+ +
+

4 · 增值服务(topup)

+

增值服务(加购)与续费共用同一后端接口 TenantRenewal,区别只在 PlanList 不含基础方案,只含加值项(增加使用人 / KYC 设备租赁 / jQuota 加量)。

+
+
FE
提交加购
type='topup';只勾选加值项。若订阅已过期 → 前端拦截,提示「先续费」(ppTopupBlocked)。
+
+
BFF
buildPlanList(topup 特判)
if (payload.type !== 'topup') 才 push 基础方案 → topup 时跳过基础方案,只映射 users / kyc / jquota 三类加值项的 PlanId/PlanDetailId/PCSroutes/subscribe.js buildPlanList()
+
POST /api/amlPortal/Order/portal/TenantRenewal(同续费接口)
+
BE
OrderService.TenantRenewal :2199
同续费:建 Renewal 订单 + OrderDetail。因 PlanList 无 Tag1=B 的基础方案,CurrentOrderServiceStartTime/EndTime 不会被设置。
+
支付成功 → 队列
+
JOB
ProcessTenantEventQueue(续费分支)
+
走与续费相同的分支,但因订单里没有带 period 的基础方案:
+ · 期限不延长(dtEffectiveEndTime 无 period 可叠加,沿用旧到期日)
+ · 只叠加:JCountLeft/QCountLeft(若买了 jQuota)、UserCountLimit(若买了增加使用人 AdlU)、KYC(若租设备)
+ · 发通知邮件,Order=Completed
+
+
加购 = 续费接口的子集 后端不区分 renew / topup(都是 OrderType=Renewal),差异完全由 PlanList 里有没有基础方案决定。因此「只加额度 / 只加用户 / 只租 KYC」而不延长期限,是 topup 的天然结果,而非后端另写了一套逻辑。
+
+ +
+

5 · 即用即付 / 单次查询(single)

+

即用即付不创建租户、不做订阅,而是「付一次、查一次、邮件发结果」。它对应后端 AML 模块的 即时检测链接(ConsumerPortal)一套流程,与订阅三类完全不同。

+ +

5.1 前端当前实现(mock)

+
+
FE
submitSingle() :1279
固定 4 检测项(DEFAULT_SQ_OPS:证件核验 V / 名单筛查 E / AI 增强 A / 失信 D),functionCodes='VEAD'。整包价来自 GetPlanList 的 PAYG(P2G) 方案。
+
POST /single-queryPOST /payments/create → 跳转 mock 收银台
+
BFF · mock
plans-plus mock
/single-query 只回一个 mock-sq-* 订单号;/payments/create 回 mock 收银台 URL;/payments/:pid/webhook 模拟支付成功事件。不触发任何真实检测server/mock/plans-plus.js
+
+ +

5.2 后端真实流程(目标 / ConsumerPortal)

+
+
BE
CreateConsumerLink :964
按邮箱建 / 更 ConsumerCustomer,生成带 AccessTokenConsumerLink,把 FunctionCodes 存到链接上(该链接允许 / 预设的检测项),发链接邮件。
+
用户经链接提交主体
+
BE
CreateConsumerOrder :325
+
FunctionCodes 用计费表算价 → 建 ConsumerPortalOrderPaymentStatus=PaidOrderStatus=Pendding)→ 每个 functionCode 建一条 DetectionTaskBatchId=订单Id)→ 更新 ConsumerLink → 发 RabbitMQ 消息(订单 Id)。:398-411, :446
+
RabbitMQ 消费
+
MQ
RabbitMQConsumerService :186
消费订单 Id → 调 ProcessConsumerPortalOrder(orderId)
+
+
BE
ProcessConsumerPortalOrder :634
委托 DetectionService.RunDetectionModulesAsync(FunctionCodes=order.FunctionCodes, WaitForCompletion=true);全部模块完成后 Order=Success 并发结果邮件。
+
逐模块按 functionCode 门控
+
BE
RunDetectionModulesAsync DetectionService.cs:175
+
每个模块 functionCodes.Contains(...) 才执行:含 E/A → ES/AI 筛查;含 V → 证件核验;含 D → 失信查询;含 O → OCR;含 S → 风评;含 R → CDD 报告。未包含的 code 对应模块直接跳过:230, :281, :363, :435, :448
+
+
「只执行对应项目」成立 后端严格按订单 functionCodes 建任务并逐模块门控,计费也基于同一批 code。因此前端展示的检测项目应与真正传入的 functionCodes 同源,否则会「展示了却不跑 / 跑了却没展示」。FunctionCodeEnums:E=ES、A=AI、O=OCR、V=证件核验、D=失信、S=风评、R=CDD 报告。Basic/Enums.cs:2365
+
+ +
+

6 · 支付回调 → 开通(订阅侧通用时序)

+

订阅三类(new/renew/topup)支付成功后的开通链路一致,关键在于「回调只置订单已付 + 入队;真正建 / 续租户由后台 Job 异步完成」。这样解耦是为了幂等与失败重试。

+
+
回调
PaymentWebhook OrderController / OrderService:1160
lock 串行化;调 OnPaymentSuccess。mock 支付时由 MockupPayment 内联触发同一方法。
+
+
同步
OnPaymentSuccess :1174
写 PaymentInfo(幂等:已存在则 return)→ Order 置 Paid → InsertTenantEventQueue(Create/Pending)。
+
请求到此返回;开通由 Job 异步完成
+
队列
TenantEventQueue
一条 OrderId + EventType=Create + Status=Pending
+
周期 IntervalSeconds
+
JOB
TenantEventQueueJob → ProcessTenantEventQueue
OrderType 走 NewReg / 续费分支,建 / 续租户、开 feature、写 TenantProperty、发邮件、订单置 Completed。失败重试 RetryTimes 次后 FailedBasic/Jobs/TenantEventQueueJob.cs
+
+

6.1 详细时序图(回调 ↔ 落库 ↔ 队列 ↔ Job)

+

纵向泳道从左到右:支付方 / 后端入口 / 服务方法 / 数据库 / 后台 Job / SaaS 建租户 / 邮件。上半部(A)是同步、发生在 HTTP 请求内;虚线是异步边界——请求返回时租户尚未创建;下半部(B)是后台周期 Job 稍后完成的真正开通。活性条表示该泳道在此期间处于活动状态。

+
+ + + + + + + + + + + + + + + + + + + + + 支付方 / Mock + PaymentWebhook + OnPaymentSuccess + DBOrder·PaymentInfo·Queue + TenantEventQueueJob + CreateTenant · SaaS + 邮件 + + + A · 同步(请求内) + B · 异步(周期 Job) + + + + + + + + + + + + + + + + + + + ① PaymentWebhook (status=1) + ② OnPaymentSuccess · lock + ③ 查 Order (OrderCode) + ④ 写 PaymentInfo(幂等) + ⑤ Order = Paid + ⑥ 入队 Create / Pending + ⑦ 200 OK(未建租户) + ⑧ 扫描队列 + 载入 Order + ⑨ CreateTenant + ⑩ tenantId + ⑪ EnableFeatures(edition) + ⑫ 建 TenantProperty · Completed + ⑬ 通知邮件 买家 + agent + + + + ═ 异步边界 · HTTP 请求已返回(租户尚未创建);以下由后台 Job 稍后执行 ═ + +
+

A · 同步(HTTP 请求内)

+
    +
  1. 支付方回调 POST /api/amlPortal/Order/portal/PaymentWebhookstatus=1、带 syssn);入口用 lock 串行化。mock 支付时由 MockupPayment 用本地 mock 数据内联触发同一入口,等价于「立即支付成功」。OrderService.cs:1160 / 2916
  2. +
  3. PaymentWebhookOnPaymentSuccess(param):1174
  4. +
  5. out_trade_no = OrderCode 查订单(禁多租户过滤 + 允许软删)。:1181
  6. +
  7. PaymentInfo(含 InvoiceNo);幂等点:若该订单已有 PaymentInfo → 直接 return,不重复处理(webhook 可能重复投递)。:1197-1217
  8. +
  9. Order.OrderStatus=PaidPaymentStatus=Paid、记 PaidAmount/PaymentTime(曾软删则恢复)。:1225-1241
  10. +
  11. InsertTenantEventQueue(order) 插入 EventType=Create / Status=Pending(已存在则不重复插)。:1249 / 1371
  12. +
  13. PaymentWebhook 返回 200。此刻租户 / 续期尚未生效 —— 真正开通在后台 Job 完成。
  14. +
+

B · 异步(TenantEventQueueJobIntervalSeconds 跑一次)

+
    +
  1. ProcessTenantEventQueue 扫描 Pending/Retry 队列,载入 Order(含 OrderDetails / PlanDetail)与 editionRetry 项按 RetryMinutes × TryTimes 退避。:528-556, 542
  2. +
  3. NewReg 分支:同名租户已存在 → 只补发邮件并置 Success(幂等);否则 CreateTenant(name, editionId, adminEmail, adminPassword)(ABP SaaS 建租户 + admin)。:570-627
  4. +
  5. CreateTenant 返回 tenantId:630
  6. +
  7. 关闭 AMLPortal.Enable,按 edition → feature 映射启用 / 禁用业务功能(行业决定可用模块)。:632-661
  8. +
  9. TenantProperty(期限叠加、JCountLeft/QCountLeft 累加、UserCountLimitEnableKYC)→ 写 TenantInfoOrder=Completed、队列 Success、清缓存。:677-801, 964-967
  10. +
  11. 发通知邮件:买家(含改密链接)+ 逐级 agent。失败 → 队列 RetryTryTimes+1),超 RetryTimesFailed:803-844, 972-986
  12. +
+
续费 / 加购分支(⑧ 之后) 不建租户:作废旧 IsActive 的 TenantProperty,新建一条(额度 / 期限 / 用户在旧值上叠加),启用 edition feature,发续费邮件,Order=CompletedOrderService.cs:848-989
+ +

6.2 幂等 · 重试 · 清理 · mock 支付

+ + + + + + + + + +
关注点机制位置
支付幂等OnPaymentSuccess 若已写过 PaymentInfo 直接 return;webhook 可重复投递OrderService.cs:1213-1217
开通幂等Job 内若同名租户已存在,只补发邮件并置 Success,不重复建租户OrderService.cs:578-615
失败重试队列 Retry + TryTimes + 退避(RetryMinutes×TryTimes),超 RetryTimes 置 FailedOrderService.cs:542, 831-843
未支付清理ClearExpiredOrderCustomer:超 ExpireOrderMins 未支付的订单,NewReg 删客户信息OrderService.cs:1271
mock 支付IsMockupPayment=true 时 CreateOrder / TenantRenewal 内联触发 OnPaymentSuccess(无需真实网关)OrderService.cs:2916
+
+ +
+

7 · 状态机

+

订阅订单 Order.OrderStatus

+
+ PenddingPaid──付款成功──▶ + Paid──Job 建/续租户──▶ + Completed +
+
+ 旁路:PlanPrice≤0 → PendingActive(点激活邮件) + 线下 → PenddingAudit(管理员审核) + 超时未付 → Expired / 清理 +
+

租户事件队列 TenantEventQueue.Status

+
+ Pending─▶Success + Retry(× TryTimes)─▶Failed +
+

即用即付订单 ConsumerPortalOrder.OrderStatus

+
+ Pendding──RabbitMQ→检测编排──▶Success + 异常 ▶Failed +
+

注:即用即付订单建单即 PaymentStatus=Paid(当前实现下支付被前置 / 简化),真正的耗时在检测编排;订阅订单则是「先建单未付 → 付款 → 异步开通」。

+
+ +
+

8 · 关键细节 · 边界 · 幂等

+ +
+ +
+

9 · 当前原型的真实运行状态(重要)

+
在线支付未开通 · plans-plus 当前实际不会走上面的「支付 → 开通」全链路 + 前端 ONLINE_PAYMENT_ENABLED = false plans-plus.js:13:「Confirm & Subscribe」按钮恒置灰,ppSubmit() 一进来就 return :1342。用户实际只能点「联络我们 / 联络推荐人」。 +
+ + + + + + + +
能力设计 / 目标链路当前原型实际
订阅下单(new/renew/topup)/subscribeCreateOrder/TenantRenewal → 支付 → 开通走线下ppContactSubmitPOST /subscribe-offline → 后端 customer/CreateFeedback(只发线索,不建订单)routes/subscribe.js
即用即付(single)CreateConsumerLinkCreateConsumerOrder → RabbitMQ → 检测纯 mock/single-query + /payments/* 只回模拟数据,不触发真实检测server/mock/plans-plus.js
真实订阅路由editions / plans / agents / tenants / subscribe 已对接 AMLPortal已实现,ONLINE_PAYMENT_ENABLED=true 后即可启用在线下单
+
要打通到真实后端时 + ① 前端置 ONLINE_PAYMENT_ENABLED=true;② 订阅链路已就绪(subscribe.js → CreateOrder/TenantRenewal),后端配 IsMockupPayment 或接真实支付;③ 即用即付需把 /single-query 从 mock 换成真正的 ConsumerPortal.CreateConsumerLink/CreateConsumerOrder,并保证前端展示的检测项与传入 functionCodes 同源。 +
+
+ +
+

本流程单基于源码撰写,仅描述订单 / 支付 / 开通 / 检测编排主链路;邮件模板、发票生成、agent 组织树、额度预警等旁路仅在需要处点到为止。若后端代码调整,请以最新 OrderService.cs / ConsumerPortalService.cs / DetectionService.cs 为准。

+ +
+ +