# Yumi 游戏王活动接口 本文对应 `chatapp3-golang` 的 Yumi Game King 实现。业务只接受 `LIKEI` 租户的权威游戏金币消耗事件,不提供事件白名单输入框,也不会统计通用钱包支出。 ## 1. 通用约定 - H5 外部网关地址:`/go/app/h5/game-king/yumi/*`;Go 服务内路由不带 `/go`。 - 后台外部网关地址:`/go/resident-activity/game-king/*`;Go 服务内路由不带 `/go`。 - H5 和后台均传 `Authorization`。H5 支持 `Bearer `;后台沿用 Console 完整 Authorization。 - 内部任务传 `X-Internal-Token: `。 - 时间字段均为毫秒时间戳;`timeZone` 使用 IANA 时区,默认 `Asia/Riyadh`。 - 所有业务 ID 在 JSON 响应中均为字符串,避免 JavaScript 丢失 Snowflake 精度。请求中的 ID 可传字符串或数字,建议统一传字符串。 - 成功响应的数据同时位于 `data` 和 `body`: ```json { "status": true, "errorCode": 0, "errorMsg": "success", "code": "success", "message": "success", "data": {}, "body": {}, "time": 1784300000000 } ``` - 错误时 HTTP 状态码与 `errorCode` 一致,稳定业务码位于 `code` / `errorCodeName`。 ## 2. H5 接口 ### 2.1 活动详情 `GET /go/app/h5/game-king/yumi/detail?activityId={activityId}` `activityId` 可不传;不传时按登录用户 `sysOrigin=LIKEI` 解析当前可见活动。返回活动、固定七个奖项、六个排名奖励档、服务器时间和活动状态。 关键返回字段: ```json { "activity": { "id": "190000000000000001", "activityCode": "YUMI_GAME_KING_S1", "sysOrigin": "LIKEI", "startTime": 1784300000000, "endTime": 1784900000000, "coinPerDraw": 1000, "eventTypes": ["GAME_CONSUME_GOLD"], "rankingType": "TYCOON", "rankingPeriod": "OVERALL", "settlementStatus": "NOT_STARTED" }, "prizes": [], "rankRewards": [], "supportedRankingTypes": ["TYCOON"], "supportedRankingPeriods": ["DAILY", "OVERALL"], "activityStatus": "UPCOMING", "serverTime": 1784299900000 } ``` `activityStatus`:`UPCOMING`、`ACTIVE`、`ENDED`、`DISABLED`。 ### 2.2 我的进度 `GET /go/app/h5/game-king/yumi/me?activityId={activityId}` 返回: - `totalConsumed`:活动时间窗内累计游戏消耗金币。 - `earnedChances`:`floor(totalConsumed / coinPerDraw)`。 - `usedChances` / `availableChances`:已用和可用抽奖次数。 - `nextChanceRemaining`:距离下一次机会仍需消耗的金币。 - `rank`:总榜名次;无消费时为 `null`。 ### 2.3 抽奖 `POST /go/app/h5/game-king/yumi/draw` ```json { "activityId": "190000000000000001", "requestId": "9af73ee9-65ca-4cf5-b23c-f4c222c3ee6e" } ``` - `activityId` 和 `requestId` 均必填;`requestId` 最长 64 字符。客户端一次点击生成一个稳定值;超时重试必须复用原值。 - 幂等范围是 `activityId + userId + requestId`。已经提交的结果即使活动随后结束,也会原样返回,且不会再次扣次数或发奖。 - 权重仅表示相对概率;库存 `-1` 为无限,`0` 为耗尽,正数为剩余库存。有限库存使用条件更新防止超发。 - 返回 `record`、`prize`、`remainingChances`、`idempotentReplay`。 ### 2.4 排行榜 `GET /go/app/h5/game-king/yumi/ranking?activityId={activityId}&rankingType=TYCOON&period=OVERALL&limit=30` - `rankingType`:正式口径仅 `TYCOON`。旧 H5 请求 `VICTORIOUS` 时返回空榜,避免兼容页面报错。 - `period`:`DAILY` 或 `OVERALL`;配置页固定保存 `OVERALL`,H5 仍可切日榜。 - `limit` 默认 30,最大 100。 - 排序固定为:消费金币降序、达到当前消费值的事件时间升序、用户 ID 升序。 - `DAILY` 按活动时区自然日统计;活动结束结算只使用 `OVERALL` Top30。 - 用户资料通过 Java 批量接口补齐;资料服务异常时榜单仍返回 `userId` 和分数。 ### 2.5 我的抽奖记录 `GET /go/app/h5/game-king/yumi/draw-records?activityId={activityId}&limit=20` `limit` 最大 100。返回奖品快照及 `PENDING / PROCESSING / SUCCESS / FAILED / UNKNOWN` 发放状态。 ## 3. 后台管理接口 所有接口要求菜单别名 `ResidentGameKingActivity`。写接口还要求对应按钮权限: | 权限别名 | 接口用途 | | --- | --- | | `resident-activity:yumi-game-king:edit` | 保存活动、奖项、排名奖励 | | `resident-activity:yumi-game-king:enable` | 启停活动 | | `resident-activity:yumi-game-king:settle` | 人工触发到期结算 | | `resident-activity:yumi-game-king:reconcile` | FAILED 重试、UNKNOWN 核账 | | `resident-activity:yumi-game-king:backfill` | 权威事件补数 | ### 3.1 活动配置 | 方法 | 地址 | 说明 | | --- | --- | --- | | GET | `/go/resident-activity/game-king/list` | 活动列表 | | GET | `/go/resident-activity/game-king/detail/{id}` | 完整详情 | | POST | `/go/resident-activity/game-king/save` | 新增或修改 | | PUT | `/go/resident-activity/game-king/enable/{id}?enabled=true` | 启停;布尔值只接受 `true/false/1/0` | | GET | `/go/resident-activity/game-king/prizes/{id}` | 七个奖项 | | PUT | `/go/resident-activity/game-king/prizes/{id}` | 保存七个奖项 | | GET | `/go/resident-activity/game-king/rank-rewards/{id}` | 六个排名奖励档 | | PUT | `/go/resident-activity/game-king/rank-rewards/{id}` | 保存六个排名奖励档 | `POST /save` 示例: ```json { "id": "0", "activityCode": "YUMI_GAME_KING_S1", "activityName": "Yumi Game King", "activityDesc": "", "sysOrigin": "LIKEI", "timeZone": "Asia/Riyadh", "startTime": 1784300000000, "endTime": 1784900000000, "settlementDelayMinutes": 30, "coinPerDraw": 1000, "rankingType": "TYCOON", "rankingPeriod": "OVERALL", "enabled": false, "prizes": [ { "id": "0", "prizeName": "Prize 1", "prizeImage": "https://example.com/prize-1.png", "weight": 100, "stock": -1, "resourceGroupId": "190000000000000101", "enabled": true, "sortOrder": 1 } ], "rankRewards": [ { "id": "0", "startRank": 1, "endRank": 1, "resourceGroupId": "190000000000000201", "rewardName": "Top 1" } ] } ``` 示例数组为节省篇幅只展示一项,真实启用配置必须满足: - 恰好 7 个奖项,全部启用,`sortOrder` 完整覆盖 1..7 且不重复;权重必须大于 0。 - 恰好 6 个排名档:`1`、`2`、`3`、`4-7`、`8-10`、`11-30`。 - `sysOrigin` 固定 `LIKEI`;`rankingType` 固定 `TYCOON`;配置 `rankingPeriod` 固定 `OVERALL`。 - 首次启用时 `startTime` 必须严格晚于当前时间;同租户启用活动时间窗不能重叠。 - 计数窗口严格为 `[startTime, endTime)`。活动开始后锁定时间、口径、奖项、排名奖励和启停状态,只允许修改名称、说明等展示信息。 - `settlementDelayMinutes` 默认 30,可配置 0..1440。 - 每次保存/启用及每次真实发奖前都会重新校验奖励资源组:必须存在、已上架、明确属于 `LIKEI` 且非空,并比对冻结的完整奖励项快照。 单独保存奖项请求: ```json { "activityId": "190000000000000001", "prizes": [] } ``` 单独保存排名奖励请求: ```json { "activityId": "190000000000000001", "rankRewards": [] } ``` 路径 ID 与 body `activityId` 必须一致。 ### 3.2 抽奖发放管理 | 方法 | 地址 | 说明 | | --- | --- | --- | | GET | `/go/resident-activity/game-king/draw-records/{id}?deliveryStatus=&limit=100` | 查看抽奖和发放明细 | | POST | `/go/resident-activity/game-king/draw/retry/{recordId}` | 只重试 `FAILED` | | POST | `/go/resident-activity/game-king/delivery-item/resolve/{itemId}?delivered=true` | 人工核销 `UNKNOWN` | `delivered` 只接受 `true/false/1/0`: - `true`:已通过 `trackId/businessNo` 确认下游到账,只记为成功,不再发送。 - `false`:已确认下游未到账,先转为 `FAILED`,再执行一次显式重试。 - 非法或缺失值返回 400,绝不默认为 `false`。 ### 3.3 排名结算 | 方法 | 地址 | 说明 | | --- | --- | --- | | GET | `/go/resident-activity/game-king/settlement-records/{id}` | 查看冻结名次与发放状态 | | POST | `/go/resident-activity/game-king/settlement/{id}` | 到结算时间后人工冻结并派发一小批 | | POST | `/go/resident-activity/game-king/settlement/retry/{recordId}` | 只重试 `FAILED` | 结算只冻结一次 `OVERALL` Top30。记录和发奖任务在同一事务生成;人工接口每次最多派发该活动 4 个 `PENDING`,其余由内部定时任务继续。单项硬超时 20 秒,因此最坏外部调用约 80 秒,并为数据库处理留出余量。 ### 3.4 事件补数 `POST /go/resident-activity/game-king/backfill/{id}?lastId=0&limit=200` - 只读取 `task_center_event_archive_outbox` 的 `LIKEI + GAME_CONSUME_GOLD + 活动时间窗` 数据。 - 在任何归档扫描前都会检查迁移 058 的四列在线索引;索引未完成、不可见或列序不符时返回 `503 backfill_index_required`,不会退化为全表扫描。 - 仅允许对已启用且结算尚未冻结的活动补数;活动结束后的结算等待期仍可补,进入结算后返回 409,游标不会静默推进。 - 调用方只能传不透明游标 `lastId` 和 `limit`,不能传 eventType 或钱包口径;`limit` 最大 500。 - 返回 `eventType`、`scanned`、`accepted`、`nextLastId`、`finished`。未完成时用返回的 `nextLastId` 继续。 - 若结算恰好在一页补数中途冻结,接口返回 409 且不返回新的游标;调用方保留原 `lastId`,已成功写入的前序事件会由活动账本幂等跳过。 - 活动账本以 `activityId + eventId` 幂等,实时事件和补数重复不会重复累计。 ## 4. 游戏消耗事件接入 唯一统计事件是任务中心的 `GAME_CONSUME_GOLD`。事件经过任务中心统一校验后、在任务中心自身业务去重之前同步扇出给 Game King;Game King 使用独立账本保证幂等。 已接入事件 ID 口径: - Java 游戏:`GAME_CONSUME::`,已覆盖 `YOMI`、`LINGXIAN`、`HOTGAME`、`BAISHUN_LEGACY`、`LUDO`、`TURNTABLE`。 - Go GameOpen:`GAME_OPEN_CONSUME:`。 - Go HotGame:`GAME_HOTGAME_CONSUME:`。 - Go Baishun:`BAISHUN_GAME_CONSUME::`。 RocketMQ 默认配置: - topic:`RC_DEFAULT_APP_ORDINARY` - tag:`task_center_event` - consumer group:`task-center-progress` - 开关:`CHATAPP_TASK_CENTER_MQ_ENABLED`(兼容 `TASK_CENTER_EVENT_MQ_ENABLED`) 活动累计使用事件的 `occurredAt` 判断 `[startTime,endTime)`,因此活动开始前的历史游戏消耗不会计入。只接受正数 `deltaValue`;退款、胜利收入和非游戏钱包消耗不计入。 ## 5. 发奖和异常状态 发奖以“奖励资源组”为一个下游调用单元,使用 Java `SendActivityReward`,`origin=GAME_KING_AWARD`,`trackId=deliveryItem.id`。后台明细同时返回稳定 `trackId`、`businessNo`、用户 ID、资源组 ID 和冻结奖励项。 - `PENDING`:本地事务已提交,尚未领取。 - `PROCESSING`:已领取,正在校验/调用下游。 - `SUCCESS`:Java 明确成功。 - `FAILED`:在发奖请求发出前就已明确失败,例如资源组不存在、下架、跨租户或与冻结快照不一致;只允许运营显式重试。 - `UNKNOWN`:发奖请求已尝试但结果不能证明,或 `PROCESSING` 超过 10 分钟;禁止自动重试,必须先核账再 resolve。 一旦活动启用,奖励组内的实际奖励项内容必须保持与冻结快照一致。若运营修改了组内容,发放会在调用 Java 前进入 `FAILED`;应先恢复原快照或完成明确处置,再重试。 ## 6. 内部结算任务 `POST /internal/game-king/settle-due` Header:`X-Internal-Token`。 本 Go 服务不启动 ticker。由 `chatapp-cron` 周期调用: - 每次最多修复 20 个过期 `PROCESSING`,并扫描、冻结 10 个到期活动。 - 每次最多派发全局 4 个 `PENDING`;单项硬超时 20 秒,调用方超时应配置为至少 120 秒。 - 只自动恢复 `PENDING`。`FAILED` / `UNKNOWN` 永不自动重试;超过 10 分钟的 `PROCESSING` 只转为 `UNKNOWN`。 响应: ```json { "scanned": 2, "settled": 2, "failedIds": [] } ``` ## 7. 数据库迁移与性能 迁移文件:`migrations/058_yumi_game_king.sql`。 - 新建活动、七奖项、六档奖励、幂等消费账本、总榜/日榜聚合、抽奖、结算和发放表。 - 排行读取物化聚合并使用复合排序索引,不对消费流水做运行时 `GROUP BY`。 - 补数给 `task_center_event_archive_outbox` 增加 `(sys_origin,event_type,occurred_at,id)` 复合索引,以范围 + keyset 扫描,避免全表排序。 - 迁移不执行历史 `UPDATE/DELETE/backfill`。新增复合索引仍属于线上 DDL,执行前应按目标 MySQL 版本、表数据量和 DDL 策略确认锁影响,并在低峰发布。