All checks were successful
构建并部署到测试环境(无 SSH) / build-and-deploy (push) Successful in 7m33s
核心变更: 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>
50 lines
5.6 KiB
Markdown
50 lines
5.6 KiB
Markdown
## 1. 数据库迁移与历史数据清洗
|
||
|
||
- [x] 1.1 创建迁移文件,清洗 `tb_package_usage` 中 `status=0` 且 `activated_at < '2000-01-01'` 的零值记录(将 `activated_at` 和 `expires_at` 更新为 NULL)
|
||
- [x] 1.2 创建迁移文件,对 `tb_package_usage.activated_at` 执行 `ALTER COLUMN ... DROP NOT NULL`
|
||
- [x] 1.3 创建迁移文件,对 `tb_package_usage.expires_at` 执行 `ALTER COLUMN ... DROP NOT NULL`
|
||
- [x] 1.4 在本地执行迁移,用 PostgreSQL MCP 验证:查询 `tb_package_usage` 确认列已 nullable、零值记录已清洗(应为 9 条)
|
||
|
||
## 2. Model 层修改
|
||
|
||
- [x] 2.1 修改 `internal/model/package.go`:`PackageUsage.ActivatedAt` 改为 `*time.Time`,更新 gorm tag 去除 `not null`
|
||
- [x] 2.2 修改 `internal/model/package.go`:`PackageUsage.ExpiresAt` 改为 `*time.Time`,更新 gorm tag 去除 `not null`
|
||
- [x] 2.3 修改 `internal/model/personal_customer_device.go`:`LastUsedAt` 改为 `*time.Time`
|
||
- [x] 2.4 修改 `internal/model/personal_customer_iccid.go`:`LastUsedAt` 改为 `*time.Time`
|
||
- [x] 2.5 修改 `internal/model/personal_customer_phone.go`:`VerifiedAt` 改为 `*time.Time`
|
||
- [x] 2.6 执行 `go build ./...`,修复所有因指针类型变更导致的编译错误(nil 判断、解引用、比较等)
|
||
- [x] 2.7 检查并修复受影响的 DTO 文件中对 `ActivatedAt/ExpiresAt/LastUsedAt/VerifiedAt` 的引用:`internal/model/dto/package_dto.go`(`PackageUsageItemResponse.ExpiresAt/ActivatedAt` 为 `string` 类型,其映射赋值代码需增加 nil 判断,nil 时输出空字符串或 `"-"`);`asset_dto.go`、`client_asset_dto.go`、`device_dto.go`、`iot_card_dto.go` 中对应字段已是 `*time.Time`,确认无需改动
|
||
|
||
## 3. 赋值逻辑修正
|
||
|
||
- [x] 3.1 修改 `internal/service/order/service.go`(`activateMainPackage` 函数):待生效路径(status=0)中,将 `activatedAt = time.Time{}` 和 `expiresAt = time.Time{}` 改为不赋值(保持 nil),去除后续对 `usage.ActivatedAt/ExpiresAt` 的条件赋值中对零值的依赖
|
||
- [x] 3.2 修改 `internal/task/auto_purchase.go`(`activateMainPackage` 函数):同上,确保待生效套餐的 `ActivatedAt/ExpiresAt` 在创建时为 nil
|
||
- [x] 3.3 检查 `internal/service/package/activation_service.go` 中所有对 `ExpiresAt` 的读取(如 `mainPackage.ExpiresAt` 传给加油包),确认指针解引用安全
|
||
- [x] 3.4 修复 `internal/service/package/activation_service.go` 中 `activateNextMainPackage` 函数的 ExpiryBase 处理:当前该函数直接使用 `now` 作为 `activatedAt`,与 `ActivateByRealname` 的 `from_purchase` 逻辑不一致;改为参照 `ActivateByRealname` 的实现,根据 `pkg.ExpiryBase` 判断激活时间基准(`"from_purchase"` 时用 `usage.CreatedAt`,否则用 `time.Now()`)
|
||
|
||
## 4. 激活服务新增 ActivateSpecificPackage
|
||
|
||
- [x] 4.1 在 `internal/service/package/activation_service.go` 新增 `ActivateSpecificPackage(ctx context.Context, packageUsageID uint) error` 方法
|
||
- [x] 4.2 实现:加载 PackageUsage → 幂等检查(status != Pending 直接返回 nil)→ 获取分布式锁(`RedisPackageActivationLockKey`)→ 加载关联 Package → 根据 `pkg.ExpiryBase` 决定激活时间基准(`"from_purchase"` 时用 `usage.CreatedAt`,否则用 `time.Now()`;与 `ActivateByRealname` 保持一致)→ 计算 activatedAt/expiresAt/nextResetAt → 事务内 Updates → 调用 `syncCarrierStatusActivated` → 异步 `resumeCallback.ResumeCardIfStopped`
|
||
- [x] 4.3 确认分布式锁在函数退出时被 `defer` 释放
|
||
|
||
## 5. 修复 HandlePackageQueueActivation
|
||
|
||
- [x] 5.1 修改 `internal/polling/package_activation_handler.go` 中的 `HandlePackageQueueActivation`:将调用 `h.activationService.ActivateQueuedPackage(ctx, payload.CarrierType, payload.CarrierID)` 替换为调用 `h.activationService.ActivateSpecificPackage(ctx, payload.PackageUsageID)`
|
||
- [x] 5.2 修改 `HandlePackageQueueActivation` 中 `activationService == nil` 的降级分支:**不删除该分支**,改为记录 Error 日志(`"激活服务未注入,无法执行套餐激活"`)并返回错误,防止静默失败(原降级分支直接 Updates 缺少 expires_at 计算,逻辑不完整,改为报错更安全)
|
||
- [ ] 5.3 用 PostgreSQL MCP 手动验证:触发一次过期检测(或手动将某套餐 expires_at 设为过去),确认排队套餐被成功激活(status: 0 → 1)
|
||
|
||
## 6. 新增孤儿套餐恢复扫描
|
||
|
||
- [x] 6.1 在 `internal/polling/package_activation_handler.go` 新增 `findAndActivateOrphanPackages(ctx context.Context) error` 函数:查询无 status=1 主套餐但有 status=0(且 `pending_realname_activation=false`)主套餐的载体,单次上限 100 个
|
||
- [x] 6.2 对每个孤儿载体,查询其 priority 最小的 status=0 主套餐,调用 `h.enqueueActivationTask` 提交激活任务
|
||
- [x] 6.3 在 `HandlePackageActivationCheck` 末尾追加调用 `h.findAndActivateOrphanPackages(ctx)`,日志记录触发的孤儿数量
|
||
- [ ] 6.4 用 PostgreSQL MCP 验证当前 TEST5G 设备(device_id=1)的 3 条待生效套餐(id=2,3,6):确认孤儿扫描触发后它们被逐步激活
|
||
|
||
## 7. 收尾验证
|
||
|
||
- [x] 7.1 执行 `go build ./...` 确认全量编译零错误
|
||
- [x] 7.2 用 PostgreSQL MCP 查询 `tb_package_usage` 验证:无 `activated_at < '2000-01-01'` 的记录
|
||
- [x] 7.3 用 PostgreSQL MCP 查询验证:`personal_customer_device.last_used_at`、`personal_customer_iccid.last_used_at`、`tb_personal_customer_phone.verified_at` 中不存在 `< '2000-01-01'` 的值(如有,补一条清洗 SQL)
|
||
- [x] 7.4 检查并更新 DTO 中所有展示 `ActivatedAt/ExpiresAt` 的字段,确认 nil 时的 JSON 输出为 `null`(而非 `"0001-01-01T00:00:00Z"`)
|