Skip to content

后端架构

设计目标

零外部依赖、单二进制、生产可用、性能足够

  • 不依赖 Docker、Redis、Postgres
  • go build 后 ≤ 30MB 单文件
  • 100 并发 P95 < 50ms
  • 6300 行代码总规模

技术栈

组件选型版本
语言Go1.25
HTTPnet/http 标准库-
数据库SQLite3.x
SQLite 驱动modernc.org/sqlite纯 Go,无 CGo
路由http.ServeMux(Go 1.22+ 增强版)-
密码哈希golang.org/x/crypto/bcrypt-

目录结构

text
backend/
├── main.go              # 入口、路由注册
├── go.mod
├── go.sum
├── auth.go              # 认证、Session、Cookie
├── quota.go             # 三类配额管理
├── handlers.go          # API Handler
├── providers.go         # 多供应商路由
├── graphrag.go          # GraphRAG 引擎
├── db.go                # SQLite 连接 + 迁移
├── reaper.go            # 配额回滚 goroutine
├── data/
│   └── app.db           # 自动生成的 SQLite 文件
└── migrations/
    ├── 0001_init_users.sql
    ├── 0002_sessions.sql
    ├── 0003_quotas.sql
    ├── 0004_providers.sql
    ├── 0005_model_configs.sql
    ├── 0006_activation_keys.sql
    ├── 0007_request_logs.sql
    ├── 0008_kg_entities.sql
    ├── 0009_kg_relations.sql
    ├── 0010_indexes.sql
    ├── 0011_constraints.sql
    └── 0012_seed.sql

启动流程

go
// main.go
func main() {
  // 1. 打开数据库
  db := openDatabase("./data/app.db")
  defer db.Close()

  // 2. 自动执行所有迁移
  runMigrations(db, "./migrations")

  // 3. 启动配额回滚 goroutine
  go startReaper(db, 30*time.Second)

  // 4. 注册路由
  mux := http.NewServeMux()
  registerRoutes(mux, db)

  // 5. 静态文件托管(生产模式)
  mux.Handle("/", http.FileServer(http.Dir("../frontend/dist")))

  // 6. 启动 HTTP 服务
  log.Println("Starting on :8788")
  log.Fatal(http.ListenAndServe(":8788", mux))
}

路由表

go
POST   /api/auth/login           // 普通用户登录
POST   /api/admin/auth/login     // 管理员登录
POST   /api/auth/logout
GET    /api/me
go
POST   /api/generate/image/prepare      // 配额预扣
POST   /api/generate/image/complete     // 扣次/回滚
POST   /api/generate/copy               // Listing 文案
POST   /api/generate/prompt             // 提示词智能生成
POST   /api/generate/product-analysis   // 选品对话
POST   /api/generate/video              // 视频生成
GET    /api/generate/image/providers    // 获取可用供应商
go
GET    /api/admin/users
POST   /api/admin/users
PATCH  /api/admin/users/:id
GET    /api/admin/providers
POST   /api/admin/providers
PATCH  /api/admin/providers/:id
GET    /api/admin/model-configs
PUT    /api/admin/model-configs
POST   /api/admin/model-configs
GET    /api/admin/activation-keys
POST   /api/admin/activation-keys
PATCH  /api/admin/activation-keys/:id
POST   /api/activation/redeem

中间件

go
// 中间件链:日志 → CORS → 认证 → 业务
func chain(handler http.HandlerFunc, middlewares ...Middleware) http.HandlerFunc {
  for i := len(middlewares) - 1; i >= 0; i-- {
    handler = middlewares[i](handler)
  }
  return handler
}

// 注册示例
mux.HandleFunc("/api/me", chain(
  handleMe,
  loggingMiddleware,
  corsMiddleware,
  authMiddleware,
))

mux.HandleFunc("/api/admin/users", chain(
  handleAdminUsers,
  loggingMiddleware,
  corsMiddleware,
  authMiddleware,
  requireAdminMiddleware,  // 只有 admin 才能进入
))

认证与 Session

go
http.SetCookie(w, &http.Cookie{
  Name:     "hyzc_session",
  Value:    plaintextToken,         // 仅在浏览器
  HttpOnly: true,                   // JS 无法读取
  Secure:   true,                   // HTTPS Only(生产)
  SameSite: http.SameSiteStrictMode, // 防 CSRF
  Path:     "/",
  MaxAge:   7 * 24 * 3600,          // 7 天
})

// 数据库只存 hash
hash := sha256.Sum256([]byte(plaintextToken))
db.Exec("INSERT INTO sessions(token_hash, user_id) VALUES(?, ?)",
        hex.EncodeToString(hash[:]), userID)

安全设计

  • 明文 token 仅存浏览器 Cookie,服务端只存 SHA-256 hash
  • 即使数据库被拖库,攻击者也无法登录现有用户
  • bcrypt 哈希密码(cost=10)

配额管理

三类配额表

sql
CREATE TABLE quotas (
  user_id        TEXT PRIMARY KEY,
  image_total    INTEGER NOT NULL DEFAULT 0,
  image_used     INTEGER NOT NULL DEFAULT 0,
  copy_total     INTEGER NOT NULL DEFAULT 0,
  copy_used      INTEGER NOT NULL DEFAULT 0,
  video_total    INTEGER NOT NULL DEFAULT 0,
  video_used     INTEGER NOT NULL DEFAULT 0
);

CREATE TABLE usage_logs (
  id           TEXT PRIMARY KEY,
  user_id      TEXT NOT NULL,
  quota_type   TEXT NOT NULL,        -- image / copy / video
  amount       INTEGER NOT NULL,     -- 扣减数量
  status       TEXT NOT NULL,        -- pending / success / failed
  created_at   TEXT NOT NULL,
  completed_at TEXT
);

两段式协议

图片生成调用流程(浏览器直连供应商 + 配额预扣与回滚)

关键交互时序图:

图片生成关键交互时序图

go
// Phase 1: 预扣
func handlePrepareImage(w http.ResponseWriter, r *http.Request) {
  user := getUser(r)

  tx, _ := db.Begin()
  defer tx.Rollback()

  // 检查并预扣
  res, _ := tx.Exec(`
    UPDATE quotas
    SET image_used = image_used + 1
    WHERE user_id = ? AND image_used < image_total
  `, user.ID)

  affected, _ := res.RowsAffected()
  if affected == 0 {
    http.Error(w, "Quota exhausted", 429)
    return
  }

  // 写 pending 日志
  ticket := uuid.New().String()
  tx.Exec(`
    INSERT INTO usage_logs(id, user_id, quota_type, amount, status, created_at)
    VALUES(?, ?, 'image', 1, 'pending', ?)
  `, ticket, user.ID, time.Now())

  tx.Commit()

  // 返回供应商配置 + ticket
  json.NewEncoder(w).Encode(map[string]interface{}{
    "ticket":   ticket,
    "provider": chooseProvider("image"),
  })
}

// Phase 2: 完成
func handleCompleteImage(w http.ResponseWriter, r *http.Request) {
  var req struct {
    Ticket string `json:"ticket"`
    Status string `json:"status"`  // success / failed
  }
  json.NewDecoder(r.Body).Decode(&req)

  tx, _ := db.Begin()
  defer tx.Rollback()

  if req.Status == "failed" {
    // 回滚配额
    tx.Exec(`
      UPDATE quotas
      SET image_used = image_used - 1
      WHERE user_id = (SELECT user_id FROM usage_logs WHERE id = ?)
    `, req.Ticket)
  }

  // 更新日志状态
  tx.Exec(`
    UPDATE usage_logs
    SET status = ?, completed_at = ?
    WHERE id = ?
  `, req.Status, time.Now(), req.Ticket)

  tx.Commit()

  w.WriteHeader(204)
}

Reaper Goroutine

go
// reaper.go
func startReaper(db *sql.DB, interval time.Duration) {
  ticker := time.NewTicker(interval)
  defer ticker.Stop()

  for range ticker.C {
    sweepStalePending(db)
  }
}

func sweepStalePending(db *sql.DB) {
  tx, _ := db.Begin()
  defer tx.Rollback()

  // 找到所有 pending 超过 30 秒的日志
  rows, _ := tx.Query(`
    SELECT id, user_id, quota_type, amount FROM usage_logs
    WHERE status = 'pending'
      AND created_at < datetime('now', '-30 seconds')
  `)

  type stale struct{ ID, UserID, QType string; Amount int }
  var stales []stale
  for rows.Next() {
    var s stale
    rows.Scan(&s.ID, &s.UserID, &s.QType, &s.Amount)
    stales = append(stales, s)
  }
  rows.Close()

  for _, s := range stales {
    // 标记 failed
    tx.Exec(`UPDATE usage_logs SET status = 'failed' WHERE id = ?`, s.ID)
    // 回滚配额
    col := s.QType + "_used"
    tx.Exec(`UPDATE quotas SET ` + col + ` = ` + col + ` - ? WHERE user_id = ?`,
            s.Amount, s.UserID)
  }

  tx.Commit()
  log.Printf("Reaper swept %d stale entries", len(stales))
}

多供应商路由

go
// providers.go
type Provider struct {
  ID       string
  Type     string
  BaseURL  string
  APIKey   string
  Priority int
  Weight   int
  Status   string
}

func chooseProvider(category string) *Provider {
  // 1. 取所有 active 的供应商
  providers := loadActiveProviders(category)

  // 2. 按 priority 升序分组
  sort.Slice(providers, func(i, j int) bool {
    return providers[i].Priority < providers[j].Priority
  })

  // 3. 取最小 priority 组
  minPri := providers[0].Priority
  var topGroup []*Provider
  for _, p := range providers {
    if p.Priority == minPri {
      topGroup = append(topGroup, p)
    } else {
      break
    }
  }

  // 4. 在该组内按 weight 加权随机
  totalWeight := 0
  for _, p := range topGroup { totalWeight += p.Weight }

  r := rand.Intn(totalWeight)
  cum := 0
  for _, p := range topGroup {
    cum += p.Weight
    if r < cum { return p }
  }

  return topGroup[0]  // fallback
}

数据库迁移

go
// db.go
func runMigrations(db *sql.DB, dir string) {
  files, _ := filepath.Glob(filepath.Join(dir, "*.sql"))
  sort.Strings(files)

  // 创建迁移记录表
  db.Exec(`CREATE TABLE IF NOT EXISTS migrations (
    id TEXT PRIMARY KEY,
    applied_at TEXT NOT NULL
  )`)

  for _, file := range files {
    name := filepath.Base(file)

    // 检查是否已应用
    var count int
    db.QueryRow("SELECT COUNT(*) FROM migrations WHERE id = ?", name).Scan(&count)
    if count > 0 { continue }

    // 执行 SQL
    sql, _ := os.ReadFile(file)
    if _, err := db.Exec(string(sql)); err != nil {
      log.Fatalf("Migration %s failed: %v", name, err)
    }

    // 记录已应用
    db.Exec("INSERT INTO migrations(id, applied_at) VALUES(?, ?)", name, time.Now())
    log.Printf("Applied migration: %s", name)
  }
}

性能与可扩展性

指标当前扩展方案
单进程并发100+Go runtime 自动调度 goroutine
数据库锁SQLite WAL读写并发良好
横向扩展单机切到 Postgres + Redis 后可水平扩展
文件存储本地磁盘切到 S3 / OSS 后可分布式

测试

bash
# 后端测试
npm run test:backend

# 单独运行某个测试文件
cd backend && go test -v ./auth_test.go ./auth.go

测试覆盖范围:

  • ✅ 认证流程(登录、Cookie、SessionExpiry)
  • ✅ 配额事务(预扣、回滚、并发安全)
  • ✅ Reaper goroutine
  • ✅ 激活码兑换(防重复、过期、上限)
  • ✅ SQL 注入防护
  • ✅ GraphRAG 实体抽取(基于 Mock LLM)

下一步

基于 MIT 协议开源 · 中国大学生计算机设计大赛软件应用与开发类作品