Skip to main content

后端项目专用开发规范

基于框架 git.topu-internal.dev/framework-go/core/ 系列包的实际代码分析整理。 本规范为强制性约定,所有后端开发人员必须遵守。


目录

  1. 项目初始化清单
  2. 目录结构
  3. 新增功能开发流程
  4. 配置文件规范
  5. 响应格式规范
  6. 错误码规范
  7. Auth 实现规范
  8. DB 操作规范
  9. 告警规范
  10. 中间件配置说明
  11. Trace 链路规范
  12. Wire DI 规范
  13. 测试规范
  14. 数据库变更规范
  15. .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_CONFconfigs配置根目录(对应 --workdir
APP_ENVdev环境目录名(对应 --config),可选 dev / staging / production
APP_LOG_PRE_DIR(空)日志输出目录(对应 --logdir
JWT_SECRET(无)JWT 签名密钥,生产环境必须设置强随机值
JWT_EXPIRE_HOURS4JWT 有效期(小时),默认 4h
BCRYPT_COST12bcrypt 加密强度,生产环境不低于 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.ymlgin_middleware 节点下配置:

中间件字段说明
RecoveryrecoveryPanic 恢复,必须启用,防止单个请求 panic 导致服务崩溃
CORScors.enable: true跨域支持,开启后按 allow_origins 白名单放行
Tracetrace.enable: true链路追踪,从 X-Request-Id 读取或生成 TraceID,写入响应头 X-Trace-Id
CopyBodycopy_body.enable: true允许多次读取 request body(默认只能读一次),Logger 中间件和业务层同时需要时必须开启
Loggerlogger.enable: trueHTTP 请求日志(method / path / status / latency / body),生产环境建议开启
Languagelanguage.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定义 ControllerTreeServiceTreeInjector struct 和对应的 wire.NewSet
wirex/wire.go声明 wire.Build,使用 //go:build wireinject tag
wirex/wire_gen.gowire 自动生成,必须提交到 git,不加入 .gitignore

12.2 新增 Controller/Service 的完整步骤

  1. wirex/injector.goControllerTreeServiceTree struct 中添加新字段
  2. 在对应的 ConstructXxx wire.NewSet 中添加构造函数
  3. 运行 wire ./internal/wirex/(或 wire ./wirex/,取决于实际路径)
  4. 确认 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