
最近总有准备毕业设计的同学来问我Java 方向到底选什么题目比较好写、好答辩又不容易烂大街。我通常会反问一句你愿不愿意老老实实做一套能跑通、能讲清楚的业务系统如果愿意基于 Spring Boot 的中老年教育学习系统——也就是老年大学信息管理系统——是一个操作性强、贴近真实场景的选择。这套项目我完整写过一版源码、配套文档资料包里常简写为 LW、部署说明和演示视频都整理齐全今天这篇就从需求拆解、功能设计、核心实现到部署排错把整个实战过程掰开讲一遍。这类系统表面上看是“老年大学教务管理”实际上它把学员、教师、课程、班级、报名、学习记录这些最常见的业务对象全部串到了一起。不管你是准备做毕业设计还是想拿一个 Spring Boot 项目练手它的复杂度刚刚好比单表 CRUD 深一点又没到微服务那种让人劝退的程度。下面我按我当时做项目的顺序一步步说清楚“为什么这样设计”和“代码到底怎么写”。1. 业务拆解老年教育系统到底要管理什么1.1 真实的业务痛点很多人一听“老年大学”就觉得很传统但你去任何一个城市的老年大学看一眼就会明白线下报名排队、课程名额靠抢、班主任用 Excel 排课、学员信息靠纸质档案。老年人想学书法、舞蹈、摄影、声乐往往不知道哪个班还有名额管理员也难统计每个班的学员结构。所以这套系统的核心价值是把“线下教务”搬到线上。学员可以在系统里看课程、选班级、报名、查学习记录管理员可以在后台维护教师、课程、班级和公告教师可以查看自己带的班级和学员情况。这正好是信息管理系统标准的三层角色架构也符合教学类毕设最常见的出题套路。1.2 角色与核心流程系统里至少有三种角色这是需求分析里最基础的用例来源管理员管理学员档案、教师档案、课程分类、课程信息、班级信息、公告查看报名统计。教师查看授课班级、学员名单维护课程资源记录课时。学员注册登录、浏览课程、选择班级、在线报名、查看学习记录。最核心的一条业务流程是学员注册 - 浏览课程分类 - 查看课程详情 - 选择某个具体班级 - 报名 - 管理员或教师能看到报名名单。这条流程串起来之后系统的主干就算立住了。至于报名审核、退课、课程资源上传、学习进度记录都是围绕主干做的分支功能。1.3 技术选型为什么是Spring Boot这颗“大树”我在做之前也纠结过用传统的 SSMSpring MVC Spring MyBatis还是 Spring Boot最后选了 Spring Boot理由很直接Spring Boot 内嵌 Tomcat打完 jar 包直接运行不用单独装 Tomcat部署演示省心。自动配置把大量 XML 配置砍掉了开发效率高适合一个人完成毕设。生态成熟不管是 MyBatis-Plus、Spring Security 还是 Thymeleaf网上案例一抓一大把遇到问题好排查。数据库操作层我选了 MyBatis-Plus而不是纯 MyBatis。原因也很简单单表 CRUD 不用写 SQL代码量减少一半分页插件、条件构造器都是现成的答辩被问到也不难解释。页面渲染选了 Thymeleaf 加 Bootstrap没有走前后端分离。因为毕设项目最重要的是“能在一个环境里完整跑起来”服务端渲染减少了跨域和打包部署的复杂度演示的时候也更稳。2. 数据库设计五张核心表撑起一套毕设2.1 核心表结构与说明数据库我命名为elder_edu字符集用 utf8mb4排序规则用 utf8mb4_general_ci。下面是核心表的设计思路建表时可按这个参考。先看用户表我习惯把登录账号单独放一张sys_user再通过外键关联学员或教师扩展信息。这样设计的好处是权限和业务资料分离以后如果要加“管理员”角色不需要在学员表或教师表塞一堆无关字段。表名核心字段作用sys_userid, username, password, real_name, role, phone, status登录账号role 区分 0 管理员、1 教师、2 学员studentid, user_id, gender, birth_date, health_note, emergency_contact学员扩展档案teacherid, user_id, gender, specialty, intro教师扩展档案course_categoryid, category_name, sort课程分类如书法、舞蹈、声乐courseid, category_id, course_name, cover_url, intro, status课程基本信息class_infoid, course_id, teacher_id, class_name, start_date, end_date, class_time, location, max_students, current_students, status具体开班计划course_enrollid, class_id, student_id, enroll_time, status学员报名记录course_resourceid, class_id, title, file_path, type课程课件/视频资源learning_recordid, student_id, resource_id, progress, study_time学员学习进度这里要重点说明class_info和course为什么要分开。老年大学同一个课程往往开多个班比如“书法基础班”周一上午上课“书法提高班”周三下午上课二者课时、地点、授课教师都不一样。如果只在课程表里加一个“开课时间”一个课程就只能有一个班业务上根本不够用。所以课程是“内容”班级是“具体开课安排”这个区分在答辩时一定要讲清楚。2.2 表的关系和设计取舍关系梳理起来很简单课程分类 1 对多 课程课程 1 对多 班级教师 1 对多 班级学员通过报名表 多对多 班级报名表里存class_id和student_id即可。学习记录表再关联学员和资源形成“谁在什么时候看了哪个课件”的流水。做毕设最容易犯的错是过度设计。比如报名时搞出一张独立的订单表还要分“待支付”“已支付”“已退款”这在老年大学场景里不是必须的。老年大学报名大多线下缴费系统只需要记录报名状态0 取消 / 1 已报名就够。把复杂的地方留给业务主线把简单的地方保持简单整套数据库才能在一两周内建完。2.3 项目目录结构长什么样我的项目目录结构大致如下src/main/java/com/example/elderedu ├── common // 统一返回结果、异常处理、工具类 ├── config // Security、MyBatis-Plus 等配置 ├── controller // 控制器 ├── entity // 数据库实体类 ├── mapper // MyBatis-Plus 的 Mapper 接口 ├── service // 业务逻辑接口和实现 └── ElEduApplication.java // Spring Boot 启动类这段结构看起来常规但很实用。controller 只负责接参和返参业务逻辑放 service数据访问放 mapper。答辩时老师问“代码分层为什么这么分”你可以说为了降低耦合、方便单元测试和后期维护。实际写代码时你也会发现分层之后改动一个功能不用把整个 Controller 翻一遍。3. 核心代码落地从登录到课程报名完整走通3.1 初始化项目与关键配置项目可以通过 Spring Initializr 初始化也可以直接用 IDEA 新建 Spring Boot 项目。依赖选择上我建议最精简的几样dependencies dependency groupIdorg.springframework.boot/groupId artifactIdspring-boot-starter-web/artifactId /dependency dependency groupIdorg.springframework.boot/groupId artifactIdspring-boot-starter-thymeleaf/artifactId /dependency dependency groupIdcom.baomidou/groupId artifactIdmybatis-plus-boot-starter/artifactId version3.5.3/version /dependency dependency groupIdmysql/groupId artifactIdmysql-connector-java/artifactId scoperuntime/scope /dependency dependency groupIdorg.projectlombok/groupId artifactIdlombok/artifactId optionaltrue/optional /dependency dependency groupIdorg.springframework.boot/groupId artifactIdspring-boot-starter-security/artifactId /dependency /dependencies然后看application.yml有几个点非常关键server: port: 8080 spring: datasource: driver-class-name: com.mysql.cj.jdbc.Driver url: jdbc:mysql://localhost:3306/elder_edu?useUnicodetruecharacterEncodingutf8useSSLfalseserverTimezoneAsia/Shanghai username: root password: 你的数据库密码 thymeleaf: cache: false mybatis-plus: mapper-locations: classpath:mapper/*.xml global-config: db-config: logic-delete-field: deleted第一处数据库 URL 里必须加serverTimezoneAsia/Shanghai不然高版本 MySQL 会报时区错误。第二处Thymeleaf 在开发阶段把缓存关掉改完页面刷新就能看到效果不用重启。第三处配置了logic-delete-field后MyBatis-Plus 会自动把删除操作转成update deleted1这是很实用的防误删方案。启动类上别忘了加MapperScan(com.example.elderedu.mapper)不加的话 Mapper 接口不会被扫描注册。3.2 登录认证与角色权限登录认证我用的是 Spring Security因为它是 Spring 官方生态答辩有得讲。密码存数据库的时候用 BCrypt 加密不是明文 MD5。下面是一个基于 Spring Security 5.7 / 6.x 的SecurityFilterChain配置写法Configuration EnableWebSecurity public class SecurityConfig { Bean public SecurityFilterChain filterChain(HttpSecurity http) throws Exception { http.authorizeHttpRequests(auth - auth .requestMatchers(/css/**, /js/**, /images/**, /login, /register, /).permitAll() .requestMatchers(/admin/**).hasRole(ADMIN) .requestMatchers(/teacher/**).hasRole(TEACHER) .anyRequest().authenticated() ) .formLogin(form - form .loginPage(/login) .defaultSuccessUrl(/index) .permitAll() ) .logout(logout - logout.logoutSuccessUrl(/login).permitAll()); return http.build(); } Bean public PasswordEncoder passwordEncoder() { return new BCryptPasswordEncoder(); } }这里最容易被忽略的是静态资源放行。如果不放行/css/**、/js/**、/images/**页面会变得光秃秃的全是样式丢失。如果用的是 Spring Boot 2.xrequestMatchers要写成antMatchers这个细节在集成时经常有人踩坑。登录逻辑本质上就是 UserDetailsService 加载用户然后把用户的角色前缀ROLE_拼好。如果不想引入 Security也可以写一个 HandlerInterceptor 做登录拦截Session 里存当前用户。但对于“管理系统”类毕设我建议用 Security因为答辩时“安全框架是如何工作的”是一个很好的加分点。3.3 课程列表与分页查询课程列表是最典型的分页查询场景。使用 MyBatis-Plus 时先配置分页插件Configuration public class MybatisPlusConfig { Bean public MybatisPlusInterceptor mybatisPlusInterceptor() { MybatisPlusInterceptor interceptor new MybatisPlusInterceptor(); interceptor.addInnerInterceptor(new PaginationInnerInterceptor(DbType.MYSQL)); return interceptor; } }然后 Service 层写查询逻辑public PageCourse getCoursePage(int current, int size, String keyword) { PageCourse page new Page(current, size); LambdaQueryWrapperCourse wrapper new LambdaQueryWrapper(); wrapper.like(StringUtils.hasText(keyword), Course::getCourseName, keyword) .orderByAsc(Course::getId); return courseMapper.selectPage(page, wrapper); }LambdaQueryWrapper的好处是不用拼字符串字段名写错了编译期就能发现。Controller 里接收current和size参数返回分页结果给前端显示。分页插件忘了配置是高频 bug配置了selectPage才能跑出正确结果否则它会查出所有数据再内存分页页面上数据量一大就卡。3.4 在线报名与学习记录报名接口是这套系统的核心业务。设计思路是校验当前学员是否已登录根据classId查班级信息校验报名表中是否存在同一学员和同一班级的记录校验班级当前人数是否达到max_students插入报名记录同时班级current_students 1。对应 Service 核心代码Transactional(rollbackFor Exception.class) public boolean enroll(Long classId, Long studentId) { ClassInfo classInfo classInfoMapper.selectById(classId); if (classInfo null || classInfo.getStatus() ! 1) { throw new RuntimeException(班级不存在或未开放报名); } Integer count courseEnrollMapper.selectCount(new LambdaQueryWrapperCourseEnroll() .eq(CourseEnroll::getClassId, classId) .eq(CourseEnroll::getStudentId, studentId) .eq(CourseEnroll::getStatus, 1)); if (count 0) { throw new RuntimeException(你已经报过这个班了); } if (classInfo.getCurrentStudents() classInfo.getMaxStudents()) { throw new RuntimeException(该班级名额已满); } CourseEnroll enroll new CourseEnroll(); enroll.setClassId(classId); enroll.setStudentId(studentId); enroll.setEnrollTime(LocalDateTime.now()); enroll.setStatus(1); courseEnrollMapper.insert(enroll); ClassInfo update new ClassInfo(); update.setId(classId); update.setCurrentStudents(classInfo.getCurrentStudents() 1); classInfoMapper.updateById(update); return true; }这里一定要加Transactional因为“插入报名表”和“修改班级人数”是两件事要么都成功要么都失败。如果不加事务中途报错就会出现“报了名但人数没增加”这种脏数据。名额增减这种操作在真正高并发下还要配合乐观锁但毕设阶段事务已经足够答辩时能说出来“为什么要用事务”就够了。学习记录模块的做法类似学员点击某个课件链接时先查learning_record表有没有该学员和该资源的记录没有就插入有就更新进度。进度可以简单记录一个 0-100 的整数表示学完百分比。这个小功能虽然简单却让系统名字里“学习”两个字有了落脚点而不是只做一个选课报名系统。4. 配套文档LW怎么写才能过查重又过答辩4.1 论文结构与每章写作重点资料包里的 LW 说白了就是毕业论文或设计文档。很多同学拿到源码后最愁的不是代码而是文档。其实论文结构和项目开发流程是对应的我推荐按六章写第一章 绪论写选题背景、国内外现状、研究内容和意义。背景可以从老龄化社会带来的教育需求入手不要长篇大论抄政策重点是自然引出“为什么需要信息化管理”。第二章 需求分析写可行性分析、功能需求、非功能需求配合用例图和使用流程。第三章 系统设计写总体架构、功能模块划分、数据库表结构和 E-R 图。第四章 系统实现按功能模块截图贴核心代码并解释逻辑。注意不是把全部代码贴进去而是贴关键方法。第五章 系统测试写测试环境、测试用例、测试结果最后给出结论。第六章 总结与展望总结做了什么、还有哪些不足不要写空话。每章的篇幅不需要平均用力。需求分析和系统设计是老师重点看的部分建议写详细一些系统实现部分以图配文页面截图要清晰。代码讲解不要写成 API 文档而是要讲“这个方法解决了一个什么问题”。4.2 图、表、测试数据的快速产出论文里的图包括但不限于系统架构图、功能结构图、用例图、业务流程图、E-R 图、核心表关系图。这些不要手画到 Word 里推荐用 ProcessOn 或者 draw.io操作快、风格统一。画完导出成图片插入论文清晰又省事。表格方面数据库表结构用三线表展示。所谓三线表就是只有顶线、栏目线和底线三条横线去掉乱七八糟的竖线和框线。这个格式是学术论文的基础要求Word 里设置表格边框时直接选“无”然后手动给三行加下边框就能实现。测试数据一定要真实感。比如课程分类不要只写“分类1、分类2”而是写“书法、国画、舞蹈、声乐、智能手机应用”。班级数据要有具体的上课时间、地点、教师和人数。老师翻论文时看到这种细节会觉得项目是真的跑过的。4.3 让论文和代码形成“对照表”答辩时最尴尬的情形是论文里写了一个功能代码里却没有或者代码里做了论文里没提。我的习惯是写文档前先列一张“功能-模块-代码位置-论文章节”的对照表。例如“学员报名”功能Controller 是CourseEnrollController中的enroll方法Service 是CourseEnrollServiceImpl数据库涉及course_enroll和class_info论文里对应第三章数据库设计和第四章系统实现。答辩抽问任何一环你都能直接定位而不是翻半天代码。这张对照表不用放进论文自己留着用就行但它的价值非常大能让你在答辩前把项目“从外到内”完整讲清楚。5. 部署流程与演示视频别让最后一个环节翻车5.1 本地IDEA启动的全流程一个项目做得再好如果考官或者老师看到的只是“运行不起来”前面全白费。本地第一次启动我建议按这个顺序来确认环境JDK 1.8 或更高版本、Maven 3.6、MySQL 5.7 / 8.0、IDEA。执行 SQL 脚本先创建数据库再执行项目里的elder_edu.sql它会自动建表并插入管理员、课程分类等基础数据。修改数据库密码打开application.yml把username和password改成自己本机的配置。启动 Redis 之类的外部依赖我这个项目没有引入 Redis所以不用装这也是我当时刻意避免的。直接运行ElEduApplication.java看到 Tomcat started 后访问http://localhost:8080/login。如果启动时报“Port 8080 was already in use”用命令查一下占用进程Windows 用netstat -ano | findstr :8080查到 PID 后结束进程或者改server.port。5.2 打jar包部署到服务器的命令文档里的部署说明一般有两种IDEA 内直接运行以及打 jar 包到服务器运行。这里把 jar 包部署的命令列出来mvn clean package -DskipTests打包完成后target目录下会生成一个elder-edu-0.0.1-SNAPSHOT.jar。把这个 jar 传到服务器执行java -jar elder-edu-0.0.1-SNAPSHOT.jar为了后台运行不因为关掉终端就退出我习惯这样启动nohup java -jar elder-edu-0.0.1-SNAPSHOT.jar run.log 21 然后通过tail -f run.log看日志。如果 8080 端口被防火墙拦需要在安全组或防火墙里放行如果访问太慢日志里通常能看到数据库连接超时的报错优先检查数据库能否从服务器远程访问。5.3 演示视频录制脚本与节奏演示视频的价值在于“让人只看视频就能知道系统能做哪些事”。我的录制节奏是开头 20 秒打开系统登录页简单介绍项目名称和角色。用管理员账号登录展示后台菜单快速点击学员管理、课程管理、班级管理每个页面停留 5 秒截个重点。展示新增班级或课程的完整操作填写表单、保存、列表刷新。切换学员角色进入前台浏览课程分类选择一个班级报名展示报名成功的反馈。再回到管理员后台展示该班级报名人数变化形成闭环。最后展示学习记录或通知公告收尾。录制工具用 OBS 或 EV 录屏都可以分辨率建议 1920x1080帧率 30 就够了。操作鼠标时不要太快给观众留出理解时间。过程中不要切出去看代码视频里出现 IDE 报错会非常影响可信度。6. 避坑手册我实际踩过的问题都在这里6.1 环境配置类问题做毕设最常见的坑首先是数据库驱动版本不匹配。MySQL 8.0 必须用com.mysql.cj.jdbc.Driver如果你复制了旧项目的com.mysql.jdbc.Driver启动大概率报数据库连接失败。第二个是时区问题很多人不写serverTimezoneAsia/Shanghai报错信息里直接让你设置时区。第三个是 Maven 依赖下载缓慢我建议使用阿里云镜像在settings.xml里配置mirror节点否则创建一个项目的时间够你喝三杯茶。6.2 框架集成类问题Spring Security 集成时最典型的现象是页面突然没有样式原因基本是静态资源没有放行。你还可能在控制台看到 CSRF 拦截导致的 403 错误可以暂时关闭 CSRF或把表单里加入 CSRF token。另一个高频问题在 MyBatis-Plus 分页上如果不配置分页插件selectPage查出的total是 0或者数据没有被真正分页记住前面那份配置类一定是注册为Bean并且DbType.MYSQL要写对。Mapper XML 扫描不到也是常见怪事。如果写了自定义 SQL 的 XML 文件application.yml里的mapper-locations和启动类的MapperScan必须同时配置。我把 XML 放在resources/mapper目录下配置文件写classpath:mapper/*.xml启动后就没有再折腾过。6.3 业务逻辑类问题报名模块有两个逻辑坑重复报名和超卖。重复报名靠数据库唯一约束可以兜底我建议在course_enroll表对class_id student_id加联合唯一索引这比只靠代码判断更稳。超卖问题在单人开发、演示场景下概率不高但代码里至少要做current_students max_students的校验。如果你想让代码更完善可以在修改班级人数时使用update class_info set current_students current_students 1 where id ? and current_students max_students这种乐观锁写法更专业。中文乱码问题也值得一提。确认数据库连接 URL 里带characterEncodingutf8同时确认数据库表使用 utf8mb4。页面乱码则检查模板文件编码为 UTF-8Thymeleaf 默认会按 UTF-8 解析只要不是本地文件被人改过编码基本不会出事。还有一个容易被忽略但很要命的问题打包后运行 jar 包时提示无法找到模板或者无法加载静态资源。这通常是因为你的模板路径写错了比如把templates目录放到了java包下面Spring Boot 默认不会去那儿找。保持标准结构模板放src/main/resources/templates静态资源放src/main/resources/static就不会出现这种问题。这套项目从头到尾做下来我的体会是毕业设计不是去拼新技术而是要把一件常规事情做完整。Spring Boot MyBatis-Plus Thymeleaf 这套组合也许不够“高级”但它的每一行代码你都能讲清楚每一个模块你都亲手写过这就是答辩时最强的底气。如果你正要动工建议先花两天把数据库表结构定死再开始写代码。表结构稳定了后面基本是流水线式干活表结构反复改才是拖延进度的真凶。