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

6.8 KiB
Raw Blame History

name, overview, todos, isProject
name overview todos isProject
Spin去spin_id加锁 手动 Spin 去掉前端必填 `spin_id`,改为 Redis 用户锁防并发 + 事务内 `spin_available` 校验;`spin_id` 仅服务端写库用;连点拿不到锁立即报错。
id content status
api-validator spin API/Validator去掉 spin_id 入参Controller 两参调用 Logic completed
id content status
logic-lock LuckyRewardLogicSET NX 用户锁 fail-fast + 服务端生成 spin_id + 移除客户端幂等分支 completed
id content status
tests-docs RedisKeyManagerService 锁 key + 单测/文档/YApi 更新 completed
false

Spin 去掉前端 spin_idRedis 锁 + 次数校验)

产品约定(已确认)

  • 防并发Redis 用户级锁,连点第二笔 拿不到锁立即报错(不重试等待)。
  • 防重复扣次:事务内校验 spin_available > 0,成功则 -1;无次数则拒绝。
  • 不做客户端幂等 / 弱网重试幂等:超时后若已扣次,再次 spin 会报「无可用 Spin」→ 前端应改调 status / records 刷新,勿无脑重试 spin。
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()

  • 移除 LuckyRewardValidator::SCENE_SPIN 校验及对 spin_id 的读取。
  • 调用 $this->logic->spin($uid, $source)(两参)。

LuckyRewardValidator

  • 删除 SCENE_SPIN / spin_id 规则(或保留空 scene 备用spin 接口不再走 Validate。

2. Logic 核心

LuckyRewardLogic::spin()

变更 说明
签名 spin(int $uid, string $source): SpinResultEntity
删除 入口 findByUidAndSpinId + buildSpinResultFromRecord客户端幂等分支
新增 方法开头 acquireSpinLock($uid) / finally releaseSpinLock
新增 服务端生成 spinIdmanual:{uid}:{cycleId}:{uniqid()}(或 random_bytes 短串),仅用于 lucky_reward_spin_record.spin_id 满足 uk_uid_spinid
保留 开宝箱校验、grantDailySpinIfNeededensureUserCanSpin、事务内二次 spin_available 校验、抽奖与落库逻辑
响应 SpinResultEntity.spinId 仍返回(服务端生成,供 records 展示);isDuplicate 固定 false(或后续从 Entity 移除该字段需评估 FE 兼容性,建议先保留恒 false

Redis 锁实现(复用现有 RedLock

// 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 NXRedis::set($key, $token, 'EX', 5, 'NX'))替代 RedLock或给 RedLock 封装 tryLockOnce()建议:新增 private tryAcquireSpinLockOnce()SET NX EX 5,符合 fail-fast 选型。

3. 辅助方法

  • RedisKeyManagerService:新增 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},不受影响。

测试

做法
单元测 新增 LuckyRewardSpinLockUnitTestmock Redis 或使用可注入 lock 的测试双(若难 mock至少测 generateManualSpinId + validator 无 spin_id
Logic 单测 更新/新增:无 spin_id 参数签名;isDuplicate 恒 false
集成测 若有 DB spin 集成测则去掉 payload spin_id;并发锁集成测可选(需 Redis

当前仓库 spin 集成测,以 Logic 单测 + 手动冒烟为主。


文档


前端契约(联调说明)

POST /api/lucky-reward/spin
Body: {} 或空
  • 点击 Spin 后 UI 防抖(禁用按钮至响应返回)。
  • 请求失败/超时:不要立即再次 spinstatus / recordsspinAvailablemyAmount、最新 record。
  • 若返回「Spin is in progress」稍后重试或等当前请求结束。

已知边界(产品已接受)

场景 行为
连点 第二笔拿不到锁 → 立即报错
弱网:已成功但响应丢失 再次 spin → spin_available=0 报错;靠 status/records 恢复 UI
弱网:仍有多次数且用户再点 会消耗下一次 Spin非重试上一把

涉及文件