1. 脱坑指南:这套系统的设计与技术选型思路
前后端分离的“大学生计算机基础网络教学系统”,名字听起来就是一套很典型的课程作业型项目,但真拿到手你会发现,它远不止“能跑起来”那么简单。整个系统围绕SpringBoot + Vue + MyBatis + MySQL展开,核心场景是:教师端上传课件、布置作业、批改评分;学生端在线看课件、提交作业、查成绩;管理员端管用户、管课程、管公告。这基本覆盖了绝大多数高校计算机基础课的教学管理需求,也是企业里真实项目中“管理后台 + 业务前台”最常见的形态。
先说结论:这套系统适合三类人参考——正在做毕业设计或课程设计的学生、想快速入门前后端分离开发模式的初学者、以及需要在内部快速搭建一套教学管理Demo的团队。
我为什么会推荐这种组合?SpringBoot 负责后端接口和业务逻辑,Vue 负责页面交互和用户体验,MyBatis 负责数据库操作映射,MySQL 存数据,中间通过 RESTful API 通信。这个组合的好处是:每一层都足够简单、足够经典,出错时你能快速定位问题在哪个环节。尤其是 MyBatis,它比 JPA 更直白,SQL 写死在 XML 或注解里,排查慢查询、复杂联表的时候一眼就能看懂,不像 Hibernate 那样需要先搞懂一堆自动生成的 SQL 在干什么。
整套系统的数据流向大概是这样的:Vue 页面发 axios 请求到 SpringBoot 的 Controller,Controller 调 Service 层,Service 调 Mapper,Mapper 通过 MyBatis 执行 SQL 操作 MySQL 数据库,查询结果再一层层返回前端渲染。这个链路不复杂,但每一步的细节都可能成为坑,后面我会逐个拆解。
2. 项目整体设计与功能拆解
2.1 角色权限划分
系统分为三种角色,权限边界一定要在前后端同时控制,不能只依赖前端按钮隐藏,这是最基本的底线。
- 学生端:登录后可查看已加入课程的课件列表、在线预览教师上传的PDF/PPT、提交作业(支持文本和文件)、查看作业批改结果和课程公告、修改个人信息。
- 教师端:登录后可创建课程、维护课程基本信息、上传/删除课件、发布作业、设定作业截止时间、在线批改学生提交的作业、给学生打分并填写评语。
- 管理员端:负责用户管理(新增教师/学生账号、重置密码、封禁账号)、课程管理(查看所有课程、强制下架违规内容)、公告管理(发布全校级别的通知)、数据统计(查看选课人数、作业提交率的简单汇总)。
注意:权限校验不只是“显示或不显示按钮”的问题。后端接口必须用拦截器或注解校验角色,防止学生直接调用教师的接口。实际开发中很多系统只做了前端路由守卫,后端接口裸奔,这是安全设计的大忌。
2.2 页面结构规划
页面设计遵循“管理端 + 用户端”双端分离的思想,但Vue项目只有一个,通过路由和登录状态来区分跳转:
- 登录页:账号密码登录 + 角色选择(其实是为了简化登录逻辑,实际生产环境应由后端根据账号自动识别角色)。
- 学生首页:课程列表、课件列表、作业列表、公告栏。
- 教师工作台:我的课程、课件管理、作业管理、批改列表。
- 管理后台:用户管理表格、课程审核表格、公告编辑器。
这里有一点实践心得:不要把前端路由设计成“一套界面适配所有角色”,虽然技术上可行,但开发和调试效率极低。我用 Vue Router 的 meta 字段记录路由需要的角色,在路由守卫里判断当前用户的角色权限,不匹配直接重定向到 403 页面。这样每个角色的页面代码独立存在,后续扩展功能时互不干扰。
2.3 核心业务流程
学生选课 → 教师发布课件 → 学生学习课件 → 教师发布作业 → 学生提交作业 → 教师批改打分 → 学生查看成绩。这个流程听起来平淡无奇,但落地时你要考虑几个关键点:
- 选课与课程加入机制:是学生自由选课,还是教师指定学生?系统里我采用“学生通过课程邀请码加入课程”的方式,既能控制人数,又不需要做复杂的选课审批流程。
- 作业提交的覆盖机制:允许学生在截止时间前重复提交,后一次覆盖前一次,数据库里保留最新一条记录即可。大批量文件存储需要提前规划路径和命名规则。
- 批改流程的状态流转:作业状态分为“未提交”“已提交待批改”“已批改”“已逾期”。每一种状态对应前端不同的按钮和后端不同的接口策略。
3. 核心技术拆解与数据库设计
3.1 SpringBoot 后端分层
后端我按经典的四层结构组织:Controller(接口层)、Service(业务层)、Mapper(数据访问层)、Entity(实体类)。很多人觉得分层麻烦,但一旦项目超过十几个接口,不分层基本没法维护,到处是互相调用的逻辑,改一个需求要动三个文件。
- Controller 层只做参数接收和结果封装,不写业务逻辑,用 @RestController 注解暴露接口。
- Service 层处理业务逻辑,比如判断作业是否过期、校验课程权限、组装返回数据,用 @Service 标注,事务控制 @Transactional 加在需要保证原子性的方法上。
- Mapper 层是接口,配合 XML 文件写 SQL,MyBatis 的标志性用法。
- Entity 类对应数据库表结构,字段命名和表字段保持一致的映射关系,用驼峰命名开启 map-underscore-to-camel-case 配置。
在 pom.xml 里,你需要引入的核心依赖包括:spring-boot-starter-web、mybatis-spring-boot-starter、mysql-connector-java、lombok。此外还需要一个 JWT 库来做登录令牌,实际项目中我会选择 jjwt 或 hutool 自带的 JWT 工具,避免重复造轮子。
3.2 Vue 前端组织方式
Vue 侧我使用 Vue 2 + Element UI 的组合。虽然 Vue 3 已经很成熟,但 Element UI 对 Vue 2 的支持最完善,很多模板代码可以直接用,遇到问题时网上的案例也多。项目脚手架用 Vue CLI 生成,后续打包发布比较顺滑。
前端目录按视图和组件拆分:
- src/views:页面级组件,比如 Login.vue、TeacherCourse.vue、AdminUser.vue。
- src/router:路由配置文件,集中管理所有页面映射和权限守卫。
- src/api:用 axios 封装的后端接口调用模块,按业务模块拆文件,比如 course.js、homework.js、user.js。
- src/utils:工具类,包含请求拦截器、token 存取、日期格式化等。
请求拦截器是很多初学者容易忽略的点。axios 的 request 拦截器统一在 headers 里添加 Authorization 字段,值为登录后存到 localStorage 的 token;response 拦截器统一处理 401 状态码,当 token 失效时自动跳回登录页。这样业务代码里每次调用接口,只需要关心数据本身,不用重复写 token 逻辑。
3.3 数据库表结构
数据库是我认为整个项目中设计价值最高的一块,直接决定后续开发是否痛苦。系统核心表一共 8 张:
- user:用户表,字段包含 id, username, password, real_name, role, status, create_time。密码存储不能用明文,要加密,我选用 BCrypt。
- course:课程表,字段包含 id, name, description, teacher_id, invite_code, create_time。
- course_student:选课关系表,记录学生与课程的关联,字段包含 id, course_id, student_id, join_time。
- course_file:课件表,字段包含 id, course_id, file_name, file_url, upload_time。
- homework:作业表,字段包含 id, course_id, title, content, deadline, create_time。
- homework_submission:作业提交表,字段包含 id, homework_id, student_id, submit_content, file_url, submit_time, status, score, comment, review_time。
- notice:公告表,字段包含 id, title, content, publisher_id, publish_time。
- admin_log:操作日志表,记录管理员关键操作,字段包含 id, admin_id, operation, detail, operate_time。
提示:课件表的 file_url 字段不要在数据库里拼接完整访问路径,只存相对路径,比如 “/files/20240112/xxx.pdf”。这样系统换域名、换端口时不需要改数据库,部署到新环境也省去很多麻烦。这是我踩过坑之后的总结,之前直接存了 “http://localhost:8080/files/...” 这种绝对地址,后来换服务器全部失效。
3.4 作业提交的文件存储策略
作业要支持文件上传,这里不引入分布式文件系统,本地磁盘存储就足够。在上传接口中,我会按“年月日/随机文件名”的目录结构保存文件,避免文件名冲突。比如用户上传一个 “report.docx”,保存后实际路径是 “/root/teaching-system/upload/20250112/8702374821.docx”,数据库里存这个相对路径。
读取文件时通过一个专门的 FileController 映射静态资源路径,前端拿到的完整 URL 是后端拼接好的。上传时还要对文件类型做白名单校验,只允许 doc、docx、pdf、zip、rar、jpg、png,并且做大小限制,我这里是单文件 50MB,超过直接返回错误信息。
4. 核心功能模块的实现细节
4.1 基于 JWT 的登录认证
登录认证我用了比较经典的 JWT 方案。后端在用户登录成功后,根据用户 ID、角色、过期时间生成一个签名后的 token 返回给前端。前端拿 token 存到 localStorage,每次请求带上,后端通过过滤器校验 token 的合法性和有效期。
JWT 的签名密钥要单独配置在 application.yml 里,不要硬编码在代码中,并且过期时间统一设置为 24 小时。值得注意的是,JWT 是无状态的,服务端不能主动让某个 token 失效,所以对于“修改密码后强制下线”这种需求,只靠 JWT 不够,你可以在用户表加一个 token_version 字段,生成 JWT 时把版本号放进去,密码修改后版本号加一,旧 token 在过滤器里校验版本号匹配直接被拒绝。这个细节我强烈推荐加,因为很多课程设计里的“修改密码”功能就是个摆设,改完密码旧 token 依然有效,安全上是硬伤。
4.2 课程与选课逻辑
教师创建课程时,后端自动生成六位邀请码,这个邀请码用随机字母和数字组合,排除易混淆的 0/O 和 1/I。学生加入课程时输入邀请码,后端先查邀请码对应的课程是否存在,再判断该学生是否已经加入,避免重复关联。
选课关系表的存在让课程-学生成为多对多关系。学生的课程列表通过联表查询获取,SQL 大概是:
SELECT c.* FROM course c INNER JOIN course_student cs ON c.id = cs.course_id WHERE cs.student_id = #{studentId}这里要注意:如果用 MyBatis 的嵌套结果映射来做多对多,XML 配置会比较绕。实际项目里我倾向于直接用联表查询返回 DTO 而不是实体类,减少不必要的层级嵌套,代码反而更直观。
4.3 作业发布与提交
教师发布作业时,需要设置截止时间。这里有一个常见业务陷阱:服务器时间和客户端时间不一致。前后端分离的项目里,后端应统一处理时间字段,前端提交的 deadline 最好是时间戳或者统一格式的字符串,后端解析后转成服务器本地时间,不要依赖前端传来的时间字段做直接存储。
学生提交作业时,后端先检查当前时间是否超过截止时间。超过的话,即使接受到请求,也要返回“已逾期,无法提交”,并且状态置为逾期。这个校验必须放在后端,不能只靠前端按钮禁用。前端任何人都可以绕过按钮直接发请求,后端才是最后的防线。
4.4 在线批改实现
教师打开待批改列表,看到学生的提交内容,输入分数和评语。批改接口是典型的更新操作,按提交记录 ID 更新 score、comment、status、review_time 四个字段。前端提交后刷新列表,已批改的记录变成可查看状态,学生端就能看到成绩了。
批改过程中要注意的一个问题是:学生在教师批改后是否允许重新提交?正常情况下不允许,但考虑实际教学场景,教师批改完发现学生有明显抄袭或文件损坏,应允许教师主动“打回重交”,把提交状态重置为“待提交”。这个功能看起来小,但在真实使用中非常必要,我第一次做系统时没设计这个功能,后来教师只能让学生找管理员改数据库,非常痛苦。
5. 完整源码结构分析与本地部署
5.1 后端代码结构一览
后端项目标准的 Maven 结构:
src/main/java/com/example/teachingsystem/ ├── controller/ # 控制层,接收前端请求 │ ├── AuthController.java │ ├── CourseController.java │ ├── HomeworkController.java │ ├── FileController.java │ └── AdminController.java ├── service/ # 业务层,处理具体逻辑 │ ├── UserService.java │ ├── CourseService.java │ ├── HomeworkService.java │ ├── FileStorageService.java │ └── NoticeService.java ├── mapper/ # MyBatis 数据访问层 │ ├── UserMapper.java │ ├── CourseMapper.java │ ├── HomeworkMapper.java │ └── NoticeMapper.java ├── entity/ # 实体类,对应数据库表 │ ├── User.java │ ├── Course.java │ ├── Homework.java │ └── CourseFile.java ├── dto/ # 数据传输对象,接口出入参 ├── config/ # 配置类,包含 JWT 拦截器、跨域配置 ├── util/ # 工具类,JWT 工具、文件上传工具 └── TeachingSystemApplication.java # 启动入口5.2 前端代码结构一览
Vue 项目结构:
src/ ├── api/ # 接口封装 │ ├── user.js │ ├── course.js │ └── homework.js ├── assets/ # 静态资源 ├── components/ # 公共组件 │ ├── FileUpload.vue │ └── Pagination.vue ├── router/ │ └── index.js # 路由配置与守卫 ├── utils/ │ └── request.js # axios 封装 ├── views/ │ ├── Login.vue │ ├── student/ │ │ ├── StudentHome.vue │ │ ├── CourseDetail.vue │ │ └── HomeworkDetail.vue │ ├── teacher/ │ │ ├── TeacherDashboard.vue │ │ ├── CourseManage.vue │ │ ├── HomeworkPublish.vue │ │ └── HomeworkReview.vue │ └── admin/ │ ├── AdminDashboard.vue │ └── UserManage.vue └── App.vue5.3 本地环境准备
你需要准备以下工具:
- JDK 1.8 或以上版本,推荐 1.8 或 11,兼容性最稳。
- Maven 3.6 以上,用于管理后端依赖和打包。
- Node.js 14 以上和 npm,Vue 项目构建依赖。
- MySQL 5.7 或 8.0,建议 8.0,字符集统一 utf8mb4。
- 开发工具推荐 IntelliJ IDEA 和 VSCode,分别打开后端和前端项目。
注意:JDK 版本不要盲目选最新。好几个版本的 JDK 刚发布时会有兼容性问题,而 SpringBoot 2.x 对高版本 JDK 的支持是滞后的。如果项目里 SpringBoot 用的是 2.5 左右,建议直接配 JDK 8,省去一堆莫名的编译错误。
5.4 详细启动步骤
第一步:初始化数据库。在 MySQL 中创建一个名为 teaching_system 的数据库,然后导入项目附带的 sql 文件。如果你是从零写的项目,用 Navicat 或命令行执行建表语句即可,注意导入时选择正确的字符集,防止中文乱码。
第二步:修改后端配置。打开 application.yml,将数据库连接串、用户名、密码改成自己的本地配置:
spring: datasource: url: jdbc:mysql://localhost:3306/teaching_system?useUnicode=true&characterEncoding=utf8&useSSL=false&serverTimezone=Asia/Shanghai username: root password: 你自己的密码 servlet: multipart: max-file-size: 50MB max-request-size: 100MB mybatis: mapper-locations: classpath:mapper/*.xml configuration: map-underscore-to-camel-case: true file: upload-dir: /your/path/to/upload第三步:启动后端。在项目根目录执行mvn spring-boot:run或者在 IDEA 里直接运行带有 main 方法的启动类。看到 “Started TeachingSystemApplication” 的日志就证明启动成功,此时后端服务跑在 8080 端口。
第四步:启动前端。在 vue 项目根目录打开终端,先npm install安装依赖,这一步会花几分钟,取决于网络状况。安装成功后执行npm run serve,默认端口 8081,在浏览器访问 localhost:8081。
第五步:配置代理解决跨域。本地开发时后端是 8080,前端是 8081,存在跨域。在 vue.config.js 里配置 devServer 代理,把/api开头的请求转发到 8080:
module.exports = { devServer: { port: 8081, proxy: { '/api': { target: 'http://localhost:8080', changeOrigin: true } } } }这样前端代码里的请求路径写/api/login这样的相对路径即可,不用拼接完整的后端地址。后端接口统一加上/api前缀,也便于在拦截器里统一处理。
5.5 打包部署到服务器
开发完成之后,后端通过mvn clean package -DskipTests打 jar 包,在服务器上用java -jar teaching-system.jar启动。前端通过npm run build生成 dist 目录,用 Nginx 或 Apache 托管静态文件。
部署时我建议:用 Nginx 监听 80 端口,将静态文件路径指向 dist,将/api路径反向代理到后端的 8080 端口。Nginx 配置核心片段:
server { listen 80; server_name your-domain.com; root /opt/teaching-system/dist; index index.html; location / { try_files $uri $uri/ /index.html; } location /api { proxy_pass http://127.0.0.1:8080; proxy_set_header Host $host; proxy_set_header X-Real-IP $remote_addr; } location /files { proxy_pass http://127.0.0.1:8080; } }这里有两个关键点:一是try_files配置,必须保留,否则 Vue Router 在 history 模式下刷新页面会返回 404;二是文件访问路径/files要单独反向代理,否则课件图片和作业文件都加载不出来。
6. 常见问题与排查技巧实录
6.1 前端请求后端报错 404
大多数情况是代理配置没生效,或者前端请求路径和后端接口路径不一致。先打开浏览器开发者工具,看 Network 面板,确认请求的完整 URL 是否正常。如果是 localhost:8081/api/login 这样的相对路径并通过代理转发,那问题大概率在后端。如果请求直接打到 8080,先检查后端是否启动、接口路径是否匹配。
6.2 数据库导入中文乱码
MySQL 字符集问题非常经典。建库时用utf8mb4,连接串里一定要加characterEncoding=utf8,如果还乱码,检查表字段的字符集是否单独被设置为 latin1。在 Navicat 里查看表设计可以直接看到,顺手改成 utf8mb4 就行。还有一点,如果 SQL 文件本身是 GBK 编码导入的,用命令行导入时指定:
mysql --default-character-set=utf8mb4 -u root -p teaching_system < init.sql6.3 MyBatis 报 BindingException
最常见原因是 Mapper 接口在启动时没有扫描到。在启动类上增加@MapperScan("com.example.teachingsystem.mapper"),或者在每个 Mapper 接口上加@Mapper注解,二选一即可。另一个原因是 XML 文件里的 namespace 和接口全限定名不一致,这个错误很隐蔽,编译时不报错,启动时才报错。
6.4 刷新前端路由出现 404
这是因为后端没有配置 fallback。如果是 Nginx 部署,前面已经提到用try_files解决。如果是在开发模式下出现,要确保 Vue Router 使用的是 hash 模式,而不是 history 模式,history 需要后端支持。我自己的项目一般在开发环境用 hash 模式保底,生产环境用 history 模式配合 Nginx,两者配合比较舒服。
6.5 上传文件成功但访问报 404
前端显示上传成功,数据库里也有记录,但访问文件 URL 时 404。大概率是后端没有把上传目录映射为静态资源。我习惯在配置类里注册 ResourceHandler:
@Configuration public class WebMvcConfig implements WebMvcConfigurer { @Override public void addResourceHandlers(ResourceHandlerRegistry registry) { registry.addResourceHandler("/files/**") .addResourceLocations("file:" + uploadDir + "/"); } }这里的 uploadDir 要与 application.yml 里的 file.upload-dir 保持一致,路径末尾要带斜杠。
6.6 打包后发现 Date 少 8 小时
这个典型问题是数据库连接串里的 serverTimezone 没用对。如果你在亚洲时区,连接串要设置serverTimezone=Asia/Shanghai。同时 MySQL 驱动版本不同表现会有差异,8.x 驱动建议显式指定时区。
6.7 前端调用接口报跨域错误
生产环境如果前后端不同源,需要后端配置 CORS,或者 Nginx 做反向代理(推荐后者)。本地开发用 devServer 代理是标准方案,不需要额外配置 CORS。注意代理的 target 一定不能写错,写成localhost:8080没问题,但要确保 8080 端口真的是后端服务在监听,而不是其他程序占用了端口。
7. 从项目中学到的东西与扩展方向
我前前后后看过不少类似的课程设计,说实话,这类的教学系统是最典型的“麻雀虽小五脏俱全”的项目。做完这套系统,你对 SpringBoot 的自动装配、MyBatis 的 SQL 映射、Vue 的组件通信和路由守卫、MySQL 的表设计都会有比较完整的认知,尤其是前后端分离模式下如何通过 JWT 保持会话、如何优雅地处理文件上传,这些都是企业开发中的基本功。
如果我想给它加功能,优先级最高的是分页搜索的优化。现在列表接口大多用 PageHelper 或者手写 limit 实现,简单够用。但要应对几百门课程、几万条作业提交记录,需要给列表查询增加缓存,比如用 Redis 缓存热点课程信息。第二个值得扩展的是数据统计可视化,比如课程选课人数趋势、作业提交率柱状图,用 ECharts 在前端画图,后端配合提供聚合接口,整个系统的完整度会提升一个台阶。第三个是发送通知,作业发布或批改完成后,通过邮件或微信公众号模板消息推送给学生,这在实际教学环境中非常提升使用体验。
最后再分享一个我实践中的小技巧:开发这类系统时,数据库备份一定要做勤快一些,尤其是改表结构之前。我习惯每天开发结束前,把整个 teaching_system 数据库导出一份 sql 文件放在专门的目录里,标注日期。一旦当天改错了数据或者把表结构搞乱了,直接导入备份回滚,比手动去改数据高效太多。这套习惯在课程设计阶段可能显得多余,但到了生产环境维护阶段,那就是救命稻草。