Files
henan-backend/docs/流水抽奖接口文档.md
2026-09-21 19:21:35 +08:00

448 lines
16 KiB
Markdown
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 流水抽奖接口文档(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`(搜索"流水抽奖") |