This commit is contained in:
ray zhou
2026-06-29 14:51:55 +08:00
parent 225fb2bd28
commit 2dd9f17da9
319 changed files with 29461 additions and 9412 deletions

View File

@@ -0,0 +1,157 @@
---
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 文档