feat: 评委角色权限补全与租户评委菜单合并,更新 menu-config 说明

Made-with: Cursor
This commit is contained in:
zhonghua 2026-04-08 11:07:11 +08:00
parent 180c22fe49
commit 197064820b
4 changed files with 189 additions and 4 deletions

View File

@ -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();
}

View File

@ -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<String> 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<SysRole> judgeRoles = roleMapper.selectList(
new LambdaQueryWrapper<SysRole>()
.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<SysPermission>()
.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<SysRolePermission>()
.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<SysPermission>()
.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<SysPermission>()
.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<SysTenant>()
.eq(SysTenant::getCode, code)
.eq(SysTenant::getValidState, 1));
return t != null ? t.getId() : null;
}
}

View File

@ -59,7 +59,15 @@ public class SysMenuServiceImpl extends ServiceImpl<SysMenuMapper, SysMenu> impl
// 获取租户分配的菜单 ID
List<SysTenantMenu> tenantMenus = tenantMenuMapper.selectList(
new LambdaQueryWrapper<SysTenantMenu>().eq(SysTenantMenu::getTenantId, tenantId));
Set<Long> tenantMenuIds = tenantMenus.stream().map(SysTenantMenu::getMenuId).collect(Collectors.toSet());
Set<Long> tenantMenuIds = new HashSet<>(tenantMenus.stream().map(SysTenantMenu::getMenuId).collect(Collectors.toSet()));
// 租户评委与平台评委共用我的评审菜单树机构租户未在 t_sys_tenant_menu 中配置时按角色合并评委端菜单
if (!isSuperAdmin) {
List<String> 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<SysMenuMapper, SysMenu> impl
.collect(Collectors.toList());
}
/**
* 评委端菜单评审任务预设评语及其父级 docs/design/menu-config 一致不依赖固定菜单 ID
*/
private Set<Long> collectJudgePortalMenuIds(List<SysMenu> allMenus) {
Set<Long> ids = new HashSet<>();
Set<String> 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<SysMenu> allMenus, List<SysMenu> filtered, Set<Long> filteredIds) {
if (menu.getParentId() == null || filteredIds.contains(menu.getParentId())) return;

View File

@ -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` 表 | 数据库中的菜单数据 |