## 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` 作为兼容完成时间
