docs(员工代收款): 新增路由前缀修复治理变更并勾选 AUG26-017 门禁
All checks were successful
构建并部署到测试环境(无 SSH) / build-and-deploy (push) Successful in 1m30s

- 新增 OpenSpec Change fix-employee-collection-route-prefix,承载已落地的 ff1362d 路由前缀修复(无规格 delta,skip_specs)
- 记录根因(Register 的 basePath 只服务文档)、影响面(7 条根级残留、15 条同层抢占)、修复方式与验证方式
- AUG26-017 全局健康门禁实际通过后勾选 5.7(tasks 27/27)
This commit is contained in:
2026-09-11 15:38:05 +08:00
parent 7891189712
commit 5ee8e3cb4a
5 changed files with 135 additions and 1 deletions

View File

@@ -37,5 +37,5 @@
- [x] 5.4 验证线下字段:引用不存在或已停用收款方式被拒、收款方式快照在改名与停用后保持冻结、缺少交易流水号被拒、重复交易流水号不阻断线下申请、企业微信审批或钱包入账,且不改变 `payment_transaction_id``payment_channel`、支付状态、钱包或 Outbox 入账消费者的对账判定;支付凭证与其他凭证分别留存并可在审批详情取得。
- [x] 5.5 验证识别预填:图片凭证识别成功后返回预填值且未创建申请、人工更正值生效、非图片与识别失败不阻断人工填写、日志与审计不含凭证内容或识别原始结果。
- [x] 5.6 验证字典引用保护:被代理充值引用的字典项不可删除、编码不可修改、仅可停用。
- [ ] 5.7 执行 `gofmt -w``go build ./cmd/api ./cmd/worker``go run cmd/gendocs/main.go``./scripts/context-health.sh``openspec validate add-agent-self-recharge-payment-methods --strict``openspec doctor --json`;上述全局健康门禁必须一并通过,自动化测试按项目决策为 N/A。
- [x] 5.7 执行 `gofmt -w``go build ./cmd/api ./cmd/worker``go run cmd/gendocs/main.go``./scripts/context-health.sh``openspec validate add-agent-self-recharge-payment-methods --strict``openspec doctor --json`;上述全局健康门禁必须一并通过,自动化测试按项目决策为 N/A。验证:六项全部通过——`gofmt -l` 无输出、`go build` 退出码 0、gendocs 成功生成、`context-health.sh` 输出「Context 健康检查通过」退出码 0、`validate --strict` 输出 `Change 'add-agent-self-recharge-payment-methods' is valid``doctor --json``healthy = true`
- [x] 5.8 仅在本 Change fixture 已清理、且维护者确认新增收款方式、交易流水号与凭证快照没有留存需求时验证迁移 up/down/updown 可以删除新增列,但不得影响既有充值记录读取。存在仍需保留的真实新增列数据时不得将 down 视为正常可执行场景MUST NOT 重置整个 `junhong_cmp_test`

View File

@@ -0,0 +1,3 @@
schema: spec-driven
created: 2026-09-11
skip_specs: true

View File

@@ -0,0 +1,73 @@
## Context
`proposal.md`。归档 Change `2026-09-11-add-employee-collection-bills`(来源提交 `ce24d56`)新增员工代收款账单闭环,其路由注册把资源前缀写进了 `Register``basePath`,导致 12 处注册落到 `/api/admin` 根上。修复提交 `ff1362d`2026-09-11 14:40`fix(员工代收款): 修正路由前缀注册方式,消除 /api/admin 根级 /:id 抢占`)已推送 `origin/Iteration/8-11`,但当时没有对应的 OpenSpec Change主规格与 OpenAPI 文档描述的是 `/api/admin/employee-collection-*`,运行时却不是。本 Change 补上这条治理记录。
主规格侧的契约现在已显式化:证据链修复提交 `e687a26``补齐员工代收款账单能力证据链与可达操作索引`)在 `openspec/specs/employee-collection-bill/spec.md` 追加 `## 可达操作索引`,列出本次修复恢复的全部 12 条 `/api/admin/employee-collection-*` 路由,并同步 `docs/verification/context-reset/` 的 Requirement 证据与入口矩阵。本修复与该索引完全一致。
## Goals / Non-Goals
**Goals:**
- 用独立 Change 承载已落地的路由前缀修复,使缺陷、影响面、修复方式与验证方式可追溯。
- 明确本次修复不产生规格 delta 的依据,以及归档时的规格与证据链影响面。
**Non-Goals:**
- 不改变业务行为、handler/application/service/domain/查询/迁移、权限可见性、OpenAPI 契约与 `Register` 语义。
- 不承载证据链修复本身(`employee-collection-bill` 证据行、可达操作索引、入口矩阵,已由 `e687a26` 落地),也不改动任何主规格文件。
## Decisions
### 1. 根因:`basePath` 只服务文档,路由注册只用相对 path
`internal/routes/registry.go``Register` 第一行是 `router.Add(method, path, handler)``basePath` 只出现在 `doc != nil` 分支中拼接 `fullPath``pathParamRegex` 转换后写入文档)。缺陷版 `ce24d56` 把 admin 根路由组与相对 path 组合,`paymentMethodPath` / `billPath` / `applicationPath` 只传给了文档生成器。结果:文档路径正确,运行时路径缺少资源前缀。
Fiber 按注册顺序匹配且没有静态优先。最小复现:同一应用先注册 `GET /:id` 再注册 `GET /refunds``app.Test` 请求 `/api/admin/refunds` 命中参数路由;调换注册顺序后命中静态路由。这就是根级 `GET /api/admin/:id` 抢占后续同层静态 GET 的机制,也是 `registerEmployeeCollectionBillRoutes` 中「统计必须先于 `/:id` 注册」注释所反映的同一种顺序敏感性。
### 2. 影响面(离线路由表与真实路由实证)
离线路由表(真实 `RegisterAdminRoutes` + `openapi.BuildDocHandlers`,内存 Fiber 实例):缺陷版 356 条、修复版 358 条;差异恰为 `/api/admin` 根级减少 10 条栈条目7 条不同 method+path`/api/admin/employee-collection-*` 增加 12 条。
同层抢占清单(同方法下先注册的参数路由命中后注册的静态路由;原始命中 29 条,按「方法 + 被抢占静态路由」去重 15 条):
| 被抢占路由 | 归属能力 |
| --- | --- |
| `GET /api/admin/refunds``GET /api/admin/order-package-invalidate-tasks` | `order-refund-exchange` |
| `GET /api/admin/system-configs` | `system-operations` |
| `GET /api/admin/agent-recharges` | `agent-funds-commission` |
| `GET /api/admin/agent-self-recharge-payment-methods` | `agent-funds-commission`(由在途 AUG26-017 引入) |
| `GET /api/admin/asset-package-batch-orders` | `package-lifecycle` |
| `GET /api/admin/wecom` | `external-integration` |
| `PUT|DELETE /api/admin/{agent-self-recharge-payment-methods,agent-recharges,refunds,wecom}` | 与 GET 相同路径的同层写端点 |
真实 Fiber 路由请求复现(同样调用真实注册函数、装载真实 handler、nil 依赖):缺陷版 6 个命名 GET 全部 `400 / 1001 无效的路径ID``GET /api/admin/statistics` 落到账单统计处理器返回 `503 / 2004``GET /api/admin/employee-collection-bills` 也被根级 `/:id` 抢走返回 `400 / 1001`;修复版这些路径命中各自处理器(权限中间件在无会话上下文时返回 `403 / 1005``GET /api/admin/statistics` 返回 `404 / 1006`,员工代收款端点命中自身处理器。
### 3. 修复方式:资源前缀交给 `router.Group`
三个注册函数各 `router.Group("/<资源前缀>")`12 处 `Register` 首参改为该 group`basePath` 拼接保持原样(文档路径不变)。这是仓库既有写法(`internal/routes` 内 50 处 `router.Group(`),不存在第二种约定,因此不引入新的抽象或兼容层。
### 4. 为什么独立成 Change 而非并入 AUG26-017
- 归属:缺陷由 AUG26-001员工代收款账单闭环引入AUG26-017代理自充收款方式只是在同层注册了被抢占的端点。并入会把根因错误归到代理自充能力。
- 契约面:本修复不改 `agent-funds-commission` 的任何行为、Schema、审批语义或配置只恢复路由AUG26-017 的 proposal/design 已限定其影响面与非目标,混入路由修复会让该 Change 的验收与回滚边界失真。
- 发布边界:`ff1362d` 已独立落地并推送AUG26-017 仍在途且工作区未提交;合并会使已发布的干净提交与未完成改动耦合。
- 治理:并入会让本次修复继续缺少独立的 OpenSpec 记录,而它的价值正是让「实现回到已归档规格」这件事可追溯。
### 5. 规格 delta 决策:无 delta`skip_specs: true`
结论:本 Change 不产生任何 delta`.openspec.yaml` 标记 `skip_specs: true`
依据:
1. `openspec/specs/employee-collection-bill/spec.md` 只有 5 条行为 Requirement均不描述 HTTP 路径;路由由独立的 `## 可达操作索引` 承载(`e687a26` 已补齐 12 条 `/api/admin/employee-collection-*``scripts/context-health.sh` 明确禁止 `### Requirement: ...接口集合`。本修复让运行时路径与该索引一致,索引文本无需改动。
2. 规格侧唯一描述路径的产物是 `docs/admin-openapi.yaml`,缺陷版、修复版与当前工作区三处 gendocs 产物逐字节相同md5 均为 `274f45440597aaafb1e168ac266cbb85`),文档契约从未改变。
3. 被阻断的端点属于其它能力,其主规格本来就要求这些端点可用(例如 `GET /api/admin/refunds` 已在入口矩阵中登记为 `order-refund-exchange` 的 HTTP 入口)。修复是让实现符合既有契约,不是改变契约。
4. 若强行写 delta只能写出「路由注册必须使用资源前缀」这类实现约束或把路由目录写成 Requirement两者都违反 `AGENTS.md`「Requirement 必须描述可观察行为,不能用接口目录代替状态、权限、金额、失败与幂等语义」。
5. OpenSpec 官方为无规格变化的 Change 提供 `skip_specs` 逃生阀(本地实测 `openspec validate --strict``openspec validate --all` 均通过,见 `tasks.md`)。归档时该 Change 不触碰任何主规格文件。
## Risks / Trade-offs
- 任何按缺陷路径写死的调用方会在修复后 404。仓库源码、注释与 OpenAPI 文档均使用 `/api/admin/employee-collection-*`,未发现此类调用方;前端如有硬编码需按文档路径核对。
- 根因是「相对路径 + 无前缀注册」这类写法;本 Change 不修改 `Register` 语义、也不做全仓扫描修复,后续新增注册若再次省略前缀仍会复现同类缺陷。`registerEmployeeCollectionBillRoutes` 对注册顺序的依赖注释表明该模式本身脆弱。
- 无 delta 意味着规格历史不记录「路由修复」这一事件,治理信息落在本 Change 的 proposal/design/tasks。若维护者希望把路由布局写入主规格应通过独立 Change 在可达操作索引中登记(`e687a26` 已完成),而不是由本修复承担。
- 归档不会改动 `docs/verification/context-reset/*.json`:本 Change 不新增 Requirement 名,也不新增可达操作索引条目,因此 Requirement 键集与 HTTP 入口集不变。归档预演(`/tmp` 全量副本):`openspec archive` 输出 `archivedAs: 2026-09-11-fix-employee-collection-route-prefix``specsUpdated: false`24 个主规格文件 sha256 与 Requirement 名集合、可达路由集合全部不变;在包含本 Change 的副本上 `./scripts/context-health.sh` 输出「Context 健康检查通过」。

View File

@@ -0,0 +1,42 @@
## Why
员工代收款闭环(`ce24d56`,归档于 `2026-09-11-add-employee-collection-bills`)的三个注册函数把资源前缀写进了 `Register``basePath``basePath` 只参与 OpenAPI 文档拼接:`internal/routes/registry.go``Register``router.Add(method, path, handler)` 注册路由,只在 `doc != nil` 分支里把 `basePath + path` 拼成文档路径。因此 12 处注册实际落成的路径只剩相对段(`""``/:id``/statistics``/:id/close`),全部挂在 `/api/admin` 根上。
Fiber 按注册顺序匹配,没有静态路由优先。先注册的根级 `GET /api/admin/:id`(账单详情)因此抢占其后注册的同层静态 GET`PUT /api/admin/:id` 抢占同层 `PUT /api/admin/agent-self-recharge-payment-methods`;同时 `/api/admin` 根上出现 4 条可写端点(`POST /api/admin``PUT /api/admin/:id``DELETE /api/admin/:id``POST /api/admin/:id/close`),与 OpenAPI 契约和归档规格描述的操作路径不一致。
离线路由表与真实 Fiber 路由复现结果:
- 缺陷版 `/api/admin` 根级多出 10 条路由栈条目7 条不同 method+path`GET|POST /api/admin``GET /api/admin/:id`(重复注册 2 次)、`PUT /api/admin/:id``DELETE /api/admin/:id``GET /api/admin/statistics``POST /api/admin/:id/close`
- `GET /api/admin/refunds``GET /api/admin/system-configs``GET /api/admin/agent-recharges``GET /api/admin/agent-self-recharge-payment-methods``GET /api/admin/order-package-invalidate-tasks``GET /api/admin/asset-package-batch-orders` 全部命中账单详情处理器并返回 `400 / 1001 无效的路径ID`;这些端点分别属于 `order-refund-exchange``system-operations``agent-funds-commission``package-lifecycle` 的既有能力,`agent-self-recharge-payment-methods` 由在途 AUG26-017 引入。
- 员工代收款自身的账单详情/统计/关闭、核销申请创建/详情/重提、收款方式增删改在文档路径下不可达(`/api/admin/employee-collection-*` 在缺陷版不存在)。
## What Changes
- `internal/routes/employee_collection.go` 的三个注册函数各自引入 `router.Group("<资源前缀>")``/employee-collection-payment-methods``/employee-collection-bills``/employee-collection-applications`12 处 `Register` 首参由 admin 根路由组改为该 group`basePath` 与文档路径拼接保持原样。
- 修正后 `/api/admin` 根级不再有员工代收款路由:缺陷版 7 条全部消失12 条路由回到 `/api/admin/employee-collection-*`;同层抢占候选(同方法下先注册的参数路由命中后注册的静态路由)由缺陷版 29 条原始命中 / 15 条按「方法 + 被抢占静态路由」去重降为 0。
- 生成产物 `docs/admin-openapi.yaml` 逐字节不变(缺陷版、修复版与当前工作区 md5 均为 `274f45440597aaafb1e168ac266cbb85`),路径、方法、鉴权标记、中间件包装与描述文本均未改变。
## Capabilities
### New Capabilities
- 无。
### Modified Capabilities
- 无。本 Change 不改变任何可观察行为契约,属于对已归档规格的实现收敛:主规格 `employee-collection-bill` 的 5 条行为 Requirement 描述账单来源、余额状态、核销申请、审批幂等与字典/退款联动,从不描述 HTTP 路径被阻断的端点属于既有能力其主规格本来就要求它们可用。规格侧唯一描述路径的产物OpenAPI 文档)在修复前后逐字节相同。
## Non-Goals
- 不改变任何业务行为、金额、状态机、权限可见性与幂等语义;不新增、删除或修改 handler、application/service、domain、查询与迁移。
- 不改变 OpenAPI 契约:不新增或删除接口,不改路径、方法、鉴权标记、请求响应结构或描述文本。
- 不修复其它模块可能存在的同层路由抢占,也不调整 `internal/routes/registry.go``Register` 语义。
- 不承载 `employee-collection-bill` 的证据链补齐与可达操作索引(已由 `e687a26` 单独落地并列出本修复恢复的 12 条路由)。
## Impact
- 代码:`internal/routes/employee_collection.go`,仅 3 处 `router.Group` 与 12 处 `Register` 首参17 insertions / 12 deletions
- 运行时:`/api/admin` 根级残留(含 4 条可写端点消失6 个命名 GET 端点及其同层 PUT/DELETE 端点恢复可达;员工代收款 12 条端点回到文档路径 `/api/admin/employee-collection-*`。无迁移、无配置、无外部渠道调用。
- 契约:`docs/admin-openapi.yaml` 逐字节不变;主规格文件不变。
- 回归风险:任何按缺陷路径(`/api/admin/:id``/api/admin/statistics` 等)写死的调用方必须迁移;仓库源码、注释与 OpenAPI 文档均使用 `/api/admin/employee-collection-*`,未发现此类调用方。
- 依赖:证据链与可达操作索引修复已由 `e687a26` 落地(`openspec/specs/employee-collection-bill/spec.md``## 可达操作索引` + `docs/verification/context-reset/*.json`),与本 Change 无文件重叠。

View File

@@ -0,0 +1,16 @@
## 1. 路由前缀修复(提交 `ff1362d`,已推送 `origin/Iteration/8-11`
- [x] 1.1 三个注册函数各引入 `router.Group("/<资源前缀>")`12 处 `Register` 首参由 admin 根路由组改为该 group`basePath` 与文档路径拼接不变。验证:`git show --numstat ff1362d``internal/routes/employee_collection.go`17 insertions / 12 deletions`grep -c "Register(group"` 为 12。
- [x] 1.2 确认 `/api/admin` 根级不再有员工代收款路由。验证:离线路由表扫描(真实 `RegisterAdminRoutes` + `openapi.BuildDocHandlers`)缺陷版 10 条根级栈条目 / 7 条不同 method+path 全部消失,路由总数 356 → 358。
- [x] 1.3 确认同层抢占候选为 0。验证同方法下先注册的参数路由命中后注册的静态路由缺陷版 29 条原始命中(按「方法 + 被抢占静态路由」去重 15 条),修复后为 0另以最小 Fiber 应用证明匹配按注册顺序(参数路由先注册时命中参数路由)。
- [x] 1.4 确认真实路由可达性。验证:装载真实注册函数与真实 handler 后请求,缺陷版 6 个命名 GET 返回 `400 / 1001 无效的路径ID``/api/admin/statistics` 返回 `503 / 2004`;修复版这些路径命中各自处理器(`403 / 1005` 权限拒绝),`/api/admin/statistics` 返回 `404 / 1006`
- [x] 1.5 确认契约与鉴权未变。验证:缺陷版、修复版与当前工作区三次 `go run cmd/gendocs/main.go` 产物逐字节相同md5 `274f45440597aaafb1e168ac266cbb85`,含路径、方法、鉴权标记与描述文本);`requireEmployeeCollectionConfigAccess``adminOnly` 的包装位置和数量不变。
- [x] 1.6 确认未触碰业务实现。验证:`ff1362d` 不包含 handler、application/service、domain、迁移或配置改动。
## 2. 本 Change 的门禁与归档
- [x] 2.1 变更在无 delta 形态下通过严格校验。验证:在 `/tmp``openspec/` 副本执行 `openspec validate fix-employee-collection-route-prefix --strict` 通过(输出 `Change 'fix-employee-collection-route-prefix' is valid`)。
- [x] 2.2 全量校验与既有条目共存。验证:同一副本 `openspec validate --all` 为 42 passed / 0 failed真实仓库对照为 41 passed / 0 failed差异仅为副本多出的本 Change
- [x] 2.3 变更不破坏上下文健康门禁。验证:把本 Change 放入仓库快照的 `/tmp` 全量副本(含 `e687a26` 的证据链与可达操作索引)后执行 `zsh ./scripts/context-health.sh`输出「Context 健康检查通过」、退出码 0。
- [x] 2.4 落地后在真实仓库运行 `./scripts/context-health.sh` 并确认输出「Context 健康检查通过」;该脚本会重写 `docs/admin-openapi.yaml`,须在并发流水线停止后执行。验证:在真实仓库(`HEAD=7891189`,含 `e687a26` 的证据链与可达操作索引执行输出「Context 健康检查通过」、退出码 0同时 `docs/admin-openapi.yaml` md5 仍为 `274f45440597aaafb1e168ac266cbb85`
- [ ] 2.5 归档本 Change`openspec archive`,不改动 `openspec/specs/**``docs/verification/context-reset/*.json`;归档预演结果见 design 的 Risks