package com.web.api.config; import com.alibaba.fastjson.JSONException; import com.google.common.collect.ImmutableMap; import com.web.api.dao.AnswerDao; import com.web.api.exception.ErrorCode; import com.web.api.exception.SgException; import com.web.api.model.dto.AnswerDto; import com.web.api.model.po.UserInfo; import com.web.api.service.mail.MailServiceImpl; import com.web.api.utils.ContextUtils; import lombok.extern.slf4j.Slf4j; import org.springframework.aop.interceptor.AsyncUncaughtExceptionHandler; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.context.annotation.Configuration; import org.springframework.dao.DataAccessException; import org.springframework.scheduling.annotation.AsyncConfigurer; import org.thymeleaf.context.Context; import org.thymeleaf.spring5.SpringTemplateEngine; import javax.annotation.Resource; import java.io.IOException; import java.lang.reflect.Method; import java.text.DecimalFormat; import java.time.Duration; import java.time.LocalDateTime; import java.time.format.DateTimeFormatter; import java.util.Arrays; import java.util.HashSet; import java.util.Set; /** * @author 王玉鹏 * @version 1.0 * @className AsyncExceptionConfig * @description TODO * @date 2020/7/12 21:42 */ @Configuration @Slf4j public class AsyncExceptionConfig implements AsyncConfigurer { @Resource private AnswerDao answerDao; @Autowired private MailServiceImpl mailService; @Autowired private SpringTemplateEngine templateEngine; private static final Set> BUSINESS_EXCEPTIONS = new HashSet>() {{ add(SgException.class); add(IOException.class); add(NullPointerException.class); add(JSONException.class); }}; private static final DateTimeFormatter DATE_TIME_FORMATTER = DateTimeFormatter.ofPattern("yyyy年MM月dd日 HH时mm分"); private static final DecimalFormat DECIMAL_FORMAT = new DecimalFormat("######0.00"); private static final String SERVER_IP = "http://42.192.203.166"; private static final String[] DEFAULT_RECIPIENTS = {"251664727@qq.com", "632062365@qq.com"}; @Override public AsyncUncaughtExceptionHandler getAsyncUncaughtExceptionHandler() { return new SpringAsyncExceptionHandler(); } class SpringAsyncExceptionHandler implements AsyncUncaughtExceptionHandler { @Override public void handleUncaughtException(Throwable throwable, Method method, Object... params) { try { if (!BUSINESS_EXCEPTIONS.contains(throwable.getClass())) { log.error("------非业务类异常,此处不做处理--------: ", throwable); return; } validateParams(params); AnswerDto answerDto = (AnswerDto) params[2]; Double startScore = (Double) params[3]; //组装用户信息 UserInfo userInfo = processUserInfo(answerDto, startScore, throwable); //更新数据库 int updateCount = handleDatabaseOperation(userInfo); //发送邮件 if (updateCount > 0) { sendNotificationEmail(answerDto, userInfo, startScore, throwable, updateCount); } cleanUpResources(answerDto, throwable); } catch (Exception e) { log.error("异常处理过程出错: ", e); } finally { log.error("异步任务异常详情: ", throwable); } } private void validateParams(Object[] params) { log.info("业务参数:{}", params); if (params.length < 4) { throw new IllegalArgumentException("参数数量不足,需要4个参数"); } if (!(params[2] instanceof AnswerDto)) { throw new IllegalArgumentException("第三个参数应为AnswerDto类型"); } if (!(params[3] instanceof Double)) { throw new IllegalArgumentException("第四个参数应为Double类型"); } } private UserInfo processUserInfo(AnswerDto answerDto, Double startScore, Throwable throwable) { UserInfo userInfo = ContextUtils.getUserInfo(answerDto.getJobNo()); userInfo.setEndTime(LocalDateTime.now().format(DATE_TIME_FORMATTER)); userInfo.setAnswerStatus("已结束"); if (userInfo.getFinalScore() != null) { double addScore = userInfo.getFinalScore() - startScore; userInfo.setAddScores(DECIMAL_FORMAT.format(addScore)); } else { userInfo.setAddScores("本次答题异常终止,无法计算分数"); } if (throwable instanceof SgException ) { userInfo.setErrMsg(throwable.getMessage()); }else { userInfo.setErrMsg("本次答题异常终止"); } handleBatchOperation(answerDto, userInfo); return userInfo; } /*** * 关闭答题 */ private void handleBatchOperation(AnswerDto answerDto, UserInfo userInfo) { if (answerDto.getBatchFlag()) { ContextUtils.setBatchUserList(answerDto.getJobNo(), userInfo); } ContextUtils.setOnOff(answerDto.getJobNo(), false); } private int handleDatabaseOperation(UserInfo userInfo) { try { if (isValidAddScore(userInfo.getAddScores())) { log.info("更新答题记录:{}", userInfo); int updateCount = answerDao.updatePassRecord(userInfo); if (updateCount == 0) { log.error("数据库更新失败:{}", userInfo); } return updateCount; } else { answerDao.deletePassRecord(userInfo.getId()); log.warn("无效分数,删除记录:{}", userInfo.getId()); return 0; } } catch (DataAccessException e) { log.error("数据库操作异常:{}", e.getMessage()); return 0; } } private boolean isValidAddScore(String addScore) { try { return Double.parseDouble(addScore) > 0.0; } catch (NumberFormatException e) { return false; } } private void sendNotificationEmail(AnswerDto answerDto, UserInfo userInfo, Double startScore, Throwable throwable, int updateCount) { try { String[] recipients = determineRecipients(answerDto); //收件人邮箱 String subject = buildEmailSubject(throwable, userInfo); //邮件主题 String content = buildEmailContent(answerDto, userInfo, startScore, throwable); //邮件内容 if (updateCount > 0 && !userInfo.getAddScores().contains("异常终止")) { mailService.sendSimpleMail(recipients, subject, content); } } catch (Exception e) { log.error("邮件发送失败: ", e); } } private String[] determineRecipients(AnswerDto answerDto) { if (answerDto.getBatchFlag() && ContextUtils.getBatchUserList().size() > 10) { return new String[]{"251664727@qq.com"}; } return DEFAULT_RECIPIENTS.clone(); } private String buildEmailSubject(Throwable throwable, UserInfo userInfo) { return (throwable instanceof SgException) ? throwable.getMessage() : userInfo.getUserName() + "【" + userInfo.getJobNo() + "】答题异常结束"; } public String buildEmailContent(AnswerDto answer, UserInfo user, Double startScore, Throwable ex) { Context ctx = new Context(); // 用户信息 ctx.setVariable("user", ImmutableMap.of( "userName", user.getUserName(), "jobNo", user.getJobNo(), "addScores", user.getAddScores(), "finalScore", user.getFinalScore() )); // 答题信息 ctx.setVariable("answer", ImmutableMap.of( "batchFlag", answer.getBatchFlag(), "jobTypeName", answer.getJobTypeName() )); // 分数卡片数据 ctx.setVariable("scores", Arrays.asList( ImmutableMap.of("type", "初始分数", "value", startScore, "color", "#2196F3"), ImmutableMap.of("type", "目标分数", "value", user.getTargetScore(), "color", "#FF9800") )); // 时间信息 ctx.setVariable("times", Arrays.asList( ImmutableMap.of("label", "开始", "value", user.getStartTime()), ImmutableMap.of("label", "结束", "value", user.getEndTime()), ImmutableMap.of("label", "用时", "value", calculateElapsedTime(user)) )); // 异常信息 if (ex != null) { ctx.setVariable("exception", ImmutableMap.of( "message", ex.getMessage() )); } // 服务器IP ctx.setVariable("serverIp", SERVER_IP); return templateEngine.process("answer-report", ctx); } /** * 计算答题时常 */ private String calculateElapsedTime(UserInfo userInfo) { try { LocalDateTime start = LocalDateTime.parse(userInfo.getStartTime(), DATE_TIME_FORMATTER); LocalDateTime end = LocalDateTime.parse(userInfo.getEndTime(), DATE_TIME_FORMATTER); Duration duration = Duration.between(start, end); return String.format("%d天%d时%d分", duration.toDays(), duration.toHours() % 24, duration.toMinutes() % 60); } catch (Exception e) { return "时间计算错误"; } } /** * 非手动结束情况下清楚用户缓存信息 */ private void cleanUpResources(AnswerDto answerDto, Throwable throwable) { if (!isManualStop(throwable)) { ContextUtils.removeUserCookies(answerDto.getJobNo()); ContextUtils.removeUserInfo(answerDto.getJobNo()); log.info("已清理用户资源:{}", answerDto.getJobNo()); } } private boolean isManualStop(Throwable throwable) { return throwable instanceof SgException && ((SgException) throwable).getCode() == ErrorCode.STOP_ANSWER.getCode(); } /* private String buildEmailContent(AnswerDto answerDto, UserInfo userInfo, Double startScore, Throwable throwable) { String elapsedTime = calculateElapsedTime(userInfo); // 增强的响应式样式 String mediaQuery = "@media only screen and (max-width:480px) { " + ".mobile-adjust { padding:12px 15px !important; } " + ".section-title { font-size:18px !important; margin-bottom:8px !important; } " + ".info-card { padding:12px !important; margin-bottom:16px !important; } " + ".score-container { gap:10px !important; margin-bottom:16px !important; } " + ".score-box { padding:12px !important; margin-bottom:12px !important; } " + ".final-score { padding:16px !important; margin:12px 0 !important; } " + ".time-record td { padding:4px 0 !important; } " + ".system-alert { margin:16px 0 !important; padding:12px !important; } " + ".action-button { padding:12px 24px !important; font-size:14px !important; } " + "}"; return String.format("" + "" + "" + "" + "" + "" + "" + "" + "
" // 头部信息 + "
" + "

✅ 答题结束通知

" + "

" + "用户:%s | 工号:%s" + "

" + "
" // 答题概览 + "
" + "
📝 答题概览
" + "
" + "

" + "模式:%s" + "

" + "

" + "工种:" + "%s" + "

" + "
" + "
" // 分数区块 + "
" + "
" + "

初始分数

" + "

%.1f

" + "
" + "
" + "

目标分数

" + "

%s

" + "
" + "
" // 增长分数 + "
" + "

" + "+%s 分数增长" + "

" + "
" // 最终分数 + "
" + "

最终得分

" + "

%s

" + "
" // 时间记录 + "
" + "
⏱ 时间记录
" + "" + "" + "" + "" + "
开始:%s
结束:%s
用时:%s
" + "
" // 系统提示 + "
" + "

%s

" + "
" // 操作按钮 + "" + "
" + "", // 参数列表 mediaQuery, "width:100% !important; max-width:600px; margin:0 auto; font-family:'Segoe UI',Tahoma,sans-serif; border-collapse:collapse;", userInfo.getUserName(), userInfo.getJobNo(), answerDto.getBatchFlag() ? "批量模式" : "单用户模式", answerDto.getJobTypeName(), startScore, userInfo.getTargetScore(), userInfo.getAddScores(), userInfo.getFinalScore(), userInfo.getStartTime(), userInfo.getEndTime(), elapsedTime, throwable != null ? "终止事件:" + throwable.getMessage() : "异常终止", SERVER_IP ); }*/ } }