可乐呢o3o

可乐呢o3o

未命名文章

0
2026-04-17

admission-api 后端框架模板设计文档

版本:v1.3(含用户模块和基础登录/注册)
目标:为志愿报考分析平台提供一套可直接复制粘贴业务代码的 Go 后端模板
核心约束:1 个月内上线 Web MVP,未来覆盖小程序/App,PostgreSQL 性能最大化


1. 设计目标与边界

1.1 目标

  • 提供一个开箱即用的 Go 后端项目模板
  • 未来开发新功能时,只需在 handler/service/store/ 里按固定模式新增文件
  • 内置扩展逃生舱:模块化单体结构、统一 API 版本前缀、清晰的三层边界

1.2 IN SCOPE(模板必须包含)

  • 项目结构、配置管理、日志、路由
  • pgx 数据库连接池 + 事务封装
  • golang-migrate 数据库迁移
  • JWT Access Token + Refresh Token 认证
  • RBAC 中间件(user / admin
  • Platform 中间件:识别请求来源平台(web / app / wxmini),注入 context
  • 统一 API 响应格式、统一错误码
  • 用户模块users 表、user_store、注册/登录/获取个人信息接口
  • 基础示例接口:GET /health 健康检查
  • 测试模板(API 测试示例)
  • Docker Compose 开发环境(PG + Redis)
  • Makefile 常用命令

1.3 NOT IN SCOPE(模板不包含,后续按需添加)

  • 志愿报考相关业务表(schoolsscoresmajors 等)
  • 志愿报考相关业务接口
  • 消息队列(NATS/Redis Streams)
  • 复杂 RBAC(组织/部门/多级权限)
  • 文件上传/OSS 接口
  • 推荐算法引擎
  • K8s 部署配置

2. 技术栈

层级技术理由
语言Go 1.22+并发性能好,编译快,部署单二进制
路由chi (go-chi/chi/v5)标准库兼容、中间件链清晰
数据库驱动pgx (jackc/pgx/v5)性能优,支持批量插入
连接池pgxpool内置健康检查
迁移golang-migrateindustry standard
认证JWT (golang-jwt/jwt/v5)Access + Refresh 双 Token
密码哈希bcrypt标准库
配置godotenv + os.Getenv零依赖
日志log/slog (JSON handler)Go 内置,结构化
接口文档swaggo/swag + http-swagger通过代码注释自动生成 Swagger UI
容器Docker Compose一键启动

3. 项目目录结构

admission-api/
├── cmd/
│   └── api/
│       └── main.go              # 唯一入口:初始化配置、DB、路由、启动 server
├── internal/
│   ├── platform/                # 跨模块共享的基础设施
│   │   ├── config/
│   │   │   └── config.go        # 配置结构体 + Load()
│   │   ├── db/
│   │   │   └── db.go            # pgxpool 初始化 + 事务封装
│   │   ├── middleware/
│   │   │   ├── jwt.go           # JWT 生成/校验中间件
│   │   │   ├── rbac.go          # 角色权限校验中间件
│   │   │   ├── platform.go      # 平台识别中间件
│   │   │   ├── cors.go          # CORS 中间件
│   │   │   ├── logger.go        # 请求日志 + Trace ID + 耗时
│   │   │   └── recover.go       # Panic 恢复
│   │   └── web/
│   │       ├── response.go      # 统一 API 响应结构体
│   │       └── handler.go       # BaseHandler 工具方法
│   ├── health/                  # 健康检查模块(最小示例)
│   │   └── handler.go           # GET /health
│   └── user/                    # 用户模块(业务模块示例)
│       ├── model.go             # User 领域模型
│       ├── store.go             # User 数据访问层
│       ├── service.go           # 认证业务逻辑
│       └── handler.go           # 注册/登录/刷新/获取个人信息接口
├── migration/
│   ├── 001_users.up.sql         # users 表
│   ├── 001_users.down.sql
│   └── .gitkeep
├── tests/
│   └── integration/
│       └── health_test.go       # API 测试示例
├── docs/                        # Swagger 自动生成的接口文档
│   ├── docs.go
│   ├── swagger.json
│   └── swagger.yaml
├── docker-compose.yml           # PG 15 + Redis 7
├── Makefile
├── .env.example
└── go.mod

目录规范

  • 按业务模块组织(Package by Feature):每个模块一个目录,包含该模块相关的 modelstoreservicehandler
  • internal/platform/ 存放跨模块共享的基础设施:配置、数据库连接、中间件、统一响应格式。
  • migration/ 包含 users 表的迁移,确保登录/注册功能可直接运行。refresh_tokens 不存 PG,改存 Redis。
  • 未来新增业务模块时,直接在 internal/ 下新建目录,如 internal/school/internal/recommend/internal/score/,按 user/ 的模式复制即可。
  • 禁止模块间直接引用对方的 store.goservice.go 内部实现,模块间通信应通过对方暴露的 Service 接口进行。

4. 基础设施设计

4.1 配置管理(config)

type Config struct {
    Port                string
    DatabaseURL         string
    JWTSecret           string
    JWTAccessTTLMinutes int
    JWTRefreshTTLHours  int
    Env                 string
}
  • 开发环境通过 .env 文件加载。
  • 生产环境通过环境变量注入,.env 文件被忽略。
  • 启动时校验必填项,缺少则直接 panic。

4.2 数据库连接池(platform/db/db.go)

  • 使用 pgxpool.New(ctx, databaseURL) 创建连接池。
  • 提供 WithTx(ctx, fn) 事务封装方法。
  • 提供 HealthCheck(ctx) 方法,用于 GET /health 验证数据库连通性。
func (db *DB) WithTx(ctx context.Context, fn func(pgx.Tx) error) error {
    tx, err := db.pool.Begin(ctx)
    if err != nil { return err }
    defer tx.Rollback(ctx)

    if err := fn(tx); err != nil {
        return err
    }
    return tx.Commit(ctx)
}

4.3 日志系统

  • 使用 log/slog 的 JSON handler。
  • 每个请求通过 logger 中间件生成 trace_id
  • trace_id 注入 context.Context

5. 数据层设计

5.1 users

CREATE TABLE users (
    id BIGSERIAL PRIMARY KEY,
    email VARCHAR(100) UNIQUE NOT NULL,
    password_hash VARCHAR(255) NOT NULL,
    role VARCHAR(20) NOT NULL DEFAULT 'user' CHECK (role IN ('user', 'admin')),
    created_at TIMESTAMPTZ DEFAULT NOW(),
    updated_at TIMESTAMPTZ DEFAULT NOW()
);

5.2 Refresh Token 存储(Redis)

Refresh Token 不存 PostgreSQL,完全由 Redis 管理,利用原生 TTL 实现自动过期和滑动窗口。

Key 设计

refresh:{token_hash} -> "{user_id}:{platform}"
  • token_hash:随机 32 字节字符串的 SHA-256 摘要(不直接暴露原始 token)
  • Value:{user_id}:{platform},如 "123:ios"
  • TTL:7 * 24 * 3600 秒(7 天)

辅助索引(可选,用于查看登录设备)

user:{user_id}:devices -> set of "{platform}"
  • 登录/刷新时写入对应 platform
  • 登出时从 Set 中移除该 platform
  • 如需"一键踢下线",可遍历该 Set 删除所有相关 refresh key

为什么不用 PG

  • Refresh Token 是短期会话状态,不是核心业务数据,丢失后让用户重新登录即可
  • Redis TTL 天然支持滑动窗口和自动清理,无需定时任务
  • 高并发读写场景下性能更优,架构更简单

6. 认证与权限设计

6.1 JWT 双 Token 模式

Token有效期用途存储
Access Token15 分钟接口鉴权Authorization: Bearer <token>
Refresh Token7 天(滑动窗口)换取新 Access TokenResponse Body + Redis(Key: refresh:{token_hash}

Token Payload

{
  "sub": "123",
  "role": "user",
  "platform": "ios",
  "typ": "access",
  "iat": 1713331200,
  "exp": 1713332100
}
  • sub: 用户 ID
  • role: 用户角色
  • platform: 登录设备类型(web / ios / android / wxmini / app)
  • typ: token 类型(access / refresh)

多设备会话管理

同一用户在不同设备登录时,生成绑定设备的独立 Refresh Token。

  • 用户在 iPhone 登录 → Redis 写入 refresh:abc123 → "123:ios",TTL 7 天
  • 用户在 Web 登录 → Redis 写入 refresh:def456 → "123:web",TTL 7 天
  • 两个设备的 token 互相独立,登出/刷新不影响另一设备

Refresh 流程

  1. POST /api/v1/auth/refresh 携带 Refresh Token
  2. 校验签名和有效期,从 payload 读取 platform
  3. 用 token 原文计算 token_hash,查 Redis refresh:{token_hash}
  4. 确认 key 存在且 value 中的 user_id + platform 与 payload 一致
  5. 生成新的双 Token(保持 platform 不变),向 Redis 写入新的 refresh:{new_hash},TTL 重新设为 7 天
  6. 删除旧的 Redis key(轮换 + 滑动窗口机制)

客户端刷新策略

  • Access Token 有效期 15 分钟:正常请求只需携带 Access Token,无需每次调用 refresh
  • 推荐主动刷新:在 Access Token 过期前 1~2 分钟调用 /api/v1/auth/refresh
  • 被动刷新兜底:若请求返回 401,客户端用本地保存的 Refresh Token 换取新双 Token,然后重试原请求
  • 持续活跃用户:大约每 15 分钟触发一次 refresh,Refresh Token 的 Redis TTL 随之滑动续期

滑动窗口说明

  • 每次刷新时,该设备的 Refresh Token 被替换为一个全新 7 天有效期的 Redis key
  • 不同 platform 的 Refresh Token 完全独立,互不影响
  • 如果某设备(如 web)连续 7 天未发起刷新,其 Redis key 因 TTL 到期自动消失,需重新登录
  • 活跃设备(如 ios)通过持续使用不断刷新,Redis TTL 不断续期,会话可长期保持登录
  • 登出时仅删除当前设备的 Redis key,其他设备会话不受影响

6.2 Context 注入

JWT 中间件校验后注入:

  • user_id (int64)
  • role (string)
  • platform (string)
type contextKey string
const ContextUserIDKey contextKey = "user_id"
const ContextRoleKey contextKey = "role"
const ContextPlatformKey contextKey = "platform"

辅助函数:

  • UserFromContext(ctx) (userID int64, role string, ok bool)
  • PlatformFromContext(ctx) string

6.3 RBAC 中间件

func RequireRole(roles ...string) func(http.Handler) http.Handler

用法:

r.Get("/api/v1/public", publicHandler)                          // 公开
r.Get("/api/v1/me", jwtMiddleware(authHandler.Me))              // 登录即可
r.Post("/api/v1/admin/data", jwtMiddleware(RequireRole("admin")(adminHandler)))

6.4 Platform 识别

Platform 信息有两个来源,优先级如下:

  1. JWT payload 中的 platform 字段(已登录用户,最可信)
  2. 请求头 X-Platform(未登录请求或公开接口)

JWT 中间件会先尝试从 token 读取 platform 注入 context;如果请求没有 token(如登录前),则由 Platform 中间件从 X-Platform 头部读取并注入。

支持的取值:

  • web — 默认
  • ios
  • android
  • app(通用 App,如果不需要区分 iOS/Android)
  • wxmini — 微信小程序

获取 platform 的辅助函数:

platform := middleware.PlatformFromContext(r.Context())

挂载方式:

r.Use(middleware.Recover)
r.Use(middleware.Logger)
r.Use(middleware.CORS)
r.Use(middleware.Platform)  // 从未认证请求读取 X-Platform

7. API 设计规范

7.1 统一响应格式

{
  "code": 0,
  "data": {},
  "message": ""
}
  • code = 0 成功
  • code > 0 业务错误
  • HTTP Status Code 照常返回

7.2 错误码定义

const (
    ErrCodeUnknown      = 0
    ErrCodeBadRequest   = 1001
    ErrCodeUnauthorized = 1002
    ErrCodeForbidden    = 1003
    ErrCodeNotFound     = 1004
    ErrCodeConflict     = 1005
    ErrCodeInternal     = 5000
)

7.3 BaseHandler 工具方法

func (h *BaseHandler) RespondJSON(w http.ResponseWriter, status int, data any)
func (h *BaseHandler) RespondError(w http.ResponseWriter, status int, code int, message string)

7.4 路由结构

GET    /health                     -> 健康检查(含数据库连通性检测)
POST   /api/v1/auth/register       -> 注册(email + password)
POST   /api/v1/auth/login          -> 登录(email + password)
POST   /api/v1/auth/refresh        -> 刷新 Access Token
GET    /api/v1/me                  -> 获取当前登录用户信息(需 JWT)

7.5 注册/登录接口设计

POST /api/v1/auth/register

Request:

{
  "email": "user@example.com",
  "password": "123456"
}

Response:

{
  "code": 0,
  "data": {
    "user": {
      "id": 1,
      "email": "user@example.com",
      "role": "user",
      "created_at": "2024-01-01T00:00:00Z"
    }
  },
  "message": "ok"
}

POST /api/v1/auth/login

请求需携带 X-Platform 头部(如 X-Platform: ios),标识登录设备。

Request:

{
  "email": "user@example.com",
  "password": "123456"
}

Response:

{
  "code": 0,
  "data": {
    "access_token": "...",
    "refresh_token": "...",
    "expires_in": 900
  },
  "message": "ok"
}

GET /api/v1/me

Response:

{
  "code": 0,
  "data": {
    "id": 1,
    "email": "user@example.com",
    "role": "user",
    "created_at": "2024-01-01T00:00:00Z"
  },
  "message": "ok"
}

8. Swagger / OpenAPI 接口文档

8.1 方案选型

  • 生成工具github.com/swaggo/swag CLI
  • UI 服务github.com/swaggo/http-swagger 中间件挂载
  • 访问地址http://localhost:8080/swagger/index.html

8.2 目录与生成

  • docs/ 目录由 swag init -g cmd/api/main.go 自动生成,包含 docs.goswagger.jsonswagger.yaml
  • docs/ 应提交到版本控制,方便不安装 swag CLI 的开发者直接运行项目
  • Makefile 提供 make swagger 命令,开发期间每次修改接口后手动执行即可

8.3 注释规范

8.3.1 通用 API 信息(写在 cmd/api/main.go

// @title           Admission API
// @version         1.0
// @description     志愿报考分析平台后端 API
// @host            localhost:8080
// @BasePath        /
// @schemes         http

8.3.2 Request / Response 结构体定义

为 Swagger 和 API 契约定义专用结构体,不直接使用 domain model

// RegisterRequest 注册请求
type RegisterRequest struct {
    Email    string `json:"email" example:"user@example.com"`
    Password string `json:"password" example:"123456"`
}

// LoginRequest 登录请求
type LoginRequest struct {
    Email    string `json:"email" example:"user@example.com"`
    Password string `json:"password" example:"123456"`
}

// UserResponse 用户信息响应体
type UserResponse struct {
    ID        int64     `json:"id" example:"1"`
    Email     string    `json:"email" example:"user@example.com"`
    Role      string    `json:"role" example:"user"`
    CreatedAt time.Time `json:"created_at" example:"2024-01-01T00:00:00Z"`
}

// TokenResponse 登录成功后的 token 响应体
type TokenResponse struct {
    AccessToken  string `json:"access_token" example:"eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9..."`
    RefreshToken string `json:"refresh_token" example:"abc123..."`
    ExpiresIn    int    `json:"expires_in" example:"900"`
}

8.3.3 统一响应格式与 Swagger 的兼容写法

由于 web.Responsedata 字段类型是 any,如果直接写 {object} web.Response,Swagger UI 会把 data 渲染成空对象 {}必须显式指定 data 字段的具体类型

swaggo 使用响应包装语法

// @Success 200 {object} web.Response{data=UserResponse}
// @Failure 400 {object} web.Response
  • 成功响应:必须写成 web.Response{data=XXXResponse},这样 Swagger UI 会把 data 展开成具体的结构体
  • 错误响应:可以裸写 web.Response(因为错误时 data 通常为 nil 或空对象)

接口注释示例(写在 handler 方法上方):

// Register godoc
// @Summary      用户注册
// @Description  使用邮箱和密码注册新账户
// @Tags         auth
// @Accept       json
// @Produce      json
// @Param        body  body      RegisterRequest  true  "注册信息"
// @Success      200   {object}  web.Response{data=UserResponse}
// @Failure      400   {object}  web.Response
// @Failure      409   {object}  web.Response
// @Router       /api/v1/auth/register [post]
func (h *UserHandler) Register(w http.ResponseWriter, r *http.Request) { ... }

带认证的接口注释

// Me godoc
// @Summary      获取当前用户信息
// @Description  返回当前登录用户的个人信息
// @Tags         user
// @Accept       json
// @Produce      json
// @Security     BearerAuth
// @Success      200  {object}  web.Response{data=UserResponse}
// @Failure      401  {object}  web.Response
// @Router       /api/v1/me [get]
func (h *UserHandler) Me(w http.ResponseWriter, r *http.Request) { ... }

通用安全声明(写在 cmd/api/main.go 或全局 swagger 注释中):

// @securityDefinitions.apikey BearerAuth
// @in header
// @name Authorization
// @description "Bearer {token}"

8.4 新增接口时的标准动作

  1. 在 handler 方法上添加 // @Summary ... 等注释
  2. 为 Request/Response body 定义专用结构体(避免直接用 domain model)
  3. 运行 make swagger 重新生成文档
  4. 浏览器打开 /swagger/index.html 确认接口已正确显示
  5. docs/ 的变更一并提交

8.5 模板内置覆盖范围

  • GET /health
  • POST /api/v1/auth/register
  • POST /api/v1/auth/login
  • POST /api/v1/auth/refresh
  • GET /api/v1/me

9. 中间件清单

中间件顺序职责
Recover最外层捕获 panic,返回 500,记录错误
Logger第二层记录请求方法、路径、耗时、状态码、Trace ID
CORS第三层允许前端本地开发跨域
Platform第四层读取 X-Platform 头部,注入平台标识到 context
JWT可选挂载校验 Access Token,注入 user_id/role 到 context
RBAC最内层校验角色权限,不匹配返回 403

10. 测试策略

  • API 测试示例tests/integration/health_test.go
  • 后续扩展
    • 每个新增 handler 配套 *_test.go
    • store 层测试建议用真实 PG

11. 部署与开发流程

cd admission-api
cp .env.example .env
make dev          # 启动 PG + Redis
make migrate-up   # 执行迁移
make run          # 启动后端

Makefile

run          -> go run ./cmd/api
dev          -> docker-compose up -d
db           -> docker-compose up -d db redis
migrate-up   -> go run ./cmd/api -migrate up
migrate-down -> go run ./cmd/api -migrate down
test         -> go test ./...
tidy         -> go mod tidy
swagger      -> swag init -g cmd/api/main.go

12. 未来扩展路线图

阶段动作当前预留
补充志愿业务表migration/ 添加 002_schools.up.sql目录已预留
添加志愿业务接口handler/service/store/ 按模式新增三层结构清晰
推荐算法新增 recommend_service.go模块化单体直接加模块
小程序/App复用 /api/v1/ 接口API 平台无关
数据量大admission_scoresyear 分区可后续修改表结构
高并发Redis 缓存、PG Read ReplicaRedis 已在 docker-compose
微服务拆分recommend 模块为独立服务边界清晰,低成本的拆分

13. 确认清单

当前决策:

  • 模板包含用户模块(users 表 + 登录/注册/获取个人信息接口)
  • 保留 pgx 连接池 + 事务封装作为基础设施
  • 保留 golang-migrate 工具链
  • 保留完整的 JWT + RBAC 中间件框架
  • JWT 携带 platform,支持多设备独立会话
  • Refresh Token 存 Redis,支持滑动窗口
  • 保留 Platform 平台识别中间件
  • 两角色:user / admin
  • 无 PG RLS
  • chi + slog + pgx + swaggo