first commit
This commit is contained in:
@@ -0,0 +1,201 @@
|
||||
# Skins 业务后端 — 详细部署文档
|
||||
|
||||
> 适用项目:chengdu-backend(基于若依 RuoYi 开发,Spring Boot 4.0.3 + JDK 21)
|
||||
> 适用场景:本地开发、测试环境、生产环境
|
||||
> 项目根 README 见 [README.md](README.md),本文为补充的详细部署手册
|
||||
|
||||
## 1. 部署架构总览
|
||||
|
||||
- 后端单进程:ruoyi-admin.jar,监听28081
|
||||
- 基础设施:MySQL 8.x、Redis、RabbitMQ(参考 docker/docker-compose.yml)
|
||||
- 前端:ruoyi-ui(Vue2 经典管理端),promo-ui2(Vue3 主播后台),cocos前端
|
||||
|
||||
|
||||
## 2. 环境准备
|
||||
|
||||
### 2.1 必备软件
|
||||
|
||||
JDK 21(项目使用 /root/jdk-21.0.10) 必须为 JDK 21
|
||||
Maven 3.6.x(/root/apache-maven-3.6.3)
|
||||
|
||||
### 2.2 安装示例
|
||||
|
||||
# JDK 21
|
||||
cd /root
|
||||
# 上传 jdk-21.x.tar.gz 到 /root
|
||||
tar -xzf jdk-21.0.10_linux-x64_bin.tar.gz
|
||||
mv jdk-21.0.10 /root/jdk-21.0.10
|
||||
|
||||
# Maven
|
||||
cd /root
|
||||
tar -xzf apache-maven-3.6.3-bin.tar.gz
|
||||
|
||||
# 配置全局环境变量(在脚本内配置,见 ry.sh / package.sh)
|
||||
cat >> /etc/profile <<'EOF'
|
||||
export JAVA_HOME=/root/jdk-21.0.10
|
||||
export MAVEN_HOME=/root/apache-maven-3.6.3
|
||||
export PATH=$JAVA_HOME/bin:$MAVEN_HOME/bin:$PATH
|
||||
EOF
|
||||
source /etc/profile
|
||||
|
||||
# 验证
|
||||
java -version
|
||||
mvn -v
|
||||
|
||||
|
||||
## 3. 基础设施部署(MySQL / Redis / RabbitMQ)
|
||||
|
||||
### 3.1 使用仓库自带 docker-compose
|
||||
|
||||
仓库 docker/ 目录下已提供文件,采用 compose override 模式,通过 deploy.sh 自动选择镜像源和资源规格。
|
||||
|
||||
bash
|
||||
cd chengdu-backend/docker/compose
|
||||
|
||||
# 自动检测环境并启动(默认:国内镜像 + 自动检测 CPU 规格)
|
||||
./deploy.sh up -d
|
||||
|
||||
# 开发环境
|
||||
ENV=dev ./deploy.sh up -d
|
||||
|
||||
# 海外镜像源(如国内镜像不可用)
|
||||
PROXY=overseas ./deploy.sh up -d
|
||||
|
||||
# 停止 / 删除
|
||||
./deploy.sh stop
|
||||
./deploy.sh down
|
||||
|
||||
容器常用操作:
|
||||
|
||||
./deploy.sh up -d # 启动
|
||||
./deploy.sh stop # 停止
|
||||
|
||||
### 3.2 首次部署:数据卷迁移
|
||||
|
||||
如果之前使用旧版 docker-compose(自动命名卷),需要迁移数据到 external 卷:
|
||||
|
||||
bash
|
||||
cd docker/compose
|
||||
bash migrate-mysql-volume.sh
|
||||
|
||||
|
||||
## 4. 后端配置yml说明(Spring Profile)
|
||||
|
||||
入口配置文件:[ruoyi-admin/src/main/resources/application.yml](ruoyi-admin/src/main/resources/application.yml)
|
||||
|
||||
### 4.1 Profile 文件
|
||||
|
||||
| Profile | 文件
|
||||
| dev | application-dev.yml
|
||||
| pro | application-pro.yml
|
||||
|
||||
### 4.2 生产关键配置(节选自 application-pro.yml)
|
||||
|
||||
yaml
|
||||
ruoyi:
|
||||
profile: /www/static # 上传文件存储目录
|
||||
demoEnabled: true
|
||||
|
||||
spring:
|
||||
datasource:
|
||||
druid:
|
||||
master:
|
||||
url: jdbc:mysql://127.0.0.1:23306/chengdu?useUnicode=true&characterEncoding=utf8&zeroDateTimeBehavior=convertToNull&useSSL=true&serverTimezone=GMT%2B8
|
||||
username: chengdu
|
||||
password: <数据库密码>
|
||||
data:
|
||||
redis:
|
||||
host: 127.0.0.1
|
||||
port: 26379
|
||||
password: <Redis 密码>
|
||||
rabbitmq:
|
||||
host: 127.0.0.1
|
||||
port: 25672
|
||||
username: skins
|
||||
password: <RabbitMQ 密码>
|
||||
virtual-host: /skins
|
||||
|
||||
token:
|
||||
secret: <强随机 32 位以上字符串> # JWT 密钥,必须修改
|
||||
expireTime: 1440 # 分钟
|
||||
|
||||
### 4.3 项目文件上传目录
|
||||
|
||||
ruoyi.profile 指上传文件物理路径(默认 /www/static),创建并赋权(宝塔直接手动创建):
|
||||
|
||||
|
||||
## 5. 编译与打包
|
||||
### 5.1 服务器编译(脚本封装)
|
||||
|
||||
仓库根目录提供 [package.sh](package.sh):
|
||||
|
||||
package.sh
|
||||
主要工作:
|
||||
1. 检查 mvn 和 java 命令是否可用。
|
||||
2. git pull 拉取最新代码。
|
||||
3. 调用 mvn clean package -Dmaven.test.skip=true。
|
||||
4. 将 ruoyi-admin/target/ruoyi-admin.jar 复制到仓库根目录 ./ruoyi-admin.jar,供 ry.sh 启动。
|
||||
|
||||
|
||||
## 6. 一键部署脚本(宝塔推荐)
|
||||
|
||||
仓库根 [publish.sh](publish.sh) 封装「备份旧jar → 打包 → 重启」完整流程:
|
||||
|
||||
bash执行流程
|
||||
cd /www/chengdu_app/chengdu-backend
|
||||
./publish.sh
|
||||
|
||||
执行解释步骤:
|
||||
1. 备份旧 jar:cp ruoyi-admin.jar ruoyi-admin.jar.bak
|
||||
2. 打包:调用 ./package.sh(含 git pull + mvn package)
|
||||
3. 重启:./ry.sh stop && ./ry.sh start
|
||||
|
||||
ry.sh 会自动检测 CPU 核数选择 JVM 规格(16c32g: 10g堆 / 32c64g: 22g堆),使用 G1GC 并开启 GC 日志。可通过 JVM_PROFILE 环境变量覆盖规格,SPRING_PROFILE 覆盖 Spring Profile(默认 pro)。
|
||||
|
||||
|
||||
## 7. 前端工程部署
|
||||
|
||||
### 7.1 ruoyi-ui(Vue2 经典管理端)
|
||||
|
||||
bash
|
||||
cd ruoyi-ui
|
||||
npm install
|
||||
npm run build:prod # 将dist目录下文件打包
|
||||
# 将 dist压缩包 上传到/www/wwwroot/静态目录
|
||||
|
||||
|
||||
### 7.2 promo-ui2(Vue3 推广后台)
|
||||
|
||||
bash
|
||||
cd promo-ui2
|
||||
pnpm install
|
||||
pnpm build:pro # 将dist目录下文件打包
|
||||
# 将 dist压缩包 上传到/www/wwwroot/静态目录
|
||||
|
||||
|
||||
## 8. 宝塔端口放行
|
||||
|
||||
| 端口 | 用途
|
||||
| 80 / 443 | Nginx
|
||||
| 28081 | 后端 Spring Boot
|
||||
| 23306 | MySQL
|
||||
| 26379 | Redis
|
||||
| 25672 / 35672 | RabbitMQ AMQP
|
||||
|
||||
执行流程
|
||||
宝塔 → 安全 → 放行 80/443/26379/23306/28081 等等所需要的端口号,其余端口可在宝塔安全设置添加和修改。
|
||||
|
||||
## 9. 日志与监控
|
||||
|
||||
### 9.1 日志文件
|
||||
|
||||
| 路径 | 内容 |
|
||||
| nohup.out | 启动 + 控制台日志(由 ry.sh 输出) |
|
||||
| /www/ruoyi/logs/sys-info.log | 系统日志(logback,按天滚动,保留60天) |
|
||||
| /www/ruoyi/logs/sys-error.log | 错误日志(logback,按天滚动,保留60天) |
|
||||
| /www/ruoyi/logs/gc.log | GC 日志(滚动 10 个文件,每个 50M) |
|
||||
|
||||
实时跟踪:
|
||||
bash脚本
|
||||
tail -f nohup.out
|
||||
|
||||
+181
@@ -0,0 +1,181 @@
|
||||
# Skins 业务后端
|
||||
|
||||
|
||||
基于 **若依(RuoYi)4.8.2** 扩展的多模块 Java 后端,集成 CSGO/饰品相关业务(用户、玩法、第三方支付与回调、定时任务、推广后台等)。仓库同时包含两套管理端前端工程。
|
||||
|
||||
克隆后请在项目根目录执行 `mvn -v` 确认 JDK 21 与 Maven 可用,再按下文完成数据库与中间件初始化。
|
||||
|
||||
---
|
||||
|
||||
## 技术栈概览
|
||||
|
||||
| 层级 | 技术 |
|
||||
|------|------|
|
||||
| 运行时 | Java **21** |
|
||||
| 框架 | Spring Boot **4.0.3**(启用虚拟线程 `spring.threads.virtual.enabled`) |
|
||||
| Web 与安全 | **Spring Security 6**(无状态会话)、JWT(`Authorization` 请求头)、若依式菜单/权限模型 |
|
||||
| 持久层 | MyBatis / MyBatis-Plus、PageHelper、Druid(含多数据源扩展) |
|
||||
| 数据库 | MySQL |
|
||||
| 缓存 | Redis(Spring Data Redis + Lettuce) |
|
||||
| 消息 | RabbitMQ(业务侧配置虚拟主机等,详见各 profile) |
|
||||
| 实时 | WebSocket(如 `/ws/**` 等路径,与安全放行配置保持一致) |
|
||||
| API 文档 | **Springfox 3.x**(主用,Swagger / OAS3 分组);另引入 **springdoc-openapi** Starter,可与 Springfox 并行存在 |
|
||||
|
||||
---
|
||||
|
||||
## 仓库结构
|
||||
|
||||
```
|
||||
chengdu-backend/
|
||||
├── ruoyi-admin/ # Web 入口(Spring Boot 启动模块)
|
||||
├── ruoyi-framework/ # 安全、过滤器、资源配置等封装
|
||||
├── ruoyi-system/ # 系统管理(用户、角色、菜单等)
|
||||
├── ruoyi-common/ # 通用工具、常量、MQ 配置等
|
||||
├── ruoyi-quartz/ # 定时任务(若依 Quartz 集成)
|
||||
├── ruoyi-generator/ # 代码生成
|
||||
├── skins-model/ # 业务领域模型(实体等)
|
||||
├── skins-service/ # 业务服务聚合 POM(子模块见下)
|
||||
│ ├── service-admin/ # 运营管理侧接口
|
||||
│ ├── service-user/ # 用户侧接口
|
||||
│ ├── service-playingmethod/ # 玩法相关
|
||||
│ ├── service-thirdparty/ # 第三方对接
|
||||
│ └── service-task/ # 任务与调度配合
|
||||
├── skins-promo/ # 推广后台 API(路径前缀多为 /promo)
|
||||
├── ruoyi-ui/ # 管理端 Vue2 + Element UI(若依经典前端)
|
||||
├── promo-ui2/ # 推广/运营后台 Vue3 + Element Plus + Vite
|
||||
└── docker/ # MySQL / Redis / RabbitMQ 编排
|
||||
```
|
||||
|
||||
### 启动类
|
||||
|
||||
后端入口:`ruoyi-admin` 模块中的 `com.ruoyi.RuoYiApplication`。使用 `@SpringBootApplication(exclude = { DataSourceAutoConfiguration.class })` 排除 Spring Boot 默认数据源自动配置(Spring Boot 4 下为 `org.springframework.boot.jdbc.autoconfigure.DataSourceAutoConfiguration`),由若依多数据源与 Druid 配置接管。
|
||||
|
||||
---
|
||||
|
||||
## 运行环境与配置
|
||||
|
||||
### 必备依赖
|
||||
|
||||
- JDK **21**
|
||||
- Maven **3.6+**
|
||||
- MySQL **8.x**(库名与账号见各 profile;本地可参考 `docker/docker-compose.yml`)
|
||||
- Redis
|
||||
- RabbitMQ(若启用了 MQ 监听或相关消费者则需可用)
|
||||
|
||||
根 `pom.xml` 已配置 **阿里云 Maven 公共仓库**,便于国内环境拉取依赖。
|
||||
|
||||
### Spring Profile
|
||||
|
||||
`ruoyi-admin/src/main/resources/application.yml` 中通过 `spring.profiles.active` 切换环境,配套文件:
|
||||
|
||||
- `application-dev.yml` — 本地/开发
|
||||
- `application-test.yml` — 测试
|
||||
- `application-pro.yml` — 生产
|
||||
|
||||
**重要:** 上述 profile 文件中常含数据库口令、第三方支付与短信密钥等敏感信息。**勿将真实密钥提交到公开仓库**;上线请改用环境变量、密钥管理系统或独立于仓库的配置挂载。
|
||||
|
||||
### 默认服务端口与上下文
|
||||
|
||||
- HTTP 端口:**28081**(见根配置 `ruoyi-admin/src/main/resources/application.yml` 中的 `server.port`;各环境可被 profile 或部署参数覆盖,默认绑定 `0.0.0.0`)
|
||||
- 上下文路径:`/`(与管理端、`VUE_APP_BASE_API` 及反向代理前缀需一致)
|
||||
|
||||
### 对外匿名接口(简述)
|
||||
|
||||
与安全配置对齐的常见分类包括:`/login`、`/register`、`/captchaImage`,以及业务侧的 `/api/**`、`/admin/**`、WebSocket、部分 Swagger 与 Druid 监控路径等。具体请以 `ruoyi-framework` 中 **SecurityFilterChain** 与白名单配置为准。
|
||||
|
||||
---
|
||||
|
||||
## 本地启动后端
|
||||
|
||||
**方式一:先打包再在启动模块运行**
|
||||
|
||||
在项目根目录执行:
|
||||
|
||||
```bash
|
||||
mvn clean package -DskipTests
|
||||
cd ruoyi-admin
|
||||
mvn spring-boot:run
|
||||
```
|
||||
|
||||
**方式二:根目录一键拉起(会解析 `ruoyi-admin` 所需模块依赖)**
|
||||
|
||||
```bash
|
||||
mvn -pl ruoyi-admin -am spring-boot:run -DskipTests
|
||||
```
|
||||
|
||||
也可在 IDE 中直接运行 `RuoYiApplication`。
|
||||
|
||||
首次运行前请:
|
||||
|
||||
1. 创建 MySQL 库并完成若依及业务所需的 SQL 初始化(以团队提供的脚本为准)。
|
||||
2. 按所选 profile 修改数据源、Redis、RabbitMQ 等连接信息。
|
||||
|
||||
---
|
||||
|
||||
## Docker 基础设施
|
||||
|
||||
目录:`docker/`
|
||||
|
||||
- `docker-compose.yml`:MySQL(含自定义镜像)、Redis、RabbitMQ(管理端口及初始化示例)。
|
||||
- `docker/docker-cn/`:面向大陆镜像拉取环境的编排变体。
|
||||
|
||||
常用操作见 **`docker/readme.md`**(如单独重建某个服务等)。
|
||||
|
||||
---
|
||||
|
||||
## 前端工程
|
||||
|
||||
### ruoyi-ui(Vue 2)
|
||||
|
||||
- 技术栈:Vue CLI、Element UI(`package.json` 声明 Node `>=8.9`,实际开发建议 Node **16 LTS** 或 **18 LTS**,并按需开启 OpenSSL 兼容)。
|
||||
- 脚本见 `ruoyi-ui/package.json`(Windows 脚本含 `set NODE_OPTIONS=--openssl-legacy-provider`;在 macOS/Linux 上需 `export NODE_OPTIONS=...` 或使用等价脚本)。
|
||||
- 开发与打包:`npm run dev`、`npm run build:prod` 等。
|
||||
- 系统工具内「Swagger」页当前 iframe 指向 **`/doc.html`**;若后端未部署对应聚合页,请改为与下文 **API 文档** 中实际地址一致(例如 `/swagger-ui.html`)。
|
||||
|
||||
### promo-ui2(Vue 3)
|
||||
|
||||
- 技术栈:Vue 3、Vite、TypeScript、Element Plus(基于 vue-element-plus-admin 演进);要求 **Node ≥ 18**、`pnpm ≥ 8.1`(以 `promo-ui2/package.json` 中 `engines` 为准)。
|
||||
- 推荐使用 **pnpm**:`pnpm install`、`pnpm dev`、`pnpm build:pro` 等。
|
||||
- 详细说明见 `promo-ui2/README.zh-CN.md`。
|
||||
|
||||
前端请求的接口基地址需指向本后端;若经 Nginx 网关,注意与团队约定的前缀(如 `/prod-api`)一致。
|
||||
|
||||
---
|
||||
|
||||
## 业务模块说明(简述)
|
||||
|
||||
| 模块 | 职责 |
|
||||
|------|------|
|
||||
| **skins-model** | 共享实体与领域常量 |
|
||||
| **service-user** | 用户相关业务 |
|
||||
| **service-admin** | 运营管理侧业务 |
|
||||
| **service-playingmethod** | 玩法相关逻辑 |
|
||||
| **service-thirdparty** | 第三方对接(支付、交易平台回调等,密钥在各 profile 中) |
|
||||
| **service-task** | 异步任务 / 与 Quartz 等协同 |
|
||||
| **skins-promo** | 推广后台:登录注册、流水与佣金、充值卡等(REST 前缀示例:`/promo`) |
|
||||
|
||||
---
|
||||
|
||||
## API 文档
|
||||
|
||||
- **Springfox(当前主要使用)**:启动后访问(端口与 `server.port` 一致,仓库默认 **28081**)
|
||||
`http://localhost:28081/swagger-ui.html`
|
||||
在页面右上角切换分组(如 `user`、`thirdParty`、`playingMethod`、`task`、`promo` 等;与 `SwaggerConfig` 中 Docket 一致)。OpenAPI JSON 路径形如 `/<group>/api-docs`(Spring Security 已对 `/*/api-docs` 等放行,以实际配置为准)。
|
||||
- **springdoc-openapi**:`ruoyi-admin` 已引入依赖;若单独启用 SpringDoc UI,需确保 **Security** 对 `/swagger-ui/**`、`/v3/api-docs` 等路径放行,避免 401。
|
||||
|
||||
---
|
||||
|
||||
## 版本信息
|
||||
|
||||
- 父 POM:`com.ruoyi:ruoyi:4.8.2`(`spring-boot.version` **4.0.3**,`springdoc.version` **3.0.2**,`swagger` / Springfox **3.0.0**)
|
||||
- 若依官网参考:<http://www.ruoyi.vip>
|
||||
|
||||
---
|
||||
|
||||
## 安全建议
|
||||
|
||||
1. 轮换并私有化所有第三方(支付宝、聚合支付、短信等)密钥;生产环境勿使用仓库内示例明文。
|
||||
2. 修改 JWT `token.secret` 与 Druid 监控口令等非默认值。
|
||||
3. 按需收紧 Druid、`/actuator`(若启用)及 Swagger 相关路径的访问范围。
|
||||
|
||||
如有与本说明不一致之处,以当前仓库代码及团队部署文档为准。
|
||||
@@ -0,0 +1,160 @@
|
||||
-- ============================================================
|
||||
-- 常州项目 数据库清理脚本(TRUNCATE 各类业务记录表)
|
||||
-- 适用库:changzhou(127.0.0.1:23306,docker 容器 mysql_db;dev/pro 配置一致)
|
||||
-- 对照时间:2026-09-17,库内共 163 张表,本脚本覆盖全部业务表
|
||||
-- ⚠️ 高危操作!执行前务必完整备份数据库!
|
||||
-- ⚠️ TRUNCATE 不可回滚,且会重置自增主键
|
||||
-- ⚠️ 若某张表在你的环境中不存在,请注释掉对应行(TRUNCATE 无 IF EXISTS)
|
||||
-- ============================================================
|
||||
|
||||
-- 关闭外键检查,避免因约束顺序报错
|
||||
SET FOREIGN_KEY_CHECKS = 0;
|
||||
|
||||
-- ============================================================
|
||||
-- Part 1: 系统日志表
|
||||
-- ============================================================
|
||||
TRUNCATE TABLE sys_logininfor; -- 登录日志
|
||||
TRUNCATE TABLE sys_oper_log; -- 操作日志
|
||||
TRUNCATE TABLE sys_job_log; -- 定时任务日志
|
||||
|
||||
-- 清用户
|
||||
TRUNCATE TABLE tt_user;
|
||||
|
||||
-- ============================================================
|
||||
-- Part 2: 财务流水
|
||||
-- ============================================================
|
||||
TRUNCATE TABLE tt_user_blend_ercash; -- 余额/积分变动主流水
|
||||
TRUNCATE TABLE tt_user_blend_ercash_archive; -- 流水归档表
|
||||
TRUNCATE TABLE tt_user_amount_records; -- 金币流水明细
|
||||
TRUNCATE TABLE tt_user_credits_records; -- 积分流水明细
|
||||
TRUNCATE TABLE tt_user_source_report; -- 用户来源日报表(累计流水统计)
|
||||
TRUNCATE TABLE tt_user_daily_turnover; -- 用户每日流水统计(VIP 返利/排行榜口径)
|
||||
TRUNCATE TABLE tt_recharge_record; -- 充值记录
|
||||
TRUNCATE TABLE tt_recharge_card; -- 充值卡密
|
||||
TRUNCATE TABLE tt_blank_card_key; -- 空白卡密(后台生成)
|
||||
TRUNCATE TABLE promo_card_key; -- 卡商卡密
|
||||
TRUNCATE TABLE promo_cdkey; -- 推广CDKEY创建记录表
|
||||
TRUNCATE TABLE promo_cdkey_redeem; -- 推广CDKEY兑换记录表
|
||||
TRUNCATE TABLE tt_order; -- 订单
|
||||
TRUNCATE TABLE tianxin_order; -- 天心支付订单
|
||||
TRUNCATE TABLE tt_loss_rebate; -- 亏损返水结算明细(回血红包)
|
||||
TRUNCATE TABLE tt_comp_record; -- 补偿值记录(user_id + box_id + comp_amount)
|
||||
TRUNCATE TABLE tt_promotion_update; -- 推广上下级关系变更记录
|
||||
TRUNCATE TABLE tt_fight_ranking_reward; -- 对战排行榜奖励发放记录
|
||||
TRUNCATE TABLE tt_recharge_ranking_reward; -- 充值排行榜奖励发放记录
|
||||
TRUNCATE TABLE tt_user_sub_account; -- 主号-副号关联
|
||||
TRUNCATE TABLE tt_user_avatar; -- 用户头像
|
||||
|
||||
-- ============================================================
|
||||
-- Part 3: 游戏记录
|
||||
-- ============================================================
|
||||
|
||||
-- 开箱
|
||||
TRUNCATE TABLE tt_box_records;
|
||||
TRUNCATE TABLE tt_user_box_open_chance; -- 用户开箱机会
|
||||
TRUNCATE TABLE tt_box_third_explosive_user; -- 第三方爆率用户记录
|
||||
|
||||
-- 对战(tt_fight_round / tt_fight_result 为 DB 标注的废弃表,0 行)
|
||||
TRUNCATE TABLE tt_fight;
|
||||
ALTER TABLE tt_fight AUTO_INCREMENT = 1000001;
|
||||
TRUNCATE TABLE tt_fight_user;
|
||||
TRUNCATE TABLE tt_fight_round;
|
||||
TRUNCATE TABLE tt_fight_result;
|
||||
|
||||
-- Roll房
|
||||
TRUNCATE TABLE tt_roll;
|
||||
TRUNCATE TABLE tt_roll_user;
|
||||
TRUNCATE TABLE tt_roll_user_prize;
|
||||
TRUNCATE TABLE tt_time_roll; -- 定时Roll房主表
|
||||
TRUNCATE TABLE tt_time_roll_user; -- 定时Roll房参与记录
|
||||
|
||||
-- 幸运升级
|
||||
TRUNCATE TABLE tt_upgrade_record;
|
||||
|
||||
-- 指数盲盒(tt_exponent 是指数期号历史,勿清空,见 Part 7)
|
||||
TRUNCATE TABLE tt_exponent_user;
|
||||
TRUNCATE TABLE tt_exponent_user_box;
|
||||
|
||||
-- 极速糖果(sweet)
|
||||
TRUNCATE TABLE game_sweet_record; -- 对局记录
|
||||
TRUNCATE TABLE game_sweet_user; -- 用户累计统计
|
||||
TRUNCATE TABLE game_sweet_win; -- 每日盈亏统计
|
||||
|
||||
-- 疯狂转盘(wheel)
|
||||
TRUNCATE TABLE game_wheel_round; -- 每期开奖
|
||||
TRUNCATE TABLE game_wheel_round_bet; -- 下注明细
|
||||
TRUNCATE TABLE game_wheel_user; -- 用户累计统计
|
||||
TRUNCATE TABLE game_wheel_pool_adjustment; -- 奖池调节记录
|
||||
|
||||
-- 玫瑰女郎(olympus)
|
||||
TRUNCATE TABLE game_olympus_record; -- 玩家游戏记录
|
||||
TRUNCATE TABLE game_olympus_user; -- 用户累计统计
|
||||
TRUNCATE TABLE game_olympus_win; -- 每日盈亏
|
||||
TRUNCATE TABLE game_olympus_reward_log; -- 奖池水位调整日志
|
||||
|
||||
-- ============================================================
|
||||
-- Part 4: 预生成回合池(默认注释)
|
||||
-- ⚠️ 这些不是配置,是预生成回合池:清空后必须在后台重新生成/重算,
|
||||
-- 否则对应玩法开局时取不到回合。数据量大(约 90 万行),
|
||||
-- 要彻底重置磁盘占用再放开本段。
|
||||
-- ============================================================
|
||||
-- TRUNCATE TABLE game_sweet_round; -- 极速糖果预生成回合(约 8.4 万行)
|
||||
-- TRUNCATE TABLE game_sweet_step; -- 极速糖果回合步骤(约 75 万行 / 626MB)
|
||||
-- TRUNCATE TABLE game_olympus_round; -- 玫瑰女郎预生成回合(约 2.9 万行)
|
||||
-- TRUNCATE TABLE game_olympus_step; -- 玫瑰女郎回合步骤(约 12.9 万行)
|
||||
|
||||
-- ============================================================
|
||||
-- Part 5: 辅助数据
|
||||
-- ============================================================
|
||||
TRUNCATE TABLE tt_attendance_record; -- 签到记录
|
||||
TRUNCATE TABLE tt_message; -- 用户消息
|
||||
TRUNCATE TABLE tt_message_send; -- 消息下发记录
|
||||
TRUNCATE TABLE tt_notice; -- 用户通知
|
||||
TRUNCATE TABLE sys_notice; -- 系统公告
|
||||
TRUNCATE TABLE tt_announcement_read; -- 公告已读记录
|
||||
TRUNCATE TABLE tt_bonus_receive_record; -- 奖励包领取记录
|
||||
TRUNCATE TABLE tt_red_packet; -- 红包
|
||||
TRUNCATE TABLE tt_red_packet_record; -- 红包领取记录
|
||||
TRUNCATE TABLE tt_commission_record; -- 佣金收益记录
|
||||
TRUNCATE TABLE tt_promotion_record; -- 推广记录
|
||||
TRUNCATE TABLE tt_task_center_user; -- 任务中心用户进度
|
||||
TRUNCATE TABLE tt_welfare_record; -- 福利领取记录
|
||||
TRUNCATE TABLE tt_welfare_box_inventory; -- 用户福利箱子库存
|
||||
TRUNCATE TABLE tt_turnover_lottery_record; -- 流水抽奖记录
|
||||
|
||||
-- ============================================================
|
||||
-- Part 6: 发货链路
|
||||
-- ============================================================
|
||||
TRUNCATE TABLE tt_delivery_record; -- 发货记录
|
||||
TRUNCATE TABLE tt_box_transfer_record; -- 饰品转移记录
|
||||
TRUNCATE TABLE tt_replacement_record; -- 饰品替换记录
|
||||
TRUNCATE TABLE tt_transfer_apply; -- 转赠申请(按批次聚合审核)
|
||||
TRUNCATE TABLE tt_user_transfer_daily; -- 用户每日转赠次数记录
|
||||
|
||||
-- ============================================================
|
||||
-- Part 7: 明确【不要】清空的表(说明,勿放开)
|
||||
-- ============================================================
|
||||
-- game_wheel_reward_matrix 疯狂转盘 120 条固定奖励矩阵,删行会导致「命中缺行漏发」
|
||||
-- tt_exponent 指数期号历史;TtExponentGenerateTask 的 @Scheduled 已注释停用,
|
||||
-- 清空后当天无指数记录,指数盲盒无法开局
|
||||
-- 宝箱 / 饰品 / 各玩法配置 tt_box*、tt_ornament*、tt_game_mould*、game_sweet_*、
|
||||
-- game_olympus_*、game_wheel_prize_*、tt_roll_prize_template*、
|
||||
-- tt_roll_jackpot*、tt_upgrade_ornaments*、tt_recharge_config/prod、
|
||||
-- tt_vip_level、tt_welfare*、tt_task_center、tt_turnover_lottery*、
|
||||
-- tt_robot_fight_*、tt_content*、tt_banner、tt_advertisement、
|
||||
-- tt_announcement、promo_admin / promo_menu_config / promo_role_menu
|
||||
-- 一律不清:清空后要重新导入配置,本脚本不负责恢复
|
||||
-- qrtz_* 调度器运行状态(任务定义在 sys_job)
|
||||
-- sys_user / sys_role / sys_menu / sys_config / sys_dict_* / gen_table*
|
||||
-- 框架、权限、字典与代码生成器数据,清了后台就进不去
|
||||
-- *_new / *_old 历史重建脚本(ruoyi-ui/main.py)遗留的影子表,确认无引用后直接 DROP,不必纳入本脚本
|
||||
|
||||
-- 恢复外键检查
|
||||
SET FOREIGN_KEY_CHECKS = 1;
|
||||
|
||||
-- ============================================================
|
||||
-- 维护提示:新增业务表后请同步本文件,可用下面两条反查差异
|
||||
-- 1) 库内全部表:SELECT table_name FROM information_schema.tables
|
||||
-- WHERE table_schema='changzhou' AND table_type='BASE TABLE' ORDER BY table_name;
|
||||
-- 2) 本脚本已覆盖的表:在本文件中搜索 TRUNCATE TABLE
|
||||
-- ============================================================
|
||||
+1748
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,340 @@
|
||||
# 幸运升级接口文档(Cocos 客户端对接)
|
||||
|
||||
> 适用版本:当前 master。所有接口路径前缀 `/api/luckyUpgrade`(另有 1 个关联接口在 `/api/userPackSack`)。
|
||||
> 代码位置:`skins-service/service-playingmethod/.../ApiLuckyUpgradeController.java`、`ApiLuckyUpgradeServiceImpl.java`、`ApiUpgradeRecordServiceImpl.java`。
|
||||
|
||||
---
|
||||
|
||||
## 一、玩法机制(实现口径)
|
||||
|
||||
### 1.1 一局流程
|
||||
|
||||
```
|
||||
选饰品位(upgradeId) + 选成功率(probability)
|
||||
↓ 后端按实时价算钱:price = 饰品价 × probability / 100
|
||||
↓ 校验(身份 / 封禁 / 删除 / 未充值 703 / 余额 702)
|
||||
↓ 加锁(饰品位 + 奖池类型)→ 扣款(source=40 幸运升级消费)
|
||||
↓ 判定(见 1.2)
|
||||
├─ 成功:获得该饰品位对应的整件饰品,进度与幸运值清 0
|
||||
└─ 失败:获得后台配置的「失败奖励」饰品(安慰奖,可多件),进度与幸运值累加
|
||||
↓ 写 tt_upgrade_record 记录(含各项快照)+ 背包 tt_box_records(source=4 幸运升级)+ 站内消息
|
||||
↓ 返回升级记录(含 gainOrnamentList,可用于结果页立即分解)
|
||||
```
|
||||
|
||||
### 1.2 消耗与判定公式
|
||||
|
||||
| 项 | 公式 / 取值 |
|
||||
|---|---|
|
||||
| 单次消耗 | `price = 饰品实时价(tt_ornament.use_price) × probability / 100`,保留 2 位小数(**前端不传价格**,由后端算,防 0 元 100% 中奖) |
|
||||
| 判定概率 | `eff = min(probability, 75)`(**封顶 75**:选 90% 也只按 75% 判定,但钱按 90% 收) |
|
||||
| 本轮空间 | `fullV`:从该饰品位对应奖池的幸运区间 `[min, max)` 里随机取 `nextInt(max-min) + min` |
|
||||
| 判定 | 每拉一次 `进度 += eff`;`进度 + eff >= fullV` 即中奖,中奖后 `fullV` 重新随机、进度清 0;否则失败继续累加 |
|
||||
| 推论 | **玩家为一件饰品付出的总成本 ≈ 饰品价 × fullV / 100**。所以幸运区间 = 该池的"售价百分比区间":`[50,250]` 表示平均要付 50%~250% 原价才出;压到个位数(如 `[9,10]`)等于必中、平台必亏 |
|
||||
|
||||
### 1.3 幸运值与幸运区间:三套独立奖池
|
||||
|
||||
`幸运值` = 该池"累计原始概率",失败累加 `probability`(原始值,不是封顶后的 75)、中奖清 0;界面与记录里展示的就是它。
|
||||
|
||||
| 奖池 | 判定/显示用 userType | 幸运区间字段 | 幸运值字段 | 本轮已投金额字段 |
|
||||
|---|---|---|---|---|
|
||||
| 普通用户 | `02` | `tt_upgrade_ornaments.luck_section` | `probability_invested` | `amount_invested` / `effective_amount_invested` |
|
||||
| 主播 | `01` | `anchor_luck_section` | `anchor_probability_invested` | `anchor_amount_invested` / `anchor_effective_amount_invested` |
|
||||
| 白号 | `06`(`is_blank_account=1` 优先判定) | `blank_luck_section`(未配置回退 `luck_section`) | `blank_probability_invested` | `blank_amount_invested` / `blank_effective_amount_invested` |
|
||||
|
||||
- 三池**完全隔离**:进度缓存在 Redis `upgrade_range_fixed:{饰品位}:{池类型}` / `upgrade_range:{饰品位}:{池类型}`,互不影响、互不阻塞(锁也是按「饰品位 + 池类型」)。
|
||||
- 同一池内是**共享进度**:同一个池子的玩家一起把进度推到 `fullV`,谁那一拉跨过阈值谁中奖(和普通/主播现有行为一致,白号同理,白号之间共享 06 池)。
|
||||
- 白号记录里的幸运值/已投金额读的都是白号自己的池,不再是真实玩家池的读数。
|
||||
|
||||
### 1.4 获得与失败奖励
|
||||
|
||||
- **成功**:获得 `tt_upgrade_ornaments.ornaments_id` 对应的整件饰品,`gain_ornaments_price` = 该饰品实时价。
|
||||
- **失败**:获得后台「幸运升级 → 失败奖励」为该饰品位配置的饰品(`tt_upgrade_fail_ornaments`,可多件,按 `ornament_number` 计数),`gain_ornaments_price` = 安慰奖总价值;该饰品位**没配失败奖励时啥都不给**(`gainOrnamentList` 为 `[]`、金额 0)。
|
||||
- 所有获得物都写入背包 `tt_box_records`(`source = 4` 幸运升级、状态 = 在背包)。
|
||||
|
||||
### 1.5 流水与返佣口径
|
||||
|
||||
| 场景 | 流水 source |
|
||||
|---|---|
|
||||
| 升级消费 | `40`(GOLD_DEVICE,「幸运升级消费」,输出) |
|
||||
| 游戏内分解(结果页立即分解) | `41`(GOLD_DEVICE_REWARD,「幸运升级奖励」,输入) |
|
||||
| 背包里分解(事后在背包页分解) | `10`(DECOMPOSE_ORNAMENT,输入) |
|
||||
|
||||
- 消费 source 40 计入:今日/历史投注流水、综合消费排行榜、流水抽奖资格、**上级返佣**(按 `classicGameCommissionRate` 口径)、**VIP 流水返利**。
|
||||
- **白号例外**:白号消费不触发上级返佣、不触发 VIP 流水返利;后台充值/流水/记录类页面普遍按 `is_blank_account=1` 排除白号。
|
||||
|
||||
---
|
||||
|
||||
## 二、通用约定
|
||||
|
||||
- **域名**:`{host}`(例:`http://103.108.67.60:28081`)。
|
||||
- **Content-Type**:`application/json`;**鉴权**:除 `getOrnamentsList` 外都需要请求头带 token,401 时按项目统一逻辑弹登录。
|
||||
- **统一响应**:
|
||||
|
||||
```json
|
||||
{ "code": 200, "msg": "操作成功", "data": { } }
|
||||
```
|
||||
|
||||
| code | 含义 |
|
||||
|---|---|
|
||||
| 200 | 成功(客户端以 `code === 200` 判定) |
|
||||
| 401 | 未登录 / 登录过期,msg「登录过期,请重新登录。」 |
|
||||
| 702 | 账户余额不足 |
|
||||
| 703 | 玩家未充值不能游玩任何游戏 |
|
||||
| 500 | 业务失败,msg 直接给玩家看 |
|
||||
|
||||
- **维护开关**(`sys_config`,后台「网站设置」):`websiteMaintenance=1` → 「网站维护中......」;`luckyUpgradeMaintenance=1` → 「幸运升级维护中......」。两个开关都**先于登录校验**返回。
|
||||
- **通用拦截**(`upgrade` 接口):非 主播(01)/普通(02)/白号 → 「非法的用户类型」;`status=1` → 「账号已被封禁,无法参与游戏」;`del_flag != 0` → 「账号状态异常,无法参与游戏」。
|
||||
- 幸运升级相关也有 `/api/luckyUpgrade/**` 其他接口,本文件只覆盖下表这些。
|
||||
|
||||
---
|
||||
|
||||
## 三、接口清单
|
||||
|
||||
| 接口 | 说明 | 鉴权 | 分页 |
|
||||
|---|---|---|---|
|
||||
| `POST /api/luckyUpgrade/getOrnamentsList` | 可升级饰品列表(含当前幸运值) | 否(未登录按普通用户口径) | 无 |
|
||||
| `POST /api/luckyUpgrade/upgrade` | 拉取一次(扣款 + 判定 + 发奖) | 是 | - |
|
||||
| `POST /api/luckyUpgrade/recordPage` | 记录页:全服/我的 × 当前饰品/全部饰品 | 是 | 真分页 |
|
||||
| `POST /api/luckyUpgrade/recentPulls` | 某饰品位最近 50 次拉取流水 | 是 | 无(固定 50) |
|
||||
| `POST /api/luckyUpgrade/myHistoryRecord` | 我的记录(全部饰品,成败都返回) | 是 | 真分页 |
|
||||
| `POST /api/luckyUpgrade/getUpgradeRecord` | 通用条件查询(内部/后台复用) | 是 | 真分页 |
|
||||
| `POST /api/luckyUpgrade/tenTopUpgradeRecord` | 十佳掉落榜(按实际获得价值 Top10) | 是 | 无(固定 10) |
|
||||
| `POST /api/userPackSack/decompose` | 分解获得的饰品(结果页或背包) | 是 | - |
|
||||
|
||||
---
|
||||
|
||||
## 四、接口详情
|
||||
|
||||
### 4.1 `POST /api/luckyUpgrade/getOrnamentsList` — 可升级饰品列表
|
||||
|
||||
**请求**(全部可选,无分页、无排序参数,返回按 `use_price` 升序)
|
||||
|
||||
| 字段 | 类型 | 说明 |
|
||||
|---|---|---|
|
||||
| id | Integer | 可直接取某一个饰品位(`tt_upgrade_ornaments.id`) |
|
||||
| type | String | 饰品类型数字字典码(1匕首 2手枪 3步枪 4微型冲锋枪 5重型武器 6手套 7印花 8其它);空串按不传处理 |
|
||||
| itemName | String | 饰品名称模糊匹配 |
|
||||
| priceMin / priceMax | BigDecimal | 按价格区间过滤(**只在传了 `priceMax` 时生效**;不传 `priceMin` 默认 0) |
|
||||
| userType | String | **后端注入,客户端不要传**(用登录用户身份决定返回哪个池的幸运值) |
|
||||
|
||||
```json
|
||||
{ "type": "3", "itemName": "", "priceMax": 5000 }
|
||||
```
|
||||
|
||||
**响应**:`data` 为数组,仅返回 `status='0'`(启用)的饰品位:
|
||||
|
||||
| 字段 | 类型 | 说明 |
|
||||
|---|---|---|
|
||||
| id | Integer | **可升级饰品ID**(后续 `upgrade.upgradeId`、`recordPage.upgradeId`、`recentPulls.upgradeRecordId` 都传它) |
|
||||
| ornamentId | Long | 饰品ID(`tt_ornament.id`) |
|
||||
| name | String | 饰品全名(优先展示) |
|
||||
| itemName | String | 名称(与 name 同源字段) |
|
||||
| usePrice | BigDecimal | 实时价格(= 计算消耗、展示目标金额的基准) |
|
||||
| imageUrl | String | 饰品图片 |
|
||||
| shortName / typeName / rarityName / exteriorName / levelImg | String | 简称 / 类型 / 稀有度 / 磨损度 / 品质图 |
|
||||
| luckyValue | Integer | **当前幸运值**(按登录身份取对应池:普通/主播/白号) |
|
||||
|
||||
### 4.2 `POST /api/luckyUpgrade/upgrade` — 拉取一次
|
||||
|
||||
**请求**
|
||||
|
||||
| 字段 | 类型 | 必填 | 说明 |
|
||||
|---|---|---|---|
|
||||
| upgradeId | Integer | 是 | 可升级饰品ID(`getOrnamentsList` 的 `id`) |
|
||||
| probability | Integer | 是 | 本次选择的成功率,1~100;**判定按 min(概率,75),扣款按传入值** |
|
||||
|
||||
```json
|
||||
{ "upgradeId": 25, "probability": 10 }
|
||||
```
|
||||
|
||||
**响应**:`data` = 本条升级记录(`tt_upgrade_record`),字段如下:
|
||||
|
||||
| 字段 | 类型 | 说明 |
|
||||
|---|---|---|
|
||||
| id | Long | 升级记录ID |
|
||||
| userId / nickName / userType / userAvatar | - | 玩家信息(昵称是升级时快照) |
|
||||
| probability | Integer | 本次选择的成功率(原始概率快照) |
|
||||
| luckyValue | Integer | 本次升级时的幸运值快照(中奖为 0) |
|
||||
| amountConsumed | BigDecimal | 本次实际消耗 |
|
||||
| amountRequired | BigDecimal | 本轮目标金额快照 = 饰品价 × fullV% |
|
||||
| amountInvested / effectiveAmountInvested | BigDecimal | 本轮已投金额(实际扣款口径 / 真实计算口径)快照 |
|
||||
| effectiveAmount | BigDecimal | 真实计算金额 = 饰品价 × min(概率,75)% |
|
||||
| targetUpgradeId | Integer | 可升级饰品ID |
|
||||
| targetOrnamentId | Long | 目标饰品ID |
|
||||
| targetOrnamentPrice | BigDecimal | 目标饰品价格 |
|
||||
| isVictory | Boolean | 成败:true=升级成功 |
|
||||
| gainOrnamentsPrice | BigDecimal | 本次获得饰品总价值(成功=目标饰品价,失败=安慰奖总价,可能为 0) |
|
||||
| gainOrnamentList | Array | **本次实际获得物**(JSON 数组,不是字符串),每项:`id`=背包记录ID(`tt_box_records.id`)、`ornamentId`、`ornamentName`、`ornamentHashName`、`ornamentPrice`、`ornamentImgUrl`、`ornamentLevelId`、`ornamentLevelImg`、`ornamentNumber`;无获得时为 `[]` |
|
||||
| openTime | String | 升级时间 `yyyy-MM-dd HH:mm:ss` |
|
||||
|
||||
```json
|
||||
{
|
||||
"code": 200,
|
||||
"msg": "操作成功",
|
||||
"data": {
|
||||
"id": 93, "userId": 34052018, "probability": 10, "luckyValue": 0,
|
||||
"amountConsumed": 181.95, "amountRequired": 1819.52, "isVictory": true,
|
||||
"gainOrnamentsPrice": 1819.52,
|
||||
"gainOrnamentList": [
|
||||
{ "id": 11530, "ornamentId": 1312995578, "ornamentName": "AK-47 | 表面淬火 (略有磨损)",
|
||||
"ornamentPrice": 1819.52, "ornamentImgUrl": "/uploads/xxx.png", "ornamentLevelImg": "/profile/xxx.png" }
|
||||
],
|
||||
"openTime": "2026-09-11 18:24:01"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**失败返回示例**:`{"code":500,"msg":"系统繁忙,请稍后重试。"}`(同池并发抢锁失败,客户端可直接重试);`{"code":703,"msg":"玩家未充值不能游玩任何游戏"}`;`{"code":702,"msg":"账户余额不足"}`;`{"code":500,"msg":"该饰品未在幸运升级开放。"}`。
|
||||
|
||||
**结果页立即分解**:取 `gainOrnamentList[i].id` 作为 `packSackIds`,调 4.8 接口(`decomposeType="game"`)。不分解就等于饰品留在背包里。
|
||||
|
||||
### 4.3 `POST /api/luckyUpgrade/recordPage` — 记录页(主力接口)
|
||||
|
||||
**请求**(二维组合,共 4 种场景,均需登录)
|
||||
|
||||
| 字段 | 类型 | 必填 | 默认 | 说明 |
|
||||
|---|---|---|---|---|
|
||||
| scope | Integer | 否 | 0 | 0=全服(不过滤用户) 1=我的 |
|
||||
| itemScope | Integer | 否 | 0 | 0=全部饰品 1=当前饰品 |
|
||||
| upgradeId | Long | 条件必填 | - | 可升级饰品ID;`itemScope=1` 时必填,为空返回 500;`itemScope=0` 时传了也忽略 |
|
||||
| page | Integer | 否 | 1 | 页码(≥1) |
|
||||
| size | Integer | 否 | 10 | 每页条数(1~20,超限参数错误) |
|
||||
|
||||
```json
|
||||
{ "scope": 1, "itemScope": 1, "upgradeId": 25, "page": 1, "size": 10 }
|
||||
```
|
||||
|
||||
**响应**:`data` 为分页对象 `{ records, total, current, size, pages }`,`records` 按 `openTime` 倒序:
|
||||
|
||||
| 字段 | 类型 | 说明 |
|
||||
|---|---|---|
|
||||
| userId / avatar / nickName / vipLevel | - | 玩家信息(昵称是升级时快照) |
|
||||
| probability | Integer | 成功率(原始概率,如 89 表示 89%) |
|
||||
| luckyValue | Integer | 幸运值快照(按该玩家所属池;白号是白号池的值) |
|
||||
| amountConsumed | BigDecimal | 消耗 |
|
||||
| gainOrnamentsPrice | BigDecimal | 获得饰品总价值 |
|
||||
| isVictory | Boolean | 成败:true=升级成功(前端胜负样式用) |
|
||||
| openTime | String | 升级时间 |
|
||||
| targetOrnamentId | Long | 目标饰品ID |
|
||||
| name / imageUrl / usePrice / exteriorName / levelImg | - | **本次实际获得的饰品**:成功=目标饰品,失败=安慰奖饰品(`gainOrnamentList` 首件);未配安慰奖的失败记录会回退成目标饰品且金额为 0,**胜负请用 isVictory 判断,不要看图片** |
|
||||
|
||||
- 全服(scope=0)不过滤白号、机器人、封禁用户,白号可正常上榜。
|
||||
- 旧的 `recentPulls` / `myHistoryRecord` 场景都被本接口覆盖,可继续用但建议逐步切到本接口。
|
||||
|
||||
### 4.4 `POST /api/luckyUpgrade/recentPulls` — 某饰品位最近 50 次拉取流水
|
||||
|
||||
**请求**:`{ "upgradeRecordId": 25 }`
|
||||
|
||||
| 字段 | 类型 | 必填 | 说明 |
|
||||
|---|---|---|---|
|
||||
| upgradeRecordId | Long | 是 | ⚠️ 字段名有历史误导性:**实际语义是「可升级饰品ID」`tt_upgrade_ornaments.id`**(`getOrnamentsList` 的 `id`)。为空返回 500「upgradeRecordId(升级饰品ID)不能为空」 |
|
||||
| 其他字段 | - | 否 | `ornamentId`/`userType`/`userId`/`page`/`size`/`limit` 全部忽略 |
|
||||
|
||||
- **无分页**:固定最近 **50** 条(SQL `LIMIT 50`),没有 `page`/`total`。
|
||||
- **不过滤**:全服所有人(含白号、机器人、封禁/删除用户),成功与失败都返回。
|
||||
- `nickName`/`avatar`/`vipLevel` 可能为 `null`(该记录的用户已不在 `tt_user`),客户端要能容错。
|
||||
|
||||
**响应**:`data` 直接是数组(不是分页对象):
|
||||
|
||||
| 字段 | 类型 | 说明 |
|
||||
|---|---|---|
|
||||
| avatar / nickName / vipLevel | - | 拉取人信息 |
|
||||
| probability | Integer | 本次拉取**选择的成功率**(不是中奖率) |
|
||||
| luckyValue | Integer | 本次拉取时的**幸运值快照**(累计原始概率;成功为 0),与 `recordPage` 同口径 |
|
||||
| isVictory | Boolean | 成败:true=升级成功 |
|
||||
| name / imageUrl / usePrice / exteriorName / levelImg | - | **目标饰品**("谁在拉这个饰品",不是获得物) |
|
||||
| openTime | String | 拉取时间 |
|
||||
|
||||
### 4.5 `POST /api/luckyUpgrade/myHistoryRecord` — 我的记录(全部饰品)
|
||||
|
||||
| 字段 | 类型 | 必填 | 默认 | 说明 |
|
||||
|---|---|---|---|---|
|
||||
| page | Integer | 否 | 1 | 页码 |
|
||||
| size | Integer | 否 | 10 | 1~20 |
|
||||
|
||||
**响应**:`data` 为分页对象,`records` 是 `tt_upgrade_record`(字段同 4.2 的响应,含 `gainOrnamentList`),仅当前登录用户、不含他人记录、成败都返回、按时间倒序;昵称/头像会补齐。
|
||||
|
||||
### 4.6 `POST /api/luckyUpgrade/getUpgradeRecord` — 通用条件查询
|
||||
|
||||
| 字段 | 类型 | 必填 | 说明 |
|
||||
|---|---|---|---|
|
||||
| page / size | Integer | **必传** | 不传会 500(内部直接用于构造分页对象) |
|
||||
| upgradeRecordId | Long | 否 | 按记录ID精确查 |
|
||||
| ornamentId | Long | 否 | 按目标饰品ID过滤 |
|
||||
| userType | String | 否 | 按用户类型过滤(01/02/06…) |
|
||||
| userId | Integer | 否 | 按用户过滤 |
|
||||
|
||||
**响应**:`data` 为分页对象(`records` 为 `tt_upgrade_record`,`userAvatar` 已补齐,`gainOrnamentList` 空值兜底为 `[]`)。主要用于内部/后台复用,客户端记录页请用 `recordPage`。
|
||||
|
||||
### 4.7 `POST /api/luckyUpgrade/tenTopUpgradeRecord` — 十佳掉落榜
|
||||
|
||||
| 字段 | 类型 | 必填 | 说明 |
|
||||
|---|---|---|---|
|
||||
| isVictory | Boolean | 否 | `true` 只看成功、`false` 只看失败;不传则全部记录一起排 |
|
||||
|
||||
**响应**:`data` 为分页对象(固定第 1 页、10 条),按 **`gainOrnamentsPrice`(实际获得价值)倒序**取 Top10 —— 即"实际掉落榜",不是目标饰品价值榜。
|
||||
|
||||
### 4.8 `POST /api/userPackSack/decompose` — 分解饰品(结果页/背包)
|
||||
|
||||
| 字段 | 类型 | 必填 | 说明 |
|
||||
|---|---|---|---|
|
||||
| packSackIds | Long[] | 条件必填 | 背包记录ID(`tt_box_records.id`,即 `gainOrnamentList[i].id`);`isAll=true` 时可不传 |
|
||||
| isAll | Boolean | 是 | 是否全选 |
|
||||
| decomposeType | String | 否 | `game`=游戏结果页立即分解(按饰品来源记 `GOLD_DEVICE_REWARD(41)`);`backpack`=背包分解(记 `DECOMPOSE_ORNAMENT(10)`) |
|
||||
|
||||
返回 `{"code":200,"msg":"成功分解N件饰品"}`。受 `websiteMaintenance` 开关拦截。
|
||||
|
||||
---
|
||||
|
||||
## 五、前端对接要点
|
||||
|
||||
1. **upgradeId 语义**:所有接口里的 `upgradeId` / `recentPulls.upgradeRecordId` 都是**可升级饰品ID**(`tt_upgrade_ornaments.id`),来自 `getOrnamentsList` 的 `id`,不是 `tt_ornament.id`,也不是升级记录ID。
|
||||
2. **扣款以服务端为准**:不要传价格;展示价格用 `usePrice × probability / 100`(与后端同公式,后端保留 2 位小数)。
|
||||
3. **概率封顶 75**:UI 上若提供 >75% 的档位,要提示"更高概率只加钱、不提升判定"。
|
||||
4. **幸运值怎么用**:主界面实时幸运值取 `getOrnamentsList.luckyValue`;拉取成功后用 `upgrade` 响应里的 `luckyValue` 立即覆盖,不必等列表刷新。记录里的幸运值是历史快照。
|
||||
5. **分池**:幸运值/幸运区间按 普通(02) / 主播(01) / 白号(06) 三套独立,白号看到的是白号池的值;同一池内进度是共享的,别人垫的进度也算在你头上。
|
||||
6. **分页**:`recordPage`/`myHistoryRecord` 的 `size` 上限 20;切换 tab(我的/全服、当前饰品/全部饰品)时 `page` 重置为 1。
|
||||
7. **失败记录展示**:以 `isVictory` 判断胜负,`name/imageUrl/...` 是"实际获得物"(失败=安慰奖);无安慰奖时金额为 0。
|
||||
8. **结果页分解**:`gainOrnamentList` 里的 `id` 就是分解入参;不调用 = 饰品留在背包。
|
||||
9. **并发**:同一饰品位 + 同一奖池同时拉取会串行,抢不到锁返回 500「系统繁忙,请稍后重试。」,可直接提示重试。
|
||||
|
||||
---
|
||||
|
||||
## 六、后台配置与调参
|
||||
|
||||
### 6.1 「幸运升级 → 饰品列表」(`tt_upgrade_ornaments`)
|
||||
|
||||
| 后台列 | 字段 | 说明 |
|
||||
|---|---|---|
|
||||
| 普通用户 / 幸运区间 | `luck_section` | 普通用户池的 `[min, max)`,即"售价百分比区间" |
|
||||
| 普通用户 / 本轮已投金额 | `amount_invested` | 普通池当前累计投入(运行时值,后台不可编辑回写) |
|
||||
| 主播用户 / 幸运区间 | `anchor_luck_section` | 主播池区间 |
|
||||
| 白号用户 / 幸运区间 | `blank_luck_section` | 白号池区间;为空则回退 `luck_section` |
|
||||
| 白号用户 / 本轮已投金额 | `blank_amount_invested` | 白号池当前累计投入 |
|
||||
| 状态 | `status` | 0=启用(=可升级、会出现在客户端列表) 1=禁用 |
|
||||
|
||||
- 调难度就调"幸运区间":区间 = 玩家为该饰品付出的**价格百分比**。想更容易出就整体下调(如 `[80,280]`→`[60,200]`);**不要压到个位数**(如 `[9,10]` 等于必中,等于按 9% 价格卖货),也不要配成 `min == max`(会被后端防护强制改成 `[0,160]`)。
|
||||
- **改完区间要清缓存才立即生效**:`DEL upgrade_range:{饰品位}:{池}` + `DEL upgrade_range_fixed:{饰品位}:{池}`(池=01/02/06)。不清的话当前这一轮仍按旧空间跑,等本轮中奖后自动按新配置重建。Redis 实测在 db0,密码见 `application-*.yml`。
|
||||
- 白号是独立奖池,可与普通/主播配不同的区间;「批量填货」新增饰品位时白号区间默认取普通用户的默认值。
|
||||
|
||||
### 6.2 「幸运升级 → 失败奖励」(`tt_upgrade_fail_ornaments`)
|
||||
|
||||
每个饰品位可配多件失败奖励(`ornament_name` / `ornament_price` / `ornament_number` / `ornament_level_id`),玩家升级失败即发放这些饰品;**没配就不发**(记录里 `gainOrnamentList=[]`、`gainOrnamentsPrice=0`)。
|
||||
|
||||
### 6.3 维护开关(后台「网站设置」,`sys_config`)
|
||||
|
||||
| key | 值 | 效果 |
|
||||
|---|---|---|
|
||||
| `websiteMaintenance` | `1` | 全站维护:`upgrade` 与分解接口都拦截 |
|
||||
| `luckyUpgradeMaintenance` | `1` | 只拦幸运升级:`upgrade` 返回「幸运升级维护中......」 |
|
||||
|
||||
### 6.4 数据落库
|
||||
|
||||
| 表 | 说明 |
|
||||
|---|---|
|
||||
| `tt_upgrade_record` | 每条拉取记录(成败、消耗、获得总额、各项快照、`gain_ornament_list` JSON、`lucky_value`) |
|
||||
| `tt_box_records` | 获得物入背包(`source=4` 幸运升级),`id` 即结果页分解入参 |
|
||||
| `tt_user_blend_ercash` | 余额流水:消费 40、游戏内分解 41、背包分解 10 |
|
||||
| `tt_upgrade_ornaments` | 饰品位配置 + 三个池的运行进度(各池幸运值/已投金额) |
|
||||
|
||||
字段变更历史见 `docs/updata_sql.md`(含白号三列的 ALTER)。
|
||||
@@ -0,0 +1,447 @@
|
||||
# 流水抽奖接口文档(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`(搜索"流水抽奖") |
|
||||
@@ -0,0 +1,707 @@
|
||||
# 玫瑰女郎 用户端 接口文档
|
||||
|
||||
---
|
||||
|
||||
## 一、功能说明
|
||||
|
||||
玫瑰女郎是一款**选箱下注 → 旋转 → 必得饰品**的电子游戏。核心产品逻辑:
|
||||
|
||||
| 规则 | 说明 |
|
||||
|------|------|
|
||||
| 三种玩法模式 | `normal` 普通旋转 / `ante` 底注 / `buy_fs` 购买免费旋转,互斥 |
|
||||
| 下注即得 | **开局即结算**:玩家扣费后,本轮结果与奖品在开局那一刻就已确定,饰品**直接写入背包**;前端播放动画仅是"揭晓" |
|
||||
| 无损模型 | 中途刷新 / 退出 / 断线,饰品已在背包,不会丢奖 |
|
||||
| 按身份差异化 | `common` 普通用户 / `anchor` 主播 / `robot` 机器人 / `white` 白号,各自独立的一套饰品桶分布与 RTP |
|
||||
| 1000x 封顶 | 总倍率 ≥ 1000x 视为已封顶,`maxWinReached=true`;**单局累计到账达 1000 倍即截断终止**,保证动画总赢额 = 实际到账 |
|
||||
| 自动分解 | 命中饰品默认写入背包;末帧由**前端**给玩家两个选择「放入背包 / 立即分解」。"自动分解"是前端自己的开关,开启后前端在末帧替玩家调一次分解接口即可,**后端不参与、也不回传该状态**;调不动(断网/超时)也只是饰品留在背包,玩家随时能自己分解,不会丢奖、不会重复入账 |
|
||||
| 消除规则 | 棋盘 5 行 × 6 列共 30 格;**同一符号满 8 个即消除**,按 8-9 / 10-11 / 12-30 三档赔付 |
|
||||
| 翻滚级联 | 消除 → 上方掉落 → 补位,可在同一旋内连锁多步,每步返回 `gridState`/`symbolLinks`(累积)/`dropData`/`fillData` |
|
||||
| 炸弹(乘数球) | **只在免费旋转内出现**(normal/ante 的基础旋、含进 FS 的触发盘一律没有炸弹);每屏最多 1 颗,乘数值 2~1000(见 `multiplierDetail`),FS 内跨旋累加 |
|
||||
| 免费旋转 | 基础盘 4+ SCATTER 触发,赠 15 旋(4/5/6 个 SCATTER 赔 3x/5x/100x);FS 内 3+ SCATTER 重触发 +5 旋,总上限 30 旋(到上限后不再落 3+ 个 SCATTER);**三种模式进 FS 后规则完全一致** |
|
||||
| 1000x 截断 | 单局累计到账达 1000 倍即截断终止(保证动画总赢额 = 实际到账) |
|
||||
|
||||
> 前端只需保证:**首次用 `boxId` 开局,之后不断调用 `继续` 逐步揭晓,直到 `state === "FINISH"`。**
|
||||
|
||||
---
|
||||
|
||||
## 二、通用约定
|
||||
|
||||
- **Base URL**:`{host}/api/olympus`
|
||||
- **请求方式**:全部为 `POST`
|
||||
- **Content-Type**:`application/json`
|
||||
- **鉴权**:需登录态,请求头携带 token(`Authorization: Bearer xxx`)。`userId` 由后端从 token 解析,**客户端不要传 userId**。
|
||||
|
||||
### 统一响应结构
|
||||
|
||||
```json
|
||||
{
|
||||
"code": 200,
|
||||
"msg": "操作成功",
|
||||
"data": {}
|
||||
}
|
||||
```
|
||||
|
||||
| code | 含义 |
|
||||
|------|------|
|
||||
| 200 | 成功 |
|
||||
| 500 | 业务失败,提示语见 `msg`(直接向玩家展示) |
|
||||
| 701 | 未实名认证,客户端统一弹实名认证窗口(**玫瑰女郎接口不会返回该码**,统一约定保留,便于前端全局处理) |
|
||||
| 702 | 账户余额不足(`msg`:`账户余额不足`) |
|
||||
| 703 | 玩家未充值,不能游玩任何游戏(`msg`:`玩家未充值不能游玩任何游戏`) |
|
||||
|
||||
> 客户端判定成功以 `code === 200` 为准;失败时直接展示 `msg`。
|
||||
|
||||
### 玩法模式(boxType)
|
||||
|
||||
| boxType | modeName | 成本倍数 costMultiplier | 备注 |
|
||||
|---------|----------|------------------------|------|
|
||||
| `normal` | 普通旋转 | 1x | 标准玩法,饰品桶 0x-150x;基础旋不出炸弹,4+ SCATTER 自然进 FS(进 FS 后与买 FS 一致) |
|
||||
| `ante` | 底注 | 2x | SCATTER 概率提升(FS 触发 0.5%→1%),饰品桶 0x-200x;基础旋同样不出炸弹 |
|
||||
| `buy_fs` | 购买免费旋转 | 50x | 保证直接进入免费旋转,饰品桶 10x-1000x |
|
||||
|
||||
> 其中 `price`(单次下注实际扣费)= 箱子基础下注 `basicBet` × 该模式成本倍数。
|
||||
|
||||
---
|
||||
|
||||
## 三、符号与面值对照表
|
||||
|
||||
`gridState`、`symbolLinks`、`dropData`、`fillData` 中的符号一律是**短码**(单字母),前端按本节映射图标与文案。
|
||||
|
||||
### 3.1 普通符号(可消除)
|
||||
|
||||
| 短码 | 符号 code | 中文名 | 8-9 个 | 10-11 个 | 12-30 个 | 出现权重 |
|
||||
|------|-----------|--------|--------|----------|----------|----------|
|
||||
| `A` | CROWN | 皇冠 | 10x | 25x | 50x | 5 |
|
||||
| `B` | HOURGLASS | 沙漏 | 2.5x | 10x | 25x | 6 |
|
||||
| `C` | RING | 环 | 2x | 5x | 15x | 7 |
|
||||
| `D` | CHALICE | 杯赛 | 1.5x | 2x | 12x | 8 |
|
||||
| `E` | RED_GEM | 红宝石 | 1x | 1.5x | 10x | 9 |
|
||||
| `F` | PURPLE_GEM | 紫宝石 | 0.8x | 1.2x | 8x | 10 |
|
||||
| `G` | YELLOW_GEM | 黄宝石 | 0.5x | 1x | 5x | 10 |
|
||||
| `H` | GREEN_GEM | 绿宝石 | 0.4x | 0.9x | 4x | 10 |
|
||||
| `I` | BLUE_GEM | 蓝宝石 | 0.25x | 0.75x | 2x | 12 |
|
||||
|
||||
> 赔付以 **1 倍基础下注**为基准(接口里返回的金额字段均已再 ×bet);同一符号不足 8 个不成奖,最多计 30 个。
|
||||
|
||||
### 3.2 特殊符号
|
||||
|
||||
| 短码 | 符号 | 说明 |
|
||||
|------|------|------|
|
||||
| `S` | SCATTER | 不参与消除、不计入 `symbolLinks` 的消除统计;基础盘 4+ 触发免费旋转(4/5/6 个赔 3x/5x/100x,即 `scatterTrigger.scatterWin`),免费旋转内 3+ 重触发 +5 旋(总上限 30 旋,到上限后不再落 3+ 个 S) |
|
||||
| `M` | BOMB | 炸弹(乘数球),**乘数值不在短码里**,见 `multiplierDetail`;不参与消除、跨 cascade 保留,免费旋转内跨旋累加(`accumulator`)。**基础旋(`spinNum=0`,含触发盘)不会出现 `M`**;进 FS 后三种模式一致,每屏最多 1 颗 |
|
||||
|
||||
炸弹品质码(`multiplierDetail[].quality`):
|
||||
|
||||
| quality | 颜色 | 等级 | 乘数值 |
|
||||
|---------|------|------|--------|
|
||||
| `GREEN` | 绿 | 1 | 2 / 3 / 4 / 5 / 6 |
|
||||
| `BLUE` | 蓝 | 2 | 8 / 10 / 12 / 15 |
|
||||
| `PURPLE` | 紫 | 3 | 20 / 25 / 50 |
|
||||
| `RED` | 红 | 4 | 100 / 250 |
|
||||
| `RAINBOW` | 彩虹 | 5 | 500 / 1000 |
|
||||
|
||||
### 3.3 位置索引约定
|
||||
|
||||
`multiplierDetail[].pos`、`symbolLinks[].loc[]`、`dropData[].from`/`.to`、`fillData[].pos` 全部是**从 1 开始的格位号**,共 30 格:
|
||||
|
||||
```
|
||||
pos = 行序号(0 起) × 6 + 列序号(0 起) + 1 // 1 ~ 30,行=5、列=6
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 四、接口清单
|
||||
|
||||
| # | 接口 | 路径 | 用途 |
|
||||
|---|------|------|------|
|
||||
| 1 | 获取箱子列表 | `POST /api/olympus/boxes` | 进入游戏页拉取可选箱子 |
|
||||
| 2 | 执行旋转(开始/继续) | `POST /api/olympus/dospin` | 开局扣费并返回第一步 / 继续揭晓下一步 |
|
||||
| 3 | 查询游戏记录(我的/全服,分页) | `POST /api/olympus/records` | `scope=1` 我的战绩 / `scope=0` 全服记录,均分页;**只含已结束的局** |
|
||||
| 4 | 查询进行中的游戏 | `POST /api/olympus/hasActive` | 断线重连时恢复动画 |
|
||||
| 5 | 背包分解(关联) | `POST /api/userPackSack/decompose` | 末帧「立即分解」(含前端自动分解) |
|
||||
|
||||
---
|
||||
|
||||
## 五、接口详情
|
||||
|
||||
### 1. 获取箱子列表
|
||||
|
||||
**`POST /api/olympus/boxes`**
|
||||
|
||||
进入游戏页时调用,返回当前可选的全部箱子(后端只返回启用 `status=1` 的箱子)。
|
||||
|
||||
#### 请求参数
|
||||
|
||||
无,body 可不传(或传空对象 `{}`)。
|
||||
|
||||
#### 响应 data 结构
|
||||
|
||||
```json
|
||||
{
|
||||
"list": [
|
||||
{
|
||||
"boxId": 1001,
|
||||
"boxName": "新手箱",
|
||||
"basicBet": 10.00,
|
||||
"price": 10.00,
|
||||
"boxType": "normal",
|
||||
"modeName": "普通旋转"
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
| 字段 | 类型 | 说明 |
|
||||
|------|------|------|
|
||||
| boxId | Long | 箱子 ID,开局时作为 `boxId` 传入 dospin |
|
||||
| boxName | String | 箱子名称 |
|
||||
| basicBet | Decimal | 基础下注金额 |
|
||||
| price | Decimal | 单次实际扣费 = basicBet × 模式成本倍数 |
|
||||
| boxType | String | `normal`/`ante`/`buy_fs` |
|
||||
| modeName | String | 模式中文名(普通旋转/底注/购买免费旋转) |
|
||||
|
||||
---
|
||||
|
||||
### 2. 执行旋转(开始 / 继续)
|
||||
|
||||
**`POST /api/olympus/dospin`**
|
||||
|
||||
这是**核心接口**,有两种用法:
|
||||
|
||||
- **开始新游戏**:body 携带非空 `boxId` → 后端校验余额/充值/身份 → 扣费 → 选回合 → 返回**第一步**动画数据。
|
||||
- **继续游戏**:body 不带 `boxId`(传 `{}` 或 `{"boxId": null}`)→ 返回**下一步**动画数据,直到 `state === "FINISH"`。
|
||||
|
||||
> ⚠️ 继续时必须传一个 JSON body(至少 `{}`)。若完全不传 body,后端可能因读取不到 boxId 而报错。
|
||||
|
||||
#### 请求参数
|
||||
|
||||
```json
|
||||
{
|
||||
"boxId": 1001
|
||||
}
|
||||
```
|
||||
|
||||
| 参数 | 类型 | 必填 | 说明 |
|
||||
|------|------|------|------|
|
||||
| boxId | Long | 否 | **非空**=开始新游戏;**为空/null/不传**=继续当前游戏 |
|
||||
|
||||
#### 响应 data 结构(SpinResponse)
|
||||
|
||||
完整响应体(`@JsonInclude(NON_NULL)`,未出现的字段即为 `null`/未返回)。下面是**真实数据**的一步(box1 普通旋转 · 自然触发的免费旋转**第 6 旋 · 第 2 波**消除,屏上还留着一颗 2 倍炸弹;回合 `round 66551`,整局实付 125.45 倍底注、30 旋、42 步):
|
||||
|
||||
```json
|
||||
{
|
||||
"state": "PLAYING",
|
||||
"spinNum": 6,
|
||||
"tumbleNum": 2,
|
||||
"globalStep": 10,
|
||||
"globalTotalSteps": 42,
|
||||
"gridState": [
|
||||
["C","G","F","I","A","F"],
|
||||
["F","M","C","D","F","F"],
|
||||
["F","H","E","I","F","D"],
|
||||
["I","C","I","E","F","H"],
|
||||
["H","H","H","I","A","E"]
|
||||
],
|
||||
"multiplierDetail": [{"pos": 8, "val": 2, "quality": "GREEN"}],
|
||||
"symbolLinks": [
|
||||
{"symbol": "G", "count": 8, "loc": [3,5,10,13,18,25,26,30], "base_score": 0.50, "tumble": 1},
|
||||
{"symbol": "F", "count": 8, "loc": [3,6,7,11,12,13,17,23], "base_score": 0.80, "tumble": 2}
|
||||
],
|
||||
"dropData": [
|
||||
{"from": 19, "to": 25, "symbol": "H"},
|
||||
{"from": 7, "to": 19, "symbol": "I"},
|
||||
{"from": 1, "to": 13, "symbol": "F"},
|
||||
{"from": 20, "to": 26, "symbol": "H"},
|
||||
{"from": 14, "to": 20, "symbol": "C"},
|
||||
{"from": 8, "to": 14, "symbol": "H"},
|
||||
{"from": 2, "to": 8, "symbol": "M"},
|
||||
{"from": 4, "to": 10, "symbol": "D"},
|
||||
{"from": 24, "to": 30, "symbol": "E"},
|
||||
{"from": 12, "to": 24, "symbol": "H"},
|
||||
{"from": 6, "to": 18, "symbol": "D"}
|
||||
],
|
||||
"fillData": [
|
||||
{"pos": 1, "symbol": "C"}, {"pos": 2, "symbol": "G"}, {"pos": 3, "symbol": "F"},
|
||||
{"pos": 4, "symbol": "I"}, {"pos": 5, "symbol": "A"}, {"pos": 6, "symbol": "F"},
|
||||
{"pos": 7, "symbol": "F"}, {"pos": 12, "symbol": "F"}
|
||||
],
|
||||
"winAmount": 1.30,
|
||||
"spinWin": 14.55
|
||||
}
|
||||
```
|
||||
|
||||
读法(这一步):
|
||||
- 触发盘(`spinNum: 0`)**不会出现 `M`**:这一局基础旋摇出 4 个 S,白送 3.00 并进 FS(见下面买 FS 示例①)
|
||||
- `symbolLinks` 有 **2 条** —— `tumble:1` 是第 1 波(G 符号 8 连)、`tumble:2` 是第 2 波(F 符号 8 连);**本步新增的只有 `tumble === tumbleNum(2)` 那条**,另一条是给面板累加用的
|
||||
- `winAmount = 1.30` 是**本旋(第 6 旋)累计到本步的赢额**(= 0.50 + 0.80,中间波只记基础分),**不是本步增量**;**本旋末帧才把累计倍数乘上去** —— 这一旋末帧 `winAmount` 会跳到 11.70(本旋基础分 1.30 × (累计 7 + 本旋炸弹 2))
|
||||
- `spinWin = 14.55` 是**本局累计到本步的赢额**(触发盘 3.00 + 前 5 旋 10.25 + 本旋 1.30)
|
||||
- `dropData` 里 `{"from":2,"to":8,"symbol":"M"}`:**炸弹也随重力下落**(第 2 格 → 第 8 格),本旋末帧统一结算后才并入累计器(7 → 9)
|
||||
- `globalStep/globalTotalSteps` 由服务端按整局排帧给出
|
||||
|
||||
> 已删除的冗余字段:`accumulatedSymbols`、`spinWinners`、`removedPositions`、`baseScore` —— 替代算法见字段表下方的说明。
|
||||
|
||||
---
|
||||
|
||||
**下面用同一局(买FS bet=1、15 旋、整局实付 66.00 倍底注、共 27 步,回合 `round 93485`)看三个典型阶段**(真实数据):
|
||||
|
||||
##### ① 起始:本局第 1 步(触发盘,4 个 S 白送 3 倍)
|
||||
|
||||
```json
|
||||
{
|
||||
"state": "PLAYING",
|
||||
"spinNum": 0,
|
||||
"tumbleNum": 1,
|
||||
"globalStep": 1,
|
||||
"globalTotalSteps": 27,
|
||||
"gridState": [
|
||||
["B","G","S","C","G","I"],
|
||||
["I","E","H","E","G","S"],
|
||||
["D","B","G","I","S","C"],
|
||||
["G","H","E","B","F","S"],
|
||||
["B","C","E","A","C","G"]
|
||||
],
|
||||
"multiplierDetail": [],
|
||||
"symbolLinks": [],
|
||||
"dropData": [],
|
||||
"fillData": [],
|
||||
"scatterTrigger": {"scatterCount": 4, "freeSpins": 15, "scatterWin": 3.00},
|
||||
"winAmount": 3.00,
|
||||
"spinWin": 3.00
|
||||
}
|
||||
```
|
||||
- `spinNum: 0` = 触发盘(买FS 必出 4 个 S,这一盘没有任何符号连线和炸弹)
|
||||
- `winAmount = spinWin = 3.00`:触发盘这一旋的总额,也是本局累计的起点
|
||||
- `state` 仍是 `PLAYING` —— **本局还有 26 步**;`scatterTrigger` 出现在**本旋的末帧**,不是本局的末帧
|
||||
|
||||
##### ② 中间:第 4 旋 · 第 2 波(这一旋连消多波,屏上还留着一颗 2 倍炸弹)
|
||||
|
||||
```json
|
||||
{
|
||||
"state": "PLAYING",
|
||||
"spinNum": 4,
|
||||
"tumbleNum": 2,
|
||||
"globalStep": 10,
|
||||
"globalTotalSteps": 27,
|
||||
"gridState": [
|
||||
["G","A","G","G","G","G"],
|
||||
["I","E","G","I","C","G"],
|
||||
["D","G","D","G","I","G"],
|
||||
["I","E","I","C","M","E"],
|
||||
["D","I","E","C","B","E"]
|
||||
],
|
||||
"multiplierDetail": [{"pos": 23, "val": 2, "quality": "GREEN"}],
|
||||
"symbolLinks": [
|
||||
{"symbol": "G", "count": 14, "loc": [2,6,8,9,12,13,14,15,16,17,18,22,27,28], "base_score": 5.00, "tumble": 1},
|
||||
{"symbol": "G", "count": 10, "loc": [1,3,4,5,6,9,12,14,16,18], "base_score": 1.00, "tumble": 2}
|
||||
],
|
||||
"dropData": [
|
||||
{"from": 7, "to": 13, "symbol": "D"},
|
||||
{"from": 1, "to": 7, "symbol": "I"},
|
||||
{"from": 21, "to": 27, "symbol": "E"},
|
||||
{"from": 3, "to": 21, "symbol": "I"},
|
||||
{"from": 10, "to": 28, "symbol": "C"},
|
||||
{"from": 4, "to": 22, "symbol": "C"},
|
||||
{"from": 11, "to": 17, "symbol": "I"},
|
||||
{"from": 5, "to": 11, "symbol": "C"}
|
||||
],
|
||||
"fillData": [
|
||||
{"pos": 1, "symbol": "G"}, {"pos": 2, "symbol": "A"}, {"pos": 3, "symbol": "G"},
|
||||
{"pos": 4, "symbol": "G"}, {"pos": 5, "symbol": "G"}, {"pos": 6, "symbol": "G"},
|
||||
{"pos": 8, "symbol": "E"}, {"pos": 9, "symbol": "G"}, {"pos": 10, "symbol": "I"},
|
||||
{"pos": 12, "symbol": "G"}, {"pos": 14, "symbol": "G"}, {"pos": 15, "symbol": "D"},
|
||||
{"pos": 16, "symbol": "G"}, {"pos": 18, "symbol": "G"}
|
||||
],
|
||||
"winAmount": 6.00,
|
||||
"spinWin": 19.25
|
||||
}
|
||||
```
|
||||
- 本步新增的连线只有 `tumble === 2` 那条(G 10 连);`tumble: 1` 那条是这一旋第 1 波的(G 14 连,5.00),继续列在这里供"获胜者面板"累加
|
||||
- `dropData` 里那条 `{"from":...,"to":...,"symbol":"M"}` 会体现炸弹随重力下落,末帧才结算倍数(本例炸弹在 `gridState` 第 23 格)
|
||||
- `winAmount = 6.00` 是**第 4 旋累计**(5.00 + 1.00),`spinWin = 19.25` 是本局累计;这一旋末帧会跳到 `12.00`(基础分 6.00 × (累计 0 + 本旋炸弹 2))
|
||||
|
||||
##### ③ 最后一旋:本局第 27 步(`state = FINISH`,可结算展示)
|
||||
|
||||
```json
|
||||
{
|
||||
"state": "FINISH",
|
||||
"spinNum": 15,
|
||||
"tumbleNum": 1,
|
||||
"globalStep": 27,
|
||||
"globalTotalSteps": 27,
|
||||
"gridState": [
|
||||
["M","C","D","E","D","A"],
|
||||
["F","C","D","C","C","G"],
|
||||
["E","C","I","E","F","B"],
|
||||
["F","F","G","H","C","I"],
|
||||
["H","F","A","C","I","A"]
|
||||
],
|
||||
"multiplierDetail": [{"pos": 1, "val": 2, "quality": "GREEN"}],
|
||||
"symbolLinks": [],
|
||||
"dropData": [],
|
||||
"fillData": [],
|
||||
"winAmount": 0.00,
|
||||
"spinWin": 66.00,
|
||||
"totalWin": 66.00,
|
||||
"totalMul": 66.00,
|
||||
"prizeInfo": {
|
||||
"id": 4,
|
||||
"name": "M4A4 | 皇家圣骑士",
|
||||
"imageUrl": "/profile/upload/2026/07/02/TTRBNCB8IFJveWFsIFBhbGFkaW4gKEZhY3RvcnkgTmV3KQ==_20260702034203A004.png",
|
||||
"grade": 4,
|
||||
"exteriorName": "破损不堪",
|
||||
"isFallback": false
|
||||
},
|
||||
"boxRecordId": 556677
|
||||
}
|
||||
```
|
||||
- 第 15 旋**没有消成**:`symbolLinks` 空、`winAmount = 0`(屏上那颗 2 倍炸弹随本旋作废,累计器不变)
|
||||
- `spinWin = 66.00` = **整局实付**,与 `totalWin`/`totalMul` 一致(各旋末帧 `winAmount` 相加也等于它)
|
||||
- `prizeInfo` = 本局命中的饰品(按整局赢额落区间查表,见 3.4);`boxRecordId` = 服务端开局时就写入背包的记录 ID
|
||||
- 1000 倍封顶时才会多两个字段:`maxWinReached: true` + `spinsRemaining: 0`;本局没触顶,所以都不返回(`spinsRemaining` 平时**每帧都在**,封顶时被强制成 0)
|
||||
- 本局第 12 旋单旋赢了 22.75 倍(≥ 买 FS 的"小"阈值 20),那一旋的末帧会带 `bigWinLevel: "small"`;最后这一旋没消成(`winAmount = 0`),所以本步没有该字段 —— 该字段按**单旋**赢额判档、只在**免费旋转**里给,达标的那一旋末帧才出现
|
||||
|
||||
##### 字段说明
|
||||
|
||||
| 字段 | 类型 | 出现 | 说明 |
|
||||
|------|------|------|------|
|
||||
| state | String | 每步 | `PLAYING`(本局还有后续步骤)/ `FINISH`(**本局最后一步**,可结算展示)。注意:**本旋的末帧不一定是 FINISH**(只有整局最后一步才是),本旋是否结束看下一步 `spinNum` 是否变化 |
|
||||
| spinNum | Integer | 每步 | 免费旋转序号,**0 = 基础旋转** |
|
||||
| tumbleNum | Integer | 每步 | 本旋内的翻滚/消除序号 |
|
||||
| globalStep | Integer | 每步 | 全局第几步(从 1 开始) |
|
||||
| globalTotalSteps | Integer | 每步 | 本局总步数 |
|
||||
| spinsRemaining | Integer | 每步 | **剩余免费旋转次数**(已发放 − 已播完旋数):已发放 = 触发帧 `scatterTrigger.freeSpins`(15) + 已播步里的 `retriggerInfo.extraSpins`(5),封顶 30。**前端直接显示这个值倒计即可,不用自己加减**(口径见下方「剩余免费旋转」)。例:第 2 旋消完出 3 个 S → 15−2+5 = **18**。基础旋/无 FS 的局恒 0;1000x 封顶时强制 0 |
|
||||
| gridState | Array\[5\]\[6\] | 每步 | 棋盘短码矩阵(5 行 × 6 列,含 `S`/`M`);**每一步都是完整的 30 格,不会有空格/null**,是渲染的权威数据(约定见本节末尾) |
|
||||
| multiplierDetail | Array | 每步 | 炸弹明细 `[{"pos":1起,"val":乘数值,"quality":"GREEN/…"}]`,**未 × bet**(quality 见 3.2) |
|
||||
| symbolLinks | Array | 每步 | **本旋累积**的成奖连线 `[{"symbol","count","loc":[1起格位],"base_score","tumble"}]`;`base_score` 已 × bet,`tumble` = 这条连线属于本旋内第几波消除(= 本步 `tumbleNum`)。**本波新增的连线 = `tumble` 等于本步 `tumbleNum` 的那些**;获胜者面板按 `symbol` 汇总即可(原来单独的 `spinWinners` 字段已删) |
|
||||
| dropData | Array | 每步 | 掉落数据 `[{"from":1起,"to":1起,"symbol"}]`:只列**位置变了**的符号,`symbol` 是它落到目的地后的符号;新旋第一波为空数组 |
|
||||
| fillData | Array | 每步 | 补位数据 `[{"pos":1起,"symbol"}]`,**只含普通符号**(不含炸弹/SCATTER);正好盖住下落之后空出来的格子,新旋第一波为空数组 |
|
||||
| scatterTrigger | Object | 每步 | 基础盘触发免费旋转时返回 `{"scatterCount":4,"freeSpins":15,"scatterWin":3.00}`:`freeSpins` 恒 15 = **赠送量**(不是剩余,剩余看 `spinsRemaining`);`scatterWin` = 本盘 SCATTER 的**现金赔付**(已 × bet,4/5/6 个 S 分别赔 3x/5x/100x,已计入本旋 `winAmount`/`spinWin`),无则 null |
|
||||
| winAmount | Decimal | 每步 | **本旋累计到本步的赢额**(已 × bet):该旋**末帧(`isFinal=1`)的值就是这一旋的总额**(含炸弹倍数与 SCATTER),中间步是这一旋的累计进度;**不是本步增量**(本步增量 = 与上一步相减) |
|
||||
| spinWin | Decimal | 每步 | **本局累计到本步的赢额**(已 × bet):前端"总赢额跳字"直接用这个;**最后一步 = 整局实付 = `totalWin`** |
|
||||
| accumulator | Decimal | 每步 | **本步后的炸弹累计倍数**(未 × bet):免费旋转内跨旋传递的累计器(基础旋恒为 0);**断线重连时用 `lastStep.accumulator` 接回累计值**,其余时候前端可按 `multiplierDetail` 自行累计 |
|
||||
| retriggerInfo | Object | 每步 | 免费旋转内重触发 `{"scatterCount":3,"extraSpins":5,"scatterWin":0.00}`:`extraSpins` 恒 5 = **追加旋数**(30 旋封顶后仍会报 5 但实际不加,所以别拿它自己加减,直接用 `spinsRemaining`);`scatterWin` = 本旋 SCATTER 的**现金赔付**(已 × bet,4/5/6 个 S 分别赔 3x/5x/100x,**3 个 S 赔 0**,已计入本旋 `winAmount`/`spinWin`),无则 null |
|
||||
| bigWinLevel | String | 每步 | 爆奖档位(小/中/大):`small` / `middle` / `big`,无则 null。**只有免费旋转的旋末帧才可能有值**(normal/ante 的自然 FS、buy_fs 的每一旋),基础旋(`spinNum=0`)恒为 null;判档用该旋末帧的 `winAmount`,阈值见本节末尾「爆奖档位 `bigWinLevel` 阈值」 |
|
||||
| **totalWin** | Decimal | 仅 FINISH | 本局总赢额 |
|
||||
| **totalMul** | Decimal | 仅 FINISH | 本局总倍率 |
|
||||
| **maxWinReached** | Boolean | 仅 FINISH | 是否已达 1000x 封顶 |
|
||||
| **prizeInfo** | Object | 仅 FINISH | 命中饰品详情(见下) |
|
||||
| **boxRecordId** | Long | 仅 FINISH | 背包记录 ID,用于末帧「放入背包」或「立即分解」(分解时作为 `packSackIds` 传入) |
|
||||
|
||||
`prizeInfo` 结构:
|
||||
|
||||
| 字段 | 类型 | 说明 |
|
||||
|------|------|------|
|
||||
| id | Long | 饰品(奖品)ID |
|
||||
| name | String | 饰品名称 |
|
||||
| imageUrl | String | 饰品图片(后台「奖品管理」的"饰品图片") |
|
||||
| levelImageUrl | String | **饰品等级/品质图片**(后台「奖品管理」的"品质图片",取 `tt_ornaments_level.level_img`,按饰品 `grade` 关联);前端结果弹窗的品质图标用它(可空) |
|
||||
| grade | Integer | 等级(= `tt_ornaments_level.id`) |
|
||||
| exteriorName | String | 外观名(可空) |
|
||||
| isFallback | Boolean | 是否命中兜底饰品(无精确区间时的 fallback) |
|
||||
|
||||
> 说明:`accumulator`(本步后炸弹累计倍数,未 × bet)为**跨旋传递的累计器**,基础旋恒为 `0`。
|
||||
> 前端按 `multiplierDetail` 自行累计即可;**唯一必须依赖它的是断线重连**——`hasActive` 只回最后一步,
|
||||
> 前几旋攒下的累计倍数不在 `lastStep.multiplierDetail` 里,必须用 `lastStep.accumulator` 把累计值接回来,
|
||||
> 否则重连后显示的累计倍数会从这次重连的那一旋重新起算(少掉之前累计的倍数)。
|
||||
>
|
||||
> **剩余免费旋转 `spinsRemaining`(每帧都返回,前端直接显示,不用自己算)**:
|
||||
> 进 FS 后从 **15** 起倒计,**每播完一旋 −1**(同一旋的中间波帧不变,到该旋**末帧**才减);
|
||||
> **那一旋出 3+ 个 S 时在同一帧 +5**(`retriggerInfo.extraSpins`;出 3/4/5/6 个 S 都是 +5,多出来的只是现金 `scatterWin` 3x/5x/100x,3 个 S 为 0)。
|
||||
> 所以"出 3 个 S"那一帧是 **−1 和 +5 一起结算 = 净 +4**:
|
||||
>
|
||||
> | 时刻 | 已发放 | 已播完 | `spinsRemaining` |
|
||||
> |---|---|---|---|
|
||||
> | 触发帧播完(进 FS) | 15 | 0 | **15** |
|
||||
> | 第 1 旋播完 | 15 | 1 | **14** |
|
||||
> | 第 2 旋·第 1/2 波(本旋还没播完) | 15 | 1 | 14(不变) |
|
||||
> | **第 2 旋末帧,这旋出 3 个 S** | 15+5 = 20 | 2 | **18**(= 14−1+5) |
|
||||
> | 第 3 旋播完 | 20 | 3 | **17** |
|
||||
> | 第 3 旋又出 4 个 S | 20+5 = 25 | 3 | **22** |
|
||||
>
|
||||
> 旋数上限 **30**(15 起、每次重触发 +5、最多加 3 次)—— 生成端已加闸门:**到 30 旋后不再落 3+ 个 S**,
|
||||
> 所以一局最多 3 次重触发(旧池子里还残留 650 个"报了 +5 但没有旋可加"的旧帧,重跑池子后会消失;
|
||||
> 后端的 `spinsRemaining` 按旋数夹 30,碰上旧帧也不会算错)。
|
||||
> 整局 1000x 封顶时强制 `spinsRemaining = 0`;无 FS 的局(基础旋空转)恒 0。
|
||||
>
|
||||
> **金额与面板字段怎么用**(都在 `steps[]` 的每一步上,金额已 × bet):
|
||||
>
|
||||
> | 想看什么 | 用哪个 |
|
||||
> |---|---|
|
||||
> | 这一步加了多少(本步增量) | `winAmount` 与上一步相减(第一步直接取 `winAmount`) |
|
||||
> | **这一旋一共赢了多少** | 该旋**末帧**的 `winAmount`(不用自己求和) |
|
||||
> | **本局一共赢了/正在累计多少** | `spinWin`(滚动跳字);最后一步的值 = `totalWin` |
|
||||
> | 本步新消出来的连线(画连线动画) | `symbolLinks` 里 `tumble` == 本步 `tumbleNum` 的那些 |
|
||||
> | 获胜者面板(本旋逐波的 符号×数量×赢额) | `symbolLinks` 按 `symbol` 汇总(`count` 累加、`base_score` 累加)即可,**已无单独的 `spinWinners` 字段** |
|
||||
> | 爆奖动画(small/middle/big 小/中/大) | `bigWinLevel`(后端按**单旋赢额**判好档,直接透传,无则 null;只在免费旋转里给) |
|
||||
> | 炸弹与倍数累计显示 | `multiplierDetail`(每屏炸弹 `pos/val/quality`),倍数按旋自行累加;断线重连后用 `lastStep.accumulator` 接回累计值 |
|
||||
>
|
||||
> 一致性:各旋末帧 `winAmount` 相加 = 整局实付 = 最后一步 `spinWin` = `totalWin`。
|
||||
> 已删除的字段(都是可从别的字段推导的冗余项):`baseScore`(= 本步 `symbolLinks[].base_score` 之和)、
|
||||
> `removedPositions`(= 上一步 `symbolLinks[].loc` 的并集)、`accumulatedSymbols`(= 本旋 `symbolLinks` 按 symbol 汇总)、
|
||||
> `spinWinners`(= 本旋 `symbolLinks` 按 symbol 汇总)。
|
||||
|
||||
> **盘面与动画数据的约定**(每一步都返回完整盘面,前端不用自己推算):
|
||||
>
|
||||
> 1. `gridState` 永远是**权威盘面**:5 行 × 6 列 = 30 格,格子里只有 `A`~`I` / `S` / `M`,**不会出现空格或 null**(全库 355,902 步已逐一校验:空白格 0、非法符号 0、复刻结果与实际盘面 100% 一致)。
|
||||
> 2. 同一旋内,第 N+1 波可以**精确**复刻:`上一波盘面` −(上一波 `symbolLinks` 里 `tumble == 上一波 tumbleNum` 的格子)→ 按 `dropData` 下落 → 按 `fillData` 补位 = `gridState`(`dropData` 与重力、`fillData` 覆盖的格子也逐条校验过)。
|
||||
> 3. `dropData[].symbol` / `fillData[].symbol` 一律是**这一波结束后**该位置的符号(与 `gridState` 同位置一致),所以按第 2 条落地后不会出现错符号。
|
||||
> 4. `dropData` 只列**位置发生变化**的符号(原地不动的格子不在里面,前端保留原样即可);`fillData` 正好覆盖"下落之后空出来的格子",不会漏格。
|
||||
> 5. **新旋的第一波**(`tumbleNum = 1`,`spinNum` 递增或进入本局第 1 步)`dropData` / `fillData` 都是空数组 —— 这一波是**整盘重发**,前端直接整盘替换/重新发牌,不要按下落补位处理。
|
||||
> 6. 免费旋转的"保底消除"让 8 个**刚落下/刚补位**的格子凑成一组消除:这些格子的新符号就在 `dropData` / `fillData` 里(见第 3 条),所以整条动画链能完整复刻。前端每一步结束都以 `gridState` 为准渲染,永远不会出现空格。
|
||||
|
||||
> **免费旋转的累计倍数**(生成端规则,前端显示不受影响):
|
||||
> 免费旋转内,**每一旋都可能带炸弹**(每屏最多 1 颗;低倍档局只出 2~8 的小球、高倍档局可出到 1000),
|
||||
> 有消除的旋会把本旋炸弹并入累计器,**本旋赢额 = 基础分 × max(1, 累计值)**,之后累计值 += 本旋炸弹和
|
||||
> —— 也就是"滚雪球":上一旋攒的倍数会作用到后面每一次消除。
|
||||
> 因为累计倍数会放大后续每一次消除,**低倍档局(10x~25x,占买FS 抽中率 76%)大约只能演 5 次左右的
|
||||
> "消除+爆"**(单个球越小、能买的次数越多);要 10 次以上的连爆,那一局本身就得是 40 倍以上的档位。
|
||||
> 表现上:15 旋里约 5 旋有消除、约 4 次炸弹并入、局末累计倍数常见 15~25(大奖局 100+)。
|
||||
>
|
||||
> **normal/ante 的自然免费旋转**:基础旋(`spinNum=0`)不出炸弹,摇出 4~6 个 SCATTER 才进 FS,
|
||||
> 进 FS 后走的是**和买 FS 完全相同**的规则(同样的炸弹密度、同样的累计器、同样的每旋保底消除),
|
||||
> 这样一局实测约 80~120 倍底注、5 旋左右有消除。因为 15 旋带炸弹的整局必然是这个量级,
|
||||
> **这两个模式的 100x / 150x / 200x 饰品桶就是靠"自然触发 FS"的回合来装的** ——
|
||||
> 前端不需要区分,`spinNum` 从 0 变成 1 就是进了 FS。
|
||||
|
||||
**爆奖档位 `bigWinLevel` 阈值**(小/中/大三档,按**单旋**赢额 ÷ 基础下注判档,达标即在该旋末帧返回、同档一局可多次;**只判免费旋转**,基础旋恒为 null):
|
||||
|
||||
| 情况 | 模式 | `small`(小) | `middle`(中) | `big`(大) |
|
||||
|------|------|------|------|------|
|
||||
| ① 自然免费旋转 | `normal` / `ante` | ≥5 | ≥25 | ≥100 |
|
||||
| ② 购买免费旋转 | `buy_fs` | ≥20 | ≥60 | ≥200 |
|
||||
|
||||
- 判档取值:该旋末帧的 `winAmount`(单位 = 基础下注倍数,与前端看到的一致)。
|
||||
- 前端逻辑:**只在 `bigWinLevel` 非空时播对应动画**,播完继续下一步即可;一个 FS 局里三档可以按旋依次出现。
|
||||
- 为什么两套阈值:normal/ante 的自然 FS 整局约 80~130 倍(预算/旋数完全一致,共用一套);`buy_fs` 花 50 倍买来的赢额分散在 15+ 旋里,单旋阈值整体下调。
|
||||
- 实测出现频率(当前池子 30 箱逐局统计):① 自然 FS(normal 519 局 / ante 818 局)—— "小" 91.7% / 93.5% 的局会出现(每局 2.3~2.5 次)、"中" 49.3% / 48.8%(0.6 次)、"大" 31.4% / 39.0%(0.3~0.4 次);② 买 FS(12,205 局)—— "小" 19.5% 的局出现(0.47 次)、"中" 6.3%(0.13 次)、"大" **1.5%**(0.02 次)。
|
||||
|
||||
##### 错误码 / 业务提示
|
||||
|
||||
| code | msg | 触发场景 | 前端处理 |
|
||||
|------|-----|---------|---------|
|
||||
| 702 | 账户余额不足 | 余额 < 单次扣费 | 提示并引导充值 |
|
||||
| 703 | 玩家未充值不能游玩任何游戏 | 从未充值 | 提示先充值 |
|
||||
| 500 | 网站维护中...... | 全站维护开启 | 提示维护 |
|
||||
| 500 | 玫瑰女郎维护中...... | 玫瑰女郎维护开启 | 提示维护 |
|
||||
| 500 | 请求频繁,请稍后再试 | 短时重复请求(Redis 锁) | 稍后重试 |
|
||||
| 500 | 箱子不可用 | boxId 不存在或下线 | 刷新箱子列表 |
|
||||
| 500 | 该箱子暂无可用回合 | 该身份该箱无可用回合 | 提示稍后再试 |
|
||||
| 500 | 游戏已结束 | 局已播完仍继续调用 | 忽略/返回首页 |
|
||||
| 500 | boxId 不能为空 | 无进行中对局,且 body 未带 `boxId` | 回到选箱页重新开局 |
|
||||
|
||||
> **本接口不会返回 701**(实名认证由支付等其它模块返回,游戏链路不做实名校验)。
|
||||
|
||||
> **维护只拦"新开一局"**:当 `websiteMaintenance`/`olympusMaintenance` 开启时,只要**携带非空 `boxId`(开新局)**就会返回维护提示;**不带 `boxId`(继续当前对局)不受维护影响**,可正常播完。进行中的对局不会被强制结算。
|
||||
|
||||
---
|
||||
|
||||
### 3. 查询游戏记录(我的 / 全服,均分页)
|
||||
|
||||
**`POST /api/olympus/records`**
|
||||
|
||||
一个接口通过 `scope` 区分「我的记录」与「全服记录」,**两者均支持分页**。
|
||||
|
||||
- `scope=1`(默认):**我的记录**,只返回当前登录玩家已结束(播完)的记录。
|
||||
- `scope=0`:**全服记录**,返回全站玩家已结束(播完)的记录(倒序)。
|
||||
- **白号记录在两种 scope 下都会出现(可上榜)**;全服口径仅排除机器人(user_type=03)、封禁(status=1)与已删除(del_flag!=0)用户。
|
||||
|
||||
#### 请求参数
|
||||
|
||||
```json
|
||||
{
|
||||
"scope": 0,
|
||||
"pageNum": 1,
|
||||
"pageSize": 10
|
||||
}
|
||||
```
|
||||
|
||||
| 参数 | 类型 | 必填 | 说明 |
|
||||
|------|------|------|------|
|
||||
| scope | Integer | 否 | `1`=我的记录(默认)/ `0`=全服记录 |
|
||||
| pageNum | Integer | 否 | 页码,默认 `1` |
|
||||
| pageSize | Integer | 否 | 每页条数,默认 `10` |
|
||||
|
||||
> 请求体为 **`RecordPageRequest` 实体类**(POST + JSON),三个字段均可选;body 也可不传(或传空对象 `{}`),此时等价于 `scope=1, pageNum=1, pageSize=10`(我的记录)。
|
||||
|
||||
#### 响应 data 结构
|
||||
|
||||
```json
|
||||
{
|
||||
"rows": [
|
||||
{
|
||||
"nickName": "玩家昵称",
|
||||
"avatar": "https://.../avatar.png",
|
||||
"cost": 10.00,
|
||||
"settledWin": 88.00,
|
||||
"prizeImageUrl": "https://.../rose.png",
|
||||
"prizeName": "黄金玫瑰 (久经沙场)",
|
||||
"createTime": "2024-01-01 12:00:00"
|
||||
}
|
||||
],
|
||||
"total": 57
|
||||
}
|
||||
```
|
||||
|
||||
| 字段 | 类型 | 说明 |
|
||||
|------|------|------|
|
||||
| nickName | String | 玩家昵称(我的记录=自己,全服记录=中奖玩家) |
|
||||
| avatar | String | 玩家头像 |
|
||||
| cost | Decimal | 用户此次消费价格(本局扣费) |
|
||||
| settledWin | Decimal | 此次获得饰品的价格(本局结算金额) |
|
||||
| prizeImageUrl | String | 饰品图片 |
|
||||
| prizeName | String | 饰品全名:`名称 (磨损品质)`,无品质时仅名称(如 `黄金玫瑰 (久经沙场)`) |
|
||||
| createTime | String | 时间(yyyy-MM-dd HH:mm:ss,东八区) |
|
||||
|
||||
> 列表项**只返回上述 7 个字段**(我的记录/全服记录同一结构):`userId`/`boxId`/`roundId`/`settled`/`modeType`/`rewardType`/`prizeId`/`prizeLevelImg`/`settleSource`/`boxName`/`basicBet` 等字段已不再返回,客户端无需判空。
|
||||
>
|
||||
> **只返回已结束的局**:开局即结算(`settled=1`)但客户端还在播动画的那一局不出现在列表里,播完末帧后(服务端把该记录 `settle_source` 由 `start` 改为 `finish`)才进入记录;中途退出/断线未播完的局,按单局缓存 TTL **30 分钟**兜底,超时后照常展示,战绩不会丢。
|
||||
>
|
||||
> `total` 为**该 scope 下的真实总数**(可用于分页计算)。
|
||||
>
|
||||
> 早期版本曾有 `autoDismantle` 字段,现已彻底移除(自动分解由前端开关 + 前端调分解接口,后端不记录、不返回);库里 `game_olympus_record.auto_dismantle` 列作为历史列保留,无代码读写。
|
||||
|
||||
---
|
||||
|
||||
### 4. 查询进行中的游戏(断线重连)
|
||||
|
||||
**`POST /api/olympus/hasActive`**
|
||||
|
||||
用户刷新页面/重连时调用,判断是否有未播完的局,若有则返回当前进度与最后一步,供前端恢复动画。**维护期间不强制结算**,进行中的对局可正常重连并播完。
|
||||
|
||||
#### 请求参数
|
||||
|
||||
无,body 可不传(或传空对象 `{}`)。
|
||||
|
||||
#### 响应 data 结构
|
||||
|
||||
**无进行中游戏:**
|
||||
```json
|
||||
{ "hasActive": false }
|
||||
```
|
||||
|
||||
**有进行中游戏:**
|
||||
```json
|
||||
{
|
||||
"hasActive": true,
|
||||
"boxId": 1001,
|
||||
"boxType": "normal",
|
||||
"bet": 10.00,
|
||||
"cost": 10.00,
|
||||
"stepIndex": 3,
|
||||
"totalSteps": 8,
|
||||
"freeSpinCount": 0,
|
||||
"totalWin": 25.00,
|
||||
"roundId": 1234567,
|
||||
"lastStep": { "...": "与 dospin 的 PLAYING 响应同构,为最后已播的一步" }
|
||||
}
|
||||
```
|
||||
|
||||
| 字段 | 类型 | 说明 |
|
||||
|------|------|------|
|
||||
| hasActive | Boolean | 是否有进行中局 |
|
||||
| boxId | Long | 箱子 ID |
|
||||
| boxType | String | 模式 |
|
||||
| bet | Decimal | 基础下注 |
|
||||
| cost | Decimal | 本局扣费(已扣) |
|
||||
| stepIndex | Integer | 当前已播步数 |
|
||||
| totalSteps | Integer | 总步数 |
|
||||
| freeSpinCount | Integer | 本局免费旋转**总**次数(15 + 重触发×5,封顶 30;1000x 截断局是实际执行数)。**不是剩余** |
|
||||
| spinsRemaining | Integer | **剩余免费旋转次数**(倒计值,= 最后已播一步的 `lastStep.spinsRemaining`,口径见「2. 执行旋转」的字段说明),无 FS 的局为 0 |
|
||||
| totalWin | Decimal | 已播步骤累计赢额 |
|
||||
| roundId | Long | 回合 ID |
|
||||
| lastStep | Object | 最后已播一步的完整动画数据(PLAYING 同构);**仅当 `stepIndex ≥ 1` 时返回**,`stepIndex=0` 时该字段不出现。`lastStep.accumulator` = 这步之后的炸弹累计倍数,重连时用它接回累计 |
|
||||
|
||||
> 前端恢复策略:先播 `lastStep` 的最后一帧,然后**把本地炸弹累计倍数重置为 `lastStep.accumulator`**(字段不出现时按 0),再继续调用 `dospin`(不传 boxId)走完剩余步骤,直至 FINISH。少了这一步重置,重连后累计倍数会从重连那一旋重新起算。
|
||||
|
||||
---
|
||||
|
||||
### 5. 背包分解(关联接口)
|
||||
|
||||
**`POST /api/userPackSack/decompose`**
|
||||
|
||||
玫瑰女郎的命中饰品默认以 `ornament` 形式写入背包(`tt_box_records.source = OLYMPUS_BOX(13)`)。末帧玩家点「立即分解」——或前端"自动分解"开关开启时代替玩家自动调用——都调这个通用背包接口把饰品兑换成金币/积分;接口没调到也只是饰品留在背包,无资金影响。
|
||||
|
||||
#### 请求参数
|
||||
|
||||
```json
|
||||
{
|
||||
"packSackIds": [556677],
|
||||
"isAll": false,
|
||||
"decomposeType": "game"
|
||||
}
|
||||
```
|
||||
|
||||
| 参数 | 类型 | 必填 | 说明 |
|
||||
|------|------|------|------|
|
||||
| packSackIds | Array<Long> | 否 | 待分解的背包记录 ID 列表(单个即 `[boxRecordId]`) |
|
||||
| isAll | Boolean | 是 | 是否全部分解(`false`=按 ids) |
|
||||
| decomposeType | String | 否 | `backpack`=背包分解(默认)/ `game`=游戏结果页立即分解。**后端当前不读取该字段**(分解结算一律按饰品来源判定),传与不传结果相同,保留仅为兼容前端调用 |
|
||||
|
||||
#### 响应
|
||||
|
||||
```json
|
||||
{
|
||||
"code": 200,
|
||||
"msg": "成功分解1件饰品",
|
||||
"data": null
|
||||
}
|
||||
```
|
||||
|
||||
> **分解流水口径**:分解结算按**饰品来源**自动分组记流水(`tt_box_records.source` → 对应游戏奖励流水),与前端传来的 `decomposeType` 无关(该字段后端不读取,传与不传结果相同)。玫瑰女郎饰品来源 `OLYMPUS_BOX(13)` → **`OLYMPUS_REWARD(61) 玫瑰女郎奖励`**:无论玩家「放入背包后再分解」还是末帧「立即分解」(含前端自动分解),入账流水一律是玫瑰女郎奖励。
|
||||
|
||||
---
|
||||
|
||||
## 六、关键流程(前端友好版)
|
||||
|
||||
### 6.1 进入游戏页
|
||||
|
||||
1. `POST /api/olympus/boxes` → 拿到箱子列表,展示箱子和价格。
|
||||
2. 玩家选箱子 → 记录 `boxId`。
|
||||
|
||||
### 6.2 开始新游戏
|
||||
|
||||
1. `POST /api/olympus/dospin`,body `{"boxId": X}`。
|
||||
2. 返回第一步(`PLAYING` 或直接 `FINISH`,若该局只有一步)。
|
||||
|
||||
### 6.3 逐步揭晓动画
|
||||
|
||||
- 对每步 `PLAYING` 响应,播放对应动画(`gridState`/`symbolLinks` 里 `tumble === tumbleNum` 的新增连线/`dropData`/`fillData`/`multiplierDetail`)。
|
||||
`winAmount` 是该旋累计(末帧即该旋总额),`spinWin` 是本局累计,可直接用于跳字。
|
||||
- 再调用 `POST /api/olympus/dospin`,body `{}`(不带 boxId),得到下一步。
|
||||
- 重复直止 `state === "FINISH"`。
|
||||
|
||||
### 6.4 末帧结算展示
|
||||
|
||||
1. 读到 `totalWin/totalMul/prizeInfo/boxRecordId`。
|
||||
2. **饰品已写入背包**,无需等待。
|
||||
3. 展示中奖弹窗,给玩家两个选择:
|
||||
- **放入背包**:什么都不用调,饰品已在背包(`boxRecordId`)。
|
||||
- **立即分解**:调 `POST /api/userPackSack/decompose`,`packSackIds:[boxRecordId], isAll:false`,入账流水为玫瑰女郎奖励(61)。
|
||||
4. 前端"自动分解"开关为开时:末帧直接按第 3 步的「立即分解」调用,等价于替玩家点了一下,**后端无需任何额外字段或开关**。
|
||||
5. 这次调用失败(断网、超时、玩家直接关页面)都不影响对局结果:饰品本来就在背包里,等于玩家选了「放入背包」,之后在背包页随时可再分解。重试也不会重复加钱(同一饰品只会被结算一次)。
|
||||
6. **结算站内信在"这一局播完"时才发**(多步局=末帧 `FINISH`,单步局=开局):开局就发会在动画播完前把结果剧透给玩家。因此中途退出/断线再没回来的局不发站内信,饰品仍在背包、战绩仍按 30 分钟兜底展示。
|
||||
|
||||
### 6.5 重连 / 刷新
|
||||
|
||||
- 调 `POST /api/olympus/hasActive`。
|
||||
- `hasActive=false` → 正常回首页/列表。
|
||||
- `hasActive=true` → 先播 `lastStep`,再继续 `dospin`(不带 boxId)走完剩余步骤。
|
||||
|
||||
### 6.6 维护行为
|
||||
|
||||
- **开启维护(`websiteMaintenance`/`olympusMaintenance`)= 只拦"新开一局"**。
|
||||
- 玩家携带非空 `boxId` 调 `dospin`(开新局)→ 返回维护提示(`网站维护中......` / `玫瑰女郎维护中......`)。
|
||||
- **进行中的对局不受影响**:不带 `boxId` 调 `dospin`(继续)可正常播完;`hasActive` 也能正常恢复动画,不会被强制结算。
|
||||
|
||||
---
|
||||
|
||||
## 七、数据口径补充
|
||||
|
||||
- **扣费来源流水**:开局扣费写入账户流水 `source = 60`(`TtAccountRecordSource.OLYMPUS_SPIN`,玫瑰女郎消费)。
|
||||
- **奖励流水**:`source = 61`(`TtAccountRecordSource.OLYMPUS_REWARD`,玫瑰女郎奖励)。**只在玩家分解饰品时入账**:无论「放入背包后再分解」还是末帧「立即分解」(含前端自动分解),口径完全一致;开局本身不发奖励流水。不会重复入账——同一饰品记录只结算一次(`status IN (0,2)` 状态机 + `user:decompose:lock` 锁)。
|
||||
- **背包记录来源**:命中饰品写入 `tt_box_records`,`source = OLYMPUS_BOX(13)`,`box_name = "OLYMPUS"`,`status=0`(我的库存)。
|
||||
- **身份维度(audience)**:`common` 普通 / `anchor` 主播(user_type=01) / `robot` 机器人(user_type=03) / `white` 白号(is_blank_account=1)。白号优先级最高,压过 user_type。
|
||||
- **注意**:白号一般为主播测试/展示用,其玩局不注水、不扣水、不受水位约束,前端无需特殊处理。
|
||||
- **1000x 封顶**:单局总倍率 ≥ 1000 即 `maxWinReached=true`,`spinsRemaining=0`;到账同样按 1000 倍截断。
|
||||
- **游戏参数(速查)**:棋盘 5×6=30 格;同符号 ≥8 消除(8-9/10-11/12-30 三档);**炸弹只在免费旋转内出现**(每屏最多 1 颗,2~1000 倍,绿/蓝/紫/红/彩虹 5 档品质;normal/ante 的基础旋无炸弹);基础盘 4+ SCATTER 触发 15 旋免费旋转(三模式进 FS 后规则一致),FS 内 3+ 再 +5 旋,上限 30 旋(到上限后不再落 3+ 个 S)。
|
||||
@@ -0,0 +1,727 @@
|
||||
# 疯狂转盘 前端接口文档
|
||||
|
||||
> **版本**:v2.0
|
||||
> **更新时间**:2025-01
|
||||
> **适用前端**:Web/H5/小程序
|
||||
> **Base URL**:`{host}/api/wheel`
|
||||
|
||||
---
|
||||
|
||||
## 📋 目录
|
||||
|
||||
1. [玩法概述](#一玩法概述)
|
||||
2. [通用约定](#二通用约定)
|
||||
3. [接口清单](#三接口清单)
|
||||
4. [接口详情](#四接口详情)
|
||||
5. [时间轴使用指南](#五时间轴使用指南)
|
||||
6. [前端对接要点](#六前端对接要点)
|
||||
|
||||
---
|
||||
|
||||
## 一、玩法概述
|
||||
|
||||
疯狂转盘是一款 **选颜色+符号 → 下注 → 转盘停 → 命中得饰品** 的真人轮盘游戏。
|
||||
|
||||
### 核心规则
|
||||
|
||||
| 项目 | 说明 |
|
||||
|------|------|
|
||||
| **盘面结构** | 内外圈各 24 格,索引一一对应<br>• 外圈符号:固定布局(A金/B红/C紫/D蓝)<br>• 内圈颜色:每轮随机排布(金6/红8/紫10) |
|
||||
| **下注方式** | 选 1 个「颜色×符号」组合 + 1 个下注额<br>**每轮每人最多 50 注** |
|
||||
| **下注额档位** | 10 档:5 / 10 / 20 / 50 / 100 / 200 / 500 / 1000 / 2000 / 5000 元 |
|
||||
| **倍率** | 12 个倍率(3 色 × 4 符号),所有档位共用<br>例:gold+D 倍率=22,押 100 元中奖得 2200 元 |
|
||||
| **命中判定** | 下注的 `color+symbol` == 开奖的 `resultColor+resultSymbol` |
|
||||
| **中奖奖励** | 饰品价值 = **下注额 × 倍率**<br>直接进背包,无需选择"保留/分解" |
|
||||
| **未中奖励** | 每注发 **0.01 元安慰饰品**(保证每注必有产出) |
|
||||
| **决策池** | 单一真实奖池,所有人同一个落点<br>• `real`(真实池,参与落点/RTP 决策)= 普通(02)+主播(01)+招商(04)+卡商(05)<br>• `display`(展示,可下注/返奖但不入真实池)= 机器人(03)+白号(06) |
|
||||
|
||||
### 一局生命周期
|
||||
|
||||
```
|
||||
newRound 创建
|
||||
↓
|
||||
下注期 (30s) ← 玩家可调用 /dobet 下注
|
||||
↓
|
||||
封盘期 (2s) ← 不再接受下注,倒计时"即将开奖"
|
||||
↓
|
||||
开奖 + 动画 (7s) ← 后端出结果,前端播转盘动画
|
||||
↓
|
||||
结果展示 → 下一局
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 二、通用约定
|
||||
|
||||
### 2.1 请求方式
|
||||
|
||||
- **协议**:HTTPS
|
||||
- **Method**:全部 `POST`
|
||||
- **Content-Type**:`application/json`
|
||||
- **鉴权**:`Authorization: Bearer {token}`(登录后从响应头或登录接口获取)
|
||||
|
||||
> ⚠️ **重要**:`userId` 由后端从 token 解析,**客户端不要传 userId 参数**。
|
||||
|
||||
### 2.2 统一响应结构
|
||||
|
||||
```json
|
||||
{
|
||||
"code": 200,
|
||||
"msg": "操作成功",
|
||||
"data": {}
|
||||
}
|
||||
```
|
||||
|
||||
| code | 含义 | 前端处理 |
|
||||
|------|------|----------|
|
||||
| 200 | 成功 | 正常处理 data |
|
||||
| 500 | 业务失败 | Toast 显示 msg |
|
||||
| 701 | 未实名认证 | 弹实名认证弹窗 |
|
||||
| 702 | 账户余额不足 | 提示充值 |
|
||||
| 703 | 玩家未充值,不能游玩 | 引导首充 |
|
||||
|
||||
### 2.3 货币单位
|
||||
|
||||
- 所有金额字段为 **String 类型**,单位:**元**(精确到小数点后 2 位)
|
||||
- 示例:`"balance": "9800.50"` 表示 9800.50 元
|
||||
|
||||
### 2.4 时间格式
|
||||
|
||||
- **时间戳**:`Long` 类型,**秒级** Unix 时间戳(如 `1738300000`)
|
||||
- **时间字符串**:`yyyy-MM-dd HH:mm:ss`(东八区)
|
||||
|
||||
---
|
||||
|
||||
## 三、接口清单
|
||||
|
||||
| # | 接口名称 | 路径 | 用途 | 调用时机 |
|
||||
|---|---------|------|------|---------|
|
||||
| 1 | ~~获取游戏配置~~ | ~~`/game_config`~~ | **已废弃**,数据已合并到 `/last_round_info` | ~~进入游戏页时调用 1 次~~ |
|
||||
| 2 | 投注 | `/dobet` | 下注一笔 | 玩家点击下注按钮 |
|
||||
| 3 | 获取当前局状态 | `/last_round_info` | **一次拿全**:配置+状态+开奖+下注 | **轮询**(1-2 秒间隔) |
|
||||
| 4 | 我的下注记录 | `/history_bet_info` | 我的全部下注记录(中奖+未中奖,分页) | 打开"记录"页 |
|
||||
| 5 | 全服中奖记录 | `/round_history_bet_info` | 全服中奖公示(只含真正命中,分页) | 打开"中奖榜"页 |
|
||||
| 6 | 历史开奖结果 | `/round_results` | 每一轮的开奖符号(分页) | 打开"历史开奖"页 |
|
||||
|
||||
---
|
||||
|
||||
## 四、接口详情
|
||||
|
||||
### 4.1 ~~获取游戏配置~~ (已废弃)
|
||||
|
||||
> **⚠️ 已废弃**:本接口数据已全部合并到 `/last_round_info`,前端只需轮询一个接口即可。
|
||||
> 旧接口保留仅为兼容旧版前端,新前端请直接使用 `/last_round_info`。
|
||||
|
||||
---
|
||||
|
||||
### 4.2 投注
|
||||
|
||||
**`POST /api/wheel/dobet`**
|
||||
|
||||
下注一笔。**每轮每人上限 50 注**。
|
||||
|
||||
#### 请求参数
|
||||
|
||||
```json
|
||||
{
|
||||
"bet": 100,
|
||||
"color": "gold",
|
||||
"symbol": "D"
|
||||
}
|
||||
```
|
||||
|
||||
| 参数 | 类型 | 必填 | 说明 |
|
||||
|------|------|------|------|
|
||||
| `bet` | Integer | ✅ | 下注额,必须是 10 档之一 |
|
||||
| `color` | String | ✅ | 内圈色:`gold` / `red` / `purple` |
|
||||
| `symbol` | String | ✅ | 外圈符:`A` / `B` / `C` / `D` |
|
||||
|
||||
#### 响应示例
|
||||
|
||||
```json
|
||||
{
|
||||
"code": 200,
|
||||
"msg": "操作成功",
|
||||
"data": {
|
||||
"roundId": 10086,
|
||||
"cost": "100.00",
|
||||
"balance": "9700.50",
|
||||
"credits": "1200.00"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
| 字段 | 类型 | 说明 |
|
||||
|------|------|------|
|
||||
| `roundId` | Long | 本回合 ID |
|
||||
| `cost` | String | 本次扣款(= 下注额) |
|
||||
| `balance` | String | 下注后余额(金币) |
|
||||
| `credits` | String | 下注后积分 |
|
||||
|
||||
#### 常见错误
|
||||
|
||||
| code | msg | 场景 |
|
||||
|------|-----|------|
|
||||
| 500 | 网站维护中...... | 全站维护 |
|
||||
| 500 | 疯狂转盘维护中...... | 转盘维护 |
|
||||
| 500 | 游戏不在投注期 | 已封盘/开奖中 |
|
||||
| 500 | 每轮最多下注50注 | 超出上限 |
|
||||
| 500 | 该下注组合未配置奖品 | 矩阵缺失(联系运营) |
|
||||
| 702 | 账户余额不足 | 需充值 |
|
||||
| 703 | 玩家未充值不能游玩任何游戏 | 需首充 |
|
||||
|
||||
---
|
||||
|
||||
### 4.3 获取当前局状态
|
||||
|
||||
**`POST /api/wheel/last_round_info`**
|
||||
|
||||
**核心接口**,轮询调用(建议 1-2 秒间隔),**一次返回全部数据**:游戏配置 + 当前回合状态 + 开奖结果 + 我的下注。
|
||||
|
||||
#### 请求参数
|
||||
|
||||
```json
|
||||
{}
|
||||
```
|
||||
|
||||
#### 响应示例
|
||||
|
||||
```json
|
||||
{
|
||||
"code": 200,
|
||||
"msg": "操作成功",
|
||||
"data": {
|
||||
"roundId": 10086,
|
||||
"periodNo": 202501011,
|
||||
"serverTime": 1738300050,
|
||||
"startTime": 1738300000,
|
||||
"bettedTime": 1738300030,
|
||||
"spinTime": 1738300032,
|
||||
"endTime": 1738300039,
|
||||
"status": "spined",
|
||||
|
||||
"outerSymbols": ["A","D","C","D","B","D","C","D","B","C","D","C","A","D","C","D","C","D","B","D","C","D","C","D"],
|
||||
"bets": [5, 10, 20, 50, 100, 200, 500, 1000, 2000, 5000],
|
||||
"myPoolType": "real",
|
||||
"rewardMatrix": [
|
||||
{
|
||||
"betAmount": 5,
|
||||
"color": "gold",
|
||||
"symbol": "D",
|
||||
"multiplier": 22,
|
||||
"prizeValue": "110.00",
|
||||
"prizeName": "黄金玫瑰",
|
||||
"prizeGrade": 3,
|
||||
"prizeExterior": "崭新出厂",
|
||||
"prizeImage": "https://example.com/prize.png",
|
||||
"prizeLevelImg": "https://example.com/level3.png"
|
||||
}
|
||||
],
|
||||
|
||||
"innerColors": ["gold","gold","red","purple","purple","red","gold","purple","red","purple","gold","purple","red","purple","gold","gold","purple","purple","red","gold","purple","red","purple"],
|
||||
"resultIndex": 5,
|
||||
"resultColor": "gold",
|
||||
"resultSymbol": "D",
|
||||
"totalWin": "2200.00",
|
||||
"userBets": [
|
||||
{
|
||||
"color": "gold",
|
||||
"symbol": "D",
|
||||
"count": 3,
|
||||
"totalBet": "300.00"
|
||||
},
|
||||
{
|
||||
"color": "red",
|
||||
"symbol": "A",
|
||||
"count": 1,
|
||||
"totalBet": "50.00"
|
||||
}
|
||||
],
|
||||
"winResult": [
|
||||
{
|
||||
"color": "gold",
|
||||
"symbol": "D",
|
||||
"hit": true,
|
||||
"boxRecordId": 123456,
|
||||
"prizeName": "黄金玫瑰",
|
||||
"prizeImage": "https://example.com/prize.png",
|
||||
"prizeGrade": 5,
|
||||
"prizeLevelImg": "https://example.com/level5.png",
|
||||
"prizeExterior": "崭新出厂",
|
||||
"prizeValue": "2200.00"
|
||||
},
|
||||
{
|
||||
"color": "red",
|
||||
"symbol": "A",
|
||||
"hit": false,
|
||||
"boxRecordId": 123457,
|
||||
"prizeName": "安慰饰品",
|
||||
"prizeImage": "https://example.com/consolation.png",
|
||||
"prizeGrade": 1,
|
||||
"prizeLevelImg": "https://example.com/level1.png",
|
||||
"prizeExterior": "崭新出厂",
|
||||
"prizeValue": "0.01"
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
#### 响应字段说明
|
||||
|
||||
**游戏配置字段**(原 `/game_config` 接口数据,现已合并):
|
||||
|
||||
| 字段 | 类型 | 说明 |
|
||||
|------|------|------|
|
||||
| `outerSymbols` | Array\<String\> | 外圈 24 格符号固定布局(`A`/`B`/`C`/`D`)<br>例:`["A","D","C","D",...]` |
|
||||
| `bets` | Array\<Integer\> | 10 档下注额 |
|
||||
| `myPoolType` | String | 当前用户所属决策池<br>`real`:真实池(普通02/主播01/招商04/卡商05,参与落点与 RTP)<br>`display`:展示(机器人03/白号06,可下注/返奖但不入真实池)<br>`null`:不可参与(如超管99) |
|
||||
| `rewardMatrix` | Array | 奖励矩阵(120 条,10 档 × 3 色 × 4 符号)<br>每条含 `betAmount`/`color`/`symbol`/`multiplier` 及饰品信息(名称/图片/品质图片/磨损度/价格) |
|
||||
|
||||
**当局状态字段**:
|
||||
|
||||
| 字段 | 类型 | 说明 |
|
||||
|------|------|------|
|
||||
| `roundId` | Long | 回合 ID |
|
||||
| `periodNo` | Long | 全局唯一期号 |
|
||||
| `serverTime` | Long | **服务器当前时间**(秒级时间戳)<br>用于和其他时间点对比算倒计时 |
|
||||
| `startTime` | Long | 本局开始时间 |
|
||||
| `bettedTime` | Long | **封盘时间点** = startTime + 下注时间<br>到这个时刻后不能再下注 |
|
||||
| `spinTime` | Long | **开奖时间点** = bettedTime + 封盘时间<br>到这个时刻后开始播转盘动画 |
|
||||
| `endTime` | Long | **本局结束时间** = spinTime + 开奖动画时间<br>动画播完、下一局即将开始 |
|
||||
| `status` | String | `betting`:投注期<br>`spined`:封盘/开奖中 |
|
||||
| `innerColors` | Array\<String\> | **本轮内圈 24 色实际排列**(`gold`/`red`/`purple`)<br>例:`["gold","red","purple",...]` 用于渲染盘面 |
|
||||
| `resultIndex` | Integer | 开奖格子索引(0-23)<br>未开奖时为 `null` |
|
||||
| `resultColor` | String | 开奖颜色 `gold`/`red`/`purple`<br>未开奖时为 `null` |
|
||||
| `resultSymbol` | String | 开奖符号 `A`/`B`/`C`/`D`<br>未开奖时为 `null` |
|
||||
| `totalWin` | String | 当前用户本轮总返奖(饰品价值合计) |
|
||||
| `userBets` | Array | **当前用户本轮下注汇总**(按颜色+符号聚合,见下表) |
|
||||
| `winResult` | Array | **当前用户本轮发奖明细**(每注一条,见下表) |
|
||||
|
||||
**userBets 结构**(我的下注汇总,按颜色+符号聚合):
|
||||
|
||||
| 字段 | 类型 | 说明 |
|
||||
|------|------|------|
|
||||
| `color` | String | 下注的颜色 |
|
||||
| `symbol` | String | 下注的符号 |
|
||||
| `count` | Integer | 下注次数 |
|
||||
| `totalBet` | String | 总下注额 |
|
||||
|
||||
**winResult 结构**(我的发奖明细,每注一条):
|
||||
|
||||
| 字段 | 类型 | 说明 |
|
||||
|------|------|------|
|
||||
| `color` | String | 下注的颜色 |
|
||||
| `symbol` | String | 下注的符号 |
|
||||
| `hit` | Boolean | 是否命中(`true`=中奖矩阵饰品,`false`=安慰饰品) |
|
||||
| `boxRecordId` | Long | **背包记录ID**(用于调用分解/出售接口) |
|
||||
| `prizeName` | String | 实际发放的饰品名称 |
|
||||
| `prizeImage` | String | 饰品图片 URL |
|
||||
| `prizeGrade` | Integer | 饰品品质等级 |
|
||||
| `prizeLevelImg` | String | 饰品品质图片 URL |
|
||||
| `prizeExterior` | String | 饰品磨损度 |
|
||||
| `prizeValue` | String | 饰品价格(命中=下注额×倍率;安慰=0.01) |
|
||||
|
||||
> 💡 **前端使用提示**:
|
||||
> - 未开奖时 `winResult` 为空
|
||||
> - 已开奖后,`winResult` 每注一条:`hit=true` 是中奖饰品,`hit=false` 是安慰饰品(0.01元)
|
||||
> - 使用 `boxRecordId` 调用分解/出售接口
|
||||
|
||||
---
|
||||
|
||||
### 4.4 我的下注记录
|
||||
|
||||
**`POST /api/wheel/history_bet_info`**
|
||||
|
||||
当前登录用户自己的全部下注记录(中奖+未中奖),分页。未中奖注单(`win = 0.01` 安慰奖)的饰品字段为 `null`。
|
||||
|
||||
#### 请求参数
|
||||
|
||||
```json
|
||||
{
|
||||
"pageNum": 1,
|
||||
"pageSize": 10
|
||||
}
|
||||
```
|
||||
|
||||
#### 响应示例
|
||||
|
||||
```json
|
||||
{
|
||||
"code": 200,
|
||||
"msg": "操作成功",
|
||||
"data": {
|
||||
"total": 25,
|
||||
"rows": [
|
||||
{
|
||||
"periodNo": 2026090725038,
|
||||
"endTime": "2026-09-07 09:14:49",
|
||||
"userId": 666299,
|
||||
"nickName": "李四2",
|
||||
"avatar": "/profile/upload/xxx.png",
|
||||
"bet": "5.00",
|
||||
"betSymbol": "D",
|
||||
"win": "40.00",
|
||||
"betCount": 3,
|
||||
"prizeGrade": 1,
|
||||
"prizeName": "某某饰品",
|
||||
"prizeImage": "/profile/upload/xxx.png"
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
| 字段 | 类型 | 说明 |
|
||||
|------|------|------|
|
||||
| `total` | Long | 总中奖注数 |
|
||||
| `rows` | Array | 中奖注单列表 |
|
||||
| `rows[].periodNo` | Long | 完整期号 |
|
||||
| `rows[].endTime` | String | 回合结束时间(yyyy-MM-dd HH:mm:ss) |
|
||||
| `rows[].userId` | Long | 用户 ID |
|
||||
| `rows[].nickName` | String | 用户昵称(完整,不脱敏) |
|
||||
| `rows[].avatar` | String | 用户头像 |
|
||||
| `rows[].bet` | String | 单注下注额 |
|
||||
| `rows[].betSymbol` | String | 中奖符号 |
|
||||
| `rows[].win` | String | 中奖额 |
|
||||
| `rows[].betCount` | Integer | 该中奖符号下注次数 |
|
||||
| `rows[].prizeGrade` | Integer | 饰品品质等级(对应 tt_ornaments_level.id:1金/2红/3紫/4蓝) |
|
||||
| `rows[].prizeName` | String | 饰品名称 |
|
||||
| `rows[].prizeImage` | String | 饰品图片 |
|
||||
|
||||
---
|
||||
|
||||
### 4.5 全服中奖记录
|
||||
|
||||
**`POST /api/wheel/round_history_bet_info`**
|
||||
|
||||
全服中奖公示(只保留真正命中 `win > 0.01` 的注单,排除 0.01 安慰奖),分页。结构与 `history_bet_info` 完全一致,只是数据范围为全服所有用户。
|
||||
|
||||
#### 请求参数
|
||||
|
||||
```json
|
||||
{
|
||||
"pageNum": 1,
|
||||
"pageSize": 30
|
||||
}
|
||||
```
|
||||
|
||||
#### 响应示例
|
||||
|
||||
```json
|
||||
{
|
||||
"code": 200,
|
||||
"msg": "操作成功",
|
||||
"data": {
|
||||
"total": 99,
|
||||
"rows": [
|
||||
{
|
||||
"periodNo": 2026090725038,
|
||||
"endTime": "2026-09-07 09:14:49",
|
||||
"userId": 666299,
|
||||
"nickName": "李四2",
|
||||
"avatar": "/profile/upload/xxx.png",
|
||||
"bet": "5.00",
|
||||
"betSymbol": "D",
|
||||
"win": "40.00",
|
||||
"betCount": 3,
|
||||
"prizeGrade": 1,
|
||||
"prizeName": "某某饰品",
|
||||
"prizeImage": "/profile/upload/xxx.png"
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
| 字段 | 类型 | 说明 |
|
||||
|------|------|------|
|
||||
| `total` | Long | 总中奖注数 |
|
||||
| `rows` | Array | 中奖注单列表 |
|
||||
| `rows[].periodNo` | Long | 完整期号 |
|
||||
| `rows[].endTime` | String | 回合结束时间 |
|
||||
| `rows[].userId` | Long | 中奖用户 ID |
|
||||
| `rows[].nickName` | String | 中奖用户昵称(完整,不脱敏) |
|
||||
| `rows[].avatar` | String | 中奖用户头像 |
|
||||
| `rows[].bet` | String | 单注下注额 |
|
||||
| `rows[].betSymbol` | String | 中奖符号 |
|
||||
| `rows[].win` | String | 中奖额 |
|
||||
| `rows[].betCount` | Integer | 该中奖符号下注次数 |
|
||||
| `rows[].prizeGrade` | Integer | 饰品品质等级(对应 tt_ornaments_level.id:1金/2红/3紫/4蓝) |
|
||||
| `rows[].prizeName` | String | 饰品名称 |
|
||||
| `rows[].prizeImage` | String | 饰品图片 |
|
||||
|
||||
---
|
||||
|
||||
### 4.6 历史开奖结果
|
||||
|
||||
**`POST /api/wheel/round_results`**
|
||||
|
||||
获取每一轮的开奖符号(用于展示历史开奖记录,如右侧滚动的历史中奖列表)。
|
||||
|
||||
#### 请求参数
|
||||
|
||||
```json
|
||||
{
|
||||
"pageNum": 1,
|
||||
"pageSize": 20
|
||||
}
|
||||
```
|
||||
|
||||
| 字段 | 类型 | 必填 | 说明 |
|
||||
|------|------|------|------|
|
||||
| `pageNum` | Integer | 否 | 页码(默认 1) |
|
||||
| `pageSize` | Integer | 否 | 每页条数(默认 20) |
|
||||
|
||||
#### 响应示例
|
||||
|
||||
```json
|
||||
{
|
||||
"code": 200,
|
||||
"msg": "操作成功",
|
||||
"data": {
|
||||
"rows": [
|
||||
{
|
||||
"periodNo": 2025010100086,
|
||||
"date": "20250101",
|
||||
"roundId": 86,
|
||||
"resultIndex": 5,
|
||||
"resultColor": "gold",
|
||||
"resultSymbol": "D",
|
||||
"endTime": "2025-01-01 12:15:39"
|
||||
},
|
||||
{
|
||||
"periodNo": 2025010100085,
|
||||
"date": "20250101",
|
||||
"roundId": 85,
|
||||
"resultIndex": 12,
|
||||
"resultColor": "red",
|
||||
"resultSymbol": "A",
|
||||
"endTime": "2025-01-01 12:15:00"
|
||||
}
|
||||
],
|
||||
"total": 2156
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
#### 响应字段
|
||||
|
||||
| 字段 | 类型 | 说明 |
|
||||
|------|------|------|
|
||||
| `rows` | Array | 历史开奖记录列表 |
|
||||
| `rows[].periodNo` | Long | **全局唯一期号**(13位,前8位=日期yyyyMMdd,后5位=局号) |
|
||||
| `rows[].date` | String | **日期**(yyyyMMdd,从 periodNo 拆分而来) |
|
||||
| `rows[].roundId` | Long | **当天局号**(从 periodNo 拆分而来) |
|
||||
| `rows[].resultIndex` | Integer | 开奖格子索引(0-23) |
|
||||
| `rows[].resultColor` | String | 开奖颜色(gold/red/purple) |
|
||||
| `rows[].resultSymbol` | String | 开奖符号(A/B/C/D) |
|
||||
| `rows[].endTime` | String | 本局结束时间(yyyy-MM-dd HH:mm:ss) |
|
||||
| `total` | Long | 总记录数 |
|
||||
|
||||
#### 前端展示建议
|
||||
|
||||
```javascript
|
||||
// 展示期号
|
||||
function formatPeriodNo(periodNo) {
|
||||
const str = String(periodNo).padStart(13, '0');
|
||||
const year = str.slice(0, 4);
|
||||
const month = str.slice(4, 6);
|
||||
const day = str.slice(6, 8);
|
||||
const round = parseInt(str.slice(8)); // 去前导零
|
||||
return `${year}-${month}-${day}-${round}`;
|
||||
}
|
||||
|
||||
// 渲染列表
|
||||
data.rows.forEach(item => {
|
||||
// 期号:2025-01-01-86
|
||||
const periodText = formatPeriodNo(item.periodNo);
|
||||
|
||||
// 开奖结果:金色箱子 × 15
|
||||
const symbolIcon = getSymbolIcon(item.resultColor, item.resultSymbol);
|
||||
|
||||
renderHistoryItem(periodText, symbolIcon);
|
||||
});
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 五、时间轴使用指南
|
||||
|
||||
**核心**:后端返回的是**具体时间点**(startTime/bettedTime/spinTime/endTime),前端根据这些时间点和 `serverTime` 做倒计时和阶段切换。
|
||||
|
||||
### 一局的完整时间轴
|
||||
|
||||
```
|
||||
startTime (本局创建)
|
||||
│
|
||||
├─ 【下注期】serverTime < bettedTime
|
||||
│ 前端显示:倒计时 "距封盘 X 秒",接受下注
|
||||
│ 倒计时 = bettedTime - serverTime
|
||||
│
|
||||
├─ bettedTime (封盘时刻)
|
||||
│
|
||||
├─ 【封盘期】bettedTime ≤ serverTime < spinTime
|
||||
│ 前端显示:"封盘中",倒计时 "X 秒后开奖"
|
||||
│ 倒计时 = spinTime - serverTime
|
||||
│
|
||||
├─ spinTime (开奖时刻)
|
||||
│
|
||||
├─ 【开奖中】spinTime ≤ serverTime < endTime
|
||||
│ 前端显示:播转盘旋转动画
|
||||
│ 指针停在 resultIndex 格,显示 resultColor+resultSymbol
|
||||
│ 动画时长 = endTime - spinTime(由后台"开奖动画时间"配置)
|
||||
│
|
||||
├─ endTime (本局结束)
|
||||
│
|
||||
└─ 【等待下一局】serverTime ≥ endTime
|
||||
前端显示:倒计时 "下一局 X 秒后开始"
|
||||
继续轮询,拿到新 roundId 进入新一轮
|
||||
```
|
||||
|
||||
### 前端倒计时计算公式
|
||||
|
||||
| 阶段 | 判断条件 | 倒计时公式 | 显示文案 |
|
||||
|------|---------|-----------|---------|
|
||||
| **下注期** | `serverTime < bettedTime` | `bettedTime - serverTime` | "距封盘 X 秒" |
|
||||
| **封盘期** | `bettedTime ≤ serverTime < spinTime` | `spinTime - serverTime` | "X 秒后开奖" |
|
||||
| **开奖中** | `spinTime ≤ serverTime < endTime` | 播动画 | "转盘旋转中" |
|
||||
| **结果展示** | `serverTime ≥ endTime` | `下一局startTime - serverTime` | "下一局 X 秒后" |
|
||||
|
||||
### 关键提醒
|
||||
|
||||
1. **后端不返回时长配置**(下注时间/封盘时间/动画时间),只返回时间点。前端不需要自己算,直接用时间点差值。
|
||||
2. **时间点会随后台配置变化**:运营改"下注时间"从 30→50 秒,新局的 `bettedTime` 就会自动延后,前端倒计时自动变长。
|
||||
3. **开奖动画时长** = `endTime - spinTime`,这个窗口内播动画 + 展示结果。
|
||||
|
||||
---
|
||||
|
||||
## 六、前端对接要点
|
||||
|
||||
### 6.1 进入游戏页
|
||||
|
||||
```javascript
|
||||
// 只需轮询 /last_round_info,一次拿到配置+状态
|
||||
setInterval(async () => {
|
||||
const round = await getLastRoundInfo();
|
||||
|
||||
// 首次进入:用配置数据渲染静态元素
|
||||
if (firstTime) {
|
||||
renderWheel(round.outerSymbols);
|
||||
renderBetPanel(round.bets, round.rewardMatrix);
|
||||
firstTime = false;
|
||||
}
|
||||
|
||||
// 每次轮询:更新动态状态
|
||||
updateRoundState(round);
|
||||
}, 2000);
|
||||
```
|
||||
```
|
||||
|
||||
### 6.2 轮询当前局状态
|
||||
|
||||
```javascript
|
||||
// 每 1-2 秒调用一次
|
||||
setInterval(async () => {
|
||||
const round = await getLastRoundInfo();
|
||||
|
||||
// 根据 status 和时间点切换 UI
|
||||
if (round.status === 'betting') {
|
||||
if (round.serverTime < round.bettedTime) {
|
||||
// 下注期
|
||||
showCountdown('距封盘', round.bettedTime - round.serverTime);
|
||||
enableBetting(true);
|
||||
} else {
|
||||
// 封盘期(status 可能还未切到 spined)
|
||||
showCountdown('即将开奖', round.spinTime - round.serverTime);
|
||||
enableBetting(false);
|
||||
}
|
||||
} else if (round.status === 'spined') {
|
||||
// 已开奖
|
||||
if (round.resultIndex !== null) {
|
||||
// 播转盘动画,指针停在 resultIndex
|
||||
playSpinAnimation(round.resultIndex, round.innerColors);
|
||||
// 展示发奖明细(winResult 每注一条,hit 区分中奖/安慰)
|
||||
showWinResult(round.winResult, round.totalWin);
|
||||
}
|
||||
// 倒计时下一局
|
||||
if (round.serverTime >= round.endTime) {
|
||||
showCountdown('下一局', estimateNextRound());
|
||||
}
|
||||
}
|
||||
}, 2000);
|
||||
```
|
||||
|
||||
### 6.3 下注
|
||||
|
||||
```javascript
|
||||
async function doBet(color, symbol, bet) {
|
||||
try {
|
||||
const res = await api.post('/api/wheel/dobet', { color, symbol, bet });
|
||||
if (res.code === 200) {
|
||||
// 扣款成功,更新余额
|
||||
updateBalance(res.data.balance, res.data.credits);
|
||||
// Toast 提示
|
||||
showToast('下注成功');
|
||||
}
|
||||
} catch (err) {
|
||||
if (err.code === 702) {
|
||||
showToast('余额不足,请充值');
|
||||
} else if (err.code === 500) {
|
||||
showToast(err.msg); // "游戏不在投注期" / "每轮最多50注"
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 6.4 展示饰品信息
|
||||
|
||||
```javascript
|
||||
// winResult 每注一条,hit 区分中奖/安慰
|
||||
round.winResult.forEach(item => {
|
||||
if (item.hit) {
|
||||
// 中奖,展示矩阵饰品
|
||||
showPrize({
|
||||
name: item.prizeName,
|
||||
image: item.prizeImage,
|
||||
levelImg: item.prizeLevelImg,
|
||||
exterior: item.prizeExterior,
|
||||
value: item.prizeValue,
|
||||
boxRecordId: item.boxRecordId
|
||||
});
|
||||
} else {
|
||||
// 未中,展示安慰饰品
|
||||
showConsolation(item.color, item.symbol);
|
||||
}
|
||||
});
|
||||
```
|
||||
|
||||
### 6.5 渲染盘面(innerColors)
|
||||
|
||||
```javascript
|
||||
// innerColors 是 24 个元素的数组,已经是 gold/red/purple
|
||||
// 例:["gold","red","purple",...]
|
||||
function renderWheel(innerColors, outerSymbols) {
|
||||
for (let i = 0; i < 24; i++) {
|
||||
const color = innerColors[i]; // 'gold' / 'red' / 'purple'
|
||||
const symbol = outerSymbols[i]; // 'A' / 'B' / 'C' / 'D'
|
||||
|
||||
// 渲染第 i 格:内圈色 + 外圈符号
|
||||
renderCell(i, color, symbol);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 6.6 注意事项
|
||||
|
||||
| 注意项 | 说明 |
|
||||
|-------|------|
|
||||
| **单接口轮询** | 只需轮询 `/last_round_info`,配置数据已合并,无需单独调 `/game_config` |
|
||||
| **不要自动重发 dobet** | 下注接口**非幂等**,失败时提示玩家手动重试 |
|
||||
| **轮询间隔** | 建议 1-2 秒,不要过于频繁 |
|
||||
| **时区** | 所有时间戳为东八区(UTC+8) |
|
||||
| **饰品已入背包** | 中奖后饰品直接进背包,无需"保留/分解"抉择<br>要兑现走通用背包分解接口 |
|
||||
| **维护模式** | 后台"游戏关闭"开关打开后:<br>① 新局无法下注(`dobet` 返回 500 维护提示)<br>② 轮询接口返回 `isMaintenance: true`,前端应弹窗并跳转<br>③ 进行中回合不强制结算,玩家可继续看结果 |
|
||||
|
||||
---
|
||||
|
||||
+1425
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user