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

50 lines
5.6 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.
## 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"`