# Backend Dev Rules (HTTP API + Model) This file condenses `backend_v1/docs/dev/http_api.md` + `backend_v1/docs/dev/model.md` into a checklist/rule format for LLMs. --- ## 0) Golden rules (DO / DO NOT) - DO follow existing module layout under `backend_v1/app/http//`. - DO keep controller methods thin: parse/bind → call `services.*` → return result/error. - DO regenerate code after changes (routes/docs/models). - DO add `// @provider` above every controller/service `struct` declaration. - DO keep HTTP middlewares in `backend_v1/app/middlewares/` only. - DO keep all `const` declarations in `backend_v1/pkg/consts/` only (do not declare constants elsewhere). - DO NOT manually edit generated files: - `backend_v1/app/http/**/routes.gen.go` - `backend_v1/app/http/**/provider.gen.go` - `backend_v1/docs/docs.go` - DO NOT manually write provider declarations (only `atomctl gen provider`). - 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: 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. - MUST: in `backend_v1/app/services`, add Chinese comments at key steps to explain business intent and invariants (e.g., 事务边界、幂等语义、余额冻结/扣减/回滚、权限/前置条件校验点), avoid “what the code does” boilerplate. --- ## 1) Add a new HTTP API endpoint ### 1.1 Where code lives - Controllers: `backend_v1/app/http//*.go` - Example module: `backend_v1/app/http/super/tenant.go`, `backend_v1/app/http/super/user.go` - DTOs: `backend_v1/app/http//dto/*` - HTTP middlewares: `backend_v1/app/middlewares/*` - Routes (generated): `backend_v1/app/http//routes.gen.go` - Swagger output (generated): `backend_v1/docs/swagger.yaml`, `backend_v1/docs/swagger.json`, `backend_v1/docs/docs.go` ### 1.2 Controller method signatures - “Return data” endpoints: return `(, error)` - Example: `(*requests.Pager, error)` for paginated list - “No data” endpoints: return `error` ### 1.3 Swagger annotations (minimum set) Place above the handler function: - `@Summary` - `@Tags` - `@Accept json` - `@Produce json` - `@Param` (query/path/body as needed) - `@Success` for 200 responses - `@Router [get|post|patch|delete|put]` - `@Bind` for parameters (see below) Common `@Success` patterns: - Paginated list: `requests.Pager{items=dto.Item}` - Single object: `dto.Item` - Array: `{array} dto.Item` ### 1.4 Parameter binding (@Bind) Format: `@Bind [key()] [model(|[:])]` Positions: - `path`, `query`, `body`, `header`, `cookie`, `local`, `file` Notes: - `paramName` MUST match function parameter name (case-sensitive). - Default key name is `paramName` ; override via `key(...)`. - Scalar types: `string/int/int32/int64/float32/float64/bool`. - Pointer types are supported (framework will handle deref for most positions). #### Model binding (path-only) Used to bind a model instance from a path value: - `model(id)` (recommended) - `model(id:int)` / `model(code:string)` - `model(pkg.Type:field)` or `model(pkg.Type)` (default field is `id`) Behavior: - Generated binder queries by field and returns first row as the parameter value. - Auto-imports field helper for query building. ### 1.5 Generate routes + providers + swagger docs Run from `backend_v1/`: - Generate routes: `atomctl gen route` - Generate providers: `atomctl gen provider` - Generate swagger docs: `atomctl swag init` ### 1.6 Local verify - Build/run: `make run` - Use REST client examples: `tests/[module]/[controller].http` (extend it for new endpoints) ### 1.7 Testing - Prefer existing test style under `backend_v1/tests/e2e`. - Run: `make test` ### 1.8 Module-level route group (Path + Middlewares) If you need to define a module HTTP middleware (applies to the module route group): 1) Run `atomctl gen route` first. 2) Edit `backend_v1/app/http//routes.manual.go`: - Update `Path()` to return the current module route group prefix (must match the prefix used in `routes.gen.go`, e.g. `/super/v1`, `/t/:tenantCode/v1`). - Update `Middlewares()` return value: return a list like `[]any{r.middlewares.MiddlewareFunc1, r.middlewares.MiddlewareFunc2, ...}` (no `(...)`), where each item is `r.middlewares.` referencing middleware definitions in `backend_v1/app/middlewares`. --- ## 2) Add / update a DB model Models live in: - `backend_v1/database/models/*` (generated model code + optional manual extensions) ### 2.1 Migration → model generation workflow 1) Create migration: - `atomctl migrate create alter_table` or `atomctl migrate create create_table` 2) Edit migration: - No explicit `BEGIN/COMMIT` needed (framework handles). - Table name should be plural (e.g. `tenants`). - MUST: when writing migration content, every field/column MUST include a brief Chinese remark, and also include commented details for that field’s usage scenario and rules/constraints (e.g., valid range/format, default behavior, special cases). 3) Apply migration: - `atomctl migrate up` 4) Map complex field types (JSON/ARRAY/UUID/…) via transform file: - `backend_v1/database/.transform.yaml` → `field_type.` 5) Generate models: - `atomctl gen model` ### 2.2 Enum strategy - DO NOT use native DB ENUM. - Define enums in Go under `backend_v1/pkg/consts/
.go`, example: ```go // swagger:enum UserStatus // ENUM(pending_verify, verified, banned, ) type UserStatus string ``` - For every enum `type` defined under `backend_v1/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. - Generate enum code: `atomctl gen enum` ### 2.3 Supported field types (`gen/types/`) `backend_v1/database/.transform.yaml` typically imports `go.ipao.vip/gen` so you can use `types.*` in `field_type`. Common types: - JSON: `types.JSON`, `types.JSONMap`, `types.JSONType[T]`, `types.JSONSlice[T]` - Array: `types.Array[T]` - UUID: `types.UUID`, `types.BinUUID` - Date/Time: `types.Date`, `types.Time` - Money/XML/URL/Binary: `types.Money`, `types.XML`, `types.URL`, `types.HexBytes` - Bit string: `types.BitString` - Network: `types.Inet`, `types.CIDR`, `types.MACAddr` - Ranges: `types.Int4Range`, `types.Int8Range`, `types.NumRange`, `types.TsRange`, `types.TstzRange`, `types.DateRange` - Geometry: `types.Point`, `types.Polygon`, `types.Box`, `types.Circle`, `types.Path` - Fulltext: `types.TSQuery`, `types.TSVector` - Nullable: `types.Null[T]` and aliases (requires DB NULL) Reference: - Detailed examples: `gen/types/README.md` ### 2.4 Relationships (GORM-aligned) via `.transform.yaml` Define in `field_relate.
.`: - `relation`: `belongs_to` | `has_one` | `has_many` | `many_to_many` - `table`: target table - `pivot`: join table (many_to_many only) - `foreign_key`, `references` - `join_foreign_key`, `join_references` (many_to_many only) - `json`: JSON field name in API outputs Generator will convert snake_case columns to Go struct field names (e.g. `class_id` → `ClassID`). ### 2.5 Extending generated models - Add manual methods/hooks by creating `backend_v1/database/models/
.go`. - Keep generated files untouched ; put custom logic only in your own file(s). --- ## 3) Service layer injection (when adding services) - Services are in `backend_v1/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_v1/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` - Injection rule: provider injected dependencies MUST be `success`. do not add business-level fallbacks for injection objects nil check. - Service call conventions: - **Service-to-service (inside `services` package)**: call directly as `CamelCaseServiceStructName.Method()` (no `services.` prefix). - **From outside (controllers/handlers/etc.)**: call via the package entrypoint `services.CamelCaseServiceStructName.Method()`. --- ## 4) Quick command summary (run in `backend_v1/`) - `make run` / `make build` / `make test` - `atomctl gen route` / `atomctl gen provider` / `atomctl swag init` - `atomctl migrate create ...` / `atomctl migrate up` - `atomctl gen model` / `atomctl gen enum` / `atomctl gen service` - `make init` (full refresh) --- ## 5) Service Layer Unit Testing Guidelines (Generic) This section is framework-agnostic and applies to any Go service layer (regardless of DI container, ORM, or web framework). ### 5.1 Decide what you are testing - **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. - 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). ### 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: 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). ### 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. ### 5.5 Assertions and error checks - Always assert both **result** and **error** (and error types via `errors.Is` / `errors.As` when wrapping is used). - Keep assertions minimal but complete: verify behavior, not implementation details. - Use the standard library (`testing`) or a single assertion library consistently across the repo. ### 5.6 Minimal test file template (DI-bootstrapped, DB-backed) This template matches a common pattern where tests boot a DI container and run against a real database. Replace the bootstrap (`testx.Default/Serve`, `Provide`) and cleanup (`database.Truncate`) with your project's equivalents. ```go package services import ( "database/sql" "testing" "quyun/v2/app/commands/testx" "quyun/v2/database" "quyun/v2/database/models" . "github.com/smartystreets/goconvey/convey" "github.com/stretchr/testify/suite" "go.ipao.vip/atom/contracts" "go.uber.org/dig" ) type XxxTestSuiteInjectParams struct { dig.In DB *sql.DB Initials []contracts.Initial `group:"initials"` } type XxxTestSuite struct { suite.Suite XxxTestSuiteInjectParams } func Test_Xxx(t *testing.T) { providers := testx.Default().With(Provide) testx.Serve(providers, t, func(p XxxTestSuiteInjectParams) { suite.Run(t, &XxxTestSuite{XxxTestSuiteInjectParams: p}) }) } func (s *XxxTestSuite) Test_Method() { Convey("describe behavior here", s.T(), func() { ctx := s.T().Context() database.Truncate(ctx, s.DB, models.TableNameUser) got, err := User.FindByUsername(ctx, "alice") So(err, ShouldNotBeNil) So(got, ShouldBeNil) }) } ```