--- name: 转盘分享周期绑定 overview: 推荐在 `share_url` 增加通用 `biz_type` + `biz_id`(转盘绑定 `lucky_reward_cycle.id`),按当前轮次复用/新建短码;过期校验以 `lucky_reward_cycle.end_at` 为真源,invite_bind 侧对过期轮次返回 skipped 而不影响注册。 todos: - id: ddl-share-url-biz content: share_url 增加 biz_type/biz_id + 唯一键;同步 slot_console/slot_user Model PHPDoc status: completed - id: share-service-wheel-reuse content: ShareService wheel 分支:按 active cycle_id 复用或新建短码 status: completed - id: invite-bind-expired-check content: LuckyRewardInviteLogic invite_bind 校验 share 绑定 cycle 未过期且匹配 active cycle,返回 skipped_expired_cycle status: completed - id: docs-tests content: 更新 05 文档;单测/集成测覆盖同轮复用、跨轮新建、过期跳过 status: completed isProject: false --- # 转盘分享链接与活动轮次绑定方案 ## 结论(直接回答你的二选一) **推荐:`biz_type` + `biz_id`(转盘 `biz_id = cycle_id`),不推荐单独加 `expire_at` 作为唯一方案。** | 方案 | 优点 | 缺点 | |------|------|------| | **仅 `expire_at`** | 查询简单(`expire_at > now`) | 与 `lucky_reward_cycle.end_at` **双份真源**,易漂移;`user_agent`/`red_packet` 等 origin 语义不同,全局 `expire_at` 难统一 | | **`biz_type` + `biz_id`(推荐)** | 与业务轮次一一对应;**有效期以 `lucky_reward_cycle.end_at` 为准**;同轮次内复用、新轮次新建;可扩展其它活动 | 需改 `ShareService` 查询与唯一键;invite_bind 需多一步校验 | 可选:**可冗余 `expire_at = cycle.end_at` 仅作索引加速**,但判定仍应 join/查 `lucky_reward_cycle`,不以 `share_url.expire_at` 为 SSOT。 --- ## 现状问题 [`ShareService::createUrl`](slot_console/app/service/ShareService.php) 通过 [`ShareUrlModel::isPlayerCreated($uid, $from)`](slot_console/app/model/ShareUrlModel.php) 按 `(creator_id, origin)` **永久复用**一条记录: ```php $model = ShareUrlModel::isPlayerCreated($this->userEntity->uid, $from); if ($model === false) { /* 新建 */ } ``` [`LuckyRewardInviteLogic::buildInviteLink`](slot_console/app/api/logic/LuckyRewardInviteLogic.php) 每次调 `createUrl(SHARE_FROM_WHEEL)`,**新 cycle 开始后仍返回旧短码**。 注册侧 [`slot_user` AbstractRegisterService](slot_user/app/service/register/AbstractRegisterService.php) 只校验 `short_code` 存在,**不校验轮次**;console `handleInviteBind` 直接用**当前 active cycle** 发奖,导致**旧链接可能计入新轮次**(与「每轮转盘有有效期」不符)。 你已确认过期策略:**注册/agent 关系照常;Lucky Rewards 不计助力**。 --- ## 推荐数据模型 在 [`s_common.share_url`](slot_user/app/model/ShareUrlModel.php) 增加: ```sql ALTER TABLE share_url ADD COLUMN biz_type varchar(32) NOT NULL DEFAULT '' COMMENT '业务类型,wheel填lucky_reward_cycle', ADD COLUMN biz_id bigint NOT NULL DEFAULT 0 COMMENT '业务ID,wheel填lucky_reward_cycle.id'; -- wheel:同一用户同一轮次唯一;其它 origin 保持 biz 为空 ADD UNIQUE KEY uk_creator_origin_biz (creator_id, creator_type, origin, biz_type, biz_id); ``` 语义约定: | origin | biz_type | biz_id | 行为 | |--------|----------|--------|------| | `user_agent` / `red_packet` / `user_self` | `''` | `0` | 现网永久短码,不变 | | `wheel` | `lucky_reward_cycle` | `cycle.id` | **每轮一条**,同轮复用 | `code` 加密 JSON 可冗余写入 `cycle_id` 便于排查,但**复用查询走 DB 列**,不走解密。 --- ## 核心流程 ```mermaid sequenceDiagram participant Client as ActivityPage participant Console as ShareService participant Cycle as lucky_reward_cycle participant Share as share_url participant User as slot_user_register participant Logic as LuckyRewardInviteLogic Client->>Console: invite-link / createShareUrl from=wheel Console->>Cycle: resolveActiveCycle Console->>Share: find uid+wheel+biz(cycle_id) alt 当前轮次已有短码 Share-->>Console: 复用 short_code else 新轮次或无记录 Console->>Share: insert 新 short_code + biz_id end Console-->>Client: code + land_url User->>Share: invite_code 查 short_code User->>Logic: invite_bind share_origin=wheel Logic->>Share: invite_code -> biz_id(cycle_id) Logic->>Cycle: 校验 cycle 仍进行中且 end_at>now alt cycle 已结束或不匹配 active Logic-->>User: skipped_expired_cycle else Logic-->>User: 正常助力流程 end ``` --- ## 实现要点(按服务) ### 1. slot_console — 分享创建 改动 [`ShareService::createUrl`](slot_console/app/service/ShareService.php): - **`from !== wheel`**:保持 `isPlayerCreated(uid, from)`(biz 为空)。 - **`from === wheel`**: 1. `LuckyRewardConfigService::requireEnabledConfig` + `resolveActiveCycle` 2. `ShareUrlModel::findByPlayerBiz($uid, 'wheel', 'lucky_reward_cycle', $cycleId)` 3. 命中则复用;否则新建并写入 `biz_type/biz_id` 4. (可选)冗余 `expire_at = cycle.end_at` 仅索引 同步改 [`GiftController::createShareUrl`](slot_console/app/api/controller/GiftController.php) 与 [`LuckyRewardInviteLogic::buildInviteLink`](slot_console/app/api/logic/LuckyRewardInviteLogic.php) 路径(均走 ShareService,一处改即可)。 [`ShareUrlModel`](slot_console/app/model/ShareUrlModel.php) + [`slot_user/ShareUrlModel`](slot_user/app/model/ShareUrlModel.php) 补 `@property` 与新查询方法。 ### 2. slot_console — invite_bind 过期校验 在 [`LuckyRewardInviteLogic::handleInviteBind`](slot_console/app/api/logic/LuckyRewardInviteLogic.php) 有效助力判定链中(`skipped_not_wheel` 之后)增加: 1. 用 `invite_code` 查 `share_url`(console 可读 `share_url` 或经 slot_user innerapi;优先 console 直读同库 `s_common.share_url`) 2. 若 `origin=wheel` 且 `biz_type=lucky_reward_cycle`: - 查 `lucky_reward_cycle` by `biz_id` - 若 `status=已结束` 或 `end_at <= now()` → 返回新 status **`skipped_expired_cycle`**(注册/agent 不受影响,仅不发 Spin) - 若 `biz_id != 当前 active cycle.id` → 同上(旧轮短码不计新轮助力) 在 [`InviteBindResultEntity`](slot_console/app/entity/luckyReward/InviteBindResultEntity.php) 增加常量 + 文档表更新([05 文档](docs/requirements/lucky_rewards/05_slot_console_邀请助力与弹窗方案.md) §3 判定表)。 ### 3. slot_user — 无需阻断注册 按你的选择:**不在注册阶段 reject invite_code**。`AbstractRegisterService` 可不改,或仅补日志(`share_url.biz_id` + cycle 状态)便于排障。 ### 4. 文档 更新 [05_slot_console_邀请助力与弹窗方案.md](docs/requirements/lucky_rewards/05_slot_console_邀请助力与弹窗方案.md) §2.1: - wheel 短码 **按 cycle 复用/新建**,非永久一条 - 过期短码:注册 OK,Lucky Rewards `skipped_expired_cycle` --- ## 测试 - **Unit**:`ShareService` wheel 分支 — mock cycle,`findByPlayerBiz` 命中复用 / 新 cycle 新建 - **Integration**:同一 uid 同 cycle 两次 `invite-link` 返回相同 `code`;模拟新 cycle 返回不同 `code` - **Integration**:旧 cycle 短码 + invite_bind → `skipped_expired_cycle`,无 helper/grant --- ## 为何不选「只加 expire_at」 1. **真源重复**:cycle 结束时间已在 [`lucky_reward_cycle.end_at`](backend/slot_admin/db/lucky_reward.sql);单独 `expire_at` 需与轮转任务同步,易不一致。 2. **无法表达「属于哪一轮」**:仅有 `expire_at` 难以区分「同用户第 N 轮 vs 第 N+1 轮」历史短码;`biz_id=cycle_id` 可精确归因与审计。 3. **影响面**:`expire_at` 若加在全局 `share_url`,需约定 `user_agent` 等为 0/NULL;而 `biz_id` 对非 wheel 恒为 0,更清晰。 --- ## 不在本次范围 - 修改 `user_agent` 分享逻辑 - 前端 Copy Link 缓存策略(后端保证同 cycle 幂等即可) - 删除历史 cycle 的 `share_url` 行(可保留作审计;过期靠 invite_bind 拦截)