All checks were successful
构建并部署到测试环境(无 SSH) / build-and-deploy (push) Successful in 6m54s
- 重构 Worker 启动流程,引入 bootstrap 模块统一管理依赖注入 - 实现套餐流量重置服务(日/月/年周期重置) - 新增套餐激活排队、加油包绑定、囤货待实名激活逻辑 - 新增订单创建幂等性防重(Redis 业务键 + 分布式锁) - 更新 AGENTS.md/CLAUDE.md:新增注释规范、幂等性规范,移除测试要求 - 添加套餐系统升级完整文档(API文档、使用指南、功能总结、运维指南) - 归档 OpenSpec package-system-upgrade 变更,同步 specs 到主目录 - 新增 queue types 抽象和 Redis 常量定义
254 lines
17 KiB
Markdown
254 lines
17 KiB
Markdown
# 实施任务清单: 套餐系统升级
|
||
|
||
## 1. 数据库迁移
|
||
|
||
- [x] 1.1 创建数据库迁移文件(`make create-migration name=package_system_upgrade`)
|
||
- [x] 1.2 编写 Package 表扩展迁移(新增 3 个字段:calendar_type, data_reset_cycle, enable_realname_activation)
|
||
- [x] 1.3 编写 PackageUsage 表扩展迁移(扩展 status 枚举 0-4,新增 7 个字段:priority, master_usage_id, has_independent_expiry, pending_realname_activation, data_reset_cycle, last_reset_at, next_reset_at)
|
||
- [x] 1.4 编写 IotCard 表扩展迁移(新增 3 个字段:first_realname_at, stopped_at, resumed_at, stop_reason)
|
||
- [x] 1.5 编写 Carrier 表扩展迁移(新增 1 个字段:billing_day)
|
||
- [x] 1.6 创建 PackageUsageDailyRecord 表迁移(package_usage_id, date, daily_usage_mb, cumulative_usage_mb)
|
||
- [x] 1.7 创建 CardDailyUsage 表迁移(card_id, usage_date, total_data_usage, carrier_id)
|
||
- [x] 1.8 创建索引迁移(priority, master_usage_id, package_usage_id+date 联合唯一索引, card_id+usage_date 联合唯一索引)
|
||
- [x] 1.9 执行迁移(`make migrate-up`),验证表结构正确
|
||
- [x] 1.10 数据初始化:运营商 billing_day 字段(联通=27,其他=1)
|
||
- [x] 1.11 编写回滚迁移脚本(删除新表、删除新字段)
|
||
|
||
## 2. 常量定义
|
||
|
||
- [x] 2.1 在 pkg/constants/constants.go 新增套餐周期类型常量(PackageCalendarTypeNaturalMonth, PackageCalendarTypeByDay)
|
||
- [x] 2.2 新增套餐流量重置周期常量(PackageDataResetDaily, PackageDataResetMonthly, PackageDataResetYearly, PackageDataResetNone)
|
||
- [x] 2.3 新增套餐使用状态常量(PackageUsageStatusPending=0, PackageUsageStatusActive=1, PackageUsageStatusDepleted=2, PackageUsageStatusExpired=3, PackageUsageStatusInvalidated=4)
|
||
- [x] 2.4 新增任务类型常量(TaskTypePackageFirstActivation, TaskTypePackageQueueActivation, TaskTypePackageDataReset)
|
||
- [x] 2.5 新增 Redis 键函数(RedisPackageActivationLockKey)
|
||
- [x] 2.6 运行 lsp_diagnostics 验证编译通过
|
||
|
||
## 3. Model 层扩展
|
||
|
||
- [x] 3.1 扩展 Package 模型(新增 CalendarType, DataResetCycle, EnableRealnameActivation, DurationDays 字段)
|
||
- [x] 3.2 扩展 PackageUsage 模型(扩展 Status 注释,新增 Priority, MasterUsageID, HasIndependentExpiry, PendingRealnameActivation, DataResetCycle, LastResetAt, NextResetAt 字段)
|
||
- [x] 3.3 创建 PackageUsageDailyRecord 模型(PackageUsageID, Date, DailyUsageMB, CumulativeUsageMB)
|
||
- [x] 3.4 实现 PackageUsageDailyRecord.TableName() 方法
|
||
- [x] 3.5 运行 lsp_diagnostics 验证编译通过
|
||
|
||
## 4. DTO 扩展
|
||
|
||
- [x] 4.1 扩展 CreatePackageRequest DTO(新增 CalendarType, DurationDays, DataResetCycle, EnableRealnameActivation 字段,添加 description 标签和验证标签)
|
||
- [x] 4.2 扩展 UpdatePackageRequest DTO(新增 CalendarType, DurationDays, DataResetCycle, EnableRealnameActivation 字段)
|
||
- [x] 4.3 扩展 PackageResponse DTO(新增 CalendarType, DurationDays, DataResetCycle, EnableRealnameActivation 字段)
|
||
- [x] 4.4 创建 PackageUsageCustomerViewResponse DTO(main_package, addon_packages, total 字段)
|
||
- [x] 4.5 创建 PackageUsageDailyRecordResponse DTO(date, daily_usage_mb, cumulative_usage_mb 字段)
|
||
- [x] 4.6 创建 PackageUsageDetailResponse DTO(package_usage_id, package_name, records, total_usage_mb 字段)
|
||
- [x] 4.7 运行 lsp_diagnostics 验证编译通过
|
||
|
||
## 5. Store 层扩展
|
||
|
||
- [x] 5.1 扩展 PackageStore.Create 方法支持新字段(calendar_type, data_reset_cycle, enable_realname_activation)
|
||
- [x] 5.2 扩展 PackageStore.Update 方法支持新字段
|
||
- [x] 5.3 扩展 PackageStore.GetByID 查询返回新字段
|
||
- [x] 5.4 扩展 PackageUsageStore.Create 方法支持新字段(priority, master_usage_id, has_independent_expiry, pending_realname_activation, data_reset_cycle)
|
||
- [x] 5.5 新增 PackageUsageStore.GetActiveMainPackage 方法(查询生效中主套餐)
|
||
- [x] 5.6 新增 PackageUsageStore.GetNextPendingMainPackage 方法(查询下一个待生效主套餐,按 priority ASC)
|
||
- [x] 5.7 新增 PackageUsageStore.GetActivePackages 方法(查询生效中的主套餐和加油包,按优先级排序)
|
||
- [x] 5.8 新增 PackageUsageStore.GetAddonsByMasterID 方法(查询主套餐下的所有加油包)
|
||
- [x] 5.9 新增 PackageUsageStore.BatchUpdateStatus 方法(批量更新加油包状态)
|
||
- [x] 5.10 新增 PackageUsageStore.UpdateDataUsage 方法(更新套餐流量使用,支持事务)
|
||
- [x] 5.11 新增 PackageUsageStore.GetPackagesForReset 方法(查询需要重置的套餐,WHERE next_reset_at <= NOW)
|
||
- [x] 5.12 新增 PackageUsageStore.ResetDataUsage 方法(重置流量,更新 last_reset_at 和 next_reset_at)
|
||
- [x] 5.13 创建 PackageUsageDailyRecordStore(NewPackageUsageDailyRecordStore 构造函数)
|
||
- [x] 5.14 实现 PackageUsageDailyRecordStore.CreateOrUpdate 方法(创建或更新日记录,使用 UPSERT)
|
||
- [x] 5.15 实现 PackageUsageDailyRecordStore.GetByDateRange 方法(按日期范围查询)
|
||
|
||
## 6. 套餐有效期计算工具函数
|
||
|
||
- [x] 6.1 在 internal/service/package/utils.go 创建 CalculateExpiryTime 函数(根据 calendar_type 和 duration 计算过期时间)
|
||
- [x] 6.2 实现自然月套餐过期时间计算(activated_at 月份 + N 个月,月末 23:59:59)
|
||
- [x] 6.3 实现按天套餐过期时间计算(activated_at + N 天,23:59:59)
|
||
- [x] 6.4 在 internal/service/package/utils.go 创建 CalculateNextResetTime 函数(根据 data_reset_cycle 计算下次重置时间)
|
||
- [x] 6.5 实现日重置计算(明天 00:00:00)
|
||
- [x] 6.6 实现月重置计算(联通27号 vs 其他1号,下月 00:00:00)
|
||
- [x] 6.7 实现年重置计算(明年 1 月 1 日 00:00:00)
|
||
|
||
## 7. Package Service 改造
|
||
|
||
- [x] 7.1 扩展 PackageService.Create 方法支持新字段验证(calendar_type=natural_month 时必须提供 duration_months)
|
||
- [x] 7.2 扩展 PackageService.Update 方法支持新字段更新
|
||
- [x] 7.3 扩展 PackageService.GetByID 返回新字段
|
||
|
||
## 8. Order Service 改造(主套餐排队 + 加油包限制 + 混买限制)
|
||
|
||
- [x] 8.1 实现订单创建校验:禁止同订单混买正式套餐和加油包
|
||
- [x] 8.2 改造 OrderService.CreateOrder 方法,购买主套餐时检查是否有生效中主套餐
|
||
- [x] 8.3 实现主套餐排队逻辑(有生效中主套餐时,新套餐 status=0, priority=MAX(priority)+1)
|
||
- [x] 8.4 实现首个主套餐立即激活逻辑(无生效中主套餐时,status=1, priority=1, 计算 activated_at 和 expires_at)
|
||
- [x] 8.5 改造 OrderService.CreateOrder 方法,购买加油包时检查是否有主套餐(status IN (0,1) 的主套餐)
|
||
- [x] 8.6 实现加油包购买限制(无主套餐时返回错误 "必须有主套餐才能购买加油包")
|
||
- [x] 8.7 实现加油包创建逻辑(master_usage_id=主套餐ID, status=1, priority=MAX(priority)+1, 根据 has_independent_expiry 计算 expires_at)
|
||
- [x] 8.8 实现客户端未实名购买限制(H5 端未实名时返回错误 403)
|
||
- [x] 8.9 实现后台囤货场景(enable_realname_activation=true 时,status=0, pending_realname_activation=true)
|
||
|
||
## 9. 套餐激活 Service(首次实名激活 + 排队激活)
|
||
|
||
- [x] 9.1 创建 internal/service/package/activation_service.go
|
||
- [x] 9.2 实现 ActivationService.ActivateByRealname 方法(首次实名激活,查询 pending_realname_activation=true 的套餐)
|
||
- [x] 9.3 实现套餐激活逻辑(计算 activated_at 和 expires_at,更新 status=1, pending_realname_activation=false)
|
||
- [x] 9.4 实现 ActivationService.ActivateQueuedPackage 方法(主套餐排队激活,使用 Redis 分布式锁避免并发)
|
||
- [x] 9.5 实现过期主套餐检测逻辑(查询 status=1 AND expires_at <= NOW 的主套餐,更新 status=3)
|
||
- [x] 9.6 实现下一个待生效主套餐查询(WHERE status=0 AND master_usage_id IS NULL ORDER BY priority ASC LIMIT 1)
|
||
- [x] 9.7 实现加油包级联失效逻辑(查询主套餐下的所有加油包,批量更新 status=4)
|
||
|
||
## 10. 流量扣减优先级 Service
|
||
|
||
- [x] 10.1 创建 internal/service/package/usage_service.go
|
||
- [x] 10.2 实现 UsageService.DeductDataUsage 方法(按优先级扣减流量:加油包按 priority ASC → 主套餐)
|
||
- [x] 10.3 实现流量扣减逻辑(FOR EACH 套餐,计算剩余额度,扣减流量,更新 data_usage_mb)
|
||
- [x] 10.4 实现套餐流量用完标记(data_usage_mb >= data_limit_mb 时,更新 status=2)
|
||
- [x] 10.5 实现停机条件检查(所有套餐 status=2 时,触发停机操作)
|
||
- [x] 10.6 实现日记录写入(每次扣减后,创建或更新 PackageUsageDailyRecord)
|
||
|
||
## 11. 流量重置 Service
|
||
|
||
- [x] 11.1 创建 internal/service/package/reset_service.go
|
||
- [x] 11.2 实现 ResetService.ResetDailyUsage 方法(查询 data_reset_cycle=daily 且 next_reset_at <= NOW 的套餐)
|
||
- [x] 11.3 实现日重置逻辑(批量更新 data_usage_mb=0, last_reset_at=NOW, next_reset_at=明天 00:00:00)
|
||
- [x] 11.4 实现 ResetService.ResetMonthlyUsage 方法(查询 data_reset_cycle=monthly 且 next_reset_at <= NOW 的套餐)
|
||
- [x] 11.5 实现月重置逻辑(区分联通27号 vs 其他1号,批量更新流量和重置时间)
|
||
- [x] 11.6 实现 ResetService.ResetYearlyUsage 方法(查询 data_reset_cycle=yearly 且 next_reset_at <= NOW 的套餐)
|
||
- [x] 11.7 实现年重置逻辑(批量更新 data_usage_mb=0, last_reset_at=NOW, next_reset_at=明年 1月 1日)
|
||
- [x] 11.8 实现分批处理逻辑(每次最多处理 10000 条,避免长事务)
|
||
|
||
## 12. 客户视图流量查询 Service
|
||
|
||
- [x] 12.1 创建 internal/service/package/customer_view_service.go
|
||
- [x] 12.2 实现 CustomerViewService.GetMyUsage 方法(根据 user_id 获取载体信息)
|
||
- [x] 12.3 实现生效套餐查询逻辑(WHERE status IN (1,2),区分主套餐和加油包)
|
||
- [x] 12.4 实现总计流量计算逻辑(主套餐 + 所有加油包的 used_mb 和 total_mb)
|
||
- [x] 12.5 实现响应 DTO 组装(main_package, addon_packages, total)
|
||
|
||
## 13. 套餐流量详单 Service
|
||
|
||
- [x] 13.1 创建 internal/service/package/daily_record_service.go
|
||
- [x] 13.2 实现 DailyRecordService.GetDailyRecords 方法(查询 package_usage_id 的日记录)
|
||
- [x] 13.3 实现越权检查(使用 middleware.CanManageShop 或 middleware.CanManageEnterprise 验证权限)
|
||
- [x] 13.4 实现日记录查询逻辑(WHERE package_usage_id=? AND date BETWEEN ? AND ?,按 date ASC 排序)
|
||
- [x] 13.5 实现响应 DTO 组装(package_usage_id, package_name, records, total_usage_mb)
|
||
|
||
## 14. Handler 层改造(套餐管理 API)
|
||
|
||
- [x] 14.1 扩展 admin.PackageHandler.Create 方法支持新字段验证(calendar_type, data_reset_cycle, enable_realname_activation)
|
||
- [x] 14.2 扩展 admin.PackageHandler.Update 方法支持新字段更新
|
||
- [x] 14.3 扩展 admin.PackageHandler.GetByID 返回新字段
|
||
|
||
## 15. Handler 层改造(客户视图 API)
|
||
|
||
- [x] 15.1 创建 internal/handler/h5/package_usage.go
|
||
- [x] 15.2 实现 PackageUsageHandler.GetMyUsage 方法(GET /api/h5/packages/my-usage)
|
||
- [x] 15.3 实现 JWT 认证和用户信息提取(从 context 获取 user_id 和载体信息)
|
||
- [x] 15.4 调用 CustomerViewService.GetMyUsage 获取流量数据
|
||
- [x] 15.5 返回 PackageUsageCustomerViewResponse 响应
|
||
|
||
## 16. Handler 层改造(套餐流量详单 API)
|
||
|
||
- [x] 16.1 扩展 admin.PackageUsageHandler(或创建新 Handler)
|
||
- [x] 16.2 实现 PackageUsageHandler.GetDailyRecords 方法(GET /api/admin/package-usage/:id/daily-records)
|
||
- [x] 16.3 实现参数验证(start_date, end_date 查询参数)
|
||
- [x] 16.4 调用 DailyRecordService.GetDailyRecords 获取日记录
|
||
- [x] 16.5 返回 PackageUsageDetailResponse 响应
|
||
|
||
## 17. 路由注册和文档生成器更新
|
||
|
||
- [x] 17.1 在 admin 路由组注册套餐管理 API(支持新字段的 POST/PUT/GET)
|
||
- [x] 17.2 在 h5 路由组注册客户视图 API(GET /api/h5/packages/my-usage)
|
||
- [x] 17.3 在 admin 路由组注册套餐流量详单 API(GET /api/admin/package-usage/:id/daily-records)
|
||
- [x] 17.4 更新 cmd/api/docs.go 文档生成器(添加新 Handler 到 handlers 结构体)
|
||
- [x] 17.5 更新 cmd/gendocs/main.go 文档生成器(添加新 Handler 到 handlers 结构体)
|
||
- [x] 17.6 运行 `make docs` 生成 OpenAPI 文档,验证新 API 出现在文档中
|
||
|
||
## 18. 轮询系统扩展(流量检查任务)
|
||
|
||
- [x] 18.1 扩展 internal/polling/carddata_handler.go
|
||
- [x] 18.2 改造 HandleCarddataCheck 方法支持流量扣减优先级(查询生效套餐,按优先级排序)
|
||
- [x] 18.3 实现流量扣减逻辑(调用 UsageService.DeductDataUsage 方法)
|
||
- [x] 18.4 改造停机条件检查(所有套餐流量用完才触发停机)
|
||
|
||
## 19. 轮询系统扩展(套餐激活检查任务)
|
||
|
||
- [x] 19.1 创建 internal/polling/package_activation_handler.go
|
||
- [x] 19.2 实现 HandlePackageActivation 方法(查询已过期主套餐,status=1 AND expires_at <= NOW)
|
||
- [x] 19.3 实现过期主套餐状态更新(更新 status=3)
|
||
- [x] 19.4 实现加油包级联失效(调用 ActivationService.CascadeInvalidateAddons)
|
||
- [x] 19.5 实现下一个待生效主套餐查询和激活(提交 Asynq 任务 TaskTypePackageQueueActivation)
|
||
- [x] 19.6 在 Scheduler.scheduleLoop 中注册套餐激活检查任务(每 10 秒调度一次)
|
||
|
||
## 20. 轮询系统扩展(流量重置调度任务)
|
||
|
||
- [x] 20.1 创建 internal/polling/data_reset_handler.go
|
||
- [x] 20.2 实现 HandleDataReset 方法(每 10 秒调度一次,检查需要重置的套餐)
|
||
- [x] 20.3 实现日重置调度(调用 ResetService.ResetDailyUsage)
|
||
- [x] 20.4 实现月重置调度(调用 ResetService.ResetMonthlyUsage)
|
||
- [x] 20.5 实现年重置调度(调用 ResetService.ResetYearlyUsage)
|
||
- [x] 20.6 在 Scheduler.scheduleLoop 中注册流量重置调度任务
|
||
|
||
## 21. 轮询系统扩展(首次实名激活触发)
|
||
|
||
- [x] 21.1 扩展 internal/task/polling_handler.go(实名检查部分)
|
||
- [x] 21.2 改造 HandleRealnameCheck 方法,检测到首次实名时(realname_status: 0/1 → 2)
|
||
- [x] 21.3 查询该卡/设备是否有待激活套餐(WHERE pending_realname_activation=true AND status=0)
|
||
- [x] 21.4 提交 Asynq 任务(TaskTypePackageFirstActivation)
|
||
|
||
## 22. Asynq Handler(首次实名激活任务)
|
||
|
||
- [x] 22.1 在 internal/polling/package_activation_handler.go 添加 HandlePackageFirstActivation 方法
|
||
- [x] 22.2 实现 HandlePackageFirstActivation 方法(解析任务 payload)
|
||
- [x] 22.3 调用 ActivationService.ActivateByRealname 激活套餐
|
||
- [x] 22.4 实现幂等性保证(任务处理前检查 pending_realname_activation=false)
|
||
- [x] 22.5 实现重试策略(MaxRetry(3), Timeout(30s))
|
||
- [x] 22.6 在 pkg/queue/handler.go 注册任务 Handler(TaskTypePackageFirstActivation → HandlePackageFirstActivation)
|
||
|
||
## 23. Asynq Handler(主套餐排队激活任务)
|
||
|
||
- [x] 23.1 在 internal/polling/package_activation_handler.go 添加 HandlePackageQueueActivation 方法
|
||
- [x] 23.2 实现 HandlePackageQueueActivation 方法(解析任务 payload)
|
||
- [x] 23.3 调用 ActivationService.ActivateQueuedPackage 激活套餐
|
||
- [x] 23.4 实现幂等性保证(任务处理前检查 status=1)
|
||
- [x] 23.5 实现重试策略(MaxRetry(3), Timeout(30s))
|
||
- [x] 23.6 在 pkg/queue/handler.go 注册任务 Handler(TaskTypePackageQueueActivation → HandlePackageQueueActivation)
|
||
|
||
## 24. 自动停复机功能(新增章节)
|
||
|
||
- [x] 24.1 扩展 IotCard Model(新增 stopped_at, resumed_at, stop_reason 字段)
|
||
- [x] 24.2 创建 internal/service/iot_card/stop_resume_service.go
|
||
- [x] 24.3 实现 CheckAndStopCard 方法(检查流量耗尽并停机)
|
||
- [x] 24.4 实现 ResumeCardIfStopped 方法(购买套餐后自动复机)
|
||
- [x] 24.5 实现运营商停复机接口调用(带重试机制,最多3次)
|
||
- [x] 24.6 在流量扣减Service中集成停机检查
|
||
- [x] 24.7 在套餐激活Service中集成复机触发
|
||
|
||
## 25. 错误码扩展
|
||
|
||
- [x] 25.1 在 pkg/errors/codes.go 新增错误码(CodePackageActivationConflict - 套餐正在激活中)
|
||
- [x] 25.2 新增错误码(CodeNoMainPackage - 必须有主套餐才能购买加油包)
|
||
- [x] 25.3 新增错误码(CodeRealnameRequired - 设备/卡必须先完成实名认证才能购买套餐)
|
||
- [x] 25.4 新增错误码(CodeMixedOrderForbidden - 同订单不能同时购买正式套餐和加油包)
|
||
- [x] 25.5 运行 lsp_diagnostics 验证编译通过
|
||
|
||
## 26. 最终检查
|
||
|
||
- [x] 26.1 运行 lsp_diagnostics,确认无编译错误和类型错误
|
||
- [x] 26.2 生成 OpenAPI 文档,确认新 API 出现在文档中
|
||
- [x] 26.3 代码审查(检查是否遵循分层架构、Go 惯用法、性能要求)
|
||
|
||
## 27. 文档更新
|
||
|
||
- [x] 27.1 更新 README.md(新增套餐系统升级功能说明)
|
||
- [x] 27.2 在 docs/package-system-upgrade/ 创建功能总结文档
|
||
- [x] 27.3 编写套餐系统升级用户指南(囤货、排队、加油包、流量查询)
|
||
- [x] 27.4 更新 API 文档(新增 API 端点和字段说明)
|
||
|
||
## 28. 部署准备
|
||
|
||
- [x] 28.1 编写数据库迁移回滚脚本
|
||
- [x] 28.2 配置监控指标(Asynq 队列长度、套餐激活延迟、API 响应时间)
|
||
- [x] 28.3 配置告警规则(套餐激活延迟 > 1 分钟、队列堆积 > 1000 个任务)
|
||
- [x] 28.4 编写回滚预案(代码回滚、数据库回滚、数据修复脚本)
|