简介:本资源是一套完整的Java毕业设计实战项目——课程网上辅助教学系统,面向计算机专业本科生及Java初学者,解决高校课程线上辅助教学场景中的信息管理、作业提交与用户权限控制等核心需求。压缩包共489个文件,涵盖83个Java源码文件(含Controller、Service等分层类)、86个编译后class文件、56个HTML页面模板、32个JS交互脚本、28个CSS样式文件,以及MySQL数据库脚本(sql)和2个MP4演示视频,整体大小为96.44MB,结构清晰、模块完整,便于理解MVC架构与B/S系统开发流程。已有321人学习下载,适合用于课程设计参考、毕业设计选题或Java Web技术栈综合实践。资源提供开箱即用的完整功能:学生端支持资料查阅、作业提交与密码修改;管理员端具备全模块管理能力;配套演示视频直观展示系统运行效果,数据库脚本可一键导入,降低部署门槛。
1. 项目概述与核心价值
最近在整理资料时,翻到了一个自己几年前带学生完成的毕业设计项目——“课程网上辅助教学系统”。这个项目在当时获得了不错的评价,其完整的源码、数据库脚本和演示视频至今仍被不少学弟学妹当作参考模板。今天,我就以这个项目为蓝本,结合当前Java技术栈的演进,系统地拆解一下如何从零开始,构建一个功能完备、架构清晰的Java Web毕业设计。无论你是正在为毕业设计发愁的计算机相关专业学生,还是希望找一个完整项目来巩固Java Web技能的开发者,这篇内容都能为你提供一条清晰的、可复现的实现路径。
这个“课程网上辅助教学系统”本质上是一个简化版的在线教育平台,核心目标是解决传统课堂教学中师生互动受限、资料分发不便、学习过程难以追踪等问题。系统需要涵盖学生、教师和管理员三类角色,实现课程发布、资料共享、作业提交批改、在线答疑、成绩管理等核心教学环节的数字化。对于毕业设计而言,它的复杂度适中,既能充分展示你对Java EE、数据库、前端等技术的综合运用能力,又不会因为过于庞大而难以在规定时间内完成。接下来,我将从设计思路、技术选型、模块实现到部署上线的全流程,毫无保留地分享其中的关键细节和踩过的坑。
2. 整体架构设计与技术选型考量
2.1 需求分析与功能模块划分
在动手写代码之前,清晰的需求分析和模块划分是成功的基石。我们当时的分析思路是围绕“教学流程”展开的。
核心角色与用例:
- 学生:核心需求是“学”。包括:查看可选课程并加入、下载课程资料、查看并提交作业、参与课程讨论区答疑、查询个人成绩和学情统计。
- 教师:核心需求是“教”与“管”。包括:创建并管理自己的课程、上传课程资料(课件、视频)、发布与批改作业、在讨论区答疑、录入并管理学生成绩。
- 管理员:核心需求是“系统运维”。包括:管理用户(学生、教师)账号、审核课程信息、维护系统公告、监控系统基础数据。
基于以上分析,我们将系统划分为以下几个核心模块:
- 用户认证与权限模块:处理登录、注册、会话管理,以及基于角色的页面与接口访问控制(RBAC)。
- 课程中心模块:课程信息的CRUD(增删改查)、学生选课/退课、课程列表与搜索。
- 教学资源模块:支持课件(PPT、PDF)、视频等文件的上传、存储、在线预览/播放及下载。
- 作业管理模块:作业的发布、提交、批改(支持附件)、成绩录入与反馈。
- 交流互动模块:以课程为单位的讨论区,实现帖子发布、回复、点赞功能。
- 成绩管理模块:成绩的录入、计算(可设置平时成绩、作业成绩、期末成绩权重)、统计与报表生成。
- 系统后台模块:用户管理、数据统计、日志查看、系统配置。
注意:毕业设计切忌贪大求全。以上模块是理想状态,在实际实施时,建议优先保证“用户-课程-作业-成绩”这条主链路的高质量完成,其他如复杂的讨论区、在线视频播放可以适当简化。
2.2 技术栈选型与版本确定
技术选型直接决定了开发效率和项目的“现代化”程度。结合当前(2023-2024年)的主流趋势,我推荐以下技术栈,这比我们当年用的Struts2+Hibernate组合更高效、更流行:
- 后端框架:Spring Boot 2.7.x / 3.0.x
- 为什么选它?约定大于配置,能快速搭建可独立运行的、生产级的Spring应用。它内嵌Tomcat,无需单独部署WAR包,极大简化了部署流程,特别适合毕业设计演示。Spring Boot 3.x需要Java 17+,如果环境受限,选择2.7.x(Java 8+)更稳妥。
- 持久层框架:MyBatis-Plus 3.5.x
- 为什么选它?相较于传统的MyBatis或JPA,MyBatis-Plus提供了强大的CRUD封装和条件构造器,能节省大量单表操作的SQL编写时间。它的代码生成器功能,能一键生成Entity、Mapper、Service、Controller层基础代码,对毕业设计这种表结构固定的项目效率提升巨大。
- 数据库:MySQL 8.0
- 为什么选它?开源、流行、资料多。8.0版本在性能、JSON支持和窗口函数上比5.7有显著提升。务必统一使用UTF8MB4字符集,以支持存储Emoji表情(讨论区可能用到)。
- 前端技术:Vue 3 + Element Plus
- 为什么选它?前后端分离是当前绝对的主流。Vue 3的Composition API让逻辑组织更灵活,生态繁荣。Element Plus是基于Vue 3的桌面端组件库,封装了表格、表单、弹窗等大量高质量组件,能让你快速构建出美观且交互一致的管理后台界面。对于毕业设计,这比用JSP或Thymeleaf写后端渲染页面要“时髦”和高效得多。
- 构建与依赖管理:Maven / Gradle
- Maven依然是Java领域最主流的构建工具,配置文件
pom.xml清晰。Gradle构建速度更快,脚本更灵活。二者选一即可,Maven的普及度使其成为更安全的选择。
- Maven依然是Java领域最主流的构建工具,配置文件
- 其他关键依赖:
- Spring Security / Sa-Token:用于权限控制。Spring Security功能强大但较重;Sa-Token是一个轻量级国产框架,API更简单直观,对于毕业设计的权限模型来说更容易上手。
- Hutool:一个Java工具类库,提供了文件操作、加密解密、日期处理等常用静态方法,能避免重复造轮子。
- Lombok:通过注解自动生成Getter/Setter、构造方法等,让实体类代码更简洁。
- Knife4j:基于Swagger的API文档生成工具,能自动生成漂亮且可在线调试的接口文档,方便前端对接和答辩演示。
3. 数据库设计与核心表结构解析
数据库设计是系统的骨架,设计好坏直接影响后续开发的复杂度。我们遵循第三范式(3NF)进行基础设计,同时兼顾查询性能。
3.1 E-R图核心实体与关系
主要实体包括:用户(User)、课程(Course)、课程资料(Resource)、作业(Assignment)、提交记录(Submission)、成绩(Score)、讨论帖(Post)、回复(Reply)。
核心关系:
- 一个教师可以创建多门课程,一门课程属于一名教师(1对多)。
- 一门课程可以被多个学生选修,一个学生可以选择多门课程(多对多),需通过
学生选课表(student_course)关联。 - 一门课程下有多份资料、多个作业、多个讨论帖(1对多)。
- 一个学生对一份作业有一次提交记录(1对1,但作业和学生是多对多,通过提交记录关联)。
- 一个学生在一门课程中有一个成绩(1对1,通过课程和学生唯一确定)。
3.2 关键表结构设计示例与思考
这里给出几个核心表的设计,并解释关键字段的用意。
用户表 (sys_user)
CREATE TABLE `sys_user` ( `id` bigint NOT NULL AUTO_INCREMENT COMMENT '主键', `username` varchar(50) NOT NULL COMMENT '用户名(学号/工号)', `password` varchar(255) NOT NULL COMMENT '加密后的密码', `real_name` varchar(50) DEFAULT NULL COMMENT '真实姓名', `email` varchar(100) DEFAULT NULL COMMENT '邮箱', `avatar` varchar(500) DEFAULT NULL COMMENT '头像URL', `role` varchar(20) NOT NULL COMMENT '角色:student/teacher/admin', `status` tinyint DEFAULT '1' COMMENT '状态:0-禁用,1-正常', `create_time` datetime DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间', PRIMARY KEY (`id`), UNIQUE KEY `uk_username` (`username`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='系统用户表';实操心得:1.
password字段一定要够长(推荐255),因为BCrypt等加密算法生成的密文很长。2.role字段用字符串存储,比用数字代码更直观,方便后续权限判断。3. 将status(状态)和role(角色)分开,逻辑更清晰,比如可以禁用某个老师账号而不影响其角色定义。
课程表 (edu_course)
CREATE TABLE `edu_course` ( `id` bigint NOT NULL AUTO_INCREMENT, `teacher_id` bigint NOT NULL COMMENT '教师ID', `course_code` varchar(20) NOT NULL COMMENT '课程代码', `course_name` varchar(100) NOT NULL COMMENT '课程名称', `description` text COMMENT '课程描述', `cover_image` varchar(500) DEFAULT NULL COMMENT '封面图URL', `credit` tinyint DEFAULT NULL COMMENT '学分', `max_students` int DEFAULT '0' COMMENT '最大选课人数,0表示不限制', `is_public` tinyint DEFAULT '1' COMMENT '是否公开选课:0-否,1-是', `status` tinyint DEFAULT '1' COMMENT '状态:0-已结束,1-进行中,2-未开始', `create_time` datetime DEFAULT CURRENT_TIMESTAMP, PRIMARY KEY (`id`), UNIQUE KEY `uk_course_code` (`course_code`), KEY `idx_teacher_id` (`teacher_id`), KEY `idx_status` (`status`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='课程表';注意事项:1. 建立了
teacher_id和status的普通索引,因为根据教师查询课程、根据状态筛选课程是高频操作。2.max_students和is_public字段共同控制选课逻辑,实现了“私有课程(邀请制)”和“公开课程(先到先得)”两种场景。
作业提交表 (edu_assignment_submission)
CREATE TABLE `edu_assignment_submission` ( `id` bigint NOT NULL AUTO_INCREMENT, `assignment_id` bigint NOT NULL COMMENT '作业ID', `student_id` bigint NOT NULL COMMENT '学生ID', `content` text COMMENT '文本答案', `attachment_url` varchar(500) DEFAULT NULL COMMENT '附件存储路径', `attachment_name` varchar(255) DEFAULT NULL COMMENT '附件原文件名', `submit_time` datetime DEFAULT CURRENT_TIMESTAMP COMMENT '提交时间', `grade` decimal(5,2) DEFAULT NULL COMMENT '得分(百分制)', `teacher_comment` varchar(500) DEFAULT NULL COMMENT '教师评语', `grade_time` datetime DEFAULT NULL COMMENT '批改时间', `status` tinyint DEFAULT '1' COMMENT '状态:1-已提交未批改,2-已批改,3-迟交', PRIMARY KEY (`id`), UNIQUE KEY `uk_assignment_student` (`assignment_id`,`student_id`), -- 唯一约束,防止重复提交 KEY `idx_assignment_id` (`assignment_id`), KEY `idx_student_id` (`student_id`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='作业提交表';踩坑记录:最初我们没有在
(assignment_id, student_id)上建立唯一约束,导致在并发情况下,一个学生可能通过快速点击提交按钮产生多条提交记录,造成数据混乱。加上唯一约束是保证数据一致性的简单有效方法。status字段的设计避免了在业务逻辑中频繁通过grade是否为NULL来判断是否批改。
4. 后端核心功能模块实现详解
4.1 使用MyBatis-Plus代码生成器快速启动
在确定数据库表后,第一件事就是使用MyBatis-Plus的代码生成器(mybatis-plus-generator)来生成基础代码。这能节省你大量编写单调的Entity、Mapper、Service和Controller模板代码的时间。
核心配置步骤:
- 在
pom.xml中引入mybatis-plus-generator依赖以及对应的模板引擎依赖(如Freemarker)。 - 编写一个简单的Java代码生成类,配置数据源、全局策略、包信息、模板引擎。
- 指定要生成的表名,运行该类的main方法。
生成后的代码结构:
entity/Course.java:实体类,使用Lombok注解。mapper/CourseMapper.java:Mapper接口,继承了MyBatis-Plus的BaseMapper<Course>,自带基本CRUD方法。service/ICourseService.java和service/impl/CourseServiceImpl.java:服务层接口与实现。controller/CourseController.java:一个最基础的RESTful控制器。
重要提示:生成器生成的Controller通常只包含最基础的增删改查API,你需要根据业务逻辑进行大幅改造和增强。它只是一个高效的“脚手架”。
4.2 业务逻辑层(Service)开发要点
Service层是业务逻辑的核心。这里以“学生选课”这个典型业务为例,展示如何编写一个具备事务性和完整业务校验的Service方法。
@Service @Transactional // 声明事务 public class CourseSelectionServiceImpl implements CourseSelectionService { @Autowired private CourseMapper courseMapper; @Autowired private StudentCourseMapper studentCourseMapper; @Autowired private UserMapper userMapper; @Override public ApiResult selectCourse(Long studentId, Long courseId) { // 1. 参数校验 if (studentId == null || courseId == null) { return ApiResult.fail("参数错误"); } // 2. 查询课程实体 Course course = courseMapper.selectById(courseId); if (course == null) { return ApiResult.fail("课程不存在"); } if (course.getStatus() != 1) { // 假设1为进行中 return ApiResult.fail("该课程暂不可选"); } // 3. 查询学生实体 User student = userMapper.selectById(studentId); if (student == null || !"student".equals(student.getRole())) { return ApiResult.fail("学生信息错误"); } // 4. 检查是否已选该课程 LambdaQueryWrapper<StudentCourse> wrapper = new LambdaQueryWrapper<>(); wrapper.eq(StudentCourse::getStudentId, studentId) .eq(StudentCourse::getCourseId, courseId); if (studentCourseMapper.selectCount(wrapper) > 0) { return ApiResult.fail("已选过该课程"); } // 5. 检查课程人数限制 if (course.getMaxStudents() > 0) { Integer currentCount = studentCourseMapper.countByCourseId(courseId); if (currentCount >= course.getMaxStudents()) { return ApiResult.fail("课程人数已满"); } } // 6. 执行选课操作 StudentCourse sc = new StudentCourse(); sc.setStudentId(studentId); sc.setCourseId(courseId); sc.setSelectTime(new Date()); studentCourseMapper.insert(sc); // 7. 可选:记录日志或发送通知 // logService.log(studentId, "选课", "课程:" + course.getCourseName()); return ApiResult.success("选课成功"); } }实操心得:1. 事务注解
@Transactional非常重要,它能保证“检查人数”和“插入选课记录”这两个操作是一个原子操作,防止在高并发下出现超选。2. 业务校验要前置,并且尽可能详细,给前端明确的错误提示。3. 复杂的查询条件,使用MyBatis-Plus的LambdaQueryWrapper,代码更清晰且类型安全。
4.3 文件上传与存储策略
教学资源模块涉及大量的文件上传。我们需要考虑文件存储的位置、命名、访问和安全性。
1. 存储位置选择:
- 本地存储:最简单,使用Spring Boot的
MultipartFile接收文件,保存到服务器磁盘的某个目录(如/upload)。缺点是扩容和备份麻烦,且分布式部署时文件不共享。 - 对象存储(推荐):使用阿里云OSS、腾讯云COS或MinIO(自建私有对象存储)。它们提供高可用、高扩展的存储服务,通过URL访问,完美解耦应用服务器。毕业设计如果预算有限,可以使用MinIO在本地搭建,体验与公有云类似。
2. 核心实现步骤(以本地存储为例):
@RestController @RequestMapping("/api/resource") public class ResourceController { @Value("${file.upload-dir}") // 从配置文件中读取存储路径 private String uploadDir; @PostMapping("/upload") public ApiResult uploadFile(@RequestParam("file") MultipartFile file, @RequestParam Long courseId) { if (file.isEmpty()) { return ApiResult.fail("文件为空"); } // 生成唯一文件名,防止覆盖 String originalFilename = file.getOriginalFilename(); String fileExtension = originalFilename.substring(originalFilename.lastIndexOf(".")); String storedFilename = UUID.randomUUID().toString() + fileExtension; // 构建存储路径 Path uploadPath = Paths.get(uploadDir, String.valueOf(courseId)); try { Files.createDirectories(uploadPath); // 创建课程目录 Path filePath = uploadPath.resolve(storedFilename); file.transferTo(filePath.toFile()); // 将文件信息存入数据库 Resource resource = new Resource(); resource.setCourseId(courseId); resource.setOriginalName(originalFilename); resource.setStoredName(storedFilename); resource.setFileSize(file.getSize()); resource.setFileType(file.getContentType()); resource.setDownloadPath("/download/" + courseId + "/" + storedFilename); // 映射的访问路径 resourceMapper.insert(resource); return ApiResult.success("上传成功", resource); } catch (IOException e) { log.error("文件上传失败", e); return ApiResult.fail("上传失败"); } } }3. 静态资源映射:为了让/download/{courseId}/{filename}这个路径能访问到实际文件,需要在Spring Boot配置中做静态资源映射。
@Configuration public class WebConfig implements WebMvcConfigurer { @Value("${file.upload-dir}") private String uploadDir; @Override public void addResourceHandlers(ResourceHandlerRegistry registry) { // 将本地文件目录映射到URL路径 registry.addResourceHandler("/download/**") .addResourceLocations("file:" + uploadDir + File.separator); } }避坑指南:1. 一定要限制上传文件的大小(在
application.yml中配置spring.servlet.multipart.max-file-size),防止恶意上传大文件拖垮服务器。2. 要对文件后缀名进行白名单校验(如只允许.docx,.mp4等),防止上传可执行脚本等危险文件。3. 使用UUID重命名文件是良好实践,既能避免重名,也能一定程度上隐藏原始文件名信息。
5. 前端Vue 3 + Element Plus整合实战
5.1 项目初始化与基础配置
使用Vite创建Vue 3项目比传统的Vue CLI更快。
npm create vue@latest my-teaching-system-frontend # 按照提示选择需要的特性(Router, Pinia, ESLint等) cd my-teaching-system-frontend npm install npm install element-plus axios在main.js中引入Element Plus和样式:
import { createApp } from 'vue' import App from './App.vue' import router from './router' import ElementPlus from 'element-plus' import 'element-plus/dist/index.css' import * as ElementPlusIconsVue from '@element-plus/icons-vue' const app = createApp(App) for (const [key, component] of Object.entries(ElementPlusIconsVue)) { app.component(key, component) } app.use(router) app.use(ElementPlus) app.mount('#app')5.2 封装Axios请求与全局状态管理
在src/utils/request.js中封装Axios实例,统一处理请求拦截(添加Token)、响应拦截(处理错误)和基础URL。
import axios from 'axios' import { ElMessage } from 'element-plus' import router from '../router' const service = axios.create({ baseURL: 'http://localhost:8080/api', // 后端API地址 timeout: 10000 }) // 请求拦截器 service.interceptors.request.use( config => { const token = localStorage.getItem('token') if (token) { config.headers['Authorization'] = 'Bearer ' + token } return config }, error => { return Promise.reject(error) } ) // 响应拦截器 service.interceptors.response.use( response => { const res = response.data // 假设后端统一返回格式为 { code: 200, data: {}, msg: 'success' } if (res.code !== 200) { ElMessage.error(res.msg || 'Error') // 如果是401未授权,跳转到登录页 if (res.code === 401) { localStorage.removeItem('token') router.push('/login') } return Promise.reject(new Error(res.msg || 'Error')) } else { return res.data // 直接返回业务数据 } }, error => { ElMessage.error(error.message || '网络错误') return Promise.reject(error) } ) export default service使用Pinia进行全局状态管理,例如管理用户登录状态。
// stores/user.js import { defineStore } from 'pinia' import { ref } from 'vue' export const useUserStore = defineStore('user', () => { const token = ref(localStorage.getItem('token') || '') const userInfo = ref(JSON.parse(localStorage.getItem('userInfo') || '{}')) const setToken = (newToken) => { token.value = newToken localStorage.setItem('token', newToken) } const setUserInfo = (info) => { userInfo.value = info localStorage.setItem('userInfo', JSON.stringify(info)) } const logout = () => { token.value = '' userInfo.value = {} localStorage.removeItem('token') localStorage.removeItem('userInfo') } return { token, userInfo, setToken, setUserInfo, logout } })5.3 典型页面组件开发:课程列表与选课
展示一个使用Element Plus组件、调用后端API的典型页面。
<template> <div class="course-container"> <el-row :gutter="20"> <el-col :span="6" v-for="course in courseList" :key="course.id"> <el-card class="course-card" shadow="hover"> <img :src="course.coverImage || defaultCover" class="course-cover"/> <div class="course-info"> <h3>{{ course.courseName }}</h3> <p class="teacher">教师:{{ course.teacherName }}</p> <p class="description">{{ course.description }}</p> <div class="card-footer"> <span>已选:{{ course.selected }}/{{ course.maxStudents || '不限' }}</span> <el-button type="primary" size="small" :disabled="course.isSelected || (course.maxStudents > 0 && course.selected >= course.maxStudents)" @click="handleSelect(course.id)"> {{ course.isSelected ? '已选' : '选课' }} </el-button> </div> </div> </el-card> </el-col> </el-row> <!-- 分页组件 --> <el-pagination @current-change="handlePageChange" :current-page="queryParams.pageNum" :page-size="queryParams.pageSize" layout="total, prev, pager, next, jumper" :total="total"> </el-pagination> </div> </template> <script setup> import { ref, onMounted } from 'vue' import { ElMessage } from 'element-plus' import request from '@/utils/request' import defaultCover from '@/assets/default-course.jpg' const courseList = ref([]) const total = ref(0) const queryParams = ref({ pageNum: 1, pageSize: 8, keyword: '' }) const fetchCourseList = async () => { try { const res = await request.get('/course/list', { params: queryParams.value }) courseList.value = res.list total.value = res.total } catch (error) { console.error('获取课程列表失败', error) } } const handleSelect = async (courseId) => { try { await request.post('/course/select', { courseId }) ElMessage.success('选课成功') // 重新获取列表,更新按钮状态 fetchCourseList() } catch (error) { // 错误信息已在axios拦截器中统一提示 } } const handlePageChange = (newPage) => { queryParams.value.pageNum = newPage fetchCourseList() } onMounted(() => { fetchCourseList() }) </script>前端开发心得:1. 善用Element Plus的布局组件(
el-row,el-col)和卡片组件(el-card)能快速搭建出规整的界面。2. 将API请求、状态管理、工具函数进行合理的封装和模块化,是保持前端代码清晰可维护的关键。3. 对于列表页,分页、搜索、筛选是标配,后端需要提供对应的支持。
6. 系统部署与演示准备
6.1 后端打包与运行
Spring Boot项目打包非常简单。在项目根目录下执行:
mvn clean package -DskipTests打包成功后,会在target目录下生成一个xxx.jar文件。这个JAR文件是可直接运行的“Fat Jar”,包含了所有依赖和嵌入式Tomcat。
运行方式:
# 最简单的方式 java -jar your-project-name.jar # 指定配置文件(常用于生产环境) java -jar your-project-name.jar --spring.config.location=application-prod.yml # 后台运行并输出日志到文件 nohup java -jar your-project-name.jar > app.log 2>&1 &关键生产配置(application-prod.yml):
server: port: 8080 servlet: context-path: /api # 统一API前缀 spring: datasource: url: jdbc:mysql://your-mysql-server:3306/teaching_system?useUnicode=true&characterEncoding=utf8&useSSL=false&serverTimezone=Asia/Shanghai username: prod_user password: strong_password hikari: maximum-pool-size: 10 # 连接池配置 servlet: multipart: max-file-size: 50MB max-request-size: 100MB file: upload-dir: /opt/upload_files # 生产环境上传目录 logging: file: name: /opt/logs/teaching-system.log level: com.yourcompany: DEBUG部署注意事项:1.永远不要在配置文件中硬编码数据库密码。应使用环境变量(
${DB_PASSWORD})或配置中心。2. 确保运行JAR文件的用户对上传目录(/opt/upload_files)和日志目录有读写权限。3. 考虑使用Nginx作为反向代理,处理静态资源、负载均衡和SSL加密(HTTPS)。
6.2 前端构建与部署
在Vue项目根目录下运行构建命令:
npm run build构建完成后,会生成一个dist目录,里面是优化和压缩过的静态文件(HTML, JS, CSS)。
部署方式:
- 直接放到后端静态资源目录(最简单):将
dist目录下的所有文件,复制到Spring Boot项目的src/main/resources/static目录下,然后重新打包后端。这样访问http://your-server:8080就能看到前端页面。但这种方式前后端耦合,不推荐用于复杂项目。 - 使用独立的Web服务器(推荐):将
dist目录放到Nginx或Apache的HTML目录下。然后配置Nginx,将API请求代理到后端Spring Boot应用。# Nginx 配置示例 server { listen 80; server_name your-domain.com; # 前端静态资源 location / { root /usr/share/nginx/html/dist; index index.html index.htm; try_files $uri $uri/ /index.html; # 支持Vue Router的history模式 } # 反向代理后端API location /api/ { proxy_pass http://localhost: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; } }
6.3 准备演示视频与毕业设计文档
对于毕业设计来说,一个清晰的演示视频和一份结构完整的论文/设计文档至关重要。
演示视频录制要点:
- 脚本先行:提前写好演示脚本,规划好演示路径(例如:管理员登录->创建课程->教师登录->发布作业->学生登录->选课->提交作业->教师批改)。
- 突出亮点:重点演示核心业务流程和你的特色功能(如自动批改、学情分析图表等)。
- 控制时长:控制在5-10分钟内,语速平稳,配合鼠标操作进行讲解。可以使用OBS、Camtasia等软件录制。
- 环境准备:确保演示环境干净、数据准备充分(提前录入一些示例课程、用户)。
毕业设计文档结构建议:
- 摘要与关键词
- 绪论:项目背景、意义、国内外研究现状。
- 相关技术介绍:简要介绍Spring Boot、Vue.js、MySQL等选型技术。
- 系统分析:可行性分析、需求分析(用例图、功能模块图)。
- 系统设计:总体架构设计、数据库设计(E-R图、核心表结构)、接口设计。
- 系统实现:分模块展示关键代码、界面截图并说明。
- 系统测试:描述测试环境、测试用例(功能测试、性能测试)和测试结果。
- 总结与展望:总结完成的工作,分析不足之处,提出未来可改进的方向。
- 参考文献
- 致谢
7. 常见问题排查与性能优化锦囊
在实际开发和答辩演示中,你肯定会遇到各种问题。这里总结几个高频问题及其解决方案。
问题1:前端调用后端API出现CORS(跨域)错误。
- 现象:浏览器控制台报错:
Access-Control-Allow-Origin。 - 原因:前端运行在
localhost:5173,后端在localhost:8080,端口不同导致跨域。 - 解决方案(后端解决):在Spring Boot的配置类中添加全局CORS配置。
@Configuration public class CorsConfig implements WebMvcConfigurer { @Override public void addCorsMappings(CorsRegistry registry) { registry.addMapping("/api/**") // 针对所有/api开头的接口 .allowedOriginPatterns("*") // 生产环境应指定具体前端地址 .allowedMethods("GET", "POST", "PUT", "DELETE", "OPTIONS") .allowedHeaders("*") .allowCredentials(true) .maxAge(3600); } }
问题2:文件上传后,无法通过URL访问。
- 现象:上传成功,数据库有记录,但
http://server:port/download/xxx返回404。 - 排查:
- 检查
WebConfig中的addResourceHandlers映射路径是否正确,特别是file:前缀和目录分隔符。 - 检查服务器上文件是否真的存在于
uploadDir/courseId/filename路径下。 - 检查服务器防火墙或安全组是否开放了对应端口。
- 检查
- 解决:确保路径拼接正确,对于Windows(
C:\upload)和Linux(/opt/upload)系统,路径写法不同。使用File.separator或直接使用/(Spring能处理)可以增强兼容性。
问题3:选课等高并发操作出现数据不一致(超选)。
- 现象:课程限选100人,最终可能录入了101人。
- 原因:单纯的“查询当前人数”和“插入记录”不是原子操作,在并发请求下,多个线程可能同时查询到人数为99,然后都执行插入,导致超员。
- 解决方案:
- 数据库唯一约束:在
student_course表上建立(student_id, course_id)的唯一索引,防止同一个学生重复选课,但无法防止不同学生超选。 - 数据库悲观锁:在查询课程人数时使用
SELECT ... FOR UPDATE锁定该行记录,但性能损耗大。 - 应用层乐观锁(推荐):在
course表中增加一个版本号字段version。更新人数时,检查版本号是否匹配。
如果更新影响的行数为0,说明并发更新失败,让前端重试或提示“选课失败,请重试”。UPDATE edu_course SET current_count = current_count + 1, version = version + 1 WHERE id = #{courseId} AND version = #{oldVersion} AND current_count < max_students; - 使用Redis分布式锁:在分布式环境下更优,但毕业设计单机场景用乐观锁足矣。
- 数据库唯一约束:在
问题4:系统运行一段时间后变慢。
- 可能原因与优化:
- 数据库慢查询:使用
EXPLAIN分析慢SQL,为高频查询条件字段添加索引(如course_id,student_id,status)。避免SELECT *,只查询需要的字段。 - 连接池耗尽:检查并合理配置HikariCP连接池参数(
maximum-pool-size,connection-timeout)。 - JVM内存问题:启动JAR时设置合理的堆内存参数,例如
java -Xms512m -Xmx1024m -jar ...。 - 大文件下载拖慢服务器:对于视频等大文件,建议使用对象存储的直链,或者通过Nginx的
X-Accel-Redirect功能进行代理下载,减轻应用服务器压力。
- 数据库慢查询:使用
问题5:如何让项目在答辩时更出彩?
- 数据可视化:使用ECharts或AntV为教师后台添加简单的数据看板,展示课程选课人数趋势、作业提交率、成绩分布等图表。
- 引入消息机制:使用WebSocket或SSE实现简单的站内信或实时通知功能,当作业被批改、收到新回复时给用户提示。
- 添加简单的全文搜索:对于课程、资料,可以集成Elasticsearch或使用MySQL的全文索引,实现关键词搜索功能。
- 编写详细的README.md:在项目根目录提供清晰的说明,包括项目简介、技术栈、快速启动指南、配置说明等,这体现了你的工程素养。
- 代码质量:保持代码规范,关键方法写好注释,使用Git进行版本管理,并将仓库提交到Gitee或GitHub。
本文还有配套的精品资源,点击获取