
说实话当初看到“基于SSMVUE的智能租房网站”这个题目的时候我第一反应是又是一个毕业设计标配题。SSMSpringSpringMVCMyBatis负责后端VUE负责前端业务选租房这个人人都有感知的场景既能展示增删改查又能塞进去“智能搜索”“推荐房源”这些加分项。真把这个项目从零做完你才会发现代码量其实不大真正耗时间的反而是环境打架、跨域、打包、路由刷新404这些东西。这篇笔记就按我自己做这套毕业设计的完整过程来写尽量把能避开的坑都标出来希望能帮到正在开机写代码的你。1. 选题动机与功能边界这个“智能租房网站”到底要做成什么样1.1 为什么租房业务适合做成SSMVUE毕业设计很多同学选题目的时候喜欢跟着热度走今天想做商城明天想做社交平台最后把自己绕晕。租房这个业务最大的优势在于业务链路短、角色清晰、数据关系容易表达。一个用户上来要么是租客要么是房东管理员负责审核和统计。一条核心业务线就是“房东发房源→租客找房源→提交租房申请→生成租赁订单”每一步都是一个标准CRUD但又不是简单的单表增删改查正好能把SSM的Mapper层、Service层、Controller层都体现出来。同时这个题目天然包含“搜索”和“推荐”这两个点可以名正言顺地叫它“智能租房网站”。这里的“智能”不需要做成什么深度学习模型而是靠关键词匹配、条件筛选和偏好推荐来实现这也是毕设答辩时最常被问到的点“你的智能体现在哪里”如果一开始就把智能定义为“多条件组合搜索基于浏览历史的房源推荐”后面写论文也好做演示也好都能说圆。1.2 核心用户角色与功能模块拆解我最终定的角色是三个游客、登录用户租客和房东共用一张用户表通过字段区分身份、管理员。这里不建议把租客和房东拆成两张表否则登录、注册、会话管理都要写两套纯属给自己加工作量。角色核心功能游客浏览房源列表、查看房源详情、关键字搜索登录用户租客收藏房源、提交租房申请、查看我的订单、维护个人信息登录用户房东发布房源、编辑/上下架房源、处理租房申请、确认成交订单管理员用户管理、房源审核、订单管理、数据统计房源数/订单数/用户数前端页面我并列了四个一级入口首页、房源列表、个人中心、后台管理。首页放搜索框和推荐房源房源列表页支持按区域、户型、价格区间筛选个人中心区分租客视角和房东视角后台管理只对管理员开放。这里有一个很关键的取舍不要给游客开放发布房源和提交订单的权限。没有登录就让人操作后面一定会冒出各种空指针和安全漏洞。所以我的做法是所有敏感接口都要求携带登录凭证前端路由也做了守卫这个会在后面的权限章节细说。2. SSM后端整合的关键取舍Spring版本、MyBatis映射和接口设计2.1 环境选型里的坑JDK、Maven、Tomcat版本匹配很多人在SSM项目上一开始就翻车不是因为代码写错而是版本组合太随意。我最初用的是传统方式Spring 5.x SpringMVC 5.x MyBatis 3.x Maven webapp部署到Tomcat 9。这套组合在老师机器上能跑换到自己电脑就报各种ClassNotFoundException。这里我建议直接用Spring Boot 2.7.x作为载体来整合SSM三件套。别觉得“SSM”必须手写一堆XML才算正宗Spring Boot内部还是SpringMVC MyBatis那一套只是把Bean配置和依赖管理简化了。对于毕业设计来说用Spring Boot做SSM属于常规操作答辩时你只要说“底层还是Spring、SpringMVC、MyBatis只是通过自动配置减少样板代码”老师基本都认可。我实测比较稳的组合是组件版本JDK1.8不要一上来就JDK 17除非你非常熟悉Spring Boot2.7.18MyBatis / mybatis-spring-boot-starter2.3.xMySQL5.7 或 8.0Maven3.8.xNode.jsVue侧16 LTS 或 18 LTSVue CLI4.x 或 5.xJDK 1.8 Spring Boot 2.7 是毕业设计里最不容易出意外的一组搭配。Spring Boot 3.x 要求JDK 17虽然新但很多旧教程里的依赖和配置都失效了不值得为了尝鲜浪费答辩前的时间。2.2 常用注解在SSM里到底怎么配从Controller到AutowiredSSM项目里注解是最容易记混的。我用自己项目的几个核心位置把注解涉及的链路理一遍。Controller层RestController RequestMapping(/api/house) public class HouseController { Autowired private HouseService houseService; GetMapping(/list) public Result page(HouseQuery query) { PageResultHouse page houseService.pageQuery(query); return Result.success(page); } PostMapping(/publish) public Result publish(RequestBody House house, RequestAttribute(userId) Long userId) { house.setUserId(userId); houseService.publish(house); return Result.success(null); } }Service层Service Transactional(rollbackFor Exception.class) public class HouseServiceImpl implements HouseService { Autowired private HouseMapper houseMapper; }Mapper层用Mapper标注或者在启动类上直接加MapperScan(com.example.rent.mapper)二选一即可。我最开始两个都加了导致MyBatis扫描器总是提示重复创建Mapper属于无害但是很烦的配置问题。需要注意的点是Spring Boot整合SSM之后事务管理只需要在Service方法上加Transactional不需要再像传统Spring那样塞一堆tx:annotation-driven。如果没有加这个注解发布房源时先插入房源表、再插入图片表第二步失败后房源数据就残留了数据库里会出现一堆半截数据。2.3 接口幂等与参数校验别让你的增删改查显得太“玩具”毕业设计里很多接口犯同一个毛病数据校验全靠前端后端不做任何防御。比如提交租房申请时前端把houseId传过来后端直接 insert这样如果用户用Postman连续提交两次就会产生两条重复申请。我的处理是使用ValidatedNotNull等注解做基础参数校验具体校验类放到house/dto包下而不是直接复用实体类。提交租房申请前先查一次“同一用户、同一房源、订单状态为待处理”的记录如果已存在直接返回“请勿重复申请”。所有Controller统一返回Result对象包含code、msg、data三个字段。Result的写法很简单public class Result { private Integer code; private String msg; private Object data; public static Result success(Object data) { Result r new Result(); r.code 200; r.msg success; r.data data; return r; } public static Result error(Integer code, String msg) { Result r new Result(); r.code code; r.msg msg; return r; } }前端axios统一判断code只要不是200就抛异常并提示后端返回的msg这样比前端自己写一堆if/else判断错误码要省事得多。3. Vue前端从零搭到可用路由、插槽、组件通信与代理3.1 vue create 之后第一件事目录结构和路由配置我用Vue CLI创建项目选的是Vue Router和Vuex没有用TypeScript因为毕业设计不需要给自己增加额外难度。项目结构大致如下src ├── api │ ├── house.js │ ├── order.js │ └── user.js ├── assets ├── components │ ├── HouseCard.vue │ ├── EmptyPage.vue │ └── SearchBar.vue ├── router │ └── index.js ├── store │ └── modules │ └── user.js ├── utils │ └── request.js ├── views │ ├── Home.vue │ ├── HouseList.vue │ ├── HouseDetail.vue │ ├── Login.vue │ └── UserCenter │ ├── UserIndex.vue │ └── UserOrders.vue └── App.vue路由配置里有一个容易忽略的坑页面刷新后路由回退出现404。Vue Router默认是hash模式打包部署后确实没有刷新问题但URL里会多一个#看着不太专业。如果换成history模式部署到服务器后就需要后端配合做跳转。开发阶段我直接用hash模式部署的时候再用history 后端转发规则具体在后面的部署章节讲。3.2 组件复用与插槽使用场景热搜词里很多人在搜“vue插槽”其实在租房项目里插槽非常好用。我举两个实际场景。第一个是房源卡片组件HouseCard.vue。列表页和首页推荐位都要展示房源卡片但首页的卡片可能多一个“推荐”标签列表页的卡片可能多一个“查看详情”按钮。如果通过一堆props来控制组件会越写越臃肿。用插槽就很干净template div classhouse-card img :srchouse.coverUrl altcover / div classinfo h4{{ house.title }}/h4 p{{ house.address }}/p slot nameextra :househouse/slot /div /div /template首页使用时HouseCard v-foritem in recommendList :houseitem template #extraslotProps el-tag typedanger推荐/el-tag /template /HouseCard列表页使用时HouseCard v-foritem in houseList :houseitem template #extraslotProps el-button sizesmall clickgoDetail(slotProps.house.id)查看详情/el-button /template /HouseCard第二个是空状态插槽。搜索没有结果时页面应该显示一个“没有找到合适的房源”。我封装了EmptyPage.vue默认显示一张插画和一行文字同时保留插槽方便在订单页“暂无订单”时额外放一个“去逛逛”按钮。3.3 封装axios请求和跨域代理调试时最省心的方式前端和后端开发时最大的矛盾是端口不一致。Vue开发服务器默认跑在8080Spring Boot跑在8081如果不做跨域配置浏览器会直接拦截所有请求。最省心的调试方式不是在Spring Boot里写CORS配置而是用Vue CLI自带的devServer代理。在vue.config.js里写module.exports { devServer: { port: 8080, proxy: { /api: { target: http://localhost:8081, changeOrigin: true } } } }这样前端请求/api/house/list时dev服务器会帮忙转发到后端。你不需要改任何axios代码里的baseURL统一写成相对路径/api就行。axios请求封装里我会统一做三件事加token、统一拦截错误、骑开发者的心把每次请求的Loading状态控制在请求数上避免并发请求时Loading被提前关闭。import axios from axios import { Message } from element-ui import router from /router const request axios.create({ baseURL: /api, timeout: 10000 }) request.interceptors.request.use(config { const token localStorage.getItem(token) if (token) { config.headers[Authorization] token } return config }) request.interceptors.response.use( response { const res response.data if (res.code ! 200) { Message.error(res.msg || 请求失败) return Promise.reject(new Error(res.msg)) } return res }, error { const status error.response?.status if (status 401) { Message.error(登录已过期请重新登录) localStorage.removeItem(token) router.push(/login) } else { Message.error(error.response?.data?.msg || 网络异常) } return Promise.reject(error) } ) export default request这里有个小细节登录过期不一定要返回401很多后端喜欢返回200 code401但那样axios统一拦截逻辑会乱掉。我后端直接设置HTTP状态码401拦截器判断status 401前端代码一眼就能看懂。4. “智能”的实现路径搜索引擎式SQL与简单推荐逻辑4.1 关键词搜索背后的SQL写法与中文分词困局“智能租房网站”的搜索功能如果你老老实实只写一条like语句其实也没问题但答辩时容易被问“你的搜索智能在哪”。所以我实现的是多字段模糊匹配条件叠加。搜索一个关键词时后端会同时匹配房源标题、地址、小区名三个字段select idpageQuery resultTypecom.example.rent.entity.House SELECT * FROM house where if testkeyword ! null and keyword ! AND (title LIKE CONCAT(%, #{keyword}, %) OR address LIKE CONCAT(%, #{keyword}, %) OR community_name LIKE CONCAT(%, #{keyword}, %)) /if if testarea ! null and area ! AND area #{area} /if if testminPrice ! null AND price gt; #{minPrice} /if if testmaxPrice ! null AND price lt; #{maxPrice} /if if testhouseType ! null and houseType ! AND house_type #{houseType} /if /where ORDER BY create_time DESC /select这个写法已经能覆盖多数场景。如果你想再“高级”一点可以引入MySQL的FULLTEXT索引但中文分词效果一般毕业设计没必要去接ES或Solr那是给自己挖坑。有的同学会在简历上写“基于ES的租房搜索”但如果只是跑通了Demo答辩老师连续追问索引分词、ik分词器配置就很容易露馅。我最后只用了LIKE 多条件动态SQL但是我把测试数据做得足够多演示效果完全不输。4.2 基于小区/户型/价格的推荐是怎么算出来的推荐功能我是这样设计的用户登录后每点击一次房源详情后端就在view_history表里插入一条记录记录用户ID、房源ID、浏览时间。当用户打开首页时后端读他最近的浏览记录统计他看过的房源中最常出现的小区、户型和价格区间然后按这些条件查出一批房源剔除已经看过的。核心SQL大概长这样select idrecommendByHistory resultTypecom.example.rent.entity.House SELECT h.* FROM house h WHERE h.status 1 AND h.id NOT IN ( SELECT house_id FROM view_history WHERE user_id #{userId} ) AND ( h.community_name IN ( SELECT community_name FROM view_history vh JOIN house hh ON vh.house_id hh.id WHERE vh.user_id #{userId} ) OR h.house_type IN ( SELECT house_type FROM view_history vh JOIN house hh ON vh.house_id hh.id WHERE vh.user_id #{userId} ) OR (h.price BETWEEN #{minPrice} AND #{maxPrice}) ) ORDER BY h.create_time DESC LIMIT 8 /select我这里没有做复杂的协同过滤而是把“推荐”拆成了可解释的规则。你要在论文里描述它可以写成“基于用户行为日志的标签偏好推荐”。老师问起来你直接说“我根据用户最近浏览记录提取偏好标签然后用标签过滤出候选房源”逻辑清晰代码也能对得上。4.3 前端联动展示省去你重复造轮子的时间推荐房源在首页展示我用的是热门房源列表相同的HouseCard组件唯一区别是多加了一个「为你推荐」的标签。这里又用到了插槽代码不用复制粘贴第二遍。除了推荐我还在详情页放了一个地图定位。这个不是必须的但做上去之后整个项目的完成度会高很多。我用的高德地图JS API后端存储房源的经纬度字段前端通过vue-amap或者直接引入高德JS标记位置。注意需要在后端配置经纬度或者发布房源时手动在地图上点选。我当时是在发布表单里集成地图选点房东发布房源时鼠标一点经纬度就填进表单了。这个功能听起来很唬人实现起来其实就一个click事件。5. 权限控制与状态保持从Session到JWT的过渡方案5.1 登录状态为什么用Session Plus Token的方案传统SSM教程里喜欢用Session但在前后端分离场景下Session有一个很大的问题跨域带上Cookie很麻烦axios默认不会携带跨域Cookie后端还得配置allowCredentials。所以我最终用了“移动端友好、前后端分离友好”的Token方案。但这里我并没有直接上Spring Security JWT那又是一个大坑。我用的是自研Token Redis存储的方案用户登录成功后生成一个随机UUID作为token存到Rediskey为token:{uuid}value为用户ID过期时间24小时。后端写一个拦截器每次请求都从请求头的Authorization里取出token去Redis查查出用户ID就放到request.setAttribute(userId, userId)。这个方案的优点是足够简单论文里写“基于Redis的Token会话管理”比硬套JWT好解释而且避开了JWT密钥管理、过期刷新等一堆琐碎问题。5.2 基于拦截器路由守卫的双重保护后端拦截器代码如下Component public class LoginInterceptor implements HandlerInterceptor { Autowired private RedisTemplate redisTemplate; Override public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception { String token request.getHeader(Authorization); if (token null || token.isEmpty()) { response.setStatus(401); return false; } Object userId redisTemplate.opsForValue().get(token: token); if (userId null) { response.setStatus(401); return false; } request.setAttribute(userId, Long.valueOf(userId.toString())); return true; } }拦截器注册时需要把登录接口、房源列表接口、房源详情接口排除掉其他接口一律拦截。要是注册的时候写错路径比如把/api/house/**拦截范围写成了/api/**那你后端所有接口都无法访问一查一个准。前端路由守卫的作用是避免用户切到未登录页面时才被后端401打断。我在router里给需要登录的页面加meta: { requiresAuth: true }然后在全局守卫里判断router.beforeEach((to, from, next) { const token localStorage.getItem(token) if (to.meta.requiresAuth !token) { next(/login) } else { next() } })管理员的页面还需要额外判断角色我就在meta里加了roles: [admin]守卫里取用户信息判断是否有这个权限。6. 打包部署实战Vue打包产物放进SpringBoot的坑6.1 前后端分离的分开部署方案开发时前后端分开跑部署时可以有两种做法。第一种是部署到两个服务Vue打包后的静态文件放到Nginx后端接口跑在Tomcat/Spring Boot上再用Nginx做反向代理。这是生产环境的标准做法但对于毕业设计演示来说你还需要一台服务器或者多开两个进程稍微麻烦。第二种就是我实际采用的把Vue打包后的静态资源直接放进Spring Boot的src/main/resources/static目录里做成一个可执行jar。这样你交付给老师的是一个jar包双击就能跑演示也方便。6.2 把dist目录放进Spring Boot静态资源里的配置Vue项目执行npm run build之后会在dist目录下生成index.html、static/css、static/js等文件。把这些文件复制到src/main/resources/static目录下Spring Boot会自动把它们当成静态资源供应。这里要提醒你一个特别容易翻车的地方Vue的静态资源路径。默认Vue打包时资源路径是绝对路径/js/xxx.js。如果Spring Boot的context-path设置为/rent也就是说你的项目访问路径是http://localhost:8080/rent/那/js/xxx.js会直接去找http://localhost:8080/js/xxx.js而不是http://localhost:8080/rent/js/xxx.js页面会出现白屏。解决方案是在vue.config.js里设置publicPath: ./让打包出来的资源路径变成相对路径module.exports { publicPath: ./, outputDir: dist, assetsDir: static }这样index.html里的资源引用就会变成./static/css/xxx.css就算context-path挂了一级也能正确加载。6.3 常见404、刷新404和API路径冲突的修复把dist放进Spring Boot之后你会发现一个典型问题在首页点进去没问题但手动刷新详情页URL时直接404。原因很简单Vue Router用了history模式当前端路由是/house/12时刷新浏览器会向后端请求/house/12后端没有这个Controller自然返回404。解决方案是在Spring Boot里写一个转发规则把前端路由的路径统一转发回index.html。我是在一个WebMvcConfigurer里这样写的Configuration public class WebMvcConfig implements WebMvcConfigurer { Override public void addViewControllers(ViewControllerRegistry registry) { registry.addViewController(/{spring:[a-zA-Z0-9-_]}) .setViewName(forward:/index.html); registry.addViewController(/**/{spring:[a-zA-Z0-9-_]}) .setViewName(forward:/index.html); } }注意这个规则不能拦截到真实存在的前端静态资源css/js否则也会被转发。我的处理是只在static目录下放构建产物同时Controller的接口路径统一以/api开头所以转发规则对静态文件没有影响。还有一个坑如果你的后端接口路径刚好和前端路由路径重合比如前端路由是/house/list后端Controller也有/house/list刷新时请求会先被SpringMVC匹配到后端接口而不是转发到index.html。我当时为了避免这种问题把所有后端接口统一放在/api前缀下彻底规避冲突。7. 论文文档LW写作与答辩准备的自我复盘7.1 LW文档的结构与核心章节怎么对应实际代码这个项目的“LW文档”其实就是毕业设计论文/说明文档。别把它当成摆设老师大概率是拿文档对照代码提问的。我写的文档目录如下绪论选题背景、国内外研究现状、研究内容和方法。相关技术介绍SSM框架、Vue.js、MySQL、Redis。注意不要大段抄官网要用自己的项目场景说明为什么选它。需求分析画用例图写功能需求和非功能需求。系统设计功能模块设计、数据库设计、接口设计。系统实现每个功能模块的界面截图、核心代码、实现说明。系统测试测试用例表、测试结果、性能简述。数据库设计这部分我画了7张表用户表、房源表、房源图片表、收藏表、浏览历史表、租房订单表、管理员表。文档里每张表都要有时间字段、外键关系的说明并且要和MySQL里的实际建表语句一致千万别文档画一张表、代码里是另一张表答辩很容易被抓。7.2 答辩老师最爱问的几个切入角度我自己答辩结束后整理了一波高频问题基本每个选同类题目的同学都会被问“你的项目里SSM各担任什么角色” 回答Spring负责Bean管理和事务SpringMVC负责请求分发和参数绑定MyBatis负责数据库操作和SQL的动态生成。“为什么叫智能租房网站” 回答因为实现了多条件组合搜索和基于浏览历史的偏好推荐。“MyBatis中#{}和${}的区别”#{}是预编译占位符能防SQL注入${}是字符串拼接有注入风险。“分页是怎么做的” 我用的PageHelper插件或者手写LIMIT。要能说清楚前端传pageNum和pageSize后端返回总条数和当前页数据列表。“如果你有1000万条房源数据搜索怎么优化” 这题其实在问你有没有扩展思路。我回答的是加索引、分表、引入搜索引擎但毕业设计本身不要求做到。7.3 测试数据与截图的重要性写文档时最耗时间的不是字而是截图。我的做法是提前把关键流程按步骤操作一遍并截图注册登录 → 房东发布房源 → 管理员审核 → 租客搜索 → 提交申请 → 房东确认 → 订单完成。每张截图对应文档里的一个功能描述演示时也用这套路径走。另外我在数据库里预置了30多条房源数据地址、价格、户型尽量分散方便演示搜索和筛选。真实场景里数据量少的话搜索条件一多就查不出结果演示体验很差。你可以在SQL脚本里写INSERT语句批量造数据也可以在管理后台手动录入我推荐用SQL脚本省时又可控。最后再分享一个我踩过的小坑Vue项目里接口地址如果写成了localhost:8081而部署后后端端口是8080一旦老师换台电脑运行页面就没数据。所以前端所有请求一定要用相对路径/api配合代理或后端静态部署换环境也不用改代码。这也是后期运维体验差异最大的一个点。