# 02 — 提供新旧资产独立搜索的换货列表契约

**What to build:** 运营人员在换货列表中可以分别提交 `old_asset_keyword` 和 `new_asset_keyword`，通过 IoT 卡的 ICCID、接入号、虚拟号或设备的虚拟号、IMEI、SN 搜索对应一侧的换货资产。两个关键词同时提交时按 AND 组合，并继续与状态、流程类型、创建时间、分页和现有店铺数据范围共同生效；旧通用 `identifier` 不再属于新接口契约。该列表能力包含接口发布、真实 PostgreSQL 性能证据和前端同维护窗口切换所需的完整验收契约，可独立交付。

**Blocked by:** None — can start immediately.

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

**架构通道:** 主通道为 Query。仅将本次明显复杂化的换货列表读取用例收口为查询能力，可直接使用 GORM、子查询或固定次数批量查询完成候选解析、权限过滤和 DTO 投影；不得让列表读取经过聚合根或执行写操作。

**完整业务边界:** 本票收口列表请求校验、候选资产解析、新旧资产主键过滤、权限、分页计数、排序、响应投影、错误转换、OpenAPI 发布、性能验证和前端联调验收说明。明确不迁移换货详情及其他读取接口，不创建换货聚合根，不修改换货写侧状态规则，不保留通用 `identifier` 的第二套长期搜索语义；不实现 UR#86 资产前代/后代关系或 UR#98 店铺继承。当前仓库未包含可实施该页面的前端工程，因此前端工作以接口契约和人工验收清单交付，不虚构前端代码改动。

- [x] 列表请求新增最长 100 字符的 `old_asset_keyword` 和 `new_asset_keyword`，并从新契约移除通用 `identifier`；空值不增加对应过滤条件。
- [x] 仅提供旧资产关键词时只按 `old_asset_type + old_asset_id` 过滤，绝不因新资产命中而返回；仅提供新资产关键词时规则对称。
- [x] 两个关键词同时提供时按 AND 组合，并与状态、流程类型、创建时间范围和分页条件按 AND 组合。
- [x] IoT 卡候选支持对 ICCID、接入号和虚拟号做包含匹配；设备候选支持对虚拟号、IMEI 和 SN 做包含匹配；候选资产必须排除软删除记录。
- [x] 换货单通过资产类型和资产主键命中，因此历史非规范快照不回填、不改写，但仍能通过所关联资产的任一受支持标识搜索到。
- [x] 无候选资产或无换货单命中时返回成功的空分页；候选查询或换货单查询发生数据库错误时返回脱敏 500，不得降级为空结果。
- [x] 最终换货单查询继续排除软删除记录并应用现有店铺数据范围；平台、超级管理员和代理账号的既有可见范围不被候选资产解析绕过。
- [x] 候选解析和换货单过滤使用数据库子查询或固定次数批量查询，不按换货单逐行反查资产；`total` 与 `items` 使用完全相同的过滤条件，结果按创建时间倒序。
- [x] Handler 对完整请求 DTO 执行校验；非法关键词长度、分页、状态、流程类型或时间参数统一返回 HTTP 400、`code=1001`、`msg=参数验证失败`，详细原因仅记录中文日志。
- [x] HTTP 集成测试覆盖仅旧关键词、仅新关键词、双关键词 AND、状态与时间组合、空参数、无匹配、非法参数、历史快照、数据库故障和各类账号数据权限。
- [x] 响应保持统一外层结构，并继续分别返回新旧资产类型、ID、快照标识、状态及状态名称。
- [x] OpenAPI 中的换货列表只公开 `old_asset_keyword` 和 `new_asset_keyword`，包含长度限制、AND 语义和中文说明，不再公开通用 `identifier`；重新生成文档并验证请求、响应和错误契约与实现一致。
- [x] 使用真实 PostgreSQL 和代表性大结果集验证查询次数固定、无逐行资产反查、`total` 与分页结果一致，并记录查询计划或等价证据；性能满足项目列表接口目标。
- [x] UR#45 中文功能总结补充搜索契约、权限与脱敏错误边界、性能结果、发布回滚方式和前端联调注意事项；README 中的 UR#45 索引可以定位该说明。
- [x] 前端人工验收清单明确要求：将单一资产输入框拆为旧资产和新资产输入框；空值不提交；双条件按 AND 提交；表格不混列新旧资产；前端不解析标识、不本地过滤当前页；清空、分页、空态和失败反馈沿用现有交互。
