diff --git a/exam-module-judgement/exam-module-judgement-biz/pom.xml b/exam-module-judgement/exam-module-judgement-biz/pom.xml index 8aec2df8..fc44e4c2 100644 --- a/exam-module-judgement/exam-module-judgement-biz/pom.xml +++ b/exam-module-judgement/exam-module-judgement-biz/pom.xml @@ -127,6 +127,12 @@ gson 2.10 + + pc.exam.gg + exam-module-exam-biz + 2.4.2-SNAPSHOT + compile + diff --git a/exam-module-judgement/exam-module-judgement-biz/src/main/java/pc/exam/pp/module/judgement/service/c_programming/JudgementService.java b/exam-module-judgement/exam-module-judgement-biz/src/main/java/pc/exam/pp/module/judgement/service/c_programming/JudgementService.java new file mode 100644 index 00000000..f4f195bf --- /dev/null +++ b/exam-module-judgement/exam-module-judgement-biz/src/main/java/pc/exam/pp/module/judgement/service/c_programming/JudgementService.java @@ -0,0 +1,29 @@ +package pc.exam.pp.module.judgement.service.c_programming; + +import pc.exam.pp.module.exam.dal.dataobject.ExamQuestion; + +/** + * 判分逻辑集合 + * + * @author rwb + */ +public interface JudgementService { + + /** + * 程序设计判分 + * @param examQuestion 程序设计题内容 + * @param path 文件路径 + * @param score 分数 + * @return 返回判分 + */ + public double ProgrammingC(double score, String path, ExamQuestion examQuestion); + + + /** + * 出题时测试运行C语言代码 + * @param code 源码 + * @return 运行结果 + */ + public String run_test_result(String code); + +} diff --git a/exam-module-judgement/exam-module-judgement-biz/src/main/java/pc/exam/pp/module/judgement/service/c_programming/JudgementServiceImpl.java b/exam-module-judgement/exam-module-judgement-biz/src/main/java/pc/exam/pp/module/judgement/service/c_programming/JudgementServiceImpl.java new file mode 100644 index 00000000..1da72d73 --- /dev/null +++ b/exam-module-judgement/exam-module-judgement-biz/src/main/java/pc/exam/pp/module/judgement/service/c_programming/JudgementServiceImpl.java @@ -0,0 +1,207 @@ +package pc.exam.pp.module.judgement.service.c_programming; + +import org.springframework.stereotype.Service; +import pc.exam.pp.module.exam.dal.dataobject.ExamQuestion; +import pc.exam.pp.module.exam.dal.dataobject.ExamQuestionAnswer; +import pc.exam.pp.module.exam.dal.dataobject.ExamQuestionKeyword; +import pc.exam.pp.module.exam.utils.file.LogFileUtils; +import pc.exam.pp.module.judgement.utils.JudgementCUtils; + +import java.util.*; + +/** + * 判分逻辑集合 + * + * @author rwb + */ +@Service +public class JudgementServiceImpl implements JudgementService +{ + + /** + * 程序设计判分 + * @param examQuestion 程序设计题内容 + * @param path 文件路径 + * @param score 分数 + * @return 返回判分 + */ + public double ProgrammingC(double score, String path, ExamQuestion examQuestion) { + + // 关键字比对,超过权重-测试用例/运行(测试用例全对,直接满分-不全对)-结果,不超过权重只给关键字几个的分 + // 获取该题有多少分 + // TODO 测试分数15,该程序设计题为15分 + // double score = 15; + // 总分 + double totalScore = 0; + // 测试用例结果分数 + double compile_score = 0; + // 关键字分数 + double key_score = 0; + // 关键字分数 + double result_score = 0; + + // 先获取题的组成部分 + // 是否需要程序编译 (0:true;1:false) + boolean is_pass = Objects.equals(examQuestion.getQuestionScores().getIsPass(), "0"); + // 是否需要程序结果 (0:true;1:false) + boolean is_result = Objects.equals(examQuestion.getQuestionScores().getIsResult(), "0"); + // 是否需要关键字 (0:true;1:false) + boolean is_keyword = Objects.equals(examQuestion.getQuestionScores().getIsKeyword(), "0"); + // 是否需要测试用例 (0:true;1:false) + boolean is_compile = Objects.equals(examQuestion.getQuestionScores().getIsCompile(), "0"); + + // 占比百分比 + double is_pass_score = Integer.parseInt(examQuestion.getQuestionScores().getIsPassScore()) / 100.0; + double is_result_score = Integer.parseInt(examQuestion.getQuestionScores().getIsResultScore()) / 100.0; + double is_keyword_score = Integer.parseInt(examQuestion.getQuestionScores().getIsKeywordScore()) / 100.0; + double is_compile_score = Integer.parseInt(examQuestion.getQuestionScores().getIsCompileScore()) / 100.0; + + // 关键字分数占比 + double keyword_score = score * is_keyword_score; + // 编译分数分数占比 + double pass_score = score * is_pass_score; + + // 试卷其中有C语言的试题 + String path_c = path + "/" + examQuestion.getQuBankName(); + // 创建log文件txt,用于记录 + LogFileUtils.createFile(path_c + "/log.txt"); + String code = JudgementCUtils.readFile(path_c, examQuestion.getQuId()+".txt"); + LogFileUtils.writeLine("✅ 系统开始读取文件:" + code); + if (code == null) { + // 如果没有读到源码 + LogFileUtils.writeLine("❌ 系统没有读取到文件。"); + LogFileUtils.close(); + // 该题不得分,直接算成0分 + return 0; + } + int true_number = 0; + // 关键字分数 + if (is_keyword) { + // 总权重值 + int weight = 0; + List> key_list = new ArrayList<>(); + // 进行关键字权重比对进行判断 + + for (ExamQuestionKeyword examQuestionKeyword : examQuestion.getQuestionKeywords()) { + boolean keyword_run = code.contains(examQuestionKeyword.getKeyword()); + // 计算权值 + Map item = new HashMap<>(); + item.put("success", keyword_run); + item.put("score_rate", examQuestionKeyword.getScoreRate()); + LogFileUtils.writeLine("✅ 关键字比对:" + examQuestionKeyword.getKeyword() + "--" + keyword_run); + weight += Integer.parseInt(examQuestionKeyword.getScoreRate()); + key_list.add(item); + } + // 所有的关键字比对完成之后进行盘端关键字的分数 + // 先获取关键字应该分数占比 + double one_keyword_score = keyword_score / weight; + // 根据权重进行给分 + for (Map item : key_list) { + // 判断首先等于true 的情况下 + if ((boolean)item.get("success")) { + // 每个选项分值 = 总分 / 总权重 + true_number += 1; + key_score += one_keyword_score * Integer.parseInt((String) item.get("score_rate")); + LogFileUtils.writeLine("✅ 关键字得分:" + key_score); + } + } + } + // 程序需要在运行测试用例,及满足关键字的情况下进行给出运行得分 + // 关键字临界得分值 + int todo_key_percentage = Integer.parseInt(examQuestion.getQuestionScores().getKeywordCutoff()); + if (key_score > keyword_score * ((double) todo_key_percentage / 100) ) { + // 编译代码运行 + if (is_pass) { + // 如果使用程序编译,进行程序编译 + LogFileUtils.writeLine("✅ 正在使用-std=c99进行编译..."); + // 使用C99 运行并得出结果 + String code_return = JudgementCUtils.run_code(code,null,"-std=c99", "编译通过运行"); + if (!code_return.contains("error")) { + // 编译没有报错,加上编译分数 + totalScore += pass_score; + LogFileUtils.writeLine("✅ 编译通过得分:" + pass_score); + } else { + LogFileUtils.writeLine("❌ 编译未通过。"); + } + } + // 进行判断测试用例 + // 测试用例 + // 判断是否要程序结果 ,需要程序结果的,就需要测试用例 + if (is_compile) { + // 先运行程序,再将测试用例进行比对 + // 运行完成后在判断是否需要进行关键字比对 + boolean run_code = false; + List runList = new ArrayList<>(); + LogFileUtils.writeLine("✅ 使用测试用例进行判分..."); + for (ExamQuestionAnswer examQuestionAnswer : examQuestion.getAnswerList()) { + // 使用C99 运行并得出结果 + String code_return = JudgementCUtils.run_code(code, examQuestionAnswer.getContentIn(),"-std=c99",null); + String actual = code_return.trim(); + String expected = examQuestionAnswer.getContent().trim(); + if (actual.equals(expected)) { + // 判断测试用例结果是否正确 + runList.add(true); + // 获取测试用例临界值,并进行判断 +// if (runList.size() >= Integer.parseInt(examQuestion.getQuestionScores().getCompileCutoff()) && !runList.contains(false)) { +// // 测试用例得分 +// compile_score += (double) (score * is_compile_score); +// // 结果得分 +// result_score += (double) (score * is_result_score); +// LogFileUtils.writeLine("✅ 测试用例得分:" + compile_score); +// break; +// } + } + } + // 记录存在多少个测试用例,并且同时记录正确测试用例个数 + int test_case_number = examQuestion.getAnswerList().size(); + int true_test_case_number = runList.size(); + + // 判断正确关系 + // 1、如果完全相等,说明完全正确,直接给满分 + if (test_case_number == true_test_case_number) { + // 满分,该题多少分就是多少分 + LogFileUtils.writeLine("✅ 测试用例全部正确:"+ score); + LogFileUtils.close(); + return score; + } else if (test_case_number > true_test_case_number) { + // 2、测试用例没有完全正确,对多少个就是多少分 + // 公式:测试用例总分数 / 测试用例数量 * 正确测试用例数量 + compile_score += (double) ((score * is_compile_score) / test_case_number) * true_test_case_number; + LogFileUtils.writeLine("✅ 测试用例数量:"+ test_case_number + ",正确数量:" + true_test_case_number + ",得分:" + compile_score); + } + } + + // 总分 = 总分 + 测试用例得分 + totalScore += compile_score; + if (compile_score > 0) { + // 如果测试用例正确有得分的 + // 结果 + if (is_result) { + // 总分 = 总分 + 结果得分 + totalScore += result_score; + LogFileUtils.writeLine("✅ 结果得分:" + result_score); + } + } + totalScore += key_score; + LogFileUtils.close(); + return totalScore; + } else { + // 关键字对几个给几分,没有达到临界值的情况下 + totalScore += key_score; + LogFileUtils.writeLine("❌ 关键字没有达到临界值,正确数量:"+ true_number); + LogFileUtils.close(); + return totalScore; + } + } + + /** + * + * @param code 源码 + * @return 运行结果 + */ + @Override + public String run_test_result(String code){ + // 使用C99 运行并得出结果 + return JudgementCUtils.run_test_code(code,"-std=c99"); + } +} diff --git a/exam-module-judgement/exam-module-judgement-biz/src/main/java/pc/exam/pp/module/judgement/service/choice/JudgementChoiceService.java b/exam-module-judgement/exam-module-judgement-biz/src/main/java/pc/exam/pp/module/judgement/service/choice/JudgementChoiceService.java new file mode 100644 index 00000000..f1cfeddd --- /dev/null +++ b/exam-module-judgement/exam-module-judgement-biz/src/main/java/pc/exam/pp/module/judgement/service/choice/JudgementChoiceService.java @@ -0,0 +1,22 @@ +package pc.exam.pp.module.judgement.service.choice; + + +import pc.exam.pp.module.exam.dal.dataobject.ExamQuestion; + +/** + * 判分逻辑集合(选择题) + * + * @author rwb + */ +public interface JudgementChoiceService { + + + /** + * + * @param score 题分 + * @param examQuestion 选择题内容(学生答题) + * @param originalExamQuestion 原始题(判断答案) + * @return 得分 + */ + public double ProgrammingChoice(double score, ExamQuestion examQuestion, ExamQuestion originalExamQuestion); +} diff --git a/exam-module-judgement/exam-module-judgement-biz/src/main/java/pc/exam/pp/module/judgement/service/choice/JudgementChoiceServiceImpl.java b/exam-module-judgement/exam-module-judgement-biz/src/main/java/pc/exam/pp/module/judgement/service/choice/JudgementChoiceServiceImpl.java new file mode 100644 index 00000000..e862519e --- /dev/null +++ b/exam-module-judgement/exam-module-judgement-biz/src/main/java/pc/exam/pp/module/judgement/service/choice/JudgementChoiceServiceImpl.java @@ -0,0 +1,54 @@ +package pc.exam.pp.module.judgement.service.choice; + +import org.springframework.stereotype.Service; +import pc.exam.pp.module.exam.dal.dataobject.ExamQuestion; +import pc.exam.pp.module.exam.dal.dataobject.ExamQuestionAnswer; + +/** + * 判分逻辑集合 + * + * @author rwb + */ +@Service +public class JudgementChoiceServiceImpl implements JudgementChoiceService +{ + + /** + * 选择题判分 + * @param examQuestion 选择题内容(学生答题) + * @param originalExamQuestion 原始题(判断答案) + * @param score 分数 + * @return 返回判分 + */ + public double ProgrammingChoice(double score, ExamQuestion examQuestion, ExamQuestion originalExamQuestion) { + // 总分重置 + double totalScore = 0; + String right_id = null; + String stu_right_id = null; + // 获取选择题的标准答案 + // 找到对应正确题的答案ID + for (ExamQuestionAnswer examQuestionAnswer : originalExamQuestion.getAnswerList()) { + // 判断正确答案 + if ("0".equals(examQuestionAnswer.getIsRight())) { + // 正确答案 + right_id = examQuestionAnswer.getAnswerId(); + break; + } + } + // 获取学生的做题答案,进行判断是否一致 + for (ExamQuestionAnswer examQuestionAnswer : examQuestion.getAnswerList()) { + // 判断正确答案 + if ("0".equals(examQuestionAnswer.getIsRight())) { + // 正确答案 + stu_right_id = examQuestionAnswer.getAnswerId(); + break; + } + } + // 判断 + if (stu_right_id.equals(right_id)) { + totalScore += score; + } + // 该题得分返回 + return totalScore; + } +} diff --git a/exam-module-judgement/exam-module-judgement-biz/src/main/java/pc/exam/pp/module/judgement/utils/JudgementCUtils.java b/exam-module-judgement/exam-module-judgement-biz/src/main/java/pc/exam/pp/module/judgement/utils/JudgementCUtils.java new file mode 100644 index 00000000..9f94b335 --- /dev/null +++ b/exam-module-judgement/exam-module-judgement-biz/src/main/java/pc/exam/pp/module/judgement/utils/JudgementCUtils.java @@ -0,0 +1,262 @@ +package pc.exam.pp.module.judgement.utils; + + +import javax.swing.*; +import java.io.*; +import java.nio.charset.StandardCharsets; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.Paths; +import java.util.List; +import java.util.concurrent.*; +import java.util.stream.Stream; + +/** + * C语言 相关工具类 + * + * @author pengchen + */ +public class JudgementCUtils +{ + /** + * 判断程序是否安装GCC 编译环境 + * @return true or false + */ + public static boolean isGCCInstalled() { + try { + ProcessBuilder builder = new ProcessBuilder("gcc", "--version"); + Process process = builder.start(); + process.waitFor(); + return process.exitValue() == 0; + } catch (Exception e) { + return false; + } + } + + /** + * 安装GCC编译环境 + * @param os 操作系统 + * @return 安装话术 + */ + public static String installGCC(String os) { + try { + ProcessBuilder builder; + if (os.equals("windows")) { + builder = new ProcessBuilder("cmd.exe", "/c", "choco install mingw"); + } else { + builder = new ProcessBuilder("bash", "-c", "sudo apt-get install gcc -y"); + } + builder.redirectErrorStream(true); + Process process = builder.start(); + process.waitFor(); + JOptionPane.showMessageDialog(null, "GCC installation completed for " + os); + } catch (Exception ex) { + JOptionPane.showMessageDialog(null, "Installation failed: " + ex.getMessage()); + throw new RuntimeException("Installation failed: " + ex.getMessage()); + } + return "GCC installation completed for " + os; + } + + /** + * 运行C语言代码 + * @param code C语言代码 + * @param input 测试用例插入值(如有) + * @param standard 编译版本 C99 + * @return 运行结果 + */ + public static String run_code(String code, String input, String standard, String text) { + try { + boolean hasInput = code.contains("scanf") || code.contains("fgets") || code.contains("getchar"); + + // 写入 C 源码到文件 + File file = new File("program.c"); + try (FileWriter writer = new FileWriter(file)) { + writer.write(code); + } + + // 编译代码 + ProcessBuilder compileBuilder = new ProcessBuilder("gcc", standard, "program.c", "-o", "program.out"); + compileBuilder.redirectErrorStream(true); + Process compileProcess = compileBuilder.start(); + + StringBuilder compileOutput = new StringBuilder(); + try (BufferedReader compileReader = new BufferedReader(new InputStreamReader(compileProcess.getInputStream()))) { + String line; + while ((line = compileReader.readLine()) != null) { + compileOutput.append(line).append("\n"); + } + } + + int compileResult = compileProcess.waitFor(); + String outputLower = compileOutput.toString().toLowerCase(); + if (outputLower.contains("error:") || !Files.exists(Paths.get("program.out"))) { +// LogFileUtils.writeLine("❌ 编译失败:"); +// LogFileUtils.writeLine(compileOutput.toString()); + return "编译失败,输出:\n" + compileOutput.toString(); + } + + // 运行程序 + ProcessBuilder runBuilder = new ProcessBuilder("./program.out"); + runBuilder.redirectErrorStream(true); + Process runProcess = runBuilder.start(); + + // 输入注入(如有) + if (hasInput && input != null) { + try (BufferedWriter inputWriter = new BufferedWriter(new OutputStreamWriter(runProcess.getOutputStream()))) { + inputWriter.write(input); + inputWriter.newLine(); + inputWriter.flush(); + } + } + + // 使用线程池处理带超时的输出读取 + ExecutorService executor = Executors.newSingleThreadExecutor(); + Future future = executor.submit(() -> { + StringBuilder output = new StringBuilder(); + try (BufferedReader reader = new BufferedReader(new InputStreamReader(runProcess.getInputStream()))) { + String line; + while ((line = reader.readLine()) != null) { + output.append(line).append("\n"); + } + } + return output.toString(); + }); + + String output; + try { + output = future.get(5, TimeUnit.SECONDS); + } catch (TimeoutException e) { +// LogFileUtils.writeLine("⏰ 程序超时,强制终止进程!"); + runProcess.destroy(); + future.cancel(true); + output = "程序运行超时(超过 5 秒)"; + } finally { + executor.shutdownNow(); + } + +// if (text != null) { +// LogFileUtils.writeLine("✅ 程序运行完成,输出:" + text); +// } else { +// LogFileUtils.writeLine("✅ 程序运行完成,输出:" + output.trim()); +// } + + return output; + + } catch (Exception ex) { + ex.printStackTrace(); +// LogFileUtils.writeLine("❌ 运行 C 代码时出错:" + ex.getMessage()); + return "运行 C 代码时出错:" + ex.getMessage(); + } + } + + /** + * 运行C语言代码,出题测试运行 + * @param code C语言代码 + * @param standard 编译版本 C99 + * @return 运行结果 + */ + public static String run_test_code(String code, String standard) { + try { + // 写入 C 语言源码到文件 + File file = new File("program.c"); + try (FileWriter writer = new FileWriter(file)) { + writer.write(code); + } + + // 编译 C 代码 + ProcessBuilder compileBuilder = new ProcessBuilder("gcc", standard, "program.c", "-o", "program.out"); + compileBuilder.redirectErrorStream(true); + Process compileProcess = compileBuilder.start(); + + StringBuilder compileOutput = new StringBuilder(); + try (BufferedReader compileReader = new BufferedReader(new InputStreamReader(compileProcess.getInputStream()))) { + String line; + while ((line = compileReader.readLine()) != null) { + compileOutput.append(line).append("\n"); + } + } + + int compileResult = compileProcess.waitFor(); + + // 检查编译是否成功 + String outputLower = compileOutput.toString().toLowerCase(); + if (outputLower.contains("error:") || !Files.exists(Paths.get("program.out"))) { + return "编译失败,输出:\n" + compileOutput.toString(); + } + + // 运行程序并添加超时机制 + ProcessBuilder runBuilder = new ProcessBuilder("./program.out"); + runBuilder.redirectErrorStream(true); + Process runProcess = runBuilder.start(); + + ExecutorService executor = Executors.newSingleThreadExecutor(); + Future future = executor.submit(() -> { + StringBuilder output = new StringBuilder(); + try (BufferedReader reader = new BufferedReader(new InputStreamReader(runProcess.getInputStream()))) { + String line; + while ((line = reader.readLine()) != null) { + output.append(line).append("\n"); + } + } + return output.toString(); + }); + + String output; + try { + output = future.get(5, TimeUnit.SECONDS); // 设置 5 秒超时 + } catch (TimeoutException e) { + runProcess.destroy(); + future.cancel(true); + output = "程序运行超时(超过 5 秒)"; + } finally { + executor.shutdownNow(); + } + + return output; + } catch (Exception ex) { + ex.printStackTrace(); + return "运行 C 代码时出错:" + ex.getMessage(); + } + } + + + + /** + * 读取文件代码 + * @param filePath 文件路径 + * @return 文件内容 + */ + public static String readFile(String filePath, String fileName) { + // 创建一个 Path 对象,指向包含 C 语言文件的文件夹 + Path folderPath = Paths.get(filePath); + + // 判断路径是否存在 + if (!Files.exists(folderPath)) { +// LogFileUtils.writeLine("❗ 文件夹路径不存在: " + folderPath.toString()); + return null; // 或者 return null,或者 throw new RuntimeException(...),看你的函数定义 + } + + // 使用 StringBuilder 来累积所有读取到的代码内容 + StringBuilder codeBuilder = new StringBuilder(); + + try (Stream paths = Files.walk(folderPath)) { + paths.filter(Files::isRegularFile) + .filter(path -> path.toString().toLowerCase().endsWith(fileName)) + .forEach(path -> { +// LogFileUtils.writeLine("📄 文件: " + path); + try { + List lines = Files.readAllLines(path, StandardCharsets.UTF_8); + for (String line : lines) { + codeBuilder.append("\n").append(line); + System.out.println(" " + line); + } + } catch (IOException e) { +// LogFileUtils.writeLine("❗ 无法读取文件: " + path); + } + }); + } catch (IOException e) { + return null; + } + return codeBuilder.toString(); + } +}