97 lines
6.7 KiB
Markdown
97 lines
6.7 KiB
Markdown
# 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 保证完成换货后的归属一致;本需求只负责只读关系投影。
|