# PRD：UR#45 换货资产标识与新旧资产独立搜索

Status: done（后端）— 前端/人工待验收

---

## Problem Statement

换货单中的卡资产标识没有统一：旧资产可能保存虚拟号，新资产可能原样保存操作员输入的 ICCID、接入号或虚拟号，导致同类记录展示不一致。现有列表又只有一个同时匹配新旧资产快照的 `identifier` 参数，无法明确查“旧资产”还是“新资产”，也无法稳定通过卡的其他标识找到对应换货单。

## Solution

新建换货单时，将卡的新旧资产标识统一规范为 ICCID；设备使用稳定的设备号。换货列表用 `old_asset_keyword` 和 `new_asset_keyword` 替代通用 `identifier`，先将关键词解析为候选卡/设备 ID，再按换货单的新旧资产主键筛选。两个参数可独立使用，同时提供时按 AND 组合。

## User Stories

1. 作为运营人员，我希望换货列表分别展示旧资产和新资产，避免混淆换出和换入对象。
2. 作为运营人员，我希望通过 ICCID、接入号或虚拟号搜索卡对应的旧资产换货记录。
3. 作为运营人员，我希望用相同标识搜索新资产，但不会误命中旧资产。
4. 作为运营人员，我希望同时填写新旧资产条件，以定位一条明确的换货关系。
5. 作为维护人员，我希望新换货单保存规范化快照，历史展示不再取决于操作员当时输入了哪种标识。
6. 作为维护人员，我希望大结果集查询不逐条反查资产，也不绕过换货单数据权限。

## Implementation Decisions

### 快照规范

- 卡资产的 `old_asset_identifier` 和 `new_asset_identifier` 均保存已解析卡记录的完整 ICCID，不保存请求原文、接入号或虚拟号。
- 设备资产优先保存虚拟号；虚拟号为空时依次使用 IMEI、SN，确保保存稳定的设备标识。
- 规范化覆盖物流换货和直接换货：创建旧资产快照、直接换货的新资产快照、物流发货时的新资产快照均使用同一解析能力。
- 请求仍可使用接口当前支持的任一资产标识定位资产，但保存快照必须取解析后的权威字段。
- 历史换货单不回填、不改写；历史列表继续按原快照展示，新的搜索能力通过资产主键关联命中历史记录。

### 列表接口

- 复用 `GET /api/admin/exchanges`。
- 新增可选 `old_asset_keyword` 和 `new_asset_keyword`，各自最长 100 个字符。
- 移除新契约中的通用 `identifier`；前后端在同一维护窗口切换，不再把一个参数解释为“新资产或旧资产”。
- 单独提供旧资产关键词时只过滤 `old_asset_type + old_asset_id`；单独提供新资产关键词时只过滤 `new_asset_type + new_asset_id`。
- 两个关键词同时提供时按 AND 组合，并继续与状态、流程类型、创建时间和分页条件按 AND 组合。
- 空参数不增加对应条件；无候选资产或无换货单命中时返回成功空分页，不返回 404。
- 卡关键词对 ICCID、接入号、虚拟号做包含匹配；设备关键词对虚拟号、IMEI、SN 做包含匹配。
- 候选资产查询必须排除软删除资产；换货单查询继续排除软删除记录并应用现有店铺数据范围。
- 候选 ID 解析和换货单过滤使用固定次数的批量查询或数据库子查询，不允许按换货单逐行读取卡或设备。
- `total` 与 `items` 必须使用完全相同的过滤条件，排序继续使用创建时间倒序。
- 响应继续分别返回新旧资产类型、ID、快照标识、状态及状态名称，不改变统一响应外层结构。

### 校验、权限与架构

- 列表 Handler 对整个请求 DTO 执行校验；非法长度、分页、状态、流程类型或时间参数统一返回 HTTP 400、`code=1001`、`msg=参数验证失败`，详细原因仅写日志。
- 代理账号只能查询其现有店铺范围内的换货单；关键词解析不得扩大最终换货单范围。
- 无权限和不存在的换货单继续使用现有防枚举错误策略，不向客户端暴露候选资产数量或主键。
- 快照规范属于换货写用例的一部分，应由统一资产解析/规范化能力提供，不在多个 Handler 或 Service 分支复制。
- 列表属于读取用例，可在现有 Store 上做简单增量；若候选解析和分页组合已达到复杂查询程度，则收口到 Exchange Query，但不得为此迁移整个换货模块。
- 数据库或候选查询失败返回脱敏的统一内部错误，不得降级为“无结果”。

### 前端与文档

- 换货列表筛选区将单一资产搜索拆为“旧资产”和“新资产”两个输入框。
- 空值不提交；两个非空值同时提交并展示 AND 查询结果。
- 表格分别显示旧资产类型/标识和新资产类型/标识；卡直接展示后端 ICCID，设备展示后端设备号。
- 前端不把接入号或虚拟号转换为 ICCID，也不在本地过滤当前页。
- 沿用列表加载、空态、失败反馈和分页交互。
- 更新 OpenAPI 参数及描述，重新生成文档；新增 UR#45 中文总结并更新 README 索引。

## Testing Decisions

- 写用例测试分别用 ICCID、接入号、虚拟号创建卡换货，验证新旧快照始终为数据库 ICCID。
- 设备测试覆盖虚拟号、IMEI、SN 输入以及标识优先级。
- 列表 HTTP 集成测试覆盖仅旧关键词、仅新关键词、两个关键词 AND、与状态/时间组合、空参数、无匹配和非法参数。
- 覆盖卡 ICCID、接入号、虚拟号以及设备虚拟号、IMEI、SN 的候选映射。
- 构造历史非规范快照，验证不回填但仍可通过资产主键搜索命中。
- 代理、平台和超级管理员测试验证现有换货单数据范围不被关键词绕过。
- 使用真实 PostgreSQL 执行代表性大结果集测试，验证查询次数固定、无逐行反查，计数和分页结果一致并满足项目性能目标。
- 验证数据库错误返回脱敏 500，不能返回空列表掩盖故障。
- 前端人工验收新旧字段不混列、组合搜索、清空、分页和错误状态。

## Out of Scope

- 不回填或清洗历史换货快照。
- 不新增独立搜索接口或换货关系表。
- 不改变换货状态机、归属继承、资料迁移或完成规则。
- 不做跨新旧资产 OR 搜索；旧通用 `identifier` 不保留为第二套长期语义。
- 不由前端解析或规范化资产标识。

## Further Notes

- UR#45 负责“快照规范与列表检索”；资产详情前代/后代关系由 UR#86 提供，店铺继承由 UR#98 提供。
- UR#86 应直接使用这里形成的不可变快照；历史非规范快照保持原样。
- 后端实现、OpenAPI、中文文档及自动化验证已由提交 `55bdc3a` 交付；当前仓库不含前端源码，页面实施与浏览器人工验收仍待前端仓库完成。
