每到毕业季,计算机相关专业的学生们都会面临一个共同的难题:如何高效、公平地完成毕业设计选题。传统的线下选题方式,如纸质表格、邮件沟通或简单的Excel共享,常常伴随着信息不同步、选题冲突、导师协调困难、进度难以追踪等一系列痛点。本文将分享一个基于Java和SpringBoot的完整毕业设计选题系统的设计与实现,从需求分析、技术选型、数据库设计到前后端代码实现,提供一套可直接部署、二次开发的实战项目方案。无论你是正在寻找毕业设计课题的学生,还是希望学习SpringBoot全栈开发的开发者,都能从本文中获得从零到一的完整构建思路和可运行的源码。
1. 系统需求分析与核心功能设计
在动手编码之前,明确系统的用户角色和核心功能是项目成功的关键。一个典型的毕业设计选题系统通常涉及三类用户:学生、教师(导师)和管理员。
1.1 用户角色与核心诉求
- 学生:
- 核心诉求:浏览所有可选的课题、查看课题详情(包括导师信息、要求、已选人数)、选择自己心仪的课题、查看自己的选题状态、与导师进行简单的在线沟通(如留言)。
- 痛点解决:避免信息不对称,实时查看课题名额,减少因沟通不畅导致的选题冲突。
- 教师(导师):
- 核心诉求:发布和管理自己的课题(增删改查)、审核选择自己课题的学生(通过/拒绝)、查看最终确定的学生名单、发布任务或通知。
- 痛点解决:简化课题发布流程,集中管理学生申请,高效完成双向选择。
- 管理员:
- 核心诉求:管理所有用户(学生、教师)账户、审核教师发布的课题(确保合规性)、监控整个选题流程的进度(如各阶段时间设置)、处理系统异常(如重置密码)。
- 痛点解决:确保系统运行的秩序和公平性,拥有最高权限进行全局调控。
1.2 系统核心功能模块
基于以上角色分析,我们可以将系统拆解为以下几个核心功能模块:
- 用户认证与权限管理模块:实现用户登录、注册(通常由管理员初始化或导入)、基于角色(ROLE_STUDENT, ROLE_TEACHER, ROLE_ADMIN)的页面访问和操作权限控制。
- 课题信息管理模块:
- 教师端:课题的创建、编辑、删除、发布与下架。
- 学生端:课题列表分页展示、多条件(如导师、课题名称、状态)检索、课题详情查看。
- 管理员端:课题审核(审核通过后方可对学生可见)。
- 选题流程管理模块:
- 学生端:提交选题申请、查看申请状态(待审核、已通过、被拒绝)、在允许的时间内取消申请或改选。
- 教师端:查看申请自己课题的学生列表、审核申请(通过或拒绝,需有理由可选)。
- 系统逻辑:确保一个学生同一时间只能有一个“待审核”或“已通过”的选题;确保课题人数不超过上限。
- 通知与简单沟通模块:系统站内消息通知,如选题结果通知、审核通知等。可扩展为简单的留言板功能。
- 后台管理模块:管理员专属功能,包括用户管理、系统公告发布、选题阶段时间节点配置(如开始时间、截止时间)、数据统计报表等。
2. 技术选型与环境准备
一个稳定、高效、易于开发和维护的技术栈是项目的基石。我们选择目前Java领域最主流的SpringBoot全家桶进行后端开发,配合成熟的前后端分离方案。
2.1 后端技术栈
- 核心框架:Spring Boot 2.7.x。它提供了极简的配置和快速启动能力,是构建现代Java Web应用的标准选择。
- 持久层框架:MyBatis-Plus 3.5.x。它在MyBatis的基础上进行了增强,提供了强大的CRUD操作和条件构造器,能极大减少SQL编写工作量。
- 安全框架:Spring Security 5.7.x。用于处理用户认证(登录)和授权(权限检查),是保护系统安全的必备组件。
- 数据库:MySQL 8.0。关系型数据库,用于存储用户、课题、选题记录等结构化数据。
- API文档:Knife4j 3.0.x。基于Swagger的增强UI,用于自动生成和调试后端RESTful API接口文档。
- 项目构建:Maven 3.6+ 或 Gradle。本文示例使用Maven。
- 其他工具:Lombok(简化Java Bean编写)、Hutool(Java工具类库)、Fastjson(JSON处理)。
2.2 前端技术栈(可选方案)
为了快速成型和降低前端复杂度,可以选择以下两种方案之一:
- 方案A(前后端不分离):使用Thymeleaf模板引擎。SpringBoot原生支持,适合全栈Java开发者,开发速度快,但前后端耦合。
- 方案B(前后端分离):使用Vue.js + Element UI。这是目前主流选择,前后端职责清晰,用户体验好。后端仅提供JSON API接口。 考虑到毕业设计项目通常需要展示完整流程,且为了聚焦后端核心逻辑,本文后续代码讲解将侧重于后端API的实现,并假定前端采用Vue+Element UI方案进行交互。前端代码会提供关键页面逻辑说明。
2.3 开发环境准备
- JDK:安装JDK 8或JDK 11,并配置好
JAVA_HOME环境变量。 - IDE:IntelliJ IDEA(推荐)或Eclipse。
- MySQL:安装MySQL 8.0,并创建一个名为
graduation_topic的数据库,字符集建议为utf8mb4。CREATE DATABASE IF NOT EXISTS `graduation_topic` DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci; - Maven:安装Maven并配置好仓库镜像(可使用阿里云镜像加速)。
- 浏览器:用于测试前端(如Chrome)和API(可使用Postman或Knife4j界面)。
3. 数据库设计与核心表结构
良好的数据库设计是系统稳定运行的基础。以下是核心表结构设计,使用MyBatis-Plus时,对应的实体类将基于此设计。
3.1 用户表 (sys_user)
存储所有系统用户(学生、教师、管理员)的基础信息。
CREATE TABLE `sys_user` ( `id` bigint NOT NULL AUTO_INCREMENT COMMENT '主键ID', `username` varchar(50) NOT NULL COMMENT '用户名(学号/工号)', `password` varchar(100) NOT NULL COMMENT '加密后的密码', `real_name` varchar(20) DEFAULT NULL COMMENT '真实姓名', `role` varchar(20) NOT NULL COMMENT '角色:STUDENT, TEACHER, ADMIN', `email` varchar(100) DEFAULT NULL COMMENT '邮箱', `phone` varchar(20) DEFAULT NULL COMMENT '电话', `create_time` datetime DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间', `update_time` datetime DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间', `is_deleted` tinyint DEFAULT '0' COMMENT '逻辑删除标志(0未删,1已删)', PRIMARY KEY (`id`), UNIQUE KEY `uk_username` (`username`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='系统用户表';3.2 课题表 (topic)
存储教师发布的课题信息。
CREATE TABLE `topic` ( `id` bigint NOT NULL AUTO_INCREMENT COMMENT '课题ID', `teacher_id` bigint NOT NULL COMMENT '发布教师ID(关联sys_user.id)', `title` varchar(200) NOT NULL COMMENT '课题标题', `description` text COMMENT '课题详细描述和要求', `max_selected` int DEFAULT '1' COMMENT '最大可选人数', `current_selected` int DEFAULT '0' COMMENT '当前已选人数', `status` varchar(20) DEFAULT 'PENDING_REVIEW' COMMENT '状态:PENDING_REVIEW(待审核), APPROVED(已审核通过), REJECTED(被驳回), CLOSED(已关闭)', `review_comment` varchar(500) DEFAULT NULL COMMENT '管理员审核意见', `create_time` datetime DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间', `update_time` datetime DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间', PRIMARY KEY (`id`), KEY `idx_teacher_id` (`teacher_id`), KEY `idx_status` (`status`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='课题表';3.3 选题记录表 (selection_record)
记录学生的选题申请和教师的审核结果。
CREATE TABLE `selection_record` ( `id` bigint NOT NULL AUTO_INCREMENT COMMENT '记录ID', `student_id` bigint NOT NULL COMMENT '学生ID(关联sys_user.id)', `topic_id` bigint NOT NULL COMMENT '课题ID(关联topic.id)', `status` varchar(20) DEFAULT 'PENDING' COMMENT '状态:PENDING(待审核), APPROVED(已通过), REJECTED(被拒绝)', `apply_comment` varchar(500) DEFAULT NULL COMMENT '学生申请理由', `review_comment` varchar(500) DEFAULT NULL COMMENT '教师审核意见', `apply_time` datetime DEFAULT CURRENT_TIMESTAMP COMMENT '申请时间', `review_time` datetime DEFAULT NULL COMMENT '审核时间', PRIMARY KEY (`id`), UNIQUE KEY `uk_student_current` (`student_id`, `status`) COMMENT '一个学生只能有一个进行中的选题', KEY `idx_topic_id` (`topic_id`), KEY `idx_student_id` (`student_id`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='选题记录表';注意:uk_student_current唯一索引配合业务逻辑,用于保证学生不能同时有多个“PENDING”或“APPROVED”状态的记录。更复杂的约束(如检查status值)可能需要在应用层代码中实现。
4. SpringBoot后端核心代码实现
接下来,我们搭建SpringBoot项目并实现核心业务逻辑。项目采用经典的分层架构:Controller -> Service -> Mapper。
4.1 项目初始化与依赖配置
使用Spring Initializr或IDE创建SpringBoot项目,主要依赖如下(pom.xml片段):
<dependencies> <!-- SpringBoot Web --> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-web</artifactId> </dependency> <!-- MyBatis-Plus --> <dependency> <groupId>com.baomidou</groupId> <artifactId>mybatis-plus-boot-starter</artifactId> <version>3.5.3.1</version> </dependency> <!-- MySQL驱动 --> <dependency> <groupId>mysql</groupId> <artifactId>mysql-connector-java</artifactId> <scope>runtime</scope> </dependency> <!-- Spring Security --> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-security</artifactId> </dependency> <!-- Lombok --> <dependency> <groupId>org.projectlombok</groupId> <artifactId>lombok</artifactId> <optional>true</optional> </dependency> <!-- Knife4j API文档 --> <dependency> <groupId>com.github.xiaoymin</groupId> <artifactId>knife4j-spring-boot-starter</artifactId> <version>3.0.3</version> </dependency> <!-- Hutool工具包 --> <dependency> <groupId>cn.hutool</groupId> <artifactId>hutool-all</artifactId> <version>5.8.16</version> </dependency> </dependencies>4.2 实体类与Mapper
使用MyBatis-Plus,实体类与Mapper的编写非常简洁。以Topic实体为例:
文件路径:src/main/java/com/graduation/topic/entity/Topic.java
package com.graduation.topic.entity; import com.baomidou.mybatisplus.annotation.*; import lombok.Data; import java.time.LocalDateTime; @Data @TableName("topic") public class Topic { @TableId(type = IdType.AUTO) private Long id; private Long teacherId; private String title; private String description; private Integer maxSelected; private Integer currentSelected; private String status; // 使用枚举更佳,此处简化为字符串 private String reviewComment; @TableField(fill = FieldFill.INSERT) private LocalDateTime createTime; @TableField(fill = FieldFill.INSERT_UPDATE) private LocalDateTime updateTime; }文件路径:src/main/java/com/graduation/topic/mapper/TopicMapper.java
package com.graduation.topic.mapper; import com.baomidou.mybatisplus.core.mapper.BaseMapper; import com.graduation.topic.entity.Topic; import org.apache.ibatis.annotations.Mapper; @Mapper public interface TopicMapper extends BaseMapper<Topic> { // 继承BaseMapper即拥有了基本的CRUD方法 }4.3 Service层业务逻辑
Service层封装核心业务规则。以下是TopicService中发布课题和选择课题的关键方法。
文件路径:src/main/java/com/graduation/topic/service/TopicService.java
package com.graduation.topic.service; import com.baomidou.mybatisplus.extension.service.IService; import com.graduation.topic.entity.Topic; import com.graduation.topic.vo.TopicVO; public interface TopicService extends IService<Topic> { /** * 教师发布新课题 * @param topic 课题信息 * @param teacherId 教师ID * @return 是否成功 */ boolean publishTopic(Topic topic, Long teacherId); /** * 学生选择课题 * @param topicId 课题ID * @param studentId 学生ID * @param applyComment 申请理由 * @return 选择结果信息 */ String selectTopic(Long topicId, Long studentId, String applyComment); }文件路径:src/main/java/com/graduation/topic/service/impl/TopicServiceImpl.java
package com.graduation.topic.service.impl; import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper; import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl; import com.graduation.topic.entity.SelectionRecord; import com.graduation.topic.entity.Topic; import com.graduation.topic.mapper.SelectionRecordMapper; import com.graduation.topic.mapper.TopicMapper; import com.graduation.topic.service.TopicService; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; import java.time.LocalDateTime; @Service public class TopicServiceImpl extends ServiceImpl<TopicMapper, Topic> implements TopicService { @Autowired private SelectionRecordMapper selectionRecordMapper; @Override public boolean publishTopic(Topic topic, Long teacherId) { // 1. 基础校验 if (topic.getMaxSelected() == null || topic.getMaxSelected() <= 0) { throw new RuntimeException("最大可选人数必须大于0"); } // 2. 设置初始状态和关联教师 topic.setTeacherId(teacherId); topic.setStatus("PENDING_REVIEW"); // 新课题需管理员审核 topic.setCurrentSelected(0); // 3. 保存到数据库 return this.save(topic); } @Override @Transactional(rollbackFor = Exception.class) // 开启事务,保证数据一致性 public String selectTopic(Long topicId, Long studentId, String applyComment) { // 1. 检查课题是否存在且状态为“已通过” Topic topic = this.getById(topicId); if (topic == null) { return "课题不存在"; } if (!"APPROVED".equals(topic.getStatus())) { return "该课题暂不可选"; } // 2. 检查课题是否已满额 if (topic.getCurrentSelected() >= topic.getMaxSelected()) { return "该课题人数已满"; } // 3. 检查该学生是否已有进行中的选题(PENDING或APPROVED) LambdaQueryWrapper<SelectionRecord> wrapper = new LambdaQueryWrapper<>(); wrapper.eq(SelectionRecord::getStudentId, studentId) .in(SelectionRecord::getStatus, "PENDING", "APPROVED"); Long count = selectionRecordMapper.selectCount(wrapper); if (count > 0) { return "您已有一个正在进行中的选题,请先取消或等待审核结果"; } // 4. 创建选题记录 SelectionRecord record = new SelectionRecord(); record.setStudentId(studentId); record.setTopicId(topicId); record.setStatus("PENDING"); record.setApplyComment(applyComment); record.setApplyTime(LocalDateTime.now()); selectionRecordMapper.insert(record); // 注意:此时课题的 currentSelected 并未增加,需教师审核通过后再增加 return "申请提交成功,等待导师审核"; } }4.4 Controller层API接口
Controller层接收前端请求,调用Service,并返回统一格式的JSON数据。这里使用一个简单的Result类包装返回结果。
文件路径:src/main/java/com/graduation/topic/controller/TopicController.java
package com.graduation.topic.controller; import com.graduation.topic.common.Result; import com.graduation.topic.entity.Topic; import com.graduation.topic.service.TopicService; import io.swagger.annotations.Api; import io.swagger.annotations.ApiOperation; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.security.core.context.SecurityContextHolder; import org.springframework.web.bind.annotation.*; import java.util.List; @RestController @RequestMapping("/api/topic") @Api(tags = "课题管理接口") public class TopicController { @Autowired private TopicService topicService; @GetMapping("/list") @ApiOperation("分页查询课题列表(学生/教师查看)") public Result<List<Topic>> listTopics(@RequestParam(required = false) String keyword, @RequestParam(defaultValue = "APPROVED") String status) { // 构建查询条件,这里简化处理,实际应使用Page对象分页 List<Topic> list = topicService.lambdaQuery() .like(Topic::getTitle, keyword) .eq(Topic::getStatus, status) .orderByDesc(Topic::getCreateTime) .list(); return Result.success(list); } @PostMapping("/publish") @ApiOperation("教师发布课题") public Result<String> publishTopic(@RequestBody Topic topic) { // 从Spring Security上下文中获取当前登录用户ID(假设已存储) // 实际项目中需要更完善的用户信息获取方式 Long teacherId = getCurrentUserId(); boolean success = topicService.publishTopic(topic, teacherId); if (success) { return Result.success("课题发布成功,等待管理员审核"); } else { return Result.error("课题发布失败"); } } @PostMapping("/select/{topicId}") @ApiOperation("学生选择课题") public Result<String> selectTopic(@PathVariable Long topicId, @RequestParam String applyComment) { Long studentId = getCurrentUserId(); String resultMsg = topicService.selectTopic(topicId, studentId, applyComment); if (resultMsg.contains("成功")) { return Result.success(resultMsg); } else { return Result.error(resultMsg); } } // 获取当前登录用户ID的辅助方法(需结合Spring Security实现) private Long getCurrentUserId() { // 示例:从SecurityContext中获取用户名,再查询数据库获取ID // 实际项目可将UserDetails扩展,直接存入ID String username = SecurityContextHolder.getContext().getAuthentication().getName(); // 模拟返回ID 1,实际应查询数据库 return 1L; } }4.5 Spring Security 安全配置
安全配置是系统的门户,负责登录认证和接口权限控制。
文件路径:src/main/java/com/graduation/topic/config/SecurityConfig.java
package com.graduation.topic.config; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.security.config.annotation.web.builders.HttpSecurity; import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity; import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder; import org.springframework.security.crypto.password.PasswordEncoder; import org.springframework.security.web.SecurityFilterChain; @Configuration @EnableWebSecurity public class SecurityConfig { @Bean public PasswordEncoder passwordEncoder() { // 使用BCrypt强哈希加密密码 return new BCryptPasswordEncoder(); } @Bean public SecurityFilterChain filterChain(HttpSecurity http) throws Exception { http .authorizeHttpRequests(auth -> auth // 允许静态资源和登录接口匿名访问 .antMatchers("/css/**", "/js/**", "/images/**", "/webjars/**").permitAll() .antMatchers("/api/auth/**", "/doc.html", "/swagger-resources/**", "/v2/api-docs").permitAll() // 基于角色的权限控制 .antMatchers("/api/teacher/**").hasRole("TEACHER") .antMatchers("/api/student/**").hasRole("STUDENT") .antMatchers("/api/admin/**").hasRole("ADMIN") // 其他所有请求都需要认证 .anyRequest().authenticated() ) .formLogin(form -> form .loginProcessingUrl("/api/auth/login") // 自定义登录处理URL .successHandler((request, response, authentication) -> { // 登录成功,返回JSON信息 response.setContentType("application/json;charset=UTF-8"); response.getWriter().write("{\"code\":200, \"msg\":\"登录成功\"}"); }) .failureHandler((request, response, exception) -> { response.setContentType("application/json;charset=UTF-8"); response.getWriter().write("{\"code\":500, \"msg\":\"用户名或密码错误\"}"); }) .permitAll() ) .logout(logout -> logout .logoutUrl("/api/auth/logout") .logoutSuccessHandler((request, response, authentication) -> { response.setContentType("application/json;charset=UTF-8"); response.getWriter().write("{\"code\":200, \"msg\":\"注销成功\"}"); }) ) .csrf().disable() // 禁用CSRF,便于API测试,生产环境需谨慎考虑 .sessionManagement().sessionFixation().newSession(); // 会话管理 return http.build(); } }5. 前端Vue.js关键页面示例
前端使用Vue 3 + Element Plus。这里展示学生选题页面的关键逻辑。
文件路径:src/views/student/TopicList.vue
<template> <div class="topic-list"> <el-card> <template #header> <div class="card-header"> <span>毕业设计课题列表</span> <el-input v-model="searchKeyword" placeholder="搜索课题标题或导师" style="width: 300px;" @keyup.enter="loadTopics"> <template #append> <el-button :icon="Search" @click="loadTopics" /> </template> </el-input> </div> </template> <el-table :data="topicList" v-loading="loading"> <el-table-column prop="title" label="课题标题" width="300" /> <el-table-column prop="teacherName" label="发布导师" width="120" /> <el-table-column prop="description" label="课题描述" show-overflow-tooltip /> <el-table-column label="人数" width="100"> <template #default="scope"> <span>{{ scope.row.currentSelected }} / {{ scope.row.maxSelected }}</span> </template> </el-table-column> <el-table-column prop="status" label="状态" width="100"> <template #default="scope"> <el-tag :type="scope.row.status === 'APPROVED' ? 'success' : 'info'"> {{ scope.row.status === 'APPROVED' ? '可选' : '待审核' }} </el-tag> </template> </el-table-column> <el-table-column label="操作" width="180" fixed="right"> <template #default="scope"> <el-button size="small" @click="showDetail(scope.row)">详情</el-button> <el-button size="small" type="primary" :disabled="scope.row.status !== 'APPROVED' || scope.row.currentSelected >= scope.row.maxSelected" @click="handleSelect(scope.row)" > 选择 </el-button> </template> </el-table-column> </el-table> <el-pagination @size-change="handleSizeChange" @current-change="handleCurrentChange" :current-page="currentPage" :page-sizes="[10, 20, 50]" :page-size="pageSize" layout="total, sizes, prev, pager, next, jumper" :total="total" style="margin-top: 20px;" /> </el-card> <!-- 选题申请对话框 --> <el-dialog v-model="selectDialogVisible" title="申请课题" width="500px"> <el-form :model="selectForm" label-width="80px"> <el-form-item label="申请理由"> <el-input v-model="selectForm.applyComment" type="textarea" :rows="4" placeholder="请简要说明你的研究兴趣、相关技能或选择该课题的原因" maxlength="500" show-word-limit /> </el-form-item> </el-form> <template #footer> <span class="dialog-footer"> <el-button @click="selectDialogVisible = false">取消</el-button> <el-button type="primary" @click="submitSelection">提交申请</el-button> </span> </template> </el-dialog> </div> </template> <script setup> import { ref, onMounted } from 'vue' import { Search } from '@element-plus/icons-vue' import { ElMessage, ElMessageBox } from 'element-plus' import axios from '@/utils/request' // 封装了axios的请求工具 const searchKeyword = ref('') const topicList = ref([]) const loading = ref(false) const currentPage = ref(1) const pageSize = ref(10) const total = ref(0) const selectDialogVisible = ref(false) const selectForm = ref({ topicId: null, applyComment: '' }) const currentTopic = ref(null) // 加载课题列表 const loadTopics = async () => { loading.value = true try { const params = { page: currentPage.value, size: pageSize.value, keyword: searchKeyword.value, status: 'APPROVED' // 只显示已审核通过的 } const res = await axios.get('/api/topic/list', { params }) // 假设后端返回 { code: 200, data: { records: [], total: 0 } } if (res.code === 200) { topicList.value = res.data.records total.value = res.data.total } } catch (error) { ElMessage.error('加载课题列表失败') } finally { loading.value = false } } // 显示课题详情 const showDetail = (topic) => { ElMessageBox.alert(topic.description, `课题详情:${topic.title}`, { confirmButtonText: '关闭', customClass: 'topic-detail-box' }) } // 处理选择课题 const handleSelect = (topic) => { currentTopic.value = topic selectForm.value.topicId = topic.id selectForm.value.applyComment = '' selectDialogVisible.value = true } // 提交选题申请 const submitSelection = async () => { if (!selectForm.value.applyComment.trim()) { ElMessage.warning('请输入申请理由') return } try { const res = await axios.post(`/api/topic/select/${selectForm.value.topicId}`, null, { params: { applyComment: selectForm.value.applyComment } }) if (res.code === 200) { ElMessage.success(res.msg || '申请提交成功!') selectDialogVisible.value = false loadTopics() // 刷新列表 } else { ElMessage.error(res.msg || '申请失败') } } catch (error) { ElMessage.error('网络错误,提交失败') } } onMounted(() => { loadTopics() }) </script>6. 系统部署与运行
6.1 后端启动与配置
- 配置文件:在
src/main/resources/application.yml中配置数据库连接等信息。server: port: 8080 spring: datasource: driver-class-name: com.mysql.cj.jdbc.Driver url: jdbc:mysql://localhost:3306/graduation_topic?useUnicode=true&characterEncoding=utf8&serverTimezone=Asia/Shanghai username: root password: your_password # MyBatis-Plus配置 mybatis-plus: configuration: log-impl: org.apache.ibatis.logging.stdout.StdOutImpl # 控制台打印SQL,生产环境关闭 global-config: db-config: logic-delete-field: isDeleted # 全局逻辑删除字段 logic-delete-value: 1 logic-not-delete-value: 0 # Knife4j配置 knife4j: enable: true setting: language: zh_cn - 启动类:标准的SpringBoot启动类。
package com.graduation.topic; import org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.SpringBootApplication; @SpringBootApplication public class GraduationTopicApplication { public static void main(String[] args) { SpringApplication.run(GraduationTopicApplication.class, args); } } - 运行:在IDE中直接运行
GraduationTopicApplication的main方法,或使用Maven命令mvn spring-boot:run启动。访问http://localhost:8080/doc.html即可查看和调试所有API接口。
6.2 前端启动
- 进入前端项目目录,安装依赖:
npm install。 - 启动开发服务器:
npm run serve。默认访问地址为http://localhost:8081。 - 修改前端项目中的
axios请求基地址,指向后端服务(localhost:8080)。
6.3 初始化数据
系统启动后,需要手动在数据库sys_user表中插入初始管理员账户(密码需使用BCrypt加密)。可以使用以下Java代码生成加密密码:
String rawPassword = "admin123"; BCryptPasswordEncoder encoder = new BCryptPasswordEncoder(); String encodedPassword = encoder.encode(rawPassword); System.out.println(encodedPassword); // 将输出结果复制到SQL中然后执行SQL插入管理员用户,再通过管理员账户登录系统后台,创建教师和学生用户。
7. 常见问题与排查思路
在开发和部署过程中,你可能会遇到以下典型问题:
| 问题现象 | 可能原因 | 排查思路与解决方案 |
|---|---|---|
启动报错:Failed to configure a DataSource | 数据库连接配置错误或数据库服务未启动。 | 1. 检查application.yml中的url,username,password。2. 确认MySQL服务已启动,且 graduation_topic数据库已创建。3. 检查MySQL驱动版本是否匹配。 |
访问接口返回403 Forbidden或401 Unauthorized | Spring Security权限配置问题或用户未登录/角色不符。 | 1. 检查请求的URL是否在Security配置的permitAll()路径中。2. 确认请求头是否携带了有效的认证Token(如JWT)或Session。 3. 检查当前登录用户的角色是否拥有访问该接口的权限( hasRole)。 |
| 前端调用API跨域(CORS)错误 | 浏览器安全策略阻止了不同源(域名、端口、协议)的请求。 | 在后端添加全局CORS配置。在Spring配置类中添加:@Beanpublic CorsFilter corsFilter() { ... },允许前端域名和端口。 |
| 插入数据时出现重复键错误 | 数据库唯一约束冲突,如用户名重复。 | 1. 检查插入的数据是否违反了表设计的唯一约束(如uk_username)。2. 在前端或后端添加数据重复性校验。 |
| 教师审核通过后,课题已选人数未增加 | 业务逻辑有漏洞,currentSelected字段未更新。 | 检查SelectionRecord状态更新为APPROVED时,是否同步执行了topic.setCurrentSelected(topic.getCurrentSelected() + 1)并更新数据库。这是一个关键的业务逻辑点,务必在事务中完成。 |
| 分页查询结果不正确 | 前端传递的分页参数与后端接收不一致,或SQL分页语句有误。 | 1. 使用MyBatis-Plus的Page对象,确保current和size参数正确接收。2. 检查前端请求参数名是否为 page和size(或pageNum和pageSize),保持前后端一致。 |
8. 项目扩展与最佳实践建议
完成基础功能后,可以考虑以下方向进行扩展和优化,这也能为你的毕业设计论文增加亮点:
- 引入Redis缓存:将频繁访问且变化不频繁的数据(如课题列表、用户基本信息)缓存到Redis中,显著提升查询性能,减轻数据库压力。
- 实现JWT无状态认证:替换默认的Session机制,使用JSON Web Token (JWT)。用户登录后获取Token,后续请求在Header中携带,更适合前后端分离架构和分布式部署。
- 增加文件上传功能:允许教师上传任务书、参考文献等附件,学生上传开题报告、中期检查等文档。需考虑文件存储(本地或OSS)和在线预览。
- 集成消息推送:使用WebSocket或第三方推送服务,实现选题状态变更、新通知等信息的实时推送,提升用户体验。
- 完善后台管理功能:
- 数据可视化:使用ECharts等库,为管理员提供选题进度统计、各导师课题数量、学生选择热度等图表。
- 操作日志:记录关键操作(如发布课题、审核、用户管理),便于审计和问题追溯。
- 系统配置:允许管理员在界面上动态配置选题的起止时间、每人可选课题数量等规则。
- 代码层面的最佳实践:
- 统一异常处理:使用
@ControllerAdvice和@ExceptionHandler全局处理异常,返回友好的错误信息给前端。 - 接口参数校验:使用
@Validated注解和JSR-303规范(如@NotBlank,@Size)对Controller入参进行校验。 - 使用枚举类:将数据库中的状态字段(如
topic.status,selection_record.status)定义为Java枚举,避免魔法字符串,提高代码可读性和安全性。 - 服务层事务管理:对于涉及多表修改的操作(如学生选课),务必在Service方法上使用
@Transactional注解,保证数据一致性。 - 定期备份数据库:编写脚本或使用MySQL的定时任务,定期对数据库进行备份,防止数据丢失。
- 统一异常处理:使用
这个基于SpringBoot的毕业设计选题系统,从需求到部署提供了一个完整的全栈开发范例。它不仅是一个可运行的毕业设计项目,更是一个学习现代Java Web开发技术栈(SpringBoot、MyBatis-Plus、Spring Security、Vue.js)的绝佳实践。在实现过程中,深入理解业务逻辑与数据一致性的处理、前后端分离的协作模式、以及安全认证的设计,远比单纯实现功能更有价值。你可以在此基础上,结合自己学校的实际流程,增加或调整功能模块,使其更贴合实际应用场景。