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 最有价值的地方之一,不是它内置了多少渠道,而是它把“如何继续接新渠道”也做成了正式扩展点。你不用把公共包源码改烂,只要按它定义好的几个注册入口去接,就能把后端渠道类、系统配置页、前端支付唤起流程和系统资金展示一起补进去。

这一章的重点不是业务流程,而是这些注入点究竟怎么配合工作。

先分清两类扩展

支付域里的扩展其实分成两大类。

1. 后端渠道实现

这类扩展决定:

  • 如何预下单
  • 如何关单
  • 如何退款
  • 如何计算手续费
  • 如何查询支付/退款状态

真实入口是:

  • registerPayClazz(...)
  • getPayClazz(...)

2. 前端和后台管理扩展

这类扩展决定:

  • 系统支付配置页出现哪些额外页签
  • 支付详情页如何拉起新的支付渠道
  • 系统资金总览页如何展示新的账户类型
  • 物流设置页如何展示新的物流配置

真实入口是:

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

后端支付类扩展

当前内建渠道

src/utils/payClazz/index.ts 里当前内建了以下支付实现:

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

这些产品实体都会被 getPayClazz(...) 识别并实例化;是否能在当前前端场景展示,还要看应用投影、产品启用状态以及 Pay feature 的平台限制。当前 Web redirect 类产品的前端 routine 才会在 Web 环境展示,不能把“后端 payClazz 已内建”理解成所有平台都能直接拉起。

registerPayClazz(...)

这个注册函数位于 src/utils/payClazz/index.ts,用途是把新的支付产品实体接进 Oak 支付域。

它不只是简单挂一个构造函数,还会校验被注册实体是否满足公共支付模型的要求。

对账户实体的约束

accountEntity 对应的实体至少要有这些字段:

  • price
  • systemId
  • allowWithdrawTransfer
  • withdrawTransferLossRatio

对支付产品实体的约束

支付产品实体至少要有这些字段:

  • applicationId
  • enabled
  • taxLossRatio
  • refundGapDays
  • refundCompensateRatio
  • needReceiving

pay.entity 的约束

还会检查 schema.pay.attributes.entity.ref 里是否真的包含你注册的实体名。

也就是说,项目层要加新支付产品,不只是写个类就行,而是实体设计本身也要符合支付域公共约束。

真实项目里的实体适配方式

haina-busitaicang 在实体层给了两个非常典型的接法。

1. 新增一个完全独立的支付渠道实体

haina-busi 的做法是:

  • CmbAccount 继承 @oak-pay-business/entities/AbstractPayAccount
  • CmbProduct 继承 @oak-pay-business/entities/AbstractPayProduct

然后只补自己的渠道字段,例如:

  • 账户侧的商户号、密钥、回调地址、是否启用
  • 产品侧的类型、关联应用、是否启用

这种方式很适合新增一个全新的支付产品体系。

2. 在公共支付实体上继续加项目字段

taicang 的做法则是:

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

也就是说,项目自己的业务字段继续往公共支付实体上叠,而不是另起一套平行实体。

getPayClazz(...)

这是运行时真正拿渠道实现的入口。它会:

  • account 按实体类型缓存
  • 对其它渠道按 entity.entityId 缓存
  • 首次取用时再调用对应的构造函数

后面的:

  • pay.startPaying
  • refund.create
  • withdraw.getWithdrawCreateData
  • watcher 轮询支付状态和退款状态

都会依赖它。

物流类扩展

registerShipClazzEntity(...)

物流扩展和支付类扩展的思路完全一样,只是入口在 src/utils/shipClazz/index.ts

被注册的物流实体当前至少要满足这些字段:

  • sort
  • systemId
  • disabled

之后 getShipEntity(...) 会按当前系统里所有已注册实体的 sort 从大到小挑可用物流类,并调用它的 available(...)

registerShipSettingComponent(...)

后端物流类注册完之后,通常还要顺手把系统设置页也接上,这就是前端对应的注入点。

前端支付流程扩展

registerFrontendPayRoutine(...)

这个注册函数定义在 src/components/pay/detail/index.ts。它接受四部分内容:

  • entity
  • routine
  • projection
  • judgeCanPay

也就是说,一个新支付渠道要想在支付详情页里真正被唤起,不只是写一个 routine 就行,还要告诉组件:

  • 为了拉起支付,前端还需要预取哪些额外字段
  • 在什么条件下允许拉起支付

当前默认实现

当前内建实现包括:

  • 小程序环境调用 wx.requestPayment(...)
  • 微信网页环境调用 chooseWXPay
  • Web 环境下,apProductepProductspProductcpProductwfProductppProduct 共用 redirect routine,从 pay.meta 读取渠道返回的跳转 URL

如果项目还有新的支付产品,比如银联或自定义聚合支付,仍应通过这个入口注册,而不是直接改 pay/detail 组件源码。

系统配置与系统资金展示扩展

registerPayChannelComponent(...)

这个入口用于把新的支付配置组件挂进 components/payConfig/system/web.pc.tsx 的页签里。

注册后,系统支付配置页会自动多出一个:

  • 以实体名为 key 的新页签

并把:

  • oakPath
  • systemId

传给对应组件。

公共渠道配置组件

除了注册入口,本仓库本身也已经给几类常见渠道准备了管理组件。它们通常都是被 registerPayChannelComponent(...) 挂进 payConfig/system 页签里的。

offlineAccount/config

这个组件的关键参数是:

  • systemId

它默认会展示并维护:

  • type
  • channel
  • name
  • qrCode
  • allowDeposit
  • allowPay
  • price
  • enabled
  • taxLossRatio
  • refundCompensateRatio
  • refundGapDays
  • allowWithdrawTransfer
  • withdrawTransferLossRatio

所以它本质上是“线下收款账户 + 提现打款账户”的系统管理页,而不只是一个收款码列表。

wpAccount/config

这个组件同样按 systemId 工作,当前主要维护:

  • mchId
  • wechatPayId
  • apiV3Key
  • publicKeyFilePath
  • privateKeyFilePath
  • refundGapDays
  • taxLossRatio
  • refundCompensateRatio
  • allowWithdrawTransfer
  • withdrawTransferLossRatio
  • needReceiving

从源码看,它还有一个很重要的限制:

  • canCreate 只有在当前系统下不存在已启用账户时才为真

也就是说,公共实现默认把 wpAccount 当成“一个系统下单一主账号”的配置方式。

另外它在展示层还有一个很容易被忽略的设计:

  • 每个账户卡片内部其实是“详情 + wpProduct/config”两个页签

所以项目层如果直接复用它,通常不需要再额外写一页“某个微信支付账号下有哪些支付产品”。

wpProduct/config

这个组件的关键参数有:

  • systemId
  • wpAccountId

它会在进入时主动刷新当前 systemId 下的所有 application,然后为某个 wpAccount 维护它挂载的支付产品。当前维护的重点字段包括:

  • type
  • applicationId
  • taxLossRatio
  • refundCompensateRatio
  • refundGapDays
  • needReceiving
  • enabled

这也说明一个关键适配点:

  • 如果系统下应用没配好,wpProduct/config 就不会有可选 application

apAccount/config

支付宝账号配置组件和 wpAccount/config 是平行设计,关键参数同样是:

  • systemId

当前源码里它会维护的重点字段包括:

  • appId
  • mchId
  • aliPayId
  • publicKeyPath
  • privateKeyPath
  • encryptKey
  • alipayRootCertPath
  • alipayPublicCertPath
  • appCertPath
  • mode
  • gateway
  • endpoint
  • callbackUrl
  • wsServiceUrl
  • setting
  • keyType
  • timeout
  • needEncrypt
  • refundGapDays
  • taxLossRatio
  • refundCompensateRatio
  • allowWithdrawTransfer
  • withdrawTransferLossRatio
  • needReceiving
  • enabled

它同样内置了两个很关键的行为:

  • canCreate 只有在当前系统下没有已启用账号时才为真
  • 每个账号卡片内部直接带一个 apProduct/config 页签

所以它不是一个“只填支付宝证书路径”的表单,而是“支付宝账户 + 支付产品”的组合管理入口。

apProduct/config

这个组件的关键参数有:

  • systemId
  • apAccountId

它和 wpProduct/config 一样,会在进入时先刷新当前系统下的 application,再给某个 apAccount 维护挂载的支付产品。当前会重点维护:

  • type
  • applicationId
  • config
  • taxLossRatio
  • refundCompensateRatio
  • refundGapDays
  • needReceiving
  • enabled

实际界面里它还有两个值得提前告诉开发的行为:

  • 列表支持直接开关 enabled
  • 删除、创建都是在当前账号上下文内完成,不需要项目层额外再传过滤条件

所以项目里真正要保证的是:

  • 当前系统已经有可选 application
  • apProduct 实体已经把 applicationIdapAccountId 等公共约束定义完整

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 相比,它当前会同时维护:

  • payNotifyUrl
  • refundNotifyUrl

因此如果你的项目同时接微信和支付宝,不能简单以为两边配置页完全对称。

registerSysAccountCardTopComponent(...)

给系统资金总览页顶部卡片增加新的账户显示样式。

registerSysAccountDetailComponent(...)

给系统资金总览页里的详情区域增加新的账户详情组件。

这两个入口通常会和新的支付账户实体一起出现。

registry.backend.tsregistry.frontend.ts

这两个文件的区别要记清楚。

src/registry.backend.ts

后端入口只导出:

  • registerPayClazz

src/registry.frontend.ts

前端环境只导出:

  • registerPayChannelComponent
  • registerFrontendPayRoutine
  • registerShipSettingComponent
  • registerSysAccountCardTopComponent
  • registerSysAccountDetailComponent

故意不导出 registerPayClazz(...),因为后端渠道类注册本来就不应该在前端运行时里做。

项目里该从哪个入口 import

这一点最好在文档里直接说死,否则项目里很容易出现“能跑但 import 路径混乱”的情况:

  • 后端运行时注册:从 registry.backend.ts import
  • 前端运行时注册:优先从 registry.frontend.ts import
  • 个别老项目可能直接从组件文件或 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 则保留了较早的页面级注册写法:

  • 直接从组件内部文件引入注册函数
  • 在页面文件里注册 cmbAccountapAccountwpAccount

两种方式都能工作,但如果是新项目或准备整理老项目,优先收敛到 registry.frontend.ts 会更清楚。

这样项目代码一眼就能看出“这是前端注入还是后端注入”。

真实项目里的注册顺序

haina-busi/src/routines/pay.ts 已经把一个完整样例跑通了:

  1. 先在实体层准备 cmbAccount / cmbProduct
  2. registerPayClazz('cmbProduct', { accountEntity: 'cmbAccount', ... }, storageSchema)
  3. 在系统支付配置页注册 registerPayChannelComponent('cmbAccount', CmbAccountConfig)
  4. 再根据需要补 registerFrontendPayRoutine(...) 或复用已有详情页逻辑

同一个文件里还注册了:

  • registerPayClazz('apProduct', { accountEntity: 'apAccount', ... }, storageSchema)

这说明一个项目里同时扩多个渠道,本来就应该通过 registry 统一管理,而不是在页面里分散硬编码。

回调 endpoint 也应该复用公共处理

haina-busiwechatPay.tscmbPay.tsaliPay.ts 都直接复用了:

  • @oak-pay-business/utils/paypayNotify
  • @oak-pay-business/utils/payrefundNotify

因此一个完整渠道接入,最好同时包括:

  • 实体
  • registerPayClazz(...)
  • 系统配置组件
  • 前端唤起
  • 回调 endpoint

项目中如何接入

一个新渠道接入时,最稳妥的顺序通常是:

  1. 先补实体,确保符合公共支付模型
  2. 后端注册 registerPayClazz(...)
  3. 系统配置页注册 registerPayChannelComponent(...)
  4. 支付详情页注册 registerFrontendPayRoutine(...)
  5. 如果涉及系统资金账户,再注册系统资金展示组件

如果只做了第 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);

使用建议

对项目层来说,最重要的不是“能不能很快写出一个新渠道类”,而是要把渠道接入看成四件一起完成的事:

  1. 后端支付能力
  2. 系统配置入口
  3. 前端支付唤起
  4. 系统资金展示

只补其中一层,后面几乎一定会在管理后台、支付详情页或者提现链路里出现断层。