From 6fa70341727e5cb194334a8a68c9e517069fb156 Mon Sep 17 00:00:00 2001 From: ryan Date: Thu, 18 Jun 2026 13:31:20 +0800 Subject: [PATCH] update AGENTS.md --- AGENTS.md | 116 ++++++++++++++++++++++++++++++++++++++++++++++++++---- 1 file changed, 109 insertions(+), 7 deletions(-) diff --git a/AGENTS.md b/AGENTS.md index ce55e5b2..14ef394d 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -43,6 +43,7 @@ - `internal/router/router.go` 的 `Serve()` 仅负责 HTTP 路由与中间件,禁止在其中执行 `SyncEvents`、`InitLogWriter` 等进程级运行时初始化。 - 核心业务模块(如 `oauth`、`user`)禁止直接 `import` `internal/apps/admin/push` 或 `custom_events` 触发通知;应通过 `internal/listener` 发射域事件,由 push 模块在 bootstrap 阶段订阅。 - 编写依赖任务注册或推送事件同步的测试时,必须在测试 setup 中显式调用 `bootstrap.RegisterTasks()`、`bootstrap.RegisterPushDomainEvents()` 等,不得依赖 `init()` 副作用。 +- API 错误响应必须通过 `response.Abort*` 中断请求,由 `ErrorHandlerMiddleware` 统一写出 JSON;禁止 `c.JSON(http.StatusOK, response.Err(...))` 及 Handler 直接 `c.JSON(status, response.Err(...))`。 ## 项目介绍 @@ -128,18 +129,119 @@ Handler 规范: - Handler 命名为 动词 + 名词,例如 `ListUsers`。 - 使用 `ShouldBindQuery` 或 `ShouldBindJSON` 进行绑定。 -- 成功时统一通过导入 `"github.com/Rain-kl/Wavelet/internal/common/response"` 使用 `response.OK(data)` 或 `response.OKNil()` 返回。 -- 失败时统一通过 `response.AbortWithError(c, code, msg)` 返回(这会自动将业务错误和状态码挂载到 Context 并中断请求,全局中间件捕获后会将其完整记录到 OpenTelemetry Trace/Jaeger,并格式化输出 JSON)。 -- API 响应的外层结构必须为 `{ "error_msg": "", "data": ... }`。 -- 分页响应在 `data` 下使用 `{ "total": 0, "results": [] }`。 - 每个 HTTP API 都需要有完整的 Swagger 注释;在 API 变更后运行 `make swagger`。 -错误处理与日志: +#### API 响应信封(统一格式) +所有 JSON API 响应的外层结构**必须**为: + +```json +{ "error_msg": "", "data": ... } +``` + +- 成功时:`error_msg` 为空字符串,`data` 承载业务载荷。 +- 失败时:`data` 为 `null`,`error_msg` 为用户可见的错误说明。 +- 分页响应在 `data` 下使用 `{ "total": 0, "results": [] }`。 + +#### 成功响应(唯一写法) + +成功时**始终**使用 HTTP `200`,由 Handler 直接写出 JSON: + +```go +import ( + "net/http" + "github.com/Rain-kl/Wavelet/internal/common/response" + "github.com/gin-gonic/gin" +) + +// 有数据 +c.JSON(http.StatusOK, response.OK(data)) + +// 无数据(data 为 null) +c.JSON(http.StatusOK, response.OKNil()) +``` + +#### 失败响应(中断请求,禁止直接写错误 JSON) + +失败时**禁止**在 Handler / 中间件中直接调用 `c.JSON(..., response.Err(msg))`,也**禁止**用 HTTP `200` 携带非空 `error_msg` 表示失败。 + +统一通过 `internal/common/response` 的 **Abort 系列函数**中断请求。这些函数会将 `*response.APIError` 挂载到 Gin 的 `c.Errors` 链并 `c.Abort()`;请求结束后由全局 `response.ErrorHandlerMiddleware()`(在 `internal/router/middlewares.go` 中注册)统一写出 JSON,并记录到 OpenTelemetry Trace/Jaeger。 + +**推荐使用的便捷函数(优先于手写状态码):** + +| 函数 | HTTP 状态码 | 典型场景 | +|------|-------------|----------| +| `response.AbortBadRequest(c, msg)` | 400 | 参数绑定失败、字段校验、业务规则拒绝(如密码错误、重复注册) | +| `response.AbortUnauthorized(c, msg)` | 401 | 未登录、Session/Token 失效(`oauth.LoginRequired()`) | +| `response.AbortForbidden(c, msg)` | 403 | 已登录但无权访问(如 Token 不允许访问的端点) | +| `response.AbortNotFound(c, msg)` | 404 | 资源不存在;管理员中间件对非管理员隐藏端点时也使用此码 | +| `response.AbortConflict(c, msg)` | 409 | 资源冲突(如唯一键重复) | +| `response.AbortTooManyRequests(c, msg)` | 429 | 限流、频率限制 | +| `response.AbortInternal(c, msg)` | 500 | 对用户返回通用提示;底层错误须先记录日志 | +| `response.AbortWithError(c, code, msg)` | 自定义 | 上表未覆盖的状态码时使用 | + +**标准 Handler 模板:** + +```go +func CreateWidget(c *gin.Context) { + var req createWidgetRequest + if err := c.ShouldBindJSON(&req); err != nil { + response.AbortBadRequest(c, errBindParamsFailed) + return + } + + widget, err := createWidgetLogic(c.Request.Context(), req) + if err != nil { + // 底层错误已记录日志时,向用户返回安全文案 + response.AbortBadRequest(c, err.Error()) // 或按语义选用 AbortConflict / AbortInternal 等 + return + } + + c.JSON(http.StatusOK, response.OK(widget)) +} +``` + +**中间件**与 Handler 遵循同一规则。参考 `oauth.LoginRequired()` → `AbortUnauthorized`,`admin.LoginAdminRequired()` → `AbortNotFound`,`cap.VerifyMiddleware` → `AbortUnauthorized`。 + +#### 错误消息定义 + +- 面向用户的错误文案定义为模块内 **camelCase 字符串常量**(放在 `errs.go`),例如 `errBindParamsFailed = "参数绑定失败"`。 +- Handler / 中间件向 Abort 函数传入这些常量或经校验的安全字符串;**禁止**将数据库驱动错误、堆栈信息等内部细节直接暴露给客户端。 +- `response.Err(msg)` 仅供 `ErrorHandlerMiddleware` 内部构造 JSON,**业务代码不得直接用于 `c.JSON`**。 + +#### `logics.go` 与 Handler 的分工 + +- `logics.go` 接受 `context.Context`,返回 `(result, error)` 或带状态的业务结果结构体(参考 `internal/apps/user/logics.go` 的 `LoginEmailVerificationResult`)。 +- `logics.go` **不得**依赖 `*gin.Context`,**不得**调用 `response.Abort*` 或 `c.JSON`。 +- Handler 负责:绑定参数 → 调用 logic → 将 logic 错误/状态映射为对应的 `Abort*` 或 `response.OK`。 + +#### 日志与内部错误 + +- 数据库、Redis、第三方 API、文件 I/O 等**运行时错误**:在 Handler 或 logic 边界用 `pkg/logger` 记录(带 `ctx`),再向用户返回安全的 `AbortInternal` 或语义匹配的业务错误常量。 - 任何关键错误在被吞掉、转换为通用响应,或由后台 worker 忽略之前,都必须通过 `pkg/logger` 打印日志。 - 禁止用 `_ = ...` 静默丢弃重要错误。如果某个错误因为 best-effort 操作或确认无害而需要忽略,必须添加简短注释说明原因。 -- Handler 可以返回对用户安全的错误信息,但如果底层运行错误对生产问题排查有价值,仍然必须记录日志。 -- 避免重复刷日志:在真正处理或抑制错误的边界记录一次,然后返回或响应。 +- 避免重复刷日志:在真正处理或抑制错误的边界记录一次,然后 `Abort*` 或成功返回。 + +#### 禁止写法(反模式) + +```go +// ❌ 禁止:HTTP 200 表示失败 +c.JSON(http.StatusOK, response.Err("密码错误")) + +// ❌ 禁止:Handler 直接写错误 JSON,绕过 ErrorHandlerMiddleware 与 OTel 记录 +c.JSON(http.StatusBadRequest, response.Err("参数错误")) + +// ❌ 禁止:gin.H 手写错误体 +c.AbortWithStatusJSON(http.StatusBadRequest, gin.H{"error_msg": "...", "data": nil}) + +// ❌ 禁止:logics.go 中中断 HTTP 请求 +func doSomething(c *gin.Context) { response.AbortBadRequest(c, "...") } +``` + +#### Swagger 注释约定 + +- `@Success 200` 的 `data` 使用具体类型或 `response.Any`。 +- 对每个可能返回的 Abort 状态码声明 `@Failure`,例如 `@Failure 400 {object} response.Any "参数错误"`、`@Failure 401 {object} response.Any "未登录"`。 路由与模块: