feat: 实现代理钱包订单创建和订单角色追踪功能
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:
2026-02-28 14:11:42 +08:00
parent c5bf85c8de
commit 8ed3d9da93
24 changed files with 3346 additions and 52 deletions

View File

@@ -0,0 +1,2 @@
schema: spec-driven
created: 2026-02-28

View File

@@ -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 = 100B 看到的订单金额)
- actual_paid_amount = 80A 实际扣款)
- 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 元
场景 1A 为 B 的卡购买套餐(代理代购)
- A 扣款80 元
- A 利润100 - 80 = 20 元
- 佣金A 已赚取差价)
场景 2平台为 B 的卡购买套餐(平台代购)
- 平台扣款0 元
- 订单金额100 元
- 佣金:给 AB 的上级),金额 = 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`
---
### 决策 9DTO 响应字段设计
**决策**`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`
- 是否需要隐藏?(业务决策)

View File

@@ -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 接口)

View File

@@ -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

View File

@@ -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 等第三方支付参数

View File

@@ -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 字段)和幂等性检查防止并发问题

View File

@@ -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` = 200010000 - 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、二级代理成本价 10000parent_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` = 200010000 - 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 准备监控指标:订单创建成功率、钱包扣款成功率、错误日志(余额不足、并发冲突)

View File

@@ -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

View File

@@ -107,3 +107,162 @@
#### Scenario: 余额扣减后套餐激活失败
- **WHEN** 余额扣减成功但套餐激活失败
- **THEN** 事务回滚,余额恢复,订单状态不变
## 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 等第三方支付参数

View File

@@ -143,3 +143,134 @@
#### Scenario: 线下支付只用于代购
- **WHEN** payment_method = "offline"
- **THEN** 订单必须标记为 is_purchase_on_behalf = true
## 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 字段)和幂等性检查防止并发问题