考试有一个功能:如果用户把浏览器关闭了,或者电脑断点了,这些异常行为就没办法给试卷及时的打分,所有就有了一个功能叫心跳监听,后端给一个接口,第一次打开的时候就创建这个Key并设置key的过期时间,然后前端每隔多少分钟调用一个后端的这个接口,如果key过期了,。后端都还没有调用,就表示前端的浏览器等出问题了(比如断网、浏览器关闭),就把考试的分数和考试的状态等更新进数据库中
这里遇到一个大问题就是我们的系统有租户后台以及管理员后台,将key存到redis中的时候,租户后台还没开始跑,管理员后台就将数据给已经运行一遍了,所以需要加一个锁,只要其中一个运行了,另外一个就不让再跑key过期的监听逻辑处理了,不然数据会不对,叠加
/**
* 监听考试页面是否被关掉
*
* @return success/false
*/
@ApiOperation(value = " 监听考试页面是否被关掉。1min前端调用一次" + SPRINT16 + LICHUNLAN)
@GetMapping("/checkExamination")
public ResponseModel checkExamination(@RequestParam Long questionPaperInstanceId) {
//redis检测用户是否正在考试
try {
redisKeyExpiredListener.checkExaminationHeartbeat(UserUtil.getCurrentTenantId(),UserUtil.getCurrentUser().getId(), questionPaperInstanceId);
} catch (Exception ex) {
log.error("记录用户是否正在考试时,redis出错", ex);
}
return ResponseHelper.succeed();
}
设置一个10分钟到redis key
/**
* 前端每隔一定周期向后端发送播放记录。用于记录用户播放时长
*
* @param tenantId
* @param userId
* @param questionPaperInstanceId
*/
public void checkExaminationHeartbeat(Long tenantId, Long userId, Long questionPaperInstanceId) {
StringBuffer sb = new StringBuffer("Listener")
.append(Constant.COLON)
.append(tenantId)
.append(Constant.COLON)
.append(userId)
.append(Constant.COLON)
.append(questionPaperInstanceId);
String valForUserExam = sb.toString();
String keyForUserExam = sb.append(Constant.COLON).append(RedisKeyExpiredListener.keyForExam).toString();
ValueOperations<String, String> opsForValue = redisTemplate.opsForValue();
String preVal = opsForValue.get(keyForUserExam);
log.info("检测考试心跳:key是{},value是:{}",keyForUserExam,valForUserExam);
//将这个值设置到redis缓存中
if (ComUtil.isEmpty(preVal)) {
opsForValue.set(keyForUserExam,valForUserExam,Constant.GenericNumber.NUMBER_TEN,TimeUnit.MINUTES);
}else{
//设置过期时间为10min
redisTemplate.expire(keyForUserExam,Constant.GenericNumber.NUMBER_TEN,TimeUnit.MINUTES);
}
}
redis监听过期key,好作自己的逻辑操作
/**
* 试卷考试心跳
*/
@Slf4j
@Service
public class RedisKeyExpiredListener extends KeyExpirationEventMessageListener {
private static final String keyForExam= "keyForExam";
@Autowired
public RedisKeyExpiredListener(RedisMessageListenerContainer redisMessageListenerContainer) {
super(redisMessageListenerContainer);
}
/**
* 接收过期消息
*
* @param message
* @param pattern
*/
@Override
public void onMessage(Message message, byte[] pattern) {
String channel = new String(message.getChannel(), StandardCharsets.UTF_8);
String patternString = new String(pattern, StandardCharsets.UTF_8);
//学习时长过期的key末尾
String studyDurationsSuffix = Constant.COLON + keyForDuration;
String key = new String(message.getBody(), StandardCharsets.UTF_8);
log.info("app:{}:callbackKey:{}",springApplicationName,key);
log.info("pattern:{}:channel:{}",patternString,channel);
//处理在线考试过期回调的问题
if(key.contains(keyForExam)){
processUserOffLineExam(key);
}
}
/**
* 处理考试设定时长过期的问题,过期了就需要设置分数为0,状态为4
*/
@Transactional(rollbackFor = Exception.class)
void processUserOffLineExam(String key){
log.info("过期的考试Key是{}",key);
String[] keyArray = key.split(Constant.COLON);
String tenantId = keyArray[keyArray.length - 4];
String userId = keyArray[keyArray.length - 3];
String questionPaperInstanceId= keyArray[keyArray.length - 2];
Boolean lock = redisTemplate.opsForValue().setIfAbsent(Constant.Redis.QUESTION_PAPER_EXAM+questionPaperInstanceId, questionPaperInstanceId, Constant.GenericNumber.NUMBER_ONE, TimeUnit.MINUTES);
log.info("租户{}下的用户{}的试卷实例ID{}为,枷锁的标志是{}:",tenantId,userId,questionPaperInstanceId,lock);
if(lock){
//不存在表示已经过期了,需要将考试的状态设置为意外结束,并设置分数为0
QuestionPaperInstance questionPaperInstance = questionPaperInstanceService.getById(Long.valueOf(questionPaperInstanceId));
//第一次考试开始
boolean isFirstStart = questionPaperInstance.getStatus().equals(Constant.QuestionPaperInstanceStatus.start.getCode());
//是否设置了补考
boolean isSettingMakeup = ComUtil.isNotEmpty(questionPaperInstance.getMakeupExamEndTime());
//补考考试开始
boolean isMakeupStart = questionPaperInstance.getStatus().equals(Constant.QuestionPaperInstanceStatus.makeUp_start.getCode());
if(ComUtil.isNotEmpty(questionPaperInstance) && (isFirstStart||isMakeupStart)){
//考试时长
LocalDateTime now = LocalDateTime.now();
Duration durationObj = Duration.between(questionPaperInstance.getStartTime(), now);
Integer duration = Math.toIntExact(durationObj.toMinutes());
questionPaperInstance.setDuration(duration);
//第一次考试异常结束,不需要补考,分数计0,状态异常结束
if(isFirstStart && !isSettingMakeup){
questionPaperInstance.setScore(Constant.GenericNumber.NUMBER_ZERO);
questionPaperInstance.setStatus(Constant.QuestionPaperInstanceStatus.exception_finish.getCode());
}
//第一次考试异常结束,需要补考就状态设置为补考未开始,分数计0,并发通知
if(isFirstStart && isSettingMakeup){
questionPaperInstance.setScore(Constant.GenericNumber.NUMBER_ZERO);
questionPaperInstanceService.sendNotice(true,questionPaperInstance,Constant.GenericNumber.NUMBER_ZERO,Long.valueOf(userId));
questionPaperInstance.setStatus(Constant.QuestionPaperInstanceStatus.makeUp_init.getCode());
}
//补考开始异常结束
if(isMakeupStart){
questionPaperInstance.setMakeupExamTime(LocalDateTime.now());
questionPaperInstance.setMakeupExamScore(Constant.GenericNumber.NUMBER_ZERO);
questionPaperInstance.setStatus(Constant.QuestionPaperInstanceStatus.exception_finish.getCode());
}
questionPaperInstance.setEndTime(now);
//更新分数和状态
questionPaperInstanceService.updateById(questionPaperInstance);
try {
//调用任务逻辑
if (questionPaperInstance.getOrigin() == 0) {
//更新考试时长和分数
questionPaperStatisticsService.updateByPaperInfo(questionPaperInstance);
//迭代19发现的问题,只有第一次考试的时候才去更新试卷的进度,第二次补考的时候不更新
log.info("试卷心跳的几个状态值是:isFirstStart:{}",isFirstStart);
if(isFirstStart){
studyTaskService.completePaper(questionPaperInstance.getStudyTaskId(), questionPaperInstance.getQuestionPaperId(), questionPaperInstance.getUserId());
}
}
} catch (Exception e) {
log.info("试卷心跳过期完成任务异常{}",e.getMessage());
}
}
}
}
}
网友评论