Files
junhong_cmp_fiber/openspec/changes/archive/2026-02-12-package-system-upgrade/tasks.md
huang c665f32976
All checks were successful
构建并部署到测试环境(无 SSH) / build-and-deploy (push) Successful in 6m54s
feat: 套餐系统升级 - Worker 重构、流量重置、文档与规范更新
- 重构 Worker 启动流程,引入 bootstrap 模块统一管理依赖注入
- 实现套餐流量重置服务(日/月/年周期重置)
- 新增套餐激活排队、加油包绑定、囤货待实名激活逻辑
- 新增订单创建幂等性防重(Redis 业务键 + 分布式锁)
- 更新 AGENTS.md/CLAUDE.md:新增注释规范、幂等性规范,移除测试要求
- 添加套餐系统升级完整文档(API文档、使用指南、功能总结、运维指南)
- 归档 OpenSpec package-system-upgrade 变更,同步 specs 到主目录
- 新增 queue types 抽象和 Redis 常量定义
2026-02-12 14:24:15 +08:00

254 lines
17 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 创建数据库迁移文件(`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 DTOmain_package, addon_packages, total 字段)
- [x] 4.5 创建 PackageUsageDailyRecordResponse DTOdate, daily_usage_mb, cumulative_usage_mb 字段)
- [x] 4.6 创建 PackageUsageDetailResponse DTOpackage_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 创建 PackageUsageDailyRecordStoreNewPackageUsageDailyRecordStore 构造函数)
- [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 路由组注册客户视图 APIGET /api/h5/packages/my-usage
- [x] 17.3 在 admin 路由组注册套餐流量详单 APIGET /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 注册任务 HandlerTaskTypePackageFirstActivation → 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 注册任务 HandlerTaskTypePackageQueueActivation → 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 编写回滚预案(代码回滚、数据库回滚、数据修复脚本)