feat: 资产套餐历史增加主子层级查询

This commit is contained in:
2026-09-07 17:17:15 +08:00
parent c7c2b17d78
commit 696120ab38
18 changed files with 1030 additions and 236 deletions

View File

@@ -1,20 +0,0 @@
## Context
套餐使用记录已有 `master_usage_id`,不能为展示重复存储关系。
## Decisions
- 查询批量读取资产全部套餐使用,按 `master_usage_id` 内存分组并稳定排序。
- 不改变套餐状态、金额或生命周期;缺失主记录只形成读模型异常项。
## 查询与响应契约
- 后台资产详情及 H5 `GET /api/c/v1/asset/package-history` 保持既有资产权限、分页和套餐字段;不新增写接口、迁移、关联表或状态变更。查询需在资产范围内一次读取该资产全部相关 `PackageUsage`,不能按每个主套餐逐条查询加油包。
- 响应主项包含原套餐使用字段、`children` 加油包数组和 `expand_by_default`;主项 `master_usage_id` 必须为 `null`。子项保留自身 `package_usage_id``master_usage_id`、状态、购买创建时间、生效时间和原历史字段。
-`master_usage_id IS NULL` 的记录为主套餐;关联存在的加油包嵌入对应主项。已生效加油包先按生效时间正序,待生效加油包后按购买创建时间正序;排序字段相同再按 `package_usage_id` 正序,保证后台和 H5 一致。
- 主套餐只要有一个关联子项即 `expand_by_default=true`;无子项为 `false`。主套餐、子项失效、过期、用尽、退款或历史状态均不得删除或改写层级。
- 加油包的 `master_usage_id` 指向物理不存在记录时,返回顶层异常项:保留原字段、`relationship_status=master_missing``relationship_status_name=关联主套餐缺失``children=[]``expand_by_default=false`;不得猜测替代主套餐或丢弃该项。
## Verification
验证多主套餐、待生效/失效加油包、缺失主记录、后台/H5 数据范围和分页。

View File

@@ -1,24 +0,0 @@
## Scope
- 迭代编号:`AUG26-013`
## Why
资产套餐历史平铺展示,无法识别主套餐与其加油包的真实关联。
## What Changes
- 在后台和 H5 按既有套餐使用关联投影主套餐—加油包层级。
- 保留失效历史;主套餐物理缺失作为可观察异常。
## Capabilities
### New Capabilities
- 无。
### Modified Capabilities
- `package-lifecycle`: 套餐历史层级展示。
## Impact
影响套餐使用查询、后台资产详情、H5 和 OpenAPI。

View File

@@ -1,16 +0,0 @@
## ADDED Requirements
### Requirement: 资产套餐层级投影
系统 SHALL 在后台资产详情和 H5 资产套餐历史中,直接以既有 `PackageUsage.master_usage_id` 将主套餐和关联加油包投影为层级结构,不新增关联表。响应中每个主套餐必须返回 `expand_by_default`:存在至少一个关联加油包时为 `true`,否则为 `false`;后台与 H5 使用同一规则。加油包按生效时间正序;待生效加油包按购买创建时间正序并排在已生效包之后。主套餐或加油包失效、过期、用尽时仍保留层级;仅关联主套餐物理缺失时作为“关联主套餐缺失”异常独立项返回。
#### Scenario: 已失效加油包
- **WHEN** 某加油包及其主套餐已失效但主套餐记录仍存在
- **THEN** 系统仍将加油包嵌入该主套餐层级,不因状态失效拆散关系
#### Scenario: 主套餐含加油包
- **WHEN** 主套餐关联至少一条加油包使用记录
- **THEN** 后台和 H5 返回该主套餐时均将 `expand_by_default` 设为 `true`
#### Scenario: 主套餐物理缺失
- **WHEN** 加油包关联的主套餐使用记录不存在
- **THEN** 系统返回带“关联主套餐缺失”标识的异常独立项

View File

@@ -1,7 +0,0 @@
## 1. 查询契约
- [ ] 1.1 追踪后台资产详情、H5 套餐历史、`PackageUsage``master_usage_id` 查询链路。
- [ ] 1.2 实现批量层级投影、稳定排序和缺失主套餐异常项;更新 DTO、路由说明和 OpenAPI。
## 2. 验证
- [ ] 2.1 验证多层级、失效保留、待生效排序、缺失主记录及后台/H5 数据范围。
- [ ] 2.2 运行 `gofmt -w``go build ./cmd/api ./cmd/worker``go run cmd/gendocs/main.go``openspec validate add-asset-package-hierarchy --strict``openspec doctor --json`;自动化测试按项目决策为 N/A。

View File

@@ -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` 和切顶层页。异常独立项以自己的创建时间参与顶层排序,每项独立计数;子项数大于页大小时仍完整随组返回。保留后台 50100、H5 20100 的默认/最大页大小,以及后台 `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 变化,不做数据回滚。不在本次规划更新中执行构建、生成器或任何业务调用。

View File

@@ -0,0 +1,33 @@
## Scope
- 迭代编号:`AUG26-013`
- 需求依据:`docs/product/2026-08-迭代-PRD-讨论稿.md` 的 AUG26-013、§2.14`111.md` 仅用于原始追溯。
- 开工检查提出的整组分页与筛选、不可展示父项失败处理、空生效时间排序方案已确认,纳入本 Change。
## Why
资产套餐历史平铺展示,无法识别主套餐与其加油包的真实关联。
## What Changes
- 在后台资产详情和 H5 套餐历史按既有 `master_usage_id` 投影主套餐—加油包层级,返回 `children``expand_by_default`,保留失效、过期、用尽及退款历史关系。
- **BREAKING**:分页单位由单条使用记录改为顶层关系组或物理缺失主项的异常独立项;`total` 改为筛选后的顶层项数量。子项不独立计数、不跨页;保留两端既有分页参数、页大小限制及响应壳字段名。
- **BREAKING**:现有状态/套餐类型条件在同一条记录上联合匹配,组内任一记录命中即选择整组,返回可见范围内完整父子,不再逐条筛掉关联项。后台仍不新增套餐类型筛选。
- **BREAKING**:历史响应从平铺项改为层级项,普通主项显式返回 `master_usage_id=null`;后台与 H5 消费者需同步适配,不保留并行平铺接口。
- 主记录物理缺失时返回既定异常独立项;父记录存在但因资产、世代或软删除范围不可展示时,整个查询返回统一读取错误,不越界补显、不静默丢项、不伪报物理缺失。
- 顶层按创建时间倒序、ID 倒序;子项按有生效时间的非待生效项、无生效时间的非待生效项、待生效项分桶排序,各桶同时间按使用记录 ID 正序。
- 后台继续读取当前解析资产的全部世代H5 继续限定资产当前世代;保留既有资产授权入口、字段可见性、商品查询失败处理和软删除商品类型筛选口径。
## Capabilities
### New Capabilities
- 无。
### Modified Capabilities
- `package-lifecycle`: 套餐历史层级、整组分页与筛选、排序、关系异常和读取兼容契约。
## Impact
- 影响 `GET /api/admin/assets/{identifier}/packages``GET /api/c/v1/asset/package-history` 的读取投影、历史专用 DTO、路由说明与 OpenAPI以及后台和 H5 消费者。
- 两端共享同一只读关系投影,批量读取使用记录与商品;不逐主套餐查询子项。当前套餐及套餐修改接口的响应不随公共 DTO 被动变化。
- 不新增写接口、关联表、Schema 迁移或外部依赖;不修改套餐状态、金额、生命周期及未触碰的权限体系。不将存量企业授权风险作为本期顺带修复。

View File

@@ -0,0 +1,94 @@
## ADDED Requirements
### Requirement: 资产套餐层级投影
系统 SHALL 在后台资产详情套餐列表和 H5 资产套餐历史中,直接依据既有 `master_usage_id` 返回主套餐与关联加油包的层级,不新增关联表。普通主项 SHALL 显式返回 `master_usage_id=null``children` 数组和 `expand_by_default`;存在至少一个可展示关联子项时默认展开,否则不展开。子项 SHALL 保留自身使用记录 ID、原主记录 ID、状态、购买创建时间、生效时间及该入口既有历史字段。正常项 SHALL 省略关系异常字段,所有叶子 SHALL 返回 `children=[]``expand_by_default=false`。主套餐或加油包失效、过期、用尽、退款均 MUST NOT 拆散既有关系或改写其生命周期事实。
#### Scenario: 多个主套餐分别关联子项
- **WHEN** 同一可见资产范围内有多个主套餐,各自关联不同加油包
- **THEN** 每个加油包仅出现在其原主套餐的 `children` 中,不在顶层重复出现;有子项的主套餐默认展开
#### Scenario: 无子项的主套餐
- **WHEN** 某主套餐没有可展示关联加油包
- **THEN** 该主项返回 `master_usage_id=null``children=[]``expand_by_default=false`,不返回关系异常字段
#### Scenario: 已失效历史关系
- **WHEN** 主套餐或关联加油包已失效、过期、用尽或退款,但使用记录仍处于本入口可展示范围
- **THEN** 响应保留原主子关系及各自原状态,不因历史状态将子项拆为顶层记录
### Requirement: 套餐历史整组筛选与分页
系统 SHALL 将一个主套餐及其全部可展示子项作为一组,每组占一个分页名额;主记录物理缺失的每条异常独立项也各占一个名额。`total` SHALL 表示筛选后、分页前的顶层项数量,子项不另计数且 MUST NOT 跨页。已有 `status` 和套餐类型筛选条件 SHALL 在同一条使用记录上联合匹配;组内任一记录满足全部条件即选中整组,返回其完整可展示父子,不因筛选拆关系。异常独立项仅以自身字段匹配。未传筛选条件时 SHALL 返回范围内全部组及异常独立项。
#### Scenario: 子项数量超过页大小
- **WHEN** 一个主套餐含三个子项,另有一个无子项主套餐,以 `page_size=1` 查询且无筛选
- **THEN** `total=2`;包含三个子项的组在同一页完整返回,子项不消耗额外分页名额,另一页只返回另一个主项
#### Scenario: 状态或类型仅命中子项
- **WHEN** 主项不满足筛选条件,但某个子项满足该请求的全部筛选条件
- **THEN** 该组被选中,返回主项及其全部可展示子项,包括未命中条件的同组记录;主项不被标记为缺失
#### Scenario: 联合条件不能分摊给不同记录
- **WHEN** 请求同时指定状态和套餐类型,组内只有主项满足状态、只有子项满足类型,且没有任何一条记录同时满足二者
- **THEN** 该组不进入结果,也不计入 `total`
#### Scenario: 异常独立项计数
- **WHEN** 筛选后有一个正常关系组和两条主记录物理缺失的加油包
- **THEN** `total=3`,两条异常项各自占一个分页名额,即使它们指向同一个缺失主记录也不合并
#### Scenario: 无匹配与超出末页
- **WHEN** 没有任何顶层项匹配,或请求页码超出筛选结果末页
- **THEN** `items=[]`;无匹配时 `total=0`,超出末页时仍返回筛选后的真实顶层项总数
### Requirement: 套餐历史稳定排序
系统 SHALL 将顶层项按自身创建时间倒序排列,同时间按 `package_usage_id` 倒序排列。每组子项 SHALL 依次分为三桶:非待生效且有生效时间的记录按生效时间正序;非待生效但生效时间为空的记录按购买创建时间正序;待生效记录按购买创建时间正序并排最后。各桶排序时间相同 SHALL 按 `package_usage_id` 正序。桶归属及排序 MUST NOT 改变记录状态;后台与 H5 对相同输入集合 SHALL 返回相同顺序。
#### Scenario: 混合生效时间与历史状态
- **WHEN** 同组包含有生效时间的已用完记录、无生效时间的已失效记录和待生效记录
- **THEN** 三者依次位于第一、第二、第三桶;已失效记录保持已失效状态,不被重新标记为待生效
#### Scenario: 同时间的确定顺序
- **WHEN** 多个顶层项创建时间相同,或同桶子项排序时间相同
- **THEN** 顶层按使用记录 ID 倒序,子项按使用记录 ID 正序;数据不变时重复请求及相邻页边界保持一致
### Requirement: 物理缺失与不可展示关系区分
系统 SHALL 仅在 `master_usage_id` 指向的主使用记录物理不存在时返回顶层异常独立项,保留原字段及非空主记录 ID并返回 `relationship_status=master_missing``relationship_status_name=关联主套餐缺失``children=[]``expand_by_default=false`。主记录未出现在筛选结果或某一页 MUST NOT 被认定为物理缺失。范围内的子项所指父记录实际存在但处于其他资产、H5 其他世代或已软删除而不可展示时,整个查询 SHALL 返回统一读取错误,不返回部分成功列表,不越界补显、不静默丢项,也不返回物理缺失标识。商品记录缺失 MUST NOT 等同于主使用记录缺失。
#### Scenario: 主套餐物理缺失
- **WHEN** 加油包所引用的主使用记录物理不存在
- **THEN** 返回带既定 `master_missing` 标识及中文名称的异常独立项,保留原主记录 ID不猜测替代主套餐
#### Scenario: 父子原本分处明细分页两侧
- **WHEN** 父子记录都在当前入口可展示范围,但按旧明细分页会落在不同页,或父项不满足展示筛选
- **THEN** 系统仍按真实关系返回同一个完整关系组,不将子项标记为主套餐缺失
#### Scenario: 主使用记录软删除或范围不可见
- **WHEN** 范围内某子项的主使用记录仍物理存在,但已软删除、属于其他资产,或不属于 H5 当前世代
- **THEN** 整个查询返回统一读取错误;不暴露父项内容或具体不可见原因,不静默丢项,不返回 `master_missing`
#### Scenario: 商品缺失但使用关系存在
- **WHEN** 主使用记录及其子项均可展示,但关联套餐商品已软删除或物理缺失
- **THEN** 主子关系保持不变,历史名称沿用既有快照回退规则,不因商品缺失返回关系异常
### Requirement: 套餐历史读取范围与兼容边界
系统 SHALL 保留各入口既有资产解析及授权前置校验后台读取当前解析资产的全部世代H5 通过当前客户资产绑定校验后仅读取资产实体当前世代卡按卡载体、设备按设备载体读取MUST NOT 为补齐关系而合并其他资产或扩张 H5 世代。后台 SHALL 继续只支持已有状态筛选H5 SHALL 继续支持状态和套餐类型筛选H5 类型匹配 SHALL 保留排除已软删除商品的既有口径,不将用于历史展示的软删除商品自动作为类型命中项。
系统 SHALL 保留后台默认页大小 50、最大 100 和 `items/total/page/page_size` 响应字段H5 默认页大小 20、最大 100 和 `items/total/page/size` 响应字段。两端仍接收 `page/page_size`,页码小于 1 时归一为 1页大小小于 1 时使用各自默认值,超过上限时截断。除明确变更的层级、分页和筛选语义外,系统 SHALL 保留各入口既有套餐字段和可见性后台成本价仅平台类账号可见H5 MUST NOT 因共享投影而新增填充原先未填的订单、退款、成本价、零售价或生效条件字段。当前生效套餐及修改套餐接口的响应 MUST NOT 被本次历史层级变更连带改变。
#### Scenario: 两端保留各自世代范围
- **WHEN** 同一资产有多个世代的独立主子组后台有权查看该资产H5 客户具有有效绑定
- **THEN** 后台可返回全部世代内的组H5 只返回资产当前世代内的组,不为对齐两端数量而扩大或缩小范围
#### Scenario: 未通过既有资产授权
- **WHEN** 代理请求其店铺范围外的资产,或 H5 客户请求非其有效绑定资产
- **THEN** 沿用既有拒绝行为,不返回该资产任何主项或子项,也不因补查主记录绕过授权
#### Scenario: 字段可见性与其他接口不变
- **WHEN** 平台、代理和 H5 客户分别读取其可见资产历史,并调用原有当前套餐或修改套餐接口
- **THEN** 历史主子项均遵守各入口既有字段可见性;代理与 H5 不因层级投影获取平台成本价,其他套餐接口保持原响应结构
#### Scenario: 已软删除商品不成为类型筛选命中项
- **WHEN** H5 按套餐类型筛选,组内只有已软删除商品对应的使用记录具有该类型
- **THEN** 该记录不使整组命中;若组内另一条记录满足全部条件而选中该组,已软删除商品对应的可展示使用记录仍随组保留
#### Scenario: 商品批量读取失败的既有差异
- **WHEN** 关联商品批量读取发生数据库错误,而非单纯商品记录不存在
- **THEN** 后台沿用使用记录快照继续投影的既有行为H5 沿用读取失败响应,不因共享关系投影统一为另一入口的行为

View File

@@ -0,0 +1,27 @@
## 1. 查询契约与共享投影
- [x] 1.1 依据 design 的开工检查证据核对后台套餐列表、H5 套餐历史、`PackageUsage` 及 DTO 引用范围确认实施时调用链仍符合既有授权、后台全部世代、H5 当前世代及入口字段差异。
- [x] 1.2 在 `internal/query/asset/package_history.go` 实现既有资产/世代范围内一次读取全部未软删除使用记录;按 `master_usage_id` 建立完整关系,去重后一次最小存在性核对未解析主 ID区分物理缺失异常项与存在但不可展示时的整体读取错误禁止逐主项查询子项。
- [x] 1.3 实现同一成员联合匹配状态/类型条件并选择整组,保持 H5 类型匹配排除软删除商品的既有口径;按规格实现三桶子项排序、顶层稳定排序、顶层 `total` 和整组分页。
## 2. 历史响应与两端接入
- [x] 2.1 在现有 DTO 文件增加历史专用层级节点及列表响应,落实普通主项显式 `master_usage_id=null``children` 空数组、默认展开及既定缺失标识;不改变当前套餐和修改套餐接口的公共响应契约。
- [x] 2.2 将后台 `Service.GetPackages` 接入共享 Query保留 Handler 资产解析、状态入参校验、全部世代、50100 页大小、`page_size` 响应字段、平台成本字段及后台商品查询失败快照策略。
- [x] 2.3 将 H5 `GetPackageHistory` 接入同一 Query保留客户有效绑定、资产实体当前世代、20100 页大小、`size` 响应字段、原历史字段及商品查询失败响应;不得因复用补出订单、退款或金额字段。
- [x] 2.4 更新两条历史路由的响应类型、整组分页/筛选和错误说明,批量复用商品加载;仅在构造依赖变化时同步 bootstrap、`cmd/api/docs.go``cmd/gendocs/main.go` 装配,删除被本次接入替代的旧明细分页与重复关系逻辑。
## 3. 隔离环境验收
- [x] 3.1 在隔离环境对两条历史入口执行多主项、无子项、多子项及子项数超过页大小的只读 smoke核对不跨页、不重复、顶层 `total`、异常项独立计数、空结果及超出末页行为。
- [x] 3.2 验证仅子项命中、主项命中、联合条件不能由不同成员分摊、无匹配及软删除商品类型资格场景,确认选中整组并保留完整可展示父子,不因筛选误报主项缺失。
- [x] 3.3 验证主子失效、过期、用尽及退款关系保留;核对三桶顺序、非待生效但生效时间为空、待生效排序及顶层/子项相同时间的确定顺序,不修改原状态。
- [x] 3.4 验证主使用记录物理缺失、软删除、跨资产、H5 跨世代、商品缺失及存在性查询失败分别遵守异常/整体错误/历史回退契约;范围内不可展示关系不因分页或筛选被掩盖,不返回范围外内容及具体不可见原因。
- [x] 3.5 验证卡/设备载体边界、后台全部世代与 H5 当前世代差异、代理店铺范围和 H5 绑定拒绝、主子项金额字段隔离;核对当前套餐及修改接口的既有响应不变,不把存量企业授权问题算作本期已修复。
- [x] 3.6 核对两端相同输入集合的层级顺序一致、两种分页响应壳不变、商品加载失败策略保留;通过隔离环境查询记录确认 usage 一次读取、未解析主 ID 至多一次存在性核对、商品/类型资格批量加载,查询次数不随主项数线性增长。
- [x] 3.7 核对后台和 H5 消费者同步适配层级、默认展开、顶层总数、整组筛选及整体读取错误,完成消费者联调并确认 API 与消费者可同步发布/回滚;未取得联调结果不得宣称展示验收完成。
## 4. 工程验证
- [x] 4.1 对变更 Go 文件运行 `gofmt -w`,运行 `go build ./cmd/api ./cmd/worker``go run cmd/gendocs/main.go`;核对生成 OpenAPI 与两条历史接口实际 JSON 一致,当前套餐和修改接口未被连带改变。
- [x] 4.2 运行 `openspec validate add-asset-package-hierarchy --strict``openspec doctor --json`,汇总上述 smoke 与兼容验收结果;自动化测试按项目决策为 N/A不恢复旧测试不运行迁移或真实外部业务调用。