第 16 章 · 毕业项目 api-go-demo MVP
本章目标:在 ch01~ch15 基础上,独立交付 api-go-demo 完整 MVP;逐文件 walkthrough 理解每一层职责;覆盖商品 REST CRUD(slug + is_published,价格分)、JWT + RBAC、Redis 缓存、/metrics + /health、Docker 镜像与 Swagger 文档;按 5 天计划实施;通过 100 分验收表自评;完成 curl 冒烟与答辩演示。深度对标 fastapi-web ch12/ch19。
学时建议:5 天 × 6~8 小时(合计 30~40 小时)
前置:完成 gin-web ch01~ch15;go-dev ch01~ch15;建议复习 fastapi-web ch12 svc-demo MVP。
16.1 项目背景与边界
api-go-demo 是虚构的「高 QPS 商品 API 服务」,为 user-demo SPA 与运营工具提供 JSON 接口。部署域名为 https://api.example.com(本地等价 http://127.0.0.1:8080)。
┌────────────────────────────────────────────────────────────┐
│ api-go-demo MVP(本章) │
├──────────┬──────────┬──────────┬──────────┬─────────────────┤
│ 商品 API │ JWT RBAC │ Redis │ 可观测 │ Docker + 文档 │
│ CRUD │ admin写 │ 详情缓存 │ metrics │ swagger │
└──────────┴──────────┴──────────┴──────────┴─────────────────┘
│
▼
MySQL 8 + Redis 7
| 模块 | MVP 必须 | 不做(加分扩展) |
|---|---|---|
| 商品 | 列表、slug 详情、创建/更新、上下架 | SKU 矩阵、库存扣减事务 |
| 字段 | slug、is_published、price 分 | sku / is_active 旧字段 |
| 认证 | 注册、登录、Bearer JWT | OAuth2 第三方 |
| 缓存 | slug 详情 Cache-Aside | 全表缓存 |
| 异步 | Redis Stream worker | Kafka(paas ch17) |
| 运维 | Dockerfile、compose、health/metrics | 全量 K8s(见 paas) |
严禁将真实公司域名、数据库密码、JWT 密钥写入仓库。统一使用 api.example.com、example.com/api-go-demo。
16.2 技术栈清单
| 层级 | 技术 | 对应章节 |
|---|---|---|
| 框架 | Gin | ch01~ch02 |
| 配置 | Viper / env | ch03 |
| ORM | GORM + MySQL | ch04 |
| 分层 | Handler/Service/Repo | ch05 |
| 认证 | jwt/v5 + bcrypt | ch06 |
| 响应 | Envelope + validator | ch07 |
| 日志 | zap | ch08 |
| 缓存 | go-redis | ch09 |
| 异步 | Redis Stream worker | ch10 |
| 文档 | swaggo | ch11 |
| 性能 | 连接池、pprof | ch12 |
| 指标 | Prometheus | ch13 |
| 测试 | testify + httptest | ch14 |
| 部署 | Docker 多阶段 | ch15 |
cd ~/learn-go/api-go-demo
go mod init example.com/api-go-demo
docker compose up --build
16.3 推荐目录结构
api-go-demo/
├── .env.example
├── .gitignore
├── Dockerfile
├── docker-compose.yml
├── Makefile
├── docs/
│ ├── docs.go # swag 生成
│ ├── swagger.json
│ ├── API.md
│ ├── DEPLOY.md
│ ├── PERF.md
│ └── SELF_REVIEW.md # 100 分自评表
├── scripts/
│ └── smoke.sh
├── cmd/
│ ├── server/main.go # 入口 + 路由 wiring
│ └── worker/main.go # 选修 ch10
├── configs/config.yaml
├── internal/
│ ├── config/config.go
│ ├── database/mysql.go
│ ├── model/product.go
│ ├── model/user.go
│ ├── dto/product.go
│ ├── repository/product.go
│ ├── repository/user.go
│ ├── service/product.go
│ ├── service/auth.go
│ ├── handler/product.go
│ ├── handler/auth.go
│ ├── middleware/request_id.go
│ ├── middleware/access_log.go
│ ├── middleware/auth.go
│ ├── middleware/metrics.go
│ ├── response/envelope.go
│ ├── auth/jwt.go
│ ├── cache/redis.go
│ ├── queue/redis_stream.go
│ ├── event/product.go
│ └── logger/logger.go
└── tests/
└── integration/
16.4 核心 API 契约
基址:/api/v1
| 方法 | 路径 | 认证 | 说明 |
|---|---|---|---|
| GET | /health | 否 | { "status": "ok" } |
| GET | /metrics | 否 | Prometheus(内网) |
| GET | /swagger/index.html | 否 | dev 环境 |
| POST | /auth/register | 否 | 注册 |
| POST | /auth/login | 否 | 返回 access_token |
| GET | /auth/me | Bearer | 当前用户 |
| GET | /products | 否 | 仅 is_published=true |
| GET | /products/:slug | 否 | 已发布详情 |
| POST | /products | admin | 创建 |
| PATCH | /products/:slug | admin | 部分更新 |
Product 响应(价格分):
{
"code": "OK",
"message": "success",
"data": {
"slug": "go-handbook",
"name": "Go 手册",
"price": 6800,
"stock": 50,
"is_published": true
}
}
16.5 逐文件 Walkthrough
16.5.1 cmd/server/main.go — 组装根
职责:读配置 → 连 MySQL/Redis → 构造各层 → 挂中间件 → 注册路由 → 启动 HTTP Server。
func main() {
cfg := config.Load()
log, _ := logger.New(cfg.Env)
defer log.Sync()
db, err := database.OpenMySQL(cfg.DatabaseURL, cfg.Env, database.DefaultPoolConfig())
if err != nil { log.Fatal("mysql", zap.Error(err)) }
if cfg.AutoMigrate {
db.AutoMigrate(&model.User{}, &model.Product{})
seed.Run(db)
}
rdb := cache.NewRedis(cache.RedisConfig{Addr: cfg.RedisAddr})
_ = cache.Ping(context.Background(), rdb)
productRepo := repository.NewProductRepository(db)
userRepo := repository.NewUserRepository(db)
publisher := queue.NewProductPublisher(rdb)
productSvc := service.NewProductService(productRepo, rdb, cfg, log, publisher)
authSvc := service.NewAuthService(userRepo, rdb, cfg, log)
r := gin.New()
r.Use(middleware.RequestID())
r.Use(middleware.Prometheus())
r.Use(middleware.AccessLog(log))
r.Use(middleware.RecoveryWithZap(log))
r.GET("/health", healthHandler)
r.GET("/metrics", gin.WrapH(promhttp.Handler()))
if cfg.Env != "prod" {
r.GET("/swagger/*any", ginSwagger.WrapHandler(swaggerFiles.Handler))
}
v1 := r.Group("/api/v1")
authH := handler.NewAuthHandler(authSvc)
v1.POST("/auth/register", authH.Register)
v1.POST("/auth/login", authH.Login)
v1.GET("/auth/me", middleware.RequireAuth(cfg.JWTSecret), authH.Me)
productH := handler.NewProductHandler(productSvc)
v1.GET("/products", productH.ListProducts)
v1.GET("/products/:slug", productH.GetProduct)
admin := v1.Group("", middleware.RequireAuth(cfg.JWTSecret), middleware.RequireRole("admin"))
admin.POST("/products", productH.CreateProduct)
admin.PATCH("/products/:slug", productH.UpdateProduct)
runHTTPServer(cfg, r)
}
检查点:中间件顺序 RequestID → Metrics → AccessLog;Listen :8080。
16.5.2 internal/config/config.go
Viper 读 env + yaml:DatabaseURL、RedisAddr、JWTSecret、ProductTTL。生产密钥仅来自环境变量。
16.5.3 internal/model/product.go
type Product struct {
ID uint `json:"id" gorm:"primaryKey"`
Slug string `json:"slug" gorm:"size:64;uniqueIndex"`
Name string `json:"name" gorm:"size:200"`
Price int64 `json:"price"` // 分
Stock int `json:"stock"`
IsPublished bool `json:"is_published" gorm:"index"`
Description string `json:"description" gorm:"type:text"`
CreatedAt time.Time `json:"created_at"`
UpdatedAt time.Time `json:"updated_at"`
}
全站与 django-web / fastapi-web 对齐:不用 float 元。
16.5.4 internal/model/user.go
Email unique、PasswordHash bcrypt、Role(user / admin)。Seed 默认 admin@example.com / admin12345。
16.5.5 internal/repository/product.go
封装 GORM:ListPublished、GetBySlug(slug, publishedOnly)、Create、UpdateBySlug。所有查询带 WithContext(ctx)。
16.5.6 internal/service/product.go
业务规则:重复 slug、未发布 404、price>0;Update 后 invalidate cache;上架发 Stream(ch09~ch10)。
16.5.7 internal/service/product_cache.go
GetPublishedCached:Cache-Aside + nil 占位 + TTL jitter。