Gin 实现角色管理的 CRUD 接口
前面实现了 菜单权限 的增删改查, 主要的难点在于 层级树 的处理, 即本质上是对 SQL 中的 自关联, 子查询 的掌握, 只要转变了编程思维, 从面向过程, 对象转为面向集合, 就很轻松.
本篇要实现的一个 角色模块, 也是和前两的 RBAC 权限一起的, 这次的难点是要按前端方便处理的方式组合数据, 就会涉及到 SQL 处理 json 的一些函数, 还有在新增 角色-权限 关系时要注意组合的 唯一性, 数据库和逻辑层面都应该做一些处理, 整体难度不大, 写着玩而已.
项目目录
我是在 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
| | |____notice
| | | |____views.go
| | |____rule
| | | |____views.go
|____.vscode
| |____extensions.json
|____tmp
| |____runner-build
|____main.go
|____pkg
| |____utils
| | |____response.go
| | |____tools.go
| |____jwtt
| | |____jwt.go
cj@mini gin-shop-admin %
直接开干!
数据库表
主要设计两个表, role 和 role_rule
-- 管理员-角色表
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`)
);
-- 管理员-角色-权限表
drop table if exists `role_rule`;
create table `role_rule`(
`id` int not null auto_increment primary key comment '自增id'
, `role_id` int default null comment '角色id'
, `rule_id` int default null comment '权限id'
, create_time datetime default current_timestamp
, update_time datetime default current_timestamp on update current_timestamp
, unique (role_id, rule_id)
);
模块路由
api/routers.go
package api
import (
"github.com/gin-gonic/gin"
"youge.com/api/handlers/role"
"youge.com/internal/middleware"
)
// 统一注册入口
func RegisterAllRouters(r *gin.Engine){
// 登录认证模块
// 用户管理模块
// 公告模块
// 菜单权限管理模块
// 角色管理模块
roleGroup := r.Group("api/role")
// ruleGroup.Use(middleware.JWT())
{
// 获取角色, 分页
roleGroup.GET("/role", role.GetRoleList)
// 新增角色
roleGroup.POST("/role", role.CreateRole)
// 修改角色, 状态
roleGroup.PUT("/role/:id", role.UpdateRole)
roleGroup.PUT("/role/:id/update_status", role.UpdateRoleStatus)
// 删除角色
roleGroup.DELETE("/role/:id/delete", role.DeleteRole)
// 设置角色-权限
roleGroup.POST("/role/set_rules", role.SetRoleRule)
}
// ... 更多模块
}
对应的接口逻辑实现都放在 /api/handlers/role/views.go 里面, 不做分层了哈, 直接干.
GET 查询角色接口
前端: http://localhost:8000/api/role/role?page=1&limit=10
后端: /api/handlers/role/views.go => GetRoleList
package role
import (
"encoding/json"
"strconv"
"strings"
"github.com/gin-gonic/gin"
"youge.com/internal/db"
"youge.com/pkg/utils"
)
type RoleRow struct {
ID int `db:"role_id" json:"id"`
Name string `db:"name" json:"name"`
Descr string `db:"descr" json:"descr"`
Status int `db:"status" json:"status"`
Rules json.RawMessage `json:"rules"` // 先作为 RawMessage 接收
}
// 接口: 获取角色信息
func GetRoleList(c *gin.Context) {
// 验证请求参数, 分页查询的 page, limit
var req struct {
Page int `form:"page" binding:"omitempty,min=1"`
Limit int `form:"limit" binding:"omitempty,min=1,max=1000"`
}
// 绑定并验证查询参数
if err := c.ShouldBindQuery(&req); err != nil {
utils.BadRequest(c, "请求参数错误")
return
}
// 设置默认值, ?page=1&limit=10
if req.Page == 0 {
req.Page = 1
}
if req.Limit == 0 {
req.Limit = 10
}
// 分页查询数据
// 页面偏移量
offset := (req.Page - 1) * req.Limit
mysql := `
select
a.id as role_id
, a.name
, a.descr
, a.status
, coalesce(
(
select json_arrayagg(
json_object(
-- 权限父id
'id', b.rule_id,
'pivot', json_object(
-- 角色-权限表的主键id, 没啥用
'id', b.id,
'role_id', b.role_id,
'rule_id', b.rule_id
)
)
)
from role_rule b
where a.id = b.role_id
),
'[]'
) as rules
from role a
limit ?, ?
`
var roleRows []RoleRow
if err := db.Query(&roleRows, mysql, offset, req.Limit); err != nil {
utils.Error(c, 500, "", err)
return
}
// 总角色数给前端作为分页
var total int
db.Get(&total, "select count(*) as total from role;")
utils.Success(c, gin.H{
"list": roleRows,
"totalCount": total,
})
}
分页查询用的是基础款, offset + limit, 然后这里用到 Mysql 的 json 函数 json_object() 和 json_arrayagg() 前者是将 成对的 key-value 组装为 json 对象, 自然嵌套也能实现. 后者是将其聚合为一个数组.
我之前用的数据库不是 Mysql8, 不支持 json 函数, 就用 concat() 拼接值给到后端, 然后再进行拼装的, 相对繁琐一些, 性能应该没有在数据库层面处理高, 就改成现在这样了.
POST 新增角色接口
前端: http://localhost:8000/api/role/role
前端请求体 json :
{
"name": "测试角色",
"descr": "",
"Status": 1
}
后端: /api/handlers/rule/views.go => CreateRole
// 接口: 新增角色
func CreateRole(c *gin.Context) {
// 验证前端 body 传过来的 json 数据
var req struct {
Name string `json:"name" validate:"required"`
Descr string `json:"descr" validate:"omitempty"`
Status int `json:"status" validate:"oneof=0 1"`
}
if err := utils.BindAndValidate(c, &req); err != nil {
utils.Error(c, 400, "", err)
return
}
// 新增数据
mysql := "insert into role(name, descr, status) values (?, ?, ?)"
rows, err := db.Exec(mysql, req.Name, req.Descr, req.Status)
if err != nil {
utils.BadRequest(c, "新增失败, 可能是角色重复啦")
return
}
utils.Success(c, gin.H{
"affectedRows": rows,
})
}
只要参数验证 ok 就没啥问题.
PUT 修改角色接口
前端: http://localhost:8000/api/role/role/10
后端: /api/handlers/role/views.go => UpdateRole
// 接口: 修改角色
func UpdateRole(c *gin.Context) {
// 验证前端路径参数过来的 id 必须是数字
idStr := c.Param("id")
id, err := strconv.Atoi(idStr)
if err != nil {
utils.BadRequest(c, "请求头参数错误")
}
// 前端请求体 json 验证
var req struct {
Name string `json:"name" validate:"required"`
Descr string `json:"descr" validate:"omitempty"`
Status int `json:"status" validate:"oneof=0 1"`
}
if err := utils.BindAndValidate(c, &req); err != nil {
utils.BadRequest(c, "请求体数据错误")
return
}
// 根据 id 更新数据
mysql := `
update role set name = ?, descr = ?, status = ?
where id = ?;
`
rows, err := db.Exec(mysql, req.Name, req.Descr, req.Status, id)
if err != nil {
utils.Error(c, 500, "", err)
return
}
utils.Success(c, gin.H{
"affectedRows": rows,
})
}
PUT 修改角色状态接口
前端: http://localhost:8000/api/role/role/19/update_status
后端: /api/handlers/role/views.go => UpdateRoleStatus
// 接口: 更新角色状态
func UpdateRoleStatus(c *gin.Context) {
// 获取并校验路径参数
idStr := c.Param("id")
id, err := strconv.Atoi(idStr)
if err != nil {
utils.BadRequest(c, "请求路径参数错误")
return
}
// 特殊场景, id = 1 是超级管理员, 不让操作
if id == 1 {
utils.BadRequest(c, "保留角色不让操作哦")
return
}
// 请求体参数验证
var req struct {
Status int `json:"status" validate:"oneof=0 1"`
}
if err := utils.BindAndValidate(c, &req); err != nil {
utils.BadRequest(c, "请求体参数错误")
return
}
// 修改数据状态
rows, err2 := db.Exec("update role set status = ? where id = ?", req.Status, id)
if err2 != nil {
utils.Error(c, 600, "", err2)
return
}
utils.Success(c, gin.H{
"affectedRows": rows,
})
}
逻辑简单, 就改个状态, 但流程一点不少呀. 路径参数要验证, 请求体也要验证, 特殊角色也要验证, sql 也要验证...
DELETE 删除角色接口
前端: http://localhost:8000/api/role/role/25/delete
后端: /api/handlers/role/views.go => DeleteRole
// 接口: 删除角色
func DeleteRole(c *gin.Context) {
idStr := c.Param("id")
id, err := strconv.Atoi(idStr)
if err != nil || id == 1 {
utils.BadRequest(c, "请求错误")
return
}
// 真正删除
rows, err := db.Exec("delete from role where id = ?", id)
if err != nil {
utils.BadRequest(c, "删除失败")
return
}
utils.Success(c, gin.H{
"affectedRows": rows,
})
}
POST 设置角色-权限接口
前端: http://localhost:8000/api/role/role/set_rules
前端请求体参数:
{
"role_id": 1,
"rule_ids": [1, 2, 3, 4]
}
后端: /api/handlers/role/views.go => SetRoleRule
// 接口: 设置角色权限
func SetRoleRule(c *gin.Context) {
// 校验请求体 json 参数, {role_id: 123, rule_ids: [1, 2, 3]}
var req struct {
RoleId int `json:"role_id" validate:"required,min=1"`
RuleIds []int `json:"rule_ids" validate:"required,dive,gt=0"` // rule_id > 0
}
if err := utils.BindAndValidate(c, &req); err != nil {
utils.BadRequest(c, "请求体参数错误")
return
}
// ids 为 [] 时, todo: 直接删除该角色的所有权限
if len(req.RuleIds) == 0 {
utils.BadRequest(c, "未设置权限")
return
}
// 根据 role_ids 的每项 id 进行 insert; rule_ids: [1, 2, 3]
// insert into role_rule(role_id, rule_id) value (1, 1), (1, 2), (1, 3)
var args []string
var values []interface{}
for _, rule_id := range req.RuleIds {
args = append(args, "(?, ?)")
values = append(values, req.RoleId, rule_id)
}
// 拼接一下 sql 语句
placeholders := strings.Join(args, ", ")
sql := "insert into role_rule(role_id, rule_id)" +
"values " + placeholders +
" on duplicate key update rule_id = rule_id"
rows, err := db.Exec(sql, values...)
if err != nil {
utils.Error(c, 500, "", err)
return
}
utils.Success(c, gin.H{
"affectedRows": rows,
})
}
这里的小技巧就是要拼接 sql 语句, 因为用的是参数化写法, 然后结合 go 的展开表达式 values... 进行对应传值.
然后还有数据库唯一组合值校验, 粗暴一点可以用 insert ignore ... 优雅一点可以用 on duplicate key update xxx.
就用 AI 帮我们写一下就行, 没有任何难度.
至于, 角色模块也基本完成的, 优化的地方还是蛮多, 后面再说吧.

浙公网安备 33010602011771号