Skip to content

项目规范 ​

本文件是所有代码规范的唯一来源。

AI 生成代码前必须读取此文件。其他文档提供详细说明,但规则以本文件为准。


核心原则:三层架构 ​

所有业务代码必须遵循 Handler → Biz → Store 三层架构,严禁跨层调用。

┌─────────────────────────────────────────┐
│         Handler Layer                   │  HTTP/WebSocket/gRPC 处理层
│  - 参数验证、请求响应转换、错误处理      │
└──────────────┬──────────────────────────┘
               │ Depends on
               ▼
┌─────────────────────────────────────────┐
│          Business Layer (Biz)           │  业务逻辑层
│  - 业务规则、流程编排、事务控制          │
└──────────────┬──────────────────────────┘
               │ Depends on
               ▼
┌─────────────────────────────────────────┐
│          Store Layer                    │  数据访问层
│  - 数据库操作、缓存操作                  │
└─────────────────────────────────────────┘

每层职责:

层级职责禁止
Handler参数绑定、调用 Biz、返回响应包含业务逻辑
Biz业务规则、流程编排、事务控制直接操作数据库
Store数据库 CRUD、缓存操作包含业务逻辑

目录 ​

  1. 文件规范
  2. 命名规范
  3. 代码组织
  4. 错误处理
  5. 日志规范
  6. 测试规范
  7. 数据库迁移与初始化
  8. 生成代码检查清单
  9. 构建规范
  10. API 规范

1. 文件规范 ​

1.1 ABOUTME 注释(必须) ​

每个 .go 文件必须以 2 行 ABOUTME 注释开头。

go
// ❌ 禁止:没有 ABOUTME 注释
package user

// ❌ 禁止:只有 1 行
// ABOUTME: User business logic
package user

// ✅ 必须:2 行 ABOUTME 注释
// ABOUTME: User business logic implementation.
// ABOUTME: Handles user registration, login, and profile management.
package user

1.2 目录结构 ​

internal/<server>/
├── app.go                  # 应用初始化
├── run.go                  # 服务启动逻辑
├── biz/                    # 业务逻辑层
│   ├── biz.go              # IBiz 接口定义
│   ├── auth/               # 认证业务
│   └── user/               # 用户业务
├── handler/                # Handler 层(支持多协议)
│   ├── http/               # HTTP Handler
│   │   ├── auth/
│   │   └── user/
│   ├── ws/                 # WebSocket Handler
│   │   └── auth.go
│   └── grpc/               # gRPC Handler
│       └── auth.go
└── router/                 # 路由定义

internal/pkg/
├── store/                  # 数据访问层(平铺,避免循环引用)
│   ├── store.go            # IStore 接口
│   ├── user.go             # 用户 Store
│   └── sys_config.go       # 系统配置 Store
├── model/                  # 数据模型
├── errno/                  # 错误码定义
└── auth/                   # 认证授权

2. 命名规范 ​

2.1 包名 ​

  • 小写、简短、有意义
  • 单数形式
  • 不使用下划线或驼峰
go
// ✅ 正确
package user
package auth

// ❌ 错误
package users          // 应该用单数
package userService    // 不使用驼峰

2.2 文件名 ​

  • 蛇形命名(snake_case)
user_handler.go
sys_config.go
auth_middleware.go

2.3 表名 ​

  • 统一使用单数形式
  • 使用模块前缀,避免命名冲突
  • 蛇形命名(snake_case)
模块前缀示例
系统sys_sys_config, sys_menu
通知ntf_ntf_message, ntf_announcement
用户user_ 或无前缀user, user_address
sql
-- ✅ 正确
CREATE TABLE ntf_message (...);
CREATE TABLE sys_config (...);

-- ❌ 错误
CREATE TABLE notifications (...);  -- 应该用单数 + 前缀
CREATE TABLE message (...);        -- 缺少模块前缀

2.4 API JSON 字段命名 ​

  • 必须使用驼峰命名(camelCase)
  • 禁止使用蛇形命名(snake_case)
go
// ✅ 正确:驼峰命名
type UserInfo struct {
    UserID      string    `json:"userId"`
    Username    string    `json:"username"`
    Nickname    string    `json:"nickname"`
    CountryCode string    `json:"countryCode"`
    CreatedAt   time.Time `json:"createdAt"`
    UpdatedAt   time.Time `json:"updatedAt"`
}

// ❌ 错误:蛇形命名
type ChatRequest struct {
    SessionID string `json:"session_id"`  // 应该是 sessionId
    MaxTokens int    `json:"max_tokens"`  // 应该是 maxTokens
    RoleID    string `json:"role_id"`     // 应该是 roleId
}

例外: OpenAI 兼容接口保持原有 snake_case 命名以符合标准

go
// ✅ OpenAI 兼容接口可以使用 snake_case
type OpenAIRequest struct {
    Model       string        `json:"model"`
    Messages    []ChatMessage `json:"messages"`
    MaxTokens   int           `json:"max_tokens"`   // OpenAI 标准
    Temperature float64       `json:"temperature"`  // OpenAI 标准
}

2.5 接口名 ​

  • I 前缀(Interface)
  • 大驼峰命名
go
type IStore interface {}
type IBiz interface {}

2.6 Store 命名规范 ​

元素规范示例
文件名<prefix>_<model>.gouser.go, sys_config.go
Store 接口<Model>StoreUserStore, SysConfigStore
实现结构体<model>Store (小写)userStore, sysConfigStore
扩展接口<Model>ExpansionUserExpansion
创建函数New<Model>Store()NewUserStore()

3. 代码组织 ​

3.1 HTTP Handler 层模板 ​

每个 HTTP Handler 方法必须编写 Swagger 注释,用于生成 API 文档:

go
// ABOUTME: HTTP handlers for user management.
// ABOUTME: Provides CRUD endpoints for user resources.
package user

type UserHandler struct {
    biz biz.IBiz
}

func New(biz biz.IBiz) *UserHandler {
    return &UserHandler{biz: biz}
}

// Create
// @Summary    Create user
// @Security   Bearer
// @Tags       User
// @Accept     application/json
// @Produce    json
// @Param      request  body      v1.CreateUserRequest  true  "Param"
// @Success    200      {object}  v1.UserInfo
// @Failure    400      {object}  core.ErrResponse
// @Failure    500      {object}  core.ErrResponse
// @Router     /v1/users [POST].
func (h *UserHandler) Create(c *gin.Context) {
    // 1. 参数验证
    var req v1.CreateUserRequest
    if err := c.ShouldBindJSON(&req); err != nil {
        core.Response(c, nil, errno.ErrInvalidArgument.WithMessage(err.Error()))
        return
    }

    // 2. 调用业务层(直接传 c,不用 c.Request.Context())
    user, err := h.biz.Users().Create(c, &req)

    // 3. 返回响应
    core.Response(c, user, err)
}

Swagger 注释说明:

注解说明示例
@Summary接口简要描述Create user
@Security认证方式(需要登录则加)Bearer
@Tags接口分组User, Auth
@Accept请求格式application/json
@Produce响应格式application/json
@Param参数定义request body v1.CreateUserRequest true "Param"
@Success成功响应200 {object} v1.UserInfo
@Failure错误响应400 {object} core.ErrResponse
@Router路由路径和方法/v1/users [POST]

注意:@Router 注解末尾需要加 . 以符合 golint 规范。

3.2 WebSocket Handler 层模板 ​

WebSocket 使用 JSON-RPC 2.0 协议,返回 *jsonrpc.Response。

go
// ABOUTME: WebSocket auth method handlers.
// ABOUTME: Provides login and user-info endpoints for WS clients.
package ws

import (
    "github.com/bingo-project/websocket"
    "github.com/bingo-project/websocket/jsonrpc"

    "<module>/internal/<server>/biz"
    "<module>/internal/pkg/errno"
    v1 "<module>/pkg/api/<server>/v1"
)

type Handler struct {
    b biz.IBiz
}

func NewHandler(ds store.IStore) *Handler {
    return &Handler{b: biz.NewBiz(ds)}
}

func (h *Handler) Login(c *websocket.Context) *jsonrpc.Response {
    // 1. 参数绑定和验证
    var req v1.LoginRequest
    if err := c.BindValidate(&req); err != nil {
        return c.Error(errno.ErrInvalidArgument.WithMessage("%s", err.Error()))
    }

    // 2. 调用业务层
    resp, err := h.b.Auth().Login(c, &req)
    if err != nil {
        return c.Error(err)
    }

    // 3. 返回 JSON-RPC 响应
    return c.JSON(resp)
}

3.3 gRPC Handler 层模板 ​

gRPC 使用 Protobuf,需嵌入 Unimplemented*Server。

go
// ABOUTME: gRPC auth method handlers.
// ABOUTME: Provides login and user-info endpoints for gRPC clients.
package grpc

import (
    "context"

    "<module>/internal/<server>/biz"
    "<module>/internal/pkg/errno"
    apiv1 "<module>/pkg/api/<server>/v1"
    v1 "<module>/pkg/proto/<server>/v1/pb"
)

type Handler struct {
    b biz.IBiz
    v1.UnimplementedApiServerServer  // 必须嵌入
}

func NewHandler(ds store.IStore) *Handler {
    return &Handler{b: biz.NewBiz(ds)}
}

func (h *Handler) Login(ctx context.Context, req *v1.LoginRequest) (*v1.LoginReply, error) {
    // 1. 参数转换和验证
    loginReq := &apiv1.LoginRequest{
        Account:  req.Account,
        Password: req.Password,
    }
    if err := validate.Struct(loginReq); err != nil {
        return nil, errno.ErrInvalidArgument
    }

    // 2. 调用业务层
    resp, err := h.b.Auth().Login(ctx, loginReq)
    if err != nil {
        return nil, err
    }

    // 3. 返回 Protobuf 响应
    return &v1.LoginReply{
        AccessToken: resp.AccessToken,
        TokenType:   "Bearer",
        ExpiresIn:   resp.ExpiresAt.Unix(),
    }, nil
}

3.4 Handler 协议对比 ​

特性HTTPWebSocketgRPC
Context*gin.Context*websocket.Contextcontext.Context
返回值void*jsonrpc.Response(*Reply, error)
参数绑定c.ShouldBindJSON()c.BindValidate()直接使用 Protobuf
成功响应core.Response(c, data, nil)return c.JSON(data)return &Reply{}, nil
错误响应core.Response(c, nil, err)return c.Error(err)return nil, err
协议格式RESTful JSONJSON-RPC 2.0Protobuf

3.5 Context 传递规范 ​

HTTP Handler 调用 Biz 层时,直接传 c,不需要 c.Request.Context():

go
// ✅ 正确:直接传 c
uid := contextx.UserID(c)
user, err := h.biz.Users().Create(c, &req)

// ❌ 错误:不必要的 c.Request.Context()
uid := contextx.UserID(c.Request.Context())
user, err := h.biz.Users().Create(c.Request.Context(), &req)

*gin.Context 实现了 context.Context 接口,直接传递即可。

3.6 Biz 层模板 ​

go
// ABOUTME: User business logic implementation.
// ABOUTME: Handles user creation, validation, and password encryption.
package user

type UserBiz interface {
    Create(ctx context.Context, req *v1.CreateUserRequest) (*model.User, error)
}

type userBiz struct {
    ds store.IStore
}

func New(ds store.IStore) UserBiz {
    return &userBiz{ds: ds}
}

func (b *userBiz) Create(ctx context.Context, req *v1.CreateUserRequest) (*model.User, error) {
    // 1. 业务规则验证
    if err := b.validateUser(req); err != nil {
        return nil, err
    }

    // 2. 业务逻辑处理
    user := &model.User{
        Username: req.Username,
        Password: auth.Encrypt(req.Password),
    }

    // 3. 数据持久化
    if err := b.ds.Users().Create(ctx, user); err != nil {
        return nil, err
    }

    return user, nil
}

3.7 Store 层模板 ​

go
// ABOUTME: User data access layer.
// ABOUTME: Provides CRUD operations for user records.
package store

type UserStore interface {
    Create(ctx context.Context, obj *model.UserM) error
    Get(ctx context.Context, opts *where.Options) (*model.UserM, error)
    // ... 其他 CRUD 方法

    UserExpansion  // 扩展接口
}

type UserExpansion interface {
    FindByEmail(ctx context.Context, email string) (*model.UserM, error)
}

type userStore struct {
    *genericstore.Store[model.UserM]
}

func NewUserStore(store *datastore) *userStore {
    return &userStore{
        Store: genericstore.NewStore[model.UserM](store, NewLogger()),
    }
}

func (s *userStore) FindByEmail(ctx context.Context, email string) (*model.UserM, error) {
    return s.Get(ctx, where.F("email", email))
}

3.8 响应数据结构规范 ​

3.8.1 数据拼装位置 ​

响应数据结构由 Biz 层组装,Handler 层直接返回 Biz 层的响应:

go
// ✅ 正确:Biz 层组装响应
func (b *userBiz) List(ctx context.Context, req *v1.ListUserRequest) (*v1.ListUserResponse, error) {
    users, total, err := b.ds.Users().List(ctx, opts)
    return &v1.ListUserResponse{
        Total: total,
        Data:  users,
    }, err
}

// Handler 层直接返回
func (h *UserHandler) List(c *gin.Context) {
    resp, err := h.biz.Users().List(c, &req)
    core.Response(c, resp, err)
}

// ❌ 错误:Handler 层组装数据结构
func (h *UserHandler) List(c *gin.Context) {
    users, total, err := h.biz.Users().List(c, &req)
    core.Response(c, &v1.ListUserResponse{
        Total: total,
        Data:  users,
    }, err)  // 禁止!拼装应在 Biz 层
}

3.8.2 分页列表查询 ​

分页列表查询必须返回包含 Total 和 Data 字段的结构体:

go
// ✅ 正确:Biz 层返回分页响应结构体
type ListUserResponse struct {
    Total int64      `json:"total"`
    Data  []UserInfo `json:"data"`
}

// @Success 200 {object} v1.ListUserResponse

3.8.3 非分页列表查询 ​

非分页列表查询直接返回切片,禁止嵌套 data 字段:

go
// ❌ 错误:非分页列表不应嵌套 data
type ListSessionsResponse struct {
    Data []SessionInfo `json:"data"`
}

// ✅ 正确:Biz 层直接返回切片
func (b *sessionBiz) List(ctx context.Context) ([]SessionInfo, error) {
    sessions, err := b.ds.Sessions().List(ctx)
    return sessions, err
}

// Handler 层直接返回
func (h *SessionHandler) List(c *gin.Context) {
    sessions, err := h.biz.Sessions().List(c)
    core.Response(c, sessions, err)
}

// ✅ Swagger 注解
// @Success 200 {object} []v1.SessionInfo

3.8.4 单对象查询 ​

单对象查询直接返回对象结构体:

go
// ✅ 正确:Biz 层直接返回对象
func (b *userBiz) Get(ctx context.Context, uid string) (*UserInfo, error) {
    user, err := b.ds.Users().Get(ctx, uid)
    return user, err
}

// ✅ Swagger 注解
// @Success 200 {object} v1.UserInfo

3.8.5 判断准则 ​

场景响应类型Biz 层返回类型Handler 调用
分页列表带分页信息的结构体*ListXxxResponsecore.Response(c, resp, err)
非分页列表直接返回切片[]XxxInfocore.Response(c, data, err)
单对象直接返回结构体*XxxInfocore.Response(c, data, err)
创建/更新返回创建后的对象*XxxInfocore.Response(c, data, err)
删除返回 nilerrorcore.Response(c, nil, err)

4. 错误处理 ​

4.1 统一错误码 ​

错误码定义在 internal/pkg/errno/:

go
var (
    ErrUserNotFound      = &errorsx.ErrorX{Code: http.StatusNotFound, Reason: "NotFound.UserNotFound", Message: "User was not found."}
    ErrUserAlreadyExist  = &errorsx.ErrorX{Code: http.StatusBadRequest, Reason: "InvalidArgument.UserAlreadyExist", Message: "User already exist."}
    ErrPasswordInvalid   = &errorsx.ErrorX{Code: http.StatusBadRequest, Reason: "InvalidArgument.PasswordInvalid", Message: "Password is incorrect."}
)

4.2 分层错误处理 ​

层级错误处理方式
Store 层返回原始 error(GORM/Redis 错误)
Biz 层必须转换为自定义错误码,用 WithMessage 附加上下文
Handler 层直接传递给 core.Response()

重要:Biz 层禁止直接返回 Store 层的错误,必须使用 errno 包装。

go
// Store 层 - 返回原始 error
func (s *userStore) Create(ctx context.Context, user *model.UserM) error {
    return s.db.Create(user).Error
}

// Biz 层 - 转换为自定义错误码
func (b *userBiz) Create(ctx context.Context, req *v1.CreateUserRequest) (*model.UserM, error) {
    // ✅ 正确:业务规则校验返回自定义错误码
    if exists, _ := b.ds.Users().FindByEmail(ctx, req.Email); exists != nil {
        return nil, errno.ErrUserAlreadyExist
    }

    user := &model.UserM{Email: req.Email}
    if err := b.ds.Users().Create(ctx, user); err != nil {
        // ✅ 正确:写操作用 ErrDBWrite,读操作用 ErrDBRead
        return nil, errno.ErrDBWrite.WithMessage("create user: %v", err)
    }

    return user, nil
}

// ❌ 错误:直接返回 Store 层的 error
if err := b.ds.Users().Create(ctx, user); err != nil {
    return nil, err  // 禁止!必须用 errno 包装
}

// ❌ 错误:用 fmt.Errorf 包装(丢失类型信息)
return nil, fmt.Errorf("failed to create user: %w", err)

// ❌ 错误:直接返回字符串错误
return nil, errors.New("用户不存在")

常用错误码:

场景错误码
数据库读操作失败errno.ErrDBRead
数据库写操作失败errno.ErrDBWrite
资源不存在errno.ErrNotFound
权限不足errno.ErrPermissionDenied
操作失败(如队列入队)errno.ErrOperationFailed

4.3 Handler 层统一响应 ​

go
// ✅ 必须使用 core.Response
core.Response(c, data, err)

// ❌ 禁止直接返回 JSON
c.JSON(200, gin.H{"data": data})

4.4 使用常量代替字面量 ​

状态值、类型值等必须定义为常量,禁止在代码中直接使用字符串字面量。

常量定义位置:

常量类型定义位置示例
Model 状态/类型internal/pkg/model/model.AdminStatus, model.GoogleStatus
错误码internal/pkg/errno/errno.ErrNotFound
已知值(如角色名)internal/pkg/known/known.UserRoot
业务层专用常量对应 biz 包TOTPIssuer
go
// ✅ 正确:在 model 包定义类型和常量
// internal/pkg/model/admin.go
type GoogleStatus string

const (
    GoogleStatusUnbind   GoogleStatus = "unbind"
    GoogleStatusDisabled GoogleStatus = "disabled"
    GoogleStatusEnabled  GoogleStatus = "enabled"
)

// ✅ 正确:在 biz 层使用 model 常量
if user.GoogleStatus == string(model.GoogleStatusEnabled) {
    // ...
}

// ❌ 错误:直接使用字符串字面量
if user.GoogleStatus == "enabled" {
    // ...
}

// ❌ 错误:在 biz 层重复定义常量
const GoogleStatusEnabled = "enabled"  // 应该使用 model.GoogleStatusEnabled

5. 日志规范 ​

5.1 使用结构化日志 ​

go
import "github.com/bingo-project/bingo/internal/pkg/log"

// ✅ 结构化日志
log.C(ctx).Infow("user created", "username", username, "user_id", userID)

// ✅ 错误日志(带上下文)
log.C(ctx).Errorw("failed to create user", "err", err, "username", username)

// ❌ 不推荐:非结构化日志
log.C(ctx).Info("user created: " + username)

5.2 日志级别 ​

  • Debug: 调试信息
  • Info: 重要业务流程
  • Warn: 警告信息,不影响主流程
  • Error: 错误信息,需要关注

6. 测试规范 ​

6.1 分层测试策略 ​

层级测试方式Mock 什么
Store 层SQLite 内存数据库不 mock
Biz 层Mock StoreMock store.IStore
Handler 层Mock BizMock biz.IBiz

6.2 测试文件命名 ​

user.go       -> user_test.go
article.go    -> article_test.go

6.3 测试用例模板 ​

go
func TestUserBiz_Create(t *testing.T) {
    t.Run("success", func(t *testing.T) {
        // 1. 准备测试数据
        store := mockstore.NewStore()
        biz := user.New(store)
        req := &CreateUserRequest{Username: "test"}

        // 2. 执行测试
        user, err := biz.Create(context.Background(), req)

        // 3. 断言
        require.NoError(t, err)
        assert.Equal(t, "test", user.Username)
    })

    t.Run("user_already_exists", func(t *testing.T) {
        // ... 测试异常路径
    })
}

6.4 Mock 代码组织 ​

internal/pkg/testing/mock/
├── store/           # Store 层 mock
│   └── store.go
└── biz/             # Biz 层 mock
    └── biz.go

7. 数据库迁移与初始化 ​

7.1 目录结构 ​

internal/pkg/database/
├── migration/          # 数据库迁移文件
└── seeder/             # 数据初始化文件

7.2 Migration(数据库迁移) ​

新增或变更表结构时,必须编写 migration 文件。

注意:bingo 是全局安装的 CLI 工具,不是项目内的命令。

bash
bingo migrate up        # 执行迁移
bingo migrate rollback  # 回滚上一次迁移
bingo migrate reset     # 重置所有迁移(⚠️ 仅开发环境)

7.2.1 数据库字段规范 ​

基本原则:

  1. 仅允许必要的字段为 NULL

    • 可选字段(如 description, icon)可为 NULL
    • 有明确业务含义的字段应设置 NOT NULL
  2. 使用默认值代替 NULL

    • 数值类型:使用 0 作为默认值(default:0)
    • 字符串类型:使用空字符串 '' 作为默认值(default:'')
    • 布尔类型:使用 false 作为默认值
    • 时间戳:使用 CURRENT_TIMESTAMP 作为默认值
  3. 有默认值的字段必须 NOT NULL

    • 如果字段有 default 约束,必须同时声明 not null
    • 这确保默认值始终生效,避免 NULL 值

示例:

go
// ✅ 正确:可选字段允许 NULL
Description string `gorm:"type:varchar(255)"`                          // 可选描述
Icon        string `gorm:"type:varchar(255)"`                          // 可选图标

// ✅ 正确:有默认值的字段必须 NOT NULL
Status      string `gorm:"type:varchar(16);not null;default:'active'"`
Sort        int    `gorm:"type:int;not null;default:0"`
Temperature float64 `gorm:"type:decimal(3,2);not null;default:0.70"`

// ✅ 正确:必填字段 NOT NULL
Name        string `gorm:"type:varchar(64);not null"`
Email       string `gorm:"type:varchar(128);not null"`

// ❌ 错误:有默认值但允许 NULL
Temperature float64 `gorm:"type:decimal(3,2);default:0.70"`  // 缺少 not null
Sort        int    `gorm:"type:int;default:0"`               // 缺少 not null

// ❌ 错误:能用默认值的不要用 NULL
Count       int    `gorm:"type:int"`                          // 应该 default:0
Enabled     bool   `gorm:"type:bool"`                         // 应该 default:false

外键字段:

go
// ✅ 正确:可选关联允许 NULL
RoleID      string `gorm:"type:varchar(64);index:idx_role_id"`
ParentID    *uint  `gorm:"index:idx_parent_id"`  // 指针类型表示可空

// ✅ 正确:必填关联 NOT NULL
UserID      string `gorm:"type:varchar(64);not null;index:idx_user_id"`

时间字段:

go
// ✅ 正确:创建和更新时间必须有默认值
CreatedAt time.Time `gorm:"type:DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3)"`
UpdatedAt time.Time `gorm:"type:DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3) ON UPDATE CURRENT_TIMESTAMP(3)"`

// ✅ 正确:可选时间允许 NULL(使用指针)
DeletedAt *time.Time `gorm:"type:DATETIME(3)"`
LastLogin *time.Time `gorm:"type:DATETIME(3)"`

状态枚举字段:

所有表示状态的字段(如 status, state)必须使用自定义类型,而非普通字符串。

go
// ✅ 正确:定义状态类型
type AdminStatus string

const (
	AdminStatusEnabled  AdminStatus = "enabled"
	AdminStatusDisabled AdminStatus = "disabled"
)

// ✅ 正确:在模型中使用状态类型
type AdminM struct {
	Status AdminStatus `gorm:"type:varchar(20);not null;default:'enabled';comment:状态:enabled正常,disabled禁用"`
}

// ❌ 错误:使用普通字符串
type AdminM struct {
	Status string `gorm:"type:varchar(20);not null;default:'enabled'"`
}

命名规范:

  • 状态类型:<Entity>Status(如 AdminStatus, AiModelStatus)
  • 状态常量:<Entity>Status<Value>(如 AdminStatusEnabled, AiModelStatusActive)
  • 常见值:active/inactive, enabled/disabled, pending/approved/rejected

7.3 Seeder(数据初始化) ​

bash
bingo db seed                       # 执行所有 seeder
bingo db seed --seeder=UserSeeder   # 执行指定 seeder

7.4 开发阶段 Migration 管理 ​

功能分支合并前,可将多次表变更合并为单个 migration:

  1. 新增 alter table migration 时,同步更新原 create table migration
  2. 执行 bingo migrate reset && bingo migrate up && bingo db seed 验证
  3. 删除临时的 alter table migration

7.5 测试数据 Seeder ​

维护 UserSeeder 等测试数据 seeder,便于 reset 后快速恢复开发环境。


8. 生成代码检查清单 ​

生成任何代码前,必须逐条确认:

架构规范 ​

  • [ ] 遵循三层架构(Handler → Biz → Store)
  • [ ] 无跨层调用(Handler 不直接调用 Store)
  • [ ] 业务逻辑在 Biz 层,不在 Handler 或 Store 层

文件规范 ​

  • [ ] 文件头有 2 行 ABOUTME 注释
  • [ ] 文件放在正确的目录
  • [ ] 命名符合规范

错误处理 ​

  • [ ] 使用定义的错误码(errno.ErrXxx)
  • [ ] Handler 层使用 core.Response() 返回
  • [ ] 错误有适当的上下文信息

日志规范 ​

  • [ ] 使用结构化日志 log.C(ctx).Infow()
  • [ ] 关键业务操作有日志记录
  • [ ] 不记录敏感信息(密码等)

测试规范 ​

  • [ ] 每层只测自己的职责
  • [ ] Store 层使用 SQLite 测试
  • [ ] Biz/Handler 层使用 Mock
  • [ ] 测试覆盖正常和异常路径

数据库变更 ​

  • [ ] 表结构变更有对应的 migration 文件
  • [ ] migration 支持 rollback
  • [ ] 必要的初始化数据有 seeder

9. 构建规范 ​

9.1 代码修改后重新构建 ​

代码修改后需要重新构建,使用 make build,不要用 go build ./...:

bash
# ✅ 正确
make build

# ❌ 错误
go build ./...

9.2 修改 API 参数定义 ​

如果修改了请求/响应结构体(pkg/api/ 下的定义),必须先执行 make swag 再构建:

bash
# 修改了 v1.CreateUserRequest 等结构体后
make swag   # 先更新 Swagger 文档
make build  # 再构建

9.3 提交前检查 ​

commit 前必须执行 make lint,确保代码符合规范:

bash
make lint   # 代码检查(会自动格式化代码)
git add .   # 必须将 lint 格式化的内容也加入提交
git commit -m "feat: add feature"

⚠️ 重要:make lint 会自动格式化代码(如调整 import 顺序、修复格式问题)。格式化后的改动必须一并提交,否则下次 lint 会产生相同的格式化 diff。


附录:常用命令 ​

bash
make build                           # 编译所有服务
make build BINS="svc1 svc2"          # 编译指定服务(可多个)
make test                            # 测试
make lint                            # 代码检查(commit 前必须执行)
make swag                            # 生成 Swagger 文档(仅 HTTP 服务)

附录:Import 路径 ​

go
// Gin 框架
import "github.com/gin-gonic/gin"

// 业务层
import "<module>/internal/<server>/biz"

// 数据层
import "<module>/internal/pkg/store"

// 错误码
import "<module>/internal/pkg/errno"

// 日志
import "<module>/internal/pkg/log"
log.C(ctx).Infow("message", "key", value)

// 统一响应
import "<module>/internal/pkg/core"
core.Response(c, data, err)

10. API 规范 ​

10.1 JSON 字段命名 ​

基本原则:

  • 必须使用驼峰命名(camelCase)
  • 禁止使用蛇形命名(snake_case)
  • 唯一例外: OpenAI 兼容接口可使用 snake_case 以符合标准
go
// ✅ 正确:驼峰命名
type UserInfo struct {
    UserID      string    `json:"userId"`
    Username    string    `json:"username"`
    Nickname    string    `json:"nickname"`
    CountryCode string    `json:"countryCode"`
    CreatedAt   time.Time `json:"createdAt"`
    UpdatedAt   time.Time `json:"updatedAt"`
}

// ✅ 正确:OpenAI 兼容接口
type OpenAIRequest struct {
    Model       string        `json:"model"`
    Messages    []ChatMessage `json:"messages"`
    MaxTokens   int           `json:"max_tokens"`   // OpenAI 标准
    Temperature float64       `json:"temperature"`  // OpenAI 标准
}

// ❌ 错误:非 OpenAI 接口使用 snake_case
type ChatRequest struct {
    SessionID string `json:"session_id"`  // 应该是 sessionId
    MaxTokens int    `json:"max_tokens"`  // 应该是 maxTokens
    RoleID    string `json:"role_id"`     // 应该是 roleId
}

10.2 Request/Response 结构 ​

命名规范:

  • 请求结构: <Verb><Resource>Request
  • 响应结构: <Verb><Resource>Response 或 <Resource>Info
  • 列表响应: List<Resource>Response
go
// 请求
type CreateUserRequest struct {}
type UpdateUserRequest struct {}
type DeleteUserRequest struct {}

// 响应
type UserInfo struct {}
type ListUserResponse struct {
    Total int64       `json:"total"`
    Data  []UserInfo  `json:"data"`
}

字段顺序:

  1. 资源标识字段 (ID, UID 等)
  2. 核心业务字段
  3. 状态字段
  4. 时间字段
go
type UserInfo struct {
    UID          string    `json:"uid"`           // 1. 标识
    Username     string    `json:"username"`      // 2. 业务字段
    Nickname     string    `json:"nickname"`
    Status       int32     `json:"status"`        // 3. 状态
    CreatedAt    time.Time `json:"createdAt"`     // 4. 时间
    UpdatedAt    time.Time `json:"updatedAt"`
}

10.3 HTTP 状态码使用 ​

场景状态码说明
成功200统一使用 200,通过业务错误码区分具体状态
参数错误200返回 errno.ErrInvalidArgument
未授权200返回 errno.ErrUnauthorized
资源不存在200返回 errno.ErrNotFound
服务器错误200返回 errno.ErrInternal

原则: HTTP 层始终返回 200,业务错误通过 errno 和 core.Response 处理

<module> = go.mod 模块名,<server> = 服务名(如 apiserver)

Released under the Apache 2.0 License.