Files
cursor/plans/转盘分享周期绑定_d7eeeacf.plan.md
ray zhou 2dd9f17da9 ok
2026-06-29 14:51:55 +08:00

173 lines
8.1 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

---
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 '业务IDwheel填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 复用/新建**,非永久一条
- 过期短码:注册 OKLucky 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 拦截)