From f805b1605250e73d7f44fc895cefe0f64e34d9f2 Mon Sep 17 00:00:00 2001 From: dlaren Date: Tue, 12 Aug 2025 17:16:06 +0800 Subject: [PATCH] =?UTF-8?q?=E3=80=90=E6=96=B0=E5=A2=9E=E3=80=91=20?= =?UTF-8?q?=E6=9C=8D=E5=8A=A1=E5=99=A8=E9=80=9A=E8=BF=87websocket=E4=B8=8E?= =?UTF-8?q?=E5=AD=A6=E7=94=9F=E7=AB=AF=E8=BF=9B=E8=A1=8C=E8=80=83=E8=AF=95?= =?UTF-8?q?=E5=80=92=E8=AE=A1=E6=97=B6=E7=AD=89=E4=BA=A4=E4=BA=92?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../AbstractWebSocketMessageSender.java | 8 +- .../paper/EducationPaperParamController.java | 43 +++--- .../exam-module-judgement-biz/pom.xml | 7 + .../admin/autoTools/AutoToolsController.java | 80 ++++++++++- .../admin/autoTools/vo/StuInTheExam.java | 18 +++ .../admin/autoTools/vo/StuTheExamInfo.java | 16 +++ .../module/judgement/service/TaskManager.java | 128 ++++++++++++++++++ 7 files changed, 270 insertions(+), 30 deletions(-) create mode 100644 exam-module-judgement/exam-module-judgement-biz/src/main/java/pc/exam/pp/module/judgement/controller/admin/autoTools/vo/StuInTheExam.java create mode 100644 exam-module-judgement/exam-module-judgement-biz/src/main/java/pc/exam/pp/module/judgement/controller/admin/autoTools/vo/StuTheExamInfo.java create mode 100644 exam-module-judgement/exam-module-judgement-biz/src/main/java/pc/exam/pp/module/judgement/service/TaskManager.java diff --git a/exam-framework/exam-spring-boot-starter-websocket/src/main/java/pc/exam/pp/framework/websocket/core/sender/AbstractWebSocketMessageSender.java b/exam-framework/exam-spring-boot-starter-websocket/src/main/java/pc/exam/pp/framework/websocket/core/sender/AbstractWebSocketMessageSender.java index 17449571..f3b42a20 100644 --- a/exam-framework/exam-spring-boot-starter-websocket/src/main/java/pc/exam/pp/framework/websocket/core/sender/AbstractWebSocketMessageSender.java +++ b/exam-framework/exam-spring-boot-starter-websocket/src/main/java/pc/exam/pp/framework/websocket/core/sender/AbstractWebSocketMessageSender.java @@ -86,19 +86,19 @@ public abstract class AbstractWebSocketMessageSender implements WebSocketMessage sessions.forEach(session -> { // 1. 各种校验,保证 Session 可以被发送 if (session == null) { - log.error("[doSend][session 为空, message({})]", message); + // log.error("[doSend][session 为空, message({})]", message); return; } if (!session.isOpen()) { - log.error("[doSend][session({}) 已关闭, message({})]", session.getId(), message); + // log.error("[doSend][session({}) 已关闭, message({})]", session.getId(), message); return; } // 2. 执行发送 try { session.sendMessage(new TextMessage(payload)); - log.info("[doSend][session({}) 发送消息成功,message({})]", session.getId(), message); + // log.info("[doSend][session({}) 发送消息成功,message({})]", session.getId(), message); } catch (IOException ex) { - log.error("[doSend][session({}) 发送消息失败,message({})]", session.getId(), message, ex); + // log.error("[doSend][session({}) 发送消息失败,message({})]", session.getId(), message, ex); } }); } diff --git a/exam-module-exam/exam-module-exam-biz/src/main/java/pc/exam/pp/module/exam/controller/admin/paper/EducationPaperParamController.java b/exam-module-exam/exam-module-exam-biz/src/main/java/pc/exam/pp/module/exam/controller/admin/paper/EducationPaperParamController.java index 8d1f1f08..36c9a5ec 100644 --- a/exam-module-exam/exam-module-exam-biz/src/main/java/pc/exam/pp/module/exam/controller/admin/paper/EducationPaperParamController.java +++ b/exam-module-exam/exam-module-exam-biz/src/main/java/pc/exam/pp/module/exam/controller/admin/paper/EducationPaperParamController.java @@ -9,7 +9,9 @@ import org.springframework.web.bind.annotation.*; import pc.exam.pp.framework.common.pojo.CommonResult; import pc.exam.pp.module.exam.dal.dataobject.EducationPaperParam; import pc.exam.pp.module.exam.service.paper.IEducationPaperParamService; + import static pc.exam.pp.module.infra.enums.ErrorCodeConstants.DEMO03_PAPER_SESSION_EXISTS; + /** * 通用参数Controller * @@ -19,23 +21,18 @@ import static pc.exam.pp.module.infra.enums.ErrorCodeConstants.DEMO03_PAPER_SESS @Tag(name = "管理后台 - 试卷通用参数") @RestController @RequestMapping("/exam/param") -public class EducationPaperParamController -{ +public class EducationPaperParamController { @Autowired private IEducationPaperParamService educationPaperParamService; - - - /** * 获取通用参数详细信息 */ @Operation(summary = "获取通用参数详细信息") @GetMapping(value = "/getInfo") - public CommonResult getInfo(@RequestParam("taskId") String taskId) - { - EducationPaperParam educationPaperParam= educationPaperParamService.selectEducationPaperParamByTaskId(taskId); + public CommonResult getInfo(@RequestParam("taskId") String taskId) { + EducationPaperParam educationPaperParam = educationPaperParamService.selectEducationPaperParamByTaskId(taskId); return CommonResult.success(educationPaperParam); } @@ -44,8 +41,7 @@ public class EducationPaperParamController */ @Operation(summary = "新增通用参数") @PostMapping - public CommonResult add(@RequestBody EducationPaperParam educationPaperParam) - { + public CommonResult add(@RequestBody EducationPaperParam educationPaperParam) { return CommonResult.success(educationPaperParamService.insertEducationPaperParam(educationPaperParam)); } @@ -54,8 +50,7 @@ public class EducationPaperParamController */ @Operation(summary = "修改通用参数") @PutMapping - public CommonResult edit(@RequestBody EducationPaperParam educationPaperParam) - { + public CommonResult edit(@RequestBody EducationPaperParam educationPaperParam) { return CommonResult.success(educationPaperParamService.updateEducationPaperParam(educationPaperParam)); } @@ -64,43 +59,45 @@ public class EducationPaperParamController */ @Operation(summary = "删除通用参数") @DeleteMapping("/{paramIds}") - public CommonResult remove(@PathVariable String[] paramIds) - { + public CommonResult remove(@PathVariable String[] paramIds) { return CommonResult.success(educationPaperParamService.deleteEducationPaperParamByParamIds(paramIds)); } /** * 步骤条 (考场) + * * @param taskId * @return */ @Operation(summary = "步骤条 (考场)警告") - @GetMapping( "/check-can-enter-step4/{taskId}") - public CommonResult ckeckSession(@PathVariable("taskId") String taskId){ + @GetMapping("/check-can-enter-step4/{taskId}") + public CommonResult ckeckSession(@PathVariable("taskId") String taskId) { EducationPaperParam educationPaperParam = educationPaperParamService.selectEducationPaperParamByTaskId(taskId); - if (educationPaperParam==null){ + if (educationPaperParam == null) { return CommonResult.error(DEMO03_PAPER_SESSION_EXISTS); } String isSession = educationPaperParam.getIsSession(); - if ("1".equals(isSession)){ + if ("1".equals(isSession)) { return CommonResult.error(DEMO03_PAPER_SESSION_EXISTS); } - return CommonResult.success("200"); + return CommonResult.success("200"); } + /** * 步骤条 (考场) + * * @param taskId * @return */ @Operation(summary = "步骤条 (考场)无警告") - @GetMapping( "/check-can-enter-step4NoMsg/{taskId}") - public CommonResult ckeckSessionNoMsg(@PathVariable("taskId") String taskId){ + @GetMapping("/check-can-enter-step4NoMsg/{taskId}") + public CommonResult ckeckSessionNoMsg(@PathVariable("taskId") String taskId) { EducationPaperParam educationPaperParam = educationPaperParamService.selectEducationPaperParamByTaskId(taskId); - if (educationPaperParam==null){ + if (educationPaperParam == null) { return CommonResult.success("1_001_401_001"); } String isSession = educationPaperParam.getIsSession(); - if ("1".equals(isSession)){ + if ("1".equals(isSession)) { return CommonResult.success("1_001_401_001"); } return CommonResult.success("200"); diff --git a/exam-module-judgement/exam-module-judgement-biz/pom.xml b/exam-module-judgement/exam-module-judgement-biz/pom.xml index fd13f8ac..c11c1b7d 100644 --- a/exam-module-judgement/exam-module-judgement-biz/pom.xml +++ b/exam-module-judgement/exam-module-judgement-biz/pom.xml @@ -185,6 +185,13 @@ 3.0.1 + + jakarta.annotation + jakarta.annotation-api + 2.1.1 + + + diff --git a/exam-module-judgement/exam-module-judgement-biz/src/main/java/pc/exam/pp/module/judgement/controller/admin/autoTools/AutoToolsController.java b/exam-module-judgement/exam-module-judgement-biz/src/main/java/pc/exam/pp/module/judgement/controller/admin/autoTools/AutoToolsController.java index d7bd9907..82a77a21 100644 --- a/exam-module-judgement/exam-module-judgement-biz/src/main/java/pc/exam/pp/module/judgement/controller/admin/autoTools/AutoToolsController.java +++ b/exam-module-judgement/exam-module-judgement-biz/src/main/java/pc/exam/pp/module/judgement/controller/admin/autoTools/AutoToolsController.java @@ -4,23 +4,34 @@ package pc.exam.pp.module.judgement.controller.admin.autoTools; import io.swagger.v3.oas.annotations.Operation; import io.swagger.v3.oas.annotations.tags.Tag; import jakarta.annotation.Resource; +import jakarta.servlet.http.HttpServletRequest; +import org.springframework.beans.factory.annotation.Autowired; import org.springframework.validation.annotation.Validated; -import org.springframework.web.bind.annotation.GetMapping; -import org.springframework.web.bind.annotation.RequestMapping; -import org.springframework.web.bind.annotation.RestController; +import org.springframework.web.bind.annotation.*; import pc.exam.pp.framework.common.pojo.CommonResult; +import pc.exam.pp.framework.common.util.servlet.ServletUtils; +import pc.exam.pp.framework.security.config.SecurityProperties; +import pc.exam.pp.framework.security.core.util.SecurityFrameworkUtils; +import pc.exam.pp.module.exam.dal.dataobject.EducationPaperParam; +import pc.exam.pp.module.exam.dal.dataobject.EducationPaperTask; import pc.exam.pp.module.exam.dal.dataobject.ExamQuestion; import pc.exam.pp.module.exam.dal.dataobject.student.StuPaperScoreDO; import pc.exam.pp.module.exam.dal.mysql.paper.EducationPaperQuMapper; import pc.exam.pp.module.exam.dal.mysql.question.ExamQuestionMapper; import pc.exam.pp.module.exam.dal.mysql.student.StuScoreVo; +import pc.exam.pp.module.exam.service.paper.IEducationPaperParamService; +import pc.exam.pp.module.exam.service.paper.IEducationPaperTaskService; import pc.exam.pp.module.exam.service.stuPaperScore.StuPaperScoreService; +import pc.exam.pp.module.judgement.controller.admin.autoTools.vo.StuInTheExam; import pc.exam.pp.module.judgement.controller.admin.autoTools.vo.StuPaperReqVo; import pc.exam.pp.module.judgement.controller.admin.autoTools.vo.StuPaperScoreInfoVo; +import pc.exam.pp.module.judgement.controller.admin.autoTools.vo.StuTheExamInfo; +import pc.exam.pp.module.judgement.service.TaskManager; import java.util.List; import java.util.Map; import java.util.Objects; +import java.util.concurrent.atomic.AtomicInteger; import java.util.function.Function; import java.util.stream.Collectors; @@ -38,6 +49,14 @@ public class AutoToolsController { EducationPaperQuMapper educationPaperQuMapper; @Resource ExamQuestionMapper examQuestionMapper; + @Resource + TaskManager taskManager; + @Resource + SecurityProperties securityProperties; + @Autowired + IEducationPaperTaskService educationPaperTaskService; + @Autowired + IEducationPaperParamService educationPaperParamService; @GetMapping("/getStuScoreInfo") @Operation(summary = "通过学生ID、试卷ID获取") @@ -70,4 +89,59 @@ public class AutoToolsController { stuPaperScoreInfoVos.setPaperAnalysis(judgementStr); return CommonResult.success(stuPaperScoreInfoVos); } + + /** + * 开始考试 通过websocket进行传输时间 + * @param stuInTheExam 信息 + * @return true + */ + @PostMapping("/startExam") + public CommonResult startExam(@RequestBody StuInTheExam stuInTheExam) { + HttpServletRequest request = ServletUtils.getRequest(); + String token = SecurityFrameworkUtils.obtainAuthorization(request, + securityProperties.getTokenHeader(), securityProperties.getTokenParameter()); + // 查找对应的task + EducationPaperParam educationPaperParam = educationPaperParamService.selectEducationPaperParamByTaskId(stuInTheExam.getTaskId()); + // 定时上传文件时间 + String time = educationPaperParam.getUploadTime(); + // 将分钟继续转换成秒 + stuInTheExam.setTimes(Integer.parseInt(time) * 60); + // 倒计时 + AtomicInteger countdown = new AtomicInteger(stuInTheExam.getStartTimes()); + // 创建初始返回数据 + StuTheExamInfo stuTheExamInfo = new StuTheExamInfo(); + // 返回数据-剩余时间 + stuTheExamInfo.setTime(formatLongDuration(countdown.get())); + // 返回数据-上传文件状态 0:上传;1:不上传 + stuTheExamInfo.setUpload(1); + // 返回数据-上传文件状态 0:结束;1:不结束 + stuTheExamInfo.setEndStatus(1); + // 返回数据-网络状态 + stuTheExamInfo.setNetwork("强"); + // 创建对应的线程池 + taskManager.startTask(stuInTheExam, stuTheExamInfo, token, countdown, new AtomicInteger(0)); + return CommonResult.success(true); + } + + /** + * 停止考试 + * @return true + */ + @GetMapping("/stopExam") + public CommonResult stopExam() { + HttpServletRequest request = ServletUtils.getRequest(); + String token = SecurityFrameworkUtils.obtainAuthorization(request, + securityProperties.getTokenHeader(), securityProperties.getTokenParameter()); + // 删除对应的线程池 + taskManager.stopTask(token); + return CommonResult.success(true); + } + + public static String formatLongDuration(int totalSeconds) { + int hours = totalSeconds / 3600; + int minutes = (totalSeconds % 3600) / 60; + int seconds = totalSeconds % 60; + + return String.format("%d:%02d:%02d", hours, minutes, seconds); + } } diff --git a/exam-module-judgement/exam-module-judgement-biz/src/main/java/pc/exam/pp/module/judgement/controller/admin/autoTools/vo/StuInTheExam.java b/exam-module-judgement/exam-module-judgement-biz/src/main/java/pc/exam/pp/module/judgement/controller/admin/autoTools/vo/StuInTheExam.java new file mode 100644 index 00000000..4daf22b2 --- /dev/null +++ b/exam-module-judgement/exam-module-judgement-biz/src/main/java/pc/exam/pp/module/judgement/controller/admin/autoTools/vo/StuInTheExam.java @@ -0,0 +1,18 @@ +package pc.exam.pp.module.judgement.controller.admin.autoTools.vo; + + +import lombok.Data; + +@Data +public class StuInTheExam { + // 学生ID + // private String stuId; + // 倒计时开始时间 单位秒 + private int startTimes; + // 定时上传文件时间 + private int times; + // 任务ID + private String taskId; + // 延迟时间 min + private Integer delayTime; +} diff --git a/exam-module-judgement/exam-module-judgement-biz/src/main/java/pc/exam/pp/module/judgement/controller/admin/autoTools/vo/StuTheExamInfo.java b/exam-module-judgement/exam-module-judgement-biz/src/main/java/pc/exam/pp/module/judgement/controller/admin/autoTools/vo/StuTheExamInfo.java new file mode 100644 index 00000000..744ae17d --- /dev/null +++ b/exam-module-judgement/exam-module-judgement-biz/src/main/java/pc/exam/pp/module/judgement/controller/admin/autoTools/vo/StuTheExamInfo.java @@ -0,0 +1,16 @@ +package pc.exam.pp.module.judgement.controller.admin.autoTools.vo; + + +import lombok.Data; + +@Data +public class StuTheExamInfo { + // 考试时间 + private String time; + // 网络状态 + private String network; + // 上传文件状态 + private Integer upload; + // 是否结束考试 + private Integer endStatus; +} diff --git a/exam-module-judgement/exam-module-judgement-biz/src/main/java/pc/exam/pp/module/judgement/service/TaskManager.java b/exam-module-judgement/exam-module-judgement-biz/src/main/java/pc/exam/pp/module/judgement/service/TaskManager.java new file mode 100644 index 00000000..b22f33b3 --- /dev/null +++ b/exam-module-judgement/exam-module-judgement-biz/src/main/java/pc/exam/pp/module/judgement/service/TaskManager.java @@ -0,0 +1,128 @@ +package pc.exam.pp.module.judgement.service; + +import jakarta.annotation.PreDestroy; +import jakarta.annotation.Resource; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.stereotype.Service; +import pc.exam.pp.framework.common.enums.UserTypeEnum; +import pc.exam.pp.framework.excel.core.convert.JsonConvert; +import pc.exam.pp.module.infra.api.websocket.WebSocketSenderApi; +import pc.exam.pp.module.judgement.controller.admin.autoTools.vo.StuInTheExam; +import pc.exam.pp.module.judgement.controller.admin.autoTools.vo.StuTheExamInfo; + +import java.util.Map; +import java.util.concurrent.*; +import java.util.concurrent.atomic.AtomicInteger; + +@Slf4j +@Service +@RequiredArgsConstructor +public class TaskManager { + + private final WebSocketSenderApi webSocketSenderApi; // 构造器注入,确保不是 null + private final ScheduledExecutorService scheduler; // 从配置类注入 + private final Map> tasks = new ConcurrentHashMap<>(); + + /** 开始任务(每秒执行一次) */ + public void startTask(StuInTheExam stuInTheExam, StuTheExamInfo stuTheExamInfo, String token, AtomicInteger countdown, AtomicInteger counter) { + // 已存在就不重复启动 + tasks.computeIfAbsent(token, k -> + scheduler.scheduleAtFixedRate(safe(() -> { + // 每秒 + 1 + int current = counter.incrementAndGet(); + // 获取当前值并递减 + int remaining = countdown.decrementAndGet(); + log.debug("任务 {} tick at {}", token, System.currentTimeMillis()); + + if (current == stuInTheExam.getTimes()) { + // 提示学生端进行文件的上传 + stuTheExamInfo.setUpload(0); + // 重新进行计时 + counter.set(0); + } + // 如果计数减到0,取消任务 + if (remaining <= 0) { + ScheduledFuture future = tasks.get(token); + if (future != null) { + future.cancel(false); + tasks.remove(token); + // 发送最后一条消息 + stuTheExamInfo.setEndStatus(0); + log.info("任务 {} 已完成并取消", token); + } + } + // 时间转变 + String time = formatLongDuration(remaining); + stuTheExamInfo.setTime(time); + webSocketSenderApi.sendObject(UserTypeEnum.ADMIN.getValue(), "InTheExam", stuTheExamInfo); + }), 0, 1, TimeUnit.SECONDS) + ); + log.info("任务 {} 已启动", token); + } + + /** 结束任务 */ + public void stopTask(String token) { + ScheduledFuture f = tasks.remove(token); + if (f != null) { + f.cancel(false); + log.info("任务 {} 已停止", token); + } else { + log.warn("任务 {} 不存在", token); + } + } + + /** 全部停止(可选) */ + public void stopAll() { + tasks.forEach((k, f) -> f.cancel(false)); + tasks.clear(); + log.info("所有任务已停止"); + } + + /** 应用关闭时收尾 */ + @PreDestroy + public void onDestroy() { + stopAll(); + scheduler.shutdownNow(); + } + + /** 包装任务,防止异常导致定时器中止 */ + private Runnable safe(Runnable r) { + return () -> { + try { + r.run(); + } catch (Throwable t) { + log.error("定时任务执行异常:{}", t.getMessage(), t); + } + }; + } + + public static String formatLongDuration(int totalSeconds) { + int hours = totalSeconds / 3600; + int minutes = (totalSeconds % 3600) / 60; + int seconds = totalSeconds % 60; + + return String.format("%d:%02d:%02d", hours, minutes, seconds); + } +} + +/** 线程池配置:单独放一个@Configuration */ +@Configuration +class TaskSchedulerConfig { + @Bean + public ScheduledExecutorService scheduledExecutorService() { + // 合理的线程数即可;自定义线程名方便排查 + return Executors.newScheduledThreadPool(8, new ThreadFactory() { + private final ThreadFactory df = Executors.defaultThreadFactory(); + private final AtomicInteger idx = new AtomicInteger(1); + @Override public Thread newThread(Runnable r) { + Thread t = df.newThread(r); + t.setName("ws-task-" + idx.getAndIncrement()); + t.setDaemon(true); + return t; + } + }); + } +}