fix: 资产钱包自动创建机制 — 修复C端购买时钱包不存在报错
All checks were successful
构建并部署到测试环境(无 SSH) / build-and-deploy (push) Successful in 7m8s
All checks were successful
构建并部署到测试环境(无 SSH) / build-and-deploy (push) Successful in 7m8s
- client_order: 新增 getOrCreateWallet 兜底,钱包不存在时自动创建 - device_import: 设备导入事务内同步创建设备钱包 - iot_card_import: IoT卡批量导入后批量创建卡钱包 - queue/handler: 传递 AssetWalletStore 给两个导入 handler - migration 000098: 为存量IoT卡和设备补建资产钱包
This commit is contained in:
@@ -0,0 +1,2 @@
|
||||
schema: spec-driven
|
||||
created: 2026-03-28
|
||||
@@ -0,0 +1,65 @@
|
||||
## Context
|
||||
|
||||
修正业务方案 v5 中 5 项业务逻辑补全,涉及佣金计算、订单查询、支付对接、订单创建、资产状态管理共 5 个模块。各项之间无强依赖,但 J-2(平台钱包代购)与 J-1(富友支付)涉及同一个文件 `order/service.go`,建议 J-2 先做避免合并冲突。
|
||||
|
||||
## Goals / Non-Goals
|
||||
|
||||
**Goals:**
|
||||
- 佣金链断裂可被平台发现(F-1)
|
||||
- C端订单查询符合 Handler → Service 分层(F-4)
|
||||
- 富友支付 JSAPI/MiniApp 可正常预下单(J-1)
|
||||
- 平台可代用代理钱包购包(J-2)
|
||||
- 卡/设备状态反映真实使用情况(J-3)
|
||||
|
||||
**Non-Goals:**
|
||||
- 不修改佣金计算核心算法
|
||||
- 不新增自动化测试
|
||||
- 不改动回调链路(已存在且正常工作)
|
||||
- 不新建 DB 表(仅插入配置数据)
|
||||
|
||||
## Decisions
|
||||
|
||||
### 1. F-1 零额待审记录的触发点
|
||||
|
||||
**决策**:在 `calculateChainOneTimeCommission` 和类似链式计算函数中,当 `getCostPrice` 返回零/无配置时,`break` 之前创建记录。
|
||||
|
||||
**原因**:这是佣金链唯一的断裂点——上游代理缺少套餐系列授权。在此处插入最精准。
|
||||
|
||||
记录写入失败不阻断主流程(`_ = store.Create()`),仅记 Error 日志。
|
||||
|
||||
### 2. F-4 迁移范围
|
||||
|
||||
**决策**:仅迁移 `ListOrders` 和 `GetOrderDetail` 两个查询方法到 Service 层。不重构其他正常工作的 Handler 方法。
|
||||
|
||||
**原因**:最小改动原则。其他 Handler 方法如果没有绕层问题则不动。
|
||||
|
||||
迁移后 Handler 中删除 `h.db` 字段引用和 `SkipPermissionCtx` 用法。
|
||||
|
||||
### 3. J-1 富友客户端构造方式
|
||||
|
||||
**决策**:每次调用时从 `wechatConfigStore.GetActive()` 读配置并构造 `fuiou.Client`,不做全局单例缓存。
|
||||
|
||||
**原因**:配置可以在管理端切换(activate/deactivate),缓存可能导致读到旧配置。构造成本很低(纯内存操作),无需优化。
|
||||
|
||||
**替代方案**:全局单例 + 配置变更时刷新 → 增加复杂度,收益不大,拒绝。
|
||||
|
||||
### 4. J-2 平台代购的身份伪装
|
||||
|
||||
**决策**:平台代购时,`orderBuyerType` 设为 `agent`,`orderBuyerID` 设为 `*resourceShopID`。成本价和钱包都使用资产所属代理的。
|
||||
|
||||
**原因**:从订单归属和佣金计算角度,这笔订单等效于"该代理用自己钱包买的",只是操作人是平台。
|
||||
|
||||
### 5. J-3 状态更新时机
|
||||
|
||||
**决策**:
|
||||
- `status → 3`:在 `PackageActivationHandler` / `ActivateByRealname` 激活套餐成功后,检查 `card.Status < 3` 则更新为 3
|
||||
- `status → 4`:在套餐到期处理逻辑中(`OrderExpireHandler` 或类似),当卡的最后一个 active 套餐过期时更新为 4
|
||||
|
||||
**原因**:与套餐生命周期绑定最自然——有活跃套餐就是"已激活",无活跃套餐就是"已停用"。
|
||||
|
||||
## Risks / Trade-offs
|
||||
|
||||
- **[F-1 零额记录堆积]** 如果某代理长期缺配置,会产生大量 status=99 记录 → 平台应定期处理待审记录(这本身就是 F-1 的目的)
|
||||
- **[F-4 SkipPermissionCtx 移除]** 迁移后 Service 层查询会经过数据权限过滤 → 需确保 C端用户的数据权限配置正确,否则可能查不到数据
|
||||
- **[J-1 凭证安全]** 富友私钥存在 DB → 当前 `wechat_config` 表已有类似敏感数据(微信支付密钥),风险等级一致
|
||||
- **[J-3 并发更新]** 多个套餐同时激活可能并发更新 status → 使用条件更新 `WHERE status < 3` 避免覆盖
|
||||
@@ -0,0 +1,33 @@
|
||||
## Why
|
||||
|
||||
修正业务方案审查后,确认 5 项业务逻辑补全仍未完成:佣金链断裂静默跳过、C端订单 Handler 绕过 Service 层直接查 DB、富友支付留桩未实现、平台无法代用代理钱包购包、卡/设备激活后状态字段未更新。这些问题直接影响业务正确性:佣金链断裂导致平台无法发现缺配置的代理、Handler 绕层违反架构约束、富友支付无法上线、平台运营受限、资产状态不反映真实使用情况。
|
||||
|
||||
## What Changes
|
||||
|
||||
- **F-1 佣金链断裂改为零额待审记录**:`commission_calculation/service.go` 中佣金链断裂(代理缺少套餐系列授权)时,不再静默 `break`,改为创建 `amount=0, status=99(待人工修正)` 的佣金记录并记录告警日志,让平台管理员能发现并处理。新增常量 `CommissionStatusPendingReview = 99`。
|
||||
- **F-4 C端订单 Handler 迁移到 Service 层**:`internal/handler/app/client_order.go` 中直接 `h.db.WithContext(resolved.SkipPermissionCtx)` 查询数据库的代码,迁移到 `internal/service/client_order/service.go` 新增 `ListOrders` / `GetOrderDetail` 方法,Handler 改为调用 Service。
|
||||
- **J-1 富友支付实现**:`order/service.go` 中 `FuiouPayJSAPI` 和 `FuiouPayMiniApp` 从留桩(返回"暂未实现")改为真实调用 `pkg/fuiou/` SDK 完成预下单。回调链路已存在,无需修改。需在 DB 插入富友支付凭证配置。
|
||||
- **J-2 平台钱包代购**:`order/service.go:CreateLegacy` 的钱包支付分支,新增支持平台操作人用某代理钱包给该代理名下资产购包的场景(`buyerType="" && resourceShopID != nil`)。
|
||||
- **J-3 卡/设备 status 3/4 业务触发**:套餐激活成功后自动将 `iot_card.status` / `device.status` 更新为 3(已激活);最后一个 active 套餐到期后更新为 4(已停用)。
|
||||
|
||||
## Capabilities
|
||||
|
||||
### New Capabilities
|
||||
|
||||
- `commission-pending-review`: 佣金链断裂时创建零额待审记录,支持按 status=99 筛选
|
||||
|
||||
### Modified Capabilities
|
||||
|
||||
- `client-order-purchase`: C端订单查询迁移到 Service 层,删除 Handler 直接 DB 访问
|
||||
- `fuiou-payment`: 从留桩改为真实实现 JSAPI 和 MiniApp 预下单
|
||||
- `admin-order-creation`: 平台钱包代购场景支持
|
||||
- `asset-lifecycle-status`: 卡/设备 status 字段 3(已激活)和 4(已停用)的自动触发
|
||||
|
||||
## Impact
|
||||
|
||||
- **新增文件**:无(`commission-pending-review` 的逻辑内嵌到现有 `commission_calculation/service.go`)
|
||||
- **修改文件**:`internal/service/commission_calculation/service.go`、`internal/handler/app/client_order.go`、`internal/service/client_order/service.go`、`internal/service/order/service.go`、`internal/service/package/activation_service.go`(或套餐激活相关文件)
|
||||
- **新增常量**:`CommissionStatusPendingReview = 99`
|
||||
- **DB 操作**:需插入富友支付凭证到 `tb_wechat_config`(J-1)
|
||||
- **无 DB 迁移**(schema 不变)
|
||||
- **前端影响**:佣金记录列表需支持 `status=99` 筛选(F-1)
|
||||
@@ -0,0 +1,12 @@
|
||||
## MODIFIED Requirements
|
||||
|
||||
### Requirement: 平台钱包代购支持
|
||||
后台创建订单时,平台操作人 SHALL 可以使用某代理的钱包,以该代理的成本价,给该代理名下的资产购买套餐。条件:`buyerType == ""(平台)`且 `resourceShopID != nil`(资产归属代理)。
|
||||
|
||||
#### Scenario: 平台代用代理钱包购包
|
||||
- **WHEN** 平台操作人提交订单,`payment_method=wallet`,资产归属代理 A
|
||||
- **THEN** 系统 SHALL 以代理 A 的成本价作为订单金额,从代理 A 的钱包扣款,订单 `buyer_type` 记录为 `agent`,`buyer_id` 为代理 A 的 shop_id
|
||||
|
||||
#### Scenario: 无归属代理时拒绝
|
||||
- **WHEN** 平台操作人提交钱包支付订单,但资产无归属代理(`resourceShopID == nil`)
|
||||
- **THEN** 系统 SHALL 返回 `CodeInvalidParam` 错误
|
||||
@@ -0,0 +1,27 @@
|
||||
## MODIFIED Requirements
|
||||
|
||||
### Requirement: 激活后状态更新为已激活
|
||||
套餐激活成功后,系统 SHALL 将对应 IoT 卡或设备的 `status` 更新为 3(已激活),前提是当前 `status < 3`。
|
||||
|
||||
#### Scenario: 首次套餐激活触发状态变更
|
||||
- **WHEN** 某卡的套餐首次激活成功,且 `iot_card.status < 3`
|
||||
- **THEN** 系统 SHALL 更新 `iot_card.status = 3`
|
||||
|
||||
#### Scenario: 设备套餐激活触发状态变更
|
||||
- **WHEN** 某设备的套餐首次激活成功,且 `device.status < 3`
|
||||
- **THEN** 系统 SHALL 更新 `device.status = 3`
|
||||
|
||||
#### Scenario: 已激活状态不被覆盖
|
||||
- **WHEN** 套餐激活成功,但 `status` 已经 >= 3
|
||||
- **THEN** 系统 SHALL NOT 修改 `status` 字段
|
||||
|
||||
### Requirement: 最后套餐过期后状态更新为已停用
|
||||
当某卡/设备的最后一个 active 套餐过期后,系统 SHALL 将 `status` 更新为 4(已停用)。
|
||||
|
||||
#### Scenario: 最后套餐过期
|
||||
- **WHEN** 套餐到期处理后,该卡/设备名下无任何 `status=active` 的 `package_usage` 记录
|
||||
- **THEN** 系统 SHALL 更新 `iot_card.status = 4` 或 `device.status = 4`
|
||||
|
||||
#### Scenario: 还有其他活跃套餐
|
||||
- **WHEN** 某套餐过期,但该卡/设备名下仍有其他 `status=active` 的套餐
|
||||
- **THEN** 系统 SHALL NOT 修改 `status` 字段
|
||||
@@ -0,0 +1,12 @@
|
||||
## MODIFIED Requirements
|
||||
|
||||
### Requirement: C端订单查询通过 Service 层
|
||||
C端订单列表和订单详情查询 SHALL 通过 `client_order/service.go` 的 Service 方法执行,Handler 层 SHALL NOT 直接访问数据库或使用 `SkipPermissionCtx`。
|
||||
|
||||
#### Scenario: C端查询订单列表
|
||||
- **WHEN** C端客户请求订单列表
|
||||
- **THEN** Handler SHALL 调用 `service.ListOrders(ctx, req)`,Service 内部执行数据库查询并应用数据权限过滤
|
||||
|
||||
#### Scenario: C端查询订单详情
|
||||
- **WHEN** C端客户请求订单详情
|
||||
- **THEN** Handler SHALL 调用 `service.GetOrderDetail(ctx, orderID)`,Service 内部验证权限并返回数据
|
||||
@@ -0,0 +1,15 @@
|
||||
## ADDED Requirements
|
||||
|
||||
### Requirement: 佣金链断裂创建待审记录
|
||||
当佣金链式计算过程中,某代理缺少套餐系列授权导致链路断裂时,系统 SHALL 创建一条 `amount=0, status=99` 的佣金记录,而非静默跳过。该记录的 `remark` SHALL 包含断裂原因(套餐系列 ID + 代理 ID)。
|
||||
|
||||
#### Scenario: 代理缺少套餐系列授权
|
||||
- **WHEN** 佣金链计算到某代理,该代理未被分配对应套餐系列的成本价
|
||||
- **THEN** 系统 SHALL 创建 `CommissionRecord{amount: 0, status: CommissionStatusPendingReview(99), remark: "套餐系列[{id}]未分配给该代理,请人工核查"}`,记录 Warn 日志,然后 break 链路
|
||||
|
||||
### Requirement: 待审佣金记录可筛选
|
||||
佣金记录列表接口 SHALL 支持按 `status=99` 筛选待人工修正的记录。
|
||||
|
||||
#### Scenario: 平台管理员筛选待审记录
|
||||
- **WHEN** 管理员查询佣金记录列表且传入 `status=99`
|
||||
- **THEN** 返回所有待人工修正的佣金记录
|
||||
@@ -0,0 +1,19 @@
|
||||
## MODIFIED Requirements
|
||||
|
||||
### Requirement: 富友 JSAPI 预下单
|
||||
`FuiouPayJSAPI` SHALL 使用 `pkg/fuiou/` SDK 完成真实预下单,返回前端调起微信支付所需的参数(appId, timeStamp, nonceStr, package, signType, paySign)。SHALL NOT 再返回"暂未实现"错误。
|
||||
|
||||
#### Scenario: 公众号内富友支付
|
||||
- **WHEN** C端用户在微信公众号内发起订单支付,支付通道为富友
|
||||
- **THEN** 系统 SHALL 从 `tb_wechat_config` 读取激活的富友配置,调用 `fuiou.Client.WxPreCreate` 完成预下单,返回 JS-SDK 调起参数
|
||||
|
||||
#### Scenario: 富友配置未激活
|
||||
- **WHEN** 发起富友支付但 `tb_wechat_config` 中无 `provider_type='fuiou' AND is_active=true` 的配置
|
||||
- **THEN** 系统 SHALL 返回 `CodeFuiouPayFailed` 错误
|
||||
|
||||
### Requirement: 富友 MiniApp 预下单
|
||||
`FuiouPayMiniApp` SHALL 使用与 JSAPI 相同的流程,但 `tradeType` 传 `LETPAY`,`subAppid` 使用小程序 AppID。
|
||||
|
||||
#### Scenario: 小程序内富友支付
|
||||
- **WHEN** C端用户在微信小程序内发起订单支付,支付通道为富友
|
||||
- **THEN** 系统 SHALL 使用小程序 AppID 和 `LETPAY` 类型完成预下单
|
||||
@@ -0,0 +1,46 @@
|
||||
# 任务清单:fix-business-completion
|
||||
|
||||
> 5 项业务逻辑补全。F-1、F-4、J-3 各自独立;J-1 和 J-2 涉及同一文件,建议 J-2 先做。
|
||||
|
||||
## 任务组 1:F-1 佣金链断裂改为零额待审记录
|
||||
|
||||
- [x] 1.1 在 `pkg/constants/` 中新增常量 `CommissionStatusPendingReview = 99`,注释"待人工修正(链路断裂,需平台处理)"
|
||||
- [x] 1.2 在 `internal/service/commission_calculation/service.go` 中,找到佣金链断裂处(`getCostPrice` 返回无配置后 `break` 的位置),在 `break` 前创建零额待审记录:`CommissionRecord{OrderID, ShopID, SeriesID, Amount: 0, Status: CommissionStatusPendingReview, Remark: "套餐系列[%d]未分配给该代理,请人工核查"}`
|
||||
- [x] 1.3 记录失败不阻断主流程(`_ = store.Create()`),同时记录 Warn 日志
|
||||
- [x] 1.4 确认佣金记录列表查询(`commission_record` 相关 Store/Service)已支持按 status 筛选(如已有 status 过滤则无需改动)
|
||||
- [x] 1.5 验证:`go build ./...` 编译通过
|
||||
|
||||
## 任务组 2:F-4 C端订单 Handler 迁移 Service 层
|
||||
|
||||
- [x] 2.1 在 `internal/service/client_order/service.go` 中新增 `ListOrders(ctx context.Context, req *dto.ClientOrderListRequest) (*dto.ClientOrderListResponse, error)` 方法,将 Handler 中的 DB 查询逻辑迁移过来,使用正常数据权限(不用 SkipPermissionCtx)
|
||||
- [x] 2.2 在 `internal/service/client_order/service.go` 中新增 `GetOrderDetail(ctx context.Context, orderID uint) (*dto.ClientOrderDetailResponse, error)` 方法
|
||||
- [x] 2.3 修改 `internal/handler/app/client_order.go`,将直接 DB 查询替换为 Service 方法调用,删除 `h.db` 字段引用和 `SkipPermissionCtx` 用法
|
||||
- [x] 2.4 如果 Handler struct 中 `db` 字段因此变为未使用,从 struct 和构造函数中移除
|
||||
- [x] 2.5 验证:`rg "SkipPermissionCtx" internal/handler/app/client_order.go` 无残留,`go build ./...` 编译通过
|
||||
|
||||
## 任务组 3:J-2 平台钱包代购
|
||||
|
||||
- [x] 3.1 在 `internal/service/order/service.go:CreateLegacy` 的钱包支付分支中,新增 `buyerType == "" && resourceShopID != nil` 的条件分支
|
||||
- [x] 3.2 该分支内:`orderBuyerType = model.BuyerTypeAgent`,`orderBuyerID = *resourceShopID`,成本价和钱包都使用 `*resourceShopID` 对应的代理
|
||||
- [x] 3.3 无归属代理时(`resourceShopID == nil`)返回 `errors.New(errors.CodeInvalidParam, "不支持的钱包支付场景")`
|
||||
- [x] 3.4 验证:`go build ./...` 编译通过
|
||||
|
||||
## 任务组 4:J-1 富友支付实现
|
||||
|
||||
- [x] 4.1 在 `internal/model/dto/` 中新增(或确认已存在)`FuiouPayJSAPIResponse` DTO:`AppID, TimeStamp, NonceStr, Package, SignType, PaySign`
|
||||
- [x] 4.2 修改 `internal/service/order/service.go:FuiouPayJSAPI`,替换留桩为真实实现:查订单 → 校验状态 → 加载富友配置 → 构造 `fuiou.Client` → 调用 `WxPreCreate(JSAPI)` → 返回 JS-SDK 参数
|
||||
- [x] 4.3 修改 `internal/service/order/service.go:FuiouPayMiniApp`,与 JSAPI 相同,`tradeType` 改为 `LETPAY`,`subAppid` 改为 `config.MiniappAppID`
|
||||
- [ ] 4.4 通过 DBHub 向 `tb_wechat_config` 插入富友支付凭证数据(`provider_type='fuiou'`,凭证见方案文档 J-1 节)
|
||||
- [x] 4.5 验证:`rg "暂未实现" internal/service/order/service.go` 确认富友相关留桩已全部替换,`go build ./...` 编译通过
|
||||
|
||||
## 任务组 5:J-3 卡/设备 status 3/4 触发
|
||||
|
||||
- [x] 5.1 确认 `pkg/constants/iot.go` 中存在 `IotCardStatusActivated = 3` 和 `IotCardStatusDeactivated = 4`(或类似常量),不存在则新增
|
||||
- [x] 5.2 在套餐激活成功后(`activation_service.go` 或 `PackageActivationHandler`),追加条件更新:`UPDATE tb_iot_card SET status = 3 WHERE id = ? AND status < 3`
|
||||
- [x] 5.3 如果是设备套餐,同时更新 `tb_device`:`UPDATE tb_device SET status = 3 WHERE id = ? AND status < 3`
|
||||
- [x] 5.4 在套餐到期处理逻辑中(`OrderExpireHandler` 或类似),套餐过期后检查该卡/设备是否还有其他 active 套餐,无则更新 `status = 4`
|
||||
- [x] 5.5 验证:`go build ./...` 编译通过
|
||||
|
||||
## 收尾验证
|
||||
|
||||
- [x] 6.1 执行 `go build ./...`,确认全量编译通过
|
||||
Reference in New Issue
Block a user