在实际 Java Web 项目开发中,尤其是毕业设计阶段,一个功能完整、结构清晰、技术栈主流的课程作业管理系统是检验学习成果的绝佳实践。它要求开发者不仅要串联起 Java 后端、Vue 前端、Spring Boot 框架和 MySQL 数据库,更要理解前后端分离架构下的数据流转、业务逻辑分层以及项目从零到一的构建过程。本文将以一个可运行的课程作业管理系统为例,带你从零开始,完成环境搭建、技术选型、核心模块开发、前后端联调,直至最终部署。整个过程将严格遵循工程实践,不仅提供可复现的代码和配置,更会深入解释每一步的设计考量与常见陷阱,确保你不仅能“做出来”,更能“弄明白”。
1. 理解系统需求与技术选型
在动手编码之前,明确系统边界和采用的技术栈是项目成功的基石。一个典型的课程作业管理系统,其核心是围绕“教师发布作业、学生提交作业、教师批改作业”这一主线展开的。
1.1 核心业务功能拆解
系统主要涉及三类角色:管理员、教师和学生。其核心功能模块如下:
- 用户管理模块:实现不同角色的注册、登录、信息维护及权限控制(如RBAC)。这是系统安全与业务隔离的基础。
- 课程管理模块:管理员或教师可以创建课程,并将学生加入到特定课程中,建立师生关联关系。
- 作业管理模块:这是系统的核心。教师可以在课程下发布作业(含标题、内容、截止日期、附件等),学生查看并提交作业(支持文件上传),教师随后进行批改并给出分数和评语。
- 通知与交互模块:系统应能提供作业发布提醒、提交截止提醒、批改完成通知等基础消息功能。
1.2 技术栈决策与组件说明
根据项目标题和主流实践,我们选择以下技术栈,并解释其选型理由:
- 后端 (Java + Spring Boot):
- Spring Boot 2.7.x: 作为项目基石,它提供了自动配置、内嵌服务器和“约定大于配置”的理念,能极大简化 Spring 应用的初始搭建和开发过程。选择 2.7.x 这个长期支持版本,兼顾了稳定性和社区支持。
- Spring MVC: 处理 HTTP 请求,实现 RESTful API,是前后端分离架构中后端的标准形态。
- Spring Data JPA / MyBatis-Plus: 用于数据库操作。JPA 更面向对象,开发速度快;MyBatis-Plus 在 MyBatis 基础上增强了功能,SQL 更灵活。本文将基于 MyBatis-Plus 进行演示,因其在复杂查询和 SQL 优化上对开发者更友好。
- Spring Security: 处理认证(登录)和授权(权限检查),是保障系统安全的核心组件。
- 前端 (Vue.js):
- Vue 3 + Composition API: 作为当前主流版本,提供了更好的 TypeScript 支持和更灵活的逻辑复用能力。
- Vue Router: 管理前端路由,实现单页面应用(SPA)的无刷新跳转。
- Pinia / Vuex: 进行状态管理,用于存储用户登录态、全局配置等跨组件数据。
- Element Plus / Ant Design Vue: UI 组件库,能快速构建出美观且交互一致的管理界面。
- 数据库 (MySQL 8.0+): 成熟的关系型数据库,事务支持完善,社区活跃。使用 8.0 及以上版本可以获得更好的性能和新特性支持(如窗口函数)。
- 构建与依赖管理:
- Maven: 管理 Java 项目依赖、构建生命周期。
- Node.js & npm/pnpm/yarn: 管理前端项目依赖和构建。
这个技术栈组合在就业市场具有很高的认可度,完成该项目能有效巩固这些核心技能。
2. 项目初始化与基础环境搭建
一个清晰的项目结构是团队协作和后期维护的保障。我们将采用前后端分离的目录结构,后端和前端作为两个独立的工程。
2.1 后端 Spring Boot 项目初始化
使用 Spring Initializr (start.spring.io) 或 IDE (如 IntelliJ IDEA) 创建项目。
- 项目信息:
- Project: Maven
- Language: Java
- Spring Boot: 2.7.18 (LTS版本)
- Group:
com.example - Artifact:
coursework-backend - Packaging: Jar
- Java: 11 或 17
- 依赖选择:
- Spring Web
- MyBatis Framework
- MySQL Driver
- Lombok (减少样板代码)
- Spring Security (可选,初期可简化)
生成项目后,其核心目录结构应如下:
coursework-backend/ ├── src/ │ ├── main/ │ │ ├── java/com/example/coursework/ │ │ │ ├── CourseworkApplication.java # 启动类 │ │ │ ├── config/ # 配置类(如MyBatis-Plus, Security, Cors) │ │ │ ├── controller/ # 控制器层,接收请求 │ │ │ ├── service/ # 业务逻辑层 │ │ │ ├── mapper/ # MyBatis Mapper接口层 │ │ │ ├── entity/ # 实体类,对应数据库表 │ │ │ └── dto/ # 数据传输对象 │ │ └── resources/ │ │ ├── application.yml # 主配置文件 │ │ └── mapper/ # MyBatis XML映射文件(如果用XML方式) │ └── test/ # 测试代码 └── pom.xml # Maven依赖管理- 关键配置
application.yml:
server: port: 8080 servlet: context-path: /api spring: datasource: driver-class-name: com.mysql.cj.jdbc.Driver url: jdbc:mysql://localhost:3306/coursework_db?useUnicode=true&characterEncoding=utf-8&useSSL=false&serverTimezone=Asia/Shanghai username: root password: yourpassword servlet: multipart: max-file-size: 10MB max-request-size: 20MB mybatis-plus: configuration: log-impl: org.apache.ibatis.logging.stdout.StdOutImpl # 开发时开启SQL日志 global-config: db-config: logic-delete-field: deleted # 全局逻辑删除字段名 logic-delete-value: 1 logic-not-delete-value: 0 mapper-locations: classpath*:/mapper/**/*.xml注意:
serverTimezone=Asia/Shanghai对于避免时区导致的日期时间问题至关重要。生产环境务必移除log-impl配置或改为文件日志。
2.2 前端 Vue 项目初始化
使用 Vue 官方脚手架创建项目。
# 使用 npm npm create vue@latest coursework-frontend # 或使用更快的 pnpm pnpm create vue@latest coursework-frontend在创建向导中,选择需要的功能:TypeScript, Vue Router, Pinia, ESLint。 创建完成后,安装 UI 库和请求库:
cd coursework-frontend npm install element-plus axios # 或 pnpm add element-plus axios调整src/main.ts引入 Element Plus:
import { createApp } from 'vue' import App from './App.vue' import router from './router' import { createPinia } from 'pinia' import ElementPlus from 'element-plus' import 'element-plus/dist/index.css' const app = createApp(App) app.use(createPinia()) app.use(router) app.use(ElementPlus) app.mount('#app')配置axios实例 (src/utils/request.ts),统一处理请求基路径、超时、拦截器(用于自动添加Token)和响应处理。
3. 数据库设计与核心实体建模
数据库设计是业务的基石。我们需要设计出规范、高效且易于扩展的表结构。
3.1 核心表结构设计
以下是简化版的核心表设计,包含必要的字段和注释。
-- 用户表 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 '真实姓名', `role` tinyint NOT NULL COMMENT '角色:0-管理员,1-教师,2-学生', `email` varchar(100) DEFAULT NULL COMMENT '邮箱', `avatar` varchar(500) DEFAULT NULL COMMENT '头像URL', `status` tinyint DEFAULT '1' COMMENT '状态:0-禁用,1-正常', `create_time` datetime DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间', `update_time` datetime DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间', `deleted` tinyint DEFAULT '0' COMMENT '逻辑删除标记', PRIMARY KEY (`id`), UNIQUE KEY `uk_username` (`username`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='用户表'; -- 课程表 CREATE TABLE `course` ( `id` bigint NOT NULL AUTO_INCREMENT COMMENT '主键', `course_code` varchar(20) NOT NULL COMMENT '课程代码', `course_name` varchar(100) NOT NULL COMMENT '课程名称', `description` text COMMENT '课程描述', `teacher_id` bigint NOT NULL COMMENT '授课教师ID', `academic_year` varchar(20) DEFAULT NULL COMMENT '学年学期,如2023-2024-1', `create_time` datetime DEFAULT CURRENT_TIMESTAMP, `update_time` datetime DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, `deleted` tinyint DEFAULT '0', PRIMARY KEY (`id`), KEY `idx_teacher_id` (`teacher_id`), CONSTRAINT `fk_course_teacher` FOREIGN KEY (`teacher_id`) REFERENCES `sys_user` (`id`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='课程表'; -- 学生选课关联表 CREATE TABLE `course_student` ( `id` bigint NOT NULL AUTO_INCREMENT, `course_id` bigint NOT NULL COMMENT '课程ID', `student_id` bigint NOT NULL COMMENT '学生ID', `join_time` datetime DEFAULT CURRENT_TIMESTAMP COMMENT '加入时间', PRIMARY KEY (`id`), UNIQUE KEY `uk_course_student` (`course_id`,`student_id`), KEY `idx_student_id` (`student_id`), CONSTRAINT `fk_cs_course` FOREIGN KEY (`course_id`) REFERENCES `course` (`id`), CONSTRAINT `fk_cs_student` FOREIGN KEY (`student_id`) REFERENCES `sys_user` (`id`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='学生选课表'; -- 作业表 CREATE TABLE `assignment` ( `id` bigint NOT NULL AUTO_INCREMENT, `course_id` bigint NOT NULL COMMENT '所属课程ID', `title` varchar(200) NOT NULL COMMENT '作业标题', `content` text COMMENT '作业要求(富文本)', `attachment_url` varchar(500) DEFAULT NULL COMMENT '附件存储路径', `deadline` datetime NOT NULL COMMENT '截止时间', `total_score` decimal(5,2) DEFAULT NULL COMMENT '总分', `publish_status` tinyint DEFAULT '0' COMMENT '发布状态:0-草稿,1-已发布', `create_time` datetime DEFAULT CURRENT_TIMESTAMP, `update_time` datetime DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, `deleted` tinyint DEFAULT '0', PRIMARY KEY (`id`), KEY `idx_course_id` (`course_id`), CONSTRAINT `fk_assignment_course` FOREIGN KEY (`course_id`) REFERENCES `course` (`id`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='作业表'; -- 作业提交表 CREATE TABLE `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 '提交附件路径', `submit_time` datetime DEFAULT CURRENT_TIMESTAMP COMMENT '提交时间', `grade` decimal(5,2) DEFAULT NULL COMMENT '得分', `feedback` text COMMENT '教师评语', `grade_time` datetime DEFAULT NULL COMMENT '批改时间', `status` tinyint DEFAULT '0' COMMENT '状态:0-已提交待批改,1-已批改', PRIMARY KEY (`id`), UNIQUE KEY `uk_assignment_student` (`assignment_id`,`student_id`), -- 防止重复提交 KEY `idx_student_id` (`student_id`), CONSTRAINT `fk_submission_assignment` FOREIGN KEY (`assignment_id`) REFERENCES `assignment` (`id`), CONSTRAINT `fk_submission_student` FOREIGN KEY (`student_id`) REFERENCES `sys_user` (`id`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='作业提交表';3.2 实体类与 MyBatis-Plus 映射
在后端项目中,创建与表对应的实体类。使用 Lombok 简化代码,并使用 MyBatis-Plus 注解进行映射。
// src/main/java/com/example/coursework/entity/User.java package com.example.coursework.entity; import com.baomidou.mybatisplus.annotation.*; import lombok.Data; import java.time.LocalDateTime; @Data @TableName("sys_user") // 指定表名 public class User { @TableId(type = IdType.AUTO) // 主键自增 private Long id; private String username; private String password; private String realName; private Integer role; // 0-admin, 1-teacher, 2-student private String email; private String avatar; private Integer status; @TableField(fill = FieldFill.INSERT) // 插入时自动填充 private LocalDateTime createTime; @TableField(fill = FieldFill.INSERT_UPDATE) // 插入和更新时自动填充 private LocalDateTime updateTime; @TableLogic // 逻辑删除注解 private Integer deleted; }需要配置一个元对象处理器 (MetaObjectHandler) 来自动填充createTime和updateTime字段。
4. 核心业务模块实现与前后端联调
我们将以实现“作业发布与提交”这个核心流程为例,串联前后端。
4.1 后端 API 开发 (Spring Boot + MyBatis-Plus)
1. 创建 Mapper 接口和 Service
// AssignmentMapper.java package com.example.coursework.mapper; import com.baomidou.mybatisplus.core.mapper.BaseMapper; import com.example.coursework.entity.Assignment; import org.apache.ibatis.annotations.Mapper; @Mapper public interface AssignmentMapper extends BaseMapper<Assignment> { // 可以在此定义复杂的自定义SQL方法 }// AssignmentService.java package com.example.coursework.service; import com.baomidou.mybatisplus.extension.service.IService; import com.example.coursework.entity.Assignment; public interface AssignmentService extends IService<Assignment> { // 定义业务方法,如发布作业 boolean publishAssignment(Long assignmentId); }// AssignmentServiceImpl.java package com.example.coursework.service.impl; import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl; import com.example.coursework.entity.Assignment; import com.example.coursework.mapper.AssignmentMapper; import com.example.coursework.service.AssignmentService; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; @Service public class AssignmentServiceImpl extends ServiceImpl<AssignmentMapper, Assignment> implements AssignmentService { @Override @Transactional(rollbackFor = Exception.class) public boolean publishAssignment(Long assignmentId) { Assignment assignment = this.getById(assignmentId); if (assignment == null || assignment.getPublishStatus() == 1) { return false; } assignment.setPublishStatus(1); return this.updateById(assignment); } }2. 创建 Controller 和 DTO使用 DTO (Data Transfer Object) 来接收请求和返回数据,避免直接暴露实体类。
// AssignmentDTO.java (用于创建/更新作业) package com.example.coursework.dto; import lombok.Data; import javax.validation.constraints.NotBlank; import javax.validation.constraints.NotNull; import java.time.LocalDateTime; @Data public class AssignmentDTO { @NotNull(message = "课程ID不能为空") private Long courseId; @NotBlank(message = "作业标题不能为空") private String title; private String content; private String attachmentUrl; @NotNull(message = "截止时间不能为空") private LocalDateTime deadline; private Double totalScore; }// AssignmentController.java package com.example.coursework.controller; import com.example.coursework.dto.AssignmentDTO; import com.example.coursework.entity.Assignment; import com.example.coursework.service.AssignmentService; import com.example.coursework.vo.Result; import org.springframework.beans.BeanUtils; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.validation.annotation.Validated; import org.springframework.web.bind.annotation.*; import java.util.List; @RestController @RequestMapping("/assignment") public class AssignmentController { @Autowired private AssignmentService assignmentService; // 创建作业(草稿) @PostMapping public Result<Long> createAssignment(@Validated @RequestBody AssignmentDTO dto) { Assignment assignment = new Assignment(); BeanUtils.copyProperties(dto, assignment); assignment.setPublishStatus(0); // 默认为草稿 assignmentService.save(assignment); return Result.success(assignment.getId()); } // 发布作业 @PutMapping("/publish/{id}") public Result<Void> publishAssignment(@PathVariable Long id) { boolean success = assignmentService.publishAssignment(id); return success ? Result.success() : Result.error("作业不存在或已发布"); } // 根据课程ID查询作业列表 @GetMapping("/list") public Result<List<Assignment>> listByCourse(@RequestParam Long courseId) { // 使用MyBatis-Plus的Wrapper构建查询条件 LambdaQueryWrapper<Assignment> wrapper = new LambdaQueryWrapper<>(); wrapper.eq(Assignment::getCourseId, courseId) .eq(Assignment::getDeleted, 0) .orderByDesc(Assignment::getCreateTime); List<Assignment> list = assignmentService.list(wrapper); return Result.success(list); } }其中Result是一个统一的响应包装类,包含code,message,data字段。
3. 处理文件上传Spring Boot 通过MultipartFile接口简化文件上传。
// FileController.java package com.example.coursework.controller; import org.springframework.beans.factory.annotation.Value; import org.springframework.web.bind.annotation.*; import org.springframework.web.multipart.MultipartFile; import java.io.File; import java.io.IOException; import java.util.UUID; @RestController @RequestMapping("/file") public class FileController { @Value("${file.upload-dir:./uploads}") // 从配置读取,默认当前目录下的uploads private String uploadDir; @PostMapping("/upload") public Result<String> uploadFile(@RequestParam("file") MultipartFile file) { if (file.isEmpty()) { return Result.error("文件为空"); } // 生成唯一文件名,防止覆盖 String originalFilename = file.getOriginalFilename(); String fileExtension = originalFilename.substring(originalFilename.lastIndexOf(".")); String newFileName = UUID.randomUUID().toString() + fileExtension; File dest = new File(uploadDir + File.separator + newFileName); if (!dest.getParentFile().exists()) { dest.getParentFile().mkdirs(); } try { file.transferTo(dest); // 返回文件的访问路径(需要配置静态资源映射或使用云存储URL) return Result.success("/uploads/" + newFileName); } catch (IOException e) { e.printStackTrace(); return Result.error("文件上传失败"); } } }需要在application.yml中配置上传路径和静态资源映射。
4.2 前端页面与接口调用 (Vue 3 + Element Plus)
1. 作业列表页面 (AssignmentList.vue)
<template> <div class="assignment-list"> <el-button type="primary" @click="handleCreate">发布新作业</el-button> <el-table :data="assignmentList" style="width: 100%"> <el-table-column prop="title" label="作业标题" /> <el-table-column prop="deadline" label="截止时间" :formatter="formatDate" /> <el-table-column prop="publishStatus" label="状态"> <template #default="scope"> <el-tag :type="scope.row.publishStatus === 1 ? 'success' : 'info'"> {{ scope.row.publishStatus === 1 ? '已发布' : '草稿' }} </el-tag> </template> </el-table-column> <el-table-column label="操作"> <template #default="scope"> <el-button size="small" @click="handleEdit(scope.row)">编辑</el-button> <el-button size="small" type="danger" @click="handleDelete(scope.row)">删除</el-button> <el-button v-if="scope.row.publishStatus === 0" size="small" type="success" @click="handlePublish(scope.row)">发布</el-button> </template> </el-table-column> </el-table> <!-- 创建/编辑作业的对话框 --> <el-dialog v-model="dialogVisible" :title="dialogTitle"> <el-form :model="form" :rules="rules" ref="formRef"> <el-form-item label="作业标题" prop="title"> <el-input v-model="form.title" /> </el-form-item> <el-form-item label="截止时间" prop="deadline"> <el-date-picker v-model="form.deadline" type="datetime" placeholder="选择截止时间" /> </el-form-item> <el-form-item label="作业要求"> <el-input v-model="form.content" type="textarea" :rows="4" /> </el-form-item> <el-form-item label="附件"> <el-upload :action="uploadUrl" :on-success="handleUploadSuccess"> <el-button type="primary">点击上传</el-button> </el-upload> </el-form-item> </el-form> <template #footer> <el-button @click="dialogVisible = false">取消</el-button> <el-button type="primary" @click="submitForm">确定</el-button> </template> </el-dialog> </div> </template> <script setup lang="ts"> import { ref, onMounted } from 'vue' import { ElMessage, ElMessageBox } from 'element-plus' import type { Assignment } from '@/types/api' import { getAssignmentList, createAssignment, publishAssignment, deleteAssignment } from '@/api/assignment' import { uploadFile } from '@/api/file' const courseId = ref(1) // 应从路由或状态管理中获取实际课程ID const assignmentList = ref<Assignment[]>([]) const dialogVisible = ref(false) const form = ref({ courseId: courseId.value, title: '', content: '', deadline: '', attachmentUrl: '' }) const uploadUrl = '/api/file/upload' // 后端文件上传接口 const loadData = async () => { const res = await getAssignmentList(courseId.value) if (res.code === 200) { assignmentList.value = res.data } } const handleCreate = () => { dialogVisible.value = true // 重置表单 } const handlePublish = async (row: Assignment) => { try { await ElMessageBox.confirm('确认发布该作业?', '提示') const res = await publishAssignment(row.id) if (res.code === 200) { ElMessage.success('发布成功') loadData() } } catch (error) { // 用户取消 } } const handleUploadSuccess = (response: any) => { if (response.code === 200) { form.value.attachmentUrl = response.data ElMessage.success('附件上传成功') } } const submitForm = async () => { // 表单验证... const res = await createAssignment(form.value) if (res.code === 200) { ElMessage.success('创建成功') dialogVisible.value = false loadData() } } onMounted(() => { loadData() }) </script>2. 封装 API 请求 (src/api/assignment.ts)
import request from '@/utils/request' import type { Assignment, Result } from '@/types/api' export function getAssignmentList(courseId: number): Promise<Result<Assignment[]>> { return request({ url: '/assignment/list', method: 'get', params: { courseId } }) } export function createAssignment(data: any): Promise<Result<number>> { return request({ url: '/assignment', method: 'post', data }) } export function publishAssignment(id: number): Promise<Result<void>> { return request({ url: `/assignment/publish/${id}`, method: 'put' }) }4.3 解决跨域问题
前后端分离开发时,前端运行在localhost:5173,后端在localhost:8080,浏览器会因同源策略阻止请求。需要在后端配置 CORS。
// src/main/java/com/example/coursework/config/CorsConfig.java package com.example.coursework.config; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.web.cors.CorsConfiguration; import org.springframework.web.cors.UrlBasedCorsConfigurationSource; import org.springframework.web.filter.CorsFilter; @Configuration public class CorsConfig { @Bean public CorsFilter corsFilter() { CorsConfiguration config = new CorsConfiguration(); // 允许的源,生产环境应配置为具体域名 config.addAllowedOriginPattern("*"); // 允许的请求方法 config.addAllowedMethod("*"); // 允许的请求头 config.addAllowedHeader("*"); // 允许携带凭证(如Cookie) config.setAllowCredentials(true); // 预检请求的缓存时间(秒) config.setMaxAge(3600L); UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource(); source.registerCorsConfiguration("/**", config); return new CorsFilter(source); } }注意:生产环境中,
addAllowedOriginPattern("*")应替换为具体的前端域名,如"https://yourdomain.com",以增强安全性。
5. 系统安全、部署与常见问题排查
一个完整的系统必须考虑安全性和可部署性。
5.1 集成 Spring Security 实现认证授权
- 添加依赖:在
pom.xml中加入spring-boot-starter-security。 - 创建安全配置类:继承
WebSecurityConfigurerAdapter(Spring Boot 2.x) 或使用新式配置,定义哪些路径需要认证,如何配置密码加密、登录成功/失败处理等。 - 实现 UserDetailsService:从数据库加载用户信息,用于认证。
- 使用 JWT (JSON Web Token):在前后端分离项目中,通常使用无状态的 JWT 替代 Session。用户登录成功后,后端生成一个签名的 Token 返回给前端,前端后续请求在
Authorization请求头中携带此 Token。 - 创建 JWT 工具类:用于生成、解析和验证 Token。
- 创建过滤器:在 Spring Security 过滤器链中添加一个 JWT 认证过滤器,在
UsernamePasswordAuthenticationFilter之前,用于解析请求头中的 Token 并设置安全上下文。 - 前端存储 Token:登录成功后,将 Token 存储在
localStorage或sessionStorage中,并在axios请求拦截器中自动添加到请求头。
5.2 项目打包与部署
后端打包:
# 在项目根目录下 mvn clean package -DskipTests生成的coursework-backend-0.0.1-SNAPSHOT.jar位于target目录。可以通过java -jar命令运行。前端打包:
# 在前端项目根目录下 npm run build # 或 pnpm build生成的静态文件位于dist目录。部署方式:
- 开发/演示环境:前后端分开部署。后端运行 Jar 包,前端使用 Nginx 或直接通过 IDE 运行开发服务器并配置代理。
- 生产环境:
- 后端:将 Jar 包部署到 Linux 服务器,使用
systemd或supervisor等进程管理工具守护进程。配置application-prod.yml指定生产数据库、Redis(如需缓存)、文件存储路径等。 - 前端:将
dist目录下的文件放置到 Nginx 的html目录下,并配置 Nginx 将 API 请求反向代理到后端服务。 - 数据库:使用云数据库或自建 MySQL,做好定期备份。
- 后端:将 Jar 包部署到 Linux 服务器,使用
5.3 常见问题与排查路径
在开发联调和生产部署中,以下几个问题是高频出现点。
| 问题现象 | 可能原因 | 检查方式与解决方案 |
|---|---|---|
前端请求后端接口出现404 | 1. 后端服务未启动。 2. 请求 URL 路径错误。 3. 后端 context-path配置与前端请求前缀不匹配。 | 1. 检查后端控制台是否启动成功,端口是否被占用。 2. 核对浏览器开发者工具 Network 标签中的请求 URL 与后端 @RequestMapping路径。3. 检查后端 server.servlet.context-path配置,前端请求基地址需包含此路径。 |
前端请求出现CORS跨域错误 | 后端未正确配置 CORS,或配置的源、方法、头不匹配。 | 1. 检查后端CorsConfig是否生效,允许的源 (AllowedOrigin) 是否包含前端地址。2. 复杂请求(如带自定义头的 PUT)需要处理预检请求 (OPTIONS)。确保配置了addAllowedMethod("*")。 |
| 文件上传失败,大小受限 | 上传文件超过 Spring Boot 默认大小限制 (1MB)。 | 在application.yml中调整spring.servlet.multipart.max-file-size和max-request-size参数。同时,Nginx 等反向代理也可能有大小限制,需要同步调整。 |
| 数据库连接失败 | 1. 数据库服务未启动。 2. 连接 URL、用户名、密码错误。 3. 时区或 SSL 配置问题。 4. 驱动版本与 MySQL 版本不匹配。 | 1. 检查 MySQL 服务状态。 2. 核对 application.yml中的配置。3. 在连接 URL 中添加 serverTimezone=Asia/Shanghai&useSSL=false尝试。4. 确保使用 mysql-connector-java8.x 驱动连接 MySQL 5.7/8.0。 |
| 插入或查询中文乱码 | 1. 数据库、表字符集不是utf8mb4。2. 连接字符串未指定字符集。 3. 服务端、客户端字符集不统一。 | 1. 创建数据库时指定CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci。2. 在 JDBC URL 中添加 characterEncoding=utf-8。3. 检查操作系统、IDE 终端字符集。 |
前端页面刷新后路由丢失,显示404 | Vue Router 使用了history模式,但 Nginx 未配置try_files回退到index.html。 | 在 Nginx 配置中,对前端路由的 location 块内添加:try_files $uri $uri/ /index.html;。 |
| 登录成功后,后续请求仍提示未登录 | 1. Token 未正确存储或发送。 2. Token 已过期。 3. 后端 JWT 过滤器配置路径有误,或 Token 解析失败。 | 1. 检查前端请求拦截器是否在每次请求的 Header 中添加了Authorization: Bearer <token>。2. 检查 Token 有效期,实现刷新 Token 逻辑。 3. 查看后端日志,确认过滤器是否被触发,以及 Token 解析是否有异常。 |
5.4 毕业设计答辩要点与扩展方向
完成基础功能后,若想提升项目深度以应对答辩,可以考虑以下扩展方向:
- 引入 Redis:缓存课程信息、用户信息、热点作业数据,减轻数据库压力,并用于实现分布式 Session 或 Token 黑名单。
- 加入消息队列:使用 RabbitMQ 或 Kafka,将作业提交成功、批改完成等事件异步发送,解耦系统模块,并实现邮件或站内信通知。
- 实现分页与复杂查询:作业列表、学生列表使用 MyBatis-Plus 的分页插件,并支持多条件组合查询。
- 集成第三方登录:如使用 OAuth2.0 接入学校统一认证系统或微信扫码登录。
- 容器化部署:编写
Dockerfile将前后端分别容器化,使用docker-compose.yml一键启动整个应用栈(包含 MySQL、Redis)。 - 编写单元测试与集成测试:使用 JUnit 5 和 Mockito 对 Service 层关键方法进行单元测试,使用
@SpringBootTest进行集成测试,提高代码质量。 - 完善 API 文档:使用 Swagger/OpenAPI 3 (SpringDoc) 自动生成在线 API 文档,方便前端对接和答辩演示。
在答辩时,重点阐述你的技术选型理由、系统架构设计、数据库设计范式考量、解决的核心技术难点(如文件上传、权限控制、前后端分离部署)以及项目的可扩展性。清晰的流程图、ER 图和核心代码片段是很好的辅助材料。