
简介这是一套基于Spring Boot和MyBatis Plus的代码作业查重系统完整源码面向需要实现作业提交、自动查重与评分管理场景的各类开发者适合作为毕业设计或企业级教学平台的基础。系统覆盖学生、教师、管理员三类角色支持作业发布、代码/PDF提交、JPlag查重、评分反馈及小组分组管理后端集成JWT认证、Redis缓存与Swagger接口文档。压缩包内共168个文件包含95个Java后端源码、21个Vue前端页面、16个XML配置、11个SQL脚本及1个JPlag查重工具JAR包整体大小仅2.36MB目录结构清晰便于二次开发。通过该资源可完整了解Spring Boot与MyBatis Plus的整合实践、JWT无状态认证的实现方式以及Redis在用户会话与缓存场景下的运用并可直接复用代码查重、分组管理等核心模块。目前已有127人浏览学习适合计算机专业学生、初中级Java开发者在实战中快速上手。1. 代码作业查重系统Spring Boot 与 MyBatis Plus 组合的真实落地项目期末收了三百份 Java 课程作业靠肉眼比对找出互相抄的代码这种事干过一次就不想干第二次。这份基于 Spring Boot MyBatis Plus 的代码作业查重系统把「提交 → 查重 → 出报告」整条链路做成了可运行的服务适合做 Java 课程设计、或者想把这套能力接到内部评审流程的开发者直接复现。系统本身不花哨但该有的都有文件上传、数据库落库、查重算法、相似度报告。它把最烦的两两比对交给了程序而不是交给眼睛——这也是我做课程设计时最看重的点。2. 架构与数据模型为什么选 Spring Boot MyBatis Plus 做查重服务2.1 模块划分与工程目录拿到源码先别急着跑。先看工程结构你才知道后面配置改哪里。这个系统按标准 Controller-Service-Mapper 三层拆开包名是com.example.plagiarism我拆过的查重系统里九成都是这个骨架src/main/java/com/example/plagiarism ├── common # 统一返回结果、异常处理 ├── controller # 作业接口、提交接口、报告接口 ├── service # 业务逻辑与查重调度 │ ├── SubmissionService.java │ └── PlagiarismService.java ├── mapper # MyBatis Plus 的 Mapper 接口 ├── entity # 与表结构对应的实体类 ├── dto # 接口入参与出参模型 └── PlagiarismApplication.java # 启动类这个分层很直白接口层只做参数校验和结果封装查重的核心逻辑全在PlagiarismService里。你接手之后如果需要加功能比如新增「按小组查重」或「导出 PDF 报告」不改动 Controller 也能往里塞因为服务层和接口层是解耦的。2.2 四张核心业务表的设计查重系统绕不开四张表学生表、作业表、提交记录表、查重结果表。这个设计几乎是固定范式因为查重的最小单位是「同一份作业下两个学生的提交记录」CREATE TABLE student ( id BIGINT PRIMARY KEY AUTO_INCREMENT, student_no VARCHAR(32) NOT NULL UNIQUE COMMENT 学号, student_name VARCHAR(64) NOT NULL COMMENT 姓名, class_name VARCHAR(128) DEFAULT COMMENT 班级, created_at DATETIME DEFAULT CURRENT_TIMESTAMP ); CREATE TABLE homework ( id BIGINT PRIMARY KEY AUTO_INCREMENT, title VARCHAR(128) NOT NULL COMMENT 作业标题, teacher_id BIGINT NOT NULL COMMENT 布置老师ID, check_threshold DECIMAL(5,2) DEFAULT 80.00 COMMENT 相似度阈值, 超过则告警, created_at DATETIME DEFAULT CURRENT_TIMESTAMP ); CREATE TABLE submission ( id BIGINT PRIMARY KEY AUTO_INCREMENT, homework_id BIGINT NOT NULL COMMENT 所属作业ID, student_id BIGINT NOT NULL COMMENT 提交学生ID, file_path VARCHAR(255) NOT NULL COMMENT 源码文件存储路径, content_hash VARCHAR(64) NOT NULL COMMENT 源码SHA-256哈希, submit_time DATETIME DEFAULT CURRENT_TIMESTAMP, UNIQUE KEY uk_homework_student (homework_id, student_id) ); CREATE TABLE plagiarism_result ( id BIGINT PRIMARY KEY AUTO_INCREMENT, homework_id BIGINT NOT NULL, submission_a_id BIGINT NOT NULL, submission_b_id BIGINT NOT NULL, similarity_score DECIMAL(5,2) NOT NULL COMMENT 相似度百分比, status TINYINT DEFAULT 0 COMMENT 0待老师复核 1已确认 2误判, created_at DATETIME DEFAULT CURRENT_TIMESTAMP, KEY idx_homework (homework_id) );注意submission表里那个唯一键uk_homework_student它保证同一个学生只能提交一次防止重复提交刷数据。查重结果表里存的是「对称的两条记录」——我不建议为了避免重复只存一份因为后续要按维度聚合对称存反而好写 SQL。另外homework表里的check_threshold很关键它把「多少算抄袭」的判定权交给老师而不是写死在代码里。2.3 MyBatis Plus 的选型理由与使用边界数据层用 MyBatis Plus 而不是原生 MyBatis核心诉求就一条单表 CRUD 不用写 SQL。比如新增一条查重结果记录直接用BaseMapper接口就完了Mapper public interface PlagiarismResultMapper extends BaseMapperPlagiarismResult { }查重结果入库时不需要手写INSERT INTO语句PlagiarismResult result new PlagiarismResult(); result.setHomeworkId(homeworkId); result.setSubmissionAId(submissionA.getId()); result.setSubmissionBId(submissionB.getId()); result.setSimilarityScore(score); result.setStatus(0); plagiarismResultMapper.insert(result);insert方法是 BaseMapper 自带的字段自动映射。MyBatis Plus 在这里的边界是「单表操作用自带的多表关联再写自定义 SQL」。比如列出某次作业的查重报告要关联学生姓名和作业标题这时候就别硬用 Wrapper 强行拼直接在 Mapper 里写Select注解还更直观Select(SELECT r.id, s1.student_name AS student_a_name, s2.student_name AS student_b_name, r.similarity_score, r.status FROM plagiarism_result r LEFT JOIN submission a ON r.submission_a_id a.id LEFT JOIN student s1 ON a.student_id s1.id LEFT JOIN submission b ON r.submission_b_id b.id LEFT JOIN student s2 ON b.student_id s2.id WHERE r.homework_id #{homeworkId} ORDER BY r.similarity_score DESC) ListReportVO selectReportByHomeworkId(Param(homeworkId) Long homeworkId);这是我实际用下来最顺手的分工。凡是单表操作依赖 MyBatis Plus 的LambdaQueryWrapper比如查某门作业下所有提交记录ListSubmission submissions submissionMapper.selectList( new LambdaQueryWrapperSubmission() .eq(Submission::getHomeworkId, homeworkId) .orderByAsc(Submission::getStudentId) );LambdaQueryWrapper的好处是类型安全字段名写错了编译期就报错不像拼字符串那样运行时才发现列名不存在。注意orderByAsc一定要在eq之后链式调用顺序反了会有告警不影响结果但日志看着难受。3. 查重算法实现从代码归一化到相似度计算3.1 预处理去注释去空白别让格式干扰判断查重最忌讳的是直接拿原文件做字符串比对。同一个逻辑一个人用 Tab 缩进、另一个人用空格或者一个注释写满了、另一个删干净了就直接比对会得到截然不同的结果。所以第一步永远是代码归一化。我一般按三条规则做预处理——去注释、去空白、去字符串字面量public class CodeNormalizer { public static String normalize(String source) { if (source null || source.isEmpty()) { return ; } // 1. 去掉单行注释 // 开头到行尾 source source.replaceAll(//[^\\n]*, ); // 2. 去掉多行注释 /* ... */ source source.replaceAll(/\\*.*?\\*/, , Pattern.DOTALL); // 3. 去掉字符串字面量, 统一替换成空字符串 source source.replaceAll(\[^\]*\, \\, Pattern.DOTALL); // 4. 统一空白: 连续空白变成单个空格 source source.replaceAll(\\s, ); return source.trim(); } }逻辑说明第一步正则//[^\n]*匹配从双斜杠到行尾的内容Java 代码里最常见的注释格式。第二步/*.*?*/配Pattern.DOTALL是因为多行注释可能跨行.*?非贪婪匹配避免把两个注释之间的代码也吞掉。第三步字符串字面量替换成空字符串是因为登录成功这种提示语学生抄的时候往往只改字面量、不改逻辑留着反而干扰相似度判断。参数说明Pattern.DOTALL是这里最容易漏的不加这个.*?默认匹配不到换行符跨行注释就过滤不干净。我遇到过预处理完还有残留注释的翻车现场排查半天发现是这里写成了/\\*.*\\*/不是非贪婪匹配导致两个注释块之间的代码全被吞了相似度虚高。3.2 相似度算法选型余弦、SimHash、编辑距离怎么选预处理做完才轮到计算相似度。这块有几种常用方案各有各的适用场景算法思路适合场景缺点余弦相似度把代码分词后映射成词频向量计算夹角文本级别对比效果稳定分词质量影响大需要做词频统计SimHash用哈希把文本降维成 64 位指纹对比汉明距离海量文本去重速度快短代码效果差阈值难调编辑距离算两个字符串最少编辑次数代码量小、逻辑相似度极高大文件两两比对 O(n²)慢到怀疑人生做代码作业查重这个场景我最终采用的是「余弦相似度为主、SimHash 做粗筛」的组合方案。为什么不是直接用 SimHash 一把梭这是血泪经验。SimHash 对 300 字以上的长文本效果好但课程作业的代码文件通常在几十行到几百行经常只有五六个for循环加一堆if文本长度不足以让 SimHash 的特征向量稳定收敛。结果就是两个完全不同的作业也可能算出 60% 相似产生大量误报。所以我把 SimHash 放在第一层先把相似度明显低于 50% 的直接排除剩下的再用余弦相似度精细计算兼顾速度与准确率。3.3 核心实现代码与参数调优先说 SimHash 粗筛的实现。拿到归一化后的代码先按空格切分成 token再对每个 token 做 MD5 哈希结合每个 token 的词频做加权import java.math.BigInteger; import java.nio.charset.StandardCharsets; import java.security.MessageDigest; import java.security.NoSuchAlgorithmException; import java.util.HashMap; import java.util.List; import java.util.Map; public class SimHash { private static final int FINGERPRINT_BITS 64; public static BigInteger computeSimHash(ListString tokens) { MapString, Integer tokenWeight new HashMap(); for (String token : tokens) { tokenWeight.put(token, tokenWeight.getOrDefault(token, 0) 1); } int[] vector new int[FINGERPRINT_BITS]; for (Map.EntryString, Integer entry : tokenWeight.entrySet()) { String token entry.getKey(); int weight entry.getValue(); BigInteger hash hashToken(token); for (int i 0; i FINGERPRINT_BITS; i) { // hash 的每一位都参与加权 BigInteger bit hash.shiftRight(i).and(BigInteger.ONE); if (bit.equals(BigInteger.ONE)) { vector[i] weight; } else { vector[i] - weight; } } } BigInteger fingerprint BigInteger.ZERO; for (int i 0; i FINGERPRINT_BITS; i) { if (vector[i] 0) { fingerprint fingerprint.setBit(i); } } return fingerprint; } public static BigInteger hashToken(String token) { try { MessageDigest digest MessageDigest.getInstance(MD5); byte[] bytes digest.digest(token.getBytes(StandardCharsets.UTF_8)); return new BigInteger(1, bytes); } catch (NoSuchAlgorithmException e) { throw new RuntimeException(MD5 algorithm not available, e); } } }逻辑说明tokenWeight统计每个 token 出现的次数作为哈希加权依据。随后把每个 token 的 64 位哈希拆成 64 个 bit每一位上如果 hash 是 1 就vector[i] weight是 0 就- weight。最后vector[i] 0的位置指纹位设为 1否则保持 0。这样高频 token 对指纹的贡献更大低频 token 的影响被稀释。参数说明FINGERPRINT_BITS固定为 64这是 SimHash 的标准位数。算完指纹后用下面的汉明距离判断相似public static double similarity(BigInteger hashA, BigInteger hashB) { int hamming hashA.xor(hashB).bitCount(); // 汉明距离越小越相似, 距离 10 以内约等于 85% 以上相似 return 1.0 - (hamming / 64.0); }注意这里的相似度是「归一化后的值」。我一般拿0.5当粗筛阈值低于 0.5 的直接跳过不进入精细比对。这个值可以按课程任务难度调整如果作业是填空题式的阈值提到 0.6如果是大项目0.4 就够防止漏网之鱼。接下来是余弦相似度。实现方式是先分词、统计词频、构造词频向量然后按余弦公式计算public class CosineSimilarity { public static double compute(MapString, Integer vectorA, MapString, Integer vectorB) { if (vectorA.isEmpty() || vectorB.isEmpty()) { return 0.0; } double dotProduct 0.0; double normA 0.0; double normB 0.0; // 计算点积: 只在两个向量都包含的维度上累加 for (Map.EntryString, Integer entry : vectorA.entrySet()) { if (vectorB.containsKey(entry.getKey())) { dotProduct entry.getValue() * vectorB.get(entry.getKey()); } normA Math.pow(entry.getValue(), 2); } for (Integer value : vectorB.values()) { normB Math.pow(value, 2); } if (normA 0.0 || normB 0.0) { return 0.0; } return dotProduct / (Math.sqrt(normA) * Math.sqrt(normB)); } }逻辑说明余弦相似度要求两个向量的维度必须一致但两段代码分词后词表可能不同。这里只在vectorB.containsKey(entry.getKey())的维度上计算点积是「稀疏向量点积」的常见做法避免为不存在的维度创建零值项。分母用两个向量的模长乘积结果范围是 -1 到 1代码取正数部分越接近 1 越相似。参数说明词频向量构造时我是「程序关键词 标识符名」一起纳入但标识符名会被替换成VAR_0、VAR_1这样的占位符再做统计。不做这步的话两个人定义了studentName和student_name语义相同但文本不同余弦相似度会被拉低。替换规则很简单public static String replaceIdentifiers(String code) { // 把变量名统一替换成 VAR_ 前缀, 消除命名差异 Pattern identifierPattern Pattern.compile(\\b[a-z][A-Za-z0-9_]*\\b); Matcher matcher identifierPattern.matcher(code); StringBuilder result new StringBuilder(); int index 0; while (matcher.find()) { String matched matcher.group(); if (!isKeyword(matched)) { result.append(code, index, matcher.start()).append(VAR_).append(index); index matcher.end(); } } result.append(code.substring(index)); return result.toString(); }参数说明isKeyword维护一张 Java 关键词表包含public、class、void、for、if、else等。关键词不替换因为两个学生都写for是正常的都写VAR_123才是可疑信号。这是把「语义相似但名字不同」的情况拉回到同一尺度。4. 完整落地部署从配置到跑通一次作业查重4.1 环境准备与配置文件这套源码跑起来需要的东西不多JDK 8 及以上、Maven 3.6、MySQL 5.7。先用 Maven 打的包是jar直接放服务器跑完全没问题。数据库初始化在源码里的sql目录下先执行建表脚本再插一条测试老师账号INSERT INTO student (student_no, student_name, class_name) VALUES (2024001, 张伟, 软工2401), (2024002, 李娜, 软工2401), (2024003, 王晓, 软工2401);然后改application.yml里的数据源配置这是整个项目里唯一必须动的地方server: port: 8080 spring: datasource: url: jdbc:mysql://localhost:3306/code_check?useUnicodetruecharacterEncodingutf8useSSLfalseserverTimezoneAsia/Shanghai username: root password: your_password driver-class-name: com.mysql.cj.jdbc.Driver servlet: multipart: max-file-size: 5MB max-request-size: 20MB mybatis-plus: configuration: map-underscore-to-camel-case: true log-impl: org.apache.ibatis.logging.stdout.StdOutImpl global-config: db-config: id-type: auto参数说明map-underscore-to-camel-case一定要开不加的话数据库里的student_no映射不到实体类的studentNo字段查询结果全是 null这个问题排查起来非常隐蔽。multipart.max-file-size我设成 5MB超过这个大小的源码文件很可能是工程包不是单文件直接拒绝比较合理。4.2 启动与初始化数据启动类上有个容易被忽略的注解MapperScan没有它所有 Mapper 都注入不进去SpringBootApplication MapperScan(com.example.plagiarism.mapper) public class PlagiarismApplication { public static void main(String[] args) { SpringApplication.run(PlagiarismApplication.class, args); } }启动前先确认 MySQL 里建好了数据库不然启动会报连接失败。执行mysql -u root -p CREATE DATABASE code_check DEFAULT CHARACTER SET utf8mb4;然后导入建表脚本。启动服务mvn spring-boot:run看到Started PlagiarismApplication就说明起来了。接着用 cURL 创建一个作业、上传作业文件验证整套链路通没通。4.3 接口调用完整示例部署完必须亲手跑通一次完整流程不然心里没底。第一步建作业curl -X POST http://localhost:8080/api/v1/homework \ -H Content-Type: application/json \ -d {title: Java课设-图书管理系统, teacherId: 1, checkThreshold: 80.0}返回的 JSON 里包含作业 ID记下来后面提交作业要用。第二步上传代码文件curl -X POST http://localhost:8080/api/v1/submissions/upload \ -F file/path/to/Student1_BookManager.java \ -F homeworkId1 \ -F studentId1这一步的 Controller 实现里做了三个我在意的事情——文件扩展名校验、大小校验、重复提交校验RestController RequestMapping(/api/v1/submissions) public class SubmissionController { private final SubmissionService submissionService; public SubmissionController(SubmissionService submissionService) { this.submissionService submissionService; } PostMapping(/upload) public ResultLong upload(RequestParam(file) MultipartFile file, RequestParam(homeworkId) Long homeworkId, RequestParam(studentId) Long studentId) { // 1. 校验扩展名, 只允许 .java .cpp .py .c String originalFilename file.getOriginalFilename(); if (originalFilename null || !isAllowedExtension(originalFilename)) { throw new BizException(不支持的文件类型, 仅允许 .java .cpp .py .c); } // 2. 校验文件大小, 不超过 5MB if (file.getSize() 5 * 1024 * 1024) { throw new BizException(文件大小超过 5MB 限制); } // 3. 调用服务层, 内部会做重复提交校验 Long submissionId submissionService.upload(file, homeworkId, studentId); return Result.success(submissionId); } }逻辑说明三个校验各自拦截一类提交错误。扩展名校验防止传exe或zip包大小校验防止有人把整个项目目录打进来重复提交校验靠submission表上的唯一键兜底。服务层里有一个细节我建议保留——计算并存储content_hashService public class SubmissionService { private final SubmissionMapper submissionMapper; public Long upload(MultipartFile file, Long homeworkId, Long studentId) { // 检查该学生是否已提交过该作业 Submission existing submissionMapper.selectOne( new LambdaQueryWrapperSubmission() .eq(Submission::getHomeworkId, homeworkId) .eq(Submission::getStudentId, studentId) ); if (existing ! null) { throw new BizException(该学生已提交过此作业, 如需重新提交请联系老师重置); } // 存文件 String filePath saveToLocal(file, homeworkId, studentId); // 计算 SHA-256 String contentHash sha256(file); Submission submission new Submission(); submission.setHomeworkId(homeworkId); submission.setStudentId(studentId); submission.setFilePath(filePath); submission.setContentHash(contentHash); submissionMapper.insert(submission); return submission.getId(); } private String sha256(MultipartFile file) { try (InputStream is file.getInputStream()) { MessageDigest digest MessageDigest.getInstance(SHA-256); byte[] buffer new byte[8192]; int len; while ((len is.read(buffer)) ! -1) { digest.update(buffer, 0, len); } StringBuilder hex new StringBuilder(); for (byte b : digest.digest()) { hex.append(String.format(%02x, b)); } return hex.toString(); } catch (Exception e) { throw new RuntimeException(计算文件哈希失败, e); } } }参数说明buffer为 8192 字节这是流式读取的一个平衡值太小频繁 IO、太大浪费内存。content_hash的用途是快速排除完全相同的文件——如果两份提交的哈希一致说明连预处理都不需要做可以直接标 100% 相似。注意用 SHA-256 而不是 MD5MD5 碰撞概率虽然低但做查重这种对抗性场景不能不防。跑通上传之后第三步才是重头戏——触发查重curl -X POST http://localhost:8080/api/v1/plagiarism/run?homeworkId1这个接口会读取该作业下所有提交记录两两组合计算相似度把结果写进plagiarism_result表。5. 避坑指南查重系统常见的五个事故与排查这套系统我前前后后部署过三轮踩过的坑比文档里写得多。每一条都值得你提前知道不然出了问题很难定位。坑 1所有对比结果都接近 100%报告没法看现象跑完查重similarity_score全在 95% 以上连两个完全不同思路的实现都显示高度相似。原因预处理阶段没排除课程提供的公共代码。实训课基本都会发一个包含Main.java、CommonUtils.java的框架包每个学生都在其上做开发框架代码占据大半个文件相似度被无意义拉高。解决在预处理前做一个「公共代码剔除」步骤。先把框架代码归一化之后存进common_code表查重前从学生提交中把公共部分去掉。具体做法是分词后凡是出现在公共代码里的 token 串直接从向量里移除。坑 2OOM内存直接被查重进程吃满现象300 人的作业提交后触发查重跑了五分钟直接OutOfMemoryError: Java heap space。原因查重实现里把所有提交的全文读进ListString再嵌套两层循环做两两比对内存峰值是「提交数 × 单文件大小」300 份文件平均每份 50KB 就有 15MB 峰值加上分词产生的大量中间对象老年代直接被塞满。解决先在保存提交时就算好指纹存入库查重阶段只读取指纹和哈希不重新读原文。SimHash 粗筛环节只加载指纹指纹的 64 位字节内存占用从 O(n) 降到常量级别。精细对比阶段再做流式读取逐行处理且用完即释放。坑 3MyBatis Plus 的分页不生效总数永远返回 0现象写查询报告的分页接口Page对象能返回记录但total一直是 0导致前端分页组件渲染不出页码。原因MyBatis Plus 3.x 的分页功能不是默认开的它依赖PaginationInnerInterceptor注册到MybatisPlusInterceptor里。没注册的时候selectPage只会执行普通查询总数统计会被忽略。解决新建一个配置类把插件注册进去Configuration public class MybatisPlusConfig { Bean public MybatisPlusInterceptor mybatisPlusInterceptor() { MybatisPlusInterceptor interceptor new MybatisPlusInterceptor(); // 第一个参数是数据库类型, 不同数据库分页 SQL 不同 PaginationInnerInterceptor pagination new PaginationInnerInterceptor(DbType.MYSQL); pagination.setMaxLimit(500L); interceptor.addInnerInterceptor(pagination); return interceptor; } }参数说明DbType.MYSQL告诉插件生成 MySQL 的LIMIT ?语法换成 Oracle 就要DbType.ORACLE。setMaxLimit(500L)防止有人传超大页码把数据库打崩是推荐设置。坑 4代码里的中文注释全变成乱码现象上传的.java文件里中文注释在预处理后变成???个一段查重相似度整体偏低。原因文件读取用的默认字符集是平台编码。Windows 下是 GBK上传的文件是 UTF-8 编码GBK 解码把中文字符读成乱码。解决统一强制 UTF-8 读取别依赖new String(bytes)这种默认行为public static String readFileUtf8(Path path) throws IOException { return new String(Files.readAllBytes(path), StandardCharsets.UTF_8); }注意Files.readAllBytes适合单文件不超过 2MB 的场景这个查重系统里足够用了。如果将来要做大文件处理要改成FileReader配合BufferedReader指定编码。坑 5同一份代码, 一次查重 65%, 一次查重 90%, 结果不稳定现象同一份 A/B 提交重启服务后再查一次结果和上次差 25 个百分点。原因把变量名替换成VAR_占位符时用了自增序号而序号取决于分词顺序。Java 的HashMap遍历顺序不保证稳定两次运行时选中的同一个变量可能被赋予不同的VAR_123导致余弦向量计算出现巨大差异。解决把所有涉及 token 顺序的容器换成LinkedHashMap或TreeMap保证遍历顺序稳定。我后来把标识符替换逻辑彻底改成「按首次出现顺序命名」且顺序由LinkedHashMap确定问题根除。这类问题最吓人因为结果表面上是正常的但一深挖就露馅属于查重系统里祸害最深的坑。6. 进阶技巧让批量查重性能翻倍报告不再被口水淹没当你把基础跑通之后接下来要解决的是两个现实问题几百人同时提交后查重要跑多久老师看到几百条相似度记录从哪看起第一个问题用多线程解决。查重任务天然适合并行——提交 A 和提交 B 的比对完全不依赖其他任务。做法是引入线程池按「同作业提交对」拆分子任务ExecutorService executor Executors.newFixedThreadPool( Runtime.getRuntime().availableProcessors() - 1 );这个参数可以这么理解四核机器留一个核心给主线程其余三个核跑查重子任务。任务拆完后用Future收集结果。但有一点要提前想好——线程池的共享数据要线程安全。如果把待比对的对组合放进ArrayList而多个线程同时读会有并发问题。我一般用ConcurrentLinkedQueue或直接不共享每个线程领一个独立文件列表。第二个问题靠阈值过滤。homework表里的check_threshold字段就是干这个的。查询报告时只拉取超过阈值的数据Select(SELECT ... FROM plagiarism_result r WHERE r.homework_id #{homeworkId} AND r.similarity_score #{threshold} ORDER BY r.similarity_score DESC) ListReportVO selectSuspiciousReport(Param(homeworkId) Long homeworkId, Param(threshold) BigDecimal threshold);应用层把阈值从配置读出来老师可以在管理界面动态调。默认设 80%低于这个的都不展示报告自然清爽。另外一个值得做的验证是「用已知抄袭样本自检」。我的习惯是每次改完算法先人工构造三组测试样例——一组原样复制、一组改变量名复制、一组换注释复制。阈值定在能抓出前两组、放走第三组的水平。从那以后每次改完查重代码我都强制走一遍这三组样例再放真实数据进来。这个习惯帮我挡住了两次「改注释导致误判率暴涨」的回归。希望帮到你。本文还有配套的精品资源点击获取