feat: 实现套餐管理模块,包含套餐系列、双状态管理、废弃模型清理
All checks were successful
构建并部署到测试环境(无 SSH) / build-and-deploy (push) Successful in 5m24s

- 新增套餐系列管理 (CRUD + 状态切换)
- 新增套餐管理 (CRUD + 启用/禁用 + 上架/下架双状态)
- 清理 8 个废弃分佣模型及对应数据库表
- Package 模型新增建议成本价、建议售价、上架状态字段
- 完整的 Store/Service/Handler 三层实现
- 包含单元测试和集成测试
- 归档 add-package-module change
- 新增多个 OpenSpec changes (订单支付、店铺套餐分配、一次性分佣、卡设备系列绑定)
This commit is contained in:
2026-01-27 19:55:47 +08:00
parent 30a0717316
commit 79c061b6fa
70 changed files with 7554 additions and 244 deletions

View File

@@ -0,0 +1,2 @@
schema: spec-driven
created: 2026-01-27

View File

@@ -0,0 +1,123 @@
## Context
Phase 2 完成了代理套餐分配机制,但卡和设备还没有关联到具体的套餐系列。本期在 IotCard 和 Device 模型上新增字段,记录其所属的套餐系列分配,为后续的套餐购买和佣金计算做准备。
**关键业务规则**
- 卡/设备关联后才能购买该系列下的套餐
- 设备关联后,其绑定的所有卡共享该套餐系列(设备级套餐)
- 每张卡/设备只能触发一次一次性佣金
## Goals / Non-Goals
**Goals:**
- 在 IotCard 模型新增套餐系列关联和佣金状态字段
- 在 Device 模型新增相同字段
- 提供批量设置卡/设备套餐系列的 API
- 验证关联的系列必须是当前店铺被分配的
**Non-Goals:**
- 不实现订单支付Phase 4
- 不实现佣金计算Phase 5
- 不自动同步设备和卡的关联(手动设置)
## Decisions
### 1. 新增字段设计
**决策**:在 IotCard 和 Device 模型各新增 3 个字段
```go
// 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`:累计充值金额,用于累计充值触发条件
### 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
- 待确认:是否需要双向同步?

View File

@@ -0,0 +1,58 @@
## Why
Phase 2 完成了代理套餐分配,但卡和设备还没有关联到具体的套餐系列分配。需要在卡/设备上记录其所属的套餐系列分配,以便后续购买套餐时验证权限、计算佣金。同时需要记录一次性佣金状态和累计充值金额,为 Phase 5 的佣金计算做准备。
## What Changes
**IotCard 模型调整:**
- 新增 `series_allocation_id`:关联的套餐系列分配 ID
- 新增 `first_commission_paid`一次性佣金是否已发放bool
- 新增 `accumulated_recharge`:累计充值金额(分)
**Device 模型调整:**
- 新增 `series_allocation_id`:关联的套餐系列分配 ID
- 新增 `first_commission_paid`一次性佣金是否已发放bool
- 新增 `accumulated_recharge`:累计充值金额(分)
**新增 API**
- 批量设置卡的套餐系列分配
- 批量设置设备的套餐系列分配
- 查询卡/设备的套餐系列分配信息
**业务规则:**
- 卡/设备只能关联当前所属店铺被分配的套餐系列
- 设备关联后,其绑定的所有卡共享该套餐系列
- 关联后可购买该系列下的套餐
## Capabilities
### New Capabilities
- `card-series-bindng`: 卡套餐系列关联 - 为 IoT 卡设置套餐系列分配,记录佣金状态
- `device-series-bindng`: 设备套餐系列关联 - 为设备设置套餐系列分配,设备下所有卡共享
### Modified Capabilities
<!-- 无 -->
## Impact
**代码影响:**
- `internal/model/iot_card.go` - 新增 3 个字段
- `internal/model/device.go` - 新增 3 个字段
- `migrations/` - 修改 tb_iot_card 和 tb_device 表
- `internal/handler/admin/` - 扩展卡/设备 Handler
- `internal/service/` - 扩展卡/设备 Service
- `internal/model/dto/` - 新增请求 DTO
**API 影响:**
- 新增 `PATCH /api/admin/iot-cards/series-bindng` 批量设置卡系列
- 新增 `PATCH /api/admin/devices/series-bindng` 批量设置设备系列
**数据库影响:**
- 修改表:`tb_iot_card` 新增 3 个字段
- 修改表:`tb_device` 新增 3 个字段
**依赖关系:**
- 依赖 Phase 2add-shop-package-allocation完成
- Phase 4订单与支付依赖本期

View File

@@ -0,0 +1,70 @@
## ADDED Requirements
### Requirement: 批量设置卡的套餐系列
系统 SHALL 允许代理批量为 IoT 卡设置套餐系列分配。只能设置当前店铺被分配且启用的套餐系列。
#### Scenario: 成功批量设置
- **WHEN** 代理提交多个 ICCID 和一个有效的 series_allocation_id
- **THEN** 系统更新这些卡的 series_allocation_id 字段
#### Scenario: 系列未分配给店铺
- **WHEN** 代理尝试设置一个未分配给卡所属店铺的系列
- **THEN** 系统返回错误 "该套餐系列未分配给此店铺"
#### Scenario: 系列分配已禁用
- **WHEN** 代理尝试设置一个已禁用的系列分配
- **THEN** 系统返回错误 "该套餐系列分配已禁用"
#### Scenario: ICCID 不存在
- **WHEN** 提交的 ICCID 中有不存在的卡
- **THEN** 系统返回错误,列出不存在的 ICCID
#### Scenario: 卡不属于当前店铺
- **WHEN** 代理尝试设置不属于自己店铺的卡
- **THEN** 系统返回错误 "部分卡不属于您的店铺"
---
### Requirement: 清除卡的套餐系列关联
系统 SHALL 允许代理清除卡的套餐系列关联(将 series_allocation_id 设为 0
#### Scenario: 清除单卡关联
- **WHEN** 代理将卡的 series_allocation_id 设为 0
- **THEN** 系统清除该卡的套餐系列关联
#### Scenario: 批量清除关联
- **WHEN** 代理批量提交 ICCID 列表series_allocation_id 为 0
- **THEN** 系统清除这些卡的套餐系列关联
---
### Requirement: 查询卡的套餐系列信息
系统 SHALL 在卡详情和列表中返回套餐系列关联信息。
#### Scenario: 卡详情包含系列信息
- **WHEN** 查询卡详情
- **THEN** 响应包含 series_allocation_id、关联的系列名称、佣金状态
#### Scenario: 卡列表支持按系列筛选
- **WHEN** 代理按 series_allocation_id 筛选卡列表
- **THEN** 系统只返回关联该系列的卡
---
### Requirement: IotCard 模型新增字段
系统 MUST 在 IotCard 模型中新增以下字段:
- `series_allocation_id`:套餐系列分配 ID
- `first_commission_paid`:一次性佣金是否已发放(默认 false
- `accumulated_recharge`:累计充值金额(默认 0
#### Scenario: 新卡默认值
- **WHEN** 创建新的 IoT 卡
- **THEN** series_allocation_id 为空first_commission_paid 为 falseaccumulated_recharge 为 0
#### Scenario: 字段在响应中可见
- **WHEN** 查询卡信息
- **THEN** 响应包含这三个新字段

View File

@@ -0,0 +1,84 @@
## ADDED Requirements
### Requirement: 批量设置设备的套餐系列
系统 SHALL 允许代理批量为设备设置套餐系列分配。只能设置当前店铺被分配且启用的套餐系列。
#### Scenario: 成功批量设置
- **WHEN** 代理提交多个设备 ID 和一个有效的 series_allocation_id
- **THEN** 系统更新这些设备的 series_allocation_id 字段
#### Scenario: 系列未分配给店铺
- **WHEN** 代理尝试设置一个未分配给设备所属店铺的系列
- **THEN** 系统返回错误 "该套餐系列未分配给此店铺"
#### Scenario: 系列分配已禁用
- **WHEN** 代理尝试设置一个已禁用的系列分配
- **THEN** 系统返回错误 "该套餐系列分配已禁用"
#### Scenario: 设备不存在
- **WHEN** 提交的设备 ID 中有不存在的设备
- **THEN** 系统返回错误,列出不存在的设备 ID
#### Scenario: 设备不属于当前店铺
- **WHEN** 代理尝试设置不属于自己店铺的设备
- **THEN** 系统返回错误 "部分设备不属于您的店铺"
---
### Requirement: 清除设备的套餐系列关联
系统 SHALL 允许代理清除设备的套餐系列关联。
#### Scenario: 清除单设备关联
- **WHEN** 代理将设备的 series_allocation_id 设为 0
- **THEN** 系统清除该设备的套餐系列关联
#### Scenario: 批量清除关联
- **WHEN** 代理批量提交设备 ID 列表series_allocation_id 为 0
- **THEN** 系统清除这些设备的套餐系列关联
---
### Requirement: 查询设备的套餐系列信息
系统 SHALL 在设备详情和列表中返回套餐系列关联信息。
#### Scenario: 设备详情包含系列信息
- **WHEN** 查询设备详情
- **THEN** 响应包含 series_allocation_id、关联的系列名称、佣金状态
#### Scenario: 设备列表支持按系列筛选
- **WHEN** 代理按 series_allocation_id 筛选设备列表
- **THEN** 系统只返回关联该系列的设备
---
### Requirement: Device 模型新增字段
系统 MUST 在 Device 模型中新增以下字段:
- `series_allocation_id`:套餐系列分配 ID
- `first_commission_paid`:一次性佣金是否已发放(默认 false
- `accumulated_recharge`:累计充值金额(默认 0
#### Scenario: 新设备默认值
- **WHEN** 创建新设备
- **THEN** series_allocation_id 为空first_commission_paid 为 falseaccumulated_recharge 为 0
#### Scenario: 字段在响应中可见
- **WHEN** 查询设备信息
- **THEN** 响应包含这三个新字段
---
### Requirement: 设备级套餐购买优先级
设备购买套餐时 MUST 使用 Device.series_allocation_id 确定可购买的套餐系列,而非设备下单卡的 series_allocation_id。
#### Scenario: 设备有系列关联
- **WHEN** 设备有 series_allocation_id且其下的卡也有各自的 series_allocation_id
- **THEN** 设备级套餐购买使用设备的 series_allocation_id
#### Scenario: 设备无系列关联
- **WHEN** 设备的 series_allocation_id 为空
- **THEN** 该设备无法购买设备级套餐

View File

@@ -0,0 +1,85 @@
## 1. IotCard 模型调整
- [ ] 1.1 在 `internal/model/iot_card.go` 中新增 `series_allocation_id` 字段uint, index, 可空)
- [ ] 1.2 新增 `first_commission_paid` 字段bool, 默认 false
- [ ] 1.3 新增 `accumulated_recharge` 字段bigint, 默认 0
## 2. Device 模型调整
- [ ] 2.1 在 `internal/model/device.go` 中新增 `series_allocation_id` 字段uint, index, 可空)
- [ ] 2.2 新增 `first_commission_paid` 字段bool, 默认 false
- [ ] 2.3 新增 `accumulated_recharge` 字段bigint, 默认 0
## 3. 数据库迁移
- [ ] 3.1 创建迁移文件,为 tb_iot_card 添加 3 个新字段
- [ ] 3.2 为 tb_device 添加 3 个新字段
- [ ] 3.3 为 series_allocation_id 添加索引
- [ ] 3.4 本地执行迁移验证
## 4. DTO 更新
- [ ] 4.1 更新 IotCard 相关 DTO新增 series_allocation_id、first_commission_paid、accumulated_recharge 字段
- [ ] 4.2 更新 Device 相关 DTO新增相同字段
- [ ] 4.3 创建 BatchSetSeriesBindngRequesticcids/device_ids + series_allocation_id
- [ ] 4.4 创建 BatchSetSeriesBindngResponse成功数、失败列表
## 5. IotCard Store 更新
- [ ] 5.1 在 IotCardStore 中添加 BatchUpdateSeriesAllocation 方法
- [ ] 5.2 添加 ListBySeriesAllocationID 方法(按系列筛选)
- [ ] 5.3 更新 List 方法支持 series_allocation_id 筛选
## 6. Device Store 更新
- [ ] 6.1 在 DeviceStore 中添加 BatchUpdateSeriesAllocation 方法
- [ ] 6.2 添加 ListBySeriesAllocationID 方法
- [ ] 6.3 更新 List 方法支持 series_allocation_id 筛选
## 7. IotCard Service 更新
- [ ] 7.1 在 IotCardService 中添加 BatchSetSeriesBindng 方法(验证权限、验证系列分配)
- [ ] 7.2 添加 ValidateSeriesAllocation 辅助方法(检查系列是否分配给店铺)
## 8. Device Service 更新
- [ ] 8.1 在 DeviceService 中添加 BatchSetSeriesBindng 方法
- [ ] 8.2 添加 ValidateSeriesAllocation 辅助方法
## 9. IotCard Handler 更新
- [ ] 9.1 在 IotCardHandler 中添加 BatchSetSeriesBindng 接口PATCH /api/admin/iot-cards/series-bindng
- [ ] 9.2 更新 List 接口支持 series_allocation_id 筛选参数
- [ ] 9.3 更新 Get 接口响应包含系列关联信息
## 10. Device Handler 更新
- [ ] 10.1 在 DeviceHandler 中添加 BatchSetSeriesBindng 接口PATCH /api/admin/devices/series-bindng
- [ ] 10.2 更新 List 接口支持 series_allocation_id 筛选参数
- [ ] 10.3 更新 Get 接口响应包含系列关联信息
## 11. 路由注册
- [ ] 11.1 注册 `PATCH /api/admin/iot-cards/series-bindng` 路由
- [ ] 11.2 注册 `PATCH /api/admin/devices/series-bindng` 路由
## 12. 文档生成器更新
- [ ] 12.1 更新 docs.go 和 gendocs/main.go如有新 Handler
- [ ] 12.2 执行文档生成验证
## 13. 测试
- [ ] 13.1 IotCardStore 批量更新方法单元测试
- [ ] 13.2 DeviceStore 批量更新方法单元测试
- [ ] 13.3 IotCardService BatchSetSeriesBindng 单元测试(覆盖权限验证)
- [ ] 13.4 DeviceService BatchSetSeriesBindng 单元测试
- [ ] 13.5 卡系列关联 API 集成测试
- [ ] 13.6 设备系列关联 API 集成测试
- [ ] 13.7 执行 `go test ./...` 确认通过
## 14. 最终验证
- [ ] 14.1 执行 `go build ./...` 确认编译通过
- [ ] 14.2 启动服务,手动测试批量设置功能
- [ ] 14.3 验证列表筛选功能正常