做毕设辅导这些年,Spring Boot 小区物业管理系统是我见过被选次数最多的题目之一。说实话,这个选题能"长盛不衰"是有原因的:业务场景贴近生活、模块边界清晰、技术点覆盖全面,从增删改查到权限控制、从文件上传到报表统计,几乎能把大学四年学的东西串一遍。但正因为它太常见,很多同学做出来的东西千篇一律——功能清单抄来抄去、数据库表结构雷同、答辩时被问两句就卡壳。这篇文章我就把这个题目的拆解思路、技术选型、核心实现和踩坑记录完完整整写出来,给正在纠结选题或者已经选了但不知道从哪下手的你一个参考。
不管是用来应付毕业设计,还是真想做一个能跑的社区物业管理系统,这篇文章都能帮你省下不少自己摸索的时间。我会从选题的价值分析讲到数据库设计,从核心模块实现讲到答辩避坑,全程用我实际带项目时的做法来讲,希望对你有用。
1. 选题拆解:这个题目到底在做什么
1.1 为什么每年都有学生选这个题目
先聊点实在的。毕设选题有个隐性要求:既要有一定的业务复杂度,又不能复杂到一个人做不完。小区物业管理系统恰好卡在这个平衡点上。
从业务层面看,物业管理涉及的角色很清晰——管理员、物业人员、业主,三种角色的权限天然不同,这决定了系统必须有登录鉴权和角色管理,这是答辩时最容易被问到的点。从功能层面看,房屋信息、业主档案、报修工单、费用收缴、公告通知,每一个模块都是标准的信息管理系统(MIS)雏形,单独拆出来都能对应一门课的大作业,合在一起就是一个完整的毕设项目。
更重要的是,这个题目有大量现成的业务规则可以挖掘。比如物业费怎么按面积计算、车位管理费怎么按周期收取、报修单从提交到处理完要经过哪些状态,这些规则你梳理得越细,系统的"设计感"越强。很多同学做出来的系统被老师批评"太像课程设计",根本原因就是业务规则太单薄——每个模块只有简单的增删改查,没有状态流转、没有权限隔离、没有数据关联。说白了,毕设和课设的区别,就差在这些业务逻辑的厚度上。
我做辅导时给学生的建议是:把这个题目当成一个小型的 SaaS 产品来思考。业主端、物业端、管理端分开设计,每端的功能清单独立整理,这样系统架构自然就撑起来了。
1.2 系统功能边界怎么划
很多同学一上来就想着功能越多越好,结果把系统做成一个大杂烩:既要在线缴费,又要智能门禁,还要对接物联网设备。我劝你冷静一下。毕设的评估重点是"设计合理性和完成度",不是功能数量。一个只有六个模块但每个模块都做得扎实的系统,远比一个宣称"全功能智慧社区"但每个功能都是半成品的系统得分高。
我建议的标准功能清单是这样的:
| 模块 | 核心功能 | 涉及角色 | 设计亮点 |
|---|---|---|---|
| 房屋信息管理 | 楼栋、单元、房屋档案维护 | 管理员/物业 | 房屋与业主一对多、车位关联 |
| 业主信息管理 | 业主档案、家属信息、联系方式 | 管理员/物业 | Excel 导入导出 |
| 报修管理 | 报修提交、派单、处理、评价 | 业主/物业 | 状态机流转 + 图片上传 |
| 费用管理 | 物业费、车位费账单生成与收缴 | 物业/管理员 | 定时生成账单 + 缴费状态统计 |
| 公告通知 | 小区公告发布、查看 | 管理员/业主 | 置顶、草稿、阅读记录 |
| 投诉建议 | 投诉提交、处理反馈 | 业主/物业 | 处理时限提醒 |
| 车位管理 | 车位信息、绑定车辆、费用关联 | 管理员/业主 | 车位状态变更 |
| 系统管理 | 用户、角色、菜单权限 | 管理员 | Spring Security/RBAC |
这个清单已经是比较克制的版本了,但覆盖了一个住宅小区物业管理的核心闭环:人(业主)—物(房屋/车位)—事(报修/投诉)—钱(费用)—信息(公告)。你在开题报告里把这条业务链讲清楚,老师的第一印象就不会差。
这里多提醒一句:能做成"闭环"的功能才是好功能。比如报修单,不是业主提交完就结束了,而是要走完"提交→审核→派单→处理→完成→评价"整个流程,每一步都要有状态记录和操作人记录。这种闭环设计,答辩时是天然的展示点。
2. 技术选型:Spring Boot 为主的技术栈怎么搭
2.1 后端框架版本和依赖怎么选
Spring Boot 的版本选择是第一道坎。我知道今年很多人电脑上装的是 Spring Boot 3.x,但你得清楚:3.x 要求 JDK 17 起步,而且 javax 包名改成了 jakarta,很多老教程和现成代码直接跑不起来。如果你不是对新技术特别熟悉,我建议毕设老老实实用Spring Boot 2.7.x + JDK 1.8/11的组合。
为什么?两个原因。第一,网上能找到的参考代码、博客教程、答辩资料,绝大多数基于 2.x 版本,你遇到问题搜解决方案时命中率高得多。第二,很多学校机房和老师的演示环境还是 JDK 8,你的项目如果只能在 JDK 17 上跑,演示时可能出幺蛾子。
核心依赖我一般这样配:
<!-- Spring Boot 2.7.18 为基础 --> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-web</artifactId> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-validation</artifactId> </dependency> <dependency> <groupId>com.baomidou</groupId> <artifactId>mybatis-plus-boot-starter</artifactId> <version>3.5.3</version> </dependency> <dependency> <groupId>mysql</groupId> <artifactId>mysql-connector-java</artifactId> <version>8.0.33</version> </dependency> <dependency> <groupId>io.jsonwebtoken</groupId> <artifactId>jjwt</artifactId> <version>0.9.1</version> </dependency> <dependency> <groupId>org.projectlombok</groupId> <artifactId>lombok</artifactId> </dependency>ORM 框架我用 MyBatis-Plus 而不是原生 MyBatis,这是我带项目时一直坚持的选择。理由很现实:毕设项目通常有七八张甚至十几张表,用 MyBatis-Plus 的 BaseMapper 能少写一大半单表 CRUD 代码,把时间省下来做业务逻辑和界面优化,性价比高得多。当然,如果你对 MyBatis 的 XML 映射很熟,用原生 MyBatis 也没问题,就是工作量会大一些。
2.2 前端方案:前后端分离还是服务端渲染
这是另一个反复被问的问题。我在辅导时见过两种主流方案,各有适用场景,给你做个对比:
| 方案 | 技术栈 | 优点 | 缺点 | 适合人群 |
|---|---|---|---|---|
| 前后端分离 | Vue 3 + Element Plus + Axios | 界面美观、简历加分、答辩效果好 | 开发量翻倍、CORS/跨域问题多 | 有前端基础、时间充裕 |
| 服务端渲染 | Thymeleaf + Bootstrap | 开发效率高、部署简单、单体项目结构清晰 | 页面交互体验一般 | 前端基础薄弱、时间紧 |
如果你的毕设时间在三到四个月,而且你之前没怎么写过 Vue,我真心建议你考虑 Thymeleaf 方案。不是说 Vue 不好,而是"用不熟的东西做大项目"风险太高。今年我见过好几个选了前后端分离的学生,最后卡在 Vue 的路由守卫和 Axios 拦截器上,浪费了大量时间。
但话说回来,如果你想冲优秀毕业论文,或者准备把这项目写进简历投后端岗位,前后端分离是更拿得出手的方案。选 Vue 的话,管理端用 Element Plus,组件现成的,表格、表单、弹窗一套下来界面不会太丑。前后端通信用 RESTful 风格接口,返回统一 JSON 格式,这一点下面会细讲。
还有一种折中方案:管理端用 Thymeleaf 做,业主端用 Vue 做。这样工作量可控,又能展示"我两种技术都会"。我带的不少学生用这种方式,效果也还不错。
2.3 数据库设计:七张核心表的关系梳理
数据库设计是整个项目的灵魂。很多同学栽在这一步,表建得随意,字段命名混乱,关联关系不清,写到后面代码越来越别扭。我建议你花至少两三天专门做表结构设计,画一张简单的 E-R 图,理清关系再动手写代码。
以我常用的核心表为例:
- sys_user(用户表):id, username, password(加密存储), real_name, phone, role_id, status
- sys_role(角色表):id, role_name, role_key, description。角色和用户是一对多
- building(楼栋表):id, building_no, unit_count, floor_count
- house(房屋表):id, building_id, house_no, area, status, owner_id。
- owner(业主表):id, user_id, name, id_card, phone, house_id。这里要注意,业主和用户表的关系,我一般让业主表通过 user_id 关联登录账号,同时保存一份冗余的名字和手机号,这样页面展示时不用频繁联表
- repair_order(报修表):id, order_no, owner_id, type, description, images, status, create_time, assignee, handle_time, rating
- fee_bill(费用账单表):id, house_id, fee_type, amount, status, period, create_time, pay_time, pay_method
字段设计上我总结了几条实战经验:
- 所有表必须包含
create_time和update_time两个字段,MyBatis-Plus 的自动填充可以帮你写,但字段必须规划好 - 金额字段用
DECIMAL(10,2),绝对不要用 float/double,不然算物业费会出现 0.1+0.2=0.30000000000000004 的经典翻车 - 状态字段用
TINYINT或VARCHAR都行,但要在代码里定义常量,比如报修状态0待分配,1处理中,2已完成,3已取消,不要散落一堆魔法数字 - 逻辑删除是标配,加一个
deleted字段,用 MyBatis-Plus 的@TableLogic注解,这样误删的数据还能找回来
关于数据库表的一个容易忽略的点:业主和房屋的关系到底是一对一还是一对多。现实中一套房可能有多个家庭成员,但产权人通常只有一个。我建议设计成:房屋表的owner_id指向主要业主,家庭成员另外建一张owner_member表来存。这样既满足了一个家庭多个成员的实际场景,又不会把关系搞得太复杂。
3. 核心模块设计与实现
3.1 业主信息与房屋管理:增删改查里也有门道
业主和房屋管理看起来是最基础的增删改查,但这里恰恰是拉开差距的地方。我用一个具体的场景来说明:物业人员在录入业主信息时,通常需要先选择"XX栋XX单元XX室",如果前端是一个下拉框直接列出来,那这栋楼有 30 层、每层 4 户的话,就是 120 个选项,用户体验非常差。
正确做法是级联选择:先选楼栋,再选单元,再选房号。数据接口可以分别提供building/list、unit/list?buildingId=xxx、house/list?buildingId=xxx&unit=xx三个查询接口。这种细节在答辩时主动提出来,老师会觉得你真的思考过业务。
房屋管理中还有一个敏感点:房屋状态变更。入住、空置、出租、装修中,不同状态下的房屋应该能执行的操作不一样。空置房不能被报修,装修中的房屋不能办理入住登记。这些规则用状态枚举和业务校验来实现,不要让人在界面上随便改状态。
在实际编码中,我通常会为业主信息的手机号、身份证号做重复性校验,同时在新增和编辑时复用同一个校验逻辑:
@Override public boolean saveOwner(OwnerDTO dto) { LambdaQueryWrapper<Owner> wrapper = new LambdaQueryWrapper<>(); wrapper.eq(Owner::getIdCard, dto.getIdCard()); Long count = ownerMapper.selectCount(wrapper); if (count > 0) { throw new BusinessException("该身份证号已登记业主信息"); } // 补充默认字段 Owner owner = new Owner(); BeanUtils.copyProperties(dto, owner); owner.setCreateTime(LocalDateTime.now()); return ownerMapper.insert(owner) > 0; }这段代码背后有一个容易被忽略的检查:异常处理。如果直接在 Controller 里抛异常,前端拿到的是默认错误页,体验很糟糕。我在项目中用@RestControllerAdvice做了统一异常处理,业务异常返回 code=500 + 具体提示信息,这个设计后面单独讲。
3.2 报修工单:状态机设计是核心竞争力
报修模块是整个系统里最值得好好做的功能,因为它的业务状态流转最丰富。我见过很多同学的报修就是一张表,业主填完内容提交就完了,这完全体现不出设计能力。
我建议的报修流程是这样的:
业主提交 → 物业审核派单 → 维修工接单处理 → 业主确认完成 → 评价打分
每一步都是一个状态节点,同时记录操作时间和操作人。对应的状态枚举:
public enum RepairStatus { PENDING(0, "待派单"), PROCESSING(1, "处理中"), COMPLETED(2, "已完成"), CANCELED(3, "已取消"), CONFIRMED(4, "已确认"); private final int code; private final String desc; }这里有个细节:业主提交报修时可以上传照片。照片怎么存?本地磁盘还是云存储?我的建议是本地存储,用一个upload/目录存文件,数据库里保存访问 URL。做的时候需要配置静态资源映射,把物理路径映射成 /upload/** 的虚拟路径。如果你用 Vue 前后端分离,这里还要注意前端预览时 URL 是否带上了完整的 IP 和端口。
在维修工处理完成后,系统要能自动通知业主确认——最简单的做法是给业主生成一条站内消息,也就是建一张message表,在状态变更时插入一条记录。这个"通知机制"虽然不复杂,但它让报修模块的闭环更完整,答辩时也是一个讲点。
报修模块的权限设计也要想清楚:业主只能看到自己的报修单、提交报修;物业人员能看到所有报修单、执行派单操作;管理员能做删除和统计分析。这些权限通过角色来控制,而不是在代码里写死判断,这样系统才有扩展性。
3.3 费用管理:定时任务与账单生成逻辑
物业费模块是很多同学最头疼的部分,因为这个功能涉及"定时生成账单"这个看着高大上、其实就是定时任务的东西。Spring Boot 里实现定时任务非常简单,一个@EnableScheduling加一个@Scheduled注解就搞定了:
@Component public class FeeBillScheduler { @Scheduled(cron = "0 0 2 1 * ?") // 每月1号凌晨2点执行 public void generateMonthlyBills() { List<House> houses = houseMapper.selectList(null); for (House house : houses) { if (house.getStatus() != HOUSE_OCCUPIED) { continue; // 空置房不生成账单 } // 物业费 = 面积 × 单价 BigDecimal amount = house.getArea() .multiply(new BigDecimal("1.50")) .setScale(2, RoundingMode.HALF_UP); // 插入账单记录 feeBillMapper.insert(buildBill(house, amount)); } } }这个 cron 表达式0 0 2 1 * ?表示每月 1 号凌晨 2 点执行。为什么选这个时间?因为物业管理费通常按月计算,月初生成账单比较符合业务习惯。在生成账单时还要做去重判断——如果本月账单已经生成过,就不能再生成一次,否则业主会看到两笔一模一样的费用。
这里有个前提要交代:定时任务在多实例环境下会有重复执行的问题。但毕设项目一般只有一个实例跑,所以不用考虑分布式锁,你可以在论文里提一句"本系统采用单机部署,暂未考虑分布式场景下的任务幂等",这反而是个加分项,显得你思考过这个问题。
缴费记录这一块,如果做真实的在线支付,需要对接微信支付或支付宝支付,牵扯到商户号申请、证书配置、回调验签,复杂度一下就上来了。毕设阶段不建议直接对接真实支付,建议做到"账单生成 + 标记缴费 + 打印收据"这个程度就够了。你可以在开题报告的"系统展望"部分写"未来可对接微信支付实现线上缴费",老师明白这个工作量,不会在这个环节为难你。
不过缴费状态的统计报表一定要做。比如本月应收、已收、欠费金额,按楼栋维度的收缴率统计,这些数据用 SQL 的 SUM 和 GROUP BY 就能查出来,然后展示成表格或柱状图。数据可视化在答辩时是颜值担当,建议用 ECharts 做一个简单的仪表盘,接口返回统计数据,前端渲染折线图或饼图,会比干巴巴的表格生动很多。
3.4 公告通知与投诉建议:容易被忽略的加分项
公告通知模块通常被认为"太简单不值得做",但我想说,这里的细节决定体验。比如:公告要不要支持发布后修改和撤回?要不要记录谁看过?要不要区分置顶公告?这些功能都不难实现,但加上之后系统就"活"了。
我的做法是这样的:公告表加is_top和status字段,status 区分草稿和已发布。已发布的公告可以撤回,撤回后业主端不再显示。阅读记录使用公告表加一个read_count字段即可,精确到用户维度的已读/未读在毕设里有点重了。
投诉建议模块的关注点是处理时限。给投诉单加一个handle_deadline字段,创建时自动设置为当前时间加 3 天,超过时间未处理就标记为超时。这个逻辑可以用一个定时任务每天扫描一次,也可以等列表查询时动态判断。动态判断更简单:
if (complaint.getHandleDeadline().isBefore(LocalDateTime.now()) && complaint.getStatus() == UNPROCESSED) { complaint.setTimeout(true); }这个设计看似小事,但答辩时你可以说"系统实现了投诉处理时限的自动监控",是一个非常清晰有说服力的功能点。
4. 从零到一:实操关键步骤记录
4.1 项目初始化与分层结构
搭项目的第一步不是急着写 Controller,而是把包结构定好。我建议的分层方式是常见的 Controller-Service-Mapper 三层结构,具体包名如下:
com.example.property ├── controller // 接口层,只做参数接收和结果返回 ├── service // 业务逻辑层,核心判断都在这里 ├── mapper // 数据访问层,继承 BaseMapper ├── entity // 数据库实体类 ├── dto // 前端传参和接口返回的数据对象 ├── config // 配置类,如 WebMvcConfig、MybatisPlusConfig ├── common // 统一返回、异常处理、工具类 └── enums // 状态枚举很多同学习惯 controller 里写一堆业务代码,看起来快捷,但后期 debug 非常痛苦。我遇到过学生把几十行逻辑写在 Controller 里,出问题后一行行打断点,完全分不清是哪一层的问题。分层不是为了好看,是为了让你定位问题时能快速缩小范围。
项目创建方式我推荐用 IDEA 自带的 Spring Initializr,填好 Group 和 Artifact,选 Web、Validation、Lombok 依赖,直接生成。如果你用 MyBatis-Plus,记得初始化时把自动填充字段的处理器配好:
@Configuration public class MybatisPlusConfig { @Bean public MetaObjectHandler metaObjectHandler() { return new MetaObjectHandler() { @Override public void insertFill(MetaObject metaObject) { this.strictInsertFill(metaObject, "createTime", LocalDateTime.class, LocalDateTime.now()); this.strictInsertFill(metaObject, "updateTime", LocalDateTime.class, LocalDateTime.now()); } @Override public void updateFill(MetaObject metaObject) { this.strictUpdateFill(metaObject, "updateTime", LocalDateTime.class, LocalDateTime.now()); } }; } }这个配置配好之后,所有实体类的 createTime 和 updateTime 都会自动填充,你不用在每个 Service 里手动 set 时间,省下的时间和少掉的低级错误都不是一点半点。
4.2 统一返回结构与全局异常处理
前后端交互时最容易出现的问题就是接口返回格式不统一。有的接口返回对象,有的返回 List,有的返回布尔值,前端解析起来非常痛苦。我在做项目时一定会定义一个统一的返回类:
@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("操作成功"); result.setData(data); return result; } public static <T> Result<T> error(String message) { Result<T> result = new Result<>(); result.setCode(500); result.setMessage(message); return result; } }同时配一个全局异常处理器:
@RestControllerAdvice public class GlobalExceptionHandler { @ExceptionHandler(BusinessException.class) public Result<?> handleBusinessException(BusinessException e) { return Result.error(e.getMessage()); } @ExceptionHandler(MethodArgumentNotValidException.class) public Result<?> handleValidationException(MethodArgumentNotValidException e) { String msg = e.getBindingResult().getFieldError().getDefaultMessage(); return Result.error(msg); } @ExceptionHandler(Exception.class) public Result<?> handleException(Exception e) { log.error("系统异常", e); return Result.error("系统繁忙,请稍后重试"); } }有了这套机制,你的 Service 里只要throw new BusinessException("房屋已被入住"),前端就能收到干净的提示信息,再也不用担心堆栈信息直接抛给用户。写项目的时候把这一步作为基础设施先做掉,后面所有接口都是基于这套返回结构开发,会顺手很多。
4.3 登录鉴权:JWT 方案的完整链路
登录模块是整个系统安全性的门面,一定不能省。我不推荐做简单的 session 登录就完事,因为答辩时老师大概率会问"你的系统是怎么做权限控制的"。用 Spring Security + JWT 虽然配置多一些,但能讲的东西多很多。
JWT 的基本流程是:用户登录成功后,服务端生成一个 token 返回给前端;前端在后续请求中把 token 放在请求头Authorization: Bearer xxx;后端通过拦截器或过滤器解析 token,取出用户信息,判断角色权限。
核心代码大致是这样:
@Service public class LoginService { @Autowired private SysUserMapper sysUserMapper; public String login(LoginDTO dto) { SysUser user = sysUserMapper.selectOne( new LambdaQueryWrapper<SysUser>() .eq(SysUser::getUsername, dto.getUsername())); if (user == null || !BCrypt.checkpw(dto.getPassword(), user.getPassword())) { throw new BusinessException("用户名或密码错误"); } if (user.getStatus() == 0) { throw new BusinessException("账号已被禁用"); } // 生成 token,payload 里放 userId 和 role return Jwts.builder() .claim("userId", user.getId()) .claim("role", user.getRole()) .setExpiration(new Date(System.currentTimeMillis() + 86400000)) .signWith(SignatureAlgorithm.HS256, SECRET_KEY) .compact(); } }密码存储用 BCrypt 加密,不要用 MD5,这一点如果你在论文里写"密码经过 BCrypt 加密存储",老师会认可你的安全意识。Token 有效期设 24 小时,前端收到 401 响应时跳回登录页,这个前端逻辑别忘了。
然后配一个拦截器,对所有/api/**接口做 token 校验(登录接口和静态资源除外)。这里有个我踩过的坑:拦截器放行的路径一定要写对,否则会出现"登录了但请求还是被拦截"或者"未登录也能访问接口"两种极端情况。我的配置是用两个废弃接口做验证:一个放行,一个不放行,再打印 token 解析日志仔细核对。
4.4 文件上传与图片预览
报修上传图片、公告插入图片都要用到文件上传。Spring Boot 实现文件上传不难,难的是"上传后怎么访问"。
我的配置做法是:
spring: servlet: multipart: max-file-size: 10MB max-request-size: 20MB file: upload-dir: ./upload access-path: /upload/**然后写一个配置类把本地路径映射到虚拟访问路径:
@Configuration public class WebMvcConfig implements WebMvcConfigurer { @Value("${file.upload-dir}") private String uploadDir; @Value("${file.access-path}") private String accessPath; @Override public void addResourceHandlers(ResourceHandlerRegistry registry) { registry.addResourceHandler(accessPath) .addResourceLocations("file:" + uploadDir + "/"); } }上传接口返回的 URL 记得拼上完整的主机地址,比如http://localhost:8080/upload/xxx.jpg。如果你用前后端分离部署,前端访问后端上传的图片时会出现跨域或者地址不对的问题,解决方法是统一用一个配置文件管理"图片基础地址",前端从配置文件读取而不是把地址写死在代码里。
这里还有个安全提醒:上传文件一定要做类型校验和后缀白名单,不要直接接收任意文件。虽然毕设项目不会真被人攻击,但代码里加上类型判断,显得你考虑了安全问题,这也是答辩时可以主动讲的一个点。
5. 常见问题与排错技巧实录
5.1 高频报错与解决办法速查表
带项目的过程中,学生问我最多的问题基本都集中在这几个地方,我整理成一张速查表,你直接对照排查:
| 现象 | 可能原因 | 解决办法 |
|---|---|---|
启动报Failed to configure a DataSource | 未配置数据源或配置被跳过 | 检查 application.yml 里 spring.datasource 配置,或排除数据源自动配置 |
| Mapper 接口找不到对应 SQL | Mapper 接口没加@Mapper注解,或 XML 路径配置错误 | 在启动类加@MapperScan("com.example.property.mapper") |
| 查询列表时间显示为 Timestamp 一大串 | 没配置时间格式化 | application.yml 加spring.jackson.date-format: yyyy-MM-dd HH:mm:ss和 time-zone |
| 删除数据后再次查询还能看到 | 未做逻辑删除配置或查询未过滤 deleted | 配置@TableLogic并在查询时使用 MyBatis-Plus 的条件构造器 |
| 跨域请求被拦截 | 前后端不在同一端口或域名 | 写 CorsConfig 配置类,允许指定来源和请求头 |
| JWT 解析报 SignatureException | SECRET_KEY 不一致,或 token 被截断 | 确认生成和校验使用同一个密钥,打印 token 对比长度 |
| 上传图片后访问 404 | 静态资源映射没生效 | 检查 WebMvcConfig 的 addResourceHandlers 是否被正确扫描 |
| 事务不生效 | 没加@Transactional,或方法被同类内部调用 | 在 Service 层方法加注解,避免同类内部 this 调用 |
这里挑两个细说。Mapper 找不到 SQL是出现频率最高的,十个人里至少三个会遇到。如果你用 MyBatis-Plus 的标准 BaseMapper 方法,一般不会出问题,但一旦自定义了 XML 中的复杂 SQL,就要检查mybatis-plus.mapper-locations是不是配成了classpath*:mapper/**/*.xml。配错就把 XML 换个位置或改个文件名试试,很多恼人的问题其实就是路径不匹配。
跨域问题是前后端分离项目的标配。出现跨域的典型表现是:前端请求正常发出,但控制台报Access-Control-Allow-Origin错误,后端接口明明返回 200 但前端拿不到数据。我的处理方式是写一个全局的 CorsFilter,允许所有来源、所有请求头、所有方法。毕设阶段不用把跨域配置收得太紧,先让项目跑通再说。
@Configuration public class CorsConfig { @Bean public CorsFilter corsFilter() { CorsConfiguration config = new CorsConfiguration(); config.addAllowedOriginPattern("*"); config.addAllowedHeader("*"); config.addAllowedMethod("*"); config.setAllowCredentials(true); UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource(); source.registerCorsConfiguration("/**", config); return new CorsFilter(source); } }5.2 答辩前最容易被问到的几个问题
答辩这个环节,很多时候老师问的问题是有套路的。我带学生模拟了几轮答辩之后,总结了几个高频问题给你打个预防针:
第一个问题:系统的角色权限是怎么设计的?这是必问的。你要能说清楚"用户—角色—权限"的关系:用户表关联角色表,角色表关联菜单权限表,登录时根据角色动态加载菜单,接口层面通过拦截器校验角色。如果只做了简单的前端按钮隐藏来"控制权限",被追着问下去容易露馅,建议至少后端要有一层角色校验。
第二个问题:数据库为什么这样设计?不要只说"根据业务需求建表",要能举出具体例子。比如"房屋表和业主表我设计成一对多,因为现实中一个家庭有多名成员,而产权人和实际居住人可能分别登记",这种回答能体现你真的思考了。
第三个问题:如果用户量大了,系统会有什么瓶颈?这是进阶问题,答不上来也没关系,但答出来就是加分项。你可以说"目前的查询没有做缓存,高频访问的公告和房屋信息可以考虑引入 Redis 缓存;数据库表没有做索引优化,后续会给常用的查询字段如 order_no、house_id 加索引"。能说到 Redis 和索引这两个点,就足够展示你的知识面了。
第四个问题:你在项目中遇到的最大困难是什么?这里千万别回答"没有遇到什么困难"。老师不是在质疑你,而是在测试你的复盘能力。我建议你讲一个真实的坑,比如"在做费用统计时,发现 BigDecimal 的除法会产生无限循环小数,后来通过指定精度和舍入模式解决",这个故事既真实又有技术含量。
5.3 我踩过的坑和给你的建议
最后说几句掏心窝的话。做这个项目,最容易让人崩溃的往往不是某一个技术难点,而是无数个小问题的叠加。我印象很深的一次:一个学生做完整个项目,部署的时候发现打包出来的 jar 包能启动,但页面样式全丢了,排查半天发现是 Thymeleaf 模板里的静态资源路径写成了绝对路径,换环境后找不到对应文件。这种问题没有太高技术含量,但很折磨人。
我的建议是:从第一天起就养成良好的开发习惯。实体类加@Data注解但别忘写序列化版本号;所有时间字段统一用LocalDateTime而不是Date;日志在关键操作里打全,打印入参和出参;每写完一个接口就立即用 Postman 测一遍,不要攒到最后一起测。这些习惯能帮你省掉大量 debug 时间。
另外一个很现实的经验:留出至少两周的缓冲时间。毕设最常见的翻车点是最后阶段的部署和演示环境问题。我见过太多学生在截止前三天还在调本地代码,根本没有时间做部署测试。提前两周把项目打包部署到服务器上,每天跑一遍核心流程,确保演示时万无一失。你自己的电脑能跑和老师评审时能跑,是两个完全不同的事。