# 实施任务清单: 套餐系统升级

## 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 编写回滚预案（代码回滚、数据库回滚、数据修复脚本）
