简介:本资源是一个面向计算机专业高校师生的代码作业查重系统后端源码工程,聚焦课程教学中作业原创性核查的实际需求,适用于Java Web开发学习、课程设计实践及轻量级教学平台二次开发。项目基于Spring Boot 2.x与MyBatis Plus构建,集成JWT鉴权、Redis缓存优化及Swagger接口文档,核心功能涵盖三角色用户管理、教师布题/学生提交(支持.java等代码文件与PDF)、JPlag驱动的自动化查重、教师在线评分与分组作业管理。压缩包共168个文件,含95个Java业务与控制层类(如TeacherController、StuWorkServiceImpl)、21个Vue前端组件(体现前后端分离结构)、16个Mapper XML映射文件、11个SQL建表与初始化脚本,以及jar依赖、配置yml和文档md等,整体仅2.36MB,结构清晰、模块职责分明。目前已有126人下载学习,可直接导入IDE运行,快速掌握教学类系统的核心架构设计、文件处理流程与查重集成方案。
1. 项目概述与核心价值
最近在整理过往项目资料时,翻到了一个挺有意思的“老伙计”——一个基于Spring Boot和MyBatis Plus的代码作业查重系统。这个项目源于几年前一个朋友在高校计算机系任教时遇到的实际痛点:每次布置编程作业,手动比对上百份学生提交的源代码,工作量巨大且效率低下,更重要的是,人工查重很难发现那些经过变量名替换、代码结构调整的“高级”抄袭。当时我们几个搞开发的一合计,觉得这事儿能用技术解决,就动手搞了这么一套系统。虽然现在回头看,代码架构和实现上还有不少可以优化的地方,但核心思路和功能模块已经相当完整,对于想了解如何构建一个轻量级代码查重工具,或者想学习Spring Boot与MyBatis Plus在真实项目中的实战应用的朋友来说,这份源码依然有很高的参考价值。
简单来说,这个系统就是一个能自动分析学生提交的Java源代码(未来可扩展其他语言),通过一系列算法计算代码之间的相似度,并生成可视化报告的后台管理系统。它主要解决了教育场景下代码抄袭检测的自动化需求,将老师从繁重的人工比对中解放出来,同时通过相对客观的算法,提高了查重的准确性和公平性。如果你是一名后端开发者,正在寻找一个融合了经典MVC架构、数据库操作、文件处理、算法集成和基础前端展示的Spring Boot综合实战案例,那么这个项目会是一个不错的“标本”。接下来,我会把这个项目的设计思路、关键技术实现、踩过的坑以及一些扩展思考,毫无保留地拆解给你看。
2. 系统整体架构与设计思路拆解
2.1 核心业务流程与模块划分
任何系统的设计都得从业务场景出发。我们的核心业务流程非常清晰:老师发布作业 -> 学生上传代码压缩包 -> 系统解析并计算代码相似度 -> 生成查重报告。围绕这个流程,我们自然地将系统划分为几个核心模块:
- 用户与权限管理模块:处理教师、学生的登录、注册和角色权限。教师可以管理班级、发布作业、查看报告;学生只能查看自己的作业和提交记录。
- 作业管理模块:这是业务的起点。教师在此创建作业,设定标题、描述、截止日期、允许上传的文件类型(如.zip, .rar)等。
- 代码提交与存储模块:学生上传代码压缩包,系统需要可靠地接收文件,将其存储到服务器特定目录(如
/upload/submissions/{作业ID}/{学号}/),并将提交记录存入数据库。 - 代码解析与预处理模块:这是查重算法的“前处理车间”。系统需要解压上传的压缩包,读取其中的源代码文件(主要是
.java文件),然后进行一系列“清洗”操作,比如去除注释、统一空格和换行符、标准化变量名和方法名(例如,将所有用户定义的变量名替换为var1,var2,方法名替换为func1,func2),目的是消除那些不影响代码逻辑但会影响相似度判断的“表面差异”。 - 相似度计算引擎模块:系统的“大脑”。我们采用了基于“字符串指纹”的算法。将预处理后的代码文本,通过哈希函数(如SimHash)或直接构建词法单元(Token)序列,并计算序列之间的编辑距离或余弦相似度。这个模块被设计为可插拔的,方便后续替换或增加更复杂的算法(如基于AST抽象语法树的比较)。
- 报告生成与展示模块:将计算出的相似度矩阵(一个N*N的表格,N是提交数)以直观的方式呈现。我们提供了列表视图(显示每两份作业间的相似度百分比)和矩阵热力图,并对高相似度(如>70%)的配对进行高亮预警。
整个系统的设计遵循了经典的分层架构:Controller层处理HTTP请求和响应;Service层封装核心业务逻辑;DAO层(由MyBatis Plus增强)负责数据库交互。这种结构清晰,职责分离,非常适合中小型项目的快速开发和维护。
2.2 技术栈选型背后的考量
为什么选择Spring Boot + MyBatis Plus这个组合?这背后有非常实际的考虑。
- Spring Boot:它是快速构建独立、生产级Spring应用的绝对首选。它最大的优势在于“约定大于配置”和自动装配。回想项目初期,我们通过
spring-boot-starter-web、spring-boot-starter-test等几个依赖,几乎没写什么XML配置,就搭起了一个能跑的后端服务。内嵌的Tomcat服务器也让部署变得极其简单,打成一个JAR包到处运行。这对于我们这种小团队、追求快速迭代验证想法的项目来说,效率提升是巨大的。 - MyBatis Plus:是对原生MyBatis的强力增强。在代码作业查重系统中,有大量结构简单的CRUD操作,比如用户、作业、提交记录的管理。如果使用原生MyBatis,每个实体都需要手动编写Mapper接口和对应的XML SQL文件,枯燥且易错。MyBatis Plus的通用Mapper(
BaseMapper)功能,让我们只需让实体类继承Model,让Mapper接口继承BaseMapper,就免费获得了全套的单表CRUD方法,开发速度飞起。它的条件构造器(QueryWrapper、UpdateWrapper)也让动态SQL的编写变得非常流畅和安全,避免了SQL注入的风险。 - 其他关键依赖:
Apache Commons Compress:用于处理学生上传的ZIP、RAR等压缩包格式,比JDK自带的ZIP工具更强大稳定。Guava或Apache Commons Lang3:提供了丰富的字符串处理、集合操作等工具类,在代码预处理阶段非常好用。JWT (JSON Web Token):用于实现无状态的API认证。学生和教师登录后,服务端生成一个Token返回给前端,后续请求都携带此Token,避免了传统的Session在分布式环境下的同步问题。Lombok:通过注解自动生成Getter/Setter、构造方法等,让实体类代码非常简洁。
注意:技术选型没有银弹。这个组合的优势在于开发效率高、生态成熟、学习资料丰富。但如果项目对数据库的极致性能或复杂的多表关联查询有极高要求,可能需要考虑JPA(Hibernate)或更底层的JDBC方案。不过对于我们这个查重系统,MyBatis Plus在灵活性和效率上取得了很好的平衡。
3. 核心模块实现细节与实操要点
3.1 基于MyBatis Plus的数据层高效设计
数据层是整个系统的基石。我们设计了几个核心表:user(用户表)、course(课程/班级表)、assignment(作业表)、submission(代码提交记录表)、similarity_result(相似度结果表)。这里重点讲一下如何使用MyBatis Plus让数据操作既高效又清晰。
首先,在实体类上,我们大量使用了MyBatis Plus的注解和Lombok。
import com.baomidou.mybatisplus.annotation.*; import lombok.Data; import java.time.LocalDateTime; @Data @TableName("submission") // 指定表名 public class Submission { @TableId(type = IdType.AUTO) // 主键自增 private Long id; private Long assignmentId; private Long userId; private String originalFileName; private String storedFilePath; // 服务器存储路径 private String status; // 状态:UPLOADED, PARSING, PARSED, ERROR @TableField(fill = FieldFill.INSERT) // 插入时自动填充 private LocalDateTime createTime; @TableField(fill = FieldFill.INSERT_UPDATE) // 插入和更新时自动填充 private LocalDateTime updateTime; // 省略 getter/setter (由Lombok @Data 生成) }对应的Mapper接口极其简洁:
import com.baomidou.mybatisplus.core.mapper.BaseMapper; import org.apache.ibatis.annotations.Mapper; @Mapper // 别忘了这个注解,让Spring能扫描到 public interface SubmissionMapper extends BaseMapper<Submission> { // 不需要写任何方法,就拥有了增删改查、分页等基础能力 }在Service层,我们可以直接注入这个Mapper使用,但更佳实践是再封装一个Service接口和实现类,在其中处理更复杂的业务逻辑。对于简单的查询,MyBatis Plus的条件构造器是神器:
@Service public class SubmissionServiceImpl implements SubmissionService { @Autowired private SubmissionMapper submissionMapper; public List<Submission> getSubmissionsByAssignmentAndStatus(Long assignmentId, String status) { QueryWrapper<Submission> queryWrapper = new QueryWrapper<>(); queryWrapper.eq("assignment_id", assignmentId) .eq("status", status) .orderByDesc("create_time"); // 等效SQL: SELECT * FROM submission WHERE assignment_id = ? AND status = ? ORDER BY create_time DESC return submissionMapper.selectList(queryWrapper); } }实操心得:
@TableField(fill = FieldFill.INSERT)这类字段填充功能,需要配合一个元对象处理器(MetaObjectHandler)来实现。我们通常创建一个配置类,实现这个接口,在insertFill和updateFill方法中统一设置createTime和updateTime。这保证了数据创建和更新时间的一致性,无需在每个业务代码中手动设置。
3.2 代码文件上传、解析与预处理流程
这是从“原始代码”到“可比较文本”的关键转换步骤,也是最容易出错的环节。
1. 文件上传与存储:我们使用Spring MVC提供的MultipartFile接口接收前端上传的文件。关键点在于:
- 安全性:必须校验文件后缀名、MIME类型,防止上传恶意可执行文件。我们有一个允许的后缀名列表(
.zip,.rar)。 - 存储路径:不要使用原始文件名直接存储,避免重名和路径遍历攻击。我们采用“作业ID/用户ID/时间戳_随机数.后缀”的目录结构。例如:
/data/uploads/assignment_101/user_2023/1654321000_abc123.zip。 - 异步处理:文件上传和保存到磁盘是很快的,但后续的解压、解析、计算非常耗时。我们绝不能阻塞HTTP请求线程。我们的做法是,上传成功后立即将提交记录状态置为
UPLOADED,并发送一个消息到消息队列(或直接提交一个异步任务到线程池),触发后续的处理流程。
2. 代码解析与清洗:这是算法的预处理核心。我们为每种支持的编程语言(目前主要是Java)实现一个CodeProcessor接口。
public interface CodeProcessor { /** * 处理代码文件 * @param sourceFile 源代码文件 * @return 清洗后的纯文本代码(无注释,标准化格式) */ String process(File sourceFile); } // Java代码处理器示例 @Component public class JavaCodeProcessor implements CodeProcessor { @Override public String process(File javaFile) { String content = FileUtils.readFileToString(javaFile, StandardCharsets.UTF_8); // 1. 移除所有单行注释和多行注释 content = removeComments(content); // 2. 移除所有空白字符(空格、制表符、换行),或统一替换为单个空格 content = normalizeWhitespace(content); // 3. 标准化标识符(将变量名、方法名等替换为通用占位符) content = normalizeIdentifiers(content); // 4. 可选:移除所有import语句和package声明(因为它们不体现核心逻辑) content = removeImportsAndPackage(content); return content; } // ... 具体实现方法 }removeComments可以使用正则表达式,但要注意处理字符串字面量中包含//或/*的情况,避免误删。更稳健的方法是使用现成的Java语法分析库(如JavaParser),但考虑到轻量级,我们初期使用了经过精心测试的正则表达式组合。
normalizeIdentifiers是抗“变量名替换抄袭”的关键。我们使用一个简单的算法:遍历代码文本,识别出用户自定义的标识符(通过词法分析或正则匹配),然后用一个映射表将它们依次替换为var1,var2,method1,method2等。这样,int studentScore = 100;和int a = 100;在经过标准化后都会变成int var1 = 100;。
3.3 相似度计算引擎的实现策略
我们实现了两种算法作为可选项,并抽象出了SimilarityCalculator接口。
策略一:基于SimHash的指纹算法SimHash是Google用于网页去重的算法,其思想是将文本映射为一个固定长度(如64位)的指纹,通过计算两个指纹的海明距离来判断相似度。它的优点是速度快,适合大数据量下的快速去重。
- 将清洗后的代码文本分词(对于代码,可以按关键字、运算符、标准化后的标识符进行切分)。
- 为每个词计算一个哈希值。
- 初始化一个长度为64的向量V,所有位为0。
- 遍历每个词的哈希值,如果哈希值的某一位为1,则向量V的对应位加1;否则减1。
- 所有词处理完后,将向量V的每一位进行符号化:大于0置1,小于0置0。得到最终的64位SimHash指纹。
- 计算两个指纹的海明距离(二进制位不同的数量),距离越小越相似。可以设定一个阈值(如距离<=3认为高度相似),或将其转换为一个百分比。
策略二:基于Token序列的编辑距离(Levenshtein Distance)这种方法更直观,精度也往往更高,但计算成本随文本长度增长。
- 将清洗后的代码文本,转换成一个Token序列。Token可以是关键字、运算符、标准化后的标识符、字面量等。
- 计算两个Token序列之间的编辑距离,即需要多少次插入、删除、替换操作才能将一个序列变成另一个。
- 相似度 = 1 - (编辑距离 / max(序列1长度, 序列2长度))。
在实际项目中,我们将两种策略结合使用。首先用SimHash进行快速粗筛,对于SimHash相似度超过某个阈值的代码对,再用Token编辑距离进行精确计算。这样可以兼顾效率和精度。
@Service public class SimilarityCalculationService { @Autowired private SimHashCalculator simHashCalculator; @Autowired private TokenEditDistanceCalculator editDistanceCalculator; public double calculateSimilarity(String code1, String code2) { // 1. 快速粗筛 long fingerprint1 = simHashCalculator.calculate(code1); long fingerprint2 = simHashCalculator.calculate(code2); int hammingDistance = hammingDistance(fingerprint1, fingerprint2); if (hammingDistance > 10) { // 阈值可调,海明距离太大说明完全不同 return 0.0; } // 2. 精确计算 List<String> tokens1 = tokenize(code1); List<String> tokens2 = tokenize(code2); double editDistanceScore = editDistanceCalculator.calculate(tokens1, tokens2); return editDistanceScore; } }注意事项:计算所有提交代码两两之间的相似度是一个时间复杂度为O(N²)的操作。当一次作业有N份提交时,需要计算C(N,2) = N*(N-1)/2次。如果N=100,就需要计算4950次。因此,异步计算和结果缓存至关重要。我们使用数据库表
similarity_result存储每对提交的计算结果,并在作业最终提交截止后,启动一个后台任务进行计算。计算完成后,结果被持久化,后续查看报告时直接查询数据库,无需重复计算。
4. 系统搭建、配置与核心代码解析
4.1 项目初始化与环境配置
我们使用Spring Initializr(start.spring.io)快速生成项目骨架。核心依赖选择:Spring Web,MyBatis Plus,MySQL Driver,Lombok。这里给出关键的pom.xml依赖和application.yml配置。
关键依赖 (pom.xml):
<dependencies> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-web</artifactId> </dependency> <dependency> <groupId>com.baomidou</groupId> <artifactId>mybatis-plus-boot-starter</artifactId> <version>3.5.3</version> <!-- 请使用当时稳定版本 --> </dependency> <dependency> <groupId>mysql</groupId> <artifactId>mysql-connector-java</artifactId> <scope>runtime</scope> </dependency> <dependency> <groupId>org.projectlombok</groupId> <artifactId>lombok</artifactId> <optional>true</optional> </dependency> <!-- 文件处理 --> <dependency> <groupId>org.apache.commons</groupId> <artifactId>commons-compress</artifactId> <version>1.21</version> </dependency> <dependency> <groupId>commons-io</groupId> <artifactId>commons-io</artifactId> <version>2.11.0</version> </dependency> <!-- 工具类 --> <dependency> <groupId>com.google.guava</groupId> <artifactId>guava</artifactId> <version>31.1-jre</version> </dependency> <!-- JWT --> <dependency> <groupId>io.jsonwebtoken</groupId> <artifactId>jjwt</artifactId> <version>0.9.1</version> </dependency> </dependencies>数据库与MyBatis Plus配置 (application.yml):
spring: datasource: driver-class-name: com.mysql.cj.jdbc.Driver url: jdbc:mysql://localhost:3306/code_plagiarism_check?useUnicode=true&characterEncoding=utf-8&useSSL=false&serverTimezone=Asia/Shanghai username: your_username password: your_password servlet: multipart: max-file-size: 50MB # 限制上传文件大小 max-request-size: 100MB mybatis-plus: configuration: log-impl: org.apache.ibatis.logging.stdout.StdOutImpl # 开发时开启SQL日志 global-config: db-config: id-type: auto # 主键自增 logic-delete-field: isDeleted # 全局逻辑删除字段(如果用到) logic-delete-value: 1 logic-not-delete-value: 0 mapper-locations: classpath*:/mapper/**/*.xml # XML映射文件位置,如果有自定义复杂SQL4.2 核心控制器与服务层代码片段
文件上传控制器 (FileUploadController):
@RestController @RequestMapping("/api/assignment/{assignmentId}/submission") public class FileUploadController { @Autowired private SubmissionService submissionService; @PostMapping("/upload") public ApiResponse<String> uploadCode( @PathVariable Long assignmentId, @RequestParam("file") MultipartFile file, HttpServletRequest request) { // 1. 权限验证(从JWT Token中获取当前用户ID) Long currentUserId = getCurrentUserIdFromToken(request); // 2. 基础文件校验 if (file.isEmpty()) { return ApiResponse.error("上传文件不能为空"); } String originalFilename = file.getOriginalFilename(); if (!isValidFileExtension(originalFilename)) { return ApiResponse.error("仅支持.zip或.rar格式文件"); } // 3. 调用服务层处理上传和后续异步任务 String result = submissionService.handleUpload(assignmentId, currentUserId, file); return ApiResponse.success(result); } // ... 其他方法 }异步处理服务 (SubmissionServiceImpl部分):
@Service @Slf4j public class SubmissionServiceImpl implements SubmissionService { @Autowired private SubmissionMapper submissionMapper; @Autowired private TaskExecutor taskExecutor; // 注入Spring的异步任务执行器 @Autowired private CodeProcessor codeProcessor; @Autowired private SimilarityCalculationService similarityService; @Override @Transactional public String handleUpload(Long assignmentId, Long userId, MultipartFile file) { // 1. 保存文件到磁盘,生成存储路径 String storedPath = saveFileToDisk(assignmentId, userId, file); // 2. 创建提交记录,状态为 UPLOADED Submission submission = new Submission(); submission.setAssignmentId(assignmentId); submission.setUserId(userId); submission.setOriginalFileName(file.getOriginalFilename()); submission.setStoredFilePath(storedPath); submission.setStatus("UPLOADED"); submissionMapper.insert(submission); // 3. 提交异步任务,进行后续处理 taskExecutor.execute(() -> { processSubmissionAsync(submission.getId()); }); return "文件上传成功,正在后台处理中..."; } @Async // 标记为异步方法,需要配置@EnableAsync public void processSubmissionAsync(Long submissionId) { Submission submission = submissionMapper.selectById(submissionId); submission.setStatus("PARSING"); submissionMapper.updateById(submission); try { // 1. 解压文件 File codeDir = extractZipFile(submission.getStoredFilePath()); // 2. 遍历目录,找到所有.java文件,进行预处理 Map<String, String> processedCodeMap = new HashMap<>(); // <文件名, 处理后的代码文本> List<File> javaFiles = findJavaFiles(codeDir); for (File javaFile : javaFiles) { String processedCode = codeProcessor.process(javaFile); processedCodeMap.put(javaFile.getName(), processedCode); } // 3. 将处理后的文本存储起来(如存到数据库或Redis) saveProcessedCode(submissionId, processedCodeMap); submission.setStatus("PARSED"); submissionMapper.updateById(submission); // 4. 触发该作业下所有已解析提交的相似度计算(通常由另一个调度任务触发) // similarityService.calculateForAssignment(submission.getAssignmentId()); } catch (Exception e) { log.error("处理提交{}时发生错误", submissionId, e); submission.setStatus("ERROR"); submissionMapper.updateById(submission); } } // ... 其他方法 }4.3 相似度计算与结果存储
当一份作业的所有提交都处理完成(状态为PARSED)后,系统会触发批量相似度计算。这个过程通常由一个定时任务或管理员手动触发。
@Service public class SimilarityCalculationServiceImpl implements SimilarityCalculationService { @Autowired private SubmissionMapper submissionMapper; @Autowired private SimilarityResultMapper similarityResultMapper; public void calculateForAssignment(Long assignmentId) { // 1. 获取该作业下所有已解析的提交 QueryWrapper<Submission> wrapper = new QueryWrapper<>(); wrapper.eq("assignment_id", assignmentId).eq("status", "PARSED"); List<Submission> submissions = submissionMapper.selectList(wrapper); if (submissions.size() < 2) { return; // 少于两份提交,无需计算 } // 2. 加载每份提交的处理后代码文本 Map<Long, String> codeMap = loadProcessedCodeForSubmissions(submissions); // 3. 两两计算相似度 for (int i = 0; i < submissions.size(); i++) { for (int j = i + 1; j < submissions.size(); j++) { Long subIdA = submissions.get(i).getId(); Long subIdB = submissions.get(j).getId(); String codeA = codeMap.get(subIdA); String codeB = codeMap.get(subIdB); double similarity = calculateSimilarity(codeA, codeB); // 调用算法 // 4. 保存结果 SimilarityResult result = new SimilarityResult(); result.setAssignmentId(assignmentId); result.setSubmissionIdA(subIdA); result.setSubmissionIdB(subIdB); result.setSimilarityScore(similarity); result.setCalculatedTime(LocalDateTime.now()); similarityResultMapper.insert(result); } } log.info("作业 {} 的相似度计算完成,共计算 {} 对。", assignmentId, submissions.size() * (submissions.size() - 1) / 2); } }5. 部署、优化与常见问题排查
5.1 系统部署与性能考量
这个系统可以作为一个标准的Spring Boot应用部署。
- 打包:使用
mvn clean package生成一个可执行的JAR文件(-DskipTests跳过测试)。 - 运行:在服务器上通过
java -jar your-system.jar即可启动。生产环境建议使用nohup或配置为系统服务(如systemd)。 - 数据库:提前创建好MySQL数据库和表结构。可以使用MyBatis Plus的代码生成器自动生成,或手动执行SQL脚本。
- 文件存储:确保应用运行用户对文件上传目录(如
/data/uploads)有读写权限。可以考虑将上传目录配置在单独的、容量较大的数据盘上。 - 性能优化点:
- 数据库连接池:Spring Boot默认使用HikariCP,配置
spring.datasource.hikari.*相关参数(如maximum-pool-size,connection-timeout)以适应并发。 - 异步与线程池:文件处理和相似度计算必须异步化。配置一个专用的线程池(通过
ThreadPoolTaskExecutor),避免占用Web容器的HTTP处理线程。 - 缓存:对于频繁访问且不常变的数据,如用户信息、作业基本信息,可以使用Redis或Caffeine进行缓存。
- 算法优化:SimHash计算可以预先进行并存储。对于大规模作业(如N>500),O(N²)的全量计算压力很大,可以考虑抽样计算、分批次计算或使用更高效的近似算法。
- 数据库连接池:Spring Boot默认使用HikariCP,配置
5.2 常见问题与排查技巧实录
在实际开发和运行中,我们遇到了不少问题,这里总结几个典型的:
问题1:文件上传后,后台异步处理任务没有执行。
- 排查:
- 检查
@Async注解是否生效。需要在主配置类上添加@EnableAsync。 - 检查线程池配置。如果未自定义,Spring会使用一个简单的线程池。如果所有线程都在忙或被阻塞,新任务会排队。可以查看日志或使用
ThreadPoolTaskExecutor的监控方法。 - 检查异步方法是否在同一个类内部被调用。Spring的
@Async是基于代理实现的,在同一个类中一个方法调用另一个@Async方法,不会走代理,导致异步失效。需要从其他Bean中调用。
- 检查
- 解决:确保
@EnableAsync已添加,将异步方法放在单独的Service Bean中,并通过@Autowired注入调用。
问题2:MyBatis Plus的updateById方法,更新了实体类中为null的字段。
- 场景:你只想更新
status字段,但实体对象中其他字段(如originalFileName)为null,执行updateById后,数据库里对应记录的original_file_name被更新成了NULL。 - 原因:MyBatis Plus默认使用
全字段更新策略。 - 解决:
- 使用
UpdateWrapper:这是推荐的做法,明确指定要更新的字段。UpdateWrapper<Submission> updateWrapper = new UpdateWrapper<>(); updateWrapper.eq("id", submissionId).set("status", "PARSING"); submissionMapper.update(null, updateWrapper); // 第一个参数传null - 在实体字段上使用
@TableField注解:设置updateStrategy为FieldStrategy.IGNORED,但这样全局忽略,可能在其他场景下不安全。@TableField(updateStrategy = FieldStrategy.IGNORED) private String originalFileName; - 先查询,再更新:先
selectById获取完整对象,只修改要变的字段,再updateById。但这会产生两次数据库交互。
- 使用
问题3:代码预处理时,正则表达式去除注释误伤了字符串内的内容。
- 场景:代码
String s = "// This is not a comment";会被错误地移除//之后的部分。 - 解决:编写更健壮的正则表达式,或者放弃正则,使用状态机(State Machine)进行解析。一个简单的状态机思路是遍历字符,记录是否在字符串内、是否在字符常量内、是否在单行/多行注释内。当不在字符串和字符常量内时,遇到的
//或/*才被视为注释开始。对于初期项目,可以寻找成熟的第三方Java代码解析库(如JavaParser)来处理,虽然引入依赖,但准确性和可维护性更高。
问题4:相似度计算耗时过长,导致请求超时或服务器负载高。
- 排查:首先确认计算是否真的是同步进行的。检查
calculateForAssignment方法是否被HTTP请求直接调用。 - 解决:
- 异步化与状态通知:计算任务必须提交到独立的线程池异步执行。计算开始和结束时,更新作业或系统的状态,并通过WebSocket或前端轮询通知用户。
- 分治与增量计算:不必每次都全量计算所有提交对。新提交加入时,只需计算该新提交与所有旧提交的相似度,然后合并结果。
- 设置超时与中断:为计算任务设置超时时间,如果超时则中断,并记录为“计算超时”,避免任务堆积。
- 结果缓存与复用:一旦计算完成,结果存入数据库。后续查询直接读取,除非源代码被重新提交和解析。
问题5:前端显示大量数据(如相似度矩阵)时浏览器卡顿。
- 解决:
- 后端分页:相似度结果列表一定要支持分页查询,避免一次性返回成千上万条记录。
- 虚拟滚动:对于矩阵热力图这种必须展示全量的视图,前端可以采用虚拟滚动技术,只渲染可视区域内的元素。
- 数据聚合:对于非常大的作业,可以考虑先不展示具体的两两对比,而是给出一个“疑似抄袭集群”的概览,点击集群后再展示内部详情。
这个项目从构思到实现,是一个典型的“用技术解决具体场景问题”的过程。它不追求大而全,而是在有限的资源下,抓住核心痛点,选择最合适的技术栈快速落地。源码中还有很多可以打磨的地方,比如引入更精确的AST比对算法、增加对更多编程语言的支持、优化前端交互体验等。但对于想深入理解Spring Boot和MyBatis Plus如何协作,以及如何设计一个完整业务闭环的后端系统来说,它提供了一个非常扎实的起点。
本文还有配套的精品资源,点击获取