feat(admin): 新增工会管理功能 feat(activity): 添加活动管理相关服务 feat(user): 实现用户道具卡和积分管理 feat(guild): 新增工会成员管理功能 fix: 修复数据库连接配置 fix: 修正jwtoken导入路径 fix: 解决端口冲突问题 style: 统一代码格式和注释风格 style: 更新项目常量命名 docs: 添加项目框架和开发规范文档 docs: 更新接口文档注释 chore: 移除无用代码和文件 chore: 更新Makefile和配置文件 chore: 清理日志文件 test: 添加道具卡测试脚本
22 KiB
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 参数绑定与校验
- 绑定(JSON):POST/PUT 等带请求体的接口使用
ctx.ShouldBindJSON(req)绑定到预定义的Request结构体。 - 绑定(Query):GET 查询接口使用
ctx.ShouldBindForm(req)绑定到预定义的Request结构体(字段使用form:"..."tag)。 - 路径参数:通过
ctx.Param("id")等读取路径参数并进行类型转换与校验。 - 校验:绑定方法会自动执行
bindingtag 定义的校验规则。如果校验失败,必须立即中断请求并返回错误。
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 都应定义清晰的
Request和Response结构体。 - 字段注释:所有结构体字段都必须有清晰的中文注释,解释其含义和用途。
- 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对象,该对象封装了错误的三个核心要素:- HTTP 状态码:如
http.StatusBadRequest。 - 业务错误码:在
internal/code/中预定义的code.Code,如code.CreateAdminError。 - 详细错误信息:一个描述错误具体原因的字符串。
- HTTP 状态码:如
-
未找到记录:当数据库查询返回
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:更详细的功能描述。@Tags:API 分组标签,便于在 Swagger UI 中分类。@Accept/@Produce:通常为json。@Param:描述请求参数,包括参数位置(body)、类型、是否必需和说明。@Success:描述成功响应的结构。@Failure:描述可能发生的错误响应。@Router:API 的路由路径和 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.go(GET 查询示例)
// @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/activities、GET /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/admin、internal/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/LoginResult在login.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
- 管理端非认证路由:
- API(Handler):负责参数绑定、权限校验、错误码映射与响应输出;不直接操作数据库。
- 绑定参数并校验:如登录
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:
ShouldBindJSON或ShouldBindForm解析请求(含路径参数ctx.Param)。- 权限检查(如
ctx.SessionUserInfo().IsSuper)。 - 将 DTO 转换为 Service 输入,调用
svc.*(ctx.RequestContext(), input)。 - 根据 Service 返回的错误映射业务错误码,或输出成功响应
ctx.Payload(res)。
- Service:
- 使用
dao.Query.WithContext与读写分离实现查询或写入。 - 必要场景内使用事务保障原子性(参考
dao.gen.go的Query.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 数据绑定与类型转换
- 登录:
loginRequest→LoginInput映射:internal/api/admin/login.go:47-51 - 创建:
createAdminRequest→CreateInput映射:internal/api/admin/admin_create.go:58-65 - 编辑:
modifyAdminRequest→ModifyInput映射:internal/api/admin/admin_modify.go:79-86 - 列表:
listRequest(form)→ListInput映射:internal/api/admin/admin_list.go:92-97 - 删除:解析
ids列表后直接传入svc.Delete:internal/api/admin/admin_delete.go:95
11.5 事务与读写分离
- 读操作统一使用
readDB,写操作统一使用writeDB。 - 所有 DAO 调用必须附带上下文:
WithContext(ctx)。 - 多步骤写操作建议在 Service 层使用事务(
Query.Transaction):internal/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()获取操作者信息(如CreatedBy、UpdatedBy)。
11.7 命名与拆分
- API 与 Service 均按端点拆分文件,命名对齐,便于定位与维护。
- Service 的输入/输出类型在对应端点文件定义(如
login.go定义LoginInput/LoginResult)。
12. Repository 规范
12.1 层职责
- 负责数据访问(CRUD、分页、事务),不包含业务规则与 HTTP 细节。
- 仅依赖底层 DAO/Model,向 Service 层提供稳定的数据操作能力。
12.2 目录与组件
- 目录:
internal/repository/mysqldao/:由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,符合轻量项目场景;当跨表协作复杂时可引入仓库接口以降低耦合。