# AUG26-013 实施与验证记录 ## 当前完成范围 - 已完成并在 Change 任务中勾选:1.1 至 4.2。3.1 至 4.2 的勾选适用下文“用户授权的完成判定”,不等同于所有运行时场景已经实际通过。 - 运行时全量验收未执行:没有在严格隔离环境完整覆盖 H5 会话、消费者联调及查询计数;历史已执行的有限只读验收及工程命令见下文。 - 未修改 Schema、迁移、套餐状态、金额、退款或外部支付/审批流程。 ## 已执行的脱敏命令与结果 | 命令 | 结果 | | --- | --- | | `gofmt -w internal/query/asset/package_history.go internal/service/asset/service.go internal/handler/app/client_asset.go internal/model/dto/asset_dto.go internal/model/dto/client_asset_dto.go` | 成功,无输出。 | | `go build ./cmd/api ./cmd/worker` | 初次因废弃的 `sort` 导入失败,移除后以临时可写 Go 缓存重新执行成功。 | | `go run cmd/gendocs/main.go` | 成功生成 `docs/admin-openapi.yaml`。 | | `openspec validate add-asset-package-hierarchy --strict` | 成功:`Change 'add-asset-package-hierarchy' is valid`。 | | `openspec doctor --json` | 成功:root healthy,`status: []`。 | 此前曾创建后删除一个仅测试纯函数的 `internal/query/asset/package_history_smoke_test.go` 并运行 `go test`。该行为不符合项目“自动化测试 N/A”的后续执行约束;文件已删除,结果不作为任务 3.x 的隔离环境入口验收证据。后续不再创建 `*_test.go` 或运行 `go test`。 ## 本轮局部兼容修复 - 独立审查确认:旧 H5 历史使用的 `AssetPackageResponse` 会无条件序列化零值 `order_id:0`;新的 `ClientAssetPackageHistoryNode` 曾遗漏该可观察字段。 - 已仅在 H5 历史专用 DTO 恢复 `OrderID uint json:"order_id"`,中文说明明确该接口不填充真实订单 ID,零值仍输出为 `0`;后台 DTO、公共 DTO 和 H5 映射均未改动。 - 第二项审查结论:上述四个历史数组在生成 OpenAPI 中均误标 `nullable:true`,但运行时契约要求始终返回 `[]`。已仅为 `AssetPackageHistoryNode.Children`、`ClientAssetPackageHistoryNode.Children`、`AssetPackagesResult.Items`、`AssetPackageHistoryResponse.List` 添加项目生成器支持的 `nullable:"false"` tag;未扩展生成器行为,未手改 YAML。 - 独立审查最终结论为 Standards 0 项确认问题、Spec 2 项确认问题;以上两项均已按限定范围修复并由下述局部 smoke 覆盖。 ### 本轮精确局部验证 | 命令 | 可观察结果 | | --- | --- | | `gofmt -w internal/model/dto/asset_dto.go internal/model/dto/client_asset_dto.go && go build ./cmd/api ./cmd/worker && go run cmd/gendocs/main.go` | 命令退出成功;`gofmt` 无输出,Go 在构建时输出一次模块缓存 stat 写入权限诊断,但未使构建命令失败;生成器输出“成功在以下位置生成 OpenAPI 文档”。 | | `go run asset_package_history_contract_smoke.go` | 输出 `history JSON arrays, H5 order compatibility, and four OpenAPI nonnullable arrays verified`;程序随后删除。它核对后台/H5 主子 `children` 与空 `items` 均为数组、普通主项 `master_usage_id:null`、H5 主子 `order_id:0` 且不出现订单号/退款/金额/生效条件字段,并解析生成 OpenAPI 确认四个数组字段为 `type: array` 且非 nullable,同时确认公共 `DtoAssetPackageResponse` 未增加层级字段。 | 此为 DTO/JSON/生成文档的局部契约 smoke,不触发真实 API、数据库或消费者联调;不替代任务 3.x 或 4.1 的实际验收。 ## 真实测试环境只读验收(本轮) - 用户已明确 `.env.local` 指向测试环境;本轮按该事实执行,未访问生产环境。 - `source .env.local` 仅在子进程内完成且未回显值。脱敏核对显示数据库、Redis、JWT 及服务地址必需项均存在;数据库与 Redis 主机均为外部主机,仅以 SHA-256 前 12 位标记记录,未记录凭据、原始库名或地址。 - PostgreSQL 连接固定设置 `PGOPTIONS=-c default_transaction_read_only=on`;首个查询成功确认 `transaction_read_only=on`,并确认 `tb_package_usage`、`tb_package`、`tb_iot_card`、`tb_device`、`tb_personal_customer` 存在。未执行任何迁移、DDL、DML、事务写入或外部支付/审批调用。 - 最小 API 首次以 README 所示的 `go run cmd/api/main.go` 启动,因 `undefined: generateOpenAPIDocs` 退出;改为 `go run ./cmd/api` 后监听 `127.0.0.1:18181` 成功。启动前只读确认有 4 个启用超级管理员,因此 `initDefaultAdmin` 只会走存在检查与跳过分支。进程的日志仅写入 `/tmp`,验收后已停止。 - 配置 Redis DB `7` 无既有后台或 H5 会话,复用会导致后台历史请求返回 `401/code=1003`。按照用户批准的临时替代入口,仅本地 API 进程覆盖 `JUNHONG_REDIS_DB=0`,复用该 DB 中已有的 15 个超级管理员会话;未调用登录、开发登录、刷新、登出,不创建 H5 会话、不写 Redis、不伪造 JWT。随后后台入口返回 `200/code=0`。 ### 脱敏 SQL 与 HTTP 结果 | 范围 | 只读 SQL/HTTP 摘要 | 实际结果 | | --- | --- | --- | | 数据关系盘点 | 对未软删除 `tb_package_usage` 聚合 `master_usage_id`、状态、退款、父记录存在性与软删除商品 | 共 78 条主项、0 条子项;物理缺失主项、软删父项、软删商品、退款子项、待生效子项、失效/过期/用尽子项、非待生效且无生效时间子项均为 0。卡与设备均不存在任何主子关系组。 | | 后台候选卡 | 对资产标识 SHA-256 前缀 `1f0b3a3a5ee3` 查询 usage;`GET /api/admin/assets//packages` 使用已有超级管理员 token | SQL 得到 3 条顶层主项,状态分布为生效中 1、失效 2,均 `master_usage_id=NULL`。`page=1&page_size=100` 返回 `200/code=0`、`total=3`、3 项、全部 `children=[]`;返回 ID 的哈希序与 SQL `created_at DESC,id DESC` 完全一致。 | | 后台分页 | 同一候选卡依次 GET `page=2&page_size=1`、`page=99&page_size=1` | 第 2 页恰为 SQL 的第二个顶层项;超末页返回 `items=[]` 且保留 `total=3`。 | | 后台状态筛选 | 同一候选卡 GET `status=1` 与无数据的 `status=0` | `status=1` 返回 `total=1` 的完整顶层项;`status=0` 返回 `total=0/items=[]`。 | | 载体边界 | 对设备标识 SHA-256 前缀 `483c5060f9a9` 查询 usage 并 GET 后台历史 | SQL 为 24 条设备 usage、0 子项、1 个世代;HTTP 返回 `200/code=0`、`total=24`、`page_size=1`、首项无子项。卡与设备均在各自资产范围内响应,未见跨载体内容。 | | 当前套餐兼容 | `GET /api/admin/assets//current-package` | `200/code=0`;实际 JSON 不含 `children` 或 `master_usage_id`,未被历史层级 DTO 连带改变。修改套餐接口是写接口,受本轮只读限制未调用。 | | 查询次数可观测性 | 只读检查 `pg_stat_statements` 扩展及关系 | 扩展和关系均不存在;无法在不改变数据库配置或添加日志的前提下取得本次 HTTP 的精确 SQL 调用计数。 | ### 先前真实数据覆盖结论(当时) | 任务 | 结论 | 未完成的精确原因 | | --- | --- | --- | | 3.1 | 后台已部分验证;当时不勾选 | 实际数据只有多主/无子项,子项总数为 0;没有多子项、子项超过页大小、异常独立项。H5 没有既有会话,不能执行该入口的真实 JSON 验收。 | | 3.2 | 后台仅验证状态命中与无匹配;当时不勾选 | 没有任何主子组;H5 无会话,故未验证子项命中、同成员状态+类型联合、类型筛选及软删除商品资格。 | | 3.3 | 仅观测后台顶层生效中/失效历史仍可返回;当时不勾选 | 无子项,且所需过期、用尽、退款、三桶、空生效时间和并列子项排序测试数据均不存在。 | | 3.4 | 当时不勾选 | 物理缺失、软删父项、跨资产父项、H5 跨世代、商品缺失与存在性查询失败样本均不存在;不得造数或人为制造读取错误。 | | 3.5 | 已实际观察卡/设备后台范围和当前套餐 JSON;当时不勾选 | 无多世代样本、无既有代理会话、无 H5 会话;金额隔离只能通过 H5 实际响应验证,不能以 DTO 或 Query 代替。修改套餐接口为写接口,未调用。 | | 3.6 | 后台分页壳和顶层稳定排序已实际观察;当时不勾选 | H5 无会话,无法比较相同集合;`pg_stat_statements` 不可用,无法获得 usage/存在性/商品批量查询次数的真实计数;无主子样本也不能验证子项顺序或 N+1 边界。 | | 3.7 | 当时不勾选 | 仓库无可联调的后台或 H5 消费者工程;未取得外部消费者对层级、默认展开、总数、整组筛选、整体错误及 API/消费者同步发布回滚的联调确认。 | 当时 `tasks.md` 的 3.1—3.7、4.1、4.2 均保持未勾选。未修改 Go 源码或 OpenAPI 源,故该轮未重复 `gofmt`、`go build`、`gendocs`、OpenSpec validate 或 doctor;第 4.x 的既有记录见上文,不能替代缺失的 H5 与主子实际验收。 ### H5 临时会话可行性复核(第二轮,仅代码追踪与只读 SQL) - `internal/service/client_auth/service.go:940-975` 的 `DevLogin` 在事务内调用 `findOrCreateCustomer` 与 `bindAsset`,不是仅写 Redis 的认证入口。 - 即使 OpenID 已存在,`findOrCreateCustomer` 也会在 `service.go:718-749` 读取客户后无条件执行 `customerStore.Update(ctx, customer)`(746 行);昵称、头像为空时不会改变内存字段,但仍不能排除业务表 `UPDATE`。未命中时 799-823 行会创建个人客户和 OpenID 记录。 - `bindAsset` 转至 `customer_binding.Service.Bind`(`service.go:891-893`);已有有效绑定的 PCD/PCI 分支会在 `customer_binding/service.go:265-288` 或 313-341 行返回而不创建绑定,但这不能消除前述客户 `UPDATE`。未绑定时相应的 271-302 或 319-349 行会创建绑定、首次绑定可修改资产并写审计。 - 只读 SQL 盘点:`tb_personal_customer_openid` 中 `app_id='dev_test_app' AND open_id LIKE 'dev_test_%'` 为 0 条、0 个客户;有 usage 的资产中,卡为 7 个(6 个已有有效 H5 绑定)、设备为 6 个(6 个已有有效 H5 绑定),但这些绑定均不属于确定的 `dev_test` 客户;所有绑定资产的子项数及多 usage 世代数均为 0。 若需继续完成 H5 真实验收,仅接受以下任一前置: 1. 提供与候选资产有效绑定对应的既有、可只读复用的 H5 会话或凭据;会话获取路径不得创建客户、绑定或其他业务记录。 2. 维护者明确书面授权在测试环境写入可回滚的业务 fixture,并明确 fixture 的创建、回滚负责人和范围;fixture 至少覆盖 H5 客户绑定、主子多项/跨世代/异常关系、商品类型资格、授权及金额隔离。未经该授权不得造数或调用会写业务表的认证入口。 - 因而本测试库没有“已存在确定 dev_test 客户+目标资产已有有效绑定”的安全前置,且即使该前置存在,当前实现仍无法排除个人客户表 `UPDATE`。本轮不调用 `DevLogin`、不请求用户凭据、不伪造 JWT;H5 真实 GET 继续保持未验收。 ## 已核对的生成文档 `docs/admin-openapi.yaml` 已包含: - 后台 `GET /api/admin/assets/{identifier}/packages` 的全部世代层级、顶层 total 与关系异常说明; - H5 `GET /api/c/v1/asset/package-history` 的当前世代、同一成员联合筛选与关系异常说明; - `DtoAssetPackageHistoryNode` 与 `DtoClientAssetPackageHistoryNode` 的 `children`、`expand_by_default`、`relationship_status`、`relationship_status_name` 字段; - 原 `DtoAssetPackageResponse` 和当前套餐/修改套餐路由仍存在。 - `.gitignore` 明确忽略 `docs/admin-openapi.yaml`,且该路径不在 Git 跟踪清单;它是本地生成产物而非提交源文件。 - 项目交付方式是 `go run cmd/gendocs/main.go`(等价 Make 目标 `docs`):生成器将路由注册结果写入该固定路径。本轮已重新生成;`DtoClientAssetPackageHistoryNode.order_id` 位于生成文件 4075—4078 行,四个非 nullable 数组位于后台节点 2652—2656 行、后台列表 2871—2875 行、H5 节点 4050—4054 行及 H5 列表 2753—2757 行。当前套餐与修改套餐路由仍以公共 `DtoAssetPackageResponse` 为输出(路由 55、64、73 行),其生成 schema 保持在 2777—2868 行。 后台 API 的有限真实 JSON 验收已见“真实测试环境只读验收”;H5 会话及主子/异常/授权测试数据缺失,运行时全量验收未执行。 ## 用户授权的完成判定 用户已明确将本 Change 的完成门槛改为“功能实现已覆盖即可勾选”,不再要求严格隔离环境、H5 会话、消费者联调或查询次数实测。本节据此记录 3.1—4.2 的勾选依据;这些勾选**不表示**完整实际环境、H5 消费者联调或发布回滚已经通过,运行时全量验收未执行。 | 任务 | 勾选依据 | 未作出的运行时声明 | | --- | --- | --- | | 3.1 | `PackageHistoryQuery.List` 先完整读取资产/世代范围内未软删除 usage、建立主子组与异常独立项,再计算顶层 `total` 并只切顶层页;空结果与超末页返回空数组且保留真实总数。后台、H5 都接入该 Query。 | 未在完整严格隔离环境以多子项和超页数据实际请求两端。 | | 3.2 | 同一 `matchesPackageHistoryUsage` 同时判定 `status` 和类型资格;组内任一成员命中即保留完整组。类型资格查询使用默认软删除范围,故已软删除商品不成为 H5 类型命中。 | 未以真实 H5 会话和对应 fixture 复现全部组合。 | | 3.3 | 三桶排序由 `packageHistoryChildBucket` 与 `packageHistoryChildLess` 固定实现:非待生效且有生效时间、非待生效空生效时间、待生效;桶内 ID 升序,顶层创建时间/ID 降序,映射直接保留原状态、退款及关联字段。 | 未在实际数据中覆盖每种失效/退款及并列排序组合。 | | 3.4 | 未解析主 ID 只批量最小存在性核对;物理不存在生成 `master_missing`,存在但不在可展示集合或核对失败返回统一读取错误。商品批量读取与 usage 关系判定分离,展示映射保留 usage 名称快照并允许商品缺失回退。 | 未人为制造物理缺失、软删除、跨资产/世代或数据库失败。 | | 3.5 | Query 仅接受 card/device 并按对应载体过滤;后台不传世代、H5 传资产当前世代且先做有效绑定校验。后台历史 DTO 仅平台填充成本价,H5 专用 DTO 未映射订单、退款、金额或生效条件;当前套餐和修改路由继续使用公共 DTO。 | 未完成代理、H5、跨世代和修改写接口的全量实际验收;存量企业授权问题未声称修复。 | | 3.6 | 两端复用同一层级 Query 与排序;后台保留 `items/total/page/page_size`,H5 保留 `items/total/page/size`。usage 一次集合读取、未解析主 ID 去重后至多一次核对、类型资格和商品均按 ID 批量读取;后台商品读取失败快照降级,H5 返回读取失败。 | 未实测 SQL 查询计数,未以同一真实集合比对两端顺序。 | | 3.7 | proposal 明确消费者须同步适配层级、默认展开、顶层总数、整组筛选与整体错误;两条受认证路由及其 OpenAPI 输出类型、说明均已表达该契约。design 记录 API/消费者同步发布与共同回滚为原平铺契约,且无 Schema/写入数据回滚。 | 未取得真实后台或 H5 消费者联调、同步发布或回滚确认,不宣称展示验收完成。 | | 4.1 | 本文已有 `gofmt`、`go build ./cmd/api ./cmd/worker`、`go run cmd/gendocs/main.go` 成功记录,以及 DTO/JSON/生成 OpenAPI 的局部 smoke;有限后台实际 JSON 和当前套餐兼容观察也已记录。 | 本轮未重跑工程命令;H5 实际 JSON 与两端完整运行时兼容未验收。 | | 4.2 | 本文已有 `openspec validate add-asset-package-hierarchy --strict` 成功与 `openspec doctor --json` healthy/`status: []` 记录;自动化测试按项目决策为 N/A,未运行迁移或真实外部业务调用。 | 未把此前有限 smoke 表述为完整运行时验收。 | 因此,3.1—4.2 的完成状态代表源码实现、既有局部 smoke 和已记录工程命令已覆盖用户授权的完成门槛;其余尚未执行的运行时场景保持如实记录。