diff --git a/backend/llm.txt b/backend/llm.txt index b84d949..a9ebe29 100644 --- a/backend/llm.txt +++ b/backend/llm.txt @@ -9,16 +9,16 @@ This file condenses `backend/docs/dev/http_api.md` + `backend/docs/dev/model.md` - DO follow existing module layout under `backend/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 `backend/app/requests/pagination.go`; DO NOT redefine pagination or pager structs in local DTOs. +- MUST: Paginated list endpoints MUST return `*requests.Pager` and use the shared `requests.Pagination` types defined in `backend/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 `backend/app/services`, prefer the generated GORM-Gen DAO (`backend/database/models/*`) for DB access; treat raw `*gorm.DB` usage as a last resort. +- MUST: in `backend/app/services`, prefer the generated GORM-Gen DAO (`backend/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 `backend/app/services/`, run `atomctl gen service --path ./app/services` to regenerate `backend/app/services/services.gen.go`; DO NOT edit `services.gen.go` manually. +- 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 `backend/app/services/`, run `atomctl gen service --path ./app/services` to regenerate `backend/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 `backend/app/middlewares/` only. - DO keep all `const` declarations in `backend/pkg/consts/` only (do not declare constants elsewhere). @@ -175,23 +175,23 @@ Reference: `backend/llm.gorm_gen.txt`. ### 3.1 Query style (preferred) - MUST: in services, build queries via: - - `tbl, q := models.
Query.QueryContext(ctx)` - - Use type-safe conditions (`tbl.ID.Eq(...)`, `tbl.TenantID.Eq(...)`, `tbl.DeletedAt.IsNull()`, etc). +- `tbl, q := models.
Query.QueryContext(ctx)` +- Use type-safe conditions (`tbl.ID.Eq(...)`, `tbl.TenantID.Eq(...)`, `tbl.DeletedAt.IsNull()`, etc). - DO NOT: use string SQL in `Where("...")` unless absolutely necessary. ### 3.2 Transactions - 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)` +- `models.Q.Transaction(func(tx *models.Query) error { ... })` +- Inside tx, use `tx.
.QueryContext(ctx)` / `tx.
.WithContext(ctx)` - DO NOT: use `_db.WithContext(ctx).Transaction(...)` / `db.Transaction(...)` in services unless Gen cannot express a required operation. ### 3.3 Updates - Prefer `UpdateSimple(...)` with typed assign expressions when possible. - Otherwise use `Updates(map[string]any{...})`, but MUST: - - include tenant boundary conditions (`tenant_id`) in the WHERE, - - avoid updating columns by concatenating user input. +- include tenant boundary conditions (`tenant_id`) in the WHERE, +- avoid updating columns by concatenating user input. ### 3.4 Columns not in generated models (temporary escape hatch) @@ -215,9 +215,30 @@ In this case: - `Kind() string`:任务类型标识(job kind);改名会导致“新旧任务类型不一致”。 - `InsertOpts() river.InsertOpts`:默认入队参数(队列、优先级、最大重试、唯一任务策略等)。 - `UniqueID() string`(项目约定):周期任务 handle 的稳定 key;通常 `return Kind()`。 +- Template: +```go +type SleepArgs struct { +Duration time.Duration `json:"duration"` +} +func (SleepArgs) Kind() string { return "sleep" } +``` ### Worker(执行器) +- MUST: Struct 命名格式必须为 `XxxWorker`。 +- MUST: Struct 定义上方必须添加 `// @provider(job)` 注释,以便运行 `atomctl gen provider` 自动生成注入代码。 +- MUST: Worker 必须实现 `river.Worker[T]` 接口,建议嵌入 `river.WorkerDefaults[T]` 以使用默认行为。 +- Template: +```go +// @provider(job) +type SleepWorker struct { +river.WorkerDefaults[args.SleepArgs] +} + +func (w *SleepWorker) Work(ctx context.Context, job *river.Job[args.SleepArgs]) error { +return nil +} +``` - `Work(ctx, job)`:执行入口;返回 `nil` 成功;返回 `error` 失败并按 River 策略重试。 - `river.JobSnooze(d)`:延后再跑一次,且 **不递增 attempt**;适合等待外部依赖就绪/限流等。 - `river.JobCancel(err)`:永久取消并记录原因;适合业务上永远不可能成功的情况(参数非法/语义过期等)。 @@ -244,10 +265,10 @@ In this case: ### 生成与结构 - 新增事件:`atomctl new event ` - - 会在 `backend/app/events/topics.go` 中新增 topic 常量(形如 `event:`)。 - - 会生成: - - `backend/app/events/publishers/.go`(publisher:实现 `contracts.EventPublisher`,负责 `Marshal()` + `Topic()`) - - `backend/app/events/subscribers/.go`(subscriber:实现 `contracts.EventHandler`,负责 `Topic()` + `Handler(...)`) +- 会在 `backend/app/events/topics.go` 中新增 topic 常量(形如 `event:`)。 +- 会生成: +- `backend/app/events/publishers/.go`(publisher:实现 `contracts.EventPublisher`,负责 `Marshal()` + `Topic()`) +- `backend/app/events/subscribers/.go`(subscriber:实现 `contracts.EventHandler`,负责 `Topic()` + `Handler(...)`) - 生成后:按项目约定运行一次 `atomctl gen provider`(用于刷新 DI/provider 生成文件)。 ### Topic 约定 @@ -267,10 +288,10 @@ type UserStatus string ``` - For every enum `type` defined under `backend/pkg/consts/`, you MUST also define: - - `Description() string`: return the Chinese label for the specific enum value (used by API/FE display). - - `XxxItems() []requests.KV`: return the KV list for FE dropdowns (typically `Key=enum string`, `Value=Description()`). Example: `func TenantStatusItems() []requests.KV` and call it via `consts.TenantStatusItems()`. - - Prefer `string(t)` as `Key`, and use a stable default label for unknown values (e.g. `未知` / `未知状态`). - - MUST: `Description()` and `XxxItems()` MUST be placed immediately below the enum `type` definition (same file, directly under `type Xxx string`), to keep the enum self-contained and easy to review. +- `Description() string`: return the Chinese label for the specific enum value (used by API/FE display). +- `XxxItems() []requests.KV`: return the KV list for FE dropdowns (typically `Key=enum string`, `Value=Description()`). Example: `func TenantStatusItems() []requests.KV` and call it via `consts.TenantStatusItems()`. +- Prefer `string(t)` as `Key`, and use a stable default label for unknown values (e.g. `未知` / `未知状态`). +- MUST: `Description()` and `XxxItems()` MUST be placed immediately below the enum `type` definition (same file, directly under `type Xxx string`), to keep the enum self-contained and easy to review. - Generate enum code: `atomctl gen enum` @@ -288,18 +309,18 @@ Common types: - `fields.TableNameFieldName` 必须定义在 `backend/database/fields/[table_name].go` 中,格式为 `type TableNameFieldName struct { ... }` 并为每个字段写好 `json` tag。 - 如果数据结构“不确定/随业务演进/允许任意键”,继续使用 `types.JSON`(不要强行 JSONType,以免丢字段或引入频繁迁移)。 - 服务层读写 `types.JSONType[T]`: - - 读取:`v := model.Field.Data()` - - 修改:`model.Field.Edit(func(v *T) { ... })` 或 `model.Field.Set(newValue)` +- 读取:`v := model.Field.Data()` +- 修改:`model.Field.Edit(func(v *T) { ... })` 或 `model.Field.Set(newValue)` ### 2.5 一个字段多种结构(判别联合) - 当同一个 `jsonb` 字段存在多种不同结构(同一字段承载多个 payload),不要让字段类型漂移为 `any/map`。 -- 推荐统一包裹为“判别联合”结构:`type Xxx struct { Kind string; Data json.RawMessage }`,并将该字段映射为 `types.JSONType[fields.Xxx]`。 +- 推荐统一包裹为“判别联合”结构:`type Xxx struct { Kind string ; Data json.RawMessage }`,并将该字段映射为 `types.JSONType[fields.Xxx]`。 - 写入时: - - `Kind` 建议与业务枚举/事件类型对齐,便于 SQL/报表按 `kind` 过滤。 - - `Data` 写入对应 payload 的 JSON(payload 可以是多个不同 struct)。 +- `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`,避免线上存量读取失败。 --- @@ -308,8 +329,8 @@ Common types: - 若你为任意表新增结构化审计字段(例如 `operator_user_id`、`biz_ref_type/biz_ref_id`),服务层写入必须同步补齐(避免只写 remark/JSON 导致追溯困难)。 - 注意:PostgreSQL 的可空列在本项目的 gen model 中可能会生成非指针类型(例如 `string/int64`),这会导致“未赋值”落库为 `''/0`: - - 若你要为 `(biz_ref_type,biz_ref_id,...)` 建唯一索引,**不要**只写 `IS NOT NULL` 条件; - - 应额外排除空/0(例如 `biz_ref_type <> '' AND biz_ref_id <> 0`),否则会因默认值冲突导致大量写入失败。 +- 若你要为 `(biz_ref_type,biz_ref_id,...)` 建唯一索引,**不要**只写 `IS NOT NULL` 条件; +- 应额外排除空/0(例如 `biz_ref_type <> '' AND biz_ref_id <> 0`),否则会因默认值冲突导致大量写入失败。 - Array: `types.Array[T]` - UUID: `types.UUID`, `types.BinUUID` - Date/Time: `types.Date`, `types.Time` @@ -349,9 +370,9 @@ Generator will convert snake_case columns to Go struct field names (e.g. `class_ - Services are in `backend/app/services`. - Data access boundary: - - MUST: only the `services` layer may query the database via `models.*Query`, `models.Q.*`, `gorm.DB`, or raw SQL. - - DO NOT: perform any direct database query from HTTP modules (`backend/app/http/**`) including controllers, DTO binders, or middlewares. - - HTTP modules must call `services.*` for all read/write operations. +- MUST: only the `services` layer may query the database via `models.*Query`, `models.Q.*`, `gorm.DB`, or raw SQL. +- DO NOT: perform any direct database query from HTTP modules (`backend/app/http/**`) including controllers, DTO binders, or middlewares. +- HTTP modules must call `services.*` for all read/write operations. - After creating/updating a service provider, regenerate wiring: - `atomctl gen service` - `atomctl gen provider` @@ -378,32 +399,32 @@ This section is framework-agnostic and applies to any Go service layer (regardle ### 5.1 Decide what you are testing -- **Pure unit tests**: no DB/network/filesystem; dependencies are mocked/faked; tests are fast and deterministic. +- **Pure unit tests**: no DB/network/filesystem ; dependencies are mocked/faked; tests are fast and deterministic. - **DB-backed tests (recommended whenever the feature touches the database)**: exercise a real database to validate SQL, constraints, transactions, and ORM behavior. - Always state which tier the test belongs to and keep the scope consistent. ### 5.2 Design the service for testability -- Inject dependencies via constructor or fields; depend on **interfaces**, not concrete DB clients. +- Inject dependencies via constructor or fields ; depend on **interfaces**, not concrete DB clients. - Keep domain logic **pure** where possible: parse/validate/compute should be testable without IO. - Make time/UUID/randomness deterministic by injecting `Clock`/`IDGenerator` when needed. -- If the feature requires database access, **do not mock the database**; test with an **actual database** (ideally same engine/version as production) to ensure data accuracy. Use mocks/fakes only for non-DB external dependencies when appropriate (e.g., HTTP, SMS, third-party APIs). +- If the feature requires database access, **do not mock the database** ; test with an **actual database** (ideally same engine/version as production) to ensure data accuracy. Use mocks/fakes only for non-DB external dependencies when appropriate (e.g., HTTP, SMS, third-party APIs). ### 5.3 Test structure and conventions - Prefer `*_test.go` with table-driven tests and subtests: `t.Run("case", func(t *testing.T) { ... })`. - Prefer testing the public API from an external package (`package xxx_test`) unless you must access unexported helpers. - Avoid “focused” tests in committed code (e.g. `FocusConvey`, `FIt`, `fit`, `it.only`, or equivalent), because they silently skip other tests. -- MUST: in service layer tests, **one test method should focus on one service method** only (e.g. `Test_Freeze` covers `Ledger.Freeze`, `Test_Unfreeze` covers `Ledger.Unfreeze`); do not bundle multiple service methods into a single `Test_*` method. +- MUST: in service layer tests, **one test method should focus on one service method** only (e.g. `Test_Freeze` covers `Ledger.Freeze`, `Test_Unfreeze` covers `Ledger.Unfreeze`) ; do not bundle multiple service methods into a single `Test_*` method. - MUST: within that single `Test_` function, cover the method’s key behavior contracts and boundary conditions via subcases (`Convey` blocks or `t.Run`) so the method’s behavior can be reviewed in one place (do NOT claim to cover “all edge cases”, but cover the important ones). -- MUST (minimum set): for each service method test, cover at least: happy path; invalid params / precondition failures; insufficient resources / permission denied (if applicable); idempotency/duplicate call behavior (if applicable); and at least one typical persistence/transaction failure branch (if it is hard to simulate reliably, move that branch coverage to a DB-backed integration/e2e test). +- MUST (minimum set): for each service method test, cover at least: happy path ; invalid params / precondition failures; insufficient resources / permission denied (if applicable); idempotency/duplicate call behavior (if applicable); and at least one typical persistence/transaction failure branch (if it is hard to simulate reliably, move that branch coverage to a DB-backed integration/e2e test). ### 5.4 Isolation rules - Each test must be independent and order-agnostic. - For integration tests: - - Use transaction rollback per test when possible; otherwise use truncate + deterministic fixtures. - - Never depend on developer-local state; prefer ephemeral DB (container) or a dedicated test database/schema. +- Use transaction rollback per test when possible ; otherwise use truncate + deterministic fixtures. +- Never depend on developer-local state ; prefer ephemeral DB (container) or a dedicated test database/schema. ### 5.5 Assertions and error checks @@ -436,7 +457,7 @@ import ( type XxxTestSuiteInjectParams struct { dig.In - DB *sql.DB + DB *sql.DB Initials []contracts.Initial `group:"initials"` } @@ -464,4 +485,4 @@ func (s *XxxTestSuite) Test_Method() { So(got, ShouldBeNil) }) } -``` \ No newline at end of file +```