From 32b75d742880332044b71c438270047cf818fa91 Mon Sep 17 00:00:00 2001 From: Rogee Date: Mon, 29 Dec 2025 14:50:24 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20=E4=BC=98=E5=8C=96=20AppError=20?= =?UTF-8?q?=E7=BB=93=E6=9E=84=EF=BC=8C=E7=A1=AE=E4=BF=9D=E9=93=BE=E5=BC=8F?= =?UTF-8?q?=E8=B0=83=E7=94=A8=E6=96=B9=E6=B3=95=E7=BA=BF=E7=A8=8B=E5=AE=89?= =?UTF-8?q?=E5=85=A8=E5=B9=B6=E8=BF=94=E5=9B=9E=E6=96=B0=E5=AE=9E=E4=BE=8B?= =?UTF-8?q?=EF=BC=9B=E6=9B=B4=E6=96=B0=E5=BC=80=E5=8F=91=E8=A7=84=E5=88=99?= =?UTF-8?q?=EF=BC=8C=E5=A2=9E=E5=BC=BA=E5=AF=B9=E5=88=86=E9=A1=B5=E5=92=8C?= =?UTF-8?q?=E9=94=99=E8=AF=AF=E5=A4=84=E7=90=86=E7=9A=84=E8=A6=81=E6=B1=82?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- templates/project/app/errorx/app_error.go.tpl | 86 ++++++++++++------- templates/project/llm.txt.raw | 22 +++-- 2 files changed, 73 insertions(+), 35 deletions(-) diff --git a/templates/project/app/errorx/app_error.go.tpl b/templates/project/app/errorx/app_error.go.tpl index b364e87..e16f17d 100644 --- a/templates/project/app/errorx/app_error.go.tpl +++ b/templates/project/app/errorx/app_error.go.tpl @@ -1,65 +1,93 @@ package errorx import ( - "fmt" - "runtime" + "fmt" + "runtime" ) // AppError 应用错误结构 type AppError struct { - Code ErrorCode `json:"code"` - Message string `json:"message"` - StatusCode int `json:"-"` - Data any `json:"data,omitempty"` - ID string `json:"id,omitempty"` + Code ErrorCode `json:"code"` + Message string `json:"message"` + StatusCode int `json:"-"` + Data any `json:"data,omitempty"` + ID string `json:"id,omitempty"` - // 调试信息 - originalErr error - file string - params []any - sql string + // 调试信息 + originalErr error + file string + params []any + sql string } // Error 实现 error 接口 func (e *AppError) Error() string { - return fmt.Sprintf("[%d] %s", e.Code, e.Message) + return fmt.Sprintf("[%d] %s", e.Code, e.Message) } // Unwrap 允许通过 errors.Unwrap 遍历到原始错误 func (e *AppError) Unwrap() error { return e.originalErr } +// copy 返回 AppError 的副本,用于链式调用时的并发安全 +func (e *AppError) copy() *AppError { + newErr := *e + return &newErr +} + +// WithCause 携带原始错误并记录调用栈 +func (e *AppError) WithCause(err error) *AppError { + newErr := e.copy() + newErr.originalErr = err + + // 记录调用者位置 + if _, file, line, ok := runtime.Caller(1); ok { + newErr.file = fmt.Sprintf("%s:%d", file, line) + } + return newErr +} + // WithData 添加数据 func (e *AppError) WithData(data any) *AppError { - e.Data = data - return e + newErr := e.copy() + newErr.Data = data + return newErr } // WithMsg 设置消息 func (e *AppError) WithMsg(msg string) *AppError { - e.Message = msg - return e + newErr := e.copy() + newErr.Message = msg + return newErr +} + +func (e *AppError) WithMsgf(format string, args ...any) *AppError { + newErr := e.copy() + newErr.Message = fmt.Sprintf(format, args...) + return newErr } // WithSQL 记录SQL信息 func (e *AppError) WithSQL(sql string) *AppError { - e.sql = sql - return e + newErr := e.copy() + newErr.sql = sql + return newErr } // WithParams 记录参数信息,并自动获取调用位置 func (e *AppError) WithParams(params ...any) *AppError { - e.params = params - if _, file, line, ok := runtime.Caller(1); ok { - e.file = fmt.Sprintf("%s:%d", file, line) - } - return e + newErr := e.copy() + newErr.params = params + if _, file, line, ok := runtime.Caller(1); ok { + newErr.file = fmt.Sprintf("%s:%d", file, line) + } + return newErr } // NewError 创建应用错误 func NewError(code ErrorCode, statusCode int, message string) *AppError { - return &AppError{ - Code: code, - Message: message, - StatusCode: statusCode, - } + return &AppError{ + Code: code, + Message: message, + StatusCode: statusCode, + } } diff --git a/templates/project/llm.txt.raw b/templates/project/llm.txt.raw index 5d20fac..c5cb174 100644 --- a/templates/project/llm.txt.raw +++ b/templates/project/llm.txt.raw @@ -8,10 +8,17 @@ This file condenses `docs/dev/http_api.md` + `docs/dev/model.md` into a checklis - DO follow existing module layout under `app/http//`. - MUST: HTTP module folder name MUST be `snake_case` (e.g. `tenant_public`), not `camelCase`/`mixedCase`. +- MUST: JSON tags in DTOs and all response/request structs MUST use `snake_case` (e.g., `json:"user_id"`), never `camelCase` (e.g., `json:"userId"`). +- MUST: Paginated list endpoints MUST return `*requests.Pager` and use the shared `requests.Pagination` types defined in `app/requests/pagination.go` ; DO NOT redefine pagination or pager structs in local DTOs. +- MUST: The JSON response for paginated data MUST follow the `requests.Pager` layout: `{ "page": 1, "limit": 10, "total": 100, "items": [...] }`. - DO keep controller methods thin: parse/bind → call `services.*` → return result/error. - DO regenerate code after changes (routes/docs/models). -- MUST: in `app/services`, prefer the generated GORM-Gen DAO (`database/models/*`) for DB access ; treat raw `*gorm.DB` usage as a last resort. -- MUST: after adding/removing/renaming any files under `app/services/`, run `atomctl gen service --path ./app/services` to regenerate `app/services/services.gen.go` ; DO NOT edit `services.gen.go` manually. +- MUST: in `app/services`, prefer the generated GORM-Gen DAO (`database/models/*`) for DB access ; treat raw `*gorm.DB` usage as a last resort. +- MUST: When building queries in services, improve readability by using the assignment: `tbl, query := models.Query.QueryContext(ctx)`. Then use `tbl` for field references (e.g., `tbl.ID.Eq(...)`) and `query` for chaining methods. +- MUST: in `services`, when an error occurs (e.g., DB error, third-party API error), NEVER return a generic `errorx.ErrXxx` alone if there is an underlying `err`. ALWAYS use `errorx.ErrXxx.WithCause(err)` to wrap the original error. This ensures the centralized Logger captures the full context (file, line, root cause) while the client receives a friendly message and a unique Error ID for tracking. +- MUST: all chainable methods on `AppError` (`WithCause`, `WithMsg`, `WithData`, etc.) are thread-safe and return a new instance (clone). Use them freely to add context to global error variables. +- MUST: service-layer transactions MUST use `models.Q.Transaction(func(tx *models.Query) error { ... })` ; DO NOT use raw `*_db.Transaction(...)` / `db.Transaction(...)` in services unless Gen cannot express the required operation. +- MUST: after adding/removing/renaming any files under `app/services/`, run `atomctl gen service --path ./app/services` to regenerate `app/services/services.gen.go` ; DO NOT edit `services.gen.go` manually. - DO add `// @provider` above every controller/service `struct` declaration. - DO keep HTTP middlewares in `app/middlewares/` only. - DO keep all `const` declarations in `pkg/consts/` only (do not declare constants elsewhere). @@ -23,6 +30,7 @@ This file condenses `docs/dev/http_api.md` + `docs/dev/model.md` into a checklis - DO NOT manually write route declarations (only `atomctl gen route`). - DO keep Swagger annotations consistent with actual Fiber route paths (including `:param`). - MUST: route path parameter placeholders MUST be `camelCase` (e.g. `:tenantCode`), never `snake_case` (e.g. `:tenant_code`). +- MUST: for numeric ID path params (`int/int64` like `tenantID/userID/orderID`), prefer Fiber typed params `:tenantID` to avoid conflicts with static subpaths (e.g. `/orders/statistics`) and reduce ambiguous routing. - MUST: when importing another HTTP module's `dto` package, the import alias MUST be `_dto` (e.g. `tenant_dto`), not `dto` (e.g. `tenantdto`). - MUST: when creating/generating Go `struct` definitions (DTOs/requests/responses/etc.), add detailed per-field comments describing meaning, usage scenario, and validation/usage rules (do not rely on “self-explanatory” names). - MUST: business code comments MUST be written in Chinese (中文注释), to keep review/maintenance consistent across the team. @@ -62,7 +70,7 @@ Place above the handler function: Common `@Success` patterns: -- Paginated list: `requests.Pager{items=dto.Item}` +- Paginated list: `requests.Pager{items=[]dto.Item}` - Single object: `dto.Item` - Array: `{array} dto.Item` @@ -150,6 +158,8 @@ Models live in: 4) Map complex field types (JSON/ARRAY/UUID/…) via transform file: - `database/.transform.yaml` → `field_type.
` +- MUST: For ALL enum fields (even simple `VARCHAR`), you MUST map them to their corresponding Go enum type (defined in `pkg/consts`) in `.transform.yaml`. This ensures strong typing in the generated models and avoids unsafe manual casting (e.g., `string(consts.GenderMale)`). +- MUST: For deterministic JSONB fields (where the structure is known), define a corresponding Go struct in `database/fields/` and map the field to `types.JSONType[fields.StructName]` in `.transform.yaml`. 5) Generate models: @@ -174,7 +184,7 @@ Reference: `llm.gorm_gen.txt`. - MUST: use Gen transaction wrapper so all queries share the same tx connection: - `models.Q.Transaction(func(tx *models.Query) error { ... })` - Inside tx, use `tx.
.QueryContext(ctx)` / `tx.
.WithContext(ctx)` -- DO NOT: use `_db.WithContext(ctx).Transaction(...)` in services unless Gen cannot express a required operation. +- DO NOT: use `_db.WithContext(ctx).Transaction(...)` / `db.Transaction(...)` in services unless Gen cannot express a required operation. ### 3.3 Updates @@ -240,7 +250,7 @@ In this case: - `app/events/subscribers/.go`(subscriber:实现 `contracts.EventHandler`,负责 `Topic()` + `Handler(...)`) - 生成后:按项目约定运行一次 `atomctl gen provider`(用于刷新 DI/provider 生成文件)。 -### Topic 约定 +### Topic约定 - 统一在 `app/events/topics.go` 维护 topic 常量,避免散落在各处形成“字符串协议”。 - topic 字符串建议使用稳定前缀(例如 `event:`),并使用 `snake_case` 命名。 @@ -289,7 +299,7 @@ Common types: - `Kind` 建议与业务枚举/事件类型对齐,便于 SQL/报表按 `kind` 过滤。 - `Data` 写入对应 payload 的 JSON(payload 可以是多个不同 struct)。 - 读取时: -- 先 `snap := model.Snapshot.Data()`,再 `switch snap.Kind` 选择对应 payload 结构去 `json.Unmarshal(snap.Data, &payload)`。 +- 先 `snap := model.Snapshot.Data()`,再 `switch snap.Kind` 选择对应 payload结构去 `json.Unmarshal(snap.Data, &payload)`。 - 兼容历史数据(旧 JSON 没有 kind/data)时,`UnmarshalJSON` 可以将其标记为 `legacy` 并把原始 JSON 放入 `Data`,避免线上存量读取失败。 ---