From 197064820b5108e446fc7ff36688344a24f2aba6 Mon Sep 17 00:00:00 2001 From: zhonghua Date: Wed, 8 Apr 2026 11:07:11 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20=E8=AF=84=E5=A7=94=E8=A7=92=E8=89=B2?= =?UTF-8?q?=E6=9D=83=E9=99=90=E8=A1=A5=E5=85=A8=E4=B8=8E=E7=A7=9F=E6=88=B7?= =?UTF-8?q?=E8=AF=84=E5=A7=94=E8=8F=9C=E5=8D=95=E5=90=88=E5=B9=B6=EF=BC=8C?= =?UTF-8?q?=E6=9B=B4=E6=96=B0=20menu-config=20=E8=AF=B4=E6=98=8E?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Made-with: Cursor --- .../impl/JudgesManagementServiceImpl.java | 4 + .../config/JudgeRolePermissionConfigurer.java | 136 ++++++++++++++++++ .../sys/service/impl/SysMenuServiceImpl.java | 37 ++++- docs/design/menu-config.md | 16 ++- 4 files changed, 189 insertions(+), 4 deletions(-) create mode 100644 backend-java/src/main/java/com/competition/modules/sys/config/JudgeRolePermissionConfigurer.java diff --git a/backend-java/src/main/java/com/competition/modules/biz/judge/service/impl/JudgesManagementServiceImpl.java b/backend-java/src/main/java/com/competition/modules/biz/judge/service/impl/JudgesManagementServiceImpl.java index bd813bc..9245c18 100644 --- a/backend-java/src/main/java/com/competition/modules/biz/judge/service/impl/JudgesManagementServiceImpl.java +++ b/backend-java/src/main/java/com/competition/modules/biz/judge/service/impl/JudgesManagementServiceImpl.java @@ -13,6 +13,7 @@ import com.competition.modules.sys.entity.SysUser; import com.competition.modules.sys.entity.SysUserRole; import com.competition.modules.sys.mapper.SysRoleMapper; import com.competition.modules.sys.mapper.SysTenantMapper; +import com.competition.modules.sys.config.JudgeRolePermissionConfigurer; import com.competition.modules.sys.mapper.SysUserMapper; import com.competition.modules.sys.mapper.SysUserRoleMapper; import lombok.RequiredArgsConstructor; @@ -34,6 +35,7 @@ public class JudgesManagementServiceImpl implements IJudgesManagementService { private final SysRoleMapper sysRoleMapper; private final SysTenantMapper sysTenantMapper; private final PasswordEncoder passwordEncoder; + private final JudgeRolePermissionConfigurer judgeRolePermissionConfigurer; /** * 获取评委专属租户 ID @@ -57,6 +59,7 @@ public class JudgesManagementServiceImpl implements IJudgesManagementService { wrapper.eq(SysRole::getTenantId, tenantId); SysRole role = sysRoleMapper.selectOne(wrapper); if (role != null) { + judgeRolePermissionConfigurer.ensureJudgeRolePermissions(tenantId, role.getId()); return role.getId(); } // 自动创建 judge 角色 @@ -67,6 +70,7 @@ public class JudgesManagementServiceImpl implements IJudgesManagementService { role.setDescription("评委角色"); sysRoleMapper.insert(role); log.info("自动创建评委角色,租户ID:{},角色ID:{}", tenantId, role.getId()); + judgeRolePermissionConfigurer.ensureJudgeRolePermissions(tenantId, role.getId()); return role.getId(); } diff --git a/backend-java/src/main/java/com/competition/modules/sys/config/JudgeRolePermissionConfigurer.java b/backend-java/src/main/java/com/competition/modules/sys/config/JudgeRolePermissionConfigurer.java new file mode 100644 index 0000000..80c2964 --- /dev/null +++ b/backend-java/src/main/java/com/competition/modules/sys/config/JudgeRolePermissionConfigurer.java @@ -0,0 +1,136 @@ +package com.competition.modules.sys.config; + +import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper; +import com.competition.modules.sys.entity.SysPermission; +import com.competition.modules.sys.entity.SysRole; +import com.competition.modules.sys.entity.SysRolePermission; +import com.competition.modules.sys.mapper.SysPermissionMapper; +import com.competition.modules.sys.mapper.SysRoleMapper; +import com.competition.modules.sys.mapper.SysRolePermissionMapper; +import com.competition.modules.sys.mapper.SysTenantMapper; +import com.competition.modules.sys.entity.SysTenant; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.boot.ApplicationArguments; +import org.springframework.boot.ApplicationRunner; +import org.springframework.core.annotation.Order; +import org.springframework.stereotype.Component; + +import java.util.List; + +/** + * 评委角色权限:与 docs/design/menu-config.md「评委端权限码」一致,并从评委租户或 gdlib 模板复制缺失的权限定义。 + */ +@Slf4j +@Component +@RequiredArgsConstructor +@Order(100) +public class JudgeRolePermissionConfigurer implements ApplicationRunner { + + /** 与 menu-config 评委端权限码一致 */ + public static final List JUDGE_PERMISSION_CODES = List.of( + "review:score", "review:read", "review:create", "review:update", + "activity:read", "judge:read", "judge:assign", "work:read", + "notice:read", "workbench:read" + ); + + private static final String TEMPLATE_TENANT_CODE_GDLIB = "gdlib"; + private static final String JUDGE_TENANT_CODE = "judge"; + + private final SysPermissionMapper permissionMapper; + private final SysRolePermissionMapper rolePermissionMapper; + private final SysRoleMapper roleMapper; + private final SysTenantMapper tenantMapper; + + @Override + public void run(ApplicationArguments args) { + List judgeRoles = roleMapper.selectList( + new LambdaQueryWrapper() + .eq(SysRole::getCode, "judge") + .eq(SysRole::getValidState, 1)); + for (SysRole role : judgeRoles) { + try { + ensureJudgeRolePermissions(role.getTenantId(), role.getId()); + } catch (Exception e) { + log.warn("启动时补全评委角色权限失败 tenantId={} roleId={}: {}", + role.getTenantId(), role.getId(), e.getMessage()); + } + } + if (!judgeRoles.isEmpty()) { + log.info("评委角色权限补全完成,共 {} 个 judge 角色", judgeRoles.size()); + } + } + + /** + * 确保指定租户下评委角色拥有 menu-config 所列权限;缺失的权限从评委租户或 gdlib 复制到本租户后再绑定。 + */ + public void ensureJudgeRolePermissions(Long tenantId, Long roleId) { + if (tenantId == null || roleId == null) { + return; + } + for (String code : JUDGE_PERMISSION_CODES) { + SysPermission perm = permissionMapper.selectOne( + new LambdaQueryWrapper() + .eq(SysPermission::getTenantId, tenantId) + .eq(SysPermission::getCode, code) + .eq(SysPermission::getValidState, 1)); + if (perm == null) { + SysPermission template = findTemplatePermission(code); + if (template == null) { + log.debug("评委权限模板中无编码 {},跳过", code); + continue; + } + perm = new SysPermission(); + perm.setTenantId(tenantId); + perm.setName(template.getName()); + perm.setCode(template.getCode()); + perm.setResource(template.getResource()); + perm.setAction(template.getAction()); + perm.setDescription(template.getDescription()); + permissionMapper.insert(perm); + } + Long permId = perm.getId(); + long exists = rolePermissionMapper.selectCount( + new LambdaQueryWrapper() + .eq(SysRolePermission::getRoleId, roleId) + .eq(SysRolePermission::getPermissionId, permId)); + if (exists == 0) { + SysRolePermission rp = new SysRolePermission(); + rp.setRoleId(roleId); + rp.setPermissionId(permId); + rolePermissionMapper.insert(rp); + } + } + } + + private SysPermission findTemplatePermission(String code) { + Long judgeTid = tenantIdByCode(JUDGE_TENANT_CODE); + if (judgeTid != null) { + SysPermission p = permissionMapper.selectOne( + new LambdaQueryWrapper() + .eq(SysPermission::getTenantId, judgeTid) + .eq(SysPermission::getCode, code) + .eq(SysPermission::getValidState, 1)); + if (p != null) { + return p; + } + } + Long gdlibTid = tenantIdByCode(TEMPLATE_TENANT_CODE_GDLIB); + if (gdlibTid != null) { + return permissionMapper.selectOne( + new LambdaQueryWrapper() + .eq(SysPermission::getTenantId, gdlibTid) + .eq(SysPermission::getCode, code) + .eq(SysPermission::getValidState, 1)); + } + return null; + } + + private Long tenantIdByCode(String code) { + SysTenant t = tenantMapper.selectOne( + new LambdaQueryWrapper() + .eq(SysTenant::getCode, code) + .eq(SysTenant::getValidState, 1)); + return t != null ? t.getId() : null; + } +} diff --git a/backend-java/src/main/java/com/competition/modules/sys/service/impl/SysMenuServiceImpl.java b/backend-java/src/main/java/com/competition/modules/sys/service/impl/SysMenuServiceImpl.java index e9e1182..da3edfa 100644 --- a/backend-java/src/main/java/com/competition/modules/sys/service/impl/SysMenuServiceImpl.java +++ b/backend-java/src/main/java/com/competition/modules/sys/service/impl/SysMenuServiceImpl.java @@ -59,7 +59,15 @@ public class SysMenuServiceImpl extends ServiceImpl impl // 获取租户分配的菜单 ID List tenantMenus = tenantMenuMapper.selectList( new LambdaQueryWrapper().eq(SysTenantMenu::getTenantId, tenantId)); - Set tenantMenuIds = tenantMenus.stream().map(SysTenantMenu::getMenuId).collect(Collectors.toSet()); + Set tenantMenuIds = new HashSet<>(tenantMenus.stream().map(SysTenantMenu::getMenuId).collect(Collectors.toSet())); + + // 租户评委与平台评委共用「我的评审」菜单树:机构租户未在 t_sys_tenant_menu 中配置时,按角色合并评委端菜单 + if (!isSuperAdmin) { + List roles = userMapper.selectRolesByUserId(userId); + if (roles != null && roles.contains("judge")) { + tenantMenuIds.addAll(collectJudgePortalMenuIds(allMenus)); + } + } if (isSuperAdmin) { // 超管:按租户菜单过滤,但不做权限码过滤 @@ -168,6 +176,33 @@ public class SysMenuServiceImpl extends ServiceImpl impl .collect(Collectors.toList()); } + /** + * 评委端菜单:评审任务、预设评语及其父级(与 docs/design/menu-config 一致,不依赖固定菜单 ID) + */ + private Set collectJudgePortalMenuIds(List allMenus) { + Set ids = new HashSet<>(); + Set leafComponents = Set.of("activities/Review", "activities/PresetComments"); + for (SysMenu m : allMenus) { + if (m.getComponent() != null && leafComponents.contains(m.getComponent())) { + ids.add(m.getId()); + Long pid = m.getParentId(); + while (pid != null) { + if (ids.contains(pid)) { + break; + } + ids.add(pid); + final Long currentPid = pid; + SysMenu parent = allMenus.stream() + .filter(x -> x.getId().equals(currentPid)) + .findFirst() + .orElse(null); + pid = parent != null ? parent.getParentId() : null; + } + } + } + return ids; + } + /** 递归补全父菜单 */ private void addParentsIfMissing(SysMenu menu, List allMenus, List filtered, Set filteredIds) { if (menu.getParentId() == null || filteredIds.contains(menu.getParentId())) return; diff --git a/docs/design/menu-config.md b/docs/design/menu-config.md index e5c3a1b..d2907e0 100644 --- a/docs/design/menu-config.md +++ b/docs/design/menu-config.md @@ -1,6 +1,6 @@ # 各端菜单配置规范 -> 最后更新:2026-04-02 +> 最后更新:2026-04-08 > 维护人:开发团队 本文档记录各端的正确菜单配置,是菜单分配的**唯一权威来源**。修改菜单时必须对照此文档。 @@ -112,6 +112,13 @@ 9, 10, 11, 12, 14, 15, 16, 20, 23, 24, 25, 26, 27, 50, 51, 52, 53, 54 ``` +### 租户评委与平台评委(菜单一致) + +- **平台评委**:在评委租户(`code=judge`)登录,见第三节「评委端」,`t_sys_tenant_menu` 仅含 **34、35、36**。 +- **租户评委**:在机构租户(如 `tenantCode=test2`)登录,角色为 `judge`,与平台评委使用**同一套**「我的评审」菜单(仍为 **34、35、36** 对应的 `component`:`activities/Review`、`activities/PresetComments` 及父级)。 +- **实现**:`GET /api/menus/user-menus` 在 `SysMenuServiceImpl.getUserMenus` 中,若当前用户角色含 `judge`,会在 `t_sys_tenant_menu` 基础上**合并**评委端菜单(按组件路径识别,不依赖固定 ID);评委角色权限由 `JudgeRolePermissionConfigurer` 与 `JudgesManagementServiceImpl` 保证与上表「评委端权限码」一致,以便 `/api/auth/user-info` 的 `permissions` 与菜单 `permission` 字段匹配。 +- **可选**:若希望机构租户在「菜单管理」中显式看到评委菜单,也可在 `t_sys_tenant_menu` 中手工追加 **34、35、36**(与第三节一致),与合并逻辑效果相同。 + ### 租户端系统设置不包含的子菜单 | 菜单 | ID | 原因 | @@ -130,7 +137,7 @@ ## 三、评委端(tenant_id=7, code='judge')— 3条 -**定位**:评委评审工作台,只能看到自己被分配的活动和作品。 +**定位**:评委评审工作台,只能看到自己被分配的活动和作品。租户评委(机构租户下的 `judge` 角色)与平台评委共用本节菜单与权限码。 **详细接口与字段说明**:[评委端评审任务](./judge-portal/review-tasks.md)。 @@ -267,7 +274,7 @@ ``` 1. 查询所有 valid_state=1 的菜单 2. 查询当前用户 tenant_id 对应的 t_sys_tenant_menu -3. 按 tenant_menus 过滤菜单 +3. 按 tenant_menus 过滤菜单;若用户角色含 judge(且非超管),合并评委端菜单 ID(评审任务/预设评语及其父级,按 component 识别) 4. 如果是超管(isSuperAdmin):不做权限码过滤 5. 如果是普通用户:按用户权限码过滤(菜单.permission 字段匹配用户 permissions) 6. 补全父菜单(确保树结构完整) @@ -276,6 +283,8 @@ **⚠️ 重要**:超管也必须按 tenant_menus 过滤,不能返回全部菜单。之前的 bug 就是超管返回全部 52 个菜单导致错乱。 +**评委角色**:`judge` 角色须绑定「评委端权限码」中的权限(见 `JudgeRolePermissionConfigurer`),否则 `permissions` 为空会导致第 5 步过滤掉所有菜单。 + ### 登录时租户识别 前端通过 URL 提取 tenantCode(如 `/gdlib/login` → `tenantCode=gdlib`),登录请求: @@ -297,6 +306,7 @@ Java 后端 `AuthService.login` 支持两种方式确定租户: | 来源 | 用途 | |------|------| | `docs/design/menu-config.md` | **本文档**,菜单配置唯一权威 | +| `backend-java/.../JudgeRolePermissionConfigurer.java` | 评委角色权限码补全、`judge`/`gdlib` 模板复制 | | `backend/data/menus.json` | 菜单定义(所有菜单的字段) | | `backend/scripts/init-menus.ts` | 菜单初始化脚本(SUPER_TENANT_MENUS / NORMAL_TENANT_MENUS) | | `t_sys_menu` 表 | 数据库中的菜单数据 |