开头
每次给学生开发作业管理系统,我都要劝自己一句:别把系统做成了"作业收集箱"。交作业、查重、看分数,这些功能说到底只是基础,真正麻烦的是评分环节——一个班几十号人,老师一个人改到天亮不说,学生交了作业就石沉大海,连同学之间互相学习的路径都没有。
这套"师生共评作业管理系统"就是我针对这个痛点设计的一套完整源码。技术栈是SpringBoot+Vue3+MyBatis,数据库用MySQL,前后端完全分离。和市面上常见的作业系统不一样的地方在于,它把"教师评分"和"学生互评"两条评分链路同时做进了核心业务里。教师可以发起互评任务,学生按匿名规则给同学的作业打分,教师再在互评结果基础上给出最终成绩,系统自动按权重汇总。这样既减轻了老师的批改压力,也逼着学生在看别人作业的过程中真正动脑子。
这套源码适用于计算机专业课程设计、毕业设计,也适合企业做内部培训考核系统二次开发。前端基于Vue3+Element Plus搭建,后端基于SpringBoot 2.7+MyBatis构建,权限用JWT做无状态认证,部署时前端打包丢到Nginx,后端打jar包启服务,结构清晰,扩展容易。我在这篇文章里会把整个系统的数据模型、评分算法、前后端核心实现、以及我在实际部署中踩过的关键坑全部拆开讲一遍。无论你是刚入门想要整套可跑项目的学生,还是工作中要快速落地一个考核系统的开发者,这篇文章都能给你省下不少摸索时间。
1. 设计这套系统的第一件事:定清楚"共评"的业务规则
很多人拿到这种题目,第一反应就是建表、写接口、画页面。但我建议你先别急着动手,先把评分规则想明白。前前后后我接触过的作业管理项目里,因为规则没定清楚导致推倒重来的,占了相当高的比例。这个系统叫"师生共评",核心不是"上传作业",而是"评"这个字。
1.1 角色权限模型:三种角色怎么共处一套系统
系统一共三类用户:管理员、教师、学生。权限模型不是简单的一个role字段就完事,而是在接口层面做了细粒度控制。后端用一个@RequireRole注解配合拦截器实现权限校验,而非使用Spring Security那种重量级框架——原因很简单:这个系统的角色数量太少,引入Security反而增加配置复杂度。
我采用的是基于JWT的认证方式,登录成功后把用户ID、角色信息放进Token,每次请求通过拦截器解析Token并放入ThreadLocal,业务代码里直接通过CurrentUser.get()取当前用户信息。这样做的好处是接口层代码非常清爽,而且天然支持跨域、支持前后端完全分离。
| 角色 | 核心权限 | 典型操作 |
|---|---|---|
| 管理员 | 全系统管理 | 班级管理、教师分配、课程创建 |
| 教师 | 作业+评分管理 | 发布作业、发起互评、教师评分、查看统计 |
| 学生 | 提交+互评 | 提交作业、参与互评、查看成绩 |
1.2 评分权重的配置策略:教师60%+互评40%只是一个起点
共评系统里最敏感的就是权重分配。我的系统默认是教师评分占60%,学生互评均分占40%,但权重不是写死在代码里的,而是存在作业配置表中。教师在发布作业的时候可以自己调,比如某些主观性强的作业,可以调成教师70%、互评30%——甚至极端情况下可以把互评权重调成0,退化成纯粹的教师评分模式。
这种可配置设计的背后逻辑是:师生共评并不是要取代教师评分,而是用互评数据辅助教师决策。互评数据量大、维度多,能反映出作业的传播度和同学认可度,但互评也有明显的缺陷——学生打分往往凭感觉、不够专业,甚至可能因为朋友关系给高分。所以系统在算互评得分时做了两层处理:
- 去掉最高分和最低分各一个,降低个别极端分数的影响
- 如果同一份作业收到的互评打分个数少于3个,则互评分数不计入总分,直接以教师评分作为最终成绩
1.3 评分的维度设计:综合评价+分项评分双轨制
作业评分如果只有一个总分,那评了等于白评,学生拿到分数也不知道自己哪里做得好、哪里做得差。所以这套系统把评分拆成两层:
- 分项评分:教师评分包括"代码质量"、"功能完整性"、"文档规范性"等多个维度,每个维度满分10分
- 综合评价:教师或学生在评分面板上填写一段文字评价,作为反馈内容展示给学生
分项评分的好处是成绩单可以生成雷达图,学生一眼就能看到自己的短板。互评也支持分项评分,但维度比教师评分少——我实际使用下来,互评维度超过3个以后,学生填写的意愿和准确度会明显下降,所以我设定互评只有"完成度"和"创新性"两个维度。
提示:评分维度尽量控制在2-4个之间。对教师可以多一些维度,对学生互评一定要精简,否则互评数据质量会很差。
2. 数据库设计:评分关系表、唯一约束与防重复提交
数据库是整个系统的地基。我设计数据库的时候遵循了几个基本原则:课程数据用逻辑外键而非物理外键、评分记录必须保证幂等、文件路径和业务数据分离存储。
2.1 核心表结构:从用户表到作业评分表的完整链路
用户表(user)、班级表(class)、课程表(course)、作业表(assignment)这四张基础表不展开细说,重点讲作业提交表(submission)和评分表(evaluation)。
CREATE TABLE `submission` ( `id` BIGINT AUTO_INCREMENT PRIMARY KEY, `assignment_id` BIGINT NOT NULL COMMENT '作业ID', `student_id` BIGINT NOT NULL COMMENT '提交学生ID', `file_url` VARCHAR(500) DEFAULT NULL COMMENT '文件下载地址', `content_text` TEXT COMMENT '文本类作业内容', `submit_time` DATETIME DEFAULT CURRENT_TIMESTAMP, `status` TINYINT DEFAULT 0 COMMENT '0-已提交 1-已评分 2-成绩已计算', UNIQUE KEY `uk_assignment_student` (`assignment_id`, `student_id`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='作业提交表';这里的uk_assignment_student联合唯一索引很关键。它保证了同一个学生针对同一份作业只能有一条提交记录,无论前端怎么重复请求,数据库这层就拦截住了。做管理系统,数据库约束永远比代码判断可靠,这是我在多次线上事故之后总结出的铁律。
评分表是这套系统的核心资产,设计上要同时兼容教师评分和学生互评:
CREATE TABLE `evaluation` ( `id` BIGINT AUTO_INCREMENT PRIMARY KEY, `submission_id` BIGINT NOT NULL COMMENT '被评作业提交ID', `evaluator_id` BIGINT NOT NULL COMMENT '评分人ID', `evaluator_role` TINYINT NOT NULL COMMENT '1-教师 2-学生', `dimension_type` TINYINT NOT NULL COMMENT '评分维度类型', `score` DECIMAL(4,2) NOT NULL COMMENT '单项维度得分', `comment_text` VARCHAR(500) DEFAULT NULL, `create_time` DATETIME DEFAULT CURRENT_TIMESTAMP, UNIQUE KEY `uk_evaluator_submission_dim` (`submission_id`, `evaluator_id`, `dimension_type`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='评分记录表';uk_evaluator_submission_dim这个联合唯一约束,保证了一条铁律:同一个人对同一份作业的同一个维度,只能评分一次。教师不能重复评分,学生也不能反复修改互评结果。如果想让学生有修改机会,可以在业务层做"评分后规定时间内可撤回重评"的逻辑,撤回时删除旧记录再插入新记录,唯一约束不受影响。
2.2 成绩汇总表:为什么要把算好的成绩物理落表
成绩计算如果每次都临时算,随着数据量增长,页面响应会越来越慢。我设计了一张score_summary表,在评分完成、权重发生变化、或者手动触发重算时,把最终成绩落表。这样成绩列表页、成绩导出、图表统计全部直接查汇总表,速度极快。
CREATE TABLE `score_summary` ( `id` BIGINT AUTO_INCREMENT PRIMARY KEY, `submission_id` BIGINT NOT NULL, `assignment_id` BIGINT NOT NULL, `student_id` BIGINT NOT NULL, `teacher_score` DECIMAL(6,2) DEFAULT NULL COMMENT '教师评分', `peer_score` DECIMAL(6,2) DEFAULT NULL COMMENT '互评得分', `final_score` DECIMAL(6,2) DEFAULT NULL COMMENT '加权最终得分', `peer_count` INT DEFAULT 0 COMMENT '有效互评人数', `update_time` DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, UNIQUE KEY `uk_submission` (`submission_id`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='成绩汇总表';2.3 文件和结构分离:MySQL不要存文件本体
作业文件的存储,我推荐本地磁盘目录存储 + MySQL存文件元数据的方案。目录规则:/data/homework/2025/03/15/{uuid}.{ext},文件名全部用UUID重命名,原始文件名只存到数据库的original_name字段里。这样做的原因是避免文件名冲突,同时也防止路径穿越等安全问题。如果你有对象存储服务,比如MinIO,也可以把file_url直接存成对象存储的访问地址,表结构完全不用改。
注意:MySQL的
max_allowed_packet默认一般是64M,如果学生上传大作业文件,要么在Nginx层调client_max_body_size,要么在SpringBoot的配置里调spring.servlet.multipart.max-file-size。这两个地方缺一个,大文件上传都会莫名其妙失败。
2.4 数据库连接与时区:一个藏得很深的坑
使用MySQL 8.x版本时,JDBC连接串里一定要带上serverTimezone=Asia/Shanghai,否则如果服务器默认时区不是东八区,Java里的LocalDateTime存进数据库后取出来会发现少了8个小时。另外如果你遇到Public Key Retrieval is not allowed报错,在连接串后面加allowPublicKeyRetrieval=true就能解决。这两个小问题我在部署到云服务器时都遇到过,属于典型环境类坑,提前配好能省去很多排查时间。
3. 后端实现:SpringBoot+MyBatis从Mapper到评分事务
后端我是基于SpringBoot 2.7.x开发的,选这个版本而不是SpringBoot 3.x,原因放在后面细说。先讲讲代码组织的思路和几个核心功能的实现。
3.1 工程结构和数据访问层的选择逻辑
工程目录用Maven标准结构,分包方式按照controller→service→mapper三层来走。DTO(数据传输对象)和Entity(实体)分开——Entity对应数据库表结构,DTO对应当前接口的入参出参。很多学生写项目喜欢直接拿Entity往外抛,项目小的时候没问题,但是一旦评分规则调整,字段变化会直接影响到前端接口,耦合度太高。养成Entity和VO分开的习惯,后期维护会轻松很多。
数据访问层选MyBatis而不是Spring Data JPA,原因是我需要写复杂的SQL来做聚合统计。比如计算某个学生所有作业的平均分、统计一次作业的分数分布区间,这些用JPA的派生查询写起来非常痛苦,而MyBatis的XML里写原生态SQL反而很顺手。
3.2 教师评分接口:事务边界划在哪里
教师评分接口是整个系统里最复杂的一个接口,因为它要同时完成三件事:插入评分记录、更新作业状态、触发一次实时成绩计算。这三步必须放在同一个事务里,否则会出现评分记录写进去了但汇总表没更新的脏数据。我在TeacherEvaluationService里用@Transactional(rollbackFor = Exception.class)保证原子性:
@Service public class EvaluationServiceImpl implements EvaluationService { @Autowired private EvaluationMapper evaluationMapper; @Autowired private SubmissionMapper submissionMapper; @Autowired private ScoreSummaryMapper scoreSummaryMapper; @Override @Transactional(rollbackFor = Exception.class) public void teacherEvaluate(TeacherEvaluationRequest request) { // 1. 检查当前用户是否为该课程教师(异常则拒绝) checkTeacherPermission(request.getAssignmentId()); // 2. 校验是否已评分,防止重复提交 EvaluationRecord record = evaluationMapper.selectExist( request.getSubmissionId(), CurrentUser.getId(), request.getDimensionType() ); if (record != null) { throw new BizException("当前维度已评分,不能重复评分"); } // 3. 插入评分记录 evaluationMapper.insertTeacherScore(request); // 4. 实时计算该提交的最终成绩 recalculateScore(request.getSubmissionId()); } }3.3 MyBatis XML中动态SQL与批量插入互评分数
学生互评的接口通常是一份作业页面上同时给多个同学打分,前端一次性传一个列表,后端要批量插入。MyBatis里批量插入直接拼foreach就好,但需要注意:如果数据量不大(几十条以内),拼接式单条插入加事务反而最可靠。我对批量插入做了分批处理,每20条一批,避免SQL语句过长:
<insert id="batchInsertPeerEvaluations" parameterType="list"> INSERT INTO evaluation (submission_id, evaluator_id, evaluator_role, dimension_type, score, comment_text, create_time) VALUES <foreach collection="list" item="item" separator=","> (#{item.submissionId}, #{item.evaluatorId}, 2, #{item.dimensionType}, #{item.score}, #{item.commentText}, NOW()) </foreach> </insert>3.4 互评得分去极值与加权计算
最终成绩的计算逻辑我单独抽了一个工具类ScoreCalculator,里面有两个核心方法。第一个方法计算互评平均分时,去掉一个最高分和一个最低分;第二个方法根据权重计算最终成绩。
public class ScoreCalculator { /** * 互评得分:去掉最高分和最低分后取平均 */ public static BigDecimal calcPeerScore(List<BigDecimal> peerDimensionScores) { if (peerDimensionScores.isEmpty()) { return BigDecimal.ZERO; } List<BigDecimal> sorted = peerDimensionScores.stream() .sorted() .collect(Collectors.toList()); if (sorted.size() <= 2) { // 人数太少不去极值,直接取平均 return avg(sorted); } List<BigDecimal> trimmed = sorted.subList(1, sorted.size() - 1); return avg(trimmed); } /** * 最终成绩 = 教师评分 * teacherWeight + 互评得分 * (1 - teacherWeight) */ public static BigDecimal calcFinalScore(BigDecimal teacherScore, BigDecimal peerScore, BigDecimal teacherWeight) { return teacherScore.multiply(teacherWeight) .add(peerScore.multiply(BigDecimal.ONE.subtract(teacherWeight))); } }权重的边界情况我在系统里做了限制:teacherWeight的取值范围是0到1,如果教师在发布作业时没设置,默认取0.6。这个值在作业基础表里冗余存了一份,避免每次算成绩都要再去查配置。
3.5 MyBatis缓存:什么时候开、什么时候一定要关
我实测下来,MyBatis自带的二级缓存(<cache/>标签)在这个系统里绝对不能开。因为评分数据的实时性要求很高,一旦开启二级缓存,某个学生评完分后,另一个学生刷新页面看到的还是旧数据,会造成严重的数据不一致问题。而一级缓存(SqlSession级别)默认开启,日常使用已经足够。
另外如果你在自己的项目里用到了TypeHandler做字段类型转换,记住一个原则:自定义TypeHandler只处理与业务无关的类型映射,不要把任何业务逻辑写进去。我在早期版本里试图用TypeHandler做状态值到枚举的自动转换,后来发现排查问题的时候要把数据链路从头捋到尾,非常痛苦。实际上MyBatis对枚举类型有原生的EnumTypeHandler支持,能覆盖绝大多数场景了。
3.6 为什么我坚持用SpringBoot 2.7而不是SpringBoot 3.x
SpringBoot 3.x目前在一些公司内部都没普及,原因主要有三:要求的JDK版本最低是17,而很多学校和老项目的JDK还是8;SpringBoot 3底层用的是Jakarta命名空间,如果你的依赖里有老包,直接编译不过;第三方组件适配SpringBoot 3也需要时间。所以我建议做这套系统时直接用2.7版本,稳定、资料多、出问题一搜就有答案。等整个系统跑通了,再考虑升3.x踩一踩新版的坑也不迟。
4. Vue3前端:从组合式API到评分面板的交互体验
前端部分我用了Vue3 + Element Plus + Pinia + Axios这套组合。Vue3最让我满意的地方是组合式API带来的逻辑复用能力,把评分相关的逻辑抽成独立composable之后,代码清爽了很多。
4.1 前端工程初始化和状态管理层
前端工程我直接用Vite初始化,命令是npm create vite@latest homework-frontend -- --template vue。装完基础依赖后还要装element-plus、axios、pinia、vue-router。千万别在生产环境把element-plus全量引入,按需引入的配置方法Element Plus官方文档写得很清楚,用unplugin-auto-import和unplugin-vue-components这两个插件,实测定下来编译速度和产物体积都比全量引入好得多。
状态管理我选的Pinia,比Vuex直观很多。整个系统只用了两个store:userStore管理登录状态和用户信息,assignmentStore管理当前作业的评分数据和缓存。以用户态为例:
// store/user.js import { defineStore } from 'pinia' import { loginApi } from '@/api/auth' export const useUserStore = defineStore('user', { state: () => ({ token: localStorage.getItem('token') || '', userInfo: null }), actions: { async login(username, password) { const res = await loginApi({ username, password }) this.token = res.data.token this.userInfo = res.data.userInfo localStorage.setItem('token', this.token) }, logout() { this.token = '' this.userInfo = null localStorage.removeItem('token') } } })Axios实例统一做了请求拦截器和响应拦截器,请求拦截器把token塞进请求头,响应拦截器统一处理后端返回的错误码。后端返回结构我定义成统一格式:{ code: 200, data: ..., message: 'ok' },这样前端拦截器只需要判断code,不需要每个页面单独处理错误分支。
4.2 作业提交面板:进度反馈和文件上传的细节
作业提交页的交互分两块:如果是文本作业,直接渲染一个富文本编辑器;如果是文件作业,用el-upload上传。上传组件有一个容易忽略的细节——action属性指向真实的上传接口地址,但headers里的token要从localStorage动态取。很多人直接把token写死在组件的header属性里,一旦登录过期重新登录后token变了,上传组件里还是旧token,上传直接401。所以我在封装上传组件时用:headers="uploadHeaders",这个计算属性每次都从store里取最新的token。
上传过程中要给用户明确的进度反馈,el-upload自带进度条,配合on-progress回调把百分比存到响应式变量里即可。
4.3 互评面板:匿名机制和维度打分如何在前端落地
学生互评面板是这个系统前端交互最复杂的页面。进入互评页面后,系统会从后端拉取一份待互评的作业列表,这个列表经过了后端权限过滤——学生自己提交的作业不会出现在列表里,同时为了避免刷分,后端还限制了一位学生最多评多少份作业(默认阈值10份)。
互评的打分交互我设计成"卡片式评分",每张卡片展示一份作业的基本信息和下载入口,下方是两个维度的滑块评分(完成度、创新性),加上一个可选填的评价文本框。评分全部选择完毕后,点击提交,前端把所有评分数据组装成一个数组一次性发给后端。这里要注意提交按钮要做防重复点击限制,使用一个submitting响应式变量,在请求发出时置为true,请求结束后再恢复false。
4.4 成绩查看页面:分布图和雷达图带来的直观反馈
学生提交作业并评完分之后,可以在成绩面板里看到自己的最终成绩、教师评语、互评平均分,以及一份成绩雷达图和全班成绩分布图。雷达图用来展示分项维度的得分情况,成绩分布图用柱状图展示自己所在的分数段位置。图表我用的ECharts,Vue3里可以直接通过echarts包配合ref拿到DOM节点初始化实例。
提示:Vue3中如果用ECharts,记得在组件销毁时调用
chart.dispose()释放实例,不然单页应用切来切去,内存占用会越来越高。
4.5 路由守卫和权限控制:前端可以做,但不能只靠前端
前端路由我配置了三套Meta信息,分别是requiresAuth(是否要登录)、roles(允许访问的角色列表)。在router.beforeEach守卫里做了跳转判断:没有token就跳登录页,有token但角色不匹配就跳403页面。
但是必须强调一点:前端的路由守卫只是用户体验层面的东西,真正的权限控制一定要在后端做。原因很简单,前端的一切代码都是公开的,任何懂技术的人打开浏览器控制台就能改路由、改角色标识。我在这套系统里后端每个接口都做了权限校验,前端守卫只是让普通用户看不到不该看到的入口而已。
5. 成绩统计与教师看板:一次作业从发布到成绩分析的全流程
一个共评系统,如果光有评分功能没有数据分析和沉淀,那它和传统作业系统也没什么本质区别了。所以我花了比较大的精力在统计报表模块上。
5.1 作业发布流程:教师端如何配置评分规则
教师在创建作业时,需要填写的信息包括:作业标题、作业要求描述、截止时间、作业类型(文本/文件)、是否开启互评、教师评分权重、互评截止时间、互评人数上限。这些配置信息在assignment表中都有对应字段。作业发布之后,学生的作业列表页就会展示出当前待做的作业,并标注状态:未提交、已提交待评分、已评分待互评、已出最终成绩。
状态流转是整个系统的业务主线,我画了一个简洁的状态枚举:
- A0 待提交:教师已发布,学生未提交
- A1 已提交:学生已上传作业,等待教师评分
- A2 互评中:教师评分完成,互评开启,等待学生打分
- A3 已完成:互评阶段结束,最终成绩已计算出,学生可查看
5.2 教师评分列表的性能优化
教师评分列表页展示的数据是一张表:所有已提交学生、文件下载按钮、教师评分输入框(多个维度)、总分交错显示。这个页面的数据量相对可控,但我的分页查询SQL里做了一个小优化——一次性把作业提交信息和该提交的已有评分记录查出来,而不是为每一个学生再做一次子查询。方案是用JOIN加GROUP BY:
SELECT s.id, s.student_id, u.name AS student_name, s.file_url, s.submit_time, MAX(CASE WHEN e.dimension_type = 1 THEN e.score END) AS score_type1, MAX(CASE WHEN e.dimension_type = 2 THEN e.score END) AS score_type2, MAX(CASE WHEN e.dimension_type = 3 THEN e.score END) AS score_type3 FROM submission s JOIN user u ON s.student_id = u.id LEFT JOIN evaluation e ON e.submission_id = s.id AND e.evaluator_role = 1 WHERE s.assignment_id = #{assignmentId} GROUP BY s.id ORDER BY s.submit_time ASC这种写法相当于把所有数据一次性取出来,前端表格直接绑定数组,评分完之后再通过接口提交。实际体验下来,即使一个班有80个人,页面响应也在毫秒级。
5.3 成绩分布统计:SQL聚合和ECharts展示
教师端看板里有一个"成绩分布统计"模块,展示当前作业的分数段分布,比如90分以上多少人、80到89分多少人、及格以下多少人。这个统计我用了一条简单的SQL再加上前端ECharts渲染:
SELECT CASE WHEN final_score >= 90 THEN 'A' WHEN final_score >= 80 THEN 'B' WHEN final_score >= 70 THEN 'C' WHEN final_score >= 60 THEN 'D' ELSE 'E' END AS grade_level, COUNT(*) AS cnt FROM score_summary WHERE assignment_id = #{assignmentId} GROUP BY grade_level;前端的柱状图、饼图渲染和切换非常简单,ECharts官方示例照着改即可。比起统计代码本身,我更想提醒的是:统计类的SQL一定要多斟酌空值情况。如果作业开启了互评但学生评分人数不足,peer_score字段会是空,最终成绩计算时直接当0分处理的话,对学生非常不公平。所以我在calcFinalScore方法里加了防御:互评分数为空时,不做加权,直接用教师评分作为最终成绩。
5.4 数据导出的实用细节:Excel导出防内存溢出
教师端"导出成绩单"功能用的EasyExcel库。这个库最大的好处是支持流式导出,不会把几十万数据一次性加载到内存里导致OOM。班级规模虽然不大,但多门课程累计起来导出几十万条记录是常事。用EasyExcel的async写法,配合Web下载响应流,实测定下来内存占用稳定。
注意:导出文件名的中文编码要处理一下,否则浏览器下载时文件名会乱码。通常在响应头里做URL编码:
Content-Disposition: attachment; filename*=UTF-8''+ URLEncoder.encode(fileName, "UTF-8")。
6. 部署上线:Nginx托管前端、jar包启动后端、MySQL初始化
最后这部分是部署实操。整个项目从前端到后端、从本地到服务器,我把它拆成几个步骤,每一步都给出实际可复制的命令。
6.1 前端构建和Nginx配置
前端本地没有问题之后,执行npm run build生成dist目录,把dist目录整个上传到服务器Nginx的/usr/share/nginx/html/homework目录下。Nginx配置文件的要点是:静态资源location、前端路由History模式的try_files回退、反向代理后端接口。完整配置如下:
server { listen 80; server_name your-domain.com; root /usr/share/nginx/html/homework; index index.html; # 前端History路由回退 location / { try_files $uri $uri/ /index.html; } # 后端接口反向代理 location /api/ { proxy_pass http://127.0.0.1:8080; proxy_set_header Host $host; proxy_set_header X-Real-IP $remote_addr; proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; proxy_read_timeout 300s; } # 上传文件目录的静态访问 location /uploads/ { alias /data/homework/; } }try_files这一行非常重要,如果不加,用户刷新页面到/assignment/5这种具体路由时,Nginx找不到对应的物理文件,会直接404。
6.2 后端jar包启动和参数调优
后端Maven打包方式:在项目根目录执行mvn clean package -DskipTests,打包产物在target/homework-system.jar。启动命令:
nohup java -Xms256m -Xmx512m -jar /opt/homework/homework-system.jar \ --spring.profiles.active=prod \ --server.port=8080 \ > /var/log/homework-system.log 2>&1 &nohup加上&是为了让进程在后台运行。这个系统规模不大,256MB初始堆、512MB最大堆足够,如果你是云服务器,建议根据配置适当调大。日志输出重定向到固定文件,后期排查问题方便。
6.3 MySQL导入初始化脚本
数据库导入非常简单,两条命令:
mysql -u root -p -e "CREATE DATABASE homework DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci;" mysql -u root -p homework < /opt/homework/homework.sql导入完成后,如果你用Navicat或者MySQL Workbench连远程数据库,记得确认两点:一是用户权限,二是mysql_native_password插件问题。MySQL 8默认的认证插件是caching_sha2_password,老版本Navicat或者某些Java驱动连不上,可以在MySQL里对这个用户单独指定IDENTIFIED WITH mysql_native_password BY '密码'。另外如果你连接时遇到SSL相关报错,连接参数里可以加sslMode=DISABLED跳过SSL校验,但仅限于内网或测试环境,生产环境建议还是启用SSL连接。
6.4 部署完成后的第一轮冒烟测试
部署完成后别急着算完事,我建议按下面这个顺序跑一轮冒烟测试:
- 浏览器访问前端首页,确认页面正常渲染、登录接口通
- 用管理员账号建班级、建课、分配教师
- 用教师账号发布作业,确认学生列表可见
- 用学生账号提交一份作业,确认文件上传成功、提交状态变更
- 用教师账号评分,确认最终成绩出现在汇总页
- 开启互评任务,用两个学生账号互相评分,确认互评得分和最终加权分都正确
- 刷新一次页面,确认登录态没有丢失
如果这七步全部通过,系统基本就稳定了。
最后分享一个我在实际做这个项目过程中的心得:共评系统的核心从来不在"评"这个动作本身,而在评分规则的透明度和数据可信度上。设计阶段多花时间想清楚每个细节。比如互评结果要不要匿名、成绩权重怎么定才合理、学生查看成绩时能不能看到互评人的身份——这些问题比任何代码细节都更影响一个系统的成败。我的建议是:匿名保护一定要做,成绩计算逻辑要让老师和学生都能看懂,并且给教师留一个"手动修正最终成绩"的后门。把规则设计得透明、可解释,学生才会真正认可这个评分机制。