bindbox-game/docs/开发规范.md
邹方成 1ab39d2f5a
Some checks failed
Build docker and publish / linux (1.24.5) (push) Failing after 25s
refactor: 重构项目结构并重命名模块
feat(admin): 新增工会管理功能
feat(activity): 添加活动管理相关服务
feat(user): 实现用户道具卡和积分管理
feat(guild): 新增工会成员管理功能

fix: 修复数据库连接配置
fix: 修正jwtoken导入路径
fix: 解决端口冲突问题

style: 统一代码格式和注释风格
style: 更新项目常量命名

docs: 添加项目框架和开发规范文档
docs: 更新接口文档注释

chore: 移除无用代码和文件
chore: 更新Makefile和配置文件
chore: 清理日志文件

test: 添加道具卡测试脚本
2025-11-14 21:10:00 +08:00

22 KiB
Raw Blame History

Go API 开发规范

1. 概述

1.1 目的

本文档旨在为 bindbox-game 项目制定一套统一、清晰的 Go API 开发规范。通过标准化代码结构、命名约定、错误处理和文档注释,我们致力于提高代码的可读性、可维护性和团队协作效率,并从根本上避免之前开发中遇到的问题(如包导入不一致、特殊字符使用不当等)。

1.2 适用范围

本规范主要适用于项目 internal/api/ 目录下的所有 API 层的开发工作。所有新的 API 模块以及对现有模块的修改,都必须严格遵守本规范。

1.3 核心原则

  • 一致性:所有代码都应遵循相同的模式和风格。
  • 清晰性:代码应易于阅读和理解,避免复杂的或晦涩的实现。
  • 可维护性:代码结构应清晰,便于未来扩展和重构。
  • api/service代码结构应清晰api 和 service 应该保持一致的命名规范。必须: 一个 api 对应一个 service。一个函数功能对应一个 service 方法。单独一个文件
  • 接口文档注释:所有 API 端点都必须包含详细的 Swagger 注释,描述请求参数、响应体、可能的错误码等。
  • 测试功能:所有 API 端点都必须包含详细的测试用例,确保功能的正确性和稳定性。(cmd/testchain/ 中写)

2. 目录与文件结构

规范的目录结构是项目可维护性的基础。

  • 模块化目录:每一个独立的业务功能模块都应在 internal/api/ 下创建一个专属目录。

    internal/api/
    ├── admin/
    ├── activity/
    └── ... (其他模块)
    
  • Handler 结构体定义:在每个模块目录下,应有一个与模块同名的 go 文件(如 admin.go),用于定义该模块的 handler 结构体和 New 初始化函数Handler 不直接进行数据库读写)。

    // internal/api/admin/admin.go
    package admin
    
    type handler struct {
        logger  logger.CustomLogger
        writeDB *dao.Query   // 仅用于注入(避免在 Handler 直接使用)
        readDB  *dao.Query   // 仅用于注入(避免在 Handler 直接使用)
        svc     adminsvc.Service
    }
    
    func New(logger logger.CustomLogger, db mysql.Repo) *handler {
        return &handler{
            logger:  logger,
            writeDB: dao.Use(db.GetDbW()),
            readDB:  dao.Use(db.GetDbR()),
            svc:     adminsvc.New(logger, db),
        }
    }
    
  • 端点文件分离:每个 API 端点Endpoint的实现应放在一个独立的 go 文件中,文件名应清晰描述其功能,示例对齐当前 admin 模块:

    internal/api/admin/
    ├── admin.go           // Handler 定义
    ├── login.go           // 管理员登录
    ├── admin_create.go    // 新增客服
    ├── admin_modify.go    // 编辑客服(含路径参数)
    ├── admin_delete.go    // 删除客服(批量)
    └── admin_list.go      // 客服列表(分页查询)
    

3. 命名规范

统一的命名规范能极大提升代码的可读性。

  • Go 文件:使用小写蛇形命名法 (snake_case),如 admin_create.go
  • Handler 函数:使用大驼峰命名法 (PascalCase),且函数名应清晰表达其动作,如 CreateAdmin()
  • 请求/响应结构体:使用小驼峰命名法 (camelCase),并以 Request/Response 作为后缀,如 createAdminRequest, createAdminResponse

4. API Handler 规范

API Handler 是业务逻辑的入口,其规范性至关重要。

4.1 函数签名

所有 API Handler 函数都必须返回 core.HandlerFunc 类型。这是框架的核心设计,用于统一处理请求上下文。

// CreateAdmin 新增客服
func (h *handler) CreateAdmin() core.HandlerFunc {
    return func(ctx core.Context) {
        // ... 实现
    }
}

4.2 参数绑定与校验

  • 绑定JSONPOST/PUT 等带请求体的接口使用 ctx.ShouldBindJSON(req) 绑定到预定义的 Request 结构体。
  • 绑定QueryGET 查询接口使用 ctx.ShouldBindForm(req) 绑定到预定义的 Request 结构体(字段使用 form:"..." tag
  • 路径参数:通过 ctx.Param("id") 等读取路径参数并进行类型转换与校验。
  • 校验:绑定方法会自动执行 binding tag 定义的校验规则。如果校验失败,必须立即中断请求并返回错误。
req := new(createAdminRequest)
res := new(createAdminResponse)
if err := ctx.ShouldBindJSON(req); err != nil {
    ctx.AbortWithError(core.Error(
        http.StatusBadRequest,
        code.ParamBindError,
        validation.Error(err)),
    )
    return
}
// GET 查询绑定示例(与 admin_list 对齐)
req := new(listRequest)
res := new(listResponse)
if err := ctx.ShouldBindForm(req); err != nil {
    ctx.AbortWithError(core.Error(
        http.StatusBadRequest,
        code.ParamBindError,
        validation.Error(err)),
    )
    return
}
// 路径参数读取示例(与 admin_modify 对齐)
id, err := strconv.Atoi(ctx.Param("id"))
if err != nil {
    ctx.AbortWithError(core.Error(
        http.StatusBadRequest,
        code.ParamBindError,
        "未传递编号ID"),
    )
    return
}

4.3 响应处理

  • 成功响应:当业务逻辑成功执行后,必须使用 ctx.Payload(res) 方法来返回标准的成功响应。res 是预定义的 Response 结构体实例。
    res.Message = "操作成功"
    ctx.Payload(res)
    
  • 错误响应:见第 6 章“错误处理规范”。

5. 数据传输对象 (DTO) 规范

DTO (Data Transfer Object) 是 API 契约的直接体现。

  • 结构体定义:每个 API 都应定义清晰的 RequestResponse 结构体。
  • 字段注释:所有结构体字段都必须有清晰的中文注释,解释其含义和用途。
  • JSON Tag:所有字段都必须包含 json:"..." tag以确保与前端交互的 JSON 字段名一致。
  • Validation Tag:对于 Request 结构体中需要校验的字段,必须添加 binding:"..." tag 来定义校验规则(如 required)。
// internal/api/admin/admin_create.go

type createAdminRequest struct {
	UserName string `json:"username" binding:"required"` // 用户名
	NickName string `json:"nickname" binding:"required"` // 昵称
	Password string `json:"password" binding:"required"` // 密码
    Mobile   string `json:"mobile"`                      // 手机号 (非必填)
}

type createAdminResponse struct {
    Message string `json:"message"` // 提示信息
}
// internal/api/admin/admin_list.go
// GET 查询示例(字段使用 form 标签)
type listRequest struct {
    Username string `form:"username"`  // 用户名
    Nickname string `form:"nickname"`  // 昵称
    Page     int    `form:"page"`      // 当前页码,默认 1
    PageSize int    `form:"page_size"` // 每页返回的数据量,最多 100 条
}

type listData struct {
    ID        int32  `json:"id"`
    UserName  string `json:"username"`
    NickName  string `json:"nickname"`
    Mobile    string `json:"mobile"`
    Avatar    string `json:"avatar"`
    CreatedAt string `json:"created_at"`
    UpdatedAt string `json:"updated_at"`
}

type listResponse struct {
    Page     int        `json:"page"`
    PageSize int        `json:"page_size"`
    Total    int64      `json:"total"`
    List     []listData `json:"list"`
}

6. 错误处理规范

统一的错误处理是保证 API 健壮性和可预测性的关键。

  • 统一入口:所有在 Handler 中捕获的错误(参数校验失败、数据库操作失败、权限不足等)都必须通过 ctx.AbortWithError() 函数来处理。

  • core.Error 结构ctx.AbortWithError 接收一个 core.Error 对象,该对象封装了错误的三个核心要素:

    1. HTTP 状态码:如 http.StatusBadRequest
    2. 业务错误码:在 internal/code/ 中预定义的 code.Code,如 code.CreateAdminError
    3. 详细错误信息:一个描述错误具体原因的字符串。
  • 未找到记录:当数据库查询返回 gorm.ErrRecordNotFound 时,需要按具体业务错误码返回明确的提示信息(如“账号不存在”)。

  • 权限校验失败:当 ctx.SessionUserInfo().IsSuper != 1 时,禁止操作,返回对应业务错误码和提示“禁止操作”。

// 示例:用户已存在
if info != nil {
    ctx.AbortWithError(core.Error(
        http.StatusBadRequest,
        code.CreateAdminError,
        fmt.Sprintf("%s: %s", code.Text(code.CreateAdminError), "该账号已存在")),
    )
    return
}
// 示例:未找到记录(与 admin_modify/login 对齐)
if err == gorm.ErrRecordNotFound {
    ctx.AbortWithError(core.Error(
        http.StatusBadRequest,
        code.AdminLoginError,
        fmt.Sprintf("%s: %s", code.Text(code.AdminLoginError), "账号不存在,请联系管理员。")),
    )
    return
}
// 示例:权限校验失败(与 create/modify/delete/list 对齐)
if ctx.SessionUserInfo().IsSuper != 1 {
    ctx.AbortWithError(core.Error(
        http.StatusBadRequest,
        code.CreateAdminError,
        fmt.Sprintf("%s: %s", code.Text(code.CreateAdminError), "禁止操作")),
    )
    return
}

6.1 数据库上下文与读写分离Service 层)

  • 上下文传递:所有数据库操作必须调用 WithContext(ctx)Service 层接收 context.Context)。
  • 读写分离:读取使用 s.readDB,写入使用 s.writeDB
  • 分页查询会话:分页列表与计数需要分别创建独立会话,避免互相影响。
query := s.readDB.Admin.WithContext(ctx)
listQueryDB := query.Session(&gorm.Session{})
countQueryDB := query.Session(&gorm.Session{})

resultData, err := listQueryDB.Order(s.readDB.Admin.ID.Desc()).Limit(in.PageSize).Offset((in.Page-1)*in.PageSize).Find()
if err != nil { /* 错误处理 */ }

count, err := countQueryDB.Count()
if err != nil { /* 错误处理 */ }

7. 日志记录规范

虽然在 admin 模块的示例中未显式大量使用,但规范的日志记录对于问题排查至关重要。

  • 使用注入的 Logger:应通过 handler 结构体中注入的 h.logger 实例来记录日志。
  • 记录关键信息:在关键业务节点或错误发生时,记录必要的上下文信息,如请求参数、用户 ID 等。
// 推荐实践 (示例)
h.logger.Error("创建管理员失败", zap.Error(err), zap.String("username", req.UserName))

8. 注释与文档 (Swagger)

API 的可发现性和易用性依赖于完善的文档。

  • 强制 Swagger 注解:每个 API Handler 函数上方都必须添加完整的 Swagger 注解块。
  • 注解内容:必须包含以下核心注解:
    • @Summary:一句话功能简介。
    • @Description:更详细的功能描述。
    • @TagsAPI 分组标签,便于在 Swagger UI 中分类。
    • @Accept / @Produce:通常为 json
    • @Param:描述请求参数,包括参数位置(body)、类型、是否必需和说明。
    • @Success:描述成功响应的结构。
    • @Failure:描述可能发生的错误响应。
    • @RouterAPI 的路由路径和 HTTP 方法。
    • @Security:如果接口需要认证,需添加此注解。
// internal/api/admin/admin_create.go

// CreateAdmin 新增客服
// @Summary 新增客服
// @Description 新增客服
// @Tags 管理端.客服管理
// @Accept json
// @Produce json
// @Param RequestBody body createAdminRequest true "请求参数"
// @Success 200 {object} createAdminResponse
// @Failure 400 {object} code.Failure
// @Router /api/admin/create [post]
// @Security LoginVerifyToken
func (h *handler) CreateAdmin() core.HandlerFunc { /* ... */ }

// internal/api/admin/admin_list.goGET 查询示例)
// @Param username query string false "用户名"
// @Param nickname query string false "昵称"
// @Param page query int true "当前页码" default(1)
// @Param page_size query int true "每页返回的数据量,最多 100 条" default(20)
// @Router /api/admin/list [get]
// @Security LoginVerifyToken

// internal/api/admin/admin_modify.go路径参数示例
// @Param id path string true "编号ID"
// @Router /api/admin/{id} [put]
// @Security LoginVerifyToken

// internal/api/admin/login.go登录接口不加 Security
// @Router /api/admin/login [post]

9. 路由与中间件规范

路由是 API 对外的入口,必须与模块职责、认证策略保持一致,并在 internal/router/router.go 统一注册。

9.1 分组与路径前缀

  • 管理端:统一前缀 "/api/admin"
    • 非认证组:用于登录、系统状态等不需要鉴权的接口。
    • 认证组:使用鉴权中间件保护,适用于敏感操作(新增、编辑、删除、列表)。
  • 用户端APP:统一前缀 "/api/app",用于对外业务接口(如活动相关)。

9.2 鉴权中间件

  • 封装方式:通过 core.WrapAuthHandler(interceptor.AdminTokenAuthVerify) 将会话信息写入上下文,认证失败统一返回业务错误。
  • 接口签名internal/router/interceptor/interceptor.go:12-18 定义了鉴权接口: AdminTokenAuthVerify(ctx core.Context) (sessionUserInfo proposal.SessionUserInfo, err core.BusinessError)
  • 实现参考:管理端鉴权实现见 internal/router/interceptor/admin_auth.go:1-80

9.3 路由注册示例(与当前 admin 模块对齐)

// internal/router/router.go

// 管理端非认证接口路由组(登录)
adminNonAuthApiRouter := mux.Group("/api/admin")
{
    adminNonAuthApiRouter.GET("/license/status", func(ctx core.Context) { /* ... */ })
    adminNonAuthApiRouter.POST("/login", adminHandler.Login())
}

// 管理端认证接口路由组(需要 Authorization
adminAuthApiRouter := mux.Group("/api/admin", core.WrapAuthHandler(intc.AdminTokenAuthVerify))
{
    adminAuthApiRouter.POST("/create", adminHandler.CreateAdmin())
    adminAuthApiRouter.PUT("/:id", adminHandler.ModifyAdmin())
    adminAuthApiRouter.POST("/delete", adminHandler.DeleteAdmin())
    adminAuthApiRouter.GET("/list", adminHandler.PageList())
}

// APP 端接口路由组
appApiRouter := mux.Group("/api/app")
{
    appHandler := app.New(logger, db)
    appApiRouter.GET("/activities", appHandler.ListActivities())
    // ... 其他 APP 端接口
}

9.4 Swagger 安全定义与使用

  • 安全定义位置main.go:23-26 定义认证方案 LoginVerifyToken,从请求头读取 Authorization
  • 注解使用规范
    • 管理端认证组接口均需添加 @Security LoginVerifyToken
    • 登录接口不需要添加 @Security 注解。

9.5 路径命名与语义

  • 资源语义
    • POST /api/admin/create:创建资源(支持复杂校验与默认值设置)。
    • PUT /api/admin/:id:更新单个资源,路径参数 id 必须有效。
    • POST /api/admin/delete:批量删除,参数 ids 以逗号分隔。
    • GET /api/admin/list:分页检索,page/page_size 合理限制(最大 100
  • 用户端:保持 RESTful 风格,如 GET /api/app/activitiesGET /api/app/activities/:activity_id

9.6 指标与别名(可选)

  • 为避免路径参数导致指标维度爆炸,可在 Handler 前添加别名记录指标:
// 例如:为 /api/app/activities/:activity_id 设置指标记录别名
appApiRouter.GET("/activities/:activity_id",
    core.AliasForRecordMetrics("/api/app/activities/:activity_id"),
    appHandler.GetActivityDetail(),
)

10. 服务层分离规范

为避免 Handler 同时承担传输与业务职责,必须在 internal/service/{module} 引入服务层,统一承载业务规则、数据协作与事务处理。

10.1 目录与命名

  • 目录:internal/service/admininternal/service/activity 等按模块划分。
  • 包名与模块名一致Handler 导入时使用别名避免冲突(如 svc "bindbox-game/internal/service/admin")。

10.1.1 文件拆分(与 API 端点保持一致)

  • 每个端点在服务层对应一个实现文件,避免所有方法堆叠在单文件:
    internal/service/admin/
    ├── service.go   // 接口、构造
    ├── login.go     // 登录
    ├── create.go    // 新增客服
    ├── modify.go    // 编辑客服
    ├── delete.go    // 删除客服
    └── list.go      // 客服列表
    
    • 规则:每个端点的输入/输出类型在对应文件中定义(例如 LoginInput/LoginResultlogin.go)。

11. 流程规范Router → API → Service

11.1 职责边界

  • Router负责分组、认证拦截与端点注册不承载业务逻辑。
    • 管理端非认证路由:internal/router/router.go:44-51
    • 管理端认证路由(鉴权中间件):internal/router/router.go:53-60
    • 用户端 APP 路由:internal/router/router.go:62-72
  • APIHandler负责参数绑定、权限校验、错误码映射与响应输出不直接操作数据库。
    • 绑定参数并校验:如登录 internal/api/admin/login.go:35-45、列表 internal/api/admin/admin_list.go:57-64
    • 权限校验:如列表 internal/api/admin/admin_list.go:83-90、创建 internal/api/admin/admin_create.go:49-56
    • 错误码映射与响应:如登录 internal/api/admin/login.go:53-59,61-64
  • Service承载领域逻辑、数据访问与事务输入/输出类型在对应端点文件中定义。
    • 登录实现:internal/service/admin/login.go:16-52
    • 创建实现:internal/service/admin/admin_create.go:14-42
    • 编辑实现:internal/service/admin/admin_modify.go:13-50
    • 删除实现:internal/service/admin/admin_delete.go:9-15
    • 列表实现:internal/service/admin/admin_list.go:12-38

11.2 端到端流程(以管理端为例)

  • Router 接收请求并路由到对应分组:非认证或认证(携带 Authorization)。
  • 若为认证接口,鉴权中间件注入会话信息:internal/router/interceptor/admin_auth.go:1-80
  • Handler
    • ShouldBindJSONShouldBindForm 解析请求(含路径参数 ctx.Param)。
    • 权限检查(如 ctx.SessionUserInfo().IsSuper)。
    • 将 DTO 转换为 Service 输入,调用 svc.*(ctx.RequestContext(), input)
    • 根据 Service 返回的错误映射业务错误码,或输出成功响应 ctx.Payload(res)
  • Service
    • 使用 dao.Query.WithContext 与读写分离实现查询或写入。
    • 必要场景内使用事务保障原子性(参考 dao.gen.goQuery.Transaction)。

11.3 错误处理策略

  • Service 返回语义化错误信息(error);不直接决定 HTTP 或业务码。
  • Handler 统一映射到 core.Error(httpCode, code.* , message)AbortWithError
    • 登录错误映射:internal/api/admin/login.go:53-59
    • 创建错误映射:internal/api/admin/admin_create.go:66-71
    • 编辑错误映射:internal/api/admin/admin_modify.go:87-93
    • 删除错误映射:internal/api/admin/admin_delete.go:95-102
    • 列表错误映射:internal/api/admin/admin_list.go:98-105

11.4 数据绑定与类型转换

  • 登录:loginRequestLoginInput 映射:internal/api/admin/login.go:47-51
  • 创建:createAdminRequestCreateInput 映射:internal/api/admin/admin_create.go:58-65
  • 编辑:modifyAdminRequestModifyInput 映射:internal/api/admin/admin_modify.go:79-86
  • 列表:listRequest(form)ListInput 映射:internal/api/admin/admin_list.go:92-97
  • 删除:解析 ids 列表后直接传入 svc.Deleteinternal/api/admin/admin_delete.go:95

11.5 事务与读写分离

  • 读操作统一使用 readDB,写操作统一使用 writeDB
  • 所有 DAO 调用必须附带上下文:WithContext(ctx)
  • 多步骤写操作建议在 Service 层使用事务(Query.Transactioninternal/repository/mysql/dao/gen.go:211-213

11.6 鉴权与会话

  • 路由层通过 core.WrapAuthHandler(intc.AdminTokenAuthVerify) 进行会话注入:internal/router/router.go:53-60
  • 鉴权逻辑参考:internal/router/interceptor/admin_auth.go:1-80
  • Handler 使用 ctx.SessionUserInfo() 获取操作者信息(如 CreatedByUpdatedBy)。

11.7 命名与拆分

  • API 与 Service 均按端点拆分文件,命名对齐,便于定位与维护。
  • Service 的输入/输出类型在对应端点文件定义(如 login.go 定义 LoginInput/LoginResult)。

12. Repository 规范

12.1 层职责

  • 负责数据访问CRUD、分页、事务不包含业务规则与 HTTP 细节。
  • 仅依赖底层 DAO/Model向 Service 层提供稳定的数据操作能力。

12.2 目录与组件

  • 目录:internal/repository/mysql
    • dao/:由 gorm/gen 生成的查询对象与读写分离封装,参考:
      • 读写分离方法:internal/repository/mysql/dao/gen.go:135-143
      • 事务封装:internal/repository/mysql/dao/gen.go:211-213
      • 上下文绑定:internal/repository/mysql/dao/gen.go:188-209
    • model/:数据库对应的实体结构体(字段、标签)。

12.3 使用规范(在 Service 层)

  • 初始化查询对象:
    • 读库:readDB := dao.Use(db.GetDbR())
    • 写库:writeDB := dao.Use(db.GetDbW())
  • 所有操作必须绑定上下文:WithContext(ctx),示例(查询管理员):
info, err := s.readDB.Admin.WithContext(ctx).
    Where(s.readDB.Admin.Username.Eq(in.Username)).
    First()
  • 分页与计数使用独立会话,避免互相影响:
listQueryDB := query.Session(&gorm.Session{})
countQueryDB := query.Session(&gorm.Session{})
items, _ := listQueryDB.Order(...).Limit(...).Offset(...).Find()
total, _ := countQueryDB.Count()
  • 多步骤写操作建议在事务中完成:
_ = s.writeDB.Transaction(func(tx *dao.Query) error {
    // 在 tx 上执行写操作
    return nil
})

12.4 分层建议

  • 如需进一步解耦 DAO可在 internal/repository/{module} 定义模块级仓库接口(如 adminrepo),内部封装 DAO 细节Service 只依赖仓库接口,便于单元测试与替换数据源。
  • 目前项目直接在 Service 层使用 DAO符合轻量项目场景当跨表协作复杂时可引入仓库接口以降低耦合。