修复一些问题,主要是生效套餐
All checks were successful
构建并部署到测试环境(无 SSH) / build-and-deploy (push) Successful in 8m16s

This commit is contained in:
2026-05-12 10:32:38 +08:00
parent b7369a9c71
commit 95fc0b0a1b
32 changed files with 5984 additions and 553 deletions

View File

@@ -1,49 +1,74 @@
## ADDED Requirements
### Requirement: 退款后套餐失效
退款审批通过后,系统 SHALL 查找该订单关联资产(`iot_card_id``device_id`)的全部套餐使用记录(`status IN (0,1,2)` 待生效/生效中/已用完),批量更新为 `status=4`(已失效)
### Requirement: 退款后套餐精准失效
退款审批通过后,系统 SHALL 仅失效本次退款关联订单生成的套餐使用记录。若退款单传入 `package_usage_id`,系统 SHALL 优先失效该套餐使用记录,且该记录必须属于退款订单;若未传入 `package_usage_id`,系统 SHALL 失效该订单生成的全部可失效套餐使用记录
#### Scenario: 失效单套餐
- **WHEN** 退款订单为单卡购买,关联 iot_card_id
- **THEN** 系统 SHALL 将该卡所有 `status IN (0,1,2)` 的套餐使用记录更新为 `status=4`
#### Scenario: 精准失效单套餐
- **WHEN** 退款单包含 `package_usage_id`
- **THEN** 系统 SHALL 将该套餐使用记录从 `status IN (0,1,2)` 更新为 `status=4`
- **AND** 系统 SHALL 写入 `refund_id``refund_no` 快照
#### Scenario: 失效设备套餐
- **WHEN** 退款订单为设备购买,关联 device_id
- **THEN** 系统 SHALL 将该设备所有 `status IN (0,1,2)` 套餐使用记录更新为 `status=4`
#### Scenario: 未指定套餐时失效订单套餐
- **WHEN** 退款单未包含 `package_usage_id`
- **THEN** 系统 SHALL 将该订单生成的所有 `status IN (0,1,2)` 套餐使用记录更新为 `status=4`
### Requirement: 退款后停机
套餐失效后,系统 SHALL 对资产执行停机操作。
#### Scenario: 主套餐关联加油包失效
- **WHEN** 被失效套餐为主套餐
- **THEN** 系统 SHALL 将其关联的可失效加油包一并更新为 `status=4`
#### Scenario: 单卡停机
- **WHEN** 退款订单为单卡购买
- **THEN** 系统 SHALL 调用 `StopResumeService.ManualStopCard(iccid)` 停机
### Requirement: 退款后待生效套餐接续
退款套餐失效后,系统 SHALL 按购买顺序尝试激活该资产队首待生效主套餐。系统 MUST NOT 跳过未满足实名条件的队首套餐。
#### Scenario: 设备停机(停所有绑定卡)
- **WHEN** 退款订单为设备购买
- **THEN** 系统 SHALL 调用 `DeviceService.StopDevice(deviceID)`,内部遍历所有绑定卡逐一停机
#### Scenario: 立即生效套餐接续
- **WHEN** 退款后资产无生效主套餐,且队首待生效主套餐 `pending_realname_activation=false`
- **THEN** 系统 SHALL 激活该套餐,并按套餐 `expiry_base` 计算生效时间和过期时间
### Requirement: 退款后资产世代重置
停机后,系统 SHALL 对资产执行世代重置(参照换货模块 `exchange/service.go` 的重置逻辑),使资产回到可重新销售状态,一次性佣金可重新触发。
#### Scenario: 实名套餐已满足条件
- **WHEN** 队首待生效主套餐 `pending_realname_activation=true`
- **AND** 单卡已实名,或设备任意已绑定卡已实名
- **THEN** 系统 SHALL 激活该套餐,并清除 `pending_realname_activation`
#### Scenario: 单卡世代重置
- **WHEN** 退款订单为单卡购买
- **THEN** 系统 SHALL 更新 IoT 卡:`generation+1``asset_status=1在库``accumulated_recharge=0``first_commission_paid=false`、清零系列充值和触发标记,清理个人客户绑定(`tb_personal_customer_device`),删除旧资产钱包并创建新空钱包
#### Scenario: 实名套餐未满足条件
- **WHEN** 队首待生效主套餐 `pending_realname_activation=true`
- **AND** 单卡未实名,或设备没有任何已绑定卡实名
- **THEN** 系统 SHALL 保持该套餐待生效
- **AND** 系统 MUST NOT 跳过该套餐激活后续待生效套餐
#### Scenario: 设备世代重置
- **WHEN** 退款订单为设备购买
- **THEN** 系统 SHALL 更新设备:`generation+1``asset_status=1在库``accumulated_recharge=0``first_commission_paid=false`、清零系列充值和触发标记,清理个人客户绑定,删除旧资产钱包并创建新空钱包
### Requirement: 退款后必要时停机
套餐失效和接续激活完成后,系统 SHALL 检查资产是否仍有生效主套餐。仅当资产没有生效主套餐时,系统 SHALL 执行停机操作。
### Requirement: 退款后订单状态更新
资产处理完成后,系统 SHALL 将订单 `payment_status` 更新为 4已退款
#### Scenario: 有接续套餐时不强停
- **WHEN** 退款后成功接续待生效主套餐,或资产仍存在其他生效主套餐
- **THEN** 系统 SHALL NOT 执行退款停机
#### Scenario: 订单标记已退款
- **WHEN** 资产处理全部成功
- **THEN** 订单 `payment_status` SHALL 变为 4已退款退款单 `asset_reset` 标记为 `true`
#### Scenario: 无可用主套餐时停机
- **WHEN** 退款后资产没有任何生效主套餐
- **THEN** 单卡订单 SHALL 调用 `StopResumeService.ManualStopCard(iccid)`
- **AND** 设备订单 SHALL 调用 `DeviceService.StopDevice(deviceID)`
### Requirement: 退款不得重置资产世代和钱包
套餐退款不是资产退回库存。退款资产处理 MUST NOT 执行资产 `generation+1`、MUST NOT 清理个人客户绑定、MUST NOT 删除或重建资产钱包。资产世代重置和钱包重建仅属于换货/转新流程。
#### Scenario: 退款后保留资产钱包
- **WHEN** 套餐退款审批通过
- **THEN** 资产钱包 ID、余额和冻结余额 SHALL 保持不被退款资产处理重建或清零
#### Scenario: 退款后保留资产世代
- **WHEN** 套餐退款审批通过
- **THEN** IoT 卡或设备的 `generation` SHALL 不因退款资产处理变化
### Requirement: 退款后处理标记
退款后资产处理成功完成后,系统 SHALL 将退款单 `asset_reset` 标记为 `true`。该字段为兼容字段,语义为“退款后资产处理是否完成”。
#### Scenario: 资产处理完成
- **WHEN** 套餐失效、接续激活检查和必要停机处理完成
- **THEN** 退款单 `asset_reset` SHALL 标记为 `true`
### Requirement: 资产处理失败容错
资产处理 SHALL 在独立 Goroutine 中执行,失败不影响审批结果。
#### Scenario: 资产处理失败
- **WHEN** 套餐失效/停机/世代重置过程中发生错误
- **THEN** 审批结果 SHALL NOT 受影响(已通过)`asset_reset` 保持 `false`,记录 Error 日志(含 refund_id 和 error
- **AND** 管理员可通过 `asset_reset=false` 筛选需手动处理的退款单
- **WHEN** 套餐失效、接续激活或生效套餐查询过程中发生错误
- **THEN** 审批结果 SHALL NOT 受影响(已通过)
- **AND** `asset_reset` SHALL 保持 `false`
- **AND** 系统 SHALL 记录 Error 日志(含 `refund_id` 和错误信息)