Pārlūkot izejas kodu

1.重构答题结束处理逻辑代码
2.发送邮件代码由拼接html改为thymeleaf本地模板动态取值
3.批量导入增加姓名列,导入失败账号邮件会现实姓名

01495251 3 mēneši atpakaļ
vecāks
revīzija
cbcad49640

+ 5 - 0
pom.xml

@@ -201,6 +201,11 @@
             <version>4.9.3</version>
         </dependency>
 
+        <dependency>
+            <groupId>org.springframework.boot</groupId>
+            <artifactId>spring-boot-starter-thymeleaf</artifactId>
+        </dependency>
+
     </dependencies>
 
     <build>

+ 323 - 75
src/main/java/com/web/api/config/AsyncExceptionConfig.java

@@ -1,6 +1,7 @@
 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;
@@ -8,20 +9,25 @@ 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 com.web.api.utils.DateUtills;
 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.springframework.util.ObjectUtils;
+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.ZoneOffset;
 import java.time.format.DateTimeFormatter;
+import java.util.Arrays;
+import java.util.HashSet;
+import java.util.Set;
 
 
 /**
@@ -42,6 +48,20 @@ public class AsyncExceptionConfig implements AsyncConfigurer {
     @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
@@ -51,94 +71,322 @@ public class AsyncExceptionConfig implements AsyncConfigurer {
 
     class SpringAsyncExceptionHandler implements AsyncUncaughtExceptionHandler {
         @Override
-        public void handleUncaughtException(Throwable throwable, Method method, Object... objects) {
-            if (!(throwable instanceof SgException) &&
-                    !(throwable instanceof IOException) &&
-                    !(throwable instanceof NullPointerException) &&
-                    !(throwable instanceof JSONException)){
-                log.error("------非业务类异常,此处不做处理--------:{}", throwable);
-                return;
+        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类型");
             }
-            log.info("业务参数:{}",objects);
-            //收件人
-            String[] toUser = {"251664727@qq.com", "632062365@qq.com"};
-            AnswerDto answerDto = (AnswerDto) objects[2];
-            Double startScore = (Double) objects[3];
+            if (!(params[3] instanceof Double)) {
+                throw new IllegalArgumentException("第四个参数应为Double类型");
+            }
+        }
+
+        private UserInfo processUserInfo(AnswerDto answerDto, Double startScore, Throwable throwable) {
             UserInfo userInfo = ContextUtils.getUserInfo(answerDto.getJobNo());
-            DateTimeFormatter fmt = DateTimeFormatter.ofPattern("yyyy年MM月dd日 HH时mm分");
-            userInfo.setEndTime(LocalDateTime.now().format(fmt));
+            userInfo.setEndTime(LocalDateTime.now().format(DATE_TIME_FORMATTER));
             userInfo.setAnswerStatus("已结束");
-            DecimalFormat df = new DecimalFormat("######0.00");
-            Object addScores = ObjectUtils.isEmpty(userInfo.getFinalScore()) ? "本次答题异常终止,无法计算分数" : df.format(userInfo.getFinalScore() - startScore);
-            if (answerDto.getBatchFlag()){
-                ContextUtils.setBatchUserList(answerDto.getJobNo(),userInfo);
+            if (userInfo.getFinalScore() != null) {
+                double addScore = userInfo.getFinalScore() - startScore;
+                userInfo.setAddScores(DECIMAL_FORMAT.format(addScore));
+            } else {
+                userInfo.setAddScores("本次答题异常终止,无法计算分数");
             }
-            //关闭当前答题
-            ContextUtils.setOnOff(answerDto.getJobNo(), false);
-            String subject="";
             if (throwable instanceof SgException ) {
-                subject=throwable.getMessage();
-//                if (((SgException) throwable).getCode() == ErrorCode.LOGOUT.getCode()) {
-//                    log.error("cookie已失效,移除本地用户:{}cookie", answerDto);
-//                }
-                if (((SgException) throwable).getCode() != ErrorCode.STOP_ANSWER.getCode()) {
-                    userInfo.setErrMsg(throwable.getMessage());
-                }
+                userInfo.setErrMsg(throwable.getMessage());
             }else {
                 userInfo.setErrMsg("本次答题异常终止");
-                subject=userInfo.getUserName()+"【"+userInfo.getJobNo()+"】答题异常结束";
             }
-            int num = 0;
-            if (!addScores.toString().equals("0.00")) {
-                userInfo.setAddScores(addScores.toString());
-                log.info("本次答题已结束,更新数据记录对应字段:{}", userInfo);
-                num = answerDao.updatePassRecord(userInfo);
-            } else {
-                answerDao.deletePassRecord(userInfo.getId());
-                log.error("本次答题增长积分小于等于0:{},不保存此数据", addScores);
+            handleBatchOperation(answerDto, userInfo);
+            return userInfo;
+        }
+
+        /***
+         * 关闭答题
+         */
+        private void handleBatchOperation(AnswerDto answerDto, UserInfo userInfo) {
+            if (answerDto.getBatchFlag()) {
+                ContextUtils.setBatchUserList(answerDto.getJobNo(), userInfo);
             }
-            double startTime = LocalDateTime.parse(userInfo.getStartTime(),fmt).toEpochSecond(ZoneOffset.of("+8"));
-            double endTime = LocalDateTime.parse(userInfo.getEndTime(),fmt).toEpochSecond(ZoneOffset.of("+8"));
-            String elapsedTime =DateUtills.getElapsedTime(startTime,endTime);
-            log.info("本次答题耗时:{}",elapsedTime);
-            String loginFrom =  answerDto.getBatchFlag()?"批量导入答题":"用户登录";
-            String content = "<html>\n" +
-                    "<body>\n" +
-                    "    <h3>用户:" + userInfo.getUserName() + " 已完成本次刷题,详情如下 :</h3>\n" +
-                    "    <p>工号:" + userInfo.getJobNo() + "</p>" +
-                    "    <p>登录方式:" +loginFrom  + "</p>" +
-                    "    <p>答题工种:<span style=\"color: blue;font-weight: bold\">" + answerDto.getJobTypeName() + "</span></p>" +
-                    "    <p>初始分数:" + startScore + "</p>" +
-                    "    <p>目标分数:" + userInfo.getTargetScore() + "</p>" +
-                    "    <p>增长分数:<span style=\"color: blue;font-weight: bold\">" + addScores + "</span></p>" +
-                    "    <p>最终分数:<span style=\"color: red;font-weight: bold\">" + userInfo.getFinalScore() + "</span></p>" +
-                    "    <p>开始时间:" + userInfo.getStartTime() + "</p>" +
-                    "    <p>结束时间:" + userInfo.getEndTime() + "</p>" +
-                    "    <p>运行时间:<span style=\"color: blue;font-weight: bold\">" + elapsedTime + "</span></p>" +
-                    "    <p>结束原因:<span style=\"color: red;font-weight: bold\">"  + throwable.getMessage() + "</span></p>" +
-                    "    <a href=\"http://49.232.153.218/answer\">更多详情请前往系统查看!</a>" +
-                    "</body>\n" +
-                    "</html>";
+            ContextUtils.setOnOff(answerDto.getJobNo(), false);
+        }
+
+        private int handleDatabaseOperation(UserInfo userInfo) {
             try {
-                if (num > 0) {
-                    if (ContextUtils.getBatchUserList().size()>10){
-                        toUser = new String[]{"251664727@qq.com"};
+                if (isValidAddScore(userInfo.getAddScores())) {
+                    log.info("更新答题记录:{}", userInfo);
+                    int updateCount = answerDao.updatePassRecord(userInfo);
+                    if (updateCount == 0) {
+                        log.error("数据库更新失败:{}", userInfo);
                     }
-                    mailService.sendSimpleMail(toUser, subject, content);
+                    return updateCount;
                 } else {
-                    log.error("答题结算更新失败:{}", userInfo);
+                    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);
+                log.error("邮件发送失败: ", e);
             }
-            //手动停止,不删除当前对象
-            if (throwable instanceof SgException && ((SgException) throwable).getCode() == ErrorCode.STOP_ANSWER.getCode()) {
-                return;
+        }
+
+        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 "时间计算错误";
             }
-            //删除当前答题对象
-            ContextUtils.removeUserCookies(answerDto.getJobNo());
-            ContextUtils.removeUserInfo(answerDto.getJobNo());
-            log.error("------Async无返回方法的异常处理方法--------:{}", throwable);
         }
+
+        /**
+         * 非手动结束情况下清楚用户缓存信息
+         */
+        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
+            );
+        }*/
     }
 }

+ 2 - 2
src/main/java/com/web/api/listener/ImportExcelListener.java

@@ -78,14 +78,14 @@ public class ImportExcelListener extends AnalysisEventListener<AnswerExcel> {
             cookieStore = answerService.login(answerExcel.getUserName(), answerExcel.getPassword(), safeCode);
         } catch (Exception e) {
             errorCount++;
-            msg.append("失败用户:" + answerExcel.getUserName() + ",失败原因:" + e.getMessage() + "</br>");
+            msg.append("失败用户:" + answerExcel.getName() + " 工号: " +answerExcel.getUserName()+ ",失败原因:" + e.getMessage() + "</br>");
             return;
         }
         //根据工种名称获取工种基本信息拿到工种id和关卡初始id
         JobTypeEntity jobTypeEntity = Constant.jobTypeMap.get(answerExcel.getJobTypeName());
         if (ObjectUtils.isEmpty(jobTypeEntity)) {
             errorCount++;
-            msg.append("失败用户:" + answerExcel.getUserName() + ",失败原因:工种填写错误</br>");
+            msg.append("失败用户:" + answerExcel.getName() + " 工号: " +answerExcel.getUserName()+ ",失败原因:工种填写错误</br>");
             return;
         }
         AnswerDto answerDto = new AnswerDto();

+ 7 - 4
src/main/java/com/web/api/model/po/AnswerExcel.java

@@ -14,15 +14,18 @@ import lombok.Data;
 @Data
 public class AnswerExcel {
 
-    @ExcelProperty(value = "用户名",index=0)
+    @ExcelProperty(value = "姓名",index=0)
+    private String name;
+
+    @ExcelProperty(value = "用户名",index=1)
     private String userName;
 
-    @ExcelProperty(value = "密码",index=1)
+    @ExcelProperty(value = "密码",index=2)
     private String password;
 
-    @ExcelProperty(value = "工种",index=2)
+    @ExcelProperty(value = "工种",index=3)
     private String jobTypeName;
 
-    @ExcelProperty(value = "目标",index=3)
+    @ExcelProperty(value = "目标",index=4)
     private Integer targetScores;
 }

+ 116 - 0
src/main/resources/templates/answer-report.html

@@ -0,0 +1,116 @@
+<!DOCTYPE html>
+<html xmlns:th="http://www.thymeleaf.org">
+<head>
+    <meta charset="UTF-8">
+    <meta name="viewport" content="width=device-width, initial-scale=1.0">
+    <title>答题结果通知</title>
+    <style th:inline="text">
+        /* 基础样式 */
+        .email-container {
+            width: 100% !important;
+            max-width: 600px;
+            margin: 0 auto;
+            font-family: 'Segoe UI', Tahoma, sans-serif;
+            background: #f5f5f5;
+            /*padding: 20px 0;*/
+        }
+
+        /* 响应式调整 */
+        @media only screen and (max-width: 480px) {
+            .mobile-adjust { padding: 12px 15px !important; }
+            .section-title { font-size: 18px !important; }
+            .info-card { padding: 12px !important; }
+            .score-container { gap: 10px !important; }
+            .score-box { padding: 12px !important; }
+            .final-score { padding: 15px !important;}
+            .time-record td { padding: 4px 0 !important; }
+            .system-alert { margin: 16px 0 !important; }
+            .action-button { padding: 12px 24px !important; }
+        }
+    </style>
+</head>
+<body>
+<div class="email-container">
+    <table role="presentation" style="width:100%;border-collapse:collapse;">
+        <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 th:text="${user.userName}"></strong> | 工号:<span th:text="${user.jobNo}"></span>
+                    </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>
+                            <span th:text="${answer.batchFlag} ? '批量模式' : '单用户模式'"></span>
+                        </p>
+                        <p style="margin:4px 0;font-size:14px;">
+                            <span style="color:#7f8c8d;">工种:</span>
+                            <span style="color:#2196F3;font-weight:600;" th:text="${answer.jobTypeName}"></span>
+                        </p>
+                    </div>
+                </div>
+
+                <!-- 分数区块 -->
+                <div class="score-container" style="display:flex;gap:15px;margin-bottom:20px;">
+                    <div th:each="score : ${scores}" 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;"
+                           th:text="${score.type}"></p>
+                        <p th:style="'font-size:20px;font-weight:600; margin:6px 0; color:' + ${score.color}"
+                           th:text="${score.value}"></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;">
+                        +<span th:text="${user.addScores}"></span> 分数增长
+                    </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;"
+                       th:text="${user.finalScore}"></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 th:each="time : ${times}">
+                            <td style="padding:4px 0;color:#2196F3;">▸</td>
+                            <td style="padding:4px 0;"
+                                th:text="${time.label} + ':' + ${time.value}"></td>
+                        </tr>
+                    </table>
+                </div>
+
+                <!-- 系统提示 -->
+                <div th:if="${exception}" class="system-alert" style="margin:25px 0;padding:15px;background:#fff3e0;border-radius:8px;">
+                    <p style="margin:4px 0;color:#EF6C00;font-size:14px;">
+                        终止事件:<span th:text="${exception.message}"></span>
+                    </p>
+                </div>
+
+                <!-- 操作按钮 -->
+                <div style="text-align:center;margin-top:30px;">
+                    <a th:href="@{${serverIp}/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>
+</div>
+</body>
+</html>