# 工程约束 本文件只记录从当前代码、配置、迁移、构建或可复现运行结果证明的长期工程规则。业务行为属于 `openspec/specs/`,第三方协议属于 `docs/integrations/`。自动化测试当前为 N/A(用户决策)。 ## ENG-ARCH-001 - **状态**:生效 - **适用范围**:`internal/domain/**` - **规则**:Domain MUST NOT import Fiber、GORM、Redis、Asynq 或具体第三方 SDK。 - **理由**:领域不变量必须能脱离传输和基础设施独立演化。 - **最小正例**:Domain 只依赖标准库、领域类型和 Port。 - **最小反例**:Domain 直接调用 GORM 或 Redis。 - **机械检查/人工原因**:`! grep -RIlE 'gofiber|gorm.io|go-redis|hibiken/asynq' internal/domain --include='*.go'`,当前通过。 - **例外条件**:既有兼容行为仍须先通过独立 Change 才能调整边界。 - **Owner**:架构负责人 - **最后验证日期**:2026-08-07 - **更新触发条件**:Domain 依赖或分层策略变化 ## ENG-ARCH-002 - **状态**:生效 - **适用范围**:新增或修改完整用例 - **规则**:复杂写 MUST 使用 Application→Domain→Port;简单写 MUST 使用 Application 事务脚本;读取 MUST 使用 Query 或既有只读 Service,且不得修改状态。 - **理由**:按用例选择最小边界,避免半迁移和形式化 DDD。 - **最小正例**:订单资金状态机收口 Domain;列表查询直接 DTO 投影。 - **最小反例**:只为单表 CRUD 创建聚合,或 Query 内更新状态。 - **机械检查/人工原因**:暂为人工:从 Route 追踪到用例和持久化,确认一个完整用例只有一个规则归属。 - **例外条件**:未触碰的旧 `internal/service` 保持现状。 - **Owner**:架构负责人 - **最后验证日期**:2026-08-07 - **更新触发条件**:新增用例或触碰旧复杂写 ## ENG-ERR-001 - **状态**:生效 - **适用范围**:到 HTTP 边界的错误链 - **规则**:到 Handler/HTTP 边界前 MUST 转换为 `pkg/errors` 稳定错误;Handler 参数校验 MUST NOT 拼接或返回底层 `err.Error()`。 - **理由**:全局 ErrorHandler 只能安全映射稳定错误,底层文本可能泄密。 - **最小正例**:`errors.Wrap(code, err)` 后返回给全局 ErrorHandler。 - **最小反例**:`return c.JSON(...err.Error())`。 - **机械检查/人工原因**:审查变更中的 Handler 返回点;运行 `go build ./cmd/api`。全仓 `fmt.Errorf` 只作候选,不能直接判错。 - **例外条件**:内部不可见、不会越过接口边界的诊断错误允许 `fmt.Errorf`。 - **Owner**:API 负责人 - **最后验证日期**:2026-08-07 - **更新触发条件**:错误系统或 ErrorHandler 变化 ## ENG-RESP-001 - **状态**:生效 - **适用范围**:HTTP Handler - **规则**:成功响应 MUST 使用 `pkg/response`;错误 MUST 返回给 `internal/middleware.ErrorHandler`,不得自造响应壳。 - **理由**:统一 `{code,msg,data,timestamp}` 和 HTTP 映射。 - **最小正例**:`return response.Success(c, data)`。 - **最小反例**:Handler 直接 `c.JSON` 返回另一套结构。 - **机械检查/人工原因**:审查新增 Handler;`rg "response\.(Success|Error)" internal/handler internal/routes`;生成 OpenAPI 人工核对。 - **例外条件**:第三方回调必须返回渠道要求的字面协议时可使用专用响应。 - **Owner**:API 负责人 - **最后验证日期**:2026-08-07 - **更新触发条件**:响应协议或第三方回调契约变化 ## ENG-DTO-001 - **状态**:生效 - **适用范围**:进入 OpenAPI 的请求/响应 DTO - **规则**:公开字段 MUST 有中文 description;枚举描述 MUST 与 `pkg/constants` 当前值一致;状态响应按现有契约提供名称字段。 - **理由**:前端和生成文档依赖字段与枚举精确一致。 - **最小正例**:从 constants 原文复制枚举值并生成 OpenAPI 核对。 - **最小反例**:凭记忆写 description 或遗漏状态名称。 - **机械检查/人工原因**:`go run cmd/gendocs/main.go` 后核对 OpenAPI;暂为人工比对枚举与 constants,单纯 grep 不足以证明完整。 - **例外条件**:仅内部、不进入接口契约的结构。 - **Owner**:API 负责人 - **最后验证日期**:2026-08-07 - **更新触发条件**:DTO、枚举或 OpenAPI 生成变化 ## ENG-MODEL-001 - **状态**:生效 - **适用范围**:新增/修改 `internal/model` 与当前根迁移 - **规则**:关联 MUST 保存 ID 并显式查询;MUST NOT 新增 GORM `foreignKey`/`hasMany`/`belongsTo` 标签或数据库外键。 - **理由**:当前隔离与渐进迁移依赖显式关联。 - **最小正例**:保存 `ShopID`,由 Store 显式查询。 - **最小反例**:新增关联切片和 `foreignKey` 标签。 - **机械检查/人工原因**:只检查变更新增行:`git diff -- internal/model migrations | grep -E "^\+.*(foreignKey|hasMany|belongsTo|REFERENCES)"`,期望空。 - **例外条件**:`migrations/archive` 的历史外键不作为新增违规。 - **Owner**:数据负责人 - **最后验证日期**:2026-08-07 - **更新触发条件**:模型或关联策略变化 ## ENG-DB-001 - **状态**:生效 - **适用范围**:生产 Go 数据访问 - **规则**:生产数据访问 MUST 使用 GORM,MUST NOT 新增 `database/sql` 直接访问。 - **理由**:统一连接、事务、Callback 数据范围和错误处理。 - **最小正例**:通过 `*gorm.DB` 或 Store 查询。 - **最小反例**:业务 Service 新建 `sql.DB`。 - **机械检查/人工原因**:检查变更新增 import:`git diff -U0 -- "*.go" | grep -E "^\+.*database/sql"`,期望空。 - **例外条件**:第三方库内部实现不受本规则约束。 - **Owner**:数据负责人 - **最后验证日期**:2026-08-07 - **更新触发条件**:数据访问基础设施变化 ## ENG-DB-002 - **状态**:生效 - **适用范围**:Agent 对项目数据库的诊断、核对与只读查询。 - **规则**:MUST 使用 dbhub MCP:测试/本地库使用 `mcp__dbhub__execute_sql_main`,正式库使用 `mcp__dbhub__execute_sql_pro_main`;MUST NOT 通过 `psql`、连接串、环境变量或其他命令行客户端直连数据库。 - **理由**:dbhub 提供受控只读访问,避免命令历史、环境凭证和目标库选择漂移。 - **最小正例**:调用 `mcp__dbhub__execute_sql_main` 查询订单与佣金记录。 - **最小反例**:`source .env && psql ...`。 - **机械检查/人工原因**:审查 Agent 执行记录中的数据库访问工具;仓库业务 Go 代码不受本条约束,仍遵守 ENG-DB-001。 - **例外条件**:维护者明确提供的、需执行写入或迁移的人工操作按生产运行说明执行,Agent 不代执行。 - **Owner**:基础设施负责人 - **最后验证日期**:2026-08-13 - **更新触发条件**:dbhub MCP 名称、访问范围或数据库运维边界变化 ## ENG-MIG-001 - **状态**:生效 - **适用范围**:`migrations/` 当前根目录 - **规则**:新迁移 MUST 使用新顺序号并提供 `.up.sql`/`.down.sql` 配对;MUST NOT 修改已发布迁移。 - **理由**:保证 Schema 可重放和回滚。 - **最小正例**:新增同号 up/down 文件并在隔离库执行。 - **最小反例**:改旧迁移或只提供 up。 - **机械检查/人工原因**:按 `migrations/*.up.sql` 与 `*.down.sql` basename 配对;当前根目录 89/89。隔离库使用 `scripts/migrate.sh` 执行 up/down/up。 - **例外条件**:不可逆数据清理须在 Change 说明恢复方式;archive 历史资产不要求补配。 - **Owner**:数据负责人 - **最后验证日期**:2026-08-07 - **更新触发条件**:Schema 或迁移工具变化 ## ENG-ROUTE-001 - **状态**:生效 - **适用范围**:新增或修改 Handler/路由 - **规则**:路由 MUST 经 `internal/routes.Register` 注册 RouteSpec;新增 Handler MUST 同步 `cmd/api/docs.go` 与 `cmd/gendocs/main.go` 的占位装配。 - **理由**:运行路由与生成文档共享元数据,但两套 composition root 仍需一致。 - **最小正例**:路由文件注册并在两处 docs Handlers 增加字段。 - **最小反例**:只在 Fiber app 上挂载 Handler。 - **机械检查/人工原因**:`go run cmd/gendocs/main.go`;对比两处 `bootstrap.Handlers` 字段;再对照真实 `internal/bootstrap` 装配。 - **例外条件**:健康/就绪路由在总入口内定义,但仍使用 RouteSpec。 - **Owner**:API 负责人 - **最后验证日期**:2026-08-07 - **更新触发条件**:新增 Handler 或文档生成方式变化 ## ENG-COMMENT-001 - **状态**:生效 - **适用范围**:Go 生产代码 - **规则**:导出符号 MUST 有中文文档注释;复杂逻辑说明原因;Handler 注释 MUST 包含 HTTP 方法和路径。 - **理由**:保持 godoc、评审和真实路由可核对。 - **最小正例**:导出 Handler 注释写职责及 `POST /api/...`。 - **最小反例**:注释复述赋值或路径过时。 - **机械检查/人工原因**:`go vet` 只做基础检查;中文、原因和路径由变更 diff 人工核对真实路由。 - **例外条件**:显而易见且少于 15 行的未导出函数可无注释。 - **Owner**:后端负责人 - **最后验证日期**:2026-08-07 - **更新触发条件**:新增导出 API 或路由变化 ## ENG-JSON-001 - **状态**:生效 - **适用范围**:新增 JSON 编解码代码 - **规则**:新增业务 JSON MUST 优先 sonic;新增 `encoding/json` import MUST 记录兼容原因。 - **理由**:Fiber 已配置 sonic,但现有 SDK/OpenAPI/协议有标准库兼容需求。 - **最小正例**:业务载荷使用 `sonic.Marshal`。 - **最小反例**:无理由新增 `encoding/json`。 - **机械检查/人工原因**:只审查 diff 中新增 import;当前仓库已有多处兼容使用,不能以全仓 grep 直接失败。 - **例外条件**:标准库接口、第三方 SDK 或协议兼容明确要求。 - **Owner**:后端负责人 - **最后验证日期**:2026-08-07 - **更新触发条件**:JSON 库或兼容协议变化 ## ENG-QUEUE-001 - **状态**:生效 - **适用范围**:`queueClient.EnqueueTask` 调用 - **规则**:payload MUST 是 struct 或 map,MUST NOT 传预序列化 `[]byte`。 - **理由**:封装会统一校验并 sonic.Marshal,字节切片会二次编码。 - **最小正例**:`EnqueueTask(ctx, typ, Payload{ID:id})`。 - **最小反例**:Marshal 后把 `[]byte` 交给 EnqueueTask。 - **机械检查/人工原因**:核对全部 EnqueueTask 第三实参及 `pkg/queue/client.go` 的 ValidatePayload;运行 `go build ./cmd/worker`。 - **例外条件**:直接调用 `asynq.NewTask` 时由调用方序列化。 - **Owner**:异步任务负责人 - **最后验证日期**:2026-08-07 - **更新触发条件**:队列封装或载荷协议变化 ## ENG-PAGE-001 - **状态**:生效 - **适用范围**:列表 API - **规则**:列表 MUST 分页并执行各接口当前上限;默认 20/最大 100 仅适用于已明确采用该契约的列表。 - **理由**:避免无界查询,同时不把不同接口的既有分页强行合并。 - **最小正例**:Handler/Query 对缺省值与上限显式归一化。 - **最小反例**:只写 OpenAPI tag,不在运行时限制。 - **机械检查/人工原因**:从 DTO→Handler→Query 逐链核对 Limit;暂为人工,不以 tag 单独证明行为。 - **例外条件**:固定且有界的小型枚举集合。 - **Owner**:API 负责人 - **最后验证日期**:2026-08-07 - **更新触发条件**:分页协议或查询性能变化 ## ENG-STATE-001 - **状态**:生效 - **适用范围**:新增或变更状态/类型字段 - **规则**:新增生命周期状态 SHOULD 使用 int,类型/方式 SHOULD 使用 string;启停新增语义使用 0=禁用、1=启用。既有不一致 MUST 按 As-Is 保留,未经 Change 不得统一。 - **理由**:避免继续扩大状态语义漂移,同时保护兼容行为。 - **最小正例**:新 Status int;新 PaymentMethod string。 - **最小反例**:基线任务顺手改既有状态值。 - **机械检查/人工原因**:比对 constants、DTO、模型与迁移;既有例外必须在 Spec 记录。 - **例外条件**:第三方原始字段在 Adapter 边界转换;既有兼容状态保留。 - **Owner**:领域负责人 - **最后验证日期**:2026-08-07 - **更新触发条件**:新增状态机或兼容映射 ## ENG-AUTHZ-001 - **状态**:生效 - **适用范围**:资源读取与写入 - **规则**:资源操作 MUST 在业务边界校验店铺/企业/个人客户数据范围;MUST NOT 只依赖路由角色中间件。无权限与不存在不得形成可枚举差异。 - **理由**:路由只做粗粒度身份,资源归属随数据变化。 - **最小正例**:Application/Service 在读写前调用当前数据范围检查。 - **最小反例**:知道资源 ID 即可更新。 - **机械检查/人工原因**:从 Handler 追踪 Application/Service 的资源校验及 GORM Callback;暂为人工抽查权限失败响应。 - **例外条件**:公开健康、回调等不以资源归属认证,但必须执行自身信任校验。 - **Owner**:安全负责人 - **最后验证日期**:2026-08-07 - **更新触发条件**:新增资源入口或数据范围变化 ## ENG-CONC-001 - **状态**:生效 - **适用范围**:状态机、余额、库存与并发领取 - **规则**:状态流转 MUST 使用 expected-status 条件更新;余额 MUST 使用 version/锁定策略;MUST NOT 读后无条件写关键事实。 - **理由**:防止重复处理、负余额和并发覆盖。 - **最小正例**:`WHERE status=expected` 并检查 RowsAffected。 - **最小反例**:先读状态再无条件 Updates。 - **机械检查/人工原因**:核对 `application/wallet`、`agentrecharge` 等现有模式;变更时人工检查条件、版本和 RowsAffected。 - **例外条件**:只读投影不适用。 - **Owner**:领域负责人 - **最后验证日期**:2026-08-07 - **更新触发条件**:并发写或状态机变化 ## ENG-OUTBOX-001 - **状态**:生效 - **适用范围**:需要提交后可靠副作用的用例 - **规则**:业务事实与可靠异步副作用 MUST 在同一事务写 Outbox;MUST NOT 用裸 goroutine 代替可靠投递;消费者 MUST 容忍重复。 - **理由**:进程崩溃和至少一次投递会造成丢失或重复。 - **最小正例**:事务写业务事实与 Outbox,消费者条件更新。 - **最小反例**:提交后启动 goroutine 调第三方。 - **机械检查/人工原因**:追踪 producer→Outbox→Relay→consumer;检查事件唯一键、租约和终态条件。 - **例外条件**:允许丢失的低价值遥测可不用 Outbox,但须明确说明。 - **Owner**:可靠性负责人 - **最后验证日期**:2026-08-07 - **更新触发条件**:新增外部副作用或事件 ## ENG-TX-001 - **状态**:生效 - **适用范围**:资金、状态与成功审计 - **规则**:资金/关键状态事实和要求成功必达的审计 MUST 在同一 GORM 事务;事务内 MUST NOT 持有不可回滚的长外部 I/O。 - **理由**:保证事实与审计一致,并控制锁时间。 - **最小正例**:事务写事实和 Audit Writer;提交后由 Outbox 外发。 - **最小反例**:事务中等待第三方网络后再提交。 - **机械检查/人工原因**:逐用例人工核对 Transaction 闭包、Audit Writer 和外部调用位置。 - **例外条件**:业务回滚后的 failed/denied 审计使用独立短事务。 - **Owner**:架构与审计负责人 - **最后验证日期**:2026-08-07 - **更新触发条件**:高风险写或外部调用变化 ## ENG-AUDIT-001 - **状态**:机械检查阻塞 - **适用范围**:状态变更、资金、权限、关键配置、敏感读取与关键拒绝 - **规则**:用例 MUST 明确 Audit Event、Domain Ledger、Integration Log、Outbox 的使用决定或 N/A 理由;四类事实不得互相替代。 - **理由**:Access Log 不能证明业务事实,Integration Log 不能替代资金流水。 - **最小正例**:状态成功写 Audit;外部尝试写 Integration Log。 - **最小反例**:只记录 access.log 后宣称已审计。 - **机械检查/人工原因**:当前 `cmd/audit-coverage` 写入已删除 `.scratch` 路径,不能作为通过门禁;修复前逐调用链人工核对并在仓库外记录证据。 - **例外条件**:低风险用例可登记 N/A 理由。 - **Owner**:审计负责人 - **最后验证日期**:2026-08-07 - **更新触发条件**:审计 CLI 输出路径修复或新增高风险用例 ## ENG-LOG-001 - **状态**:生效 - **适用范围**:运行日志、审计、资金事实和外部交互 - **规则**:Access Log、Audit Event、Domain Ledger、Integration Log 与 Outbox MUST 分责;日志 MUST 使用结构化 Zap 且不得记录密钥或完整敏感体。 - **理由**:不同保留期、查询维度与一致性要求不可混用。 - **最小正例**:外部失败写脱敏 Integration Log,HTTP 请求写 Access Log。 - **最小反例**:把完整凭证写 app.log。 - **机械检查/人工原因**:人工核对 Logger 字段、Sanitizer 与事实存储;本地按 request_id/correlation_id 查询日志。 - **例外条件**:第三方要求回显的非敏感渠道码可保留。 - **Owner**:审计与安全负责人 - **最后验证日期**:2026-08-07 - **更新触发条件**:日志字段、保留策略或外部集成变化 ## ENG-SECRET-001 - **状态**:生效 - **适用范围**:配置、代码、文档、日志和示例 - **规则**:密钥、Token、证书和真实凭证 MUST 由生效配置/环境注入,MUST NOT 新增到仓库或日志;示例只用占位符。 - **理由**:仓库历史和日志传播范围不可控。 - **最小正例**:文档使用 ``,运行环境注入 Secret。 - **最小反例**:提交可用 AppSecret。 - **机械检查/人工原因**:对 git diff 做敏感模式人工扫描;核对 `pkg/config` 绑定和日志字段。当前历史凭证属于待决策存量,不得复制。 - **例外条件**:明确不可用的假值可用于文档占位;当前无测试夹具。 - **Owner**:安全负责人 - **最后验证日期**:2026-08-07 - **更新触发条件**:配置字段、凭证或集成变化 ## ENG-CONFIG-001 - **状态**:生效 - **适用范围**:运行配置和依赖版本 - **规则**:依赖版本 MUST 以 `go.mod` 为准;运行参数 MUST 经 `pkg/config` 加载/校验;业务代码 MUST NOT 散落读取环境变量。 - **理由**:保证默认值、环境覆盖和启动校验只有一个入口。 - **最小正例**:在 Config 增字段并由 loader 绑定。 - **最小反例**:业务 Service 直接 `os.Getenv`。 - **机械检查/人工原因**:检查变更新增 `os.Getenv`;运行 `go build` 并核对 `pkg/config/config.go` 校验。 - **例外条件**:启动脚本读取环境变量用于组装进程配置。 - **Owner**:基础设施负责人 - **最后验证日期**:2026-08-07 - **更新触发条件**:新增配置或依赖升级 ## ENG-TEST-001 - **状态**:生效 - **适用范围**:Agent 执行的迁移、Redis、API/Worker、部署与集成 Smoke 验证。 - **规则**:维护者指定的测试 PostgreSQL `junhong_cmp_test`、Redis DB 6、`Iteration/8-11` 测试部署和 `cmp-test` 日志主机构成唯一测试验证面;MUST 使用该环境,不额外要求独立数据库、Redis DB 或 namespace。迁移从本地工作区以明确 `DB_*` 参数执行;测试 fixture 仅可创建、删除当前 Change 自己的记录,MUST NOT 重置整个测试库。测试部署通过 Gitea 工作流完成,SSH 仅用于日志、容器状态与受控 Smoke。 - **理由**:同一可控测试面避免每个 Change 重复索取环境,且保留可复现的迁移、缓存、并发和部署证据。 - **最小正例**:当前 Change 在 `junhong_cmp_test` 执行迁移 up/down/up,清理自己的 fixture,并在测试部署后读取 `cmp-test` 日志。 - **最小反例**:以未提供额外“隔离环境”为由暂停,或重置整个测试数据库。 - **机械检查/人工原因**:记录显式目标、迁移命令、fixture 清理范围、Redis 操作与测试部署 SHA;通过 API/Worker 日志和可观察状态核对。 - **例外条件**:真实支付渠道和外部审批系统不作自动验证;第三方协议变更以契约文档和维护者提供的证据为准。生产仍按生产运行说明由维护者执行。 - **Owner**:基础设施负责人 - **最后验证日期**:2026-09-08 - **更新触发条件**:测试库、Redis DB、部署分支、测试主机或验证授权变化