--- name: 全局活动轮次 overview: Lucky Rewards 活动配置与轮次均改为全平台唯一:`lucky_reward_config`、`lucky_reward_tier_config`、`lucky_reward_cycle` 移除 `source`;转盘奖项已迁至 `reward_pool_item`(无 source,legacy `lucky_reward_prize_config` 确认删除);C 端仍用 `lucky_reward_player.source` 记录用户注册渠道。 todos: - id: ddl-migration content: 新增 lucky_reward_drop_source.sql(config/tier/cycle 删 source);确认 legacy prize_config 已 DROP;同步 lucky_reward.sql 与需求文档 status: completed - id: config-model-service content: LuckyRewardConfigModel 改为 findEnabled() 单例;ConfigService requireEnabledConfig/resolveActiveCycle/createNextCycle 去 source 参数 status: completed - id: cycle-model-rollover content: LuckyRewardCycleModel 全局 findActive/maxCycleNo;CycleRolloverService 去 source;TierConfig 去 source status: completed - id: console-callers content: Logic/Service 配置查询改 findEnabled();轮次查询改 findActive();player 写入仍保留 resolvedSource status: completed - id: admin-crud content: slot_admin Config/Tier Logic/Validate/Controller 去 source 字段;主配置改为单例校验 status: completed - id: tests-verify content: 更新集成测试 seed(全局 config+cycle);补跨渠道同 config/cycle 用例;phpunit + verify-slot-backend.sh status: completed isProject: false --- # Lucky Rewards 配置与轮次全局化(不区分渠道) ## 目标与边界 | 维度 | 变更后 | | --- | --- | | `lucky_reward_config` | **全平台唯一**主配置,**移除 `source` 列**(表内仅保留一行或业务层强制单例) | | `lucky_reward_tier_config` | **移除 `source` 列**,仅通过 `config_id` 关联主配置 | | `lucky_reward_prize_config` | **已废弃**;奖项 SSOT 为 **`reward_pool` + `reward_pool_item`**(`pool_code=lucky_reward`,**本身无 `source`**)。若环境仍残留 legacy 表,执行 DROP,**无需**对其 ALTER 删列 | | `lucky_reward_cycle` | **全平台唯一**进行中轮次,**移除 `source` 列** | | `lucky_reward_player.source` 等业务表 | **保留**,表示**用户注册/归属渠道**,供后台统计筛选,与活动配置无关 | | wheel 短码 `biz_id=cycle_id` | **不变**,全渠道共用同一 `cycle_id` | ```mermaid flowchart TB subgraph before [现状] ConfigA[config source=A] ConfigB[config source=B] ConfigA --> CycleA[cycle source=A] ConfigB --> CycleB[cycle source=B] end subgraph after [改造后] GlobalConfig[单一 lucky_reward_config] GlobalCycle[单一 active cycle] PlayerA[player source=渠道A] PlayerB[player source=渠道B] GlobalConfig --> GlobalCycle GlobalCycle --> PlayerA GlobalCycle --> PlayerB end ``` 与 [`00_整体技术方案.md`](docs/requirements/lucky_rewards/00_整体技术方案.md) 一致:cycle 表本无 `source`;配置亦改为全局单套参数。 --- ## 1. DDL 与数据迁移 新增 [`backend/slot_admin/db/lucky_reward_drop_source.sql`](backend/slot_admin/db/lucky_reward_drop_source.sql)(合并原 cycle 脚本,一次迁移三表): ### 1.1 `lucky_reward_config` 1. **合并多行配置**(若存在按渠道多行) - 保留 **`source='default'` 且 `status=1` 的行**;若无 default 启用行则保留 `id` 最小且 `status=1` 的行 - 将其余行的 `lucky_reward_tier_config.config_id` **重指向**保留行 - 删除其余 config 行 2. **改表** - `DROP INDEX idx_source` - `DROP COLUMN source` ### 1.2 `lucky_reward_tier_config` - `DROP COLUMN source`(已通过 `config_id` 关联,列冗余;admin Tier Logic/Validate 同步去 source 字段) ### 1.3 `lucky_reward_prize_config`(legacy) > 现状:转盘奖项已迁移至 [`reward_pool.sql`](backend/slot_admin/db/reward_pool.sql) 的 `reward_pool_item`,console 通过 [`LuckyRewardConfigService::listLuckyRewardPoolItems()`](slot_console/app/service/luckyReward/LuckyRewardConfigService.php) 读 `pool_code=lucky_reward`,**无渠道维度**。 迁移步骤(写入 `lucky_reward_drop_source.sql` 末尾或独立段落): 1. 若 **`lucky_reward_prize_config` 表仍存在** 且尚未迁移:先跑 [`reward_pool_migrate_lucky_reward_prize.sql`](backend/slot_admin/db/reward_pool_migrate_lucky_reward_prize.sql)(迁移脚本内 `@config_id` 改为取合并后的**唯一** config 行,不再 `WHERE source='default'`) 2. 验证 `reward_pool_item` 数据完整后,执行 [`reward_pool_drop_legacy_prize_config.sql`](backend/slot_admin/db/reward_pool_drop_legacy_prize_config.sql):`DROP TABLE IF EXISTS lucky_reward_prize_config` 3. **无 PHP Model/Logic 改动**(已无 `LuckyRewardPrizeConfig*` 代码路径;后台菜单已指向 `RewardPoolItemController`) 文档同步:[`04_slot_console_活动主流程方案.md`](docs/requirements/lucky_rewards/04_slot_console_活动主流程方案.md) 中将 `lucky_reward_prize_config` 表述改为 `reward_pool_item`。 ### 1.4 `lucky_reward_cycle` 1. 合并重复 `status=1`:保留 `id` 最大一行,其余改 `STATUS_ENDED=2`(不合并 player 行) 2. `DROP INDEX idx_source_status` → `DROP COLUMN source` → `ADD KEY idx_status (status)` ### 1.5 同步基线 DDL - [`backend/slot_admin/db/lucky_reward.sql`](backend/slot_admin/db/lucky_reward.sql) — config / tier / cycle 去掉 `source`;**不包含**已废弃的 prize_config 建表 - [`docs/requirements/lucky_rewards/02_管理后台方案.md`](docs/requirements/lucky_rewards/02_管理后台方案.md) §2.1 / §2.3 / §2.4(去掉「用 source 区分渠道」;§2.2 已说明 prize 在 reward_pool) --- ## 2. Model 层 ### 2.1 [`LuckyRewardConfigModel`](slot_console/app/model/common/LuckyRewardConfigModel.php)(console + admin 同步) - 删除 `@property string $source` - 删除 `findEnabledBySourceWithFallback(string $source)` - 新增 `findEnabled(): ?self` — `status=STATUS_ENABLED`,`order id asc`,取唯一启用配置 - admin Model 去掉 search 按 source 的 scope(若有) ### 2.2 [`LuckyRewardTierConfigModel`](slot_console/app/model/common/LuckyRewardTierConfigModel.php) - 删除 `@property string $source` - `listEnabledByConfigId(int $configId)` 不变 ### 2.3 [`LuckyRewardCycleModel`](slot_console/app/model/common/LuckyRewardCycleModel.php) - 删除 `source` 与 `findActiveBySource` / `maxCycleNo(string $source)` - 新增 `findActive(): ?self`、`maxCycleNo(): int` --- ## 3. Service 层(slot_console) ### 3.1 [`LuckyRewardConfigService`](slot_console/app/service/luckyReward/LuckyRewardConfigService.php) | 方法 | 改造 | | --- | --- | | `requireEnabledConfig()` | 无参;内部 `findEnabled()`,未开启抛 `BusinessException` | | `resolveActiveCycle(LuckyRewardConfigModel $activityConfig)` | 去掉 `$source`;查 `findActive()`;创建下一轮调 `createNextCycle($activityConfig, ...)` | | `createNextCycle(LuckyRewardConfigModel $activityConfig, ?int $baseTimestamp = null)` | 去掉 `$source`;`cycle_days` 直接取自传入的**唯一** `$activityConfig`;不写 cycle.source;事务 + `FOR UPDATE` 防并发双 active | ### 3.2 [`LuckyRewardCycleRolloverService`](slot_console/app/service/luckyReward/LuckyRewardCycleRolloverService.php) - `rolloverExpiredCycle`:去掉 `$expiredCycle->source` - 开启下一轮:`findActive()` 为空且 `findEnabled()` 非空时 `createNextCycle($activityConfig, $nowTimestamp)` --- ## 4. Logic / 其它调用点(slot_console) **配置读取**:所有 `findEnabledBySourceWithFallback($resolvedSource)` / `requireEnabledConfig($source)` 改为无参全局读取。 **轮次读取**:所有 `findActiveBySource(...)` 改为 `findActive()`。 **仍保留 `resolveSource($uid, $source)` 的场景**(仅写业务行渠道归属,**不**用于查配置): - `lucky_reward_player.source` - `lucky_reward_spin_grant.source` - `lucky_reward_helper.source` - `lucky_reward_spin_record.source` - 日志 / MQ 回调 `channel_code` 字段 | 文件 | 改动要点 | | --- | --- | | [`LuckyRewardLogic.php`](slot_console/app/api/logic/LuckyRewardLogic.php) | enter/spin/openBox/toBalance:配置无参;轮次 `findActive()`;player 写入仍用 `$resolvedSource` | | [`LuckyRewardInviteLogic.php`](slot_console/app/api/logic/LuckyRewardInviteLogic.php) | invite_bind、弹窗、claim:配置/轮次全局;helper/player 仍写用户渠道 | | [`LuckyRewardSupportInvitePendingService.php`](slot_console/app/service/luckyReward/LuckyRewardSupportInvitePendingService.php) | flush 时查全局 config + active cycle | | [`ShareService.php`](slot_console/app/service/ShareService.php) | `resolveOrCreateWheelShareRow`:`requireEnabledConfig()` + `resolveActiveCycle($config)` | Controller 仍向 Logic 传 `$request->userEntity->source`(供 player 归属),Logic 内部不再用它查 config。 --- ## 5. 管理后台(slot_admin) | 文件 | 改动 | | --- | --- | | [`LuckyRewardConfigLogic`](backend/slot_admin/app/game/logic/LuckyRewardConfigLogic.php) | 去掉 `source` 写入与 `ensureSourceUnique`;新增 **单例校验**(全表仅允许 1 行,update 除外) | | [`LuckyRewardConfigValidate`](backend/slot_admin/app/game/validate/LuckyRewardConfigValidate.php) | save/update scene 移除 `source` | | [`LuckyRewardConfigController`](backend/slot_admin/app/game/controller/LuckyRewardConfigController.php) | index 去掉 source 筛选;options 返回 `id,status`(无 source) | | [`LuckyRewardTierConfigLogic`](backend/slot_admin/app/game/logic/LuckyRewardTierConfigLogic.php) | 去掉 `source` 写入与列表 `config_source` 展示 | | [`LuckyRewardTierConfigValidate`](backend/slot_admin/app/game/validate/LuckyRewardTierConfigValidate.php) | 移除 `source` | | Model PHPDoc | Config / Tier / Cycle 三表同步 | [`LuckyRewardStatsLogic`](backend/slot_admin/app/game/logic/LuckyRewardStatsLogic.php):**不变** — 统计仍可按 `lucky_reward_player.source` 筛用户渠道。 **前端**(若 [`slot_admin_vue`](backend/slot_admin_vue) 有 Lucky Reward 配置页渠道字段):移除主配置/分层表单中的「渠道」输入与列表列(本计划标注为 admin 联调项,改 PHP 后前端需同步)。 --- ## 6. 测试 [`LuckyRewardDbTestCase::seedEnabledActivity`](slot_console/tests/Integration/LuckyRewardDbTestCase.php): - 不再按 `TEST_SOURCE` 创建独立 config;改为 **更新全局唯一 config 为 ENABLED**(或 insert 仅当表空) - cycle seed 不写 `source`;先 `findActive()` 结束旧 active - player / helper 测试行仍可写 `source => TEST_SOURCE` 模拟用户渠道 更新所有集成测试中的 `findEnabledBySourceWithFallback` / `findActiveBySource` / config 按 source 查询。 **新增用例**:两渠道用户(不同 `player.source`)进入活动,得到相同 `config` 参数与 `cycle_id`。 --- ## 7. 部署顺序 1. 执行 `lucky_reward_drop_source.sql`(合并 config + 删列) 2. **同批发布** slot_console + slot_admin(删列后旧代码会 SQL 报错) 3. slot_admin_vue 去渠道字段(可与后端同批或紧随其后) --- ## 8. 风险与验收 | 风险 | 处理 | | --- | --- | | 线上多渠道各有 config / active cycle | 迁移脚本合并为单行 config + 单行 active cycle | | 非 default 渠道 config 有差异化参数 | 迁移保留 default 启用行;运营需事先确认 default 参数为 SSOT | | 并发创建双 active cycle | `createNextCycle` 事务 + 行锁 | | 后台误建第二条 config | Logic 单例校验拦截 | **验收清单**: - 全渠道用户 `enter` 读到同一套 target_amount / cycle_days / tier - 全渠道同一 `cycle_id`;轮转后仅一条新 active - 后台主配置 CRUD 无渠道字段,不可新增第二条 - 统计页仍可按 **玩家 source** 筛选 - `phpunit` Lucky Rewards 集成测 + `verify-slot-backend.sh` PASS --- ## 涉及文件汇总 **DDL**:`lucky_reward_drop_source.sql`、`lucky_reward.sql`、`reward_pool_migrate_lucky_reward_prize.sql`(改 config 选取逻辑)、`reward_pool_drop_legacy_prize_config.sql`、需求文档 **slot_console**:`LuckyRewardConfigModel`、`LuckyRewardTierConfigModel`、`LuckyRewardCycleModel`、`LuckyRewardConfigService`、`LuckyRewardCycleRolloverService`、`LuckyRewardLogic`、`LuckyRewardInviteLogic`、`LuckyRewardSupportInvitePendingService`、`ShareService`、集成测试 **slot_admin**:`LuckyRewardConfigModel`、`LuckyRewardTierConfigModel`、`LuckyRewardCycleModel`、`LuckyRewardConfigLogic`、`LuckyRewardTierConfigLogic`、对应 Validate/Controller **无需改动(已无 source)**:`reward_pool` / `reward_pool_item`、`RewardPoolItemLogic`(转盘奖项 CRUD) **保留 source 不改**:`lucky_reward_player` 及 spin/helper/record/grant 业务表;`LuckyRewardStatsLogic` 玩家渠道筛选