1. 项目概述与需求拆解
看到这个标题时,我第一反应是——这不像一个普通的毕设题目,而是一个已经把自己想到的完整闭环都放进去了的工程级选题。把"互动小课堂"、"智慧课堂"、"云课堂"三个名字放一起看,本质就是同一个东西:一个跑在移动端的即时教学互动平台,技术上锁定Java生态、小程序前端、SpringBoot后端,并且明确提出了"安全开发"这个要求。
很多同学选毕设题目,容易掉进"功能堆砌"的坑——用户模块、课程模块、考试模块、留言板,全部堆上去,看着功能多,实际上每个模块都浮于表面。而这个题目的聪明之处在于,它足够聚焦:"教学互动"四个字划定了业务边界,你不会去做电商、外卖、旅游这些和教学无关的事情;"即时"两个字又抬高了技术含金量,要求你处理实时消息、在线状态、课堂问答这类需要设计功底的内容。
这篇文章我会从项目整体拆解开始,逐步把技术选型、核心功能实现、安全开发思路、部署测试、常见问题排查串成一个可以照着做的完整方案。无论你是正在选毕设题目,还是已经搭建了一半想找参考,都能从中拿到可以直接用的东西。
2. 整体设计与技术选型分析
2.1 为什么是SpringBoot加小程序这套组合
先讲一个很多同学容易忽略的点:毕设选题的技术栈,不能只图"热门",还要考虑你在答辩时能讲清楚什么。Java和SpringBoot的组合在这个题目里,属于"既稳又能讲深"的选择。
SpringBoot相比传统SSH或者SSM框架,核心优势是"约定优于配置"——你不需要像以前那样写一堆XML配置文件,写了Application类就能跑起来。这看起来是省事,但真正的好处是:你在答辩时有更多的精力去讲业务设计、讲安全方案、讲性能优化,而不是被"配置怎么导包"这种基础问题绊住。
小程序端选微信小程序而不是其他跨端方案,是因为它和SpringBoot后端配合时,有着最成熟的生态。微信官方提供的wx.requestAPI天然支持HTTPS请求,SpringBoot天然支持开发和运维上几乎不需要做额外适配。另外,小程序有一套完善的登录态体系,也就是wx.login加code2Session换token的流程,这套流程本身就是教学互动平台安全设计的一部分,用起来非常合适。
三端架构本身也很清晰:
- 小程序端:负责展示课程列表、发起签到、参与弹幕互动、交作业、查看成绩
- SpringBoot后端:负责提供RESTful API、管理用户状态、处理业务逻辑、记录操作日志
- MySQL数据库:负责存储用户、课程、签到记录、互动消息、作业成绩等核心业务数据
2.2 核心模块怎么划分才合理
一个好的互动教学平台,不建议上来就分"用户管理、课程管理、作业管理"这种业务模块,而是先想清楚"一个完整的教学互动流程需要哪些环节"。
我推荐的模块划分思路是:
- 认证与授权模块:注册、登录、token签发与校验、角色区分(教师/学生/管理员)
- 课程内容模块:课程创建、课程列表、课程详情、课件上传与浏览
- 课堂互动模块:签到、随机点名、实时问答、课堂讨论
- 作业与评价模块:作业发布、作业提交、教师批改、成绩查询
- 系统支撑模块:消息推送、数据统计、日志记录
这里想提醒一句:课堂互动模块是整个题目的重点与难点。很多同学做互动平台,做着做着就变成了"信息展示平台",课程列表做好了、课件上传做好了,互动却只有留言板式的评论。实际上,"即时"这个字才是分数最高的地方。
即时互动在本项目中不一定要上WebSocket,但如果你设计了"实时弹幕""在线提问""教师实时查看参与人数"这些场景,WebSocket基本是绕不开的。好在SpringBoot对WebSocket的支持非常成熟,原生@ServerEndpoint注解就能搞定,不需要引入额外的中间件。
2.3 安全设计从一开始就嵌入架构
"安全开发"在这个题目里不是一句空话,它应该渗透到架构的各个层面。我建议从下面几个维度来规划:
- 传输层:小程序端与后端之间全部走HTTPS,配置SSL证书后由Nginx统一处理
- 认证层:采用JWT(JSON Web Token)做无状态认证,token带过期时间,服务端可以校验签名
- 业务层:基于Spring Security或自定义拦截器实现权限控制,区分教师/学生/管理员操作边界
- 数据层:SQL语句全部使用预编译参数,防止SQL注入;密码使用BCrypt加密存储,绝不存明文
- 日志层:关键操作(登录、签到、作业提交、删除课程)都写入操作日志,方便审计
这样一套设计做下来,答辩时安全部分能讲的点非常充足,而且每一条都有对应的代码实现支撑。
3. 关键技术点实现详解
3.1 小程序登录态与JWT认证体系
小程序登录这块特别适合作为项目里的第一个核心功能来讲,因为它是前后端协作的起点。
微信小程序端的标准登录流程是:调用wx.login()获取一个临时code,把code通过wx.request发送到自己的后端接口;后端拿到code后,调用微信的code2Session接口,获取用户的openid和session_key。这里有一个关键点:你的后端绝对不能直接信任小程序前端传过来的openid,因为openid是敏感信息,前端可以伪造。正确的做法是只传code,后端拿着code去微信服务器兑换。
拿到openid之后,我建议不要直接用openid作为身份标识传递给前端,而是生成一个JWT返回给小程序。JWT里面可以封装userId、角色、过期时间,小程序端每次请求时把JWT放在Authorization头里带给后端,后端在拦截器/过滤器里统一解析校验。
JWT的核心代码实现大致长这样:
// 生成Token public String generateToken(User user) { return Jwts.builder() .setSubject(user.getId().toString()) .claim("role", user.getRole()) .setIssuedAt(new Date()) .setExpiration(new Date(System.currentTimeMillis() + 7200000)) .signWith(SignatureAlgorithm.HS256, SECRET_KEY) .compact(); } // 解析Token public Claims parseToken(String token) { return Jwts.parser() .setSigningKey(SECRET_KEY) .parseClaimsJws(token) .getBody(); }这里要说明一下两个小时的过期时间不是随便拍的。两小时覆盖一节课的时长,同时避免了token长期有效带来的安全隐患。如果你希望用户长期免登录,可以做refresh token机制,但作为毕设项目,单纯使用JWT加过期时间已经完全够用。
3.2 SpringBoot后端接口设计规范
接口设计决定了整个项目写起来是顺手还是别扭,也直接关系到最后答辩演示的效果。我推荐统一使用 RESTful 风格,返回格式固定为Result<T>通用响应体:
{ "code": 200, "message": "操作成功", "data": { } }在项目里通常会用枚举定义响应码,比如200表示成功、401表示未认证、403表示无权限、500表示服务器内部错误。这样做最大的好处是:小程序端可以统一解析响应结构,不需要为每个接口单独写错误处理逻辑。
目录结构同样很重要。我建议采用下面这种分包结构,不要全部塞在一个Controller里:
com.example.smartclassroom ├── controller // 接口层,对外暴露HTTP接口 ├── service // 业务逻辑层,处理核心业务 │ └── impl ├── mapper // 数据访问层,MyBatis-Plus操作数据库 ├── entity // 实体类 ├── dto // 前端传输对象,接收参数 ├── vo // 视图对象,返回给前端 ├── config // 配置类,包括WebSocket、拦截器、跨域配置 ├── utils // 工具类 └── security // 安全相关组件值得强调的是DTO和VO不能省略。很多同学偷懒,直接用实体类接收前端传参,前端多传一个字段后端就多存一个字段,非常容易出安全漏洞。用DTO把前端参数接进来,拷贝到实体类再落库,这是一个看着琐碎但是非常必要的习惯。
数据库表设计上也不用搞得过于复杂,核心表我觉得有这些就够了:
- user表:id、username、password(BCrypt加密)、role、avatar、create_time
- course表:id、teacher_id、title、description、cover、status
- course_student表:id、course_id、student_id,完成课程与学生的多对多关联
- sign_in表:id、course_id、sign_code、expire_time、create_time
- sign_in_record表:id、sign_in_id、student_id、sign_time、location
- assignment表:id、course_id、title、content、deadline
- assignment_submit表:id、assignment_id、student_id、content、file_url、status、score、comment
- interaction_message表:id、course_id、sender_id、content、message_type、create_time
这是一套完全满足教学互动场景的最小表设计,字段不多,但每张表都有完整业务用途。
3.3 即时互动模块的WebSocket实现
课堂讨论、弹幕互动、在线状态,这三个场景如果使用HTTP轮询去实现,小程序端每隔两三秒请求一次接口,后端压力大、实时性还差,体验非常糟糕。WebSocket的引入是自然的选择。
在SpringBoot中实现WebSocket主要有两种方式:一种是使用Spring封装的WebSocketHandler,一种是用Java自带的@ServerEndpoint。对毕设项目而言,推荐后者,代码量更少、结构也更清晰。
关键实现步骤是这样的:
@Component @ServerEndpoint("/ws/{token}") @Slf4j public class ClassroomWebSocket { private static Map<String, Session> sessionPool = new ConcurrentHashMap<>(); @OnOpen public void onOpen(Session session, @PathParam("token") String token) { String userId = JwtUtil.parseToken(token).getSubject(); sessionPool.put(userId, session); log.info("用户 {} 建立连接", userId); } @OnMessage public void onMessage(String message, Session session) { // 解析消息并广播 WebSocketMessage wsMessage = JSON.parseObject(message, WebSocketMessage.class); sendToAll(wsMessage); } @OnClose public void onClose(@PathParam("token") String token) { String userId = JwtUtil.parseToken(token).getSubject(); sessionPool.remove(userId); } private void sendToAll(WebSocketMessage message) { sessionPool.values().forEach(s -> { try { s.getBasicRemote().sendText(JSON.toJSONString(message)); } catch (IOException e) { log.error("发送消息失败", e); } }); } }两个容易踩的坑必须提醒:第一,sessionPool一定得用ConcurrentHashMap,不能用普通的HashMap,因为WebSocket回调是多线程并发的;第二,token放在路径参数里传,会出现在日志里有泄露风险,更严谨的做法是前端连上之后先发一条认证消息,后端校验完再放行——不过毕设场景下把token放路径里也能跑通,看你导师的要求。
小程序端的WebSocket API也很好用:
wx.connectSocket({ url: 'wss://your.domain.com/ws/' + token }); wx.onSocketMessage(function(res) { var data = JSON.parse(res.data); // 渲染到页面 });3.4 签到与作业闭环的业务逻辑
签到功能是教学互动平台一个很好的差异化亮点。老师每次上课生成一个签到码,学生输入签到码完成签到,后台自动记录签到时间,系统判定是否迟到。这个功能虽然实现不难,但业务逻辑细节要反复想清楚。
后端生成签到码的代码大概是这样:
public SignIn createSignIn(Long courseId) { SignIn signIn = new SignIn(); signIn.setCourseId(courseId); signIn.setSignCode(String.format("%04d", new Random().nextInt(10000))); signIn.setExpireTime(LocalDateTime.now().plusMinutes(5)); signIn.setCreateTime(LocalDateTime.now()); signInMapper.insert(signIn); return signIn; }为什么要限制签到码5分钟过期?一是防止学生课后补签,二是体现"即时"场景的真实性。此外,还要做防重复签到校验,同一名学生不能对同一场签到提交多次,这需要在sign_in_record表加唯一索引:
ALTER TABLE sign_in_record ADD UNIQUE KEY uk_sign_student (sign_in_id, student_id);作业模块和签到类似,是一条完整的业务链:教师发布作业 → 学生提交 → 教师批改 → 学生查看成绩。这里的核心建议是,将作业的"提交状态"在表里显式记录,不要通过"有没有提交记录"来判断学生是否交了作业。这样老师在批改列表里能直接区分"未提交"和"已提交未批改",页面上就不用写一堆复杂的逻辑判断。
3.5 文件上传与课件管理
课件上传是教学平台里绕不开的功能。SpringBoot接文件上传很简单,关键是上传后文件存在哪里。毕设项目不建议引入MinIO之类的对象存储中间件,直接把文件存在本地磁盘上,然后通过一个访问URL暴露出来就够了。
建议的存储路径结构是:
/upload/2026/04/按年月分目录存放,一是避免一个目录塞太多文件,二是方便后续按时间清理。上传接口设定单文件大小限制,SpringBoot里这样配置:
spring: servlet: multipart: max-file-size: 20MB max-request-size: 50MB上传后把文件URL存到数据库。需要注意的是,小程序端的下载域名需要在微信公众平台后台配置白名单,本地开发时可以直接用IP加端口访问,但正式环境必须走HTTPS域名,否则文件在小程序里面打不开。
4. 安全开发实践的落地细节
4.1 接口鉴权与越权防护
很多同学的安全意识停留在"登录后才能访问接口"这一层,能想到用拦截器做登录校验已经不错了。但拦截器校验的只是"有没有token",真正精细的权限控制在于"你是什么样的角色""你能操作哪些数据"。
我建议在两个层面做权限控制:
- 方法级别:使用
@RequiresRole("teacher")这类自定义注解标记接口允许哪些角色访问 - 数据级别:教师只能修改自己创建的课程,学生只能提交自己选课的作业
数据级别越权这块,是最容易忽略又最被答辩评委看重的点。什么叫数据级别越权?比如学生A调用更新作业提交的API,把请求参数里的submitId改成学生B的提交记录ID,他就能修改别人的作业了。防止这类问题的核心思路是:服务端永远相信登录用户的身份,而不是相信前端传来的参数判定归属权。
具体做法很简单——从token拿到当前用户ID,SQL查询时把userId带进去作为条件:
@Override public void updateSubmit(SubmitUpdateDTO dto, Long currentUserId) { AssignmentSubmit submit = submitMapper.selectById(dto.getSubmitId()); if (!submit.getStudentId().equals(currentUserId)) { throw new BusinessException("无权操作他人提交记录"); } // 更新操作 }记住一个原则:你的后端永远不信任前端传的任何身份标识。
4.2 SQL注入与XSS攻击的预防
SQL注入是Java Web面试必问、答辩也特别容易展开的话题。在SpringBoot项目里只要用了MyBatis-Plus,默认就是预编译机制,能够规避绝大部分SQL注入问题。但如果你自己写了SQL,就要非常小心${}和#{}的区别。
${}是字符串拼接,直接把用户输入拼进SQL,等于打开了注入的大门。#{}是预编译占位符,参数通过JDBC的PreparedStatement传进去,这是安全的写法。写自定义SQL时,能用#{}就一定不用${}。如果实在需要动态排序字段(比如根据前端传的排序字段排序),那也必须在代码里做白名单校验,绝不能直接把前端值拼进SQL。
XSS攻击方面,后端接口返回的富文本内容不能盲目信任。比如学生提交的作业内容里可能包含<script>标签,小程序端虽然默认有过滤机制,但服务端最好还是做一层清洗。建议引入Jsoup工具类做HTML过滤:
public static String clean(String html) { Jsoup.clean(html, Safelist.basic()); }4.3 敏感信息保护与日志安全
密码存储这块,Spring Security自带的BCryptPasswordEncoder是标准答案。BCrypt是加盐哈希算法,哪怕两条数据密码相同,生成的哈希值也不一样,而且哈希过程本身很慢,暴力破解成本高。注册和登录的代码大致这样:
// 注册时 passwordEncoder.encode(user.getPassword()); // 登录校验时 passwordEncoder.matches(rawPassword, encryptedPassword);除了密码,还有一些信息需要注意:接口返回给前端的数据里,不能把数据库自增ID直接当主键暴露,不然爬虫可以顺着ID把全部数据抓一遍。返回学生信息时,密码、手机号字段必须设置为null再去返回。这里可以用一个简单思路:entity负责持久化,vo负责输出,输出的字段在vo里自己定。
日志审计也是安全的一环。我会在登录成功、签到成功、删除课程这些敏感操作上做日志记录,包括操作人、操作时间、操作内容、IP地址。答辩时你可以直接演示"如何通过日志追溯一次异常操作",这种细节对分数提升很有帮助。
5. 工程搭建与实测过程记录
5.1 从零搭建SpringBoot工程的完整流程
实际操作的时候,不要直接去找一个现成代码改,也不用非得从Spring官网初始化项目。我建议用Idea自带的Spring Initializr创建工程,选好依赖之后直接开码。
毕设项目需要的依赖清单大致如下:
spring-boot-starter-web:Web基础能力,Controller、RESTful接口都依赖它spring-boot-starter-validation:参数校验,@Valid加@NotNull等注解就能做字段校验mybatis-plus-boot-starter:数据访问层,比原生MyBatis少写很多XMLmysql-connector-java:MySQL驱动jjwt或java-jwt:JWT生成与解析lombok:减少getter/setter代码fastjson或jackson:JSON序列化反序列化
启动类只需要一个注解就能让整个项目跑起来:
@SpringBootApplication @MapperScan("com.example.smartclassroom.mapper") public class SmartClassroomApplication { public static void main(String[] args) { SpringApplication.run(SmartClassroomApplication.class, args); } }5.2 关键接口的实现示例
我挑一个特别有代表性的接口讲讲完整链路——教师发起签到。这个接口涵盖了身份认证、参数校验、业务处理、返回响应四个环节。
Controller层的代码:
@PostMapping("/course/{courseId}/sign-in") @RequiresRole("teacher") public Result<SignInVO> createSignIn(@PathVariable Long courseId) { User teacher = CurrentUserHolder.get(); SignInVO signInVO = signInService.createSignIn(courseId, teacher.getId()); return Result.success(signInVO); }Service层的核心逻辑:
public SignInVO createSignIn(Long courseId, Long teacherId) { Course course = courseMapper.selectById(courseId); if (course == null || !course.getTeacherId().equals(teacherId)) { throw new BusinessException("课程不存在或无权操作"); } SignIn signIn = new SignIn(); signIn.setCourseId(courseId); signIn.setSignCode(GenerateCodeUtil.studentSignInCode()); signIn.setExpireTime(LocalDateTime.now().plusMinutes(5)); signInMapper.insert(signIn); return BeanUtil.copyProperties(signIn, SignInVO.class); }这里的核心是Service层做了课程归属校验——只有这门课的任课教师才能发起签到。这个校验放在Controller里也可以跑,但放在Service层是更规范的做法,因为Service层可以被多个Controller复用,避免校验逻辑遗漏。
学生端的签到接口同样需要校验:
public void studentSignIn(Long signInId, Long studentId) { SignIn signIn = signInMapper.selectById(signInId); if (signIn == null || signIn.getExpireTime().isBefore(LocalDateTime.now())) { throw new BusinessException("签到码已失效"); } // 判断学生是否在课程内 boolean enrolled = courseStudentMapper.checkEnrolled(signIn.getCourseId(), studentId); if (!enrolled) { throw new BusinessException("未选课,无法签到"); } // 插入签到记录,唯一索引保证不重复 SignInRecord record = new SignInRecord(); record.setSignInId(signInId); record.setStudentId(studentId); record.setSignTime(LocalDateTime.now()); signInRecordMapper.insert(record); }5.3 小程序端页面与请求封装
小程序端我建议对请求做统一封装,不要在每个页面里直接写wx.request。封装之后,自动带token、统一处理错误码、统一加载提示,写页面时会省非常多的时间。
// utils/request.js const request = (url, method = 'GET', data = {}) => { return new Promise((resolve, reject) => { wx.request({ url: BASE_URL + url, method: method, data: data, header: { 'Content-Type': 'application/json', 'Authorization': wx.getStorageSync('token') }, success: (res) => { if (res.data.code === 200) { resolve(res.data.data); } else if (res.data.code === 401) { wx.navigateTo({ url: '/pages/login/login' }); reject(res.data); } else { wx.showToast({ title: res.data.message, icon: 'none' }); reject(res.data); } }, fail: (err) => { wx.showToast({ title: '网络异常', icon: 'none' }); reject(err); } }); }); }; module.exports = { request };页面里调用就非常简单了:
const data = await request('/course/list', 'GET', { page: 1 }); this.setData({ courseList: data.records });5.4 本地联调与内网穿透访问
联调是整个项目落地过程中最花时间也最容易出问题的一环。小程序开发者工具有一个"不校验合法域名"的选项,本地联调时勾上,可以方便地访问http://localhost:8080。
但小程序端有个比较麻烦的限制:手机预览时访问不了电脑的localhost。这时候,需要让手机和电脑连同一个WiFi,后端启动时监听0.0.0.0,然后小程序里把接口地址改成电脑的局域网IP加端口,例如http://192.168.1.23:8080。注意,IP地址需要在同一个网段内,不能跨网络段。
如果想要外网访问,就需要做内网穿透,将本地8080端口映射到一个公网HTTPS域名。这个方案能让小程序真机调试跑通,但只能自己测试,不能部署到线上生产环境。
需要特别提醒的是:不要用任何抓包类工具去拦截或分析加密流量,更不要教授绕过HTTPS校验去看别人小程序内部数据的方法。调试自己开发的毕设项目,用开发者工具自带的Network面板就完全够用,那里能清楚看到每个请求的headers、query参数、响应body,真要定位问题用这些信息足够了。
6. 测试方法与性能优化实录
6.1 接口测试的完整步骤
拿到工程后,第一步先把接口测试跑通,再去做页面。后端启动后,建议先用Postman或者Apifox测接口,把"注册→登录→创建课程→发起签到→学生签到"这条业务链走一遍,确认没有任何问题后再写小程序页面。
测试要点建议按下面的顺序来:
- 未登录访问受保护接口,返回401
- 学生token调用教师接口,返回403
- 正常流程注册登录,拿到token且能在有效期内访问
- 课程创建成功后,教师和学生的课程列表数据正确
- 重复签到同一场签到,第二次必须报错
- 过期签到码签到,必须报"已失效"
- 提交空内容作业,后端必须拒绝
6.2 代码层面的性能优化实践
毕设阶段的系统并发量虽然不高,但代码习惯要从一开始就养好。几个我实测下来非常有效的优化点:
数据库字段能加索引的一定要加。
sign_in_record表的sign_in_id字段、course_student表的course_id和student_id字段、assignment_submit表的assignment_id字段,都是高频查询条件,没有索引就是全表扫描。实际测试中,加了索引之后签到查询耗时从几十毫秒降到个位数毫秒,差距非常明显。列表接口一定要做分页。MyBatis-Plus 的
Page对象已经封装好了分页插件,小程序端传pageNum和pageSize,后端返回total和records即可。不要一次把全表数据返回给前端,既浪费流量又拖慢渲染。避免在循环里查数据库。比如课程列表接口要返回每个课程的学生人数,最忌讳的写法是循环查每个课程的学生数。正确方式是用一条
GROUP BY course_id的SQL把全部课程人数查出来,然后在内存中组装。热点数据加本地缓存。课程详情这类接口的访问频率很高,而且数据变更频率低,用
Caffeine做一个简单的本地缓存,能有效减少数据库压力。
6.3 部署上线:从本地到云服务器
毕设演示一般都是在本地跑,但如果你想在系统演示前把项目部署到服务器上,给评委一个稳定、随时可访问的演示环境,那部署流程需要提前规划。
我推荐的部署方案是:一台轻量云服务器,Docker安装MySQL,Java工程打包成jar包用nohup后台启动,Nginx监听443端口做HTTPS反向代理,同时托管小程序的静态资源文件。
后端打包命令很简单:
mvn clean package -DskipTests打包完的smart-classroom-0.0.1-SNAPSHOT.jar就可以扔到服务器上了:
nohup java -jar smart-classroom-0.0.1-SNAPSHOT.jar --spring.profiles.active=prod > app.log 2>&1 &生产环境的数据库连接、Redis地址等不能和开发环境一样,建议用application-prod.yml配置文件单独管理,启动时用--spring.profiles.active=prod切换。
Nginx反向代理配置的关键片段:
server { listen 443 ssl; server_name your.domain.com; ssl_certificate /etc/nginx/ssl/your.domain.com.pem; ssl_certificate_key /etc/nginx/ssl/your.domain.com.key; location / { proxy_pass http://127.0.0.1:8080; proxy_set_header Host $host; proxy_set_header X-Real-IP $remote_addr; # WebSocket反向代理需要的配置 proxy_http_version 1.1; proxy_set_header Upgrade $http_upgrade; proxy_set_header Connection "upgrade"; } }WebSocket走Nginx时必须带Upgrade相关的几个header,否则小程序里的课堂讨论连接会一直失败。这是我在实际部署中踩过的坑,配置写全之后问题立刻就消失了。
7. 常见问题与排查技巧实录
7.1 登录时微信code2Session接口调用失败
这个问题非常常见,主要原因有这几个:AppID填错、AppSecret填错、code重复使用。尤其第三个坑,微信的code是一次性的,用过即失效,不能在同一请求中调用多次code2Session,更不能在小程序端连续调用wx.login()后把最新的code当成第一次使用的code。
排查办法很简单,在后端代码里加日志,把调用微信接口的请求参数和响应结果原样打印出来,看微信接口返回的errcode是多少。微信返回40029就是code无效,返回40163就是code被重复使用了。
7.2 小程序端请求一直报"网络异常"
前端报网络异常,不要急着改前端代码。先用浏览器直接访问后端接口,能通就说明后端没问题,问题出在小程序端到后端之间。
然后逐项排查:后端监听的是不是0.0.0.0;小程序里配的地址是不是http://开头;手机和电脑是不是同一个局域网。本地调试时最容易忽略的是"域名白名单"——小程序正式环境不允许请求HTTP接口,但本地开发勾选"不校验合法域名"后就可以。另外,如果用的是HTTPS域名,SSL证书必须是有效的,不能是自签名证书。
7.3 WebSocket连接成功但收不到消息
前后端WebSocket连接建立成功后,消息收不到大概率出在消息广播的逻辑上。最常见的错误是:只在建立连接的客户端发送消息,忘记写给所有人广播的sendToAll方法。另外,前后端消息格式必须一致,比如后端用JSON.toJSONString发送对象,前端就必须JSON.parse解析,否则拿到的是字符串而不是对象。
7.4 频繁请求接口导致数据库连接耗尽
后端默认的数据库连接池参数大概率够用,但如果接口里出现了循环查库、大文件查询等操作,连接池很容易被占满,日志里就会出现Connection is not available, request timed out之类的报错。
这个问题的排查思路是:先看代码里有没有循环查库、有没有没关闭的连接,再加好慢SQL日志,最后考虑调大连接池参数:
spring: datasource: hikari: maximum-pool-size: 10 minimum-idle: 5 connection-timeout: 300007.5 常见问题速查表
| 现象 | 可能原因 | 解决思路 |
|---|---|---|
| 登录报401 | token过期或者没有传Authorization头 | 检查token有效期,调整封装请求头逻辑 |
| 接口报403 | 当前角色无权访问该接口 | 检查权限注解与用户角色是否匹配 |
| 签到提示已失效 | 签到码过期或已被使用 | 查看签到记录的expire_time字段 |
| 上传文件报500 | 磁盘路径没有写权限 | 确认上传目录权限并手动创建 |
| 图片在小程序端打不开 | 域名不在下载白名单 | 微信公众平台后台添加合法域名 |
| WebSocket无法建立 | 端口未开放或Nginx缺少Upgrade配置 | 检查服务器安全组与Nginx配置 |
| 中文乱码 | 数据库编码或连接参数不是UTF-8 | 数据库建库用utf8mb4,连接串加characterEncoding=UTF-8 |
8. 写在最后的一点经验之谈
带着学生做同类项目做过好几次之后,我最大的体会是:这个题目的技术难度并不算高,真正的功夫都花在"闭环完成度"上面。很多同学做到课程列表和登录就停了,剩下的时间全浪费在反复调样式和改小BUG上,最后拿出来的成品看起来功能很多,实际交互流程残缺不全。
我的建议是,在动手编码前,先用两个小时把整个业务流程走一遍:“学生注册登录 → 老师建课 → 学生在课程详情页选课 → 老师发起签到 → 学生签到时能看到实时结果 → 老师在作业模块布置作业 → 学生提交后老师批改打分 → 学生端能看到自己的成绩”。这条链路每敲一环,代码往前推进一步,系统就越像一个真正能用的产品,而不是各种功能点的拼凑。
另外,安全开发这块千万不要当成应付论文的装饰。把登录态设计、越权校验、参数校验、日志审计真正落实到代码里,一方面让系统更靠谱,另一方面在答辩讲演示时,这些点都是可以正面回答提问的硬素材——比起"我的系统能跑"这种底气,还是"它跑得安全、设计得严谨"更有说服力。
最后分享一个开发顺手的细节:小程序端页面尽量用官方组件库的现成组件,不要自己手写一大堆样式,把节省下来的时间投入后端代码质量和安全设计上,收益高得多。碰到不确定的接口设计,先写接口文档再写代码,自己定义好字段格式,前后端联调会顺畅很多。这个项目做完,不管答辩结果如何,你至少会实实在在地理解一个完整Web应用的诞生过程,这种能力在后面的学习和工作里都值钱。