package agent import ( "context" "slices" "strings" "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/rolepolicy" "git.5iot.com/iotlcm-public/iotlcm-admin-svr/internal/library/rolesync" "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" "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" ) type sAgentRole struct { *sAgentMenu } // NewAgentRole 创建代理商角色管理服务实例。 func NewAgentRole() *sAgentRole { return &sAgentRole{ sAgentMenu: &sAgentMenu{ agentBase: newAgentBase(), }, } } func init() { service.RegisterAgentRole(NewAgentRole()) } // RoleList 查询当前登录组织本级角色列表。 func (s *sAgentRole) RoleList(ctx context.Context, in *model.AgentRoleListParam) (list []*model.AgentRoleListModel, total int, err error) { currentDeptId := contexts.GetDeptId(ctx) if currentDeptId == 0 { return nil, 0, gerror.New("当前用户未绑定组织") } if err = service.AgentDept().EnsureCurrentDeptDefaultRole(ctx); err != nil { return nil, 0, err } m := s.roleModel(ctx).Where(dao.SysRole.Columns().DeptId, currentDeptId) if in.RoleName != "" { m = m.WhereLike(dao.SysRole.Columns().RoleName, "%"+in.RoleName+"%") } if in.RoleKey != "" { m = m.WhereLike(dao.SysRole.Columns().RoleKey, "%"+in.RoleKey+"%") } if in.Status != "" { m = m.Where(dao.SysRole.Columns().Status, in.Status) } if len(in.CreatedAt) == 2 { begin, end := gtime.NewFromStr(in.CreatedAt[0]), gtime.NewFromStr(in.CreatedAt[1]) m = m.WhereBetween(dao.SysRole.Columns().CreatedAt, begin, end.EndOfDay()) } total, err = m.Count() if err != nil { return nil, 0, err } err = m.Order(dao.SysRole.Columns().RoleId, "ASC"). Scan(&list) if err != nil { return nil, 0, err } defaultRoleId, err := s.currentDeptDefaultRoleId(ctx) if err != nil { return nil, 0, err } for _, role := range list { role.IsDefaultRole = defaultRoleId != 0 && role.RoleId == defaultRoleId } return list, total, nil } // RoleView 查询角色详情,并返回当前登录组织可分配的菜单树。 func (s *sAgentRole) RoleView(ctx context.Context, roleId int64) (res *model.AgentRoleViewModel, err error) { role, err := service.SysRole().View(ctx, &model.SysRoleViewParam{RoleId: roleId}) if err != nil { return nil, err } if role == nil || role.RoleId == 0 { return nil, gerror.New("角色不存在") } if role.DeptId != contexts.GetDeptId(ctx) { return nil, gerror.New("只能操作本级角色") } tree, allowed, err := s.AllowedMenuTree(ctx, 0) if err != nil { return nil, err } isDefaultRole, err := s.isCurrentDeptDefaultRole(ctx, role.RoleId) if err != nil { return nil, err } return &model.AgentRoleViewModel{ SysRoleViewModel: *role, AllowedMenuIds: allowed, MenuTree: tree, IsDefaultRole: isDefaultRole, }, nil } // RoleSave 保存代理商角色,按角色ID区分新增和修改流程。 func (s *sAgentRole) RoleSave(ctx context.Context, in *model.AgentRoleSaveParam) (roleId int64, err error) { err = g.DB().Transaction(ctx, func(ctx context.Context, tx gdb.TX) error { var saveErr error roleId, saveErr = s.saveTemplateRole(ctx, in) if saveErr != nil { return saveErr } return nil }) if err != nil { return 0, err } if err := rolesync.EnqueueRole(ctx, roleId, contexts.GetUserId(ctx)); err != nil { return roleId, err } return roleId, nil } func (s *sAgentRole) saveTemplateRole(ctx context.Context, in *model.AgentRoleSaveParam) (roleId int64, err error) { user := contexts.GetUser(ctx) if user == nil { return 0, gerror.New("登录已过期") } if in.DeptId == 0 { in.DeptId = user.DeptId } if in.DeptId != user.DeptId { return 0, gerror.New("只能维护本级角色") } if err = s.ensureRoleNotCurrentDeptDefault(ctx, in.RoleId); err != nil { return 0, err } if in.RoleId > 0 { if err = s.ensureRoleNotInherited(ctx, in.RoleId); err != nil { return 0, err } } if in.Status == "" { in.Status = "0" } if in.DataScope == "" { in.DataScope = consts.SysRoleDataScopeDeptAndBelow } if !contexts.IsSuperAdmin(ctx) { allowed, err := s.filterMenuIdsByAllowed(ctx, in.DeptId, in.MenuIds) if err != nil { return 0, err } for _, id := range in.MenuIds { if !slices.Contains(allowed, id) { return 0, gerror.New("角色包含超出本机构权限范围的菜单或按钮") } } switch in.DataScope { case consts.SysRoleDataScopeDept, consts.SysRoleDataScopeDeptAndBelow, consts.SysRoleDataScopePersonal, consts.SysRoleDataScopeDeptAndBelowOrPersonal, consts.SysRoleDataScopeOrgAndNextLevel, consts.SysRoleDataScopeOrgNextLevel: default: return 0, gerror.New("代理角色数据权限不能超出本机构及下级范围") } } // 只有本次保存的目标角色是平台代理模板时才走模板语义; // 普通角色(如「运营人员」)不能被强制改写成 is_pass=1, // 否则任何已配置模板的机构都无法再保存第二个角色。 template, templateErr := rolepolicy.TemplateForRoles(ctx, in.RoleId) if templateErr != nil { return 0, templateErr } if in.IsPass != 0 && in.IsPass != 1 { return 0, gerror.New("传递标记无效") } if template != nil { in.DataScope = consts.SysRoleDataScopeDeptAndBelow } // 未配置平台模板时,保留原有「本级唯一 is_pass=1 角色」下发流程。 legacyPass := in.IsPass == 1 if legacyPass { configured, err := dao.SysConfig.Ctx(ctx).Master().Where("tenant_id", consts.DefaultSystemTenantCode).Where("config_key", rolepolicy.TemplateConfigKey).Count() if err != nil { return 0, err } legacyPass = configured == 0 } if legacyPass { in.DataScope = consts.SysRoleDataScopeDeptAndBelow var dept *entity.SysDept deptModel := s.deptModel(ctx).Where(dao.SysDept.Columns().DeptId, in.DeptId) if dao.SysDept.DB().GetConfig().Type != "sqlite" { deptModel = deptModel.LockUpdate() } if err := deptModel.Scan(&dept); err != nil { return 0, err } if dept == nil { return 0, gerror.New("当前机构不存在") } count, err := s.roleModel(ctx). Where(dao.SysRole.Columns().DeptId, in.DeptId). Where("is_pass", 1). WhereNot(dao.SysRole.Columns().RoleId, in.RoleId). Count() if err != nil { return 0, err } if count > 0 { return 0, gerror.New("每个机构只能配置一个下级管理员角色") } } if in.RoleId == 0 { return s.addRole(ctx, in) } return s.editRole(ctx, in) } // addRole 新增代理商角色,并同步角色的数据权限配置。 func (s *sAgentRole) addRole(ctx context.Context, in *model.AgentRoleSaveParam) (int64, error) { deptId := in.DeptId role, err := service.SysRole().Add(ctx, &model.SysRoleAddParam{ RoleName: in.RoleName, RoleKey: in.RoleKey, RoleSort: in.RoleSort, DataScope: in.DataScope, MenuCheckStrictly: in.MenuCheckStrictly, Status: in.Status, Remark: in.Remark, MenuIds: in.MenuIds, DeptId: &deptId, }) if err != nil { return 0, err } if role == nil { return 0, nil } if _, err = dao.SysRole.Ctx(ctx). Where(dao.SysRole.Columns().RoleId, role.RoleId). Data(do.SysRole{IsPass: in.IsPass}).Update(); err != nil { return 0, err } if err = s.updateRoleDataScope(ctx, role.RoleId, in); err != nil { return 0, err } return role.RoleId, nil } // editRole 修改代理商角色,并禁止在代理商页面移动角色所属组织。 func (s *sAgentRole) editRole(ctx context.Context, in *model.AgentRoleSaveParam) (int64, error) { current, err := s.RoleView(ctx, in.RoleId) if err != nil { return 0, err } if current.DeptId != in.DeptId { return 0, gerror.New("不能在代理商页面移动角色所属组织") } deptId := in.DeptId roleName := in.RoleName roleKey := in.RoleKey roleSort := in.RoleSort menuCheckStrictly := in.MenuCheckStrictly status := in.Status remark := in.Remark role, err := service.SysRole().Edit(ctx, &model.SysRoleEditParam{ RoleId: in.RoleId, RoleName: &roleName, RoleKey: &roleKey, RoleSort: &roleSort, MenuCheckStrictly: &menuCheckStrictly, Status: &status, Remark: &remark, MenuIds: in.MenuIds, DeptId: &deptId, }) if err != nil { return 0, err } if err = s.updateRoleDataScope(ctx, in.RoleId, in); err != nil { return 0, err } if _, err = dao.SysRole.Ctx(ctx). Where(dao.SysRole.Columns().RoleId, in.RoleId). Data(do.SysRole{IsPass: in.IsPass}).Update(); err != nil { return 0, err } if role == nil { return in.RoleId, nil } return role.RoleId, nil } // updateRoleDataScope 更新角色数据权限和组织树勾选配置。 func (s *sAgentRole) updateRoleDataScope(ctx context.Context, roleId int64, in *model.AgentRoleSaveParam) error { deptCheckStrictly := in.DeptCheckStrictly return service.SysRole().EditRoleDataScope(ctx, &model.SysRoleDataScopeEditParam{ RoleId: roleId, DataScope: in.DataScope, DeptCheckStrictly: &deptCheckStrictly, DeptIds: []int64{in.DeptId}, }) } func (s *sAgentRole) isCurrentDeptDefaultRole(ctx context.Context, roleId int64) (bool, error) { defaultRoleId, err := s.currentDeptDefaultRoleId(ctx) if err != nil { return false, err } return defaultRoleId != 0 && roleId == defaultRoleId, nil } func (s *sAgentRole) ensureRoleInCurrentDept(ctx context.Context, roleId int64) error { currentDeptId := contexts.GetDeptId(ctx) if currentDeptId == 0 { return gerror.New("当前用户未绑定组织") } count, err := s.roleModel(ctx). Where(dao.SysRole.Columns().RoleId, roleId). Where(dao.SysRole.Columns().DeptId, currentDeptId). Count() if err != nil { return err } if count == 0 { return gerror.New("只能操作本级角色") } return nil } func (s *sAgentRole) ensureRoleNotInherited(ctx context.Context, roleId int64) error { if roleId <= 0 { return nil } if contexts.IsSuperAdmin(ctx) { return nil } count, err := s.inheritedRoleModel(ctx). Where("r.role_id", roleId). Count() if err != nil { return err } if count > 0 { return gerror.New("上级下发角色由上级角色统一维护,当前组织只能查看") } return nil } func (s *sAgentRole) ensureRoleNoUsers(ctx context.Context, roleId int64) error { count, err := dao.SysUserRole.Ctx(ctx). Where(dao.SysUserRole.Columns().RoleId, roleId). Count() if err != nil { return err } if count > 0 { return gerror.New("该角色已绑定用户,不能删除") } return nil } // RoleDelete 删除代理商角色,删除前校验本级机构默认角色不可被删除。 func (s *sAgentRole) RoleDelete(ctx context.Context, roleIds []int64) (err error) { for _, roleId := range roleIds { if err = s.ensureRoleInCurrentDept(ctx, roleId); err != nil { return err } if err = s.ensureRoleNotCurrentDeptDefault(ctx, roleId); err != nil { return err } if err = s.ensureRoleNotInherited(ctx, roleId); err != nil { return err } if err = s.ensureRoleNoUsers(ctx, roleId); err != nil { return err } } _, err = service.SysRole().Delete(ctx, &model.SysRoleDeleteParam{RoleIds: roleIds}) return err } func (s *sAgentRole) inheritedRoleModel(ctx context.Context) *gdb.Model { m := g.Model(dao.SysRole.Table()+" r"). LeftJoin(dao.SysDept.Table()+" d", "d.dept_id = r.dept_id AND d.tenant_id = r.tenant_id"). 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") if tenantId := contexts.GetTenantId(ctx); tenantId != "" { m = m.Where("r.tenant_id", tenantId) } return m } func (s *sAgentRole) SyncInheritedRole(ctx context.Context, message *model.AgentRoleSyncMessage) error { if message == nil || message.SourceRoleId <= 0 { return gerror.New("invalid agent role sync message") } template, templateErr := rolepolicy.Template(ctx) if templateErr != nil { return templateErr } if template != nil && message.SourceDeptId == template.DeptId && (message.TenantId == template.TenantId || message.TenantId == "") { return syncTemplateRoles(ctx, template, message.ActorId) } var source *entity.SysRole sourceModel := dao.SysRole.Ctx(ctx).Where(dao.SysRole.Columns().RoleId, message.SourceRoleId) if message.TenantId != "" { sourceModel = sourceModel.Where(dao.SysRole.Columns().TenantId, message.TenantId) } if err := sourceModel.Scan(&source); err != nil { return err } if source == nil || source.IsPass != 1 || source.DeletedAt != nil { return nil } if message.SourceDeptId > 0 && message.SourceDeptId != source.DeptId { return gerror.New("role sync source department changed") } var children []*entity.SysDept if err := dao.SysDept.Ctx(ctx). Where(dao.SysDept.Columns().TenantId, source.TenantId). Where(dao.SysDept.Columns().ParentId, source.DeptId). Where(dao.SysDept.Columns().DeptType, 1). Scan(&children); err != nil { return err } failed := 0 var firstErr error for _, child := range children { err := g.DB().Transaction(ctx, func(ctx context.Context, tx gdb.TX) error { return s.syncInheritedRoleChildrenTx(ctx, tx, message.SourceRoleId, source.TenantId, source.DeptId, message.ActorId, child.DeptId) }) if err != nil { failed++ if firstErr == nil { firstErr = err } g.Log().Warningf(ctx, "agent role sync child failed, sourceRoleId:%d, childDeptId:%d, err:%v", source.RoleId, child.DeptId, err) } } if firstErr != nil { return gerror.Wrapf(firstErr, "agent role sync failed for %d of %d children", failed, len(children)) } return nil } func (s *sAgentRole) syncInheritedRoleChildrenTx(ctx context.Context, tx gdb.TX, sourceRoleID int64, tenantId string, sourceDeptId, actorId, childDeptId int64) error { if sourceRoleID == 0 { return nil } var source *entity.SysRole sourceModel := tx.Ctx(ctx).Model(dao.SysRole.Table()).Where(dao.SysRole.Columns().RoleId, sourceRoleID) if tenantId != "" { sourceModel = sourceModel.Where(dao.SysRole.Columns().TenantId, tenantId) } if !strings.EqualFold(g.DB().GetConfig().Type, "sqlite") { sourceModel = sourceModel.LockUpdate() } if err := sourceModel.Scan(&source); err != nil { return err } if source == nil || source.RoleId == 0 || source.DeletedAt != nil || source.IsPass != 1 { return nil } if sourceDeptId > 0 && source.DeptId != sourceDeptId { return gerror.New("role sync source department changed") } var childDepts []*entity.SysDept childDeptModel := tx.Ctx(ctx).Model(dao.SysDept.Table()).Where(dao.SysDept.Columns().TenantId, source.TenantId).Where(dao.SysDept.Columns().ParentId, source.DeptId).Where(dao.SysDept.Columns().DeptType, 1).Where(dao.SysDept.Columns().DeptId, childDeptId) if !strings.EqualFold(g.DB().GetConfig().Type, "sqlite") { childDeptModel = childDeptModel.LockUpdate() } if err := childDeptModel.Scan(&childDepts); err != nil { return err } var menus []*entity.SysRoleMenu if err := tx.Ctx(ctx).Model(dao.SysRoleMenu.Table()).Where(dao.SysRoleMenu.Columns().RoleId, sourceRoleID).OrderAsc(dao.SysRoleMenu.Columns().MenuId).Scan(&menus); err != nil { return err } menuIDs := make([]int64, 0, len(menus)) for _, menu := range menus { menuIDs = append(menuIDs, menu.MenuId) } allowed, err := rolepolicy.RestrictMenus(ctx, source.TenantId, source.DeptId, menuIDs) if err != nil { return err } template, templateErr := rolepolicy.Template(ctx) if templateErr != nil { return templateErr } filtered := make([]*entity.SysRoleMenu, 0, len(menus)) for _, menu := range menus { if slices.Contains(allowed, menu.MenuId) { copy := *menu if template != nil { copy.DataScope = consts.SysRoleMenuDataScopeRole } filtered = append(filtered, ©) } } menus = filtered for _, childDept := range childDepts { var children []*entity.SysRole if err := tx.Ctx(ctx).Model(dao.SysRole.Table()+" r"). Fields("r.*"). Where("r.tenant_id", source.TenantId). Where("r.dept_id", childDept.DeptId). Where("r.created_dept", source.DeptId). Where("r.role_id", childDept.DeptRole). Where("r.deleted_at IS NULL"). Scan(&children); err != nil { return err } if len(children) > 1 { return gerror.Newf("multiple inherited roles in department %d for role %d", childDept.DeptId, source.RoleId) } if len(children) == 0 { var legacy *entity.SysRole if childDept.DeptRole > 0 { if err := tx.Ctx(ctx).Model(dao.SysRole.Table()). Where(dao.SysRole.Columns().RoleId, childDept.DeptRole). Where(dao.SysRole.Columns().DeptId, childDept.DeptId). Where(dao.SysRole.Columns().TenantId, source.TenantId). Scan(&legacy); err != nil { return err } } if legacy != nil && legacy.RoleId > 0 { if legacy.CreatedDept != source.DeptId || legacy.RoleKey != source.RoleKey { return gerror.Newf("cannot identify inherited role for department %d", childDept.DeptId) } children = []*entity.SysRole{legacy} } else { if childDept.DeptRole > 0 { return gerror.Newf("missing default role for department %d", childDept.DeptId) } roleId, err := s.createInheritedRoleTx(ctx, tx, source, childDept.DeptId, menus, actorId) if err != nil { return err } children = []*entity.SysRole{{RoleId: roleId}} } } for _, child := range children { if _, err := tx.Ctx(ctx).Model(dao.SysRole.Table()).Where(dao.SysRole.Columns().RoleId, child.RoleId).Data(do.SysRole{RoleName: strings.TrimSpace(source.RoleName), RoleKey: source.RoleKey, RoleSort: source.RoleSort, DataScope: source.DataScope, MenuCheckStrictly: source.MenuCheckStrictly, DeptCheckStrictly: source.DeptCheckStrictly, Status: source.Status, Remark: source.Remark, UpdatedBy: actorId, UpdatedAt: gtime.Now()}).OmitNil().Update(); err != nil { return err } if _, err := tx.Ctx(ctx).Model(dao.SysRoleMenu.Table()).Where(dao.SysRoleMenu.Columns().RoleId, child.RoleId).Delete(); err != nil { return err } for _, menu := range menus { if _, err := tx.Ctx(ctx).Model(dao.SysRoleMenu.Table()).Data(do.SysRoleMenu{TenantId: source.TenantId, RoleId: child.RoleId, MenuId: menu.MenuId, DataScope: menu.DataScope}).OmitNil().Insert(); err != nil { return err } } } } return nil } func (s *sAgentRole) createInheritedRoleTx(ctx context.Context, tx gdb.TX, source *entity.SysRole, deptId int64, menus []*entity.SysRoleMenu, actorId int64) (int64, error) { if actorId == 0 { actorId = source.UpdatedBy } roleId, err := tx.Ctx(ctx).Model(dao.SysRole.Table()).Data(do.SysRole{TenantId: source.TenantId, DeptId: deptId, RoleName: strings.TrimSpace(source.RoleName), RoleKey: source.RoleKey, RoleSort: source.RoleSort, DataScope: source.DataScope, MenuCheckStrictly: source.MenuCheckStrictly, DeptCheckStrictly: source.DeptCheckStrictly, IsPass: 0, Status: source.Status, CreatedDept: source.DeptId, CreatedBy: actorId, CreatedAt: gtime.Now(), UpdatedBy: actorId, UpdatedAt: gtime.Now(), Remark: source.Remark}).OmitNil().InsertAndGetId() if err != nil { return 0, err } for _, menu := range menus { if _, err = tx.Ctx(ctx).Model(dao.SysRoleMenu.Table()).Data(do.SysRoleMenu{TenantId: source.TenantId, RoleId: roleId, MenuId: menu.MenuId, DataScope: menu.DataScope}).OmitNil().Insert(); err != nil { return 0, err } } _, err = tx.Ctx(ctx).Model(dao.SysRoleDept.Table()).Data(do.SysRoleDept{RoleId: roleId, DeptId: deptId}).OmitNil().Insert() return roleId, err }