每年到了毕设季,总有同学跑来问我:想做一套"看起来有技术含量、又能快速完成"的系统,选什么题目好?我一般会推荐公寓出租管理系统。为什么?因为这个题目踩在了两个点上:一是物业管理、长租公寓这些场景大家都接触过,需求清楚,不用花时间做虚假的调研;二是基于SpringBoot做这类CRUD加业务规则的系统,技术覆盖度刚刚好——既要写后端接口、又要管数据库、还要处理权限和账单这些真实业务逻辑,作为计算机专业本科毕设,查重、答辩、演示都拿得出手。今天这篇文章,我就把这套系统的选题思路、数据库设计、核心代码逻辑、以及我从源码调试和跑通全流程中踩过的坑,一次性讲清楚。
1. 为什么公寓出租管理系统适合做毕设:选题价值与需求拆解
1.1 选题价值:年年有人做,为什么年年稳
毕设选题最怕什么?怕需求太虚,自己都讲不清楚系统要干什么;也怕范围太大,做到最后收不住。公寓出租管理系统正好避开了这两个坑。
首先,公寓出租是所有人都有体感的场景。合租房、长租公寓、隔断间,哪怕你没租过房,也一定听过"押一付三""水电费自理"。这就意味着你在写需求分析、画用例图时不需要编造一堆虚无缥缈的功能,随便找一间公寓管理处的业务流程,抄一抄就能理出七八个真实用例。
其次,这个系统的功能边界很清晰:房源管理、租客管理、合同管理、账单管理、统计分析。每个模块之间逻辑自然,不会出现那种"功能强加"的突兀感。哪怕是基础CRUD,因为有业务状态和金额计算放在里面,答辩时也能体现出你确实理解了业务,而不是只会调MyBatis-Plus的store方法。
更重要的一点是,网上相关的开源项目非常多,随便一搜就是"SpringBoot公寓出租管理系统源码"。对毕设来说,"有参考源码"不是作弊,而是让你能把精力花在改代码、理解代码上,而不是从零开始搭项目骨架。关键在于你能不能把源码吃透,并能回答老师的追问。这篇文章后面会专门讲怎么处理源码里的坑。
1.2 功能性需求:两个角色,四组核心流程
我之前拆过一套比较完整的公寓管理系统源码,它的角色设计得很克制,只有管理员和租客两类。这里我建议你也这么做,千万别为了凑页面数量硬加"维修工""保洁员"这种角色,角色一多,权限模型就要变复杂,毕设工作量成倍上涨,答辩还容易把自己绕进去。
管理员端(对应房东或者公寓运营人员)主要做这些事:
- 楼栋与房间管理:录入公寓楼、楼层、房间类型,维护房间状态(空置 / 已租 / 维修中)
- 租客管理:维护租客档案,包含身份证号、手机号、紧急联系人等信息
- 合同管理:创建租赁合同,关联租客和房间,支持退租、续租
- 账单管理:按月生成租金账单,记录缴费,处理逾期罚金
- 数据统计:查看出租率、月度营收、缴费率等图表
租客端的业务相对简单,通常是:
- 浏览房源:查看可出租的房间及其价格、面积、朝向
- 在线预约:提交看房申请
- 账单查询:查看自己的租金账单、缴费记录
- 个人信息维护
四组核心流程可以概括为:房源录入上架、租客签订合同、账单生成缴费、退租空置流转。这四条流程串起来,整个系统的骨架就都有了。
1.3 非功能性需求:容易被忽略,却是答辩加分项
很多毕设做完了,老师一问"你这个系统安全吗?并发怎么样?"就卡壳。这部分不是让你真去做高并发,而是要明确说出系统做了哪些基本保障:
- 密码不能明文存库,至少用MD5加盐或BCrypt哈希存储
- 接口要有统一的返回格式和全局异常处理,不能一报错就把500页面甩给用户
- 列表页必须做分页,不然数据量一大页面就卡死
- 删除关键数据(比如合同、账单)时要做状态校验,不能物理删除已归档记录
我见过有同学把以上几点写进论文的"非功能性需求"章节,答辩时轻松多拿了几分。这套系统虽然在技术上不难,但只要在这几个细节上体现出专业意识,老师就觉得你是真的在做工程,而不是交作业。
2. 技术选型与项目结构:SpringBoot版本的坑和推荐搭配
2.1 技术栈全景:别光用SpringBoot,还要会搭配
大部分同学拿到的基于SpringBoot的公寓出租管理系统源码,都不是只有后端。前端一般有两种形态:
- 传统形态:后端用模板引擎Thymeleaf,直接渲染HTML页面,项目结构简单,适合Java功底一般的同学
- 前后端分离形态:后端只提供JSON接口,前端用Vue3和Element Plus搭建管理后台,适合想展示更多技术亮点的同学
我个人更建议你选择前后端分离的方案,因为答辩时老师问你"你的系统架构是什么样的",你画出前后端分离的架构图,解释一句"前端通过Axios调用后端RESTful API,后端用JWT维护无状态会话",这个回答的含金量比"这是MVC页面"高不少。
典型的项目搭配如下:
| 层次 | 技术选型 | 说明 |
|---|---|---|
| 后端框架 | SpringBoot 2.7.x | 稳定、资料多,避免版本过高 |
| ORM | MyBatis-Plus 3.5.x | 性能足够,条件构造器写动态SQL很方便 |
| 数据库 | MySQL 5.7 / 8.0 | 学校环境都能满足 |
| 权限方案 | JWT + 拦截器 | 比Spring Security轻量,代码更能讲清楚 |
| 接口文档 | Swagger / knife4j | 演示时调用接口很方便 |
| 前端 | Vue3 + Element Plus | 管理后台组件现成,见效快 |
| 统计图表 | ECharts | 出租率、营收趋势图必备 |
这套组合不是最炫的,但它是毕设场景下性价比最高的方案。每一层你都能说出选择理由,而且出了问题网上都能搜到解决方案。
2.2 SpringBoot版本怎么选,为什么"最新版"会坑到你
这是我最想强调的一点。我看到很多同学下载了源码,本地明明装的是JDK 8,却兴冲冲去pom.xml里把SpringBoot改成了3.1.5,结果一启动就是一堆报错,最后灰溜溜回来问怎么办。
SpringBoot 3.x要求JDK 17起步,同时把很多旧的API位置都换了。最典型的就是javax.*变成jakarta.*。如果你拿到的是SpringBoot 2.7的源码,里面的javax.servlet.Filter、javax.annotation.Resource等类,在SpringBoot 3下直接编译不过。还有,SpringBoot 3的自动配置类注册机制从spring.factories改成了AutoConfiguration.imports,一些老的自定义starter会在启动时静默失效。
此外,部分网上的源码是两三年以前的项目,用的还是SpringBoot 2.1甚至2.0。这时候如果你换成2.7,配置项的兼容问题也没那么大,但spring-boot-starter-parent版本升级可能会引入依赖冲突。我的建议是:
- 如果源码明确写了SpringBoot版本,就尽量保持作者用的版本,不要动
- 如果源码没写版本,优先选择2.7.x搭配JDK 1.8或11,这是绝大多数毕设源码默认的环境
- 如果老师要求必须用新版本,再考虑升级,并且做好解决
jakarta迁移的准备
一个判断技巧:看源码里pom.xml的<parent>部分,没有版本号的可能用了Maven的dependencyManagement,这时候看Effective POM(IDEA右侧Maven窗口里能看到)最终用的版本。
2.3 标准项目结构:拿到源码第一眼看哪里
拿到一套源码之后,不要急着启动,先看目录结构,这决定了你能不能快速定位核心代码。一套标准的SpringBoot分离式公寓出租管理系统,后端目录大概是这样的:
apartment-rent/ ├── src/main/java/com/apartment/rent/ │ ├── ApartmentRentApplication.java # 启动类 │ ├── controller/ # 接口层 │ │ ├── AdminController.java │ │ ├── RoomController.java │ │ ├── ContractController.java │ │ └── BillController.java │ ├── service/ # 业务逻辑层 │ │ ├── RoomService.java │ │ └── impl/RoomServiceImpl.java │ ├── mapper/ # 数据访问层 │ │ ├── RoomMapper.java │ │ └── xml/RoomMapper.xml │ ├── entity/ # 数据库实体 │ │ ├── Room.java │ │ ├── Tenant.java │ │ └── LeaseContract.java │ ├── config/ # 配置类 │ │ ├── CorsConfig.java │ │ ├── JwtInterceptorConfig.java │ │ └── MybatisPlusConfig.java │ ├── common/ # 统一返回封装 │ │ ├── Result.java │ │ └── GlobalExceptionHandler.java │ └── utils/ # 工具类 │ ├── JwtUtil.java │ └── UuidUtil.java └── src/main/resources/ ├── application.yml # 核心配置 ├── mapper/ # MyBatis XML文件目录 └── sql/ └── apartment_rent.sql # 数据库初始化脚本启动类通常是xxxApplication,里面有个main方法。配置文件在application.yml或application.properties,数据库连接、Redis配置(如果有)、JWT密钥都在这里。SQL脚本一般在resources/sql或项目根目录的db文件夹里——这是你第一步要检查的东西,后面我会细说。
前端如果是Vue工程,一般是另一个独立目录,里面有package.json,用npm install后执行npm run dev启动,默认端口通常是5173或8080。
3. 数据库设计与核心表结构:把"公寓出租"落到关系模型
3.1 核心表有哪些:这套系统的数据地基
数据库设计是整个毕设的基石,也是论文里最重要的一章。我记得自己当年答辩时,老师对着一张ER图问了十分钟,如果当时表关系理不清楚,肯定要挂在黑板上。公寓出租管理系统的核心表一般有7张左右:
user:系统用户表,管理员账号apartment_building:公寓楼栋表room:房间表tenant:租客表lease_contract:租赁合同表bill:租金账单表payment_record:缴费记录表
如果要做预约看房和维修工单的话,还可以加appointment和repair_order,但这两张不是核心,时间不够可以先不做。
表之间的关系不难:
- 一栋楼有多个房间,所以
apartment_building到room是一对多 - 一个租客可以多次租房,但一份合同在同一时间只对应一个租客和一个房间
- 一份合同会生成多个月度账单,所以
lease_contract到bill是一对多 - 一笔账单对应多次缴费记录(可能分期或者部分缴费),所以
bill到payment_record是一对多
我画ER图时习惯用Workbench导出,但在这里就用文字描述,你自己画的时候建议用draw.io,画完直接嵌到论文附录里。
3.2 关键字段与表设计要点
很多网上的源码表字段设计得特别随意,比如房间价格用double、时间用String,短期跑通没问题,但一到写论文或者答辩就会被老师抓到细节。我建议你按下面的规范检查一遍:
room表的关键字段:
id:主键,自增整数即可building_id:外键,关联楼栋表room_no:房间号,比如"2-301"area:面积,用decimal(5,1)rent:月租金,用decimal(10,2)status:状态,0空置、1已租、2维修中created_time:录入时间
lease_contract表的关键字段:
contract_no:合同编号,可以用"LC20240101"格式tenant_id:租客IDroom_id:房间IDstart_date:合同开始日期end_date:合同结束日期monthly_rent:月租金(快照值,避免后续改价影响历史合同)deposit:押金status:合同状态,0生效中、1已退租、2已到期
这里要注意,合同里必须存一份"月租金快照",因为房价可能会变。如果账单关联的是房间表的实时租金,涨价后退租时历史账单就乱了。
bill表的关键字段:
contract_id:关联合同period:账单月份,比如2024-01amount:应付金额due_date:缴费截止日期status:状态,0未缴、1已缴、2逾期paid_time:缴费时间
3.3 状态机与业务约束:这才是体现思考的地方
光建表还不够,你得把业务规则落到代码里。我总结了几条最关键的状态流转:
第一,房间的状态流转是"空置 -> 已租 -> 空置"。签合同时,要把房间状态从空置改已租;退租结算后,再把房间状态改回空置,同时把合同状态改成已退租。这段逻辑一定要放在同一个事务里,否则可能出现合同说已经签了,房间还显示空置。
第二,账单的生成不是手动的。可以在合同有效期内的每个月1号,定时任务扫描所有生效中的合同,为每个合同生成当月账单。如果你只是手动在页面上点"生成账单",那老师问"如果这个月没人点呢?"你就难解释了。所以建议用SpringBoot的@Scheduled定时任务,或者至少在Service里写好"批量生成账单"的方法。
第三,逾期罚金的计算规则要定义清楚。比如,"缴费截止日后每天按未缴金额的0.5%计罚金,封顶不超过账单金额"。这个规则可以写在服务端,每次查询账单时动态计算,也可以用定时任务写回数据库。我建议动态计算,不落库,这样改规则时不用动历史数据。
4. 核心功能实现:从登录鉴权到账单计算
4.1 登录鉴权:JWT还是要简单好用的方案
毕设项目里,我最推荐的做法是:把Spring Security放一边,用JWT加一个拦截器来实现登录鉴权。不是Spring Security不好,而是它配置复杂,学三天才能跑通一个最简单的登录,而毕设时间有限。JWT的方案短平快,代码量少,而且你能讲清楚它"无状态"的原理,老师反而觉得你懂。
核心逻辑是这样:用户登录成功后,服务端用密钥生成一个Token,里面存着userId和role,然后返回给前端。前端把Token放在每次请求的Authorization头里。后端写一个拦截器,拦截除了登录、注册之外的请求,校验Token是否合法。
一个简化后的JWT工具类长这样:
public class JwtUtil { private static final String SECRET = "apartment-rent-secret-key"; public static String generateToken(Integer userId, String role) { return Jwts.builder() .setSubject(String.valueOf(userId)) .claim("role", role) .setExpiration(new Date(System.currentTimeMillis() + 2 * 60 * 60 * 1000)) .signWith(SignatureAlgorithm.HS256, SECRET) .compact(); } public static Claims parseToken(String token) { return Jwts.parser() .setSigningKey(SECRET) .parseClaimsJws(token) .getBody(); } }然后写一个LoginInterceptor,在preHandle里取请求头:
@Component public class LoginInterceptor implements HandlerInterceptor { @Override public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception { String token = request.getHeader("Authorization"); if (token == null || !token.startsWith("Bearer ")) { response.setStatus(401); response.setContentType("application/json;charset=UTF-8"); response.getWriter().write("{\"code\":401,\"msg\":\"未登录\"}"); return false; } try { Claims claims = JwtUtil.parseToken(token.replace("Bearer ", "")); request.setAttribute("userId", claims.getSubject()); return true; } catch (Exception e) { response.setStatus(401); response.getWriter().write("{\"code\":401,\"msg\":\"登录已过期\"}"); return false; } } }记得在配置类里注册拦截器,并排除/ api/user/login和/api/user/register路径。这样一套下来,登录功能就完成了,而且代码短到答辩时可以直接逐行解释。
4.2 房源管理:动态条件查询的写法
列表页的筛选条件是公寓管理系统最典型的动态SQL场景:用户可能选城市,可能选价格区间,还可能选房间状态。用MyBatis-Plus的条件构造器写起来非常简单,而且比手写拼接SQL安全得多。
@Override public PageResult<RoomVO> pageRooms(Integer page, Integer size, RoomQuery query) { Page<Room> pageParam = new Page<>(page, size); LambdaQueryWrapper<Room> wrapper = new LambdaQueryWrapper<>(); if (StrUtil.isNotBlank(query.getCity())) { wrapper.eq(Room::getCity, query.getCity()); } if (query.getStatus() != null) { wrapper.eq(Room::getStatus, query.getStatus()); } if (query.getMinRent() != null) { wrapper.ge(Room::getRent, query.getMinRent()); } if (query.getMaxRent() != null) { wrapper.le(Room::getRent, query.getMaxRent()); } wrapper.orderByDesc(Room::getCreatedTime); Page<Room> result = roomMapper.selectPage(pageParam, wrapper); return convertToPageResult(result); }如果你拿到手的源码里,selectPage返回的total是0,或者列表翻不了页,那大概率是少了分页插件。这个问题我在下一章专门讲。
4.3 合同与账单:租金计算和逾期罚金
合同管理里最核心的一段逻辑是"退租结算"。它不是简单地把合同状态改成"已退租",而是要把最后一期的租金算清楚,并把押金处理逻辑走完。
比如租客在合同到期前10天退租,按照"未住满整月按实际天数折算"的规则,你需要计算:
BigDecimal actualDays = new BigDecimal(endDate.getDayOfMonth() - startDate.getDayOfMonth() + 1); BigDecimal monthDays = BigDecimal.valueOf(30); BigDecimal payAmount = monthlyRent.multiply(actualDays).divide(monthDays, 2, RoundingMode.HALF_UP);记得所有的金额计算都用BigDecimal,不要用double。这笔结算金额会生成一张退租账单,然后检查之前每月账单有没有欠款,一起汇总后,再扣除或退还押金。整个过程建议用@Transactional包裹,保证同一时间只能操作这一单,并且中途发生异常时数据能回滚。
月度账单的生成相对简单,合同生效期内每个月生成一条bill记录,金额就是合同的monthly_rent。到期日和逾期罚金的规则可以放在Service层的calculateOverdueFee()方法里。
4.4 数据可视化:用ECharts展示出租率与营收
很多毕设系统做完了统计页,其实就是把表格数据原样展示,没有图表。加上ECharts之后,视觉效果和功能完整度都会明显提升。后端只需要提供两个接口:
第一个是"出租率趋势":按月份统计每个月的已租房间数除以总房间数。SQL大概是:
select DATE_FORMAT(create_time, '%Y-%m') as month, sum(if(status = 1, 1, 0)) as rented_total, count(*) as total from room group by month order by month;第二个是"月度营收":统计每个月已缴账单的金额总和:
select date_format(paid_time, '%Y-%m') as month, sum(amount) as income from bill where status = 1 group by month order by month;前端拿到数据后,直接把option传给ECharts即可。答辩演示时,把图表展示出来,顺势讲一句"这部分的报表数据是实时从数据库聚合计算出来的,不是写死的",老师就会觉得你考虑到了实际运营需求。
5. 实操过程中最容易踩的六个坑:来自真实调试记录
光讲设计和代码还不够,我把自己拿到这类源码后跑项目时踩过的坑,还有帮学生调试时看到的问题,集中整理成六个典型场景。这些坑网上基本没有一份汇总,但几乎每个做这套毕设的人都会遇到。
5.1 SpringBoot版本太高导致配置文件不生效
有位同学下载的源码是SpringBoot 2.5,他本地IDEA里新建项目时默认选了3.0.0,然后直接把源码文件一股脑粘了进来。结果启动不报错,但访问页面时发现静态资源和模板路径全都乱了,application.yml里配的spring.thymeleaf.prefix也不生效。
排查到最后发现,SpringBoot 3使用了新的配置绑定机制和类路径扫描机制,@ConfigurationProperties等注解的包路径也换了。所以,我强烈建议拿到源码后,先看pom.xml里SpringBoot的版本,然后在你本地Maven仓库里确认是否存在对应版本,没有就通过修改Maven镜像源解决。不要图新鲜去升级。
5.2 MyBatis-Plus分页失效,total一直是0
这几乎是MyBatis-Plus项目的通病。常见表现是:列表能查出第一页数据,但total等于0,或者翻页时永远只有那一页。
原因很简单:没有配置分页插件。MyBatis-Plus的分页功能不是内置自动生效的,必须手动创建一个MybatisPlusInterceptor并添加PaginationInnerInterceptor。在任意一个@Configuration类里加上这段即可:
@Configuration public class MybatisPlusConfig { @Bean public MybatisPlusInterceptor mybatisPlusInterceptor() { MybatisPlusInterceptor interceptor = new MybatisPlusInterceptor(); interceptor.addInnerInterceptor(new PaginationInnerInterceptor(DbType.MYSQL)); return interceptor; } }加完之后重启服务,再查看total就有值了。这个问题在答辩时也经常被老师拿来问,你要是现场答不上来就很尴尬。
5.3 时间字段的时区问题:早上8点的坑
数据库里面存的时间比页面显示的时间慢了8个小时,或者反过来,页面传的"2024-01-01 00:00:00"存到库里变成了"2024-01-01 08:00:00"。这就是时区没配好。
解决办法是在application.yml的数据库连接URL上加上参数:
spring: datasource: url: jdbc:mysql://localhost:3306/apartment_rent?useUnicode=true&characterEncoding=utf8&serverTimezone=Asia/Shanghai同时,如果实体里的时间是LocalDateTime,JSON序列化时建议加一个全局格式配置,避免前端收到的是数组格式:
spring: jackson: date-format: yyyy-MM-dd HH:mm:ss time-zone: GMT+85.4 金额精度问题:double算钱出现一堆小数
我见过有人把rent字段设为double,然后在计算一个月的剩余租金时出现1234.9999999998,这是几乎所有用浮点数算钱的程序都会踩的坑。必须统一使用BigDecimal。数据库里用decimal(10,2),Java实体里用BigDecimal,运算时调用add、subtract、multiply、divide方法,并指定舍入模式。
还有,从前端接收金额参数时,也建议用BigDecimal类型接收,而不是String或Double,否则精度会在入口就丢了。
5.5 前后端分离部署时的跨域问题
前端Vue跑在localhost:8080,后端SpringBoot跑在localhost:8081,你直接在前端页面上调用接口,浏览器会报CORS错误。解决方式有两种,我推荐在后端做一个全局CORS配置:
@Configuration public class CorsConfig implements WebMvcConfigurer { @Override public void addCorsMappings(CorsRegistry registry) { registry.addMapping("/**") .allowedOriginPatterns("*") .allowedMethods("GET", "POST", "PUT", "DELETE", "OPTIONS") .allowedHeaders("*") .allowCredentials(true) .maxAge(3600); } }这段配置放在后端,比前端代理省心,而且答辩时老师如果问"为什么接口能跨域",你就可以理直气壮地说"后端统一做了跨域策略"。
5.6 源码常见隐患:SQL脚本缺失、密码明文、路径写死
网上不少源码,下载下来才发现没有SQL初始化脚本,或者脚本里只有空表结构没有演示数据,甚至管理员密码直接写死成123456。遇到这种情况,第一步不要慌,先补数据。
我会准备一个demo.sql,里面包含:
- 1个管理员账号(密码用BCrypt加密后的字符串,如果你不知道怎么生成,可以用SpringBoot的
BCryptPasswordEncoder临时写一个main方法跑一遍) - 2栋楼、10个房间,覆盖空置/已租/维修三态
- 3个租客、2份生效合同、1份已退租合同
- 12条月度账单,其中包含已缴、未缴、逾期三种状态
有了这些演示数据,你答辩时演示任何一个页面都不至于空荡荡。还有的源码里把文件上传路径写成D:/upload/...,这种绝对路径在别人电脑上一定报错,记得改成相对路径或通过配置项注入。
6. 如何把毕设源码跑起来:从导入到验收全流程
6.1 运行前检查三件套
如果你下载的是别人分享的源码包,拿到手先别急着点启动按钮。我建议你按下面的顺序检查三样东西:
第一,环境版本。打开IDEA的File -> Project Structure,确认Project SDK是不是JDK 8或11,Maven用的是不是3.6以上。不合要求就先调整,不然各种依赖下载慢和编译报错会折磨你一下午。
第二,依赖是否完整。打开Maven面板,点Reload All Maven Projects,观察有没有红色报错。如果某个依赖下载不了,去阿里的镜像仓库配置看看,settings.xml里把mirrorOf设成central,大部分都能解决。
第三,配置文件。检查application.yml中的数据源地址、数据库名、账号密码是否和你本机一致。这步是源码跑不通的最大元凶,没有之一。
6.2 数据库初始化和演示数据导入
如果源码包里带了.sql文件,就用Navicat或命令行工具执行。执行之前先确认这个SQL脚本使用的是MySQL语法还是Oracle语法——有些源码明明写着MySQL,脚本里却用了NUMBER之类的Oracle类型,执行到一半就会报错。
如果没有SQL脚本,你就得手动建库建表了。建库时注意字符集统一用utf8mb4,否则中文会乱码。
执行完脚本后,打开user表,看一眼管理员账号是否存在。如果表是空的,你就需要自己往里插一条,密码记得先加密。部分源码用的加密方式可能是MD5加盐,也可能是BCrypt,你要跟代码里验证密码的方式对应上。
6.3 本地启动与接口自测
启动后端SpringBoot工程,看到日志输出Started ApartmentRentApplication或者Tomcat started on port(s): 8081就说明成功了。接下来不要急着打开前端页面,先用接口测试工具验证后端核心接口。
用Postman或Apifox测试登录接口,比如POST /api/user/login,请求体是:
{ "username": "admin", "password": "123456" }返回结果中如果有token字段,说明登录逻辑正常。把token复制到请求头里,再调用一个需要鉴权的接口,比如GET /api/room/list,能拿到房间列表,说明JWT拦截器也正常。
如果前端Vue也跑起来了,打开浏览器控制台发现请求403或401,回头检查跨域配置和Token的前缀格式。大多数前后端对接失败都是"Bearer "这个前缀在请求头里被写错了。
6.4 答辩演示动线:5分钟怎么讲才能拿高分
系统能跑起来只是及格,答辩才是你的决胜场。我建议你准备一个完整的演示脚本,按业务流程走,不要按菜单走。比如这样:
演示前,先讲清楚你做了什么:管理员登录后,先展示"公寓总览"页面,里面有出租率、月营收、房源分布三张图表,让老师一上来就看到系统能干什么。然后进入"楼栋管理",点击新增一栋楼,新增完顺便展示数据已经生效。
接下来进入"房间管理",按"空置"筛选出一批房间,然后选择其中一个演示"录入租客并签订合同"。在创建合同这一步,把起止日期设置成当月,保存后立即去"账单管理"里刷新,让老师看到系统自动生成了这个月的账单。最后一键点击"收款",回到统计页面,看到营收金额发生了变化。
整个流程不超过5分钟,但把新增、查询、业务联动、计算统计都覆盖了。答辩时老师最常问的问题也提前准备一下:
- "合同快到期了怎么办?"——可以答:通过定时任务生成到期提醒或自动更新状态
- "逾期罚金怎么算?"——把前面提的动态计算方法复述一遍
- "你的密码安全吗?"——答:使用了哈希加密,不是明文
这套系统的难度刚好卡在"能做出来,又能讲清楚"的位置。我个人实际操作下来的感受是,它不像电商系统那样要处理订单库存这种复杂状态,也不像纯OA系统那样只做增删改查没有一点业务含量。如果你能在以上几个细节上多用点心,把状态流转、账单计算、定时任务、统计报表这些点吃透,你不仅能顺利通过答辩,还能在后续面试写项目时,多一个真实可聊的业务场景。
最后再分享一个小技巧:做完之后,把你的源码从本地Maven仓库路径中清理一遍,确保别人下载后不需要额外配置本地依赖路径就能跑起来。然后把README写好,写清楚环境版本、数据库初始化方式、默认账号密码。这份源码不论是自己留档,还是分享给学弟学妹,都会是一个很完整的毕设作品。