基于Spring Boot的房产中介管理系统:从数据库建模到权限管理实战解析
做毕设选了Java房产中介管理系统这个方向我一开始觉得不就是个CRUD嘛把房源、客户、合同几个表建好写几个增删改查就完事了。真正动手才发现房屋租赁和销售业务混在一个系统里牵扯到的状态流转、权限划分、数据统计远比想象中复杂。这篇文章把我从需求分析到最终答辩走过的路完整梳理一遍重点讲清楚系统设计时的核心决策、数据库怎么建模、几个关键模块的实现思路以及我踩过的那些坑。不管你是准备拿这个题目做毕设还是工作中要接手类似的中介业务系统这篇应该都能给你省不少时间。1. 项目全景解读这套系统到底要解决什么问题1.1 核心需求解析租售一体的业务复杂度房产中介的业务链条其实很长。租房的流程是房源录入、房源核验、客户登记、带看记录、意向沟通、签约合同、后续收租管理。卖房的流程类似但多了产权核验、价格谈判、过户手续、贷款协同这些环节。一套系统如果只做简单的房源展示和客户登记根本没法真正用起来。我当时做的这个“广厦房屋中介管理系统”核心定位就是租售业务一体化管理。它要同时支撑租赁和销售两条业务线并且两条线共用一套房源池、客户池和员工账号体系但各自又有独立的业务流程和状态机。具体拆解下来系统要覆盖这些核心诉求房源全生命周期管理从业主委托录入到审核上架、带看预约、成交下架租赁房源和销售房源要能区分管理还要支持“租转售”、“售转租”这种业务场景。客户资源池化管理求租客和求购客按需求类型分组客户跟进记录要留痕防止业务员之间抢单。合同与交易闭环租赁合同要管押金、租金、租期、付款方式销售合同要管总价、首付、尾款、过户日期。合同状态要跟房源状态联动。员工角色与权限管理员、店长、经纪人三种角色经纪人只能看自己的客户和房源店长能看全店数据管理员负责系统配置和账号管理。数据看板租售成交量、佣金收入、房源去化周期、客户转化率这些核心指标要能按时间范围统计展示。1.2 目标用户与使用场景这个系统的使用场景要区分清楚。管理员和店长在PC端做配置和数据决策经纪人主要在业务操作界面处理日常事务。我设计的时候刻意避免了移动端适配的坑——毕设阶段做响应式页面工作量太大我直接按PC端业务系统来做浏览器访问不做手机端适配。答辩的时候老师问起来我就说“该系统定位为门店内网业务系统不对公网开放”理由充分工作量也省了。1.3 系统边界做什么和不做什么明确系统不做的事情同样重要。我这个版本没有做在线支付、没有做电子合同签章、没有做短信通知对接。不是做不了而是毕设项目的核心是展示你对业务建模、架构设计、编码实现的综合能力这些外围功能每多一个就要多一倍的联调和测试成本做不好反而扣分。把租售流程的核心闭环做扎实远比分摊精力做一堆半成品功能强。2. 技术选型与架构设计为什么是这套组合2.1 技术栈清单与选型理由我用的是纯Java技术栈加前端模板引擎的方案没有拆前后端分离。具体选型如下表层级选型选型理由后端框架Spring Boot 2.7.x起步快、约定优于配置、内置Tomcat部署简单毕设神器ORM框架MyBatis Plus单表CRUD不用手写SQL多表关联用注解和XML区分处理简洁灵活数据库MySQL 8.0免费、资料多、面试常问学校机房环境也常见前端Thymeleaf Bootstrap 5 jQuery服务端渲染不用跨域配置Bootstrap组件现成界面规范权限控制Spring Security JWT无状态鉴权REST风格接口答辩时讲权限设计有亮点开发工具IntelliJ IDEA Maven Git业界标准组合团队协作和版本管理都方便数据库管理Navicat / DBeaver可视化操作快速调试SQL有人可能会问为什么不选Vue前后端分离我当时权衡了很久。前后端分离方案确实看起来更高端但引出的问题特别多跨域配置、Token存储、接口文档维护、Nginx部署任何一个环节出问题足以消耗掉你两周时间。而服务端渲染方案一个Templates目录加几个Controller就能把页面串起来数据填充逻辑直观出错排查也容易。关键是答辩时老师更关心你怎么用技术解决业务问题而不是技术本身多新。2.2 项目包结构与分层思想我采用的是经典的四层架构Controller层接收参数和返回视图、Service层处理业务逻辑、Mapper层负责数据访问、Model层放实体类和DTO。包结构如下com.guangsha ├── controller # 控制层 │ ├── admin # 后台管理端接口 │ ├── agent # 经纪人端接口 │ └── common # 公共接口登录、数据字典 ├── service # 业务层接口 │ └── impl # 业务实现 ├── mapper # 数据访问层 ├── entity # 数据库实体 ├── dto # 传输对象 ├── vo # 视图对象 ├── config # 配置类Security、WebMvc等 ├── utils # 工具类JwtUtils、DateUtils等 └── common # 通用返回结果、异常处理、常量这个分层要硬性坚持一条规则Controller里不写业务逻辑Service里不出现SQL语句。我见过很多同学的代码Controller里直接调用Mapper查询看起来省事但业务一旦复杂就完全失控。比如“房源下架”这个动作你先要校验房源状态是不是“已出租/已出售”再判断有没有未履约的合同最后把房源状态改了还要同步关掉所有带看预约。这段逻辑放Controller里代码会膨胀得很难看放Service里就是三行方法调用清晰明了。2.3 Spring Security JWT这套组合怎么落地登录认证这块我踩了不少坑。用Session方案其实是常规做法但我选择了JWT方案不是因为它复杂而是答辩时候有东西讲。核心逻辑是这样的用户登录成功后后端把用户ID、角色、登录时间这些信息放进JWT令牌里返回给前端。前端把令牌存在localStorage每次请求在Header里带上。后端用Spring Security的过滤器链拦截请求从Header解析令牌验证身份。// JWT工具类核心方法 public String generateToken(Integer userId, String role) { return Jwts.builder() .setSubject(String.valueOf(userId)) .claim(role, role) .setIssuedAt(new Date()) .setExpiration(new Date(System.currentTimeMillis() EXPIRE_TIME)) .signWith(SignatureAlgorithm.HS256, SECRET_KEY) .compact(); }初始化BearerTokenFilter来实现认证逻辑public class JwtAuthenticationFilter extends OncePerRequestFilter { Override protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain chain) throws ServletException, IOException { String token request.getHeader(Authorization); if (StringUtils.hasText(token) token.startsWith(Bearer )) { String realToken token.substring(7); // 解析校验 } chain.doFilter(request, response); } }这里有一个血泪教训JWT密钥不要硬编码在代码里要放到application.yml配置文件中提交Git时记得排除。有的同学把SECRET_KEY就写死在类里别人拿到代码就能伪造任意身份登录这属于必须避免的低级安全问题。3. 数据库设计整套系统最核心的部分3.1 核心表结构解析数据库设计是我整个项目花时间最多的地方。我前后重构了三版最终保留了11张核心表。这里按业务域分组讲解。用户权限域sys_user用户表。字段包括id、用户名、密码BCrypt加密存储、真实姓名、手机号、角色ID、门店ID、状态启用/禁用、创建时间。sys_role角色表。预置三种角色管理员、店长、经纪人。房源域house房源主表。字段包括id、房源编号、产权人姓名、产权人电话、所在小区、楼栋号、单元号、门牌号、建筑面积、户型几室几厅、朝向、楼层、装修情况、租金/售价、出租状态、出售状态、房源类型租/售/租售均可、审核状态、录入人ID、录入时间。house_attach房源附属信息表。存放图片地址、房源描述、配套设施标签等。拆这张表是为了避免主表字段过多也方便后期扩展视频看房等新功能。house_follow房源跟进记录表。记录经纪人对房源的每次回访、价格调整、状态变化形成房源历史轨迹。客户域customer客户表。字段包括id、姓名、手机号、客户类型求租/求购/租购均可、需求户型、预算范围、意向区域、来源渠道网络/门店/朋友介绍、状态、所属经纪人ID、创建时间。customer_follow客户跟进记录表。每次电话、微信、带看后的沟通结论形成一个时间线。交易域contract_lease租赁合同表。合同编号、房源ID、客户ID、经纪人ID、起租日期、结束日期、月租金、押金、付款方式、水电表底数、合同状态。contract_sale销售合同表。合同编号、房源ID、客户ID、经纪人ID、成交总价、首付款、尾款、尾款截止日、过户日期、合同状态。view_record带看记录表。房源ID、客户ID、经纪人ID、带看时间、客户反馈满意/不满意/考虑中、备注。系统管理域dict_type、dict_data数据字典表。用来维护小区列表、户型选项、装修档次这些可枚举的业务选项避免硬编码在代码里。operation_log操作日志表。记录谁在什么时间做了什么操作我用来做项目亮点的格式为“用户【张三】在【2024-05-20 14:32】将房源【H202405001】状态从【在售】变更为【已锁定】”。3.2 房源状态机设计租售联动的关键这是整个数据库设计中最容易出错的地方。我开始真就设计了两个独立的字段出租状态和出售状态。后来发现问题来了一套房源可以同时挂出租和出售但“租出去了还能不能卖”这种业务规则怎么控制所以我设计了这样一套状态机房源实例有一个主状态字段house_status取值包括待审核、已上架、已锁定、已下架、已成交、已失效。租售状态用两个独立标记字段rent_status未出租/已出租和sale_status未出售/已出售来描述。“已成交”是终态一旦进入不可逆而“已锁定”是业务中间态比如客户交了定金房源暂时冻结交易但随时可能解冻。租售联动的核心逻辑在Service层通过事务控制Transactional public void dealHouse(DealRequest request) { // 1. 校验房源状态是否允许成交 // 2. 创建对应的合同记录 // 3. 更新房源状态 // 4. 更新客户状态从有购房需求变为已购房 // 5. 记录操作日志 }这里必须加事务注解否则中间任何一步失败数据就彻底不一致了。比如合同建好了房源状态没更新成功第二天房源还能被另一个客户预订这种bug在答辩演示时被老师抓到印象分直接崩盘。3.3 典型查询SQL设计房源列表页的模糊查询这个SQL我觉得值得分享一下。项目里房屋管理既要按小区搜、又要按价格区间过滤还要按户型筛选我用MyBatis Plus的条件构造器来做效率比手写动态SQL简单得多。public PageHouseVO queryHousePage(HouseQuery query) { LambdaQueryWrapperHouse wrapper new LambdaQueryWrapper(); wrapper.eq(StringUtils.isNotBlank(query.getHouseType()), House::getHouseType, query.getHouseType()) .like(StringUtils.isNotBlank(query.getCommunity()), House::getCommunity, query.getCommunity()) .ge(query.getMinPrice() ! null, House::getRentPrice, query.getMinPrice()) .le(query.getMaxPrice() ! null, House::getRentPrice, query.getMaxPrice()) .between(query.getMinArea() ! null query.getMaxArea() ! null, House::getArea, query.getMinArea(), query.getMaxArea()) .eq(StringUtils.isNotBlank(query.getStatus()), House::getSaleStatus, query.getStatus()) .orderByDesc(House::getCreateTime); return houseMapper.selectPage(new Page(query.getCurrent(), query.getSize()), wrapper); }MyBatis Plus用起来虽然省事但遇到超过三张表关联的统计查询还是老老实实写XML里的联表SQL更清晰。统计成交量的时候需要把合同表、房源表、客户表、员工表四张表关联起来用条件构造器硬拼接反而难读。4. 核心功能模块的实现细节4.1 登录认证与权限拦截登录模块我做了图形验证码虽然很多人觉得毕设不需要这个但做过你就知道加验证码能堵住一个非常尴尬的演示场景——老师拿着测试账号连续输错密码。图形验证码的实现逻辑不复杂用一个工具类生成图片把验证码字符串存到Redis有效期两分钟输入正确就清除。权限控制这块我在Spring Security的配置类里做了接口级别的访问控制。配置代码authorizeRequests() .antMatchers(/admin/**).hasRole(ADMIN) .antMatchers(/manager/**).hasAnyRole(ADMIN, MANAGER) .antMatchers(/agent/**).hasAnyRole(ADMIN, MANAGER, AGENT) .antMatchers(/login, /captcha, /logout).permitAll() .anyRequest().authenticated()这样做权限控制的好处是接口层面的硬性拦截。就算某个按钮在前端被藏起来了用户只要知道接口地址直接访问后端也会拒绝。这一点是我答辩时重点讲解的部分。4.2 房源管理字段校验与状态联动房源录入是这个系统里表单最复杂的一个模块。我第一次做的时候把所有字段堆在一个页面上二十多个输入框实测下来用户根本填不完而且报错信息满天飞。优化后的方案是把录入流程拆成三步第一步基本信息包括户型、面积、朝向、楼层、装修档次。第二步价格与委托信息租金、售价、委托方式、委托到期日。第三步图片与描述支持多图上传和文字描述。房源价格这里有一个业务细节住宅租金和售价都是整数但商业房源偶尔会带小数点。我在后台直接用BigDecimal类型存储前面加校验金额必须大于0且最多两位小数。字段校验我用了JSR 303这套规范在实体类上加注解NotBlank(message 小区名称不能为空) private String community; NotNull(message 建筑面积不能为空) DecimalMin(value 1.0, message 建筑面积必须大于0) private BigDecimal area; NotNull(message 价格不能为空) DecimalMin(value 0.01, message 价格必须大于0) private BigDecimal rentPrice;在Controller层加Validated注解后再接收参数Spring就会自动校验并且把错误信息返回给前端。前端再弹窗提示整个交互体验就规范了。4.3 租赁合同与销售合同一个容易混淆的设计点租赁和销售合同虽然都是合同但字段差异很大。租赁合同要管租期、月租、押金、付款方式销售合同要管总价、首付、贷款的付款节点。如果硬塞到一张表里大量字段会空着既不规范也不优雅。我选择拆成两张表但在Service层做了策略模式的基类来统一处理合同创建和审核流程。我先定义了一个合同处理接口public interface ContractProcessor { Integer getContractType(); Long createContract(ContractCreateRequest request); void auditContract(Long contractId, boolean approved, String remark); String getContractStatus(Long contractId); }租赁和销售各自实现一个处理器Service层根据请求中的类型字段来选择对应的实现。这个设计模式在答辩时是加分项因为老师很看重代码的可扩展性——以后如果新增“车位出租”业务只需要再写一个处理器实现类完全不需要改动现有代码。4.4 带看预约与动态看房单带看记录这个模块容易被低估它是连接房源和客户的关键桥梁。我设计了预约、确认、完成、评价四个阶段预约经纪人选择房源和客户设定带看时间。确认系统检查该房源在指定时间段是否有其他带看有冲突则提醒。完成经纪人标记带看已结束录入客户反馈。评价客户反馈生成回访建议。这个功能的核心是时间冲突检测。写SQL的时候要注意不能简单比较日期要按时间段交叉来判断。比如房源A在周六下午3点到5点有一个带看新预约的时间是和这个时间段有交集的话就要弹冲突提示。SELECT COUNT(*) FROM view_record WHERE house_id #{houseId} AND view_date #{viewDate} AND status IN (预约, 确认) AND ((start_time #{endTime} AND end_time #{startTime}))4.5 数据统计看板的实现数据看板是兼顾视觉和业务分析的部分。我用了ECharts来画图折线图展示近30天成交量变化柱状图展示各门店业绩对比饼图展示房源类型占比。数据来源是Mapper层的聚合查询SQL。月度佣金统计的核心SQLSELECT DATE_FORMAT(create_time, %Y-%m) AS month, SUM(commission) AS total_commission FROM contract_sale WHERE contract_status 已成交 GROUP BY DATE_FORMAT(create_time, %Y-%m) ORDER BY month DESC这里要提醒一点ECharts的初始化需要容器有明确的宽高。我遇到过图一直显示不出来的情况最后发现是div容器高度被Bootstrap样式给覆盖成了0。解决方法是给图表容器写内联样式或者单独定义样式表优先级更高。5. 实操部署与运行调优5.1 本地开发环境一键启动项目的启动步骤很简单前提是环境装好# 1. 创建数据库 mysql -uroot -p sql/guangsha.sql # 2. 修改配置文件 vim src/main/resources/application.yml # 数据库地址、账号密码、Redis地址 # 3. Maven编译打包 mvn clean package -DskipTests # 4. 启动Java进程 java -jar target/guangsha-system.jar如果是在IDEA里跑的化更简单直接运行主类不需要打包这一步。但为了答辩方便我当时还是会打包成jar放到服务器上跑一下这样能展示“该系统的生产和开发环境行为一致”这个优点。5.2 初始化数据脚本的编写技巧初始化数据这块很多人会忽略。你想象一下答辩时打开系统里面空荡荡的什么数据都没有老师一眼看过去全空白观感极差。我专门写了一个初始化SQL脚本里面造了30条房源、20条客户、5个经纪人账号、若干条合同和带看记录。数据要造得足够真实——小区名、价格、面积、朝向都要合理。比如某小区二手房挂牌均价在每平米2万到3万之间录入的房源价格就不能出现离谱值不然老师扫一眼价格就知道是瞎编的。管理员账号也要写在初始化脚本里-- 密码是123456的BCrypt密文 INSERT INTO sys_user (username, password, real_name, role_id, status) VALUES (admin, $2a$10$X7vTuh0y3EFaZcV0kp3x0.6J9VmP3QfXrCUDRzVDBz7Z0q0rQ3o0m, 系统管理员, 1, 1);注意数据库密码不要用明文存直接用BCrypt加密后的值。这样的话项目代码里也不会出现任何明文密码更具有专业性。5.3 性能调优从启动报错到流畅运行这个项目在低配置电脑上跑也是可以的。但我遇到了一个典型的启动很慢的问题。分析下来发现是MyBatis Plus的Mapper XML文件里写了大量无用的SELECT *查询。适当优化后把列表页的分页查询改成只查必需字段启动速度明显提升。另外要重点优化的是房源列表页的图片加载。我刚开始设计时房源图片全部用数据库二进制存储一张图片几兆B查询全表的时候把所有图片都读出来结果列表页直接卡死。后来改成图片存磁盘路径数据库只存相对路径访问时通过Nginx或者Spring的静态资源映射来读取。这样列表查询的负载大幅降低。静态资源映射配置Override public void addResourceHandlers(ResourceHandlerRegistry registry) { registry.addResourceHandler(/upload/**) .addResourceLocations(file: uploadPath /); }5.4 Redis缓存的使用场景有人觉得Redis是加分项我就把验证码、JWT黑名单、热门房源缓存都用Redis来做。其实毕设阶段适当的用一两个场景就好不要过度设计。我只在房源详情页加了缓存首次查询时把数据放进Redis有效期10分钟之后同一房源的请求直接走缓存。因为房源详情是访问量最大的接口缓存命中率能到90%以上效果明显。缓存更新时机要盯着房源信息一旦被修改必须立刻删除对应缓存否则用户看到的还是旧价格会造成很严重的业务误会。6. 常见问题排查与避坑实录6.1 Spring Security放行请求永远404这个问题几乎每个用Spring Security的同学都会遇到。我把登录接口配了permitAll()但访问时还是返回404。排查了半天发现是Security放行的路径和Controller的路由不一致。比如Controller里的路径是/api/loginSecurity配置里写的/login当然放行不了。排查方法很简单把Security的放行规则跟Controller的路由表单独打印出来对照一眼就能看出问题。加上debug日志logging: level: org.springframework.security: DEBUG日志会输出每个请求匹配的过滤器链和拒绝原因非常好用。6.2 数据库中文乱码MySQL数据库出现中文乱码九成是连接URL没设置编码导致的。在application.yml中写url: jdbc:mysql://localhost:3306/guangsha?useUnicodetruecharacterEncodingutf8serverTimezoneAsia/Shanghai另外建库的时候也要指定utf8mb4字符集CREATE DATABASE guangsha DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci;utf8mb4和utf8的区别在于前者能存emoji表情和一些特殊字符系统如果涉及客户备注里带emoji的情况用utf8mb4最稳妥。6.3 事务不生效的隐蔽原因我的合同删除接口用的是Transactional注解测试发现删除一半的时候模拟异常数据没有被回滚。后来排查才发现Spring的声明式事务是通过AOP代理实现的而代理默认采用JDK动态代理目标是接口时才能工作。我的Service实现类没走接口直接是Class导致代理失效。有三种方案可以解决方案一给Service类定义接口实现类实现接口这是最常见也是推荐的做法。方案二把Transactional加在Controller调用的最外层方法上但这样粒度太粗容易把查询也包进事务里。方案三在配置类开启EnableTransactionManagement查看代理模式或者切换成CGLIB代理模式spring.aop.proxy-target-classtrue。我给出的建议是别偷懒老老实实定义接口。代码层次清晰也是答辩加分项。6.4 JWT登录后用户信息丢失JWT有个特点是服务端不存储状态用户信息完全靠客户端每次请求时在Header中携带。我用JWT解析出userId后每次请求都要重新查数据库拿用户对象导致接口响应变慢。后来我引入了ThreadLocal来缓存当前请求的用户信息。过滤器里解析完JWT就把用户对象放到ThreadLocal里Controller和Service层直接从ThreadLocal取。请求结束时要记得清理ThreadLocal否则线程池复用线程的时候下一个请求会读到上一个请求的用户数据出现用户串号这种非常严重的错误。6.5 前端时间格式化JSP或者Thymeleaf页面展示时间的时候默认输出格式是2024-05-20T14:32:11这种带T的ISO格式非常丑。我在定义VO对象的时候给时间字段加了JsonFormat注解JsonFormat(pattern yyyy-MM-dd HH:mm:ss, timezone GMT8) private LocalDateTime createTime;这样返回到前端的JSON就变成了2024-05-20 14:32:11简洁美观。后端用的LocalDateTime类型记得在配置里加jackson-datatype-jsr310依赖否则序列化会报错。7. 系统扩展与后续演进方向项目做完之后我复盘了一下这个系统如果要往生产级别去演进有三个方向值得投入第一个方向是工作流引擎的引入。现在合同审批、房源审核这些流程逻辑都写死在Service里一旦审批节点增加比如新增一个区域总监审批环节就要改代码。引入工作流引擎后流程配置可以做成可视化业务人员自己就能调整流程。第二个方向是消息通知的异步化。客户签约、房源状态变化、带看提醒这些事件目前只能登录系统后才看到。如果引入消息队列或者WebSocket服务就能实现签约完成后实时推送通知到相关人员的站内消息中心体验会好很多。第三个方向是数据分析的深度挖掘。当前统计看板只做了基础的聚合报表能进一步细分维度例如对客户的购买力做分层、对房源的去化效率做排名预警、对经纪人的转化率做漏斗分析。这些分析功能用简单的SQL聚合是支撑不起来的需要引入更专业的数据分析库。我在实际做这个项目的过程中最大的体会是毕业设计也好、工作中的小型业务系统也好核心从来不是把某个框架用得多熟练而是能不能真正理解业务并且把业务规则准确翻译成数据结构和代码逻辑。房源状态机设计得好后面所有功能都顺畅状态机模糊后续每个模块都在缝缝补补。我给后面做这个题目的同学一个建议动手写代码之前先把房源租售状态流转图画明白把数据库表关系列清楚这部分投入的时间会在后面全部赚回来。如果你在做这个系统过程中遇到了我上面说的某个问题按我写的排查思路走一遍大部分都能直接解决。