This commit is contained in:
@@ -0,0 +1,2 @@
|
||||
schema: spec-driven
|
||||
created: 2026-01-31
|
||||
@@ -0,0 +1,724 @@
|
||||
## Context
|
||||
|
||||
### 当前系统状态
|
||||
|
||||
**钱包系统现状**:
|
||||
- 已有 Wallet、WalletTransaction 模型和 Store 层实现
|
||||
- 已有 RechargeRecord 模型定义,但完全未使用(表已创建,无 Store/Service/Handler)
|
||||
- 个人客户只能通过购买套餐间接充值钱包,无法直接充值
|
||||
- 订单支付采用"强充"机制:用户必须通过购买套餐来充值,不支持纯钱包充值
|
||||
|
||||
**订单系统现状**:
|
||||
- Order 模型已支持单卡购买和设备购买两种类型
|
||||
- 支持 wallet、wechat、alipay 三种支付方式
|
||||
- 订单创建 → 支付 → 激活套餐 → 触发佣金计算的完整流程已实现
|
||||
- 支付回调处理支持微信和支付宝
|
||||
|
||||
**佣金计算现状**:
|
||||
- 支持成本价差佣金和一次性佣金两种类型
|
||||
- 一次性佣金支持首次充值和累计充值两种触发方式
|
||||
- 订单支付成功后自动更新 `AccumulatedRecharge`
|
||||
- **存在问题**:所有订单(包括代购订单)都会更新累计充值,都会触发一次性佣金
|
||||
|
||||
**系列分配配置现状**:
|
||||
- ShopSeriesAllocation 已支持一次性佣金配置(类型、触发方式、阈值、模式、值)
|
||||
- 支持梯度佣金配置(独立表 ShopSeriesOneTimeCommissionTier)
|
||||
- **缺失**:没有强充金额配置字段
|
||||
|
||||
### 业务需求背景
|
||||
|
||||
1. **线下收款场景**:平台/代理线下已收款,需要为代理代购套餐,但系统无法支持
|
||||
2. **个人客户充值体验**:用户想充值钱包但不想立即购买套餐,当前系统无法满足
|
||||
3. **强充机制完善**:
|
||||
- 首次充值需强制充值阈值金额(如100元)
|
||||
- 累计充值可选启用强充,每次充值固定金额(如100元),避免"买39元套餐却要充1000元"的不合理情况
|
||||
4. **佣金计算准确性**:代购订单不应触发一次性佣金,因为不是客户真实充值
|
||||
|
||||
### 约束条件
|
||||
|
||||
- **技术栈**:必须使用 Fiber + GORM + Viper + Zap + Asynq,禁止外键和 GORM 关联
|
||||
- **架构分层**:Handler → Service → Store → Model,严格分层
|
||||
- **性能要求**:预检接口 < 100ms,充值创建 < 200ms,支付回调 < 500ms
|
||||
- **测试要求**:核心业务逻辑覆盖率 ≥ 90%
|
||||
- **向后兼容**:新增字段必须有默认值,不能破坏现有订单和佣金计算逻辑
|
||||
|
||||
---
|
||||
|
||||
## Goals / Non-Goals
|
||||
|
||||
### Goals
|
||||
|
||||
1. **实现钱包充值系统**
|
||||
- 个人客户可以直接给卡/设备钱包充值(不购买套餐)
|
||||
- 充值支持微信支付和支付宝支付
|
||||
- 充值成功自动更新钱包余额和累计充值金额
|
||||
- 充值达到阈值触发一次性佣金
|
||||
|
||||
2. **实现强充预检机制**
|
||||
- 提供充值预检接口,告知用户强充要求
|
||||
- 提供套餐购买预检接口,计算实际支付金额
|
||||
- 创建订单/充值订单时后端强制验证,防止前端绕过
|
||||
|
||||
3. **实现代购订单功能**
|
||||
- 平台/代理可为其他代理代购套餐
|
||||
- 支持线下支付方式(offline)
|
||||
- 代购订单不触发一次性佣金,不更新累计充值
|
||||
- 代购订单仍计算差价佣金
|
||||
|
||||
4. **扩展强充配置**
|
||||
- ShopSeriesAllocation 增加强充配置字段
|
||||
- 首次充值:强充金额 = 阈值(不可配置)
|
||||
- 累计充值:可选启用强充,配置固定充值金额
|
||||
|
||||
5. **修复佣金计算逻辑**
|
||||
- 代购订单不累加 AccumulatedRecharge
|
||||
- 代购订单不触发一次性佣金
|
||||
- 充值订单正常触发佣金
|
||||
|
||||
### Non-Goals
|
||||
|
||||
1. **不支持钱包转账**:用户钱包间转账不在本次范围
|
||||
2. **不支持退款流程**:充值退款、订单退款流程留待后续实现
|
||||
3. **不修改梯度佣金逻辑**:梯度佣金计算保持不变
|
||||
4. **不修改差价佣金逻辑**:成本价差计算保持不变
|
||||
5. **不支持企业客户钱包**:企业客户无钱包,本次不涉及
|
||||
6. **不实现充值优惠**:充值满减、赠送等营销功能不在范围
|
||||
|
||||
---
|
||||
|
||||
## Decisions
|
||||
|
||||
### Decision 1: 数据库模型设计
|
||||
|
||||
**选择**:使用现有 RechargeRecord 表,新增必要字段到 Order 和 ShopSeriesAllocation 表。
|
||||
|
||||
**理由**:
|
||||
- RechargeRecord 表已存在,结构合理,只需激活使用
|
||||
- Order 表新增 `is_purchase_on_behalf BOOLEAN DEFAULT false`
|
||||
- ShopSeriesAllocation 表新增 `enable_force_recharge BOOLEAN DEFAULT false` 和 `force_recharge_amount BIGINT DEFAULT 0`
|
||||
|
||||
**备选方案及拒绝原因**:
|
||||
- ~~创建新表 PurchaseOnBehalfOrder~~:增加复杂度,Order 表扩展一个字段即可
|
||||
- ~~使用订单备注字段标识代购~~:不利于查询和统计,需要独立字段
|
||||
|
||||
**实现细节**:
|
||||
```sql
|
||||
-- 迁移文件 1: 订单表增加代购标识
|
||||
ALTER TABLE tb_order
|
||||
ADD COLUMN is_purchase_on_behalf BOOLEAN DEFAULT false
|
||||
COMMENT '是否为代购订单(平台/代理代购)';
|
||||
|
||||
-- 迁移文件 2: 系列分配表增加强充配置
|
||||
ALTER TABLE tb_shop_series_allocation
|
||||
ADD COLUMN enable_force_recharge BOOLEAN DEFAULT false
|
||||
COMMENT '是否启用强充(累计充值时可选)',
|
||||
ADD COLUMN force_recharge_amount BIGINT DEFAULT 0
|
||||
COMMENT '强充金额(分,0表示使用阈值金额)';
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### Decision 2: 强充验证策略
|
||||
|
||||
**选择**:前端预检 + 后端强制验证的双重保障策略。
|
||||
|
||||
**架构**:
|
||||
```
|
||||
前端调用预检接口 → 获取强充要求 → 显示给用户
|
||||
↓
|
||||
用户提交订单/充值 → 后端验证金额是否符合强充要求
|
||||
↓
|
||||
验证不通过 → 拒绝创建订单,返回错误
|
||||
验证通过 → 创建订单/充值订单
|
||||
```
|
||||
|
||||
**预检接口设计**:
|
||||
|
||||
1. **钱包充值预检**:`GET /api/h5/wallets/recharge-check?resource_type=iot_card&resource_id=123`
|
||||
```go
|
||||
type RechargeCheckResponse struct {
|
||||
NeedForceRecharge bool `json:"need_force_recharge"`
|
||||
ForceRechargeAmount int64 `json:"force_recharge_amount"`
|
||||
TriggerType string `json:"trigger_type"` // single_recharge/accumulated_recharge
|
||||
MinAmount int64 `json:"min_amount"`
|
||||
MaxAmount *int64 `json:"max_amount"`
|
||||
CurrentAccumulated int64 `json:"current_accumulated"`
|
||||
Threshold int64 `json:"threshold"`
|
||||
Message string `json:"message"`
|
||||
}
|
||||
```
|
||||
|
||||
2. **套餐购买预检**:`POST /api/h5/orders/purchase-check`
|
||||
```go
|
||||
type PurchaseCheckRequest struct {
|
||||
OrderType string `json:"order_type"`
|
||||
ResourceID uint `json:"resource_id"` // iot_card_id/device_id
|
||||
PackageIDs []uint `json:"package_ids"`
|
||||
}
|
||||
|
||||
type PurchaseCheckResponse struct {
|
||||
TotalPackageAmount int64 `json:"total_package_amount"`
|
||||
NeedForceRecharge bool `json:"need_force_recharge"`
|
||||
ForceRechargeAmount int64 `json:"force_recharge_amount"`
|
||||
ActualPayment int64 `json:"actual_payment"`
|
||||
WalletCredit int64 `json:"wallet_credit"`
|
||||
Message string `json:"message"`
|
||||
}
|
||||
```
|
||||
|
||||
**验证逻辑**:
|
||||
```go
|
||||
func (s *RechargeService) checkForceRechargeRequirement(ctx, resourceType, resourceID) (*ForceRechargeRequirement, error) {
|
||||
// 1. 查询资源(卡/设备)
|
||||
resource := queryResource(resourceType, resourceID)
|
||||
|
||||
// 2. 查询系列分配
|
||||
allocation := s.allocationStore.GetByID(ctx, resource.SeriesAllocationID)
|
||||
|
||||
// 3. 判断是否需要强充
|
||||
if !allocation.EnableOneTimeCommission {
|
||||
return &ForceRechargeRequirement{NeedForceRecharge: false}, nil
|
||||
}
|
||||
|
||||
if resource.FirstCommissionPaid {
|
||||
return &ForceRechargeRequirement{NeedForceRecharge: false}, nil
|
||||
}
|
||||
|
||||
// 4. 根据触发类型判断
|
||||
if allocation.OneTimeCommissionTrigger == "single_recharge" {
|
||||
// 首次充值:强充金额 = 阈值
|
||||
return &ForceRechargeRequirement{
|
||||
NeedForceRecharge: true,
|
||||
ForceRechargeAmount: allocation.OneTimeCommissionThreshold,
|
||||
TriggerType: "single_recharge",
|
||||
}, nil
|
||||
} else {
|
||||
// 累计充值:检查是否启用强充
|
||||
if allocation.EnableForceRecharge {
|
||||
return &ForceRechargeRequirement{
|
||||
NeedForceRecharge: true,
|
||||
ForceRechargeAmount: allocation.ForceRechargeAmount,
|
||||
TriggerType: "accumulated_recharge",
|
||||
}, nil
|
||||
}
|
||||
return &ForceRechargeRequirement{NeedForceRecharge: false}, nil
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**备选方案及拒绝原因**:
|
||||
- ~~仅前端验证~~:不安全,用户可以绕过前端直接调用 API
|
||||
- ~~仅后端验证~~:用户体验差,提交后才知道金额不对
|
||||
|
||||
---
|
||||
|
||||
### Decision 3: 充值订单与套餐订单的关系
|
||||
|
||||
**选择**:充值订单和套餐订单完全独立,使用不同的表和流程。
|
||||
|
||||
**理由**:
|
||||
- **关注点分离**:充值订单关注钱包余额变动,套餐订单关注套餐激活
|
||||
- **业务语义清晰**:RechargeRecord 表示纯充值,Order 表示购买套餐
|
||||
- **支付回调区分**:通过订单号前缀区分(RCH 开头是充值,ORD 开头是订单)
|
||||
- **佣金计算独立**:充值和购买的佣金触发逻辑不同
|
||||
|
||||
**处理流程对比**:
|
||||
|
||||
| 流程步骤 | 充值订单(RechargeRecord) | 套餐订单(Order) |
|
||||
|---------|---------------------------|------------------|
|
||||
| 创建订单 | 创建 RechargeRecord | 创建 Order + OrderItem |
|
||||
| 验证强充 | ✅ 验证充值金额 | ✅ 验证支付金额 |
|
||||
| 生成订单号 | RCH + 时间戳 + 随机数 | ORD + 时间戳 + 随机数 |
|
||||
| 支付 | 微信/支付宝 | 钱包/微信/支付宝/线下 |
|
||||
| 支付成功 | 增加钱包余额 | 激活套餐,可能返还余额 |
|
||||
| 更新累计充值 | ✅ 更新 AccumulatedRecharge | ✅ 更新(代购除外)|
|
||||
| 触发佣金 | ✅ 触发一次性佣金判断 | ✅ 触发差价+一次性佣金(代购除外)|
|
||||
|
||||
**支付回调路由**:
|
||||
```go
|
||||
func (h *PaymentHandler) WechatPayCallback(c *fiber.Ctx) error {
|
||||
result := parseWechatCallback(c.Body())
|
||||
|
||||
// 根据订单号前缀判断类型
|
||||
if strings.HasPrefix(result.OutTradeNo, "RCH") {
|
||||
// 充值订单回调
|
||||
return h.rechargeService.HandlePaymentCallback(ctx, result.OutTradeNo, "wechat")
|
||||
} else if strings.HasPrefix(result.OutTradeNo, "ORD") {
|
||||
// 套餐订单回调
|
||||
return h.orderService.HandlePaymentCallback(ctx, result.OutTradeNo, "wechat")
|
||||
}
|
||||
|
||||
return errors.New(errors.CodeInvalidParam, "无效的订单号")
|
||||
}
|
||||
```
|
||||
|
||||
**备选方案及拒绝原因**:
|
||||
- ~~使用同一个 Order 表,通过 order_type 区分~~:语义混乱,充值不是"订单"
|
||||
- ~~充值也创建 Order,但 order_items 为空~~:违反业务语义,items 为空表示什么?
|
||||
|
||||
---
|
||||
|
||||
### Decision 4: 代购订单处理
|
||||
|
||||
**选择**:代购订单使用 `is_purchase_on_behalf` 字段标识,创建时直接标记为已支付,跳过支付流程。
|
||||
|
||||
**创建流程**:
|
||||
```
|
||||
平台/代理创建代购订单
|
||||
↓
|
||||
查询卡/设备归属的代理店铺
|
||||
↓
|
||||
计算买家的成本价(不是卖价)
|
||||
↓
|
||||
创建订单:
|
||||
- buyer_id = 代理店铺ID
|
||||
- is_purchase_on_behalf = true
|
||||
- payment_method = "offline"
|
||||
- payment_status = 2 (已支付)
|
||||
- total_amount = 买家成本价
|
||||
↓
|
||||
立即激活套餐(创建 PackageUsage)
|
||||
↓
|
||||
触发佣金计算(仅差价佣金,不触发一次性佣金)
|
||||
```
|
||||
|
||||
**权限控制**:
|
||||
```go
|
||||
func (h *OrderHandler) Create(c *fiber.Ctx) error {
|
||||
req := parseRequest(c)
|
||||
userType := middleware.GetUserTypeFromContext(ctx)
|
||||
|
||||
// 检查线下支付权限
|
||||
if req.PaymentMethod == model.PaymentMethodOffline {
|
||||
if userType != constants.UserTypePlatform {
|
||||
return errors.New(errors.CodeForbidden, "只有平台账号可以使用线下支付")
|
||||
}
|
||||
}
|
||||
|
||||
// 平台代购 vs 普通订单
|
||||
if userType == constants.UserTypePlatform && req.PaymentMethod == "offline" {
|
||||
// 平台代购逻辑
|
||||
buyerShopID := queryResourceOwner(req.OrderType, req.ResourceID)
|
||||
return h.service.CreatePurchaseOnBehalf(ctx, req, buyerShopID)
|
||||
} else {
|
||||
// 普通订单逻辑
|
||||
shopID := middleware.GetShopIDFromContext(ctx)
|
||||
return h.service.Create(ctx, req, buyerType, shopID)
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**备选方案及拒绝原因**:
|
||||
- ~~使用独立的 purchase_on_behalf_orders 表~~:增加复杂度,查询和统计不便
|
||||
- ~~使用订单状态区分(如 payment_status = 5 表示代购)~~:滥用状态字段,语义不清
|
||||
|
||||
---
|
||||
|
||||
### Decision 5: 佣金计算修复
|
||||
|
||||
**选择**:在佣金计算Service中增加 `is_purchase_on_behalf` 判断,代购订单跳过一次性佣金和累计充值更新。
|
||||
|
||||
**修改逻辑**:
|
||||
```go
|
||||
func (s *CommissionCalculationService) CalculateCommission(ctx, orderID) error {
|
||||
order := s.orderStore.GetByID(ctx, orderID)
|
||||
|
||||
// 1. 差价佣金:所有订单都计算(包括代购)
|
||||
costDiffRecords := s.CalculateCostDiffCommission(ctx, order)
|
||||
|
||||
// 2. 累计充值:仅非代购订单更新
|
||||
if !order.IsPurchaseOnBehalf {
|
||||
s.updateAccumulatedRecharge(ctx, order)
|
||||
}
|
||||
|
||||
// 3. 一次性佣金:仅非代购订单触发
|
||||
if !order.IsPurchaseOnBehalf {
|
||||
s.triggerOneTimeCommission(ctx, order)
|
||||
}
|
||||
|
||||
// 4. 更新订单佣金状态
|
||||
s.orderStore.UpdateCommissionStatus(ctx, orderID, CommissionStatusCalculated)
|
||||
}
|
||||
|
||||
func (s *CommissionCalculationService) updateAccumulatedRecharge(ctx, order) error {
|
||||
if order.OrderType == "single_card" && order.IotCardID != nil {
|
||||
return s.db.Model(&model.IotCard{}).
|
||||
Where("id = ?", *order.IotCardID).
|
||||
Update("accumulated_recharge", gorm.Expr("accumulated_recharge + ?", order.TotalAmount)).
|
||||
Error
|
||||
} else if order.OrderType == "device" && order.DeviceID != nil {
|
||||
return s.db.Model(&model.Device{}).
|
||||
Where("id = ?", *order.DeviceID).
|
||||
Update("accumulated_recharge", gorm.Expr("accumulated_recharge + ?", order.TotalAmount)).
|
||||
Error
|
||||
}
|
||||
return nil
|
||||
}
|
||||
```
|
||||
|
||||
**充值订单的佣金触发**:
|
||||
```go
|
||||
func (s *RechargeService) HandlePaymentCallback(ctx, rechargeNo, paymentMethod) error {
|
||||
return s.db.Transaction(func(tx *gorm.DB) error {
|
||||
// 1. 更新充值订单状态
|
||||
s.rechargeStore.UpdateStatus(ctx, rechargeNo, RechargeStatusPaid)
|
||||
|
||||
// 2. 增加钱包余额
|
||||
s.walletStore.IncreaseBalance(ctx, walletID, amount)
|
||||
|
||||
// 3. 更新累计充值
|
||||
s.updateAccumulatedRecharge(ctx, resourceType, resourceID, amount)
|
||||
|
||||
// 4. 触发一次性佣金判断
|
||||
s.triggerOneTimeCommissionIfNeeded(ctx, resourceType, resourceID, amount)
|
||||
|
||||
return nil
|
||||
})
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### Decision 6: 强充金额来源
|
||||
|
||||
**选择**:首次充值使用阈值,累计充值使用独立配置字段。
|
||||
|
||||
**规则矩阵**:
|
||||
|
||||
| 触发类型 | 强充开关 | 强充金额来源 | 说明 |
|
||||
|---------|---------|-------------|------|
|
||||
| 首次充值 | 不可配置(必须强充) | `OneTimeCommissionThreshold` | 首充必须充值阈值金额 |
|
||||
| 累计充值 | `EnableForceRecharge=true` | `ForceRechargeAmount`(独立配置) | 每次必须充值固定金额 |
|
||||
| 累计充值 | `EnableForceRecharge=false` | - | 不强充,自由金额 |
|
||||
|
||||
**查询逻辑**:
|
||||
```go
|
||||
func getForceRechargeAmount(allocation *ShopSeriesAllocation) int64 {
|
||||
if allocation.OneTimeCommissionTrigger == "single_recharge" {
|
||||
// 首次充值:强充金额 = 阈值
|
||||
return allocation.OneTimeCommissionThreshold
|
||||
} else {
|
||||
// 累计充值:强充金额 = 配置字段(如果为0则使用阈值)
|
||||
if allocation.ForceRechargeAmount > 0 {
|
||||
return allocation.ForceRechargeAmount
|
||||
}
|
||||
return allocation.OneTimeCommissionThreshold
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**备选方案及拒绝原因**:
|
||||
- ~~累计充值的强充金额也固定为阈值~~:不合理,会导致"买39元套餐要充1000元"的问题
|
||||
- ~~累计充值的强充金额动态计算(阈值 - 当前累计)~~:不合理,每次充值金额不固定
|
||||
|
||||
---
|
||||
|
||||
### Decision 7: 模块依赖注入
|
||||
|
||||
**选择**:使用现有的 `bootstrap` 包统一管理依赖注入。
|
||||
|
||||
**注入结构**:
|
||||
```go
|
||||
// bootstrap/stores.go
|
||||
type Stores struct {
|
||||
// ... 现有 stores
|
||||
Recharge *postgres.RechargeStore // 新增
|
||||
}
|
||||
|
||||
// bootstrap/services.go
|
||||
type Services struct {
|
||||
// ... 现有 services
|
||||
Recharge *recharge.Service // 新增
|
||||
}
|
||||
|
||||
// bootstrap/handlers.go
|
||||
type Handlers struct {
|
||||
// ... 现有 handlers
|
||||
H5Recharge *h5.RechargeHandler // 新增
|
||||
}
|
||||
|
||||
// 初始化顺序:Stores → Services → Handlers
|
||||
func Bootstrap(deps *Dependencies) (*Handlers, error) {
|
||||
stores := initStores(deps.DB, deps.Redis)
|
||||
services := initServices(deps, stores)
|
||||
handlers := initHandlers(services)
|
||||
return handlers, nil
|
||||
}
|
||||
```
|
||||
|
||||
**理由**:
|
||||
- 遵循现有架构模式
|
||||
- 集中管理依赖,易于测试和维护
|
||||
- 避免循环依赖
|
||||
|
||||
---
|
||||
|
||||
## Risks / Trade-offs
|
||||
|
||||
### Risk 1: 数据库迁移风险
|
||||
|
||||
**风险**:新增字段的迁移可能影响现有数据。
|
||||
|
||||
**缓解措施**:
|
||||
- 所有新增字段都有默认值(`is_purchase_on_behalf DEFAULT false`)
|
||||
- 分阶段迁移:先添加字段,再部署代码,最后更新数据
|
||||
- 迁移前备份数据库
|
||||
- 在测试环境完整验证迁移流程
|
||||
|
||||
---
|
||||
|
||||
### Risk 2: 支付回调幂等性
|
||||
|
||||
**风险**:充值订单和套餐订单都支持支付回调,可能重复处理。
|
||||
|
||||
**缓解措施**:
|
||||
- 检查订单/充值订单状态,已支付则直接返回成功
|
||||
- 使用数据库事务保证原子性
|
||||
- 钱包余额更新使用乐观锁(version 字段)
|
||||
|
||||
```go
|
||||
func (s *RechargeService) HandlePaymentCallback(ctx, rechargeNo, method) error {
|
||||
// 幂等性检查
|
||||
recharge := s.rechargeStore.GetByRechargeNo(ctx, rechargeNo)
|
||||
if recharge.Status == RechargeStatusPaid || recharge.Status == RechargeStatusCompleted {
|
||||
return nil // 已处理,直接返回成功
|
||||
}
|
||||
|
||||
// 事务处理
|
||||
return s.db.Transaction(func(tx *gorm.DB) error {
|
||||
// 更新充值订单状态(带状态检查)
|
||||
result := tx.Model(&RechargeRecord{}).
|
||||
Where("recharge_no = ? AND status = ?", rechargeNo, RechargeStatusPending).
|
||||
Updates(map[string]any{
|
||||
"status": RechargeStatusPaid,
|
||||
"payment_method": method,
|
||||
"paid_at": time.Now(),
|
||||
})
|
||||
|
||||
if result.RowsAffected == 0 {
|
||||
return nil // 已被处理,跳过
|
||||
}
|
||||
|
||||
// 增加钱包余额(使用乐观锁)
|
||||
// ...
|
||||
})
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### Risk 3: 强充验证被绕过
|
||||
|
||||
**风险**:前端或恶意用户可能绕过强充验证。
|
||||
|
||||
**缓解措施**:
|
||||
- 后端创建订单/充值订单时强制验证,拒绝不符合要求的请求
|
||||
- 记录验证失败的日志,监控异常行为
|
||||
- API 接口使用认证中间件,防止未授权调用
|
||||
|
||||
---
|
||||
|
||||
### Risk 4: 佣金计算逻辑复杂度增加
|
||||
|
||||
**风险**:增加代购订单判断后,佣金计算逻辑更复杂,容易出错。
|
||||
|
||||
**缓解措施**:
|
||||
- 单元测试覆盖所有场景(普通订单、代购订单、充值订单)
|
||||
- 使用 table-driven tests 测试各种组合
|
||||
- 添加详细的日志记录,便于排查问题
|
||||
|
||||
**测试场景**:
|
||||
```go
|
||||
func TestCommissionCalculation_PurchaseOnBehalf(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
isPurchaseOnBehalf bool
|
||||
expectUpdateAccumulated bool
|
||||
expectOneTimeCommission bool
|
||||
}{
|
||||
{"普通订单", false, true, true},
|
||||
{"代购订单", true, false, false},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
// 测试逻辑
|
||||
})
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### Trade-off 1: 预检接口性能 vs 实时性
|
||||
|
||||
**权衡**:预检接口需要查询系列分配配置,可能影响性能。
|
||||
|
||||
**选择**:不缓存系列分配配置,保证实时性。
|
||||
|
||||
**理由**:
|
||||
- 系列分配配置变更频率低
|
||||
- 单次查询性能可接受(< 10ms)
|
||||
- 实时性更重要(避免用户看到过期的强充要求)
|
||||
- 如果后续性能成为瓶颈,可以引入短期缓存(如 1 分钟)
|
||||
|
||||
---
|
||||
|
||||
### Trade-off 2: 充值订单和套餐订单独立 vs 统一
|
||||
|
||||
**权衡**:使用独立的 RechargeRecord 表增加了一定复杂度。
|
||||
|
||||
**选择**:保持独立,不合并到 Order 表。
|
||||
|
||||
**理由**:
|
||||
- **语义清晰**:充值不是"订单",是钱包操作
|
||||
- **查询方便**:充值记录和订单记录可以独立查询和统计
|
||||
- **扩展性好**:未来可能支持银行转账充值等,不适合放在 Order 表
|
||||
- **复杂度可控**:只是多一个 Store/Service/Handler,符合分层架构
|
||||
|
||||
---
|
||||
|
||||
## Migration Plan
|
||||
|
||||
### 阶段 1: 数据库迁移(停机时间 < 1 分钟)
|
||||
|
||||
1. **创建迁移文件**:
|
||||
```bash
|
||||
# 迁移文件 1
|
||||
000XXX_add_order_purchase_on_behalf.up.sql
|
||||
000XXX_add_order_purchase_on_behalf.down.sql
|
||||
|
||||
# 迁移文件 2
|
||||
000XXX_add_shop_series_allocation_force_recharge.up.sql
|
||||
000XXX_add_shop_series_allocation_force_recharge.down.sql
|
||||
```
|
||||
|
||||
2. **执行迁移**:
|
||||
```bash
|
||||
# 测试环境验证
|
||||
migrate -path migrations -database "postgres://..." up
|
||||
|
||||
# 生产环境执行
|
||||
migrate -path migrations -database "postgres://..." up
|
||||
```
|
||||
|
||||
3. **验证迁移**:
|
||||
```sql
|
||||
-- 检查字段是否添加成功
|
||||
SELECT column_name, data_type, column_default
|
||||
FROM information_schema.columns
|
||||
WHERE table_name IN ('tb_order', 'tb_shop_series_allocation');
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### 阶段 2: 代码部署(灰度发布)
|
||||
|
||||
1. **部署顺序**:
|
||||
- 先部署 API 服务(包含新接口和修复后的佣金计算)
|
||||
- 再部署 Worker 服务(佣金计算任务处理)
|
||||
|
||||
2. **灰度策略**:
|
||||
- 第1天:50% 流量
|
||||
- 第2天:100% 流量
|
||||
- 监控错误率、响应时间、佣金计算准确性
|
||||
|
||||
3. **回滚策略**:
|
||||
- 如果发现严重问题,立即回滚到旧版本
|
||||
- 数据库字段保留(有默认值,不影响旧代码)
|
||||
- 充值订单数据保留,后续可重新处理
|
||||
|
||||
---
|
||||
|
||||
### 阶段 3: 功能验证
|
||||
|
||||
1. **充值功能验证**:
|
||||
- 个人客户创建充值订单
|
||||
- 微信/支付宝支付成功
|
||||
- 钱包余额正确增加
|
||||
- 累计充值正确更新
|
||||
- 达到阈值时正确触发佣金
|
||||
|
||||
2. **代购功能验证**:
|
||||
- 平台创建代购订单
|
||||
- 订单自动完成
|
||||
- 套餐正确激活
|
||||
- 差价佣金正确计算
|
||||
- 一次性佣金不触发
|
||||
- 累计充值不更新
|
||||
|
||||
3. **强充验证**:
|
||||
- 预检接口返回正确的强充要求
|
||||
- 创建订单时正确验证强充金额
|
||||
- 不符合要求的订单被拒绝
|
||||
|
||||
---
|
||||
|
||||
### 阶段 4: 数据监控
|
||||
|
||||
1. **监控指标**:
|
||||
- 充值订单创建数量、成功率
|
||||
- 代购订单创建数量
|
||||
- 佣金计算准确性(抽样检查)
|
||||
- 累计充值更新准确性
|
||||
- 预检接口响应时间
|
||||
- 支付回调成功率
|
||||
|
||||
2. **告警规则**:
|
||||
- 充值订单创建失败率 > 5%
|
||||
- 支付回调处理失败率 > 1%
|
||||
- 预检接口响应时间 > 200ms
|
||||
- 佣金计算失败率 > 0.1%
|
||||
|
||||
---
|
||||
|
||||
## Open Questions
|
||||
|
||||
### Q1: 代理能否为下级代理代购?
|
||||
|
||||
**当前设计**:平台可以为任何代理代购,代理暂不支持。
|
||||
|
||||
**待确认**:
|
||||
- 代理是否需要为下级代理代购的能力?
|
||||
- 如果需要,权限如何控制(只能为直属下级?还是所有下级?)
|
||||
- 代购时使用谁的成本价(创建人的成本价?还是买家的成本价?)
|
||||
|
||||
**影响**:如果需要支持,Handler 层的权限检查需要调整。
|
||||
|
||||
---
|
||||
|
||||
### Q2: 充值订单是否需要取消功能?
|
||||
|
||||
**当前设计**:充值订单创建后不支持取消,只能超时自动关闭。
|
||||
|
||||
**待确认**:
|
||||
- 用户是否需要主动取消充值订单?
|
||||
- 如果支持取消,状态流转如何设计?
|
||||
|
||||
**影响**:如果需要支持,需要增加 Cancel 接口和状态流转逻辑。
|
||||
|
||||
---
|
||||
|
||||
### Q3: 强充金额是否需要支持范围(最小-最大)?
|
||||
|
||||
**当前设计**:强充金额是一个固定值(如100元)。
|
||||
|
||||
**待确认**:
|
||||
- 是否需要支持金额范围(如 100-500 元之间任意金额)?
|
||||
- 如果支持,配置字段如何设计?
|
||||
|
||||
**影响**:如果需要支持,ShopSeriesAllocation 需要增加 `force_recharge_min` 和 `force_recharge_max` 字段。
|
||||
|
||||
---
|
||||
|
||||
### Q4: 充值订单是否需要支持优惠券/折扣?
|
||||
|
||||
**当前设计**:充值订单不支持任何优惠。
|
||||
|
||||
**待确认**:
|
||||
- 未来是否需要支持充值满减、折扣等营销活动?
|
||||
- 如果需要,是否在本次实现?
|
||||
|
||||
**影响**:如果需要支持,需要设计优惠券系统,超出本次范围。
|
||||
|
||||
**建议**:留待后续实现,本次保持简单。
|
||||
@@ -0,0 +1,82 @@
|
||||
## Why
|
||||
|
||||
当前系统缺少完整的强充(强制充值)机制和代购订单支持,导致以下问题:(1) 个人客户无法直接给钱包充值,必须通过购买套餐间接充值;(2) 平台和代理无法为其他代理代购套餐(线下已收款场景);(3) 一次性佣金触发机制不完善,代购订单错误触发佣金且累加充值金额;(4) 缺少强充预检接口,前端无法提前告知用户充值限制。这些限制影响了业务灵活性和用户体验,需要立即完善。
|
||||
|
||||
## What Changes
|
||||
|
||||
- **新增钱包充值系统**:实现个人客户直接充值钱包功能,包含充值订单(RechargeRecord)的创建、支付、回调处理,充值成功触发佣金计算
|
||||
- **新增强充预检接口**:提供钱包充值预检和套餐购买预检接口,返回强充要求、金额限制、实际支付金额等信息
|
||||
- **新增代购订单功能**:支持平台/代理给其他代理代购套餐,使用线下支付方式,订单标记为代购类型
|
||||
- **扩展强充配置**:ShopSeriesAllocation 模型新增 `enable_force_recharge` 和 `force_recharge_amount` 字段,支持累计充值强充配置(可选)
|
||||
- **修复佣金计算逻辑**:代购订单不触发一次性佣金,不累加 `AccumulatedRecharge`,确保佣金计算准确性
|
||||
- **扩展订单模型**:Order 模型新增 `is_purchase_on_behalf` 字段和 `offline` 支付方式,区分代购订单和普通订单
|
||||
- **完善充值验证**:创建充值订单和购买订单时强制验证强充要求,防止前端绕过限制
|
||||
|
||||
## Capabilities
|
||||
|
||||
### New Capabilities
|
||||
|
||||
- `wallet-recharge`: 钱包充值系统,包含充值订单创建、支付集成(微信/支付宝)、回调处理、充值成功后触发佣金计算
|
||||
- `force-recharge-check`: 强充预检接口,包含钱包充值预检、套餐购买预检,返回强充要求和金额限制
|
||||
- `purchase-on-behalf`: 代购订单功能,支持平台/代理为其他代理代购套餐,使用线下支付,区分代购和普通订单
|
||||
|
||||
### Modified Capabilities
|
||||
|
||||
- `commission-calculation`: 修改佣金计算逻辑,代购订单不触发一次性佣金,不累加 AccumulatedRecharge
|
||||
- `order-management`: 订单模型增加 `is_purchase_on_behalf` 字段,支持代购订单类型
|
||||
- `order-payment`: 支付方式增加 `offline` 线下支付,代购订单创建后直接标记为已支付
|
||||
- `shop-series-allocation`: 增加强充配置字段(`enable_force_recharge`、`force_recharge_amount`),支持累计充值强充设置
|
||||
|
||||
## Impact
|
||||
|
||||
### 数据库变更
|
||||
- **tb_order 表**:新增 `is_purchase_on_behalf` 字段(BOOLEAN),`payment_method` 增加 `offline` 枚举值
|
||||
- **tb_shop_series_allocation 表**:新增 `enable_force_recharge` 字段(BOOLEAN)、`force_recharge_amount` 字段(BIGINT)
|
||||
- **tb_recharge_record 表**:已存在但未使用,需要创建对应的 Store/Service/Handler
|
||||
- **数据库迁移**:需要创建迁移文件添加新字段
|
||||
|
||||
### 新增代码模块
|
||||
- **Store 层**:`RechargeStore`(充值订单数据访问)
|
||||
- **Service 层**:`RechargeService`(充值业务逻辑)、强充预检逻辑(在现有 Service 中)
|
||||
- **Handler 层**:`RechargeHandler`(充值 HTTP 接口)、充值预检接口(在现有 Handler 中)
|
||||
- **Task 层**:充值支付回调处理(在现有 callback handler 中扩展)
|
||||
|
||||
### 修改现有代码
|
||||
- **CommissionCalculationService**:增加代购订单判断逻辑
|
||||
- **OrderService**:增加代购订单创建逻辑、强充验证逻辑
|
||||
- **OrderHandler**(admin):增加平台创建代购订单接口
|
||||
- **ShopSeriesAllocationService**:支持强充配置的创建和更新
|
||||
|
||||
### API 变更
|
||||
- **新增接口**:
|
||||
- `GET /api/h5/wallets/recharge-check` - 钱包充值预检
|
||||
- `POST /api/h5/recharge-records` - 创建充值订单
|
||||
- `GET /api/h5/recharge-records` - 查询充值订单列表
|
||||
- `GET /api/h5/recharge-records/:id` - 查询充值订单详情
|
||||
- `POST /api/h5/orders/purchase-check` - 套餐购买预检
|
||||
- `POST /api/admin/orders` - 修改以支持代购订单创建
|
||||
- **修改接口**:
|
||||
- `POST /api/admin/shop-series-allocations` - 支持强充配置参数
|
||||
- `PUT /api/admin/shop-series-allocations/:id` - 支持强充配置更新
|
||||
|
||||
### 支付回调处理
|
||||
- **微信支付回调**:扩展支持充值订单的回调处理
|
||||
- **支付宝回调**:扩展支持充值订单的回调处理
|
||||
|
||||
### 业务逻辑影响
|
||||
- **佣金计算**:代购订单不触发一次性佣金,但仍计算差价佣金
|
||||
- **累计充值**:只有真实充值(个人客户充值或购买套餐)才累加 AccumulatedRecharge
|
||||
- **强充触发**:
|
||||
- 首次充值:必须充值阈值金额(OneTimeCommissionThreshold)
|
||||
- 累计充值:如果启用强充,必须充值固定金额(ForceRechargeAmount)
|
||||
- **订单支付**:代购订单创建后直接标记为已支付,跳过钱包扣款
|
||||
|
||||
### 测试影响
|
||||
- **单元测试**:需要为所有新增 Service 方法编写测试
|
||||
- **集成测试**:需要测试完整的充值流程、强充预检、代购订单流程
|
||||
- **测试覆盖率**:核心业务逻辑测试覆盖率需保持 ≥ 90%
|
||||
|
||||
### 性能考虑
|
||||
- 预检接口响应时间 < 100ms(涉及数据库查询)
|
||||
- 充值订单创建响应时间 < 200ms
|
||||
- 支付回调处理时间 < 500ms(异步处理佣金计算)
|
||||
@@ -0,0 +1,103 @@
|
||||
## MODIFIED Requirements
|
||||
|
||||
### Requirement: 订单支付后触发佣金计算
|
||||
|
||||
系统 SHALL 在订单支付成功后自动触发佣金计算。计算通过异步任务执行。代购订单和普通订单的佣金计算逻辑不同。
|
||||
|
||||
#### Scenario: 普通订单支付成功触发计算
|
||||
- **WHEN** 普通订单(is_purchase_on_behalf = false)支付状态变为已支付
|
||||
- **THEN** 系统发送佣金计算异步任务
|
||||
|
||||
#### Scenario: 代购订单支付成功触发计算
|
||||
- **WHEN** 代购订单(is_purchase_on_behalf = true)创建成功(自动已支付)
|
||||
- **THEN** 系统发送佣金计算异步任务
|
||||
|
||||
#### Scenario: 重复支付不重复计算
|
||||
- **WHEN** 订单已计算过佣金(commission_status=2)
|
||||
- **THEN** 系统不重复触发计算
|
||||
|
||||
---
|
||||
|
||||
### Requirement: 更新累计充值金额
|
||||
|
||||
订单支付成功后系统 SHALL 更新卡/设备的累计充值金额,但代购订单除外。
|
||||
|
||||
**关键修复**:每次真实充值(个人客户充值或购买套餐)都必须写回累计充值金额,代购订单不更新。
|
||||
|
||||
#### Scenario: 普通单卡订单更新累计充值
|
||||
- **WHEN** 普通单卡订单(is_purchase_on_behalf = false)支付成功,金额 100 元
|
||||
- **THEN** 系统读取 IotCard.accumulated_recharge 当前值
|
||||
- **AND** 增加 10000 分(100 元 = 10000 分)
|
||||
- **AND** 将新值写回 IotCard.accumulated_recharge
|
||||
- **AND** 使用更新后的累计值判断是否触发一次性佣金
|
||||
|
||||
#### Scenario: 普通设备订单更新累计充值
|
||||
- **WHEN** 普通设备订单(is_purchase_on_behalf = false)支付成功,金额 300 元
|
||||
- **THEN** 系统读取 Device.accumulated_recharge 当前值
|
||||
- **AND** 增加 30000 分(300 元 = 30000 分)
|
||||
- **AND** 将新值写回 Device.accumulated_recharge
|
||||
- **AND** 使用更新后的累计值判断是否触发一次性佣金
|
||||
|
||||
#### Scenario: 代购订单不更新累计充值
|
||||
- **WHEN** 代购订单(is_purchase_on_behalf = true)完成,金额 100 元
|
||||
- **THEN** 系统不更新卡/设备的 accumulated_recharge 字段
|
||||
- **AND** accumulated_recharge 保持原值
|
||||
|
||||
#### Scenario: 累计充值更新使用原子操作
|
||||
- **WHEN** 更新累计充值金额
|
||||
- **THEN** 系统使用 SQL 原子操作(如 `accumulated_recharge = accumulated_recharge + ?`)
|
||||
- **OR** 使用 GORM 乐观锁(version 字段)
|
||||
- **AND** 确保并发场景下累计值不会丢失
|
||||
|
||||
#### Scenario: 更新失败不影响佣金计算
|
||||
- **WHEN** 累计充值金额更新失败(数据库错误、并发冲突等)
|
||||
- **THEN** 系统记录错误日志
|
||||
- **AND** 继续执行后续的佣金计算流程(成本价差、一次性佣金等)
|
||||
- **AND** 不因累计值更新失败而导致整个佣金计算失败
|
||||
|
||||
---
|
||||
|
||||
## ADDED Requirements
|
||||
|
||||
### Requirement: 代购订单佣金计算规则
|
||||
|
||||
代购订单 SHALL 计算差价佣金,但不触发一次性佣金。
|
||||
|
||||
#### Scenario: 代购订单计算差价佣金
|
||||
- **WHEN** 代购订单(is_purchase_on_behalf = true)完成,买家有上级代理
|
||||
- **THEN** 系统计算差价佣金(买家成本价 - 上级成本价),发放给上级代理链
|
||||
|
||||
#### Scenario: 代购订单不触发一次性佣金
|
||||
- **WHEN** 代购订单完成,佣金计算时检查订单类型
|
||||
- **THEN** 系统跳过一次性佣金判断逻辑,不发放一次性佣金
|
||||
|
||||
#### Scenario: 代购订单示例
|
||||
- **WHEN** 平台为三级代理代购,订单金额 100 元(三级成本价),各级成本价:一级 60 → 二级 70 → 三级 80
|
||||
- **THEN** 二级获得 10 元(80 - 70)差价佣金,一级获得 10 元(70 - 60)差价佣金
|
||||
- **AND** 三级、二级、一级都不获得一次性佣金
|
||||
|
||||
---
|
||||
|
||||
### Requirement: 钱包充值触发一次性佣金
|
||||
|
||||
钱包充值成功后 SHALL 更新累计充值,并检查是否触发一次性佣金。
|
||||
|
||||
#### Scenario: 充值成功更新累计充值
|
||||
- **WHEN** 卡钱包充值 100 元成功,当前累计充值 200 元
|
||||
- **THEN** 系统更新卡的 accumulated_recharge 为 300 元
|
||||
|
||||
#### Scenario: 充值达到首次充值阈值
|
||||
- **WHEN** 卡配置为首次充值触发,阈值 100 元,充值 100 元成功,未发放过佣金
|
||||
- **THEN** 系统触发一次性佣金计算,发放佣金,标记 first_commission_paid = true
|
||||
|
||||
#### Scenario: 充值达到累计充值阈值
|
||||
- **WHEN** 卡配置为累计充值触发,阈值 1000 元,充值后累计达到 1000 元,未发放过佣金
|
||||
- **THEN** 系统触发一次性佣金计算,发放佣金,标记 first_commission_paid = true
|
||||
|
||||
#### Scenario: 充值未达阈值不触发
|
||||
- **WHEN** 充值后累计充值未达到阈值
|
||||
- **THEN** 系统不触发一次性佣金计算
|
||||
|
||||
#### Scenario: 已发放过不重复触发
|
||||
- **WHEN** 卡的一次性佣金已发放过(first_commission_paid = true)
|
||||
- **THEN** 系统不触发一次性佣金计算
|
||||
@@ -0,0 +1,109 @@
|
||||
## ADDED Requirements
|
||||
|
||||
### Requirement: 钱包充值预检
|
||||
|
||||
系统 SHALL 提供钱包充值预检接口,返回强充要求、允许的充值金额等信息。
|
||||
|
||||
#### Scenario: 无强充要求
|
||||
- **WHEN** 客户查询卡钱包充值预检,卡配置为累计充值触发且未启用强充
|
||||
- **THEN** 系统返回 need_force_recharge = false,min_amount = 100(1元),max_amount = null
|
||||
|
||||
#### Scenario: 首次充值强充
|
||||
- **WHEN** 客户查询卡钱包充值预检,卡配置为首次充值触发,阈值 10000 分(100元),未发放佣金
|
||||
- **THEN** 系统返回 need_force_recharge = true,force_recharge_amount = 10000,trigger_type = "single_recharge",message = "首次充值需充值100元"
|
||||
|
||||
#### Scenario: 累计充值启用强充
|
||||
- **WHEN** 客户查询卡钱包充值预检,卡配置为累计充值触发,启用强充,强充金额 10000 分(100元)
|
||||
- **THEN** 系统返回 need_force_recharge = true,force_recharge_amount = 10000,trigger_type = "accumulated_recharge",message = "每次充值需充值100元"
|
||||
|
||||
#### Scenario: 一次性佣金已发放
|
||||
- **WHEN** 客户查询卡钱包充值预检,卡的一次性佣金已发放过
|
||||
- **THEN** 系统返回 need_force_recharge = false(不再强充)
|
||||
|
||||
#### Scenario: 未启用一次性佣金
|
||||
- **WHEN** 客户查询卡钱包充值预检,卡关联系列未启用一次性佣金
|
||||
- **THEN** 系统返回 need_force_recharge = false
|
||||
|
||||
---
|
||||
|
||||
### Requirement: 套餐购买预检
|
||||
|
||||
系统 SHALL 提供套餐购买预检接口,计算实际支付金额、钱包到账金额等信息。
|
||||
|
||||
#### Scenario: 无强充要求正常购买
|
||||
- **WHEN** 客户购买 90 元套餐,无强充要求
|
||||
- **THEN** 系统返回 total_package_amount = 9000,need_force_recharge = false,actual_payment = 9000,wallet_credit = 0
|
||||
|
||||
#### Scenario: 首次充值强充,套餐价低于阈值
|
||||
- **WHEN** 客户购买 90 元套餐,首次充值阈值 100 元
|
||||
- **THEN** 系统返回 total_package_amount = 9000,need_force_recharge = true,force_recharge_amount = 10000,actual_payment = 10000,wallet_credit = 1000,message = "需充值100元,购买套餐后余额10元"
|
||||
|
||||
#### Scenario: 首次充值强充,套餐价高于阈值
|
||||
- **WHEN** 客户购买 150 元套餐,首次充值阈值 100 元
|
||||
- **THEN** 系统返回 total_package_amount = 15000,need_force_recharge = true,force_recharge_amount = 10000,actual_payment = 15000,wallet_credit = 0,message = "套餐总价150元,无需额外充值"
|
||||
|
||||
#### Scenario: 首次充值强充,套餐价等于阈值
|
||||
- **WHEN** 客户购买 100 元套餐,首次充值阈值 100 元
|
||||
- **THEN** 系统返回 total_package_amount = 10000,need_force_recharge = true,force_recharge_amount = 10000,actual_payment = 10000,wallet_credit = 0
|
||||
|
||||
#### Scenario: 累计充值启用强充,套餐价低于强充金额
|
||||
- **WHEN** 客户购买 50 元套餐,累计充值启用强充,强充金额 100 元
|
||||
- **THEN** 系统返回 actual_payment = 10000,wallet_credit = 5000,message = "需充值100元,购买套餐后余额50元"
|
||||
|
||||
#### Scenario: 累计充值启用强充,套餐价高于强充金额
|
||||
- **WHEN** 客户购买 150 元套餐,累计充值启用强充,强充金额 100 元
|
||||
- **THEN** 系统返回 actual_payment = 15000,wallet_credit = 0,message = "套餐总价150元,无需额外充值"
|
||||
|
||||
#### Scenario: 购买多个套餐
|
||||
- **WHEN** 客户购买 3 个套餐,总价 120 元,首次充值阈值 100 元
|
||||
- **THEN** 系统返回 total_package_amount = 12000,actual_payment = 12000,wallet_credit = 0
|
||||
|
||||
---
|
||||
|
||||
### Requirement: 预检接口响应格式
|
||||
|
||||
预检接口响应 SHALL 包含完整的充值/购买指引信息。
|
||||
|
||||
#### Scenario: 充值预检响应字段
|
||||
- **WHEN** 调用钱包充值预检接口
|
||||
- **THEN** 响应包含:need_force_recharge, force_recharge_amount, trigger_type, min_amount, max_amount, current_accumulated, threshold, message
|
||||
|
||||
#### Scenario: 购买预检响应字段
|
||||
- **WHEN** 调用套餐购买预检接口
|
||||
- **THEN** 响应包含:total_package_amount, need_force_recharge, force_recharge_amount, actual_payment, wallet_credit, message
|
||||
|
||||
---
|
||||
|
||||
### Requirement: 预检接口性能
|
||||
|
||||
预检接口响应时间 MUST 小于 100ms。
|
||||
|
||||
#### Scenario: 快速响应
|
||||
- **WHEN** 调用预检接口
|
||||
- **THEN** 系统在 100ms 内返回结果
|
||||
|
||||
#### Scenario: 缓存系列分配配置
|
||||
- **WHEN** 频繁查询同一卡的预检信息
|
||||
- **THEN** 系统可以缓存系列分配配置,减少数据库查询
|
||||
|
||||
---
|
||||
|
||||
### Requirement: 预检接口错误处理
|
||||
|
||||
预检接口 SHALL 正确处理异常情况。
|
||||
|
||||
#### Scenario: 卡不存在
|
||||
- **WHEN** 查询不存在的卡的充值预检
|
||||
- **THEN** 系统返回错误 "卡不存在"
|
||||
|
||||
#### Scenario: 卡未关联系列
|
||||
- **WHEN** 查询未关联套餐系列的卡的充值预检
|
||||
- **THEN** 系统返回 need_force_recharge = false(无系列分配,无强充要求)
|
||||
|
||||
#### Scenario: 设备不存在
|
||||
- **WHEN** 查询不存在的设备的充值预检
|
||||
- **THEN** 系统返回错误 "设备不存在"
|
||||
|
||||
#### Scenario: 套餐不存在
|
||||
- **WHEN** 套餐购买预检时,套餐 ID 不存在
|
||||
- **THEN** 系统返回错误 "套餐不存在"
|
||||
@@ -0,0 +1,93 @@
|
||||
## ADDED Requirements
|
||||
|
||||
### Requirement: 订单类型标识
|
||||
|
||||
系统 SHALL 在订单模型中增加 is_purchase_on_behalf 字段,标识是否为代购订单。
|
||||
|
||||
#### Scenario: 普通订单创建
|
||||
- **WHEN** 个人客户或代理为自己创建订单
|
||||
- **THEN** 系统设置 is_purchase_on_behalf = false
|
||||
|
||||
#### Scenario: 代购订单创建
|
||||
- **WHEN** 平台或代理为其他代理创建代购订单
|
||||
- **THEN** 系统设置 is_purchase_on_behalf = true
|
||||
|
||||
#### Scenario: 查询订单列表返回订单类型
|
||||
- **WHEN** 查询订单列表或详情
|
||||
- **THEN** 响应包含 is_purchase_on_behalf 字段
|
||||
|
||||
---
|
||||
|
||||
## MODIFIED Requirements
|
||||
|
||||
### Requirement: 创建套餐购买订单
|
||||
|
||||
系统 SHALL 允许买家创建套餐购买订单。订单类型分为单卡购买和设备购买。创建前 MUST 验证购买权限和强充要求。
|
||||
|
||||
#### Scenario: 个人客户创建单卡订单
|
||||
- **WHEN** 个人客户为自己的卡创建订单,选择一个套餐
|
||||
- **THEN** 系统创建订单,状态为待支付,is_purchase_on_behalf = false,返回订单信息
|
||||
|
||||
#### Scenario: 个人客户创建设备订单
|
||||
- **WHEN** 个人客户为自己的设备创建订单
|
||||
- **THEN** 系统创建订单,订单类型为设备购买,is_purchase_on_behalf = false
|
||||
|
||||
#### Scenario: 代理创建订单
|
||||
- **WHEN** 代理为店铺关联的卡/设备创建订单
|
||||
- **THEN** 系统创建订单,买家类型为代理商,买家ID为店铺ID,is_purchase_on_behalf = false
|
||||
|
||||
#### Scenario: 平台创建代购订单
|
||||
- **WHEN** 平台账号为代理的卡/设备创建订单,支付方式选择 offline
|
||||
- **THEN** 系统创建订单,is_purchase_on_behalf = true,payment_method = "offline",payment_status = 2(已支付)
|
||||
|
||||
#### Scenario: 套餐购买验证强充要求
|
||||
- **WHEN** 个人客户创建订单,存在强充要求,订单金额低于强充金额
|
||||
- **THEN** 系统返回错误 "支付金额不符合强充要求"
|
||||
|
||||
#### Scenario: 套餐不在可购买范围
|
||||
- **WHEN** 买家尝试购买不在关联系列下的套餐
|
||||
- **THEN** 系统返回错误 "该套餐不在可购买范围内"
|
||||
|
||||
#### Scenario: 套餐已下架
|
||||
- **WHEN** 买家尝试购买已下架的套餐
|
||||
- **THEN** 系统返回错误 "该套餐已下架"
|
||||
|
||||
---
|
||||
|
||||
### Requirement: 查询订单列表
|
||||
|
||||
系统 SHALL 提供订单列表查询,支持按支付状态、订单类型、是否代购筛选。
|
||||
|
||||
#### Scenario: 个人客户查询自己的订单
|
||||
- **WHEN** 个人客户查询订单列表
|
||||
- **THEN** 系统只返回该客户的订单
|
||||
|
||||
#### Scenario: 代理查询店铺订单
|
||||
- **WHEN** 代理查询订单列表
|
||||
- **THEN** 系统返回该店铺及下级店铺的订单(包含代购订单和普通订单)
|
||||
|
||||
#### Scenario: 按代购类型筛选
|
||||
- **WHEN** 指定 is_purchase_on_behalf = true 筛选
|
||||
- **THEN** 系统只返回代购订单
|
||||
|
||||
#### Scenario: 按支付状态筛选
|
||||
- **WHEN** 指定支付状态筛选
|
||||
- **THEN** 系统只返回匹配状态的订单
|
||||
|
||||
---
|
||||
|
||||
### Requirement: 取消订单
|
||||
|
||||
系统 SHALL 允许买家取消未支付的订单,但代购订单不可取消。
|
||||
|
||||
#### Scenario: 取消待支付的普通订单
|
||||
- **WHEN** 买家取消一个待支付的普通订单(is_purchase_on_behalf = false)
|
||||
- **THEN** 系统更新订单状态为已取消
|
||||
|
||||
#### Scenario: 取消已支付订单
|
||||
- **WHEN** 买家尝试取消已支付的订单
|
||||
- **THEN** 系统返回错误 "已支付订单无法取消"
|
||||
|
||||
#### Scenario: 尝试取消代购订单
|
||||
- **WHEN** 买家尝试取消代购订单(is_purchase_on_behalf = true)
|
||||
- **THEN** 系统返回错误 "代购订单不可取消"
|
||||
@@ -0,0 +1,75 @@
|
||||
## ADDED Requirements
|
||||
|
||||
### Requirement: 线下支付方式
|
||||
|
||||
系统 SHALL 支持线下支付方式(offline),仅用于代购订单。线下支付的订单创建后直接标记为已支付,跳过支付流程。
|
||||
|
||||
#### Scenario: 创建线下支付订单
|
||||
- **WHEN** 平台账号创建订单时选择支付方式为 offline
|
||||
- **THEN** 系统创建订单,payment_status 直接设为 2(已支付),payment_method = "offline"
|
||||
|
||||
#### Scenario: 线下支付权限限制
|
||||
- **WHEN** 非平台账号(代理/个人客户)尝试使用线下支付
|
||||
- **THEN** 系统返回错误 "只有平台账号可以使用线下支付"
|
||||
|
||||
#### Scenario: 线下支付订单自动激活套餐
|
||||
- **WHEN** 创建线下支付订单成功
|
||||
- **THEN** 系统自动激活套餐,创建 PackageUsage 记录
|
||||
|
||||
#### Scenario: 线下支付不扣钱包
|
||||
- **WHEN** 订单使用线下支付
|
||||
- **THEN** 系统不扣减任何钱包余额
|
||||
|
||||
---
|
||||
|
||||
## MODIFIED Requirements
|
||||
|
||||
### Requirement: 第三方支付回调
|
||||
|
||||
系统 SHALL 处理微信支付和支付宝的支付回调,支持订单支付和钱包充值两种场景。回调处理 MUST 幂等。
|
||||
|
||||
#### Scenario: 微信支付成功回调(订单)
|
||||
- **WHEN** 收到微信支付成功回调,订单号格式为 ORD 开头
|
||||
- **THEN** 系统验证签名,更新订单状态,激活套餐,返回成功响应
|
||||
|
||||
#### Scenario: 微信支付成功回调(充值)
|
||||
- **WHEN** 收到微信支付成功回调,订单号格式为 RCH 开头
|
||||
- **THEN** 系统验证签名,更新充值订单状态,增加钱包余额,更新累计充值,触发佣金判断,返回成功响应
|
||||
|
||||
#### Scenario: 支付宝成功回调(订单)
|
||||
- **WHEN** 收到支付宝支付成功回调,订单号格式为 ORD 开头
|
||||
- **THEN** 系统验证签名,更新订单状态,激活套餐,返回成功响应
|
||||
|
||||
#### Scenario: 支付宝成功回调(充值)
|
||||
- **WHEN** 收到支付宝支付成功回调,订单号格式为 RCH 开头
|
||||
- **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** 有效期 = 激活时间 + 套餐时长(月)
|
||||
|
||||
#### Scenario: 代购订单激活套餐
|
||||
- **WHEN** 代购订单(is_purchase_on_behalf = true)创建成功
|
||||
- **THEN** 系统激活套餐,但不更新卡/设备的 accumulated_recharge
|
||||
@@ -0,0 +1,139 @@
|
||||
## ADDED Requirements
|
||||
|
||||
### Requirement: 平台创建代购订单
|
||||
|
||||
系统 SHALL 允许平台账号为代理创建代购订单,使用线下支付方式,订单创建后直接标记为已支付。
|
||||
|
||||
#### Scenario: 平台为一级代理代购
|
||||
- **WHEN** 平台账号为一级代理的卡创建代购订单,选择套餐,支付方式为线下支付
|
||||
- **THEN** 系统创建订单,buyer_id = 一级代理店铺ID,is_purchase_on_behalf = true,payment_method = "offline",payment_status = 2(已支付)
|
||||
|
||||
#### Scenario: 平台为二级代理代购
|
||||
- **WHEN** 平台账号为二级代理的卡创建代购订单
|
||||
- **THEN** 系统创建订单,buyer_id = 二级代理店铺ID,is_purchase_on_behalf = true
|
||||
|
||||
#### Scenario: 代购订单价格使用代理成本价
|
||||
- **WHEN** 平台为代理创建代购订单,套餐价格 100 元,代理成本价 80 元
|
||||
- **THEN** 订单金额为 80 元(代理成本价)
|
||||
|
||||
#### Scenario: 查询卡归属代理
|
||||
- **WHEN** 平台选择卡创建代购订单
|
||||
- **THEN** 系统查询卡的 shop_id,作为订单的 buyer_id
|
||||
|
||||
#### Scenario: 查询设备归属代理
|
||||
- **WHEN** 平台选择设备创建代购订单
|
||||
- **THEN** 系统查询设备的 shop_id,作为订单的 buyer_id
|
||||
|
||||
---
|
||||
|
||||
### Requirement: 代理创建代购订单
|
||||
|
||||
系统 SHALL 允许代理账号为其他代理(通常是下级代理)创建代购订单。
|
||||
|
||||
#### Scenario: 一级代理为二级代理代购
|
||||
- **WHEN** 一级代理为二级代理的卡创建代购订单,选择套餐,支付方式为线下支付
|
||||
- **THEN** 系统创建订单,buyer_id = 二级代理店铺ID,is_purchase_on_behalf = true,payment_method = "offline"
|
||||
|
||||
#### Scenario: 代购订单使用买家成本价
|
||||
- **WHEN** 一级代理为二级代理代购,套餐价格 100 元,二级代理成本价 90 元
|
||||
- **THEN** 订单金额为 90 元(买家成本价)
|
||||
|
||||
---
|
||||
|
||||
### Requirement: 代购订单自动完成
|
||||
|
||||
代购订单创建后 SHALL 自动完成支付流程,激活套餐,但不触发一次性佣金。
|
||||
|
||||
#### Scenario: 代购订单自动激活套餐
|
||||
- **WHEN** 创建代购订单成功
|
||||
- **THEN** 系统自动激活套餐(创建 PackageUsage 记录)
|
||||
|
||||
#### Scenario: 代购订单不扣钱包
|
||||
- **WHEN** 创建代购订单
|
||||
- **THEN** 系统不扣减任何钱包余额(线下已收款)
|
||||
|
||||
#### Scenario: 代购订单不更新累计充值
|
||||
- **WHEN** 代购订单完成
|
||||
- **THEN** 系统不更新卡/设备的 accumulated_recharge 字段
|
||||
|
||||
#### Scenario: 代购订单计算差价佣金
|
||||
- **WHEN** 代购订单完成,买家有上级代理
|
||||
- **THEN** 系统计算差价佣金(买家成本价 - 上级成本价),发放给上级代理
|
||||
|
||||
#### Scenario: 代购订单不触发一次性佣金
|
||||
- **WHEN** 代购订单完成
|
||||
- **THEN** 系统不检查一次性佣金阈值,不发放一次性佣金
|
||||
|
||||
---
|
||||
|
||||
### Requirement: 代购订单查询
|
||||
|
||||
系统 SHALL 在订单列表中正确显示代购订单。
|
||||
|
||||
#### Scenario: 平台查询代购订单
|
||||
- **WHEN** 平台账号查询订单列表
|
||||
- **THEN** 系统返回所有代购订单(is_purchase_on_behalf = true),包含创建人信息
|
||||
|
||||
#### Scenario: 代理查询收到的代购订单
|
||||
- **WHEN** 代理查询订单列表,包含别人为自己代购的订单
|
||||
- **THEN** 系统返回买家为自己的代购订单
|
||||
|
||||
#### Scenario: 代购订单标识
|
||||
- **WHEN** 查询订单详情
|
||||
- **THEN** 订单响应包含 is_purchase_on_behalf 字段,前端可以显示"代购订单"标签
|
||||
|
||||
---
|
||||
|
||||
### Requirement: 代购订单权限控制
|
||||
|
||||
系统 SHALL 严格控制代购订单的创建权限。
|
||||
|
||||
#### Scenario: 只有平台账号可以使用线下支付
|
||||
- **WHEN** 代理账号尝试创建订单时选择支付方式为 offline
|
||||
- **THEN** 系统返回错误 "只有平台账号可以使用线下支付"
|
||||
|
||||
#### Scenario: 平台账号可以为任何代理代购
|
||||
- **WHEN** 平台账号为任意层级代理创建代购订单
|
||||
- **THEN** 系统允许创建
|
||||
|
||||
#### Scenario: 代理账号只能为下级代理代购
|
||||
- **WHEN** 代理账号尝试为上级或平级代理创建代购订单
|
||||
- **THEN** 系统返回错误 "只能为下级代理代购套餐"
|
||||
|
||||
---
|
||||
|
||||
### Requirement: 代购订单记录
|
||||
|
||||
系统 SHALL 完整记录代购订单的创建人和买家信息。
|
||||
|
||||
#### Scenario: 记录创建人
|
||||
- **WHEN** 平台/代理创建代购订单
|
||||
- **THEN** 订单的 creator 字段记录创建人账号ID
|
||||
|
||||
#### Scenario: 区分创建人和买家
|
||||
- **WHEN** 查询代购订单详情
|
||||
- **THEN** creator(创建人)!= buyer_id(买家),可以追溯是谁代购的
|
||||
|
||||
---
|
||||
|
||||
### Requirement: 代购订单不可取消
|
||||
|
||||
代购订单创建后 SHALL 不可取消,因为已自动完成。
|
||||
|
||||
#### Scenario: 尝试取消代购订单
|
||||
- **WHEN** 尝试取消一个代购订单(is_purchase_on_behalf = true)
|
||||
- **THEN** 系统返回错误 "代购订单不可取消"
|
||||
|
||||
---
|
||||
|
||||
### Requirement: 线下支付方式常量
|
||||
|
||||
系统 SHALL 定义线下支付方式常量。
|
||||
|
||||
#### Scenario: 支付方式枚举
|
||||
- **WHEN** 创建订单时选择支付方式
|
||||
- **THEN** payment_method 可选值包含:wallet, wechat, alipay, offline
|
||||
|
||||
#### Scenario: 线下支付只用于代购
|
||||
- **WHEN** payment_method = "offline"
|
||||
- **THEN** 订单必须标记为 is_purchase_on_behalf = true
|
||||
@@ -0,0 +1,85 @@
|
||||
## ADDED Requirements
|
||||
|
||||
### Requirement: 强充配置
|
||||
|
||||
系统 SHALL 在套餐系列分配中支持强充配置。仅累计充值触发时可选启用强充,首次充值触发时强充是必须的(无需配置)。
|
||||
|
||||
#### Scenario: 累计充值启用强充
|
||||
- **WHEN** 创建系列分配,一次性佣金触发类型为累计充值,设置 enable_force_recharge = true,force_recharge_amount = 10000(100元)
|
||||
- **THEN** 系统保存强充配置,下级客户每次充值/购买必须充值 100 元
|
||||
|
||||
#### Scenario: 累计充值不启用强充
|
||||
- **WHEN** 创建系列分配,一次性佣金触发类型为累计充值,设置 enable_force_recharge = false
|
||||
- **THEN** 系统保存配置,下级客户可以自由充值任意金额
|
||||
|
||||
#### Scenario: 首次充值无需设置强充
|
||||
- **WHEN** 创建系列分配,一次性佣金触发类型为首次充值,阈值 10000(100元)
|
||||
- **THEN** 系统使用阈值作为强充金额,无需单独配置 force_recharge_amount
|
||||
|
||||
#### Scenario: 强充金额为0表示使用阈值
|
||||
- **WHEN** 创建系列分配,启用强充,force_recharge_amount = 0
|
||||
- **THEN** 系统使用一次性佣金阈值作为强充金额
|
||||
|
||||
---
|
||||
|
||||
## MODIFIED Requirements
|
||||
|
||||
### Requirement: 为下级店铺分配套餐系列
|
||||
|
||||
系统 SHALL 允许代理为其直属下级店铺分配套餐系列。分配时 MUST 指定基础返佣配置(返佣模式和返佣值),MAY 启用一次性佣金和强充配置。分配者只能分配自己已被分配的套餐系列。
|
||||
|
||||
#### Scenario: 成功分配套餐系列
|
||||
- **WHEN** 代理为直属下级店铺分配一个自己拥有的套餐系列,设置基础返佣为百分比200(20%)
|
||||
- **THEN** 系统创建分配记录
|
||||
|
||||
#### Scenario: 分配时启用一次性佣金和强充
|
||||
- **WHEN** 代理为下级分配系列,启用一次性佣金,触发类型为累计充值,阈值 100000(1000元),启用强充,强充金额 10000(100元)
|
||||
- **THEN** 系统保存配置:enable_one_time_commission = true,trigger = "accumulated_recharge",threshold = 100000,enable_force_recharge = true,force_recharge_amount = 10000
|
||||
|
||||
#### Scenario: 尝试分配未拥有的系列
|
||||
- **WHEN** 代理尝试分配自己未被分配的套餐系列
|
||||
- **THEN** 系统返回错误 "您没有该套餐系列的分配权限"
|
||||
|
||||
#### Scenario: 尝试分配给非直属下级
|
||||
- **WHEN** 代理尝试分配给非直属下级店铺
|
||||
- **THEN** 系统返回错误 "只能为直属下级分配套餐"
|
||||
|
||||
#### Scenario: 重复分配同一系列
|
||||
- **WHEN** 代理尝试为同一下级店铺重复分配同一套餐系列
|
||||
- **THEN** 系统返回错误 "该店铺已分配此套餐系列"
|
||||
|
||||
---
|
||||
|
||||
### Requirement: 更新套餐系列分配
|
||||
|
||||
系统 SHALL 允许代理更新分配的基础返佣配置、一次性佣金配置和强充配置。更新返佣配置时 MUST 创建新的配置版本。
|
||||
|
||||
#### Scenario: 更新基础返佣配置时创建新版本
|
||||
- **WHEN** 代理将基础返佣从20%改为25%
|
||||
- **THEN** 系统更新分配记录,并创建新配置版本
|
||||
|
||||
#### Scenario: 更新强充配置
|
||||
- **WHEN** 代理将 enable_force_recharge 从 false 改为 true,设置 force_recharge_amount = 10000
|
||||
- **THEN** 系统更新分配记录,后续下级客户需遵守新强充要求
|
||||
|
||||
#### Scenario: 禁用强充
|
||||
- **WHEN** 代理将 enable_force_recharge 从 true 改为 false
|
||||
- **THEN** 系统更新分配记录,后续下级客户可以自由充值
|
||||
|
||||
#### Scenario: 更新不存在的分配
|
||||
- **WHEN** 代理更新不存在的分配 ID
|
||||
- **THEN** 系统返回 "分配记录不存在" 错误
|
||||
|
||||
---
|
||||
|
||||
### Requirement: 平台分配套餐系列
|
||||
|
||||
平台管理员 SHALL 能够为一级代理分配套餐系列,可配置强充要求。平台的成本价基准为 Package.suggested_cost_price。
|
||||
|
||||
#### Scenario: 平台为一级代理分配
|
||||
- **WHEN** 平台管理员为一级代理分配套餐系列
|
||||
- **THEN** 系统创建分配记录
|
||||
|
||||
#### Scenario: 平台配置强充要求
|
||||
- **WHEN** 平台为一级代理分配系列,启用强充,force_recharge_amount = 10000
|
||||
- **THEN** 系统保存强充配置,一级代理的客户需遵守强充要求
|
||||
@@ -0,0 +1,182 @@
|
||||
## ADDED Requirements
|
||||
|
||||
### Requirement: 创建钱包充值订单
|
||||
|
||||
系统 SHALL 允许个人客户创建钱包充值订单。创建前 MUST 验证强充要求,强充场景下充值金额必须等于要求的强充金额。
|
||||
|
||||
#### Scenario: 无强充要求时自由充值
|
||||
- **WHEN** 个人客户为卡/设备创建充值订单,该卡/设备无强充要求,充值金额 100 元
|
||||
- **THEN** 系统创建充值订单,状态为待支付,金额 10000 分
|
||||
|
||||
#### Scenario: 首次充值强充
|
||||
- **WHEN** 卡关联系列配置为首次充值触发,阈值 100 元,客户尝试充值 100 元
|
||||
- **THEN** 系统验证通过,创建充值订单,金额 10000 分
|
||||
|
||||
#### Scenario: 首次充值金额不符
|
||||
- **WHEN** 卡关联系列配置为首次充值触发,阈值 100 元,客户尝试充值 50 元
|
||||
- **THEN** 系统返回错误 "必须充值100元"
|
||||
|
||||
#### Scenario: 累计充值启用强充
|
||||
- **WHEN** 卡关联系列配置为累计充值触发,启用强充,强充金额 100 元,客户尝试充值 100 元
|
||||
- **THEN** 系统验证通过,创建充值订单
|
||||
|
||||
#### Scenario: 累计充值强充金额不符
|
||||
- **WHEN** 卡关联系列配置为累计充值触发,启用强充,强充金额 100 元,客户尝试充值 50 元
|
||||
- **THEN** 系统返回错误 "必须充值100元"
|
||||
|
||||
#### Scenario: 累计充值未启用强充
|
||||
- **WHEN** 卡关联系列配置为累计充值触发,未启用强充,客户充值任意金额
|
||||
- **THEN** 系统创建充值订单
|
||||
|
||||
#### Scenario: 充值订单号唯一
|
||||
- **WHEN** 创建充值订单
|
||||
- **THEN** 系统生成唯一充值单号,格式为 RCH + 14位时间戳 + 6位随机数
|
||||
|
||||
---
|
||||
|
||||
### Requirement: 查询充值订单列表
|
||||
|
||||
系统 SHALL 提供充值订单列表查询,支持按状态筛选、时间范围筛选。
|
||||
|
||||
#### Scenario: 查询个人客户的充值订单
|
||||
- **WHEN** 个人客户查询充值订单列表
|
||||
- **THEN** 系统返回该客户的所有充值订单
|
||||
|
||||
#### Scenario: 按状态筛选
|
||||
- **WHEN** 客户指定充值状态筛选(待支付/已支付/已完成)
|
||||
- **THEN** 系统只返回匹配状态的充值订单
|
||||
|
||||
#### Scenario: 分页查询
|
||||
- **WHEN** 查询充值订单列表
|
||||
- **THEN** 系统使用分页返回,默认每页 20 条,最大 100 条
|
||||
|
||||
---
|
||||
|
||||
### Requirement: 查询充值订单详情
|
||||
|
||||
系统 SHALL 允许个人客户查询充值订单详情。
|
||||
|
||||
#### Scenario: 查询自己的充值订单
|
||||
- **WHEN** 客户查询自己的充值订单详情
|
||||
- **THEN** 系统返回订单信息(充值单号、金额、支付方式、状态、时间等)
|
||||
|
||||
#### Scenario: 查询他人充值订单
|
||||
- **WHEN** 客户尝试查询不属于自己的充值订单
|
||||
- **THEN** 系统返回 "充值订单不存在" 错误
|
||||
|
||||
---
|
||||
|
||||
### Requirement: 充值支付(微信/支付宝)
|
||||
|
||||
系统 SHALL 支持通过微信支付和支付宝支付完成充值。
|
||||
|
||||
#### Scenario: 微信 JSAPI 支付
|
||||
- **WHEN** 客户在微信内选择充值,使用微信支付
|
||||
- **THEN** 系统调用微信支付 JSAPI 接口,返回支付参数
|
||||
|
||||
#### Scenario: 微信 H5 支付
|
||||
- **WHEN** 客户在浏览器内选择充值,使用微信支付
|
||||
- **THEN** 系统调用微信支付 H5 接口,返回支付跳转 URL
|
||||
|
||||
#### Scenario: 支付宝支付
|
||||
- **WHEN** 客户选择支付宝支付充值
|
||||
- **THEN** 系统调用支付宝接口,返回支付参数
|
||||
|
||||
---
|
||||
|
||||
### Requirement: 充值支付回调处理
|
||||
|
||||
系统 SHALL 处理微信和支付宝的支付回调,验证签名,更新充值订单状态,增加钱包余额。
|
||||
|
||||
#### Scenario: 微信支付回调成功
|
||||
- **WHEN** 收到微信支付成功回调,验证签名通过
|
||||
- **THEN** 系统更新充值订单状态为已支付
|
||||
- **AND** 增加对应钱包余额
|
||||
- **AND** 创建钱包交易记录
|
||||
- **AND** 返回成功响应给微信
|
||||
|
||||
#### Scenario: 支付宝回调成功
|
||||
- **WHEN** 收到支付宝支付成功回调,验证签名通过
|
||||
- **THEN** 系统更新充值订单状态为已支付
|
||||
- **AND** 增加对应钱包余额
|
||||
- **AND** 创建钱包交易记录
|
||||
|
||||
#### Scenario: 签名验证失败
|
||||
- **WHEN** 收到支付回调,签名验证失败
|
||||
- **THEN** 系统记录错误日志,不处理订单,返回失败响应
|
||||
|
||||
#### Scenario: 重复回调幂等处理
|
||||
- **WHEN** 收到同一充值订单的重复支付回调
|
||||
- **THEN** 系统检查订单状态,如果已支付则直接返回成功,不重复处理
|
||||
|
||||
---
|
||||
|
||||
### Requirement: 充值成功更新累计充值金额
|
||||
|
||||
充值支付成功后系统 SHALL 更新卡/设备的累计充值金额(AccumulatedRecharge)。
|
||||
|
||||
#### Scenario: 充值成功累加充值金额
|
||||
- **WHEN** 卡钱包充值 100 元成功,当前累计充值 200 元
|
||||
- **THEN** 系统更新卡的累计充值为 300 元(200 + 100)
|
||||
|
||||
#### Scenario: 设备充值成功累加充值金额
|
||||
- **WHEN** 设备钱包充值 200 元成功,当前累计充值 500 元
|
||||
- **THEN** 系统更新设备的累计充值为 700 元(500 + 200)
|
||||
|
||||
#### Scenario: 使用原子操作更新
|
||||
- **WHEN** 更新累计充值金额
|
||||
- **THEN** 系统使用 SQL 原子操作或 GORM 乐观锁确保并发安全
|
||||
|
||||
---
|
||||
|
||||
### Requirement: 充值成功触发一次性佣金判断
|
||||
|
||||
充值支付成功后系统 SHALL 检查是否达到一次性佣金阈值,如果达到则触发佣金计算。
|
||||
|
||||
#### Scenario: 首次充值达到阈值
|
||||
- **WHEN** 卡配置为首次充值触发,阈值 100 元,客户充值 100 元
|
||||
- **THEN** 系统触发一次性佣金计算,发放佣金
|
||||
|
||||
#### Scenario: 累计充值达到阈值
|
||||
- **WHEN** 卡配置为累计充值触发,阈值 1000 元,累计充值已达到 1000 元
|
||||
- **THEN** 系统触发一次性佣金计算,发放佣金
|
||||
|
||||
#### Scenario: 未达阈值不触发
|
||||
- **WHEN** 充值后累计充值未达到阈值
|
||||
- **THEN** 系统不触发一次性佣金计算
|
||||
|
||||
#### Scenario: 已发放过不重复触发
|
||||
- **WHEN** 卡的一次性佣金已发放过(first_commission_paid = true)
|
||||
- **THEN** 系统不触发一次性佣金计算
|
||||
|
||||
---
|
||||
|
||||
### Requirement: 充值订单状态流转
|
||||
|
||||
充值订单状态 SHALL 按以下流程流转:待支付 → 已支付 → 已完成。
|
||||
|
||||
#### Scenario: 正常流转
|
||||
- **WHEN** 创建充值订单 → 支付成功 → 钱包余额增加完成
|
||||
- **THEN** 订单状态依次为:1(待支付)→ 2(已支付)→ 3(已完成)
|
||||
|
||||
#### Scenario: 超时未支付
|
||||
- **WHEN** 充值订单创建 30 分钟后仍未支付
|
||||
- **THEN** 系统标记订单为已关闭(状态 4)
|
||||
|
||||
---
|
||||
|
||||
### Requirement: 充值金额限制
|
||||
|
||||
系统 SHALL 限制单次充值金额范围。
|
||||
|
||||
#### Scenario: 充值金额范围
|
||||
- **WHEN** 创建充值订单
|
||||
- **THEN** 充值金额必须在 1 元到 100000 元之间
|
||||
|
||||
#### Scenario: 充值金额过小
|
||||
- **WHEN** 客户尝试充值 0.5 元
|
||||
- **THEN** 系统返回错误 "充值金额不能小于1元"
|
||||
|
||||
#### Scenario: 充值金额过大
|
||||
- **WHEN** 客户尝试充值 200000 元
|
||||
- **THEN** 系统返回错误 "单次充值金额不能超过100000元"
|
||||
@@ -0,0 +1,227 @@
|
||||
## 1. 数据库迁移
|
||||
|
||||
- [x] 1.1 创建迁移文件:tb_order 表新增 is_purchase_on_behalf 字段
|
||||
- [x] 1.2 创建迁移文件:tb_shop_series_allocation 表新增强充配置字段(enable_force_recharge, force_recharge_amount)
|
||||
- [x] 1.3 在测试环境执行迁移并验证字段添加成功
|
||||
- [x] 1.4 验证迁移:检查字段默认值和数据类型是否正确
|
||||
|
||||
## 2. 常量定义
|
||||
|
||||
- [x] 2.1 在 pkg/constants/ 定义充值订单状态常量(待支付、已支付、已完成、已关闭)
|
||||
- [x] 2.2 在 pkg/constants/ 定义充值订单号前缀常量(RCH)
|
||||
- [x] 2.3 在 pkg/constants/ 定义线下支付方式常量(offline)
|
||||
- [x] 2.4 在 pkg/constants/ 定义强充相关 Redis Key 生成函数(可选,如缓存系列配置)
|
||||
- [x] 2.5 在 pkg/constants/ 定义充值金额限制常量(最小1元,最大100000元)
|
||||
|
||||
## 3. 错误码定义
|
||||
|
||||
- [x] 3.1 在 pkg/errors/ 定义充值相关错误码(充值金额不符、充值订单不存在等)
|
||||
- [x] 3.2 在 pkg/errors/ 定义代购相关错误码(只有平台可使用线下支付、只能为下级代理代购等)
|
||||
- [x] 3.3 在 pkg/errors/ 定义强充验证错误码(必须充值X元等)
|
||||
|
||||
## 4. Model 层修改
|
||||
|
||||
- [x] 4.1 修改 internal/model/order.go:新增 IsPurchaseOnBehalf 字段(bool, default false)
|
||||
- [x] 4.2 修改 internal/model/shop_series_allocation.go:新增 EnableForceRecharge 字段(bool, default false)
|
||||
- [x] 4.3 修改 internal/model/shop_series_allocation.go:新增 ForceRechargeAmount 字段(int64, default 0)
|
||||
- [x] 4.4 验证 Model 修改:运行 lsp_diagnostics 检查类型错误
|
||||
|
||||
## 5. RechargeStore 数据访问层
|
||||
|
||||
- [x] 5.1 创建 internal/store/postgres/recharge_store.go
|
||||
- [x] 5.2 实现 Create 方法:创建充值订单
|
||||
- [x] 5.3 实现 GetByRechargeNo 方法:根据充值单号查询
|
||||
- [x] 5.4 实现 GetByID 方法:根据ID查询充值订单详情
|
||||
- [x] 5.5 实现 List 方法:分页查询充值订单列表,支持状态筛选、时间范围筛选
|
||||
- [x] 5.6 实现 UpdateStatus 方法:更新充值订单状态(支付成功回调时使用)
|
||||
- [x] 5.7 实现 UpdatePaymentInfo 方法:更新支付方式和支付时间
|
||||
- [x] 5.8 编写单元测试:测试覆盖率 ≥ 90%
|
||||
- [x] 5.9 验证测试:运行测试并确保全部通过
|
||||
|
||||
## 6. RechargeService 业务逻辑层
|
||||
|
||||
- [x] 6.1 创建 internal/service/recharge/service.go
|
||||
- [x] 6.2 实现 Create 方法:创建充值订单
|
||||
- 验证资源存在(卡/设备)
|
||||
- 验证充值金额范围(1元~100000元)
|
||||
- 检查强充要求并验证金额
|
||||
- 生成充值单号(RCH + 时间戳 + 随机数)
|
||||
- 创建充值订单记录
|
||||
- [x] 6.3 实现 GetRechargeCheck 方法:充值预检
|
||||
- 查询资源(卡/设备)
|
||||
- 查询系列分配配置
|
||||
- 判断是否需要强充
|
||||
- 返回强充要求、允许金额范围、提示信息
|
||||
- [x] 6.4 实现 GetByID 方法:查询充值订单详情(数据权限过滤)
|
||||
- [x] 6.5 实现 List 方法:查询充值订单列表(分页、筛选、数据权限过滤)
|
||||
- [x] 6.6 实现 HandlePaymentCallback 方法:处理支付回调
|
||||
- 幂等性检查(检查订单状态)
|
||||
- 使用数据库事务:更新订单状态 → 增加钱包余额 → 更新累计充值 → 触发佣金判断
|
||||
- 钱包余额更新使用原子操作
|
||||
- [x] 6.7 实现 updateAccumulatedRecharge 私有方法:更新卡/设备的累计充值金额
|
||||
- [x] 6.8 实现 triggerOneTimeCommissionIfNeeded 私有方法:检查并触发一次性佣金
|
||||
- [x] 6.9 实现 checkForceRechargeRequirement 私有方法:检查强充要求(供创建订单时使用)
|
||||
- [x] 6.10 编写单元测试:测试覆盖率 ≥ 90%(包含各种充值场景、强充验证、支付回调幂等性)
|
||||
- [x] 6.11 验证测试:运行测试并确保全部通过
|
||||
|
||||
## 7. OrderService 修改(代购订单和强充验证)
|
||||
|
||||
- [x] 7.1 修改 internal/service/order/service.go:Create 方法增加强充验证
|
||||
- 调用预检逻辑获取强充要求
|
||||
- 验证支付金额是否符合强充要求
|
||||
- 不符合则返回错误
|
||||
- [x] 7.2 新增 CreatePurchaseOnBehalf 方法:创建代购订单
|
||||
- 验证权限(只有平台可以使用线下支付)
|
||||
- 查询资源归属代理(buyer_id)
|
||||
- 查询买家的成本价
|
||||
- 创建订单(is_purchase_on_behalf = true, payment_method = offline, payment_status = 2)
|
||||
- 自动激活套餐(创建 PackageUsage)
|
||||
- 触发佣金计算任务
|
||||
- [x] 7.3 新增 GetPurchaseCheck 方法:套餐购买预检
|
||||
- 查询套餐总价
|
||||
- 检查强充要求
|
||||
- 计算实际支付金额和钱包到账金额
|
||||
- 返回预检信息(含提示消息)
|
||||
- [x] 7.4 修改支付成功后的处理逻辑:增加 is_purchase_on_behalf 判断
|
||||
- 如果是代购订单,跳过钱包扣款
|
||||
- 如果是普通订单,正常扣款
|
||||
- [x] 7.5 编写单元测试:测试代购订单创建、强充验证、预检逻辑
|
||||
- [x] 7.6 验证测试:运行测试并确保全部通过
|
||||
|
||||
## 8. CommissionCalculationService 修改
|
||||
|
||||
- [x] 8.1 修改 internal/service/commission_calculation/service.go:CalculateCommission 方法增加代购订单判断
|
||||
- 差价佣金:所有订单都计算(包括代购)
|
||||
- 累计充值更新:仅非代购订单更新(is_purchase_on_behalf = false)
|
||||
- 一次性佣金:仅非代购订单触发(is_purchase_on_behalf = false)
|
||||
- [x] 8.2 修改 updateAccumulatedRecharge 方法:增加代购订单检查
|
||||
- 如果 is_purchase_on_behalf = true,直接返回,不更新累计充值
|
||||
- 如果 is_purchase_on_behalf = false,正常更新累计充值
|
||||
- [x] 8.3 修改 triggerOneTimeCommission 方法:增加代购订单检查
|
||||
- 如果 is_purchase_on_behalf = true,直接返回,不触发一次性佣金
|
||||
- 如果 is_purchase_on_behalf = false,正常检查阈值并触发佣金
|
||||
- [x] 8.4 编写单元测试:使用 table-driven tests 测试各种场景(普通订单、代购订单、充值订单)
|
||||
- [x] 8.5 验证测试:运行测试并确保全部通过
|
||||
|
||||
## 9. RechargeHandler HTTP 接口层
|
||||
|
||||
- [x] 9.1 创建 internal/handler/h5/recharge.go
|
||||
- [x] 9.2 实现 POST /api/h5/wallets/recharge:创建充值订单
|
||||
- 参数验证(resource_type, resource_id, amount)
|
||||
- 调用 Service 创建充值订单
|
||||
- 返回充值订单信息
|
||||
- [x] 9.3 实现 GET /api/h5/wallets/recharge-check:充值预检
|
||||
- 参数验证(resource_type, resource_id)
|
||||
- 调用 Service 获取强充要求
|
||||
- 返回预检信息
|
||||
- [x] 9.4 实现 GET /api/h5/wallets/recharges:查询充值订单列表
|
||||
- 支持分页参数(page, page_size)
|
||||
- 支持状态筛选(status)
|
||||
- 支持时间范围筛选(start_time, end_time)
|
||||
- [x] 9.5 实现 GET /api/h5/wallets/recharges/:id:查询充值订单详情
|
||||
- [x] 9.6 为所有接口添加中文注释(路由路径、参数说明、响应说明)
|
||||
- [x] 9.7 验证接口:运行 lsp_diagnostics 检查类型错误
|
||||
|
||||
## 10. OrderHandler 修改(代购订单接口)
|
||||
|
||||
- [x] 10.1 修改 internal/handler/admin/order.go:Create 方法增加代购订单支持
|
||||
- 检查 payment_method = offline 时,验证用户类型为 Platform
|
||||
- 如果是平台账号且线下支付,调用 CreatePurchaseOnBehalf
|
||||
- 否则调用正常的 Create 方法
|
||||
- [x] 10.2 新增 POST /api/admin/orders/purchase-check:套餐购买预检
|
||||
- 参数验证(order_type, resource_id, package_ids)
|
||||
- 调用 Service 获取预检信息
|
||||
- 返回预检结果
|
||||
- [x] 10.3 为新增接口添加中文注释
|
||||
- [x] 10.4 验证接口:运行 lsp_diagnostics 检查类型错误
|
||||
|
||||
## 11. PaymentCallback 修改(充值订单回调)
|
||||
|
||||
- [x] 11.1 修改 internal/handler/callback/payment.go:WechatPayCallback 方法增加充值订单判断
|
||||
- 根据订单号前缀判断类型(RCH 开头 → 充值订单)
|
||||
- 如果是充值订单,调用 RechargeService.HandlePaymentCallback
|
||||
- 如果是套餐订单,调用 OrderService.HandlePaymentCallback
|
||||
- [x] 11.2 修改 AlipayCallback 方法:增加充值订单判断(同上)
|
||||
- [x] 11.3 验证修改:运行 lsp_diagnostics 检查类型错误
|
||||
|
||||
## 12. Bootstrap 依赖注入
|
||||
|
||||
- [x] 12.1 修改 internal/bootstrap/stores.go:注册 RechargeStore
|
||||
- [x] 12.2 修改 internal/bootstrap/services.go:注册 RechargeService
|
||||
- [x] 12.3 修改 internal/bootstrap/handlers.go:注册 RechargeHandler(H5)
|
||||
- [x] 12.4 验证依赖注入:确保所有依赖正确传递
|
||||
|
||||
## 13. 路由注册
|
||||
|
||||
- [x] 13.1 修改 internal/router/h5.go:注册充值相关路由
|
||||
- POST /api/h5/wallets/recharge
|
||||
- GET /api/h5/wallets/recharge-check
|
||||
- GET /api/h5/wallets/recharges
|
||||
- GET /api/h5/wallets/recharges/:id
|
||||
- [x] 13.2 修改 internal/router/admin.go:注册代购预检路由
|
||||
- POST /api/admin/orders/purchase-check
|
||||
- [x] 13.3 验证路由:确保所有路由正确绑定到 Handler 方法
|
||||
|
||||
## 14. API 文档生成器更新
|
||||
|
||||
- [x] 14.1 修改 cmd/api/docs.go:在 Handlers 初始化中添加 RechargeHandler
|
||||
- [x] 14.2 修改 cmd/gendocs/main.go:在 Handlers 初始化中添加 RechargeHandler
|
||||
- [x] 14.3 运行文档生成命令:go run cmd/gendocs/main.go
|
||||
- [x] 14.4 验证生成的 OpenAPI 文档:检查充值和代购相关接口是否出现
|
||||
|
||||
## 15. 集成测试
|
||||
|
||||
- [x] 15.1 编写充值完整流程集成测试
|
||||
- 创建充值订单(无强充)
|
||||
- 创建充值订单(首次强充验证)
|
||||
- 创建充值订单(累计强充验证)
|
||||
- 模拟支付回调
|
||||
- 验证钱包余额增加
|
||||
- 验证累计充值更新
|
||||
- 验证一次性佣金触发
|
||||
- [x] 15.2 编写代购订单完整流程集成测试
|
||||
- 平台创建代购订单
|
||||
- 验证订单自动完成
|
||||
- 验证套餐激活
|
||||
- 验证差价佣金计算
|
||||
- 验证一次性佣金不触发
|
||||
- 验证累计充值不更新
|
||||
- [x] 15.3 编写强充预检集成测试
|
||||
- 充值预检(各种强充场景)
|
||||
- 套餐购买预检(各种强充场景)
|
||||
- [x] 15.4 验证测试:运行所有集成测试并确保通过
|
||||
|
||||
## 16. 功能手动验证(开发环境)
|
||||
|
||||
- [x] 16.1 验证充值预检接口:调用接口确认返回正确的强充要求
|
||||
- [x] 16.2 验证购买预检接口:调用接口确认实际支付金额计算正确
|
||||
- [x] 16.3 验证充值订单创建:创建订单并确认数据库记录正确
|
||||
- [x] 16.4 验证代购订单创建:创建代购订单并确认套餐自动激活
|
||||
|
||||
## 17. 数据库验证(使用 PostgreSQL MCP)
|
||||
|
||||
- [x] 17.1 验证 tb_order 表字段:检查 is_purchase_on_behalf 字段及默认值
|
||||
- [x] 17.2 验证 tb_shop_series_allocation 表字段:检查 enable_force_recharge 和 force_recharge_amount 字段
|
||||
- [x] 17.3 验证充值订单创建:执行创建后查询数据库确认记录正确
|
||||
- [x] 17.4 验证代购订单创建:执行创建后查询订单表和套餐使用表
|
||||
- [x] 17.5 验证累计充值更新:执行充值/购买后查询卡/设备的 accumulated_recharge 字段
|
||||
- [x] 17.6 验证佣金计算:执行订单后查询佣金记录表,确认代购订单不触发一次性佣金
|
||||
|
||||
## 18. 文档更新
|
||||
|
||||
- [x] 18.1 在 docs/ 目录创建功能总结文档(中文,简要说明业务规则和 API 接口)
|
||||
- [x] 18.2 更新 README.md:添加强充系统和代购订单功能说明(可选)
|
||||
|
||||
## 19. 代码规范检查
|
||||
|
||||
- [x] 19.1 运行 lsp_diagnostics 检查所有修改的文件
|
||||
- [x] 19.2 运行代码规范检查脚本(如有)
|
||||
- [x] 19.3 确保所有注释使用中文
|
||||
- [x] 19.4 确保所有常量定义在 pkg/constants/
|
||||
- [x] 19.5 确保所有错误码定义在 pkg/errors/
|
||||
|
||||
## 20. 开发完成验证
|
||||
|
||||
- [x] 20.1 执行数据库迁移(开发环境)
|
||||
- [x] 20.2 运行完整测试套件并确保全部通过
|
||||
- [x] 20.3 本地启动服务验证功能可用性
|
||||
Reference in New Issue
Block a user