feat: 实现代理钱包订单创建和订单角色追踪功能
All checks were successful
构建并部署到测试环境(无 SSH) / build-and-deploy (push) Successful in 7m0s
All checks were successful
构建并部署到测试环境(无 SSH) / build-and-deploy (push) Successful in 7m0s
新增功能: - 代理在后台使用 wallet 支付时,订单直接完成(扣款 + 激活套餐) - 支持代理自购和代理代购场景 - 新增订单角色追踪字段(operator_id、operator_type、actual_paid_amount、purchase_role) - 订单查询支持 OR 逻辑(buyer_id 或 operator_id) - 钱包流水记录交易子类型和关联店铺 - 佣金逻辑调整:代理代购不产生佣金 数据库变更: - 订单表新增 4 个字段和 2 个索引 - 钱包流水表新增 2 个字段 - 包含迁移脚本和回滚脚本 文档: - 功能总结文档 - 部署指南 - OpenAPI 文档更新 - Specs 同步(新增 agent-order-role-tracking capability) Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,2 @@
|
||||
schema: spec-driven
|
||||
created: 2026-02-28
|
||||
@@ -0,0 +1,617 @@
|
||||
## Context
|
||||
|
||||
当前订单创建逻辑存在两条路径:
|
||||
1. **平台代购(offline)**:平台为代理创建订单,使用 `offline` 支付方式,订单创建后立即标记已支付并激活套餐,不扣钱包
|
||||
2. **其他支付方式(wallet/wechat/alipay)**:创建待支付订单(`payment_status = pending`),后续调用支付接口完成支付
|
||||
|
||||
问题在于:代理在后台使用 `wallet` 创建订单时,走的是第 2 条路径(创建待支付订单),但后台没有支付接口,导致订单无法完成。
|
||||
|
||||
**实际业务场景**:代理帮客户购买套餐(代购),需要从自己钱包扣款并立即激活套餐,这是一步完成的操作,不应该创建待支付订单。
|
||||
|
||||
**现有代码分析**:
|
||||
- `Service.Create()` 方法中,只有 `payment_method == "offline"` 才会调用 `createOrderWithActivation()`(立即完成)
|
||||
- `wallet` 支付会直接调用 `orderStore.Create()`,创建待支付订单,不扣款不激活
|
||||
- 缺少"操作者"和"买家"的区分,无法追踪代购关系
|
||||
|
||||
## Goals / Non-Goals
|
||||
|
||||
**Goals:**
|
||||
1. 支持代理在后台使用 wallet 一步完成订单(检查余额 → 扣款 → 激活套餐)
|
||||
2. 区分订单中的"操作者"(谁下单)和"买家"(资源所属者),支持数据追溯和业务分析
|
||||
3. 正确处理三种代购场景的价格逻辑:
|
||||
- 代理自购:订单金额 = 实际支付 = 自己成本价
|
||||
- 代理代购(给下级):订单金额 = 下级成本价,实际支付 = 自己成本价
|
||||
- 平台代购:订单金额 = 下级成本价,实际支付 = 0(不扣款)
|
||||
4. 支持按订单角色筛选查询(自购、上级购买、平台购买、给下级购买)
|
||||
5. 钱包流水记录支持场景区分和关联店铺追踪
|
||||
6. 佣金逻辑调整:代理代购不产生佣金(已赚差价)
|
||||
|
||||
**Non-Goals:**
|
||||
- 不修改 H5 端的支付流程(H5 端仍然是两步:创建待支付订单 → 调用 WalletPay)
|
||||
- 不修改平台代购(offline)的现有逻辑
|
||||
- 不支持代理在后台使用微信/支付宝支付(后台只支持 wallet 和 offline)
|
||||
- 不涉及个人客户(C 端)的订单流程
|
||||
|
||||
## Decisions
|
||||
|
||||
### 决策 1:新增订单角色追踪字段
|
||||
|
||||
**决策**:在 `tb_order` 表新增 4 个字段:
|
||||
- `operator_id` (INT, nullable):操作者 ID(店铺 ID)
|
||||
- `operator_type` (VARCHAR, nullable):操作者类型(`platform` / `agent`)
|
||||
- `actual_paid_amount` (BIGINT, nullable):实际支付金额(分)
|
||||
- `purchase_role` (VARCHAR):订单角色枚举
|
||||
|
||||
**理由**:
|
||||
- 现有 `buyer_id` 字段只记录买家(资源所属者),无法区分"谁下单"和"谁买单"
|
||||
- `operator_id` 记录操作者,支持追溯代购关系(如:平台为代理 A 代购,代理 A 为代理 B 代购)
|
||||
- `actual_paid_amount` 记录实际扣款金额,与 `total_amount`(订单金额)可能不同(代理代购场景)
|
||||
- `purchase_role` 枚举字段支持高效筛选,避免依赖文本备注
|
||||
|
||||
**替代方案(已拒绝)**:
|
||||
- ❌ 使用 `creator` 字段代替 `operator_id`:`creator` 是审计字段,语义不同,不应混用
|
||||
- ❌ 使用 `remark` 字段记录代购信息:文本字段无法高效索引和筛选
|
||||
- ❌ 创建单独的代购订单表:增加系统复杂度,查询需要 JOIN 多表
|
||||
|
||||
**`purchase_role` 枚举值**:
|
||||
```go
|
||||
const (
|
||||
PurchaseRoleSelfPurchase = "self_purchase" // 自己购买
|
||||
PurchaseRolePurchasedByParent = "purchased_by_parent" // 上级代理购买
|
||||
PurchaseRolePurchasedByPlatform = "purchased_by_platform" // 平台代购
|
||||
PurchaseRolePurchaseForSubordinate = "purchase_for_subordinate" // 给下级购买
|
||||
)
|
||||
```
|
||||
|
||||
**索引设计**:
|
||||
- `idx_orders_operator_id` (operator_id):支持"我作为操作者的订单"查询
|
||||
- `idx_orders_purchase_role` (purchase_role):支持按角色筛选
|
||||
|
||||
---
|
||||
|
||||
### 决策 2:订单创建流程重构
|
||||
|
||||
**决策**:在 `Service.Create()` 方法中,根据 `payment_method` 和资源归属判断场景:
|
||||
|
||||
```
|
||||
IF payment_method == "offline":
|
||||
→ 平台代购场景(保持现有逻辑)
|
||||
→ buyer = 资源所属者, operator = nil, operator_type = "platform"
|
||||
→ 价格 = 买家成本价, 实际支付 = nil(不扣款)
|
||||
→ purchase_role = "purchased_by_platform"
|
||||
→ 调用 createOrderWithActivation()
|
||||
|
||||
ELSE IF payment_method == "wallet":
|
||||
→ 获取操作者店铺 ID
|
||||
→ 获取资源所属店铺 ID
|
||||
|
||||
IF 资源所属 == 操作者:
|
||||
→ 代理自购场景
|
||||
→ buyer = 操作者, operator = 操作者
|
||||
→ 价格 = 操作者成本价, 实际支付 = 操作者成本价
|
||||
→ purchase_role = "self_purchase"
|
||||
→ is_purchase_on_behalf = false
|
||||
ELSE:
|
||||
→ 代理代购场景
|
||||
→ buyer = 资源所属者, operator = 操作者
|
||||
→ 价格 = 买家成本价, 实际支付 = 操作者成本价
|
||||
→ purchase_role = "purchase_for_subordinate"
|
||||
→ is_purchase_on_behalf = true
|
||||
|
||||
→ 调用 createOrderWithWalletPayment()
|
||||
```
|
||||
|
||||
**理由**:
|
||||
- 清晰区分三种场景,每种场景的价格逻辑和字段填充规则不同
|
||||
- 复用现有的 `createOrderWithActivation()`(平台代购)
|
||||
- 新增 `createOrderWithWalletPayment()`(代理钱包支付,含扣款逻辑)
|
||||
|
||||
**替代方案(已拒绝)**:
|
||||
- ❌ 使用策略模式分离三种场景:过度设计,增加代码复杂度
|
||||
- ❌ 在 Handler 层判断场景:违反分层架构,业务逻辑应在 Service 层
|
||||
|
||||
---
|
||||
|
||||
### 决策 3:价格计算逻辑
|
||||
|
||||
**决策**:区分"订单金额"(`total_amount`)和"实际支付"(`actual_paid_amount`):
|
||||
|
||||
| 场景 | 订单金额(total_amount) | 实际支付(actual_paid_amount) | 说明 |
|
||||
|------|------------------------|------------------------------|------|
|
||||
| 代理自购 | 操作者成本价 | 操作者成本价 | 两者相同 |
|
||||
| 代理代购 | 买家成本价 | 操作者成本价 | 操作者实际扣款少于订单金额(赚取差价) |
|
||||
| 平台代购 | 买家成本价 | NULL | 平台不扣款 |
|
||||
|
||||
**理由**:
|
||||
- **订单金额**面向买家:买家看到的应该是"他的成本价",这样才符合业务逻辑
|
||||
- **实际支付**面向操作者:操作者实际扣款金额,用于钱包流水和财务对账
|
||||
- 代理代购时,操作者按自己的成本价扣款,但订单显示下级成本价,差价即为利润
|
||||
|
||||
**示例**:
|
||||
```
|
||||
一级代理 A 成本价:80 元
|
||||
二级代理 B 成本价:100 元
|
||||
|
||||
A 为 B 的卡购买套餐:
|
||||
- total_amount = 100(B 看到的订单金额)
|
||||
- actual_paid_amount = 80(A 实际扣款)
|
||||
- A 赚取差价:20 元
|
||||
```
|
||||
|
||||
**成本价查询**:
|
||||
```go
|
||||
// 通过 ShopPackageAllocation 表查询店铺对套餐的成本价
|
||||
allocation, err := s.shopPackageAllocationStore.GetByShopAndPackage(ctx, shopID, packageID)
|
||||
costPrice := allocation.CostPrice
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### 决策 4:事务处理设计
|
||||
|
||||
**决策**:新增 `createOrderWithWalletPayment()` 方法,使用 GORM 事务确保原子性:
|
||||
|
||||
```go
|
||||
func (s *Service) createOrderWithWalletPayment(
|
||||
ctx context.Context,
|
||||
order *model.Order,
|
||||
items []*model.OrderItem,
|
||||
operatorShopID uint,
|
||||
buyerShopID uint,
|
||||
) (*dto.OrderResponse, error) {
|
||||
actualAmount := *order.ActualPaidAmount
|
||||
|
||||
// 1. 事务外:检查钱包余额(快速失败)
|
||||
wallet, err := s.agentWalletStore.GetMainWallet(ctx, operatorShopID)
|
||||
if wallet.Balance < actualAmount {
|
||||
return nil, errors.New(errors.CodeInsufficientBalance, "余额不足")
|
||||
}
|
||||
|
||||
// 2. 事务内:创建订单 + 扣款 + 创建流水 + 激活套餐
|
||||
err = s.db.WithContext(ctx).Transaction(func(tx *gorm.DB) error {
|
||||
// 2.1 创建订单
|
||||
if err := tx.Create(order).Error; err != nil {
|
||||
return errors.Wrap(errors.CodeDatabaseError, err, "创建订单失败")
|
||||
}
|
||||
|
||||
// 2.2 创建订单明细
|
||||
for _, item := range items {
|
||||
item.OrderID = order.ID
|
||||
}
|
||||
if err := tx.CreateInBatches(items, 100).Error; err != nil {
|
||||
return errors.Wrap(errors.CodeDatabaseError, err, "创建订单明细失败")
|
||||
}
|
||||
|
||||
// 2.3 扣减钱包余额(乐观锁)
|
||||
walletResult := tx.Model(&model.AgentWallet{}).
|
||||
Where("id = ? AND balance >= ? AND version = ?", wallet.ID, actualAmount, wallet.Version).
|
||||
Updates(map[string]any{
|
||||
"balance": gorm.Expr("balance - ?", actualAmount),
|
||||
"version": gorm.Expr("version + 1"),
|
||||
})
|
||||
if walletResult.RowsAffected == 0 {
|
||||
return errors.New(errors.CodeInsufficientBalance, "余额不足或并发冲突")
|
||||
}
|
||||
|
||||
// 2.4 创建钱包流水
|
||||
if err := s.createWalletTransaction(ctx, tx, wallet.ID, order.ID, actualAmount, order.PurchaseRole, buyerShopID); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// 2.5 激活套餐
|
||||
return s.activatePackage(ctx, tx, order)
|
||||
})
|
||||
|
||||
// 3. 事务外:佣金计算(异步)
|
||||
if order.OperatorID == nil {
|
||||
s.enqueueCommissionCalculation(ctx, order.ID)
|
||||
}
|
||||
|
||||
return s.buildOrderResponse(order, items), nil
|
||||
}
|
||||
```
|
||||
|
||||
**事务步骤顺序理由**:
|
||||
1. **先创建订单**:确保订单记录存在,后续步骤失败时可以回滚
|
||||
2. **后扣款**:避免扣款成功但订单创建失败的情况(钱扣了但没有订单)
|
||||
3. **最后激活套餐**:套餐激活依赖订单 ID,必须在订单创建后执行
|
||||
|
||||
**乐观锁设计**:
|
||||
- 使用 `version` 字段防止并发扣款导致余额不一致
|
||||
- `WHERE balance >= ?` 确保余额充足
|
||||
- `RowsAffected == 0` 表示余额不足或版本冲突,回滚事务
|
||||
|
||||
**替代方案(已拒绝)**:
|
||||
- ❌ 使用悲观锁(SELECT FOR UPDATE):会降低并发性能,乐观锁已足够
|
||||
- ❌ 先扣款后创建订单:扣款成功但订单失败时难以回滚(需要补偿事务)
|
||||
|
||||
---
|
||||
|
||||
### 决策 5:订单查询 OR 逻辑
|
||||
|
||||
**决策**:修改 `OrderStore.List()` 方法,支持 `buyer_id = shopID OR operator_id = shopID` 查询:
|
||||
|
||||
```go
|
||||
func (s *OrderStore) List(ctx context.Context, opts *store.QueryOptions, filters map[string]any) ([]*model.Order, int64, error) {
|
||||
query := s.db.WithContext(ctx).Model(&model.Order{})
|
||||
|
||||
// 代理用户:查询作为买家或操作者的订单
|
||||
if shopID, ok := filters["shop_id"].(uint); ok {
|
||||
query = query.Where(
|
||||
"(buyer_type = ? AND buyer_id = ?) OR operator_id = ?",
|
||||
model.BuyerTypeAgent, shopID, shopID,
|
||||
)
|
||||
delete(filters, "shop_id")
|
||||
}
|
||||
|
||||
// 其他筛选条件...
|
||||
}
|
||||
```
|
||||
|
||||
**理由**:
|
||||
- 代理需要看到两类订单:
|
||||
1. 作为买家的订单(`buyer_id = 自己`):别人为自己代购、自己购买
|
||||
2. 作为操作者的订单(`operator_id = 自己`):自己为下级代购
|
||||
- OR 查询在 PostgreSQL 中性能可接受(已有索引)
|
||||
|
||||
**性能优化**:
|
||||
- `buyer_id` 已有索引 `idx_order_buyer`
|
||||
- `operator_id` 新建索引 `idx_orders_operator_id`
|
||||
- OR 查询会走两个索引的 UNION,性能可接受
|
||||
|
||||
**替代方案(已拒绝)**:
|
||||
- ❌ 两次查询后合并结果:代码复杂,分页逻辑难以实现
|
||||
- ❌ 冗余存储(同一订单插入两次):违反数据一致性原则
|
||||
|
||||
---
|
||||
|
||||
### 决策 6:钱包流水记录设计
|
||||
|
||||
**决策**:钱包流水表新增字段(如果不存在):
|
||||
- `transaction_subtype` (VARCHAR):交易子类型,细分 `order_payment` 场景
|
||||
- `related_shop_id` (INT, nullable):关联店铺 ID,代购时记录下级店铺
|
||||
|
||||
**子类型枚举**(在 `pkg/constants/wallet.go` 中定义):
|
||||
```go
|
||||
// 钱包交易子类型(当 transaction_type = "order_payment" 时)
|
||||
const (
|
||||
WalletTransactionSubtypeSelfPurchase = "self_purchase"
|
||||
WalletTransactionSubtypePurchaseForSubordinate = "purchase_for_subordinate"
|
||||
)
|
||||
```
|
||||
|
||||
**流水创建逻辑**:
|
||||
```go
|
||||
func (s *Service) createWalletTransaction(
|
||||
ctx context.Context,
|
||||
tx *gorm.DB,
|
||||
walletID uint,
|
||||
orderID uint,
|
||||
amount int64,
|
||||
purchaseRole string,
|
||||
relatedShopID *uint,
|
||||
) error {
|
||||
var subtype string
|
||||
var remark string
|
||||
|
||||
switch purchaseRole {
|
||||
case constants.PurchaseRoleSelfPurchase:
|
||||
subtype = constants.WalletTransactionSubtypeSelfPurchase
|
||||
remark = "购买套餐"
|
||||
|
||||
case constants.PurchaseRolePurchaseForSubordinate:
|
||||
subtype = constants.WalletTransactionSubtypePurchaseForSubordinate
|
||||
// 查询下级店铺名称
|
||||
buyerShop, _ := s.shopStore.GetByID(ctx, *relatedShopID)
|
||||
if buyerShop != nil {
|
||||
remark = fmt.Sprintf("为下级代理【%s】购买套餐", buyerShop.ShopName)
|
||||
} else {
|
||||
remark = "为下级代理购买套餐"
|
||||
}
|
||||
}
|
||||
|
||||
transaction := &model.AgentWalletTransaction{
|
||||
WalletID: walletID,
|
||||
TransactionType: constants.AgentTransactionTypeDeduct,
|
||||
TransactionSubtype: subtype,
|
||||
Amount: -amount,
|
||||
OrderID: &orderID,
|
||||
RelatedShopID: relatedShopID,
|
||||
Remark: remark,
|
||||
}
|
||||
|
||||
return tx.Create(transaction).Error
|
||||
}
|
||||
```
|
||||
|
||||
**理由**:
|
||||
- `transaction_subtype` 支持高效筛选(如:查询所有自购流水)
|
||||
- `related_shop_id` 支持追溯(如:查询为哪些下级代理购买过)
|
||||
- `remark` 作为辅助展示字段,包含店铺名称便于人工查看
|
||||
|
||||
---
|
||||
|
||||
### 决策 7:佣金逻辑调整
|
||||
|
||||
**决策**:在 `Service.Create()` 方法中,只有 `operator_id == nil`(平台代购)才入队佣金计算任务:
|
||||
|
||||
```go
|
||||
// 佣金计算(异步)
|
||||
if order.OperatorID == nil {
|
||||
// 只有平台代购才产生佣金
|
||||
s.enqueueCommissionCalculation(ctx, order.ID)
|
||||
}
|
||||
// 代理代购不入队佣金(operator_id != nil)
|
||||
```
|
||||
|
||||
**理由**:
|
||||
- **代理代购**:操作者从自己钱包扣自己的成本价,已经赚取了成本价差(自己成本价 vs 下级成本价),不应再产生佣金
|
||||
- **平台代购**:平台不扣款,按买家成本价计算差价佣金,激励上级代理
|
||||
|
||||
**示例**:
|
||||
```
|
||||
一级代理 A 成本价:80 元
|
||||
二级代理 B 成本价:100 元
|
||||
|
||||
场景 1:A 为 B 的卡购买套餐(代理代购)
|
||||
- A 扣款:80 元
|
||||
- A 利润:100 - 80 = 20 元
|
||||
- 佣金:无(A 已赚取差价)
|
||||
|
||||
场景 2:平台为 B 的卡购买套餐(平台代购)
|
||||
- 平台扣款:0 元
|
||||
- 订单金额:100 元
|
||||
- 佣金:给 A(B 的上级),金额 = 100 - 80 = 20 元
|
||||
```
|
||||
|
||||
**替代方案(已拒绝)**:
|
||||
- ❌ 代理代购也计算佣金:会导致双重利润(差价 + 佣金),不符合业务逻辑
|
||||
|
||||
---
|
||||
|
||||
### 决策 8:常量定义位置
|
||||
|
||||
**决策**:
|
||||
- **订单角色枚举**(`PurchaseRole*`):定义在 `internal/model/order.go`,紧邻 Order 模型
|
||||
- **钱包交易子类型**:扩展现有 `pkg/constants/wallet.go`,新增 `WalletTransactionSubtype*` 常量
|
||||
- **操作者类型**:复用现有 `pkg/constants/iot.go` 中的 `OwnerTypePlatform` 和自定义 `"agent"` 字符串
|
||||
|
||||
**理由**:
|
||||
- 订单角色枚举只在订单模块使用,放在 `model/order.go` 避免常量文件膨胀
|
||||
- 钱包交易子类型属于钱包系统,应在 `wallet.go` 中管理
|
||||
- 避免重复定义已有常量(如 `OwnerTypePlatform`)
|
||||
|
||||
---
|
||||
|
||||
### 决策 9:DTO 响应字段设计
|
||||
|
||||
**决策**:`OrderResponse` 新增字段:
|
||||
|
||||
```go
|
||||
type OrderResponse struct {
|
||||
// ... 现有字段
|
||||
|
||||
// 操作者信息
|
||||
OperatorID *uint `json:"operator_id,omitempty"`
|
||||
OperatorType string `json:"operator_type,omitempty"`
|
||||
OperatorName string `json:"operator_name,omitempty"`
|
||||
ActualPaidAmount *int64 `json:"actual_paid_amount,omitempty"`
|
||||
|
||||
// 订单角色
|
||||
PurchaseRole string `json:"purchase_role"`
|
||||
IsPurchasedByParent bool `json:"is_purchased_by_parent"`
|
||||
PurchaseRemark string `json:"purchase_remark,omitempty"`
|
||||
}
|
||||
```
|
||||
|
||||
**字段说明**:
|
||||
- `operator_id`、`operator_type`、`actual_paid_amount`:直接映射数据库字段
|
||||
- `operator_name`:查询操作者名称(Shop 表的 `shop_name`),便于前端展示
|
||||
- `purchase_role`:订单角色枚举,支持前端筛选和标签展示
|
||||
- `is_purchased_by_parent`:派生字段,`purchase_role == "purchased_by_parent"`
|
||||
- `purchase_remark`:派生字段,如"由上级代理【XX】购买"或"由平台代购"
|
||||
|
||||
**理由**:
|
||||
- 前端需要友好的文本展示(如操作者名称、购买备注)
|
||||
- `is_purchased_by_parent` 便于前端判断是否显示"上级代购"标签
|
||||
- `purchase_remark` 避免前端拼接文本逻辑
|
||||
|
||||
---
|
||||
|
||||
## Risks / Trade-offs
|
||||
|
||||
### [风险] 数据库迁移失败 → 回滚策略
|
||||
|
||||
**问题**:新增字段的迁移脚本可能在生产环境执行失败(如表锁、超时)
|
||||
|
||||
**缓解措施**:
|
||||
1. 所有新增字段设为 `nullable`,不影响现有数据
|
||||
2. 迁移脚本分步执行:
|
||||
- Step 1: 添加字段(不加 NOT NULL 约束)
|
||||
- Step 2: 数据回填(如有需要)
|
||||
- Step 3: 添加索引(CONCURRENTLY 方式,不锁表)
|
||||
3. 回滚脚本:`DROP COLUMN IF EXISTS`
|
||||
4. 测试环境充分验证后再上生产
|
||||
|
||||
---
|
||||
|
||||
### [风险] OR 查询性能下降 → 索引优化
|
||||
|
||||
**问题**:`WHERE (buyer_id = X) OR (operator_id = X)` 可能无法有效使用索引
|
||||
|
||||
**缓解措施**:
|
||||
1. 使用 `EXPLAIN ANALYZE` 验证查询计划
|
||||
2. 确保两个字段都有索引
|
||||
3. 如果性能不佳,考虑使用 UNION:
|
||||
```sql
|
||||
SELECT * FROM tb_order WHERE buyer_id = X
|
||||
UNION
|
||||
SELECT * FROM tb_order WHERE operator_id = X
|
||||
```
|
||||
4. 监控慢查询日志,按需优化
|
||||
|
||||
**预期性能**:
|
||||
- 单表查询,无 JOIN
|
||||
- 订单表数据量级:百万级
|
||||
- OR 查询在 PostgreSQL 中会走 BITMAP INDEX SCAN,性能可接受
|
||||
|
||||
---
|
||||
|
||||
### [权衡] 两个金额字段增加存储成本 → 业务清晰度
|
||||
|
||||
**问题**:`total_amount` 和 `actual_paid_amount` 在大部分场景下相同(代理自购),存在冗余
|
||||
|
||||
**权衡理由**:
|
||||
- **优势**:业务语义清晰,查询无需计算(如:统计实际收入用 `actual_paid_amount`)
|
||||
- **劣势**:存储成本增加(每订单 8 字节),数据冗余
|
||||
- **结论**:业务清晰度优先,存储成本可接受(8 字节在订单数据中占比很小)
|
||||
|
||||
---
|
||||
|
||||
### [权衡] 钱包流水查询店铺名称 → 性能 vs 便利性
|
||||
|
||||
**问题**:创建钱包流水时查询店铺名称(`shopStore.GetByID`)会增加一次数据库查询
|
||||
|
||||
**权衡理由**:
|
||||
- **优势**:备注字段包含店铺名称,便于人工查看,无需二次查询
|
||||
- **劣势**:订单创建时增加一次查询(~5ms)
|
||||
- **结论**:可接受,因为:
|
||||
1. 查询频率低(仅代购场景)
|
||||
2. Shop 表有缓存机制
|
||||
3. 事务内查询,不影响一致性
|
||||
4. 如果性能敏感,可以异步更新备注
|
||||
|
||||
---
|
||||
|
||||
### [风险] 佣金逻辑调整导致收入计算错误 → 回归测试
|
||||
|
||||
**问题**:佣金计算逻辑变更可能影响现有代理的收入
|
||||
|
||||
**缓解措施**:
|
||||
1. 充分的单元测试和集成测试
|
||||
2. 上线前在测试环境验证佣金计算结果
|
||||
3. 灰度发布,监控佣金数据异常
|
||||
4. 保留 `operator_id == nil` 的判断逻辑,避免误伤平台代购
|
||||
|
||||
---
|
||||
|
||||
## Migration Plan
|
||||
|
||||
### 数据库迁移步骤
|
||||
|
||||
**Step 1: 创建迁移脚本**
|
||||
|
||||
文件:`migrations/xxx_add_operator_fields_to_orders.up.sql`
|
||||
|
||||
```sql
|
||||
-- 添加字段(nullable)
|
||||
ALTER TABLE tb_order ADD COLUMN operator_id INT;
|
||||
ALTER TABLE tb_order ADD COLUMN operator_type VARCHAR(20);
|
||||
ALTER TABLE tb_order ADD COLUMN actual_paid_amount BIGINT;
|
||||
ALTER TABLE tb_order ADD COLUMN purchase_role VARCHAR(50);
|
||||
|
||||
-- 添加注释
|
||||
COMMENT ON COLUMN tb_order.operator_id IS '操作者ID(谁下的单)';
|
||||
COMMENT ON COLUMN tb_order.operator_type IS '操作者类型(platform/agent)';
|
||||
COMMENT ON COLUMN tb_order.actual_paid_amount IS '实际支付金额(分)';
|
||||
COMMENT ON COLUMN tb_order.purchase_role IS '订单角色(self_purchase/purchased_by_parent/purchased_by_platform/purchase_for_subordinate)';
|
||||
|
||||
-- 添加索引(CONCURRENTLY 避免锁表)
|
||||
CREATE INDEX CONCURRENTLY idx_orders_operator_id ON tb_order(operator_id);
|
||||
CREATE INDEX CONCURRENTLY idx_orders_purchase_role ON tb_order(purchase_role);
|
||||
```
|
||||
|
||||
**Step 2: 钱包流水表迁移**(如果字段不存在)
|
||||
|
||||
文件:`migrations/xxx_add_transaction_subtype_to_wallet_transaction.up.sql`
|
||||
|
||||
```sql
|
||||
-- 检查字段是否存在,不存在则添加
|
||||
DO $$
|
||||
BEGIN
|
||||
IF NOT EXISTS (SELECT 1 FROM information_schema.columns
|
||||
WHERE table_name='tb_agent_wallet_transaction'
|
||||
AND column_name='transaction_subtype') THEN
|
||||
ALTER TABLE tb_agent_wallet_transaction ADD COLUMN transaction_subtype VARCHAR(50);
|
||||
COMMENT ON COLUMN tb_agent_wallet_transaction.transaction_subtype IS '交易子类型(细分 order_payment 场景)';
|
||||
END IF;
|
||||
|
||||
IF NOT EXISTS (SELECT 1 FROM information_schema.columns
|
||||
WHERE table_name='tb_agent_wallet_transaction'
|
||||
AND column_name='related_shop_id') THEN
|
||||
ALTER TABLE tb_agent_wallet_transaction ADD COLUMN related_shop_id INT;
|
||||
COMMENT ON COLUMN tb_agent_wallet_transaction.related_shop_id IS '关联店铺ID(代购时记录下级店铺)';
|
||||
END IF;
|
||||
END
|
||||
$$;
|
||||
```
|
||||
|
||||
**Step 3: 数据回填**(可选,视历史数据需求而定)
|
||||
|
||||
```sql
|
||||
-- 回填现有订单的 purchase_role
|
||||
-- 平台代购订单(offline)
|
||||
UPDATE tb_order
|
||||
SET purchase_role = 'purchased_by_platform',
|
||||
operator_type = 'platform'
|
||||
WHERE payment_method = 'offline'
|
||||
AND is_purchase_on_behalf = true
|
||||
AND purchase_role IS NULL;
|
||||
|
||||
-- 其他订单暂不回填(保持 NULL,不影响业务)
|
||||
```
|
||||
|
||||
### 部署步骤
|
||||
|
||||
1. **测试环境验证**:
|
||||
- 执行迁移脚本
|
||||
- 验证索引创建成功
|
||||
- 运行集成测试
|
||||
- 手工测试三种代购场景
|
||||
|
||||
2. **灰度发布**:
|
||||
- 代码部署到灰度环境
|
||||
- 观察日志和监控指标
|
||||
- 验证订单创建、查询、钱包扣款功能
|
||||
|
||||
3. **生产环境部署**:
|
||||
- 低峰期执行数据库迁移
|
||||
- 部署代码
|
||||
- 监控错误日志和业务指标
|
||||
- 验证核心功能
|
||||
|
||||
### 回滚策略
|
||||
|
||||
**代码回滚**:
|
||||
- 回滚到上一版本代码即可,新增字段为 `nullable`,不影响老代码
|
||||
|
||||
**数据库回滚**:
|
||||
- 文件:`migrations/xxx_add_operator_fields_to_orders.down.sql`
|
||||
```sql
|
||||
DROP INDEX IF EXISTS idx_orders_operator_id;
|
||||
DROP INDEX IF EXISTS idx_orders_purchase_role;
|
||||
ALTER TABLE tb_order DROP COLUMN IF EXISTS operator_id;
|
||||
ALTER TABLE tb_order DROP COLUMN IF EXISTS operator_type;
|
||||
ALTER TABLE tb_order DROP COLUMN IF EXISTS actual_paid_amount;
|
||||
ALTER TABLE tb_order DROP COLUMN IF EXISTS purchase_role;
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Open Questions
|
||||
|
||||
1. **是否需要支持代理在后台为个人客户(C 端)代购?**
|
||||
- 当前设计只支持代理为代理(B2B),不支持代理为个人客户(B2C)
|
||||
- 如果未来需要,需要扩展 `buyer_type` 判断逻辑
|
||||
|
||||
2. **钱包流水的 `related_shop_id` 是否需要索引?**
|
||||
- 当前设计未加索引,因为查询频率低
|
||||
- 如果未来需要"查询我为哪些下级购买过"功能,需要添加索引
|
||||
|
||||
3. **是否需要支持订单角色的批量变更?**
|
||||
- 当前设计 `purchase_role` 在订单创建时填充,后续不可修改
|
||||
- 如果历史订单需要回填角色,需要单独的数据修复脚本
|
||||
|
||||
4. **代理代购时,下级代理是否能看到操作者信息?**
|
||||
- 当前设计:下级可以看到 `operator_id` 和 `operator_name`
|
||||
- 是否需要隐藏?(业务决策)
|
||||
@@ -0,0 +1,61 @@
|
||||
## Why
|
||||
|
||||
代理在后台使用钱包支付(wallet)创建订单时,系统只创建待支付订单(payment_status = pending),不扣款也不激活套餐,导致订单无法完成。后台没有支付接口,代理无法对待支付订单进行支付。这个问题阻塞了代理的核心业务场景:代理帮客户购买套餐(代购),从自己钱包扣款并立即激活。
|
||||
|
||||
## What Changes
|
||||
|
||||
- **新增订单角色追踪字段**:在订单表中新增 `operator_id`(操作者ID)、`operator_type`(操作者类型)、`actual_paid_amount`(实际支付金额)、`purchase_role`(订单角色)字段,用于区分"谁下单"和"谁买单"
|
||||
- **支持代理钱包一步购买**:代理在后台使用 wallet 创建订单时,立即检查余额、扣款、激活套餐,订单状态直接为已支付(一步完成,无需后续支付接口)
|
||||
- **区分代购场景**:
|
||||
- 代理自购(资源属于自己):从自己钱包扣自己的成本价,订单金额 = 实际支付
|
||||
- 代理代购(资源属于下级):从自己钱包扣自己的成本价,但订单金额显示下级成本价(让下级看到他的成本)
|
||||
- 平台代购(offline):保持现有逻辑(不扣款,立即激活)
|
||||
- **订单查询增强**:代理可以查询 `buyer_id = 自己` 或 `operator_id = 自己` 的订单(看到自己作为买家或操作者的所有订单)
|
||||
- **钱包流水记录**:钱包扣款时记录交易子类型(自购 / 给下级购买)和关联店铺ID,支持按场景筛选
|
||||
- **佣金逻辑调整**:代理代购订单不产生佣金(操作者已赚取成本价差),只有平台代购才触发佣金计算
|
||||
|
||||
## Capabilities
|
||||
|
||||
### New Capabilities
|
||||
|
||||
- `agent-order-role-tracking`: 订单角色追踪能力,记录并区分订单中的操作者、买家、支付者等角色关系,支持多种代购场景的数据查询和业务分析
|
||||
|
||||
### Modified Capabilities
|
||||
|
||||
- `purchase-on-behalf`: 扩展代购订单需求,新增代理使用钱包(wallet)代购的场景,区别于现有的平台线下(offline)代购
|
||||
- `order-payment`: 新增后台订单钱包一步支付需求,代理创建订单时立即扣款并激活套餐,区别于 H5 端的两步支付流程(创建待支付订单 → 调用支付接口)
|
||||
|
||||
## Impact
|
||||
|
||||
### 数据库变更
|
||||
- **订单表**(`tb_order`)新增字段:
|
||||
- `operator_id` (INT, 可空):操作者ID
|
||||
- `operator_type` (VARCHAR, 可空):操作者类型(platform/agent)
|
||||
- `actual_paid_amount` (BIGINT, 可空):实际支付金额(分)
|
||||
- `purchase_role` (VARCHAR):订单角色枚举(self_purchase/purchased_by_parent/purchased_by_platform/purchase_for_subordinate)
|
||||
- 新增索引:`idx_orders_operator_id`、`idx_orders_purchase_role`
|
||||
|
||||
- **钱包流水表**(`tb_agent_wallet_transaction`)新增/确认字段(如果不存在):
|
||||
- `transaction_subtype` (VARCHAR):交易子类型(细分 order_payment 场景)
|
||||
- `related_shop_id` (INT, 可空):关联店铺ID(代购时记录下级店铺)
|
||||
|
||||
### 受影响的模块
|
||||
- `internal/model/order.go`:新增字段和枚举常量
|
||||
- `internal/model/agent_wallet.go`:确认流水表字段
|
||||
- `internal/model/dto/order_dto.go`:OrderResponse 新增字段,OrderListRequest 新增筛选参数
|
||||
- `internal/service/order/service.go`:重构 `Create()` 方法,新增 `createOrderWithWalletPayment()` 方法
|
||||
- `internal/store/postgres/order_store.go`:修改 `List()` 支持 OR 查询(buyer_id 或 operator_id)
|
||||
- `internal/handler/admin/order.go`:调整权限检查和查询逻辑
|
||||
|
||||
### API 影响
|
||||
- **后台订单创建 API**(POST `/api/admin/orders`):
|
||||
- 行为变更:代理使用 wallet 支付时,订单直接完成(payment_status = paid),无需后续支付
|
||||
- 响应新增字段:`operator_id`, `operator_type`, `actual_paid_amount`, `purchase_role`, `is_purchased_by_parent`, `purchase_remark`
|
||||
- **订单列表 API**(GET `/api/admin/orders`):
|
||||
- 新增查询参数:`purchase_role`(可选,筛选订单角色类型)
|
||||
- 查询逻辑变更:代理可以看到作为操作者或买家的所有订单
|
||||
|
||||
### 兼容性
|
||||
- **向后兼容**:现有订单字段为空值,不影响已有订单查询
|
||||
- **平台代购(offline)逻辑不变**:保持现有行为
|
||||
- **H5 钱包支付不受影响**:H5 端仍使用两步流程(创建待支付订单 → 调用 WalletPay 接口)
|
||||
@@ -0,0 +1,167 @@
|
||||
# Capability: 订单角色追踪
|
||||
|
||||
## Purpose
|
||||
|
||||
本 capability 定义订单角色追踪能力,记录并区分订单中的操作者、买家、支付者等角色关系,支持多种代购场景的数据查询和业务分析。
|
||||
|
||||
## ADDED Requirements
|
||||
|
||||
### Requirement: 订单操作者记录
|
||||
|
||||
系统 SHALL 在订单创建时记录操作者信息(谁下的单),区别于买家信息(资源所属者)。
|
||||
|
||||
#### Scenario: 平台创建订单
|
||||
- **WHEN** 平台账号创建订单
|
||||
- **THEN** 订单的 `operator_id` 为 NULL,`operator_type` 为 "platform"
|
||||
|
||||
#### Scenario: 代理创建订单
|
||||
- **WHEN** 代理账号创建订单
|
||||
- **THEN** 订单的 `operator_id` 为代理店铺 ID,`operator_type` 为 "agent"
|
||||
|
||||
#### Scenario: 代理自购
|
||||
- **WHEN** 代理为自己的资源创建订单
|
||||
- **THEN** 订单的 `buyer_id` 等于 `operator_id`
|
||||
|
||||
#### Scenario: 代理代购
|
||||
- **WHEN** 代理为下级代理的资源创建订单
|
||||
- **THEN** 订单的 `buyer_id` 为资源所属店铺 ID,`operator_id` 为操作者店铺 ID,两者不同
|
||||
|
||||
---
|
||||
|
||||
### Requirement: 实际支付金额记录
|
||||
|
||||
系统 SHALL 记录订单的实际支付金额,区别于订单金额(买家视角的价格)。
|
||||
|
||||
#### Scenario: 代理自购订单
|
||||
- **WHEN** 代理为自己的资源创建订单,成本价 80 元
|
||||
- **THEN** 订单的 `total_amount` = 80 元,`actual_paid_amount` = 80 元
|
||||
|
||||
#### Scenario: 代理代购订单
|
||||
- **WHEN** 一级代理(成本价 80 元)为二级代理(成本价 100 元)的资源创建订单
|
||||
- **THEN** 订单的 `total_amount` = 100 元(买家成本价),`actual_paid_amount` = 80 元(操作者实际扣款)
|
||||
|
||||
#### Scenario: 平台代购订单
|
||||
- **WHEN** 平台为代理创建订单
|
||||
- **THEN** 订单的 `total_amount` = 代理成本价,`actual_paid_amount` 为 NULL(平台不扣款)
|
||||
|
||||
---
|
||||
|
||||
### Requirement: 订单角色枚举
|
||||
|
||||
系统 SHALL 使用 `purchase_role` 字段标识订单角色关系,支持高效筛选。
|
||||
|
||||
#### Scenario: 自己购买
|
||||
- **WHEN** 代理为自己的资源创建订单
|
||||
- **THEN** 订单的 `purchase_role` = "self_purchase"
|
||||
|
||||
#### Scenario: 上级代理购买
|
||||
- **WHEN** 代理查询作为买家的订单,且 `operator_id` 不为 NULL 且不等于 `buyer_id`
|
||||
- **THEN** 该订单的 `purchase_role` = "purchased_by_parent"(从买家视角)或 "purchase_for_subordinate"(从操作者视角)
|
||||
|
||||
#### Scenario: 平台代购
|
||||
- **WHEN** 平台为代理创建订单
|
||||
- **THEN** 订单的 `purchase_role` = "purchased_by_platform"
|
||||
|
||||
#### Scenario: 给下级购买
|
||||
- **WHEN** 代理为下级代理的资源创建订单
|
||||
- **THEN** 订单的 `purchase_role` = "purchase_for_subordinate"
|
||||
|
||||
---
|
||||
|
||||
### Requirement: 订单查询增强
|
||||
|
||||
系统 SHALL 支持代理查询作为买家或操作者的所有订单。
|
||||
|
||||
#### Scenario: 代理查询自己相关的订单
|
||||
- **WHEN** 代理查询订单列表
|
||||
- **THEN** 系统返回 `buyer_id = 代理店铺 ID` 或 `operator_id = 代理店铺 ID` 的所有订单
|
||||
|
||||
#### Scenario: 按订单角色筛选
|
||||
- **WHEN** 代理查询订单列表,指定 `purchase_role = "self_purchase"`
|
||||
- **THEN** 系统只返回自己购买的订单
|
||||
|
||||
#### Scenario: 按订单角色筛选给下级购买的订单
|
||||
- **WHEN** 代理查询订单列表,指定 `purchase_role = "purchase_for_subordinate"`
|
||||
- **THEN** 系统只返回为下级代理购买的订单
|
||||
|
||||
---
|
||||
|
||||
### Requirement: 订单响应包含角色信息
|
||||
|
||||
系统 SHALL 在订单响应中包含操作者和角色信息,支持前端展示。
|
||||
|
||||
#### Scenario: 订单响应包含操作者 ID
|
||||
- **WHEN** 查询订单详情
|
||||
- **THEN** 响应包含 `operator_id`、`operator_type` 字段
|
||||
|
||||
#### Scenario: 订单响应包含操作者名称
|
||||
- **WHEN** 查询订单详情,且 `operator_type = "agent"`
|
||||
- **THEN** 响应包含 `operator_name` 字段(从 Shop 表查询)
|
||||
|
||||
#### Scenario: 订单响应包含角色标识
|
||||
- **WHEN** 查询订单详情
|
||||
- **THEN** 响应包含 `purchase_role`、`is_purchased_by_parent`、`purchase_remark` 字段
|
||||
|
||||
#### Scenario: 上级代购订单的备注
|
||||
- **WHEN** 查询上级代理购买的订单
|
||||
- **THEN** `purchase_remark` 为"由上级代理【XX】购买"
|
||||
|
||||
#### Scenario: 平台代购订单的备注
|
||||
- **WHEN** 查询平台代购的订单
|
||||
- **THEN** `purchase_remark` 为"由平台代购"
|
||||
|
||||
---
|
||||
|
||||
### Requirement: 数据权限保持一致
|
||||
|
||||
系统 SHALL 确保订单角色追踪不影响现有数据权限逻辑。
|
||||
|
||||
#### Scenario: 代理只能查询有权限的订单
|
||||
- **WHEN** 代理查询订单列表
|
||||
- **THEN** 系统应用数据权限过滤,只返回 `buyer_id` 或 `operator_id` 在权限范围内的订单
|
||||
|
||||
#### Scenario: 平台可查询所有订单
|
||||
- **WHEN** 平台账号查询订单列表
|
||||
- **THEN** 系统不应用数据权限过滤,返回所有订单
|
||||
|
||||
---
|
||||
|
||||
### Requirement: 订单角色常量定义
|
||||
|
||||
系统 SHALL 在 `internal/model/order.go` 中定义订单角色枚举常量。
|
||||
|
||||
#### Scenario: 订单角色枚举值
|
||||
- **WHEN** 代码中使用订单角色
|
||||
- **THEN** 可用的枚举值包括:
|
||||
- `PurchaseRoleSelfPurchase` = "self_purchase"
|
||||
- `PurchaseRolePurchasedByParent` = "purchased_by_parent"
|
||||
- `PurchaseRolePurchasedByPlatform` = "purchased_by_platform"
|
||||
- `PurchaseRolePurchaseForSubordinate` = "purchase_for_subordinate"
|
||||
|
||||
---
|
||||
|
||||
### Requirement: 数据库索引支持
|
||||
|
||||
系统 SHALL 为订单角色追踪字段创建索引,支持高效查询。
|
||||
|
||||
#### Scenario: operator_id 索引
|
||||
- **WHEN** 查询 `operator_id = X` 的订单
|
||||
- **THEN** 数据库使用 `idx_orders_operator_id` 索引
|
||||
|
||||
#### Scenario: purchase_role 索引
|
||||
- **WHEN** 查询 `purchase_role = 'self_purchase'` 的订单
|
||||
- **THEN** 数据库使用 `idx_orders_purchase_role` 索引
|
||||
|
||||
---
|
||||
|
||||
### Requirement: 向后兼容性
|
||||
|
||||
系统 SHALL 确保新增字段不影响现有订单数据和查询。
|
||||
|
||||
#### Scenario: 现有订单字段为 NULL
|
||||
- **WHEN** 查询历史订单
|
||||
- **THEN** `operator_id`、`operator_type`、`actual_paid_amount`、`purchase_role` 字段为 NULL 或空值,不影响查询结果
|
||||
|
||||
#### Scenario: 订单列表查询兼容
|
||||
- **WHEN** 代理查询订单列表,不指定 `purchase_role` 筛选
|
||||
- **THEN** 系统返回所有订单,包括历史订单(role 为 NULL)
|
||||
@@ -0,0 +1,159 @@
|
||||
## ADDED Requirements
|
||||
|
||||
### Requirement: 后台钱包一步支付
|
||||
|
||||
系统 SHALL 支持后台订单创建时使用钱包支付立即完成订单,无需后续调用支付接口。
|
||||
|
||||
#### Scenario: 后台订单创建时钱包支付
|
||||
- **WHEN** 代理在后台创建订单,支付方式为 wallet,钱包余额充足
|
||||
- **THEN** 系统创建订单,立即扣减钱包余额,订单状态为已支付(`payment_status` = 2),激活套餐
|
||||
|
||||
#### Scenario: 后台钱包支付余额不足
|
||||
- **WHEN** 代理在后台创建订单,支付方式为 wallet,钱包余额不足
|
||||
- **THEN** 系统返回错误"余额不足",订单创建失败
|
||||
|
||||
#### Scenario: 后台钱包支付订单响应
|
||||
- **WHEN** 后台钱包支付订单创建成功
|
||||
- **THEN** API 响应包含已支付的订单信息,`payment_status` = 2,`payment_method` = "wallet",`paid_at` 为当前时间
|
||||
|
||||
#### Scenario: 后台钱包支付不创建待支付订单
|
||||
- **WHEN** 代理在后台创建 wallet 订单
|
||||
- **THEN** 系统不创建待支付订单(`payment_status` != 1),直接完成支付
|
||||
|
||||
---
|
||||
|
||||
### Requirement: H5 钱包两步支付保持不变
|
||||
|
||||
系统 SHALL 保持 H5 端钱包支付的两步流程(创建待支付订单 → 调用支付接口)。
|
||||
|
||||
#### Scenario: H5 创建待支付订单
|
||||
- **WHEN** 个人客户在 H5 端创建订单,支付方式为 wallet
|
||||
- **THEN** 系统创建订单,`payment_status` = 1(待支付),不扣减钱包余额
|
||||
|
||||
#### Scenario: H5 调用 WalletPay 接口支付
|
||||
- **WHEN** 个人客户调用 WalletPay 接口支付待支付订单
|
||||
- **THEN** 系统扣减钱包余额,更新订单状态为已支付,激活套餐
|
||||
|
||||
#### Scenario: H5 和后台钱包支付流程独立
|
||||
- **WHEN** H5 端创建 wallet 订单
|
||||
- **THEN** 不影响后台 wallet 订单的一步支付逻辑
|
||||
|
||||
---
|
||||
|
||||
### Requirement: 钱包流水记录扩展
|
||||
|
||||
系统 SHALL 在钱包流水中记录交易子类型和关联店铺,支持按场景筛选。
|
||||
|
||||
#### Scenario: 自购钱包流水
|
||||
- **WHEN** 代理为自己的资源购买套餐,使用 wallet
|
||||
- **THEN** 钱包流水的 `transaction_subtype` = "self_purchase",`related_shop_id` 为 NULL,`remark` = "购买套餐"
|
||||
|
||||
#### Scenario: 代购钱包流水
|
||||
- **WHEN** 代理为下级代理购买套餐,使用 wallet
|
||||
- **THEN** 钱包流水的 `transaction_subtype` = "purchase_for_subordinate",`related_shop_id` = 下级代理店铺 ID,`remark` = "为下级代理【XX】购买套餐"
|
||||
|
||||
#### Scenario: 钱包流水查询店铺名称
|
||||
- **WHEN** 创建代购钱包流水
|
||||
- **THEN** 系统查询下级店铺名称,填充到 `remark` 字段
|
||||
|
||||
#### Scenario: 钱包流水筛选
|
||||
- **WHEN** 代理查询钱包流水,筛选 `transaction_subtype` = "purchase_for_subordinate"
|
||||
- **THEN** 系统返回所有为下级代理购买的流水记录
|
||||
|
||||
---
|
||||
|
||||
### Requirement: 钱包支付乐观锁
|
||||
|
||||
系统 SHALL 使用乐观锁防止钱包并发扣款导致余额不一致。
|
||||
|
||||
#### Scenario: 钱包扣款使用 version 字段
|
||||
- **WHEN** 扣减钱包余额
|
||||
- **THEN** SQL 语句包含 `WHERE balance >= ? AND version = ?`,更新时 `version + 1`
|
||||
|
||||
#### Scenario: 钱包并发扣款失败
|
||||
- **WHEN** 两个请求同时扣减同一钱包
|
||||
- **THEN** 只有一个请求成功,另一个返回"余额不足或并发冲突"
|
||||
|
||||
#### Scenario: 乐观锁重试逻辑
|
||||
- **WHEN** 钱包扣款因 version 冲突失败
|
||||
- **THEN** 系统不自动重试,返回错误(由客户端决定是否重试)
|
||||
|
||||
---
|
||||
|
||||
### Requirement: 钱包支付幂等性
|
||||
|
||||
系统 SHALL 防止同一订单重复创建和重复扣款。
|
||||
|
||||
#### Scenario: 订单创建幂等性检查
|
||||
- **WHEN** 同一买家对同一载体的同一套餐组合在短时间内重复创建订单
|
||||
- **THEN** 系统返回已创建的订单,不重复扣款
|
||||
|
||||
#### Scenario: 幂等性使用 Redis 业务键
|
||||
- **WHEN** 检查订单幂等性
|
||||
- **THEN** 系统使用 Redis key `order:idempotency:{buyer_type}:{buyer_id}:{order_type}:{carrier_type}:{carrier_id}:{sorted_package_ids}`
|
||||
|
||||
#### Scenario: 幂等性 TTL
|
||||
- **WHEN** 订单创建成功后标记幂等性
|
||||
- **THEN** Redis key 的 TTL 为 3 分钟
|
||||
|
||||
#### Scenario: 分布式锁防止并发
|
||||
- **WHEN** 订单创建前检查幂等性
|
||||
- **THEN** 系统使用分布式锁 `order:create:lock:{carrier_type}:{carrier_id}`,TTL 10 秒
|
||||
|
||||
---
|
||||
|
||||
### Requirement: 后台订单 API 响应扩展
|
||||
|
||||
系统 SHALL 在后台订单创建和查询 API 响应中包含钱包支付相关字段。
|
||||
|
||||
#### Scenario: 订单响应包含实际支付金额
|
||||
- **WHEN** 查询钱包支付的订单
|
||||
- **THEN** 响应包含 `actual_paid_amount` 字段
|
||||
|
||||
#### Scenario: 订单响应包含操作者信息
|
||||
- **WHEN** 查询代购订单
|
||||
- **THEN** 响应包含 `operator_id`、`operator_type`、`operator_name` 字段
|
||||
|
||||
#### Scenario: 订单响应包含购买备注
|
||||
- **WHEN** 查询上级代理购买的订单
|
||||
- **THEN** 响应包含 `purchase_remark` 字段,如"由上级代理【XX】购买"
|
||||
|
||||
---
|
||||
|
||||
### Requirement: 钱包支付错误处理
|
||||
|
||||
系统 SHALL 在钱包支付失败时返回明确的错误信息。
|
||||
|
||||
#### Scenario: 钱包不存在
|
||||
- **WHEN** 钱包支付时钱包不存在
|
||||
- **THEN** 系统返回错误"钱包不存在"(`CodeWalletNotFound`)
|
||||
|
||||
#### Scenario: 余额不足
|
||||
- **WHEN** 钱包支付时余额不足
|
||||
- **THEN** 系统返回错误"余额不足"(`CodeInsufficientBalance`)
|
||||
|
||||
#### Scenario: 并发冲突
|
||||
- **WHEN** 钱包扣款因 version 冲突失败
|
||||
- **THEN** 系统返回错误"余额不足或并发冲突"(`CodeInsufficientBalance`)
|
||||
|
||||
#### Scenario: 套餐激活失败
|
||||
- **WHEN** 钱包扣款成功但套餐激活失败
|
||||
- **THEN** 事务回滚,钱包余额恢复,返回激活失败错误
|
||||
|
||||
---
|
||||
|
||||
### Requirement: 钱包支付与第三方支付的区别
|
||||
|
||||
系统 SHALL 区分后台钱包支付和第三方支付的业务逻辑。
|
||||
|
||||
#### Scenario: 后台不支持第三方支付
|
||||
- **WHEN** 代理在后台创建订单时选择 wechat 或 alipay
|
||||
- **THEN** 系统返回错误"后台只支持 wallet 和 offline 支付方式"
|
||||
|
||||
#### Scenario: H5 支持第三方支付
|
||||
- **WHEN** 个人客户在 H5 端创建订单时选择 wechat 或 alipay
|
||||
- **THEN** 系统创建待支付订单,返回支付参数(prepay_id 或 h5_url)
|
||||
|
||||
#### Scenario: 钱包支付不需要支付参数
|
||||
- **WHEN** 后台钱包支付订单创建成功
|
||||
- **THEN** 响应不包含 prepay_id、h5_url 等第三方支付参数
|
||||
@@ -0,0 +1,131 @@
|
||||
## ADDED Requirements
|
||||
|
||||
### Requirement: 代理钱包代购
|
||||
|
||||
系统 SHALL 允许代理使用钱包支付(wallet)为下级代理创建代购订单,从自己钱包扣款并立即激活套餐。
|
||||
|
||||
#### Scenario: 代理为下级代理钱包代购
|
||||
- **WHEN** 代理选择下级代理的资源创建订单,支付方式为 wallet
|
||||
- **THEN** 系统创建订单,`buyer_id` = 下级代理店铺 ID,`operator_id` = 操作者店铺 ID,`is_purchase_on_behalf` = true,`payment_method` = "wallet",`payment_status` = 2(已支付)
|
||||
|
||||
#### Scenario: 钱包代购扣款操作者钱包
|
||||
- **WHEN** 代理使用 wallet 为下级代理购买套餐
|
||||
- **THEN** 系统从操作者(上级代理)的钱包扣款
|
||||
|
||||
#### Scenario: 钱包代购使用操作者成本价扣款
|
||||
- **WHEN** 一级代理(成本价 80 元)为二级代理(成本价 100 元)的资源创建 wallet 代购订单
|
||||
- **THEN** 系统从一级代理钱包扣款 80 元(操作者成本价)
|
||||
|
||||
#### Scenario: 钱包代购订单金额显示买家成本价
|
||||
- **WHEN** 一级代理为二级代理钱包代购
|
||||
- **THEN** 订单的 `total_amount` = 100 元(买家成本价),`actual_paid_amount` = 80 元(操作者实际扣款)
|
||||
|
||||
#### Scenario: 钱包代购余额不足
|
||||
- **WHEN** 代理使用 wallet 代购,但钱包余额不足
|
||||
- **THEN** 系统返回错误"余额不足",订单创建失败
|
||||
|
||||
#### Scenario: 钱包代购自动激活套餐
|
||||
- **WHEN** 钱包代购订单创建成功
|
||||
- **THEN** 系统自动激活套餐(创建 PackageUsage 记录)
|
||||
|
||||
#### Scenario: 钱包代购不触发佣金
|
||||
- **WHEN** 代理使用 wallet 代购订单完成
|
||||
- **THEN** 系统不计算佣金,不发放佣金(操作者已赚取成本价差)
|
||||
|
||||
#### Scenario: 钱包代购创建钱包流水
|
||||
- **WHEN** 代理使用 wallet 代购扣款成功
|
||||
- **THEN** 系统创建钱包流水记录,`transaction_type` = "deduct",`transaction_subtype` = "purchase_for_subordinate",`related_shop_id` = 下级代理店铺 ID
|
||||
|
||||
---
|
||||
|
||||
### Requirement: 代理自购使用钱包
|
||||
|
||||
系统 SHALL 允许代理使用钱包支付为自己的资源购买套餐,立即扣款并激活。
|
||||
|
||||
#### Scenario: 代理为自己的资源购买套餐
|
||||
- **WHEN** 代理选择自己的资源创建订单,支付方式为 wallet
|
||||
- **THEN** 系统创建订单,`buyer_id` = 代理店铺 ID,`operator_id` = 代理店铺 ID,`is_purchase_on_behalf` = false,`payment_method` = "wallet",`payment_status` = 2(已支付)
|
||||
|
||||
#### Scenario: 代理自购扣款自己成本价
|
||||
- **WHEN** 代理为自己的资源购买套餐,成本价 80 元
|
||||
- **THEN** 系统从代理钱包扣款 80 元,订单金额 = 80 元,实际支付 = 80 元
|
||||
|
||||
#### Scenario: 代理自购自动激活套餐
|
||||
- **WHEN** 代理自购订单创建成功
|
||||
- **THEN** 系统自动激活套餐
|
||||
|
||||
#### Scenario: 代理自购创建钱包流水
|
||||
- **WHEN** 代理自购扣款成功
|
||||
- **THEN** 系统创建钱包流水记录,`transaction_type` = "deduct",`transaction_subtype` = "self_purchase"
|
||||
|
||||
---
|
||||
|
||||
### Requirement: 钱包代购权限控制
|
||||
|
||||
系统 SHALL 在后台订单创建 API 中允许代理使用 wallet 支付方式。
|
||||
|
||||
#### Scenario: 代理可使用 wallet
|
||||
- **WHEN** 代理账号创建订单时选择支付方式为 wallet
|
||||
- **THEN** 系统允许创建订单(不返回权限错误)
|
||||
|
||||
#### Scenario: 平台可使用 wallet
|
||||
- **WHEN** 平台账号创建订单时选择支付方式为 wallet
|
||||
- **THEN** 系统允许创建订单
|
||||
|
||||
#### Scenario: 企业账号不可使用 wallet
|
||||
- **WHEN** 企业账号尝试在后台创建订单
|
||||
- **THEN** 系统返回错误"无权限创建订单"
|
||||
|
||||
---
|
||||
|
||||
### Requirement: 后台订单钱包支付与 H5 端区分
|
||||
|
||||
系统 SHALL 区分后台订单创建和 H5 端订单创建的钱包支付流程。
|
||||
|
||||
#### Scenario: 后台 wallet 订单一步完成
|
||||
- **WHEN** 代理在后台使用 wallet 创建订单
|
||||
- **THEN** 订单创建后立即标记为已支付(`payment_status` = 2),无需调用后续支付接口
|
||||
|
||||
#### Scenario: H5 端 wallet 订单两步流程
|
||||
- **WHEN** 个人客户在 H5 端使用 wallet 创建订单
|
||||
- **THEN** 订单创建后标记为待支付(`payment_status` = 1),需要调用 WalletPay 接口完成支付
|
||||
|
||||
---
|
||||
|
||||
### Requirement: 钱包代购与平台代购的区别
|
||||
|
||||
系统 SHALL 区分钱包代购(wallet)和平台代购(offline)的业务逻辑。
|
||||
|
||||
#### Scenario: 平台代购不扣款
|
||||
- **WHEN** 平台使用 offline 创建代购订单
|
||||
- **THEN** 系统不扣减任何钱包余额
|
||||
|
||||
#### Scenario: 钱包代购扣款
|
||||
- **WHEN** 代理使用 wallet 创建代购订单
|
||||
- **THEN** 系统扣减操作者钱包余额
|
||||
|
||||
#### Scenario: 平台代购产生佣金
|
||||
- **WHEN** 平台使用 offline 创建代购订单
|
||||
- **THEN** 系统计算并发放佣金
|
||||
|
||||
#### Scenario: 钱包代购不产生佣金
|
||||
- **WHEN** 代理使用 wallet 创建代购订单
|
||||
- **THEN** 系统不计算佣金
|
||||
|
||||
---
|
||||
|
||||
### Requirement: 钱包代购事务保证
|
||||
|
||||
系统 SHALL 在事务中完成钱包代购的订单创建、扣款、流水记录、套餐激活。
|
||||
|
||||
#### Scenario: 钱包代购事务成功
|
||||
- **WHEN** 钱包代购的所有步骤成功
|
||||
- **THEN** 事务提交,订单创建、钱包扣款、流水记录、套餐激活全部完成
|
||||
|
||||
#### Scenario: 钱包代购事务失败回滚
|
||||
- **WHEN** 钱包代购过程中任一步骤失败(如余额不足、套餐激活失败)
|
||||
- **THEN** 事务回滚,订单不创建,钱包余额不变
|
||||
|
||||
#### Scenario: 钱包代购并发控制
|
||||
- **WHEN** 多个请求同时为同一载体创建订单
|
||||
- **THEN** 系统使用乐观锁(version 字段)和幂等性检查防止并发问题
|
||||
@@ -0,0 +1,159 @@
|
||||
## 1. 数据库结构变更
|
||||
|
||||
- [x] 1.1 创建订单表字段迁移脚本(`migrations/xxx_add_operator_fields_to_orders.up.sql`),新增 `operator_id`、`operator_type`、`actual_paid_amount`、`purchase_role` 字段,添加字段注释
|
||||
- [x] 1.2 在迁移脚本中创建索引(`idx_orders_operator_id`、`idx_orders_purchase_role`),使用 CONCURRENTLY 避免锁表
|
||||
- [x] 1.3 创建钱包流水表字段迁移脚本(`migrations/xxx_add_transaction_subtype_to_wallet_transaction.up.sql`),检查并添加 `transaction_subtype` 和 `related_shop_id` 字段(如果不存在)
|
||||
- [x] 1.4 创建数据回滚迁移脚本(`*.down.sql`),包含 DROP INDEX 和 DROP COLUMN 语句
|
||||
- [x] 1.5 在测试环境执行迁移,验证字段创建成功,检查 `\d tb_order` 和 `\d tb_agent_wallet_transaction` 输出
|
||||
|
||||
## 2. Model 层:订单角色追踪
|
||||
|
||||
- [x] 2.1 在 `internal/model/order.go` 中的 `Order` 结构体添加新字段:`OperatorID`、`OperatorType`、`ActualPaidAmount`、`PurchaseRole`,添加 gorm 标签和中文注释
|
||||
- [x] 2.2 在 `internal/model/order.go` 中定义订单角色枚举常量(`PurchaseRoleSelfPurchase`、`PurchaseRolePurchasedByParent`、`PurchaseRolePurchasedByPlatform`、`PurchaseRolePurchaseForSubordinate`),添加中文注释
|
||||
- [x] 2.3 在 `internal/model/agent_wallet.go` 中确认 `AgentWalletTransaction` 结构体包含 `TransactionSubtype` 和 `RelatedShopID` 字段(如果不存在则添加)
|
||||
- [x] 2.4 运行 `go build ./...` 验证编译通过
|
||||
|
||||
## 3. 常量定义:钱包流水子类型
|
||||
|
||||
- [x] 3.1 在 `pkg/constants/wallet.go` 中新增钱包交易子类型常量(`WalletTransactionSubtypeSelfPurchase`、`WalletTransactionSubtypePurchaseForSubordinate`),添加中文注释
|
||||
- [x] 3.2 运行 `go build ./...` 验证编译通过
|
||||
|
||||
## 4. DTO 层:订单请求和响应
|
||||
|
||||
- [x] 4.1 在 `internal/model/dto/order_dto.go` 的 `OrderResponse` 中添加新字段:`OperatorID`、`OperatorType`、`OperatorName`、`ActualPaidAmount`、`PurchaseRole`、`IsPurchasedByParent`、`PurchaseRemark`,添加 JSON 标签和 description 注释
|
||||
- [x] 4.2 在 `internal/model/dto/order_dto.go` 的 `OrderListRequest` 中添加 `PurchaseRole` 筛选字段,添加验证标签(`validate:"omitempty,oneof=self_purchase purchased_by_parent purchased_by_platform purchase_for_subordinate"`)
|
||||
- [x] 4.3 运行 `go build ./...` 验证编译通过
|
||||
|
||||
## 5. Store 层:订单查询 OR 逻辑
|
||||
|
||||
- [x] 5.1 修改 `internal/store/postgres/order_store.go` 的 `List()` 方法,支持 `shop_id` 筛选时使用 OR 查询:`WHERE (buyer_type = 'agent' AND buyer_id = ?) OR operator_id = ?`
|
||||
- [x] 5.2 在 `List()` 方法中添加 `purchase_role` 精确匹配筛选支持
|
||||
- [x] 5.3 运行 `go build ./...` 验证编译通过
|
||||
- [x] 5.4 使用 PostgreSQL MCP 工具验证查询逻辑:创建测试订单,执行 `SELECT * FROM tb_order WHERE (buyer_id = X) OR (operator_id = X)` 并检查 EXPLAIN 输出
|
||||
|
||||
## 6. Service 层:成本价查询辅助方法
|
||||
|
||||
- [x] 6.1 在 `internal/service/order/service.go` 中新增 `getCostPrice(ctx, shopID, packageID)` 方法,通过 `ShopPackageAllocation` 查询店铺对套餐的成本价
|
||||
- [x] 6.2 添加错误处理:如果查询失败,返回 `errors.New(errors.CodeInvalidParam, "店铺没有该套餐的分配配置")`
|
||||
- [x] 6.3 运行 `go build ./...` 验证编译通过
|
||||
|
||||
## 7. Service 层:钱包流水创建方法
|
||||
|
||||
- [x] 7.1 在 `internal/service/order/service.go` 中新增 `createWalletTransaction(ctx, tx, walletID, orderID, amount, purchaseRole, relatedShopID)` 方法
|
||||
- [x] 7.2 在方法中根据 `purchaseRole` 确定 `transaction_subtype` 和 `remark`:自购场景填充"购买套餐",代购场景查询下级店铺名称填充"为下级代理【XX】购买套餐"
|
||||
- [x] 7.3 创建 `AgentWalletTransaction` 记录,设置 `TransactionType` = `AgentTransactionTypeDeduct`,`TransactionSubtype`、`Amount`(负数)、`RelatedShopID`、`Remark`
|
||||
- [x] 7.4 运行 `go build ./...` 验证编译通过
|
||||
|
||||
## 8. Service 层:钱包支付订单创建方法
|
||||
|
||||
- [x] 8.1 在 `internal/service/order/service.go` 中新增 `createOrderWithWalletPayment(ctx, order, items, operatorShopID, buyerShopID)` 方法
|
||||
- [x] 8.2 在方法开头(事务外)检查钱包余额,如果余额不足返回错误
|
||||
- [x] 8.3 开启 GORM 事务,在事务中依次执行:创建订单(`tx.Create(order)`)、创建订单明细(`tx.CreateInBatches(items, 100)`)
|
||||
- [x] 8.4 在事务中扣减钱包余额,使用乐观锁:`WHERE id = ? AND balance >= ? AND version = ?`,更新 `balance = balance - ?` 和 `version = version + 1`
|
||||
- [x] 8.5 检查 `RowsAffected`,如果为 0 返回 `errors.New(errors.CodeInsufficientBalance, "余额不足或并发冲突")`
|
||||
- [x] 8.6 在事务中调用 `createWalletTransaction()` 创建钱包流水
|
||||
- [x] 8.7 在事务中调用 `activatePackage()` 激活套餐
|
||||
- [x] 8.8 事务外判断是否入队佣金计算:`if order.OperatorID == nil { s.enqueueCommissionCalculation() }`(平台代购才入队)
|
||||
- [x] 8.9 运行 `go build ./...` 验证编译通过
|
||||
|
||||
## 9. Service 层:订单创建流程重构
|
||||
|
||||
- [x] 9.1 在 `internal/service/order/service.go` 的 `Create()` 方法中,在幂等性检查后添加场景判断逻辑
|
||||
- [x] 9.2 提取资源所属店铺 ID(从 `validationResult.Card.ShopID` 或 `validationResult.Device.ShopID`)
|
||||
- [x] 9.3 处理 `offline` 场景:设置 `operator_id = nil`、`operator_type = "platform"`、`purchase_role = "purchased_by_platform"`,调用 `resolvePurchaseOnBehalfInfo()` 获取买家成本价,保持现有逻辑调用 `createOrderWithActivation()`
|
||||
- [x] 9.4 处理 `wallet` 场景:获取操作者店铺 ID,判断资源是否属于操作者
|
||||
- [x] 9.5 如果资源属于操作者(自购):设置 `buyer = operator`、`purchase_role = "self_purchase"`、`is_purchase_on_behalf = false`,调用 `getCostPrice()` 获取成本价,`total_amount = actual_paid_amount = 操作者成本价`
|
||||
- [x] 9.6 如果资源不属于操作者(代购):设置 `buyer = 资源所属者`、`operator = 操作者`、`purchase_role = "purchase_for_subordinate"`、`is_purchase_on_behalf = true`,分别调用 `getCostPrice()` 获取买家和操作者成本价,`total_amount = 买家成本价`、`actual_paid_amount = 操作者成本价`
|
||||
- [x] 9.7 `wallet` 场景调用 `createOrderWithWalletPayment()` 而不是 `orderStore.Create()`
|
||||
- [x] 9.8 运行 `go build ./...` 验证编译通过
|
||||
|
||||
## 10. Service 层:订单响应构建方法
|
||||
|
||||
- [x] 10.1 在 `internal/service/order/service.go` 的 `buildOrderResponse()` 方法中添加新字段映射:`OperatorID`、`OperatorType`、`ActualPaidAmount`、`PurchaseRole`
|
||||
- [x] 10.2 添加 `OperatorName` 字段逻辑:如果 `operator_type = "agent"` 且 `operator_id` 不为空,查询 `Shop` 表获取店铺名称
|
||||
- [x] 10.3 添加 `IsPurchasedByParent` 派生字段:`purchase_role == "purchased_by_parent"`
|
||||
- [x] 10.4 添加 `PurchaseRemark` 派生字段:根据 `purchase_role` 和 `operator_name` 生成备注文本(如"由上级代理【XX】购买"、"由平台代购")
|
||||
- [x] 10.5 运行 `go build ./...` 验证编译通过
|
||||
|
||||
## 11. Handler 层:权限检查调整
|
||||
|
||||
- [x] 11.1 在 `internal/handler/admin/order.go` 的 `Create()` 方法中,修改 `wallet` 支付方式的权限检查,允许代理、平台、超管使用
|
||||
- [x] 11.2 保持 `offline` 支付方式只允许平台和超管使用的限制
|
||||
- [x] 11.3 运行 `go build ./...` 验证编译通过
|
||||
|
||||
## 12. Handler 层:订单查询参数传递
|
||||
|
||||
- [x] 12.1 在 `internal/handler/admin/order.go` 的 `List()` 方法中,从查询参数解析 `purchase_role`
|
||||
- [x] 12.2 将 `purchase_role` 传递给 Service 层的 `List()` 方法
|
||||
- [x] 12.3 运行 `go build ./...` 验证编译通过
|
||||
|
||||
## 13. 文档生成器更新(OpenAPI)
|
||||
|
||||
- [x] 13.1 确认 `cmd/api/docs.go` 和 `cmd/gendocs/main.go` 中的 `Handlers` 结构体已包含 `Order` Handler(如果不存在则添加)
|
||||
- [x] 13.2 运行 `go run cmd/gendocs/main.go` 生成 OpenAPI 文档
|
||||
- [x] 13.3 检查生成的文档中订单创建和列表接口的请求/响应字段是否包含新字段
|
||||
|
||||
## 14. 集成测试:代理自购场景
|
||||
|
||||
- [ ] 14.1 使用 PostgreSQL MCP 工具创建测试数据:创建代理账号、代理钱包(余额 10000 分)、IoT 卡(shop_id = 代理店铺 ID)、套餐分配配置(成本价 8000 分)
|
||||
- [ ] 14.2 使用 Postman/curl 调用后台订单创建 API,代理账号创建订单,支付方式 wallet,选择自己的卡和套餐
|
||||
- [ ] 14.3 验证响应:`payment_status` = 2,`operator_id` = 代理店铺 ID,`buyer_id` = 代理店铺 ID,`purchase_role` = "self_purchase",`total_amount` = 8000,`actual_paid_amount` = 8000
|
||||
- [ ] 14.4 使用 PostgreSQL MCP 查询订单表,验证订单记录正确
|
||||
- [ ] 14.5 使用 PostgreSQL MCP 查询钱包表,验证余额扣减:`balance` = 2000(10000 - 8000)
|
||||
- [ ] 14.6 使用 PostgreSQL MCP 查询钱包流水表,验证流水记录:`transaction_subtype` = "self_purchase",`amount` = -8000,`remark` = "购买套餐"
|
||||
- [ ] 14.7 使用 PostgreSQL MCP 查询套餐使用表(`tb_package_usage`),验证套餐已激活:`status` = 1
|
||||
|
||||
## 15. 集成测试:代理代购场景
|
||||
|
||||
- [ ] 15.1 使用 PostgreSQL MCP 工具创建测试数据:一级代理(成本价 8000)、二级代理(成本价 10000,parent_shop_id = 一级代理)、一级代理钱包(余额 10000)、IoT 卡(shop_id = 二级代理店铺 ID)、套餐分配配置
|
||||
- [ ] 15.2 使用 Postman/curl 调用后台订单创建 API,一级代理账号创建订单,支付方式 wallet,选择二级代理的卡和套餐
|
||||
- [ ] 15.3 验证响应:`payment_status` = 2,`operator_id` = 一级代理店铺 ID,`buyer_id` = 二级代理店铺 ID,`purchase_role` = "purchase_for_subordinate",`total_amount` = 10000,`actual_paid_amount` = 8000
|
||||
- [ ] 15.4 使用 PostgreSQL MCP 查询订单表,验证订单记录正确
|
||||
- [ ] 15.5 使用 PostgreSQL MCP 查询一级代理钱包,验证余额扣减:`balance` = 2000(10000 - 8000)
|
||||
- [ ] 15.6 使用 PostgreSQL MCP 查询钱包流水表,验证流水记录:`transaction_subtype` = "purchase_for_subordinate",`amount` = -8000,`related_shop_id` = 二级代理店铺 ID,`remark` 包含二级代理店铺名称
|
||||
- [ ] 15.7 使用 PostgreSQL MCP 查询套餐使用表,验证套餐已激活
|
||||
- [ ] 15.8 使用 PostgreSQL MCP 查询佣金表,验证未产生佣金记录(代理代购不产生佣金)
|
||||
|
||||
## 16. 集成测试:平台代购场景(回归测试)
|
||||
|
||||
- [ ] 16.1 使用 PostgreSQL MCP 工具创建测试数据:代理、IoT 卡(shop_id = 代理店铺 ID)、套餐分配配置(成本价 10000)
|
||||
- [ ] 16.2 使用 Postman/curl 调用后台订单创建 API,平台账号创建订单,支付方式 offline,选择代理的卡和套餐
|
||||
- [ ] 16.3 验证响应:`payment_status` = 2,`operator_id` = NULL,`operator_type` = "platform",`buyer_id` = 代理店铺 ID,`purchase_role` = "purchased_by_platform",`total_amount` = 10000,`actual_paid_amount` = NULL
|
||||
- [ ] 16.4 使用 PostgreSQL MCP 查询订单表,验证订单记录正确
|
||||
- [ ] 16.5 使用 PostgreSQL MCP 查询套餐使用表,验证套餐已激活
|
||||
- [ ] 16.6 验证平台代购逻辑未被破坏(不扣款、立即激活、产生佣金)
|
||||
|
||||
## 17. 集成测试:订单查询场景
|
||||
|
||||
- [ ] 17.1 使用 PostgreSQL MCP 工具创建测试数据:一级代理、二级代理、多个订单(自购、代购、被代购)
|
||||
- [ ] 17.2 使用 Postman/curl 调用后台订单列表 API,一级代理账号查询订单列表(不指定 purchase_role)
|
||||
- [ ] 17.3 验证响应包含:buyer_id = 一级代理的订单 + operator_id = 一级代理的订单
|
||||
- [ ] 17.4 使用 Postman/curl 调用后台订单列表 API,一级代理账号查询订单列表,指定 `purchase_role=self_purchase`
|
||||
- [ ] 17.5 验证响应只包含自购订单
|
||||
- [ ] 17.6 使用 Postman/curl 调用后台订单列表 API,一级代理账号查询订单列表,指定 `purchase_role=purchase_for_subordinate`
|
||||
- [ ] 17.7 验证响应只包含为下级代理购买的订单
|
||||
|
||||
## 18. 集成测试:边界场景
|
||||
|
||||
- [ ] 18.1 测试钱包余额不足:代理钱包余额 5000,创建订单金额 8000,验证返回错误"余额不足"
|
||||
- [ ] 18.2 测试并发扣款:模拟两个请求同时为同一钱包扣款,验证乐观锁生效,只有一个请求成功
|
||||
- [ ] 18.3 测试幂等性:同一买家对同一载体的同一套餐组合短时间内重复创建订单,验证返回相同订单 ID,不重复扣款
|
||||
- [ ] 18.4 测试 H5 端 wallet 订单:使用 H5 端 API 创建 wallet 订单,验证订单状态为待支付(`payment_status` = 1),不影响后台逻辑
|
||||
|
||||
## 19. 数据回填(可选)
|
||||
|
||||
- [x] 19.1 编写数据回填脚本,将现有 `payment_method = 'offline'` 且 `is_purchase_on_behalf = true` 的订单回填 `purchase_role = 'purchased_by_platform'` 和 `operator_type = 'platform'`
|
||||
- [ ] 19.2 在测试环境执行回填脚本,验证历史订单可正常查询
|
||||
|
||||
## 20. 文档更新
|
||||
|
||||
- [x] 20.1 更新接口文档说明订单创建 API 的行为变更(后台 wallet 支付一步完成)
|
||||
- [x] 20.2 更新接口文档说明订单响应新增字段的含义
|
||||
- [x] 20.3 更新接口文档说明订单列表 API 新增 `purchase_role` 查询参数
|
||||
|
||||
## 21. 生产环境部署准备
|
||||
|
||||
- [ ] 21.1 在测试环境充分验证所有场景通过
|
||||
- [x] 21.2 准备生产环境迁移脚本和回滚脚本
|
||||
- [x] 21.3 准备灰度发布计划:代码部署 → 观察日志 → 验证核心功能 → 全量发布
|
||||
- [x] 21.4 准备监控指标:订单创建成功率、钱包扣款成功率、错误日志(余额不足、并发冲突)
|
||||
Reference in New Issue
Block a user