瀏覽代碼

初始化

haogh 2 月之前
父節點
當前提交
3075304ce6
共有 33 個文件被更改,包括 1462 次插入0 次删除
  1. 14 0
      insall/run.bat
  2. 8 0
      insall/run.sh
  3. 17 0
      insall/stop.sh
  4. 82 0
      pom.xml
  5. 71 0
      src/main/java/com/qmth/pdf/tools/PdfToolsApplication.java
  6. 26 0
      src/main/java/com/qmth/pdf/tools/bean/BlockBean.java
  7. 19 0
      src/main/java/com/qmth/pdf/tools/bean/BodyBean.java
  8. 18 0
      src/main/java/com/qmth/pdf/tools/bean/ControlBean.java
  9. 17 0
      src/main/java/com/qmth/pdf/tools/bean/DescriptionBean.java
  10. 33 0
      src/main/java/com/qmth/pdf/tools/bean/DetailBean.java
  11. 16 0
      src/main/java/com/qmth/pdf/tools/bean/OptionBean.java
  12. 44 0
      src/main/java/com/qmth/pdf/tools/bean/PaperBean.java
  13. 35 0
      src/main/java/com/qmth/pdf/tools/bean/PaperDetailBean.java
  14. 21 0
      src/main/java/com/qmth/pdf/tools/bean/ParamBean.java
  15. 59 0
      src/main/java/com/qmth/pdf/tools/bean/QuestionBean.java
  16. 20 0
      src/main/java/com/qmth/pdf/tools/bean/SectionBean.java
  17. 30 0
      src/main/java/com/qmth/pdf/tools/bean/StudentAnswerBean.java
  18. 45 0
      src/main/java/com/qmth/pdf/tools/bean/StudentBean.java
  19. 29 0
      src/main/java/com/qmth/pdf/tools/bean/StudentScoreBean.java
  20. 23 0
      src/main/java/com/qmth/pdf/tools/bean/SubjectiveAnswerBean.java
  21. 9 0
      src/main/java/com/qmth/pdf/tools/config/AsyncConfig.java
  22. 33 0
      src/main/java/com/qmth/pdf/tools/config/PdfConfig.java
  23. 40 0
      src/main/java/com/qmth/pdf/tools/config/RestTemplateConfig.java
  24. 22 0
      src/main/java/com/qmth/pdf/tools/config/SwaggerConfig.java
  25. 37 0
      src/main/java/com/qmth/pdf/tools/config/ThreadPoolConfig.java
  26. 35 0
      src/main/java/com/qmth/pdf/tools/constant/SystemConstant.java
  27. 34 0
      src/main/java/com/qmth/pdf/tools/control/StudentJsonControl.java
  28. 11 0
      src/main/java/com/qmth/pdf/tools/service/StudentPaperService.java
  29. 413 0
      src/main/java/com/qmth/pdf/tools/service/impl/StudentPaperServiceImpl.java
  30. 89 0
      src/main/java/com/qmth/pdf/tools/util/HttpClientUtil.java
  31. 48 0
      src/main/java/com/qmth/pdf/tools/util/Result.java
  32. 35 0
      src/main/java/com/qmth/pdf/tools/util/ResultUtil.java
  33. 29 0
      src/main/resources/application.properties

+ 14 - 0
insall/run.bat

@@ -0,0 +1,14 @@
+@echo off
+chcp 65001
+echo.
+echo 请确保已经安装了Jdk1.8
+echo.
+echo 执行结束之前,请不要关闭此窗口
+echo.
+echo 日志输出到Jar所在目录下的pdf.log中
+echo.
+echo 如何生成考生的json文件,详见README.md
+cd ..
+java -Dspring.config.location=application.properties -jar pdf-tools.jar > pdf.log 2>&1
+echo.
+pause

+ 8 - 0
insall/run.sh

@@ -0,0 +1,8 @@
+#!/bin/bash
+echo
+echo "请确保已经安装了Jdk1.8"
+echo
+echo "日志输出到 pdf.log"
+cd ..
+nohup java -Dspring.config.location=application.properties -jar pdf-tools.jar > pdf.log 2>&1 &
+echo

+ 17 - 0
insall/stop.sh

@@ -0,0 +1,17 @@
+#!/bin/bash
+
+echo "查找pdf-tools进程..."
+PROCESS_ID=$(ps -ef | grep pdf-tools.jar | grep -v grep | awk '{print $2}')
+
+if [ -z "$PROCESS_ID" ]; then
+    echo "没有找到pdf-tools进程。"
+else
+    echo "找到的进程ID: $PROCESS_ID"
+    echo "终止进程..."
+    kill -9 $PROCESS_ID
+    if [ $? -eq 0 ]; then
+        echo "进程已成功终止。"
+    else
+        echo "终止进程失败。"
+    fi
+fi

+ 82 - 0
pom.xml

@@ -0,0 +1,82 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<project xmlns="http://maven.apache.org/POM/4.0.0"
+         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
+         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
+    <modelVersion>4.0.0</modelVersion>
+
+    <groupId>com.qmth.pdf.tools</groupId>
+    <artifactId>pdf-tools</artifactId>
+    <version>1.0.0</version>
+    <packaging>jar</packaging>
+
+    <properties>
+        <maven.compiler.source>8</maven.compiler.source>
+        <maven.compiler.target>8</maven.compiler.target>
+        <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
+        <spring-boot.version>2.3.7.RELEASE</spring-boot.version>
+    </properties>
+
+    <parent>
+        <groupId>org.springframework.boot</groupId>
+        <artifactId>spring-boot-starter-parent</artifactId>
+        <version>2.3.7.RELEASE</version>
+        <relativePath/>
+    </parent>
+
+    <dependencies>
+        <dependency>
+            <groupId>org.springframework.boot</groupId>
+            <artifactId>spring-boot-starter</artifactId>
+        </dependency>
+        <dependency>
+            <groupId>org.springframework.boot</groupId>
+            <artifactId>spring-boot-starter-web</artifactId>
+        </dependency>
+        <!-- 添加 Lombok 依赖 -->
+        <dependency>
+            <groupId>org.projectlombok</groupId>
+            <artifactId>lombok</artifactId>
+            <optional>true</optional>
+        </dependency>
+        <!-- 添加 Swagger 依赖 -->
+        <dependency>
+            <groupId>io.springfox</groupId>
+            <artifactId>springfox-swagger2</artifactId>
+            <version>2.9.2</version>
+        </dependency>
+        <dependency>
+            <groupId>io.springfox</groupId>
+            <artifactId>springfox-swagger-ui</artifactId>
+            <version>2.9.2</version>
+        </dependency>
+        <dependency>
+            <groupId>com.google.guava</groupId>
+            <artifactId>guava</artifactId>
+            <version>29.0-jre</version>
+        </dependency>
+    </dependencies>
+
+    <build>
+        <plugins>
+            <plugin>
+                <groupId>org.springframework.boot</groupId>
+                <artifactId>spring-boot-maven-plugin</artifactId>
+            </plugin>
+            <!-- 添加 Lombok 注解处理器插件 -->
+            <plugin>
+                <groupId>org.apache.maven.plugins</groupId>
+                <artifactId>maven-compiler-plugin</artifactId>
+                <version>3.8.1</version>
+                <configuration>
+                    <annotationProcessorPaths>
+                        <path>
+                            <groupId>org.projectlombok</groupId>
+                            <artifactId>lombok</artifactId>
+                            <version>1.18.24</version>
+                        </path>
+                    </annotationProcessorPaths>
+                </configuration>
+            </plugin>
+        </plugins>
+    </build>
+</project>

+ 71 - 0
src/main/java/com/qmth/pdf/tools/PdfToolsApplication.java

@@ -0,0 +1,71 @@
+package com.qmth.pdf.tools;
+
+import com.qmth.pdf.tools.config.PdfConfig;
+import com.qmth.pdf.tools.service.StudentPaperService;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.boot.CommandLineRunner;
+import org.springframework.boot.SpringApplication;
+import org.springframework.boot.autoconfigure.SpringBootApplication;
+import org.springframework.boot.context.properties.EnableConfigurationProperties;
+import org.springframework.util.StringUtils;
+import springfox.documentation.swagger2.annotations.EnableSwagger2;
+
+import java.util.Arrays;
+
+@SpringBootApplication
+@EnableConfigurationProperties
+@EnableSwagger2
+public class PdfToolsApplication implements CommandLineRunner {
+
+    private static final Logger logger = LoggerFactory.getLogger(PdfToolsApplication.class);
+
+    @Autowired
+    private StudentPaperService studentPaperService;
+
+    @Autowired
+    private PdfConfig pdfConfig;
+
+    public static void main(String[] args) {
+        SpringApplication.run(PdfToolsApplication.class, args);
+    }
+
+    @Override
+    public void run(String... args) throws Exception {
+        // 生成考生的试卷json文件
+        String subjectCode = pdfConfig.getSubjectCode();
+        String examId = pdfConfig.getExamId();
+
+        if (examId == null || StringUtils.isEmpty(examId)) {
+            logger.warn("examId不能为空,请在application.properties中配置");
+            return;
+        }
+        if(StringUtils.isEmpty(subjectCode)) {
+            subjectCode = null;
+        }
+
+        String[] examIdArr = splitAndCleanExamIds(examId);
+        // 多个examId时,subjectCode设置为null
+        if (examIdArr.length > 1) {
+            subjectCode = null;
+        }
+        for (String id : examIdArr) {
+            try {
+                long examIdLong = Long.parseLong(id);
+                studentPaperService.outputStudentPaperJson(examIdLong, subjectCode);
+            } catch (NumberFormatException e) {
+                logger.warn("examId:{} 格式错误,只能是数字,请不要包含其他字符", id, e);
+            } catch (Exception e) {
+                logger.error("处理examId:{}时发生未知错误", id, e);
+            }
+        }
+    }
+
+    private String[] splitAndCleanExamIds(String examId) {
+        return Arrays.stream(examId.split(","))
+                .map(String::trim) // 去除前后空格
+                .filter(id -> !StringUtils.isEmpty(id)) // 过滤掉空字符串
+                .toArray(String[]::new);
+    }
+}

+ 26 - 0
src/main/java/com/qmth/pdf/tools/bean/BlockBean.java

@@ -0,0 +1,26 @@
+package com.qmth.pdf.tools.bean;
+
+import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
+import com.fasterxml.jackson.annotation.JsonInclude;
+import io.swagger.annotations.ApiModelProperty;
+import lombok.Getter;
+import lombok.Setter;
+
+import java.util.Map;
+
+@Getter
+@Setter
+@JsonIgnoreProperties(ignoreUnknown = true)
+@JsonInclude(JsonInclude.Include.NON_NULL)
+public class BlockBean {
+
+    @ApiModelProperty("类型")
+    private String type;
+
+    @ApiModelProperty("值")
+    private String value;
+
+    @ApiModelProperty("参数")
+    private ParamBean param;
+
+}

+ 19 - 0
src/main/java/com/qmth/pdf/tools/bean/BodyBean.java

@@ -0,0 +1,19 @@
+package com.qmth.pdf.tools.bean;
+
+import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
+import com.fasterxml.jackson.annotation.JsonInclude;
+import io.swagger.annotations.ApiModelProperty;
+import lombok.Getter;
+import lombok.Setter;
+
+import java.util.List;
+
+@Getter
+@Setter
+@JsonIgnoreProperties(ignoreUnknown = true)
+@JsonInclude(JsonInclude.Include.NON_NULL)
+public class BodyBean {
+
+    @ApiModelProperty("大题说明")
+    private List<SectionBean> sections;
+}

+ 18 - 0
src/main/java/com/qmth/pdf/tools/bean/ControlBean.java

@@ -0,0 +1,18 @@
+package com.qmth.pdf.tools.bean;
+
+import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
+import com.fasterxml.jackson.annotation.JsonInclude;
+import io.swagger.annotations.ApiModelProperty;
+import lombok.Getter;
+import lombok.Setter;
+
+@Getter
+@Setter
+@JsonIgnoreProperties(ignoreUnknown = true)
+@JsonInclude(JsonInclude.Include.NON_NULL)
+public class ControlBean {
+
+    @ApiModelProperty("考试控制参数,可选")
+    private int maxAnswerTime;
+
+}

+ 17 - 0
src/main/java/com/qmth/pdf/tools/bean/DescriptionBean.java

@@ -0,0 +1,17 @@
+package com.qmth.pdf.tools.bean;
+
+import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
+import io.swagger.annotations.ApiModelProperty;
+import lombok.Getter;
+import lombok.Setter;
+
+@Getter
+@Setter
+@JsonIgnoreProperties(ignoreUnknown = true)
+public class DescriptionBean {
+
+    @ApiModelProperty("描述")
+    private BodyBean description;
+
+
+}

+ 33 - 0
src/main/java/com/qmth/pdf/tools/bean/DetailBean.java

@@ -0,0 +1,33 @@
+package com.qmth.pdf.tools.bean;
+
+import com.fasterxml.jackson.annotation.JsonInclude;
+import io.swagger.annotations.ApiModelProperty;
+import lombok.Getter;
+import lombok.Setter;
+
+import java.util.List;
+
+@Getter
+@Setter
+@JsonInclude(JsonInclude.Include.NON_NULL)
+public class DetailBean {
+
+    @ApiModelProperty("大题号")
+    private int number;
+
+    @ApiModelProperty("大题名称")
+    private String name;
+
+    @ApiModelProperty("小题数量")
+    private int questionCount;
+
+    @ApiModelProperty("大题总分")
+    private Double totalScore;
+
+    @ApiModelProperty("小题集合")
+    private List<QuestionBean> questions;
+
+    @ApiModelProperty("大题说明")
+    private BodyBean description;
+
+}

+ 16 - 0
src/main/java/com/qmth/pdf/tools/bean/OptionBean.java

@@ -0,0 +1,16 @@
+package com.qmth.pdf.tools.bean;
+
+import io.swagger.annotations.ApiModelProperty;
+import lombok.Getter;
+import lombok.Setter;
+
+@Getter
+@Setter
+public class OptionBean {
+
+    @ApiModelProperty("选项序号")
+    private int number;
+
+    @ApiModelProperty("选项内容")
+    private BodyBean body;
+}

+ 44 - 0
src/main/java/com/qmth/pdf/tools/bean/PaperBean.java

@@ -0,0 +1,44 @@
+package com.qmth.pdf.tools.bean;
+
+import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
+import com.fasterxml.jackson.annotation.JsonInclude;
+import io.swagger.annotations.ApiModelProperty;
+import lombok.Getter;
+import lombok.Setter;
+
+import java.util.List;
+
+@Getter
+@Setter
+@JsonIgnoreProperties(ignoreUnknown = true)
+@JsonInclude(JsonInclude.Include.NON_NULL)
+public class PaperBean {
+
+    @ApiModelProperty("试卷名称")
+    private String name;
+
+    @ApiModelProperty("课程代码")
+    private String courseCode;
+
+    @ApiModelProperty("课程名称")
+    private String courseName;
+
+    @ApiModelProperty("试卷总分,100")
+    private int totalScore;
+
+    @ApiModelProperty("大题数量")
+    private int detailCount;
+
+    @ApiModelProperty("试题题干是否包含音频")
+    private boolean hasAudio;
+
+    @ApiModelProperty("大题集合")
+    private List<DetailBean> details;
+
+    @ApiModelProperty("考生姓名")
+    private String studentName;
+
+    @ApiModelProperty("考生学号")
+    private String studentCode;
+
+}

+ 35 - 0
src/main/java/com/qmth/pdf/tools/bean/PaperDetailBean.java

@@ -0,0 +1,35 @@
+package com.qmth.pdf.tools.bean;
+
+import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
+import io.swagger.annotations.ApiModelProperty;
+import lombok.Getter;
+import lombok.Setter;
+
+/**
+ * @Description
+ * @Author haoguanghui
+ * @date 2025/03/12
+ */
+@Getter
+@Setter
+@JsonIgnoreProperties(ignoreUnknown = true)
+public class PaperDetailBean {
+
+    @ApiModelProperty("大题号")
+    private int number;
+
+    @ApiModelProperty("大题名称")
+    private String name;
+
+    @ApiModelProperty("大题总分")
+    private int totalScore;
+
+    @ApiModelProperty("小题数量")
+    private int questionCount;
+
+    @ApiModelProperty("大题说明")
+    private DescriptionBean description;
+
+}
+
+

+ 21 - 0
src/main/java/com/qmth/pdf/tools/bean/ParamBean.java

@@ -0,0 +1,21 @@
+package com.qmth.pdf.tools.bean;
+
+import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
+import com.fasterxml.jackson.annotation.JsonInclude;
+import io.swagger.annotations.ApiModelProperty;
+import lombok.Getter;
+import lombok.Setter;
+
+@Getter
+@Setter
+@JsonIgnoreProperties(ignoreUnknown = true)
+@JsonInclude(JsonInclude.Include.NON_NULL)
+public class ParamBean {
+
+    @ApiModelProperty("宽度")
+    private int width;
+
+    @ApiModelProperty("高度")
+    private int height;
+
+}

+ 59 - 0
src/main/java/com/qmth/pdf/tools/bean/QuestionBean.java

@@ -0,0 +1,59 @@
+package com.qmth.pdf.tools.bean;
+
+import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
+import com.fasterxml.jackson.annotation.JsonInclude;
+import io.swagger.annotations.ApiModelProperty;
+import lombok.Getter;
+import lombok.Setter;
+
+import java.util.List;
+import java.util.Map;
+
+@Getter
+@Setter
+@JsonIgnoreProperties(ignoreUnknown = true)
+@JsonInclude(JsonInclude.Include.NON_NULL)
+public class QuestionBean {
+
+    @ApiModelProperty("小题全局标识")
+    private String id;
+
+    @ApiModelProperty("小题号")
+    private int number;
+
+    @ApiModelProperty("小题分数")
+    private Double score;
+
+    @ApiModelProperty("结构类型:1-单选,2-多选,3-判断,4-填空,5-问答,6-套题,7-听力,8-配对题")
+    private int structType;
+
+    @ApiModelProperty("是否客观题")
+    private boolean objective;
+
+    @ApiModelProperty("答案")
+    // 支持 List<Integer>, boolean, 和 JSON 数组格式
+    private Object answer;
+
+    @ApiModelProperty("试题扩展属性,可选")
+    private Map<String, Object> param;
+
+    @ApiModelProperty("考生答案")
+    //支持 List<Integer>, boolean, 和 JSON 数组格式
+    private Object studentAnswer;
+
+    @ApiModelProperty("考生分数")
+    private Double studentScore;
+
+    @ApiModelProperty("选项集合,单选或多选时存在")
+    private List<OptionBean> options;
+
+    @ApiModelProperty("属性,无用")
+    private List<Object> property;
+
+    @ApiModelProperty("考试控制参数,可选")
+    private ControlBean control;
+
+    @ApiModelProperty("题目说明")
+    private BodyBean body;
+
+}

+ 20 - 0
src/main/java/com/qmth/pdf/tools/bean/SectionBean.java

@@ -0,0 +1,20 @@
+package com.qmth.pdf.tools.bean;
+
+import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
+import com.fasterxml.jackson.annotation.JsonInclude;
+import io.swagger.annotations.ApiModelProperty;
+import lombok.Getter;
+import lombok.Setter;
+
+import java.util.List;
+
+@Getter
+@Setter
+@JsonIgnoreProperties(ignoreUnknown = true)
+@JsonInclude(JsonInclude.Include.NON_NULL)
+public class SectionBean {
+
+    @ApiModelProperty("大题说明")
+    private List<BlockBean> blocks;
+
+}

+ 30 - 0
src/main/java/com/qmth/pdf/tools/bean/StudentAnswerBean.java

@@ -0,0 +1,30 @@
+package com.qmth.pdf.tools.bean;
+
+import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
+import io.swagger.annotations.ApiModelProperty;
+import lombok.Getter;
+import lombok.Setter;
+
+/**
+ * @Description 考生答案bean
+ * @Author haoguanghui
+ * @date 2025/03/13
+ */
+@Getter
+@Setter
+@JsonIgnoreProperties(ignoreUnknown = true)
+public class StudentAnswerBean {
+
+    @ApiModelProperty("大题号")
+    private int mainNumber;
+
+    @ApiModelProperty("小题号")
+    private int subNumber;
+
+    @ApiModelProperty("子题序号")
+    private Integer subIndex;
+
+    @ApiModelProperty("答案")
+    private Object answer;
+
+}

+ 45 - 0
src/main/java/com/qmth/pdf/tools/bean/StudentBean.java

@@ -0,0 +1,45 @@
+package com.qmth.pdf.tools.bean;
+
+import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
+import com.fasterxml.jackson.annotation.JsonProperty;
+import io.swagger.annotations.Api;
+import io.swagger.annotations.ApiModelProperty;
+import lombok.Getter;
+import lombok.Setter;
+import lombok.ToString;
+
+import java.io.Serializable;
+import java.util.List;
+
+/**
+ * @Description 考生信息
+ * @Author haoguanghui
+ * @date 2025/03/11
+ */
+@Getter
+@Setter
+@ToString
+@JsonIgnoreProperties(ignoreUnknown = true)
+public class StudentBean implements Serializable {
+
+    @ApiModelProperty("考生姓名")
+    private String name;
+
+    @ApiModelProperty("学号")
+    private String studentCode;
+
+    @ApiModelProperty("用于获取考生的答案json")
+    private String secretNumber;
+
+    @ApiModelProperty("科目编码,获取试卷的结构json")
+    private String subjectCode;
+
+    @ApiModelProperty("考生客观题得分详情")
+    private List<StudentScoreBean> objectiveScoreDetail;
+
+    @ApiModelProperty("考生主观题得分详情")
+    private List<StudentScoreBean> subjectiveScoreDetail;
+
+    @ApiModelProperty("是否完成评卷")
+    private boolean upload;
+}

+ 29 - 0
src/main/java/com/qmth/pdf/tools/bean/StudentScoreBean.java

@@ -0,0 +1,29 @@
+package com.qmth.pdf.tools.bean;
+
+import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
+import io.swagger.annotations.ApiModelProperty;
+import lombok.Getter;
+import lombok.Setter;
+import lombok.ToString;
+
+/**
+ * @Description 考生主观题得分详情
+ * @Author haoguanghui
+ * @date 2025/03/12
+ */
+@Getter
+@Setter
+@JsonIgnoreProperties(ignoreUnknown = true)
+@ToString
+public class StudentScoreBean {
+
+    @ApiModelProperty("大题序号")
+    private int mainNumber;
+
+    @ApiModelProperty("小题序号")
+    private int subNumber;
+
+    @ApiModelProperty("考生得分")
+    private Double score;
+
+}

+ 23 - 0
src/main/java/com/qmth/pdf/tools/bean/SubjectiveAnswerBean.java

@@ -0,0 +1,23 @@
+package com.qmth.pdf.tools.bean;
+
+import io.swagger.annotations.ApiModelProperty;
+import lombok.Getter;
+import lombok.Setter;
+
+import java.util.List;
+
+/**
+ * @Description 主观题答案
+ * @Author haoguanghui
+ * @date 2025/03/12
+ */
+@Getter
+@Setter
+public class SubjectiveAnswerBean {
+
+    @ApiModelProperty("序号")
+    private int index;
+
+    @ApiModelProperty("答案")
+    private List<SectionBean> sections;
+}

+ 9 - 0
src/main/java/com/qmth/pdf/tools/config/AsyncConfig.java

@@ -0,0 +1,9 @@
+package com.qmth.pdf.tools.config;
+
+import org.springframework.context.annotation.Configuration;
+import org.springframework.scheduling.annotation.EnableAsync;
+
+@Configuration
+@EnableAsync
+public class AsyncConfig {
+}

+ 33 - 0
src/main/java/com/qmth/pdf/tools/config/PdfConfig.java

@@ -0,0 +1,33 @@
+package com.qmth.pdf.tools.config;
+
+import lombok.Getter;
+import org.springframework.beans.factory.annotation.Value;
+import org.springframework.stereotype.Component;
+
+@Component
+@Getter
+public class PdfConfig {
+
+    @Value("${file.output.path}")
+    private String outputPath;
+
+    @Value("${server.student.url}")
+    private String studentUrl;
+
+    @Value("${server.resource.url}")
+    private String resourceUrl;
+
+    @Value("${login.user.name}")
+    private String username;
+
+    @Value("${login.user.password}")
+    private String password;
+
+    @Value("${interface.param.examId}")
+    private String examId;
+
+    @Value("${interface.param.subjectCode}")
+    private String subjectCode;
+
+}
+

+ 40 - 0
src/main/java/com/qmth/pdf/tools/config/RestTemplateConfig.java

@@ -0,0 +1,40 @@
+package com.qmth.pdf.tools.config;
+
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.context.annotation.Bean;
+import org.springframework.context.annotation.Configuration;
+import org.springframework.http.client.ClientHttpRequestFactory;
+import org.springframework.http.client.ClientHttpRequestInterceptor;
+import org.springframework.http.client.SimpleClientHttpRequestFactory;
+import org.springframework.web.client.RestTemplate;
+
+import java.util.ArrayList;
+import java.util.List;
+
+@Configuration
+public class RestTemplateConfig {
+
+    @Autowired
+    private PdfConfig pdfConfig;
+
+    @Bean
+    public RestTemplate restTemplate(ClientHttpRequestFactory factory) {
+        RestTemplate restTemplate = new RestTemplate(factory);
+        List<ClientHttpRequestInterceptor> interceptors = new ArrayList<>();
+        interceptors.add((request, body, execution) -> {
+            request.getHeaders().add("auth-info", "loginname=" + pdfConfig.getUsername() + ";password=" + pdfConfig.getPassword());
+            return execution.execute(request, body);
+        });
+        restTemplate.setInterceptors(interceptors);
+        return restTemplate;
+    }
+
+
+    @Bean
+    public ClientHttpRequestFactory simpleClientHttpRequestFactory() {
+        SimpleClientHttpRequestFactory factory = new SimpleClientHttpRequestFactory();
+        factory.setConnectTimeout(15000); // 连接超时时间(ms)
+        factory.setReadTimeout(30000);    // 读取超时时间(ms)
+        return factory;
+    }
+}

+ 22 - 0
src/main/java/com/qmth/pdf/tools/config/SwaggerConfig.java

@@ -0,0 +1,22 @@
+package com.qmth.pdf.tools.config;
+
+import org.springframework.context.annotation.Bean;
+import org.springframework.context.annotation.Configuration;
+import springfox.documentation.builders.PathSelectors;
+import springfox.documentation.builders.RequestHandlerSelectors;
+import springfox.documentation.spi.DocumentationType;
+import springfox.documentation.spring.web.plugins.Docket;
+import springfox.documentation.swagger2.annotations.EnableSwagger2;
+
+@Configuration
+@EnableSwagger2
+public class SwaggerConfig {
+    @Bean
+    public Docket api() {
+        return new Docket(DocumentationType.SWAGGER_2)
+                .select()
+                .apis(RequestHandlerSelectors.basePackage("com.qmth.pdf.tools.control"))
+                .paths(PathSelectors.any())
+                .build();
+    }
+}

+ 37 - 0
src/main/java/com/qmth/pdf/tools/config/ThreadPoolConfig.java

@@ -0,0 +1,37 @@
+package com.qmth.pdf.tools.config;
+
+import org.springframework.context.annotation.Bean;
+import org.springframework.context.annotation.Configuration;
+import org.springframework.scheduling.concurrent.ThreadPoolTaskExecutor;
+
+import java.util.concurrent.ThreadPoolExecutor;
+
+@Configuration
+public class ThreadPoolConfig {
+
+    @Bean
+    public ThreadPoolTaskExecutor taskExecutor() {
+        ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor();
+
+        // 获取可用处理器核心数
+        int availableProcessors = Runtime.getRuntime().availableProcessors();
+
+        // 设置核心线程数为核心数
+        executor.setCorePoolSize(availableProcessors);
+
+        // 设置最大线程数为核心数的两倍
+        executor.setMaxPoolSize(availableProcessors * 2);
+
+        // 设置队列大小为核心数的四倍
+        executor.setQueueCapacity(availableProcessors * 4);
+
+        executor.setThreadNamePrefix("StudentPaperTaskExecutor-");
+        executor.setRejectedExecutionHandler(new ThreadPoolExecutor.CallerRunsPolicy()); // 拒绝策略
+        executor.setKeepAliveSeconds(60); // 线程空闲时间
+        executor.setAllowCoreThreadTimeOut(true); // 允许核心线程超时
+        executor.initialize();
+        return executor;
+    }
+}
+
+

+ 35 - 0
src/main/java/com/qmth/pdf/tools/constant/SystemConstant.java

@@ -0,0 +1,35 @@
+package com.qmth.pdf.tools.constant;
+
+/**
+ * @Description
+ * @Author haoguanghui
+ * @date 2025/03/11
+ */
+public class SystemConstant {
+
+    //接口请求前缀
+    public static final String DEFAULT_URI_PREFIX = "/api";
+
+    //考生成绩接口地址
+    public final static String STUDENT_SCORE_URL = "/api/exam/students";
+
+    //试卷结构接口地址前缀
+    public final static String PAPER_URL_PREFIX = "/paper/";
+
+    //考生试卷答案接口地址前缀
+    public final static String STUDENT_ANSWER_URL_PREFIX = "/json/";
+
+    //成功代码
+    public final static int SUCCESS_CODE = 200;
+
+    //成功信息
+    public final static String SUCCESS_MSG = "成功";
+
+    //等待信息
+    public final static String WAIT_MSG = "正在执行中,执行过程请查看日志";
+
+    //分页数量
+    public final static int PAGE_SIZE = 100;
+
+
+}

+ 34 - 0
src/main/java/com/qmth/pdf/tools/control/StudentJsonControl.java

@@ -0,0 +1,34 @@
+package com.qmth.pdf.tools.control;
+
+import com.qmth.pdf.tools.constant.SystemConstant;
+import com.qmth.pdf.tools.service.StudentPaperService;
+import com.qmth.pdf.tools.util.Result;
+import com.qmth.pdf.tools.util.ResultUtil;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.web.bind.annotation.RequestMapping;
+import org.springframework.web.bind.annotation.RestController;
+
+/**
+ * @Description 学生的试卷json处理控制器
+ * @Author haoguanghui
+ * @date 2025/03/11
+ */
+@RestController
+@RequestMapping(SystemConstant.DEFAULT_URI_PREFIX)
+public class StudentJsonControl {
+
+    @Autowired
+    private StudentPaperService studentPaperService;
+
+    /**
+     * 生成考生的试卷json文件
+     * @param examId 考试ID-不能为空
+     * @param subjectCode 科目代码-可为空
+     */
+    @RequestMapping("/student/paper/output")
+    public Result outputStudentPaperJson(Long examId, String subjectCode){
+        studentPaperService.outputStudentPaperJson(examId, subjectCode);
+        return ResultUtil.ok(SystemConstant.WAIT_MSG);
+    }
+
+}

+ 11 - 0
src/main/java/com/qmth/pdf/tools/service/StudentPaperService.java

@@ -0,0 +1,11 @@
+package com.qmth.pdf.tools.service;
+
+/**
+ * @Description 考试试卷服务
+ * @Author haoguanghui
+ * @date 2025/03/11
+ */
+public interface StudentPaperService {
+
+    void outputStudentPaperJson(Long examId, String subjectCode);
+}

+ 413 - 0
src/main/java/com/qmth/pdf/tools/service/impl/StudentPaperServiceImpl.java

@@ -0,0 +1,413 @@
+package com.qmth.pdf.tools.service.impl;
+
+import com.fasterxml.jackson.core.type.TypeReference;
+import com.fasterxml.jackson.databind.ObjectMapper;
+import com.google.common.cache.Cache;
+import com.google.common.cache.CacheBuilder;
+import com.qmth.pdf.tools.bean.*;
+import com.qmth.pdf.tools.config.PdfConfig;
+import com.qmth.pdf.tools.constant.SystemConstant;
+import com.qmth.pdf.tools.service.StudentPaperService;
+import com.qmth.pdf.tools.util.HttpClientUtil;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.scheduling.annotation.Async;
+import org.springframework.scheduling.concurrent.ThreadPoolTaskExecutor;
+import org.springframework.stereotype.Service;
+import org.springframework.util.CollectionUtils;
+import org.springframework.util.LinkedMultiValueMap;
+import org.springframework.util.MultiValueMap;
+import org.springframework.web.client.RestTemplate;
+
+import java.io.File;
+import java.io.FileWriter;
+import java.io.IOException;
+import java.nio.charset.StandardCharsets;
+import java.nio.file.Files;
+import java.nio.file.Paths;
+import java.util.*;
+import java.util.concurrent.CompletableFuture;
+import java.util.concurrent.ConcurrentHashMap;
+import java.util.concurrent.TimeUnit;
+import java.util.concurrent.atomic.AtomicInteger;
+import java.util.concurrent.locks.ReentrantLock;
+import java.util.stream.Collectors;
+
+/**
+ * @Description 学生的试卷json处理Service
+ * @Author haoguanghui
+ * @date 2025/03/13
+ */
+@Service
+public class StudentPaperServiceImpl implements StudentPaperService {
+
+    private final static Logger log = LoggerFactory.getLogger(StudentPaperServiceImpl.class);
+
+    private final PdfConfig pdfConfig;
+
+    private final HttpClientUtil httpClient;
+
+    private final ThreadPoolTaskExecutor taskExecutor;
+
+    // 内存锁
+    private final ConcurrentHashMap<Long, ReentrantLock> lockMap = new ConcurrentHashMap<>();
+
+    // 缓存
+    private final Cache<String, PaperBean> paperCache;
+
+    // 使用单例模式的 ObjectMapper
+    private static final ObjectMapper objectMapper = new ObjectMapper();
+
+    // 考生答案类型引用
+    private static final TypeReference<List<StudentAnswerBean>> STUDENT_ANSWER_LIST_TYPE_REFERENCE =
+            new TypeReference<List<StudentAnswerBean>>() {};
+
+    @Autowired
+    public StudentPaperServiceImpl(RestTemplate restTemplate, PdfConfig pdfConfig, ThreadPoolTaskExecutor taskExecutor) {
+        this.httpClient = new HttpClientUtil(restTemplate);
+        this.pdfConfig = pdfConfig;
+        this.taskExecutor = taskExecutor;
+        this.paperCache = CacheBuilder.newBuilder()
+                .maximumSize(100) // 最大缓存数量
+                .expireAfterWrite(10, TimeUnit.MINUTES) // 缓存10分钟后过期
+                .build();
+    }
+
+
+    @Override
+    @Async
+    public void outputStudentPaperJson(Long examId, String subjectCode) {
+        if (examId == null) {
+            log.error("examId不能为空");
+            throw new IllegalArgumentException("参数错误");
+        }
+
+        ReentrantLock lock = lockMap.computeIfAbsent(examId, k -> new ReentrantLock());
+
+        // 尝试获取锁
+        if (!lock.tryLock()) {
+            log.warn("正在生成考生的试卷,请不要重复执行,examId: {}, subjectCode: {}", examId, subjectCode);
+            return;
+        }
+
+        try {
+            long startTime = System.currentTimeMillis();
+            log.warn("开始生成考生的试卷json文件,examId: {}, subjectCode: {}", examId, subjectCode);
+
+            //分页查询所有考生
+            List<StudentBean> allStudentBeans = fetchAllStudentWithPagination(examId, subjectCode);
+            if (CollectionUtils.isEmpty(allStudentBeans)) {
+                log.warn("没有找到符合条件的考生,examId: {}, subjectCode: {}", examId, subjectCode);
+                return;
+            }
+            log.warn("待生成json文件的考生数量:{},examId:{}, subjectCode:{}", allStudentBeans.size(), examId, subjectCode);
+
+            AtomicInteger successCounter = new AtomicInteger(0);
+            List<CompletableFuture<Void>> futures = new ArrayList<>();
+
+            for (StudentBean studentBean : allStudentBeans) {
+                CompletableFuture<Void> future = CompletableFuture.runAsync(() -> {
+                            try {
+                                processStudent(studentBean, examId);
+                                successCounter.incrementAndGet(); // 成功加1
+                            } catch (Exception ex) {
+                                log.error("处理考生信息失败,studentCode: {}, studentName: {}, examId: {}, subjectCode: {}",
+                                        studentBean.getStudentCode(), studentBean.getName(), examId, subjectCode, ex);
+                            }
+                        }, taskExecutor)
+                        .exceptionally(ex -> {
+                            log.error("考生试卷生成失败,studentCode: {}, studentName: {}, examId: {}, subjectCode: {}",
+                                    studentBean.getStudentCode(), studentBean.getName(), examId, subjectCode, ex);
+                            return null;
+                        });
+
+                futures.add(future);
+            }
+
+            // 等待所有任务完成
+            CompletableFuture.allOf(futures.toArray(new CompletableFuture[0])).join();
+
+            long endTime = System.currentTimeMillis();
+            log.warn("完成考生试卷的生成,耗时: {}s, 处理成功的考生数量: {}, examId: {}, subjectCode: {}",
+                    (endTime - startTime) / 1000, successCounter.get(), examId, subjectCode);
+
+        } finally {
+            lock.unlock();
+            // 移除不再需要的锁
+            lockMap.remove(examId, lock);
+        }
+    }
+
+    private void processStudent(StudentBean studentBean, Long examId) {
+        String subjectCode = studentBean.getSubjectCode();
+        PaperBean paperBean;
+
+        // 从缓存中获取试卷结构
+        if ((paperBean = paperCache.getIfPresent(subjectCode)) == null) {
+            synchronized (this) {
+                if ((paperBean = paperCache.getIfPresent(subjectCode)) == null) {
+                    try {
+                        paperBean = fetchPaperStructure(examId, subjectCode);
+                        if (paperBean != null) {
+                            paperCache.put(subjectCode, paperBean);
+                        }
+                    } catch (Exception e) {
+                        log.error("从远程获取试卷结构失败, examId:{}, subjectCode:{}", examId, subjectCode, e);
+                    }
+                }
+            }
+        }
+
+        if (paperBean == null) {
+            log.warn("找不到该科目对应的试卷, examId:{}, subjectCode:{}", examId, subjectCode);
+            return;
+        }
+
+        //试卷的大题集合
+        List<DetailBean> details = paperBean.getDetails();
+        if (CollectionUtils.isEmpty(details)) {
+            log.warn("试卷不存在题目, examId:{}, subjectCode:{}", examId, subjectCode);
+            return;
+        }
+
+        //填充考生姓名和学号
+        paperBean.setStudentName(studentBean.getName());
+        paperBean.setStudentCode(studentBean.getStudentCode());
+
+        // 获取考生的答案
+        List<StudentAnswerBean> studentAnswerBeans;
+        try {
+            studentAnswerBeans = fetchStudentScore(examId, studentBean.getSecretNumber());
+        } catch (Exception e) {
+            log.error("获取考生答案失败, examId:{}, secretNumber:{}", examId, studentBean.getSecretNumber(), e);
+            return;
+        }
+
+        // 填充考生的答案和得分
+        fillStudentScoreAndAnswer(studentBean, studentAnswerBeans, details);
+
+        // 将填充后的试卷写到文件中
+        outputToFile(studentBean, paperBean, examId);
+    }
+
+    private void outputToFile(StudentBean studentBean, PaperBean paperBean, Long examId) {
+        try {
+            String json = objectMapper.writeValueAsString(paperBean);
+            //文件名称:姓名_学号.json
+            String fileName = studentBean.getName() + "_" + studentBean.getStudentCode() + ".json";
+            String filePath = pdfConfig.getOutputPath() + examId + "/" + fileName;
+
+            // 判断目录是否存在,不存在,创建
+            File file = new File(filePath);
+            if (!file.getParentFile().exists()) {
+                file.getParentFile().mkdirs();
+            }
+            Files.write(Paths.get(filePath), json.getBytes(StandardCharsets.UTF_8));
+        } catch (Exception e) {
+            log.error("文件写入失败,studentCode: {}, 文件名: {}", studentBean.getStudentCode(), studentBean.getName(), e);
+        }
+    }
+
+    private void fillStudentScoreAndAnswer(StudentBean studentBean, List<StudentAnswerBean> studentAnswerBeans, List<DetailBean> details) {
+        //客观题得分
+        List<StudentScoreBean> objectiveScoreDetail = studentBean.getObjectiveScoreDetail();
+        //主观题得分
+        List<StudentScoreBean> subjectiveScoreDetail = studentBean.getSubjectiveScoreDetail();
+
+        //考生答案map
+        Map<String, StudentAnswerBean> answerMap = buildAnswerMap(studentAnswerBeans);
+        //客观题得分map
+        Map<String, Double> objectiveScoreMap = buildScoreMap(objectiveScoreDetail);
+        //主观题得分map
+        Map<String, Double> subjectiveScoreMap = buildScoreMap(subjectiveScoreDetail);
+
+        for (DetailBean detail : details) {
+            //大题号
+            int mainNumber = detail.getNumber();
+            List<QuestionBean> questions = detail.getQuestions();
+            for (QuestionBean question : questions) {
+                //小题号
+                int subNumber = question.getNumber();
+                boolean objective = question.isObjective();
+                //考生得分
+                question.setStudentScore(getStudentScore(mainNumber, subNumber, objective, objectiveScoreMap, subjectiveScoreMap));
+                //考生答案
+                question.setStudentAnswer(getStudentAnswer(answerMap, mainNumber, subNumber));
+            }
+        }
+    }
+
+
+    private Object getStudentAnswer(Map<String, StudentAnswerBean> answerMap, int mainNumber, int subNumber) {
+        String key = getKey(mainNumber, subNumber);
+        StudentAnswerBean answerBean = answerMap.get(key);
+        return answerBean != null ? answerBean.getAnswer() : "";
+    }
+
+    private Double getStudentScore(int mainNumber, int subNumber, boolean objective, Map<String, Double> objectiveScoreMap,
+            Map<String, Double> subjectiveScoreMap) {
+        // 根据 objective 选择对应的 Map
+        Map<String, Double> scoreMap = objective ? objectiveScoreMap : subjectiveScoreMap;
+        String key = getKey(mainNumber, subNumber);
+
+        return scoreMap.getOrDefault(key, 0.0);
+    }
+
+    private String getKey(int mainNumber, int subNumber) {
+        return mainNumber + "-" + subNumber;
+    }
+
+    private Map<String, Double> buildScoreMap(List<StudentScoreBean> scoreDetail) {
+        if (scoreDetail == null) {
+            return new HashMap<>();
+        }
+        Map<String, Double> scoreMap = new HashMap<>();
+        for (StudentScoreBean studentScoreBean : scoreDetail) {
+            if (studentScoreBean != null) {
+                String key = getKey(studentScoreBean.getMainNumber(), studentScoreBean.getSubNumber());
+                scoreMap.put(key, studentScoreBean.getScore());
+            }
+        }
+        return scoreMap;
+    }
+
+    private Map<String, StudentAnswerBean> buildAnswerMap(List<StudentAnswerBean> studentAnswerBeans) {
+        Map<String, StudentAnswerBean> answerMap = new HashMap<>();
+        for (StudentAnswerBean answerBean : studentAnswerBeans) {
+            String key = getKey(answerBean.getMainNumber(), answerBean.getSubNumber());
+            answerMap.put(key, answerBean);
+        }
+        return answerMap;
+    }
+
+    /**
+     *  获取考生的答案
+     */
+    private List<StudentAnswerBean> fetchStudentScore(Long examId, String secretNumber) {
+        // secretNumber不能为空且长度要大于3
+        if (secretNumber == null || secretNumber.length() < 3) {
+            log.error("secretNumber不能为空且长度要大于3");
+            return Collections.emptyList();
+        }
+
+        //提取 secretNumber最后3位数字
+        String path = secretNumber.substring(secretNumber.length() - 3);
+        // 获取学生成绩url
+        String url = String.format("%s%s/%s/%s/%s.json", pdfConfig.getResourceUrl(), SystemConstant.STUDENT_ANSWER_URL_PREFIX, examId, path, secretNumber);
+
+        try {
+            // 获取考生的答案
+            String response = httpClient.get(url, null, null);
+            if (response == null || response.isEmpty()) {
+                log.warn("考生答案返回为空。examId:{}, secretNumber:{}", examId, secretNumber);
+                return Collections.emptyList();
+            }
+            // 解析 JSON 响应
+            return objectMapper.readValue(response, STUDENT_ANSWER_LIST_TYPE_REFERENCE);
+        } catch (Exception e) {
+            log.error("获取考生列表失败,examId:{}, secretNumber:{}, url:{}", examId, secretNumber, url, e);
+        }
+        return Collections.emptyList();
+    }
+
+    // 获取试卷结构
+    private PaperBean fetchPaperStructure(Long examId, String subjectCode) {
+        if (subjectCode == null) {
+            log.error("科目编码不能为空");
+            throw new IllegalArgumentException("参数错误");
+        }
+        try {
+            // 请求获取试卷结构
+            String url = String.format("%s%s%s/%s.json", pdfConfig.getResourceUrl(), SystemConstant.PAPER_URL_PREFIX, examId, subjectCode);
+            String response = httpClient.get(url, null, null);
+            if (response == null || response.isEmpty()) {
+                log.warn("获取试卷结构返回为空。examId:{}, subjectCode:{}", examId, subjectCode);
+                return null;
+            }
+            //将response返回的结果,写入到文件中
+         /*   try {
+                File file = new File("D:/test/test.json");
+                if(!file.exists()) {
+                    file.createNewFile();
+                }
+                if(file.length() <= 0) {
+                    log.warn(response);
+                    writeStringToFile(file, response, "UTF-8");
+                }
+            } catch (IOException e) {
+            log.error("写入文件失败", e);
+        }*/
+            // 解决返回的json有特殊字符的问题
+            int start = response.indexOf("{");
+            if (start > 0) {
+                response = response.substring(start);
+            }
+            return objectMapper.readValue(response, PaperBean.class);
+        } catch (Exception e) {
+            log.error("获取试卷结构失败", e);
+        }
+        return null;
+    }
+
+    private void writeStringToFile(File file, String data, String encoding) throws IOException {
+        try (FileWriter fileWriter = new FileWriter(file, true)) {
+            fileWriter.write(data);
+        }
+    }
+
+
+    // 分页查询所有考生
+    private List<StudentBean> fetchAllStudentWithPagination(Long examId, String subjectCode) {
+        List<StudentBean> allStudentBeans = new ArrayList<>();
+        int pageNumber = 1;
+        while (true) {
+            try {
+                List<StudentBean> pageResult = fetchPageStudent(examId, subjectCode, pageNumber);
+                if (CollectionUtils.isEmpty(pageResult)) {
+                    break;
+                }
+                //过滤未评卷的考生
+                pageResult =  pageResult.stream()
+                        .filter(StudentBean::isUpload)
+                        .collect(Collectors.toList());
+                if (!pageResult.isEmpty()) {
+                    allStudentBeans.addAll(pageResult);
+                }
+                pageNumber++;
+            } catch (Exception e) {
+                log.error("分页查询考生信息失败,examId: {}, pageNum: {}", examId, pageNumber, e);
+                throw new RuntimeException("分页查询考生信息失败", e);
+            }
+        }
+        return allStudentBeans;
+    }
+
+    private List<StudentBean> fetchPageStudent(Long examId, String subjectCode, int pageNum) {
+        // 参数设置
+        MultiValueMap<String, String> params = new LinkedMultiValueMap<>();
+        params.put("examId", Collections.singletonList(String.valueOf(examId)));
+        params.put("withScoreDetail", Collections.singletonList(String.valueOf(Boolean.TRUE)));
+        params.put("pageNumber", Collections.singletonList(String.valueOf(pageNum)));
+        params.put("pageSize", Collections.singletonList(String.valueOf(SystemConstant.PAGE_SIZE)));
+        if (subjectCode != null && !subjectCode.isEmpty()) {
+            params.put("subjectCode", Collections.singletonList(subjectCode));
+        }
+
+        //请求获取考生列表
+        String response = httpClient.postForm(pdfConfig.getStudentUrl() + SystemConstant.STUDENT_SCORE_URL, null, params);
+
+        if(response == null || response.isEmpty()) {
+            log.warn("考生列表响应为空: examId:{}, pageNum:{}", examId, pageNum);
+            return Collections.emptyList();
+        }
+
+        try {
+            return objectMapper.readValue(response, new TypeReference<List<StudentBean>>() {});
+        } catch (Exception e) {
+            log.error("获取考生列表失败: examId:{}, pageNum:{}, error:{}", examId, pageNum, e.getMessage(), e);
+            return Collections.emptyList();
+        }
+    }
+
+}

+ 89 - 0
src/main/java/com/qmth/pdf/tools/util/HttpClientUtil.java

@@ -0,0 +1,89 @@
+package com.qmth.pdf.tools.util;
+
+import org.springframework.http.*;
+import org.springframework.util.MultiValueMap;
+import org.springframework.web.client.RestTemplate;
+import org.springframework.web.util.UriComponentsBuilder;
+
+import java.nio.charset.StandardCharsets;
+import java.util.Collections;
+import java.util.Map;
+import java.util.Objects;
+
+/**
+ * http请求工具类
+ */
+public class HttpClientUtil {
+
+    private final RestTemplate restTemplate;
+
+    public HttpClientUtil(RestTemplate restTemplate) {
+        this.restTemplate = restTemplate;
+    }
+
+    /**
+     * 通用请求方法
+     *
+     * @param url     请求地址
+     * @param method  请求方法
+     * @param headers 请求头
+     * @param params  请求参数(GET/DELETE时使用)
+     * @param body    请求体(POST/PUT时使用)
+     * @return 响应内容
+     */
+    public String request(String url, HttpMethod method,
+            HttpHeaders headers,
+            MultiValueMap<String, String> params,
+            Object body) {
+
+        // 设置 Accept-Charset 为 UTF-8
+        headers.setAcceptCharset(Collections.singletonList(StandardCharsets.UTF_8));
+        // 构建请求实体
+        HttpEntity<?> requestEntity = new HttpEntity<>(body, headers);
+
+        // 处理URL参数
+        UriComponentsBuilder builder = UriComponentsBuilder.fromHttpUrl(url);
+        if (params != null) {
+            builder.queryParams(params);
+        }
+
+        // 发送请求
+        ResponseEntity<byte[]> response = restTemplate.exchange(
+                builder.build().toUri(),
+                method,
+                requestEntity,
+                byte[].class
+        );
+
+
+        // 强制设置字符编码为 UTF-8
+        return new String(Objects.requireNonNull(response.getBody()), StandardCharsets.UTF_8);
+    }
+
+    // GET请求
+    public String get(String url, Map<String, String> headers, MultiValueMap<String, String> params) {
+        HttpHeaders httpHeaders = new HttpHeaders();
+        if (headers != null) {
+            httpHeaders.setAll(headers);
+        }
+        return request(url, HttpMethod.GET, httpHeaders, params, null);
+    }
+
+    // POST请求
+    public String postJson(String url, Map<String, String> headers, Object body) {
+        HttpHeaders httpHeaders = new HttpHeaders();
+        httpHeaders.setAll(headers);
+        httpHeaders.setContentType(MediaType.APPLICATION_JSON);
+        return request(url, HttpMethod.POST, httpHeaders, null, body);
+    }
+
+    // POST请求(表单提交)
+    public String postForm(String url, Map<String, String> headers, MultiValueMap<String, String> formData) {
+        HttpHeaders httpHeaders = new HttpHeaders();
+        if (headers != null) {
+            httpHeaders.setAll(headers);
+        }
+        httpHeaders.setContentType(MediaType.APPLICATION_FORM_URLENCODED);
+        return request(url, HttpMethod.POST, httpHeaders, null, formData);
+    }
+}

+ 48 - 0
src/main/java/com/qmth/pdf/tools/util/Result.java

@@ -0,0 +1,48 @@
+package com.qmth.pdf.tools.util;
+
+import lombok.Getter;
+import lombok.Setter;
+
+import java.io.Serializable;
+
+
+@Getter
+@Setter
+public class Result implements Serializable {
+
+    private int code;
+
+    private String message;
+
+    private Object data;
+
+    public Result() {
+
+    }
+
+    public Result(int code) {
+        this.code = code;
+    }
+
+    public Result(String message) {
+        this.message = message;
+    }
+
+    public Result(int code, String message) {
+        this.code = code;
+        this.message = message;
+    }
+
+    public Result(int code, String message, Object data) {
+        this.code = code;
+        this.message = message;
+        this.data = data;
+    }
+
+    public Result(int code, Object data, String message) {
+        this.code = code;
+        this.data = data;
+        this.message = message;
+    }
+
+}

+ 35 - 0
src/main/java/com/qmth/pdf/tools/util/ResultUtil.java

@@ -0,0 +1,35 @@
+package com.qmth.pdf.tools.util;
+
+import com.qmth.pdf.tools.constant.SystemConstant;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import java.util.Collections;
+
+public class ResultUtil {
+
+    public static Result success(Object object) {
+        return new Result(SystemConstant.SUCCESS_CODE, SystemConstant.SUCCESS_MSG, object);
+    }
+
+    public static Result success() {
+        return success(null);
+    }
+
+    public static Result ok(String message) {
+        return new Result(SystemConstant.SUCCESS_CODE, message);
+    }
+
+    public static Result ok(Object data) {
+        return new Result(SystemConstant.SUCCESS_CODE, data, SystemConstant.SUCCESS_MSG);
+    }
+
+    public static Result error(int code, String message) {
+        return new Result(code, message);
+    }
+
+    public static Result ok(boolean success) {
+        return new Result(SystemConstant.SUCCESS_CODE, Collections.singletonMap("success", success),SystemConstant.SUCCESS_MSG);
+    }
+
+}

+ 29 - 0
src/main/resources/application.properties

@@ -0,0 +1,29 @@
+#项目端口
+server.port=8081
+#项目名称
+spring.application.name=pdf-tools
+
+#日志配置
+logging.pattern.console=%d{yyyy-MM-dd HH:mm:ss} - %msg%n
+# 日志级别
+logging.level.root=INFO
+# 日志文件输出
+logging.file.name=d:/pdf/pdf.log
+
+#考生外部接口请求地址
+server.student.url=http://192.168.10.65:8000
+#资源接口机请求地址
+server.resource.url=http://192.168.10.65:9000
+
+#登录用户名
+login.user.name=admin-pzhu
+#登录密码
+login.user.password=123456
+
+#json文件输出目录
+file.output.path=d:/pdf/
+
+#生成试卷json的参数examId,如需生成多个考试,以英文逗号隔开
+interface.param.examId=715
+#生成试卷json的参数subjectCode,非必填字段,当examId为多个的时候,subjectCode不起作用
+interface.param.subjectCode=