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 路由后端接口后端服务方法订单枚举支付后动作
新增租户new POST /api/subscribe
routes/subscribe.js
/api/amlPortal/Order/portal/CreateOrder OrderService.CreateOrder
OrderService.cs:146
OrderType=NewReg 新建 ABP 租户 + 按 edition 开关 feature + 建 TenantProperty
续费renew POST /api/subscribe /api/amlPortal/Order/portal/TenantRenewal OrderService.TenantRenewal
OrderService.cs:2199
OrderType=Renewal 作废旧 TenantProperty,建新的(期限往后叠加、额度累加)
增值服务topup POST /api/subscribe /api/amlPortal/Order/portal/TenantRenewal OrderService.TenantRenewal(PlanList 只含加值项) OrderType=Renewal 同续费分支;但无基础方案 → 只叠加额度 / 用户数 / KYC,不延长期限
即用即付single POST /api/single-query
当前 mock
目标:/api/aml/ConsumerPortal/CreateConsumerLinkCreateConsumerOrder ConsumerPortalService
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. PaymentWebhookOnPaymentSuccess(param):1174
  3. out_trade_no = OrderCode 查订单(禁多租户过滤 + 允许软删)。:1181
  4. PaymentInfo(含 InvoiceNo);幂等点:若该订单已有 PaymentInfo → 直接 return,不重复处理(webhook 可能重复投递)。:1197-1217
  5. Order.OrderStatus=PaidPaymentStatus=Paid、记 PaidAmount/PaymentTime(曾软删则恢复)。:1225-1241
  6. InsertTenantEventQueue(order) 插入 EventType=Create / Status=Pending(已存在则不重复插)。:1249 / 1371
  7. PaymentWebhook 返回 200。此刻租户 / 续期尚未生效 —— 真正开通在后台 Job 完成。

B · 异步(TenantEventQueueJobIntervalSeconds 跑一次)

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