feat: 资产套餐历史增加主子层级查询
This commit is contained in:
@@ -0,0 +1,102 @@
|
||||
## Context
|
||||
|
||||
动机及兼容影响见 `proposal.md`;可观察契约及验收条件以本 Change 的 `specs/package-lifecycle/spec.md` 为准。开工检查后三项方案已确认,本设计不保留影响实施的未决问题。
|
||||
|
||||
| 当前事实 | 代码位置 | 对设计的约束 |
|
||||
| --- | --- | --- |
|
||||
| 后台按载体读取全部世代,状态筛选后内存排序、计数、切页 | `internal/service/asset/service.go:766-881`、`internal/store/postgres/package_usage_store.go:288-301` | 保留载体与世代范围,不复用旧明细分页结果组装关系 |
|
||||
| H5 按客户绑定资产当前世代查询,状态和类型筛选后数据库明细分页 | `internal/handler/app/client_asset.go:117-175,467-554,590-612` | 在完整当前代集合上建立关系,不能使用当前页判断父项缺失 |
|
||||
| 使用记录包含 `gorm.Model`、`master_usage_id`、可空生效时间和世代 | `internal/model/package.go:62-98` | 区分物理缺失、软删除和范围外存在,明确空时间排序 |
|
||||
| 商品可批量含软删除读取;H5 类型筛选仍排除已软删除商品 | `internal/store/postgres/package_store.go:129-138`、`internal/handler/app/client_asset.go:498-500` | 展示商品集合不等于类型筛选命中集合 |
|
||||
| 公共套餐 DTO 同时供历史、当前套餐和修改接口使用 | `internal/model/dto/asset_dto.go:135-168`、`internal/routes/asset.go:42-73` | 使用历史专用节点,避免对公共 DTO 的修改扩大响应变化 |
|
||||
| 数据权限在 Store 显式过滤,不是自动 GORM 权限插件 | `internal/bootstrap/bootstrap.go:60-69`、`pkg/middleware/data_scope.go:23-33` | 必须保留调用入口授权,不能仅传递 context 就声称有资产隔离 |
|
||||
|
||||
## Goals / Non-Goals
|
||||
|
||||
**Goals:**
|
||||
- 将主子归属、关系异常、整组筛选、排序、计数和分页收口到一个只读 Query;两端相同范围输入得到相同关系结果。
|
||||
- 明确两端范围和字段投影差异,不以统一实现为由统一权限、金额字段或失败策略。
|
||||
- 主项数量增长不增加逐组数据库查询;完整组不跨页。
|
||||
|
||||
**Non-Goals:**
|
||||
- 不修改套餐状态、激活、退款、失效、流量或金额计算,不新增 Schema、关联表、写接口、外部调用或依赖。
|
||||
- 不将卡及其关联设备合并为新资产范围,不扩大 H5 历史世代,不缩小后台历史世代。
|
||||
- 不重构其他套餐视图或当前套餐/修改套餐接口,不在本 Change 修复存量企业授权问题。
|
||||
|
||||
## Decisions
|
||||
|
||||
### 1. 一个共享关系 Query,保留两端字段映射
|
||||
|
||||
在现有 `internal/query/asset` 下新增 `package_history.go`,负责批量读取使用记录和关系投影。输入须来自已经通过既有入口校验的资产类型、资产 ID、可选世代范围,以及该入口已有筛选/分页参数;资产类型只接受已解析的卡或设备,不能让未知载体退化为无范围查询。后台不传世代限制,H5 传资产实体当前世代。
|
||||
|
||||
Query 返回选中顶层关系节点及 `total`,节点保留原使用记录供入口投影。后台 `Service.GetPackages` 与 H5 `GetPackageHistory` 消费同一关系规则;已有商品批量加载和字段映射保留入口差异,不在两个入口各自实现分组、排序或分页。复用 `PackageStore.GetByIDsUnscoped`,不逐节点加载商品。
|
||||
|
||||
拒绝在后台和 H5 分别写 assembler:两端会再次出现筛选及异常判断漂移。也不复用 `CustomerViewService.GetMyUsage`:该视图只取部分状态、只保留单个主项且逐使用记录加载商品,不适合多主套餐历史。
|
||||
|
||||
### 2. 关系查询先于展示筛选和分页
|
||||
|
||||
```text
|
||||
入口认证及资产校验
|
||||
→ 固定资产/世代范围,一次读取未软删除的全部使用记录
|
||||
→ 建立使用记录 ID 索引和 master_usage_id 关系
|
||||
→ 对未解析主 ID 一次批量最小存在性核对
|
||||
→ 形成完整关系组及物理缺失异常项
|
||||
→ 同一成员联合匹配筛选,选中整组
|
||||
→ 顶层与子项排序,计算顶层 total,切顶层页
|
||||
→ 各入口批量商品补充与字段投影,保留各自响应壳
|
||||
```
|
||||
|
||||
不将 `status` 或展示分页施加到关系基础集合。H5 类型筛选的匹配资格另按既有未软删除商品查询口径取得,只需当前资产使用记录引用的商品 ID;不能把 `Unscoped` 商品展示映射直接作为类型命中资格。组内任一条记录同时满足所有请求条件才使组命中,不允许父项匹配状态、子项匹配类型来拼成一次联合命中。
|
||||
|
||||
基础 usage 查询一次;未解析主 ID 去重后至多一次存在性查询;类型资格和商品补充均批量查询。空 ID 集合不发查询,数据库读取次数不随主项数量线性增长。不是按主项循环查询子项,也不是先切明细页再补父项。
|
||||
|
||||
### 3. 物理缺失判定不扩大展示权限
|
||||
|
||||
基础集合沿用原未软删除 usage 范围。仅对其中真实持久化引用、但不在索引中的主 ID,执行一次含软删除的最小存在性核对;只选择判定所需的 ID、载体、世代和删除标记。不得使用客户端任意父 ID 批量探测,不得读取或返回范围外主套餐业务内容。
|
||||
|
||||
- 全局物理不存在:创建既定 `master_missing` 异常独立项。
|
||||
- 物理存在但不在允许展示集合:整个查询经既有全局 ErrorHandler 返回统一读取错误,不返回部分列表,不透露是其他资产、其他世代还是软删除,也不静默丢掉子项。不能借 `Unscoped` 将软删除主项重新展示。
|
||||
- 存在性核对失败:按读取失败处理,不将失败当成不存在。
|
||||
- 仅商品不存在:沿用名称快照等历史回退,不影响 usage 关系判定。
|
||||
|
||||
关系完整性在展示筛选及切页前判定;范围集合内存在不可展示父项时,不因该子项恰好不在请求页或不命中筛选而掩盖错误。代价是异常数据会阻断本次历史读取,但这比泄露范围外数据或返回错误关系更安全。
|
||||
|
||||
### 4. 排序、分页和响应结构
|
||||
|
||||
严格执行 delta spec 的三桶子项排序,不能用 `priority` 或创建时间倒序替代生效时间规则。待生效以状态判断,生效时间为空的非待生效记录进入独立中间桶,不重写其状态。顶层同创建时间按 ID 倒序,子项同桶同时间按 ID 正序,排序比较条件构成确定顺序。
|
||||
|
||||
先选组再计算 `total` 和切顶层页。异常独立项以自己的创建时间参与顶层排序,每项独立计数;子项数大于页大小时仍完整随组返回。保留后台 50/100、H5 20/100 的默认/最大页大小,以及后台 `page_size`、H5 `size` 响应差异。
|
||||
|
||||
历史专用节点保留该入口原有套餐字段,普通主项显式输出 `master_usage_id=null`,不继承旧字段 `omitempty` 导致缺失的行为;空子数组始终为 `[]`。正常节点省略关系异常字段,叶子不展开;只有物理缺失项输出既定异常状态及中文名称,不新增其他异常独立项状态。不要直接扩展公共 `AssetPackageResponse` 而使当前套餐及修改接口也出现层级字段。
|
||||
|
||||
### 5. 字段与错误兼容
|
||||
|
||||
- 后台继续仅向平台类账号填充 `paid_amount`,按现状保留其他历史字段;H5 不因复用而补出原先未填的订单、退款、金额和生效条件字段。主项和子项执行相同入口字段策略。
|
||||
- 商品名称优先使用 usage 快照,空快照再回退到商品名称。商品软删除仍可用于历史展示,不等于 usage 软删除或主记录物理缺失。
|
||||
- 后台商品批量读取失败仍沿用原先告警并继续快照投影的路径;H5 同类错误仍返回失败。usage 读取、类型资格查询或主记录存在性核对失败不适用后台商品降级路径。
|
||||
- 查询仅保留既有授权机制,不声称因此修复了企业授权隔离。后台角色允许企业账号而当前套餐链未见企业授权过滤,是静态检查发现的存量风险,未做账号运行验证;另行处理,不增加本期修复任务。
|
||||
|
||||
### 6. 文件归属
|
||||
|
||||
| 文件 | 本期职责 |
|
||||
| --- | --- |
|
||||
| `internal/query/asset/package_history.go`(实施时新增) | 共享关系读取、批量存在性核对、类型匹配资格、整组筛选、排序、计数和分页 |
|
||||
| `internal/service/asset/service.go` | 后台接入 Query,保留资产范围、平台成本字段和商品错误策略 |
|
||||
| `internal/handler/app/client_asset.go` | H5 接入 Query,保留客户绑定、当前世代、历史字段和统一分页响应 |
|
||||
| `internal/model/dto/asset_dto.go`、`internal/model/dto/client_asset_dto.go` | 历史专用层级节点和列表响应;不改变其他接口公共字段契约 |
|
||||
| `internal/routes/asset.go`、`internal/routes/personal.go` | 更新历史响应类型、分页筛选描述及异常说明 |
|
||||
| `internal/bootstrap/`、`cmd/api/docs.go`、`cmd/gendocs/main.go` | 仅在 Query 注入/构造依赖变化时同步真实装配及两套文档装配 |
|
||||
|
||||
## Risks / Trade-offs
|
||||
|
||||
- [分页与筛选不向后兼容] → proposal 明确标记 BREAKING;后台和 H5 消费者须同步适配,不以保留字段名冒充语义兼容。
|
||||
- [全量关系集合与大组增加内存/响应体] → 只读取当前入口已有资产范围;商品和存在性核对保持批量、最小字段。页大小约束顶层项而非子项,不能截断子项破坏契约。
|
||||
- [误判缺失或越界补显] → 未解析主 ID 只做内部最小存在性核对,存在但不可展示时整体失败;不返回范围外业务数据及具体原因。
|
||||
- [共享 DTO 泄漏字段或影响无关接口] → 使用历史专用节点,保留入口字段投影,核对当前套餐和修改接口响应不变。
|
||||
- [同时存在多个读取失败策略] → 明确只有后台商品补充读取可沿用既有快照降级,关系事实查询不得降级成缺失。
|
||||
|
||||
## Migration Plan
|
||||
|
||||
无需数据库迁移或数据回填。实现完成后,在隔离环境按 delta spec 对两条历史入口做只读 smoke,核对实际 JSON、页边界、授权拒绝和查询次数;按项目决定不恢复自动化测试。
|
||||
|
||||
发布前同步消费者对层级结构、顶层 `total`、整组筛选、默认展开和整体读取错误的处理,核对 OpenAPI 与实际响应一致。生产发布由维护者按既有运行说明执行。回滚时 API 与对应消费者一起退回原平铺契约;由于没有本期写入或 Schema 变化,不做数据回滚。不在本次规划更新中执行构建、生成器或任何业务调用。
|
||||
Reference in New Issue
Block a user