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 支付业务逻辑

oak-pay-business 是 Oak 生态里的支付域公共业务包。它不是“微信支付 SDK 的简单封装”,也不是“只有订单和支付两个对象”的薄层模块,而是一套已经把订单、支付、退款、充值、提现、物流、系统资金、结算计划这些能力拆成对象、checker、trigger、aspect、endpoint、feature、组件和后台补偿任务的完整支付底座。

如果你前面已经读过 oak-general-business,那么可以把 oak-pay-business 理解成它在支付域上的继续延伸:

  • oak-general-business 负责应用、用户、token、微信、小程序、文件、消息这些通用基础能力;
  • oak-pay-business 则在这套基础之上继续补上资金、支付渠道、充值退款、提现拆单、物流同步和结算。

近期 oak-pay-business 已适配新的 oak-domain 发布包实体解析,并和 oak-general-business 的 general-system 翻译能力对齐。支付域里的 System 覆盖必须继续兼容通用业务包的翻译字段、翻译状态和翻译动作。

这一章为什么不按实体名拆

第一次打开 oak-pay-business/src/entities,很容易被这些对象绕晕:

  • OrderPayRefund 看起来是一组;
  • AccountDepositSysAccountOper 又像另一组;
  • WithdrawWithdrawTransferWithdrawAccount 还会和退款串起来;
  • ShipShipServiceSystemWechatMpShip 明明是物流,却又会反过来影响充值到账;
  • SettlePlanSettlement 又会继续影响订单的 settledsettlePlanned

所以这一章不再按“一个实体一章”的方式写,而是按真实开发时更容易理解的能力域拆成:

  • 系统支付配置与系统资金
  • 订单、支付与退款
  • 账户、充值与流水
  • 提现
  • 物流
  • 渠道、组件注入与扩展点
  • 结算

这种拆法更接近项目实际接入和排查问题时的思路。

先看能力入口,再看具体对象

真正接入 oak-pay-business 时,最重要的入口一共有五类。

1. feature

src/features/index.ts 只创建了一个前端 feature:

  • pay

它内部真实实现位于 src/features/Pay.ts,目前暴露三个方法:

  • getPayChannels(type?: 'deposit' | 'pay', accountId?: string)
  • calcDepositLoss(price, channel)
  • getDepositRatio(channel),当前源码里仍然直接抛错,尚未实现

另外,@oak-pay-business/featuresinitialize(...) 会先调用 @oak-general-business/featuresinitialize(...),也就是说支付域初始化本身就依赖通用业务包的应用识别、登录态、微信环境等基础能力。

因此同时依赖通用业务包和支付业务包的项目,通常只需要调用 initializeOpb1Features(...)。不要再手工重复调用一遍 initializeOgb0Features(...),除非你明确知道初始化顺序和投影合并边界。

2. aspect

src/aspects/index.ts 当前只导出四个 aspect:

  • getWithdrawCreateData
  • getMpShipState
  • getExpressPrintInfo
  • shipConfirmSuccess

这四个入口都是真实在前端组件里被调用的:

  • 提现创建页会先调 getWithdrawCreateData
  • 小程序收货确认会调 shipConfirmSuccess
  • 物流详情和面单打印会调 getMpShipStategetExpressPrintInfo

3. endpoint

支付域当前提供了多组可选回调模块:

  • wechatPay.ts:支付与退款回调;
  • aliPay.ts:支付与退款回调;
  • epay.ts:支付回调;
  • stripePay.ts:支付与退款回调;
  • creemPay.ts:支付回调;
  • waffoPay.ts:支付与退款回调;
  • paypalPay.ts:支付与退款回调。

src/endpoints/index.ts 当前默认导出空对象,并明确要求项目按需注入。框架会自动加载依赖包的 endpoint 索引,不等于这些可选渠道模块会自动启用;项目必须在自己的 src/endpoints/index.ts 中显式选择所需模块。

4. registry 与注入点

支付域最重要的项目级扩展点不在 feature 里,而在 registry.backend.ts / registry.frontend.ts

src/registry.backend.ts 只暴露:

  • registerPayClazz

src/registry.frontend.ts 则只暴露前端可用的这几项:

  • registerPayChannelComponent
  • registerFrontendPayRoutine
  • registerShipSettingComponent
  • registerSysAccountCardTopComponent
  • registerSysAccountDetailComponent

这意味着:

  • 新支付渠道的“后端实现”通过 registerPayClazz(...) 注入;
  • 新支付渠道的“系统配置 UI”通过 registerPayChannelComponent(...) 注入;
  • 新支付渠道的“前端唤起流程”通过 registerFrontendPayRoutine(...) 注入;
  • 新物流系统的“系统设置页”通过 registerShipSettingComponent(...) 注入;
  • 系统资金总览页顶部卡片和详情页也都可以继续扩展。

5. watcher 与 timer

除了实体动作本身,支付域还有一套后台补偿逻辑:

  • watchers/order.ts 会把过期订单自动置为 timeout
  • watchers/pay.ts 会轮询外部支付状态,并在超时后自动关闭 pay
  • watchers/refund.ts 会轮询外部退款状态
  • watchers/settlePlan.ts 会在到达结算时间后自动执行 settle
  • timers/ship.ts 会定时同步快递状态和小程序虚拟/自提发货状态

相反,src/routines/start.ts 当前没有真正启用的启动例程,支付域主要依靠 trigger、watcher、timer 驱动。

先建立组件地图

第一次接 oak-pay-business,最省时间的做法不是先把所有实体看完,而是先知道“这条支付链应该从哪组组件进”。下面这张地图最适合新手先建立整体感。

1. 系统配置与系统资金

这一组通常先看:

  • payConfig/system
  • offlineAccount/config
  • wpAccount/config
  • wpProduct/config
  • apAccount/config
  • apProduct/config
  • sysAccount/survey
  • sysAccountOper/list
  • sysAccount/transferList
  • sysAccountMove/create

这组组件解决的不是“支付过程”,而是:

  • 系统层有哪些支付渠道
  • 每个渠道怎么配置
  • 系统账户余额和流水怎么看
  • 提现打款任务在哪里处理

也就是说,后台管理台通常先从这一组落。

2. 订单支付与退款主线

这一组最常直接复用的是:

  • order/pay
  • order/list
  • pay/detail
  • pay/list
  • pay/channelPicker2
  • refund/list

如果项目里已经有订单详情页,最常见的组合就是:

  • 订单页里弹出 order/pay
  • 创建完支付单后切到 pay/detail
  • 后台管理页再补 pay/listrefund/list

这样“发起支付”和“排查支付”两条线就都有现成入口。

3. 账户、充值与流水

这组组件通常先看:

  • account/detail
  • deposit/new
  • accountOper/list

真实使用时,它们的职责边界很清楚:

  • account/detail 更像一个完整账户主页,内部已经串了充值和未完成支付跳转
  • deposit/new 更像受控的“充值参数录入器”
  • accountOper/list 负责账户流水

如果项目要做钱包页、保证金页、账户历史页,通常先从这一组搭。

4. 提现与打款

提现链路最值得先看的组件是:

  • withdraw/create
  • withdraw/detail
  • withdraw/display
  • withdraw/list
  • withdrawAccount/list
  • withdrawAccount/upsert
  • withdrawTransfer/list

这一组组件加在一起,才构成完整提现链:

  • 创建申请
  • 选择提现账户
  • 查看拆单详情
  • 查看历史
  • 运营处理打款

如果只接 withdraw/create 一个表单,后面排查“为什么部分成功”会非常难受。

5. 物流与收货确认

物流这组最关键的是:

  • ship/system
  • ship/wechatMpShip
  • shipServiceSystem/list

它们更偏后台配置,而不是前台纯展示组件。真正要理解的重点是:

  • 哪个系统启用了哪些物流服务
  • 微信小程序发货配置有没有挂上
  • 后端有没有注册真实 shipClazz

所以新项目接物流时,通常先搭后台配置页,再考虑订单或充值页怎么消费物流状态。

6. 哪些能力本来就是项目层自己包

oak-general-business 一样,这里也不是“每条链都有完整页面成品”。

例如:

  • 结算 settlePlan / settlement 当前没有公共成品组件
  • 新支付渠道的回调 endpoint 常常是项目层自己包一层路由,再复用公共 utils/pay

所以阅读支付域源码时,最合理的预期应该是:

  • 公共包把主模型、状态机、公共组件和注入点准备好
  • 项目层再按自己的业务路由和页面壳把它们串起来

最小接入思路

第一次把 oak-pay-business 接进项目时,建议按下面的顺序来。

1. 先声明依赖并生成装配代码

当前推荐先在 src/configuration/dependency.ts 中声明 oak-pay-business,再执行 project:initmake:domainmake:dep。生成后的 initialize.server.ts 会创建支付域 feature;server:start 启动的 AppLoader 会按依赖图自动从项目和依赖包 lib/... 合并支付域的 checker、trigger、watcher、timer、aspect、endpoint、data、port、routine。

支付包的 src/endpoints/index.ts 默认为空。项目应在自己的 src/endpoints/index.ts 中显式导入并组织需要的渠道 endpoint,例如:

import * as wechatPay from '@oak-pay-business/endpoints/wechatPay';

export default {
    wechatPay: [wechatPay.payNotify, wechatPay.refundNotify],
};

这一步只是选择并挂载公共回调合同,不是重写支付状态推进。其它渠道按实际启用情况分别导入,避免把未配置密钥或路由的回调全部暴露出去。

2. 创建并初始化支付 feature

前端初始化时,真实入口就是 src/features/index.ts

import { create as createPayFeatures, initialize as initializePayFeatures } from '@oak-pay-business/features';

const opbFeatures = createPayFeatures(totalFeatures);
Object.assign(totalFeatures, opbFeatures);

await initializePayFeatures(totalFeatures, accessConfiguration, {
  applicationExtraProjection: {
    system: {
      id: 1,
      payConfig: 1,
      offlineAccount$system: {
        $entity: 'offlineAccount',
        data: {
          id: 1,
          type: 1,
          allowDeposit: 1,
          allowPay: 1,
        },
      },
    },
    wpProduct$application: {
      $entity: 'wpProduct',
      data: {
        id: 1,
        type: 1,
        enabled: 1,
      },
    },
  },
});

这里最关键的不是“把 pay feature 建出来”,而是要确保当前应用投影里真的带上了支付域会读到的对象,例如 system.payConfigofflineAccount$systemwpProduct$application

3. 再决定是否注册项目层扩展

如果项目只用当前内建的 accountofflineAccountwpProductapProductepProductspProductcpProductwfProductppProduct 渠道,可以直接开始用;Web redirect 类产品仍受平台限制。

如果项目要继续扩展:

  • 新支付产品,注册 registerPayClazz(...)
  • 新支付配置页,注册 registerPayChannelComponent(...)
  • 新支付唤起流程,注册 registerFrontendPayRoutine(...)
  • 新物流系统配置页,注册 registerShipSettingComponent(...)

参考项目里的真实接法

如果你对接入方式还有点抽象,haina-busitaicang 这两个项目基本已经把 oak-pay-business 的常见接法都跑过一遍了。

1. 初始化通常只调用 initializeOpb1Features(...)

当前项目通常会在前端运行时初始化链路里同时创建:

  • createOgb0Features(...)
  • createOpb1Features(...)

但真正启动时,很多地方只调用:

await initializeOpb1Features(features, accessConfiguration, config, cosClazzes);

这是因为 oak-pay-business/src/features/index.tsinitialize(...) 内部已经先调用了 oak-general-business 的初始化。也就是说,支付域初始化本身就把应用识别、登录态、微信环境、文件上传这些基础能力一起带上了。

2. 支付域的项目扩展一般写在 routines 和初始化文件里

从这两个项目的真实写法看,最常见的扩展点分布是:

  • initializeFeatures.web.ts:注册前端支付配置组件,例如 registerPayChannelComponent('wpAccount', WpAccountConfig)
  • routines/pay.ts:注册后端支付类,例如 registerPayClazz('cmbProduct', ...)
  • routines/start.ts:注册短信、COS、消息类型,以及项目自己的通知处理器
  • 页面层:直接包 payConfig/systemorder/paypay/detailaccount/detailwithdraw/createship/system

2.1 支付回调经常是“项目路由壳 + 公共处理逻辑”

这里有一个很值得新手提前建立的认知:

  • oak-pay-business 已提供微信、支付宝、Epay、Stripe、Creem、Waffo、PayPal 等可选 endpoint 模块;
  • 项目层仍可以按同样模式补自己的 cmbPay.ts,或为公共模块包一层不同 HTTP 契约。

haina-busi 就是这么做的。它自己的 src/endpoints 里补了:

  • wechatPay.ts
  • aliPay.ts
  • cmbPay.ts

但内部并没有重写整套状态推进,而是继续复用公共的:

  • @oak-pay-business/utils/pay.payNotify
  • @oak-pay-business/utils/pay.refundNotify

所以项目层如果扩新渠道,最稳的做法通常不是“完全自己写 controller”,而是:

  • 项目里补自己的 endpoint 路由名和参数结构
  • 真正的回调处理继续复用公共支付逻辑

3. 实体层通常不是“重写一套支付模型”,而是扩展公共模型

haina-busiCmbAccountCmbProduct 是典型例子:

  • CmbAccount 继承 AbstractPayAccount
  • CmbProduct 继承 AbstractPayProduct

taicang 则更偏向“在公共支付实体上继续补业务字段”:

  • System 继承 @oak-pay-business/entities/System
  • Order 继承 @oak-pay-business/entities/Order
  • Ship 继承 @oak-pay-business/entities/Ship

这两种方式都说明:项目层最好是在公共支付模型上扩展,而不是脱离公共模型重新做一套资金域。

阅读源码时建议按这个顺序

第一次系统阅读 oak-pay-business,最推荐的顺序是:

  1. src/features/index.tssrc/features/Pay.ts
  2. src/registry.backend.tssrc/registry.frontend.ts
  3. src/aspects/index.ts
  4. src/entities/*.ts
  5. src/checkers
  6. src/triggers
  7. src/watcherssrc/timers
  8. 最后再看 src/components

原因和 oak-general-business 一样:新手最容易先被组件数量吸走注意力,但真正决定支付域能力边界的,还是对象定义、状态机、trigger、checker 和注入点。

接下来的各章,我都会明确写出:

  • 这一块解决什么问题;
  • 对应哪些实体;
  • 已经有哪些组件、aspect、endpoint、feature 可以直接复用;
  • 后台有哪些 checker、trigger、watcher、timer 在兜底;
  • 项目层该在哪里接入,以及应该写成什么样子。