
过去帮人做毕业设计和技术改造的时候我经常遇到一个很有意思的项目形态一个房源管理系统后端主体用 Java 的 SSMSpring SpringMVC MyBatis写但又专门塞了一个 Flask 服务处理某些杂活。第一次看到这种组合的人会觉得很怪甚至有人直接判定这是为了凑技术栈而凑技术栈。实际上这种双后端架构在真实的小团队项目里比想象中常见得多。它不是什么优雅范本但确实能在特定场景下把 Java 生态和 Python 生态各自的优势都用起来。这类房源管理系统一般要覆盖的角色很明确管理员、中介/房东、租客。核心业务是房源的上架、检索、收藏、预约看房、租赁订单管理再加上后台的数据统计。如果只用 SSM也能把全部功能做出来但一旦涉及到推荐算法、模糊联想搜索、定时数据清洗这类 Python 更顺手的事情硬用 Java 去写会非常痛苦。反过来如果全用 Flask 写又会在事务管理、权限框架、企业级部署上吃亏。于是 SSM Flask 的组合就成了一种务实选择——SSM 负责交易主链路Flask 负责算法和辅助任务两者通过 HTTP 接口通信。这篇文章我会从架构设计、数据库建模、SSM 主链路实现、Flask 辅助服务、联调细节、部署调试六个方向把这个系统完整拆一遍。如果你正在做类似的课程设计、毕业设计或者小团队里想搭一个轻量级房源信息平台这里面的思路和踩坑经验可以直接拿去用。1. SSM和Flask同时出现不是凑技术栈1.1 这套双后端架构到底在解决什么问题我先用一个比较直白的比喻来解释为什么会有这种组合。SSM 像是一个正规军的后勤仓库事务、权限、流程管控都很规范Flask 则像一个专门做灵活活计的机动小队适合处理临时性、算法性、需要频繁调整逻辑的工作。房源管理系统里真正的主链路是用户登录 → 浏览房源 → 收藏/预约 → 签租赁单这条链路要求事务一致性强、状态流转清晰交给 SSM 再合适不过。而像根据你浏览过的房源推荐相似房源输入关键词时给出搜索建议每天凌晨抓取外部公开的房价数据做参考这些非核心但锦上添花的模块交给 Flask 写能让代码量少一半迭代也更快。很多人在做项目展示时最怕被问为什么用两套后端。其实只要把分工逻辑讲清楚这反而是加分项。你就说主业务需要强事务和成熟的 MVC 分层所以用 SSM推荐和爬取类功能需要快速实现并且 Python 有很成熟的算法库和爬虫生态所以用 Flask。两边职责清晰互不干扰这才是合理的工程决策。1.2 服务间调用方式为什么选HTTP短连接双后端系统首先要决定的就是两边怎么通信。常见方案有三种HTTP 短连接、消息队列、直接共享数据库。在这个项目规模下我推荐直接用 HTTP 短连接原因很现实——部署简单、调试直观、不依赖额外中间件。实际做法是SSM 作为主服务在业务逻辑里通过 RestTemplate 或 HttpClient 调用 Flask 的接口。比如用户在房源详情页停留超过 10 秒前端会把行为数据发给 SSMSSM 再异步调用 Flask 的/api/recommend接口把推荐结果存进 Redis 或 MySQL 的推荐缓存表。Flask 那边的接口设计也很简单只返回 JSON不维护会话状态。这里有个容易被忽视的点SSM 调用 Flask 时必须做超时控制。我用的是 RestTemplate 加setConnectTimeout(2000)和setReadTimeout(3000)一旦 Flask 服务没起来或者算法计算超时主业务不能等它直接降级为默认推荐列表。很多项目在演示时翻车就是因为没做超时控制Java 线程卡在等待 Flask 响应上整个页面加载被拖垮。1.3 模块边界划分两套服务如果边界没划清楚最后会变成一锅粥。我的划分原则是凡是需要写事务、需要严格权限控制、需要操作核心业务表的逻辑全部留在 SSM凡是纯计算、纯抓取、纯数据清洗、不需要强一致性的辅助逻辑全放到 Flask。具体到房源管理系统SSM 负责的模块包括用户注册、登录、角色权限管理房源信息的增删改查、上下架、审核收藏、预约看房、租赁订单、合同记录后台管理端的数据看板基础统计Flask 负责的模块包括基于用户历史的房源推荐搜索关键词建议与热词统计定时抓取公开房源数据用于市场参考邮件通知、短信验证码对接这样划分之后两边的代码量大概在 7:3 左右SSM 偏重Flask 偏轻。如果你发现 Flask 那边开始出现大量业务判断和事务逻辑说明边界已经歪了需要及时把功能往 SSM 迁。2. 房源数据库建模五张核心表决定了后面所有的功能2.1 从房源信息流开始拆表房源管理系统的核心是房源信息流。一条房源从房东/中介发布到管理员审核到租客浏览收藏最后成交中间要经过多个状态。数据库设计如果一开始没留够扩展余地后面写代码会处处别扭。我最开始设计时只建了三张表用户表、房源表、租赁表。结果做到收藏功能和图片管理时发现字段没地方放只能加字段甚至加表改了一大轮。所以后面重新建模时直接拆成五张核心表加若干辅助表。这个设计思路后来在同类型的房屋租赁项目中复用效果都很稳。2.2 核心表字段设计第一张是t_user用户表。这张表要同时容纳租客、房东、中介、管理员四类角色所以必须有个role字段区分。基础字段包括用户名、密码MD5 加盐或 BCrypt 加密、手机号、邮箱、真实姓名、身份证号用于实名认证、注册时间、状态正常/禁用。我额外加了一个user_source字段标记用户是来自 App 端还是后台手动创建这个字段排查问题时会很有用。第二张是t_house房源表这是整个系统的绝对主角。字段设计直接决定后续查询和管理功能好不好做。我最终用的核心字段如下字段名类型说明house_idbigint主键自增landlord_idbigint房东/中介用户IDtitlevarchar(100)房源标题descriptiontext房源描述pricedecimal(10,2)月租金元areadecimal(8,2)面积平方米house_typetinyint户型1室1厅1卫等用编码addressvarchar(255)详细地址longitude / latitudedecimal(10,6)经纬度做地图检索时用rent_typetinyint整租/合租statustinyint0已下架 1上架 2已租出audit_statustinyint0待审核 1通过 2驳回browse_countint浏览数publish_time / update_timedatetime发布时间/更新时间有个细节要特意提一下price字段别用int要用decimal(10,2)否则后期如果系统要支持短租按周计价或优惠活动整数租金根本没法处理。longitude和latitude也要用decimal(10,6)精度足够支撑地图上的附近房源查询。第三张是t_lease租赁订单表。它记录一次完整的租赁交易字段包括订单号、房源ID、租客ID、房东ID、起租日期、结束日期、月租金、押金、签约状态、支付状态、合同附件URL。注意订单号一定不要用自增ID要单独生成一个如LS20250101XXXX的格式否则打印合同或做客服查询时会很难受。第四张是t_favorite收藏表记录用户收藏了哪些房源字段很简单id、user_id、house_id、create_time加一个唯一索引(user_id, house_id)防止重复收藏。第五张是t_house_img房源图片表字段包括 id、house_id、img_url、sort_order多个图片用多条记录管理比挤在房源表的一个字段里专业得多。2.3 查询压力点和索引设计房源系统的查询压力主要集中在两个场景用户按条件筛选房源列表后台管理员按状态审核房源。前者是高频低代价后者是低频高代价两者都要在索引上提前规划。我的建议是用户列表页高频使用status字段过滤必须加索引。组合条件里最常用的是price、area、house_type可以建联合索引(status, price, area)。按区域检索时address字段用模糊查询LIKE %xx区%很难走索引如果数据量超过十万建议加一个district字段单独存储行政区再对这个字段建索引。t_lease表按租客查询历史订单user_id加普通索引即可。t_favorite表上面已经说了建唯一索引(user_id, house_id)。还有个推荐做法是给t_house表加一个扩展字段ext_json类型用 JSON 文本或 MySQL 的 JSON 类型用来存空调、电梯、阳台、朝南这些零散配套信息。这样既不用建十几张标签表又能在房源详情页一次性读取展示。有些评委或面试官会质疑 JSON 字段的查询效率但只要不拿它做高频条件过滤只是存储和展示用完全没问题。3. SSM主链路实现房源发布到上架的全过程3.1 工程结构与Maven依赖版本对齐SSM 项目怎么搭建我并不想花费太多篇幅网上教程很多。但有一个问题必须强调SSM 框架的版本兼容性是新建项目时最容易踩的坑。很多人的项目跑不起来不是因为代码写错而是 Spring、SpringMVC、MyBatis 的版本互相不兼容。我用的是一套经过验证的稳定组合JDK 1.8Maven 3.6.3Spring 5.2.15.RELEASESpringMVC 5.2.15.RELEASEMyBatis 3.5.6mybatis-spring 2.0.6MySQL Connector/J 8.0.28Druid 连接池 1.2.8PageHelper 5.3.0Jackson 2.12.5注意 Spring 和 SpringMVC 的版本号必须完全一致否则会出现诡异的组件扫描异常。MyBatis 和 mybatis-spring 也尽量用上表中对应的版本太新的 mybatis-spring 可能会和 Spring 5.2 产生兼容问题。工程结构采用标准的分层模式src/main/java ├── com.rent.controller # Controller层接收请求 ├── com.rent.service # Service层业务逻辑 ├── com.rent.dao # Mapper接口 ├── com.rent.entity # 实体类 ├── com.rent.common # 通用工具、常量、结果封装 └── com.rent.config # 拦截器等配置3.2 MyBatis动态SQL处理多条件房源检索房源列表页是整个系统最核心的查询场景因为用户会组合各种条件价格区间、面积区间、户型、区域、是否有电梯、是否整租。如果为每一种组合都写一条固定的 SQL代码会爆炸。MyBatis 的whereif动态 SQL 就是为此设计的。我写的HouseMapper.xml核心片段长这样select idselectHouseList resultTypecom.rent.entity.House SELECT * FROM t_house where if teststatus ! null AND status #{status} /if if testauditStatus ! null AND audit_status #{auditStatus} /if if testminPrice ! null AND price gt; #{minPrice} /if if testmaxPrice ! null AND price lt; #{maxPrice} /if if testminArea ! null AND area gt; #{minArea} /if if testdistrict ! null and district ! AND district #{district} /if if testkeyWord ! null and keyWord ! AND (title LIKE CONCAT(%, #{keyWord}, %) OR address LIKE CONCAT(%, #{keyWord}, %)) /if /where ORDER BY publish_time DESC /select两个要点gt;和lt;是 XML 中的转义写法直接写会导致 XML 解析失败这个坑出现过无数次。关键词搜索用CONCAT(%, #{keyWord}, %)拼接别在 Java 代码里先拼好再传进来那样容易产生 SQL 注入隐患。分页直接用 PageHelper一行代码搞定。在 Service 层调用前先PageHelper.startPage(pageNum, pageSize)紧接着执行查询返回的 List 会自动带上分页信息。这个插件的坑在于它只对下一条 SQL 生效所以startPage和查询之间不要插任何其他数据库操作否则分页会作用到错误的 SQL 上。3.3 房源图片上传与本地存储映射房源管理肯定要传图片。图片上传这部分我建议先不要引入对象存储服务用本地磁盘存储加 Tomcat 静态映射就够了后续流量大了再迁云。具体做法是在spring-mvc.xml里配置文件上传解析器bean idmultipartResolver classorg.springframework.web.multipart.commons.CommonsMultipartResolver property namedefaultEncoding valueUTF-8/ property namemaxUploadSize value5242880/ /beanController 里接收MultipartFile然后保存到服务器指定目录String originalFilename file.getOriginalFilename(); String suffix originalFilename.substring(originalFilename.lastIndexOf(.)); String fileName UUID.randomUUID().toString().replace(-, ) suffix; File dir new File(/data/upload/house/ dateStr); if (!dir.exists()) { dir.mkdirs(); } file.transferTo(new File(dir, fileName)); String relativePath /upload/house/ dateStr / fileName;这里我用 UUID 重命名文件名主要目的是防止中文文件名和重名导致的路径乱码与覆盖问题。relativePath存进数据库的t_house_img表页面img标签直接引用。为了让这个 URL 能被访问还要在 SpringMVC 里配置资源映射mvc:resources mapping/upload/** locationfile:/data/upload//注意location必须是绝对路径加 file 前缀写成相对路径大概率访问不到。这一步很多人漏掉传完图片发现浏览器 404九成是这里的问题。3.4 登录拦截和房东/管理员权限控制房源系统的角色权限不能只在后端接口里写if (user.getRole() 1)就完事还要在拦截器层做统一控制。我写了一个AuthInterceptor实现HandlerInterceptor在preHandle里判断所有/api/**请求必须带 tokentoken 通常在登录时生成存放在 Redis 或本地缓存里。请求/admin/**时用户角色必须是管理员否则直接返回 JSON 提示无权限。房东发布房源、编辑房源时必须是登录状态且被审核通过的账号。拦截器注册在 SpringMVC 配置里注意要排除登录接口、房源列表接口和静态资源路径否则用户没登录连房源列表都看不了那就尴尬了。有一点要提醒拦截器只做粗粒度的权限过滤像房东只能编辑自己发布的房源这种细粒度权限还是要写在 Service 层里查询时带上landlordId条件。如果只靠前端按钮隐藏来控制直接构造 HTTP 请求就能越权修改别人的房源这在答辩或代码评审时是硬伤。4. Flask在项目里承担的三个脏活4.1 基于用户行为的房源推荐接口推荐功能是整个系统最容易体现亮点的地方也是 Flask 存在的最大理由。我用的是最经典的协同过滤思路实现逻辑并不复杂。流程是用户在浏览房源时前端会调用一个统计接口/api/recordView?houseIdxxSSM 将这个行为记录到t_user_behavior表。Flask 的/api/recommend/int:userId接口被 SSM 调用时会做这些事读取用户最近 30 天的浏览记录。找到同样浏览过这些房源的相似用户群。在这些相似用户浏览过的房源里找到目标用户没看过的房源按出现频率排序。返回 Top 10 的 houseId 列表。代码核心部分def get_recommend(user_id): conn get_db() viewed fetch_viewed(conn, user_id) if not viewed: return [] similar_users fetch_users_also_viewed(conn, viewed) candidates {} for uid in similar_users: for hid in fetch_viewed(conn, uid): if hid in viewed: continue candidates[hid] candidates.get(hid, 0) 1 top sorted(candidates.items(), keylambda x: x[1], reverseTrue)[:10] return [item[0] for item in top]这个接口返回的是纯 JSON 列表SSM 拿到后根据 houseId 查出房源的完整信息组装返回给前端。整个过程不需要 Flask 写任何页面它只做算法计算和数据存取。这里要注意Flask 端连接 MySQL 时我建议用 SQLAlchemy PyMySQL不要直接裸写 MySQLdb因为 MySQLdb 在 Python 3 环境下的安装非常折磨人。4.2 搜索建议和热词统计房源列表页的搜索框是用户使用频率最高的入口。为了提升体验我做了一个输入前缀实时提示的小功能用户输入北的时候下拉框提示北京路北苑小区北站附近精装一室等关键词。实现方式很简单Flask 提供一个/api/suggest?keywordxx接口内部执行SELECT DISTINCT title FROM t_house WHERE title LIKE %s LIMIT 10;再用缓存减少数据库压力。我用了 Flask-Caching 缓存 30 秒避免每次敲键盘都打数据库。热词统计则是每天定时统计t_user_behavior表中搜索关键词的频率写入t_hot_keyword表后台管理端可以在大屏上展示今日热搜词 TOP20。这个功能拿去答辩时很容易讲出彩因为它是真实的用户行为数据驱动不是写死的静态假数据。4.3 定时任务与数据补全Flask 还有一个 SSM 很难替代的优势写定时任务特别方便。我用 APScheduler 实现了两个定时任务每天早上 6 点抓取几家公开房产网站上对应小区的挂牌均价更新到t_market_price表用于前端展示该小区参考均价。每晚 11 点清理超过 90 天未登录的临时验证码记录和无效 token 数据。第一个任务如果放在 Java 里写也不是不行但用 Python 的 Requests BeautifulSoup 解析效率高很多。有一点必须强调抓取外部网站数据时一定要控制频率做好异常处理否则对方网站把服务器 IP 封了后面所有任务都会失败。APScheduler 在 Flask 里的启动方式我踩过一次坑。不能在每次请求时都创建调度器那样会产生多个任务副本。正确的做法是在应用工厂里创建全局调度器只启动一次scheduler APScheduler() scheduler.init_app(app) scheduler.start()然后在服务启动模块里启动 Flask app。如果用 Flask 自带的开发服务器记得debugTrue时 APScheduler 会启动两次因为 reloader 会执行两遍模块代码。解决方案是加一个条件判断if __name__ __main__: app.run(host0.0.0.0, port5000, debugFalse)直接关掉 debug reload。上线时反正也要用 waitress 或 gunicorn这个开发环境的小问题可以不用太纠结。5. Java与Flask联调时最容易翻车的五个细节5.1 JSON序列化numpy类型和中文编码双后端联调第一个拦路虎一定是 JSON。Flask 的jsonify看起来好用但当你返回的数据里包含 numpy 类型时它直接报错TypeError: Object of type int64 is not JSON serializable。比如推荐接口里候选房源 ID 可能在 Python 的列表推导中变成numpy.int64。解决办法是在接口返回前做一层转换def to_json_safe(obj): if isinstance(obj, np.integer): return int(obj) if isinstance(obj, np.floating): return float(obj) return obj还有一个坑是中文编码。Flask 默认的app.json.ensure_ascii是 True返回的 JSON 里面的中文会变成\uXXXX格式如果 Java 端没做处理前端拿到的可能是转义字符。建议在 Flask 配置里显式设置app.json.ensure_ascii False这样接口返回的就是真正的中文字符。5.2 跨域配置与前端直连场景如果前端页面直接通过 AJAX 调用 Flask 接口比如搜索建议这种对实时性要求高的请求不想绕到 Java 再转一遍就必须处理跨域问题。开发环境下浏览器会拦截跨域请求报 CORS 错误导致调试半天以为是接口写错了。解决方式是给 Flask 装上flask-corsfrom flask_cors import CORS CORS(app, resources{r/api/*: {origins: *}})配置后 Flask 接口的响应里会自动带上Access-Control-Allow-Origin头前端就能直接访问。但注意生产环境不要用通配符*要改成实际的前端域名否则接口可以被任意网站调用存在被恶意刷接口的风险。5.3 日期格式的一致性问题Java 端的 LocalDateTime 默认序列化为带T的格式比如2025-01-15T14:30:00而 Python 端的时间格式是2025-01-15 14:30:00。如果两边都要在接口里返回时间字段同一个字段在不同接口里格式不同前端处理时会很头疼。我的建议是统一约定所有接口的时间字段全部使用yyyy-MM-dd HH:mm:ss格式。Java 端的实体类时间属性上加上JsonFormat(pattern yyyy-MM-dd HH:mm:ss, timezone GMT8)注解Python 端在拼接时间字符串时用datetime.now().strftime(%Y-%m-%d %H:%M:%S)。另外还要注意时区问题MySQL 连接串里加上serverTimezoneAsia/Shanghai否则时间和数据库对不上排查起来会怀疑人生。5.4 静态图片跨服务引用Flask 的推荐接口只返回 houseId实际图片还是 SSM 提供的静态路径这就不存在跨域问题。但是有一种情况会出问题如果后期把 Flask 单独部署在另一个域名下而图片路径的img_url字段存的是相对路径/upload/xxx.jpg前端在 Flask 服务的页面下拼接这个地址就会变成访问 Flask 域名下的/upload/xxx.jpg导致图片 404。解决办法是房源列表接口返回数据时把相对路径拼成完整的绝对 URL 地址String fullUrl request.getScheme() :// request.getServerName() : request.getServerPort() relativePath;或者在前端统一配置一个baseUrl所有相对路径都基于这个地址拼接。这个小细节在打包部署后非常容易翻车提前处理能省很多事。5.5 服务挂掉之后的降级处理双后端系统最怕的就是一个服务挂了拖垮整个系统。真实项目中Flask 服务因为算法计算量大或者数据库连接池耗尽偶尔卡死很正常。如果 SSM 主链路同步调用 Flask 不设超时用户触发推荐功能时页面就会白等好几秒甚至整个 Tomcat 线程池被占满。我的做法是SSM 里所有调用 Flask 的方法都设了连接超时和读取超时并且在线程池里异步执行主线程不阻塞。调用失败时返回一个空集合或默认推荐列表不影响主流程。同时给 Flask 加一个健康检查接口/api/health返回{status: ok}SSM 定时探测连续三次失败就标记为不可用直接跳过调用。这个降级策略在答辩时会被认为是很有工程经验的点建议重点准备。6. 部署上线和调试文档的整理方法6.1 生产环境的双服务部署这个项目最终要跑在服务器上或者至少要在演示时让评审老师看到一个稳定运行的状态。部署我建议用一台 Linux 服务器把 SSM 打成 WAR 包丢进 Tomcat 的 webapps 目录Flask 用 waitress 跑在 5000 端口。具体步骤Maven 打包mvn clean package -DskipTests生成ssm-rent.war。把 WAR 包复制到 Tomcatwebapps目录启动时自动解压。Flask 环境用虚拟环境隔离安装好 requirements.txt 里的依赖。用 waitress 启动 Flaskwaitress-serve --host0.0.0.0 --port5000 app:app配置 Nginx 反向代理把/api/flask/路径转发到 5000 端口或者前后端分离时直接把 Flask 接口暴露给前端。MySQL 安装在服务器本地创建数据库时务必设置字符集为utf8mb4CREATE DATABASE rent_db DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci;不设 utf8mb4 的话用户昵称和房源标题里一旦出现中文以外的特殊符号比如 Emoji入库时直接报错或变问号这种问题特别低端但特别常见。6.2 调试文档与演示数据准备一个完整的毕设或课程设计项目调试文档和演示数据往往比代码本身更能影响评价。能跑起来永远比代码写得优雅重要。我的调试文档一般包含这几部分环境版本清单JDK、Maven、Tomcat、MySQL、Python、Flask 的精确版本。初始化步骤导入 SQL 脚本、修改数据库连接配置、启动 Flask、启动 Tomcat 的顺序。常见问题排查手册比如 8080 端口被占用怎么办、MySQL 密码错误怎么重配、Flask 依赖装不上怎么处理。演示账号清单管理员账号、房东账号、租客账号各一个标注好角色和权限。演示数据是很多人忽略的一点。我强烈建议在t_house表里准备 20 到 30 条不同价格段、不同区域、不同户型的房源数据统一带真实的地址和图片地址。演示时评委大概率会搜索一室一厅或者筛选2000 元以下如果查出来空列表整个系统看起来就像没做完。可以写一个init_demo_data.sql用批量 INSERT 生成数据价格和区域尽量覆盖全面。6.3 结合个人经验的一些建议最后分享几点我对这类项目的真实体会。第一不要把 Flask 当成摆设。如果你决定用双后端就要确保 Flask 承担的功能是 SSM 做起来相对费力的比如推荐、定时任务、数据清洗。答辩时老师通常会追问为什么不用纯 Java你自己分析了两种方案的区别吗能逻辑自洽地回答清楚分数不会低。第二安全相关的细节能加就加。比如登录密码加密存储、后台接口的防重复提交、修改密码后的旧 token 清理。这些代码量不大但能体现工程素养。第三代码注释不需要多但关键节点一定要写清原因。像拦截器里排除路径的原因、数据库索引设计的选择依据这类注释在评审和后续维护时价值非常高。第四如果时间充裕可以给系统加一个简单的数据统计模块比如展示每周新增房源数、签约订单量、热门区域 TOP10 的折线图或柱状图。用到 ECharts 就行数据接口由 SSM 查询后以 JSON 返回。这类可视化内容非常容易做出大屏效果视觉效果一提上来整个项目观感立刻就不一样了。我见过太多类似的项目功能基本齐全但部署文档随意写、演示数据裸奔、Flask 服务启动方式靠猜结果在验收环节频频出错。希望这篇拆解能帮你把这类房源管理系统真正做扎实无论是用来交作业还是自己实际搭一个轻量平台这套思路都值得一试。