- 新增 OpenSpec Change fix-employee-collection-route-prefix,承载已落地的 ff1362d 路由前缀修复(无规格 delta,skip_specs)
- 记录根因(Register 的 basePath 只服务文档)、影响面(7 条根级残留、15 条同层抢占)、修复方式与验证方式
- AUG26-017 全局健康门禁实际通过后勾选 5.7(tasks 27/27)
8.3 KiB
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}` |
真实 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。
依据:
openspec/specs/employee-collection-bill/spec.md只有 5 条行为 Requirement,均不描述 HTTP 路径;路由由独立的## 可达操作索引承载(e687a26已补齐 12 条/api/admin/employee-collection-*),scripts/context-health.sh明确禁止### Requirement: ...接口集合。本修复让运行时路径与该索引一致,索引文本无需改动。- 规格侧唯一描述路径的产物是
docs/admin-openapi.yaml,缺陷版、修复版与当前工作区三处 gendocs 产物逐字节相同(md5 均为274f45440597aaafb1e168ac266cbb85),文档契约从未改变。 - 被阻断的端点属于其它能力,其主规格本来就要求这些端点可用(例如
GET /api/admin/refunds已在入口矩阵中登记为order-refund-exchange的 HTTP 入口)。修复是让实现符合既有契约,不是改变契约。 - 若强行写 delta,只能写出「路由注册必须使用资源前缀」这类实现约束,或把路由目录写成 Requirement;两者都违反
AGENTS.md「Requirement 必须描述可观察行为,不能用接口目录代替状态、权限、金额、失败与幂等语义」。 - 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 健康检查通过」。