资讯动态

SpringBoot+Vue3构建高校在线教学系统:从需求到部署全程解析

发布时间:2026/10/18 14:38:53 来源:尧图企业网站定制
1. 为什么大学生计算机基础课需要一套专属在线教学系统1.1 从线下到线上传统教学方式的真实痛点我带过几年大学计算机基础课也做过好几套课程管理系统最深的感受是计算机基础课是所有非计算机专业学生的必修课但它的教学管理难度一点也不“基础”。先说说线下教学常见的几个问题。大一新生来自全国各地入学时计算机水平差距相当大——有人高中就接触过Python编程和Office高级应用有人连文件保存到哪个盘都分不清。老师在课堂上只能按中等偏上的节奏走结果就是基础好的觉得无聊、基础差的跟不上。课后作业就更难管理了几百份Word版实验报告收上来、逐个批改体量大的时候要花掉整整一周时间。等到课程进入在线教学阶段问题更明显。当时我们临时借助各种通用直播软件和群聊工具课件发在群里、作业发在邮箱里、测验另外找在线问卷工具做——整个流程被切成了好几段学生常常找不到作业入口老师统计成绩时要手动汇总三四个渠道的数据。最尴尬的是考试环节计算机基础课的操作题无法通过普通的小测验工具完成而理论题又要防作弊、要限时、要随机出题通用工具根本满足不了。所以这套网络教学系统的定位非常明确面向大学生的计算机基础课程场景把课件管理、课程视频、作业提交与批改、在线考试、成绩统计这些环节全部放到一个系统里让学生和老师都只用一个入口完成所有操作。1.2 系统要服务的三种角色以及各自的核心需求在动手开发前我先画了三张“用户画像”表因为不同角色的需求是完全不同的设计顺序错了后面返工会很严重。角色核心诉求典型场景系统侧重点学生快速找课、看课件/视频、提交作业、查成绩每周上完理论课课后需要回看录播并完成实验报告学习路径清晰作业提交要有记录凭证教师维护课程资料、批改作业、发布考试、统计成绩开课时上传大纲课件每周批改实验报告批量处理能力成绩导出方便管理员管理系统基础数据、账号权限、日志监控学期初导入师生账号、配置系统参数权限边界清晰操作审计完整以最常见的作业提交场景为例学生需要的不只是“能传一个文件上去”而是知道自己哪门课的哪次作业还没交、提交后是否成功、教师是否已批改、得了多少分。教师在另一端要能一次看到全班同学的提交列表快速打分并写下评语最好还能一键导出Excel成绩表。这些需求看起来简单但在需求评审阶段如果不逐条列清楚开发时很容易做成“能传文件但两边都别扭”的半成品。1.3 这个标题里的“系统源码”意味着什么给谁用、怎么用很多刚接触这个项目的同学会问市面上现成的网课平台那么多比如各大MOOC平台为什么还要自己用SpringBootVue3从零写一套我个人的看法是这里有两层价值。第一层是教学管理落地价值。转型在线教学之后MOOC平台的公共课视频确实多但每个学校有自己的人才培养方案、教务排课节奏和考核标准直接拿公共平台的资源来做校内课程管理契合度并不高。尤其是成绩计算规则平时成绩30%、实验20%、期末50%这类比例和补交作业、重考这类特殊流程只有自己维护的系统才能灵活改。第二层是源码学习价值。对计算机专业的学生来说一个结构清晰、完整可运行的前后端分离教学项目本身就是绝佳的课程设计参考。你在学校里做课设时大概率也写过类似的管理系统但通常规模小、只有教科书里的例子比如只有一个用户表加一个学生信息表。而我们这套系统要承载学生、教师、管理员、课程、课件、作业、考试、成绩等十几张表之间的复杂关联前后端分离、带JWT认证几乎覆盖了企业级应用最典型的功模块。所以这篇博文主要写给两种读者一是需要在校园内部署远程教学平台的老师和技术人员二是想通过一个完整项目搞懂SpringBootVue3企业级开发的在校学生。阅读时建议带着“我要不要基于它改造”的思维去看而不是单纯一个月度代码。2. 项目整体策划与需求边界先想清楚再写代码2.1 功能模块拆解不做大而全只做高频刚需开始写代码前我花了两周时间梳理需求。看过太多团队一上来就设计了一堆花哨功能最后能用的不到一半。我的做法是先把业务部门教务、任课老师和高频使用者学生的真实动作列出来再按使用频次排序砍掉低频和伪需求。最后确定的核心模块有六个用户与权限管理区分学生、教师、管理员三种角色基于 Spring Security JWT 做认证和鉴权。课程管理教师创建课程、维护课程简介与封面学生选课/退课系统按选课关系形成学生-课程的关联。课件与视频资料管理教师上传课件Word、PPT、PDF、视频链接设定可见时间范围学生进入课程后查看并下载。作业管理与批改教师布置作业设置截止时间、附件要求学生提交作业支持多次提交、覆盖原记录教师在线批改、打分、写评语学生可查询成绩与评语。在线考试与题库管理教师录入选择题、判断题组成试卷发布学生限时答题系统自动判分客观题教师可手动批改主观题成绩自动汇总。统计与导出教师维度查看课程下每个学生的学习进度、作业完成率、考试平均分支持成绩导出为 Excel。这三个模块听上去不少但在实现时我刻意保持了每个模块的“最小闭环”。比如在线考试模块第一版只做单选判断两种题型不碰填空和大题。因为主观题需要人工批改界面涉及阅读内容、给分、写评语等复杂交互在项目第一版中容易拖慢整体进度。后来确实证明这个取舍是对的考试模块比预期更快上线也给后续扩展留下了余地。2.2 角色权限边界设计前端控制是习惯后端控制是底线在这个系统里权限设计贯穿了整个开发过程。最常见的错误是只在 Vue3 前端页面里根据角色隐藏菜单按钮但后端接口不做校验。演示的时候看起来没问题一到真实环境就会被人通过直接调用接口越权。我的做法是采用后端接口 前端菜单双重控制后端通过 Spring Security 配置接口路径的权限规则。比如/api/teacher/**开头的接口只有ROLE_TEACHER角色能访问/api/admin/**只有系统管理员能访问学生登录后访问这些路径会直接收到 403 响应而不是进入页面后才提示。这里的关键点是任何权限判断都不能依赖前端参数如某个字段传 teacherId必须从登录态中获取当前用户信息。前端通过 Vue Router 的全局守卫判断角色能访问哪些路由同时根据角色动态生成菜单。学生登录后系统只显示“我的课程、待办作业、考试成绩”教师登录后显示“课程管理、题库管理、作业批改”——菜单项本身就是按角色过滤后的结果。如果一个用户有多个角色比如既是教师又是管理员我预留了角色集合字段ListString roles在登录认证时一次性返回所有角色前端通过某个hasRole(ADMIN)方法判断权限。这种设计在后续扩展“助教”这类半权限角色时不用改底层权限模型只要新增角色并在路由表里配一次即可。2.3 为什么用 MySQL 而不是别的数据库场景决定了选择我在早期做课程管理系统时还在一家公司和别人一起评估过使用 MongoDB 或 PostgreSQL 的可能。最终 MySQL 胜出并不是因为它“最好”而是它最贴合这个项目的客观条件团队同学对 MySQL 最熟悉维护成本最低量产时遇到问题能快速定位。系统核心数据是强结构化数据用户、课程、成绩单、作业记录关系明确需要 ACID 事务保证。提交作业和批改成绩过程中涉及多张表同时更新事务的原子性非常重要。服务器预算有限MySQL 本身轻量、流行云数据库套餐也便宜权限管理成熟备份恢复文档丰富。在表结构设计上我坚持每张业务表都有create_time和update_time字段作业提交记录表额外保留submit_version字段。否则学生第一次提交的文件出错了第二次覆盖提交后老师就看不到学生的第一次文件了这很容易造成“压死提交”的争执。记录提交版本既保留了痕迹也能应对“老师我明明传过”这种常见交流问题。3. 数据库设计十几张表如何关联成一套完整业务3.1 核心表结构一览数据库是整个系统的地基设计不好后面写 MyBatis 的 SQL 会异常痛苦。我按照“用户体系 → 课程体系 → 作业/考试体系 → 成绩体系”的顺序拆解总共设计了 14 张表。这里挑最重要的几张说明用户主表sys_user字段类型说明idbigint主键usernamevarchar(50)登录账号passwordvarchar(100)BCrypt 加密后的密码real_namevarchar(50)真实姓名rolevarchar(20)STUDENT / TEACHER / ADMINcollegevarchar(100)学院学生选课筛选常用statustinyint0-禁用 1-启用密码没有用明文存储而是用了BCryptPasswordEncoder加密。这算是一个基本的安全底线但在课设项目里非常多人会忽略。之前见过有项目直接把密码明文存在数据库里然后被负责测试的同学在浏览器控制台看到网络返回里的password: 123456瞬间社死。那个测试同学后来还总是拿这件事开玩笑。课程表course和选课关系表course_selection是课程模块的关键。course课程名、课程代码如CS101、学分、任课教师ID、课程简介、封面图URL。course_selectionstudent_id和course_id联合唯一索引防止学生重复选同一门课。选课时间通过period_id字段关联教学周方便实现“只在规定时间范围内选课”的业务逻辑。作业模块是分两张表设计的homework作业标题、要求描述、所属课程ID、截止时间、满分分值、是否允许补交。homework_submission作业ID、学生ID、提交文件路径、提交时间、批改状态未提交/已提交/已批改、得分、教师评语、提交版本号。这里两个表分开的核心原因是作业是课程级数据提交是用户级数据。如果合并成一张大表布置一份作业时就要为全班每个学生生成一条记录选课100人就100条数据冗余不说后续增加补交逻辑也会很乱。分开后教师查询“某次作业谁没交”时用一条LEFT JOIN就能通过空值判断搞定。题库与考试表则是另一条线question_bank题型SINGLE/JUDGE、题干、选项A/B/C/DJSON字符串存、正确答案、分值、所属教师ID。exam考试名称、课程ID、开始时间、结束时间、总时长、总分、是否自动发布成绩。exam_paper试卷ID与题目ID的关联关系含每道题在试卷中的顺序和分值。exam_record每个学生的答题状态、开始时间、提交时间、客观题自动得分。3.2 设计时容易被忽视的三个细节第一所有多对多关系表必须加联合唯一索引。选课表、试卷题目表这种关系表不加唯一索引的话代码里漏一个判断就会产生重复数据。重复选课后学生端能看见同一门课两次成绩统计时还会把分数计算两遍排查起来很痛苦。第二文件存储的路径不能直接用用户上传的文件名。我在上传接口里用 UUID 重写了文件名保留原始文件名只存在数据库字段里。这样做的原因一是中文文件名在部分服务器上会出现编码问题二是防止路径穿越攻击。数据库里可以记录两列一列是存储路径UUID名另一列是用户原始文件名用于下载时设置下载名。第三考试成绩的敏感字段要有快照。考试发布后题目如果被老师误删或修改学生已提交的答卷会失去参照。因此exam_paper表里设计了question_snapshot字段生成试卷时把题干和答案序列化存入后续题目改动不影响已生成的试卷记录。这个“快照”思路来源于电商系统的订单快照在多对多动态数据场景下非常实用。3.3 MyBatis 的 XML 配置与联表查询实践项目持久层用了 MyBatis我对它的定位很清楚单表操作和基础CRUD用注解复杂动态查询用 XML。复杂的典型场景是教师端“作业批改列表”。这个页面的查询条件是按课程筛选某次作业 → 查出所有选课学生 → 逐一关联他们的提交记录 → 没有提交的学生也需要显示状态标记为未交。用一个 SQL 描述就是SELECT su.id AS student_id, su.real_name AS student_name, hs.submission_path, hs.status, hs.score, hs.submit_time FROM course_selection cs INNER JOIN sys_user su ON cs.student_id su.id LEFT JOIN homework_submission hs ON hs.homework_id #{homeworkId} AND hs.student_id su.id WHERE cs.course_id #{courseId}这里最关键的是LEFT JOIN的用法以选课表为驱动表提交表作为左连接的目标表。这样即使某个学生一次作业都没交他的记录依然会出现只是status字段为NULL。我第一次写这个查询时用了INNER JOIN结果作业批改列表里永远看不到尚未提交的学生老师在催作业时根本不知道还差谁这是非常典型的错误。后来修正后前端拿到数据凡是status为NULL的行统一渲染成“未交”状态。这个“全部学生 左连接提交记录”的查询模式在考试记录、学习进度统计里也反复用到。4. 后端 SpringBoot 实现认证、课程、作业与考试的核心逻辑4.1 基于 Spring Security JWT 的登录认证链路后端用的是 SpringBoot 3.x Spring Security 6 的组合。认证流程是这样的用户提交用户名密码到/api/auth/login。AuthService调用UserMapper查找用户并用BCryptPasswordEncoder.matches()比对密码。比对成功后生成 JWT TokenToken 中封装用户ID、用户名、角色集合设置过期时间为 24 小时。前端将 Token 存储在内存中刷新页面丢失后自动调/api/auth/refresh接口用 refresh_token 换取新 access_token所有后续请求在请求头中带上Authorization: Bearer xxx。Spring Security 的OncePerRequestFilter拦截请求解析 Token将用户信息放入SecurityContextHolder后续业务代码通过AuthenticationPrincipal获取当前登录用户。实际编码时最容易出错的地方不是 JWT 生成本身而是 Spring Security 6 的配置方式。在 Spring Boot 2.7 之前常见写法是继承WebSecurityConfigurerAdapter并重写configure(HttpSecurity http)方法。但 Spring Security 6 移除了这个抽象类必须改为基于SecurityFilterChain的 Bean 配置方式。如果你网上搜到的大多数旧教程代码直接搬过来编译大概率会报错。Bean SecurityFilterChain filterChain(HttpSecurity http) throws Exception { http.csrf(csrf - csrf.disable()) .sessionManagement(sm - sm.sessionCreationPolicy(SessionCreationPolicy.STATELESS)) .authorizeHttpRequests(auth - auth .requestMatchers(/api/auth/**).permitAll() .requestMatchers(/api/teacher/**).hasRole(TEACHER) .requestMatchers(/api/admin/**).hasRole(ADMIN) .anyRequest().authenticated() ) .addFilterBefore(jwtAuthFilter, UsernamePasswordAuthenticationFilter.class); return http.build(); }注意hasRole(TEACHER)会自动在你数据库中存的角色名前加ROLE_前缀再比对所以数据库里的role字段我存的是TEACHER。如果是各种权限注解PreAuthorize则要在配置类上加上EnableMethodSecurity否则PreAuthorize(hasRole(TEACHER))不会生效——这也是新手常见的问题。4.2 课程与选课接口的设计思路课程模块的接口不算多但每个接口的设计都要考虑“数据边界”。教师A创建的课程教师B不应该能修改学生选课时只能选状态为“已发布”的课程。我通过下面几种方式保证数据安全修改、删除课程的接口先通过courseMapper.selectById(courseId)查出课程再比较course.getTeacherId()和当前登录用户ID是否一致。不一致直接抛出业务异常。选课接口在插入course_selection之前先查一次选课记录判断是否已选过同时检查课程状态和选课时间段。这两个检查不能交给数据库唯一索引兜底因为数据库报错是“服务器内部错误”前端无法转化为友好提示。选课接口的业务伪代码大致如下Transactional public void selectCourse(Long courseId, Long studentId) { Course course courseMapper.selectById(courseId); if (course null || !PUBLISHED.equals(course.getStatus())) { throw new BusinessException(课程不存在或未开放选课); } int count selectionMapper.checkExists(studentId, courseId); if (count 0) { throw new BusinessException(不能重复选课); } CourseSelection selection new CourseSelection(); selection.setStudentId(studentId); selection.setCourseId(courseId); selection.setSelectTime(LocalDateTime.now()); selectionMapper.insert(selection); }这里的Transactional必须有。单看这段代码一个SELECT加一个INSERT好像不需要事务但实际业务中选课后通常还要初始化学习进度记录给每个新选课学生生成一条learning_progress记录一旦第二步失败第一步已插入的选课记录就会悬空导致学生选课成功但课程列表为空。4.3 作业提交与补交逻辑版本控制是痛点作业接口是学生与老师互动最频繁的地方。最初版本的提交逻辑很简单——学生上传文件后直接覆盖写入homework_submission表。但在试运行时出现了问题学生误传了一个空白文件或者网络原因上传了一个损坏的文件再想重新提交时系统不允许第二次提交。后来我把提交逻辑改为“写入新记录 软更新”Transactional public void submitHomework(HomeworkSubmitDTO dto, Long studentId) { Homework homework homeworkMapper.selectById(dto.getHomeworkId()); // 检查截止时间…… HomeworkSubmission submission submissionMapper.selectLatest(studentId, dto.getHomeworkId()); if (submission ! null) { // 已有记录更新文件路径版本号1 submissionMapper.updatePath(submission.getId(), dto.getFilePath(), submission.getSubmitVersion() 1); } else { // 首次提交插入新记录 HomeworkSubmission newSubmission new HomeworkSubmission(); // 填充字段…… submissionMapper.insert(newSubmission); } }最终表里只保留最新一条提交记录但submit_version字段记录提交次数update_time记录最后提交时间同时我在文件存储层保留了每次上传的物理文件按版本号命名这样做既有当前工作的语义又不丢失历史版本。教师批改时前端会显示“第 3 次提交最后提交时间”学生没办法再说“我没交过”。补交逻辑上我支持管理员设置“允许补交”由教师端在发布作业时勾选。补交只允许在截止时间后打开一个窗口期如两天超过窗口期后端直接拒绝。这样既照顾学生忘交的情况又不会无限拖延教学进度。4.4 在线考试模块限时、防作弊基础方案与自动判分考试模块设计时第一版的目标是“别翻车”把流程跑通比做花哨功能重要。考试开始前学生点击“开始考试”按钮后端校验当前时间是否在考试时间窗口内然后调用examRecordMapper.createOrGet(studentId, examId)创建一条考试记录同时记录start_time。这里做了一个防刷新小机制同一学生点击开始多次不会创建多条考试记录而是返回已存在的记录保证考试中断线重连后不丢答题状态。学生答题过程中前端将答题数据暂存本地并定时每30秒自动保存到后端exam_answers表。提交时后端逐题校验答案选择题和判断题属于objective_judge类型可以直接比对答案累加客观题得分主观题如果后续添加则由教师进入批改页面给分。限时逻辑上我采用“以开始时间为准后端计算剩余时间”。第一次做的时候我犯过错误把剩余时间放在前端倒计时结果学生修改本地时间就能无限延长。后来改成每次提交/自动保存时后端都根据start_time与当前时间差判断是否超时超时则拒绝提交。这是整个考试模块最容易漏掉的环节也是测试阶段必须重点验证的。关于防作弊第一版我只做了最简单有效的几项后端记录同一 IP 下短时间内多次切换试卷的异常日志。考试期间打开其他浏览器的答题标签页重复进入时只允许一个可用的考试会话并把旧的会话标记为“异常切换”。更高级的摄像头监控、屏幕录制这类功能我没有在首版加入。原因很简单这类功能涉及浏览器权限、实时推流、视频存储复杂度和成本会成倍增长且计算机基础课的考试场景暂时用不上。如果后续有需求可以在考试记录表增加字段并做独立模块扩展。5. 前端 Vue3 的实现要点从动态路由到考试交互5.1 项目结构与状态管理不要一上来就写页面前端采用了 Vue3 Vite Vue Router 4 Pinia Element Plus 的组合。开始写第一个页面前我花了半天时间把项目目录整理好。一个合理的目录结构能让后续开发事半功倍src/ ├── api/ # 所有接口请求封装按模块拆文件 ├── assets/ # 静态资源 ├── components/ # 通用组件上传、分页、富文本等 ├── layouts/ # 主布局组件根据角色动态渲染侧边菜单 ├── router/ # 路由配置 守卫 ├── stores/ # Pinia 状态管理userStore、courseStore ├── views/ # 页面视图 │ ├── student/ │ ├── teacher/ │ └── admin/ └── utils/ # 工具函数时间格式化、文件下载等这里尤其要说一下Pinia 的使用边界。很多初学者会习惯把所有数据都放进全局状态管理结果页面一刷新所有状态清空又要重新请求。我做了几个明确约定登录用户信息、角色、课程当前状态放入stores因为很多东西都要跨组件共享。某个页面独有的数据如某次作业的提交列表只在页面内通过ref管理不塞进全局 store。后端返回的数据尽可能“原样存”只有在渲染时才做转换。避免在同一份数据的多个副本之间同步不同步的问题。5.2 动态路由如何让不同角色看到不同菜单动态路由是这个系统前端最核心的机制之一。它的实现思路是这样的用户登录成功后登录接口返回角色列表roles。前端拿到角色后通过router.addRoute()动态追加该角色对应的路由表。同时在store的menuList中写入该角色能看到的菜单项侧边栏组件根据menuList渲染菜单。这与传统的“所有路由都写死在router/index.js中进入页面后根据角色隐藏部分菜单”的思路不同。动态路由的优点是没有权限的人根本不在路由表中即使他手动在浏览器地址栏输入一个受保护页面的路径路由守卫也找不到对应组件无法渲染。// router/index.js router.beforeEach(async (to, from, next) { const userStore useUserStore(); if (!userStore.token to.path ! /login) { next(/login); return; } if (userStore.token !userStore.isRoutesReady) { // 根据角色动态注册路由 const accessibleRoutes generateRoutes(userStore.roles); accessibleRoutes.forEach(route router.addRoute(route)); userStore.setRoutesReady(true); next({ ...to, replace: true }); return; } next(); });这里有一个容易出问题的细节动态路由注册后直接next()会导致页面刷新时白屏或路由不匹配。因此我在注册完成后使用了next({ ...to, replace: true })让路由重新跳转一遍确保新加入的路由能被命中。这个处理不写的话页面刷新后经常出现一片白屏非常影响体验。5.3 作业上传与教师批改界面交互细节决定系统口碑前端最好做的部分是页面布局最难做的是符合业务直觉的交互。学生端的作业提交页我设计成“卡片式”作业列表每张卡片显示课程名称、作业标题、截止时间、提交状态未提交/已提交/已批改、得分。卡片上的按钮在不同状态下是不同的未提交显示“去提交”点击后打开弹窗拖拽上传文件。已提交未批改显示“查看提交”同时保留“重新提交”按钮。已批改显示“查看批改结果”点进去能看到教师评语和得分。这里有个容易踩的坑el-upload组件的action属性要求一个独立的上传接口 URL。如果你直接用统一的上传接口比如/api/common/upload传完文件后返回的是文件路径。此时前端要把这个路径保存到表单中再随作业提交按钮一起发送给业务接口。很多第一次写上传功能的同学会把“文件上传成功”和“作业提交成功”混为一谈导致传了文件但没有点“提交”按钮作业状态一直是未提交。教师端的批改页也有类似的交互问题。批改列表要显示所有学生包括未交的批改时教师点击“批改”按钮进入一个抽屉面板左边显示作业要求右边显示学生提交的文件预览和打分输入框。文件预览我用了浏览器的原生 PDF 预览能力iframe嵌入文件 URL和图片预览组件文档类文件则提供下载按钮。批改完成后抽屉面板关闭列表里该学生的状态从“已提交”变为“已批改”同时显示分数——这个过程不能让学生刷新页面才能看到结果所以前端在后端返回成功后会立即更新本地数组。5.4 考试倒计时与答题卡前端的一次性体验优化在线考试前端最重要的体验就是“别丢数据、别闪退”。答题页面我设计成左右布局左侧是试题区按顺序展示题目右侧是答题卡以网格形式展示所有题目序号已答题目显示绿色背景未答题目显示灰色。倒计时固定在顶部中间时间只在整分钟变化时跳动避免秒级跳动造成紧迫感。数据的自动保存通过setInterval实现每 30 秒调用一次/api/exam/autosave接口把当前所有答案发送至后端。离开页面或刷新前通过beforeunload事件提示用户“考试尚未提交确定要离开吗”。有一个在测试阶段发现的问题setInterval的定时器在浏览器标签页切换到后台一段时间后会被浏览器降频甚至暂停尤其 Chrome 的节能机制。这意味着学生切到其他标签页查资料再切回来时可能好几分钟没有自动保存。我的应对方案是监听visibilitychange事件当页面恢复可见时立即触发一次自动保存同时恢复倒计时计算。保存和倒计时都要以服务器时间为准前端只是基于服务端返回的开始时间做剩余时间展示。6. 前后端联调与常见踩坑从能跑到稳定运行6.1 开发环境下的跨域问题与联调接口规范前后端分离开发时前端跑在localhost:5173Vite 默认端口后端跑在localhost:8080跨域问题无法避免。我没用后端CrossOrigin注解因为那需要在每个接口上加或写全局配置且对生产环境没有意义。开发环境的跨域我选择在 Vite 配置中做代理// vite.config.js server: { proxy: { /api: { target: http://localhost:8080, changeOrigin: true } } }这样前端请求/api/auth/login时Vite 会转成http://localhost:8080/api/auth/login浏览器看到的还是同源请求自然没有跨域问题。联调时还要提前约定接口返回的 JSON 格式。我们统一采用{ code: 200, message: success, data: { } }后端封装了统一的ResultT对象业务异常时code返回500message显示具体错误信息如“作业已过期无法提交”。前端 axios 拦截器统一处理响应当code ! 200时弹出ElMessage提示。这比直接抛出 HTTP 状态码 裸字符串的设计更可控尤其是表单校验类提示统一格式后前端不用每个请求单独写好几十种错误分支。6.2 一个典型的线上问题排查实录考完试成绩不显示系统上线后遇到的一个典型的、很值得复盘的问题学生提交了考试教师端也能看到答题记录但成绩始终不显示为“已批改”。排掉代码逻辑之后我决定检查数据库记录。用一条 SQL 查询后发现考试记录表里的submit_time是NULL。这意味着学生虽然在界面上点了“提交”但那只是前端把提交按钮改成“已提交”状态后端接口并没有真正被调用或者被调用了但事务回滚了。再进一步查后端日志看到如下错误SQLIntegrityConstraintViolationException: Column total_score cannot be null原因清楚了exam_record表中的total_score字段没有设置默认值且数据库插入时没有该值。正常情况下这个字段应该在提交时计算后写入但该学生在考试开始时点了“开始考试”前端自动创建了考试记录此时total_score还没计算出来因此写入了NULL。到了提交时接口按理说应该更新total_score但后端代码里更新语句漏掉了这个字段于是数据库保持了NULL。修复方案其实有两步后端代码中提交接口更新时显式查一次试卷总分并写入total_score。数据库层面把total_score字段设为NOT NULL DEFAULT 0防止再次出现类似遗漏。这给我一个提醒数据库字段的默认值设计不能马虎它是最底层的防线。像“总分”“状态”这类字段宁可先给默认值也不要允许NULL因为NULL会一路渗透到前端造成各种判断问题比如v-ifscore ! null永远为 false排查起来还特别费劲。6.3 测试与验收哪些环节必须人工验证自动化测试能覆盖部分功能但教学系统有不少业务流程需要人工验收。我整理了一份最核心的验收清单供你参考功能点验收动作通过标准学生选课重复选同一门课弹出“不能重复选课”提示数据库无重复记录作业提交截止时间后提交后端拒绝前端显示“已截止”作业补交教师在后台开启补交后再提交允许提交记录版本号1教师批改未提交的学生是否在列表显示显示为“未交”状态考试限时将本机时间改后继续提交后端判定超时拒绝提交成绩导出导出学生成绩表后 Excel 打开表格内容与数据库一致权限越权用学生账号直接访问教师接口返回 403文件下载下载课件时文件名正确浏览器提示为原始文件名导出 Excel 是平时最不起眼、但最容易出问题的环节。我用的方案是后端用 Apache POI 生成.xlsx文件返回文件字节流前端用blob下载。这里的问题在于浏览器下载的文件名用Content-Disposition响应头指定如果文件名是中文且后端没有对文件名做 URL 编码部分浏览器会下载文件名乱码。解决办法是先URLEncoder.encode再放入响应头前端再读取时做一次decodeURIComponent。这个细节不处理好导出功能会频繁被用户“报Bug”。7. 部署上线与项目复盘一些值得长期保留的经验7.1 自动化部署从手动传 jar 包到一键脚本项目第一版部署上线时我还是手动执行mvn package打 jar 包然后通过文件传输工具传到服务器、再手动重启进程。操作了几次后发现这种模式效率低且容易出错经常忘记停掉旧进程导致端口占用。后来我写了一个简单的部署 Shell 脚本#!/bin/bash # 部署脚本 deploy.sh echo start build... mvn clean package -DskipTests echo upload jar... scp target/teaching-system.jar root服务器IP:/opt/app/ echo restart service... ssh root服务器IP systemctl restart teaching-backend这个脚本虽然简陋但它把“构建、上传、重启”三个步骤串起来了半年内我给系统部署更新了不下 20 次几乎没出过问题。后续如果能继续优化可以引入流水线工具做 CI/CD但对校内项目来说这个脚本已经足够轻量可靠。前端部署也很简单npm run build生成dist目录将打包产物放到 Nginx 的html目录下。关键问题在于 Nginx 需要配置反向代理/api到后端地址server { listen 80; server_name your-server-domain; location / { root /usr/share/nginx/html; index index.html; try_files $uri $uri/ /index.html; # 防止刷新404 } location /api/ { proxy_pass http://127.0.0.1:8080/api/; proxy_set_header Host $host; proxy_set_header X-Real-IP $remote_addr; proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; } }try_files这行配置极其重要。如果不加访问/student/courses这类前端路由时Nginx 会尝试找真实的/student/courses文件找不到就返回 404页面刷新就会白屏。7.2 上线后的运维观察与性能优化系统上线后最明显的性能瓶颈不在后端接口而在文件上传下载的带宽占用。几百名学生同时登录看课程视频或下载课件时如果文件直接通过后端接口流出后端进程的 CPU 和带宽会被占满导致登录接口都跟着变慢。我的处理方式是用 Nginx 做静态资源分离上传时后端将文件存在服务器/data/upload/目录下数据库记录的是/static/upload/xxx.pdf相对路径。Nginx 增加如下配置location /static/ { alias /data/upload/; expires 7d; add_header Cache-Control public; }这样前端获取文件时直接请求 Nginx 的资源路径http://域名/static/upload/xxx.pdf走 Nginx 的高并发静态文件处理不会占用后端应用线程。涉及断点续传的大文件课程视频也能由 Nginx 自动处理Range请求非常省心。数据库层面我给几张高频查询表加上了必要的索引course_selection(student_id, course_id)联合索引支撑“学生我的课程列表”查询。homework_submission(homework_id, student_id)联合索引支撑批改列表查询。exam_record(student_id, exam_id)联合索引支撑考试记录查询。实测数据量在几千行级别时这些索引速度提升的效果不明显但系统运行到第二学期数据量翻倍后批改列表的查询响应时间稳定保持在 200ms 以内说明索引提前建是值得的。7.3 关于这套系统后续扩展方向的一点个人思考系统第一版从设计到上线大约用了两个多月整体结构是稳的但也有不少可以继续改进的地方。以我个人的角度说说几个方向一是引入作业查重。计算机基础课的实验报告和代码作业里直接复制粘贴的现象不少。第一版只实现了“批量下载”功能老师下载后人工对比效率很低。如果后续要升级建议在提交时调用查重接口对学生提交的文本内容按 n-gram 切分做相似度计算超过阈值直接标红提醒教师能有效减少重复作业排查的时间。二是增加学习行为分析。现在系统积累了大量的用户行为日志观看课件时间、提交作业时间、考试用时等这些数据在学期初统计时用 Excel 处理太粗糙。后续可以做一张learning_analysis报表按课程维度统计“学生活跃时间分布”“作业逾期率趋势”“考试用时与得分关联”帮助教师调整教学节奏。这块技术上用定时任务跑统计数据即可难度不大但业务价值很高。三是更细粒度的权限拆分。目前系统角色只有学生、教师、管理员三种但实际上有些院系配了“助教”帮老师批改作业有些场景需要“教务员”帮管理员维护课程数据。后续可以引入更通用的 RBAC 表模型用户-角色-权限三张关系表把目前写死在代码里的接口权限规则改为从数据库动态加载。这会是系统从“课程管理工具”走向“平台”的一个关键转折点。8. 写在最后如果你也要开发类似的教学系统如果你准备照着这个思路自己做一个类似的系统我的建议是先控制规模、先跑通主流程。学生选课、作业提交批改、在线考试这三个环节是系统的“骨架”先把这三个做扎实再去扩展课程讨论区、通知公告、学习资料个性化推荐等功能。很多功能听着刚需实际第一学期用下来高频访问的还是那几个核心页面。另外团队协作时要统一接口文档管理方式。我们的后端接口定义由一个人在开发前统一维护两份文档一份是接口路径与请求响应字段的 Markdown、一份是数据库中表和字段的 Excel。前后端各拿一份文档做开发联调时基本不需要来回对字段名效率高很多。我自己在这两次开发周期里最深的体会是做一个面向真实用户的教学系统难点从来不在某个单独的技术栈有多新而在把学生、老师、管理员三个角色的真实动作翻译成合理的表结构和接口流程。需求梳理透彻SpringBoot 和 Vue3 都只是实现工具。最后再和你分享一个实际操作中的小技巧上线第一周一定要盯紧“作业提交失败”类的用户反馈因为这是学生和老师最先碰到的核心功能。如果第一周提交作业环节稳定无差评后面系统的口碑基本就稳了。开发阶段再花哨的测试也不如真实用户的第一波操作暴露问题快保持好响应节奏周期性优化就好。这套系统的架构和源码思路就分享到这里。如果你正在开发类似的网络教学平台欢迎从本文的表结构设计和权限控制思路开始做取舍没有必要一上来就堆功能稳扎稳打把核心链路做顺畅了系统自然能持续运转下去。

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价 →
↑