Files
junhong_cmp_fiber/openspec/changes/fix-employee-collection-route-prefix/design.md
break 5ee8e3cb4a
All checks were successful
构建并部署到测试环境(无 SSH) / build-and-deploy (push) Successful in 1m30s
docs(员工代收款): 新增路由前缀修复治理变更并勾选 AUG26-017 门禁
- 新增 OpenSpec Change fix-employee-collection-route-prefix,承载已落地的 ff1362d 路由前缀修复(无规格 delta,skip_specs)
- 记录根因(Register 的 basePath 只服务文档)、影响面(7 条根级残留、15 条同层抢占)、修复方式与验证方式
- AUG26-017 全局健康门禁实际通过后勾选 5.7(tasks 27/27)
2026-09-11 15:38:05 +08:00

8.3 KiB
Raw Blame History

Context

proposal.md。归档 Change 2026-09-11-add-employee-collection-bills(来源提交 ce24d56)新增员工代收款账单闭环,其路由注册把资源前缀写进了 RegisterbasePath,导致 12 处注册落到 /api/admin 根上。修复提交 ff1362d2026-09-11 14:40fix(员工代收款): 修正路由前缀注册方式,消除 /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.goRegister 第一行是 router.Add(method, path, handler)basePath 只出现在 doc != nil 分支中拼接 fullPathpathParamRegex 转换后写入文档)。缺陷版 ce24d56 把 admin 根路由组与相对 path 组合,paymentMethodPath / billPath / applicationPath 只传给了文档生成器。结果:文档路径正确,运行时路径缺少资源前缀。

Fiber 按注册顺序匹配且没有静态优先。最小复现:同一应用先注册 GET /:id 再注册 GET /refundsapp.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/refundsGET /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 无效的路径IDGET /api/admin/statistics 落到账单统计处理器返回 503 / 2004GET /api/admin/employee-collection-bills 也被根级 /:id 抢走返回 400 / 1001;修复版这些路径命中各自处理器(权限中间件在无会话上下文时返回 403 / 1005GET /api/admin/statistics 返回 404 / 1006,员工代收款端点命中自身处理器。

3. 修复方式:资源前缀交给 router.Group

三个注册函数各 router.Group("/<资源前缀>")12 处 Register 首参改为该 groupbasePath 拼接保持原样(文档路径不变)。这是仓库既有写法(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 决策:无 deltaskip_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 --strictopenspec 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-prefixspecsUpdated: false24 个主规格文件 sha256 与 Requirement 名集合、可达路由集合全部不变;在包含本 Change 的副本上 ./scripts/context-health.sh 输出「Context 健康检查通过」。