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

5.6 KiB
Raw Blame History

1. 数据库迁移与历史数据清洗

  • 1.1 创建迁移文件,清洗 tb_package_usagestatus=0activated_at < '2000-01-01' 的零值记录(将 activated_atexpires_at 更新为 NULL
  • 1.2 创建迁移文件,对 tb_package_usage.activated_at 执行 ALTER COLUMN ... DROP NOT NULL
  • 1.3 创建迁移文件,对 tb_package_usage.expires_at 执行 ALTER COLUMN ... DROP NOT NULL
  • 1.4 在本地执行迁移,用 PostgreSQL MCP 验证:查询 tb_package_usage 确认列已 nullable、零值记录已清洗应为 9 条)

2. Model 层修改

  • 2.1 修改 internal/model/package.goPackageUsage.ActivatedAt 改为 *time.Time,更新 gorm tag 去除 not null
  • 2.2 修改 internal/model/package.goPackageUsage.ExpiresAt 改为 *time.Time,更新 gorm tag 去除 not null
  • 2.3 修改 internal/model/personal_customer_device.goLastUsedAt 改为 *time.Time
  • 2.4 修改 internal/model/personal_customer_iccid.goLastUsedAt 改为 *time.Time
  • 2.5 修改 internal/model/personal_customer_phone.goVerifiedAt 改为 *time.Time
  • 2.6 执行 go build ./...修复所有因指针类型变更导致的编译错误nil 判断、解引用、比较等)
  • 2.7 检查并修复受影响的 DTO 文件中对 ActivatedAt/ExpiresAt/LastUsedAt/VerifiedAt 的引用:internal/model/dto/package_dto.goPackageUsageItemResponse.ExpiresAt/ActivatedAtstring 类型,其映射赋值代码需增加 nil 判断nil 时输出空字符串或 "-"asset_dto.goclient_asset_dto.godevice_dto.goiot_card_dto.go 中对应字段已是 *time.Time,确认无需改动

3. 赋值逻辑修正

  • 3.1 修改 internal/service/order/service.goactivateMainPackage 函数待生效路径status=0activatedAt = time.Time{}expiresAt = time.Time{} 改为不赋值(保持 nil去除后续对 usage.ActivatedAt/ExpiresAt 的条件赋值中对零值的依赖
  • 3.2 修改 internal/task/auto_purchase.goactivateMainPackage 函数):同上,确保待生效套餐的 ActivatedAt/ExpiresAt 在创建时为 nil
  • 3.3 检查 internal/service/package/activation_service.go 中所有对 ExpiresAt 的读取(如 mainPackage.ExpiresAt 传给加油包),确认指针解引用安全
  • 3.4 修复 internal/service/package/activation_service.goactivateNextMainPackage 函数的 ExpiryBase 处理:当前该函数直接使用 now 作为 activatedAt,与 ActivateByRealnamefrom_purchase 逻辑不一致;改为参照 ActivateByRealname 的实现,根据 pkg.ExpiryBase 判断激活时间基准("from_purchase" 时用 usage.CreatedAt,否则用 time.Now()

4. 激活服务新增 ActivateSpecificPackage

  • 4.1 在 internal/service/package/activation_service.go 新增 ActivateSpecificPackage(ctx context.Context, packageUsageID uint) error 方法
  • 4.2 实现:加载 PackageUsage → 幂等检查status != Pending 直接返回 nil→ 获取分布式锁(RedisPackageActivationLockKey)→ 加载关联 Package → 根据 pkg.ExpiryBase 决定激活时间基准("from_purchase" 时用 usage.CreatedAt,否则用 time.Now();与 ActivateByRealname 保持一致)→ 计算 activatedAt/expiresAt/nextResetAt → 事务内 Updates → 调用 syncCarrierStatusActivated → 异步 resumeCallback.ResumeCardIfStopped
  • 4.3 确认分布式锁在函数退出时被 defer 释放

5. 修复 HandlePackageQueueActivation

  • 5.1 修改 internal/polling/package_activation_handler.go 中的 HandlePackageQueueActivation:将调用 h.activationService.ActivateQueuedPackage(ctx, payload.CarrierType, payload.CarrierID) 替换为调用 h.activationService.ActivateSpecificPackage(ctx, payload.PackageUsageID)
  • 5.2 修改 HandlePackageQueueActivationactivationService == nil 的降级分支:不删除该分支,改为记录 Error 日志("激活服务未注入,无法执行套餐激活")并返回错误,防止静默失败(原降级分支直接 Updates 缺少 expires_at 计算,逻辑不完整,改为报错更安全)
  • 5.3 用 PostgreSQL MCP 手动验证:触发一次过期检测(或手动将某套餐 expires_at 设为过去确认排队套餐被成功激活status: 0 → 1

6. 新增孤儿套餐恢复扫描

  • 6.1 在 internal/polling/package_activation_handler.go 新增 findAndActivateOrphanPackages(ctx context.Context) error 函数:查询无 status=1 主套餐但有 status=0pending_realname_activation=false)主套餐的载体,单次上限 100 个
  • 6.2 对每个孤儿载体,查询其 priority 最小的 status=0 主套餐,调用 h.enqueueActivationTask 提交激活任务
  • 6.3 在 HandlePackageActivationCheck 末尾追加调用 h.findAndActivateOrphanPackages(ctx),日志记录触发的孤儿数量
  • 6.4 用 PostgreSQL MCP 验证当前 TEST5G 设备device_id=1的 3 条待生效套餐id=2,3,6确认孤儿扫描触发后它们被逐步激活

7. 收尾验证

  • 7.1 执行 go build ./... 确认全量编译零错误
  • 7.2 用 PostgreSQL MCP 查询 tb_package_usage 验证:无 activated_at < '2000-01-01' 的记录
  • 7.3 用 PostgreSQL MCP 查询验证:personal_customer_device.last_used_atpersonal_customer_iccid.last_used_attb_personal_customer_phone.verified_at 中不存在 < '2000-01-01' 的值(如有,补一条清洗 SQL
  • 7.4 检查并更新 DTO 中所有展示 ActivatedAt/ExpiresAt 的字段,确认 nil 时的 JSON 输出为 null(而非 "0001-01-01T00:00:00Z"