Files
junhong_cmp_fiber/docs/engineering/工程约束.md
2026-09-10 10:53:07 +08:00

316 lines
20 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.
# 工程约束
本文件只记录从当前代码、配置、迁移、构建或可复现运行结果证明的长期工程规则。业务行为属于 `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 使用 GORMMUST 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 或 mapMUST 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 在同一事务写 OutboxMUST 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 LogHTTP 请求写 Access Log。
- **最小反例**:把完整凭证写 app.log。
- **机械检查/人工原因**:人工核对 Logger 字段、Sanitizer 与事实存储;本地按 request_id/correlation_id 查询日志。
- **例外条件**:第三方要求回显的非敏感渠道码可保留。
- **Owner**:审计与安全负责人
- **最后验证日期**2026-08-07
- **更新触发条件**:日志字段、保留策略或外部集成变化
## ENG-SECRET-001
- **状态**:生效
- **适用范围**:配置、代码、文档、日志和示例
- **规则**密钥、Token、证书和真实凭证 MUST 由生效配置/环境注入MUST NOT 新增到仓库或日志;示例只用占位符。
- **理由**:仓库历史和日志传播范围不可控。
- **最小正例**:文档使用 `<TOKEN>`,运行环境注入 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、部署分支、测试主机或验证授权变化