账户、充值与流水
支付域里除了“对订单付款”,还有另一条经常单独存在的主线:账户充值。oak-pay-business 没把它做成一个孤立的钱包模块,而是把充值、到账、系统资金、账户流水放进了一套统一模型里。
如果你从源码角度看,这条线的核心其实是:
AccountDepositAccountOperSysAccountOperSysAccountMove
它和订单支付会复用同一套渠道能力,但状态推进和记账方式并不完全一样。
主要对象
Account
账户本身记录的是用户或业务对象的余额。最常见的两个数是:
totalavail
Deposit
src/entities/Deposit.ts 定义了充值单状态:
depositingsuccessfulfailedshipped
动作包括:
succeedfailship
这里的 shipped 很容易让人意外。原因是 oak-pay-business 允许把某些充值做成“先支付,后确认收货再到账”的模式,尤其是小程序虚拟发货场景。
AccountOper
这是账户流水。充值到账、退款返还、提现冻结或回退,最后都会落到 accountOper 上。
SysAccountOper / SysAccountMove
这两类对象对应的是系统资金层面的流水和划拨。也就是说,用户账户余额变化之外,平台自身的系统账户也有一套独立流水。
组件
这条主线最关键的现成组件有三组。
1. src/components/deposit/new/index.ts
这是“创建充值”的真实前端组件。它会:
- 通过
features.pay.getPayChannels('deposit')列出充值可用渠道 - 通过
features.pay.calcDepositLoss(...)计算充值手续费 - 在小程序环境里默认优先选
wpProduct
如果项目里自己做充值输入框和渠道选择,至少也要把这三件事一起补上。
deposit/new 常用参数与组件边界
这不是一个“自己持有全部状态并直接提交充值”的黑盒组件,而是一个受控表单。当前源码里暴露的关键参数有:
accountIddepositMinCentdepositMaxCentpricechannelonSetPriceonSetChannellossfocus
其中真正会直接参与渲染和联动的,是:
depositMinCent / depositMaxCent:控制金额上下限,并在ready()时用最小充值额初始化输入框price / channel / loss:父组件维护的受控状态onSetPrice / onSetChannel:组件内部只负责把变更往外抛
focus 目前主要用于小程序输入框聚焦,web 端并不会额外依赖它。accountId 虽然保留在属性里,但当前组件本体并不直接用它创建充值单,所以真正的提交动作仍然应该放在父组件,例如 account/detail 或项目自己的充值页里。
从实现上看,它还有两个默认行为很适合直接写进接入文档:
- 小程序环境且未指定渠道时,会自动优先选
wpProduct price和loss变化后,会重新计算小程序里的cursorSpacing,保证渠道和手续费区域不会挡住输入框
也就是说,deposit/new 更适合做“充值参数录入器”,而不是一个独立完成整条充值链路的页面。
2. src/components/account/detail/index.ts
这是账户详情页的核心组件。它不是只读组件,内部还封装了完整的充值创建流程:
- 调用
account.deposit - 嵌套创建
deposit - 再嵌套创建
pay - 轮询
pay直到进入paying - 最后跳转到未完成支付详情
也就是说,项目层如果直接复用它,充值链路已经是完整的。
3. 系统资金相关组件
系统侧的资金展示和流水组件主要分布在:
src/components/sysAccount/surveysrc/components/sysAccountMove/createsrc/components/sysAccountOper/listsrc/components/accountOper/*
这些组件通常会和上一章的系统支付配置页一起出现在管理后台里。
account/detail 常用参数
oak-pay-business/src/components/account/detail/index.ts 不是一个纯展示组件,项目里最常传的参数有:
depositMinCentdepositMaxCentautoStartPayonGoToUnfinishedPayonWithdrawpreWithdrawonGoToHistory
taicang/src/pages/frontend/account/detail/web.pc.tsx 的真实包法是:
<AccountDetail
autoStartPay={true}
depositMinCent={depositMinCent}
oakId={accountId}
oakPath={`${oakFullpath}.account`}
onGoToUnfinishedPay={(payId) => navigateTo({
url: '/pay/detail',
oakId: payId,
})}
onWithdraw={() => gotoWithdraw()}
preWithdraw={() => preWithdraw()}
onGoToHistory={gotoHistory}
/>
从组件内部逻辑看,它默认还会关心:
- 是否存在
depositing状态的充值单 - 是否存在
shipped状态、待确认收货的充值单 - 最近几条
accountOper$account
所以它适合做“账户主页”而不是只做一个充值按钮。项目层如果只是想要极简充值入口,才更适合单独包 deposit/new。
组件适合放在哪里
这组组件最适合的几个落点其实很明确:
- 用户钱包页、保证金账户页:用
account/detail - 账户流水页:用
accountOper/list - 系统资金总览页:用
sysAccount/survey
taicang 前台账户页和后台账户页都是围绕这套公共组件搭出来的。
accountOper/list 的真实筛选能力
这个列表组件并不只是简单把流水按时间倒序列出来。它当前的关键参数是:
accountId
但内部还已经内置了两组常见筛选:
- 按月份筛选
$$createAt$$ - 按收支方向筛选
availPlus
具体来说:
type: 'in'只看收入流水type: 'out'只看支出流水type: 'both'会过滤掉availPlus === 0的无效流水
所以项目层如果做账户历史页,通常不需要自己再额外写一个月份/收支筛选器,直接复用这个组件即可。
系统资金列表组件
除了总览卡片,系统资金后台里还有两组很值得直接复用的列表组件:
sysAccountOper/listsysAccount/transferList
sysAccountOper/list 当前通过下面两个参数工作:
entityentityId
它会按这两个字段过滤系统流水,并内置:
- 月份筛选
- 类型筛选,当前枚举包括
withdrawTransfer、pay、refund、compensate、moveIn、moveOut
sysAccount/transferList 则更偏运营处理页。它默认只看:
- 当前应用所属系统下的提现转账
iState === 'transferring'
并且会把 withdrawAccount.channel 自动转成可读渠道名。所以它很适合做“待打款提现处理列表”,而不是通用历史查询页。
开发注意事项
account/detail 的一个重要适配前提是:项目实体关系不要把公共充值链路打断。至少要保证这些关系还能正常工作:
deposit$accountpay$depositaccountOper$account
因为组件内部会直接沿着这些关系判断“是否有未完成充值”“是否需要去支付详情页”“最近流水是什么”。
前端入口
这条线主要依赖的前端入口还是 features.pay。
getPayChannels('deposit')
会返回所有允许充值的渠道。当前内建来源是:
offlineAccount.allowDeposit === true- 当前环境可用的
wpProduct
calcDepositLoss(price, channel)
会按 System.payConfig.depositLoss 直接计算充值损耗。返回值是一个三元组:
- 手续费金额
- 说明文案 key
- 文案参数
这也是 deposit/new 和 account/detail 里实际在用的方法。
后台规则
account.detail 发起充值时的真实操作
src/components/account/detail/index.ts 里最值得注意的代码,是它并不是直接创建一个 pay,而是走:
account.deposit- 嵌套
deposit$account.create - 再嵌套
pay$deposit.create
也就是说,在 Oak 模型里,“账户充值”不是一个页面自定义动作,而是账户对象上的正式业务动作。
Pay trigger 与 Deposit trigger 的配合
充值能不能真正到账,主要靠两层 trigger 协作:
triggers/pay.tstriggers/deposit.ts
当前源码里的真实行为是:
pay.succeedPaying后会根据depositId去推进充值单- 如果支付产品要求确认收货,充值单会先进入
ship deposit.succeed时才真正补记账户和系统账户的流水deposit状态变化还会推送数据事件
这就是为什么有些充值会“支付成功但余额还没到账”,因为它还卡在确认收货这一层。
小程序确认收货
如果是带确认收货的小程序充值,前端组件会调用:
shipConfirmSuccess
这个 aspect 定义在 src/aspects/ship.ts,如果微信侧物流状态已经确认收货,就会把 ship 从 receiving 推到 succeedReceiving,再由后续 trigger 推动 deposit.succeed。
注入点
账户充值这一章本身没有额外的独立 registry,但它会直接复用支付域已有的几个扩展点:
registerPayClazz(...)registerFrontendPayRoutine(...)registerSysAccountCardTopComponent(...)registerSysAccountDetailComponent(...)
也就是说,项目一旦加了新的充值渠道,不只订单支付会受影响,账户充值入口和系统资金展示也要一起补。
项目中如何接入
1. 先让应用能拿到充值渠道
features.pay.getPayChannels('deposit') 依赖当前应用和系统的投影,所以初始化阶段一定要把:
system.offlineAccount$systemwpProduct$applicationsystem.payConfig
带进来。
2. 充值入口优先复用现成组件
对新项目来说,最简单且最稳的做法一般是:
- 账户页直接挂
components/account/detail - 需要单独充值表单时,再挂
components/deposit/new
这样充值手续费、充值渠道和支付详情跳转都能统一起来。
3. 如果有系统资金管理台,再补系统侧组件
当项目需要运营后台查看平台收支时,再接:
sysAccount/surveysysAccountMove/createsysAccountOper/list
真实项目里的页面拆法
taicang 的做法很值得直接照搬:
/frontend/account/detail负责账户余额、充值入口、提现入口/frontend/pay/detail负责未完成支付/frontend/account/history负责账户流水
这样充值、支付、流水三条链路各自有独立页面,但底层仍然是同一套公共组件。
使用示例
1. 单独使用充值创建组件
<DepositNew
price={price}
channel={channel}
loss={loss}
depositMinCent={100}
depositMaxCent={200000}
onSetPrice={(nextPrice) => this.setState({ price: nextPrice })}
onSetChannel={(nextChannel) => this.setState({ channel: nextChannel })}
/>
当价格或渠道变化时,可以直接用 feature 重新计算手续费:
const loss = price && channel
? this.features.pay.calcDepositLoss(price, channel)
: [0, '', undefined];
2. 通过账户动作创建充值和支付
这就是 components/account/detail 当前真实采用的 Oak 操作结构:
await this.execute(undefined, undefined, undefined, [
{
entity: 'account',
operation: {
id: await generateNewIdAsync(),
action: 'deposit',
data: {
deposit$account: [
{
id: await generateNewIdAsync(),
action: 'create',
data: {
id: await generateNewIdAsync(),
price: depPrice,
loss: depositLoss[0] || 0,
creatorId: this.features.token.getUserId()!,
pay$deposit: [
{
id: await generateNewIdAsync(),
action: 'create',
data: {
id: payId,
autoStart: true,
price: depPrice,
entity: depositChannel.entity,
entityId: depositChannel.entityId,
},
},
],
},
},
],
},
filter: {
id: accountId,
},
},
},
]);
这段结构的意义在于:充值、支付、账户动作三者天然保持同一条 Oak 数据链。
使用建议
如果项目里既有订单支付,又有账户充值,最容易犯的错是把两条链拆成两套完全不同的前端和后台逻辑。实际上更推荐:
- 渠道层统一复用
features.pay和registerFrontendPayRoutine(...) - 记账层统一复用
deposit、accountOper、sysAccountOper - 需要确认收货的充值,直接复用
ship主线,不要额外发明一个“待到账状态”
这样后面要查“钱为什么没到”“系统资金为什么少了一笔”“用户余额为什么和支付成功不一致”时,排查路径会清楚很多。