diff --git a/features/sys_menu/api_sys_menu.go b/features/sys_menu/api_sys_menu.go new file mode 100644 index 00000000..5dc18f1e --- /dev/null +++ b/features/sys_menu/api_sys_menu.go @@ -0,0 +1,353 @@ +package sysmenu + +import ( + "fmt" + "net/http" + + "ems.agt/features/sys_menu/consts" + "ems.agt/features/sys_menu/model" + "ems.agt/features/sys_menu/service" + "ems.agt/lib/core/conf" + "ems.agt/lib/core/utils/ctx" + "ems.agt/lib/core/utils/regular" + "ems.agt/lib/core/vo/result" + "ems.agt/lib/midware" + "ems.agt/lib/services" + "ems.agt/restagent/config" +) + +// 菜单接口添加到路由 +func Routers() []services.RouterItem { + // 实例化控制层 SysMenuApi 结构体 + var apis = &SysMenuApi{ + sysMenuService: service.NewServiceSysMenu, + } + + rs := [...]services.RouterItem{ + { + Method: "GET", + Pattern: "/menuManage/{apiVersion}/list", + Handler: apis.List, + Middleware: midware.Authorize(map[string][]string{ + "hasPerms": {"system:menu:list"}, + }), + }, + { + Method: "GET", + Pattern: "/menuManage/{apiVersion}/info/{menuId}", + Handler: apis.Info, + Middleware: midware.Authorize(map[string][]string{ + "hasPerms": {"system:menu:query"}, + }), + }, + { + Method: "POST", + Pattern: "/menuManage/{apiVersion}/add", + Handler: apis.Add, + Middleware: midware.Authorize(map[string][]string{ + "hasPerms": {"system:menu:add"}, + }), + }, + { + Method: "PUT", + Pattern: "/menuManage/{apiVersion}/edit", + Handler: apis.Edit, + Middleware: midware.Authorize(map[string][]string{ + "hasPerms": {"system:menu:edit"}, + }), + }, + { + Method: "DELETE", + Pattern: "/menuManage/{apiVersion}/del/{menuId}", + Handler: apis.Remove, + Middleware: midware.Authorize(map[string][]string{ + "hasPerms": {"system:menu:edit"}, + }), + }, + { + Method: "GET", + Pattern: "/menuManage/{apiVersion}/treeSelect", + Handler: apis.TreeSelect, + Middleware: midware.Authorize(map[string][]string{ + "hasPerms": {"system:menu:list"}, + }), + }, + { + Method: "GET", + Pattern: "/menuManage/{apiVersion}/roleMenuTreeSelect/{roleId}", + Handler: apis.RoleMenuTreeSelect, + Middleware: midware.Authorize(map[string][]string{ + "hasPerms": {"system:menu:list"}, + }), + }, + // 添加更多的 Router 对象... + } + + // 生成两组前缀路由 + rsPrefix := []services.RouterItem{} + for _, v := range rs { + path := v.Pattern + // 固定前缀 + v.Pattern = config.DefaultUriPrefix + path + rsPrefix = append(rsPrefix, v) + // 可配置 + v.Pattern = config.UriPrefix + path + rsPrefix = append(rsPrefix, v) + } + return rsPrefix +} + +// // 实例化控制层 SysMenuApi 结构体 +// var NewSysMenu = &SysMenuApi{ +// sysMenuService: NewServiceSysMenu, +// } + +// 菜单信息 +// +// PATH /menuManage +type SysMenuApi struct { + // 菜单服务 + sysMenuService *service.ServiceSysMenu +} + +// 菜单列表 +// +// GET /list +func (s *SysMenuApi) List(w http.ResponseWriter, r *http.Request) { + query := model.SysMenu{} + if v := ctx.GetQuery(r, "menuName"); v != "" { + query.MenuName = v + } + if v := ctx.GetQuery(r, "status"); v != "" { + query.Status = v + } + + userId := ctx.LoginUserToUserID(r) + if conf.IsAdmin(userId) { + userId = "*" + } + data := s.sysMenuService.SelectMenuList(query, userId) + ctx.JSON(w, 200, result.OkData(data)) +} + +// 菜单信息 +// +// GET /:menuId +func (s *SysMenuApi) Info(w http.ResponseWriter, r *http.Request) { + menuId := ctx.Param(r, "menuId") + if menuId == "" { + ctx.JSON(w, 400, result.CodeMsg(400, "参数错误")) + return + } + data := s.sysMenuService.SelectMenuById(menuId) + if data.MenuID == menuId { + ctx.JSON(w, 200, result.OkData(data)) + return + } + ctx.JSON(w, 200, result.Err(nil)) +} + +// 菜单新增 +// +// POST / +func (s *SysMenuApi) Add(w http.ResponseWriter, r *http.Request) { + var body model.SysMenu + err := ctx.ShouldBindJSON(r, &body) + if err != nil || body.MenuID != "" { + ctx.JSON(w, 400, result.CodeMsg(400, "参数错误")) + return + } + + // 目录和菜单检查地址唯一 + if consts.TYPE_DIR == body.MenuType || consts.TYPE_MENU == body.MenuType { + uniqueNenuPath := s.sysMenuService.CheckUniqueMenuPath(body.Path, "") + if !uniqueNenuPath { + msg := fmt.Sprintf("菜单新增【%s】失败,菜单路由地址已存在", body.MenuName) + ctx.JSON(w, 200, result.ErrMsg(msg)) + return + } + } + + // 检查名称唯一 + uniqueNenuName := s.sysMenuService.CheckUniqueMenuName(body.MenuName, body.ParentID, "") + if !uniqueNenuName { + msg := fmt.Sprintf("菜单新增【%s】失败,菜单名称已存在", body.MenuName) + ctx.JSON(w, 200, result.ErrMsg(msg)) + return + } + + // 外链菜单需要符合网站http(s)开头 + if body.IsFrame == "0" && !regular.ValidHttp(body.Path) { + msg := fmt.Sprintf("菜单新增【%s】失败,非内部地址必须以http(s)://开头", body.MenuName) + ctx.JSON(w, 200, result.ErrMsg(msg)) + return + } + + body.CreateBy = ctx.LoginUserToUserName(r) + insertId := s.sysMenuService.InsertMenu(body) + if insertId != "" { + ctx.JSON(w, 200, result.Ok(nil)) + return + } + ctx.JSON(w, 200, result.Err(nil)) +} + +// 菜单修改 +// +// PUT / +func (s *SysMenuApi) Edit(w http.ResponseWriter, r *http.Request) { + var body model.SysMenu + err := ctx.ShouldBindJSON(r, &body) + if err != nil || body.MenuID == "" { + ctx.JSON(w, 400, result.CodeMsg(400, "参数错误")) + return + } + + // 上级菜单不能选自己 + if body.MenuID == body.ParentID { + msg := fmt.Sprintf("菜单修改【%s】失败,上级菜单不能选择自己", body.MenuName) + ctx.JSON(w, 200, result.ErrMsg(msg)) + return + } + + // 检查数据是否存在 + menuInfo := s.sysMenuService.SelectMenuById(body.MenuID) + if menuInfo.MenuID != body.MenuID { + ctx.JSON(w, 200, result.ErrMsg("没有权限访问菜单数据")) + return + } + // 父级ID不为0是要检查 + if body.ParentID != "0" { + menuParent := s.sysMenuService.SelectMenuById(body.ParentID) + if menuParent.MenuID != body.ParentID { + ctx.JSON(w, 200, result.ErrMsg("没有权限访问菜单数据")) + return + } + } + + // 目录和菜单检查地址唯一 + if consts.TYPE_DIR == body.MenuType || consts.TYPE_MENU == body.MenuType { + uniqueNenuPath := s.sysMenuService.CheckUniqueMenuPath(body.Path, body.MenuID) + if !uniqueNenuPath { + msg := fmt.Sprintf("菜单修改【%s】失败,菜单路由地址已存在", body.MenuName) + ctx.JSON(w, 200, result.ErrMsg(msg)) + return + } + } + + // 检查名称唯一 + uniqueNenuName := s.sysMenuService.CheckUniqueMenuName(body.MenuName, body.ParentID, body.MenuID) + if !uniqueNenuName { + msg := fmt.Sprintf("菜单修改【%s】失败,菜单名称已存在", body.MenuName) + ctx.JSON(w, 200, result.ErrMsg(msg)) + return + } + + // 外链菜单需要符合网站http(s)开头 + if body.IsFrame == "0" && !regular.ValidHttp(body.Path) { + msg := fmt.Sprintf("菜单修改【%s】失败,非内部地址必须以http(s)://开头", body.MenuName) + ctx.JSON(w, 200, result.ErrMsg(msg)) + return + } + + body.UpdateBy = ctx.LoginUserToUserName(r) + rows := s.sysMenuService.UpdateMenu(body) + if rows > 0 { + ctx.JSON(w, 200, result.Ok(nil)) + return + } + ctx.JSON(w, 200, result.Err(nil)) +} + +// 菜单删除 +// +// DELETE /:menuId +func (s *SysMenuApi) Remove(w http.ResponseWriter, r *http.Request) { + menuId := ctx.Param(r, "menuId") + if menuId == "" { + ctx.JSON(w, 400, result.CodeMsg(400, "参数错误")) + return + } + + // 检查数据是否存在 + menu := s.sysMenuService.SelectMenuById(menuId) + if menu.MenuID != menuId { + ctx.JSON(w, 200, result.ErrMsg("没有权限访问菜单数据!")) + return + } + + // 检查是否存在子菜单 + hasChild := s.sysMenuService.HasChildByMenuId(menuId) + if hasChild > 0 { + msg := fmt.Sprintf("不允许删除,存在子菜单数:%d", hasChild) + ctx.JSON(w, 200, result.ErrMsg(msg)) + return + } + + // 检查是否分配给角色 + existRole := s.sysMenuService.CheckMenuExistRole(menuId) + if existRole > 0 { + msg := fmt.Sprintf("不允许删除,菜单已分配给角色数:%d", existRole) + ctx.JSON(w, 200, result.ErrMsg(msg)) + return + } + + rows := s.sysMenuService.DeleteMenuById(menuId) + if rows > 0 { + msg := fmt.Sprintf("删除成功:%d", rows) + ctx.JSON(w, 200, result.OkMsg(msg)) + return + } + ctx.JSON(w, 200, result.Err(nil)) +} + +// 菜单树结构列表 +// +// GET /treeSelect +func (s *SysMenuApi) TreeSelect(w http.ResponseWriter, r *http.Request) { + query := model.SysMenu{} + if v := ctx.GetQuery(r, "menuName"); v != "" { + query.MenuName = v + } + if v := ctx.GetQuery(r, "status"); v != "" { + query.Status = v + } + + userId := ctx.LoginUserToUserID(r) + if conf.IsAdmin(userId) { + userId = "*" + } + data := s.sysMenuService.SelectMenuTreeSelectByUserId(query, userId) + ctx.JSON(w, 200, result.OkData(data)) + +} + +// 菜单树结构列表(指定角色) +// +// GET /roleMenuTreeSelect/:roleId +func (s *SysMenuApi) RoleMenuTreeSelect(w http.ResponseWriter, r *http.Request) { + roleId := ctx.Param(r, "roleId") + if roleId == "" { + ctx.JSON(w, 400, result.CodeMsg(400, "参数错误")) + return + } + + query := model.SysMenu{} + if v := ctx.GetQuery(r, "menuName"); v != "" { + query.MenuName = v + } + if v := ctx.GetQuery(r, "status"); v != "" { + query.Status = v + } + + userId := ctx.LoginUserToUserID(r) + if conf.IsAdmin(userId) { + userId = "*" + } + menuTreeSelect := s.sysMenuService.SelectMenuTreeSelectByUserId(query, userId) + checkedKeys := s.sysMenuService.SelectMenuListByRoleId(roleId) + ctx.JSON(w, 200, result.OkData(map[string]any{ + "menus": menuTreeSelect, + "checkedKeys": checkedKeys, + })) +} diff --git a/features/sys_menu/consts/consts_menu.go b/features/sys_menu/consts/consts_menu.go new file mode 100644 index 00000000..6a1dca31 --- /dev/null +++ b/features/sys_menu/consts/consts_menu.go @@ -0,0 +1,24 @@ +package consts + +// 系统菜单常量信息 + +const ( + // 组件布局类型-基础布局组件标识 + COMPONENT_LAYOUT_BASIC = "BasicLayout" + // 组件布局类型-空白布局组件标识 + COMPONENT_LAYOUT_BLANK = "BlankLayout" + // 组件布局类型-内链接布局组件标识 + COMPONENT_LAYOUT_LINK = "LinkLayout" +) + +const ( + // 菜单类型-目录 + TYPE_DIR = "D" + // 菜单类型-菜单 + TYPE_MENU = "M" + // 菜单类型-按钮 + TYPE_BUTTON = "B" +) + +// 菜单内嵌地址标识-带/前缀 +const PATH_INLINE = "/inline" diff --git a/features/sys_menu/model/sys_menu.go b/features/sys_menu/model/sys_menu.go new file mode 100644 index 00000000..397e30eb --- /dev/null +++ b/features/sys_menu/model/sys_menu.go @@ -0,0 +1,46 @@ +package model + +// SysMenu 菜单权限对象 sys_menu +type SysMenu struct { + // 菜单ID + MenuID string `json:"menuId"` + // 菜单名称 + MenuName string `json:"menuName" binding:"required"` + // 父菜单ID 默认0 + ParentID string `json:"parentId" binding:"required"` + // 显示顺序 + MenuSort int `json:"menuSort"` + // 路由地址 + Path string `json:"path"` + // 组件路径 + Component string `json:"component"` + // 是否内部跳转(0否 1是) + IsFrame string `json:"isFrame"` + // 是否缓存(0不缓存 1缓存) + IsCache string `json:"isCache"` + // 菜单类型(D目录 M菜单 B按钮) + MenuType string `json:"menuType" binding:"required"` + // 是否显示(0隐藏 1显示) + Visible string `json:"visible"` + // 菜单状态(0停用 1正常) + Status string `json:"status"` + // 权限标识 + Perms string `json:"perms"` + // 菜单图标(#无图标) + Icon string `json:"icon"` + // 创建者 + CreateBy string `json:"createBy"` + // 创建时间 + CreateTime int64 `json:"createTime"` + // 更新者 + UpdateBy string `json:"updateBy"` + // 更新时间 + UpdateTime int64 `json:"updateTime"` + // 备注 + Remark string `json:"remark"` + + // ====== 非数据库字段属性 ====== + + // 子菜单 + Children []SysMenu `json:"children,omitempty"` +} diff --git a/features/sys_menu/service/repo_sys_menu.go b/features/sys_menu/service/repo_sys_menu.go new file mode 100644 index 00000000..abfc93d6 --- /dev/null +++ b/features/sys_menu/service/repo_sys_menu.go @@ -0,0 +1,452 @@ +package service + +import ( + "fmt" + "strings" + "time" + + "ems.agt/features/sys_menu/consts" + "ems.agt/features/sys_menu/model" + "ems.agt/lib/core/datasource" + "ems.agt/lib/core/utils/parse" + "github.com/go-admin-team/go-admin-core/logger" +) + +// 实例化数据层 RepoSysMenu 结构体 +var NewRepoSysMenu = &RepoSysMenu{ + selectSql: `select + m.menu_id, m.menu_name, m.parent_id, m.menu_sort, m.path, m.component, m.is_frame, m.is_cache, m.menu_type, m.visible, m.status, ifnull(m.perms,'') as perms, m.icon, m.create_time, m.remark + from sys_menu m`, + + selectSqlByUser: `select distinct + m.menu_id, m.menu_name, m.parent_id, m.menu_sort, m.path, m.component, m.is_frame, m.is_cache, m.menu_type, m.visible, m.status, ifnull(m.perms,'') as perms, m.icon, m.create_time, m.remark + from sys_menu m + left join sys_role_menu rm on m.menu_id = rm.menu_id + left join sys_user_role ur on rm.role_id = ur.role_id + left join sys_role ro on ur.role_id = ro.role_id`, + + resultMap: map[string]string{ + "menu_id": "MenuID", + "menu_name": "MenuName", + "parent_name": "ParentName", + "parent_id": "ParentID", + "path": "Path", + "menu_sort": "MenuSort", + "component": "Component", + "is_frame": "IsFrame", + "is_cache": "IsCache", + "menu_type": "MenuType", + "visible": "Visible", + "status": "Status", + "perms": "Perms", + "icon": "Icon", + "create_by": "CreateBy", + "create_time": "CreateTime", + "update_by": "UpdateBy", + "update_time": "UpdateTime", + "remark": "Remark", + }, +} + +// RepoSysMenu 菜单表 数据层处理 +type RepoSysMenu struct { + // 查询视图对象SQL + selectSql string + // 查询视图用户对象SQL + selectSqlByUser string + // 结果字段与实体映射 + resultMap map[string]string +} + +// convertResultRows 将结果记录转实体结果组 +func (r *RepoSysMenu) convertResultRows(rows []map[string]any) []model.SysMenu { + arr := make([]model.SysMenu, 0) + for _, row := range rows { + sysMenu := model.SysMenu{} + for key, value := range row { + if keyMapper, ok := r.resultMap[key]; ok { + datasource.SetFieldValue(&sysMenu, keyMapper, value) + } + } + arr = append(arr, sysMenu) + } + return arr +} + +// SelectMenuList 查询系统菜单列表 +func (r *RepoSysMenu) SelectMenuList(sysMenu model.SysMenu, userId string) []model.SysMenu { + // 查询条件拼接 + var conditions []string + var params []any + if sysMenu.MenuName != "" { + conditions = append(conditions, "m.menu_name like concat(?, '%')") + params = append(params, sysMenu.MenuName) + } + if sysMenu.Visible != "" { + conditions = append(conditions, "m.visible = ?") + params = append(params, sysMenu.Visible) + } + if sysMenu.Status != "" { + conditions = append(conditions, "m.status = ?") + params = append(params, sysMenu.Status) + } + + fromSql := r.selectSql + + // 个人菜单 + if userId != "*" { + fromSql = r.selectSqlByUser + conditions = append(conditions, "ur.user_id = ?") + params = append(params, userId) + } + + // 构建查询条件语句 + whereSql := "" + if len(conditions) > 0 { + whereSql += " where " + strings.Join(conditions, " and ") + } + + // 查询数据 + orderSql := " order by m.parent_id, m.menu_sort" + querySql := fromSql + whereSql + orderSql + results, err := datasource.RawDB("", querySql, params) + if err != nil { + logger.Errorf("query err => %v", err) + return []model.SysMenu{} + } + + // 转换实体 + return r.convertResultRows(results) +} + +// SelectMenuPermsByUserId 根据用户ID查询权限 +func (r *RepoSysMenu) SelectMenuPermsByUserId(userId string) []string { + querySql := `select distinct m.perms as 'str' from sys_menu m + left join sys_role_menu rm on m.menu_id = rm.menu_id + left join sys_user_role ur on rm.role_id = ur.role_id + left join sys_role r on r.role_id = ur.role_id + where m.status = '1' and m.perms != '' and r.status = '1' and ur.user_id = ? ` + + // 查询结果 + results, err := datasource.RawDB("", querySql, []any{userId}) + if err != nil { + logger.Errorf("query err => %v", err) + return []string{} + } + + // 读取结果 + rows := make([]string, 0) + for _, m := range results { + rows = append(rows, fmt.Sprintf("%v", m["str"])) + } + return rows +} + +// SelectMenuTreeByUserId 根据用户ID查询菜单 +func (r *RepoSysMenu) SelectMenuTreeByUserId(userId string) []model.SysMenu { + var params []any + var querySql string + + if userId == "*" { + // 管理员全部菜单 + querySql = r.selectSql + ` where + m.menu_type in (?,?) and m.status = '1' + order by m.parent_id, m.menu_sort` + params = append(params, consts.TYPE_DIR) + params = append(params, consts.TYPE_MENU) + } else { + // 用户ID权限 + querySql = r.selectSqlByUser + ` where + m.menu_type in (?, ?) and m.status = '1' + and ur.user_id = ? and ro.status = '1' + order by m.parent_id, m.menu_sort` + params = append(params, consts.TYPE_DIR) + params = append(params, consts.TYPE_MENU) + params = append(params, userId) + } + + // 查询结果 + results, err := datasource.RawDB("", querySql, params) + if err != nil { + logger.Errorf("query err => %v", err) + return []model.SysMenu{} + } + + return r.convertResultRows(results) +} + +// SelectMenuListByRoleId 根据角色ID查询菜单树信息 +func (r *RepoSysMenu) SelectMenuListByRoleId(roleId string, menuCheckStrictly bool) []string { + querySql := `select m.menu_id as 'str' from sys_menu m + left join sys_role_menu rm on m.menu_id = rm.menu_id + where rm.role_id = ? ` + var params []any + params = append(params, roleId) + // 展开 + if menuCheckStrictly { + querySql += ` and m.menu_id not in + (select m.parent_id from sys_menu m + inner join sys_role_menu rm on m.menu_id = rm.menu_id + and rm.role_id = ?) ` + params = append(params, roleId) + } + + // 查询结果 + results, err := datasource.RawDB("", querySql, params) + if err != nil { + logger.Errorf("query err => %v", err) + return []string{} + } + + if len(results) > 0 { + ids := make([]string, 0) + for _, v := range results { + ids = append(ids, fmt.Sprintf("%v", v["str"])) + } + return ids + } + return []string{} +} + +// SelectMenuByIds 根据菜单ID查询信息 +func (r *RepoSysMenu) SelectMenuByIds(menuIds []string) []model.SysMenu { + placeholder := datasource.KeyPlaceholderByQuery(len(menuIds)) + querySql := r.selectSql + " where m.menu_id in (" + placeholder + ")" + parameters := datasource.ConvertIdsSlice(menuIds) + results, err := datasource.RawDB("", querySql, parameters) + if err != nil { + logger.Errorf("query err => %v", err) + return []model.SysMenu{} + } + // 转换实体 + return r.convertResultRows(results) +} + +// HasChildByMenuId 存在菜单子节点数量 +func (r *RepoSysMenu) HasChildByMenuId(menuId string) int64 { + querySql := "select count(1) as 'total' from sys_menu where parent_id = ?" + results, err := datasource.RawDB("", querySql, []any{menuId}) + if err != nil { + logger.Errorf("query err => %v", err) + return 0 + } + if len(results) > 0 { + return parse.Number(results[0]["total"]) + } + return 0 +} + +// InsertMenu 新增菜单信息 +func (r *RepoSysMenu) InsertMenu(sysMenu model.SysMenu) string { + // 参数拼接 + params := make(map[string]any) + if sysMenu.MenuID != "" { + params["menu_id"] = sysMenu.MenuID + } + if sysMenu.ParentID != "" { + params["parent_id"] = sysMenu.ParentID + } + if sysMenu.MenuName != "" { + params["menu_name"] = sysMenu.MenuName + } + if sysMenu.MenuSort > 0 { + params["menu_sort"] = sysMenu.MenuSort + } + if sysMenu.Path != "" { + params["path"] = sysMenu.Path + } + if sysMenu.Component != "" { + params["component"] = sysMenu.Component + } + if sysMenu.IsFrame != "" { + params["is_frame"] = sysMenu.IsFrame + } + if sysMenu.IsCache != "" { + params["is_cache"] = sysMenu.IsCache + } + if sysMenu.MenuType != "" { + params["menu_type"] = sysMenu.MenuType + } + if sysMenu.Visible != "" { + params["visible"] = sysMenu.Visible + } + if sysMenu.Status != "" { + params["status"] = sysMenu.Status + } + if sysMenu.Perms != "" { + params["perms"] = sysMenu.Perms + } + if sysMenu.Icon != "" { + params["icon"] = sysMenu.Icon + } else { + params["icon"] = "#" + } + if sysMenu.Remark != "" { + params["remark"] = sysMenu.Remark + } + if sysMenu.CreateBy != "" { + params["create_by"] = sysMenu.CreateBy + params["create_time"] = time.Now().UnixMilli() + } + + // 根据菜单类型重置参数 + if sysMenu.MenuType == consts.TYPE_BUTTON { + params["component"] = "" + params["path"] = "" + params["icon"] = "#" + params["is_cache"] = "1" + params["is_frame"] = "1" + params["visible"] = "1" + params["status"] = "1" + } + if sysMenu.MenuType == consts.TYPE_DIR { + params["component"] = "" + params["perms"] = "" + } + + // 构建执行语句 + keys, placeholder, values := datasource.KeyPlaceholderValueByInsert(params) + sql := "insert into sys_menu (" + strings.Join(keys, ",") + ")values(" + placeholder + ")" + + // 执行插入 + rows, err := datasource.ExecDB("", sql, values) + if err != nil { + logger.Errorf("insert row : %v", err.Error()) + return "" + } + + return fmt.Sprint(rows) +} + +// UpdateMenu 修改菜单信息 +func (r *RepoSysMenu) UpdateMenu(sysMenu model.SysMenu) int64 { + // 参数拼接 + params := make(map[string]any) + if sysMenu.MenuID != "" { + params["menu_id"] = sysMenu.MenuID + } + if sysMenu.ParentID != "" { + params["parent_id"] = sysMenu.ParentID + } + if sysMenu.MenuName != "" { + params["menu_name"] = sysMenu.MenuName + } + if sysMenu.MenuSort > 0 { + params["menu_sort"] = sysMenu.MenuSort + } + if sysMenu.Path != "" { + params["path"] = sysMenu.Path + } + if sysMenu.Component != "" { + params["component"] = sysMenu.Component + } + if sysMenu.IsFrame != "" { + params["is_frame"] = sysMenu.IsFrame + } + if sysMenu.IsCache != "" { + params["is_cache"] = sysMenu.IsCache + } + if sysMenu.MenuType != "" { + params["menu_type"] = sysMenu.MenuType + } + if sysMenu.Visible != "" { + params["visible"] = sysMenu.Visible + } + if sysMenu.Status != "" { + params["status"] = sysMenu.Status + } + if sysMenu.Perms != "" { + params["perms"] = sysMenu.Perms + } + if sysMenu.Icon != "" { + params["icon"] = sysMenu.Icon + } else { + params["icon"] = "#" + } + if sysMenu.Remark != "" { + params["remark"] = sysMenu.Remark + } + if sysMenu.UpdateBy != "" { + params["update_by"] = sysMenu.UpdateBy + params["update_time"] = time.Now().UnixMilli() + } + + // 根据菜单类型重置参数 + if sysMenu.MenuType == consts.TYPE_BUTTON { + params["component"] = "" + params["path"] = "" + params["icon"] = "#" + params["is_cache"] = "1" + params["is_frame"] = "1" + params["visible"] = "1" + params["status"] = "1" + } + if sysMenu.MenuType == consts.TYPE_DIR { + params["component"] = "" + params["perms"] = "" + } + + // 构建执行语句 + keys, values := datasource.KeyValueByUpdate(params) + sql := "update sys_menu set " + strings.Join(keys, ",") + " where menu_id = ?" + + // 执行更新 + values = append(values, sysMenu.MenuID) + rows, err := datasource.ExecDB("", sql, values) + if err != nil { + logger.Errorf("update row : %v", err.Error()) + return 0 + } + return rows +} + +// DeleteMenuById 删除菜单管理信息 +func (r *RepoSysMenu) DeleteMenuById(menuId string) int64 { + sql := "delete from sys_menu where menu_id = ?" + results, err := datasource.ExecDB("", sql, []any{menuId}) + if err != nil { + logger.Errorf("delete err => %v", err) + return 0 + } + return results +} + +// CheckUniqueMenu 校验菜单是否唯一 +func (r *RepoSysMenu) CheckUniqueMenu(sysMenu model.SysMenu) string { + // 查询条件拼接 + var conditions []string + var params []any + if sysMenu.MenuName != "" { + conditions = append(conditions, "menu_name = ?") + params = append(params, sysMenu.MenuName) + } + if sysMenu.ParentID != "" { + conditions = append(conditions, "parent_id = ?") + params = append(params, sysMenu.ParentID) + } + if sysMenu.Path != "" { + conditions = append(conditions, "path = ?") + params = append(params, sysMenu.Path) + } + + // 构建查询条件语句 + whereSql := "" + if len(conditions) > 0 { + whereSql += " where " + strings.Join(conditions, " and ") + } + if whereSql == "" { + return "" + } + + // 查询数据 + querySql := "select menu_id as 'str' from sys_menu " + whereSql + " limit 1" + results, err := datasource.RawDB("", querySql, params) + if err != nil { + logger.Errorf("query err %v", err) + return "" + } + if len(results) > 0 { + return fmt.Sprintf("%v", results[0]["str"]) + } + return "" +} diff --git a/features/sys_menu/service/service_sys_menu.go b/features/sys_menu/service/service_sys_menu.go new file mode 100644 index 00000000..41b99fe4 --- /dev/null +++ b/features/sys_menu/service/service_sys_menu.go @@ -0,0 +1,397 @@ +package service + +import ( + "encoding/base64" + "strings" + + "ems.agt/features/sys_menu/consts" + "ems.agt/features/sys_menu/model" + sysRoleService "ems.agt/features/sys_role/service" + sysrolemenu "ems.agt/features/sys_role_menu" + "ems.agt/lib/core/utils/parse" + "ems.agt/lib/core/utils/regular" + "ems.agt/lib/core/vo" +) + +// 实例化服务层 ServiceSysMenu 结构体 +var NewServiceSysMenu = &ServiceSysMenu{ + sysMenuRepository: NewRepoSysMenu, + sysRoleMenuRepository: sysrolemenu.NewRepoSysRoleMenu, + sysRoleRepository: sysRoleService.NewRepoSysRole, +} + +// ServiceSysMenu 菜单 服务层处理 +type ServiceSysMenu struct { + // 菜单服务 + sysMenuRepository *RepoSysMenu + // 角色与菜单关联服务 + sysRoleMenuRepository *sysrolemenu.RepoSysRoleMenu + // 角色服务 + sysRoleRepository *sysRoleService.RepoSysRole +} + +// SelectMenuList 查询系统菜单列表 +func (r *ServiceSysMenu) SelectMenuList(sysMenu model.SysMenu, userId string) []model.SysMenu { + return r.sysMenuRepository.SelectMenuList(sysMenu, userId) +} + +// SelectMenuPermsByUserId 根据用户ID查询权限 +func (r *ServiceSysMenu) SelectMenuPermsByUserId(userId string) []string { + return r.sysMenuRepository.SelectMenuPermsByUserId(userId) +} + +// SelectMenuTreeByUserId 根据用户ID查询菜单 +func (r *ServiceSysMenu) SelectMenuTreeByUserId(userId string) []model.SysMenu { + sysMenus := r.sysMenuRepository.SelectMenuTreeByUserId(userId) + return r.parseDataToTree(sysMenus) +} + +// SelectMenuTreeSelectByUserId 根据用户ID查询菜单树结构信息 +func (r *ServiceSysMenu) SelectMenuTreeSelectByUserId(sysMenu model.SysMenu, userId string) []vo.TreeSelect { + sysMenus := r.sysMenuRepository.SelectMenuList(sysMenu, userId) + menus := r.parseDataToTree(sysMenus) + tree := make([]vo.TreeSelect, 0) + for _, menu := range menus { + tree = append(tree, sysMenuTreeSelect(menu)) + } + return tree +} + +// sysMenuTreeSelect 使用给定的 SysMenu 对象解析为 TreeSelect 对象 +func sysMenuTreeSelect(sysMenu model.SysMenu) vo.TreeSelect { + t := vo.TreeSelect{} + t.ID = sysMenu.MenuID + t.Label = sysMenu.MenuName + + if len(sysMenu.Children) > 0 { + for _, menu := range sysMenu.Children { + child := sysMenuTreeSelect(menu) + t.Children = append(t.Children, child) + } + } else { + t.Children = []vo.TreeSelect{} + } + + return t +} + +// SelectMenuListByRoleId 根据角色ID查询菜单树信息 TODO +func (r *ServiceSysMenu) SelectMenuListByRoleId(roleId string) []string { + roles := r.sysRoleRepository.SelectRoleByIds([]string{roleId}) + if len(roles) > 0 { + role := roles[0] + if role.RoleID == roleId { + return r.sysMenuRepository.SelectMenuListByRoleId( + role.RoleID, + role.MenuCheckStrictly == "1", + ) + } + } + return []string{} +} + +// SelectMenuById 根据菜单ID查询信息 +func (r *ServiceSysMenu) SelectMenuById(menuId string) model.SysMenu { + if menuId == "" { + return model.SysMenu{} + } + menus := r.sysMenuRepository.SelectMenuByIds([]string{menuId}) + if len(menus) > 0 { + return menus[0] + } + return model.SysMenu{} +} + +// HasChildByMenuId 存在菜单子节点数量 +func (r *ServiceSysMenu) HasChildByMenuId(menuId string) int64 { + return r.sysMenuRepository.HasChildByMenuId(menuId) +} + +// CheckMenuExistRole 查询菜单是否存在角色 +func (r *ServiceSysMenu) CheckMenuExistRole(menuId string) int64 { + return r.sysRoleMenuRepository.CheckMenuExistRole(menuId) +} + +// InsertMenu 新增菜单信息 +func (r *ServiceSysMenu) InsertMenu(sysMenu model.SysMenu) string { + return r.sysMenuRepository.InsertMenu(sysMenu) +} + +// UpdateMenu 修改菜单信息 +func (r *ServiceSysMenu) UpdateMenu(sysMenu model.SysMenu) int64 { + return r.sysMenuRepository.UpdateMenu(sysMenu) +} + +// DeleteMenuById 删除菜单管理信息 +func (r *ServiceSysMenu) DeleteMenuById(menuId string) int64 { + // 删除菜单与角色关联 + r.sysRoleMenuRepository.DeleteMenuRole([]string{menuId}) + return r.sysMenuRepository.DeleteMenuById(menuId) +} + +// CheckUniqueMenuName 校验菜单名称是否唯一 +func (r *ServiceSysMenu) CheckUniqueMenuName(menuName, parentId, menuId string) bool { + uniqueId := r.sysMenuRepository.CheckUniqueMenu(model.SysMenu{ + MenuName: menuName, + ParentID: parentId, + }) + if uniqueId == menuId { + return true + } + return uniqueId == "" +} + +// CheckUniqueMenuPath 校验路由地址是否唯一(针对目录和菜单) +func (r *ServiceSysMenu) CheckUniqueMenuPath(path, menuId string) bool { + uniqueId := r.sysMenuRepository.CheckUniqueMenu(model.SysMenu{ + Path: path, + }) + if uniqueId == menuId { + return true + } + return uniqueId == "" +} + +// BuildRouteMenus 构建前端路由所需要的菜单 +func (r *ServiceSysMenu) BuildRouteMenus(sysMenus []model.SysMenu, prefix string) []vo.Router { + routers := []vo.Router{} + for _, item := range sysMenus { + router := vo.Router{} + router.Name = r.getRouteName(item) + router.Path = r.getRouterPath(item) + router.Component = r.getComponent(item) + router.Meta = r.getRouteMeta(item) + + // 子项菜单 目录类型 非路径链接 + cMenus := item.Children + if len(cMenus) > 0 && item.MenuType == consts.TYPE_DIR && !regular.ValidHttp(item.Path) { + // 获取重定向地址 + redirectPrefix, redirectPath := r.getRouteRedirect( + cMenus, + router.Path, + prefix, + ) + router.Redirect = redirectPath + // 子菜单进入递归 + router.Children = r.BuildRouteMenus(cMenus, redirectPrefix) + } else if item.ParentID == "0" && item.IsFrame == "1" && item.MenuType == consts.TYPE_MENU { + // 父菜单 内部跳转 菜单类型 + menuPath := "/" + item.MenuID + childPath := menuPath + r.getRouterPath(item) + children := vo.Router{ + Name: r.getRouteName(item), + Path: childPath, + Component: item.Component, + Meta: r.getRouteMeta(item), + } + router.Meta.HideChildInMenu = true + router.Children = append(router.Children, children) + router.Name = item.MenuID + router.Path = menuPath + router.Redirect = childPath + router.Component = consts.COMPONENT_LAYOUT_BASIC + } else if item.ParentID == "0" && item.IsFrame == "1" && regular.ValidHttp(item.Path) { + // 父菜单 内部跳转 路径链接 + menuPath := "/" + item.MenuID + childPath := menuPath + r.getRouterPath(item) + children := vo.Router{ + Name: r.getRouteName(item), + Path: childPath, + Component: consts.COMPONENT_LAYOUT_LINK, + Meta: r.getRouteMeta(item), + } + router.Meta.HideChildInMenu = true + router.Children = append(router.Children, children) + router.Name = item.MenuID + router.Path = menuPath + router.Redirect = childPath + router.Component = consts.COMPONENT_LAYOUT_BASIC + } + + routers = append(routers, router) + } + return routers +} + +// getRouteName 获取路由名称 路径英文首字母大写 +func (r *ServiceSysMenu) getRouteName(sysMenu model.SysMenu) string { + routerName := parse.FirstUpper(sysMenu.Path) + // 路径链接 + if regular.ValidHttp(sysMenu.Path) { + return routerName[:5] + "Link" + sysMenu.MenuID + } + return routerName +} + +// getRouterPath 获取路由地址 +func (r *ServiceSysMenu) getRouterPath(sysMenu model.SysMenu) string { + routerPath := sysMenu.Path + + // 显式路径 + if routerPath == "" || strings.HasPrefix(routerPath, "/") { + return routerPath + } + + // 路径链接 内部跳转 + if regular.ValidHttp(routerPath) && sysMenu.IsFrame == "1" { + routerPath = regular.Replace(routerPath, `/^http(s)?:\/\/+/`, "") + routerPath = base64.StdEncoding.EncodeToString([]byte(routerPath)) + } + + // 父菜单 内部跳转 + if sysMenu.ParentID == "0" && sysMenu.IsFrame == "1" { + routerPath = "/" + routerPath + } + + return routerPath +} + +// getComponent 获取组件信息 +func (r *ServiceSysMenu) getComponent(sysMenu model.SysMenu) string { + // 内部跳转 路径链接 + if sysMenu.IsFrame == "1" && regular.ValidHttp(sysMenu.Path) { + return consts.COMPONENT_LAYOUT_LINK + } + + // 非父菜单 目录类型 + if sysMenu.ParentID != "0" && sysMenu.MenuType == consts.TYPE_DIR { + return consts.COMPONENT_LAYOUT_BLANK + } + + // 组件路径 内部跳转 菜单类型 + if sysMenu.Component != "" && sysMenu.IsFrame == "1" && sysMenu.MenuType == consts.TYPE_MENU { + // 父菜单套外层布局 + if sysMenu.ParentID == "0" { + return consts.COMPONENT_LAYOUT_BASIC + } + return sysMenu.Component + } + + return consts.COMPONENT_LAYOUT_BASIC +} + +// getRouteMeta 获取路由元信息 +func (r *ServiceSysMenu) getRouteMeta(sysMenu model.SysMenu) vo.RouterMeta { + meta := vo.RouterMeta{} + if sysMenu.Icon == "#" { + meta.Icon = "" + } else { + meta.Icon = sysMenu.Icon + } + meta.Title = sysMenu.MenuName + meta.HideChildInMenu = false + meta.HideInMenu = sysMenu.Visible == "0" + meta.Cache = sysMenu.IsCache == "1" + meta.Target = "" + + // 路径链接 非内部跳转 + if regular.ValidHttp(sysMenu.Path) && sysMenu.IsFrame == "0" { + meta.Target = "_blank" + } + + return meta +} + +// getRouteRedirect 获取路由重定向地址(针对目录) +// +// cMenus 子菜单数组 +// routerPath 当前菜单路径 +// prefix 菜单重定向路径前缀 +func (r *ServiceSysMenu) getRouteRedirect(cMenus []model.SysMenu, routerPath string, prefix string) (string, string) { + redirectPath := "" + + // 重定向为首个显示并启用的子菜单 + var firstChild *model.SysMenu + for _, item := range cMenus { + if item.IsFrame == "1" && item.Visible == "1" { + firstChild = &item + break + } + } + + // 检查内嵌隐藏菜单是否可做重定向 + if firstChild == nil { + for _, item := range cMenus { + if item.IsFrame == "1" && item.Visible == "1" && strings.Contains(item.Path, consts.PATH_INLINE) { + firstChild = &item + break + } + } + } + + if firstChild != nil { + firstChildPath := r.getRouterPath(*firstChild) + if strings.HasPrefix(firstChildPath, "/") { + redirectPath = firstChildPath + } else { + // 拼接追加路径 + if !strings.HasPrefix(routerPath, "/") { + prefix += "/" + } + prefix = prefix + routerPath + redirectPath = prefix + "/" + firstChildPath + } + } + + return prefix, redirectPath +} + +// parseDataToTree 将数据解析为树结构,构建前端所需要下拉树结构 +func (r *ServiceSysMenu) parseDataToTree(sysMenus []model.SysMenu) []model.SysMenu { + // 节点分组 + nodesMap := make(map[string][]model.SysMenu) + // 节点id + treeIds := []string{} + // 树节点 + tree := []model.SysMenu{} + + for _, item := range sysMenus { + parentID := item.ParentID + // 分组 + mapItem, ok := nodesMap[parentID] + if !ok { + mapItem = []model.SysMenu{} + } + mapItem = append(mapItem, item) + nodesMap[parentID] = mapItem + // 记录节点ID + treeIds = append(treeIds, item.MenuID) + } + + for key, value := range nodesMap { + // 选择不是节点ID的作为树节点 + found := false + for _, id := range treeIds { + if id == key { + found = true + break + } + } + if !found { + tree = append(tree, value...) + } + } + + for i, node := range tree { + iN := r.parseDataToTreeComponet(node, &nodesMap) + tree[i] = iN + } + + return tree +} + +// parseDataToTreeComponet 递归函数处理子节点 +func (r *ServiceSysMenu) parseDataToTreeComponet(node model.SysMenu, nodesMap *map[string][]model.SysMenu) model.SysMenu { + id := node.MenuID + children, ok := (*nodesMap)[id] + if ok { + node.Children = children + } + if len(node.Children) > 0 { + for i, child := range node.Children { + icN := r.parseDataToTreeComponet(child, nodesMap) + node.Children[i] = icN + } + } + return node +}