diff --git a/docs/updata_sql.md b/docs/updata_sql.md index 4d53804..e69de29 100644 --- a/docs/updata_sql.md +++ b/docs/updata_sql.md @@ -1,1748 +0,0 @@ --- VIP等级表新增流水返利百分比字段 -ALTER TABLE tt_vip_level ADD COLUMN flow_rebate_percent DECIMAL(10,4) DEFAULT 0.0000 COMMENT 'VIP流水返利百分比(%),如 2.0000 代表 2%'; - -新增疯狂转盘/极速糖果维护配置 -INSERT INTO sys_config (config_name, config_key, config_value, config_type, create_by, create_time, remark) -VALUES ('疯狂转盘维护', 'wheelMaintenance', '0', 'Y', 'admin', NOW(), '疯狂转盘维护开关 0正常 1维护'); - -INSERT INTO sys_config (config_name, config_key, config_value, config_type, create_by, create_time, remark) -VALUES ('极速糖果维护', 'sweetMaintenance', '0', 'Y', 'admin', NOW(), '极速糖果维护开关 0正常 1维护'); - -新增充值维护配置 -INSERT INTO sys_config (config_name, config_key, config_value, config_type, create_by, create_time, remark) -VALUES ('充值维护', 'rechargeMaintenance', '0', 'Y', 'admin', NOW(), '充值维护开关 0正常 1维护'); - -添加公告类型 -ALTER TABLE tt_announcement -ADD COLUMN `type` TINYINT NOT NULL DEFAULT 0 COMMENT '公告类型 0用户公告 1系统公告' AFTER content; - -ALTER TABLE tt_announcement MODIFY COLUMN title VARCHAR(255) NULL; - - -添加索引 -ALTER TABLE tt_box_records -ADD INDEX idx_tbr_holder_status_time (holder_user_id, status, create_time); - -ALTER TABLE tt_upgrade_record -ADD INDEX idx_tur_victory_gain (is_victory, gain_ornaments_price); - - -添加公告目标用户 -ALTER TABLE tt_announcement ADD COLUMN target_user_id BIGINT DEFAULT NULL COMMENT '目标用户ID,NULL表示全部用户'; - -公告加对个人 -ALTER TABLE tt_box_type ADD COLUMN is_show VARCHAR(2) DEFAULT '0' COMMENT '是否前端显示 0-显示 1-隐藏'; - -增加字典数据 -INSERT INTO sys_config (config_name, config_key, config_value, config_type, create_by, create_time, remark) -VALUES ('西部对决维护', 'westernDuelMaintenance', '0', 'Y', 'admin', NOW(), '西部对决维护开关 0正常 1维护'); - -INSERT INTO sys_config (config_name, config_key, config_value, config_type, create_by, create_time, remark) -VALUES ('幸运升级维护', 'luckyUpgradeMaintenance', '0', 'Y', 'admin', NOW(), '幸运升级维护开关 0正常 1维护'); - --- 推广后台角色菜单权限表(按用户类型分配菜单,支持排序) -CREATE TABLE `promo_role_menu` ( - `id` INT NOT NULL AUTO_INCREMENT, - `role_type` VARCHAR(10) NOT NULL COMMENT '用户类型: 01=主播, 04=招商, 05=卡商', - `menu_path` VARCHAR(255) NOT NULL COMMENT '菜单路径', - `sort_order` INT DEFAULT 0 COMMENT '排序序号,越小越靠前', - `create_time` DATETIME DEFAULT CURRENT_TIMESTAMP, - PRIMARY KEY (`id`), - UNIQUE KEY `uk_role_menu` (`role_type`, `menu_path`), - KEY `idx_role_type` (`role_type`) -) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='推广后台角色菜单权限表'; - --- 推广后台管理员表(独立于tt_user前端用户表) -CREATE TABLE `promo_admin` ( - `id` INT NOT NULL AUTO_INCREMENT, - `username` VARCHAR(50) NOT NULL COMMENT '登录账号(手机号)', - `password` VARCHAR(100) NOT NULL COMMENT '密码(BCrypt加密)', - `nick_name` VARCHAR(50) DEFAULT '管理员' COMMENT '昵称', - `phone_number` VARCHAR(20) DEFAULT NULL COMMENT '手机号', - `email` VARCHAR(64) DEFAULT NULL COMMENT '邮箱', - `avatar` TEXT DEFAULT NULL COMMENT '头像(base64 dataURL)', - `status` CHAR(1) DEFAULT '0' COMMENT '状态 0正常 1停用', - `create_time` DATETIME DEFAULT CURRENT_TIMESTAMP, - PRIMARY KEY (`id`), - UNIQUE KEY `uk_username` (`username`), - KEY `idx_phone_number` (`phone_number`) -) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='推广后台管理员表'; - --- 老库升级(已存在 promo_admin 表时执行这几条 + 回填手机号) --- ALTER TABLE promo_admin ADD COLUMN phone_number VARCHAR(20) DEFAULT NULL COMMENT '手机号' AFTER nick_name; --- ALTER TABLE promo_admin ADD COLUMN email VARCHAR(64) DEFAULT NULL COMMENT '邮箱' AFTER phone_number; --- ALTER TABLE promo_admin ADD COLUMN avatar TEXT DEFAULT NULL COMMENT '头像(base64 dataURL)' AFTER email; --- ALTER TABLE promo_admin ADD KEY idx_phone_number (phone_number); --- UPDATE promo_admin SET phone_number = username WHERE phone_number IS NULL OR phone_number = ''; - --- 插入超级管理员账号(密码: admin123) --- BCrypt加密后的值需根据实际环境生成,这里给出模板 -INSERT INTO promo_admin (username, password, nick_name, status, create_time) -VALUES ('admin', '$2a$10$7JB720yubVSZvUI0rEqK/.VqGOZTH.ulu33dHOiBE8ByOhJIrdAu2', '超级管理员', '0', NOW()); - --- 卡商卡密表(推广后台独立卡密系统) -CREATE TABLE `promo_card_key` ( - `id` BIGINT NOT NULL AUTO_INCREMENT, - `card_code` VARCHAR(32) NOT NULL COMMENT '卡密编码(唯一)', - `amount` DECIMAL(10,2) NOT NULL COMMENT '面值金额', - `recharge_list_id` INT DEFAULT NULL COMMENT '充值列表ID', - `creator_id` BIGINT NOT NULL COMMENT '创建者ID(卡商)', - `creator_name` VARCHAR(50) DEFAULT '' COMMENT '创建者名称', - `used_by_id` BIGINT DEFAULT NULL COMMENT '使用者ID', - `used_by_name` VARCHAR(50) DEFAULT NULL COMMENT '使用者名称', - `used_time` DATETIME DEFAULT NULL COMMENT '使用时间', - `status` TINYINT NOT NULL DEFAULT 0 COMMENT '状态: 0=未使用, 1=已使用', - `create_time` DATETIME DEFAULT CURRENT_TIMESTAMP, - PRIMARY KEY (`id`), - UNIQUE KEY `uk_card_code` (`card_code`), - KEY `idx_creator_id` (`creator_id`), - KEY `idx_status` (`status`) -) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='卡商卡密表'; - --- 推广后台菜单配置表(所有可分配的菜单从这里读取,不再硬编码) -CREATE TABLE `promo_menu_config` ( - `id` INT NOT NULL AUTO_INCREMENT, - `menu_path` VARCHAR(255) NOT NULL COMMENT '菜单路径', - `menu_label` VARCHAR(100) NOT NULL COMMENT '菜单显示名称', - `sort_order` INT DEFAULT 0 COMMENT '排序序号', - PRIMARY KEY (`id`), - UNIQUE KEY `uk_menu_path` (`menu_path`) -) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='推广后台菜单配置表'; - --- 初始菜单数据(menu_label必须与前端路由title一致) -INSERT INTO promo_menu_config (menu_path, menu_label, sort_order) VALUES -('/home/index', '首页', 1), -('/dashboard/index', '数据面板', 2), -('/user/index', '个人信息', 3), -('/mymoney/index', '我的统计', 4), -('/turnover/index', '流水明细', 5), -('/subFlow/index', '下级流水', 6), -('/cardMerchant/index', '卡密管理', 7), -('/permission/index', '权限管理', 8), -('/businessManage/index', '招商管理', 9), -('/anchorManage/index', '主播管理', 10), -('/cardMerchantManage/index', '卡商管理', 11); - --- 修正已有菜单标签(如果表中已有数据,执行以下UPDATE) -UPDATE promo_menu_config SET menu_label = '数据面板' WHERE menu_path = '/dashboard/index'; -UPDATE promo_menu_config SET menu_label = '我的统计' WHERE menu_path = '/mymoney/index'; -UPDATE promo_menu_config SET menu_label = '流水明细' WHERE menu_path = '/turnover/index'; -UPDATE promo_menu_config SET menu_label = '下级流水' WHERE menu_path = '/subFlow/index'; - --- 新增超管专属页面菜单 -INSERT IGNORE INTO promo_menu_config (menu_path, menu_label, sort_order) VALUES -('/businessManage/index', '招商管理', 9), -('/anchorManage/index', '主播管理', 10), -('/cardMerchantManage/index', '卡商管理', 11); - --- 新增9个页面菜单配置 -INSERT IGNORE INTO promo_menu_config (menu_path, menu_label, sort_order) VALUES -('/mySubordinates/index', '我的下级', 12), -('/flowRecord/index', '流水记录', 13), -('/cdkeyCreate/index', '创建CDKEY记录', 14), -('/cdkeyRedeem/index', '兑换CDKEY记录', 15), -('/cdkeyCreateDaily/index', '创建CDKEY日报', 16), -('/cdkeyRedeemDaily/index', '兑换CDKEY日报', 17), -('/rollRoom/index', 'ROLL房管理', 18), -('/commissionReport/index', '返点报表', 19), -('/subordinateRecharge/index', '下级充值报表', 20); - --- 新增「管理员管理」菜单 --- ① 菜单注册表(前端路由 + 权限管理页可勾选列表都从这里读) -INSERT IGNORE INTO promo_menu_config (menu_path, menu_label, sort_order) VALUES -('/adminManage/index', '管理员管理', 21); - --- ② 给超级管理员角色授权(role_type=99 已有配置行时必须补这行,否则侧边栏不显示; --- 也可以直接去「权限管理 → 超级管理员 → 勾选管理员管理 → 保存权限」) -INSERT IGNORE INTO promo_role_menu (role_type, menu_path, sort_order) VALUES -('99', '/adminManage/index', 21); - --- 修复 user_type 字典:补充卡商类型(05) -INSERT IGNORE INTO sys_dict_data (dict_sort, dict_label, dict_value, dict_type, css_class, list_class, is_default, status, create_by, create_time, remark) -VALUES (5, '卡商', '05', 'user_type', '', '', 'N', '0', 'admin', NOW(), '卡商用户类型'); - --- tt_user 表新增卡商专用余额字段 -ALTER TABLE tt_user ADD COLUMN merchant_balance DECIMAL(18,2) DEFAULT 0.00 COMMENT '卡商专用余额(仅卡商用户使用)'; - - -ALTER TABLE tt_box_ornaments ADD COLUMN odds_percentum VARCHAR(20) DEFAULT NULL COMMENT '显示爆率百分比'; - -ALTER TABLE tt_recharge_record ADD COLUMN card_merchant_id INT DEFAULT 0 COMMENT '卡商ID,默认0表示非卡商充值'; - - -ALTER TABLE tt_user -ADD COLUMN new_game_commission_rate DECIMAL(10,4) DEFAULT NULL COMMENT '西部对决新游戏消费返点比例', -ADD COLUMN classic_game_commission_rate DECIMAL(10,4) DEFAULT NULL COMMENT '经典玩法游戏消费返点比例'; - --- 推广CDKEY创建记录表 -CREATE TABLE `promo_cdkey` ( - `id` BIGINT NOT NULL AUTO_INCREMENT, - `title` VARCHAR(255) DEFAULT NULL COMMENT 'CDKEY标题', - `cdkey_code` VARCHAR(100) NOT NULL COMMENT 'CDKEY码(唯一)', - `amount_min` DECIMAL(12,2) NOT NULL DEFAULT 0 COMMENT '最低金额(金币)', - `amount_max` DECIMAL(12,2) NOT NULL DEFAULT 0 COMMENT '最高金额(金币)', - `num` INT NOT NULL DEFAULT 1 COMMENT '每个码可用次数', - `usable_count` INT NOT NULL DEFAULT 1 COMMENT '可用次数', - `used_count` INT NOT NULL DEFAULT 0 COMMENT '已用次数', - `description` VARCHAR(500) DEFAULT NULL COMMENT '描述', - `opening_time` DATETIME DEFAULT NULL COMMENT '开启时间', - `validity` DATETIME DEFAULT NULL COMMENT '有效期', - `status` TINYINT NOT NULL DEFAULT 0 COMMENT '状态: 0=进行中, 1=已结束', - `creator_id` BIGINT NOT NULL COMMENT '创建者ID', - `creator_name` VARCHAR(100) DEFAULT NULL COMMENT '创建者名称', - `create_time` DATETIME DEFAULT CURRENT_TIMESTAMP, - PRIMARY KEY (`id`), - UNIQUE KEY `uk_cdkey_code` (`cdkey_code`), - KEY `idx_creator_id` (`creator_id`), - KEY `idx_status` (`status`), - KEY `idx_create_time` (`create_time`) -) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='推广CDKEY创建记录表'; - --- 推广CDKEY兑换记录表 -CREATE TABLE `promo_cdkey_redeem` ( - `id` BIGINT NOT NULL AUTO_INCREMENT, - `cdkey_id` BIGINT NOT NULL COMMENT '关联promo_cdkey.id', - `cdkey_code` VARCHAR(100) NOT NULL COMMENT 'CDKEY码', - `redeemer_id` BIGINT NOT NULL COMMENT '兑换者ID', - `redeemer_name` VARCHAR(100) DEFAULT NULL COMMENT '兑换者名称', - `unit_value` DECIMAL(12,2) NOT NULL COMMENT '单个价值(金币)', - `redeem_time` DATETIME DEFAULT CURRENT_TIMESTAMP COMMENT '兑换时间', - PRIMARY KEY (`id`), - KEY `idx_cdkey_id` (`cdkey_id`), - KEY `idx_redeemer_id` (`redeemer_id`), - KEY `idx_redeem_time` (`redeem_time`) -) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='推广CDKEY兑换记录表'; - --- tt_recharge_card 表新增空白卡密标识字段 -ALTER TABLE tt_recharge_card ADD COLUMN is_blank TINYINT DEFAULT 0 COMMENT '是否空白卡密: 0=否, 1=是(空白卡密用户使用后仅加余额,不计入充值记录)'; - --- tt_recharge_record 表新增空白卡密标记字段(空白卡密产生的充值记录在充值记录列表中不展示) -ALTER TABLE tt_recharge_record ADD COLUMN is_blank_card TINYINT DEFAULT 0 COMMENT '是否空白卡密产生的充值记录: 0=否, 1=是'; - --- tt_roll_jackpot_ornaments 表新增指定获奖用户字段(多个用户ID用英文逗号分隔) -ALTER TABLE tt_roll_jackpot_ornaments ADD COLUMN designated_user_id VARCHAR(500) DEFAULT NULL COMMENT '指定获奖用户ID,多个用英文逗号分隔,NULL表示不指定(随机分配)'; - --- tt_user 表新增白号标识字段(白号:可正常游戏、可出现在榜单,但提货虚假成功、充值不计真实充值、不触发返佣) -ALTER TABLE tt_user ADD COLUMN is_blank_account TINYINT DEFAULT 0 COMMENT '是否白号: 0=否, 1=是'; - --- 疯狂转盘返奖饰品表 -CREATE TABLE game_wheel_prize_ornament ( - id BIGINT NOT NULL AUTO_INCREMENT, - game_type VARCHAR(32) DEFAULT 'wheel', - name VARCHAR(100) NOT NULL, - image_url VARCHAR(500) DEFAULT NULL, - grade INT DEFAULT 1, - is_fallback TINYINT DEFAULT 0 COMMENT '是否兜底: 0否 1是', - sort INT DEFAULT 0, - status CHAR(1) DEFAULT '1', - create_time DATETIME DEFAULT CURRENT_TIMESTAMP, - PRIMARY KEY (id) -) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='疯狂转盘返奖饰品'; - -CREATE TABLE game_wheel_prize_range ( - id BIGINT NOT NULL AUTO_INCREMENT, - game_type VARCHAR(32) DEFAULT 'wheel', - win_low DECIMAL(14,2) NOT NULL DEFAULT 0.00 COMMENT '返奖金额下限', - win_high DECIMAL(14,2) NOT NULL DEFAULT 0.00 COMMENT '返奖金额上限', - ornament_id BIGINT NOT NULL, - sort INT DEFAULT 0, - PRIMARY KEY (id) -) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='疯狂转盘返奖区间'; - --- 疯狂转盘返奖区间初始数据 -INSERT INTO game_wheel_prize_range (game_type, win_low, win_high, ornament_id, sort) VALUES -('wheel', 0.01, 0.02, 1, 1), -('wheel', 0.02, 10.00, 2, 2), -('wheel', 10.00, 50.00, 3, 3), -('wheel', 50.00, 200.00, 4, 4), -('wheel', 200.00, 500.00, 5, 5), -('wheel', 500.00, 1000.00, 6, 6), -('wheel', 1000.00, 2000.00, 7, 7), -('wheel', 2000.00, 5000.00, 8, 8), -('wheel', 5000.00, 99999999.00, 9, 9); - --- 对战出金数据 -ALTER TABLE tt_fight ADD COLUMN gold_drop JSON DEFAULT NULL COMMENT 'first gold ornament drop info for hall display'; - --- 极速糖果步骤表:新增消除位置、下落数据、填充数据字段 -ALTER TABLE game_sweet_step - ADD COLUMN removed_positions JSON DEFAULT NULL COMMENT '消除位置 [pos,...]' AFTER symbol_links, - ADD COLUMN drop_data JSON DEFAULT NULL COMMENT '下落数据 [{"from":int,"to":int,"symbol":"A"},...]' AFTER removed_positions, - ADD COLUMN fill_data JSON DEFAULT NULL COMMENT '填充数据 [{"pos":int,"symbol":"A"},...]' AFTER drop_data; - --- 推广后台:网站设置新增"支付公司"配置项 -INSERT INTO sys_config (config_id, config_name, config_key, config_value, config_type, create_by, create_time, remark) -SELECT 135, '支付公司', 'payCompany', 'chengduhipay', 'Y', 'admin', NOW(), '充值对接支付平台选择' -FROM DUAL WHERE NOT EXISTS (SELECT 1 FROM sys_config WHERE config_key = 'payCompany'); - --- 推广后台:网站设置新增"支付公司权重"配置项 -INSERT INTO sys_config (config_id, config_name, config_key, config_value, config_type, create_by, create_time, remark) -SELECT 136, '支付公司权重', 'payCompanyWeights', '[{"name":"chengduhipay","weight":10}]', 'Y', 'admin', NOW(), '支付公司权重分配,JSON格式' -FROM DUAL WHERE NOT EXISTS (SELECT 1 FROM sys_config WHERE config_key = 'payCompanyWeights'); - --- ROLL房奖项模板表 -CREATE TABLE `tt_roll_prize_template` ( - `id` INT NOT NULL AUTO_INCREMENT, - `template_name` VARCHAR(100) NOT NULL COMMENT '模板名称', - `create_by` VARCHAR(50) DEFAULT NULL COMMENT '创建者', - `create_time` DATETIME DEFAULT CURRENT_TIMESTAMP, - `update_time` DATETIME DEFAULT NULL, - PRIMARY KEY (`id`) -) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='ROLL房奖项模板'; - --- ROLL房奖项模板明细表(模板内每个饰品) -CREATE TABLE `tt_roll_prize_template_detail` ( - `id` INT NOT NULL AUTO_INCREMENT, - `template_id` INT NOT NULL COMMENT '关联tt_roll_prize_template.id', - `ornament_id` BIGINT NOT NULL COMMENT '饰品ID', - `ornament_name` VARCHAR(255) DEFAULT NULL COMMENT '饰品名称', - `img_url` VARCHAR(500) DEFAULT NULL COMMENT '饰品图片', - `price` DECIMAL(12,2) DEFAULT NULL COMMENT '饰品价格', - `create_time` DATETIME DEFAULT CURRENT_TIMESTAMP, - PRIMARY KEY (`id`), - KEY `idx_template_id` (`template_id`) -) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='ROLL房奖项模板明细'; - --- 奖项模板明细表新增饰品数量字段 -ALTER TABLE tt_roll_prize_template_detail ADD COLUMN ornaments_num INT DEFAULT 1 COMMENT '饰品数量'; - --- 极速糖果:游戏记录表新增结算/补发字段 -ALTER TABLE game_sweet_record - ADD COLUMN settled TINYINT DEFAULT 0 COMMENT '是否已结算 0未1已', - ADD COLUMN mode_type VARCHAR(20) DEFAULT NULL COMMENT '游戏模式 normal/ante/buy_fs/buy_super_fs', - ADD COLUMN auto_dismantle TINYINT DEFAULT 0 COMMENT '开局自动分解快照 0关1开', - ADD COLUMN reward_type VARCHAR(10) DEFAULT NULL COMMENT '发放类型 gold/ornament', - ADD COLUMN settled_win DECIMAL(10,2) DEFAULT 0.00 COMMENT '本局价值(金币金额=饰品价格)', - ADD COLUMN prize_id BIGINT DEFAULT NULL COMMENT '饰品ID game_sweet_prize_ornament.id', - ADD COLUMN prize_image_url VARCHAR(500) DEFAULT NULL COMMENT '饰品图片URL', - ADD COLUMN prize_level_img VARCHAR(500) DEFAULT NULL COMMENT '品质图片URL', - ADD COLUMN settle_source VARCHAR(20) DEFAULT NULL COMMENT '结算来源 finish/reconnect/scheduled'; - --- 极速糖果:返奖饰品表新增品质名称字段 -ALTER TABLE game_sweet_prize_ornament - ADD COLUMN exterior_name VARCHAR(100) DEFAULT NULL COMMENT '品质名称 崭新出厂/略有磨损/久经沙场/破损不堪/战痕累累'; - --- 极速糖果:未结算记录兜底补发定时任务(后台:系统监控>定时任务) -INSERT INTO sys_job (job_name, job_group, invoke_target, cron_expression, misfire_policy, concurrent, status, create_by, create_time, remark) -VALUES ('极速糖果未结算补发', 'SWEET', 'sweetSettleTask.scheduledSettle()', '0 */10 * * * ?', '1', '0', '0', 'admin', NOW(), '每10分钟扫描超35分钟未结算记录并补发金币/饰品'); - --- RichKu:支付宝金额<300强制走richku,>=300按权重分配(chengduhipay:richku=7:3),银联仅走chengduhipay -UPDATE sys_config -SET config_value = '[{"name":"chengduhipay","weight":7},{"name":"richku","weight":3,"payType":1}]' -WHERE config_key = 'payCompanyWeights'; - --- itpay:接入新支付渠道并纳入权重分配(权重值按需调整;payType 可选:1=支付宝,2=银联,不填=全渠道) --- 示例:chengduhipay:5、richku:3(仅支付宝)、itpay:2 -UPDATE sys_config -SET config_value = '[{"name":"chengduhipay","weight":5},{"name":"richku","weight":3,"payType":1},{"name":"itpay","weight":2}]' -WHERE config_key = 'payCompanyWeights'; - --- 极速糖果:饰品桶新增已消耗回合数(库存配额计数),剩余 = count - used --- 玩家抽奖消耗对应桶一个回合(used+1);整盒回合抽干时自动重算、或后台手动重算分布时 used 归 0(剩余回归满,玩家可重新抽奖) -ALTER TABLE game_sweet_box_ornament - ADD COLUMN used INT DEFAULT 0 COMMENT '已消耗回合数,剩余=count-used' AFTER count; - --- 极速糖果:奖品饰品表填充磨损度名称(根据 grade 映射) -UPDATE game_sweet_prize_ornament SET exterior_name = '崭新出厂' WHERE grade = 1 AND (exterior_name IS NULL OR exterior_name = ''); -UPDATE game_sweet_prize_ornament SET exterior_name = '略有磨损' WHERE grade = 2 AND (exterior_name IS NULL OR exterior_name = ''); -UPDATE game_sweet_prize_ornament SET exterior_name = '久经沙场' WHERE grade = 3 AND (exterior_name IS NULL OR exterior_name = ''); -UPDATE game_sweet_prize_ornament SET exterior_name = '破损不堪' WHERE grade = 4 AND (exterior_name IS NULL OR exterior_name = ''); -UPDATE game_sweet_prize_ornament SET exterior_name = '战痕累累' WHERE grade = 5 AND (exterior_name IS NULL OR exterior_name = ''); - --- 白号管理目录及子页面菜单 -INSERT INTO sys_menu (menu_id, menu_name, parent_id, order_num, path, component, query, is_frame, is_cache, menu_type, visible, status, perms, icon, create_by, create_time, update_by, update_time, remark) -VALUES (3100, '白号管理', 0, 5, 'blank', NULL, NULL, 1, 0, 'M', '0', '0', '', 'peoples', 'admin', NOW(), '', NULL, '白号管理目录'); - -INSERT INTO sys_menu (menu_id, menu_name, parent_id, order_num, path, component, query, is_frame, is_cache, menu_type, visible, status, perms, icon, create_by, create_time, update_by, update_time, remark) -VALUES (3101, '白号用户', 3100, 1, 'blankAccount', 'skins/blankAccount/index', NULL, 1, 0, 'C', '0', '0', 'admin:blankAccount:list', 'user', 'admin', NOW(), '', NULL, '白号用户列表'); - -INSERT INTO sys_menu (menu_id, menu_name, parent_id, order_num, path, component, query, is_frame, is_cache, menu_type, visible, status, perms, icon, create_by, create_time, update_by, update_time, remark) -VALUES (3102, '生成空白卡密', 3100, 2, 'blankCardKey', 'skins/blankCardKey/index', NULL, 1, 0, 'C', '0', '0', 'admin:blankCardKey:list', 'edit', 'admin', NOW(), '', NULL, '空白卡密管理'); - --- 空白卡密表(管理后台独立生成,与卡商卡密表 promo_card_key 分离) --- 表名规范化:加 tt_ 前缀(原 blank_card_key) -CREATE TABLE `tt_blank_card_key` ( - `id` BIGINT NOT NULL AUTO_INCREMENT, - `card_code` VARCHAR(32) NOT NULL COMMENT '卡密编码(唯一)', - `amount` DECIMAL(10,2) NOT NULL COMMENT '面值金额', - `used_by_id` BIGINT DEFAULT NULL COMMENT '使用者ID', - `used_by_name` VARCHAR(50) DEFAULT NULL COMMENT '使用者名称', - `used_time` DATETIME DEFAULT NULL COMMENT '使用时间', - `status` TINYINT NOT NULL DEFAULT 0 COMMENT '状态: 0=未使用, 1=已使用', - `creator_name` VARCHAR(50) DEFAULT '' COMMENT '创建者名称', - `create_time` DATETIME DEFAULT CURRENT_TIMESTAMP, - PRIMARY KEY (`id`), - UNIQUE KEY `uk_card_code` (`card_code`), - KEY `idx_status` (`status`) -) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='空白卡密表(管理后台生成)'; - --- 已上线库执行(老库表名规范化,幂等:表不存在时执行 rename 报错可忽略/或先判断) -RENAME TABLE blank_card_key TO tt_blank_card_key; - --- 饰品表新增商城价格字段(独立于 usePrice,专供前端商城展示) -ALTER TABLE tt_ornament ADD COLUMN store_price DECIMAL(18,2) DEFAULT NULL COMMENT '商城价格(= 第三方价格 × 汇率 × 溢价率)'; - - -ALTER TABLE game_wheel_round_bet ADD COLUMN settled INT DEFAULT 1 COMMENT '0=等决定 1=已结算'; - --- 疯狂转盘:奖品饰品表新增磨损度字段 -ALTER TABLE game_wheel_prize_ornament - ADD COLUMN exterior_name VARCHAR(100) DEFAULT NULL COMMENT '磨损度名称 崭新出厂/略有磨损/久经沙场/破损不堪/战痕累累'; - --- 疯狂转盘:奖品饰品表填充磨损度名称(根据 grade 映射) -UPDATE game_wheel_prize_ornament SET exterior_name = '崭新出厂' WHERE grade = 1 AND (exterior_name IS NULL OR exterior_name = ''); -UPDATE game_wheel_prize_ornament SET exterior_name = '略有磨损' WHERE grade = 2 AND (exterior_name IS NULL OR exterior_name = ''); -UPDATE game_wheel_prize_ornament SET exterior_name = '久经沙场' WHERE grade = 3 AND (exterior_name IS NULL OR exterior_name = ''); -UPDATE game_wheel_prize_ornament SET exterior_name = '破损不堪' WHERE grade = 4 AND (exterior_name IS NULL OR exterior_name = ''); -UPDATE game_wheel_prize_ornament SET exterior_name = '战痕累累' WHERE grade = 5 AND (exterior_name IS NULL OR exterior_name = ''); - - --- 西部对决 → 荒野争锋:更新 sys_config 维护开关显示名 -UPDATE sys_config SET config_name = '荒野争锋维护', remark = '荒野争锋维护开关 0正常 1维护' WHERE config_key = 'westernDuelMaintenance'; - --- 新增用户流水管理页面菜单 -INSERT INTO sys_menu (menu_name, parent_id, order_num, path, component, menu_type, visible, status, perms, icon, create_by, create_time, remark) -VALUES ('用户流水', 2000, 10, 'userTurnover', 'skins/ttuser/userTurnover', 'C', '0', '0', 'system:user:turnover', 'money', 'admin', NOW(), '用户流水统计页面'); - --- ========== 招商主播报表系统 ========== - --- 1. 用户来源日报表(按user_id+source+report_date统计累计流水) -CREATE TABLE `tt_user_source_report` ( - `id` BIGINT NOT NULL AUTO_INCREMENT COMMENT '主键', - `user_id` INT NOT NULL COMMENT '用户ID', - `parent_id` INT DEFAULT NULL COMMENT '邀请人ID(来自tt_user.parent_id)', - `source` INT NOT NULL COMMENT '来源source码(对应TtAccountRecordSource.code)', - `total` DECIMAL(18,2) NOT NULL DEFAULT 0.00 COMMENT '截至当日的累计金额绝对值总和', - `report_date` DATE NOT NULL COMMENT '报表日期', - `create_time` DATETIME DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间', - `update_time` DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间', - PRIMARY KEY (`id`), - UNIQUE KEY `uk_user_source_date` (`user_id`, `source`, `report_date`), - KEY `idx_parent_id` (`parent_id`), - KEY `idx_source` (`source`) -) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='用户来源日报表(按user_id+source统计累计流水)'; - --- 2. 流水归档表(存储7天前的历史流水数据) -CREATE TABLE `tt_user_blend_ercash_archive` ( - `id` BIGINT NOT NULL COMMENT '与原表id一致', - `user_id` INT DEFAULT NULL, - `amount` DECIMAL(18,2) DEFAULT NULL, - `final_amount` DECIMAL(18,2) DEFAULT NULL, - `credits` DECIMAL(18,2) DEFAULT NULL, - `final_credits` DECIMAL(18,2) DEFAULT NULL, - `total` DECIMAL(18,2) DEFAULT NULL, - `source` INT DEFAULT NULL, - `type` INT DEFAULT NULL, - `remark` VARCHAR(500) DEFAULT NULL, - `create_time` DATETIME DEFAULT NULL, - `update_time` DATETIME DEFAULT NULL, - `archive_time` DATETIME DEFAULT CURRENT_TIMESTAMP COMMENT '归档时间', - PRIMARY KEY (`id`) -) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci COMMENT='流水归档表(7天前的历史流水)'; - --- 3. 注册定时任务到 sys_job 表 - --- 用户来源日报表生成(每天凌晨3点,增量递推统计各用户各source累计流水) -INSERT INTO sys_job (job_name, job_group, invoke_target, cron_expression, misfire_policy, concurrent, status, create_by, create_time, remark) -VALUES ('用户来源日报表生成', 'REPORT', 'userSourceReportTask.generateDailyReport()', '0 0 3 * * ?', '3', '1', '0', 'admin', NOW(), '每天凌晨3点生成用户来源日报表,增量递推统计累计流水'); - --- 流水数据归档(每天凌晨5点,将7天前的流水迁移到归档表) -INSERT INTO sys_job (job_name, job_group, invoke_target, cron_expression, misfire_policy, concurrent, status, create_by, create_time, remark) -VALUES ('流水数据归档', 'ARCHIVE', 'flowArchiveTask.archiveOldData()', '0 0 5 * * ?', '3', '1', '0', 'admin', NOW(), '每天凌晨5点将7天前的流水数据迁移到归档表,分批1000条'); - --- 荒野争锋(西部对决)奖励池同步(每5分钟执行一次) -INSERT INTO sys_job (job_name, job_group, invoke_target, cron_expression, misfire_policy, concurrent, status, create_by, create_time, remark) -VALUES ('荒野争锋奖励池同步', 'BULLET', 'bulletSettleTask.syncRewardPool()', '0 */5 * * * ?', '3', '1', '0', 'admin', NOW(), '每5分钟把Redis奖励池增量同步到game_bullet_win表'); - --- 荒野争锋(西部对决)未结算兜底(每5分钟执行一次) -INSERT INTO sys_job (job_name, job_group, invoke_target, cron_expression, misfire_policy, concurrent, status, create_by, create_time, remark) -VALUES ('荒野争锋未结算兜底', 'BULLET', 'bulletSettleTask.closeTimeoutRecords()', '0 */5 * * * ?', '3', '1', '0', 'admin', NOW(), '每5分钟扫描超30分钟未结算的status=0记录并自动兜底结算'); - --- VIP流水返利发放(每天凌晨0点10分,按昨日游戏流水总额 × VIP等级 flow_rebate_percent 统一发放,取代原实时发放) -INSERT INTO sys_job (job_name, job_group, invoke_target, cron_expression, misfire_policy, concurrent, status, create_by, create_time, remark) -VALUES ('VIP流水返利发放', 'VIP_REBATE', 'vipFlowRebateTask.grantDailyVipRebate()', '0 10 0 * * ?', '3', '1', '0', 'admin', NOW(), '每天凌晨0点10分发放昨日游戏流水的VIP返利,白号不发放,含当日幂等保护'); - --- 修复 tt_user_blend_ercash_archive 与 tt_user_blend_ercash 排序规则不一致导致的 UNION 报错 -ALTER TABLE tt_user_blend_ercash_archive CONVERT TO CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci; - --- ========== 流水归档保留天数可配置化 ========== - --- 推广后台:网站设置新增“流水归档保留天数”配置项(兜底 7 天,超过该天数的流水迁移至归档表) -INSERT INTO sys_config (config_id, config_name, config_key, config_value, config_type, create_by, create_time, remark) -SELECT 137, '流水归档保留天数', 'flowArchiveRetainDays', '7', 'Y', 'admin', NOW(), '流水归档保留天数,超过该天数的流水迁移至归档表,兜底 7' -FROM DUAL WHERE NOT EXISTS (SELECT 1 FROM sys_config WHERE config_key = 'flowArchiveRetainDays'); - - --- 归档明细页菜单:余额变动-归档 / 积分变动-归档 -INSERT INTO `chengdu`.`sys_menu` (`menu_id`, `menu_name`, `parent_id`, `order_num`, `path`, `component`, `query`, `is_frame`, `is_cache`, `menu_type`, `visible`, `status`, `perms`, `icon`, `create_by`, `create_time`, `update_by`, `update_time`, `remark`) VALUES (3104, '余额变动-归档', 2000, 11, 'blendErcashArchiveAmount', 'skins/ttuser/blendErcashArchive', NULL, 1, 0, 'C', '0', '0', 'admin:blendErcash:archiveList', 'money', 'admin', '2026-07-07 14:46:52', '', NULL, '余额变动归档明细页(仅归档表)'); -INSERT INTO `chengdu`.`sys_menu` (`menu_id`, `menu_name`, `parent_id`, `order_num`, `path`, `component`, `query`, `is_frame`, `is_cache`, `menu_type`, `visible`, `status`, `perms`, `icon`, `create_by`, `create_time`, `update_by`, `update_time`, `remark`) VALUES (3105, '积分变动-归档', 2000, 12, 'blendErcashArchiveCredits', 'skins/ttuser/blendErcashArchive', NULL, 1, 0, 'C', '0', '0', 'admin:blendErcash:archiveList', 'star', 'admin', '2026-07-07 14:46:57', '', NULL, '积分变动归档明细页(仅归档表)'); - - --- 对战宝箱-查看奖项 页面性能优化:tt_box_records 复合索引 -ALTER TABLE tt_box_records -ADD INDEX idx_tbr_box_ornament_time (box_id, ornament_id, create_time); - --- /admin/blendErcash/list 收支明细页面性能优化:tt_user_blend_ercash 索引 --- 执行前先 SHOW INDEX FROM tt_user_blend_ercash; 确认无等价索引; --- 1) 默认打开页面无筛选时按 create_time 倒序分页,解决全表扫描+filesort(核心) -ALTER TABLE tt_user_blend_ercash -ADD INDEX idx_tube_ctime (create_time); --- 2) 常见「按用户ID查流水」+ 时间倒序场景 -ALTER TABLE tt_user_blend_ercash -ADD INDEX idx_tube_user_ctime (user_id, create_time); --- 3) 归档表 /admin/blendErcash/archive/list 同样按 create_time 倒序,归档表当前仅有主键 -ALTER TABLE tt_user_blend_ercash_archive -ADD INDEX idx_tubea_ctime (create_time); -ALTER TABLE tt_user_blend_ercash_archive -ADD INDEX idx_tubea_user_ctime (user_id, create_time); - --- 首页统计折线图 /admin/rechargeRecord/chartData 性能优化:tt_user_blend_ercash 按 source+时间聚合 --- 执行前先 SHOW INDEX FROM tt_user_blend_ercash; 确认无等价索引; --- 命中 WHERE source = ? AND create_time >= ? AND create_time < ? GROUP BY source(等值列 source 在前、范围列 create_time 在后) -ALTER TABLE tt_user_blend_ercash -ADD INDEX idx_tube_source_ctime (source, create_time); - --- 首页折线图按周维度(近4周=28天)会 UNION 归档表聚合:归档表补 (source, create_time) 复合索引, --- 让 UNION 归档侧的按 source+时间聚合也走索引(归档表原本仅有 create_time / user_id+create_time 索引) -ALTER TABLE tt_user_blend_ercash_archive -ADD INDEX idx_tubea_source_ctime (source, create_time); - --- ========== 招商佣金周结改造 ========== - --- 扩展佣金结算表,记录结算周期与流水快照 -ALTER TABLE tt_commission_record -ADD COLUMN settlement_start_time DATETIME DEFAULT NULL COMMENT '结算周期开始时间', -ADD COLUMN settlement_end_time DATETIME DEFAULT NULL COMMENT '结算周期结束时间', -ADD COLUMN total_flow DECIMAL(18,2) DEFAULT 0.00 COMMENT '结算周期内参与计算的流水总额', -ADD COLUMN rate DECIMAL(10,4) DEFAULT 0.0000 COMMENT '结算时综合费率快照(%)'; - --- 注册招商佣金周结定时任务(后台:系统监控 > 定时任务) -INSERT INTO sys_job (job_name, job_group, invoke_target, cron_expression, misfire_policy, concurrent, status, create_by, create_time, remark) -VALUES ('招商佣金周结', 'PROMO', 'commissionSettlementTask.settleWeeklyCommission()', '0 30 0 ? * MON', '0', '1', '0', 'admin', NOW(), '每周一 00:30 结算招商账号上周佣金(上周一 00:00 ~ 本周一 00:00)'); - --- ========== 极速糖果箱子饰品分布模板持久化(模板从前端 localStorage 迁移到后端) ========== --- 用途:保存每个箱子的饰品回合分布模板;箱子库存抽干时,按该箱子「最近一次应用的生效模板(is_active=1)」重新填桶并重算总回合。 -CREATE TABLE IF NOT EXISTS game_sweet_box_template ( - id BIGINT NOT NULL AUTO_INCREMENT COMMENT 'PK', - box_id BIGINT NOT NULL COMMENT '所属箱子ID', - name VARCHAR(100) NOT NULL COMMENT '模板名称(同一箱子内唯一)', - total_rounds INT NOT NULL DEFAULT 0 COMMENT '总回合数 = items 各 count 之和', - items_json TEXT NOT NULL COMMENT '饰品分布JSON: [{"ornamentId":1,"count":100},...]', - is_active TINYINT NOT NULL DEFAULT 0 COMMENT '是否为该箱子生效模板(抽干补桶依据): 1=是 0=否', - create_time DATETIME DEFAULT CURRENT_TIMESTAMP, - PRIMARY KEY (id), - UNIQUE KEY uk_box_name (box_id, name), - KEY idx_box_active (box_id, is_active) -) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci COMMENT='极速糖果箱子饰品分布模板'; - --- ========== 修复:Roll房奖池饰品ID列存不下饰品雪花大ID(如金币饰品 1783581913332) ========== --- 根因:tt_roll_jackpot_ornaments.ornaments_id 旧表定义为 int,无法存储 tt_ornament.id(BIGINT大值), --- 创建Roll房填充奖池时报错:Out of range value for column 'ornaments_id'。改为 BIGINT 与实体 Long 对齐。 -ALTER TABLE tt_roll_jackpot_ornaments MODIFY COLUMN ornaments_id BIGINT COMMENT 'Roll房奖池饰品ID'; - --- ========== 修复:指定获奖用户列类型错误导致写入截断 ========== --- 根因:tt_roll_jackpot_ornaments.designated_user_id 实际列类型为数值型(int/bigint), --- 而实体字段为 String 且业务支持多个用户ID用英文逗号分隔(如 '874624,123456'), --- 推广后台"指定获奖用户"保存时报错:Data truncated for column 'designated_user_id'。 --- 改为 VARCHAR(500) 与实体定义及第198行的ADD COLUMN定义对齐。 -ALTER TABLE tt_roll_jackpot_ornaments MODIFY COLUMN designated_user_id VARCHAR(500) DEFAULT NULL COMMENT '指定获奖用户ID,多个用英文逗号分隔,NULL表示不指定(随机分配)'; - --- ========== 修复:机器人VIP等级统一固定为 1-10(保证大厅/房间/观战/所有提示显示一致) ========== --- 根因:机器人创建时未设置 vip_level(为 NULL),而对战落座时曾用 ThreadLocalRandom 随机 0-13, --- 大厅列表与观战又从 tt_user.vip_level 读取(NULL→0),导致同一机器人在不同场景 VIP 显示不一致。 --- 现改为:机器人 VIP 在创建时固定入库为 1-10,所有场景统一读 tt_user.vip_level。 --- 本脚本为历史机器人(user_type='03')补齐固定的 1-10 随机 VIP(每个机器人固定一个值,不再动态变化)。 -UPDATE tt_user -SET vip_level = FLOOR(1 + RAND() * 10) -WHERE user_type = '03' AND (vip_level IS NULL OR vip_level < 1 OR vip_level > 10); - --- ========== 修复:管理后台Roll房奖池 total_price 存量数据(填货后未维护导致为0) ========== --- 根因:管理后台批量填货/单个新增/删除奖池饰品时未重算 tt_roll_jackpot.total_price, --- 导致奖池总价值一直为0,前端显示“奖池总价值 0”且主播房创建时不扣款。 --- 代码已修复(填货/新增/删除后都会重算),本脚本用于一次性修复历史存量奖池。 -UPDATE tt_roll_jackpot j -SET j.total_price = ( - SELECT COALESCE(SUM(o.price * o.ornaments_num), 0) - FROM tt_roll_jackpot_ornaments o - WHERE o.jackpot_id = j.jackpot_id -); - - -ALTER TABLE tt_roll ADD COLUMN deduct_credits DECIMAL(10,2) NOT NULL DEFAULT 0.00 -COMMENT '主播房创建时实际扣除的积分,用于开奖/删除按比例退还'; - --- ========== 每日福利箱子发放:@Scheduled 迁移到 sys_job 统一调度 ========== --- 原 WelfareBoxTask.autoIssueDailyWelfareBox() 使用硬编码 @Scheduled(cron="0 0 0 * * ?"), --- 现改为 sys_job 可配置调度,便于后台系统监控>定时任务动态启停/调整 cron。 -INSERT INTO sys_job (job_name, job_group, invoke_target, cron_expression, misfire_policy, concurrent, status, create_by, create_time, remark) -VALUES ('每日福利箱子发放', 'WELFARE', 'welfareBoxTask.autoIssueDailyWelfareBox()', '0 0 0 * * ?', '3', '1', '0', 'admin', NOW(), '每天0点为所有正常用户发放对应VIP等级的福利箱子'); - --- ========== 官方周期Roll房:配置驱动 + 定时自动创建 ========== --- 需求:官方周roll(一周一次)、日roll(一天一次)自动创建,创建时按周期设置充值统计起点(recharge_start_time) --- 与开奖时间(end_time)。日roll统计"今日充值",周roll统计"本周充值",门槛复用 tt_roll.min_recharge。 --- 复用现有 tt_roll 房间体系、加入校验(joinRollCheck)与到期开奖(RollTask),不引入并行体系。 - --- 1. 官方周期Roll配置表(模板,可配置多条,各自按周期自动再生房间) -CREATE TABLE `tt_official_roll_config` ( - `id` INT NOT NULL AUTO_INCREMENT, - `config_name` VARCHAR(100) NOT NULL COMMENT '配置标识(便于后台识别)', - `jackpot_id` INT NOT NULL COMMENT '指定奖池ID(tt_roll_jackpot.jackpot_id)', - `period_type` VARCHAR(16) NOT NULL COMMENT '周期类型: DAILY=每日, WEEKLY=每周', - `room_name` VARCHAR(100) NOT NULL COMMENT '生成的Roll房名称', - `description` VARCHAR(500) DEFAULT NULL COMMENT '房间描述', - `roll_password` VARCHAR(50) DEFAULT NULL COMMENT '参与密码(可空)', - `min_recharge` DECIMAL(12,2) NOT NULL DEFAULT 0.00 COMMENT '充值门槛金额,0=无门槛(日=今日充值,周=本周充值)', - `vip_level` INT DEFAULT 0 COMMENT 'VIP等级门槛,0=不限', - `people_num` INT NOT NULL DEFAULT 100 COMMENT '人数上限', - `sort_by` INT DEFAULT 0 COMMENT '排序依据', - `status` CHAR(1) NOT NULL DEFAULT '0' COMMENT '状态: 0=启用, 1=停用', - `last_generate_time` DATETIME DEFAULT NULL COMMENT '上次成功生成房间的时间(幂等辅助)', - `create_by` VARCHAR(50) DEFAULT NULL COMMENT '创建者', - `create_time` DATETIME DEFAULT CURRENT_TIMESTAMP, - `update_by` VARCHAR(50) DEFAULT NULL COMMENT '更新者', - `update_time` DATETIME DEFAULT NULL, - `del_flag` CHAR(1) DEFAULT '0' COMMENT '删除标志: 0=存在, 2=删除', - PRIMARY KEY (`id`), - KEY `idx_status_period` (`status`, `period_type`), - KEY `idx_jackpot_id` (`jackpot_id`) -) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='官方周期Roll房配置模板'; - --- 2. tt_roll 增列 official_config_id:标记该房由哪条配置生成(溯源 + 幂等去重键的一部分) -ALTER TABLE tt_roll ADD COLUMN official_config_id INT DEFAULT NULL COMMENT '官方周期Roll配置ID(tt_official_roll_config.id),手动创建的房间为NULL'; --- 幂等去重键:同一配置 + 同一充值统计起点 至多生成一个房间 -ALTER TABLE tt_roll ADD INDEX idx_official_config_start (official_config_id, recharge_start_time); - --- 3. 注册定时任务(后台:系统监控 > 定时任务) --- 日roll生成:每天 00:01 为所有启用的 DAILY 配置创建当日房间 -INSERT INTO sys_job (job_name, job_group, invoke_target, cron_expression, misfire_policy, concurrent, status, create_by, create_time, remark) -VALUES ('官方日Roll生成', 'ROLL', 'officialRollTask.generateDailyRolls()', '0 1 0 * * ?', '3', '1', '0', 'admin', NOW(), '每天00:01为启用的DAILY配置创建官方日roll房,起点=当天0点,开奖=次日0点'); - --- 周roll生成:每周一 00:10 为所有启用的 WEEKLY 配置创建本周房间 -INSERT INTO sys_job (job_name, job_group, invoke_target, cron_expression, misfire_policy, concurrent, status, create_by, create_time, remark) -VALUES ('官方周Roll生成', 'ROLL', 'officialRollTask.generateWeeklyRolls()', '0 10 0 ? * MON', '3', '1', '0', 'admin', NOW(), '每周一00:10为启用的WEEKLY配置创建官方周roll房,起点=本周一0点,开奖=下周一0点'); - --- 4. 后台菜单:在“Roll房列表”所在目录下新增“官方周期Roll配置”页面(作为其兄弟菜单,parent 自动取现有Roll房列表页的父级) --- 菜单驱动路由:component 指向前端 src/views/skins/ttRoll/officialRollConfig.vue -INSERT INTO sys_menu (menu_name, parent_id, order_num, path, component, is_frame, is_cache, menu_type, visible, status, perms, icon, create_by, create_time, remark) -SELECT '官方周期Roll配置', m.parent_id, 20, 'officialRollConfig', 'skins/ttRoll/officialRollConfig', 1, 0, 'C', '0', '0', 'admin:officialRollConfig:list', 'time', 'admin', NOW(), '官方周期Roll配置页面(定时自动创建官方日/周Roll房)' -FROM (SELECT parent_id FROM sys_menu WHERE component = 'skins/ttRoll/index' LIMIT 1) m; - -INSERT INTO sys_menu (menu_name, parent_id, order_num, path, component, is_frame, is_cache, menu_type, visible, status, perms, icon, create_by, create_time, remark) -SELECT '支付订单', m.menu_id, 13, 'ttOrder', 'skins/ttOrder/index', 1, 0, 'C', '0', '0', 'admin:order:list', 'money', 'admin', NOW(), '网站支付订单查询页面(可按状态查看订单)' -FROM (SELECT menu_id FROM sys_menu WHERE path = 'currency' AND menu_type = 'M' LIMIT 1) m; - --- ========== 主播推广CDKEY 过期/兑完 精确退还 ========== --- 需求:CDK创建时按 amountMax×num×createNum 等额预扣主播金币+积分;兑换时仅随机派发 min~max 金币。 --- 当CDK过期(validity到期)或兑完(usable_count用尽)后,按「预扣总额 - 实际已派发金币总额」精确返还 --- 主播的金币与积分(同额),由定时任务统一兜底,refund_time 幂等防重复。 - --- 1. promo_cdkey 增加退还标记字段(MySQL 不支持 ADD COLUMN IF NOT EXISTS,直接 ADD) -ALTER TABLE promo_cdkey - ADD COLUMN refund_amount DECIMAL(18,2) NULL COMMENT 'CDK结束退还金额(金币/积分同额)', - ADD COLUMN refund_time DATETIME NULL COMMENT '退还时间(NULL=未退还)'; - --- 2. 注册定时任务(后台:系统监控 > 定时任务):每10分钟扫描已结束(过期/兑完)且未退还的CDK并精确退还 -INSERT INTO sys_job (job_name, job_group, invoke_target, cron_expression, misfire_policy, concurrent, status, create_by, create_time, remark) -VALUES ('CDK结束退还', 'PROMO', 'promoCdkeyRefundTask.settleEndedCdkeyRefunds()', '0 0/10 * * * ?', '3', '1', '0', 'admin', NOW(), '每10分钟扫描已过期或已兑完且未退还的CDK,按预扣-实际派发精确返还主播金币/积分并通知'); - - --- 转赠申请表 -CREATE TABLE IF NOT EXISTS `tt_transfer_apply` ( -`id` INT AUTO_INCREMENT PRIMARY KEY COMMENT '主键ID', -`batch_id` VARCHAR(64) NOT NULL DEFAULT '' COMMENT '转赠批次号(同一批次内多个饰品一起审核)', -`box_record_id` BIGINT NULL COMMENT '背包记录ID(tt_box_records.id)', -`from_user_id` INT NOT NULL COMMENT '转赠人用户ID', -`to_user_id` INT NOT NULL COMMENT '受赠人用户ID', -`vip_level` INT NOT NULL DEFAULT 0 COMMENT '提交转赠时的VIP等级', -`ornament_id` BIGINT NULL COMMENT '饰品ID', -`ornament_name` VARCHAR(255) NULL COMMENT '饰品名称', -`ornament_image_url` VARCHAR(500) NULL COMMENT '饰品图片URL', -`ornament_price` DECIMAL(10,2) NOT NULL COMMENT '饰品价格(单件超过1000才可转赠)', -`status` TINYINT DEFAULT 0 COMMENT '状态:0-待审核,1-审核通过,2-审核驳回', -`reject_reason` VARCHAR(500) NULL COMMENT '驳回原因', -`create_by` VARCHAR(64) NULL COMMENT '创建者', -`create_time` DATETIME DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间', -`update_by` VARCHAR(64) NULL COMMENT '更新者', -`update_time` DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间', -INDEX `idx_batch_id` (`batch_id`), -INDEX `idx_from_user_id` (`from_user_id`), -INDEX `idx_to_user_id` (`to_user_id`), -INDEX `idx_status` (`status`), -INDEX `idx_create_time` (`create_time`) -) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci COMMENT='转赠申请表(按批次聚合审核)'; - --- 如果表已存在但缺少 box_record_id 列,执行以下语句: --- ALTER TABLE `tt_transfer_apply` ADD COLUMN `box_record_id` BIGINT NULL COMMENT '背包记录ID(tt_box_records.id)' AFTER `id`; - --- ============================================================ --- 菜单配置(请根据实际数据库中的父菜单ID调整 parent_id) --- 查找"三方发货链路"父菜单ID:SELECT menu_id, menu_name FROM sys_menu WHERE menu_name LIKE '%三方发货%' OR menu_name LIKE '%发货%'; --- ============================================================ - --- 新增"转赠申请"菜单项(menu_type=C 表示菜单) --- 注意:parent_id 需替换为实际的"三方发货链路"目录菜单ID --- component 路径对应 ruoyi-ui/src/views/skins/delivery/transferApply.vue -INSERT INTO `sys_menu` ( -`menu_name`, -`parent_id`, -`order_num`, -`path`, -`component`, -`is_frame`, -`is_cache`, -`menu_type`, -`visible`, -`status`, -`perms`, -`icon`, -`create_by`, -`create_time`, -`remark` -) -VALUES -( -'转赠申请', -( SELECT menu_id FROM ( SELECT menu_id FROM sys_menu WHERE menu_name LIKE '%三方发货%' LIMIT 1 ) AS tmp ),-- 替换为实际的父菜单ID -10, -'transferApply', -'skins/delivery/transferApply', -1, -0, -'C', -'0', -'0', -'skins:delivery:transferApply', -'transfer', -'admin', -NOW( ), -'转赠申请管理页面' -);-- 新增按钮权限 --- 同意按钮 -INSERT INTO `sys_menu` ( -`menu_name`, -`parent_id`, -`order_num`, -`path`, -`component`, -`is_frame`, -`is_cache`, -`menu_type`, -`visible`, -`status`, -`perms`, -`icon`, -`create_by`, -`create_time`, -`remark` -) -VALUES -( -'转赠审核通过', -( SELECT menu_id FROM ( SELECT menu_id FROM sys_menu WHERE menu_name = '转赠申请' LIMIT 1 ) AS tmp ), -1, -'', -NULL, -1, -0, -'F', -'0', -'0', -'skins:delivery:transferApply:approve', -'#', -'admin', -NOW( ), -'' -);-- 驳回按钮 -INSERT INTO `sys_menu` ( -`menu_name`, -`parent_id`, -`order_num`, -`path`, -`component`, -`is_frame`, -`is_cache`, -`menu_type`, -`visible`, -`status`, -`perms`, -`icon`, -`create_by`, -`create_time`, -`remark` -) -VALUES -( -'转赠审核驳回', -( SELECT menu_id FROM ( SELECT menu_id FROM sys_menu WHERE menu_name = '转赠申请' LIMIT 1 ) AS tmp ), -2, -'', -NULL, -1, -0, -'F', -'0', -'0', -'skins:delivery:transferApply:reject', -'#', -'admin', -NOW( ), -'' -); - --- 新增"已处理转赠申请"菜单项(与"转赠申请"同级) --- component 路径对应 ruoyi-ui/src/views/skins/delivery/transferApplyProcessed.vue -INSERT INTO `sys_menu` ( -`menu_name`, -`parent_id`, -`order_num`, -`path`, -`component`, -`is_frame`, -`is_cache`, -`menu_type`, -`visible`, -`status`, -`perms`, -`icon`, -`create_by`, -`create_time`, -`remark` -) -VALUES -( -'已处理转赠申请', -( SELECT menu_id FROM ( SELECT menu_id FROM sys_menu WHERE menu_name LIKE '%三方发货%' LIMIT 1 ) AS tmp ), -11, -'transferApplyProcessed', -'skins/delivery/transferApplyProcessed', -1, -0, -'C', -'0', -'0', -'skins:delivery:transferApplyProcessed', -'transfer', -'admin', -NOW( ), -'已处理转赠申请管理页面' -); - --- 全服背包饰品批量分解定时任务(每天凌晨2点,分解所有用户背包中的饰品,等额返还金币和积分,跳过正在提货中的饰品和机器人用户) --- 分解记录通过 tt_user_blend_ercash 流水表记录(source=10 背包分解饰品收入) -INSERT INTO sys_job (job_name, job_group, invoke_target, cron_expression, misfire_policy, concurrent, status, create_by, create_time, remark) -VALUES ('全服背包饰品分解', 'DECOMPOSE', 'boxRecordsDecomposeTask.decomposeAllUserPackSack()', '0 0 2 * * ?', '3', '1', '1', 'admin', NOW(), '每天凌晨2点分解所有用户背包中的饰品(status IN 0,2),等额增加金币和积分,写入分解流水记录;跳过正在提货中的饰品和机器人用户'); - --- ========== 转赠记录增加状态列 ========== --- 前端「赠送记录」三态:0-审核中,1-成功,2-失败 -ALTER TABLE `tt_box_transfer_record` ADD COLUMN `status` TINYINT NOT NULL DEFAULT 0 COMMENT '转赠状态:0-审核中,1-成功,2-失败'; -ALTER TABLE `tt_box_transfer_record` ADD INDEX `idx_status` (`status`); - --- 删除唯一索引:同一个饰品可多次转赠给同一副号(如驳回后再转赠),每次应独立成记录 -ALTER TABLE `tt_box_transfer_record` DROP INDEX `idx_src_dst_boxid`; - --- 对战机器人爆率分离:新增欧皇/非酋独立爆率字段 -ALTER TABLE tt_box_ornaments ADD COLUMN robot_odds_lucky INT DEFAULT 0 COMMENT '机器人欧皇模式爆率'; -ALTER TABLE tt_box_ornaments ADD COLUMN robot_odds_unlucky INT DEFAULT 0 COMMENT '机器人非酋模式爆率'; - --- 数据回填:初始值等于原 robot_odds -UPDATE tt_box_ornaments SET robot_odds_lucky = robot_odds, robot_odds_unlucky = robot_odds; - --- ========== VIP 等级新增每日转赠次数配置 ========== -ALTER TABLE tt_vip_level ADD COLUMN transfer_count INT NOT NULL DEFAULT 0 COMMENT '达到该VIP等级后每日可转赠次数'; - --- ========== VIP 等级新增每日提货次数配置 ========== -ALTER TABLE tt_vip_level ADD COLUMN delivery_count INT NOT NULL DEFAULT 0 COMMENT '达到该VIP等级后每日可提货次数'; - --- ========== 用户每日转赠次数记录 ========== --- 按 (用户ID, VIP等级, 日期) 记录,升级后不同 VIP 等级的次数独立计算 -CREATE TABLE IF NOT EXISTS tt_user_transfer_daily ( - id BIGINT NOT NULL AUTO_INCREMENT COMMENT '主键ID', - user_id INT NOT NULL COMMENT '用户ID', - vip_level INT NOT NULL DEFAULT 0 COMMENT '提交/生效时的VIP等级(tt_vip_level.id)', - stat_date DATE NOT NULL COMMENT '统计日期', - total_count INT NOT NULL DEFAULT 0 COMMENT '该VIP等级当日转赠次数上限', - used_count INT NOT NULL DEFAULT 0 COMMENT '该VIP等级当日已成功转赠次数', - pending_count INT NOT NULL DEFAULT 0 COMMENT '该VIP等级当日审核中预扣次数', - delivery_total_count INT NOT NULL DEFAULT 0 COMMENT '该VIP等级当日提货次数上限', - delivery_used_count INT NOT NULL DEFAULT 0 COMMENT '该VIP等级当日已成功提货次数', - delivery_pending_count INT NOT NULL DEFAULT 0 COMMENT '该VIP等级当日审核中提货预扣次数', - create_time DATETIME DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间', - update_time DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间', - PRIMARY KEY (id), - UNIQUE KEY uk_user_vip_date (user_id, vip_level, stat_date), - INDEX idx_stat_date (stat_date) -) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci COMMENT='用户每日转赠/提货次数记录(按VIP等级隔离)'; - --- 若此前已按旧结构 (user_id, stat_date) 建表,执行以下迁移语句 --- ALTER TABLE tt_user_transfer_daily DROP PRIMARY KEY; --- ALTER TABLE tt_user_transfer_daily ADD COLUMN id BIGINT NOT NULL AUTO_INCREMENT PRIMARY KEY FIRST; --- ALTER TABLE tt_user_transfer_daily ADD UNIQUE KEY uk_user_vip_date (user_id, vip_level, stat_date); - --- ========== 转赠/提货次数拆分上线初始化 ========== --- 1. 把 VIP 等级表中的 delivery_count 初始化为和 transfer_count 一致(避免上线后用户无法提货) -UPDATE tt_vip_level SET delivery_count = transfer_count WHERE delivery_count = 0; - --- 2. 把已有每日次数记录中的 delivery_total_count 初始化为对应 VIP 等级的 delivery_count -UPDATE tt_user_transfer_daily t -JOIN tt_vip_level v ON t.vip_level = v.id -SET t.delivery_total_count = v.delivery_count -WHERE t.delivery_total_count = 0; - --- 转赠申请表记录提交时的 VIP 等级,审核时按该等级扣/返次数 -ALTER TABLE tt_transfer_apply ADD COLUMN vip_level INT NOT NULL DEFAULT 0 COMMENT '提交转赠时的VIP等级'; - --- 转赠申请表增加批次号,同一批次内的饰品需整体审核通过/驳回 -ALTER TABLE tt_transfer_apply ADD COLUMN batch_id VARCHAR(64) NOT NULL DEFAULT '' COMMENT '转赠批次号(同一批次内多个饰品一起审核)'; -ALTER TABLE tt_transfer_apply ADD INDEX idx_batch_id (batch_id); - --- 用户表新增赠送状态:0=启用(可被赠送),1=禁用(不可被赠送) -ALTER TABLE tt_user ADD COLUMN gift_status VARCHAR(1) NOT NULL DEFAULT '0' COMMENT '赠送状态:0=启用,1=禁用'; -## 极速糖果领取流程简化 - 存量数据迁移 - -### 部署前执行(处理待决定记录) - -```sql --- 1. 查看待处理记录数量(先确认影响范围) -SELECT COUNT(*) AS pending_count FROM game_sweet_record -WHERE settled = 0 AND auto_dismantle = 0 AND prize_name IS NOT NULL; - --- 2. 将待处理记录标记为已结算(reward_type=ornament) -UPDATE game_sweet_record -SET settled = 1, - reward_type = 'ornament', - settle_source = 'data_migration' -WHERE settled = 0 - AND auto_dismantle = 0 - AND prize_name IS NOT NULL; - --- 3. 补写背包记录(对于有 prize_id 的记录写入 tt_box_records) -INSERT INTO tt_box_records (user_id, holder_user_id, box_id, box_name, ornament_id, - ornament_name, ornaments_price, image_url, ornaments_level_id, source, status, is_show, create_time) -SELECT user_id, user_id, box_id, 'SWEET', prize_id, prize_name, settled_win, - prize_image_url, 1, 11, 0, 1, NOW() -FROM game_sweet_record -WHERE settle_source = 'data_migration' AND prize_id IS NOT NULL; - --- 4. 验证迁移结果 -SELECT COUNT(*) AS migrated_records FROM game_sweet_record WHERE settle_source = 'data_migration'; -SELECT COUNT(*) AS new_box_records FROM tt_box_records WHERE source = 11 AND DATE(create_time) = CURDATE(); -``` - -新增推广后台维护配置 -INSERT INTO sys_config (config_name, config_key, config_value, config_type, create_by, create_time, remark) -VALUES ('推广后台维护', 'promoMaintenance', '0', 'Y', 'admin', NOW(), '推广后台维护开关 0正常 1维护'); - - --- 推广后台-Roll房中奖详情接口性能优化:tt_box_records 复合索引 --- 查询模式:WHERE roll_id = ? AND source = ? ORDER BY create_time(等值列在前、排序列在后) --- 执行前先 SHOW INDEX FROM tt_box_records; 确认无等价索引;大表请加索引在低峰期执行 -ALTER TABLE tt_box_records -ADD INDEX idx_tbr_roll_source_time (roll_id, source, create_time); - --- 疯狂转盘全服中奖记录过滤白号和机器人 -ALTER TABLE game_wheel_round_bet ADD INDEX idx_win_filter (win, user_id); --- 优化查询性能:WHERE win > 0 AND user 状态过滤 - - --- VIP归档兼容性修复:tt_user增加累计充值缓存字段(避免归档后VIP归零和未充值误判) -ALTER TABLE tt_user ADD COLUMN total_recharge_amount DECIMAL(14,2) NOT NULL DEFAULT 0.00 COMMENT '累计充值金额(缓存,充值时原子递增)'; - --- 初始化回填:从主表+归档表计算历史充值总额(仅更新有充值记录的用户,避免全表扫描) -UPDATE tt_user u SET u.total_recharge_amount = ( - SELECT COALESCE(SUM(sub.total), 0) FROM ( - SELECT total FROM tt_user_blend_ercash WHERE user_id = u.user_id AND type = 0 AND source = 1 - UNION ALL - SELECT total FROM tt_user_blend_ercash_archive WHERE user_id = u.user_id AND type = 0 AND source = 1 - ) sub -) -WHERE EXISTS ( - SELECT 1 FROM tt_user_blend_ercash WHERE user_id = u.user_id AND type = 0 AND source = 1 -) OR EXISTS ( - SELECT 1 FROM tt_user_blend_ercash_archive WHERE user_id = u.user_id AND type = 0 AND source = 1 -); - --- 2026-07-22 修复CDK兑换重复检查并发漏洞,添加唯一索引兄底 -ALTER TABLE promo_cdkey_redeem -ADD UNIQUE KEY uk_cdkey_redeemer (cdkey_id, redeemer_id); - --- 2026-07-22 修复口令红包重复领取漏洞,添加唯一索引兄底 -ALTER TABLE tt_red_packet_record -ADD UNIQUE KEY uk_packet_user (red_packet_id, user_id); - --- 2026-07-22 后台菜单:在“cdk管理”目录下新增“推广CDK记录”子菜单 -INSERT INTO sys_menu (menu_name, parent_id, order_num, path, component, is_frame, is_cache, menu_type, visible, status, perms, icon, create_by, create_time, remark) -SELECT '推广CDK记录', m.parent_id, 4, 'promoCdkeyRedeem', 'skins/promoCdkeyRedeem/index', 1, 0, 'C', '0', '0', '', '#', 'admin', NOW(), '推广后台CDKEY兑换记录' -FROM (SELECT parent_id FROM sys_menu WHERE component = 'skins/ttGenerateCard/index' LIMIT 1) m; - - --- 2026-07-22 百佳与历史接口性能优化索引 -ALTER TABLE tt_box_records ADD INDEX idx_tbr_fight_id (fight_id); -ALTER TABLE tt_fight ADD INDEX idx_ttf_status_model_endtime (status, model, end_time); -ALTER TABLE tt_fight_user ADD INDEX idx_tfu_user_fight (user_id, fight_id); - --- 2026-07-24 极速糖果一轮内RTP动态收敛:箱子新增收敛回合占比(空=默认0.15,逐箱可配) -ALTER TABLE game_sweet_box ADD COLUMN rtp_converge_ratio DECIMAL(4,3) NULL COMMENT '一轮收敛回合占比,空=默认0.15'; - --- 2026-07-25 abkpay:接入新代收支付渠道并纳入统一支付权重分配 --- 说明:abkpay 是否被 /api/recharge/unifiedPay 路由选中由本配置决定(PayCompanyRouter 读取 payCompanyWeights); --- 不在列表或 weight<=0 视为停用;weight 按需调整;payType 可选(1=支付宝,2=银联,不填=全渠道,abkpay 不限渠道故不填)。 --- 注意:本语句为整段 JSON 覆盖,若线上权重已被运营调整过,请以现有值为基础追加 {"name":"abkpay","weight":N} 项后再执行。 --- 示例:chengduhipay:5、richku:3(仅支付宝)、itpay:2、abkpay:2 -UPDATE sys_config -SET config_value = '[{"name":"chengduhipay","weight":5},{"name":"richku","weight":3,"payType":1},{"name":"itpay","weight":2},{"name":"abkpay","weight":2}]' -WHERE config_key = 'payCompanyWeights'; - --- 2026-07-25 支付路由按金额分层调整:<300 默认走 itpay(取代原“支付宝<300强制 richku”规则),>=300 按权重分配; --- abkpay 内部再按金额选产品编码(yml 配置:300<=金额<800 用 wayCode=8012,金额>=800 用 wayCodeHigh=8003)。 --- 注意:itpay 权重必须>0,否则 <300 默认规则失效回落到权重选择;itpay 的权重同时决定其在 >=300 流量中的占比。 --- 示例:itpay:2、abkpay:8 → <300 全部 itpay;>=300 按 2:8 分流(若希望 >=300 基本全走 abkpay,可改配 itpay:1、abkpay:9) -UPDATE sys_config -SET config_value = '[{"name":"itpay","weight":2},{"name":"abkpay","weight":8}]' -WHERE config_key = 'payCompanyWeights'; - --- 2026-07-25 VIP等级奖励防重复领取修复(配合代码:归档任务 selectArchiveIds 永久跳过 source=30) --- 背景:VIP等级达标奖励(source=30)的领取判重只查主表 tt_user_blend_ercash, --- 领取流水被归档后主表查不到,玩家重新登录可重复领取。 --- 方案:领取凭证永久保留在主表;存量已归档的凭证从归档表搬回主表(同一事务先插后删,避免 UNION 双算)。 --- 【踩坑订正】TtAccountRecordType 枚举:INPUT(收入)=1、OUTPUT(支出)=0,与直觉相反! --- 初版 SQL 误写 type=0 导致回填 0 行;凭证是收入流水,此处直接不限 type(source=30 只会以收入入账)。 -START TRANSACTION; - --- 1) 回填主表(id 与归档表一致,IGNORE 防止极端情况下主键冲突报错) -INSERT IGNORE INTO tt_user_blend_ercash - (id, user_id, amount, final_amount, credits, final_credits, total, source, type, remark, create_time, update_time) -SELECT id, user_id, amount, final_amount, credits, final_credits, total, source, type, remark, create_time, update_time -FROM tt_user_blend_ercash_archive -WHERE source = 30; - --- 2) 从归档表删除,避免主表+归档表 UNION ALL 查询时同一条记录双算 -DELETE FROM tt_user_blend_ercash_archive -WHERE source = 30 - AND id IN (SELECT id FROM tt_user_blend_ercash WHERE source = 30); - -COMMIT; - --- 排查:找出已重复领取的玩家(同一用户同一等级 remark 领取超过 1 次,跨主表+归档表),用于追损 -SELECT t.user_id, t.remark, COUNT(*) AS claim_count, SUM(t.amount) AS total_claimed, - MIN(t.create_time) AS first_claim, MAX(t.create_time) AS last_claim -FROM ( - SELECT user_id, remark, amount, create_time FROM tt_user_blend_ercash WHERE source = 30 - UNION ALL - SELECT user_id, remark, amount, create_time FROM tt_user_blend_ercash_archive WHERE source = 30 -) t -GROUP BY t.user_id, t.remark -HAVING COUNT(*) > 1 -ORDER BY claim_count DESC, total_claimed DESC; - - - -## 2026-07-26 机器人对战饰品清出机器人背包 - --- 背景:对战结算时曾把整场 tt_box_records 统一置为 status=0(在背包显示),导致机器人(user_type=03) --- 赢得的对战饰品进入机器人背包。代码已修复(FightStateManager.settleFight / ApiFightServiceImpl.fightEnd / --- fightTask.refreshDayTask 三处结算排除机器人持有记录),此 SQL 清理存量:机器人持有的对战来源饰品 --- 翻回 status=1(背包不显示),不影响前端对战结果展示(结果查询不按 status 过滤)。 -UPDATE tt_box_records tbr -JOIN tt_user tu ON tbr.holder_user_id = tu.user_id -SET tbr.status = 1, tbr.update_time = NOW() -WHERE tu.user_type = '03' - AND tbr.fight_id IS NOT NULL - AND tbr.status = 0; - --- 验证:应返回 0 -SELECT COUNT(*) FROM tt_box_records tbr -JOIN tt_user tu ON tbr.holder_user_id = tu.user_id -WHERE tu.user_type = '03' AND tbr.fight_id IS NOT NULL AND tbr.status = 0; - - - -## 2026-07-26 招商周结防重复结算唯一索引 - --- 背景:CommissionSettlementTask 幂等仅靠 countCommissionRecord 先查后插, --- 存在并发窗口可能重复结算。数据库层面加唯一索引兜底: --- 同一招商(user_id) + 同一结算周期起点(settlement_start_time) 只允许一条记录。 --- 注意:执行前先用下方排查 SQL 确认无存量重复数据,否则建索引会失败。 - --- 排查存量重复(应返回空) -SELECT user_id, settlement_start_time, COUNT(*) AS cnt -FROM tt_commission_record -GROUP BY user_id, settlement_start_time -HAVING COUNT(*) > 1; - --- 新增唯一索引 -ALTER TABLE tt_commission_record - ADD UNIQUE INDEX uk_commission_user_period (user_id, settlement_start_time); - - - -## 2026-07-26 疯狂转盘注单表增加 date 列并重建唯一键 - --- 背景:注单改为 doBet 扣款事务内即落库(settled=0),但 round_id 每日重置, --- 原唯一键 uk_round_user_symbol(round_id,user_id,bet_symbol) 不含日期, --- 跨天同 roundId+用户+符号会插入冲突导致下注失败。 --- 增加 date 列(yyyyMMdd)并将唯一键改为 (date, round_id, user_id, bet_symbol)。 --- 同步修复 selectByDateAndRoundId 改为直接按注单 date 列过滤。 - --- 1. 增加 date 列 -ALTER TABLE game_wheel_round_bet - ADD COLUMN `date` varchar(10) NOT NULL DEFAULT '' COMMENT '所属日期yyyyMMdd(roundId每日重置,需与date联合定位回合)' AFTER `round_id`; - --- 2. 回填存量数据(按创建时间推断所属日期) -UPDATE game_wheel_round_bet SET `date` = DATE_FORMAT(create_time, '%Y%m%d') WHERE `date` = ''; - --- 3. 排查新唯一键的存量重复(应返回空;若有重复需先人工处理,否则建索引失败) -SELECT `date`, round_id, user_id, bet_symbol, COUNT(*) AS cnt -FROM game_wheel_round_bet -GROUP BY `date`, round_id, user_id, bet_symbol -HAVING COUNT(*) > 1; - --- 4. 重建唯一键 -ALTER TABLE game_wheel_round_bet - DROP INDEX uk_round_user_symbol, - ADD UNIQUE INDEX uk_date_round_user_symbol (`date`, round_id, user_id, bet_symbol); - - -## 2026-07-27 修复用户积分为负数的存量数据 - --- 背景:TtUserMapper.subtractAccountAmount 原 SQL 为 --- set account_amount = account_amount - #{money}, --- account_credits = LEAST(account_credits, account_amount - #{money}) --- MySQL 的 SET 子句从左到右求值,第二个赋值中的 account_amount 已是扣减后的新值, --- 实际执行等价于 LEAST(积分, 原金币 - 2×扣款额),单笔扣款超过金币余额一半时积分被扣成负数。 --- 代码已修复(积分赋值前置 + GREATEST 兜底防负),存量负积分数据需执行以下 SQL 归零。 - --- 排查存量负积分用户 -SELECT user_id, user_name, account_amount, account_credits -FROM tt_user -WHERE account_credits < 0; - --- 修复:负积分归零 -UPDATE tt_user SET account_credits = 0 WHERE account_credits < 0; - - -## 2026-07-28 SnapPay代收渠道接入:新增后台开关配置 - --- 背景:SnapPay(snappay168)代收渠道不参与 payCompanyWeights 权重池, --- 充值金额 1000-50000 时强制路由到 SnapPay(ApiRechargeController.unifiedPay); --- 本开关控制该强制路由:1=启用,0=停用(停用/下单失败时自动回落权重路由)。 --- 后台入口:推广后台 → 网站设置 → 支付设置 → SnapPay开关(未执行本SQL时页面保存会自动跳过该项,代码兜底视为启用)。 -INSERT INTO sys_config (config_id, config_name, config_key, config_value, config_type, create_by, create_time, remark) -SELECT 139, 'SnapPay开关', 'snappayEnabled', '1', 'Y', 'admin', NOW(), 'SnapPay代收开关:1=启用(1000-50000强制走SnapPay),0=停用(回落权重路由)' -FROM DUAL WHERE NOT EXISTS (SELECT 1 FROM sys_config WHERE config_key = 'snappayEnabled'); - - -## 2026-07-30 对战新增2v2组队模式 - --- 背景:欧皇/非酋对战新增2v2组队玩法。座位0/1为一队、座位2/3为二队, --- 按队伍累计掉落总价值判胜(欧皇高者胜、非酋低者胜); --- 胜方两人各自保留自己的掉落,并平分败方掉落总价值(复用平分奖机制); --- 败方仅获得安慰奖(fightLoserPrize 配置)。 --- team_mode:0或NULL=个人对战(存量数据不受影响),1=2v2组队(创建时强制 player_num=4)。 -ALTER TABLE tt_fight ADD COLUMN team_mode TINYINT DEFAULT 0 COMMENT '组队模式:0=个人对战 1=2v2组队(座位0/1一队、座位2/3二队)'; - - -## 2026-07-31 Olympus 电子游戏模块建表 - --- 背景:新增 Olympus 消除类电子游戏(类 sweet 体系)。全量 DDL 见 olympus-py/schema.sql,已在本地开发库执行。 --- 表清单(11张,前缀 game_olympus_):box / ornament(17档桶 0x~1000x 含种子) / box_ornament(含 used) --- / round / step / record(建表即带 settled 等结算字段) / user / win / prize_ornament / prize_range / box_template --- 设计要点:三模式互斥 normal|ante|buy_fs(CM=1/2/20);单局封顶 1000x; --- step 表自带 accumulator_before/after(跨回合炸弹累计)与 win_capped(封顶截断标记)。 --- Java 服务接入时再执行(语句注释在 schema.sql 尾部): --- sys_config 新增 olympusMaintenance 维护开关。 - - -## 2026-08-01 Olympus Java 服务接入(game-olympus-service) - --- 背景:olympus Java 游戏服务已建成并编译通过。以下为上线需执行的配置。 --- 账户流水源:TtAccountRecordSource 新增 OLYMPUS_SPIN(60,消费) / OLYMPUS_REWARD(61,奖励); --- 背包源 TtboxRecordSource 新增 OLYMPUS_BOX(13)。(枚举已在代码中,无需 SQL) - --- 1) 维护开关 -INSERT INTO sys_config (config_name, config_key, config_value, config_type, create_by, create_time, remark) -VALUES ('奥林匹斯维护', 'olympusMaintenance', '0', 'Y', 'admin', NOW(), '奥林匹斯维护开关 0正常 1维护'); - --- 2) 后台菜单(ruoyi-ui 动态菜单,component 对应 src/views/game/olympus/*/index.vue) --- 自动挂到“游戏管理”下(与极速糖果同级);定位失败则挂到顶级根目录,可在后台菜单管理里拖动。 -SET @sweet_dir := (SELECT parent_id FROM sys_menu WHERE component = 'game/sweet/box/index' LIMIT 1); -SET @game_root := (SELECT parent_id FROM sys_menu WHERE menu_id = @sweet_dir LIMIT 1); - -INSERT INTO sys_menu (menu_name, parent_id, order_num, path, component, is_frame, is_cache, menu_type, visible, status, perms, icon, create_by, create_time, remark) -VALUES ('奥林匹斯', IFNULL(@game_root, 0), 5, 'olympus', '', 1, 0, 'M', '0', '0', '', 'star', 'admin', NOW(), '奥林匹斯电子游戏管理'); -SET @oly_dir := LAST_INSERT_ID(); - -INSERT INTO sys_menu (menu_name, parent_id, order_num, path, component, is_frame, is_cache, menu_type, visible, status, perms, icon, create_by, create_time, remark) VALUES -('箱子配置', @oly_dir, 1, 'box', 'game/olympus/box/index', 1, 0, 'C', '0', '0', 'game:olympus:box', 'shopping', 'admin', NOW(), '奥林匹斯箱子配置与身份桶分布'), -('饰品列表', @oly_dir, 2, 'ornament', 'game/olympus/ornament/index', 1, 0, 'C', '0', '0', 'game:olympus:ornament', 'star', 'admin', NOW(), '奥林匹斯 17 档饰品桶定义'), -('回合管理', @oly_dir, 3, 'round', 'game/olympus/round/index', 1, 0, 'C', '0', '0', 'game:olympus:round', 'documentation', 'admin', NOW(), '奥林匹斯预生成回合查询'), -('奖池水位', @oly_dir, 4, 'rewardPool', 'game/olympus/rewardPool/index', 1, 0, 'C', '0', '0', 'game:olympus:pool', 'money', 'admin', NOW(), '奥林匹斯按箱奖池水位(注水/抽水/批量/重置)'), -('玩家记录', @oly_dir, 5, 'record', 'game/olympus/record/index', 1, 0, 'C', '0', '0', 'game:olympus:record', 'list', 'admin', NOW(), '奥林匹斯游戏记录'), -('每日统计', @oly_dir, 6, 'dailywin', 'game/olympus/dailywin/index', 1, 0, 'C', '0', '0', 'game:olympus:win', 'chart', 'admin', NOW(), '奥林匹斯每日盈亏统计'), -('奖品管理', @oly_dir, 7, 'prize', 'game/olympus/prize/index', 1, 0, 'C', '0', '0', 'game:olympus:prize', 'gift', 'admin', NOW(), '奥林匹斯返奖饰品与区间配置'); - --- 前端页面文件(已在 ruoyi-ui 创建,7 页对齐糖果): --- src/api/game/olympus/{box,ornament,round,rewardPool,record,dailywin,prize}/api.js --- src/views/game/olympus/{box,ornament,round,rewardPool,record,dailywin,prize}/index.vue - --- 3) 【返奖饰品】从极速糖果整套导入到奥林匹斯 --- 幂等:先清空 olympus 两张返奖表再导入;会覆盖 olympus 现有返奖饰品配置。保留原 id 以保持区间映射。 -DELETE FROM game_olympus_prize_range; -DELETE FROM game_olympus_prize_ornament; - -INSERT INTO game_olympus_prize_ornament - (id, game_type, name, image_url, grade, exterior_name, sort, status, is_fallback, create_time) -SELECT id, 'OLYMPUS', name, image_url, grade, exterior_name, sort, status, is_fallback, NOW() -FROM game_sweet_prize_ornament; - --- 糖果 range 用 ornament_id,olympus range 用 prize_id,一一对应 -INSERT INTO game_olympus_prize_range (prize_id, win_low, win_high) -SELECT ornament_id, win_low, win_high -FROM game_sweet_prize_range; - --- 4) 【奖池水位调整日志】新增审计表 -CREATE TABLE IF NOT EXISTS game_olympus_reward_log ( - id BIGINT NOT NULL AUTO_INCREMENT COMMENT 'PK', - box_id BIGINT DEFAULT NULL COMMENT '箱子ID(批量/重置为汇总,可空)', - box_name VARCHAR(100) DEFAULT NULL COMMENT '箱子名/汇总描述', - type TINYINT NOT NULL COMMENT '1注水 2抽水 3重置', - amount DECIMAL(14,2) NOT NULL DEFAULT '0.00' COMMENT '调整金额(绝对值)', - balance_before DECIMAL(14,2) DEFAULT NULL COMMENT '调整前水位', - balance_after DECIMAL(14,2) DEFAULT NULL COMMENT '调整后水位', - pool_balance DECIMAL(14,2) DEFAULT NULL COMMENT '操作后总池余额', - operator VARCHAR(64) DEFAULT NULL COMMENT '操作人', - remark VARCHAR(200) DEFAULT NULL COMMENT '备注', - create_time DATETIME DEFAULT CURRENT_TIMESTAMP, - PRIMARY KEY (id), - KEY idx_create (create_time) -) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='Olympus奖池水位调整日志'; --- 本地也可跑:cd olympus-py && python _create_reward_log.py(需 DB 可连)。前端奖池水位页已对齐 bullet:说明面板/本轮RTP进度/收敛占比/备注/预估/调整后预览/调整历史。 --- 【已建过该表的环境】补列(总池余额): -ALTER TABLE game_olympus_reward_log ADD COLUMN pool_balance DECIMAL(14,2) DEFAULT NULL COMMENT '操作后总池余额' AFTER balance_after; - --- 5) 【奖池水位拆身份】奖池由「按箱 1 个池」改为「按 箱子 × 身份 4 个池」(common/anchor/robot/white) --- bet/win 由 game_olympus_record + tt_user 按身份派生(口径同 OlympusAudience.resolve),历史数据自动分流,无需回填。 -ALTER TABLE game_olympus_reward_log ADD COLUMN audience VARCHAR(16) NOT NULL DEFAULT 'common' COMMENT '身份 common/anchor/robot/white' AFTER box_name; -ALTER TABLE game_olympus_reward_log ADD KEY idx_audience (audience); - -## 2026-08-01 经典开箱管理菜单(复用宝箱列表/开箱记录,按 isFight=1 / source=1 过滤) - --- 背景:新增独立“经典开箱管理”菜单模块。复用现有页面 skins/ttBox/index 与 skins/ttBox/boxRecords, --- 通过菜单 query 传 isFight=1(仅经典宝箱)与 source=1(仅经典开箱记录)过滤;不改数据模型。 --- 说明:顶级目录 parent_id=0(可在后台“菜单管理”拖动到合适位置);两个子菜单 is_cache=1(不做 keep-alive 缓存), --- 避免与原“宝箱管理”下同组件菜单共享实例导致 query 过滤失效。按钮权限沿用 admin:box:add / admin:box:edit。 -INSERT INTO sys_menu (menu_name, parent_id, order_num, path, component, query, is_frame, is_cache, menu_type, visible, status, perms, icon, create_by, create_time, remark) -VALUES ('经典开箱管理', 0, 8, 'classicBox', '', NULL, 1, 0, 'M', '0', '0', '', 'box', 'admin', NOW(), '经典开箱管理目录'); -SET @classic_dir := LAST_INSERT_ID(); -INSERT INTO sys_menu (menu_name, parent_id, order_num, path, component, query, is_frame, is_cache, menu_type, visible, status, perms, icon, create_by, create_time, remark) VALUES -('经典宝箱列表', @classic_dir, 1, 'classicBoxList', 'skins/ttBox/index', '{"isFight":"1"}', 1, 1, 'C', '0', '0', 'admin:box:list', 'list', 'admin', NOW(), '仅展示经典开箱宝箱(isFight=1)'), -('经典开箱记录', @classic_dir, 2, 'classicBoxRecords', 'skins/ttBox/boxRecords', '{"source":"1"}', 1, 1, 'C', '0', '0', '', 'documentation', 'admin', NOW(), '仅展示经典开箱记录(source=1)'); - - --- 2026-08-05 对战宝箱查看奖项增加白号爆率列 --- 对战宝箱饰品表增加白号数量字段 -ALTER TABLE tt_box_ornaments ADD COLUMN white_odds INT DEFAULT 0 COMMENT '白号数量(白号爆率池)'; - - -## 2026-08-01 经典开箱管理补充:经典宝箱分类 + 临时倍率迁移 --- (type=0=对战分类、type=1=经典分类、不传=全部),boxType.vue 已加 $route 监听避免串味。 -SET @classic_dir := (SELECT menu_id FROM sys_menu WHERE menu_name = '经典开箱管理' AND menu_type = 'M' LIMIT 1); - --- 1) 新增“经典宝箱分类”:复用 skins/ttBox/boxType,query type=1(仅经典分类 isFightType=1)。is_cache=1 不缓存。 -INSERT INTO sys_menu (menu_name, parent_id, order_num, path, component, query, is_frame, is_cache, menu_type, visible, status, perms, icon, create_by, create_time, remark) -VALUES ('经典宝箱分类', @classic_dir, 3, 'classicBoxType', 'skins/ttBox/boxType', '{"type":"1"}', 1, 1, 'C', '0', '0', '', 'component', 'admin', NOW(), '仅展示经典宝箱分类(isFightType=1)'); - --- 2) 将“临时倍率”从“宝箱管理”迁移到“经典开箱管理”下(仅改归属与排序,组件/权限不变)。 -UPDATE sys_menu SET parent_id = @classic_dir, order_num = 4 WHERE menu_name = '临时倍率'; - - -## 2026-08-05 充值维护拆分为充值渠道维护 + 卡密充值维护 - --- 业务原因 rechargeMaintenance 同时控制卡密充值 (cardPay) 和充值渠道 (unifiedPay) --- 两个开关会同时生效,无法独立维护,互不影响。 - --- 1) 将原"充值维护"重命名为"充值渠道维护",仅控制第三方支付渠道 unifiedPay -UPDATE sys_config -SET config_name = '充值渠道维护', - remark = '充值渠道维护开关 0 正常 1 维护(仅控制第三方支付渠道)' -WHERE config_key = 'rechargeMaintenance'; - --- 2) 新增"卡密充值维护"配置,仅控制卡密充值 cardPay -INSERT INTO sys_config (config_name, config_key, config_value, config_type, create_by, create_time, remark) -VALUES ('卡密充值维护', 'cardRechargeMaintenance', '0', 'Y', 'admin', NOW(), '卡密充值维护开关 0 正常 1 维护(仅控制卡密充值)') -ON DUPLICATE KEY UPDATE config_value = config_value; - -## 2026-08-05 用户ID改为8位随机数字 - --- 1) 取消 tt_user.user_id 的自增属性,改为程序生成 -ALTER TABLE tt_user MODIFY COLUMN user_id INT NOT NULL COMMENT '用户ID'; - --- 2) 说明:新注册用户/后台生成账号的 user_id 将由程序生成 10000000~99999999 之间的8位随机数字,并保证不与已有用户ID及副号绑定记录重复 - -## 2026-08-06 白号管理支持生成白号用户 - --- 1) 在 user_type 字典中新增“白号”选项(键值 06) -INSERT INTO sys_dict_data (dict_sort, dict_label, dict_value, dict_type, status, create_by, create_time, remark) -SELECT 6, '白号', '06', 'user_type', '0', 'admin', NOW(), '白号用户' -FROM dual -WHERE NOT EXISTS (SELECT 1 FROM sys_dict_data WHERE dict_type = 'user_type' AND dict_value = '06'); - - -## 2026-08-06 对战房间房主昵称入库(用户被物理删除后仍可展示) - --- 背景:owner_nick_name 原为 @TableField(exist=false) 不入库字段,每次查询时从 tt_user 实时填充。 --- 用户被物理删除(forceDelete)后,tt_user 查不到,对战大厅/历史记录中的房主昵称显示为空。 --- 现将 owner_nick_name 入库,创建对战时写入昵称快照;enrichOwnerNickNames 方法仅在用户存在时 --- 用实时昵称覆盖(用户改了昵称能同步),用户被删除时保留入库快照。 -ALTER TABLE tt_fight ADD COLUMN owner_nick_name VARCHAR(100) DEFAULT NULL COMMENT '房主昵称快照(创建时写入,用户被物理删除后仍可展示)'; - --- 回填存量数据:从 tt_user 把现有对战的房主昵称填入 owner_nick_name -UPDATE tt_fight tf -LEFT JOIN tt_user tu ON tf.user_id = tu.user_id -SET tf.owner_nick_name = COALESCE(tf.owner_nick_name, tu.nick_name) -WHERE tf.owner_nick_name IS NULL; - - -## 2026-08-06 流水抽奖系统建表及菜单 - --- ========== 1. 流水抽奖活动表 ========== -CREATE TABLE IF NOT EXISTS `tt_turnover_lottery` ( - `id` BIGINT(20) NOT NULL AUTO_INCREMENT COMMENT '主键 ID', - `lottery_name` VARCHAR(200) NOT NULL COMMENT '抽奖名称', - `status` INT DEFAULT 0 COMMENT '状态:0=关闭,1=开启', - `lottery_type` INT DEFAULT 1 COMMENT '类型:0=普通宝箱,1=流水抽奖', - `turnover_threshold` INT DEFAULT 3000 COMMENT '流水门槛(多少流水可抽1次)', - `image_url` VARCHAR(500) DEFAULT NULL COMMENT '宝箱封面图', - `sort` INT DEFAULT 0 COMMENT '排序', - `total_value` DECIMAL(18,2) DEFAULT NULL COMMENT '奖品总价值', - `profit_per_round` INT DEFAULT NULL COMMENT '每轮利润', - `profit_rate` DECIMAL(10,4) DEFAULT NULL COMMENT '利润率', - `create_by` VARCHAR(64) DEFAULT NULL COMMENT '创建者', - `create_time` DATETIME DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间', - `update_by` VARCHAR(64) DEFAULT NULL COMMENT '更新者', - `update_time` DATETIME DEFAULT NULL ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间', - `del_flag` INT DEFAULT 0 COMMENT '删除标志:0=存在,2=删除', - PRIMARY KEY (`id`), - KEY `idx_status` (`status`), - KEY `idx_sort` (`sort`) -) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='流水抽奖活动表'; - --- ========== 2. 流水抽奖奖品表 ========== -CREATE TABLE IF NOT EXISTS `tt_turnover_lottery_prize` ( - `id` BIGINT(20) NOT NULL AUTO_INCREMENT COMMENT '主键 ID', - `lottery_id` BIGINT(20) NOT NULL COMMENT '关联抽奖活动 ID', - `ornament_id` BIGINT(20) DEFAULT NULL COMMENT '饰品 ID(已废弃,奖品改为自定义)', - `ornament_name` VARCHAR(200) DEFAULT NULL COMMENT '饰品名称/自定义名称', - `image_url` VARCHAR(500) DEFAULT NULL COMMENT '图片 URL', - `price` DECIMAL(18,2) DEFAULT NULL COMMENT '饰品价格', - `ornament_level` INT DEFAULT NULL COMMENT '饰品等级 ID(关联 tt_ornaments_level)', - `display_count` INT DEFAULT 10 COMMENT '展示数量(前端库存显示)', - `real_count` INT DEFAULT 10 COMMENT '真实数量(实际权重,当前剩余)', - `initial_real_count` INT DEFAULT 10 COMMENT '初始真实数量(重置奖池时恢复值)', - `sort` INT DEFAULT 0 COMMENT '展示排序', - `total_count` INT DEFAULT 0 COMMENT '已发放次数', - `create_time` DATETIME DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间', - `update_time` DATETIME DEFAULT NULL ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间', - PRIMARY KEY (`id`), - KEY `idx_lottery_id` (`lottery_id`) -) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='流水抽奖奖品表'; - --- 已创建表的环境补充字段( ornaments_id 改为可空,新增 ornament_level、initial_real_count) -ALTER TABLE `tt_turnover_lottery_prize` - MODIFY COLUMN `ornament_id` BIGINT(20) DEFAULT NULL COMMENT '饰品 ID(已废弃,奖品改为自定义)', - ADD COLUMN `ornament_level` INT DEFAULT NULL COMMENT '饰品等级 ID(关联 tt_ornaments_level)', - ADD COLUMN `initial_real_count` INT DEFAULT 10 COMMENT '初始真实数量(重置奖池时恢复值)'; - --- 历史数据初始化:已存在的奖品把初始真实数量同步为当前真实数量 -UPDATE `tt_turnover_lottery_prize` SET `initial_real_count` = `real_count` WHERE `initial_real_count` = 10; - --- 2026-08-08 流水抽奖奖品新增磨损度字段(自定义饰品可配置磨损度,发放入背包时写入 tt_box_records.exterior_name) -ALTER TABLE `tt_turnover_lottery_prize` - ADD COLUMN `exterior_name` VARCHAR(20) DEFAULT NULL COMMENT '磨损度名称(崭新出厂/略有磨损/久经沙场/破损不堪/战痕累累/无涂装)'; - --- ========== 3. 流水抽奖记录表 ========== -CREATE TABLE IF NOT EXISTS `tt_turnover_lottery_record` ( - `id` BIGINT(20) NOT NULL AUTO_INCREMENT COMMENT '主键 ID', - `user_id` INT NOT NULL COMMENT '用户 ID', - `lottery_id` BIGINT(20) NOT NULL COMMENT '抽奖活动 ID', - `prize_id` BIGINT(20) DEFAULT NULL COMMENT '奖品 ID', - `ornament_id` BIGINT(20) DEFAULT NULL COMMENT '饰品 ID', - `ornament_name` VARCHAR(200) DEFAULT NULL COMMENT '饰品名称', - `image_url` VARCHAR(500) DEFAULT NULL COMMENT '图片 URL', - `ornaments_price` DECIMAL(18,2) DEFAULT NULL COMMENT '奖品价值', - `cost_turnover` INT DEFAULT NULL COMMENT '消耗的流水值', - `create_time` DATETIME DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间', - PRIMARY KEY (`id`), - KEY `idx_user_id` (`user_id`), - KEY `idx_lottery_id` (`lottery_id`), - KEY `idx_create_time` (`create_time`) -) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='流水抽奖记录表'; - --- 2026-08-08 全服中奖记录接口(/api/turnoverLottery/globalRecords)性能优化 --- 查询模式:WHERE lottery_id = ? AND ornaments_price >= ? ORDER BY create_time DESC --- 单列 idx_lottery_id / idx_create_time 无法同时命中「按宝箱过滤 + 按时间倒序」,补复合索引 --- 执行前先 SHOW INDEX FROM tt_turnover_lottery_record; 确认无等价索引 -ALTER TABLE `tt_turnover_lottery_record` - ADD INDEX `idx_ttlr_lottery_ctime` (`lottery_id`, `create_time`); - --- ========== 4. 后台菜单 ========== --- 流水抽奖目录 -INSERT INTO sys_menu (menu_name, parent_id, order_num, path, component, is_frame, is_cache, menu_type, visible, status, perms, icon, create_by, create_time, remark) -VALUES ('流水抽奖', 0, 9, 'turnoverLottery', '', 1, 0, 'M', '0', '0', '', 'gift', 'admin', NOW(), '流水抽奖管理目录'); - -SET @lottery_dir := LAST_INSERT_ID(); - --- 抽奖活动列表 -INSERT INTO sys_menu (menu_name, parent_id, order_num, path, component, is_frame, is_cache, menu_type, visible, status, perms, icon, create_by, create_time, remark) -VALUES ('抽奖活动', @lottery_dir, 1, 'lotteryList', 'turnoverLottery/list', 1, 0, 'C', '0', '0', 'system:turnoverLottery:list', 'list', 'admin', NOW(), '流水抽奖活动管理'); - --- 抽奖记录查询 -INSERT INTO sys_menu (menu_name, parent_id, order_num, path, component, is_frame, is_cache, menu_type, visible, status, perms, icon, create_by, create_time, remark) -VALUES ('抽奖记录', @lottery_dir, 2, 'lotteryRecords', 'turnoverLottery/records', 1, 0, 'C', '0', '0', 'system:turnoverLotteryRecord:list', 'documentation', 'admin', NOW(), '流水抽奖记录查询'); - --- 奖项配置页面 (隐藏菜单,通过抽奖活动列表“查看奖项”按钮跳转) -INSERT INTO sys_menu (menu_name, parent_id, order_num, path, component, is_frame, is_cache, menu_type, visible, status, perms, icon, create_by, create_time, remark) -VALUES ('奖项配置', @lottery_dir, 3, 'prize', 'turnoverLottery/prize', 1, 0, 'C', '1', '0', 'system:turnoverLotteryPrize:list', '#', 'admin', NOW(), '流水抽奖奖项配置页面'); - --- 按钮权限 (menu_type='F',不显示在左侧导航) -INSERT INTO sys_menu (menu_name, parent_id, order_num, path, component, is_frame, is_cache, menu_type, visible, status, perms, icon, create_by, create_time, remark) -VALUES -('抽奖新增', @lottery_dir, 4, '', NULL, 1, 0, 'F', '1', '0', 'system:turnoverLottery:add', '#', 'admin', NOW(), ''), -('抽奖修改', @lottery_dir, 5, '', NULL, 1, 0, 'F', '1', '0', 'system:turnoverLottery:edit', '#', 'admin', NOW(), ''), -('抽奖删除', @lottery_dir, 6, '', NULL, 1, 0, 'F', '1', '0', 'system:turnoverLottery:remove', '#', 'admin', NOW(), ''); - --- ========== 5. 消息分类字段 ========== -ALTER TABLE `tt_notice` - ADD COLUMN `category` VARCHAR(50) DEFAULT NULL COMMENT '消息分类:GAME_*/SYS_*'; - -ALTER TABLE `tt_announcement` - ADD COLUMN `category` VARCHAR(50) DEFAULT NULL COMMENT '消息分类:GAME_*/SYS_*'; - - --- ========== 6. 转赠/提货次数数据库幂等字段 ========== --- 用于防止 Redis 防重标记过期后重复扣/放次数 - -ALTER TABLE `tt_delivery_record` - ADD COLUMN `times_deducted` TINYINT NOT NULL DEFAULT 0 COMMENT '该批次次数是否已处理:0=未处理,1=已处理(扣次或释放)'; - -ALTER TABLE `tt_transfer_apply` - ADD COLUMN `times_deducted` TINYINT NOT NULL DEFAULT 0 COMMENT '该批次次数是否已处理:0=未处理,1=已处理(扣次或释放)'; - --- 上线后:历史已审核订单的 times_deducted 默认为 0,但因为 Redis 标记可能已过期, --- 如果再次对历史订单操作,需要先把已完结的批次标记为已处理,避免重复扣/放次数。 --- 以下 SQL 把状态为已完成(10)或已取消(11)的提货批次标记为已处理: -UPDATE `tt_delivery_record` -SET `times_deducted` = 1 -WHERE `status` IN (10, 11) - AND `times_deducted` = 0; - --- 以下 SQL 把状态为已通过(1)或已驳回(2)的转赠批次标记为已处理: -UPDATE `tt_transfer_apply` -SET `times_deducted` = 1 -WHERE `status` IN (1, 2) - AND `times_deducted` = 0; - - --- ======================================================================== --- 疯狂转盘(彩虹旋风)内圈玩法改造 DDL --- 详见 docs/疯狂转盘内圈玩法设计.md --- ======================================================================== --- 执行前务必先选中数据库,否则会报 1046 - No database selected -USE `changzhou`; - --- 1) 新建奖励矩阵表(方案B自定义饰品:10档 × 3内圈色 × 4外圈符号 = 120条) -CREATE TABLE `game_wheel_reward_matrix` ( - `id` BIGINT NOT NULL AUTO_INCREMENT COMMENT '主键', - `bet_amount` INT NOT NULL COMMENT '下注额层:5/10/20/50/100/200/500/1000/2000/5000', - `color` VARCHAR(16) NOT NULL COMMENT '内圈颜色:gold/red/purple', - `symbol` VARCHAR(8) NOT NULL COMMENT '外圈符号:A/B/C/D', - `multiplier` INT NOT NULL COMMENT '倍率(后台可配,12个跨下注额共用)', - `prize_value` DECIMAL(18,2) NOT NULL DEFAULT 0.00 COMMENT '价值=下注额×倍率(自动算)', - `prize_name` VARCHAR(200) NOT NULL DEFAULT '' COMMENT '饰品名称(自定义)', - `prize_grade` INT NULL COMMENT '品质等级(自定义,对应tt_ornaments_level.id)', - `prize_exterior` VARCHAR(64) NULL COMMENT '磨损度(自定义)', - `prize_image` VARCHAR(500) NULL COMMENT '饰品图片(自定义)', - `status` CHAR(1) NOT NULL DEFAULT '1' COMMENT '0停用 1启用', - `create_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间', - `update_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间', - PRIMARY KEY (`id`), - UNIQUE KEY `uk_combo` (`bet_amount`,`color`,`symbol`) -) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='疯狂转盘奖励矩阵(自定义饰品120条)'; - --- 2) 回合表 game_wheel_round 改造 --- period_no:全局唯一期号 = date(yyyyMMdd) × 100000 + 当日序号(round_id),作为业务主键替代 date+round_id 复合键 --- inner_colors:本轮内圈24色随机排布快照(外圈 symbols 已存在,为固定布局) --- result 扩容以容纳4池结果JSON,如 {"real":3,"robot":10,"blank":7,"promo":15} -ALTER TABLE `game_wheel_round` - ADD COLUMN `period_no` BIGINT NOT NULL DEFAULT 0 COMMENT '全局唯一期号=date×100000+当日序号' AFTER `round_id`, - ADD COLUMN `inner_colors` VARCHAR(64) NOT NULL DEFAULT '' COMMENT '本轮内圈24色随机排布快照' AFTER `symbols`, - MODIFY COLUMN `result` VARCHAR(128) NOT NULL DEFAULT '' COMMENT '4池开奖结果JSON {real,robot,blank,promo}'; --- 历史数据回填 period_no(date 为 yyyyMMdd 文本;新数据由代码直接写入 period_no) -UPDATE `game_wheel_round` SET `period_no` = CAST(`date` AS UNSIGNED) * 100000 + `round_id` WHERE `period_no` = 0; -ALTER TABLE `game_wheel_round` ADD UNIQUE KEY `uk_period_no` (`period_no`); - --- 3) 下注表 game_wheel_round_bet 改造 --- period_no:关联回合的全局唯一期号(查询/结算/判重单列定位) --- color:下注内圈颜色;pool_type:落库定池 real/robot/blank/promo --- multiplier/prize_id:下注时快照倍率与命中奖品矩阵行ID(防中途改配置影响已下注) --- credits_cut:下注时实际扣减的积分额(退款按此原路退回;仅金币扣款时为0) -ALTER TABLE `game_wheel_round_bet` - ADD COLUMN `period_no` BIGINT NOT NULL DEFAULT 0 COMMENT '全局唯一期号' AFTER `round_id`, - ADD COLUMN `color` VARCHAR(16) NOT NULL DEFAULT '' COMMENT '下注内圈颜色 gold/red/purple' AFTER `bet_symbol`, - ADD COLUMN `pool_type` VARCHAR(16) NOT NULL DEFAULT '' COMMENT '决策池 real/robot/blank/promo', - ADD COLUMN `multiplier` INT NOT NULL DEFAULT 0 COMMENT '下注时快照倍率', - ADD COLUMN `prize_id` BIGINT NULL COMMENT '命中奖品矩阵行ID(game_wheel_reward_matrix.id)快照', - ADD COLUMN `credits_cut` DECIMAL(10,2) NOT NULL DEFAULT 0.00 COMMENT '下注实际扣减积分额(退款原路退回)'; -UPDATE `game_wheel_round_bet` SET `period_no` = CAST(`date` AS UNSIGNED) * 100000 + `round_id` WHERE `period_no` = 0; -ALTER TABLE `game_wheel_round_bet` ADD KEY `idx_period_no` (`period_no`); --- 移除旧唯一键:新玩法每次下注独立一条、绝不累加(同格同额也各记一条), --- 旧 uk_date_round_user_symbol 会阻止同用户同符号多注独立落库,必须删除 -ALTER TABLE `game_wheel_round_bet` DROP INDEX `uk_date_round_user_symbol`; - --- 4) 账变来源:疯狂转盘退款(孤儿局无法开奖时静默退本金)在 TtAccountRecordSource 枚举新增来源码即可, --- 账变写入 tt_user_blend_ercash 复用现有结构,无需额外DDL。 --- 新玩法余额变动仅两类:下注扣款(LUCKLY_WHEEL_SPIN) 与 退本金(新增退款来源); --- 中奖与安慰奖均发饰品进背包(source=WHEEL_BOX),不动余额。 - --- 5) 奖励矩阵种子:生成 120 行骨架(10档 × 3色 × 4符号), --- multiplier 按默认倍率、prize_value = bet_amount × multiplier 自动计算, --- prize_name/prize_grade/prize_exterior/prize_image 留空由后台配置(本 INSERT 为一次性,uk_combo 保证不重复)。 -INSERT INTO `game_wheel_reward_matrix` - (`bet_amount`, `color`, `symbol`, `multiplier`, `prize_value`, `prize_name`, `prize_grade`, `prize_exterior`, `prize_image`, `status`) -SELECT b.amount, m.color, m.symbol, m.mult, b.amount * m.mult, '', NULL, NULL, NULL, '1' -FROM ( - SELECT 5 amount UNION ALL SELECT 10 UNION ALL SELECT 20 UNION ALL SELECT 50 UNION ALL SELECT 100 - UNION ALL SELECT 200 UNION ALL SELECT 500 UNION ALL SELECT 1000 UNION ALL SELECT 2000 UNION ALL SELECT 5000 -) b -CROSS JOIN ( - SELECT 'gold' color, 'A' symbol, 44 mult - UNION ALL SELECT 'gold','B',26 UNION ALL SELECT 'gold','C',11 UNION ALL SELECT 'gold','D',8 - UNION ALL SELECT 'red','A',33 UNION ALL SELECT 'red','B',21 UNION ALL SELECT 'red','C',8 UNION ALL SELECT 'red','D',6 - UNION ALL SELECT 'purple','A',27 UNION ALL SELECT 'purple','B',17 UNION ALL SELECT 'purple','C',7 UNION ALL SELECT 'purple','D',5 -) m; - --- 6) 生产配置 sys_config(game.wheel.setting) 迁移为新结构 --- 现有旧结构(symbols/symbol_scores/special_effects)已不适用新玩法。 --- 最稳妥方式:删除该配置行,应用下次 newRound/getGameConfig 检测到缺失会 --- 由 CommonWheelService 自动按新结构(内圈分布/外圈布局/12倍率/10档下注/奖池比例0.6/RTP0.9)重建,无需手拼 JSON。 -DELETE FROM `sys_config` WHERE `config_key` = 'game.wheel.setting'; - --- 机器人对战进房时间配置表新增随机秒数字段 --- 含义:在 join_seconds 基础上额外随机增加 1~random_seconds 秒,保证机器人错峰进房 -ALTER TABLE tt_robot_fight_join_config ADD COLUMN random_seconds INT NOT NULL DEFAULT 10 COMMENT '机器人进房随机偏移秒数(在join_seconds基础上额外随机增加1~randomSeconds秒)'; - --- 机器人对战价格档位配置表 --- 含义:控制机器人为每种对战模式自动创建房间时,各价格档位需要维持的目标房间数量 -CREATE TABLE `tt_robot_fight_price_config` ( - `id` INT NOT NULL AUTO_INCREMENT COMMENT '主键ID', - `price_limit` DECIMAL(15,2) NOT NULL COMMENT '价格上限(房间宝箱总金额 <= price_limit 命中该档位)', - `target_count` INT NOT NULL COMMENT '该档位目标房间数量', - `sort_order` INT NOT NULL COMMENT '排序序号(越小越靠前)', - `status` CHAR(1) NOT NULL DEFAULT '0' COMMENT '状态 0启用 1停用', - `remark` VARCHAR(255) DEFAULT NULL COMMENT '备注', - `create_time` DATETIME DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间', - `update_time` DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间', - PRIMARY KEY (`id`), - KEY `idx_status_sort` (`status`, `sort_order`) -) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='机器人对战价格档位配置表'; - --- 默认初始化数据,与原硬编码配置保持一致:200档6个、800档4个、1500档1个 -INSERT INTO `tt_robot_fight_price_config` (`price_limit`, `target_count`, `sort_order`, `status`, `remark`, `create_time`, `update_time`) -VALUES - (200.00, 6, 1, '0', '0-200金额房间', NOW(), NOW()), - (800.00, 4, 2, '0', '200-800金额房间', NOW(), NOW()), - (1500.00, 1, 3, '0', '800-1500金额房间', NOW(), NOW()); - --- 机器人对战分组表新增价格档位配置ID字段 --- 含义:每个机器人对战分组关联一个价格档位,用于按档位目标数量创建房间 -ALTER TABLE tt_robot_fight_group ADD COLUMN price_config_id INT DEFAULT NULL COMMENT '价格档位配置ID(关联 tt_robot_fight_price_config.id)'; - --- ===================================================== --- 提货次数管理功能 - 数据库迁移脚本 --- 创建时间:2025-08-05 --- 说明:添加批次号、申请时VIP等级、次数扣减状态字段 --- ===================================================== - --- ===================================================== --- 幸运升级新增"幸运值"(累计原始概率)字段 --- 说明:按用户类型区分,玩家每次拉取累加前端原始概率(如90),出货成功清0重置;返回给前端cocos展示 --- ===================================================== -ALTER TABLE tt_upgrade_ornaments - ADD COLUMN probability_invested INT NOT NULL DEFAULT 0 COMMENT '普通用户累计原始概率(幸运值),出货成功清0', - ADD COLUMN anchor_probability_invested INT NOT NULL DEFAULT 0 COMMENT '主播累计原始概率(幸运值),出货成功清0'; - --- ===================================================== --- 幸运升级记录页:持久化每条记录的"幸运值"快照 + 分页查询索引 --- 说明:tt_upgrade_ornaments.probability_invested 只保存"当前累计值", --- 历史每条记录的幸运值需单独持久化;新增列后历史旧数据为 NULL(前端显示0) --- ===================================================== -ALTER TABLE tt_upgrade_record - ADD COLUMN lucky_value INT DEFAULT NULL COMMENT '本次升级幸运值(累计原始概率)快照' AFTER probability; - --- 记录分页排序索引(open_time 倒序) --- 全服+当前饰品 -ALTER TABLE tt_upgrade_record ADD INDEX idx_upgrade_opentime (target_upgrade_id, open_time); --- 我的+当前饰品 -ALTER TABLE tt_upgrade_record ADD INDEX idx_user_upgrade_opentime (user_id, target_upgrade_id, open_time); --- 注:我的+全部饰品依赖已有 (user_id, open_time);全服+全部饰品依赖 open_time 排序 - --- 第 1 个字段 -ALTER TABLE `tt_user_blend_ercash` -ADD COLUMN `anchor_id` INT COMMENT '上级主播 ID(消费时的直属上级主播)' AFTER `user_id`; - --- 第 2 个字段 -ALTER TABLE `tt_user_blend_ercash` -ADD COLUMN `business_id` INT COMMENT '最终招商 ID(上级主播所属的招商)' AFTER `anchor_id`; - --- 创建索引 -CREATE INDEX idx_business_commission ON tt_user_blend_ercash (business_id, create_time, source); -CREATE INDEX idx_anchor_commission ON tt_user_blend_ercash (anchor_id, create_time, source); - - --- 1. 先检查字段是否已存在 -DESCRIBE tt_user_blend_ercash_archive; - --- 2. 如果 anchor_id 和 business_id 列不存在,就执行下面的语句: -ALTER TABLE `tt_user_blend_ercash_archive` -ADD COLUMN `anchor_id` INT COMMENT '上级主播 ID' AFTER `user_id`; - -ALTER TABLE `tt_user_blend_ercash_archive` -ADD COLUMN `business_id` INT COMMENT '最终招商 ID' AFTER `anchor_id`; - --- 3. 创建索引 -CREATE INDEX idx_business_commission_archive ON tt_user_blend_ercash_archive (business_id, create_time, source); -CREATE INDEX idx_anchor_commission_archive ON tt_user_blend_ercash_archive (anchor_id, create_time, source); - - -CREATE INDEX idx_olympus_record_box_settled ON game_olympus_record (box_id, settled); - - --- 为 tt_box_records 表添加 period_no 字段,用于精确匹配转盘记录 -ALTER TABLE tt_box_records -ADD COLUMN period_no BIGINT NULL COMMENT '转盘期号(WHEEL专用)' AFTER box_price; - --- 为 period_no 添加索引,加速查询 -CREATE INDEX idx_period_no ON tt_box_records(period_no); - - --- 疯狂转盘奖池调节记录表 -CREATE TABLE IF NOT EXISTS game_wheel_pool_adjustment ( -id BIGINT PRIMARY KEY AUTO_INCREMENT COMMENT '主键ID', -pool_type VARCHAR(20) NOT NULL COMMENT '池类型(real/display)', -adjustment_amount DECIMAL(12,2) NOT NULL COMMENT '调整金额(正数=注水,负数=抽水)', -balance_before DECIMAL(12,2) NOT NULL COMMENT '调整前余额', -balance_after DECIMAL(12,2) NOT NULL COMMENT '调整后余额', -reason VARCHAR(200) DEFAULT NULL COMMENT '调整原因', -operator VARCHAR(50) DEFAULT NULL COMMENT '操作人', -create_time DATETIME DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间', -INDEX idx_pool_type (pool_type), -INDEX idx_create_time (create_time) -) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='疯狂转盘奖池调节记录'; - --- ========== tt_user 新增安全密码字段(提货时校验,与登录密码 password 独立) ========== -ALTER TABLE tt_user ADD COLUMN safe_password VARCHAR(100) DEFAULT NULL COMMENT '安全密码(6位数字,BCrypt加密,提货时校验;NULL=未设置)'; --- 安全密码明文:仅供后台用户详情展示与编辑回填(与 password 明文存 remark 同一约定) -ALTER TABLE tt_user ADD COLUMN safe_password_plain VARCHAR(16) DEFAULT NULL COMMENT '安全密码明文(仅后台展示/回填,不参与校验)'; - - --- ========== 玫瑰女郎:删除 game_olympus_record.auto_dismantle(历史列,已无代码读写) ========== --- 背景:自动分解改为「前端开关 + 末帧前端调 /api/userPackSack/decompose」,后端不再记录该快照, --- 实体 OlympusRecord.autoDismantle 字段已删除;该列历史上也从未被写过非 0 值(旧 Redis 开关无入口),全为默认 0。 --- 执行前确认列是否还在(MySQL 不支持 DROP COLUMN IF EXISTS,先查可避免 1091 报错): -SELECT COLUMN_NAME FROM information_schema.COLUMNS -WHERE TABLE_SCHEMA = DATABASE() AND TABLE_NAME = 'game_olympus_record' AND COLUMN_NAME = 'auto_dismantle'; - --- 确认存在后再执行: -ALTER TABLE game_olympus_record DROP COLUMN auto_dismantle; - - --- ===================================================== --- 幸运升级:白号独立幸运池(幸运值 + 独立幸运区间) --- 背景:白号原先走"纯概率"分支(不写任何共享字段),但记录/界面里的幸运值读的是普通池 --- (tt_upgrade_ornaments.probability_invested),导致白号看到的是别人的数(恒 0 或别人垫出来的值)。 --- 现改为:白号有自己的幸运值列 + 自己的幸运区间,进度与判定都走白号池 --- (Redis key 用 userType=06,白号之间共享这一个池,与宝箱白号池/奥林匹斯 white audience 口径一致)。 --- blank_luck_section 为空时回退普通 luck_section(后台「幸运升级饰品」编辑接口已支持传该字段)。 --- ===================================================== -ALTER TABLE tt_upgrade_ornaments - ADD COLUMN blank_luck_section VARCHAR(20) DEFAULT NULL COMMENT '白号幸运区间[min,max),NULL/空=回退普通luck_section', - ADD COLUMN blank_probability_invested INT NOT NULL DEFAULT 0 COMMENT '白号累计原始概率(幸运值),出货成功清0', - ADD COLUMN blank_amount_invested DECIMAL(19,2) NOT NULL DEFAULT 0 COMMENT '白号本轮已投金额(实际扣款口径),出货成功清0', - ADD COLUMN blank_effective_amount_invested DECIMAL(19,2) NOT NULL DEFAULT 0 COMMENT '白号本轮已投金额(真实计算口径),出货成功清0'; - --- ===================================================== --- 疯狂转盘:注单增加"是否退过本金"标记 --- 背景:game_wheel_round_bet.settled 只有两态(0=未结算 / 1=已结算), --- 正常发奖与兜底退本金在库里长得完全一样,事后无法逐注区分,客服/对账只能靠流水 source=64 反推。 --- 退款时额外打 refunded=1(ApiWheelService.refundBets 内与 settled 一起 CAS 更新)。 --- ===================================================== -ALTER TABLE game_wheel_round_bet - ADD COLUMN refunded TINYINT NOT NULL DEFAULT 0 COMMENT '是否走过退本金 0=否 1=是(settled=1 时用于区分发奖/退款)'; - - --- ===================================================== --- 亏损返水(玩家可见名:亏损返利)—— 每半个月给亏损玩家自动发返水 CDK --- ===================================================== - --- 1. 结算明细表(后台「CDK管理 - 亏损返水」页的数据源) -CREATE TABLE IF NOT EXISTS `tt_loss_rebate` ( - `id` BIGINT NOT NULL AUTO_INCREMENT COMMENT '主键', - `period` VARCHAR(32) NOT NULL COMMENT '期次 yyyy-MM-dd~yyyy-MM-dd', - `user_id` INT NOT NULL COMMENT '用户ID', - `user_name` VARCHAR(64) DEFAULT NULL COMMENT '用户账号', - `nick_name` VARCHAR(64) DEFAULT NULL COMMENT '用户昵称', - `bet_amount` DECIMAL(19,2) NOT NULL DEFAULT 0 COMMENT '本期游戏投入', - `win_amount` DECIMAL(19,2) NOT NULL DEFAULT 0 COMMENT '本期游戏产出', - `loss_amount` DECIMAL(19,2) NOT NULL DEFAULT 0 COMMENT '净亏=投入-产出', - `rebate_amount` DECIMAL(19,2) NOT NULL DEFAULT 0 COMMENT '应返水=净亏×比例(人工发放时可能被下调为实发金额)', - `status` TINYINT NOT NULL DEFAULT 0 COMMENT '0=待人工发放(超单期上限) 1=已自动发放 2=已人工发放 3=自动发放失败', - `red_packet_id` INT DEFAULT NULL COMMENT '关联 tt_red_packet.id', - `cdkey` VARCHAR(64) DEFAULT NULL COMMENT '发放的CDK口令', - `grant_time` DATETIME DEFAULT NULL COMMENT '发放时间', - `grant_by` VARCHAR(64) DEFAULT NULL COMMENT '发放人(自动发放为 lossRebate,人工发放为管理员账号)', - `remark` VARCHAR(255) DEFAULT NULL COMMENT '备注', - `create_time` DATETIME DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间', - `update_time` DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间', - PRIMARY KEY (`id`), - UNIQUE KEY `uk_period_user` (`period`, `user_id`), - KEY `idx_status` (`status`), - KEY `idx_period` (`period`) -) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci COMMENT='亏损返水结算明细(亏损返利)'; - --- 1b. ⚠ 排序规则必须跟库里其他表一致(tt_red_packet / tt_notice / tt_user_blend_ercash 都是 utf8mb4_general_ci)。 --- 2026-09-16 首次建表时漏了 COLLATE,MySQL 8 默认给了 utf8mb4_0900_ai_ci, --- 导致「tt_red_packet.description LIKE tt_loss_rebate.period」这类跨表字符串比较直接报 --- Illegal mix of collations。(Java 绑定参数的查询不受影响,但跨表关联会炸,必须修。) --- 已建过表的环境执行这一条;新建环境上面的 CREATE TABLE 已带 COLLATE,不用跑。 -ALTER TABLE tt_loss_rebate CONVERT TO CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci; - --- 2. 后台菜单:CDK管理 → 亏损返水(父菜单按名字找;若报错先确认「CDK管理」的 menu_name) -INSERT INTO sys_menu (menu_name, parent_id, order_num, path, component, is_frame, is_cache, menu_type, visible, status, perms, icon, create_by, create_time, remark) -SELECT '亏损返水', menu_id, 4, 'lossRebate', 'skins/ttredPacket/lossRebate', 1, 0, 'C', '0', '0', 'admin:lossRebate:list', 'money', 'admin', NOW(), '亏损返水结算明细,超单期上限的记录在此人工发放CDK' -FROM (SELECT menu_id FROM sys_menu WHERE menu_name = 'CDK管理' AND menu_type = 'M' LIMIT 1) m; - --- 3. 注册定时任务(每月 1 日与 16 日 01:30,结算上一个自然半月) -INSERT INTO sys_job (job_name, job_group, invoke_target, cron_expression, misfire_policy, concurrent, status, create_by, create_time, remark) -VALUES ('亏损返水结算', 'LOSS_REBATE', 'lossRebateService.generateHalfMonthLossRebate()', '0 30 1 1,16 * ?', '3', '1', '0', 'admin', NOW(), '每月1日与16日01:30结算上一个自然半月:返水≤上限自动发CDK+站内信,超上限转后台人工发放;白号/机器人不参与'); - --- 4. 可选:比例 / 最低发放金额 / 单期上限 / CDK有效期。不建这四条也能跑,代码内兜底 5% / 1元 / 500元 / 30天。 -INSERT INTO sys_config (config_name, config_key, config_value, config_type, create_by, create_time, remark) -VALUES ('亏损返水比例', 'lossRebateRate', '0.05', 'N', 'admin', NOW(), '0.05=5%,玩家可见的「亏损返利」返水比例'), - ('亏损返水最低发放金额', 'lossRebateMinAmount', '1', 'N', 'admin', NOW(), '返水低于该金额不结算,避免几毛钱的码刷屏'), - ('亏损返水单期上限', 'lossRebateMaxAmount', '500', 'N', 'admin', NOW(), '返水超过该金额不自动发放,转「CDK管理-亏损返水」页人工发放;填 0 表示不设上限'), - ('亏损返利CDK有效期(天)', 'lossRebateValidDays', '30', 'N', 'admin', NOW(), 'CDK有效期 = 生成时刻 + N 天'); - --- 5. 改名(回血红包 → 亏损返利):只在已建过旧数据的环境执行,新环境不用跑。 --- 代码里红包标题=「亏损返利CDK」、站内信标题=「亏损返利」,执行后历史数据与新数据命名一致。 -UPDATE tt_red_packet SET title = '亏损返利CDK' WHERE title = '回血红包'; -UPDATE tt_notice SET title = '亏损返利' WHERE category = 'SYS_LOSS_REBATE' AND title = '回血红包'; -UPDATE sys_job SET job_name = '亏损返水结算' WHERE invoke_target = 'lossRebateService.generateHalfMonthLossRebate()' AND job_name = '亏损返水回血红包'; -UPDATE sys_config SET config_name = '亏损返利CDK有效期(天)' WHERE config_key = 'lossRebateValidDays' AND config_name = '回血红包有效期(天)'; - --- 6. 亏损返利CDK 绑定收款人(只有指定用户能兑,口令被转发/截图也兑不走) --- ⚠ 必须先执行这条 ALTER,再重启后端:新代码建红包时会写 receiver_user_id,列不存在会插入失败(服务本身能起来)。 -ALTER TABLE tt_red_packet - ADD COLUMN receiver_user_id INT DEFAULT NULL COMMENT '指定领取人用户ID:NULL=不限用户(平台/主播红包原有行为);非NULL=仅该用户可领(亏损返利CDK)'; - --- 历史数据不用回填:这列是 NULL 时行为不变(不限用户)。 --- 已发出的那几条测试返利红包如果也想绑上收款人,可以从明细表按 red_packet_id 关联回填: --- UPDATE tt_red_packet p INNER JOIN tt_loss_rebate r ON r.red_packet_id = p.id SET p.receiver_user_id = r.user_id WHERE p.title = '亏损返利CDK' AND p.receiver_user_id IS NULL; \ No newline at end of file diff --git a/docs/玫瑰女郎接口文档.md b/docs/玫瑰女郎接口文档.md deleted file mode 100644 index 450c926..0000000 --- a/docs/玫瑰女郎接口文档.md +++ /dev/null @@ -1,707 +0,0 @@ -# 玫瑰女郎 用户端 接口文档 - ---- - -## 一、功能说明 - -玫瑰女郎是一款**选箱下注 → 旋转 → 必得饰品**的电子游戏。核心产品逻辑: - -| 规则 | 说明 | -|------|------| -| 三种玩法模式 | `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 | 否 | 待分解的背包记录 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)。 diff --git a/docs/麻将胡了设计方案.md b/docs/麻将胡了设计方案.md index 5870779..7a30c9f 100644 --- a/docs/麻将胡了设计方案.md +++ b/docs/麻将胡了设计方案.md @@ -82,51 +82,134 @@ | `1, 2, 3, 4, 4` | ✅ 5 连,ways=96 | 满链 | | `1, 1, 1, 1, 1` | ✅ 最低 ways=1 | 每轴 1 个,共 1 路 | -- 单笔赢额(倍基础下注)= `赔付表[X][n] × ways × 本波乘数`。 +- 单笔赢额 = `符号倍率(赔付表) × ways × 投注总额 × 本波阶梯倍率`。 - 多个符号各自独立成奖、**累加**(互不影响,各有各的前缀链);同一符号只按上述规则算一笔,不存在"同符号多个子路径重复叠加"。 -- 参考例子(截图):符号 `?` 三连赔 10,ways=6(各轴 1/3/2)→ 本笔 60。 +- 参考例子(截图):符号 `?` 三连赔 10(原版点数,= **0.5 倍**),ways=6(各轴 1/3/2)→ 本笔 3.00 倍。 -**赔付表**(数值抄自赔付表截图,含义见下方"计量单位"): +#### 2.1.1 赔付算例(**原样复现截图里那个 `1×3×2=6` 的例子**) + +**盘面**(投注总额 **¥20**): + +| 轴 → | 1 | 2 | 3 | 4 | 5 | +|---|---|---|---|---|---| +| 行1 | **B** | **B** | **B** | E | H | +| 行2 | D | **B** | **B** | G | F | +| 行3 | E | **B** | D | A | G | +| 行4 | H | C | E | F | D | + +(**`B` = 中**,本波的中奖符号;其余是干扰符号,故意让它们在第 2 轴就断) + +**第 1 步 — 逐轴数「中」的个数**(从第 1 轴往右数,**断了就停**): + +| 轴 | 1 | 2 | 3 | 4 | 5 | +|---|---|---|---|---|---| +| 「中」的个数 | **1** | **3** | **2** | **0** | 不看 | +| 判定 | ✓ | ✓ | ✓ | ✗ **断** | — | + +→ 连续了 **3 个轴** ⇒ 档位取 **3 连** + +**第 2 步 — 算路数**:`ways = 1 × 3 × 2 = 6` + +**第 3 步 — 查赔付表**:「中」的 3 连 = **0.5 倍** + +**第 4 步 — 算金额**(**赔付表已经是倍率,直接乘,没有中间商**): + +``` +本笔赢额 = 符号倍率 × ways × 投注总额 × 本波阶梯倍率 + = 0.5 × 6 × ¥20 × 1 + = 3.00 倍 × ¥20 = ¥60 +``` + +→ **本笔赢额 ¥60**,也就是 **3.00 倍投注总额**。 + +**换个投注档位看**(同一个盘面、同一笔奖): + +| 投注总额 | 计算 | 本笔金额 | 本笔倍率 | +|---|---|---|---| +| ¥1 | 0.5 × 6 × 1 × 1 | ¥3.00 | **3.00x** | +| ¥20 | 0.5 × 6 × 20 × 1 | ¥60 | **3.00x** | +| ¥100 | 0.5 × 6 × 100 × 1 | ¥300 | **3.00x** | +| ¥500 | 0.5 × 6 × 500 × 1 | ¥1500 | **3.00x** | + +> 倍率**恒定 3.00x**,金额随投注线性放大 —— 这就是我们要的性质。 + +**和原版截图对照**:截图写 `1 × 3 × 2 = 6`(第 2 步)→ `10 × 6 = 60`(第 3、4 步)。 +原版用的是"**每条基础投注的点数**",我们用"**倍率**",两者差一个固定系数 20: +`10 点 ÷ 20 = 0.5 倍`;`60 点 ÷ 20 = 3.00 倍` —— **结果完全一致,只是换了说法**。 + +**反例(截图右边那个 ✗)**:只要**中间某一轴一个「中」都没有**,链就在那一轴断掉 ⇒ **整笔不成奖**(不是"按 1×3=3 路算",是 **0**)。所以 `1、2、4` 有、`3` 没有 ⇒ 不成奖。 + +**同一笔落在不同波的结果**: + +| 波 | 阶梯 | 计算 | 赢额 | +|---|---|---|---| +| 第 1 波 | x1 | 0.5 × 6 × 20 × 1 = 3.00 倍 | ¥60 | +| 第 2 波 | x2 | 0.5 × 6 × 20 × 2 = 6.00 倍 | ¥120 | +| 第 3 波 | x3 | 0.5 × 6 × 20 × 3 = 9.00 倍 | ¥180 | +| 第 4 波起 | x5 | 0.5 × 6 × 20 × 5 = 15.0 倍 | **¥300** | + +**⚠️ 为什么不能把赔付表当成"倍投注总额"直接用**:原版那张表写的是 `10 / 60 / 100` 这种"点数",如果我们**原样照抄、又不除以 20**,这一笔就会变成 `10 × 6 × ¥20 = ¥1200`(**60 倍投注**)—— 基础旋 RTP 直接冲到几百个百分点,游戏发不出去。 +**现在我们改成直接把赔付表写成倍率(0.5 / 3 / 5 …),这个问题就自然消失了** —— 见 §2.1 上方那段说明。 + +**赔付表**(单位:**倍投注总额**;原版截图上的"点数"已预先 ÷20 内化,见下方说明): | 短码 | code | 中文名 | 3 连 | 4 连 | 5 连 | |---|---|---|---|---|---| -| `A` | FAT_CHOI | 發 | 15 | 60 | 100 | -| `B` | RED_DRAGON | 中 | 10 | 40 | 80 | -| `C` | WHITE_DRAGON | 白板(紫框) | 8 | 20 | 60 | -| `D` | CHAR_8 | 八萬 | 6 | 15 | 40 | -| `E` | DOT_MULTI | 花筒(四色) | 4 | 10 | 20 | -| `F` | BAMBOO_MULTI | 索(红绿) | 4 | 10 | 20 | -| `G` | DOT_PURPLE | 筒(紫绿) | 2 | 5 | 10 | -| `H` | BAMBOO_GREEN | 索(绿) | 2 | 5 | 10 | +| `A` | FAT_CHOI | 發 | **0.75** | **3** | **5** | +| `B` | RED_DRAGON | 中 | **0.5** | **2** | **4** | +| `C` | WHITE_DRAGON | 白板(紫框) | **0.4** | **1** | **3** | +| `D` | CHAR_8 | 八萬 | **0.3** | **0.75** | **2** | +| `E` | DOT_MULTI | 花筒(四色) | **0.2** | **0.5** | **1** | +| `F` | BAMBOO_MULTI | 索(红绿) | **0.2** | **0.5** | **1** | +| `G` | DOT_PURPLE | 筒(紫绿) | **0.1** | **0.25** | **0.5** | +| `H` | BAMBOO_GREEN | 索(绿) | **0.1** | **0.25** | **0.5** | | `W` | WILD | 百搭(元宝) | — | — | — | **不由卷轴落下**,只能由金色符号转化产生(**每个金色格各留 1 个,无上限**),且只可能出现在第 2/3/4 轴 | | `S` | SCATTER | 胡(夺宝符号) | — | — | — | 只触发免费旋转,不赔现金 | -> ✅ **赔付计量单位 = 投注总额 ÷ 20(已用排除法锁死,Q0)** +> ✅ **赔付表直接用"倍投注总额"表达(Q0 定案;原版的"÷20"已内化进表里)** > -> **两个已知事实**(均来自用户确认): -> ① 赔付表里的 `15 / 60 / 100` 是**最基础的 1 倍价**,不带任何阶梯倍率(用户 2026-09-21 原话:"这个是最基础的 1 倍的价格"); -> ② 规则页写着「游戏共有 1,024 个中奖路(固定)**并规定了 20 条基础投注**」——这句就是计量口径。 +> 原版规则页那张表的数字是 `15 / 60 / 100` 这类**"每条基础投注的点数"**——它规定「游戏共有 1,024 个中奖路(固定)**并规定了 20 条基础投注**」,所以要除以 20 才是倍率。 +> **我们不改数学,只改写法**:把那张表**预先除以 20**,赔付表上直接就是倍率。于是公式里不再出现 ÷20。 > -> **排除法**(用真实运行截图:投注总额 ¥8.00、横幅「共赢得 **0.80**」、阶梯条高亮 **x2**): +> **排除法(确认"原版点数 ÷ 20 = 倍率"这个换算是对的)**:用真实运行截图(投注总额 ¥8.00、横幅「共赢得 **0.80**」、阶梯条高亮 **x2**) > > | 假设 | 推论 | 结论 | > |---|---|---| > | 这 ¥0.80 **已含** x2 倍率 | 基准价 = 0.80 ÷ 2 = ¥0.40 = **1 个赔付单位** | ❌ 赔付表里**没有值为 1 的档**(最小是 2),不可能 | -> | 这 ¥0.80 **未含**倍率(x1 基准价) | 若 1 单位 = ¥0.40 → 最低档 3 连(赔付值 2)× ways 1 = **¥0.80** ✅ | ✅ 成立,且 `8.00 ÷ 20 = 0.40` **正好** | +> | 这 ¥0.80 **未含**倍率(x1 基准价) | 最低档 3 连(原版点数 2)× ways 1 × (8.00 ÷ 20) = **¥0.80** ✅ | ✅ 成立,且 `8.00 ÷ 20 = 0.40` **正好** | > -> → **赔付单位 = 投注总额 ÷ 20 是唯一自洽解**;同时确认「阶梯条高亮 x2」表示的是**下一波(第 2 波)即将生效的倍率**,不是本波已生效的。 +> → 原版点数 **2** 对应倍率 `2 ÷ 20 = **0.1 倍**`,与 `¥0.80 ÷ ¥8.00 = 0.1` 完全吻合 ✅ +> 同时确认「阶梯条高亮 x2」表示的是**下一波(第 2 波)即将生效的倍率**,不是本波已生效的。 > -> 所以本游戏的完整赔付公式是: +> **所以本游戏的完整赔付公式是(没有 ÷20)**: > ``` -> 本笔赢额 = 赔付表[X][连轴数] × ways × (投注总额 ÷ 20) × 本波阶梯倍率 -> = 赔付表[X][n] × ways × (box.bet × betMul ÷ 20) × rotationMul +> 本笔赢额 = 赔付表[X][连轴数] × ways × 投注总额 × 本波阶梯倍率 > ``` -> `PAY_UNIT_DIVISOR = 20` 写进 `config.py`,**不要散落在各处**。 +> 赔付表见上方表格(**已经是倍率**:發 3 连 0.75、中 3 连 0.5、筒 3 连 0.1 ……)。 > -> **真机复核(30 秒,做一次就彻底放心)**:投注总额 **¥8.00** 时,若 K=20,则**所有中奖金额都必须是 ¥0.40 的整数倍**(0.80 / 1.20 / 1.60 / 2.00 …)。只要出现过 **¥0.20 / ¥0.60 / ¥1.00** 这类**不是 0.40 整数倍**的金额,就说明 K=40,回来改一个常量即可。另外:¥8.00 时最低档符号 3 连 1 路应赔 **¥0.80**。 +> **换算对照表**(原版点数 → 我们的倍率,一个固定系数 20): > -> 为什么必须搞清:如果按"× 投注总额"实现,基础旋 RTP 会到 **300%~400%**(粗算:约 40% 的旋有 3+ 前缀链,均 1.7 路、均赔付 5.2 → 期望 3.5 倍/旋),游戏根本发不出去;除以 20 后基础旋自然 RTP ≈ **17%**,剩下交给免费旋转与阶梯——这正是这类游戏该有的结构。 -> 规则页展示的赔付表数值保持原样(15/60/100…),**但必须在规则页同时写明"每条基础投注"**,否则玩家会按"倍总额"理解,产生对账投诉。 +> | 符号 | 原版点数(3连 / 4连 / 5连) | 我们的倍率(3连 / 4连 / 5连) | +> |---|---|---| +> | 發 | 15 / 60 / 100 | **0.75 / 3 / 5** | +> | 中 | 10 / 40 / 80 | **0.5 / 2 / 4** | +> | 白板 | 8 / 20 / 60 | **0.4 / 1 / 3** | +> | 八萬 | 6 / 15 / 40 | **0.3 / 0.75 / 2** | +> | 花筒 / 索红绿 | 4 / 10 / 20 | **0.2 / 0.5 / 1** | +> | 筒 / 索绿 | 2 / 5 / 10 | **0.1 / 0.25 / 0.5** | +> +> **好处(为什么这么改更好)**: +> ① 公式少一步,实现、对账、仿真都不用再记那个 20; +> ② **赔付表与饰品桶表用同一个单位(倍率)** —— 后台看到桶是「0.5 倍 / 1.5 倍」,赔付表也是「0.5 倍 / 3 倍」,一脉相承,和玫瑰女郎的口径也一致; +> ③ 规则页不用再标「每条基础投注」,玩家看到 0.75 就知道是 0.75 倍,不会误解; +> ④ 生成端算出来的 `raw_mul` **直接就是倍率**,落桶不用再换算。 +> +> **唯一的取舍**:赔付表上的数字会**比原版"看着小"**(原版 `100` vs 我们 `5`)。这是原版把数字放大 20 倍展示的结果,数学完全一样。 +> 如果运营坚持"要看着慷慨",只需在**客户端展示层**把倍率 ×20 显示成原版数字(并且**必须标注"每条基础投注"**)——**内部数学一行都不用改**。 +> +> **⚠️ 千万不能只做一半**:如果照抄原版的 `15/60/100`、又去掉 ÷20,这一笔就会变成 `10 × 6 × ¥20 = ¥1200`(**60 倍投注**),基础旋 RTP 冲到几百个百分点,游戏发不出去。 +> +> **真机复核(30 秒,确认换算没搞错)**:原版投注总额 **¥8.00** 时,所有中奖金额都必须是 **¥0.40 的整数倍**(0.80 / 1.20 / 1.60 / 2.00 …);最低档符号 3 连 1 路应赔 **¥0.80**(= 0.1 倍)。 > 出现权重 `SYMBOL_WEIGHTS` 初值建议 `{A:5,B:6,C:8,D:9,E:11,F:11,G:13,H:13,S:2}`,最终由 §5 的 RTP 校准反推,不在方案里定死。 > ⚠️ **权重表里没有 `W`** —— 百搭不由卷轴落下(见 §2.2),`_fill` / 新旋 / 补位都必须断言不产出 `W`。 @@ -230,29 +313,46 @@ - 金色符号在**波次判定时用的是自然符号**,所以"金色「中」链"就是「中」的链;转化后下一波起它才变成通用百搭。 - 金色格也受"百搭同样会被消除"约束(§2.2 边界二):本波转成 W 之后,若在后面的波里参与成奖,就随那一波被消除。 -### 2.4 乘数阶梯(奖金翻倍) +### 2.4 乘数阶梯(奖金翻倍)—— 「本波阶梯倍率」是什么 -- **赔付表是"1 倍基准价"**(用户 2026-09-21 确认:"这个是最基础的 1 倍的价格")——表里的 15/60/100 不含任何阶梯倍率,阶梯是**在它之上再乘**的。 -- 本旋内**每一波(rotation,即一次消除结算)用一个乘数**;该波所有 1024 路赔付 × 该乘数。 -- **基础旋转(用户原话)**:一次消除 x1、两次消除 x2、三次消除 x3、**四次消除 x5**(第 4 波及以后都是 x5)→ `LADDER_BASE = (1, 2, 3, 5)`。 -- **免费旋转**:`LADDER_FS = (2, 4, 6, 10)`(截图里 FS 模式下卷轴上方的 x2 x4 x6 x10)——**FS 的第 1 波就是 x2**。 -- 第 n 波取 `ladder[min(n, 4)]`。由于"第 n 波存在"本身就等价于"第 n−1 波有中奖",无需额外判断递进条件。 -- **每旋重新从第一档开始**(基础旋从 x1、FS 旋从 x2),不跨旋累计。 +**先把三个词定死,后面全篇都按这个来**: + +| 词 | 含义 | 例子 | +|---|---|---| +| **一波**(rotation) | 级联里的**一次消除结算**。消除→掉落→补位之后,就是**下一波** | 第 1 波、第 2 波… | +| **符号倍率** | 从**赔付表**查出来的那个数(不含任何阶梯) | 中 3 连 = **0.5** | +| **本波阶梯倍率** | **这一波整体适用的阶梯倍率**——本波所有中奖都要乘它 | 第 1 波 x1、第 2 波 x2… | + +**它们怎么串起来**: + +``` +本笔基础倍率 = 符号倍率 × ways ← 一笔奖(一个符号的一条链) +本波基础倍率 = Σ 本波所有「本笔基础倍率」 ← 本波可能有多个符号同时中奖 +本波赢额 = 本波基础倍率 × 本波阶梯倍率 × 投注总额 +``` + +**本波阶梯倍率取多少**: + +- **基础旋转**:一次消除 x1、两次消除 x2、三次消除 x3、**四次消除 x5**(第 4 波及以后都是 x5)→ `LADDER_BASE = (1, 2, 3, 5)` +- **免费旋转**:`LADDER_FS = (2, 4, 6, 10)`(截图里 FS 模式下卷轴上方的 x2 x4 x6 x10)——**FS 的第 1 波就是 x2** +- 第 n 波取 `ladder[min(n, 4)]`。因为"第 n 波存在"本身就等价于"第 n−1 波有中奖",所以不需要额外判断递进条件 +- **每旋重新从第一档开始**(基础旋从 x1、FS 旋从 x2),**不跨旋累计** +- HUD 就是截图里那条「1024 路中奖组合」横条下方的 `x1 x2 x3 x5`(当前档位金色高亮) **完整算式串起来(一张表看懂)**: | 环节 | 值 | |---|---| -| 赔付表基准值 | `赔付表[X][连轴数]`(如 發 3 连 = 15) | +| 符号倍率 | `赔付表[X][连轴数]`(如 發 3 连 = 0.75、中 3 连 = 0.5) | | × 路数 | `ways`(各轴个数的乘积) | -| × 每单位金额 | `投注总额 ÷ 20`(Q0) | -| × 本波阶梯 | 基础旋 `1/2/3/5`;FS 旋 `2/4/6/10` | -| = 本笔赢额 | `赔付值 × ways × (bet×betMul÷20) × rotationMul` | +| = 本笔基础倍率 | 如 `0.5 × 6 = 3.00` | +| × 投注总额 | 直接乘(赔付表已经是倍率) | +| × 本波阶梯倍率 | 基础旋 `1/2/3/5`;FS 旋 `2/4/6/10` | +| = 本波赢额 | `本笔基础倍率 × 投注总额 × rotationMul` | -> 由此可推出**本局最小中奖**(基础旋第 1 波、最低档符号、1 路):`2 × 1 × (bet÷20) × 1 = bet ÷ 10` —— 这条直接约束了下注档下限(见 §2.8.2)。 -- 截图里那条「1024 路中奖组合」横条下方的 `x1 x2 x3 x5`(当前档位金色高亮)就是这条阶梯的 HUD。 +> 由此可推出**本局最小中奖**(基础旋第 1 波、最低档符号、1 路):`0.1 × 1 × 投注额 × 1 = 投注额 × 0.1` —— 这条直接约束了下注档下限(见 §2.8.2)。 -> 实现提示(来自 olympus 经验):Olympus 的"炸弹累计器"(`accumulator_before/after`,FS 内跨旋传递)机制**原封不动复用**——麻将胡了虽然乘数不跨旋累计,但"本步乘数 / 本波序号"需要每帧下发,字段名可直接沿用 `accumulator`(语义改为"本波乘数")以免客户端大改。**建议改名为 `rotationMul`**,避免误解。 +> 实现提示(来自 olympus 经验):Olympus 的"炸弹累计器"(`accumulator_before/after`,FS 内跨旋传递)机制**原封不动复用**——麻将胡了虽然乘数不跨旋累计,但"本波阶梯倍率 / 本波序号"需要每帧下发,字段名可直接沿用 `accumulator`(语义改为"本波阶梯倍率")以免客户端大改。**建议改名为 `rotationMul`**,避免误解。 ### 2.5 级联(tumble) @@ -260,11 +360,63 @@ ### 2.6 免费旋转 -- 触发:3+ 胡(任意位置)。 -- 赠送:12 + 2×(胡数−3);重触发同式。 +- 触发:**普通旋转**里落 **3+ 个胡**(卷轴任意位置)→ 进入免费旋转。 +- 首赠:**12 旋 + 每个额外胡 +2 旋**。 +- 🎯 **一屏最多只出现 5 个胡**(用户 2026-09-21 定案)——**不会出现 6 个及以上**。 + 于是单次赠送被锁死在 **`{12, 14, 16}`**: + + | 一屏胡数 | 赠送旋数 | + |---|---| + | 3 个 | **12** | + | 4 个 | **14** | + | 5 个(上限) | **16** | + | ~~6 个及以上~~ | 不出现 | - 免费旋转内乘数阶梯为 `[x2, x4, x6, x10]`。 -- 可重复触发;**总旋数上限 = 30 旋**(Q3 定案)。到顶后不再落 3+ 胡(生成端保证:一旦已发旋数到 30,后续 FS 旋不再摇出胡)。 -- 免费旋转内投注大小/倍数与触发时一致。 +- 免费旋转内投注档与触发时一致(中途不换注)。 + +#### 重触发与上限(Q3 + Q11 定案) + +- **重触发 = 同首触发公式**(规则页只有一句「免费旋转可以被重复触发」→ 那套规则再来一遍): + FS 内再落 3 / 4 / 5 个胡 → **再送 12 / 14 / 16 旋**。 +- **总旋数上限 `MAX_FREE_SPINS = 36`**。 +- 🎯 **硬规则:发不满就不发**(用户 2026-09-21 定案)—— + 生成端在**落 3+ 胡之前先检查**: + ``` + if 已发放总旋数 + 本次应赠旋数 ≤ 36 → 正常落 3+ 胡并发放 + else → 本旋【不落】3+ 胡(绝不出现"落了胡但只补几旋") + ``` + 用户原话:「**可以 36 旋,但是如果出现 32 旋了,后面的免费旋转肯定是不会再出 3 个以上的 fs 了。**」 +- 🎯 **触发边界(用户追问确认)**:因为单次最少赠 12 旋,所以 + + ``` + 已发放 ≤ 24 → 才可能再触发(24 + 12 = 36 ≤ 36) + 已发放 ≥ 25 → 后面所有旋一律不再落 3+ 胡 + ``` + + ⇒ **整局 FS 永远 ≤ 36 旋,37 旋以上百分之百不会出现**。 +- **可达的旋数只有这 9 个值**(起点 12~16,每次追加 12~16,追加后不超 36): + + ``` + 12 / 14 / 16 ← 没重触发 + 24 / 26 / 28 / 30 / 32 ← 有重触发 + 36 ← 只有 12+12+12 这一种走法能到 + ``` + + 其中 **26 就是"最后一次能触发的边界情况之一"**:`12 + 14 = 26` 之后 `26 + 12 = 38 > 36`,所以**从 26 旋往后,盘面再也不会出现 3 个及以上的胡**(见下方示例)。 +- 举例: + + | 时刻 | 检查 | 结果 | + |---|---|---| + | 基础旋落 3 个胡 | 0 + 12 = 12 ≤ 36 ✓ | 发 12 旋 | + | FS 第 3 旋又落 3 个胡 | 12 + 12 = 24 ≤ 36 ✓ | 发到 24 旋 | + | FS 第 10 旋又落 3 个胡 | 24 + 12 = **36** ≤ 36 ✓ | 发到 36 旋(顶到上限) | + | 已发放 **24** 旋时落 4 个胡 | 24 + 14 = 38 > 36 ✗ | **不落胡**(这一旋只能落"恰好 3 个胡") | + | 已发放 **26** 旋时落 3 个胡 | 26 + 12 = 38 > 36 ✗ | **不落胡,且后面所有旋都不再出胡** | + +> 用户原话「再出现 fs,还会送 2 次」对应的是规则页**另一句**:「**每个额外的夺宝符号都将触发多 2 次的免费旋转**」——指同一屏里第 4、5 个胡各 +2 旋,**不是**重触发的额度。 +> 三句合起来才是完整规则:① 一屏 3 个胡 = **12 旋**;② 每多 1 个胡 **+2 旋**(最多 5 个胡 → 16 旋);③ 这套规则**在 FS 内可以再来一遍**(受 36 旋上限 +"发不满就不发"约束)。 +> +> 实现上保留常量以便口径反复时一行切换:`FS_RETRIGGER_AWARD = null`(`null` = 走首触发同式,当前口径;填整数 = 固定追加)。 ### 2.7 封顶 @@ -275,77 +427,92 @@ 原版:投注大小 0.02~50.00 × 投注倍数 1~10,另有现金钱包与自动旋转。 -**方案 A(推荐)**:`箱子 = 投注档`,请求可带 `betMul(1~10)`: -- `price(实际扣费) = box.bet × betMul × costMultiplier`(本游戏 costMultiplier ≡ 1) -- **回合池里存的全是倍率**(`actual_mul`),与 `bet`/`betMul` 无关 → 调档/调倍数**不需要重生成池子**(这正是玫瑰女郎能做到"改配比不动池子"的根因)。 -- 奖池水位按 `(boxId, audience)` 聚合,`betMul` 只影响单局金额,不影响池结构。 +**方案(推荐)**:`箱子 = 投注档`,**不设投注倍数**(见 §2.8.1): +- `price(实际扣费) = box.bet`(本游戏 costMultiplier ≡ 1) +- **回合池里存的全是倍率**(`actual_mul`),与投注额无关 → **改档位不需要重生成池子**(这正是玫瑰女郎能做到"改配比不动池子"的根因)。 +- 奖池水位按 `(boxId, audience)` 聚合,投注额只影响单局金额,不影响池结构。 -**方案 B(不推荐)**:给每个 `bet × betMul` 组合建一个箱子 → 100 个箱子、池子碎片化、水位口径混乱。 +**曾经的备选(已否)**:给"档位 × 倍数"每个组合建一个箱子 → 池子碎片化、水位口径混乱。 #### 2.8.1 设计多少个下注额、哪几个、能多大? **先分清两件事,因为成本差一个数量级:** -| 加什么 | 池子要不要动 | 成本 | +| 加什么 | 池子要不要动 | 单局理论最大赔付 | 成本 | +|---|---|---|---| +| **加档位**(多一个箱子) | **要**:每档一个独立回合池 ~4500 回合 / 8 万帧 | 线性上涨(`500x × 新档位`) | 贵(生成时间 + 40 条配比 + 一套水位) | +| **加 betMul 上限** | **不用**:池子只存倍率(`actual_mul`),与下注额无关 | **同箱内直接放大 10 倍** | 几乎为零 | + +> ⚠️ **关键认识**:`betMul` 不是"免费做大",它**把同一箱内的金额跨度拉到 10 倍,等价于偷偷多开了 9 个档位**,却只用一个池子——风控口径和水位观察口径都会变粗。 +> 而且在 500x 封顶下,`betMul` 会把**理论单局最大赔付**直接乘以 10。 +> 所以本次**不再推荐"档位 × 倍数"双控件**,改为**只保留投注档**(与玫瑰女郎完全一致)。 + +#### 2.8.1 推荐方案(11 档,只保留投注档) + +**结论:`1 / 3 / 5 / 10 / 20 / 30 / 50 / 100 / 200 / 300 / 500`,共 11 档;不设投注倍数。** + +| boxId | 投注额(= 扣费) | boxId | 投注额 | +|---|---|---|---| +| 1 | ¥1 | 7 | ¥50 | +| 2 | ¥3 | 8 | ¥100 | +| 3 | ¥5 | 9 | ¥200 | +| 4 | ¥10 | 10 | ¥300 | +| 5 | ¥20 | **11** | **¥500** | +| 6 | ¥30 | | | + +**为什么是这 11 档** + +| 维度 | 测算 | 结论 | |---|---|---| -| **加档位**(多一个箱子) | **要**:每档一个独立回合池 ~4500 回合 / 8 万帧 | 贵(生成时间 + 40 条配比 + 一套水位) | -| **加 betMul 上限** | **不用**:池子只存倍率(`actual_mul`),与下注额无关 | **几乎为零** | +| **下限(不丢分)** | 最小中奖 = 投注 ÷ 10(最低档符号 3 连 × 1 路 × x1);¥1 → **¥0.10** | ✅ 远高于 0.01 | +| **上限(风控)** | 单局理论最大赔付 = **500x × 500 = ¥250,000**;玫瑰女郎现状 = 1000x × 300 = **¥300,000** | ✅ **压在玫瑰女郎之下** | +| **档位取整** | 档位都是整数 → 任何中奖金额都是整分 | ✅ 不会出现除不尽的数 | +| **前 10 档** | 与玫瑰女郎**一字不差** | ✅ 玩家熟悉、报表口径统一 | +| **规模** | 11 箱 × ~4500 = **5 万回合 / ~90 万帧** / 生成 20~30 分钟 / 704 条配比行 | ✅ 与玫瑰女郎同量级(它 30 箱 / 106 万帧) | -→ 所以"想把数额做大"有两条路:**改档位表**(贵)或**抬高倍数上限**(免费)。 +**为什么不设投注倍数**(与玫瑰女郎保持一致的做法) -**推荐:12 档,覆盖到 ¥1000**(在玫瑰女郎 10 档基础上加 500 / 1000 两档),betMul 保持 1~10: +1. **它会把风控上限放大 10 倍**:带 1~10 倍时,顶档只能压到 ¥50(50×10 = 500),实际覆盖反而更差;想保留大额档位就得放弃倍数。 +2. **同一箱内金额跨度 10 倍**,奖池水位的观察口径变粗(水位是按箱 × 身份记的)。 +3. **玩家要多调一个控件**,玫瑰女郎只有一个"选箱",本平台玩家已经习惯。 +4. **想做大的正路是"加档"**:加一个箱不影响任何已有逻辑,**客户端不用改**(箱子列表是接口下发的),而加倍数做不到。 -| boxId | box.bet | × betMul(1~10) 实际扣费 | +**如果以后要更大的数额**(升级路径,按需再开) + +| 目标 | 做法 | 单局理论最大赔付 | |---|---|---| -| 1 | 1 | 1 ~ 10 | -| 2 | 3 | 3 ~ 30 | -| 3 | 5 | 5 ~ 50 | -| 4 | 10 | 10 ~ 100 | -| 5 | 20 | 20 ~ 200 | -| 6 | 30 | 30 ~ 300 | -| 7 | 50 | 50 ~ 500 | -| 8 | 100 | 100 ~ 1000 | -| 9 | 200 | 200 ~ 2000 | -| 10 | 300 | 300 ~ 3000 | -| **11** | **500** | **500 ~ 5000** | -| **12** | **1000** | **1000 ~ 10000** | +| 到 ¥1000 | 加第 12 档 ¥1000 | 500x × 1000 = **¥500,000** ⚠️ 超玫瑰女郎 67%(要同时降封顶,见下一行)| +| 到 ¥1000 **且守住风控** | 加 ¥1000 档的同时把封顶降到 **300x** | 300x × 1000 = **¥300,000** ✅ 正好对齐玫瑰女郎 | +| 到 ¥2000 | 加 ¥2000 档 + 封顶 150x | ¥300,000 ✅(但 150x 封顶会让桶表整体缩一档,要重配池子) | -- 成本增量很小:12 箱 → 5.4 万回合 / ~96 万帧 / 生成 20~30 分钟 / **480 条**配比行(比 10 档只多 20%)。 -- **单注上限 ¥10,000 仍在平台既有范围内**:玫瑰女郎 `buy_fs` 最高一注就是 `300 × 50 = ¥15,000`,所以这不是新量级的敞口。 -- 备选(想零成本扩大):**保持 10 档,把 betMul 放到 1~20** → 实际覆盖 1~6000,池子一行不动。 +> 每加一档的成本:+4500 回合 / +8 万帧 / +52 条配比行 / 多一套水位要运营盯。**建议先上 11 档,看实际流水再决定要不要开高档**。 -**⚠️ 做大之前必须先定的两件事** +**已否掉的方案** -1. **单局最大赔付口径(风控)** —— **已定案:封顶 500x**: - -| 组合 | 单局最大赔付 | -|---|---| -| 玫瑰女郎现状(1000x × bet 300) | ¥300,000 | -| **麻将胡了(500x × bet 10000)** | **¥5,000,000** | -| ~~麻将胡了(1000x × bet 10000)~~ | ~~¥10,000,000~~(已否) | - - 运行期还有奖池水位 `payoutCap` 兜底(水不够就出不了大奖),但 500x 这条线是玩家预期与风控口径,**代码里写死为 `MAX_WIN_CAP_MUL = 500`**。 +- ❌ 原版双控件(档位 1~300 × 倍数 1~10):顶档 ¥3000 一注、单局理论最大赔付 **¥1,500,000**(玫瑰女郎的 5 倍),且同箱金额跨度 10 倍。 +- ❌ 12 档到 ¥1000 + 倍数 1~10(最高 ¥10,000 一注):单局理论最大赔付 **¥5,000,000**,是玫瑰女郎的 16 倍,风控口径直接破。 +- ❌ 直接抄原版 0.02~50.00:那是社娱场面额,本平台是金币场;而且原版 0.02 是"每条基础投注"的金额(见 §2.8.2),当成总额抄过来会让最低中奖显示成 +0.00。 2. **冷门高档位会水位饥饿**:¥500 / ¥1000 这两档玩家天然少,水位长期接近 0 → `payoutCap` 把它们压到出不了奖。**上线时必须给这两档单独注一笔初始水位**,或干脆先只放 10 档、等有需求再开高档(**加档不需要改客户端,只需加箱 + 生成池子**)。 **为什么不是更多档** 1. **每多一档 = 多一整套成本**(这才是决定档位数量的真正约束): - 多一个**回合池**:约 4500 回合 / 8 万帧,池子必须按箱独立生成; - - 多 40 条**配比行**(10 桶 × 4 身份)要配、要重算、要维护模板; + - 多 64 条**配比行**(16 桶 × 4 身份)要配、要重算、要维护模板; - 多一套**奖池水位**(4 个身份各一条)要运营盯。 10 档 → 4.5 万回合 / 80 万帧 / 生成 15~25 分钟 / 400 条配比行,正好是玫瑰女郎已经在跑的量级。 2. **档位越多,冷门档越容易"饿死"**:奖池水位是**按箱独立**的,没人玩的档水位长期为 0 → `payoutCap = max(0, 水位+成本) × rtp` 被压到接近 0 → **那个档几乎出不了奖**,只能靠运营注水救。所以档位不是越多越好。 -3. **玩家侧足够**:1~1000 的 12 档 + 1~10 倍 = 实际覆盖 1~10000,比原版 0.02~50 宽两个数量级,选注习惯也够密。 +3. **玩家侧足够**:1~500 共 11 档,比原版 0.02~50 的覆盖面更宽;相邻档位间距 2~3 倍,选注手感是行业常规。 4. **对齐玫瑰女郎**:前 10 档与玫瑰女郎一字不差,玩家已熟悉,运营报表/返利阈值/消费排行榜口径统一。 **不推荐的做法** -- ❌ 直接抄原版的 0.02~50.00:那是社娱场(social casino)面额,本平台是金币场,玫瑰女郎最低档是 ¥1;而且 §2.8.2 会证明 **0.02 档下最小中奖会丢分**(显示成 +0.00)。 +- ❌ 直接抄原版的 0.02~50.00:那是社娱场(social casino)面额,本平台是金币场、玫瑰女郎最低档是 ¥1;**而且原版的 0.02 是"每条基础投注"的金额**(见 §2.8.2 的说明),当成总额抄过来会让最低中奖显示成 +0.00。 - ❌ 为了"档位够密"铺 20~30 档:池子生成与后台维护成本线性上涨,且冷门档水位饥饿问题成倍放大。 - ❌ 一次把最高档拉到 ¥5000 / ¥10000:单局最大赔付会到千万级,且这两档几乎没人玩、水位必饿死。 **两个附带决策** - **档位表里 3 / 30 保留**(它们是整数,只是不是 10 的整数倍;玫瑰女郎就这么定的,玩家点"+"往上加时体验更顺),不改成 2 / 25 之类。 -- **投注倍数默认 1**,只作为"快速加倍/加注"的细调;若实测玩家几乎不用,可后续在需求里砍掉(**表结构与接口预留,砍掉不用改池子**)。 +- **不设投注倍数**;接口里的 `betMul` 作为**预留字段**保留(缺省 1、当前服务端只接受 1),万一以后要开也不用动池子。 > ⚠️ **上线前必须做的一件事**:新游戏的奖池水位从 0 开始,而 `payoutCap = max(0, 水位 + 本局成本) × rtp` —— 意味着**开局头几局玩家最多只能赢 0.8 倍下注**,大奖要等水位被玩起来才出得来。所以**上线前要按箱 × 身份人工注水**(后台奖池水位页,原因必填),否则前几天的"爆奖概率"会低到不像这个游戏。玫瑰女郎也是这么运营的。 @@ -355,21 +522,27 @@ **约束一:来自赔付表 —— 最低档下注的最低中奖不能小于 ¥0.01** -赔付公式里最小的那一笔是:最低档符号(`G`/`H`,3 连赔付值 **2**)× ways **1** × 第 1 波阶梯 **x1**: +赔付公式里最小的那一笔是:最低档符号(`G`/`H`,**3 连赔 0.1 倍**)× ways **1** × 第 1 波阶梯 **x1**: ``` -最小中奖 = 2 × (投注总额 ÷ 20) = 投注总额 ÷ 10 +最小中奖 = 0.1 × 1 × 投注总额 = 投注总额 × 0.1 ``` | 投注总额 | 最小中奖 | 结论 | |---|---|---| -| ¥0.02(原版最低档) | ¥0.002 | ❌ 金额字段是 `DECIMAL(10,2)`,**显示成 +0.00**,玩家会认为"中了不给钱" | | ¥0.10 | ¥0.01 | ⚠️ 理论下限,刚好不丢分 | | **¥1(建议最低档)** | **¥0.10** | ✅ 留 10 倍余量 | -→ **`box.bet ≥ 0.10` 是硬下限**;这是"不能照抄原版 0.02"的**可证明理由**(不是我拍脑袋说社娱场面额)。 +→ **`box.bet ≥ 0.10` 是硬下限**(我们的模型里 `box.bet` = **投注总额**)。 -**附带的一个好性质**:档位全部取整数时,`unit = bet ÷ 20` 必是 **0.05 的整数倍**(1→0.05、3→0.15、5→0.25、20→1.00、300→15.00),所以**任何"赔付值 × ways"都是整分**,不会出现 0.0333 这种除不尽的数。`betMul` 取整也一样(3×3=9 → 0.45)。建议档位表**只放整数**,把这条性质保住。 +> ⚠️ **关于原版"投注大小 0.02~50.00"的理解**:规则页有一条「一条中奖路所得的奖励等同于赔付表上的赔付值**乘以投注大小和投注倍数**」。 +> 这条要成立,只有一种读法:**原版的「投注大小」= 每条基础投注的金额**(即 `投注总额 = 投注大小 × 20 × 投注倍数`)。 +> 按这个读法,原版最低档 0.02 的**最小中奖 = 2 × 0.02 = ¥0.04**,**不会丢分**。 +> 而我们的模型里 `box.bet` 是**投注总额**,所以下限是 `0.10`;单纯把原版的 `0.02` 抄过来当**总额**才会丢分(0.002 显示成 +0.00)。 +> **待确认(一个 30 秒的真机测试)**:把原版「投注大小」设成 **1.00**、「投注倍数」设成 **1**,看「投注总额」显示 **20.00** 还是 **1.00**—— +> 显示 **20.00** ⇒ 投注大小 = 每条基础投注(上面那种读法,规则页第 1 条成立);显示 **1.00** ⇒ 投注大小 = 总额(那第 1 条就该改写成"乘以投注总额后除以 20")。 + +**附带的一个好性质**:**赔付表里的倍率都是 0.05 的整数倍**(0.1 / 0.2 / 0.25 / 0.3 / 0.4 / 0.5 / 0.75 / 1 / 2 / 3 / 4 / 5),而投注档又都是整数金额,所以**任何一笔中奖金额都是整分**,不会出现 0.0333 这种除不尽的数。建议投注档表**只放整数**,把这条性质保住。 **约束二:来自饰品区间表 —— 最低档饰品的价格决定"每局保底返奖"** @@ -401,36 +574,242 @@ | `normal` | 普通旋转 | **1x** | 唯一的玩法:自然触发免费旋转(3+ 胡) | > 砍掉 `ante` / `buy_fs` 带来的连锁简化(都要落地,别留半套): -> - `MahjongModeType` 只留 `normal` 一个枚举值,`costMultiplier` 恒为 1,`price = box.bet × betMul`; +> - `MahjongModeType` 只留 `normal` 一个枚举值,`costMultiplier` 恒为 1,`price = box.bet`; > - **没有 `need_fs` 过滤**了(那是 `buy_fs` 专用),挑回合只按桶; > - 箱数从 30 降到 **10**(只按投注档分箱)→ 池子规模、生成时间、后台配置量全部降到 1/3; > - 高倍桶(100x 以上)**只能靠自然触发 FS 的回合供给**,没有"买 FS"那批高倍回合兜底 → 生成端要用 bias 主动**构造**高倍局,不能靠碰运气(见 §5.4 风险)。 --- +### 2.9 赢奖计算公式(最终版 · 一页速查) + +> 这就是**唯一一套**公式。生成端用它造回合;运行端只用最后一行的结论。 + +**常量(配置,写死在 `config.py`)** + +| 常量 | 值 | 说明 | +|---|---|---| +| `PAYTABLE[X][n]` | 0.75 / 0.5 / 0.4 / 0.3 / 0.2 / 0.1 … | **符号倍率**,单位 = 倍投注总额(表见 §2.1) | +| `LADDER_BASE` | `(1, 2, 3, 5)` | 基础旋的**本波阶梯倍率** | +| `LADDER_FS` | `(2, 4, 6, 10)` | 免费旋的**本波阶梯倍率** | +| `MAX_WIN_CAP_MUL` | **500** | 单局封顶倍率 | +| 投注总额 | `box.bet` | 玩家选的投注档(11 档,无倍数) | + +**层级公式(自下而上四层)** + +``` +【第 1 层】一笔奖(某符号的一条前缀链) + n = 链长(从第 1 轴起连续有「该符号或百搭」的轴数,n ∈ {3,4,5}) + ways = 链内各轴「该符号 + 百搭」个数的乘积 + 本笔基础倍率 = PAYTABLE[X][n] × ways + +【第 2 层】一波(rotation = 一次消除结算) + 本波阶梯倍率 = ladder[min(波序, 4)] # 基础旋 1/2/3/5;FS 旋 2/4/6/10 + 本波基础倍率 = Σ 本波所有符号的所有「本笔基础倍率」 + 本波赢额(倍率) = 本波基础倍率 × 本波阶梯倍率 + +【第 3 层】一旋(spin) + 本旋倍率 = Σ 本旋所有波的「本波赢额(倍率)」 + (注意:本波阶梯倍率**每旋重置**,不跨旋累计) + +【第 4 层】一局(round) + 本局原始倍率 = Σ 所有旋的「本旋倍率」 + 本局总倍率 = min(本局原始倍率, 500) # ← 封顶在这里截断 + 本局赢额 = 本局总倍率 × 投注总额 +``` + +**合并成一行(实现速查)** + +``` +本局赢额 = min( Σ旋 Σ波 [ (Σ符号 PAYTABLE[X][n_X] × ways_X) × ladder[min(波序,4)] ] , 500 ) × 投注总额 +``` + +**运行端只有一行** + +因为回合池里**存的就是倍率**(`round.actual_mul`,见 §6.7),运行时不需要重算上面那套: + +``` +本局赢额 = round.actual_mul × 投注总额 # 与玫瑰女郎同一个公式 +``` + +→ 再走「落 `prize_range` 区间 → 匹配饰品 → 写背包」发奖(§6.3)。 + +**示例核对**(数据来自 §15 那个完整盘面,投注总额 ¥100) + +| 波 | 一笔奖 | 符号倍率 | ways | 本笔基础倍率 | 本波阶梯倍率 | 本波基础倍率(合计) | 本波赢额(倍率) | 本波赢额(钱) | +|---|---|---|---|---|---|---|---|---| +| 1 | 中 3 连 | 0.5 | 4 | 2.00 | x1 | **2.80** | 2.80 | ¥280 | +| 1 | 白板 3 连 | 0.4 | 2 | 0.80 | x1 | ↑ 并入上行 | ↑ | ↑ | +| 2 | 八萬 4 连 | 0.75 | 4 | 3.00 | x2 | 3.00 | 6.00 | ¥600 | +| 3 | 花筒 5 连 | 1 | 1 | 1.00 | x3 | 1.00 | 3.00 | ¥300 | +| 4 | 筒 5 连 | 0.5 | 2 | 1.00 | x5 | **2.00** | 10.00 | ¥1000 | +| 4 | 索红绿 5 连 | 1 | 1 | 1.00 | x5 | ↑ 并入上行 | ↑ | ↑ | + +- **本局总倍率 = 2.80 + 6.00 + 3.00 + 10.00 = 21.80 倍**(未触 500x 封顶) +- **本局赢额 = 21.80 × ¥100 = ¥2180** ✓(与 §15 总账一致) +- 全程只有 **1 个胡** → 若满 3 个胡就会先走 FS(FS 内阶梯换成 `2/4/6/10`,其余公式不变) + +--- + +### 2.9.1 完整消除示例:跟着公式走一遍,看钱怎么来 + +> **投注档 ¥20**(= 投注总额,无倍数)。3 波结束。为了看清公式,这个盘面**故意不含百搭/金色/胡**。 +> 图例:`A`發 `B`中 `C`白板 `D`八萬 `E`花筒 `F`索红绿 `G`筒 `H`索绿。 + +#### 第 1 波(基础旋第 1 波 → 本波阶梯倍率 = **x1**) + +| 轴 → | 1 | 2 | 3 | 4 | 5 | +|---|---|---|---|---|---| +| 行1 | **B** | **C** | **B** | E | H | +| 行2 | D | **B** | A | G | F | +| 行3 | E | D | **C** | A | G | +| 行4 | **C** | E | H | F | D | + +**① 逐符号找链 → 算「本笔基础倍率」** + +| 符号 | 轴1 | 轴2 | 轴3 | 轴4 | 链长 n | ways | 符号倍率 | 本笔基础倍率 | +|---|---|---|---|---|---|---|---|---| +| 中 `B` | 1(行1) | 1(行2) | 1(行1) | 0(E/G/A/F) | **3** | 1×1×1 = **1** | 0.5 | **0.50** | +| 白板 `C` | 1(行4) | 1(行1) | 1(行3) | 0 | **3** | 1×1×1 = **1** | 0.4 | **0.40** | +| 八萬 `D` | 1(行2) | 1(行3) | 0 | — | — | — | — | 轴3 断,不成奖 | +| 花筒 `E` | 1(行3) | 1(行4) | 0 | — | — | — | — | 轴3 断 | +| 其余 | 轴1 里没有 | — | — | — | — | — | — | 不成奖 | + +**② Σ 得「本波基础倍率」**:`0.50 + 0.40 = **0.90**` +**③ × 本波阶梯倍率**:`0.90 × x1 = **0.90 倍**` +**④ × 投注总额**:`0.90 × ¥20 = **¥18**` + +> 注意这一波**赢了但不到 1 倍**——只中了 3 连、ways 又只有 1,这是麻将基础旋的常态。想赚钱要靠**更多的 ways** 和**更高的波次**。 + +**消除 6 格**:(行1,轴1)B、(行2,轴2)B、(行1,轴3)B、(行4,轴1)C、(行1,轴2)C、(行3,轴3)C → 全部变空洞(本波无金色 → 不产生百搭) + +**落定后**(补位取值见下): + +| 轴 → | 1 | 2 | 3 | 4 | 5 | +|---|---|---|---|---|---| +| 行1 | **E** | **G** | **F** | E | H | +| 行2 | **A** | **A** | **G** | G | F | +| 行3 | D | D | A | A | G | +| 行4 | E | E | H | F | D | + +#### 第 2 波(第 2 波 → 本波阶梯倍率 = **x2**) + +**① 找链** + +| 符号 | 轴1 | 轴2 | 轴3 | 轴4 | 轴5 | 链长 n | ways | 符号倍率 | 本笔基础倍率 | +|---|---|---|---|---|---|---|---|---|---| +| 發 `A` | 1(行2) | 1(行2) | 1(行3) | 1(行3) | 0 | **4** | 1×1×1×1 = **1** | 3 | **3.00** | +| 八萬 `D` | 1(行3) | 1(行3) | 0 | — | — | — | — | — | 轴3 断 | +| 花筒 `E` | 2(行1、行4) | 1(行4) | 0 | — | — | — | — | — | 轴3 断 | +| 其余 | 轴1 里没有 | — | — | — | — | — | — | — | 不成奖 | + +**② 本波基础倍率** = `3.00` +**③ × x2** = `6.00 倍` **④ × ¥20** = **¥120** + +**消除 4 格**:(行2,轴1)A、(行2,轴2)A、(行3,轴3)A、(行3,轴4)A(无金色 → 无百搭) + +**落定后**(轴1 行1 补 `G`、轴2 行1 补 `G`、轴3 行1 补 `D`、轴4 行1 补 `D`): + +| 轴 → | 1 | 2 | 3 | 4 | 5 | +|---|---|---|---|---|---| +| 行1 | **G** | **G** | **D** | **D** | H | +| 行2 | E | G | F | E | F | +| 行3 | D | D | G | G | G | +| 行4 | E | E | H | F | D | + +#### 第 3 波(第 3 波 → 本波阶梯倍率 = **x3**) + +**① 找链** + +| 符号 | 轴1 | 轴2 | 轴3 | 轴4 | 轴5 | 链长 n | ways | 符号倍率 | 本笔基础倍率 | +|---|---|---|---|---|---|---|---|---|---| +| **八萬 `D`** | 1(行3) | 1(行3) | 1(行1) | 1(行1) | 1(行4) | **5** | 1×1×1×1×1 = **1** | **2** | **2.00** | +| **筒 `G`** | 1(行1) | **2**(行1、行2) | 1(行3) | 1(行3) | 1(行3) | **5** | 1×**2**×1×1×1 = **2** | **0.5** | **1.00** | +| 花筒 `E` | 2(行2、行4) | 1(行4) | 0 | — | — | — | — | — | 轴3 断 | +| 其余 | 轴1 里没有 | — | — | — | — | — | — | — | 不成奖 | + +**② 本波基础倍率** = `2.00 + 1.00 = 3.00`(**两个符号同时中奖,相加**) +**③ × x3** = `9.00 倍` **④ × ¥20** = **¥180** + +→ 补位取"不会再形成新链"的组合 → **本旋结束(共 3 波)** + +#### 第 4 层:算出整局金额 + +| 波 | 本波基础倍率 | 本波阶梯倍率 | 本波赢额(倍率) | 本波赢额(钱) | +|---|---|---|---|---| +| 1 | 0.90 | x1 | **0.90** | ¥18 | +| 2 | 3.00 | x2 | **6.00** | ¥120 | +| 3 | 3.00 | x3 | **9.00** | ¥180 | +| | | | **本局总倍率 15.90** | **¥318** | + +**套最终公式**: + +``` +本局总倍率 = min(0.90 + 6.00 + 9.00, 500) = min(15.90, 500) = 15.90 ← 未触顶,原样保留 +本局赢额 = 15.90 × ¥20 = ¥318 +``` + +**发奖**:`¥318` 落 `prize_range` 的对应区间(例如 250~400)→ 匹配到那件饰品 → 写背包(`MAHJONG_BOX(15)`)→ 末帧玩家选「放入背包 / 立即分解」。 +`record.total_win = 15.90`、`record.settled_win = 318`、`round.actual_mul = 15.90`。 + +> **如果这是免费旋转里的旋**:只需把「本波阶梯倍率」换成 `LADDER_FS = (2,4,6,10)`,其余每一层**完全一样**。例如本示例第 1 波落在 FS 里就是 `0.90 × x2 = 1.80 倍 = ¥36`。 + +--- + ## 3. RTP 与桶设计(数学核心) -### 3.1 倍率桶(10 档) +### 3.1 饰品桶表(16 档,0x ~ 500x)—— **沿用玫瑰女郎的桶表结构** -替代玫瑰女郎的"饰品桶"。约定:区间连续无缝、`expect_mul = (low+high)/2`、顶桶受封顶影响"匹配区间 ≠ 发放上限"。 +**直接沿用玫瑰女郎那张 17 档桶表,只去掉最高那档**(它到 1000x,我们封顶 500x,1000x 那档永远不会被命中)。 +这样后台页面、配比算法、收敛逻辑、运营经验**全部零改动**;与玫瑰女郎的差异只有顶桶一行。 -| id | 名称 | range_low | range_high | expect_mul | 说明 | -|---|---|---|---|---|---| -| 1 | 空 | 0 | 0 | 0.00 | 完全未中 | -| 2 | 微 | 0.01 | 0.50 | 0.25 | | -| 3 | 小 | 0.50 | 2.00 | 1.25 | | -| 4 | 中 | 2.00 | 5.00 | 3.50 | | -| 5 | 中大 | 5.00 | 15.00 | 10.00 | | -| 6 | 大 | 15.00 | 40.00 | 27.50 | 自然触发的 FS 多落这里 | -| 7 | 很大 | 40.00 | 100.00 | 70.00 | | -| 8 | 巨 | 100.00 | 250.00 | 175.00 | | -| 9 | 超级 | 250.00 | 400.00 | 325.00 | | -| 10 | 封顶 | 400.00 | 700.00 | **500.00** | 区间上界故意开到 1.4× 封顶(对齐玫瑰女郎"匹配区间 ≠ 发放上限"),**实际发放截断 500** | +物理表名 `game_mahjong_bucket`;**后台页显示名「饰品桶」**(对应玫瑰女郎的 `game_olympus_ornament`,见附录 A 映射)。 -> 因为封顶是 **500x**(Q4),顶桶的"名义期望 500"在池内实测均值会明显低于 500(被截断的局都堆在这里),配比计算时要按池内 `AVG(actual_mul)` 走,必要时用 `PAYOUT_EXPECT_OVERRIDE`(玫瑰女郎顶桶就是这么用 835 顶 1000 的)。 +| 桶ID | 名称 | 期望倍率 | 落桶下限 | 落桶上限 | +|---|---|---|---|---| +| 1 | 0倍饰品 | 0 | 0 | 0 | +| 2 | 0.5倍饰品 | 0.5 | 0.01 | 1 | +| 3 | 1.5倍饰品 | 1.5 | 1 | 2 | +| 4 | 2.5倍饰品 | 2.5 | 2 | 3 | +| 5 | 4倍饰品 | 4 | 3 | 5 | +| 6 | 6.5倍饰品 | 6.5 | 5 | 8 | +| 7 | 10倍饰品 | 10 | 8 | 12 | +| 8 | 16倍饰品 | 16 | 12 | 20 | +| 9 | 25倍饰品 | 25 | 20 | 30 | +| 10 | 40倍饰品 | 40 | 30 | 50 | +| 11 | 60倍饰品 | 60 | 50 | 70 | +| 12 | 100倍饰品 | 100 | 70 | 130 | +| 13 | 150倍饰品 | 150 | 130 | 170 | +| 14 | 200倍饰品 | 200 | 170 | 230 | +| 15 | 300倍饰品 | 300 | 230 | 370 | +| **16** | **500倍饰品(封顶)** | **500** | 370 | **700** | + +约定(与玫瑰女郎一致): +- **期望倍率 = (落桶下限 + 落桶上限) / 2**;区间**连续无缝、覆盖 0~封顶**。 +- **桶 1 = 空转**(本局没有任何 1024 路中奖)。注意:**空转局照样发最低档饰品**("必得饰品",见 §6.3),所以桶 1 也是要配库存的。 +- **桶 16 = 封顶档**:区间上界**故意开到 700(1.4× 封顶)**,这样"本来该赔 600x 的局"也落进这一档;**实际发放一律截断到 500x**(对齐玫瑰女郎"匹配区间 ≠ 发放上限"的写法)。 +- 因为 500x 封顶,桶 16 的池内实测均值会明显低于名义 500(被截断的局都堆在这里),配比计算按池内 `AVG(actual_mul)` 走,必要时用 `PAYOUT_EXPECT_OVERRIDE`(玫瑰女郎顶桶就是名义 1000、实用 835)。 + +**这张表**只管两件事:① 一局落在哪个倍率档(配比与库存的单位);② RTP 配平。 +**实际发哪一件饰品**是另一回事——由 `prize_range` **按金额(¥)匹配**,见 §3.8。 + +**配置量与规模** +- `16 桶 × 11 箱 × 4 身份 = **704 条**` 配比行(玫瑰女郎是 `17 × 30 × 4 = 2040` 条)。 +- 注册轴范围 / 生成器/后台页面清单里对应的桶数见 §4、§5、§8。 + +**可选优化(先不做,看实测再决定)** + +麻将的赢额**集中在 1x 以下**(最小非零倍率只有 **0.1x** = 最低档符号 3 连赔 0.1 倍),而玫瑰女郎桶 2 一口气吃掉 `0.01~1` 整个区间。 +如果上线后实测发现"小奖过于集中、桶 2 占了大头、调不动形状",再在低端插两档即可(变成 18 档,**不影响任何已有逻辑**,只多 88 条配比行): + +| 插入 | 名称 | 期望倍率 | 落桶下限 | 落桶上限 | +|---|---|---|---|---| +| 2a | 0.2倍饰品 | 0.2 | 0.01 | 0.4 | +| 2b | 0.7倍饰品 | 0.7 | 0.4 | 1 | + +> 另外记一个数学性质:可达到的倍率是**离散**的,全是 **0.05 的整数倍**(0.10 / 0.20 / 0.25 / 0.30 / 0.40 / 0.50 / 0.75 / 1.00…)——因为 `赔付值 × ways × ladder ÷ 20` 里赔付值都是整数。所以任何桶边界都会自然对齐,不需要额外处理。 ### 3.2 RTP 恒等式 - ``` Σ (share_i × expect_mul_i) = rtp × costMultiplier # 本游戏 costMultiplier ≡ 1 ``` @@ -500,6 +879,54 @@ payoutCap = max(0, 水位 + 本局成本) × box.rtp # 白号 = nu - 玫瑰女郎有现成的同类配置可参照(`olympus-py/olympus/config.py:181` 的 `BASE_NEARMISS_SCATTER`); - 少了这条,客户端那段"最后一列放大"的动画**永远不会被触发**(纯自然概率太低),爽点白做。 +### 3.8 「饰品倍率」其实是**两层**,别混(用户 2026-09-21 问) + +麻将胡了这里的"饰品/奖励"分两层,**它们各自有不同的"倍率"含义**: + +| 层 | 表 | 倍率含义 | 谁定 | +|---|---|---|---| +| **① 饰品桶表**(池子层) | `game_mahjong_bucket`(16 档,见 §3.1) | **倍率区间** —— 决定回合池组成与 RTP 配平 | **沿用玫瑰女郎的 17 档结构,去掉 1000x 档 → 16 档**(表见 §3.1) | +| **② 饰品区间表**(发放层) | `game_mahjong_prize_ornament` + `game_mahjong_prize_range` | **没有固定倍率**:区间是按**金额(¥)**划的 | **直接复用玫瑰女郎/糖果那套**(按金额匹配,与游戏无关) | + +> ⚠️ **关键点:饰品的"倍率"不是一个固定数,它随投注档变化**——因为②是**按金额**匹配的: +> +> | 玩家 | 投注档 | 赢额 | = 倍率 | 实际拿到的饰品 | +> |---|---|---|---|---| +> | 甲 | ¥1 | ¥50 | **50x** | 饰品 A(价格落在 ¥50 区间) | +> | 乙 | ¥500 | ¥50 | **0.1x** | **同一个**饰品 A | +> +> 所以"每个饰品对应多少倍"这句话,只有"该饰品价格 ÷ 当前投注档"才有意义。 +> 想要"饰品 ↔ 固定倍率"的对应关系,那是**①倍率桶**在做的事(池子按倍率分档),不是②。 + +**② 饰品区间表怎么来(不要重新设计)** + +照玫瑰女郎的做法(`docs/updata_sql.md:1187-1200`)**整套导入**: + +```sql +DELETE FROM game_mahjong_prize_range; +DELETE FROM game_mahjong_prize_ornament; +INSERT INTO game_mahjong_prize_ornament (id, game_type, name, image_url, grade, exterior_name, sort, status, is_fallback, create_time) +SELECT id, 'MAHJONG', name, image_url, grade, exterior_name, sort, status, is_fallback, NOW() FROM game_sweet_prize_ornament; +INSERT INTO game_mahjong_prize_range (prize_id, win_low, win_high) +SELECT ornament_id, win_low, win_high FROM game_sweet_prize_range; +``` + +**导入后必须核的 3 条(都直接决定 RTP,不能跳过)** + +| # | 检查项 | 为什么 | 判据 | +|---|---|---|---| +| 1 | **最低档饰品的价格** | "必得饰品"意味着**空转局也发最低档饰品**,它的价格就是每局的**保底返奖**:`保底 RTP ≈ 空转率 × 最低饰品价 ÷ 投注额`。空转率按 **65%** 估,¥1 档下若最低饰品价 **¥0.50** → **保底 RTP 32.5%**,直接把目标 80% 吃掉一半 | **最低饰品价 ≤ ¥0.05**(→ 保底 ≈3%) | +| 2 | **区间上界要覆盖到 500x × 最高档 = ¥250,000** | 覆盖不到的话,高倍局全都走 `findPrizeWithFallback` 落到兜底饰品,出现"赢了 20 万给个 50 块的皮肤" | 最高区间下限 ≥ ¥250,000(玫瑰女郎是 1000x × 300 = ¥300,000,量级相同,大概率够) | +| 3 | **区间连续无缝、无空洞** | 有空洞就会走 fallback(落到 `is_fallback=1` 或最低/最高区间),发奖与赢额不匹配 | 按 `win_low` 排序后逐条核 `high_i + 0.01 == low_{i+1}` | + +> 本地 MySQL 没启动,我没法直连查现成的区间数据;**上线前请在测试库跑一次**: +> ```sql +> SELECT MIN(win_low) AS 最低区间下限, MAX(win_high) AS 最高区间上限, +> COUNT(*) AS 条数 FROM game_olympus_prize_range; -- 玫瑰女郎现成的 +> SELECT o.name, o.grade, r.win_low, r.win_high FROM game_olympus_prize_range r +> JOIN game_olympus_prize_ornament o ON o.id = r.prize_id ORDER BY r.win_low LIMIT 5; +> ``` + --- ## 4. 回合池数据模型(11 张表) @@ -509,8 +936,8 @@ payoutCap = max(0, 水位 + 本局成本) × box.rtp # 白号 = nu | # | 表 | 对应 olympus 表 | 关键列 | 差异 | |---|---|---|---|---| | 1 | `game_mahjong_box` | `game_olympus_box` | box_id, box_type, bet, price, total_rounds, rtp, rtp_converge_ratio, status | `total_ornaments`→`total_rounds` | -| 2 | `game_mahjong_bucket` | `game_olympus_ornament` | id(1-10), expect_mul, name, range_low, range_high | 10 档倍率桶 | -| 3 | `game_mahjong_box_bucket` | `game_olympus_box_ornament` | box_id, bucket_id, audience, count, used | 同构,唯一键 `(box_id,bucket_id,audience)` | +| 2 | `game_mahjong_bucket` | `game_olympus_ornament` | id(1-16), expect_mul, name, range_low, range_high | **16 档饰品桶** | +| 3 | `game_mahjong_box_bucket` | `game_olympus_box_ornament` | box_id, bucket_id, audience, count, used | 同构,唯一键 `(box_id,bucket_id,audience)`;**16 桶 × 11 箱 × 4 身份 = 704 条** | | 4 | `game_mahjong_round` | `game_olympus_round` | round_id, box_id, bucket_id, actual_mul, free_spin_count, total_rotations, has_fs, near_miss | 去 `total_cascades` 语义改为 `total_rotations`(波数);`need_fs` 不需要了(没有买 FS 模式);加 `near_miss` 标记 2 胡悬念盘 | | 5 | `game_mahjong_step` | `game_olympus_step` | 见 §4.1 | 字段大改(ways/golden/阶梯) | | 6 | `game_mahjong_record` | `game_olympus_record` | user_id, box_id, round_id, cost, bet_mul, total_win, settled, settle_source, audience, **prize_id / prize_name / prize_image_url / prize_level_img / settled_win / reward_type** | **保留全部 prize_* 列**(发饰品要走区间匹配),只多一个 `bet_mul` | @@ -540,7 +967,7 @@ payoutCap = max(0, 水位 + 本局成本) × box.rtp # 白号 = nu "fill_data": [{"pos":2,"symbol":"G"}], "scatter_trigger": {"scatterCount":3,"freeSpins":12,"extraSpins":0}, // 或 null "retrigger_info": null, - "rotation_mul": 2, // 本波乘数(阶梯档) + "rotation_mul": 2, // 本波阶梯倍率(本波适用的档位) "win_amount": 120.00, // 本旋累计到本步(末帧 = 本旋总额),不是增量 "spin_win": 340.00, // 本局累计到本步 "win_capped": 0, @@ -559,7 +986,7 @@ payoutCap = max(0, 水位 + 本局成本) × box.rtp # 白号 = nu ``` mahjong-py/ -├── schema.sql # 11 张表 DDL + 10 档桶种子(改前缀与种子) +├── schema.sql # 11 张表 DDL + 16 档桶种子(改前缀与种子) ├── mahjong_db.py # 建表 + 批量写(executemany,死锁重试 5 次) ├── mahjong/ │ ├── config.py # ★ 唯一事实来源:符号/赔付表/桶/模式/身份/RTP/FS 参数 @@ -598,7 +1025,7 @@ mahjong-py/ 2. **几何**:`GRID_ROWS=4 / GRID_COLS=5 / GRID_SIZE=20`;`_force_win` 的 `random.sample(free, cnt)`、`_force_small_win` 的可用格数、`_place_scatter` 的保护集都要跟着改。 3. **乘数阶梯替换炸弹**: - 删掉 `BOMB_CELL_PROBABILITY` / `BOMB_VALUES` / `BOMB_QUALITIES` / `FS_SCREEN_BOMB_PROB` 一整组; - - 新增 `LADDER_BASE = (1,2,3,5)` / `LADDER_FS = (2,4,6,10)`,第 n 波乘数 = `ladder[min(n,4)]`; + - 新增 `LADDER_BASE = (1,2,3,5)` / `LADDER_FS = (2,4,6,10)`,**第 n 波的阶梯倍率** = `ladder[min(n,4)]`; - `_force_win` 的目标反解改为"基础组合 × 本波阶梯乘数",即给定目标倍率 T 时,需要的基础分 = `T / ladder[n]`; - **累计器机制不删**:`accumulator_before/after` 改成 `rotation_mul`,语义从"跨旋累加"变为"本波档位"。 @@ -627,7 +1054,7 @@ mahjong-py/ ```bash cd mahjong-py -python mahjong_db.py # 建 11 张表 + 10 档桶种子(幂等) +python mahjong_db.py # 建 11 张表 + 16 档桶种子(幂等) python generate_rounds.py --per-bucket 20 # 干跑,不写库 python generate_rounds.py --all --alloc config --write --workers 4 # 12 箱写库(只有 normal) python _make_templates.py # 12 箱 × 4 身份 = 48 条生效模板 @@ -689,10 +1116,10 @@ com.ruoyi.game.mahjong ### 6.3 开局事务(与玫瑰女郎的唯一实质差异) ``` -startNewGame(userId, boxId, betMul = 1..10): +startNewGame(userId, boxId): 校验箱子/维护/充值(703)/余额(702) audience = resolve(player); isBlank = audience.isWhite() - bet = box.bet × betMul // 没有模式倍率(只有 normal) + bet = box.bet // 无模式倍率、无投注倍数 cost = bet // 实际扣费 = 下注总额 payoutCap = isBlank ? null : scale2(max(0, 水位(box,aud) + cost) × box.rtp) picked = roundPicker.pick(boxId, audience, bet, payoutCap) @@ -755,9 +1182,46 @@ startNewGame(userId, boxId, betMul = 1..10): ### 6.6 封顶与"匹配/发放分离" -- 回合池里桶 10 的 `actual_mul` 一律 **= 1000**(发放值),但配比计算时期望用 **835**(实测池内均值,可配 `PAYOUT_EXPECT_OVERRIDE`),否则 RTP 会系统性偏低。 +- 回合池里桶 10 的 `actual_mul` 一律 **= 500**(发放值),但配比计算时期望用池内实测均值(可配 `PAYOUT_EXPECT_OVERRIDE`),否则 RTP 会系统性偏低。 - 运行期 `payoutCap` 过滤 + 收敛阶段 `capMul = payoutCap / bet` 双保险。 -- FINISH 时 `totalMul ≥ 1000` → `maxWinReached=true`、`spinsRemaining=0`。 +- FINISH 时 `totalMul ≥ 500` → `maxWinReached=true`、`spinsRemaining=0`。 + +### 6.7 赢奖金额口径**与玫瑰女郎完全一致**(用户 2026-09-21 要求) + +麻将胡了**沿用玫瑰女郎那一整套"倍率 × 基础下注"的金额框架**,只在"倍率怎么算出来"这一步不同(玫瑰女郎靠 5×6 消除聚簇,麻将靠 1024 路前缀链 + 级联 + 阶梯)。 + +| 环节 | 玫瑰女郎(Olympus) | 麻将胡了 | 是否一致 | +|---|---|---|---| +| 扣费 | `cost = box.bet × modeCM`(CM = 1/2/50) | `cost = box.bet`(CM ≡ 1、无倍数) | 同结构 | +| 回合池里存什么 | **倍率** `round.actual_mul`(≤1000) | **倍率** `round.actual_mul`(≤500) | ✅ 一致(存倍率,不存金额) | +| 单笔/单波金额 | `base_score × bet`(base_score 由生成端算好) | `倍率 × ways × 投注额 × rotationMul` | 同结构(都是"乘数 × bet") | +| **本局赢额** | `totalWin = actual_mul × bet` | `totalWin = actual_mul × bet` | ✅ **完全一致** | +| 本局倍率 | `totalMul = actual_mul` | `totalMul = actual_mul` | ✅ 一致 | +| 发奖 | 赢额 → 落 `prize_range` 区间 → 饰品 → 写背包 → `settled_win` = 饰品价 | 同 | ✅ 一致 | +| 分解 | `OLYMPUS_BOX(13) → OLYMPUS_REWARD(61)` | `MAHJONG_BOX(15) → MAHJONG_REWARD(69)` | 同结构 | +| 空转局 | `win = max(totalWin, 0.01)` → 最低档饰品(必得饰品) | 同 | ✅ 一致 | +| 奖池水位记账 | `水位 = Σcost − Σsettled_win + adjust` | 同 | ✅ 一致 | +| 单局封顶 | **1000x** | **500x**(Q4 定案) | ⚠️ **唯一有意差异** | + +**关键恒等式(两边通用)**: + +``` +round.actual_mul = 本局总赢额 ÷ bet +本局赢额 = round.actual_mul × bet ← 与玫瑰女郎同一个公式 +``` + +麻将胡了这边,`actual_mul` 是由生成端按 §2.1 的公式**反推出来的**: + +``` +actual_mul = Σ(每一波、每一个符号: 赔付表[X][连轴数] × ways × rotationMul) + └──────── 赔付表已是倍率,所以加起来就是本局倍率 ────────┘ +``` + +所以: +- **玩家看到的金额、`totalWin/totalMul`、`settled_win`、饰品区间匹配、水位记账——全部与玫瑰女郎同口径**; +- 麻将独有的只有"倍率怎么生成"(前缀链/级联/金色转百搭/阶梯/FS),以及**封顶 1000x→500x** 这一个有意差异。 + +> 若日后要与玫瑰女郎**完全对齐封顶**,只需把 `MAX_WIN_CAP_MUL` 与顶部桶区间一起改回 1000,不影响其他任何环节。 --- @@ -768,12 +1232,12 @@ Base:`{host}/api/mahjong`,全部 POST + JSON + token 鉴权。 | # | 接口 | 用途 | |---|---|---| | 1 | `POST /boxes` | 箱子列表(含 `bet`、`price`;**没有 `modeName`**,只有一个普通旋转模式) | -| 2 | `POST /dospin` | 开局(`{boxId, betMul}`)/ 续播(空 body `{}`) | +| 2 | `POST /dospin` | 开局(`{boxId}`)/ 续播(空 body `{}`) | | 3 | `POST /records` | `scope=1` 我的 / `scope=0` 全服,分页,只含已结束局 | | 4 | `POST /hasActive` | 断线重连恢复 | | 5 | `POST /api/userPackSack/decompose` | **复用玫瑰女郎那个通用背包接口**,末帧「立即分解」(含前端自动分解) | -> `dospin` 开局请求:`{"boxId": 1001, "betMul": 1}`;`betMul` 缺省按 1 处理,范围 1~10,服务端夹取。 +> `dospin` 开局请求:`{"boxId": 1001}`。协议里保留可选字段 `betMul`(缺省 1、当前服务端只接受 1),给将来留口子,不影响当前逻辑。 ### 7.1 帧字段(`SpinResponse`) @@ -788,7 +1252,7 @@ Base:`{host}/api/mahjong`,全部 POST + JSON + token 鉴权。 | `wildConverted` | 每帧 | `[{"pos":1起,"fromSymbol":"B"}]` 本波**就地转成百搭**的格:该格 `gridState` 已是 `W`、不清空不补新符,客户端播"金色→百搭"变形动画 | | `waysDetail` | 每帧 | `[{"symbol","reelCount","counts":[..5],"loc":[格位…],"ways","payBase","rotationMul","win"}]`:`loc` = **本笔中奖的全部格位(1 起,含金色格)**,客户端按它播爆裂动画;`win` = `payBase × ways × (bet÷K) × rotationMul`(**K 见 §2.1 计量单位**) | | `dropData` / `fillData` | 每帧 | 与玫瑰女郎同构(`from/to/symbol`、`pos/symbol`) | -| `rotationMul` | 每帧 | 本波乘数(阶梯档位),重连时用它接回档位 | +| `rotationMul` | 每帧 | **本波阶梯倍率**(基础旋 `1/2/3/5`、FS `2/4/6/10`);重连时用它接回档位 | | `winAmount` | 每帧 | **本旋累计到本帧**(末帧 = 本旋总额),不是增量 | | `spinWin` | 每帧 | 本局累计到本帧;末帧 = 整局实付 | | `scatterTrigger` | 有则 | `{"scatterCount":3,"freeSpins":12,"extraSpins":0}` | @@ -804,7 +1268,7 @@ Base:`{host}/api/mahjong`,全部 POST + JSON + token 鉴权。 **位置约定**:`pos = 行序号(0 起) × 5 + 列序号(0 起) + 1`,1~20。 **短码**:`A~H` 付费符号 / `W` 百搭 / `S` 胡。 -> 前端只需保证:**首次用 `{boxId, betMul}` 开局,之后不断调 `dospin({})` 逐步揭晓,直到 `state === "FINISH"`**,然后在末帧用 `boxRecordId` 让玩家选「放入背包 / 立即分解」。 +> 前端只需保证:**首次用 `{boxId}` 开局,之后不断调 `dospin({})` 逐步揭晓,直到 `state === "FINISH"`**,然后在末帧用 `boxRecordId` 让玩家选「放入背包 / 立即分解」。 ### 7.2 错误码(照抄) @@ -854,7 +1318,7 @@ Base:`{host}/api/mahjong`,全部 POST + JSON + token 鉴权。 | 标题条下方 | **乘数阶梯行** `x1 x2 x3 x5`(FS 内 `x2 x4 x6 x10`):**当前档金色高亮+放大,未到的档压暗** | `rotationMul` | | 中部 | **5 列 × 4 行**牌面;**金色符号 = 普通牌 + 金色底/描边**;**本波中奖牌 = 红色边框** | `gridState` / `goldenDetail` / `waysDetail` | | 牌面下方 | 「共赢得 X.XX」金边横幅(本局累计,跳字) | `spinWin` | -| 横幅下方三格 | 余额 ¥ / 投注 ¥ / 赢额 ¥ | 余额接口 / `bet×betMul` / 本局赢额 | +| 横幅下方三格 | 余额 ¥ / 投注 ¥ / 赢额 ¥ | 余额接口 / 本局投注额 / 本局赢额 | | 底部按钮 | 闪电(加速)/ − / 大 Spin / + / 自动旋转,右上角 ✕ 退出 | 静态 + 投注档联动 | > 截图实测细节:投注 ¥8.00、阶梯高亮在 **x2**(本旋第 2 波)、横幅「共赢得 0.80」→ 说明"**每波结算后 HUD 立刻按当前波次刷新阶梯、并按本局累计刷新横幅**",不是等整旋播完才更新。这两处要按帧驱动,别做成一旋只刷一次。 @@ -934,14 +1398,14 @@ Base:`{host}/api/mahjong`,全部 POST + JSON + token 鉴权。 | # | 问题 | 结论 | |---|---|---| -| ~~Q0~~ | ~~赔付值乘的基数是哪个?~~ | ✅ **投注总额 ÷ 20**(赔付表是 1 倍基准价 + 规则页"20 条基础投注" + 截图排除法,见 §2.1) | +| ~~Q0~~ | ~~赔付值乘的基数是哪个?~~ | ✅ **赔付表直接用倍率表达**(原版点数 ÷ 20 已内化进表里),公式里不再有 ÷20;换算关系与排除法见 §2.1 | | ~~Q1~~ | ~~纯百搭链(第1/2/3轴全百搭)算什么?~~ | ✅ **由"金色只在第 2/3/4 轴 + 百搭只能由金色转化"自动消解**——百搭永远不在第 1 轴,链的第 1 轴必然是自然符号 ⇒ 每笔中奖天然含自然符号,**纯百搭链数学上不可能出现**,不需要单独规则、也不需要 `WILD_PAYS` 开关(见 §2.2) | | ~~Q2~~ | ~~金色→百搭的那个百搭是否"钉住不动"?~~ | ✅ **不钉住**("符号是可以下落的,下面符号消除之后,上面符号会往下掉落") | -| ~~Q3~~ | ~~免费旋转总旋数上限~~ | ✅ **30 旋** | +| ~~Q3~~ | ~~免费旋转总旋数上限~~ | ✅ **36 旋**(用户 2026-09-21 定案),并配"**发不满就不发**"硬规则:`已发放 + 本次应赠 > 36` 时后续旋不再落 3+ 胡,杜绝"落胡只补几旋" | | ~~Q4~~ | ~~单局封顶倍率~~ | ✅ **500x** → `MAX_WIN_CAP_MUL = 500`;配 ¥1000 档 ×10 倍,单局最大赔付 ¥5,000,000 | | ~~Q5~~ | ~~是否要做 `ante` / `buy_fs`~~ | ✅ **都不做,只保留普通旋转** → 12 箱、无 `need_fs`、`costMultiplier≡1` | | ~~Q6~~ | ~~各身份目标 RTP~~ | ✅ **不是拍板项**:后台配置(箱子级 `rtp` + 身份级桶配比),建库统一 0.80,上线后按身份调,见 §3.6 | -| Q7 | 投注档数量与档位表 | 🔧 **技术默认:12 档** `1/3/5/10/20/30/50/100/200/300/500/1000` × betMul 1~10(实际 1~10000)。零成本备选:10 档 + betMul 1~20,见 §2.8.1 | +| Q7 | 投注档数量与档位表 | 🔧 **技术默认:11 档、不设投注倍数** —— `1/3/5/10/20/30/50/100/200/300/500`,单局理论最大赔付 ¥250,000(压在玫瑰女郎 ¥300,000 之下)。理由与升级路径见 §2.8.1 | | Q8 | 每日统计要不要独立日表 | 🔧 **技术默认:不要**,后台按 `record` 聚合 | | ~~Q9~~ | ~~奖励形态:现金直付 vs 饰品入背包~~ | ✅ **赢额变成一个饰品发到背包** → 区间匹配 → `tt_box_records`(source=15) → 末帧放入/分解 → 分解时写 `MAHJONG_REWARD(69)` | | Q10 | 后台"回合管理"要不要暴露金色/阶梯明细 | 🔧 **技术默认**:只读展示 `actual_mul / 旋数 / 波数 / 是否含 FS / 悬念盘`,不展示内部帧 | @@ -963,7 +1427,7 @@ Base:`{host}/api/mahjong`,全部 POST + JSON + token 鉴权。 | 项 | 规则 | |---|---| -| 投注构成 | 「**投注大小**」(12 档)× 「**投注倍数**」(1~10)= **投注总额**,开局即扣 | +| 投注构成 | 选一个**投注档**(11 档,见下),即为**投注总额**,开局即扣(**不设投注倍数**) | | 投注大小档位 | `1 / 3 / 5 / 10 / 20 / 30 / 50 / 100 / 200 / 300 / 500 / 1000` | | 游戏单位 | 金额以 ¥ 为单位 | | 现金钱包 | 显示当下玩家可用的现金余额 | @@ -971,36 +1435,36 @@ Base:`{host}/api/mahjong`,全部 POST + JSON + token 鉴权。 | 中奖派发 | 按「**赔付表**」执行;不同中奖路上的奖金**叠加** | | 结算形态 | 本局赢额**按区间转换为一个饰品发到背包**;末帧可选择「放入背包 / 立即分解」;开启「自动分解」则末帧自动分解为金币 | -> **开发参数**:`box.bet ∈ 上表 12 档`;`price = box.bet × betMul`(`costMultiplier ≡ 1`);饰品按 `game_mahjong_prize_range` 的赢额区间匹配;开局即 `settleAtStart` 写背包(无损模型)。 +> **开发参数**:`box.bet ∈ 上表 11 档`;`price = box.bet`(`costMultiplier ≡ 1`、无倍数);饰品按 `game_mahjong_prize_range` 的赢额区间匹配;开局即 `settleAtStart` 写背包(无损模型)。 ### 14.3 中奖判定(1024 路 = 前缀链) -- 赔付表上的数值是「**每条基础投注**」的单价:**1 条基础投注 = 投注总额 ÷ 20**(游戏固定 20 条基础投注)。 +- 赔付表上的数值**就是倍投注总额**(原版"每条基础投注点数"已预先除以 20 内化,见 §2.1)。 - 一条中奖路必须**从最左(第 1 轴)开始,按 1 → 2 → 3 → 4 → 5 的顺序连续出现**该符号; - 第 1 轴是**必需**的; - **中间断了就整笔作废**——例如 1、2、4 有、3 没有,不算中奖;2、3、4 有、1 没有,也不算。 - 每一轴上该符号可以有**好几个**(最多 4 个),只要求「每轴至少 1 个」。 - **路数** = 链内各轴该符号个数的**乘积**。 -- **本笔赢额 = 赔付值 × 路数 × (投注总额 ÷ 20) × 本波乘数**。 +- **本笔赢额 = 符号倍率 × 路数 × 投注总额 × 本波阶梯倍率**。 - 中奖路奖金**从最左至右卷轴的顺序派发**;不同符号各自独立成奖、**互相叠加**。 -- 参考例子:`1 × 3 × 2 = 6` 路;赔付值 `10 × 6 = 60`(基础注),说明见游戏内「赔付表」。 +- 参考例子:`1 × 3 × 2 = 6` 路,**3 连赔 0.5 倍** → 本笔 `0.5 × 6 = 3.00 倍`。**逐步算例(含 ¥ 换算)见 §2.1.1**。 -> **开发参数**:`PAY_UNIT_DIVISOR = 20`;判定实现见 §5.2(`break` 就停,禁止写成"任意 3 轴凑齐")。 +> **开发参数**:赔付表**直接以倍率存**(`PAYTABLE` 里就是 0.75/0.5/0.4…),**没有 `PAY_UNIT_DIVISOR` 这一步**;判定实现见 §5.2(`break` 就停,禁止写成"任意 3 轴凑齐")。 ### 14.4 符号与赔付表 -赔付值(每条基础投注,**1 倍基准价**): +赔付倍率(**倍投注总额**): | 符号 | 5 连 | 4 连 | 3 连 | |---|---|---|---| -| 發 | 100 | 60 | 15 | -| 中 | 80 | 40 | 10 | -| 白板(紫框) | 60 | 20 | 8 | -| 八萬 | 40 | 15 | 6 | -| 花筒(四色) | 20 | 10 | 4 | -| 索(红绿) | 20 | 10 | 4 | -| 筒(紫绿) | 10 | 5 | 2 | -| 索(绿) | 10 | 5 | 2 | +| 發 | **5** | **3** | **0.75** | +| 中 | **4** | **2** | **0.5** | +| 白板(紫框) | **3** | **1** | **0.4** | +| 八萬 | **2** | **0.75** | **0.3** | +| 花筒(四色) | **1** | **0.5** | **0.2** | +| 索(红绿) | **1** | **0.5** | **0.2** | +| 筒(紫绿) | **0.5** | **0.25** | **0.1** | +| 索(绿) | **0.5** | **0.25** | **0.1** | | **百搭(元宝)** | — | — | — | | **胡(夺宝符号)** | — | — | — | @@ -1063,11 +1527,11 @@ Base:`{host}/api/mahjong`,全部 POST + JSON + token 鉴权。 - **当 3 个夺宝符号(胡)出现在卷轴的任意位置时,免费旋转模式将被触发,并获得 12 次免费旋转**。 - **每个额外的夺宝符号都将触发多 2 次的免费旋转**:3 个 → 12 次,4 个 → 14 次,5 个 → 16 次,6 个 → 18 次。 - **免费旋转模式期间,卷轴上方的乘数将分别增加至 x2、x4、x6 和 x10**。 -- **免费旋转可以被重复触发**(免费旋转内再出现 3+ 个胡,同样按"12 + 每个额外胡 +2"追加)。 -- **免费旋转的总旋数上限为 30 旋**(到顶后不再出现 3+ 个胡)。 -- 免费旋转期间投注大小与投注倍数、以及触发免费旋转模式时的参数**保持一致**。 +- **免费旋转可以被重复触发**(FS 内再落 3 / 4 / 5 个胡 → **再送 12 / 14 / 16 旋**,与首触发同一套公式,见 §2.6)。 +- **免费旋转的总旋数上限为 36 旋**:`已发放 + 本次应赠 > 36` 时,后续旋**不再出现 3+ 个胡**(避免"落了胡却只补几旋")。因为单次最少赠 12 旋,所以**已发放 ≥ 25 旋之后一律不再出胡**,整局永远 ≤ 36 旋。 +- 免费旋转期间**投注档与触发免费旋转时的参数保持一致**(中途不换注)。 -> **开发参数**:`FS_AWARD = 12 + 2 × (scatterCount − 3)`;重触发同式;**总旋数上限 30**(Q3 定案);到顶后不再落 3+ 胡。 +> **开发参数**:`FS_AWARD = 12 + 2 × (scatterCount − 3)`;重触发同式;**总旋数上限 36**(Q3 定案);到顶后不再落 3+ 胡。 ### 14.10 悬念表现(2 个胡) @@ -1089,16 +1553,16 @@ Base:`{host}/api/mahjong`,全部 POST + JSON + token 鉴权。 |---|---| | 盘面 | 5 轴 × 4 行 = 20 格 | | 路数 | 4⁵ = 1024 | -| 赔付单位 | 投注总额 ÷ 20(`PAY_UNIT_DIVISOR = 20`) | +| 赔付表单位 | **倍投注总额**(原版"每条基础投注点数"已预先 ÷20 内化) | | 判定 | 前缀链(第 1 轴起连续,断链作废,`ways` 只乘链内轴) | | 百搭 | **只能由金色符号转化产生**(原生盘面不落);**每个金色格各留一个(无上限)**,**只可能在第 2/3/4 轴**;可替代除「胡」外全部;满足消除链才被消除 | | 金色符号 | 只在第 2/3/4 轴;按自然符号成奖;消除后就地留百搭 | | 普通旋阶梯 | `1 / 2 / 3 / 5`(第 4 波及以后 x5) | | FS 阶梯 | `2 / 4 / 6 / 10` | -| 免费旋转 | 3+ 胡 → 12 旋,每个额外胡 +2,可重触发,**上限 30 旋** | +| 免费旋转 | 3+ 胡 → 12 旋,每个额外胡 +2(**一屏最多 5 个胡 → 单次最多 16 旋**);FS 内重触发同式;**上限 36 旋**,且"发不满就不发" | | 悬念盘 | 2 个胡 → 第 5 轴放大;生成端占比 1%~2% | -| 投注档 | 12 档:1/3/5/10/20/30/50/100/200/300/500/1000 | -| 投注倍数 | 1~10 | +| 投注档 | **11 档:1/3/5/10/20/30/50/100/200/300/500** | +| 投注倍数 | **无**(只选投注档;协议预留 `betMul` 恒为 1) | | 封顶 | **500x** | | 发奖 | 赢额 → 区间匹配饰品 → 背包 → 可分解 | @@ -1106,7 +1570,7 @@ Base:`{host}/api/mahjong`,全部 POST + JSON + token 鉴权。 ## 15. 完整消除示例(逐波走一遍,用来校验规则理解) -> 场景:**普通旋转**,投注大小 ¥100 × 投注倍数 1 → **投注总额 ¥100**,故 **1 条基础投注 = 100 ÷ 20 = ¥5.00**。 +> 场景:**普通旋转**,投注档 **¥100**(即投注总额,无倍数)。 > 本旋共 **4 波消除**,覆盖:两个符号同时中奖(叠加)、**两个金色格各留一个百搭**、百搭随重力下落、百搭参与成链后自己也被消除、3/4/5 连三种档位、x1→x2→x3→x5 全阶梯。 > 图例:`A`=發 `B`=中 `C`=白板 `D`=八萬 `E`=花筒 `F`=索(红绿) `G`=筒(紫绿) `H`=索(绿) `W`=百搭(元宝) `S`=胡 **加粗带 `*` = 金色**。 > ⚠️ 金色格只能出现在**第 2/3/4 轴**(本示例的两张金色都在轴2、轴3)。 @@ -1132,10 +1596,10 @@ Base:`{host}/api/mahjong`,全部 POST + JSON + token 鉴权。 **赔付**: -| 中奖 | 赔付值 | ways | 基础注 | 阶梯 | 赢额 | +| 中奖 | 符号倍率 | ways | 本笔基础倍率 | 本波阶梯倍率 | 赢额 | |---|---|---|---|---|---| -| 中 3 连 | 10 | 4 | 40 | x1 | **¥200** | -| 白板 3 连 | 8 | 2 | 16 | x1 | **¥80** | +| 中 3 连 | 0.5 | 4 | **2.00 倍** | x1 | **¥200** | +| 白板 3 连 | 0.4 | 2 | **0.80 倍** | x1 | **¥80** | | | | | | 小计 | **¥280** | **本波共 9 个符号被消除**(9 格全播爆裂动画): @@ -1201,7 +1665,7 @@ Base:`{host}/api/mahjong`,全部 POST + JSON + token 鉴权。 > 本波盘面**只有轴 1 是纯八萬**(补位恰好都补成 D),所以只有八萬成链。这也顺带说明了:**有百搭的那几轴对所有符号都成立**,链能不能成完全取决于"没有百搭的那些轴"。 -**赔付**:八萬 **4 连**赔付值 **15** × ways **4** = 60 基础注 = **¥300** × 阶梯 **x2** = **¥600** +**赔付**:八萬 **4 连** 赔 **0.75 倍** × ways **4** = **3.00 倍** → `3.00 × ¥100 × x2` = **¥600** **本波共 7 个符号被消除**:轴1 行1~行4 的 4 张 D、**(行3,轴2) W**、**(行3,轴3) W**、**(行2,轴4) D**。 - **两个百搭都参与了成立的中奖链** → 按"满足消除链才消除" → **两个 `W` 一起被消除**(盘面回到 0 个百搭) @@ -1256,7 +1720,7 @@ Base:`{host}/api/mahjong`,全部 POST + JSON + token 鉴权。 | 索红绿 F | 1(行4) | 1(行2) | 0 | — | — | 轴3 断 | | 其余 | 轴1 没有 | — | — | — | — | 不成奖 | -**赔付**:花筒 **5 连**赔付值 **20** × ways **1** = 20 基础注 = **¥100** × 阶梯 **x3** = **¥300** +**赔付**:花筒 **5 连** 赔 **1 倍** × ways **1** = **1.00 倍** → `1.00 × ¥100 × x3` = **¥300** **本波共 5 个符号被消除**:(行1,轴1)E、(行4,轴2)E、**(行1,轴3) 金**、(行2,轴4)E、(行3,轴5)E——5 格全爆。 其中 4 格变空洞;金色格 (行1,轴3) **原地留百搭 `W`**(本波开始时盘面 0 个百搭 ✅,本波只有 1 张金色格中奖 → **留 1 个**)。 @@ -1314,10 +1778,10 @@ Base:`{host}/api/mahjong`,全部 POST + JSON + token 鉴权。 **赔付**: -| 中奖 | 赔付值 | ways | 基础注 | 阶梯 | 赢额 | +| 中奖 | 符号倍率 | ways | 本笔基础倍率 | 本波阶梯倍率 | 赢额 | |---|---|---|---|---|---| -| 筒 5 连 | 10 | 2 | 20 | x5 | **¥500** | -| 索红绿 5 连 | 20 | 1 | 20 | x5 | **¥500** | +| 筒 5 连 | 0.5 | 2 | **1.00 倍** | x5 | **¥500** | +| 索红绿 5 连 | 1 | 1 | **1.00 倍** | x5 | **¥500** | | | | | | 小计 | **¥1000** | **本波共 9 个符号被消除**(筒链 5 格 ∪ 索红绿链 5 格,重叠在 `W` 与部分格): @@ -1348,16 +1812,19 @@ Base:`{host}/api/mahjong`,全部 POST + JSON + token 鉴权。 ### 本旋总账 -| 波 | 中奖 | 赔付值 | ways | 基础注 | 阶梯 | 本波赢额 | +| 波 | 中奖 | 符号倍率 | ways | 本笔基础倍率 | 本波阶梯倍率 | 本波赢额 | |---|---|---|---|---|---|---| -| 1 | 中 3 连 | 10 | 4 | 40 | x1 | ¥200 | -| 1 | 白板 3 连 | 8 | 2 | 16 | x1 | ¥80 | -| 2 | 八萬 4 连 | 15 | 4 | 60 | x2 | ¥600 | -| 3 | 花筒 5 连 | 20 | 1 | 20 | x3 | ¥300 | -| 4 | 筒 5 连 | 10 | 2 | 20 | x5 | ¥500 | -| 4 | 索红绿 5 连 | 20 | 1 | 20 | x5 | ¥500 | +| 1 | 中 3 连 | 0.5 | 4 | **2.00 倍** | x1 | ¥200 | +| 1 | 白板 3 连 | 0.4 | 2 | **0.80 倍** | x1 | ¥80 | +| 2 | 八萬 4 连 | 0.75 | 4 | **3.00 倍** | x2 | ¥600 | +| 3 | 花筒 5 连 | 1 | 1 | **1.00 倍** | x3 | ¥300 | +| 4 | 筒 5 连 | 0.5 | 2 | **1.00 倍** | x5 | ¥500 | +| 4 | 索红绿 5 连 | 1 | 1 | **1.00 倍** | x5 | ¥500 | | | | | | | 合计 | **¥2180** | +> 计算方法:**本笔基础倍率 = 符号倍率 × ways**;**本波赢额 = Σ本笔基础倍率 × 投注总额 × 本波阶梯倍率**(投注总额 ¥100)。 +> 例:第 2 波 `0.75 × 4 = 3.00 倍` → `3.00 × 100 × 2 = ¥600` ✓ + - 投注总额 ¥100 → **本局总倍率 = 2180 ÷ 100 = 21.80x**(未触及 500x 封顶) - 本旋只有 **1 个胡**(一直挂在轴5)→ **不触发免费旋转**(需 3+) @@ -1380,12 +1847,152 @@ Base:`{host}/api/mahjong`,全部 POST + JSON + token 鉴权。 --- +## 16. 免费旋转完整示例(触发 → 逐旋 → 重触发 → 触顶 30 → FINISH) + +> 场景:**普通旋转**,投注档 **¥100**。 +> 覆盖:3 胡触发 12 旋、**FS 内阶梯起点是 x2**、金色→百搭、百搭在 FS 内被用掉、FS 内重触发、**总旋数封顶 30**、FINISH 帧。 +> 图例同 §15:`A`發 `B`中 `C`白板 `D`八萬 `E`花筒 `F`索红绿 `G`筒 `H`索绿 `W`百搭 `S`胡 **`*`=金色**。 + +### 16.1 基础旋(`spinNum = 0`,阶梯 x1)——触发盘 + +| 轴 → | 1 | 2 | 3 | 4 | 5 | +|---|---|---|---|---|---| +| 行1 | B | C | **S** | E | H | +| 行2 | D | A | B | **S** | F | +| 行3 | E | F | G | D | **S** | +| 行4 | H | C | D | G | B | + +- **判定**:轴1 只有 B/D/E/H,它们在第 2 轴全断 → **本旋没有任何 1024 路中奖组合** +- **胡**:3 个(轴3 行1、轴4 行2、轴5 行3)→ **触发免费旋转,赠 12 旋** +- **赔付**:`0`(胡本身不赔现金) +- 本步下发:`spinNum=0`、`scatterTrigger={scatterCount:3, freeSpins:12, extraSpins:0}`、`spinsRemaining=**12**`、`state=PLAYING` + +### 16.2 FS 第 1 旋(`spinNum = 1`,**阶梯从 x2 起**)——两波 + 金色转百搭 + +**① 消除前盘面**(FS 每旋都会整盘重发) + +| 轴 → | 1 | 2 | 3 | 4 | 5 | +|---|---|---|---|---|---| +| 行1 | B | D | B | C | F | +| 行2 | H | **B\*** | E | E | G | +| 行3 | C | E | H | A | D | +| 行4 | F | G | D | H | C | + +**② 第 1 波(阶梯 x2)**:中 B:轴1 = 1(行1)/ 轴2 = 1(行2金)/ 轴3 = 1(行1)/ 轴4 = 0 → **3 连,ways = 1** +→ 赔付 **0.5 倍**(中 3 连)× ways 1 = 0.50 倍 → 本波赢额 = `0.50 × ¥100 × x2` = **¥100** +→ 金色格 (行2,轴2) 参与中奖 → **就地变 `W`**(本旋第一个百搭) + +**③ 第 1 波落定后**:轴1 顶部补 1 张;轴3 顶部补 1 张;**轴2 没有空洞**(金色格变 W)→ 不动 + +| 轴 → | 1 | 2 | 3 | 4 | 5 | +|---|---|---|---|---|---| +| 行1 | **A** | D | **A** | C | F | +| 行2 | H | **W** | E | E | G | +| 行3 | C | E | H | A | D | +| 行4 | F | G | D | H | C | + +**④ 第 2 波(阶梯 x3)**:索绿 H:轴1 = 1(行2)/ 轴2 = **1(百搭)** / 轴3 = 1(行3)/ 轴4 = 1(行4)/ 轴5 = 0 → **4 连,ways = 1** +→ 赔付 **0.25 倍**(索绿 4 连)× ways 1 = 0.25 倍 → 本波赢额 = `0.25 × ¥100 × x3` = **¥75** +→ `W` 参与成链 → **被消除**;本波无金色 → 不产生新百搭 + +**⑤ 本旋合计** ¥100 + ¥75 = **¥175**;补位符号取"不会形成新链"的组合 → **本旋结束** +→ `spinNum=1`、`spinsRemaining=**11**`(12 − 1) + +### 16.3 FS 第 2 旋(`spinNum = 2`,阶梯重新从 x2 起) +| 轴 → | 1 | 2 | 3 | 4 | 5 | +|---|---|---|---|---|---| +| 行1 | C | E | C | A | D | +| 行2 | G | C | F | C | E | +| 行3 | H | D | G | H | F | +| 行4 | E | F | A | D | G | + +- **白板 C**:轴1 = 1 / 轴2 = 1 / 轴3 = 1 / 轴4 = 1 / 轴5 = 0 → **4 连,ways = 1** +- 赔付 **1 倍**(白板 4 连)× ways 1 = 1.00 倍 → 本波赢额 = `1.00 × ¥100 × x2` = **¥200** +- 补位后无新链 → **本旋结束**;`spinsRemaining=**10**` + +> **注意**:阶梯是**每旋重置**的——第 2 旋的起点又回到 **x2**,不是接着上一旋的 x3。 + +### 16.4 FS 第 3 旋(`spinNum = 3`)——**重触发 +12** + +| 轴 → | 1 | 2 | 3 | 4 | 5 | +|---|---|---|---|---|---| +| 行1 | H | D | **S** | A | E | +| 行2 | E | F | C | **S** | G | +| 行3 | A | B | G | C | **S** | +| 行4 | G | H | D | E | F | + +- 本旋**没有 1024 路中奖组合**(轴1 的 H/E/A/G 在第 2 轴全断) +- **但落了 3 个胡** → **重触发**:`retriggerInfo={scatterCount:3, extraSpins:12}` +- 旋数账本:已发 = 12 + 12 = **24**;已播 3 旋 → `spinsRemaining = 24 − 3 = **21**` +- 本旋赢额 `0` + +### 16.5 旋数账本 + 整局金额(全程 36 旋) + +**① 旋数账本** + +| 时刻 | 事件 | 检查(已发放 + 应赠 ≤ 36?) | 已发放总旋数 | 已播 | `spinsRemaining` | +|---|---|---|---|---|---| +| 基础旋 | 3 胡 → 触发 +12 | 0 + 12 = 12 ✓ | 12 | 0 | **12** | +| FS 1 | 中 3 连(x2) + 索绿 4 连(x3) | — | 12 | 1 | **11** | +| FS 2 | 白板 4 连(x2) | — | 12 | 2 | **10** | +| FS 3 | 3 胡 → **重触发 +12** | 12 + 12 = 24 ✓ | **24** | 3 | **21** | +| FS 4 ~ 9 | 普通旋(各 1~3 波) | — | 24 | 9 | 15 | +| **FS 10** | 又落 3 胡 → **重触发 +12** | 24 + 12 = **36** ✓ | **36**(顶到上限) | 10 | **26** | +| FS 11 ~ 35 | 普通旋。**这一段里任何一个旋即便摇出 3 个胡,也会因为 `36 + 12 = 48 > 36` 而【不落胡】** | 48 > 36 ✗ | 36 | 35 | 1 | +| **FS 36** | 最后一旋播完 | — | 36 | 36 | **0** | + +> **封顶规则(用户定案)**:**落胡前先检查** `已发放总旋数 + 本次应赠 ≤ 36`,不满足就**本旋不落 3+ 胡**。 +> 所以最终旋数一定是"若干次完整赠送之和"(12 / 24 / 26 / 36 …),**永远不会出现"落了 3 个胡却只补几旋"**。 +> **触发边界**:`已发放 ≤ 24` 才可能再触发;`≥ 25` 之后一律不再落 3+ 胡 ⇒ **整局 FS 永远 ≤ 36 旋,37 旋以上不可能出现**。 +> 检查用的是**该旋实际摇出的胡数**(3 个 = +12、4 个 = +14…),所以会出现"同一个已发放旋数下,3 个胡能落、4 个胡不能落"——这是正确的。 + +**② 整局金额** + +投注档 **¥100**。前三旋已在前文逐步算过;FS 4~36 是普通旋,本示例按**每旋平均 1.0 倍**计(真实值由生成器决定,这里只为把账算通): + +| 段 | 旋数 | 每旋 | 倍率小计 | 金额 | +|---|---|---|---|---| +| 基础旋(触发盘) | 1 | 0 | 0 | ¥0 | +| FS 1 | 1 | — | 1.00 + 0.75 = **1.75** | ¥175 | +| FS 2 | 1 | — | **2.00** | ¥200 | +| FS 3(只重触发,本旋无中奖) | 1 | 0 | 0 | ¥0 | +| FS 4 ~ 9 | 6 | 1.0 | **6.00** | ¥600 | +| FS 10(只重触发,本旋无中奖) | 1 | 0 | 0 | ¥0 | +| FS 11 ~ 36 | 26 | 1.0 | **26.00** | ¥2600 | +| | **36 旋** | | **本局总倍率 35.75** | **¥3575** | + +- 未触 500x 封顶 ⇒ `maxWinReached = false` +- `payoutCap` 仍会按水位兜一层(水位不够时这场大奖不会满额出) + +### 16.6 FINISH 帧(整局最后一步) + +``` +state = "FINISH" +spinNum = 36 // 最后一旋 +spinsRemaining = 0 +totalMul = 35.75 // 整局总倍率 +totalWin = 3575.00 // 整局总赢额(= 35.75 × ¥100) +maxWinReached = false // 35.75 < 500,未触顶 +prizeInfo = { ¥3575 落 prize_range 区间匹配到的饰品 } +boxRecordId = 背包记录 id // 末帧「放入背包 / 立即分解」用它 +``` + +### 16.7 FS 内必须注意的 5 条 + +1. **阶梯每旋重置**:FS 每一旋都从 **x2** 开始(`2 → 4 → 6 → 10`),不会跨旋累计。 +2. **FS 内金色 / 百搭规则与基础旋完全一致**:金色只在第 2/3/4 轴;被消除后各自留一个百搭;百搭要满足消除链才被消除。 +3. **旋数用 `spinsRemaining`**:它是服务端每帧算好的倒计值,前端**直接显示即可**,不要自己加减(重触发那一帧 `−1` 和 `+12` 是同时发生的)。 +4. **触发盘本身可以有中奖**(本示例为了干净取 0 赔付);触发盘的中奖同样按 x1 阶梯结算。 +5. **爆奖档位 `bigWinLevel`(小/中/大)只在 FS 的旋末帧判**,按**单旋**赢额 ÷ 基础下注判档,同档一局可多次;基础旋恒为 `null`。 + +--- + ## 附录 A:与玫瑰女郎的字段/表名映射速查 | 玫瑰女郎 | 麻将胡了 | |---|---| | `game_olympus_box.total_ornaments` | `game_mahjong_box.total_rounds` | -| `game_olympus_ornament`(17 档饰品桶) | `game_mahjong_bucket`(10 档倍率桶) | +| `game_olympus_ornament`(17 档饰品桶) | `game_mahjong_bucket`(16 档饰品桶) | | `game_olympus_box_ornament.ornament_id` | `game_mahjong_box_bucket.bucket_id` | | `game_olympus_round.ornament_id` | `game_mahjong_round.bucket_id` | | `game_olympus_step.symbol_links` | `game_mahjong_step.ways_detail` |