提案
All checks were successful
构建并部署到测试环境(无 SSH) / build-and-deploy (push) Successful in 56s

This commit is contained in:
Break
2026-06-04 09:39:20 +08:00
parent 5089a71764
commit 27fba9160c
14 changed files with 1341 additions and 0 deletions

View File

@@ -0,0 +1,73 @@
## MODIFIED Requirements
### Requirement: 资产表新增代际字段
系统 MUST 在资产主表新增 `generation int NOT NULL DEFAULT 1` 字段,覆盖 `IotCard``Device`
#### Scenario: 新资产默认代际为 1
- **WHEN** 创建新的 IoT 卡或设备
- **THEN** 系统 MUST 将 `generation` 初始化为 `1`
---
### Requirement: 关联业务表新增代际字段
系统 MUST 在以下关联业务表新增 `generation int NOT NULL DEFAULT 1` 字段:`Order``PackageUsage``AssetRechargeRecord`
#### Scenario: 新关联记录默认代际为 1
- **WHEN** 创建订单、套餐使用记录或资产充值记录
- **THEN** 系统 MUST 将记录的 `generation` 默认为 `1`
---
### Requirement: 写时快照代际规则
系统 MUST 在创建关联记录时执行代际写时快照从当前资产IoT 卡/设备)的 `generation` 复制到新建的 `Order``PackageUsage``AssetRechargeRecord` 记录。
#### Scenario: 创建订单时复制资产代际
- **WHEN** 某资产当前 `generation=3`,并基于该资产创建订单
- **THEN** 该订单记录的 `generation` MUST 写入为 `3`
---
### Requirement: 查询过滤规则
系统 MUST 支持客户端按 `generation` 过滤历史数据;后台管理侧 MUST 不默认按 `generation` 过滤。
#### Scenario: 客户端按代际查看历史
- **WHEN** 客户端请求携带指定 `generation`
- **THEN** 系统 MUST 仅返回该代际的数据(在后续提案中实现)
#### Scenario: 后台查询不按代际裁剪
- **WHEN** 管理端查询订单或充值记录且未显式指定 `generation`
- **THEN** 系统 MUST 返回全部代际数据
---
### Requirement: 钱包流水不引入代际字段
系统 MUST NOT 在钱包流水相关表新增 `generation` 字段,因为钱包流水已通过 `wallet_id` 天然隔离。
#### Scenario: 钱包流水按钱包隔离
- **WHEN** 查询某资产钱包流水
- **THEN** 系统 MUST 仅依赖 `wallet_id` 完成数据隔离,不新增 `generation` 参与过滤
---
### Requirement: 换货与转新的代际边界
系统 SHALL 将换货链与代际链视为两条不同的业务语义链。
系统 MUST 满足:
- 换货完成时,旧资产 `generation` MUST NOT 变化
- 换货完成时,新资产 `generation` MUST NOT 变化
- 仅在 `renew` 时,旧资产 `generation` 才允许递增
- 新资产来源追溯 MUST 通过 `ExchangeOrder.old_* / new_*` 表达,而不是通过 `generation` 推导
#### Scenario: 换货完成不改变旧资产世代
- **WHEN** 后台完成一次换货
- **THEN** 旧资产 `generation` MUST 保持原值不变
#### Scenario: 转新时世代递增
- **WHEN** 后台执行 `renew`
- **THEN** 系统 MUST 将旧资产 `generation + 1`

View File

@@ -0,0 +1,81 @@
## MODIFIED Requirements
### Requirement: 查询资产跨代历史订单(含前代)
`include_previous=true` 时,系统 SHALL 通过换货链追溯前代资产,返回前代订单,并在响应中区分世代来源。
**追溯逻辑**
1. 通过 `ExchangeOrder.new_asset_id` 逆向查找当前资产的换货来源
2. 得到前代的 `old_asset_identifier`,查询该标识符的订单
3. 递归追溯,最多向前 10 代(安全上限)
4. 追溯范围 MUST 同时覆盖 `flow_type=shipping``flow_type=direct` 的已完成换货单
5. 追溯 MUST 仅使用 `status=4` 的已完成换货单,禁止把发货中、取消或半成品记录纳入链路
6. 历史记录若 `flow_type` 为空或缺失,系统 MUST 按 `shipping` 兼容处理
**响应结构AssetOrdersResponse**
```json
{
"current_generation": {
"generation": 2,
"identifier": "DEV-001",
"asset_type": "device",
"total": 5,
"page": 1,
"page_size": 20,
"items": [ ...... ]
},
"previous_generations": [
{
"generation": 1,
"identifier": "DEV-OLD-001",
"asset_type": "device",
"exchange_no": "EXC20260101XXXXXX",
"exchanged_at": "2026-01-01T00:00:00Z",
"total": 3,
"items": [ ...20... ]
}
],
"truncated": false
}
```
`previous_generations[].exchanged_at` MUST 优先取换货单 `completed_at`,不得在 `completed_at` 存在时直接将 `updated_at` 视为真实换货完成时间。
若历史单据尚无 `completed_at`,系统 MUST 采用 `updated_at` 兼容回退策略;新完成单据 MUST 优先使用 `completed_at`
#### Scenario: 查询 direct 换货后的全代际订单
- **WHEN** 管理员请求 `GET /api/admin/assets/DEV-NEW-001/orders?include_previous=true`,且当前资产来自一次 `flow_type=direct` 的已完成换货
- **THEN** `previous_generations` MUST 返回来源旧资产的订单链
- **AND** `exchanged_at` MUST 返回该换货单的 `completed_at`
#### Scenario: 查询 shipping 换货后的全代际订单
- **WHEN** 管理员请求 `GET /api/admin/assets/DEV-001/orders?include_previous=true`DEV-001 来自一次 `flow_type=shipping` 的已完成换货
- **THEN** `current_generation` 包含 DEV-001 本代的订单
- **AND** `previous_generations[0]` 包含来源旧资产的订单,并附带换货单号和真实完成时间
#### Scenario: 未完成换货单不进入追溯链
- **WHEN** 当前资产只存在 `status=3` 的发货待确认换货单来源记录
- **THEN** `include_previous=true` MUST NOT 将该换货单作为前代来源
#### Scenario: 历史完成单据回退完成时间
- **WHEN** 前代来源换货单 `status=4``completed_at` 为空
- **THEN** `previous_generations[].exchanged_at` MUST 使用该换货单 `updated_at` 兼容回退
#### Scenario: 资产本身就是第一代(无前代)
- **WHEN** 管理员请求带 `include_previous=true`,但该资产从未经过换货
- **THEN** `previous_generations` 为空数组 `[]`
- **AND** `current_generation` 正常返回本代订单
#### Scenario: 换货链超过追溯上限
- **WHEN** 换货链深度超过 10 代
- **THEN** 追溯在第 10 代截断,`truncated=true`
- **AND** 已追溯到的前代数据正常返回
#### Scenario: 前代订单分页
- **WHEN** 请求带 `include_previous=true`
- **THEN** 分页参数page/page_size只对 `current_generation` 的订单生效
- **AND** 前代订单每代最多返回 20 条(不支持前代内分页)
#### Scenario: 无 include_previous 时响应不含前代字段
- **WHEN** 管理员请求不带 `include_previous=true`(或传 false
- **THEN** 响应结构中 `previous_generations` 字段为 null 或不返回,节省带宽

View File

@@ -0,0 +1,68 @@
## MODIFIED Requirements
### Requirement: 资产生命周期状态字段定义
系统 MUST 在 `IotCard``Device` 数据模型中新增 `asset_status int NOT NULL DEFAULT 1` 字段,用于表达资产生命周期状态。
状态值域 MUST 固定为:`1-在库``2-已销售``3-已换货``4-已停用`
#### Scenario: 新建资产默认在库
- **WHEN** 系统创建新的 IoT 卡或设备记录
- **THEN** `asset_status` MUST 默认为 `1`(在库)
#### Scenario: 非法状态值被拒绝
- **WHEN** 写入 `asset_status``0``5` 或其他非约定值
- **THEN** 系统 MUST 拒绝该写入并提示状态值不合法
---
### Requirement: 资产生命周期状态常量定义
系统 MUST 在 `pkg/constants/` 中定义资产生命周期状态常量,并统一由业务层引用,禁止在业务代码中硬编码状态值。
#### Scenario: 业务代码引用常量
- **WHEN** Service 层执行资产状态判断或赋值
- **THEN** 代码 MUST 使用 `pkg/constants/` 中定义的资产状态常量而不是硬编码数字
---
### Requirement: 资产状态与网络状态独立
系统 MUST 保证 `asset_status` 与运营商侧 `network_status` 完全独立,二者不互相推导、不互相覆盖。
状态流转逻辑 MUST 至少包括:
- 导入/建档后:`asset_status=1`(在库)
- 首次绑定/交付客户后:`asset_status=2`(已销售)
- 换货完成时:
- 旧资产完成前必须为 `asset_status=2`(已销售)
- 新资产完成前必须为 `asset_status=1`(在库)
- 旧资产 `asset_status -> 3`(已换货)
- 新资产 `asset_status -> 2`(已销售)
- 转新时:
- 旧资产 `generation + 1`
- 旧资产 `asset_status -> 1`(在库)
- 手动停用时:`asset_status -> 4`(已停用)
#### Scenario: 网络状态变化不影响资产状态
- **WHEN** Gateway 同步将 `network_status` 从开机改为停机
- **THEN** 系统 MUST 保持 `asset_status` 不变
#### Scenario: 资产状态变化不强制修改网络状态
- **WHEN** 管理端将资产手动停用(`asset_status=4`
- **THEN** 系统 MUST 不自动改写 `network_status`
#### Scenario: 换货完成后旧资产标记为已换货
- **WHEN** 任一换货流程完成成功
- **THEN** 系统 MUST 将旧资产 `asset_status` 更新为 `3`
#### Scenario: 换货完成后新资产标记为已销售
- **WHEN** 任一换货流程完成成功
- **THEN** 系统 MUST 将新资产 `asset_status` 更新为 `2`
#### Scenario: 旧资产非已销售禁止完成换货
- **WHEN** 旧资产 `asset_status != 2`
- **THEN** 系统 MUST 拒绝换货完成
#### Scenario: 新资产非在库禁止完成换货
- **WHEN** 新资产 `asset_status != 1`
- **THEN** 系统 MUST 拒绝换货完成

View File

@@ -0,0 +1,52 @@
## ADDED Requirements
### Requirement: 设备换货换出行为
系统 SHALL 在设备作为旧资产完成换货后,将其视为“已被换出、不可继续作为当前客户资产使用”的资产。
系统 MUST 满足:
- 换货完成前旧设备必须 `asset_status=2`(已销售)
- 换货完成时旧设备 `asset_status -> 3`
- 旧设备 `generation` 在换货完成时保持不变
- 若后续执行 `renew`,才允许旧设备重新进入新一代库存
#### Scenario: shipping 完成后旧设备标记为已换货
- **WHEN** 一台设备作为旧资产完成 `shipping` 换货
- **THEN** 系统 MUST 将该设备 `asset_status` 更新为 `3`
#### Scenario: direct 完成后旧设备标记为已换货
- **WHEN** 一台设备作为旧资产完成 `direct` 换货
- **THEN** 系统 MUST 将该设备 `asset_status` 更新为 `3`
---
### Requirement: 设备换货换入行为
系统 SHALL 在设备作为新资产完成换货后,将其视为当前客户正在使用的资产。
系统 MUST 满足:
- 换货完成前新设备必须 `asset_status=1`(在库)
- 换货完成前新设备必须与旧设备 `shop_id` 一致,含二者同为平台库存 `NULL`
- 换货完成前新设备不得存在有效客户绑定或被其他进行中换货单占用
- 换货完成后新设备 `asset_status -> 2`
- 新设备 `generation` 在换货完成时保持不变
- 新设备来源旧设备关系 MUST 通过 `ExchangeOrder` 可追溯
#### Scenario: 新设备完成换货后切为已销售
- **WHEN** 一台设备作为新资产完成换货
- **THEN** 系统 MUST 将该设备 `asset_status` 更新为 `2`
---
### Requirement: 设备转新重置规则
系统 SHALL 在 H7 转新时对设备执行以下重置:
- `generation = generation + 1`
- `asset_status = 1`(在库)
- 清空累计充值与首充触发相关状态(含系列累计/首充字段)
- 清除个人客户绑定关系
- 删除旧钱包并创建新空钱包
#### Scenario: 转新后进入新代际
- **WHEN** 对旧设备执行转新
- **THEN** 系统 MUST 使该设备进入新代际并以在库状态重新销售

View File

@@ -0,0 +1,215 @@
## MODIFIED Requirements
### Requirement: H1 发起换货单
系统 SHALL 提供 `POST /api/admin/exchanges`(需后台认证 `Auth=true`),用于发起换货单。
请求体 MUST 包含:`old_asset_type``old_identifier``exchange_reason`,可选 `flow_type``remark`
`flow_type` 未传或为空时,系统 MUST 按 `shipping` 处理,以兼容旧后台调用;传入非空且不属于 `shipping/direct` 时,系统 MUST 返回参数错误。
`flow_type=shipping` 时:
- 请求体 MUST NOT 要求 `new_identifier`
- 请求体 MUST NOT 要求 `migrate_data`
- 系统创建成功后 SHALL 返回新建换货单信息(含 `id``exchange_no``flow_type=shipping``status=1`
`flow_type=direct` 时:
- 请求体 MUST 额外包含 `new_identifier`
- 请求体 MAY 包含 `migrate_data`;未传时 MUST 按 `false` 处理
- 系统 MUST 在创建接口内完成新资产校验与换货完成事务
- 创建成功后 SHALL 返回新建换货单信息(含 `id``exchange_no``flow_type=direct``status=4``completed_at`
- 任一步失败时 MUST 回滚整个事务,且 MUST NOT 保留半成品 `direct` 换货单
系统 MUST 校验:
- 旧资产存在且当前用户有权限
- 旧资产当前 `asset_status=2`(已销售)
- 同一资产不存在进行中的 `shipping` 换货单(`status IN (1,2,3)`
- `direct` 场景下新资产存在
- `direct` 场景下新资产当前用户有权限
- `direct` 场景下新旧资产类型必须一致(卡换卡/设备换设备)
- `direct` 场景下新资产必须 `asset_status=1`(在库)
- `direct` 场景下新旧资产 `shop_id` 必须一致(含二者同为平台库存 `NULL`
- 若旧资产存在客户绑定,则 `direct` 场景下新资产必须可承接绑定关系
- `direct` 场景下新资产不得存在 `status=1` 的有效客户绑定
- `direct` 场景下新资产不得已被其他 `shipping + status=3` 换货单占用
错误响应 MUST 至少包含:参数错误、资产不存在或无权限、旧资产状态不允许换货、存在进行中换货单、新资产不存在、资产类型不匹配、新资产非在库、新旧资产归属不一致、新资产已被占用、绑定无法承接、钱包冻结余额未处理、迁移失败。
#### Scenario: shipping 正常创建
- **WHEN** 后台以 `flow_type=shipping` 或未传 `flow_type` 发起换货,且旧资产为已销售并无进行中单据
- **THEN** 系统 MUST 创建 `status=1` 的换货单
#### Scenario: direct 创建即完成
- **WHEN** 后台以 `flow_type=direct` 发起换货且新旧资产校验通过
- **THEN** 系统 MUST 在同一事务内创建换货单并完成换货
- **AND** 返回结果 MUST 为 `status=4`
#### Scenario: direct 缺少新资产标识
- **WHEN** 后台以 `flow_type=direct` 发起换货但未传 `new_identifier`
- **THEN** 系统 MUST 拒绝创建并返回参数错误
#### Scenario: direct 迁移标记未传
- **WHEN** 后台以 `flow_type=direct` 发起换货且未传 `migrate_data`
- **THEN** 系统 MUST 按 `migrate_data=false` 创建并完成换货
#### Scenario: direct 完成失败不留半成品单据
- **WHEN** 后台以 `flow_type=direct` 发起换货,但完成事务中的绑定承接或迁移步骤失败
- **THEN** 系统 MUST 回滚整笔事务
- **AND** MUST NOT 查询到本次请求创建的半成品 direct 换货单
#### Scenario: 资产已有进行中 shipping 换货单
- **WHEN** 后台为同一资产重复发起 `shipping` 换货
- **THEN** 系统 MUST 拒绝创建并返回“存在进行中的换货单”
#### Scenario: 旧资产非已销售禁止换货
- **WHEN** 旧资产 `asset_status != 2`
- **THEN** 系统 MUST 拒绝创建并返回资产状态不允许换货
---
### Requirement: H2 换货单列表
系统 SHALL 提供 `GET /api/admin/exchanges``Auth=true`),支持分页与条件查询。
查询条件 SHOULD 支持:`status``flow_type``identifier`(资产标识搜索)、`created_at_start``created_at_end`、分页参数。
响应 SHALL 返回列表与分页元数据。
响应项 MUST 返回:旧/新资产标识、`flow_type``status``shipped_at``completed_at`
#### Scenario: 按流程类型查询 direct 已完成单
- **WHEN** 运营查询 `flow_type=direct``status=4`
- **THEN** 系统返回所有 direct 已完成换货单并按创建时间倒序
---
### Requirement: H3 换货单详情
系统 SHALL 提供 `GET /api/admin/exchanges/:id``Auth=true`)查询换货单详情。
响应 MUST 返回旧/新资产信息、流程类型、收货信息、物流信息、迁移状态信息、`shipped_at``completed_at`
错误响应 MUST 至少包含:换货单不存在或无权限。
#### Scenario: 查询 direct 换货单详情
- **WHEN** 查询一张 `flow_type=direct` 的已完成换货单
- **THEN** 响应 MUST 返回 `flow_type=direct`
- **AND** 收货信息与物流信息可以为空
- **AND** `completed_at` 必须存在
---
### Requirement: H4 发货
系统 SHALL 提供 `POST /api/admin/exchanges/:id/ship``Auth=true`)。
请求体 MUST 包含:`express_company``express_no``new_identifier``migrate_data`
系统 MUST 校验:
- 换货单 `flow_type` 必须为 `shipping`
- 当前状态必须为 `2`
- 旧资产当前必须仍为 `asset_status=2`(已销售)
- 新旧资产类型必须一致(卡换卡/设备换设备)
- 新资产必须 `asset_status=1`(在库)
- 新资产当前用户有权限
- 新旧资产 `shop_id` 必须一致(含二者同为平台库存 `NULL`
- 新资产不得存在 `status=1` 的有效客户绑定
- 新资产不得已被其他 `shipping + status=3` 换货单占用
- 系统 MUST 通过条件更新、行锁或等效机制确保发货成功时新资产仍满足在库且未被占用
成功后 SHALL
- 更新新资产信息
- 更新物流信息
- 写入 `migrate_data`
- 记录 `shipped_at`
- 将状态改为 `3`
- 将新资产视为被当前换货单占用;在确认完成前不改变新资产 `asset_status`
错误响应 MUST 至少包含:非法状态、流程类型不支持发货、旧资产状态不允许换货、资产类型不匹配、新资产非在库、新旧资产归属不一致、新资产已被占用、资产不存在或无权限。
#### Scenario: direct 单据禁止发货
- **WHEN** `flow_type=direct` 的换货单调用发货接口
- **THEN** 系统 MUST 拒绝并返回流程类型不支持该操作的错误
#### Scenario: 新资产类型不一致
- **WHEN** 旧资产为 `iot_card` 且新资产为 `device`
- **THEN** 系统 MUST 拒绝发货并返回“换货资产类型必须一致”
#### Scenario: 新资产已被其他换货单占用
- **WHEN** 新资产已经作为其他 `shipping + status=3` 换货单的新资产
- **THEN** 系统 MUST 拒绝发货并返回新资产已被占用
---
### Requirement: H5 确认完成
系统 SHALL 提供 `POST /api/admin/exchanges/:id/complete``Auth=true`)。
系统 MUST 校验:
- 换货单 `flow_type` 必须为 `shipping`
- 当前状态必须为 `3`
系统 MUST 在**单一数据库事务**中执行完成换货动作。该事务至少包括:
- 校验新资产快照完整且新资产仍满足换货条件
- 校验旧资产仍为 `asset_status=2`(已销售)
- 校验新资产仍为 `asset_status=1`(在库),且未被当前换货单之外的有效记录占用
- 旧资产 `asset_status -> 3`
- 新资产 `asset_status -> 2`
- 若旧资产存在 `PersonalCustomerDevice` 绑定,则绑定切换到新资产资产绑定键
-`migrate_data=true`,执行全量迁移事务(见 `exchange-data-migration` 能力)
- 写入 `completed_at`
- 换货单状态更新为 `4`
成功后 SHALL
- `migration_completed=true`(若执行迁移)
- 换货单状态更新为 `4`
错误响应 MUST 至少包含:非法状态、流程类型不支持确认完成、旧资产状态不允许换货、新资产状态不允许换货、迁移失败、绑定无法承接、钱包冻结余额未处理、换货单不存在或无权限。
#### Scenario: 需要迁移并完成
- **WHEN** `shipping` 换货单状态为 `3``migrate_data=true`
- **THEN** 系统 MUST 在同一事务成功后将状态变为 `4` 并记录迁移结果
#### Scenario: 不迁移也必须完成切换
- **WHEN** `shipping` 换货单状态为 `3``migrate_data=false`
- **THEN** 系统 MUST 仍然在事务内完成旧资产状态切换、新资产状态切换、绑定切换和单据完成
---
### Requirement: H6 取消换货
系统 SHALL 提供 `POST /api/admin/exchanges/:id/cancel``Auth=true`)。
系统 MUST 仅允许 `flow_type=shipping``status IN (1,2)` 时取消,成功后状态更新为 `5`
系统 MUST 禁止已发货单取消(`status=3`)。
系统 MUST 禁止 `direct` 单据进入取消分支。
#### Scenario: 已发货单取消失败
- **WHEN** `shipping` 换货单状态为 `3` 发起取消
- **THEN** 系统 MUST 返回状态非法错误
#### Scenario: direct 单据取消失败
- **WHEN** `direct` 换货单发起取消
- **THEN** 系统 MUST 返回流程类型不支持该操作的错误
---
### Requirement: H7 旧资产转新
系统 SHALL 提供 `POST /api/admin/exchanges/:id/renew``Auth=true`)。
系统 MUST 校验旧资产当前 `asset_status=3`(已换货),并执行:
- `generation + 1`
- `asset_status -> 1`
- 清除累计充值/首充相关状态
- 清除个人客户绑定
- 创建新空钱包
系统 MUST 保留历史数据,不执行历史删除。
系统 MUST NOT 在换货完成阶段修改旧资产 `generation``generation` 仅在 `renew` 阶段递增。
错误响应 MUST 至少包含:资产状态不满足转新条件、换货单不存在或无权限。
#### Scenario: 旧资产未处于已换货状态
- **WHEN** 旧资产 `asset_status != 3` 发起转新
- **THEN** 系统 MUST 拒绝并返回“资产当前状态不允许转新”

View File

@@ -0,0 +1,51 @@
## MODIFIED Requirements
### Requirement: G1 查询进行中换货通知
系统 SHALL 提供 `GET /api/c/v1/exchange/pending?identifier=xxx`(需个人客户认证 `Auth=true`)。
系统 MUST 根据资产标识查询当前客户可见的进行中换货单。
查询规则 MUST 满足:
- 仅返回 `flow_type=shipping`
- 仅返回 `status IN (1,2,3)` 的记录
- `direct` 单据无论状态如何都 MUST NOT 出现在该接口结果中
- 历史记录若 `flow_type` 为空或缺失,系统 MUST 按 `shipping` 兼容处理
响应 SHALL 至少包含:换货单 ID、单号、流程类型、状态、换货原因、创建时间。
错误响应 MUST 至少包含:参数错误、资产不存在或无权限。
#### Scenario: 命中 shipping 进行中换货单
- **WHEN** 客户按资产标识查询且存在 `flow_type=shipping` 且状态为 `2` 的换货单
- **THEN** 系统返回该换货单并标识当前状态为待发货
#### Scenario: direct 已完成单据不进入待处理
- **WHEN** 客户按资产标识查询,但该资产最近一次换货为 `flow_type=direct` 且已完成
- **THEN** 系统 MUST 返回空结果,不将该单据视为待处理通知
---
### Requirement: G2 填写收货信息
系统 SHALL 提供 `POST /api/c/v1/exchange/:id/shipping-info`(需个人客户认证 `Auth=true`)。
请求体 MUST 包含:`recipient_name``recipient_phone``recipient_address`
系统 MUST 校验:
- 换货单存在且当前客户有权限
- `flow_type` 必须为 `shipping`
- 当前状态必须为 `1`
- 历史记录若 `flow_type` 为空或缺失,系统 MUST 按 `shipping` 兼容处理
成功后 SHALL 写入收货信息并将状态更新为 `2`
错误响应 MUST 至少包含:参数错误、状态非法、流程类型不支持该操作、换货单不存在或无权限。
#### Scenario: 非待填写状态禁止更新收货信息
- **WHEN** `shipping` 换货单当前状态为 `2``3`
- **THEN** 系统 MUST 拒绝填写并返回状态非法错误
#### Scenario: direct 单据禁止填写收货信息
- **WHEN** `direct` 换货单调用填写收货信息接口
- **THEN** 系统 MUST 拒绝并返回流程类型不支持该操作的错误

View File

@@ -0,0 +1,132 @@
## MODIFIED Requirements
### Requirement: 完成换货事务边界
系统 MUST 在换货完成时使用**单一数据库事务**执行“完成换货必做切换”,并在 `migrate_data=true` 时把全量迁移纳入同一事务。
该事务 SHALL 覆盖:
- 旧资产合法性校验
- 新资产合法性校验
- 新旧资产归属一致性校验
- 旧资产 `asset_status -> 3`
- 新资产 `asset_status -> 2`
- 个人客户绑定切换
- `migrate_data=true` 时的钱包、套餐、标签、累计状态迁移
- 换货单 `migration_completed``migration_balance``completed_at``status=4` 更新
任一步骤失败 MUST 回滚。`shipping` 确认完成失败时,换货单状态保持未完成;`direct` 创建即完成失败时,系统 MUST 回滚整笔创建事务且不保留半成品 `direct` 换货单。
该事务适用范围 MUST 包括:
- `shipping` 流程的 H5 确认完成
- `direct` 流程的创建即完成
#### Scenario: 迁移中途失败回滚
- **WHEN** 完成换货事务第 N 步发生数据库错误
- **THEN** 系统 MUST 回滚整个事务,换货单状态保持未完成
#### Scenario: direct 完成事务失败不落单
- **WHEN** `direct` 创建即完成事务第 N 步失败
- **THEN** 系统 MUST 回滚整个事务
- **AND** MUST NOT 保留本次创建的换货单
#### Scenario: 不迁移也必须使用完成事务
- **WHEN** `migrate_data=false` 且执行 `shipping` 完成或 `direct` 创建即完成
- **THEN** 系统 MUST 仍然在事务中执行旧资产状态切换、新资产状态切换、绑定切换和单据完成
---
### Requirement: 完成换货必做切换规则
系统 SHALL 将以下动作定义为“完成换货必做切换”,无论 `migrate_data` 为真或假都必须执行:
1. 校验新资产存在且与旧资产同类型。
2. 校验旧资产当前 `asset_status=2`(已销售)。
3. 校验新资产当前 `asset_status=1`(在库)。
4. 校验新旧资产 `shop_id` 一致,含二者同为平台库存 `NULL`
5. 校验新资产未被当前换货单之外的有效客户绑定或进行中换货单占用。
6. 将旧资产 `asset_status` 更新为 `3`(已换货)。
7. 将新资产 `asset_status` 更新为 `2`(已销售)。
8. 若旧资产存在 `PersonalCustomerDevice` 绑定,则将绑定记录中的资产标识字段更新为新资产资产绑定键。
9. 记录换货单 `completed_at`
10. 将换货单状态更新为 `4`
若旧资产存在客户绑定但新资产无法承接绑定,系统 MUST 视为完成换货失败并回滚。
#### Scenario: 不迁移但完成换货
- **WHEN** 后台执行换货完成且 `migrate_data=false`
- **THEN** 系统 MUST 仍然将旧资产标记为已换货
- **AND** MUST 将新资产标记为已销售
- **AND** MUST 更新客户绑定关系
#### Scenario: 新资产无法承接客户绑定
- **WHEN** 旧资产存在个人客户绑定,但新资产缺少可承接的资产绑定键
- **THEN** 系统 MUST 拒绝完成换货并回滚
#### Scenario: 新资产被其他进行中换货占用
- **WHEN** 新资产已经作为其他 `shipping + status=3` 换货单的新资产
- **THEN** 系统 MUST 拒绝完成换货并回滚
---
### Requirement: 11 张表迁移规则
系统 SHALL 在 `migrate_data=true` 时按以下规则处理 11 张表:
1. `tb_asset_wallet`:将旧资产钱包余额转移到新资产钱包。
2. `tb_asset_wallet_transaction`:生成一条迁移流水记录(明确来源钱包、目标钱包、金额、业务类型)。
3. `tb_asset_recharge_record`:历史充值记录保留,不做更新。
4. `tb_package_usage`:将生效套餐关联到新资产(更新 `iot_card_id``device_id`)。
5. `tb_package_usage_daily_record`:随 `tb_package_usage` 关系迁移(保持套餐日明细连续性)。
6. `tb_order`:历史订单保留,不做更新。
7. `tb_commission`:历史分佣记录保留,不做更新。
8. `tb_data_usage_record`:历史流量记录保留,不做更新。
9. `tb_resource_tag`:复制旧资产标签到新资产。
10. `tb_personal_customer_device`:若旧资产存在绑定,绑定记录中的资产标识字段更新为新资产资产绑定键。
11. `tb_iot_card`/`tb_device`:复制累计充值与首充状态到新资产。
旧资产 `asset_status -> 3` 与新资产 `asset_status -> 2` 属于“完成换货必做切换”,不再视为仅在迁移开启时执行的动作。
钱包迁移 MUST 满足:
- 旧资产钱包存在 `frozen_balance > 0` 时,系统 MUST 拒绝迁移并回滚整个完成事务
- 系统 MUST NOT 对冻结余额做部分迁移或静默清零
- 旧资产没有钱包时,迁移余额按 `0` 处理
- 新资产没有钱包时,系统 MAY 在同一事务内创建新钱包
- 写入迁移流水时 MUST 使用能表达“换货迁移”的业务类型或备注,不能伪装为普通充值、退款或消费
#### Scenario: 钱包余额转移并记录流水
- **WHEN** 旧资产钱包余额为 5000 分
- **THEN** 新资产钱包余额增加 5000 分,旧钱包余额按迁移策略清零,并写入迁移流水
#### Scenario: 旧钱包存在冻结余额
- **WHEN** `migrate_data=true` 且旧资产钱包 `frozen_balance > 0`
- **THEN** 系统 MUST 拒绝完成换货并回滚
- **AND** MUST 返回钱包冻结余额未处理的错误语义
---
### Requirement: 设备换设备特殊规则
设备换设备流程 MUST NOT 迁移 `DeviceSimBinding`
系统 SHALL 视新设备为新硬件交付,新设备卡绑定由其自身体系决定,旧设备绑定关系保留历史。
#### Scenario: 设备换设备不复制绑定卡
- **WHEN** 执行设备换设备全量迁移
- **THEN** 系统 MUST 不创建或复制任何 `DeviceSimBinding` 记录到新设备
---
### Requirement: 转新规则
系统 SHALL 在 H7 转新时执行代际隔离策略:
- 资产 `generation + 1`
- 创建新空钱包(新 `wallet_id`
- 清除累计充值状态与首充触发状态
- 清除 `PersonalCustomerDevice` 绑定
- 不删除历史业务数据
系统 MUST NOT 在换货完成阶段变更 `generation`
#### Scenario: 转新后历史数据保留
- **WHEN** 资产转新完成
- **THEN** 历史订单、充值、分佣、流量数据 MUST 仍可在旧代际查询链路中追溯

View File

@@ -0,0 +1,135 @@
## MODIFIED Requirements
### Requirement: ExchangeOrder 换货单模型定义
系统 SHALL 定义 `ExchangeOrder` 模型并映射到 `tb_exchange_order`,用于承载客户端换货完整生命周期。
模型字段 MUST 至少包含:
- 基础:`id``created_at``updated_at``deleted_at``creator``updater`
- 单号:`exchange_no`
- 流程:`flow_type`
- 旧资产:`old_asset_type``old_asset_id``old_asset_identifier`
- 新资产:`new_asset_type``new_asset_id``new_asset_identifier`
- 收货:`recipient_name``recipient_phone``recipient_address`
- 物流:`express_company``express_no`
- 迁移:`migrate_data``migration_completed``migration_balance`
- 时间:`shipped_at``completed_at`
- 业务:`exchange_reason``remark``status`
- 多租户:`shop_id`
`flow_type` MUST 使用字符串枚举,至少支持:
- `shipping`:需要客户填写收货地址、后台发货、后台确认完成
- `direct`:创建时直接完成,不经过客户填写地址和后台发货
`flow_type` MUST 在数据库层设置默认值 `shipping`,用于兼容历史记录与旧创建请求。
`ExchangeOrder` SHALL 嵌入 `BaseModel` 并实现 `TableName() string`,返回 `tb_exchange_order`
#### Scenario: 创建 shipping 换货单模型实例
- **WHEN** 系统创建新的 `shipping` 换货单记录
- **THEN** 记录 MUST 包含旧资产快照、流程类型 `flow_type=shipping`、收货信息占位、迁移状态字段和多租户字段
#### Scenario: 创建 direct 换货单模型实例
- **WHEN** 系统创建新的 `direct` 换货单记录
- **THEN** 记录 MUST 包含旧资产快照、新资产快照、流程类型 `flow_type=direct`
- **AND** 记录在创建成功时即可具备 `completed_at`
---
### Requirement: 换货状态常量定义
系统 MUST 使用 int 常量定义换货状态:
- `1` 待填写信息
- `2` 待发货
- `3` 已发货待确认
- `4` 已完成
- `5` 已取消
系统 MUST 使用独立的字符串常量定义换货流程类型:
- `shipping`
- `direct`
#### Scenario: 状态与流程常量一致性
- **WHEN** Service、Store、Handler 读取或更新换货状态与流程类型
- **THEN** 各层 MUST 使用统一常量值,禁止硬编码散落魔法数字和字符串
---
### Requirement: 换货状态机流转规则
系统 SHALL 执行以下状态机:
- `shipping`
- 创建换货单后:`status=1`
- 客户填写收货信息后:`1 -> 2`
- 后台发货后:`2 -> 3`
- 后台确认完成后:`3 -> 4`
- 取消:仅允许 `1/2 -> 5`
- `direct`
- 创建换货单并完成后:`status=4`
- 不经过 `1/2/3`
- 不走客户端填写收货地址
- 不走后台发货
- 不进入取消状态机
- 任一步失败时不保留半成品 `direct` 单据
系统 MUST 禁止非法流转(如 `3 -> 5``4 -> 2``direct 进入 2`)。
#### Scenario: shipping 已发货不可取消
- **WHEN** `shipping` 换货单状态为 `3` 且请求取消
- **THEN** 系统 MUST 拒绝并返回状态流转非法错误
#### Scenario: direct 创建即完成
- **WHEN** 后台以 `flow_type=direct` 创建换货单且所有校验通过
- **THEN** 系统 MUST 直接创建 `status=4` 的换货单
- **AND** MUST 写入 `completed_at`
#### Scenario: direct 失败不落半成品状态
- **WHEN** 后台以 `flow_type=direct` 创建换货单但完成事务失败
- **THEN** 系统 MUST 回滚创建
- **AND** MUST NOT 产生 `flow_type=direct AND status IN (1,2,3)` 的换货单
#### Scenario: direct 不允许进入发货阶段
- **WHEN** `direct` 换货单请求执行发货或填写收货地址
- **THEN** 系统 MUST 拒绝并返回流程类型不支持该操作的错误
---
### Requirement: 换货单号生成规则
系统 MUST 为每个换货单生成全局可追踪单号,格式为:`EXC + 时间戳片段 + 随机数片段`
生成规则 SHALL 满足:
- 前缀固定为 `EXC`
- 包含日期/时间信息用于人工排查
- 包含随机片段降低并发冲突概率
#### Scenario: 生成换货单号
- **WHEN** 后台发起换货并创建新单
- **THEN** 系统 MUST 生成形如 `EXC20260319XXXXXX` 的单号并写入 `exchange_no`
---
### Requirement: 换货关键业务时间字段语义
系统 SHALL 使用独立的业务时间字段表达发货完成和换货完成,而不是复用 `updated_at`
字段语义 MUST 满足:
- `shipped_at`:仅在 `shipping` 流程发货成功后写入
- `completed_at`:仅在换货完成成功后写入,`shipping``direct` 共用
- `direct` 流程 MUST 保持 `shipped_at` 为空
- 历史已完成单据若 `completed_at` 为空,查询与追溯层 MUST 回退使用 `updated_at`
#### Scenario: shipping 写入发货时间
- **WHEN** `shipping` 换货单执行后台发货成功
- **THEN** 系统 MUST 记录 `shipped_at=当前时间`
#### Scenario: direct 不写发货时间
- **WHEN** `direct` 换货单创建并完成成功
- **THEN** 系统 MUST 保持 `shipped_at` 为空
- **AND** MUST 写入 `completed_at=当前时间`
#### Scenario: 历史单据完成时间兼容
- **WHEN** 查询历史已完成换货单且 `completed_at` 为空
- **THEN** 系统 MUST 在追溯展示中回退使用 `updated_at` 作为兼容完成时间

View File

@@ -0,0 +1,52 @@
## ADDED Requirements
### Requirement: IoT 卡换货换出行为
系统 SHALL 在 IoT 卡作为旧资产完成换货后,将其视为“已被换出、不可继续作为当前客户资产使用”的资产。
系统 MUST 满足:
- 换货完成前旧卡必须 `asset_status=2`(已销售)
- 换货完成时旧卡 `asset_status -> 3`
- 旧卡 `generation` 在换货完成时保持不变
- 若后续执行 `renew`,才允许旧卡重新进入新一代库存
#### Scenario: shipping 完成后旧卡标记为已换货
- **WHEN** 一张 IoT 卡作为旧资产完成 `shipping` 换货
- **THEN** 系统 MUST 将该卡 `asset_status` 更新为 `3`
#### Scenario: direct 完成后旧卡标记为已换货
- **WHEN** 一张 IoT 卡作为旧资产完成 `direct` 换货
- **THEN** 系统 MUST 将该卡 `asset_status` 更新为 `3`
---
### Requirement: IoT 卡换货换入行为
系统 SHALL 在 IoT 卡作为新资产完成换货后,将其视为当前客户正在使用的资产。
系统 MUST 满足:
- 换货完成前新卡必须 `asset_status=1`(在库)
- 换货完成前新卡必须与旧卡 `shop_id` 一致,含二者同为平台库存 `NULL`
- 换货完成前新卡不得存在有效客户绑定或被其他进行中换货单占用
- 换货完成后新卡 `asset_status -> 2`
- 新卡 `generation` 在换货完成时保持不变
- 新卡来源旧卡关系 MUST 通过 `ExchangeOrder` 可追溯
#### Scenario: 新卡完成换货后切为已销售
- **WHEN** 一张 IoT 卡作为新资产完成换货
- **THEN** 系统 MUST 将该卡 `asset_status` 更新为 `2`
---
### Requirement: IoT 卡转新重置规则
系统 SHALL 在 H7 转新时对 IoT 卡执行以下重置:
- `generation = generation + 1`
- `asset_status = 1`(在库)
- 清空累计充值与首充触发相关状态(含 `AccumulatedRecharge`、系列累计/首充字段)
- 清除个人客户绑定关系
- 删除旧钱包并创建新空钱包
#### Scenario: 转新后进入新代际
- **WHEN** 对旧卡执行转新
- **THEN** 系统 MUST 使该卡进入新代际并以在库状态重新销售

View File

@@ -0,0 +1,56 @@
## MODIFIED Requirements
### Requirement: 换货迁移时更新个人客户资产绑定
系统 SHALL 在换货完成成功后,更新 `PersonalCustomerDevice` 的资产标识绑定关系:
- 若旧资产存在客户绑定,绑定中的 `virtual_no` MUST 更新为新资产资产绑定键
- 更新后客户对资产访问连续,不需重新登录即可看到新资产
该规则 MUST 同时适用于:
- `shipping` 流程完成换货
- `direct` 流程创建即完成
该规则 MUST NOT 仅依赖 `migrate_data=true` 才执行。
#### Scenario: 不迁移也要切换客户绑定
- **WHEN** 旧资产存在个人客户绑定且执行了 `migrate_data=false` 的换货完成
- **THEN** 系统 MUST 仍然将绑定记录的资产标识字段更新为新资产资产绑定键
#### Scenario: 迁移后客户绑定跟随新资产
- **WHEN** 旧资产存在个人客户绑定且执行了 `migrate_data=true`
- **THEN** 系统 MUST 将绑定记录的资产标识字段更新为新资产资产绑定键
---
### Requirement: 换货完成前的绑定承接校验
系统 SHALL 在换货完成前校验新资产是否可以承接现有个人客户绑定。
若旧资产存在 `PersonalCustomerDevice` 绑定,则系统 MUST 满足:
- 新资产存在可写入绑定的资产绑定键
- IoT 卡资产绑定键 MUST 使用新卡 `virtual_no`
- 设备资产绑定键 MUST 优先使用新设备 `virtual_no`,为空时 MAY 使用新设备 `imei`,与现有登录绑定规则保持一致
- 新资产绑定键当前不得存在 `status=1``PersonalCustomerDevice` 绑定记录
- 已禁用或软删除绑定记录不视为当前绑定冲突
若任一条件不满足,系统 MUST 拒绝完成换货并回滚整个事务。
#### Scenario: 新资产无法承接绑定时回滚
- **WHEN** 旧资产存在客户绑定,但新资产缺少可承接的资产绑定键
- **THEN** 系统 MUST 拒绝完成换货
- **AND** MUST 保持换货单未完成
#### Scenario: 新资产已有有效客户绑定时回滚
- **WHEN** 旧资产存在客户绑定,但新资产绑定键已经存在 `status=1` 的客户绑定记录
- **THEN** 系统 MUST 拒绝完成换货
- **AND** MUST 回滚整个完成事务
---
### Requirement: 转新时清除个人客户绑定
系统 SHALL 在 H7 转新时清除该资产在 `PersonalCustomerDevice` 中的绑定关系,避免旧客户继续访问新代际资产。
#### Scenario: 转新后旧客户需重新绑定
- **WHEN** 资产转新完成
- **THEN** 系统 MUST 删除或失效对应客户绑定,使旧客户再次访问时触发重新绑定流程