hyapp-server/docs/币商库存授信架构.md
2026-05-12 20:07:49 +08:00

422 lines
14 KiB
Markdown
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# 币商进货与金币补偿架构设计
## 目标
币商管理列表增加“加金币”按钮。运营点击后打开“币商进货”弹窗,给币商的专用金币账户 `COIN_SELLER_COIN` 增加库存。
表单字段:
- `充值金币`:增加到币商库存的金币数量,必填。
- `类型``USDT进货` / `金币补偿`,必填。
- `充值金额`:币商线下支付给平台的 USDT 金额,仅 `USDT进货` 必填。
业务口径:
- `USDT进货`:平台收到币商线下 USDT 后,后台给币商发放 `COIN_SELLER_COIN` 库存;这笔记录计入币商进货/充值统计。
- `金币补偿`:平台因为异常、活动、人工修正等原因给币商补库存;没有充值金额,不计入币商进货/充值统计。
- 两种类型都不能给币商发放 `USD_BALANCE`,也不能直接影响玩家普通 `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 里做统计。
```sql
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 进货
- 是否计入币商充值统计
- 线下付款凭证是否唯一
- 金币补偿没有充值金额这个约束
## 主链路
```mermaid
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
新增后台接口:
```http
POST /admin/coin-sellers/{user_id}/stock-credits
```
权限:
```text
coin-seller:stock-credit
```
请求:
```json
{
"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 进货"
}
```
金币补偿请求:
```json
{
"commandId": "admin-coin-seller-comp-20260509-001",
"type": "coin_compensation",
"coinAmount": 50000,
"evidenceRef": "ticket-12345",
"reason": "上次转账异常补偿"
}
```
响应:
```json
{
"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 > 0``paymentRef` 必填、`paid_currency_code=USDT`
- `type=coin_compensation` 时,不能提交 `rechargeAmount``paid_amount_micro` 固定为 0。
- `reason` 必填。
- `evidenceRef` 首版建议必填,方便财务和客服追踪。
## gRPC 契约建议
`wallet.v1.WalletService` 增加后台命令:
```protobuf
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` 投影表。
可选投影表:
```sql
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 进货缺少 `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 金额和操作人。