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

系统支付配置与系统资金

支付域里最容易被低估的对象不是 pay,而是 system。在 oak-pay-business 里,很多真正影响资金行为的规则,并不挂在订单或支付对象上,而是统一挂在 System.payConfig 和系统级支付渠道配置上。

换句话说,订单怎么付、充值怎么收手续费、提现怎么扣手续费、系统层有哪些线下收款方式、有没有可用的微信支付产品,这些能力最终都要回到 system

主要对象

这一章最重要的对象有四组:

  • System.payConfig
  • OfflineAccount
  • WpAccount / WpProduct
  • SysAccountOper / SysAccountMove

其中 System.payConfigsrc/entities/System.ts 里额外定义了两块支付配置:

  • withdrawLoss
  • depositLoss

这个 System 是对 oak-general-business 中系统对象的扩展。近期支付包已经适配 general-system 的翻译字段、翻译状态和翻译动作,项目层如果继续覆盖 System,必须把这些通用字段和动作保留下来。

withdrawLoss 当前支持这些字段:

  • conservative
  • ratio
  • lowest
  • highest
  • trim: 'jiao' | 'yuan'

depositLoss 当前支持这些字段:

  • ratio
  • lowest
  • highest

OfflineAccount 则表示系统下配置的线下收款渠道,例如银行卡、微信收款码、支付宝收款码等。WpAccount / WpProduct 对应的则是微信支付账号与具体支付产品。

组件

系统支付配置最核心的现成组件有两组。

1. src/components/payConfig/system/web.pc.tsx

这是系统支付配置页的真实入口,它不是一个“只有手续费表单”的页面,而是一个带多个页签的总入口。源码里当前固定提供了两类页签:

  • system:编辑 System.payConfig
  • offlineAccount:管理 OfflineAccount

除此之外,它还会把所有通过 registerPayChannelComponent(...) 注册进来的渠道配置组件继续拼成额外页签。

也就是说,这个页面本身就是支付渠道配置的前端注入点。

2. src/components/sysAccount/survey/web.pc.tsx

这是系统资金总览页的核心组件。它内置了两类系统账户卡片和详情组件:

  • offlineAccount
  • wpAccount

并且继续提供两个注入点:

  • registerSysAccountCardTopComponent(...)
  • registerSysAccountDetailComponent(...)

项目层如果又加了新的系统资金账户类型,可以把展示卡片和详情区一起注册进来,而不用改这个组件本身。

真实项目里的页面包法

haina-busitaicang 这两个项目在系统支付配置页上的思路非常统一,都是只写一层很薄的页面壳:

<SystemPayConfig
  oakId={systemId}
  oakPath={`${oakFullpath}.system`}
/>

页面本身只负责:

  • 拿到当前 systemId
  • 给组件一个稳定的 oakPath
  • 在组件外面补 PageHeader 或路由壳

payConfig/systemsysAccount/survey 常用参数

这两个组件项目里最常用的参数分别是:

  • payConfig/systemoakIdoakPath
  • sysAccount/surveyoakIdoakPath

如果重新生成 oak-app-domain 后发现 system 缺少 translatetranslateSuccesstranslateFail 等动作,通常说明发布包实体解析或 ActionDef 别名兼容出了问题。应优先检查依赖包的 es/entities/System.d.ts 和同名 .js 是否一起发布。

如果项目层要给系统支付配置页追加新的支付渠道页签,真正传给被注册组件的参数是:

  • systemId
  • oakPath

也就是 registerPayChannelComponent(...) 对应的组件签名。

从源码看,这两个组件还有几个很关键的隐含行为:

  • payConfig/system 的基础投影里已经固定带了 payConfigwpAccount$systemofflineAccount$system
  • sysAccount/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-busitaicang 刚好给了两个真实样例:

  • haina-busi/src/pages/business/square/payConfig/web.pc.tsx:页面文件顶部直接注册 cmbAccountapAccountwpAccount
  • taicang/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 通过 entityentityId 锁定某一类系统账户
  • sysAccount/transferList 默认只查当前系统下 iState === 'transferring'withdrawTransfer

也就是说,前者适合做“某个系统账户的历史流水”,后者适合做“运营人员当前要处理的打款任务列表”。

sysAccount/transferList 的真实边界

这个组件当前几乎没有项目层参数,它的过滤条件是写死在组件里的:

  • withdrawAccount.ofSystemId = 当前 application.systemId
  • iState = 'transferring'

并且展示时会自动把渠道转成人类可读文案:

  • 普通渠道直接按 withdraw::channel.${entity}
  • offlineAccount 会进一步展开成具体 type

这意味着它非常适合:

  • 当前系统运营后台的“待打款列表”
  • 财务处理页

但如果你要做:

  • 跨系统打款总表
  • 已完成 / 已失败历史
  • 指定账户或指定渠道筛选

通常就要像 haina-busi 一样,在项目层再包一层列表组件,而不要直接拿公共 transferList 当万能列表。

sysAccountMove/create 适合放在哪里

这组组件在系统资金后台里也很有用,但文档里很容易漏掉。它当前最关键的参数有:

  • systemId
  • entities
  • onSuccess

真实行为则是:

  • 默认会拉 wpAccountofflineAccount
  • 再把 entities 里追加的系统账户实体一起并进可选列表
  • 让用户选择 fromto 两个系统账户
  • 输入 priceexternalIdremark
  • 最终创建一条 sysAccountMove
  • 同时自动带两条 sysAccountOper$sysAccountMove
    • moveOut
    • moveIn

也就是说,它不是一个简单“记一条备注”的组件,而是专门给系统账户之间做内部划拨用的。

这组组件尤其适合:

  • 财务后台做手工调账
  • 系统账户之间做内部资金搬运
  • 新增支付账户后,需要临时从旧账户转一笔资金过去的场景

如果项目要支持更多系统账户类型,最关键的不是改组件本身,而是把额外实体名通过 entities 传进来。

前端入口

系统支付配置相关的前端入口主要有两类。

features.pay

src/features/Pay.ts 会直接依赖当前应用和当前系统配置:

  • getPayChannels('pay') 会从 system.offlineAccount$systemapplication.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.ratio
  • system.payConfig.depositLoss.lowest
  • system.payConfig.depositLoss.highest

并返回:

  • 扣多少手续费
  • 用哪条多语言文案解释这次手续费
  • 解释文案的参数

提现手续费

aspects/withdraw.ts#getWithdrawCreateData(...) 会读取:

  • system.payConfig.withdrawLoss.conservative
  • system.payConfig.withdrawLoss.ratio
  • system.payConfig.withdrawLoss.lowest
  • system.payConfig.withdrawLoss.highest
  • system.payConfig.withdrawLoss.trim

如果这块配置根本没配,aspect 会直接抛 error::system.withdrawLossUnSet

换句话说,提现功能是否能正常创建,不只是取决于账户余额,更先取决于系统有没有把提现损耗规则配完整。

注入点

系统支付配置相关的注入点一共有四个:

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

其中:

  • 前三个决定系统管理后台“怎么配置、怎么展示”
  • 最后一个决定支付详情页“怎么真正发起支付”

项目中如何接入

这部分能力真正接进项目里,通常要做三件事。

1. 确保应用投影带上支付域需要的系统数据

features.pay 不是靠远程现查渠道,而是直接从当前应用数据里拿:

  • application.system.offlineAccount$system
  • application.wpProduct$application
  • application.system.payConfig

所以初始化 oak-pay-business feature 时,就要把这些投影带进去。

2. 管理后台直接复用现成系统配置页

如果项目已经有系统详情页,最稳妥的做法是直接挂 components/payConfig/system,不要自己再画一套支付配置表单。因为这个组件已经把:

  • System.payConfig
  • OfflineAccount
  • 注册进来的其它支付渠道组件

统一放到了同一个入口里。

3. 按需要补系统账户展示

如果项目层扩展了新的支付账户实体,除了注册支付类本身,最好顺手把系统资金总览页也补上:

import {
  registerSysAccountCardTopComponent,
  registerSysAccountDetailComponent,
} from '@oak-pay-business/registry.frontend';

registerSysAccountCardTopComponent('myPayAccount', MyPayAccountCard);
registerSysAccountDetailComponent('myPayAccount', MyPayAccountDetail);

真实项目里的系统资金入口

taicang 里,系统支付配置和系统资金总览已经拆成两张独立页面:

  • pages/console/system/payConfig
  • pages/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

都一起利用起来了。

使用建议

对一个新项目来说,最推荐的顺序是:

  1. 先把 System.payConfig 配完整;
  2. 再把 OfflineAccountWpProduct 这些系统级渠道配好;
  3. 然后才开始接订单支付、充值、提现页面;
  4. 如果项目有新的渠道实体,再继续补注入点。

如果顺序反过来做,最常见的问题就是:页面已经能选渠道了,但系统配置页和系统资金总览页还是缺的,最后很难排查“为什么这个渠道能展示但不能真正工作”。