Files
quyun-v2/backend/llm.txt
2025-12-18 15:14:48 +08:00

331 lines
13 KiB
Plaintext
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 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.
---
## 0) Golden rules (DO / DO NOT)
- DO follow existing module layout under `backend/app/http/<module>/`.
- 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/app/middlewares/` only.
- DO keep all `const` declarations in `backend/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`
- 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/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/app/http/<module>/*.go`
- Example module: `backend/app/http/super/tenant.go`, `backend/app/http/super/user.go`
- DTOs: `backend/app/http/<module>/dto/*`
- HTTP middlewares: `backend/app/middlewares/*`
- Routes (generated): `backend/app/http/<module>/routes.gen.go`
- Swagger output (generated): `backend/docs/swagger.yaml`, `backend/docs/swagger.json`, `backend/docs/docs.go`
### 1.2 Controller method signatures
- “Return data” endpoints: return `(<T>, 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 <path> [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 <paramName> <position> [key(<key>)] [model(<field>|<type>[:<field>])]`
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/`:
- 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/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/app/http/<module>/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.<MiddlewareFunc>` referencing middleware definitions in `backend/app/middlewares`.
---
## 2) Add / update a DB model
Models live in:
- `backend/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 fields 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/database/.transform.yaml` → `field_type.<table>`
5) Generate models:
- `atomctl gen model`
### 2.2 Enum strategy
- DO NOT use native DB ENUM.
- Define enums in Go under `backend/pkg/consts/<table>.go`, example:
```go
// swagger:enum UserStatus
// ENUM(pending_verify, verified, banned, )
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.
- 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`.
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.<table>.<FieldName>`:
- `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/database/models/<table>.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`.
- 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/`)
- `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_<Method>` function, cover the methods key behavior contracts and boundary conditions via subcases (`Convey` blocks or `t.Run`) so the methods 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)
})
}
```