【新增】 服务器通过websocket与学生端进行考试倒计时等交互
This commit is contained in:
@@ -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);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
@@ -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");
|
||||
|
@@ -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>
|
||||
|
@@ -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);
|
||||
}
|
||||
}
|
||||
|
@@ -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;
|
||||
}
|
@@ -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;
|
||||
}
|
@@ -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;
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
Reference in New Issue
Block a user