每年学期末,学校的教务部门往往要为评教这件事忙掉一层皮。过去很多学校还在用Excel收集、人工催办、VLOOKUP算平均分那一套,学生要么被安排到机房集中填报,要么教务员逐个导出数据再按公式汇总。流程长、错误多,最麻烦的是无法做到“对谁保密、对谁公开”的权限控制——学生写了哪个老师的评语,不该被第三人看到,但Excel文件传来传去,谁也说不准有没有外泄。我参与过两所高校的评教系统改造,今天这篇笔记聊的,就是一套基于Spring Boot + Vue的高校学生评教系统。它不是大而全的教务中台,而是专门解决“评教”这一件核心事的独立应用:学生登录后看到自己本学期要评的课程,逐门打分、填写意见;教师端查看自己的评分结果和匿名评语;教务处和院系管理员负责创建评教任务、设置指标权重、统计汇总和导出报表。如果你正在学Spring Boot和Vue,想找个练手的完整前后端分离项目,或者工作中恰好接手了教学评估这类需求,那这篇文章的内容应该能帮你少走不少弯路。
1. 先理清评教系统的业务闭环,再聊Spring Boot和Vue怎么分工
很多初学者拿到“评教系统”这个需求,第一反应就是建几张表、写几个增删改查接口,然后开始堆页面。这么做的结果往往是做到一半发现规则对不上——谁能评谁、什么时候能评、提交之后能不能改、分数怎么加权……全是坑。所以我会先把业务闭环拆开讲明白,技术选型反而放在后面。
1.1 四类用户,四种不同的工作台
评教系统不是只有“学生评老师”这一件事。按角色划分,至少有四类用户:
- 学生:登录后只能看到本学期与自己相关的评教任务,也就是“我需要评哪些课程”,逐门提交。已提交的评教记录不能再修改,更不能重复提交。
- 教师:查看自己课程的评分均分、分项得分、排名区间和匿名评语。教师看不到具体是哪个学生写的评语,也不允许看到其他老师的详细结果。
- 院系管理员:管理本院系的课程、师生归属,创建本院的评教任务,配置指标权重,查看本院排名。
- 教务处/超级管理员:发布评教通知、全局配置、查看全校汇总、导出报表,必要时可以开放补评通道。
这四类用户对应了系统的核心权限模型。前端要做成四个不同的“工作台”,后端接口也要按角色做数据隔离。很多项目把教师和管理员界面混在一起做,最后权限判断写得非常痛苦。我建议一开始就按角色建不同的路由和页面结构。
1.2 评教任务、问卷指标与权重,是业务的三个核心
评教业务可以拆成三层:
第一层是任务。一次评教周期就是一个任务,通常包含学期名称、开始时间、结束时间、任务状态。比如“2024-2025学年第一学期学生评教”,从12月1日开始,到12月31日截止,状态是“进行中”。
第二层是任务下的课程和评教范围。不是所有学生评所有课程,而是“选课关系”决定了评教范围。比如小张选了王老师的《高等数学》,那么这次评教任务里,小张就有一条待评记录。如果小张这学期没选李老师的课,那他就不该看到李老师的评教表单。
第三层是问卷与权重。课程问卷通常按指标维度拆分,常见三类:教学态度、教学能力、教学效果。每个维度下还有若干道题,比如“备课是否充分”“课堂互动是否有效”“作业批改是否及时”。教务处可以给每个维度设置权重,例如教学态度占20%、教学能力占50%、教学效果占30%。
权重这个点特别重要,因为最终总分不是把所有题目分数简单求平均,而是要按维度归一化后再加权。很多第一次做评教系统的人,看到“算出平均分”就以为完成了需求,结果被业务老师拿着规则一问,才发现完全对不上。
1.3 为什么选Spring Boot + Vue这套组合
评教系统的核心场景是“集中式填报”,这意味着开发周期不能太长、部署不能太重,同时又要能扛住评教高峰期学生同时登录提交的压力。Spring Boot + Vue在今天依然是一套非常务实的选择。
Spring Boot负责后端业务和接口。它把Spring生态里那些繁琐的配置做成了自动配置,内嵌Tomcat,打包成jar以后直接java -jar就能启动,不需要单独部署Servlet容器。配合Spring Security和JWT做登录鉴权,再配合MyBatis或MyBatis-Plus操作数据库,开发效率很高。对我来说,Spring Boot最大的吸引力不是技术多新,而是生态成熟,碰到任何问题几乎都能搜到案例,不会让项目卡在环境层面。
Vue负责前端交互。学生评教页面的核心是问卷表单:题目列表、评分单选按钮、评语输入框。用Vue的组件化能力,这些可以被拆成多个可复用组件,维护起来比原来的JSP模板舒服得多。加上Element Plus这套现成的UI组件库,表格、表单、弹出确认框、分页都能直接拿来用,页面开发效率能提升一大截。
前后端分离还有一个额外的好处:教务导出报表、教师查看趋势图表这些场景,未来可以平滑扩展到其他终端,后端接口可以直接复用。虽然目前系统只在PC浏览器上跑,但接口设计得干净,后续要做移动端也不会伤筋动骨。
2. 后端骨架搭建:数据模型、登录鉴权与评教提交
业务规则理清楚之后,后端开发就有方向了。我不会把整个项目的代码贴出来,而是重点讲设计思路和数据模型,因为评教系统真正的复杂度不在增删改查,而在“数据怎么组织”和“提交时怎么防错”。
2.1 数据库表设计:十张表,一次建明白
评教系统的表可以分成四组:用户权限组、基础数据组、评教配置组、评教结果组。
用户权限组:
- sys_user:用户表,字段包括id、username、password、real_name、dept_id等。password保存的是BCrypt加密后的值。
- sys_role:角色表,比如STUDENT、TEACHER、DEPT_ADMIN、ADMIN。
- sys_user_role:用户角色关联表,一个用户可以有多个角色。
- sys_dept:院系表,便于按院系做数据隔离。
基础数据组:
- t_course:课程表,字段包括course_name、course_code、teacher_id、dept_id等。
- t_student_course:选课关系表,保存哪个学生选了哪门课。评教范围最终由这张表推导。
评教配置组:
- t_eval_task:评教任务表,字段包括term、task_name、start_time、end_time、status。
- t_eval_question:问卷题目表,保存题目内容、所属指标维度、分值。题目可以做成可配置,也可以按任务复制一份快照,防止历史任务被后来的修改影响。
- t_eval_weight:指标权重表,保存教学态度、教学能力、教学效果各占多少比例。
评教结果组:
- t_eval_answer:评教答卷表,每一行表示某个学生对某门课程某道题的打分。字段包括eval_task_id、course_id、student_id、question_id、score、comment。
- t_eval_submit:评教提交记录表,每个学生针对一门课程的正式提交记录,用于幂等判断。
这样设计的好处是职责单一。t_eval_answer存的是“明细数据”,后续要做任何维度的统计都能从明细里算出来;t_eval_submit只负责“是否已提交”这个状态,避免在大量明细数据上做count判断是否重复提交。
2.2 Spring Security + JWT:登录鉴权怎么做才不绕
学生评教系统对权限的要求很简单但也严格:学生不能看到别人的评教结果,教师不能看到其他老师的数据,管理员不能越权操作其他院系。建议使用Spring Security配合JWT实现无状态登录,流程图上一眼就能看懂:
- 用户提交用户名、密码到
/api/auth/login。 - 后端校验密码,通过后生成JWT,响应给前端。
- 前端把JWT存到localStorage,并在后续请求的Header里带上
Authorization: Bearer <token>。 - 后端的OncePerRequestFilter拦截请求,解析Token并设置安全上下文。
关键代码很简单,核心是一个过滤器:
@Component public class JwtAuthenticationFilter extends OncePerRequestFilter { private final JwtUtil jwtUtil; public JwtAuthenticationFilter(JwtUtil jwtUtil) { this.jwtUtil = jwtUtil; } @Override protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException { String authHeader = request.getHeader("Authorization"); if (authHeader != null && authHeader.startsWith("Bearer ")) { String token = authHeader.substring(7); String username = jwtUtil.extractUsername(token); if (username != null && SecurityContextHolder.getContext().getAuthentication() == null) { UserDetails userDetails = userDetailsService.loadUserByUsername(username); if (jwtUtil.validateToken(token, userDetails)) { UsernamePasswordAuthenticationToken authentication = new UsernamePasswordAuthenticationToken( userDetails, null, userDetails.getAuthorities()); SecurityContextHolder.getContext().setAuthentication(authentication); } } } filterChain.doFilter(request, response); } }在SecurityConfig里,把登录接口和静态资源接口放进白名单,其余接口一律需要认证。角色控制用@PreAuthorize("hasRole('TEACHER')")这类注解就行,比到处写if判断清爽得多。
2.3 评教提交接口的事务与幂等控制
评教提交是系统里最核心的写接口,调用频率高,业务约束也多。接口接收的数据至少包含这三部分:评教任务ID、课程ID、题目答案列表和总评语。
提交时后端必须做三轮校验:
- 校验评教时间。当前时间必须在任务开始时间和结束时间之间,早于或晚于都不允许提交。
- 校验评教范围。当前学生确实选了这个老师的课,才能对它评教。
- 校验重复提交。同一个学生对同一门课程只能提交一次。
我建议把这三轮校验放在一个事务里,配合数据库唯一索引兜底。表的逻辑大概是:
CREATE TABLE t_eval_submit ( id BIGINT PRIMARY KEY AUTO_INCREMENT, eval_task_id BIGINT NOT NULL, course_id BIGINT NOT NULL, student_id BIGINT NOT NULL, submit_time DATETIME NOT NULL, UNIQUE KEY uk_task_course_student (eval_task_id, course_id, student_id) ) ENGINE=InnoDB;Service层的方法签名和逻辑如下,这是简化版,但能看明白事务和校验的组合方式:
@Override @Transactional(rollbackFor = Exception.class) public void submitEvaluation(EvalSubmitDTO dto) { Long studentId = SecurityUtils.getCurrentUserId(); // 1. 校验评教时间 EvalTask task = evalTaskMapper.selectById(dto.getEvalTaskId()); LocalDateTime now = LocalDateTime.now(); if (now.isBefore(task.getStartTime()) || now.isAfter(task.getEndTime())) { throw new BizException("当前不在评教时间段内"); } // 2. 校验评教范围 if (studentCourseMapper.selectCount(studentId, dto.getCourseId()) <= 0) { throw new BizException("您不在该课程的评教范围内"); } // 3. 校验并插入提交记录 EvalSubmit submit = new EvalSubmit(); submit.setEvalTaskId(dto.getEvalTaskId()); submit.setCourseId(dto.getCourseId()); submit.setStudentId(studentId); submit.setSubmitTime(now); try { evalSubmitMapper.insert(submit); } catch (DuplicateKeyException e) { throw new BizException("该课程已提交过评教,请勿重复操作"); } // 4. 明细表批量插入 dto.getAnswers().forEach(answer -> { EvalAnswer record = new EvalAnswer(); record.setEvalTaskId(dto.getEvalTaskId()); record.setCourseId(dto.getCourseId()); record.setStudentId(studentId); record.setQuestionId(answer.getQuestionId()); record.setScore(answer.getScore()); record.setComment(dto.getComment()); evalAnswerMapper.insert(record); }); }唯一索引加@Transactional,是我在这个项目里最满意的一处设计。即使前端没做按钮禁用、即使两个请求同时到达,数据库也会拦住重复提交,不会出现“学生评了两次、成绩被算了两次”的脏数据。
2.4 评教任务自动截止的定时任务
评教任务有明确的开始和结束时间,教务不可能半夜手动把状态改成“已结束”。这里用Spring Boot的定时任务就能解决。在启动类加上@EnableScheduling,然后写一个关闭过期任务的组件:
@Component public class EvalTaskScheduler { private final EvalTaskMapper evalTaskMapper; public EvalTaskScheduler(EvalTaskMapper evalTaskMapper) { this.evalTaskMapper = evalTaskMapper; } @Scheduled(cron = "0 0 2 * * ?") public void closeExpiredTasks() { evalTaskMapper.updateStatusByEndTime(LocalDateTime.now()); } }注意,单机@Scheduled够用,但如果部署了多个后端实例,定时任务会在每台机器上同时执行。量小的场景问题不大,量大的时候建议引入分布式锁,或者直接用更成熟的任务调度框架,避免重复更新同一批数据。
3. 前端Vue实战:从环境搭建到动态路由,再到评教表单组件
前端我用的是Vue3 + Vite + Element Plus + Pinia,Vue Router负责路由。下面这些内容是我实际搭项目时的操作记录,给你做个参考。
3.1 初始化项目:Vue3配Vite,还是用Webpack?
现在新项目我基本都推荐Vue3 + Vite,启动速度快、依赖安装简单。如果你用的是IDEA开发,建议直接在命令行操作,不要用IDEA自带的模板,版本容易踩坑。
初始化命令:
npm create vite@latest eval-frontend -- --template vue cd eval-frontend npm install然后安装需要的依赖:
npm install vue-router@4 pinia axios element-plus装完之后按模块建目录:src/api放接口请求,src/router放路由配置,src/store放Pinia状态,src/views放页面,src/components放公共组件。这个目录结构很简单,但足够清晰,后续扩展不会乱。
3.2 路由守卫和基于角色的动态路由
学生、教师、管理员看到的页面完全不一样。我不想把所有页面都堆在导航菜单里再靠v-if判断显示哪个,而是希望“不在当前角色权限内的页面,根本进不去”。这里用Vue Router的动态路由就能实现。
基础思路是:登录成功后,后端返回当前用户的角色和可访问的菜单列表,前端把这些信息存到Pinia里。路由守卫每次跳转前检查:
- 如果没登录且访问的不是登录页,则跳转到
/login。 - 如果已经登录且访问登录页,则跳转到首页。
- 如果当前路由配置了
meta.roles,则判断用户角色是否匹配。
路由守卫的典型实现:
router.beforeEach((to, from, next) => { const authStore = useAuthStore(); const token = authStore.token; if (!token && to.path !== '/login') { next('/login'); return; } if (token && to.path === '/login') { next('/'); return; } if (token && to.meta.roles && !to.meta.roles.includes(authStore.roleCode)) { next('/403'); return; } next(); });实际项目里我把教师端路由和学生端路由拆成了两个模块,管理员的任务管理、院系管理、用户管理单独一个模块。每个模块的meta.roles写明确,比如:
const adminRoutes = { path: '/admin', component: () => import('@/views/admin/Layout.vue'), meta: { roles: ['ADMIN', 'DEPT_ADMIN'] }, children: [ { path: 'task', component: () => import('@/views/admin/EvalTask.vue'), meta: { title: '评教任务管理' } }, { path: 'weight', component: () => import('@/views/admin/WeightConfig.vue'), meta: { title: '指标权重设置' } }, { path: 'result', component: () => import('@/views/admin/EvalResult.vue'), meta: { title: '汇总报表' } }, ] };这样每个角色登录后,看到的菜单和能访问的路由是天然隔离的,不会出现学生打开教师端页面再被接口鉴权弹出一堆错误的情况。
3.3 评教问卷页面的组件化设计
评教页面是整个系统使用频率最高的页面,做得顺手不顺手,直接关系到学生愿不愿意认真填。我把它拆成了三个组件:
EvalTaskList.vue:展示待评课程列表,每张卡片显示课程名、教师名、当前状态,以及实时进度。EvalForm.vue:评教表单容器,负责加载问卷题目、校验填答完整度、提交数据。QuestionItem.vue:单道题目的渲染组件,根据题目类型动态渲染单选、多选、文本输入框。
打分题我用的是Element Plus的el-rate或el-radio-group。这里有一个细节:分数值在后端设计成1到5分。展示的时候,如果直接用数字会显得生硬,可以给每一档配一个描述,比如“5分=非常满意,4分=满意,3分=一般,2分=不满意,1分=非常不满意”。有了描述,学生打分的时候会更稳定,不会因为理解不一致导致评分标准漂移。
提交前必须校验完整度。做法是遍历当前页面的题目,检查每道题是否有值;有漏答就弹提示并定位到第一个未答项。代码大致是这样:
const submit = async () => { const unanswered = questions.value.filter(q => !answers.value[q.id]); if (unanswered.length > 0) { ElMessage.warning(`还有 ${unanswered.length} 道题未评分,请检查后提交`); return; } await evalApi.submit({ taskId, courseId, answers: answers.value, comment }); };还有一个容易被忽略的点:学生提交完一门课后,列表页要立刻把该课程标记为“已评”,并在侧边显示剩余待评数量。这个状态我放在Pinia里维护,提交成功后调接口重新拉取待评列表,保证多页面切换时数据一致。
3.4 接口封装与状态管理
前端直接在每个页面里写axios请求会非常难受,尤其是Token失效要统一跳登录、接口报错要统一弹提示这些公共逻辑。我习惯在src/api里建一个request实例:
import axios from 'axios'; const request = axios.create({ baseURL: '/api', timeout: 10000, }); request.interceptors.request.use(config => { const token = localStorage.getItem('token'); if (token) { config.headers.Authorization = `Bearer ${token}`; } return config; }); request.interceptors.response.use( response => response.data, error => { if (error.response && error.response.status === 401) { localStorage.removeItem('token'); window.location.href = '/login'; } return Promise.reject(error); } );然后把每个后端接口按模块封装成函数,比如src/api/eval.js里导出getTaskList、getQuestions、submitEvaluation。页面里只调用这些函数,完全不感知axios细节。这样做的收益在项目后期非常明显,改接口地址、加统一参数都只需要动一个文件。
4. 评教汇总、权重计算与结果可视化
评教数据在学生提交那一刻开始才真正产生价值。教务和老师最关心的,是“某某老师这学期评分怎么样”“和上学期比是涨是降”“哪个维度拉了后腿”。这一章讲统计与展示,也是系统里最容易算错账的地方。
4.1 权重计算:别把平均分直接当总分
评教系统的分项指标体系通常是这样:问卷分为教学态度、教学能力、教学效果三类,每类各若干道题,每道题满分5分。假设教学态度有4道题、教学能力有6道题、教学效果有4道题,某个老师收到的有效答卷是200份。怎么算这个老师的总分?
第一步,算每个维度的平均分。把该维度下所有学生所有题目的得分求和,再除以题目总数和学生总数。
第二步,把维度平均分乘以对应权重。假设权重配置是教学态度20%、教学能力50%、教学效果30%,那么:
| 指标维度 | 平均分 | 权重 | 加权得分 |
|---|---|---|---|
| 教学态度 | 4.2 | 20% | 0.84 |
| 教学能力 | 4.5 | 50% | 2.25 |
| 教学效果 | 4.0 | 30% | 1.20 |
| 总分 | 4.29 |
加权得分的计算逻辑要写清楚,否则教务处追问“为什么平均分4.2,总分却显示4.29”的时候,解释起来很费劲。公式是:
总分 = Σ(维度平均分 × 维度权重)SQL可以这样查某个任务下每个教师的维度平均分:
SELECT teacher_id, dimension, AVG(score) AS avg_score FROM t_eval_answer WHERE eval_task_id = ? GROUP BY teacher_id, dimension然后再在Java内存里做权重乘法,最后按总分排序。这个方案比试图用一大段SQL直接算出排名要清晰得多,而且调试方便。
4.2 教师端报表:ECharts展示历年趋势和维度雷达图
教师端不应该只给一个冷冰冰的总分。我做了三个展示模块:
- 当前任务的总分和排名区间。老师只需要知道自己在全校或本院的大致位置,不需要精确排名。做法是后端算完排名后,返回该教师排名和人数的比值,比如“前15%”。
- 维度得分雷达图。用ECharts的雷达图展示教学态度、教学能力、教学效果这三个维度上的得分,教师一眼就能看出自己的短板。
- 历年趋势折线图。按学期展示总分变化,方便教师复盘。
ECharts的引入很简单,使用echarts库,关键是后端提供的接口数据格式要稳定。我返回的JSON结构类似:
{ "termList": ["2023-2024-1", "2023-2024-2", "2024-2025-1"], "scoreList": [4.12, 4.28, 4.35], "dimensionData": { "教学态度": [4.0, 4.3, 4.4], "教学能力": [4.1, 4.2, 4.3], "教学效果": [4.0, 4.3, 4.3] } }前端拿到数据后按固定格式装配进ECharts的option,代码非常机械,但稳定性很重要,别在接口里塞一堆字段名风格不一致的key,否则前端的适配工作会把你折磨到怀疑人生。
4.3 管理员汇总页面:明细导出与数据透视
管理员最终需要的是“能拿去开会”的报表。我提供了两个操作:按院系统计的汇总表和明细表导出。
汇总表展示每个院系、每名教师的得分,支持按排名排序。明细表则是所有答卷的原始数据,教务可以用来做抽样复核。导出功能后端用EasyExcel实现,前端点击按钮后请求接口下载文件流:
const exportExcel = async () => { const response = await request.get('/api/admin/eval/export', { params: filters, responseType: 'blob' }); const blob = new Blob([response.data]); const link = document.createElement('a'); link.href = URL.createObjectURL(blob); link.download = '评教汇总.xlsx'; link.click(); };这里有一个安全细节:导出的Excel里如果包含学生姓名、学号、IP地址这类个人信息,要脱敏。学号中间四位用*代替,学生姓名只显示姓和“同学”。别小看这一步,很多学校的信息安全检查会把报表外泄当成重大事故来处理。
5. 联调、部署与上线前必须避开的坑
最后这部分是我真实踩过的坑,比前面的设计问题更实在。
5.1 开发环境跨域:Vite代理与后端点对点配置
前后端分离开发,最常见的问题是“前端页面访问端口5173,后端接口在8080,跨域了怎么办”。两种处理方式,我更推荐开发时用Vite代理,生产时用Nginx反向代理。
Vite的配置写法:
// vite.config.js export default { server: { port: 5173, proxy: { '/api': { target: 'http://localhost:8080', changeOrigin: true } } } };这样前端代码里请求/api/eval/task/list,Vite开发服务器会自动转发到http://localhost:8080/api/...,浏览器层面不存在跨域问题。如果你的后端加了Spring Security,记得放行/api/auth/login等白名单接口,否则代理配上也会收到401。
生产环境用Nginx,配置写在后端服务的上一层:
server { listen 80; server_name eval.example.edu.cn; root /var/www/eval/dist; index index.html; 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; } }location /里的try_files必须带,否则Vue Router使用history模式时,刷新非首页地址会返回404。这是新手最容易踩的坑,没有之一。
5.2 数据库时区与JSON日期格式
评教任务有时间段,统计报表里有提交时间,稍微搞错时区,就会出现“任务明明是今天截止,却提前一天显示已结束”的诡异问题。反正我在上线前被这个问题坑过。
MySQL连接串里务必加serverTimezone=Asia/Shanghai,比如:
spring: datasource: url: jdbc:mysql://localhost:3306/eval_db?useUnicode=true&characterEncoding=utf8&serverTimezone=Asia/Shanghai同时,Spring Boot返回给前端的LocalDateTime默认格式是ISO格式,比如2025-01-15T10:30:00,前端直接用不太友好。我建议统一配置成yyyy-MM-dd HH:mm:ss:
spring: jackson: date-format: yyyy-MM-dd HH:mm:ss time-zone: GMT+8后端也不要混用Date和LocalDateTime。项目里如果两种类型混着用,序列化和比较时的坑会连环爆。我现在的做法是全项目统一用LocalDateTime,数据库字段统一用datetime类型,逻辑清爽不少。
5.3 打包构建与部署脚本
后端部署是常规操作:
mvn clean package -DskipTests java -jar target/eval-system.jar --spring.profiles.active=prod前端部署:
npm run buildVite会生成dist目录,把dist里的静态文件复制到Nginx的/var/www/eval/dist下就行。更新部署时要注意:前端文件更新后,Nginx不需要重启,浏览器会自动加载新资源;但如果使用了带hash的静态文件名,还要留意CDN缓存问题,没有CDN的话基本不用担心。
5.4 上线前一定要检查的三件事
第一,数据库迁移脚本必须走版本控制。评教系统会经历多次需求和规则调整,比如权重从固定值变成可配置、题目从固定变成按任务快照。如果靠手工改数据库,早晚出事故。我建议引入Flyway管理数据库结构变更。
第二,敏感信息的访问日志要过滤。学生评教属于教学管理信息,评教明细最好不打印到应用日志里。我上线前把日志配置里的org.springframework.web调成了WARN级别,避免请求参数中暴露学生ID和题目答案。
第三,高并发场景要提前做压测。学生评教通常集中在截止前两三天,登录、提交接口的瞬时流量不小。用JMeter模拟个几百并发打一下/api/auth/login和/api/eval/submit,看看响应时间是否可接受,连接池数量是否够用。如果数据库连接池默认的10条不够,可以调大到30或者50,但也要考虑数据库上限。
最后说一点个人体会。评教系统这类“小而明确”的校园应用,其实是最适合拿来练手完整前后端项目的题材。麻雀虽小,五脏俱全:有权限、有任务状态机、有问卷渲染、有统计计算、有批量导出,还有典型的联调部署问题。我在把系统交给教务处试运行的那个学期,最经常收到的反馈不是“界面不好看”,而是“导出Excel能不能按分位数自动标色”“能不能让班长看到班级提交进度”。这类需求听着小,但每次都能带出一轮新的设计思考。如果你也在做类似的系统,希望这篇笔记里的经验能帮你提前躲开我踩过的那些坑。