Gin 项目实现管理员用户模块 CRUD

前篇是将的是一个应用模块, 设计 文件上传, oss服务 , 数据库事务处理 等常规通用技能, 以及一些逻辑封装, 让其更清晰.

本篇来补充一个之前就应该编写的管理员用户模块, 因为其涉及到了图片相关, 所以放后面了, 这里的知识点就是对用户进行新增, 密码哈希加密 等常规处理, 没有新的东西, 复习一下增删改查而已.

项目目录

我是在 mac 终端下, 用命令行来模拟一下 tree 的功能, 体现目录结构.

find . -path './.git' -prune -o -print | sed -e 's;[^/]*/;|____;g;s;____|; |;g'
.
|____go.mod
|____internal
| |____middleware
| | |____auth.go
| |____db
| | |____db.go
|____go.sum
|____api
| |____routers.go
| |____handlers
| | |____role
| | | |____views.go
| | |____auth
| | | |____views.go
| | |____user
| | | |____menu.go
| | | |____views.go
| | |____image
| | | |____dealUploadFile.go
| | | |____views.go
| | | |____image.go
| | |____notice
| | | |____views.go
| | |____rule
| | | |____views.go
| | |____manager
| | | |____views.go
|____.vscode
| |____extensions.json
|____tmp
| |____runner-build
|____main.go
|____pkg
| |____utils
| | |____response.go
| | |____tools.go
| | |____imageOss.go
| |____jwtt
| | |____jwt.go
cj@mini gin-shop-admin % 

再写逻辑之前也是先提一下优化的地方, 主要在工具函数

明文密码的哈希和验证

就用户的密码存储一定要进行加密处理, 搞明文就出大事了.

pkg/utils/tools.go

// 明文明码进行哈希加密 bcrypt
func GetHashPassword(password string) (string, error) {
	bytes, err := bcrypt.GenerateFromPassword([]byte(password), bcrypt.DefaultCost)
	return string(bytes), err
}

// 校验明文密码和哈希值是否一致
func CheckPasswordHash(password, hash string) bool {
	err := bcrypt.CompareHashAndPassword([]byte(hash), []byte(password))
	return err == nil
}


修正统一参数校验的逻辑

之前的验证完全用 AI写的, 看能跑就没管了, 后来仔细看才发现又大问题 逻辑冗余, 重复验证等问题突出.

// 验证和绑定: uri, request, form, json ...等前端参数
func BindAndValidate(c *gin.Context, request interface{}) error {
	// 支持多种验证
	// Step 1: 绑定 URI 参数
	if err := c.ShouldBindUri(request); err != nil {
		return err
	}

	// Step 2: 显式绑定 Query 参数
	if err := c.ShouldBindQuery(request); err != nil {
		return err
	}

	// Step 3: 自动绑定 JSON / Form 数据
	if err := c.ShouldBind(request); err != nil {
		return err
	}

	// Step 4: 验证结构体
	validate := validator.New()
	if err := validate.Struct(request); err != nil {
		return err
	}

	return nil
}


// 随机生成 uuid, 除了图片命名用, 可以用作对外暴露的 user_id
func GetUUID() string {
	newUuid, _ := uuid.NewRandom()
	return newUuid.String()
}

然后开始直接干吧.

数据库表

本篇涉及 managerole 表, 即管理用户信息, 管理员所属角色等, 目前管理员和角色是一对一的关系.

-- 管理员-用户表
drop table if exists `manager`; 
create table `manager` (
  id int not null auto_increment comment '自增id',
  user_id  varchar(36) not null comment '对外暴露的uuid',
  role_id  int not null default 0 comment '角色',
  is_super tinyint not null default 0 comment '是否为超级管理员',
  status   tinyint not null default 0 comment '0禁用, 1启用',
  username varchar(50) unique not null comment '用户名',
  password varchar(255) not null comment '用户哈希密码',
  phone    varchar(20)  not null comment '手机号',
  avatar   varchar(255) default null comment '头像地址',
  create_time datetime default current_timestamp comment '创建时间',
  update_time datetime default current_timestamp on update current_timestamp comment '更新时间',
  primary key (`id`)
);


-- 管理员-角色表
drop table if exists role;
create table role (
  id int not null auto_increment comment '自增id'
  , name varchar(50) unique not null comment '角色名称'
  , descr varchar(255) default null comment '角色描述'
  , status int not null default 1 comment '角色状态'
  , create_time datetime default current_timestamp comment '创建时间'
  , update_time datetime default current_timestamp on update current_timestamp comment '更新时间'
  , primary key (`id`)
);

二者通过 manager.role_id = role.id 关联即可.

模块路由

package api

import (
	"github.com/gin-gonic/gin"
	"youge.com/api/handlers/manager"
	"youge.com/internal/middleware"
)

// 统一注册入口

	// 用户管理模块

	// 公告模块

	// 菜单权限管理模块

	// 角色管理模块

	// 图库管理模块

		// 图库相关

		// 图片相关


	// 管理员管理模块
	managerGrop := r.Group("/api/manager")
	// ruleGroup.Use(middleware.JWT())
	{
		managerGrop.GET("/manager", manager.GetManagerList)
		managerGrop.POST("/manager", manager.CrateManager)
		managerGrop.PUT("/manager/:id", manager.UpdateManager)
		managerGrop.PUT("/manager/:id/update_status", manager.UpdateManagerStatus)
		managerGrop.DELETE("/manager/:id/delete", manager.DeleteManager)
	}

}

对应的逻辑都放在 /api/handlers/manager/manager.go 里面, 也是不做分层, 全部搞一起.

GET 查询管理员接口

前端: http://localhost:8000/api/manager/manager?page=10&limit=10&keyword=test

后端: /api/handlers/manager/manager.go => GetManagerList

package manager

import (
	"github.com/gin-gonic/gin"
	"youge.com/internal/db"
	"youge.com/pkg/utils"
)

// 管理员信息表
type Manager struct {
	Id       int    `db:"id" json:"id"`
	UserId   string `db:"user_id" json:"userId"`
	RoleId   int    `db:"role_id" json:"role_id"`
	IsSuper  int    `db:"is_super" json:"isSuper"`
	Status   int    `db:"status" json:"status"`
	Username string `db:"username" json:"username"`
	Phone    string `db:"phone" json:"phone"`
	Avatar   string `db:"avatar" json:"avatar"`
	RoleName string `db:"role_name" json:"roleName"`
}

// 角色数据
type Role struct {
	Id   int    `db:"id" json:"id"`
	Name string `db:"name" json:"name"`
}

// 接口: 获取管理员用户信息
func GetManagerList(c *gin.Context) {
	// 查询参数验证
	var req struct {
		Page    int    `form:"page" validate:"omitempty,min=1,max=1000"`
		Limit   int    `form:"limit" validate:"omitempty,min=10,max=1000"`
		KeyWord string `form:"keyword" validate:"omitempty"`
	}

	if err := utils.BindAndValidate(c, &req); err != nil {
		utils.BadRequest(c, "请求参数错误")
		return
	}
	// 设置默认值
	req.Page, req.Limit = utils.SetDefaultPagination(req.Page, req.Limit)

	// 用户名模糊搜索
	keyword := req.KeyWord
	if keyword == "" {
		keyword = "%"
	} else {
		keyword = "%" + keyword + "%"
	}

	// 分页查询
	offset := (req.Page - 1) * req.Limit

	sql := `
	select 
		m.id 
		, coalesce(m.user_id, '') as user_id
		, coalesce(m.role_id, 0)  as role_id 
		, m.is_super
		, m.status 
		, m.username 
		, coalesce(m.phone, '') as phone
		, coalesce(m.avatar, '') as avatar 
		, coalesce(r.name, '')   as role_name
	from manager m 
	left join role r on m.role_id = r.id
	where m.username like ?
	limit ?, ?;
	`
	var managerList []Manager
	if err := db.Query(&managerList, sql, keyword, offset, req.Limit); err != nil {
		utils.BadRequest(c, "获取管理员数据失败")
		return
	}

	// 查询总条数
	var total int
	// 这个sql 肯定不会错, 除非表不在, 不校验了
	countSql := "select count(*) as total from manager where username like ?;"
	db.Get(&total, countSql, keyword)

	var roles []Role
	if err := db.Query(&roles, "select id, name from role;"); err != nil {
		utils.BadRequest(c, "获取角色数据失败")
		return
	}

	utils.Success(c, gin.H{
		"list":       managerList,
		"totalCount": total,
		"roles":      roles,
	})
}

这里的注意点

  • 分页查询 offset + limit , 数据量小就直接写了, 等量大换个方式.
  • 模糊查询 like
  • 业务细节 查询用户不要返回密码, 新增用户要传密码后端进行加密

POST 新增管理员接口

前端: http://localhost:8000/api/manager/manager

前端 json:

{
    "role_id": 1,
    "isSuper": 1,
    "status": 1,
    "avatar": "",
    "username": "test",
    "password": "test",
    "phone": "16603047643"
}

后端: api/handlers/manager/manager.go => CreateManager

// 接口: 新增管理员
func CrateManager(c *gin.Context) {
	// 验证前端 body 传的 json 参数
	var req struct {
		RoleId   int    `json:"role_id" validate:"required,min=1"`
		IsSuper  int    `json:"isSuper" validate:"oneof=0 1"`
		Status   int    `json:"status" validate:"oneof=0 1"`
		Username string `json:"username" validate:"required"`
		Password string `jons:"password" validate:"required"`
		Phone    string `json:"phone" validate:"required"`
		Avatar   string `json:"avatar" validate:"omitempty"`
	}

	if err := utils.BindAndValidate(c, &req); err != nil {
		utils.BadRequest(c, "请求体参数错误")
		return
	}

	// 对用户密码进行哈希加密
	hashedPassword, _ := utils.GetHashPassword(req.Password)
	// 对外暴露 user_id, 用 uuid 来自动生成
	userId := utils.GetUUID()

	// 新增数据
	sql := `
	insert into manager
		(user_id, role_id, is_super, status, username, password, phone, avatar)
		values (?, ?, ?, ?, ?, ?, ?, ?);
	`
	rows, err := db.Exec(sql, userId, req.RoleId, req.IsSuper, req.Status, req.Username, hashedPassword, req.Phone, req.Avatar)

	if err != nil {
		utils.BadRequest(c, "新增管理员失败")
		return
	}

	utils.Success(c, gin.H{
		"affectedRows": rows,
	})
}

这里要注意的是 一定要将明文密码做加密存储, 顺带生成一个uuid对外暴露 .

然后是参数验证, validate:"require,oneof=0 1" 是有问题的, 就如果不传, 会自动处理为零值 然后其实后端就不知道到底有没有传值了, 方案有2个:

  • 方案1: 转为指针类型, 然后用的时候解指针即可
  • 方案2: 直接不处理, 改成 validate:"oneof=0 1", 前端不传就是0, 传了只能是 0, 1 不也刚好吗

PUT 修改管理员接口

前端: http://localhost:8000/api/manager/manager/2

前端请求体 json :

{
    "role_id": 1,
    "isSuper": 0,
    "status": 1,
    "avatar": "",
    "username": "test",
    "password": "test",
    "phone": "16603041234"
}

后端: api/handlers/manager/manager.go => UpdateManager

// 接口: 修改管理员信息
func UpdateManager(c *gin.Context) {
	// 验证路径id参数和请求体json 数据
	var req struct {
		Id       int    `uri:"id" validate:"required,min=1"`
		RoleId   int    `json:"role_id" validate:"required,min=1"`
		IsSuper  int    `json:"isSuper" validate:"oneof=0 1"`
		Status   int    `json:"status" validate:"oneof=0 1"`
		Username string `json:"username" validate:"required"`
		Password string `jons:"password" validate:"required"`
		Phone    string `json:"phone" validate:"required"`
		Avatar   string `json:"avatar" validate:"omitempty"`
	}

	if err := utils.BindAndValidate(c, &req); err != nil {
		utils.BadRequest(c, "请求参数错误")
		return
	}

	// 将明文密码加密
	hashedPassword, _ := utils.GetHashPassword(req.Password)

	// 修改数据
	sql := `
	update manager set 
		role_id = ?, 
		is_super = ?, 
		status = ?, 
		username = ?, 
		password = ?, 
		phone = ?, 
		avatar = ?
	where id = ?;
	`
	rows, err := db.Exec(sql, req.RoleId, req.IsSuper, req.Status, req.Username, hashedPassword, req.Phone, req.Avatar, req.Id)
	if err != nil {
		utils.BadRequest(c, "修改管理员数据错误")
		return
	}

	utils.Success(c, gin.H{
		"affectedRows": rows,
	})
}

只要别传错参数位置就好了, 后续可以优化为一个结构体来弄, 但也可以这样.

PUT 修改管理员状态接口

前端: http://localhost:8000/api/manager/manager/2/update_status

前端请求体 json :

{
    "status": 1,
}

后端: api/handlers/manager/manager.go => UpdateManagerStatus

// 接口: 更改管理员状态
func UpdateManagerStatus(c *gin.Context) {
	// 验证路径参数和请求体 json 参数
	var req struct {
		Id     int `uri:"id" validate:"required,min=1"`
		Status int `json:"status" validate:"oneof=0 1"`
	}
	if err := utils.BindAndValidate(c, &req); err != nil {
		utils.BadRequest(c, "请求参数错误")
		return
	}

	// 修改状态
	sql := "update manager set status = ? where id = ?;"
	rows, err := db.Exec(sql, req.Status, req.Id)
	if err != nil {
		utils.BadRequest(c, "修改管理员状态失败")
		return
	}

	utils.Success(c, gin.H{
		"affectedRows": rows,
	})
}

这种就是属于逻辑简单, 但流程一点也不能少.

DELETE 删除管理员接口

前端: http://localhost:8000/api/manager/manager/4/delete

后端: api/handlers/manager/manager.go => DeleteManager

// 接口: 删除管理员
func DeleteManager(c *gin.Context) {
	// 获取和校验路径参数 id
	var req struct {
		Id int `uri:"id" validate:"required,min=1"`
	}
	if err := utils.BindAndValidate(c, &req); err != nil {
		utils.BadRequest(c, "请求参数错误")
		return
	}

	// 删除数据
	rows, err := db.Exec("delete from manager where id = ?", req.Id)
	if err != nil {
		utils.BadRequest(c, "删除管理员失败")
		return
	}

	utils.Success(c, gin.H{
		"affectedRows": rows,
	})
}

常规操作, 没啥.

至此, 管理员用户的 crud 接口就写完了, 没啥重要的技能点, 主要还是以熟悉为主, 下篇尝试将这个简单模块给进行拆分为 mvc 试一试.

posted @ 2025-06-04 23:19  致于数据科学家的小陈  阅读(33)  评论(0)    收藏  举报