Golang + Vue 全栈项目实战

从零开始构建一个现代化的任务管理系统,掌握前后端分离架构的完整开发流程

后端技术栈

  • Golang 1.23+
  • Gin Web框架
  • GORM ORM框架
  • JWT身份认证
  • WebSocket实时通信
  • MySQL数据库
  • Docker容器化

前端技术栈

  • Vue 3 + Composition API
  • Vite构建工具
  • Vue Router 4
  • Pinia状态管理
  • Element Plus UI组件
  • Axios HTTP客户端
  • Socket.io客户端

项目功能特性

任务管理

创建、编辑、删除任务,支持任务状态跟踪

用户系统

用户注册、登录、权限管理

实时通知

任务状态变更实时推送

项目架构设计

📁 task-manager/
📁 backend/
📁 cmd/
📄 main.go
📁 internal/
📁 auth/
📁 handlers/
📁 models/
📁 middleware/
📁 websocket/
📁 pkg/
📁 config/
📁 database/
📁 utils/
📄 go.mod
📄 go.sum
📄 Dockerfile
📁 frontend/
📁 src/
📁 components/
📁 views/
📁 stores/
📁 services/
📁 utils/
📄 package.json
📄 vite.config.js
📄 Dockerfile
📄 docker-compose.yml
📄 README.md

开发环境准备

必需工具

  • Go 1.23 或更高版本
  • Node.js 18+ 和 npm
  • MySQL 8.0
  • Docker 和 Docker Compose

推荐工具

  • VS Code + Go插件
  • Postman / Insomnia
  • TablePlus / DBeaver
  • Git 版本控制

重要提示

  • • 确保所有工具版本符合要求,特别是Go 1.23+以使用最新语言特性
  • • 建议先在本地环境完成开发,再使用Docker进行容器化部署
  • • 本项目采用前后端分离架构,前后端可以独立开发和部署
  • • 代码将遵循RESTful API设计原则和Vue 3最佳实践

后端API开发

2.1 项目初始化

首先创建项目目录并初始化Go模块:

终端命令
mkdir task-manager && cd task-manager
mkdir -p backend/{cmd/{server,worker},internal/{auth,handlers,models,middleware,websocket},pkg/{config,database,utils,logger}}
cd backend
go mod init github.com/yourusername/task-manager

2.2 安装依赖包

安装依赖
go get -u github.com/gin-gonic/gin
go get -u gorm.io/gorm
go get -u gorm.io/driver/mysql
go get -u github.com/golang-jwt/jwt/v5
go get -u golang.org/x/crypto
go get -u github.com/gorilla/websocket
go get -u github.com/spf13/viper
go get -u go.uber.org/zap

2.3 数据库模型定义

internal/models 目录下创建模型:

internal/models/user.go
package models

import (
    "gorm.io/gorm"
    "time"
)

type User struct {
    ID        uint           `json:"id" gorm:"primaryKey"`
    Username  string         `json:"username" gorm:"unique;not null"`
    Email     string         `json:"email" gorm:"unique;not null"`
    Password  string         `json:"-" gorm:"not null"`
    CreatedAt time.Time      `json:"created_at"`
    UpdatedAt time.Time      `json:"updated_at"`
    DeletedAt gorm.DeletedAt `json:"-" gorm:"index"`
    
    // 关联关系
    Tasks []Task `json:"tasks,omitempty" gorm:"foreignKey:UserID"`
}

type Task struct {
    ID          uint           `json:"id" gorm:"primaryKey"`
    Title       string         `json:"title" gorm:"not null"`
    Description string         `json:"description"`
    Status      string         `json:"status" gorm:"default:'pending'"`
    Priority    string         `json:"priority" gorm:"default:'medium'"`
    DueDate     *time.Time     `json:"due_date,omitempty"`
    UserID      uint           `json:"user_id"`
    CreatedAt   time.Time      `json:"created_at"`
    UpdatedAt   time.Time      `json:"updated_at"`
    DeletedAt   gorm.DeletedAt `json:"-" gorm:"index"`
    
    // 关联关系
    User User `json:"user,omitempty" gorm:"foreignKey:UserID"`
}
internal/models/setup.go
package models

import (
    "fmt"
    "gorm.io/driver/mysql"
    "gorm.io/gorm"
    "gorm.io/gorm/logger"
    "log"
)

var DB *gorm.DB

func InitDB() (*gorm.DB, error) {
    dsn := "user:password@tcp(127.0.0.1:3306)/taskmanager?charset=utf8mb4&parseTime=True&loc=Local"
    
    db, err := gorm.Open(mysql.Open(dsn), &gorm.Config{
        Logger: logger.Default.LogMode(logger.Info),
    })
    
    if err != nil {
        return nil, fmt.Errorf("failed to connect database: %w", err)
    }
    
    // 自动迁移
    err = db.AutoMigrate(&User{}, &Task{})
    if err != nil {
        return nil, fmt.Errorf("failed to migrate database: %w", err)
    }
    
    DB = db
    log.Println("Database connected successfully")
    return db, nil
}

2.4 配置管理

pkg/config/config.go
package config

import (
    "github.com/spf13/viper"
    "log"
)

type Config struct {
    Server   ServerConfig   `mapstructure:"server"`
    Database DatabaseConfig `mapstructure:"database"`
    JWT      JWTConfig      `mapstructure:"jwt"`
}

type ServerConfig struct {
    Port string `mapstructure:"port"`
    Host string `mapstructure:"host"`
}

type DatabaseConfig struct {
    Host     string `mapstructure:"host"`
    Port     string `mapstructure:"port"`
    Username string `mapstructure:"username"`
    Password string `mapstructure:"password"`
    Database string `mapstructure:"database"`
}

type JWTConfig struct {
    Secret     string `mapstructure:"secret"`
    ExpireHours int   `mapstructure:"expire_hours"`
}

func LoadConfig() (*Config, error) {
    viper.SetConfigName("config")
    viper.SetConfigType("yaml")
    viper.AddConfigPath(".")
    viper.AddConfigPath("./config")
    
    viper.SetDefault("server.port", "8080")
    viper.SetDefault("server.host", "localhost")
    
    if err := viper.ReadInConfig(); err != nil {
        log.Printf("Error reading config file: %v", err)
    }
    
    var cfg Config
    if err := viper.Unmarshal(&cfg); err != nil {
        return nil, err
    }
    
    return &cfg, nil
}

2.5 JWT认证中间件

internal/middleware/auth.go
package middleware

import (
    "github.com/gin-gonic/gin"
    "github.com/yourusername/taskmanager/internal/auth"
    "net/http"
    "strings"
)

func AuthRequired() gin.HandlerFunc {
    return func(c *gin.Context) {
        authHeader := c.GetHeader("Authorization")
        if authHeader == "" {
            c.JSON(http.StatusUnauthorized, gin.H{"error": "Authorization header required"})
            c.Abort()
            return
        }
        
        parts := strings.SplitN(authHeader, " ", 2)
        if !(len(parts) == 2 && parts[0] == "Bearer") {
            c.JSON(http.StatusUnauthorized, gin.H{"error": "Authorization header format must be Bearer {token}"})
            c.Abort()
            return
        }
        
        claims, err := auth.ParseToken(parts[1])
        if err != nil {
            c.JSON(http.StatusUnauthorized, gin.H{"error": "Invalid token"})
            c.Abort()
            return
        }
        
        c.Set("user_id", claims.UserID)
        c.Set("username", claims.Username)
        c.Next()
    }
}

2.6 API处理器实现

internal/handlers/task.go
package handlers

import (
    "github.com/gin-gonic/gin"
    "github.com/yourusername/taskmanager/internal/models"
    "net/http"
    "strconv"
)

type TaskHandler struct {
    db *models.DB
}

func NewTaskHandler(db *models.DB) *TaskHandler {
    return &TaskHandler{db: db}
}

// GetTasks 获取用户的所有任务
func (h *TaskHandler) GetTasks(c *gin.Context) {
    userID := c.GetUint("user_id")
    
    var tasks []models.Task
    if err := h.db.Where("user_id = ?", userID).Find(&tasks).Error; err != nil {
        c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
        return
    }
    
    c.JSON(http.StatusOK, gin.H{"tasks": tasks})
}

// CreateTask 创建新任务
func (h *TaskHandler) CreateTask(c *gin.Context) {
    var req struct {
        Title       string  `json:"title" binding:"required"`
        Description string  `json:"description"`
        Priority    string  `json:"priority"`
        DueDate     *string `json:"due_date,omitempty"`
    }
    
    if err := c.ShouldBindJSON(&req); err != nil {
        c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
        return
    }
    
    task := models.Task{
        Title:       req.Title,
        Description: req.Description,
        Priority:    req.Priority,
        UserID:      c.GetUint("user_id"),
        Status:      "pending",
    }
    
    if err := h.db.Create(&task).Error; err != nil {
        c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
        return
    }
    
    c.JSON(http.StatusCreated, gin.H{"task": task})
}

// UpdateTask 更新任务
func (h *TaskHandler) UpdateTask(c *gin.Context) {
    taskID, err := strconv.ParseUint(c.Param("id"), 10, 32)
    if err != nil {
        c.JSON(http.StatusBadRequest, gin.H{"error": "Invalid task ID"})
        return
    }
    
    var req struct {
        Title       string `json:"title"`
        Description string `json:"description"`
        Status      string `json:"status"`
        Priority    string `json:"priority"`
    }
    
    if err := c.ShouldBindJSON(&req); err != nil {
        c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
        return
    }
    
    var task models.Task
    if err := h.db.First(&task, taskID).Error; err != nil {
        c.JSON(http.StatusNotFound, gin.H{"error": "Task not found"})
        return
    }
    
    // 检查任务所有者
    if task.UserID != c.GetUint("user_id") {
        c.JSON(http.StatusForbidden, gin.H{"error": "Not authorized"})
        return
    }
    
    // 更新字段
    if req.Title != "" {
        task.Title = req.Title
    }
    if req.Description != "" {
        task.Description = req.Description
    }
    if req.Status != "" {
        task.Status = req.Status
    }
    if req.Priority != "" {
        task.Priority = req.Priority
    }
    
    if err := h.db.Save(&task).Error; err != nil {
        c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
        return
    }
    
    c.JSON(http.StatusOK, gin.H{"task": task})
}

// DeleteTask 删除任务
func (h *TaskHandler) DeleteTask(c *gin.Context) {
    taskID, err := strconv.ParseUint(c.Param("id"), 10, 32)
    if err != nil {
        c.JSON(http.StatusBadRequest, gin.H{"error": "Invalid task ID"})
        return
    }
    
    var task models.Task
    if err := h.db.First(&task, taskID).Error; err != nil {
        c.JSON(http.StatusNotFound, gin.H{"error": "Task not found"})
        return
    }
    
    // 检查任务所有者
    if task.UserID != c.GetUint("user_id") {
        c.JSON(http.StatusForbidden, gin.H{"error": "Not authorized"})
        return
    }
    
    if err := h.db.Delete(&task).Error; err != nil {
        c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
        return
    }
    
    c.JSON(http.StatusOK, gin.H{"message": "Task deleted successfully"})
}

2.7 路由配置

internal/routes/routes.go
package routes

import (
    "github.com/gin-gonic/gin"
    "github.com/yourusername/taskmanager/internal/handlers"
    "github.com/yourusername/taskmanager/internal/middleware"
    "github.com/yourusername/taskmanager/internal/websocket"
    "gorm.io/gorm"
)

func SetupRoutes(db *gorm.DB, hub *websocket.Hub) *gin.Engine {
    r := gin.Default()
    
    // 中间件
    r.Use(gin.Recovery())
    r.Use(gin.Logger())
    
    // CORS配置
    r.Use(func(c *gin.Context) {
        c.Writer.Header().Set("Access-Control-Allow-Origin", "*")
        c.Writer.Header().Set("Access-Control-Allow-Credentials", "true")
        c.Writer.Header().Set("Access-Control-Allow-Headers", "Content-Type, Content-Length, Accept-Encoding, X-CSRF-Token, Authorization, accept, origin, Cache-Control, X-Requested-With")
        c.Writer.Header().Set("Access-Control-Allow-Methods", "POST, OPTIONS, GET, PUT, DELETE")
        
        if c.Request.Method == "OPTIONS" {
            c.AbortWithStatus(204)
            return
        }
        
        c.Next()
    })
    
    // 公开路由
    public := r.Group("/api/v1")
    {
        public.POST("/register", handlers.Register)
        public.POST("/login", handlers.Login)
    }
    
    // 受保护路由
    protected := r.Group("/api/v1")
    protected.Use(middleware.AuthRequired())
    {
        // 用户相关
        protected.GET("/profile", handlers.GetProfile)
        protected.PUT("/profile", handlers.UpdateProfile)
        
        // 任务相关
        taskHandler := handlers.NewTaskHandler(db)
        protected.GET("/tasks", taskHandler.GetTasks)
        protected.POST("/tasks", taskHandler.CreateTask)
        protected.GET("/tasks/:id", taskHandler.GetTask)
        protected.PUT("/tasks/:id", taskHandler.UpdateTask)
        protected.DELETE("/tasks/:id", taskHandler.DeleteTask)
    }
    
    // WebSocket路由
    r.GET("/ws", func(c *gin.Context) {
        websocket.HandleWebSocket(c, hub)
    })
    
    return r
}

2.8 主程序入口

cmd/server/main.go
package main

import (
    "context"
    "log"
    "net/http"
    "os"
    "os/signal"
    "syscall"
    "time"
    
    "github.com/yourusername/taskmanager/internal/models"
    "github.com/yourusername/taskmanager/internal/routes"
    "github.com/yourusername/taskmanager/internal/websocket"
    "github.com/yourusername/taskmanager/pkg/config"
)

func main() {
    // 加载配置
    cfg, err := config.LoadConfig()
    if err != nil {
        log.Fatalf("Failed to load config: %v", err)
    }
    
    // 初始化数据库
    db, err := models.InitDB()
    if err != nil {
        log.Fatalf("Failed to initialize database: %v", err)
    }
    
    // 创建WebSocket Hub
    hub := websocket.NewHub()
    go hub.Run()
    
    // 设置路由
    router := routes.SetupRoutes(db, hub)
    
    // 创建HTTP服务器
    srv := &http.Server{
        Addr:    ":" + cfg.Server.Port,
        Handler: router,
    }
    
    // 启动服务器(协程)
    go func() {
        log.Printf("Starting server on %s", srv.Addr)
        if err := srv.ListenAndServe(); err != nil && err != http.ErrServerClosed {
            log.Fatalf("Failed to start server: %v", err)
        }
    }()
    
    // 等待中断信号优雅关闭服务器
    quit := make(chan os.Signal, 1)
    signal.Notify(quit, syscall.SIGINT, syscall.SIGTERM)
    <-quit
    log.Println("Shutting down server...")
    
    // 设置5秒超时
    ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
    defer cancel()
    
    if err := srv.Shutdown(ctx); err != nil {
        log.Printf("Server forced to shutdown: %v", err)
    }
    
    log.Println("Server exited")
}

API接口说明

方法 路径 描述 认证
POST /api/v1/register 用户注册
POST /api/v1/login 用户登录
GET /api/v1/tasks 获取任务列表
POST /api/v1/tasks 创建新任务
PUT /api/v1/tasks/:id 更新任务
DELETE /api/v1/tasks/:id 删除任务

身份认证系统

3.1 JWT认证原理

JSON Web Token (JWT) 是一种开放标准 (RFC 7519),用于在各方之间安全地传输信息作为JSON对象。 本项目使用JWT实现无状态的身份认证。

JWT工作流程

  1. 1. 用户登录,提交用户名和密码
  2. 2. 服务器验证凭据,生成JWT Token
  3. 3. 客户端存储Token(通常是localStorage)
  4. 4. 后续请求携带Token(Authorization Header)
  5. 5. 服务器验证Token,处理请求

3.2 JWT工具类实现

internal/auth/jwt.go
package auth

import (
    "fmt"
    "github.com/golang-jwt/jwt/v5"
    "github.com/yourusername/taskmanager/pkg/config"
    "time"
)

type Claims struct {
    UserID   uint   `json:"user_id"`
    Username string `json:"username"`
    jwt.RegisteredClaims
}

func GenerateToken(userID uint, username string, cfg *config.JWTConfig) (string, error) {
    claims := Claims{
        UserID:   userID,
        Username: username,
        RegisteredClaims: jwt.RegisteredClaims{
            ExpiresAt: jwt.NewNumericDate(time.Now().Add(time.Duration(cfg.ExpireHours) * time.Hour)),
            IssuedAt:  jwt.NewNumericDate(time.Now()),
            NotBefore: jwt.NewNumericDate(time.Now()),
            Issuer:    "task-manager",
        },
    }
    
    token := jwt.NewWithClaims(jwt.SigningMethodHS256, claims)
    return token.SignedString([]byte(cfg.Secret))
}

func ParseToken(tokenString string) (*Claims, error) {
    token, err := jwt.ParseWithClaims(tokenString, &Claims{}, func(token *jwt.Token) (interface{}, error) {
        if _, ok := token.Method.(*jwt.SigningMethodHMAC); !ok {
            return nil, fmt.Errorf("unexpected signing method: %v", token.Header["alg"])
        }
        return []byte(config.GetJWTSecret()), nil
    })
    
    if err != nil {
        return nil, err
    }
    
    if claims, ok := token.Claims.(*Claims); ok && token.Valid {
        return claims, nil
    }
    
    return nil, fmt.Errorf("invalid token")
}

3.3 密码加密

internal/auth/password.go
package auth

import (
    "golang.org/x/crypto/bcrypt"
)

// HashPassword 使用bcrypt加密密码
func HashPassword(password string) (string, error) {
    bytes, err := bcrypt.GenerateFromPassword([]byte(password), bcrypt.DefaultCost)
    return string(bytes), err
}

// CheckPassword 验证密码
func CheckPassword(password, hash string) bool {
    err := bcrypt.CompareHashAndPassword([]byte(hash), []byte(password))
    return err == nil
}

3.4 用户注册和登录

internal/handlers/auth.go
package handlers

import (
    "github.com/gin-gonic/gin"
    "github.com/yourusername/taskmanager/internal/auth"
    "github.com/yourusername/taskmanager/internal/models"
    "github.com/yourusername/taskmanager/pkg/config"
    "net/http"
)

func Register(c *gin.Context) {
    var req struct {
        Username string `json:"username" binding:"required,min=3,max=20"`
        Email    string `json:"email" binding:"required,email"`
        Password string `json:"password" binding:"required,min=6"`
    }
    
    if err := c.ShouldBindJSON(&req); err != nil {
        c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
        return
    }
    
    // 检查用户名或邮箱是否已存在
    var existingUser models.User
    if err := models.DB.Where("username = ? OR email = ?", req.Username, req.Email).First(&existingUser).Error; err == nil {
        c.JSON(http.StatusConflict, gin.H{"error": "Username or email already exists"})
        return
    }
    
    // 加密密码
    hashedPassword, err := auth.HashPassword(req.Password)
    if err != nil {
        c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to hash password"})
        return
    }
    
    // 创建用户
    user := models.User{
        Username: req.Username,
        Email:    req.Email,
        Password: hashedPassword,
    }
    
    if err := models.DB.Create(&user).Error; err != nil {
        c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to create user"})
        return
    }
    
    c.JSON(http.StatusCreated, gin.H{
        "message": "User registered successfully",
        "user": gin.H{
            "id":       user.ID,
            "username": user.Username,
            "email":    user.Email,
        },
    })
}

func Login(c *gin.Context) {
    var req struct {
        Username string `json:"username" binding:"required"`
        Password string `json:"password" binding:"required"`
    }
    
    if err := c.ShouldBindJSON(&req); err != nil {
        c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
        return
    }
    
    // 查找用户
    var user models.User
    if err := models.DB.Where("username = ?", req.Username).First(&user).Error; err != nil {
        c.JSON(http.StatusUnauthorized, gin.H{"error": "Invalid credentials"})
        return
    }
    
    // 验证密码
    if !auth.CheckPassword(req.Password, user.Password) {
        c.JSON(http.StatusUnauthorized, gin.H{"error": "Invalid credentials"})
        return
    }
    
    // 生成JWT Token
    cfg, _ := config.LoadConfig()
    token, err := auth.GenerateToken(user.ID, user.Username, &cfg.JWT)
    if err != nil {
        c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to generate token"})
        return
    }
    
    c.JSON(http.StatusOK, gin.H{
        "message": "Login successful",
        "token":   token,
        "user": gin.H{
            "id":       user.ID,
            "username": user.Username,
            "email":    user.Email,
        },
    })
}

func GetProfile(c *gin.Context) {
    userID := c.GetUint("user_id")
    
    var user models.User
    if err := models.DB.First(&user, userID).Error; err != nil {
        c.JSON(http.StatusNotFound, gin.H{"error": "User not found"})
        return
    }
    
    c.JSON(http.StatusOK, gin.H{
        "user": gin.H{
            "id":       user.ID,
            "username": user.Username,
            "email":    user.Email,
            "created_at": user.CreatedAt,
        },
    })
}

3.5 配置文件示例

config.yaml
server:
  port: "8080"
  host: "localhost"

database:
  host: "localhost"
  port: "3306"
  username: "root"
  password: "your_password"
  database: "taskmanager"

jwt:
  secret: "your-super-secret-jwt-key-change-this-in-production"
  expire_hours: 24

安全最佳实践

  • • 使用强密码策略,至少6个字符,包含大小写字母和数字
  • • JWT密钥必须保密,生产环境使用环境变量存储
  • • 设置合理的Token过期时间,本项目设置为24小时
  • • 使用HTTPS传输,防止Token被窃听
  • • 定期轮换JWT密钥

前端开发

4.1 Vue项目初始化

使用Vite创建Vue 3项目:

终端命令
cd task-manager
npm create vue@latest frontend
cd frontend
npm install
npm install element-plus
npm install axios
npm install pinia
npm install vue-router@4
npm install @element-plus/icons-vue
npm install socket.io-client

4.2 Vite配置

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

export default defineConfig({
  plugins: [vue()],
  resolve: {
    alias: {
      '@': resolve(__dirname, 'src'),
    },
  },
  server: {
    port: 3000,
    proxy: {
      '/api': {
        target: 'http://localhost:8080',
        changeOrigin: true,
        rewrite: (path) => path.replace(/^\/api/, '/api'),
      },
    },
  },
  build: {
    outDir: 'dist',
    assetsDir: 'assets',
  },
})

4.3 路由配置

frontend/src/router/index.js
import { createRouter, createWebHistory } from 'vue-router'
import { useAuthStore } from '@/stores/auth'

const routes = [
  {
    path: '/',
    name: 'Home',
    component: () => import('@/views/Home.vue'),
    meta: { requiresAuth: true },
  },
  {
    path: '/login',
    name: 'Login',
    component: () => import('@/views/Login.vue'),
    meta: { requiresGuest: true },
  },
  {
    path: '/register',
    name: 'Register',
    component: () => import('@/views/Register.vue'),
    meta: { requiresGuest: true },
  },
  {
    path: '/tasks',
    name: 'Tasks',
    component: () => import('@/views/Tasks.vue'),
    meta: { requiresAuth: true },
  },
  {
    path: '/profile',
    name: 'Profile',
    component: () => import('@/views/Profile.vue'),
    meta: { requiresAuth: true },
  },
  {
    path: '/:pathMatch(.*)*',
    name: 'NotFound',
    component: () => import('@/views/NotFound.vue'),
  },
]

const router = createRouter({
  history: createWebHistory(),
  routes,
})

// 路由守卫
router.beforeEach((to, from, next) => {
  const authStore = useAuthStore()
  
  if (to.meta.requiresAuth && !authStore.isAuthenticated) {
    next('/login')
  } else if (to.meta.requiresGuest && authStore.isAuthenticated) {
    next('/')
  } else {
    next()
  }
})

export default router

4.4 Pinia状态管理

frontend/src/stores/auth.js
import { defineStore } from 'pinia'
import { ref, computed } from 'vue'
import api from '@/services/api'

export const useAuthStore = defineStore('auth', () => {
  const user = ref(null)
  const token = ref(localStorage.getItem('token') || null)
  
  const isAuthenticated = computed(() => !!token.value)
  
  const login = async (credentials) => {
    try {
      const response = await api.post('/login', credentials)
      const { token: userToken, user: userData } = response.data
      
      token.value = userToken
      user.value = userData
      
      localStorage.setItem('token', userToken)
      api.defaults.headers.common['Authorization'] = `Bearer ${userToken}`
      
      return { success: true }
    } catch (error) {
      return { 
        success: false, 
        error: error.response?.data?.error || 'Login failed'
      }
    }
  }
  
  const register = async (userData) => {
    try {
      const response = await api.post('/register', userData)
      return { 
        success: true, 
        message: response.data.message 
      }
    } catch (error) {
      return { 
        success: false, 
        error: error.response?.data?.error || 'Registration failed'
      }
    }
  }
  
  const logout = () => {
    token.value = null
    user.value = null
    localStorage.removeItem('token')
    delete api.defaults.headers.common['Authorization']
  }
  
  const loadUser = async () => {
    if (!token.value) return
    
    try {
      api.defaults.headers.common['Authorization'] = `Bearer ${token.value}`
      const response = await api.get('/profile')
      user.value = response.data.user
    } catch (error) {
      logout()
    }
  }
  
  // 初始化时加载用户信息
  if (token.value) {
    loadUser()
  }
  
  return {
    user,
    token,
    isAuthenticated,
    login,
    register,
    logout,
    loadUser,
  }
})
frontend/src/stores/tasks.js
import { defineStore } from 'pinia'
import { ref } from 'vue'
import api from '@/services/api'

export const useTaskStore = defineStore('tasks', () => {
  const tasks = ref([])
  const loading = ref(false)
  const error = ref(null)
  
  const fetchTasks = async () => {
    loading.value = true
    error.value = null
    
    try {
      const response = await api.get('/tasks')
      tasks.value = response.data.tasks
    } catch (err) {
      error.value = err.response?.data?.error || 'Failed to fetch tasks'
    } finally {
      loading.value = false
    }
  }
  
  const createTask = async (taskData) => {
    try {
      const response = await api.post('/tasks', taskData)
      tasks.value.unshift(response.data.task)
      return { success: true, task: response.data.task }
    } catch (err) {
      return { 
        success: false, 
        error: err.response?.data?.error || 'Failed to create task'
      }
    }
  }
  
  const updateTask = async (taskId, updates) => {
    try {
      const response = await api.put(`/tasks/${taskId}`, updates)
      const index = tasks.value.findIndex(task => task.id === taskId)
      if (index !== -1) {
        tasks.value[index] = response.data.task
      }
      return { success: true, task: response.data.task }
    } catch (err) {
      return { 
        success: false, 
        error: err.response?.data?.error || 'Failed to update task'
      }
    }
  }
  
  const deleteTask = async (taskId) => {
    try {
      await api.delete(`/tasks/${taskId}`)
      tasks.value = tasks.value.filter(task => task.id !== taskId)
      return { success: true }
    } catch (err) {
      return { 
        success: false, 
        error: err.response?.data?.error || 'Failed to delete task'
      }
    }
  }
  
  const getTaskById = (taskId) => {
    return tasks.value.find(task => task.id === taskId)
  }
  
  const getTasksByStatus = (status) => {
    return tasks.value.filter(task => task.status === status)
  }
  
  const tasksByStatus = ref({
    pending: [],
    in_progress: [],
    completed: []
  })
  
  const updateTasksByStatus = () => {
    tasksByStatus.value = {
      pending: tasks.value.filter(task => task.status === 'pending'),
      in_progress: tasks.value.filter(task => task.status === 'in_progress'),
      completed: tasks.value.filter(task => task.status === 'completed')
    }
  }
  
  return {
    tasks,
    tasksByStatus,
    loading,
    error,
    fetchTasks,
    createTask,
    updateTask,
    deleteTask,
    getTaskById,
    getTasksByStatus,
    updateTasksByStatus,
  }
})

4.5 HTTP服务封装

frontend/src/services/api.js
import axios from 'axios'
import { ElMessage } from 'element-plus'

const api = axios.create({
  baseURL: import.meta.env.VITE_API_BASE_URL || 'http://localhost:8080',
  timeout: 10000,
  headers: {
    'Content-Type': 'application/json',
  },
})

// 请求拦截器
api.interceptors.request.use(
  (config) => {
    const token = localStorage.getItem('token')
    if (token) {
      config.headers.Authorization = `Bearer ${token}`
    }
    return config
  },
  (error) => {
    return Promise.reject(error)
  }
)

// 响应拦截器
api.interceptors.response.use(
  (response) => {
    return response
  },
  (error) => {
    const message = error.response?.data?.error || 'Request failed'
    
    if (error.response?.status === 401) {
      localStorage.removeItem('token')
      window.location.href = '/login'
    } else if (error.response?.status === 403) {
      ElMessage.error('You do not have permission to perform this action')
    } else {
      ElMessage.error(message)
    }
    
    return Promise.reject(error)
  }
)

export default api

4.6 任务管理页面

frontend/src/views/Tasks.vue
<template>
  <div class="tasks-container">
    <el-row :gutter="20">
      <el-col :span="24">
        <el-card>
          <template #header>
            <div class="card-header">
              <span>任务管理</span>
              <el-button type="primary" @click="showCreateDialog = true">
                <el-icon><Plus /></el-icon> 新建任务
              </el-button>
            </div>
          </template>
          
          <el-row :gutter="20">
            <el-col :span="8" v-for="status in statusColumns" :key="status.value">
              <div class="task-column">
                <h3 class="column-title">{{ status.label }} ({{ getTasksByStatus(status.value).length }})</h3>
                <el-card
                  v-for="task in getTasksByStatus(status.value)"
                  :key="task.id"
                  class="task-card"
                  shadow="hover"
                >
                  <div class="task-content">
                    <h4>{{ task.title }}</h4>
                    <p v-if="task.description" class="task-description">{{ task.description }}</p>
                    <div class="task-meta">
                      <el-tag :type="getPriorityType(task.priority)" size="small">
                        {{ task.priority }}
                      </el-tag>
                      <span class="task-date">{{ formatDate(task.created_at) }}</span>
                    </div>
                    <div class="task-actions">
                      <el-button
                        type="primary"
                        size="small"
                        @click="editTask(task)"
                      >
                        编辑
                      </el-button>
                      <el-button
                        type="danger"
                        size="small"
                        @click="deleteTask(task.id)"
                      >
                        删除
                      </el-button>
                    </div>
                  </div>
                </el-card>
              </div>
            </el-col>
          </el-row>
        </el-card>
      </el-col>
    </el-row>

    <!-- 创建/编辑任务对话框 -->
    <el-dialog
      v-model="showCreateDialog"
      :title="editingTask ? '编辑任务' : '新建任务'"
      width="500px"
    >
      <el-form :model="taskForm" label-width="80px">
        <el-form-item label="标题" required>
          <el-input v-model="taskForm.title" />
        </el-form-item>
        <el-form-item label="描述">
          <el-input
            v-model="taskForm.description"
            type="textarea"
            :rows="3"
          />
        </el-form-item>
        <el-form-item label="优先级">
          <el-select v-model="taskForm.priority">
            <el-option label="低" value="low" />
            <el-option label="中" value="medium" />
            <el-option label="高" value="high" />
          </el-select>
        </el-form-item>
        <el-form-item label="状态" v-if="editingTask">
          <el-select v-model="taskForm.status">
            <el-option label="待处理" value="pending" />
            <el-option label="进行中" value="in_progress" />
            <el-option label="已完成" value="completed" />
          </el-select>
        </el-form-item>
      </el-form>
      <template #footer>
        <el-button @click="cancelDialog">取消</el-button>
        <el-button type="primary" @click="saveTask" :loading="saving">
          保存
        </el-button>
      </template>
    </el-dialog>
  </div>
</template>

<script setup>
import { ref, reactive, onMounted, computed } from 'vue'
import { useTaskStore } from '@/stores/tasks'
import { ElMessage, ElMessageBox } from 'element-plus'
import { Plus } from '@element-plus/icons-vue'

const taskStore = useTaskStore()

const showCreateDialog = ref(false)
const editingTask = ref(null)
const saving = ref(false)

const taskForm = reactive({
  title: '',
  description: '',
  priority: 'medium',
  status: 'pending',
})

const statusColumns = ref([
  { value: 'pending', label: '待处理' },
  { value: 'in_progress', label: '进行中' },
  { value: 'completed', label: '已完成' },
])

onMounted(() => {
  taskStore.fetchTasks()
})

const getTasksByStatus = (status) => {
  return taskStore.getTasksByStatus(status)
}

const getPriorityType = (priority) => {
  const types = {
    low: 'info',
    medium: 'warning',
    high: 'danger',
  }
  return types[priority] || 'info'
}

const formatDate = (dateString) => {
  return new Date(dateString).toLocaleDateString('zh-CN')
}

const editTask = (task) => {
  editingTask.value = task
  Object.assign(taskForm, task)
  showCreateDialog.value = true
}

const deleteTask = async (taskId) => {
  try {
    await ElMessageBox.confirm('确定要删除这个任务吗?', '警告', {
      confirmButtonText: '确定',
      cancelButtonText: '取消',
      type: 'warning',
    })
    
    const result = await taskStore.deleteTask(taskId)
    if (result.success) {
      ElMessage.success('任务删除成功')
    } else {
      ElMessage.error(result.error)
    }
  } catch (error) {
    if (error !== 'cancel') {
      ElMessage.error('删除失败')
    }
  }
}

const saveTask = async () => {
  if (!taskForm.title.trim()) {
    ElMessage.warning('请输入任务标题')
    return
  }
  
  saving.value = true
  
  try {
    let result
    if (editingTask.value) {
      result = await taskStore.updateTask(editingTask.value.id, taskForm)
    } else {
      result = await taskStore.createTask(taskForm)
    }
    
    if (result.success) {
      ElMessage.success(editingTask.value ? '任务更新成功' : '任务创建成功')
      cancelDialog()
    } else {
      ElMessage.error(result.error)
    }
  } catch (error) {
    ElMessage.error('操作失败')
  } finally {
    saving.value = false
  }
}

const cancelDialog = () => {
  showCreateDialog.value = false
  editingTask.value = null
  Object.assign(taskForm, {
    title: '',
    description: '',
    priority: 'medium',
    status: 'pending',
  })
}
</script>

<style scoped>
.tasks-container {
  padding: 20px;
}

.card-header {
  display: flex;
  justify-content: space-between;
  align-items: center;
}

.task-column {
  min-height: 500px;
}

.column-title {
  margin-bottom: 16px;
  color: #333;
  font-size: 16px;
  font-weight: 500;
}

.task-card {
  margin-bottom: 12px;
  cursor: pointer;
}

.task-content h4 {
  margin: 0 0 8px 0;
  color: #333;
}

.task-description {
  color: #666;
  font-size: 14px;
  margin: 8px 0;
  line-height: 1.4;
}

.task-meta {
  display: flex;
  justify-content: space-between;
  align-items: center;
  margin: 12px 0;
}

.task-date {
  color: #999;
  font-size: 12px;
}

.task-actions {
  display: flex;
  gap: 8px;
}
</style>

4.7 登录页面

frontend/src/views/Login.vue
<template>
  <div class="login-container">
    <el-card class="login-card">
      <template #header>
        <h2 class="login-title">用户登录</h2>
      </template>
      
      <el-form
        ref="loginFormRef"
        :model="loginForm"
        :rules="loginRules"
        label-width="80px"
        @submit.prevent="handleLogin"
      >
        <el-form-item label="用户名" prop="username">
          <el-input
            v-model="loginForm.username"
            placeholder="请输入用户名"
            prefix-icon="User"
          />
        </el-form-item>
        
        <el-form-item label="密码" prop="password">
          <el-input
            v-model="loginForm.password"
            type="password"
            placeholder="请输入密码"
            prefix-icon="Lock"
            show-password
          />
        </el-form-item>
        
        <el-form-item>
          <el-button
            type="primary"
            :loading="loading"
            native-type="submit"
            style="width: 100%"
          >
            登录
          </el-button>
        </el-form-item>
      </el-form>
      
      <div class="login-footer">
        <span>还没有账号?</span>
        <router-link to="/register" class="link">立即注册</router-link>
      </div>
    </el-card>
  </div>
</template>

<script setup>
import { ref, reactive } from 'vue'
import { useRouter } from 'vue-router'
import { useAuthStore } from '@/stores/auth'
import { ElMessage } from 'element-plus'
import { User, Lock } from '@element-plus/icons-vue'

const router = useRouter()
const authStore = useAuthStore()

const loginFormRef = ref()
const loading = ref(false)

const loginForm = reactive({
  username: '',
  password: '',
})

const loginRules = {
  username: [
    { required: true, message: '请输入用户名', trigger: 'blur' },
    { min: 3, max: 20, message: '用户名长度在 3 到 20 个字符', trigger: 'blur' },
  ],
  password: [
    { required: true, message: '请输入密码', trigger: 'blur' },
    { min: 6, message: '密码长度至少 6 个字符', trigger: 'blur' },
  ],
}

const handleLogin = async () => {
  const valid = await loginFormRef.value.validate().catch(() => false)
  if (!valid) return
  
  loading.value = true
  
  try {
    const result = await authStore.login(loginForm)
    
    if (result.success) {
      ElMessage.success('登录成功')
      router.push('/')
    } else {
      ElMessage.error(result.error)
    }
  } catch (error) {
    ElMessage.error('登录失败')
  } finally {
    loading.value = false
  }
}
</script>

<style scoped>
.login-container {
  display: flex;
  justify-content: center;
  align-items: center;
  min-height: 100vh;
  background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
}

.login-card {
  width: 400px;
  margin: 20px;
}

.login-title {
  text-align: center;
  margin: 0;
  color: #333;
}

.login-footer {
  text-align: center;
  margin-top: 20px;
  color: #666;
}

.link {
  color: #409EFF;
  text-decoration: none;
}

.link:hover {
  text-decoration: underline;
}
</style>

前后端联调

5.1 跨域处理(CORS)

当前端和后端运行在不同的域名或端口时,需要处理跨域请求。本项目在Gin中配置CORS中间件:

internal/middleware/cors.go
package middleware

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

func CORSMiddleware() gin.HandlerFunc {
    return func(c *gin.Context) {
        c.Writer.Header().Set("Access-Control-Allow-Origin", "*")
        c.Writer.Header().Set("Access-Control-Allow-Credentials", "true")
        c.Writer.Header().Set("Access-Control-Allow-Headers", "Content-Type, Content-Length, Accept-Encoding, X-CSRF-Token, Authorization, accept, origin, Cache-Control, X-Requested-With")
        c.Writer.Header().Set("Access-Control-Allow-Methods", "POST, OPTIONS, GET, PUT, DELETE")
        
        if c.Request.Method == "OPTIONS" {
            c.AbortWithStatus(204)
            return
        }
        
        c.Next()
    }
}

生产环境CORS配置

在生产环境中,应该将 Access-Control-Allow-Origin 设置为具体的前端域名,而不是使用通配符 *

c.Writer.Header().Set("Access-Control-Allow-Origin", "https://your-frontend-domain.com")

5.2 开发环境代理配置

在Vite中配置代理,解决开发环境的跨域问题:

前端代理配置
// vite.config.js
server: {
  port: 3000,
  proxy: {
    '/api': {
      target: 'http://localhost:8080',  // 后端地址
      changeOrigin: true,
      rewrite: (path) => path.replace(/^\/api/, '/api'),
    },
    '/ws': {
      target: 'ws://localhost:8080',
      ws: true,
    },
  },
},

5.3 API请求封装

统一处理请求和响应,包括错误处理和Token刷新:

前端API封装增强版
// services/api.js
import axios from 'axios'
import { ElMessage } from 'element-plus'
import { useAuthStore } from '@/stores/auth'

const api = axios.create({
  baseURL: import.meta.env.VITE_API_BASE_URL || 'http://localhost:8080',
  timeout: 10000,
})

// 请求队列(用于Token刷新)
let isRefreshing = false
let failedQueue = []

const processQueue = (error, token = null) => {
  failedQueue.forEach(prom => {
    if (error) {
      prom.reject(error)
    } else {
      prom.resolve(token)
    }
  })
  
  failedQueue = []
}

// 请求拦截器
api.interceptors.request.use(
  (config) => {
    const token = localStorage.getItem('token')
    if (token) {
      config.headers.Authorization = `Bearer ${token}`
    }
    return config
  },
  (error) => {
    return Promise.reject(error)
  }
)

// 响应拦截器
api.interceptors.response.use(
  (response) => {
    return response
  },
  async (error) => {
    const originalRequest = error.config
    
    if (error.response?.status === 401 && !originalRequest._retry) {
      if (isRefreshing) {
        return new Promise((resolve, reject) => {
          failedQueue.push({ resolve, reject })
        }).then(token => {
          originalRequest.headers.Authorization = `Bearer ${token}`
          return api(originalRequest)
        })
      }
      
      originalRequest._retry = true
      isRefreshing = true
      
      try {
        // 这里可以实现刷新Token的逻辑
        // const refreshToken = localStorage.getItem('refreshToken')
        // const response = await api.post('/refresh-token', { refreshToken })
        // const { token } = response.data
        
        // localStorage.setItem('token', token)
        // api.defaults.headers.common['Authorization'] = `Bearer ${token}`
        // processQueue(null, token)
        
        // 简单处理:直接跳转到登录页
        localStorage.removeItem('token')
        window.location.href = '/login'
        
        return Promise.reject(error)
      } catch (refreshError) {
        processQueue(refreshError, null)
        localStorage.removeItem('token')
        window.location.href = '/login'
        return Promise.reject(refreshError)
      } finally {
        isRefreshing = false
      }
    }
    
    // 错误处理
    const message = error.response?.data?.error || 'Request failed'
    
    if (error.response?.status === 403) {
      ElMessage.error('You do not have permission to perform this action')
    } else if (error.response?.status === 404) {
      ElMessage.error('Resource not found')
    } else if (error.response?.status === 500) {
      ElMessage.error('Server error, please try again later')
    } else {
      ElMessage.error(message)
    }
    
    return Promise.reject(error)
  }
)

export default api

5.4 错误处理策略

前端错误处理

  • • 网络错误:检查网络连接
  • • 401错误:跳转到登录页
  • • 403错误:显示权限不足
  • • 404错误:显示页面不存在
  • • 500错误:显示服务器错误
  • • 超时错误:提示请求超时

后端错误处理

  • • 验证错误:返回400状态码
  • • 认证失败:返回401状态码
  • • 权限不足:返回403状态码
  • • 资源不存在:返回404状态码
  • • 服务器错误:返回500状态码
  • • 统一错误响应格式

5.5 调试工具和技巧

调试工具推荐

浏览器开发者工具

Network面板查看请求,Console查看日志

Postman / Insomnia

API测试工具,独立测试后端接口

数据库客户端

TablePlus, DBeaver查看数据

5.6 联调检查清单

联调前检查项

后端检查

  • ☐ 数据库连接正常
  • ☐ API服务启动成功
  • ☐ CORS配置正确
  • ☐ 端口未被占用
  • ☐ 日志输出正常

前端检查

  • ☐ 代理配置正确
  • ☐ 依赖安装完成
  • ☐ 环境变量配置
  • ☐ 构建无错误
  • ☐ 热更新正常

实时通信

6.1 WebSocket原理

WebSocket是一种在单个TCP连接上进行全双工通信的协议。相比于HTTP轮询, WebSocket能够实时推送数据,大大减少了网络开销和延迟。

WebSocket工作流程

  1. 1. 客户端发起HTTP请求,包含Upgrade: websocket头部
  2. 2. 服务器返回101状态码,协议切换成功
  3. 3. 建立WebSocket连接,双方可以互相发送数据
  4. 4. 连接保持打开状态,直到任一方关闭连接

6.2 后端WebSocket实现

internal/websocket/hub.go
package websocket

import (
    "encoding/json"
    "log"
    "github.com/gorilla/websocket"
    "net/http"
    "time"
)

// Client 表示一个WebSocket客户端连接
type Client struct {
    hub      *Hub
    conn     *websocket.Conn
    send     chan []byte
    userID   uint
    username string
}

// Hub 维护活跃客户端并广播消息
type Hub struct {
    clients    map[*Client]bool
    broadcast  chan []byte
    register   chan *Client
    unregister chan *Client
}

// Message 定义消息结构
type Message struct {
    Type    string          `json:"type"`
    Data    json.RawMessage `json:"data"`
    UserID  uint            `json:"user_id,omitempty"`
    Time    time.Time       `json:"time"`
}

// TaskUpdateMessage 任务更新消息
type TaskUpdateMessage struct {
    TaskID     uint   `json:"task_id"`
    Title      string `json:"title"`
    Status     string `json:"status"`
    UpdatedBy  string `json:"updated_by"`
}

// NewHub 创建新的Hub实例
func NewHub() *Hub {
    return &Hub{
        broadcast:  make(chan []byte),
        register:   make(chan *Client),
        unregister: make(chan *Client),
        clients:    make(map[*Client]bool),
    }
}

// Run 启动Hub的消息处理循环
func (h *Hub) Run() {
    for {
        select {
        case client := <-h.register:
            h.clients[client] = true
            log.Printf("Client %s connected", client.username)
            
        case client := <-h.unregister:
            if _, ok := h.clients[client]; ok {
                delete(h.clients, client)
                close(client.send)
                log.Printf("Client %s disconnected", client.username)
            }
            
        case message := <-h.broadcast:
            // 广播消息给所有客户端
            for client := range h.clients {
                select {
                case client.send <- message:
                default:
                    close(client.send)
                    delete(h.clients, client)
                }
            }
        }
    }
}

// BroadcastToUser 向特定用户发送消息
func (h *Hub) BroadcastToUser(userID uint, message []byte) {
    for client := range h.clients {
        if client.userID == userID {
            select {
            case client.send <- message:
            default:
                close(client.send)
                delete(h.clients, client)
            }
        }
    }
}

// BroadcastTaskUpdate 广播任务更新消息
func (h *Hub) BroadcastTaskUpdate(taskID uint, title, status, updatedBy string) {
    message := Message{
        Type: "task_update",
        Data: mustMarshal(TaskUpdateMessage{
            TaskID:    taskID,
            Title:     title,
            Status:    status,
            UpdatedBy: updatedBy,
        }),
        Time: time.Now(),
    }
    
    data, _ := json.Marshal(message)
    h.broadcast <- data
}

// mustMarshal 辅助函数,确保JSON序列化不失败
func mustMarshal(v interface{}) json.RawMessage {
    data, err := json.Marshal(v)
    if err != nil {
        panic(err)
    }
    return data
}
internal/websocket/client.go
package websocket

import (
    "github.com/gin-gonic/gin"
    "github.com/gorilla/websocket"
    "log"
    "net/http"
    "time"
)

var upgrader = websocket.Upgrader{
    CheckOrigin: func(r *http.Request) bool {
        // 生产环境应该检查Origin
        return true
    },
}

// HandleWebSocket 处理WebSocket连接
func HandleWebSocket(c *gin.Context, hub *Hub) {
    userID, exists := c.Get("user_id")
    if !exists {
        c.JSON(http.StatusUnauthorized, gin.H{"error": "Unauthorized"})
        return
    }
    
    username, _ := c.Get("username")
    
    conn, err := upgrader.Upgrade(c.Writer, c.Request, nil)
    if err != nil {
        log.Printf("WebSocket upgrade failed: %v", err)
        return
    }
    
    client := &Client{
        hub:      hub,
        conn:     conn,
        send:     make(chan []byte, 256),
        userID:   userID.(uint),
        username: username.(string),
    }
    
    client.hub.register <- client
    
    // 启动读写协程
    go client.writePump()
    go client.readPump()
}

// readPump 从WebSocket连接读取消息
func (c *Client) readPump() {
    defer func() {
        c.hub.unregister <- c
        c.conn.Close()
    }()
    
    c.conn.SetReadDeadline(time.Now().Add(60 * time.Second))
    c.conn.SetPongHandler(func(string) error {
        c.conn.SetReadDeadline(time.Now().Add(60 * time.Second))
        return nil
    })
    
    for {
        _, message, err := c.conn.ReadMessage()
        if err != nil {
            if websocket.IsUnexpectedCloseError(err, websocket.CloseGoingAway, websocket.CloseAbnormalClosure) {
                log.Printf("WebSocket error: %v", err)
            }
            break
        }
        
        // 处理接收到的消息(可以根据需要扩展)
        log.Printf("Received message from %s: %s", c.username, string(message))
    }
}

// writePump 向WebSocket连接写入消息
func (c *Client) writePump() {
    ticker := time.NewTicker(54 * time.Second)
    defer func() {
        ticker.Stop()
        c.conn.Close()
    }()
    
    for {
        select {
        case message, ok := <-c.send:
            c.conn.SetWriteDeadline(time.Now().Add(10 * time.Second))
            if !ok {
                c.conn.WriteMessage(websocket.CloseMessage, []byte{})
                return
            }
            
            c.conn.WriteMessage(websocket.TextMessage, message)
            
        case <-ticker.C:
            c.conn.SetWriteDeadline(time.Now().Add(10 * time.Second))
            if err := c.conn.WriteMessage(websocket.PingMessage, nil); err != nil {
                return
            }
        }
    }
}

6.3 前端Socket.io集成

frontend/src/services/websocket.js
import { io } from 'socket.io-client'
import { ElMessage } from 'element-plus'

class WebSocketService {
  constructor() {
    this.socket = null
    this.connected = false
    this.reconnectAttempts = 0
    this.maxReconnectAttempts = 5
  }
  
  connect(token) {
    return new Promise((resolve, reject) => {
      this.socket = io(import.meta.env.VITE_WS_URL || 'ws://localhost:8080', {
        auth: {
          token: token
        },
        reconnection: true,
        reconnectionDelay: 1000,
        reconnectionAttempts: this.maxReconnectAttempts,
      })
      
      this.socket.on('connect', () => {
        console.log('WebSocket connected')
        this.connected = true
        this.reconnectAttempts = 0
        resolve()
      })
      
      this.socket.on('disconnect', (reason) => {
        console.log('WebSocket disconnected:', reason)
        this.connected = false
      })
      
      this.socket.on('connect_error', (error) => {
        console.error('WebSocket connection error:', error)
        this.reconnectAttempts++
        
        if (this.reconnectAttempts >= this.maxReconnectAttempts) {
          ElMessage.error('无法连接到服务器,请检查网络连接')
          reject(error)
        }
      })
      
      // 注册消息处理器
      this.setupMessageHandlers()
    })
  }
  
  disconnect() {
    if (this.socket) {
      this.socket.disconnect()
      this.socket = null
      this.connected = false
    }
  }
  
  setupMessageHandlers() {
    // 任务更新消息
    this.socket.on('task_update', (data) => {
      console.log('Task update received:', data)
      ElMessage.info(`任务 "${data.title}" 已被 ${data.updated_by} 更新为 ${data.status}`)
    })
    
    // 新任务通知
    this.socket.on('new_task', (data) => {
      console.log('New task received:', data)
      ElMessage.success(`新任务 "${data.title}" 已创建`)
    })
    
    // 系统通知
    this.socket.on('notification', (data) => {
      ElMessage[data.type || 'info'](data.message)
    })
  }
  
  // 发送消息
  sendMessage(event, data) {
    if (this.connected && this.socket) {
      this.socket.emit(event, data)
    } else {
      console.warn('WebSocket is not connected, message not sent')
    }
  }
  
  // 订阅事件
  on(event, callback) {
    if (this.socket) {
      this.socket.on(event, callback)
    }
  }
  
  // 取消订阅
  off(event, callback) {
    if (this.socket) {
      this.socket.off(event, callback)
    }
  }
}

export default new WebSocketService()

6.4 实时任务通知

增强的任务处理器(带WebSocket通知)
// 在任务处理器中添加WebSocket通知
func (h *TaskHandler) CreateTask(c *gin.Context) {
    var req struct {
        Title       string  `json:"title" binding:"required"`
        Description string  `json:"description"`
        Priority    string  `json:"priority"`
        DueDate     *string `json:"due_date,omitempty"`
    }
    
    if err := c.ShouldBindJSON(&req); err != nil {
        c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
        return
    }
    
    task := models.Task{
        Title:       req.Title,
        Description: req.Description,
        Priority:    req.Priority,
        UserID:      c.GetUint("user_id"),
        Status:      "pending",
    }
    
    if err := h.db.Create(&task).Error; err != nil {
        c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
        return
    }
    
    // 广播新任务通知
    username, _ := c.Get("username")
    h.hub.BroadcastTaskUpdate(task.ID, task.Title, task.Status, username.(string))
    
    c.JSON(http.StatusCreated, gin.H{"task": task})
}

func (h *TaskHandler) UpdateTask(c *gin.Context) {
    taskID, err := strconv.ParseUint(c.Param("id"), 10, 32)
    if err != nil {
        c.JSON(http.StatusBadRequest, gin.H{"error": "Invalid task ID"})
        return
    }
    
    var req struct {
        Title       string `json:"title"`
        Description string `json:"description"`
        Status      string `json:"status"`
        Priority    string `json:"priority"`
    }
    
    if err := c.ShouldBindJSON(&req); err != nil {
        c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
        return
    }
    
    var task models.Task
    if err := h.db.First(&task, taskID).Error; err != nil {
        c.JSON(http.StatusNotFound, gin.H{"error": "Task not found"})
        return
    }
    
    // 检查任务所有者
    if task.UserID != c.GetUint("user_id") {
        c.JSON(http.StatusForbidden, gin.H{"error": "Not authorized"})
        return
    }
    
    // 更新字段
    oldStatus := task.Status
    if req.Title != "" {
        task.Title = req.Title
    }
    if req.Description != "" {
        task.Description = req.Description
    }
    if req.Status != "" {
        task.Status = req.Status
    }
    if req.Priority != "" {
        task.Priority = req.Priority
    }
    
    if err := h.db.Save(&task).Error; err != nil {
        c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
        return
    }
    
    // 如果状态改变,广播通知
    if oldStatus != task.Status {
        username, _ := c.Get("username")
        h.hub.BroadcastTaskUpdate(task.ID, task.Title, task.Status, username.(string))
    }
    
    c.JSON(http.StatusOK, gin.H{"task": task})
}

6.5 前端实时更新

前端集成WebSocket实时更新
// stores/tasks.js 增强版
import { defineStore } from 'pinia'
import { ref } from 'vue'
import api from '@/services/api'
import websocketService from '@/services/websocket'

export const useTaskStore = defineStore('tasks', () => {
  const tasks = ref([])
  const loading = ref(false)
  const error = ref(null)
  
  const fetchTasks = async () => {
    loading.value = true
    error.value = null
    
    try {
      const response = await api.get('/tasks')
      tasks.value = response.data.tasks
    } catch (err) {
      error.value = err.response?.data?.error || 'Failed to fetch tasks'
    } finally {
      loading.value = false
    }
  }
  
  const createTask = async (taskData) => {
    try {
      const response = await api.post('/tasks', taskData)
      tasks.value.unshift(response.data.task)
      return { success: true, task: response.data.task }
    } catch (err) {
      return { 
        success: false, 
        error: err.response?.data?.error || 'Failed to create task'
      }
    }
  }
  
  const updateTask = async (taskId, updates) => {
    try {
      const response = await api.put(`/tasks/${taskId}`, updates)
      const index = tasks.value.findIndex(task => task.id === taskId)
      if (index !== -1) {
        tasks.value[index] = response.data.task
      }
      return { success: true, task: response.data.task }
    } catch (err) {
      return { 
        success: false, 
        error: err.response?.data?.error || 'Failed to update task'
      }
    }
  }
  
  const deleteTask = async (taskId) => {
    try {
      await api.delete(`/tasks/${taskId}`)
      tasks.value = tasks.value.filter(task => task.id !== taskId)
      return { success: true }
    } catch (err) {
      return { 
        success: false, 
        error: err.response?.data?.error || 'Failed to delete task'
      }
    }
  }
  
  // 通过WebSocket接收任务更新
  const handleTaskUpdate = (data) => {
    const index = tasks.value.findIndex(task => task.id === data.task_id)
    if (index !== -1) {
      tasks.value[index].status = data.status
    }
  }
  
  // 通过WebSocket接收新任务
  const handleNewTask = (data) => {
    // 检查任务是否已存在
    const exists = tasks.value.some(task => task.id === data.id)
    if (!exists) {
      tasks.value.unshift(data)
    }
  }
  
  // 初始化WebSocket监听
  const initializeWebSocket = (token) => {
    websocketService.connect(token)
    
    // 注册事件监听器
    websocketService.on('task_update', handleTaskUpdate)
    websocketService.on('new_task', handleNewTask)
  }
  
  // 清理WebSocket监听
  const cleanupWebSocket = () => {
    websocketService.off('task_update', handleTaskUpdate)
    websocketService.off('new_task', handleNewTask)
    websocketService.disconnect()
  }
  
  return {
    tasks,
    loading,
    error,
    fetchTasks,
    createTask,
    updateTask,
    deleteTask,
    initializeWebSocket,
    cleanupWebSocket,
  }
})

WebSocket最佳实践

  • • 心跳检测:定期发送ping/pong消息保持连接
  • • 自动重连:连接断开时自动尝试重连
  • • 消息确认:重要消息需要确认机制
  • • 限流控制:防止过多消息导致性能问题
  • • 身份验证:连接时验证用户身份

Docker部署

7.1 Docker基础知识

Docker是一个开源的应用容器引擎,让开发者可以打包应用及其依赖包到一个可移植的容器中, 然后发布到任何流行的Linux机器或Windows机器上。

容器化

应用及其依赖打包到轻量级容器中

镜像分层

使用分层文件系统,提高构建效率

快速部署

一次构建,到处运行

7.2 后端Docker配置

backend/Dockerfile
# 构建阶段
FROM golang:1.23-alpine AS builder

# 设置工作目录
WORKDIR /app

# 安装构建依赖
RUN apk add --no-cache git

# 复制Go模块文件
COPY go.mod go.sum ./

# 下载依赖
RUN go mod download

# 复制源代码
COPY . .

# 构建应用
RUN CGO_ENABLED=0 GOOS=linux go build -a -installsuffix cgo -o main cmd/server/main.go

# 运行阶段
FROM alpine:latest

# 安装运行时依赖
RUN apk --no-cache add ca-certificates tzdata

# 设置时区
ENV TZ=Asia/Shanghai

# 创建非root用户
RUN addgroup -g 1000 -S appuser && \
    adduser -u 1000 -S appuser -G appuser

# 设置工作目录
WORKDIR /root/

# 从构建阶段复制二进制文件
COPY --from=builder /app/main .

# 复制配置文件
COPY --from=builder /app/config.yaml ./config/

# 更改文件所有者
RUN chown -R appuser:appuser /root/

# 切换到非root用户
USER appuser

# 暴露端口
EXPOSE 8080

# 健康检查
HEALTHCHECK --interval=30s --timeout=3s --start-period=5s --retries=3 \
  CMD wget --no-verbose --tries=1 --spider http://localhost:8080/health || exit 1

# 启动应用
CMD ["./main"]

7.3 前端Docker配置

frontend/Dockerfile
# 构建阶段
FROM node:18-alpine AS builder

# 设置工作目录
WORKDIR /app

# 复制package文件
COPY package*.json ./

# 安装依赖
RUN npm ci --only=production

# 复制源代码
COPY . .

# 构建应用
RUN npm run build

# 运行阶段
FROM nginx:alpine

# 复制自定义nginx配置
COPY nginx.conf /etc/nginx/nginx.conf

# 从构建阶段复制构建文件
COPY --from=builder /app/dist /usr/share/nginx/html

# 创建非root用户
RUN adduser -D -s /bin/sh nginx-user

# 更改文件权限
RUN chown -R nginx-user:nginx-user /usr/share/nginx/html

# 切换到非root用户
USER nginx-user

# 暴露端口
EXPOSE 80

# 健康检查
HEALTHCHECK --interval=30s --timeout=3s --start-period=5s --retries=3 \
  CMD wget --no-verbose --tries=1 --spider http://localhost:80/ || exit 1

# 启动nginx
CMD ["nginx", "-g", "daemon off;"]

7.4 Nginx配置

frontend/nginx.conf
events {
    worker_connections 1024;
}

http {
    include /etc/nginx/mime.types;
    default_type application/octet-stream;
    
    # 日志格式
    log_format main '$remote_addr - $remote_user [$time_local] "$request" '
                    '$status $body_bytes_sent "$http_referer" '
                    '"$http_user_agent" "$http_x_forwarded_for"';
    
    access_log /var/log/nginx/access.log main;
    error_log /var/log/nginx/error.log;
    
    # 性能优化
    sendfile on;
    tcp_nopush on;
    tcp_nodelay on;
    keepalive_timeout 65;
    types_hash_max_size 2048;
    
    # Gzip压缩
    gzip on;
    gzip_vary on;
    gzip_proxied any;
    gzip_comp_level 6;
    gzip_types
        text/plain
        text/css
        text/xml
        text/javascript
        application/json
        application/javascript
        application/xml+rss
        application/atom+xml
        image/svg+xml;
    
    server {
        listen 80;
        server_name localhost;
        root /usr/share/nginx/html;
        index index.html;
        
        # 安全头部
        add_header X-Frame-Options "SAMEORIGIN" always;
        add_header X-Content-Type-Options "nosniff" always;
        add_header X-XSS-Protection "1; mode=block" always;
        
        # 缓存静态资源
        location ~* \.(js|css|png|jpg|jpeg|gif|ico|svg)$ {
            expires 1y;
            add_header Cache-Control "public, immutable";
        }
        
        # API代理(开发环境)
        location /api {
            proxy_pass http://backend:8080;
            proxy_http_version 1.1;
            proxy_set_header Upgrade $http_upgrade;
            proxy_set_header Connection 'upgrade';
            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;
            proxy_cache_bypass $http_upgrade;
        }
        
        # WebSocket代理
        location /ws {
            proxy_pass http://backend:8080;
            proxy_http_version 1.1;
            proxy_set_header Upgrade $http_upgrade;
            proxy_set_header Connection "upgrade";
            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;
            proxy_read_timeout 86400;
        }
        
        # 单页应用路由
        location / {
            try_files $uri $uri/ /index.html;
        }
        
        # 错误页面
        error_page 404 /index.html;
        error_page 500 502 503 504 /50x.html;
        location = /50x.html {
            root /usr/share/nginx/html;
        }
    }
}

7.5 Docker Compose配置

docker-compose.yml
version: '3.8'

services:
  # MySQL数据库
  mysql:
    image: mysql:8.0
    container_name: taskmanager-mysql
    environment:
      MYSQL_ROOT_PASSWORD: rootpassword
      MYSQL_DATABASE: taskmanager
      MYSQL_USER: taskuser
      MYSQL_PASSWORD: taskpassword
    ports:
      - "3306:3306"
    volumes:
      - mysql_data:/var/lib/mysql
      - ./backend/init.sql:/docker-entrypoint-initdb.d/init.sql
    networks:
      - taskmanager-network
    healthcheck:
      test: ["CMD", "mysqladmin", "ping", "-h", "localhost"]
      timeout: 20s
      retries: 10
      interval: 10s
      start_period: 30s

  # 后端API服务
  backend:
    build:
      context: ./backend
      dockerfile: Dockerfile
    container_name: taskmanager-backend
    environment:
      DB_HOST: mysql
      DB_PORT: 3306
      DB_USER: taskuser
      DB_PASSWORD: taskpassword
      DB_NAME: taskmanager
      JWT_SECRET: your-super-secret-jwt-key-change-this-in-production
      JWT_EXPIRE_HOURS: 24
    ports:
      - "8080:8080"
    depends_on:
      mysql:
        condition: service_healthy
    networks:
      - taskmanager-network
    volumes:
      - ./backend/config.yaml:/root/config/config.yaml
    restart: unless-stopped
    healthcheck:
      test: ["CMD", "wget", "--no-verbose", "--tries=1", "--spider", "http://localhost:8080/health"]
      timeout: 10s
      retries: 3
      interval: 30s
      start_period: 40s

  # 前端Web服务
  frontend:
    build:
      context: ./frontend
      dockerfile: Dockerfile
    container_name: taskmanager-frontend
    ports:
      - "80:80"
    depends_on:
      - backend
    networks:
      - taskmanager-network
    restart: unless-stopped
    healthcheck:
      test: ["CMD", "wget", "--no-verbose", "--tries=1", "--spider", "http://localhost:80/"]
      timeout: 10s
      retries: 3
      interval: 30s
      start_period: 40s

  # Redis缓存(可选)
  redis:
    image: redis:7-alpine
    container_name: taskmanager-redis
    ports:
      - "6379:6379"
    volumes:
      - redis_data:/data
    networks:
      - taskmanager-network
    command: redis-server --appendonly yes
    restart: unless-stopped

  # Nginx反向代理(生产环境)
  nginx:
    image: nginx:alpine
    container_name: taskmanager-nginx
    ports:
      - "443:443"
    volumes:
      - ./nginx/nginx.conf:/etc/nginx/nginx.conf
      - ./nginx/ssl:/etc/nginx/ssl
    depends_on:
      - frontend
      - backend
    networks:
      - taskmanager-network
    restart: unless-stopped
    profiles:
      - production

volumes:
  mysql_data:
    driver: local
  redis_data:
    driver: local

networks:
  taskmanager-network:
    driver: bridge

7.6 环境变量配置

.env.example
# 数据库配置
DB_HOST=localhost
DB_PORT=3306
DB_USER=taskuser
DB_PASSWORD=taskpassword
DB_NAME=taskmanager

# JWT配置
JWT_SECRET=your-super-secret-jwt-key-change-this-in-production
JWT_EXPIRE_HOURS=24

# 服务器配置
SERVER_PORT=8080
SERVER_HOST=localhost

# 前端API地址
VITE_API_BASE_URL=http://localhost:8080
VITE_WS_URL=ws://localhost:8080

# MySQL Root密码
MYSQL_ROOT_PASSWORD=rootpassword

7.7 部署步骤

1

克隆项目

克隆项目到服务器
git clone https://github.com/yourusername/task-manager.git
cd task-manager
2

配置环境变量

复制并编辑环境变量
cp .env.example .env
# 编辑 .env 文件,设置实际的配置值
3

启动服务

使用Docker Compose启动
# 开发环境
docker-compose up -d

# 生产环境(包含Nginx和SSL)
docker-compose --profile production up -d
4

验证部署

检查服务状态
# 查看容器状态
docker-compose ps

# 查看日志
docker-compose logs -f backend
docker-compose logs -f frontend

# 测试API
curl http://localhost:8080/health

# 访问前端
open http://localhost

7.8 常用Docker命令

容器管理

容器操作
# 启动服务
docker-compose up -d

# 停止服务
docker-compose down

# 重启服务
docker-compose restart

# 查看日志
docker-compose logs -f [service]

镜像管理

镜像操作
# 构建镜像
docker-compose build

# 重新构建
docker-compose up --build

# 清理镜像
docker image prune

# 查看镜像
docker images

生产环境注意事项

  • • 使用强密码和密钥,不要在代码中硬编码
  • • 配置SSL/TLS证书,使用HTTPS
  • • 定期更新Docker镜像和依赖
  • • 配置日志收集和监控
  • • 使用防火墙限制端口访问
  • • 定期备份数据库
  • • 配置资源限制,防止容器占用过多资源

完整源码

8.1 项目完整结构

📁 task-manager/
📁 backend/
📁 cmd/
📄 server.go
📁 internal/
📁 auth/
📄 jwt.go
📄 password.go
📁 handlers/
📄 auth.go
📄 task.go
📄 user.go
📁 middleware/
📄 auth.go
📄 cors.go
📁 models/
📄 user.go
📄 task.go
📄 setup.go
📁 websocket/
📄 hub.go
📄 client.go
📁 pkg/
📁 config/
📄 config.go
📁 utils/
📄 response.go
📄 validator.go
📄 go.mod
📄 go.sum
📄 Dockerfile
📄 .env.example
📁 frontend/
📁 public/
📁 src/
📁 assets/
📁 components/
📄 TaskCard.vue
📄 TaskForm.vue
📄 UserAvatar.vue
📁 views/
📄 Home.vue
📄 Login.vue
📄 Register.vue
📄 Tasks.vue
📄 Profile.vue
📁 stores/
📄 auth.js
📄 tasks.js
📁 services/
📄 api.js
📄 websocket.js
📁 router/
📄 index.js
📁 utils/
📄 date.js
📄 validate.js
📄 App.vue
📄 main.js
📄 index.html
📄 vite.config.js
📄 package.json
📄 Dockerfile
📄 nginx.conf
📄 docker-compose.yml
📄 docker-compose.prod.yml
📄 .env.example
📄 README.md
📄 .gitignore

8.2 核心配置文件

后端主程序

backend/cmd/server/main.go(完整版)
package main

import (
    "context"
    "fmt"
    "log"
    "net/http"
    "os"
    "os/signal"
    "syscall"
    "time"
    
    "github.com/gin-gonic/gin"
    "github.com/spf13/viper"
    "gorm.io/driver/mysql"
    "gorm.io/gorm"
    "gorm.io/gorm/logger"
    
    "github.com/yourusername/taskmanager/internal/handlers"
    "github.com/yourusername/taskmanager/internal/middleware"
    "github.com/yourusername/taskmanager/internal/models"
    "github.com/yourusername/taskmanager/internal/websocket"
    "github.com/yourusername/taskmanager/pkg/config"
)

var (
    cfg *config.Config
    db  *gorm.DB
)

func init() {
    // 加载配置
    var err error
    cfg, err = loadConfig()
    if err != nil {
        log.Fatalf("Failed to load config: %v", err)
    }
    
    // 初始化数据库
    db, err = initDB(cfg.Database)
    if err != nil {
        log.Fatalf("Failed to initialize database: %v", err)
    }
    
    // 自动迁移
    if err := db.AutoMigrate(&models.User{}, &models.Task{}); err != nil {
        log.Fatalf("Failed to migrate database: %v", err)
    }
}

func main() {
    // 创建WebSocket Hub
    hub := websocket.NewHub()
    go hub.Run()
    
    // 设置Gin模式
    gin.SetMode(gin.ReleaseMode)
    
    // 创建路由
    router := setupRouter(hub)
    
    // 创建HTTP服务器
    srv := &http.Server{
        Addr:    ":" + cfg.Server.Port,
        Handler: router,
    }
    
    // 启动服务器
    go func() {
        log.Printf("Starting server on %s", srv.Addr)
        if err := srv.ListenAndServe(); err != nil && err != http.ErrServerClosed {
            log.Fatalf("Failed to start server: %v", err)
        }
    }()
    
    // 等待中断信号
    quit := make(chan os.Signal, 1)
    signal.Notify(quit, syscall.SIGINT, syscall.SIGTERM)
    <-quit
    log.Println("Shutting down server...")
    
    // 优雅关闭
    ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
    defer cancel()
    
    if err := srv.Shutdown(ctx); err != nil {
        log.Printf("Server forced to shutdown: %v", err)
    }
    
    log.Println("Server exited")
}

func loadConfig() (*config.Config, error) {
    viper.SetConfigName("config")
    viper.SetConfigType("yaml")
    viper.AddConfigPath(".")
    viper.AddConfigPath("./config")
    
    // 从环境变量读取
    viper.AutomaticEnv()
    
    // 设置默认值
    viper.SetDefault("server.port", "8080")
    viper.SetDefault("server.host", "localhost")
    viper.SetDefault("database.host", "localhost")
    viper.SetDefault("database.port", "3306")
    viper.SetDefault("database.username", "root")
    viper.SetDefault("database.password", "")
    viper.SetDefault("database.database", "taskmanager")
    viper.SetDefault("jwt.secret", "default-secret-change-this")
    viper.SetDefault("jwt.expire_hours", 24)
    
    if err := viper.ReadInConfig(); err != nil {
        log.Printf("Error reading config file: %v", err)
    }
    
    var cfg config.Config
    if err := viper.Unmarshal(&cfg); err != nil {
        return nil, err
    }
    
    return &cfg, nil
}

func initDB(cfg config.DatabaseConfig) (*gorm.DB, error) {
    dsn := fmt.Sprintf("%s:%s@tcp(%s:%s)/%s?charset=utf8mb4&parseTime=True&loc=Local",
        cfg.Username, cfg.Password, cfg.Host, cfg.Port, cfg.Database)
    
    db, err := gorm.Open(mysql.Open(dsn), &gorm.Config{
        Logger: logger.Default.LogMode(logger.Info),
    })
    
    if err != nil {
        return nil, fmt.Errorf("failed to connect database: %w", err)
    }
    
    log.Println("Database connected successfully")
    return db, nil
}

func setupRouter(hub *websocket.Hub) *gin.Engine {
    r := gin.Default()
    
    // 中间件
    r.Use(middleware.CORSMiddleware())
    
    // 健康检查
    r.GET("/health", func(c *gin.Context) {
        c.JSON(http.StatusOK, gin.H{"status": "ok"})
    })
    
    // 公开路由
    public := r.Group("/api/v1")
    {
        public.POST("/register", handlers.Register)
        public.POST("/login", handlers.Login)
    }
    
    // 受保护路由
    protected := r.Group("/api/v1")
    protected.Use(middleware.AuthRequired())
    {
        protected.GET("/profile", handlers.GetProfile)
        protected.PUT("/profile", handlers.UpdateProfile)
        
        taskHandler := handlers.NewTaskHandler(db, hub)
        protected.GET("/tasks", taskHandler.GetTasks)
        protected.POST("/tasks", taskHandler.CreateTask)
        protected.GET("/tasks/:id", taskHandler.GetTask)
        protected.PUT("/tasks/:id", taskHandler.UpdateTask)
        protected.DELETE("/tasks/:id", taskHandler.DeleteTask)
    }
    
    // WebSocket路由
    r.GET("/ws", func(c *gin.Context) {
        websocket.HandleWebSocket(c, hub)
    })
    
    return r
}

前端主应用

frontend/src/main.js
import { createApp } from 'vue'
import { createPinia } from 'pinia'
import { createRouter, createWebHistory } from 'vue-router'
import ElementPlus from 'element-plus'
import 'element-plus/dist/index.css'
import * as ElementPlusIconsVue from '@element-plus/icons-vue'

import App from './App.vue'
import router from './router'
import { useAuthStore } from './stores/auth'
import { useTaskStore } from './stores/tasks'

const app = createApp(App)
const pinia = createPinia()

// 注册所有图标
for (const [key, component] of Object.entries(ElementPlusIconsVue)) {
  app.component(key, component)
}

app.use(pinia)
app.use(router)
app.use(ElementPlus)

// 初始化WebSocket连接
const authStore = useAuthStore()
const taskStore = useTaskStore()

if (authStore.isAuthenticated) {
  taskStore.initializeWebSocket(authStore.token)
}

// 监听登录状态变化
authStore.$subscribe((mutation, state) => {
  if (state.token) {
    taskStore.initializeWebSocket(state.token)
  } else {
    taskStore.cleanupWebSocket()
  }
})

app.mount('#app')

8.3 数据库初始化脚本

backend/init.sql
-- 创建数据库
CREATE DATABASE IF NOT EXISTS taskmanager CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;

-- 使用数据库
USE taskmanager;

-- 创建用户表
CREATE TABLE IF NOT EXISTS users (
    id INT AUTO_INCREMENT PRIMARY KEY,
    username VARCHAR(50) UNIQUE NOT NULL,
    email VARCHAR(100) UNIQUE NOT NULL,
    password VARCHAR(255) NOT NULL,
    created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
    updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
    deleted_at TIMESTAMP NULL DEFAULT NULL,
    INDEX idx_username (username),
    INDEX idx_email (email)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;

-- 创建任务表
CREATE TABLE IF NOT EXISTS tasks (
    id INT AUTO_INCREMENT PRIMARY KEY,
    title VARCHAR(255) NOT NULL,
    description TEXT,
    status ENUM('pending', 'in_progress', 'completed') DEFAULT 'pending',
    priority ENUM('low', 'medium', 'high') DEFAULT 'medium',
    due_date DATETIME NULL DEFAULT NULL,
    user_id INT NOT NULL,
    created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
    updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
    deleted_at TIMESTAMP NULL DEFAULT NULL,
    FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE,
    INDEX idx_user_id (user_id),
    INDEX idx_status (status),
    INDEX idx_created_at (created_at)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;

-- 插入测试用户(密码: 123456)
-- 注意:生产环境请删除此用户
INSERT IGNORE INTO users (username, email, password) VALUES 
('testuser', 'test@example.com', '$2a$10$92IXUNpkjO0rOQ5byMi.Ye4oKoEa3Ro9llC/.og/at2.uheWG/igi');

8.4 部署脚本

deploy.sh
#!/bin/bash

# 部署脚本
set -e

echo "🚀 开始部署任务管理系统..."

# 检查Docker和Docker Compose
echo "📋 检查环境..."
if ! command -v docker &> /dev/null; then
    echo "❌ Docker未安装,请先安装Docker"
    exit 1
fi

if ! command -v docker-compose &> /dev/null; then
    echo "❌ Docker Compose未安装,请先安装Docker Compose"
    exit 1
fi

# 读取环境变量
if [ -f .env ]; then
    export $(cat .env | xargs)
    echo "✅ 环境变量已加载"
else
    echo "⚠️  未找到.env文件,使用默认配置"
fi

# 停止现有服务
echo "🛑 停止现有服务..."
docker-compose down --remove-orphans || true

# 拉取最新镜像
echo "📥 拉取最新镜像..."
docker-compose pull

# 构建镜像
echo "🏗️  构建镜像..."
docker-compose build

# 启动服务
echo "🚀 启动服务..."
docker-compose up -d

# 等待服务启动
echo "⏳ 等待服务启动..."
sleep 30

# 检查服务状态
echo "📊 检查服务状态..."
docker-compose ps

# 健康检查
echo "🏥 执行健康检查..."
if curl -f http://localhost:${SERVER_PORT:-8080}/health > /dev/null 2>&1; then
    echo "✅ 后端服务健康"
else
    echo "❌ 后端服务异常"
    docker-compose logs backend
fi

if curl -f http://localhost:80/ > /dev/null 2>&1; then
    echo "✅ 前端服务健康"
else
    echo "❌ 前端服务异常"
    docker-compose logs frontend
fi

echo "🎉 部署完成!"
echo "📱 访问地址: http://localhost"
echo "🔧 API地址: http://localhost:${SERVER_PORT:-8080}"
echo "📊 查看日志: docker-compose logs -f"
echo "🛑 停止服务: docker-compose down"

8.5 项目README

README.md
# Task Manager - 任务管理系统

一个现代化的全栈任务管理系统,使用Golang + Vue 3构建。

## ✨ 特性

- 🚀 现代化的前后端分离架构
- 🔐 JWT身份认证
- 📱 响应式设计,支持移动端
- ⚡ 实时通知(WebSocket)
- 🐳 Docker容器化部署
- 🎨 优雅的UI设计(Element Plus)
- 📝 完整的CRUD操作
- 🔍 任务状态跟踪
- 📊 优先级管理

## 🛠️ 技术栈

### 后端
- **Golang** 1.23+
- **Gin** - Web框架
- **GORM** - ORM框架
- **JWT** - 身份认证
- **WebSocket** - 实时通信
- **MySQL** - 数据库

### 前端
- **Vue 3** - 渐进式框架
- **Vite** - 构建工具
- **Pinia** - 状态管理
- **Vue Router 4** - 路由管理
- **Element Plus** - UI组件库
- **Axios** - HTTP客户端
- **Socket.io** - WebSocket客户端

## 📦 项目结构

```
task-manager/
├── backend/          # 后端代码
│   ├── cmd/         # 命令入口
│   ├── internal/    # 内部包
│   │   ├── auth/    # 认证相关
│   │   ├── handlers/# 请求处理器
│   │   ├── middleware/# 中间件
│   │   ├── models/  # 数据模型
│   │   └── websocket/# WebSocket
│   ├── pkg/         # 公共包
│   └── Dockerfile   # Docker配置
├── frontend/        # 前端代码
│   ├── src/
│   │   ├── components/# 组件
│   │   ├── views/   # 页面视图
│   │   ├── stores/  # 状态管理
│   │   ├── services/# 服务封装
│   │   └── utils/   # 工具函数
│   └── Dockerfile   # Docker配置
└── docker-compose.yml # 容器编排
```

## 🚀 快速开始

### 环境要求

- Go 1.23+
- Node.js 18+
- MySQL 8.0
- Docker & Docker Compose(可选)

### 本地开发

1. **克隆项目**
```bash
git clone https://github.com/yourusername/task-manager.git
cd task-manager
```

2. **配置环境**
```bash
cp .env.example .env
# 编辑 .env 文件
```

3. **启动后端**
```bash
cd backend
go mod download
go run cmd/server/main.go
```

4. **启动前端**
```bash
cd frontend
npm install
npm run dev
```

5. **访问应用**
- 前端: http://localhost:3000
- 后端: http://localhost:8080

### Docker部署

```bash
# 启动所有服务
docker-compose up -d

# 访问应用
# 前端: http://localhost
# API: http://localhost:8080
```

## 📖 API文档

### 认证相关

#### 用户注册
```http
POST /api/v1/register
Content-Type: application/json

{
  "username": "testuser",
  "email": "test@example.com",
  "password": "password123"
}
```

#### 用户登录
```http
POST /api/v1/login
Content-Type: application/json

{
  "username": "testuser",
  "password": "password123"
}
```

### 任务管理

#### 获取任务列表
```http
GET /api/v1/tasks
Authorization: Bearer {token}
```

#### 创建任务
```http
POST /api/v1/tasks
Authorization: Bearer {token}
Content-Type: application/json

{
  "title": "完成项目文档",
  "description": "编写API文档",
  "priority": "high"
}
```

#### 更新任务
```http
PUT /api/v1/tasks/{id}
Authorization: Bearer {token}
Content-Type: application/json

{
  "title": "完成项目文档",
  "status": "in_progress"
}
```

#### 删除任务
```http
DELETE /api/v1/tasks/{id}
Authorization: Bearer {token}
```

## 🔧 配置说明

### 环境变量

| 变量名 | 说明 | 默认值 |
|--------|------|--------|
| DB_HOST | 数据库主机 | localhost |
| DB_PORT | 数据库端口 | 3306 |
| DB_USER | 数据库用户 | taskuser |
| DB_PASSWORD | 数据库密码 | - |
| DB_NAME | 数据库名 | taskmanager |
| JWT_SECRET | JWT密钥 | - |
| JWT_EXPIRE_HOURS | Token过期时间 | 24 |
| SERVER_PORT | 服务器端口 | 8080 |

## 🧪 测试

### 后端测试
```bash
cd backend
go test ./...
```

### 前端测试
```bash
cd frontend
npm run test
```

## 📱 功能演示

### 用户功能
- ✅ 用户注册和登录
- ✅ JWT身份认证
- ✅ 个人资料管理

### 任务功能
- ✅ 创建任务
- ✅ 编辑任务
- ✅ 删除任务
- ✅ 任务状态管理(待处理/进行中/已完成)
- ✅ 任务优先级设置
- ✅ 实时通知

## 🚀 部署

### 使用脚本部署
```bash
chmod +x deploy.sh
./deploy.sh
```

### 手动部署
```bash
# 构建和启动
docker-compose up -d

# 查看状态
docker-compose ps

# 查看日志
docker-compose logs -f
```

## 🔒 安全建议

1. 使用强密码
2. 配置HTTPS
3. 定期更新依赖
4. 使用防火墙
5. 定期备份数据
6. 监控服务器状态

## 📝 许可证

MIT License

## 🤝 贡献

欢迎提交Issue和Pull Request!

## 📧 联系方式

- Email: your-email@example.com
- GitHub: [@yourusername](https://github.com/yourusername)

---

⭐ 如果这个项目对你有帮助,请给个Star!

获取完整源码

完整的项目源码已包含在本教程中,您可以:

  • • 按照章节逐步构建项目
  • • 复制完整的代码文件
  • • 使用Docker直接部署
  • • 根据需要自定义功能