ARTICLE DETAIL

资讯详情

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

SpringBoot+Vue小区管理系统开发实战:从架构到部署全流程解析

SpringBoot+Vue小区管理系统开发实战:从架构到部署全流程解析 1. 项目全貌与技术选型思路1.1 为什么是SpringBootVue这套组合做小区管理系统听起来只是个“增删改查”但真上手之后会发现业主、房产、缴费、报修、车位、访客、公告这些模块纠缠在一起数据关系比想象中复杂。市面上现成的物业软件要么收费贵、要么流程死板自己从零搭一套反而更可控。这个项目用SpringBoot做后端、Vue做前端本质上就是把最经典、资料最多的技术栈拼在一起踩坑的时候能搜到答案扩展的时候也不缺轮子。先说后端选SpringBoot的理由。SpringBoot不是新东西但它把Spring生态里繁琐的配置全收敛了一个SpringBootApplication注解加上内嵌Tomcat打个jar包就能跑。对于小区管理系统这种业务密集型项目稳定压倒一切SpringBoot的生态足够成熟事务管理、定时任务、安全框架都有现成方案。项目里如果还要接短信通知、对接门禁设备、生成缴费账单PDFSpringBoot的starter机制能省掉一半工作量。再说前端选Vue。小区管理系统的使用人群很杂物业前台、保安、业主、管理员都有操作界面必须够直观。Vue的双向绑定让表单交互写起来很顺手组件化拆分之后一个“缴费管理”页面就是一个独立组件改样式、调逻辑都不会牵连其他页面。配合Element UI这类组件库表格、弹窗、表单校验半天就能搭出能看的界面。Vue在国内社区活跃度高不管是招人还是找现成代码片段都比其他框架容易。这个组合真正的好处是前后端彻底分离。后端只输出JSON接口前端只负责渲染页面两边各干各的。小区管理系统往往要部署在物业的普通PC上前端打包成静态文件丢给Nginx后端打成jar包用java -jar启动一台2核4G的旧机器就能扛住一个中型小区的访问量。而且后续要加微信小程序端后端接口可以原封不动复用Vue那套逻辑也能通过uni-app移植过去。1.2 数据持久层选MyBatis而不是JPA的考量持久层框架我见过不少人纠结Spring Data JPA上手快、代码少但小区管理系统这种项目SQL往往比较复杂——比如按楼栋、单元、楼层多条件筛选业主统计一个季度各楼栋的收费率JPA写起来要么拼接 Specification要么就得写原生SQL。而MyBatis的核心优势就是把SQL控制权完全交给你XML文件里写什么sql就执行什么调优起来心里有底。MyBatis另一个实用点是动态SQL。复杂查询条件组合用if标签判断参数是否为null物业端“业主列表”要支持按姓名、手机号、楼栋号、入住状态任意组合筛选这个需求在MyBatis里几乎不用动Java代码全部在XML里用where和if解决。JPA的Specification虽然也能做但语法绕团队成员不熟的话维护成本高。还有分页。MyBatis配合PageHelper插件一行代码搞定分页不用写LIMIT ? OFFSET ?的封装。项目里所有列表页——业主列表、缴费记录、报修工单——都是分页查询PageHelper能自动拦截SQL生成count查询和limit拼接这块省了不少事。当然用PageHelper也要注意一个坑后面我会讲。MySQL作为存储层没有争议。小区管理系统的数据量撑死几十万条MySQL完全够用而且InnoDB引擎支持事务缴费扣款、报修状态变更这种操作必须保证原子性。存储引擎选InnoDB基本是标配主要是支持行级锁和外键约束数据一致性有保证。1.3 系统整体架构与项目结构规划项目采用经典前后端分离结构后端按Controller-Service-Mapper三层划分前端按页面路由划分组件。后端目录结构大致如下community-system/ ├── src/main/java/com/community/ │ ├── controller/ # 接口层只做参数接收和结果返回 │ ├── service/ # 业务逻辑层事务默认加在这里 │ │ └── impl/ │ ├── mapper/ # MyBatis接口对应XML文件 │ ├── entity/ # 数据库实体类 │ ├── dto/ # 接口出入参对象 │ ├── config/ # 配置类拦截器、跨域、文件上传 │ ├── common/ # 统一返回结果、异常处理、工具类 │ └── CommunityApplication.java └── src/main/resources/ ├── mapper/ # MyBatis XML文件所在目录 └── application.yml前端用Vue CLI创建路由用vue-router状态管理看项目复杂度决定是否引入Vuex。如果只是展示列表和表单不跨组件共享太多数据直接用ref和provide/inject就够没必要为了用Vuex而用Vuex。前端目录community-web/ ├── src/ │ ├── router/ # 路由配置包含动态路由注册逻辑 │ ├── views/ # 页面组件 │ ├── components/ # 公共组件表格、弹窗、上传 │ ├── api/ # 接口请求封装 │ ├── utils/ # axios实例、token处理 │ └── main.js数据库设计是整个系统的地基先把核心表梳理清楚再动手写代码会少走很多弯路。核心表包括业主表owner、房产表house、业主房产关联表、缴费记录表payment、报修工单表repair、车位表parking、访客登记表visitor、公告表notice、用户表user。后面单独用一节讲表结构设计。2. 核心模块设计与数据库建模2.1 业主与房产关系的数据处理小区系统最基础的关系是“人”和“房”。一个业主可以有多套房产一套房产也可能登记多位共同所有人这是典型的多对多关系。所以不能简单地在业主表里加一个“房产ID”字段而是要拆出一张关联表。设计思路是这样的house表存房产信息字段包括house_id、building_no楼栋号、unit_no单元号、floor_no楼层、room_no房号、area面积、house_type户型、status空置/已入住/装修中owner表存业主信息owner_id、name、id_card身份证号、phone、is_main是否为主业主等owner_house关联表存id、owner_id、house_id、bind_time。为什么要单独拆关联表因为后续缴费、报修都是针对“某套房”发起的如果业主信息直接在关联表里冗余一份改手机号时要同步多处容易出错。而查询的时候只要JOIN一下关联表就能拿到某套房的所有业主或者某个业主的所有房产MySQL走索引之后性能完全不是问题。这里有个细节is_main字段用来标识主业主。物业沟通、缴费通知默认发给主业主其他共同所有人只保留知情权。业务规则是主业主可以修改房产的入住状态非主业主只能查看。这个逻辑放在Service层判断避免把业务规则散落在Controller里。房产状态维护也很关键。新小区交付时所有房子是“空置”状态业主收房后变“已入住”装修期间可以单独设“装修中”状态。这些状态值我建议用枚举类维护数据库中存字符串比如OCCUPIED、EMPTYJava代码里用枚举对应避免魔法值散落各处。MyBatis处理枚举有两种方式默认是存枚举的name()但更推荐用TypeHandler把枚举转成指定的字符串或数字存储。后面在MyBatis配置那节我会详细说怎么自定义TypeHandler。2.2 缴费模块的状态设计与账单生成逻辑缴费是小区系统最核心的模块之一物业靠它收钱账目不能乱。缴费记录表设计如下payment ├── payment_id bigint 主键 ├── house_id bigint 房产ID ├── payment_type varchar 费用类型物业费/停车费/水费/维修费 ├── amount decimal 金额 ├── status varchar 状态UNPAID/PAID/OVERDUE/REFUNDED ├── period_start date 费用周期开始 ├── period_end date 费用周期结束 ├── created_time datetime 生成时间 ├── paid_time datetime 支付时间 └── order_no varchar 订单号对接支付用缴费状态用状态机管理会清晰很多账单生成时是UNPAID业主支付成功变成PAID超过截止日期未支付自动变OVERDUE支付后由于特殊原因退款则变成REFUNDED。状态流转一定要在Service层封装方法不要允许任意地方直接改状态——否则业务逻辑一多改状态的地方不统一迟早出bug。物业费账单的生成逻辑一般有两种方式手动生成和定时自动生成。手动生成是老式做法月底管理员逐一操作自动生成则用SpringBoot的Scheduled定时任务每月1号凌晨自动扫描所有已入住的房产生成本月账单。自动生成要考虑两个问题一是避免重复生成判断条件用“当前月份是否已存在该房产的物业费账单”二是费用计算规则常见的是“每平米单价 × 房产面积 × 月份数”单价可以在房产所属的小区配置表里维护。订单号我习惯用时间戳加随机数生成格式类似20250101120000123456保证唯一即可。如果后续要对接微信支付、支付宝支付回调里也是拿这个订单号去更新缴费状态所以唯一性很重要。2.3 报修工单与访客车位的业务闭环报修流程是业主和物业互动最频繁的场景业务链路比较长业主提交报修单描述问题、上传图片→ 物业派单给维修工 → 维修工接单、上门处理 → 业主确认完成 → 评价。整个链路拆成两张表更合适repair表存工单本身工单号、业主ID、房屋ID、问题描述、图片URL、状态repair_record表存状态流转记录谁在什么时间把状态改成了什么。为什么要存状态流转记录因为物业和业主经常扯皮——“我没收到报修通知”“我早就修好了”。有了操作日志一查时间线就能定责。这个思路适用于所有有状态流转的模块不只是报修。访客管理比较简单但有个点容易漏访客和车辆是联动的。开车进小区要登记车牌号所以visitor表里可以加一个plate_no字段登记时把车牌信息一起存下来。这样访客出场时门卫直接按车牌查有没有登记记录不用重新录入。车位分固定车位和临停车位固定车位绑定业主按月收停车费临停车位按小时计费进出场时记录时间戳出场时自动计算费用。公告模块比较简单一个notice表搞定——标题、内容、发布人、发布时间、置顶标记。前端首页只显示未读和置顶公告已读逻辑可以用一张notice_read_record关联表记录用户ID和公告ID避免每次加载都拉全量数据。2.4 权限模型设计三种角色两套校验小区系统的用户角色就三类超级管理员物业公司总部、物业人员项目经理、前台、维修工、业主。业主只能看自己的房产、缴费记录能提交报修不能看别人的信息物业人员可以管理自己管辖范围的业主和工单超级管理员拥有全部权限。后端用SpringBoot拦截器做接口权限校验。登录成功后签发一个JWT令牌前端每次请求在Authorization请求头带上这个令牌后端拦截器解析令牌获取用户ID和角色然后判断Controller方法上标注的角色权限注解是否满足。这种做法比定义一大堆URL白名单灵活新加接口时只要在方法上加RequireRole(ADMIN)之类的注解就行。前端同样要做一层路由权限控制。因为不同角色登录后看到的菜单不一样业主端没有“缴费管理”这个菜单物业管理端没有“报修提交”这个入口。实现方式是动态路由登录成功后根据用户角色从后端拉取菜单数据用router.addRoute()动态注册路由而不是一次性把全部路由写死在代码里。这样既保证菜单干净也避免前端暴露不需要的页面地址。这里要注意前端权限控制只是优化体验真正安全防线在后端。前端隐藏了菜单不代表调用方不能直接请求后端接口。每个接口都必须在后端校验权限这是原则性问题。3. 实操过程与核心环节实现3.1 环境准备JDK、Maven、Node、MySQL动手写代码之前先把环境装好版本不对后面全是坑。我建议的版本组合是组件版本说明JDK1.8 或 11SpringBoot 2.x 用 JDK 8 完全没问题不用追求高版本Maven3.6项目构建和依赖管理Node14 或 16Vue CLI 4/5 在 Node 14/16 下最稳定MySQL5.7 或 8.05.7 稳定8.0 性能更好驱动和连接串有区别SpringBoot2.7.x2.7 是 2.x 的最后版本稳定资料最多Vue2.x 或 3.xVue2 ElementUI 最稳Vue3 ElementPlus 更有前瞻性MySQL 8.0 的驱动已经从com.mysql.jdbc.Driver改成了com.mysql.cj.jdbc.Driver连接串需要加serverTimezoneAsia/Shanghai否则会报时区错误。另外新版MySQL默认使用caching_sha2_password认证插件有些老驱动连接不上要么升级驱动到8.0版本要么在创建用户时指定mysql_native_password认证方式。之前有朋友一直报Public Key Retrieval is not allowed错误排查半天就是认证插件闹的加上allowPublicKeyRetrievaltrue参数就解决了。MySQL的安装建议直接下载官方安装包Windows下安装MySQL 5.7基本是下一步下一步安装完把bin目录加入系统环境变量。Linux服务器上部署可以用rpm安装包一条rpm -ivh mysql-community-server-xxx.rpm装完再systemctl start mysqld。安装完成后用mysql_secure_installation命令设置root密码和安全选项。3.2 后端项目搭建与核心配置用IDEA创建一个Maven项目或者直接访问start.spring.io生成SpringBoot基础工程。关键的pom依赖就这几个dependency groupIdorg.springframework.boot/groupId artifactIdspring-boot-starter-web/artifactId /dependency dependency groupIdorg.mybatis.spring.boot/groupId artifactIdmybatis-spring-boot-starter/artifactId version2.3.1/version /dependency dependency groupIdmysql/groupId artifactIdmysql-connector-java/artifactId version8.0.33/version /dependency dependency groupIdcom.github.pagehelper/groupId artifactIdpagehelper-spring-boot-starter/artifactId version1.4.7/version /dependency dependency groupIdio.jsonwebtoken/groupId artifactIdjjwt/artifactId version0.9.1/version /dependencyapplication.yml里最容易被忽略的是MyBatis配置mybatis: mapper-locations: classpath:mapper/*.xml type-aliases-package: com.community.entity configuration: map-underscore-to-camel-case: true log-impl: org.apache.ibatis.logging.stdout.StdOutImplmap-underscore-to-camel-case这一项非常重要。数据库字段是building_no这种下划线风格Java实体类是buildingNo这种驼峰风格开启自动映射后不用写一堆resultMap系统自动转换。很多新手数据库查询出来一堆字段为null八成就是忘开这个开关。log-impl配成StdOutImplSQL语句会打印在控制台开发阶段排查SQL问题非常好用。Debug阶段还能看到MyBatis往里传入的参数和查出来的行数基本就不用再单独装SQL日志插件了。生产环境记得把日志级别调到WARN别把SQL全打到日志文件里。SpringBoot版本选择上有个坑SpringBoot 3.x 把javax包全部升级成了jakarta很多老项目代码直接迁移会编译报错。如果只是做管理系统用2.7.x就够了完全没必要追新版本。如果一定要用SpringBoot 3.x所有javax.*的导入要全局替换成jakarta.*拦截器、Servlet相关的类也要相应调整。3.3 MyBatis核心配置与XML写法MyBatis的XML文件是SQL的核心战场。拿业主分页查询这个最常见功能举例Mapper接口方法ListOwnerVO selectOwnerPage(OwnerQueryDTO query);对应XMLselect idselectOwnerPage resultTypecom.community.entity.Owner SELECT o.owner_id, o.name, o.phone, h.building_no, h.unit_no, h.room_no FROM owner o LEFT JOIN owner_house oh ON o.owner_id oh.owner_id LEFT JOIN house h ON oh.house_id h.house_id where if testname ! null and name ! AND o.name LIKE CONCAT(%, #{name}, %) /if if testbuildingNo ! null and buildingNo ! AND h.building_no #{buildingNo} /if if teststatus ! null AND h.status #{status} /if /where ORDER BY h.building_no, h.unit_no, h.room_no /select这种写法叫动态SQLMyBatis最值得学的特性。难点有两个一个是where标签会自动补WHERE关键字并去掉第一个多余的AND所以每个条件前面都可以放心写AND另一个是参数绑定用#{}而不是${}前者是预编译占位符能防SQL注入后者是字符串拼接只适合传表名、排序字段这种不会由用户直接控制的值。再说TypeHandler。默认情况下MyBatis把Java枚举存到数据库时调用的是枚举的name()方法也就是存UNPAID、PAID这种字符串。如果数据库里想存数字0表示未缴1表示已缴那就要自定义TypeHandler。写一个类继承BaseTypeHandlerPaymentStatusEnum重写四个方法在setNonNullParameter里写ps.setInt(parameter.getCode())在getNullableResult里根据数字反查枚举。然后在XML字段映射的地方写typeHandlercom.community.handler.PaymentStatusHandler。这个操作不复杂但能让枚举和数据库解耦后面改枚举名字不影响历史数据。MyBatis缓存也是个常被问到的话题。一级缓存是SqlSession级别的同一个SqlSession执行两次完全相同的SQL第二次直接返回缓存结果不需要查数据库。二级缓存是namespace级别的默认不开启要配置cache/标签才能用。但管理系统这种实时性要求高的业务场景我不建议随便开二级缓存——数据更新后缓存刷新不及时业主明明缴费了页面还显示未缴投诉电话马上就来。缓存不灵活不如不开数据库查询性能由索引保证。3.4 前端项目搭建与路由权限实现创建Vue项目npm install -g vue/cli vue create community-web创建过程中选择Router特性其他看需求勾选不用的别乱装。安装Element UI或Element Plusnpm install element-ui --save # 如果你用的是Vue 3 npm install element-plus --saveaxios的封装要提前做好。所有接口需要通过axios发送建议在utils/request.js里统一创建axios实例配置baseURL和请求拦截器import axios from axios import { Message } from element-ui import router from /router const service axios.create({ baseURL: process.env.VUE_APP_BASE_URL, timeout: 10000 }) service.interceptors.request.use(config { const token localStorage.getItem(token) if (token) { config.headers[Authorization] Bearer ${token} } return config }) service.interceptors.response.use( response { const res response.data if (res.code ! 200) { Message.error(res.message || 请求失败) return Promise.reject(new Error(res.message)) } return res.data }, error { if (error.response error.response.status 401) { localStorage.removeItem(token) router.push(/login) Message.error(登录已过期请重新登录) } return Promise.reject(error) } ) export default service动态路由的实现一点也不神秘。路由不再一次性全部注册而是只注册基础路由登录页、首页框架菜单数据由后端根据角色返回。Vue Router 4Vue 3的写法是const modules import.meta.glob(/views/**/*.vue) function buildRoutes(menuList) { const routes [] menuList.forEach(item { const component modules[/src/views/${item.componentPath}.vue] routes.push({ path: item.path, name: item.name, component: component, meta: { title: item.title, icon: item.icon } }) }) return routes } // 登录后获取菜单 const menuList await getMenuByRole() const dynamicRoutes buildRoutes(menuList) dynamicRoutes.forEach(route router.addRoute(route))路由按需加载的好处一是首屏不用一次性加载所有页面组件二是菜单和数据权限完全由后端控制前端代码里不会出现“虽然看不到但源码里有链接”的尴尬。开发环境的跨域问题在vue.config.js里配置代理解决module.exports { devServer: { proxy: { /api: { target: http://localhost:8080, changeOrigin: true } } } }这样前端请求/api/xxx时开发服务器自动转发到http://localhost:8080。生产环境就不需要代理了Nginx反向代理配置一下就行。3.5 文件上传接入MinIO报修图片、业主头像、公告封面这些都是文件上传需求。文件存哪里不能直接存数据库也不能长期放本地磁盘重新部署就丢。本地磁盘另一个问题是集群部署时不共享前端传上去的文件后端另一台机器上读不到。解决方案是引入对象存储MinIO。MinIO和SpringBoot集成很干净就三步。第一步部署MinIO服务Docker一行命令docker run -p 9000:9000 -p 9001:9001 \ -e MINIO_ROOT_USERadmin \ -e MINIO_ROOT_PASSWORDadmin123456 \ -v /data/minio:/data \ minio/minio server /data --console-address :9001第二步引入Java SDK依赖dependency groupIdio.minio/groupId artifactIdminio/artifactId version8.5.7/version /dependency第三步在配置类里注册MinioClient的Bean封装上传下载方法。上传的核心代码大致是public String uploadFile(MultipartFile file) { String fileName UUID.randomUUID().toString() getSuffix(file.getOriginalFilename()); minioClient.putObject( PutObjectArgs.builder() .bucket(bucketName) .object(fileName) .stream(file.getInputStream(), file.getSize(), -1) .contentType(file.getContentType()) .build() ); return minioClient.getPresignedObjectUrl( GetPresignedObjectUrlArgs.builder() .method(Method.GET) .bucket(bucketName) .object(fileName) .build() ); }注意几个细节文件名必须用UUID重命名防止用户上传同名文件互相覆盖也避免中文文件名在URL传输时编码问题返回给前端的是预签名URL带时间戳有效期比公开读权限安全桶的访问权限设为私有上传用API下载用预签名URL这样即便URL泄露也有时间限制不会永久暴露。3.6 前后端联调与部署联调阶段最容易出的问题是接口字段对不上。后端返回houseId前端拿到的是house_id页面上怎么都显示不了数据。解决办法一是前后端约定统一的JSON返回格式比如{ code, message, data }二是用Swagger自动生成接口文档前端照着文档联调总不会错。SpringBoot加Swagger很简单加一个springfox-boot-starter依赖访问/swagger-ui/index.html就能看到所有接口的测试页面这个对于开发自测帮助非常大。接口返回统一如下public class ResultT { private Integer code; private String message; private T data; public static T ResultT success(T data) { ResultT result new Result(); result.code 200; result.message success; result.data data; return result; } public static T ResultT error(String message) { ResultT result new Result(); result.code 500; result.message message; return result; } }Controller里所有接口都返回Result前端axios响应拦截器统一判断code这样业务异常和系统异常的展示逻辑就只有一套不会出现有的接口弹Alert有的接口直接报错的情况。部署环节前端打包npm run build会生成dist目录里面是纯静态文件。后端打包mvn clean package -DskipTests生成target/community-system.jar。服务器上有Java环境和MySQL就行一个jar包一个dist文件夹就能跑起来。Nginx配置很简单server { listen 80; server_name your-domain.com; location / { root /var/www/community; index index.html; try_files $uri $uri/ /index.html; } location /api/ { proxy_pass http://localhost:8080/api/; proxy_set_header Host $host; proxy_set_header X-Real-IP $remote_addr; } }try_files $uri $uri/ /index.html;这行一定要写否则Vue路由在非根路径刷新会404。这个坑我在部署时踩过不加这行用户刷新/payment/list页面直接白屏Nginx返回404。4. 常见问题与排查技巧实录4.1 MySQL连接与驱动相关错误这类问题在项目初始化阶段最集中网上报错也是五花八门我整理了几个典型报错信息原因解决方案Public Key Retrieval is not allowedMySQL 8 使用caching_sha2_password认证连接串加allowPublicKeyRetrievaltrueThe server time zone value Öйú±ê׼ʱ¼ä is unrecognized时区配置问题连接串加serverTimezoneAsia/ShanghaiUnknown database community数据库没创建MySQL里执行CREATE DATABASE community DEFAULT CHARACTER SET utf8mb4;Connection refusedMySQL没启动或端口不对确认MySQL服务状态、端口号默认3306Access denied for user rootlocalhost密码错误或用户权限检查密码或执行ALTER USER rootlocalhost IDENTIFIED WITH mysql_native_password BY 新密码;MySQL 5.7和8.0的默认字符集有区别5.7默认是latin18.0默认是utf8mb4。建库时建议统一指定utf8mb4否则插入中文偶尔会出现乱码。utf8mb4比utf8多支持了emoji和一些生僻字兼容性更好以后接入小程序、微信昵称都不怕。4.2 MyBatis常见错误与分页坑最常见的MyBatis问题是“所有查询结果字段都是null”。原因基本就两个没开map-underscore-to-camel-case或XML里的resultType写错。前者改配置后者检查resultType对应的类路径是否正确。另一个顽固问题是PageHelper分页不生效。检查两点第一PageHelper.startPage()必须在查询方法执行前调用它只对紧接着的下一条查询生效第二如果Mapper查询后紧接着做了stream()或其他集合操作分页总数可能算错。PageHelper源码里通过拦截器去解析SQL只拦截查询方法如果用了自定义的嵌套查询count语句可能生成错误。还有个MyBatis升级带来的坑mybatis-spring-boot-starter新版本里configuration.map-underscore-to-camel-case默认是true但如果你用Configuration写配置类时先加载了SqlSessionFactory配置可能被覆盖。建议配置统一写在application.yml里别在Java里又写一遍。4.3 Vue项目和前端部署的坑前端跨域问题在开发环境和生产环境的表现完全不同。开发环境用proxy代理能解决生产环境必须在Nginx里配反向代理。有一种错误做法是把baseURL直接写成后端地址前后端端口不同就会触发CORS浏览器拦截跨域请求。规范做法是前端全部请求相对路径/api/xxx通过Nginx映射到后端。Vue 3 Element Plus的按需引入有个坑如果用unplugin-vue-components插件组件样式文件可能没有正确加载界面白屏。排查方式是看控制台有没有样式加载失败的警告。解决方案是把组件库改成完整引入或者手动按需引入样式。另一个常见问题打包后访问/路径空白页。打开浏览器控制台发现一堆JS/CSS加载404。原因是vue.config.js里publicPath配置不对。默认publicPath: /是根路径如果部署在Nginx子路径下比如/community/就要改成publicPath: /community/否则资源文件路径对不上。4.4 业务逻辑和并发相关的坑缴费模块有个并发场景值得注意业主同时从PC端和手机端分别支付同一笔账单两个请求同时打到后端如果代码没做幂等处理可能收到两笔钱。解决思路是在支付前用数据库唯一索引限制同一笔账单的支付请求或者加状态判断——只有UNPAID状态的账单才允许发起支付并且更新操作要带WHERE status UNPAID条件用乐观锁思路防止重复支付。报修派单也有类似问题两个管理员同时把同一个工单派给不同维修工工单状态就乱了。处理办法是在工单表加version字段更新时判断version是否还是旧值不是就说明被别人改过了直接拒绝更新并返回错误提示。这类并发问题在做管理系统时容易被忽略因为测试环境只有几个人点触发不了并发但上线后一个小区几百个业主同时操作某个月底集中缴费问题就暴露了。提前在更新语句里加乐观锁不费什么事能挡掉一大片潜在bug。5. 项目扩展方向与最终交付清单系统能跑通基础的业主、房产、缴费、报修、访客、公告流程之后可以往几个方向扩展。最实用的是对接微信通知缴费成功后、报修状态变更时主动推送消息给业主这能大幅减少物业接电话的量。技术上是调用微信模板消息或小程序的订阅消息接口在缴费记录表加一个notice_status字段记录推送状态。其次是数据统计可视化。物业经理需要看每个月的收费率、各楼栋的报修分布、空置率变化趋势这些从现有表就能统计出来需要的是SQL聚合查询和前端图表展示。可以用ECharts画折线图和饼图后端加一个statistics接口返回聚合结果。这类功能对物业的吸引力很大也因此是市场上管理系统的核心卖点。如果考虑多个小区共用一套系统就要在表里统一加community_id字段做数据隔离所有查询都带上这个条件防止跨小区数据串了。再把小区信息提成独立表管理员登录后选择当前管理的小区。最后检查一遍交付清单后端jar包、前端dist文件夹、数据库初始化脚本、部署说明文档。数据库脚本里要包含建库语句、建表语句和初始管理员账号数据不然接手的人拿到代码跑不起来。我建议把默认管理员账号写在部署文档里密码指定为必须修改的强密码。项目交付不是代码能跑就行文档和初始化数据要完整后边维护的人才不会骂人。做这套系统我个人最大的体会是技术栈本身不是难点难点全在业务细节和工程规范——权限边界怎么划、状态流转怎么做、并发场景怎么防御、部署环境怎么适配。SpringBoot和Vue都是用来承载业务逻辑的工具如果对业务规则的理解不透再新的框架也救不了。反过来把表结构设计、状态机约束、权限校验这些基本功打扎实哪怕换一套技术栈做出来的系统依然立得住。这也是一套“综合小区管理系统”最有价值的地方。
返回列表