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,很容易被这些对象绕晕:
Order、Pay、Refund看起来是一组;Account、Deposit、SysAccountOper又像另一组;Withdraw、WithdrawTransfer、WithdrawAccount还会和退款串起来;Ship、ShipServiceSystem、WechatMpShip明明是物流,却又会反过来影响充值到账;SettlePlan、Settlement又会继续影响订单的settled、settlePlanned。
所以这一章不再按“一个实体一章”的方式写,而是按真实开发时更容易理解的能力域拆成:
系统支付配置与系统资金订单、支付与退款账户、充值与流水提现物流渠道、组件注入与扩展点结算
这种拆法更接近项目实际接入和排查问题时的思路。
先看能力入口,再看具体对象
真正接入 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/features 的 initialize(...) 会先调用 @oak-general-business/features 的 initialize(...),也就是说支付域初始化本身就依赖通用业务包的应用识别、登录态、微信环境等基础能力。
因此同时依赖通用业务包和支付业务包的项目,通常只需要调用 initializeOpb1Features(...)。不要再手工重复调用一遍 initializeOgb0Features(...),除非你明确知道初始化顺序和投影合并边界。
2. aspect
src/aspects/index.ts 当前只导出四个 aspect:
getWithdrawCreateDatagetMpShipStategetExpressPrintInfoshipConfirmSuccess
这四个入口都是真实在前端组件里被调用的:
- 提现创建页会先调
getWithdrawCreateData - 小程序收货确认会调
shipConfirmSuccess - 物流详情和面单打印会调
getMpShipState、getExpressPrintInfo
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 则只暴露前端可用的这几项:
registerPayChannelComponentregisterFrontendPayRoutineregisterShipSettingComponentregisterSysAccountCardTopComponentregisterSysAccountDetailComponent
这意味着:
- 新支付渠道的“后端实现”通过
registerPayClazz(...)注入; - 新支付渠道的“系统配置 UI”通过
registerPayChannelComponent(...)注入; - 新支付渠道的“前端唤起流程”通过
registerFrontendPayRoutine(...)注入; - 新物流系统的“系统设置页”通过
registerShipSettingComponent(...)注入; - 系统资金总览页顶部卡片和详情页也都可以继续扩展。
5. watcher 与 timer
除了实体动作本身,支付域还有一套后台补偿逻辑:
watchers/order.ts会把过期订单自动置为timeoutwatchers/pay.ts会轮询外部支付状态,并在超时后自动关闭paywatchers/refund.ts会轮询外部退款状态watchers/settlePlan.ts会在到达结算时间后自动执行settletimers/ship.ts会定时同步快递状态和小程序虚拟/自提发货状态
相反,src/routines/start.ts 当前没有真正启用的启动例程,支付域主要依靠 trigger、watcher、timer 驱动。
先建立组件地图
第一次接 oak-pay-business,最省时间的做法不是先把所有实体看完,而是先知道“这条支付链应该从哪组组件进”。下面这张地图最适合新手先建立整体感。
1. 系统配置与系统资金
这一组通常先看:
payConfig/systemofflineAccount/configwpAccount/configwpProduct/configapAccount/configapProduct/configsysAccount/surveysysAccountOper/listsysAccount/transferListsysAccountMove/create
这组组件解决的不是“支付过程”,而是:
- 系统层有哪些支付渠道
- 每个渠道怎么配置
- 系统账户余额和流水怎么看
- 提现打款任务在哪里处理
也就是说,后台管理台通常先从这一组落。
2. 订单支付与退款主线
这一组最常直接复用的是:
order/payorder/listpay/detailpay/listpay/channelPicker2refund/list
如果项目里已经有订单详情页,最常见的组合就是:
- 订单页里弹出
order/pay - 创建完支付单后切到
pay/detail - 后台管理页再补
pay/list、refund/list
这样“发起支付”和“排查支付”两条线就都有现成入口。
3. 账户、充值与流水
这组组件通常先看:
account/detaildeposit/newaccountOper/list
真实使用时,它们的职责边界很清楚:
account/detail更像一个完整账户主页,内部已经串了充值和未完成支付跳转deposit/new更像受控的“充值参数录入器”accountOper/list负责账户流水
如果项目要做钱包页、保证金页、账户历史页,通常先从这一组搭。
4. 提现与打款
提现链路最值得先看的组件是:
withdraw/createwithdraw/detailwithdraw/displaywithdraw/listwithdrawAccount/listwithdrawAccount/upsertwithdrawTransfer/list
这一组组件加在一起,才构成完整提现链:
- 创建申请
- 选择提现账户
- 查看拆单详情
- 查看历史
- 运营处理打款
如果只接 withdraw/create 一个表单,后面排查“为什么部分成功”会非常难受。
5. 物流与收货确认
物流这组最关键的是:
ship/systemship/wechatMpShipshipServiceSystem/list
它们更偏后台配置,而不是前台纯展示组件。真正要理解的重点是:
- 哪个系统启用了哪些物流服务
- 微信小程序发货配置有没有挂上
- 后端有没有注册真实
shipClazz
所以新项目接物流时,通常先搭后台配置页,再考虑订单或充值页怎么消费物流状态。
6. 哪些能力本来就是项目层自己包
和 oak-general-business 一样,这里也不是“每条链都有完整页面成品”。
例如:
- 结算
settlePlan/settlement当前没有公共成品组件 - 新支付渠道的回调 endpoint 常常是项目层自己包一层路由,再复用公共
utils/pay
所以阅读支付域源码时,最合理的预期应该是:
- 公共包把主模型、状态机、公共组件和注入点准备好
- 项目层再按自己的业务路由和页面壳把它们串起来
最小接入思路
第一次把 oak-pay-business 接进项目时,建议按下面的顺序来。
1. 先声明依赖并生成装配代码
当前推荐先在 src/configuration/dependency.ts 中声明 oak-pay-business,再执行 project:init、make:domain 和 make: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.payConfig、offlineAccount$system、wpProduct$application。
3. 再决定是否注册项目层扩展
如果项目只用当前内建的 account、offlineAccount、wpProduct、apProduct、epProduct、spProduct、cpProduct、wfProduct、ppProduct 渠道,可以直接开始用;Web redirect 类产品仍受平台限制。
如果项目要继续扩展:
- 新支付产品,注册
registerPayClazz(...) - 新支付配置页,注册
registerPayChannelComponent(...) - 新支付唤起流程,注册
registerFrontendPayRoutine(...) - 新物流系统配置页,注册
registerShipSettingComponent(...)
参考项目里的真实接法
如果你对接入方式还有点抽象,haina-busi 和 taicang 这两个项目基本已经把 oak-pay-business 的常见接法都跑过一遍了。
1. 初始化通常只调用 initializeOpb1Features(...)
当前项目通常会在前端运行时初始化链路里同时创建:
createOgb0Features(...)createOpb1Features(...)
但真正启动时,很多地方只调用:
await initializeOpb1Features(features, accessConfiguration, config, cosClazzes);
这是因为 oak-pay-business/src/features/index.ts 的 initialize(...) 内部已经先调用了 oak-general-business 的初始化。也就是说,支付域初始化本身就把应用识别、登录态、微信环境、文件上传这些基础能力一起带上了。
2. 支付域的项目扩展一般写在 routines 和初始化文件里
从这两个项目的真实写法看,最常见的扩展点分布是:
initializeFeatures.web.ts:注册前端支付配置组件,例如registerPayChannelComponent('wpAccount', WpAccountConfig)routines/pay.ts:注册后端支付类,例如registerPayClazz('cmbProduct', ...)routines/start.ts:注册短信、COS、消息类型,以及项目自己的通知处理器- 页面层:直接包
payConfig/system、order/pay、pay/detail、account/detail、withdraw/create、ship/system
2.1 支付回调经常是“项目路由壳 + 公共处理逻辑”
这里有一个很值得新手提前建立的认知:
oak-pay-business已提供微信、支付宝、Epay、Stripe、Creem、Waffo、PayPal 等可选 endpoint 模块;- 项目层仍可以按同样模式补自己的
cmbPay.ts,或为公共模块包一层不同 HTTP 契约。
haina-busi 就是这么做的。它自己的 src/endpoints 里补了:
wechatPay.tsaliPay.tscmbPay.ts
但内部并没有重写整套状态推进,而是继续复用公共的:
@oak-pay-business/utils/pay.payNotify@oak-pay-business/utils/pay.refundNotify
所以项目层如果扩新渠道,最稳的做法通常不是“完全自己写 controller”,而是:
- 项目里补自己的 endpoint 路由名和参数结构
- 真正的回调处理继续复用公共支付逻辑
3. 实体层通常不是“重写一套支付模型”,而是扩展公共模型
haina-busi 的 CmbAccount、CmbProduct 是典型例子:
CmbAccount继承AbstractPayAccountCmbProduct继承AbstractPayProduct
taicang 则更偏向“在公共支付实体上继续补业务字段”:
System继承@oak-pay-business/entities/SystemOrder继承@oak-pay-business/entities/OrderShip继承@oak-pay-business/entities/Ship
这两种方式都说明:项目层最好是在公共支付模型上扩展,而不是脱离公共模型重新做一套资金域。
阅读源码时建议按这个顺序
第一次系统阅读 oak-pay-business,最推荐的顺序是:
src/features/index.ts与src/features/Pay.tssrc/registry.backend.ts与src/registry.frontend.tssrc/aspects/index.tssrc/entities/*.tssrc/checkerssrc/triggerssrc/watchers与src/timers- 最后再看
src/components
原因和 oak-general-business 一样:新手最容易先被组件数量吸走注意力,但真正决定支付域能力边界的,还是对象定义、状态机、trigger、checker 和注入点。
接下来的各章,我都会明确写出:
- 这一块解决什么问题;
- 对应哪些实体;
- 已经有哪些组件、aspect、endpoint、feature 可以直接复用;
- 后台有哪些 checker、trigger、watcher、timer 在兜底;
- 项目层该在哪里接入,以及应该写成什么样子。