Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

项目支付系统设计与接入

如果你已经看完前面几篇,会发现 oak-pay-business 并不是“拿来就能直接付款”的一个页面组件包,而是一套完整的支付域基础设施。真正做项目时,最重要的问题也不是“怎么拉起微信支付”,而是:

  • 支付系统应该怎么分层;
  • 哪些能力应该沉到公共支付域里;
  • 哪些规则应该留在项目自己的 trigger / aspect 里;
  • 一个新项目最小应该接哪些东西,才不至于只把支付页面做出来,却把状态机、回调和补偿链路丢了。

这一篇就用 taicang 作为已经跑通的参考项目,把“项目支付系统应该怎么设计”完整梳理一遍。

先说结论

一个 Oak 项目的支付系统,最稳的设计不是“自己写一套 payment service”,而是分成下面四层:

1. 支付域公共底座

这层由 oak-pay-business 提供,负责:

  • 支付、退款、充值、提现、物流、系统资金、结算这些通用实体;
  • checker、trigger、watcher、timer 组成的资金状态机;
  • 支付渠道抽象 payClazz
  • 支付回调处理;
  • 前端 pay feature;
  • 后台配置组件、支付详情组件、账户与流水组件。

2. 项目支付接入层

这层在项目里负责:

  • oak-pay-businesscheckers / triggers / watchers / timers / aspects / features 合并进来;
  • make:dep 生成的 Context 组合支付域 module,项目自己的 RuntimeContext 继承生成的 Context;
  • 暴露项目自己的支付回调 endpoint;
  • 按需要注册新的支付渠道、前端拉起流程和后台配置页。

3. 业务支付编排层

这层仍然在项目里,但职责不是“解支付协议”,而是:

  • 决定当前订单该拆成几笔 pay
  • 决定能不能余额支付、能不能押金抵扣、能不能部分支付;
  • 决定支付成功之后业务对象怎么变化。

4. 渠道协议层

这层才是各支付渠道自己的 SDK 交互,例如:

  • 微信预下单;
  • 微信回调解密;
  • 微信查单、关单、退款;
  • 其它支付机构的下单和状态查询。

这一层应该放进 payClazz,而不应该散在页面、controller 或项目自己的 service 里。

taicang 当前是怎么做的

taicang 基本就是这套分层的标准样板。

1. 依赖层直接接入 oak-pay-business

src/configuration/dependency.ts 直接声明了:

  • oak-general-business
  • oak-pay-business

这一步的意义不是“安装一个库”,而是把支付域对象和运行时能力纳入 Oak 依赖体系。

2. 初始化阶段合并支付域能力

taicang 在初始化时并没有自己重写支付流程,而是把公共支付域能力接入当前 Oak 运行时。需要区分当前模板和历史文件:

  • 新项目先在 src/configuration/dependency.ts 声明 oak-pay-business,再执行 project:initmake:domainmake:dep
  • 当前模板的前端运行时主入口是 src/initialize.ts -> src/initialize.server.ts
  • 应用启动后继续执行 src/initializeFeatures.ts,web 端可按需追加 src/initializeFeatures.web.ts
  • 老项目里如果还保留 src/initialize.frontend.ts,通常只是历史 DebugConnector 场景,不应再当作纯前台模式入口复制

这里合并的真实内容包括:

  • 前端运行时:checkers / common / render / features
  • 后端运行时:aspects / checkers / triggers / watchers / timers / data / ports / routines

同时,taicang 的前后端 RuntimeContext 都继承各自的 generatedBackend / generatedFrontend。生成文件从 oak-pay-business 原 RuntimeContext 入口取得具名 module,并按 oak-general-business -> oak-pay-business -> 项目 的依赖顺序构造最终 Context:

  • src/context/BackendRuntimeContext.ts
  • src/context/FrontendRuntimeContext.ts

这一步非常关键。项目不应再手工直继承支付库的 RuntimeContext,也不需要直接依赖 pay 已经传递依赖的 general Context;具体 layer/module 写法和冲突规则见上下文。很多项目失败就失败在这里只是“引了组件”,却没有把支付域运行时真的接进来。

3. 实体层复用公共支付模型

taicang 没有自己另起一套 paymentOrderpaymentRecordwallet 模型,而是直接扩展公共实体:

  • src/entities/Order.ts 继承 @oak-pay-business/entities/Order
  • src/entities/System.ts 继承 @oak-pay-business/entities/System
  • src/entities/Ship.ts 继承 @oak-pay-business/entities/Ship
  • src/entities/Supplier.ts 使用 AccountWithdrawAccount

也就是说,订单支付状态机、系统支付配置、账户与提现账户这些核心模型,在 taicang 里本质都沿用了公共支付域。

4. 项目只在业务差异点上扩展

taicang 真正自己补的,是那些明显不属于通用支付域的规则:

  • 号牌押金能否抵扣;
  • 号牌押金在支付成功后如何返还或消费;
  • 拍卖业务下订单完成后如何生成后续结算计划;
  • 小程序确认收货时如何结合号牌、物流和支付元数据继续推进业务。

这些逻辑主要落在:

  • src/utils/spPlate.ts
  • src/aspects/spPlate.ts
  • src/triggers/pay.ts
  • src/triggers/order.ts

这就是正确的边界。通用支付域负责支付本身,项目触发器负责“支付成功后这门生意该怎么走”。

oak-pay-business 真正负责什么

如果要正确设计项目支付系统,先要知道 oak-pay-business 已经帮你做了什么。

1. 它已经提供了完整支付主模型

核心对象至少包括:

  • Pay
  • Refund
  • Deposit
  • Account
  • Withdraw
  • WithdrawTransfer
  • System.payConfig
  • OfflineAccount
  • WpAccount
  • WpProduct
  • Settlement
  • SettlePlan

这些对象不是孤立存在的,而是已经通过 checker、trigger 和 watcher 形成了一条可运行的支付状态机。

2. 它已经提供了支付状态推进机制

项目真正发起支付时,正确路径不是“页面调用 SDK 成功后自己改状态”,而是:

  1. 创建 pay
  2. 执行 startPaying
  3. trigger 在 before 阶段调用渠道 prepay
  4. 回调或 watcher 再把 pay 推进到 paid / closed / refunding / refunded
  5. trigger 再把 orderdepositaccountsysAccount 往前推进

这套状态推进主要由这些文件负责:

  • src/checkers/order.ts
  • src/checkers/pay.ts
  • src/triggers/pay.ts
  • src/watchers/pay.ts
  • src/utils/pay.ts

3. 它已经提供了渠道抽象

真正和支付机构打交道的,不应该是页面,也不应该是项目 controller,而应该是 payClazz

oak-pay-business 当前默认已经内建:

  • account
  • offlineAccount
  • wpProduct
  • apProduct
  • epProduct
  • spProduct
  • cpProduct
  • wfProduct
  • ppProduct

并通过下面这些入口暴露扩展点:

  • registerPayClazz(...)
  • registerFrontendPayRoutine(...)
  • registerPayChannelComponent(...)

其中 ap/ep/sp/cp/wf/pp 的前端选择与跳转当前只在 Web 环境启用。新项目只有在接入这些内建合同之外的支付机构时,才需要扩展渠道层。

4. 它已经提供了后台和前台现成组件

最常直接复用的组件包括:

  • payConfig/system
  • order/pay
  • pay/detail
  • pay/list
  • refund/list
  • account/detail
  • deposit/new
  • withdraw/create
  • sysAccount/survey

因此一个新项目的正确思路,不是自己重搭管理后台,而是优先复用这些组件,再在项目层补壳。

一个项目的支付系统应该怎么分对象

这部分最容易设计错。下面是推荐的对象分法。

1. 订单对象只负责业务订单

订单对象应该表达的是:

  • 买了什么;
  • 应付多少钱;
  • 已付多少钱;
  • 已退多少钱;
  • 当前处于待支付、支付中、已支付还是退款中。

订单不应该自己塞一堆渠道协议字段,也不应该自己承担回调验签逻辑。

正确做法就是像 taicang/src/entities/Order.ts 一样,继承支付域 Order,然后只补业务字段。

2. 支付对象只负责一次资金动作

Pay 是一笔支付分录,不一定等于一个订单。

一个订单可能拆成多笔 pay

  • 一笔账户余额支付;
  • 一笔外部渠道支付;
  • 甚至多笔不同外部渠道支付。

因此项目不要把“订单”和“支付单”混成一个对象。真正的组合支付能力,就是靠订单下挂多笔 pay$order 实现的。

3. 系统对象统一承载支付策略

充值和提现手续费、系统可用渠道、系统资金账户这些内容,应该挂在 System,而不是挂在订单、用户或某个页面配置表里。

这一点 oak-pay-business/entities/System.ts 已经定好了,项目继续扩展这个对象即可。

4. 渠道对象统一承载支付机构配置

推荐分成两层:

  • 支付账号,例如 WpAccountOfflineAccount
  • 支付产品,例如 WpProduct

这样好处非常大:

  • 一个系统可以有多个支付产品;
  • 多个应用可以挂到不同支付产品;
  • 账号配置和产品配置分离;
  • 前端可以根据当前应用自动算出可用渠道。

一个项目的支付系统应该怎么分流程

1. 下单和选择支付方式

页面只负责收集支付方案,不直接触发第三方支付。

推荐做法是:

  • 订单页里挂一个支付方案组件;
  • 这个组件只回传 pay$order 创建数据;
  • 父组件再调用 order.startPaying

taicang 的前台订单详情页就是这样做的:

  • 自定义组件 src/components/pay/index.ts 负责拼 pay$order
  • 页面 src/pages/frontend/order/detail/index.ts 负责执行 startPaying

2. 真正拉起支付

真正外部支付不应在订单页直接完成,而应该切到 pay/detail 或复用它的内部能力。

因为支付详情页已经内建:

  • 渠道可支付性判断;
  • 小程序 wx.requestPayment(...)
  • 微信网页 chooseWXPay
  • 线下支付展示;
  • 支付失败后的关闭或回退策略。

taicang 就是在 createOrderPay() 后跳到 /pay/detail 来完成外部支付。

3. 回调和补偿

异步通知应该只做一件事:把请求交给支付域公共处理逻辑。

taicang 的做法非常标准:

  • src/endpoints/wechatPay.ts 里暴露项目 endpoint
  • 内部直接调用 @oak-pay-business/utils/paypayNotify / refundNotify

这一步不要自己重写支付状态推进,否则后面的 watcher 和 trigger 会越来越难协调。

4. 支付成功后的业务动作

支付成功本身只是资金域事件,不应该直接写死在公共包里。

项目应该在自己的 triggers/pay.tstriggers/order.ts 里处理:

  • 这笔钱对应哪个业务对象;
  • 押金怎么消费或返还;
  • 订单之后如何结算;
  • 是否要创建物流或确认收货流程。

taicang 正是把这部分放在项目 trigger 里,而不是改 oak-pay-business 本体。

新项目最小落地清单

这是最值得直接照抄的部分。一个新项目要接 oak-pay-business,最少应该做下面这些事。

1. 依赖与实体

  • dependency.ts 里加入 oak-pay-business
  • 让项目的 System 继承支付域 System
  • 让项目的 Order 继承支付域 Order
  • 如果有账户或提现需求,接入 AccountWithdrawAccount

2. 初始化与上下文

  • dependency.ts 里声明 oak-pay-business,执行 project:initmake:domainmake:dep
  • 创建并注入 pay feature,当前模板由生成的 initialize.server.ts 处理
  • 调用 initializeOpb1Features(...)
  • 让前后端 RuntimeContext 继承 make:dep 生成的 Context;由 pay 的 module 递归带入 general Context

3. 系统配置后台

  • 直接挂 payConfig/system
  • 让运营可以配置:
    • System.payConfig
    • OfflineAccount
    • WpAccount
    • WpProduct
  • 如果需要,再挂 sysAccount/survey

4. 订单支付前台

  • 准备一个订单支付方案组件
  • 由它生成 pay$order
  • 父组件执行 order.startPaying
  • 外部支付统一进入 pay/detail

5. 支付回调

  • 项目里暴露自己的 endpoint 路由
  • 内部复用 oak-pay-business/utils/pay
  • 至少接上:
    • payNotify
    • refundNotify

6. 业务 trigger

  • 把支付成功后的业务差异逻辑写在项目自己的 trigger 里
  • 不要直接修改公共支付域的主状态机

什么时候应该扩展 oak-pay-business

并不是每个项目都要去注册新渠道。

不需要扩展的场景

如果项目只需要:

  • 账户余额支付;
  • 线下收款码或银行转账;
  • 微信支付;
  • 标准充值、退款、提现;

那么大多数情况下:

  • 公共实体够用;
  • 公共 payClazz 够用;
  • 公共前端支付 routine 也够用。

这时项目只需要做接入和业务 trigger,不需要扩展渠道层。

需要扩展的场景

如果项目要接:

  • 新支付机构;
  • 新的支付产品实体;
  • 新的后台支付配置页;
  • 新的前端拉起支付流程;
  • 新的系统资金账户展示;

才需要用这些扩展点:

  • registerPayClazz(...)
  • registerFrontendPayRoutine(...)
  • registerPayChannelComponent(...)
  • registerSysAccountCardTopComponent(...)
  • registerSysAccountDetailComponent(...)

推荐的接入顺序

实践里,最稳的顺序通常是:

  1. 先接实体和初始化
  2. 再接系统支付配置后台
  3. 再接订单支付前台
  4. 再接支付回调
  5. 最后补业务 trigger 和补偿规则

不要一上来先写页面。只把页面做出来,是最容易形成“能点支付但状态不对、回调不进、资金不平”的假接入。

taicang 最值得复用的经验

最后把最值得照抄的经验直接列出来。

1. 公共支付域和业务规则边界划得很清楚

  • 公共支付域负责支付本身;
  • 项目 trigger 负责拍卖押金和订单结算。

2. 页面不直接持有支付协议

  • 页面只创建 pay$order
  • 真正支付在 pay/detail 里拉起
  • 回调在 endpoint 里统一处理

3. 项目扩展点只落在必要位置

  • 需要自定义前台支付交互时,包一层本地组件
  • 需要业务差异时,写本地 aspect / trigger
  • 不去改公共支付域主链路

4. 小程序特殊规则不污染主状态机

像“确认收货后到账”这种微信小程序特性,在支付域里通过 needReceivingship 和前端确认收货流程承接,而不是把订单和支付状态机写乱。

最后再强调一次

一个 Oak 项目的支付系统,正确目标不是“把支付接口调通”,而是把下面这五件事一起接完整:

  • 资金对象模型
  • 状态推进机制
  • 支付渠道抽象
  • 回调与补偿
  • 项目自己的业务后处理

taicang 已经证明,这套方式是能跑通复杂业务的。新项目最不应该做的,就是绕开 oak-pay-business 重新做一套平行支付系统。那样前期看似快,后期几乎一定会在退款、补偿、回调、对账和系统资金上吃大亏。