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()
}
然后开始直接干吧.
数据库表
本篇涉及 manage 和 role 表, 即管理用户信息, 管理员所属角色等, 目前管理员和角色是一对一的关系.
-- 管理员-用户表
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 试一试.

浙公网安备 33010602011771号