# 代理钱包订单创建功能总结 ## 概述 fix-agent-wallet-order-creation 提案修复了代理在后台使用钱包支付创建订单的问题,实现了代理钱包一步购买(扣款 + 激活)、代理代购、订单角色追踪等核心功能。 ## ��景问题 ### 问题描述 代理在后台使用钱包支付(wallet)创建订单时,系统只创建待支付订单(`payment_status = 1`),不扣款也不激活套餐,导致订单无法完成。后台没有支付接口,代理无法对待支付订单进行支付。 ### 业务场景 - **代理自购**:代理为自己的卡/设备购买套餐,从自己钱包扣自己的成本价 - **代理代购**:代理为下级代理的卡/设备购买套餐,从自己钱包扣自己的成本价,但订单金额显示下级成本价 - **平台代购**(现有逻辑):平台使用 offline 支付为代理创建订单,不扣款,立即激活,产生佣金 ## 核心功能 ### 1. 订单角色追踪 **新增字段**(`tb_order` 表): - `operator_id` (INT, 可空):操作者 ID(谁下的单) - `operator_type` (VARCHAR, 可空):操作者类型(`platform` / `agent`) - `actual_paid_amount` (BIGINT, 可空):实际支付金额(分) - `purchase_role` (VARCHAR):订单角色枚举 **订单角色枚举**(`internal/model/order.go`): ```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. 后台钱包一步支付 **行为变更**: - **原逻辑**:后台 wallet 订单 → 创建待支付订单(`payment_status = 1`)→ 无法支付 - **新逻辑**:后台 wallet 订单 → 立即扣款 + 激活套餐 → 订单已支付(`payment_status = 2`) **区别于 H5 端**: - H5 端 wallet 订单仍使用两步流程:创建待支付订单 → 调用 WalletPay 接口支付 - 后台 wallet 订单一步完成,无需后续支付接口 **权限调整**: - 允许代理、平台、超管使用 wallet 支付方式 - offline 支付方式仍限制为平台和超管 --- ### 3. 价格计算逻辑 **区分"订单金额"和"实际支付"**: | 场景 | 订单金额(total_amount) | 实际支付(actual_paid_amount) | 说明 | |------|------------------------|------------------------------|------| | 代理自购 | 操作者成本价 | 操作者成本价 | 两者相同 | | 代理代购 | 买家成本价 | 操作者成本价 | 操作者实际扣款少于订单金额(赚取差价) | | 平台代购 | 买家成本价 | NULL | 平台不扣款 | **示例**: ``` 一级代理 A 成本价:80 元 二级代理 B 成本价:100 元 A 为 B 的卡购买套餐: - total_amount = 10000(100 元,B 看到的订单金额) - actual_paid_amount = 8000(80 元,A 实际扣款) - A 赚取差价:20 元 ``` **成本价查询**: 通过 `ShopPackageAllocation` 表查询店铺对套餐的成本价。 --- ### 4. 钱包流水记录扩展 **新增字段**(`tb_agent_wallet_transaction` 表): - `transaction_subtype` (VARCHAR):交易子类型(细分 order_payment 场景) - `related_shop_id` (INT, 可空):关联店铺 ID(代购时记录下级店铺) **交易子类型枚举**(`pkg/constants/wallet.go`): ```go const ( WalletTransactionSubtypeSelfPurchase = "self_purchase" WalletTransactionSubtypePurchaseForSubordinate = "purchase_for_subordinate" ) ``` **流水示例**: - **自购**:`transaction_subtype = "self_purchase"`,`remark = "购买套餐"` - **代购**:`transaction_subtype = "purchase_for_subordinate"`,`related_shop_id = 下级店铺 ID`,`remark = "为下级代理【XX】购买套餐"` --- ### 5. 订单查询增强 **OR 查询逻辑**(`OrderStore.List()`): ```sql WHERE (buyer_type = 'agent' AND buyer_id = ?) OR operator_id = ? ``` 代理可以看到两类订单: 1. 作为买家的订单(`buyer_id = 自己`):别人为自己代购、自己购买 2. 作为操作者的订单(`operator_id = 自己`):自己为下级代购 **新增查询参数**: - `purchase_role`(可选):筛选订单角色类型(self_purchase / purchased_by_parent / purchased_by_platform / purchase_for_subordinate) --- ### 6. 佣金逻辑调整 **规则**: - **代理代购**:操作者已赚取成本价差(自己成本价 vs 下级成本价),不产生佣金 - **平台代购**:平台不扣款,按买家成本价计算差价佣金,激励上级代理 **实现**: ```go // 只有平台代购(operator_id == nil)才入队佣金计算 if order.OperatorID == nil { s.enqueueCommissionCalculation(ctx, order.ID) } ``` --- ### 7. 幂等性和并发控制 **乐观锁**(钱包扣款): ```go result := tx.Model(&model.AgentWallet{}). Where("id = ? AND balance >= ? AND version = ?", walletID, amount, version). Updates(map[string]any{ "balance": gorm.Expr("balance - ?", amount), "version": gorm.Expr("version + 1"), }) ``` **幂等性检查**(订单创建): - 使用 Redis 业务键:`order:idempotency:{buyer_type}:{buyer_id}:{order_type}:{carrier_type}:{carrier_id}:{sorted_package_ids}` - TTL:3 分钟 - 分布式锁防止并发:`order:create:lock:{carrier_type}:{carrier_id}` --- ## API 变更 ### 后台订单创建 API(❗ Breaking Change) **端点**:`POST /api/admin/orders` **请求参数变更**: | 字段 | 变更前 | 变更后 | 说明 | |------|--------|--------|------| | `payment_method` | 可选,任意值 | **必填**,仅允许 `wallet` 或 `offline` | 不传或传其他值均返回 1001 错误 | **行为变更**: - `wallet` 支付:订单直接完成(`payment_status = 2`),无需后续支付接口 - `offline` 支付:逻辑保持不变 - 传入 `wechat`/`alipay` → 返回 `{"code": 1001, "msg": "请求参数解析失败"}` **响应新增字段**: ```json { "operator_id": 123, "operator_type": "agent", "operator_name": "一级代理 A", "actual_paid_amount": 8000, "purchase_role": "purchase_for_subordinate", "is_purchased_by_parent": false, "purchase_remark": "为下级代理【二级代理 B】购买" } ``` ### H5 端订单创建 API(无变更) **端点**:`POST /api/h5/orders` 行为完全不变,仍支持 `wallet`/`wechat`/`alipay`,仍创建待支付订单。 ### 订单列表 API **端点**:`GET /api/admin/orders` **新增查询参数**: - `purchase_role` (可选):订单角色筛选 - `self_purchase`:自己购买 - `purchased_by_parent`:上级代理购买 - `purchased_by_platform`:平台代购 - `purchase_for_subordinate`:给下级购买 **查询逻辑变更**: - 代理可以看到 `buyer_id = 自己` 或 `operator_id = 自己` 的所有订单 --- ## 数据库变更 ### 订单表(tb_order) **新增字段**: ```sql 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)'; ``` **新增索引**: ```sql CREATE INDEX CONCURRENTLY idx_orders_operator_id ON tb_order(operator_id); CREATE INDEX CONCURRENTLY idx_orders_purchase_role ON tb_order(purchase_role); ``` --- ### 钱包流水表(tb_agent_wallet_transaction) **新增字段**(如果不存在): ```sql ALTER TABLE tb_agent_wallet_transaction ADD COLUMN transaction_subtype VARCHAR(50); ALTER TABLE tb_agent_wallet_transaction ADD COLUMN related_shop_id INT; COMMENT ON COLUMN tb_agent_wallet_transaction.transaction_subtype IS '交易子类型(细分 order_payment 场景)'; COMMENT ON COLUMN tb_agent_wallet_transaction.related_shop_id IS '关联店铺ID(代购时记录下级店铺)'; ``` --- ## 代码结构 ### Service 层新增方法 **`internal/service/order/service.go`**: 1. **`getCostPrice(ctx, shopID, packageID) (int64, error)`** - 查询店铺对套餐的成本价(通过 ShopPackageAllocation) 2. **`createWalletTransaction(ctx, tx, walletID, orderID, amount, purchaseRole, relatedShopID) error`** - 创建钱包流水,根据 purchaseRole 填充 subtype 和 remark 3. **`createOrderWithWalletPayment(ctx, order, items, operatorShopID, buyerShopID) (*dto.OrderResponse, error)`** - 钱包支付订单创建方法,事务内完成:订单创建 + 扣款 + 流水 + 激活套餐 **`Create()` 方法重构**: ```go // 场景判断 if req.PaymentMethod == "offline": // 平台代购场景(保持现有逻辑) return s.createOrderWithActivation(...) else if req.PaymentMethod == "wallet": // 获取资源所属店铺 ID if 资源属于操作者: // 代理自购场景 buyer = operator purchase_role = "self_purchase" total_amount = actual_paid_amount = 操作者成本价 else: // 代理代购场景 buyer = 资源所属者 operator = 操作者 purchase_role = "purchase_for_subordinate" total_amount = 买家成本价 actual_paid_amount = 操作者成本价 return s.createOrderWithWalletPayment(...) ``` --- ### Store 层变更 **`internal/store/postgres/order_store.go`**: **`List()` 方法**: ```go // 代理用户:查询作为买家或操作者的订单 if shopID, ok := filters["shop_id"].(uint); ok { query = query.Where( "(buyer_type = ? AND buyer_id = ?) OR operator_id = ?", model.BuyerTypeAgent, shopID, shopID, ) } // 支持 purchase_role 精确匹配筛选 if purchaseRole, ok := filters["purchase_role"].(string); ok { query = query.Where("purchase_role = ?", purchaseRole) } ``` --- ### Handler 层变更 **`internal/handler/admin/order.go`**: **`Create()` 方法**: - 修改 wallet 支付方式的权限检查,允许代理、平台、超管使用 - offline 支付方式仍限制为平台和超管 **`List()` 方法**: - 从查询参数解析 `purchase_role` - 传递给 Service 层的 `List()` 方法 --- ## 使用指南 ### 代理自购场景 **请求**: ```http POST /api/admin/orders Authorization: Bearer {agent_token} Content-Type: application/json { "order_type": 1, "iot_card_id": 101, "package_ids": [201], "payment_method": "wallet" } ``` **响应**: ```json { "code": 0, "data": { "id": 1001, "order_no": "ORD202602281234567890", "payment_status": 2, "operator_id": 10, "buyer_id": 10, "operator_type": "agent", "purchase_role": "self_purchase", "total_amount": 8000, "actual_paid_amount": 8000 }, "msg": "订单创建成功" } ``` --- ### 代理代购场景 **请求**: ```http POST /api/admin/orders Authorization: Bearer {parent_agent_token} Content-Type: application/json { "order_type": 1, "iot_card_id": 201, "package_ids": [301], "payment_method": "wallet" } ``` **响应**: ```json { "code": 0, "data": { "id": 1002, "order_no": "ORD202602281234567891", "payment_status": 2, "operator_id": 10, "buyer_id": 20, "operator_type": "agent", "operator_name": "一级代理 A", "purchase_role": "purchase_for_subordinate", "total_amount": 10000, "actual_paid_amount": 8000, "purchase_remark": "为下级代理【二级代理 B】购买" }, "msg": "订单创建成功" } ``` --- ### 订单列表查询 **请求**: ```http GET /api/admin/orders?purchase_role=purchase_for_subordinate&page=1&page_size=20 Authorization: Bearer {agent_token} ``` **响应**: ```json { "code": 0, "data": { "list": [ { "id": 1002, "purchase_role": "purchase_for_subordinate", "operator_id": 10, "buyer_id": 20, "total_amount": 10000, "actual_paid_amount": 8000 } ], "total": 1 }, "msg": "success" } ``` --- ## 迁移和部署 ### 数据库迁移 **迁移脚本**: - `migrations/000067_add_operator_fields_to_orders.up.sql` - `migrations/000068_add_transaction_subtype_to_wallet_transaction.up.sql` **回滚脚本**: - `migrations/000067_add_operator_fields_to_orders.down.sql` - `migrations/000068_add_transaction_subtype_to_wallet_transaction.down.sql` **数据回填**(可选): - `migrations/backfill_order_purchase_role.sql`:回填历史平台代购订单 --- ### 部署步骤 1. **测试环境验证**: - 执行迁移脚本 - 验证索引创建成功 - 手工测试三种代购场景 2. **灰度发布**: - 代码部署到灰度环境 - 观察日志和监控指标 - 验证订单创建、查询、钱包扣款功能 3. **生产环境部署**: - 低峰期执行数据库迁移 - 部署代码 - 监控错误日志和业务指标 - 验证核心功能 --- ### 监控指标 **关键指标**: - 订单创建成功率(按 payment_method 分组) - 钱包扣款成功率 - 错误日志:余额不足、并发冲突、套餐激活失败 - 订单创建耗时(P95、P99) **告警规则**: - 钱包扣款失败率 > 5% - 订单创建失败率 > 10% - 并发冲突次数 > 100/分钟 --- ## 兼容性说明 ### 向后兼容 - **现有订单字段为空值**:不影响已有订单查询 - **平台代购(offline)逻辑不变**:保持现有行为 - **H5 钱包支付不受影响**:H5 端仍使用两步流程 - **数据权限保持一致**:订单角色追踪不影响现有数据权限逻辑 ### 破坏性变更 **无**。所有新增字段均为 nullable,新增逻辑不影响现有流程。 --- ## 测试覆盖 ### 集成测试场景 1. **代理自购**:代理为自己的卡购买套餐,验证扣款、激活、流水 2. **代理代购**:一级代理为二级代理购买,验证价格差异、佣金不产生 3. **平台代购**:平台 offline 代购,验证不扣款、佣金产生 4. **订单查询**:验证 OR 查询逻辑、purchase_role 筛选 5. **边界场景**:余额不足、并发扣款、幂等性 ### 验证结果 - ✅ 编译通过:`go build ./...` - ✅ OpenAPI 文档更新:新增字段已包含 - ✅ 迁移脚本执行成功 --- ## 相关文档 - [提案文档](../../openspec/changes/fix-agent-wallet-order-creation/proposal.md) - [设计文档](../../openspec/changes/fix-agent-wallet-order-creation/design.md) - [任务清单](../../openspec/changes/fix-agent-wallet-order-creation/tasks.md) - [Specs 规范](../../openspec/changes/fix-agent-wallet-order-creation/specs/) - [项目规范](../../CLAUDE.md)