diff --git a/backend-java/src/main/java/com/competition/modules/biz/contest/service/IContestWorkService.java b/backend-java/src/main/java/com/competition/modules/biz/contest/service/IContestWorkService.java index 3d4f9cf..57700f7 100644 --- a/backend-java/src/main/java/com/competition/modules/biz/contest/service/IContestWorkService.java +++ b/backend-java/src/main/java/com/competition/modules/biz/contest/service/IContestWorkService.java @@ -11,6 +11,11 @@ import java.util.Map; public interface IContestWorkService extends IService { + /** + * 为指定赛事生成下一个作品编号(与 {@link #submitWork} 所用规则一致:W{contestId}-{序号})。 + */ + String nextContestWorkNo(Long contestId); + Map submitWork(SubmitWorkDto dto, Long tenantId, Long submitterId); PageResult> findAll(QueryWorkDto dto, Long tenantId, boolean isSuperTenant); 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 e67ecea..6c39570 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 @@ -110,7 +110,7 @@ public class ContestWorkServiceImpl extends ServiceImpl wrapper = new LambdaQueryWrapper<>(); wrapper.eq(BizContestWork::getContestId, contestId); long count = count(wrapper); diff --git a/backend-java/src/main/java/com/competition/modules/biz/homework/service/impl/HomeworkSubmissionServiceImpl.java b/backend-java/src/main/java/com/competition/modules/biz/homework/service/impl/HomeworkSubmissionServiceImpl.java index 27da0c1..3adb9e4 100644 --- a/backend-java/src/main/java/com/competition/modules/biz/homework/service/impl/HomeworkSubmissionServiceImpl.java +++ b/backend-java/src/main/java/com/competition/modules/biz/homework/service/impl/HomeworkSubmissionServiceImpl.java @@ -55,10 +55,17 @@ public class HomeworkSubmissionServiceImpl extends ServiceImpl seqWrapper = new LambdaQueryWrapper<>(); + seqWrapper.eq(BizHomeworkSubmission::getHomeworkId, dto.getHomeworkId()); + seqWrapper.eq(BizHomeworkSubmission::getValidState, 1); + long homeworkSubmitCount = count(seqWrapper); + String workNo = "H" + dto.getHomeworkId() + "-" + (homeworkSubmitCount + 1); + BizHomeworkSubmission entity = new BizHomeworkSubmission(); entity.setTenantId(tenantId); entity.setHomeworkId(dto.getHomeworkId()); entity.setStudentId(studentId); + entity.setWorkNo(workNo); entity.setWorkName(dto.getWorkName()); entity.setWorkDescription(dto.getWorkDescription()); entity.setFiles(dto.getFiles()); diff --git a/backend-java/src/main/java/com/competition/modules/biz/review/service/impl/ContestResultServiceImpl.java b/backend-java/src/main/java/com/competition/modules/biz/review/service/impl/ContestResultServiceImpl.java index bc3d46f..b648fdc 100644 --- a/backend-java/src/main/java/com/competition/modules/biz/review/service/impl/ContestResultServiceImpl.java +++ b/backend-java/src/main/java/com/competition/modules/biz/review/service/impl/ContestResultServiceImpl.java @@ -7,8 +7,10 @@ import com.competition.common.enums.PublishStatus; import com.competition.common.exception.BusinessException; import com.competition.common.result.PageResult; import com.competition.modules.biz.contest.entity.BizContest; +import com.competition.modules.biz.contest.entity.BizContestRegistration; import com.competition.modules.biz.contest.entity.BizContestWork; 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.review.dto.AutoSetAwardsDto; import com.competition.modules.biz.review.dto.BatchSetAwardsDto; @@ -38,6 +40,7 @@ public class ContestResultServiceImpl implements IContestResultService { private final ContestWorkMapper workMapper; private final ContestMapper contestMapper; + private final ContestRegistrationMapper contestRegistrationMapper; private final ContestReviewRuleMapper reviewRuleMapper; private final ContestWorkScoreMapper scoreMapper; private final ContestJudgeMapper judgeMapper; @@ -309,7 +312,19 @@ public class ContestResultServiceImpl implements IContestResultService { wrapper.like(BizContestWork::getWorkNo, workNo); } if (StringUtils.hasText(accountNo)) { - wrapper.like(BizContestWork::getSubmitterAccountNo, accountNo); + LambdaQueryWrapper regQw = new LambdaQueryWrapper<>(); + regQw.eq(BizContestRegistration::getContestId, contestId); + regQw.eq(BizContestRegistration::getValidState, 1); + regQw.like(BizContestRegistration::getAccountNo, accountNo); + List matchRegIds = contestRegistrationMapper.selectList(regQw).stream() + .map(BizContestRegistration::getId) + .collect(Collectors.toList()); + wrapper.and(q -> { + q.like(BizContestWork::getSubmitterAccountNo, accountNo); + if (!matchRegIds.isEmpty()) { + q.or().in(BizContestWork::getRegistrationId, matchRegIds); + } + }); } wrapper.orderByDesc(BizContestWork::getFinalScore); @@ -317,8 +332,21 @@ public class ContestResultServiceImpl implements IContestResultService { Page pageObj = new Page<>(page, pageSize); Page result = workMapper.selectPage(pageObj, wrapper); - List> voList = result.getRecords().stream().map(w -> { + List records = result.getRecords(); + Set registrationIds = records.stream() + .map(BizContestWork::getRegistrationId) + .filter(Objects::nonNull) + .collect(Collectors.toSet()); + Map registrationById = new HashMap<>(); + if (!registrationIds.isEmpty()) { + for (BizContestRegistration reg : contestRegistrationMapper.selectBatchIds(registrationIds)) { + registrationById.put(reg.getId(), reg); + } + } + + List> voList = records.stream().map(w -> { Map map = new LinkedHashMap<>(); + map.put("id", w.getId()); map.put("workId", w.getId()); map.put("workNo", w.getWorkNo()); map.put("title", w.getTitle()); @@ -330,6 +358,10 @@ public class ContestResultServiceImpl implements IContestResultService { map.put("awardName", w.getAwardName()); map.put("certificateUrl", w.getCertificateUrl()); map.put("submitTime", w.getSubmitTime()); + BizContestRegistration reg = w.getRegistrationId() != null + ? registrationById.get(w.getRegistrationId()) + : null; + map.put("registration", reg != null ? registrationToNestedMap(reg) : null); return map; }).collect(Collectors.toList()); @@ -397,20 +429,63 @@ public class ContestResultServiceImpl implements IContestResultService { } } + BizContest contest = contestMapper.selectById(contestId); + if (contest == null) { + throw BusinessException.of(ErrorCode.NOT_FOUND, "赛事不存在"); + } + + Map contestMap = new LinkedHashMap<>(); + contestMap.put("id", contest.getId()); + contestMap.put("contestName", contest.getContestName()); + contestMap.put("resultState", contest.getResultState()); + contestMap.put("resultPublishTime", contest.getResultPublishTime()); + contestMap.put("contestType", contest.getContestType()); + + long unscoredWorks = Math.max(0, totalWorks - scoredWorks); + + Map summaryMap = new LinkedHashMap<>(); + summaryMap.put("totalWorks", totalWorks); + summaryMap.put("scoredWorks", scoredWorks); + summaryMap.put("rankedWorks", rankedWorks); + summaryMap.put("awardedWorks", awardedWorks); + summaryMap.put("unscoredWorks", unscoredWorks); + + Map scoreStats = new LinkedHashMap<>(); + scoreStats.put("avgScore", scoredList.isEmpty() ? null : avgScore.toPlainString()); + scoreStats.put("maxScore", scoredList.isEmpty() ? null : maxScore.toPlainString()); + scoreStats.put("minScore", scoredList.isEmpty() ? null : minScore.toPlainString()); + Map result = new LinkedHashMap<>(); - result.put("totalWorks", totalWorks); - result.put("scoredWorks", scoredWorks); - result.put("rankedWorks", rankedWorks); - result.put("awardedWorks", awardedWorks); - result.put("avgScore", avgScore); - result.put("maxScore", maxScore); - result.put("minScore", minScore); + result.put("contest", contestMap); + result.put("summary", summaryMap); + result.put("scoreStats", scoreStats); result.put("awardDistribution", awardDistribution); return result; } // ====== 私有辅助方法 ====== + /** + * 与前端成果列表/作品管理一致:registration.user / registration.team 结构 + */ + private Map registrationToNestedMap(BizContestRegistration reg) { + Map regMap = new LinkedHashMap<>(); + Map user = new LinkedHashMap<>(); + user.put("id", reg.getUserId()); + user.put("username", reg.getAccountNo()); + user.put("nickname", reg.getAccountName()); + regMap.put("user", user); + Map team = null; + if ("team".equals(reg.getRegistrationType()) + && (reg.getTeamId() != null || StringUtils.hasText(reg.getTeamName()))) { + team = new LinkedHashMap<>(); + team.put("id", reg.getTeamId()); + team.put("teamName", reg.getTeamName()); + } + regMap.put("team", team); + return regMap; + } + private BigDecimal doCalculate(List scores, String calculationRule, Map weightMap) { List scoreValues = scores.stream() .map(BizContestWorkScore::getTotalScore) diff --git a/backend-java/src/main/java/com/competition/modules/biz/review/service/impl/ContestReviewServiceImpl.java b/backend-java/src/main/java/com/competition/modules/biz/review/service/impl/ContestReviewServiceImpl.java index 2f2b85b..fbeeff3 100644 --- a/backend-java/src/main/java/com/competition/modules/biz/review/service/impl/ContestReviewServiceImpl.java +++ b/backend-java/src/main/java/com/competition/modules/biz/review/service/impl/ContestReviewServiceImpl.java @@ -621,12 +621,40 @@ public class ContestReviewServiceImpl implements IContestReviewService { List scores = scoreMapper.selectList(wrapper); + Set judgeIds = scores.stream() + .map(BizContestWorkScore::getJudgeId) + .filter(Objects::nonNull) + .collect(Collectors.toSet()); + Map judgeUserById = new HashMap<>(); + if (!judgeIds.isEmpty()) { + for (SysUser u : sysUserMapper.selectBatchIds(judgeIds)) { + if (u != null && u.getId() != null) { + judgeUserById.put(u.getId(), u); + } + } + } + return scores.stream().map(s -> { Map map = new LinkedHashMap<>(); + map.put("id", s.getId()); map.put("scoreId", s.getId()); map.put("assignmentId", s.getAssignmentId()); map.put("judgeId", s.getJudgeId()); - map.put("judgeName", s.getJudgeName()); + SysUser judgeUser = s.getJudgeId() != null ? judgeUserById.get(s.getJudgeId()) : null; + String judgeName = s.getJudgeName(); + if (!StringUtils.hasText(judgeName) && judgeUser != null) { + judgeName = StringUtils.hasText(judgeUser.getNickname()) + ? judgeUser.getNickname() + : judgeUser.getUsername(); + } + map.put("judgeName", judgeName); + if (judgeUser != null) { + Map judge = new LinkedHashMap<>(); + judge.put("id", judgeUser.getId()); + judge.put("username", judgeUser.getUsername()); + judge.put("nickname", judgeUser.getNickname()); + map.put("judge", judge); + } map.put("dimensionScores", s.getDimensionScores()); map.put("totalScore", s.getTotalScore()); map.put("comments", s.getComments()); diff --git a/backend-java/src/main/java/com/competition/modules/pub/service/PublicActivityService.java b/backend-java/src/main/java/com/competition/modules/pub/service/PublicActivityService.java index 5af0f94..95daa4e 100644 --- a/backend-java/src/main/java/com/competition/modules/pub/service/PublicActivityService.java +++ b/backend-java/src/main/java/com/competition/modules/pub/service/PublicActivityService.java @@ -13,6 +13,7 @@ import com.competition.modules.biz.contest.entity.BizContestWork; 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.IContestWorkService; import com.competition.modules.pub.dto.PublicRegisterActivityDto; import com.competition.modules.ugc.entity.UgcWork; import com.competition.modules.ugc.entity.UgcWorkPage; @@ -37,6 +38,7 @@ public class PublicActivityService { private final ContestMapper contestMapper; private final ContestRegistrationMapper contestRegistrationMapper; private final ContestWorkMapper contestWorkMapper; + private final IContestWorkService contestWorkService; private final UserChildMapper userChildMapper; private final UgcWorkMapper ugcWorkMapper; private final UgcWorkPageMapper ugcWorkPageMapper; @@ -378,6 +380,8 @@ public class PublicActivityService { work.setSubmitTime(LocalDateTime.now()); work.setVersion(nextVersion); work.setIsLatest(true); + work.setWorkNo(contestWorkService.nextContestWorkNo(contestId)); + work.setSubmitterAccountNo(reg.getAccountNo()); // 从作品库选择作品提交(快照复制) if (dto.get("userWorkId") != null) { diff --git a/backend-java/src/main/resources/db/migration/V8__backfill_work_no.sql b/backend-java/src/main/resources/db/migration/V8__backfill_work_no.sql new file mode 100644 index 0000000..a9cb30c --- /dev/null +++ b/backend-java/src/main/resources/db/migration/V8__backfill_work_no.sql @@ -0,0 +1,9 @@ +-- 回填历史数据中空的作品编号(与运行时生成形态一致:W{contestId}-* / H{homeworkId}-*) + +UPDATE t_biz_contest_work +SET work_no = CONCAT('W', contest_id, '-', id) +WHERE work_no IS NULL OR TRIM(work_no) = ''; + +UPDATE t_biz_homework_submission +SET work_no = CONCAT('H', homework_id, '-', id) +WHERE work_no IS NULL OR TRIM(work_no) = ''; diff --git a/frontend/src/api/contests.ts b/frontend/src/api/contests.ts index 3a40349..b89dedc 100644 --- a/frontend/src/api/contests.ts +++ b/frontend/src/api/contests.ts @@ -1374,6 +1374,8 @@ export interface ContestResult { id: number; workNo: string | null; title: string; + /** 作品上冗余的提交账号,与报名账号一致时优先展示 */ + submitterAccountNo?: string | null; finalScore: number | null; rank: number | null; awardLevel: string | null; @@ -1404,6 +1406,7 @@ export interface ResultsSummary { contest: { id: number; contestName: string; + contestType?: "individual" | "team"; resultState: string; resultPublishTime: string | null; }; diff --git a/frontend/src/views/contests/components/WorkDetailModal.vue b/frontend/src/views/contests/components/WorkDetailModal.vue index 10908a3..cea8ab7 100644 --- a/frontend/src/views/contests/components/WorkDetailModal.vue +++ b/frontend/src/views/contests/components/WorkDetailModal.vue @@ -84,42 +84,43 @@ - -
+ +
评审记录
@@ -161,9 +162,29 @@ const visible = ref(false) const loading = ref(false) const workDetail = ref(null) const reviewRecords = ref([]) -const activeReviewTab = ref("") const showPreviewBtn = ref(false) +/** 评委展示名:接口扁平字段 judgeName,或嵌套 judge(与列表接口一致) */ +const judgeNameText = (record: any) => { + const flat = record?.judgeName + if (flat != null && String(flat).trim() !== '') return String(flat).trim() + return ( + record?.judge?.nickname || + record?.judge?.username || + '' + ) +} + +/** 纵向列表标题:有姓名用姓名,否则「评委一、评委二…」 */ +const judgeDisplayName = (record: any, index: number) => { + const name = judgeNameText(record) + if (name) return name + const numerals = ['一', '二', '三', '四', '五', '六', '七', '八', '九', '十'] + const n = index + 1 + if (n >= 1 && n <= 10) return `评委${numerals[n - 1]}` + return `评委 ${n}` +} + // 抽屉标题 const drawerTitle = computed(() => { if (workDetail.value) { @@ -268,9 +289,6 @@ const fetchReviewRecords = async (workId: number) => { try { const records = await reviewsApi.getWorkScores(workId) reviewRecords.value = records || [] - if (records && records.length > 0) { - activeReviewTab.value = records[0].id - } } catch (error) { console.error("获取评审记录失败", error) reviewRecords.value = [] @@ -354,6 +372,9 @@ watch( .work-detail-drawer { :deep(.ant-drawer-body) { padding: 16px 24px; + overflow-x: hidden; + max-width: 100%; + box-sizing: border-box; } } @@ -475,9 +496,31 @@ watch( } } +.section-review { + min-width: 0; +} + .review-records { - :deep(.ant-tabs-nav) { + min-width: 0; + width: 100%; + + .review-judge-block { margin-bottom: 16px; + + &:last-child { + margin-bottom: 0; + } + } + + .review-judge-header { + margin-bottom: 8px; + } + + .review-judge-title { + font-size: 14px; + font-weight: 600; + color: #1f2937; + word-break: break-all; } .review-card { @@ -486,11 +529,15 @@ watch( padding: 16px; border-radius: 0 8px 8px 0; box-shadow: 0 1px 4px rgba(0, 0, 0, 0.08); + max-width: 100%; + box-sizing: border-box; .review-item { margin-bottom: 12px; font-size: 14px; line-height: 1.6; + word-break: break-word; + overflow-wrap: anywhere; &:last-child { margin-bottom: 0; @@ -498,6 +545,7 @@ watch( .review-label { color: #666; + flex-shrink: 0; } .review-value { diff --git a/frontend/src/views/contests/results/Detail.vue b/frontend/src/views/contests/results/Detail.vue index 9c5a3d6..af4f40f 100644 --- a/frontend/src/views/contests/results/Detail.vue +++ b/frontend/src/views/contests/results/Detail.vue @@ -105,10 +105,15 @@ -