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

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

View File

@@ -0,0 +1,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. **代理为终端用户购买?**
- 当前设计:代理只能为自己店铺购买
- 待确认:是否需要代理帮终端用户购买的场景?