
1. 项目到底在解决什么问题先说人话绵城学生公寓系统本质上就是一个面向高校后勤场景的信息化管理工具。你做任何以XX管理系统为名的Spring Boot项目第一步不是急着建工程、写Controller而是先把业务范围圈清楚。学生公寓场景核心就三件事人、房、事——人指学生和宿管员房指楼栋、楼层、房间和床位事指入住、退宿、调宿、报修、查寝、水电抄表这些日常动作。我见过不少同学一上来就跟着网上的模板堆功能什么班级管理、课程管理、成绩管理全都塞进去结果做出来一个四不像。公寓系统的边界应该死死咬住公寓两个字学生管理、宿舍分配、入住退宿、来访登记、报修工单、水电费记录、公告通知这些才是核心。至于其他和学生公寓没有强关联的功能能砍就砍。这个项目的典型用户是两类角色学生端和宿管/超级管理员端。学生能在线报修、查看宿舍信息、水电费、公告管理端能维护楼栋房间、审核入住退宿、分配床位、处理报修工单、发布通知。搞清楚这些数据库表和接口设计自然就有方向了。再说这套系统的真实价值。高校宿舍管理如果全靠Excel和微信群里吼那效率低得可怕——床位空着没人知道学生报修要跑宿管站、写纸质单子宿管员每天被各种重复问题缠到焦头烂额。一个Web化的公寓系统把这些流程搬到线上之后每个环节都有记录、有状态、有责任人信息实时同步。这也是为什么每年的毕业设计、课程设计里这类题目长盛不衰因为它业务逻辑完整、技术覆盖全面而且有真实落地场景。2. 技术栈选型与版本选择的门道2.1 为什么是Spring Boot而不是别的选Spring Boot做这类项目核心原因不是大家都用所以我也用而是它真的适合这个场景。Spring Boot把过去Spring MVC Spring MyBatis那一套繁琐的XML配置全部干掉内嵌Tomcatjar包一打直接跑这对项目开发效率来说是质的提升。你写一个spring-boot-starter-web依赖一个SpringBootApplication注解启动类一跑一个Web项目就起来了不用再像老项目那样去配置web.xml、去装外部Tomcat。从业务角度来看学生公寓系统的功能模块虽然多但都属于标准的CRUD加权状态流转Spring Boot最擅长的就是这个。它不需要像大数据系统那样上Flink做实时流计算也不需要在单体应用里强行做微服务和消息队列——除非你项目要演示扩展性那可以像有些同学那样额外整合ActiveMQ做消息异步通知、整合Quartz做定时统计任务但这些都是加分项不是必需品。先把核心的、用Spring Boot能稳扎稳打做完的功能做好比什么都重要。2.2 版本和依赖到底怎么选版本问题真的是Spring Boot新手第一个大坑。你去看那些课程项目可能用的是2.2.x、2.3.x但你现在去Spring官网拉依赖默认可能已经到3.x甚至更高。Spring Boot 3.x是基于Java 17的如果你的JDK还是1.8直接启动报错给你看——UnsupportedClassVersionError。这就是热搜词里springboot版本太高出现的根源。我给你的建议是如果你是自己做项目不是非要用新技术那Spring Boot 2.7.x JDK 1.8是最稳妥的组合。为什么因为这个组合的生态最成熟网上能找到的资料最多MyBatis-Plus、PageHelper、各种生成器都完美兼容。等到你水平上来了再去冲Spring Boot 3.x也不迟不要在自己本就不熟悉的时候又叠加一个不熟悉的版本变量排查起来很痛苦。核心依赖清单如下parent groupIdorg.springframework.boot/groupId artifactIdspring-boot-starter-parent/artifactId version2.7.18/version relativePath/ /parent dependencies !-- Web 核心 -- dependency groupIdorg.springframework.boot/groupId artifactIdspring-boot-starter-web/artifactId /dependency !-- MyBatis-Plus操作数据库的效率神器 -- dependency groupIdcom.baomidou/groupId artifactIdmybatis-plus-boot-starter/artifactId version3.5.3.1/version /dependency !-- MySQL 驱动 -- dependency groupIdmysql/groupId artifactIdmysql-connector-java/artifactId version8.0.33/version /dependency !-- Lombok消灭 getter/setter 的样板代码 -- dependency groupIdorg.projectlombok/groupId artifactIdlombok/artifactId optionaltrue/optional /dependency !-- Hutool工具类全家桶生成验证码、日期处理都靠它 -- dependency groupIdcn.hutool/groupId artifactIdhutool-all/artifactId version5.8.22/version /dependency !-- JWT登录令牌 -- dependency groupIdio.jsonwebtoken/groupId artifactIdjjwt/artifactId version0.9.1/version /dependency /dependencies注意MySQL驱动的artifactId在Spring Boot 2.1之后建议用mysql-connector-java到了Spring Boot 3.x则要用com.mysql:mysql-connector-j。又是一个版本不对就启动报错的点留心。2.3 项目结构怎么摆Spring Boot项目结构这件事网上的样例五花八门但作为一个要维护、要答辩、后面还要扩展的项目我建议用标准的按技术分层 按业务模块分包的组合方式com.miancheng.apartment ├── common // 通用模块统一返回结果、全局异常处理、常量 │ ├── result │ ├── exception │ └── utils ├── config // 配置类MyBatis-Plus分页插件、跨域、拦截器注册 ├── controller // 控制层接收请求、参数校验 ├── service // 业务层核心业务逻辑都在这 │ └── impl ├── mapper // 数据访问层接口 XML文件 ├── entity // 数据库实体 ├── dto // 前端传参对象VO、DTO 分开 └── interceptor // 拦截器登录校验、JWT验证为什么DTO和entity要分开这是很多同学容易忽略的地方。数据库实体是内部结构直接返回给前端容易泄露多余字段比如密码而且前端需要的字段组合和数据库表不是一一对应的。你比如查询房间详情时需要附带当前已住人数用一个RoomVO来承接聚合数据而不是在Room实体上硬塞一个与数据库无关的字段。代码结构清晰了后面维护和改需求都舒服。3. 核心功能模块的落地实现3.1 登录鉴权别傻傻用Session了学生公寓系统分两种角色学生端着可能是PC网页也可能是小程序宿管员也分布在各个楼栋如果用传统的Session登录问题很多Session存在服务器内存服务一旦重启所有登录状态全丢而且跨域请求处理Session也麻烦。我用的是JWT方案。JWT的原理用一句话说服务器在用户登录成功后用秘钥签发一个加密的Token字符串返回给前端前端每次请求都在请求头里带上这个Token服务器验签通过就认为是合法登录状态。这个过程服务器不存储任何登录状态天然适合API接口。具体实现// 登录成功生成Token public String createToken(Long userId, String username, String role) { return Jwts.builder() .setSubject(username) .claim(role, role) .claim(userId, userId) .setExpiration(new Date(System.currentTimeMillis() 7 * 24 * 3600 * 1000)) .signWith(SignatureAlgorithm.HS256, your-secret-key) .compact(); }然后在Spring Boot里写一个拦截器注册到WebMvcConfigurer中统一拦截需要登录才可访问的接口。放行登录接口、注册接口和静态资源。一个关键细节对OPTIONS请求要直接放行否则前端跨域预检请求会被拦截器拦下来明明接口写得好好的却联调失败。我踩过的坑JWT的secret直接明文写在代码里后来发现这样在代码仓库里一搜就暴露了。正确做法是用ConfigurationProperties或者Value把它配置到application.yml里环境不同可以替换。3.2 宿舍分配与调寝事务和锁一个都不能少宿舍分配是整个系统里最容易出并发问题的模块。想象一个场景每年开学季95号床在数据库里显示空闲两个学生同时申请入住如果没有并发控制两个请求都通过了校验最终这个床上住了两个人。解决思路是把查询空闲 更新状态为已入住做成一个原子操作。用MyBatis-Plus实现的话可以走乐观锁机制给床位表加一个version字段更新时带上WHERE version ?更新成功后version 1如果受影响行数为0说明这床位已经被别人抢占了直接返回手慢了。同时分配宿舍的业务逻辑涉及多张表更新——床位表状态变更、宿舍入住记录新增、学生宿舍字段更新——任何一个失败都会导致数据不一致所以必须加上TransactionalTransactional(rollbackFor Exception.class) public boolean assignBed(Long studentId, Long bedId) { // 1. 校验学生是否存在且未分配 // 2. 校验床位是否空闲乐观锁方式 // 3. 更新床位状态为已入住 // 4. 插入入住记录 // 5. 更新学生信息和入住状态 // 6. 更新楼栋/房间当前入住人数 }这个事务有一处需要特别注意出现异常要能让事务回滚所以rollbackFor必须显式声明为Exception.class。Spring默认只回滚RuntimeException如果你某些业务异常是自定义异常且不继承RuntimeException那不加这个参数就会出现程序报错了但数据却写进去了的幽灵问题。3.3 报修工单状态机思想报修模块表面看是个简单的增删改查实际写起来比想象中要复杂。一个报修单从学生提交到维修完成中间有明确的流转状态待受理 → 维修中 → 待验收 → 已完成中间还有已驳回和已取消的分支。直接在最前端写死这些状态逻辑会到处是if-else而且分支一多就非常容易遗漏。我的做法是把状态流转收敛到Service层。工单表设计上放status字段然后每次状态变更都用方法去处理比如public void acceptOrder(Long orderId, Long repairerId) { RepairOrder order getById(orderId); if (order null) { throw new BizException(工单不存在); } if (!待受理.equals(order.getStatus())) { throw new BizException(当前状态不可受理); } order.setStatus(维修中); order.setRepairerId(repairerId); order.setAcceptTime(LocalDateTime.now()); updateById(order); }这样改的好处是状态流转逻辑全部写在后台前端只负责调用接口和展示想乱操作会被系统拦下来。你去做这个项目的时候推荐把状态枚举定义出来所有状态流转都走枚举判断避免中文到处写、写错字排查半天的问题。我遇到过学生把已维修完成写成了维修已完成导致状态匹配不上前台数据全乱的情况用枚举就没有这个问题。3.4 定时任务水电抄表与查寝报表学生公寓系统里有一个容易被忽略但实际很出彩的功能——水电费自动结算。如果每月1号要让宿管员逐宿舍录入电表读数然后计算费用那工作量太大了。Spring Boot里做定时任务非常简单只需两步。启动类加EnableScheduling注解然后在具体方法上加Scheduled(cron 0 0 2 1 * ?)表示每月1号凌晨2点执行。写一个定时任务组件遍历有变更的宿舍表根据当前读数减上次读数算出用量叠加到宿舍账单上然后把状态更新为待缴费。Component public class ElectricityTask { Scheduled(cron 0 0 2 1 * ?) public void monthlyElectricityBill() { // 获取所有电表档案 // 循环计算各宿舍本期用量 // 生成账单记录 // 更新宿舍欠费状态 } }注意Scheduled默认是单线程串行执行的。如果你项目中真的有多个定时任务报表统计、数据备份、自动提醒一定要配置一个线程池Configuration public class ScheduleConfig implements SchedulingConfigurer { Override public void configureTasks(ScheduledTaskRegistrar taskRegistrar) { taskRegistrar.setScheduler(Executors.newScheduledThreadPool(10)); } }否则任务多了互相排队一个任务执行太久其它任务的时间就全乱套了。4. 前端配合与部署运维4.1 前端技术选型和跨域问题你肯定会遇到一个现实问题这个项目的标题叫基于Spring Boot绵城学生公寓系统但实际做完前端通常是独立的Vue项目。Spring Boot负责接口Vue负责页面渲染。这里有一个关键的联调痛点——跨域。开发环境下你可以用Vue CLI或Vite的proxy代理转发请求生产环境下更建议把Vue打包后的静态资源直接放进Spring Boot项目的src/main/resources/static目录。这就是热搜词里vue打包放进springboot中的操作。# 在Vue项目目录执行 npm run build然后把dist目录下的所有文件复制到Spring Boot的static目录下。这样打出来的jar包既是后端接口服务、又是前端静态资源服务器真正做到了一个jar包部署整个系统。我踩过的坑Vue打包后通常使用history路由模式如果你刷新一个非首页路径比如/apartment/detail服务器返回404。解决办法是后端加一个转发规则如果不是以/api开头的路径一律转发到index.html交给Vue路由去处理。这一条不处理你部署完了会被人追着问为什么一刷新就404。4.2 宝塔面板还是Docker部署方式直接决定你后续运维省不省心。我个人实测下来学生对两种方式的使用场景不同如果你只是想快速把项目跑起来给老师或同事演示那宝塔面板最直观图形化界面、上传jar包、配置nginx、MySQL、Redis都在一个界面里搞定几乎零学习成本。如果你想让项目更专业一点、有版本管理和环境隔离那就用Docker部署。一个典型的DockerfileFROM openjdk:8-jre-alpine ENV TZAsia/Shanghai RUN ln -snf /usr/share/zoneinfo/$TZ /etc/localtime echo $TZ /etc/timezone COPY app.jar /app.jar EXPOSE 8080 ENTRYPOINT [java, -jar, /app.jar]为什么一定要设置时区这是血泪教训。Spring Boot默认时区是UTC你数据库里存的是北京时间如果服务器的时区没设置用LocalDateTime查数据会莫名其妙差了8个小时。部署完之后一定要验证登录、查数据、定时任务触发的时间全对才可以。更多情况下数据库容器和Redis容器用docker-compose一起编排保证重启服务不丢数据。4.3 application.yml里的那些小细节配置文件说大不大说小不小有些字段你写错了一个整个项目启动不起来。我把学生公寓系统常用的配置放出来你对照着自己项目看一下server: port: 8080 spring: application: name: miancheng-apartment datasource: url: jdbc:mysql://localhost:3306/apartment_db?useUnicodetruecharacterEncodingutf8serverTimezoneAsia/ShanghaiuseSSLfalseallowPublicKeyRetrievaltrue username: root password: 123456 driver-class-name: com.mysql.cj.jdbc.Driver servlet: multipart: max-file-size: 10MB max-request-size: 50MB mybatis-plus: configuration: map-underscore-to-camel-case: true log-impl: org.apache.ibatis.logging.stdout.StdOutImpl global-config: db-config: logic-delete-field: deleted logic-delete-value: 1 logic-not-delete-value: 0这里几个关键点你要知道为什么serverTimezoneAsia/Shanghai解决时区问题allowPublicKeyRetrievaltrue解决MySQL 8.0连接时的一个认证报错map-underscore-to-camel-case开启驼峰映射让数据库的student_name自动映射到实体的studentName字段逻辑删除字段配上之后deleteById操作自动变成UPDATE而不是DELETE数据不会真正被删掉这个对后台系统太重要了误操作还能恢复。5. 常见问题与排查技巧实录我把做这类项目最常碰到的几个问题整理成一份速查表都是我实际踩过或者帮别人排查过的问题你直接对照着查问题现象根本原因解决办法启动报错Unable to start embedded Tomcat端口被占用命令行执行netstat -ano | findstr :8080查PID任务管理器杀掉或者改server.port连接数据库报Access denied for user用户名密码或权限不对先命令行测试mysql -u root -p能否登录确认密码无误后检查远程授权前端接口数据拿到但页面渲染空白后端的字段和前端JS对象属性名不一致下划线vs驼峰统一走DTO/VO不要直接返回entity开启驼峰映射上传图片报MaxUploadSizeExceededExceptionSpring Boot默认上传限制是1MB在配置中显式设置spring.servlet.multipart.max-file-size把所有 session 用完就报内存溢出用了无状态请求但误用了HttpSession存东西用JWT替代Session请求头传Token定时任务不触发启动类忘了加EnableScheduling启动类加注解确认cron表达式是否正确打包出来的jar运行后访问404前端Dist没有放到static目录或路由模式问题确认dist下的index.html在classpath的static根目录部署数据库中文乱码建库时字符集不是utf8建库语句用CREATE DATABASE ... DEFAULT CHARACTER SET utf8mb4上传文件大小为0用了getOriginalFilename()拿文件名时报空配置中加spring.servlet.multipart.enabledtrue页面可以访问但接口全401登录拦截器把所有请求都拦了在拦截器注册时放行/api/user/login、/api/user/register和静态资源路径还有一个值得单独提醒的问题Spring Boot 2.7.18的自动装配原理。你装一个新的starter依赖它会自动帮你配置好很多东西比如MyBatis-Plus一加到依赖里自动装配就会去读META-INF/spring.factories文件里的配置类。如果你在自己写的config类里重复配置了同一个Bean可能会报BeanDefinitionStoreException。记住一个原则依赖提供的默认配置能工作就别重复造轮子除非你对源码非常清楚。6. 个人实操体会与建议做完一套学生公寓系统我最深的体会是这个项目真正的难度不在Spring Boot框架本身而在业务状态流转和数据结构设计。Spring Boot把大部分底层复杂度屏蔽掉了你上手的门槛很低但那些后端的接口写好了和系统能跑通之间隔着一大堆细节——数据库的字段设计是否合理、并发场景是否能抗住、状态流转是否完备、前端是否有空值判断。这些才是决定系统能否真正落地的关键。如果你现在正打算做这个题目我给你几个最实诚的建议。第一先花两天时间把数据库表设计图表完宿舍楼栋表、房间表、床位表、学生表、入住记录表、报修单表、水电账单表表之间的外键关系和状态字段都确定了再动手写代码后面会顺手非常多。第二不要把过多精力花在那些炫技的功能上比如开头说的整合Flink、Drools、消息队列这类做完主流程、把异常场景处理好比集成十个你完全不会用的中间件有价值一百倍。第三一定要本地先完整跑通再部署后端先跑、前端再跑、最后合体每一步都验证不要等到最后一起出问题再排查那时候定位问题的成本非常高。最后再分享一个小技巧。开发过程中用MyBatis-Plus的Wrapper查询很爽比如查一个楼栋的所有空闲床位LambdaQueryWrapperBed wrapper new LambdaQueryWrapper(); wrapper.eq(Bed::getBuildingId, buildingId) .eq(Bed::getStatus, 0) .orderByAsc(Bed::getRoomId, Bed::getBedNo);你只需要在写SQL思路的时候想清楚筛选条件和排序规则剩下交给框架就够了。至于那些复杂的多表关联查询或者报名工单的历史记录列表这种带条件的复杂查询老老实实写XML里的自定义SQL反而比在Java代码里堆条件更清晰、更容易调优。这是一条很实在的分界线——简单查询用框架复杂查询用原生SQL两条腿走路项目才走得稳。