Compare commits

...

2 Commits

Author SHA1 Message Date
9b7093da26 feat: add llm.txt 2025-12-17 17:50:14 +08:00
70c9094001 feat: 重构认证控制器,统一类型命名为auth 2025-12-17 16:22:10 +08:00
9 changed files with 539 additions and 223 deletions

View File

@@ -12,7 +12,7 @@ import (
)
// @provider
type authController struct {
type auth struct {
app *app.Config
jwt *jwt.JWT
}
@@ -27,7 +27,7 @@ type authController struct {
//
// @Router /super/v1/auth/login [post]
// @Bind form body
func (ctl *authController) login(ctx fiber.Ctx, form *dto.LoginForm) (*dto.LoginResponse, error) {
func (ctl *auth) login(ctx fiber.Ctx, form *dto.LoginForm) (*dto.LoginResponse, error) {
m, err := services.User.FindByUsername(ctx, form.Username)
if err != nil {
return nil, errorx.Wrap(err).WithMsg("用户名或密码错误")

View File

@@ -14,8 +14,8 @@ func Provide(opts ...opt.Option) error {
if err := container.Container.Provide(func(
app *app.Config,
jwt *jwt.JWT,
) (*authController, error) {
obj := &authController{
) (*auth, error) {
obj := &auth{
app: app,
jwt: jwt,
}
@@ -25,14 +25,14 @@ func Provide(opts ...opt.Option) error {
return err
}
if err := container.Container.Provide(func(
authController *authController,
auth *auth,
tenant *tenant,
user *user,
) (contracts.HttpRoute, error) {
obj := &Routes{
authController: authController,
tenant: tenant,
user: user,
auth: auth,
tenant: tenant,
user: user,
}
if err := obj.Prepare(); err != nil {
return nil, err

View File

@@ -20,9 +20,9 @@ import (
type Routes struct {
log *log.Entry `inject:"false"`
// Controller instances
authController *authController
tenant *tenant
user *user
auth *auth
tenant *tenant
user *user
}
// Prepare initializes the routes provider with logging configuration.
@@ -40,10 +40,10 @@ func (r *Routes) Name() string {
// Register registers all HTTP routes with the provided fiber router.
// Each route is registered with its corresponding controller action and parameter bindings.
func (r *Routes) Register(router fiber.Router) {
// Register routes for controller: authController
r.log.Debugf("Registering route: Post /super/v1/auth/login -> authController.login")
// Register routes for controller: auth
r.log.Debugf("Registering route: Post /super/v1/auth/login -> auth.login")
router.Post("/super/v1/auth/login", DataFunc1(
r.authController.login,
r.auth.login,
Body[dto.LoginForm]("form"),
))
// Register routes for controller: tenant

View File

@@ -1,71 +0,0 @@
package tenancy
import (
"database/sql"
"regexp"
"strings"
"quyun/v2/app/errorx"
"github.com/gofiber/fiber/v3"
"github.com/google/uuid"
)
const (
LocalTenantCode = "tenant_code"
LocalTenantID = "tenant_id"
LocalTenantUUID = "tenant_uuid"
)
var tenantCodeRe = regexp.MustCompile(`^[a-z0-9_-]+$`)
type Tenant struct {
ID int64
Code string
UUID uuid.UUID
}
func ResolveTenant(c fiber.Ctx, db *sql.DB) (*Tenant, error) {
raw := strings.TrimSpace(c.Params("tenant_code"))
code := strings.ToLower(raw)
if code == "" || !tenantCodeRe.MatchString(code) {
return nil, errorx.ErrInvalidParameter.WithMsg("invalid tenant_code")
}
var (
id int64
tenantUUID uuid.UUID
status int16
)
err := db.QueryRowContext(
c.Context(),
`SELECT id, tenant_uuid, status FROM tenants WHERE lower(tenant_code) = $1 LIMIT 1`,
code,
).Scan(&id, &tenantUUID, &status)
if err != nil {
if err == sql.ErrNoRows {
return nil, fiber.ErrNotFound
}
return nil, errorx.ErrDatabaseError.WithMsg("database error").WithParams(err.Error())
}
// status: 0 enabled (by default)
if status != 0 {
return nil, fiber.ErrNotFound
}
return &Tenant{ID: id, Code: code, UUID: tenantUUID}, nil
}
func Middleware(db *sql.DB) fiber.Handler {
return func(c fiber.Ctx) error {
tenant, err := ResolveTenant(c, db)
if err != nil {
return err
}
c.Locals(LocalTenantCode, tenant.Code)
c.Locals(LocalTenantID, tenant.ID)
c.Locals(LocalTenantUUID, tenant.UUID.String())
return c.Next()
}
}

View File

@@ -1,138 +0,0 @@
# Backend 新增 HTTP 接口流程Go + Fiber + atomctl
本文档描述在本仓库 `backend/` 中新增一个 HTTP 接口(例如 `/super/v1/...`的标准流程包含路由生成、Swagger 文档生成、参数绑定与测试验证。
## 相关目录
- 路由与 Controller`backend/app/http/**`
- Super 端示例:`backend/app/http/super/*.go`
- 生成路由:`backend/app/http/super/routes.gen.go`(自动生成,勿手改)
- Swagger 文档:
- `backend/docs/swagger.yaml`
- `backend/docs/swagger.json`
- `backend/docs/docs.go`(自动生成,勿手改)
- 本地接口调试示例:`backend/super.http`
- 命令:`backend/Makefile``make init`/`make run`/`make test` 等)
## 增加一个新接口(推荐步骤)
### 1) 选择模块与 BasePath
- **Super 管理端**:一般挂在 `/super/v1/...`,代码放在 `backend/app/http/super/`
- **租户端**:项目 `main.go` 注解里有 `@BasePath /t/{tenant_code}/v1`,通常租户端接口会以该前缀为基础(具体以现有路由模块为准)。
先决定:
- `METHOD`GET/POST/PATCH/DELETE…
- `PATH`:例如 `/super/v1/users/{id}``/super/v1/users/:id`
- 鉴权:是否需要 token/权限(跟随模块现有中间件)
- 请求path/query/body
- 响应:结构体 / 分页 / KV 列表等
### 2) 定义请求 DTOFilter/Form与响应 DTO
Super 模块常见模式:
- 列表分页:`dto.*Filter` / `dto.*PageFilter` + 返回 `requests.Pager`
- 更新类接口:`dto.*UpdateForm`body
- KV 枚举列表:返回 `[]requests.KV`
可参考:
- `backend/app/http/super/dto/*`
- `backend/app/http/super/user.go``backend/app/http/super/tenant.go`
### 3) 编写 Controller 方法(带 Swagger 注解 + Bind
在对应模块的 `*.go` 中新增方法,保持和现有风格一致:
- Controller struct 上保留 `// @provider`
- 方法上补齐 swagger 注解:`@Summary/@Tags/@Param/@Success/@Router`
- 使用 `@Bind` 约定参数来源path/query/body
示例(参考 `backend/app/http/super/user.go`
```go
// @Summary 用户状态列表
// @Tags Super
// @Accept json
// @Produce json
// @Success 200 {array} requests.KV
// @Router /super/v1/users/statuses [get]
func (*user) statusList(ctx fiber.Ctx) ([]requests.KV, error) { ... }
```
注意:
- `@Router` 的路径写法通常与 Fiber 路由一致(例如 `:userID`)。
- 参数绑定会驱动路由代码生成(见下一步)。
### 4) 连接业务层Service
Controller 内尽量只做:
- 参数解析/校验
- 调用 `services.*` 完成业务
- 返回结果或 error
业务逻辑集中放在 `backend/app/services`(结合现有实现),涉及 DB 的部分走现有模型/仓储层(依项目既有组织)。
### 5) 生成路由代码routes.gen.go
本项目路由是由 `atomctl` 自动生成的。
常用命令(在 `backend/` 下执行):
- 仅生成路由:`atomctl gen route`
- 全量初始化/更新(含 swagger/enum/route/service 等):`make init`
生成完成后检查:
- `backend/app/http/<module>/routes.gen.go` 是否出现新路由
- 路由 METHOD/PATH、参数绑定是否正确
### 6) 生成 Swagger 文档swagger.yaml/json/docs.go
Swagger 也是由工具生成并落盘到 `backend/docs/`
- `atomctl swag init`
- 或直接 `make init`(会包含该步骤)
生成完成后检查:
- `backend/docs/swagger.yaml``backend/docs/swagger.json` 是否包含新接口
- `backend/docs/docs.go` 是否同步更新
### 7) 本地验证
启动:
- `make run`(会先 `make build`
验证方式:
- 使用 `backend/super.http` 增加/执行请求
- 或用 curl/Swagger UI若项目已暴露 swagger 页面)
### 8) 增加测试(建议)
优先参考现有 e2e 测试:
- `backend/tests/e2e/*`
覆盖至少:
- 正常请求返回
- 参数缺失/非法
- 权限不足/未登录(如该接口需要鉴权)
运行:
- `make test`
## 常见注意事项
- 不要手改 `*.gen.go``backend/docs/docs.go`:它们由 `atomctl` 生成。
- 确认查询参数命名与 swagger 一致(例如 `page/limit/asc/desc/status`),前端会按 swagger 拼 query。
- 路由路径参数请在 `@Router` 与函数签名/`@Bind` 里保持一致(例如 `tenantID``userID`)。

View File

@@ -0,0 +1,92 @@
# 新增 HTTP 接口流程
项目 `controller` 定义于 `backend/app/http/[module_name]/[controller].go` 文件中。 每个 `controller` 负责处理一组相关的 HTTP 请求。 例如,`backend/app/http/super/tenant.go` 负责处理与租户相关的请求。
## 新增接口步骤
1.`controller` 中新增方法以处理特定的 HTTP 请求。 例如,在 `tenant.go` 新增一个 `list` 方法来处理列出租户的请求。
2. 定义相关 `swagger` 注解以生成 API 文档。 这些注解通常位于方法上方,描述了请求路径、参数和响应格式。
3. 在模块 `dto/`(数据传输对象)目录中定义请求和响应的数据结构, 以确保数据的一致性和类型安全。
4. 运行 `atomctl gen route` 生成路由文件,确保新接口被正确注册。
5. 运行 `atomctl gen provider` 生成路由文件,确保新接口被正确注册。
## 接口定义示例:
1. 实现需要返回数据的接口。
```go
func (*tenant) list(ctx fiber.Ctx, filter *dto.TenantFilter) (*requests.Pager, error) {
return nil,nil
}
```
2. 实现不需要返回数据的接口
```go
func (*tenant) update(ctx fiber.Ctx, tenantID int64, form *dto.TenantExpireUpdateForm) error {
return nil
}
```
## swagger 注解说明
- **@Summary**: 接口的简要描述。
- **@Description**: 接口的详细描述。
- **@Tags**: 接口所属的分类标签。
- **@Accept**: 接口接受的数据格式。通常为 `json`
- **@Produce**: 接口返回的数据格式。通常为 `json`
- **@Param**: 定义接口的参数,包括参数名称、数据来源位置、数据类型、是否必须、和描述。如:`// @Param form body dto.LoginForm true "form"`
- **@Success**: 定义接口成功时的响应格式。如:
- 返回分页列表 `// @Success 200 {object} requests.Pager{items=dto.Item}`
- 直接返回对象 `// @Success 200 {object} dto.Item`
- 返回数据对象列表 `// @Success 200 {array} dto.Item`
- **@Router**: 定义接口的路由信息,包括路径和请求方法。如:`// @Router /super/tenants [get|post|put|delete|patch]`, 如果需要定义 path 参数,使用 `:paramName` 语法表示,如:`/super/tenants/:tenantID`
- **@Bind**: 定义参数绑定方式,格式: `@Bind <paramName> <position> [key(<key>)] [model(<field>|<model>[:<field>])]`
- `paramName` 与方法参数名一致(大小写敏感)
- `position``path``query``body``header``cookie``local``file`
- 可选:
- `key()` 覆盖默认键名;
- `model()` 详见“模型绑定”。
### 参数绑定
- query标量用 `QueryParam[T]("key")`,非标量用 `Query[T]("key")`
- path标量用 `PathParam[T]("key")`,非标量用 `Path[T]("key")`
- 若使用 `model()`(仅在 path 有效),会按字段值查询并绑定为 `T`,详见下文
- header`Header[T]("key")`
- body`Body[T]("key")`
- cookie`string``CookieParam("key")`,其他用 `Cookie[T]("key")`
- file`File[multipart.FileHeader]("key")`
- local`Local[T]("key")`
说明:
- 标量类型集合:`string``int``int32``int64``float32``float64``bool`
- `key` 默认等于 `paramName`;设置 `key(...)` 后以其为准
- `file` 使用固定类型 `multipart.FileHeader`
### 类型与指针处理
- 支持 `T``*T``pkg.T``*pkg.T`;会正确收集选择子表达式对应 import
- 忽略结尾为 `Context``Ctx` 的参数(框架上下文)
- 指针处理:除 `local` 外会去掉前导 `*` 作为泛型实参;`local` 保留指针(便于写回)
### 模型绑定
`@Bind ... model(...)` 配合 `position=path` 使用时,将根据路径参数值查询模型并绑定为方法参数类型的实例(`T` 来自方法参数)。
- 语法:
- 仅字段:`model(id)`(推荐)
- 指定字段与类型:`model(id:int)``model(code:string)`(用于非字符串路径参数)
- 指定类型与字段:`model(pkg.Type:field)``model(pkg.Type)`(字段缺省为 `id`
- 行为:
- 生成的绑定器会按给定字段构造查询条件并返回首条记录
- 自动注入 import`field "go.ipao.vip/gen/field"`,用于构造字段条件表达式
示例:
```go
// @Router /users/:id [get]
// @Bind user path key(id) model(id)
func (uc *UserController) Show(ctx context.Context, user *models.User) (*UserDTO, error)
```

226
backend/docs/dev/model.md Normal file
View File

@@ -0,0 +1,226 @@
# 新增 model 流程
项目 `models` 定义于 `backend/database/models` 文件中。 每个 `model` 对应数据库中的一张表。 新增 `model` 的步骤如下:
## 步骤
1. 运行 `atomctl migrate create [alter|create_table]` 创建迁移文件。
2. 编辑生成的迁移文件,定义数据库表结构变更。不需要声明 `BEGIN``COMMIT`框架会自动处理。table 名称使用复数形式,例如 `tenants`
3. 执行 `atomctl migrate up` 应用迁移,更新数据库结构。
4. 对于 `JSON` `ARRAY` `ENUM` 等复杂字段类型,编辑 `database/.transform.yaml` 文件,在 `field_type.[table_name]` 定义字段与 Go 类型的映射关系。支持定义的数据类型参考数据类型章节
5. 运行 `atomctl gen model` 生成或更新 `models` 代码,确保代码与数据库结构同步。
## 数据类型
### Enum 类型
不使用数据库原生 `ENUM` 类型,使用业务代码来声明枚举类型,步骤如下:
1.`pkg/consts/[table].go` 文件中,定义枚举字段的 Go 类型。例如:
```go
// swagger:enum UserStatus
// ENUM(pending_verify, verified, banned, )
type UserStatus string
```
2. 执行 `atomctl gen enum`,生成 `pkg/consts/[table].gen.go`
### 其它支持的数据类型
`database/.transform.yaml``field_type` 支持将表字段映射为 `go.ipao.vip/gen/types` 提供的 PostgreSQL 扩展类型(在 `.transform.yaml``imports` 中引入 `go.ipao.vip/gen` 后,通常可直接使用 `types.*`)。
常用类型清单(对应 `gen/types/`
- `types.JSON``json/jsonb`(建议列类型用 `jsonb`
- `types.JSONMap``json/jsonb``map[string]any` 形态
- `types.JSONType[T]` / `types.JSONSlice[T]`:强类型 JSON读写用不提供 JSON 路径查询能力)
- `types.Array[T]`PostgreSQL 数组(如 `text[]/int[]` 等)
- `types.UUID` / `types.BinUUID``uuid``BinUUID` 主要用于二进制存储场景)
- `types.Date` / `types.Time``date` / `time`
- `types.Money``money`
- `types.URL`URL通常落库为 `text/varchar`,由类型负责解析/序列化)
- `types.XML``xml`
- `types.HexBytes``bytea`hex 表示)
- `types.BitString``bit/varbit`
- 网络类型:`types.Inet``inet`)、`types.CIDR``cidr`)、`types.MACAddr``macaddr`
- 范围类型:
- `types.Int4Range``int4range`
- `types.Int8Range``int8range`
- `types.NumRange``numrange`
- `types.TsRange``tsrange`
- `types.TstzRange``tstzrange`
- `types.DateRange``daterange`
- 几何类型:`types.Point` / `types.Polygon` / `types.Box` / `types.Circle` / `types.Path`
- 全文检索:`types.TSQuery` / `types.TSVector`
- 可空类型:`types.Null[T]` 以及别名 `types.NullString/NullInt64/...`(需要字段允许 NULL
示例(`database/.transform.yaml`
```yaml
imports:
- go.ipao.vip/gen
- quyun/v2/pkg/consts
field_type:
users:
roles: types.Array[consts.Role]
meta: types.JSON
home_ip: types.Inet
profile: types.JSONType[Profile]
tenants:
uuid: types.UUID
```
### 关联关系字段说明(对齐 GORM
支持在 `database/.transform.yaml` 中为模型定义关联关系字段,生成对应的 GORM 关系标签。
示例:
```yaml
field_relate:
students:
Class:
# belong_to, has_one, has_many, many_to_many
relation: belongs_to
table: classes
references: id # 关联表的主键/被引用键(通常是 id
foreign_key: class_id # 当前表上的外键列(如 students.class_id
json: class
Teachers:
# belong_to, has_one, has_many, many_to_many
relation: many_to_many
table: teachers
pivot: class_teacher
foreign_key: class_id # 当前表students用于关联的键转为结构体字段名 ClassID
join_foreign_key: class_id # 中间表中指向当前表的列class_teacher.class_id
references: id # 关联表teachers被引用的列转为结构体字段名 ID
join_references: teacher_id # 中间表中指向关联表的列class_teacher.teacher_id
json: teachers
teachers:
Classes:
relation: many_to_many
table: classes
pivot: class_teacher
classes:
Teachers:
relation: many_to_many
table: teachers
pivot: class_teacher
```
关联关系配置项如下:
- relation
- 取值:`belongs_to``has_one``has_many``many_to_many`
- 对应 GORM 的四种关系Belongs To、Has One、Has Many、Many2Many。
- table
- 关联的目标表名(即另一侧模型对应的表)。
- pivot仅 many_to_many
- 多对多中间表名称,对应 GORM 标签 `many2many:<pivot>`
- foreign_key按关系含义不同
- 对应 GORM 标签 `foreignKey:<Field>`
- belongs_to当前表上的外键列例如 `students.class_id`),会映射为当前模型上的字段(如 `ClassID`)。
- has_one / has_many外键在对端表上例如 `credit_cards.user_id`)。配置时仍在当前表的配置块里填“外键列名”,生成时会正确落到 GORM 标签中。
- references按关系含义不同
- 对应 GORM 标签 `references:<Field>`
- belongs_to对端表被引用的列一般是 `id`),映射为对端模型字段名(如 `ID`)。
- has_one / has_many被对端外键引用的当前模型列一般是当前模型的 `ID` 字段)。
- join_foreign_key仅 many_to_many
- 对应 GORM 标签 `joinForeignKey:<Field>`,指中间表里“指向当前模型”的列(如 `class_teacher.class_id`)。
- join_references仅 many_to_many
- 对应 GORM 标签 `joinReferences:<Field>`,指中间表里“指向关联模型”的列(如 `class_teacher.teacher_id`)。
说明:生成器会结合数据库的 NamingStrategy 将列名(如 `class_id``teacher_id`)转换为结构体字段名(如 `ClassID``TeacherID`),并据此写入正确的 GORM 标签。
### 与 GORM 标签的对应关系
- belongs_to 示例students → classes
- YAML
- `foreign_key: class_id`
- `references: id`
- 生成的模型字段(示意):
- `Class Class gorm:"foreignKey:ClassID;references:ID"`
- has_many 示例users → credit_cards
- YAML`users` 下配置 `CreditCards` 关系):
- `relation: has_many`
- `table: credit_cards`
- `foreign_key: user_id` (对端表上的外键列)
- `references: id` (当前模型被引用的列)
- 生成的模型字段(示意):
- `CreditCards []CreditCard gorm:"foreignKey:UserID;references:ID"`
- many_to_many 示例students ⇄ teachers经由 class_teacher
- YAML`students` 下配置 `Teachers` 关系):
- `relation: many_to_many`
- `table: teachers`
- `pivot: class_teacher`
- `foreign_key: class_id`
- `join_foreign_key: class_id`
- `references: id`
- `join_references: teacher_id`
- 生成的模型字段(示意):
- `Teachers []Teacher gorm:"many2many:class_teacher;foreignKey:ClassID;references:ID;joinForeignKey:ClassID;joinReferences:TeacherID"`
提示GORM 在 many2many 下允许省略部分键,生成器也支持“只给必要字段”。若不确定,建议显式全部写出,避免命名不一致导致推断失败。
## model 功能扩展
模型生成完后可以创建 `database/models/[table].go` 文件添加自定义方法、GORM Hook 来扩展 model 的使用。例如:
```go
func (m *User) ComparePassword(ctx context.Context, password string) bool {
err := bcrypt.CompareHashAndPassword([]byte(m.Password), []byte(password))
return err == nil
}
```
## 生成模型的使用
模型通常在 service 中使用service 定义于 `app/services`, 通常用于对一类功能进行封装,方便 controller 层调用,不需要与表进行一一对应。 示例:
```go
package services
import "context"
// @provider
type test struct{
app *app.Config
}
func (t *test) Test(ctx context.Context) (string, error) {
return "Test", nil
}
```
struct 中可以定义一个多上需要注入的 provider 对象,示例中的 app 会自动注入对应的 app.Config 实例。
service 文件创建完成后需要运行 `atomctl gen service``atomctl gen provider` 完成依赖对象注入。
service 调用 model 示例:
```go
func (t *test) FindByID(ctx context.Context, userID int64) (*models.User, error) {
tbl, query := models.UserQuery.QueryContext(ctx)
model, err := query.Preload(tbl.OwnedTenant, tbl.Tenants).Where(tbl.ID.Eq(userID)).First()
if err != nil {
return nil, errors.Wrapf(err, "FindByID failed, %d", userID)
}
return model, nil
}
···
```

207
backend/llm.txt Normal file
View File

@@ -0,0 +1,207 @@
# 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 NOT manually edit generated files:
- `backend/app/http/**/routes.gen.go`
- `backend/app/http/**/provider.gen.go`
- `backend/docs/docs.go`
- DO keep Swagger annotations consistent with actual Fiber route paths (including `:param`).
---
## 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/*`
- 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: `backend/test/[module]/[controller].http` (extend it for new endpoints)
### 1.7 Testing
- Prefer existing test style under `backend/tests/e2e`.
- Run: `make test`
---
## 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`).
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
```
- 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`
- 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)