Files
junhong_cmp_fiber/openspec/changes/archive/2026-04-16-fix-package-activation-and-time-fields/proposal.md
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

39 lines
3.5 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.
## Why
套餐激活系统存在两类根本性缺陷:一是多个 Model 的时间字段定义为非指针 `time.Time`,导致"待生效"状态下零值(`0001-01-01 00:00:43`)被写入数据库,污染历史数据并造成查询误判;二是排队激活的核心执行链存在逻辑断链——`HandlePackageQueueActivation` 调用的 `ActivateQueuedPackage(carrierType, carrierID)` 方法设计为"发现过期包 → 标记过期 → 激活下一个"的完整循环,但轮询处理器(`HandlePackageActivationCheck`)已提前完成了"标记过期"步骤。当 Asynq 任务执行时,过期触发条件已消失,`ActivateQueuedPackage` 空转返回payload 中携带的 `PackageUsageID` 未被使用,导致所有排队中的套餐永久卡在 `status=0`,且系统对重启后的孤儿 pending 套餐完全无感知。当前已有至少 4 条套餐使用记录受影响TEST5G 设备 3 条 + 其他 6 条被零值污染)。
## What Changes
- **Model 层**:将 `PackageUsage.ActivatedAt``ExpiresAt` 改为 `*time.Time`(需同步数据库迁移去除 NOT NULL 约束);将 `PersonalCustomerDevice.LastUsedAt``PersonalCustomerICCID.LastUsedAt``PersonalCustomerPhone.VerifiedAt` 改为 `*time.Time`DB 列已 nullable无需迁移
- **数据修复**:将现有 9 条 `tb_package_usage``activated_at/expires_at = '0001-01-01'` 的记录更新为 `NULL`
- **激活逻辑修复**:在 `ActivationService` 新增 `ActivateSpecificPackage(packageUsageID)` 方法,`HandlePackageQueueActivation` 改为直接调用该方法激活 payload 指定的具体套餐,而非重跑"查找过期包"流程
- **孤儿恢复机制**`HandlePackageActivationCheck`(每 10 秒)新增孤儿扫描:检测载体无 `status=1` 包但存在 `status=0 AND pending_realname_activation=false` 包的情形,直接触发激活任务,同时覆盖重启恢复和 Asynq 任务丢失两种场景
## Capabilities
### New Capabilities
- `time-field-nullable-fix`:将语义上可为空的时间字段从 `time.Time` 改为 `*time.Time`,防止 GORM 将零值写入数据库;包含 Model 修改、数据库迁移、历史数据清洗、赋值代码修正(共 5 个字段,跨 4 个 Model
### Modified Capabilities
- `package-queue-activation`:修复排队激活执行链断链问题,新增孤儿套餐恢复机制;原 spec 定义的"主套餐过期后 20 秒内激活下一个"的 SLA 要求将真正得到保障,同时新增系统重启后的自动恢复保证
## Impact
**受影响文件:**
- `internal/model/package.go``PackageUsage` 模型
- `internal/model/personal_customer_device.go` — 个人客户设备绑定模型
- `internal/model/personal_customer_iccid.go` — 个人客户 ICCID 绑定模型
- `internal/model/personal_customer_phone.go` — 个人客户手机验证模型
- `internal/service/package/activation_service.go` — 新增 `ActivateSpecificPackage` 方法
- `internal/polling/package_activation_handler.go` — 修改 `HandlePackageQueueActivation` 逻辑 + 新增孤儿扫描
- `internal/service/order/service.go` — 修正零值赋值
- `internal/task/auto_purchase.go` — 修正零值赋值(同类问题)
- 数据库迁移文件(新增)— `tb_package_usage.activated_at/expires_at` DROP NOT NULL
**下游影响:**
- 所有读取 `PackageUsage.ActivatedAt/ExpiresAt` 的代码需适配指针类型nil 判断)
- 现有 9 条脏数据将被修复为 NULL
- 排队中的 TEST5G 套餐id=2,3,6将在修复部署后自动被孤儿扫描激活