feat: 评委角色权限补全与租户评委菜单合并,更新 menu-config 说明
Made-with: Cursor
This commit is contained in:
parent
180c22fe49
commit
197064820b
@ -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();
|
||||
}
|
||||
|
||||
|
||||
@ -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;
|
||||
}
|
||||
}
|
||||
@ -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;
|
||||
|
||||
@ -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` 表 | 数据库中的菜单数据 |
|
||||
|
||||
Loading…
Reference in New Issue
Block a user