Files
henan-backend/docs/玫瑰女郎接口文档.md
T
2026-09-21 19:21:35 +08:00

40 KiB
Raw Blame History

玫瑰女郎 用户端 接口文档


一、功能说明

玫瑰女郎是一款选箱下注 → 旋转 → 必得饰品的电子游戏。核心产品逻辑:

规则 说明
三种玩法模式 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,多出来的只是现金 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 颗;低倍档局只出 28 的小球、高倍档局可出到 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.32.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 进入游戏页

  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)。