SpringBoot水务管理系统实践:从架构设计到部署全解析

发布时间:2026/10/2 11:29:25
SpringBoot水务管理系统实践:从架构设计到部署全解析 做这套基于 SpringBoot 的水务管理系统其实不是一个“写代码”的项目更像是一次对水务行业信息化痛点的系统梳理。水务行业和普通互联网项目差别很大表计数据分散、计费规则复杂、终端用户基数大、还牵扯到管网 GIS、远程抄表硬件对接。这些业务细节堆在一起如果没有一套清晰的架构和文档体系光靠几个 CRUD 接口根本撑不住。我最初接这个项目的时候甲方拿来的需求文档就有一百多页里面光水价规则就分了居民阶梯、非居民定额、特种行业三种大类每种下面还有季节系数、损耗系数。所以我在整理这套源码和配套文档的时候一直坚持一个原则代码是业务的翻译文档是代码的说明书两者对不上项目迟早要翻车。这篇内容适合三类人看一是刚入行、想拿一个完整项目练手 SpringBoot 的 Java 开发者这套源码的目录结构和注释规范可以直接当模板二是水务行业或泛公用事业领域的 IT 人员可以参考业务模块怎么划分、计费流程怎么落地三是准备做毕业设计或课程项目的学生源码加部署文档加讲解视频的配套方式比自己从零憋一个 demo 要省力得多。1. 项目整体设计与思路拆解1.1 业务全景与模块划分逻辑水务管理系统听起来是个大而全的平台但落到实际业务核心就三件事抄表、算费、收费。所有功能模块都是围绕这三件事展开的。我把系统拆成了几个核心模块用户档案管理、水表档案管理、抄表管理、计费管理、缴费管理、报表统计、系统权限管理。乍一看和普通的管理系统很像但里面的细节完全是水务行业的玩法。比如“用户档案”不是只存一个姓名和手机号而是要区分居民用户、非居民用户、特种行业用户还要绑定对应的用水地址、供水区域、水表编号、历史欠费状态。因为计费规则是跟着用户类别走的类别错了后面整套水费都会算错。我在设计表结构的时候用户表和水表表是分开的中间通过关联关系连接这样一块表可以换多个水表也能追踪每一个水表的使用历史。“水表档案”这块更重要。水务公司的水表不是统一型号有机械表、智能远传表、NB-IoT 表口径也各不相同。不同表计的读数方式、倍率、起始码都不一样。我的做法是在水表表里单独存一个“表计类型”字段抄表的时候根据不同类型走不同的接入逻辑同时把常用倍率比如 3 倍、5 倍直接配置在档案里抄表录入时乘以倍率才是真实用水量。抄表管理是整个系统的数据入口也是最容错的部分。人工抄表、远传表自动上传、批量导入 Excel 三条路都要支持。抄表数据一进系统就要和同期历史读数做比对如果本次读数小于上次读数要么是换表了要么是录入错误系统必须给出提示不能默默接受脏数据。这个校验逻辑我当时反反复复改了好几版因为真实场景里确实存在负增长的情况比如水表倒装、表计故障回退不能一刀切报错最终做成了“可配置的异常告警”抄表员可以填备注说明原因再由主管审核确认这样既能拦下大多数误操作也不挡正常业务。1.2 技术选型的几个关键决策这套系统的主干选了 SpringBoot不是因为它多时髦而是因为它最适合这种“业务重、并发一般、但要稳”的企业级应用。先说持久层。我用的是 MyBatis-Plus它能在 MyBatis 的基础上省掉大量单表 CRUD 的模板代码。水务系统里像操作日志、抄表记录这种表字段动辄二三十个手写 XML 很不划算直接继承 BaseMapper 就能用现成方法。但多表关联查询我坚持手写 SQL 放在 XML 里因为计费报表这类查询逻辑复杂用 QueryWrapper 硬拼容易把代码写得又乱又难以优化手写 SQL 起码可以把 JOIN 条件和临时表梳理清楚。缓存方面选 Redis主要存两类东西一是登录令牌和权限缓存二是热门查询的临时结果比如首页大屏的统计数据。水务系统不是高并发互联网应用Redis 在这里更多是缓解数据库压力和做分布式会话够用就好不需要上特别复杂的缓存策略。文件存储这块用了 MinIO。水务公司经常要上传现场抄表照片、合同扫描件、工程竣工图这些不能直接堆在应用服务器上也没必要买昂贵的商业对象存储。MinIO 部署在本地或机房服务器上通过 S3 协议对接SpringBoot 里集成非常简单注册一个客户端 Bean调用 putObject 就能完成上传。我后面会在部署文档环节详细展开。定时任务这块我用的是 XXL-Job。水务系统有大量定时任务每天晚上自动生成欠费账单、每月初批量生成上月用水报表、定时从采集平台同步远传表数据。XXL-Job 的可视化控制台和失败重试机制比 Spring 自带的 Scheduled 好用太多尤其是任务执行日志排错的时候能省大量时间。2. 源码结构拆解十几个子模块如何各司其职2.1 后端工程目录与分包规范拿到这套源码后你首先会看到一个按 Maven 标准结构组织的 SpringBoot 工程。我先拧出几个关键的包路径来讲这样你打开源码心里有数。com.water.sys ├── common // 公共模块统一返回结果、异常处理、工具类 ├── config // 配置类MyBatis、Redis、MinIO、拦截器、CORS ├── controller // Web 入口层只做参数接收和结果封装 ├── service // 业务层接口 实现承担事务边界和业务规则 ├── mapper // 数据访问层MyBatis-Plus 接口 XML 文件 ├── entity // 实体类与数据库表结构对应 ├── dto // 前端交互对象接收请求参数、封装返回视图数据 ├── vo // 视图对象给前端页面用的聚合数据 └── quartz // 定时任务与异步任务所有 Controller 的返回值我统一封装成了 Result 对象包含 code、message、data 三个字段。前端只需要解析这一种结构不管成功失败都是同一套 JSON 格式省去了大量前后端扯皮的沟通成本。全局异常处理器里做了不同异常类型的统一捕获自定义的业务异常直接带着友好提示抛给前端不应该让用户看到一堆英文的堆栈信息。2.2 数据表设计与核心模块代码关联数据库我拆了大概三十多张表这里挑几张核心表说说设计思路对应关系在源码的sql目录下都能找到初始化脚本。用户表和水表表分别是user_info和meter_info关联关系在user_meter_rel表中。这样做的好处是一户多表、一表多户的历史切换都有迹可循。抄表记录表meter_read_record是系统数据量增长最快的表我给它加了复合索引查询时默认只取最近十二个月的数据三个月前的数据自动进入归档表。计费结果表bill_info保存每次算费的快照数据包括用水量、单价、总金额、周期、滞纳金一旦生成就不允许直接修改如果计费规则调整需要重算要走作废原账单并生成新账单的流程保证操作可审计。把表结构和模块对应起来看代码就好懂了。比如MeterReadController里的接口本质都是在围绕meter_read_record表做增删改查但关键在于读数的校验、倍率换算、异常标记这些规则写在 Service 层而不是散落在 Controller 里。我见过很多项目业务规则这里写一点、那里写一点出了 bug 翻代码翻到怀疑人生所以在这套源码里我刻意把规则收敛在 Service 层每个方法只干一件事。2.3 前端工程与 Vue 集成方式前端用的是 Vue 全家桶路由、状态管理、UI 组件库一个不少。源码里前端工程独立放在web目录下通过npm run build把产物打包成一个dist目录。这里有一个很实用的做法打包产物可以直接放进 SpringBoot 的src/main/resources/static目录里这样只要启动一个后端服务就能同时提供 API 接口和前端页面部署成本非常低。有些读者会问为什么不直接前后端分离开两个服务对于水务公司这种私有化部署环境很多机房根本不具备复杂的运维条件一台服务器能跑完的事情绝不分两台。所以我把这种“单包部署”做成默认推荐方式后端一个可执行的 JAR 包加一个前端静态资源目录丢到服务器上就能跑。如果后续要做负载均衡、前后端分离只要在 Nginx 里把/api反向代理到后端服务即可配置文件里已经预留好了开关。3. 部署文档解读从零搭建到生产可用3.1 环境准备部署这套系统我推荐的软件环境是这样一套组合JDK 1.8 或 11、Maven 3.6、MySQL 8.0、Redis 6.x、MinIO可选、Nginx可选。JDK 1.8 是兼容性最稳的选择很多水务公司服务器上还是老系统更高版本的 JDK 反而会出现兼容问题。部署前先检查端口占用。SpringBoot 默认端口 8080如果机器上有其他服务占了可以在配置文件里改掉。我当时第一次交付的时候客户那边服务器上已经跑着一个 PHP 项目占着 8080结果我启动一看端口冲突日志里直接报Port already in use。后面我就学乖了在生产环境裸机上部署前一定先执行netstat -tlnp | grep 8080看一眼。3.2 三个核心配置文件的调整要点SpringBoot 的配置文件有application.yml、application-dev.yml、application-prod.yml三份。把多环境的配置分离是基本素养开发环境用内网数据库,生产环境数据源独立切环境的时候只需要一个参数。数据库配置这块我建议在prod配置里务必开启连接参数校验spring: datasource: url: jdbc:mysql://你的IP:3306/water_sys?useUnicodetruecharacterEncodingutf-8useSSLfalseserverTimezoneAsia/Shanghai username: root password: 你自己的密码 hikari: minimum-idle: 5 maximum-pool-size: 20 connection-test-query: SELECT 1serverTimezoneAsia/Shanghai这个参数特别容易踩坑。MySQL 驱动 8.0 之后默认时区是 UTC如果你不指定时区存进去的时间比北京时间整整少了八小时用户在系统里看到的缴费时间全是前一天晚上。我第一次部署时没注意这个客户打电话来说账单时间不对排查了很久才发现是这个原因。Redis 配置类似注意密码不要写在代码里放在配置文件中用环境变量引用更安全。MinIO 的配置项包括 endpoint、accessKey、secretKey、bucketName上传的图片 URL 会存成http://你的MinIO地址/桶名/文件名的形式如果后面用 Nginx 做了反代记得把访问域名一起换掉。3.3 Maven 构建与产物部署环境配好之后构建其实很简单。源码根目录下执行# 如果是前端一起构建先到 web 目录 cd web npm install npm run build # 回到后端根目录 cd .. mvn clean package -Dmaven.test.skiptrue构建完成后target目录下会生成一个water-system.jar。把 jar 包和前端打包好的静态资源放到同一个目录按约定 springboot 的静态资源默认读取classpath:/static所以我建议先把dist目录内容拷到src/main/resources/static下再打 jar 包这样单包部署就很干净。启动命令建议写成 shell 脚本避免关闭终端时进程被杀掉nohup java -jar water-system.jar --spring.profiles.activeprod \ -Xms512m -Xmx1024m logs/water.log 21 注意日志目录必须提前创建。生产环境我推荐把logs/目录软链到磁盘空间比较大的分区因为系统跑起来之后操作日志和定时任务日志增长很快根分区被写满的教训我见过不止一次。3.4 交换机和防火墙的安全组配置生产环境通常会有防火墙。部署完成后强烈建议在安全组里只开放必要端口SSH 22、应用端口 8080、Nginx 80/443。如果 MySQL 和 Redis 与应用在同一台机器不要对公网开放 3306 和 6379只监听内网或 localhost 即可。MySQL 默认监听 0.0.0.0需要在配置文件里绑定地址Redis 也建议设置requirepass并改成内网 IP 监听。这套系统部署文档里我专门写了一页“安全加固清单”就是希望用户别把数据库裸奔在公网上水务数据涉及民生信息安全该做的防护一步也不能省。4. 代码讲解源码里几个核心环节是怎么写的4.1 启动流程与自动装配的观察入口拿到源码后先看主类SpringBootApplication MapperScan(com.water.sys.mapper) public class WaterSystemApplication { public static void main(String[] args) { SpringApplication.run(WaterSystemApplication.class, args); } }SpringBootApplication相当于把Configuration、EnableAutoConfiguration、ComponentScan三个注解打包在一起。SpringBoot 自动装配的原理是启动时扫描META-INF/spring.factories里配置的自动配置类根据条件注解决定哪些 Bean 生效。你要观察这个项目里究竟装配了哪些组件可以在配置文件中打开 debug 日志debug: true启动日志里会有很长一段的自动配置报告哪些生效、哪些被排除一目了然。这在排查“我明明配了 Redis 为什么没生效”之类问题时特别有用。4.2 登录鉴权与权限控制的实现方式这个系统的权限模型是经典的用户-角色-菜单三层结构。用户登录后后端生成一个 token 返回给前端前端存在 localStorage 里后续请求带上 token后端用拦截器校验。拦截器的实现代码在config包下核心是继承HandlerInterceptor重写preHandle方法用 Redis 查 token 是否存在、是否过期并顺带把用户信息放入 ThreadLocal。需要注意静态资源要放行否则前端页面加载时会被拦截器挡住。角色权限这块我用的不是 Spring Security 那套厚重的权限框架而是自己用注解加拦截器做了一套轻量级方案。自定义一个RequirePermission(water:meter:add)注解标注在 Controller 方法上拦截器拿到当前用户的权限集合做匹配。这样做的优点是直观、侵入小、和水务系统现有的菜单管理天然契合二次开发时加一个新页面权限只需要三步菜单表加记录、角色绑定菜单、方法上标注注解。4.3 抄表计费的核心业务代码抄表计费是这套系统里最值得反复读的代码段。核心逻辑在MeterReadService和BillingService中。以“手工抄表录入”为例流程是这样调用端传入水表编号和当前读数后端先去meter_info表查水表档案拿到表计倍率和上次抄表记录。计算真实用水量(当前读数 - 上次读数) * 倍率。读取用户类别和当前计费周期生效的水价规则。根据阶梯价格计算本期水费、污水处理费、垃圾处理费等附加费。生成应收账单落库并锁定不允许随意修改。计费规则我放在数据库表中动态读取而不是把 if-else 硬编码在代码里。水价规则表water_price_rule包含用户类别、起始阶梯、结束阶梯、单价、生效时间。这样水务公司调整水价时只需要在后台管理界面里改一条规则并设置新的生效日期不需要改代码重新发版。二次开发的时候你重点看BillingService.buildBill()这个方法整个算费流程全部在这里串联。远程抄表的实现逻辑也不复杂。采集平台定时把 NB-IoT 表计读数推送到系统的 HTTP 接口或者系统通过定时任务主动拉取然后走同样的算费流程。区别在于远程表数据是程序自动写入必须额外做异常监测比如读数突变超过三倍就要告警等待人工确认。4.4 定时任务与报表统计的实现报表功能是水务公司管理层每天都要看的。首页大屏展示今日用水总量、本月应收水费、欠费笔数等指标这些数据如果每次都去数据库里实时聚合数据库压力会很大。我的方案是XXL-Job 每天凌晨异步生成前一天的统计快照存入报表汇总表前端查询直接读汇总表。日积月累汇总表的数据量很小查询速度毫秒级。在代码里定时任务类统一继承一个基类基类里封装了日志记录、异常捕获、执行状态上报的逻辑。这对排查问题非常有帮助。我见过很多项目里定时任务挂了就静默失败了过了一个月才发现少了数据全部重新跑一次性任务。这套系统里每个任务执行完都会写一条任务日志控制台里能看到每次执行的成功失败状态失败原因、耗时、影响行数全都有。4.5 文件上传与 MinIO 集成的标准写法MinIO 集成在 SpringBoot 里几乎是样板代码。先创建一个MinioConfig注册MinioClientBeanBean public MinioClient minioClient() { return MinioClient.builder() .endpoint(minioProperties.getEndpoint()) .credentials(minioProperties.getAccessKey(), minioProperties.getSecretKey()) .build(); }上传接口里先生成唯一的文件对象名避免文件名冲突然后调用putObject上传成功后把访问地址拼接好返回给前端。需要注意的一点是 MinIO 客户端需要依赖minio的 Java SDK版本要与服务端匹配我之前遇到过客户端版本过高导致签名不兼容的坑后来把版本固定下来就再也没出过问题。5. 部署与运行中的常见问题排查这项目交付之后我在后续维护中着实踩了不少坑挑几个高频问题写在这里当作一份排查速查表。5.1 问题速查表问题现象可能原因解决方法启动时报Port already in use8080 端口被占用netstat -tlnp找出占用进程换端口或停止冲突服务页面可以打开接口全部 401token 校验失败可能是 Redis 缓存被清空重新登录获取新 token检查 Redis 连接是否正常数据库中文乱码连接串缺少编码参数或建表字符集不对确认 URL 里有characterEncodingutf-8建库时指定 utf8mb4上传图片后访问 403MinIO bucket 权限设置不正确在 MinIO 控制台把 bucket 访问策略设为只读查询报表非常慢数据量持续增长索引失效用EXPLAIN分析慢查询 SQL补建联合索引启用按月分区表定时任务没有执行任务调度中心没有配置执行器或任务下发了但执行器未注册检查 XXL-Job 控制台执行器与任务是否正常部署到服务器后前端资源 404静态资源目录没打进去确认dist已拷贝到src/main/resources/static后再 package5.2 经验分享部署排查的思路远比具体命令重要遇到问题先看日志这看起来是句废话但很多人真出问题的时候第一反应是瞎猜。SpringBoot 的日志默认输出在控制台和文件里生产环境我建议在配置里单独配置 logback 的滚动策略保留最近 30 天每天一个文件单个文件超过 100MB 自动切割。这样出问题的时候你能根据时间点快速定位到那一天的日志文件。排查定时任务问题还有一个很实用的技巧XXL-Job 控制台里能看到每次执行的完整日志包括触发参数、执行结果、异常堆栈。我排查了几次任务失败后总结出顺序先看控制台的执行记录是不是成功再看业务日志里有没有报错最后看数据库里的数据是否正确。三层排查下来90% 的问题都能定位。5.3 日志切分与 SQL 慢查询的监控配置MySQL 开启慢查询日志是我每次部署必做的动作。在 my.cnf 里配置slow_query_log ON long_query_time 2 slow_query_log_file /var/log/mysql/slow.log超过 2 秒的 SQL 会全部写入日志。水务系统到了月末缴费高峰期报表查询和账单生成的 SQL 很容易变慢通过慢查询日志能够精准定位是哪条 SQL 性能不行然后针对性优化索引或改写关联查询。不要等用户抱怨“系统卡死了”再开始排查平时多看一眼日志能挽回很多口碑。6. 拿到源码后要怎么学习和二次开发6.1 推荐的学习顺序第一次拿到这套源码不要从头到尾一行行读我建议按这样的顺序来第一步跑起来。按部署文档把系统在本地启动注册一个管理员账号把增删改查的页面都点一遍。先建立整体认知知道这个系统“长什么样”比什么都重要。第二步跟一次核心流程。从创建用户档案开始添加一块水表录入一次抄表读数触发计费生成账单最后完成缴费。把每个环节的页面交互和接口调用对应起来你就理解了整个系统的资金流转逻辑。第三步精读核心 Service。重点看BillingService和MeterReadService这两个类涵盖了系统的关键业务规则。业务代码比技术代码难写因为每一个规则背后都对应一条行业要求或历史经验。第四步动手改需求。我建议你自己加一个“用水异常预警”的功能比如某用户连续三个月用水量为零系统自动生成预警工单。这种扩展练习会让你把学到的 CRUD、定时任务、消息通知全部串联起来比机械地抄代码有用得多。6.2 二次开发时的注意事项水务系统的二次开发和互联网产品还不一样。互联网项目上线后可以快速迭代试错水务系统一旦上线它服务的可能是几十万人口的日常生活改错一笔账就是一次事故。所以在二次开发时我给自己定了几条规矩涉及金额计算的逻辑改动必须先在测试环境用历史数据重算比对水费账单相关接口严禁直接改数据库必须走系统流程新增配置文件项时必须补齐默认值和注释避免其他同事部署时踩坑定时任务改动没经过验证不允许直接部署到生产。还有一点很实际水务公司的 IT 人员水平参差不齐你写的代码和文档越友好后期运维越省心。这套系统里我尽量在关键逻辑上写了中文注释每个模块的 README 也交代了内部结构和扩展点就是希望接手的同事不至于从零摸索。7. 对这次项目交付的一些实在体会项目做完复盘我最想说的是交付一套源码不难难的是交付一套别人能看懂、能部署、能维护的东西。很多开发者把精力都放在写新功能上文档和注释能省就省等三个月后自己回头看不带注释的代码都觉得陌生更别提客户那端的维护人员了。这套水务系统我从一开始就要求自己把部署文档写成一个“没有技术背景的人也能按步骤操作”的手册包括 Linux 命令怎么敲、可能出现什么报错、每种报错怎么解决。后来团队内部复用这套文档新同事从拿到代码到跑通整套环境平均只花了一天半这在很大程度上得益于文档里把坑都提前标明。在实际开发里我对代码讲解这件事也有了新的理解。以前认为“代码写出来别人就能看懂”不是这样的。代码呈现的是逻辑结果但当时的背景、取舍、踩坑过程只存在于开发者的脑子里。把这些讲清楚比贴一堆注释有用得多。所以这套项目里我同步整理了一份代码讲解文档按照“为什么这么设计—怎么实现的—如果我来改会怎么做”的节奏去阐述核心模块希望能让后来者少走一些弯路。最后再分享一个小技巧如果你接手的是一个像水务管理系统这样的老业务领域的项目最优先的事情不是把技术栈升级到多新而是把业务流程梳理清楚。业务理解到位之后用什么框架去实现都只是工具选择问题。反过来技术再花哨业务规则错了系统上线就是给用户添堵。这套系统的成功上线靠的不是 SpringBoot 多高深而是把供水企业那套“抄表、计费、收费、报装、维修”的流程一点一点啃明白之后再用代码老老实实地落实下来。