diff --git a/backend-java/src/main/java/com/competition/modules/biz/contest/service/impl/ContestServiceImpl.java b/backend-java/src/main/java/com/competition/modules/biz/contest/service/impl/ContestServiceImpl.java index 1032a1e..a558e8e 100644 --- a/backend-java/src/main/java/com/competition/modules/biz/contest/service/impl/ContestServiceImpl.java +++ b/backend-java/src/main/java/com/competition/modules/biz/contest/service/impl/ContestServiceImpl.java @@ -18,6 +18,8 @@ import com.competition.modules.biz.contest.mapper.ContestMapper; import com.competition.modules.biz.contest.mapper.ContestRegistrationMapper; import com.competition.modules.biz.contest.mapper.ContestWorkMapper; import com.competition.modules.biz.contest.service.IContestService; +import com.competition.modules.biz.review.entity.BizContestWorkJudgeAssignment; +import com.competition.modules.biz.review.mapper.ContestWorkJudgeAssignmentMapper; import com.competition.modules.sys.entity.SysTenant; import com.competition.modules.sys.mapper.SysTenantMapper; import lombok.RequiredArgsConstructor; @@ -39,6 +41,7 @@ public class ContestServiceImpl extends ServiceImpl i private final ContestAttachmentMapper contestAttachmentMapper; private final ContestRegistrationMapper contestRegistrationMapper; private final ContestWorkMapper contestWorkMapper; + private final ContestWorkJudgeAssignmentMapper contestWorkJudgeAssignmentMapper; private final SysTenantMapper sysTenantMapper; // 支持两种日期格式:ISO 格式 (T 分隔) 和空格分隔格式 @@ -160,6 +163,7 @@ public class ContestServiceImpl extends ServiceImpl i Map registrationCountMap = new HashMap<>(); Map workCountMap = new HashMap<>(); + Map reviewedWorkCountMap = new HashMap<>(); if (!contestIds.isEmpty()) { // 报名数(所有状态) contestRegistrationMapper.selectList( @@ -169,23 +173,42 @@ public class ContestServiceImpl extends ServiceImpl i .collect(Collectors.groupingBy(BizContestRegistration::getContestId, Collectors.counting())) .forEach(registrationCountMap::put); - // 作品数(最新版本) - contestWorkMapper.selectList( + // 作品:最新有效版本;评审完成数 = 已分配且该作品全部分配记录均为 completed(与评委端、ProgressDetail 一致) + List contestWorks = contestWorkMapper.selectList( new LambdaQueryWrapper() .in(BizContestWork::getContestId, contestIds) - .eq(BizContestWork::getIsLatest, true)) - .stream() - .collect(Collectors.groupingBy(BizContestWork::getContestId, Collectors.counting())) - .forEach(workCountMap::put); + .eq(BizContestWork::getIsLatest, true) + .eq(BizContestWork::getValidState, 1)); + Set workIdSet = contestWorks.stream().map(BizContestWork::getId).collect(Collectors.toSet()); + Map> assignByWorkId = new HashMap<>(); + if (!workIdSet.isEmpty()) { + List allAssign = contestWorkJudgeAssignmentMapper.selectList( + new LambdaQueryWrapper() + .in(BizContestWorkJudgeAssignment::getWorkId, workIdSet)); + assignByWorkId = allAssign.stream() + .collect(Collectors.groupingBy(BizContestWorkJudgeAssignment::getWorkId)); + } + for (BizContestWork w : contestWorks) { + Long cid = w.getContestId(); + workCountMap.merge(cid, 1L, Long::sum); + List assigns = assignByWorkId.getOrDefault(w.getId(), Collections.emptyList()); + if (!assigns.isEmpty() && assigns.stream().allMatch(a -> "completed".equals(a.getStatus()))) { + reviewedWorkCountMap.merge(cid, 1L, Long::sum); + } + } } List> voList = result.getRecords().stream() .map(entity -> { Map map = entityToMap(entity); Map countMap = new LinkedHashMap<>(); + long works = workCountMap.getOrDefault(entity.getId(), 0L); + long reviewedWorks = reviewedWorkCountMap.getOrDefault(entity.getId(), 0L); countMap.put("registrations", registrationCountMap.getOrDefault(entity.getId(), 0L)); - countMap.put("works", workCountMap.getOrDefault(entity.getId(), 0L)); + countMap.put("works", works); map.put("_count", countMap); + map.put("totalWorksCount", works); + map.put("reviewedCount", reviewedWorks); return map; }) .collect(Collectors.toList()); 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 6830473..02244ec 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,8 +18,15 @@ 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.BizContestJudge; +import com.competition.modules.biz.review.entity.BizContestReviewRule; import com.competition.modules.biz.review.entity.BizContestWorkJudgeAssignment; +import com.competition.modules.biz.review.entity.BizContestWorkScore; +import com.competition.modules.biz.review.mapper.ContestJudgeMapper; +import com.competition.modules.biz.review.mapper.ContestReviewRuleMapper; import com.competition.modules.biz.review.mapper.ContestWorkJudgeAssignmentMapper; +import com.competition.modules.biz.review.mapper.ContestWorkScoreMapper; +import com.competition.modules.biz.review.util.ContestFinalScoreCalculator; import com.competition.modules.sys.entity.SysUser; import com.competition.modules.sys.mapper.SysUserMapper; import lombok.RequiredArgsConstructor; @@ -28,6 +35,7 @@ import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; import org.springframework.util.StringUtils; +import java.math.BigDecimal; import java.time.LocalDate; import java.time.LocalDateTime; import java.time.format.DateTimeFormatter; @@ -46,6 +54,9 @@ public class ContestWorkServiceImpl extends ServiceImpl finalContestMap = contestMap; Map> finalAssignmentMap = assignmentMap; Map finalJudgeNameMap = judgeNameMap; + + // 批量评分:列表「评委评分」与详情抽屉一致;作品表 final_score 未落库时按评审规则从评分表回算 + Map> scoresByWorkId = new HashMap<>(); + if (!workIds.isEmpty()) { + LambdaQueryWrapper scoreWrapper = new LambdaQueryWrapper<>(); + scoreWrapper.in(BizContestWorkScore::getWorkId, workIds); + scoreWrapper.eq(BizContestWorkScore::getValidState, 1); + List allPageScores = contestWorkScoreMapper.selectList(scoreWrapper); + scoresByWorkId = allPageScores.stream() + .collect(Collectors.groupingBy(BizContestWorkScore::getWorkId)); + } + + Map contestCalculationRuleCache = new HashMap<>(); + Map> contestWeightMapCache = new HashMap<>(); + for (Long cid : contestIds) { + BizContest c = contestMap.get(cid); + if (c == null) { + continue; + } + String calculationRule = "average"; + if (c.getReviewRuleId() != null) { + BizContestReviewRule rule = contestReviewRuleMapper.selectById(c.getReviewRuleId()); + if (rule != null && StringUtils.hasText(rule.getCalculationRule())) { + calculationRule = rule.getCalculationRule(); + } + } + contestCalculationRuleCache.put(cid, calculationRule); + + LambdaQueryWrapper judgeWrapper = new LambdaQueryWrapper<>(); + judgeWrapper.eq(BizContestJudge::getContestId, cid); + judgeWrapper.eq(BizContestJudge::getValidState, 1); + List judges = contestJudgeMapper.selectList(judgeWrapper); + Map weightMap = new HashMap<>(); + for (BizContestJudge j : judges) { + weightMap.put(j.getJudgeId(), j.getWeight() != null ? j.getWeight() : BigDecimal.ONE); + } + contestWeightMapCache.put(cid, weightMap); + } + + Map> finalScoresByWorkId = scoresByWorkId; + Map finalContestCalculationRuleCache = contestCalculationRuleCache; + Map> finalContestWeightMapCache = contestWeightMapCache; + List> voList = result.getRecords().stream() .map(work -> { Map map = workToMap(work); @@ -413,6 +467,27 @@ public class ContestWorkServiceImpl extends ServiceImpl "completed".equals(a.getStatus())) + .count(); + map.put("totalJudgesCount", totalJudgesCount); + map.put("reviewedCount", reviewedCount); + + BigDecimal displayFinal = work.getFinalScore(); + if (displayFinal == null) { + List scores = finalScoresByWorkId.getOrDefault(work.getId(), Collections.emptyList()); + if (!scores.isEmpty()) { + String rule = finalContestCalculationRuleCache.getOrDefault(work.getContestId(), "average"); + Map wm = finalContestWeightMapCache.getOrDefault(work.getContestId(), Collections.emptyMap()); + displayFinal = ContestFinalScoreCalculator.compute(scores, rule, wm); + } + } + if (displayFinal != null) { + map.put("finalScore", displayFinal); + map.put("averageScore", displayFinal); + } + return map; }) .collect(Collectors.toList()); diff --git a/backend-java/src/main/java/com/competition/modules/biz/review/controller/ContestJudgeController.java b/backend-java/src/main/java/com/competition/modules/biz/review/controller/ContestJudgeController.java index 3c2a72b..bdd3e57 100644 --- a/backend-java/src/main/java/com/competition/modules/biz/review/controller/ContestJudgeController.java +++ b/backend-java/src/main/java/com/competition/modules/biz/review/controller/ContestJudgeController.java @@ -1,6 +1,7 @@ package com.competition.modules.biz.review.controller; import com.competition.common.result.Result; +import com.competition.modules.biz.review.dto.ContestJudgesForContestVo; import com.competition.modules.biz.review.entity.BizContestJudge; import com.competition.modules.biz.review.service.IContestJudgeService; import com.competition.security.annotation.RequirePermission; @@ -10,7 +11,6 @@ import lombok.RequiredArgsConstructor; import org.springframework.web.bind.annotation.*; import java.math.BigDecimal; -import java.util.List; import java.util.Map; @Tag(name = "赛事评委") @@ -35,8 +35,9 @@ public class ContestJudgeController { @GetMapping("/contest/{contestId}") @RequirePermission("contest:read") - @Operation(summary = "查询赛事评委列表") - public Result>> findByContest(@PathVariable Long contestId) { + @Operation(summary = "查询赛事评委列表", + description = "返回 assigned(显式关联)与 implicitPool(平台隐式池)。添加评委抽屉仅用 assigned 回显;作品分配可选池为 assigned ∪ implicitPool(前端合并)。") + public Result findByContest(@PathVariable Long contestId) { return Result.success(contestJudgeService.findByContest(contestId)); } diff --git a/backend-java/src/main/java/com/competition/modules/biz/review/dto/ContestJudgesForContestVo.java b/backend-java/src/main/java/com/competition/modules/biz/review/dto/ContestJudgesForContestVo.java new file mode 100644 index 0000000..5970b7d --- /dev/null +++ b/backend-java/src/main/java/com/competition/modules/biz/review/dto/ContestJudgesForContestVo.java @@ -0,0 +1,21 @@ +package com.competition.modules.biz.review.dto; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; + +import java.util.List; +import java.util.Map; + +/** + * 某赛事下的评委数据:显式关联与平台隐式池分离,避免扁平列表语义混淆。 + */ +@Data +@Schema(description = "赛事评委查询结果") +public class ContestJudgesForContestVo { + + @Schema(description = "机构为该赛事显式添加的评委(t_biz_contest_judge),每条必有 id、judgeId;添加评委抽屉回显与提交差集仅基于此列表") + private List> assigned; + + @Schema(description = "平台评委租户下对该赛事默认可用、未写入关联表的用户(id 为 null,isPlatform 为 true);可与 assigned 合并作为作品分配可选池") + private List> implicitPool; +} diff --git a/backend-java/src/main/java/com/competition/modules/biz/review/service/IContestJudgeService.java b/backend-java/src/main/java/com/competition/modules/biz/review/service/IContestJudgeService.java index c0cbf37..95c0541 100644 --- a/backend-java/src/main/java/com/competition/modules/biz/review/service/IContestJudgeService.java +++ b/backend-java/src/main/java/com/competition/modules/biz/review/service/IContestJudgeService.java @@ -1,17 +1,21 @@ package com.competition.modules.biz.review.service; import com.baomidou.mybatisplus.extension.service.IService; +import com.competition.modules.biz.review.dto.ContestJudgesForContestVo; import com.competition.modules.biz.review.entity.BizContestJudge; import java.math.BigDecimal; -import java.util.List; import java.util.Map; public interface IContestJudgeService extends IService { BizContestJudge createJudge(Long contestId, Long judgeId, String specialty, BigDecimal weight, String description); - List> findByContest(Long contestId); + /** + * 查询某赛事评委:{@link ContestJudgesForContestVo#getAssigned()} 为显式关联; + * {@link ContestJudgesForContestVo#getImplicitPool()} 为平台默认可用未落库项。 + */ + ContestJudgesForContestVo findByContest(Long contestId); Map findDetail(Long id); 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 4e2a63b..618a4b0 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 @@ -4,6 +4,7 @@ import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper; 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.dto.ContestJudgesForContestVo; import com.competition.modules.biz.review.entity.BizContestJudge; import com.competition.modules.biz.review.entity.BizContestWorkJudgeAssignment; import com.competition.modules.biz.review.mapper.ContestJudgeMapper; @@ -57,7 +58,7 @@ public class ContestJudgeServiceImpl extends ServiceImpl> findByContest(Long contestId) { + public ContestJudgesForContestVo findByContest(Long contestId) { log.info("查询赛事评委列表,赛事ID:{}", contestId); // 获取平台评委租户 ID @@ -111,7 +112,8 @@ public class ContestJudgeServiceImpl extends ServiceImpl> result = new ArrayList<>(); + List> assigned = new ArrayList<>(); + List> implicitPool = new ArrayList<>(); // 构建已显式分配的评委数据 for (BizContestJudge j : judges) { @@ -136,10 +138,10 @@ public class ContestJudgeServiceImpl extends ServiceImpl> getJudgeContests(Long judgeId) { log.info("查询评委关联赛事,评委ID:{}", judgeId); - LambdaQueryWrapper wrapper = new LambdaQueryWrapper<>(); - wrapper.eq(BizContestJudge::getJudgeId, judgeId); - wrapper.eq(BizContestJudge::getValidState, 1); + Set contestIds = new LinkedHashSet<>(); - List judgeRecords = judgeMapper.selectList(wrapper); - Set contestIds = judgeRecords.stream().map(BizContestJudge::getContestId).collect(Collectors.toSet()); + LambdaQueryWrapper judgeQw = new LambdaQueryWrapper<>(); + judgeQw.eq(BizContestJudge::getJudgeId, judgeId); + judgeQw.eq(BizContestJudge::getValidState, 1); + for (BizContestJudge r : judgeMapper.selectList(judgeQw)) { + contestIds.add(r.getContestId()); + } + + LambdaQueryWrapper assignQw = new LambdaQueryWrapper<>(); + assignQw.eq(BizContestWorkJudgeAssignment::getJudgeId, judgeId); + assignQw.select(BizContestWorkJudgeAssignment::getContestId); + for (BizContestWorkJudgeAssignment a : assignmentMapper.selectList(assignQw)) { + contestIds.add(a.getContestId()); + } if (contestIds.isEmpty()) { return Collections.emptyList(); } List contests = contestMapper.selectBatchIds(contestIds); - return contests.stream().map(c -> { - Map map = new LinkedHashMap<>(); - map.put("contestId", c.getId()); - map.put("contestName", c.getContestName()); - map.put("contestState", c.getContestState()); - map.put("status", c.getStatus()); - map.put("reviewStartTime", c.getReviewStartTime()); - map.put("reviewEndTime", c.getReviewEndTime()); - return map; - }).collect(Collectors.toList()); + return contests.stream() + .sorted(Comparator.comparing(BizContest::getId).reversed()) + .map(c -> { + Map map = new LinkedHashMap<>(); + map.put("contestId", c.getId()); + map.put("contestName", c.getContestName()); + map.put("contestState", c.getContestState()); + map.put("status", c.getStatus()); + map.put("reviewStartTime", c.getReviewStartTime()); + map.put("reviewEndTime", c.getReviewEndTime()); + + LambdaQueryWrapper totalW = new LambdaQueryWrapper<>(); + totalW.eq(BizContestWorkJudgeAssignment::getContestId, c.getId()); + totalW.eq(BizContestWorkJudgeAssignment::getJudgeId, judgeId); + long totalAssigned = assignmentMapper.selectCount(totalW); + + LambdaQueryWrapper doneW = new LambdaQueryWrapper<>(); + doneW.eq(BizContestWorkJudgeAssignment::getContestId, c.getId()); + doneW.eq(BizContestWorkJudgeAssignment::getJudgeId, judgeId); + doneW.eq(BizContestWorkJudgeAssignment::getStatus, "completed"); + long reviewed = assignmentMapper.selectCount(doneW); + + map.put("totalAssigned", totalAssigned); + map.put("reviewed", reviewed); + map.put("pending", totalAssigned - reviewed); + return map; + }) + .collect(Collectors.toList()); } @Override @@ -348,7 +375,13 @@ public class ContestReviewServiceImpl implements IContestReviewService { wrapper.eq(BizContestWorkJudgeAssignment::getContestId, contestId); if (StringUtils.hasText(reviewStatus)) { - wrapper.eq(BizContestWorkJudgeAssignment::getStatus, reviewStatus); + if ("reviewed".equalsIgnoreCase(reviewStatus)) { + wrapper.eq(BizContestWorkJudgeAssignment::getStatus, "completed"); + } else if ("pending".equalsIgnoreCase(reviewStatus)) { + wrapper.ne(BizContestWorkJudgeAssignment::getStatus, "completed"); + } else { + wrapper.eq(BizContestWorkJudgeAssignment::getStatus, reviewStatus); + } } wrapper.orderByAsc(BizContestWorkJudgeAssignment::getAssignmentTime); @@ -675,13 +708,19 @@ public class ContestReviewServiceImpl implements IContestReviewService { public Map getJudgeContestDetail(Long judgeId, Long contestId) { log.info("获取评委赛事详情,评委ID:{},赛事ID:{}", judgeId, contestId); - // 验证评委属于该赛事 + // 显式评委 或 已有作品分配记录 LambdaQueryWrapper judgeWrapper = new LambdaQueryWrapper<>(); judgeWrapper.eq(BizContestJudge::getContestId, contestId); judgeWrapper.eq(BizContestJudge::getJudgeId, judgeId); judgeWrapper.eq(BizContestJudge::getValidState, 1); - if (judgeMapper.selectCount(judgeWrapper) == 0) { - throw BusinessException.of(ErrorCode.FORBIDDEN, "您不是该赛事的评委"); + boolean explicitJudge = judgeMapper.selectCount(judgeWrapper) > 0; + if (!explicitJudge) { + LambdaQueryWrapper assignCheck = new LambdaQueryWrapper<>(); + assignCheck.eq(BizContestWorkJudgeAssignment::getContestId, contestId); + assignCheck.eq(BizContestWorkJudgeAssignment::getJudgeId, judgeId); + if (assignmentMapper.selectCount(assignCheck) == 0) { + throw BusinessException.of(ErrorCode.FORBIDDEN, "您不是该赛事的评委"); + } } BizContest contest = contestMapper.selectById(contestId); diff --git a/backend-java/src/main/java/com/competition/modules/biz/review/util/ContestFinalScoreCalculator.java b/backend-java/src/main/java/com/competition/modules/biz/review/util/ContestFinalScoreCalculator.java new file mode 100644 index 0000000..581dbda --- /dev/null +++ b/backend-java/src/main/java/com/competition/modules/biz/review/util/ContestFinalScoreCalculator.java @@ -0,0 +1,72 @@ +package com.competition.modules.biz.review.util; + +import com.competition.modules.biz.review.entity.BizContestWorkScore; + +import java.math.BigDecimal; +import java.math.RoundingMode; +import java.util.Collections; +import java.util.List; +import java.util.Map; +import java.util.stream.Collectors; + +/** + * 与 {@code ContestReviewServiceImpl#calculateFinalScore}、成果发布计算逻辑一致的终分计算(列表场景对不足条数规则做温和回退)。 + */ +public final class ContestFinalScoreCalculator { + + private ContestFinalScoreCalculator() { + } + + public static BigDecimal compute( + List scores, + String calculationRule, + Map weightMap) { + if (scores == null || scores.isEmpty()) { + return null; + } + String rule = calculationRule != null ? calculationRule : "average"; + Map wm = weightMap != null ? weightMap : Collections.emptyMap(); + + List scoreValues = scores.stream() + .map(BizContestWorkScore::getTotalScore) + .sorted() + .collect(Collectors.toList()); + + switch (rule) { + case "max": + return Collections.max(scoreValues); + case "min": + return Collections.min(scoreValues); + case "weighted": + BigDecimal weightedSum = BigDecimal.ZERO; + BigDecimal totalWeight = BigDecimal.ZERO; + for (BizContestWorkScore s : scores) { + BigDecimal w = wm.getOrDefault(s.getJudgeId(), BigDecimal.ONE); + weightedSum = weightedSum.add(s.getTotalScore().multiply(w)); + totalWeight = totalWeight.add(w); + } + return totalWeight.compareTo(BigDecimal.ZERO) > 0 + ? weightedSum.divide(totalWeight, 2, RoundingMode.HALF_UP) + : BigDecimal.ZERO; + case "remove_max_min": + if (scoreValues.size() < 3) { + BigDecimal sum = scoreValues.stream().reduce(BigDecimal.ZERO, BigDecimal::add); + return sum.divide(BigDecimal.valueOf(scoreValues.size()), 2, RoundingMode.HALF_UP); + } + List trimmed = scoreValues.subList(1, scoreValues.size() - 1); + BigDecimal trimmedSum = trimmed.stream().reduce(BigDecimal.ZERO, BigDecimal::add); + return trimmedSum.divide(BigDecimal.valueOf(trimmed.size()), 2, RoundingMode.HALF_UP); + case "remove_min": + if (scoreValues.size() < 2) { + return scoreValues.get(0); + } + List withoutMin = scoreValues.subList(1, scoreValues.size()); + BigDecimal withoutMinSum = withoutMin.stream().reduce(BigDecimal.ZERO, BigDecimal::add); + return withoutMinSum.divide(BigDecimal.valueOf(withoutMin.size()), 2, RoundingMode.HALF_UP); + case "average": + default: + BigDecimal sum = scoreValues.stream().reduce(BigDecimal.ZERO, BigDecimal::add); + return sum.divide(BigDecimal.valueOf(scoreValues.size()), 2, RoundingMode.HALF_UP); + } + } +} diff --git a/docs/design/README.md b/docs/design/README.md index 2eefc1d..c8dbac3 100644 --- a/docs/design/README.md +++ b/docs/design/README.md @@ -31,4 +31,6 @@ ## 评委端 -(暂无) +| 文档 | 模块 | 状态 | 日期 | +|------|------|------|------| +| [评审任务](./judge-portal/review-tasks.md) | 评审任务 / 作品列表 | 已实现 | 2026-04-08 | diff --git a/docs/design/judge-portal/review-tasks.md b/docs/design/judge-portal/review-tasks.md new file mode 100644 index 0000000..394b1fc --- /dev/null +++ b/docs/design/judge-portal/review-tasks.md @@ -0,0 +1,60 @@ +# 评委端:评审任务 + +> 所属端:评委端(`tenant_id` 对应评委租户,如 `code=judge`) +> 菜单与定位见 [菜单配置说明](../menu-config.md) 中「评委端」章节:仅能查看**被分配**的活动与作品。 + +## 活动列表 + +**接口**:`GET /contests/reviews/judge/contests` + +**活动来源(并集)**: + +1. `t_biz_contest_judge` 中 `judge_id` 且 `valid_state=1` 的赛事(机构显式添加的评委); +2. `t_biz_contest_work_judge_assignment` 中该评委出现过的 `contest_id`(含仅通过作品分配参与的隐式场景)。 + +**响应字段(与前端 `activities/Review.vue` 对齐)**: + +| 字段 | 说明 | +|------|------| +| `contestId` | 活动 ID | +| `contestName` | 活动名称 | +| `contestState` / `status` | 活动状态 | +| `reviewStartTime` / `reviewEndTime` | 评审时间窗 | +| `totalAssigned` | 该评委在该活动下的分配记录总数 | +| `reviewed` | 其中 `status=completed` 的数量(已提交评分) | +| `pending` | `totalAssigned - reviewed`(待评审) | + +## 活动下作品列表 + +**接口**:`GET /contests/reviews/judge/contests/{contestId}/works` + +**分配状态 `reviewStatus` 查询参数(与库表兼容)**: + +- 库中 `t_biz_contest_work_judge_assignment.status` 实际使用:`assigned`(已分配未评完)、`completed`(已评审)。 +- 前端下拉「未评审」传 `pending` → 后端按 **`status != completed`** 筛选。 +- 前端「已评审」传 `reviewed` → 后端按 **`status = completed`** 筛选。 + +**作品编号**:`workNo` 为空时,前端可用作品 `workId` 展示兜底(如 `#123`)。 + +## 活动详情(含评审规则) + +**接口**:`GET /contests/reviews/judge/contests/{contestId}/detail` + +**权限**:满足以下**任一**即可: + +- 存在有效的 `t_biz_contest_judge` 关联;或 +- 存在该 `contestId` + `judgeId` 的作品分配记录。 + +避免「列表能进、详情 403」与隐式评委场景不一致。 + +--- + +## 与租户端「评审进度」的口径对齐 + +| 维度 | 租户机构端 `contests/reviews/progress`(活动列表行) | 评委端 `activities/review`(上表) | +|------|------------------------------------------------------|-------------------------------------| +| 数据来源 | `GET /contests` 列表项中的 `reviewedCount` / `totalWorksCount` | `GET /contests/reviews/judge/contests` | +| 含义 | `totalWorksCount`:该活动最新有效作品总数。`reviewedCount`:**该活动下已分配评委且全部分配记录均为 `completed` 的作品数**(与分配表 `t_biz_contest_work_judge_assignment` 一致,与作品表 `accepted`/`awarded` 终态无关) | **评委维度**:`reviewed`/`totalAssigned`/`pending` 为该评委在分配表上的任务数;与租户「整作品是否全部评委评完」为不同聚合粒度 | +| 作品列表/详情 | `GET /contests/works` 每条作品含 `reviewedCount`/`totalJudgesCount`(按该作品分配条数统计) | 评委在单活动下作品列表同样基于分配 `completed` | + +说明:顶部「作品统计」卡片若仍按作品 `status` 汇总,可能与逐活动行「分配完成作品数」不完全同数,属汇总维度不同;列表/详情/评委任务以分配表为准。 diff --git a/docs/design/menu-config.md b/docs/design/menu-config.md index 2f53667..e5c3a1b 100644 --- a/docs/design/menu-config.md +++ b/docs/design/menu-config.md @@ -132,6 +132,8 @@ **定位**:评委评审工作台,只能看到自己被分配的活动和作品。 +**详细接口与字段说明**:[评委端评审任务](./judge-portal/review-tasks.md)。 + **一级菜单**:1 个(我的评审) ``` diff --git a/docs/design/org-admin/tenant-portal-optimization.md b/docs/design/org-admin/tenant-portal-optimization.md index 1f0eedf..29b181c 100644 --- a/docs/design/org-admin/tenant-portal-optimization.md +++ b/docs/design/org-admin/tenant-portal-optimization.md @@ -3,7 +3,7 @@ > 所属端:租户端(机构管理员视角) > 状态:已优化 > 创建日期:2026-03-31 -> 最后更新:2026-03-31 +> 最后更新:2026-04-08 --- @@ -58,6 +58,12 @@ - [x] 主色调统一 #6366f1 - [x] 冻结/解冻二次确认 +#### 赛事评委接口(`GET /contests/judges/contest/:id`) + +- 响应为结构化对象,包含两部分:**`assigned`**(机构在该赛事下**显式添加**的评委,对应 `t_biz_contest_judge`,每条均有 `id`、`judgeId` 等)与 **`implicitPool`**(平台评委租户下对该赛事**默认可用**、尚未写入关联表的用户,`id` 为 null,`isPlatform` 为 true)。 +- **添加评委抽屉**:「已选」回显与提交时的增删差集**仅基于 `assigned`**;可选评委仍来自评委管理分页接口。 +- **作品分配**:可选评委池为 **`assigned` ∪ `implicitPool`**(前端合并);表格行键与选中状态统一使用 **`judgeId`**,与分配接口提交的 `judgeIds` 一致。 + ### 报名管理(Index) - [x] 去掉个人/团队 Tab,合并展示加类型列 - [x] 统计概览(总报名/待审核/已通过/已拒绝) @@ -93,6 +99,7 @@ - [x] 评审状态改用实际完成率(无作品/未开始/进行中/已完成) - [x] 进度数字颜色区分 - [x] 评审进度详情页筛选修复(评审进度前端过滤生效) +- 活动列表接口 `GET /contests` 为每行返回 `reviewedCount`(该活动下**已分配且全部分配均为 completed** 的作品数)与 `totalWorksCount`(最新有效作品总数),与分配表及评委端评审任务口径一致;见 [评委端评审任务](../judge-portal/review-tasks.md#与租户端评审进度的口径对齐)。 ### 评审规则 - [x] 组件映射修复 diff --git a/frontend/src/api/contests.ts b/frontend/src/api/contests.ts index d99b171..3a40349 100644 --- a/frontend/src/api/contests.ts +++ b/frontend/src/api/contests.ts @@ -66,6 +66,10 @@ export interface Contest { teams: number; judges: number; }; + /** 评审进度(列表接口 findAll 填充):已分配且全部分配均为 completed 的作品数(与分配表一致) */ + reviewedCount?: number; + /** 评审进度(列表接口 findAll 填充):最新有效作品总数,与 _count.works 一致 */ + totalWorksCount?: number; } export interface CreateContestForm { @@ -119,7 +123,12 @@ export interface QueryContestParams extends PaginationParams { status?: "ongoing" | "finished"; contestType?: string; visibility?: string; - stage?: "unpublished" | "registering" | "submitting" | "reviewing" | "finished"; + stage?: + | "unpublished" + | "registering" + | "submitting" + | "reviewing" + | "finished"; creatorTenantId?: number; role?: "student" | "teacher" | "judge"; } @@ -383,6 +392,8 @@ export interface ContestWork { // 评审统计字段(由后端计算返回) reviewedCount?: number; totalJudgesCount?: number; + /** 终分(作品列表 workToMap 等接口返回) */ + finalScore?: number | null; averageScore?: number | null; } @@ -459,6 +470,8 @@ export interface ContestWorkJudgeAssignment { } export interface ContestWorkScore { + /** 与 id 同源,评分列表扁平接口常用 scoreId */ + scoreId?: number; id: number; tenantId: number; contestId: number; @@ -523,7 +536,8 @@ export interface CreateNoticeForm { // ==================== 评委相关类型 ==================== export interface ContestJudge { - id: number; + /** t_biz_contest_judge 主键;implicitPool 中隐式平台评委为 null */ + id?: number | null; contestId: number; judgeId: number; specialty?: string; @@ -534,6 +548,14 @@ export interface ContestJudge { createTime?: string; modifyTime?: string; validState?: number; + /** 隐式平台评委(未写入关联表时由后端追加) */ + isPlatform?: boolean; + judgeName?: string; + judgeUsername?: string; + assignedCount?: number; + organization?: string; + status?: string; + tenantId?: number; contest?: Contest; judge?: { id: number; @@ -541,8 +563,8 @@ export interface ContestJudge { nickname: string; email?: string; phone?: string; - gender?: 'male' | 'female'; - status?: 'enabled' | 'disabled'; + gender?: "male" | "female"; + status?: "enabled" | "disabled"; tenantId?: number; tenant?: { id: number; @@ -562,6 +584,19 @@ export interface ContestJudge { }; } +/** GET /contests/judges/contest/:id 结构化响应:显式关联与隐式平台池分离 */ +export interface ContestJudgesForContestResponse { + assigned: ContestJudge[]; + implicitPool: ContestJudge[]; +} + +/** 作品分配等场景:合并为可选评委池(assigned ∪ implicitPool) */ +export function flattenContestJudgePool( + r: ContestJudgesForContestResponse, +): ContestJudge[] { + return [...(r.assigned ?? []), ...(r.implicitPool ?? [])]; +} + export interface CreateJudgeForm { contestId: number; judgeId: number; @@ -582,22 +617,22 @@ export const contestsApi = { // 获取活动列表 getList: async ( - params: QueryContestParams + params: QueryContestParams, ): Promise> => { const response = await request.get>( "/contests", - { params } + { params }, ); return response; }, // 获取我参与的活动列表 getMyContests: async ( - params: QueryContestParams + params: QueryContestParams, ): Promise> => { const response = await request.get>( "/contests/my-contests", - { params } + { params }, ); return response; }, @@ -623,11 +658,11 @@ export const contestsApi = { // 发布/撤回活动 publish: async ( id: number, - contestState: "unpublished" | "published" + contestState: "unpublished" | "published", ): Promise => { const response = await request.patch( `/contests/${id}/publish`, - { contestState } + { contestState }, ); return response; }, @@ -639,13 +674,17 @@ export const contestsApi = { // 标记活动完结 finish: async (id: number): Promise => { - const response = await request.patch(`/contests/${id}/finish`); + const response = await request.patch( + `/contests/${id}/finish`, + ); return response; }, // 重新开启已完结的活动 reopen: async (id: number): Promise => { - const response = await request.patch(`/contests/${id}/reopen`); + const response = await request.patch( + `/contests/${id}/reopen`, + ); return response; }, }; @@ -655,7 +694,7 @@ export const attachmentsApi = { // 获取活动附件列表 getList: async (contestId: number): Promise => { const response = await request.get( - `/contests/attachments/contest/${contestId}` + `/contests/attachments/contest/${contestId}`, ); return response; }, @@ -664,7 +703,7 @@ export const attachmentsApi = { create: async (data: CreateAttachmentForm): Promise => { const response = await request.post( "/contests/attachments", - data + data, ); return response; }, @@ -710,25 +749,30 @@ export interface QueryReviewRuleParams { export const reviewRulesApi = { // 获取评审规则列表 - getList: async (params?: QueryReviewRuleParams): Promise<{ + getList: async ( + params?: QueryReviewRuleParams, + ): Promise<{ list: ReviewRule[]; total: number; page: number; pageSize: number; }> => { - const response = await request.get("/contests/review-rules", { params }); + const response = await request.get< + any, + { + list: ReviewRule[]; + total: number; + page: number; + pageSize: number; + } + >("/contests/review-rules", { params }); return response; }, // 获取所有可用的评审规则(用于活动创建时选择) getForSelect: async (): Promise => { const response = await request.get( - "/contests/review-rules/select" + "/contests/review-rules/select", ); return response; }, @@ -736,7 +780,7 @@ export const reviewRulesApi = { // 获取评审规则详情 getDetail: async (id: number): Promise => { const response = await request.get( - `/contests/review-rules/${id}` + `/contests/review-rules/${id}`, ); return response; }, @@ -745,7 +789,7 @@ export const reviewRulesApi = { create: async (data: CreateReviewRuleForm): Promise => { const response = await request.post( "/contests/review-rules", - data + data, ); return response; }, @@ -753,11 +797,11 @@ export const reviewRulesApi = { // 更新评审规则 update: async ( id: number, - data: Partial + data: Partial, ): Promise => { const response = await request.patch( `/contests/review-rules/${id}`, - data + data, ); return response; }, @@ -776,14 +820,14 @@ export const registrationsApi = { if (contestId) params.contestId = contestId; const response = await request.get( "/contests/registrations/stats", - { params } + { params }, ); return response; }, // 获取报名列表 getList: async ( - params: QueryRegistrationParams + params: QueryRegistrationParams, ): Promise> => { const response = await request.get< any, @@ -795,28 +839,28 @@ export const registrationsApi = { // 获取报名详情 getDetail: async (id: number): Promise => { const response = await request.get( - `/contests/registrations/${id}` + `/contests/registrations/${id}`, ); return response; }, // 获取当前用户在某活动中的报名记录(包括作为团队成员的情况) getMyRegistration: async ( - contestId: number + contestId: number, ): Promise => { const response = await request.get( - `/contests/registrations/my/${contestId}` + `/contests/registrations/my/${contestId}`, ); return response; }, // 创建报名 create: async ( - data: CreateRegistrationForm + data: CreateRegistrationForm, ): Promise => { const response = await request.post( "/contests/registrations", - data + data, ); return response; }, @@ -824,11 +868,11 @@ export const registrationsApi = { // 添加指导老师 addTeacher: async ( registrationId: number, - teacherUserId: number + teacherUserId: number, ): Promise => { const response = await request.post( `/contests/registrations/${registrationId}/teachers`, - { teacherUserId } + { teacherUserId }, ); return response; }, @@ -836,33 +880,42 @@ export const registrationsApi = { // 移除指导老师 removeTeacher: async ( registrationId: number, - teacherUserId: number + teacherUserId: number, ): Promise => { await request.delete( - `/contests/registrations/${registrationId}/teachers/${teacherUserId}` + `/contests/registrations/${registrationId}/teachers/${teacherUserId}`, ); }, // 审核报名 review: async ( id: number, - data: ReviewRegistrationForm + data: ReviewRegistrationForm, ): Promise => { const response = await request.patch( `/contests/registrations/${id}/review`, - data + data, ); return response; }, // 撤销报名审核 revokeReview: async (id: number): Promise => { - return await request.patch(`/contests/registrations/${id}/revoke`); + return await request.patch( + `/contests/registrations/${id}/revoke`, + ); }, // 批量审核报名 - batchReview: async (data: { ids: number[]; registrationState: string; reason?: string }): Promise<{ success: boolean; count: number }> => { - return await request.post('/contests/registrations/batch-review', data); + batchReview: async (data: { + ids: number[]; + registrationState: string; + reason?: string; + }): Promise<{ success: boolean; count: number }> => { + return await request.post( + "/contests/registrations/batch-review", + data, + ); }, // 删除报名 @@ -876,7 +929,7 @@ export const teamsApi = { // 获取团队列表 getList: async (contestId: number): Promise => { const response = await request.get( - `/contests/teams/contest/${contestId}` + `/contests/teams/contest/${contestId}`, ); return response; }, @@ -884,7 +937,7 @@ export const teamsApi = { // 获取团队详情 getDetail: async (id: number): Promise => { const response = await request.get( - `/contests/teams/${id}` + `/contests/teams/${id}`, ); return response; }, @@ -893,7 +946,7 @@ export const teamsApi = { create: async (data: CreateTeamForm): Promise => { const response = await request.post( "/contests/teams", - data + data, ); return response; }, @@ -901,11 +954,11 @@ export const teamsApi = { // 更新团队 update: async ( id: number, - data: Partial + data: Partial, ): Promise => { const response = await request.patch( `/contests/teams/${id}`, - data + data, ); return response; }, @@ -913,11 +966,11 @@ export const teamsApi = { // 邀请成员 inviteMember: async ( teamId: number, - data: InviteMemberForm + data: InviteMemberForm, ): Promise => { const response = await request.post( `/contests/teams/${teamId}/members`, - data + data, ); return response; }, @@ -925,7 +978,7 @@ export const teamsApi = { // 移除成员 removeMember: async (teamId: number, userId: number): Promise => { return await request.delete( - `/contests/teams/${teamId}/members/${userId}` + `/contests/teams/${teamId}/members/${userId}`, ); }, @@ -956,29 +1009,29 @@ export const worksApi = { if (contestId) params.contestId = contestId; const response = await request.get( "/contests/works/stats", - { params } + { params }, ); return response; }, // 获取作品列表 getList: async ( - params: QueryWorkParams + params: QueryWorkParams, ): Promise> => { const response = await request.get>( "/contests/works", - { params } + { params }, ); return response; }, // 获取教师指导的作品列表 getGuidedWorks: async ( - params: QueryGuidedWorkParams + params: QueryGuidedWorkParams, ): Promise> => { const response = await request.get>( "/contests/works/guided", - { params } + { params }, ); return response; }, @@ -986,7 +1039,7 @@ export const worksApi = { // 获取作品详情 getDetail: async (id: number): Promise => { const response = await request.get( - `/contests/works/${id}` + `/contests/works/${id}`, ); return response; }, @@ -995,7 +1048,7 @@ export const worksApi = { submit: async (data: SubmitWorkForm): Promise => { const response = await request.post( "/contests/works/submit", - data + data, ); return response; }, @@ -1003,7 +1056,7 @@ export const worksApi = { // 获取作品版本列表 getVersions: async (registrationId: number): Promise => { const response = await request.get( - `/contests/works/registration/${registrationId}/versions` + `/contests/works/registration/${registrationId}/versions`, ); return response; }, @@ -1107,11 +1160,11 @@ export const reviewsApi = { // 分配作品给评委 assignWork: async ( contestId: number, - data: AssignWorkForm + data: AssignWorkForm, ): Promise => { const response = await request.post( `/contests/reviews/assign?contestId=${contestId}`, - data + data, ); return response; }, @@ -1119,11 +1172,11 @@ export const reviewsApi = { // 批量分配作品给评委 batchAssignWorks: async ( contestId: number, - data: BatchAssignForm + data: BatchAssignForm, ): Promise => { const response = await request.post( `/contests/reviews/batch-assign?contestId=${contestId}`, - data + data, ); return response; }, @@ -1131,7 +1184,7 @@ export const reviewsApi = { // 自动分配作品给评委 autoAssignWorks: async (contestId: number): Promise => { const response = await request.post( - `/contests/reviews/auto-assign?contestId=${contestId}` + `/contests/reviews/auto-assign?contestId=${contestId}`, ); return response; }, @@ -1140,7 +1193,7 @@ export const reviewsApi = { score: async (data: CreateScoreForm): Promise => { const response = await request.post( "/contests/reviews/score", - data + data, ); return response; }, @@ -1148,21 +1201,21 @@ export const reviewsApi = { // 更新评分 updateScore: async ( scoreId: number, - data: Partial + data: Partial, ): Promise => { const response = await request.patch( `/contests/reviews/score/${scoreId}`, - data + data, ); return response; }, // 获取分配给当前评委的作品 getAssignedWorks: async ( - contestId: number + contestId: number, ): Promise => { const response = await request.get( - `/contests/reviews/assigned?contestId=${contestId}` + `/contests/reviews/assigned?contestId=${contestId}`, ); return response; }, @@ -1170,7 +1223,7 @@ export const reviewsApi = { // 获取评审进度统计 getReviewProgress: async (contestId: number): Promise => { const response = await request.get( - `/contests/reviews/progress/${contestId}` + `/contests/reviews/progress/${contestId}`, ); return response; }, @@ -1178,7 +1231,7 @@ export const reviewsApi = { // 获取作品状态统计 getWorkStatusStats: async (contestId: number): Promise => { const response = await request.get( - `/contests/reviews/work-status/${contestId}` + `/contests/reviews/work-status/${contestId}`, ); return response; }, @@ -1186,14 +1239,14 @@ export const reviewsApi = { // 获取作品评分列表 getWorkScores: async (workId: number): Promise => { const response = await request.get( - `/contests/reviews/work/${workId}/scores` + `/contests/reviews/work/${workId}/scores`, ); return response; }, // 计算最终得分 calculateFinalScore: async ( - workId: number + workId: number, ): Promise<{ finalScore: number; scoreCount: number; @@ -1209,18 +1262,18 @@ export const reviewsApi = { // 替换评委 replaceJudge: async ( assignmentId: number, - newJudgeId: number + newJudgeId: number, ): Promise => { - await request.post( - `/contests/reviews/replace-judge`, - { assignmentId, newJudgeId } - ); + await request.post(`/contests/reviews/replace-judge`, { + assignmentId, + newJudgeId, + }); }, // 获取评委参与的活动列表 getJudgeContests: async (): Promise => { const response = await request.get( - `/contests/reviews/judge/contests` + `/contests/reviews/judge/contests`, ); return response; }, @@ -1234,8 +1287,13 @@ export const reviewsApi = { workNo?: string; accountNo?: string; reviewStatus?: string; - } - ): Promise<{ list: any[]; total: number; page: number; pageSize: number }> => { + }, + ): Promise<{ + list: any[]; + total: number; + page: number; + pageSize: number; + }> => { const response = await request.get< any, { list: any[]; total: number; page: number; pageSize: number } @@ -1246,7 +1304,7 @@ export const reviewsApi = { // 获取评委视角的赛事详情(含评审规则) getJudgeContestDetail: async (contestId: number): Promise => { const response = await request.get( - `/contests/reviews/judge/contests/${contestId}/detail` + `/contests/reviews/judge/contests/${contestId}/detail`, ); return response; }, @@ -1257,7 +1315,7 @@ export const noticesApi = { // 获取公告列表(按活动) getList: async (contestId: number): Promise => { const response = await request.get( - `/contests/notices/contest/${contestId}` + `/contests/notices/contest/${contestId}`, ); return response; }, @@ -1270,8 +1328,8 @@ export const noticesApi = { pageSize?: number; }): Promise> => { const response = await request.get>( - '/contests/notices', - { params } + "/contests/notices", + { params }, ); return response; }, @@ -1279,7 +1337,7 @@ export const noticesApi = { // 获取公告详情 getDetail: async (id: number): Promise => { const response = await request.get( - `/contests/notices/${id}` + `/contests/notices/${id}`, ); return response; }, @@ -1288,7 +1346,7 @@ export const noticesApi = { create: async (data: CreateNoticeForm): Promise => { const response = await request.post( "/contests/notices", - data + data, ); return response; }, @@ -1296,11 +1354,11 @@ export const noticesApi = { // 更新公告 update: async ( id: number, - data: Partial + data: Partial, ): Promise => { const response = await request.patch( `/contests/notices/${id}`, - data + data, ); return response; }, @@ -1373,7 +1431,7 @@ export interface SetAwardForm { export interface BatchSetAwardsForm { awards: Array<{ workId: number; - awardLevel: 'first' | 'second' | 'third' | 'excellent' | 'none'; + awardLevel: "first" | "second" | "third" | "excellent" | "none"; awardName?: string; }>; } @@ -1385,44 +1443,61 @@ export interface AutoSetAwardsForm { // 成果管理 export const resultsApi = { // 计算所有作品的最终得分 - calculateScores: async (contestId: number): Promise<{ message: string; calculatedCount: number; calculationRule: string }> => { + calculateScores: async ( + contestId: number, + ): Promise<{ + message: string; + calculatedCount: number; + calculationRule: string; + }> => { const response = await request.post( - `/contests/results/${contestId}/calculate-scores` + `/contests/results/${contestId}/calculate-scores`, ); return response; }, // 计算排名 - calculateRankings: async (contestId: number): Promise<{ message: string; rankedCount: number }> => { + calculateRankings: async ( + contestId: number, + ): Promise<{ message: string; rankedCount: number }> => { const response = await request.post( - `/contests/results/${contestId}/calculate-rankings` + `/contests/results/${contestId}/calculate-rankings`, ); return response; }, // 设置单个作品奖项 - setAward: async (workId: number, data: SetAwardForm): Promise => { + setAward: async ( + workId: number, + data: SetAwardForm, + ): Promise => { const response = await request.patch( `/contests/results/work/${workId}/award`, - data + data, ); return response; }, // 批量设置奖项 - batchSetAwards: async (contestId: number, data: BatchSetAwardsForm): Promise => { + batchSetAwards: async ( + contestId: number, + data: BatchSetAwardsForm, + ): Promise => { const response = await request.post( `/contests/results/${contestId}/batch-set-awards`, - data + data, ); return response; }, // 根据排名自动设置奖项 - autoSetAwards: async (contestId: number, data: AutoSetAwardsForm): Promise => { + autoSetAwards: async ( + contestId: number, + data: AutoSetAwardsForm, + ): Promise => { const response = await request.post( `/contests/results/${contestId}/auto-set-awards`, - data + data, ); return response; }, @@ -1430,7 +1505,7 @@ export const resultsApi = { // 发布成果 publish: async (contestId: number): Promise => { const response = await request.post( - `/contests/results/${contestId}/publish` + `/contests/results/${contestId}/publish`, ); return response; }, @@ -1438,7 +1513,7 @@ export const resultsApi = { // 撤回发布 unpublish: async (contestId: number): Promise => { const response = await request.post( - `/contests/results/${contestId}/unpublish` + `/contests/results/${contestId}/unpublish`, ); return response; }, @@ -1451,11 +1526,17 @@ export const resultsApi = { pageSize?: number; workNo?: string; accountNo?: string; - } = {} + } = {}, ): Promise => { const response = await request.get( `/contests/results/${contestId}`, - { params: { page: params.page || 1, pageSize: params.pageSize || 10, ...params } } + { + params: { + page: params.page || 1, + pageSize: params.pageSize || 10, + ...params, + }, + }, ); return response; }, @@ -1463,7 +1544,7 @@ export const resultsApi = { // 获取活动结果统计摘要 getSummary: async (contestId: number): Promise => { const response = await request.get( - `/contests/results/${contestId}/summary` + `/contests/results/${contestId}/summary`, ); return response; }, @@ -1471,10 +1552,12 @@ export const resultsApi = { // 评委管理 export const judgesApi = { - // 获取评委列表 - getList: async (contestId: number): Promise => { - const response = await request.get( - `/contests/judges/contest/${contestId}` + // 获取赛事评委(assigned + implicitPool) + getList: async ( + contestId: number, + ): Promise => { + const response = await request.get( + `/contests/judges/contest/${contestId}`, ); return response; }, @@ -1482,7 +1565,7 @@ export const judgesApi = { // 获取评委详情 getDetail: async (id: number): Promise => { const response = await request.get( - `/contests/judges/${id}` + `/contests/judges/${id}`, ); return response; }, @@ -1491,7 +1574,7 @@ export const judgesApi = { create: async (data: CreateJudgeForm): Promise => { const response = await request.post( "/contests/judges", - data + data, ); return response; }, @@ -1499,11 +1582,11 @@ export const judgesApi = { // 更新评委 update: async ( id: number, - data: Partial + data: Partial, ): Promise => { const response = await request.patch( `/contests/judges/${id}`, - data + data, ); return response; }, diff --git a/frontend/src/views/activities/Review.vue b/frontend/src/views/activities/Review.vue index 5f0d762..66b57ae 100644 --- a/frontend/src/views/activities/Review.vue +++ b/frontend/src/views/activities/Review.vue @@ -29,7 +29,8 @@ {{ record.reviewed }}/{{ record.totalAssigned }}