6.7 KiB
6.7 KiB
PRD:UR#86 资产详情前代与后代换货标识
Status: ready-for-agent
Problem Statement
卡和设备详情目前无法说明当前资产是否由换货产生、是否已经换出,也无法从 A→B→C 的连续换货中查看当前资产的前代和后代。运营只能人工搜索换货单,且直接返回关联资产主键会造成越权跳转风险。
Solution
在统一资产详情响应中增加 exchange_trace,基于已完成换货单分别查询当前资产作为新资产时的前代,以及作为旧资产时的后代。关联资产仍在当前权限范围内时返回可跳转 ID;无权限时保留不可变换货快照标识,但隐藏内部 ID 并禁止跳转。
User Stories
- 作为运营人员,我希望看到当前资产是否是换货后的新资产,并查看其前代。
- 作为运营人员,我希望看到当前资产是否已经换出,并查看其后代。
- 作为运营人员,我希望链路中间资产同时显示前代和后代。
- 作为受限账号,我希望仍能理解换货历史,但不能借此跳转或枚举无权限资产主键。
- 作为维护人员,我希望直接复用换货单,不再维护一张可能与换货状态不一致的关系表。
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 保证完成换货后的归属一致;本需求只负责只读关系投影。