SpringBoot+Vue3物业管理系统实战:前后端分离与MyBatis详解

发布时间:2026/10/3 1:42:34
SpringBoot+Vue3物业管理系统实战:前后端分离与MyBatis详解 一个人管理一个物业项目最头疼的事是什么业主信息散落在Excel里缴费记录乱七八糟报修单要打电话催。前阵子帮朋友的小区物业做管理系统我用Java SpringBoot搭建后端、Vue3写前端、MyBatis操作MySQL数据库前后端分离的形式把业主、房屋、缴费、报修、车位这些日常业务串成了一套完整系统。这套源码不复杂但麻雀虽小五脏俱全非常适合正在学Java后端、准备毕业设计或者想给中小物业做二开的开发者。本文就是基于这套源码的实战拆解把我踩过的坑、摸清楚的原理、还有可以直接抄的代码全部写出来。1. 项目整体设计与技术选型1.1 为什么是SpringBoot Vue3 MyBatis很多初学者喜欢纠结技术栈问为什么不直接用JSP加Servlet或者为什么不用JPA。我的真实感受是中小型管理系统选技术栈稳定性和可控性比新潮更重要。SpringBoot的优势在于“约定大于配置”不需要写一堆XML配置文件内嵌Tomcat一个java -jar就能跑起来。它把原本Spring繁琐的Bean配置简化成自动装配对业务系统开发特别友好。Vue3选择它是因为组合式APIComposition API对复杂业务逻辑的组织比Vue2清晰得多。在后台管理系统里一个页面往往同时要处理表格数据、筛选条件、弹窗表单、分页跳转Vue3的setup函数配合ref、reactive、computed可以把这些状态集中管理代码不会越写越乱。加上Vite构建工具冷启动速度明显快于webpack开发体验好很多。MyBatis是半自动ORM框架它和JPA最大的区别是SQL完全自己写。很多复杂的关联查询、报表统计、多条件筛选自己写SQL反而最简单。比如“按小区楼栋、业主姓名、缴费状态组合查询”MyBatis的动态SQL可以很优雅地拼接条件换成JPA生成JPQL调试成本会高不少。所以我个人在做管理系统时优先选择MyBatis而不是JPA。MySQL就不多说了中小项目、毕业设计、企业内网系统用它成本最低、兼容性最好文档也多。这套组合选下来后端写接口快前端做交互快SQL可控后期维护也不容易出幺蛾子。1.2 前后端分离架构的思路所谓前后端分离就是前端只负责页面展示和交互后端只提供JSON格式的接口数据。在这个项目里前端Vue3跑在5173端口后端SpringBoot跑在8080端口两边通过HTTP请求沟通。这样拆分有几个实在的好处第一前端工程师和后端工程师可以并行开发只要接口约定好了互不阻塞第二项目部署灵活前端构建出的静态文件可以扔到Nginx也可以放进后端的static目录还可以挂到CDN第三调试方便我可以在浏览器里直接看网络请求哪一步返回慢、哪一步报错打开控制台就清楚了。但分离也带来一个坑跨域。因为前端端口和后端端口不一样浏览器默认会拦截跨域请求。解决方式有两种一种是在后端写一个CorsConfig配置类允许前端来源另一种是让Vue开发服务器做代理把请求转发到后端。我两种都试过开发阶段用Vite代理更干净后端不用做任何处理。进入生产环境后前端Nginx配置反向代理也算是一种“归位”。除了跨域分离架构还要考虑Token认证。后端不存用户状态前端把登录成功后拿到的Token存起来每次请求放进请求头。这样后端服务可以水平扩展谁收到请求都能认出你是谁。这套系统里的角色权限也是靠Token里的用户ID和角色ID去后端查出来的。2. 核心模块与数据库设计2.1 小区管理系统的核心模块梳理这套系统的业务围绕小区物业的日常运转展开我把它梳理成了八个模块。第一是用户与权限模块管理员、物业工作人员、业主三种角色权限各不相同。第二是业主管理记录业主姓名、身份证号、手机号、紧急联系人还要绑定房屋。第三是房屋管理维护楼栋、单元、房号、面积、用途要支持住户入住和退房。第四是缴费管理物业费、水费、电费、停车费支持生成账单、标记已缴和打印收据。第五是报修管理业主提交报修物业派单、维修、回访状态要有流转。第六是车位管理记录车位编号、绑定车辆和业主月租到期要提醒。第七是公告管理物业发通知比如停水停电、交费提醒。第八是访客管理临时访客登记和业主关联方便放行。你不要小看这些模块它们不是孤立的。业主和房屋是一对多关系一个业主可能名下有多套房缴费和房屋是一对多关系账单挂在房号上报修和业主、房屋都关联车位和业主关联。如果数据库设计不好后面写接口会到处join越写越想哭。所以我在动手写代码前用两天时间专门建模把字段都推演了一遍。2.2 MySQL表结构设计要点我直接给出几张核心表的建表SQL你可以看到设计时的几个关键决策。CREATE TABLE house ( id BIGINT NOT NULL AUTO_INCREMENT COMMENT 房屋ID, building_no VARCHAR(20) NOT NULL COMMENT 楼栋号, unit_no VARCHAR(20) DEFAULT NULL COMMENT 单元号, room_no VARCHAR(20) NOT NULL COMMENT 房号, area DECIMAL(10,2) DEFAULT NULL COMMENT 建筑面积, owner_id BIGINT DEFAULT NULL COMMENT 当前业主ID, status TINYINT DEFAULT 0 COMMENT 0空置 1入住, create_time DATETIME DEFAULT CURRENT_TIMESTAMP, update_time DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, PRIMARY KEY (id), KEY idx_building_room (building_no, unit_no, room_no) ) ENGINEInnoDB COMMENT房屋表;CREATE TABLE owner ( id BIGINT NOT NULL AUTO_INCREMENT COMMENT 业主ID, name VARCHAR(30) NOT NULL, phone VARCHAR(20) NOT NULL, id_card VARCHAR(18) DEFAULT NULL, is_household TINYINT DEFAULT 1 COMMENT 1常住 0暂住, create_time DATETIME DEFAULT CURRENT_TIMESTAMP, PRIMARY KEY (id) ) ENGINEInnoDB COMMENT业主表;CREATE TABLE fee_order ( id BIGINT NOT NULL AUTO_INCREMENT, house_id BIGINT NOT NULL COMMENT 房屋ID, fee_type TINYINT NOT NULL COMMENT 1物业费 2水电费 3停车费, amount DECIMAL(10,2) NOT NULL, status TINYINT DEFAULT 0 COMMENT 0未缴 1已缴 2逾期, deadline DATE NOT NULL COMMENT 缴费截止日期, pay_time DATETIME DEFAULT NULL, create_time DATETIME DEFAULT CURRENT_TIMESTAMP, PRIMARY KEY (id), KEY idx_house (house_id), KEY idx_status (status) ) ENGINEInnoDB COMMENT缴费订单表;表设计的经验给你几条避坑建议。金额字段一定要用DECIMAL不要用FLOAT或者DOUBLE否则后面算账会出现0.10.2不等于0.3的尴尬。时间字段建议用DATETIME如果不需要时区换算别用TIMESTAMP因为TIMESTAMP有2038年问题。外键能不用就不用用逻辑关联就好。我见过很多初学者给每个表都加FOREIGN KEY结果改一个数据报外键约束错误找半天原因。在业务系统里外键约束会让迁移和归档变得很痛苦我们只需要在关联字段上建普通索引靠应用层来保证一致性。索引也要控制数量不是越多越好。这个项目最重要的查询场景是“按楼栋房号找房”“按业主手机号找人”“按缴费状态统计订单”所以我建的都是联合索引和覆盖索引避免回表。比如房屋表建的idx_building_room查询某一栋楼的所有房屋时一次索引就能覆盖。2.3 MyBatis的运用与XML映射MyBatis的XML映射是整个数据层的关键。我见过不少同学把SQL写在注解里比如Select一两个查询还看得过去一旦遇到动态多条件查询注解里的字符串拼接会让人崩溃。XML文件配上if、where标签写起来和维护起来都舒服得多。这个项目里最典型的一个查询是“业主列表组合筛选”管理员输入业主姓名、手机号选择小区楼栋分页查询。我用XML写就是这样select idselectOwnerPage resultTypecom.xxx.entity.Owner SELECT o.*, h.building_no, h.unit_no, h.room_no FROM owner o LEFT JOIN house h ON h.owner_id o.id where if testname ! null and name ! AND o.name LIKE CONCAT(%, #{name}, %) /if if testphone ! null and phone ! AND o.phone LIKE CONCAT(%, #{phone}, %) /if if testbuildingNo ! null and buildingNo ! AND h.building_no #{buildingNo} /if /where ORDER BY o.create_time DESC /select这里有几个关键点。第一where标签会自动处理第一个条件前面的AND不需要自己拼字符串。第二所有参数绑定都用#{}MyBatis会生成预编译SQL防止注入。千万不要用${}直接拼接用户输入除非你十分确定它是固定枚举值。第三CONCAT(%, #{name}, %)这种写法比在Java里拼好%再传进去更安全因为防止了SQL注入而且可读性好。有人问MyBatis的TypeHandler到底怎么用这个项目里就有场景。比如数据库里fee_type字段我存的是TINYINTJava里呢我不想用裸Integer希望直接拿到“物业费”“水电费”这种语义更强的枚举对象。最优雅的方式是自定义一个TypeHandler把Integer和枚举互转。你可以继承BaseTypeHandler在setNonNullParameter里把枚举转为Integer在getNullableResult里把Integer转为枚举。编写好之后在mybatis-config里注册或者在XML里直接指定。还有个坑必须讲MyBatis的一级缓存默认开启二级缓存默认关闭。在单体系统里一级缓存作用不大因为每次Mapper调用结束后SqlSession就关闭了缓存就失效。如果后面你开了二级缓存要小心脏数据问题。我之前有个项目给一个统计报表开了二级缓存结果新数据插入后相同查询还是返回旧结果排查了好久才发现是缓存没刷新。所以对于实时性要求高的数据不要把缓存级别开太大。3. 前后端实现的关键细节3.1 SpringBoot后端分层与接口设计后端代码我按Controller - Service - Mapper三层来写再加一个entity实体层、dto传输层、vo视图层。Controller只负责接参数、调Service、返回结果Service只处理业务逻辑Mapper只做SQL数据访问。这样做的好处是出错后凭异常堆栈就能很快定位到是在哪一层出错。每个Controller的返回类型都统一用Result对象。Result类是我自己定义的包含code、message、data三个字段。成功时code为200业务错误为400或者500。前端axios解析到code不为200时统一弹出Message提示不用每个接口单独去处理错误分支。全局异常处理器用RestControllerAdvice兜底把未捕获的异常、参数校验异常、业务异常分别包装成Result返回。这样能避免把Java堆栈直接甩给前端。接口设计上我尽量遵循RESTful风格。比如业主管理的接口长这样GET /api/owner/page?pageNum1pageSize10 POST /api/owner PUT /api/owner/{id} DELETE /api/owner/{id}分页参数我统一用pageNum和pageSize返回结果是PageResult里面包含list、total、pageNum、pageSize。这个PageResult也是自定义的让前端知道总共有多少条记录方便做分页组件。有一个细节容易忽略Service层接口命名。别太随意要让人一眼看出意图。getOwnerById、createOwner、updateOwner、deleteOwner这套命名比doSomething可维护性高得多。另外事务处理很关键比如“注册业主并绑定房屋”两步操作必须在一个事务里完成我就在Service方法上加了Transactional。如果中途失败数据库自动回滚不会出现业主建了但房子没绑上的脏数据。3.2 Vue3前端工程化与组件设计前端我用Vite创建项目模板选vue然后手动装上了Vue Router和Pinia。UI组件库选了Element Plus它对Vue3的兼容性很好表格、表单、弹窗、分页这些后台组件已经有了不用自己造轮子。页面的组织方式是这样的src/ api/ // 和后端接口一一对应的请求函数 assets/ components/ // 通用组件比如UploadButton router/ // 路由配置 store/ // Pinia状态管理我用来存用户信息和菜单权限 views/ login/ dashboard/ owner/ house/ fee/ repair/ ...axios的封装是重点我单独写了一个request.js文件。里面做了三件事baseURL从环境变量读取开发环境指向http://localhost:8080生产环境指向部署地址请求拦截器从localStorage取Token塞到请求头响应拦截器统一判断HTTP状态码遇到401就清理用户信息并跳转到登录页。表单校验是前端很容易忽略的环节。比如缴费账单模块新增账单截止日期是必填项而且不能早于今天。Element Plus的表单rules里日期校验可以这样写const rules { deadline: [ { required: true, message: 请选择缴费截止日期, trigger: blur }, { validator: (rule, value, callback) { if (value value.getTime() Date.now()) { callback(new Error(截止日期不能早于今天)); } else { callback(); } }, trigger: change } ] };自定义validator这个用法很实用。Element Plus的日期组件绑定值本身是Date对象直接用getTime比较就行。这里有个小坑如果你绑定的是字符串就需要先转成Date再比较很多同学就是在这里报“value.getTime is not a function”。3.3 前后端联调与权限控制逻辑联调阶段最容易出问题的就是跨域和Token。开发阶段我建议在Vue的vite.config.js里配置代理server: { proxy: { /api: { target: http://localhost:8080, changeOrigin: true } } }这样前端请求/api/owner的时候Vite会自动转发到后端的8080接口浏览器里看不到跨域。如果你非要后端开CORS也可以但要记得在SpringBoot里设置allowedOriginPatterns同时把allowCredentials设为true否则带Cookie的请求会失败。权限控制这块我采用了“前端路由守卫后端拦截器”的经典组合。前端在登录后存下用户信息里面包含角色。路由配置里给需要权限的页面加上meta.roles字段例如管理员和物业人员的页面roles里写[ADMIN,PROPERTY]。Vue Router的beforeEach守卫里检查当前用户角色是否在允许列表里不在就跳转到403页面。但前端限制只是用户体验层面的真正的权限控制一定在后端。我在SpringBoot里写了一个拦截器拦截所有/api/**请求解析Token然后把用户ID和角色放入ThreadLocal。在Controller方法上加上RequireRole(ADMIN)这类自定义注解由拦截器或者AOP判断当前用户是否有权限访问。这样即使有人绕过前端直接调接口也能被拦住。4. 实操过程从源码到运行4.1 环境准备与项目初始化拿到源码后第一步是检查环境。这套系统需要JDK 1.8以上推荐JDK 17、Maven 3.6以上、Node.js 16以上、MySQL 5.7或者8.0。我用的版本是JDK 17、Maven 3.8.8、Node 18.16、MySQL 8.0整体没有遇到版本兼容的大坑。后端项目是标准的Maven结构pom.xml里引入的依赖包括spring-boot-starter-web、mybatis-spring-boot-starter、mysql-connector-j、lombok、jjwt做Token等。开始跑项目前先执行mvn clean install把依赖拉下来。有时候因为网络原因拉包会失败可以换Maven源我这里是用的阿里云镜像速度快很多。前端项目执行npm install这个步骤最容易卡住。如果你发现一堆peer dependency冲突报错不要慌通常不是代码问题而是版本依赖关系太严。执行npm install --legacy-peer-deps可以绕过严格依赖检查。另外npm run dev启动时如果提示端口被占用就去vite.config.js里改一下server.port就行。4.2 数据库导入与配置源码的根目录里一般会有一个sql文件夹里面放着初始化脚本比如community.sql。先在MySQL里创建一个数据库然后导入脚本CREATE DATABASE community DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci; USE community; SOURCE /你的路径/sql/community.sql;导入成功后可以快速查看一下表数量和数据。然后打开后端项目的application.yml把数据库连接信息改成你自己的spring: datasource: url: jdbc:mysql://localhost:3306/community?useUnicodetruecharacterEncodingutf8useSSLfalseserverTimezoneAsia/ShanghaiallowPublicKeyRetrievaltrue username: root password: 你的密码 driver-class-name: com.mysql.cj.jdbc.Driver mybatis: mapper-locations: classpath:mapper/*.xml type-aliases-package: com.example.community.entity configuration: map-underscore-to-camel-case: true log-impl: org.apache.ibatis.logging.stdout.StdOutImpl这里有两个关键的坑我一个个说。useSSLfalse一定要加不然MySQL 8会尝试建立SSL连接报一堆SSL相关的警告甚至直接报错。serverTimezoneAsia/Shanghai是解决时区问题不加的话日期字段会差8小时。allowPublicKeyRetrievaltrue是配合MySQL 8的caching_sha2_password认证插件用的不然可能报“Public Key Retrieval is not allowed”。MyBatis的map-underscore-to-camel-case必须打开不然数据库里的create_time字段映射不到Java的createTime属性你会发现明明SQL查出来了但对象里全是null。log-impl可以在开发时打印SQL让你看到实际执行的语句生产环境记得关掉。4.3 启动后端与前端后端启动很简单在项目根目录执行mvn spring-boot:run或者用IDE直接运行启动类的main方法。启动日志里出现“Tomcat started on port 8080”就说明成功。接着启动前端在vue目录下执行npm run dev浏览器访问http://localhost:5173看到的是登录页。如果前端调用接口一切正常登录之后就能看到工作台表格数据。如果后端返回404先检查前端访问的接口路径和后端Controller的RequestMapping路径是否一致。我经常遇到的一个问题是前端请求/api/owner后端接口类上写的是RequestMapping(/api/owners)多了一个s这属于低级错误排查起来却要花时间。生产部署时我习惯把前端构建产物放到后端Web服务里。执行npm run build生成了dist目录把里面的文件全部复制到SpringBoot的src/main/resources/static目录下然后重新打包mvn package启动后直接访问后端端口就能看到前端页面。这适合小规模部署一台服务器搞定。如果后续并发上来了把前端部署到Nginx后端单独部署到Tomcat再做一个反向代理也是水到渠成的事。5. 常见问题与排查技巧实录5.1 MySQL连接常见报错这个项目遇到的数据库连接报错我把典型问题整理成一个速查表。报错现象原因解决办法SSL连接警告或报错MySQL 8默认启用SSLJDBC URL加useSSLfalsePublic Key Retrieval is not allowedcaching_sha2_password插件限制JDBC URL加allowPublicKeyRetrievaltrueUnknown database数据库名输错检查community是否已创建Access denied for user用户名密码错误或权限不足确认账号能访问该库可以GRANT ALL ON community.* TO rootlocalhostConnection refusedMySQL服务没启动/端口不是3306检查MySQL进程和端口如果你刚安装MySQL建议用8.0的二进制包解压后执行mysqld --initialize-insecure初始化然后net start mysql启动密码为空。生产环境再用ALTER USER把密码改掉。这个流程我在好几台机器上跑过基本不会出错。5.2 前端依赖安装与启动问题前端最常见的报错就是npm install时node-sass相关报错。由于新版本的Node已经移除node-sass支持如果老项目依赖node-sass十有八九会编译失败。这套系统的Vue3项目不会用node-sass一般用sassdart sass兼容性更好。如果还是遇到sass的编译问题检查Python和C构建工具是否安装Windows上可能需要npm install -g windows-build-tools。还有一个问题npm run dev启动后页面白屏打开控制台看到“Uncaught TypeError: Cannot read properties of null”这类错误通常是入口文件里挂载元素没找到。检查index.html里有没有div idapp/divmain.js里createApp(App).mount(#app)的id是否匹配。我遇到过一次同事把id写成了root前端白屏半小时。如果路由刷新后404说明前端路由用的是history模式而部署环境没有做回退配置。开发模式没事生产环境需要给Nginx配上try_files $uri $uri/ /index.html;或者在SpringBoot里让404请求全部转发到index.html。5.3 MyBatis映射不生效与SQL排查很多人第一次跑MyBatis项目控制台报“Invalid bound statement (not found)”其实就是Mapper接口和XML映射没有正确关联。检查三处第一XML文件是否在mapper-locations配置的路径下比如classpath:mapper/*.xml第二XML的namespace是否等于Mapper接口的全限定名第三Mapper接口的方法名和XML中select/update的id是否完全一致。还有个容易忽略的问题XML文件在Maven项目里默认不会被编译到target目录。如果你的XML放在src/main/java下面需要在pom.xml里配置resources把.xml也打包进去。我自己习惯把XML统一放在src/main/resources/mapper下天然就会被打包省了很多心。开发时一定要把MyBatis的SQL日志打开。配置了log-impl: org.apache.ibatis.logging.stdout.StdOutImpl之后每个SQL执行前会打印完整SQL和参数。我看到过不少同学问为什么查不到数据一开日志发现SQL少了一个where条件或者参数传成了null一眼就能定位。真正排查疑难杂症时把SQL复制到Navicat里执行一遍对比结果基本都能找到答案。5.4 跨域与Token链路排查前后端分离项目一旦出现请求失败先看控制台网络请求。如果报CORS错误确认后端是否开放了对应域名。我之前遇到过一个小知识点localhost:5173和127.0.0.1:5173是两个不同Origin后端CORS如果只允许了http://localhost:5173用127.0.0.1访问前端就会跨域失败。统一用localhost访问或者CORS里写成allowedOriginPatterns(*)并设置好allowCredentials。Token失效的排查也很有代表性。我通常按三步走第一步看浏览器Network里请求头有没有Authorization第二步看后端日志里Token解析是否报错第三步看用户信息是否过期。很多同学把Token存到sessionStorage刷新浏览器就没数据了我建议用localStorage存Token因为sessionStorage在标签页关闭后会清空不符合“记住登录状态”的预期。这里的权限拦截器还有一个坑对登录接口、验证码接口、静态资源路径要放行否则会出现死循环。你在拦截器里写完excludePathPatterns(/api/login, /api/captcha, /error)之后记得打印一条日志看看每次请求是否真的被放行。我见过因为写错了路径导致登录接口永远返回401前端卡在登录页的案例。这套系统跑起来之后我最大的感受是项目不是代码写完了就算完成能顺利部署运行、能应对基础故障才是真正理解了这套技术栈。如果你正打算拿它做毕业设计或者接手类似项目花半天时间把数据表结构和前后端请求链路捋清楚后面就能畅通无阻了。

关于本文作者

来自尧图内容编辑团队

尧图内容编辑团队 内容团队

尧图内容编辑团队

本文由尧图网络内容编辑团队执笔。团队由资深项目经理、前端工程师与设计师组成,所有内容均来自亲手交付的真实项目,先讲清问题、再给出可落地的解法。尧图深耕北京网站建设十年,服务过京华建材集团、智造科技等各行业客户,把一线经验沉淀为可复用的行业观察。

  • 十年建站经验,覆盖建材、制造、服务、文创等
  • 项目经理把关选题与事实准确性
  • 工程师与设计师联合撰写专业细节
  • 统一编辑规范,保证文风与排版一致
  • 每月复盘转化数据,迭代选题方向

延伸阅读

相关资讯与近期热门内容

深度阅读推荐

建站决策前值得细读的三篇

网站改版的5个关键决策
2024-08-12

网站改版的5个关键决策

什么时候该改版、改到什么程度、如何避免流量掉光,京华建材集团改版复盘给出答案。

获取专属建站方案

看完文章,把您的行业与预算告诉我们,免费获取一份量身定制的官网建设方案与报价。

立即免费咨询