☰
SpringBoot+Vue在线课程管理系统:从数据库设计到部署实战
2026/10/15 18:15:37 网站建设 项目流程

1. 项目全貌与技术选型:为什么是SpringBoot+Vue这套组合

先聊点实在的。在线课程管理系统这种题目,在毕业设计和初级开发者练手项目里面出现频率极高,但你千万别觉得它"烂大街"就没含金量。恰恰相反,正因为做的人多,面试官和评审老师对它的功能边界、代码质量、设计思路都会有比较明确的预期。一个把选课并发、权限控制、课程状态流转这些细节都处理干净的项目,远比一个堆砌了十几个华而不实功能点的项目更能体现你的工程能力。

这个项目本质上做的是这么一件事:用SpringBoot提供后端API服务,用Vue搭建前端管理界面,用MySQL存数据,用MyBatis做数据持久化访问,实现一个支持多角色(管理员、教师、学生)的课程管理平台。学生能浏览课程、选课退课、查看自己的课表;教师能发布课程、管理课程内容与学生名单;管理员负责用户管理、课程审核、数据统计。整套系统是前后端分离架构,前端通过HTTP请求调用后端RESTful接口,数据以JSON格式交互。

我强烈建议初学者用这个项目作为前后端分离的入门实战。理由很简单:它的业务模型足够清晰,不涉及复杂的算法和分布式问题,但又完整覆盖了用户认证、权限控制、CRUD操作、关联查询、文件上传、数据统计这些企业级开发的核心场景。换句话说,你把这个项目吃透了,市面上大部分管理类系统的开发套路你基本就摸清了。

技术选型为什么是这套组合而不是别的?先看后端,SpringBoot目前依然是Java后端开发的绝对主流,它解决了传统Spring配置繁琐的问题,内嵌Tomcat、自动配置、起步依赖这些特性让你能快速搭建一个可运行的服务。MyBatis则是一个轻量级的持久层框架,SQL由开发者自己控制,对于课程管理这种涉及多表关联查询、动态条件筛选的业务场景,写SQL反而比ORM框架更直观、可控。再看前端,Vue有渐进式的学习曲线,无论是用Vue2还是Vue3,其响应式数据绑定和组件化思维能让你比较快地上手页面开发。Element Plus这套UI组件库更是把后台管理页面的表单、表格、弹窗都给你封装好了,不需要从零写样式。

这套方案对应届生来说还有一层现实意义:Java后端岗位的招聘需求量大,SpringBoot几乎是必问技术栈;Vue则是前端框架里最好上手、文档最友好的之一。用一套技术栈同时覆盖前后端,你简历上能写的东西立刻充实起来,而且每一块都是面试高频考点。

2. 数据库设计:课程系统的地基,别在这里省时间

很多同学做这类项目喜欢上来就写代码,结果做到后面发现表结构不合理,又回头改数据库,连带后端Mapper和前端页面一起返工,非常痛苦。我踩过这个坑之后总结出一个原则:数据库设计宁可多花两天,也不要动工写业务代码,表结构定了,后面就是填肉的过程。

2.1 核心表结构规划

一个在线课程管理系统,核心表的数量控制在6到8张比较合理。太少说明功能有缺失,太多说明你没有做好抽象,把不该拆的拆了。

第一张是user用户表。这张表要能同时承载管理员、教师、学生三种角色,所以除了基本的id、username、password、real_name、email、phone、avatar之外,必须有一个role字段做角色区分。我建议role用整数类型,0表示管理员、1表示教师、2表示学生,不要用字符串,因为整数在做权限判断时写起来更简洁,而且没有历史包袱。

第二张是course课程表。核心字段包括:id、course_name、course_code(课程编号)、teacher_id(关联用户表,表示授课教师)、category_id(关联课程分类)、credit(学分)、class_hour(学时)、capacity(选课容量)、selected_count(已选人数)、course_time(上课时间,比如"周一 3-4节"这样的描述)、course_location(上课地点)、course_desc(课程简介)、cover_url(课程封面图)、status(课程状态,0未发布、1已发布、2已结课)、create_time、update_time。

第三张是course_category课程分类表,字段很简单,就是id、category_name、sort_order。之所以单独建表而不是在课程表里直接存个分类字符串,是为了后续做分类筛选和统计时SQL写起来更优雅,而且避免了分类名称重复、不一致的问题。

第四张是student_course选课记录表,这是整个系统的核心关联表。字段为:id、student_id、course_id、score(最终成绩)、status(选课状态,0已选、1已退选、2已结课)、create_time(选课时间)。这里要注意,成绩字段放在选课记录表而不是课程表,因为一个学生对应一门课只有一条选课记录,成绩是这条记录的属性。

第五张是course_resource课程资料表,用于存放教师上传的课件、教案等文件。字段包括:id、course_id、resource_name、resource_url、resource_type(1课件、2教案、3其他)、upload_time。这张表要不要做,取决于你想不想把文件上传功能纳入系统,我个人建议加,因为这是简历上可以体现的亮点,而且实现起来并不复杂。

第六张是notice公告通知表,用于管理员发布系统公告或者教师发布课程通知。字段:id、title、content、publisher_id、publish_time、target_role(通知对象,0全部、1教师、2学生)。

2.2 建表SQL关键片段与设计说明

用户表的核心SQL大概是这样的:

CREATE TABLE `user` ( `id` bigint NOT NULL AUTO_INCREMENT COMMENT '用户ID', `username` varchar(50) NOT NULL COMMENT '用户名', `password` varchar(100) NOT NULL COMMENT '密码(BCrypt加密存储)', `real_name` varchar(50) DEFAULT NULL COMMENT '真实姓名', `role` tinyint NOT NULL DEFAULT '2' COMMENT '角色:0管理员 1教师 2学生', `email` varchar(100) DEFAULT NULL COMMENT '邮箱', `phone` varchar(20) DEFAULT NULL COMMENT '手机号', `avatar` varchar(255) DEFAULT NULL COMMENT '头像URL', `status` tinyint NOT NULL DEFAULT '1' COMMENT '状态:0禁用 1正常', `create_time` datetime NOT NULL COMMENT '创建时间', PRIMARY KEY (`id`), UNIQUE KEY `uk_username` (`username`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='用户表';

密码字段这里要特别说明一下。很多教程直接明文存密码,我只能说毕设答辩的时候遇到懂行的老师这就是送命题。实际开发中密码一定不能明文存储,我们这里用的是BCrypt加密,Spring Security里自带这个能力,即使你不打算引入完整的Spring Security框架,也可以用spring-security-crypto这个单独的库来做密码加密和校验。BCrypt的特点是每次加密同一个明文得到的密文都不一样,但校验时能正确匹配,这增加了暴力破解的成本。

选课记录表的设计需要特别注意联合唯一索引:

CREATE TABLE `student_course` ( `id` bigint NOT NULL AUTO_INCREMENT, `student_id` bigint NOT NULL COMMENT '学生ID', `course_id` bigint NOT NULL COMMENT '课程ID', `score` decimal(5,2) DEFAULT NULL COMMENT '成绩', `status` tinyint NOT NULL DEFAULT '0' COMMENT '状态:0已选 1已退选 2已结课', `create_time` datetime NOT NULL COMMENT '选课时间', PRIMARY KEY (`id`), UNIQUE KEY `uk_student_course` (`student_id`, `course_id`, `status`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='选课记录表';

我把status放进联合唯一索引,这里有些讲究。假设一个学生选了课又退选,再重新选同一门课,如果唯一索引只包含student_id和course_id,第二次插入就会主键冲突。把status放进去,同一学生同一课程可以分别存在一条已选记录和一条已退选记录,这个设计既保证了不会重复选课,又保留了学生的选课历史轨迹。当然,如果你想更严格地只保留一条记录,退选时用UPDATE而不是DELETE,那也是另一种思路,但那样就看不到退课历史了,不利于后面做统计分析。

2.3 表关系与冗余字段的取舍

表之间的关系比较直观:课程表通过teacher_id关联用户表,通过category_id关联分类表;选课记录表通过student_id和course_id分别关联用户表与课程表。这是一个典型的多对多关系通过中间表解耦的案例:一个学生可以选多门课,一门课可以被多个学生选。

冗余字段方面,课程表里的selected_count已选人数就是一个刻意为之的冗余设计。按理说想统计已选人数,SELECT COUNT(*) FROM student_course WHERE course_id = ? AND status = 0就能查到,但在选课列表页每次查询都要做一次COUNT汇总,对数据库压力不小,而且查询性能会随着数据量增长明显下降。所以我在课程表里直接维护一个selected_count字段,学生选课成功后对它加1,退选成功后减1。这里一定要在同一个数据库事务里完成两条SQL的更新,否则会出现数据不一致。很多初学者忽略了这个点,就会出现"课程列表显示没选满,点选课却提示已满"的诡异Bug。

另外还有一个容易忽略的点:数据库的字符集排序规则要统一使用utf8mb4而不是utf8。为什么?因为utf8在MySQL里最多只支持3个字节的字符,存储不了emoji和部分生僻字。人名这种东西你永远不知道会出现什么字,直接用utf8mb4最稳妥。这属于建表初期省事后面省心的小细节。

3. 后端核心模块实现:接口设计、权限控制与业务逻辑

后端是整个系统的中枢神经。SprinBoot的作用是快速搭建服务骨架,真正体现水平的是分层设计、权限控制、事务处理和并发场景下的业务逻辑。这一节我把几个核心模块的实路径和设计思路拆开讲。

3.1 项目目录分层与启动配置

项目后端目录我习惯这样分:

src/main/java/com/example/course/ ├── config # 配置类(CORS跨域、拦截器、全局异常) ├── controller # 控制层(接收请求、参数校验、返回结果) ├── service # 业务层(核心业务逻辑) │ └── impl # Service接口实现 ├── mapper # MyBatis数据访问层(接口 + XML) ├── entity # 实体类(与数据库表对应) ├── dto # 数据传输对象(入参/出参封装) ├── common # 通用类(统一返回结果、状态码、异常处理) └── interceptor # 拦截器(JWT鉴权、角色校验)

为什么一定要分层?最直接的好处是出了问题能精准定位。前端传参不对,看Controller;业务逻辑有漏洞,查Service;SQL写错了,看Mapper。如果你把所有逻辑全塞在Controller里,一个类几百行代码,排查问题时想找业务规则就像在杂物间找一根针。另外,分层的代码在简历上写起来也更好看,面试官一看就知道你有工程化意识。

application.yml中比较关键的配置是数据库连接和数据源:

spring: datasource: driver-class-name: com.mysql.cj.jdbc.Driver url: jdbc:mysql://localhost:3306/course_system?useUnicode=true&characterEncoding=utf8&serverTimezone=Asia/Shanghai username: root password: 123456 mybatis: mapper-locations: classpath:mapper/*.xml type-aliases-package: com.example.course.entity configuration: map-underscore-to-camel-case: true

map-underscore-to-camel-case这个配置强烈建议打开。它能把数据库字段的snake_case(下划线风格,如create_time)自动映射到实体类的camelCase(驼峰风格,如createTime),少写大量无聊的映射配置。

3.2 登录认证与JWT权限控制

登录认证是一个管理系统最基本的门槛。我用的方案是JWT配合拦截器做无状态鉴权,这也是目前前后端分离项目的主流做法。

流程是这样的:用户提交用户名密码,后端验证通过后生成一个JWT令牌返回给前端,前端把令牌存起来,后续每次请求都在HTTP头部的Authorization字段带上这个令牌,后端拦截器解析令牌判断请求是否合法、当前用户是谁、有没有权限操作这个接口。

JWT本身是一个三段式的字符串,分别是Header(头部)、Payload(载荷)、Signature(签名)。Payload里我一般存放userId、username、role这三个核心信息,过期时间设置为2小时。不存放密码等敏感信息,因为JWT的Payload只是Base64编码,不是加密,任何人解出来都能看到内容,存密码等于裸奔。

拦截器的实现关键在于自定义注解配合HandlerInterceptor。开发一个@RequireRole注解,标注哪些接口需要什么角色才能访问:

@Target(ElementType.METHOD) @Retention(RetentionPolicy.RUNTIME) public @interface RequireRole { int[] value(); }

然后在拦截器的preHandle方法里,先解析JWT拿到角色,再判断当前请求的HandlerMethod上有没有标注@RequireRole,有的话就校验角色是否匹配。管理员接口标@RequireRole(0),教师接口标@RequireRole({0,1}),学生选课接口标@RequireRole({0,2})。这样做的好处是权限逻辑集中在一处,而不是散落在每个Controller方法里,新增一个接口时在方法上加一行注解就能完成权限声明。

CORS跨域也是一个必踩的坑。前后端分离开发时,前端跑在8080端口,后端跑在8081或者9090端口,浏览器会拦截跨域请求。解决办法是在后端写一个CORS配置类,允许指定的前端源访问,同时允许携带Authorization请求头。注意这里不能直接allowedOrigins("*")再加allowedHeaders("*"),因为在携带凭证(allowCredentials)的场景下,通配符是不被允许的。这是一个细节性的坑,很多人配置完CORS发现明明后端接口用Postman测没问题,前端一调就报CORS错误,就是这个地方没处理干净。

3.3 课程管理与选课业务:事务与并发控制

选课功能是系统核心中的核心,这里有两个典型的业务难题:超卖问题和重复选课问题。

超卖问题说的是,课程容量只有50人,但第51个学生也能选课成功。如果代码逻辑只是先查一下selected_count是不是小于capacity,小于的话就插入选课记录并更新selected_count加1,那么在高并发场景下,两个学生同时查到selected_count=49,都判断可以选课,结果都走了后续流程,课程实际选入人数变成51。这就是经典的"并发竞态条件"。

解决方案不外乎两种:乐观锁和悲观锁。

悲观锁思路是使用数据库的SELECT ... FOR UPDATE,在事务里先把课程行锁住,其他请求只能等待当前事务提交后才能继续:

<select id="selectCourseForUpdate" resultType="Course"> SELECT * FROM course WHERE id = #{courseId} FOR UPDATE </select>

在执行插入选课记录之前先锁住课程记录,再判断容量,如果没满则插入并更新数量,然后提交事务释放锁。这样并发请求会排队执行,从根源上杜绝了超卖。

乐观锁思路是在课程表加一个version字段,执行更新时检查版本号:

UPDATE course SET selected_count = selected_count + 1, version = version + 1 WHERE id = #{courseId} AND version = #{version}

如果更新影响的行数是0,说明版本号已经被其他事务改过,就抛出异常提示"选课人数已满或课程状态已变化"。

对于课程管理系统这种并发量级,两种锁都够用。我个人偏好乐观锁,因为它在读多写少的场景下性能更好,也不需要长事务占用数据库连接。但要注意,乐观锁需要处理更新失败后的重试逻辑,代码上会稍微复杂一些。

重复选课问题靠的是我们在数据库设计阶段加的联合唯一索引兜底。即使代码层面有Bug,两个请求同时发起,数据库的student_id + course_id + status唯一索引也能保证只有一条插入成功,另一条抛DuplicateKeyException。我在Service里catch住这个异常,统一转换成业务提示"你已经选过该课程了",用户看到的是一个友好的业务提示而不是一串错误堆栈,体验完全不一样。

选课Service的完整流程应该是这样:

  1. 从JWT中获取当前用户id和角色,校验角色是学生。
  2. 查询课程信息,校验课程状态为已发布。
  3. 校验该学生是否已经选过该课程(查询student_course表),选过则直接返回提示。
  4. 开启事务,用乐观锁/悲观锁控制容量更新。
  5. 插入选课记录,更新课程已选人数。
  6. 提交事务,返回选课成功。

这6步里第3步是提前拦截,第4步是并发兜底,两层防护合在一起,选课逻辑才算安稳。

3.4 文件上传:课件资源的存取方案

课程资料上传这个功能,实现方案用的是本地磁盘存储加数据库记录URL的方式。前端通过Element Plus的上传组件选中文件,把文件POST到后端的/api/resource/upload接口,后端用MultipartFile接收,校验文件大小和后缀名后写到服务器指定目录,文件名用UUID + 原始扩展名重命名以避免冲突,然后把文件访问路径存到数据库。

public String uploadResource(MultipartFile file) { if (file.isEmpty()) { throw new BusinessException("文件不能为空"); } String originalFilename = file.getOriginalFilename(); String ext = originalFilename.substring(originalFilename.lastIndexOf(".")); String newFileName = UUID.randomUUID() + ext; String filePath = uploadDir + "/" + newFileName; file.transferTo(new File(filePath)); return "/files/" + newFileName; }

这里要配置一个静态资源映射,让外部可以通过URL直接访问到上传目录里的文件。SpringBoot里通过WebMvcConfigurer来配置:

@Override public void addResourceHandlers(ResourceHandlerRegistry registry) { registry.addResourceHandler("/files/**") .addResourceHandler("file:" + uploadDir + "/"); }

上传目录我建议放在项目外面,不要放在resources里。因为项目重启或者重新部署时,resources目录会被覆盖,放在外面才能保证文件持久存在。这个属于典型的实操经验,文档里不会写,Demo项目里也往往不会涉及,但真实项目百分之百要考虑。

4. 前端页面搭建:Vue3 + Element Plus实战记录

前端部分我用的是Vue3加Element Plus加Vite构建工具。这套组合是目前前端开发的主流配置,Vite的冷启动速度和热更新体验比旧版本的Webpack快得多,能显著提升开发效率。前端架构采用的是标准SPA(单页面应用)模式,通过Vue Router管理页面路由,通过Pinia管理用户登录状态和全局信息。

4.1 前端项目结构与路由权限

前端目录结构这样规划:

src/ ├── api/ # 接口请求封装(按模块拆文件) ├── assets/ # 静态资源 ├── components/ # 公共组件(头部、侧边栏、分页) ├── layout/ # 主布局(侧边栏+顶栏+主内容区) ├── router/ # 路由配置 ├── store/ # 状态管理(Pinia) ├── utils/ # 工具函数(request封装、token存取) └── views/ # 页面组件 ├── admin/ # 管理员页面 ├── teacher/ # 教师页面 ├── student/ # 学生页面 ├── login.vue # 登录页 └── dashboard.vue # 首页仪表盘

路由权限这里我用的是简单实用的方案:路由分为公共路由和需要登录的路由,登录后根据角色动态生成具体业务路由。具体做法是在router的beforeEach守卫里检查本地存储中的token和用户信息,没有token就跳转到登录页,有token但访问的路由不在当前角色的允许列表里,就跳转到首页。

菜单栏的渲染数据不是写死的,而是根据登录用户的角色从后端拉取。比如管理员看到的是用户管理、课程审核、数据统计、公告管理;教师看到的是我的课程、课程审核、选课名单、资料上传;学生看到的是课程浏览、我的选课、我的成绩、课程公告。这种动态菜单的好处是前端不用写一大堆v-if判断显示逻辑,改权限只需要后端返回不一样的菜单配置。

4.2 Axios请求封装与接口对接

Axios的二次封装是前端工程里必不可少的环节。我从项目实践的角度讲一下要解决的问题:统一处理请求头、统一错误提示、统一处理后端返回格式、携带token。

import axios from 'axios' import { ElMessage } from 'element-plus' import router from '@/router' const service = axios.create({ baseURL: '/api', timeout: 10000 }) service.interceptors.request.use(config => { const token = localStorage.getItem('token') if (token) { config.headers['Authorization'] = 'Bearer ' + token } return config }) service.interceptors.response.use( response => { const res = response.data if (res.code !== 200) { ElMessage.error(res.msg || '请求失败') return Promise.reject(new Error(res.msg)) } return res }, error => { if (error.response && error.response.status === 401) { localStorage.removeItem('token') router.push('/login') } else { ElMessage.error(error.message || '网络异常') } return Promise.reject(error) } ) export default service

这个封装的思路是:token统一从本地存储取出放到请求头里,后端返回的code不等于200就弹出错误提示,遇到401(未认证或token过期)就清除登录状态并跳回登录页。一套代码管全局,每个页面调接口时只需要关心业务数据本身,不用每次重复处理鉴权和错误逻辑。

开发调试阶段有个在Vite里配置代理的技巧。前端开发服务器跑在5173端口,后端跑在9090端口,在vite.config.js里配置:

server: { proxy: { '/api': { target: 'http://localhost:9090', changeOrigin: true } } }

这样前端请求/api/course/list会自动转发到http://localhost:9090/api/course/list,在开发阶段完美避开跨域问题。等项目上线部署时,再用Nginx做反向代理,把/api前缀的请求转发到后端服务。

4.3 关键页面实现细节

课程列表页是学生端最核心的页面,承担着课程浏览和选课操作两大功能。我用Element Plus的表格组件展示课程信息,包括课程名、授课教师、学分、上课时间、已选人数/容量、课程状态等列。课程封面图用图片预览组件展示,课程简介超长时用tooltip显示全文。页面顶部放搜索栏,支持按课程名称模糊搜索和按分类下拉筛选,这两个条件搭配后端的分页参数,能实现一个比较丝滑的查询体验。

需要着重说明的是选课按钮的状态控制逻辑。按钮的disabled状态绑定一个计算属性,当且仅当以下条件全部满足时才允许点击:用户已登录、用户角色是学生、课程状态是已发布、已选人数小于容量、并且当前用户尚未选过这门课。这个判断最好不要在前端只做一层,后端Service里还要再做一次完整性校验,前端控制体验,后端控制安全,双保险都上了才算合格。

我的选课页面用卡片方式展示已选课程,每张卡片包含课程名、教师、上课时间和退课按钮。退课操作需要弹确认框,避免学生误触导致退课成功。成绩展示则是一个表格,列出课程信息和成绩分数,成绩为空时显示"暂无成绩"。

教师端的课程管理页面定位略有不同,教师需要的是对课程全生命周期的操作。列表页能看到自己创建的课程,每行有编辑、发布/下架、查看选课名单、上传资料等按钮。这里要处理一个权限细节:教师只能操作自己创建的课程,所以查询时Service里要额外加一个teacher_id = 当前登录用户id的过滤条件,防止教师在知道接口地址后直接篡改其他教师的课程信息。这种"越权访问"问题在安全测试中非常常见,属于典型的IDOR漏洞,初写接口时就要注意规避。

管理员端的用户管理页面和课程审核页面相对标准化,用户管理就是对用户表做CRUD,课程审核则是查看教师提交的课程,决定通过还是驳回。

5. 前后端联调、项目部署与常见报错排查

前后端各自开发完,最重要也最容易出问题的是联调阶段。前后端联调的过程中,最大的障碍往往不是功能逻辑本身,而是定位问题的效率。我一般会按固定排查顺序来:先在浏览器F12开发者工具看Network面板里请求的状态码,再看后端控制台日志异常堆栈,最后检查是不是数据库层面出了问题。这个顺序能帮你快速缩小问题范围。

5.1 联调阶段的经典问题实录

第一个高频问题是后端接口返回的JSON日期格式和前端组件要求的格式不一致。比如选了课,后端返回的createTime: "2025-01-15T10:30:00",这种带T的ISO格式在前端表格里显示就非常难看。解决办法一个是后端在application.yml里统一配置JSON序列化格式,另一个是前端利用第三方库格式化,我更推荐后端配置的方式,全局生效不用每个页面处理。

spring: jackson: date-format: yyyy-MM-dd HH:mm:ss time-zone: GMT+8

第二个高频问题是前端传递的JSON字段名和后端实体类的属性对不上。比如前端传createTime,而后端实体字段是create_time,结果死活绑定不上。这个问题的根源是命名规范不统一,解决办法是前端统一用驼峰命名,后端配置map-underscore-to-camel-case: true,这样两边就能自动对齐。

第三个高频问题是文件上传接口请求头没设置对。前端用FormData方式上传时,不要手动设置Content-Type为application/json,要让它保持multipart/form-data边界自动生成,否则后端接收不到文件。很多初学者在这里卡很久,其实是因为对HTTP请求头理解不透。

5.2 项目部署:从开发环境到服务器

部署这块,最简化的方案是把前端构建后的静态文件放到Nginx里托管,后端打成JAR包用Java命令在服务器上运行,然后通过Nginx反向代理把/api开头的请求转发到后端端口。这套部署方式虽然是简化的,但已经覆盖了生产环境部署的核心思路。

前端构建命令是:

npm run build

构建完成后会在dist目录生成静态文件,把这些文件整个上传到服务器的/usr/share/nginx/html目录。Nginx的配置如下:

server { listen 80; server_name your_domain; location / { root /usr/share/nginx/html; index index.html; try_files $uri $uri/ /index.html; } location /api/ { proxy_pass http://localhost:9090; proxy_set_header Host $host; proxy_set_header X-Real-IP $remote_addr; } }

需要特别注意的是try_files $uri $uri/ /index.html;这一行,它的作用是所有前端路由都回退到index.html,由Vue Router接管,避免用户刷新页面时出现404。没有这行配置,你部署上线后点菜单进到子页面,一刷新浏览器就报404,非常影响体验。

后端打包部署:

mvn clean package -DskipTests java -jar target/course-system-0.0.1-SNAPSHOT.jar

生产环境下我一般会用nohup命令让JAR包后台运行,并配合日志输出:

nohup java -jar target/course-system-0.0.1-SNAPSHOT.jar > app.log 2>&1 &

启动后先看日志确认端口和数据库连接正常,然后用curl测试一下接口通不通,最后再把域名解析指向Nginx。

5.3 常见问题排查速查表

我整理了这五类项目里最常遇到的报错及排查方法,做成一个速查表,卡住时对着看效率很高。

报错现象可能原因排查步骤与解决
前端请求接口报404Nginx配置错误或后端接口路径不匹配先看Network里请求的完整URL有没有经过/api前缀,再看后端控制台有没有对应的请求日志
前端请求接口报401token缺失、过期或被拦截器拒绝查看浏览器Application里有没有token,F12看请求头Authorization是否携带,用Postman直接测接口确认是否是前端问题
报400 Bad Request参数类型不匹配或缺少必需参数检查前端传递参数名与后端@RequestParam或DTO字段是否一致,日期格式是否被正确解析
报500 Internal Server Error后端运行时异常,通常是SQL或空指针查看后端日志异常堆栈第一行定位到具体代码行,数据库中是否有对应数据,SQL语句是否有语法问题
数据库中文乱码连接字符串或数据库字符集不对确认连接URL含characterEncoding=utf8,MySQL表字符集为utf8mb4,这些都是建库时就要定好的

对于报错排查,最核心的建议是养成看日志的习惯。不要一报错就复制错误信息去搜索引擎,先自己花两分钟看异常堆栈最顶部的几个关键行,往往问题就发生在你能看懂的那一行附近。

6. 防坑经验总结:做完这个项目我才悟到的几件事

这是我做完整套系统之后最大的几点体会,专门写在这里。

第一点体会是不要把JWT当黑盒用。很多教程教你依赖库生成解析JWT就行,但你必须清楚Signature部分的签名机制是怎么一回事。后端每次收到请求解析出token里的userId和role之后,要对这两份关键信息保持"轻信"态度,更稳妥的做法是在内存里维护一张在线用户表,每次请求都从在线表里获取最新角色信息,而不是完全依赖token里的角色判断。否则用户被管理员改了角色或禁用后,原本签发的token在过期前依然有旧权限,这在真实系统里是严重的安全隐患。

第二点是关于MyBatis的XML与注解选择。我个人强烈推荐把复杂SQL写在XML里,注解适合写简单查询,多表关联和动态SQL写在注解里可读性非常差。接口与XML通过namespace绑定,需要注意确保XML文件里的<mapper namespace="...">与Mapper接口的全限定类名完全一致,否则启动时就报错,这个错误信息虽然看得懂,但第一次遇到时还是挺容易犯迷糊的。

第三点是所有前端传过来的数据都要做后端校验。有同学觉得前端已经用了Element Plus的表单校验,后端就不管了,这是把安全底线完全交给了客户端。前端校验是用户体验,后端校验才是真正的安全屏障。非法用户在控制台构造一个JSON请求就能绕过前端校验直接攻击后端接口,所以后端每个写入操作都要校验参数非空、长度限制、类型正确,简单场景用Spring的Validation注解就够。

第四点也是最重要的一点,这个课程系统的后续扩展空间非常大。如果你还有余力,可以往这些方向演进:引入Redis缓存课程热门列表、用Spring Security替换手写拦截器、引入消息队列处理选课高峰的削峰、用WebSocket实现课程公告的实时推送、接入阿里云OSS存储课件文件。每多走一步,你简历上的项目含金量就上升一个台阶。但步子也不要迈太大,先把主线功能做扎实,把权限控制和并发问题处理干净,这个系统就已经超出大部分同类型毕设项目的水平了。

我个人在实际操作中的体会是,做这种全栈项目从一开始就要有全局视角,数据库设计时就想到后面的业务代码怎么写,后端写接口时就要考虑到前端使用方会怎么调用。把这条思路贯穿始终,你会发现开发过程意外地顺滑,很多问题根本不会在联调阶段才暴露出来。最后再分享一个小技巧:开发完项目后,一定要写一份README文档,把你做的功能点、用的技术栈、部署步骤、包含的配置都整理清楚。这份文档不仅是答辩时的底气和背稿,更是毕业设计评分和面试官考察的加分项。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询