Skip to content

Gin 实战 7:用户 CRUD 接口

本章从用户模块开始,把 Model、Repository、Service、Handler 串起来,实现后台系统最常见的列表、新增、详情、编辑、删除接口。

1. 用户模型

创建 internal/system/user/model/user.go

go
package model

import "time"

type User struct {
    // ID 使用自增主键。
    ID        int64     `gorm:"primaryKey" json:"id"`
    // username 在后台系统里通常要求唯一。
    Username  string    `gorm:"size:50;not null;uniqueIndex" json:"username"`
    Nickname  string    `gorm:"size:50" json:"nickname"`
    // 密码不返回给前端,所以 json tag 使用 "-"。
    Password  string    `gorm:"size:100;not null" json:"-"`
    // status 用于控制账号启用/禁用。
    Status    int       `gorm:"default:1" json:"status"`
    CreatedAt time.Time `json:"createdAt"`
    UpdatedAt time.Time `json:"updatedAt"`
}

// TableName 明确指定表名,避免 GORM 自动复数表名不符合项目规范。
func (User) TableName() string {
    return "sys_user"
}

2. 请求与响应模型

go
type UserQuery struct {
    // form tag 用于绑定 URL 查询参数:?pageNum=1&pageSize=10。
    PageNum  int    `form:"pageNum"`
    PageSize int    `form:"pageSize"`
    Keywords string `form:"keywords"`
}

type UserCreateForm struct {
    // json tag 用于绑定请求体 JSON。
    Username string `json:"username" binding:"required"`
    Nickname string `json:"nickname"`
    Password string `json:"password" binding:"required"`
    Status   int    `json:"status"`
}

type UserUpdateForm struct {
    // 编辑接口通过路径参数拿 id,请求体只放允许修改的字段。
    Nickname string `json:"nickname"`
    Password string `json:"password"`
    Status   int    `json:"status"`
}

实体、查询参数、表单参数分开,后面字段变多时不会互相污染。

3. Repository

创建 internal/system/user/repository/repository.go

go
package repository

import (
    "youlai-gin/internal/common/database"
    "youlai-gin/internal/system/user/model"
)

type Repository struct{}

func NewRepository() *Repository {
    return &Repository{}
}

// Page 只负责数据库查询,不处理 HTTP 响应。
func (r *Repository) Page(query model.UserQuery) ([]model.User, int64, error) {
    // 先拿到查询构造器,后面按条件继续追加 Where、分页和排序。
    db := database.DB.Model(&model.User{})

    // 关键字不为空时,按用户名或昵称模糊查询。
    if query.Keywords != "" {
        like := "%" + query.Keywords + "%"
        db = db.Where("username LIKE ? OR nickname LIKE ?", like, like)
    }

    // 分页接口通常需要同时返回列表和总数。
    var total int64
    if err := db.Count(&total).Error; err != nil {
        return nil, 0, err
    }

    // 给分页参数兜底,避免 pageNum/pageSize 为空时产生异常分页。
    if query.PageNum <= 0 {
        query.PageNum = 1
    }
    if query.PageSize <= 0 {
        query.PageSize = 10
    }

    var list []model.User
    // Offset 计算跳过多少条,Limit 限制本页条数。
    err := db.Offset((query.PageNum - 1) * query.PageSize).
        Limit(query.PageSize).
        Order("id desc").
        Find(&list).Error
    return list, total, err
}

// Create 只负责把用户写入数据库。
func (r *Repository) Create(user *model.User) error {
    return database.DB.Create(user).Error
}

func (r *Repository) GetByID(id int64) (*model.User, error) {
    var user model.User
    // First 会按主键查询一条记录;查不到时返回 gorm.ErrRecordNotFound。
    if err := database.DB.First(&user, id).Error; err != nil {
        return nil, err
    }
    return &user, nil
}

func (r *Repository) Update(user *model.User) error {
    // Save 会根据主键决定更新哪条记录。
    return database.DB.Save(user).Error
}

func (r *Repository) Delete(id int64) error {
    // Delete 默认按主键删除。
    return database.DB.Delete(&model.User{}, id).Error
}

4. Service

go
type Service struct {
    // Service 依赖 Repository 完成数据读写。
    repo *repository.Repository
}

func NewService(repo *repository.Repository) *Service {
    return &Service{repo: repo}
}

func (s *Service) Page(query model.UserQuery) ([]model.User, int64, error) {
    return s.repo.Page(query)
}

func (s *Service) Create(form model.UserCreateForm) error {
    // Service 把表单参数转换为数据库实体。
    // 后续用户名唯一校验、密码加密、事务都放在这一层。
    user := &model.User{
        Username: form.Username,
        Nickname: form.Nickname,
        Password: form.Password,
        Status:   form.Status,
    }
    return s.repo.Create(user)
}

func (s *Service) GetByID(id int64) (*model.User, error) {
    return s.repo.GetByID(id)
}

func (s *Service) Update(id int64, form model.UserUpdateForm) error {
    user, err := s.repo.GetByID(id)
    if err != nil {
        return err
    }

    // 只更新允许前端修改的字段,避免把主键、创建时间等字段误改掉。
    user.Nickname = form.Nickname
    user.Status = form.Status
    if form.Password != "" {
        user.Password = form.Password
    }
    return s.repo.Update(user)
}

func (s *Service) Delete(id int64) error {
    return s.repo.Delete(id)
}

Service 负责业务规则,Repository 只负责数据访问。密码加密、用户名唯一校验会在登录认证章节继续完善。

5. Handler

下面的 Handler 会解析路径参数,文件顶部需要引入 strconv

go
type Handler struct {
    // Handler 只依赖 Service,不直接访问数据库。
    service *service.Service
}

func NewHandler(service *service.Service) *Handler {
    return &Handler{service: service}
}

func (h *Handler) Page(c *gin.Context) {
    var query model.UserQuery
    // ShouldBindQuery 会把 URL 查询参数绑定到结构体。
    if err := c.ShouldBindQuery(&query); err != nil {
        c.JSON(400, gin.H{"msg": "参数错误"})
        return
    }

    // Handler 调用 Service,不关心底层 GORM 查询细节。
    list, total, err := h.service.Page(query)
    if err != nil {
        c.JSON(500, gin.H{"msg": "查询失败"})
        return
    }

    // 这里先直接返回,后面统一响应章节会替换为 response.Page。
    c.JSON(200, gin.H{"list": list, "total": total})
}

func (h *Handler) Create(c *gin.Context) {
    var form model.UserCreateForm
    // ShouldBindJSON 会把请求体 JSON 绑定到 UserForm。
    if err := c.ShouldBindJSON(&form); err != nil {
        c.JSON(400, gin.H{"msg": "参数错误"})
        return
    }

    if err := h.service.Create(form); err != nil {
        c.JSON(500, gin.H{"msg": "新增失败"})
        return
    }
    c.JSON(200, gin.H{"msg": "新增成功"})
}

func (h *Handler) Get(c *gin.Context) {
    // 路径参数来自 /users/:id。
    id, err := strconv.ParseInt(c.Param("id"), 10, 64)
    if err != nil {
        c.JSON(400, gin.H{"msg": "用户 ID 无效"})
        return
    }

    user, err := h.service.GetByID(id)
    if err != nil {
        c.JSON(404, gin.H{"msg": "用户不存在"})
        return
    }
    c.JSON(200, user)
}

func (h *Handler) Update(c *gin.Context) {
    id, err := strconv.ParseInt(c.Param("id"), 10, 64)
    if err != nil {
        c.JSON(400, gin.H{"msg": "用户 ID 无效"})
        return
    }

    var form model.UserUpdateForm
    if err := c.ShouldBindJSON(&form); err != nil {
        c.JSON(400, gin.H{"msg": "参数错误"})
        return
    }

    if err := h.service.Update(id, form); err != nil {
        c.JSON(500, gin.H{"msg": "更新失败"})
        return
    }
    c.JSON(200, gin.H{"msg": "更新成功"})
}

func (h *Handler) Delete(c *gin.Context) {
    id, err := strconv.ParseInt(c.Param("id"), 10, 64)
    if err != nil {
        c.JSON(400, gin.H{"msg": "用户 ID 无效"})
        return
    }

    if err := h.service.Delete(id); err != nil {
        c.JSON(500, gin.H{"msg": "删除失败"})
        return
    }
    c.JSON(200, gin.H{"msg": "删除成功"})
}

这时先能跑通 CRUD。统一响应和错误处理放到后续章节治理。

6. 注册用户路由

go
func RegisterRoutes(r *gin.RouterGroup) {
    // 先手动组装依赖:Repository -> Service -> Handler。
    // 下一章会用 Wire 生成这段代码。
    repo := repository.NewRepository()
    svc := service.NewService(repo)
    h := handler.NewHandler(svc)

    // 最终路径为 /api/v1/users。
    users := r.Group("/users")
    users.GET("", h.Page)
    users.POST("", h.Create)
    users.GET("/:id", h.Get)
    users.PUT("/:id", h.Update)
    users.DELETE("/:id", h.Delete)
}

下一章引入 Wire,把这些手写依赖组装收敛起来。

基于 MIT 许可发布 · 如需部署协助或二开定制,请查看 支持与合作