房产信息管理系统Spring Boot实战:从需求到部署全流程

发布时间:2026/10/5 3:12:32
房产信息管理系统Spring Boot实战:从需求到部署全流程 最近把基于 Spring Boot 的房产信息管理系统完整整理了一遍源码、数据库脚本、配套说明文档都打包好了。这个项目不像网上那些只贴几个 Controller 和 Service 的“半成品”而是一个能从零跑起来、能直接改造成生产项目的完整闭环。核心场景是房产中介门店的业务管理房源录入与维护、客户登记与跟进、预约带看、成交登记加上后台的用户和权限管理。如果你是刚学完 Spring Boot 想做点像样项目来充实简历或者需要快速搭一套管理后台给公司内部用又或者单纯想看看一个正经的 CRUD 项目该怎么组织代码这个项目都能给你提供挺完整的参考。这篇文章我会从需求拆解、技术选型、数据库设计、核心代码实现、部署踩坑这五条线把这个项目彻底翻一遍。已经跑过一遍的可以直接跳到第五节看疑难排查刚开始接触的建议顺着顺序读每一步都对应到我能找到的实际代码和配置尽量让你看完不光知道“这个系统有哪些功能”更知道“每一块为什么要这么写”。1. 先搞明白需求这个房产系统到底管什么1.1 业务边界不是售楼系统是门店管理系统很多拿到这个项目的人第一反应是这不是个“房产信息管理系统”吗那应该把楼盘字典、预售许可证、网签备案这些功能都做进去这里得先泼盆冷水。如果你真按售楼系统的规格去理解那这个项目连需求分析这关都过不了。实际上这套系统定位的是存量房经纪业务也就是中介门店的管理工具。核心服务对象是经纪人、店长和门店内勤解决的问题是一套房源从业主报盘到成交的完整生命周期管理。典型的业务流是这样的业主把房源挂到门店 → 经纪人录入系统并拍照上传 → 店长审批后房源状态变成“可售” → 客户进店咨询经纪人登记客户需求 → 安排带看记录看房反馈 → 双方价格谈拢录入成交信息 → 后台统计本月的成交量、佣金、房源去化周期。听起来不复杂但只有把手里的数据管好了后面所有“数据驱动决策”才有基础。这也是为什么我把表结构设计看得比前端页面还重——业务再花哨落到数据库里就是几张表之间怎么关联、状态怎么流转。1.2 六大核心功能模块拆解整个系统我拆成了六个模块每一项都有明确的业务产出模块核心功能对应核心表系统管理用户登录、角色分配、菜单权限sys_user、sys_role、sys_menu房源管理房源录入、审核、上下架、条件查询house_info、house_audit_log客户管理客户登记、需求标签、跟进记录customer_info、customer_follow带看管理预约带看、反馈结果、状态流转visit_record成交管理成交登记、佣金计算、合同信息deal_record数据统计房源存量、成交量、佣金排行基于以上表聚合六个模块里最容易写崩的是房源管理和客户管理之间的关系。最开始我确实考虑过直接在 house_info 表里加一个 customer_id 字段让“这套房被哪个客户看中”直接挂死。后来一复盘立刻放弃——一套房源可以带看多个客户一个客户也可以看多套房这就是典型的多对多关系。正确的做法是中间加一个带看记录表 visit_record每次带看都是一条独立记录关联 house_id 和 customer_id带看时间、带看结果、客户反馈都挂在记录上。这样设计以后做任何统计报表都特别顺某套房源的带看次数、某个客户的看房历史、某个经纪人本月的带看转化率全是单表条件查询加 group by。状态流转这块我在房源表里设计了 status 字段通过数字标识状态而不用字符串这样既省空间又方便条件筛选。0 草稿、1 待审核、2 可售、3 已预定、4 已成交、5 下架。状态流转的规则写在 Service 层里而不是前端界面控制防止有人跳过状态乱改数据。比如说只有“可售”状态的房源才能被预约带看带看后可以一键把状态改成“已预定”这是业务逻辑不能靠前端禁用按钮就完事。2. 技术选型与项目搭建为什么是 Spring Boot MyBatis MySQL2.1 Spring Boot 版本怎么选这个项目用的是 Spring Boot 2.7.xJava 8不是最新的 3.x。这可能是被问得最多的问题新项目为什么不用 Spring Boot 3原因很现实生态兼容性。Spring Boot 3 基于 Jakarta EEjavax 包全换成 jakarta 包很多老一点的第三方库版本不支持。当时如果直接上 3.xMyBatis 得用 mybatis-spring-boot-starter 3.x 版本PageHelper 得用 5.3连接池这边 Druid 也得搭配 1.2.6任何一个版本没对齐都会踩坑。而且公司里大量的存量项目都是 JDK 8用 3.x 意味着所有依赖都得跟着升成本太高。对于学习型项目2.7.x 还有一个好处网上资料最多遇到问题基本都能搜到答案。等到你想体验新特性再单独拿一个项目去升级迁移也不迟没必要拿这个系统当实验品。2.2 持久层框架为什么选 MyBatis 而不是 JPA这个选择我纠结过一阵子。JPA 在做简单 CRUD 的时候确实爽建好实体类继承 JpaRepository增删改查全出来了连 SQL 都不用写。但房产信息管理系统的查询有一个特点查询条件复杂且动态。房源列表页的筛选条件至少有这些小区名模糊、户型精确、面积区间范围、价格区间范围、朝向、楼层、发布时间、状态。如果用 JPA 的 Specification 写动态查询代码会复杂得难以阅读而且稍微有点经验的开发都知道JPA 在复杂多表关联查询上性能调优比 MyBatis 痛苦得多。MyBatis 的 XML 里写where配合if标签做动态 SQL逻辑一目了然SQL 也好单独拿出来在数据库客户端里调试。这就是我最终选 MyBatis 的决定性理由把 SQL 的控制权完全握在自己手里。对房产这种数据关联复杂、报表需求多的系统这个优势太重要了。2.3 项目初始化和依赖清单项目直接用 Spring Initializr 创建核心依赖非常简单dependency groupIdorg.springframework.boot/groupId artifactIdspring-boot-starter-web/artifactId /dependency dependency groupIdorg.mybatis.spring.boot/groupId artifactIdmybatis-spring-boot-starter/artifactId version2.3.1/version /dependency dependency groupIdcom.github.pagehelper/groupId artifactIdpagehelper-spring-boot-starter/artifactId version1.4.7/version /dependency dependency groupIdmysql/groupId artifactIdmysql-connector-java/artifactId scoperuntime/scope /dependency dependency groupIdorg.projectlombok/groupId artifactIdlombok/artifactId optionaltrue/optional /dependency没有引入 Spring Security。是我有意为之——这个项目如果交给 Spring Security 管Configuration 类就要写一大堆对刚学 Spring Boot 的同学来说认知负荷太重。我最后用的是Interceptor 自定义注解实现了登录拦截和权限校验。效果足够满足需求而且代码直观得多。如果你想让项目更“企业级”后续自己升级到 Spring Security JWT 也不复杂我会在第四节讲这块的替换路径。配置文件application.yml里有几个关键点值得展开server: port: 8080 spring: datasource: driver-class-name: com.mysql.cj.jdbc.Driver url: jdbc:mysql://localhost:3306/house_manager?useUnicodetruecharacterEncodingutf8useSSLfalseserverTimezoneAsia/ShanghaiallowPublicKeyRetrievaltrue username: root password: 123456 hikari: minimum-idle: 5 maximum-pool-size: 20 connection-timeout: 30000 mybatis: mapper-locations: classpath:mapper/*.xml type-aliases-package: com.house.entity configuration: map-underscore-to-camel-case: true pagehelper: helper-dialect: mysql reasonable: true注意map-underscore-to-camel-case: true这行配置把数据库的create_time自动映射到 Java 属性的createTime不用手动写 resultMap 的每个字段映射省掉大量冗余代码。前提是数据库字段规范用下划线命名。3. 数据库设计这个项目的核心资产3.1 ER 关系和建表思路房产信息系统的核心表一共六张表关系可以用文字描述清楚。sys_user是登录用户表sys_role是角色表中间通过sys_user_role关联。业务侧house_info是房源主表customer_info是客户主表两张表不直接关联而是通过visit_record带看记录表建立联系。deal_record成交记录表同时关联房源和客户一张成交记录代表一单业务完成。建表时我命中了一个很多新手容易犯的错误能用逻辑外键就别用物理外键。不要在表上定义 FOREIGN KEY 约束而是在应用层保证关联一致性。为什么物理外键会导致插入数据时必须先插父表再插子表批量导入数据时极其痛苦而且后期做分库分表时物理外键根本没法用。业务系统里的“外键关系”应该通过查询时的 JOIN 来体现。3.2 房源表设计字段就是业务要求的映射我挑house_info表重点讲因为这张表最能体现需求分析的质量CREATE TABLE house_info ( id bigint(20) NOT NULL AUTO_INCREMENT COMMENT 主键ID, community_name varchar(128) NOT NULL COMMENT 小区名称, building_no varchar(32) DEFAULT NULL COMMENT 楼栋号, unit_no varchar(32) DEFAULT NULL COMMENT 单元号, room_no varchar(32) DEFAULT NULL COMMENT 房号, bedroom_count tinyint(4) DEFAULT NULL COMMENT 室, living_room_count tinyint(4) DEFAULT NULL COMMENT 厅, bathroom_count tinyint(4) DEFAULT NULL COMMENT 卫, area decimal(10,2) DEFAULT NULL COMMENT 建筑面积(㎡), total_price decimal(12,2) DEFAULT NULL COMMENT 总价(万), unit_price decimal(10,2) DEFAULT NULL COMMENT 单价(元/㎡), orientation varchar(16) DEFAULT NULL COMMENT 朝向, floor_desc varchar(32) DEFAULT NULL COMMENT 楼层描述, decoration varchar(16) DEFAULT NULL COMMENT 装修情况, house_type varchar(16) DEFAULT NULL COMMENT 房源类型:住宅/别墅/商铺, status tinyint(4) NOT NULL DEFAULT 0 COMMENT 状态:0草稿,1待审核,2可售,3预定,4成交,5下架, owner_name varchar(32) DEFAULT NULL COMMENT 业主姓名, owner_phone varchar(20) DEFAULT NULL COMMENT 业主电话, remark varchar(512) DEFAULT NULL COMMENT 备注, create_by bigint(20) DEFAULT NULL COMMENT 创建人ID, create_time datetime DEFAULT CURRENT_TIMESTAMP COMMENT 创建时间, update_time datetime DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT 更新时间, deleted tinyint(1) NOT NULL DEFAULT 0 COMMENT 逻辑删除:0正常,1删除 ) ENGINEInnoDB AUTO_INCREMENT1 DEFAULT CHARSETutf8mb4 COMMENT房源信息表;几个设计决策解释一下第一deleted字段做逻辑删除而不是物理 DELETE。这是数据安全性的基本要求房源数据属于业务核心资产误删之后找不回来是重大事故。所有查询语句强制带deleted 0条件直接在 Mapper XML 里写死不依赖程序员自觉。第二单价unit_price在数据库里冗余存储而不是通过total_price / area计算。原因是展示房源列表时计算太频繁而且后期单价可能会有手工调整的场景比如车位和储藏室的价格不计入均价。这个冗余字段是通过业务规则来保证一致性的——录入房源时后端计算好单价再落库。第三金额字段全部用decimal绝对不能用float或double。这是做金融相关业务的基本常识浮点数的精度误差在金额计算里是不可接受的。3.3 初始化数据数据库脚本里除了建表语句还带了初始化和测试数据。默认管理员账号是 admin / 123456密码在数据库里存的不是明文而是 MD5 加盐后的密文。注意加盐这个操作很关键不加盐的 MD5 在彩虹表面前就跟没加密一样。我用的盐值是用户名加固定后缀每次登录校验时用同样规则重新计算比对。测试数据这块我造了二十条房源、十五条客户、十条带看记录、五条成交记录时间分布在最近三个月这样直接跑起来之后统计报表页面就有数据可看不用自己一条条录。4. 源码解读核心模块是怎么实现的4.1 项目分层Controller、Service、Mapper 各司其职拿到源码后建议先看包结构我在com.house下分得很清楚com.house ├── controller # 接收请求参数校验调用service ├── service # 业务逻辑层事务边界都在这 │ └── impl # service接口实现类 ├── mapper # MyBatis的mapper接口纯数据访问 ├── entity # 实体类和数据库表字段对应 ├── config # 配置类拦截器、跨域、WebMvc配置 ├── common # 通用类全局异常、统一返回结果、工具类 └── interceptor # 登录拦截器Controller 层我只做三件事接收参数、简单校验、调用 Service 并封装返回。业务规则绝不允许写进 Controller。比如“只有可售状态的房源才能修改”这种规则必须放在 Service 层里因为它是业务流程的一部分而不是接口格式的一部分。Transactional事务注解用在 Service 实现类上事务边界是一个完整的业务操作比如“成交登记”同时要改房源状态、插入成交记录、修改客户状态三个操作要么全成功要么全回滚一个Transactional搞定。新手最容易搞错的是把事务注解加在 Controller 或 Mapper 上前者事务粒度太粗后者来不及控制跨表逻辑。4.2 统一返回结果与全局异常处理前后端交互的数据格式我统一用Result类包装。这个类虽然简单但它是整个系统的“接口约定”public class ResultT { private Integer code; // 200成功500业务失败401未登录 private String msg; private T data; public static T ResultT success(T data) { ResultT result new Result(); result.code 200; result.msg success; result.data data; return result; } public static T ResultT error(String msg) { ResultT result new Result(); result.code 500; result.msg msg; return result; } }配合RestControllerAdvice做全局异常处理业务代码里只需要 new 一个自定义异常类BusinessException并设置错误信息前端拿到的就是统一的 JSON 结构。这样后端再怎么报错前端都不用担心拿到一堆乱七八糟的堆栈文本。这里有个经验要分享全局异常处理里不要把异常堆栈直接返回给前端往日志里打印就好。堆栈信息暴露出去一方面不安全另一方面对调用方没有意义。4.3 登录与权限控制Interceptor 加自定义注解登录校验这块我设计了两个一级路径白名单放行和拦截器校验。登录接口、静态资源直接放行其他/api/**请求全部经过拦截器。拦截器的核心逻辑简单直白public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) { // 放行 OPTIONS 预检请求 if (request.getMethod().equalsIgnoreCase(OPTIONS)) { return true; } // 从session中获取登录用户 HttpSession session request.getSession(); SysUser user (SysUser) session.getAttribute(loginUser); if (user null) { response.setStatus(401); return false; } // 方法级权限校验 if (handler instanceof HandlerMethod) { HandlerMethod handlerMethod (HandlerMethod) handler; RequirePermission requirePermission handlerMethod.getMethodAnnotation(RequirePermission.class); if (requirePermission ! null) { String required requirePermission.value(); if (!user.getPermissions().contains(required)) { response.setStatus(403); return false; } } } return true; }权限数据在用户登录成功后一次性查出该用户拥有的所有权限码放进 Session。由于本项目最多有店长、经纪人、内勤三种角色权限码的量级很小适合这种简单粗暴的 Session 方式。如果你要替换成 JWT核心思路不变拦截器里从请求头取 token解析后把用户信息放进 ThreadLocal。只是把 Session 从服务端存状态换成了客户端存状态。多说一句很多学习项目在权限这块有个通病把权限校验逻辑写成if (admin.equals(username))这种硬编码。万一要加一个店长角色你得改完代码再重新部署。正确的做法是像上面看到的权限用代码位表示角色和权限的对应关系存数据库表后台可以随时调整角色能访问的资源不用动代码。4.4 房屋列表查询动态 SQL 是关键难点项目里最有代表性的代码应该算房源列表的多条件查询这也是我要细讲的一段。客户在列表页可以任意组合筛选条件如果不用 MyBatis 动态 SQL那你就只能写 if/else 一整套拼接 SQL 的逻辑。MyBatis 的whereif就是为此而生的select idselectHouseList resultTypecom.house.entity.HouseInfo SELECT * FROM house_info where deleted 0 if testcommunityName ! null and communityName ! AND community_name LIKE CONCAT(%, #{communityName}, %) /if if testminArea ! null AND area gt; #{minArea} /if if testmaxArea ! null AND area lt; #{maxArea} /if if testminPrice ! null AND total_price gt; #{minPrice} /if if testmaxPrice ! null AND total_price lt; #{maxPrice} /if if teststatus ! null AND status #{status} /if if testorientation ! null and orientation ! AND orientation #{orientation} /if /where ORDER BY create_time DESC /select注意gt;和lt;XML 里直接写会报错必须用转义字符。这是 MyBatis XML 里最常见的坑之一错得毫无防备。分页用的是 PageHelper一行代码搞定PageHelper.startPage(pageNum, pageSize); ListHouseInfo list houseInfoMapper.selectHouseList(query); PageInfoHouseInfo pageInfo new PageInfo(list);这里有非常重要的一条注意事项PageHelper.startPage必须紧跟 MyBatis 查询方法中间不能有任何其他 SQL 操作否则分页会作用到错误的那条 SQL 上。以前有过一次查询之前调了个其他 Mapper 方法取配置数据结果分页插件把这个查询当成目标列表页返回条数完全不对排查了半小时才发现。这条经验写在这里希望能帮你绕开。4.5 多表联查与统计报表数据统计模块里有个典型需求查本月每个经纪人的带看次数和成交量。这要用到 JOIN 和 GROUP BY 的组合SELECT u.real_name, COUNT(DISTINCT v.id) AS visit_count, COUNT(DISTINCT d.id) AS deal_count FROM sys_user u LEFT JOIN visit_record v ON v.create_by u.id AND v.visit_time #{monthStart} AND v.visit_time #{monthEnd} LEFT JOIN deal_record d ON d.create_by u.id AND d.deal_time #{monthStart} AND d.deal_time #{monthEnd} WHERE u.role_type 2 GROUP BY u.id这里用DISTINCT防止带看记录和成交记录关联时产生重复计数属于报表查询中非常经典的写法。如果没用DISTINCT一个经纪人又带看过又成交过JOIN 出来的中间结果会被放大统计数字直接翻倍。5. 部署运行与踩坑实录从拿到代码到跑起来5.1 三步快速启动拿到代码之后最快的启动路径就三件事建库、改配置、启动。第一步用 Navicat 或命令行执行house_manager.sql脚本自动建库建表并插入测试数据。脚本开头就有CREATE DATABASE IF NOT EXISTS house_manager DEFAULT CHARACTER SET utf8mb4;不用手动建库直接跑完即可。第二步改application.yml里的数据库账号密码改成你自己本机的配置。第三步在项目根目录执行mvn spring-boot:run看到 Tomcat started on port(s): 8080 就说明启动成功。浏览器访问http://localhost:8080/login.html输入 admin/123456 登录即可。如果你想用 IDE 启动IDEA 里直接运行HouseApplication主类也一样。注意运行时的工作目录需要是项目根目录否则mapper-locations的相对路径可能会找不到 XML。5.2 高频报错与解决方案我把这个项目从开发到部署遇到的所有典型问题整理成了一张排查表每一列都是实际会发生的问题。报错现象根本原因解决方案Access denied for user rootlocalhost数据库密码配置错误检查 application.yml 的 username/passwordUnknown database house_managerSQL 脚本未导入或导错库重新执行 sql 脚本确认连接 URL 中的库名The server time zone value is unrecognizedMySQL 驱动版本和时区不匹配URL 末尾加serverTimezoneAsia/ShanghaiInvalid bound statement (not found)Mapper 接口和 XML 不对应检查mapper-locations路径和 XML namespacejava.sql.SQLException: Public Key Retrieval is not allowedMySQL 8.0 的认证插件问题URL 加allowPublicKeyRetrievaltrue控制台输出中文乱码连接 URL 未指定 UTF-8加characterEncodingutf8并检查数据库默认字符集端口被占用8080 被已有程序占用server.port换个端口或找到占用进程关闭以上问题里Invalid bound statement对新手来说最隐蔽。Spring Boot 启动不报错但一调用 Mapper 方法就报“找不到绑定语句”。排查方向一是看 target/classes 目录下有没有把 XML 拷贝进来二是看 IDEA 的 Build 配置有没有把 resources 排除三是核对 XML namespace 是否和 Mapper 接口的全限定名一致。5.3 打包部署到服务器的正确姿势项目开发完毕后部署到 Linux 服务器上是另一个常见场景。Maven 打包时记得先跳过测试mvn clean package -DskipTests打出来的 jar 包在target目录下我用nohup后台运行nohup java -jar house-manager-1.0.0.jar --spring.profiles.activeprod house.log 21 日志输出重定向到house.log建议部署脚本里加一句旧的 jar 进程先杀掉再启动避免端口冲突。如果项目要连线上数据库推荐的做法是把application.yml里的数据库密码改成通过环境变量注入别明文写死在 jar 包配置里spring: datasource: password: ${DB_PASSWORD}这样即使代码泄露到 GitHub数据库密码也不会泄露。写在最后关于这个项目的一点个人感受这次整理项目的过程里最大的体会不是“我写了多少行代码”而是“我做了多少决策”。数据库字段该不该冗余、状态流转该在哪一层控制、登录校验用 Session 还是 JWT、事务边界画到哪里每一个问题背后都有真实业务场景的考量逻辑。这些决策能力和踩坑经验恰恰是单纯刷 Spring Boot 语法学不到的。我自己在实际使用中还有一个习惯每做完一个功能模块会顺手把这次的踩坑记录追加到项目文档里。这个项目的说明文档里就包含了完整的接口文档、部署步骤、问题排查目录后续交接给任何人他们都能从零开始把系统跑起来。强烈建议你也养成这个习惯做完一次完整项目之后把文档、脚本、代码一并归档。用的时候回翻比什么都灵。这个系统后续的扩展空间也很大。数据量上来之后可以引入 Redis 做热点房源缓存减少数据库压力图片上传可以先本地存储后续接 OSS 对象存储带看和成交环节可以加入消息通知模块。这些扩展点我在源码里都留了接口等你有需要时动手试试也祝你顺利跑通。