// package system // @Link https://github.com/xiujiecn/xiu-admin // @Copyright Copyright (c) 2025 LiXiujie // @Author Lxj // @License https://github.com/xiujiecn/xiu-admin/blob/master/LICENSE package system import ( "context" "fmt" v1 "git.5iot.com/iotlcm-public/iotlcm-admin-svr/api/system/v1" "git.5iot.com/iotlcm-public/iotlcm-admin-svr/internal/consts" "git.5iot.com/iotlcm-public/iotlcm-admin-svr/internal/dao" "git.5iot.com/iotlcm-public/iotlcm-admin-svr/internal/library/contexts" "git.5iot.com/iotlcm-public/iotlcm-admin-svr/internal/library/event" "git.5iot.com/iotlcm-public/iotlcm-admin-svr/internal/library/rolepolicy" "git.5iot.com/iotlcm-public/iotlcm-admin-svr/internal/library/xgorm/handler" "git.5iot.com/iotlcm-public/iotlcm-admin-svr/internal/model" "git.5iot.com/iotlcm-public/iotlcm-admin-svr/internal/model/entity" "git.5iot.com/iotlcm-public/iotlcm-admin-svr/internal/service" "slices" "sort" "strings" "github.com/gogf/gf/v2/database/gdb" "github.com/gogf/gf/v2/errors/gcode" "github.com/gogf/gf/v2/errors/gerror" "github.com/gogf/gf/v2/frame/g" "github.com/gogf/gf/v2/os/gtime" ) type sSysMenu struct { } func NewSysMenu() *sSysMenu { return &sSysMenu{} } func init() { service.RegisterSysMenu(NewSysMenu()) } func (s *sSysMenu) Model(ctx context.Context, option ...*handler.Option) *gdb.Model { if len(option) == 0 { option = append(option, &handler.Option{ FilterTenant: false, FilterAuth: false, }) } return handler.Model(dao.SysMenu.Ctx(ctx), option...) } func (s *sSysMenu) ModelQuery(ctx context.Context, option ...*handler.Option) *gdb.Model { if len(option) == 0 { option = append(option, &handler.Option{ FilterTenant: false, FilterAuth: false, }) } return handler.Model(service.MemoryDB().DB(ctx).Ctx(ctx).Model(dao.SysMenu.Table()), option...) } func (l *sSysMenu) List(ctx context.Context, param *model.SysMenuListParam) (data []*model.SysMenuListModel, total int, err error) { m := l.ModelQuery(ctx) if param.MenuIds != nil { m = m.WhereIn(dao.SysMenu.Columns().MenuId, param.MenuIds) } if param.MenuName != "" { m = m.WhereLike(dao.SysMenu.Columns().MenuName, "%"+param.MenuName+"%") } if param.Status != "" { m = m.Where(dao.SysMenu.Columns().Status, param.Status) } total, err = m.Count() if err != nil { return nil, 0, err } err = m.Order(dao.SysMenu.Columns().OrderNum).Page(0, 5000).Scan(&data) if err == nil { placeDocumentCenterInMenuList(data) } return } // 获取租户菜单列表, 系统租户返回所有菜单,其他租户返回当前租户菜单 func (l *sSysMenu) GetTenantMenu(ctx context.Context, query *model.SysMenuListParam) (data []*model.SysMenuListModel, total int, err error) { // 获取当前用户信息 tenantId := contexts.GetTenantId(ctx) m := l.ModelQuery(ctx) if tenantId != consts.DefaultSystemTenantCode { // 获取租户信息 tenantInfo, err := service.SysTenant().View(ctx, &model.SysTenantViewParam{ TenantId: tenantId, }) if err != nil { return nil, 0, err } // 获取租户套餐 tenantPackage, err := service.SysTenantPackage().View(ctx, &model.SysTenantPackageViewParam{ PackageId: tenantInfo.PackageId, }) if err != nil { return nil, 0, err } if tenantPackage == nil { g.Log().Errorf(ctx, "sSysMenu.GetTenantMenu not found tenantPackage. TenantId:%s PackageId:%d", tenantId, tenantInfo.PackageId) return nil, 0, gerror.NewCode(gcode.CodeInternalError, "系统内部错误") } if tenantPackage.MenuIds != "" { menuIds := strings.Split(tenantPackage.MenuIds, ",") m = m.WhereIn(dao.SysMenu.Columns().MenuId, menuIds) } else { return nil, 0, nil } } if query.MenuName != "" { m = m.WhereLike(dao.SysMenu.Columns().MenuName, "%"+query.MenuName+"%") } if query.Status != "" { m = m.Where(dao.SysMenu.Columns().Status, query.Status) } if query.MenuIds != nil { m = m.WhereIn(dao.SysMenu.Columns().MenuId, query.MenuIds) } total, err = m.Count() if err != nil { return nil, 0, err } err = m.Order(dao.SysMenu.Columns().OrderNum).Page(0, 5000).Scan(&data) if err == nil { placeDocumentCenterInMenuList(data) } return } func placeDocumentCenterInMenuList(menus []*model.SysMenuListModel) { order := 33 for _, menu := range menus { if menu.ParentId == 0 && menu.Path == "batchExecutionLog" { order = menu.OrderNum + 1 break } } for _, menu := range menus { if menu.MenuId == 2153 && menu.Path == "docs" && menu.MenuType == consts.SysMenuTypeDir && menu.ParentId != 0 { menu.ParentId = 0 menu.OrderNum = order } } sort.SliceStable(menus, func(i, j int) bool { return menus[i].OrderNum < menus[j].OrderNum }) } // GetTenantMenuTree 获取租户菜单树。 // // 与 GetTenantMenu 使用完全相同的租户隔离与筛选条件,区别只在于返回结构: // 这里由后端一次性建好嵌套 children,前端可直接交给 vxe-table 渲染, // 无需再依赖 treeConfig.transform 在浏览器主线程把扁平列表二次转成树。 // // 建树采用「id 索引 + 单次遍历」的实现(O(n)),不使用递归逐层过滤(O(n²)), // 因为菜单量级已达数百行(其中绝大多数是 F 按钮),递归过滤会有可观开销。 func (l *sSysMenu) GetTenantMenuTree(ctx context.Context, query *model.SysMenuListParam) (items []*model.SysMenuFullTreeModel, err error) { list, _, err := l.GetTenantMenu(ctx, query) if err != nil { return nil, err } return buildSysMenuFullTree(list), nil } // buildSysMenuFullTree 把扁平菜单列表组装成嵌套树。 // // 处理两种「可见根」: // 1. ParentId == 0 的顶层菜单; // 2. ParentId 指向本批次中不存在的菜单(例如按状态/名称过滤后父级被滤掉), // 这类节点若丢弃会导致整棵子树不可见,因此提升为根节点。 func buildSysMenuFullTree(list []*model.SysMenuListModel) []*model.SysMenuFullTreeModel { if len(list) == 0 { return make([]*model.SysMenuFullTreeModel, 0) } // 保持入参顺序(查询已按 order_num 排序),同时建立 id 索引。 nodes := make([]*model.SysMenuFullTreeModel, 0, len(list)) nodeMaps := make(map[int64]*model.SysMenuFullTreeModel, len(list)) for _, item := range list { if item == nil { continue } node := &model.SysMenuFullTreeModel{ SysMenuListModel: *item, Children: make([]*model.SysMenuFullTreeModel, 0), } nodes = append(nodes, node) nodeMaps[item.MenuId] = node } roots := make([]*model.SysMenuFullTreeModel, 0) for _, node := range nodes { if node.ParentId == 0 { roots = append(roots, node) continue } if parent, ok := nodeMaps[node.ParentId]; ok { parent.Children = append(parent.Children, node) continue } // 父级不在本批次内:提升为根,避免子树整体丢失。 roots = append(roots, node) } return roots } // 构建树结构 func (l *sSysMenu) MenuTree(ctx context.Context, parentMenu *model.SysMenuTree, menuList []*entity.SysMenu) (data []*model.SysMenuTree, err error) { data = make([]*model.SysMenuTree, 0) for _, menu := range menuList { pId := int64(0) if parentMenu != nil { pId = parentMenu.MenuId } if menu.ParentId == pId { item := &model.SysMenuTree{ SysMenu: *menu, Children: nil, } subMenu, err := l.MenuTree(ctx, item, menuList) if err != nil { return nil, err } item.Children = subMenu data = append(data, item) } } return } // 获取用户动态路由列表 func (l *sSysMenu) GetUserMenu(ctx context.Context) (data []*entity.SysMenu, err error) { assignedRoleIDs, err := service.SysUser().GetUserRoleIds(ctx, contexts.GetUserId(ctx)) if err != nil { return nil, err } roleIds := make([]int64, 0) roleIds = append(roleIds, assignedRoleIDs...) g.Log().Infof(ctx, "sSysMenu.GetUserMenu roleIds: %v", roleIds) var validRoles []*entity.SysRole if len(roleIds) > 0 { if err := dao.SysRole.Ctx(ctx).Where("tenant_id", contexts.GetTenantId(ctx)).WhereIn("role_id", roleIds).Where("status", "0").Scan(&validRoles); err != nil { return nil, err } } roleIds = nil for _, role := range validRoles { roleIds = append(roleIds, role.RoleId) } if contexts.IsSuperAdmin(ctx) && contexts.GetTenantId(ctx) == consts.DefaultSystemTenantCode && slices.Contains(roleIds, consts.SuperAdminRoleId) { err = l.ModelQuery(ctx).Where(dao.SysMenu.Columns().Status, consts.SysMenuStatusNormal). Order(dao.SysMenu.Columns().OrderNum).Scan(&data) return } roleIds = slices.DeleteFunc(roleIds, func(roleID int64) bool { return roleID == consts.SuperAdminRoleId }) // 获取角色菜单列表 menuList, err := service.SysRole().GetRoleListMenu(ctx, roleIds) if err != nil { return nil, err } g.Log().Infof(ctx, "sSysMenu.GetUserMenu menuList: %v", menuList) menuIds := make([]int64, 0) for _, menu := range menuList { menuIds = append(menuIds, menu.MenuId) } menuIds, err = rolepolicy.RestrictMenus(ctx, contexts.GetTenantId(ctx), contexts.GetDeptId(ctx), menuIds) if err != nil { return nil, err } if len(menuIds) == 0 { return []*entity.SysMenu{}, nil } g.Log().Infof(ctx, "sSysMenu.GetUserMenu menuIds: %v", menuIds) // 只返回角色直接勾选的菜单;未勾选的父目录不强制补齐, // 树构建时会把缺少授权父级的菜单提升为一级菜单。 err = l.ModelQuery(ctx).WhereIn(dao.SysMenu.Columns().MenuId, menuIds). Where(dao.SysMenu.Columns().Status, consts.SysMenuStatusNormal). Order(dao.SysMenu.Columns().OrderNum).Scan(&data) g.Log().Infof(ctx, "sSysMenu.GetUserMenu data: %v", data) return } // 构建用户动态路由树 func (l *sSysMenu) BuildUserMenuTree(ctx context.Context, parentMenu *v1.RouteMenu, menuList []*entity.SysMenu, allPath string) (data v1.MenuAllRes, err error) { data = make(v1.MenuAllRes, 0) menuIdSet := make(map[int64]struct{}, len(menuList)) for _, menu := range menuList { menuIdSet[menu.MenuId] = struct{}{} } for _, menu := range menuList { if menu.Status == consts.SysMenuStatusDisable { continue } if menu.MenuType == consts.SysMenuTypeBtn { continue } pId := int64(0) if parentMenu != nil { pId = parentMenu.Id } isRootOrphan := parentMenu == nil && menu.ParentId != 0 if isRootOrphan { _, isRootOrphan = menuIdSet[menu.ParentId] isRootOrphan = !isRootOrphan } if menu.ParentId == pId || isRootOrphan { openInNewWindow := false var link *string if menu.IsFrame == consts.SysMenuIsFrameYes { openInNewWindow = true link = &menu.Path } query := make(map[string]string) if menu.QueryParam != "" { query2 := strings.Split(menu.QueryParam, "&") for _, q := range query2 { kv := strings.Split(q, "=") if len(kv) == 2 { query[kv[0]] = kv[1] } } } hideInMenu := false if menu.Visible == consts.SysMenuVisibleHide { hideInMenu = true } keepAlive := true item := &v1.RouteMenu{ Id: menu.MenuId, ParentId: menu.ParentId, Path: menu.Path, Redirect: nil, Name: fmt.Sprintf("menu_%d", menu.MenuId), Component: menu.Component, Meta: &v1.RouteMeta{ Title: menu.MenuName, Icon: &menu.Icon, Order: &menu.OrderNum, OpenInNewWindow: &openInNewWindow, Query: query, HideInMenu: &hideInMenu, Authority: normalizeMenuAccessCodes(menu.Perms), Link: link, KeepAlive: &keepAlive, }, } if hideInMenu { activePath := allPath + "/" if strings.Contains(menu.Path, "-") { activePath = activePath + strings.Split(menu.Path, "-")[0] } item.Meta.ActivePath = &activePath } subMenu, err := l.BuildUserMenuTree(ctx, item, menuList, allPath+"/"+menu.Path) if err != nil { return nil, err } item.Children = subMenu if len(subMenu) == 0 && menu.MenuType == consts.SysMenuTypeDir { item.Children = []*v1.RouteMenu{} } data = append(data, item) } } // 排序 sort.Slice(data, func(i, j int) bool { return *data[i].Meta.Order < *data[j].Meta.Order }) return } // 获取用户动态路由树 func (l *sSysMenu) GetUserMenuTree(ctx context.Context) (data v1.MenuAllRes, err error) { // 获取用户动态路由列表 menuList, err := l.GetUserMenu(ctx) if err != nil { return nil, err } menuList = placeDocumentCenterAfterExecutionLog(menuList) // 构建树结构 data, err = l.BuildUserMenuTree(ctx, nil, menuList, "") if err != nil { return nil, err } return } func placeDocumentCenterAfterExecutionLog(menuList []*entity.SysMenu) []*entity.SysMenu { menus := append([]*entity.SysMenu(nil), menuList...) order := 33 for _, menu := range menus { if menu.ParentId == 0 && menu.Path == "batchExecutionLog" { order = menu.OrderNum + 1 break } } for index, menu := range menus { if menu.Path != "docs" || menu.MenuName != "文档中心" || menu.MenuType != consts.SysMenuTypeDir || menu.ParentId == 0 { continue } copyOfMenu := *menu copyOfMenu.ParentId = 0 copyOfMenu.Level = 1 copyOfMenu.OrderNum = order menus[index] = ©OfMenu } return menus } func normalizeMenuAccessCodes(perms string) []string { items := strings.Split(perms, ",") codes := make([]string, 0, len(items)) seen := make(map[string]struct{}, len(items)) for _, item := range items { item = strings.TrimSpace(item) if item == "" { continue } if !strings.HasPrefix(item, consts.SysCheckPermissionPrefix) { item = consts.SysCheckPermissionMenuPrefix + item } if _, ok := seen[item]; ok { continue } seen[item] = struct{}{} codes = append(codes, item) } return codes } func (l *sSysMenu) GetSysMenuView(ctx context.Context, menuId int64) (data *model.SysMenuViewModel, err error) { menu := &entity.SysMenu{} err = l.ModelQuery(ctx).Where(dao.SysMenu.Columns().MenuId, menuId).Scan(menu) if err != nil { return nil, err } data = &model.SysMenuViewModel{ SysMenu: *menu, } return } func (l *sSysMenu) UpdateSysMenu(ctx context.Context, menu *model.SysMenuUpdateModel) (data *model.SysMenuViewModel, err error) { menu.UpdatedAt = gtime.Now() menu.UpdatedBy = contexts.GetUserId(ctx) _, err = l.Model(ctx).Where(dao.SysMenu.Columns().MenuId, menu.MenuId).Data(menu).OmitNil().Update(menu) if err != nil { return nil, err } event.EventsInstance().Emit(ctx, consts.EventKeyDBSysMenuUpdate, menu.MenuId) data, err = l.GetSysMenuView(ctx, menu.MenuId) if err != nil { return nil, err } return } func (l *sSysMenu) AddSysMenu(ctx context.Context, menu *model.SysMenuAddModel) (data *model.SysMenuViewModel, err error) { menu.CreatedDept = contexts.GetDeptId(ctx) menu.CreatedAt = gtime.Now() menu.CreatedBy = contexts.GetUserId(ctx) menu.UpdatedAt = gtime.Now() menu.UpdatedBy = contexts.GetUserId(ctx) re, err := l.Model(ctx).Data(menu).Insert() if err != nil { return nil, err } menuId, err := re.LastInsertId() if err != nil { return nil, err } event.EventsInstance().Emit(ctx, consts.EventKeyDBSysMenuCreate, menuId) data, err = l.GetSysMenuView(ctx, menuId) if err != nil { return nil, err } return } func (l *sSysMenu) DeleteSysMenu(ctx context.Context, menuId int64) (err error) { _, err = l.Model(ctx).Where(dao.SysMenu.Columns().MenuId, menuId).Delete() event.EventsInstance().Emit(ctx, consts.EventKeyDBSysMenuDelete, menuId) return } func (s *sSysMenu) GetFastList(ctx context.Context) (res map[int64]*entity.SysMenu, err error) { var models []*entity.SysMenu if err = s.ModelQuery(ctx).Scan(&models); err != nil { return } res = make(map[int64]*entity.SysMenu, len(models)) for _, v := range models { res[v.MenuId] = v } return }