# PRD:UR#86 资产详情前代与后代换货标识 Status: ready-for-agent --- ## Problem Statement 卡和设备详情目前无法说明当前资产是否由换货产生、是否已经换出,也无法从 A→B→C 的连续换货中查看当前资产的前代和后代。运营只能人工搜索换货单,且直接返回关联资产主键会造成越权跳转风险。 ## Solution 在统一资产详情响应中增加 `exchange_trace`,基于已完成换货单分别查询当前资产作为新资产时的前代,以及作为旧资产时的后代。关联资产仍在当前权限范围内时返回可跳转 ID;无权限时保留不可变换货快照标识,但隐藏内部 ID 并禁止跳转。 ## User Stories 1. 作为运营人员,我希望看到当前资产是否是换货后的新资产,并查看其前代。 2. 作为运营人员,我希望看到当前资产是否已经换出,并查看其后代。 3. 作为运营人员,我希望链路中间资产同时显示前代和后代。 4. 作为受限账号,我希望仍能理解换货历史,但不能借此跳转或枚举无权限资产主键。 5. 作为维护人员,我希望直接复用换货单,不再维护一张可能与换货状态不一致的关系表。 ## Implementation Decisions ### 响应契约 - 复用 `GET /api/admin/assets/resolve/{identifier}`,在 `AssetResolveResponse` 中增加稳定的 `exchange_trace` 对象。 - `exchange_trace` 包含 `previous_asset` 和 `next_asset`;不存在对应关系时字段为 null,不省略整个对象。 - 单个关联项包含 `asset_type`、可空 `asset_id`、`identifier`、`exchange_no`、`can_view`。 - `asset_type` 使用换货模型现有枚举 `iot_card` 或 `device`。 - `previous_asset` 来自“当前资产是新资产”的已完成换货单,返回该单旧资产快照。 - `next_asset` 来自“当前资产是旧资产”的已完成换货单,返回该单新资产快照。 - 只使用 `status=已完成` 且未软删除的换货单;待填写、待发货、已发货待确认和已取消均不形成资产链。 - A→B→C 时,B 同时返回 A 和 C。若异常历史数据在同一方向存在多条完成记录,选择完成时间与主键顺序最新的一条并记录异常日志。 - `identifier` 使用换货单保存的不可变快照,不回查当前资产可变字段;UR#45 上线后的卡快照为 ICCID,历史快照保持原样。 ### 权限与防泄露 - 当前资产仍必须先通过资产详情现有数据权限;无权查看当前资产时维持现有不存在/禁止访问行为。 - 找到前代或后代关系后,分别按关联资产当前数据权限判断 `can_view`。 - 有权查看关联资产时返回真实 `asset_id`、`can_view=true`,前端可跳到对应卡或设备详情。 - 无权查看关联资产时返回 `asset_id=null`、`can_view=false`,但按标准评审稿保留换货单快照 `identifier` 和 `exchange_no`,前端仅作历史文本展示。 - 不返回关联资产店铺、客户、套餐、钱包、状态等额外信息。 - 不以“查不到受权限过滤的资产”抹掉换货关系;关系查询与关联资产可见性检查分开完成。 - 超级管理员、平台和代理继续使用现有资产权限规则,本需求不建立新的角色矩阵。 ### Query 与索引 - 这是跨换货单和资产权限的读取用例,收口到 Asset/Exchange Query,直接使用 GORM/DTO 投影,不经过聚合根。 - 复用换货单作为关系权威来源,不新增换货链表或冗余前代/后代字段。 - 分别提供“按新资产查已完成换货”和“按旧资产查已完成换货”的批量查询能力;资产详情一次完成,不逐节点递归整条历史链。 - 新增两个非唯一部分 B-tree 索引:已完成且未软删除范围内的旧资产类型+旧资产 ID,以及新资产类型+新资产 ID;索引支持选择最新记录。 - 卡和设备走同一 Query,不为两种资产复制查询逻辑。 - 关联资产权限检查采用固定次数批量加载;详情增加换货信息后不得产生按列表或绑定卡数量增长的 N+1。 - 查询故障返回统一脱敏内部错误,不得把故障当作“没有换货关系”。 ### 前端与文档 - `previous_asset` 存在时显示“换货新资产”标签、前代标识和换货单号。 - `next_asset` 存在时显示“已换出旧资产”标签、后代标识和换货单号。 - 两者同时存在时同时展示,不互相覆盖。 - `can_view=true` 且 `asset_id` 非空时允许点击进入关联卡或设备详情;否则只显示文本,不渲染链接或可点击样式。 - 点击目标是关联资产详情,不直接跳换货单;换货单号仅作辅助信息。 - 不根据当前资产状态、generation 或前端本地数据自行推导关系。 - 更新资产详情 OpenAPI、UR#86 中文总结和 README 索引。 ### 发布与回滚 - 索引随维护窗口上线,回滚只删除新增索引和响应字段,不修改换货单业务数据。 - 不回填历史换货快照;上线前抽样核验历史新旧资产 ID 和标识快照完整性,并记录无法展示的异常。 - 与 UR#45、UR#98 同窗发布时,先完成数据库索引和写侧规范,再发布 Query 与前端。 ## Testing Decisions - Query 测试覆盖无关系、仅前代、仅后代、A→B→C 中间资产,以及卡/设备两种资产。 - 状态测试证明只有已完成换货进入链路,其他状态和软删除记录均忽略。 - 权限测试覆盖关联资产可见与不可见:不可见时仍有快照文本,但 ID 为 null 且不可跳转。 - 当前资产无权限时维持现有防枚举响应,不能因为换货查询泄露其存在。 - 构造同方向异常多条完成记录,验证确定性选择最新记录并产生可观测日志。 - HTTP 集成测试穿过真实认证、权限、资产解析、Query 和统一响应,使用真实 PostgreSQL/Redis/JWT 配置。 - 验证新增索引的结构、向下/向上迁移和代表性查询计划;查询满足项目性能目标且无 N+1。 - 前端人工验收四类页面:无换货、换货新资产、已换出旧资产、中间资产;另验收无权限不可点击。 ## Out of Scope - 不返回整条递归换货树或新增换货链列表接口。 - 不新建关系表,不修改换货状态机。 - 不回填或改写历史快照。 - 不向无权限用户返回关联资产内部 ID或其他业务详情。 - 不直接跳转换货单,不在前端自行拼接关系。 - 不改变资产详情现有数据权限。 ## Further Notes - 标准评审稿明确无权限时保留标识、隐藏可跳转 ID;该口径优先于禅道草稿中未定义算法的“脱敏后标识”。 - UR#45 负责未来快照规范,UR#98 保证完成换货后的归属一致;本需求只负责只读关系投影。