This commit is contained in:
@@ -1,19 +1,28 @@
|
|||||||
# Backend Dev Rules (HTTP API + Model)
|
# 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)
|
## 0) Golden rules (DO / DO NOT)
|
||||||
|
|
||||||
- DO follow existing module layout under `backend/app/http/<module>/`.
|
- DO follow existing module layout under `backend_v1/app/http/<module>/`.
|
||||||
- DO keep controller methods thin: parse/bind → call `services.*` → return result/error.
|
- DO keep controller methods thin: parse/bind → call `services.*` → return result/error.
|
||||||
- DO regenerate code after changes (routes/docs/models).
|
- 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:
|
- DO NOT manually edit generated files:
|
||||||
- `backend/app/http/**/routes.gen.go`
|
- `backend_v1/app/http/**/routes.gen.go`
|
||||||
- `backend/app/http/**/provider.gen.go`
|
- `backend_v1/app/http/**/provider.gen.go`
|
||||||
- `backend/docs/docs.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`).
|
- 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
|
### 1.1 Where code lives
|
||||||
|
|
||||||
- Controllers: `backend/app/http/<module>/*.go`
|
- Controllers: `backend_v1/app/http/<module>/*.go`
|
||||||
- Example module: `backend/app/http/super/tenant.go`, `backend/app/http/super/user.go`
|
- Example module: `backend_v1/app/http/super/tenant.go`, `backend_v1/app/http/super/user.go`
|
||||||
- DTOs: `backend/app/http/<module>/dto/*`
|
- DTOs: `backend_v1/app/http/<module>/dto/*`
|
||||||
- Routes (generated): `backend/app/http/<module>/routes.gen.go`
|
- HTTP middlewares: `backend_v1/app/middlewares/*`
|
||||||
- Swagger output (generated): `backend/docs/swagger.yaml`, `backend/docs/swagger.json`, `backend/docs/docs.go`
|
- Routes (generated): `backend_v1/app/http/<module>/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
|
### 1.2 Controller method signatures
|
||||||
|
|
||||||
@@ -84,7 +94,7 @@ Behavior:
|
|||||||
|
|
||||||
### 1.5 Generate routes + providers + swagger docs
|
### 1.5 Generate routes + providers + swagger docs
|
||||||
|
|
||||||
Run from `backend/`:
|
Run from `backend_v1/`:
|
||||||
|
|
||||||
- Generate routes: `atomctl gen route`
|
- Generate routes: `atomctl gen route`
|
||||||
- Generate providers: `atomctl gen provider`
|
- Generate providers: `atomctl gen provider`
|
||||||
@@ -93,20 +103,29 @@ Run from `backend/`:
|
|||||||
### 1.6 Local verify
|
### 1.6 Local verify
|
||||||
|
|
||||||
- Build/run: `make run`
|
- 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
|
### 1.7 Testing
|
||||||
|
|
||||||
- Prefer existing test style under `backend/tests/e2e`.
|
- Prefer existing test style under `backend_v1/tests/e2e`.
|
||||||
- Run: `make test`
|
- 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/<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_v1/app/middlewares`.
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## 2) Add / update a DB model
|
## 2) Add / update a DB model
|
||||||
|
|
||||||
Models live in:
|
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
|
### 2.1 Migration → model generation workflow
|
||||||
|
|
||||||
@@ -118,6 +137,7 @@ Models live in:
|
|||||||
|
|
||||||
- No explicit `BEGIN/COMMIT` needed (framework handles).
|
- No explicit `BEGIN/COMMIT` needed (framework handles).
|
||||||
- Table name should be plural (e.g. `tenants`).
|
- 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:
|
3) Apply migration:
|
||||||
|
|
||||||
@@ -125,7 +145,7 @@ Models live in:
|
|||||||
|
|
||||||
4) Map complex field types (JSON/ARRAY/UUID/…) via transform file:
|
4) Map complex field types (JSON/ARRAY/UUID/…) via transform file:
|
||||||
|
|
||||||
- `backend/database/.transform.yaml` → `field_type.<table>`
|
- `backend_v1/database/.transform.yaml` → `field_type.<table>`
|
||||||
|
|
||||||
5) Generate models:
|
5) Generate models:
|
||||||
|
|
||||||
@@ -134,7 +154,7 @@ Models live in:
|
|||||||
### 2.2 Enum strategy
|
### 2.2 Enum strategy
|
||||||
|
|
||||||
- DO NOT use native DB ENUM.
|
- DO NOT use native DB ENUM.
|
||||||
- Define enums in Go under `backend/pkg/consts/<table>.go`, example:
|
- Define enums in Go under `backend_v1/pkg/consts/<table>.go`, example:
|
||||||
|
|
||||||
```go
|
```go
|
||||||
// swagger:enum UserStatus
|
// swagger:enum UserStatus
|
||||||
@@ -142,11 +162,17 @@ Models live in:
|
|||||||
type UserStatus string
|
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`
|
- Generate enum code: `atomctl gen enum`
|
||||||
|
|
||||||
### 2.3 Supported field types (`gen/types/`)
|
### 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:
|
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
|
### 2.5 Extending generated models
|
||||||
|
|
||||||
- Add manual methods/hooks by creating `backend/database/models/<table>.go`.
|
- Add manual methods/hooks by creating `backend_v1/database/models/<table>.go`.
|
||||||
- Keep generated files untouched ; put custom logic only in your own file(s).
|
- Keep generated files untouched ; put custom logic only in your own file(s).
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## 3) Service layer injection (when adding services)
|
## 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:
|
- After creating/updating a service provider, regenerate wiring:
|
||||||
- `atomctl gen service`
|
- `atomctl gen service`
|
||||||
- `atomctl gen provider`
|
- `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 call conventions:
|
||||||
- **Service-to-service (inside `services` package)**: call directly as `CamelCaseServiceStructName.Method()` (no `services.` prefix).
|
- **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()`.
|
- **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`
|
- `make run` / `make build` / `make test`
|
||||||
- `atomctl gen route` / `atomctl gen provider` / `atomctl swag init`
|
- `atomctl gen route` / `atomctl gen provider` / `atomctl swag init`
|
||||||
- `atomctl migrate create ...` / `atomctl migrate up`
|
- `atomctl migrate create ...` / `atomctl migrate up`
|
||||||
- `atomctl gen model` / `atomctl gen enum` / `atomctl gen service`
|
- `atomctl gen model` / `atomctl gen enum` / `atomctl gen service`
|
||||||
- `make init` (full refresh)
|
- `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 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)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|||||||
Reference in New Issue
Block a user