1. 这个项目到底在做什么,以及为什么值得参考
1.1 先拆一下这个标题,不要看到一堆字母就慌
springboot基于微信小程序的四六级小助手系统x82rj4l9(源码+LW+部署讲解)——这个标题其实已经把项目最核心的三件事说清楚了:后端是Spring Boot,前端是微信小程序,业务是四六级小助手。后面的“x82rj4l9”是项目唯一标识,跟功能本身没关系,很多同学第一次看到这种随机串会懵,担心是不是什么特殊版本,其实不用管,它只是生成项目时用来区分文件的编号。
“源码”很好理解,就是完整的工程代码,分后端和小程序两个目录。“LW”在毕设场景里指的是配套文档,也就是项目说明书、需求分析、数据库设计、接口说明、答辩稿这一整套东西。对要拿这个项目去做毕设或者课设的人来说,LW和源码同样重要,因为答辩的时候老师先看文档,再问代码,文档写得不清楚,代码再完整也很难拿高分。“部署讲解”也很关键,光能跑起来不算完,你还得能说清楚项目怎么从本地环境搬到云服务器上、小程序端怎么配置合法域名、后端怎么打包启动。
这套技术组合在近几年的学生项目里非常常见,Spring Boot负责提供接口和业务逻辑,微信小程序负责界面展示和用户交互,两个端通过HTTP接口通信。小程序天然免安装、扫码即用,在校园场景下传播成本极低,四六级又正好是大学生群体里的刚需话题,所以选题方向本身没有问题。下面我结合自己实际带过的类似项目,把这套东西从头到尾拆一遍。
1.2 一个标准的四六级小助手,核心功能应该有哪些
很多人拿到需求后第一反应是“四六级小助手?是不是就一个背单词的App”,实际上完整的需求拆下来,至少包含这几块:单词学习、真题练习、错题整理、考试倒计时、个人中心,如果还要考虑运营方,就再加一个后台管理模块。
落实到页面上,小程序端大致是五个Tab:首页、背单词、练习、错题本、我的。首页展示今日学习进度、考试日期倒计时、每日打卡状态,顺便放一些四六级考试相关的小贴士;背单词页是核心学习场景,一次给一批单词,支持卡片式浏览、标记“认识/不认识”、记录熟练度;练习页按历年真题或者模拟题分类,多半是选择题,做完提交之后自动判分、展示答案解析;错题本把做错的题按来源和知识点收纳起来,方便后面集中复习;我的页面展示用户的学习统计、打卡天数、单词总量,有管理员角色的还可以从侧边栏进入管理入口,维护词库和真题数据。
后端这边,Spring Boot项目里对应的模块也很清晰:用户模块处理微信登录、token签发、个人信息;单词模块负责词库的增删改查和按计划抽取单词;练习模块管试卷、题目、答题记录和判分;错题模块管错题上报和列表查询;管理模块管公告发布、数据统计。整体下来就是一个小型前后端分离系统,麻雀虽小五脏俱全。
1.3 为什么偏偏选Spring Boot加微信小程序这套组合
单纯从做毕设的角度讲,这个组合胜在三个点。
第一,微信小程序把前端门槛压得非常低。小程序用的WXML和WXSS语法跟HTML和CSS很接近,JavaScript部分也是标准的ES语法,你哪怕之前只写过一点前端,花一周时间就能把页面框架跑起来。不需要自己买域名搭H5,不需要折腾跨浏览器兼容,微信开发者工具里直接预览效果,真机和模拟器同步调试,比传统Web开发省心太多。
第二,Spring Boot把后端门槛也压下来了。内嵌Tomcat意味着你不需要单独装Web容器,打成jar包直接java -jar就能跑;自动装配省掉了SSM时代那一堆XML配置;Spring Data JPA或者MyBatis的集成都有现成的starter,写CRUD基本是模板化操作。对一个没有太多企业级开发经验的学生来说,学习曲线比SSM平滑很多,而且后期部署也不需要面对复杂的容器环境。
第三,前后端分离的结构天然适合毕设答辩。你可以把一次完整的用户请求讲清楚:小程序发起wx.request,后端Controller接收,Service处理业务,Mapper操作数据库,数据再一层层返回去。整个调用链路清晰,老师问问题也有抓手,不至于你讲完PPT之后无话可说。
顺带提一个容易犯的错:有人觉得用小程序做前端就必须配一个“小程序后端”,其实微信小程序没有自己的服务端,所有的数据交互都是走你自建的HTTP接口,微信官方只提供登录态校验(code2session)和文件存储之类的辅助能力。换句话说,小程序和后端之间就是普通的前后端分离关系,这一点先搞清楚,后面设计接口的时候才不会走偏。
2. 后端设计与核心实现:先搞定数据库,再谈业务逻辑
2.1 数据库到底要建几张表,字段怎么设计
四六级小助手这种项目,数据库不需要搞得很花哨,核心表控制在七到八张就够了,太多了自己维护累,太少了功能撑不起来。我按实际项目整理了一张表清单:
| 表名 | 职责 | 关键字段 | 关联关系 |
|---|---|---|---|
| user | 学生用户 | openid、nickname、avatar、study_days | 全局主表 |
| word | 单词库 | word、phonetic、translation、level | level区分四级/六级 |
| user_word | 用户单词学习记录 | user_id、word_id、proficiency、review_count | 多对多中间表 |
| paper | 真题/模拟卷 | title、level、exam_date、duration | 练习模块主表 |
| question | 题目 | paper_id、content、option_a/b/c/d、answer、analysis | 关联试卷 |
| answer_record | 答题记录 | user_id、question_id、user_answer、is_correct | 判分和统计 |
| wrong_book | 错题本 | user_id、question_id、wrong_count | 去重记录 |
| notice | 公告 | title、content、create_time | 首页信息流 |
用户表和单词表之间用user_word中间表连接,这样每个学生看到的“待学单词”“已掌握单词”“复习单词”都是基于自己独立的学习记录,而不是所有人共享一份进度。这里有两个设计细节值得注意:
第一个是单词表里加level字段比较好,四级词库和六级词库不要分成两张表。分开建表看起来直观,但代码里所有涉及单词的查询都要写两套逻辑,后续如果加一个“高频核心词”分类又得再建表,非常痛苦。用level字段区分,一行SQL就能按难度过滤,扩展性也更好。
第二个是proficiency(熟练度)字段不要设计得太复杂。网上有些文章上来就推荐艾宾浩斯记忆曲线、间隔重复算法,听起来高大上,但学生项目的周期根本撑不起那么复杂的设计。我实际用的是一个简化模型:0代表未学,1代表学习中,2代表已掌握,3代表忘记。用户标记“认识”就把值往上涨一级,标记“不认识”就降到3,系统定时把熟练度为3的单词重新加入今日计划。简单粗暴,但完全够用,而且答辩时能讲清楚。
DDL里有一个容易被坑的点,MySQL的版本不同,默认字符集可能不是utf8mb4。小程序端用户昵称很可能带emoji,比如用😀这种表情做昵称头像,如果表是utf8字符集,insert的时候直接报错。建库的时候务必写成:
CREATE DATABASE cet_assistant DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;创建单词表的核心字段大致是这样:
CREATE TABLE word ( id BIGINT PRIMARY KEY AUTO_INCREMENT, word VARCHAR(64) NOT NULL COMMENT '单词本身', phonetic VARCHAR(128) COMMENT '音标', translation VARCHAR(512) NOT NULL COMMENT '中文释义', example_sentence TEXT COMMENT '例句', level TINYINT NOT NULL DEFAULT 1 COMMENT '1-四级 2-六级', create_time DATETIME DEFAULT CURRENT_TIMESTAMP, UNIQUE KEY uk_word_level (word, level) ) ENGINE=InnoDB COMMENT='四六级词库';这里的唯一索引uk_word_level建议加上,不然词库导入的时候重复执行脚本会插入一堆重复单词。后面你在管理后台做词库导入,直接INSERT IGNORE就能跳过已有的记录,省去手动去重。
2.2 后端接口怎么规划,统一返回体是必须的
前后端分离的项目,接口设计直接决定联调效率。我习惯把所有接口按业务模块拆分,统一以/api开头,大致这样分:
/api/auth/login微信登录/api/user/info获取个人信息/api/word/today获取今日学习计划/api/word/submit提交单词学习结果/api/paper/list获取试卷列表/api/paper/detail获取试卷详情/api/paper/submit提交试卷答案/api/wrong/list获取错题本/api/wrong/remove移除错题
所有接口的返回值必须统一格式,这一点看着简单,但很多人一开始图方便,某些接口返回成功对象,某些接口只返回布尔值,到小程序端写res.data判断逻辑的时候就会非常混乱。我用的统一返回体是这样:
public class Result<T> { private Integer code; private String msg; private T data; public static <T> Result<T> success(T data) { Result<T> r = new Result<>(); r.setCode(200); r.setMsg("ok"); r.setData(data); return r; } public static <T> Result<T> error(Integer code, String msg) { Result<T> r = new Result<>(); r.setCode(code); r.setMsg(msg); return r; } }小程序端拿到res.data之后,只要判断code === 200就进成功分支,否则直接弹出msg提示。后面无论是接微信登录还是接单词接口,这套模式一套到底,前端代码的冗余量会小很多。
业务模块之间要注意一个问题:单词、试卷、错题这些接口都依赖登录态,不可能裸奔。所以Controller层不要每个方法都手动写“从token里取userId”,这种重复代码写多了既啰嗦又容易漏。正确做法是自定义一个拦截器,在请求进入Controller之前统一完成鉴权,把userId塞到request.getAttribute()里,Controller直接取。
2.3 微信登录的完整链路:核心是openid,不是用户密码
很多刚接触小程序开发的同学被登录卡住,因为小程序不像传统网站有账号密码体系,它的登录建立在微信的code换openid机制上。完整的链路分成四步:
第一步,小程序端调用wx.login(),拿到一个一次性凭证code,这个code有效期只有五分钟,用完即废。第二步,小程序把code通过POST /api/auth/login发送给后端。第三步,后端拿这个code加上小程序的appid和appSecret,向后端接口jscode2session发起请求,换来openid和session_key。第四步,后端用openid去用户表查记录,查到就直接登录,查不到就自动注册一个新用户,然后用JWT给前端签发一个token。
这段逻辑里有一个很多人会踩的坑:jscode2session这个请求必须由后端发起,不能在小程序端直接调。因为调这个接口需要appSecret,它一旦暴露在小程序代码里,任何人都能拿到你的密钥去伪造用户身份。小程序代码本质上是下发到用户手机里的,没有任何保密性可言,所以appSecret只能存在自己的服务器上。
后端拿到openid后,不要直接把openid当token返回给前端。openid相当于你在这个小程序里的身份证号,它不应该暴露给客户端,所有涉及身份的地方都应该用JWT。JWT的生成用现成的库,比如jjwt,核心代码很短:
String token = Jwts.builder() .setSubject(userId.toString()) .setExpiration(new Date(System.currentTimeMillis() + 7 * 24 * 60 * 60 * 1000)) .signWith(SignatureAlgorithm.HS256, secretKey) .compact();有效期我一般设七天,小程序端超过七天token失效就重新走一次wx.login流程,对用户来说无感刷新,体验不会受到影响。
这里再补充一个细节:微信登录和传统账号体系并不是互斥的。你可以在用户表里既保留openid,又允许管理员手动维护一个账号密码,用于模拟登录。这样你在本地调试、或者在答辩现场演示的时候,不需要依赖真机微信环境,直接用测试账号就能跑通全流程。我当时的做法是在登录接口加一个loginType参数,wx走微信登录,admin走密码登录,两个入口到最后都落到同一个用户体系上,演示起来非常方便。
2.4 今日学习计划和单词抽取逻辑怎么实现
单词学习模块是整个系统的灵魂,但实现逻辑其实不复杂。所谓“今日计划”,核心是从单词库里挑出一批用户没学过的单词,再加上一批需要复习的单词,合在一起形成当天的学习队列。
我用的是先查复习再补新词的策略:先从user_word表里找proficiency = 3(忘记状态)的记录,同时满足next_review_date <= 今天,把这一批单词拉出来;数量不足目标数量时,再从未学的单词里随机抽。每批目标单词数量我设为20个,这个数字参考了市面主流背单词软件的新词档位,再少没效果,再多背不完。
随机抽取用MySQL的ORDER BY RAND()就能实现,数据量小的时候性能完全没问题:
SELECT w.* FROM word w WHERE w.level = #{level} AND w.id NOT IN (SELECT word_id FROM user_word WHERE user_id = #{userId}) ORDER BY RAND() LIMIT #{count}有同学担心ORDER BY RAND()在数据量大的时候会慢,实际是四六级词库加起来也就一万多词条,单用户查询带上user_word过滤之后,扫描量根本不大。没必要为了这个上复杂的算法,毕设项目讲究的是“能跑、能讲、能扩展”,不是说一定要用多高深的技术。
当用户点击“认识”或“不认识”之后,后端更新user_word表:认识就把熟练度加一,不认识就把熟练度置为3。顺手再把last_review_date更新成今天,这个字段既能用来画学习热力图,也能在后期做“连续打卡”统计,数据价值很高。
3. 小程序端页面与交互:不要只顾着写页面,请求层先搞好
3.1 小程序整体页面结构和TabBar配置
小程序的页面结构其实很像一个简化版的前端路由系统,每个页面由.wxml、.wxss、.js、.json四个文件组成,在pages目录下注册。我当时的页面清单是下面这些:
| 页面路径 | 对应功能 | 说明 |
|---|---|---|
| pages/home/index | 首页 | 学习概览、倒计时、公告 |
| pages/word/index | 背单词 | 卡片翻页交互 |
| pages/exam/index | 真题练习 | 试卷列表 |
| pages/exam/detail | 做题页 | 答题交互 |
| pages/wrong/index | 错题本 | 错题列表与移除 |
| pages/mine/index | 个人中心 | 用户信息与统计 |
| pages/login/index | 登录页 | 微信一键登录 |
TabBar在app.json里配置,我选了首页、背单词、练习、错题本、我的五个页面。这里有一个设计取舍问题:做题页(exam/detail)不要在TabBar里,因为它是二级页面,用户点进试卷后应该进入一个专注的答题界面,再挂TabBar会很干扰。非TabBar页面通过wx.navigateTo跳转,返回的时候也能保留上一页的状态。
页面的视觉风格,我建议整体走简洁路线。四六级小助手的目标用户是在校大学生,他们每天打开的学习类小程序可能不止一个,界面太花哨反而显得廉价。主色调我用了藏蓝色加一点橙色点缀,藏蓝色跟“学习、可靠”的气质相配,橙色用于打卡按钮和“掌握”按键这类需要突出动作的元素,整体看起来既有识别度又不会过于活泼。
3.2 微信登录和用户信息在小程序端怎么落地
小程序端登录的第一步永远是wx.login获取code,然后调用后端接口换token。我把它封装在utils/auth.js里:
function wxLogin() { return new Promise((resolve, reject) => { wx.login({ success: (res) => { if (res.code) { request.post('/auth/login', { code: res.code }) .then((data) => { wx.setStorageSync('token', data.token) wx.setStorageSync('userInfo', data.userInfo) resolve(data) }) .catch(reject) } else { reject(new Error('登录失败')) } } }) }) }登录成功之后,token必须持久化存储在wx.setStorageSync里,而不是只放在内存变量中。小程序随时可能因为切后台被系统回收,内存数据丢得一干二净,只有Storage里的数据还在。
小程序里获取用户昵称和头像有个历史变数:微信官方从某段时间开始,wx.getUserProfile返回的头像变成灰色默认图,昵称变成“微信用户”,这是微信隐私政策调整的结果。不要在登录流程里卡这个字段,更不要因为拿不到昵称就让用户卡在登录页出不来。正确做法是先进入系统,用户可以在“个人中心”手动编辑昵称上传头像,数据通过普通接口存到后端,不依赖微信的授权开放能力。
3.3 请求封装:统一搞定Token、错误提示、加载状态
小程序端请求封装是决定开发效率的关键一环,很多人的代码里wx.request铺天盖地,每个请求都重复设置header、处理错误、弹toast,一旦后端改了返回结构,前端要改的地方多到崩溃。我在项目里用了一个统一的request方法,核心逻辑是:
const request = (url, method, data) => { const token = wx.getStorageSync('token') return new Promise((resolve, reject) => { wx.request({ url: `${BASE_URL}${url}`, method: method || 'GET', data: data || {}, header: { 'Content-Type': 'application/json', 'Authorization': token ? `Bearer ${token}` : '' }, success: (res) => { if (res.data.code === 200) { resolve(res.data.data) } else if (res.data.code === 401) { // token过期,重新登录 wxLogin().then(() => { wx.showToast({ title: '登录已过期,请重试', icon: 'none' }) }) } else { wx.showToast({ title: res.data.msg || '请求失败', icon: 'none' }) reject(res.data) } }, fail: (err) => { wx.showToast({ title: '网络异常,请检查', icon: 'none' }) reject(err) } }) }) }这个封装有几点细节很值得讲。第一是统一处理401,token过期不应该让用户手动作任何操作,自动重新走登录流程,用户体验会自然很多。第二是resolve的时候直接返回data字段,页面代码拿到的就是业务数据本身,不需要每个页面再res.data.data这样解包,代码干净很多。第三是失败情况统一弹toast,页面里不需要再各自处理错误提示。
还有一个坑必须提醒:小程序请求的url必须填完整路径,很多人把BASE_URL写成https://xxx.com就完了,结果路径拼接出来是错的。建议BASE_URL不带尾部的斜杠,接口url统一以斜杠开头,例如BASE_URL = 'https://api.example.com',请求时拼/auth/login,这样最不容易出问题。
3.4 背单词页和做题页的核心交互实现
背单词页的交互是典型的“卡片式学习”:屏幕上一张卡片,正面是英文单词和音标,点击卡片翻转,背面显示中文释义和例句;卡片下方两个按钮,左边“不认识”,右边“认识”。点按钮之后卡片切换下一个,同时把学习结果通过接口提交给后端。
卡片翻转在原生小程序里用CSS动画实现,核心是transform: rotateY(180deg)加transition过渡。这里要说一个实操经验:一个小程序的.wxml里控制翻面状态,通常是一个isFlipped的布尔变量,样式切换用wx:if或者动态class,动画写起来并不难,难的是翻页时快速连续点击导致的动画错乱,解决方案是做一个防抖:
setTimeout(() => { this.setData({ isFlipped: !this.data.isFlipped }) }, 300)把isFlipped的变更包在300毫秒的延时里,用户点完“认识”按钮之后再触发翻面切换,可以从观感上避免按钮还没动画完、下一张卡已经闪出来的情况。
做题页的交互就简单一些,多数真题是选择题,我的做法是:题目区域展示题干和选项,选项用列表渲染,用户点击某个选项后高亮选中的项,底部“上一题”“下一题”按钮切换题目,全部做完点击“提交”按钮,后端统一判分并返回正确答案和解析。
做题页有一个细节要注意:不要一次把整张试卷的所有题目一次性渲染出来,选择题还好,如果未来加阅读理解这种大段文本题目,整个页面的渲染性能会明显下降。我的做法是只加载当前题目和下一题的数据,切题时再向接口索要新的数据,小程序端页面保持轻量。
4. 实操中踩过的坑与问题排查实录
4.1 真机调试连不上本地后端:别把localhost当救命稻草
我第一次用真机调试时信心满满地在小程序里填了http://localhost:8080,结果手机一打开就报网络异常。原因很简单:手机上的localhost指的手机自己,不是你的电脑。真机要把后端地址改成电脑在局域网里的IP,比如http://192.168.1.100:8080,同时确保手机和电脑在同一个WiFi下。
但这里又牵出一个更大的坑:微信开发者工具默认强制HTTPS,非HTTPS域名在真机上根本不发请求。本地调试的时候,在开发者工具的“详情 - 本地设置”里勾选“不校验合法域名、web-view(业务域名)、TLS版本以及HTTPS证书”,正式发布是绝对不允许的,调试完记得取消。每次项目重开这个选项都会重置,属于正常现象,不要以为是自己代码写错了。
如果你已经部署到云服务器上,后端接口必须配HTTPS证书,可以用免费的证书服务,配置好Nginx反向代理把https://你的域名/api转发到http://localhost:8080/api。小程序后台配置request合法域名的时候,域名不能带端口号,必须是标准443端口,这个限制让很多刚接触的人头疼,但这是平台的硬性规则,只能去适配。
4.2 JSON序列化循环引用导致接口直接报错
做了一个“获取试卷详情”的接口,本地用Postman测得好好的,一到小程序端就报错,后来看后端日志才发现是StackOverflowError。原因是实体类的关系设计:Paper里面有一个List<Question>,而Question里面又回引了Paper对象,Spring Boot默认的JSON序列化工具在序列化时看到这种循环引用,直接爆栈。
解决方案有两个,写法上倾向于在回引字段上加@JsonIgnore注解,让序列化跳过这一端:
@ManyToOne @JoinColumn(name = "paper_id") @JsonIgnore private Paper paper;需要注意,加了@JsonIgnore之后,前端拿到的题目JSON里就不会包含paper字段了,如果前端做“从题目反查试卷信息”的场景,就需要额外再提供一个接口。另外一个方案是用@JsonIdentityInfo处理循环引用,让对象以ID代替实例序列化,但这个方法对前端解析有一点要求,复杂度更高,学生项目完全没必要上。
4.3 数据库里的时间字段格式化问题
小程序端拿到的时间字符串默认是2025-01-01T12:00:00这种带T的格式,直接显示在页面上很丑。问题根源是后端默认用的Jackson序列化格式不满足业务需求。
解决办法是在application.yml里统一配置全局时间格式:
spring: jackson: date-format: yyyy-MM-dd HH:mm:ss time-zone: GMT+8这里time-zone一定要显式设置成GMT+8,否则服务器部署在别的时区,或者MySQL连接参数里时区没配对,用户看到的时间就会差八小时。我当时被这个问题坑了整整一个晚上,最后发现是MySQL连接串里没加serverTimezone=Asia/Shanghai,加上之后才恢复正常。
4.4 单词进度统计不准确:先确认是代码问题还是数据问题
项目做完之后,有同学反馈“今日计划里的复习单词跟实际学习记录对不上”,排查了好久发现是user_word表里review_count字段更新逻辑有问题。我在提交学习结果时,只更新了proficiency和last_review_date,忘记了review_count的累加,导致统计页面永远显示初始值。
这类“统计不对”的问题,排查思路有个优先级:先看数据库里原始数据对不对,再看接口返回的JSON对不对,最后看前端渲染对不对。很多人上来就在小程序页面里翻代码,翻半天发现是后端逻辑出了错。调试的正确路径应该是先用Postman直接请求接口,确认接口返回的数据业务上没问题,再去怀疑前端展示层,效率会高很多。
4.5 小程序端明明请求成功,但页面不刷新
这个问题非常典型,多发于在success回调里修改了数据却忘了setData。小程序和Vue、React不一样,它没有响应式绑定机制,你在this.data里改了值,页面不会自动更新,必须调用this.setData({ key: value })才触发视图刷新。
还有更隐蔽的一种情况:你在回调函数里用了箭头函数,this指向是对的,然后调用了this.setData,但数据是嵌套对象,你直接改嵌套属性发现也没生效。比如:
this.setData({ 'wordList[0].proficiency': 2 })这种以路径字符串嵌套的写法在小程序里是支持的,但很多人不清楚,遇到复杂数据结构时直接在this.data里改,页面毫无反应。遇到页面不刷新,先把这两点自查一遍,比重新编译项目十次都管用。
5. 部署上线全流程:从本地调试到服务器运行
5.1 打包后端:Maven构建是标准操作
后端代码写完之后,部署第一步是打包。在项目根目录执行:
mvn clean package -DskipTests这个命令会跳过单元测试,直接打出一个可执行的jar包,在target目录下生成类似cet-assistant-0.0.1-SNAPSHOT.jar的文件。-DskipTests这个参数建议本地打包时都加上,因为有些项目里配置了测试类,测试环境连不上数据库就会导致打包失败,跳过测试能避免不必要的麻烦。
如果服务器上不方便装Maven,也可以在本地打包完把jar包上传到服务器,这是大多数学生的操作方式。如果服务器上有Maven环境,也可以把源码传上去直接构建,但那个流程更长,一般没必要。
5.2 服务器环境的准备与项目启动
我以一台最普通的云服务器为例,操作系统是常见的Linux发行版,部署需要准备JDK、MySQL和Nginx三个核心组件。JDK版本必须跟本地开发保持一致,如果本地用的是JDK8,服务器也装JDK8,版本不一致会报ClassNotFoundException之类的诡异错误。MySQL的版本也尽量一致,防止字符集或者SQL模式的差异导致线上出问题。
上传jar包到服务器之后,启动命令我用的是:
nohup java -jar cet-assistant-0.0.1-SNAPSHOT.jar --server.port=8080 > app.log 2>&1 &nohup保证SSH断开后进程继续运行,> app.log 2>&1把标准输出和错误日志都写进app.log,方便事后排查。第一次启动之后别急着测试接口,先看日志里有没有异常:
tail -f app.log正常启动的标志是日志里出现Started CetAssistantApplication in X seconds这样的内容。如果端口被占用,日志里会明确报出Port 8080 was already in use,改端口就行。
服务器的安全组和防火墙要在控制台里放行8080端口,不然本地访问不到接口。这一步很多人会忘,接口从服务器本机curl一切正常,但小程序端就是连不上,十有八九是安全组没配。
5.3 Nginx反向代理和HTTPS配置
小程序正式环境强制要求域名HTTPS,所以服务器上要部署Nginx做反向代理:用户访问https://你的域名/api/xxx,Nginx把请求转给http://localhost:8080/api/xxx。Nginx配置的核心片段:
server { listen 443 ssl; server_name yourdomain.com; ssl_certificate /etc/nginx/cert/yours.pem; ssl_certificate_key /etc/nginx/cert/yours.key; location /api/ { proxy_pass http://localhost:8080; proxy_set_header Host $host; proxy_set_header X-Real-IP $remote_addr; } }证书申请好之后,把pem和key文件放到服务器上,改好Nginx配置,执行nginx -t校验配置没有语法错误,再nginx -s reload重新加载配置。
有一个容易遗漏的细节:如果域名备案还没有完成,微信小程序后台会拒绝添加request合法域名,这是硬性的平台合规要求。所以部署计划一定要把备案耗时算进去,备案通常需要一到两周,不要等答辩前一周才开始申请。
5.4 部署完成后的小程序端配置
后端服务跑起来之后,小程序端要做两件事。第一件事,把utils/config.js里的BASE_URL从开发环境的http://localhost:8080改成https://你的域名。第二件事,登录微信公众平台,在“开发管理 - 服务器域名”里添加request合法域名。
然后重新编译小程序,用手机扫码预览,完整跑一遍登录、取单词、做题、提交答案的流程。这一步一定要在真机上跑,因为开发者工具的模拟器环境和真机有差异,真机跑通了才说明部署真正成功。
部署和本地调试还有一个细微区别:本地调试时后端日志直接打印在控制台,出了问题一眼就能看到;服务器上日志全在文件的海洋里,排查问题的时候建议把日志加一些便于搜索的关键词,比如[login] userId=123、[submitWord] userId=123 wordId=456,到时候用grep一查一个准,比在没头没尾的大段日志里人工翻效率高出几个数量级。
6. 想把这个项目做成高分毕设,这几点经验值得记住
我带过的学生里,有些人在同一个项目上费了很大的劲,但答辩分数平平;有些人按部就班做完,却能拿出漂亮的演示效果。差异往往不在代码本身,而在于对整个系统的理解深度和演示细节。
第一个建议是把用户身份体系讲透。微信登录的code换openid、JWT鉴权流程、session与token的区别,这三个问题几乎是这类项目答辩必问的。不要只停留在“调用了接口、返回了token”这种层面,要能讲清楚为什么小程序端不能直接调用微信接口,为什么openid不能直接返回前端,为什么选用JWT而不是传统Session。能把这三个“为什么”答清楚,技术层面的分数基本就稳了。
第二个建议是准备一套完整的测试数据。项目里默认词库只有几十个单词,试卷只有一套,演示的时候背单词页面没翻几页就到底了,效果非常拉胯。提前导入几千个四六级核心词汇,准备两到三套真题试卷,演示时可以连续操作很久,考官看到的是一个扎实、完整的系统,而不是一个空架子。
第三个建议是演示前先走一遍全流程。从登录开始,到背单词、做练习、看错题,再到管理员端发布公告、维护词库,整个演示控制在五到八分钟,节奏要稳。很多人平时测试都是断断续续点两下,真到答辩现场手忙脚乱,页面切错、接口报错、网络卡顿,本来能拿高分的项目硬生生变成了翻车现场。提前排练的收益,在答辩这种场合比再改一个月代码都大。
最后再分享一个后续扩展方向:做完四六级小助手这套基础功能之后,可以顺手把“AI作文批改”或者“听力精听训练”作为一个亮点模块加进去,哪怕只做一个简单的实现,也能让项目从“普通管理系统”中跳出来。这类学习工具类的毕设,一个独特的功能点远比堆十个平庸页面更有说服力。