results = educationPaperQus.stream().filter(quLists -> quLists.getQuId().equals(quId)).findFirst();
+
+ EducationPaperScheme educationPaperScheme = result.get();
+ EducationPaperQu educationPaperQu = results.get();
+ String quScore = educationPaperScheme.getQuScores();
+ ExamQuestion examQuestion = examQuestionService.selectExamQuestionByQuId(quId);
+ for (File lastFile : lastFiles) {
+ if (lastFile.getName().contains("原始")) {
+ String judgementStr = "-----------------------------------------------------------
";
+ judgementStr += "试题序号:" + educationPaperQu.getSort() + "
";
+ judgementStr += "试题编号:" + examQuestion.getQuNum() + "
";
+ judgementStr += "试题分数:" + Double.parseDouble(quScore) + "
";
+ judgementStr += "试题名称:" + name + "
";
+ SourceAndText wordpojo = psService.Judgement(Double.parseDouble(quScore), lastFilePath, lastFile.getPath(), examQuestion, judgementStr);
+ score += wordpojo.getScore();
+ judgementStr = wordpojo.getText();
+ judgementStr += "试题得分:" + wordpojo.getScore() + "
";
+ // 4、需要更新学生试题得分,首先需要查询试题的数据库是否保存信息
+ // 通过 quId,stuId,paperId 查询
+ StuPaperScoreDO stuPaperScoreDO = stuPaperScoreService.getStuScoreByPaperIdAndQuid(stuInfoVo.getStuId(), stuInfoVo.getPaperId(), quId);
+ if (stuPaperScoreDO != null) {
+ // 说明已经是做过该题,需要更新数据
+ stuPaperScoreDO.setScore(new BigDecimal(wordpojo.getScore()));
+ stuPaperScoreDO.setContent(judgementStr);
+ stuPaperScoreDO.setSort(educationPaperQu.getSort());
+ stuPaperScoreDO.setSubjectName(name);
+ stuPaperScoreDO.setIsTrue(wordpojo.getScore() == 0 ? 1 : wordpojo.getScore() == Double.parseDouble(quScore) ? 0 : 2);
+ stuPaperScoreDO.setTrueScore(new BigDecimal(quScore));
+ stuPaperScoreDO.setTenantId(systemTenant.getId());
+ stuPaperScoreService.updateStuPaperScore(stuPaperScoreDO);
+ } else {
+ StuPaperScoreDO insertInfo = new StuPaperScoreDO();
+ insertInfo.setStuId(stuInfoVo.getStuId());
+ insertInfo.setPaperId(stuInfoVo.getPaperId());
+ insertInfo.setQuId(quId);
+ insertInfo.setScore(new BigDecimal(wordpojo.getScore()));
+ insertInfo.setContent(judgementStr);
+ insertInfo.setSort(educationPaperQu.getSort());
+ insertInfo.setSubjectName(name);
+ insertInfo.setTrueScore(new BigDecimal(quScore));
+ insertInfo.setTenantId(systemTenant.getId());
+ insertInfo.setIsTrue(wordpojo.getScore() == 0 ? 1 : wordpojo.getScore() == Double.parseDouble(quScore) ? 0 : 2);
+ stuPaperScoreService.insertStuPaperScore(insertInfo);
+ }
+ break;
+ }
+ }
+ }
+ }
+ return score;
+ }
+}
diff --git a/src/main/java/com/example/exam/exam/service/ps/PsService.java b/src/main/java/com/example/exam/exam/service/ps/PsService.java
new file mode 100644
index 0000000..e6fc0f2
--- /dev/null
+++ b/src/main/java/com/example/exam/exam/service/ps/PsService.java
@@ -0,0 +1,19 @@
+package com.example.exam.exam.service.ps;
+
+import com.example.exam.exam.dal.ExamQuestion;
+import com.example.exam.exam.dal.SourceAndText;
+
+import java.io.File;
+import java.io.IOException;
+
+public interface PsService {
+ /**
+ * 读取考生文件,与题型中要求进行判断
+ * @param path 文件路径
+ * @param examQuestion 试题参数
+ * @param sorce 试题分数
+ * @return 得分
+ * @throws Exception 异常
+ */
+ SourceAndText Judgement(double sorce, String lastFilePath, String path, ExamQuestion examQuestion, String judgementStr) throws IOException;
+}
diff --git a/src/main/java/com/example/exam/exam/service/ps/PsServiceImpl.java b/src/main/java/com/example/exam/exam/service/ps/PsServiceImpl.java
new file mode 100644
index 0000000..f27ecf3
--- /dev/null
+++ b/src/main/java/com/example/exam/exam/service/ps/PsServiceImpl.java
@@ -0,0 +1,287 @@
+package com.example.exam.exam.service.ps;
+
+import com.example.exam.exam.dal.ExamQuestion;
+import com.example.exam.exam.dal.SourceAndText;
+import com.example.exam.exam.mapper.ExamQuestionAnswerMapper;
+import com.example.exam.exam.dal.ExamPsKeyword;
+import com.example.exam.exam.utils.HtmlAppender;
+import com.example.exam.exam.utils.c.LogFileUtils;
+import com.example.exam.exam.utils.ps.PsUtil;
+import com.fasterxml.jackson.core.type.TypeReference;
+import com.fasterxml.jackson.databind.ObjectMapper;
+import jakarta.annotation.Resource;
+import org.apache.commons.collections4.CollectionUtils;
+import org.json.JSONArray;
+import org.json.JSONObject;
+import org.springframework.stereotype.Service;
+
+import java.io.BufferedWriter;
+import java.io.File;
+import java.io.FileWriter;
+import java.io.IOException;
+import java.nio.charset.StandardCharsets;
+import java.nio.file.*;
+import java.time.LocalDateTime;
+import java.time.format.DateTimeFormatter;
+import java.util.*;
+import java.util.stream.Collectors;
+
+@Service
+public class PsServiceImpl implements PsService{
+ static String answerLogPath ; // 文件路径
+ @Resource
+ private ExamQuestionAnswerMapper examQuestionAnswerMapper;
+ private static final DateTimeFormatter formatter =
+ DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss");
+
+ private static final DateTimeFormatter DATE_FORMATTER =
+ DateTimeFormatter.ofPattern("yyyyMMdd_HHmmss");
+
+ @Override
+ public SourceAndText Judgement(double score, String lastFilePath, String path, ExamQuestion examQuestion, String judgementStr) throws IOException {
+ // 创建log文件txt,用于记录
+ answerLogPath = lastFilePath + File.separator + "log.txt";
+ SourceAndText sourceAndText = new SourceAndText();
+ // 构建树形结构
+ List examPsKeywordList = examQuestionAnswerMapper.selectPsAnswerById(examQuestion.getQuId());
+ List treeAnswerList = buildTree(examPsKeywordList);
+
+ String homeDir = System.getProperty("user.dir");
+ String jsxTemplatePath = homeDir + "\\checkPSD.jsx"; // 模板路径
+ String photoshopExe = PsUtil.findPhotoshopExe();
+
+ if (photoshopExe == null) {
+ appendToFile(answerLogPath, "未检测到本机的PhotoShop!");
+ judgementStr = HtmlAppender.appendHtmlLine(judgementStr, "未检测到本机的PhotoShop!");
+ sourceAndText.setText(judgementStr);
+ sourceAndText.setScore(0.0);
+ return sourceAndText;
+ }
+
+ String sthJsonPath = path.replaceAll("(?i)\\.psd$", ".json");
+ Path jsonFilePath = Paths.get(sthJsonPath);
+
+ Path jsxPath=null;
+ // 执行 Photoshop 脚本
+ try {
+ String jsxTargetPath= PsUtil.runTwoPsdsInOneScript(path, jsxTemplatePath, photoshopExe);
+ jsxPath = Paths.get(jsxTargetPath);
+
+ appendToFile(answerLogPath, "Photoshop脚本执行完毕");
+ } catch (Exception e) {
+ appendToFile(answerLogPath, "执行 Photoshop 脚本失败: " + e.getMessage());
+ throw new RuntimeException("执行 Photoshop 脚本失败: " + e.getMessage(), e);
+ }
+
+ // 读取学生的JSON文件
+ try {
+ String sthJsonStr = Files.readString(Paths.get(sthJsonPath), StandardCharsets.UTF_8);
+ JSONObject studentJson = new JSONObject(sthJsonStr);
+
+ double totalScore = 0.0;
+
+ // 遍历树形结构,对比每个节点
+ for (ExamPsKeyword item : treeAnswerList) {
+ SourceAndText studentScorePojo = compareItemWithStudentJson(item, studentJson, "", judgementStr);
+ totalScore += studentScorePojo.getScore();
+ judgementStr = studentScorePojo.getText();
+ }
+
+ // 计算最终得分
+ double maxPossibleScore = calculateMaxPossibleScore(treeAnswerList);
+ double finalScore = ((totalScore / maxPossibleScore) * score);
+ double finalScoreRatio = Math.round(finalScore * 100.0) / 100.0; // 四舍五入到2位小数
+ appendToFile(answerLogPath, "最终得分: " + finalScoreRatio);
+ judgementStr = HtmlAppender.appendHtmlLine(judgementStr, "最终得分: " +finalScoreRatio);
+ sourceAndText.setScore(finalScoreRatio);
+ sourceAndText.setText(judgementStr);
+ return sourceAndText;
+
+ } catch (IOException e) {
+ appendToFile(answerLogPath, "读取 JSON 文件失败");
+ judgementStr = HtmlAppender.appendHtmlLine(judgementStr, "读取 JSON 文件失败!");
+ sourceAndText.setText(judgementStr);
+ sourceAndText.setScore(0.0);
+ return sourceAndText;
+ }finally {
+ // 检查文件是否存在
+ if (Files.exists(jsonFilePath)) {
+ try {
+ // 如果存在则删除
+ Files.delete(jsonFilePath);
+ } catch (IOException e) {
+ }
+ }
+ if (Files.exists(jsxPath)) {
+ try {
+ // 如果存在则删除
+ Files.delete(jsxPath);
+ } catch (IOException e) {
+ }
+ }
+
+ }
+ }
+
+ private SourceAndText compareItemWithStudentJson(ExamPsKeyword item, JSONObject studentJson, String parentPath, String judgementStr) {
+ SourceAndText result = new SourceAndText();
+
+ // 当前节点路径(用中文【】包裹是为了兼容你已有的格式)
+ String currentPath = parentPath.isEmpty()
+ ? "【" + item.getKey() + "】"
+ : parentPath + "【" + item.getKey() + "】";
+
+ // 如果是叶子节点,直接比较值
+ if (item.getChildren() == null || item.getChildren().isEmpty()) {
+ Object valueObj = getValueFromJson(studentJson, currentPath); // 用路径查找学生值
+ String studentValue = valueObj != null ? valueObj.toString() : null;
+ String correctValue = item.getValue();
+
+ boolean isCorrect = correctValue != null && correctValue.equals(studentValue);
+
+ if (isCorrect) {
+ judgementStr = HtmlAppender.appendHtmlLine(judgementStr, currentPath + "【" + correctValue + "】【✅】");
+ appendToFile(answerLogPath, currentPath + "【" + correctValue + "】【✅】");
+ result.setScore(result.getScore() + Double.parseDouble(item.getRate()));
+ } else {
+ judgementStr = HtmlAppender.appendHtmlLine(judgementStr, currentPath + "【" + correctValue + "】【❌】");
+ appendToFile(answerLogPath, currentPath + "【" + correctValue + "】【❌】");
+ }
+
+ result.setText(judgementStr);
+ } else {
+ // 有子节点,递归比较
+ for (ExamPsKeyword child : item.getChildren()) {
+ SourceAndText childResult = compareItemWithStudentJson(child, studentJson, currentPath, judgementStr);
+ judgementStr = childResult.getText(); // ✅ 更新外层judgementStr
+ result.setScore(result.getScore() + childResult.getScore());
+ }
+ result.setText(judgementStr);
+ }
+
+ return result;
+ }
+
+ // 计算最大可能得分
+ private double calculateMaxPossibleScore(List treeAnswerList) {
+ double total = 0;
+ for (ExamPsKeyword item : treeAnswerList) {
+ if (item.getChildren() == null || item.getChildren().isEmpty()) {
+ if (item.getRate() != null) {
+ total += Double.parseDouble(item.getRate());
+ }
+ } else {
+ total += calculateMaxPossibleScore(item.getChildren());
+ }
+ }
+ return total;
+ }
+
+ private Object getValueFromJson(JSONObject json, String path) {
+ String[] keys = path.split("【|】");
+ Object current = json;
+
+ for (String rawKey : keys) {
+ if (rawKey == null || rawKey.isEmpty()) continue;
+
+ if (current instanceof JSONObject) {
+ current = ((JSONObject) current).opt(rawKey);
+ } else if (current instanceof JSONArray) {
+ JSONArray array = (JSONArray) current;
+ Object matched = null;
+
+ // 如果数组项是 JSONObject,并且包含 "图层名",我们按图层名匹配
+ for (int i = 0; i < array.length(); i++) {
+ JSONObject obj = array.optJSONObject(i);
+ if (obj != null && rawKey.equals(obj.optString("图层名"))) {
+ matched = obj;
+ break;
+ }
+ }
+
+ // 若未按图层名匹配成功,就尝试找任何包含该key的对象
+ if (matched == null) {
+ for (int i = 0; i < array.length(); i++) {
+ JSONObject obj = array.optJSONObject(i);
+ if (obj != null && obj.has(rawKey)) {
+ matched = obj.get(rawKey);
+ break;
+ }
+ }
+ }
+
+ current = matched;
+ } else {
+ return null;
+ }
+ }
+
+ return current;
+ }
+
+
+
+
+
+ /**
+ * 构建答案树形结构
+ */
+ private List buildTree(List flatList) {
+ if (CollectionUtils.isEmpty(flatList)) {
+ return Collections.emptyList();
+ }
+
+ // 第一步:构建所有节点
+ Map nodeMap = new LinkedHashMap<>();
+ Map> parentChildMap = new HashMap<>();
+
+ flatList.forEach(item -> {
+ ExamPsKeyword node = new ExamPsKeyword();
+ node.setId(item.getId());
+ node.setKey(item.getKey());
+ node.setValue(item.getValue()); // 先保留原始值
+ node.setRate(item.getRate());
+ node.setType(item.getType());
+ node.setSort(item.getSort());
+ nodeMap.put(item.getId(), node);
+ String parentId = item.getParentId() != null ? item.getParentId() : "0";
+ parentChildMap.computeIfAbsent(parentId, k -> new ArrayList<>()).add(node);
+ });
+
+ // 第二步:建立父子关系并识别真正父节点
+ Set realParents = new HashSet<>();
+ parentChildMap.forEach((parentId, children) -> {
+ if ("0".equals(parentId)) {
+ children.forEach(node -> {
+ // 标记没有子节点的独立节点
+ if (!parentChildMap.containsKey(node.getId())) {
+ node.setChildren(null);
+ }
+ });
+ } else {
+ ExamPsKeyword parent = nodeMap.get(parentId);
+ if (parent != null) {
+ parent.setChildren(children);
+ realParents.add(parent); // 记录真正有子节点的父节点
+ }
+ }
+ });
+
+ // 第三步:对有子节点的父节点设置value=null
+ realParents.forEach(parent -> parent.setValue(null));
+
+ return parentChildMap.getOrDefault("0", Collections.emptyList());
+ }
+
+ public static void appendToFile(String filePath, String content) {
+ try (BufferedWriter writer = new BufferedWriter(new FileWriter(filePath, true))) {
+ String timestamp = LocalDateTime.now().format(formatter);
+ String logLine = String.format("[%s] %s", timestamp, content);
+ writer.write(logLine);
+ writer.newLine(); // 可选:添加换行符
+ } catch (IOException e) {
+ e.printStackTrace();
+ }
+ }
+
+}
diff --git a/src/main/java/com/example/exam/exam/utils/ps/PsUtil.java b/src/main/java/com/example/exam/exam/utils/ps/PsUtil.java
new file mode 100644
index 0000000..a4d4fc0
--- /dev/null
+++ b/src/main/java/com/example/exam/exam/utils/ps/PsUtil.java
@@ -0,0 +1,123 @@
+package com.example.exam.exam.utils.ps;
+
+import java.io.BufferedReader;
+import java.io.File;
+import java.io.IOException;
+import java.io.InputStreamReader;
+import java.nio.charset.StandardCharsets;
+import java.nio.file.Files;
+import java.nio.file.Paths;
+
+public class PsUtil {
+
+
+
+ // 查询注册表路径,返回command字符串(即exe路径+参数)
+ private static String queryReg(String regPath) throws IOException, InterruptedException {
+ Process process = Runtime.getRuntime().exec("reg query \"" + regPath + "\" /ve");
+ BufferedReader reader = new BufferedReader(new InputStreamReader(process.getInputStream(), "GBK")); // 注册表输出一般GBK编码
+ String line;
+ while ((line = reader.readLine()) != null) {
+ line = line.trim();
+ if (line.startsWith("(默认)")) {
+ String[] parts = line.split(" ");
+ if (parts.length >= 3) {
+ return parts[parts.length - 1].replace("\"", "").trim(); // 去掉双引号
+ }
+ }
+ }
+ process.waitFor();
+ return null;
+ }
+
+ // 尝试读取多个注册表路径获取 Photoshop 路径
+ public static String findPhotoshopExe() {
+ String[] regPaths = {
+ "HKLM\\SOFTWARE\\Classes\\Applications\\Photoshop.exe\\shell\\edit\\command",
+ "HKLM\\SOFTWARE\\WOW6432Node\\Classes\\Applications\\Photoshop.exe\\shell\\edit\\command"
+ };
+
+ for (String path : regPaths) {
+ try {
+ String exePath = queryReg(path);
+ if (exePath != null && !exePath.isEmpty()) {
+ // 通常 exe 路径后面会跟参数 %1,只取 exe 路径部分
+ int idx = exePath.toLowerCase().indexOf("photoshop.exe");
+ if (idx != -1) {
+ exePath = exePath.substring(0, idx + "photoshop.exe".length());
+ }
+ return exePath;
+ }
+ } catch (Exception e) {
+ // 忽略异常,继续尝试其他路径
+ }
+ }
+ return null; // 没找到
+ }
+
+
+ public static String runTwoPsdsInOneScript(String psdPath, String jsxTemplatePath, String photoshopExe)
+ throws IOException, InterruptedException {
+
+ File psdFile1 = new File(psdPath);
+ String baseDir = psdFile1.getParent();
+ String jsxTargetPath = baseDir + File.separator + "run_both_" + System.currentTimeMillis() + ".jsx";
+
+ String jsxTemplate = Files.readString(Paths.get(jsxTemplatePath), StandardCharsets.UTF_8);
+
+ String safePath1 = psdPath.replace("\\", "\\\\").replace("'", "\\'");
+ String jsxContent = jsxTemplate
+ .replace("${inputPath1}", safePath1);
+
+ Files.writeString(Paths.get(jsxTargetPath), jsxContent, StandardCharsets.UTF_8);
+
+ String command = String.format("\"%s\" -r \"%s\"", photoshopExe, jsxTargetPath);
+ System.out.println("运行 Photoshop 脚本: " + command);
+
+ Process process = Runtime.getRuntime().exec(command);
+
+ // 异步打印输出日志
+ new Thread(() -> {
+ try (BufferedReader br = new BufferedReader(new InputStreamReader(process.getInputStream()))) {
+ String line;
+ while ((line = br.readLine()) != null) {
+ System.out.println("[PS OUT] " + line);
+ }
+ } catch (IOException ignored) {}
+ }).start();
+
+ new Thread(() -> {
+ try (BufferedReader br = new BufferedReader(new InputStreamReader(process.getErrorStream()))) {
+ String line;
+ while ((line = br.readLine()) != null) {
+ System.err.println("[PS ERR] " + line);
+ }
+ } catch (IOException ignored) {}
+ }).start();
+
+ // 不使用 waitFor 阻塞,改成轮询判断两个 JSON 文件是否生成
+ String jsonPath1 = psdPath.replaceAll("(?i)\\.psd$", ".json");
+
+ int maxWaitSeconds = 60; // 最多等待 60秒,根据实际调整
+ int waited = 0;
+ while (!(Files.exists(Paths.get(jsonPath1)))) {
+ Thread.sleep(1000); // 每秒检查一次
+ waited++;
+ if (waited > maxWaitSeconds) {
+ // 超时,结束进程并抛异常
+ process.destroyForcibly();
+ throw new RuntimeException("等待 Photoshop 生成 JSON 文件超时");
+ }
+ }
+
+ System.out.println("检测到 JSON 文件生成,关闭 Photoshop 进程...");
+
+ // 手动杀死 Photoshop 进程,确保释放资源
+ Process killProcess = Runtime.getRuntime().exec("taskkill /IM Photoshop.exe /F");
+ killProcess.waitFor();
+
+ System.out.println("Photoshop 进程已关闭");
+
+ return jsxTargetPath;
+ }
+}
diff --git a/src/main/resources/mapper/question/ExamQuestionAnswerMapper.xml b/src/main/resources/mapper/question/ExamQuestionAnswerMapper.xml
index 08e22b7..d58ad40 100644
--- a/src/main/resources/mapper/question/ExamQuestionAnswerMapper.xml
+++ b/src/main/resources/mapper/question/ExamQuestionAnswerMapper.xml
@@ -15,7 +15,17 @@
+
+
+
+
+
+
+
+
+
+
select answer_id, qu_id, is_right, image, content,contentIn, score_rate,attribute,sort from exam_question_answer
@@ -44,4 +54,10 @@
+
\ No newline at end of file