## Context

当前换货实现已经具备后台建单、客户端填写地址、后台发货、后台完成、旧资产转新的主链路，但实现上仍然是单一路径，且关键业务动作没有被统一到稳定的事务边界内。

当前实现存在四个直接问题：

1. `ExchangeOrder` 模型没有 `flow_type`，无法表达“走物流”与“直转新资产”两种流程，只能依赖状态字段隐式区分。
2. `Complete()` 不是单事务。当前 `migrate_data=true` 时，`executeMigration()` 会先独立提交一次事务，随后 `Complete()` 再单独把换货单状态改为已完成，存在“迁移已生效但单据仍停在待完成”的不一致窗口。
3. 旧资产 `asset_status -> 3` 只在迁移逻辑里执行。也就是说，`migrate_data=false` 时单据虽然可以完成，但旧资产不会被系统视为“已换货”，后续 `Renew()` 会直接失败。
4. 个人客户绑定切换当前只在迁移逻辑中执行。这会导致“换货成功但客户仍绑定旧资产”的脏状态，尤其在新增 `direct + migrate_data=false` 场景下会被明显放大。

本次变更横跨模型、DTO、后台/客户端接口、换货服务、迁移事务、资产追溯和生命周期语义，是一个典型的跨模块行为修正型变更。

## Goals / Non-Goals

**Goals:**

- 在现有换货系统内新增 `direct` 流程，同时保留 `shipping` 原流程不变。
- 将换货流程类型与状态机拆开：`flow_type` 表达业务路径，`status` 继续表达处理阶段。
- 定义一个统一、可回滚的“完成换货”单事务，覆盖必做切换与可选迁移。
- 保证 `migrate_data=false` 仍然能形成业务闭环：旧资产被换出、新资产成为当前资产、客户绑定切到新资产、单据已完成。
- 明确旧资产/新资产/世代/追溯链的状态变化，使后续实现无需再靠推测还原业务语义。
- 保证 `iot_card` 与 `device` 两类资产都支持 `direct` 流程。

**Non-Goals:**

- 不新增第三种换货流程类型。
- 不改变现有 `renew` 的业务定位，仍然由运营后台显式触发，而不是在换货完成时自动执行。
- 不新增自动消息推送或物流轨迹查询能力。
- 不在本次变更中回填历史换货单数据，也不把 legacy 记录回灌为新语义数据。

## Decisions

### 决策 1：新增 `flow_type`，保留现有状态值不变

换货流程存在两条业务路径：

- `shipping`：建单后等待客户填写收货地址，再由后台发货、后台确认完成。
- `direct`：建单时已经确定新资产，无需客户填写地址，也无需物流，建单即可完成。

如果继续只用 `status` 表达两种路径，会导致以下问题：

- 客户端待处理查询无法区分“真的待客户处理”和“已跳过客户节点”。
- 后台列表中 `status=4` 的已完成单据看不出是走物流还是直转。
- 取消、发货、填写地址等接口无法自然约束适用范围。

因此本次设计明确：

- `flow_type` 使用字符串枚举：`shipping` / `direct`
- 为保持旧后台调用兼容，创建接口未传 `flow_type` 时按 `shipping` 处理
- 创建接口传入非空且不属于 `shipping/direct` 的 `flow_type` 时必须返回参数错误
- `status` 继续保留现有 int 常量：`1/2/3/4/5`
- `shipping` 流程状态机保持不变
- `direct` 流程允许“创建后立即处于 `status=4`”

选择原因：

- 对原流程兼容性最好，不需要重命名现有状态常量。
- 后台与客户端都能用 `flow_type + status` 明确判断当前单据语义。
- 查询、追溯和审计都更稳定。

备选方案与拒绝理由：

- 方案 A：新增 `status=6 直转已完成`
  - 拒绝原因：状态值被混入路径语义，客户端和后台会被迫理解“特殊完成态”，长期维护成本更高。
- 方案 B：新增第二张 `direct_exchange_order` 表
  - 拒绝原因：核心事实被拆散，追溯链和列表逻辑会重复实现。

### 决策 2：完成换货拆为“必做切换 + 可选迁移”，并统一纳入单事务

完成换货后，系统至少要落定以下事实：

- 本次换货单已经结束。
- 旧资产不能再继续作为当前客户资产使用。
- 新资产已经成为客户当前资产。
- 若客户原先已绑定旧资产，绑定关系必须切到新资产。

这些动作并不是“可选迁移”，而是换货成功本身的最小闭环。因此本次设计把完成换货分为两层：

1. 必做切换：
	   - 校验新资产存在、类型一致、当前在库
	   - 写入 `new_asset_type/new_asset_id/new_asset_identifier`
	   - 旧资产 `asset_status -> 3`
	   - 新资产 `asset_status -> 2`
	   - 若旧资产存在 `PersonalCustomerDevice` 绑定，则绑定切换到新资产资产绑定键
   - 写 `completed_at`
   - 换货单 `status -> 4`
2. 可选迁移，仅 `migrate_data=true` 时执行：
   - 钱包余额迁移
   - 迁移流水记录
   - 套餐使用记录迁移
   - 套餐日明细连续性保留
   - 标签复制
   - 累计充值/首充触发状态复制

上述两层必须在同一个数据库事务中完成。事务任一步失败，整个完成动作回滚，换货单保持未完成。

选择原因：

- 避免 `migrate_data=false` 出现“单据完成但旧资产仍可被系统当成正常资产”的错误状态。
- 避免迁移提交成功但换货单状态未完成的分裂状态。
- 让 `direct` 与 `shipping` 共用同一个“完成换货”内核，只是入口不同。

备选方案与拒绝理由：

- 方案 A：继续保留 `executeMigration()` 自己开事务，`Complete()` 外层只做单据状态更新
  - 拒绝原因：状态不一致风险已经存在，不能带到新流程。
- 方案 B：`migrate_data=false` 时只更新单据状态，不更新资产和绑定
  - 拒绝原因：这不是换货成功，只是“记录一张单子”。

### 决策 3：`direct` 在创建接口里完成“建单 + 完成换货”

`direct` 不是后台先建一个待处理单再调用 `complete`，而是后台在创建时就已经给出：

- 旧资产
- 新资产
- 是否迁移；`migrate_data` 未传时按 `false` 处理，传入时必须是布尔值
- 换货原因

因此 `POST /api/admin/exchanges` 在 `flow_type=direct` 时应直接执行：

1. 校验旧资产存在、无进行中的换货单
2. 校验旧资产处于 `asset_status=2` 已销售
3. 校验新资产存在、类型一致、在库、归属店铺一致、未被有效客户绑定占用
4. 创建换货单并写入 `old_*` / `new_*` / `flow_type` / `migrate_data`
5. 在同一事务内执行“完成换货必做切换”
6. 若 `migrate_data=true`，继续执行可选迁移
7. 直接返回 `status=4`

事务语义：

- `direct` 创建、完成切换、可选迁移必须共用同一个数据库事务
- 任一步失败时整个事务回滚，不保留 `status=1/2/3` 或其他半成品 `direct` 单据
- 成功返回时，数据库中已存在且只能存在 `status=4` 的 `direct` 单据

这样做的好处：

- 不产生半成品 `direct` 单据
- 不需要为 `direct` 再暴露一个“后台补完成”的额外接口
- 事务边界清晰：创建成功即代表换货成功

### 决策 4：`shipping` 的发货与完成职责分离，但完成时仍重用同一事务内核

`shipping` 流程保留现有操作感知：

- 创建单据：`status=1`
- 客户填写地址：`1 -> 2`
- 后台发货：`2 -> 3`
- 后台确认完成：`3 -> 4`

其中：

- `ship` 只负责落新资产快照、物流信息、`migrate_data`、`shipped_at`，并把状态推进到 `3`
- `complete` 负责执行“完成换货单事务”

`shipping` 发货后的新资产占用规则：

- `ship` 选择新资产时必须使用条件更新或行锁确认新资产仍为 `asset_status=1`
- `ship` 成功后新资产已被该换货单占用，其他换货单或销售绑定不得再使用该新资产
- 若实现不新增独立“占用中”状态，则必须通过换货单 `new_asset_type/new_asset_id + status IN (3)` 的唯一性/查询校验阻止重复占用
- `complete` 时仍需再次校验新资产未被外部写入破坏；若不满足条件，完成失败并保持单据未完成

这样处理可以保留现有后台操作习惯，同时让 `shipping` 和 `direct` 的最终完成动作完全一致。

### 决策 5：客户绑定切换属于必做动作，且需要可承接性前置校验

如果旧资产存在 `PersonalCustomerDevice` 绑定，则完成换货后客户必须继续指向新资产，否则业务上等于“客户仍在使用旧资产”。

因此本次设计将绑定切换定义为必做动作，并增加一个硬前置规则：

- 若旧资产存在客户绑定，则新资产必须具备可写入绑定的资产绑定键
- IoT 卡使用 `virtual_no` 作为绑定键；设备优先使用 `virtual_no`，为空时可按现有登录绑定规则使用 `imei`
- 新资产绑定键不得已存在 `status=1` 的 `PersonalCustomerDevice` 绑定记录，避免多个客户当前态混入同一资产
- 若新资产无法承接绑定，换货完成动作必须失败并回滚

这里不要求系统为缺失资产绑定键的新资产自动生成标识，因为那会扩大本次变更边界，也会影响其他资产管理能力。

### 决策 6：新资产在换货完成后必须从“在库”切为“已销售”

当前 `ship` 和 `complete` 流程都要求新资产必须先是 `asset_status=1` 在库，但完成换货后，系统实际上已经把该资产交付给了当前客户。如果不更新新资产状态，会出现“库存资产已被客户使用”的脏状态。

因此本次设计明确：

- 完成换货成功后，新资产 `asset_status -> 2`
- 此规则同时适用于 `shipping` 与 `direct`
- 旧资产必须在完成前为 `asset_status=2` 已销售；已换货、在库或已停用资产不得发起/完成换货
- 新资产必须在选择和完成时都满足 `asset_status=1` 在库
- 旧资产 `generation` 不变；新资产 `generation` 也不变

这与 `renew` 不冲突。`renew` 处理的是旧资产重新回炉，不是新资产接棒。

### 决策 7：补充 `shipped_at` 和 `completed_at`，不再复用 `updated_at` 表达关键业务时间

当前资产历史订单追溯中的 `exchanged_at` 实际取的是换货单 `updated_at`，这会混淆以下行为：

- 后台补备注
- 重新发货修正
- 迁移状态更新

本次设计要求：

- `shipped_at`：仅在 `shipping` 发货成功时写入
- `completed_at`：仅在换货成功完成时写入，`shipping` 与 `direct` 共用
- 资产追溯里的 `exchanged_at` 改取 `completed_at`
- 历史已完成换货单没有 `completed_at` 时，追溯接口必须兼容回退到 `updated_at`

这样可以避免追溯链把“最后更新时间”错当成“换货完成时间”。

### 决策 8：`generation` 只在 `renew` 阶段递增

换货完成时的本质是“当前客户从旧资产切到新资产”，而不是“旧资产重新变成新货”。

因此：

- 换货完成阶段：
  - 旧资产 `generation` 不变
  - 新资产 `generation` 不变
- `renew` 阶段：
  - 旧资产 `generation + 1`
  - 旧资产 `asset_status -> 1`
  - 清空累计充值/首充状态
  - 清理个人客户绑定
  - 删除旧钱包并重建新空钱包

这样可以保证：

- 换货链由 `ExchangeOrder.old_* / new_*` 表达
- 世代链由 `generation` 表达
- 两条语义链不互相污染

### 决策 9：钱包冻结余额阻止可选迁移

`migrate_data=true` 时，旧资产钱包余额迁移会改写旧钱包余额并给新钱包入账。如果旧钱包存在冻结余额，说明仍有未完成扣款、退款、支付或其他资金占用。

因此：

- 旧资产钱包 `frozen_balance > 0` 时必须拒绝迁移并回滚整个完成事务
- 不迁移可用余额、不迁移冻结余额，也不做部分迁移
- 后台需要先处理冻结业务，再重新执行换货完成或 direct 创建

这样避免把仍在占用中的资金静默搬到新资产，导致后续支付/退款按旧钱包执行失败。

### 决策 10：多租户归属必须保持一致

换货不是跨店铺调拨能力。本次不扩大资产归属变更范围。

因此：

- 旧资产和新资产必须都在当前操作者权限范围内
- 新旧资产 `shop_id` 必须一致，含二者同为平台库存 `NULL`
- 换货单 `shop_id` 取旧资产 `shop_id`
- 如业务未来需要跨店铺换入，必须另起提案处理调拨、审计和权限边界

## Risks / Trade-offs

- [风险] `direct` 创建接口职责变重，参数和校验分支增加
  - 缓解：在 DTO 和 Service 层明确按 `flow_type` 分支校验，避免 handler 拼接复杂条件。

- [风险] 完成换货被收敛为大事务后，单次事务执行时间会增长
  - 缓解：换货属于低频后台操作，可接受；同时将“必做切换”和“可选迁移”分层，避免未来继续在事务里堆无关动作。

- [风险] 新资产可承接绑定的约束可能暴露出历史库存数据不完整问题
  - 缓解：把“无法承接绑定则完成失败”写成明确规则，避免系统默默产出脏数据；实际实现时通过错误码和后台提示引导运营修正库存数据。

- [风险] 主线 spec 与 archived spec 对 `personal-customer` 的换货 requirement 已有历史沉淀，修改时容易遗漏一致性
  - 缓解：本次 change 直接修改主线 capability，不再依赖 archived 文档语义。

- [风险] 新增 `completed_at` 后，旧数据没有该字段值
  - 缓解：本次不做历史回填；追溯逻辑对历史已完成单据统一回退到 `updated_at`，新完成单据必须写入并优先使用 `completed_at`。

## Migration Plan

1. 数据库变更
   - 为 `tb_exchange_order` 增加 `flow_type`、`shipped_at`、`completed_at`
   - 为 `flow_type` 设置默认值 `shipping`
   - 视查询需要补充 `flow_type + status`、`new_asset_type + new_asset_id` 等索引

2. 模型与 DTO 变更
   - 更新 `ExchangeOrder` 模型
   - 更新创建、发货、详情、列表、客户端待处理等 DTO

3. 服务层重构
   - 提炼统一的“完成换货事务”内部方法
   - 让 `shipping complete` 与 `direct create` 共用同一完成内核
   - 将现有 `executeMigration()` 重构为可注入外部事务的内部步骤，而不是自开事务

4. 接口兼容与文档更新
   - 后台创建接口支持 `flow_type`
   - 保持原 `shipping` 路径向后兼容
   - 更新 OpenAPI 文档生成器注册与中文描述

5. 手工验证
   - 执行 `go build ./...`
   - 使用 PostgreSQL MCP 核对字段、状态、时间字段和索引
   - 手工验证 `shipping/direct`、`migrate_data=true/false`、`renew`、`include_previous=true` 等关键场景

6. 回滚策略
   - 若实现尚未上线，可回滚代码并回滚新增字段
   - 若已产生 `direct` 单据数据，则不能只回滚代码而不处理数据，必须连带考虑接口兼容和字段保留

## Open Questions

- 无。当前边界按兼容优先、失败回滚、不引入新流程状态处理。
