ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

SpringBoot3 + Vue3 + MySQL 社区物业管理系统全栈开发实战

SpringBoot3 + Vue3 + MySQL 社区物业管理系统全栈开发实战 社区物业管理系统是一个很典型的“业务不复杂但链路很完整”的 Java 全栈项目。常见功能包括业主与房屋档案维护、物业费账单生成、欠费统计、报修工单流转、公告发布等。它不像电商系统那样有高并发和复杂状态机但仍然需要处理多表关联、角色权限、金额精度和前后端联调问题非常适合把 JAVA SpringBoot3 Vue.js3 MySQL 这条完整开发链路串起来。下面从一个可运行的最小闭环出发先建库建表再用 Spring Boot3 提供房屋和费用账单接口然后用 Vue.js3 写一个管理页面最后完成后端接口、前端页面与 MySQL 数据的联调验证。学习环境里把它跑通后再按最后一部分的清单向生产环境靠拢。1. 社区物业管理系统真正要解决的是“状态一致”1.1 先理解系统里的核心对象物业管理系统表面上功能很多但核心对象其实可以归纳成几个维度对象典型字段说明房屋小区、楼栋、单元、房号、面积、户型整张业务表都围绕房屋建立业主姓名、电话、证件信息、关联房屋一个房屋可能对应多个共同居住人缴费账单房屋、费用项目、金额、账期、状态物业费的核心流程落在这张表上报修工单房屋、报修内容、状态、处理人需要工作流状态流转公告标题、内容、发布时间、发布范围面向业主的通知内容在一个真实项目中房屋房屋表和业主表往往不是同一条记录。房屋是物理对象业主是居住人对象两者通过“房屋业主关系表”关联这样业主换房或转让时不需要把物理房号改掉。第一版练习可以先不做关系表把“业主姓名、电话”直接放在房屋表里逻辑更简单但要知道这个简化到生产环境通常不够。1.2 为什么选 SpringBoot3 Vue.js3 MySQL很多社区类管理系统使用这组技术栈不是因为它在所有场景中性能最强而是因为它能满足前后端分离开发、数据库事务、窗口化 UI 开发这几个基本要求。Spring Boot3 负责接口层和业务层。它内置了 Web 容器、数据访问、参数校验、异常处理等能力让开发者不需要先搭建 Tomcat也不需要手动绑定请求处理链。Vue.js3 负责页面层。它用响应式数据管理界面状态配合 Element Plus 这类组件库可以快速搭建表格、表单、弹窗和筛选条件。MySQL 负责数据层。像物业费账单、房屋列表、报修工单这类结构化数据用关系型数据库管理最直接也容易做报表统计和事务控制。如果你是练习这套组合的价值是把后端分层、前端组件化、数据库设计与 API 设计完整地串联一次。1.3 第一版功能边界怎么定义不要一开始就把门禁、停车、资产、投诉、巡检全部做进去。推荐的切入方式是“一栋一费一工单”。一栋房屋列表能按楼栋、单元、房号查询。一费物业费账单能按房屋和缴费状态筛选。一工单报修工单能提交和变更状态。完成这三个流程后权限、消息通知、导入导出再逐步叠加。这样每一轮都有可运行的交付不会因为实体过多导致前后端代码失控。2. 环境准备JDK17、SpringBoot3、Vue3、MySQL 8 必须版本对齐2.1 开发环境清单Spring Boot 3 对基础环境有明确要求最低需要 Java 17。这里使用的示例以 Spring Boot 3.2 为基线高版本兼容性以你当前项目实际为准。软件用途建议版本JDK编译运行后端17 或 21Maven后端依赖管理3.8 以上Spring Boot后端应用框架3.2.x 或更高稳定版MySQL数据存储8.0 系列Node.js运行前端构建工具20 LTS 或更高npm安装前端依赖随 Node 自带Vue.js页面框架Vue.js3Vite前端开发服务器和打包5 或兼容当前 Node 的稳定版开始前先检查版本避免在后端启动阶段才发现 JDK 或 MySQL 版本不匹配。java -version mvn -version node -v npm -v mysql --version注意不要把本地“能启动”当作环境正确的标准。Spring Boot3 使用jakarta.*取代javax.*如果旧项目里大量使用javax.servlet升级到 Boot3 时这些引用会直接编译失败。2.2 数据库初始化用房屋表和缴费表承载主链路新建一个数据库命名为community_pms。字符集统一使用utf8mb4因为 MySQL 旧默认字符集的utf8在遇到部分特殊字符时会出问题。CREATE DATABASE IF NOT EXISTS community_pms DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci; USE community_pms; CREATE TABLE house ( id BIGINT PRIMARY KEY AUTO_INCREMENT, building_no VARCHAR(20) NOT NULL, unit_no VARCHAR(20) NOT NULL, room_no VARCHAR(20) NOT NULL, owner_name VARCHAR(50) NOT NULL, owner_phone VARCHAR(20), area DECIMAL(10,2) NOT NULL DEFAULT 0.00, status TINYINT NOT NULL DEFAULT 1 COMMENT 1 已交付0 空置, create_time DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, UNIQUE KEY uk_building_unit_room (building_no, unit_no, room_no) ) ENGINEInnoDB COMMENT房屋档案表;缴费表设计时要注意金额字段必须使用DECIMAL不能使用浮点类型。财务金额如果使用FLOAT或DOUBLE累计对账时会差出分钱。CREATE TABLE fee_bill ( id BIGINT PRIMARY KEY AUTO_INCREMENT, house_id BIGINT NOT NULL, bill_name VARCHAR(100) NOT NULL COMMENT 费用项物业费、水费等, bill_period VARCHAR(20) NOT NULL COMMENT 账期例如 2025-01, amount DECIMAL(10,2) NOT NULL COMMENT 应收金额, paid_amount DECIMAL(10,2) NOT NULL DEFAULT 0.00 COMMENT 已缴金额, status TINYINT NOT NULL DEFAULT 0 COMMENT 0 未缴1 部分缴2 已缴清, due_date DATE, pay_time DATETIME, create_time DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, KEY idx_house_id (house_id), KEY idx_status (status), CONSTRAINT fk_fee_bill_house FOREIGN KEY (house_id) REFERENCES house (id) ) ENGINEInnoDB COMMENT缴费账单表;这里把status设计成冗余状态后续每次缴费完成后都要重新计算它。虽然也可以每次只根据amount和paid_amount推算但状态字段在列表筛选场景下性能更好也更直观。插入几条测试数据方便前后端联调时观察结果INSERT INTO house (building_no, unit_no, room_no, owner_name, owner_phone, area) VALUES (1, 1, 101, 张三, 13800000001, 89.50), (1, 1, 102, 李四, 13800000002, 95.00), (2, 2, 201, 王五, 13800000003, 120.00); INSERT INTO fee_bill (house_id, bill_name, bill_period, amount, paid_amount, status, due_date) VALUES (1, 物业费, 2025-01, 896.00, 896.00, 2, 2025-02-15), (2, 物业费, 2025-01, 950.00, 500.00, 1, 2025-02-15), (3, 物业费, 2025-01, 1200.00, 0.00, 0, 2025-02-15);2.3 后端工程和服务端口规划建议后端项目名使用 Pascal 风格目录例如community-pms-server前端目录用community-pms-web。两个目录不要混在一起后续部署时前后端可以独立构建。后端端口固定为8080前端 Vite 开发服务端口为5173。前端开发模式下通过代理把/api转发到后端从而绕开跨域问题。生产构建时再把前端dist目录交给 Nginx 或 Spring Boot 静态资源目录处理。3. SpringBoot3 后端实现先跑通房屋、缴费账单接口3.1 Maven 依赖与 Spring Boot 3 的包名变化创建一个 Maven 工程后pom.xml核心内容如下parent groupIdorg.springframework.boot/groupId artifactIdspring-boot-starter-parent/artifactId version3.2.5/version relativePath/ /parent properties java.version17/java.version /properties dependencies dependency groupIdorg.springframework.boot/groupId artifactIdspring-boot-starter-web/artifactId /dependency dependency groupIdorg.springframework.boot/groupId artifactIdspring-boot-starter-jdbc/artifactId /dependency dependency groupIdcom.mysql/groupId artifactIdmysql-connector-j/artifactId scoperuntime/scope /dependency dependency groupIdorg.springframework.boot/groupId artifactIdspring-boot-starter-test/artifactId scopetest/scope /dependency /dependencies这里没有引入 JPA 或 MyBatis-Plus而是使用JdbcTemplate。原因是第一版只是想验证接口能不能跑通数据链路少一层 ORM 配置后续排错范围更小。如果已经熟悉 JPA 或 MyBatis-Plus可以替换为对应依赖但核心表设计和接口设计不会变化。启动类package com.example.pms; import org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.SpringBootApplication; SpringBootApplication public class CommunityPmsApplication { public static void main(String[] args) { SpringApplication.run(CommunityPmsApplication.class, args); } }3.2 application.yml 中的数据源配置MySQL 8 和旧版 MySQL 的驱动类名不同。8.x 使用com.mysql.cj.jdbc.Driver连接地址通常建议加上时区和编码参数。server: port: 8080 spring: datasource: url: jdbc:mysql://localhost:3306/community_pms?useUnicodetruecharacterEncodingutf8useSSLfalseserverTimezoneAsia/ShanghaiallowPublicKeyRetrievaltrue username: root password: your_password driver-class-name: com.mysql.cj.jdbc.Driver这里需要注意几个点characterEncodingutf8保证中文正常写入。serverTimezoneAsia/Shanghai避免日期时间少 8 小时。allowPublicKeyRetrievaltrue是 MySQL 8 使用caching_sha2_password认证时常见配置本地开发可以开启。3.3 实体类与查询 Repository先定义一个最精简的FeeBill实体类对应查询结果。代码中省略 getter/setterIDE 会自动生成package com.example.pms.model; import java.math.BigDecimal; public class FeeBill { private Long id; private Long houseId; private String houseAddress; private String billName; private BigDecimal amount; private BigDecimal paidAmount; private Integer status; // 自动生成 getter/setter }查询逻辑放在 Repository 中避免 Controller 直接拼 SQL。这里的 SQL 用了 MySQL 8 支持的文本块写法阅读性更清晰package com.example.pms.repository; import com.example.pms.model.FeeBill; import org.springframework.jdbc.core.JdbcTemplate; import org.springframework.stereotype.Repository; import java.util.List; Repository public class FeeBillRepository { private final JdbcTemplate jdbcTemplate; public FeeBillRepository(JdbcTemplate jdbcTemplate) { this.jdbcTemplate jdbcTemplate; } public ListFeeBill query(Integer status, Long houseId) { String sql SELECT f.id AS id, f.house_id AS houseId, CONCAT(h.building_no, -, h.unit_no, -, h.room_no) AS houseAddress, f.bill_name AS billName, f.amount AS amount, f.paid_amount AS paidAmount, f.status AS status FROM fee_bill f INNER JOIN house h ON h.id f.house_id WHERE (? IS NULL OR f.status ?) AND (? IS NULL OR f.house_id ?) ORDER BY f.due_date DESC, f.id DESC ; return jdbcTemplate.query(sql, (rs, rowNum) - { FeeBill bill new FeeBill(); bill.setId(rs.getLong(id)); bill.setHouseId(rs.getLong(houseId)); bill.setHouseAddress(rs.getString(houseAddress)); bill.setBillName(rs.getString(billName)); bill.setAmount(rs.getBigDecimal(amount)); bill.setPaidAmount(rs.getBigDecimal(paidAmount)); bill.setStatus(rs.getInt(status)); return bill; }, status, status, houseId, houseId); } }这个 SQL 有两个关键设计使用INNER JOIN house把房屋地址拼出来前端不需要再去请求房屋接口。使用? IS NULL OR f.xxx ?实现可选筛选条件。传入空值时不限制传入状态时只查询对应状态。3.4 统一返回结果与接口 Controller为了让前端统一处理成功和失败接口返回包装成统一结构package com.example.pms.common; public record ResultT(int code, String message, T data) { public static T ResultT success(T data) { return new Result(0, ok, data); } public static T ResultT error(String message) { return new Result(500, message, null); } }Controller 使用RestController直接返回对象package com.example.pms.controller; import com.example.pms.common.Result; import com.example.pms.model.FeeBill; import com.example.pms.repository.FeeBillRepository; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RequestParam; import org.springframework.web.bind.annotation.RestController; import java.util.List; RestController RequestMapping(/api/fees) public class FeeBillController { private final FeeBillRepository feeBillRepository; public FeeBillController(FeeBillRepository feeBillRepository) { this.feeBillRepository feeBillRepository; } GetMapping public ResultListFeeBill list( RequestParam(required false) Integer status, RequestParam(required false) Long houseId) { return Result.success(feeBillRepository.query(status, houseId)); } }这里为什么路径用复数/api/fees因为 REST 风格约定资源用复数命名前端看到/fees会自然理解成“费用账单集合”。方法名不叫getList或queryList而是通过查询参数表达筛选条件接口语义更清晰。3.5 CORS 配置与本地联调如果前端直接请求http://localhost:8080/api/fees而不是走 Vite 代理就会触发浏览器跨域限制。可以先配置一个允许本地开发源的 CORS 规则package com.example.pms.config; import org.springframework.context.annotation.Configuration; import org.springframework.web.servlet.config.annotation.CorsRegistry; import org.springframework.web.servlet.config.annotation.WebMvcConfigurer; Configuration public class CorsConfig implements WebMvcConfigurer { Override public void addCorsMappings(CorsRegistry registry) { registry.addMapping(/api/**) .allowedOrigins(http://localhost:5173) .allowedMethods(GET, POST, PUT, DELETE, OPTIONS) .allowedHeaders(*); } }注意不要把allowedOrigins配置成*后直接部署上线。生产环境应该使用实际域名或通过网关统一处理跨域。4. Vue.js3 前端实现搭建缴费管理页面4.1 创建 Vite Vue3 工程在 backend 同级的目录下创建前端工程npm create vitelatest community-pms-web -- --template vue cd community-pms-web npm install npm install axios element-plusnpm create vite会生成基础目录。安装 Element Plus 后在入口文件中做全局注册。为了快速跑通可以先全量引入后续项目增大后再改成按需引入。import { createApp } from vue import ElementPlus from element-plus import element-plus/dist/index.css import App from ./App.vue const app createApp(App) app.use(ElementPlus) app.mount(#app)4.2 配置 Vite 代理与 Axios 封装后端接口路径是/api前端开发服务器是5173。在vite.config.js中配置代理import { defineConfig } from vite import vue from vitejs/plugin-vue export default defineConfig({ plugins: [vue()], server: { host: 0.0.0.0, port: 5173, proxy: { /api: { target: http://localhost:8080, changeOrigin: true } } } })封装 Axios 实例时统一处理后端Result结构。这里采取“接口成功直接返回data”的策略页面代码不需要每次都写response.data.data。import axios from axios const request axios.create({ baseURL: /api, timeout: 10000 }) request.interceptors.response.use( (response) { const res response.data if (res.code ! 0) { return Promise.reject(new Error(res.message || 请求失败)) } return res.data }, (error) Promise.reject(error) ) export default request然后新建费用相关的 API 模块import request from ./request export function getFeeList(params) { return request.get(/fees, { params }) }4.3 实现账单列表和状态筛选页面新建src/views/FeeList.vue这个页面是“业主缴费列表”的核心界面。它通过查询参数status调用后端接口再在表格中展示房屋、费用项、金额和状态。script setup import { onMounted, reactive, ref } from vue import { getFeeList } from ../api/fee const loading ref(false) const list ref([]) const query reactive({ status: null }) async function load() { loading.value true try { list.value await getFeeList({ ...query }) } finally { loading.value false } } function money(value) { return Number(value).toFixed(2) } function statusText(value) { const map { 0: 待缴费, 1: 部分缴费, 2: 已缴清 } return map[value] || 未知 } onMounted(load) /script template section div stylemargin-bottom: 16px el-select v-modelquery.status placeholder缴费状态 clearable stylewidth: 200px el-option label待缴费 :value0 / el-option label部分缴费 :value1 / el-option label已缴清 :value2 / /el-select el-button typeprimary stylemargin-left: 12px clickload 查询 /el-button /div el-table v-loadingloading :datalist border stripe el-table-column prophouseAddress label房屋 width180 / el-table-column propbillName label费用项 width140 / el-table-column label应收金额 width120 template #default{ row }{{ money(row.amount) }}/template /el-table-column el-table-column label已缴金额 width120 template #default{ row }{{ money(row.paidAmount) }}/template /el-table-column el-table-column label状态 width120 template #default{ row } el-tag :typerow.status 2 ? success : row.status 1 ? warning : danger {{ statusText(row.status) }} /el-tag /template /el-table-column /el-table /section /template在这个页面上查询按钮的职责是“重新拉取接口”而不是在本地过滤数据。这样设计的目的是让前端始终与后端数据保持一致避免用户看到的列表和数据库实际记录不一致。5. 联调验证从启动命令到接口用例5.1 后端启动和接口自测启动后端cd community-pms-server mvn spring-boot:run看到Started CommunityPmsApplication日志后先用浏览器或curl验证接口curl http://localhost:8080/api/fees?status0预期返回 JSON 结构{ code: 0, message: ok, data: [ { id: 3, houseId: 3, houseAddress: 2-2-201, billName: 物业费, amount: 1200.00, paidAmount: 0.00, status: 0 } ] }如果接口返回空数组优先检查 MySQL 是否插入了测试数据以及后端连接的库名是不是community_pms。这个问题最常见不是代码逻辑错而是连错了数据库。再验证带房屋筛选curl http://localhost:8080/api/fees?houseId2此时应该只返回 2 号房屋的账单列表中房屋地址是1-1-102。5.2 前端启动和代理联调启动前端cd community-pms-web npm run dev访问http://localhost:5173。打开浏览器开发者工具的 Network 面板点击“查询”按钮时应该看到请求http://localhost:5173/api/fees然后由 Vite 转发到http://localhost:8080/api/fees。如果 Network 面板里出现 CORS 错误说明请求没有经过代理而是直接请求了后端绝对地址。检查 Axios 的baseURL是否设置了/api以及vite.config.js中的代理是否配置了/api。5.3 验证维度不只是“能跑”还要看数据一致性联调验证至少要看四层结果验证层检查内容判断标准环境层后端和前端端口启动成功日志无启动异常接口层/api/fees返回正常 JSON状态码是 2xxcode0数据层数据库中的账单记录正确返回记录的金额与 SQL 查询一致交互层点击状态筛选后列表变化页面数据随参数更新不要只看到页面有数据就认为联调成功。应该先改 MySQL 中某条记录的status再刷新页面确认页面数据和数据库一致。这样才能验证“前端读的是数据库”而不是前端自己造的假数据。6. 全栈项目里最容易踩到的几个坑6.1 CORS 配置了却仍然报跨域现象前端访问/api/fees返回 CORS 错误但后端明明加了CorsConfig。排查路径先确认请求是不是从 Vite 开发服务器发出的。如果前端地址是5173请求地址却是http://localhost:8080/api/fees这时才会触发跨域。走 Vite 代理时浏览器看到的是同源请求。如果使用代理就不需要依赖 CORS 配置。如果必须跨域检查allowedOrigins是否写成了http://localhost:5173/末尾斜杠会导致源不匹配。推荐做法开发环境统一走 Vite 代理CORS 只作为接口层兜底。6.2 Axios 返回结果被包裹了两层后端返回的是{ code: 0, data: [] }如果页面代码直接写res.data而响应拦截器已经返回了res.data页面拿到的是data字段里的数组再接一层res.data就会变成undefined。统一约定很关键响应拦截器成功时返回data页面只用list.value await getFeeList()错误时由拦截器统一处理。不要在页面里再判断res.code否则每个方法都会重复。6.3 Long 类型主键转成 JSON 后精度丢失MySQL 的BIGINT对应 Java 的Long。当主键超过 JavaScript 安全整数范围时前端拿到的 ID 最后几位会变成 0后续用这个 ID 提交更新时就会更新到错误记录。低风险做法是提醒自己演示数据量小ID 不超范围真实项目则应该将 Long 序列化成字符串。Spring Boot 里可以配置 Jackson 将 Long 类型统一序列化为字符串或者在 DTO 中把 ID 声明为String。更常见的方式是使用JsonFormat或全局ToStringSerializer具体实现与 Jackson 版本有关落地前要结合公司代码规范统一。6.4 MySQL 表字段与 SQL 别名不一致导致查询失败如果直接查询rs.getBigDecimal(paidAmount)但 SQL 没有给列起别名MySQL 返回的列名可能是paid_amountRowMapper 就会抛出异常或读到 null。检查方法看错误日志中是否出现Column paid_amount not found。查看 SQL 是否用了f.paid_amount AS paidAmount。RowMapper 中引用的别名要和 SQL 中保持一致。这个问题在表和实体字段越多时越容易出现。统一 SQL 别名规范是成本最低的规避方式。6.5 常见问题汇总问题现象常见原因检查方式解决建议后端启动报数据库连接失败用户名密码错误或数据库不存在查看application.yml、MySQL 服务状态先用命令行连库确认中文乱码数据库字符集不对或连接串缺少编码查询表字符集统一utf8mb4页面列表不刷新查询按钮没有真正调用接口打开 Network 面板查看绑定clickload修改 MySQL 数据后页面不变化前端写了本地 mock刷新页面对比数据删除 mock统一从接口读取7. 从练习到生产这套系统还缺什么7.1 权限和角色矩阵要先补齐当前示例没有登录也没有权限控制。真实社区物业管理系统至少需要三类用户角色典型权限系统管理员分配账号、查看全量数据、配置字典物业工作人员录入账单、处理报修、维护房屋档案业主查看自己房屋账单、提交报修如果引入 Spring Security 或 Sa-Token建议先把角色与接口权限矩阵画出来。不要等接口很多以后再加权限那样每个 Controller 都要大面积改造。7.2 生产部署方式不能照搬开发环境开发环境里前后端分开跑但生产环境不能直接开放 Vite Dev Server。常见做法有两种方式一后端作为静态资源服务把前端npm run build生成的dist复制到src/main/resources/static重新打包成单个 Jar。方式二前端dist交给 NginxNginx 将/api反向代理到后端服务。方式二更接近主流前后端分离架构也更容易做 HTTPS、负载均衡和灰度发布。但要注意打包后看到的接口地址不再是localhost:8080必须把数据库连接、Redis、文件存储路径等配置外置。7.3 发布前检查清单检查项完成标准数据库密码不要硬编码在代码仓库中使用环境变量或配置中心接口权限未登录不能访问业主和账单接口金额处理所有金额计算使用BigDecimal数据库使用DECIMAL分页真实列表接口必须分页避免一次性查询所有账单日志记录操作人、方法、参数、耗时不打印完整密码或手机号数据备份MySQL 定期备份报修和缴费记录不能丢异常处理防止未处理异常把完整堆栈返回给前端回滚方案前端和后端保留上个版本包启动失败可快速回滚7.4 扩展方向当前系统已经跑通了“数据库 - 后端接口 - Vue3 页面”的最小链路。下一步可以从两个方向继续深入。业务方向增加“缴费操作”时更新paid_amount并重新计算status这需要在事务中完成。建议用一条 SQL 的幂等写法避免并发重复扣费。比如更新时加上WHERE paid_amount ? amount而不是先查询再更新。工程方向尝试把 JdbcTemplate 替换成 MyBatis-Plus将单表 CRUD 交给框架处理再为缴费列表接入 Spring Data 的分页接口。前端可以加入 Vue Router 和 Pinia把登录状态和业主信息放入全局状态。一个全栈练习项目真正能提升能力的地方往往不在代码量多少而在于“一张表、一个接口、一个页面”之间所有状态是否一致。社区物业管理系统正好能逼你把房屋、账单、权限和联调问题从头到尾走一遍值得现在就用最小功能跑通它。
返回列表