diff --git a/docs/design/judge-portal/review-tasks.md b/docs/design/judge-portal/review-tasks.md index 1c1356b..9bc900b 100644 --- a/docs/design/judge-portal/review-tasks.md +++ b/docs/design/judge-portal/review-tasks.md @@ -47,6 +47,22 @@ 避免「列表能进、详情 403」与隐式评委场景不一致。 +## 预设评语(跨活动同步) + +与 [菜单配置说明](../menu-config.md) 中「预设评语」能力一致:评委维护常用评语模板,**不按活动筛选**;列表与评审弹窗「老师点评 → 选择评语」使用**同一查询**。 + +**列表**:`GET /contests/preset-comments` + +- 按当前登录用户的评委身份(`judge_id`)返回其全部有效预设评语。 +- **不要**使用 query 参数 `contestId`(接口不提供按活动筛选)。 + +**创建**:`POST /contests/preset-comments` + +- 请求体中 `contestId` **可选**;不传或为 `null` 表示全局模板(库表 `t_biz_preset_comment.contest_id` 为空,所有活动评审可选用)。 +- `content` 必填。 + +**其它**:详情/修改/删除/批量删除/增加使用次数等见 `PresetCommentController`(Knife4j)。 + ## 标记作品违规(活动参赛作品) **接口**:`POST /contests/reviews/work/{workId}/violation` diff --git a/lesingle-creation-backend/src/main/java/com/lesingle/modules/biz/review/controller/PresetCommentController.java b/lesingle-creation-backend/src/main/java/com/lesingle/modules/biz/review/controller/PresetCommentController.java index 89c2a58..f482eb1 100644 --- a/lesingle-creation-backend/src/main/java/com/lesingle/modules/biz/review/controller/PresetCommentController.java +++ b/lesingle-creation-backend/src/main/java/com/lesingle/modules/biz/review/controller/PresetCommentController.java @@ -3,7 +3,7 @@ package com.lesingle.modules.biz.review.controller; import com.lesingle.common.result.Result; import com.lesingle.common.util.SecurityUtil; import com.lesingle.modules.biz.review.dto.CreatePresetCommentDto; -import com.lesingle.modules.biz.review.dto.SyncPresetCommentsDto; +import com.lesingle.modules.biz.review.dto.PresetCommentVo; import com.lesingle.modules.biz.review.entity.BizPresetComment; import com.lesingle.modules.biz.review.service.IPresetCommentService; import io.swagger.v3.oas.annotations.Operation; @@ -31,22 +31,15 @@ public class PresetCommentController { } @GetMapping - @Operation(summary = "查询预设评语列表") - public Result>> findAll(@RequestParam(required = false) Long contestId) { + @Operation(summary = "查询当前登录评委的预设评语列表(不按活动筛选)") + public Result> findAll() { Long judgeId = SecurityUtil.getCurrentUserId(); - return Result.success(presetCommentService.findAll(contestId, judgeId)); - } - - @GetMapping("/judge/contests") - @Operation(summary = "获取评委参与的活动列表") - public Result>> getJudgeContests() { - Long judgeId = SecurityUtil.getCurrentUserId(); - return Result.success(presetCommentService.getJudgeContests(judgeId)); + return Result.success(presetCommentService.findAll(judgeId)); } @GetMapping("/{id}") @Operation(summary = "查询预设评语详情") - public Result> findDetail(@PathVariable Long id) { + public Result findDetail(@PathVariable Long id) { Long judgeId = SecurityUtil.getCurrentUserId(); return Result.success(presetCommentService.findDetail(id, judgeId)); } @@ -75,13 +68,6 @@ public class PresetCommentController { return Result.success(); } - @PostMapping("/sync") - @Operation(summary = "同步评语到其他活动") - public Result> syncComments(@Valid @RequestBody SyncPresetCommentsDto dto) { - Long judgeId = SecurityUtil.getCurrentUserId(); - return Result.success(presetCommentService.syncComments(dto.getSourceContestId(), dto.getTargetContestIds(), judgeId)); - } - @PostMapping("/{id}/use") @Operation(summary = "增加评语使用次数") public Result incrementUseCount(@PathVariable Long id) { diff --git a/lesingle-creation-backend/src/main/java/com/lesingle/modules/biz/review/dto/CreatePresetCommentDto.java b/lesingle-creation-backend/src/main/java/com/lesingle/modules/biz/review/dto/CreatePresetCommentDto.java index 05065fc..67eff2b 100644 --- a/lesingle-creation-backend/src/main/java/com/lesingle/modules/biz/review/dto/CreatePresetCommentDto.java +++ b/lesingle-creation-backend/src/main/java/com/lesingle/modules/biz/review/dto/CreatePresetCommentDto.java @@ -2,7 +2,6 @@ package com.lesingle.modules.biz.review.dto; import io.swagger.v3.oas.annotations.media.Schema; import jakarta.validation.constraints.NotBlank; -import jakarta.validation.constraints.NotNull; import lombok.Data; import java.math.BigDecimal; @@ -11,8 +10,7 @@ import java.math.BigDecimal; @Schema(description = "创建预设评语DTO") public class CreatePresetCommentDto { - @NotNull(message = "活动ID不能为空") - @Schema(description = "活动ID") + @Schema(description = "活动ID;不传或为 null 表示全局模板(跨活动复用)") private Long contestId; @NotBlank(message = "评语内容不能为空") diff --git a/lesingle-creation-backend/src/main/java/com/lesingle/modules/biz/review/dto/PresetCommentVo.java b/lesingle-creation-backend/src/main/java/com/lesingle/modules/biz/review/dto/PresetCommentVo.java new file mode 100644 index 0000000..8a33629 --- /dev/null +++ b/lesingle-creation-backend/src/main/java/com/lesingle/modules/biz/review/dto/PresetCommentVo.java @@ -0,0 +1,64 @@ +package com.lesingle.modules.biz.review.dto; + +import com.lesingle.modules.biz.review.entity.BizPresetComment; +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; + +import java.math.BigDecimal; +import java.time.LocalDateTime; + +/** + * 预设评语展示对象,与「预设评语管理」列表列一致:序号(前端本地)、评语内容、使用次数、操作依赖 id。 + */ +@Data +@Schema(description = "预设评语") +public class PresetCommentVo { + + @Schema(description = "主键") + private Long id; + + @Schema(description = "活动ID,可为空") + private Long contestId; + + @Schema(description = "评委用户ID") + private Long judgeId; + + @Schema(description = "评语内容") + private String content; + + @Schema(description = "关联分数") + private BigDecimal score; + + @Schema(description = "排序") + private Integer sortOrder; + + @Schema(description = "使用次数") + private Integer useCount; + + @Schema(description = "有效状态:1-有效") + private Integer validState; + + @Schema(description = "创建时间") + private LocalDateTime createTime; + + @Schema(description = "修改时间") + private LocalDateTime modifyTime; + + public static PresetCommentVo fromEntity(BizPresetComment entity) { + if (entity == null) { + return null; + } + PresetCommentVo vo = new PresetCommentVo(); + vo.setId(entity.getId()); + vo.setContestId(entity.getContestId()); + vo.setJudgeId(entity.getJudgeId()); + vo.setContent(entity.getContent()); + vo.setScore(entity.getScore()); + vo.setSortOrder(entity.getSortOrder()); + vo.setUseCount(entity.getUseCount()); + vo.setValidState(entity.getValidState()); + vo.setCreateTime(entity.getCreateTime()); + vo.setModifyTime(entity.getModifyTime()); + return vo; + } +} diff --git a/lesingle-creation-backend/src/main/java/com/lesingle/modules/biz/review/dto/SyncPresetCommentsDto.java b/lesingle-creation-backend/src/main/java/com/lesingle/modules/biz/review/dto/SyncPresetCommentsDto.java deleted file mode 100644 index 2662e82..0000000 --- a/lesingle-creation-backend/src/main/java/com/lesingle/modules/biz/review/dto/SyncPresetCommentsDto.java +++ /dev/null @@ -1,21 +0,0 @@ -package com.lesingle.modules.biz.review.dto; - -import io.swagger.v3.oas.annotations.media.Schema; -import jakarta.validation.constraints.NotEmpty; -import jakarta.validation.constraints.NotNull; -import lombok.Data; - -import java.util.List; - -@Data -@Schema(description = "同步预设评语DTO") -public class SyncPresetCommentsDto { - - @NotNull(message = "源活动ID不能为空") - @Schema(description = "源活动ID") - private Long sourceContestId; - - @NotEmpty(message = "目标活动列表不能为空") - @Schema(description = "目标活动ID列表") - private List targetContestIds; -} diff --git a/lesingle-creation-backend/src/main/java/com/lesingle/modules/biz/review/service/IPresetCommentService.java b/lesingle-creation-backend/src/main/java/com/lesingle/modules/biz/review/service/IPresetCommentService.java index e40e37b..5d2be04 100644 --- a/lesingle-creation-backend/src/main/java/com/lesingle/modules/biz/review/service/IPresetCommentService.java +++ b/lesingle-creation-backend/src/main/java/com/lesingle/modules/biz/review/service/IPresetCommentService.java @@ -2,18 +2,18 @@ package com.lesingle.modules.biz.review.service; import com.baomidou.mybatisplus.extension.service.IService; import com.lesingle.modules.biz.review.dto.CreatePresetCommentDto; +import com.lesingle.modules.biz.review.dto.PresetCommentVo; import com.lesingle.modules.biz.review.entity.BizPresetComment; import java.util.List; -import java.util.Map; public interface IPresetCommentService extends IService { BizPresetComment createComment(CreatePresetCommentDto dto, Long judgeId); - List> findAll(Long contestId, Long judgeId); + List findAll(Long judgeId); - Map findDetail(Long id, Long judgeId); + PresetCommentVo findDetail(Long id, Long judgeId); BizPresetComment updateComment(Long id, CreatePresetCommentDto dto, Long judgeId); @@ -22,8 +22,4 @@ public interface IPresetCommentService extends IService { void batchDelete(List ids, Long judgeId); void incrementUseCount(Long id, Long judgeId); - - List> getJudgeContests(Long judgeId); - - Map syncComments(Long sourceContestId, List targetContestIds, Long judgeId); } diff --git a/lesingle-creation-backend/src/main/java/com/lesingle/modules/biz/review/service/impl/PresetCommentServiceImpl.java b/lesingle-creation-backend/src/main/java/com/lesingle/modules/biz/review/service/impl/PresetCommentServiceImpl.java index 5ec762b..b3f6abe 100644 --- a/lesingle-creation-backend/src/main/java/com/lesingle/modules/biz/review/service/impl/PresetCommentServiceImpl.java +++ b/lesingle-creation-backend/src/main/java/com/lesingle/modules/biz/review/service/impl/PresetCommentServiceImpl.java @@ -6,29 +6,20 @@ import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl; import com.lesingle.common.enums.ErrorCode; import com.lesingle.common.exception.BusinessException; import com.lesingle.modules.biz.review.dto.CreatePresetCommentDto; -import com.lesingle.modules.biz.contest.entity.BizContest; -import com.lesingle.modules.biz.contest.mapper.ContestMapper; -import com.lesingle.modules.biz.review.entity.BizContestJudge; +import com.lesingle.modules.biz.review.dto.PresetCommentVo; import com.lesingle.modules.biz.review.entity.BizPresetComment; -import com.lesingle.modules.biz.review.mapper.ContestJudgeMapper; import com.lesingle.modules.biz.review.mapper.PresetCommentMapper; import com.lesingle.modules.biz.review.service.IPresetCommentService; -import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; import org.springframework.stereotype.Service; -import java.util.*; +import java.util.List; import java.util.stream.Collectors; @Slf4j @Service -@RequiredArgsConstructor public class PresetCommentServiceImpl extends ServiceImpl implements IPresetCommentService { - private final PresetCommentMapper presetCommentMapper; - private final ContestJudgeMapper contestJudgeMapper; - private final ContestMapper contestMapper; - @Override public BizPresetComment createComment(CreatePresetCommentDto dto, Long judgeId) { log.info("创建预设评语,评委ID:{},活动ID:{}", judgeId, dto.getContestId()); @@ -47,24 +38,21 @@ public class PresetCommentServiceImpl extends ServiceImpl> findAll(Long contestId, Long judgeId) { - log.info("查询预设评语列表,活动ID:{},评委ID:{}", contestId, judgeId); + public List findAll(Long judgeId) { + log.info("查询预设评语列表(当前评委全部),评委ID:{}", judgeId); LambdaQueryWrapper wrapper = new LambdaQueryWrapper<>(); wrapper.eq(BizPresetComment::getValidState, 1); wrapper.eq(BizPresetComment::getJudgeId, judgeId); - if (contestId != null) { - wrapper.eq(BizPresetComment::getContestId, contestId); - } wrapper.orderByAsc(BizPresetComment::getSortOrder); wrapper.orderByDesc(BizPresetComment::getUseCount); - List list = presetCommentMapper.selectList(wrapper); - return list.stream().map(this::entityToMap).collect(Collectors.toList()); + List rows = list(wrapper); + return rows.stream().map(PresetCommentVo::fromEntity).collect(Collectors.toList()); } @Override - public Map findDetail(Long id, Long judgeId) { + public PresetCommentVo findDetail(Long id, Long judgeId) { log.info("查询预设评语详情,ID:{},评委ID:{}", id, judgeId); BizPresetComment entity = getById(id); @@ -74,7 +62,7 @@ public class PresetCommentServiceImpl extends ServiceImpl> getJudgeContests(Long judgeId) { - log.info("查询评委关联活动(预设评语视角),评委ID:{}", judgeId); - - LambdaQueryWrapper wrapper = new LambdaQueryWrapper<>(); - wrapper.eq(BizContestJudge::getJudgeId, judgeId); - wrapper.eq(BizContestJudge::getValidState, 1); - - List judgeRecords = contestJudgeMapper.selectList(wrapper); - - return judgeRecords.stream().map(j -> { - Map map = new LinkedHashMap<>(); - map.put("id", j.getContestId()); - // 查询活动详情获取名称和状态 - BizContest contest = contestMapper.selectById(j.getContestId()); - if (contest != null) { - map.put("contestName", contest.getContestName()); - map.put("contestState", contest.getContestState()); - map.put("status", contest.getStatus()); - } - return map; - }).collect(Collectors.toList()); - } - - @Override - public Map syncComments(Long sourceContestId, List targetContestIds, Long judgeId) { - log.info("同步预设评语,源活动ID:{},目标活动数:{},评委ID:{}", sourceContestId, targetContestIds.size(), judgeId); - - // 查询源活动的评语 - LambdaQueryWrapper sourceWrapper = new LambdaQueryWrapper<>(); - sourceWrapper.eq(BizPresetComment::getContestId, sourceContestId); - sourceWrapper.eq(BizPresetComment::getJudgeId, judgeId); - sourceWrapper.eq(BizPresetComment::getValidState, 1); - List sourceComments = presetCommentMapper.selectList(sourceWrapper); - - if (sourceComments.isEmpty()) { - throw BusinessException.of(ErrorCode.BAD_REQUEST, "源活动没有预设评语"); - } - - int created = 0; - for (Long targetContestId : targetContestIds) { - for (BizPresetComment source : sourceComments) { - BizPresetComment copy = new BizPresetComment(); - copy.setContestId(targetContestId); - copy.setJudgeId(judgeId); - copy.setContent(source.getContent()); - copy.setScore(source.getScore()); - copy.setSortOrder(source.getSortOrder()); - copy.setUseCount(0); - - save(copy); - created++; - } - } - - log.info("预设评语同步完成,新建数量:{}", created); - - Map result = new LinkedHashMap<>(); - result.put("message", "同步成功"); - result.put("count", created); - return result; - } - - // ====== 私有辅助方法 ====== - - private Map entityToMap(BizPresetComment entity) { - Map map = new LinkedHashMap<>(); - map.put("id", entity.getId()); - map.put("contestId", entity.getContestId()); - map.put("judgeId", entity.getJudgeId()); - map.put("content", entity.getContent()); - map.put("score", entity.getScore()); - map.put("sortOrder", entity.getSortOrder()); - map.put("useCount", entity.getUseCount()); - map.put("createTime", entity.getCreateTime()); - map.put("modifyTime", entity.getModifyTime()); - return map; - } } diff --git a/lesingle-creation-frontend/src/api/preset-comments.ts b/lesingle-creation-frontend/src/api/preset-comments.ts index 9ec3dd3..f700467 100644 --- a/lesingle-creation-frontend/src/api/preset-comments.ts +++ b/lesingle-creation-frontend/src/api/preset-comments.ts @@ -2,21 +2,42 @@ import request from "@/utils/request"; export interface PresetComment { id: number; - contestId: number; + /** 可能为 null(库表允许活动为空) */ + contestId: number | null; judgeId: number; content: string; score?: number; sortOrder: number; useCount: number; - validState: number; + validState?: number; creator?: number; modifier?: number; - createTime: string; - modifyTime: string; + createTime?: string; + modifyTime?: string; +} + +/** 统一列表项字段类型,避免后端 Long/字符串与表格展示不一致 */ +function normalizePresetCommentRow(raw: Record): PresetComment { + return { + id: Number(raw.id), + contestId: + raw.contestId === undefined || raw.contestId === null + ? null + : Number(raw.contestId), + judgeId: Number(raw.judgeId ?? 0), + content: String(raw.content ?? ""), + score: raw.score != null ? Number(raw.score) : undefined, + sortOrder: Number(raw.sortOrder ?? 0), + useCount: Number(raw.useCount ?? 0), + validState: raw.validState != null ? Number(raw.validState) : undefined, + createTime: raw.createTime as string | undefined, + modifyTime: raw.modifyTime as string | undefined, + }; } export interface CreatePresetCommentParams { - contestId: number; + /** 不传或为 null 表示全局模板(跨活动) */ + contestId?: number | null; content: string; score?: number; sortOrder?: number; @@ -28,29 +49,13 @@ export interface UpdatePresetCommentParams { sortOrder?: number; } -export interface SyncPresetCommentsParams { - sourceContestId: number; - targetContestIds: number[]; -} - -export interface JudgeContest { - id: number; - contestName: string; - contestState: string; - status: string; -} - -// 获取预设评语列表 -export async function getPresetCommentsList( - contestId: number -): Promise { - const response = await request.get( - "/contests/preset-comments", - { - params: { contestId }, - } +/** 列表:不带 query,按当前登录评委返回全部预设评语(与评审弹窗「选择评语」一致) */ +export async function getPresetCommentsList(): Promise { + const response = await request.get[]>( + "/contests/preset-comments" ); - return response; + const list = Array.isArray(response) ? response : []; + return list.map((row) => normalizePresetCommentRow(row)); } // 获取单个预设评语详情 @@ -98,25 +103,6 @@ export async function batchDeletePresetComments(ids: number[]): Promise { }); } -// 同步预设评语到其他活动 -export async function syncPresetComments( - data: SyncPresetCommentsParams -): Promise<{ message: string; count: number }> { - const response = await request.post( - "/contests/preset-comments/sync", - data - ); - return response; -} - -// 获取评委的活动列表 -export async function getJudgeContests(): Promise { - const response = await request.get( - "/contests/preset-comments/judge/contests" - ); - return response; -} - // 增加使用次数 export async function incrementUseCount(id: number): Promise { const response = await request.post( @@ -133,7 +119,5 @@ export const presetCommentsApi = { update: updatePresetComment, delete: deletePresetComment, batchDelete: batchDeletePresetComments, - sync: syncPresetComments, - getJudgeContests: getJudgeContests, incrementUseCount: incrementUseCount, }; diff --git a/lesingle-creation-frontend/src/views/activities/PresetComments.vue b/lesingle-creation-frontend/src/views/activities/PresetComments.vue index 58d9169..1d9f3e9 100644 --- a/lesingle-creation-frontend/src/views/activities/PresetComments.vue +++ b/lesingle-creation-frontend/src/views/activities/PresetComments.vue @@ -4,11 +4,7 @@ @@ -99,59 +88,15 @@ - - - - - - - - - - diff --git a/lesingle-creation-frontend/src/views/activities/components/ReviewWorkModal.vue b/lesingle-creation-frontend/src/views/activities/components/ReviewWorkModal.vue index 8eee4d8..8259002 100644 --- a/lesingle-creation-frontend/src/views/activities/components/ReviewWorkModal.vue +++ b/lesingle-creation-frontend/src/views/activities/components/ReviewWorkModal.vue @@ -473,19 +473,25 @@ const handlePresetSelect = (presetId: number | undefined) => { } }; +// 并发请求序号,避免快速切换作品或重复触发 watch 时旧请求覆盖新数据 +let fetchWorkDetailSeq = 0; + // 获取作品详情 const fetchWorkDetail = async () => { if (!props.workId) return; + const seq = ++fetchWorkDetailSeq; loading.value = true; try { const detail = await worksApi.getDetail(props.workId); + if (seq !== fetchWorkDetailSeq) return; workDetail.value = detail; // 获取活动的评审规则 if (props.contestId) { try { const contest = await reviewsApi.getJudgeContestDetail(props.contestId); + if (seq !== fetchWorkDetailSeq) return; if (contest.reviewRule) { // 确保 dimensions 是数组(可能是 JSON 字符串) @@ -513,18 +519,23 @@ const fetchWorkDetail = async () => { // 获取评审规则失败,使用简单评分模式 reviewRule.value = null; } + } else { + reviewRule.value = null; + } - // 单独获取预设评语,不影响评审规则 - try { - const presets = await presetCommentsApi.getList(props.contestId); - presetComments.value = presets; - } catch { - presetComments.value = []; - } + // 预设评语:不传 contestId,拉取当前评委全部模板(与活动是否带 contestId 无关) + try { + const presets = await presetCommentsApi.getList(); + if (seq !== fetchWorkDetailSeq) return; + presetComments.value = presets; + } catch { + if (seq !== fetchWorkDetailSeq) return; + presetComments.value = []; } // 获取当前评委的评分记录 const scores = await reviewsApi.getWorkScores(props.workId); + if (seq !== fetchWorkDetailSeq) return; const myScore = scores.find((s: any) => s.assignmentId === props.assignmentId); if (myScore && myScore.totalScore !== null && myScore.totalScore !== undefined) { existingScore.value = myScore; @@ -553,11 +564,16 @@ const fetchWorkDetail = async () => { resetForm(); } + if (seq !== fetchWorkDetailSeq) return; currentFileIndex.value = 0; } catch (error: any) { - message.error(error?.response?.data?.message || "获取作品详情失败"); + if (seq === fetchWorkDetailSeq) { + message.error(error?.response?.data?.message || "获取作品详情失败"); + } } finally { - loading.value = false; + if (seq === fetchWorkDetailSeq) { + loading.value = false; + } } }; @@ -696,27 +712,20 @@ const handleClose = () => { emit("update:open", false); }; -// 监听抽屉打开 +// 合并监听:避免同时设置 open=true 与 workId 时两个 watch 各调一次 fetch 造成重复请求 watch( - () => props.open, - (val) => { - if (val && props.workId) { - fetchWorkDetail(); - } else { + () => + [props.open, props.workId, props.contestId, props.assignmentId] as const, + ([open]) => { + if (!open) { workDetail.value = null; existingScore.value = null; reviewRule.value = null; presetComments.value = []; resetForm(); + return; } - } -); - -// 监听 workId 变化(切换作品时) -watch( - () => props.workId, - (val) => { - if (props.open && val) { + if (props.workId) { fetchWorkDetail(); } } diff --git a/lesingle-creation-frontend/src/views/contests/Activities.vue b/lesingle-creation-frontend/src/views/contests/Activities.vue index 42defd8..413f5f2 100644 --- a/lesingle-creation-frontend/src/views/contests/Activities.vue +++ b/lesingle-creation-frontend/src/views/contests/Activities.vue @@ -161,7 +161,7 @@ 预设评语 @@ -488,13 +488,9 @@ const handleReviewWorks = (id: number) => { router.push(`/${tenantCode}/activities/review/${id}`) } -// 预设评语 -const handlePresetComments = (id: number) => { - console.log( - "预设评审", - `/${tenantCode}/activities/preset-comments?contestId=${id}`, - ) - router.push(`/${tenantCode}/activities/preset-comments?contestId=${id}`) +// 预设评语(全局模板,与活动无绑定) +const handlePresetComments = () => { + router.push(`/${tenantCode}/activities/preset-comments`) } // 图片加载错误记录