diff --git a/openspec/changes/add-agent-self-recharge-payment-methods/tasks.md b/openspec/changes/add-agent-self-recharge-payment-methods/tasks.md index 5413b04..217d31d 100644 --- a/openspec/changes/add-agent-self-recharge-payment-methods/tasks.md +++ b/openspec/changes/add-agent-self-recharge-payment-methods/tasks.md @@ -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/up;down 可以删除新增列,但不得影响既有充值记录读取。存在仍需保留的真实新增列数据时不得将 down 视为正常可执行场景,MUST NOT 重置整个 `junhong_cmp_test`。 diff --git a/openspec/changes/fix-employee-collection-route-prefix/.openspec.yaml b/openspec/changes/fix-employee-collection-route-prefix/.openspec.yaml new file mode 100644 index 0000000..c1b279b --- /dev/null +++ b/openspec/changes/fix-employee-collection-route-prefix/.openspec.yaml @@ -0,0 +1,3 @@ +schema: spec-driven +created: 2026-09-11 +skip_specs: true diff --git a/openspec/changes/fix-employee-collection-route-prefix/design.md b/openspec/changes/fix-employee-collection-route-prefix/design.md new file mode 100644 index 0000000..1f06cc1 --- /dev/null +++ b/openspec/changes/fix-employee-collection-route-prefix/design.md @@ -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 健康检查通过」。 diff --git a/openspec/changes/fix-employee-collection-route-prefix/proposal.md b/openspec/changes/fix-employee-collection-route-prefix/proposal.md new file mode 100644 index 0000000..e06a731 --- /dev/null +++ b/openspec/changes/fix-employee-collection-route-prefix/proposal.md @@ -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 无文件重叠。 diff --git a/openspec/changes/fix-employee-collection-route-prefix/tasks.md b/openspec/changes/fix-employee-collection-route-prefix/tasks.md new file mode 100644 index 0000000..5891bf8 --- /dev/null +++ b/openspec/changes/fix-employee-collection-route-prefix/tasks.md @@ -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)。