192 lines
14 KiB
Markdown
192 lines
14 KiB
Markdown
# Aslan 游戏王活动接口
|
||
|
||
本文对应 H5 `activity/game-king/aslan.html` 与 webconsole 游戏王配置页。接口契约以当前仓库的 `AslanGameKingRestController`、`AslanGameKingBackRestController`、`AslanGameKingModels` 和 `AslanGameKingWalletListener` 为准。
|
||
|
||
## 通用约定
|
||
|
||
- 下列地址均为网关或 webconsole 域名后的相对路径;H5 通过现有 `common_aslan` 请求层调用。
|
||
- Controller 返回值会由框架包装为 `ResultResponse`:成功时核心结构为 `{"status":true,"errorCode":0,"time":<毫秒时间戳>,"body":...}`;业务数据均在 `body`。
|
||
- DTO 中的 Long ID 在响应 JSON 中按字符串输出,H5 应保留字符串,不要转成 JavaScript Number。
|
||
- 时间字段 `startTime`、`endTime`、`settlementTime`、`serverTime`、`drawTime`、`deliverTime` 均为 Unix 毫秒时间戳。
|
||
|
||
### H5 鉴权头
|
||
|
||
H5 五个接口都使用网关鉴权,不接受客户端提交 `userId` 或 `sysOrigin` 业务字段。
|
||
|
||
| Header | 必填 | 精确格式/说明 |
|
||
| --- | --- | --- |
|
||
| `Authorization` | 是 | `Bearer <app access token>`;网关从已校验 token 取得用户 ID,再通过内部头注入 `AppExtCommand.reqUserId`。 |
|
||
| `Req-Sys-Origin` | 是 | `origin=<活动所属系统>;originChild=<子系统>;`。`origin` 必须与后台活动的 `sysOrigin` 完全相同;未传 `originChild` 时框架回落为 `origin`。 |
|
||
| `Req-Client` | 是 | H5 固定传 `H5`。 |
|
||
| `Req-App-Intel` | 是 | 分号分隔,例如 `version=<版本>;build=<正整数>;channel=<渠道>;`;`version`、正数 `build`、`channel` 都由网关校验。 |
|
||
|
||
`Req-Inner-Internal` 是网关生成的内部头,H5 不应自行构造。抽奖 JSON 即使额外带入 `reqUserId`、`reqSysOrigin`,Controller 也不会使用这些字段。
|
||
|
||
### Webconsole 鉴权
|
||
|
||
webconsole 接口使用 `Authorization: Bearer <console JWT>`。迁移 SQL 同时登记了 `activity:game-king:*` 资源与 `ActivityGameKing` 菜单;账号需拥有相应后台菜单/资源权限。
|
||
|
||
## H5 接口
|
||
|
||
基础路径:`/activity/game/king/aslan`
|
||
|
||
### 1. 活动详情
|
||
|
||
`GET /activity/game/king/aslan/detail`
|
||
|
||
Query:
|
||
|
||
| 参数 | 必填 | 说明 |
|
||
| --- | --- | --- |
|
||
| `activityId` | 否 | 活动 ID。省略时,按当前 `Req-Sys-Origin` 依次选择进行中、最近将开始、最近结束的已启用活动。 |
|
||
|
||
`body` 为 `Detail`:
|
||
|
||
- `activity`:活动配置,主要字段为 `id`、`activityCode`、`activityName`、`activityDesc`、`sysOrigin`、`timeZone`、`startTime`、`endTime`、`settlementDelayMinutes`、`settlementTime`、`coinPerDraw`、`rankingType`、`rankingPeriod`、`enabled`、`settlementStatus`。兼容字段 `eventTypes` 恒为 `["GAME_EXPENDITURE"]`,不代表可配置白名单。
|
||
- `prizes`:7 个奖项;主要字段为 `id`、`prizeName`、`prizeImage`、`weight`、`stock`、`resourceGroupId`、`sortOrder`、`rewardItems`。
|
||
- `rankRewards`:固定档位 `1`、`2`、`3`、`4-7`、`8-10`、`11-30` 及其 `resourceGroupId`、`rewardName`、`rewardItems`。
|
||
- `supportedRankingTypes`:固定为 `TYCOON`、`VICTORIOUS`。
|
||
- `supportedRankingPeriods`:固定为 `DAILY`、`OVERALL`。
|
||
- `activityStatus`:`UPCOMING`、`ACTIVE`、`ENDED` 或 `DISABLED`。
|
||
- `serverTime`:服务端当前毫秒时间戳。
|
||
|
||
### 2. 我的活动状态
|
||
|
||
`GET /activity/game/king/aslan/me`
|
||
|
||
Query:可选 `activityId`,选择规则同详情接口。
|
||
|
||
`body` 为 `UserState`:`activityId`、`totalConsumed`、`earnedChances`、`usedChances`、`availableChances`、`nextChanceRemaining`、`rank`。其中抽奖机会只由支出流水累计,`earnedChances = floor(totalConsumed / coinPerDraw)`。
|
||
|
||
### 3. 抽奖
|
||
|
||
`POST /activity/game/king/aslan/draw`
|
||
|
||
Body:
|
||
|
||
```json
|
||
{
|
||
"activityId": "可选活动ID",
|
||
"requestId": "本次点击生成且重试时保持不变的唯一值"
|
||
}
|
||
```
|
||
|
||
- `requestId` 必填、去除首尾空格后非空、最长 64 字符。
|
||
- 幂等范围为“活动 + 当前用户 + `requestId`”。网络重试必须复用原值;新的一次抽奖必须生成新值。
|
||
- 仅 `ACTIVE` 活动且 `availableChances > 0` 时可抽。
|
||
|
||
`body` 为 `DrawResult`:
|
||
|
||
- `record`:`id`、`activityId`、`userId`、`requestId`、`prizeId`、`prizeName`、`prizeImage`、`resourceGroupId`、`deliveryStatus`、`failureReason`、`drawTime`、`deliverTime`。
|
||
- `prize`:命中奖项及展示用 `rewardItems`。
|
||
- `remainingChances`:本次扣减后的剩余机会。
|
||
- `idempotentReplay`:是否返回同一 `requestId` 的已有结果。
|
||
|
||
### 4. 排行榜
|
||
|
||
`GET /activity/game/king/aslan/ranking`
|
||
|
||
| Query 参数 | 必填 | 说明 |
|
||
| --- | --- | --- |
|
||
| `activityId` | 否 | 活动 ID。 |
|
||
| `rankingType` | 否 | `TYCOON`(游戏支出)或兼容维度 `VICTORIOUS`;省略时使用活动配置。本活动事件采集只接收支出,因此运营配置固定使用 `TYCOON`。 |
|
||
| `period` | 否 | `DAILY` 或 `OVERALL`;省略时使用活动配置。 |
|
||
| `limit` | 否 | 默认 30,服务端限制到 1~100。 |
|
||
|
||
`body` 为 `Ranking`:`rankingType`、`period`、`statDate`、`entries`、`my`。`DAILY` 的 `statDate` 是活动 `timeZone` 下当天的 `yyyy-MM-dd`,`OVERALL` 时为空。每个 `RankEntry` 包含 `rank`、`userId`、`account`、`nickname`、`avatar`、`score`、`totalConsumed`、`totalWon`、`me`。
|
||
|
||
同分排序固定为:分值降序、首次到达该分值时间升序、`userId` 升序。
|
||
|
||
### 5. 我的抽奖记录
|
||
|
||
`GET /activity/game/king/aslan/draw-records`
|
||
|
||
| Query 参数 | 必填 | 说明 |
|
||
| --- | --- | --- |
|
||
| `activityId` | 否 | 活动 ID。 |
|
||
| `limit` | 否 | 默认 20,服务端限制到 1~100。 |
|
||
|
||
`body` 为 `DrawRecord[]`,字段同抽奖结果中的 `record`。逐项发奖明细 `deliveryItems` 只在管理端填充。
|
||
|
||
## Webconsole 管理接口
|
||
|
||
基础路径:`/activity/game-king`
|
||
|
||
| 方法与路径 | 关键入参 | `body`/作用 |
|
||
| --- | --- | --- |
|
||
| `GET /list` | 无 | `Activity[]`。 |
|
||
| `GET /detail/{id}` | 活动 ID | `Detail`。 |
|
||
| `POST /save` | `SaveCommand` JSON | 新增或修改活动,返回 `Activity`。 |
|
||
| `PUT /enable/{id}` | Query `enabled=true/false` | 启停活动。活动开始后不能再改变启用状态。 |
|
||
| `GET /prizes/{id}` | 活动 ID | `Prize[]`。 |
|
||
| `PUT /prizes/{id}` | `{"activityId":"同路径ID","prizes":[...]}` | 覆盖保存 7 个奖项;活动开始后禁止修改。 |
|
||
| `GET /rank-rewards/{id}` | 活动 ID | `RankReward[]`。 |
|
||
| `PUT /rank-rewards/{id}` | `{"activityId":"同路径ID","rankRewards":[...]}` | 覆盖保存 6 个固定排名奖励档;活动开始后禁止修改。 |
|
||
| `GET /draw-records/{id}` | 可选 `deliveryStatus`、`limit` | 抽奖记录及 `deliveryItems`;状态可为 `PENDING/PROCESSING/SUCCESS/FAILED/UNKNOWN`,`limit` 默认/最大均为 100。 |
|
||
| `POST /draw/retry/{recordId}` | 抽奖记录 ID | 仅重试已核实可重试的 `FAILED` 记录,不处理 `UNKNOWN`。 |
|
||
| `GET /settlement-records/{id}` | 活动 ID | Top 30 冻结结算记录及逐项 `deliveryItems`。 |
|
||
| `POST /settlement/{id}` | 活动 ID | 到达 `settlementTime` 后冻结 TYCOON/OVERALL Top 30 并发奖。 |
|
||
| `POST /settlement/retry/{recordId}` | 结算记录 ID | 仅重试已核实可重试的 `FAILED` 记录,不处理 `UNKNOWN`。 |
|
||
| `POST /delivery-item/resolve/{itemId}` | Query `delivered=true/false` | 只处理 `UNKNOWN` 单项;语义见下文。 |
|
||
| `POST /backfill/{id}` | `lastId`、`limit` 可选 | 服务端按固定第三方游戏支出口径分页补算钱包流水,返回 `BackfillResult`。 |
|
||
|
||
### `SaveCommand` 关键字段与约束
|
||
|
||
| 字段 | 约束 |
|
||
| --- | --- |
|
||
| `id` | 修改时传;新增不传。 |
|
||
| `activityCode` | 必填,最长 64,活动间唯一。 |
|
||
| `activityName` | 必填,最长 128。 |
|
||
| `activityDesc` | 可选,最长 1000。 |
|
||
| `sysOrigin` | 必填,必须是服务端支持的 `SysOriginPlatformEnum` 值,并与 H5 `Req-Sys-Origin.origin`、钱包事件 `sysOrigin` 一致。 |
|
||
| `timeZone` | 必填;默认 `Asia/Riyadh`,必须是合法 IANA 时区。 |
|
||
| `startTime`、`endTime` | 必填毫秒时间戳,且 `startTime < endTime`。 |
|
||
| `settlementDelayMinutes` | 0~1440,默认 30;`settlementTime = endTime + settlementDelayMinutes`。 |
|
||
| `coinPerDraw` | 必填,正整数金币数。 |
|
||
| `eventTypes` | 已废弃且可不传;旧客户端传值也会被忽略,服务端统一保存 `GAME_EXPENDITURE` 兼容哨兵。 |
|
||
| `rankingType` | `TYCOON` 或 `VICTORIOUS`,默认 `TYCOON`。 |
|
||
| `rankingPeriod` | `DAILY` 或 `OVERALL`,默认 `OVERALL`。 |
|
||
| `enabled` | 默认 `false`。首次以 `true` 保存时,`startTime` 必须晚于当前时间。 |
|
||
| `prizes`、`rankRewards` | 可随活动一起保存,也可通过独立接口保存。启用前两者都必须完整。 |
|
||
|
||
同一 `sysOrigin` 的已启用活动时间窗不得重叠。活动开始后,统计时间、系统、时区、结算延时、`coinPerDraw` 和启用状态等统计口径会锁定。事件资格由服务端固定分类器决定,不允许 webconsole 手工配置。
|
||
|
||
奖项必须恰好 7 个且全部 `enabled=true`,`sortOrder` 为不重复的 1~7,`prizeName`/`prizeImage` 非空,`weight > 0`,`stock >= -1`,并配置 `resourceGroupId`。`stock=-1` 表示无限库存,0 表示耗尽。
|
||
|
||
排名奖励必须恰好为 `1-1`、`2-2`、`3-3`、`4-7`、`8-10`、`11-30` 六档。奖项和排名奖励引用的资源组必须已上架、`sysOrigin` 与活动一致且至少包含一个奖励配置。
|
||
|
||
### 补数返回
|
||
|
||
`POST /backfill/{id}` 不接收 `eventType` 或 `receiptType`。`limit` 默认 200、最大 500;调用方只需把本次 `nextLastId` 作为下次 `lastId` 继续请求,直到返回 `finished=true`。钱包侧会强制 `EXPENDITURE`,并以四个转义后的左锚定游戏前缀查询,避免扫描活动时间窗内的全部非游戏支出。
|
||
|
||
`BackfillResult` 字段:`eventType`(恒为 `GAME_EXPENDITURE`)、`receiptType`(恒为 `1/EXPENDITURE`)、`scanned`、`accepted`、`nextLastId`、`finished`。补数查询边界同实时统计,为 `[startTime,endTime)`;调用钱包历史接口时实现为 `endTime - 1ms`。
|
||
|
||
## 钱包事件统计契约
|
||
|
||
- RocketMQ topic:逻辑 topic `RC_DEFAULT_APP_ORDINARY`,运行时由 `${rocketmq.topics.RC_DEFAULT_APP_ORDINARY}` 解析。
|
||
- tag:`wallet_sync_gold_v2`。
|
||
- 独立 consumer group:`ASLAN_GAME_KING_WALLET_SYNC_V1`。不得和钱包落库消费者共用 group,否则会竞争消费。
|
||
- 事件模型:`WalletReceiptSyncEvent`。使用字段 `assetRecordId`、`userId`、`sysOrigin`、`eventType`、`eventId`、`receiptType`、`bizType`、`amount`、`createTime`。
|
||
- 唯一有效分类条件为:`bizType=WalletBizType.GOLD.getType()`、`receiptType=EXPENDITURE(1)`,并且 `eventType` 以 `HOT_GAME_`、`HKYS_GAME_`、`BAISHUN_GAME_`、`YOMI_GAME_` 之一开头;此外金额必须非 0、`sysOrigin` 必须与活动相同、`createTime` 必须位于 `[startTime,endTime)`。
|
||
- 分类器使用严格 `startsWith`,且前缀后必须有具体游戏 ID/标识;裸 `HOT_GAME_` 等前缀不会计入。禁止仅判断“支出”、`contains("GAME")` 或 `GoldOriginMapping`,这些宽泛条件会误收礼物/奖励等流水或遗漏 YOMI。
|
||
- 有效流水只累计 TYCOON 与抽奖机会;`INCOME` 不进入该活动统计。
|
||
- 实时 MQ、MQ 重投和后台补数共用唯一幂等键 `(activity_id, asset_record_id)`。
|
||
- DAILY 按活动 `timeZone` 落日表;OVERALL 使用全活动累计。活动结算奖励固定按 TYCOON/OVERALL,而不是当前 H5 正在查看的榜单维度。
|
||
|
||
## 结算截点与发奖核账
|
||
|
||
- 默认在 `endTime + 30 分钟` 到达结算截点;后台可通过 `settlementDelayMinutes` 配置 0~1440 分钟。定时任务每 5 分钟扫描一次到期活动。
|
||
- 统计有效时间仍只到 `endTime`(不含);30 分钟只是给异步消息到达留出的缓冲,不代表 RocketMQ 已提供“无更早消息”的 watermark。要求严格完整时,应在截点前按 `nextLastId` 连续调用固定口径补数直至 `finished=true`,再执行结算。
|
||
- 首次结算会在活动行排他锁内冻结 TYCOON/OVERALL Top 30;已进入统计事务会先完成,冻结后到达的 MQ 或补数会被拒绝,排名快照不会再漂移。
|
||
- 每个奖励组拆成 `deliveryItems` 逐项发放。普通失败可标为 `FAILED` 并在运营核实后走 retry;外部适配器抛错,或 `PROCESSING` 超过 10 分钟租期时,结果不能判定是否已到账,因此标为 `UNKNOWN`。
|
||
- `UNKNOWN` 永远不会被定时任务或普通 retry 盲目补发。运营核对下游账目后调用 `/delivery-item/resolve/{itemId}`:`delivered=true` 表示确认已到账,仅把该项记为成功且不再次发送;`delivered=false` 表示确认未到账,原子解锁父记录与该项后再补发。
|
||
- 结算中存在 `FAILED` 或 `UNKNOWN` 时活动聚合状态为 `PARTIAL_FAILED`;全部成功后为 `COMPLETED`。
|
||
|
||
## 数据库迁移
|
||
|
||
- 测试环境:`.deploy/test-deploy/sql/20260717_aslan_game_king.sql`
|
||
- 生产环境:`.deploy/prod-deploy/sql/20260717_aslan_game_king.sql`
|
||
- 两份 SQL 内容应保持字节一致。迁移创建 9 张独立活动表,并登记 webconsole 菜单/资源。活动表 `event_types` 只为兼容旧服务保留,默认且只写 `GAME_EXPENDITURE`,真实流水的 `ledger.event_type` 仍保留用于审计。
|
||
- SQL 文件开头已记录性能评估:实时写路径使用唯一键/主键聚合,榜单直接读取维度索引,恢复与到期扫描均有状态时间索引;补数绑定 `sys_origin + EXPENDITURE` 并使用四个转义后的左锚定前缀,利用钱包 `(sys_origin,event_type,create_time)` 索引。迁移不回填钱包流水,也不扫描更新既有业务大表。
|
||
- 当前交付只新增了迁移文件,**尚未在测试或生产数据库执行**。执行前仍需按环境发布流程复核目标库、变更窗口和执行计划。
|