feat: 实现套餐管理模块,包含套餐系列、双状态管理、废弃模型清理
All checks were successful
构建并部署到测试环境(无 SSH) / build-and-deploy (push) Successful in 5m24s
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:
@@ -0,0 +1,2 @@
|
||||
schema: spec-driven
|
||||
created: 2026-01-27
|
||||
123
openspec/changes/add-card-device-series-bindng/design.md
Normal file
123
openspec/changes/add-card-device-series-bindng/design.md
Normal 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
|
||||
- 待确认:是否需要双向同步?
|
||||
58
openspec/changes/add-card-device-series-bindng/proposal.md
Normal file
58
openspec/changes/add-card-device-series-bindng/proposal.md
Normal 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 2(add-shop-package-allocation)完成
|
||||
- Phase 4(订单与支付)依赖本期
|
||||
@@ -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 为 false,accumulated_recharge 为 0
|
||||
|
||||
#### Scenario: 字段在响应中可见
|
||||
- **WHEN** 查询卡信息
|
||||
- **THEN** 响应包含这三个新字段
|
||||
@@ -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 为 false,accumulated_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** 该设备无法购买设备级套餐
|
||||
85
openspec/changes/add-card-device-series-bindng/tasks.md
Normal file
85
openspec/changes/add-card-device-series-bindng/tasks.md
Normal 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 创建 BatchSetSeriesBindngRequest(iccids/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 验证列表筛选功能正常
|
||||
2
openspec/changes/add-one-time-commission/.openspec.yaml
Normal file
2
openspec/changes/add-one-time-commission/.openspec.yaml
Normal file
@@ -0,0 +1,2 @@
|
||||
schema: spec-driven
|
||||
created: 2026-01-27
|
||||
252
openspec/changes/add-one-time-commission/design.md
Normal file
252
openspec/changes/add-one-time-commission/design.md
Normal file
@@ -0,0 +1,252 @@
|
||||
## Context
|
||||
|
||||
Phase 4 完成了订单和支付流程,现在需要实现佣金计算。当终端用户购买套餐支付成功后,系统自动计算各级代理的佣金并入账。
|
||||
|
||||
**佣金来源**:
|
||||
1. **成本价差收入**:每笔订单必触发,售价 - 成本价 = 代理收入
|
||||
2. **一次性佣金**:满足触发条件时发放一次,金额从梯度配置获取
|
||||
|
||||
**当前 CommissionRecord 模型过于复杂**(包含冻结/解冻字段),需要简化。
|
||||
|
||||
## Goals / Non-Goals
|
||||
|
||||
**Goals:**
|
||||
- 简化 CommissionRecord 模型
|
||||
- 实现成本价差收入计算(每笔订单)
|
||||
- 实现一次性佣金触发(充值阈值)
|
||||
- 佣金直接入账到店铺钱包
|
||||
- 提供佣金记录查询和统计
|
||||
|
||||
**Non-Goals:**
|
||||
- 不实现冻结/解冻机制
|
||||
- 不实现长期佣金(号卡专用)
|
||||
- 不实现梯度佣金统计(本期只做配置,统计后续优化)
|
||||
- 不实现佣金审批流程
|
||||
|
||||
## Decisions
|
||||
|
||||
### 1. CommissionRecord 模型简化
|
||||
|
||||
**决策**:删除冻结相关字段,新增来源和关联字段
|
||||
|
||||
```go
|
||||
// 简化后的 CommissionRecord
|
||||
type CommissionRecord struct {
|
||||
gorm.Model
|
||||
BaseModel
|
||||
ShopID uint // 店铺ID(佣金归属)
|
||||
OrderID uint // 关联订单ID
|
||||
IotCardID uint // 关联卡ID(可空)
|
||||
DeviceID uint // 关联设备ID(可空)
|
||||
CommissionSource string // 佣金来源: cost_diff-成本价差 one_time-一次性佣金 tier_bonus-梯度奖励
|
||||
Amount int64 // 佣金金额(分)
|
||||
BalanceAfter int64 // 入账后钱包余额(分)
|
||||
Status int // 状态: 1-已入账 2-已失效
|
||||
ReleasedAt *time.Time // 入账时间
|
||||
Remark string // 备注
|
||||
}
|
||||
```
|
||||
|
||||
**删除字段**:
|
||||
- `agent_id`(改用 shop_id)
|
||||
- `rule_id`(不再关联复杂规则)
|
||||
- `commission_type`(改用 commission_source)
|
||||
- `unfrozen_at`、冻结相关状态
|
||||
|
||||
### 2. 佣金计算流程
|
||||
|
||||
**决策**:订单支付成功后异步计算
|
||||
|
||||
```
|
||||
订单支付成功
|
||||
↓
|
||||
发送异步任务 (Asynq)
|
||||
↓
|
||||
佣金计算任务执行:
|
||||
1. 获取订单信息
|
||||
2. 遍历代理层级(从销售店铺到顶级)
|
||||
3. 每级计算成本价差收入
|
||||
4. 检查一次性佣金触发条件
|
||||
5. 创建 CommissionRecord
|
||||
6. 更新店铺钱包余额
|
||||
7. 更新订单 commission_status
|
||||
```
|
||||
|
||||
### 3. 成本价差收入计算
|
||||
|
||||
**决策**:各级代理按自己的成本价差计算
|
||||
|
||||
**计算规则**:
|
||||
- 终端销售代理:收入 = 售价 - 自己的成本价
|
||||
- 中间层级代理:收入 = 下级的成本价 - 自己的成本价
|
||||
|
||||
```go
|
||||
func CalculateCostDiffCommission(order *Order) []CommissionRecord {
|
||||
var records []CommissionRecord
|
||||
|
||||
// 获取销售店铺(终端销售的代理)
|
||||
sellerShop := GetShop(order.SellerShopID)
|
||||
sellerCostPrice := GetCostPrice(sellerShop.ID, order.PackageID)
|
||||
|
||||
// 终端销售代理的收入 = 售价 - 成本价
|
||||
sellerProfit := order.TotalAmount - sellerCostPrice
|
||||
if sellerProfit > 0 {
|
||||
records = append(records, CommissionRecord{
|
||||
ShopID: sellerShop.ID,
|
||||
OrderID: order.ID,
|
||||
CommissionSource: "cost_diff",
|
||||
Amount: sellerProfit,
|
||||
})
|
||||
}
|
||||
|
||||
// 遍历上级代理链
|
||||
childCostPrice := sellerCostPrice
|
||||
currentShop := GetShop(sellerShop.ParentID)
|
||||
|
||||
for currentShop != nil {
|
||||
// 获取当前店铺的成本价
|
||||
myCostPrice := GetCostPrice(currentShop.ID, order.PackageID)
|
||||
|
||||
// 收入 = 下级成本价 - 自己成本价
|
||||
profit := childCostPrice - myCostPrice
|
||||
if profit > 0 {
|
||||
records = append(records, CommissionRecord{
|
||||
ShopID: currentShop.ID,
|
||||
OrderID: order.ID,
|
||||
CommissionSource: "cost_diff",
|
||||
Amount: profit,
|
||||
})
|
||||
}
|
||||
|
||||
// 移动到上级
|
||||
childCostPrice = myCostPrice
|
||||
currentShop = GetShop(currentShop.ParentID)
|
||||
}
|
||||
|
||||
return records
|
||||
}
|
||||
```
|
||||
|
||||
### 4. 一次性佣金触发
|
||||
|
||||
**决策**:两种触发类型,每张卡/设备只触发一次
|
||||
|
||||
```go
|
||||
// 触发类型 A:一次性充值 ≥ 阈值
|
||||
func CheckOneTimeRecharge(order *Order, threshold int64) bool {
|
||||
return order.TotalAmount >= threshold
|
||||
}
|
||||
|
||||
// 触发类型 B:累计充值 ≥ 阈值
|
||||
func CheckAccumulatedRecharge(card *IotCard, threshold int64) bool {
|
||||
return card.AccumulatedRecharge >= threshold
|
||||
}
|
||||
|
||||
// 检查并发放一次性佣金
|
||||
func TriggerOneTimeCommission(order *Order, card *IotCard) {
|
||||
if card.FirstCommissionPaid {
|
||||
return // 已发放过
|
||||
}
|
||||
|
||||
// 获取配置的触发条件和金额
|
||||
tier := GetCommissionTier(card.SeriesAllocationID)
|
||||
|
||||
// 检查触发条件
|
||||
triggered := false
|
||||
switch tier.TriggerType {
|
||||
case "one_time_recharge":
|
||||
triggered = CheckOneTimeRecharge(order, tier.ThresholdValue)
|
||||
case "accumulated_recharge":
|
||||
triggered = CheckAccumulatedRecharge(card, tier.ThresholdValue)
|
||||
}
|
||||
|
||||
if triggered {
|
||||
// 发放佣金
|
||||
CreateCommissionRecord(...)
|
||||
// 标记已发放
|
||||
card.FirstCommissionPaid = true
|
||||
UpdateCard(card)
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 5. 钱包入账
|
||||
|
||||
**决策**:直接入账,无冻结期
|
||||
|
||||
```go
|
||||
func CreditCommission(record *CommissionRecord) error {
|
||||
return Transaction(func(tx *gorm.DB) error {
|
||||
// 1. 获取店铺钱包
|
||||
wallet := GetWallet("shop", record.ShopID)
|
||||
|
||||
// 2. 增加余额
|
||||
wallet.Balance += record.Amount
|
||||
UpdateWallet(wallet)
|
||||
|
||||
// 3. 记录余额
|
||||
record.BalanceAfter = wallet.Balance
|
||||
record.Status = 1 // 已入账
|
||||
record.ReleasedAt = time.Now()
|
||||
UpdateCommissionRecord(record)
|
||||
|
||||
// 4. 创建钱包交易记录
|
||||
CreateWalletTransaction(...)
|
||||
|
||||
return nil
|
||||
})
|
||||
}
|
||||
```
|
||||
|
||||
### 6. API 设计
|
||||
|
||||
```
|
||||
# 佣金记录查询
|
||||
GET /api/admin/commission-records 佣金记录列表
|
||||
GET /api/admin/commission-records/:id 佣金记录详情
|
||||
|
||||
# 佣金统计
|
||||
GET /api/admin/commission-stats 佣金统计(总收入、各来源占比)
|
||||
GET /api/admin/commission-stats/daily 每日佣金统计
|
||||
```
|
||||
|
||||
## Risks / Trade-offs
|
||||
|
||||
### 风险 1:异步计算失败
|
||||
|
||||
**风险**:佣金计算任务失败导致佣金未发放
|
||||
|
||||
**缓解**:
|
||||
- Asynq 自动重试机制
|
||||
- 记录任务执行日志
|
||||
- 提供手动触发补偿接口
|
||||
|
||||
### 风险 2:并发更新钱包余额
|
||||
|
||||
**风险**:多笔佣金同时入账导致余额计算错误
|
||||
|
||||
**缓解**:
|
||||
- 使用数据库事务
|
||||
- 钱包更新使用乐观锁或悲观锁
|
||||
|
||||
### 风险 3:代理层级变更
|
||||
|
||||
**风险**:订单支付后代理层级变更,佣金计算基于哪个时间点?
|
||||
|
||||
**缓解**:
|
||||
- 佣金计算基于订单支付时的代理关系
|
||||
- 订单中可记录销售店铺ID快照
|
||||
|
||||
## Open Questions
|
||||
|
||||
1. **梯度佣金何时统计?**
|
||||
- 当前设计:本期只做配置,不做实际统计
|
||||
- 待确认:是否需要定时任务统计并发放梯度奖励?
|
||||
|
||||
2. **累计充值是否包含当前订单?**
|
||||
- 当前设计:先更新累计充值,再检查触发条件
|
||||
- 待确认:是否正确?
|
||||
|
||||
3. **一次性佣金发放给谁?**
|
||||
- 当前设计:发放给卡/设备的直接归属店铺
|
||||
- 待确认:是否需要多级分佣?
|
||||
77
openspec/changes/add-one-time-commission/proposal.md
Normal file
77
openspec/changes/add-one-time-commission/proposal.md
Normal file
@@ -0,0 +1,77 @@
|
||||
## Why
|
||||
|
||||
Phase 4 完成了订单与支付流程,现在需要实现一次性佣金计算。当终端用户购买套餐时,各级代理根据成本价差获得收入,并根据配置的触发条件(一次性充值阈值/累计充值阈值)发放一次性佣金。
|
||||
|
||||
## What Changes
|
||||
|
||||
**CommissionRecord 模型简化:**
|
||||
- 移除冻结/解冻相关字段(unfreeze_days, unfrozen_at 等)
|
||||
- 移除 rule_id(不再关联复杂规则)
|
||||
- 移除 agent_id(改用 shop_id,佣金归属店铺而非个人账号)
|
||||
- 保留:shop_id, order_id, amount, status, released_at, balance_after
|
||||
- 新增:commission_source(成本价差/一次性佣金/梯度佣金)
|
||||
- 新增:iot_card_id/device_id(关联的卡/设备)
|
||||
- 新增:remark(备注)
|
||||
|
||||
**佣金计算逻辑:**
|
||||
|
||||
1. **成本价差收入**(每笔订单必触发)
|
||||
- 终端销售代理:售价 - 自己的成本价 = 收入
|
||||
- 中间层级代理:下级的成本价 - 自己的成本价 = 收入
|
||||
- 各级代理按自己的成本价差计算,确保每级都有利润
|
||||
|
||||
2. **一次性佣金**(满足条件触发一次)
|
||||
- 触发类型 A:一次性充值 ≥ 阈值
|
||||
- 触发类型 B:累计充值 ≥ 阈值
|
||||
- 每张卡/设备只触发一次
|
||||
- 佣金金额从 ShopSeriesCommissionTier 获取(支持梯度)
|
||||
|
||||
3. **多级分佣**
|
||||
- 订单支付成功后,遍历代理层级
|
||||
- 每级代理计算成本价差收入
|
||||
- 检查一次性佣金触发条件
|
||||
|
||||
**新增 API:**
|
||||
- 佣金记录列表查询(按店铺/时间/来源筛选)
|
||||
- 佣金统计(总收入、各来源占比)
|
||||
- 手动触发佣金计算(补偿机制)
|
||||
|
||||
**业务规则:**
|
||||
- 佣金直接入账到店铺钱包,无冻结期
|
||||
- 一次性佣金只发放一次,通过 card.first_commission_paid 标记
|
||||
- 累计充值记录在 card.accumulated_recharge
|
||||
- 梯度佣金根据配置的时间范围统计销量/销售额
|
||||
|
||||
## Capabilities
|
||||
|
||||
### New Capabilities
|
||||
|
||||
- `commission-calculation`: 佣金计算 - 订单支付后自动计算各级代理的成本价差收入
|
||||
- `one-time-commission-trigger`: 一次性佣金触发 - 根据充值阈值触发一次性佣金发放
|
||||
- `commission-record-query`: 佣金记录查询 - 查询佣金明细和统计数据
|
||||
|
||||
### Modified Capabilities
|
||||
|
||||
<!-- 无,CommissionRecord 的修改在 proposal 中说明,不需要单独 spec -->
|
||||
|
||||
## Impact
|
||||
|
||||
**代码影响:**
|
||||
- `internal/model/commission.go` - 简化 CommissionRecord 模型
|
||||
- `migrations/` - 修改 tb_commission_record 表结构
|
||||
- `internal/handler/admin/` - 新增/修改佣金查询 Handler
|
||||
- `internal/service/` - 新增佣金计算 Service
|
||||
- `internal/store/postgres/` - 修改 CommissionRecordStore
|
||||
- `internal/task/` - 佣金计算异步任务
|
||||
|
||||
**API 影响:**
|
||||
- 修改 `/api/admin/commission-records/*` 佣金记录查询
|
||||
- 新增 `/api/admin/commission-stats` 佣金统计
|
||||
|
||||
**数据库影响:**
|
||||
- 修改表:`tb_commission_record`(简化字段、新增字段)
|
||||
|
||||
**依赖关系:**
|
||||
- 依赖 Phase 4(add-order-payment)完成
|
||||
- 依赖 Phase 2 的 ShopSeriesCommissionTier 梯度配置
|
||||
- 依赖 Phase 3 的卡/设备佣金状态字段
|
||||
@@ -0,0 +1,73 @@
|
||||
## ADDED Requirements
|
||||
|
||||
### Requirement: 订单支付后触发佣金计算
|
||||
|
||||
系统 SHALL 在订单支付成功后自动触发佣金计算。计算通过异步任务执行。
|
||||
|
||||
#### Scenario: 支付成功触发计算
|
||||
- **WHEN** 订单支付状态变为已支付
|
||||
- **THEN** 系统发送佣金计算异步任务
|
||||
|
||||
#### Scenario: 重复支付不重复计算
|
||||
- **WHEN** 订单已计算过佣金(commission_status=2)
|
||||
- **THEN** 系统不重复触发计算
|
||||
|
||||
---
|
||||
|
||||
### Requirement: 成本价差收入计算
|
||||
|
||||
系统 SHALL 为代理链上的每一级代理计算成本价差收入。终端销售代理收入 = 售价 - 成本价;中间层级代理收入 = 下级成本价 - 自己成本价。
|
||||
|
||||
#### Scenario: 单级代理
|
||||
- **WHEN** 一级代理销售套餐,售价 100 元,成本价 80 元
|
||||
- **THEN** 一级代理获得 20 元(100 - 80)成本价差收入
|
||||
|
||||
#### Scenario: 多级代理
|
||||
- **WHEN** 三级代理销售套餐,售价 100 元,各级成本价为:平台 50 → 一级 60 → 二级 70 → 三级 80
|
||||
- **THEN** 三级获得 20 元(100 - 80),二级获得 10 元(80 - 70),一级获得 10 元(70 - 60),平台获得 10 元(60 - 50)
|
||||
|
||||
#### Scenario: 成本价相同
|
||||
- **WHEN** 某级代理成本价等于下级成本价
|
||||
- **THEN** 该级代理成本价差收入为 0,不创建佣金记录
|
||||
|
||||
---
|
||||
|
||||
### Requirement: 佣金直接入账
|
||||
|
||||
成本价差收入 SHALL 直接入账到店铺钱包,无冻结期。
|
||||
|
||||
#### Scenario: 佣金入账
|
||||
- **WHEN** 计算出代理的成本价差收入
|
||||
- **THEN** 系统直接增加店铺钱包余额,创建佣金记录和钱包交易记录
|
||||
|
||||
#### Scenario: 记录入账后余额
|
||||
- **WHEN** 佣金入账
|
||||
- **THEN** CommissionRecord.balance_after 记录入账后的钱包余额
|
||||
|
||||
---
|
||||
|
||||
### Requirement: 更新累计充值金额
|
||||
|
||||
订单支付成功后系统 SHALL 更新卡/设备的累计充值金额。
|
||||
|
||||
#### Scenario: 单卡订单更新累计充值
|
||||
- **WHEN** 单卡订单支付成功,金额 100 元
|
||||
- **THEN** IotCard.accumulated_recharge 增加 10000 分
|
||||
|
||||
#### Scenario: 设备订单更新累计充值
|
||||
- **WHEN** 设备订单支付成功,金额 300 元
|
||||
- **THEN** Device.accumulated_recharge 增加 30000 分
|
||||
|
||||
---
|
||||
|
||||
### Requirement: CommissionRecord 模型简化
|
||||
|
||||
系统 MUST 简化 CommissionRecord 模型,移除冻结相关字段。
|
||||
|
||||
#### Scenario: 新佣金记录字段
|
||||
- **WHEN** 创建佣金记录
|
||||
- **THEN** 包含:shop_id, order_id, iot_card_id, device_id, commission_source, amount, balance_after, status, released_at, remark
|
||||
|
||||
#### Scenario: 佣金来源类型
|
||||
- **WHEN** 创建佣金记录
|
||||
- **THEN** commission_source 为以下之一:cost_diff(成本价差)、one_time(一次性佣金)、tier_bonus(梯度奖励)
|
||||
@@ -0,0 +1,67 @@
|
||||
## ADDED Requirements
|
||||
|
||||
### Requirement: 查询佣金记录列表
|
||||
|
||||
系统 SHALL 提供佣金记录列表查询,支持按店铺、佣金来源、时间范围、状态筛选。
|
||||
|
||||
#### Scenario: 代理查询自己店铺的佣金
|
||||
- **WHEN** 代理查询佣金记录列表
|
||||
- **THEN** 系统返回该店铺的所有佣金记录
|
||||
|
||||
#### Scenario: 按佣金来源筛选
|
||||
- **WHEN** 指定 commission_source 为 cost_diff
|
||||
- **THEN** 系统只返回成本价差类型的佣金记录
|
||||
|
||||
#### Scenario: 按时间范围筛选
|
||||
- **WHEN** 指定开始时间和结束时间
|
||||
- **THEN** 系统只返回该时间范围内的佣金记录
|
||||
|
||||
#### Scenario: 响应包含关联信息
|
||||
- **WHEN** 查询佣金记录列表
|
||||
- **THEN** 每条记录包含:订单号、卡/设备信息、套餐名称
|
||||
|
||||
---
|
||||
|
||||
### Requirement: 查询佣金记录详情
|
||||
|
||||
系统 SHALL 允许查询单条佣金记录的详细信息。
|
||||
|
||||
#### Scenario: 查询佣金详情
|
||||
- **WHEN** 代理查询指定佣金记录详情
|
||||
- **THEN** 系统返回完整的佣金信息和关联的订单、卡/设备信息
|
||||
|
||||
#### Scenario: 查询他人佣金
|
||||
- **WHEN** 代理尝试查询其他店铺的佣金记录
|
||||
- **THEN** 系统返回 "记录不存在" 错误
|
||||
|
||||
---
|
||||
|
||||
### Requirement: 佣金统计
|
||||
|
||||
系统 SHALL 提供佣金统计功能,包含总收入和各来源占比。
|
||||
|
||||
#### Scenario: 查询总收入
|
||||
- **WHEN** 代理查询佣金统计
|
||||
- **THEN** 系统返回总收入金额(所有已入账佣金之和)
|
||||
|
||||
#### Scenario: 各来源占比
|
||||
- **WHEN** 代理查询佣金统计
|
||||
- **THEN** 系统返回各佣金来源的金额和占比(cost_diff、one_time、tier_bonus)
|
||||
|
||||
#### Scenario: 按时间范围统计
|
||||
- **WHEN** 指定时间范围查询统计
|
||||
- **THEN** 系统只统计该时间范围内的佣金
|
||||
|
||||
---
|
||||
|
||||
### Requirement: 每日佣金统计
|
||||
|
||||
系统 SHALL 提供每日佣金统计查询。
|
||||
|
||||
#### Scenario: 查询每日统计
|
||||
- **WHEN** 代理查询指定日期范围的每日统计
|
||||
- **THEN** 系统返回每天的佣金总额和笔数
|
||||
|
||||
#### Scenario: 默认最近30天
|
||||
- **WHEN** 代理查询每日统计不指定日期范围
|
||||
- **THEN** 系统返回最近 30 天的数据
|
||||
@@ -0,0 +1,69 @@
|
||||
## ADDED Requirements
|
||||
|
||||
### Requirement: 一次性充值触发佣金
|
||||
|
||||
系统 SHALL 支持"一次性充值"触发条件:当单笔订单金额 ≥ 配置阈值时触发一次性佣金。
|
||||
|
||||
#### Scenario: 达到一次性充值阈值
|
||||
- **WHEN** 订单金额 500 元,配置阈值 300 元,该卡未发放过一次性佣金
|
||||
- **THEN** 系统发放一次性佣金,标记卡的 first_commission_paid 为 true
|
||||
|
||||
#### Scenario: 未达到阈值
|
||||
- **WHEN** 订单金额 200 元,配置阈值 300 元
|
||||
- **THEN** 系统不发放一次性佣金
|
||||
|
||||
#### Scenario: 已发放过一次性佣金
|
||||
- **WHEN** 订单金额 500 元,但卡的 first_commission_paid 已为 true
|
||||
- **THEN** 系统不重复发放一次性佣金
|
||||
|
||||
---
|
||||
|
||||
### Requirement: 累计充值触发佣金
|
||||
|
||||
系统 SHALL 支持"累计充值"触发条件:当卡/设备的累计充值金额 ≥ 配置阈值时触发一次性佣金。
|
||||
|
||||
#### Scenario: 累计达到阈值
|
||||
- **WHEN** 卡之前累计充值 200 元,本次充值 150 元,配置阈值 300 元
|
||||
- **THEN** 累计 350 元 ≥ 300 元,系统发放一次性佣金
|
||||
|
||||
#### Scenario: 累计未达到阈值
|
||||
- **WHEN** 卡之前累计充值 100 元,本次充值 100 元,配置阈值 300 元
|
||||
- **THEN** 累计 200 元 < 300 元,系统不发放一次性佣金
|
||||
|
||||
---
|
||||
|
||||
### Requirement: 一次性佣金只发放一次
|
||||
|
||||
每张卡/设备的一次性佣金 SHALL 只发放一次,通过 first_commission_paid 字段控制。
|
||||
|
||||
#### Scenario: 首次触发
|
||||
- **WHEN** 首次满足触发条件
|
||||
- **THEN** 发放佣金,设置 first_commission_paid = true
|
||||
|
||||
#### Scenario: 再次满足条件
|
||||
- **WHEN** 再次满足触发条件但 first_commission_paid 已为 true
|
||||
- **THEN** 不发放佣金
|
||||
|
||||
---
|
||||
|
||||
### Requirement: 一次性佣金配置获取
|
||||
|
||||
一次性佣金的触发条件和金额 SHALL 从 ShopSeriesAllocation 配置获取。
|
||||
|
||||
#### Scenario: 获取触发条件和金额
|
||||
- **WHEN** 触发一次性佣金检查
|
||||
- **THEN** 系统从卡关联的 ShopSeriesAllocation 获取 one_time_commission_trigger(触发类型)、one_time_commission_threshold(阈值)、one_time_commission_amount(金额)
|
||||
|
||||
#### Scenario: 无一次性佣金配置
|
||||
- **WHEN** 卡关联的系列分配未配置一次性佣金(one_time_commission_amount = 0)
|
||||
- **THEN** 不发放一次性佣金
|
||||
|
||||
---
|
||||
|
||||
### Requirement: 一次性佣金发放对象
|
||||
|
||||
一次性佣金 SHALL 发放给卡/设备的直接归属店铺。
|
||||
|
||||
#### Scenario: 发放给归属店铺
|
||||
- **WHEN** 卡归属店铺 A,触发一次性佣金
|
||||
- **THEN** 佣金入账到店铺 A 的钱包
|
||||
99
openspec/changes/add-one-time-commission/tasks.md
Normal file
99
openspec/changes/add-one-time-commission/tasks.md
Normal file
@@ -0,0 +1,99 @@
|
||||
## 1. CommissionRecord 模型简化
|
||||
|
||||
- [ ] 1.1 修改 `internal/model/commission.go`,简化 CommissionRecord 结构
|
||||
- [ ] 1.2 删除冻结相关字段(unfrozen_at 等)
|
||||
- [ ] 1.3 删除 rule_id、agent_id 字段
|
||||
- [ ] 1.4 新增 commission_source 字段(varchar: cost_diff, one_time, tier_bonus)
|
||||
- [ ] 1.5 新增 iot_card_id、device_id 字段
|
||||
- [ ] 1.6 新增 remark 字段
|
||||
|
||||
## 2. 数据库迁移
|
||||
|
||||
- [ ] 2.1 创建迁移文件,修改 tb_commission_record 表结构
|
||||
- [ ] 2.2 删除废弃字段
|
||||
- [ ] 2.3 添加新字段
|
||||
- [ ] 2.4 添加索引(shop_id, order_id, commission_source, iot_card_id, device_id)
|
||||
- [ ] 2.5 本地执行迁移验证
|
||||
|
||||
## 3. DTO 更新
|
||||
|
||||
- [ ] 3.1 更新 `internal/model/dto/commission.go`,调整 CommissionRecordResponse
|
||||
- [ ] 3.2 定义 CommissionRecordListRequest(shop_id, commission_source, start_time, end_time, status)
|
||||
- [ ] 3.3 定义 CommissionStatsResponse(total_amount, cost_diff_amount, one_time_amount, tier_bonus_amount)
|
||||
- [ ] 3.4 定义 DailyCommissionStatsResponse
|
||||
|
||||
## 4. CommissionRecord Store 更新
|
||||
|
||||
- [ ] 4.1 更新 `internal/store/postgres/commission_record_store.go`,适配新模型
|
||||
- [ ] 4.2 更新 Create 方法
|
||||
- [ ] 4.3 更新 List 方法支持新筛选条件
|
||||
- [ ] 4.4 实现 GetStats 方法(统计总收入和各来源占比)
|
||||
- [ ] 4.5 实现 GetDailyStats 方法(每日统计)
|
||||
|
||||
## 5. 佣金计算 Service
|
||||
|
||||
- [ ] 5.1 创建 `internal/service/commission_calculation/service.go`
|
||||
- [ ] 5.2 实现 CalculateCommission 主方法(协调整体计算流程)
|
||||
- [ ] 5.3 实现 CalculateCostDiffCommission 方法(遍历代理层级计算成本价差)
|
||||
- [ ] 5.4 实现 CheckAndTriggerOneTimeCommission 方法(检查一次性佣金触发条件)
|
||||
- [ ] 5.5 实现 CreditCommission 方法(佣金入账到钱包)
|
||||
- [ ] 5.6 实现 UpdateAccumulatedRecharge 方法(更新累计充值金额)
|
||||
|
||||
## 6. 异步任务
|
||||
|
||||
- [ ] 6.1 创建 `internal/task/commission_calculation.go`,定义佣金计算任务类型
|
||||
- [ ] 6.2 实现任务处理函数 HandleCommissionCalculation
|
||||
- [ ] 6.3 在 OrderService.WalletPay 中添加任务发送逻辑
|
||||
- [ ] 6.4 在支付回调处理中添加任务发送逻辑
|
||||
- [ ] 6.5 在 Worker 中注册任务处理器
|
||||
|
||||
## 7. 佣金查询 Service
|
||||
|
||||
- [ ] 7.1 更新 `internal/service/my_commission/service.go`,适配新模型
|
||||
- [ ] 7.2 实现 List 方法
|
||||
- [ ] 7.3 实现 Get 方法
|
||||
- [ ] 7.4 实现 GetStats 方法
|
||||
- [ ] 7.5 实现 GetDailyStats 方法
|
||||
|
||||
## 8. Handler 更新
|
||||
|
||||
- [ ] 8.1 更新 `internal/handler/admin/my_commission.go`,适配新接口
|
||||
- [ ] 8.2 实现 List 接口
|
||||
- [ ] 8.3 实现 Get 接口
|
||||
- [ ] 8.4 实现 GetStats 接口
|
||||
- [ ] 8.5 实现 GetDailyStats 接口
|
||||
|
||||
## 9. Bootstrap 注册
|
||||
|
||||
- [ ] 9.1 在 services.go 中注册 CommissionCalculationService
|
||||
- [ ] 9.2 确认 MyCommissionService 注册正确
|
||||
|
||||
## 10. 路由更新
|
||||
|
||||
- [ ] 10.1 确认 `/api/admin/my-commission/records` 路由
|
||||
- [ ] 10.2 添加 `/api/admin/my-commission/stats` 路由
|
||||
- [ ] 10.3 添加 `/api/admin/my-commission/daily-stats` 路由
|
||||
|
||||
## 11. 文档生成器更新
|
||||
|
||||
- [ ] 11.1 更新 docs.go 和 gendocs/main.go
|
||||
- [ ] 11.2 执行文档生成验证
|
||||
|
||||
## 12. 测试
|
||||
|
||||
- [ ] 12.1 CommissionRecordStore 单元测试
|
||||
- [ ] 12.2 CommissionCalculationService 单元测试(覆盖成本价差计算)
|
||||
- [ ] 12.3 一次性佣金触发逻辑测试(覆盖各种触发条件)
|
||||
- [ ] 12.4 佣金入账事务测试
|
||||
- [ ] 12.5 异步任务测试
|
||||
- [ ] 12.6 佣金统计 API 集成测试
|
||||
- [ ] 12.7 执行 `go test ./...` 确认通过
|
||||
|
||||
## 13. 最终验证
|
||||
|
||||
- [ ] 13.1 执行 `go build ./...` 确认编译通过
|
||||
- [ ] 13.2 启动服务,创建订单并支付
|
||||
- [ ] 13.3 验证佣金记录正确创建
|
||||
- [ ] 13.4 验证钱包余额正确增加
|
||||
- [ ] 13.5 验证一次性佣金触发逻辑
|
||||
- [ ] 13.6 验证佣金统计数据正确
|
||||
2
openspec/changes/add-order-payment/.openspec.yaml
Normal file
2
openspec/changes/add-order-payment/.openspec.yaml
Normal file
@@ -0,0 +1,2 @@
|
||||
schema: spec-driven
|
||||
created: 2026-01-27
|
||||
237
openspec/changes/add-order-payment/design.md
Normal file
237
openspec/changes/add-order-payment/design.md
Normal file
@@ -0,0 +1,237 @@
|
||||
## Context
|
||||
|
||||
Phase 3 完成了卡/设备的套餐系列关联,现在需要实现订单和支付流程。核心是"强充"机制:用户必须通过购买套餐来充值,不能直接给钱包充值。这确保每笔资金流入都有对应的套餐购买记录。
|
||||
|
||||
**三类买家**:
|
||||
1. 个人客户:通过 H5/小程序购买,使用卡/设备钱包或第三方支付
|
||||
2. 代理商:通过后台购买,使用店铺钱包
|
||||
3. 企业客户:后台直接分配套餐,不走订单流程(本期不做)
|
||||
|
||||
## Goals / Non-Goals
|
||||
|
||||
**Goals:**
|
||||
- 设计订单和订单明细模型
|
||||
- 实现套餐购买订单创建流程
|
||||
- 实现钱包支付和第三方支付回调
|
||||
- 验证购买权限(卡/设备的套餐系列关联)
|
||||
- 套餐生效后更新流量额度
|
||||
|
||||
**Non-Goals:**
|
||||
- 不实现企业客户的套餐分配(后台直接操作)
|
||||
- 不实现第三方支付发起(仅处理回调)
|
||||
- 不实现佣金计算(Phase 5)
|
||||
- 不实现退款流程
|
||||
|
||||
## Decisions
|
||||
|
||||
### 1. 订单模型设计
|
||||
|
||||
**决策**:Order + OrderItem 两级结构
|
||||
|
||||
```go
|
||||
// Order 订单模型
|
||||
type Order struct {
|
||||
gorm.Model
|
||||
BaseModel
|
||||
OrderNo string // 订单号(唯一)
|
||||
OrderType string // 订单类型: single_card-单卡购买 device-设备购买
|
||||
BuyerType string // 买家类型: personal-个人客户 agent-代理商
|
||||
BuyerID uint // 买家ID(个人客户ID或店铺ID)
|
||||
IotCardID uint // IoT卡ID(单卡购买时)
|
||||
DeviceID uint // 设备ID(设备购买时)
|
||||
TotalAmount int64 // 订单总金额(分)
|
||||
PaymentMethod string // 支付方式: wallet-钱包 wechat-微信 alipay-支付宝
|
||||
PaymentStatus int // 支付状态: 1-待支付 2-已支付 3-已取消 4-已退款
|
||||
PaidAt *time.Time // 支付时间
|
||||
CommissionStatus int // 佣金状态: 1-待计算 2-已计算
|
||||
}
|
||||
|
||||
// OrderItem 订单明细模型
|
||||
type OrderItem struct {
|
||||
gorm.Model
|
||||
BaseModel
|
||||
OrderID uint // 订单ID
|
||||
PackageID uint // 套餐ID
|
||||
PackageName string // 套餐名称(快照)
|
||||
Quantity int // 数量(通常为1)
|
||||
UnitPrice int64 // 单价(分)
|
||||
Amount int64 // 小计(分)
|
||||
}
|
||||
```
|
||||
|
||||
**理由**:
|
||||
- 支持一个订单购买多个套餐(虽然初期可能只买一个)
|
||||
- 快照套餐名称,避免套餐修改影响历史订单
|
||||
- 佣金状态用于 Phase 5 的异步佣金计算
|
||||
|
||||
### 2. 订单号生成规则
|
||||
|
||||
**决策**:时间戳 + 随机数
|
||||
|
||||
```
|
||||
格式:ORD{YYYYMMDDHHMMSS}{6位随机数}
|
||||
示例:ORD20260127143052123456
|
||||
```
|
||||
|
||||
**理由**:
|
||||
- 可读性好,包含时间信息
|
||||
- 随机数避免并发冲突
|
||||
- 长度固定,便于存储和展示
|
||||
|
||||
### 3. 购买价格确定
|
||||
|
||||
**决策**:使用 Package.suggested_retail_price 作为统一售价
|
||||
|
||||
```
|
||||
个人客户购买:支付金额 = Package.suggested_retail_price
|
||||
代理为店铺购买:支付金额 = 代理的成本价(用于囤货/测试)
|
||||
```
|
||||
|
||||
**理由**:
|
||||
- 简化首期实现,所有终端用户统一售价
|
||||
- 代理的利润 = suggested_retail_price - 成本价
|
||||
- 后续如需支持代理自定义售价,可扩展 ShopPackageAllocation 增加 retail_price 字段
|
||||
|
||||
**非首期功能**:
|
||||
- 代理自定义售价
|
||||
- 促销折扣价
|
||||
|
||||
---
|
||||
|
||||
### 4. 购买权限验证
|
||||
|
||||
**决策**:多层验证
|
||||
|
||||
```go
|
||||
func ValidatePurchase(card/device, packageID) error {
|
||||
// 1. 获取卡/设备的 series_allocation_id
|
||||
allocationID := card.SeriesAllocationID
|
||||
if allocationID == 0 {
|
||||
return "该卡未关联套餐系列"
|
||||
}
|
||||
|
||||
// 2. 获取套餐信息
|
||||
pkg := GetPackage(packageID)
|
||||
|
||||
// 3. 验证套餐属于该系列
|
||||
allocation := GetAllocation(allocationID)
|
||||
if pkg.SeriesID != allocation.SeriesID {
|
||||
return "该套餐不在可购买范围内"
|
||||
}
|
||||
|
||||
// 4. 验证套餐状态
|
||||
if pkg.Status != 1 || pkg.ShelfStatus != 1 {
|
||||
return "该套餐已下架"
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
```
|
||||
|
||||
### 5. 支付流程
|
||||
|
||||
**决策**:同步钱包支付 + 异步第三方支付
|
||||
|
||||
```
|
||||
钱包支付流程:
|
||||
1. 创建订单(待支付)
|
||||
2. 检查钱包余额
|
||||
3. 扣减钱包余额(事务)
|
||||
4. 更新订单状态(已支付)
|
||||
5. 套餐生效
|
||||
6. 触发佣金计算(异步)
|
||||
|
||||
第三方支付流程:
|
||||
1. 创建订单(待支付)
|
||||
2. 返回订单信息,前端发起支付
|
||||
3. 支付回调更新订单状态
|
||||
4. 套餐生效
|
||||
5. 触发佣金计算(异步)
|
||||
```
|
||||
|
||||
### 6. 套餐生效逻辑
|
||||
|
||||
**决策**:创建 PackageUsage 记录
|
||||
|
||||
```go
|
||||
func ActivatePackage(order *Order) {
|
||||
for _, item := range order.Items {
|
||||
pkg := GetPackage(item.PackageID)
|
||||
|
||||
usage := &PackageUsage{
|
||||
OrderID: order.ID,
|
||||
PackageID: item.PackageID,
|
||||
UsageType: order.OrderType, // single_card 或 device
|
||||
IotCardID: order.IotCardID,
|
||||
DeviceID: order.DeviceID,
|
||||
DataLimitMB: pkg.DataAmountMB,
|
||||
ActivatedAt: time.Now(),
|
||||
ExpiresAt: time.Now().AddDate(0, pkg.DurationMonths, 0),
|
||||
Status: 1, // 生效中
|
||||
}
|
||||
CreatePackageUsage(usage)
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 7. API 设计
|
||||
|
||||
```
|
||||
# 订单管理(后台)
|
||||
POST /api/admin/orders 代理创建订单
|
||||
GET /api/admin/orders 订单列表
|
||||
GET /api/admin/orders/:id 订单详情
|
||||
POST /api/admin/orders/:id/cancel 取消订单
|
||||
|
||||
# 订单操作(H5/个人客户)
|
||||
POST /api/h5/orders 个人客户创建订单
|
||||
GET /api/h5/orders 我的订单列表
|
||||
GET /api/h5/orders/:id 订单详情
|
||||
POST /api/h5/orders/:id/pay 钱包支付
|
||||
|
||||
# 支付回调
|
||||
POST /api/callback/wechat-pay 微信支付回调
|
||||
POST /api/callback/alipay 支付宝回调
|
||||
```
|
||||
|
||||
## Risks / Trade-offs
|
||||
|
||||
### 风险 1:并发支付
|
||||
|
||||
**风险**:同一订单被重复支付
|
||||
|
||||
**缓解**:
|
||||
- 支付前检查订单状态
|
||||
- 使用数据库乐观锁或 Redis 分布式锁
|
||||
- 支付回调幂等处理
|
||||
|
||||
### 风险 2:套餐生效失败
|
||||
|
||||
**风险**:支付成功但套餐生效失败
|
||||
|
||||
**缓解**:
|
||||
- 使用事务保证支付和套餐生效原子性
|
||||
- 失败时自动退款或人工处理
|
||||
- 记录详细日志便于排查
|
||||
|
||||
### 风险 3:价格不一致
|
||||
|
||||
**风险**:下单时和支付时套餐价格变化
|
||||
|
||||
**缓解**:
|
||||
- 订单中存储下单时的价格快照
|
||||
- 支付时使用订单金额,不重新查询套餐价格
|
||||
|
||||
## Open Questions
|
||||
|
||||
1. **订单超时取消?**
|
||||
- 当前设计:不自动取消
|
||||
- 待确认:是否需要定时任务取消超时未支付订单?
|
||||
|
||||
2. **部分支付?**
|
||||
- 当前设计:不支持
|
||||
- 待确认:是否需要支持钱包余额不足时组合支付?
|
||||
|
||||
3. **代理为终端用户购买?**
|
||||
- 当前设计:代理只能为自己店铺购买
|
||||
- 待确认:是否需要代理帮终端用户购买的场景?
|
||||
70
openspec/changes/add-order-payment/proposal.md
Normal file
70
openspec/changes/add-order-payment/proposal.md
Normal file
@@ -0,0 +1,70 @@
|
||||
## Why
|
||||
|
||||
Phase 3 完成了卡/设备的套餐系列关联,现在需要实现订单和支付流程。核心是"强充"机制:用户不能直接给钱包充值,必须通过购买套餐来充值。这样每笔充值都有对应的套餐购买记录,便于佣金计算和业务追踪。
|
||||
|
||||
## What Changes
|
||||
|
||||
**新增模型:**
|
||||
- `Order`:订单模型,记录套餐购买信息
|
||||
- `OrderItem`:订单明细(支持一个订单购买多个套餐)
|
||||
|
||||
**Order 核心字段:**
|
||||
- 订单号、订单类型(单卡购买/设备购买)
|
||||
- 买家信息(个人客户/代理店铺)
|
||||
- 关联的卡/设备 ID
|
||||
- 支付金额、支付状态、支付方式
|
||||
- 佣金计算状态
|
||||
|
||||
**强充业务流程:**
|
||||
1. 用户选择套餐,创建订单
|
||||
2. 用户支付(微信/支付宝/钱包余额)
|
||||
3. 支付成功后,套餐生效,流量额度增加
|
||||
4. 触发佣金计算(Phase 5)
|
||||
|
||||
**新增 API:**
|
||||
- 创建套餐购买订单
|
||||
- 查询订单列表/详情
|
||||
- 订单支付(钱包支付)
|
||||
- 支付回调处理
|
||||
- 取消订单
|
||||
|
||||
**业务规则:**
|
||||
- 只能购买卡/设备关联的套餐系列下的套餐
|
||||
- 只能购买已上架且启用的套餐
|
||||
- 设备购买时,套餐分配给设备下所有卡(流量共享)
|
||||
- 订单金额 = 套餐零售价(代理设置的售价)
|
||||
|
||||
## Capabilities
|
||||
|
||||
### New Capabilities
|
||||
|
||||
- `order-management`: 订单管理 - 创建/查询/取消套餐购买订单
|
||||
- `order-payment`: 订单支付 - 钱包支付、第三方支付回调处理
|
||||
- `package-purchase-validation`: 套餐购买验证 - 验证卡/设备是否有权购买指定套餐
|
||||
|
||||
### Modified Capabilities
|
||||
|
||||
<!-- 无 -->
|
||||
|
||||
## Impact
|
||||
|
||||
**代码影响:**
|
||||
- `internal/model/` - 新增 order.go(Order, OrderItem)
|
||||
- `migrations/` - 创建 tb_order, tb_order_item 表
|
||||
- `internal/handler/` - 新增订单 Handler(admin + app/h5)
|
||||
- `internal/service/` - 新增订单 Service
|
||||
- `internal/store/postgres/` - 新增订单 Store
|
||||
- `internal/model/dto/` - 新增订单相关 DTO
|
||||
|
||||
**API 影响:**
|
||||
- 新增 `/api/admin/orders/*` 后台订单管理
|
||||
- 新增 `/api/h5/orders/*` H5 端订单操作
|
||||
- 新增 `/api/app/orders/*` 个人客户订单操作
|
||||
|
||||
**数据库影响:**
|
||||
- 新增表:`tb_order`, `tb_order_item`
|
||||
|
||||
**依赖关系:**
|
||||
- 依赖 Phase 3(add-card-device-series-bindng)完成
|
||||
- Phase 5(一次性佣金)依赖本期
|
||||
- 依赖现有 Wallet 模型
|
||||
@@ -0,0 +1,85 @@
|
||||
## ADDED Requirements
|
||||
|
||||
### Requirement: 创建套餐购买订单
|
||||
|
||||
系统 SHALL 允许买家创建套餐购买订单。订单类型分为单卡购买和设备购买。创建前 MUST 验证购买权限。
|
||||
|
||||
#### Scenario: 个人客户创建单卡订单
|
||||
- **WHEN** 个人客户为自己的卡创建订单,选择一个套餐
|
||||
- **THEN** 系统创建订单,状态为待支付,返回订单信息
|
||||
|
||||
#### Scenario: 个人客户创建设备订单
|
||||
- **WHEN** 个人客户为自己的设备创建订单
|
||||
- **THEN** 系统创建订单,订单类型为设备购买
|
||||
|
||||
#### Scenario: 代理创建订单
|
||||
- **WHEN** 代理为店铺关联的卡/设备创建订单
|
||||
- **THEN** 系统创建订单,买家类型为代理商,买家ID为店铺ID
|
||||
|
||||
#### Scenario: 套餐不在可购买范围
|
||||
- **WHEN** 买家尝试购买不在关联系列下的套餐
|
||||
- **THEN** 系统返回错误 "该套餐不在可购买范围内"
|
||||
|
||||
#### Scenario: 套餐已下架
|
||||
- **WHEN** 买家尝试购买已下架的套餐
|
||||
- **THEN** 系统返回错误 "该套餐已下架"
|
||||
|
||||
---
|
||||
|
||||
### Requirement: 查询订单列表
|
||||
|
||||
系统 SHALL 提供订单列表查询,支持按支付状态、订单类型、时间范围筛选。
|
||||
|
||||
#### Scenario: 个人客户查询自己的订单
|
||||
- **WHEN** 个人客户查询订单列表
|
||||
- **THEN** 系统只返回该客户的订单
|
||||
|
||||
#### Scenario: 代理查询店铺订单
|
||||
- **WHEN** 代理查询订单列表
|
||||
- **THEN** 系统返回该店铺及下级店铺的订单
|
||||
|
||||
#### Scenario: 按支付状态筛选
|
||||
- **WHEN** 指定支付状态筛选
|
||||
- **THEN** 系统只返回匹配状态的订单
|
||||
|
||||
---
|
||||
|
||||
### Requirement: 查询订单详情
|
||||
|
||||
系统 SHALL 允许买家查询订单详情,包含订单明细。
|
||||
|
||||
#### Scenario: 查询订单详情
|
||||
- **WHEN** 买家查询指定订单详情
|
||||
- **THEN** 系统返回订单信息和订单明细列表
|
||||
|
||||
#### Scenario: 查询他人订单
|
||||
- **WHEN** 买家尝试查询不属于自己的订单
|
||||
- **THEN** 系统返回 "订单不存在" 错误
|
||||
|
||||
---
|
||||
|
||||
### Requirement: 取消订单
|
||||
|
||||
系统 SHALL 允许买家取消未支付的订单。
|
||||
|
||||
#### Scenario: 取消待支付订单
|
||||
- **WHEN** 买家取消一个待支付的订单
|
||||
- **THEN** 系统更新订单状态为已取消
|
||||
|
||||
#### Scenario: 取消已支付订单
|
||||
- **WHEN** 买家尝试取消已支付的订单
|
||||
- **THEN** 系统返回错误 "已支付订单无法取消"
|
||||
|
||||
---
|
||||
|
||||
### Requirement: 订单号生成
|
||||
|
||||
系统生成的订单号 MUST 全局唯一,格式为 ORD{YYYYMMDDHHMMSS}{6位随机数}。
|
||||
|
||||
#### Scenario: 订单号格式
|
||||
- **WHEN** 创建新订单
|
||||
- **THEN** 订单号格式为 ORD + 14位时间戳 + 6位随机数
|
||||
|
||||
#### Scenario: 订单号唯一
|
||||
- **WHEN** 并发创建多个订单
|
||||
- **THEN** 每个订单的订单号都唯一
|
||||
@@ -0,0 +1,75 @@
|
||||
## ADDED Requirements
|
||||
|
||||
### Requirement: 钱包支付
|
||||
|
||||
系统 SHALL 支持使用钱包余额支付订单。支付成功后 MUST 扣减钱包余额并激活套餐。
|
||||
|
||||
#### Scenario: 钱包余额充足
|
||||
- **WHEN** 买家使用钱包支付,余额充足
|
||||
- **THEN** 系统扣减钱包余额,更新订单状态为已支付,创建套餐使用记录
|
||||
|
||||
#### Scenario: 钱包余额不足
|
||||
- **WHEN** 买家使用钱包支付,余额不足
|
||||
- **THEN** 系统返回错误 "钱包余额不足"
|
||||
|
||||
#### Scenario: 订单已支付
|
||||
- **WHEN** 买家尝试支付已支付的订单
|
||||
- **THEN** 系统返回错误 "订单已支付"
|
||||
|
||||
#### Scenario: 订单已取消
|
||||
- **WHEN** 买家尝试支付已取消的订单
|
||||
- **THEN** 系统返回错误 "订单已取消"
|
||||
|
||||
---
|
||||
|
||||
### Requirement: 第三方支付回调
|
||||
|
||||
系统 SHALL 处理微信支付和支付宝的支付回调。回调处理 MUST 幂等。
|
||||
|
||||
#### Scenario: 微信支付成功回调
|
||||
- **WHEN** 收到微信支付成功回调
|
||||
- **THEN** 系统验证签名,更新订单状态,激活套餐,返回成功响应
|
||||
|
||||
#### Scenario: 支付宝成功回调
|
||||
- **WHEN** 收到支付宝支付成功回调
|
||||
- **THEN** 系统验证签名,更新订单状态,激活套餐,返回成功响应
|
||||
|
||||
#### Scenario: 重复回调
|
||||
- **WHEN** 收到已处理订单的重复回调
|
||||
- **THEN** 系统返回成功响应,不重复处理
|
||||
|
||||
#### Scenario: 签名验证失败
|
||||
- **WHEN** 回调签名验证失败
|
||||
- **THEN** 系统拒绝处理,返回失败响应
|
||||
|
||||
---
|
||||
|
||||
### Requirement: 套餐激活
|
||||
|
||||
支付成功后系统 MUST 激活套餐,创建 PackageUsage 记录。
|
||||
|
||||
#### Scenario: 单卡套餐激活
|
||||
- **WHEN** 单卡订单支付成功
|
||||
- **THEN** 系统创建 PackageUsage,usage_type 为 single_card,关联 iot_card_id
|
||||
|
||||
#### Scenario: 设备套餐激活
|
||||
- **WHEN** 设备订单支付成功
|
||||
- **THEN** 系统创建 PackageUsage,usage_type 为 device,关联 device_id
|
||||
|
||||
#### Scenario: 套餐有效期计算
|
||||
- **WHEN** 套餐激活
|
||||
- **THEN** 有效期 = 激活时间 + 套餐时长(月)
|
||||
|
||||
---
|
||||
|
||||
### Requirement: 支付事务保证
|
||||
|
||||
钱包支付 MUST 在事务中完成:余额扣减、订单状态更新、套餐激活。任一步骤失败则全部回滚。
|
||||
|
||||
#### Scenario: 事务成功
|
||||
- **WHEN** 所有步骤成功
|
||||
- **THEN** 事务提交,支付完成
|
||||
|
||||
#### Scenario: 余额扣减后套餐激活失败
|
||||
- **WHEN** 余额扣减成功但套餐激活失败
|
||||
- **THEN** 事务回滚,余额恢复,订单状态不变
|
||||
@@ -0,0 +1,67 @@
|
||||
## ADDED Requirements
|
||||
|
||||
### Requirement: 验证卡/设备的套餐购买权限
|
||||
|
||||
创建订单前系统 MUST 验证卡/设备是否有权购买指定套餐。
|
||||
|
||||
#### Scenario: 卡有套餐系列关联
|
||||
- **WHEN** 卡的 series_allocation_id 有值,且套餐属于该系列
|
||||
- **THEN** 验证通过
|
||||
|
||||
#### Scenario: 卡无套餐系列关联
|
||||
- **WHEN** 卡的 series_allocation_id 为空
|
||||
- **THEN** 验证失败,返回 "该卡未关联套餐系列"
|
||||
|
||||
#### Scenario: 套餐不属于关联系列
|
||||
- **WHEN** 套餐的 series_id 与卡关联的分配系列不匹配
|
||||
- **THEN** 验证失败,返回 "该套餐不在可购买范围内"
|
||||
|
||||
#### Scenario: 系列分配已禁用
|
||||
- **WHEN** 卡关联的系列分配状态为禁用
|
||||
- **THEN** 验证失败,返回 "套餐系列已禁用"
|
||||
|
||||
---
|
||||
|
||||
### Requirement: 验证套餐状态
|
||||
|
||||
创建订单前系统 MUST 验证套餐处于可购买状态。
|
||||
|
||||
#### Scenario: 套餐启用且上架
|
||||
- **WHEN** 套餐 status=1 且 shelf_status=1
|
||||
- **THEN** 验证通过
|
||||
|
||||
#### Scenario: 套餐已禁用
|
||||
- **WHEN** 套餐 status=2
|
||||
- **THEN** 验证失败,返回 "套餐已禁用"
|
||||
|
||||
#### Scenario: 套餐已下架
|
||||
- **WHEN** 套餐 shelf_status=2
|
||||
- **THEN** 验证失败,返回 "套餐已下架"
|
||||
|
||||
---
|
||||
|
||||
### Requirement: 获取购买价格
|
||||
|
||||
系统 MUST 根据买家身份返回正确的购买价格。
|
||||
|
||||
#### Scenario: 个人客户购买
|
||||
- **WHEN** 个人客户购买套餐
|
||||
- **THEN** 使用 Package.suggested_retail_price 作为支付金额
|
||||
|
||||
#### Scenario: 代理为店铺购买
|
||||
- **WHEN** 代理为自己店铺购买套餐(囤货/测试)
|
||||
- **THEN** 使用代理的成本价作为支付金额
|
||||
|
||||
---
|
||||
|
||||
### Requirement: 设备购买时的卡验证
|
||||
|
||||
设备购买套餐时 MUST 使用设备的 series_allocation_id 验证,不使用设备下单卡的关联。
|
||||
|
||||
#### Scenario: 设备有系列关联
|
||||
- **WHEN** 设备的 series_allocation_id 有值
|
||||
- **THEN** 使用设备的关联验证购买权限
|
||||
|
||||
#### Scenario: 设备无系列关联
|
||||
- **WHEN** 设备的 series_allocation_id 为空
|
||||
- **THEN** 验证失败,返回 "该设备未关联套餐系列"
|
||||
105
openspec/changes/add-order-payment/tasks.md
Normal file
105
openspec/changes/add-order-payment/tasks.md
Normal file
@@ -0,0 +1,105 @@
|
||||
## 1. 新增模型
|
||||
|
||||
- [ ] 1.1 创建 `internal/model/order.go`,定义 Order 模型(order_no, order_type, buyer_type, buyer_id, iot_card_id, device_id, total_amount, payment_method, payment_status, paid_at, commission_status)
|
||||
- [ ] 1.2 定义 OrderItem 模型(order_id, package_id, package_name, quantity, unit_price, amount)
|
||||
|
||||
## 2. 数据库迁移
|
||||
|
||||
- [ ] 2.1 创建迁移文件,创建 tb_order 表
|
||||
- [ ] 2.2 创建 tb_order_item 表
|
||||
- [ ] 2.3 添加索引(order_no 唯一索引, buyer_type+buyer_id, payment_status, iot_card_id, device_id)
|
||||
- [ ] 2.4 本地执行迁移验证
|
||||
|
||||
## 3. 订单 DTO
|
||||
|
||||
- [ ] 3.1 创建 `internal/model/dto/order.go`,定义 CreateOrderRequest(order_type, iot_card_id/device_id, package_ids)
|
||||
- [ ] 3.2 定义 OrderListRequest(payment_status, order_type, start_time, end_time, page, page_size)
|
||||
- [ ] 3.3 定义 PayOrderRequest(payment_method)
|
||||
- [ ] 3.4 定义 OrderResponse(包含订单信息和明细列表)
|
||||
- [ ] 3.5 定义 OrderItemResponse
|
||||
|
||||
## 4. 订单 Store
|
||||
|
||||
- [ ] 4.1 创建 `internal/store/postgres/order_store.go`,实现 Create 方法(事务创建订单和明细)
|
||||
- [ ] 4.2 实现 GetByID 方法(含明细)
|
||||
- [ ] 4.3 实现 GetByOrderNo 方法
|
||||
- [ ] 4.4 实现 Update 方法
|
||||
- [ ] 4.5 实现 List 方法(支持分页和筛选)
|
||||
- [ ] 4.6 实现 UpdatePaymentStatus 方法
|
||||
- [ ] 4.7 实现 GenerateOrderNo 方法(ORD + 时间戳 + 随机数)
|
||||
|
||||
## 5. 订单明细 Store
|
||||
|
||||
- [ ] 5.1 创建 `internal/store/postgres/order_item_store.go`,实现 BatchCreate 方法
|
||||
- [ ] 5.2 实现 ListByOrderID 方法
|
||||
|
||||
## 6. 购买验证 Service
|
||||
|
||||
- [ ] 6.1 创建 `internal/service/purchase_validation/service.go`,实现 ValidateCardPurchase 方法
|
||||
- [ ] 6.2 实现 ValidateDevicePurchase 方法
|
||||
- [ ] 6.3 实现 ValidatePackageStatus 方法
|
||||
- [ ] 6.4 实现 GetPurchasePrice 方法(根据买家身份返回价格)
|
||||
|
||||
## 7. 订单 Service
|
||||
|
||||
- [ ] 7.1 创建 `internal/service/order/service.go`,实现 Create 方法(验证权限、创建订单和明细)
|
||||
- [ ] 7.2 实现 Get 方法
|
||||
- [ ] 7.3 实现 List 方法
|
||||
- [ ] 7.4 实现 Cancel 方法(验证状态、更新为已取消)
|
||||
- [ ] 7.5 实现 WalletPay 方法(事务:扣减余额、更新状态、激活套餐)
|
||||
- [ ] 7.6 实现 HandlePaymentCallback 方法(验证签名、幂等处理、激活套餐)
|
||||
- [ ] 7.7 实现 ActivatePackage 辅助方法(创建 PackageUsage 记录)
|
||||
|
||||
## 8. 订单 Handler(后台)
|
||||
|
||||
- [ ] 8.1 创建 `internal/handler/admin/order.go`,实现 Create 接口
|
||||
- [ ] 8.2 实现 Get 接口
|
||||
- [ ] 8.3 实现 List 接口
|
||||
- [ ] 8.4 实现 Cancel 接口
|
||||
|
||||
## 9. 订单 Handler(H5)
|
||||
|
||||
- [ ] 9.1 创建 `internal/handler/h5/order.go`,实现 Create 接口
|
||||
- [ ] 9.2 实现 Get 接口
|
||||
- [ ] 9.3 实现 List 接口
|
||||
- [ ] 9.4 实现 WalletPay 接口
|
||||
|
||||
## 10. 支付回调 Handler
|
||||
|
||||
- [ ] 10.1 创建 `internal/handler/callback/payment.go`,实现 WechatPayCallback 接口
|
||||
- [ ] 10.2 实现 AlipayCallback 接口
|
||||
|
||||
## 11. Bootstrap 注册
|
||||
|
||||
- [ ] 11.1 在 stores.go 中注册 OrderStore, OrderItemStore
|
||||
- [ ] 11.2 在 services.go 中注册 PurchaseValidationService, OrderService
|
||||
- [ ] 11.3 在 handlers.go 中注册 AdminOrderHandler, H5OrderHandler, PaymentCallbackHandler
|
||||
|
||||
## 12. 路由注册
|
||||
|
||||
- [ ] 12.1 注册 `/api/admin/orders` 路由组(POST, GET, GET/:id, POST/:id/cancel)
|
||||
- [ ] 12.2 注册 `/api/h5/orders` 路由组(POST, GET, GET/:id, POST/:id/pay)
|
||||
- [ ] 12.3 注册 `/api/callback/wechat-pay` 回调路由
|
||||
- [ ] 12.4 注册 `/api/callback/alipay` 回调路由
|
||||
|
||||
## 13. 文档生成器更新
|
||||
|
||||
- [ ] 13.1 在 docs.go 和 gendocs/main.go 中添加新 Handler
|
||||
- [ ] 13.2 执行文档生成验证
|
||||
|
||||
## 14. 测试
|
||||
|
||||
- [ ] 14.1 OrderStore 单元测试
|
||||
- [ ] 14.2 PurchaseValidationService 单元测试(覆盖各种验证场景)
|
||||
- [ ] 14.3 OrderService 单元测试(覆盖创建、支付、取消)
|
||||
- [ ] 14.4 WalletPay 事务测试(覆盖成功和失败回滚)
|
||||
- [ ] 14.5 订单创建 API 集成测试
|
||||
- [ ] 14.6 钱包支付 API 集成测试
|
||||
- [ ] 14.7 执行 `go test ./...` 确认通过
|
||||
|
||||
## 15. 最终验证
|
||||
|
||||
- [ ] 15.1 执行 `go build ./...` 确认编译通过
|
||||
- [ ] 15.2 启动服务,手动测试订单创建流程
|
||||
- [ ] 15.3 手动测试钱包支付流程
|
||||
- [ ] 15.4 验证套餐激活(PackageUsage 记录创建)
|
||||
@@ -0,0 +1,2 @@
|
||||
schema: spec-driven
|
||||
created: 2026-01-27
|
||||
217
openspec/changes/add-shop-package-allocation/design.md
Normal file
217
openspec/changes/add-shop-package-allocation/design.md
Normal file
@@ -0,0 +1,217 @@
|
||||
## Context
|
||||
|
||||
Phase 1 完成了套餐系列和套餐的基础管理,但代理商还不能分销套餐。本期实现代理套餐分配机制,使上级代理能够:
|
||||
1. 为下级店铺分配可销售的套餐系列
|
||||
2. 通过加价模式设置下级的成本价
|
||||
3. 配置梯度佣金(基于销量/销售额的阶梯奖励)
|
||||
|
||||
**当前代理层级结构**:
|
||||
- 店铺通过 `Shop.parent_id` 维护层级关系
|
||||
- 最多 7 级代理
|
||||
- 数据权限通过 `GetSubordinateShopIDs()` 递归查询
|
||||
|
||||
## Goals / Non-Goals
|
||||
|
||||
**Goals:**
|
||||
- 实现套餐系列级别的分配机制
|
||||
- 支持固定金额和百分比两种加价模式
|
||||
- 支持梯度佣金配置(月度/季度/年度/自定义时间范围)
|
||||
- 代理能查看自己被分配的套餐及成本价
|
||||
- 可选的单套餐级别成本价覆盖
|
||||
|
||||
**Non-Goals:**
|
||||
- 不实现卡/设备的套餐系列关联(Phase 3)
|
||||
- 不实现订单支付流程(Phase 4)
|
||||
- 不实现佣金计算逻辑(Phase 5)
|
||||
- 不支持跨级分配(只能分配给直属下级)
|
||||
|
||||
## Decisions
|
||||
|
||||
### 1. 分配模型设计
|
||||
|
||||
**决策**:三个独立模型
|
||||
|
||||
```go
|
||||
// ShopSeriesAllocation 店铺套餐系列分配
|
||||
type ShopSeriesAllocation struct {
|
||||
gorm.Model
|
||||
BaseModel
|
||||
ShopID uint // 被分配的店铺 ID
|
||||
SeriesID uint // 套餐系列 ID
|
||||
AllocatorShopID uint // 分配者店铺 ID(上级)
|
||||
PricingMode string // 加价模式: fixed-固定金额 percent-百分比
|
||||
PricingValue int64 // 加价值(分或千分比)
|
||||
OneTimeCommissionTrigger string // 一次性佣金触发类型: one_time_recharge-单次充值 accumulated_recharge-累计充值
|
||||
OneTimeCommissionThreshold int64 // 一次性佣金触发阈值(分)
|
||||
OneTimeCommissionAmount int64 // 一次性佣金金额(分)
|
||||
Status int // 状态 1-启用 2-禁用
|
||||
}
|
||||
|
||||
// ShopSeriesCommissionTier 梯度佣金配置
|
||||
type ShopSeriesCommissionTier struct {
|
||||
gorm.Model
|
||||
BaseModel
|
||||
AllocationID uint // 关联的分配 ID
|
||||
TierType string // 梯度类型: sales_count-销量 sales_amount-销售额
|
||||
PeriodType string // 周期类型: monthly-月度 quarterly-季度 yearly-年度 custom-自定义
|
||||
PeriodStartDate *time.Time // 自定义周期开始日期
|
||||
PeriodEndDate *time.Time // 自定义周期结束日期
|
||||
ThresholdValue int64 // 阈值(销量或金额)
|
||||
CommissionAmount int64 // 佣金金额(分)
|
||||
}
|
||||
|
||||
// ShopPackageAllocation 店铺单套餐分配(可选覆盖)
|
||||
type ShopPackageAllocation struct {
|
||||
gorm.Model
|
||||
BaseModel
|
||||
ShopID uint // 被分配的店铺 ID
|
||||
PackageID uint // 套餐 ID
|
||||
AllocationID uint // 关联的系列分配 ID
|
||||
CostPrice int64 // 覆盖的成本价(分)
|
||||
Status int // 状态 1-启用 2-禁用
|
||||
}
|
||||
```
|
||||
|
||||
**理由**:
|
||||
- 系列级别分配是主要方式,减少配置工作量
|
||||
- 单套餐分配用于特殊场景(如某个套餐给特定代理优惠价)
|
||||
- 梯度佣金独立模型,支持多档配置
|
||||
|
||||
### 2. 加价模式与成本价计算
|
||||
|
||||
**决策**:成本价 = 上级成本价 + 加价值
|
||||
|
||||
```
|
||||
# 固定金额加价
|
||||
下级成本价 = 上级成本价 + pricing_value
|
||||
|
||||
# 百分比加价(pricing_value 为千分比,如 100 = 10%)
|
||||
下级成本价 = 上级成本价 × (1 + pricing_value / 1000)
|
||||
```
|
||||
|
||||
**理由**:
|
||||
- 基于上级成本价加价,确保每级都有利润空间
|
||||
- 千分比精度满足业务需求(0.1% 精度)
|
||||
- 平台作为顶级,其成本价 = Package.suggested_cost_price
|
||||
|
||||
**约束**:
|
||||
- 下级成本价 ≥ 上级成本价(禁止负加价)
|
||||
- 验证时需递归获取上级成本价
|
||||
|
||||
### 3. 成本价获取逻辑
|
||||
|
||||
**决策**:递归查询 + 缓存
|
||||
|
||||
```go
|
||||
func GetCostPrice(shopID, packageID uint) int64 {
|
||||
// 1. 检查是否有单套餐覆盖
|
||||
if override := GetPackageAllocation(shopID, packageID); override != nil {
|
||||
return override.CostPrice
|
||||
}
|
||||
|
||||
// 2. 获取系列分配
|
||||
allocation := GetSeriesAllocation(shopID, package.SeriesID)
|
||||
if allocation == nil {
|
||||
return 0 // 未分配,不可购买
|
||||
}
|
||||
|
||||
// 3. 获取上级成本价
|
||||
parentCostPrice := GetParentCostPrice(allocation.AllocatorShopID, packageID)
|
||||
|
||||
// 4. 计算当前成本价
|
||||
return CalculatePrice(parentCostPrice, allocation.PricingMode, allocation.PricingValue)
|
||||
}
|
||||
```
|
||||
|
||||
**理由**:
|
||||
- 单套餐覆盖优先级最高
|
||||
- 递归到平台级别时,使用 Package.suggested_cost_price
|
||||
- 可考虑缓存热点套餐的成本价(后续优化)
|
||||
|
||||
### 4. 梯度佣金周期计算
|
||||
|
||||
**决策**:支持固定周期和自定义周期
|
||||
|
||||
| PeriodType | 计算方式 |
|
||||
|------------|----------|
|
||||
| monthly | 当月 1 日 00:00 至月末 23:59:59 |
|
||||
| quarterly | 当季度第一天至最后一天 |
|
||||
| yearly | 当年 1 月 1 日至 12 月 31 日 |
|
||||
| custom | PeriodStartDate 至 PeriodEndDate |
|
||||
|
||||
**理由**:
|
||||
- 固定周期覆盖常见场景
|
||||
- 自定义周期支持促销活动等特殊需求
|
||||
|
||||
### 5. API 设计
|
||||
|
||||
**决策**:RESTful + 嵌套资源
|
||||
|
||||
```
|
||||
# 套餐系列分配
|
||||
POST /api/admin/shop-series-allocations 为下级分配系列
|
||||
GET /api/admin/shop-series-allocations 查询分配列表
|
||||
GET /api/admin/shop-series-allocations/:id 分配详情
|
||||
PUT /api/admin/shop-series-allocations/:id 更新分配
|
||||
DELETE /api/admin/shop-series-allocations/:id 删除分配
|
||||
PATCH /api/admin/shop-series-allocations/:id/status 启用/禁用
|
||||
|
||||
# 梯度佣金(嵌套在分配下)
|
||||
POST /api/admin/shop-series-allocations/:id/tiers 添加梯度
|
||||
GET /api/admin/shop-series-allocations/:id/tiers 梯度列表
|
||||
PUT /api/admin/shop-series-allocations/:id/tiers/:tierId 更新梯度
|
||||
DELETE /api/admin/shop-series-allocations/:id/tiers/:tierId 删除梯度
|
||||
|
||||
# 单套餐分配
|
||||
POST /api/admin/shop-package-allocations 分配单套餐
|
||||
GET /api/admin/shop-package-allocations 查询列表
|
||||
PUT /api/admin/shop-package-allocations/:id 更新
|
||||
DELETE /api/admin/shop-package-allocations/:id 删除
|
||||
|
||||
# 代理可售套餐
|
||||
GET /api/admin/my-packages 查询我的可售套餐
|
||||
GET /api/admin/my-packages/:id 套餐详情(含成本价)
|
||||
```
|
||||
|
||||
## Risks / Trade-offs
|
||||
|
||||
### 风险 1:递归成本价计算性能
|
||||
|
||||
**风险**:多级代理场景下,递归查询成本价可能较慢
|
||||
|
||||
**缓解**:
|
||||
- 首期不做缓存,观察实际性能
|
||||
- 如有问题,后续增加 Redis 缓存(按 shop_id + package_id 缓存)
|
||||
- 缓存失效策略:分配变更时清除相关缓存
|
||||
|
||||
### 风险 2:分配一致性
|
||||
|
||||
**风险**:上级删除分配后,下级的分配关系如何处理
|
||||
|
||||
**缓解**:
|
||||
- 删除分配时检查是否有下级依赖
|
||||
- 如有下级依赖,禁止删除或级联禁用
|
||||
- 本期采用禁止删除策略,要求先清理下级分配
|
||||
|
||||
### 风险 3:梯度佣金统计复杂度
|
||||
|
||||
**风险**:统计周期内的销量/销售额可能涉及大量数据
|
||||
|
||||
**缓解**:
|
||||
- 佣金计算在 Phase 5 实现
|
||||
- 可考虑定时任务预计算周期统计数据
|
||||
- 本期只做配置,不做实际统计
|
||||
|
||||
## Open Questions
|
||||
|
||||
1. **是否支持批量分配?**
|
||||
- 当前设计:单个分配
|
||||
- 待确认:是否需要批量为多个下级分配同一系列?
|
||||
|
||||
2. **分配删除策略?**
|
||||
- 当前设计:有下级依赖时禁止删除
|
||||
- 待确认:是否需要级联删除或级联禁用?
|
||||
|
||||
3. **梯度佣金是否可叠加?**
|
||||
- 当前设计:达到最高档位只拿最高档佣金
|
||||
- 待确认:是否需要累加所有达标档位的佣金?
|
||||
61
openspec/changes/add-shop-package-allocation/proposal.md
Normal file
61
openspec/changes/add-shop-package-allocation/proposal.md
Normal file
@@ -0,0 +1,61 @@
|
||||
## Why
|
||||
|
||||
Phase 1 完成了套餐基础模块,但代理商还不能分销套餐。需要实现代理套餐分配机制:上级代理为下级分配套餐系列,设置成本价(通过加价模式计算),并支持梯度佣金配置。代理只能看到和销售被分配的套餐。
|
||||
|
||||
## What Changes
|
||||
|
||||
**新增模型:**
|
||||
- `ShopSeriesAllocation`:店铺套餐系列分配,记录哪个店铺被分配了哪个套餐系列、成本价加价模式、**一次性佣金触发配置**
|
||||
- `ShopSeriesCommissionTier`:梯度佣金配置,基于销量/销售额设置不同的阶梯奖励金额
|
||||
- `ShopPackageAllocation`:店铺单套餐分配(可选),用于覆盖系列级别的成本价设置
|
||||
|
||||
**新增 API:**
|
||||
- 为下级店铺分配套餐系列(设置加价模式)
|
||||
- 查询店铺的套餐系列分配列表
|
||||
- 更新/删除套餐系列分配
|
||||
- 配置梯度佣金(按系列)
|
||||
- 为下级店铺分配单个套餐(覆盖成本价)
|
||||
- 代理查看自己可销售的套餐列表(含成本价)
|
||||
|
||||
**业务规则:**
|
||||
- 加价模式:固定金额加价 或 百分比加价(基于上级成本价)
|
||||
- 代理给下级设置的成本价 ≥ 自己的成本价(不可亏本)
|
||||
- 梯度佣金支持时间范围配置(月度/季度/年度/自定义)
|
||||
- 套餐系列分配是主要方式,单套餐分配用于特殊覆盖
|
||||
|
||||
## Capabilities
|
||||
|
||||
### New Capabilities
|
||||
|
||||
- `shop-series-allocation`: 店铺套餐系列分配 - 为下级店铺分配套餐系列,设置加价模式计算成本价
|
||||
- `shop-commission-tier`: 梯度佣金配置 - 基于销量/销售额配置不同档位的一次性佣金
|
||||
- `shop-package-allocation`: 店铺单套餐分配 - 可选的单套餐级别成本价覆盖
|
||||
- `agent-available-packages`: 代理可售套餐查询 - 代理查看自己被分配的套餐及成本价
|
||||
|
||||
### Modified Capabilities
|
||||
|
||||
<!-- 无 -->
|
||||
|
||||
## Impact
|
||||
|
||||
**代码影响:**
|
||||
- `internal/model/` - 新增 3 个模型文件
|
||||
- `migrations/` - 创建 3 个新表
|
||||
- `internal/handler/admin/` - 新增分配管理 Handler
|
||||
- `internal/service/` - 新增分配管理 Service
|
||||
- `internal/store/postgres/` - 新增 3 个 Store
|
||||
- `internal/model/dto/` - 新增请求/响应 DTO
|
||||
- `internal/bootstrap/` - 注册新组件
|
||||
- `internal/router/` - 注册新路由
|
||||
|
||||
**API 影响:**
|
||||
- 新增 `/api/admin/shop-series-allocations/*` 路由组
|
||||
- 新增 `/api/admin/shop-package-allocations/*` 路由组
|
||||
- 新增 `/api/admin/my-packages` 代理可售套餐查询
|
||||
|
||||
**数据库影响:**
|
||||
- 新增表:`tb_shop_series_allocation`, `tb_shop_series_commission_tier`, `tb_shop_package_allocation`
|
||||
|
||||
**依赖关系:**
|
||||
- 依赖 Phase 1(add-package-module)完成
|
||||
- Phase 3(卡/设备关联)依赖本期
|
||||
@@ -0,0 +1,65 @@
|
||||
## ADDED Requirements
|
||||
|
||||
### Requirement: 查询代理可售套餐列表
|
||||
|
||||
系统 SHALL 允许代理查询自己被分配的所有套餐。结果 MUST 包含套餐信息和代理的成本价。支持按套餐系列筛选、按套餐类型筛选。
|
||||
|
||||
#### Scenario: 查询所有可售套餐
|
||||
- **WHEN** 代理查询可售套餐列表
|
||||
- **THEN** 系统返回该代理被分配的所有套餐系列下的启用且上架的套餐
|
||||
|
||||
#### Scenario: 响应包含成本价
|
||||
- **WHEN** 代理查询可售套餐
|
||||
- **THEN** 每个套餐包含:套餐信息、建议售价、代理成本价、利润空间
|
||||
|
||||
#### Scenario: 按系列筛选
|
||||
- **WHEN** 代理指定套餐系列 ID 筛选
|
||||
- **THEN** 系统只返回该系列下的套餐
|
||||
|
||||
#### Scenario: 只返回可售套餐
|
||||
- **WHEN** 代理查询可售套餐
|
||||
- **THEN** 系统只返回状态为启用(1)且上架状态为上架(1)的套餐
|
||||
|
||||
---
|
||||
|
||||
### Requirement: 查询代理可售套餐详情
|
||||
|
||||
系统 SHALL 允许代理查询单个套餐的详细信息,包含完整的价格信息。
|
||||
|
||||
#### Scenario: 查询可售套餐详情
|
||||
- **WHEN** 代理查询指定套餐的详情
|
||||
- **THEN** 系统返回套餐完整信息,包含:成本价、建议售价、价格来源(系列加价/单套餐覆盖)
|
||||
|
||||
#### Scenario: 查询未分配的套餐
|
||||
- **WHEN** 代理查询一个未被分配的套餐详情
|
||||
- **THEN** 系统返回 "您没有该套餐的销售权限" 错误
|
||||
|
||||
---
|
||||
|
||||
### Requirement: 成本价计算优先级
|
||||
|
||||
系统计算代理成本价时 MUST 遵循以下优先级:
|
||||
1. 单套餐覆盖价(如果存在且启用)
|
||||
2. 系列级别加价计算
|
||||
|
||||
#### Scenario: 存在单套餐覆盖
|
||||
- **WHEN** 代理查询一个有覆盖价的套餐
|
||||
- **THEN** 成本价使用覆盖价,价格来源标记为 "单套餐覆盖"
|
||||
|
||||
#### Scenario: 使用系列加价
|
||||
- **WHEN** 代理查询一个无覆盖价的套餐
|
||||
- **THEN** 成本价 = 上级成本价 + 加价值,价格来源标记为 "系列加价"
|
||||
|
||||
---
|
||||
|
||||
### Requirement: 查询代理被分配的套餐系列
|
||||
|
||||
系统 SHALL 允许代理查询自己被分配的套餐系列列表。
|
||||
|
||||
#### Scenario: 查询被分配的系列
|
||||
- **WHEN** 代理查询自己的套餐系列分配
|
||||
- **THEN** 系统返回所有分配给该代理的套餐系列(启用状态的)
|
||||
|
||||
#### Scenario: 响应包含系列下套餐数量
|
||||
- **WHEN** 代理查询被分配的系列
|
||||
- **THEN** 每个系列包含:系列信息、可售套餐数量、加价模式信息
|
||||
@@ -0,0 +1,77 @@
|
||||
## ADDED Requirements
|
||||
|
||||
### Requirement: 配置梯度佣金
|
||||
|
||||
系统 SHALL 允许代理为套餐系列分配配置梯度佣金。每个梯度包含:梯度类型(销量/销售额)、周期类型、阈值、佣金金额。
|
||||
|
||||
#### Scenario: 添加销量梯度佣金
|
||||
- **WHEN** 代理为分配添加梯度:类型=销量,周期=月度,阈值=100,佣金=5000分
|
||||
- **THEN** 系统创建梯度配置,当下级月销量达到 100 时可获得 50 元佣金
|
||||
|
||||
#### Scenario: 添加销售额梯度佣金
|
||||
- **WHEN** 代理添加梯度:类型=销售额,周期=季度,阈值=100000分,佣金=10000分
|
||||
- **THEN** 系统创建梯度配置,当下级季度销售额达到 1000 元时可获得 100 元佣金
|
||||
|
||||
#### Scenario: 配置自定义周期
|
||||
- **WHEN** 代理添加梯度,周期类型=自定义,指定开始和结束日期
|
||||
- **THEN** 系统创建梯度配置,统计指定日期范围内的数据
|
||||
|
||||
#### Scenario: 添加多个梯度档位
|
||||
- **WHEN** 代理为同一分配添加多个梯度(如:100件=50元,200件=120元,500件=350元)
|
||||
- **THEN** 系统创建多个梯度记录,支持阶梯奖励
|
||||
|
||||
---
|
||||
|
||||
### Requirement: 查询梯度佣金配置
|
||||
|
||||
系统 SHALL 提供梯度佣金配置的查询功能,按分配 ID 查询。
|
||||
|
||||
#### Scenario: 查询分配的梯度配置
|
||||
- **WHEN** 代理查询指定分配的梯度配置
|
||||
- **THEN** 系统返回该分配下的所有梯度配置,按阈值升序排列
|
||||
|
||||
#### Scenario: 分配无梯度配置
|
||||
- **WHEN** 代理查询一个没有配置梯度的分配
|
||||
- **THEN** 系统返回空列表
|
||||
|
||||
---
|
||||
|
||||
### Requirement: 更新梯度佣金配置
|
||||
|
||||
系统 SHALL 允许代理更新梯度配置的阈值和佣金金额。
|
||||
|
||||
#### Scenario: 更新梯度阈值
|
||||
- **WHEN** 代理将梯度阈值从 100 改为 150
|
||||
- **THEN** 系统更新梯度记录
|
||||
|
||||
#### Scenario: 更新梯度佣金金额
|
||||
- **WHEN** 代理将佣金金额从 5000 改为 6000
|
||||
- **THEN** 系统更新梯度记录
|
||||
|
||||
---
|
||||
|
||||
### Requirement: 删除梯度佣金配置
|
||||
|
||||
系统 SHALL 允许代理删除梯度配置。
|
||||
|
||||
#### Scenario: 删除梯度配置
|
||||
- **WHEN** 代理删除指定的梯度配置
|
||||
- **THEN** 系统软删除该梯度记录
|
||||
|
||||
---
|
||||
|
||||
### Requirement: 梯度佣金周期类型
|
||||
|
||||
系统 MUST 支持以下周期类型:
|
||||
- monthly:月度(当月 1 日至月末)
|
||||
- quarterly:季度(当季第一天至最后一天)
|
||||
- yearly:年度(1 月 1 日至 12 月 31 日)
|
||||
- custom:自定义(指定开始和结束日期)
|
||||
|
||||
#### Scenario: 月度周期
|
||||
- **WHEN** 配置月度周期的梯度
|
||||
- **THEN** 统计范围为当月 1 日 00:00:00 至月末 23:59:59
|
||||
|
||||
#### Scenario: 自定义周期必填日期
|
||||
- **WHEN** 代理选择自定义周期但未提供开始或结束日期
|
||||
- **THEN** 系统返回参数验证错误
|
||||
@@ -0,0 +1,65 @@
|
||||
## ADDED Requirements
|
||||
|
||||
### Requirement: 为下级店铺分配单个套餐
|
||||
|
||||
系统 SHALL 允许代理为下级店铺的特定套餐设置覆盖成本价。此功能用于对单个套餐给予特殊定价,优先级高于系列级别的加价计算。
|
||||
|
||||
#### Scenario: 成功分配单套餐覆盖价
|
||||
- **WHEN** 代理为下级的某个套餐设置覆盖成本价 8000 分
|
||||
- **THEN** 系统创建单套餐分配记录,该下级购买此套餐时成本价为 8000 分(不再使用系列加价计算)
|
||||
|
||||
#### Scenario: 覆盖价低于上级成本价
|
||||
- **WHEN** 代理尝试设置的覆盖价低于自己的成本价
|
||||
- **THEN** 系统返回错误 "覆盖价不能低于您的成本价"
|
||||
|
||||
#### Scenario: 套餐未在系列分配中
|
||||
- **WHEN** 代理尝试为一个未分配系列下的套餐设置覆盖价
|
||||
- **THEN** 系统返回错误 "该套餐的系列未分配给此店铺"
|
||||
|
||||
---
|
||||
|
||||
### Requirement: 查询单套餐分配列表
|
||||
|
||||
系统 SHALL 提供单套餐分配的查询功能,支持按店铺、套餐、状态筛选。
|
||||
|
||||
#### Scenario: 查询店铺的单套餐分配
|
||||
- **WHEN** 代理查询指定店铺的单套餐分配列表
|
||||
- **THEN** 系统返回该店铺的所有单套餐覆盖配置
|
||||
|
||||
#### Scenario: 查询结果包含套餐信息
|
||||
- **WHEN** 代理查询单套餐分配列表
|
||||
- **THEN** 响应包含套餐名称、套餐编码、原计算成本价、覆盖成本价
|
||||
|
||||
---
|
||||
|
||||
### Requirement: 更新单套餐分配
|
||||
|
||||
系统 SHALL 允许代理更新单套餐分配的覆盖成本价。
|
||||
|
||||
#### Scenario: 更新覆盖成本价
|
||||
- **WHEN** 代理将覆盖成本价从 8000 改为 7500
|
||||
- **THEN** 系统更新记录,下级的该套餐成本价变为 7500
|
||||
|
||||
---
|
||||
|
||||
### Requirement: 删除单套餐分配
|
||||
|
||||
系统 SHALL 允许代理删除单套餐分配。删除后恢复使用系列级别的加价计算。
|
||||
|
||||
#### Scenario: 删除单套餐覆盖
|
||||
- **WHEN** 代理删除单套餐分配记录
|
||||
- **THEN** 系统软删除记录,下级的该套餐成本价恢复为系列加价计算值
|
||||
|
||||
---
|
||||
|
||||
### Requirement: 单套餐分配状态管理
|
||||
|
||||
系统 SHALL 允许代理启用/禁用单套餐分配。禁用后恢复使用系列级别价格。
|
||||
|
||||
#### Scenario: 禁用单套餐覆盖
|
||||
- **WHEN** 代理禁用单套餐分配
|
||||
- **THEN** 该套餐暂时使用系列级别的加价计算
|
||||
|
||||
#### Scenario: 启用单套餐覆盖
|
||||
- **WHEN** 代理启用已禁用的单套餐分配
|
||||
- **THEN** 该套餐恢复使用覆盖成本价
|
||||
@@ -0,0 +1,103 @@
|
||||
## ADDED Requirements
|
||||
|
||||
### Requirement: 为下级店铺分配套餐系列
|
||||
|
||||
系统 SHALL 允许代理为其直属下级店铺分配套餐系列。分配时 MUST 指定加价模式(固定金额或百分比)和加价值。可选配置一次性佣金触发条件(触发类型、阈值、金额)。分配者只能分配自己已被分配的套餐系列。
|
||||
|
||||
#### Scenario: 成功分配套餐系列
|
||||
- **WHEN** 代理为直属下级店铺分配一个自己拥有的套餐系列,设置固定金额加价 1000 分
|
||||
- **THEN** 系统创建分配记录,下级成本价 = 上级成本价 + 1000
|
||||
|
||||
#### Scenario: 百分比加价分配
|
||||
- **WHEN** 代理设置百分比加价模式,加价值为 100(10%)
|
||||
- **THEN** 系统创建分配记录,下级成本价 = 上级成本价 × 1.1
|
||||
|
||||
#### Scenario: 尝试分配未拥有的系列
|
||||
- **WHEN** 代理尝试分配自己未被分配的套餐系列
|
||||
- **THEN** 系统返回错误 "您没有该套餐系列的分配权限"
|
||||
|
||||
#### Scenario: 尝试分配给非直属下级
|
||||
- **WHEN** 代理尝试分配给非直属下级店铺
|
||||
- **THEN** 系统返回错误 "只能为直属下级分配套餐"
|
||||
|
||||
#### Scenario: 重复分配同一系列
|
||||
- **WHEN** 代理尝试为同一下级店铺重复分配同一套餐系列
|
||||
- **THEN** 系统返回错误 "该店铺已分配此套餐系列"
|
||||
|
||||
#### Scenario: 配置一次性佣金触发条件
|
||||
- **WHEN** 代理分配时设置一次性佣金触发类型为"单次充值",阈值 30000 分,金额 5000 分
|
||||
- **THEN** 系统创建分配记录,下级的卡/设备在单次充值 ≥ 300 元时可获得 50 元一次性佣金
|
||||
|
||||
#### Scenario: 配置累计充值触发条件
|
||||
- **WHEN** 代理分配时设置一次性佣金触发类型为"累计充值",阈值 50000 分,金额 8000 分
|
||||
- **THEN** 系统创建分配记录,下级的卡/设备在累计充值 ≥ 500 元时可获得 80 元一次性佣金
|
||||
|
||||
---
|
||||
|
||||
### Requirement: 查询套餐系列分配列表
|
||||
|
||||
系统 SHALL 提供分配列表查询,支持按下级店铺筛选、按套餐系列筛选、按状态筛选。结果 MUST 包含计算后的成本价。
|
||||
|
||||
#### Scenario: 查询所有分配
|
||||
- **WHEN** 代理查询分配列表,不带筛选条件
|
||||
- **THEN** 系统返回该代理创建的所有分配记录
|
||||
|
||||
#### Scenario: 按店铺筛选
|
||||
- **WHEN** 代理指定下级店铺 ID 筛选
|
||||
- **THEN** 系统只返回该店铺的分配记录
|
||||
|
||||
#### Scenario: 响应包含成本价
|
||||
- **WHEN** 代理查询分配列表
|
||||
- **THEN** 每条记录包含计算后的下级成本价
|
||||
|
||||
---
|
||||
|
||||
### Requirement: 更新套餐系列分配
|
||||
|
||||
系统 SHALL 允许代理更新分配的加价模式和加价值。更新后下级的成本价 MUST 同步变化。
|
||||
|
||||
#### Scenario: 更新加价值
|
||||
- **WHEN** 代理将加价值从 1000 改为 2000
|
||||
- **THEN** 系统更新分配记录,下级成本价相应增加
|
||||
|
||||
#### Scenario: 更新不存在的分配
|
||||
- **WHEN** 代理更新不存在的分配 ID
|
||||
- **THEN** 系统返回 "分配记录不存在" 错误
|
||||
|
||||
---
|
||||
|
||||
### Requirement: 删除套餐系列分配
|
||||
|
||||
系统 SHALL 允许代理删除分配记录。如果有下级依赖此分配,MUST 禁止删除。
|
||||
|
||||
#### Scenario: 成功删除无依赖的分配
|
||||
- **WHEN** 代理删除一个没有下级依赖的分配记录
|
||||
- **THEN** 系统软删除该记录
|
||||
|
||||
#### Scenario: 尝试删除有下级依赖的分配
|
||||
- **WHEN** 代理尝试删除一个已被下级使用的分配(下级基于此分配又分配给了更下级)
|
||||
- **THEN** 系统返回错误 "存在下级依赖,无法删除"
|
||||
|
||||
---
|
||||
|
||||
### Requirement: 启用/禁用套餐系列分配
|
||||
|
||||
系统 SHALL 允许代理切换分配的启用状态。禁用后下级 MUST NOT 能使用该分配购买套餐。
|
||||
|
||||
#### Scenario: 禁用分配
|
||||
- **WHEN** 代理将分配状态设为禁用
|
||||
- **THEN** 系统更新状态,下级无法基于此分配购买套餐
|
||||
|
||||
#### Scenario: 启用分配
|
||||
- **WHEN** 代理将禁用的分配设为启用
|
||||
- **THEN** 系统更新状态,下级可以继续使用
|
||||
|
||||
---
|
||||
|
||||
### Requirement: 平台分配套餐系列
|
||||
|
||||
平台管理员 SHALL 能够为一级代理分配套餐系列。平台的成本价基准为 Package.suggested_cost_price。
|
||||
|
||||
#### Scenario: 平台为一级代理分配
|
||||
- **WHEN** 平台管理员为一级代理分配套餐系列
|
||||
- **THEN** 系统创建分配记录,一级代理成本价 = suggested_cost_price + 加价值
|
||||
167
openspec/changes/add-shop-package-allocation/tasks.md
Normal file
167
openspec/changes/add-shop-package-allocation/tasks.md
Normal file
@@ -0,0 +1,167 @@
|
||||
## 1. 新增模型
|
||||
|
||||
- [ ] 1.1 创建 `internal/model/shop_series_allocation.go`,定义 ShopSeriesAllocation 模型(shop_id, series_id, allocator_shop_id, pricing_mode, pricing_value, one_time_commission_trigger, one_time_commission_threshold, one_time_commission_amount, status)
|
||||
- [ ] 1.2 创建 `internal/model/shop_series_commission_tier.go`,定义 ShopSeriesCommissionTier 模型(allocation_id, tier_type, period_type, period_start_date, period_end_date, threshold_value, commission_amount)
|
||||
- [ ] 1.3 创建 `internal/model/shop_package_allocation.go`,定义 ShopPackageAllocation 模型(shop_id, package_id, allocation_id, cost_price, status)
|
||||
|
||||
## 2. 数据库迁移
|
||||
|
||||
- [ ] 2.1 创建迁移文件,创建 tb_shop_series_allocation 表
|
||||
- [ ] 2.2 创建 tb_shop_series_commission_tier 表
|
||||
- [ ] 2.3 创建 tb_shop_package_allocation 表
|
||||
- [ ] 2.4 添加必要的索引(shop_id, series_id, allocation_id)
|
||||
- [ ] 2.5 本地执行迁移验证
|
||||
|
||||
## 3. 套餐系列分配 DTO
|
||||
|
||||
- [ ] 3.1 创建 `internal/model/dto/shop_series_allocation.go`,定义 CreateShopSeriesAllocationRequest(含 one_time_commission_trigger, one_time_commission_threshold, one_time_commission_amount 可选字段)
|
||||
- [ ] 3.2 定义 UpdateShopSeriesAllocationRequest
|
||||
- [ ] 3.3 定义 ShopSeriesAllocationListRequest(支持 shop_id, series_id, status 筛选)
|
||||
- [ ] 3.4 定义 UpdateStatusRequest
|
||||
- [ ] 3.5 定义 ShopSeriesAllocationResponse(包含计算后的成本价)
|
||||
|
||||
## 4. 梯度佣金 DTO
|
||||
|
||||
- [ ] 4.1 定义 CreateCommissionTierRequest(tier_type, period_type, period_start_date, period_end_date, threshold_value, commission_amount)
|
||||
- [ ] 4.2 定义 UpdateCommissionTierRequest
|
||||
- [ ] 4.3 定义 CommissionTierResponse
|
||||
|
||||
## 5. 单套餐分配 DTO
|
||||
|
||||
- [ ] 5.1 创建 `internal/model/dto/shop_package_allocation.go`,定义 CreateShopPackageAllocationRequest
|
||||
- [ ] 5.2 定义 UpdateShopPackageAllocationRequest
|
||||
- [ ] 5.3 定义 ShopPackageAllocationListRequest
|
||||
- [ ] 5.4 定义 ShopPackageAllocationResponse
|
||||
|
||||
## 6. 代理可售套餐 DTO
|
||||
|
||||
- [ ] 6.1 定义 MyPackageListRequest(series_id, package_type 筛选)
|
||||
- [ ] 6.2 定义 MyPackageResponse(包含成本价、建议售价、价格来源)
|
||||
- [ ] 6.3 定义 MySeriesAllocationResponse
|
||||
|
||||
## 7. 套餐系列分配 Store
|
||||
|
||||
- [ ] 7.1 创建 `internal/store/postgres/shop_series_allocation_store.go`,实现 Create 方法
|
||||
- [ ] 7.2 实现 GetByID 方法
|
||||
- [ ] 7.3 实现 GetByShopAndSeries 方法(检查重复分配)
|
||||
- [ ] 7.4 实现 Update 方法
|
||||
- [ ] 7.5 实现 Delete 方法
|
||||
- [ ] 7.6 实现 List 方法(支持分页和筛选)
|
||||
- [ ] 7.7 实现 UpdateStatus 方法
|
||||
- [ ] 7.8 实现 HasDependentAllocations 方法(检查下级依赖)
|
||||
- [ ] 7.9 实现 GetByShopID 方法(获取店铺的所有分配)
|
||||
|
||||
## 8. 梯度佣金 Store
|
||||
|
||||
- [ ] 8.1 创建 `internal/store/postgres/shop_series_commission_tier_store.go`,实现 Create 方法
|
||||
- [ ] 8.2 实现 GetByID 方法
|
||||
- [ ] 8.3 实现 Update 方法
|
||||
- [ ] 8.4 实现 Delete 方法
|
||||
- [ ] 8.5 实现 ListByAllocationID 方法
|
||||
|
||||
## 9. 单套餐分配 Store
|
||||
|
||||
- [ ] 9.1 创建 `internal/store/postgres/shop_package_allocation_store.go`,实现 Create 方法
|
||||
- [ ] 9.2 实现 GetByID 方法
|
||||
- [ ] 9.3 实现 GetByShopAndPackage 方法
|
||||
- [ ] 9.4 实现 Update 方法
|
||||
- [ ] 9.5 实现 Delete 方法
|
||||
- [ ] 9.6 实现 List 方法
|
||||
- [ ] 9.7 实现 UpdateStatus 方法
|
||||
|
||||
## 10. 套餐系列分配 Service
|
||||
|
||||
- [ ] 10.1 创建 `internal/service/shop_series_allocation/service.go`,实现 Create 方法(验证权限、检查重复、计算成本价)
|
||||
- [ ] 10.2 实现 Get 方法
|
||||
- [ ] 10.3 实现 Update 方法
|
||||
- [ ] 10.4 实现 Delete 方法(检查下级依赖)
|
||||
- [ ] 10.5 实现 List 方法
|
||||
- [ ] 10.6 实现 UpdateStatus 方法
|
||||
- [ ] 10.7 实现 GetParentCostPrice 辅助方法(递归获取上级成本价)
|
||||
- [ ] 10.8 实现 CalculateCostPrice 辅助方法(根据加价模式计算)
|
||||
|
||||
## 11. 梯度佣金 Service
|
||||
|
||||
- [ ] 11.1 在 shop_series_allocation service 中实现 AddTier 方法
|
||||
- [ ] 11.2 实现 UpdateTier 方法
|
||||
- [ ] 11.3 实现 DeleteTier 方法
|
||||
- [ ] 11.4 实现 ListTiers 方法
|
||||
|
||||
## 12. 单套餐分配 Service
|
||||
|
||||
- [ ] 12.1 创建 `internal/service/shop_package_allocation/service.go`,实现 Create 方法(验证系列已分配、验证成本价)
|
||||
- [ ] 12.2 实现 Get 方法
|
||||
- [ ] 12.3 实现 Update 方法
|
||||
- [ ] 12.4 实现 Delete 方法
|
||||
- [ ] 12.5 实现 List 方法
|
||||
- [ ] 12.6 实现 UpdateStatus 方法
|
||||
|
||||
## 13. 代理可售套餐 Service
|
||||
|
||||
- [ ] 13.1 创建 `internal/service/my_package/service.go`,实现 ListMyPackages 方法(获取可售套餐列表)
|
||||
- [ ] 13.2 实现 GetMyPackage 方法(获取单个套餐详情含成本价)
|
||||
- [ ] 13.3 实现 ListMySeriesAllocations 方法(获取被分配的系列)
|
||||
- [ ] 13.4 实现 GetCostPrice 核心方法(成本价计算,考虑优先级)
|
||||
|
||||
## 14. 套餐系列分配 Handler
|
||||
|
||||
- [ ] 14.1 创建 `internal/handler/admin/shop_series_allocation.go`,实现 Create 接口
|
||||
- [ ] 14.2 实现 Get 接口
|
||||
- [ ] 14.3 实现 Update 接口
|
||||
- [ ] 14.4 实现 Delete 接口
|
||||
- [ ] 14.5 实现 List 接口
|
||||
- [ ] 14.6 实现 UpdateStatus 接口
|
||||
- [ ] 14.7 实现 AddTier 接口
|
||||
- [ ] 14.8 实现 UpdateTier 接口
|
||||
- [ ] 14.9 实现 DeleteTier 接口
|
||||
- [ ] 14.10 实现 ListTiers 接口
|
||||
|
||||
## 15. 单套餐分配 Handler
|
||||
|
||||
- [ ] 15.1 创建 `internal/handler/admin/shop_package_allocation.go`,实现 Create 接口
|
||||
- [ ] 15.2 实现 Get 接口
|
||||
- [ ] 15.3 实现 Update 接口
|
||||
- [ ] 15.4 实现 Delete 接口
|
||||
- [ ] 15.5 实现 List 接口
|
||||
- [ ] 15.6 实现 UpdateStatus 接口
|
||||
|
||||
## 16. 代理可售套餐 Handler
|
||||
|
||||
- [ ] 16.1 创建 `internal/handler/admin/my_package.go`,实现 ListMyPackages 接口
|
||||
- [ ] 16.2 实现 GetMyPackage 接口
|
||||
- [ ] 16.3 实现 ListMySeriesAllocations 接口
|
||||
|
||||
## 17. Bootstrap 注册
|
||||
|
||||
- [ ] 17.1 在 stores.go 中注册 ShopSeriesAllocationStore, ShopSeriesCommissionTierStore, ShopPackageAllocationStore
|
||||
- [ ] 17.2 在 services.go 中注册 ShopSeriesAllocationService, ShopPackageAllocationService, MyPackageService
|
||||
- [ ] 17.3 在 handlers.go 中注册 ShopSeriesAllocationHandler, ShopPackageAllocationHandler, MyPackageHandler
|
||||
|
||||
## 18. 路由注册
|
||||
|
||||
- [ ] 18.1 注册 `/api/admin/shop-series-allocations` 路由组
|
||||
- [ ] 18.2 注册 `/api/admin/shop-series-allocations/:id/tiers` 嵌套路由
|
||||
- [ ] 18.3 注册 `/api/admin/shop-package-allocations` 路由组
|
||||
- [ ] 18.4 注册 `/api/admin/my-packages` 路由
|
||||
- [ ] 18.5 注册 `/api/admin/my-series-allocations` 路由
|
||||
|
||||
## 19. 文档生成器更新
|
||||
|
||||
- [ ] 19.1 在 docs.go 和 gendocs/main.go 中添加新 Handler
|
||||
- [ ] 19.2 执行文档生成验证
|
||||
|
||||
## 20. 测试
|
||||
|
||||
- [ ] 20.1 ShopSeriesAllocationStore 单元测试
|
||||
- [ ] 20.2 ShopPackageAllocationStore 单元测试
|
||||
- [ ] 20.3 ShopSeriesAllocationService 单元测试(覆盖权限验证、成本价计算)
|
||||
- [ ] 20.4 MyPackageService 单元测试(覆盖成本价优先级)
|
||||
- [ ] 20.5 套餐系列分配 API 集成测试
|
||||
- [ ] 20.6 代理可售套餐 API 集成测试
|
||||
- [ ] 20.7 执行 `go test ./...` 确认通过
|
||||
|
||||
## 21. 最终验证
|
||||
|
||||
- [ ] 21.1 执行 `go build ./...` 确认编译通过
|
||||
- [ ] 21.2 启动服务,手动测试分配流程
|
||||
- [ ] 21.3 验证成本价计算逻辑正确
|
||||
@@ -0,0 +1,2 @@
|
||||
schema: spec-driven
|
||||
created: 2026-01-27
|
||||
199
openspec/changes/archive/2026-01-27-add-package-module/design.md
Normal file
199
openspec/changes/archive/2026-01-27-add-package-module/design.md
Normal file
@@ -0,0 +1,199 @@
|
||||
## 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 模型字段设计
|
||||
|
||||
**决策**:新增三个字段
|
||||
|
||||
```go
|
||||
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 层统一处理状态变更逻辑
|
||||
- 添加单元测试覆盖所有状态组合
|
||||
|
||||
### 风险 3:API 命名与现有冲突
|
||||
|
||||
**风险**:`/packages` 路径可能与未来其他套餐类型冲突
|
||||
|
||||
**缓解**:
|
||||
- 当前只有流量卡套餐,命名合理
|
||||
- 未来如有号卡套餐,可使用 `/number-card-packages`
|
||||
|
||||
## Migration Plan
|
||||
|
||||
### 部署步骤
|
||||
|
||||
1. **代码部署前**:
|
||||
- 确认生产环境废弃表无数据
|
||||
- 备份数据库(预防措施)
|
||||
|
||||
2. **执行迁移**:
|
||||
```bash
|
||||
go run cmd/migrate/main.go up
|
||||
```
|
||||
|
||||
3. **验证**:
|
||||
- 确认 8 个表已删除
|
||||
- 确认 tb_package 新增 3 个字段
|
||||
- API 健康检查
|
||||
|
||||
### 回滚策略
|
||||
|
||||
```bash
|
||||
go run cmd/migrate/main.go down
|
||||
```
|
||||
|
||||
迁移 down 脚本:
|
||||
- 重建 8 个废弃表(结构保留)
|
||||
- 删除 tb_package 的 3 个新字段
|
||||
|
||||
**注意**:回滚不恢复数据,仅恢复表结构
|
||||
|
||||
## Open Questions
|
||||
|
||||
1. **套餐系列禁用是否级联影响套餐?**
|
||||
- 当前设计:不级联,套餐系列禁用只影响系列本身
|
||||
- 待确认:是否需要禁用系列时自动禁用下属套餐?
|
||||
|
||||
2. **删除套餐/套餐系列的约束?**
|
||||
- 当前设计:物理删除(soft delete via GORM)
|
||||
- 待确认:是否需要检查关联数据(如已分配给代理的套餐)?
|
||||
- 建议:Phase 2 实现代理分配后再添加约束检查
|
||||
@@ -0,0 +1,63 @@
|
||||
## Why
|
||||
|
||||
当前分佣模型过于复杂(包含冻结/解冻审批、组合分佣、号卡结算等),而流量卡业务只需要简单的一次性佣金。现有的 `AgentPackageAllocation` 模型也不支持套餐系列级别的分配和梯度佣金配置。需要清理废弃模型,调整 Package 模型支持建议价格和上架状态,并提供完整的套餐/套餐系列 CRUD API。
|
||||
|
||||
## What Changes
|
||||
|
||||
**模型清理(commission.go):**
|
||||
- **BREAKING** 删除 `AgentHierarchy` - 代理层级通过 `Shop.parent_id` 维护
|
||||
- **BREAKING** 删除 `CommissionRule` - 过于复杂,后续用新模型替代
|
||||
- **BREAKING** 删除 `CommissionLadder` - 后续用 `ShopSeriesCommissionTier` 替代
|
||||
- **BREAKING** 删除 `CommissionCombinedCondition` - 流量卡不需要组合分佣
|
||||
- **BREAKING** 删除 `CommissionApproval` - 不需要冻结/解冻审批流程
|
||||
- **BREAKING** 删除 `CommissionTemplate` - 简化后不需要模板
|
||||
- **BREAKING** 删除 `CarrierSettlement` - 号卡专用,本期不做
|
||||
|
||||
**模型清理(package.go):**
|
||||
- **BREAKING** 删除 `AgentPackageAllocation` - 用新的分配模型替代
|
||||
|
||||
**Package 模型调整:**
|
||||
- 新增 `suggested_cost_price` 字段(建议成本价,分为单位)
|
||||
- 新增 `suggested_retail_price` 字段(建议售价,分为单位)
|
||||
- 新增 `shelf_status` 字段(上架状态:1-上架 2-下架)
|
||||
|
||||
**新增 API:**
|
||||
- 套餐系列 CRUD(创建、更新、删除、列表、详情、启用/禁用)
|
||||
- 套餐 CRUD(创建、更新、删除、列表、详情、启用/禁用、上架/下架)
|
||||
|
||||
## Capabilities
|
||||
|
||||
### New Capabilities
|
||||
|
||||
- `package-series-management`: 套餐系列管理 - 创建/更新/删除/列表/详情,支持启用/禁用状态切换
|
||||
- `package-management`: 套餐管理 - 创建/更新/删除/列表/详情,支持启用/禁用和上架/下架双状态管理
|
||||
|
||||
### Modified Capabilities
|
||||
|
||||
<!-- 无需修改现有 capability,这是全新的套餐管理模块 -->
|
||||
|
||||
## Impact
|
||||
|
||||
**代码影响:**
|
||||
- `internal/model/commission.go` - 删除 7 个模型
|
||||
- `internal/model/package.go` - 删除 1 个模型,修改 Package 模型
|
||||
- `migrations/` - 需要创建迁移文件删除废弃表、修改 package 表
|
||||
- `internal/handler/admin/` - 新增套餐系列和套餐管理 Handler
|
||||
- `internal/service/` - 新增套餐系列和套餐管理 Service
|
||||
- `internal/store/postgres/` - 新增套餐系列和套餐 Store
|
||||
- `internal/model/dto/` - 新增请求/响应 DTO
|
||||
- `internal/bootstrap/` - 注册新的 Store/Service/Handler
|
||||
- `internal/router/` - 注册新的 API 路由
|
||||
- `cmd/api/docs.go` 和 `cmd/gendocs/main.go` - 更新文档生成器
|
||||
|
||||
**API 影响:**
|
||||
- 新增 `/api/admin/package-series/*` 路由组
|
||||
- 新增 `/api/admin/packages/*` 路由组
|
||||
|
||||
**数据库影响:**
|
||||
- 删除表:`tb_agent_hierarchy`, `tb_commission_rule`, `tb_commission_ladder`, `tb_commission_combined_condition`, `tb_commission_approval`, `tb_commission_template`, `tb_carrier_settlement`, `tb_agent_package_allocation`
|
||||
- 修改表:`tb_package` 新增 3 个字段
|
||||
|
||||
**依赖关系:**
|
||||
- 本期不涉及外部依赖变更
|
||||
- 后续 Phase 2(代理套餐分配)依赖本期完成
|
||||
@@ -0,0 +1,180 @@
|
||||
## ADDED Requirements
|
||||
|
||||
### Requirement: 创建套餐
|
||||
|
||||
系统 SHALL 允许平台管理员创建套餐,包含套餐编码、套餐名称、所属系列、套餐类型、时长、流量配置、价格和建议价格。套餐编码 MUST 全局唯一(排除已删除记录)。新创建的套餐默认为启用状态(1)和下架状态(2)。
|
||||
|
||||
#### Scenario: 成功创建套餐
|
||||
- **WHEN** 管理员提交有效的套餐信息
|
||||
- **THEN** 系统创建套餐记录,状态为启用(1),上架状态为下架(2),返回创建的套餐详情
|
||||
|
||||
#### Scenario: 套餐编码重复
|
||||
- **WHEN** 管理员提交的套餐编码已存在(未删除)
|
||||
- **THEN** 系统返回错误 "套餐编码已存在"
|
||||
|
||||
#### Scenario: 关联不存在的套餐系列
|
||||
- **WHEN** 管理员指定的系列 ID 不存在
|
||||
- **THEN** 系统返回错误 "套餐系列不存在"
|
||||
|
||||
#### Scenario: 缺少必填字段
|
||||
- **WHEN** 管理员未提供必填字段(套餐编码、套餐名称、套餐类型、时长、价格)
|
||||
- **THEN** 系统返回参数验证错误
|
||||
|
||||
---
|
||||
|
||||
### Requirement: 查询套餐列表
|
||||
|
||||
系统 SHALL 提供套餐列表查询功能,支持按套餐名称模糊搜索、按系列 ID 筛选、按状态筛选、按上架状态筛选、按套餐类型筛选。结果 MUST 分页返回,按创建时间倒序排列。
|
||||
|
||||
#### Scenario: 查询所有套餐
|
||||
- **WHEN** 管理员请求套餐列表,不带筛选条件
|
||||
- **THEN** 系统返回所有未删除的套餐,分页显示
|
||||
|
||||
#### Scenario: 按系列筛选
|
||||
- **WHEN** 管理员指定套餐系列 ID
|
||||
- **THEN** 系统只返回属于该系列的套餐
|
||||
|
||||
#### Scenario: 按名称搜索
|
||||
- **WHEN** 管理员提供套餐名称关键字
|
||||
- **THEN** 系统返回名称包含该关键字的套餐
|
||||
|
||||
#### Scenario: 按状态筛选
|
||||
- **WHEN** 管理员指定启用状态
|
||||
- **THEN** 系统只返回匹配启用状态的套餐
|
||||
|
||||
#### Scenario: 按上架状态筛选
|
||||
- **WHEN** 管理员指定上架状态
|
||||
- **THEN** 系统只返回匹配上架状态的套餐
|
||||
|
||||
#### Scenario: 按套餐类型筛选
|
||||
- **WHEN** 管理员指定套餐类型(formal/addon)
|
||||
- **THEN** 系统只返回匹配类型的套餐
|
||||
|
||||
---
|
||||
|
||||
### Requirement: 查询套餐详情
|
||||
|
||||
系统 SHALL 允许管理员查询单个套餐的详细信息。
|
||||
|
||||
#### Scenario: 查询存在的套餐
|
||||
- **WHEN** 管理员请求指定 ID 的套餐详情
|
||||
- **THEN** 系统返回该套餐的完整信息
|
||||
|
||||
#### Scenario: 查询不存在的套餐
|
||||
- **WHEN** 管理员请求不存在或已删除的套餐 ID
|
||||
- **THEN** 系统返回 "套餐不存在" 错误
|
||||
|
||||
---
|
||||
|
||||
### Requirement: 更新套餐
|
||||
|
||||
系统 SHALL 允许管理员更新套餐的基本信息。套餐编码创建后 MUST NOT 允许修改。
|
||||
|
||||
#### Scenario: 成功更新套餐
|
||||
- **WHEN** 管理员提交有效的更新信息
|
||||
- **THEN** 系统更新套餐记录,返回更新后的详情
|
||||
|
||||
#### Scenario: 尝试修改套餐编码
|
||||
- **WHEN** 管理员尝试修改套餐编码
|
||||
- **THEN** 系统忽略套餐编码字段,不进行修改
|
||||
|
||||
#### Scenario: 更新不存在的套餐
|
||||
- **WHEN** 管理员更新不存在的套餐
|
||||
- **THEN** 系统返回 "套餐不存在" 错误
|
||||
|
||||
#### Scenario: 关联不存在的套餐系列
|
||||
- **WHEN** 管理员将套餐关联到不存在的系列
|
||||
- **THEN** 系统返回错误 "套餐系列不存在"
|
||||
|
||||
---
|
||||
|
||||
### Requirement: 删除套餐
|
||||
|
||||
系统 SHALL 允许管理员删除套餐(软删除)。
|
||||
|
||||
#### Scenario: 成功删除套餐
|
||||
- **WHEN** 管理员删除指定的套餐
|
||||
- **THEN** 系统软删除该记录,后续查询不再返回
|
||||
|
||||
#### Scenario: 删除不存在的套餐
|
||||
- **WHEN** 管理员删除不存在的套餐
|
||||
- **THEN** 系统返回 "套餐不存在" 错误
|
||||
|
||||
---
|
||||
|
||||
### Requirement: 启用/禁用套餐
|
||||
|
||||
系统 SHALL 允许管理员切换套餐的启用状态。禁用套餐时 MUST 同时将上架状态设置为下架。
|
||||
|
||||
#### Scenario: 启用套餐
|
||||
- **WHEN** 管理员将禁用的套餐设置为启用
|
||||
- **THEN** 系统更新状态为启用(1),上架状态保持不变
|
||||
|
||||
#### Scenario: 禁用套餐
|
||||
- **WHEN** 管理员将启用的套餐设置为禁用
|
||||
- **THEN** 系统更新状态为禁用(2),同时将上架状态设置为下架(2)
|
||||
|
||||
#### Scenario: 禁用已上架的套餐
|
||||
- **WHEN** 管理员禁用一个当前已上架的套餐
|
||||
- **THEN** 系统更新状态为禁用(2),上架状态强制设置为下架(2)
|
||||
|
||||
---
|
||||
|
||||
### Requirement: 上架/下架套餐
|
||||
|
||||
系统 SHALL 允许管理员切换套餐的上架状态。只有启用状态的套餐才能上架。
|
||||
|
||||
#### Scenario: 上架启用的套餐
|
||||
- **WHEN** 管理员将启用且下架的套餐设置为上架
|
||||
- **THEN** 系统更新上架状态为上架(1)
|
||||
|
||||
#### Scenario: 尝试上架禁用的套餐
|
||||
- **WHEN** 管理员尝试上架一个禁用的套餐
|
||||
- **THEN** 系统返回错误 "禁用的套餐不能上架,请先启用"
|
||||
|
||||
#### Scenario: 下架套餐
|
||||
- **WHEN** 管理员将上架的套餐设置为下架
|
||||
- **THEN** 系统更新上架状态为下架(2)
|
||||
|
||||
#### Scenario: 状态未变化
|
||||
- **WHEN** 管理员设置的上架状态与当前状态相同
|
||||
- **THEN** 系统正常返回成功,不产生错误
|
||||
|
||||
---
|
||||
|
||||
### Requirement: Package 模型新增字段
|
||||
|
||||
系统 MUST 在 Package 模型中新增以下字段:
|
||||
- `suggested_cost_price`:建议成本价(分为单位),默认 0
|
||||
- `suggested_retail_price`:建议售价(分为单位),默认 0
|
||||
- `shelf_status`:上架状态,1-上架 2-下架,默认 2
|
||||
|
||||
#### Scenario: 创建套餐时设置建议价格
|
||||
- **WHEN** 管理员创建套餐并设置建议成本价和建议售价
|
||||
- **THEN** 系统保存这些价格信息
|
||||
|
||||
#### Scenario: 查询套餐时返回建议价格
|
||||
- **WHEN** 管理员查询套餐详情或列表
|
||||
- **THEN** 响应中包含 suggested_cost_price、suggested_retail_price、shelf_status 字段
|
||||
|
||||
---
|
||||
|
||||
### Requirement: 清理废弃模型
|
||||
|
||||
系统 MUST 删除以下废弃的分佣相关模型和对应的数据库表:
|
||||
- `AgentHierarchy` (tb_agent_hierarchy)
|
||||
- `CommissionRule` (tb_commission_rule)
|
||||
- `CommissionLadder` (tb_commission_ladder)
|
||||
- `CommissionCombinedCondition` (tb_commission_combined_condition)
|
||||
- `CommissionApproval` (tb_commission_approval)
|
||||
- `CommissionTemplate` (tb_commission_template)
|
||||
- `CarrierSettlement` (tb_carrier_settlement)
|
||||
- `AgentPackageAllocation` (tb_agent_package_allocation)
|
||||
|
||||
#### Scenario: 迁移后废弃表不存在
|
||||
- **WHEN** 执行数据库迁移后
|
||||
- **THEN** 上述 8 个表在数据库中不再存在
|
||||
|
||||
#### Scenario: 代码中无废弃模型引用
|
||||
- **WHEN** 删除模型定义后
|
||||
- **THEN** 项目能够正常编译,无编译错误
|
||||
@@ -0,0 +1,99 @@
|
||||
## ADDED Requirements
|
||||
|
||||
### Requirement: 创建套餐系列
|
||||
|
||||
系统 SHALL 允许平台管理员创建套餐系列,包含系列编码、系列名称、描述信息。系列编码 MUST 全局唯一(排除已删除记录)。新创建的套餐系列默认为启用状态。
|
||||
|
||||
#### Scenario: 成功创建套餐系列
|
||||
- **WHEN** 管理员提交有效的套餐系列信息(系列编码、系列名称)
|
||||
- **THEN** 系统创建套餐系列记录,返回创建的套餐系列详情,状态为启用(1)
|
||||
|
||||
#### Scenario: 系列编码重复
|
||||
- **WHEN** 管理员提交的系列编码已存在(未删除)
|
||||
- **THEN** 系统返回错误 "系列编码已存在"
|
||||
|
||||
#### Scenario: 缺少必填字段
|
||||
- **WHEN** 管理员未提供系列编码或系列名称
|
||||
- **THEN** 系统返回参数验证错误
|
||||
|
||||
---
|
||||
|
||||
### Requirement: 查询套餐系列列表
|
||||
|
||||
系统 SHALL 提供套餐系列列表查询功能,支持按系列名称模糊搜索、按状态筛选。结果 MUST 分页返回,按创建时间倒序排列。
|
||||
|
||||
#### Scenario: 查询所有套餐系列
|
||||
- **WHEN** 管理员请求套餐系列列表,不带筛选条件
|
||||
- **THEN** 系统返回所有未删除的套餐系列,分页显示
|
||||
|
||||
#### Scenario: 按名称搜索
|
||||
- **WHEN** 管理员提供系列名称关键字
|
||||
- **THEN** 系统返回名称包含该关键字的套餐系列
|
||||
|
||||
#### Scenario: 按状态筛选
|
||||
- **WHEN** 管理员指定状态筛选(启用/禁用)
|
||||
- **THEN** 系统只返回匹配状态的套餐系列
|
||||
|
||||
---
|
||||
|
||||
### Requirement: 查询套餐系列详情
|
||||
|
||||
系统 SHALL 允许管理员查询单个套餐系列的详细信息。
|
||||
|
||||
#### Scenario: 查询存在的套餐系列
|
||||
- **WHEN** 管理员请求指定 ID 的套餐系列详情
|
||||
- **THEN** 系统返回该套餐系列的完整信息
|
||||
|
||||
#### Scenario: 查询不存在的套餐系列
|
||||
- **WHEN** 管理员请求不存在或已删除的套餐系列 ID
|
||||
- **THEN** 系统返回 "套餐系列不存在" 错误
|
||||
|
||||
---
|
||||
|
||||
### Requirement: 更新套餐系列
|
||||
|
||||
系统 SHALL 允许管理员更新套餐系列的基本信息(系列名称、描述)。系列编码创建后 MUST NOT 允许修改。
|
||||
|
||||
#### Scenario: 成功更新套餐系列
|
||||
- **WHEN** 管理员提交有效的更新信息
|
||||
- **THEN** 系统更新套餐系列记录,返回更新后的详情
|
||||
|
||||
#### Scenario: 尝试修改系列编码
|
||||
- **WHEN** 管理员尝试修改系列编码
|
||||
- **THEN** 系统忽略系列编码字段,不进行修改
|
||||
|
||||
#### Scenario: 更新不存在的套餐系列
|
||||
- **WHEN** 管理员更新不存在的套餐系列
|
||||
- **THEN** 系统返回 "套餐系列不存在" 错误
|
||||
|
||||
---
|
||||
|
||||
### Requirement: 删除套餐系列
|
||||
|
||||
系统 SHALL 允许管理员删除套餐系列(软删除)。
|
||||
|
||||
#### Scenario: 成功删除套餐系列
|
||||
- **WHEN** 管理员删除指定的套餐系列
|
||||
- **THEN** 系统软删除该记录,后续查询不再返回
|
||||
|
||||
#### Scenario: 删除不存在的套餐系列
|
||||
- **WHEN** 管理员删除不存在的套餐系列
|
||||
- **THEN** 系统返回 "套餐系列不存在" 错误
|
||||
|
||||
---
|
||||
|
||||
### Requirement: 启用/禁用套餐系列
|
||||
|
||||
系统 SHALL 允许管理员切换套餐系列的启用状态。
|
||||
|
||||
#### Scenario: 启用套餐系列
|
||||
- **WHEN** 管理员将禁用的套餐系列设置为启用
|
||||
- **THEN** 系统更新状态为启用(1)
|
||||
|
||||
#### Scenario: 禁用套餐系列
|
||||
- **WHEN** 管理员将启用的套餐系列设置为禁用
|
||||
- **THEN** 系统更新状态为禁用(2)
|
||||
|
||||
#### Scenario: 状态未变化
|
||||
- **WHEN** 管理员设置的状态与当前状态相同
|
||||
- **THEN** 系统正常返回成功,不产生错误
|
||||
128
openspec/changes/archive/2026-01-27-add-package-module/tasks.md
Normal file
128
openspec/changes/archive/2026-01-27-add-package-module/tasks.md
Normal file
@@ -0,0 +1,128 @@
|
||||
## 1. 模型清理
|
||||
|
||||
- [x] 1.1 删除 `internal/model/commission.go` 中的废弃模型(AgentHierarchy, CommissionRule, CommissionLadder, CommissionCombinedCondition, CommissionApproval, CommissionTemplate, CarrierSettlement)
|
||||
- [x] 1.2 删除 `internal/model/package.go` 中的 `AgentPackageAllocation` 模型
|
||||
- [x] 1.3 执行 `go build ./...` 确认无编译错误,如有引用则同步清理
|
||||
|
||||
## 2. Package 模型调整
|
||||
|
||||
- [x] 2.1 在 `internal/model/package.go` 的 Package 结构体中新增 `suggested_cost_price` 字段(bigint, 默认 0, 注释:建议成本价)
|
||||
- [x] 2.2 在 Package 结构体中新增 `suggested_retail_price` 字段(bigint, 默认 0, 注释:建议售价)
|
||||
- [x] 2.3 在 Package 结构体中新增 `shelf_status` 字段(int, 默认 2, 注释:上架状态 1-上架 2-下架)
|
||||
|
||||
## 3. 数据库迁移
|
||||
|
||||
- [x] 3.1 创建迁移文件,UP 脚本删除 8 个废弃表:tb_agent_hierarchy, tb_commission_rule, tb_commission_ladder, tb_commission_combined_condition, tb_commission_approval, tb_commission_template, tb_carrier_settlement, tb_agent_package_allocation
|
||||
- [x] 3.2 在迁移 UP 脚本中添加 tb_package 表的 3 个新字段
|
||||
- [x] 3.3 编写迁移 DOWN 脚本(重建表结构、删除新字段)
|
||||
- [x] 3.4 本地执行迁移验证
|
||||
|
||||
## 4. 套餐系列 DTO
|
||||
|
||||
- [x] 4.1 创建 `internal/model/dto/package_series.go`,定义 CreatePackageSeriesRequest(series_code 必填, series_name 必填, description 可选)
|
||||
- [x] 4.2 定义 UpdatePackageSeriesRequest(series_name, description)
|
||||
- [x] 4.3 定义 PackageSeriesListRequest(page, page_size, series_name 模糊, status 筛选)
|
||||
- [x] 4.4 定义 UpdatePackageSeriesStatusRequest(status 必填)
|
||||
- [x] 4.5 定义 PackageSeriesResponse 响应结构
|
||||
|
||||
## 5. 套餐系列 Store
|
||||
|
||||
- [x] 5.1 创建 `internal/store/postgres/package_series_store.go`,实现 Create 方法
|
||||
- [x] 5.2 实现 GetByID 方法
|
||||
- [x] 5.3 实现 GetByCode 方法(用于编码唯一性检查)
|
||||
- [x] 5.4 实现 Update 方法
|
||||
- [x] 5.5 实现 Delete 方法(软删除)
|
||||
- [x] 5.6 实现 List 方法(支持分页、名称模糊搜索、状态筛选)
|
||||
- [x] 5.7 实现 UpdateStatus 方法
|
||||
|
||||
## 6. 套餐系列 Service
|
||||
|
||||
- [x] 6.1 创建 `internal/service/package_series/service.go`,实现 Create 方法(检查编码唯一性)
|
||||
- [x] 6.2 实现 Get 方法
|
||||
- [x] 6.3 实现 Update 方法(忽略编码修改)
|
||||
- [x] 6.4 实现 Delete 方法
|
||||
- [x] 6.5 实现 List 方法
|
||||
- [x] 6.6 实现 UpdateStatus 方法
|
||||
|
||||
## 7. 套餐系列 Handler
|
||||
|
||||
- [x] 7.1 创建 `internal/handler/admin/package_series.go`,实现 Create 接口
|
||||
- [x] 7.2 实现 Get 接口
|
||||
- [x] 7.3 实现 Update 接口
|
||||
- [x] 7.4 实现 Delete 接口
|
||||
- [x] 7.5 实现 List 接口
|
||||
- [x] 7.6 实现 UpdateStatus 接口
|
||||
|
||||
## 8. 套餐 DTO
|
||||
|
||||
- [x] 8.1 创建 `internal/model/dto/package.go`,定义 CreatePackageRequest(package_code 必填, package_name 必填, series_id, package_type 必填, duration_months 必填, data_type, real_data_mb, virtual_data_mb, data_amount_mb, price 必填, suggested_cost_price, suggested_retail_price)
|
||||
- [x] 8.2 定义 UpdatePackageRequest(除 package_code 外的字段)
|
||||
- [x] 8.3 定义 PackageListRequest(page, page_size, package_name 模糊, series_id, status, shelf_status, package_type)
|
||||
- [x] 8.4 定义 UpdatePackageStatusRequest(status 必填)
|
||||
- [x] 8.5 定义 UpdatePackageShelfStatusRequest(shelf_status 必填)
|
||||
- [x] 8.6 定义 PackageResponse 响应结构(包含新增的 3 个字段)
|
||||
|
||||
## 9. 套餐 Store
|
||||
|
||||
- [x] 9.1 创建 `internal/store/postgres/package_store.go`,实现 Create 方法
|
||||
- [x] 9.2 实现 GetByID 方法
|
||||
- [x] 9.3 实现 GetByCode 方法
|
||||
- [x] 9.4 实现 Update 方法
|
||||
- [x] 9.5 实现 Delete 方法
|
||||
- [x] 9.6 实现 List 方法(支持分页、名称模糊、系列筛选、状态筛选、上架状态筛选、类型筛选)
|
||||
- [x] 9.7 实现 UpdateStatus 方法
|
||||
- [x] 9.8 实现 UpdateShelfStatus 方法
|
||||
|
||||
## 10. 套餐 Service
|
||||
|
||||
- [x] 10.1 创建 `internal/service/package/service.go`,实现 Create 方法(检查编码唯一性、验证系列存在)
|
||||
- [x] 10.2 实现 Get 方法
|
||||
- [x] 10.3 实现 Update 方法(忽略编码修改、验证系列存在)
|
||||
- [x] 10.4 实现 Delete 方法
|
||||
- [x] 10.5 实现 List 方法
|
||||
- [x] 10.6 实现 UpdateStatus 方法(禁用时强制下架)
|
||||
- [x] 10.7 实现 UpdateShelfStatus 方法(检查启用状态才能上架)
|
||||
|
||||
## 11. 套餐 Handler
|
||||
|
||||
- [x] 11.1 创建 `internal/handler/admin/package.go`,实现 Create 接口
|
||||
- [x] 11.2 实现 Get 接口
|
||||
- [x] 11.3 实现 Update 接口
|
||||
- [x] 11.4 实现 Delete 接口
|
||||
- [x] 11.5 实现 List 接口
|
||||
- [x] 11.6 实现 UpdateStatus 接口
|
||||
- [x] 11.7 实现 UpdateShelfStatus 接口
|
||||
|
||||
## 12. Bootstrap 注册
|
||||
|
||||
- [x] 12.1 在 `internal/bootstrap/stores.go` 中注册 PackageSeriesStore 和 PackageStore
|
||||
- [x] 12.2 在 `internal/bootstrap/services.go` 中注册 PackageSeriesService 和 PackageService
|
||||
- [x] 12.3 在 `internal/bootstrap/handlers.go` 中注册 PackageSeriesHandler 和 PackageHandler
|
||||
|
||||
## 13. 路由注册
|
||||
|
||||
- [x] 13.1 在 `internal/router/` 中注册套餐系列路由组 `/api/admin/package-series`(POST, GET, GET/:id, PUT/:id, DELETE/:id, PATCH/:id/status)
|
||||
- [x] 13.2 注册套餐路由组 `/api/admin/packages`(POST, GET, GET/:id, PUT/:id, DELETE/:id, PATCH/:id/status, PATCH/:id/shelf)
|
||||
|
||||
## 14. 文档生成器更新
|
||||
|
||||
- [x] 14.1 在 `cmd/api/docs.go` 中添加 PackageSeriesHandler 和 PackageHandler
|
||||
- [x] 14.2 在 `cmd/gendocs/main.go` 中添加 PackageSeriesHandler 和 PackageHandler
|
||||
- [x] 14.3 执行 `go run cmd/gendocs/main.go` 生成 OpenAPI 文档
|
||||
|
||||
## 15. 测试
|
||||
|
||||
- [x] 15.1 为 PackageSeriesStore 编写单元测试
|
||||
- [x] 15.2 为 PackageStore 编写单元测试
|
||||
- [x] 15.3 为 PackageSeriesService 编写单元测试(覆盖编码唯一性检查)
|
||||
- [x] 15.4 为 PackageService 编写单元测试(覆盖双状态逻辑)
|
||||
- [x] 15.5 编写套餐系列 API 集成测试
|
||||
- [x] 15.6 编写套餐 API 集成测试(覆盖禁用强制下架、禁用不能上架场景)
|
||||
- [x] 15.7 执行 `go test ./...` 确认所有测试通过
|
||||
|
||||
## 16. 最终验证
|
||||
|
||||
- [x] 16.1 执行 `go build ./...` 确认编译通过
|
||||
- [x] 16.2 执行 `go vet ./...` 检查代码质量
|
||||
- [x] 16.3 启动服务,手动测试 API 接口
|
||||
- [x] 16.4 确认 OpenAPI 文档正确生成
|
||||
180
openspec/specs/package-management/spec.md
Normal file
180
openspec/specs/package-management/spec.md
Normal file
@@ -0,0 +1,180 @@
|
||||
## ADDED Requirements
|
||||
|
||||
### Requirement: 创建套餐
|
||||
|
||||
系统 SHALL 允许平台管理员创建套餐,包含套餐编码、套餐名称、所属系列、套餐类型、时长、流量配置、价格和建议价格。套餐编码 MUST 全局唯一(排除已删除记录)。新创建的套餐默认为启用状态(1)和下架状态(2)。
|
||||
|
||||
#### Scenario: 成功创建套餐
|
||||
- **WHEN** 管理员提交有效的套餐信息
|
||||
- **THEN** 系统创建套餐记录,状态为启用(1),上架状态为下架(2),返回创建的套餐详情
|
||||
|
||||
#### Scenario: 套餐编码重复
|
||||
- **WHEN** 管理员提交的套餐编码已存在(未删除)
|
||||
- **THEN** 系统返回错误 "套餐编码已存在"
|
||||
|
||||
#### Scenario: 关联不存在的套餐系列
|
||||
- **WHEN** 管理员指定的系列 ID 不存在
|
||||
- **THEN** 系统返回错误 "套餐系列不存在"
|
||||
|
||||
#### Scenario: 缺少必填字段
|
||||
- **WHEN** 管理员未提供必填字段(套餐编码、套餐名称、套餐类型、时长、价格)
|
||||
- **THEN** 系统返回参数验证错误
|
||||
|
||||
---
|
||||
|
||||
### Requirement: 查询套餐列表
|
||||
|
||||
系统 SHALL 提供套餐列表查询功能,支持按套餐名称模糊搜索、按系列 ID 筛选、按状态筛选、按上架状态筛选、按套餐类型筛选。结果 MUST 分页返回,按创建时间倒序排列。
|
||||
|
||||
#### Scenario: 查询所有套餐
|
||||
- **WHEN** 管理员请求套餐列表,不带筛选条件
|
||||
- **THEN** 系统返回所有未删除的套餐,分页显示
|
||||
|
||||
#### Scenario: 按系列筛选
|
||||
- **WHEN** 管理员指定套餐系列 ID
|
||||
- **THEN** 系统只返回属于该系列的套餐
|
||||
|
||||
#### Scenario: 按名称搜索
|
||||
- **WHEN** 管理员提供套餐名称关键字
|
||||
- **THEN** 系统返回名称包含该关键字的套餐
|
||||
|
||||
#### Scenario: 按状态筛选
|
||||
- **WHEN** 管理员指定启用状态
|
||||
- **THEN** 系统只返回匹配启用状态的套餐
|
||||
|
||||
#### Scenario: 按上架状态筛选
|
||||
- **WHEN** 管理员指定上架状态
|
||||
- **THEN** 系统只返回匹配上架状态的套餐
|
||||
|
||||
#### Scenario: 按套餐类型筛选
|
||||
- **WHEN** 管理员指定套餐类型(formal/addon)
|
||||
- **THEN** 系统只返回匹配类型的套餐
|
||||
|
||||
---
|
||||
|
||||
### Requirement: 查询套餐详情
|
||||
|
||||
系统 SHALL 允许管理员查询单个套餐的详细信息。
|
||||
|
||||
#### Scenario: 查询存在的套餐
|
||||
- **WHEN** 管理员请求指定 ID 的套餐详情
|
||||
- **THEN** 系统返回该套餐的完整信息
|
||||
|
||||
#### Scenario: 查询不存在的套餐
|
||||
- **WHEN** 管理员请求不存在或已删除的套餐 ID
|
||||
- **THEN** 系统返回 "套餐不存在" 错误
|
||||
|
||||
---
|
||||
|
||||
### Requirement: 更新套餐
|
||||
|
||||
系统 SHALL 允许管理员更新套餐的基本信息。套餐编码创建后 MUST NOT 允许修改。
|
||||
|
||||
#### Scenario: 成功更新套餐
|
||||
- **WHEN** 管理员提交有效的更新信息
|
||||
- **THEN** 系统更新套餐记录,返回更新后的详情
|
||||
|
||||
#### Scenario: 尝试修改套餐编码
|
||||
- **WHEN** 管理员尝试修改套餐编码
|
||||
- **THEN** 系统忽略套餐编码字段,不进行修改
|
||||
|
||||
#### Scenario: 更新不存在的套餐
|
||||
- **WHEN** 管理员更新不存在的套餐
|
||||
- **THEN** 系统返回 "套餐不存在" 错误
|
||||
|
||||
#### Scenario: 关联不存在的套餐系列
|
||||
- **WHEN** 管理员将套餐关联到不存在的系列
|
||||
- **THEN** 系统返回错误 "套餐系列不存在"
|
||||
|
||||
---
|
||||
|
||||
### Requirement: 删除套餐
|
||||
|
||||
系统 SHALL 允许管理员删除套餐(软删除)。
|
||||
|
||||
#### Scenario: 成功删除套餐
|
||||
- **WHEN** 管理员删除指定的套餐
|
||||
- **THEN** 系统软删除该记录,后续查询不再返回
|
||||
|
||||
#### Scenario: 删除不存在的套餐
|
||||
- **WHEN** 管理员删除不存在的套餐
|
||||
- **THEN** 系统返回 "套餐不存在" 错误
|
||||
|
||||
---
|
||||
|
||||
### Requirement: 启用/禁用套餐
|
||||
|
||||
系统 SHALL 允许管理员切换套餐的启用状态。禁用套餐时 MUST 同时将上架状态设置为下架。
|
||||
|
||||
#### Scenario: 启用套餐
|
||||
- **WHEN** 管理员将禁用的套餐设置为启用
|
||||
- **THEN** 系统更新状态为启用(1),上架状态保持不变
|
||||
|
||||
#### Scenario: 禁用套餐
|
||||
- **WHEN** 管理员将启用的套餐设置为禁用
|
||||
- **THEN** 系统更新状态为禁用(2),同时将上架状态设置为下架(2)
|
||||
|
||||
#### Scenario: 禁用已上架的套餐
|
||||
- **WHEN** 管理员禁用一个当前已上架的套餐
|
||||
- **THEN** 系统更新状态为禁用(2),上架状态强制设置为下架(2)
|
||||
|
||||
---
|
||||
|
||||
### Requirement: 上架/下架套餐
|
||||
|
||||
系统 SHALL 允许管理员切换套餐的上架状态。只有启用状态的套餐才能上架。
|
||||
|
||||
#### Scenario: 上架启用的套餐
|
||||
- **WHEN** 管理员将启用且下架的套餐设置为上架
|
||||
- **THEN** 系统更新上架状态为上架(1)
|
||||
|
||||
#### Scenario: 尝试上架禁用的套餐
|
||||
- **WHEN** 管理员尝试上架一个禁用的套餐
|
||||
- **THEN** 系统返回错误 "禁用的套餐不能上架,请先启用"
|
||||
|
||||
#### Scenario: 下架套餐
|
||||
- **WHEN** 管理员将上架的套餐设置为下架
|
||||
- **THEN** 系统更新上架状态为下架(2)
|
||||
|
||||
#### Scenario: 状态未变化
|
||||
- **WHEN** 管理员设置的上架状态与当前状态相同
|
||||
- **THEN** 系统正常返回成功,不产生错误
|
||||
|
||||
---
|
||||
|
||||
### Requirement: Package 模型新增字段
|
||||
|
||||
系统 MUST 在 Package 模型中新增以下字段:
|
||||
- `suggested_cost_price`:建议成本价(分为单位),默认 0
|
||||
- `suggested_retail_price`:建议售价(分为单位),默认 0
|
||||
- `shelf_status`:上架状态,1-上架 2-下架,默认 2
|
||||
|
||||
#### Scenario: 创建套餐时设置建议价格
|
||||
- **WHEN** 管理员创建套餐并设置建议成本价和建议售价
|
||||
- **THEN** 系统保存这些价格信息
|
||||
|
||||
#### Scenario: 查询套餐时返回建议价格
|
||||
- **WHEN** 管理员查询套餐详情或列表
|
||||
- **THEN** 响应中包含 suggested_cost_price、suggested_retail_price、shelf_status 字段
|
||||
|
||||
---
|
||||
|
||||
### Requirement: 清理废弃模型
|
||||
|
||||
系统 MUST 删除以下废弃的分佣相关模型和对应的数据库表:
|
||||
- `AgentHierarchy` (tb_agent_hierarchy)
|
||||
- `CommissionRule` (tb_commission_rule)
|
||||
- `CommissionLadder` (tb_commission_ladder)
|
||||
- `CommissionCombinedCondition` (tb_commission_combined_condition)
|
||||
- `CommissionApproval` (tb_commission_approval)
|
||||
- `CommissionTemplate` (tb_commission_template)
|
||||
- `CarrierSettlement` (tb_carrier_settlement)
|
||||
- `AgentPackageAllocation` (tb_agent_package_allocation)
|
||||
|
||||
#### Scenario: 迁移后废弃表不存在
|
||||
- **WHEN** 执行数据库迁移后
|
||||
- **THEN** 上述 8 个表在数据库中不再存在
|
||||
|
||||
#### Scenario: 代码中无废弃模型引用
|
||||
- **WHEN** 删除模型定义后
|
||||
- **THEN** 项目能够正常编译,无编译错误
|
||||
99
openspec/specs/package-series-management/spec.md
Normal file
99
openspec/specs/package-series-management/spec.md
Normal file
@@ -0,0 +1,99 @@
|
||||
## ADDED Requirements
|
||||
|
||||
### Requirement: 创建套餐系列
|
||||
|
||||
系统 SHALL 允许平台管理员创建套餐系列,包含系列编码、系列名称、描述信息。系列编码 MUST 全局唯一(排除已删除记录)。新创建的套餐系列默认为启用状态。
|
||||
|
||||
#### Scenario: 成功创建套餐系列
|
||||
- **WHEN** 管理员提交有效的套餐系列信息(系列编码、系列名称)
|
||||
- **THEN** 系统创建套餐系列记录,返回创建的套餐系列详情,状态为启用(1)
|
||||
|
||||
#### Scenario: 系列编码重复
|
||||
- **WHEN** 管理员提交的系列编码已存在(未删除)
|
||||
- **THEN** 系统返回错误 "系列编码已存在"
|
||||
|
||||
#### Scenario: 缺少必填字段
|
||||
- **WHEN** 管理员未提供系列编码或系列名称
|
||||
- **THEN** 系统返回参数验证错误
|
||||
|
||||
---
|
||||
|
||||
### Requirement: 查询套餐系列列表
|
||||
|
||||
系统 SHALL 提供套餐系列列表查询功能,支持按系列名称模糊搜索、按状态筛选。结果 MUST 分页返回,按创建时间倒序排列。
|
||||
|
||||
#### Scenario: 查询所有套餐系列
|
||||
- **WHEN** 管理员请求套餐系列列表,不带筛选条件
|
||||
- **THEN** 系统返回所有未删除的套餐系列,分页显示
|
||||
|
||||
#### Scenario: 按名称搜索
|
||||
- **WHEN** 管理员提供系列名称关键字
|
||||
- **THEN** 系统返回名称包含该关键字的套餐系列
|
||||
|
||||
#### Scenario: 按状态筛选
|
||||
- **WHEN** 管理员指定状态筛选(启用/禁用)
|
||||
- **THEN** 系统只返回匹配状态的套餐系列
|
||||
|
||||
---
|
||||
|
||||
### Requirement: 查询套餐系列详情
|
||||
|
||||
系统 SHALL 允许管理员查询单个套餐系列的详细信息。
|
||||
|
||||
#### Scenario: 查询存在的套餐系列
|
||||
- **WHEN** 管理员请求指定 ID 的套餐系列详情
|
||||
- **THEN** 系统返回该套餐系列的完整信息
|
||||
|
||||
#### Scenario: 查询不存在的套餐系列
|
||||
- **WHEN** 管理员请求不存在或已删除的套餐系列 ID
|
||||
- **THEN** 系统返回 "套餐系列不存在" 错误
|
||||
|
||||
---
|
||||
|
||||
### Requirement: 更新套餐系列
|
||||
|
||||
系统 SHALL 允许管理员更新套餐系列的基本信息(系列名称、描述)。系列编码创建后 MUST NOT 允许修改。
|
||||
|
||||
#### Scenario: 成功更新套餐系列
|
||||
- **WHEN** 管理员提交有效的更新信息
|
||||
- **THEN** 系统更新套餐系列记录,返回更新后的详情
|
||||
|
||||
#### Scenario: 尝试修改系列编码
|
||||
- **WHEN** 管理员尝试修改系列编码
|
||||
- **THEN** 系统忽略系列编码字段,不进行修改
|
||||
|
||||
#### Scenario: 更新不存在的套餐系列
|
||||
- **WHEN** 管理员更新不存在的套餐系列
|
||||
- **THEN** 系统返回 "套餐系列不存在" 错误
|
||||
|
||||
---
|
||||
|
||||
### Requirement: 删除套餐系列
|
||||
|
||||
系统 SHALL 允许管理员删除套餐系列(软删除)。
|
||||
|
||||
#### Scenario: 成功删除套餐系列
|
||||
- **WHEN** 管理员删除指定的套餐系列
|
||||
- **THEN** 系统软删除该记录,后续查询不再返回
|
||||
|
||||
#### Scenario: 删除不存在的套餐系列
|
||||
- **WHEN** 管理员删除不存在的套餐系列
|
||||
- **THEN** 系统返回 "套餐系列不存在" 错误
|
||||
|
||||
---
|
||||
|
||||
### Requirement: 启用/禁用套餐系列
|
||||
|
||||
系统 SHALL 允许管理员切换套餐系列的启用状态。
|
||||
|
||||
#### Scenario: 启用套餐系列
|
||||
- **WHEN** 管理员将禁用的套餐系列设置为启用
|
||||
- **THEN** 系统更新状态为启用(1)
|
||||
|
||||
#### Scenario: 禁用套餐系列
|
||||
- **WHEN** 管理员将启用的套餐系列设置为禁用
|
||||
- **THEN** 系统更新状态为禁用(2)
|
||||
|
||||
#### Scenario: 状态未变化
|
||||
- **WHEN** 管理员设置的状态与当前状态相同
|
||||
- **THEN** 系统正常返回成功,不产生错误
|
||||
Reference in New Issue
Block a user