资讯动态

SpringBoot+Vue+MyBatis实现房产销售管理系统全解析

发布时间:2026/10/21 12:31:01 来源:尧图企业网站定制
我见过不少开发团队拿这类系统做内部培训项目也见过很多毕业生拿它当毕设。原因很直接房产销售管理系统的业务链条够长——楼盘、房源、客户、跟进、成交、回款每一环都有数据关联和状态流转比单纯的学生管理系统有技术含量但同时它的并发压力远低于电商秒杀那种场景非常适合用来打磨 SpringBoot Vue MyBatis 这一套前后端分离的工程落地能力。这篇文章就围绕这个系统的完整实现展开。如果你正准备自己从零搭建一套或者拿到源码想二次改造希望能帮你省掉不少弯路。1. 为什么“房产销售管理系统”恰好是这类项目最稳的选型先说一个经常被忽略的结论同类型管理系统里房产销售领域的业务模型最接近企业级真实项目。原因有三点第一业务角色足够立体。销售顾问、销售经理、运营人员、系统管理员四类角色对数据权限和操作权限的要求完全不同。销售只能看自己的客户和跟进记录经理能看整个团队的业绩漏斗运营要维护楼盘字典和价格策略管理员负责账号和权限分配。这个天然的权限分层正好用来设计 RBAC基于角色的访问控制模型而不是像简单系统那样一个 admin 字段打天下。第二对象之间的关联关系复杂但又有规律。一个客户可能看了多个楼盘一个楼盘有多套房源一次成交要关联客户、房源、销售、合同、回款计划五张表。这种“一对多”“多对多”的数据关系做数据库设计时能真正用上外键逻辑和中间表思路。比起图书管理系统那种单线条 CRUD有营养得多。第三业务状态机非常典型。房源状态在售、预定、已成交、下架客户状态新客户、意向中、谈判中、已成交、流失合同状态草稿、待审、已签约、已回款。这些状态之间的流转规则写代码时能很好地锻炼枚举设计、状态校验和业务流程控制能力。所以你会发现完成这样一个系统后稍作调整就能迁移到其他行业——比如二手车进销存、招商租赁管理、甚至教育培训的学员合同管理。底层架构完全不用动。技术栈选型同理。SpringBoot 解决了 Spring 时代 XML 配置繁琐的问题Vue 把页面交互和数据绑定的成本降了一大截MyBatis 则在复杂 SQL 场景下保留了灵活性和可控性MySQL 则完全满足这类业务系统的存储压力。这四样东西放一起是当前中小型企业管理后台里出现频率最高的组合没有之一。2. 整体架构与功能模块拆解从销售流程反推系统设计拿到项目源码第一件事应该是先看目录结构。我的建议是不要按包名顺序从上往下看而是按销售流程去理解每个包为什么存在。2.1 前后端分离的工程结构后端采用经典分层结构controller接收请求参数校验返回统一响应service业务逻辑层处理事务和状态流转mapperMyBatis 数据访问层接口 XML 映射entity数据库表的实体映射common统一响应、异常处理、工具类config拦截器、跨域、静态资源等配置前端 Vue 工程结构src/api按模块封装的请求接口src/views页面组件src/router路由表src/store登录态、用户信息等全局状态src/utilsaxios 实例、工具函数这套目录划分几乎成了行业默契。它的核心思路是每个包只做一类事改动一个需求时能快速定位文件位置。2.2 功能模块怎么划分最合理按照房产销售实际业务线系统通常会拆成六个模块模块核心功能涉及的关键数据系统管理用户、角色、菜单权限user / role / menu楼盘房源管理楼盘维护、房源录入、上下架building / house客户管理客户登记、分配归属、状态更新customer跟进管理跟进记录、下次跟进提醒follow_record交易管理意向单、合同签署、审核流程contract回款与统计回款登记、业绩统计、销售漏斗payment / report其中最容易做歪的是“客户分配”。很多初版系统会把客户直接绑定在销售名下用一个owner_id字段搞定。但实际业务里存在“公池客户”这个概念——未分配的客户属于公池销售可以从公池中领取经理可以把长期未跟进的客户重新收回公池。代码实现时需要区分客户表的status公池/已分配和owner_id归属人两个独立字段缺一不可。2.3 权限模型为什么用 RBAC 而不是直接给用户打标签直接给用户一个角色字符串比如role admin写判断时确实简单。但只要需求变成“经理可以查看团队业绩但不能编辑房源价格”你就得改代码加 if 判断。而基于 RBAC 设计这些需求全部可以通过数据配置完成不需要动一行代码。RBAC 的核心表就三张用户表、角色表、菜单表再配合用户角色关联表和角色菜单关联表。登录后查询当前用户的菜单列表和按钮权限标识前端动态生成路由后端通过拦截器校验接口权限。源码里如果这五张表都齐全权限体系就算完整了。3. 数据库设计房源、客户、跟进、成交、回款五条主线的落库逻辑数据库是整个系统的基础。源码中sql目录下的建表脚本值得逐行看懂。这里挑几张核心表说说不容易理解的设计点。3.1 房源表状态与冗余字段房源表是整套系统里字段最多的表之一。除了基本的楼栋、房号、面积、朝向、单价、总价还有几个容易忽略的设计status字段在售、预定、已成交、下架。成交之后的房源不能再被其他客户预定这个逻辑在后端下单接口里必须用事务加行锁控制。building_id关联楼盘表。之所以不把楼盘信息直接冗余进房源表是因为楼盘档案地址、栋数、物业费只维护一份房源通过外键引用。数据字典字段用tinyint存储例如朝向 1东、2南、3西、4北。好处是数据库体积小、查询条件简洁坏处是可读性差所以 MyBatis 返回时要通过字典翻译成中文。3.2 客户表用户归属与去重问题客户表的几个关键设计点owner_id为 NULL 时表示该客户在公池中任何人可领取。phone字段必须加唯一索引。房产销售场景里电话是客户的唯一标识重复录入会直接污染整个业务数据。但实际业务中经常出现夫妻两人都来登记的情况所以单独加一个dup_source_id字段用于指向重复数据的主键而不是简单拒绝录入。这属于业务层面的人工合并逻辑。3.3 跟进记录表最简单的表反而是最容易写错的跟进记录表字段很简单id、customer_id、content、next_time、create_by、create_time。但它承载的查询需求很重按客户查时间线、按销售查今日待跟进列表、按时间范围查跟进次数统计。所以索引至少要建两个(customer_id, create_time)联合索引以及(create_by, next_time)联合索引。建错索引或者漏建索引数据量到几万行后就会出现明显的查询延迟。3.4 合同表和回款表金额字段的类型选择合同总价、定金、回款金额这些字段统一用 DECIMAL(10, 2)不能使用 DOUBLE 或 FLOAT。我在实际项目中遇到过用 DOUBLE 存金额导致对账差几分钱的问题根源就是浮点数精度。MyBatis 里映射时对应BigDecimal前端展示时再格式化保留两位小数。合同表需要设计一个approval_status字段草稿、待审、已通过、已驳回。考虑到合同审核是一个流程提交审核、审核通过、驳回这三个动作对应三个不同的后端接口方法每个方法都要在事务里更新合同状态并写入操作日志。4. SpringBoot 后端关键实现统一响应、登录鉴权与 MyBatis 的动态 SQL后端部分重点看四个关键点的实现思路。4.1 统一响应结构所有接口返回同一种格式项目里所有 controller 接口返回的格式应该统一{ code: 200, message: 操作成功, data: { ... } }源码中通常对应一个ResultT泛型类封装成功和失败两种静态构造方法。这样做的好处是前端 axios 拦截器可以统一处理错误提示不用每个接口单独判断。如果看到一个接口返回MapString, Object那说明代码规范还有优化空间。4.2 JWT 登录鉴权流程这个系统采用 JWTJSON Web Token做无状态登录。核心流程用户输入账号密码后端校验通过后生成 token把用户 id、用户名、角色编码放进 token 的 claims 里。token 返回给前端前端存在 localStorage 或 Vuex 中。前端 axios 请求拦截器在 header 中携带Authorization: Bearer token。后端的拦截器解析 token校验有效性把用户信息放入ThreadLocal或RequestContext中供后续业务方法随时取用。这里不得不提一个常见的坑JWT 一旦签发在过期之前无法主动失效。如果用户修改了密码或者管理员封禁了账号老 token 在有效期内依然能通过校验。解决思路有两个方向一是把 token 有效期设短并配合前端定时刷新二是在 Redis 里维护一个 token 黑名单封禁时写入。这个系统源码如果只是演示用途通常采用第一个方案二次开发时需要留意。4.3 MyBatis 的 XML 写法与动态 SQLMyBatis 在这套系统里的核心优势是动态 SQL。比如房源列表查询筛选条件可能是价格区间、面积区间、朝向、状态中的任意组合。如果用 JDBC 拼 SQL 会写很多 if 判断而 MyBatis 的where配合if标签直接解决select idselectHouseList resultTypecom.example.entity.House SELECT * FROM house where if teststatus ! null AND status #{status} /if if testminPrice ! null AND total_price gt; #{minPrice} /if if testmaxPrice ! null AND total_price lt; #{maxPrice} /if if testkeyword ! null and keyword ! AND (house_name LIKE CONCAT(%, #{keyword}, %) OR address LIKE CONCAT(%, #{keyword}, %)) /if /where ORDER BY create_time DESC /select还有几个要注意的细节MySQL 的LIKE拼接建议用CONCAT(%, #{keyword}, %)而不是直接在代码里写%${keyword}%后者存在 SQL 注入风险。大于小于号在 XML 中必须转义写成gt;写成lt;。分页查询项目里一般用 PageHelper 插件PageHelper.startPage(pageNum, pageSize)之后紧跟第一条查询语句即可原理是通过 MyBatis 拦截器自动改写 SQL 为LIMIT语句。4.4 事务控制的使用边界涉及金额和状态变更的操作必须加事务。比如“客户下定房源”这个接口要同时做三件事更新房源状态为预定、创建合同单、生成一条操作日志。任何一个步骤失败都需要全部回滚。SpringBoot 中最简单的做法是用Transactional注解标注在 service 方法上。注意事务失效的经典场景同一个类内部方法调用Transactional默认不会生效异常被 catch 住后没有抛出事务不会回滚Transactional加在非 public 方法上不生效。5. Vue 前端实战动态菜单、销售漏斗页面与等待状态管理前端部分这套系统的代码量占了一半以上。主要围绕三个重点来理解。5.1 axios 请求封装后端返回结构变了怎么办一般src/utils/request.js会创建一个 axios 实例配置baseURL添加请求拦截器和响应拦截器。响应拦截器里判断res.data.codeservice.interceptors.response.use( response { const res response.data if (res.code 200) { return res.data } if (res.code 401) { // token 过期跳转登录页 router.push(/login) return Promise.reject(new Error(unauthorized)) } Message.error(res.message || 请求失败) return Promise.reject(new Error(res.message || 请求失败)) }, error { Message.error(error.message || 网络异常) return Promise.reject(error) } )这样每个页面调用接口后拿到的就是data字段部分处理逻辑统一页面代码也能简洁不少。5.2 动态菜单与路由守卫前端不能把菜单写死在路由表里因为不同角色能看到的菜单不一样。正确做法是登录后调用/auth/menus接口拿到当前用户的菜单列表通过router.addRoute动态添加路由。同时要在路由守卫里做三层判断router.beforeEach((to, from, next) { const token store.getters.token if (!token) { if (to.path /login) { next() } else { next(/login) } return } if (to.path /login) { next(/) return } // 已登录且路由已动态注册直接放行 next() })这里有个前后端配合的约定接口权限校验在后端菜单显隐在前端。前端动态菜单只是用户体验层面的控制真正防止越权访问必须靠后端拦截器。5.3 销售漏斗页面与数据可视化销售人员最关心的页面是“我的漏斗”把客户按状态分组从新客户到意向客户到谈判中到最终成交形成一道漏斗。实现方式是在后端写一个分组统计接口SELECT status, COUNT(*) AS cnt FROM customer WHERE owner_id #{userId} GROUP BY status前端拿到数据后用进度条比例渲染即可不一定非要引入 ECharts 这种重量级图表库。如果系统里还有业绩排行、回款趋势这类更复杂的图表再考虑使用 ECharts。5.4 一个容易被忽略的点等待状态前端调用接口后在返回结果之前按钮要防止重复点击。一个比较成熟的方案是维护一个loading变量const loading ref(false) const handleSubmit async () { if (loading.value) return loading.value true try { await submitData(formData) Message.success(提交成功) } finally { loading.value false } }虽然看起来很简单但很多初版页面都漏掉了这个处理用户双击提交按钮后生成了两条合同后续排查起来特别麻烦。6. 本地启动、部署上线与二次开发避坑建议6.1 本地启动完整流程拿到源码后如果本地能一次跑通说明环境配置基本没问题。完整链路通常是使用 IDEA 打开后端工程等待 Maven 下载依赖完成。创建本地 MySQL 数据库设置字符集为utf8mb4导入sql目录下的初始化脚本。修改application.yml中的数据库连接信息和 Redis 配置。启动后端项目观察控制台日志确认端口常见为 8080 或 8081正常启动。用命令行npm install安装前端依赖。修改前端request.js中的baseURL指向后端地址。执行npm run dev浏览器访问前端地址使用初始账号登录。整个过程最容易卡住的地方有两个一是Maven 依赖下载不完整可以从阿里云镜像仓库加快下载二是Node 版本与前端项目要求的版本不一致常见表现是npm install报 node-sass 编译错误解决方式是切换 Node 版本或者将node-sass替换成sass。6.2 生产环境部署方案生产环境推荐部署方式组件部署方式后端mvn clean package -DskipTests打 jar 包nohup java -jar app.jar 启动前端npm run build生成 dist 目录由 Nginx 托管数据库MySQL 单独一台服务器开启 binlog 自动备份反向代理Nginx 配置两个 server 块静态资源指向 dist/api前缀转发到后端服务Nginx 一个关键的配置片段location /api/ { proxy_pass http://127.0.0.1:8080; proxy_set_header Host $host; proxy_set_header X-Real-IP $remote_addr; }注意/api/结尾的斜杠如果漏掉会导致代理路径拼接不对。另外就是前端刷新页面时路由 404需要在 Nginx 里加上try_files $uri $uri/ /index.html;否则 history 模式刷新就白屏。6.3 二次开发时最值得扩展的方向基于这套源码做二次开发有几个高性价比的扩展方向把跟进提醒从“列表查询”升级为“定时任务推送”用 Spring 的Scheduled每天扫描当天需要跟进的客户发送站内消息。增加操作日志表记录关键实体字段变更前后的值方便审计。把价格字段的修改权限单独抽出来通过角色菜单的按钮权限来控制。用 Redis 缓存热门楼盘列表和销售排行数据减少数据库压力。6.4 几个实操中的常见问题现象可能原因解决方向前端访问接口跨域后端未配置 CORS 或 Nginx 代理未生效开发环境由后端CorsFilter解决生产环境走 Nginx 同域代理上传图片后无法回显静态资源映射路径未配置SpringBoot 添加WebMvcConfigurer映射本地目录分页查询总数不准确PageHelper 页码从 0 或 1 的约定不一致确认 PageHelper 配置reasonable参数前端页码统一新增角色后菜单为空角色和菜单未建立关联数据通过菜单管理页面分配权限或直接 SQL 插入中间表最后说一点实际体验。这类系统源码拿到手最重要的不是“跑起来”而是动手删掉一部分代码再自己重新写一遍。把房源上下架改成商品上下架把客户跟进改成工单流转你会发现整个架构的迁移能力比想象中强。技术栈本身没有多高深但通过完整业务把 SpringBoot 的事务、MyBatis 的动态 SQL、Vue 的状态管理和路由守卫串起来之后再遇到类似的管理系统开发需求心里会非常有底。如果手里正好有这套源码建议按这个顺序去读代码先跑起来再看数据库脚本接着追一个查询接口的全部执行路径然后改一个前端页面的字段提交最后尝试加一张表。四步走完这套架构基本就吃透了。

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价 →
↑