# 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 `;网关从已校验 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 `。迁移 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)` 索引。迁移不回填钱包流水,也不扫描更新既有业务大表。 - 当前交付只新增了迁移文件,**尚未在测试或生产数据库执行**。执行前仍需按环境发布流程复核目标库、变更窗口和执行计划。