Files
cursor/plans/spin去spin_id加锁_0e00ca05.plan.md
ray zhou 2dd9f17da9 ok
2026-06-29 14:51:55 +08:00

158 lines
6.8 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: Spin去spin_id加锁
overview: 手动 Spin 去掉前端必填 `spin_id`,改为 Redis 用户锁防并发 + 事务内 `spin_available` 校验;`spin_id` 仅服务端写库用;连点拿不到锁立即报错。
todos:
- id: api-validator
content: spin API/Validator去掉 spin_id 入参Controller 两参调用 Logic
status: completed
- id: logic-lock
content: LuckyRewardLogicSET NX 用户锁 fail-fast + 服务端生成 spin_id + 移除客户端幂等分支
status: completed
- id: tests-docs
content: RedisKeyManagerService 锁 key + 单测/文档/YApi 更新
status: completed
isProject: false
---
# Spin 去掉前端 spin_idRedis 锁 + 次数校验)
## 产品约定(已确认)
- **防并发**Redis 用户级锁,连点第二笔 **拿不到锁立即报错**(不重试等待)。
- **防重复扣次**:事务内校验 `spin_available > 0`,成功则 `-1`;无次数则拒绝。
- **不做**客户端幂等 / 弱网重试幂等:超时后若已扣次,再次 spin 会报「无可用 Spin」→ **前端应改调 `status` / `records` 刷新**,勿无脑重试 spin。
```mermaid
sequenceDiagram
participant FE as Frontend
participant Logic as LuckyRewardLogic
participant Redis
participant DB
FE->>Logic: POST spin 无 body
Logic->>Redis: SET lock uid NX TTL 5s
alt lockFail
Redis-->>Logic: 未拿到
Logic-->>FE: BusinessException Spin处理中
else lockOk
Logic->>DB: spin_available>0 校验+抽奖+写record
Logic->>Redis: unlock
Logic-->>FE: SpinResultEntity
end
```
---
## 代码改动slot_console
### 1. API 层
**[`LuckyRewardController::spin()`](slot_console/app/api/controller/LuckyRewardController.php)**
- 移除 `LuckyRewardValidator::SCENE_SPIN` 校验及对 `spin_id` 的读取。
- 调用 `$this->logic->spin($uid, $source)`(两参)。
**[`LuckyRewardValidator`](slot_console/app/api/validator/LuckyRewardValidator.php)**
- 删除 `SCENE_SPIN` / `spin_id` 规则(或保留空 scene 备用spin 接口不再走 Validate。
### 2. Logic 核心
**[`LuckyRewardLogic::spin()`](slot_console/app/api/logic/LuckyRewardLogic.php)**
| 变更 | 说明 |
| --- | --- |
| 签名 | `spin(int $uid, string $source): SpinResultEntity` |
| 删除 | 入口 `findByUidAndSpinId` + `buildSpinResultFromRecord` 的**客户端幂等**分支 |
| 新增 | 方法开头 `acquireSpinLock($uid)` / `finally releaseSpinLock` |
| 新增 | 服务端生成 `spinId``manual:{uid}:{cycleId}:{uniqid()}`(或 `random_bytes` 短串),仅用于 `lucky_reward_spin_record.spin_id` 满足 `uk_uid_spinid` |
| 保留 | 开宝箱校验、`grantDailySpinIfNeeded``ensureUserCanSpin`、事务内二次 `spin_available` 校验、抽奖与落库逻辑 |
| 响应 | `SpinResultEntity.spinId` 仍返回(服务端生成,供 records 展示);`isDuplicate` 固定 `false`(或后续从 Entity 移除该字段需评估 FE 兼容性,建议先保留恒 false |
**Redis 锁实现**(复用现有 [`RedLock`](slot_console/app/service/RedLock.php)
```php
// RedisKeyManagerService 新增
/** Lucky Rewards 手动 Spin 用户锁前缀 */
const LUCKY_REWARD_SPIN_LOCK = 'lucky_reward:spin:lock:';
// Logic 内
$lockKey = RedisKeyManagerService::luckyRewardSpinLockKey($uid);
$lock = RedLock::getInstance('default')->lock($lockKey, 5000);
if ($lock === false) {
throw new BusinessException('Spin is in progress, please try again');
}
try {
// ... 原 spin 业务 ...
} finally {
RedLock::getInstance('default')->unlock($lock);
}
```
- TTL **5000ms**:覆盖一次抽奖+事务;`RedLock::lock` 默认 `retryCount=3` 会在内部短暂重试——若需严格「一次拿不到就失败」,在 Logic 侧用 **单次 `SET NX`**`Redis::set($key, $token, 'EX', 5, 'NX')`)替代 RedLock或给 RedLock 封装 `tryLockOnce()`。**建议**:新增 private `tryAcquireSpinLockOnce()``SET NX EX 5`,符合 fail-fast 选型。
### 3. 辅助方法
- [`RedisKeyManagerService`](slot_console/app/service/RedisKeyManagerService.php):新增 `luckyRewardSpinLockKey(int $uid): string`
- 可选private `generateManualSpinId(int $uid, int $cycleId): string` 集中生成写库 id。
---
## 不改 / 兼容
- **表结构**`lucky_reward_spin_record.spin_id` + `uk_uid_spinid` 保留;值改为服务端生成。
- **records API**:仍返回 `spinId` 字段,无 breaking change。
- **开宝箱自动首次 Spin**:仍用 `auto_first:{uid}:{cycle_id}`,不受影响。
---
## 测试
| 项 | 做法 |
| --- | --- |
| 单元测 | 新增 `LuckyRewardSpinLockUnitTest`mock Redis 或使用可注入 lock 的测试双(若难 mock至少测 `generateManualSpinId` + validator 无 spin_id |
| Logic 单测 | 更新/新增:无 `spin_id` 参数签名;`isDuplicate` 恒 false |
| 集成测 | 若有 DB spin 集成测则去掉 payload `spin_id`;并发锁集成测可选(需 Redis |
当前仓库 **无** spin 集成测,以 Logic 单测 + 手动冒烟为主。
---
## 文档
- [`04_slot_console_活动主流程方案.md`](docs/requirements/lucky_rewards/04_slot_console_活动主流程方案.md) §5改为 Redis 锁 + `spin_available`,去掉「前端 spin_id 必填」。
- [`00_整体技术方案.md`](docs/requirements/lucky_rewards/00_整体技术方案.md) §3.4 / §3.6:手动 Spin 幂等说明更新。
- [`lucky_reward_deploy.md`](slot_console/doc/lucky_reward_deploy.md) 冒烟项:去掉「带 spin_id 幂等」。
- **YApi** spin 接口cat 115删除 request `spin_id` 必填;补充并发锁错误文案;**FE 约定**spin 失败/超时先调 status。
---
## 前端契约(联调说明)
```text
POST /api/lucky-reward/spin
Body: {} 或空
```
- 点击 Spin 后 **UI 防抖**(禁用按钮至响应返回)。
- 请求失败/超时:**不要**立即再次 spin`status` / `records``spinAvailable``myAmount`、最新 record。
- 若返回「Spin is in progress」稍后重试或等当前请求结束。
---
## 已知边界(产品已接受)
| 场景 | 行为 |
| --- | --- |
| 连点 | 第二笔拿不到锁 → 立即报错 |
| 弱网:已成功但响应丢失 | 再次 spin → `spin_available=0` 报错;靠 status/records 恢复 UI |
| 弱网:仍有多次数且用户再点 | 会消耗**下一次** Spin非重试上一把 |
---
## 涉及文件
- [`slot_console/app/api/controller/LuckyRewardController.php`](slot_console/app/api/controller/LuckyRewardController.php)
- [`slot_console/app/api/logic/LuckyRewardLogic.php`](slot_console/app/api/logic/LuckyRewardLogic.php)
- [`slot_console/app/api/validator/LuckyRewardValidator.php`](slot_console/app/api/validator/LuckyRewardValidator.php)
- [`slot_console/app/service/RedisKeyManagerService.php`](slot_console/app/service/RedisKeyManagerService.php)
- 测试 + 需求/deploy/YApi 文档