Files
junhong_cmp_fiber/openspec/changes/archive/2026-01-27-add-package-module/design.md
huang 79c061b6fa
All checks were successful
构建并部署到测试环境(无 SSH) / build-and-deploy (push) Successful in 5m24s
feat: 实现套餐管理模块,包含套餐系列、双状态管理、废弃模型清理
- 新增套餐系列管理 (CRUD + 状态切换)
- 新增套餐管理 (CRUD + 启用/禁用 + 上架/下架双状态)
- 清理 8 个废弃分佣模型及对应数据库表
- Package 模型新增建议成本价、建议售价、上架状态字段
- 完整的 Store/Service/Handler 三层实现
- 包含单元测试和集成测试
- 归档 add-package-module change
- 新增多个 OpenSpec changes (订单支付、店铺套餐分配、一次性分佣、卡设备系列绑定)
2026-01-27 19:55:47 +08:00

6.3 KiB
Raw Blame History

Context

当前系统中存在大量为号卡业务设计的分佣模型(冻结/解冻、组合分佣、运营商结算等),但流量卡业务只需要简单的一次性佣金机制。这些模型增加了代码复杂度且从未使用。

现有套餐模型 Package 缺少建议价格字段和上架状态管理,无法支持后续的代理套餐分配功能。

当前代码结构

  • Handler 在 internal/handler/admin/ 下,每个模块一个文件
  • Service 在 internal/service/{module}/service.go,每个模块一个包
  • Store 在 internal/store/postgres/{module}_store.go
  • Bootstrap 在 internal/bootstrap/ 负责组件注册

Goals / Non-Goals

Goals:

  • 清理 8 个废弃模型,减少代码复杂度
  • 扩展 Package 模型支持建议价格和上架状态
  • 提供完整的套餐系列 CRUD API
  • 提供完整的套餐 CRUD API含双状态管理
  • 遵循现有代码架构风格

Non-Goals:

  • 不实现代理套餐分配Phase 2
  • 不实现一次性佣金计算Phase 5
  • 不迁移现有数据(表内无数据)
  • 不修改 CommissionRecord 模型(后续 Phase 简化)

Decisions

1. 模型文件处理策略

决策:直接删除废弃模型定义,不保留注释或空文件

理由

  • 这些模型从未在生产环境使用
  • Git 历史可追溯
  • 保留空定义增加维护负担

替代方案

  • 标记为 deprecated 保留:增加代码噪音
  • 移到 archive 目录:过度设计

2. Package 模型字段设计

决策:新增三个字段

type Package struct {
    // ... 现有字段 ...
    SuggestedCostPrice   int64 `gorm:"column:suggested_cost_price;type:bigint;default:0;comment:建议成本价(分为单位)" json:"suggested_cost_price"`
    SuggestedRetailPrice int64 `gorm:"column:suggested_retail_price;type:bigint;default:0;comment:建议售价(分为单位)" json:"suggested_retail_price"`
    ShelfStatus          int   `gorm:"column:shelf_status;type:int;default:2;not null;comment:上架状态 1-上架 2-下架" json:"shelf_status"`
}

理由

  • suggested_cost_price:平台定义的建议成本价,代理分配时参考
  • suggested_retail_price:平台定义的建议零售价,代理设置售价时参考
  • shelf_status:与 status(启用/禁用)分离,支持独立的上架控制
  • 默认 shelf_status=2(下架):新套餐需要显式上架

替代方案

  • 用 JSON 字段存储扩展属性:查询不便,类型不安全
  • 合并 status 和 shelf_status语义不同分开更清晰

3. 双状态业务规则

决策启用状态status和上架状态shelf_status独立但有约束

status shelf_status 允许操作
启用(1) 上架(1) 可购买
启用(1) 下架(2) 不可购买,可上架
禁用(2) 上架(1) 禁止 - 禁用时强制下架
禁用(2) 下架(2) 不可购买,需先启用再上架

理由

  • 禁用套餐不应该可购买,强制下架保证数据一致性
  • 启用但下架:允许平台配置套餐但暂不开放购买

4. API 路由设计

决策:使用 RESTful 风格,状态变更使用 PATCH

# 套餐系列
POST   /api/admin/package-series          创建
GET    /api/admin/package-series          列表
GET    /api/admin/package-series/:id      详情
PUT    /api/admin/package-series/:id      更新
DELETE /api/admin/package-series/:id      删除
PATCH  /api/admin/package-series/:id/status  启用/禁用

# 套餐
POST   /api/admin/packages                创建
GET    /api/admin/packages                列表
GET    /api/admin/packages/:id            详情
PUT    /api/admin/packages/:id            更新
DELETE /api/admin/packages/:id            删除
PATCH  /api/admin/packages/:id/status     启用/禁用
PATCH  /api/admin/packages/:id/shelf      上架/下架

理由

  • 与现有 API 风格一致(参考 /api/admin/carriers
  • 状态变更使用 PATCH 符合 HTTP 语义
  • 路径清晰,易于前端对接

5. Service 层设计

决策:每个模块独立 Service 包

internal/service/package_series/service.go  # 套餐系列 Service
internal/service/package/service.go         # 套餐 Service

理由

  • 与现有架构一致carrier, iot_card 等)
  • 便于后续扩展(如套餐关联其他模块)

6. 数据库迁移策略

决策:单个迁移文件,先删后改

迁移顺序:

  1. DROP 8 个废弃表
  2. ALTER tb_package 添加 3 个新字段

理由

  • 这些表无生产数据,可直接删除
  • 单文件便于回滚

Risks / Trade-offs

风险 1删除模型后发现有隐藏引用

风险:代码中可能有对废弃模型的隐藏引用导致编译失败

缓解

  • 删除模型后执行 go build ./... 确认编译通过
  • 使用 IDE 全局搜索确认无引用

风险 2双状态逻辑复杂度

风险:禁用时强制下架的逻辑可能被遗漏

缓解

  • 在 Service 层统一处理状态变更逻辑
  • 添加单元测试覆盖所有状态组合

风险 3API 命名与现有冲突

风险/packages 路径可能与未来其他套餐类型冲突

缓解

  • 当前只有流量卡套餐,命名合理
  • 未来如有号卡套餐,可使用 /number-card-packages

Migration Plan

部署步骤

  1. 代码部署前

    • 确认生产环境废弃表无数据
    • 备份数据库(预防措施)
  2. 执行迁移

    go run cmd/migrate/main.go up
    
  3. 验证

    • 确认 8 个表已删除
    • 确认 tb_package 新增 3 个字段
    • API 健康检查

回滚策略

go run cmd/migrate/main.go down

迁移 down 脚本:

  • 重建 8 个废弃表(结构保留)
  • 删除 tb_package 的 3 个新字段

注意:回滚不恢复数据,仅恢复表结构

Open Questions

  1. 套餐系列禁用是否级联影响套餐?

    • 当前设计:不级联,套餐系列禁用只影响系列本身
    • 待确认:是否需要禁用系列时自动禁用下属套餐?
  2. 删除套餐/套餐系列的约束?

    • 当前设计物理删除soft delete via GORM
    • 待确认:是否需要检查关联数据(如已分配给代理的套餐)?
    • 建议Phase 2 实现代理分配后再添加约束检查