Files
huang 5065d925ad
All checks were successful
构建并部署到测试环境(无 SSH) / build-and-deploy (push) Successful in 7m33s
fix: 修正套餐激活和时间字段nullable问题
核心变更:
1. Model层时间字段改为*time.Time并设为nullable
   - PackageUsage.ActivatedAt/ExpiresAt
   - PersonalCustomerDevice/ICCID/Phone.LastUsedAt/VerifiedAt

2. 数据库迁移:
   - activated_at/expires_at列移除NOT NULL约束
   - 清洗零值记录(status=0且activated_at<'2000-01-01')

3. 新增ActivateSpecificPackage方法:精准激活指定套餐,
   修复HandlePackageQueueActivation从"查找过期包"改为直接激活payload指定套餐

4. 新增孤儿套餐恢复扫描:Worker启动或每次套餐检查时,
   自动发现并恢复无status=1主套餐的孤儿载体

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-04-16 11:14:39 +08:00

3.5 KiB
Raw Blame History

Spec Delta: 主套餐排队生效机制(激活链修复 + 孤儿恢复)

MODIFIED Requirements

Requirement: 排队激活任务精准激活指定套餐

HandlePackageQueueActivation Asynq 任务处理器 SHALL 直接激活 payload 中 PackageUsageID 指定的具体套餐,不得重新执行"查找过期包"的发现流程。

ActivationService SHALL 提供 ActivateSpecificPackage(ctx, packageUsageID uint) error 方法,职责为:加载指定套餐 → 幂等检查 → 计算激活时间和过期时间 → 事务内更新 status=1 → 同步载体状态 → 触发复机回调。

激活前 SHALL 获取 RedisPackageActivationLockKey(carrierType, carrierID) 分布式锁30 秒超时),防止并发激活同一载体。

Scenario: Asynq 任务激活指定的排队套餐

  • WHEN HandlePackageQueueActivation 接收到 payload {PackageUsageID: 2, CarrierType: "device", CarrierID: 1}
  • THEN 系统加载 id=2 的 PackageUsage 记录
  • AND 若该记录 status=0待生效则将其激活status → 1activated_at → nowexpires_at → 计算值)
  • AND 激活成功后更新关联载体状态

Scenario: 幂等保护:已激活的套餐不重复处理

  • WHEN HandlePackageQueueActivation 接收到的套餐 id 对应的套餐已是 status=1生效中
  • THEN 系统直接返回成功,不做任何修改
  • AND 不产生重复激活的副作用

Scenario: 激活时正确计算到期时间

  • WHEN ActivateSpecificPackage 激活一个排队的主套餐
  • THEN activated_at 为激活时刻(time.Now()
  • AND expires_at 根据关联 Package 的 CalendarTypenatural_monthby_day)、DurationMonthsDurationDays 正确计算
  • ANDDataResetCycle != "none",则同步更新 next_reset_at

ADDED Requirements

Requirement: 孤儿待激活套餐自动恢复

系统 SHALL 在每次 HandlePackageActivationCheck 执行时(每 10 秒)检测孤儿载体,即:存在 status=0 AND master_usage_id IS NULL AND pending_realname_activation=false 的主套餐,但该载体不存在任何 status=1 的主套餐。

检测到孤儿后,系统 SHALL 为每个孤儿载体提交 TaskTypePackageQueueActivation Asynq 任务,激活其 priority 最小的待生效主套餐。单次扫描处理上限 SHALL 为 100 个载体。

此机制 SHALL 覆盖以下两种场景:

  1. Worker 服务重启后,重启前已标记过期但 Asynq 任务丢失的载体
  2. Asynq 任务 MaxRetry 耗尽后进入 dead queue 的载体

Scenario: 重启后孤儿套餐在 10 秒内被自动恢复

  • WHEN Worker 服务重启数据库中存在载体device_id=1无 status=1 主套餐、但有 status=0 且 pending_realname_activation=false 的主套餐
  • THEN Worker 启动后的首次 HandlePackageActivationCheck(最长 10 秒内)检测到该孤儿载体
  • AND 提交激活任务
  • AND 待生效套餐在 20 秒内完成激活status → 1

Scenario: Asynq 任务失败后孤儿机制兜底

  • WHEN 激活任务因 Asynq MaxRetry 耗尽进入 dead queue载体套餐仍为 status=0
  • THEN 孤儿扫描在下一个 10 秒周期内检测到该载体
  • AND 重新提交激活任务
  • AND 套餐最终完成激活

Scenario: 有生效套餐的载体不被孤儿扫描触发

  • WHEN 载体已有 status=1 的主套餐,同时存在 status=0 的排队套餐
  • THEN 孤儿扫描不为该载体提交激活任务
  • AND 排队套餐继续等待当前套餐过期后的正常激活流程