SpringBoot+Vue房地产销售管理系统:业务建模到部署全解析
做这套东西之前我劝你先想清楚一个问题网上搜得到的某某管理系统源码真正值钱的部分从来不是CRUD而是它背后怎么抽象业务。房地产销售管理系统这个题目在毕业设计和外包项目里出现频率极高但大多数人拿到源码后干的第一件事是改个Logo、换张登录页图片然后兴冲冲跑起来截几张图——结果一开数据库发现房源、客户、订单、回款这几个表之间的状态根本没有串起来销售主管想看个去化率还得手动用Excel汇总。这篇文章我不打算给你贴一个开源地址然后让你自己研究而是把这套基于SpringBoot Vue MyBatis MySQL的售楼系统从业务模型、表结构设计、后端工程组织、前端页面骨架到几个最容易翻车的核心场景一层层拆给你看。你可以直接把它当作一个参考实现来对照自己的项目改。适合哪类人看第一种是准备做毕业设计、需要快速把系统跑通并讲清楚设计思路的同学第二种是刚工作不久、被分配了类似企业信息管理系统活儿的初级开发。这篇内容会控制在一万字的干货范围内把每一个为什么这么做讲透而不是让你单纯抄作业。1. 先弄清楚一个售楼系统到底在管什么很多初学者拿到题目后第一反应是建用户表、房源表、订单表三张表就开干。这是典型的会CRUD但不会做业务的做法。你站在售楼处的置业顾问角度想一想他每天的工作是什么录入新到访的客户、给客户配房源、带客户看房、记录每次带看结果、客户意向升级后帮他算价、算完价走认购、认购之后约定签约时间、签约后跟踪付款进度。这里面每一环都涉及状态变化而状态变化又会反过去限制下一步能做什么。1.1 业务主链路拆解我把这套系统的核心链路画成一条线你照着这条线去理解业务比背表结构有用得多楼盘管理 → 房源建档 → 客户报备 → 跟进/带看 → 意向确认 → 认购登记 → 签约审核 → 付款计划 → 回款登记关键点在这里客户不是一进来就能买房子的。大多数售楼系统会区分公客和私客。公客存放在公共池里谁都可以看到但不能随意跟进只有当置业顾问把客户报备到自己名下这个客户才变成私客。报备之后还要有保护期比如7天内如果这个顾问没有进行有效跟进客户会自动回到公客池重新被别人抢走。这个保护期逻辑是这类系统里最容易做的也是少数会真正被使用的功能。再来就是房源和楼栋的关系。楼盘下有楼栋楼栋下有不同单元、不同楼层、不同户型的房源。每套房源在初始化时需要带着建筑面积、套内面积、单价基数、总价基数这些字段在后续算价、签约、贷款计划里都会被反复引用所以建表的时候不能当成普通商品来设计。1.2 核心数据表与字段设计思路基于上面这条链路一套不算复杂的售楼系统至少需要下面这些表表名用途关键字段说明sys_user系统用户账密、手机号、所属部门、职位销售/主管/财务/管理员sys_role / sys_user_role角色权限用角色控制按钮级操作权限building楼盘楼盘名称、地址、开盘时间、楼栋数house房源主表楼栋、单元、房号、户型、朝向、建筑面积、单价、总价、销售状态customer客户姓名、手机号、证件号、意向等级、来源渠道、归属销售、保护截止时间follow_record跟进记录跟进方式电话/到访/带看、跟进内容、下次跟进时间sale_order认购/订单客户ID、房源ID或房源快照、折前总价、折扣、成交总价、订单状态payment_plan回款计划付款方式一次性/按揭/分期、计划付款日期、应付金额、实收金额payment_record回款流水关联回款计划记录每笔打款凭证号、到账时间operation_log操作日志记录谁在什么时间改了哪套房源的状态这里单独说一下house表里销售状态这个字段。别看它只是一个整数或者字符串它的流转是整个系统的心脏。正常状态链路是0未售 → 1锁定 → 2认购 → 3签约 → 4已备案 → 9已退房有些公司还会在未售和锁定之间插入预留给关系户留房在签约之后插入贷款审批中。无论怎么细化你要做的是把所有合法流转路径画出来并写代码控制。比如已退房只能在认购或签约状态才能发起并且退房后房源要回滚到未售同时原订单要标红作废。这一步做不好后面销售数永远对不上。字段命名上我建议统一用下划线风格order_id、house_id、customer_id这种外键字段在查询前就把索引设计好。别等数据超过十万条再返工。2. 后端SpringBoot MyBatis MySQL版本选型和工程落地这部分看起来是常规操作但恰恰是大多数人卡住的地方。你在网上看到的大部分烂尾源码问题都出在版本不匹配SpringBoot 3.x 强制要求 JDK 17而很多学校的毕业设计环境默认是 JDK 8MyBatis 3.5.9 以上的spring-boot-starter兼容性范围也有限制MySQL 换成 8.0 之后驱动类名从com.mysql.jdbc.Driver变成了com.mysql.cj.jdbc.Driver旧代码直接报ClassNotFoundException。2.1 版本选型这件事不能全听网上教程我个人建议一个稳妥组合也是我实际跑通过的组件推荐版本理由JDK8 / 11兼容性最好不要为了追新用17除非你已经用了SpringBoot 3SpringBoot2.7.x目前最稳的2.x末代版本停止更新后仍有大量参考文档MyBatis3.5.13配合 mybatis-spring-boot-starter 2.3.xMySQL8.08.0以上是主流但要做时区配置Node.js16/18Vue2前端推荐16Vue3推荐18或20看到有人用SpringBoot 3.x写这套系统脸上写满了我能行。但实际生产环境里很多老牌企业还是抱着JDK8不放。原因很简单维护成本。JDK8的法定义务更新虽然停了但它的稳定性和生态兼容性依旧无敌。SpringBoot 2.7.x 配上 JDK 8哪怕你用的是最老的教学版IDEA也能正常启动、断点、热部署。2.2 MyBatis的XML与注解别被零XML的言论带偏MyBatis最大的价值在于动态SQL和灵活的映射规则。Redis缓存那些花活先放一边你面对的是几十个字段的房源表、多条件组合查询的客户列表这时候如果全部用注解写SelectSQL里凡是有if判断的都得拼字符串字符串一多就是灾难。我推荐的写法是注解接口 XML映射的组合简单的单表查询用注解比如Select(SELECT * FROM building WHERE is_deleted 0)复杂的分页条件查询、多表join、动态更新全部放XML。在application.yml里配置好mapper-locations: classpath:mapper/*.xml再配上map-underscore-to-camel-case: true数据库字段的下划线命名就能自动映射到Java的驼峰属性省掉一大半resultMap。还有一个细节mybatis.type-aliases-package配置了实体类包名之后XML里的resultType可以直接写类名不用写全限定名。少打几个字倒是次要的关键是代码里看起来干净很多。2.3 登录鉴权与拦截器其实不用上Spring Security这个项目很多人会纠结要不要引入Spring Security。我的看法是如果这是一个课程设计或者内部管理系统你真没必要上Security全家桶。Spring Security一方面学习成本高另一方面默认过滤链会拦截你的静态资源和SHIRO风格的接口调试起来相当痛苦。用JWT 自定义拦截器就够了用户登录时校验用户名密码BCrypt加密存储。登录成功生成JWT包含userId、userName、roleKey等必要信息过期时间设个24小时。写一个AuthInterceptor实现preHandle从请求头Authorization里取token解析如果失败直接返回401成功就把UserId塞进ThreadLocal的UserContext里。注册拦截器时放行登录接口、Swagger文档、静态资源。关于密码加密千万别用MD5。BCrypt的随机盐机制决定了同一密码每次加密的结果都不同就算数据库泄露要跑字典也得花不少时间。这个习惯越早养成越好。事务处理上Service层需要写上Transactional(rollbackFor Exception.class)。很多人只写Transactional而不指定rollbackFor一旦业务方法里抛出的是RuntimeException以外的异常事务不会回滚库存扣了但订单没生成数据错乱会让你欲哭无泪。自调用场景同一个类里this调用另一个带事务的方法事务会失效这个也是老生常谈了解决思路是拆分Service或者用Autowired注入自身代理。3. 前端Vue项目的组织路由、权限、接口封装一次理清前端的难度不在写页面而在于工程化的组织方式。很多源码包前端只有一个App.vue加几十个组件文件路由也只有一个router/index.js硬怼全部页面最后打包出来一个3MB以上的JS文件首屏加载慢到被客户吐槽。3.1 Vue版本选择与工程初始化如果你拿到的源码是Vue2别急着升级Vue3。Element UI是Vue2的黄金搭档文档全、组件齐、踩坑的博客多到数不清Vue3的话对应的是Element Plus虽然官方看着挺美但遇到表格树、级联选择器这种复杂组件的兼容问题时你搜出来的答案多半是英文帖子阅读成本高不少。初始化前端工程时我用的命令永远是这样的vue create sales-front cd sales-front npm install vue-router3 element-ui axios sass sass-loader10 -S注意sass-loader我专门指定了10版本为什么因为 Node 的版本一旦超过17太高版本的sass-loader会要求node-sass而node-sass的编译问题是前端话题里永远的神坑。用dart-sass替代node-sass后这个指定版本是当前最稳的组合。src目录下面的组织方式我是这么分的src/ ├─ api/ // 按模块拆分接口请求 │ ├─ login.js │ ├─ house.js │ └─ order.js ├─ assets/ ├─ components/ // 通用组件如Upload、RegionSelect ├─ router/index.js ├─ store/ // Vuex或Pinia ├─ utils/request.js └─ views/ // 页面级组件 ├─ house/ │ ├─ HouseList.vue │ └─ HouseDetail.vue ├─ customer/ │ └─ CustomerList.vue └─ dashboard/这层组织方式的好处等后续要加权限控制、或者一个模块要复用另一个模块的接口时你就能体会到。所有直接写在页面里的axios请求后期全要重构成api/目录越早分越省事。3.2 路由配置与侧边栏权限路由设计上采用层级嵌套方案登录后的主框架是一个Layout组件里面包含顶部导航、侧边菜单、主内容区。子页面作为Layout的children这样切换页面时导航不会重新渲染用户体验好很多。{ path: /home, component: Layout, redirect: /home/dashboard, meta: { title: 首页, icon: el-icon-s-home, roles: [admin, sale, manager] }, children: [ { path: dashboard, component: () import(/views/dashboard/index.vue), meta: { title: 销售看板 } } ] }在meta.roles里声明哪些角色能看到这个菜单然后在路由守卫里做一次判断router.beforeEach((to, from, next) { if (getToken()) { if (to.path /login) return next(/home/dashboard) if (to.meta.roles !to.meta.roles.includes(roleKey)) { return next(/403) } } })这里看起来简单但已经能满足大部分小型系统的权限需求。唯一的坑是刷新后路由可能丢失因为你把菜单数据是存在Vuex里的页面一刷新state就清空了。解决办法有两种一是把路由菜单的数据持久化到localStorage二是动态生成路由时通过后端接口返回菜单List每次刷新重新拉取。第二个方案更稳推荐直接走接口。3.3 Axios实例封装与跨域问题的正确解法utils/request.js里创建Axios实例时请求拦截器统一把token加到请求头响应拦截器统一处理HTTP错误码和业务错误码。遇到401时要先清掉本地token然后跳转登录页避免进入死循环。跨域是我见到最多人卡住的点。后端同事一急就CrossOrigin或者搞个全局CORS配置其实开发环境的最优解是前端开代理。在vue.config.js里这样配置devServer: { proxy: { /api: { target: http://localhost:8080, changeOrigin: true, pathRewrite: { ^/api: } } } }这样前端访问/api/login时实际转到后端的是http://localhost:8080/login。等上了生产环境用Nginx再来一层反向代理把/api指到后端的进程端口上后端就能保持干净的CORS配置。4. 三个核心业务场景的代码级拆解这一章是整篇文章的精华部分。我挑售楼系统中最容易翻车、也最能体现你水平的三个场景房源状态流转、客户跟进时间线、销售看板统计SQL。4.1 房源状态流转用状态机约束非法操作绝不能用前端隐藏按钮后端if判断来管房源状态。正确做法是在后端定义一个枚举把所有合法流转路径放进一个Map里流转前做校验。public enum HouseStatus { UNSOLD(0, 未售), LOCKED(1, 锁定), SUBSCRIBED(2, 已认购), CONTRACTED(3, 已签约), RECORDED(4, 已备案), RETURNED(9, 已退房); private final int value; private final String desc; private static final MapInteger, SetInteger TRANSITIONS new HashMap(); static { TRANSITIONS.put(UNSOLD.value, Set.of(LOCKED.value, SUBSCRIBED.value)); TRANSITIONS.put(LOCKED.value, Set.of(UNSOLD.value, SUBSCRIBED.value)); TRANSITIONS.put(SUBSCRIBED.value, Set.of(CONTRACTED.value, RETURNED.value)); TRANSITIONS.put(CONTRACTED.value, Set.of(RECORDED.value, RETURNED.value)); TRANSITIONS.put(RETURNED.value, Set.of(UNSOLD.value, LOCKED.value)); } public static boolean canChange(int from, int to) { SetInteger allowed TRANSITIONS.get(from); return allowed ! null allowed.contains(to); } }Service里对应的方法逻辑Transactional(rollbackFor Exception.class) public void changeHouseStatus(House house, int targetStatus, Long operatorId) { if (!HouseStatus.canChange(house.getStatus(), targetStatus)) { throw new BizException(该房源当前状态不允许执行此操作); } HouseStatus newStatus HouseStatus.fromValue(targetStatus); house.setStatus(newStatus.getValue()); houseMapper.updateById(house); // 写一条状态变更日志 operationLogService.log(house.getId(), house, house.getStatus(), targetStatus, operatorId); }这段代码的核心价值是把允许谁改状态和允许什么状态改到什么状态彻底分离。如果后续业务要求只有财务角色才能把签约状态改成备案状态你只需在Service里再加一层角色判断状态机逻辑完全不用动。还有一个容易被忽略的细节套房源一旦进入认购状态它的价格字段、所属订单ID就要被快照下来。不能等订单生成后再去查当时的单价——因为项目后期完全可能调价。建议在sale_order表里冗余存储house_no,build_name,unit_price,total_price等冗余字段。冗余会带来数据一致性成本但在交易记录这类不可变历史场景里快照远比实时联表更正确。4.2 客户跟进时间线为什么不能只存最后跟进时间很多简陋系统的客户跟进就是更新customer表里的last_follow_time字段。看起来需求实现了但销售主管一问这个客户前三次带看都看了什么户型没人能答上来。所以follow_record表必须存在且要有足够的信息建立完整的时间线。页面端用的是Element UI的el-timeline组件数据倒序排列。后端接口按customer_id查询所有跟进记录带时间、跟进方式、内容、下次跟进日期。查询完后需要标记是否过期未跟进——这是私客管理里保护期判断的关键当前时间大于最后跟进时间加上N天客户就会被系统标记为即将流失。一个关于下次跟进时间的建议不要做成纯日期选择而是做成today 1天、today 3天、today 7天这种快捷选项。你面对的用户是销售没时间慢慢去日历里点半天。把常用场景固化下来系统就会显得好用。4.3 销售看板统计SQL最容易被面试官追问的角落做完了增删改查你还需要一个首页看板来体现系统价值。销售额、订单数、房源去化率、客户转化率、渠道占比这些都是管理层每天会看的。去化率统计的SQL我直接给你一份可跑通的版本SELECT COUNT(*) AS total_house, SUM(CASE WHEN status IN (2,3,4) THEN 1 ELSE 0 END) AS sold_house, ROUND(SUM(CASE WHEN status IN (2,3,4) THEN 1 ELSE 0 END) / COUNT(*) * 100, 2) AS sold_rate FROM house WHERE is_deleted 0成交额按月份分组统计的SQLSELECT DATE_FORMAT(payment_date, %Y-%m) AS month, SUM(pay_amount) AS month_amount FROM payment_record WHERE payment_date DATE_SUB(CURDATE(), INTERVAL 12 MONTH) GROUP BY DATE_FORMAT(payment_date, %Y-%m) ORDER BY month注意两点金额字段永远用DECIMAL(10, 2)不要用Java的double更不要用MySQL的FLOATSUM时注意大字段别传给前端丢了精度后端可以转成字符串再返回。如果统计慢给payment_date、status、customer_id加上普通索引几千条数据的项目根本不用考虑查询优化问题反而是过度设计能坑死你。5. 从本地跑通到部署上线三分钟定位环境类问题最后聊一个很多文章不爱提的环节环境问题。项目代码本身没问题但就是跑不起来这是最让人崩溃的阶段。我把最常见的坑整理成一个排查清单按下面顺序来查通常五分钟内能解决。5.1 启动SpringBoot阶段的坑第一个坑出现在启动阶段Failed to configure a DataSource: url attribute is not specified或CLIENT_PLUGIN_AUTH is required。url attribute is not specifiedapplication.yml里spring.datasource配置没生效。先确认你的编辑环境加载的配置文件是哪个——IDEA默认只编译src/main/resources下的内容如果你改的是其他目录下的副本永远不生效。CLIENT_PLUGIN_AUTH is requiredMySQL 8.0的默认认证插件是caching_sha2_password老版MySQL驱动不支持。要么升级驱动mysql-connector-java到8.0要么把连接串加上useSSLfalseserverTimezoneAsia/ShanghaiallowPublicKeyRetrievaltrue。serverTimezone不填的话经常会报The server time zone value ... is unrecognized。这个老坑到现在还有人在踩。还有一种是MyBatis的XML文件没有被打进target/classes目录。明明mapper-locations配了也放在src/main/resources/mapper下但启动时提示Invalid bound statement (not found)。八成是因为构建工具只把*.xml识别成资源文件需要在pom.xml里补充build resources resource directorysrc/main/java/directory includes include**/*.xml/include /includes /resource resource directorysrc/main/resources/directory /resource /resources /build5.2 前端启动与部署阶段的坑前端最常见的反馈是npm install跑着跑着报node-sass或者sass-loader相关错误。这种问题根因基本是Node版本和依赖版本不匹配。我的建议是到Node官网装一个16.20.x LTS版本可以用nvm管理多版本。删除package-lock.json和node_modules目录重新npm install。如果还报错检查有没有用镜像源。个人用户推荐配置.npmrc指向官方源或国内镜像避免私有源不完整的问题。前端跑通之后还有配置问题端口不一致。很多前端项目默认端口是8080SpringBoot也默认8080。你前端起在8080后端也起在8080必然会冲突。我习惯在vue.config.js中给前端设置port: 8089后端保持在8080这样跨域代理只涉及环境切换不用改后端。打包部署时有另一个经典问题history模式的路由在Nginx下刷新页面会404。原因是刷新时Nginx找不到对应的后端路由。解决方法是Nginx配置里加location / { try_files $uri $uri/ /index.html; }这是前端刷新404的标准解法。如果还不行检查Nginx里root路径是否正确指向dist目录。5.3 排查思路的整体方法论上面列了具体问题但更宝贵的是排查思路。我给一个分层定位法先看浏览器F12的Network面板。如果请求发出去了但状态码为4xx/5xx问题一定在后端如果请求根本没发出去或者显示为拦截的状态问题在前端路由守卫或拦截器。再看后端控制台日志。重点不是日志最底下的Exception而是最上面三行——大多数Java报错信息在第一行就能看清原因的苗头真正的根因往往需要从第一行开始往下数10行内定位。最后看SQL。控制台如果开了MyBatis的SQL日志打印拿到实际执行的SQL放到数据库客户端里跑一下。数据查不出来大概率是SQL条件写错了而不是代码逻辑写错了。这套方法能覆盖约80%的本地环境问题剩下20%大概率是版本、缓存、端口这些环境因素。如果日志显示的是类找不到优先去Maven面板执行clean package一次再重启应用多半能解决依赖不完整的问题。最后的一点个人经验我以前刚开始做这类全栈管理系统时拿到项目第一反应是跑起来再说跑起来之后完全看不懂于是开始改。改了三天把原本能跑的改崩了再换一个源码重新来。循环了很多次才明白一件事成熟的接手流程应该是先看数据库脚本再读application.yml和pom.xml最后看前端vue.config.js。数据库脚本告诉你业务有多复杂配置文件告诉你环境依赖前端的代理配置告诉你所有接口约定的前缀。这三个东西理顺了项目结构基本就清晰了大半剩下那些业务代码不过是在验证你的猜测而已。如果你正在做的项目正好是这套售楼系统建议你按这篇内容把业务链路画一遍再动手改代码。状态流转、客户保护期、回款计划这三块是最能拉开代码水平差距的地方也是面试官最喜欢深挖的点。把这三个场景真正吃透比多刷五十道面试题强得多。