15 KiB
币商进货与金币补偿架构设计
目标
币商管理列表增加“加金币”按钮。运营点击后打开“币商进货”弹窗,给币商的专用金币账户 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_micro,USDT 按 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=true,paid_currency_code='USDT',paid_amount_micro>0。stock_type='coin_compensation':counts_as_seller_recharge=false,paid_currency_code='',paid_amount_micro=0。coin_amount是发给币商的库存金币,不是玩家充值金币。payment_ref对 USDT 进货必须唯一,防止同一笔线下收款重复入账。balance_after保存账后币商库存,后台详情不用再回放钱包分录。
注意:这张表不是 wallet_recharge_records。wallet_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必须是 activecoin_seller。coinAmount > 0。type=usdt_purchase时,rechargeAmount > 0、paymentRef必填、paid_currency_code=USDT。type=coin_compensation时,不能提交rechargeAmount,paid_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_records 或 biz_type='coin_seller_transfer' |
金币补偿不计入币商充值统计的落地方式:
- 表单不展示
充值金额。 - API 不接受
rechargeAmount。 - 数据库存
paid_amount_micro=0。 counts_as_seller_recharge=false。- 统计 SQL 必须带
counts_as_seller_recharge=true或stock_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 事件
钱包事务成功后写出:
WalletCoinSellerStockPurchasedWalletCoinSellerCoinCompensatedWalletBalanceChanged
事件 payload:
seller_user_idstock_typecoin_amountpaid_currency_codepaid_amount_microcounts_as_seller_rechargetransaction_idoperator_user_idcreated_at_ms
这些事件可用于后台通知、财务报表、风控告警和统计投影。
权限和审计
新增权限:
| Permission | Kind | Usage |
|---|---|---|
coin-seller:stock-credit |
button |
币商进货、金币补偿 |
审计 action:
| Action | Resource | 说明 |
|---|---|---|
coin-seller-stock-credit |
coin_seller_stock_records |
USDT 进货或金币补偿成功 |
审计 detail 必须包含:
command_idseller_user_idstock_typecoin_amountpaid_currency_codepaid_amount_micropayment_reftransaction_idreason
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。
实现顺序
- 增加权限种子:
coin-seller:stock-credit,币商列表行操作按权限展示。 - 扩展
api/proto/wallet/v1/wallet.proto:新增AdminCreditCoinSellerStock请求和响应。 - 增加 wallet MySQL 表
coin_seller_stock_records。 - 在
wallet-service实现专用命令:校验类型、金额、payment_ref、幂等,锁定COIN_SELLER_COIN账户并入账。 - 在
server/admin增加接口:校验 active 币商身份,调用 wallet-service,成功后写admin_operation_logs。 - 后台页面币商列表增加“加金币”按钮和弹窗表单。
- 增加列表成功后的刷新策略:成功后刷新当前行余额或重新拉取当前页。
- 如需累计统计,再增加当前页聚合查询或
coin_seller_stock_stats投影。
测试重点
必须覆盖:
- active 币商 USDT 进货成功,
COIN_SELLER_COIN增加,counts_as_seller_recharge=true。 - USDT 进货缺少
rechargeAmount或paymentRef被拒绝。 - 同一个
paymentRef不能重复进货。 - 金币补偿成功,
COIN_SELLER_COIN增加,paid_amount_micro=0,counts_as_seller_recharge=false。 - 金币补偿传入
rechargeAmount被拒绝。 - disabled 或不存在的币商不能加金币。
- 相同
command_id和相同 payload 重试返回原交易结果。 - 相同
command_id但 payload 不同返回幂等冲突。 - wallet 事务失败时不写进货记录、不改余额。
- admin 审计记录包含 transaction_id、类型、金币数、USDT 金额和操作人。