// 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" "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/mcache" "git.5iot.com/iotlcm-public/iotlcm-admin-svr/internal/library/rolepolicy" "git.5iot.com/iotlcm-public/iotlcm-admin-svr/internal/library/rolesync" "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/do" "git.5iot.com/iotlcm-public/iotlcm-admin-svr/internal/model/entity" "git.5iot.com/iotlcm-public/iotlcm-admin-svr/internal/service" "git.5iot.com/iotlcm-public/iotlcm-admin-svr/utility" "slices" "github.com/gogf/gf/v2/database/gdb" "github.com/gogf/gf/v2/errors/gerror" "github.com/gogf/gf/v2/frame/g" "github.com/gogf/gf/v2/os/gtime" "github.com/gogf/gf/v2/util/gconv" ) type sSysRole struct { } func NewSysRole() *sSysRole { return &sSysRole{} } func init() { service.RegisterSysRole(NewSysRole()) } func (l *sSysRole) Model(ctx context.Context, option ...*handler.Option) *gdb.Model { if len(option) == 0 { option = append(option, &handler.Option{ FilterTenant: true, FilterAuth: false, // 角色表不验证数据权限 }) } return handler.Model(dao.SysRole.Ctx(ctx).Master(), option...) } func (l *sSysRole) ModelQuery(ctx context.Context, option ...*handler.Option) *gdb.Model { if len(option) == 0 { option = append(option, &handler.Option{ FilterTenant: true, FilterAuth: false, // 角色表不验证数据权限 }) } return handler.Model(dao.SysRole.Ctx(ctx).Master(), option...) } // 获取租户下角色列表 func (s *sSysRole) List(ctx context.Context, param *model.SysRoleListParam) (res []*model.SysRoleListModel, total int, err error) { db := s.ModelQuery(ctx) if param.RoleName != "" { db = db.WhereLike(dao.SysRole.Columns().RoleName, "%"+param.RoleName+"%") } if param.Status != "" { db = db.Where(dao.SysRole.Columns().Status, param.Status) } if param.DeptId != nil { db = db.Where(dao.SysRole.Columns().DeptId, *param.DeptId) } if len(param.CreatedAt) == 2 { start, end := gtime.NewFromStr(param.CreatedAt[0]), gtime.NewFromStr(param.CreatedAt[1]) db = db.WhereBetween(dao.SysRole.Columns().CreatedAt, start, end.EndOfDay()) } if len(param.RoleIds) > 0 { db = db.WhereIn(dao.SysRole.Columns().RoleId, param.RoleIds) } total, err = db.Count() if err != nil { return nil, 0, err } if total == 0 { return nil, 0, nil } companyMap, err := service.SysDept().GetDeptCompanyMap(ctx) if err != nil { return nil, 0, err } g.Log().Debugf(ctx, "sSysRole.List companyMap: %v", companyMap) err = db.Page(param.Page, param.PageSize).Scan(&res) if err != nil { return nil, 0, err } for _, role := range res { role.DeptName = companyMap[role.DeptId] } return } // 获取角色详情 func (s *sSysRole) View(ctx context.Context, param *model.SysRoleViewParam) (res *model.SysRoleViewModel, err error) { err = s.ModelQuery(ctx).Where(dao.SysRole.Columns().RoleId, param.RoleId).Scan(&res) if err != nil { return nil, err } if res == nil { return nil, gerror.New("角色不存在或已删除") } template, err := rolepolicy.TemplateForRoles(ctx, res.RoleId) if err != nil { return nil, err } res.IsTemplateRole = template != nil defaultRoleCount, err := dao.SysDept.Ctx(ctx).Master().Where("tenant_id", res.TenantId).Where("dept_id", res.DeptId).Where("dept_role", res.RoleId).Count() if err != nil { return nil, err } res.IsDeptDefaultRole = defaultRoleCount > 0 // 获取角色菜单 roleMenus, err := s.getRoleMenuRelations(ctx, param.RoleId) if err != nil { return nil, err } menuIdList := make([]int64, 0) roleMenuDataScopes := make(map[int64]string) for _, menu := range roleMenus { menuIdList = append(menuIdList, menu.MenuId) roleMenuDataScopes[menu.MenuId] = menu.DataScope } res.MenuIds = menuIdList res.RoleMenuDataScopes = roleMenuDataScopes // 获取角色部门 roleDepts, err := s.GetRoleDept(ctx, param.RoleId) if err != nil { return nil, err } deptIdList := make([]int64, 0) for _, dept := range roleDepts { deptIdList = append(deptIdList, dept.DeptId) } res.DeptIds = deptIdList return } func (s *sSysRole) getRoleMenuRelations(ctx context.Context, id int64) (res []*entity.SysRoleMenu, err error) { err = dao.SysRoleMenu.Ctx(ctx).Master(). Where(dao.SysRoleMenu.Columns().TenantId, contexts.GetTenantId(ctx)). Where(dao.SysRoleMenu.Columns().RoleId, id).Scan(&res) return } // 获取角色菜单 func (s *sSysRole) GetRoleMenu(ctx context.Context, id int64) (res []*entity.SysMenu, err error) { err = dao.SysRoleMenu.Ctx(ctx).Master(). Where(dao.SysRoleMenu.Columns().TenantId, contexts.GetTenantId(ctx)). Where(dao.SysRoleMenu.Columns().RoleId, id).Scan(&res) return } // 获取角色部门 func (s *sSysRole) GetRoleDept(ctx context.Context, id int64) (res []*entity.SysDept, err error) { err = dao.SysRoleDept.Ctx(ctx).Master(). Where(dao.SysRoleDept.Columns().TenantId, contexts.GetTenantId(ctx)). Where(dao.SysRoleDept.Columns().RoleId, id).Scan(&res) return } // 获取角色列表对应菜单 func (s *sSysRole) GetRoleListMenu(ctx context.Context, ids []int64) (res []*entity.SysRoleMenu, err error) { res = make([]*entity.SysRoleMenu, 0) if slices.Contains(ids, consts.SuperAdminRoleId) { menus, _, err := service.SysMenu().List(ctx, &model.SysMenuListParam{ Status: consts.SysMenuStatusNormal, }) if err != nil { return nil, err } for _, menu := range menus { res = append(res, &entity.SysRoleMenu{ TenantId: contexts.GetTenantId(ctx), RoleId: consts.SuperAdminRoleId, MenuId: menu.MenuId, }) } return res, nil } err = dao.SysRoleMenu.Ctx(ctx).Master(). Where(dao.SysRoleMenu.Columns().TenantId, contexts.GetTenantId(ctx)). WhereIn(dao.SysRoleMenu.Columns().RoleId, ids).Scan(&res) return } // 新增角色 func (s *sSysRole) Add(ctx context.Context, param *model.SysRoleAddParam) (role *model.SysRoleAddModel, err error) { err = g.DB().Transaction(ctx, func(ctx context.Context, tx gdb.TX) error { var saveErr error role, saveErr = s.addRoleAtomic(ctx, param) if saveErr != nil { return saveErr } return nil }) if err == nil && role != nil { event.EventsInstance().Emit(ctx, consts.EventKeyDBSysRoleCreate, role.RoleId) err = rolesync.EnqueueRole(ctx, role.RoleId, contexts.GetUserId(ctx)) } return } func (s *sSysRole) addRoleAtomic(ctx context.Context, param *model.SysRoleAddParam) (role *model.SysRoleAddModel, err error) { user := contexts.GetUser(ctx) if user == nil { return nil, gerror.New("登录已过期") } data := do.SysRole{} gconv.Struct(param, &data) if data.DataScope == nil || data.DataScope == "" { data.DataScope = consts.SysRoleDataScopePersonal } data.CreatedBy = user.ID data.CreatedAt = gtime.Now() data.CreatedDept = user.DeptId data.TenantId = user.TenantId data.UpdatedBy = user.ID data.UpdatedAt = gtime.Now() roleId, err := s.Model(ctx).Data(data).OmitNil().InsertAndGetId() if err != nil { return nil, err } role = &model.SysRoleAddModel{ RoleId: roleId, } // 新增角色菜单 err = s.RoleMenu(ctx, roleId, param.MenuIds, param.RoleMenuDataScopes) if err != nil { return role, err } return } // 编辑角色 func (s *sSysRole) Edit(ctx context.Context, param *model.SysRoleEditParam) (role *model.SysRoleEditModel, err error) { err = g.DB().Transaction(ctx, func(ctx context.Context, tx gdb.TX) error { if _, err := tx.Ctx(ctx).Model(dao.SysRole.Table()).Where("role_id", param.RoleId).Where("tenant_id", contexts.GetTenantId(ctx)).Data(do.SysRole{UpdatedBy: contexts.GetUserId(ctx)}).Update(); err != nil { return err } var saveErr error role, saveErr = s.editRoleAtomic(ctx, param) if saveErr != nil { return saveErr } return nil }) if err == nil { event.EventsInstance().Emit(ctx, consts.EventKeyDBSysRoleUpdate, param.RoleId) err = rolesync.EnqueueRole(ctx, param.RoleId, contexts.GetUserId(ctx)) } return } func (s *sSysRole) editRoleAtomic(ctx context.Context, param *model.SysRoleEditParam) (role *model.SysRoleEditModel, err error) { role = &model.SysRoleEditModel{ RoleId: param.RoleId, } if err = s.ensureRolesNotInherited(ctx, []int64{param.RoleId}); err != nil { return role, err } user := contexts.GetUser(ctx) if user == nil { return role, gerror.New("登录已过期") } template, templateErr := rolepolicy.TemplateForRoles(ctx, param.RoleId) if templateErr != nil { return role, templateErr } if template != nil && template.RoleId == param.RoleId { if !contexts.IsSuperAdmin(ctx) { return role, gerror.New("仅平台超级管理员可修改代理模板") } if param.Status != nil && *param.Status != "0" { return role, gerror.New("代理模板不能停用,请先切换模板角色") } if param.DeptId != nil && *param.DeptId != template.DeptId { return role, gerror.New("代理模板不能变更所属机构") } } data := do.SysRole{} gconv.Struct(param, &data) data.UpdatedBy = user.ID data.UpdatedAt = gtime.Now() _, err = s.Model(ctx).Data(data).Where(dao.SysRole.Columns().RoleId, param.RoleId).OmitNil().Update() if err != nil { return role, err } // 更新角色菜单 err = s.RoleMenu(ctx, param.RoleId, param.MenuIds, param.RoleMenuDataScopes) if err != nil { return role, err } return } // 删除角色 func (s *sSysRole) Delete(ctx context.Context, param *model.SysRoleDeleteParam) (role *model.SysRoleDeleteModel, err error) { targetIDs := []int64{param.RoleId} if param.RoleId == 0 { targetIDs = param.RoleIds } template, templateErr := rolepolicy.TemplateForRoles(ctx, targetIDs...) if templateErr != nil { return nil, templateErr } if template != nil && (slices.Contains(param.RoleIds, template.RoleId) || param.RoleId == template.RoleId) { return nil, gerror.New("代理模板不能删除,请先切换模板角色") } role = &model.SysRoleDeleteModel{ RoleId: param.RoleId, RoleIds: param.RoleIds, } roleIds := make([]int64, 0) if param.RoleId != 0 { roleIds = append(roleIds, param.RoleId) } else { roleIds = param.RoleIds } if len(roleIds) == 0 { return role, gerror.New("请选择要删除的角色") } if err = s.ensureRolesNotInherited(ctx, roleIds); err != nil { return role, err } if err = s.ensureRolesHaveNoInheritedChildren(ctx, roleIds); err != nil { return role, err } user := contexts.GetUser(ctx) if user == nil { return role, gerror.New("登录已过期") } data := do.SysRole{} data.DeletedBy = user.ID err = g.DB().Transaction(ctx, func(ctx context.Context, _ gdb.TX) error { if _, deleteErr := s.Model(ctx).WhereIn(dao.SysRole.Columns().RoleId, roleIds).Data(data).OmitNil().Update(); deleteErr != nil { return deleteErr } if _, deleteErr := s.Model(ctx).WhereIn(dao.SysRole.Columns().RoleId, roleIds).Delete(); deleteErr != nil { return deleteErr } return nil }) if err != nil { return role, err } event.EventsInstance().Emit(ctx, consts.EventKeyDBSysRoleDelete, roleIds) clearRoleUsersAccessCache(ctx, roleIds...) return } func (s *sSysRole) RoleMenu(ctx context.Context, roleId int64, menuIds []int64, dataScopes map[int64]string) (err error) { template, templateErr := rolepolicy.TemplateForRoles(ctx, roleId) if templateErr != nil { return templateErr } if template != nil && template.RoleId == roleId { if !contexts.IsSuperAdmin(ctx) { return gerror.New("仅平台超级管理员可修改代理模板") } for _, scope := range dataScopes { if scope == consts.SysRoleDataScopeAll || scope == consts.SysRoleDataScopeCustom { return gerror.New("模板按钮不允许全部或自定义数据范围") } } } tenantId := contexts.GetTenantId(ctx) if !contexts.IsSuperAdmin(ctx) { allowed, err := rolepolicy.NormalizeAssignableMenus(ctx, tenantId, contexts.GetDeptId(ctx), contexts.GetUserId(ctx), menuIds) if err != nil { return err } menuIds = allowed for _, scope := range dataScopes { if scope == consts.SysRoleDataScopeAll || scope == consts.SysRoleDataScopeCustom { return gerror.New("不允许授予全部或自定义数据范围") } } } // 查询角色菜单 roleMenu := make([]*entity.SysRoleMenu, 0) err = dao.SysRoleMenu.Ctx(ctx).Master().Where(dao.SysRoleMenu.Columns().TenantId, tenantId).Where(dao.SysRoleMenu.Columns().RoleId, roleId).Scan(&roleMenu) if err != nil { return err } currMenuIds := make([]int64, 0) for _, menu := range roleMenu { currMenuIds = append(currMenuIds, menu.MenuId) } delMenuIds := utility.ArrayRightDiff(currMenuIds, menuIds) addMenuIds := utility.ArrayRightDiff(menuIds, currMenuIds) updateMenuIds := utility.ArrayRightDiff(menuIds, addMenuIds) // 删除角色菜单 if len(delMenuIds) > 0 { _, err = dao.SysRoleMenu.Ctx(ctx).Where(dao.SysRoleMenu.Columns().TenantId, tenantId).Where(dao.SysRoleMenu.Columns().RoleId, roleId).WhereIn(dao.SysRoleMenu.Columns().MenuId, delMenuIds).Delete() if err != nil { return err } } // 新增角色菜单 if len(addMenuIds) > 0 { addData := make([]*do.SysRoleMenu, 0) for _, menuId := range addMenuIds { addData = append(addData, &do.SysRoleMenu{ TenantId: tenantId, RoleId: roleId, MenuId: menuId, DataScope: normalizeRoleMenuDataScope(dataScopes[menuId]), }) } _, err = dao.SysRoleMenu.Ctx(ctx).Data(addData).Insert() if err != nil { return err } } for _, menuId := range updateMenuIds { _, err = dao.SysRoleMenu.Ctx(ctx).Master(). Where(dao.SysRoleMenu.Columns().TenantId, tenantId). Where(dao.SysRoleMenu.Columns().RoleId, roleId). Where(dao.SysRoleMenu.Columns().MenuId, menuId). Data(do.SysRoleMenu{ DataScope: normalizeRoleMenuDataScope(dataScopes[menuId]), }).Update() if err != nil { return err } } clearRoleUsersAccessCache(ctx, roleId) return } func normalizeRoleMenuDataScope(dataScope string) string { switch dataScope { case consts.SysRoleDataScopeAll, consts.SysRoleDataScopeDept, consts.SysRoleDataScopeDeptAndBelow, consts.SysRoleDataScopePersonal, consts.SysRoleDataScopeDeptAndBelowOrPersonal, consts.SysRoleDataScopeOrgAndNextLevel, consts.SysRoleDataScopeOrgNextLevel: return dataScope default: return consts.SysRoleMenuDataScopeRole } } // clearRoleUsersAccessCache removes stale permission caches after role changes. func clearRoleUsersAccessCache(ctx context.Context, roleIds ...int64) { if len(roleIds) == 0 { return } relations := make([]*entity.SysUserRole, 0) if err := dao.SysUserRole.Ctx(ctx).Master(). Fields(dao.SysUserRole.Columns().UserId). Where(dao.SysUserRole.Columns().TenantId, contexts.GetTenantId(ctx)). WhereIn(dao.SysUserRole.Columns().RoleId, roleIds). Scan(&relations); err != nil { g.Log().Warningf(ctx, "clear role users access cache failed, roleIds:%v, err:%+v", roleIds, err) return } seen := make(map[int64]struct{}, len(relations)) for _, relation := range relations { if relation == nil || relation.UserId <= 0 { continue } if _, ok := seen[relation.UserId]; ok { continue } seen[relation.UserId] = struct{}{} if err := mcache.RemoveUserAccessCodeList(ctx, relation.UserId); err != nil { g.Log().Warningf(ctx, "remove user access code cache failed, userId:%d, err:%+v", relation.UserId, err) } if err := mcache.RemoveUserRoleDataAccessCodeList(ctx, relation.UserId); err != nil { g.Log().Warningf(ctx, "remove user role data access code cache failed, userId:%d, err:%+v", relation.UserId, err) } } } // 自定义角色部门数据权限 func (s *sSysRole) RoleDept(ctx context.Context, roleId int64, deptIds []int64) (err error) { tenantId := contexts.GetTenantId(ctx) // 查询角色部门 roleDept := make([]*entity.SysRoleDept, 0) err = dao.SysRoleDept.Ctx(ctx).Master().Where(dao.SysRoleDept.Columns().TenantId, tenantId).Where(dao.SysRoleDept.Columns().RoleId, roleId).Scan(&roleDept) if err != nil { return err } currDeptIds := make([]int64, 0) for _, dept := range roleDept { currDeptIds = append(currDeptIds, dept.DeptId) } delDeptIds := utility.ArrayRightDiff(currDeptIds, deptIds) addDeptIds := utility.ArrayRightDiff(deptIds, currDeptIds) // 删除角色部门 if len(delDeptIds) > 0 { _, err = dao.SysRoleDept.Ctx(ctx).Where(dao.SysRoleDept.Columns().TenantId, tenantId).Where(dao.SysRoleDept.Columns().RoleId, roleId).WhereIn(dao.SysRoleDept.Columns().DeptId, delDeptIds).Delete() } // 新增角色部门 if len(addDeptIds) > 0 { addData := make([]map[string]interface{}, 0) for _, deptId := range addDeptIds { addData = append(addData, map[string]interface{}{ dao.SysRoleDept.Columns().TenantId: tenantId, dao.SysRoleDept.Columns().RoleId: roleId, dao.SysRoleDept.Columns().DeptId: deptId, }) } _, err = dao.SysRoleDept.Ctx(ctx).Data(addData).Insert() } if err == nil { clearRoleUsersAccessCache(ctx, roleId) } return } // 编辑角色数据权限 func (s *sSysRole) EditRoleDataScope(ctx context.Context, model *model.SysRoleDataScopeEditParam) (err error) { template, templateErr := rolepolicy.TemplateForRoles(ctx, model.RoleId) if templateErr != nil { return templateErr } if template != nil && !contexts.IsSuperAdmin(ctx) { return gerror.New("仅平台超级管理员可修改代理模板") } if !contexts.IsSuperAdmin(ctx) || (template != nil && template.RoleId == model.RoleId) { if model.DataScope == consts.SysRoleDataScopeAll || model.DataScope == consts.SysRoleDataScopeCustom || model.DataScope == "" { return gerror.New("不允许全部、自定义或空数据权限") } } if err = s.ensureRolesNotInherited(ctx, []int64{model.RoleId}); err != nil { return err } data := do.SysRole{} gconv.Struct(model, &data) data.UpdatedBy = contexts.GetUserId(ctx) data.UpdatedAt = gtime.Now() if template != nil && template.RoleId == model.RoleId && model.DataScope != consts.SysRoleDataScopeDeptAndBelow { return gerror.New("代理模板数据范围必须为本机构及下级") } err = g.DB().Transaction(ctx, func(ctx context.Context, tx gdb.TX) error { if _, updateErr := s.Model(ctx).Data(data).Where(dao.SysRole.Columns().RoleId, model.RoleId).OmitNil().Update(); updateErr != nil { return updateErr } if err := s.RoleDept(ctx, model.RoleId, model.DeptIds); err != nil { return err } return nil }) if err != nil { return err } event.EventsInstance().Emit(ctx, consts.EventKeyDBSysRoleUpdate, model.RoleId) err = rolesync.EnqueueRole(ctx, model.RoleId, contexts.GetUserId(ctx)) return } func (s *sSysRole) ensureRolesNotInherited(ctx context.Context, roleIds []int64) error { if len(roleIds) == 0 { return nil } if contexts.IsSuperAdmin(ctx) { return nil } count, err := g.Model(dao.SysRole.Table()+" r").Ctx(ctx). LeftJoin(dao.SysDept.Table()+" d", "d.dept_id = r.dept_id AND d.tenant_id = r.tenant_id"). WhereIn("r.role_id", roleIds). Where("r.deleted_at IS NULL"). Where("d.deleted_at IS NULL"). Where("r.created_dept > 0"). Where("r.dept_id <> r.created_dept"). Where("d.parent_id = r.created_dept"). Where("r.tenant_id", contexts.GetTenantId(ctx)). Count() if err != nil { return err } if count > 0 { return gerror.New("上级下发角色由上级统一维护,当前组织只能查看和使用") } return nil } func (s *sSysRole) ensureRolesHaveNoInheritedChildren(ctx context.Context, roleIds []int64) error { if len(roleIds) == 0 { return nil } configured, err := dao.SysConfig.Ctx(ctx).Master().Where("tenant_id", consts.DefaultSystemTenantCode).Where("config_key", rolepolicy.TemplateConfigKey).Count() if err != nil { return err } if configured > 0 { return nil } count, err := g.Model(dao.SysRole.Table()+" r").Ctx(ctx). LeftJoin(dao.SysDept.Table()+" child_dept", "child_dept.dept_id = r.dept_id AND child_dept.tenant_id = r.tenant_id"). Where("r.created_dept IN (SELECT dept_id FROM sys_role WHERE role_id IN (?) AND is_pass = 1 AND deleted_at IS NULL)", roleIds). Where("r.deleted_at IS NULL"). Where("child_dept.deleted_at IS NULL"). Where("r.created_dept > 0"). Where("r.dept_id <> r.created_dept"). Where("child_dept.parent_id = r.created_dept"). Where("r.tenant_id", contexts.GetTenantId(ctx)). Count() if err != nil { return err } if count > 0 { return gerror.New("该角色已下发给下级,不能删除;可关闭后续下发或停用角色") } return nil }