SSM迁移SpringBoot实战:小程序成绩查询后端全解析

发布时间:2026/9/16 5:59:10
SSM迁移SpringBoot实战:小程序成绩查询后端全解析 简介这份基于SpringBoot的微信小程序学生成绩查询后端项目专为毕业设计或课程设计打造适合正在学习SpringBoot、SSM整合及小程序接口开发的计算机专业学生。压缩包共1217个文件约20.17MB其中Java源码、JSP/WXML页面、JS/CSS、JSON配置、SQL脚本及JAR依赖等类型齐全前后端目录分层清晰便于按模块研读。项目覆盖SpringBoot自动配置、SSM框架整合、微信小程序API通信等关键知识点并提供完整源码与配置示例能帮助学习者快速理解从数据库表设计到接口响应的完整链路。已有99人学习/下载对需要实战案例支撑毕业设计或课程报告的同学是一份可直接运行、便于二次开发的参考资料。1. 从SSM迁移过来的SpringBoot成绩查询后端解决的不只是查询问题期中期末成绩发布那几天接口的压力是平时的几十倍。学生拿手机刷新小程序端倒还好真正扛不住的是后端查库和鉴权链路。这套毕业设计源码是用SSM的编程模型、SpringBoot的自动化装配写出来的小程序后端学生端通过wx.login拿到code传给后端后端再去微信服务端换openid签发token最后带着token查成绩列表。它的价值在于不是为展示某个新框架的玩具工程而是把SSM时代留下的技能平滑迁移到SpringBoot上——你打开代码能同时看到XML映射文件和自动配置类适合课程设计参考也适合后端新手入职前对照学一遍小程序登录的完整链路。2. 技术选型与项目结构SSM与SpringBoot的搭配逻辑2.1 SSM与SpringBoot不是二选一的关系SSM指Spring、SpringMVC、MyBatis三件套SpringBoot把这三个框架的装配过程自动化了但底层组件还是那三套。IoC容器继续管理Bean生命周期DispatcherServlet仍旧充当SpringMVC的前端控制器SqlSessionFactory负责MyBatis连接与SQL会话管理。区别在于你不再需要为它们手工拼接一堆applicationContext.xml和spring-mvc.xml。装配维度传统SSM手动配置SpringBoot自动配置Web容器外置Tomcat部署war包spring-boot-starter-web内嵌Tomcat数据源在properties里声明并注册Druidspring.datasource按约定自动装配MyBatis手工声明SqlSessionFactoryBeanmybatis-spring-boot-starter自动读取mapper-locations事务用 tx:annotation-driven 开启EnableTransactionManagement默认生效这个项目的价值在于两套东西都保留Controller、Service、Mapper分层的包结构跟SSM教程里完全一致而启动类、自动配置、starter依赖又是SpringBoot风格。课程设计答辩时老师问SSM问SpringBoot都能接得住。2.2 源码目录结构一览解压后src/main/java下是这个布局src/main/java/com/school/grades/ ├── config/ # JwtInterceptor、WebMvcConfig、WxProperties ├── controller/ # 对外REST接口 ├── service/ # 业务逻辑层 ├── mapper/ # MyBatis数据访问接口 ├── entity/ # 数据库实体 ├── dto/ # 请求参数对象 ├── vo/ # 返回视图对象 └── util/ # JwtUtil等工具类 src/main/resources/ ├── mapper/ # ScoreMapper.xml等映射文件 ├── application.yml └── sql/init.sql每一层只做一件事controller收参数service判断业务流程mapper只写SQL。前端参数变化不会影响SQL演进换数据库时只动mapper这就是分层拆开的收益。如果一上来就在controller里拼SQL后面加个学期筛选、加个平均分统计改起来会很痛苦。2.3 核心配置application.yml中值得抠的几行server: port: 8080 servlet: context-path: /api spring: datasource: url: jdbc:mysql://localhost:3306/student_grade?useUnicodetruecharacterEncodingutf8serverTimezoneAsia/Shanghai username: root password: 123456 driver-class-name: com.mysql.cj.jdbc.Driver jackson: date-format: yyyy-MM-dd HH:mm:ss time-zone: GMT8 mybatis: mapper-locations: classpath:mapper/*.xml type-aliases-package: com.school.grades.entity configuration: map-underscore-to-camel-case: true几个参数要重点说明。serverTimezoneAsia/Shanghai不能省MySQL 8对时区敏感不写会直接报连接错误。context-path: /api意味着小程序端baseUrl要拼成http://localhost:8080/api改这个路径时前端所有请求地址都要同步。type-aliases-package配了之后mapper XML里写resultTypeentity.Student即可不用每次写全限定类名。map-underscore-to-camel-casetrue让student_no自动映射成studentNo实体类用驼峰命名就能直接接收查询结果。新手常见问题是照抄网上老配置驱动还写com.mysql.jdbc.DriverMySQL 8下会报ClassNotFound。正确写法是com.mysql.cj.jdbc.Driver这个项目已经用对了。3. 成绩表设计与MyBatis持久层查询慢的原因往往在表结构3.1 学生、课程、成绩三张表的关联设计学生和课程是多对多关系所以必须拆出成绩关联表而不是在学生表里塞一个score字段。CREATE TABLE student ( id BIGINT PRIMARY KEY AUTO_INCREMENT, student_no VARCHAR(20) NOT NULL UNIQUE COMMENT 学号, name VARCHAR(32) NOT NULL, class_name VARCHAR(64) DEFAULT NULL ) ENGINEInnoDB DEFAULT CHARSETutf8mb4; CREATE TABLE course ( id BIGINT PRIMARY KEY AUTO_INCREMENT, course_name VARCHAR(64) NOT NULL, credit DECIMAL(3,1) DEFAULT 0.0 COMMENT 学分 ) ENGINEInnoDB DEFAULT CHARSETutf8mb4; CREATE TABLE score ( id BIGINT PRIMARY KEY AUTO_INCREMENT, student_no VARCHAR(20) NOT NULL, course_id BIGINT NOT NULL, score DECIMAL(5,2) DEFAULT NULL COMMENT 百分制成绩, semester VARCHAR(20) NOT NULL COMMENT 2024-2025-1, KEY idx_stu_semester (student_no, semester), CONSTRAINT fk_course FOREIGN KEY (course_id) REFERENCES course(id) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4;score表为什么直接用student_no而不是student.id关联成绩查询接口最常带的过滤条件是学号和学期学号对学生是公开且唯一的直接用student_no做关联查询可以减少一次子查询。联合索引idx_stu_semester的字段顺序也是按查询条件出现的频率排的学号在前学期在后这样最左前缀匹配才能生效。如果反过来建索引单独按学号查时就走不上。3.2 ScoreMapper动态SQL与预编译参数Mapper public interface ScoreMapper { ListScoreVO selectScores(Param(studentNo) String studentNo, Param(semester) String semester); }select idselectScores resultTypecom.school.grades.vo.ScoreVO SELECT c.course_name AS courseName, c.credit, s.score, s.semester FROM score s JOIN course c ON s.course_id c.id WHERE s.student_no #{studentNo} if testsemester ! null and semester ! AND s.semester #{semester} /if ORDER BY c.id DESC /select#{studentNo}是预编译占位符MyBatis会替换成?再传值比${}直接拼接字符串安全得多——后者会把用户输入拼进SQL存在注入风险。if标签让一个方法同时覆盖两个场景不传semester返回全部历史成绩传了只查某个学期。resultType指向ScoreVO是因为实体里没有courseName字段单独建一个VO对象承载联表结果比在entity里塞冗余字段干净。这个查询返回的是一个学生某一学期或全部学期的成绩列表。学生端单次展示的条数通常不超过几十条全量返回并不会造成网络负担所以不需要上来就套PageHelper分页只有当一个学生的成绩单累积几学期超过200条时才考虑分页否则小程序端翻页体验反而割裂。3.3 平均分与总学分放在哪一层计算成绩查出来后还要算平均分和总学分。常见做法是放在Service层循环计算而不是用SQL的AVG()和SUM()一把梭public GradeSummary buildSummary(ListScoreVO scores) { BigDecimal scoreSum BigDecimal.ZERO; BigDecimal creditSum BigDecimal.ZERO; int validCount 0; for (ScoreVO item : scores) { if (item.getScore() ! null) { scoreSum scoreSum.add(item.getScore()); validCount; } creditSum creditSum.add(item.getCredit() null ? BigDecimal.ZERO : item.getCredit()); } GradeSummary summary new GradeSummary(); summary.setAverage(validCount 0 ? null : scoreSum .divide(BigDecimal.valueOf(validCount), 2, RoundingMode.HALF_UP)); summary.setTotalCredit(creditSum); return summary; }这里有两个细节。第一缺考记录的成绩是null统计平均分时不能把它当成0算进去所以用validCount记录有效成绩数。第二用BigDecimal而不是double做累加避免1.1加2.2得到3.3000000000000003这类浮点误差除法时指定RoundingMode.HALF_UP保留两位和数据库字段的DECIMAL(5,2)精度对齐。SQL里直接写AVG也能算但学分合计的口径、空值过滤逻辑全混在SQL里后面要调整规则就得改语句再重新发布放Service层可读性和可维护性都更好。4. REST接口层与登录鉴权学生怎么安全地拿到自己的成绩4.1 统一响应体Result的设计小程序前后端联调最怕每个接口返回结构都不一样。这个项目用统一的Result对象包住所有响应登录、查询、异常都走同一个JSON格式。public class ResultT { private Integer code; private String msg; private T data; public static T ResultT success(T data) { ResultT r new Result(); r.code 200; r.msg success; r.data data; return r; } public static T ResultT error(int code, String msg) { ResultT r new Result(); r.code code; r.msg msg; return r; } // getter / setter 省略 }业务码约定为200成功、400参数错误、401未登录或token失效、403无权限、500服务端异常。小程序端拿到code再决定是弹提示、跳登录页还是直接展示数据不需要每个页面重复写错误处理逻辑。4.2 登录换token的完整链路微信小程序没有传统用户名密码登录而是靠wx.login返回的临时code交给后端调微信服务端接口换身份信息。RestController RequestMapping(/auth) public class AuthController { Resource private WxAuthService wxAuthService; Resource private JwtUtil jwtUtil; PostMapping(/login) public ResultString login(RequestBody LoginDTO dto) { WxSessionInfo session wxAuthService.code2Session(dto.getCode()); if (session null || session.getOpenid() null) { return Result.error(401, code无效或已过期); } String studentNo wxAuthService.findStudentNoByOpenId(session.getOpenid()); if (studentNo null) { return Result.error(403, 该微信未绑定学生账号); } String token jwtUtil.generateToken(session.getOpenid(), studentNo); return Result.success(token); } }流程拆开是四步小程序端调wx.login得到临时codePOST给后端后端带appid、secret、code请求微信服务端换回openid和session_keyopenid去student表反查绑定关系找到学生编号最后用JWT签发token返回前端。dto.getCode()是一次性的有效期5分钟且只能用一次同一个code拿第二次去换会报40029错误。有个安全底线必须守住微信小程序的AppSecret只能保存在后端绝不能写进小程序代码或打包资源里。任何人从小程序包里解出AppSecret就能伪造openid冒充任意学生身份。JWT里只放openid和学生编号不放姓名、成绩这类个人数据因为JWT体积会随内容膨胀而成绩信息每次查询都是实时从库里取的。4.3 拦截器校验与身份一致性检查token签发出来之后成绩查询接口不能只校验token有效还要校验token里的身份跟查询参数里的学号一致否则学生A可以改studentNo参数去查学生B的成绩。Component public class AuthInterceptor implements HandlerInterceptor { Override public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception { if (OPTIONS.equals(request.getMethod())) { return true; } String token request.getHeader(Authorization); Claims claims JwtUtil.parse(token); if (claims null) { response.setStatus(401); return false; } request.setAttribute(studentNo, claims.get(studentNo)); return true; } }Configuration public class WebMvcConfig implements WebMvcConfigurer { Resource private AuthInterceptor authInterceptor; Override public void addInterceptors(InterceptorRegistry registry) { registry.addInterceptor(authInterceptor) .addPathPatterns(/**) .excludePathPatterns(/auth/login, /error); } }OPTIONS请求要直接放行否则小程序端调试、浏览器跨域调用时发的预检请求会被拦截器拦掉前端拿不到任何响应。拦截器解析token后把studentNo放进request attribute业务接口再从attribute里取当前登录人和参数里的studentNo比对GetMapping(/list) public ResultListScoreVO list(RequestParam String studentNo, RequestParam(required false) String semester, HttpServletRequest request) { String tokenStudentNo (String) request.getAttribute(studentNo); if (!tokenStudentNo.equals(studentNo)) { return Result.error(403, 只能查询本人成绩); } return Result.success(scoreService.query(studentNo, semester)); }数据越权在小程序后端是高发漏洞很多课程设计项目只做了登录校验没做数据归属校验接口能查出所有人的成绩。把身份一致性校验放在业务入口处比在每个Service方法里重复判断更省事。4.4 成绩查询接口的参数设计studentNo必填semester可选。不传semester时返回全部学期历史成绩传了例如2024-2025-1就只返回该学期。参数用RequestParam接收查询字符串POST方式则用RequestBody接收JSON对象两种方式在小程序端的wx.request里对应data字段的位置不同不要混用。5. 微信小程序端联调从wx.login到成绩卡片渲染5.1 登录态存储与请求封装小程序端登录后把token存进storage所有业务请求在header里携带它。这里用一个统一的request方法封装业务页面只管调用// utils/request.js const BASE_URL http://localhost:8080/api function request(path, method GET, data {}) { return new Promise((resolve, reject) { wx.request({ url: BASE_URL path, method, data, header: { Content-Type: application/json, Authorization: wx.getStorageSync(token) || }, success(res) { if (res.statusCode 401) { wx.removeStorageSync(token) wx.navigateTo({ url: /pages/index/index }) reject(new Error(未登录)) return } if (res.data.code ! 200) { wx.showToast({ title: res.data.msg, icon: none }) reject(new Error(res.data.msg)) return } resolve(res.data.data) }, fail: reject }) }) } module.exports { request, BASE_URL }逻辑说明statusCode 401时说明token失效直接清掉本地缓存并跳回登录页业务码非200时用wx.showToast弹出后端返回的msg调用方不需要重复写错误分支。wx.request的success回调里拿到的res是HTTP响应对象res.data才是后端返回的Result结构。5.2 登录与查询成绩的组装代码// pages/score/score.js const { request } require(../../utils/request) Page({ data: { scores: [], semester: , average: 0, loading: false }, onLoad() { const token wx.getStorageSync(token) if (token) { this.fetchScores() } else { this.login() } }, login() { wx.login({ success: (res) { request(/auth/login, POST, { code: res.code }) .then((token) { wx.setStorageSync(token, token) wx.setStorageSync(studentNo, token.split(.)[1]) // 实际应按后端返回处理 this.fetchScores() }) } }) }, fetchScores() { this.setData({ loading: true }) request(/score/list?studentNo${wx.getStorageSync(studentNo)}) .then((data) { this.setData({ scores: data.scores, average: data.summary.average }) }) .finally(() this.setData({ loading: false })) } })关键点在onLoad里先判断token是否存在存在就直接查成绩不存在再走wx.login。如果每个页面进来都重新调wx.login换一次code微信服务端会频繁响应而且code本身是一次性的重复调用会拿到无效code。实际项目里studentNo应是后端登录接口返回给前端的或由后端再提供绑定关系查询接口这里为演示简化为从storage取值生产项目要以后端返回为准。5.3 联调时绕不开的几个坑本地开发时小程序默认只允许访问已在公众平台配置的HTTPS域名直接请求http://localhost:8080会报url not in domain list。解决方法是微信开发者工具的详情-本地设置里勾选“不校验合法域名、web-view业务域名、TLS版本及HTTPS证书”。上线前把BASE_URL换成正式HTTPS域名并在公众平台后台的request合法域名里加入该域名。第二个坑是wx.request不携带浏览器Cookie所以这个项目用Authorization头传token而不是依赖Session。第三个坑是后端返回的JSON里如果包含LocalDateTime字段Jackson默认序列化格式是ISO字符串小程序端解析时要注意项目里已经在application.yml里统一配了yyyy-MM-dd HH:mm:ss格式实体字段用Date或LocalDateTime都能正常解析。6. 部署验证与常见排错跑通后再说的三条经验6.1 数据库初始化与启动验证拿到源码先做三件事建库、改配置、启动。mysql -uroot -p src/main/resources/sql/init.sql mvn spring-boot:run验证登录接口curl -X POST http://localhost:8080/api/auth/login \ -H Content-Type: application/json \ -d {code:test_code_001}返回{code:200,msg:success,data:jwt}说明登录链路通。再拿token调查询接口curl -X GET http://localhost:8080/api/score/list?studentNo2024001 \ -H Authorization: jwt6.2 高频问题排查方向报错或现象优先级最高的检查处理方式数据库连接超时时区、驱动类JDBC URL补serverTimezoneAsia/Shanghai驱动用com.mysql.cj.jdbc.Driver端口被占用服务重复启动改server.port或任务管理器结束占用进程Invalid bound statementmapper路径不对确认namespace等于接口全限定名mapper-locations匹配code2session返回40029code过期重新调用wx.login生成新codecurl查不到数据表里没对应学号SELECT * FROM score WHERE student_no...核对6.3 一个能快速定位SQL瓶颈的验证技巧课程设计答辩时最常被问的是性能问题。可以在Service层加一段耗时统计long start System.currentTimeMillis(); ListScoreVO scores scoreMapper.selectScores(studentNo, semester); long cost System.currentTimeMillis() - start; log.info(查询学号{}的{}成绩耗时{}ms, studentNo, semester, cost);如果耗时超过200ms用EXPLAIN看索引EXPLAIN SELECT * FROM score WHERE student_no2024001 AND semester2024-2025-1;看到typeref且keyidx_stu_semester说明走了联合索引如果看到typeALL就说明全表扫描需要检查idx_stu_semester是否存在或者查询条件里是否在student_no前加了函数导致索引失效。这个检查过程能直接把问题定位到SQL层而不是在Controller和Service里瞎翻。本文还有配套的精品资源点击获取

关于本文作者

来自尧图内容编辑团队

尧图内容编辑团队 内容团队

尧图内容编辑团队

本文由尧图网络内容编辑团队执笔。团队由资深项目经理、前端工程师与设计师组成,所有内容均来自亲手交付的真实项目,先讲清问题、再给出可落地的解法。尧图深耕北京网站建设十年,服务过京华建材集团、智造科技等各行业客户,把一线经验沉淀为可复用的行业观察。

  • 十年建站经验,覆盖建材、制造、服务、文创等
  • 项目经理把关选题与事实准确性
  • 工程师与设计师联合撰写专业细节
  • 统一编辑规范,保证文风与排版一致
  • 每月复盘转化数据,迭代选题方向

延伸阅读

相关资讯与近期热门内容

深度阅读推荐

建站决策前值得细读的三篇

网站改版的5个关键决策
2024-08-12

网站改版的5个关键决策

什么时候该改版、改到什么程度、如何避免流量掉光,京华建材集团改版复盘给出答案。

获取专属建站方案

看完文章,把您的行业与预算告诉我们,免费获取一份量身定制的官网建设方案与报价。

立即免费咨询