xiatian 3 ay önce
ebeveyn
işleme
fff010591c

+ 5 - 1
pom.xml

@@ -81,7 +81,11 @@
             <artifactId>tools-poi</artifactId>
             <version>${qmth-boot-version}</version>
         </dependency>
-
+		<dependency>
+			<groupId>com.qmth.boot</groupId>
+			<artifactId>tools-freemarker</artifactId>
+			<version>${qmth-boot-version}</version>
+		</dependency>
 
         <!-- Swagger jars start -->
         <dependency>

+ 27 - 0
src/main/java/cn/com/qmth/am/bean/ds/ChatContent.java

@@ -0,0 +1,27 @@
+package cn.com.qmth.am.bean.ds;
+
+import com.qmth.boot.core.ai.model.llm.ChatRole;
+
+public class ChatContent {
+
+    private ChatRole role;
+
+    private String content;
+
+    public ChatRole getRole() {
+        return role;
+    }
+
+    public void setRole(ChatRole role) {
+        this.role = role;
+    }
+
+    public String getContent() {
+        return content;
+    }
+
+    public void setContent(String content) {
+        this.content = content;
+    }
+
+}

+ 17 - 0
src/main/java/cn/com/qmth/am/bean/ds/ChatResult.java

@@ -0,0 +1,17 @@
+package cn.com.qmth.am.bean.ds;
+
+import java.util.List;
+
+public class ChatResult {
+
+    private List<DsChoice> choices;
+
+    public List<DsChoice> getChoices() {
+        return choices;
+    }
+
+    public void setChoices(List<DsChoice> choices) {
+        this.choices = choices;
+    }
+
+}

+ 25 - 0
src/main/java/cn/com/qmth/am/bean/ds/DsChoice.java

@@ -0,0 +1,25 @@
+package cn.com.qmth.am.bean.ds;
+
+public class DsChoice {
+
+    private ChatContent message;
+
+    private String finish_reason;
+
+    public ChatContent getMessage() {
+        return message;
+    }
+
+    public void setMessage(ChatContent message) {
+        this.message = message;
+    }
+
+    public String getFinish_reason() {
+        return finish_reason;
+    }
+
+    public void setFinish_reason(String finish_reason) {
+        this.finish_reason = finish_reason;
+    }
+
+}

+ 46 - 0
src/main/java/cn/com/qmth/am/bean/ds/ReqMessages.java

@@ -0,0 +1,46 @@
+package cn.com.qmth.am.bean.ds;
+
+import java.util.ArrayList;
+import java.util.List;
+
+import com.qmth.boot.core.ai.model.llm.ChatRole;
+
+import cn.com.qmth.am.enums.ModelType;
+
+public class ReqMessages {
+
+    private List<ChatContent> messages;
+
+    private String model;
+
+    public ReqMessages(ModelType modelType) {
+        super();
+        this.model = modelType.getCode();
+    }
+
+    public List<ChatContent> getMessages() {
+        return messages;
+    }
+
+    public void setMessages(List<ChatContent> messages) {
+        this.messages = messages;
+    }
+
+    public String getModel() {
+        return model;
+    }
+
+    public void setModel(String model) {
+        this.model = model;
+    }
+
+    public void addMsg(ChatRole role, String msg) {
+        if (messages == null) {
+            messages = new ArrayList<>();
+        }
+        ChatContent cc = new ChatContent();
+        cc.setRole(role);
+        cc.setContent(msg);
+        messages.add(cc);
+    }
+}

+ 74 - 40
src/main/java/cn/com/qmth/am/config/SysProperty.java

@@ -4,10 +4,11 @@ import org.springframework.beans.factory.annotation.Value;
 import org.springframework.stereotype.Component;
 
 import cn.com.qmth.am.enums.DataType;
+import cn.com.qmth.am.enums.ModelType;
 
 @Component
 public class SysProperty {
-	
+
     @Value("${com.qmth.solar.app-version}")
     private String version;
 
@@ -16,62 +17,95 @@ public class SysProperty {
 
     @Value("${am.ocr-task.enable}")
     private Boolean ocrTaskEnable;
-    
+
     @Value("${am.marking-task.enable}")
     private Boolean markingTaskEnable;
-    
+
     @Value("${am.image-server}")
     private String imageServer;
 
     @Value("${am.data-type}")
     private DataType dataType;
 
-	public String getVersion() {
-		return version;
-	}
+    @Value("${am.marking-model}")
+    private ModelType markingModel;
+
+    @Value("${am.marking-key:none}")
+    private String markingKey;
+
+    @Value("${am.marking-server:none}")
+    private String markingServer;
+
+    public String getVersion() {
+        return version;
+    }
+
+    public void setVersion(String version) {
+        this.version = version;
+    }
+
+    public String getImageServer() {
+        return imageServer;
+    }
+
+    public void setImageServer(String imageServer) {
+        this.imageServer = imageServer;
+    }
+
+    public DataType getDataType() {
+        return dataType;
+    }
+
+    public void setDataType(DataType dataType) {
+        this.dataType = dataType;
+    }
+
+    public String getDataDir() {
+        return dataDir;
+    }
+
+    public void setDataDir(String dataDir) {
+        this.dataDir = dataDir;
+    }
 
-	public void setVersion(String version) {
-		this.version = version;
-	}
+    public Boolean getOcrTaskEnable() {
+        return ocrTaskEnable;
+    }
 
-	public String getImageServer() {
-		return imageServer;
-	}
+    public void setOcrTaskEnable(Boolean ocrTaskEnable) {
+        this.ocrTaskEnable = ocrTaskEnable;
+    }
 
-	public void setImageServer(String imageServer) {
-		this.imageServer = imageServer;
-	}
+    public Boolean getMarkingTaskEnable() {
+        return markingTaskEnable;
+    }
 
-	public DataType getDataType() {
-		return dataType;
-	}
+    public void setMarkingTaskEnable(Boolean markingTaskEnable) {
+        this.markingTaskEnable = markingTaskEnable;
+    }
 
-	public void setDataType(DataType dataType) {
-		this.dataType = dataType;
-	}
+    public ModelType getMarkingModel() {
+        return markingModel;
+    }
 
-	public String getDataDir() {
-		return dataDir;
-	}
+    public void setMarkingModel(ModelType markingModel) {
+        this.markingModel = markingModel;
+    }
 
-	public void setDataDir(String dataDir) {
-		this.dataDir = dataDir;
-	}
+    public String getMarkingKey() {
+        return markingKey;
+    }
 
-	public Boolean getOcrTaskEnable() {
-		return ocrTaskEnable;
-	}
+    public void setMarkingKey(String markingKey) {
+        this.markingKey = markingKey;
+    }
 
-	public void setOcrTaskEnable(Boolean ocrTaskEnable) {
-		this.ocrTaskEnable = ocrTaskEnable;
-	}
+    public String getMarkingServer() {
+        return markingServer;
+    }
 
-	public Boolean getMarkingTaskEnable() {
-		return markingTaskEnable;
-	}
+    public void setMarkingServer(String markingServer) {
+        this.markingServer = markingServer;
+    }
 
-	public void setMarkingTaskEnable(Boolean markingTaskEnable) {
-		this.markingTaskEnable = markingTaskEnable;
-	}
-	
 }

+ 18 - 0
src/main/java/cn/com/qmth/am/enums/ModelType.java

@@ -0,0 +1,18 @@
+package cn.com.qmth.am.enums;
+
+public enum ModelType {
+
+    qwen_max("qwen_max"), qwen2_5("Qwen2.5-32B-Instruct-GPTQ-Int4"), ds_r1("deepseek-r1-distill-qwen-32b-awq"), glm4_9(
+            "glm-4-9b-chat"),;
+
+    private ModelType(String code) {
+        this.code = code;
+    }
+
+    private String code;
+
+    public String getCode() {
+        return code;
+    }
+
+}

+ 13 - 0
src/main/java/cn/com/qmth/am/service/DsMarkingService.java

@@ -0,0 +1,13 @@
+package cn.com.qmth.am.service;
+
+import com.qmth.boot.core.ai.model.llm.AutoScoreRequest;
+import com.qmth.boot.core.ai.model.llm.AutoScoreResult;
+
+/**
+ * 类注释
+ */
+public interface DsMarkingService {
+
+    AutoScoreResult autoScore(AutoScoreRequest req);
+
+}

+ 14 - 14
src/main/java/cn/com/qmth/am/service/StudentService.java

@@ -16,31 +16,31 @@ import cn.com.qmth.am.enums.DataStatus;
 /**
  * 类注释
  */
-public interface StudentService  extends IService<StudentEntity> {
+public interface StudentService extends IService<StudentEntity> {
 
-	void importStudent();
+    void importStudent();
 
-	ImportResult disposeFile(InputStream inputStream);
+    ImportResult disposeFile(InputStream inputStream);
 
-	List<StudentEntity> findToDispose();
+    List<StudentEntity> findToDispose();
 
-	void buildImage(StudentEntity student, Map<Long,QuestionEntity> quetions);
+    void buildImage(StudentEntity student, Map<Long, QuestionEntity> quetions);
 
-	List<StudentScoreEntity> getOrCreateScores(StudentEntity student, Map<Long,QuestionEntity> quetions);
+    List<StudentScoreEntity> getOrCreateScores(StudentEntity student, Map<Long, QuestionEntity> quetions);
 
-	void createSlice(StudentScoreEntity score, Map<Long, QuestionEntity> quetions,Map<Integer,AnswerImageDto> answerImages);
+    void createSlice(StudentScoreEntity score, Map<Long, QuestionEntity> quetions,
+            Map<Integer, AnswerImageDto> answerImages);
 
-	void updateStatus(Long id,DataStatus to);
+    void updateStatus(Long id, DataStatus to);
 
-	void resetStatus();
+    void resetStatus();
 
-	int countBy(Long examId, DataStatus success);
+    int countBy(Long examId, DataStatus success);
 
-	void reset(Long examId, String subjectCode);
+    void reset(Long examId, String subjectCode);
 
-	void clear(Long examId, String subjectCode);
-
-	List<StudentEntity> findToMrking();
+    void clear(Long examId, String subjectCode);
 
+    List<StudentEntity> findToMarking(Long examId);
 
 }

+ 120 - 0
src/main/java/cn/com/qmth/am/service/impl/DsMarkingServiceImpl.java

@@ -0,0 +1,120 @@
+package cn.com.qmth.am.service.impl;
+
+import java.math.BigDecimal;
+import java.math.RoundingMode;
+import java.util.Arrays;
+import java.util.HashMap;
+import java.util.Map;
+
+import org.apache.commons.io.IOUtils;
+import org.apache.commons.lang3.StringUtils;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.stereotype.Service;
+
+import com.alibaba.fastjson.JSONObject;
+import com.qmth.boot.core.ai.model.llm.AutoScoreRequest;
+import com.qmth.boot.core.ai.model.llm.AutoScoreResult;
+import com.qmth.boot.core.ai.model.llm.ChatRole;
+import com.qmth.boot.core.retrofit.exception.RetrofitResponseError;
+
+import cn.com.qmth.am.bean.ds.ChatResult;
+import cn.com.qmth.am.bean.ds.ReqMessages;
+import cn.com.qmth.am.config.SysProperty;
+import cn.com.qmth.am.service.DsMarkingService;
+import cn.com.qmth.am.utils.FreeMarkerUtil;
+import cn.com.qmth.am.utils.HttpMethod;
+import cn.com.qmth.am.utils.OKHttpUtil;
+import okhttp3.Response;
+
+@Service
+public class DsMarkingServiceImpl implements DsMarkingService {
+
+    @Autowired
+    private SysProperty sysProperty;
+
+    private static final Logger log = LoggerFactory.getLogger(DsMarkingService.class);
+
+    @SuppressWarnings("deprecation")
+    public static void main(String[] args) {
+
+        Map<String, String> headers = new HashMap<>();
+        headers.put("Authorization", "Bearer 7dac2f2166994b8f9c6de0a8eff2814c");
+        Response resp = null;
+        try {
+            resp = OKHttpUtil.call(HttpMethod.POST, "http://39.174.90.3:31091/spiritx-api/v1/chat/completions", headers,
+                    "{\"model\":\"deepseek-r1-distill-qwen-32b-awq\",\"messages\":[{\"role\":\"user\",\"content\":\"你是谁?\"}]}");
+            if (resp.code() != 200) {
+                throw new RuntimeException("err :" + resp.body().string());
+            } else {
+                System.out.println("成功处理:" + resp.body().string());
+            }
+        } catch (Exception e) {
+            throw new RuntimeException(e);
+        } finally {
+            IOUtils.closeQuietly(resp);
+        }
+    }
+
+    @Override
+    public AutoScoreResult autoScore(AutoScoreRequest request) {
+        String question = FreeMarkerUtil.getDsMarkingReq(request);
+        ReqMessages dreq = new ReqMessages(sysProperty.getMarkingModel());
+        dreq.addMsg(ChatRole.user, question);
+        String res = chat(dreq);
+        ChatResult result = JSONObject.parseObject(res, ChatResult.class);
+        String text = result.getChoices().stream().filter(choice -> choice.getMessage().getRole() == ChatRole.assistant)
+                .map(choice -> choice.getMessage().getContent()).findFirst().orElse("");
+        try {
+            AutoScoreResult scoreResult = new AutoScoreResult();
+            // 依据总分与步骤分计算最大精度
+            int scale = Math.max(getDecimalPlaces(request.getIntervalScore()),
+                    getDecimalPlaces(request.getTotalScore()));
+            int stepCount = request.getStandardAnswer().size();
+            String scoreStr = text.substring(text.lastIndexOf("\n") + 1).trim();
+            String[] scores = StringUtils.split(scoreStr.replaceAll(",", ",").replaceAll("[0-9]\\.", ""), ",");
+            double[] scoreArray = new double[stepCount];
+            for (int i = 0; i < stepCount; i++) {
+                // 根据得分率与步骤总分计算实际得分,按最大精度保留小数位数
+                double score = BigDecimal
+                        .valueOf(Math.min(Integer.parseInt(scores[i].trim()), 100)
+                                * request.getStandardAnswer().get(i).getScore())
+                        .divide(BigDecimal.valueOf(100), scale, RoundingMode.HALF_UP).doubleValue();
+                scoreArray[i] = score;
+            }
+            scoreResult.setStepScore(scoreArray);
+            scoreResult.setTotalScore(Arrays.stream(scoreArray).mapToObj(BigDecimal::new)
+                    .reduce(BigDecimal.ZERO, BigDecimal::add).doubleValue());
+            return scoreResult;
+        } catch (Exception e) {
+            log.error(e.getMessage() + " | " + res);
+            return null;
+        }
+    }
+
+    private int getDecimalPlaces(double value) {
+        return Math.max(0, BigDecimal.valueOf(value).stripTrailingZeros().scale());
+    }
+
+    @SuppressWarnings("deprecation")
+    private String chat(ReqMessages dreq) {
+
+        Map<String, String> headers = new HashMap<>();
+        headers.put("Authorization", "Bearer " + sysProperty.getMarkingKey());
+        Response resp = null;
+        try {
+            resp = OKHttpUtil.call(HttpMethod.POST, sysProperty.getMarkingServer(), headers,
+                    JSONObject.toJSONString(dreq));
+            if (resp.code() != 200) {
+                throw new RetrofitResponseError(resp.code(), resp.body().string());
+            } else {
+                return resp.body().string();
+            }
+        } catch (Exception e) {
+            throw new RetrofitResponseError(resp.code(), e.getMessage(), e);
+        } finally {
+            IOUtils.closeQuietly(resp);
+        }
+    }
+}

+ 15 - 2
src/main/java/cn/com/qmth/am/service/impl/StudentScoreServiceImpl.java

@@ -60,6 +60,8 @@ import cn.com.qmth.am.entity.StudentScoreEntity;
 import cn.com.qmth.am.enums.DataStatus;
 import cn.com.qmth.am.enums.DataType;
 import cn.com.qmth.am.enums.ImportFileName;
+import cn.com.qmth.am.enums.ModelType;
+import cn.com.qmth.am.service.DsMarkingService;
 import cn.com.qmth.am.service.QuestionService;
 import cn.com.qmth.am.service.StudentScoreService;
 import cn.com.qmth.am.service.StudentService;
@@ -90,6 +92,9 @@ public class StudentScoreServiceImpl extends ServiceImpl<StudentScoreDao, Studen
     @Autowired
     private AiService aiService;
 
+    @Autowired
+    private DsMarkingService dsMarkingService;
+
     @Autowired
     private QuestionService questionService;
     static {
@@ -692,9 +697,17 @@ public class StudentScoreServiceImpl extends ServiceImpl<StudentScoreDao, Studen
     }
 
     private AutoScoreResult aiMarkingDispose(AiMarkingDto dto, OrgInfo org, AutoScoreRequest req) {
-        SignatureInfo signature = SignatureInfo.secret(org.getAccessKey(), org.getAccessSecret());
+        SignatureInfo signature = null;
+        if (ModelType.qwen_max.equals(sysProperty.getMarkingModel())) {
+            signature = SignatureInfo.secret(org.getAccessKey(), org.getAccessSecret());
+        }
         try {
-            return aiService.autoScore(req, signature);
+            if (ModelType.qwen_max.equals(sysProperty.getMarkingModel())) {
+                return aiService.autoScore(req, signature);
+            } else {
+                return dsMarkingService.autoScore(req);
+            }
+
         } catch (Exception e) {
             log.error("aiScore异常", e);
             if (e instanceof RetrofitResponseError) {

+ 338 - 328
src/main/java/cn/com/qmth/am/service/impl/StudentServiceImpl.java

@@ -48,332 +48,342 @@ import cn.com.qmth.am.service.StudentService;
 
 @Service
 public class StudentServiceImpl extends ServiceImpl<StudentDao, StudentEntity> implements StudentService {
-	private static final Logger log = LoggerFactory.getLogger(StudentService.class);
-	private static final String[] EXCEL_HEADER = new String[] { "考试ID", "科目代码","考生编号" };
-	@Autowired
-	private SysProperty sysProperty;
-	@Autowired
-	private StudentService studentService;
-	@Autowired
-	private StudentScoreService studentScoreService;
-	@Autowired
-	private QuestionService questionService;
-
-	@Override
-	public void importStudent() {
-		File dir = new File(sysProperty.getDataDir());
-		File[] fs = dir.listFiles();
-		if (fs == null || fs.length == 0) {
-			return;
-		}
-		for (File file : fs) {
-			if (!file.isFile() || !file.getName().equals(ImportFileName.STUDENT_IMPORT.getName())) {
-				continue;
-			}
-			InputStream inputStream = null;
-			ImportResult ret=null;
-			try {
-				inputStream = new FileInputStream(file);
-				ret = studentService.disposeFile(inputStream);
-			} catch (Exception e) {
-				String errMsg;
-				if(e instanceof FileNotFoundException) {
-					errMsg="未找到文件:" + file.getAbsolutePath();
-				}else {
-					errMsg="系统错误:" + e.getMessage();
-				}
-				ret=new ImportResult(errMsg);
-			} finally {
-				if (inputStream != null) {
-					try {
-						inputStream.close();
-					} catch (IOException e) {
-					}
-				}
-			}
-			moveFile(dir,file,ret);
-		}
-	}
-
-	private void moveFile(File dir,File file, ImportResult ret) {
-		try {
-			boolean succss=CollectionUtils.isEmpty(ret.getErrMsg());
-			if(succss) {
-				File sucDir=new File(dir.getAbsoluteFile()+"/success/");
-				if(!sucDir.exists()) {
-					sucDir.mkdir();
-				}
-				File targetFile=new File(sucDir.getAbsoluteFile()+"/"+file.getName());
-				if(targetFile.exists()) {
-					targetFile.delete();
-				}
-				FileUtils.copyFile(file, targetFile);
-				file.delete();
-				String fname=file.getName().substring(0, file.getName().lastIndexOf("."));
-				File msgFile=new File(sucDir.getAbsoluteFile()+"/"+fname+"_info.txt");
-				if(msgFile.exists()) {
-					msgFile.delete();
-				}
-				FileUtils.write(msgFile, ret.getCountInfo(), "utf-8");
-			}else {
-				File sucDir=new File(dir.getAbsoluteFile()+"/failed/");
-				if(!sucDir.exists()) {
-					sucDir.mkdir();
-				}
-				File targetFile=new File(sucDir.getAbsoluteFile()+"/"+file.getName());
-				if(targetFile.exists()) {
-					targetFile.delete();
-				}
-				FileUtils.copyFile(file, targetFile);
-				file.delete();
-				String fname=file.getName().substring(0, file.getName().lastIndexOf("."));
-				File msgFile=new File(sucDir.getAbsoluteFile()+"/"+fname+"_info.txt");
-				if(msgFile.exists()) {
-					msgFile.delete();
-				}
-				FileUtils.writeLines(msgFile, StandardCharsets.UTF_8.name(), ret.getErrMsg());
-			}
-		} catch (IOException e) {
-			throw new StatusException("文件处理出错", e);
-		}
-		
-	}
-	
-
-	private String errorMsg(int lineNum, String msg) {
-		return "第" + lineNum + "行 " + msg;
-	}
-
-	private String trimAndNullIfBlank(String s) {
-		if (StringUtils.isBlank(s)) {
-			return null;
-		}
-		return s.trim();
-	}
-
-	@Transactional
-	@Override
-	public ImportResult disposeFile(InputStream inputStream) {
-		List<DataMap> lineList = null;
-		ExcelReader reader = ExcelReader.create(ExcelType.XLSX, inputStream, 0);
-		try {
-			lineList = reader.getDataMapList();
-		} catch (Exception e) {
-			throw new StatusException("Excel 解析失败");
-		}
-		if (!Arrays.equals(EXCEL_HEADER, reader.getColumnNames())) {
-			throw new StatusException("Excel表头错误");
-		}
-		if (CollectionUtils.isEmpty(lineList)) {
-			throw new StatusException("Excel无内容");
-		}
-		if (100001 < lineList.size()) {
-			throw new StatusException("数据行数不能超过100000");
-		}
-		List<StudentEntity> ss = new ArrayList<>();
-		ImportResult ret = new ImportResult();
-		List<String> failRecords = new ArrayList<>();
-		ret.setErrMsg(failRecords);
-		for (int i = 0; i < lineList.size(); i++) {
-			DataMap line = lineList.get(i);
-
-			StringBuilder msg = new StringBuilder();
-
-			StudentEntity imp = new StudentEntity();
-			imp.setDataStatus(DataStatus.WAITING);
-			String examId = trimAndNullIfBlank(line.get(EXCEL_HEADER[0]));
-			if (StringUtils.isBlank(examId)) {
-				msg.append("  考试ID不能为空");
-			} else if (examId.length() > 20) {
-				msg.append("  考试ID不能超过20个字符");
-			} else {
-				try {
-					Long examIdVal = Long.parseLong(examId);
-					imp.setExamId(examIdVal);
-				} catch (NumberFormatException e) {
-					msg.append("  考试ID只能是数字");
-				}
-			}
-
-			String subjectCode = trimAndNullIfBlank(line.get(EXCEL_HEADER[1]));
-			if (StringUtils.isBlank(subjectCode)) {
-				msg.append("  科目代码不能为空");
-			} else if (subjectCode.length() > 100) {
-				msg.append("  科目代码不能超过100个字符");
-			}
-			imp.setSubjectCode(subjectCode);
-
-			String studentCode = trimAndNullIfBlank(line.get(EXCEL_HEADER[2]));
-			if (StringUtils.isBlank(studentCode)) {
-				msg.append("  考生编号不能为空");
-			} else if (studentCode.length() > 100) {
-				msg.append("  考生编号不能超过100个字符");
-			}
-			imp.setStudentCode(studentCode);
-
-			if (msg.length() > 0) {
-				failRecords.add(errorMsg(i + 2, msg.toString()));
-			} else {
-				ss.add(imp);
-			}
-
-		}
-
-		if (CollectionUtils.isNotEmpty(failRecords)) {
-			return ret;
-		}
-		try {
-			saveStudentBatch(ret,ss);
-		} catch (Exception e) {
-			failRecords.add("系统错误:" + e.getMessage());
-		}
-		return ret;
-	}
-
-	private void saveStudentBatch(ImportResult ret,List<StudentEntity> ss) {
-		if (CollectionUtils.isEmpty(ss)) {
-			ret.setCountInfo("新增数量:0");
-			return;
-		}
-		List<StudentEntity> all = this.list();
-		Set<String> set = new HashSet<>();
-		if (CollectionUtils.isNotEmpty(all)) {
-			for (StudentEntity s : all) {
-				String key = s.getExamId() + "-" + s.getSubjectCode() + "-" + s.getStudentCode();
-				set.add(key);
-			}
-		}
-		List<StudentEntity> adds = new ArrayList<>();
-		for (StudentEntity s : ss) {
-			String key = s.getExamId() + "-" + s.getSubjectCode() + "-" + s.getStudentCode();
-			if (!set.contains(key)) {
-				adds.add(s);
-			}else {
-				set.add(key);
-			}
-		}
-		if (CollectionUtils.isNotEmpty(adds)) {
-			saveBatch(adds);
-		}
-		ret.setCountInfo("新增数量:"+adds.size());
-	}
-
-	@Override
-	public List<StudentEntity> findToDispose() {
-		QueryWrapper<StudentEntity> wrapper = new QueryWrapper<>();
-		LambdaQueryWrapper<StudentEntity> lw = wrapper.lambda();
-		lw.in(StudentEntity::getDataStatus, DataStatus.WAITING,DataStatus.FAILED);
-		return this.list(wrapper);
-	}
-	
-	@Override
-	public List<StudentEntity> findToMrking() {
-		QueryWrapper<StudentEntity> wrapper = new QueryWrapper<>();
-		LambdaQueryWrapper<StudentEntity> lw = wrapper.lambda();
-		lw.in(StudentEntity::getDataStatus, DataStatus.WAITING,DataStatus.FAILED,DataStatus.PROCESSING);
-		return this.list(wrapper);
-	}
-
-	@Override
-	public void buildImage(StudentEntity student, Map<Long,QuestionEntity> quetions) {
-		List<StudentScoreEntity> scores=studentService.getOrCreateScores(student, quetions);
-		Map<Integer,AnswerImageDto> answerImages=new HashMap<>();
-		for(StudentScoreEntity score:scores) {
-			if(DataStatus.WAITING.equals(score.getAnswerStatus())||DataStatus.FAILED.equals(score.getAnswerStatus())) {
-				studentService.createSlice(score,quetions,answerImages);
-			}
-		}
-	}
-	
-	@Transactional
-	@Override
-	public void createSlice(StudentScoreEntity score, Map<Long,QuestionEntity> quetions,Map<Integer,AnswerImageDto> answerImages) {
-		QuestionEntity q=quetions.get(score.getQuestionId());
-		if(q==null) {
-			studentScoreService.updateAnswerErr(score.getId(),"未找到试题信息");
-			updateStatus(score.getStudentId(),DataStatus.FAILED);
-			return;
-		}
-		try {
-			studentScoreService.createSlice(score,q,answerImages);
-		} catch (Exception e) {
-			if(e instanceof StatusException) {
-				studentScoreService.updateAnswerErr(score.getId(),e.getMessage());
-				updateStatus(score.getStudentId(),DataStatus.FAILED);
-			}else {
-				log.error("系统异常",e);
-				studentScoreService.updateAnswerErr(score.getId(),"系统异常");
-				updateStatus(score.getStudentId(),DataStatus.FAILED);
-			}
-		}
-	}
-
-	@Transactional
-	@Override
-	public List<StudentScoreEntity> getOrCreateScores(StudentEntity student, Map<Long,QuestionEntity> quetions) {
-		List<StudentScoreEntity> oldscores=studentScoreService.getByStudentId(student.getId());
-		studentScoreService.add(student,quetions,oldscores);
-		oldscores=studentScoreService.getByStudentId(student.getId());
-		updateStatus(student.getId(),DataStatus.PROCESSING);
-		return oldscores;
-	}
-	
-	@Transactional
-	@Override
-	public void updateStatus(Long id,DataStatus to) {
-		UpdateWrapper<StudentEntity> wrapper = new UpdateWrapper<>();
-		LambdaUpdateWrapper<StudentEntity> lw = wrapper.lambda();
-		lw.set(StudentEntity::getDataStatus, to);
-		lw.eq(StudentEntity::getId, id);
-		this.update(wrapper);
-	}
-	
-	@Transactional
-	@Override
-	public void resetStatus() {
-		UpdateWrapper<StudentEntity> wrapper = new UpdateWrapper<>();
-		LambdaUpdateWrapper<StudentEntity> lw = wrapper.lambda();
-		lw.set(StudentEntity::getDataStatus, DataStatus.WAITING);
-		lw.eq(StudentEntity::getDataStatus, DataStatus.PROCESSING);
-		this.update(wrapper);
-	}
-
-	@Override
-	public int countBy(Long examId, DataStatus status) {
-		QueryWrapper<StudentEntity> wrapper = new QueryWrapper<>();
-		LambdaQueryWrapper<StudentEntity> lw = wrapper.lambda();
-		if(status!=null) {
-			lw.eq(StudentEntity::getDataStatus, status);
-		}
-		lw.eq(StudentEntity::getExamId, examId);
-		return this.count(wrapper);
-	}
-
-	@Transactional
-	@Override
-	public void reset(Long examId, String subjectCode) {
-		studentScoreService.removeBy(examId,subjectCode);
-		UpdateWrapper<StudentEntity> wrapper = new UpdateWrapper<>();
-		LambdaUpdateWrapper<StudentEntity> lw = wrapper.lambda();
-		lw.set(StudentEntity::getDataStatus, DataStatus.WAITING);
-		if(subjectCode!=null) {
-			lw.eq(StudentEntity::getSubjectCode, subjectCode);
-		}
-		lw.eq(StudentEntity::getExamId, examId);
-		this.update(wrapper);
-	}
-
-	@Transactional
-	@Override
-	public void clear(Long examId, String subjectCode) {
-		studentScoreService.removeBy(examId,subjectCode);
-		questionService.removeBy(examId,subjectCode);
-		QueryWrapper<StudentEntity> wrapper = new QueryWrapper<>();
-		LambdaQueryWrapper<StudentEntity> lw = wrapper.lambda();
-		if(subjectCode!=null) {
-			lw.eq(StudentEntity::getSubjectCode, subjectCode);
-		}
-		lw.eq(StudentEntity::getExamId, examId);
-		this.remove(wrapper);
-	}
+
+    private static final Logger log = LoggerFactory.getLogger(StudentService.class);
+
+    private static final String[] EXCEL_HEADER = new String[] { "考试ID", "科目代码", "考生编号" };
+
+    @Autowired
+    private SysProperty sysProperty;
+
+    @Autowired
+    private StudentService studentService;
+
+    @Autowired
+    private StudentScoreService studentScoreService;
+
+    @Autowired
+    private QuestionService questionService;
+
+    @Override
+    public void importStudent() {
+        File dir = new File(sysProperty.getDataDir());
+        File[] fs = dir.listFiles();
+        if (fs == null || fs.length == 0) {
+            return;
+        }
+        for (File file : fs) {
+            if (!file.isFile() || !file.getName().equals(ImportFileName.STUDENT_IMPORT.getName())) {
+                continue;
+            }
+            InputStream inputStream = null;
+            ImportResult ret = null;
+            try {
+                inputStream = new FileInputStream(file);
+                ret = studentService.disposeFile(inputStream);
+            } catch (Exception e) {
+                String errMsg;
+                if (e instanceof FileNotFoundException) {
+                    errMsg = "未找到文件:" + file.getAbsolutePath();
+                } else {
+                    errMsg = "系统错误:" + e.getMessage();
+                }
+                ret = new ImportResult(errMsg);
+            } finally {
+                if (inputStream != null) {
+                    try {
+                        inputStream.close();
+                    } catch (IOException e) {
+                    }
+                }
+            }
+            moveFile(dir, file, ret);
+        }
+    }
+
+    private void moveFile(File dir, File file, ImportResult ret) {
+        try {
+            boolean succss = CollectionUtils.isEmpty(ret.getErrMsg());
+            if (succss) {
+                File sucDir = new File(dir.getAbsoluteFile() + "/success/");
+                if (!sucDir.exists()) {
+                    sucDir.mkdir();
+                }
+                File targetFile = new File(sucDir.getAbsoluteFile() + "/" + file.getName());
+                if (targetFile.exists()) {
+                    targetFile.delete();
+                }
+                FileUtils.copyFile(file, targetFile);
+                file.delete();
+                String fname = file.getName().substring(0, file.getName().lastIndexOf("."));
+                File msgFile = new File(sucDir.getAbsoluteFile() + "/" + fname + "_info.txt");
+                if (msgFile.exists()) {
+                    msgFile.delete();
+                }
+                FileUtils.write(msgFile, ret.getCountInfo(), "utf-8");
+            } else {
+                File sucDir = new File(dir.getAbsoluteFile() + "/failed/");
+                if (!sucDir.exists()) {
+                    sucDir.mkdir();
+                }
+                File targetFile = new File(sucDir.getAbsoluteFile() + "/" + file.getName());
+                if (targetFile.exists()) {
+                    targetFile.delete();
+                }
+                FileUtils.copyFile(file, targetFile);
+                file.delete();
+                String fname = file.getName().substring(0, file.getName().lastIndexOf("."));
+                File msgFile = new File(sucDir.getAbsoluteFile() + "/" + fname + "_info.txt");
+                if (msgFile.exists()) {
+                    msgFile.delete();
+                }
+                FileUtils.writeLines(msgFile, StandardCharsets.UTF_8.name(), ret.getErrMsg());
+            }
+        } catch (IOException e) {
+            throw new StatusException("文件处理出错", e);
+        }
+
+    }
+
+    private String errorMsg(int lineNum, String msg) {
+        return "第" + lineNum + "行 " + msg;
+    }
+
+    private String trimAndNullIfBlank(String s) {
+        if (StringUtils.isBlank(s)) {
+            return null;
+        }
+        return s.trim();
+    }
+
+    @Transactional
+    @Override
+    public ImportResult disposeFile(InputStream inputStream) {
+        List<DataMap> lineList = null;
+        ExcelReader reader = ExcelReader.create(ExcelType.XLSX, inputStream, 0);
+        try {
+            lineList = reader.getDataMapList();
+        } catch (Exception e) {
+            throw new StatusException("Excel 解析失败");
+        }
+        if (!Arrays.equals(EXCEL_HEADER, reader.getColumnNames())) {
+            throw new StatusException("Excel表头错误");
+        }
+        if (CollectionUtils.isEmpty(lineList)) {
+            throw new StatusException("Excel无内容");
+        }
+        if (100001 < lineList.size()) {
+            throw new StatusException("数据行数不能超过100000");
+        }
+        List<StudentEntity> ss = new ArrayList<>();
+        ImportResult ret = new ImportResult();
+        List<String> failRecords = new ArrayList<>();
+        ret.setErrMsg(failRecords);
+        for (int i = 0; i < lineList.size(); i++) {
+            DataMap line = lineList.get(i);
+
+            StringBuilder msg = new StringBuilder();
+
+            StudentEntity imp = new StudentEntity();
+            imp.setDataStatus(DataStatus.WAITING);
+            String examId = trimAndNullIfBlank(line.get(EXCEL_HEADER[0]));
+            if (StringUtils.isBlank(examId)) {
+                msg.append("  考试ID不能为空");
+            } else if (examId.length() > 20) {
+                msg.append("  考试ID不能超过20个字符");
+            } else {
+                try {
+                    Long examIdVal = Long.parseLong(examId);
+                    imp.setExamId(examIdVal);
+                } catch (NumberFormatException e) {
+                    msg.append("  考试ID只能是数字");
+                }
+            }
+
+            String subjectCode = trimAndNullIfBlank(line.get(EXCEL_HEADER[1]));
+            if (StringUtils.isBlank(subjectCode)) {
+                msg.append("  科目代码不能为空");
+            } else if (subjectCode.length() > 100) {
+                msg.append("  科目代码不能超过100个字符");
+            }
+            imp.setSubjectCode(subjectCode);
+
+            String studentCode = trimAndNullIfBlank(line.get(EXCEL_HEADER[2]));
+            if (StringUtils.isBlank(studentCode)) {
+                msg.append("  考生编号不能为空");
+            } else if (studentCode.length() > 100) {
+                msg.append("  考生编号不能超过100个字符");
+            }
+            imp.setStudentCode(studentCode);
+
+            if (msg.length() > 0) {
+                failRecords.add(errorMsg(i + 2, msg.toString()));
+            } else {
+                ss.add(imp);
+            }
+
+        }
+
+        if (CollectionUtils.isNotEmpty(failRecords)) {
+            return ret;
+        }
+        try {
+            saveStudentBatch(ret, ss);
+        } catch (Exception e) {
+            failRecords.add("系统错误:" + e.getMessage());
+        }
+        return ret;
+    }
+
+    private void saveStudentBatch(ImportResult ret, List<StudentEntity> ss) {
+        if (CollectionUtils.isEmpty(ss)) {
+            ret.setCountInfo("新增数量:0");
+            return;
+        }
+        List<StudentEntity> all = this.list();
+        Set<String> set = new HashSet<>();
+        if (CollectionUtils.isNotEmpty(all)) {
+            for (StudentEntity s : all) {
+                String key = s.getExamId() + "-" + s.getSubjectCode() + "-" + s.getStudentCode();
+                set.add(key);
+            }
+        }
+        List<StudentEntity> adds = new ArrayList<>();
+        for (StudentEntity s : ss) {
+            String key = s.getExamId() + "-" + s.getSubjectCode() + "-" + s.getStudentCode();
+            if (!set.contains(key)) {
+                adds.add(s);
+            } else {
+                set.add(key);
+            }
+        }
+        if (CollectionUtils.isNotEmpty(adds)) {
+            saveBatch(adds);
+        }
+        ret.setCountInfo("新增数量:" + adds.size());
+    }
+
+    @Override
+    public List<StudentEntity> findToDispose() {
+        QueryWrapper<StudentEntity> wrapper = new QueryWrapper<>();
+        LambdaQueryWrapper<StudentEntity> lw = wrapper.lambda();
+        lw.in(StudentEntity::getDataStatus, DataStatus.WAITING, DataStatus.FAILED);
+        return this.list(wrapper);
+    }
+
+    @Override
+    public List<StudentEntity> findToMarking(Long examId) {
+        QueryWrapper<StudentEntity> wrapper = new QueryWrapper<>();
+        LambdaQueryWrapper<StudentEntity> lw = wrapper.lambda();
+        if (examId != null) {
+            lw.eq(StudentEntity::getExamId, examId);
+        }
+        lw.in(StudentEntity::getDataStatus, DataStatus.WAITING, DataStatus.FAILED, DataStatus.PROCESSING);
+        return this.list(wrapper);
+    }
+
+    @Override
+    public void buildImage(StudentEntity student, Map<Long, QuestionEntity> quetions) {
+        List<StudentScoreEntity> scores = studentService.getOrCreateScores(student, quetions);
+        Map<Integer, AnswerImageDto> answerImages = new HashMap<>();
+        for (StudentScoreEntity score : scores) {
+            if (DataStatus.WAITING.equals(score.getAnswerStatus())
+                    || DataStatus.FAILED.equals(score.getAnswerStatus())) {
+                studentService.createSlice(score, quetions, answerImages);
+            }
+        }
+    }
+
+    @Transactional
+    @Override
+    public void createSlice(StudentScoreEntity score, Map<Long, QuestionEntity> quetions,
+            Map<Integer, AnswerImageDto> answerImages) {
+        QuestionEntity q = quetions.get(score.getQuestionId());
+        if (q == null) {
+            studentScoreService.updateAnswerErr(score.getId(), "未找到试题信息");
+            updateStatus(score.getStudentId(), DataStatus.FAILED);
+            return;
+        }
+        try {
+            studentScoreService.createSlice(score, q, answerImages);
+        } catch (Exception e) {
+            if (e instanceof StatusException) {
+                studentScoreService.updateAnswerErr(score.getId(), e.getMessage());
+                updateStatus(score.getStudentId(), DataStatus.FAILED);
+            } else {
+                log.error("系统异常", e);
+                studentScoreService.updateAnswerErr(score.getId(), "系统异常");
+                updateStatus(score.getStudentId(), DataStatus.FAILED);
+            }
+        }
+    }
+
+    @Transactional
+    @Override
+    public List<StudentScoreEntity> getOrCreateScores(StudentEntity student, Map<Long, QuestionEntity> quetions) {
+        List<StudentScoreEntity> oldscores = studentScoreService.getByStudentId(student.getId());
+        studentScoreService.add(student, quetions, oldscores);
+        oldscores = studentScoreService.getByStudentId(student.getId());
+        updateStatus(student.getId(), DataStatus.PROCESSING);
+        return oldscores;
+    }
+
+    @Transactional
+    @Override
+    public void updateStatus(Long id, DataStatus to) {
+        UpdateWrapper<StudentEntity> wrapper = new UpdateWrapper<>();
+        LambdaUpdateWrapper<StudentEntity> lw = wrapper.lambda();
+        lw.set(StudentEntity::getDataStatus, to);
+        lw.eq(StudentEntity::getId, id);
+        this.update(wrapper);
+    }
+
+    @Transactional
+    @Override
+    public void resetStatus() {
+        UpdateWrapper<StudentEntity> wrapper = new UpdateWrapper<>();
+        LambdaUpdateWrapper<StudentEntity> lw = wrapper.lambda();
+        lw.set(StudentEntity::getDataStatus, DataStatus.WAITING);
+        lw.eq(StudentEntity::getDataStatus, DataStatus.PROCESSING);
+        this.update(wrapper);
+    }
+
+    @Override
+    public int countBy(Long examId, DataStatus status) {
+        QueryWrapper<StudentEntity> wrapper = new QueryWrapper<>();
+        LambdaQueryWrapper<StudentEntity> lw = wrapper.lambda();
+        if (status != null) {
+            lw.eq(StudentEntity::getDataStatus, status);
+        }
+        lw.eq(StudentEntity::getExamId, examId);
+        return this.count(wrapper);
+    }
+
+    @Transactional
+    @Override
+    public void reset(Long examId, String subjectCode) {
+        studentScoreService.removeBy(examId, subjectCode);
+        UpdateWrapper<StudentEntity> wrapper = new UpdateWrapper<>();
+        LambdaUpdateWrapper<StudentEntity> lw = wrapper.lambda();
+        lw.set(StudentEntity::getDataStatus, DataStatus.WAITING);
+        if (subjectCode != null) {
+            lw.eq(StudentEntity::getSubjectCode, subjectCode);
+        }
+        lw.eq(StudentEntity::getExamId, examId);
+        this.update(wrapper);
+    }
+
+    @Transactional
+    @Override
+    public void clear(Long examId, String subjectCode) {
+        studentScoreService.removeBy(examId, subjectCode);
+        questionService.removeBy(examId, subjectCode);
+        QueryWrapper<StudentEntity> wrapper = new QueryWrapper<>();
+        LambdaQueryWrapper<StudentEntity> lw = wrapper.lambda();
+        if (subjectCode != null) {
+            lw.eq(StudentEntity::getSubjectCode, subjectCode);
+        }
+        lw.eq(StudentEntity::getExamId, examId);
+        this.remove(wrapper);
+    }
 }

+ 47 - 42
src/main/java/cn/com/qmth/am/task/AiMarkingJob.java

@@ -18,49 +18,54 @@ import cn.com.qmth.am.service.StudentService;
 
 @Service
 public class AiMarkingJob {
-	@Autowired
-	private StudentScoreService studentScoreService;
-	@Autowired
-	private StudentService studentService;
-	@Autowired
-	private ConcurrentService concurrentService;
-	@Autowired
-	private SysProperty sysProperty;
 
-	@Scheduled(fixedDelay = 5 * 1000, initialDelay = 20 * 1000)
-	public void doJob() {
-		if(!sysProperty.getMarkingTaskEnable()) {
-			return;
-		}
-		List<StudentEntity> students=studentService.findToMrking();
-		if (CollectionUtils.isEmpty(students)) {
-			return;
-		}
-		boolean lock = concurrentService.getReadWriteLock(LockType.AI_MARKING.name()).writeLock().tryLock();
-		try {
-			if (!lock) {
-				return;
-			}
-			this.dispose(students);
-		} finally {
-			if (lock) {
-				concurrentService.getReadWriteLock(LockType.AI_MARKING.name()).writeLock().unlock();
-			}
-		}
-	}
+    @Autowired
+    private StudentScoreService studentScoreService;
 
-	private void dispose(List<StudentEntity> students) {
-		for (StudentEntity student : students) {
-			if(!sysProperty.getMarkingTaskEnable()) {
-				return;
-			}
-			List<StudentScoreEntity> scores = studentScoreService.findToAiMarking(student.getId());
-			if (CollectionUtils.isNotEmpty(scores)) {
-				for (StudentScoreEntity score : scores) {
-					studentScoreService.aiMarking(score);
-				}
-			}
-		}
-	}
+    @Autowired
+    private StudentService studentService;
+
+    @Autowired
+    private ConcurrentService concurrentService;
+
+    @Autowired
+    private SysProperty sysProperty;
+
+    @Scheduled(fixedDelay = 5 * 1000, initialDelay = 5 * 1000)
+    public void doJob() {
+        if (!sysProperty.getMarkingTaskEnable()) {
+            return;
+        }
+        Long examId = null;
+        List<StudentEntity> students = studentService.findToMarking(examId);
+        if (CollectionUtils.isEmpty(students)) {
+            return;
+        }
+        boolean lock = concurrentService.getReadWriteLock(LockType.AI_MARKING.name()).writeLock().tryLock();
+        try {
+            if (!lock) {
+                return;
+            }
+            this.dispose(students);
+        } finally {
+            if (lock) {
+                concurrentService.getReadWriteLock(LockType.AI_MARKING.name()).writeLock().unlock();
+            }
+        }
+    }
+
+    private void dispose(List<StudentEntity> students) {
+        for (StudentEntity student : students) {
+            if (!sysProperty.getMarkingTaskEnable()) {
+                return;
+            }
+            List<StudentScoreEntity> scores = studentScoreService.findToAiMarking(student.getId());
+            if (CollectionUtils.isNotEmpty(scores)) {
+                for (StudentScoreEntity score : scores) {
+                    studentScoreService.aiMarking(score);
+                }
+            }
+        }
+    }
 
 }

+ 51 - 0
src/main/java/cn/com/qmth/am/utils/FormFilePart.java

@@ -0,0 +1,51 @@
+package cn.com.qmth.am.utils;
+
+import java.io.File;
+
+public class FormFilePart {
+
+    private String paramName;
+
+    private String filename;
+
+    private File file;
+
+    /**
+     * 构造函数
+     *
+     * @param paramName
+     * @param filename
+     * @param file
+     */
+    public FormFilePart(String paramName, String filename, File file) {
+        super();
+        this.paramName = paramName;
+        this.filename = filename;
+        this.file = file;
+    }
+
+    public String getParamName() {
+        return paramName;
+    }
+
+    public void setParamName(String paramName) {
+        this.paramName = paramName;
+    }
+
+    public String getFilename() {
+        return filename;
+    }
+
+    public void setFilename(String filename) {
+        this.filename = filename;
+    }
+
+    public File getFile() {
+        return file;
+    }
+
+    public void setFile(File file) {
+        this.file = file;
+    }
+
+}

+ 44 - 0
src/main/java/cn/com/qmth/am/utils/FreeMarkerUtil.java

@@ -0,0 +1,44 @@
+package cn.com.qmth.am.utils;
+
+import java.io.IOException;
+import java.io.StringWriter;
+
+import com.qmth.boot.core.ai.model.llm.AutoScoreRequest;
+
+import freemarker.template.Configuration;
+import freemarker.template.Template;
+
+public class FreeMarkerUtil {
+
+    private static final String ENCODING = "UTF-8";
+
+    private static Configuration config;
+
+    private static Template dsMarkingReq;
+
+    static {
+
+        config = new Configuration(Configuration.VERSION_2_3_25);
+        // 设置编码
+        config.setDefaultEncoding(ENCODING);
+        // 设置ftl模板路径
+        config.setClassForTemplateLoading(FreeMarkerUtil.class, "/templates/");
+
+        try {
+            dsMarkingReq = config.getTemplate("ds_marking.ftl", ENCODING);
+        } catch (IOException e) {
+            throw new RuntimeException(e);
+        }
+    }
+
+    public static String getDsMarkingReq(AutoScoreRequest req) {
+        StringWriter result = null;
+        try {
+            result = new StringWriter();
+            dsMarkingReq.process(req, result);
+            return result.toString();
+        } catch (Exception e) {
+            throw new RuntimeException(e);
+        }
+    }
+}

+ 0 - 91
src/main/java/cn/com/qmth/am/utils/HttpClientBuilder.java

@@ -1,91 +0,0 @@
-package cn.com.qmth.am.utils;
-
-import okhttp3.ConnectionPool;
-import okhttp3.OkHttpClient;
-import org.slf4j.Logger;
-import org.slf4j.LoggerFactory;
-
-import javax.net.ssl.*;
-import java.security.SecureRandom;
-import java.security.cert.CertificateException;
-import java.security.cert.X509Certificate;
-import java.util.concurrent.TimeUnit;
-
-public class HttpClientBuilder {
-
-    private static final Logger log = LoggerFactory.getLogger(HttpClientBuilder.class);
-
-    private static OkHttpClient client;
-
-    static {
-        client = Client.INSTANCE.getInstance();
-    }
-
-    public static OkHttpClient getClient() {
-        return client;
-    }
-
-    private enum Client {
-
-        INSTANCE;
-
-        Client() {
-            ConnectionPool connectionPool = new ConnectionPool(10, 5L, TimeUnit.MINUTES);
-
-            instance = new OkHttpClient
-                    .Builder()
-                    // .retryOnConnectionFailure(false)
-                    .connectionPool(connectionPool)
-                    .connectTimeout(30, TimeUnit.SECONDS)
-                    .readTimeout(60, TimeUnit.SECONDS)
-                    .writeTimeout(60, TimeUnit.SECONDS)
-                    .sslSocketFactory(sslSocketFactory(), trustAllCert())
-                    .hostnameVerifier(trustAllHost())
-                    .build();
-
-            log.debug("OkHttpClient init..");
-        }
-
-        private OkHttpClient instance;
-
-        public OkHttpClient getInstance() {
-            return instance;
-        }
-    }
-
-    private static SSLSocketFactory sslSocketFactory() {
-        try {
-            SSLContext sslContext = SSLContext.getInstance("TLS");
-            sslContext.init(null, new TrustManager[]{trustAllCert()}, new SecureRandom());
-            return sslContext.getSocketFactory();
-        } catch (Exception e) {
-            log.error(e.getMessage());
-            throw new RuntimeException(e);
-        }
-    }
-
-    private static HostnameVerifier trustAllHost() {
-        return (hostname, sslSession) -> true;
-    }
-
-    private static X509TrustManager trustAllCert() {
-        return new X509TrustManager() {
-
-            @Override
-            public void checkClientTrusted(X509Certificate[] chain, String authType) throws CertificateException {
-
-            }
-
-            @Override
-            public void checkServerTrusted(X509Certificate[] chain, String authType) throws CertificateException {
-
-            }
-
-            @Override
-            public X509Certificate[] getAcceptedIssuers() {
-                return new X509Certificate[0];
-            }
-        };
-    }
-
-}

+ 108 - 0
src/main/java/cn/com/qmth/am/utils/HttpMethod.java

@@ -0,0 +1,108 @@
+package cn.com.qmth.am.utils;
+
+import java.util.HashMap;
+import java.util.Map;
+
+public class HttpMethod {
+
+    public static final HttpMethod OPTIONS = new HttpMethod("OPTIONS");
+
+    public static final HttpMethod GET = new HttpMethod("GET");
+
+    public static final HttpMethod HEAD = new HttpMethod("HEAD");
+
+    public static final HttpMethod POST = new HttpMethod("POST");
+
+    public static final HttpMethod PUT = new HttpMethod("PUT");
+
+    public static final HttpMethod PATCH = new HttpMethod("PATCH");
+
+    public static final HttpMethod DELETE = new HttpMethod("DELETE");
+
+    public static final HttpMethod TRACE = new HttpMethod("TRACE");
+
+    public static final HttpMethod CONNECT = new HttpMethod("CONNECT");
+
+    private static final Map<String, HttpMethod> METHOD_MAP = new HashMap<String, HttpMethod>();
+
+    static {
+        METHOD_MAP.put(OPTIONS.toString(), OPTIONS);
+        METHOD_MAP.put(GET.toString(), GET);
+        METHOD_MAP.put(HEAD.toString(), HEAD);
+        METHOD_MAP.put(POST.toString(), POST);
+        METHOD_MAP.put(PUT.toString(), PUT);
+        METHOD_MAP.put(PATCH.toString(), PATCH);
+        METHOD_MAP.put(DELETE.toString(), DELETE);
+        METHOD_MAP.put(TRACE.toString(), TRACE);
+        METHOD_MAP.put(CONNECT.toString(), CONNECT);
+    }
+
+    private final String name;
+
+    /**
+     * 构造函数
+     *
+     * @param name
+     */
+    private HttpMethod(String name) {
+        if (name == null) {
+            throw new NullPointerException("name");
+        }
+
+        name = name.trim();
+        if (name.length() == 0) {
+            throw new IllegalArgumentException("empty name");
+        }
+
+        for (int i = 0; i < name.length(); i++) {
+            if (Character.isISOControl(name.charAt(i)) || Character.isWhitespace(name.charAt(i))) {
+                throw new IllegalArgumentException("invalid character in name");
+            }
+        }
+
+        this.name = name;
+    }
+
+    public String getName() {
+        return name;
+    }
+
+    public static HttpMethod valueOf(String name) {
+        if (name == null) {
+            throw new NullPointerException("name");
+        }
+
+        name = name.trim();
+        if (name.length() == 0) {
+            throw new IllegalArgumentException("empty name");
+        }
+
+        HttpMethod result = METHOD_MAP.get(name);
+        if (result != null) {
+            return result;
+        } else {
+            throw new IllegalArgumentException("undefined name");
+        }
+    }
+
+    @Override
+    public int hashCode() {
+        return getName().hashCode();
+    }
+
+    @Override
+    public boolean equals(Object o) {
+        if (!(o instanceof HttpMethod)) {
+            return false;
+        }
+
+        HttpMethod that = (HttpMethod) o;
+        return getName().equals(that.getName());
+    }
+
+    @Override
+    public String toString() {
+        return getName();
+    }
+
+}

+ 277 - 0
src/main/java/cn/com/qmth/am/utils/OKHttpUtil.java

@@ -0,0 +1,277 @@
+package cn.com.qmth.am.utils;
+
+import java.io.IOException;
+import java.util.List;
+import java.util.Map;
+import java.util.Map.Entry;
+import java.util.concurrent.TimeUnit;
+
+import org.apache.commons.collections4.CollectionUtils;
+
+import okhttp3.FormBody;
+import okhttp3.MediaType;
+import okhttp3.MultipartBody;
+import okhttp3.OkHttpClient;
+import okhttp3.Request;
+import okhttp3.Request.Builder;
+import okhttp3.RequestBody;
+import okhttp3.Response;
+
+public class OKHttpUtil {
+
+    public static final class MediaTypes {
+
+        public static final MediaType JSON = MediaType.parse("application/json; charset=utf-8");
+    }
+
+    public static interface RequestBodyBuilder {
+
+        RequestBody build();
+    }
+
+    /**
+     * json请求体构建器
+     *
+     * @author
+     * @date 2019年4月10日
+     * @Copyright (c) 2018-2020 [QQ:522080330] All Rights Reserved.
+     */
+    public static final class JsonBodyBuilder implements RequestBodyBuilder {
+
+        private String json;
+
+        public JsonBodyBuilder(String json) {
+            super();
+            this.json = json;
+        }
+
+        @Override
+        public RequestBody build() {
+            return RequestBody.create(MediaTypes.JSON, json);
+        }
+
+        @Override
+        public String toString() {
+            return json;
+        }
+    }
+
+    private static OkHttpClient okHttpClient;
+
+    // public static void initOkHttpClient() {
+    static {
+        okHttpClient = new OkHttpClient.Builder()
+                .sslSocketFactory(SSLSocketClient.getSSLSocketFactory(), SSLSocketClient.getX509TrustManager())
+                .hostnameVerifier(SSLSocketClient.getHostnameVerifier()).readTimeout(2000, TimeUnit.SECONDS)
+                .writeTimeout(2000, TimeUnit.SECONDS).build();
+    }
+
+    public static OkHttpClient getOkHttpClient() {
+        return okHttpClient;
+    }
+
+    public static void close() {
+        okHttpClient.connectionPool().evictAll();
+    }
+
+    /**
+     * 发送请求 (带json请求体)
+     *
+     * @author
+     * @param httpMethod
+     * @param url
+     * @param headers
+     * @param jsonBody
+     * @return
+     */
+    public static Response call(HttpMethod httpMethod, String url, Map<String, String> headers, String jsonBody) {
+        return call(httpMethod, url, headers, new JsonBodyBuilder(jsonBody));
+    }
+
+    /**
+     * 发送请求 (带请求体)
+     *
+     * @author
+     * @param httpMethod
+     * @param url
+     * @param headers
+     * @param requestBodyBuilder
+     * @return
+     */
+    public static Response call(HttpMethod httpMethod, String url, Map<String, String> headers,
+            RequestBodyBuilder requestBodyBuilder) {
+
+        Builder builder = null;
+        if (httpMethod.equals(HttpMethod.POST)) {
+            builder = new Request.Builder().url(url).post(requestBodyBuilder.build());
+        } else if (httpMethod.equals(HttpMethod.PUT)) {
+            builder = new Request.Builder().url(url).put(requestBodyBuilder.build());
+        } else if (httpMethod.equals(HttpMethod.DELETE)) {
+            builder = new Request.Builder().url(url).delete(requestBodyBuilder.build());
+        }
+
+        if (null != headers && 0 != headers.size()) {
+            for (Entry<String, String> entry : headers.entrySet()) {
+                builder.addHeader(entry.getKey(), entry.getValue());
+            }
+        }
+
+        Request request = builder.build();
+
+        Response response = null;
+        try {
+            response = okHttpClient.newCall(request).execute();
+            return response;
+        } catch (IOException e) {
+            throw new RuntimeException(e);
+        }
+    }
+
+    /**
+     * 发送请求
+     *
+     * @author
+     * @param httpMethod
+     * @param url
+     * @return
+     */
+    public static Response call(HttpMethod httpMethod, String url) {
+        return call(httpMethod, url, null);
+    }
+
+    /**
+     * 发送请求
+     *
+     * @author
+     * @param httpMethod
+     * @param url
+     * @param headers
+     * @return
+     */
+    public static Response call(HttpMethod httpMethod, String url, Map<String, String> headers) {
+
+        Builder builder = null;
+        if (httpMethod.equals(HttpMethod.GET)) {
+            builder = new Request.Builder().url(url);
+        } else if (httpMethod.equals(HttpMethod.POST)) {
+            builder = new Request.Builder().url(url).post(new FormBody.Builder().build());
+        } else if (httpMethod.equals(HttpMethod.PUT)) {
+            builder = new Request.Builder().url(url).put(new FormBody.Builder().build());
+        } else if (httpMethod.equals(HttpMethod.DELETE)) {
+            builder = new Request.Builder().url(url).delete(new FormBody.Builder().build());
+        }
+
+        if (null != headers && 0 != headers.size()) {
+            for (Entry<String, String> entry : headers.entrySet()) {
+                builder.addHeader(entry.getKey(), entry.getValue());
+            }
+        }
+
+        Request request = builder.build();
+
+        Response response = null;
+        try {
+            response = okHttpClient.newCall(request).execute();
+            return response;
+        } catch (IOException e) {
+            throw new RuntimeException(e);
+        }
+    }
+
+    /**
+     * 发送请求 (表单)
+     *
+     * @author
+     * @param httpMethod
+     * @param url
+     * @param headers
+     * @param params
+     * @return
+     */
+    public static Response call(HttpMethod httpMethod, String url, Map<String, String> headers,
+            Map<String, String> params) {
+
+        okhttp3.FormBody.Builder formBody = new FormBody.Builder();
+
+        if (null != params && 0 != params.size()) {
+            for (Entry<String, String> entry : params.entrySet()) {
+                formBody.add(entry.getKey(), entry.getValue());
+            }
+        }
+
+        Builder builder = null;
+        if (httpMethod.equals(HttpMethod.POST)) {
+            builder = new Request.Builder().url(url).post(formBody.build());
+        } else if (httpMethod.equals(HttpMethod.PUT)) {
+            builder = new Request.Builder().url(url).put(formBody.build());
+        } else if (httpMethod.equals(HttpMethod.DELETE)) {
+            builder = new Request.Builder().url(url).delete(formBody.build());
+        }
+
+        if (null != headers && 0 != headers.size()) {
+            for (Entry<String, String> entry : headers.entrySet()) {
+                builder.addHeader(entry.getKey(), entry.getValue());
+            }
+        }
+
+        Request request = builder.build();
+
+        Response response = null;
+        try {
+            response = okHttpClient.newCall(request).execute();
+            return response;
+        } catch (IOException e) {
+            throw new RuntimeException(e);
+        }
+    }
+
+    /**
+     * 发送请求 (包含文件表单)
+     *
+     * @author
+     * @param httpMethod
+     * @param url
+     * @param headers
+     * @param params
+     * @param formFilePartList
+     * @return
+     */
+    public static Response call(HttpMethod httpMethod, String url, Map<String, String> headers,
+            Map<String, String> params, List<FormFilePart> formFilePartList) {
+
+        okhttp3.MultipartBody.Builder multipartBodyBuilder = new MultipartBody.Builder()
+                .setType(MultipartBody.ALTERNATIVE);
+
+        if (null != params) {
+            for (Entry<String, String> entry : params.entrySet()) {
+                multipartBodyBuilder.addFormDataPart(entry.getKey(), entry.getValue());
+            }
+        }
+
+        if (CollectionUtils.isNotEmpty(formFilePartList)) {
+            MediaType type = MediaType.parse("application/octet-stream");
+            for (FormFilePart part : formFilePartList) {
+                RequestBody fileBody = RequestBody.create(type, part.getFile());
+                multipartBodyBuilder.addFormDataPart(part.getParamName(), part.getFilename(), fileBody);
+            }
+        }
+
+        Builder builder = new Request.Builder().url(url).post(multipartBodyBuilder.build());
+        if (null != headers && 0 != headers.size()) {
+            for (Entry<String, String> entry : headers.entrySet()) {
+                builder.addHeader(entry.getKey(), entry.getValue());
+            }
+        }
+
+        Request request = builder.build();
+
+        Response response = null;
+        try {
+            response = okHttpClient.newCall(request).execute();
+            return response;
+        } catch (IOException e) {
+            throw new RuntimeException(e);
+        }
+    }
+
+}

+ 63 - 0
src/main/java/cn/com/qmth/am/utils/SSLSocketClient.java

@@ -0,0 +1,63 @@
+package cn.com.qmth.am.utils;
+
+import javax.net.ssl.*;
+import java.security.KeyStore;
+import java.security.SecureRandom;
+import java.security.cert.X509Certificate;
+import java.util.Arrays;
+
+public class SSLSocketClient {
+
+    // 获取这个SSLSocketFactory
+    public static SSLSocketFactory getSSLSocketFactory() {
+        try {
+            SSLContext sslContext = SSLContext.getInstance("SSL");
+            sslContext.init(null, getTrustManager(), new SecureRandom());
+            return sslContext.getSocketFactory();
+        } catch (Exception e) {
+            throw new RuntimeException(e);
+        }
+    }
+
+    // 获取TrustManager
+    private static TrustManager[] getTrustManager() {
+        return new TrustManager[] { new X509TrustManager() {
+
+            @Override
+            public void checkClientTrusted(X509Certificate[] chain, String authType) {
+            }
+
+            @Override
+            public void checkServerTrusted(X509Certificate[] chain, String authType) {
+            }
+
+            @Override
+            public X509Certificate[] getAcceptedIssuers() {
+                return new X509Certificate[] {};
+            }
+        } };
+    }
+
+    // 获取HostnameVerifier
+    public static HostnameVerifier getHostnameVerifier() {
+        return (s, sslSession) -> true;
+    }
+
+    public static X509TrustManager getX509TrustManager() {
+        X509TrustManager trustManager = null;
+        try {
+            TrustManagerFactory trustManagerFactory = TrustManagerFactory
+                    .getInstance(TrustManagerFactory.getDefaultAlgorithm());
+            trustManagerFactory.init((KeyStore) null);
+            TrustManager[] trustManagers = trustManagerFactory.getTrustManagers();
+            if (trustManagers.length != 1 || !(trustManagers[0] instanceof X509TrustManager)) {
+                throw new IllegalStateException("Unexpected default trust managers:" + Arrays.toString(trustManagers));
+            }
+            trustManager = (X509TrustManager) trustManagers[0];
+        } catch (Exception e) {
+            e.printStackTrace();
+        }
+
+        return trustManager;
+    }
+}

+ 8 - 5
src/main/resources/application.properties

@@ -12,10 +12,10 @@ com.qmth.mybatis.block-attack=false
 # ********** db config **********
 #
 db.host=localhost
-db.port=3309
-db.database=ai_marking_db
-com.qmth.datasource.username=ai_marking
-com.qmth.datasource.password=ai_marking
+db.port=3306
+db.database=ai_marking_ds
+com.qmth.datasource.username=root
+com.qmth.datasource.password=123456
 com.qmth.datasource.url=jdbc:mysql://${db.host}:${db.port}/${db.database}?useUnicode=true&characterEncoding=UTF-8&useSSL=false&serverTimezone=GMT%2b8&rewriteBatchedStatements=true
 
 #
@@ -38,8 +38,11 @@ com.qmth.solar.access-key=7bbdc11570bc474dbf50e0d4a5dff328
 com.qmth.solar.access-secret=IOodRvbp2LspJTHOScgB7Yx8MRloMpyl
 
 am.ocr-task.enable=false
-am.marking-task.enable=false
+am.marking-task.enable=true
 am.data-type=MARKING_CLOUD
 am.image-server=https://file.markingcloud.com
 am.data-dir=./data
+am.marking-server=http://39.174.90.3:31091/spiritx-api/v1/chat/completions
+am.marking-model=glm4_9
+am.marking-key=sk-loBBngbg1ymvUo6f647bF35d69684f1280E5D544F1F59f20
 ##################################setting##########################################

+ 23 - 0
src/main/resources/templates/ds_marking.ftl

@@ -0,0 +1,23 @@
+作为"${subjectName}"科目的评分教师,您的任务是依据详细的评分指南,对考生关于特定试题的回答进行全面评估。请按照以下结构化流程进行细致评判:
+
+#### 试题内容:
+${questionBody}
+
+#### 参考答案由${standardAnswer?size}条关键内容组成:
+<#list standardAnswer as item> 
+${item?counter}. ${item.content}
+</#list>
+
+#### 考生回答:
+${studentAnswer}
+
+#### 评判细则:
+- **完整性检查**:对照考生答案与关键内容,确认是否全面包含所有指定要点。
+- **语义通畅性**:分析考生回答是否条理清晰、逻辑连贯。
+- **术语准确性**:考察考生对相关概念与术语使用的恰当程度。
+
+#### 评分操作指引:
+针对每一条关键内容,依据完整性、语义通畅性及术语准确性对考生回答进行细致评判,得分为介于0至100之间的整数,准确反映考生回答质量。
+
+#### 最终输出要求:
+直接输出${standardAnswer?size}条关键内容的评分结果,评分结果无需其他文字说明,各分数间以英文逗号分隔,分数项不要加序号且无需其他文字说明。