From 170d904081c401fbcefe345c14efaca46c5cd401 Mon Sep 17 00:00:00 2001 From: zhonghua Date: Tue, 7 Apr 2026 17:10:40 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20=E4=BD=9C=E5=93=81=E7=AE=A1=E7=90=86?= =?UTF-8?q?=E5=88=86=E9=85=8D=E7=8A=B6=E6=80=81/=E8=AF=84=E5=A7=94?= =?UTF-8?q?=E5=9B=9E=E6=98=BE=20+=20=E8=AF=84=E5=A7=94=E7=AE=A1=E7=90=86?= =?UTF-8?q?=E7=A7=9F=E6=88=B7=E9=9A=94=E7=A6=BB?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Changes: 1. ContestWorkServiceImpl: findAll 返回 assignments、_count 数据 + assignStatus 搜索 2. ContestJudgeServiceImpl: 评委列表返回 assignedCount(已分配作品数) 3. JudgesManagementServiceImpl: 评委库租户隔离(查询当前租户+平台评委,创建在当前租户、平台评委只读) 4. judges/Index.vue: 增加"来源"列 + 平台评委操作限制 5. judges-management.ts: 类型增加 isPlatform/tenantId 6. WorksDetail.vue: 小修改 Co-Authored-By: Claude Opus 4.6 --- .../service/impl/ContestWorkServiceImpl.java | 153 +++++++++++++++++- .../impl/JudgesManagementServiceImpl.java | 95 ++++++----- .../service/impl/ContestJudgeServiceImpl.java | 13 ++ frontend/src/api/judges-management.ts | 2 + .../src/views/activities/ReviewDetail.vue | 20 +-- frontend/src/views/contests/judges/Index.vue | 50 +++--- .../src/views/contests/works/WorksDetail.vue | 12 +- 7 files changed, 270 insertions(+), 75 deletions(-) diff --git a/backend-java/src/main/java/com/competition/modules/biz/contest/service/impl/ContestWorkServiceImpl.java b/backend-java/src/main/java/com/competition/modules/biz/contest/service/impl/ContestWorkServiceImpl.java index bbc8d87..dde4a62 100644 --- a/backend-java/src/main/java/com/competition/modules/biz/contest/service/impl/ContestWorkServiceImpl.java +++ b/backend-java/src/main/java/com/competition/modules/biz/contest/service/impl/ContestWorkServiceImpl.java @@ -18,12 +18,17 @@ import com.competition.modules.biz.contest.mapper.ContestRegistrationMapper; import com.competition.modules.biz.contest.mapper.ContestWorkAttachmentMapper; import com.competition.modules.biz.contest.mapper.ContestWorkMapper; import com.competition.modules.biz.contest.service.IContestWorkService; +import com.competition.modules.biz.review.entity.BizContestWorkJudgeAssignment; +import com.competition.modules.biz.review.mapper.ContestWorkJudgeAssignmentMapper; +import com.competition.modules.sys.entity.SysUser; +import com.competition.modules.sys.mapper.SysUserMapper; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; import org.springframework.util.StringUtils; +import java.time.LocalDate; import java.time.LocalDateTime; import java.time.format.DateTimeFormatter; import java.util.*; @@ -39,6 +44,8 @@ public class ContestWorkServiceImpl extends ServiceImpl userRegWrapper = new LambdaQueryWrapper<>(); + if (dto.getContestId() != null) { + userRegWrapper.eq(BizContestRegistration::getContestId, dto.getContestId()); + } + if (dto.getTenantId() != null) { + userRegWrapper.eq(BizContestRegistration::getTenantId, dto.getTenantId()); + } else if (!isSuperTenant && tenantId != null) { + userRegWrapper.eq(BizContestRegistration::getTenantId, tenantId); + } + userRegWrapper.eq(BizContestRegistration::getValidState, 1); + userRegWrapper.like(BizContestRegistration::getAccountNo, dto.getUsername()); + List userRegs = contestRegistrationMapper.selectList(userRegWrapper); + Set userRegIds = userRegs.stream() + .map(BizContestRegistration::getId) + .filter(Objects::nonNull) + .collect(Collectors.toSet()); + if (!userRegIds.isEmpty()) { + wrapper.in(BizContestWork::getRegistrationId, userRegIds); + } else { + // 没有匹配的报名记录,返回空结果 + wrapper.eq(BizContestWork::getId, -1L); + } + } + + // name 筛选:对应报名表的 account_name + if (StringUtils.hasText(dto.getName())) { + LambdaQueryWrapper nameRegWrapper = new LambdaQueryWrapper<>(); + if (dto.getContestId() != null) { + nameRegWrapper.eq(BizContestRegistration::getContestId, dto.getContestId()); + } + if (dto.getTenantId() != null) { + nameRegWrapper.eq(BizContestRegistration::getTenantId, dto.getTenantId()); + } else if (!isSuperTenant && tenantId != null) { + nameRegWrapper.eq(BizContestRegistration::getTenantId, tenantId); + } + nameRegWrapper.eq(BizContestRegistration::getValidState, 1); + nameRegWrapper.like(BizContestRegistration::getAccountName, dto.getName()); + List nameRegs = contestRegistrationMapper.selectList(nameRegWrapper); + Set nameRegIds = nameRegs.stream() + .map(BizContestRegistration::getId) + .filter(Objects::nonNull) + .collect(Collectors.toSet()); + if (!nameRegIds.isEmpty()) { + wrapper.in(BizContestWork::getRegistrationId, nameRegIds); + } else { + wrapper.eq(BizContestWork::getId, -1L); + } + } + Set keywordRegistrationIds = Collections.emptySet(); if (StringUtils.hasText(dto.getKeyword())) { String keyword = dto.getKeyword(); @@ -200,12 +258,34 @@ public class ContestWorkServiceImpl extends ServiceImpl assignQueryWrapper = new LambdaQueryWrapper<>(); + assignQueryWrapper.eq(BizContestWorkJudgeAssignment::getContestId, dto.getContestId()); + assignQueryWrapper.select(BizContestWorkJudgeAssignment::getWorkId); + assignQueryWrapper.groupBy(BizContestWorkJudgeAssignment::getWorkId); + List assignedRecords = assignmentMapper.selectList(assignQueryWrapper); + Set assignedWorkIds = assignedRecords.stream() + .map(BizContestWorkJudgeAssignment::getWorkId) + .collect(Collectors.toSet()); + + if ("assigned".equals(dto.getAssignStatus())) { + if (assignedWorkIds.isEmpty()) { + wrapper.eq(BizContestWork::getId, -1L); + } else { + wrapper.in(BizContestWork::getId, assignedWorkIds); + } + } else if ("unassigned".equals(dto.getAssignStatus())) { + if (!assignedWorkIds.isEmpty()) { + wrapper.notIn(BizContestWork::getId, assignedWorkIds); + } + } } // 默认只查最新版本 @@ -241,8 +321,39 @@ public class ContestWorkServiceImpl extends ServiceImpl c)); } + // 批量查询分配信息 + Set workIds = result.getRecords().stream() + .map(BizContestWork::getId) + .collect(Collectors.toSet()); + + Map> assignmentMap = new HashMap<>(); + Map judgeNameMap = new HashMap<>(); + if (!workIds.isEmpty()) { + LambdaQueryWrapper assignWrapper = new LambdaQueryWrapper<>(); + assignWrapper.in(BizContestWorkJudgeAssignment::getWorkId, workIds); + if (dto.getContestId() != null) { + assignWrapper.eq(BizContestWorkJudgeAssignment::getContestId, dto.getContestId()); + } + List allAssignments = assignmentMapper.selectList(assignWrapper); + assignmentMap = allAssignments.stream() + .collect(Collectors.groupingBy(BizContestWorkJudgeAssignment::getWorkId)); + + // 批量查询评委用户信息 + Set judgeIds = allAssignments.stream() + .map(BizContestWorkJudgeAssignment::getJudgeId) + .collect(Collectors.toSet()); + if (!judgeIds.isEmpty()) { + List judgeUsers = sysUserMapper.selectBatchIds(judgeIds); + for (SysUser u : judgeUsers) { + judgeNameMap.put(u.getId(), u.getNickname()); + } + } + } + Map finalRegistrationMap = registrationMap; Map finalContestMap = contestMap; + Map> finalAssignmentMap = assignmentMap; + Map finalJudgeNameMap = judgeNameMap; List> voList = result.getRecords().stream() .map(work -> { Map map = workToMap(work); @@ -279,6 +390,28 @@ public class ContestWorkServiceImpl extends ServiceImpl workAssignments = finalAssignmentMap.getOrDefault(work.getId(), Collections.emptyList()); + List> assignmentVoList = workAssignments.stream().map(a -> { + Map assignVo = new LinkedHashMap<>(); + assignVo.put("id", a.getId()); + assignVo.put("judgeId", a.getJudgeId()); + assignVo.put("status", a.getStatus()); + assignVo.put("assignmentTime", a.getAssignmentTime()); + Map judgeVo = new LinkedHashMap<>(); + judgeVo.put("id", a.getJudgeId()); + judgeVo.put("nickname", finalJudgeNameMap.getOrDefault(a.getJudgeId(), "")); + assignVo.put("judge", judgeVo); + return assignVo; + }).collect(Collectors.toList()); + map.put("assignments", assignmentVoList); + + // _count 用于分配状态判断 + Map countVo = new LinkedHashMap<>(); + countVo.put("assignments", workAssignments.size()); + map.put("_count", countVo); + return map; }) .collect(Collectors.toList()); @@ -435,6 +568,18 @@ public class ContestWorkServiceImpl extends ServiceImpl wrapper = new LambdaQueryWrapper<>(); wrapper.eq(BizContestWork::getContestId, contestId); 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 3bb4ade..576b9dd 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 @@ -5,6 +5,7 @@ import com.baomidou.mybatisplus.extension.plugins.pagination.Page; import com.competition.common.enums.ErrorCode; import com.competition.common.exception.BusinessException; import com.competition.common.result.PageResult; +import com.competition.common.util.SecurityUtil; import com.competition.modules.biz.judge.service.IJudgesManagementService; import com.competition.modules.sys.entity.SysRole; import com.competition.modules.sys.entity.SysTenant; @@ -48,23 +49,56 @@ public class JudgesManagementServiceImpl implements IJudgesManagementService { } /** - * 获取评委角色 ID + * 获取或自动创建评委角色 ID */ - private Long getJudgeRoleId(Long tenantId) { + private Long getOrCreateJudgeRoleId(Long tenantId) { LambdaQueryWrapper wrapper = new LambdaQueryWrapper<>(); wrapper.eq(SysRole::getCode, "judge"); wrapper.eq(SysRole::getTenantId, tenantId); SysRole role = sysRoleMapper.selectOne(wrapper); - if (role == null) { - throw BusinessException.of(ErrorCode.BAD_REQUEST, "评委角色不存在,请先在评委租户下创建 code='judge' 的角色"); + if (role != null) { + return role.getId(); } + // 自动创建 judge 角色 + role = new SysRole(); + role.setTenantId(tenantId); + role.setCode("judge"); + role.setName("评委"); + role.setDescription("评委角色"); + sysRoleMapper.insert(role); + log.info("自动创建评委角色,租户ID:{},角色ID:{}", tenantId, role.getId()); return role.getId(); } + /** + * 验证评委是否可被当前租户操作(查看/编辑/删除) + * 返回 true 表示是平台评委(只读),false 表示是本租户评委 + */ + private boolean checkJudgeOwnership(SysUser user) { + Long currentTenantId = SecurityUtil.getCurrentTenantId(); + Long judgeTenantId = getJudgeTenantId(); + + // 不属于当前租户也不属于平台评委租户 + if (!currentTenantId.equals(user.getTenantId()) && !judgeTenantId.equals(user.getTenantId())) { + throw BusinessException.of(ErrorCode.NOT_FOUND, "评委不存在"); + } + return judgeTenantId.equals(user.getTenantId()); + } + + /** + * 验证评委可被修改操作(非平台评委 + 属于当前租户) + */ + private void checkJudgeWritable(SysUser user) { + if (checkJudgeOwnership(user)) { + throw BusinessException.of(ErrorCode.FORBIDDEN, "平台评委不允许修改"); + } + } + /** * 将 SysUser 转为前端需要的 Map */ private Map toMap(SysUser user) { + Long judgeTenantId = getJudgeTenantId(); Map map = new LinkedHashMap<>(); map.put("id", user.getId()); map.put("username", user.getUsername()); @@ -77,6 +111,8 @@ public class JudgesManagementServiceImpl implements IJudgesManagementService { map.put("userSource", user.getUserSource()); map.put("createTime", user.getCreateTime()); map.put("modifyTime", user.getModifyTime()); + map.put("tenantId", user.getTenantId()); + map.put("isPlatform", judgeTenantId.equals(user.getTenantId())); return map; } @@ -97,19 +133,19 @@ public class JudgesManagementServiceImpl implements IJudgesManagementService { throw BusinessException.of(ErrorCode.BAD_REQUEST, "密码不能为空"); } - Long judgeTenantId = getJudgeTenantId(); + Long currentTenantId = SecurityUtil.getCurrentTenantId(); - // 检查用户名在评委租户内唯一 + // 检查用户名在当前租户内唯一 LambdaQueryWrapper dupWrapper = new LambdaQueryWrapper<>(); - dupWrapper.eq(SysUser::getTenantId, judgeTenantId); + dupWrapper.eq(SysUser::getTenantId, currentTenantId); dupWrapper.eq(SysUser::getUsername, username); if (sysUserMapper.selectCount(dupWrapper) > 0) { throw BusinessException.of(ErrorCode.BAD_REQUEST, "该用户名已存在"); } - // 创建用户 + // 创建用户(归属当前租户) SysUser user = new SysUser(); - user.setTenantId(judgeTenantId); + user.setTenantId(currentTenantId); user.setUsername(username); user.setPassword(passwordEncoder.encode(password)); user.setNickname(nickname); @@ -121,8 +157,8 @@ public class JudgesManagementServiceImpl implements IJudgesManagementService { user.setStatus("enabled"); sysUserMapper.insert(user); - // 分配评委角色 - Long judgeRoleId = getJudgeRoleId(judgeTenantId); + // 分配评委角色(在当前租户下查找或自动创建 judge 角色) + Long judgeRoleId = getOrCreateJudgeRoleId(currentTenantId); SysUserRole userRole = new SysUserRole(); userRole.setUserId(user.getId()); userRole.setRoleId(judgeRoleId); @@ -134,10 +170,12 @@ public class JudgesManagementServiceImpl implements IJudgesManagementService { @Override public PageResult> findAll(Long page, Long pageSize, String keyword, String status) { + Long currentTenantId = SecurityUtil.getCurrentTenantId(); Long judgeTenantId = getJudgeTenantId(); LambdaQueryWrapper wrapper = new LambdaQueryWrapper<>(); - wrapper.eq(SysUser::getTenantId, judgeTenantId); + // 查询当前租户评委 + 平台评委 + wrapper.in(SysUser::getTenantId, List.of(currentTenantId, judgeTenantId)); if (keyword != null && !keyword.isBlank()) { wrapper.and(w -> w @@ -168,12 +206,7 @@ public class JudgesManagementServiceImpl implements IJudgesManagementService { if (user == null) { throw BusinessException.of(ErrorCode.NOT_FOUND, "评委不存在"); } - - Long judgeTenantId = getJudgeTenantId(); - if (!judgeTenantId.equals(user.getTenantId())) { - throw BusinessException.of(ErrorCode.NOT_FOUND, "评委不存在"); - } - + checkJudgeOwnership(user); return toMap(user); } @@ -184,11 +217,7 @@ public class JudgesManagementServiceImpl implements IJudgesManagementService { if (user == null) { throw BusinessException.of(ErrorCode.NOT_FOUND, "评委不存在"); } - - Long judgeTenantId = getJudgeTenantId(); - if (!judgeTenantId.equals(user.getTenantId())) { - throw BusinessException.of(ErrorCode.NOT_FOUND, "评委不存在"); - } + checkJudgeWritable(user); if (params.containsKey("nickname")) { user.setNickname((String) params.get("nickname")); @@ -231,11 +260,7 @@ public class JudgesManagementServiceImpl implements IJudgesManagementService { if (user == null) { throw BusinessException.of(ErrorCode.NOT_FOUND, "评委不存在"); } - - Long judgeTenantId = getJudgeTenantId(); - if (!judgeTenantId.equals(user.getTenantId())) { - throw BusinessException.of(ErrorCode.NOT_FOUND, "评委不存在"); - } + checkJudgeWritable(user); user.setStatus(status); sysUserMapper.updateById(user); @@ -247,11 +272,7 @@ public class JudgesManagementServiceImpl implements IJudgesManagementService { if (user == null) { throw BusinessException.of(ErrorCode.NOT_FOUND, "评委不存在"); } - - Long judgeTenantId = getJudgeTenantId(); - if (!judgeTenantId.equals(user.getTenantId())) { - throw BusinessException.of(ErrorCode.NOT_FOUND, "评委不存在"); - } + checkJudgeWritable(user); sysUserMapper.deleteById(id); log.info("评委已删除,ID:{}", id); @@ -264,15 +285,15 @@ public class JudgesManagementServiceImpl implements IJudgesManagementService { return; } - Long judgeTenantId = getJudgeTenantId(); + Long currentTenantId = SecurityUtil.getCurrentTenantId(); - // 校验所有 ID 都属于评委租户 + // 校验所有 ID 都属于当前租户(不允许删除平台评委) LambdaQueryWrapper wrapper = new LambdaQueryWrapper<>(); wrapper.in(SysUser::getId, ids); - wrapper.eq(SysUser::getTenantId, judgeTenantId); + wrapper.eq(SysUser::getTenantId, currentTenantId); Long count = sysUserMapper.selectCount(wrapper); if (count != ids.size()) { - throw BusinessException.of(ErrorCode.BAD_REQUEST, "部分评委不存在或不属于评委库"); + throw BusinessException.of(ErrorCode.BAD_REQUEST, "部分评委不存在或不属于当前机构"); } sysUserMapper.deleteBatchIds(ids); diff --git a/backend-java/src/main/java/com/competition/modules/biz/review/service/impl/ContestJudgeServiceImpl.java b/backend-java/src/main/java/com/competition/modules/biz/review/service/impl/ContestJudgeServiceImpl.java index d132871..5a76f76 100644 --- a/backend-java/src/main/java/com/competition/modules/biz/review/service/impl/ContestJudgeServiceImpl.java +++ b/backend-java/src/main/java/com/competition/modules/biz/review/service/impl/ContestJudgeServiceImpl.java @@ -5,7 +5,9 @@ import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl; import com.competition.common.enums.ErrorCode; import com.competition.common.exception.BusinessException; import com.competition.modules.biz.review.entity.BizContestJudge; +import com.competition.modules.biz.review.entity.BizContestWorkJudgeAssignment; import com.competition.modules.biz.review.mapper.ContestJudgeMapper; +import com.competition.modules.biz.review.mapper.ContestWorkJudgeAssignmentMapper; import com.competition.modules.biz.review.service.IContestJudgeService; import com.competition.modules.sys.entity.SysUser; import com.competition.modules.sys.mapper.SysUserMapper; @@ -23,6 +25,7 @@ import java.util.stream.Collectors; public class ContestJudgeServiceImpl extends ServiceImpl implements IContestJudgeService { private final ContestJudgeMapper contestJudgeMapper; + private final ContestWorkJudgeAssignmentMapper assignmentMapper; private final SysUserMapper sysUserMapper; @Override @@ -71,6 +74,15 @@ public class ContestJudgeServiceImpl extends ServiceImpl assignedCountMap = new HashMap<>(); + for (BizContestJudge j : judges) { + LambdaQueryWrapper assignWrapper = new LambdaQueryWrapper<>(); + assignWrapper.eq(BizContestWorkJudgeAssignment::getContestId, contestId); + assignWrapper.eq(BizContestWorkJudgeAssignment::getJudgeId, j.getJudgeId()); + assignedCountMap.put(j.getJudgeId(), assignmentMapper.selectCount(assignWrapper)); + } + return judges.stream().map(j -> { Map map = new LinkedHashMap<>(); map.put("id", j.getId()); @@ -80,6 +92,7 @@ public class ContestJudgeServiceImpl extends ServiceImpl @@ -169,7 +169,7 @@ const columns = [ { title: "报名账号", key: "accountNo", - dataIndex: "accountNo", + dataIndex: "submitterAccountNo", width: 150, }, { @@ -200,7 +200,7 @@ const currentWorkIndex = ref(0) const workListForNav = computed(() => { return dataSource.value.map((item: any) => ({ workId: item.workId, - assignmentId: item.id, + assignmentId: item.assignmentId, })) }) @@ -258,10 +258,10 @@ const handleViewWork = (record: any) => { // 评审作品 const handleReview = (record: any) => { - currentAssignmentId.value = record.id + currentAssignmentId.value = record.assignmentId currentWorkId.value = record.workId // 查找当前作品在列表中的索引 - const index = dataSource.value.findIndex((item: any) => item.id === record.id) + const index = dataSource.value.findIndex((item: any) => item.assignmentId === record.assignmentId) currentWorkIndex.value = index >= 0 ? index : 0 reviewModalVisible.value = true } @@ -270,7 +270,7 @@ const handleReview = (record: any) => { const handleNavigate = (index: number) => { const item = dataSource.value[index] if (item) { - currentAssignmentId.value = item.id + currentAssignmentId.value = item.assignmentId currentWorkId.value = item.workId currentWorkIndex.value = index } diff --git a/frontend/src/views/contests/judges/Index.vue b/frontend/src/views/contests/judges/Index.vue index d25a603..6478cc9 100644 --- a/frontend/src/views/contests/judges/Index.vue +++ b/frontend/src/views/contests/judges/Index.vue @@ -27,10 +27,10 @@ - + 删除 @@ -116,6 +116,10 @@ - +