10 KiB
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. 关系查询先于展示筛选和分页
入口认证及资产校验
→ 固定资产/世代范围,一次读取未软删除的全部使用记录
→ 建立使用记录 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 变化,不做数据回滚。不在本次规划更新中执行构建、生成器或任何业务调用。