☰
SpringBoot+Vue前后端分离物业管理系统实战:从技术选型到部署避坑
2026/10/27 17:00:08 网站建设 项目流程

简介:这份资源是基于Spring Boot与Vue.js的物业管理系统完整源码包,采用前后端分离架构,适合正在做Java课程设计、毕业设计的学生,以及想学习企业级项目架构的开发者参考。压缩包共2025个文件,约13.1MB,其中svg与scss、css、less等样式资源占比较大,用于前端界面与图标渲染;js文件支撑Vue组件交互逻辑,32个java文件对应后端控制器与服务层,另有sql脚本、properties配置、xml映射及少量pdf、xmind文档,便于理解数据库设计与模块划分。资源已有87人学习下载,可作为课程设计或毕设的完整参考方案。通过阅读源码,读者能掌握前后端分离下的接口通信方式、Spring Boot自动配置与内嵌服务器用法,以及Vue组件化开发与后台管理界面的组织思路,同时借鉴其清晰的目录结构与模块划分,用于自己的项目架构设计与优化。

1. 前后端分离的物业管理系统:为什么它是练手全栈的最佳切口

很多做 Java 的同行都有个尴尬期:SpringBoot 的增删改查写得滚瓜烂熟,但一到 Vue 就发怵,或者反过来,前端页面切得飞起,后端接口一联调就翻车。物业管理系统这个题目之所以被反复拿来练手,恰恰因为它把「前后端分离」这件事的复杂度压到了一个刚好能啃下来的量级——既不像电商秒杀那样要处理高并发和分布式事务,也不像纯 CRUD 后台那样毫无业务纵深。它天然带着楼栋、房屋、业主、车位、报修工单、收费账单这几张互相有关联的表,逼着你去想清楚一对多、多对一怎么在接口层表达,权限怎么按角色切分,前端路由怎么跟后端菜单对上。这套东西跑通一遍,你对「基于 SpringBoot + Vue 的项目」到底长什么样,心里就有底了。这篇文章不聊虚的,就按我实际搭过几套的经验,把从环境到联调、从建表到打包部署的路径拆开讲,新手能照着走,熟手能对着看边界和坑。

2. 技术选型与工程骨架:先把地基打对

2.1 为什么是 SpringBoot + Vue 而不是别的组合

选型这件事,落到物业管理系统上其实没太多纠结空间。后端用 SpringBoot,核心原因是它的自动配置和起步依赖能把一个 Web 项目的搭建成本压到极低,你不需要自己去配 DispatcherServlet、不用手写一堆 XML,spring-boot-starter-web一引,内嵌 Tomcat 就起来了。对于物业这种以 REST 接口为主、并发量中等的场景,SpringBoot 的默认配置完全够用,没必要上 SpringCloud 那一套微服务,那是给自己找麻烦。

前端选 Vue,主要是两点:一是上手曲线比 React 平缓,模板语法对后端出身的人友好;二是 Vue 的生态里 Element Plus 这类组件库对管理后台的表格、表单、弹窗覆盖得非常全,物业系统里大量的列表页和编辑弹窗可以直接拼出来,省掉大量造轮子的时间。前后端分离的关键在于,前端只负责渲染和交互,所有数据通过 HTTP 接口拿,两边通过 JSON 通信,各自独立开发、独立部署。

这里有个选型上的细节值得说:Vue 2 和 Vue 3 怎么选。如果是新起项目,直接上 Vue 3 + Vite,构建速度快,组合式 API 在逻辑复用上更清晰。但如果你的参考资料大多是 Vue 2 的,或者团队里有人对 Options API 更熟,Vue 2 + Webpack 也不是不能用,只是启动和热更新会慢一些。我一般建议新手先用 Vue 3,因为现在社区的主流示例和组件库文档都在往 3 靠。

2.2 后端工程结构怎么分层

一个能长期维护的 SpringBoot 项目,目录结构不能乱。我见过太多人把所有 Controller 堆在一个包里的,后期找接口像大海捞针。物业系统我一般按这样分:

com.example.property ├── config // 跨域、拦截器、Swagger 等配置 ├── controller // 接口层,只做参数校验和调用 service ├── service │ ├── impl // 业务逻辑实现 ├── mapper // MyBatis 的 DAO 接口 ├── entity // 数据库实体,和表一一对应 ├── dto // 前端传过来的参数对象 ├── vo // 返回给前端的视图对象 ├── common // 统一返回结果、异常、常量 └── utils // 工具类

分层的意义在于职责隔离。Controller 不写业务,Service 不碰 HttpServletRequest,Mapper 只做数据访问。DTO 和 VO 分开是为了避免直接把数据库实体暴露给前端——实体里可能有密码字段、逻辑删除标记,直接返回就是安全事故。

统一返回结果这个类一定要有,我一般叫Result<T>,包含 code、msg、data 三个字段。前端拿到响应后先判断 code,再决定是渲染数据还是弹错误提示。没有这个统一结构,前端每个接口都要写一套判断逻辑,维护起来是灾难。

public class Result<T> { private Integer code; // 200 成功,其他为业务错误码 private String msg; private T data; public static <T> Result<T> success(T data) { Result<T> r = new Result<>(); r.code = 200; r.msg = "操作成功"; r.data = data; return r; } public static <T> Result<T> fail(String msg) { Result<T> r = new Result<>(); r.code = 500; r.msg = msg; return r; } }

这段代码的关键在于泛型T,它让 data 字段能承载任意类型——单个对象、列表、分页结果都行。code 用 200 表示成功是沿用 HTTP 状态码的习惯,你也可以自定义成 0 表示成功,但团队内要统一。msg 是给前端直接展示的,所以写的时候要用人话,别把堆栈信息塞进去。

2.3 前端工程初始化与目录约定

前端用 Vite 创建 Vue 3 项目,命令是npm create vite@latest property-web -- --template vue。创建完之后,我一般会装这几个依赖:vue-router管路由,pinia管状态,axios发请求,element-plus做 UI。安装命令一行搞定:

npm install vue-router pinia axios element-plus

目录结构上,src/api放所有接口请求函数,src/views放页面组件,src/components放可复用组件,src/router放路由配置,src/store放 Pinia 的 store。这个约定不是强制的,但按这个来,别人接手你的代码能快速定位。

axios 一定要做一层封装,不能在每个页面里直接axios.get。封装的核心是两件事:请求拦截器里统一加 token,响应拦截器里统一处理错误码和 401 跳登录。

import axios from 'axios' import { ElMessage } from 'element-plus' import router from '@/router' const request = axios.create({ baseURL: '/api', // 开发环境走代理,生产环境由 nginx 转发 timeout: 10000 }) // 请求拦截:带上登录后存的 token request.interceptors.request.use(config => { const token = localStorage.getItem('token') if (token) { config.headers.Authorization = token } return config }) // 响应拦截:统一处理业务错误和登录失效 request.interceptors.response.use( response => { const res = response.data if (res.code !== 200) { ElMessage.error(res.msg || '请求失败') return Promise.reject(new Error(res.msg)) } return res.data }, error => { if (error.response && error.response.status === 401) { localStorage.removeItem('token') router.push('/login') } return Promise.reject(error) } ) export default request

这里baseURL设成/api是为了配合开发环境的代理。Vite 的vite.config.js里要配 proxy,把/api开头的请求转发到后端端口,这样开发时不会有跨域问题。响应拦截器里直接返回res.data,意味着页面里拿到的就是业务数据本身,不用每次.data.data两层剥,这个习惯能省不少事。

3. 核心业务模块的接口与页面落地

3.1 数据库表设计与业主-房屋-楼栋的关联

物业系统的表不多,但关联关系要想清楚。最核心的四张表:楼栋building、房屋house、业主owner、报修工单repair。楼栋和房屋是一对多,房屋和业主是一对多(一套房可能有多个业主),业主和报修单是一对多。

房屋表里要有一个building_id外键,业主表里要有一个house_id外键。查询房屋列表时,前端需要显示所属楼栋名称,这就涉及联表查询。我一般不在实体里做关联映射,而是在 Mapper 的 XML 里写 resultMap,或者直接用 VO 接收联表结果。

CREATE TABLE house ( id BIGINT PRIMARY KEY AUTO_INCREMENT, building_id BIGINT NOT NULL COMMENT '所属楼栋', house_no VARCHAR(20) NOT NULL COMMENT '房号,如 1-101', area DECIMAL(10,2) COMMENT '面积', status TINYINT DEFAULT 0 COMMENT '0空置 1已入住', create_time DATETIME DEFAULT CURRENT_TIMESTAMP );

status用 TINYINT 存状态码是常见做法,比存字符串省空间,但前端要维护一份映射表把 0/1 翻译成「空置/已入住」。create_time设默认值,插入时不用手动传。

3.2 分页查询接口的写法与前端表格对接

列表页是物业系统里出现频率最高的页面,分页查询必须封装好。后端用 MyBatis-Plus 的话,分页插件一配,Page对象一传,代码很简洁。不用 MyBatis-Plus 的话,手写 limit 也行,但要自己算 offset。

@GetMapping("/page") public Result<Page<HouseVO>> page( @RequestParam(defaultValue = "1") Integer pageNum, @RequestParam(defaultValue = "10") Integer pageSize, @RequestParam(required = false) String houseNo) { Page<HouseVO> page = houseService.pageQuery(pageNum, pageSize, houseNo); return Result.success(page); }

pageNum和pageSize给了默认值,前端不传也能正常返回第一页。houseNo是可选筛选条件,用required = false标注。Service 层里要把pageNum转成 offset,公式是(pageNum - 1) * pageSize,这个减一是新手最容易漏的地方,漏了第一页就会跳过前十条数据。

前端表格用 Element Plus 的el-table加el-pagination,数据加载放在onMounted里,分页组件的current-change事件触发重新请求。这里有个细节:筛选条件变化时要重置pageNum为 1,否则你在第 5 页筛选,结果只有 2 页,就会显示空数据。

3.3 报修工单的状态流转与前端交互

报修工单是物业系统里少有的带状态流转的模块,状态一般有:待处理、处理中、已完成、已取消。状态流转不能随便改,要有约束——比如已完成的单子不能再改回待处理。这个约束放在 Service 层做校验,不要只靠前端按钮置灰,因为接口是可以被直接调用的。

public void updateStatus(Long repairId, Integer targetStatus) { Repair repair = repairMapper.selectById(repairId); if (repair == null) { throw new BusinessException("工单不存在"); } // 已完成的工单不允许再变更状态 if (repair.getStatus() == 2) { throw new BusinessException("工单已完成,不可修改"); } repair.setStatus(targetStatus); repairMapper.updateById(repair); }

BusinessException是自定义异常,配合全局异常处理器@RestControllerAdvice,能把异常统一转成Result.fail返回,前端拿到的还是标准结构。这样前端不用区分是网络错误还是业务错误,拦截器里统一处理就行。

前端这边,工单列表里每一行的操作按钮要根据状态动态显示。待处理显示「接单」,处理中显示「完成」,已完成不显示操作按钮。这个判断用计算属性或者方法都行,但逻辑要跟后端的约束保持一致,否则会出现前端能点、后端拒绝的尴尬。

4. 联调、跨域与登录鉴权的避坑清单

4.1 开发环境跨域:代理配置比 @CrossOrigin 更省心

前后端分离开发时,前端跑在 5173,后端跑在 8080,浏览器同源策略会拦请求。解决办法有两种:后端加@CrossOrigin注解,或者前端配代理。我强烈建议用前端代理,因为@CrossOrigin在生产环境是个隐患,而且每个 Controller 都要加,容易漏。

Vite 的代理配置写在vite.config.js里:

export default defineConfig({ server: { proxy: { '/api': { target: 'http://localhost:8080', changeOrigin: true, rewrite: path => path.replace(/^\/api/, '') } } } })

changeOrigin: true是为了让后端收到的 Host 头是目标地址,某些框架会校验这个。rewrite把/api前缀去掉,所以后端接口路径里不用带/api。如果你后端本身就有/api前缀,那 rewrite 这行就去掉。

4.2 登录鉴权:JWT 存哪、怎么带、过期怎么办

登录成功后后端返回一个 token,前端存起来,后续请求带上。存哪里?localStorage 最简单,但要注意 XSS 风险;sessionStorage 关掉标签页就没了,适合安全性要求高的场景。物业系统这种内部使用的,localStorage 够用。

token 的携带方式,我一般放在请求头的Authorization字段里,不加 Bearer 前缀(加了也行,后端解析时对应处理)。后端用一个拦截器统一校验,放行登录接口和静态资源。

public class AuthInterceptor implements HandlerInterceptor { @Override public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) { // 放行预检请求 if ("OPTIONS".equals(request.getMethod())) { return true; } String token = request.getHeader("Authorization"); if (token == null || !JwtUtil.verify(token)) { response.setStatus(401); return false; } return true; } }

OPTIONS请求必须放行,否则跨域预检就失败了,这是很多人配了拦截器之后发现所有请求都 401 的原因。token 过期后返回 401,前端拦截器捕获到就清 token 跳登录页,这套闭环要打通。

4.3 避坑清单:五个我实际踩过的坑

现象一:前端请求一直报 404,但浏览器直接访问后端地址是通的。原因:代理的 rewrite 把路径改错了,或者 baseURL 和后端接口路径对不上。 解决:打开浏览器 Network 面板,看请求的实际 URL 是什么,跟后端 Controller 的@RequestMapping逐段比对。rewrite 规则用path.replace(/^\/api/, '')时,如果后端接口本身是/api/house/page,就会变成/house/page,对不上。

现象二:登录接口能通,其他接口全部 401。原因:拦截器把登录接口也拦了,或者 token 没正确带上。 解决:检查拦截器的excludePathPatterns里有没有放行/login。再看前端请求拦截器里 token 是从 localStorage 取的,登录成功后有没有真的存进去。我见过存的时候 key 写错,取的时候取不到的。

现象三:分页查询第二页开始数据重复或丢失。原因:SQL 的 limit offset 算错,或者排序字段不唯一导致分页不稳定。 解决:offset 公式确认是(pageNum - 1) * pageSize。排序要加一个唯一字段兜底,比如ORDER BY create_time DESC, id DESC,只按 create_time 排,时间相同的记录顺序是随机的,翻页就会乱。

现象四:前端打包后放到 SpringBoot 的 static 目录,刷新页面 404。原因:Vue 是单页应用,路由用的是 history 模式,刷新时浏览器直接请求/house/list这个路径,后端没有对应的 Controller。 解决:要么把 Vue 路由改成 hash 模式(路径带 #),要么在后端加一个转发配置,把所有非接口请求转发到 index.html。我一般用后者,配一个WebMvcConfigurer把未知路径 forward 到/index.html。

现象五:跨域配置了但还是报 CORS 错误。原因:同时配了前端代理和后端@CrossOrigin,或者拦截器在跨域处理之前就返回了 401。 解决:开发环境只用代理,生产环境用 nginx 同源部署,两边都不需要@CrossOrigin。如果非要用,确保拦截器放行了 OPTIONS 请求,并且@CrossOrigin加在 Controller 类上而不是方法上。

5. 打包部署与一个提效的接口调试习惯

5.1 前端打包放进 SpringBoot 的两种方式

部署方式有两种常见做法。第一种是前后端分开部署:前端npm run build生成 dist 目录,扔到 nginx 的 html 目录;后端打成 jar 单独跑。nginx 配一个 location 把/api转发到后端端口,其余走静态文件。这种方式职责清晰,前端更新不用重启后端。

第二种是前端打包后放进 SpringBoot 的src/main/resources/static目录,打成一个 jar 一起部署。适合小项目或者演示环境,省一台 nginx。做法是把 dist 里的文件复制到 static 下,然后配一个转发规则处理 history 路由。

@Configuration public class WebConfig implements WebMvcConfigurer { @Override public void addResourceHandlers(ResourceHandlerRegistry registry) { registry.addResourceHandler("/**") .addResourceLocations("classpath:/static/"); } @Override public void addViewControllers(ViewControllerRegistry registry) { // 所有非 api 开头的路径都转发到 index.html registry.addViewController("/{path:[^\\.]*}") .setViewName("forward:/index.html"); } }

addViewControllers里的正则[^\\.]*表示不包含点的路径,这样/house/list会被转发,而/js/app.js这种带点的静态资源请求不会被误转发。这个正则是关键,写错了要么静态资源 404,要么接口被转发。

5.2 用统一响应结构反推接口问题

最后分享一个我调试接口的习惯:因为所有接口都返回Result结构,我在前端 axios 的响应拦截器里加了一行日志,把res.code和res.msg打出来。这样联调时,如果某个接口返回的数据不对,我第一眼就能看到是 code 非 200 的业务错误,还是 code 200 但 data 为空的数据问题。前者去后端看异常日志,后者去检查 SQL 和参数。这个习惯帮我省掉了大量「到底是前端没传对还是后端没查对」的扯皮时间。

还有一点,接口文档不要偷懒。用 Swagger 或者 Knife4j 把每个接口的参数、返回值标注清楚,前端同事不用来问你「这个字段是必填吗」。物业系统的接口数量不多,但字段不少,标注清楚是一次投入长期受益的事。我自己踩过的最大一个坑就是早期没写文档,前端把houseId传成了buildingId,查出来的数据驴唇不对马嘴,排查了半天才发现是字段名传错了。从那以后,接口文档和参数校验我都是同步写的,@NotNull、@NotBlank这些注解加上,前端传错直接返回明确提示,比事后排查高效得多。希望帮到你。

本文还有配套的精品资源,点击获取

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询