Files
junhong_cmp_fiber/openspec/changes/archive/2026-01-28-add-card-device-series-bindng/design.md
huang a945a4f554
All checks were successful
构建并部署到测试环境(无 SSH) / build-and-deploy (push) Successful in 5m37s
feat: 实现卡和设备的套餐系列绑定功能
- 添加 Device 和 IotCard 模型的 SeriesID 字段
- 实现 DeviceService 和 IotCardService 的套餐系列绑定逻辑
- 添加 DeviceStore 和 IotCardStore 的数据库操作方法
- 更新 API 接口和路由支持套餐系列绑定
- 创建数据库迁移脚本(000027_add_series_binding_fields)
- 添加完整的单元测试和集成测试
- 更新 OpenAPI 文档
- 归档 OpenSpec 变更文档

Ultraworked with [Sisyphus](https://github.com/code-yeongyu/oh-my-opencode)

Co-authored-by: Sisyphus <clio-agent@sisyphuslabs.ai>
2026-01-28 19:49:45 +08:00

4.8 KiB
Raw Blame History

Context

Phase 2 完成了代理套餐分配机制,但卡和设备还没有关联到具体的套餐系列。本期在 IotCard 和 Device 模型上新增字段,记录其所属的套餐系列分配,为后续的套餐购买和佣金计算做准备。

关键业务规则

  • 卡/设备关联后才能购买该系列下的套餐
  • 设备关联后,其绑定的所有卡共享该套餐系列(设备级套餐)
  • 每张卡/设备只能触发一次一次性佣金

Goals / Non-Goals

Goals:

  • 在 IotCard 模型新增套餐系列关联和佣金状态字段
  • 在 Device 模型新增相同字段
  • 提供批量设置卡/设备套餐系列的 API
  • 验证关联的系列必须是当前店铺被分配的

Non-Goals:

  • 不实现订单支付Phase 4
  • 不实现佣金计算Phase 5
  • 不自动同步设备和卡的关联(手动设置)

Decisions

1. 新增字段设计

决策:在 IotCard 和 Device 模型各新增 3 个字段

// IotCard 新增字段
SeriesAllocationID    uint  `gorm:"column:series_allocation_id;index;comment:套餐系列分配ID" json:"series_allocation_id"`
FirstCommissionPaid   bool  `gorm:"column:first_commission_paid;default:false;comment:一次性佣金是否已发放" json:"first_commission_paid"`
AccumulatedRecharge   int64 `gorm:"column:accumulated_recharge;type:bigint;default:0;comment:累计充值金额(分)" json:"accumulated_recharge"`

// Device 新增字段(相同)
SeriesAllocationID    uint  `gorm:"column:series_allocation_id;index;comment:套餐系列分配ID" json:"series_allocation_id"`
FirstCommissionPaid   bool  `gorm:"column:first_commission_paid;default:false;comment:一次性佣金是否已发放" json:"first_commission_paid"`
AccumulatedRecharge   int64 `gorm:"column:accumulated_recharge;type:bigint;default:0;comment:累计充值金额(分)" json:"accumulated_recharge"`

理由

  • series_allocation_id:关联到 ShopSeriesAllocation决定可购买的套餐和一次性佣金配置
  • first_commission_paid:标记一次性佣金状态,防止重复发放(每张卡/设备仅发放一次)
  • accumulated_recharge累计充值金额用于累计充值触发条件判断trigger="accumulated_recharge"时使用)

一次性佣金触发流程

  1. 用户购买套餐并支付成功
  2. 系统检查 first_commission_paid 是否为 false未发放过
  3. 根据 OneTimeCommissionTrigger 判断触发条件:
    • single_recharge:检查本次充值金额是否 ≥ 阈值
    • accumulated_recharge:检查 accumulated_recharge + 本次充值 是否 ≥ 阈值
  4. 如果触发查询该系列分配的销售业绩ShopSeriesCommissionStats选择梯度档位
  5. 创建佣金记录并入账
  6. 标记 first_commission_paid = true

2. 设备与卡的关系

决策:设备和卡独立设置套餐系列

场景 1单卡销售
- IotCard.series_allocation_id 有值
- 购买套餐时使用卡的 series_allocation_id

场景 2设备销售整机出货
- Device.series_allocation_id 有值
- 设备下的卡可以不设置 series_allocation_id
- 购买套餐时优先使用 Device.series_allocation_id

理由

  • 单卡和设备是两种不同的销售模式
  • 设备级套餐购买时,所有卡共享流量
  • 佣金按设备计算,不按卡数倍增

3. 批量设置 API 设计

决策:使用 PATCH 方法批量更新

PATCH /api/admin/iot-cards/series-bindng
Body: { "iccids": ["xxx", "yyy"], "series_allocation_id": 123 }

PATCH /api/admin/devices/series-bindng
Body: { "device_ids": [1, 2, 3], "series_allocation_id": 123 }

理由

  • PATCH 语义合适(部分更新)
  • 支持批量操作提高效率
  • 通过 ICCID/设备 ID 定位资源

4. 权限验证

决策:只能关联当前店铺被分配的套餐系列

验证逻辑:

  1. 获取卡/设备的 shop_id
  2. 检查 series_allocation_id 对应的分配是否属于该店铺
  3. 检查分配状态是否启用

理由

  • 防止关联未被分配的系列
  • 确保数据一致性

Risks / Trade-offs

风险 1批量操作性能

风险:大批量设置时可能超时

缓解

  • 限制单次批量数量(如最多 500 条)
  • 使用批量更新 SQL 而非循环单条更新

风险 2设备和卡关联不一致

风险:设备设置了系列但卡没设置,或反过来

缓解

  • 购买套餐时明确优先级:设备级 > 卡级
  • 查询接口明确返回实际使用的系列来源

Open Questions

  1. 是否需要清除关联功能?

    • 当前设计:可以将 series_allocation_id 设为 0 清除关联
    • 待确认:清除后是否影响已购买的套餐?
  2. 设备和卡的 accumulated_recharge 如何同步?

    • 当前设计:设备级购买时更新 Device.accumulated_recharge
    • 单卡购买时更新 IotCard.accumulated_recharge
    • 待确认:是否需要双向同步?