☰
SSM+微信小程序成绩查询系统:从部署到二次开发完整指南
2026/10/1 12:46:52 网站建设 项目流程

简介:面向软件工程、计科、人工智能等计算机相关专业的学生,这份基于SSM框架与微信小程序的学生成绩查询系统,是一套完整的高分毕业设计项目,提供可运行的工程源码、数据库脚本及使用文档,适用于毕业设计、课程设计、项目初期演示,也适合作为Java与小程序开发的学习进阶案例。包内共1218个文件,整体约20.17MB,覆盖Java后端源码、JSP页面、SSM整合配置、微信小程序WXML/WXSS/JS逻辑,以及SQL数据库脚本、HTML/CSS样式、图片图标等资源,结构清晰便于对照理解前后端数据交互。目前已有92人学习浏览,源码在mac与Windows10/11环境下均通过运行测试,且项目曾获导师认可、答辩评审95分,参考价值较高。资源将前端界面、后端接口、数据库表结构及配套说明文档分模块整理,既可整体部署快速体验,也可按需修改扩展实现其他功能,对系统掌握SSM框架整合与微信小程序开发流程很有帮助。

1. 这个东西到底是什么:SSM+微信小程序成绩查询的完整交付物

每年毕业季都能看到大量类似标题的资源挂在网上:java毕业设计、SSM框架、微信小程序、成绩查询、源码+数据库+使用文档。如果你正在找毕设选题或者刚拿到这样一份压缩包,大概率想知道三件事:这套东西能不能跑起来、好不好改、答辩时讲什么。这篇文章基于这类项目的常见组织方式,把从解压到部署再到二次开发的完整链路讲清楚。它本质上是把一个学生的成绩查询场景拆成了两个端:老师/管理员在Web后台维护成绩数据,学生在微信小程序里查成绩。SSM负责拦请求、算业务、读写数据库,小程序负责把数据展示成手机上的界面,中间用HTTP接口对齐。适合用作Java方向毕业设计,也适合想快速上手SSM+小程序开发的人当脚手架,但这不是一个拿来就能商用的高并发系统,搞清楚它的边界再动手会省很多时间。

2. 工程全景:从解压到本地跑通的目录结构与技术选型

拿到源码+数据库+使用文档.zip之后,第一步不是打开IDE,而是先建立对工程结构的全局认识。这类项目通常解压后包含三个物理块:一个SSM后端工程、一个微信小程序前端目录、一份数据库初始化SQL文件,外加一个说明文档。很多人卡在“源码有了但跑不起来”,九成是没搞清楚这三个块之间的依赖关系就开始乱试。你要是能把这个关系梳理明白,后面每一行代码都有它的落点。

2.1 SSM后端三个框架各自干了什么

SSM是Spring、SpringMVC、MyBatis三个框架的缩写组合,在这套成绩查询系统里,每个框架的职责分得很清楚。

Spring是整体的容器与粘合剂。它管理Service层和DAO层的对象生命周期,控制事务边界。成绩新增、批量导入、删除这些操作落在同一个事务里,中间任何一步出错,数据库都不会留下半截数据。Spring还接管了数据源配置,通过applicationContext.xml连接MySQL连接池。

SpringMVC负责HTTP层。微信小程序发过来的每一次请求,比如“ queryScore?studentNo=2023001 ”,都由DispatcherServlet接住,映射到ScoreController的具体方法,再通过@ResponseBody把返回结果序列化成JOSN字符串送回小程序端。这套系统的所有对外接口都走这个模式。

MyBatis负责SQL层。你可以在XML里写具体的查询语句,比如按学号查全部课程成绩、按课程查全班成绩分布,然后在Mapper接口里声明同名Java方法,二者通过namespace和id绑定。这样SQL和Java代码分离,改查询逻辑时不用动Java编译单元,比纯JDBC好维护得多。

2.2 小程序端和Web后端的物理边界

源码里的小程序目录一般是典型原生微信小程序结构:

miniprogram/ ├── app.js // 小程序全局逻辑,初始化时获取用户登录态 ├── app.json // 页面注册与tabBar配置 ├── pages/ │ ├── login/ // 学生登录页:学号+密码 │ ├── score/ // 成绩查询主页:按学期筛选课程 │ ├── detail/ // 成绩详情页:单科成绩与学分 │ └── profile/ // 个人中心:绑定学号与重置密码 └── utils/ └── request.js // 封装wx.request,统一带token和错误提示

Web后台的前端静态页面一般放在webapp目录下,管理员登录后可以录入学生信息、录入成绩、按班级查看统计。后端工程里得分清楚哪些请求是给小程序用的API,哪些是给浏览器访问的页面。常规做法是API路径统一以/api/开头,页面路径保持原样,避免两类请求在拦截器里混淆。

2.3 使用文档里必须确认的四项配置

拿到资料后不要急着导入,按顺序核对四份配置:首先是jdbc.properties里的数据库连接字符串、用户名、密码,这三项不对,后端连数据库这一步直接失败。第二是web.xml里的springMVC映射路径,确认它拦截的是/api/*而不是/*,否则静态资源全部被拦。第三是小程序project.config.json里的appid,第四是utils/request.js里baseURL指向的IP和端口。

这四件事看起来琐碎,但任何一个不对,都会在调试时报出迷惑性错误。比如数据库连不上时报的是Communications link failure,你第一反应觉得是网络问题,其实十有八九是jdbc配置里的IP写成了localhost而数据库只允许本机连接。把这些问题前置处理完,部署才是一个顺畅的过程。

2.4 配置数据源的最小可运行集合

从技术落地价值看,spring-dao.xml里数据源这块是最值得手工核对的部分。常见配置如下:

<bean id="dataSource" class="com.zaxxer.hikari.HikariDataSource" destroy-method="close"> <property name="driverClassName" value="com.mysql.cj.jdbc.Driver" /> <property name="jdbcUrl" value="jdbc:mysql://localhost:3306/score_db?useSSL=false&amp;serverTimezone=Asia/Shanghai&amp;characterEncoding=utf8" /> <property name="username" value="root" /> <property name="password" value="123456" /> <property name="maximumPoolSize" value="20" /> <property name="minimumIdle" value="5" /> <property name="connectionTimeout" value="30000" /> </bean>

这段配置里的几个参数值得展开说。useSSL=false必须加,MySQL 8默认开启SSL握手,不加的话连接日志里会出现大量SSL告警,虽然不影响运行但干扰排查问题。serverTimezone=Asia/Shanghai也不能省,否则MySQL驱动会把服务器时区当成UTC,导致查询结果里的日期比北京时间早8小时。maximumPoolSize=20是应届项目里比较安全的池大小,太高会浪费数据库连接,太低在答辩演示同时打开小程序和后台时容易撑不住。connectionTimeout=30000的意思是最多等待30秒获取一个连接,如果池子被占满而等待超时,再排查是哪条慢SQL造成的。

这些参数在本地实验环境下基本都能一次跑通,但换一台机器部署时最容易出问题的反而是jdbcUrl里的localhost——你的数据库如果装在别的服务器上,这里要改成对应IP。很多人在自己电脑上开发时一切正常,拷到室友电脑上就翻车,就是这个原因。

3. 把后端跑起来:从源码导入到登录接口能通

后端部分的落地目标很明确:在本地把这套SSM工程启动起来,让小程序能够拿到成绩数据。这里涉及三个连续动作:导入工程、建库、启动验证。每一步都有常见误操作,我在对应位置说明坑在哪。

3.1 导入源码时的JDK和Maven版本要求

这类项目一般基于Java 8或Java 11开发,对应Tomcat 8.5或9.0。导入前先检查本地环境,命令行里执行java -version确认JDK版本,再执行mvn -v确认Maven版本。JDK版本过高会带来麻烦:JDK 17及以上移除了javax.annotation包,而SSM项目里普遍引用了@Resource注解,编译会直接报错。解决办法是降回JDK 8,或引入javax.annotation-api依赖,后者需要去pom.xml手动加。

Maven仓库用阿里云镜像还是默认中央仓库,决定了首次导入的体验。中央仓库在国内拉依赖非常慢,容易让人误以为工程卡住了。settings.xml里配置阿里云镜像是一个常规操作:

<mirror> <id>aliyunmaven</id> <mirrorOf>central</mirrorOf> <url>https://maven.aliyun.com/repository/public</url> </mirror>

配置完成后,刷新Maven工程等待依赖下载完毕。观察重点不是进度条走了多少,而是pom.xml里的依赖是否全部成功解析。常见的失败样板是spring-webmvc和mybatis-spring版本不匹配导致的NoClassDefFoundError,这种问题多半要在pom.xml的<properties>里统一版本号。

3.2 初始化数据库:执行SQL脚本的顺序别搞反了

数据库初始化是很多人习惯跳步的环节。拿到database/score_db.sql后,不要直接在IDE里打开,而是用命令行或Navicat执行整个脚本。常规脚本里包含建库语句、建表语句、初始数据三条内容。用命令行执行的顺序是:

mysql -u root -p < score_db.sql

执行之前先在MySQL里看一眼默认字符集,常见做法是CREATE DATABASE score_db DEFAULT CHARSET utf8mb4;。如果你拿到的脚本里已经有建库语句,直接执行即可;如果没有,先手动建库再执行建表数据部分。字符集千万不能是utf8,MySQL的utf8最多只能存3字节,而微信小程序端提交的用户昵称或留言里的emoji占4字节,写入时直接报Incorrect string value错误。

初始数据里一般包含一个管理员账号和几个测试学生账号,记下来,后面登录要用。常见的库存账号是admin/admin123作为Web后台登录,2023001/123456作为小程序端学生登录。这些数据帮你省去手工录入学号密码的过程。

验证建库成功的SQL是:

USE score_db; SHOW TABLES; SELECT * FROM sys_user LIMIT 5;

如果sys_user表能查出数据,说明脚本执行成功。如果表名不是sys_user,是t_user或user_info,不用慌,这类项目命名风格各异,以脚本实际结构为准。重点是你得能找到存放账号密码的那张表,以及存放成绩记录的那张核心表,后面的二次开发全靠这两张表。

3.3 启动Tomcat并解决端口冲突

项目结构如果不是Maven父子工程,而是传统的webapp目录,需要配置一个Tomcat Server运行环境。在IDEA里选择“Tomcat Server→Local”,把Deployment指向该工程的war exploded包,然后设置Application context为根路径/。这里有一个容易忽略的点:如果context设置的路径不是/,小程序端请求路径就要拼接上这个前缀,否则全部404。

端口冲突也是高频问题。8080端口经常被其他进程占用,Tomcat启动日志里会直接报Port 8080 was already in use。解决方式是换空闲端口:

lsof -i :8080 # mac/linux查看占用进程 netstat -ano | findstr :8080 # windows查看占用进程

把Tomcat的HTTP端口改为8081,重启后访问http://localhost:8081/api/health验证接口是否通。设计后端时一般预留一个健康检查接口,返回{"code":200,"msg":"ok"},没这个路径则直接访问登录接口。

3.4 SSM三层架构里的请求流向与关键代码

请求从进入后端到返回数据,经历的是Controller→Service→Mapper三层。在答辩或改代码时,别人问你“成绩是怎么查出来的”,你得能把这个链路讲清楚。以查询成绩为例,Controller层接收前端传来的学号和学期,调用Service层方法,Service层根据业务规则决定直接查库还是先校验权限,最后Mapper层执行SQL返回结果。

对应的Controller核心代码大致如下:

@RestController @RequestMapping("/api/score") public class ScoreController { @Autowired private ScoreService scoreService; @GetMapping("/query") public Result queryScore(@RequestParam String studentNo, @RequestParam(required = false) String term) { if (studentNo == null || studentNo.trim().isEmpty()) { return Result.error("学号不能为空"); } List<ScoreVO> list = scoreService.queryByStudentNo(studentNo, term); return Result.success(list); } }

@RequestParam String studentNo对应小程序端请求的studentNo字段;required = false表示学期字段允许为空,不传时查全部学期的成绩。Result是一个统一返回对象,常见设计里包含code、msg、data三个字段。前端拿到code=200才解析data,否则弹出错误提示。这套协议一定要定死,后面小程序端解析数据全靠它。

Service层常见的逻辑是校验学号是否存在,再判断该学生是否允许查询,如果学生被锁定则禁止查询。具体现场项目里,也可以不校验这些,直接调用Mapper查询。但答辩时能讲出权限校验这个点,比单纯说“我查了一下数据库”更有说服力。

4. 微信小程序端:从登录到成绩列表的完整链路

小程序端的目标是让用户打开微信就能查成绩,不用装App,也不用记住Web后台的地址。实现上有两条路可以走:用原生微信小程序语法,或用uni-app跨端框架。这个标题下最常见的是原生小程序,下面按原生方式拆解。

4.1 wx.request的封装方式与鉴权流程

小程序端所有请求都打在utils/request.js里统一封装,这是比在每个页面单独写wx.request更可靠的实践。统一封装能处理三件重复出现的事:请求头带登录凭证、HTTP状态码统一判断、登录失效时自动跳转登录页。

核心封装代码示意:

const BASE_URL = 'http://localhost:8081/api'; function request(path, method, data) { return new Promise((resolve, reject) => { wx.request({ url: BASE_URL + path, method: method || 'GET', data: data || {}, header: { 'Content-Type': 'application/json', 'token': wx.getStorageSync('token') || '' }, success(res) { if (res.data.code === 200) { resolve(res.data.data); } else if (res.data.code === 401) { wx.redirectTo({ url: '/pages/login/login' }); reject(res.data); } else { wx.showToast({ title: res.data.msg, icon: 'none' }); reject(res.data); } }, fail(err) { wx.showToast({ title: '网络请求失败', icon: 'none' }); reject(err); } }); }); }

BASE_URL指向后端服务的IP和端口。真机调试时不能填localhost,因为手机访问不到你电脑上的回环地址。常规做法是填电脑的局域网IP,比如http://192.168.1.101:8081/api,并在微信开发者工具的“不校验合法域名”选项上打勾。这个选项只存在于开发版和体验版,发布线上版本时必须配HTTPS域名,否则请求被微信拦截。

token从小程序的Storage取出,每次请求自动带上,后端拦截器校验token是否存在且有效。这类毕业设计项目一般选择简单方案:登录成功后后端签发一个UUID作为token,存放在服务端内存或数据库表中,客户端后续请求带上这个值。它不完美,无法水平扩展,但用于毕业设计和演示完全够用。

4.2 登录页的实现逻辑与密码传输问题

登录页是学生接触系统的第一个界面,实现上是表单+校验+提交请求三个动作。关键点在提交前的非空校验和提交后的状态处理。

Page({ data: { studentNo: '', password: '' }, handleLogin() { const { studentNo, password } = this.data; if (!studentNo) { wx.showToast({ title: '请输入学号', icon: 'none' }); return; } if (!password) { wx.showToast({ title: '请输入密码', icon: 'none' }); return; } request('/user/login', 'POST', { studentNo, password }).then(data => { wx.setStorageSync('token', data.token); wx.setStorageSync('studentNo', studentNo); wx.switchTab({ url: '/pages/score/score' }); }); } });

wx.setStorageSync把token和学号存到本地,一次登录后续请求不用重复输入。密码的传输方式是这类项目最常见的软肋:小程序端没有做RSA或MD5加盐,密码明文躺在请求体里。毕设场景这么干勉强可以,但如果你想让答辩老师挑不出毛病,可以在登录前用crypto-js做一次MD5摘要。注意MD5只是摘要不是加密,后端需要做同样处理再比对。整条链路缺了这一步,等于向局域网内所有人公开了登录凭据。这里算是一个可以写进论文的改进点。

4.3 成绩列表页的渲染与下拉刷新

成绩数据回来后要渲染成可读的界面。原生小程序的列表渲染用wx:for,核心难点不是界面布局,而是状态管理:加载中显示什么、空数据提醒什么、加载失败提示什么。页面代码大致如下:

<view class="score-card" wx:for="{{scoreList}}" wx:key="id"> <view class="course-name">{{item.courseName}}</view> <view class="score-value">{{item.score}}</view> <view class="credit">学分:{{item.credit}}</view> <view class="term">{{item.term}}</view> </view>

wx:key="id"不只是为了消除控制台警告,更重要的是帮助小程序复用DOM节点,列表多的时候渲染性能差异明显。数据绑定用{{item.xxx}}取当前遍历项字段,字段名要与后端返回的JSON字段一一对应。如果后端返回的是course_name而前端写courseName,显示出来的就是空白。应对这种情况,两种方案任选其一:在后端VO里加一个@JsonProperty("courseName")注解映射,或者在接收数据后做一层字段名转换。前者更符合Java习惯,改动最小。

下拉刷新在onPullDownRefresh生命周期函数里处理,执行后需要手动调用wx.stopPullDownRefresh()结束动画。忘记停动画是最常见的翻车现场,用户在页面上疯狂下拉,旋转菊花一直在转,其实数据已经拿到了。只要在接口调用正常返回或失败抛出后,都调用一次停止动画,这个坑就避开了。

4.4 空态、异常态、加载态的三态处理

成绩查询这种工具型小程序最容易在分页和空结果上翻车。状态处理的逻辑大致是:进入页面先显示加载中,接口返回后若列表为空则展示空状态提示,若报错则展示错误提示和重试按钮。三态缺了任何一态,体验都会变得残缺。

实操上可以用data.status控制页面显示:

data: { status: 'loading', // loading | success | empty | error scoreList: [] }

每个状态对应一个wx:if的模板片段。看起来只是多写了几个分支,但碰到“用户没选学期就点查询”这种交互场景,可以快速定位问题,而不是对着白屏猜原因。

5. 数据库设计避坑:成绩表字段、外键约束与SQL排查

把项目部署起来之后,你会开始动数据库。成绩查询系统的核心表结构并不复杂,但恰恰因为简单,很多细节容易被忽略,等到临近答辩才发现数据对不上。这一章集中写成绩表设计的取舍、常用SQL的几个坑,以及联调阶段数据对不齐时的排查方法。

5.1 成绩表需要哪些字段才算不缺不滥

一份标准的成绩记录表设计大致如下:

CREATE TABLE score_record ( id BIGINT PRIMARY KEY AUTO_INCREMENT COMMENT '主键', student_no VARCHAR(20) NOT NULL COMMENT '学号', course_code VARCHAR(20) NOT NULL COMMENT '课程编码', course_name VARCHAR(100) NOT NULL COMMENT '课程名称', score DECIMAL(5,2) NOT NULL COMMENT '成绩分数', credit DECIMAL(3,1) DEFAULT 0 COMMENT '学分', term VARCHAR(20) NOT NULL COMMENT '学期,如2024-2025-1', exam_type VARCHAR(10) DEFAULT '正常' COMMENT '考试类型:正常/补考/重修', create_time DATETIME DEFAULT CURRENT_TIMESTAMP COMMENT '录入时间', update_time DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间' );

DECIMAL(5,2)存成绩,是因为浮点类型存99.9这类小数时可能产生精度误差,用DECIMAL才能保证总分100.00精确存储。VARCHAR(20)存学号,学号前面通常有0,如果改用BIGINT存储,前导0会被吞掉,2023001变成2023001的本意没问题,但0101开头的学号会出问题。term字段设计成VARCHAR存储”2024-2025-1“这类字符串,不要拆成年份和学期两个字段,查询时一个等值条件就能匹配。

exam_type这个字段是加分项,却没多少人用。正常考试、补考、重修的成绩对绩点影响不同,如果没有这个字段,系统就丢失了业务完整性。答辩时能主动讲出这个字段存在的必要性,远胜于背一段SSM原理。

5.2 为什么不建议在这张表上物理外键

有一个常见的坏味道:创建表时顺手加上FOREIGN KEY (student_no) REFERENCES student(student_no)。物理外键在毕业设计里能体现出“数据库设计很规范”,但实际运行中它带来两个麻烦:

第一,删除学生记录前必须先删除成绩记录,否则违反约束,这在批量操作时会打断程序员的直觉。第二,MySQL在存在物理外键时会对子表加锁,并发写入性能下降,虽然毕业设计这点并发量根本感知不到,但你会把注意力从逻辑正确性挪到约束不通过上。

常规做法是逻辑外键:表结构里保留student_no字段,但不声明FOREIGN KEY。在应用层通过事务保证一致性。这样改起来灵活,删除学生时还可以选择是否保留历史成绩。如果答辩老师问你为什么不用物理外键,回答“用应用层事务控制数据一致性,同时保持表的可扩展性”是比较得体的说法。

5.3 查询SQL里拉高印象分的三条写法

成绩查询系统里用得最多的三条SQL分别是:按学号和学期查成绩、按班级排名、统计某课程及格率。排名和统计是答辩时的亮点,但写法上坑很多。

按学号查成绩的SQL最简单,SELECT * FROM score_record WHERE student_no = ?不用加ORDER BY的话,返回结果顺序是不确定的,所以务必加上ORDER BY term DESC, course_code ASC,否则两次查询顺序不同,前端展示就会跳变。

按班级统计平均分时比SUBSTRING截断学号的写法更合理。如果学号编码规则是前4位代表班级,比如2023表示2023级的某个班,那么SQL可以写成:

SELECT SUBSTRING(student_no, 1, 7) AS class_no, COUNT(*) AS total_cnt, ROUND(AVG(score), 2) AS avg_score FROM score_record GROUP BY class_no;

这里的SUBSTRING参数长度要以实际学号规则为准。很多毕设翻车就是这里写死了截断位数,而学生的学号长度并不一致。先确认学号格式再写死位数,才是正确顺序。

课程及格率的SQL是答辩常问的一道题,“你系统里怎么统计及格率?”:

SELECT course_code, COUNT(*) AS total_cnt, SUM(CASE WHEN score >= 60 THEN 1 ELSE 0 END) AS pass_cnt, ROUND(SUM(CASE WHEN score >= 60 THEN 1 ELSE 0 END) / COUNT(*) * 100, 2) AS pass_rate FROM score_record GROUP BY course_code;

这段在GROUP BY上做条件聚合,而不是先查出来再用Java算。数据库中直接完成统计,既减少传输量又体现SQL功底,面试时也能直接拿出来当项目亮点讲。

5.4 联调查数对不上时的排查顺序

前后端联调阶段最常见的问题是:小程序查询的结果和数据库里手工查的结果不一致。排查时有顺序,别一上来就翻代码。先看数据库里用同样参数执行一遍SQL,确认数据本身没毛病。如果没问题,再看后端接口返回的JSON和数据库里的数据是否能对上,查查Mapper的resultMap有没有字段映射遗漏。

Mapper字段映射是独立于Java代码的坑区。如果一条数据的course_name在数据库里是“操作系统”,接口返回的JSON里却是null,基本可以断定是MyBatis的<resultMap>里字段名对不上。数据库的course_name和POJO的courseName之间必须有显式的映射配置,或开启mapUnderscoreToCamelCase。我在考场上见过不少人在这个环节反复重新部署Tomcat,白白浪费时间,实际上改一下mybatis-config.xml里的驼峰映射开关就能解决:

<settings> <setting name="mapUnderscoreToCamelCase" value="true"/> </settings>

这个开关打开后,下划线字段自动映射为驼峰属性,省掉一大堆<resultMap>手写标签。注意这个功能在MyBatis 3.0.8之后的版本默认关闭,必须在配置里显式打开。知道这一点的人,就能少折腾至少两个小时的联调时间。

6. 避坑与常见问题:从环境翻车到答辩漏洞

这一章把做这个项目过程中高频踩中的问题集中列出来,按“现象→原因→解决”的方式写。这些问题散落在环境搭建、数据一致性、前端渲染和答辩演示四个环节,每一条都是真实开发里遇到过、重构过、最后复盘过的。

6.1 JDK 17编译直接失败,报错package javax.annotation does not exist

现象:Maven编译时在@Resource注入处报错,找不到javax.annotation包。

原因:JDK 11之后javax.annotation被移除出标准JDK。SSM项目大多数教学习惯使用@Resource注解,而JDK 17不再包含这个包。

解决:二选一。要么把JDK降回8,重新配置IDEA的Project Structure;要么在pom.xml里加依赖:

<dependency> <groupId>javax.annotation</groupId> <artifactId>javax.annotation-api</artifactId> <version>1.3.2</version> </dependency>

降JDK是更稳的方式,因为Tomcat和Spring老版本对高版本JDK的支持并不完善。如果项目里还用了CGLIB代理,JDK 17的模块化限制会让代理生成直接抛IllegalAccessError。

6.2 小程序真机预览白屏,接口请求全部失败

现象:开发者工具里一切正常,扫码真机预览后页面空白,打开调试面板看到所有请求都在转圈。

原因:开发工具默认不校验合法域名,真机上没有这个豁免。若BASE_URL使用的是http://localhost或http://192.168.x.x,真机请求直接连不通。

解决:BASE_URL必须填电脑的局域网IP加端口,并且手机和电脑连同一个WiFi。在开发者工具中勾选“不校验合法域名,TLS版本以及HTTPS证书”之后,再用预览模式扫码测试。线上发布时必须在微信公众平台配置HTTPS业务域名,否则正式版永远调不通。很多人在这一关反复打包发布,绕了一圈,最后发现只是本地联调阶段没加豁免选项。

6.3 数据库中文乱码,插入Emoji报错

现象:后台录入学生姓名后,界面上显示为乱码,或者小程序端提交含Emoji的备注信息直接报Incorrect string value: '\xF0\x9F\x98\x80'。

原因:数据库、表连接、页面三处字符集不统一。MySQL的utf8最多只能存3字节,Emoji占4字节,必报错。之所以反复出现,是因为根本源头在jdbcUrl没加characterEncoding=utf8参数,或数据库本身建库时用了utf8。

解决:先把建库语句改成DEFAULT CHARSET utf8mb4,再把jdbcUrl补上characterEncoding=utf8,最后把数据库连接层配置里的连接参数和驱动jar包都换成对应版本。执行下面两句修改后重启Tomcat:

ALTER DATABASE score_db CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci; ALTER TABLE score_record CONVERT TO CHARACTER SET utf8mb4;

6.4 查询成绩返回成功但所有分数为空

现象:接口返回HTTP 200,code也是200,但data数组里的score字段值全是null。

原因:纯数据库存储的DECIMAL字段在MyBatis映射时,如果POJO属性是Double或BigDecimal,字段名匹配不上就映射不上。也就是常见的字段名映射问题,发生在数据库score和POJO字段scoreValue之间没有对应关系。

解决:要么给POJO字段改名,直接叫score,和数据库保持一致;要么在resultMap里显式配置<result column="score" property="scoreValue" />。如果项目里配置了mapUnderscoreToCamelCase=true,但属性名和列名不是下划线对齐关系,同样映射不上。所以最稳妥的做法是统一命名:Java属性名与数据库列名完全一致,或都采用驼峰对应关系。

6.5 答辩演示时Tomcat启动失败

现象:答辩当天现场演示,Tomcat启动报错Failed to start component [Connector[HTTP/1.1-8080]],前台演示被迫中断。

原因:演示机上的8080端口被占用,不出意外就是现场电脑上已经有一个Tomcat或其他Web服务在跑。

解决:提前准备两个应对方案。一是改端口并重启,命令是netstat -ano | findstr :8080,查PID并结束进程,或者在server.xml里把端口改成8081。但更稳妥的办法是:把数据库、后端、小程序三件套放在同一个局域网内,提前在演示机上完整跑一遍流程,确定所有端口都空闲。临场改端口需要重启Tomcat,期间网页白屏,很容易让答辩现场气氛冷场。熟练工程师一般会在答辩前一周把所有环境固定下来,不轻易换机器换端口。

7. 手动跑通全链路后,这套系统还能往哪改:三个值得一提的进阶方向

全链路跑通之后,这个项目就只是地基了。以这个SSM+小程序的骨架,你可以在几天之内把它的定制深度直接拔高一级,下面三个方向是我觉得性价比最高、也最能体现个人工作量所在的进阶动作。

7.1 把成绩导出做成后端解决,而不是前端截屏

现在的导出功能通常是前端截图或复制表格,这种做法在打印存档时非常痛苦。你可以在后端加一个导出接口,用Apache POI把查询结果写进Excel,小程序端点击“导出”后下载文件。

核心代码示意:

@GetMapping("/export") public void exportScore(@RequestParam String studentNo, HttpServletResponse response) throws IOException { List<ScoreVO> list = scoreService.queryByStudentNo(studentNo, null); XSSFWorkbook workbook = new XSSFWorkbook(); Sheet sheet = workbook.createSheet("成绩单"); Row header = sheet.createRow(0); header.createCell(0).setCellValue("课程名称"); header.createCell(1).setCellValue("成绩"); header.createCell(2).setCellValue("学分"); header.createCell(3).setCellValue("学期"); for (int i = 0; i < list.size(); i++) { Row row = sheet.createRow(i + 1); ScoreVO vo = list.get(i); row.createCell(0).setCellValue(vo.getCourseName()); row.createCell(1).setCellValue(vo.getScore().doubleValue()); row.createCell(2).setCellValue(vo.getCredit().doubleValue()); row.createCell(3).setCellValue(vo.getTerm()); } response.setContentType("application/vnd.openxmlformats-officedocument.spreadsheetml.sheet"); response.setHeader("Content-Disposition", "attachment; filename=score.xlsx"); workbook.write(response.getOutputStream()); workbook.close(); }

XSSFWorkbook对应Excel 2007以上的.xlsx格式,HSSFWorkbook才是老版的.xls。如果你对POI版本有顾虑,先确认pom.xml里引入的是4.1.2以上版本,4.x版本对.xlsx支持比3.x稳定得多。这里还有一个参数坑:response.setHeader里的文件名如果带中文,不同浏览器兼容性不同,最好用URLEncoder.encode("成绩单.xlsx", "UTF-8")再拼到filename=后面。

7.2 把密码存储升级成BCrypt

原始项目里大概率是明文存储。这里说的明文指的是表里直接存密码本身,而不是缩写或编码。一旦数据库泄露,所有账号都直接暴露。BCrypt是一个适合毕设场景的密码哈希算法,使用成本低,改造范围小,还自带盐值。

改造路径清晰:登录时把用户输入的密码用BCrypt.hashpw(rawPassword, BCrypt.gensalt())转成哈希再存库;查询时用BCrypt.checkpw(inputPassword, hashedPassword)验证,不再使用等值SQL匹配密码字段。这两个方法来自jBCrypt库,pom.xml引入依赖后,只需改动登录Service层和密码更新方法。

但注意一个兼容问题:如果数据库里已有的密码是明文,升级时会面临老用户无法登录的情况。常规做法是写一个一次性转换脚本,把所有明文密码批量替换成BCrypt哈希,再在登录方法里加判断:发现用户密码没有$2a$前缀时,先按明文校验,成功后自动更新为BCrypt哈希。这个兼容逻辑即是典型的实际工程处理方式,比干净利落地一刀切更值得借鉴。

7.3 成绩缓存:用注解给热门查询加速

答辩时老师很容易问,“这个系统如果有几千个学生同时在高峰期查成绩,性能扛得住吗?”一个直接在Service上加缓存注解的做法可以回答这个问题。

在Spring配置里开启缓存支持:

@EnableCaching @Configuration public class CacheConfig { @Bean public CacheManager cacheManager() { ConcurrentMapCacheManager cacheManager = new ConcurrentMapCacheManager("scoreCache"); return cacheManager; } }

然后在Service方法上标注:

@Cacheable(value = "scoreCache", key = "#studentNo + '_' + #term") public List<ScoreVO> queryByStudentNo(String studentNo, String term) { return scoreMapper.selectByStudentNo(studentNo, term); }

#term为null时要处理拼接,传null时内存里会生成两条不同缓存条目,不影响正确性但浪费空间。更细的写法是加一个判断,如果term为空,则key就只用学号拼一个特殊版本。这个例子拿出来,可以在答辩现场直接解释@Cacheable的value和key的作用,以及缓存什么情况下适合、什么情况下不适合。比单纯说“我加了一个Redis”要实在得多。

这个项目做到底,你会发现它最大的价值不是跑通了成绩查询的页面和接口,而是把SSM、小程序、MySQL之间的关系在真实业务里完整缝了一遍。我做了这些年,带过的应届生里,能把字段映射、版本冲突、模拟真机联调这几个环节讲清楚的人,面试现场的表现普遍比只会背框架八股文的人好一截。动手改代码、升级密码存储、加一层缓存,这三个动作做完,这套毕设就不只是“高分优秀项目”,而是你自己的项目了。希望帮到你。

本文还有配套的精品资源,点击获取

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

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

立即咨询