ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

从SSM到小程序:考研刷题毕设项目前后端联调与部署全解析

从SSM到小程序:考研刷题毕设项目前后端联调与部署全解析 简介这是一个基于微信小程序的考研学习平台毕业设计项目采用SSM框架面向计算机相关专业毕业生及课程设计学习者。系统分为小程序端与后台管理端覆盖注册审核、登录、普通与付费文件浏览下载、科目分类、在线评论、收藏及个人中心等完整功能后台还可对文件发布、审核、交易信息及用户进行管理。压缩包共1281个文件约108.59MB包含Vue前端页面、Java后端源码、微信小程序WXML/WXSS页面、SQL数据库脚本以及PNG图片、MP4演示视频、DOCX论文等并提供1-install.bat、2-run.bat、3-build.bat等部署脚本便于直接导入开发环境运行调试。配合演示视频可快速核验页面交互与功能流程节省调试时间完整源码与论文也有助于深入理解SSM框架和微信小程序的业务实现。当前已有93人学习适合作为毕业设计或课程设计的完整参考。1. 考研小程序 SSM毕业设计项目里最有复用价值的那一层一个一起考研小程序小程序ssm完整源码LW演示视频.zip这样的压缩包在网盘和毕设交易里出现频率极高。打开之后通常是三样东西一个wechat目录小程序前端、一个ssm目录Java 后端、一份 Word 格式的 LW论文。很多人拿到手第一反应是“能跑就行”但这恰恰浪费了它真正的价值——小程序端和 SSM 后端之间的联调链路才是这个项目里值得拆开看的部分。我把这类项目视为“单体毕设工程”的典型样本前端用微信小程序原生框架后端用 Spring SpringMVC MyBatis 三件套数据库是 MySQL部署形态是 Tomcat 打 war 包。它不新潮但结构完整适合用来理解一个前后端分离物理分离、逻辑半分离的系统是怎么从零拼起来的。本文要解决的问题很具体这套东西的每一层在干什么、参数怎么配、坑在哪里以及拿到手之后怎么从“能跑”改造成“像正经项目”。2. SSM 后端考研业务建模与三层架构落地2.1 为什么毕设生态偏爱 SSM 而不是 Spring BootSSM 意味着 Spring、SpringMVC、MyBatis 三个框架手动整合Spring Boot 则是把这三者打包成自动配置。毕设项目里大量使用 SSM不是因为性能优势而是因为它的分层足够清晰论文里好写“架构设计”这一章。Controller — Service — Mapper 三层是硬隔离的每层各干各的事这种结构对代码量不大、业务逻辑简单的系统反而比 Spring Boot 更直观。从学习价值角度看SSM 整合过程本身就是一道坎。web.xml里要手动挂ContextLoaderListener、DispatcherServletSpring 配置文件和 SpringMVC 配置文件要分开写MyBatis 的SqlSessionFactoryBean要手动声明。这套组合拳打下来你对 IoC 容器和 Servlet 生命周期的理解会比直接上手 Spring Boot 深得多。applicationContext.xml里的核心配置大致长这样context:component-scan base-packagecom.kaoyan.service / bean iddataSource classcom.alibaba.druid.pool.DruidDataSource property namedriverClassName valuecom.mysql.jdbc.Driver / property nameurl valuejdbc:mysql://localhost:3306/kaoyan_db?useUnicodetrueamp;characterEncodingutf8 / property nameusername valueroot / property namepassword value123456 / /bean bean idsqlSessionFactory classorg.mybatis.spring.SqlSessionFactoryBean property namedataSource refdataSource / property nametypeAliasesPackage valuecom.kaoyan.entity / property namemapperLocations valueclasspath:mapper/*.xml / /bean bean classorg.mybatis.spring.mapper.MapperScannerConfigurer property namebasePackage valuecom.kaoyan.dao / /bean这段配置的要点mapperLocations指向 XML 映射文件目录MapperScannerConfigurer负责把 DAO 接口直接注册成 Bean这样 Service 层Autowired一个接口就能用。注意url里的在 XML 里必须转义成amp;这是最常见的低级错误。2.2 考研业务的数据表设计题目、错题本、学习计划三张核心表考研小程序的业务通常集中在刷题、错题收集、学习打卡几个模块。和电商、社交类项目比它的表结构简单得多但这恰恰是好事——你能在一张表里看清楚字段设计的基本功。以题库模块为例题目表的设计直接决定后续刷题逻辑的复杂度。CREATE TABLE question ( id int(11) NOT NULL AUTO_INCREMENT, subject_id int(11) NOT NULL COMMENT 科目ID1政治 2英语 3数学 4专业课, type tinyint(4) DEFAULT 1 COMMENT 1单选 2多选 3判断, stem text NOT NULL COMMENT 题干, options text COMMENT 选项JSON数组存储, answer varchar(10) DEFAULT NULL COMMENT 正确答案多选用逗号分隔, analysis text COMMENT 答案解析, difficulty tinyint(4) DEFAULT 3 COMMENT 1-5难度等级, create_time datetime DEFAULT CURRENT_TIMESTAMP, PRIMARY KEY (id), KEY idx_subject (subject_id) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4;这里的options字段用 JSON 串存储是一种取舍。规范化的做法是单独建一张question_option表但考研题目大多是四选一或四选多选项数量固定用 JSON 存可以少写一次联表查询。小程序端拿到options字段后直接JSON.parse就能渲染后端也省掉组装 VO 的代码。代价是 SQL 层面没法针对选项内容做查询但对刷题场景来说完全够用。错题本表则是刷题类小程序的核心。一个用户可能对同一道题反复做错表设计上不能简单存一个question_id就完事CREATE TABLE wrong_book ( id int(11) NOT NULL AUTO_INCREMENT, user_id int(11) NOT NULL, question_id int(11) NOT NULL, wrong_count int(11) DEFAULT 1 COMMENT 累计错误次数, last_answer varchar(10) DEFAULT NULL COMMENT 最近一次错误答案, last_wrong_time datetime DEFAULT NULL COMMENT 最近做错时间, PRIMARY KEY (id), UNIQUE KEY uk_user_question (user_id, question_id) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4;唯一键uk_user_question保证了同一用户对同一题只有一条错题记录插入时用ON DUPLICATE KEY UPDATE让wrong_count自增而不是先查再改。这种写法在刷题、背单词类应用里非常通用。做这道题错了就更新做对了才从错题本删除语义干净。2.3 MyBatis 动态 SQL 的一个容易出错的写法刷题模块最常见的接口是“按科目和题型抽题”参数可能为空也可能只传其中一个。用动态 SQL 时要特别注意条件拼接不然容易查出全表或者查不到数据。我见过很多毕设项目在这里直接写死 SQL换个筛选条件就报错。select idselectQuestionByFilter resultTypecom.kaoyan.entity.Question SELECT * FROM question where if testsubjectId ! null AND subject_id #{subjectId} /if if testtype ! null AND type #{type} /if if testdifficulty ! null AND difficulty lt; #{difficulty} /if /where ORDER BY RAND() LIMIT #{limit} /selectwhere标签会自动去掉第一个多余的AND这是 MyBatis 给的最省心的处理方式。ORDER BY RAND()在数据量小的时候是“随机抽题”最简单的实现但要注意题目量一旦过万这条语句会让数据库全表扫描加临时排序接口响应时间会指数级上升。生产环境该改成WHERE id (SELECT FLOOR(RAND() * (SELECT MAX(id) FROM question))) LIMIT #{limit}这种主键随机的方式毕设阶段用RAND()图省事可以接受但你要知道它的上限在哪。Service 层调用这个 Mapper 时如果subjectId和type都是空where标签不会生成任何条件SQL 就变成了SELECT * FROM question ORDER BY RAND() LIMIT ?。这在测试时不容易暴露因为测试数据量小看不出问题等运营阶段数据多了才会发现这个接口是慢查询的元凶之一。3. 小程序前端页面结构、交互状态与后端接口对接3.1 小程序端目录结构与 tabBar 的配置逻辑微信小程序原生开发的项目结构有固定套路app.json是整个应用的骨架配置文件pages数组里第一个页面就是启动页。考研类小程序的 tabBar 通常是三个到四个入口首页、刷题、错题本、我的。这个设计符合工具类应用的导航习惯用户在刷题场景里切换的路径要短。{ pages: [ pages/index/index, pages/practice/practice, pages/wrong/wrong, pages/profile/profile ], window: { navigationBarTitleText: 一起考研, navigationBarBackgroundColor: #2b5cf5 }, tabBar: { color: #999999, selectedColor: #2b5cf5, list: [ { pagePath: pages/index/index, text: 首页 }, { pagePath: pages/practice/practice, text: 刷题 }, { pagePath: pages/wrong/wrong, text: 错题 }, { pagePath: pages/profile/profile, text: 我的 } ] } }tabBar的pagePath必须存在于pages数组中否则编译直接报错。图标字段iconPath和selectedIconPath在这个示例里省略了但实际项目里必须提供 PNG 格式的图片且大小限制在 40KB 以内。很多人第一次做 tabBar 会用 iconfont 字体图标那是 Web 思路小程序原生 tabBar 不支持只能放图片文件。3.2 wx.request 封装统一处理 baseURL、超时和错误码小程序端调后端接口不像浏览器那样可以直接跨域它有自己的域名校验机制。request 请求的 URL 必须以 HTTPS 开头而且域名必须在小程序管理后台配置到 request 合法域名列表里。开发阶段可以在开发者工具里勾选“不校验合法域名”真机预览就得关掉这个选项改用局域网 IP 加端口访问。我一般会在utils/request.js里做一层 Promise 封装避免每个页面重复写 loading 和错误弹窗。const BASE_URL http://localhost:8080/kaoyan; function request(path, method GET, data {}) { return new Promise((resolve, reject) { wx.request({ url: BASE_URL path, method: method, data: data, timeout: 10000, header: { Content-Type: application/json, token: wx.getStorageSync(token) || }, success: (res) { if (res.statusCode 200 res.data.code 0) { resolve(res.data.data); } else if (res.statusCode 401) { wx.redirectTo({ url: /pages/login/login }); reject(new Error(登录已过期)); } else { wx.showToast({ title: res.data.message || 请求失败, icon: none }); reject(new Error(res.data.message)); } }, fail: (err) { wx.showToast({ title: 网络异常, icon: none }); reject(err); } }); }); } module.exports { request, BASE_URL };这段封装里的设计决策后端统一返回{ code, message, data }结构前端只看code是否为 0 来判断业务成功与否而不是依赖 HTTP 状态码。401 状态码单独处理用于跳转登录页。token从wx.getStorageSync读取放到 header 里这套逻辑在 Session 和 JWT 两种认证方式下都能通用——后端用 Session 时读的是 cookie用 JWT 时读的是这个 header 字段。3.3 微信登录从 wx.login 到 OpenID 的完整闭环小程序登录和传统 Web 登录最大的区别在于小程序端拿不到用户的密码认证的基石是微信的code换openid机制。用户打开小程序时前端调wx.login()获取一个临时凭证code把这个code发给后端后端再用code去微信接口换openid和session_key。这个session_key绝不能返回给前端否则任何人都能解密用户信息。// AuthController.java PostMapping(/login) ResponseBody public Result login(RequestBody MapString, String params) { String code params.get(code); String url https://api.weixin.qq.com/sns/jscode2session?appid APPID secret SECRET js_code code grant_typeauthorization_code; String response HttpClientUtil.get(url); // 简写实际用 RestTemplate 或 HttpClient // 解析返回的 openid 和 session_key JSONObject json JSONObject.parseObject(response); String openid json.getString(openid); // 查数据库不存在则注册新用户 User user userService.findByOpenid(openid); if (user null) { user new User(); user.setOpenid(openid); user.setNickname(考研人 openid.substring(openid.length() - 6)); user.setCreateTime(new Date()); userService.register(user); } String token UUID.randomUUID().toString().replace(-, ); redis.setex(token: token, 7 * 24 * 3600, String.valueOf(user.getId())); return Result.success(token); }这段逻辑最关键的边界是openid是用户在微信生态里的唯一身份标识同一个用户在不同的小程序下openid不同。拿到openid后先查库查不到就是新用户自动注册一个空账号并生成默认昵称。这里的token用 UUID 生成后扔进 Redis 设 7 天过期属于服务端会话管理的简易版。毕设项目里如果不想引 Redis可以存在 Map 里但服务重启后所有登录态会消失体验不好。前端拿到token后存到wx.setStorageSync后续所有请求通过统一封装的 request 自动携带。这里注意一个时间差问题wx.login的成功回调里拿到的code有效期只有 5 分钟而且只能用一次所以每次进入需要登录态的页面时理论上都应该重新调wx.login换取新 code而不是缓存 code。3.4 刷题页的交互与本地缓存策略刷题页是小程序端的核心承载页面交互细节集中在答题反馈和题目切换上。常见的交互模式是“一题一页”上滑或点击下一题切换题目点击选项后立即显示对错并弹出解析。这种交互在原生小程序里实现不难但有几个状态同步问题容易出 bug。选项点击的处理逻辑我习惯用 data 里的selectedIndex和isAnswered两个字段来控制渲染状态// pages/practice/practice.js Page({ data: { questionList: [], currentIndex: 0, selectedIndex: -1, isAnswered: false }, onOptionTap(e) { if (this.data.isAnswered) return; const idx e.currentTarget.dataset.index; const question this.data.questionList[this.data.currentIndex]; this.setData({ selectedIndex: idx, isAnswered: true }); // 判断对错后提交结果到后端 const isCorrect this.checkAnswer(question.answer, idx); this.submitAnswer(question.id, isCorrect); // 如果答错写入错题本 if (!isCorrect) { this.addWrongBook(question.id, idx); } } });这段代码里的三行setData顺序有讲究。第一次setData只改selectedIndex和isAnswered让 UI 先高亮用户选中的选项。然后异步提交答案到后端提交失败也不影响前端展示。最后一步是把错题同步到错题本接口这个接口即使调用失败用户下次进入错题本时也可以下拉刷新重新拉取。做题体验流畅性的关键是不能让网络请求阻塞 UI 更新。还有一个容易忽略的细节题目列表加载。刷题请求返回 10 道题用户做到第 3 题就退出了下次进来希望从第 4 题继续。这个用后端游标实现最稳妥——提交答案时后端记录question_id和答题进度下次刷题接口带上lastQuestionId参数从断点之后继续出题。如果只在本地存currentIndex换设备或清缓存之后进度就丢了。4. 把 zip 包跑起来的完整步骤与 SSM 常见坑4.1 环境准备JDK、Tomcat、MySQL、微信开发者工具拿到一个 SSM 毕设项目第一步不是急着改代码而是把环境对齐。SSM 项目的经典组合是 JDK 1.8 Tomcat 8.5 MySQL 5.7。这个组合虽然老但兼容性最好任何毕设源码用这三件套跑起来的概率最高。用 JDK 11 以上版本跑老项目经常会遇到javax.xml.bind包缺失的问题因为 JAXB 在 JDK 11 里被移除了。启动顺序是先启动 MySQL再启动 Tomcat或 IDE 里的 Tomcat 插件最后打开微信开发者工具。前端小程序依赖后端接口后端依赖数据库这个依赖链不能倒。数据库导入用 Navicat 或者命令行都行重点检查 SQL 文件里的建库语句——很多毕设给的 SQL 文件不包含CREATE DATABASE只建表需要手动建库。mysql -u root -p -e CREATE DATABASE kaoyan_db DEFAULT CHARACTER SET utf8mb4; mysql -u root -p kaoyan_db kaoyan_db.sql导入时有个坑SQL 文件里如果写死了ENGINEInnoDB DEFAULT CHARSETutf8而你的库是utf8mb4表结构会以 SQL 文件里的为准。考研项目里题目解析字段经常包含emoji之外的生僻汉字utf8在 MySQL 里是utf8mb3的别名存不了四字节字符。建议导入前全局替换utf8为utf8mb4一劳永逸。4.2 配置文件改动清单数据库账号、端口、小程序域名项目跑不起来的多数原因都集中在配置文件上SSM 项目的配置文件分散在src/main/resources目录和web.xml里。拿到项目先检查这几处配置文件需要改的内容典型错误jdbc.properties数据库地址、账号、密码密码含特殊字符未转义spring-mvc.xml静态资源映射、注解扫描包路径扫描包名和实际代码包名不一致web.xmlDispatcherServlet 的 url-pattern拦截了.js、.css等静态资源pom.xmlJDK 编译版本、依赖版本Tomcat 8.5 配 servlet-api 版本过高小程序utils/request.jsBASE_URL 地址用了 localhost真机访问不到jdbc.properties里最常见的报错是Access denied for user rootlocalhost。这个报错有 50% 的概率是密码确实错了另外 50% 是 MySQL 8.0 以上版本的认证插件问题。MySQL 8.0 默认用caching_sha2_password而老版本的 MySQL Connector/J 驱动只支持mysql_native_password。解决方案有二把驱动升级到 8.0 以上或者执行ALTER USER rootlocalhost IDENTIFIED WITH mysql_native_password BY 你的密码;把认证插件改回旧版。毕设项目给的驱动版本通常很老碰到报错先看驱动不要先改密码。4.3 三个每跑必踩的坑静态资源拦截、跨域、JSON 乱码坑一DispatcherServlet 把静态资源拦截了。web.xml里常见的配置是url-pattern//url-pattern这样所有请求都会先走 DispatcherServlet导致*.js、*.css、图片等静态资源 404。SSM 项目必须在 SpringMVC 配置里加上mvc:resources mapping/static/** location/static//。如果确实用不到静态资源小程序不需要 JSP 和静态文件那这个问题可以跳过但绝大多数毕设项目后端带一个简单的后台管理页面所以这个配置大概率需要。坑二小程序请求后端接口时跨域。后端接口响应头里没有Access-Control-Allow-Origin在小程序开发者工具里会报url not in domain list或跨域错误。小程序端可以在开发者工具里勾选“不校验合法域名、web-view业务域名、TLS 版本以及 HTTPS 证书”这是开发期的标准操作。后端侧更稳妥的做法是加一个 CORS 过滤器public class CorsFilter implements Filter { Override public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException { HttpServletResponse resp (HttpServletResponse) response; resp.setHeader(Access-Control-Allow-Origin, *); resp.setHeader(Access-Control-Allow-Methods, GET, POST, PUT, DELETE, OPTIONS); resp.setHeader(Access-Control-Allow-Headers, Content-Type, token); resp.setHeader(Access-Control-Max-Age, 3600); chain.doFilter(request, response); } }Access-Control-Allow-Origin在生产环境下不应该设成*因为你无法控制谁在调用你的 API。毕设和演示阶段可以放宽但要清楚这是有安全代价的。坑三POST 请求中文乱码。小程序端提交Content-Type: application/json时如果后端 Controller 方法参数是RequestBody MapSpring 默认用 ISO-8859-1 解码中文会变成问号。在web.xml里配一个CharacterEncodingFilter并设forceEncodingtrue能解决表单提交的乱码但 JSON 请求的乱码根源在 Spring 的MappingJackson2HttpMessageConverter默认编码上。filter filter-nameencodingFilter/filter-name filter-classorg.springframework.web.filter.CharacterEncodingFilter/filter-class init-param param-nameencoding/param-name param-valueUTF-8/param-value /init-param init-param param-nameforceEncoding/param-name param-valuetrue/param-value /init-param /filter filter-mapping filter-nameencodingFilter/filter-name url-pattern/*/url-pattern /filter-mapping如果配了过滤器还是乱码检查 Tomcat 的server.xml里 Connector 是否加了URIEncodingUTF-8。GET 请求的参数走的是 URI 解析这个配置不写Tomcat 默认按 ISO-8859-1 解析 query string。5. 从毕设到可维护三个立竿见影的改造点5.1 把 UUID 换 Session 逻辑替换成 JWT上一节登录接口用 Redis 存 UUID 作为 token逻辑没问题但维护成本偏高。改成 JWT 后每次请求是无状态的后端不需要存储任何会话数据这对部署到多台服务器、后面要水平扩展的场景友好得多。引入jjwt依赖后登录成功后直接签发一个 token对这种单体毕设项目JWT 的量级完全够用public class JwtUtil { private static final String SECRET kaoyan-secret-key; private static final long EXPIRE 7 * 24 * 3600 * 1000L; public static String createToken(Long userId) { return Jwts.builder() .setSubject(String.valueOf(userId)) .setExpiration(new Date(System.currentTimeMillis() EXPIRE)) .signWith(SignatureAlgorithm.HS256, SECRET) .compact(); } public static Long parseToken(String token) { Claims claims Jwts.parser() .setSigningKey(SECRET) .parseClaimsJws(token) .getBody(); return Long.valueOf(claims.getSubject()); } }改造后需要在 SpringMVC 里加一个拦截器在preHandle里从 header 中取 token解析成功就把 userId 放到 request 属性中Controller 从请求上下文里直接取当前用户。注意密钥SECRET不能硬编码在代码里正式项目应该放到配置文件中。这个改造大约半小时能完成但代码的架构感完全不一样了。5.2 刷题接口的分页与缓存原来ORDER BY RAND() LIMIT #{limit}的写法在数据量上去后会拖垮数据库。改成按主键随机后还可以再加一层条件用户上次刷到的题号之后继续避免重复出题。一个更实用的小改造是增加一个answeredLog表记录答题轨迹刷题接口通过LEFT JOIN排除已经做过的题。对高频访问的题目详情接口和科目列表接口可以用Cacheable或在 Service 层加一个ConcurrentHashMap做本地缓存。毕设项目引 Redis 有点重本地缓存足以应对测试环境。缓存时要格外注意题目修改后必须同步更新缓存否则用户会看到旧题目。5.3 部署到服务器的实操验证本地跑通之后部署到 Linux 服务器是一个很好的检验。把项目用 Maven 打成 war 包放进 Tomcat 的webapps目录启动后访问http://服务器IP:8080/kaoyan/验证后端。小程序端要想在真机上访问BASE_URL必须改成 HTTPS 域名——这也是小程序上线前实际绕不开的边界云开发、服务器部署、域名备案、HTTPS 证书都要备齐。测试阶段可以用内网穿透工具的临时域名但上线必须走正规链路。验证部署成功的最快方法是用 curl 直打一个接口curl -X POST http://服务器IP:8080/kaoyan/auth/login \ -H Content-Type: application/json \ -d {code:test-code}返回 JSON 格式的错误信息比如code无效说明接口通路已经打通。接下来把小程序开发者工具里的BASE_URL改为服务器地址真机预览走一遍完整的登录、刷题、错题流程这个毕设项目就真正变成一个可以拿出来展示的完整作品了。本文还有配套的精品资源点击获取
返回列表