简介基于SpringBootVue的小区物业管理系统是一份面向计算机专业学生与Java初学者的毕业设计完整项目适合毕业设计、课程设计或期末大作业场景。项目涵盖前端页面、后端服务与数据库设计代码注释清晰新手也能较快上手下载后进行简单配置即可运行。资源共454个文件压缩包约10.02MB其中包含130个Java后台源码、48个Vue前端组件、17个XML配置另有SQL数据库脚本、启动/运行/构建批处理脚本以及图片、字体、样式等界面辅助资源目录结构较为完整便于按模块查找。项目在IDEA环境下基于SpringBoot框架开发前端采用Vue数据库建议使用MySQL 5.7部署依赖Tomcat与Maven导出的SQL脚本可直接初始化小区物业相关数据表系统已通过严格调试确保可运行功能完善且具有较高的实际应用价值。除了源码和数据库脚本压缩包内还提供多个Vue页面组件如侧边栏、头部导航、更新密码等并附有一键安装、启动、构建脚本可帮助快速完成本地部署和二次开发目前已有1142人学习下载适合作为毕业设计或课程设计的参考与实战项目。1. 一个 SpringBootVue 的小区物业管理系统毕业设计里最稳的落地形态每年毕业季都能看到同一个现象选题表翻来翻去最后有一半人落回“管理系统”这条路上。而小区物业管理系统在管理系统里又格外受欢迎——业务不算复杂几张表就能说清但功能面又足够宽业主、房产、缴费、报修、车位、公告每个模块都能做出可展示的页面和接口。用 SpringBootVue 做这套系统几乎是 Java 后端方向毕业设计里最常见的组合之一后端不用碰花哨的分布式中间件前端不用纠结性能优化重点全放在“能不能跑起来、逻辑是否闭环、答辩能不能讲明白”这三件事上。这篇笔记的目的很直接把这份含源码和教程的小区物业管理系统从数据库设计拆到前端页面打通再落到本地运行和排错。新手按步骤能复现整套环境熟手可以跳过铺垫直接看第 6 章的踩坑清单。标题里的关键词——SpringBoot、Vue、数据库、Java 毕业设计——每一块都会在下面的章节里对应到具体文件和代码而不是停留在概念层。2. 技术选型与项目结构这套组合为什么是毕设体系的“标准答案”2.1 SpringBoot 做后端为什么不用 SSM 或更重的框架小区物业管理系统最核心的诉求是“快速开发 逻辑清晰”。SpringBoot 的价值在毕业设计这个场景下体现得非常直白内置 Tomcat打 jar 包就能跑不用单独配服务器自动装配把 Spring 和 MyBatis 的整合成本压到只剩几行配置项目结构天然分层Controller、Service、Mapper 各管一段答辩时按这个分层讲逻辑比 SSM 手写 XML 配置要清楚得多。如果选传统 SSMSpring 配置、MyBatis 配置、事务配置各占一段 XML运行环境稍微有出入就会报各种奇怪的 Context 初始化错误。SpringBoot 把这些问题留给 starter 自动处理本地开发省下的时间足够把报修流程和缴费逻辑多做两轮测试。当然SpringBoot 也不是没有代价——版本升级后配置项变动较大比如拦截器注册方式、Redis 连接配置这类细节在不同版本间写法不同。所以做毕设时选一个自己熟悉的稳定版本就好不必追新。2.2 Vue 做前端页面组织方式和后端接口对应关系Vue 在这套系统里负责的层是“管理后台 用户端页面”。管理后台管业主信息、房屋信息、缴费记录、报修工单用户端负责缴费、报修提交、公告浏览。两个端共用同一套后端接口只是菜单和权限不同。前端的技术选型上毕业设计通常用 Vue 2 Element UI 的组合生态最稳组件丰富文档对新手友好。Vue 3 Element Plus 也可以但组件库的某些用法有差异如果不是很熟不建议在毕设阶段冒险换新。Axios 做接口请求是标配路由用 Vue Router状态管理看情况——项目不大时用 localStorage 存登录态就够了不用强行引入 Vuex/Pinia。2.3 项目目录结构与前后端分离模式拿到项目源码后第一件事应该是确认前后端是否分离。常见的做法是后端一个目录、前端一个目录偶尔也有把打包后的前端静态资源放到后端 resources 目录下的单体部署方式。我习惯把后端按这种结构组织backend/ # SpringBoot 后端根目录 ├── src/main/java/com/xxx/property/ │ ├── controller/ # 接口层接收请求、返回结果 │ │ ├── AdminController.java # 管理员登录、信息管理 │ │ ├── OwnerController.java # 业主信息管理 │ │ ├── PaymentController.java # 缴费记录管理 │ │ └── RepairController.java # 报修工单管理 │ ├── service/ # 业务逻辑层处理规则 │ ├── mapper/ # 数据访问层操作数据库 │ ├── entity/ # 实体类对应数据库表结构 │ ├── config/ # 配置类拦截器、跨域等 │ └── util/ # 工具类JWT、密码加密等 ├── src/main/resources/ │ ├── application.yml # 数据源、端口、MyBatis配置 │ └── mapper/ # MyBatis XML 文件 └── pom.xmlfrontend/ # Vue 前端根目录 ├── src/ │ ├── api/ # 接口封装每个模块一个 js 文件 │ ├── router/ # 路由配置 │ ├── views/ # 页面组件 │ │ ├── Login.vue # 登录页 │ │ ├── Dashboard.vue # 首页数据看板 │ │ ├── OwnerList.vue # 业主管理页 │ │ ├── PaymentList.vue # 缴费管理页 │ │ └── RepairList.vue # 报修管理页 │ ├── App.vue │ └── main.js └── package.json前后端分离模式下后端跑 8080 端口前端开发服务器跑 8081 端口Vue 的 vite.config.js 或 vue.config.js 里配置代理转发把 /api 开头的请求转到后端。这样本地开发时前端页面能直接调通接口不用关心跨域问题——但要注意代理只在开发环境下生效打包后的静态文件部署时跨域问题就需要后端配合处理了这一点后面第 6 章会细说。3. 数据库设计八张核心表与物业业务的三层关系3.1 业主、房产、车位第一层基础数据物业管理系统的基础是“人和房”。业主表owner和房屋表house是一对多的关系一个业主可以名下有多套房屋。车位表parking可以挂在业主名下也可以独立管理按月份自动算出应缴费用。建表时要注意两个点。第一房屋表建议加上楼栋和单元字段后续按楼栋筛选或者统计时有独立字段比在编号里截取字符串要方便得多。第二业主表和用户表如果要分开就一定要留好关联字段有些毕设偷懒把登录账号密码直接放进业主表答辩时很容易被追问“如果业主同时也是物业工作人员怎么办”。CREATE TABLE owner ( id int NOT NULL AUTO_INCREMENT, username varchar(50) NOT NULL COMMENT 登录账号, password varchar(100) NOT NULL COMMENT 加密后的密码, real_name varchar(50) DEFAULT NULL COMMENT 真实姓名, phone varchar(20) DEFAULT NULL COMMENT 联系电话, id_card varchar(18) DEFAULT NULL COMMENT 身份证号, create_time datetime DEFAULT CURRENT_TIMESTAMP, PRIMARY KEY (id), UNIQUE KEY uk_username (username) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4;CREATE TABLE house ( id int NOT NULL AUTO_INCREMENT, owner_id int DEFAULT NULL COMMENT 业主ID关联 owner 表, building varchar(20) DEFAULT NULL COMMENT 楼栋号, unit varchar(20) DEFAULT NULL COMMENT 单元号, room_no varchar(20) DEFAULT NULL COMMENT 房号, area decimal(10,2) DEFAULT NULL COMMENT 建筑面积物业费计算用, status tinyint DEFAULT 1 COMMENT 1-已售 2-空置, PRIMARY KEY (id) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4;两个表建好后物业费的计算规则就有着落了用house.area乘以单价就是每月的物业费。这个逻辑在缴费模块会反复用到。3.2 缴费与报修第二层业务流转缴费记录表payment是这套系统里最核心的表之一。以房屋为主键每月生成一条记录字段里带上费用类型、金额、状态。报修表repair则记录业主提交的报修单从提交、分配到完成状态流转要靠status字段维护不能只保存“未处理/已处理”两种还要有“处理中”的中间态。CREATE TABLE payment ( id int NOT NULL AUTO_INCREMENT, house_id int NOT NULL COMMENT 房屋ID, fee_type varchar(20) DEFAULT 物业费 COMMENT 费用类型物业费/水费/停车费, amount decimal(10,2) NOT NULL COMMENT 应缴金额, status tinyint DEFAULT 0 COMMENT 0-未缴 1-已缴, pay_time datetime DEFAULT NULL COMMENT 缴费时间, due_date varchar(20) DEFAULT NULL COMMENT 费用所属月份如 2025-06, PRIMARY KEY (id), KEY idx_house (house_id) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4;CREATE TABLE repair ( id int NOT NULL AUTO_INCREMENT, owner_id int NOT NULL COMMENT 报修业主ID, house_id int DEFAULT NULL COMMENT 房屋ID, content varchar(500) DEFAULT NULL COMMENT 报修内容, status tinyint DEFAULT 0 COMMENT 0-待分配 1-处理中 2-已完成 3-已取消, handler varchar(50) DEFAULT NULL COMMENT 处理人, create_time datetime DEFAULT CURRENT_TIMESTAMP, handle_time datetime DEFAULT NULL COMMENT 完成时间, PRIMARY KEY (id) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4;缴费表按月份生成记录这是一个容易翻车的点有些同学只在业主缴费时插入一条记录导致月底统计缴了多少、欠了多少时无据可查。建议是每个月月初由定时任务或管理员一键生成本月所有房屋的应缴记录状态默认未缴业主在列表里看到待缴金额后在线支付或线下缴费后点击确认。这样统计报表只需要一个GROUP BY status就能出来答辩演示时数据看得清楚。3.3 公告、访客与投诉第三层扩展功能这三张表属于“让系统显得完整”的部分。公告表notice字段简单标题、内容、发布时间面向所有业主可见。访客登记表visitor记录来访人员和到访房屋是安全模块的加分项。投诉建议表complaint和报修表结构类似但业务状态不同要单独维护。这些表不是必须一口气全部做完。如果时间紧张优先保证业主、房屋、缴费、报修四张表闭环公告表可以只保留最基本的字段。访客和投诉建议如果做了但要保证有对应页面。我见过不少项目表建了一堆页面只有两三个答辩时老师逐个点菜单点一个 404 一次观感很差。表与表之间的关系最终要落到页面上的联动逻辑业主列表点“查看房产”能跳到该业主名下房屋缴费列表点“缴费”能看到该房屋的建筑面积和费用明细报修列表点“处理”能更新状态并记录处理人。这些联动依赖的是外键字段设计得是否完整而不是 SQL 里的关联查询是否花哨。4. 后端实现从登录鉴权到缴费统计的完整调用链4.1 登录接口与密码加密先解决“谁在用系统”的问题系统里的用户角色至少有管理员和业主两种。管理员维护所有数据业主只能看自己的信息和缴费记录。后端接口的第一步是登录然后是权限校验。密码加密是最基本的安全底线用 MD5 加盐或 BCrypt 都行。毕业设计里用 MD5 加固定的盐也能应付但用 BCrypt 省事且更规范——Spring Security 的BCryptPasswordEncoder直接引入就能用做了加盐处理相同密码每次加密结果不同答辩时这个点可以主动提。// PasswordUtil.java import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder; public class PasswordUtil { private static final BCryptPasswordEncoder ENCODER new BCryptPasswordEncoder(); // 加密注册时调用 public static String encode(String rawPassword) { return ENCODER.encode(rawPassword); } // 校验登录时调用 public static boolean matches(String rawPassword, String encodedPassword) { return ENCODER.matches(rawPassword, encodedPassword); } }登录接口的逻辑是接收用户名和密码按用户名查库取到加密后的密码和当前用户角色校验通过后返回 token。token 可以用 JWT 生成也可以直接用 UUID 存到 Redis 或内存里。毕设阶段用 JWT 更通用前后端分离场景下不需要服务端存 session前端拿到 token 后放到请求头里后端拦截器统一解析。// LoginController.java RestController RequestMapping(/api/auth) public class LoginController { Autowired private OwnerService ownerService; PostMapping(/login) public Result login(RequestBody LoginDTO loginDTO) { // 1. 查询用户是否存在 Owner owner ownerService.findByUsername(loginDTO.getUsername()); if (owner null) { return Result.error(用户不存在); } // 2. 校验密码 if (!PasswordUtil.matches(loginDTO.getPassword(), owner.getPassword())) { return Result.error(密码错误); } // 3. 生成 token注意这里一定要把角色放进去 String token JwtUtil.generateToken(owner.getId(), owner.getRole()); return Result.success(token); } }这段代码里最容易忽略的是第 3 步的注释token 里只存用户 ID 不够必须带上角色信息因为管理员和业主看到的是完全不同的菜单和接口。拦截器校验 token 时把角色解析出来比对当前请求的路径是否有访问权限这样区分角色的逻辑就统一了。4.2 拦截器做请求校验哪些路径要放行哪些必须带 token登录接口本身必须放行否则连登录都进不去静态资源一般在后端不提供前端单独跑所以也无需考虑其余接口全部拦截。// AuthInterceptor.java Component public class AuthInterceptor implements HandlerInterceptor { Override public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception { // 放行登录接口 String uri request.getRequestURI(); if (uri.contains(/auth/login)) { return true; } // 检查 token String token request.getHeader(Authorization); if (token null || token.isEmpty()) { response.setStatus(401); return false; } try { // 解析 token解析失败说明 token 无效或过期 Claims claims JwtUtil.parseToken(token.replace(Bearer , )); request.setAttribute(userId, claims.get(userId)); request.setAttribute(role, claims.get(role)); return true; } catch (Exception e) { response.setStatus(401); return false; } } }拦截器注册到 WebMvc 配置里后别忘了同时配置跨域规则。前端开发服务器是 8081后端是 8080如果不配置跨域页面上所有请求都会被浏览器拦下来。最简单的方式是允许所有来源毕设场景没有安全问题答辩时老师问起来能解释清楚就行。// WebMvcConfig.java Configuration public class WebMvcConfig implements WebMvcConfigurer { Autowired private AuthInterceptor authInterceptor; Override public void addInterceptors(InterceptorRegistry registry) { registry.addInterceptor(authInterceptor) .addPathPatterns(/api/**) .excludePathPatterns(/api/auth/**); } Override public void addCorsMappings(CorsRegistry registry) { registry.addMapping(/api/**) .allowedOriginPatterns(*) .allowedMethods(GET, POST, PUT, DELETE) .allowCredentials(true) .maxAge(3600); } }这里有个坑容易踩跨域配置里allowCredentials(true)时allowedOrigins不能写*必须用allowedOriginPatterns(*)否则启动后会报异常。这个异常不是启动时立刻报的而是第一次发跨域请求时才触发排查起来很隐蔽。4.3 缴费模块一条记录从生成到核销的接口设计缴费模块最核心的两个接口是生成月度账单和确认缴费。管理员登录后点“生成账单”后端按当月所有房产生成未缴记录业主在前端看到自己的账单点“缴费”后账单状态改为已缴。// PaymentService.java Service public class PaymentService { Autowired private PaymentMapper paymentMapper; Autowired private HouseMapper houseMapper; // 生成月度账单 Transactional public void generateMonthlyBills(String month) { // 1. 查询所有已售房屋 ListHouse houses houseMapper.findByStatus(1); // 2. 按月生成账单 for (House house : houses) { Payment payment new Payment(); payment.setHouseId(house.getId()); payment.setFeeType(物业费); payment.setAmount(house.getArea() * 2.5); // 单价 2.5 元/平米 payment.setStatus(0); payment.setDueDate(month); paymentMapper.insert(payment); } } // 业主确认缴费 Transactional public void confirmPayment(Integer paymentId) { Payment payment paymentMapper.findById(paymentId); if (payment null) { throw new BusinessException(账单不存在); } if (payment.getStatus() 1) { throw new BusinessException(该账单已缴费请勿重复操作); } payment.setStatus(1); payment.setPayTime(new Date()); paymentMapper.update(payment); } }这段逻辑里有几个细节值得注意。生成账单前可以先按“月份 房屋 ID”查重防止管理员手滑点了两次生成产生重复账单确认缴费时要先查状态已缴的不能再缴否则前端没拦截住的话就会出现重复扣费的问题。Transactional两个方法都加了因为涉及先查后写不能让中间步骤失败时留下脏数据。缴费统计接口也是一样直接SELECT SUM(amount) FROM payment WHERE status 1 GROUP BY due_date就能按月折线图展示物业费收缴率。前端首页看板的数据来源就是这几个聚合查询。5. 前端 Vue 页面与接口联调从页面骨架到数据打通5.1 登录页与路由拦截前端如何配合后端 token 做访问控制前端登录页的逻辑是提交表单到/api/auth/login拿到 token 后存到 localStorage然后跳转到首页。路由拦截器在每次跳转前检查 localStorage 里有没有 token没有就踢回登录页。// src/router/index.js import Vue from vue import Router from vue-router import Login from /views/Login.vue import Dashboard from /views/Dashboard.vue Vue.use(Router) const router new Router({ routes: [ { path: /, redirect: /dashboard }, { path: /login, component: Login }, { path: /dashboard, component: Dashboard }, // 其他页面路由 ] }) // 全局前置守卫 router.beforeEach((to, from, next) { const token localStorage.getItem(token) if (to.path ! /login !token) { next(/login) } else { next() } })axios 请求也要配合 token。统一在请求拦截器里加上 Authorization 头响应拦截器里处理 401token 过期或未登录时跳回登录页。这一步不做的话会出现一种尴尬情况后端返回 401页面却停在原地没有任何反应用户以为系统卡死了。// src/api/request.js import axios from axios import router from /router const request axios.create({ baseURL: /api, timeout: 10000 }) // 请求拦截器自动携带 token request.interceptors.request.use(config { const token localStorage.getItem(token) if (token) { config.headers.Authorization Bearer token } return config }) // 响应拦截器统一处理错误 request.interceptors.response.use( response response.data, error { if (error.response error.response.status 401) { localStorage.removeItem(token) router.push(/login) } return Promise.reject(error) } ) export default request前端路由守卫和后端拦截器组合起来构成了这套系统的双保险。前端拦截保证页面上进不去后端拦截保证即使有人绕过前端直接调接口也拿不到数据。答辩时如果被问到“怎么防止未登录用户访问”把这两层说清楚就足够了。5.2 缴费管理页列表渲染、状态切换与接口联动以缴费管理页为例完整走一遍从接口封装到页面渲染的流程。先封装接口// src/api/payment.js import request from ./request // 分页查询账单 export function getPaymentList(params) { return request.get(/payment/list, { params }) } // 确认缴费 export function confirmPayment(id) { return request.put(/payment/confirm/${id}) }页面里先调用getPaymentList拿到账单列表渲染成表格每行末尾放一个“确认缴费”按钮点击后调用confirmPayment(id)成功后再重新拉一次列表。!-- src/views/PaymentList.vue -- template div classpayment-list el-table :datapayments border stripe el-table-column prophouseId label房屋ID width80/el-table-column el-table-column propdueDate label月份 width100/el-table-column el-table-column propamount label金额 width100 template slot-scopescope ¥{{ scope.row.amount.toFixed(2) }} /template /el-table-column el-table-column propstatus label状态 width80 template slot-scopescope el-tag :typescope.row.status 1 ? success : warning {{ scope.row.status 1 ? 已缴 : 未缴 }} /el-tag /template /el-table-column el-table-column label操作 template slot-scopescope el-button v-ifscope.row.status 0 typeprimary sizesmall clickhandleConfirm(scope.row) 确认缴费/el-button /template /el-table-column /el-table /div /template script import { getPaymentList, confirmPayment } from /api/payment export default { data() { return { payments: [] } }, mounted() { this.loadList() }, methods: { async loadList() { const res await getPaymentList({ page: 1, size: 20 }) this.payments res.data }, async handleConfirm(row) { await confirmPayment(row.id) this.$message.success(缴费成功) this.loadList() } } } /script这个页面的价值在于完整呈现了“接口封装 → 页面渲染 → 操作联动 → 数据刷新”的闭环。字段渲染时的格式处理金额toFixed(2)、状态用el-tag区分颜色以及操作按钮按状态条件控制已缴的记录不显示缴费按钮都是前端里容易漏掉的细节。漏掉这些不会报错但每次操作完数据对不上、或者已缴费还能再点体验就很差。5.3 首页数据看板用聚合查询撑起一张图表看板页面是答辩时的加分项。常见做法是放几张统计卡片和一张折线图总业主数、总房屋数、本月应收物业费、本月实收物业费、近六个月的收缴趋势。后端只需要一个聚合接口// DashboardController.java RestController RequestMapping(/api/dashboard) public class DashboardController { Autowired private OwnerMapper ownerMapper; Autowired private HouseMapper houseMapper; Autowired private PaymentMapper paymentMapper; GetMapping(/summary) public Result summary() { MapString, Object data new HashMap(); data.put(ownerCount, ownerMapper.countAll()); data.put(houseCount, houseMapper.countAll()); data.put(monthlyIncome, paymentMapper.sumByMonth(null)); // 本月应收 data.put(monthlyPaid, paymentMapper.sumByStatus(1)); // 本月实收 return Result.success(data); } }前端用 Element UI 的卡片组件展示数字用 ECharts 画折线图。ECharts 的数据源就是后端按月返回的收缴数据和应收数据两个数组分别映射到折线图的两个 series 就行。注意不要让前端做前端渲染再传原始数据而是后端尽量把要展示的格式算好前端只负责画图这样页面代码量小答辨时也能一笔带过。6. 本地运行与避坑五步跑通项目和 5 个高频报错排查6.1 运行环境的准备顺序拿到项目后环境准备按这个顺序走能避免大多数根目录问题先装 JDK 和 Maven再装 MySQL导入 SQL 文件最后启动后端、启动前端。顺序不建议乱因为后端启动时如果连不上数据库报错信息会干扰你定位问题——你以为是代码问题查了半天发现是 MySQL 服务没起来。数据库导入时注意默认字符集。SQL 文件里建表语句写了DEFAULT CHARSETutf8mb4就用它不要手动改成 utf8否则中文容易乱码。导入后再看一眼表数据确认至少有能登录的账号。很多项目初始化数据里带 admin 账号如果你导入后发现登录不了去数据库里看owner表的密码字段是否加密过以及和代码里注册时用的加密方式是否一致——这是登录失败最常见的根源之一。后端启动前改配置文件里三样东西数据库地址、账号、密码。application.yml 里把这几个值确认好比启动后看一串 Connection refused 再回头改要省时间。6.2 5 个高频报错排查现象、原因、解决数据库连接失败。现象是后端启动时控制台报Cannot create PoolableConnectionFactory或者Communications link failure。原因大多数是 MySQL 没启动、端口不是 3306、密码不对。解决步骤先确认 MySQL 服务在跑再确认账号密码能手动连上最后确认配置文件里地址是否带?useSSLfalseserverTimezoneAsia/Shanghai缺了时区配置在 MySQL 8 下会直接报错。前端接口请求 404。现象是页面能打开一旦调用接口就报 404但后端接口在浏览器直接访问 URL 是好的。原因是前端代理没配置或配置路径不对。检查 vue.config.js 里 devServer.proxy 是否把/api转发到了http://localhost:8080以及后端的 context-path 是否额外加了一层前缀。端口被占用。现象是启动报Port 8080 was already in use。解决方式改端口或者杀进程。改端口时要注意前端代理里的 target 地址也要跟着改很多人改了后端端口忘了改前端代理接口又连不上了。跨域请求被拦截。现象是浏览器控制台报CORS policy后端日志里能看到请求进来但没返回。原因很可能是后端跨域配置写法错误——用了allowedOrigins(*)同时又开了allowCredentials(true)。按第 4 章的allowedOriginPatterns写法就能解决。token 失效跳转循环。现象是每次刷新页面都跳回登录页登录后不久又弹回。原因是前端响应拦截器对 401 的全局处理里没有区分业务场景登录接口本身返回 401 时也触发了跳转。解决方式是在响应拦截器里判断当前请求 URL 是否登录接口登录接口失败时不跳转只提示错误。6.3 确认系统真正跑通的验收路径运行环境全部就绪后建议按这个顺序走一遍验收流程确认系统不是“能启动页面”而是“业务链路完整”。第一步用初始化账号登录分别验证管理员和业主两个角色能看到不同菜单。第二步管理员进入缴费管理点生成账单确认所有房屋生成了对应月份的未缴记录。第三步切到业主账号看到自己的账单并完成缴费再切回管理员账号确认状态变为已缴。第四步提交一条报修走完分配、处理、完成的流转。第五步在首页看板上检查统计数据有没有同步变化。这条链路走通核心功能就闭环了。验收时有一个经验值得记住不要每测一个功能就截一张图而是把完整流程录屏从登录开始到最后统计数据变化一气呵成。答辩演示时这种连续演示远比单张图片有说服力也更容易掩盖紧张时操作卡顿的问题。部署到学校服务器或写进论文附录时后端打成 jar 包用java -jar运行前端npm run build后把 dist 目录交给后端静态资源托管或者扔到 Nginx 里。如果选择静态托管方式后端要加一个 Static 资源映射配置否则前端页面打开是空白的白屏。这个白屏问题每年都有人遇到——前端打包后路由用的是 history 模式刷新子页面时服务器找不到对应路径需要后端把所有非 API 请求都转发到 index.html或者前端路由改 hash 模式。我一般会直接让前端路由用 hash 模式省掉这个配置也不影响演示效果。这套系统做完之后回头看它的价值不在于技术有多高深而在于它把管理的核心链路完整打通了从人和房的档案管理到按月账单的自动生成再到缴费和报修的闭环流转。真正的难点不在某个接口怎么写而在每个模块之间接缝处的细节——权限区分、状态流转、异常提示、数据联动。把这些处理好系统自然就站得住。希望这篇笔记能帮你少走几趟弯路把时间花在真正值得打磨的业务逻辑和演示效果上。本文还有配套的精品资源点击获取