ARTICLE · INTELLIGENCE

战地情报 · 详情页

来自尧图项目组的一线实战观察与深度解析

基于Vue与Spring Boot的校园房屋租赁系统设计与实现

基于Vue与Spring Boot的校园房屋租赁系统设计与实现 每年五六月份毕业生离校季一到校园周边的租房需求就开始集中爆发。做vue基于spring boot的校园高校毕业生房屋租赁这套系统最初就是为了解决一个很现实的问题毕业生找房靠中介、看房靠电话、签约靠纸质合同、入住后报修靠缘分。它覆盖了房源发布、预约看房、电子合同、报修工单四个核心环节是一套完整的前后端分离应用。这篇博文把整个应用从需求梳理、架构设计到模块实现、线上排错的思路都过一遍适合正在做毕设、想系统练一遍Vue和Spring Boot联调、或者需要一个全栈项目撑简历的朋友参考。如果你只想抄个CRUD模板交差也能省下不少弯路。1. 从毕业租房难倒推出来的项目需求1.1 校园毕业租房的核心痛点拆解做系统之前先别急着建表、写接口要把业务场景里真正让人头疼的问题列清楚。校园毕业生租房和普通社会租房最大的不同在于这个人群几乎没有租房经验而且信任半径非常小。我梳理下来有四类痛点信息不对称校园周边的房源大多掌握在中介手里刚出校门的学生很难辨别真实房源和虚假房源。很多学生只能在QQ群、贴吧、校园告示栏里碰运气信息碎片化严重。时间成本高约看房全靠电话或微信反复沟通房东和学生的时间经常对不上要么学生等房东要么房东等学生一个周末最多跑两三家。合同风险大校园周边租房口头约定多、条款模糊押金怎么退、水电怎么算、房屋损坏谁负责全凭一张嘴。真出了纠纷学生基本是弱势方。入住后售后缺失灯泡坏了、马桶堵了、热水器不热学生不知道找谁房东可能在外地拖半个月没人处理。所以这个项目的定位不是做成链家那种大平台而是做一个校内可信的信息与管理平台。管理员审核房源、学生实名认证平台自建预约、合同、报修流程把找房-看房-签约-入住-售后串成一条完整的服务链。这个信任链才是区别于普通租房App的核心价值。1.2 为什么选Vue Spring Boot这套组合技术选型不是越新越好也不是越复杂越好而是要匹配项目的交付场景。这个项目最终确定用Vue做前端、Spring Boot做后端、MySQL存数据、MyBatis-Plus操作数据库理由很朴素Vue上手快、生态完整vue-router管路由、Pinia或Vuex管状态、Element Plus直接拖组件搭管理后台对个人开发者极其友好。社区教程多遇到问题搜一下就有答案。Spring Boot约定优于配置内嵌Tomcat一个jar包就能跑starter机制把Spring MVC、Jackson、数据源这些基础配置都自动化了能把精力放在业务逻辑而不是配置文件的堆砌上。前后端分离是当下主流企业项目基本都是这个形态毕设答辩时讲前端Vue通过Axios调用后端RESTful接口面试官一听就懂也方便展示你的全栈能力。替代方案对比有朋友用Thymeleaf做服务端渲染虽然代码量更少但前后端耦合严重展示效果也不如单页应用。还有用Django Vue的也可以但Spring Boot在Java生态里的就业面和案例量明显更大。1.3 功能模块的价值排序一个毕设项目最忌讳什么都想做什么都没做透。我在梳理需求时按业务闭环和价值大小排了优先级核心业务必须做扎实房源管理、预约看房、合同管理、报修管理。这四个模块构成了租前-租中-租后的完整闭环。支撑能力保证能用用户注册登录、角色权限学生/房东/管理员、个人中心、公告管理。没有这些系统根本跑不起来。迭代方向有余力再做地图找房、房源收藏、租金流水、站内消息。这些属于锦上添花不会写也不影响主流程。这套排序在后面写代码、写论文时很有用核心模块每个都能单独开一章讲支撑能力几句话带过就行。论文和答辩PPT的章节甚至可以直接照这个顺序组织。2. 系统架构设计前后端分离怎么搭才不乱2.1 后端工程结构与四层架构落地Spring Boot项目最常用的组织方式是四层架构Controller层接收HTTP请求、Service层写业务逻辑、Mapper层做数据持久化、Entity层映射数据库表。如果项目只是把所有代码堆在Controller里前期跑得爽后期改一个预约状态流转能让你怀疑人生。我实际用的包结构是这样com.example.rent ├── controller // 接收请求参数校验 ├── service // 业务逻辑事务管理 ├── mapper // MyBatis-Plus接口 ├── entity // 数据库实体 ├── dto // 接收前端参数的传输对象 ├── vo // 返回给前端的视图对象 ├── config // 跨域、拦截器、JWT配置 ├── common // 统一返回Result、异常处理、常量 └── utils // 工具类这里有个小建议前端传参不要直接用Entity接。比如预约接口只需要houseId、appointmentTime、remark三个字段但你用Appointment实体接收时前端随意传个id或status进来都可能被绑定。用DTO接收然后手动转成Entity既安全又清晰。Spring Boot版本选择方面如果你电脑装的是JDK8老老实实用Spring Boot 2.7.x如果已经升级到JDK17可以直接上Spring Boot 3.x。3.x之后javax包名改成了jakarta很多老教程的import语句会报红这个心理要有数。Spring Boot 4.0虽然已经出现但生态刚起步毕设别当小白鼠。2.2 前端Vue项目的目录组织与路由设计创建前端项目时我习惯用Vite而不是Vue CLI因为Vite冷启动快太多开发体验好。跑一下命令npm create vitelatest frontend -- --template vue前端目录结构我组织成src ├── api // 所有接口请求按模块拆分文件 ├── assets // 静态资源 ├── components // 公共组件上传、分页、步骤条等 ├── router // 路由配置 ├── store // Pinia状态管理 ├── utils // 请求封装、token处理 └── views // 页面组件 ├── house // 房源模块 ├── appointment // 预约看房模块 ├── contract // 合同模块 └── repair // 报修模块vue-router配置时后端管理页面尽量用懒加载const routes [ { path: /house/:id, name: HouseDetail, component: () import(../views/house/HouseDetail.vue) } ]这里有个前端新手必踩的坑路由参数用queryURL上显式?id1还是params路径占位符/house/:id。如果是跳转详情页我推荐用params 动态路由刷新页面参数不丢URL也更干净。如果只是列表页传筛选条件用query更合适因为刷新后筛选条件还能保留在URL里。Axios封装也要提前做好。统一设置baseURL在请求拦截器里把登录后存的token塞进Header在响应拦截器里统一处理401跳登录、200以外的错误提示。这样业务代码里不用每个接口都写一遍错误处理代码量能少三分之一。2.3 数据模型设计六张核心表的关联关系数据库设计是这类系统的地基。我最终落地的核心表有六张字段不求多但关键状态位和关联字段不能省表名核心字段说明userusername, password, phone, role, student_norole区分student/landlord/adminhouselandlord_id, title, address, area, price, statusstatus用于上下架、已租appointmenthouse_id, student_id, appoint_time, statusstatus存预约状态contracthouse_id, student_id, landlord_id, start_date, end_date, statusstatus存合同生命周期repairhouse_id, user_id, type, description, image, statusstatus存报修进度noticetitle, content, create_time平台公告表之间的关系用逻辑外键不建数据库物理外键。原因很简单物理外键虽然保证了完整性但后期做分表、迁移数据时极其痛苦而且MyBatis-Plus做联表查询更灵活。所谓逻辑外键就是在service层自己校验user_id对应的用户必须存在避免脏数据。还有一个细节状态字段我统一用tinyint存数字0/1/2而不是直接存字符串待确认已确认。数字做条件查询快占空间小而且状态枚举值可以写进常量类前端配合字典翻译显示中文。注意每张表都加上create_time和update_time后面排查问题的时间线全靠它。3. 核心业务模块的实现逻辑从预约看房到合同报修的闭环3.1 房源发布与检索筛选背后的SQL细节房源模块是门面前端展示效果直接影响系统观感。房东发布房源时除了标题、描述、地址、面积、价格这些基础字段图片上传是业务里第一个技术含量点。图片处理我用MultipartFile接收上传到本地磁盘指定目录然后通过一个映射URL访问。生产环境推荐走OSS但毕设项目本地存储nginx映射完全够用。房源列表页的筛选条件一般有区域、价格区间、面积区间、排序方式。用MyBatis-Plus的LambdaQueryWrapper写查询很舒服LambdaQueryWrapperHouse wrapper new LambdaQueryWrapper(); wrapper.eq(StringUtils.isNotBlank(area), House::getAddress, area) .between(minPrice ! null, House::getPrice, minPrice, maxPrice) .eq(House::getStatus, HouseStatus.PUBLISHED) .orderByDesc(House::getCreateTime);注意每个条件都带上判断前端没传对应参数时这个条件就不拼进去。分页用MyBatis-Plus内置的Page对象前端Element Plus的el-pagination可以直接对接total和records字段。搜索框是个容易被忽略的性能点。简单实现可以用like %关键字%但用户输入一长串表数据一大查询就肉眼可见地慢。我的做法是先查标题再查地址两个字段用OR连接并且给这两个字段建立联合索引。只要房源量在十万以内这个方案完全够用不用上Elasticsearch那种重武器。3.2 预约看房的业务流程状态机设计预约看房是整个系统里业务逻辑最值得拿出来讲的部分。如果只是做个提交预约-展示列表那项目就真的只是CRUD了。要把这个模块做出亮点重点在于状态流转的设计。我把预约状态定义为五种0待确认 - 1已确认 - 2已完成 0待确认 - 3已取消 1已确认 - 3已取消 1已确认 - 2已完成用状态机的方式管理核心好处是防止乱序流转。比如学生刚提交预约状态0理论上不允许直接变成已完成状态2中间必须经过房东确认状态1。在Service层写一个updateStatus方法里面用switch判断当前状态和目标状态是否合法不合法直接抛业务异常。这里还有一个关键业务校验同一套房源同一时间段不能被重复预约。学生选了今天下午3点另一个学生也选了同一时间房东不可能同时接待。实现时在Service层查重long count appointmentMapper.selectCount( new LambdaQueryWrapperAppointment() .eq(Appointment::getHouseId, houseId) .eq(Appointment::getAppointTime, appointTime) .in(Appointment::getStatus, Arrays.asList(0, 1)) ); if (count 0) { throw new BusinessException(该时段已被预约请选择其他时间); }这里把状态0和状态1都纳入冲突范围因为待确认和已确认都意味着这个时间段被占用了。查重逻辑写在事务方法里配合数据库行锁并发情况下也能保证不会两个人同时约上。预约时间过期自动取消我用的一个很轻量的方案查询时每次先执行一条UPDATE appointment SET status 3 WHERE appoint_time NOW() AND status IN (0, 1)相当于懒清理。不需要引入定时任务框架对毕设来说简单可靠。3.3 合同生成与履约从签署到生效的生命周期合同是租赁双方最关心的法律文件。这个模块的核心不是做一个合同编辑器而是把合同的生成、确认、归档流程固化下来。我实现的方案是学生确认租房意向后系统根据房源信息和租期自动生成一条合同草稿。合同编号规则是HT yyyyMMdd 四位随机数比如HT20240510xxxx保证唯一且可从编号看出签订日期。合同内容包括租期起止、月租金、押金、租赁双方信息、房屋地址等前端以只读表单展示学生确认无误后点击确认签署状态从待签署变为生效中。合同状态管理这步有讲究状态值含义触发条件0待签署系统生成草稿1生效中学生确认签署2已到期结束日期小于当前日期3已解约房东或管理员手动操作已到期这个状态不必依赖定时任务查询时根据end_date动态计算也行。但如果合同列表页需要显示状态建议还是在每日首次访问时批量更新一次避免每条记录都现场算。对于想增加技术亮点的同学可以试试在合同模块集成PDF导出。用itext库把合同内容渲染成PDF文件存入服务器方便学生下载打印。这一步对答辩非常加分因为评委能直观看到数据落盘的效果而不只是数据库里多了一条记录。3.4 报修工单从提交到闭环的消息驱动报修模块看似简单其实是整个系统里用户使用频率最高的功能。它的核心价值是让报修有人管这件事变得可见、可追踪。学生提交报修时选择报修类型水电/门窗/家电/其他填写描述可以上传照片。这里的图片上传走的是和房源图片同一套接口建议把上传功能封装成一个公共组件避免每个页面都复制一遍。工单流转我用四个状态0待处理 - 1处理中 - 2已完成 0待处理 - 3已驳回房东登录后看到待处理的工单点接单变成处理中处理完填一下处理结果提交后变成已完成。学生端可以实时看到工单进度如果觉得处理结果不满意可以重新发起报修。技术实现上我用了一个操作日志表repair_log来记录每次状态变更的操作人、时间和备注。这样学生投诉为什么没人管我的报修时管理员一查日志就知道卡在哪个环节、是谁的锅。这个设计在答辩时非常加分因为很多学生做报修模块就是一个update status完事根本不考虑可追溯性。如果你想让报修模块更有实时感可以用WebSocket给房东推送有新报修单的站内通知或者用更简单的方式——前端每30秒轮询一次待处理数量。考虑到后端接口和性能轮询在毕设场景下完全够用WebSocket可以作为加分项做在原型的进阶版本里。4. 联调与部署阶段踩过的坑完整排查链路4.1 跨域问题前后端联调的第一道坎前后端分离项目第一个必踩坑就是跨域。现象是前端Axios发请求浏览器控制台报CORS error后端日志却一条请求记录都没有。我的排查链路是这样的打开浏览器开发者工具Network看请求有没有发出预检请求OPTIONS是否正常返回。如果OPTIONS都失败了说明后端根本没处理跨域去后端加CORS配置。如果OPTIONS成功但POST失败大概率是后端接口本身的报错或者Header没带够。后端的标准解决方案是写一个CorsConfig类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); } }但这里有个隐藏坑如果你写了Spring MVC拦截器比如JWT鉴权拦截器OPTIONS预检请求会被拦截器拦下来直接返回401。所以拦截器里面要对OPTIONS请求放行if (OPTIONS.equals(request.getMethod())) { return true; }否则你会发现CORS配置明明写对了预检请求还是报错。这个问题排查起来很隐蔽光看后端代码根本看不出毛病必须打日志或者debug看拦截器执行逻辑。生产环境部署时我更推荐用nginx把前端静态资源和后端接口代理到同一个域名下从根源上消除跨域但本地联调阶段用CorsConfig最方便。4.2 Vue打包后布局异常的具体处理开发环境一切正常一执行npm run build部署到服务器后页面打开一片混乱图片不显示、字体丢失、样式全飞这是Vue项目最常见的问题之一。根因通常是构建资源路径配成了绝对路径。默认情况下Vue打包后index.html里引用的JS和CSS路径是/assets/xxx.js如果部署在服务器根目录没问题但部署到子路径比如http://ip:8080/rent/就会全部404。解决方案在vue.config.js里设置module.exports { publicPath: ./ }设置成相对路径后资源引用会是./assets/xxx.js无论部署到哪个子路径都能正确加载。但注意publicPath: ./会影响路由中的createWebHistory模式导致刷新页面时404。这个跟部署模式有关使用createWebHashHistoryURL带#号刷新不404但URL不美观。使用createWebHistory刷新会404需要nginx配置try_files $uri $uri/ /index.html;。我的建议是毕设项目统一用Hash模式少踩一个坑截图展示时URL里的#号也不影响观感。如果坚持用History模式一定要在nginx里配置好兜底跳转。4.3 Spring Boot版本和Java版本兼容问题现在很多人一上来就下载Spring Boot最新版结果本地Java版本不对项目启动时报UnsupportedClassVersionError: org/springframework/boot/... has been compiled by a more recent version of the Java Runtime这个报错的含义就是Spring Boot编译用的Java版本比你本地运行时高。Spring Boot 3.x最低要求Java 17如果你电脑只装了JDK8必须用Spring Boot 2.7.x系列。同理如果你已经用了JDK17就别选Spring Boot 2.4那种老版本部分依赖也会不兼容。我的建议是以你本地的Java版本为基准反推Spring Boot版本而不是反过来。在pom.xml里写版本号时顺带把java.version标签也配上properties java.version17/java.version spring-boot.version3.2.4/spring-boot.version /properties这样才能保证打包出来的jar在目标环境能跑。另外如果项目里引入了spring-boot-starter-actuator一定要小心未授权访问问题。Actuator是Spring Boot提供的运维监控端点默认会暴露/actuator/env、/actuator/beans、/actuator/heapdump等敏感信息如果部署到公网又没配权限等于把应用内部结构、配置密码、甚至内存快照直接放网上。我排查过一个朋友的服务器就是被扫描工具扫到heapdump数据全被脱走了。安全的做法是至少限制生产环境只开放health和info端点management: endpoints: web: exposure: include: health,info4.4 时间字段传输的时区与格式问题前后端联调时时间字段也是重灾区。前端提交2024-05-10 14:00:00后端收到的却变成2024-05-10 06:00:00相差8小时。八成的可能是Jackson反序列化时按UTC时区处理了。解决方案在application.yml里统一配置spring: jackson: date-format: yyyy-MM-dd HH:mm:ss time-zone: GMT8同时实体类里的时间字段用LocalDateTime不要用Date。LocalDateTime配合Jackson的JSR310模块能更好地处理格式问题而且作为Java 8后的新时间API从头就是为这些场景设计的。前端那边我建议统一用dayjs做格式化再传字符串避免把JavaScript的Date对象直接丢给后端造成序列化歧义。还有一个小坑数据库连接串上也要加时区参数否则同样的时间数据Java读出来和MySQL存进去可能差8小时jdbc:mysql://localhost:3306/rent?serverTimezoneAsia/ShanghaiuseUnicodetruecharacterEncodingutf8这三处Jackson配置、实体类型、JDBC时区只要有一处没统一时间就可能错乱。排查的时候从数据库原始存储值开始看先确认是存的时候就错了还是读出时错了定位就快很多。5. 项目复盘这套系统还能怎么演进5.1 从毕设到简历项目的三个价值点项目做完只是一半另一半是怎么把工作量讲出来。很多同学简历上写实现了房屋租赁管理系统面试官看到的就是一个平平无奇的CRUD项目。同样是这个系统换个写法效果完全不同平庸写法有亮点的写法实现了预约看房功能设计并实现了基于状态机的预约看房流程定义了五态流转模型通过查重校验与事务机制解决并发重复预约问题实现了合同管理实现合同草稿自动生成、编号规则设计与PDF导出归档覆盖合同全生命周期管理实现了报修功能基于状态机模型实现报修工单闭环流转通过操作日志表实现全程可追溯使用Vue Spring Boot开发前后端分离架构JWT无状态鉴权统一异常处理与响应体封装前端基于Vue3 Vite Pinia构建这四行改动面试官能看到的深度完全不一样。写简历和写论文时每句话都要往我解决了什么实际问题上靠而不是我用了什么技术。5.2 可扩展方向同一套骨架还能长出什么功能做完这个系统后你会发现用户管理角色权限状态机工单追踪这套骨架几乎可以复用到任何校园场景。我列几个实际可行的扩展方向在线支付接入支付宝沙箱环境实现租金在线缴纳、押金原路退回打通签约-付款闭环。地图找房集成腾讯地图或高德地图API房源打点展示按距离筛选通勤范围前端组件有现成的vue-amap或vue-leaflet可用。消息推送用WebSocket实现站内信和预约提醒房东接单、看房前一天自动提醒双方解决信息滞后问题。智能推荐根据学生的预算、区域偏好、历史浏览记录做房源推荐这个可以作为论文的创新点章节。移动端适配用uniapp或移动端H5把现有Vue页面包一层生成小程序版适合讲多端一体化方案。这些方向不需要推翻现有代码都是在现有模块上加接口、加页面、加状态的方法对毕设延展和后续学习都值得投入。最后说一点个人体会做这类前后端分离项目最大的成本不在写代码而在需求拆解和联调排错。预约状态机画清楚、数据表字段定明白、跨域和时区问题提前预防开发效率能翻一倍。我自己做完这套系统后最大的感受是那些所谓的技术难点其实都是业务规则没有先用语言表达清楚导致的——状态机一画出来代码就顺了。如果你正准备做类似项目建议动手前先花一晚上把业务流程和状态流转图画出来磨刀不误砍柴工。
RELATED READING

延伸阅读

更多一线实战笔记与深度复盘,助您持续精进