
做小区管理系统这类项目是很多走Java路线的人绕不开的一道坎。它不像电商秒杀那么复杂但麻雀虽小五脏俱全业主档案、房产绑定、物业缴费、报修工单、车位管理、公告发布每一块都是典型的CRUD业务加上前后端分离的架构要求正好把SpringBootVueMyBatisMySQL这条完整技术链路串起来。我手头这套综合小区管理系统完整源码前后端加起来代码量中等偏上单表管理和复杂关联查询都有覆盖很适合作为项目实战练手或者毕设参考。这篇内容就围绕它的设计思路、核心模块、联调踩坑和部署上线展开想自己动手复现的可以直接照抄。适合三类人看一是学完SpringBoot基础但缺完整项目经验的Java后端二是选了这个题目的毕业生三是刚开始尝试前后端分离开发模式的初级工程师。前端基础薄弱也没关系这套系统的Vue部分用的是主流组件库照着项目结构能看懂八成。1. 项目概述这个小区管理系统到底做了什么1.1 核心业务需求拆解接手这类项目第一步不是写代码而是把业务边界划清楚。综合小区管理系统听起来名字大实际落地就是围绕小区里的人、房、钱、事做管理。人业主、住户、家属成员以及物业内部的操作人员管理员、收费员、维修工等。房楼栋、单元、房间号、建筑面积、户型以及业主与房产的绑定关系。钱物业费、停车费的账单生成、缴费记录、欠费统计。事报修工单的提交、派单、维修、回访公告通知的发布访客登记等。这块系统的典型功能模块我整理成一张表开发时照着拆任务就行模块核心实体关键操作关联关系业主管理业主、家庭成员新增、编辑、迁出、导入导出业主1对N房产房产管理楼栋、房屋楼栋维护、房屋绑定业主、空置房筛选楼栋1对N房屋物业收费账单、缴费记录账单生成、收费、退费、欠费催缴账单关联房屋报修工单报修单提交、派单、接单、完工、评价工单关联业主车位管理车位、绑定记录车位出租、到期提醒车位关联业主公告管理公告发布、置顶、下线后台管理系统管理用户、角色、菜单登录、RBAC权限、操作日志用户关联角色这个结构看起来常规但每个模块都有值得做的细节。比如房屋的已售/未售/已入住/空置状态流转账单的待缴/已缴/逾期自动判定都是面试和答辩时能拿出来讲的点。1.2 为什么选前后端分离架构早期很多小区管理系统是JSPSpringMVC的老结构页面和后端代码混在一起改个按钮样式都要重新编译部署。前后端分离的核心区别是前端只关心页面渲染和交互通过HTTP接口拿数据后端只提供JSON接口不关心页面长什么样。两边独立开发、独立部署甚至可以同时开工只要提前约定好接口文档。对学习者的实际好处也很直接调试效率高。前端用Vue的devServer跑在8080端口后端SpringBoot跑在8081端口前端通过代理转发请求改前端代码热更新秒级生效不用像老项目那样每次重启整个应用。后端开发也能用Postman或Apifox单独测接口不必依赖页面跳转。部署层面后端打成一个jar包前端构建成纯静态文件交给Nginx托管接口路径通过代理转发到后端端口。这套模式也是目前绝大多数互联网公司的标准做法做完这个项目你等于把生产环境的部署思路也过了一遍。2. 技术选型与工程结构设计2.1 SpringBoot版本与基础配置后端我用的SpringBoot 2.7.x搭配JDK8。这里特意提醒不要一上来就追最新版本。SpringBoot 3.x要求JDK17起步部分老版本的MyBatis启动器、代码生成器、第三方SDK还没完全适配遇到问题排查成本高。2.7.x是2.x系列的成熟版本网上资料最多踩坑也最少等把整个链路跑通了再考虑升不升。核心依赖就这几样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 groupIdcom.mysql/groupId artifactIdmysql-connector-j/artifactId scoperuntime/scope /dependency dependency groupIdio.jsonwebtoken/groupId artifactIdjjwt/artifactId version0.9.1/version /dependency配置文件里有两个关键参数必须交代清楚不然启动就报错spring: datasource: url: jdbc:mysql://localhost:3306/community?useUnicodetruecharacterEncodingutf8useSSLfalseserverTimezoneAsia/ShanghaiallowPublicKeyRetrievaltrue username: root password: 123456 driver-class-name: com.mysql.cj.jdbc.DriveruseSSLfalse是因为本机开发环境没必要走SSL握手不然会报SSL连接警告甚至错误serverTimezoneAsia/Shanghai解决MySQL 8.0时区导致的日期时间差8小时问题allowPublicKeyRetrievaltrue是配合MySQL 8.0的caching_sha2_password认证插件用的很多人连不上数据库就是栽在这三个参数上。2.2 Vue端工程结构与版本选择前端用的Vue2 Element-UI配合Vue Router和Axios。为什么不选Vue3不是Vue3不好而是这套系统里大量页面是表格表单弹窗的经典管理后台形态Element-UI在这类场景下组件最齐、示例最多Vue2的资料覆盖了几乎所有能踩的坑。如果你是刚接触VueVue2的技术栈学起来更平滑面试时再补Vue3的差异点完全来得及。前端目录结构按模块划分src/ api/ # 按模块拆分的接口请求文件 assets/ # 静态资源 components/ # 公共组件分页、上传、富文本等 router/ # 路由配置含动态路由 store/ # Vuex状态管理用户信息、token views/ system/ # 系统管理页面 owner/ # 业主管理页面 property/ # 房产管理页面 fee/ # 收费管理页面 repair/ # 报修管理页面 parking/ # 车位管理页面 notice/ # 公告管理页面 utils/ # 请求封装、工具函数utils/request.js是整个前端的命脉所有请求都走这一个封装统一处理token注入、响应拦截、错误提示。后期要加接口鉴权或者全局处理超时只改这一个文件就行。2.3 MyBatis层设计思路MyBatis在这套系统里承担数据访问层的工作我选择用XML方式管理复杂SQL注解方式只保留最简单的单表查询。理由很实际多表联查、动态条件、批量更新这类操作XML里的sql片段和where标签比注解里的字符串拼接清晰太多而且SQL调整不用重新编译Java代码。一个典型的动态查询例子业主列表的模糊搜索加房产状态筛选select idselectOwnerList resultTypecom.demo.entity.Owner SELECT o.*, h.building_no, h.unit_no, h.room_no FROM owner o LEFT JOIN house h ON o.house_id h.id where if testownerName ! null and ownerName ! AND o.owner_name LIKE CONCAT(%, #{ownerName}, %) /if if testhouseStatus ! null and houseStatus ! AND h.status #{houseStatus} /if /where ORDER BY o.create_time DESC /selectwhere标签会自动去掉第一个多余的条件前缀避免手动写where 11的土办法。接口层只需要定义一个方法签名参数通过Param注解传入MyBatis自动完成参数映射。这里还要说一个容易忽略的点实体类的字段命名要用驼峰数据库字段用下划线然后在application.yml里开启驼峰映射mybatis: configuration: map-underscore-to-camel-case: true不开这个配置create_time查出来就映射不到createTime属性上结果全是null排查半天都找不到原因。2.4 MySQL数据库设计要点数据库名community字符集用utf8mb4而不是utf8因为utf8在MySQL里最多存3字节遇到生僻字或者表情符号就报错utf8mb4才是完整的UTF-8支持。表设计上几个关键点需要特别注意逻辑删除所有业务表加deleted字段默认0。删除操作走UPDATE而不是DELETE避免误删数据也给后续审计留余地。创建时间/更新时间每张表都加create_time和update_time插入和更新时由后端统一填充不用数据库触发器保持逻辑在代码里可见。金额字段用DECIMAL(10,2)禁止用FLOAT或DOUBLE。浮点数的二进制存储会带来精度误差涉及钱一分都不能差。状态字段用TINYINT加注释比如房屋状态0未售、1已售未入住、2已入住、3空置。数字比字符串省空间加注释保证可读性。业主和房产的关系是一对多一个业主名下可能有多套房所以owner表里不应该直接存house_id而是反过来在house表里存owner_id。如果系统支持一个房产多个共有人那就需要一张中间表house_owner_rel这属于多对多业务上更灵活。3. 核心模块实现与关键逻辑3.1 登录认证与权限拦截登录接口的逻辑不复杂流程是前端提交用户名密码后端用BCrypt校验密码BCrypt是单向哈希同一密码每次加密结果都不同比MD5安全得多校验通过后生成JWT返回给前端前端存在localStorage里之后每次请求都在请求头带上Authorization: Bearer token。JWT的结构分三段Header加密算法、Payload用户信息、Signature签名。我用jjwt库生成密钥放在配置文件中String token Jwts.builder() .setSubject(user.getUsername()) .claim(userId, user.getId()) .claim(role, user.getRole()) .setExpiration(new Date(System.currentTimeMillis() 1000 * 60 * 60 * 2)) .signWith(SignatureAlgorithm.HS256, secretKey) .compact();服务端校验token这一步用Spring的HandlerInterceptor实现拦截器注册时排除登录接口和静态资源Override public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception { String token request.getHeader(Authorization); if (token null || !token.startsWith(Bearer )) { response.setStatus(401); return false; } try { Claims claims Jwts.parser().setSigningKey(secretKey).parseClaimsJwt(token.replace(Bearer , )).getBody(); request.setAttribute(userId, claims.get(userId)); return true; } catch (Exception e) { response.setStatus(401); return false; } }这套方案的单点问题在于token一旦签发在有效期内无法主动失效用户退出登录只是前端删了token后端并没有真正踢掉会话。如果追求更严格的控制可以把token存一份到Redis登录时写入、退出时删除、每次请求对比这是生产环境的做法。自己学习阶段用纯JWT够用了。3.2 业主信息与房产绑定业主管理页面是典型的CRUD但有两个功能值得单独讲。第一个是分页查询前端传pageNum和pageSize后端用PageHelper做物理分页PageHelper.startPage(pageNum, pageSize); ListOwnerVO list ownerMapper.selectOwnerList(query); PageInfoOwnerVO pageInfo new PageInfo(list);PageHelper的原理是拦截器在执行SQL前自动拼接LIMIT但要注意它必须在查询方法调用前一行执行中间不能有其他查询操作否则分页会作用到错误的SQL上。第二个是Excel导入导出用EasyExcel组件比Apache POI上手快得多内存占用也小。导入的坑在字段校验手机号格式、身份证位数、楼栋房号是否存在逐行校验并收集错误行号一次性把错误信息返回前端让用户知道哪一行填错了而不是导入失败后一头雾水。3.3 物业缴费与账单状态流转物业缴费模块的逻辑是整套系统里最贴近真实业务的。账单的生成有两种方式按月定时生成和手动补录。定时生成可以用Scheduled注解每月1号扫描所有已入住的房屋按面积乘以单价生成当月账单Scheduled(cron 0 0 0 1 * ?) public void generateMonthlyBill() { ListHouse houses houseMapper.selectOccupiedHouses(); for (House house : houses) { Bill bill new Bill(); bill.setHouseId(house.getId()); bill.setOwnerId(house.getOwnerId()); bill.setAmount(house.getArea().multiply(feeRate)); bill.setStatus(0); // 待缴 bill.setDueDate(lastDayOfMonth()); billMapper.insert(bill); } }注意金额计算用BigDecimal的multiply不能用double直接乘。账单状态有四种0待缴、1已缴、2逾期、3已作废。逾期状态不是定时任务改出来的而是查询时判断status 0 AND due_date now动态计算为逾期。这样保证了状态永远由真实数据推导而来不会出现定时任务漏跑导致状态不一致的问题。缴费动作背后还有一个连锁逻辑账单状态改为已缴的同时要生成一条缴费记录记录缴费人、缴费方式、操作时间这笔记录就是财务报表的原始凭证。前端缴费页面接入微信/支付宝支付需要商户号个人项目通常用线下缴费后台确认的方式模拟前端点击确认收款后端把账单置为已缴流程一样能跑通。3.4 报修工单的状态机报修工单模块是一个简化的状态机0待派单、1已派单待接单、2已接单维修中、3待验收、4已完成、5已取消。每个状态之间的流转不是随便跳的必须有对应的操作业主提交 - 0待派单管理员派单 - 1已派单维修工接单 - 2维修中维修工填报完工 - 3待验收业主确认验收 - 4已完成业主取消或管理员关闭 - 5已取消实现上我在updateStatus方法里写了一个状态迁移校验表用Map当前状态, 允许的目标状态集合控制流转非法操作直接抛业务异常。这个设计的价值在答辩或面试时非常加分比单纯写一堆if判断优雅得多也方便后人维护——状态流转规则集中在类常量层面一眼看全。4. 前后端联调中的典型问题4.1 跨域问题与三种解决方式前后端分离开发时前端页面在http://localhost:8080后端接口在http://localhost:8081浏览器就会因为同源策略拦截跨域请求。解决方式有三种我按推荐程度排序第一种后端加全局CORS配置。写一个WebMvcConfigurer配置类统一允许跨域Configuration public class CorsConfig implements WebMvcConfigurer { Override public void addCorsMappings(CorsRegistry registry) { registry.addMapping(/**) .allowedOriginPatterns(*) .allowedMethods(GET, POST, PUT, DELETE, OPTIONS) .allowedHeaders(*) .allowCredentials(true) .maxAge(3600); } }第二种前端devServer代理。在vue.config.js里配置devServer: { proxy: { /api: { target: http://localhost:8081, changeOrigin: true } } }这样前端请求/api/owner/list时开发服务器把请求转发到后端8081端口浏览器看到的请求是同源的不触发跨域。第三种是Nginx反向代理部署阶段用的就是这个后面部署部分会详细讲。我的经验是开发阶段用第二种后端不用写任何跨域代码保持接口纯净部署阶段用第三种Nginx把/api路径转发到后端端口。第一种方案如果配置不好会把OPTIONS预检请求也拦截掉反而多出问题。4.2 统一返回结构与Axios封装前后端联调最烦的事情是接口返回格式不统一有的接口返回{code: 200, data: [...]}有的直接返回[...]前端每个请求都要单独判断代码混乱到没法维护。这套系统从一开始就定了统一返回结构{ code: 200, message: 操作成功, data: { } }后端用ResultT泛型类包装所有接口返回值成功调用Result.success(data)失败调用Result.error(code, message)。配合全局异常处理器RestControllerAdvice业务异常、参数校验异常、系统异常分别返回不同的code前端拦截器根据code统一处理。前端的Axios封装对应做两件事请求拦截器里从localStorage取出token加到请求头响应拦截器里判断code是否200不是就弹出message401就跳转登录页。这样业务代码里只需要写成功之后做什么错误处理全部集中代码量能减少三分之一。4.3 日期格式化和空值处理联调阶段最容易出现的灵异现象往往是JSON序列化搞的鬼。比如后端返回LocalDateTime类型默认序列化出来是2024-01-15T10:30:00中间带个T前端Element-UI的el-date-picker直接显示异常。统一在配置里指定格式spring: jackson: date-format: yyyy-MM-dd HH:mm:ss time-zone: GMT8另一个坑是字段值为null时前端el-table单元格空白一片用户看着像数据丢失。处理方式有两种后端在实体类的getter上返回空字符串或者前端用formatter函数兜底。我更倾向前端处理后端保持数据的真实性null就是null不应该伪装成空字符串。5. 部署教程从源码到上线5.1 环境准备清单部署前先确认环境缺一不可我把版本和下载要点列出来组件版本要求说明JDK1.8用java -version确认注意不要装成JREMaven3.6配置阿里云镜像不然依赖下载能等到怀疑人生Node.js14Vue2项目建议14或16太高版本会有兼容警告MySQL8.0.x安装时选utf8mb4字符集记住root密码Nginx1.20Windows直接下zip包解压即用MySQL 8.0的安装有个常见问题安装到最后一步提示Start Service失败。多半是之前装过MySQL残留的服务或数据目录冲突解决办法是把C:\ProgramData\MySQL目录清掉再重装或者用管理员身份运行安装程序。5.2 后端打包与启动后端打包前先确认application.yml里的数据库地址和密码是生产环境的不要用本机的localhost。mvn clean package -DskipTests-DskipTests跳过单元测试打包速度快很多。打包完成后target目录下会生成community-admin.jar直接启动java -jar community-admin.jar如果服务器内存紧张可以加JVM参数限制内存java -Xms256m -Xmx512m -jar community-admin.jar后台运行用nohupnohup java -jar community-admin.jar app.log 21 启动后访问http://服务器IP:8081看到JSON接口说明后端起来了。查看日志用tail -f app.log出现Started Application in x seconds就是启动成功。5.3 前端构建与Nginx部署前端构建前先确认接口请求地址。开发时用的是相对路径/api配合devServer代理生产环境没有devServer这个/api就由Nginx来代理。构建命令npm install npm run build构建产物在dist目录把这个目录整个上传到服务器然后配置Nginxserver { listen 80; server_name your-domain.com; # 前端静态文件 location / { root /usr/share/nginx/html/community; index index.html; try_files $uri $uri/ /index.html; } # 后端接口代理 location /api { proxy_pass http://localhost:8081; proxy_set_header Host $host; proxy_set_header X-Real-IP $remote_addr; } }try_files $uri $uri/ /index.html;这一行必须写否则Vue Router用了history模式时刷新页面就404。后端接口通过/api前缀透传前端所有请求都以/api开头这样后端接口不用改任何东西。如果不想装Nginx还有一条偷懒的路径把前端构建好的dist目录里的静态文件复制到后端的src/main/resources/static目录重新打包前端页面就跟着jar一起被SpringBoot托管了。这种方式适合个人演示或答辩生产环境还是老老实实用Nginx。5.4 数据库初始化与迁移数据库脚本的维护是个容易被忽视的大问题。这套系统的SQL脚本我分了三个文件01_schema.sql建表、02_data.sql初始化数据包含默认管理员账号、03_update.sql后续升级的增量脚本。增量脚本的设计很重要尤其是项目已经上线后不能直接改旧表结构而是新增脚本保证任何环境都能从零按顺序执行。执行导入命令mysql -u root -p community /opt/scripts/01_schema.sql mysql -u root -p community /opt/scripts/02_data.sql导入前确认数据库已创建CREATE DATABASE community DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci;没有这一步直接导表会报Unknown database。6. 常见问题与排查技巧实录6.1 数据库连接失败这一类问题在部署求助帖里能占到一半症状都是启动时报Cannot connect to MySQL server或者Access denied。逐个排查报错关键词原因解决Access denied for user用户名或密码错误核对配置文件的账号密码Public Key Retrieval is not allowed8.0认证插件问题URL加allowPublicKeyRetrievaltrueCommunications link failure端口不通或服务没起确认3306端口监听防火墙放行The server time zone value时区不正确URL加serverTimezoneAsia/ShanghaiUnknown database数据库没创建先执行CREATE DATABASE一个排查技巧先用命令行工具连数据库mysql -u root -p能连上说明数据库本身没问题再回头看程序的连接串命令行都连不上先从MySQL服务是否启动、端口是否被占用入手。6.2 Maven依赖下载慢或失败Java项目卡在依赖下载是家常便饭mvn clean package跑半天最后报超时多半是中央仓库连不上。配置阿里云镜像在settings.xml的mirrors节点加mirror idaliyunmaven/id mirrorOfcentral/mirrorOf name阿里云公共仓库/name urlhttps://maven.aliyun.com/repository/public/url /mirror还有一类灵异问题是昨天还能编译今天突然报依赖找不到先检查本机~/.m2/repository里对应的jar包是否损坏删除对应目录重新mvn clean下载即可。6.3 Vue页面白屏或刷新404前端Build完部署到Nginx后打开页面白屏控制台报错加载不到JS文件通常有两个原因。第一个是静态资源路径问题Vue CLI默认构建出的资源路径是绝对路径/js/app.js如果把前端部署在子路径下就404需要在vue.config.js里设publicPath: ./。第二个是路由history模式的问题刷新页面时Nginx找不到对应的服务端路由就是前面说的try_files配置没写。6.4 MyBatis绑定异常与SQL报错报Invalid bound statement (not found)是最常见的MyBatis问题原因就几种Mapper接口没有Mapper注解或者没在启动类上配MapperScanXML文件的namespace写错XML文件没有放在resources目录下导致打包时没被复制。检查打包后的jar文件看看com/example/mapper下面有没有对应的XML文件这个手段能定位九成问题。SQL层面的坑主要集中在#{}和${}的混用。#{}是预编译参数占位符会生成?占位防止SQL注入${}是字符串拼接直接把值拼进SQL存在注入风险。排序字段、表名这类不能参数化的场景才用${}但必须做白名单校验。比如排序功能String sortField create_time; // 从白名单里取不直接信任前端传值我曾经见过有人把用户输入的字段名直接拼进order by结果线上数据被恶意删了教训极其深刻。6.5 端口占用与日志排查启动失败还有个高频原因是端口被占用。SpringBoot默认8080如果本机别的服务占了这个端口启动日志会报Port already in use。几个排查命令# Linux netstat -tlnp | grep 8080 lsof -i :8080 # Windows netstat -ano | findstr 8080 taskkill /PID 进程号 /F遇到启动报错不要慌着百度全文先把堆栈信息里第一个Caused by找到那才是根因。很多新手一看到大段异常就慌其实后面的信息都是前因的结果定位第一个Caused by基本就能锁定问题。7. 项目扩展方向与总结做完这套系统之后往深了走有几个方向值得投入。代码层面可以把原有的全局异常处理器扩展成参数校验业务异常未知异常三分层配合Validated注解实现入参自动校验项目会规范很多。功能层面给文件上传模块接入MinIO对象存储比存在本机目录更接近生产环境网上也有不少SpringBoot整合MinIO的现成方案。业务层面加一个业主端小程序或者H5页面后端接口完全复用这就是一个完整的B端C端项目简历上写出来含金量高一大截。我实际做完这套系统最深的体会是前后端分离项目练的不只是技术栈本身更多是契约意识。接口参数怎么定义、返回结构怎么统一、状态码怎么约定、异常怎么传递这些在写代码之前就要定清楚。项目越到后期联调成本越高前期多花半小时把接口文档写细后面能省下几天的时间。另外日志千万别省每个关键操作留一行日志线上出问题时它就是救命稻草。最后分享一个个人习惯每做完一个模块把该模块涉及的表结构、关键SQL、接口列表和踩坑记录整理到项目根目录的README里。这个习惯坚持下来你会发现半年后你还能记起这个项目的细节而不只是记得做过。技术债可以欠文档债尽量别欠。