Skip to content

第39章:实战项目一 —— RESTful API 用户管理系统 ​

本章目标 ​

学完本章后,你将能够:

  1. 搭建一个完整的 Go Web 项目
  2. 使用 Gin + GORM 构建 RESTful API
  3. 实现用户注册、登录、增删改查
  4. 使用 Hook 实现密码加密
  5. 使用 Scope 封装通用查询

39.1 项目结构 ​

user-management/
├── main.go              # 入口
├── config/
│   └── config.go        # 数据库配置
├── models/
│   └── user.go          # 用户模型 + Hook
├── handlers/
│   └── user.go          # HTTP 处理器
├── middleware/
│   └── error.go         # 错误处理中间件
└── scopes/
    └── paginate.go      # 分页 Scope

39.2 模型定义 ​

go
package models

import (
	"time"
	"golang.org/x/crypto/bcrypt"
	"gorm.io/gorm"
)

type User struct {
	ID        uint           `gorm:"primaryKey" json:"id"`
	Username  string         `gorm:"type:varchar(50);uniqueIndex;not null" json:"username"`
	Password  string         `gorm:"type:varchar(255);not null;->:false" json:"-"`
	RealName  string         `gorm:"type:varchar(50)" json:"real_name"`
	Email     string         `gorm:"type:varchar(255);uniqueIndex" json:"email"`
	Phone     string         `gorm:"type:varchar(20)" json:"phone"`
	Status    string         `gorm:"type:varchar(20);default:'active'" json:"status"`
	CreatedAt time.Time      `json:"created_at"`
	UpdatedAt time.Time      `json:"updated_at"`
	DeletedAt gorm.DeletedAt `gorm:"index" json:"-"`
}

// 密码加密 Hook
func (u *User) BeforeCreate(tx *gorm.DB) error {
	if u.Password != "" {
		hashed, err := bcrypt.GenerateFromPassword([]byte(u.Password), bcrypt.DefaultCost)
		if err != nil {
			return err
		}
		u.Password = string(hashed)
	}
	return nil
}

// 登录验证
func (u *User) CheckPassword(password string) bool {
	err := bcrypt.CompareHashAndPassword([]byte(u.Password), []byte(password))
	return err == nil
}

39.3 API 设计 ​

go
package handlers

import (
	"net/http"
	"strconv"
	"github.com/gin-gonic/gin"
	"gorm.io/gorm"
)

type UserHandler struct {
	db *gorm.DB
}

// GET /api/users?page=1&page_size=10&status=active
func (h *UserHandler) List(c *gin.Context) {
	var users []User
	var total int64

	page, _ := strconv.Atoi(c.DefaultQuery("page", "1"))
	pageSize, _ := strconv.Atoi(c.DefaultQuery("page_size", "10"))
	status := c.Query("status")

	query := h.db.Model(&User{})
	if status != "" {
		query = query.Where("status = ?", status)
	}

	query.Count(&total)
	query.Scopes(Paginate(page, pageSize)).Order("id desc").Find(&users)

	c.JSON(http.StatusOK, gin.H{
		"data": users,
		"meta": gin.H{
			"total":     total,
			"page":      page,
			"page_size": pageSize,
		},
	})
}

// POST /api/users
func (h *UserHandler) Create(c *gin.Context) {
	var input struct {
		Username string `json:"username" binding:"required"`
		Password string `json:"password" binding:"required,min=6"`
		RealName string `json:"real_name"`
		Email    string `json:"email" binding:"required,email"`
		Phone    string `json:"phone"`
	}
	if err := c.ShouldBindJSON(&input); err != nil {
		c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
		return
	}

	user := User{
		Username: input.Username,
		Password: input.Password,  // Hook 自动加密
		RealName: input.RealName,
		Email:    input.Email,
		Phone:    input.Phone,
	}

	if err := h.db.Create(&user).Error; err != nil {
		c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
		return
	}

	c.JSON(http.StatusCreated, gin.H{"data": user})
}

// GET /api/users/:id
func (h *UserHandler) Get(c *gin.Context) {
	id := c.Param("id")
	var user User
	if err := h.db.First(&user, id).Error; err != nil {
		c.JSON(http.StatusNotFound, gin.H{"error": "用户不存在"})
		return
	}
	c.JSON(http.StatusOK, gin.H{"data": user})
}

// PUT /api/users/:id
func (h *UserHandler) Update(c *gin.Context) {
	id := c.Param("id")
	var input map[string]interface{}
	c.ShouldBindJSON(&input)

	// 不要从 API 更新密码(密码更新走专门接口)
	delete(input, "password")

	if err := h.db.Model(&User{}).Where("id = ?", id).Updates(input).Error; err != nil {
		c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
		return
	}
	c.JSON(http.StatusOK, gin.H{"message": "更新成功"})
}

// DELETE /api/users/:id
func (h *UserHandler) Delete(c *gin.Context) {
	id := c.Param("id")
	var user User
	if err := h.db.First(&user, id).Error; err != nil {
		c.JSON(http.StatusNotFound, gin.H{"error": "用户不存在"})
		return
	}
	h.db.Delete(&user)  // 软删除
	c.JSON(http.StatusOK, gin.H{"message": "删除成功"})
}

39.4 路由注册 ​

go
package main

import (
	"github.com/gin-gonic/gin"
	"gorm.io/driver/mysql"
	"gorm.io/gorm"
	"gorm.io/gorm/logger"
)

func main() {
	dsn := "root:123456@tcp(127.0.0.1:3306)/user_management?charset=utf8mb4&parseTime=True&loc=Local"
	db, _ := gorm.Open(mysql.Open(dsn), &gorm.Config{
		Logger: logger.Default.LogMode(logger.Info),
	})
	db.AutoMigrate(&User{})

	handler := &UserHandler{db: db}
	r := gin.Default()

	api := r.Group("/api")
	{
		api.GET("/users", handler.List)
		api.POST("/users", handler.Create)
		api.GET("/users/:id", handler.Get)
		api.PUT("/users/:id", handler.Update)
		api.DELETE("/users/:id", handler.Delete)
	}

	r.Run(":8080")
}

39.5 测试 API ​

bash
# 创建用户
curl -X POST http://localhost:8080/api/users \
  -H "Content-Type: application/json" \
  -d '{"username":"zhangsan","password":"123456","email":"zs@example.com"}'

# 列表查询
curl http://localhost:8080/api/users?page=1&page_size=10

# 获取详情
curl http://localhost:8080/api/users/1

# 更新
curl -X PUT http://localhost:8080/api/users/1 \
  -H "Content-Type: application/json" \
  -d '{"real_name":"张三","phone":"13800138000"}'

# 删除
curl -X DELETE http://localhost:8080/api/users/1

本章小结 ​

本项目综合运用了:模型定义、Hook(密码加密)、CRUD、软删除、分页查询、Gin 框架集成。

练习题 ​

  1. 重启项目,完成上面所有 API 的测试。
  2. 添加"用户登录"接口(POST /api/login),验证用户名和密码。
  3. 添加"按关键词搜索"功能,支持按用户名和邮箱模糊搜索。
  4. 添加"按创建时间范围"查询的 API 参数。

把零散知识,整理成可以反复走的路。