diff --git a/backend_v1/llm.txt b/backend_v1/llm.txt index 6d69a38..70e0515 100644 --- a/backend_v1/llm.txt +++ b/backend_v1/llm.txt @@ -1,19 +1,28 @@ # Backend Dev Rules (HTTP API + Model) -This file condenses `backend/docs/dev/http_api.md` + `backend/docs/dev/model.md` into a checklist/rule format for LLMs. +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/app/http//`. +- 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/app/http/**/routes.gen.go` -- `backend/app/http/**/provider.gen.go` -- `backend/docs/docs.go` +- `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. --- @@ -21,11 +30,12 @@ This file condenses `backend/docs/dev/http_api.md` + `backend/docs/dev/model.md` ### 1.1 Where code lives -- Controllers: `backend/app/http//*.go` -- Example module: `backend/app/http/super/tenant.go`, `backend/app/http/super/user.go` -- DTOs: `backend/app/http//dto/*` -- Routes (generated): `backend/app/http//routes.gen.go` -- Swagger output (generated): `backend/docs/swagger.yaml`, `backend/docs/swagger.json`, `backend/docs/docs.go` +- 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 @@ -84,7 +94,7 @@ Behavior: ### 1.5 Generate routes + providers + swagger docs -Run from `backend/`: +Run from `backend_v1/`: - Generate routes: `atomctl gen route` - Generate providers: `atomctl gen provider` @@ -93,20 +103,29 @@ Run from `backend/`: ### 1.6 Local verify - Build/run: `make run` -- Use REST client examples: `backend/test/[module]/[controller].http` (extend it for new endpoints) +- Use REST client examples: `tests/[module]/[controller].http` (extend it for new endpoints) ### 1.7 Testing -- Prefer existing test style under `backend/tests/e2e`. +- 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/database/models/*` (generated model code + optional manual extensions) +- `backend_v1/database/models/*` (generated model code + optional manual extensions) ### 2.1 Migration → model generation workflow @@ -118,6 +137,7 @@ Models live in: - 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: @@ -125,7 +145,7 @@ Models live in: 4) Map complex field types (JSON/ARRAY/UUID/…) via transform file: -- `backend/database/.transform.yaml` → `field_type.` +- `backend_v1/database/.transform.yaml` → `field_type.
` 5) Generate models: @@ -134,7 +154,7 @@ Models live in: ### 2.2 Enum strategy - DO NOT use native DB ENUM. -- Define enums in Go under `backend/pkg/consts/
.go`, example: +- Define enums in Go under `backend_v1/pkg/consts/
.go`, example: ```go // swagger:enum UserStatus @@ -142,11 +162,17 @@ Models live in: 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/database/.transform.yaml` typically imports `go.ipao.vip/gen` so you can use `types.*` in `field_type`. +`backend_v1/database/.transform.yaml` typically imports `go.ipao.vip/gen` so you can use `types.*` in `field_type`. Common types: @@ -181,27 +207,128 @@ Generator will convert snake_case columns to Go struct field names (e.g. `class_ ### 2.5 Extending generated models -- Add manual methods/hooks by creating `backend/database/models/
.go`. -- Keep generated files untouched ; put custom logic only in your own file(s). +- 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/app/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` +- `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()`. +- **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/`) +## 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) + }) +} +```