【新增】 服务器通过websocket与学生端进行考试倒计时等交互

This commit is contained in:
dlaren
2025-08-12 17:16:06 +08:00
parent 28c75d52c5
commit f805b16052
7 changed files with 270 additions and 30 deletions

View File

@@ -185,6 +185,13 @@
<version>3.0.1</version>
</dependency>
<dependency>
<groupId>jakarta.annotation</groupId>
<artifactId>jakarta.annotation-api</artifactId>
<version>2.1.1</version>
</dependency>
</dependencies>
</project>

View File

@@ -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<Boolean> 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<Boolean> 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);
}
}

View File

@@ -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;
}

View File

@@ -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;
}

View File

@@ -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<String, ScheduledFuture<?>> 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;
}
});
}
}