渠道、组件注入与扩展点
oak-pay-business 最有价值的地方之一,不是它内置了多少渠道,而是它把“如何继续接新渠道”也做成了正式扩展点。你不用把公共包源码改烂,只要按它定义好的几个注册入口去接,就能把后端渠道类、系统配置页、前端支付唤起流程和系统资金展示一起补进去。
这一章的重点不是业务流程,而是这些注入点究竟怎么配合工作。
先分清两类扩展
支付域里的扩展其实分成两大类。
1. 后端渠道实现
这类扩展决定:
- 如何预下单
- 如何关单
- 如何退款
- 如何计算手续费
- 如何查询支付/退款状态
真实入口是:
registerPayClazz(...)getPayClazz(...)
2. 前端和后台管理扩展
这类扩展决定:
- 系统支付配置页出现哪些额外页签
- 支付详情页如何拉起新的支付渠道
- 系统资金总览页如何展示新的账户类型
- 物流设置页如何展示新的物流配置
真实入口是:
registerPayChannelComponent(...)registerFrontendPayRoutine(...)registerShipSettingComponent(...)registerSysAccountCardTopComponent(...)registerSysAccountDetailComponent(...)
后端支付类扩展
当前内建渠道
src/utils/payClazz/index.ts 里当前内建了以下支付实现:
accountofflineAccountwpProductapProductepProductspProductcpProductwfProductppProduct
这些产品实体都会被 getPayClazz(...) 识别并实例化;是否能在当前前端场景展示,还要看应用投影、产品启用状态以及 Pay feature 的平台限制。当前 Web redirect 类产品的前端 routine 才会在 Web 环境展示,不能把“后端 payClazz 已内建”理解成所有平台都能直接拉起。
registerPayClazz(...)
这个注册函数位于 src/utils/payClazz/index.ts,用途是把新的支付产品实体接进 Oak 支付域。
它不只是简单挂一个构造函数,还会校验被注册实体是否满足公共支付模型的要求。
对账户实体的约束
accountEntity 对应的实体至少要有这些字段:
pricesystemIdallowWithdrawTransferwithdrawTransferLossRatio
对支付产品实体的约束
支付产品实体至少要有这些字段:
applicationIdenabledtaxLossRatiorefundGapDaysrefundCompensateRationeedReceiving
对 pay.entity 的约束
还会检查 schema.pay.attributes.entity.ref 里是否真的包含你注册的实体名。
也就是说,项目层要加新支付产品,不只是写个类就行,而是实体设计本身也要符合支付域公共约束。
真实项目里的实体适配方式
haina-busi 和 taicang 在实体层给了两个非常典型的接法。
1. 新增一个完全独立的支付渠道实体
haina-busi 的做法是:
CmbAccount继承@oak-pay-business/entities/AbstractPayAccountCmbProduct继承@oak-pay-business/entities/AbstractPayProduct
然后只补自己的渠道字段,例如:
- 账户侧的商户号、密钥、回调地址、是否启用
- 产品侧的类型、关联应用、是否启用
这种方式很适合新增一个全新的支付产品体系。
2. 在公共支付实体上继续加项目字段
taicang 的做法则是:
System继承@oak-pay-business/entities/SystemOrder继承@oak-pay-business/entities/OrderShip继承@oak-pay-business/entities/Ship
也就是说,项目自己的业务字段继续往公共支付实体上叠,而不是另起一套平行实体。
getPayClazz(...)
这是运行时真正拿渠道实现的入口。它会:
- 对
account按实体类型缓存 - 对其它渠道按
entity.entityId缓存 - 首次取用时再调用对应的构造函数
后面的:
pay.startPayingrefund.createwithdraw.getWithdrawCreateData- watcher 轮询支付状态和退款状态
都会依赖它。
物流类扩展
registerShipClazzEntity(...)
物流扩展和支付类扩展的思路完全一样,只是入口在 src/utils/shipClazz/index.ts。
被注册的物流实体当前至少要满足这些字段:
sortsystemIddisabled
之后 getShipEntity(...) 会按当前系统里所有已注册实体的 sort 从大到小挑可用物流类,并调用它的 available(...)。
registerShipSettingComponent(...)
后端物流类注册完之后,通常还要顺手把系统设置页也接上,这就是前端对应的注入点。
前端支付流程扩展
registerFrontendPayRoutine(...)
这个注册函数定义在 src/components/pay/detail/index.ts。它接受四部分内容:
entityroutineprojectionjudgeCanPay
也就是说,一个新支付渠道要想在支付详情页里真正被唤起,不只是写一个 routine 就行,还要告诉组件:
- 为了拉起支付,前端还需要预取哪些额外字段
- 在什么条件下允许拉起支付
当前默认实现
当前内建实现包括:
- 小程序环境调用
wx.requestPayment(...) - 微信网页环境调用
chooseWXPay - Web 环境下,
apProduct、epProduct、spProduct、cpProduct、wfProduct、ppProduct共用 redirect routine,从pay.meta读取渠道返回的跳转 URL
如果项目还有新的支付产品,比如银联或自定义聚合支付,仍应通过这个入口注册,而不是直接改 pay/detail 组件源码。
系统配置与系统资金展示扩展
registerPayChannelComponent(...)
这个入口用于把新的支付配置组件挂进 components/payConfig/system/web.pc.tsx 的页签里。
注册后,系统支付配置页会自动多出一个:
- 以实体名为 key 的新页签
并把:
oakPathsystemId
传给对应组件。
公共渠道配置组件
除了注册入口,本仓库本身也已经给几类常见渠道准备了管理组件。它们通常都是被 registerPayChannelComponent(...) 挂进 payConfig/system 页签里的。
offlineAccount/config
这个组件的关键参数是:
systemId
它默认会展示并维护:
typechannelnameqrCodeallowDepositallowPaypriceenabledtaxLossRatiorefundCompensateRatiorefundGapDaysallowWithdrawTransferwithdrawTransferLossRatio
所以它本质上是“线下收款账户 + 提现打款账户”的系统管理页,而不只是一个收款码列表。
wpAccount/config
这个组件同样按 systemId 工作,当前主要维护:
mchIdwechatPayIdapiV3KeypublicKeyFilePathprivateKeyFilePathrefundGapDaystaxLossRatiorefundCompensateRatioallowWithdrawTransferwithdrawTransferLossRationeedReceiving
从源码看,它还有一个很重要的限制:
canCreate只有在当前系统下不存在已启用账户时才为真
也就是说,公共实现默认把 wpAccount 当成“一个系统下单一主账号”的配置方式。
另外它在展示层还有一个很容易被忽略的设计:
- 每个账户卡片内部其实是“详情 +
wpProduct/config”两个页签
所以项目层如果直接复用它,通常不需要再额外写一页“某个微信支付账号下有哪些支付产品”。
wpProduct/config
这个组件的关键参数有:
systemIdwpAccountId
它会在进入时主动刷新当前 systemId 下的所有 application,然后为某个 wpAccount 维护它挂载的支付产品。当前维护的重点字段包括:
typeapplicationIdtaxLossRatiorefundCompensateRatiorefundGapDaysneedReceivingenabled
这也说明一个关键适配点:
- 如果系统下应用没配好,
wpProduct/config就不会有可选application
apAccount/config
支付宝账号配置组件和 wpAccount/config 是平行设计,关键参数同样是:
systemId
当前源码里它会维护的重点字段包括:
appIdmchIdaliPayIdpublicKeyPathprivateKeyPathencryptKeyalipayRootCertPathalipayPublicCertPathappCertPathmodegatewayendpointcallbackUrlwsServiceUrlsettingkeyTypetimeoutneedEncryptrefundGapDaystaxLossRatiorefundCompensateRatioallowWithdrawTransferwithdrawTransferLossRationeedReceivingenabled
它同样内置了两个很关键的行为:
canCreate只有在当前系统下没有已启用账号时才为真- 每个账号卡片内部直接带一个
apProduct/config页签
所以它不是一个“只填支付宝证书路径”的表单,而是“支付宝账户 + 支付产品”的组合管理入口。
apProduct/config
这个组件的关键参数有:
systemIdapAccountId
它和 wpProduct/config 一样,会在进入时先刷新当前系统下的 application,再给某个 apAccount 维护挂载的支付产品。当前会重点维护:
typeapplicationIdconfigtaxLossRatiorefundCompensateRatiorefundGapDaysneedReceivingenabled
实际界面里它还有两个值得提前告诉开发的行为:
- 列表支持直接开关
enabled - 删除、创建都是在当前账号上下文内完成,不需要项目层额外再传过滤条件
所以项目里真正要保证的是:
- 当前系统已经有可选
application apProduct实体已经把applicationId、apAccountId等公共约束定义完整
aliPay/upsert
它和前面的 wechatPay/upsert 是平行的“基础支付配置单页”,关键参数也是:
systemId
它同样会根据 system.domain$system 自动计算 serverUrl,并在创建态自动写入 systemId。
不过这里有一个很关键的源码细节:
- 当前
web.pc.tsx只真正暴露了payNotifyUrl refundNotifyUrl的表单项虽然存在,但在前端代码里被注释掉了
所以项目如果需要在后台界面里单独配置支付宝退款回调,要么确认已有默认约定,要么自己在项目层补表单,不要直接假设公共页面已经把退款回调入口放出来了。
wechatPay/upsert
这个组件更偏“微信支付基础配置单页”,关键参数是:
systemId
它会根据 system.domain$system 自动算出 serverUrl,并在创建态自动把 systemId 回填进去。所以项目层在接微信支付基础配置时,最好保证系统域名已经先配好,否则回调地址这类字段很难一次配准确。
和 aliPay/upsert 相比,它当前会同时维护:
payNotifyUrlrefundNotifyUrl
因此如果你的项目同时接微信和支付宝,不能简单以为两边配置页完全对称。
registerSysAccountCardTopComponent(...)
给系统资金总览页顶部卡片增加新的账户显示样式。
registerSysAccountDetailComponent(...)
给系统资金总览页里的详情区域增加新的账户详情组件。
这两个入口通常会和新的支付账户实体一起出现。
registry.backend.ts 与 registry.frontend.ts
这两个文件的区别要记清楚。
src/registry.backend.ts
后端入口只导出:
registerPayClazz
src/registry.frontend.ts
前端环境只导出:
registerPayChannelComponentregisterFrontendPayRoutineregisterShipSettingComponentregisterSysAccountCardTopComponentregisterSysAccountDetailComponent
故意不导出 registerPayClazz(...),因为后端渠道类注册本来就不应该在前端运行时里做。
项目里该从哪个入口 import
这一点最好在文档里直接说死,否则项目里很容易出现“能跑但 import 路径混乱”的情况:
- 后端运行时注册:从
registry.backend.tsimport - 前端运行时注册:优先从
registry.frontend.tsimport - 个别老项目可能直接从组件文件或 utils 文件 import,例如
haina-busi就有直接从components/payConfig/system/web.pc注册渠道的写法
从长期维护角度看,更推荐:
- 前端统一走
registry.frontend.ts - 后端统一走
registry.backend.ts
taicang/src/initializeFeatures.web.ts 就是这种较新的写法:
- 从
@oak-pay-business/registry.frontend引入registerPayChannelComponent - 注册
wpAccount -> WpAccountConfig
而 haina-busi/src/pages/business/square/payConfig/web.pc.tsx 则保留了较早的页面级注册写法:
- 直接从组件内部文件引入注册函数
- 在页面文件里注册
cmbAccount、apAccount、wpAccount
两种方式都能工作,但如果是新项目或准备整理老项目,优先收敛到 registry.frontend.ts 会更清楚。
这样项目代码一眼就能看出“这是前端注入还是后端注入”。
真实项目里的注册顺序
haina-busi/src/routines/pay.ts 已经把一个完整样例跑通了:
- 先在实体层准备
cmbAccount/cmbProduct registerPayClazz('cmbProduct', { accountEntity: 'cmbAccount', ... }, storageSchema)- 在系统支付配置页注册
registerPayChannelComponent('cmbAccount', CmbAccountConfig) - 再根据需要补
registerFrontendPayRoutine(...)或复用已有详情页逻辑
同一个文件里还注册了:
registerPayClazz('apProduct', { accountEntity: 'apAccount', ... }, storageSchema)
这说明一个项目里同时扩多个渠道,本来就应该通过 registry 统一管理,而不是在页面里分散硬编码。
回调 endpoint 也应该复用公共处理
haina-busi 的 wechatPay.ts、cmbPay.ts、aliPay.ts 都直接复用了:
@oak-pay-business/utils/pay的payNotify@oak-pay-business/utils/pay的refundNotify
因此一个完整渠道接入,最好同时包括:
- 实体
registerPayClazz(...)- 系统配置组件
- 前端唤起
- 回调 endpoint
项目中如何接入
一个新渠道接入时,最稳妥的顺序通常是:
- 先补实体,确保符合公共支付模型
- 后端注册
registerPayClazz(...) - 系统配置页注册
registerPayChannelComponent(...) - 支付详情页注册
registerFrontendPayRoutine(...) - 如果涉及系统资金账户,再注册系统资金展示组件
如果只做了第 2 步,后台虽然能跑,但系统管理台和支付详情页都还不知道这个渠道怎么配置、怎么发起。
使用示例
1. 注册新的支付渠道类
下面是一个项目层示例。名字用 myPayProduct / myPayAccount,表示这是项目自己的扩展实体:
import { registerPayClazz } from '@oak-pay-business/registry.backend';
registerPayClazz(
'myPayProduct',
{
accountEntity: 'myPayAccount',
clazzConstructor: async (entityId, context) => new MyPayClazz(entityId, context),
},
storageSchema,
);
2. 注册系统支付配置页
import { registerPayChannelComponent } from '@oak-pay-business/registry.frontend';
registerPayChannelComponent('myPayProduct', MyPayProductConfig);
3. 注册前端支付唤起流程
import { registerFrontendPayRoutine } from '@oak-pay-business/registry.frontend';
registerFrontendPayRoutine(
'myPayProduct',
async (pay, features) => {
await myPaySdk.start(pay.meta);
},
{
myPayProduct: {
id: 1,
config: 1,
},
},
(pay) => pay.iState === 'paying',
);
4. 注册系统资金展示
import {
registerSysAccountCardTopComponent,
registerSysAccountDetailComponent,
} from '@oak-pay-business/registry.frontend';
registerSysAccountCardTopComponent('myPayAccount', MyPayAccountCard);
registerSysAccountDetailComponent('myPayAccount', MyPayAccountDetail);
使用建议
对项目层来说,最重要的不是“能不能很快写出一个新渠道类”,而是要把渠道接入看成四件一起完成的事:
- 后端支付能力
- 系统配置入口
- 前端支付唤起
- 系统资金展示
只补其中一层,后面几乎一定会在管理后台、支付详情页或者提现链路里出现断层。