40 KiB
玫瑰女郎 用户端 接口文档
一、功能说明
玫瑰女郎是一款选箱下注 → 旋转 → 必得饰品的电子游戏。核心产品逻辑:
| 规则 | 说明 |
|---|---|
| 三种玩法模式 | 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。
统一响应结构
{
"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 结构
{
"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 而报错。
请求参数
{
"boxId": 1001
}
| 参数 | 类型 | 必填 | 说明 |
|---|---|---|---|
| boxId | Long | 否 | 非空=开始新游戏;为空/null/不传=继续当前游戏 |
响应 data 结构(SpinResponse)
完整响应体(@JsonInclude(NON_NULL),未出现的字段即为 null/未返回)。下面是真实数据的一步(box1 普通旋转 · 自然触发的免费旋转第 6 旋 · 第 2 波消除,屏上还留着一颗 2 倍炸弹;回合 round 66551,整局实付 125.45 倍底注、30 旋、42 步):
{
"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 倍)
{
"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 倍炸弹)
{
"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,可结算展示)
{
"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,多出来的只是现金scatterWin3x/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 汇总)。
盘面与动画数据的约定(每一步都返回完整盘面,前端不用自己推算):
gridState永远是权威盘面:5 行 × 6 列 = 30 格,格子里只有A~I/S/M,不会出现空格或 null(全库 355,902 步已逐一校验:空白格 0、非法符号 0、复刻结果与实际盘面 100% 一致)。- 同一旋内,第 N+1 波可以精确复刻:
上一波盘面−(上一波symbolLinks里tumble == 上一波 tumbleNum的格子)→ 按dropData下落 → 按fillData补位 =gridState(dropData与重力、fillData覆盖的格子也逐条校验过)。dropData[].symbol/fillData[].symbol一律是这一波结束后该位置的符号(与gridState同位置一致),所以按第 2 条落地后不会出现错符号。dropData只列位置发生变化的符号(原地不动的格子不在里面,前端保留原样即可);fillData正好覆盖"下落之后空出来的格子",不会漏格。- 新旋的第一波(
tumbleNum = 1,spinNum递增或进入本局第 1 步)dropData/fillData都是空数组 —— 这一波是整盘重发,前端直接整盘替换/重新发牌,不要按下落补位处理。- 免费旋转的"保底消除"让 8 个刚落下/刚补位的格子凑成一组消除:这些格子的新符号就在
dropData/fillData里(见第 3 条),所以整条动画链能完整复刻。前端每一步结束都以gridState为准渲染,永远不会出现空格。
免费旋转的累计倍数(生成端规则,前端显示不受影响): 免费旋转内,每一旋都可能带炸弹(每屏最多 1 颗;低倍档局只出 2
8 的小球、高倍档局可出到 1000), 有消除的旋会把本旋炸弹并入累计器,本旋赢额 = 基础分 × max(1, 累计值),之后累计值 += 本旋炸弹和 —— 也就是"滚雪球":上一旋攒的倍数会作用到后面每一次消除。 因为累计倍数会放大后续每一次消除,**低倍档局(10x25x,占买FS 抽中率 76%)大约只能演 5 次左右的 "消除+爆"**(单个球越小、能买的次数越多);要 10 次以上的连爆,那一局本身就得是 40 倍以上的档位。 表现上:15 旋里约 5 旋有消除、约 4 次炸弹并入、局末累计倍数常见 15~25(大奖局 100+)。normal/ante 的自然免费旋转:基础旋(
spinNum=0)不出炸弹,摇出 46 个 SCATTER 才进 FS, 进 FS 后走的是和买 FS 完全相同的规则(同样的炸弹密度、同样的累计器、同样的每旋保底消除), 这样一局实测约 80120 倍底注、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.30.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)用户。
请求参数
{
"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 结构
{
"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 结构
无进行中游戏:
{ "hasActive": false }
有进行中游戏:
{
"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))。末帧玩家点「立即分解」——或前端"自动分解"开关开启时代替玩家自动调用——都调这个通用背包接口把饰品兑换成金币/积分;接口没调到也只是饰品留在背包,无资金影响。
请求参数
{
"packSackIds": [556677],
"isAll": false,
"decomposeType": "game"
}
| 参数 | 类型 | 必填 | 说明 |
|---|---|---|---|
| packSackIds | Array | 否 | 待分解的背包记录 ID 列表(单个即 [boxRecordId]) |
| isAll | Boolean | 是 | 是否全部分解(false=按 ids) |
| decomposeType | String | 否 | backpack=背包分解(默认)/ game=游戏结果页立即分解。后端当前不读取该字段(分解结算一律按饰品来源判定),传与不传结果相同,保留仅为兼容前端调用 |
响应
{
"code": 200,
"msg": "成功分解1件饰品",
"data": null
}
分解流水口径:分解结算按饰品来源自动分组记流水(
tt_box_records.source→ 对应游戏奖励流水),与前端传来的decomposeType无关(该字段后端不读取,传与不传结果相同)。玫瑰女郎饰品来源OLYMPUS_BOX(13)→OLYMPUS_REWARD(61) 玫瑰女郎奖励:无论玩家「放入背包后再分解」还是末帧「立即分解」(含前端自动分解),入账流水一律是玫瑰女郎奖励。
六、关键流程(前端友好版)
6.1 进入游戏页
POST /api/olympus/boxes→ 拿到箱子列表,展示箱子和价格。- 玩家选箱子 → 记录
boxId。
6.2 开始新游戏
POST /api/olympus/dospin,body{"boxId": X}。- 返回第一步(
PLAYING或直接FINISH,若该局只有一步)。
6.3 逐步揭晓动画
- 对每步
PLAYING响应,播放对应动画(gridState/symbolLinks里tumble === tumbleNum的新增连线/dropData/fillData/multiplierDetail)。winAmount是该旋累计(末帧即该旋总额),spinWin是本局累计,可直接用于跳字。 - 再调用
POST /api/olympus/dospin,body{}(不带 boxId),得到下一步。 - 重复直止
state === "FINISH"。
6.4 末帧结算展示
- 读到
totalWin/totalMul/prizeInfo/boxRecordId。 - 饰品已写入背包,无需等待。
- 展示中奖弹窗,给玩家两个选择:
- 放入背包:什么都不用调,饰品已在背包(
boxRecordId)。 - 立即分解:调
POST /api/userPackSack/decompose,packSackIds:[boxRecordId], isAll:false,入账流水为玫瑰女郎奖励(61)。
- 放入背包:什么都不用调,饰品已在背包(
- 前端"自动分解"开关为开时:末帧直接按第 3 步的「立即分解」调用,等价于替玩家点了一下,后端无需任何额外字段或开关。
- 这次调用失败(断网、超时、玩家直接关页面)都不影响对局结果:饰品本来就在背包里,等于玩家选了「放入背包」,之后在背包页随时可再分解。重试也不会重复加钱(同一饰品只会被结算一次)。
- 结算站内信在"这一局播完"时才发(多步局=末帧
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)。