4.8 KiB
资料兑换码模块落地设计
1. 目标
本轮在现有“我的”资料与钱包 projection 上新增兑换码能力。用户兑换成功后直接增加光点余额,写入 profile_wallet_ledger,并同步刷新 profile_dashboard_state.wallet_balance。
管理侧本轮只提供后端 API,不新增管理后台页面。私有兑换码创建时支持内部 userId 与公开百梦号两类输入,后端创建阶段统一解析成内部 userId 存储。
2. 兑换码类型
RuntimeProfileRedeemCodeMode 固定为三种:
| 类型 | 规则 |
|---|---|
Public |
任意用户可兑换,max_uses 按用户独立计算。 |
Unique |
任意用户可兑换,max_uses 全局共用。 |
Private |
仅 allowed_user_ids 中的用户可兑换,max_uses 全局共用。 |
兑换码入库前必须 trim + uppercase。空兑换码、奖励为 0、次数为 0 均拒绝。
3. 表结构
3.1 profile_redeem_code
| 字段 | 类型 | 说明 |
|---|---|---|
code |
String |
主键,标准化后的兑换码。 |
mode |
RuntimeProfileRedeemCodeMode |
兑换码模式。 |
reward_points |
u64 |
单次到账光点。 |
max_uses |
u32 |
公共码为单用户上限,唯一码/私有码为全局上限。 |
global_used_count |
u32 |
全局已使用次数。公共码也记录总使用次数,但不参与公共码上限判断。 |
enabled |
bool |
是否启用。 |
allowed_user_ids |
Vec<String> |
私有码允许用户;公共/唯一码存空数组。 |
created_by |
String |
管理员用户 ID。 |
created_at |
Timestamp |
创建时间。 |
updated_at |
Timestamp |
更新时间。 |
3.2 profile_redeem_code_usage
| 字段 | 类型 | 说明 |
|---|---|---|
usage_id |
String |
主键,格式 redeem:{code}:{user_id}:{micros}:{sequence}。 |
code |
String |
兑换码。 |
user_id |
String |
兑换用户。 |
amount_granted |
u64 |
到账光点。 |
created_at |
Timestamp |
兑换时间。 |
索引:code、user_id、(code, user_id)。
4. SpacetimeDB 过程
4.1 用户兑换
redeem_profile_reward_code(input: RuntimeProfileRewardCodeRedeemInput) -> RuntimeProfileRewardCodeRedeemProcedureResult
流程:
- 标准化 code。
- 校验兑换码存在、启用、奖励大于 0。
- 按模式校验使用范围与次数。
- 同一事务内写入
profile_redeem_code_usage、增加钱包余额、写入profile_wallet_ledger,最后更新profile_redeem_code.global_used_count。 - 返回
walletBalance、amountGranted与本次ledgerEntry。
4.2 管理创建/更新
admin_upsert_profile_redeem_code(input: RuntimeProfileRedeemCodeAdminUpsertInput) -> RuntimeProfileRedeemCodeAdminProcedureResult
私有码必须至少解析出一个内部用户 ID。公共码与唯一码忽略 allowed 列表并存空数组。
4.3 管理停用
admin_disable_profile_redeem_code(input: RuntimeProfileRedeemCodeAdminDisableInput) -> RuntimeProfileRedeemCodeAdminProcedureResult
只更新 enabled=false 与 updated_at,不存在时返回“兑换码不存在”。
5. Axum API
用户接口:
POST /api/profile/redeem-codes/redeemPOST /api/runtime/profile/redeem-codes/redeem
请求:{ "code": "WELCOME2026" }
成功返回:
{
"walletBalance": 130,
"amountGranted": 100,
"ledgerEntry": {
"id": "redeem:WELCOME2026:user:1777392000000000:0",
"amountDelta": 100,
"balanceAfter": 130,
"sourceType": "redeem_code_reward",
"createdAt": "2026-04-28T00:00:00Z"
}
}
管理员接口:
POST /admin/api/profile/redeem-codesPOST /admin/api/profile/redeem-codes/disable
管理员接口复用现有 require_admin_auth。
6. 错误文案
| 场景 | message |
|---|---|
| 空 code | 兑换码不能为空 |
| 不存在 | 兑换码不存在 |
| 停用 | 兑换码已停用 |
| 奖励为 0 | 兑换码奖励无效 |
| 次数耗尽 | 兑换次数已用完 |
| 私有码账号不匹配 | 该兑换码不适用于当前账号 |
| 私有码无允许用户 | 私有兑换码必须指定可兑换用户 |
7. 前端交互
“我的”页头像右侧入口由 会员充值 改为 兑换码。点击打开独立模态窗口,窗口内只保留输入框、兑换按钮和后端返回提示,不展示兑换规则说明。
成功后展示 已到账 X 光点,并刷新 profile dashboard。失败后直接展示后端 message。
8. 测试矩阵
- Rust/module-runtime:覆盖公共码、唯一码、私有码、失败场景、流水来源和余额累加。
- Axum:覆盖用户鉴权、管理员鉴权、runtime error 到 400 的映射和兼容路径。
- 前端:覆盖入口替换、独立 modal、成功刷新余额和失败展示后端 message。
- 验证命令:
cargo test、目标前端测试、npm run api-server、npm run check:encoding。