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

74 lines
8.3 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
## 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 健康检查通过」。