# 流水抽奖接口文档(Cocos 客户端对接) ## 一、功能说明 玩家**无需额外付费**,仅凭当日已发生的游戏下注流水自动累计抽奖资格。 核心规则: | 规则 | 说明 | |------|------| | 流水来源 | 仅统计游戏消费流水,**排除** Roll 房、商城消费、福利类 | | 兑换比例 | 每个宝箱可单独配置流水门槛(默认 3000 流水 = 1 次) | | 过期机制 | 按自然日统计,次日零点自动清零,不累积 | | 抽奖消耗 | **只消耗"今日流水额度",不扣余额、不扣积分** | | 中奖发放 | 饰品直接进背包(`tt_box_records.source = 14`) | | 余额变动 | 只有玩家在背包**分解**该饰品时才产生余额/积分变动(`source = 63` 流水抽奖奖励) | | 同时开启 | 全站同一时间只有一个抽奖活动为开启状态 | 参与统计的流水来源码(`TtAccountRecordSource.getTurnoverLotteryCodes()`): | source | 含义 | |--------|------| | 2 | 经典开箱消费 | | 4 | 对战消费 | | 40 | 幸运升级消费 | | 38 | 疯狂转盘消费 | | 60 | 奥林匹斯消费 | --- ## 二、通用约定 - **Base URL**:`{host}/api/turnoverLottery` - **请求方式**:全部为 `POST` - **Content-Type**:`application/json` - **鉴权**:需登录态,请求头携带 token。`userId` 由后端从 token 解析,**客户端不要传 userId** ### 统一响应结构 ```json { "code": 200, "msg": "操作成功", "data": {} } ``` | code | 含义 | |------|------| | 200 | 成功 | | 500 | 业务失败,提示语见 `msg` | | 701 | 未实名认证,客户端统一弹实名认证窗口 | > 客户端判定成功请以 `code === 200` 为准;失败时直接向玩家展示 `msg`。 --- ## 三、接口清单 | # | 接口 | 路径 | 用途 | |---|------|------|------| | 1 | 获取抽奖信息 | `POST /api/turnoverLottery/myStats` | 进入抽奖页拉取流水、次数、宝箱与奖品 | | 2 | 执行抽奖 | `POST /api/turnoverLottery/draw` | 点击抽奖,返回中奖结果供播放动画 | | 3 | 我的抽奖记录 | `POST /api/turnoverLottery/myRecords` | 展示玩家历史中奖记录 | | 4 | 全服中奖记录 | `POST /api/turnoverLottery/globalRecords` | 展示全服玩家中奖公示(跑马灯/滚动列表) | --- ## 四、接口详情 ### 1. 获取抽奖信息 **`POST /api/turnoverLottery/myStats`** 进入抽奖页时调用,一次性返回玩家流水统计 + 所有开启中的抽奖宝箱及其奖品。 #### 请求参数 无需参数,body 可不传(或传空对象 `{}`)。 #### 响应示例 ```json { "code": 200, "msg": "操作成功", "data": { "totalTurnover": 12000.00, "usedTurnover": 3000.00, "availableTurnover": 9000.00, "lotteryList": [ { "id": 1, "lotteryName": "每日流水抽奖", "status": 1, "turnoverThreshold": 3000, "sort": 0, "lotteryChances": 3, "totalValue": 8250.00, "prizes": [ { "id": 6, "ornamentName": "M9 刺刀 | 渐变之色", "price": 8200.00, "imageUrl": "/profile/upload/2026/08/08/xxx.png", "ornamentLevel": 3, "exteriorName": "略有磨损", "levelImg": "/profile/upload/level/red.png", "displayCount": 10, "displayPercent": "50.00%", "realCount": 5, "realPercent": "45.45%", "sort": 0 } ] } ] } } ``` #### 字段说明 **顶层字段** | 字段 | 类型 | 说明 | |------|------|------| | `totalTurnover` | Decimal | 今日累计有效流水总额 | | `usedTurnover` | Decimal | 今日已消耗的流水(抽奖已用掉的部分) | | `availableTurnover` | Decimal | **当前可用流水** = `totalTurnover - usedTurnover`,不会为负 | | `lotteryList` | Array | 开启中的抽奖宝箱列表,按 `sort` 升序 | **`lotteryList[]` 宝箱字段** | 字段 | 类型 | 说明 | |------|------|------| | `id` | Long | 宝箱 ID,抽奖时作为 `lotteryId` 传入 | | `lotteryName` | String | 宝箱名称 | | `status` | Int | 状态,始终为 1(接口只返回开启中的) | | `turnoverThreshold` | Int | 该宝箱的流水门槛(多少流水抽 1 次) | | `sort` | Int | 展示排序,越小越靠前 | | `lotteryChances` | Int | **该宝箱可抽次数** = `availableTurnover ÷ turnoverThreshold` 向下取整 | | `totalValue` | Decimal | 奖池总价值(按初始真实数量 × 单价累加) | | `prizes` | Array | 奖品列表,按 `sort` 升序 | **`prizes[]` 奖品字段** | 字段 | 类型 | 说明 | |------|------|------| | `id` | Long | 奖品 ID | | `ornamentName` | String | 饰品名称 | | `price` | Decimal | 饰品价值 | | `imageUrl` | String | 饰品图片 | | `ornamentLevel` | Int | 品质等级 ID | | `exteriorName` | String | 磨损度(崭新出厂/略有磨损/久经沙场/破损不堪/战痕累累/无涂装),可能为 `null` | | `levelImg` | String | 品质底图,可用于饰品卡片背景 | | `displayCount` | Int | 展示数量 | | `displayPercent` | String | **展示爆率**,如 `"50.00%"`,用于前端展示给玩家 | | `realCount` | Int | 当前剩余库存 | | `realPercent` | String | 真实爆率(内部数据,**不建议展示给玩家**) | | `sort` | Int | 展示排序 | > **图片路径处理**:`imageUrl` / `levelImg` 若以 `/` 开头(且非 `//`),是服务器相对路径,需拼接资源域名前缀;否则为完整 URL 直接使用。 > **展示建议**:转盘/宝箱格子的爆率请使用 `displayPercent`,`realPercent` 是后台真实概率,不要展示。 --- ### 2. 执行抽奖 **`POST /api/turnoverLottery/draw`** 玩家点击抽奖时调用。**建议点击立即请求接口,拿到结果后再播放动画**——饰品在接口返回成功时已经入背包,动画只是表现层,即使动画中途活动被关闭也不影响已发的奖。 #### 请求参数 | 参数 | 类型 | 必填 | 说明 | |------|------|------|------| | `lotteryId` | Long | 是 | 宝箱 ID,取 `myStats` 返回的 `lotteryList[].id` | ```json { "lotteryId": 1 } ``` #### 成功响应 ```json { "code": 200, "msg": "操作成功", "data": { "prizeId": 6, "ornamentName": "M9 刺刀 | 渐变之色", "price": 8200.00, "imageUrl": "/profile/upload/2026/08/08/xxx.png", "ornamentLevel": 3, "exteriorName": "略有磨损", "lotteryName": "每日流水抽奖", "costTurnover": 3000, "msg": "抽奖成功!获得: M9 刺刀 | 渐变之色" } } ``` | 字段 | 类型 | 说明 | |------|------|------| | `prizeId` | Long | 中奖奖品 ID,可与 `myStats` 的 `prizes[].id` 对应,用于定位动画停留格子 | | `ornamentName` | String | 中奖饰品名称 | | `price` | Decimal | 饰品价值 | | `imageUrl` | String | 饰品图片 | | `ornamentLevel` | Int | 品质等级 ID | | `exteriorName` | String | 磨损度,可能为 `null` | | `lotteryName` | String | 宝箱名称 | | `costTurnover` | Int | 本次消耗的流水值 | | `msg` | String | 中奖文案,可直接展示 | #### 失败响应(`code = 500`,提示语在外层 `msg`) | msg | 触发场景 | 客户端处理建议 | |-----|----------|----------------| | `抽奖活动不存在或未开启` | 活动被后台关闭或已删除 | 提示后刷新 `myStats` | | `抽奖次数不足` | 可用流水不够一次门槛 | 提示玩家去游戏累计流水 | | `奖品配置为空` | 该宝箱未配置奖品 | 提示"活动准备中" | | `奖品库存不足` | 全部奖品初始数量为 0 | 提示"活动准备中" | | `抽奖流水门槛配置错误,请联系管理员` | 门槛配置为 0 或负数 | 提示联系客服 | | `抽奖处理中,请勿重复操作` | 同一用户并发点击被锁拦截 | **禁用按钮防连点**,稍后可重试 | | `抽奖处理中,请稍后再试` | 同一宝箱有其他玩家正在抽 | 短暂延迟后自动重试一次 | | `系统繁忙,请稍后重试` | 服务端异常(事务已回滚,未扣流水未发奖) | 提示重试 | > **重要**:抽奖接口已加**双层 Redis 分布式锁**(用户级 + 宝箱级),客户端仍需在按钮上做防连点,避免频繁触发"抽奖处理中"。 #### 抽奖成功后的处理 1. 用 `prizeId` 定位动画停留位置并播放中奖动画 2. 动画结束后重新调用 `myStats` 刷新可用流水与剩余次数 3. 饰品已在背包,如需"立即分解"走通用背包分解接口 `/api/userPackSack/decompose`,分解产生的余额/积分变动来源为 `63 流水抽奖奖励` --- ### 3. 我的抽奖记录 **`POST /api/turnoverLottery/myRecords`** 分页查询当前登录玩家的历史中奖记录,按抽奖时间倒序。 #### 请求参数 | 参数 | 类型 | 必填 | 默认 | 说明 | |------|------|------|------|------| | `lotteryId` | Long | 否 | 全部 | 按宝箱筛选,不传查全部 | | `pageNum` | Int | 否 | 1 | 页码,从 1 开始 | | `pageSize` | Int | 否 | 10 | 每页条数,**最大 100** | ```json { "pageNum": 1, "pageSize": 20 } ``` #### 响应示例 ```json { "code": 200, "msg": "操作成功", "data": { "total": 35, "pageNum": 1, "pageSize": 20, "rows": [ { "id": 128, "userId": 688660, "lotteryId": 1, "prizeId": 6, "ornamentId": null, "ornamentName": "M9 刺刀 | 渐变之色", "imageUrl": "/profile/upload/2026/08/08/xxx.png", "ornamentsPrice": 8200.00, "costTurnover": 3000, "createTime": "2026-08-08 17:18:37" } ] } } ``` | 字段 | 类型 | 说明 | |------|------|------| | `total` | Long | 总记录数,用于分页组件 | | `pageNum` | Int | 当前页码 | | `pageSize` | Int | 每页条数 | | `rows` | Array | 记录列表 | | `rows[].id` | Long | 记录 ID | | `rows[].lotteryId` | Long | 宝箱 ID | | `rows[].prizeId` | Long | 奖品 ID | | `rows[].ornamentName` | String | 饰品名称 | | `rows[].imageUrl` | String | 饰品图片 | | `rows[].ornamentsPrice` | Decimal | 奖品价值 | | `rows[].costTurnover` | Int | 该次消耗的流水值 | | `rows[].createTime` | String | 抽奖时间 `yyyy-MM-dd HH:mm:ss` | 失败时返回 `code = 500`,`msg` 为 `查询失败,请稍后重试`。 --- ### 4. 全服中奖记录 **`POST /api/turnoverLottery/globalRecords`** 分页查询**全服所有玩家**的中奖记录,按中奖时间倒序。用于抽奖页的中奖公示区(跑马灯、滚动榜单)。 > **已在服务端过滤**:机器人(`user_type='03'`)、白号(`is_blank_account=1`)、封禁用户(`status='1'`)、已删除用户(`del_flag!='0'`)的记录不会出现在结果里,客户端无需再做过滤。 #### 请求参数 | 参数 | 类型 | 必填 | 默认 | 说明 | |------|------|------|------|------| | `lotteryId` | Long | 否 | 全部 | 按宝箱筛选,不传查全部宝箱 | | `minPrice` | Decimal | 否 | 不限 | 最低奖品价值门槛,只展示价值 ≥ 该值的记录(做"大奖公示"时用) | | `pageNum` | Int | 否 | 1 | 页码,从 1 开始 | | `pageSize` | Int | 否 | 20 | 每页条数,**最大 100** | ```json { "minPrice": 1000, "pageNum": 1, "pageSize": 20 } ``` #### 响应示例 ```json { "code": 200, "msg": "操作成功", "data": { "total": 1286, "pageNum": 1, "pageSize": 20, "rows": [ { "id": 128, "userId": 688660, "nickName": "云深不知", "avatar": "/profile/avatar/2026/08/08/aaa.png", "lotteryId": 1, "lotteryName": "每日流水抽奖", "prizeId": 6, "ornamentName": "M9 刺刀 | 渐变之色", "imageUrl": "/profile/upload/2026/08/08/xxx.png", "ornamentsPrice": 8200.00, "ornamentLevel": 3, "levelImg": "/profile/upload/level/red.png", "exteriorName": "略有磨损", "createTime": "2026-08-08 17:18:37" } ] } } ``` #### 字段说明 | 字段 | 类型 | 说明 | |------|------|------| | `total` | Long | 总记录数 | | `pageNum` | Int | 当前页码 | | `pageSize` | Int | 每页条数 | | `rows` | Array | 记录列表,按中奖时间倒序 | | `rows[].id` | Long | 记录 ID | | `rows[].userId` | Int | 中奖用户 ID | | `rows[].nickName` | String | **中奖用户昵称**,用于公示展示 | | `rows[].avatar` | String | **中奖用户头像** | | `rows[].lotteryId` | Long | 宝箱 ID | | `rows[].lotteryName` | String | 宝箱名称 | | `rows[].prizeId` | Long | 奖品 ID | | `rows[].ornamentName` | String | 饰品名称 | | `rows[].imageUrl` | String | 饰品图片 | | `rows[].ornamentsPrice` | Decimal | 奖品价值 | | `rows[].ornamentLevel` | Int | 品质等级 ID | | `rows[].levelImg` | String | 品质底图,可作饰品卡片背景 | | `rows[].exteriorName` | String | 磨损度,可能为 `null` | | `rows[].createTime` | String | 中奖时间 `yyyy-MM-dd HH:mm:ss` | 失败时返回 `code = 500`,`msg` 为 `查询失败,请稍后重试`。 > **与 `myRecords` 的区别**:`myRecords` 只返回当前登录玩家自己的记录(不含昵称/头像/品质图);`globalRecords` 返回全服记录并附带中奖者昵称、头像、品质底图,专供公示展示。 > **轮询建议**:公示区如需实时滚动,建议 15~30 秒轮询一次 `pageNum=1`,不要高频请求。 --- ## 五、典型调用流程 ``` 进入抽奖页 │ ├─→ POST /myStats │ 拿到 availableTurnover、lotteryList │ 渲染宝箱、奖品格子(爆率用 displayPercent) │ 显示可抽次数 lotteryChances │ ├─→ 玩家点击抽奖(按钮置灰防连点) │ POST /draw { lotteryId } │ ├─ code=200 → 用 prizeId 定位并播放动画 │ │ 动画结束后重新 POST /myStats 刷新 │ └─ code=500 → 按钮恢复,展示 msg │ ├─→ 查看我的中奖记录 │ POST /myRecords { pageNum, pageSize } │ └─→ 中奖公示区(跑马灯,15~30s 轮询) POST /globalRecords { pageNum: 1, pageSize: 20 } ``` --- ## 六、对接注意事项 1. **不要传 userId**:所有接口的用户身份从 token 解析,传了也会被忽略。 2. **`myStats` 无需 body**:之前该接口曾要求传 `{}`,现已改为无参,body 传或不传都可以。 3. **次数按宝箱各算**:`lotteryChances` 在每个宝箱内单独计算(因为门槛可能不同),不是全局共享的次数。多个宝箱的次数是"同一份可用流水"的不同换算结果,抽任一宝箱都会消耗同一份流水。 4. **抽奖不动余额**:客户端不要在抽奖后刷新余额,抽奖不产生金币/积分变化;只有分解饰品才会变。 5. **动画与发奖解耦**:接口返回成功即已发奖入背包,动画播放期间活动被关闭不影响结果。 6. **爆率字段区分**:给玩家看 `displayPercent`,`realPercent` / `realCount` 属于内部数据。 7. **奖池自动重置**:当宝箱所有奖品剩余库存耗尽时,下一次抽奖会自动按后台配置的初始真实数量重置奖池,客户端无需处理。 8. **全服记录已过滤脏数据**:`globalRecords` 服务端已排除机器人、白号、封禁及已删除用户,客户端直接展示即可。 9. **公示区别高频轮询**:`globalRecords` 建议 15~30 秒轮询一次,`pageSize` 上限 100。 --- ## 七、相关代码位置 | 内容 | 文件 | |------|------| | 用户端接口 | `skins-service/service-admin/src/main/java/com/ruoyi/admin/controller/TurnoverLotteryUserController.java` | | 业务实现 | `skins-service/service-admin/src/main/java/com/ruoyi/admin/service/impl/TurnoverLotteryServiceImpl.java` | | 全服记录查询 | `skins-service/service-admin/src/main/java/com/ruoyi/admin/mapper/TurnoverLotteryRecordMapper.java` | | 全服记录 VO | `skins-model/src/main/java/com/ruoyi/domain/vo/lottery/TurnoverLotteryWinVO.java` | | 奖品实体 | `skins-model/src/main/java/com/ruoyi/domain/entity/lottery/TtTurnoverLotteryPrize.java` | | 记录实体 | `skins-model/src/main/java/com/ruoyi/domain/entity/lottery/TtTurnoverLotteryRecord.java` | | 建表 SQL | `docs/updata_sql.md`(搜索"流水抽奖") |