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:
56
openspec/specs/refund-api/spec.md
Normal file
56
openspec/specs/refund-api/spec.md
Normal file
@@ -0,0 +1,56 @@
|
||||
## ADDED Requirements
|
||||
|
||||
### Requirement: 发起退款申请接口
|
||||
`POST /api/admin/refunds` SHALL 创建退款申请。请求体 MUST 包含 `order_id`、`actual_received_amount`、`requested_refund_amount`、`refund_reason`。可选 `package_usage_id`。仅平台用户(user_type IN 1,2)可调用。
|
||||
|
||||
#### Scenario: 成功创建退款申请
|
||||
- **WHEN** 平台管理员提交合法的退款申请(关联已支付套餐订单且无进行中的退款记录)
|
||||
- **THEN** 系统 SHALL 返回创建成功的退款记录,状态为待审批
|
||||
|
||||
#### Scenario: 订单不可退款
|
||||
- **WHEN** 管理员对非已支付订单、充值订单或已有进行中退款记录的订单发起退款
|
||||
- **THEN** 系统 SHALL 返回对应错误
|
||||
|
||||
### Requirement: 退款列表查询接口
|
||||
`GET /api/admin/refunds` SHALL 返回分页退款列表,支持按 `status`、`order_id`、`shop_id` 筛选。自动应用数据权限过滤(通过 `shop_id` 字段)。
|
||||
|
||||
#### Scenario: 按状态筛选退款列表
|
||||
- **WHEN** 管理员查询 `status=1` 的退款列表
|
||||
- **THEN** 系统 SHALL 返回所有待审批的退款记录,支持分页
|
||||
|
||||
### Requirement: 退款详情查询接口
|
||||
`GET /api/admin/refunds/:id` SHALL 返回退款单详情,包含关联订单信息。
|
||||
|
||||
#### Scenario: 查询退款详情
|
||||
- **WHEN** 管理员查询某退款单详情
|
||||
- **THEN** 系统 SHALL 返回退款单完整信息(含 `commission_deducted` 和 `asset_reset` 标记状态)
|
||||
|
||||
### Requirement: 审批通过接口
|
||||
`POST /api/admin/refunds/:id/approve` SHALL 审批通过退款。请求体可选 `approved_refund_amount`(不填则等于 `requested_refund_amount`)和 `remark`。审批通过后异步触发资产处理和佣金全额回扣。
|
||||
|
||||
#### Scenario: 审批通过并触发副作用
|
||||
- **WHEN** 审批人通过退款申请
|
||||
- **THEN** 退款状态 SHALL 变为已通过,记录审批人和审批时间
|
||||
- **AND** 系统 SHALL 异步执行:失效所有套餐 → 停机 → 世代重置资产 → 更新订单状态为已退款 → 标记 `asset_reset=true`
|
||||
- **AND** 系统 SHALL 异步执行:全额回扣该订单所有佣金 → 标记 `commission_deducted=true`
|
||||
|
||||
### Requirement: 审批拒绝接口
|
||||
`POST /api/admin/refunds/:id/reject` SHALL 拒绝退款。请求体 MUST 包含 `reject_reason`。
|
||||
|
||||
#### Scenario: 拒绝退款
|
||||
- **WHEN** 审批人拒绝退款且填写了拒绝原因
|
||||
- **THEN** 退款状态 SHALL 变为已拒绝
|
||||
|
||||
### Requirement: 退回申请接口
|
||||
`POST /api/admin/refunds/:id/return` SHALL 退回退款申请。请求体可选 `remark`。
|
||||
|
||||
#### Scenario: 退回退款申请
|
||||
- **WHEN** 审批人退回退款申请
|
||||
- **THEN** 退款状态 SHALL 变为已退回,申请人可重新提交
|
||||
|
||||
### Requirement: 重新提交接口
|
||||
`POST /api/admin/refunds/:id/resubmit` SHALL 重新提交被退回的退款申请。请求体可修改 `actual_received_amount`、`requested_refund_amount`、`refund_reason`。
|
||||
|
||||
#### Scenario: 重新提交退款申请
|
||||
- **WHEN** 申请人重新提交被退回的退款
|
||||
- **THEN** 退款状态 SHALL 从已退回变回待审批
|
||||
49
openspec/specs/refund-asset-reset/spec.md
Normal file
49
openspec/specs/refund-asset-reset/spec.md
Normal file
@@ -0,0 +1,49 @@
|
||||
## ADDED Requirements
|
||||
|
||||
### Requirement: 退款后套餐失效
|
||||
退款审批通过后,系统 SHALL 查找该订单关联资产(`iot_card_id` 或 `device_id`)的全部套餐使用记录(`status IN (0,1,2)` 待生效/生效中/已用完),批量更新为 `status=4`(已失效)。
|
||||
|
||||
#### Scenario: 失效单卡套餐
|
||||
- **WHEN** 退款订单为单卡购买,关联 iot_card_id
|
||||
- **THEN** 系统 SHALL 将该卡所有 `status IN (0,1,2)` 的套餐使用记录更新为 `status=4`
|
||||
|
||||
#### Scenario: 失效设备套餐
|
||||
- **WHEN** 退款订单为设备购买,关联 device_id
|
||||
- **THEN** 系统 SHALL 将该设备所有 `status IN (0,1,2)` 的套餐使用记录更新为 `status=4`
|
||||
|
||||
### Requirement: 退款后停机
|
||||
套餐失效后,系统 SHALL 对资产执行停机操作。
|
||||
|
||||
#### Scenario: 单卡停机
|
||||
- **WHEN** 退款订单为单卡购买
|
||||
- **THEN** 系统 SHALL 调用 `StopResumeService.ManualStopCard(iccid)` 停机
|
||||
|
||||
#### Scenario: 设备停机(停所有绑定卡)
|
||||
- **WHEN** 退款订单为设备购买
|
||||
- **THEN** 系统 SHALL 调用 `DeviceService.StopDevice(deviceID)`,内部遍历所有绑定卡逐一停机
|
||||
|
||||
### Requirement: 退款后资产世代重置
|
||||
停机后,系统 SHALL 对资产执行世代重置(参照换货模块 `exchange/service.go` 的重置逻辑),使资产回到可重新销售状态,一次性佣金可重新触发。
|
||||
|
||||
#### Scenario: 单卡世代重置
|
||||
- **WHEN** 退款订单为单卡购买
|
||||
- **THEN** 系统 SHALL 更新 IoT 卡:`generation+1`、`asset_status=1(在库)`、`accumulated_recharge=0`、`first_commission_paid=false`、清零系列充值和触发标记,清理个人客户绑定(`tb_personal_customer_device`),删除旧资产钱包并创建新空钱包
|
||||
|
||||
#### Scenario: 设备世代重置
|
||||
- **WHEN** 退款订单为设备购买
|
||||
- **THEN** 系统 SHALL 更新设备:`generation+1`、`asset_status=1(在库)`、`accumulated_recharge=0`、`first_commission_paid=false`、清零系列充值和触发标记,清理个人客户绑定,删除旧资产钱包并创建新空钱包
|
||||
|
||||
### Requirement: 退款后订单状态更新
|
||||
资产处理完成后,系统 SHALL 将订单 `payment_status` 更新为 4(已退款)。
|
||||
|
||||
#### Scenario: 订单标记已退款
|
||||
- **WHEN** 资产处理全部成功
|
||||
- **THEN** 订单 `payment_status` SHALL 变为 4(已退款),退款单 `asset_reset` 标记为 `true`
|
||||
|
||||
### Requirement: 资产处理失败容错
|
||||
资产处理 SHALL 在独立 Goroutine 中执行,失败不影响审批结果。
|
||||
|
||||
#### Scenario: 资产处理失败
|
||||
- **WHEN** 套餐失效/停机/世代重置过程中发生错误
|
||||
- **THEN** 审批结果 SHALL NOT 受影响(已通过),`asset_reset` 保持 `false`,记录 Error 日志(含 refund_id 和 error)
|
||||
- **AND** 管理员可通过 `asset_reset=false` 筛选需手动处理的退款单
|
||||
26
openspec/specs/refund-commission-deduct/spec.md
Normal file
26
openspec/specs/refund-commission-deduct/spec.md
Normal file
@@ -0,0 +1,26 @@
|
||||
## ADDED Requirements
|
||||
|
||||
### Requirement: 退款佣金全额回扣
|
||||
退款审批通过后,系统 SHALL 查找该订单产生的所有已入账佣金记录(`tb_commission_record WHERE order_id=? AND status=1`,使用 `CommissionRecord` 模型的 `CommissionStatusReleased=1` 常量),全额从各代理佣金钱包(`wallet_type="commission"`)中扣减。扣减允许余额为负。
|
||||
|
||||
#### Scenario: 正常佣金全额回扣
|
||||
- **WHEN** 退款审批通过,该订单有 N 条已入账佣金记录
|
||||
- **THEN** 系统 SHALL 对每条佣金记录全额扣减 `deductAmount = commission.Amount`,从对应代理的佣金钱包扣减(使用 `GetCommissionWallet`,乐观锁更新),创建 `transaction_type="commission_deduct"` 的交易流水,全部完成后标记退款单 `commission_deducted=true`
|
||||
|
||||
#### Scenario: 无佣金记录
|
||||
- **WHEN** 退款审批通过,但该订单无已入账佣金记录
|
||||
- **THEN** 系统 SHALL 直接标记 `commission_deducted=true`,不执行扣减
|
||||
|
||||
#### Scenario: 回扣执行失败
|
||||
- **WHEN** 佣金回扣过程中发生错误
|
||||
- **THEN** 审批结果 SHALL NOT 受影响(已通过),`commission_deducted` 保持 `false`,记录 Error 日志
|
||||
|
||||
### Requirement: 佣金回扣交易流水
|
||||
每次佣金扣减 SHALL 创建交易流水记录:`transaction_type` 为 `commission_deduct`,`reference_type` 为 `refund`,`reference_id` 为退款单 ID,金额为负值(`-commission.Amount`)。
|
||||
|
||||
#### Scenario: 交易流水格式
|
||||
- **WHEN** 佣金回扣成功执行
|
||||
- **THEN** `tb_agent_wallet_transaction` SHALL 新增记录:`shop_id=佣金所属代理, transaction_type=commission_deduct, amount=-commission.Amount, reference_type=refund, reference_id=退款单ID, remark=退款佣金回扣`
|
||||
|
||||
### Requirement: 佣金钱包为负对提现的影响
|
||||
佣金钱包余额为负时,代理 SHALL NOT 能发起新的提现申请。后续新佣金入账会逐步补填负数。退款回扣时不考虑是否有正在审批的提现申请,正常扣减佣金钱包。
|
||||
40
openspec/specs/refund-request/spec.md
Normal file
40
openspec/specs/refund-request/spec.md
Normal file
@@ -0,0 +1,40 @@
|
||||
## ADDED Requirements
|
||||
|
||||
### Requirement: 退款申请数据模型
|
||||
系统 SHALL 提供 `tb_refund_request` 表存储退款申请,包含退款单号(唯一)、关联订单 ID、店铺 ID(数据权限过滤)、实收金额、申请退款金额、审批退款金额、状态、审批人、审批时间、佣金回扣标记、资产重置标记等字段。
|
||||
|
||||
#### Scenario: 创建退款申请
|
||||
- **WHEN** 平台管理员提交退款申请,填写订单 ID、实收金额、申请退款金额、退款原因
|
||||
- **THEN** 系统 SHALL 生成唯一退款单号(格式 RF+日期时间+随机数),创建 `status=1(待审批)` 的退款记录,`shop_id` 从关联订单自动读取
|
||||
|
||||
#### Scenario: 仅针对已支付套餐订单
|
||||
- **WHEN** 管理员对非已支付订单或充值订单发起退款
|
||||
- **THEN** 系统 SHALL 返回错误,拒绝创建
|
||||
|
||||
#### Scenario: 重复退款防护
|
||||
- **WHEN** 管理员对已有待审批/已通过/已退回退款记录的订单再次发起退款
|
||||
- **THEN** 系统 SHALL 返回错误,拒绝创建
|
||||
- **WHEN** 管理员对仅有已拒绝退款记录的订单重新发起退款
|
||||
- **THEN** 系统 SHALL 允许创建新的退款申请
|
||||
|
||||
### Requirement: 退款状态流转
|
||||
退款单状态 SHALL 按以下规则流转:待审批(1) → 已通过(2)/已拒绝(3)/已退回(4);已退回(4) → 待审批(1)。已通过和已拒绝为终态。
|
||||
|
||||
#### Scenario: 审批通过
|
||||
- **WHEN** 审批人对待审批退款单执行通过操作
|
||||
- **THEN** 状态 SHALL 变为 2(已通过),记录审批人和审批时间,异步触发资产处理和佣金回扣
|
||||
|
||||
#### Scenario: 审批拒绝
|
||||
- **WHEN** 审批人对待审批退款单执行拒绝操作,填写拒绝原因
|
||||
- **THEN** 状态 SHALL 变为 3(已拒绝),记录拒绝原因
|
||||
|
||||
#### Scenario: 退回重提
|
||||
- **WHEN** 审批人退回退款申请,申请人修改后重新提交
|
||||
- **THEN** 状态 SHALL 从 4(已退回)变回 1(待审批)
|
||||
|
||||
#### Scenario: 非法状态变更被拒绝
|
||||
- **WHEN** 对已通过或已拒绝的退款单执行任何状态变更
|
||||
- **THEN** 系统 SHALL 返回错误,状态不变
|
||||
|
||||
### Requirement: 仅平台账号可操作
|
||||
退款的发起和审批 SHALL 限制为平台用户(user_type IN 1,2)。同一人可以既发起又审批。
|
||||
Reference in New Issue
Block a user