首页 / 资讯中心 / 文章详情

Spring Boot房产中介管理系统实战:从数据库设计到部署避坑

Spring Boot房产中介管理系统实战:从数据库设计到部署避坑 ★ FEATURED ARTICLE
上个月刚交付了一个基于Spring Boot的Java Web房产中介管理系统项目代号1sy6u5r2。从需求核对到生产部署整整改了三版才稳定。这个项目表面看是典型的增删改查但真正做起来才发现房产中介的业务链路比想象中长房源、客户、带看、成交、佣金五个环节的状态环环相扣随便一处漏了校验后面数据就全乱了。如果你正在规划类似的毕业设计或者小公司管理系统这篇文章可以当一份落地笔记来用。我会把需求拆解、数据库设计、工程结构、核心代码以及上线前后的坑都过一遍很多细节在常规教程里不会写。1. 项目到底在解决什么问题1.1 房产中介的业务闭环我在做需求调研的时候找了三家中介门店的店长聊过。他们的日常工作可以压缩成一条主流程经纪人拿到房源录入系统客户进店登记需求经纪人根据客户预算、区域、户型做匹配匹配成功后预约带看带看满意进入谈价、成交成交后结算佣金。每一步都有对应的数据和状态但很多门店还在用Excel加微信群管理结果就是同一套房源被录了三次客户跟进了几轮也查不到记录佣金算错只能扯皮。系统要解决的核心问题不是做一个漂亮的前端页面而是把这条链路串起来让每一个状态变更都有迹可循。我实际画流程图的时候发现最容易被忽略的是“跟进记录”。客户不会看一次房就签合同中间可能隔两周经纪人有没有跟进、客户最近态度有没有变化这些信息在成交决策中的作用非常大。所以在功能清单里除了标准模块我还单独加了一张跟进记录表每次电话、微信、带看之后都要求经纪人填一条follow记录。这个决定在后期验收时得到店长们的认可因为他们终于不用翻聊天记录来回忆客户情况了。1.2 角色权限怎么划分才不会后期返工权限模型我选的是RBAC也就是用户-角色-权限三层。系统里有三类基础角色管理员、店长、经纪人。管理员管系统配置和账号店长看门店整体数据并可审批房源经纪人只维护自己的房源和客户。权限分配可以用下面这个方式梳理清楚角色核心功能权限数据权限管理员用户管理、系统设置、数据字典全店数据店长房源审核、门店统计、经纪人管理本门店所有经纪人数据经纪人房源维护、客户维护、带看记录、成交申请仅本人创建的数据这里除了菜单权限还要特别注意数据权限。比如店长能看所有经纪人的业绩而经纪人只能看自己的。这个如果不提前设计后面写SQL时很容易出现越权所以我用一个简单方式控制在Service层通过当前登录用户ID拼进查询条件不依赖数据库视图。这样权限过滤和业务查询放在一起排查越权问题非常直观。1.3 为什么这个时间点依然推荐Spring Boot也许有人会问现在前端都流行Vue、React后端是不是该上微服务我个人在中小业务系统里的原则是能用一个工程解决的绝不上微服务。Spring Boot相比传统的Servlet/XML配置式Java Web开发最大优势是自动装配和内嵌Tomcat开发期不需要单独装Tomcat丢个jar就能跑。它通过starter把常用的第三方库集成好比如我们要用的MyBatis-Plus、Redis、MinIO都是加一个依赖再写几行配置就完事。Spring Boot本身是Java Web领域非常成熟的开发脚手架它不新潮但足够稳定团队上手成本低遇到问题也能查到大量资料。选择Spring Boot还有一个隐藏收益它能跟目前的Java面试题紧密结合。无论你是准备校招还是往高级开发走Spring Boot的自动配置原理、starter机制、AOP事务处理都是高频考点。把这套房产中介系统做完面试时可以聊的东西非常多而不是只说“我会用SSM”。2. 从表结构开始先想清楚数据怎么存2.1 核心表与关系概览我建库时没有直接照搬网上开源项目的表而是按业务场景拆。第一版设计是八张表后面砍掉两张合并进主表最终保留以下核心表表名用途关键关联house房源主表归属经纪人user_idcustomer客户主表归属经纪人user_idappointment带看预约表house_id, customer_id, agent_idcustomer_follow客户跟进记录customer_id, agent_iddeal成交订单表house_id, customer_id, agent_idsys_user用户表role_idsys_role角色表无为什么deal和house不合并因为房源可能在成交后又要重新流转比如买方毁约房子要重新上架。独立表才能保留完整的成交历史而且后续要做业绩统计直接从deal表聚合比去house表里翻状态可靠得多。2.2 房源表字段设计房源表是所有业务的基础字段设计直接决定后续查询是否顺畅。我最终使用的建表SQL大致是这样CREATE TABLE house ( id BIGINT PRIMARY KEY AUTO_INCREMENT, house_no VARCHAR(32) NOT NULL COMMENT 业务编号, title VARCHAR(100) COMMENT 房源标题, community VARCHAR(100) COMMENT 小区名称, area DECIMAL(10,2) COMMENT 建筑面积, bedroom TINYINT COMMENT 卧室数量, price DECIMAL(12,2) COMMENT 挂牌价格, decoration VARCHAR(20) COMMENT 装修情况, owner_name VARCHAR(50) COMMENT 业主姓名, owner_mobile VARCHAR(20) COMMENT 业主电话, status VARCHAR(20) DEFAULT PENDING COMMENT PENDING/ON_SALE/OFF_SALE/DEAL, version INT DEFAULT 0 COMMENT 乐观锁版本号, create_by BIGINT COMMENT 创建人ID, create_time DATETIME, update_time DATETIME );house_no是给线下经纪人看的业务编号格式可以做成HS加日期加流水号比如HS202506120001。status字段用varchar存枚举名而不是数字因为可读性好排查线上问题的时候一看到ON_SALE就知道房源在上架状态。version是乐观锁版本号在并发更新时避免互相覆盖这个字段在第四章会详细说怎么用。面积和价格一定要用DECIMAL不要用FLOAT。房产交易涉及金额计算和税费核算浮点数在累乘之后会产生精度偏差虽然误差很小但涉及钱的问题容不得这种意外。2.3 带看预约与客户跟进两张表的设计appointment表是业务链路上最容易出错的地方主要字段包括id、house_id、customer_id、agent_id、appoint_time、confirm_status、visit_status、remark。confirm_status表示预约确认状态visit_status表示实际带看状态这两组状态必须分开。因为预约确认了不代表客户一定到场实际业务中爽约率不低如果混在一个字段里统计到访率的时候就会很痛苦。customer_follow表更简单字段有id、customer_id、agent_id、follow_content、next_time、create_time。不要小看这张表它承担了经纪人的日常工作记录。很多系统只做客户表不做跟进结果就是客户跟了三个月临门一脚要签约时下一个接手的经纪人对前史一无所知。有了跟进记录客户偏好、价格敏感点、看房时间规律都能沉淀成结构化数据。2.4 索引、外键与数据一致性取舍物理外键是很多刚用MySQL的人喜欢的设计但我在这类业务系统里会刻意避免。原因有三个一是物理外键在插入、更新时要额外检查父表高并发场景下会带来性能损耗二是后期如果做分库分表外键约束基本没法迁移三是一旦业务逻辑复杂外键会限制数据流转的灵活性。这个系统所有表关系都用普通索引加代码逻辑保证前提是业务层必须把事务边界控制好。索引方面我加了几个组合索引查询效率提升非常明显ALTER TABLE house ADD INDEX idx_community_status (community, status); ALTER TABLE house ADD INDEX idx_status_time (status, create_time); ALTER TABLE appointment ADD INDEX idx_agent_time (agent_id, appoint_time);组合索引的顺序有讲究等值条件放前面范围条件放后面。比如community是等值查询status也是等值create_time用于范围排序所以idx_status_time比单列索引更合适。appointment表的(agent_id, appoint_time)索引用来快速判断经纪人某个时间段是否已经有带看安排这在第四章的排重逻辑里会用到。3. Spring Boot工程落地结构比代码重要3.1 分层的边界必须画清楚这个项目的前后端没有强制分离管理端页面放在resources/static下接口走/api前缀。工程结构采用的是最常见的分层com.example.estate ├── controller ├── service │ └── impl ├── mapper ├── entity ├── common │ ├── result │ ├── exception │ └── config └── util我见过太多人把写SQL的逻辑放进Controller短项目看着快后面一加需求就崩。正确做法是Controller只负责接收请求、参数校验、调用Service、返回统一结构Service写业务判断和事务控制Mapper只描述数据访问。分层清晰以后就算代码量和项目复杂度翻倍改起来也不会牵连一片。特别是后来客户要求新增一个“门店成交报表”模块我在Service层加一个聚合查询方法就行Controller和Mapper基本没动。3.2 依赖选型和版本避坑pom.xml里的依赖不多但每个都是核心dependency groupIdorg.springframework.boot/groupId artifactIdspring-boot-starter-web/artifactId /dependency dependency groupIdcom.baomidou/groupId artifactIdmybatis-plus-boot-starter/artifactId version3.5.3.2/version /dependency dependency groupIdmysql/groupId artifactIdmysql-connector-j/artifactId scoperuntime/scope /dependency dependency groupIdorg.projectlombok/groupId artifactIdlombok/artifactId optionaltrue/optional /dependency dependency groupIdcom.auth0/groupId artifactIdjava-jwt/artifactId version4.4.0/version /dependency关于MyBatis-Plus和JPA的选择国内做管理系统MyBatis-Plus是主流。它的QueryWrapper能解决大部分动态SQL同时又允许手写XML做复杂查询。JPA虽然封装程度更高但遇到多表关联动态排序时写起来反而不直观。这里有一个很重要的版本坑如果你的Spring Boot版本比较高比如3.x必须用MyBatis-Plus 3.5.3以上的版本否则会存在javax和jakarta包名不兼容的问题启动时直接NoClassDefFoundError。我这次选的是Spring Boot 2.7.x因为服务器JDK还是8兼容性最稳。3.3 配置文件里有多少隐藏坑application.yml是Spring Boot项目里出现频率最高的故障源。我这里给出一份能直接跑起来的核心配置server: port: 8080 servlet: context-path: / spring: datasource: url: jdbc:mysql://localhost:3306/estate?useUnicodetruecharacterEncodingutf8serverTimezoneAsia/ShanghaiuseSSLfalseallowPublicKeyRetrievaltrue username: root password: 123456 driver-class-name: com.mysql.cj.jdbc.Driver jackson: date-format: yyyy-MM-dd HH:mm:ss time-zone: Asia/Shanghai mybatis-plus: configuration: map-underscore-to-camel-case: true log-impl: org.apache.ibatis.logging.stdout.StdOutImpl global-config: db-config: logic-delete-field: deleted logic-delete-value: 1 logic-not-delete-value: 0几个容易踩的坑url必须加serverTimezoneAsia/Shanghai否则数据库时间会比中国时间差8小时MySQL 8.x默认SSL验证可能失败加上useSSLfalse能省事allowPublicKeyRetrievaltrue是为了避免连接MySQL 8时出现Public Key Retrieval is not allowed。逻辑删除配置好后删除房源只是update语句不会真删记录这种设计在数据审计时非常有用。比如店长想恢复一条被误删的房源直接改deleted字段为0就行。3.4 统一响应与全局异常是省事神器前后端接口必须约定统一返回结构否则前端处理逻辑能写出花。我定义了一个Result 字段包括code、message、data。成功返回code0业务失败返回code1登录失效返回code401。Controller中写法统一比如RestController RequestMapping(/api/house) public class HouseController { PostMapping(/create) public ResultLong create(Validated RequestBody HouseCreateDTO dto) { Long houseId houseService.createHouse(dto); return Result.success(houseId); } }配合全局异常处理在common/exception/GlobalExceptionHandler中用RestControllerAdvice捕获BizException和Exception。业务异常类可以在Service层直接throw前端能拿到友好的中文提示后台异常只记录日志不把堆栈暴露给调用方。这个统一响应加全局异常的模式熟练以后每次新模块开发都能节省大量时间错误提示也会保持一致的风格。4. 核心业务代码不是增删改查那么简单4.1 房源审核与状态机控制房源创建后默认是PENDING待审核状态店长审核通过才能上架。审核这段逻辑看着简单但坑在状态校验。我第一版代码是直接update status后来测试发现只要连续点两次审核状态就变成已上架再变成已上架没有防护。正确做法是先查出来判断状态只有PENDING才允许流转。核心代码Transactional public void reviewHouse(Long houseId, Boolean approved) { House house houseMapper.selectById(houseId); if (house null) { throw new BizException(房源不存在); } if (!PENDING.equals(house.getStatus())) { throw new BizException(当前状态不允许审核); } house.setStatus(Boolean.TRUE.equals(approved) ? ON_SALE : REJECTED); int rows houseMapper.updateById(house); if (rows 0) { throw new BizException(房源已被其他操作修改请重试); } }updateById返回值在MyBatis-Plus中默认是影响行数。这里其实用了乐观锁version字段如果更新时版本不一致影响行数会是0从而阻止并发覆盖。这比在应用层用synchronized可靠因为多实例部署时单机锁根本没有作用。把状态机判断放在Service层最前面也是让后来人一眼看懂业务规则的做法。4.2 客户需求匹配与推荐排序很多教程喜欢在房产系统里堆协同过滤、推荐算法实际上对中小中介来说客户的需求条件就那么几个区域、预算、户型、面积。做精准匹配比做推荐更实在。用MyBatis-Plus QueryWrapper拼接条件public ListHouseVO matchHouses(CustomerQuery query) { LambdaQueryWrapperHouse wrapper Wrappers.lambdaQuery(House.class) .eq(House::getStatus, ON_SALE) .eq(StringUtils.isNotBlank(query.getCommunity()), House::getCommunity, query.getCommunity()) .le(query.getMaxPrice() ! null, House::getPrice, query.getMaxPrice()) .ge(query.getMinArea() ! null, House::getArea, query.getMinArea()) .orderByDesc(House::getCreateTime); return houseMapper.selectList(wrapper).stream() .map(house - convertToVO(house, query)) .toList(); }这里要用LambdaQueryWrapper避免手写字符串拼接条件带来的SQL注入风险。条件用eq、le、ge这些结构化操作可读性也强。排序先按最新录入的房源优先。如果后续想更智能可以在内存里给每条房源加一个匹配评分比如预算差越少得分越高然后按评分排序但这个版本先不引入复杂算法。4.3 带看排重同一套房源同一天不能重复带看房产中介带看有个强规则一套房子在同一个时间段只能带一个客户去看。如果两个经纪人同时预约同一套房源就会造成两个客户在家门口碰面的尴尬。设计时不能只靠前端按钮禁用后端必须控制并发。我这里用了两个手段一是在appointment表对house_id和appoint_time加联合唯一索引二是Service层先做查询校验然后insert捕获DuplicateKeyException再提示用户。索引SQLALTER TABLE appointment ADD UNIQUE KEY uk_house_time (house_id, appoint_time);为什么还要先查询校验因为索引冲突产生的异常提示不友好直接交给全局异常处理前端看到的是“系统繁忙”用户完全摸不着头脑。先查一次如果存在就抛出“该房源此时间段已有带看预约”这样用户能立刻明白原因。注意在高并发下查询和插入之间仍可能被插入数据所以唯一索引是最后的兜底两者配合才是完整方案。同理如果要做经纪人时间冲突校验再加一个(agent_id, appoint_time)唯一索引即可。4.4 佣金计算与事务边界成交模块的佣金计算很容易出错。以一套500万的房子为例假如规则是成交价100万以下部分收1.5%100万到500万部分收1%超过500万部分收0.5%。这个分段计算不能写成一刀切比例必须按区间累计。会在工具类里写一个分段计税方法输入成交价和费率配置输出佣金总额。成交过程还涉及多张表变动deal表新增、house状态改为已成交、customer状态改为已成交、经纪人业绩表累加。要么全部成功要么全部回滚所以Service方法上要加Transactional。这里有两个非常典型的坑一是事务方法必须public且不能是通过this调用的内部方法否则事务注解失效二是不能把整段逻辑包在try-catch里吞掉异常否则事务无法感知。佣金结算场景频率低并发冲突不多所以在成交时可以使用selectForUpdate悲观锁锁定deal记录保证价格不会同时被多个流程修改。5. 上线前后的常见坑与排查方法5.1 jar包还是war包部署选型别踩坑Spring Boot项目默认使用spring-boot-maven-plugin打成可执行的jar包内嵌的Tomcat会在启动时加载。这种方式最方便交付时给一个jar文件服务器上只要有JDK就能跑不管是Linux还是Windows都好使。但有些客户的运维环境要求把应用放到外部Tomcat的webapps目录里这时必须改三处pom.xml的packaging改成war启动类继承SpringBootServletInitializer并重写configure方法再把spring-boot-starter-tomcat的scope设为provided。启动类改成这样SpringBootApplication public class EstateApplication extends SpringBootServletInitializer { Override protected SpringApplicationBuilder configure(SpringApplicationBuilder builder) { return builder.sources(EstateApplication.class); } public static void main(String[] args) { SpringApplication.run(EstateApplication.class, args); } }注意打war包后外部Tomcat版本最好在9以上否则Servlet API版本和Spring Boot不兼容。有人问怎么将Spring Boot jar反编译成项目这种情况经常出现在别人给一个打好的包需要在此基础上改需求。反编译工具一般就是CFR或jd-gui但只能还原出可读代码的大致逻辑注解和配置文件会丢失不建议当作工程恢复的手段至少不要指望反编译后能直接编译通过。5.2 静态资源、跨域和前端联调管理端页面放在src/main/resources/static目录下Spring Boot默认把/映射到static所以直接访问http://localhost:8080/index.html即可。如果接口和页面在同一个域名端口下就不存在跨域问题。但如果你把前端项目单独跑在8081端口接口在8080就会出现跨域需要配置一个CORS过滤器Configuration public class CorsConfig implements WebMvcConfigurer { Override public void addCorsMappings(CorsRegistry registry) { registry.addMapping(/**) .allowedOriginPatterns(*) .allowedMethods(GET, POST, PUT, DELETE) .allowedHeaders(*) .allowCredentials(true) .maxAge(3600); } }本地联调强烈建议加上spring-boot-devtools依赖改完代码自动重启比手动重启省太多时间。但生产环境不要打包这个依赖避免产生不必要的监控进程和内存开销。5.3 常见问题速查表我把这个项目开发过程中遇到的高频问题整理成表格方便查阅现象可能原因解决办法启动报端口占用8080被占用lsof -i:8080杀掉旧进程或改server.port数据库时间慢了8小时URL缺serverTimezone加serverTimezoneAsia/Shanghai插入中文乱码数据库编码不是utf8mb4库表统一utf8mb4URL加characterEncodingutf8分页不生效没有注册MybatisPlusInterceptor新建配置类添加分页插件Bean访问静态页面404文件没放到static根目录放到classpath:/static/下登录后接口返回401JWT过期时间太短或secret泄露设置较长的过期时间并更换复杂secret查询超时慢缺索引或深分页检查执行计划补组合索引这些坑多数不是技术难点但每个都会浪费半天时间。开发完成后把排查经验沉淀到项目文档里交付的时候客户和队友都会感谢你。5.4 安全加固与后续扩展思路安全方面用户密码必须用BCrypt加密不要用MD5MD5可以逆向撞库。接口统一走JWT拦截器白名单只放login、register这类公开接口。参数校验用Validated配合实体类上的NotBlank、Max、Min等注解避免出现非法输入。MyBatis-Plus的#{}参数占位是预编译的天然防SQL注入但如果你在某个查询里用了${}一定要手动过滤关键字比如剔除空格和注释符。系统上线后可以考虑继续扩展房源图片和附件用MinIO对象存储把文件服务从应用里拆出去客户在带看后希望接收提醒可以用Spring Boot集成WebSocket推送站内信如果门店规模扩大带看排重也可以从数据库唯一索引升级到Redis分布式锁。这些扩展不需要推倒重来因为当前的分层结构给后续留了足够空间。这个项目做完我自己最大的体会是房产中介管理系统的开发难点从来不在写多高深的算法而在把业务状态和数据一致性稳稳地控制住。带看状态、审核状态、成交状态每一条流转都值得用状态机去设计而不是拍脑袋写一堆if else。如果你准备做类似项目我建议先把表结构和状态流转画清楚再动手写代码。最后再分享一个小技巧我习惯在每个Service方法里把业务规则校验写在最前面如果当前状态不符合就抛出BizException。这样后来接手的人看逻辑时先看前面几行校验基本就能明白这段代码的意图。系统结构清晰比多写几个分页查询重要得多。
阅读完成 · 觉得有帮助?
咨询建站