Files
junhong_cmp_fiber/.scratch/ur86-asset-exchange-trace/PRD.md
2026-07-21 15:26:07 +09:00

97 lines
6.7 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# PRDUR#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 保证完成换货后的归属一致;本需求只负责只读关系投影。