跳到内容

金额与资产

摘要:金额、币种、资产编码和时间格式的第一阶段通用底座。

金额与资产底座用于统一 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-paymentFacade / 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 的 PaymentProvider SPI。
  • 业务接支付:实现 CashierPaymentOrderSuccessHandler / CashierPaymentOrderRefundHandler,按 scene 注册。
  • 提现打款方式:payment / payment-<provider> 内部打款、web3-signer(KMS / Vault 经部署侧 client Bean)、bank-transfer(部署侧 BankTransferPaymentClient Bean)。
  • 提现状态联动:实现 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 / SmsManagerStorage / StorageManager 一致)。静态门面由 MoneyManagerProvider 在容器启动时自动装配;DefaultMoneyManager 是包内实现细节,业务模块不要直接依赖。

java
@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.facadespring-open-core-money)。账本核心的幂等、乐观锁、防负数、账户状态校验全部内置,业务代码只描述"给谁、什么资产、多少钱、什么业务、什么幂等键"。

入账与扣减

java
// 订单奖励入账:账户不存在时自动创建,幂等键重放不会重复加钱
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:CNYpoints:MALLcrypto:BTCevm:bsc:token:0x...
  • 金额按账户记账精度 RoundingMode.DOWN 取整,只少给不凭空造币。
  • 返回 MoneyTransactionResultAPPLIED 表示新流水,REPLAYED 表示同幂等键重放(未重复记账)。

预占 → 结算 / 释放(先冻结后实扣)

适合"先按预估冻结、成功后按实际用量扣费、失败退回"的场景(AI 网关计费就是这个模式):

java
// 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) 定位。

账户与资产展示

java
// 幂等确保账户存在(精度按资产类型推导,也可显式指定)
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 装入静态门面,用完销毁:

java
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,例如 CNYUSDJPY
  • 虚拟币和 Web3 资产不硬套 java.util.Currency
  • 所有业务使用稳定资产编码表示资产;源码统一使用 AssetCode 值对象,value() 是完整资产编码,symbol() 只用于展示短码。
  • Web3 金额同时保留展示金额和链上最小单位。
  • 历史订单、账单、扣费和结算应保存汇率快照,不能用实时汇率回算历史金额;当前用户余额流水和账单已支持快照透传。

请求上下文与默认货币

金额展示会受到语言、国家 / 地区、默认法币和时区影响。框架统一使用请求级上下文承载这些信息,避免 App、PC、Admin 和业务模块各自推断。

前端推荐请求头:

http
Accept-Language: zh-CN
X-Country: CN
X-Currency: CNY
X-Time-Zone: Asia/Shanghai

其中 X-CountryX-CurrencyX-Time-Zone 是可选增强。用户没有主动选择时,前端至少应传 Accept-Language,浏览器或 App 能拿到时也可以传 X-Time-Zone

服务端解析优先级:

  1. 前端明确传入或用户主动选择。
  2. 登录用户偏好。
  3. 服务端 GeoIP 或可信网关头推断。
  4. Accept-Language 推断语言;带地区的语言可弱推国家。
  5. 站点默认配置。
  6. 框架默认:zh-CN / CN / CNY / Asia/Shanghai

重要边界:

  • Accept-Language 主要决定默认语言,不应单独决定资金币种。
  • IP / GeoIP 适合首次访问时推荐国家和默认法币,但用户可以切换。
  • 客户端传入的 X-Country / X-Currency 只能作为展示偏好,不作为 KYC、税务、支付准入、风控或合规事实。
  • 资金和合规场景必须结合服务端 GeoIP、账号资料、KYC、支付渠道回调、账单地址或监管规则。
  • 虚拟币 / Web3 资产不从语言、国家或 IP 推断,继续使用稳定资产编码、链、合约地址、rawAmountdecimals 和链上状态。

当前底座计划:

说明
core common提供请求上下文值对象、国家码、国家到默认法币映射
Web MVC starter已提供请求级 ClientContext、Filter、Holder、请求头解析和 GeoIP 回退链
GeoIP Provider已接入 noop、可信网关 Header、mica-ip2region、MaxMind;aliyun / tencent 在线 Provider 接入计划已取消
Site / IAM / MoneySite 提供默认国家、默认法币、默认时区和支持货币列表;IAM 保存当前用户语言、国家 / 地区、默认展示法币和时区偏好;Money 负责余额、汇率和金额展示
money 底座displayAmountdisplayAssetdisplayTime 等展示字段由后端统一生成;法币展示会读取请求级 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回退链
开箱默认noopnoop
生产有 CDN / 网关国家码headermica-ip2region -> maxmind -> noop
国内私有化 / 国内服务器mica-ip2regionmaxmind -> noop
海外 / 国际化部署maxmindmica-ip2region -> noop

资产编码

AssetCode 是通用资产编码值对象。

java
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:CNYcrypto:USDTevm:bsc:token:0xabcpoints:AI_TOKEN
kindfiatcryptoweb3-nativeweb3-tokenpoints
symbolCNYUSDTBTCAI_TOKEN 等展示短码
chainWeb3 链标识,法币为空
contractAddressToken 合约地址,原生币和法币为空

资金表为查询便利会冗余保存资产分类、展示短码、链、合约地址和精度等快照列;写入时必须通过模块内资产列 helper 从 AssetCode 或账户快照统一复制,避免业务 Service 手工逐列赋值导致这些列与稳定资产编码漂移。

当前 HTTP DTO 和数据库列统一使用 assetCode 承载稳定 AssetCode(例如 fiat:CNYevm: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:CNYcrypto:USDTevm:bsc:native
kindfiatcryptoweb3-nativeweb3-tokenpoints
symbol业务短码和展示符号
displayName后台和前端展示名称
fiatCurrency法币资产对应 ISO 4217 代码
chain / chainId / contractAddressWeb3 原生币和 Token 的链与合约信息
decimals / displayDecimals计算精度和展示精度
enabled / sortOrder是否可用和展示排序

默认 seed 会写入常用法币 fiat:CNYfiat:USDfiat:EURfiat:HKDfiat:TWDfiat:JPYfiat:GBPfiat:AUDfiat:CADfiat: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禁用资产

资产报价示例:

json
{
  "baseSymbol": "fiat:CNY",
  "baseAmount": 100,
  "paySymbol": "crypto:USDT",
  "provider": "database",
  "scene": "cashier"
}

同资产报价会直接返回 rateProvider=same-assetrate=1 和相同的应付金额;跨资产报价会复用 Money 汇率 Provider 链,包括配置汇率、数据库汇率、外部 HTTP 和 Web3 price Provider。报价结果会返回定价资产、支付资产、汇率、反向汇率、来源、快照时间、过期时间、置信度和应付金额。Money 已通过 CashierPaymentQuoteRatePort 为 Cashier 支付 quote 生命周期提供资产报价适配;业务模块和 Payment Provider 后续应保存 Cashier quote 快照,不再另算汇率。

报价、支付和结算接入时应先校验资产目录存在且启用。历史支付订单、报价、账本和结算单必须保存资产快照字段,不能在资产目录修改后回算历史数据。

法币

CurrencyCodes 提供常用法币常量和 ISO 4217 校验。

java
CurrencyCodes.DEFAULT;              // CNY
CurrencyCodes.normalize("cny");     // CNY
CurrencyCodes.isSupported("USD");   // true
CurrencyCodes.fractionDigits("JPY"); // 0

后续业务保存法币金额时,应同时保存:

  • currency
  • amount
  • 必要时保存 exchangeRaterateProviderrateSnapshotAt

金额换算

MoneyAmounts 负责金额格式和 Web3 最小单位换算。

java
MoneyAmounts.fromRaw(new BigInteger("1234500"), 6); // 1.2345
MoneyAmounts.toRaw(new BigDecimal("1.2345"), 6);    // 1234500
MoneyAmounts.plain(new BigDecimal("1.2300"));       // 1.23

toRaw 使用精确换算。如果金额小数位超过资产精度,会抛出 ArithmeticException,避免静默截断资金金额。

业务计算中产生超出资产精度的小数时,资金口径统一使用 RoundingMode.DOWN 直接截断,不四舍五入、不向上取整。当前覆盖余额兑换目标金额 / 反推汇率、兑换手续费、提现百分比手续费、Mall 折扣金额、Open Platform AI 网关预占 / 实扣金额、用量成本快照、模型价格目录和毛利统计;用户或外部请求传入的已成型金额仍使用 UNNECESSARY 校验精度,超过资产精度时拒绝。

AssetAmount 用于把金额和资产绑定在一起:

java
AssetAmount amount = AssetAmount.fromRaw(
        AssetCode.web3Token("bsc", "USDT", "0xabc"),
        new BigInteger("1234500"),
        6
);

AssetAmount 上的运算强制同资产:plus / minus / compareAmountassetCodedecimals 不一致时直接抛 IllegalArgumentException,避免跨币种、跨精度静默混算。业务层按需把守卫失败翻译成自己的错误码(例如 Mall 下单合计把它映射为 MALL_ORDER_CURRENCY_MISMATCH)。 当 AssetAmount 带有 decimals 时,plus / minus 会同步重算 rawAmount;如果金额无法按该精度精确换算,也会通过 ArithmeticException 暴露,不做截断。

java
AssetAmount.fiat("CNY", new BigDecimal("1.20"))
        .plus(AssetAmount.fiat("CNY", new BigDecimal("0.80"))); // 2.00

取整策略和尾差分配

资金取整必须显式选择策略,不能在业务代码里随手调用 setScale。默认规则:

场景推荐入口说明
用户 / 外部系统传入的已成型金额MoneyRoundingPolicy.EXACT超过资产精度直接拒绝
系统内部派生资金金额MoneyRoundingPolicy.DOWNdefaultInternal()直接截断,不四舍五入、不向上取整
税费 / 发票 / 渠道清结算明确要求HALF_UP / HALF_EVEN只在外部规则明确要求时使用
分账 / 多人分摊 / 批量结算MoneyAllocations.allocateEqually / allocateByWeights先按最小单位截断,再把剩余最小单位确定性分配
java
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 同时包含 baseAmountadjustmentAmountfinalAmount,业务落账时应把 adjustment 作为尾差调整留痕,而不是静默丢弃。

时间格式

通用展示时间使用 DateTimePatterns / DateTimeFormatters

java
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

json
{
  "userId": 1,
  "assetCode": "fiat:CNY"
}

也可以传资产组件,由后端统一生成稳定 assetCode(即 AssetCode 值):

json
{
  "userId": 1,
  "assetType": "web3-token",
  "symbol": "USDT",
  "chain": "bsc",
  "contractAddress": "0xabc..."
}

返回字段包含 displayNamedisplayAssetdisplayAvailableBalancedisplayFrozenBalancedisplayTotalIncomedisplayTotalExpensedisplayCreatedAtdisplayUpdatedAt,前端优先直接展示这些字段,不需要自行拼接链名、资产短码、金额和时间。Web3 平台内余额同样使用这些字段展示,例如 evm:bsc:token:<contract> 会由后端展示为 USDT / bsc

用户余额流水

用户余额流水是余额变更的唯一入口。充值、消费、退款、冻结、解冻、提现和后台调整都必须通过流水服务完成,禁止业务模块直接修改账户余额。

核心规则:

  • 每次余额变更必须传入 idempotencyKey,重复提交同一个幂等键会直接返回已有流水,不会重复改余额。
  • 流水保存业务来源:businessTypebusinessIdbusinessNosourceTypesourceIdoperatorIdtraceId
  • 流水保存可用余额、冻结余额、累计收入和累计支出的变更前后快照,方便后续对账和审计。
  • 涉及跨币种展示、结算、兑换或扣费时,流水可保存汇率快照:exchangeRaterateProviderrateSnapshotAt。历史账单和审计必须读取快照,不用事后实时汇率反推。
  • 账户停用时不能写入新流水。
  • 扣减可用余额或解冻冻结余额时,后端会检查余额不能为负数。
  • 金额精度按账户 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 到账、退款和提现继续沿用同一套幂等与审计规则。

流水响应会返回 displayAssetdisplayAmountdisplayAvailableDeltadisplayFrozenDeltadisplayAfterAvailableBalancedisplayTime。这些字段只用于展示,真实对账仍以原始 amountassetCode、余额快照、汇率快照和发生时间为准。

用户余额兑换

用户余额兑换是在现有币价和汇率快照之上的真实业务单据层,用于把用户某一种余额资产兑换成另一种余额资产,例如 fiat:CNY 兑换 fiat:USDfiat:CNY 兑换 evm:bsc:token:<contract>evm:bsc:token:<contract> 兑换 evm:bsc:native

核心规则:

  • 兑换必须经过白名单资产账户,不允许绕过余额账户直接改金额。
  • 创建兑换会生成一张兑换单,并写入幂等余额流水:exchange_out 扣减来源账户本金,exchange_in 增加目标账户本金;存在手续费时追加 exchange_fee 独立扣款流水。
  • 后端会保存兑换单号、来源账户、目标账户、来源资产、目标资产、来源金额、目标金额、汇率、汇率 Provider、汇率快照时间和幂等键。
  • quote 接口只计算预估目标金额,不写兑换单、不写流水,适合前端确认页。
  • 当前用户兑换默认使用平台内置配置汇率,数据库初始化也会写入同一组发布态默认汇率;前端只提交来源资产、目标资产和来源金额,用户侧传入的 toAmountexchangeRaterateProviderrateSnapshotAt 会被忽略,避免客户端篡改汇率。
  • 当前用户来源资产必须来自自己的余额账户,目标资产可从已启用的统一资产目录选择;创建兑换时后端会自动确保目标余额账户存在。
  • 后台运维接口可以显式传入目标金额或兑换汇率,用于人工调账或特殊运营场景;未传时同样回退到平台内部汇率。
  • 没有命中任何汇率 Provider 且后台也没有显式传入目标金额或兑换汇率时会拒绝。
  • 可选启用 web3-pricerealtime-http 汇率 Provider:前者复用 Web3 starter 的价格 Provider 查询同链 ERC-20 / Token 对行情,后者接企业自有或第三方实时行情 HTTP 服务;两者都只生成兑换报价快照,不触发链上 Swap。
  • 来源资产和目标资产不能相同。
  • 兑换试算会返回 rateExpiresAtdisplayRateExpiresAt;Provider 返回的报价如果已经过期,后端会拒绝继续创建真实兑换单。
  • 兑换手续费由后端按配置规则计算,默认生产配置匹配所有资产对并按来源金额扣 0.3%quote 会返回 fees[],创建兑换时会把手续费快照保存到兑换单。source 扣费模式会对来源账户生成独立 exchange_fee 扣款流水;target 扣费模式会先按目标资产 gross 金额写入 exchange_in,再对目标账户生成独立 exchange_fee 扣款流水,最终到账仍等于扣费后的 net 金额。
  • 兑换历史账单会归类为 exchange,前端可直接展示 displayFromAmountdisplayToAmountdisplayRateSnapshotAt 等字段。

后台运维接口:

方法路径说明
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当前用户创建兑换单

示例:

json
{
  "fromAssetCode": "fiat:CNY",
  "fromAmount": "100.00",
  "toAssetCode": "fiat:USD",
  "idempotencyKey": "exchange:sample:10001"
}

上述用户侧请求会按平台配置的汇率 Provider 扣减 100.00 CNY,并写入对应 USD 余额。真实资金对账以余额流水和兑换单快照为准;无论使用 internal-fixeddatabaseweb3-price 还是 realtime-http,都必须先生成快照再创建兑换单,不能用事后实时价格覆盖历史兑换结果。

/api/v1/admin/money/assets 读取统一资产目录,返回 assetCodeassetTypesymboldisplayNamefiatCurrencychainchainIdcontractAddressdecimalsdisplayDecimalsenableddisplayAsset 等字段。它是报价、支付和结算的资产准入入口;资产存在不代表任意资产对可兑换,汇率规则、手续费规则、Payment Provider 能力和 Web3 风控仍会继续限制实际可用组合。

内部汇率配置:

yaml
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: true

rate 的含义是 1 个 from-asset-code 可兑换多少 to-asset-codebidirectional=true 时系统会按倒数支持反向兑换,例如 fiat:USDfiat:CNYquote-validity 是试算锁价有效期,默认 2m,小于等于 0 时只返回汇率快照时间,不返回过期时间。平台内余额兑换不会触发链上 Swap;Web3 原生币和 Token 作为用户余额资产参与兑换,真实链上划转仍由充值、提现和归集链路负责。

应用默认配置会启用一组开箱基准价:fiat:CNY / fiat:USDfiat:USD / crypto:USDTcrypto: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=truestatus=publishedpublishedAt;回滚会设置 status=rolled_backenabled=falseeffectiveTo=now,保留历史快照供审计。
  • grayRatio 支持 0..100grayUserIds 可记录灰度用户 ID;数据库 Provider 只使用已发布且处于生效窗口内的规则参与报价。
  • 未显式指定 rateProvider 时,数据库 Provider 会作为报价链候选参与 fallback:如果配置汇率或默认 Provider 没有可用报价,已发布数据库规则仍可承接当前用户兑换;显式指定其它 Provider 时不会误命中数据库规则。
  • Admin 资金运维页支持新增数据库汇率、编辑草稿、保存并发布、发布和回滚;余额账户、资金流水、账单、充值、提现和兑换记录仍保持只读核对。

Web3 外部行情 Provider 配置:

yaml
spring:
  open:
    money:
      balance:
        exchange:
          rate:
            default-provider: web3-price
            quote-validity: 2m
            web3-price:
              enabled: true
              provider-name: pancake-v2-pool
              quote-validity: 30s

web3-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 配置:

yaml
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.confidence

external-http.enabled=false 是默认值,避免本地和私有部署启动后访问外部报价服务。启用后,系统支持 GET 查询参数或 POST JSON 请求,发送字段包含 userIdfromAssetCodetoAssetCodefromAmountsceneprovider=external-httprequestedAt。响应至少需要按 rate-path 解析出大于 0 的汇率;providersourcesnapshotAtconfidence 可通过对应路径读取,缺省时分别使用 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 配置:

yaml
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.confidence

realtime-httpexternal-http 使用同一套 HTTP 请求和响应解析 contract,但 Provider 名独立,适合把余额兑换切到企业实时行情聚合服务。启用后,请求字段中的 providerrealtime-http;响应未返回 provider 时,兑换快照也会记录为 realtime-http。显式传 rateProvider=realtime-http 会强制使用实时行情;若要让实时行情成为默认报价口径,把 default-provider 设为 realtime-http 即会优先尝试该 Provider,失败或无报价时再回退到数据库 / 配置汇率等后续候选。

兑换手续费配置:

yaml
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: source

calculation-type=percent 表示按基准金额百分比扣费,rate=0.0030.3%calculation-type=fixed 表示固定数量扣费,读取 amountdeduct-mode=source 表示手续费与来源资产一起扣除,例如用户用 100 CNY 兑换 USD,手续费 0.30 CNY 时来源账户实际出账 100.30 CNYdeduct-mode=target 表示手续费从目标到账资产中扣除,例如应到账 14 USD、手续费 1 USD 时目标账户实际入账 13 USD。兑换单会保存手续费资产、计算方式、费率、基准金额、扣费方式等快照,后续汇率或配置变化不会影响历史兑换结果。

后台资金运维入口会把当前应用装配后的汇率 Provider、配置汇率、数据库汇率和手续费规则返回给管理端。配置汇率和手续费规则仍以配置文件或配置中心配置为准;数据库汇率可在 /api/v1/admin/money/rates/** 内完成草稿、发布和回滚闭环。

用户账单

用户账单是面向用户和后台运维的展示视图,不是新的账务数据源。它由余额流水投影生成,用于把底层审计流水整理成更容易理解的账单列表。

核心规则:

  • 账单不重复保存金额数据,底层事实仍以余额流水为准。
  • 账单按业务来源归类,例如充值、消费、退款、提现、商城、支付、会员、积分、Web3、开放平台等。
  • 账单返回 categoryDisplayNametypeDisplayNamedirectionDisplayNametitle 等可直接展示字段,前端不需要维护硬编码翻译表。
  • 账单返回 displayAssetdisplayAmountdisplaySignedAmountdisplayBalanceAfterdisplayTime,用于列表和详情页直接展示;Web3 资产会按 assetCode 解析链名和资产短码,前端不要重新拼接。
  • 账单透传底层流水的 exchangeRaterateProviderrateSnapshotAt,用于展示或审计历史汇率来源。
  • 账单返回 signedAmount,收入为正数,支出为负数,冻结 / 解冻属于划转方向。
  • 当前用户账单不暴露幂等键、余额变更前快照等底层审计字段;需要审计时使用后台余额流水接口。

后台运维接口:

方法路径说明
GET/api/v1/admin/money/user-bills分页查询用户账单展示视图

当前用户接口:

方法路径说明
GET/api/v1/money/users/me/bills当前用户账单分页

账单常用筛选项:

参数说明
assetCode / assetType / symbol按资产筛选
changeType按底层流水类型筛选,例如 rechargeconsumerefund
businessType / businessId按业务来源筛选,例如商城订单、会员充值、Web3 到账
keyword匹配业务单号、业务类型、备注等展示关联字段

用户余额充值

用户余额充值是充值流程的业务单据层,负责把支付订单、链上充值确认和余额流水串起来。第一阶段已完成通用充值单、Payment 订单预创建、后台受控入账、Payment 成功自动入账、Payment 退款自动扣回、Web3 结算成功联动入账、Web3 充值收款地址分配、Web3 充值链上确认运维入口、Queue 自动确认、ERC-20 Transfer 事件匹配、归集状态 contract,以及原生币 / ERC-20 自动归集任务。

核心规则:

  • 充值单保存 rechargeNo,作为业务侧稳定单号。
  • 默认来源为 payment,可预留 web3manual 等来源。
  • 默认资产为 fiat:CNY,也可以传入 assetCode 或资产组件。
  • 创建充值单会幂等创建用户余额账户。
  • Payment 可用时,后端会以充值单号创建支付订单,支付订单的 scenebalance-recharge
  • Payment 异步通知或补偿查询确认订单成功后,会按支付订单号查找充值单并自动触发幂等入账。
  • Web3 支付结算执行器在订单条件更新为成功并写入 web3-settlement 支付流水后,会分发 Payment 成功处理器;成功处理器完成后还会写入 money-web3-settlement Outbox 事件,供后续重放 / 失败观测。余额充值场景通过同一 balance-recharge scene 读取交易哈希和确认数并幂等入账。
  • 真正入账必须走余额流水,流水类型为 recharge,幂等键为充值单幂等键。
  • 已入账充值再次确认不会重复写流水。
  • Payment 退款回调会按充值支付订单查找已入账充值,并写入 adjust_out 流水扣回余额;幂等键由充值单号和退款单号 / 退款 ID / 交易号组成,重复退款回调不会重复扣款。
  • 如果账户余额不足,余额流水会拒绝扣回,避免账户出现负数;这类异常需要进入后台支付 / 账务运维处理。
  • expiredAt 的待支付充值单会由过期补偿任务自动标记失败,不写余额流水;该任务只处理 sourceType=payment、充值状态 pending、支付状态 pending 的单据,不会误伤已入账、已支付待补偿或 Web3 待确认充值。
  • 后台确认入账是运维 / 补偿入口,正式支付回调、补偿查询和 Web3 确认都复用同一 Service contract。
  • sourceType=web3 的充值单可以在创建时写入 txHashconfirmationsrequiredConfirmations,不会预创建 Payment 支付订单。
  • sourceType=web3 的充值单可以记录链上付款地址 payerAddress、平台收款地址 receiveAddress、归集状态 collectionStatus、归集交易哈希 collectionTxHash 和归集时间 collectedAt
  • 创建 Web3 充值单时,如果请求未传 receiveAddress,后端会按 spring.open.money.web3.recharge.receive-addresses 中的链、资产符号和合约地址分配平台收款地址;请求显式传入的收款地址仍最高优先。
  • 内置 user-balance-recharge-erc20-transfer 扫描器可匹配 ERC-20 Transfer 事件:当链、合约地址、平台收款地址和链上金额与待处理充值单一致时,自动写入付款地址、收款地址和 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当前用户创建充值单

充值响应会返回 displayAssetdisplayAmountdisplayTimedisplayPaidAtdisplayCreditedAtdisplayCollectedAtdisplayExpiredAt。其中 displayTime 默认取充值单创建时间,其他展示时间按实际状态字段生成。sourceType=web3 的充值单也使用同一套展示字段,链名、Token 短码和金额格式由后端统一生成。

当前用户创建法币充值示例:

json
{
  "assetCode": "fiat:CNY",
  "amount": 100,
  "provider": "alipay",
  "idempotencyKey": "user-7-recharge-20260531-001"
}

当前用户创建 Web3 充值示例:

json
{
  "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"
}

后台确认入账示例:

json
{
  "transactionId": "202605310001",
  "payerId": "payer-001",
  "traceId": "trace-001",
  "remark": "支付回调补偿确认"
}

Web3 充值链上确认示例:

json
{
  "requiredConfirmations": 12,
  "traceId": "trace-web3-recharge-001",
  "remark": "后台链上确认"
}

Web3 充值归集标记示例:

json
{
  "collectionStatus": "collected",
  "collectionTxHash": "0x...",
  "traceId": "trace-web3-recharge-collection-001",
  "remark": "后台确认已完成归集"
}

Web3 充值会在充值单上继续使用 txHashconfirmationsrequiredConfirmationspayerAddressreceiveAddresscollectionStatus 等字段,不单独发明另一套到账模型。当前链上确认入口适合后台人工补偿和运维确认;地址分配适合给用户生成平台收款地址;事件匹配入口适合把 ERC-20 转账日志绑定到充值单;归集标记入口适合记录人工或外部任务的归集结果;自动归集任务适合托管原生币充值地址的场景,继续沿用同一张充值单和余额流水幂等规则。

Web3 充值收款地址配置:

yaml
spring:
  open:
    money:
      web3:
        recharge:
          receive-addresses:
            - enabled: true
              chain: bsc
              symbol: USDT
              contract-address: 0x0000000000000000000000000000000000000001
              address: 0x2222222222222222222222222222222222222222

配置说明:

配置说明
enabled是否启用当前收款地址
chain链标识,例如 bscethereumbesu
symbol资产符号,例如 USDT;为空时作为当前链的通用地址
contract-addressToken 合约地址;为空时作为当前链与资产的通用地址
address平台收款地址

匹配时会选择最具体的一条配置:合约地址匹配优先于资产符号,资产符号优先于链级通用配置。没有匹配地址时,充值单仍可创建,但 receiveAddress 为空,适合后台人工补充或自定义 Resolver 接管。

如果需要不重启服务即可调整收款钱包,可以在配置中心新增同名配置 spring.open.money.web3.recharge.receive-addresses,值使用 JSON 数组:

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 必须全部一致
处理结果写入 payerAddressreceiveAddresstxHashtransactionIdtraceId,不直接入账

事件匹配只负责把链上转账绑定到充值单。真正入账仍由 Web3 充值链上确认入口或 Queue 自动确认任务在达到确认数后完成。

Web3 充值自动确认配置:

yaml
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 控制扫描间隔。

待支付充值单过期补偿配置:

yaml
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-idQueue Job 名称和实例 ID
fixed-delay旧固定间隔扫描时间,保留给外部配置兼容
limit单批最多扫描充值单数量

补偿任务只关闭已经超过 expiredAt 的支付充值单。创建待支付充值单后系统会向专用 money-balance 队列投递 money.balance.recharge.expiration 延迟 Queue 消息;消费时会重新读取充值单状态,已支付、已入账、已失败或已删除的单据会安全跳过。支付网关成功回调、支付补偿查询成功、Web3 链上确认和后台人工入账仍走原有确认入账链路。

Web3 充值自动归集配置:

yaml
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=web3status=creditedcollectionStatus=pendingreceiveAddresschain 已存在的充值单。没有匹配托管钱包配置时,充值单保持 pending,方便后续补齐配置后继续处理;原生币或 Token 链上余额不足等明确跳过场景会标记为 skipped;RPC、签名或广播异常会标记为 failed,可由后台归集标记入口人工复核后重新处理。

用户余额提现

用户余额提现是提现流程的业务单据层,负责把用户提现申请、冻结、审核、手续费预估、打款结果和余额流水串起来。第一阶段先提供后台受控 contract,默认 manual Provider 用于人工打款;Web3 原生币提现可通过外部已签名原始交易广播 Provider 进入链上处理中状态,也可以在显式开启后使用本地签名 Provider 临时签名并广播。

核心规则:

  • 提现单保存 withdrawNo,作为业务侧稳定单号。
  • 创建提现单会幂等创建或定位用户余额账户。
  • 创建提现单时立即冻结 amount + feeAmount,流水类型为 freeze
  • 提现手续费支持 fixed 固定金额和 percent 百分比两种计算方式。percent 按提现金额计算,并按资金统一口径直接截断到资产精度,不四舍五入、不向上取整。
  • Web3 或虚拟资产提现可额外配置另一种资产手续费,例如提现 USDT 时额外扣除 BNB 原生币手续费。额外手续费同样支持 fixedpercent,会在独立余额账户中冻结、释放和出账。
  • 接口会在保留 feeAmountfeeRawAmount 等历史直观字段的同时返回统一 fees 数组。普通提现手续费使用 feeType=withdraw;Web3 gas 使用 feeType=web3-gas;额外代币扣费使用 feeType=extra-asset。前端新能力优先读取 fees,避免把普通提现费、链上 gas 和额外扣费混成一个字段。
  • 后台审核拒绝、用户取消或打款失败会释放冻结金额,流水类型为 unfreeze
  • 后台标记已打款会先解冻,再写 withdraw 流水完成出账,保证最终可用余额和冻结余额都正确。
  • 已完成、已拒绝、已失败或已取消的提现属于终态,不能再次处理。
  • 第一阶段只提供 manualbankpayment-accountweb3 目标类型;普通 / 法币打款可走 manualexternal-httppayment Provider。分销、结算等自动打款首版优先走内部资金驱动:法币使用 Money 提现状态机 + payment / payment-<provider> 的 Payment.transfer,Web3 代币使用 Money Web3 提现签名 / 广播 / 确认。
  • provider=payment 会调用 Payment starter 默认 Provider 的 transfer 能力;provider=payment-alipaypayment-wechat 等会调用对应 Payment Provider 的打款能力;provider=payment-bank-transfer 会调用 Payment starter 的 bank-transfer Provider,并由部署侧 BankTransferPaymentClient 接入具体银行 / 三方 SDK。payment-bank-transferexternal-http 和额外链上广播实现属于后置扩展,适合内部 Payment / Web3 驱动不支持目标渠道或生产通道验收时使用。Mock / Log 已支持本地调试;真实服务商如未实现转账会返回统一 unsupported 异常,不会伪造成功。
  • provider=web3-nativeprovider=web3destinationType=web3assetType=web3-native 时,可以通过已签名原始交易广播链上原生币提现。
  • provider=web3-signerdestinationType=web3assetType=web3-native 时,可以使用配置私钥或外部签名源签名并广播链上原生币提现;该 Provider 不要求额外开关,配置最少签名参数后即生效,未配置签名源时自动打款会安全空跑,生产建议优先接 KMS、Vault 或外部签名服务。
  • Web3 广播 Provider 只负责把已签名交易提交到链上并记录 txHash / transactionId,提现状态保持 processing;后台链上确认接口会在交易成功且达到确认数后标记已打款,链上失败时标记失败并释放冻结金额,不能把广播成功直接当成已到账。
  • Web3 提现自动确认任务默认由 Queue 触发处理 processing + web3 + txHash 的提现单,复用同一链上确认 Service 推进状态;只有显式开启固定调度时才周期补扫。
  • Web3 提现手续费预估只执行 eth_estimateGaseth_gasPrice,支持原生币转账和 ERC20 transfer,不签名、不广播、不保存私钥。
  • 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当前用户取消自己的待处理提现

提现响应会返回 displayAssetdisplayAmountdisplayFeeAmountdisplayTotalAmountdisplayExtraFeeAssetdisplayExtraFeeAmountdisplayTime 和各状态时间展示字段。fees[] 也会返回 displayAssetdisplayAmountdisplayBaseAmount,便于前端统一渲染普通手续费、Web3 gas 和额外资产扣费。

当前用户创建法币提现示例:

json
{
  "assetCode": "fiat:CNY",
  "amount": 80,
  "feeAmount": 2,
  "destinationType": "bank",
  "provider": "manual-bank",
  "payeeName": "张三",
  "payeeAccount": "6222000000000000",
  "idempotencyKey": "user-7-withdraw-20260531-001"
}

Web3 提现可使用 destinationType=web3,必须传入 chainwithdrawAddress,可按需要填写 memo。后台标记 Web3 提现已打款时必须填写链上 txHash,这样后续签名、广播、失败回滚和审计都能复用同一张提现单和余额流水。

Web3 提现手续费预估示例:

json
{
  "chain": "bsc",
  "fromAddress": "0x0000000000000000000000000000000000000001",
  "withdrawAddress": "0x88cdb83cacbff82855cd8a9b18299d7115888888",
  "contractAddress": "0x0000000000000000000000000000000000000001",
  "decimals": 18,
  "amount": 12.3
}

返回会包含 gasLimitgasPricefeeRawAmountfeeAmountfeeSymbolfeeSymbol 和统一 fees。其中 fees[0].feeType=web3-gascalculationType=gas,并带有 rawAmountgasLimitgasPrice 和原生币资产信息。如果 contractAddress 为空,则按原生币转账预估;如果有合约地址,则按 ERC20 transfer(withdrawAddress, amount) 预估。fromAddress 可在请求中传入,也可以通过 spring.open.money.web3.withdraw.fee.from-address 配置默认值;两者都为空时会交给 RPC 默认处理。

当前用户创建 Web3 提现示例:

json
{
  "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 提现标记已打款示例:

json
{
  "txHash": "0x...",
  "provider": "manual-web3",
  "traceId": "trace-001",
  "remark": "链上转账完成"
}

提现打款 Provider:

  • 后台可调用 /api/v1/admin/money/user-balance-withdraws/{id}/payout 触发打款 Provider。
  • 默认 manual Provider 不连接银行、支付机构或链上节点,只把已审核提现推进到 processing,用于人工线下打款和后台观测。
  • Web3 原生币广播 Provider 使用 signedRawTransaction 字段提交已签名原始交易,广播成功后记录链上交易哈希并保持 processing
  • Web3 签名打款 Provider 使用 web3-signer 名称,读取 spring.open.money.web3.withdraw.signer.* 配置,按 configexternal-httpkmsvault 或自定义签名来源先签名再广播,广播成功后同样记录链上交易哈希并保持 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 的处理中提现,不负责签名、广播或私钥管理。
  • 普通 / 法币提现可使用 payment Provider 复用 Payment starter 的打款 / 转账能力,适合“平台通过支付服务商向用户打款”的场景。自动结算、分销佣金等前期场景默认优先使用 payment 或已接入的 payment-<provider>,由 Money 继续负责冻结、释放、处理中同步和出账;银行 / 三方 SDK 型法币打款使用 provider=payment-bank-transfer,部署侧实现 Payment starter 的 BankTransferPaymentClient 即可接入真实 SDK,作为需要真实银行卡 / 代付通道时的后置扩展。
  • 普通 / 法币提现也可使用 external-http Provider 对接企业自有打款服务,框架只负责提交标准提现数据和接收处理结果,不直接绑定某家银行或支付 SDK。
  • 后台实际使用时建议配置业务化 Provider 名称,例如 bank-cardpaypal-payoutstripe-payout,再通过 provider: external-http 指向 HTTP Provider。这样接口和运维记录里保留业务 Provider 名称,底层实现后续也可以替换为银行 / 三方支付 SDK。
  • 后续银行 / 三方支付 SDK 型法币 Provider 和具体云厂商签名 Client 必须继续实现统一 Provider / Service 边界,并复用提现单、冻结释放和幂等流水。
  • Provider 返回 paid 时会走现有已打款逻辑;Web3 提现仍必须带链上 txHash 才能出账。
  • Provider 返回 failed 时会释放冻结金额并标记失败。

普通 / 法币提现外部 HTTP 打款配置示例:

yaml
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 时传入:

json
{
  "provider": "bank-card",
  "traceId": "trace-004",
  "remark": "提交企业打款服务"
}

外部打款服务会收到业务 Provider 名称、提现单号、用户 ID、账户 ID、资产、金额、手续费、收款人、收款账号、操作人和追踪号等标准字段。响应至少建议返回:

json
{
  "status": "processing",
  "transactionId": "payout-202606010001",
  "traceId": "remote-trace-001",
  "remark": "accepted"
}

status 支持 processingpaidfailed。返回 processing 时提现保持处理中,后续可由后台人工标记或补偿任务继续推进;返回 paid 会立即复用现有出账逻辑;返回 failed 会释放冻结金额并标记失败。external-http Provider 不处理 Web3 提现,Web3 仍使用 web3-nativeweb3-signer

如果外部服务支持结果查询,可以配置 query-url。后台调用 /api/v1/admin/money/user-balance-withdraws/{id}/sync-payout 时,系统会向 query-url 发送当前提现单号、状态、三方交易号、资产和金额等字段,并按外部返回的 processingpaidfailed 同步提现状态。未配置 query-url 的 Provider 不支持主动同步,仍可由后台人工标记或业务方通过自定义 Provider 扩展。

Payment 打款 Provider 示例:

json
{
  "provider": "payment",
  "traceId": "withdraw-transfer-1",
  "remark": "使用默认 Payment Provider 打款"
}

如果需要指定 Payment 服务商,可以使用 payment-<provider>。分销、结算等内部自动打款前期优先使用 payment / payment-<provider>;如果要走银行 / 三方 SDK 型法币打款 Provider,再使用 payment-bank-transfer

json
{
  "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 配置默认规则。规则只在请求没有显式传入手续费字段时生效;如果接口请求已经提供 feeAmountfeeRatefeeCalculationType 或额外扣费字段,系统优先使用请求值。

示例:

yaml
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-typeproviderasset-codeasset-typesymbolchaincontract-address;配置越具体优先级越高。普通提现通常只配置主手续费;Web3 提现可额外配置 extra-* 字段,用于扣除原生币 gas 或其他平台代币费用。

Web3 原生币广播示例:

json
{
  "provider": "web3-native",
  "signedRawTransaction": "0xf86c...",
  "traceId": "trace-001",
  "remark": "外部签名服务已签名并提交广播"
}

Web3 签名打款 Provider 配置示例:

yaml
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: 10s

web3-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-httpkmsvault,自定义签名来源实现 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.clientKMS Client Bean 选择名,例如 aws-kmsaliyun-kms;为空时由 Client 自行按配置判定
kms.key-id / region / endpoint / timeout传给 KMS Client 的密钥、区域、可选私有 Endpoint 和超时配置
vault.enabled显式启用 Vault Transit 签名 Provider 的可选开关;通常只配置 vault.address / vault.key-name 和部署侧 Client
vault.clientVault Client Bean 选择名,例如 hashicorp-vault;为空时由 Client 自行按配置判定
vault.address / namespace / mount-path / key-name / timeout传给 Vault Client 的地址、命名空间、Transit 路径、密钥名和超时配置

外部 HTTP 签名 Provider 请求由框架发起,主要字段包含提现单号、链、资产、目标地址和待签名交易;响应至少返回:

json
{
  "rawTransaction": "0x...",
  "transactionHash": "0x..."
}

启用外部签名时把 provider 改为 external-http,并配置签名服务地址。签名服务可以在内部继续接 KMS、Vault、硬件签名机或其他私钥托管系统。

启用 KMS / Vault SDK 型签名时,把 provider 改为 kmsvault,并填写 key-idaddress / key-name 等必要参数。框架内置的 KMS / Vault Provider 不直接绑定某个云厂商 SDK,而是查找部署侧提供的 MoneyBalanceWithdrawWeb3KmsSignerClientMoneyBalanceWithdrawWeb3VaultSignerClient Bean。Client 收到提现单、操作人、追踪号、待签名交易和对应配置后返回 rawTransaction,框架继续负责广播、记录 txHash、处理中状态和后续链上确认。

KMS Client 最小示例:

java
@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() 读取 addressnamespacemountPathkeyName

Web3 签名打款示例:

json
{
  "provider": "web3-signer",
  "traceId": "trace-003",
  "remark": "本地签名并广播原生币提现"
}

web3-signer 只支持 web3-native 原生币提现。它是受控打款 Provider:先通过 MoneyBalanceWithdrawWeb3SignerProvider 生成已签名原始交易,再由 Web3 Manager 广播。默认 config signer provider 读取本地配置私钥,适合开发环境或明确受控的单体部署;生产环境涉及私钥安全时,推荐新增 KMS、Vault、硬件签名机或外部签名服务 Provider,只替换签名来源,不改提现单、冻结释放、广播和链上确认流程。

Web3 提现链上确认示例:

json
{
  "requiredConfirmations": 12,
  "traceId": "trace-002",
  "remark": "链上确认复核"
}

Open Platform 平台账务

Open Platform 平台账务账户和用户余额账户主体分离,但复用同一套资产、金额精度、幂等流水和余额快照规则。第一阶段已在 Open Platform 模块内落地内部 Service 能力,后台接口仍保持只读运维查询,不暴露任意手工改账入口。

核心规则:

  • 一个开放平台应用同一种法币只有一个账务账户。
  • 账务账户保存 assetCodeassetTypesymboldecimals,与用户余额账户使用同一资产语义。
  • 账务流水必须传入 idempotencyKey,重复提交同一幂等键不会重复改余额。
  • 已支持 creditdebitfreezeunfreeze 四个方向。
  • 流水保存余额变更前后快照,并检查可用余额和冻结余额不能为负数。
  • 后台账务账户和流水 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 确认和自动归集已接入同一余额流水
普通 / 法币提现manualexternal-httppayment / payment-<provider> / payment-bank-transfer前期自动打款优先用 payment / payment-<provider> 复用内部 Payment.transfer;external-http 用于企业自有打款服务,payment-bank-transfer 通过 Payment starter 的 BankTransferPaymentClient 接入银行 / 三方 SDK,作为后置扩展
Web3 提现web3-nativeweb3-signer前期自动打款优先用 Money Web3 提现签名 / 广播 / 确认;广播和链上确认分离,广播成功不等于到账;web3-signer 内部可选择 configexternal-httpkmsvault 或自定义签名来源
汇率报价internal-fixeddatabase、可选 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 平台内余额展示复验。

Released under the Apache License 2.0.