下载工作台
Gin Web 开发

毕业项目 api-go-demo

试读上半部分 · 解锁后可读全文

第 16 章 · 毕业项目 api-go-demo MVP

本章目标:在 ch01~ch15 基础上,独立交付 api-go-demo 完整 MVP;逐文件 walkthrough 理解每一层职责;覆盖商品 REST CRUD(slug + is_published,价格分)JWT + RBACRedis 缓存/metrics + /healthDocker 镜像Swagger 文档;按 5 天计划实施;通过 100 分验收表自评;完成 curl 冒烟与答辩演示。深度对标 fastapi-web ch12/ch19

学时建议:5 天 × 6~8 小时(合计 30~40 小时)

前置:完成 gin-web ch01~ch15go-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 矩阵、库存扣减事务
字段slugis_publishedprice 分sku / is_active 旧字段
认证注册、登录、Bearer JWTOAuth2 第三方
缓存slug 详情 Cache-Aside全表缓存
异步Redis Stream workerKafka(paas ch17)
运维Dockerfile、compose、health/metrics全量 K8s(见 paas)
严禁将真实公司域名、数据库密码、JWT 密钥写入仓库。统一使用 api.example.comexample.com/api-go-demo

16.2 技术栈清单

层级技术对应章节
框架Ginch01~ch02
配置Viper / envch03
ORMGORM + MySQLch04
分层Handler/Service/Repoch05
认证jwt/v5 + bcryptch06
响应Envelope + validatorch07
日志zapch08
缓存go-redisch09
异步Redis Stream workerch10
文档swaggoch11
性能连接池、pprofch12
指标Prometheusch13
测试testify + httptestch14
部署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/metricsPrometheus(内网)
GET/swagger/index.htmldev 环境
POST/auth/register注册
POST/auth/login返回 access_token
GET/auth/meBearer当前用户
GET/products仅 is_published=true
GET/products/:slug已发布详情
POST/productsadmin创建
PATCH/products/:slugadmin部分更新

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:DatabaseURLRedisAddrJWTSecretProductTTL。生产密钥仅来自环境变量。

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、Roleuser / admin)。Seed 默认 admin@example.com / admin12345

16.5.5 internal/repository/product.go

封装 GORM:ListPublishedGetBySlug(slug, publishedOnly)CreateUpdateBySlug。所有查询带 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。

以下内容需解锁后阅读

试读已结束。解锁本章 ¥5.00,或开通年度会员畅读全部教程。
年度会员 ¥199.00/年; 小紫 AI 工作台有效会员 ¥99.00/年

正文仅在服务端鉴权后下发,未付费无法获取下半部分内容。