hyapp-server/docs/币商库存授信架构.md

15 KiB
Raw Permalink Blame History

币商进货与金币补偿架构设计

目标

币商管理列表增加“加金币”按钮。运营点击后打开“币商进货”弹窗,给币商的专用金币账户 COIN_SELLER_COIN 增加库存。

表单字段:

  • 充值金币:增加到币商库存的金币数量,必填。
  • 类型USDT进货 / 金币补偿,必填。
  • 充值金额:币商线下支付给平台的 USDT 金额,仅 USDT进货 必填。

业务口径:

  • USDT进货:平台收到币商线下 USDT 后,后台给币商发放 COIN_SELLER_COIN 库存;这笔记录计入币商进货/充值统计。
  • 金币补偿:平台因为异常、活动、人工修正等原因给币商补库存;没有充值金额,不计入币商进货/充值统计。
  • 两种类型都不能给币商发放 HOST_SALARY_USD, AGENCY_SALARY_USD, BD_SALARY_USD, ADMIN_SALARY_USD,也不能直接影响玩家普通 COIN

服务边界

模块 职责
server/admin 币商列表、按钮权限、弹窗表单、后台审计、调用 user-service 校验币商身份、调用 wallet-service 发放库存
user-service 保存 coin_seller_profiles 身份事实,判断目标用户是否是有效币商
wallet-service 拥有 COIN_SELLER_COIN 账户、钱包流水、币商进货记录、幂等和 outbox
gateway-service 不参与后台币商进货App 币商给玩家转金币仍走现有 /api/v1/wallet/coin-seller/transfer

边界规则:

  • server/admin 不能直接改 wallet_accounts
  • user-service 不能保存币商余额。
  • wallet-service 不判断后台菜单权限,但必须保存完整账务流水和进货记录。
  • 币商库存入账必须走钱包命令,不能用通用 SQL 修余额。

后台交互

币商列表

现有币商列表继续展示:

  • 用户 ID / 短号
  • 昵称 / 头像
  • 区域
  • 币商状态
  • 币商库存余额 merchantBalance
  • 创建时间 / 更新时间

新增行操作:

按钮 权限 状态
加金币 coin-seller:stock-credit 仅 active 币商可点击disabled 币商按钮置灰

如果后续产品要求 disabled 币商也能补偿库存,必须新增单独权限或二次确认,不在首版默认放开。

弹窗表单

弹窗标题:币商进货

只读信息:

  • 币商用户 ID
  • 币商短号
  • 昵称
  • 当前库存余额
  • 区域

可编辑字段:

字段 类型 规则
type 单选 usdt_purchase / coin_compensation
coin_amount 正整数 两种类型都必填,表示增加的 COIN_SELLER_COIN
recharge_amount 金额 usdt_purchase 展示并必填;coin_compensation 不展示也不提交
payment_ref 文本 usdt_purchase 必填,建议填链上 tx hash、收款单号或财务凭证号
evidence_ref 文本 凭证附件或后台上传后的文件 ID首版建议两种类型都必填
reason 文本 必填,写清楚进货或补偿原因
command_id 文本 操作端前端按单次授信动作生成,同一动作重试必须复用

金额存储规则:

  • recharge_amount 前端可以展示小数,例如 120.50
  • 服务端统一转成 paid_amount_microUSDT 按 6 位精度保存。
  • coin_compensation 必须满足 paid_currency_code=''paid_amount_micro=0,如果客户端传了金额,服务端直接拒绝。

账务模型

币商库存仍然是钱包资产:

  • 资产类型:COIN_SELLER_COIN
  • 账户表:wallet_accounts
  • 流水表:wallet_transactions
  • 分录表:wallet_entries
  • 事件表:wallet_outbox

新增专门的币商进货明细表,避免从通用钱包 metadata 里做统计。

coin_seller_stock_records(
  app_code VARCHAR(32) NOT NULL DEFAULT 'lalu',
  stock_id VARCHAR(96) NOT NULL,
  transaction_id VARCHAR(96) NOT NULL,
  command_id VARCHAR(128) NOT NULL,
  seller_user_id BIGINT NOT NULL,
  stock_type VARCHAR(32) NOT NULL,
  counts_as_seller_recharge BOOLEAN NOT NULL,
  coin_amount BIGINT NOT NULL,
  paid_currency_code VARCHAR(8) NOT NULL DEFAULT '',
  paid_amount_micro BIGINT NOT NULL DEFAULT 0,
  payment_ref VARCHAR(128) NULL,
  evidence_ref VARCHAR(256) NOT NULL DEFAULT '',
  operator_user_id BIGINT NOT NULL,
  reason VARCHAR(512) NOT NULL DEFAULT '',
  balance_after BIGINT NOT NULL,
  metadata_json JSON NULL,
  created_at_ms BIGINT NOT NULL,
  PRIMARY KEY (app_code, stock_id),
  UNIQUE KEY uk_coin_seller_stock_command (app_code, command_id),
  UNIQUE KEY uk_coin_seller_stock_payment_ref (app_code, stock_type, payment_ref),
  KEY idx_coin_seller_stock_seller_time (app_code, seller_user_id, created_at_ms),
  KEY idx_coin_seller_stock_type_time (app_code, stock_type, created_at_ms)
);

字段语义:

  • stock_type='usdt_purchase'counts_as_seller_recharge=truepaid_currency_code='USDT'paid_amount_micro>0
  • stock_type='coin_compensation'counts_as_seller_recharge=falsepaid_currency_code=''paid_amount_micro=0
  • coin_amount 是发给币商的库存金币,不是玩家充值金币。
  • payment_ref 对 USDT 进货必须唯一,防止同一笔线下收款重复入账。
  • balance_after 保存账后币商库存,后台详情不用再回放钱包分录。

注意:这张表不是 wallet_recharge_recordswallet_recharge_records 只记录币商给玩家转金币后形成的“玩家充值事实”。币商自己进货应该单独统计,不能混到玩家充值记录里。

业务类型

钱包交易 biz_type 建议拆成两个值:

biz_type 说明 是否计入币商充值统计
coin_seller_stock_purchase USDT 进货,平台收到币商付款后发放库存
coin_seller_coin_compensation 人工金币补偿、异常修复、活动补库存

不要复用 manual_credit,因为通用后台入账无法表达:

  • 是否是币商库存
  • 是否是 USDT 进货
  • 是否计入币商充值统计
  • 线下付款凭证是否唯一
  • 金币补偿没有充值金额这个约束

主链路

sequenceDiagram
    autonumber
    participant Admin as Admin Web
    participant AdminSrv as server/admin
    participant User as user-service
    participant Wallet as wallet-service
    participant DB as wallet MySQL
    participant Audit as admin_operation_logs

    Admin->>AdminSrv: POST /admin/coin-sellers/{user_id}/stock-credits
    AdminSrv->>AdminSrv: 校验 coin-seller:stock-credit 权限和表单
    AdminSrv->>User: GetCoinSellerProfile(user_id)
    User-->>AdminSrv: active coin_seller profile
    AdminSrv->>Wallet: AdminCreditCoinSellerStock(command)
    Wallet->>DB: BEGIN
    Wallet->>DB: 按 command_id / payment_ref 做幂等和重复收款校验
    Wallet->>DB: SELECT wallet_accounts FOR UPDATE
    Wallet->>DB: 增加 COIN_SELLER_COIN
    Wallet->>DB: 写 wallet_transactions / wallet_entries
    Wallet->>DB: 写 coin_seller_stock_records
    Wallet->>DB: 写 wallet_outbox
    Wallet->>DB: COMMIT
    AdminSrv->>Audit: 写后台操作审计
    AdminSrv-->>Admin: 返回 transaction_id 和账后余额

失败边界:

  • user-service 判断不是 active 币商时,不调用 wallet-service。
  • wallet-service 入账失败时,不写成功审计;可以写失败审计或错误日志。
  • 钱包事务提交成功但后台审计写失败时,必须记录告警;后续需要审计补偿任务,不能静默吞掉。

Admin API

新增后台接口:

POST /admin/coin-sellers/{user_id}/stock-credits

权限:

coin-seller:stock-credit

请求:

{
  "commandId": "admin-coin-seller-stock-20260509-001",
  "type": "usdt_purchase",
  "coinAmount": 8000000,
  "rechargeAmount": "100.000000",
  "paymentRef": "usdt-tx-hash-or-offline-receipt",
  "evidenceRef": "oss://admin-evidence/receipt.png",
  "reason": "币商 USDT 进货"
}

金币补偿请求:

{
  "commandId": "admin-coin-seller-comp-20260509-001",
  "type": "coin_compensation",
  "coinAmount": 50000,
  "evidenceRef": "ticket-12345",
  "reason": "上次转账异常补偿"
}

响应:

{
  "transactionId": "lalu_admin_coin_seller_stock_...",
  "sellerUserId": "163001",
  "stockType": "usdt_purchase",
  "coinAmount": 8000000,
  "paidCurrencyCode": "USDT",
  "paidAmountMicro": 100000000,
  "countsAsSellerRecharge": true,
  "balanceAfter": 12800000,
  "createdAtMs": 1760000000000
}

校验:

  • user_id 必须是 active coin_seller
  • coinAmount > 0
  • type=usdt_purchase 时,rechargeAmount > 0paymentRef 必填、paid_currency_code=USDT
  • type=coin_compensation 时,不能提交 rechargeAmountpaid_amount_micro 固定为 0。
  • reason 必填。
  • evidenceRef 首版建议必填,方便财务和客服追踪。

gRPC 契约建议

wallet.v1.WalletService 增加后台命令:

message AdminCreditCoinSellerStockRequest {
  string command_id = 1;
  int64 seller_user_id = 2;
  string stock_type = 3;
  int64 coin_amount = 4;
  string paid_currency_code = 5;
  int64 paid_amount_micro = 6;
  string payment_ref = 7;
  string evidence_ref = 8;
  int64 operator_user_id = 9;
  string reason = 10;
  string app_code = 11;
}

message AdminCreditCoinSellerStockResponse {
  string transaction_id = 1;
  int64 seller_user_id = 2;
  string stock_type = 3;
  int64 coin_amount = 4;
  string paid_currency_code = 5;
  int64 paid_amount_micro = 6;
  bool counts_as_seller_recharge = 7;
  int64 balance_after = 8;
  int64 created_at_ms = 9;
}

server/admin 负责在调用前通过 user-service 校验币商身份;wallet-service 负责校验金额、类型、幂等、重复 payment_ref 和钱包事务。

统计口径

币商库存相关统计从 coin_seller_stock_records 读取:

指标 查询条件
币商累计 USDT 进货金币 stock_type='usdt_purchase'
币商累计 USDT 进货金额 counts_as_seller_recharge=true,汇总 paid_amount_micro
币商累计金币补偿 stock_type='coin_compensation'
当前库存余额 wallet_accounts.asset_type='COIN_SELLER_COIN'
币商给玩家充值金币 现有 wallet_recharge_recordsbiz_type='coin_seller_transfer'

金币补偿不计入币商充值统计的落地方式:

  • 表单不展示 充值金额
  • API 不接受 rechargeAmount
  • 数据库存 paid_amount_micro=0
  • counts_as_seller_recharge=false
  • 统计 SQL 必须带 counts_as_seller_recharge=truestock_type='usdt_purchase'

列表性能

币商列表不要每行实时做大范围流水汇总。

首版建议:

  • 列表只展示当前库存余额 merchantBalance,继续按当前实现从 wallet_accounts 按当页 user_id 批量查询。
  • 如果产品要展示累计进货金额、累计补偿金币、最近进货时间,只对当前页 user_id 从 coin_seller_stock_records 做 group by。
  • 如果后续需要按累计进货金额排序或筛选,再增加 coin_seller_stock_stats 投影表。

可选投影表:

coin_seller_stock_stats(
  app_code VARCHAR(32) NOT NULL DEFAULT 'lalu',
  seller_user_id BIGINT NOT NULL,
  purchase_coin_total BIGINT NOT NULL DEFAULT 0,
  purchase_paid_usdt_micro_total BIGINT NOT NULL DEFAULT 0,
  compensation_coin_total BIGINT NOT NULL DEFAULT 0,
  last_stock_at_ms BIGINT NOT NULL DEFAULT 0,
  updated_at_ms BIGINT NOT NULL,
  PRIMARY KEY (app_code, seller_user_id)
);

投影更新可以和钱包事务同事务完成,也可以由 wallet outbox 异步更新。首版如果列表不展示累计值,不需要这张表。

Outbox 事件

钱包事务成功后写出:

  • WalletCoinSellerStockPurchased
  • WalletCoinSellerCoinCompensated
  • WalletBalanceChanged

事件 payload

  • seller_user_id
  • stock_type
  • coin_amount
  • paid_currency_code
  • paid_amount_micro
  • counts_as_seller_recharge
  • transaction_id
  • operator_user_id
  • created_at_ms

这些事件可用于后台通知、财务报表、风控告警和统计投影。

权限和审计

新增权限:

Permission Kind Usage
coin-seller:stock-credit button 币商进货、金币补偿

审计 action

Action Resource 说明
coin-seller-stock-credit coin_seller_stock_records USDT 进货或金币补偿成功

审计 detail 必须包含:

  • command_id
  • seller_user_id
  • stock_type
  • coin_amount
  • paid_currency_code
  • paid_amount_micro
  • payment_ref
  • transaction_id
  • reason

USDT 进货金额属于财务口径,不能只放前端日志。

错误码建议

纳入 pkg/xerr catalog

业务错误 gRPC code 说明
COIN_SELLER_NOT_ACTIVE FailedPrecondition 目标用户不是 active 币商
COIN_SELLER_STOCK_TYPE_INVALID InvalidArgument 类型不是 USDT 进货或金币补偿
COIN_SELLER_STOCK_AMOUNT_INVALID InvalidArgument 金币数或 USDT 金额不合法
COIN_SELLER_PAYMENT_REF_DUPLICATED AlreadyExists 同一 USDT 凭证已入账
IDEMPOTENCY_CONFLICT AlreadyExists command_id 重复但 payload 不同

客户端展示文案由 admin 前端根据业务 code 转换,不从内部错误字符串拼 UI。

实现顺序

  1. 增加权限种子:coin-seller:stock-credit,币商列表行操作按权限展示。
  2. 扩展 api/proto/wallet/v1/wallet.proto:新增 AdminCreditCoinSellerStock 请求和响应。
  3. 增加 wallet MySQL 表 coin_seller_stock_records
  4. wallet-service 实现专用命令校验类型、金额、payment_ref、幂等锁定 COIN_SELLER_COIN 账户并入账。
  5. server/admin 增加接口:校验 active 币商身份,调用 wallet-service成功后写 admin_operation_logs
  6. 后台页面币商列表增加“加金币”按钮和弹窗表单。
  7. 增加列表成功后的刷新策略:成功后刷新当前行余额或重新拉取当前页。
  8. 如需累计统计,再增加当前页聚合查询或 coin_seller_stock_stats 投影。

测试重点

必须覆盖:

  • active 币商 USDT 进货成功,COIN_SELLER_COIN 增加,counts_as_seller_recharge=true
  • USDT 进货缺少 rechargeAmountpaymentRef 被拒绝。
  • 同一个 paymentRef 不能重复进货。
  • 金币补偿成功,COIN_SELLER_COIN 增加,paid_amount_micro=0counts_as_seller_recharge=false
  • 金币补偿传入 rechargeAmount 被拒绝。
  • disabled 或不存在的币商不能加金币。
  • 相同 command_id 和相同 payload 重试返回原交易结果。
  • 相同 command_id 但 payload 不同返回幂等冲突。
  • wallet 事务失败时不写进货记录、不改余额。
  • admin 审计记录包含 transaction_id、类型、金币数、USDT 金额和操作人。