1. 先把需求想透:这套平台到底服务谁、解决什么问题
翻开源站和毕设题目库,"基于web的在线学习平台"这个题目的出现频率高得离谱,几乎每个做Java Web或者前后端分离方向的人都会撞上一次。但绝大多数人拿到题目后的第一反应是打开IDE建工程,直接开始写登录页——这是最容易踩坑的起步方式。我带过几届做这类项目的同学,也review过不少类似仓库,真正能把功能跑通、代码可读、开题报告写得像样的,往往是那些先在纸上把需求画清楚的人。这篇东西我打算按"需求→架构→数据→实现→跑起来→写开题→排错"的顺序,把这套系统的里里外外拆一遍,重点是那些文档里不会写、但你自己动手一定会撞上的地方。
所谓"基于web的在线学习平台",说人话就是:把线下的课程、讲义、练习、考试搬到浏览器里,让老师能发课、学生能学课、双方能看到进度和成绩。它的核心关键词是在线学习、课程管理、学习进度、在线考试、权限控制。这套东西表面上是个"教学网站",底下其实解决四个层面的问题:内容怎么组织(课程结构)、人怎么管(角色权限)、学习行为怎么记录(进度追踪)、学习效果怎么衡量(测验与考试)。谁适合看这篇:正在做这套毕设的人、想拿一套现成结构快速改造成企业内训系统的人、以及单纯想弄明白一个中等规模CRUD系统该怎么设计的初学者。
需要提前说清楚一点,下面提到的所有分层方案、表结构、参数取值,都是基于这类项目的常见实践做的合理补全,不是某个特定仓库的逐行代码解读。你手上的源码如果字段名对不上,看思路就行,别死抠名字。另外,"源码+开题"这个组合意味着两件事都得管:代码要能跑起来跑得对,文档要能讲清楚为什么这么做。很多人代码写得不错,开题报告一塌糊涂,最后答辩被追问得满头汗,挺可惜的。
2. 需求边界怎么划:哪些必须做、哪些坚决不做
2.1 三类角色的诉求拆解
一个学习平台最少要区分三类人,这一点在动手写代码前就得定死,因为它直接决定了权限模型和页面路由的设计。学生的诉求很直接:我要能看到我选的课、上课能看视频看讲义、看完知道学到哪儿了、有作业和考试能提交、能看到自己的成绩和错题。教师的诉求是反向的:我要能建课程、往里塞章节和小节、上传视频和讲义、出题组卷、看班里每个人的进度和成绩分布。管理员管的是平台层面:账号的开通与禁用、角色的分配、课程分类的维护、公告的发布。
把这三类人的诉求列成清单之后,你会发现一个规律——所有功能都可以归到"增删改查"加上"状态流转"这两类动作上。课程是数据,章节是数据,题目是数据,学习记录和成绩也是数据。真正有难度的不是CRUD本身,而是数据之间的关联关系以及状态的一致性。比如学生退课之后,他的学习记录是删掉还是留着?老师把某道题从题库删除,已经考过的试卷里那道题还在不在?这些边界问题,等到写了一半代码再想,改动成本会高到你怀疑人生。
我一般的做法是画一张"角色-功能"矩阵表,把每个功能点归属到具体角色上,谁可读、谁可写、谁完全不可见,三列填完,权限模型基本就出来了。这张表后面还能直接搬到开题报告的"系统功能模块"章节里,一举两得。
| 功能模块 | 学生 | 教师 | 管理员 |
|---|---|---|---|
| 浏览课程列表 | 读 | 读 | 读 |
| 创建/编辑课程 | 不可见 | 写(限本人) | 写(全部) |
| 上传视频/讲义 | 不可见 | 写(限本人课程) | 写 |
| 选课/退课 | 写 | 不可见 | 可代操作 |
| 学习进度查看 | 读(本人) | 读(本课程全部) | 读(全部) |
| 出题组卷 | 不可见 | 写 | 写 |
| 参加考试 | 写 | 不可见 | 不可见 |
| 成绩查看 | 读(本人) | 读(本课程) | 读(全部) |
| 账号管理 | 不可见 | 不可见 | 读写 |
2.2 功能边界:砍掉一半需求反而更容易拿高分
这里有个很现实的经验:功能不是越多越好,而是越完整越好。我见过太多项目,一口气列了直播、AI答疑、积分商城、学习社区、证书生成十几个模块,最后能完整跑通的只有登录和课程列表,答辩老师随便点两下就露馅了。相比之下,一个只有课程管理、学习进度、在线考试三块,但每块都闭环的项目,评价会高得多。
所谓闭环,指的是每个功能都有完整的生命周期。以"课程管理"为例,闭环是:教师创建课程→填写基础信息→添加章节→添加小节→上传资源→发布(状态从草稿变已发布)→学生可见→学生选课→学生学习→教师看统计。中间任何一环断了,这个功能就是半成品。所以我建议先把三到四个核心模块做成闭环,再考虑扩展。
至于哪些功能该砍,我的判断标准是:依赖外部服务的、需要实时通信的、涉及资金或支付的、强依赖审核内容的,一律砍掉。在线直播需要流媒体服务器,实时聊天需要WebSocket长连接和消息可靠性保证,这两块单独拿出来都能做一篇毕设,塞进主项目里只会拖垮进度。而"课程讨论区"这种异步留言功能,成本低、演示效果好,反而值得保留。
2.3 非功能性需求:写在开题里比写在代码里更值钱
很多人写开题报告只写功能,一句"系统需具备良好的性能和安全性"糊弄过去。实际上非功能性需求恰恰是答辩时老师最爱追问的点,因为功能谁都会写,能说清楚"为什么这么定"的人不多。我一般会明确写这么几条,并且给出可验证的口径:
- 并发规模:定位为课程设计级别,支持50~100人同时在线,这个数字不是拍脑袋,而是根据"一个教学班的人数"倒推出来的,答辩时能自圆其说。
- 响应时间:普通列表查询在1秒内返回,视频加载首帧在3秒内,超过这个阈值就说明索引或者分页有问题。
- 数据安全:密码必须加盐哈希存储(BCrypt),不允许明文或简单MD5;接口必须做鉴权,不能只靠前端隐藏按钮。
- 兼容性:主流浏览器的最近两个大版本,页面在1366×768及以上分辨率下正常显示。
把这些写进开题,比堆十个花哨功能更能体现你对系统的理解深度。
3. 技术选型与整体架构:地基歪了后面全是补丁
3.1 前后端分离还是服务端渲染
这是第一个分岔路口。服务端渲染(JSP、Thymeleaf、模板引擎那一套)的好处是上手快,一个工程搞定,页面跳转由后端控制,不用处理跨域,对于只熟悉Java、前端功底薄的人来说风险最低。缺点是前后端耦合,改一个按钮颜色都要重新打包部署,而且现在企业里基本不这么干了,写在简历上也不太加分。
前后端分离(Vue/React + RESTful接口)的好处是职责清晰,前端专注交互、后端专注数据,接口可以同时供网页端和移动端使用,扩展性好。代价是要处理跨域、鉴权令牌、联调时接口对不上等一堆琐事。我的建议是:如果你前端基础还行,直接上分离方案,用Vue2或Vue3配Element UI/Element Plus,组件库能把表单、表格、分页、上传这些重复劳动吃掉一大半,省下来的时间够你把业务逻辑想清楚。
真正需要注意的是"分离"要彻底。我见过不少半吊子工程,前端用Vue,后端却还在用Session存登录态,跨域时Cookie带不过去,最后靠改配置硬凑,非常别扭。既然分离,就用令牌方案,把状态从服务端挪到客户端。
3.2 单体还是微服务:别给自己找麻烦
微服务这个词在开题报告里出现的频率很高,但我要泼一盆冷水:这套系统用单体架构就够了,而且更合适。理由很实在——在线学习平台的业务量级根本撑不到需要拆服务的程度,拆开之后你要处理服务注册发现、配置中心、跨服务调用、分布式事务、链路追踪,随便一个都是坑,而以你的时间预算,光是把单体跑通就够呛。
单体分层做清楚,同样能体现架构能力。我一般分四层:**控制层(Controller)**只管接收参数、校验、调服务、返回统一格式;**业务层(Service)**承担核心逻辑和事务边界;**数据访问层(Mapper/Repository)**只做SQL;**实体层(Entity/DTO/VO)**负责数据载体。分层的价值在于职责边界清晰,出问题时能快速定位是参数错了、逻辑错了还是SQL错了。DTO和VO分开这件事,很多人嫌麻烦就合成了一个类,结果前端要的字段和后端表的字段打架,写着写着就乱了。
3.3 技术组合的具体清单与版本对齐
一套能跑的组合,我给个参考:后端 JDK 1.8 或 17(看框架版本,Spring Boot 2.x 配 1.8 比较稳)、Spring Boot 2.7.x、MyBatis-Plus(比原生MyBatis省掉大量样板代码)、MySQL 5.7 或 8.0、Redis(可选,用于令牌缓存和热点数据)、Maven 3.6+。前端 Node 14/16(别用太新的版本,某些老依赖会编译报错)、Vue 2.6 或 Vue 3、Element UI 或 Element Plus、axios、vue-router。
版本对齐这件事值得单独强调,因为它是新手卡住时间最长的环节。举几个高频坑:JDK版本和Spring Boot版本不匹配会报"unrecognized class file version";Node版本过高会导致node-sass编译失败,这时候要么降Node版本,要么换dart-sass;MySQL 8.0的连接串必须带时区和SSL参数,否则启动就报连接异常。这些错误信息看着吓人,其实都是配置问题,跟代码质量无关。
提示:把所有依赖版本写进一个文档或者README,包括JDK、Maven、Node、npm、MySQL的确切小版本号。换台机器重装环境时,这份清单能省你半天时间,也是开题报告"开发环境"章节的现成素材。
4. 数据库设计:这部分偷懒,后面全是返工
4.1 核心表结构怎么拆
数据库是整套系统的骨架,表设计错了,后面每一行代码都在还债。我按模块把核心表列一下,字段只写关键的,具体的类型和长度你可以按实际情况调。
用户与权限部分,最少五张表:用户表、角色表、用户角色关联表、权限表、角色权限关联表。这是标准的RBAC模型,好处是新增一种角色只需要插数据,不用改代码。
CREATE TABLE sys_user ( id BIGINT PRIMARY KEY AUTO_INCREMENT, username VARCHAR(50) NOT NULL UNIQUE COMMENT '登录名', password VARCHAR(100) NOT NULL COMMENT 'BCrypt哈希', real_name VARCHAR(50), avatar VARCHAR(255), email VARCHAR(100), status TINYINT DEFAULT 1 COMMENT '1正常 0禁用', create_time DATETIME DEFAULT CURRENT_TIMESTAMP, deleted TINYINT DEFAULT 0 ); CREATE TABLE sys_role ( id BIGINT PRIMARY KEY AUTO_INCREMENT, role_code VARCHAR(30) NOT NULL UNIQUE COMMENT 'STUDENT/TEACHER/ADMIN', role_name VARCHAR(50) NOT NULL ); CREATE TABLE sys_user_role ( id BIGINT PRIMARY KEY AUTO_INCREMENT, user_id BIGINT NOT NULL, role_id BIGINT NOT NULL, UNIQUE KEY uk_user_role (user_id, role_id) );课程部分要拆成三层:课程、章节、小节。很多人图省事做成两层(课程+小节),结果发现章节标题没地方放,只能在课程里加一堆冗余字段。三层结构虽然多一张表,但结构清晰,前端渲染目录树也方便。
CREATE TABLE course ( id BIGINT PRIMARY KEY AUTO_INCREMENT, title VARCHAR(120) NOT NULL, cover VARCHAR(255) COMMENT '封面图路径', category_id BIGINT, teacher_id BIGINT NOT NULL, intro TEXT, price DECIMAL(10,2) DEFAULT 0, status TINYINT DEFAULT 0 COMMENT '0草稿 1已发布 2已下架', student_count INT DEFAULT 0 COMMENT '冗余统计', create_time DATETIME DEFAULT CURRENT_TIMESTAMP, deleted TINYINT DEFAULT 0 ); CREATE TABLE course_chapter ( id BIGINT PRIMARY KEY AUTO_INCREMENT, course_id BIGINT NOT NULL, title VARCHAR(120) NOT NULL, sort_order INT DEFAULT 0 ); CREATE TABLE course_section ( id BIGINT PRIMARY KEY AUTO_INCREMENT, chapter_id BIGINT NOT NULL, course_id BIGINT NOT NULL COMMENT '冗余,避免多表关联', title VARCHAR(120) NOT NULL, type TINYINT COMMENT '1视频 2图文 3附件', resource_url VARCHAR(255), duration INT DEFAULT 0 COMMENT '视频时长,秒', sort_order INT DEFAULT 0, free_flag TINYINT DEFAULT 0 );注意course_section里那个冗余的course_id,这是故意的。查"某课程下所有小节"时,能少一次关联;代价是插入时要保证一致性。这种冗余字段在中小系统里是划算的,但如果你的表结构要写进开题报告,记得准备一句解释——"以适度冗余换取查询效率,通过业务层保证一致性",这句话能堵住不少追问。
选课与学习记录是这套系统区别于普通内容管理的关键。选课表记录"谁选了哪门课",学习记录表记录"谁在哪个小节学到哪一秒"。
CREATE TABLE course_enroll ( id BIGINT PRIMARY KEY AUTO_INCREMENT, course_id BIGINT NOT NULL, student_id BIGINT NOT NULL, enroll_time DATETIME DEFAULT CURRENT_TIMESTAMP, progress DECIMAL(5,2) DEFAULT 0 COMMENT '课程总进度百分比', last_section_id BIGINT COMMENT '最后学习的小节,用于续播', UNIQUE KEY uk_course_student (course_id, student_id) ); CREATE TABLE study_record ( id BIGINT PRIMARY KEY AUTO_INCREMENT, student_id BIGINT NOT NULL, course_id BIGINT NOT NULL, section_id BIGINT NOT NULL, position INT DEFAULT 0 COMMENT '视频播放位置,秒', watched INT DEFAULT 0 COMMENT '累计有效观看秒数', finished TINYINT DEFAULT 0, update_time DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, UNIQUE KEY uk_stu_sec (student_id, section_id) );这里有个设计选择值得说说:进度到底存在enroll表还是study_record表?两个都要存。study_record是按小节粒度的原始数据,enroll.progress是按课程粒度的汇总值。汇总值属于"可以算出来但每次算都很慢"的数据,冗余存一份,更新时机放在学习行为上报之后,这是典型的空间换时间。至于什么时候重算,我的做法是每次上报后只更新该学员该课程的汇总,不触发全表扫描。
考试部分拆成题库、试卷、试卷题目关联、考试记录、答案记录五张表。题库表要能覆盖单选、多选、判断、简答四种题型,用type字段区分,选项内容用JSON字符串存或者单独开一张选项表。
CREATE TABLE question ( id BIGINT PRIMARY KEY AUTO_INCREMENT, course_id BIGINT, type TINYINT COMMENT '1单选 2多选 3判断 4简答', content TEXT NOT NULL, options TEXT COMMENT 'JSON数组,简答题为空', answer VARCHAR(255) NOT NULL COMMENT '多选按字母升序存,如ABD', analysis TEXT COMMENT '解析', score INT DEFAULT 5, difficulty TINYINT DEFAULT 2, teacher_id BIGINT ); CREATE TABLE exam_paper ( id BIGINT PRIMARY KEY AUTO_INCREMENT, course_id BIGINT NOT NULL, title VARCHAR(120), total_score INT DEFAULT 100, pass_score INT DEFAULT 60, duration INT COMMENT '考试时长,分钟', start_time DATETIME, end_time DATETIME, status TINYINT DEFAULT 0 COMMENT '0未开始 1进行中 2已结束' );4.2 三个最容易设计错的地方
第一,软删除和唯一索引打架。course_enroll上建了uk_course_student唯一索引,如果学生退课执行的是deleted=1的软删除,再次选课插入时就会撞唯一键报错。解决办法要么是退课时真删,要么把唯一索引改成包含deleted字段的复合索引(但这样同一个人多次退选会产生多条记录)。我的建议是关联表用物理删除,主数据表用软删除,逻辑最干净。
第二,状态字段用数字还是字符串。用TINYINT存枚举值省空间,但可读性差,直接看数据库根本不知道status=2是什么意思。折中方案是数据库存数字,在代码里用枚举类映射,同时把映射关系写进文档。别用中文直接当字段值,编码问题会让你哭。
第三,时间和时区。MySQL 8.0 的连接串如果不指定serverTimezone,DATETIME写进去和读出来可能差8小时。统一用Asia/Shanghai,并且后端所有时间字段用LocalDateTime而不是Date,能省掉很多转换烦恼。
5. 核心模块的落地实现要点
5.1 登录鉴权:令牌方案的两个隐藏坑
登录这块,形式上是"账号密码换一个凭证,后续请求带着凭证走",但细节决定成败。密码存储必须用 BCrypt 这类带盐的慢哈希,注册时passwordEncoder.encode(rawPassword),登录时用matches比对。绝对不要用 MD5,网上那些彩虹表一查就出结果,答辩老师如果懂安全,看到MD5基本就判定你不了解基本常识了。
令牌用 JWT,结构是"头.载荷.签名"三段,服务端不存状态,靠签名验证真伪。好处是天然支持分布式,坏处是注销做不了——令牌签发出去就有效,除非等它自己过期。解决办法是引入 Redis 存一份黑名单,注销时把令牌的jti或者令牌本身写进去并设置与过期时间相同的 TTL,每次校验时多查一次。这个方案我在实际项目里用过,成本很低,一次Redis查询的事。
第二个坑是权限校验不能只做前端。前端根据角色隐藏菜单、隐藏按钮,这只是体验优化,真正的拦截必须放在后端。做法是自定义一个注解(比如@RequireRole("TEACHER"))配一个拦截器,在方法执行前读取当前用户的角色列表做判定,不通过直接抛异常返回403。我见过只做前端拦截的项目,用 Postman 直接调接口就能删别人的课程,这种漏洞在答辩演示时被点出来,场面会很尴尬。
5.2 视频与文件上传:几个必须设的阈值
文件上传看着简单,实际是全套系统里最容易出安全问题的地方。最少要加四道限制:类型白名单(只允许 mp4、pdf、jpg、png 等明确列出的扩展名,不要用黑名单)、大小上限(视频单文件500MB以内,图片5MB以内,讲义20MB以内)、文件名重写(用UUID加时间戳重命名,绝对不能用用户上传的原始文件名,否则可能出现路径穿越)、存储路径隔离(上传目录不要放在Web根目录下直接可访问,要通过接口鉴权后再输出流)。
视频这块有个现实选择:小项目不建议自己搭流媒体服务,直接把 mp4 文件放到静态资源目录,前端用<video>标签加载就行。代价是没有防盗链、没有转码、大文件加载慢。如果只是演示,够用;如果想让体验好一点,可以引入对象存储,但这就引出外部依赖的问题了。我的建议是本地存储 + 后端做一次鉴权跳转,也就是请求视频地址时先经过一个接口校验学生是否选了这门课,通过后再302到真实地址。虽然防不住有心人,但至少体现了你有安全意识。
视频时长这个字段,很多人手填,容易错。可以用 ffmpeg 在上传后异步抽取,命令大致是ffmpeg -i input.mp4然后解析输出里的 Duration 行。如果嫌麻烦,让前端在加载元数据时读video.duration一起提交上来,也是可行的偷懒方案。
5.3 学习进度:怎么算才不算自欺欺人
进度计算是这套系统里最容易被忽略、又最能体现设计思考的地方。最粗暴的做法是"小节看完打勾,进度=已完成小节数/总小节数",简单但有个致命问题:学生把视频拖到最后就算看完了。
稍微好一点的做法是按观看时长算:前端每隔15秒上报一次当前播放位置和本次增量,后端累加有效观看秒数,当watched达到duration的90%以上时标记该小节完成。为什么要卡90%而不是100%?因为片尾往往有黑屏或者字幕,卡100%会有人永远学不完。这个90%的阈值,写进开题报告时可以解释为"预留片尾容错空间",是个加分点。
还有个细节:拖动进度条怎么识别。前端可以监听seeking事件,如果一次跳转超过30秒,本次增量不计入有效观看。这个逻辑放在前端做,同时后端做一次校验——单次上报的增量不允许超过上报间隔的1.5倍,超了就按间隔值算。双保险,防君子也防小人。
课程总进度的更新时机,我一般放在每次小节进度变更后同步计算:progress = 已完成小节数 / 总小节数 × 100,保留两位小数。计算本身不复杂,注意别在循环里查数据库就行。
5.4 在线考试:组卷、判分与防作弊的分寸
组卷有两种思路,固定卷和随机卷。固定卷是老师手动选题、手动排序,实现简单,试卷内容稳定。随机卷是按题型和难度从题库里抽,每人卷子不同,防作弊效果好。我建议两种都支持,exam_paper加一个mode字段区分,随机卷在开考时生成一份"考生-题目"的快照记录,这样后续题库改动不会影响已生成的卷子。
判分逻辑按题型分:单选、判断直接字符串比对;多选要求完全一致才给分(也可以做半对给一半分,看老师要求);简答和论述无法自动判,标记为"待批阅",由老师手动打分。这里有个数据一致性问题:总分必须是所有题目得分之和,不能各自独立存。我的做法是交卷时先算客观题分数,主观题留空,老师批阅后重新汇总一次总成绩,两次汇总走同一个方法,避免口径不一致。
防作弊的分寸要把握好,做得太狠会误伤。切屏检测(监听页面失焦)可以记录次数并在交卷时提示,但不要直接判零分;限时交卷必须做,到点自动提交;题目和选项乱序可以做,成本低效果好。至于摄像头监考那种,超出这个项目的范围了,不要碰。
// 客观题判分的核心逻辑,多选按字母升序比较 public int judge(Question q, String userAnswer) { if (userAnswer == null || userAnswer.trim().isEmpty()) return 0; String std = q.getAnswer().trim().toUpperCase(); String usr = userAnswer.trim().toUpperCase(); if (q.getType() == 2) { // 多选题,排序后比较,避免顺序不同判错 char[] a = std.toCharArray(); char[] b = usr.toCharArray(); Arrays.sort(a); Arrays.sort(b); return Arrays.equals(a, b) ? q.getScore() : 0; } return std.equals(usr) ? q.getScore() : 0; }这段代码没什么高深技巧,但"多选题排序后比较"这个细节,是很多人第一次写会漏掉的——用户选BA,标准答案AB,不排序就判错了。
6. 从零跑起来:环境搭建与联调的实操顺序
6.1 环境依赖的安装与版本核对
拿到一份源码,第一件事不是打开IDE,而是把环境准备好。按这个顺序来不容易乱:先装JDK,配好JAVA_HOME,命令行敲java -version确认输出;再装Maven,配MAVEN_HOME,敲mvn -v看是否认到了正确的JDK;然后装MySQL,建库、导入SQL脚本;最后装Node,敲node -v和npm -v。
数据库导入这一步,很多人栽在编码上。建库时明确指定字符集:CREATE DATABASE learn_platform DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci;,用utf8mb4而不是utf8,因为后者在MySQL里其实是三字节的,存不了emoji和一些特殊字符,虽然学习平台不一定用得上,但一步到位省事。
配置文件里的连接信息要逐项核对:地址、端口、库名、用户名、密码、时区。useUnicode=true&characterEncoding=utf8&serverTimezone=Asia/Shanghai这几个参数建议直接带上,省得后面出问题再回头找。
6.2 后端启动与常见报错定位
后端启动失败,报错信息其实已经把答案写在脸上了,关键是会看。端口被占用:Port 8080 was already in use,要么改端口,要么去任务管理器干掉占用进程。数据库连不上:Communications link failure,先确认MySQL服务起没起,再确认账号密码和库名。找不到Mapper:Invalid bound statement (not found),检查Mapper接口和XML的包路径是否一致,或者MyBatis-Plus的@MapperScan有没有配对。
启动成功后,先别急着开前端,用浏览器或Postman直接打一个最简单的接口,比如登录或者课程列表,确认返回结构是你要的。这一步叫"接口先行",能帮你把前后端问题隔离开——如果接口本身就不对,前端再怎么调也是白搭。
6.3 前端启动与跨域处理
前端npm install之后npm run dev,最常见的两个问题:依赖装不上(换镜像源或者删掉node_modules重来)、跨域被拦。跨域的本质是浏览器的同源策略,解决办法有两个方向:后端加CORS配置,或者前端配代理。开发阶段我更推荐前端代理,在vue.config.js里配devServer.proxy,把/api前缀转发到后端地址,这样前端代码里所有请求都写成相对路径,上线时改Nginx配置就行,不用改代码。
// vue.config.js 里的开发代理配置 module.exports = { devServer: { port: 8081, proxy: { '/api': { target: 'http://localhost:8080', changeOrigin: true, pathRewrite: { '^/api': '' } } } } }联调阶段最高效的做法是打开浏览器开发者工具的Network面板,看每个请求的状态码和响应体。401是没带令牌或者令牌过期,403是权限不够,500是后端异常(这时候去看后端控制台日志),404是路径写错。把这几个状态码的含义记牢,排查效率能翻倍。
6.4 打包部署:把项目变成一个能演示的成品
演示的时候总不能开着IDE跑npm run dev,得打包。前端npm run build生成dist目录,后端mvn clean package生成 jar 包。部署到服务器上,用Nginx托管前端静态文件并反向代理后端接口,一条java -jar xxx.jar把后端跑起来。
Nginx配置的关键点是:前端路由用 history 模式的话,要加try_files $uri $uri/ /index.html;,否则刷新页面会404;/api的代理要指向后端真实端口;上传目录要做静态映射或者交给后端接口输出。这些配置项不多,但每一项漏了都会在演示当天出问题,建议提前一天完整走一遍部署流程。
7. 开题报告怎么写才经得起追问
7.1 研究现状部分:别写成产品介绍
开题报告里"国内外研究现状"这一节,最常见的写法是罗列一堆平台名字然后说"功能很强大",这属于无效内容。有效的写法是按技术路线归纳:早期是基于服务端渲染的课程管理系统,特点是部署简单但交互弱;后来随着前后端分离普及,交互体验提升,但对接口设计要求更高;再往后是移动端和碎片化学习的兴起,对响应式布局和断点续播提出了新要求。归纳完技术路线,再指出"当前面向小型教学场景的轻量级方案在进度追踪和考试闭环上仍有优化空间",你的选题理由就立住了。
7.2 研究内容与技术路线必须一一对应
这是答辩时被追问最多的地方。很多人的研究内容写了五条,技术路线画了三个框,两者对不上,一问就露馅。正确做法是让它们严格映射:研究内容写"课程内容组织与展示",技术路线里就得有对应的"三层课程结构设计 + 树形目录渲染";研究内容写"学习行为追踪",技术路线里就得有"心跳上报 + 有效时长判定 + 进度汇总"。
我一般建议研究内容控制在4到5条,每条都能在系统里找到对应的模块,每条都能说出用了什么技术、解决了什么难点。宁可少写两条写透,也别写八条全是空话。
7.3 创新点怎么写才不显得假
本科或专科层次的毕设,硬凑"理论创新"是自找麻烦。可行的写法是应用层面的改良,比如:针对视频学习中"拖动进度条刷进度"的问题,设计了基于有效观看时长与跳转行为识别的进度判定机制;针对随机组卷后题库变动导致的历史试卷不一致问题,采用开考时生成题目快照的方式保证数据可追溯。这类表述有具体问题、有具体做法、有具体效果,比"采用了先进的前后端分离架构"这种话强一百倍。
7.4 进度安排与参考文献的细节
进度安排别写成"第一个月完成需求分析,第二个月完成编码"这种。按周拆,并且把"完成数据库设计与建表""完成登录鉴权与权限拦截""完成课程模块前后端联调"这种可验收的节点写进去,显得你是有计划地推进而不是临时抱佛脚。
参考文献挑近三到五年的,至少包含一到两篇期刊论文、一本相关技术书籍,格式统一。数量不用多,8到12篇足够,但每一篇最好都跟你写的内容有点关系,别全是凑数的。
8. 常见问题速查与踩坑记录
下面这些是我在实际操作和帮人排错过程中反复遇到的,按现象、原因、处理方式整理成表,遇到问题时可以先扫一眼。
| 现象 | 大概率原因 | 处理方式 |
|---|---|---|
启动报ClassNotFoundException或版本不兼容 | JDK与框架版本不匹配 | 核对pom.xml里的编译版本和本机JDK |
| 数据库中文变问号 | 库或表字符集不是 utf8mb4 | 重建库并显式指定字符集,连接串加编码参数 |
| 登录成功但刷新页面就退出 | 令牌只存在内存里,没做持久化 | 存 localStorage,并在请求拦截器统一注入 |
| 接口返回401但明明登录了 | 请求头没带 Authorization | 检查 axios 拦截器是否漏配 token |
| 上传大文件报 413 | Nginx 或框架的请求体大小限制 | 调大client_max_body_size和 multipart 配置 |
| 视频进度条拖不动 | 后端未支持 Range 请求 | 静态资源交给 Nginx 处理,或手动实现分段响应 |
| 删除课程后关联数据还在 | 未做级联处理 | 业务层显式删除或标记,别依赖数据库外键 |
| 分页查询总数不对 | count 语句带了 limit | 检查是否复用了同一个 wrapper |
| 考试交卷后成绩为0 | 答案未保存或判分字段为空 | 检查提交结构,加日志打印原始答案 |
| 前端打包后白屏 | 静态资源路径不对 | 配置publicPath为./或正确的子路径 |
除了表里这些,还有几个属于"文档不会写但很要命"的经验。第一,养成打日志的习惯,尤其是接口入口和关键分支,log.info打一下入参和结果,出问题时不用靠猜。日志级别别全开 debug,生产环境会拖慢速度。第二,改代码前先提交一次版本,哪怕是本地git,也比改崩了找不回来强。第三,联调时接口文档要跟着改,前后端两个人各写各的,最后对不上字段名,浪费的时间远超写文档的时间。
还有一个特别容易被忽略的点:演示数据要提前准备。空数据库点进去全是空白页,演示效果极差。提前造几条课程数据、几道题目、几个学生的学习记录,让每个页面都有内容,整个系统的完成度看起来会完全不一样。这件事花不了半小时,但收益巨大。
9. 代码结构组织与后续可扩展的方向
源码目录怎么摆,其实也影响别人(包括答辩老师)对你项目的判断。后端建议按业务模块分包:controller、service、mapper、entity、dto、vo、common、config,其中common放统一返回体、全局异常处理、常量、工具类。统一返回体这个事,一开始就定好,格式是{code, message, data},所有接口都遵循,前端处理响应时只写一套逻辑,省心。
前端按视图和组件分层:views放页面,components放可复用组件,api放接口封装,router放路由和守卫,store放全局状态。路由守卫这块,可以在beforeEach里判断令牌是否存在,不存在直接跳登录页,比在每个页面里写判断干净得多。
如果这套系统做完还想继续深入,有几个方向是投入产出比比较高的:引入缓存,把课程列表、分类这类读多写少的数据放到 Redis,顺便在开题或论文里写一节"性能优化实践";加数据统计看板,用 ECharts 画学习时长分布、成绩分布、章节完成率,视觉效果和实用性都不错;做移动端适配,用响应式布局让页面在手机上也能用,成本不高但显得完整。至于微服务化、分布式部署这些,等你真正有并发压力了再说,现阶段加进来只会增加复杂度,不会增加分数。
我自己带项目时的体会是,这类系统的技术难度并不在某一项技术上,而在于把十几个平凡的功能串成一个自洽的整体:数据要对得上、权限要拦得住、异常要兜得住、演示要跑得顺。把这四件事做扎实,比多写三个花哨模块有价值得多。真正拉开差距的往往不是你会不会用某个框架,而是你有没有认真想过"学生退课之后那条学习记录到底该不该留"这种没人提醒但一定会被问到的问题。