# 应用奖励兑换码（CDK，原通用码券）

认证中心的应用奖励兑换码服务与用户软件授权激活码相互独立。每个码批次归属于一个应用；接入方使用本应用 API Key 时，不能查询或核销其他应用的码。奖励由接入方根据核销快照完成实际发放，不等于平台软件使用权益。

## 管理能力

管理后台的「应用奖励兑换码」（旧入口名「通用码券」）支持：

- 激活码、兑换码、CDK、礼包码、试用码和自定义码；
- 按批次生成最多 1000 个随机码，加密保存原码并仅以尾号展示；
- 为批次配置 `entitlement_key` 和不超过 16 KiB 的 JSON 权益数据，并可保存为按应用隔离的权益模板；
- 配置开始时间、截止时间、单码总兑换次数、单个主体兑换次数；
- 在「批次 / 单码 / 核销记录 / 权益模板」四个视角查询和管理；批次可以编辑、整批作废或恢复，单码可以停用或恢复；
- 未发生核销的批次或单码可永久删除。已有核销记录的对象保留审计历史，只允许作废，避免把已经发生的业务事实抹掉；
- 记录每次核销的应用、外部主体、幂等键、业务元数据和时间。

修改批次权益只影响修改后的新核销。每次成功核销都会保存当时的权益快照，因此旧请求的幂等重试仍返回原来的权益，不会因管理员后来编辑批次而改变。

当前管理接口仍使用平台范围的 `authorizations:write`，没有独立的 CDK 只读、写入或原码导出权限。管理页按应用筛选不是授权隔离；上文的应用边界指接入方 API Key 接口。独立权限及历史角色映射由 #23 跟踪，须经明确批准和迁移验收后再实施，不自动扩大既有角色权限。

`subject_id` 由接入方定义，可以是游戏角色 ID、游戏账号 ID、软件账号 ID 或设备主体。若奖励是账号共享的，应传账号 ID；若奖励属于单角色，应传经过游戏服校验的角色 ID。

## 应用配置

应用需要启用以下 API Key 权限：

- `codes:read`：调用校验接口；
- `codes:redeem`：调用原子核销接口。

API Key 只能保存在接入方服务端，不得打包进游戏或软件客户端。两个接口不接受浏览器 Cookie 或 OAuth 用户令牌。

认证中心不会直接修改第三方客户端或游戏账号。客户端只负责收集兑换码并提交给自己的业务服务端；业务服务端核销成功后，必须根据白名单中的 `entitlement_key` 和经过校验的 `payload`，在自己的数据库、库存、会员或邮件系统中完成实际发放。

## 校验接口

```http
POST /api/v1/codes/validate
X-API-Key: <应用 API Key>
Content-Type: application/json

{
  "code": "CDK-...",
  "subject_id": "role-123"
}
```

HTTP 200 只表示请求成功，必须检查 `data.valid`。可能的 `reason` 包括 `not_found`、`disabled`、`not_started`、`expired`、`exhausted` 和 `subject_limit`。校验不会预占或消费兑换次数，不能代替核销。

## 核销接口

```http
POST /api/v1/codes/redeem
X-API-Key: <应用 API Key>
Content-Type: application/json

{
  "code": "CDK-...",
  "subject_id": "role-123",
  "idempotency_key": "cdk:role-123:request-uuid",
  "metadata": {
    "server_id": "area-1"
  }
}
```

成功响应示例：

```json
{
  "code": 0,
  "message": "success",
  "data": {
    "redemption_id": "...",
    "duplicate": false,
    "code_type": "cdk",
    "entitlement_key": "starter_pack_v1",
    "payload": {
      "mail_template": "starter",
      "items": [{"id": 1001, "count": 5}]
    },
    "batch_id": "...",
    "batch_name": "新手礼包",
    "starts_at": "...",
    "expires_at": null
  }
}
```

同一业务请求重试时必须复用相同的 `idempotency_key`。认证中心会返回原核销结果并将 `duplicate` 置为 `true`；同一幂等键若改用其他码或主体，会返回 HTTP 409。

## 游戏服接入流程

1. 客户端把 `{code, role_id}` 发给游戏服的 `cdk/redeem` RPC。
2. 游戏服从已认证会话获取用户 ID，并验证 `role_id` 确实属于该用户。
3. 游戏服生成并持久化请求 ID，以该值构造稳定的 `idempotency_key`，调用认证中心核销接口。
4. 游戏服只按 `entitlement_key` 查找本地白名单奖励规则；`payload` 用于经过严格校验的参数，不直接执行任意操作。
5. 游戏服使用 `redemption_id` 作为发奖唯一键，在本地事务或发件箱中创建奖励邮件。超时重试认证中心时复用原幂等键，发奖重试时复用原 `redemption_id`。
6. 客户端重新拉取账号权益、背包或邮件列表并刷新界面；不要只在客户端本地加数值，因为那既不可信也无法跨设备生效。

这样即使认证中心成功核销后游戏服短暂崩溃，重试也能取得相同结果且不会重复发奖。

## 数据与安全边界

- 原码使用 SHA-256 哈希核验，并通过现有 `DATA_KEY_FILE` 的 AES-GCM 密钥加密保存；日志和核销记录不保存原码。
- 核销会锁定目标码记录，在一个数据库事务中检查有效期及次数并写入使用记录。
- 应用 ID 由 API Key 推导，不接受请求体指定，避免跨应用核销。
- `metadata` 最大 4 KiB，权益 JSON 最大 16 KiB；管理端批次名称、权益键、备注和次数均有边界校验。
- 上线前应使用 HTTPS、轮换已泄露的 API Key，并限制认证中心入口仅允许可信游戏服访问。
