diff --git a/services/lucky-gift-service/deploy/mysql/initdb/001_lucky_gift_service.sql b/services/lucky-gift-service/deploy/mysql/initdb/001_lucky_gift_service.sql index c53ab2ad..04f3534b 100644 --- a/services/lucky-gift-service/deploy/mysql/initdb/001_lucky_gift_service.sql +++ b/services/lucky-gift-service/deploy/mysql/initdb/001_lucky_gift_service.sql @@ -106,7 +106,9 @@ CREATE TABLE IF NOT EXISTS lucky_gift_rule_versions ( user_24h_ordinary_win_factor_ppm BIGINT NOT NULL DEFAULT 1000000 COMMENT '触发后普通非零奖档保留权重,ppm;1000000表示不降权', jackpot_multiplier_ppms JSON NOT NULL DEFAULT (JSON_ARRAY()) COMMENT '动态大奖倍率列表,1x=1000000', jackpot_global_rtp_max_ppm BIGINT NOT NULL DEFAULT 0 COMMENT '动态大奖全局 RTP 上限,ppm', + jackpot_user_day_rtp_max_ppm BIGINT NOT NULL DEFAULT 0 COMMENT '兼容旧实例的用户日 RTP 上限影子列;新代码与轮次列双写', jackpot_user_round_rtp_max_ppm BIGINT NOT NULL DEFAULT 0 COMMENT '动态大奖用户已结算轮次 RTP 严格上限,ppm', + jackpot_user_72h_rtp_max_ppm BIGINT NOT NULL DEFAULT 0 COMMENT '兼容旧实例的用户 72 小时 RTP 上限影子列;新代码与 48 小时列双写', jackpot_user_48h_rtp_max_ppm BIGINT NOT NULL DEFAULT 0 COMMENT '动态大奖用户滚动 48 小时 RTP 严格上限,ppm', jackpot_spend_threshold_coins BIGINT NOT NULL DEFAULT 0 COMMENT '规则版本动态配置的用户 UTC 日累计消费大奖门槛金币', max_jackpot_hits_per_user_day BIGINT NOT NULL DEFAULT 0 COMMENT '单用户 UTC 日动态大奖命中上限', diff --git a/services/lucky-gift-service/deploy/mysql/migrations/006_settled_round_rtp_48h.sql b/services/lucky-gift-service/deploy/mysql/migrations/010_settled_round_rtp_48h.sql similarity index 77% rename from services/lucky-gift-service/deploy/mysql/migrations/006_settled_round_rtp_48h.sql rename to services/lucky-gift-service/deploy/mysql/migrations/010_settled_round_rtp_48h.sql index 04ae2f59..57187f07 100644 --- a/services/lucky-gift-service/deploy/mysql/migrations/006_settled_round_rtp_48h.sql +++ b/services/lucky-gift-service/deploy/mysql/migrations/010_settled_round_rtp_48h.sql @@ -2,43 +2,48 @@ SET NAMES utf8mb4 COLLATE utf8mb4_unicode_ci; USE hyapp_lucky_gift; +-- 生产 main 的 006 已用于 AB 实验;本迁移编号调整为 010 以保证发布目录内版本唯一。 -- 性能边界: --- 1. 规则版本表只保存低频发布的不可变配置,两个 CHANGE 仅改列名/注释且保持 BIGINT/NOT NULL/DEFAULT 不变; --- 显式要求 ALGORITHM=INSTANT,若目标 MySQL 不支持即时 DDL 就直接失败,禁止静默退化为整表 COPY。 --- 只有旧、新列异常并存时才扫描这张小配置表把旧非零值补到新列,再即时删除旧列;不会扫描任何抽奖事实。 +-- 1. 规则版本表只保存低频发布的不可变配置。生产使用双实例滚动,旧实例仍硬编码 day/72h 列, +-- 因此本次只能执行 expand:补齐 old/new 四列并双向回填,绝不 CHANGE/DROP 旧列。 +-- 新版本仓储会双写两套列;确认所有旧实例与回滚窗口都结束后,才能用独立 contract migration 删除影子列。 +-- 所有加列显式要求 ALGORITHM=INSTANT,若目标 MySQL 不支持则直接失败,禁止退化为整表 COPY。 -- 2. lucky_rtp_windows 是热状态表。三个常量默认列分别使用即时 DDL,不回写历史行;每条 ALTER 仍需短暂 MDL, -- 上线前应确认没有长事务占用该表,否则迁移会等待元数据锁,但不会扫描 lucky_draw_records 等明细大表。 -- 3. lucky_user_rtp_windows 是全新空表,主键与待资格索引随建表一次完成;迁移不做历史资格回填,避免全表聚合。 --- 旧库保留 day/72h 列,新库可能已由 initdb 创建 round/48h 列;逐列探测保证两种环境和重复执行都安全。 --- CHANGE 保持列类型、空值属性与默认值完全不变,只把唯一业务口径改成“已结算轮次”和“滚动 48 小时”。 +-- 旧库只有 day/72h,新测试库可能只有 round/48h;逐列补齐四列,让新旧二进制在发布和回滚窗口内都能工作。 SET @has_old_column := (SELECT COUNT(*) FROM information_schema.COLUMNS WHERE TABLE_SCHEMA = DATABASE() AND TABLE_NAME = 'lucky_gift_rule_versions' AND COLUMN_NAME = 'jackpot_user_day_rtp_max_ppm'); -SET @has_new_column := (SELECT COUNT(*) FROM information_schema.COLUMNS WHERE TABLE_SCHEMA = DATABASE() AND TABLE_NAME = 'lucky_gift_rule_versions' AND COLUMN_NAME = 'jackpot_user_round_rtp_max_ppm'); -SET @ddl := CASE - WHEN @has_old_column > 0 AND @has_new_column = 0 THEN 'ALTER TABLE lucky_gift_rule_versions CHANGE COLUMN jackpot_user_day_rtp_max_ppm jackpot_user_round_rtp_max_ppm BIGINT NOT NULL DEFAULT 0 COMMENT ''动态大奖用户已结算轮次 RTP 严格上限,ppm'', ALGORITHM=INSTANT' - WHEN @has_old_column = 0 AND @has_new_column = 0 THEN 'ALTER TABLE lucky_gift_rule_versions ADD COLUMN jackpot_user_round_rtp_max_ppm BIGINT NOT NULL DEFAULT 0 COMMENT ''动态大奖用户已结算轮次 RTP 严格上限,ppm'', ALGORITHM=INSTANT' - ELSE 'SELECT 1' -END; +SET @ddl := IF(@has_old_column = 0, + 'ALTER TABLE lucky_gift_rule_versions ADD COLUMN jackpot_user_day_rtp_max_ppm BIGINT NOT NULL DEFAULT 0 COMMENT ''兼容旧实例的用户日 RTP 上限影子列;新代码与轮次列双写'' AFTER jackpot_global_rtp_max_ppm, ALGORITHM=INSTANT', + 'SELECT 1'); +PREPARE stmt FROM @ddl; EXECUTE stmt; DEALLOCATE PREPARE stmt; + +SET @has_new_column := (SELECT COUNT(*) FROM information_schema.COLUMNS WHERE TABLE_SCHEMA = DATABASE() AND TABLE_NAME = 'lucky_gift_rule_versions' AND COLUMN_NAME = 'jackpot_user_round_rtp_max_ppm'); +SET @ddl := IF(@has_new_column = 0, + 'ALTER TABLE lucky_gift_rule_versions ADD COLUMN jackpot_user_round_rtp_max_ppm BIGINT NOT NULL DEFAULT 0 COMMENT ''动态大奖用户已结算轮次 RTP 严格上限,ppm'' AFTER jackpot_user_day_rtp_max_ppm, ALGORITHM=INSTANT', + 'SELECT 1'); PREPARE stmt FROM @ddl; EXECUTE stmt; DEALLOCATE PREPARE stmt; --- 历史 004 可能在全量重放时再次补出旧列。若旧、新列同时存在,以已发布的新列为准,仅在新值为 0 时承接旧非零值, --- 随后删除旧列,保证仓储永远只面对一个口径;规则版本表数据量是发布次数级,不是送礼流水级。 SET @has_old_column := (SELECT COUNT(*) FROM information_schema.COLUMNS WHERE TABLE_SCHEMA = DATABASE() AND TABLE_NAME = 'lucky_gift_rule_versions' AND COLUMN_NAME = 'jackpot_user_day_rtp_max_ppm'); SET @has_new_column := (SELECT COUNT(*) FROM information_schema.COLUMNS WHERE TABLE_SCHEMA = DATABASE() AND TABLE_NAME = 'lucky_gift_rule_versions' AND COLUMN_NAME = 'jackpot_user_round_rtp_max_ppm'); SET @ddl := IF(@has_old_column > 0 AND @has_new_column > 0, 'UPDATE lucky_gift_rule_versions SET jackpot_user_round_rtp_max_ppm = jackpot_user_day_rtp_max_ppm WHERE jackpot_user_round_rtp_max_ppm = 0 AND jackpot_user_day_rtp_max_ppm <> 0', 'SELECT 1'); PREPARE stmt FROM @ddl; EXECUTE stmt; DEALLOCATE PREPARE stmt; SET @ddl := IF(@has_old_column > 0 AND @has_new_column > 0, - 'ALTER TABLE lucky_gift_rule_versions DROP COLUMN jackpot_user_day_rtp_max_ppm, ALGORITHM=INSTANT', 'SELECT 1'); + 'UPDATE lucky_gift_rule_versions SET jackpot_user_day_rtp_max_ppm = jackpot_user_round_rtp_max_ppm WHERE jackpot_user_day_rtp_max_ppm = 0 AND jackpot_user_round_rtp_max_ppm <> 0', 'SELECT 1'); PREPARE stmt FROM @ddl; EXECUTE stmt; DEALLOCATE PREPARE stmt; SET @has_old_column := (SELECT COUNT(*) FROM information_schema.COLUMNS WHERE TABLE_SCHEMA = DATABASE() AND TABLE_NAME = 'lucky_gift_rule_versions' AND COLUMN_NAME = 'jackpot_user_72h_rtp_max_ppm'); +SET @ddl := IF(@has_old_column = 0, + 'ALTER TABLE lucky_gift_rule_versions ADD COLUMN jackpot_user_72h_rtp_max_ppm BIGINT NOT NULL DEFAULT 0 COMMENT ''兼容旧实例的用户 72 小时 RTP 上限影子列;新代码与 48 小时列双写'' AFTER jackpot_user_round_rtp_max_ppm, ALGORITHM=INSTANT', + 'SELECT 1'); +PREPARE stmt FROM @ddl; EXECUTE stmt; DEALLOCATE PREPARE stmt; + SET @has_new_column := (SELECT COUNT(*) FROM information_schema.COLUMNS WHERE TABLE_SCHEMA = DATABASE() AND TABLE_NAME = 'lucky_gift_rule_versions' AND COLUMN_NAME = 'jackpot_user_48h_rtp_max_ppm'); -SET @ddl := CASE - WHEN @has_old_column > 0 AND @has_new_column = 0 THEN 'ALTER TABLE lucky_gift_rule_versions CHANGE COLUMN jackpot_user_72h_rtp_max_ppm jackpot_user_48h_rtp_max_ppm BIGINT NOT NULL DEFAULT 0 COMMENT ''动态大奖用户滚动 48 小时 RTP 严格上限,ppm'', ALGORITHM=INSTANT' - WHEN @has_old_column = 0 AND @has_new_column = 0 THEN 'ALTER TABLE lucky_gift_rule_versions ADD COLUMN jackpot_user_48h_rtp_max_ppm BIGINT NOT NULL DEFAULT 0 COMMENT ''动态大奖用户滚动 48 小时 RTP 严格上限,ppm'', ALGORITHM=INSTANT' - ELSE 'SELECT 1' -END; +SET @ddl := IF(@has_new_column = 0, + 'ALTER TABLE lucky_gift_rule_versions ADD COLUMN jackpot_user_48h_rtp_max_ppm BIGINT NOT NULL DEFAULT 0 COMMENT ''动态大奖用户滚动 48 小时 RTP 严格上限,ppm'' AFTER jackpot_user_72h_rtp_max_ppm, ALGORITHM=INSTANT', + 'SELECT 1'); PREPARE stmt FROM @ddl; EXECUTE stmt; DEALLOCATE PREPARE stmt; SET @has_old_column := (SELECT COUNT(*) FROM information_schema.COLUMNS WHERE TABLE_SCHEMA = DATABASE() AND TABLE_NAME = 'lucky_gift_rule_versions' AND COLUMN_NAME = 'jackpot_user_72h_rtp_max_ppm'); @@ -47,7 +52,7 @@ SET @ddl := IF(@has_old_column > 0 AND @has_new_column > 0, 'UPDATE lucky_gift_rule_versions SET jackpot_user_48h_rtp_max_ppm = jackpot_user_72h_rtp_max_ppm WHERE jackpot_user_48h_rtp_max_ppm = 0 AND jackpot_user_72h_rtp_max_ppm <> 0', 'SELECT 1'); PREPARE stmt FROM @ddl; EXECUTE stmt; DEALLOCATE PREPARE stmt; SET @ddl := IF(@has_old_column > 0 AND @has_new_column > 0, - 'ALTER TABLE lucky_gift_rule_versions DROP COLUMN jackpot_user_72h_rtp_max_ppm, ALGORITHM=INSTANT', 'SELECT 1'); + 'UPDATE lucky_gift_rule_versions SET jackpot_user_72h_rtp_max_ppm = jackpot_user_48h_rtp_max_ppm WHERE jackpot_user_72h_rtp_max_ppm = 0 AND jackpot_user_48h_rtp_max_ppm <> 0', 'SELECT 1'); PREPARE stmt FROM @ddl; EXECUTE stmt; DEALLOCATE PREPARE stmt; -- 历史窗口无法可靠推导首笔、末笔和闭窗业务时间,因此默认 0 明确表示 unknown;新运行逻辑只对新窗口写真实 paid_at。 diff --git a/services/lucky-gift-service/deploy/mysql/migrations/010_v2_high_water_boost.sql b/services/lucky-gift-service/deploy/mysql/migrations/011_v2_high_water_boost.sql similarity index 100% rename from services/lucky-gift-service/deploy/mysql/migrations/010_v2_high_water_boost.sql rename to services/lucky-gift-service/deploy/mysql/migrations/011_v2_high_water_boost.sql diff --git a/services/lucky-gift-service/internal/storage/mysql/lucky_gift_experiment_repository.go b/services/lucky-gift-service/internal/storage/mysql/lucky_gift_experiment_repository.go index 2ba5ed42..75fb7ec4 100644 --- a/services/lucky-gift-service/internal/storage/mysql/lucky_gift_experiment_repository.go +++ b/services/lucky-gift-service/internal/storage/mysql/lucky_gift_experiment_repository.go @@ -19,17 +19,31 @@ const luckyExperimentSelectColumns = `app_code, experiment_id, pool_id, name, // resolveLuckyGiftRuleConfigForUser 是实验期的配置选择入口: // 无未完成实验时保持"最新版本生效"的原始语义;有实验时按白名单和哈希分桶钉住组内版本。 -// 实验行使用一致性快照读(不加锁):给它加锁会与创建/结束实验的"规则行→实验行"锁序构成 AB-BA。 -// 已知取舍:创建实验提交瞬间,快照早于提交而规则行锁读晚于提交的在途抽奖,会按"最新版本=treatment" -// 开奖一次——毫秒级窗口,两组配置都是已校验可运行的版本,无资金风险,仅统计上可忽略的归组误差。 +// 事务开奖先锁最新规则行,再一致性读取实验:这与创建/结束实验保持"规则行→实验行"顺序, +// 同时确保 REPEATABLE READ 快照建立在前一笔同奖池结算提交之后,滚动 RTP 不会漏掉刚完成的抽奖。 +// Check 等只读入口不加锁,仍使用普通一致性快照,避免把低频配置检查串行化。 // 实验状态(放量/白名单/暂停)变更会让分组在 Check 与 Draw 之间漂移;创建入口强制两组同策略,漂移无害。 // 钉住的版本行缺失表示不可变事实被外部破坏,必须 fail-close,不能静默回落最新版本改变实验语义。 func (r *Repository) resolveLuckyGiftRuleConfigForUser(ctx context.Context, q luckyRuleQueryer, appCode string, poolID string, userKey string, forUpdate bool) (domain.RuleConfig, bool, error) { + var lockedLatest domain.RuleConfig + var lockedLatestExists bool + var err error + if forUpdate { + // 最新规则行是同奖池开奖和实验管理共享的串行化入口。先锁它再读取实验, + // 既固定事务快照边界,也避免 control/treatment 两个版本按用户分组产生反向锁序。 + lockedLatest, lockedLatestExists, err = r.getLuckyGiftRuleConfig(ctx, q, appCode, poolID, true) + if err != nil { + return domain.RuleConfig{}, false, err + } + } experiment, exists, err := r.getActiveLuckyGiftExperiment(ctx, q, appCode, poolID, false) if err != nil { return domain.RuleConfig{}, false, err } if !exists { + if forUpdate { + return lockedLatest, lockedLatestExists, nil + } return r.getLuckyGiftRuleConfig(ctx, q, appCode, poolID, forUpdate) } override := "" diff --git a/services/lucky-gift-service/internal/storage/mysql/lucky_gift_rule_config_repository.go b/services/lucky-gift-service/internal/storage/mysql/lucky_gift_rule_config_repository.go index eb6e58da..2b9e77c4 100644 --- a/services/lucky-gift-service/internal/storage/mysql/lucky_gift_rule_config_repository.go +++ b/services/lucky-gift-service/internal/storage/mysql/lucky_gift_rule_config_repository.go @@ -79,14 +79,18 @@ func (r *Repository) publishLuckyGiftRuleConfigTx(ctx context.Context, tx *sql.T if config.JackpotMultiplierPPMs == nil { config.JackpotMultiplierPPMs = []int64{} } - // 旧的进程内调用方可能仍填写 day/72h 别名;仓储只持久化轮次/48h 两个唯一口径, - // 兼容赋值必须在生成不可变版本前完成,避免同一规则出现两套可漂移的数据库字段。 + // 旧的进程内调用方可能仍填写 day/72h 别名;业务口径只认轮次/48h, + // 兼容赋值必须在生成不可变版本前完成,随后再把同一值双写到旧影子列供滚动升级和回滚读取。 if config.JackpotUserRoundRTPMaxPPM == 0 { config.JackpotUserRoundRTPMaxPPM = config.JackpotUserDayRTPMaxPPM } if config.JackpotUser48hRTPMaxPPM == 0 { config.JackpotUser48hRTPMaxPPM = config.JackpotUser72hRTPMaxPPM } + // 生产双实例需要允许新旧二进制短时共存和安全回滚。兼容窗口内新代码同时写入 + // day/round 与 72h/48h 两组列;旧代码只读取影子列,新代码只把新语义暴露到领域层。 + config.JackpotUserDayRTPMaxPPM = config.JackpotUserRoundRTPMaxPPM + config.JackpotUser72hRTPMaxPPM = config.JackpotUser48hRTPMaxPPM jackpotMultiplierPPMsJSON, err := json.Marshal(config.JackpotMultiplierPPMs) if err != nil { return domain.RuleConfig{}, fmt.Errorf("marshal lucky gift jackpot multipliers: %w", err) @@ -125,11 +129,13 @@ func (r *Repository) publishLuckyGiftRuleConfigTx(ctx context.Context, tx *sql.T v2_high_water_boost_threshold_coins, v2_high_water_boost_factor_ppm, v2_high_water_boost_recover_coins, recharge_boost_window_ms, recharge_boost_factor_ppm, user_24h_rtp_threshold_ppm, user_24h_ordinary_win_factor_ppm, jackpot_multiplier_ppms, - jackpot_global_rtp_max_ppm, jackpot_user_round_rtp_max_ppm, jackpot_user_48h_rtp_max_ppm, + jackpot_global_rtp_max_ppm, + jackpot_user_day_rtp_max_ppm, jackpot_user_round_rtp_max_ppm, + jackpot_user_72h_rtp_max_ppm, jackpot_user_48h_rtp_max_ppm, jackpot_spend_threshold_coins, max_jackpot_hits_per_user_day, max_single_payout, user_hourly_payout_cap, user_daily_payout_cap, device_daily_payout_cap, room_hourly_payout_cap, anchor_daily_payout_cap - ) VALUES (?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?)`, + ) VALUES (?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?)`, config.AppCode, config.PoolID, config.RuleVersion, config.Enabled, config.StrategyVersion, config.TargetRTPPPM, config.PoolRatePPM, config.ProfitRatePPM, config.AnchorRatePPM, config.SettlementWindowWager, config.ControlBandPPM, config.GiftPriceReference, @@ -141,7 +147,9 @@ func (r *Repository) publishLuckyGiftRuleConfigTx(ctx context.Context, tx *sql.T config.V2HighWaterBoostThresholdCoins, config.V2HighWaterBoostFactorPPM, config.V2HighWaterBoostRecoverCoins, config.RechargeBoostWindowMS, config.RechargeBoostFactorPPM, config.User24HourRTPThresholdPPM, config.User24HourOrdinaryWinFactorPPM, jackpotMultiplierPPMsJSON, - config.JackpotGlobalRTPMaxPPM, config.JackpotUserRoundRTPMaxPPM, config.JackpotUser48hRTPMaxPPM, + config.JackpotGlobalRTPMaxPPM, + config.JackpotUserDayRTPMaxPPM, config.JackpotUserRoundRTPMaxPPM, + config.JackpotUser72hRTPMaxPPM, config.JackpotUser48hRTPMaxPPM, config.JackpotSpendThresholdCoins, config.MaxJackpotHitsPerUserDay, config.MaxSinglePayout, config.UserHourlyPayoutCap, config.UserDailyPayoutCap, config.DeviceDailyPayoutCap, config.RoomHourlyPayoutCap, config.AnchorDailyPayoutCap, @@ -205,7 +213,11 @@ func (r *Repository) ListLuckyGiftRuleConfigs(ctx context.Context, appCode strin v.v2_high_water_boost_threshold_coins, v.v2_high_water_boost_factor_ppm, v.v2_high_water_boost_recover_coins, v.recharge_boost_window_ms, v.recharge_boost_factor_ppm, v.user_24h_rtp_threshold_ppm, v.user_24h_ordinary_win_factor_ppm, v.jackpot_multiplier_ppms, - v.jackpot_global_rtp_max_ppm, v.jackpot_user_round_rtp_max_ppm, v.jackpot_user_48h_rtp_max_ppm, + v.jackpot_global_rtp_max_ppm, + CASE WHEN v.jackpot_user_round_rtp_max_ppm = 0 AND v.jackpot_user_day_rtp_max_ppm <> 0 + THEN v.jackpot_user_day_rtp_max_ppm ELSE v.jackpot_user_round_rtp_max_ppm END, + CASE WHEN v.jackpot_user_48h_rtp_max_ppm = 0 AND v.jackpot_user_72h_rtp_max_ppm <> 0 + THEN v.jackpot_user_72h_rtp_max_ppm ELSE v.jackpot_user_48h_rtp_max_ppm END, v.jackpot_spend_threshold_coins, v.max_jackpot_hits_per_user_day, v.max_single_payout, v.user_hourly_payout_cap, v.user_daily_payout_cap, v.device_daily_payout_cap, v.room_hourly_payout_cap, v.anchor_daily_payout_cap @@ -251,7 +263,11 @@ const luckyRuleConfigSelectSQL = ` v2_high_water_boost_threshold_coins, v2_high_water_boost_factor_ppm, v2_high_water_boost_recover_coins, recharge_boost_window_ms, recharge_boost_factor_ppm, user_24h_rtp_threshold_ppm, user_24h_ordinary_win_factor_ppm, jackpot_multiplier_ppms, - jackpot_global_rtp_max_ppm, jackpot_user_round_rtp_max_ppm, jackpot_user_48h_rtp_max_ppm, + jackpot_global_rtp_max_ppm, + CASE WHEN jackpot_user_round_rtp_max_ppm = 0 AND jackpot_user_day_rtp_max_ppm <> 0 + THEN jackpot_user_day_rtp_max_ppm ELSE jackpot_user_round_rtp_max_ppm END, + CASE WHEN jackpot_user_48h_rtp_max_ppm = 0 AND jackpot_user_72h_rtp_max_ppm <> 0 + THEN jackpot_user_72h_rtp_max_ppm ELSE jackpot_user_48h_rtp_max_ppm END, jackpot_spend_threshold_coins, max_jackpot_hits_per_user_day, max_single_payout, user_hourly_payout_cap, user_daily_payout_cap, device_daily_payout_cap, room_hourly_payout_cap, anchor_daily_payout_cap @@ -415,7 +431,7 @@ func scanLuckyGiftRuleConfig(scanner luckyScanner) (domain.RuleConfig, error) { if config.JackpotMultiplierPPMs == nil { config.JackpotMultiplierPPMs = []int64{} } - // 数据库只有轮次/48h 两个真实字段;回填旧别名仅用于同进程源码兼容,运行策略仍由 marker 选择新语义。 + // 数据库兼容期保留旧影子列,但查询已收敛到轮次/48h 两个业务口径;旧别名仅供同进程源码兼容。 config.JackpotUserDayRTPMaxPPM = config.JackpotUserRoundRTPMaxPPM config.JackpotUser72hRTPMaxPPM = config.JackpotUser48hRTPMaxPPM config.SettledRoundRTPEligibility = true