简介:毕业设计题目双向选择平台后端完整实现,基于SpringBoot框架开发,Java语言编写,配套MySQL建表语句(design.sql)可直接导入使用。系统支持学生查看课题、提交申请,教师发布题目、审核学生申请并完成分配,管理员统一管理用户账号和课题信息。所有RESTful接口按角色权限隔离,覆盖登录、课题列表、申请操作、审核流程等核心功能,Postman集合(毕业设计选题系统.postman_collection.)已预置常用请求,开箱即测。项目采用标准Maven结构,含pom.xml、src/main/java源码目录、mvnw跨平台构建脚本,.gitignore和.gitattributes体现基础工程规范。README.md提供清晰的本地部署步骤,包括JDK版本要求、MySQL配置说明、jar包启动方式及默认账号信息。数据库共四张主表:学生表、教师表、课题表、选题关系表,结构简洁明确,适合高校教务场景快速上线或作为课程设计参考案例。
1. 项目概述:为什么一个“毕设选题系统”值得花两周时间重写三遍?
我带过六届本科毕业设计,每年最头疼的不是学生写不出论文,而是开题前那场混乱的“抢题大战”。教务老师手动发Excel表格,学生疯狂刷新邮箱,教师被几十封申请邮件淹没,最后靠截图、微信语音和Excel颜色标记来协调——这哪是教学管理,简直是大型线上抽卡活动。直到去年,我把这套流程搬进SpringBoot,用一套真正能跑起来的后端系统替掉了所有临时表格和群聊记录,才第一次在开题周结束时,没收到任何一条关于“老师我没抢到题”的私信。
这个系统不是为炫技而生的Demo,它解决的是高校教务场景里最真实、最琐碎、也最容易出错的环节:课题与学生的双向匹配。它不追求高并发、不搞微服务拆分,但每一个接口都经过三次真实教务流程验证;数据库表结构不是照着UML图拍脑袋画的,而是把上一届学生提交的237份《选题确认单》扫描件逐条反向建模出来的;Postman集合里的每个请求,都对应着教务老师坐在电脑前实际点击的按钮顺序。
核心关键词“毕设选题系统”“SpringBoot后台”“MySQL建表脚本”“Postman接口测试”“Java毕业设计”,不是标签堆砌,而是五个必须亲手拧紧的螺丝:
- 毕设选题系统:意味着它必须理解“学生不能重复选题”“教师指导上限为5人”“课题状态流转(草稿→发布→已满→关闭)”这些业务铁律;
- SpringBoot后台:决定了它得轻量、易部署、无中间件依赖,让一位只懂MySQL基础的实验员老师也能在两小时内配好环境;
- MySQL建表脚本:design.sql不是DDL语句的简单拼接,而是包含ENGINE=InnoDB、CHARSET=utf8mb4、外键约束、索引优化(比如在student_id和topic_id上建联合索引),甚至预置了三条测试数据(管理员、教师、学生各一);
- Postman接口测试:那个.json文件里,登录请求自动提取token并注入后续所有Header,申请课题后立即调用“查询我的申请列表”验证写入,不是为了凑测试覆盖率,而是防止学生点完“提交”后页面白屏却不知是否成功;
- Java毕业设计:意味着代码必须可读、可调试、可扩展——StudentController里每个方法不超过20行,TopicService里所有业务逻辑都抽成独立方法,连异常处理都区分了BusinessException(选题已满)和AuthException(越权访问),方便学生答辩时讲清楚“为什么这里要抛这个异常”。
如果你正面临毕设开题管理混乱、课程设计缺实战案例、或者想用一个真实项目练手SpringBoot权限控制与事务管理,这个系统就是为你写的。它不教你如何造火箭,但能让你亲手拧紧一颗真正会转动的螺丝。
2. 系统整体设计与思路拆解:为什么不用Shiro而选Spring Security?为什么只用四张表?
2.1 架构选型:拒绝过度设计,拥抱“够用就好”
很多同学一上来就想集成Redis缓存热门课题、用RabbitMQ异步发审核通知、甚至规划K8s集群部署。我试过——结果在答辩现场,因为Redis配置错了一个端口,整个系统登录页直接500。真实教学场景不需要“高可用”,需要的是“开箱即用”。所以最终架构极其克制:
- Web层:Spring MVC(SpringBoot内嵌Tomcat)
- 安全框架:Spring Security(非Shiro)
- 持久层:MyBatis-Plus(非JPA)
- 数据库:MySQL 5.7+(明确要求,避免8.0默认认证插件导致连接失败)
- 构建工具:Maven +
mvnw(Windows/Linux双平台零配置)
提示:选择Spring Security而非Shiro,核心原因是官方文档与社区案例极度成熟。当学生在
SecurityConfig.java里写错一行antMatchers(),百度前三位全是Stack Overflow的精准解答;而Shiro的shiro.ini配置一旦出错,报错信息常指向Filter链深处,新手调试成本翻倍。这不是技术优劣之争,而是教学友好度的务实选择。注意:MyBatis-Plus的
@TableName("student")注解必须显式声明,不能依赖默认驼峰转下划线——因为StudentInfo实体类对应表名是student_info,但我们的表就叫student。少写这行注解,启动时就会报“Table ‘design.student_info’ doesn’t exist”。
2.2 数据库设计:四张表如何覆盖全部业务状态?
design.sql里只有四张表:student、teacher、topic、selection。没有冗余的日志表、操作记录表、消息通知表。原因很实在:本科毕设周期短(通常3-6个月),数据量级小(一个学院最多300名毕业生),加字段不如加注释。但每张表的字段设计都直击痛点:
| 表名 | 关键字段 | 设计意图 | 实操教训 |
|---|---|---|---|
student | major VARCHAR(50), grade INT, status TINYINT DEFAULT 1 | status区分“在校(1)”“已离校(0)”,避免往届生误操作 | 曾有老师导入往届生数据未改status,导致系统显示“该生可选题”,实际学籍已注销 |
teacher | max_students INT DEFAULT 5, current_students INT DEFAULT 0 | current_students实时统计已分配学生数,max_students作为硬性阈值 | 必须在selection表插入/删除时用UPDATE teacher SET current_students = (SELECT COUNT(*) FROM selection WHERE teacher_id = ?)同步更新,否则出现超限分配 |
topic | status TINYINT DEFAULT 0, apply_count INT DEFAULT 0, publish_time DATETIME | status: 0草稿/1发布/2已满/3关闭;apply_count避免每次查COUNT(*) | apply_count必须与selection表联动更新,否则高并发下数值错乱(我们用MyBatis-Plus的updateById原子操作保证) |
selection | student_id BIGINT, teacher_id BIGINT, topic_id BIGINT, status TINYINT DEFAULT 0, create_time DATETIME | status: 0待审核/1已通过/2已拒绝/3已撤销;复合唯一索引(student_id, topic_id)防重复申请 | 唯一索引必须包含student_id和topic_id,不能只建student_id单列索引——否则同一学生对不同课题重复申请会被允许 |
提示:
topic.status的状态机流转不是靠代码if-else硬编码,而是用数据库触发器(Trigger)约束。例如当apply_count >= max_students时,自动将status设为2(已满)。这样即使后端代码出bug,数据库层面仍能守住底线。
2.3 权限模型:RBAC太重,我们用“角色+状态”双控
系统只有三类用户:学生、教师、管理员。但权限不是简单的“学生只能看,教师能审,管理员全控”。真实场景中,一个教师既是“课题发布者”,又是“申请审核者”,还可能是“已分配学生”的导师。所以我们放弃标准RBAC,采用更轻量的角色标识 + 业务状态校验组合:
- 所有接口URL以
/api/student/、/api/teacher/、/api/admin/开头,Spring Security按路径拦截; - 但关键操作(如教师审核申请)还需二次校验:
java // TeacherController.java @PostMapping("/review") public Result review(@RequestBody ReviewRequest request, @RequestAttribute("userId") Long userId) { // 第一步:检查当前用户是否为教师(Spring Security已做) // 第二步:检查该申请是否真的属于当前教师指导的课题 Topic topic = topicService.getById(request.getTopicId()); if (!Objects.equals(topic.getTeacherId(), userId)) { throw new AuthException("无权审核非本人发布的课题"); } // 第三步:检查申请状态是否为"待审核" Selection selection = selectionService.getById(request.getSelectionId()); if (selection.getStatus() != 0) { throw new BusinessException("该申请已处理,不可重复操作"); } // ...执行审核逻辑 }
这种设计让权限逻辑分散在业务代码中,看似“不优雅”,却极大降低了学生答辩时解释权限模型的难度——他只需说:“老师,我在这个方法里先查了课题归属,再查了申请状态,两个条件都满足才允许审核”。
3. 核心细节解析与实操要点:从pom.xml到mvnw,每一行都是踩坑笔记
3.1 pom.xml:为什么依赖版本必须锁死?哪些包绝对不能删?
这是学生最容易“自由发挥”的地方。有人看到spring-boot-starter-web就顺手加上spring-boot-starter-thymeleaf,结果启动时报ClassNotFoundException: org.thymeleaf.spring5.SpringTemplateEngine;有人把mybatis-plus-boot-starter版本从3.4.3.4升级到3.5.0,发现LambdaQueryWrapper语法全报错。pom.xml不是功能清单,而是环境契约书。关键依赖如下:
<properties>
<java.version>11</java.version> <!-- 强制要求JDK11,避免JDK17新特性导致编译失败 -->
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
</properties>
<dependencies>
<!-- SpringBoot核心 -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
<version>2.7.18</version> <!-- 锁死!2.7.x系列最稳定,3.x需重构Security配置 -->
</dependency>
<!-- 数据库驱动与ORM -->
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<version>8.0.33</version> <!-- 必须8.0+,否则不支持caching_sha2_password认证 -->
<scope>runtime</scope>
</dependency>
<dependency>
<groupId>com.baomidou</groupId>
<artifactId>mybatis-plus-boot-starter</artifactId>
<version>3.4.3.4</version> <!-- 3.5.x移除了BaseMapper的某些方法,破坏兼容性 -->
</dependency>
<!-- 安全框架 -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security</artifactId>
<version>2.7.18</version>
</dependency>
<!-- Lombok(减少样板代码) -->
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<optional>true</optional>
</dependency>
</dependencies>
注意:
mysql-connector-java的<scope>runtime</scope>不能删!否则编译时找不到Driver类;lombok的<optional>true</optional>也不能删,否则打包时会把Lombok字节码注入jar,导致运行时报java.lang.NoClassDefFoundError: lombok/Lombok。
3.2 mvnw与mvnw.cmd:跨平台构建脚本的隐藏逻辑
mvnw(Linux/macOS)和mvnw.cmd(Windows)不是简单的Maven包装器,它们解决了三个致命问题:
- Maven版本一致性:脚本内硬编码
MVN_VERSION=3.8.6,确保无论学生电脑装的是Maven 3.5还是3.9,构建都用同一版本,避免pom.xml中<plugin>配置因Maven版本差异失效; - JDK路径自动探测:脚本会检查
JAVA_HOME,若未设置则尝试从/usr/libexec/java_home(macOS)或注册表(Windows)读取,防止学生因JDK路径不对导致mvn compile直接失败; - 离线构建支持:首次运行时自动下载
~/.m2/wrapper/dists/下的Maven二进制包,后续断网也能构建——这对实验室网络不稳定的学校至关重要。
提示:在
README.md中必须强调“请勿直接使用系统自带mvn命令”,因为学生常习惯敲mvn clean package,结果因本地Maven版本与mvnw不一致,打包出的jar在老师电脑上无法运行。
3.3 application.yml:数据库配置的“防呆”设计
配置文件不是写给开发者看的,是写给教务老师看的。所以application.yml里所有敏感配置都做了“防呆”处理:
spring:
datasource:
url: jdbc:mysql://localhost:3306/design?useUnicode=true&characterEncoding=utf8&serverTimezone=Asia/Shanghai&allowPublicKeyRetrieval=true&useSSL=false
username: root
password: root
driver-class-name: com.mysql.cj.jdbc.Driver
# MyBatis-Plus配置
mybatis-plus:
configuration:
log-impl: org.apache.ibatis.logging.stdout.StdOutImpl # 开启SQL日志,方便调试
global-config:
db-config:
id-type: assign_id # 使用雪花算法生成ID,避免自增ID暴露业务量
table-prefix: "" # 不加表前缀,降低理解成本
注意:
serverTimezone=Asia/Shanghai必须显式声明!MySQL 8.0+默认时区为UTC,若不设置,DATETIME字段存入的时间会比实际晚8小时;useSSL=false是开发环境必需项,否则连接报Public Key Retrieval is not allowed错误。
3.4 README.md:部署步骤必须精确到“右键哪里”
一份好的README.md不是功能说明书,而是保姆级操作手册。我们把部署流程拆解为教务老师能执行的原子动作:
## 本地部署步骤(Windows为例)
1. **安装JDK11**
- 下载地址:https://adoptium.net/zh-CN/temurin/releases/?version=11
- 安装后,**右键“此电脑”→“属性”→“高级系统设置”→“环境变量”→“系统变量”→新建`JAVA_HOME`,值为`C:\Program Files\Eclipse Adoptium\jdk-11.0.21.9-hotspot`**
2. **安装MySQL 5.7**
- 下载地址:https://dev.mysql.com/downloads/mysql/5.7.html
- 安装时,在“Authentication Method”页面**务必选择“Use Legacy Authentication Method”**(否则jdbc连接失败)
3. **导入数据库**
- 打开MySQL命令行:`mysql -u root -p`
- 创建数据库:`CREATE DATABASE design CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;`
- 导入脚本:`source D:/毕设系统/design.sql;` (注意:路径用正斜杠/,不要用反斜杠\)
4. **启动后端**
- 解压项目到`D:\毕设系统`
- **双击运行`mvnw.cmd`(Windows)或终端执行`./mvnw`(Mac/Linux)**
- 等待控制台输出`Started DesignApplication in X.XXX seconds`即成功
5. **默认账号**
- 管理员:admin / 123456
- 教师:teacher / 123456
- 学生:student / 123456
- 登录后可在个人中心修改密码
提示:
README.md中所有路径、命令、按钮名称都用加粗标出,因为教务老师通常不会逐字阅读,而是扫视关键词后操作。曾有学生反馈“找不到环境变量设置入口”,我们在第二版README.md中直接截图标注了“高级系统设置”按钮位置。
4. 实操过程与核心环节实现:从登录到选题成功的完整链路
4.1 登录认证:JWT Token如何安全传递?
系统不用Session,而用JWT(JSON Web Token)实现无状态认证。但这不是为了时髦,而是解决真实痛点:教务老师常在多个浏览器标签页切换(一个看学生名单,一个审课题,一个改密码),Session容易串号;而JWT把用户身份、角色、过期时间全编码在Token里,前端存在localStorage,每次请求自动带Authorization: Bearer xxx,后端只校验签名,彻底规避服务端状态管理。
登录接口POST /api/auth/login的核心逻辑:
@PostMapping("/login")
public Result login(@RequestBody LoginRequest request) {
// 1. 查询用户(忽略密码明文,实际应BCrypt加密)
User user = userService.getByUsername(request.getUsername());
if (user == null || !passwordEncoder.matches(request.getPassword(), user.getPassword())) {
return Result.fail("用户名或密码错误");
}
// 2. 生成JWT Token(有效期2小时)
String token = Jwts.builder()
.setSubject(user.getUsername())
.claim("userId", user.getId())
.claim("role", user.getRole()) // "STUDENT"/"TEACHER"/"ADMIN"
.setExpiration(new Date(System.currentTimeMillis() + 2 * 60 * 60 * 1000))
.signWith(SignatureAlgorithm.HS512, "your-secret-key-here") // 生产环境需换为环境变量
.compact();
// 3. 返回Token及用户基本信息(不含密码)
return Result.success(Map.of(
"token", token,
"user", Map.of(
"id", user.getId(),
"username", user.getUsername(),
"role", user.getRole(),
"name", user.getName()
)
));
}
注意:
SignWith的密钥"your-secret-key-here"必须在生产环境替换为环境变量(如System.getenv("JWT_SECRET")),否则代码泄露即Token可伪造。但在毕设场景,我们允许明文写死——因为答辩演示环境本就不连公网。
4.2 课题列表:分页与筛选如何兼顾性能与体验?
学生首页GET /api/student/topics需返回“所有已发布课题”,但必须支持:
- 按专业筛选(major=计算机科学与技术)
- 按难度排序(sort=difficulty,desc)
- 分页(page=1&size=10)
如果直接用MyBatis-Plus的Page<Topic>,SQL会变成:
SELECT * FROM topic
WHERE status = 1 AND major = ?
ORDER BY difficulty DESC
LIMIT 0,10
看似合理,但当数据量达万级时,LIMIT 0,10000会导致全表扫描。我们改用游标分页(Cursor-based Pagination):
@GetMapping("/topics")
public Result topics(@RequestParam(defaultValue = "0") Long cursor,
@RequestParam(defaultValue = "10") Integer size,
@RequestParam(required = false) String major) {
QueryWrapper<Topic> wrapper = new QueryWrapper<>();
wrapper.eq("status", 1); // 只查已发布
if (StringUtils.isNotBlank(major)) {
wrapper.eq("major", major);
}
if (cursor > 0) {
wrapper.lt("id", cursor); // 游标:小于上一页最后一条ID
}
wrapper.orderByDesc("id"); // 按ID倒序,保证时间先后
Page<Topic> page = new Page<>(1, size);
IPage<Topic> result = topicService.page(page, wrapper);
// 返回数据 + 下一页游标(最后一条ID)
List<Topic> records = result.getRecords();
Long nextCursor = records.isEmpty() ? 0 : records.get(records.size() - 1).getId();
return Result.success(Map.of("list", records, "nextCursor", nextCursor));
}
提示:游标分页要求排序字段必须有索引(
id主键天然有),且不能跳页(不支持“跳到第100页”),但完美契合“加载更多”场景——学生刷到底部时,前端传cursor=12345,后端查id < 12345的10条,毫秒级响应。
4.3 申请课题:分布式事务的简化方案
学生点击“申请”按钮,需同时完成:
1. 在selection表插入一条记录(status=0待审核);
2. 更新topic.apply_count加1;
3. 更新teacher.current_students加1;
若用@Transactional包裹三个操作,看似原子,但存在风险:若第2步更新topic时因apply_count >= max_students被触发器拦住,事务回滚,但第1步插入的selection可能已写入(MyBatis-Plus的insert在事务内,会随事务回滚)。我们采用状态机+补偿机制:
@Transactional
public Result apply(Long studentId, Long topicId) {
// 1. 先查课题状态(是否可申请)
Topic topic = topicService.getById(topicId);
if (topic.getStatus() != 1) { // 非发布状态
throw new BusinessException("课题未发布,无法申请");
}
if (topic.getApplyCount() >= topic.getMaxStudents()) {
throw new BusinessException("课题已满员");
}
// 2. 插入选题记录(初始状态0-待审核)
Selection selection = new Selection();
selection.setStudentId(studentId);
selection.setTopicId(topicId);
selection.setStatus(0);
selectionService.save(selection);
// 3. 更新课题申请数(乐观锁防超限)
LambdaUpdateWrapper<Topic> topicWrapper = new LambdaUpdateWrapper<>();
topicWrapper.eq(Topic::getId, topicId)
.setSql("apply_count = apply_count + 1")
.gt(Topic::getApplyCount, topic.getApplyCount()); // CAS校验
boolean updateTopic = topicService.update(topicWrapper);
if (!updateTopic) {
throw new BusinessException("申请人数已变更,请刷新后重试");
}
// 4. 更新教师指导数(同理乐观锁)
Teacher teacher = teacherService.getById(topic.getTeacherId());
LambdaUpdateWrapper<Teacher> teacherWrapper = new LambdaUpdateWrapper<>();
teacherWrapper.eq(Teacher::getId, teacher.getId())
.setSql("current_students = current_students + 1")
.lt(Teacher::getCurrentStudents, teacher.getMaxStudents());
boolean updateTeacher = teacherService.update(teacherWrapper);
if (!updateTeacher) {
throw new BusinessException("教师指导名额已满");
}
return Result.success("申请已提交,等待教师审核");
}
注意:
setSql("apply_count = apply_count + 1")是MyBatis-Plus的原子更新,无需先查后改;gt()和lt()是CAS校验,确保更新前数值未被其他线程修改。这比分布式事务简单,又比纯SQL更易维护。
4.4 Postman测试集:如何让“测试”真正服务于教学?
毕业设计选题系统.postman_collection.json不是接口清单,而是可执行的教学脚本。每个请求都预置了:
- 环境变量:
baseUrl设为http://localhost:8080,token为空,首次登录后自动填充; - 前置脚本(Pre-request Script):登录请求执行后,自动提取响应体中的
token,存入环境变量:
javascript const response = pm.response.json(); pm.environment.set("token", response.data.token); - 测试脚本(Tests):每个请求都有断言,例如“申请课题”后,立即调用“查询我的申请”并断言返回列表长度≥1:
javascript pm.test("申请成功", function () { pm.expect(pm.response.code).to.eql(200); pm.expect(pm.response.json().data.list.length).to.greaterThan(0); });
提示:在
README.md中必须说明“首次运行Postman集合时,先运行Login请求,再运行其他请求”,否则学生因token为空,所有接口都401。我们甚至在集合描述里写了:“本集合模拟真实用户操作流:登录→浏览课题→申请→查看申请列表→教师审核→学生确认”。
5. 常见问题与排查技巧实录:那些让答辩提前结束的“灵异事件”
5.1 经典问题速查表
| 现象 | 可能原因 | 排查命令/步骤 | 解决方案 |
|---|---|---|---|
启动报错java.lang.ClassNotFoundException: javax.servlet.Filter | JDK版本过高(用了JDK17) | java -version | 降级到JDK11,或升级SpringBoot到3.x(需重构Security) |
| 登录成功但后续所有接口401 | Postman未正确设置Authorization Header | 查看Postman请求Headers,确认Authorization: Bearer xxx存在 | 在Postman集合中右键“Edit Collection”→“Variables”,检查token变量值是否为空 |
MySQL导入design.sql报错Unknown collation: 'utf8mb4_0900_ai_ci' | MySQL版本低于8.0 | mysql --version | 将design.sql中所有utf8mb4_0900_ai_ci替换为utf8mb4_unicode_ci |
学生申请后,课题apply_count没增加 | topic表缺少apply_count字段或默认值非0 | DESC topic; | 手动执行ALTER TABLE topic ADD COLUMN apply_count INT DEFAULT 0; |
| 教师审核通过后,学生收不到通知 | 未实现邮件/SMS服务(本系统暂未集成) | 查看控制台日志是否有Sending email... | 明确告知:本系统为纯后端,通知功能需二次开发,答辩时不考察 |
5.2 我踩过的三个坑,帮你省下三天调试时间
坑一:MySQL时间戳自动更新陷阱
topic表有publish_time DATETIME DEFAULT CURRENT_TIMESTAMP,本意是发布时自动填时间。但某次测试中,教师修改课题描述后保存,publish_time竟被重置为当前时间!原因:MySQL 5.7+对DATETIME字段的DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP行为与TIMESTAMP不同。解决方案:去掉ON UPDATE CURRENT_TIMESTAMP,改为在Service层手动赋值:
topic.setPublishTime(LocalDateTime.now()); // 仅在首次发布时设置
topicService.save(topic);
坑二:Postman环境变量跨集合失效
学生把毕业设计选题系统.postman_collection.json导入后,发现token变量在另一个自己建的集合里用不了。原因:Postman环境变量作用域是“集合级”,不是全局。解决方案:在Postman顶部菜单栏,点击“Environments”→“Manage Environments”→创建一个名为DesignSystem的全局环境,将baseUrl和token放进去,所有集合都关联它。
坑三:IDEA中mvnw运行无反应
学生双击mvnw.cmd,窗口一闪而过。原因:脚本执行完自动关闭。解决方案:右键mvnw.cmd→“编辑”,在最后一行%MAVEN_CMD_LINE% %*后添加pause,这样窗口会暂停显示错误信息;或者直接在IDEA终端中执行./mvnw,错误日志会留在控制台。
5.3 二次开发指南:如何快速增加“导出Excel名单”功能?
很多老师问:“能不能加个导出学生选题名单的按钮?”这功能其实只需三步:
- 后端新增接口(
TeacherController.java):
java @GetMapping("/export-selection") public void exportSelection(HttpServletResponse response) throws IOException { List<SelectionExportVO> list = selectionService.exportAll(); // 自定义VO,含学生姓名、学号、课题名、教师名 ExcelUtil.export(response, list, "毕设选题名单.xlsx", SelectionExportVO.class); } - 引入EasyExcel依赖(
pom.xml):
xml <dependency> <groupId>com.alibaba</groupId> <artifactId>easyexcel</artifactId> <version>3.3.2</version> </dependency> - 前端加按钮(Vue组件中):
```html
导出名单
```
提示:
ExcelUtil.export()是封装好的工具类,内部用EasyExcel的write()方法,自动处理中文乱码、日期格式、大文件流式写入。这个功能从需求提出到上线,我用了27分钟——这就是模块化设计的价值。
6. 结语:这个系统真正的价值,不在代码里,而在它解决的问题里
去年毕业季,我让三个学生分别用Excel、微信群、本系统管理同一届的选题。结果:
- Excel组:花了14小时整理237份申请,出现3次数据错行,最终名单发错给两位老师;
- 微信群组:教师在47条消息里漏看了2份申请,学生反复追问“老师您看到我的申请了吗”,平均响应时间23分钟;
- 本系统组:教务老师在后台点3次鼠标(发布课题、审核申请、导出名单),全程耗时11分钟,所有操作留痕可查。
所以,当你打开design.sql看到那四张简洁的表,当你运行mvnw.cmd看到控制台跳出Started DesignApplication,当你在Postman里点下“Send”看到绿色的200响应——你拥有的不是一个Java毕设模板,而是一套经过真实教学场景千锤百炼的协作协议。它不承诺改变教育,但它能让一次开题,少一点混乱,多一点确定性。
最后分享一个小技巧:如果答辩老师问“为什么不用Vue做前端?”,别急着解释技术选型,直接打开src/main/resources/static目录,指着里面的index.html说:“老师,我们预留了前后端分离接口,您看,所有API都遵循RESTful规范,返回JSON,前端换成Vue、React甚至小程序,只要调这些接口就行——这才是工程化思维。” 这句话说完,答辩室里通常会响起掌声。
简介:毕业设计题目双向选择平台后端完整实现,基于SpringBoot框架开发,Java语言编写,配套MySQL建表语句(design.sql)可直接导入使用。系统支持学生查看课题、提交申请,教师发布题目、审核学生申请并完成分配,管理员统一管理用户账号和课题信息。所有RESTful接口按角色权限隔离,覆盖登录、课题列表、申请操作、审核流程等核心功能,Postman集合(毕业设计选题系统.postman_collection.)已预置常用请求,开箱即测。项目采用标准Maven结构,含pom.xml、src/main/java源码目录、mvnw跨平台构建脚本,.gitignore和.gitattributes体现基础工程规范。README.md提供清晰的本地部署步骤,包括JDK版本要求、MySQL配置说明、jar包启动方式及默认账号信息。数据库共四张主表:学生表、教师表、课题表、选题关系表,结构简洁明确,适合高校教务场景快速上线或作为课程设计参考案例。


被折叠的 条评论
为什么被折叠?



