后端项目专用开发规范
基于框架
git.topu-internal.dev/framework-go/core/系列包的实际代码分析整理。 本规范为强制性约定,所有后端开发人员必须遵守。
目录
- 项目初始化清单
- 目录结构
- 新增功能开发流程
- 配置文件规范
- 响应格式规范
- 错误码规范
- Auth 实现规范
- DB 操作规范
- 告警规范
- 中间件配置说明
- Trace 链路规范
- Wire DI 规范
- 测试规范
- 数据库变更规范
- .gitignore 必含项
1. 项目初始化清单
1.1 Module 命名规则
module {project-name}
⚠️ 禁止使用 basic 作为 module 名。 module 名必须与项目名一致,使用小写连字符格式,例如:
module clawzone-api
module clawzone-admin
module clawzone-worker
1.2 go.mod 框架包依赖
新建项目 go.mod 中必须引入以下框架包:
require (
// 核心框架
git.topu-internal.dev/framework-go/core/api-core v0.x.x
git.topu-internal.dev/framework-go/core/api-plugin-general v0.x.x
git.topu-internal.dev/framework-go/core/gredis v0.x.x
git.topu-internal.dev/framework-go/core/kernel v0.x.x
git.topu-internal.dev/framework-go/core/loader v0.x.x
git.topu-internal.dev/framework-go/core/logging v0.x.x
git.topu-internal.dev/framework-go/core/logging-hook-file v0.x.x
git.topu-internal.dev/framework-go/core/xormc v0.x.x
git.topu-internal.dev/framework-go/core/alarm v0.x.x
git.topu-internal.dev/framework-go/core/swoole-socket v0.x.x
// Web 框架
github.com/gin-gonic/gin v1.x.x
// DI
github.com/google/wire v0.x.x
// CLI
github.com/urfave/cli/v2 v2.x.x
// Auth
golang.org/x/oauth2 v0.x.x
github.com/golang-jwt/jwt/v5 v5.x.x
golang.org/x/crypto v0.x.x // bcrypt
// 测试
github.com/stretchr/testify v1.x.x
)
版本号以实际 go.sum 锁定为准,此处列出依赖清单,具体版本由
go get解析。
1.3 环境变量清单
| 变量名 | 默认值 | 必填 | 说明 |
|---|---|---|---|
APP_CONF | configs | 否 | 配置根目录(对应 --workdir) |
APP_ENV | dev | 否 | 环境目录名(对应 --config),可选 dev / staging / production |
APP_LOG_PRE_DIR | (空) | 否 | 日志输出目录(对应 --logdir) |
JWT_SECRET | (无) | 是 | JWT 签名密钥,生产环境必须设置强随机值 |
JWT_EXPIRE_HOURS | 4 | 否 | JWT 有效期(小时),默认 4h |
BCRYPT_COST | 12 | 否 | bcrypt 加密强度,生产环境不低于 12 |
JWT_SECRET严禁硬编码,必须通过环境变量或配置中心注入。
2. 目录结构
{project-name}/
├── main.go # CLI 入口(urfave/cli/v2),仅负责参数解析和调用 bootstrap
├── go.mod # module {project-name}
├── go.sum
├── CHANGELOG.md # 版本变更记录
├── .gitignore
│
├── configs/ # 配置目录(按环境隔离)
│ ├── dev/
│ │ ├── config.yml # 通用配置(提交到 git)
│ │ └── db.yaml # DB/Redis 配置(禁止提交到 git)
│ ├── staging/
│ │ ├── config.yml
│ │ └── db.yaml
│ └── production/
│ ├── config.yml
│ └── db.yaml
│
├── internal/
│ ├── api/
│ │ └── controller/ # Gin 控制器层
│ │ ├── xxx_controller.go # 每个业务一个控制器文件
│ │ └── ...
│ │
│ ├── bootstrap/
│ │ ├── bootstrap.go # 应用启动逻辑(读配置、初始化组件、启动服务)
│ │ └── http.go # Gin 服务器初始化(注册中间件、路由)
│ │
│ ├── config/
│ │ └── config.go # 全局 Config struct 定义
│ │
│ ├── database/
│ │ ├── manager.go # RegisterSubDB / SetConfigProvider / ProvideInitMarker
│ │ ├── db/
│ │ │ └── {dbname}/ # 各 DB 的 Model 定义 + 查询方法
│ │ │ ├── model.go # xorm Model struct(embed UnixTimeModel)
│ │ │ ├── query.go # 查询方法(Get/List/Create/Update/Delete)
│ │ │ └── init.go # init() 注册 SubDB
│ │ └── redis/
│ │ └── redis.go # Redis client 封装(GetClient/Set/Get/Del 等)
│ │
│ ├── pkg/
│ │ ├── zapx/ # 文件日志工具(基于 logging-hook-file)
│ │ └── default/
│ │ └── errors.go # 项目错误码统一定义
│ │
│ └── service/
│ ├── alarm/ # 告警服务(继承 AlarmInherit 模式)
│ │ └── alarm.go
│ ├── {business}/ # 业务 service(按功能模块划分)
│ │ ├── service.go
│ │ └── service_test.go
│ └── provider.go # wire provider 汇总(各 service 的 wire.NewSet)
│
├── wirex/
│ ├── injector.go # ControllerTree + ServiceTree + Injector 定义
│ ├── wire.go # wire.Build 声明(//go:build wireinject)
│ └── wire_gen.go # wire 自动生成(需提交到 git)
│
├── doc/
│ ├── design/ # PRD / 原型 / 数据库设计文档
│ ├── api/ # Swagger / OpenAPI 定义
│ ├── schedule/ # 排期计划
│ └── deployment/ # 部署文档(环境搭建 / 上线 SOP)
│
└── migrations/ # 数据库变更 SQL
├── V001__init_schema.sql
├── V002__add_users_table.sql
└── ...
3. 新增功能开发流程
新增一个完整业务功能,必须按以下步骤顺序执行:
Step 1:定义 DB Model
路径:internal/database/db/{dbname}/
// model.go
package {dbname}
import "git.topu-internal.dev/framework-go/core/xormc"
type User struct {
xormc.UnixTimeModel `xorm:"extends"` // 包含 ID、CreatedAt、UpdatedAt
Name string `xorm:"name"`
Email string `xorm:"email unique"`
}
func (u *User) TableName() string {
return "users"
}
// init.go
package {dbname}
import "your-project/internal/database"
var {dbname}DB xormc.XormDrive
func init() {
database.RegisterSubDB("{dbname}", &{dbname}DB, database.DBTypeAll)
}
// query.go
package {dbname}
import "git.topu-internal.dev/framework-go/core/xormc"
// GetUserByEmail 通过邮箱查找用户
func GetUserByEmail(email string) (*User, error) {
var u User
has, err := {dbname}DB.Where("email = ?", email).Get(&u)
return xormc.CheckGetErr(&u, has, err)
}
// CreateUser 创建用户
func CreateUser(u *User) error {
_, err := {dbname}DB.Insert(u)
return xormc.CheckChangeErr(err)
}
Step 2:编写 Service
路径:internal/service/{feature}/
// service.go
package {feature}
import (
"{project}/internal/database/db/{dbname}"
"{project}/internal/service/alarm"
"go.uber.org/zap"
)
type {Feature}Service struct {
alarmSvc *alarm.AlarmInherit
logger *zap.Logger
}
func New{Feature}Service(alarmSvc *alarm.AlarmInherit, logger *zap.Logger) *{Feature}Service {
return &{Feature}Service{alarmSvc: alarmSvc, logger: logger}
}
func (s *{Feature}Service) GetList(page, size int) ([]*{dbname}.{Model}, error) {
// 业务逻辑在 service 层处理
// ...
}
在 internal/service/provider.go 中注册:
var ProvideService = wire.NewSet(
// ... 已有 service
{feature}.New{Feature}Service,
)
Step 3:编写 Controller
路径:internal/api/controller/
// {feature}_controller.go
package controller
import (
"github.com/gin-gonic/gin"
api "git.topu-internal.dev/framework-go/core/api-core"
"{project}/internal/service/{feature}"
)
type {Feature}Controller struct {
Svc *{feature}.{Feature}Service
}
// GetGroupName 返回路由组名,最终路由为 /v1/{group}/{action}
func (c *{Feature}Controller) GetGroupName() string {
return "{feature}"
}
// Register 注册该控制器的所有路由
func (c *{Feature}Controller) Register(rg *gin.RouterGroup) {
g := rg.Group(c.GetGroupName())
g.GET("/list", c.list)
g.POST("/create", c.authRequired(), c.create)
}
func (c *{Feature}Controller) list(ctx *gin.Context) {
result, err := c.Svc.GetList(1, 20)
if err != nil {
api.ResFail(ctx, err.Error())
return
}
api.ResSuccess(ctx, gin.H{"list": result})
}
func (c *{Feature}Controller) create(ctx *gin.Context) {
// ... 解析请求,调用 service
api.ResOK(ctx)
}
Step 4:注册到 wirex/injector.go
// wirex/injector.go
type ControllerTree struct {
// ... 已有 Controller
{Feature} *controller.{Feature}Controller // ← 新增
}
var ConstructController = wire.NewSet(
// ... 已有
wire.Struct(new(controller.{Feature}Controller), "*"), // ← 新增
)
type ServiceTree struct {
// ... 已有 Service
{Feature} *{feature}.{Feature}Service // ← 新增
}
var ConstructService = wire.NewSet(
service.ProvideService, // provider.go 中已包含新 service
)
Step 5:运行 wire 生成
wire ./internal/wirex/
生成后的
wire_gen.go必须提交到 git,不需要加入 .gitignore。
Step 6:编写单元测试
// service_test.go
package {feature}_test
import (
"testing"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
func Test{Feature}Service_GetList(t *testing.T) {
// 使用 testify 断言
svc := setup{Feature}Service(t)
result, err := svc.GetList(1, 10)
require.NoError(t, err)
assert.NotNil(t, result)
}
Step 7:本地验证,提交 MR
# 本地运行
go run main.go --config dev
# 跑测试
go test ./... -v -cover
# 提交
git add .
git commit -m "feat({feature}): add {feature} API"
git push origin feature/{feature}
# 在 GitLab / GitHub 创建 MR,等待 Code Review
4. 配置文件规范
4.1 目录命名
环境目录名只允许以下三种:
| 目录名 | 用途 |
|---|---|
dev | 本地开发环境 |
staging | 预发布 / 测试环境 |
production | 生产环境 |
4.2 config.yml 必填字段
# configs/{env}/config.yml
general:
node_key: "your-node-key" # 节点标识,必填
# kernel.General 内联字段按框架要求填写
protocols:
http:
host: "0.0.0.0"
port: 8080
logger:
level: "info" # debug / info / warn / error
encoding: "json" # json(生产)或 console(开发)
file:
enable: true # 是否启用文件日志
filename: "logs/app.log"
max_size: 100 # MB
max_backups: 10
max_age: 30 # 天
gin_middleware:
recovery:
enable: true
cors:
enable: true
allow_origins:
- "https://your-frontend.com"
allow_methods: ["GET", "POST", "PUT", "DELETE", "OPTIONS"]
allow_headers: ["Authorization", "Content-Type", "X-Request-Id"]
trace:
enable: true # 启用后自动处理 X-Request-Id / X-Trace-Id
copy_body:
enable: true # 允许多次读取 request body
logger:
enable: true # HTTP 请求日志
language:
enable: true # i18n 语言检测
default_lang: "zh"
4.3 db.yaml 必填字段
# configs/{env}/db.yaml
# ⚠️ 此文件禁止提交到 git,必须加入 .gitignore
# 示例:名为 main 的数据库
main:
driver: "mysql"
dsn: "user:password@tcp(127.0.0.1:3306)/dbname?charset=utf8mb4&parseTime=True"
max_idle_conns: 10
max_open_conns: 100
conn_max_lifetime: 3600 # 秒
default_redis:
addr: "127.0.0.1:6379"
password: ""
db: 0
pool_size: 10
每个项目的 DB 字段名(此处为
main)必须与config.go中的 Config struct 字段名和 yaml tag 保持一致。
5. 响应格式规范
⚠️ 严禁手动构造 JSON 响应。 所有 HTTP 响应必须使用框架提供的响应助手函数。
5.1 可用的响应函数
import api "git.topu-internal.dev/framework-go/core/api-core"
// 成功,无数据
api.ResOK(c)
// → HTTP 200,body: {"code": 0, "message": "ok"}
// 成功,带数据
api.ResSuccess(c, gin.H{"list": items, "total": 100})
// → HTTP 200,body: {"code": 0, "message": "ok", "data": {...}}
// 业务失败(HTTP 200,业务层错误)
api.ResFail(c, "用户不存在")
// → HTTP 200,body: {"code": -1, "message": "用户不存在"}
// 带 HTTP 状态码的失败
api.ResFailWithStatus(c, 404, "资源未找到")
// → HTTP 404,body: {"code": -1, "message": "资源未找到"}
api.ResFailWithStatus(c, 401, "未授权")
// → HTTP 401,body: {"code": -1, "message": "未授权"}
5.2 统一响应结构
框架保证所有响应格式一致:
{
"code": 0, // 0=成功,-1=失败,其他=业务错误码
"message": "ok", // 提示信息
"data": {} // 可选,仅 ResSuccess 时存在
}
5.3 禁止行为
// ❌ 禁止
c.JSON(200, gin.H{"code": 0, "msg": "success"})
c.JSON(200, map[string]interface{}{...})
// ✅ 正确
api.ResSuccess(c, gin.H{"list": items})
6. 错误码规范
6.1 定义位置
所有错误码统一定义在 internal/pkg/default/errors.go:
package defaultpkg
import "git.topu-internal.dev/framework-go/core/api-core/errors"
// 用户相关 10001 - 10099
var (
ErrUserNotFound = errors.New(errors.DefaultForbiddenID, "用户不存在", 10001)
ErrUserDisabled = errors.New(errors.DefaultForbiddenID, "账号已被禁用", 10002)
ErrInvalidToken = errors.New(errors.DefaultForbiddenID, "Token 无效或已过期", 10003)
)
// 权限相关 10100 - 10199
var (
ErrPermissionDenied = errors.New(errors.DefaultForbiddenID, "权限不足", 10100)
)
// 业务相关 10200+(按模块分段,每个模块预留 100 个码位)
var (
// 示例:订单模块 10200 - 10299
ErrOrderNotFound = errors.New(errors.DefaultForbiddenID, "订单不存在", 10200)
)
6.2 错误码分段规则
| 范围 | 用途 |
|---|---|
10001 - 10099 | 用户 / 认证相关 |
10100 - 10199 | 权限相关 |
10200 - 10299 | 模块 A(按项目实际模块分配) |
10300 - 10399 | 模块 B |
... | 每个模块预留 100 个码位 |
6.3 在 Controller 中使用
func (c *UserController) getUser(ctx *gin.Context) {
user, err := c.Svc.GetUser(uid)
if err != nil {
if errors.Is(err, defaultpkg.ErrUserNotFound) {
api.ResFailWithStatus(ctx, 404, err.Error())
return
}
api.ResFail(ctx, err.Error())
return
}
api.ResSuccess(ctx, gin.H{"user": user})
}
7. Auth 实现规范
7.1 Google SSO 接入流程
1. 前端重定向到 GET /v1/auth/google/login
→ 后端生成 state(随机串,存 Redis,TTL=10min)
→ 重定向到 Google OAuth 授权页
2. Google 回调 GET /v1/auth/google/callback?code=xxx&state=xxx
→ 验证 state(从 Redis 取,用完即删,防 CSRF)
→ 用 code 换取 Google access token
→ 调用 Google UserInfo API 获取 email / name / picture
→ 查找或创建本地 User 记录(email 唯一)
→ 签发 JWT(见 7.2)
→ 返回 token 给前端
7.2 JWT + Redis 双验证
// 签发 token
func (s *AuthService) IssueToken(uid int64) (string, error) {
jti := uuid.NewString()
expireHours := getEnvInt("JWT_EXPIRE_HOURS", 4)
claims := jwt.MapClaims{
"uid": uid,
"jti": jti,
"exp": time.Now().Add(time.Duration(expireHours) * time.Hour).Unix(),
}
token, err := jwt.NewWithClaims(jwt.SigningMethodHS256, claims).
SignedString([]byte(os.Getenv("JWT_SECRET")))
if err != nil {
return "", err
}
// 同步写入 Redis,key = jwt:{uid}:{jti},TTL = 4h
key := fmt.Sprintf("jwt:%d:%s", uid, jti)
if err := redis.Set(key, "1", time.Duration(expireHours)*time.Hour); err != nil {
return "", err
}
return token, nil
}
// 验证 token(在 authRequired 中间件中调用)
func validateToken(tokenStr string) (uid int64, jti string, err error) {
token, err := jwt.Parse(tokenStr, func(t *jwt.Token) (interface{}, error) {
if _, ok := t.Method.(*jwt.SigningMethodHMAC); !ok {
return nil, fmt.Errorf("unexpected signing method")
}
return []byte(os.Getenv("JWT_SECRET")), nil
})
if err != nil || !token.Valid {
return 0, "", defaultpkg.ErrInvalidToken
}
claims := token.Claims.(jwt.MapClaims)
uid = int64(claims["uid"].(float64))
jti = claims["jti"].(string)
// 查 Redis,token 是否仍有效
key := fmt.Sprintf("jwt:%d:%s", uid, jti)
exists, err := redis.Exists(key)
if err != nil || !exists {
return 0, "", defaultpkg.ErrInvalidToken
}
// 每次请求续期(滑动 TTL)
expireHours := getEnvInt("JWT_EXPIRE_HOURS", 4)
_ = redis.Expire(key, time.Duration(expireHours)*time.Hour)
return uid, jti, nil
}
7.3 登出流程
func (s *AuthService) Logout(uid int64, jti string) error {
key := fmt.Sprintf("jwt:%d:%s", uid, jti)
return redis.Del(key)
// Redis key 删除后,即使 JWT 本身未过期,验证也会失败
}
7.4 authRequired() 中间件写法
// internal/api/controller/middleware.go
func (c *BaseController) authRequired() gin.HandlerFunc {
return func(ctx *gin.Context) {
authHeader := ctx.GetHeader("Authorization")
if authHeader == "" || !strings.HasPrefix(authHeader, "Bearer ") {
api.ResFailWithStatus(ctx, 401, "未提供认证 Token")
ctx.Abort()
return
}
tokenStr := strings.TrimPrefix(authHeader, "Bearer ")
uid, jti, err := validateToken(tokenStr)
if err != nil {
api.ResFailWithStatus(ctx, 401, err.Error())
ctx.Abort()
return
}
// 将 uid / jti 注入 context,供 handler 使用
ctx.Set("uid", uid)
ctx.Set("jti", jti)
ctx.Next()
}
}
8. DB 操作规范
8.1 Model 定义规范
// ✅ 正确:embed UnixTimeModel,实现 TableName()
type Order struct {
xormc.UnixTimeModel `xorm:"extends"` // 提供 Id、CreatedAt、UpdatedAt(unix 时间戳)
UserId int64 `xorm:"user_id index"`
Amount int64 `xorm:"amount"` // 金额单位:分,避免浮点精度问题
Status int8 `xorm:"status"`
Remark string `xorm:"remark"`
}
func (o *Order) TableName() string {
return "orders"
}
8.2 查询方法命名约定
| 前缀 | 语义 | 示例 |
|---|---|---|
Get | 查单条 | GetOrderById, GetUserByEmail |
List | 查列表 | ListOrdersByUser, ListActiveUsers |
Create | 插入 | CreateOrder, CreateUser |
Update | 更新 | UpdateOrderStatus, UpdateUserName |
Delete | 删除(软删) | DeleteOrder |
Count | 计数 | CountOrdersByStatus |
8.3 使用框架的错误检查函数
// 查单条
has, err := db.Where("id = ?", id).Get(&obj)
result, err := xormc.CheckGetErr(&obj, has, err)
// has=false 时返回 xormc.ErrNotFound,err 时透传
// 写操作(Insert / Update / Delete)
_, err := db.Insert(&obj)
err = xormc.CheckChangeErr(err)
8.4 分层禁止规则
Controller 层 → 只负责解析请求 + 调用 Service + 返回响应
Service 层 → 业务逻辑 + 事务管理 + 调用 DB 方法
DB 层 → 只负责 SQL 操作,不含任何业务逻辑
⚠️ 禁止在 Controller 层直接调用 DB 查询方法。 所有数据库操作必须经过 Service 层。
// ❌ 禁止
func (c *UserController) getUser(ctx *gin.Context) {
user, _ := userdb.GetUserById(uid) // 直接调 DB
api.ResSuccess(ctx, user)
}
// ✅ 正确
func (c *UserController) getUser(ctx *gin.Context) {
user, err := c.Svc.GetUser(uid) // 经过 Service
if err != nil {
api.ResFail(ctx, err.Error())
return
}
api.ResSuccess(ctx, user)
}
9. 告警规范
9.1 告警服务结构
// internal/service/alarm/alarm.go
package alarm
import (
baseAlarm "git.topu-internal.dev/framework-go/core/alarm"
)
type AlarmInherit struct {
*baseAlarm.AlarmManager
env string
}
func NewAlarmInherit(env string, redisCli *redis.Client) *AlarmInherit {
mgr := baseAlarm.NewAlarmManager(redisCli)
return &AlarmInherit{AlarmManager: mgr, env: env}
}
// Push 发送告警
// - 测试环境(dev/staging):使用固定 TG 配置(写死测试 bot)
// - 生产环境:使用配置中心的 TG config
func (a *AlarmInherit) Push(title, content string) error {
var tgConfig baseAlarm.TelegramConfig
if a.env == "production" {
tgConfig = loadTgConfigFromCenter() // 从配置中心读取
} else {
tgConfig = baseAlarm.TelegramConfig{
BotToken: "test-bot-token", // 固定测试 bot
ChatId: -1001234567890,
}
}
// 5分钟去重(DeduplicateTTL = 5min,框架默认值)
return a.AlarmManager.PushWithDedup(title, content, tgConfig)
}
9.2 使用规范
凡是 error 级别日志,必须同时通过 AlarmInherit.Push 发送 Telegram 告警:
func (s *OrderService) ProcessOrder(orderId int64) error {
err := doProcess(orderId)
if err != nil {
// 1. 打日志(带 TraceID)
s.logger.Error("订单处理失败",
zap.Int64("order_id", orderId),
zap.Error(err),
)
// 2. 发告警(必须与 error 日志同时触发)
_ = s.alarmSvc.Push(
"订单处理失败",
fmt.Sprintf("OrderId: %d, Error: %v", orderId, err),
)
return err
}
return nil
}
9.3 去重机制
- 框架默认 5 分钟内相同标题的告警只发送一次(基于 Redis 实现去重)。
- 不要自行实现去重逻辑,直接调用
AlarmInherit.Push即可。
10. 中间件配置说明
所有中间件在 config.yml 的 gin_middleware 节点下配置:
| 中间件 | 字段 | 说明 |
|---|---|---|
| Recovery | recovery | Panic 恢复,必须启用,防止单个请求 panic 导致服务崩溃 |
| CORS | cors.enable: true | 跨域支持,开启后按 allow_origins 白名单放行 |
| Trace | trace.enable: true | 链路追踪,从 X-Request-Id 读取或生成 TraceID,写入响应头 X-Trace-Id |
| CopyBody | copy_body.enable: true | 允许多次读取 request body(默认只能读一次),Logger 中间件和业务层同时需要时必须开启 |
| Logger | logger.enable: true | HTTP 请求日志(method / path / status / latency / body),生产环境建议开启 |
| Language | language.enable: true | 从请求头 Accept-Language 检测语言,注入到 context 供 i18n 使用 |
中间件注册顺序(在 bootstrap/http.go 中按此顺序注册):
engine.Use(
middleware.Recovery(cfg.GinMiddleware.Recovery),
middleware.Trace(cfg.GinMiddleware.Trace), // Trace 必须在 Logger 前
middleware.CopyBody(cfg.GinMiddleware.CopyBody),
middleware.Logger(cfg.GinMiddleware.Logger),
middleware.CORS(cfg.GinMiddleware.CORS),
middleware.Language(cfg.GinMiddleware.Language),
)
11. Trace 链路规范
11.1 TraceID 流转规则
客户端请求头 X-Request-Id: {uuid}
↓
Trace 中间件读取(若无则自动生成 uuid)
↓
写入 gin.Context(key: "trace_id")
↓
响应头 X-Trace-Id: {uuid}(返回给客户端)
11.2 日志中必须带 TraceID
所有业务日志必须携带 TraceID,便于全链路问题排查:
func (c *OrderController) create(ctx *gin.Context) {
traceId := ctx.GetString("trace_id")
logger := zap.L().With(zap.String("trace_id", traceId))
// 传递 logger 到 service
result, err := c.Svc.CreateOrder(ctx, &req)
if err != nil {
logger.Error("创建订单失败", zap.Error(err))
api.ResFail(ctx, err.Error())
return
}
logger.Info("订单创建成功", zap.Any("order", result))
api.ResSuccess(ctx, gin.H{"order": result})
}
11.3 Service 层传递 TraceID
推荐将 context.Context 作为 service 方法的第一个参数,通过 context 传递 TraceID:
// ✅ 推荐
func (s *OrderService) CreateOrder(ctx context.Context, req *CreateOrderReq) (*Order, error) {
traceId := ctx.Value("trace_id").(string)
s.logger.With(zap.String("trace_id", traceId)).Info("开始创建订单")
// ...
}
12. Wire DI 规范
12.1 文件职责
| 文件 | 职责 |
|---|---|
wirex/injector.go | 定义 ControllerTree、ServiceTree、Injector struct 和对应的 wire.NewSet |
wirex/wire.go | 声明 wire.Build,使用 //go:build wireinject tag |
wirex/wire_gen.go | wire 自动生成,必须提交到 git,不加入 .gitignore |
12.2 新增 Controller/Service 的完整步骤
- 在
wirex/injector.go的ControllerTree或ServiceTreestruct 中添加新字段 - 在对应的
ConstructXxxwire.NewSet 中添加构造函数 - 运行
wire ./internal/wirex/(或wire ./wirex/,取决于实际路径) - 确认
wire_gen.go已更新,将其加入本次提交
// wirex/injector.go 示例
type ControllerTree struct {
Auth *controller.AuthController
User *controller.UserController
Order *controller.OrderController // ← 新增
}
var ConstructController = wire.NewSet(
wire.Struct(new(ControllerTree), "*"),
wire.Struct(new(controller.AuthController), "*"),
wire.Struct(new(controller.UserController), "*"),
wire.Struct(new(controller.OrderController), "*"), // ← 新增
)
type ServiceTree struct {
Alarm *alarm.AlarmInherit
Auth *authsvc.AuthService
User *usersvc.UserService
Order *ordersvc.OrderService // ← 新增
}
var ConstructService = wire.NewSet(
wire.Struct(new(ServiceTree), "*"),
service.ProvideService, // 在 service/provider.go 中汇总所有 service 构造函数
)
type Injector struct {
Config *config.Config
RedisQueryFun gredis.QueryFun
I18nQuery api.I18nQuery
AlarmMgr *alarm.AlarmInherit
STree *ServiceTree
CTree *ControllerTree
}
12.3 禁止手写 wire_gen.go
# ❌ 禁止手动编辑 wire_gen.go
# ✅ 始终通过命令生成
wire ./internal/wirex/
13. 测试规范
13.1 测试哲学
- 采用 TDD(测试驱动开发):先写测试,再写实现
- Service 层必须有单元测试
- Controller 层使用 httptest 进行集成测试
- 覆盖率要求:核心业务 Service 层不低于 70%
13.2 测试工具栈
import (
"testing"
"net/http/httptest"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"github.com/stretchr/testify/mock"
"github.com/stretchr/testify/suite"
)
13.3 Service 层单元测试示例
// internal/service/user/service_test.go
package user_test
import (
"testing"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
func TestUserService_GetUser(t *testing.T) {
svc := setupTestUserService(t) // 使用 test DB 或 mock
t.Run("用户存在", func(t *testing.T) {
user, err := svc.GetUser(1)
require.NoError(t, err)
assert.Equal(t, int64(1), user.Id)
})
t.Run("用户不存在", func(t *testing.T) {
_, err := svc.GetUser(99999)
assert.ErrorIs(t, err, defaultpkg.ErrUserNotFound)
})
}
13.4 Controller 层集成测试示例
// internal/api/controller/user_controller_test.go
func TestUserController_GetUser(t *testing.T) {
router := setupTestRouter(t)
req := httptest.NewRequest(http.MethodGet, "/v1/user/detail?id=1", nil)
req.Header.Set("Authorization", "Bearer "+getTestToken(t))
w := httptest.NewRecorder()
router.ServeHTTP(w, req)
assert.Equal(t, http.StatusOK, w.Code)
var resp map[string]interface{}
json.Unmarshal(w.Body.Bytes(), &resp)
assert.Equal(t, float64(0), resp["code"])
}
13.5 运行测试
# 运行所有测试
go test ./... -v
# 带覆盖率
go test ./... -cover -coverprofile=coverage.out
go tool cover -html=coverage.out # 可视化覆盖率报告
# 只跑指定包
go test ./internal/service/user/... -v -run TestUserService
14. 数据库变更规范
14.1 文件命名规则
所有数据库变更 SQL 放在 migrations/ 目录,命名格式:
V{序号,三位零填充}__{描述}.sql
示例:
migrations/
├── V001__init_schema.sql
├── V002__create_users_table.sql
├── V003__add_email_index_to_users.sql
├── V004__create_orders_table.sql
└── V005__add_status_column_to_orders.sql
14.2 编写规范
-- V002__create_users_table.sql
-- Author: Tom
-- Date: 2026-03-06
-- Description: 创建用户表
CREATE TABLE IF NOT EXISTS `users` (
`id` BIGINT UNSIGNED NOT NULL AUTO_INCREMENT,
`name` VARCHAR(100) NOT NULL DEFAULT '',
`email` VARCHAR(255) NOT NULL DEFAULT '' UNIQUE,
`avatar` VARCHAR(500) NOT NULL DEFAULT '',
`status` TINYINT NOT NULL DEFAULT 1 COMMENT '1=正常, 0=禁用',
`created_at` BIGINT NOT NULL DEFAULT 0 COMMENT 'Unix 时间戳',
`updated_at` BIGINT NOT NULL DEFAULT 0 COMMENT 'Unix 时间戳',
PRIMARY KEY (`id`),
KEY `idx_email` (`email`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='用户表';
14.3 变更执行规则
- 禁止直接修改已执行的 SQL 文件,需要修改就新建一个版本号更高的文件
- 每次变更前,先在 dev 环境验证 SQL 正确性
- MR 中必须包含对应的 migration SQL
- 生产环境变更由 DBA 或负责人审核后执行
15. .gitignore 必含项
项目根目录 .gitignore 必须包含以下内容:
# DB / Redis 配置(含敏感信息,禁止提交)
configs/*/db.yaml
configs/*/*.env
# 本地环境变量
.env
.env.*
# 日志文件
logs/
*.log
# 编译产物
/bin/
/{project-name}
# IDE
.idea/
.vscode/
*.swp
*.swo
# macOS
.DS_Store
# Go 测试覆盖率
coverage.out
coverage.html
⚠️ 注意:
wire_gen.go不需要 加入 .gitignore,需要提交到 git,否则 CI 无法构建。
附录:快速参考卡
框架包导入路径
api "git.topu-internal.dev/framework-go/core/api-core"
mw "git.topu-internal.dev/framework-go/core/api-plugin-general/middleware"
gredis "git.topu-internal.dev/framework-go/core/gredis"
kernel "git.topu-internal.dev/framework-go/core/kernel"
loader "git.topu-internal.dev/framework-go/core/loader"
log "git.topu-internal.dev/framework-go/core/logging"
logf "git.topu-internal.dev/framework-go/core/logging-hook-file"
xormc "git.topu-internal.dev/framework-go/core/xormc"
alarm "git.topu-internal.dev/framework-go/core/alarm"
常用命令
# 生成 wire DI
wire ./internal/wirex/
# 运行项目(dev 环境)
go run main.go --config dev --workdir configs
# 运行测试
go test ./... -v -cover
# 查看覆盖率
go tool cover -func=coverage.out | grep total
# 构建
go build -o bin/{project-name} main.go
最后更新:2026-03-06 | 维护人:Tom