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,把这些手写依赖组装收敛起来。
