未命名文章
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(模板不包含,后续按需添加)
- 志愿报考相关业务表(
schools、scores、majors等) - 志愿报考相关业务接口
- 消息队列(NATS/Redis Streams)
- 复杂 RBAC(组织/部门/多级权限)
- 文件上传/OSS 接口
- 推荐算法引擎
- K8s 部署配置
2. 技术栈
| 层级 | 技术 | 理由 |
|---|---|---|
| 语言 | Go 1.22+ | 并发性能好,编译快,部署单二进制 |
| 路由 | chi (go-chi/chi/v5) | 标准库兼容、中间件链清晰 |
| 数据库驱动 | pgx (jackc/pgx/v5) | 性能优,支持批量插入 |
| 连接池 | pgxpool | 内置健康检查 |
| 迁移 | golang-migrate | industry 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):每个模块一个目录,包含该模块相关的
model、store、service、handler。 internal/platform/存放跨模块共享的基础设施:配置、数据库连接、中间件、统一响应格式。migration/包含users表的迁移,确保登录/注册功能可直接运行。refresh_tokens不存 PG,改存 Redis。- 未来新增业务模块时,直接在
internal/下新建目录,如internal/school/、internal/recommend/、internal/score/,按user/的模式复制即可。 - 禁止模块间直接引用对方的
store.go或service.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 Token | 15 分钟 | 接口鉴权 | Authorization: Bearer <token> |
| Refresh Token | 7 天(滑动窗口) | 换取新 Access Token | Response Body + Redis(Key: refresh:{token_hash}) |
Token Payload
{
"sub": "123",
"role": "user",
"platform": "ios",
"typ": "access",
"iat": 1713331200,
"exp": 1713332100
}
sub: 用户 IDrole: 用户角色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 流程
POST /api/v1/auth/refresh携带 Refresh Token- 校验签名和有效期,从 payload 读取
platform - 用 token 原文计算
token_hash,查 Redisrefresh:{token_hash} - 确认 key 存在且 value 中的
user_id + platform与 payload 一致 - 生成新的双 Token(保持
platform不变),向 Redis 写入新的refresh:{new_hash},TTL 重新设为 7 天 - 删除旧的 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 信息有两个来源,优先级如下:
- JWT payload 中的
platform字段(已登录用户,最可信) - 请求头
X-Platform(未登录请求或公开接口)
JWT 中间件会先尝试从 token 读取 platform 注入 context;如果请求没有 token(如登录前),则由 Platform 中间件从 X-Platform 头部读取并注入。
支持的取值:
web— 默认iosandroidapp(通用 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/swagCLI - UI 服务:
github.com/swaggo/http-swagger中间件挂载 - 访问地址:
http://localhost:8080/swagger/index.html
8.2 目录与生成
docs/目录由swag init -g cmd/api/main.go自动生成,包含docs.go、swagger.json、swagger.yamldocs/应提交到版本控制,方便不安装swagCLI 的开发者直接运行项目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.Response 的 data 字段类型是 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 新增接口时的标准动作
- 在 handler 方法上添加
// @Summary ...等注释 - 为 Request/Response body 定义专用结构体(避免直接用 domain model)
- 运行
make swagger重新生成文档 - 浏览器打开
/swagger/index.html确认接口已正确显示 - 将
docs/的变更一并提交
8.5 模板内置覆盖范围
GET /healthPOST /api/v1/auth/registerPOST /api/v1/auth/loginPOST /api/v1/auth/refreshGET /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
- 每个新增 handler 配套
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_scores 按 year 分区 | 可后续修改表结构 |
| 高并发 | Redis 缓存、PG Read Replica | Redis 已在 docker-compose |
| 微服务拆分 | 拆 recommend 模块为独立服务 | 边界清晰,低成本的拆分 |
13. 确认清单
当前决策:
- 模板包含用户模块(users 表 + 登录/注册/获取个人信息接口)
- 保留 pgx 连接池 + 事务封装作为基础设施
- 保留 golang-migrate 工具链
- 保留完整的 JWT + RBAC 中间件框架
- JWT 携带 platform,支持多设备独立会话
- Refresh Token 存 Redis,支持滑动窗口
- 保留 Platform 平台识别中间件
- 两角色:
user/admin - 无 PG RLS
- chi + slog + pgx + swaggo