☰
Spring Boot学生公寓管理APP开发全攻略:从数据库设计到答辩通关
2026/10/24 2:47:12 网站建设 项目流程

1. 先拆业务:学生公寓APP到底要管哪些事

看到“基于Spring Boot学生公寓管理APP”这个题目,很多同学的第一反应是“这不就是个增删改查吗”。这么说没有错,但如果你真把它当成一个纯CRUD项目来做,做到一半就会发现自己卡在各种“关系”上——宿舍和学生的关系、报修单和维修工的关系、水电费和管理员的关系。这些东西捋不顺,代码写着写着就会返工。

我当年带过的某学弟,第一次做这个题目的时候,上来就建了一张表叫dorm_student,把所有字段塞进去,结果做到报修模块发现数据根本没法拆,最后推倒重来,距离答辩只剩两周。所以第一步不是翻Spring Boot的文档,而是把业务角色和业务流程先搞清楚。

1.1 学生公寓管理场景下的四类角色

一个学生公寓管理APP,往大了说可以做成智慧公寓,往小了说至少要覆盖四类角色的日常需求:

  • 学生:查宿舍分配、在线报修、查水电费、看通知公告、登记访客。
  • 宿管员:楼栋管理、房间管理、入住/退宿办理、查寝记录。
  • 维修工:接收报修单、处理报修、回填维修结果。
  • 系统管理员:账号管理、角色权限分配、数据统计看板。

这四类角色对应的权限层级完全不同。学生只能看自己的数据,宿管员能看整栋楼,系统管理员能看全部。权限这块如果一开始不设计好,后面写接口的时候就会纠结“这个接口到底谁能调”。

业务流程上,核心要打通两条主线。第一条是“人”的线:学生从申请入住到分配宿舍,再到退宿离校,中间可能换宿舍。第二条是“事”的线:学生提交报修,维修工接单处理,宿管员确认完成。这两条线是项目的主动脉,数据库表设计、接口设计、前端页面设计都围绕它们展开。

1.2 功能模块怎么划分才清晰

按我实际做下来的经验,功能模块可以切成五个大块,这也是答辩时画系统功能结构图的标准画法:

模块核心功能面向角色
宿舍管理楼栋/房间/床位维护、宿舍类型管理宿管员、系统管理员
学生管理学生档案、入住登记、调宿/退宿宿管员、系统管理员
报修管理报修提交、派单接单、维修回填、评价学生、维修工、宿管员
费用管理水电费录入/生成、账单查询、在线缴费学生、宿管员
系统管理用户管理、角色权限、通知公告、数据看板系统管理员

为什么把系统管理单独拉出来?因为毕业设计答辩时老师一定会问“权限怎么控制的”“有哪些角色”。如果你把用户角色硬编码在代码里,虽然也能跑,但一被追问就会露馅。用Spring Boot + Sa-Token或Spring Security做一套标准的RBAC权限模型,这个模块本身就是答辩亮点。

至于“APP”怎么理解,我建议做成“学生端App + 管理后台”的组合:学生用App(我用的是uni-app打包Android商店安装包),宿管员和维修工用后台网页(Vue3 + Element Plus)。一套后端接口服务两套前端,这也更贴合真实企业项目的前后端分离架构。

2. 技术栈怎么选才能稳过答辩

选技术栈这事,很多同学喜欢“跟风”——看到别人用Spring Cloud就上Spring Cloud,看到别人用Docker就用Docker。但毕业设计的第一原则不是炫技,而是“你能讲清楚为什么这么选”。我在项目里用的这套组合,既有含金量,难度又能控制在一个人能完成的范围。

2.1 后端:Spring Boot + MyBatis-Plus这套组合的底气

后端最核心的是Spring Boot版本选择。我推荐大家用Spring Boot 2.7.x而不是3.x,原因很现实:2.7的生态最成熟,网上能找到的资料最多,遇到问题一搜就有答案。3.x要求Java 17起步,有些学校的机房环境还停留在JDK 8,用3.x可能会踩环境兼容性的坑。

持久层框架,我用的是MyBatis-Plus而不是原生MyBatis。单纯从毕业设计的CRUD量来看,MyBatis-Plus的BaseMapper内置方法能省掉80%的单表SQL。比如分页查询、条件构造器LambdaQueryWrapper、逻辑删除这些功能,都是开箱即用。

注意:如果你在简历里写了“熟悉MyBatis”,那答辩时至少能说清楚#{}和${}的区别以及SQL注入的原理;如果写“熟悉MyBatis-Plus”,最好能主动说出selectById在底层是怎么拼SQL的。用MP图省事可以,但要能解释清楚。

权限认证这块,我见过太多同学卡在Spring Security的过滤器链配置上,配一个拦截放行规则能折腾一天。我的建议是:如果项目偏中小型,直接上Sa-Token,它的StpUtil.login()登录逻辑比Spring Security的AuthenticationManager那套东西直观太多了。Sa-Token的注解鉴权@SaCheckPermission配合RBAC模型,做权限管理是一行注解的事。答辩时老师问起来,你就说“基于RBAC模型设计了用户-角色-权限三级关联,用JWT风格的Token做无状态认证”,这句话就能把大部分问题挡回去。

2.2 前端:uni-app和Vue3怎么分工

学生端App我选的uni-app,理由很功利:一套代码可以编译到Android App、iOS App、微信小程序和H5四个平台。毕业设计做App端,老师们现在默认用模拟器或者真机演示。如果你用Android原生写,界面没做好的话观感真的很减分;用uni-app的话,页面布局和Vue组件写法一致,学习成本低很多。

管理后台我用Vue3 + Vite + Element Plus。Vue3的组合式API配合Element Plus的表格、表单、弹窗组件,做后台页面属于“所见即所得”。数据看板再配一个ECharts,把宿舍入住率、报修完成率用饼图和柱状图展示出来,页面的视觉完成度立刻上一个档次。

前后端联调用Axios,统一封装好请求拦截器,让每个请求自动携带Token请求头。响应拦截器统一处理后端返回的code,比如Token过期时自动跳转登录页。这套东西在答辩演示时会显得非常流畅——老师会看到你能处理真实的异常情况,而不是所有接口都一次成功。

2.3 数据库与中间件:MySQL + Redis扮演什么角色

数据库用MySQL 8.0,这个版本在Windows和Linux上安装都很省心。字符集统一用utf8mb4,排序规则用utf8mb4_general_ci,这样学生姓名里即使有表情符号也不会乱码。

Redis在这套架构里主要干三件事:一是缓存验证码和Token,二是缓存宿舍房间的状态信息(比如哪些房间已满、哪些房间可住),三是配合定时任务做分布式锁防止重复生成账单。Redis这部分不用设计得特别复杂,但一定要在论文里体现出来。答辩老师看到你用了Redis而不是只用MySQL硬扛,第一印象就不同了。

3. 数据库设计:建对这几张表,后面写代码又快又稳

数据库设计是毕业设计的灵魂。很多同学程序写着写着跑不通,翻来覆去找不到问题,最后发现是表结构设计不合理。学生公寓这个题库,数据库设计得好不好,只要看三张关联表就能判断:学生表和宿舍表的关联方式、报修单表的状态字段、账单表的唯一性约束。

3.1 核心表的规划与关联关系

我最终落地的表结构大概是下面这样,你在设计时可以直接参考:

  • sys_user(用户表):id、username、password(BCrypt加密)、real_name、phone、avatar、status、create_time。用户不分角色表,角色通过sys_user_role关联。
  • sys_role(角色表):id、role_name、role_code(如STUDENT、DORM_ADMIN、REPAIR_WORKER、ADMIN)、description。
  • sys_permission(权限表):id、perm_name、perm_code(如dorm:room:add)、parent_id。
  • sys_user_role和sys_role_permission(关联表):这类关联表就是两个外键字段加一个主键id,没有其他多余设计。
  • dorm_building(楼栋表):id、building_name、building_no、floor_count、room_count、address。
  • dorm_room(房间表):id、building_id、room_no、room_type(四人间/六人间)、beds_total、beds_used、status(0空闲/1部分入住/2已满/3维修中)。
  • dorm_bed(床位表):id、room_id、bed_no、status(0空闲/1已入住)。
  • student(学生表):id、student_no(学号,唯一)、user_id、name、gender、college(学院)、major(专业)、class_name、phone、status(0在读/1已退宿)。
  • checkin_record(入住记录表):id、student_id、room_id、bed_id、checkin_time、checkout_time、is_active、reason。
  • repair_order(报修单表):id、order_no、student_id、room_id、repair_type(水电/门窗/设备)、description、images、status(0待派单/1处理中/2待验收/3已完成/4已取消)、create_time、assign_time、finish_time。
  • repair_worker(维修工表):id、user_id、worker_name、phone、work_type、status。如果你不想单独建维修工表,也可以在sys_user里通过角色标记,但单独建表写起来更顺手。
  • water_electric_bill(水电费表):id、room_id、period(账单月份,如2025-06)、water_amount、electric_amount、water_fee、electric_fee、total_fee、status(0未缴/1已缴)、pay_time、create_time。
  • notice(通知公告表):id、title、content、publish_user_id、publish_time、status。
  • visitor_record(访客登记表):id、student_id、visitor_name、visitor_phone、visit_time、leave_time、reason。

这套表设计最核心的一点:关联不要乱建外键。外键约束在开发阶段很烦人,删数据时老提示“有外键约束不能删”。我用的是逻辑关联——在实体类上定义@TableField(exist = false)的关联字段,查询时手动组装,既方便测试,也不影响数据完整性。

3.2 几个容易踩坑的关键设计细节

第一个坑是金额字段类型。水电费金额必须用DECIMAL(10,2),千万别用double或float。数据库中浮点数计算会产生误差,比如0.1 + 0.2 = 0.30000000000000004,交钱这种事情出误差会非常尴尬。Java代码里对应BigDecimal,所有金额计算都用BigDecimal的add和multiply方法。

第二个坑是状态字段。所有状态字段都统一用tinyint类型,用数字表示状态,同时在后端定义一个枚举类统一管理。比如报修单状态:0待派单、1处理中、2待验收、3已完成、4已取消。写代码时用枚举而不是if (status == 1)这种魔法数字,后期维护会舒服很多。

第三个坑是逻辑删除。不要物理删数据。比如学生退宿,你只是在student表上把status改成1,在checkin_record表里记录退宿时间,而不是把记录删掉。这也符合审计留痕的逻辑。MyBatis-Plus里配置@TableLogic注解就能自动实现逻辑删除。

第四个坑是索引设计。业务上高频查询的字段都要加索引:学生表的student_no加唯一索引,房间表的room_no和building_id建联合索引,水电费表的room_id + period建联合唯一索引——这能保证同一房间同一月份不会生成两条账单。

3.3 建库脚本与初始化数据的落地建议

建库脚本一定要配套初始数据。我见过很多项目代码没问题,但演示时因为数据库是空的,页面光秃秃的,完全没有展示效果。项目里至少要有:

  • 一栋楼(6层,每层10间房),每间房有床位记录;
  • 20个学生账号和对应的学生信息(密码统一初始化成123456);
  • 5条不同状态的报修单;
  • 最近三个月的水电费账单数据,部分已缴、部分未缴;
  • 3个角色的账号各一个(宿管员、维修工、系统管理员)。

这些初始化数据我直接写在sql文件里,用spring.sql.init.schema-locations和spring.sql.init.data-locations配置,项目启动时自动执行。演示时不用手动导数据,直接启动就能看到效果。

4. 核心接口开发:登录、入住、报修这些实现怎么落地

数据库和表结构确定之后,后端的开发节奏就非常快了。这部分我挑四个核心点来讲,都是学生公寓项目里最有“含金量”的部分,也是答辩时老师大概率会追问的部分。

4.1 统一返回结构、全局异常处理和登录认证

先写统一返回结构Result<T>。所有接口的返回值都是它,前端拿到后统一处理。类很简单:

@Data public class Result<T> { private Integer code; private String message; private T data; public static <T> Result<T> success(T data) { Result<T> result = new Result<>(); result.setCode(200); result.setMessage("success"); result.setData(data); return result; } public static <T> Result<T> error(Integer code, String message) { Result<T> result = new Result<>(); result.setCode(code); result.setMessage(message); return result; } }

全局异常处理用@RestControllerAdvice。业务异常统一抛BizException,参数校验失败的统一返回400,系统异常返回500。这样不管哪里出错,前端页面都能弹出统一格式的提示,不会出现“Network Error”这种让老师皱眉头的玩意。

登录接口是项目的门面。用Sa-Token的逻辑,整个登录过程非常干净:

@PostMapping("/login") public Result<LoginVO> login(@RequestBody @Validated LoginDTO dto) { // 1. 查用户 SysUser user = sysUserService.lambdaQuery() .eq(SysUser::getUsername, dto.getUsername()) .one(); if (user == null) { throw new BizException("用户名或密码错误"); } if (user.getStatus() == 1) { throw new BizException("账号已被禁用"); } // 2. 校验密码(BCrypt比对) if (!BCrypt.checkpw(dto.getPassword(), user.getPassword())) { throw new BizException("用户名或密码错误"); } // 3. Sa-Token登录 StpUtil.login(user.getId()); // 4. 返回token和用户基本信息 LoginVO vo = new LoginVO(); vo.setToken(StpUtil.getTokenValue()); vo.setUserId(user.getId()); vo.setRealName(user.getRealName()); vo.setRoles(stpUtil.getRoleList()); return Result.success(vo); }

登录的逻辑要顺着答辩的思路去设计:先验证用户存在,再验证密码,然后签发Token。密码用的BCrypt加密,不是MD5——这个点答辩老师一般都会问到,你要能说出MD5为什么不够安全(彩虹表暴力破解、无盐值),以及BCrypt加盐对抗暴力破解的原理。

4.2 宿舍分配与入住登记的实现要点

宿舍分配是“人”这条主线的核心。我设计了三种分配方式:宿管员手动指定房间床位、提交入住申请后由管理员分配、按学院专业自动匹配同楼层。三种方式在接口层是同一个入口:POST /checkin/assign,入参里带studentId、roomId、bedId。

住宿分配最怕的就是“超卖”——房间明明只剩一个床位,但两个同学同时提交入住请求,结果数据不一致。传统做法是在分配前查询床位状态,然后在事务里更新状态,但这套逻辑在高并发下依然有并发问题。毕业设计不需要上分布式锁这么重的方案,但至少要做到数据库层面的原子更新:

@Transactional(rollbackFor = Exception.class) public void assignRoom(AssignRoomDTO dto) { // 1. 校验学生状态 Student student = studentService.getById(dto.getStudentId()); if (student == null || student.getStatus() != 0) { throw new BizException("学生不存在或已退宿"); } // 2. 原子更新床位状态(关键SQL) int updated = dormBedService.lambdaUpdate() .eq(DormBed::getId, dto.getBedId()) .eq(DormBed::getStatus, 0) // 必须是空闲状态 .set(DormBed::getStatus, 1) .update(); if (updated == 0) { throw new BizException("床位已被占用,请刷新后重试"); } // 3. 更新房间已用床位数、标识状态 dormRoomService.updateUsedBeds(dto.getRoomId()); // 4. 生成入住记录 CheckinRecord record = new CheckinRecord(); record.setStudentId(student.getId()); record.setRoomId(dto.getRoomId()); record.setBedId(dto.getBedId()); record.setCheckinTime(LocalDateTime.now()); record.setIsActive(1); checkinRecordService.save(record); // 5. 更新学生状态 studentService.lambdaUpdate() .eq(Student::getId, student.getId()) .set(Student::getStatus, 1) .set(Student::getRoomId, dto.getRoomId()) .update(); }

这段代码的亮点在eq(DormBed::getStatus, 0)和set在同一SQL里完成条件更新。MySQL的行锁会保证同一时刻只有一个事务能成功更新这条床位的状态。熟练掌握这套“乐观锁思路”的写法和底层原理,答辩时至少能讲半分钟。

4.3 报修工单的状态机流转设计

报修模块最忌讳的就是把状态写死在if-else里。我建议用状态机或者至少用枚举 + 状态流转表来管理。

状态枚举:

public enum RepairStatusEnum { PENDING(0, "待派单"), PROCESSING(1, "处理中"), WAITING_ACCEPT(2, "待验收"), COMPLETED(3, "已完成"), CANCELLED(4, "已取消"); private final Integer code; private final String desc; }

状态流转的合法性校验是一个容易丢分的点。比如“待派单”状态的工单不能直接变成“已完成”,必须一步步流转。我写了一个简单的状态流转校验方法:

public void transferStatus(Long orderId, RepairStatusEnum from, RepairStatusEnum to) { RepairOrder order = repairOrderService.getById(orderId); if (order == null) { throw new BizException("工单不存在"); } if (!order.getStatus().equals(from.getCode())) { throw new BizException("当前工单状态不允许此操作"); } repairOrderService.lambdaUpdate() .eq(RepairOrder::getId, orderId) .set(RepairOrder::getStatus, to.getCode()) .update(); }

各角色的操作对应不同的状态流转:学生提交工单后状态为PENDING;维修工接单后调用transferStatus(orderId, PENDING, PROCESSING);维修完成回填结果后状态变成WAITING_ACCEPT;宿管员确认无误后调用transferStatus(orderId, WAITING_ACCEPT, COMPLETED);学生取消自己的工单时只有PENDING状态允许取消,防止工单已经在处理中还能被单方面取消。这种设计在答辩演示时边界很清晰——“没有在规定状态的操作一律拒绝”,一句话就能把业务逻辑讲完整。

4.4 水电费账单与定时任务

水电费的核心逻辑是“按月生成账单、按房间关联学生、缴费后更新状态”。这里面的坑是:同一房间同一月份不能生成多次账单。靠的就是前面提到的联合唯一约束。

生成账单我用Spring的@Scheduled定时任务,每月1号凌晨执行一次:

@Component public class BillGenerateTask { @Scheduled(cron = "0 0 1 1 * ?") // 每月1号凌晨1点执行 public void generateMonthlyBill() { String period = YearMonth.now().minusMonths(1).toString(); // 生成上月账单 List<DormRoom> rooms = dormRoomService.list(); for (DormRoom room : rooms) { // 通过唯一约束防重复 WaterElectricBill bill = billService.lambdaQuery() .eq(WaterElectricBill::getRoomId, room.getId()) .eq(WaterElectricBill::getPeriod, period) .one(); if (bill != null) { continue; // 已生成过,跳过 } WaterElectricBill newBill = new WaterElectricBill(); newBill.setRoomId(room.getId()); newBill.setPeriod(period); // 水量、电量需要从抄表记录里算差值,这里简化 newBill.setWaterAmount(calculateWater(room.getId(), period)); newBill.setElectricAmount(calculateElectric(room.getId(), period)); newBill.setWaterFee(newBill.getWaterAmount().multiply(priceMap.get("water"))); newBill.setElectricFee(newBill.getElectricAmount().multiply(priceMap.get("electric"))); newBill.setTotalFee(newBill.getWaterFee().add(newBill.getElectricFee())); newBill.setStatus(0); billService.save(newBill); } } }

定时任务这里有一个细节:如果项目部署在多实例环境,多个实例同时执行定时任务会重复生成账单。虽然SQL层面的唯一约束会兜底,但更好的方案是用Redis的setIfAbsent做分布式锁,获取不到锁就直接返回,避免不必要的数据库压力。这个细节写在论文“系统优化”里非常加分。

4.5 数据看板接口

最后说下数据看板的实现。管理后台首页必然要有一个统计面板,展示总宿舍数、已入住人数、入住率、当月报修总量、待处理工单数、水电费收缴率等指标。接口可以直接写聚合SQL:

@GetMapping("/dashboard") public Result<DashboardVO> dashboard() { DashboardVO vo = new DashboardVO(); // 楼栋数 vo.setBuildingCount(dormBuildingService.count()); // 房间总数 vo.setRoomCount(dormRoomService.count()); // 入住人数:room表beds_used字段求和 vo.setOccupiedBeds(dormRoomService.getBaseMapper().selectOccupiedBeds()); // 待处理工单 vo.setPendingRepair(repairOrderService.lambdaQuery() .eq(RepairOrder::getStatus, 0) .count()); vo.setOccupancyRate(calculateOccupancyRate()); return Result.success(vo); }
<select id="selectOccupiedBeds" resultType="java.lang.Integer"> SELECT SUM(beds_used) FROM dorm_room </select>

然后前端用ECharts把入住率、报修状态分布、近6月水电费曲线画出来。答辩演示时这个页面一亮相,项目档次立刻不一样。

5. 前端App与后台联调最容易翻车的几个地方

后端接口写完之后,前后端联调是另一个重灾区。很多同学后端接口测试全过,结果一接前端就报错,卡了半天发现是跨域问题或者字段名对不上。这块我踩过的坑比较多,列几个重点。

5.1 uni-app和Spring Boot联调时的接口对接

uni-app的请求封装建议放在utils/request.js里。核心逻辑是:请求拦截器里带上Token,响应拦截器里统一处理业务错误码和HTTP错误码。

import { BASE_URL } from '@/config/env.js' export function request(options) { return new Promise((resolve, reject) => { uni.request({ url: BASE_URL + options.url, method: options.method || 'GET', data: options.data || {}, header: { 'Content-Type': 'application/json', 'token': uni.getStorageSync('token') || '' }, success: (res) => { if (res.data.code === 200) { resolve(res.data.data) } else if (res.data.code === 401) { uni.navigateTo({ url: '/pages/login/login' }) reject(res.data) } else { uni.showToast({ title: res.data.message, icon: 'none' }) reject(res.data) } }, fail: (err) => { uni.showToast({ title: '网络异常', icon: 'none' }) reject(err) } }) }) }

前后端字段名的坑,这是最大的翻车点。后端Java实体类如果用驼峰命名,比如roomNo、buildingId,前端拿到的JSON里就是roomNo、buildingId,这个没问题。问题出在后端返回了LocalDateTime类型时,JSON序列化出来是一串数字或数组格式,前端解析不了。处理方案:在application.yml里配置Jackson的日期格式,统一成yyyy-MM-dd HH:mm:ss。这笔配置一定要加上,别等到联调才发现。

5.2 管理后台的角色权限路由

管理后台的权限路由,用的是前端路由守卫配合后端返回的菜单数据来做。后端登录接口返回roles字段,前端拿到角色后,在路由守卫里判断当前用户是否有权限访问对应路由,没有权限直接重定向到403页面。这里的关键是在路由表里给每个路由配置meta.roles,比如/admin/room只有ADMIN和DORM_ADMIN角色能访问。

5.3 跨域、Token和图片上传这几个老难题

跨域问题在开发环境非常常见:前端localhost:8080,后端localhost:8081,浏览器默认阻止跨域请求。解决方案是在后端写一个处理CORS的过滤器或者直接在配置类实现WebMvcConfigurer的addCorsMappings方法。注意allowedOrigins不要写*,这样会跟Token的Authorization头冲突。

Token过期的处理:Sa-Token的Token默认30分钟过期。前端在响应拦截器里判断HTTP 401状态码,如果是Token过期就清除本地缓存,跳转登录页。同时后端要注意在登录接口里对Token做“续签”或者“记住我”的逻辑,避免演示时操作到一半被强制下线。

图片上传是另一个高频槽点。报修单要上传现场照片,学生头像要上传图片。本地存储方案就是在application.yml里配置上传路径,然后用ResourceHandler映射虚拟路径,让上传的图片可以通过URL直接访问。别用File类的绝对路径硬编码,一来换机器就失效,二来答辩演示时换一台电脑就看不到之前上传的图片了。

6. 常见问题与解决方案实录

开发过程中遇到Bug是常态,我把几个出现频率最高的问题和排查思路整理出来,方便你遇到的时候直接对照。所有问题都是我在实际搭这类项目时真实踩过的,不是编的。

6.1 项目启动失败或Mapper注入报错的排查过程

现象:启动时提示Consider defining a bean of type 'xxxMapper' in your configuration。

排查步骤:

  1. 看@SpringBootApplication所在包路径。启动类必须在所有包的上一层,否则@MapperScan扫描不到Mapper接口。
  2. 看@MapperScan("com.xxx.modules.**.mapper")注释写的是不是实际Mapper包路径。
  3. 检查UserMapper接口上有没有加@Mapper注解,如果加了@MapperScan,接口上就不需要重复加。
  4. 检查pom.xml有没有引入mybatis-plus-boot-starter的依赖版本冲突,尤其是Spring Boot 2.7配MyBatis-Plus 3.5.x是最稳妥的。

现象:启动成功但请求接口返回404。

这大概率是@RestController的类路径问题,或者接口方法的@RequestMapping写错。先用curl http://localhost:8081/api/v1/xxx测一下接口,看是不是真的调不到。也很常见的一个问题是context-path配置了/api,前端请求时没有带这个前缀。

6.2 前端页面数据展示异常,但接口测试是好的

这种大部分出在JSON序列化上。我在答辩前用Postman测接口一切正常,但uni-app里页面一直显示不出数据,最后发现是后端返回了个LocalDateTime字段导致序列化异常,前端拿不到整个对象。解决方案就是在全局配置Jackson格式化。

另一种情况是MyBatis-Plus分页插件没配置。如果用了MybatisPlusInterceptor的PaginationInnerInterceptor,但没配置数据库类型,MySQL下分页查询会失效,返回出来的total是0。配置代码:

@Configuration public class MybatisPlusConfig { @Bean public MybatisPlusInterceptor mybatisPlusInterceptor() { MybatisPlusInterceptor interceptor = new MybatisPlusInterceptor(); interceptor.addInnerInterceptor(new PaginationInnerInterceptor(DbType.MYSQL)); return interceptor; } }

6.3 定时任务不执行和事务不回滚,这两个坑很隐蔽

定时任务不执行:检查启动类或配置类有没有加@EnableScheduling注解。这是最容易被忽略的。如果加了还不执行,检查cron表达式是否写错。Spring的@Scheduled默认是单线程执行,如果一个任务卡住了,后面的任务都会排队等待。可以配上@Async,或者配置一个线程池来跑定时任务。

事务不回滚:我强烈建议在@Transactional上显式声明rollbackFor = Exception.class,因为Spring默认只在RuntimeException时回滚,检查异常(比如文件读写失败)不会触发回滚。另一个常见的坑是同类内部方法调用导致@Transactional失效,因为代理对象没生效。解决方案是注入自身代理或者把事务方法拆到另一个Service里。

6.4 给你一份答辩前必须自测的清单

每次答辩之前,我建议按下面这个清单完整走一遍用户流程,任何一步卡住都必须修好再上台:

  1. 用学生账号登录App,查看宿舍信息、水电费账单。
  2. 学生提交一条报修单,拍一张照片上传,提交成功。
  3. 退出登录,用维修工账号登录,看到新报修单,接单并回填处理结果。
  4. 用宿管员账号登录,确认验收这张报修单。
  5. 用管理员账号登录,新增一间宿舍房间,再新增一名学生并把TA分配到新房间。
  6. 打开数据看板,确认入住率、报修数等数据有变化。
  7. 慢速网络下刷新页面,能看到正常的前端加载提示而不是一直白屏。

7. 最后说点心里话

这个项目从零开始搭到能答辩演示,我完整走过一遍,前后大概花了三周,其中数据库设计用了一周,后端写接口用了十天,前端页面和联调用了四天。最难熬的不是写代码,而是需求不清晰的时候硬着头皮往下写,写到一半发现表结构不对,推倒重来。

但也是这些重来的经历让我明白了一件事:毕业设计的核心不是代码量多大、技术多花哨,而是你能不能把每一张表、每一个接口、每一处设计决策的“原因”讲清楚。答辩老师问你的问题,九成都会落在“为什么这么做”上。为什么用Sa-Token不用Spring Security?为什么金额用BigDecimal?为什么报修单要设计成状态机流转?这些问题如果你在写代码的时候就想明白了,答辩就是走过场;如果只是照搬别人的项目,随便追问两句就会露怯。

最后分享一个小技巧,这个对答辩拿高分特别有用:打印一份完整的数据库设计文档,包含每张表的字段说明、每个状态的枚举说明、每个核心接口的请求/响应示例,装订成册带进答辩现场。老师问任何问题,你都能在三秒内翻到对应页面,这种“项目是你自己的”的感觉,装是装不出来的。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询