SpringBoot+Vue中小学成绩查询系统:从搭建到部署全解析
2026/9/8 23:27:24 网站建设 项目流程

这套“基于SpringBoot和Vue的中小学学生成绩查询系统”,我前后做过几次,也带过不少新人用它入门前后端分离开发。它不复杂,但五脏俱全——用户登录、权限区分、成绩录入、报表统计、前端展示、接口联调全都覆盖,非常适合作为学习和毕设项目的模板。这篇东西我尽量把从零搭建的思路、核心代码、SQL写法、还有那些文档里不写的坑都讲透,你照着做基本能跑通。

先说明一下典型的技术选型:后端用SpringBoot + MyBatis-Plus + MySQL,前端用Vue2或Vue3 + Element UI/Element Plus + Axios。这套组合在中小型管理系统里非常成熟,网上资料多,出了问题也好查。但选型容易,真正上手你会发现一堆细节要处理:权限控制怎么做、成绩排名SQL怎么写才不踩坑、跨域问题怎么解决、打包部署怎样才能不翻车,这些才是这个项目的重点和难点。

1. 项目整体设计与思路拆解

1.1 需求分析:谁在用、解决什么问题

很多人一上来就写代码,容易把系统做成“增删改查工具”,这是大忌。中小学成绩查询系统的核心场景就三个:老师录成绩、学生查成绩、管理层看统计。先把角色和业务理清楚,再做设计,才不会跑偏。

  • 系统管理员:维护教师账号、班级年级信息、课程设置,偶尔还要重置密码。
  • 教师:录入自己所带班级的考试成绩,查看本班及格率、平均分,必要时导出Excel上报。
  • 学生:查询个人单科成绩、总分、班级排名,最好还能看到每次考试的成绩变化趋势。
  • 家长:通常家长复用学生的账号,或者单独做关联,主要就是看成绩,不参与录入。

你把这个角色矩阵写进文档里,后面做权限控制、接口设计、数据库字段设计就有据可依。比如老师录成绩的页面,只能看到自己负责的班级和课程,这就是数据权限的问题;学生查询时只能看到自己的成绩,这又是另一层权限。很多系统做出来被人诟病“没权限概念”,就是因为在需求阶段没把这些场景想清楚。

1.2 技术选型:为什么是SpringBoot+Vue而不是别的

现在做这种管理系统,SpringBoot + Vue已经是绝对的主流,替代了以前JSP + Servlet那套老架构。前后端分离的好处很明显:前端专注页面交互,后端专注数据和业务逻辑,两边通过JSON通信,互不干扰。

拿SpringBoot来说,它对Spring生态做了大量自动化配置,你不需要再写繁琐的XML配置文件,跑一个main方法就能起服务,内嵌Tomcat也省去了单独部署Web容器的步骤。配合Spring MVC、MyBatis-Plus,写CRUD接口的效率非常高。

Vue这边,组件化开发让页面复用变得很舒服。比如成绩表格、学生选择器、筛选条件栏,这些都可以抽成独立组件,在多个页面复用。响应式数据绑定也让表单交互的开发效率提升不少——你只要维护数据,页面自动更新,不用像以前jQuery时代那样手动操作DOM。

有同学会问,用传统模板引擎Thymeleaf不行吗?行,但如果你后面想接小程序、App,或者同一个接口给多个客户端用,前后端分离会舒服得多。这个系统虽然简单,但用分离架构做一遍,等于把当前企业级开发的主流模式练了一遍。

1.3 整体架构与模块划分

我习惯把系统拆成前端和后端两大块来看,后端再按功能模块拆成几个package。

后端结构: com.example.score ├── controller // 接口层,接收前端请求,返回结果 ├── service // 业务层,处理具体业务逻辑 ├── mapper // 数据访问层,操作数据库 ├── entity // 实体类,对应数据库表 ├── dto // 数据传输对象,用于接口入参出参 ├── config // 配置类,跨域、拦截器、安全配置等 ├── common // 公共类,统一返回结果、异常处理等 ├── utils // 工具类,JWT工具、导入导出工具等 前端结构: src ├── api // Axios请求封装,按模块拆分 ├── router // 路由配置 ├── store // Vuex/Pinia状态管理 ├── views // 页面组件 ├── components // 公共组件 ├── utils // 工具函数, request.js等

模块划分上,后端不必刻意做得太复杂,但至少要保证Controller瘦、Service有实际业务逻辑、Mapper只管SQL。很多人做毕设把一堆业务逻辑都堆在Controller里,看着也能跑,但代码一团糟,答辩时老师多问两句就容易露馅。

2. 核心细节解析与实操要点

2.1 成绩表设计:怎么建表才能不返工

数据库表设计是这个系统最重要的一步,表一旦建错,后面改起来会非常痛苦。我先说最核心的几张表,然后重点聊一下成绩表的设计思路。

核心表结构如下:

  • sys_user(用户表):id、username、password(加密码加密存储)、real_name、role(1管理员、2教师、3学生)、create_time。
  • student(学生表):id、user_id(关联用户表)、student_no(学号)、name、class_id、parent_mobile。
  • clazz(班级表):id、grade_name(年级)、class_name(班级)、head_teacher。
  • course(课程表):id、course_name、maybe 还有所属年级。
  • exam(考试表):id、exam_name(如“2025年上学期期中考试”)、exam_type、exam_time。
  • score(成绩表):id、student_id、course_id、exam_id、score、create_time。

成绩表的字段看起来简单,但有几个细节必须注意。

第一,加唯一约束,否则重复数据能烦死你。同一个学生同一门课同一次考试只能有一条成绩记录。建表时直接加上唯一索引:

ALTER TABLE score ADD UNIQUE KEY uk_stu_course_exam (student_id, course_id, exam_id);

这样即使前端点了两次提交,或者两个老师同时录入了同一份成绩,数据库也会帮你挡住重复数据。没有这个约束,后面做统计时数据会莫名其妙翻倍。

第二,冗余字段别加太多,但必要的冗余可以留。有些人在成绩表里直接存学生姓名、课程名称,图省事。这样做的代价是如果学生改名或者课程名称调整,历史成绩表要跟着改。但如果你在成绩表里存了班级ID,后面做“班级平均分统计”会方便不少,这个冗余是可以接受的。

第三,数据类型要选对。成绩字段用DECIMAL(5,2),而不是FLOAT或DOUBLE,因为浮点数会有精度问题,虽然成绩最多100分,但涉及到小数比较和求和,DECIMAL是最稳的。考试时间用DATE或DATETIME,不要用字符串存,不然后面排序和按月统计会很麻烦。

2.2 后端接口设计:统一返回体与接口规范

接口设计是前后端联调的基础。我最想强调的一点是:所有接口返回必须统一格式。很多毕设项目接口返回乱七八糟,一会儿返回一个对象,一会儿返回一个数组,一会儿返回一个报错字符串,前端判断逻辑写到你怀疑人生。

我习惯用这样一个统一的返回体:

public class Result<T> { private Integer code; // 200成功,500失败,401未登录 private String message; private T data; public static <T> Result<T> success(T data) { Result<T> result = new Result<>(); result.setCode(200); result.setMessage("success"); result.setData(data); return result; } public static <T> Result<T> error(String message) { Result<T> result = new Result<>(); result.setCode(500); result.setMessage(message); return result; } }

所有Controller接口都返回这个Result对象,前端Axios拦截器里统一判断code,如果是401就跳转登录页,如果是500就弹错误提示。这样前后端联调时,大家只需要约定好code的含义,不用每个接口单独对接。

接口路径也要规范一些,给几个参考:

  • POST /api/auth/login登录,返回Token和用户信息
  • GET /api/student/profile获取当前学生信息
  • GET /api/score/my学生查询个人成绩(带考试类型筛选)
  • POST /api/teacher/score/import教师批量导入成绩(支持Excel上传)
  • POST /api/teacher/score教师录入/修改成绩
  • GET /api/admin/score/statistics管理员查看统计数据(及格率、平均分等)

这里有个小原则:接口路径尽量用名词,不要出现动词,比如不要写/getStudentList这种,而是写/students配合GET方法。RESTful风格不一定要严格到偏执,但接口设计清晰了,文档都好写很多。

还有一点,接口入参要用DTO对象接收,而不是用Map。用Map的问题是一旦字段拼写错误,到运行时才会发现,而且没有任何IDE提示。用DTO可以让字段类型检查前置,写代码的效率和安全性能高不少。

2.3 前端路由与权限拦截

前端是学生和老师直接看到的东西,做得好不好直接影响体验。我用Vue Router做路由管理,登录后根据角色动态生成菜单和路由。

一个典型的登录后跳转逻辑是这样的:用户登录成功,后端返回用户信息和角色,前端存到Vuex(或Pinia,看你用Vue几),然后路由守卫统一判断:

router.beforeEach((to, from, next) => { const token = localStorage.getItem('token'); if (to.path === '/login') { next(); } else { if (!token) { next('/login'); } else { // 已登录,检查当前用户是否有权限访问该路由 const role = store.state.user.role; if (to.meta.roles && !to.meta.roles.includes(role)) { next('/403'); // 无权限页面 } else { next(); } } } });

路由配置时,给每个页面标记允许访问的角色:

{ path: '/teacher/score', name: 'TeacherScore', component: () => import('@/views/teacher/ScoreManage.vue'), meta: { roles: ['teacher', 'admin'] } }

这样学生角色即使猜到URL,也访问不了教师录入页面。当然,前端路由守卫只是用户体验层面的拦截,真正的安全校验还是要在后端做,这一点答辩时老师特别喜欢问。

Axios封装这块,我摘录一个核心拦截器片段:

service.interceptors.request.use(config => { const token = localStorage.getItem('token'); if (token) { config.headers['Authorization'] = 'Bearer ' + token; } return config; }, error => Promise.reject(error)); service.interceptors.response.use(response => { const res = response.data; if (res.code === 401) { localStorage.removeItem('token'); router.push('/login'); return Promise.reject(new Error('未登录')); } if (res.code !== 200) { Message({ message: res.message, type: 'error' }); return Promise.reject(new Error(res.message)); } return res; }, error => { Message({ message: '网络异常,请稍后重试', type: 'error' }); return Promise.reject(error); });

这样所有接口请求自动带上Token,响应统一先判断状态码,业务代码里不用每次重复写错误处理逻辑。

3. 实操过程与核心环节实现

3.1 从零初始化项目:SpringBoot和Vue的两端搭建

我把初始化步骤拆开来说,这是很多人第一次接触会卡壳的地方。

后端SpringBoot项目初始化。用IDEA自带功能建项目,或者去Spring Initializr官网生成基础工程,都能用。选好Java版本(建议JDK 8或11,别追新,很多教程和依赖对JDK 17的支持还有坑),然后引入这几个关键依赖:

<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.1</version> </dependency> <dependency> <groupId>com.mysql</groupId> <artifactId>mysql-connector-j</artifactId> <scope>runtime</scope> </dependency> <dependency> <groupId>io.jsonwebtoken</groupId> <artifactId>jjwt</artifactId> <version>0.9.1</version> </dependency>

注意MyBatis-Plus的版本别乱选,3.5.x系列比较稳。数据库连接信息写在application.yml里:

spring: datasource: driver-class-name: com.mysql.cj.jdbc.Driver url: jdbc:mysql://localhost:3306/score_db?useUnicode=true&characterEncoding=utf8&serverTimezone=Asia/Shanghai username: root password: 你的密码 mybatis-plus: configuration: map-underscore-to-camel-case: true global-config: db-config: logic-delete-field: deleted logic-delete-value: 1 logic-not-delete-value: 0

map-underscore-to-camel-case这个配置要打开,这样数据库字段student_no可以自动映射到实体类的studentNo属性,省掉一大波XML里繁琐的字段映射。

前端Vue项目初始化。Vue2项目我一般用Vue CLI创建,Vue3项目更推荐Vite,启动速度快很多:

# Vue CLI 方式(Vue2体系) npm install -g @vue/cli vue create score-web # Vite方式(Vue3体系) npm create vite@latest score-web -- --template vue

Vue版本选择上,如果你熟悉Element UI就选Vue2 + Element UI;如果你有精力折腾,直接上Vue3 + Vite + Element Plus + Pinia,代码更现代,组合式API写起来也很舒服。个人建议如果是做毕设,Vue2 + Element UI的资料更多,出问题好搜;如果是练手提升自己,就上Vue3。

项目依赖装完,第一步建议先把Axios和路由配置好,再跑通一个登录接口,确认前后端能够联通。

3.2 登录认证与JWT实现:从登录接口到拦截器

这个系统的权限模块我单独拿出来讲,因为它是所有页面的入口,也是很多同学搞不清楚的地方。

登录接口的流程是:前端传username和password,后端查询用户表,用BCrypt(或MD5加盐,但强烈建议BCrypt)校验密码,密码正确就生成一个JWT Token返回给前端。Token里可以携带用户ID和角色信息,但不放敏感数据。前端拿到Token后存到localStorage,后续每个请求都在请求头带上Token,后端通过拦截器统一校验。

核心的JWT工具类不复杂,主要就是生成和解析这两个方法。以jjwt为例:

public class JwtUtil { private static final String SECRET_KEY = "你的密钥字符串"; private static final long EXPIRE_TIME = 7 * 24 * 60 * 60 * 1000; // 7天 public static String generateToken(Long userId, String role) { return Jwts.builder() .claim("userId", userId) .claim("role", role) .setIssuedAt(new Date()) .setExpiration(new Date(System.currentTimeMillis() + EXPIRE_TIME)) .signWith(SignatureAlgorithm.HS256, SECRET_KEY) .compact(); } public static Claims parseToken(String token) { return Jwts.parser() .setSigningKey(SECRET_KEY) .parseClaimsJws(token) .getBody(); } }

然后写一个Spring MVC拦截器,对需要登录的接口统一处理Token校验:

public class JwtInterceptor implements HandlerInterceptor { @Override public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception { if (!(handler instanceof HandlerMethod)) { return true; } String token = request.getHeader("Authorization"); if (token != null && token.startsWith("Bearer ")) { token = token.substring(7); try { Claims claims = JwtUtil.parseToken(token); request.setAttribute("userId", claims.get("userId")); request.setAttribute("role", claims.get("role")); return true; } catch (Exception e) { // Token无效或过期 } } response.setStatus(401); return false; } }

注册拦截器时注意排除登录接口和静态资源路径,否则会造成死循环:

public void addInterceptors(InterceptorRegistry registry) { registry.addInterceptor(new JwtInterceptor()) .addPathPatterns("/api/**") .excludePathPatterns("/api/auth/login"); }

这里给个小建议:JWT的密钥不要写死在代码里,放到application.yml里通过@Value读取。一是方便修改,二是代码提交到公开仓库时不至于泄露密钥。

密码存储必须用BCrypt。Spring Security里自带BCryptPasswordEncoder,但如果你不想引入整套Spring Security,单独引入spring-security-crypto这个依赖也行。存密码时做hash,千万不要明文存储——这一步如果你做了,答辩时老师问到你也能答得漂亮。

3.3 成绩录入与统计排名:把SQL写明白

成绩录入这个功能,核心业务就是教师选择考试、班级、课程,然后逐个或者批量录入成绩。批量录入比较实用,我做了两种方式:一种是在页面上动态添加行,一次性提交多条;另一种是上传Excel文件,后端解析逐条插入。Excel解析用EasyExcel(阿里出品)比POI写起来简单很多,不推荐直接用原生POI处理,代码量大不说,还容易内存溢出。

批量提交后端接口,接收一个DTO列表,循环插入或更新。这里用到MyBatis-Plus的saveOrUpdateBatch,它可以根据唯一索引判断是新增还是更新,比较省心。

真正亮眼的统计功能在排名SQL这里。学生查自己成绩时,不仅要看到分数,还要看到这门课在班里排第几、总分排名多少。排名SQL的经典写法有两种,不同MySQL版本支持情况不一样。

MySQL 8.0及以上版本,用窗口函数

SELECT student_id, course_id, score, RANK() OVER (PARTITION BY course_id ORDER BY score DESC) AS course_rank FROM score WHERE exam_id = #{examId};

MySQL 5.7及以下版本,用子查询或者用户变量

SELECT s1.student_id, s1.course_id, s1.score, (SELECT COUNT(*) + 1 FROM score s2 WHERE s2.exam_id = s1.exam_id AND s2.course_id = s1.course_id AND s2.score > s1.score) AS course_rank FROM score s1 WHERE s1.exam_id = #{examId};

这个子查询的写法,意思是“比我分数高的人有几个,我就是第几名”,如果分数相同,排名会并列。这个逻辑需要给学生解释清楚,因为他们可能会问:“为什么我跟另一个同学同分,排名却显示一样?”这在成绩系统里反而是正确的处理方式。

总分排名也类似,只是先按学生分组汇总总分,再对总分做排名:

SELECT t1.student_id, t1.total_score, (SELECT COUNT(*) + 1 FROM ( SELECT student_id, SUM(score) AS total_score FROM score WHERE exam_id = #{examId} GROUP BY student_id ) t2 WHERE t2.total_score > t1.total_score) AS total_rank FROM ( SELECT student_id, SUM(score) AS total_score FROM score WHERE exam_id = #{examId} GROUP BY student_id ) t1 ORDER BY total_rank ASC;

这段子查询嵌套比较绕,建议先在Navicat里调试好再搬进Mapper XML里。如果某个考试刚录完成绩还没出总分,这个SQL也能跑出结果,因为空的总分参与排名时会排到末尾,不会报错。

成绩统计报表方面,需求一般包括各科平均分、最高分、及格率。及格率可以写成:

SELECT course_id, COUNT(*) AS total_count, SUM(CASE WHEN score >= 60 THEN 1 ELSE 0 END) AS pass_count, AVG(score) AS avg_score, MAX(score) AS max_score FROM score WHERE exam_id = #{examId} GROUP BY course_id;

再配合ECharts前端画个柱状图展示平均分对比,及格率用饼图显示,页面效果一下就上来了。这个可视化效果在毕设答辩里会很加分,因为老师一眼能看到你的系统不只是简单的数据录入。

3.4 前端页面的核心组件实现

页面这块我挑两个核心场景讲一下:成绩录入页和成绩查询页。

成绩录入页的核心是一个可编辑的表格。教师选择考试、班级、课程后,页面加载该班级学生列表,每个学生后面跟一个输入框。教师填写分数后点击保存,前端把整张表的数据组装成数组,POST给后端。这里要注意的是,分数输入框需要做前端校验:必须为0-100之间的数字,空值提示教师填写,非法字符直接拦截。不然脏数据传到后端处理起来更麻烦。

成绩查询页对学生来说很简单,就是一个筛选区(选择考试)加一个结果表格。表格展示各科成绩、总分、单科排名和班级排名。这里有个细节,低于60分的分数建议用红色标出,高于85分的用绿色标出,让学生一眼能看出哪些科目是薄弱项。前端代码上,通过表格列的cellClassName回调,根据返回值动态设置样式,很简单但体验感提升很大。

如果还想做得更有亮点,可以加一个成绩趋势折线图——学生选择一个时间段内多次考试,ECharts画出各科成绩的波动曲线,学生能看到自己进步还是退步。这个功能不用新增后端接口,查询成绩时带上日期范围过滤就行,前端把多组考试数据传给图表组件,几十行代码就能搞定。

4. 常见问题与排查技巧实录

4.1 跨域问题:前端页面打不开接口怎么办

前后端分离项目遇到跨域简直是家常便饭。浏览器会拦截跨域请求,表现为前端控制台报错CORS error。解决方案有两个方向。

一是后端允许跨域,在SpringBoot里配置一个配置类:

@Configuration public class CorsConfig implements WebMvcConfigurer { @Override public void addCorsMappings(CorsRegistry registry) { registry.addMapping("/**") .allowedOriginPatterns("*") .allowedMethods("GET", "POST", "PUT", "DELETE") .allowedHeaders("*") .allowCredentials(true) .maxAge(3600); } }

二是前端配置代理,开发环境下在vue.config.js里写:

module.exports = { devServer: { proxy: { '/api': { target: 'http://localhost:8080', changeOrigin: true } } } };

两种方案我都用过。开发阶段我更推荐前端代理方案,部署到服务器时再把后端接口路径换成完整域名,这样不用在代码里到处改接口地址。如果你用后端CORS方案,要注意allowCredentials(true)时allowedOriginPatterns不能为*,有些浏览器会报错。还有,在自定义Token拦截器里,如果Token校验失败返回了401状态码,浏览器在CORS预检阶段可能直接把错误截获了,导致你看不到真正的响应信息,排查起来很困惑。这时可以先在浏览器Network里看预检请求的响应头,确认Access-Control-Allow-Origin是否正常返回。

4.2 排名SQL的坑:并列名次和性能问题

排名SQL最常见的坑就是并列排名。用窗口函数RANK()时,同分并列是正常的——两个第2名,下一个人就是第4名,跳过第3名。这是标准排名逻辑。但有些业务希望同分时按学号先后排列,不出现并列,那就要用ROW_NUMBER()加一个排序条件:

ROW_NUMBER() OVER (PARTITION BY course_id ORDER BY score DESC, student_id ASC) AS course_rank

数据量大的时候,排名SQL如果不加索引会非常慢。成绩表多的时候十万条数据照样会把接口拖垮。记得给score表的exam_id、course_id、student_id这三个查询条件字段建联合索引,排名查询性能会提升很多。如果你查的还是总数上千上万的全校排名,后端可以考虑做缓存,每分钟更新一次排名结果,而不是每次请求都实时算。

另外提醒一下,考试ID这个参数,前端传给后端的一定要校验,否则别人可以构造请求去查其他考试的数据。这是在拦截器里已经校验过登录了,但接口级别的参数校验同样要做好,比如校验该班级是否属于该教师、该考试是否存在等。别觉得项目小就不做,数据泄露和越权访问恰恰是这类系统最容易被挑出的毛病。

4.3 Vue和Element版本不匹配的坑

Vue2对应Element UI,Vue3对应Element Plus,这俩组件库不能混用。很多新手一搜教程,把Element UI的代码复制进Vue3项目里,结果组件全渲染不出来,报错信息各种各样。

判断方法很简单:如果你项目package.json里是vue: "^2.x",装的是element-ui;如果是vue: "^3.x",装的是element-plus。如果你用Vue3但用了Element UI的标签,页面会显示成纯HTML而没有样式,控制台报Unknown custom element

表格组件两个版本差异不大,但有几个常用组件的API名称变了。比如「表格分页」在Element UI里叫el-pagination,Element Plus里同样叫这个;但对话框组件,一个叫el-dialogtitle属性,另一个用header……具体记不清的时候直接看官方文档,别凭记忆硬写。

还有,Element Plus的按需引入如果配置不对,组件样式会丢失,页面看着像没渲染一样。最简单的解决方式是不做按需引入,在main.js里全量引入:

import ElementPlus from 'element-plus' import 'element-plus/dist/index.css' app.use(ElementPlus)

小项目全量引入体积也就大几百KB,完全能接受。按需引入的优化等部署上线前再研究也不迟。

4.4 部署打包:前后端怎么放才能跑

开发完要部署,这一步很多人也卡壳。两种常见的方式我都说下。

方式一:前端dist文件扔进SpringBoot静态目录。前端执行npm run build生成dist文件夹,把里面的文件复制到后端src/main/resources/static目录下,然后直接用SpringBoot启动对外提供服务。这个方案的优点是简单,一个jar包全搞定,适合部署在单一服务器上。缺点是前后端混在一起,不太符合“前后端分离”的理念,但如果只是临时用或者演示,真的方便。还要注意,部署模式下前端请求接口的baseURL要写相对路径,不要写死http://localhost:8080/api,不然换服务器就要改代码重新打包。

方式二:Nginx部署前端 + 独立后端jar包。前端dist目录放到Nginx指定的web目录下,Nginx配一个代理,把/api请求转发到后端的SpringBoot服务。SpringBoot单独用java -jar启动。这是目前比较规范的生产部署方式,前后端各管各的,以后扩展前端负载均衡或后端水平扩容都容易做。

server { listen 80; server_name your-domain.com; root /var/www/score-web; 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; } }

你注意到没有,这个配置里try_files用于支持Vue的路由历史模式(history模式),不然刷新页面时Nginx会报404。如果你用的是hash模式,通常不会有这个问题,但URL会带个#号,不太好看。生产环境建议用history模式加Nginx配置,这个坑我帮很多人排过。

还有,如果你数据库部署在同一台机器,记得配置好MySQL的字符集,建库时用utf8mb4,防止中文名乱码。SpringBoot的配置文件里连接数据库的URL要加characterEncoding=utf8参数,否则写入的中文可能变成问号。

4.5 其他容易让人头疼的细节

有几类问题页面每次做这种系统都会遇到,我统一列一下:

  • 时间格式化问题:前端传的日期字符串,后端用@DateTimeFormat接收,否则会报参数绑定失败,或者时间少了8小时。在SpringBoot启动类或配置里统一设置Jackson的时间格式化也能避坑。
  • ID自增和雪花ID:MyBatis-Plus里如果主键用的是数据库自增,实体类ID字段要加@TableId(type = IdType.AUTO);如果是分布式ID场景才考虑雪花算法。单机项目用自增最省心。
  • 批量插入性能:几千条成绩一次性insert,如果循环单条插入,可能要好几秒,用户体验极差。用MyBatis的foreach标签拼成批量insert,或者用saveBatch,速度能快几十倍。
  • 分页查询:列表页的分页记得用PageHelper或MyBatis-Plus的分页插件,不要自己写limit。PageHelper用的时候注意不要嵌套查询,否则count语句可能生成错误,导致返回的总条数不对。
  • 日志打印:接口联调阶段一定要把入参、出参打印到日志里,不然出了问题全靠猜。可以用一个简单的AOP切面,统一打印所有接口请求耗时和参数。

这个项目做到这里,核心功能已经完整覆盖了需求分析里提到的几个角色和场景。成绩录入、成绩查询、排名统计、权限控制、部署上线,一条链路走下来,前后端分离开发里最常碰到的技术点基本都练了一遍。我自己做过不少类似的管理系统,最大的感受是:这类项目的难点不在于某个技术有多深,而在于把每个环节的细节都处理好——表设计够不够合理,接口返还不统一,跨域代理配置对不对,SQL在大数据量下快不快,这些才是决定一个项目质量的关键。

如果你打算拿这个题目做毕设或者用来练习,我建议你在基础功能跑通之后,再加一个自己感兴趣的功能点,比如成绩导出PDF、家长微信端查询、或者成绩异常自动预警。加一个超出教程范围的小功能,做出来之后对技术的理解深度会完全不一样。等这些坑一个个踩过去,你再回头看这个项目,就会觉得当初那些让人抓狂的问题,其实都不过是流程里必经的一环罢了。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询