系统支付配置与系统资金
支付域里最容易被低估的对象不是 pay,而是 system。在 oak-pay-business 里,很多真正影响资金行为的规则,并不挂在订单或支付对象上,而是统一挂在 System.payConfig 和系统级支付渠道配置上。
换句话说,订单怎么付、充值怎么收手续费、提现怎么扣手续费、系统层有哪些线下收款方式、有没有可用的微信支付产品,这些能力最终都要回到 system。
主要对象
这一章最重要的对象有四组:
System.payConfigOfflineAccountWpAccount/WpProductSysAccountOper/SysAccountMove
其中 System.payConfig 在 src/entities/System.ts 里额外定义了两块支付配置:
withdrawLossdepositLoss
这个 System 是对 oak-general-business 中系统对象的扩展。近期支付包已经适配 general-system 的翻译字段、翻译状态和翻译动作,项目层如果继续覆盖 System,必须把这些通用字段和动作保留下来。
withdrawLoss 当前支持这些字段:
conservativeratiolowesthighesttrim: 'jiao' | 'yuan'
depositLoss 当前支持这些字段:
ratiolowesthighest
OfflineAccount 则表示系统下配置的线下收款渠道,例如银行卡、微信收款码、支付宝收款码等。WpAccount / WpProduct 对应的则是微信支付账号与具体支付产品。
组件
系统支付配置最核心的现成组件有两组。
1. src/components/payConfig/system/web.pc.tsx
这是系统支付配置页的真实入口,它不是一个“只有手续费表单”的页面,而是一个带多个页签的总入口。源码里当前固定提供了两类页签:
system:编辑System.payConfigofflineAccount:管理OfflineAccount
除此之外,它还会把所有通过 registerPayChannelComponent(...) 注册进来的渠道配置组件继续拼成额外页签。
也就是说,这个页面本身就是支付渠道配置的前端注入点。
2. src/components/sysAccount/survey/web.pc.tsx
这是系统资金总览页的核心组件。它内置了两类系统账户卡片和详情组件:
offlineAccountwpAccount
并且继续提供两个注入点:
registerSysAccountCardTopComponent(...)registerSysAccountDetailComponent(...)
项目层如果又加了新的系统资金账户类型,可以把展示卡片和详情区一起注册进来,而不用改这个组件本身。
真实项目里的页面包法
haina-busi 和 taicang 这两个项目在系统支付配置页上的思路非常统一,都是只写一层很薄的页面壳:
<SystemPayConfig
oakId={systemId}
oakPath={`${oakFullpath}.system`}
/>
页面本身只负责:
- 拿到当前
systemId - 给组件一个稳定的
oakPath - 在组件外面补
PageHeader或路由壳
payConfig/system 与 sysAccount/survey 常用参数
这两个组件项目里最常用的参数分别是:
payConfig/system:oakId、oakPathsysAccount/survey:oakId、oakPath
如果重新生成 oak-app-domain 后发现 system 缺少 translate、translateSuccess、translateFail 等动作,通常说明发布包实体解析或 ActionDef 别名兼容出了问题。应优先检查依赖包的 es/entities/System.d.ts 和同名 .js 是否一起发布。
如果项目层要给系统支付配置页追加新的支付渠道页签,真正传给被注册组件的参数是:
systemIdoakPath
也就是 registerPayChannelComponent(...) 对应的组件签名。
从源码看,这两个组件还有几个很关键的隐含行为:
payConfig/system的基础投影里已经固定带了payConfig、wpAccount$system、offlineAccount$systemsysAccount/survey如果没传systemId,会回退到features.application.getApplication().systemId
也就是说:
- 前者适合明确挂在“某个系统的配置页”
- 后者既能做后台总览页,也能做当前应用上下文下的系统资金概览
sysAccount/survey 当前实际统计了什么
这个组件不只是把各类系统账户列出来,它还会在 ready() 时主动做几组聚合统计:
- 所有
sysAccountOper.entity.ref里声明过的系统账户实体余额汇总 account侧的账户数、总额、可用额汇总refund侧“退款中”的数量和金额汇总withdrawTransfer侧“打款中”的数量和金额汇总order侧“未结算订单”的数量、已付总额、已退总额汇总
也就是说,它本质上不是“账户卡片容器”,而是一个已经把系统资金全景统计做好的总览组件。
另一个很值得提前写清楚的行为是:
- 它会读取
schema.sysAccountOper.attributes.entity.ref - 再把这些实体按
systemId全部拉出来
所以项目层新增系统账户实体时,只补展示组件还不够,最好确保新实体也真的进入了 sysAccountOper.entity.ref。
真实项目里的渠道注册位置
这一点 haina-busi 和 taicang 刚好给了两个真实样例:
haina-busi/src/pages/business/square/payConfig/web.pc.tsx:页面文件顶部直接注册cmbAccount、apAccount、wpAccounttaicang/src/initializeFeatures.web.ts:前端初始化阶段注册wpAccount
更推荐的项目写法仍然是放在初始化文件里统一注册,但如果项目结构较轻,也可以像 haina-busi 一样在具体页面里先注册再渲染。
sysAccount/survey 的适配要求
这个组件比表面上看更“动态”。它会读取:
schema.sysAccountOper.attributes.entity.ref
然后把这里声明过的所有系统账户实体都按 systemId 拉出来汇总展示。
这意味着如果项目层新增了一个系统账户实体,只注册展示组件还不够,最好同时确认:
- 该实体已经进入
sysAccountOper.entity.ref - 该实体具备
systemId字段,能按系统维度查询
否则系统资金总览页根本不知道应该去拉哪类账户。
系统资金明细组件怎么搭配
很多项目只接了 sysAccount/survey,但没有把后续明细页搭起来。结合公共组件源码,更推荐的搭配是:
- 总览页:
sysAccount/survey - 单账户流水页:
sysAccountOper/list - 待处理转账页:
sysAccount/transferList
其中:
sysAccountOper/list通过entity、entityId锁定某一类系统账户sysAccount/transferList默认只查当前系统下iState === 'transferring'的withdrawTransfer
也就是说,前者适合做“某个系统账户的历史流水”,后者适合做“运营人员当前要处理的打款任务列表”。
sysAccount/transferList 的真实边界
这个组件当前几乎没有项目层参数,它的过滤条件是写死在组件里的:
withdrawAccount.ofSystemId = 当前 application.systemIdiState = 'transferring'
并且展示时会自动把渠道转成人类可读文案:
- 普通渠道直接按
withdraw::channel.${entity} offlineAccount会进一步展开成具体type
这意味着它非常适合:
- 当前系统运营后台的“待打款列表”
- 财务处理页
但如果你要做:
- 跨系统打款总表
- 已完成 / 已失败历史
- 指定账户或指定渠道筛选
通常就要像 haina-busi 一样,在项目层再包一层列表组件,而不要直接拿公共 transferList 当万能列表。
sysAccountMove/create 适合放在哪里
这组组件在系统资金后台里也很有用,但文档里很容易漏掉。它当前最关键的参数有:
systemIdentitiesonSuccess
真实行为则是:
- 默认会拉
wpAccount、offlineAccount - 再把
entities里追加的系统账户实体一起并进可选列表 - 让用户选择
from、to两个系统账户 - 输入
price、externalId、remark - 最终创建一条
sysAccountMove - 同时自动带两条
sysAccountOper$sysAccountMovemoveOutmoveIn
也就是说,它不是一个简单“记一条备注”的组件,而是专门给系统账户之间做内部划拨用的。
这组组件尤其适合:
- 财务后台做手工调账
- 系统账户之间做内部资金搬运
- 新增支付账户后,需要临时从旧账户转一笔资金过去的场景
如果项目要支持更多系统账户类型,最关键的不是改组件本身,而是把额外实体名通过 entities 传进来。
前端入口
系统支付配置相关的前端入口主要有两类。
features.pay
src/features/Pay.ts 会直接依赖当前应用和当前系统配置:
getPayChannels('pay')会从system.offlineAccount$system和application.wpProduct$application里计算可用支付渠道getPayChannels('deposit')会只挑允许充值的渠道calcDepositLoss(price, channel)会按system.payConfig.depositLoss计算充值手续费
要特别注意一点:getDepositRatio(channel) 当前还没有实现,源码里直接 throw new Error('method not implemented')。如果项目层要展示“某渠道费率”,不要误以为这个方法已经可用。
系统配置组件
payConfig/system 组件会直接对 system.payConfig 发起更新。也就是说,这块能力并不是单独通过一个 aspect 暴露的,而是作为 system 的正常更新操作进入 Oak 数据流。
后台规则
系统支付配置本身虽然只是几个字段,但后面的资金流程都依赖它。
充值手续费
features.pay.calcDepositLoss(...) 实际会读取:
system.payConfig.depositLoss.ratiosystem.payConfig.depositLoss.lowestsystem.payConfig.depositLoss.highest
并返回:
- 扣多少手续费
- 用哪条多语言文案解释这次手续费
- 解释文案的参数
提现手续费
aspects/withdraw.ts#getWithdrawCreateData(...) 会读取:
system.payConfig.withdrawLoss.conservativesystem.payConfig.withdrawLoss.ratiosystem.payConfig.withdrawLoss.lowestsystem.payConfig.withdrawLoss.highestsystem.payConfig.withdrawLoss.trim
如果这块配置根本没配,aspect 会直接抛 error::system.withdrawLossUnSet。
换句话说,提现功能是否能正常创建,不只是取决于账户余额,更先取决于系统有没有把提现损耗规则配完整。
注入点
系统支付配置相关的注入点一共有四个:
registerPayChannelComponent(...)registerSysAccountCardTopComponent(...)registerSysAccountDetailComponent(...)registerFrontendPayRoutine(...)
其中:
- 前三个决定系统管理后台“怎么配置、怎么展示”
- 最后一个决定支付详情页“怎么真正发起支付”
项目中如何接入
这部分能力真正接进项目里,通常要做三件事。
1. 确保应用投影带上支付域需要的系统数据
features.pay 不是靠远程现查渠道,而是直接从当前应用数据里拿:
application.system.offlineAccount$systemapplication.wpProduct$applicationapplication.system.payConfig
所以初始化 oak-pay-business feature 时,就要把这些投影带进去。
2. 管理后台直接复用现成系统配置页
如果项目已经有系统详情页,最稳妥的做法是直接挂 components/payConfig/system,不要自己再画一套支付配置表单。因为这个组件已经把:
System.payConfigOfflineAccount- 注册进来的其它支付渠道组件
统一放到了同一个入口里。
3. 按需要补系统账户展示
如果项目层扩展了新的支付账户实体,除了注册支付类本身,最好顺手把系统资金总览页也补上:
import {
registerSysAccountCardTopComponent,
registerSysAccountDetailComponent,
} from '@oak-pay-business/registry.frontend';
registerSysAccountCardTopComponent('myPayAccount', MyPayAccountCard);
registerSysAccountDetailComponent('myPayAccount', MyPayAccountDetail);
真实项目里的系统资金入口
在 taicang 里,系统支付配置和系统资金总览已经拆成两张独立页面:
pages/console/system/payConfigpages/console/system/accountSurvey
这是一种很实用的拆法。配置页负责“能不能用、费率怎么配”,总览页负责“现在账上还有多少钱、流水长什么样”。
而从这次对 haina-busi 的检索看,它在系统资金历史和转账处理上则更偏项目层包裹:
- 系统资金流水会再包自己的
squareBusiness/sysAccountOper/list - 提现打款列表也会再包自己的
wallet/deposit/transferList
这说明公共组件当前更像:
taicang这种常规系统后台可以直接落的基础页haina-busi这种平台化更强的后台,会在此基础上再补业务筛选和展示字段
开发注意事项
系统支付配置和系统资金总览通常要一起看。最常见的误区是只注册了:
registerPayChannelComponent(...)
却没有补:
registerSysAccountCardTopComponent(...)registerSysAccountDetailComponent(...)
结果就是配置页能看到新渠道,但资金总览页完全不知道怎么展示它。
使用示例
1. 在系统页里配置充值和提现手续费
这就是 payConfig/system 组件实际维护的数据形状:
await this.features.cache.exec('operate', {
entity: 'system',
operation: {
id: await generateNewIdAsync(),
action: 'update',
data: {
payConfig: {
depositLoss: {
ratio: 0.6,
lowest: 1,
highest: 200,
},
withdrawLoss: {
conservative: false,
ratio: 0.8,
lowest: 100,
highest: 5000,
trim: 'jiao',
},
},
},
filter: {
id: systemId,
},
},
});
2. 在前端读取当前可用支付渠道
const payChannels = this.features.pay.getPayChannels('pay', accountId);
const depositChannels = this.features.pay.getPayChannels('deposit');
const depositLoss = this.features.pay.calcDepositLoss(10000, depositChannels[0]);
这三行背后实际就已经把:
- 当前应用挂着的
wpProduct - 当前系统配置的
offlineAccount System.payConfig.depositLoss
都一起利用起来了。
使用建议
对一个新项目来说,最推荐的顺序是:
- 先把
System.payConfig配完整; - 再把
OfflineAccount和WpProduct这些系统级渠道配好; - 然后才开始接订单支付、充值、提现页面;
- 如果项目有新的渠道实体,再继续补注入点。
如果顺序反过来做,最常见的问题就是:页面已经能选渠道了,但系统配置页和系统资金总览页还是缺的,最后很难排查“为什么这个渠道能展示但不能真正工作”。