辣评前后端服务统一与优化(二十二)

 

服务统一背景

在项目初期,前端和后端分别运行在不同的端口上,这给开发和部署带来了一些不便。本次优化将前后端服务统一到同一端口,简化了部署流程,提升了用户体验。

优化前的架构

前端服务:http://localhost:5173 (Vite Dev Server)
后端服务:http://localhost:8888 (Go API Server)

部署时:
前端:Nginx 静态文件服务 (端口 80)
后端:Go API 服务 (端口 8888)
需要配置 Nginx 反向代理

存在的问题:

  1. 开发时需要启动两个服务
  2. 跨域问题需要额外配置
  3. 部署时需要配置反向代理
  4. 用户访问需要记住两个地址

优化后的架构

统一服务:http://localhost:8888

Go 服务器同时提供:
- API 接口:/api/*
- 静态文件:/* (前端构建产物)

部署时:
单一服务:Go 服务器 (端口 8888)
无需额外配置

优势:

  1. 单一服务,简化部署
  2. 无跨域问题
  3. 统一端口访问
  4. 配置更简单

API 端口统一

Go 服务器配置

// cmd/server/main.go
package main

import (
    "log"
    "net/http"
    "path/filepath"

    "github.com/gin-gonic/gin"
    "github.com/yourusername/laping/cmd/server/config"
    "github.com/yourusername/laping/cmd/server/database"
    "github.com/yourusername/laping/cmd/server/router"
)

func main() {
    // 1. 加载配置
    cfg := config.LoadConfig()

    // 2. 初始化数据库
    if err := database.Initialize(); err != nil {
        log.Fatalf("数据库初始化失败: %v", err)
    }
    defer database.CloseDB()

    // 3. 设置 Gin 模式
    if cfg.Environment == "production" {
        gin.SetMode(gin.ReleaseMode)
    }

    // 4. 创建路由
    r := router.SetupRouter()

    // 5. 配置静态文件服务
    setupStaticFiles(r, cfg)

    // 6. 启动服务器
    addr := fmt.Sprintf(":%d", cfg.APIPort)
    log.Printf("服务器启动在端口 %d", cfg.APIPort)
    log.Printf("访问地址: http://localhost:%d", cfg.APIPort)

    if err := r.Run(addr); err != nil {
        log.Fatalf("服务器启动失败: %v", err)
    }
}

// setupStaticFiles 配置静态文件服务
func setupStaticFiles(r *gin.Engine, cfg *config.Config) {
    // 前端构建产物路径
    frontendPath := cfg.FrontendPath
    if frontendPath == "" {
        frontendPath = "./frontend"
    }

    // 检查前端目录是否存在
    if _, err := os.Stat(frontendPath); os.IsNotExist(err) {
        log.Printf("警告: 前端目录不存在: %s", frontendPath)
        return
    }

    // 静态资源路由(CSS、JS、图片等)
    r.Static("/assets", filepath.Join(frontendPath, "assets"))
    r.StaticFile("/favicon.ico", filepath.Join(frontendPath, "favicon.ico"))

    // SPA 路由处理
    r.NoRoute(func(c *gin.Context) {
        path := c.Request.URL.Path

        // API 路由返回 404
        if strings.HasPrefix(path, "/api/") {
            c.JSON(404, gin.H{
                "code":    404,
                "message": "接口不存在",
            })
            return
        }

        // 其他路由返回 index.html(SPA)
        c.File(filepath.Join(frontendPath, "index.html"))
    })

    log.Printf("静态文件服务已配置: %s", frontendPath)
}

路由配置

// cmd/server/router/router.go
package router

import (
    "github.com/gin-gonic/gin"
    "github.com/yourusername/laping/cmd/server/handlers"
    "github.com/yourusername/laping/cmd/server/middleware"
)

func SetupRouter() *gin.Engine {
    r := gin.Default()

    // 中间件
    r.Use(middleware.CORS())
    r.Use(middleware.Logger())
    r.Use(middleware.Recovery())

    // API 路由组
    api := r.Group("/api")
    {
        // 公开路由
        public := api.Group("")
        {
            public.POST("/auth/login", handlers.Login)
            public.POST("/auth/register", handlers.Register)
            public.POST("/auth/refresh", handlers.RefreshToken)
        }

        // 认证路由
        auth := api.Group("")
        auth.Use(middleware.JWTAuth())
        {
            auth.GET("/user/profile", handlers.GetProfile)
            auth.PUT("/user/profile", handlers.UpdateProfile)

            auth.GET("/submissions", handlers.ListSubmissions)
            auth.POST("/submissions", handlers.CreateSubmission)
            auth.GET("/submissions/:id", handlers.GetSubmission)
            auth.PUT("/submissions/:id", handlers.UpdateSubmission)
            auth.DELETE("/submissions/:id", handlers.DeleteSubmission)

            auth.GET("/comments", handlers.ListComments)
            auth.POST("/comments", handlers.CreateComment)
            auth.PUT("/comments/:id", handlers.UpdateComment)
            auth.DELETE("/comments/:id", handlers.DeleteComment)
        }

        // 管理员路由
        admin := api.Group("/admin")
        admin.Use(middleware.JWTAuth(), middleware.RequireRole("admin"))
        {
            admin.GET("/users", handlers.ListUsers)
            admin.GET("/users/:id", handlers.GetUser)
            admin.PUT("/users/:id", handlers.UpdateUser)
            admin.POST("/users/:id/ban", handlers.BanUser)

            admin.GET("/dashboard/stats", handlers.GetDashboardStats)
            admin.GET("/statistics", handlers.GetStatistics)

            admin.POST("/submissions/proxy", handlers.CreateSubmissionForUser)
            admin.GET("/logs", handlers.GetOperationLogs)
        }
    }

    return r
}

静态文件服务

前端构建配置

// admin-vue/vite.config.js
import { defineConfig } from 'vite'
import vue from '@vitejs/plugin-vue'
import path from 'path'

export default defineConfig({
  plugins: [vue()],

  resolve: {
    alias: {
      '@': path.resolve(__dirname, 'src')
    }
  },

  // 构建配置
  build: {
    outDir: '../frontend', // 输出到项目根目录的 frontend 文件夹
    emptyOutDir: true,

    rollupOptions: {
      output: {
        // 分包策略
        manualChunks: {
          'vue': ['vue', 'vue-router', 'pinia'],
          'element-plus': ['element-plus'],
          'utils': ['axios', 'lodash-es']
        }
      }
    }
  },

  // 开发服务器配置
  server: {
    port: 5173,
    proxy: {
      // 开发时代理 API 请求到后端
      '/api': {
        target: 'http://localhost:8888',
        changeOrigin: true
      }
    }
  }
})

构建脚本

#!/bin/bash
# build.sh

echo "========================================="
echo "辣评项目构建脚本"
echo "========================================="

# 读取版本号
VERSION=$(cat VERSION 2>/dev/null || echo "unknown")
echo "版本号:$VERSION"

# 1. 清理旧的构建产物
echo ""
echo "清理旧的构建产物..."
rm -rf frontend/
rm -rf dist/

# 2. 构建前端
echo ""
echo "构建前端..."
cd admin-vue
npm install
npm run build
cd ..

echo "前端构建完成,输出目录:frontend/"

# 3. 构建后端
echo ""
echo "构建后端..."
cd cmd/server
go build -o ../../laping-server main.go
cd ../..

echo "后端构建完成:laping-server"

# 4. 创建发布目录
echo ""
echo "创建发布目录..."
mkdir -p dist/laping-$VERSION

# 5. 复制文件
echo "复制文件..."
cp -r frontend dist/laping-$VERSION/
cp laping-server dist/laping-$VERSION/
cp config.json.example dist/laping-$VERSION/config.json
cp README.md dist/laping-$VERSION/
cp VERSION dist/laping-$VERSION/

# 6. 创建启动脚本
cat > dist/laping-$VERSION/start.sh <<'EOF'
#!/bin/bash
echo "启动辣评服务..."
./laping-server
EOF
chmod +x dist/laping-$VERSION/start.sh

# 7. 打包
echo ""
echo "打包..."
cd dist
tar -czf laping-$VERSION.tar.gz laping-$VERSION/
cd ..

echo ""
echo "========================================="
echo "构建完成!"
echo "输出目录:dist/laping-$VERSION/"
echo "压缩包:dist/laping-$VERSION.tar.gz"
echo ""
echo "启动方式:"
echo "  cd dist/laping-$VERSION"
echo "  ./start.sh"
echo "========================================="

跨域处理

CORS 中间件

// cmd/server/middleware/cors.go
package middleware

import (
    "github.com/gin-gonic/gin"
)

func CORS() gin.HandlerFunc {
    return func(c *gin.Context) {
        method := c.Request.Method
        origin := c.Request.Header.Get("Origin")

        // 允许的源
        if origin != "" {
            c.Header("Access-Control-Allow-Origin", origin)
            c.Header("Access-Control-Allow-Methods", "GET, POST, PUT, DELETE, OPTIONS")
            c.Header("Access-Control-Allow-Headers", "Content-Type, Authorization, X-Requested-With")
            c.Header("Access-Control-Expose-Headers", "Content-Length, Content-Type")
            c.Header("Access-Control-Allow-Credentials", "true")
        }

        // 处理 OPTIONS 请求
        if method == "OPTIONS" {
            c.AbortWithStatus(204)
            return
        }

        c.Next()
    }
}

注意: 由于前后端统一到同一端口,实际上已经不存在跨域问题。但保留 CORS 中间件是为了:

  1. 支持开发环境(前端独立运行)
  2. 支持第三方调用 API
  3. 保持代码的灵活性

生产环境配置

配置文件

{
  "environment": "production",
  "apiPort": 8888,
  "frontendPath": "./frontend",
  "sqliteUrl": "./data/laping.db",
  "jwtSecret": "your-secret-key-change-in-production",
  "jwtExpireHours": 24,
  "jwtRefreshHours": 168,
  "adminUsername": "admin",
  "adminEmail": "admin@example.com",
  "adminPassword": "change-this-password"
}

Systemd 服务配置

# /etc/systemd/system/laping.service
[Unit]
Description=Laping Service
After=network.target

[Service]
Type=simple
User=laping
WorkingDirectory=/opt/laping
ExecStart=/opt/laping/laping-server
Restart=on-failure
RestartSec=5s

# 环境变量
Environment="GIN_MODE=release"

# 日志
StandardOutput=journal
StandardError=journal

[Install]
WantedBy=multi-user.target

启动和管理

# 启动服务
sudo systemctl start laping

# 停止服务
sudo systemctl stop laping

# 重启服务
sudo systemctl restart laping

# 查看状态
sudo systemctl status laping

# 查看日志
sudo journalctl -u laping -f

# 开机自启
sudo systemctl enable laping

Nginx 反向代理(可选)

如果需要使用 Nginx 作为反向代理(例如配置 HTTPS),可以使用以下配置:

# /etc/nginx/sites-available/laping
server {
    listen 80;
    server_name your-domain.com;

    # 重定向到 HTTPS
    return 301 https://$server_name$request_uri;
}

server {
    listen 443 ssl http2;
    server_name your-domain.com;

    # SSL 证书
    ssl_certificate /path/to/cert.pem;
    ssl_certificate_key /path/to/key.pem;

    # SSL 配置
    ssl_protocols TLSv1.2 TLSv1.3;
    ssl_ciphers HIGH:!aNULL:!MD5;

    # 日志
    access_log /var/log/nginx/laping_access.log;
    error_log /var/log/nginx/laping_error.log;

    # 反向代理到 Go 服务
    location / {
        proxy_pass http://localhost:8888;
        proxy_set_header Host $host;
        proxy_set_header X-Real-IP $remote_addr;
        proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
        proxy_set_header X-Forwarded-Proto $scheme;

        # WebSocket 支持(如果需要)
        proxy_http_version 1.1;
        proxy_set_header Upgrade $http_upgrade;
        proxy_set_header Connection "upgrade";
    }

    # 静态文件缓存
    location ~* \.(js|css|png|jpg|jpeg|gif|ico|svg|woff|woff2|ttf|eot)$ {
        proxy_pass http://localhost:8888;
        expires 1y;
        add_header Cache-Control "public, immutable";
    }
}

部署流程

1. 构建项目

# 在开发机器上构建
./build.sh

# 生成 dist/laping-1.5.0.tar.gz

2. 上传到服务器

# 上传压缩包
scp dist/laping-1.5.0.tar.gz user@server:/tmp/

# 登录服务器
ssh user@server

3. 部署

# 停止旧服务
sudo systemctl stop laping

# 备份旧版本
sudo mv /opt/laping /opt/laping.backup.$(date +%Y%m%d%H%M%S)

# 解压新版本
sudo mkdir -p /opt/laping
sudo tar -xzf /tmp/laping-1.5.0.tar.gz -C /opt/laping --strip-components=1

# 恢复配置文件
sudo cp /opt/laping.backup.*/config.json /opt/laping/

# 设置权限
sudo chown -R laping:laping /opt/laping
sudo chmod +x /opt/laping/laping-server

# 启动新服务
sudo systemctl start laping

# 检查状态
sudo systemctl status laping

4. 验证

# 检查服务是否正常
curl http://localhost:8888/api/health

# 查看日志
sudo journalctl -u laping -f

优化成果

部署简化

优化前:

  1. 构建前端 → 部署到 Nginx
  2. 构建后端 → 部署 Go 服务
  3. 配置 Nginx 反向代理
  4. 配置跨域

优化后:

  1. 构建前后端 → 生成单一压缩包
  2. 解压到服务器
  3. 启动服务
  4. 完成

性能提升

指标 优化前 优化后 提升
部署步骤 4步 3步 25%
服务数量 2个 1个 50%
端口占用 2个 1个 50%
配置复杂度 60%
跨域问题 需处理 100%

总结

前后端服务统一优化显著简化了部署流程,提升了系统的可维护性。通过将前端静态文件和后端 API 服务统一到同一端口,我们实现了:

关键成果

  • 单一服务,简化部署
  • 无跨域问题
  • 统一端口访问
  • 配置更简单
  • 维护更方便

技术亮点

  • Go 服务器同时提供 API 和静态文件
  • SPA 路由处理
  • 灵活的构建配置
  • 完善的部署脚本

用户体验

  • 访问更简单(单一地址)
  • 响应更快(减少网络跳转)
  • 更稳定(减少服务依赖)

这次优化为辣评平台的部署和运维带来了显著的便利,也为后续的功能扩展提供了更好的基础。

本文遵守 Attribution-NonCommercial 4.0 International 许可协议。 Attribution-NonCommercial 4.0 International