# PRD：UR#98 换货新资产继承旧资产店铺

Status: ready-for-agent

---

## Problem Statement

当前换货要求新旧资产已经属于同一店铺，平台库存中的新资产不能直接换入；换货完成也没有统一维护新资产店铺归属、多租户字段和资产流转记录。这会迫使运营先做额外分配，或造成资产、钱包、标签、客户绑定和换货单之间的租户信息不一致。

换货完成同时修改多类资产数据，是必须原子完成的复杂写用例，不能继续由旧 Service 中分散的状态更新承担。

## Solution

换货新资产允许来自平台库存，或已经属于旧资产店铺；属于其他店铺时拒绝。换货完成时，新资产自动继承旧资产店铺，旧资产保留原归属。归属字段、多租户派生字段和资产分配记录始终同步；普通业务资料是否迁移继续由现有 `migrate_data` 决定。

完整完成流程迁入 Exchange Domain/Application，在同一数据库事务内锁定换货单和新旧资产，完成校验、归属继承、客户绑定、可选资料迁移、状态流转、分配记录和 Audit Event。

## User Stories

1. 作为运营人员，我希望直接选择平台库存中的新资产完成换货，无需提前人工分配。
2. 作为运营人员，我希望选择新资产时看到它完成后将归属哪个店铺，而不能手工指定目标店铺。
3. 作为运营人员，我希望其他店铺已拥有的资产不能被换入，防止跨租户侵占。
4. 作为店铺用户，我希望换货完成后能访问新资产，并继续查看保留原归属的旧资产历史。
5. 作为运营人员，我希望“不迁移资料”时仍正确继承归属，但不会意外迁移钱包、套餐或普通业务标签。
6. 作为维护人员，我希望重复完成请求不重复迁移、分配或写资金流水。
7. 作为审计人员，我希望一条换货链能追溯原店铺、目标归属、新旧资产和操作者。

## Implementation Decisions

### 归属规则

- 目标归属始终取旧资产当前 `shop_id`，请求不得携带目标店铺字段。
- 新资产 `shop_id` 为空（平台库存）时允许换入，并在完成时继承目标归属。
- 新资产已属于目标店铺时允许换入，完成时仍执行一致性校验和审计。
- 新资产属于任意其他店铺时拒绝；返回统一禁止访问错误，不透露其他店铺详情。
- 旧资产保留原 `shop_id` 和对应租户归属，用于历史订单、退款、换货链和权限追踪，仅将业务状态推进为“已换货”。
- 旧资产目标归属为空时，新资产完成后仍为平台库存；响应以 `inherited_shop_id=null`、空店铺名称表示。
- 卡只能换卡、设备只能换设备；新资产仍须处于可换入的在库业务状态、未被其他进行中换货占用且没有冲突客户绑定。

### 必做归属同步与可选资料迁移

- 无论 `migrate_data` 为 true 或 false，新资产的 `shop_id`、归属状态以及所有直接由资产店铺归属派生的多租户字段必须与目标归属一致。
- 已存在的新资产钱包等租户隔离记录，其 `shop_id_tag` 必须同步；新资产已有资源标签关联的 `shop_id` 等租户字段也必须按目标归属校正。
- 不把“租户字段同步”实现成无条件复制旧资产全部普通业务标签。
- 每次换货完成写一条 `allocation_type=exchange` 的资产分配记录，记录新资产、原所有者、目标所有者、操作人和换货单号；使用换货单号作为可追踪且幂等的分配批次标识。
- 客户绑定沿用当前换货规则，完成时从旧资产迁移到新资产，不受 `migrate_data` 开关影响。
- `migrate_data=false` 时不迁移旧资产钱包余额、套餐使用记录、累计充值/首充数据和普通业务标签。
- `migrate_data=true` 时继续迁移上述既有资料；钱包、套餐、累计字段和标签迁移必须与归属同步使用同一事务。
- 新资产钱包余额等目标数据存在冲突时沿用现有安全校验，不覆盖或吞掉冲突数据。

### 完成用例与幂等

- 物流换货在确认完成时执行继承；直接换货在创建并立即完成的同一事务中执行。
- Application 用例先锁定换货单、旧资产和新资产，再基于锁内最新状态重新校验；不得只信任创建或发货阶段的旧校验结果。
- 推荐顺序为：锁定并校验 → 同步新资产归属/租户字段 → 写分配记录 → 迁移客户绑定 → 按开关迁移资料 → 更新新旧资产状态 → 完成换货单 → 写 Audit Event。
- 任一步失败回滚整笔事务，不允许留下已改归属但未完成换货、已迁移钱包但状态失败等半成品。
- 完成状态使用条件更新。相同换货单重复提交完成时返回幂等成功，不重复执行归属更新、客户迁移、资金/套餐迁移、分配记录或审计副作用。
- 并发完成只能有一个执行者取得状态推进权；其他请求读取已完成结果后按幂等成功处理。
- Exchange Domain 保存资产类型、状态、归属和迁移不变量；Application 负责编排事务与端口。旧 Service 仅可作为转发门面，不保留第二套完成逻辑。
- 关键 Audit Event 必须与业务事务同成同败，记录换货单、新旧资产、原归属、继承归属、状态变化、`migrate_data` 和操作人。

### API 与前端

- 复用换货创建、发货和完成接口，不增加目标店铺参数或单独归属接口。
- `ExchangeOrderResponse` 增加 `inherited_shop_id` 和 `inherited_shop_name`；创建、发货、列表和详情中语义一致。
- 完成接口可保持现有成功外层结构；前端成功后重新读取详情获得最终继承结果。
- 列表返回店铺名称时必须批量加载，禁止逐条查询。
- 选择旧资产后展示其当前店铺；选择新资产后展示只读文案“换货完成后将归属：{店铺名称}”。平台库存目标使用明确的平台文案。
- 不提供目标店铺选择控件。其他店铺资产被拒绝时展示后端统一错误，并保留表单。
- 创建、发货确认和详情页面覆盖加载中、失败、重复提交和成功刷新状态。
- 更新 OpenAPI、UR#98 中文总结和 README 索引。

### 发布与迁移

- 新的分配类型常量定义在统一常量包并带中文注释；若数据库存在枚举或约束，迁移同步扩展 `exchange`。
- 为分配记录建立足以按换货单号和资产追踪的索引或约束，确保同一换货单不会重复记录同一新资产。
- 发布前核查新资产现存钱包租户标签、资源标签租户字段和分配记录结构，列出无法自动校正的异常数据。
- API、Worker 和前端在维护窗口同步切换；不对历史已完成换货批量改归属。

## Testing Decisions

- Domain 测试覆盖新资产平台库存、同店铺、其他店铺以及旧资产平台库存四种归属组合。
- 事务集成测试覆盖卡和设备、物流和直接换货、`migrate_data` true/false。
- `migrate_data=false` 验证归属、多租户字段、分配记录和客户绑定更新，但钱包余额、套餐、累计字段、普通业务标签不迁移。
- `migrate_data=true` 验证全部既有资料与归属在同一事务完成。
- 验证旧资产店铺不变，新资产归属状态与 `shop_id` 一致，其他店铺资产返回 403 且无副作用。
- 在每个关键步骤注入失败，验证资产、绑定、钱包、套餐、标签、分配记录、换货状态和 Audit Event 全部回滚。
- 并发与重复完成测试验证只生成一条分配记录、一次资金/套餐迁移和一组业务审计。
- HTTP 集成测试穿过真实认证、权限、Handler、Application 和 PostgreSQL；Redis 使用开发配置，外部 Gateway 不参与本需求。
- 前端人工验收创建、发货、完成、详情的继承提示及其他店铺拒绝。

## Out of Scope

- 不允许前端或调用方选择目标店铺。
- 不覆盖其他店铺资产的归属。
- 不把旧资产回收到平台，也不删除旧资产店铺历史。
- 不因归属继承而无条件迁移钱包、套餐、累计数据或普通业务标签。
- 不回填历史已完成换货的归属数据；异常历史数据另行核对。
- 不改变换货资产类型限制、物流流程或客户绑定业务规则。

## Further Notes

- “归属继承”与“资料迁移”是两个独立维度：前者始终执行，后者受 `migrate_data` 控制。
- UR#45 提供规范的新旧资产快照，UR#86 使用完成后的换货单生成前代/后代链路。
