Files
Genarrative/docs/technical/PROFILE_REDEEM_CODE_IMPLEMENTATION_2026-04-28.md

4.9 KiB
Raw Blame History

资料兑换码模块落地设计

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 兑换时间。

索引:codeuser_id(code, user_id)

4. SpacetimeDB 过程

4.1 用户兑换

redeem_profile_reward_code(input: RuntimeProfileRewardCodeRedeemInput) -> RuntimeProfileRewardCodeRedeemProcedureResult

流程:

  1. 标准化 code。
  2. 校验兑换码存在、启用、奖励大于 0。
  3. 按模式校验使用范围与次数。
  4. 同一事务内写入 profile_redeem_code_usage、增加钱包余额、写入 profile_wallet_ledger,最后更新 profile_redeem_code.global_used_count
  5. 返回 walletBalanceamountGranted 与本次 ledgerEntry

4.2 管理创建/更新

admin_upsert_profile_redeem_code(input: RuntimeProfileRedeemCodeAdminUpsertInput) -> RuntimeProfileRedeemCodeAdminProcedureResult

私有码必须至少解析出一个内部用户 ID。公共码与唯一码忽略 allowed 列表并存空数组。

4.3 管理停用

admin_disable_profile_redeem_code(input: RuntimeProfileRedeemCodeAdminDisableInput) -> RuntimeProfileRedeemCodeAdminProcedureResult

只更新 enabled=falseupdated_at,不存在时返回“兑换码不存在”。

5. Axum API

用户接口:

  • POST /api/profile/redeem-codes/redeem
  • POST /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-codes
  • POST /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:maincloudnpm run check:encoding