123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392 |
- 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<Class<? extends Throwable>> BUSINESS_EXCEPTIONS = new HashSet<Class<? extends Throwable>>() {{
- 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("<html>"
- + "<head>"
- + "<meta name=\"viewport\" content=\"width=device-width, initial-scale=1.0\">"
- + "<style>%s</style>"
- + "</head>"
- + "<body style=\"margin:0; padding:20px 0; background:#f5f5f5;\">"
- + "<table role=\"presentation\" cellpadding=\"0\" cellspacing=\"0\" style=\"%s\">"
- + "<tr><td class=\"mobile-adjust\" style=\"padding:20px; background:white;\">"
- // 头部信息
- + "<div style=\"border-bottom:2px solid #2196F3; padding-bottom:12px; margin-bottom:16px;\">"
- + "<h2 class=\"section-title\" style=\"margin:0; font-size:24px;\">✅ 答题结束通知</h2>"
- + "<p style=\"color:#7f8c8d; margin:6px 0; font-size:14px;\">"
- + "用户:<strong>%s</strong> | 工号:%s"
- + "</p>"
- + "</div>"
- // 答题概览
- + "<div style=\"margin-bottom:20px;\">"
- + "<div class=\"section-title\" style=\"color:#2c3e50; font-size:20px; margin-bottom:12px;\">📝 答题概览</div>"
- + "<div class=\"info-card\" style=\"background:#f8f9fa; padding:15px; border-radius:8px;\">"
- + "<p style=\"margin:4px 0; font-size:14px;\">"
- + "<span style=\"color:#7f8c8d;\">模式:</span>%s"
- + "</p>"
- + "<p style=\"margin:4px 0; font-size:14px;\">"
- + "<span style=\"color:#7f8c8d;\">工种:</span>"
- + "<span style=\"color:#2196F3; font-weight:600;\">%s</span>"
- + "</p>"
- + "</div>"
- + "</div>"
- // 分数区块
- + "<div class=\"score-container\" style=\"display:flex; gap:15px; margin-bottom:20px;\">"
- + "<div class=\"score-box\" style=\"flex:1; background:#f3f5f7; padding:15px; border-radius:8px; box-sizing:border-box;\">"
- + "<p style=\"margin:2px 0; color:#7f8c8d; font-size:14px;\">初始分数</p>"
- + "<p style=\"font-size:24px; margin:6px 0; color:#2196F3;\">%.1f</p>"
- + "</div>"
- + "<div class=\"score-box\" style=\"flex:1; background:#f3f5f7; padding:15px; border-radius:8px; box-sizing:border-box;\">"
- + "<p style=\"margin:2px 0; color:#7f8c8d; font-size:14px;\">目标分数</p>"
- + "<p style=\"font-size:24px; margin:6px 0; color:#FF9800;\">%s</p>"
- + "</div>"
- + "</div>"
- // 增长分数
- + "<div class=\"score-box\" style=\"text-align:center; background:#4CAF50; padding:15px; border-radius:8px;\">"
- + "<p style=\"margin:0; color:white; font-size:16px; font-weight:bold;\">"
- + "+%s 分数增长"
- + "</p>"
- + "</div>"
- // 最终分数
- + "<div class=\"final-score\" style=\"text-align:center; background:white; padding:20px; margin:20px 0; border-radius:8px; box-shadow:0 2px 8px rgba(0,0,0,0.1);\">"
- + "<p style=\"margin:4px 0; color:#7f8c8d; font-size:14px;\">最终得分</p>"
- + "<p style=\"font-size:32px; margin:8px 0; color:#4CAF50; font-weight:800;\">%s</p>"
- + "</div>"
- // 时间记录
- + "<div>"
- + "<div class=\"section-title\" style=\"color:#2c3e50; font-size:20px; margin:12px 0;\">⏱ 时间记录</div>"
- + "<table class=\"time-record\" style=\"width:100%%; font-size:14px;\">"
- + "<tr><td style=\"padding:4px 0; color:#2196F3;\">▸</td><td style=\"padding:4px 0;\">开始:%s</td></tr>"
- + "<tr><td style=\"padding:4px 0; color:#2196F3;\">▸</td><td style=\"padding:4px 0;\">结束:%s</td></tr>"
- + "<tr><td style=\"padding:4px 0; color:#2196F3;\">▸</td><td style=\"padding:4px 0;\">用时:%s</td></tr>"
- + "</table>"
- + "</div>"
- // 系统提示
- + "<div class=\"system-alert\" style=\"margin:25px 0; padding:15px; background:#fff3e0; border-radius:8px;\">"
- + "<p style=\"margin:4px 0; color:#EF6C00; font-size:14px;\">%s</p>"
- + "</div>"
- // 操作按钮
- + "<div style=\"text-align:center; margin-top:30px;\">"
- + "<a href=\"%s/answer\" class=\"action-button\" "
- + "style=\"display:inline-block; background:#2196F3; color:white!important; "
- + "padding:16px 40px; text-decoration:none; border-radius:30px; font-size:16px; "
- + "letter-spacing:1px; min-width:200px;\">🚀 前往答题系统查看</a>"
- + "</div>"
- + "</td></tr>"
- + "</table>"
- + "</body></html>",
- // 参数列表
- 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
- );
- }*/
- }
- }
|