金额与资产
摘要:金额、币种、资产编码和时间格式的第一阶段通用底座。
金额与资产底座用于统一 Mall、Payment、Open Platform、Web3、会员余额和用户账单中的金额语义。当前已完成资产规则、用户余额账户、余额流水、用户账单、用户余额兑换、余额充值、Payment 成功自动入账、Payment 退款自动扣回已入账充值、Payment 平台打款边界、待支付充值单过期补偿、Web3 结算成功触发充值入账、Web3 充值收款地址分配、Web3 充值链上 Queue 自动确认、Web3 充值 ERC-20 Transfer 事件匹配、Web3 充值归集状态 contract、Web3 原生币 / ERC-20 自动归集、余额提现、提现费用模型、Web3 原生币已签名交易广播 Provider、Web3 提现签名 Provider、KMS / Vault Web3 签名 Provider SPI、Open Platform 平台账务和 Mall 余额支付消费扣款 / 售后退款。默认充值、提现和业务接入链路已具备生产可用边界,具体银行 / 三方 SDK、会员余额和积分商城作为后续可选增强。
资金域分层总览
资金能力按四层分工,业务接入时按职责选层,不跨层直改:
| 层 | 模块 | 职责 | 不负责 |
|---|---|---|---|
| 支付渠道 | spring-open-starter-payment | Facade / Manager / Provider 支付驱动:下单、查询、退款、打款和回调验签的渠道适配(mock / alipay / wechat / paypal / yungouos / web3 / bank-transfer 等) | 订单、流水、对账、补偿、资金账本 |
| 收银台 | spring-open-core-cashier | 通用支付订单(scene 路由)、支付报价 quote、渠道编排、回调 / 交易幂等 / 回调日志、对账与补偿 | 余额账本、商户结算 |
| 资金账本 | spring-open-core-money | 余额账户、余额流水(幂等 / 乐观锁 / 防负数)、充值、提现(审批 / 打款 / 确认)、兑换、统一资产目录与汇率报价 | 支付渠道适配、商户聚合 |
| 商户聚合 | spring-open-module-payment-platform | 商户入驻、聚合收款、退款、结算批次;结算只通过 MoneyBalanceLedgerService.applyLedger 写账 | 自建余额表、绕过账本直改余额 |
三条主链路:
- 支付:业务模块按
scene登记 Cashier 支付单 → 用户获取 quote 报价 → 提交支付走 Payment Provider → 回调 / 补偿确认(金额与 quote 一致性校验)→ 按scene触发成功处理器。 - 充值:用户创建充值单(
sourceType=payment走统一收银台;sourceType=web3提交链上txHash后自动确认)→ 成功后creditRecharge幂等入账。 - 提现:创建提现单并冻结 → 审批(approval-workflow,Web3 提现可配置跳过人工审核)→ 打款(
Payment.transfer/ Web3 签名广播 /bank-transfer/ 人工登记)→ 确认后解冻出账。
扩展点入口:
- 新支付渠道:实现 Payment starter 的
PaymentProviderSPI。 - 业务接支付:实现
CashierPaymentOrderSuccessHandler/CashierPaymentOrderRefundHandler,按scene注册。 - 提现打款方式:
payment/payment-<provider>内部打款、web3-signer(KMS / Vault 经部署侧 client Bean)、bank-transfer(部署侧BankTransferPaymentClientBean)。 - 提现状态联动:实现
MoneyBalanceWithdrawStatusHandler消费审核 / 打款 / 失败状态变化。 - 汇率来源:实现
MoneyRateProvider(内置 config / database / external-http / realtime-http / web3-price)。 - 业务记账(入账 / 扣减 / 预占结算 / 资产展示):直接调用
Money静态门面,不直接修改余额表;需要更底层控制时才降级到MoneyBalanceLedgerService。
路由口径:后台管理面统一挂 /api/v1/admin/money/** 与 /api/v1/admin/cashier/**;用户侧统一挂 /api/v1/money/users/me/** 与 /api/v1/cashier/**。
业务模块 5 分钟接入(Money)
业务模块给用户加钱、扣钱、预占结算或展示资产时,调用 Money 静态门面,或注入能力主入口接口 MoneyManager(两者方法一一对应,范式与 Sms / SmsManager、Storage / StorageManager 一致)。静态门面由 MoneyManagerProvider 在容器启动时自动装配;DefaultMoneyManager 是包内实现细节,业务模块不要直接依赖。
@Service
public class OrderRewardService {
public void reward(Long userId, Long orderId, String orderNo, BigDecimal amount) {
Money.credit(MoneyTransaction.of(userId, "points:MALL", amount)
.key("mall:order-reward:" + orderNo)
.business("mall-order-reward", orderId, orderNo));
}
}Money 位于 com.springopen.core.money.facade(spring-open-core-money)。账本核心的幂等、乐观锁、防负数、账户状态校验全部内置,业务代码只描述"给谁、什么资产、多少钱、什么业务、什么幂等键"。
入账与扣减
// 订单奖励入账:账户不存在时自动创建,幂等键重放不会重复加钱
Money.credit(MoneyTransaction.of(userId, "points:MALL", amount)
.key("mall:order-reward:" + orderNo)
.business("mall-order-reward", orderId, orderNo));
// 消费扣减:账户不存在直接报错,余额不足由账本防负数拒绝
Money.debit(MoneyTransaction.of(userId, "fiat:CNY", price)
.key("reading:chapter:" + chapterOrderNo)
.business("reading-chapter", chapterId, chapterOrderNo));credit默认流水类型adjust_in,可.type(MoneyBalanceLedgerTypes.RECHARGE / REFUND)覆盖;debit默认consume,可覆盖withdraw/adjust_out。- 资产用稳定资产编码字符串表达:
fiat:CNY、points:MALL、crypto:BTC、evm:bsc:token:0x...。 - 金额按账户记账精度
RoundingMode.DOWN取整,只少给不凭空造币。 - 返回
MoneyTransactionResult:APPLIED表示新流水,REPLAYED表示同幂等键重放(未重复记账)。
预占 → 结算 / 释放(先冻结后实扣)
适合"先按预估冻结、成功后按实际用量扣费、失败退回"的场景(AI 网关计费就是这个模式):
// 1. 预占:冻结预估金额
Money.hold(MoneyTransaction.of(userId, assetCode, estimated).key(requestKey).business("ai:gateway:billing"));
// 2a. 成功:解冻预占并按实际用量扣费(actual 为 0 时只留结算证据,不扣钱)
Money.capture(MoneyTransaction.of(userId, assetCode, estimated).key(requestKey).business("ai:gateway:billing"), actual);
// 2b. 失败:释放预占
Money.release(MoneyTransaction.of(userId, assetCode, estimated).key(requestKey).business("ai:gateway:billing"));三步共用同一个基础幂等键,子键由门面派生;互斥为先到语义:已结算不再释放(返回 SKIPPED_CAPTURED)、已释放不再结算(返回 SKIPPED_RELEASED),重试 / 补偿任务可以放心重放。结算 / 释放阶段只持有账户 ID 时用 MoneyTransaction.ofAccount(accountId, amount) 定位。
账户与资产展示
// 幂等确保账户存在(精度按资产类型推导,也可显式指定)
MoneyAccountView account = Money.ensureAccount(userId, "points:AI_TOKEN");
// 可用余额快捷读取:账户不存在返回 0
BigDecimal available = Money.balance(userId, "points:AI_TOKEN");
// 资产展示信息:已在统一资产目录登记时返回运营配置的展示名(如「AI 点数」)
MoneyAssetView asset = Money.asset("points:AI_TOKEN");
String label = asset.displayName();业务 VO 展示币种 / 资产时优先用 Money.asset(...).displayName(),不要把 points:AI_TOKEN 这类稳定编码直接透给前端。
单元测试替身
测试中不启动 Spring 容器时,mock MoneyManager 并用 MoneyManagerProvider 装入静态门面,用完销毁:
MoneyManager money = mock(MoneyManager.class);
MoneyManagerProvider moneyProvider = new MoneyManagerProvider(money);
moneyProvider.afterPropertiesSet();
// ... 测试逻辑,或直接注入 mock 的 MoneyManager
moneyProvider.destroy();什么时候不用 Money
- 充值 / 提现 / 兑换有独立状态机与审批链路,走
MoneyBalanceRechargeService/MoneyBalanceWithdrawService/MoneyBalanceExchangeService。 - 需要汇率快照透传、批量结算聚合等高级控制时,直接用
MoneyBalanceLedgerService.applyLedger(Money 的MoneyTransaction.rate(...)也支持常见快照透传)。 - 支付收款不碰账本:按
scene走 Cashier 收银台。
核心原则
- 法币使用 ISO 4217,例如
CNY、USD、JPY。 - 虚拟币和 Web3 资产不硬套
java.util.Currency。 - 所有业务使用稳定资产编码表示资产;源码统一使用
AssetCode值对象,value()是完整资产编码,symbol()只用于展示短码。 - Web3 金额同时保留展示金额和链上最小单位。
- 历史订单、账单、扣费和结算应保存汇率快照,不能用实时汇率回算历史金额;当前用户余额流水和账单已支持快照透传。
请求上下文与默认货币
金额展示会受到语言、国家 / 地区、默认法币和时区影响。框架统一使用请求级上下文承载这些信息,避免 App、PC、Admin 和业务模块各自推断。
前端推荐请求头:
Accept-Language: zh-CN
X-Country: CN
X-Currency: CNY
X-Time-Zone: Asia/Shanghai其中 X-Country、X-Currency、X-Time-Zone 是可选增强。用户没有主动选择时,前端至少应传 Accept-Language,浏览器或 App 能拿到时也可以传 X-Time-Zone。
服务端解析优先级:
- 前端明确传入或用户主动选择。
- 登录用户偏好。
- 服务端 GeoIP 或可信网关头推断。
Accept-Language推断语言;带地区的语言可弱推国家。- 站点默认配置。
- 框架默认:
zh-CN / CN / CNY / Asia/Shanghai。
重要边界:
Accept-Language主要决定默认语言,不应单独决定资金币种。- IP / GeoIP 适合首次访问时推荐国家和默认法币,但用户可以切换。
- 客户端传入的
X-Country/X-Currency只能作为展示偏好,不作为 KYC、税务、支付准入、风控或合规事实。 - 资金和合规场景必须结合服务端 GeoIP、账号资料、KYC、支付渠道回调、账单地址或监管规则。
- 虚拟币 / Web3 资产不从语言、国家或 IP 推断,继续使用稳定资产编码、链、合约地址、
rawAmount、decimals和链上状态。
当前底座计划:
| 层 | 说明 |
|---|---|
| core common | 提供请求上下文值对象、国家码、国家到默认法币映射 |
| Web MVC starter | 已提供请求级 ClientContext、Filter、Holder、请求头解析和 GeoIP 回退链 |
| GeoIP Provider | 已接入 noop、可信网关 Header、mica-ip2region、MaxMind;aliyun / tencent 在线 Provider 接入计划已取消 |
| Site / IAM / Money | Site 提供默认国家、默认法币、默认时区和支持货币列表;IAM 保存当前用户语言、国家 / 地区、默认展示法币和时区偏好;Money 负责余额、汇率和金额展示 |
| money 底座 | displayAmount、displayAsset、displayTime 等展示字段由后端统一生成;法币展示会读取请求级 ClientContext 的 locale / timeZone |
GeoIP Provider 第一批范围:
| Provider | 适用场景 |
|---|---|
noop | 默认占位,不推断 IP,保证本地和默认 Docker 开箱即用 |
header | 读取 Cloudflare、Nginx、SLB、CDN 或 API 网关注入的可信国家码 |
mica-ip2region | 国内离线 IP 库,使用 net.dreamlu:mica-ip2region,默认可读取依赖内置库,也支持私有库路径 |
maxmind | 国外 / 国际通用 Provider,使用 com.maxmind.geoip2:geoip2 读取部署方提供的 GeoLite2 / GeoIP2 离线库,减少请求期外部依赖和隐私外传 |
阿里云 / 腾讯云在线 IP 查询接口当前不作为内置 Provider,且已取消接入计划。需要云厂商在线能力时,建议先由 CDN / 网关注入可信国家码,再使用 header Provider。
推荐 GeoIP 链路:
| 场景 | 默认 Provider | 回退链 |
|---|---|---|
| 开箱默认 | noop | noop |
| 生产有 CDN / 网关国家码 | header | mica-ip2region -> maxmind -> noop |
| 国内私有化 / 国内服务器 | mica-ip2region | maxmind -> noop |
| 海外 / 国际化部署 | maxmind | mica-ip2region -> noop |
资产编码
AssetCode 是通用资产编码值对象。
AssetCode.fiat("CNY"); // fiat:CNY
AssetCode.crypto("BTC"); // crypto:BTC
AssetCode.web3Native("bsc"); // evm:bsc:native
AssetCode.web3Token("bsc", "USDT", "0xabc"); // evm:bsc:token:0xabc
AssetCode.points("ai_token"); // points:AI_TOKEN
AssetCode.parse("evm:bsc:token:0xabc");
AssetCode.resolve(null, "fiat", "CNY", null, null); // 松散字段兜底解码resolve 统一「优先用已有稳定资产编码,否则按资产分类 + 字段组装」的解码规则,业务层不再各自手写 switch。
常见字段:
| 字段 | 说明 |
|---|---|
assetCode | 稳定资产编码,例如 fiat:CNY、crypto:USDT、evm:bsc:token:0xabc、points:AI_TOKEN |
kind | fiat、crypto、web3-native、web3-token、points |
symbol | CNY、USDT、BTC、AI_TOKEN 等展示短码 |
chain | Web3 链标识,法币为空 |
contractAddress | Token 合约地址,原生币和法币为空 |
资金表为查询便利会冗余保存资产分类、展示短码、链、合约地址和精度等快照列;写入时必须通过模块内资产列 helper 从 AssetCode 或账户快照统一复制,避免业务 Service 手工逐列赋值导致这些列与稳定资产编码漂移。
当前 HTTP DTO 和数据库列统一使用 assetCode 承载稳定 AssetCode(例如 fiat:CNY、evm:bsc:token:<contract>、points:AI_TOKEN)。新增业务代码应以 AssetCode 作为领域值对象,展示短码单独使用 symbol。
积分资产同样走 Money 中性账户和流水 contract。业务模块创建积分账户时优先使用 EnsureMoneyBalanceAccountCommand.points(userId, code, remark),后端会统一生成 points:CODE,并固定 decimals=0;写入积分流水时优先使用 ApplyMoneyBalanceLedgerCommand.pointsLedger(...),仍然由余额流水状态机负责幂等、防负数、快照和精度校验。
统一资产目录
Money 维护统一资产目录 money_asset,作为支付报价、余额兑换、Web3 Token 支付、商户结算和后台运维的资产准入事实源。资产目录只描述资产本身,不代表某个资产对一定允许兑换或支付;汇率、手续费、通道路由和风控仍由各自规则决定。
资产目录核心字段:
| 字段 | 说明 |
|---|---|
assetCode | 稳定资产编码,例如 fiat:CNY、crypto:USDT、evm:bsc:native |
kind | fiat、crypto、web3-native、web3-token、points |
symbol | 业务短码和展示符号 |
displayName | 后台和前端展示名称 |
fiatCurrency | 法币资产对应 ISO 4217 代码 |
chain / chainId / contractAddress | Web3 原生币和 Token 的链与合约信息 |
decimals / displayDecimals | 计算精度和展示精度 |
enabled / sortOrder | 是否可用和展示排序 |
默认 seed 会写入常用法币 fiat:CNY、fiat:USD、fiat:EUR、fiat:HKD、fiat:TWD、fiat:JPY、fiat:GBP、fiat:AUD、fiat:CAD、fiat:SGD,以及 crypto:USDT、BSC BNB、BSC USDT、BSC USDC、Ethereum ETH、Ethereum USDT 和 Ethereum USDC,方便本地支付、余额和 Web3 报价链路开箱调试。生产环境应按实际可支持的法币、Token 和积分资产补齐目录,并禁用未开放资产。
后台资产目录接口:
| 方法 | 路径 | 说明 |
|---|---|---|
GET | /api/v1/admin/money/assets | 分页查看统一资产目录 |
POST | /api/v1/admin/money/assets/quote | 试算定价资产到支付资产的统一报价 |
GET | /api/v1/admin/money/assets/{id} | 查看资产详情 |
POST | /api/v1/admin/money/assets | 创建资产目录记录 |
PUT | /api/v1/admin/money/assets/{id} | 更新资产目录记录 |
POST | /api/v1/admin/money/assets/{id}/enable | 启用资产 |
POST | /api/v1/admin/money/assets/{id}/disable | 禁用资产 |
资产报价示例:
{
"baseSymbol": "fiat:CNY",
"baseAmount": 100,
"paySymbol": "crypto:USDT",
"provider": "database",
"scene": "cashier"
}同资产报价会直接返回 rateProvider=same-asset、rate=1 和相同的应付金额;跨资产报价会复用 Money 汇率 Provider 链,包括配置汇率、数据库汇率、外部 HTTP 和 Web3 price Provider。报价结果会返回定价资产、支付资产、汇率、反向汇率、来源、快照时间、过期时间、置信度和应付金额。Money 已通过 CashierPaymentQuoteRatePort 为 Cashier 支付 quote 生命周期提供资产报价适配;业务模块和 Payment Provider 后续应保存 Cashier quote 快照,不再另算汇率。
报价、支付和结算接入时应先校验资产目录存在且启用。历史支付订单、报价、账本和结算单必须保存资产快照字段,不能在资产目录修改后回算历史数据。
法币
CurrencyCodes 提供常用法币常量和 ISO 4217 校验。
CurrencyCodes.DEFAULT; // CNY
CurrencyCodes.normalize("cny"); // CNY
CurrencyCodes.isSupported("USD"); // true
CurrencyCodes.fractionDigits("JPY"); // 0后续业务保存法币金额时,应同时保存:
currencyamount- 必要时保存
exchangeRate、rateProvider、rateSnapshotAt
金额换算
MoneyAmounts 负责金额格式和 Web3 最小单位换算。
MoneyAmounts.fromRaw(new BigInteger("1234500"), 6); // 1.2345
MoneyAmounts.toRaw(new BigDecimal("1.2345"), 6); // 1234500
MoneyAmounts.plain(new BigDecimal("1.2300")); // 1.23toRaw 使用精确换算。如果金额小数位超过资产精度,会抛出 ArithmeticException,避免静默截断资金金额。
业务计算中产生超出资产精度的小数时,资金口径统一使用 RoundingMode.DOWN 直接截断,不四舍五入、不向上取整。当前覆盖余额兑换目标金额 / 反推汇率、兑换手续费、提现百分比手续费、Mall 折扣金额、Open Platform AI 网关预占 / 实扣金额、用量成本快照、模型价格目录和毛利统计;用户或外部请求传入的已成型金额仍使用 UNNECESSARY 校验精度,超过资产精度时拒绝。
AssetAmount 用于把金额和资产绑定在一起:
AssetAmount amount = AssetAmount.fromRaw(
AssetCode.web3Token("bsc", "USDT", "0xabc"),
new BigInteger("1234500"),
6
);AssetAmount 上的运算强制同资产:plus / minus / compareAmount 在 assetCode 或 decimals 不一致时直接抛 IllegalArgumentException,避免跨币种、跨精度静默混算。业务层按需把守卫失败翻译成自己的错误码(例如 Mall 下单合计把它映射为 MALL_ORDER_CURRENCY_MISMATCH)。 当 AssetAmount 带有 decimals 时,plus / minus 会同步重算 rawAmount;如果金额无法按该精度精确换算,也会通过 ArithmeticException 暴露,不做截断。
AssetAmount.fiat("CNY", new BigDecimal("1.20"))
.plus(AssetAmount.fiat("CNY", new BigDecimal("0.80"))); // 2.00取整策略和尾差分配
资金取整必须显式选择策略,不能在业务代码里随手调用 setScale。默认规则:
| 场景 | 推荐入口 | 说明 |
|---|---|---|
| 用户 / 外部系统传入的已成型金额 | MoneyRoundingPolicy.EXACT | 超过资产精度直接拒绝 |
| 系统内部派生资金金额 | MoneyRoundingPolicy.DOWN 或 defaultInternal() | 直接截断,不四舍五入、不向上取整 |
| 税费 / 发票 / 渠道清结算明确要求 | HALF_UP / HALF_EVEN | 只在外部规则明确要求时使用 |
| 分账 / 多人分摊 / 批量结算 | MoneyAllocations.allocateEqually / allocateByWeights | 先按最小单位截断,再把剩余最小单位确定性分配 |
MoneyRoundingPolicy.EXACT.apply(new BigDecimal("1.23"), 2); // 1.23
MoneyRoundingPolicy.defaultInternal().apply(new BigDecimal("1.239"), 2); // 1.23
MoneyAllocation allocation = MoneyAllocations.allocateEqually(
AssetAmount.of(AssetCode.fiat("CNY"), new BigDecimal("0.10"), 2),
List.of("merchant-a", "merchant-b", "merchant-c")
);上面 0.10 CNY 三方均分时,每方基础金额先截断为 0.03,剩余 0.01 会按入参顺序分给第一方;返回的 MoneyAllocationLine 同时包含 baseAmount、adjustmentAmount 和 finalAmount,业务落账时应把 adjustment 作为尾差调整留痕,而不是静默丢弃。
时间格式
通用展示时间使用 DateTimePatterns / DateTimeFormatters。
DateTimePatterns.NORMAL_DATETIME; // yyyy-MM-dd HH:mm:ss
DateTimeFormatters.NORMAL_DATETIME.format(value);业务接口仍可以按场景返回 ISO 时间或时间戳;用户可见展示文案需要统一时,优先复用这些常量,避免各模块各自定义格式。
用户余额账户
用户余额账户是后续充值、提现、扣费、退款、冻结和账单的基础表。账户接口本身只负责“账户存在”和“账户状态”;后台手动增加或扣减余额时,必须调用专用调账入口写入 Money 余额流水,不允许直接修改余额字段。
核心规则:
- 一个用户同一种资产只有一个账户,唯一键为
userId + assetCode;其中assetCode字段值必须是规范AssetCode。 - 创建接口是幂等的,重复提交同一资产会返回已有账户。
- 账户初始可用余额、冻结余额、累计收入和累计支出均为
0。 - 非零账户不能删除,后续必须通过流水、冻结、解冻、退款等受控能力变更余额。
- 软删除账户再次创建时会恢复原账户,避免唯一键冲突和历史资产关系丢失。
后台运维接口:
| 方法 | 路径 | 说明 |
|---|---|---|
GET | /api/v1/admin/money/user-balance-accounts | 分页查询用户余额账户 |
GET | /api/v1/admin/money/user-balance-accounts/{id} | 查询账户详情 |
POST | /api/v1/admin/money/user-balance-accounts | 幂等创建用户余额账户 |
PUT | /api/v1/admin/money/user-balance-accounts/{id} | 修改账户状态和备注 |
POST | /api/v1/admin/money/user-balance-accounts/{id}/adjust | 后台手动调增 / 调减余额,写入 adjust_in / adjust_out 流水 |
DELETE | /api/v1/admin/money/user-balance-accounts/{id} | 删除零余额账户 |
DELETE | /api/v1/admin/money/user-balance-accounts | 批量删除零余额账户 |
当前用户接口:
| 方法 | 路径 | 说明 |
|---|---|---|
GET | /api/v1/money/users/me/balance-accounts | 当前用户余额账户分页 |
POST | /api/v1/money/users/me/balance-accounts | 当前用户幂等创建余额账户 |
创建账户可以直接传现有 API 字段 assetCode,字段值使用规范 AssetCode:
{
"userId": 1,
"assetCode": "fiat:CNY"
}也可以传资产组件,由后端统一生成稳定 assetCode(即 AssetCode 值):
{
"userId": 1,
"assetType": "web3-token",
"symbol": "USDT",
"chain": "bsc",
"contractAddress": "0xabc..."
}返回字段包含 displayName、displayAsset、displayAvailableBalance、displayFrozenBalance、displayTotalIncome、displayTotalExpense、displayCreatedAt、displayUpdatedAt,前端优先直接展示这些字段,不需要自行拼接链名、资产短码、金额和时间。Web3 平台内余额同样使用这些字段展示,例如 evm:bsc:token:<contract> 会由后端展示为 USDT / bsc。
用户余额流水
用户余额流水是余额变更的唯一入口。充值、消费、退款、冻结、解冻、提现和后台调整都必须通过流水服务完成,禁止业务模块直接修改账户余额。
核心规则:
- 每次余额变更必须传入
idempotencyKey,重复提交同一个幂等键会直接返回已有流水,不会重复改余额。 - 流水保存业务来源:
businessType、businessId、businessNo、sourceType、sourceId、operatorId、traceId。 - 流水保存可用余额、冻结余额、累计收入和累计支出的变更前后快照,方便后续对账和审计。
- 涉及跨币种展示、结算、兑换或扣费时,流水可保存汇率快照:
exchangeRate、rateProvider、rateSnapshotAt。历史账单和审计必须读取快照,不用事后实时汇率反推。 - 账户停用时不能写入新流水。
- 扣减可用余额或解冻冻结余额时,后端会检查余额不能为负数。
- 金额精度按账户
decimals校验,例如fiat:CNY默认只允许 2 位小数。 - 积分账户固定
decimals=0,因此积分入账、消费、退款和调整只能写入整数,禁止1.5这类小数积分。
第一阶段已支持的流水类型:
| 类型 | 语义 |
|---|---|
recharge | 充值入账,可用余额增加,累计收入增加 |
consume | 消费扣款,可用余额减少,累计支出增加 |
refund | 退款入账,可用余额增加,累计收入增加 |
freeze | 冻结余额,可用余额减少,冻结余额增加 |
unfreeze | 解冻余额,可用余额增加,冻结余额减少 |
withdraw | 提现扣款,可用余额减少,累计支出增加 |
exchange_out | 余额兑换出账,可用余额减少,累计支出增加 |
exchange_in | 余额兑换入账,可用余额增加,累计收入增加 |
exchange_fee | 余额兑换手续费扣款,可用余额减少,累计支出增加 |
adjust_in | 后台调增,可用余额增加,累计收入增加 |
adjust_out | 后台调减,可用余额减少,累计支出增加 |
后台运维接口:
| 方法 | 路径 | 说明 |
|---|---|---|
GET | /api/v1/admin/money/user-balance-ledgers | 分页查询用户余额流水 |
GET | /api/v1/admin/money/user-balance-ledgers/{id} | 查询流水详情 |
当前用户接口:
| 方法 | 路径 | 说明 |
|---|---|---|
GET | /api/v1/money/users/me/balance-ledgers | 当前用户余额流水分页 |
业务模块接入时应调用后端 Service,不直接开放“任意写流水”的通用用户接口。Mall 余额支付已经通过应用装配层调用用户余额流水写入 consume 扣款记录;Mall 售后退款成功时会按 订单号 + 售后单 幂等写入 refund 流水,把订单售后状态和余额账本联动起来。Marketing 的 balance 抽奖奖项通过中立 MoneyBalanceRewardRequestedEvent 交给 Money 写入用户余额 adjust_in 流水,幂等键为 money:balance:marketing-lottery:{userId}:{operationKey},业务类型为 marketing-lottery-reward;Marketing 不直接依赖 Money Service,也不自建余额表。后续会员充值、Web3 到账、退款和提现继续沿用同一套幂等与审计规则。
流水响应会返回 displayAsset、displayAmount、displayAvailableDelta、displayFrozenDelta、displayAfterAvailableBalance 和 displayTime。这些字段只用于展示,真实对账仍以原始 amount、assetCode、余额快照、汇率快照和发生时间为准。
用户余额兑换
用户余额兑换是在现有币价和汇率快照之上的真实业务单据层,用于把用户某一种余额资产兑换成另一种余额资产,例如 fiat:CNY 兑换 fiat:USD、fiat:CNY 兑换 evm:bsc:token:<contract>、evm:bsc:token:<contract> 兑换 evm:bsc:native。
核心规则:
- 兑换必须经过白名单资产账户,不允许绕过余额账户直接改金额。
- 创建兑换会生成一张兑换单,并写入幂等余额流水:
exchange_out扣减来源账户本金,exchange_in增加目标账户本金;存在手续费时追加exchange_fee独立扣款流水。 - 后端会保存兑换单号、来源账户、目标账户、来源资产、目标资产、来源金额、目标金额、汇率、汇率 Provider、汇率快照时间和幂等键。
quote接口只计算预估目标金额,不写兑换单、不写流水,适合前端确认页。- 当前用户兑换默认使用平台内置配置汇率,数据库初始化也会写入同一组发布态默认汇率;前端只提交来源资产、目标资产和来源金额,用户侧传入的
toAmount、exchangeRate、rateProvider和rateSnapshotAt会被忽略,避免客户端篡改汇率。 - 当前用户来源资产必须来自自己的余额账户,目标资产可从已启用的统一资产目录选择;创建兑换时后端会自动确保目标余额账户存在。
- 后台运维接口可以显式传入目标金额或兑换汇率,用于人工调账或特殊运营场景;未传时同样回退到平台内部汇率。
- 没有命中任何汇率 Provider 且后台也没有显式传入目标金额或兑换汇率时会拒绝。
- 可选启用
web3-price或realtime-http汇率 Provider:前者复用 Web3 starter 的价格 Provider 查询同链 ERC-20 / Token 对行情,后者接企业自有或第三方实时行情 HTTP 服务;两者都只生成兑换报价快照,不触发链上 Swap。 - 来源资产和目标资产不能相同。
- 兑换试算会返回
rateExpiresAt和displayRateExpiresAt;Provider 返回的报价如果已经过期,后端会拒绝继续创建真实兑换单。 - 兑换手续费由后端按配置规则计算,默认生产配置匹配所有资产对并按来源金额扣
0.3%。quote会返回fees[],创建兑换时会把手续费快照保存到兑换单。source扣费模式会对来源账户生成独立exchange_fee扣款流水;target扣费模式会先按目标资产 gross 金额写入exchange_in,再对目标账户生成独立exchange_fee扣款流水,最终到账仍等于扣费后的 net 金额。 - 兑换历史账单会归类为
exchange,前端可直接展示displayFromAmount、displayToAmount、displayRateSnapshotAt等字段。
后台运维接口:
| 方法 | 路径 | 说明 |
|---|---|---|
GET | /api/v1/admin/money/user-balance-exchanges | 分页查询用户余额兑换单 |
GET | /api/v1/admin/money/user-balance-exchanges/{id} | 查询兑换详情 |
POST | /api/v1/admin/money/user-balance-exchanges/quote | 后台试算兑换金额 |
POST | /api/v1/admin/money/user-balance-exchanges | 后台创建兑换单并写入双向流水 |
GET | /api/v1/admin/money/assets | 分页查看统一资产目录 |
GET | /api/v1/admin/money/rate-providers | 只读查看当前汇率 Provider 及规则数量 |
GET | /api/v1/admin/money/rates | 查看平台内配置汇率和数据库汇率规则 |
POST | /api/v1/admin/money/rates | 新增数据库汇率规则,可保存草稿或直接发布 |
PUT | /api/v1/admin/money/rates/{id} | 修改数据库汇率规则,可保存草稿或直接发布 |
POST | /api/v1/admin/money/rates/{id}/publish | 发布数据库汇率规则 |
POST | /api/v1/admin/money/rates/{id}/rollback | 回滚数据库汇率规则 |
GET | /api/v1/admin/money/exchange-fee-rules | 只读查看平台内兑换手续费规则 |
当前用户接口:
| 方法 | 路径 | 说明 |
|---|---|---|
GET | /api/v1/money/users/me/balance-exchanges/assets | 查询当前用户可选择的启用资产目录,用作兑换目标资产候选 |
GET | /api/v1/money/users/me/balance-exchanges | 当前用户余额兑换分页 |
POST | /api/v1/money/users/me/balance-exchanges/quote | 当前用户试算兑换金额 |
POST | /api/v1/money/users/me/balance-exchanges | 当前用户创建兑换单 |
示例:
{
"fromAssetCode": "fiat:CNY",
"fromAmount": "100.00",
"toAssetCode": "fiat:USD",
"idempotencyKey": "exchange:sample:10001"
}上述用户侧请求会按平台配置的汇率 Provider 扣减 100.00 CNY,并写入对应 USD 余额。真实资金对账以余额流水和兑换单快照为准;无论使用 internal-fixed、database、web3-price 还是 realtime-http,都必须先生成快照再创建兑换单,不能用事后实时价格覆盖历史兑换结果。
/api/v1/admin/money/assets 读取统一资产目录,返回 assetCode、assetType、symbol、displayName、fiatCurrency、chain、chainId、contractAddress、decimals、displayDecimals、enabled 和 displayAsset 等字段。它是报价、支付和结算的资产准入入口;资产存在不代表任意资产对可兑换,汇率规则、手续费规则、Payment Provider 能力和 Web3 风控仍会继续限制实际可用组合。
内部汇率配置:
spring:
open:
money:
balance:
exchange:
rate:
default-provider: internal-fixed
quote-validity: 2m
rules:
- enabled: true
from-asset-code: fiat:CNY
to-asset-code: fiat:USD
provider: internal-fixed
rate: 0.140000000000000000
bidirectional: true
- enabled: true
from-asset-code: fiat:USD
to-asset-code: crypto:USDT
provider: internal-fixed
rate: 1.000000000000000000
bidirectional: true
- enabled: true
from-asset-code: evm:bsc:native
to-asset-code: evm:bsc:token:0x55d398326f99059ff775485246999027b3197955
provider: internal-fixed
rate: 666.666666666666666666
bidirectional: truerate 的含义是 1 个 from-asset-code 可兑换多少 to-asset-code。bidirectional=true 时系统会按倒数支持反向兑换,例如 fiat:USD 到 fiat:CNY。quote-validity 是试算锁价有效期,默认 2m,小于等于 0 时只返回汇率快照时间,不返回过期时间。平台内余额兑换不会触发链上 Swap;Web3 原生币和 Token 作为用户余额资产参与兑换,真实链上划转仍由充值、提现和归集链路负责。
应用默认配置会启用一组开箱基准价:fiat:CNY / fiat:USD、fiat:USD / crypto:USDT、crypto:USDT / BSC USDT、fiat:USD / BSC USDT、BSC BNB / BSC USDT。BNB 等波动资产的默认值只用于安装后完成兑换链路验证;生产资金口径应通过后台数据库汇率发布或 realtime-http / web3-price Provider 接入实时行情后再开放大额兑换。
数据库汇率管理:
- 数据库汇率规则通过
source=database标识,只能由后台接口新增、修改、发布和回滚;配置文件汇率仍保持只读展示。 - 初始化迁移会写入
source=bootstrap-default的发布态数据库汇率种子,覆盖默认资产目录中的 CNY、USD、USDT、BSC USDT 和 BSC BNB 常用组合;运营可在后台新建更高版本规则并发布,或回滚默认种子。 - 发布时如果未传
effectiveFrom,系统默认立即生效;未传versionNo时会按同一资产对已有版本自动递增。 - 已发布规则会设置
enabled=true、status=published和publishedAt;回滚会设置status=rolled_back、enabled=false、effectiveTo=now,保留历史快照供审计。 grayRatio支持0..100,grayUserIds可记录灰度用户 ID;数据库 Provider 只使用已发布且处于生效窗口内的规则参与报价。- 未显式指定
rateProvider时,数据库 Provider 会作为报价链候选参与 fallback:如果配置汇率或默认 Provider 没有可用报价,已发布数据库规则仍可承接当前用户兑换;显式指定其它 Provider 时不会误命中数据库规则。 - Admin 资金运维页支持新增数据库汇率、编辑草稿、保存并发布、发布和回滚;余额账户、资金流水、账单、充值、提现和兑换记录仍保持只读核对。
Web3 外部行情 Provider 配置:
spring:
open:
money:
balance:
exchange:
rate:
default-provider: web3-price
quote-validity: 2m
web3-price:
enabled: true
provider-name: pancake-v2-pool
quote-validity: 30sweb3-price.enabled=false 是默认值,避免本地和私有部署在未配置 Web3 price provider 时访问外部行情。启用后,系统会把 evm:<chain>:<symbol>:<contract> 到同链另一个 Token 的报价请求转给 Web3 starter 的 Web3PriceClient,并把实际价格来源写入汇率快照。provider-name 为空时使用 Web3 starter 的默认价格 Provider;显式请求 rateProvider=web3-price 时,如果底层 Web3 provider 报错,会向调用方暴露异常,便于运维发现行情配置问题。未显式指定 provider 时,web3-price 报错会返回空报价,让 Provider 链继续回退到后续候选。
外部 HTTP 汇率 Provider 配置:
spring:
open:
money:
balance:
exchange:
rate:
default-provider: external-http
external-http:
enabled: true
url: https://rates.example.com/quote
method: GET
timeout: 5s
quote-validity: 30s
cache-ttl: 30s
stale-ttl: 10m
token: ${RATE_SERVICE_TOKEN:}
token-header: Authorization
token-prefix: "Bearer "
rate-path: data.rate
provider-path: data.provider
source-path: data.source
snapshot-at-path: data.snapshotAt
confidence-path: data.confidenceexternal-http.enabled=false 是默认值,避免本地和私有部署启动后访问外部报价服务。启用后,系统支持 GET 查询参数或 POST JSON 请求,发送字段包含 userId、fromAssetCode、toAssetCode、fromAmount、scene、provider=external-http 和 requestedAt。响应至少需要按 rate-path 解析出大于 0 的汇率;provider、source、snapshotAt 和 confidence 可通过对应路径读取,缺省时分别使用 external-http、请求 host、请求时间和 1。
cache-ttl 控制成功报价的短时内存复用,stale-ttl 控制外部服务失败后可使用最近成功报价兜底的时间窗口。命中 stale 兜底时,报价会标记 stale=true 且置信度降为 0.5,调用方仍必须把汇率、provider、source、snapshotAt 和 raw payload 写入兑换快照,后续配置或实时行情变化不能覆盖历史兑换结果。即使默认 Provider 不是 external-http,用户或后台显式传入 rateProvider=external-http 也会尝试该 Provider。
实时 HTTP 行情 Provider 配置:
spring:
open:
money:
balance:
exchange:
rate:
default-provider: realtime-http
realtime-http:
enabled: true
url: https://rates.example.com/realtime
method: GET
timeout: 3s
quote-validity: 30s
cache-ttl: 10s
stale-ttl: 2m
token: ${RATE_SERVICE_TOKEN:}
token-header: Authorization
token-prefix: "Bearer "
rate-path: data.rate
provider-path: data.provider
source-path: data.source
snapshot-at-path: data.snapshotAt
confidence-path: data.confidencerealtime-http 与 external-http 使用同一套 HTTP 请求和响应解析 contract,但 Provider 名独立,适合把余额兑换切到企业实时行情聚合服务。启用后,请求字段中的 provider 为 realtime-http;响应未返回 provider 时,兑换快照也会记录为 realtime-http。显式传 rateProvider=realtime-http 会强制使用实时行情;若要让实时行情成为默认报价口径,把 default-provider 设为 realtime-http 即会优先尝试该 Provider,失败或无报价时再回退到数据库 / 配置汇率等后续候选。
兑换手续费配置:
spring:
open:
money:
balance:
exchange:
fee:
rules:
# 更具体的资产对规则放前面,优先命中。
- enabled: true
from-asset-code: fiat:CNY
to-asset-code: fiat:USD
calculation-type: percent
rate: 0.010000000000000000
min-amount: 1.00
max-amount: 20.00
deduct-mode: source
- enabled: true
from-asset-code: fiat:USD
to-asset-code: evm:bsc:token:0x0000000000000000000000000000000000000000
calculation-type: fixed
amount: 1.00
deduct-mode: target
# 默认生产口径:匹配所有资产对,从来源账户按 0.3% 扣除兑换手续费。
# 全局兜底规则应放在最后,避免覆盖上面的具体资产对规则。
- enabled: true
calculation-type: percent
rate: 0.003
deduct-mode: sourcecalculation-type=percent 表示按基准金额百分比扣费,rate=0.003 即 0.3%;calculation-type=fixed 表示固定数量扣费,读取 amount。deduct-mode=source 表示手续费与来源资产一起扣除,例如用户用 100 CNY 兑换 USD,手续费 0.30 CNY 时来源账户实际出账 100.30 CNY。deduct-mode=target 表示手续费从目标到账资产中扣除,例如应到账 14 USD、手续费 1 USD 时目标账户实际入账 13 USD。兑换单会保存手续费资产、计算方式、费率、基准金额、扣费方式等快照,后续汇率或配置变化不会影响历史兑换结果。
后台资金运维入口会把当前应用装配后的汇率 Provider、配置汇率、数据库汇率和手续费规则返回给管理端。配置汇率和手续费规则仍以配置文件或配置中心配置为准;数据库汇率可在 /api/v1/admin/money/rates/** 内完成草稿、发布和回滚闭环。
用户账单
用户账单是面向用户和后台运维的展示视图,不是新的账务数据源。它由余额流水投影生成,用于把底层审计流水整理成更容易理解的账单列表。
核心规则:
- 账单不重复保存金额数据,底层事实仍以余额流水为准。
- 账单按业务来源归类,例如充值、消费、退款、提现、商城、支付、会员、积分、Web3、开放平台等。
- 账单返回
categoryDisplayName、typeDisplayName、directionDisplayName、title等可直接展示字段,前端不需要维护硬编码翻译表。 - 账单返回
displayAsset、displayAmount、displaySignedAmount、displayBalanceAfter和displayTime,用于列表和详情页直接展示;Web3 资产会按assetCode解析链名和资产短码,前端不要重新拼接。 - 账单透传底层流水的
exchangeRate、rateProvider、rateSnapshotAt,用于展示或审计历史汇率来源。 - 账单返回
signedAmount,收入为正数,支出为负数,冻结 / 解冻属于划转方向。 - 当前用户账单不暴露幂等键、余额变更前快照等底层审计字段;需要审计时使用后台余额流水接口。
后台运维接口:
| 方法 | 路径 | 说明 |
|---|---|---|
GET | /api/v1/admin/money/user-bills | 分页查询用户账单展示视图 |
当前用户接口:
| 方法 | 路径 | 说明 |
|---|---|---|
GET | /api/v1/money/users/me/bills | 当前用户账单分页 |
账单常用筛选项:
| 参数 | 说明 |
|---|---|
assetCode / assetType / symbol | 按资产筛选 |
changeType | 按底层流水类型筛选,例如 recharge、consume、refund |
businessType / businessId | 按业务来源筛选,例如商城订单、会员充值、Web3 到账 |
keyword | 匹配业务单号、业务类型、备注等展示关联字段 |
用户余额充值
用户余额充值是充值流程的业务单据层,负责把支付订单、链上充值确认和余额流水串起来。第一阶段已完成通用充值单、Payment 订单预创建、后台受控入账、Payment 成功自动入账、Payment 退款自动扣回、Web3 结算成功联动入账、Web3 充值收款地址分配、Web3 充值链上确认运维入口、Queue 自动确认、ERC-20 Transfer 事件匹配、归集状态 contract,以及原生币 / ERC-20 自动归集任务。
核心规则:
- 充值单保存
rechargeNo,作为业务侧稳定单号。 - 默认来源为
payment,可预留web3、manual等来源。 - 默认资产为
fiat:CNY,也可以传入assetCode或资产组件。 - 创建充值单会幂等创建用户余额账户。
- Payment 可用时,后端会以充值单号创建支付订单,支付订单的
scene为balance-recharge。 - Payment 异步通知或补偿查询确认订单成功后,会按支付订单号查找充值单并自动触发幂等入账。
- Web3 支付结算执行器在订单条件更新为成功并写入
web3-settlement支付流水后,会分发 Payment 成功处理器;成功处理器完成后还会写入money-web3-settlementOutbox 事件,供后续重放 / 失败观测。余额充值场景通过同一balance-rechargescene 读取交易哈希和确认数并幂等入账。 - 真正入账必须走余额流水,流水类型为
recharge,幂等键为充值单幂等键。 - 已入账充值再次确认不会重复写流水。
- Payment 退款回调会按充值支付订单查找已入账充值,并写入
adjust_out流水扣回余额;幂等键由充值单号和退款单号 / 退款 ID / 交易号组成,重复退款回调不会重复扣款。 - 如果账户余额不足,余额流水会拒绝扣回,避免账户出现负数;这类异常需要进入后台支付 / 账务运维处理。
- 带
expiredAt的待支付充值单会由过期补偿任务自动标记失败,不写余额流水;该任务只处理sourceType=payment、充值状态pending、支付状态pending的单据,不会误伤已入账、已支付待补偿或 Web3 待确认充值。 - 后台确认入账是运维 / 补偿入口,正式支付回调、补偿查询和 Web3 确认都复用同一 Service contract。
sourceType=web3的充值单可以在创建时写入txHash、confirmations、requiredConfirmations,不会预创建 Payment 支付订单。sourceType=web3的充值单可以记录链上付款地址payerAddress、平台收款地址receiveAddress、归集状态collectionStatus、归集交易哈希collectionTxHash和归集时间collectedAt。- 创建 Web3 充值单时,如果请求未传
receiveAddress,后端会按spring.open.money.web3.recharge.receive-addresses中的链、资产符号和合约地址分配平台收款地址;请求显式传入的收款地址仍最高优先。 - 内置
user-balance-recharge-erc20-transfer扫描器可匹配 ERC-20Transfer事件:当链、合约地址、平台收款地址和链上金额与待处理充值单一致时,自动写入付款地址、收款地址和txHash。 - Web3 充值链上确认会查询链上交易状态:未出块或确认数不足时保持待处理;链上成功且达到确认数后复用充值入账 Service;链上失败则标记充值失败且不写余额流水。
- Web3 充值自动确认任务默认由 Queue 触发处理
pending / paid + web3 + txHash的充值单,复用同一确认 Service 推进状态,不负责监听充值地址、归集资金、签名或广播;只有显式开启固定调度时才周期补扫。 - 后台归集标记入口只记录归集处理结果和审计字段,不触碰私钥、不签名、不广播链上交易。
- Web3 充值自动归集任务处理已入账、归集状态为
pending、已分配平台收款地址的充值单;contractAddress为空时按原生币归集,存在 ERC-20 合约地址时按 Token 归集。 - 自动归集默认关闭。只有显式配置托管钱包私钥、托管地址和归集目标地址后,任务才会调用 Web3 starter 的原生币资金归集或 ERC-20 转账能力签名并广播。
后台运维接口:
| 方法 | 路径 | 说明 |
|---|---|---|
GET | /api/v1/admin/money/user-balance-recharges | 分页查询用户余额充值 |
GET | /api/v1/admin/money/user-balance-recharges/{id} | 查询充值详情 |
POST | /api/v1/admin/money/user-balance-recharges | 后台创建用户余额充值单 |
POST | /api/v1/admin/money/user-balance-recharges/{id}/credit | 后台确认充值入账 |
POST | /api/v1/admin/money/user-balance-recharges/{id}/confirm-web3 | 查询 Web3 充值链上确认状态 |
POST | /api/v1/admin/money/user-balance-recharges/{id}/collection | 标记 Web3 充值归集状态 |
当前用户接口:
| 方法 | 路径 | 说明 |
|---|---|---|
GET | /api/v1/money/users/me/balance-recharges | 当前用户充值记录分页 |
POST | /api/v1/money/users/me/balance-recharges | 当前用户创建充值单 |
充值响应会返回 displayAsset、displayAmount、displayTime、displayPaidAt、displayCreditedAt、displayCollectedAt、displayExpiredAt。其中 displayTime 默认取充值单创建时间,其他展示时间按实际状态字段生成。sourceType=web3 的充值单也使用同一套展示字段,链名、Token 短码和金额格式由后端统一生成。
当前用户创建法币充值示例:
{
"assetCode": "fiat:CNY",
"amount": 100,
"provider": "alipay",
"idempotencyKey": "user-7-recharge-20260531-001"
}当前用户创建 Web3 充值示例:
{
"assetCode": "evm:bsc:token:0x0000000000000000000000000000000000000001",
"assetType": "web3-token",
"symbol": "USDT",
"chain": "bsc",
"contractAddress": "0x0000000000000000000000000000000000000001",
"decimals": 18,
"amount": 1.23,
"sourceType": "web3",
"txHash": "0x...",
"confirmations": 0,
"requiredConfirmations": 12,
"idempotencyKey": "user-7-web3-recharge-001"
}后台确认入账示例:
{
"transactionId": "202605310001",
"payerId": "payer-001",
"traceId": "trace-001",
"remark": "支付回调补偿确认"
}Web3 充值链上确认示例:
{
"requiredConfirmations": 12,
"traceId": "trace-web3-recharge-001",
"remark": "后台链上确认"
}Web3 充值归集标记示例:
{
"collectionStatus": "collected",
"collectionTxHash": "0x...",
"traceId": "trace-web3-recharge-collection-001",
"remark": "后台确认已完成归集"
}Web3 充值会在充值单上继续使用 txHash、confirmations、requiredConfirmations、payerAddress、receiveAddress、collectionStatus 等字段,不单独发明另一套到账模型。当前链上确认入口适合后台人工补偿和运维确认;地址分配适合给用户生成平台收款地址;事件匹配入口适合把 ERC-20 转账日志绑定到充值单;归集标记入口适合记录人工或外部任务的归集结果;自动归集任务适合托管原生币充值地址的场景,继续沿用同一张充值单和余额流水幂等规则。
Web3 充值收款地址配置:
spring:
open:
money:
web3:
recharge:
receive-addresses:
- enabled: true
chain: bsc
symbol: USDT
contract-address: 0x0000000000000000000000000000000000000001
address: 0x2222222222222222222222222222222222222222配置说明:
| 配置 | 说明 |
|---|---|
enabled | 是否启用当前收款地址 |
chain | 链标识,例如 bsc、ethereum、besu |
symbol | 资产符号,例如 USDT;为空时作为当前链的通用地址 |
contract-address | Token 合约地址;为空时作为当前链与资产的通用地址 |
address | 平台收款地址 |
匹配时会选择最具体的一条配置:合约地址匹配优先于资产符号,资产符号优先于链级通用配置。没有匹配地址时,充值单仍可创建,但 receiveAddress 为空,适合后台人工补充或自定义 Resolver 接管。
如果需要不重启服务即可调整收款钱包,可以在配置中心新增同名配置 spring.open.money.web3.recharge.receive-addresses,值使用 JSON 数组:
[
{
"enabled": true,
"chain": "bsc",
"symbol": "USDT",
"contractAddress": "0x0000000000000000000000000000000000000001",
"address": "0x2222222222222222222222222222222222222222"
}
]配置中心值优先生效,适合多收款钱包、按链或按 Token 动态切换;JSON 解析失败时会回退到 application.yml 绑定值,避免单次错误配置影响已有收款地址。
Web3 充值 ERC-20 Transfer 事件扫描器:
| 配置项 | 说明 |
|---|---|
scannerName | 固定为 user-balance-recharge-erc20-transfer |
chainName | 与充值单 chain 一致,例如 bsc |
attributes | 可直接填写 Token 合约地址,也可填写 {"contractAddress":"0x..."} |
| 匹配条件 | 链、合约地址、平台收款地址、链上原始金额换算后的 amount 必须全部一致 |
| 处理结果 | 写入 payerAddress、receiveAddress、txHash、transactionId 和 traceId,不直接入账 |
事件匹配只负责把链上转账绑定到充值单。真正入账仍由 Web3 充值链上确认入口或 Queue 自动确认任务在达到确认数后完成。
Web3 充值自动确认配置:
spring:
open:
money:
web3:
recharge:
confirmation:
enabled: true
auto-startup: false
task-name: money.web3.recharge.confirmation
instance-id: default
fixed-delay: 5m
trigger-delay: 10s
limit: 50
required-confirmations: 12配置说明:
| 配置 | 说明 |
|---|---|
enabled | 是否启用 Web3 充值自动确认 |
auto-startup | 是否额外注册固定间隔调度;默认关闭,主路径为 Queue 延迟确认 |
fixed-delay | 显式开启固定调度后的扫描时间 |
trigger-delay | 用户提交 Web3 充值 txHash 后,额外延迟投递一次 Queue 确认任务;默认 10s,非正值表示不额外触发 |
limit | 单批最多扫描充值单数量 |
required-confirmations | 默认要求确认数,单据或请求未指定时使用该值 |
用户提交 Web3 充值 hash 或链上事件扫描匹配到充值单后,系统会按 spring.open.money.web3.recharge.confirmation.trigger-delay 投递一次延迟 Queue 确认任务,默认不再注册固定间隔调度。只有特殊部署需要周期补扫时,才显式把 auto-startup 设为 true 并使用 fixed-delay 控制扫描间隔。
待支付充值单过期补偿配置:
spring:
open:
money:
balance:
recharge:
expiration:
enabled: true
auto-startup: false
task-name: money.balance.recharge.expiration
instance-id: default
fixed-delay: 5m
limit: 100| 配置 | 说明 |
|---|---|
enabled | 是否启用待支付充值单过期补偿 |
auto-startup | 旧固定间隔调度开关,默认关闭;主路径是创建待支付充值单时按 expiredAt 投递 Queue 延迟消息 |
task-name / instance-id | Queue Job 名称和实例 ID |
fixed-delay | 旧固定间隔扫描时间,保留给外部配置兼容 |
limit | 单批最多扫描充值单数量 |
补偿任务只关闭已经超过 expiredAt 的支付充值单。创建待支付充值单后系统会向专用 money-balance 队列投递 money.balance.recharge.expiration 延迟 Queue 消息;消费时会重新读取充值单状态,已支付、已入账、已失败或已删除的单据会安全跳过。支付网关成功回调、支付补偿查询成功、Web3 链上确认和后台人工入账仍走原有确认入账链路。
Web3 充值自动归集配置:
spring:
open:
money:
web3:
recharge:
collection:
enabled: false
auto-startup: false
task-name: money.web3.recharge.collection
instance-id: default
fixed-delay: 10m
limit: 50
receiver-address: 0x3333333333333333333333333333333333333333
reserve-amount: 0
broadcast: true
wallets:
- enabled: true
chain: bsc
from-address: 0x2222222222222222222222222222222222222222
private-key: ${SPRING_OPEN_MONEY_WEB3_RECHARGE_COLLECTION_WALLET_PRIVATE_KEY:}配置说明:
| 配置 | 说明 |
|---|---|
enabled | 是否启用 Web3 充值自动归集任务,默认关闭 |
auto-startup | 应用启动后是否自动注册调度任务 |
receiver-address | 平台归集目标地址 |
reserve-amount | 原生币归集时单钱包默认预留金额,单位 wei;ERC-20 归集不使用该字段 |
gas-price / gas-limit | 可选统一 gas 参数;为空时按链上或默认值处理,ERC-20 归集同样复用 |
broadcast | 是否广播归集交易;自动归集正式启用时通常为 true |
wallets[].chain | 托管钱包所属链 |
wallets[].from-address | 托管充值地址,必须和充值单 receiveAddress 匹配 |
wallets[].private-key | 托管充值地址私钥;只在签名时使用,不写入业务表或日志 |
自动归集只扫描 sourceType=web3、status=credited、collectionStatus=pending、receiveAddress 和 chain 已存在的充值单。没有匹配托管钱包配置时,充值单保持 pending,方便后续补齐配置后继续处理;原生币或 Token 链上余额不足等明确跳过场景会标记为 skipped;RPC、签名或广播异常会标记为 failed,可由后台归集标记入口人工复核后重新处理。
用户余额提现
用户余额提现是提现流程的业务单据层,负责把用户提现申请、冻结、审核、手续费预估、打款结果和余额流水串起来。第一阶段先提供后台受控 contract,默认 manual Provider 用于人工打款;Web3 原生币提现可通过外部已签名原始交易广播 Provider 进入链上处理中状态,也可以在显式开启后使用本地签名 Provider 临时签名并广播。
核心规则:
- 提现单保存
withdrawNo,作为业务侧稳定单号。 - 创建提现单会幂等创建或定位用户余额账户。
- 创建提现单时立即冻结
amount + feeAmount,流水类型为freeze。 - 提现手续费支持
fixed固定金额和percent百分比两种计算方式。percent按提现金额计算,并按资金统一口径直接截断到资产精度,不四舍五入、不向上取整。 - Web3 或虚拟资产提现可额外配置另一种资产手续费,例如提现 USDT 时额外扣除 BNB 原生币手续费。额外手续费同样支持
fixed和percent,会在独立余额账户中冻结、释放和出账。 - 接口会在保留
feeAmount、feeRawAmount等历史直观字段的同时返回统一fees数组。普通提现手续费使用feeType=withdraw;Web3 gas 使用feeType=web3-gas;额外代币扣费使用feeType=extra-asset。前端新能力优先读取fees,避免把普通提现费、链上 gas 和额外扣费混成一个字段。 - 后台审核拒绝、用户取消或打款失败会释放冻结金额,流水类型为
unfreeze。 - 后台标记已打款会先解冻,再写
withdraw流水完成出账,保证最终可用余额和冻结余额都正确。 - 已完成、已拒绝、已失败或已取消的提现属于终态,不能再次处理。
- 第一阶段只提供
manual、bank、payment-account、web3目标类型;普通 / 法币打款可走manual、external-http或paymentProvider。分销、结算等自动打款首版优先走内部资金驱动:法币使用 Money 提现状态机 +payment/payment-<provider>的 Payment.transfer,Web3 代币使用 Money Web3 提现签名 / 广播 / 确认。 provider=payment会调用 Payment starter 默认 Provider 的transfer能力;provider=payment-alipay、payment-wechat等会调用对应 Payment Provider 的打款能力;provider=payment-bank-transfer会调用 Payment starter 的bank-transferProvider,并由部署侧BankTransferPaymentClient接入具体银行 / 三方 SDK。payment-bank-transfer、external-http和额外链上广播实现属于后置扩展,适合内部 Payment / Web3 驱动不支持目标渠道或生产通道验收时使用。Mock / Log 已支持本地调试;真实服务商如未实现转账会返回统一 unsupported 异常,不会伪造成功。provider=web3-native或provider=web3且destinationType=web3、assetType=web3-native时,可以通过已签名原始交易广播链上原生币提现。provider=web3-signer且destinationType=web3、assetType=web3-native时,可以使用配置私钥或外部签名源签名并广播链上原生币提现;该 Provider 不要求额外开关,配置最少签名参数后即生效,未配置签名源时自动打款会安全空跑,生产建议优先接 KMS、Vault 或外部签名服务。- Web3 广播 Provider 只负责把已签名交易提交到链上并记录
txHash/transactionId,提现状态保持processing;后台链上确认接口会在交易成功且达到确认数后标记已打款,链上失败时标记失败并释放冻结金额,不能把广播成功直接当成已到账。 - Web3 提现自动确认任务默认由 Queue 触发处理
processing + web3 + txHash的提现单,复用同一链上确认 Service 推进状态;只有显式开启固定调度时才周期补扫。 - Web3 提现手续费预估只执行
eth_estimateGas和eth_gasPrice,支持原生币转账和 ERC20transfer,不签名、不广播、不保存私钥。 web3-native广播 Provider 不接收、不保存、不传输私钥;签名应在受控签名服务、离线签名流程或专门安全签名 Provider 中完成。
后台运维接口:
| 方法 | 路径 | 说明 |
|---|---|---|
GET | /api/v1/admin/money/user-balance-withdraws | 分页查询用户余额提现 |
GET | /api/v1/admin/money/user-balance-withdraws/{id} | 查询提现详情 |
POST | /api/v1/admin/money/user-balance-withdraws | 后台创建用户余额提现单 |
POST | /api/v1/admin/money/user-balance-withdraws/estimate-web3-fee | 后台预估 Web3 提现手续费 |
POST | /api/v1/admin/money/user-balance-withdraws/{id}/approve | 审核通过提现 |
POST | /api/v1/admin/money/user-balance-withdraws/{id}/reject | 审核拒绝提现并释放冻结金额 |
POST | /api/v1/admin/money/user-balance-withdraws/{id}/processing | 标记提现处理中 |
POST | /api/v1/admin/money/user-balance-withdraws/{id}/payout | 通过提现 Provider 发起打款 |
POST | /api/v1/admin/money/user-balance-withdraws/{id}/confirm-web3 | 查询 Web3 提现链上确认状态 |
POST | /api/v1/admin/money/user-balance-withdraws/{id}/paid | 标记提现已打款并出账 |
POST | /api/v1/admin/money/user-balance-withdraws/{id}/failed | 标记提现失败并释放冻结金额 |
当前用户接口:
| 方法 | 路径 | 说明 |
|---|---|---|
GET | /api/v1/money/users/me/balance-withdraws | 当前用户提现记录分页 |
POST | /api/v1/money/users/me/balance-withdraws | 当前用户创建提现单 |
POST | /api/v1/money/users/me/balance-withdraws/estimate-web3-fee | 当前用户预估 Web3 提现手续费 |
POST | /api/v1/money/users/me/balance-withdraws/{id}/cancel | 当前用户取消自己的待处理提现 |
提现响应会返回 displayAsset、displayAmount、displayFeeAmount、displayTotalAmount、displayExtraFeeAsset、displayExtraFeeAmount、displayTime 和各状态时间展示字段。fees[] 也会返回 displayAsset、displayAmount、displayBaseAmount,便于前端统一渲染普通手续费、Web3 gas 和额外资产扣费。
当前用户创建法币提现示例:
{
"assetCode": "fiat:CNY",
"amount": 80,
"feeAmount": 2,
"destinationType": "bank",
"provider": "manual-bank",
"payeeName": "张三",
"payeeAccount": "6222000000000000",
"idempotencyKey": "user-7-withdraw-20260531-001"
}Web3 提现可使用 destinationType=web3,必须传入 chain 和 withdrawAddress,可按需要填写 memo。后台标记 Web3 提现已打款时必须填写链上 txHash,这样后续签名、广播、失败回滚和审计都能复用同一张提现单和余额流水。
Web3 提现手续费预估示例:
{
"chain": "bsc",
"fromAddress": "0x0000000000000000000000000000000000000001",
"withdrawAddress": "0x88cdb83cacbff82855cd8a9b18299d7115888888",
"contractAddress": "0x0000000000000000000000000000000000000001",
"decimals": 18,
"amount": 12.3
}返回会包含 gasLimit、gasPrice、feeRawAmount、feeAmount、feeSymbol、feeSymbol 和统一 fees。其中 fees[0].feeType=web3-gas、calculationType=gas,并带有 rawAmount、gasLimit、gasPrice 和原生币资产信息。如果 contractAddress 为空,则按原生币转账预估;如果有合约地址,则按 ERC20 transfer(withdrawAddress, amount) 预估。fromAddress 可在请求中传入,也可以通过 spring.open.money.web3.withdraw.fee.from-address 配置默认值;两者都为空时会交给 RPC 默认处理。
当前用户创建 Web3 提现示例:
{
"assetCode": "evm:bsc:token:0x0000000000000000000000000000000000000001",
"assetType": "web3-token",
"symbol": "USDT",
"chain": "bsc",
"contractAddress": "0x0000000000000000000000000000000000000001",
"decimals": 18,
"amount": 12.3,
"feeCalculationType": "percent",
"feeRate": 0.01,
"extraFeeSymbol": "evm:bsc:native",
"extraFeeAssetKind": "web3-native",
"extraFeeSymbol": "BNB",
"extraFeeChain": "bsc",
"extraFeeDecimals": 18,
"extraFeeCalculationType": "fixed",
"extraFeeAmount": 0.00021,
"destinationType": "web3",
"provider": "manual-web3",
"withdrawAddress": "0x88cdb83cacbff82855cd8a9b18299d7115888888",
"memo": "用户提现",
"idempotencyKey": "user-7-web3-withdraw-001"
}Web3 提现标记已打款示例:
{
"txHash": "0x...",
"provider": "manual-web3",
"traceId": "trace-001",
"remark": "链上转账完成"
}提现打款 Provider:
- 后台可调用
/api/v1/admin/money/user-balance-withdraws/{id}/payout触发打款 Provider。 - 默认
manualProvider 不连接银行、支付机构或链上节点,只把已审核提现推进到processing,用于人工线下打款和后台观测。 - Web3 原生币广播 Provider 使用
signedRawTransaction字段提交已签名原始交易,广播成功后记录链上交易哈希并保持processing。 - Web3 签名打款 Provider 使用
web3-signer名称,读取spring.open.money.web3.withdraw.signer.*配置,按config、external-http、kms、vault或自定义签名来源先签名再广播,广播成功后同样记录链上交易哈希并保持processing。 - 自动打款由
spring.open.money.web3.withdraw.auto-payout.*控制,Web3 提现审批通过后会向专用web3队列立即投递money.web3.withdraw.auto-payout任务;任务只处理已审核通过的 Web3 提现,签名来源未配置时安全跳过。 - 后台可调用
/api/v1/admin/money/user-balance-withdraws/{id}/confirm-web3查询链上确认状态;未出块或确认数不足时保持processing,链上失败时标记失败并释放冻结金额,链上成功且达到确认数时标记已打款并出账。 - 自动确认由
spring.open.money.web3.withdraw.confirmation.*控制,主路径是 Web3 打款广播成功写入txHash后,按trigger-delay(默认30s,可由配置中心覆盖)向专用web3队列延迟投递一次 Queue 确认任务。默认不再注册固定间隔调度;只有特殊部署需要周期补扫时,才显式把auto-startup设为true并使用fixed-delay控制扫描间隔。确认任务只确认已有txHash的处理中提现,不负责签名、广播或私钥管理。 - 普通 / 法币提现可使用
paymentProvider 复用 Payment starter 的打款 / 转账能力,适合“平台通过支付服务商向用户打款”的场景。自动结算、分销佣金等前期场景默认优先使用payment或已接入的payment-<provider>,由 Money 继续负责冻结、释放、处理中同步和出账;银行 / 三方 SDK 型法币打款使用provider=payment-bank-transfer,部署侧实现 Payment starter 的BankTransferPaymentClient即可接入真实 SDK,作为需要真实银行卡 / 代付通道时的后置扩展。 - 普通 / 法币提现也可使用
external-httpProvider 对接企业自有打款服务,框架只负责提交标准提现数据和接收处理结果,不直接绑定某家银行或支付 SDK。 - 后台实际使用时建议配置业务化 Provider 名称,例如
bank-card、paypal-payout、stripe-payout,再通过provider: external-http指向 HTTP Provider。这样接口和运维记录里保留业务 Provider 名称,底层实现后续也可以替换为银行 / 三方支付 SDK。 - 后续银行 / 三方支付 SDK 型法币 Provider 和具体云厂商签名 Client 必须继续实现统一 Provider / Service 边界,并复用提现单、冻结释放和幂等流水。
- Provider 返回
paid时会走现有已打款逻辑;Web3 提现仍必须带链上txHash才能出账。 - Provider 返回
failed时会释放冻结金额并标记失败。
普通 / 法币提现外部 HTTP 打款配置示例:
spring:
open:
money:
balance:
withdraw:
payout:
external-http:
enabled: true
url: https://payout.internal.example.com/withdraws
query-url: https://payout.internal.example.com/withdraws/query
timeout: 10s
token: ${PAYOUT_TOKEN:}
token-header: Authorization
token-prefix: "Bearer "
providers:
bank-card:
provider: external-http
enabled: true
url: https://bank.internal.example.com/withdraws
query-url: https://bank.internal.example.com/withdraws/query
timeout: 10s
token: ${BANK_PAYOUT_TOKEN:}
token-header: Authorization
token-prefix: "Bearer "后台调用 /api/v1/admin/money/user-balance-withdraws/{id}/payout 时传入:
{
"provider": "bank-card",
"traceId": "trace-004",
"remark": "提交企业打款服务"
}外部打款服务会收到业务 Provider 名称、提现单号、用户 ID、账户 ID、资产、金额、手续费、收款人、收款账号、操作人和追踪号等标准字段。响应至少建议返回:
{
"status": "processing",
"transactionId": "payout-202606010001",
"traceId": "remote-trace-001",
"remark": "accepted"
}status 支持 processing、paid、failed。返回 processing 时提现保持处理中,后续可由后台人工标记或补偿任务继续推进;返回 paid 会立即复用现有出账逻辑;返回 failed 会释放冻结金额并标记失败。external-http Provider 不处理 Web3 提现,Web3 仍使用 web3-native 或 web3-signer。
如果外部服务支持结果查询,可以配置 query-url。后台调用 /api/v1/admin/money/user-balance-withdraws/{id}/sync-payout 时,系统会向 query-url 发送当前提现单号、状态、三方交易号、资产和金额等字段,并按外部返回的 processing、paid、failed 同步提现状态。未配置 query-url 的 Provider 不支持主动同步,仍可由后台人工标记或业务方通过自定义 Provider 扩展。
Payment 打款 Provider 示例:
{
"provider": "payment",
"traceId": "withdraw-transfer-1",
"remark": "使用默认 Payment Provider 打款"
}如果需要指定 Payment 服务商,可以使用 payment-<provider>。分销、结算等内部自动打款前期优先使用 payment / payment-<provider>;如果要走银行 / 三方 SDK 型法币打款 Provider,再使用 payment-bank-transfer:
{
"provider": "payment-bank-transfer",
"traceId": "withdraw-transfer-bank-1",
"remark": "使用银行 SDK Provider 打款"
}payment Provider 会把提现单号映射为 PaymentTransferRequest.outTransferNo,把提现金额映射为 amount,把收款账号映射为 recipientAccount。Payment 返回 success 时提现立即标记为 paid;返回 processing / pending 时提现保持处理中,可通过 /api/v1/admin/money/user-balance-withdraws/{id}/sync-payout 查询;返回 failed / closed 时提现标记失败并释放冻结金额。
提现手续费可通过 spring.open.money.balance.withdraw.fee.rules 配置默认规则。规则只在请求没有显式传入手续费字段时生效;如果接口请求已经提供 feeAmount、feeRate、feeCalculationType 或额外扣费字段,系统优先使用请求值。
示例:
spring:
open:
money:
balance:
withdraw:
fee:
rules:
# 银行卡提现:固定收取 2 元。
- destination-type: bank
asset-code: fiat:CNY
calculation-type: fixed
amount: 2.00
# Web3 USDT 提现:按提现数量收取 1%,并额外冻结一笔 BNB 原生币费用。
- destination-type: web3
symbol: USDT
chain: bsc
calculation-type: percent
rate: 0.01
extra-asset-code: evm:bsc:native
extra-asset-type: web3-native
extra-symbol: BNB
extra-chain: bsc
extra-decimals: 18
extra-calculation-type: fixed
extra-amount: 0.00021规则匹配字段包括 destination-type、provider、asset-code、asset-type、symbol、chain、contract-address;配置越具体优先级越高。普通提现通常只配置主手续费;Web3 提现可额外配置 extra-* 字段,用于扣除原生币 gas 或其他平台代币费用。
Web3 原生币广播示例:
{
"provider": "web3-native",
"signedRawTransaction": "0xf86c...",
"traceId": "trace-001",
"remark": "外部签名服务已签名并提交广播"
}Web3 签名打款 Provider 配置示例:
spring:
open:
money:
web3:
withdraw:
signer:
provider: config
private-key: ${SPRING_OPEN_MONEY_WEB3_WITHDRAW_SIGNER_PRIVATE_KEY:}
from-address: ""
external-http:
url: ""
timeout: 10s
token: ""
token-header: Authorization
token-prefix: "Bearer "
kms:
client: ""
key-id: ""
region: ""
endpoint: ""
timeout: 10s
vault:
client: ""
address: ""
namespace: ""
mount-path: transit
key-name: ""
timeout: 10sweb3-signer 不要求额外开关。需要自动链上打款时,最少只配置 SPRING_OPEN_MONEY_WEB3_WITHDRAW_SIGNER_PRIVATE_KEY;使用外部签名时,把 provider 改成 external-http / kms / vault 并填写该签名源必要参数即可。未配置签名源时自动打款会安全空跑。桥放款的 SPRING_OPEN_MONEY_WEB3_BRIDGE_TRANSFER_PRIVATE_KEY 不是提现 signer 私钥,不会让提现自动打款生效。
配置说明:
| 配置 | 说明 |
|---|---|
enabled | 显式启用 web3-signer Provider 的可选开关;通常不需要配置,填写签名源必要参数即可启用 |
provider | 签名来源 Provider,默认 config;内置 external-http、kms、vault,自定义签名来源实现 MoneyBalanceWithdrawWeb3SignerProvider 并让 name() 与该值一致 |
private-key | 本地签名私钥,建议只通过环境变量或数据库配置注入,不写入仓库 |
from-address | 可选发起地址;填写后会随签名请求传给 Web3 Manager |
external-http.enabled | 显式启用外部 HTTP 签名 Provider 的可选开关;通常只配置 external-http.url |
external-http.url | 外部签名服务地址;provider=external-http 时必填,填写后即可启用 |
external-http.timeout | 外部签名请求超时时间 |
external-http.token / token-header / token-prefix | 可选认证 Header 配置 |
kms.enabled | 显式启用 KMS 签名 Provider 的可选开关;通常只配置 kms.key-id 和部署侧 Client |
kms.client | KMS Client Bean 选择名,例如 aws-kms、aliyun-kms;为空时由 Client 自行按配置判定 |
kms.key-id / region / endpoint / timeout | 传给 KMS Client 的密钥、区域、可选私有 Endpoint 和超时配置 |
vault.enabled | 显式启用 Vault Transit 签名 Provider 的可选开关;通常只配置 vault.address / vault.key-name 和部署侧 Client |
vault.client | Vault Client Bean 选择名,例如 hashicorp-vault;为空时由 Client 自行按配置判定 |
vault.address / namespace / mount-path / key-name / timeout | 传给 Vault Client 的地址、命名空间、Transit 路径、密钥名和超时配置 |
外部 HTTP 签名 Provider 请求由框架发起,主要字段包含提现单号、链、资产、目标地址和待签名交易;响应至少返回:
{
"rawTransaction": "0x...",
"transactionHash": "0x..."
}启用外部签名时把 provider 改为 external-http,并配置签名服务地址。签名服务可以在内部继续接 KMS、Vault、硬件签名机或其他私钥托管系统。
启用 KMS / Vault SDK 型签名时,把 provider 改为 kms 或 vault,并填写 key-id 或 address / key-name 等必要参数。框架内置的 KMS / Vault Provider 不直接绑定某个云厂商 SDK,而是查找部署侧提供的 MoneyBalanceWithdrawWeb3KmsSignerClient 或 MoneyBalanceWithdrawWeb3VaultSignerClient Bean。Client 收到提现单、操作人、追踪号、待签名交易和对应配置后返回 rawTransaction,框架继续负责广播、记录 txHash、处理中状态和后续链上确认。
KMS Client 最小示例:
@Component
public class AwsKmsWithdrawSignerClient implements MoneyBalanceWithdrawWeb3KmsSignerClient {
@Override
public String name() {
return "aws-kms";
}
@Override
public boolean supports(MoneyWeb3Properties.Withdraw.Signer.Kms config) {
return name().equals(config.getClient());
}
@Override
public MoneyBalanceWithdrawWeb3SignResult sign(MoneyBalanceWithdrawWeb3KmsSignRequest request) {
// 调用真实 KMS SDK 生成已签名 raw transaction。
return new MoneyBalanceWithdrawWeb3SignResult(
"0x...",
null,
name(),
request.traceId(),
request.remark());
}
}Vault Client 的接入方式相同,实现 MoneyBalanceWithdrawWeb3VaultSignerClient,从 request.config() 读取 address、namespace、mountPath 和 keyName。
Web3 签名打款示例:
{
"provider": "web3-signer",
"traceId": "trace-003",
"remark": "本地签名并广播原生币提现"
}web3-signer 只支持 web3-native 原生币提现。它是受控打款 Provider:先通过 MoneyBalanceWithdrawWeb3SignerProvider 生成已签名原始交易,再由 Web3 Manager 广播。默认 config signer provider 读取本地配置私钥,适合开发环境或明确受控的单体部署;生产环境涉及私钥安全时,推荐新增 KMS、Vault、硬件签名机或外部签名服务 Provider,只替换签名来源,不改提现单、冻结释放、广播和链上确认流程。
Web3 提现链上确认示例:
{
"requiredConfirmations": 12,
"traceId": "trace-002",
"remark": "链上确认复核"
}Open Platform 平台账务
Open Platform 平台账务账户和用户余额账户主体分离,但复用同一套资产、金额精度、幂等流水和余额快照规则。第一阶段已在 Open Platform 模块内落地内部 Service 能力,后台接口仍保持只读运维查询,不暴露任意手工改账入口。
核心规则:
- 一个开放平台应用同一种法币只有一个账务账户。
- 账务账户保存
assetCode、assetType、symbol和decimals,与用户余额账户使用同一资产语义。 - 账务流水必须传入
idempotencyKey,重复提交同一幂等键不会重复改余额。 - 已支持
credit、debit、freeze、unfreeze四个方向。 - 流水保存余额变更前后快照,并检查可用余额和冻结余额不能为负数。
- 后台账务账户和流水 API 仍只做查询;正式扣费调度、正式账单、支付补款和套餐续费放到 Open Platform 付费调用阶段。
后台运维接口:
| 方法 | 路径 | 说明 |
|---|---|---|
GET | /api/v1/admin/open-platform/billing/accounts | 分页查询开放平台账务账户 |
GET | /api/v1/admin/open-platform/billing/accounts/{id} | 查询开放平台账务账户详情 |
GET | /api/v1/admin/open-platform/billing/ledgers | 分页查询开放平台账务流水 |
GET | /api/v1/admin/open-platform/billing/ledgers/{id} | 查询开放平台账务流水详情 |
生产可用闭口
当前 Money / Currency 的 P1-4 余额充值、P1-5 余额提现和 P1-7 业务接入按默认 Provider 与现有业务消费方闭口为生产可用。
生产环境建议先使用以下稳定路径:
| 能力 | 默认可用路径 | 说明 |
|---|---|---|
| 余额充值 | Payment 充值单 + 支付成功入账;Web3 充值单 + 链上确认入账 | Payment 回调、补偿查询、退款扣回、过期补偿、Web3 确认和自动归集已接入同一余额流水 |
| 普通 / 法币提现 | manual、external-http、payment / payment-<provider> / payment-bank-transfer | 前期自动打款优先用 payment / payment-<provider> 复用内部 Payment.transfer;external-http 用于企业自有打款服务,payment-bank-transfer 通过 Payment starter 的 BankTransferPaymentClient 接入银行 / 三方 SDK,作为后置扩展 |
| Web3 提现 | web3-native、web3-signer | 前期自动打款优先用 Money Web3 提现签名 / 广播 / 确认;广播和链上确认分离,广播成功不等于到账;web3-signer 内部可选择 config、external-http、kms、vault 或自定义签名来源 |
| 汇率报价 | internal-fixed、database、可选 web3-price / external-http / realtime-http | 默认配置汇率和数据库种子保证安装后可兑换;web3-price 复用 Web3 starter 外部行情,realtime-http 适合接实时行情聚合服务;所有 Provider 都只生成报价快照,不做链上 Swap |
| 业务接入 | Mall 余额支付 / 售后退款、Payment 成功 / 退款联动、Web3 结算入账、Open Platform 平台账务 | 统一消费 assetCode、余额流水、用户账单、展示金额、汇率快照和幂等键 |
仍建议项目上线前按自身业务补齐:
- 配置真实 Payment Provider、Web3 RPC、充值收款地址、归集钱包和提现打款 Provider;分销 / 结算等自动打款先复用内部
payment/payment-<provider>和 Money Web3 提现驱动。 - 对
external-http打款服务、payment-bank-transfer银行 SDK Client、外部签名服务或其他 Payment 打款 Provider 做小额联调;这些属于内部驱动之外的生产通道扩展。 - 保持真实资金、链上交易、私钥和签名服务在受控环境中验证。
以下能力属于后续增强,不再作为当前生产可用闭口阻塞:
- 具体银行 / 三方支付 SDK Client 示例和真实通道联调脚手架。
- 具体云厂商 KMS / Vault Client 示例和联调脚手架。
- 更多外部行情 Provider、汇率缓存、异常波动熔断和多 Provider 可信度评分。
- Open Platform 正式余额扣费调度、正式账单、套餐续费和支付补款。
- 会员余额、积分商城和 Web3 平台内余额展示复验。
后续阶段
已完成:
- 资产与金额规则。
- 用户余额账户。
- 用户余额流水。
- 用户账单。
- 用户余额兑换:兑换单、试算接口、本金 / 手续费余额流水、汇率快照和账单展示分类。
- 用户余额充值:充值单、Payment 订单预创建、后台幂等入账、Payment 成功自动入账、Payment 退款扣回、待支付充值单过期补偿、Web3 结算成功联动入账、Web3 充值链上确认、Queue 自动确认、归集状态 contract、原生币 / ERC-20 自动归集。
- 用户余额提现:提现单、创建冻结、审核、失败释放、打款出账、提现 Provider 边界、普通 / 法币 external-http 打款、Payment 平台打款、Payment
bank-transfer银行 / 三方 SDK 型法币打款 Provider、Web3 原生币已签名交易广播、Web3 签名打款 Provider、外部 HTTP 签名 Provider、KMS / Vault 签名 Provider SPI、Web3 链上确认接口、Queue 自动确认和 Web3 提现手续费预估。 - Open Platform 平台账务第一阶段:应用账户幂等创建、资产字段、账务流水写入、余额快照、防负数校验和幂等键去重。
后续会继续补:
- 人工对账。
- 具体银行 / 三方支付 SDK Client 示例和真实通道联调脚手架。
- 具体云厂商 KMS / Vault Client 示例和联调脚手架。
- Open Platform 正式余额扣费调度、正式账单、套餐续费和支付补款。
- 会员余额、积分商城和 Web3 平台内余额展示复验。