拓冰建站拓冰建站
首页 / 资讯中心 / 正文

SSM校园水电费管理系统:计费、事务与幂等设计

简介一套面向Java毕业设计的校园水电费管理系统完整项目包适合需要快速完成毕设或课程设计的学生。项目基于SSM框架采用Spring、SpringMVC、MyBatis分层架构结合微信小程序前端与MySQL数据库覆盖水电费账单管理、在线缴费、数据统计、用户管理等核心功能并已获得97分答辩评审认可。包内资源共1010个文件其中Java源码负责业务逻辑与数据访问Vue/JS实现管理端界面交互WXML/WXSS构建小程序端页面JSON/XML负责配置SVG/PNG提供界面素材与图标SQL脚本包含初始化数据另有论文、开题报告、使用文档及安装启动批处理整体仅17.13MB便于在Windows10/11下直接导入运行。目前已有395人学习下载无论作为毕业设计还是课程设计都能帮助读者快速理解SSM整合、小程序交互与水电费业务实现完整的配套文档也适合用作论文写作和项目答辩的参考。1. 这个压缩包解压后真正决定毕业设计上限的是哪几个文件每年毕业季都会出现一批类似标题的压缩包文件名里写着“源码数据库论文开题报告使用文档”看起来五件套齐了。但把这种包解开审视一番之后会发现里边最不值钱的恰恰是论文和开题报告那些通常是通用模板改的。真正决定这个项目能不能跑通、答辩时能不能挡住提问的只有两个东西一个是xxx.sql数据库脚本另一个是Service层里那段抄表、结算、生成账单的计费代码。SSM 框架下的管理系统到处都是但把“本月读数减上月读数”这笔账算对、算稳才是这个校园水电费管理系统区别于普通增删改查项目的分水岭。这篇文章就顺着这个压缩包的骨架展开SSM 工程怎么搭、水电费数据表怎么建、计费逻辑怎么写、本地怎么跑起来最后再讲一个能写进答辩 PPT 的硬核细节。适合正在做这个课题的学生也适合要接手改造这类 SSM 项目的开发者。2. SSM 整合与水电费管理系统的工程骨架2.1 为什么这套题锁死 SSM而不是换成 Spring Boot很多同学拿到这份源码后第一反应是“怎么不用 Spring Boot”。从开发效率上讲 Spring Boot 确实省事但毕业设计这个场景里SSM 有它不可替代的位置。多数高校软件工程专业的评分标准里会明确列出“表现层Controller/业务层Service/持久层Mapper分层是否清晰”这样的打分项SSM 框架这三个层次边界分明每层一个配置文件、一组注解正好对应教学大纲里的知识点覆盖要求。而且压缩包内这份项目的数据库脚本和 Mapper XML 都是按 MyBatis 的写法组织的硬迁到 Spring Boot 要改配置类、事务注解方式、页面模板依赖工作量不小不如直接在 SSM 环境里把它跑透。2.2 最小可用依赖组合Spring 5.3 MyBatis 3.5 Druid这套项目在 JDK 8 Tomcat 8.5 环境下运行最稳。我常用的依赖版本组合如下Maven 构建时直接写进pom.xmlproperties spring.version5.3.27/spring.version mybatis.version3.5.16/mybatis.version mysql.version8.0.33/mysql.version /properties dependencies dependency groupIdorg.springframework/groupId artifactIdspring-context/artifactId version${spring.version}/version /dependency dependency groupIdorg.springframework/groupId artifactIdspring-webmvc/artifactId version${spring.version}/version /dependency dependency groupIdorg.springframework/groupId artifactIdspring-jdbc/artifactId version${spring.version}/version /dependency dependency groupIdorg.mybatis/groupId artifactIdmybatis/artifactId version${mybatis.version}/version /dependency dependency groupIdorg.mybatis/groupId artifactIdmybatis-spring/artifactId version2.1.2/version /dependency dependency groupIdcom.alibaba/groupId artifactIddruid/artifactId version1.2.20/version /dependency dependency groupIdmysql/groupId artifactIdmysql-connector-java/artifactId version${mysql.version}/version /dependency /dependencies这里每个依赖都有明确分工spring-jdbc提供事务管理器DataSourceTransactionManager是 SSM 里必备的mybatis-spring负责把 MyBatis 的SqlSessionFactory交给 Spring 容器管理没有它 Mapper 接口注入不进去。MySQL 8.0.33 驱动类名是com.mysql.cj.jdbc.Driver不是老项目里常见的com.mysql.jdbc.Driver这一点在配置文件里最容易踩坑。2.3 三个配置文件决定整套项目的生命周期SSM 项目的配置核心是web.xml、spring-context.xml、spring-mvc.xml三件套。web.xml负责装配前面两个容器web-app xmlnshttp://xmlns.jcp.org/xml/ns/javaee xmlns:xsihttp://www.w3.org/2001/XMLSchema-instance xsi:schemaLocationhttp://xmlns.jcp.org/xml/ns/javaee http://xmlns.jcp.org/xml/ns/javaee/web-app_3_1.xsd version3.1 context-param param-namecontextConfigLocation/param-name param-valueclasspath:spring-context.xml/param-value /context-param listener listener-classorg.springframework.web.context.ContextLoaderListener/listener-class /listener filter filter-nameencodingFilter/filter-name filter-classorg.springframework.web.filter.CharacterEncodingFilter/filter-class init-param param-nameencoding/param-name param-valueUTF-8/param-value /init-param /filter filter-mapping filter-nameencodingFilter/filter-name url-pattern/*/url-pattern /filter-mapping servlet servlet-namedispatcher/servlet-name servlet-classorg.springframework.web.servlet.DispatcherServlet/servlet-class init-param param-namecontextConfigLocation/param-name param-valueclasspath:spring-mvc.xml/param-value /init-param load-on-startup1/load-on-startup /servlet servlet-mapping servlet-namedispatcher/servlet-name url-pattern//url-pattern /servlet-mapping /web-appContextLoaderListener启动的是 Spring 根容器管理 Service、Mapper、数据源DispatcherServlet启动的是 Web 容器管理 Controller 和视图解析器。两者扫描包必须错开不能让 Controller 被根容器扫到否则事务代理会出现奇怪的失效问题。spring-context.xml里只放数据源、事务、Service 和 Mapper 扫描context:property-placeholder locationclasspath:jdbc.properties/ bean iddataSource classcom.alibaba.druid.pool.DruidDataSource init-methodinit destroy-methodclose property namedriverClassName value${jdbc.driver}/ property nameurl value${jdbc.url}/ property nameusername value${jdbc.username}/ property namepassword value${jdbc.password}/ property namemaxActive value20/ property nameinitialSize value5/ /bean bean idsqlSessionFactory classorg.mybatis.spring.SqlSessionFactoryBean property namedataSource refdataSource/ property namemapperLocations valueclasspath:mapper/*.xml/ property nametypeAliasesPackage valuecom.campus.water.entity/ property nameconfigLocation valueclasspath:mybatis-config.xml/ /bean bean idtransactionManager classorg.springframework.jdbc.datasource.DataSourceTransactionManager property namedataSource refdataSource/ /bean tx:annotation-driven transaction-managertransactionManager/ mybatis:scan base-packagecom.campus.water.mapper/ context:component-scan base-packagecom.campus.water.service/mapperLocations指向classpath:mapper/*.xml如果你的 Mapper XML 没放在这个路径下启动时会报Invalid bound statement (not found)。mybatis:scan负责把com.campus.water.mapper包下的接口直接注册为 Spring Bean这样 Service 里AutowiredMapper 就能成功。tx:annotation-driven开启注解事务后续在 Service 方法上写Transactional就生效了。2.4 spring-mvc.xml 与包扫描边界spring-mvc.xml只扫描 Controller同时配置注解驱动和 JSP 视图解析器mvc:annotation-driven/ context:component-scan base-packagecom.campus.water.controller/ bean classorg.springframework.web.servlet.view.InternalResourceViewResolver property nameprefix value/WEB-INF/views// property namesuffix value.jsp/ /bean mvc:default-servlet-handler/mvc:default-servlet-handler很重要不配的话 JSP 里的静态资源CSS、JS会被 DispatcherServlet 拦截后 404。记住一点根容器和 Web 容器之间的扫描范围不能重叠这属于 SSM 整合时最常见的配置事故。3. 数据库设计与计费核心代码3.1 水电费系统的 7 张核心表这个系统的业务数据围绕一个核心对象展开宿舍。每间宿舍关联一块水表和一块电表按月读取表底数系统根据读数差生成账单。完整建表脚本如下CREATE DATABASE IF NOT EXISTS campus_water DEFAULT CHARSET utf8mb4; USE campus_water; CREATE TABLE tb_building ( id INT PRIMARY KEY AUTO_INCREMENT, name VARCHAR(32) NOT NULL, remark VARCHAR(255) ); CREATE TABLE tb_room ( id INT PRIMARY KEY AUTO_INCREMENT, building_id INT NOT NULL, room_no VARCHAR(16) NOT NULL, charge_type VARCHAR(8) DEFAULT ROOM, UNIQUE KEY uk_room (building_id, room_no) ); CREATE TABLE tb_student ( id INT PRIMARY KEY AUTO_INCREMENT, student_no VARCHAR(20) NOT NULL, name VARCHAR(32) NOT NULL, password VARCHAR(64) NOT NULL, room_id INT, role VARCHAR(8) DEFAULT STUDENT, UNIQUE KEY uk_stu_no (student_no) ); CREATE TABLE tb_reading ( id INT PRIMARY KEY AUTO_INCREMENT, room_id INT NOT NULL, read_month VARCHAR(7) NOT NULL, water_reading DECIMAL(10,2) NOT NULL DEFAULT 0, electricity_reading DECIMAL(10,2) NOT NULL DEFAULT 0, is_settled TINYINT NOT NULL DEFAULT 0, create_time DATETIME DEFAULT CURRENT_TIMESTAMP, UNIQUE KEY uk_room_month (room_id, read_month) ); CREATE TABLE tb_price ( id INT PRIMARY KEY AUTO_INCREMENT, meter_type VARCHAR(8) NOT NULL, level_name VARCHAR(16), start_num DECIMAL(10,2) NOT NULL DEFAULT 0, end_num DECIMAL(10,2), unit_price DECIMAL(6,2) NOT NULL ); CREATE TABLE tb_bill ( id INT PRIMARY KEY AUTO_INCREMENT, room_id INT NOT NULL, bill_month VARCHAR(7) NOT NULL, water_usage DECIMAL(10,2) DEFAULT 0, electricity_usage DECIMAL(10,2) DEFAULT 0, water_fee DECIMAL(10,2) DEFAULT 0, electricity_fee DECIMAL(10,2) DEFAULT 0, total_fee DECIMAL(10,2) DEFAULT 0, status TINYINT DEFAULT 0, pay_time DATETIME, UNIQUE KEY uk_room_month (room_id, bill_month) ); CREATE TABLE tb_payment ( id INT PRIMARY KEY AUTO_INCREMENT, bill_id INT NOT NULL, pay_no VARCHAR(32) NOT NULL, pay_amount DECIMAL(10,2) NOT NULL, pay_type VARCHAR(16) DEFAULT ALIPAY, pay_time DATETIME DEFAULT CURRENT_TIMESTAMP );外键这种写法不用。表数量不多表间关系靠room_id、bill_id这种业务字段维系查询时 JOIN 就行。重点看两个唯一键tb_reading上的uk_room_month和tb_bill上的uk_room_month这两个唯一键是防止同一房间同一个月重复抄表、重复出账的兜底保障。这是和普通增删改查数据库课程设计最大的区别所在防重不是靠 Java 代码而是靠数据库索引兜底。3.2 抄表生成账单的 Service 实现抄表员录入本月水表电表读数后系统要做三件事判断是否已有该月记录、计算本次用量、按阶梯价格算费并落账单。核心代码在BillService里Service public class BillService { Resource private ReadingMapper readingMapper; Resource private BillMapper billMapper; Resource private PriceMapper priceMapper; Transactional(rollbackFor Exception.class) public BillDO generateBill(Long roomId, String month) { // 同一房间同一个月不允许重复出账 BillDO exist billMapper.selectByRoomAndMonth(roomId, month); if (exist ! null) { return exist; } ReadingDO current readingMapper.selectByRoomAndMonth(roomId, month); if (current null) { throw new BusinessException(该房间本月还未抄表); } // 上一条记录用小于当前月份的最近一条 ReadingDO previous readingMapper.selectLastBefore(roomId, month); BigDecimal waterUsage current.getWaterReading() .subtract(previous null ? BigDecimal.ZERO : previous.getWaterReading()); BigDecimal elecUsage current.getElectricityReading() .subtract(previous null ? BigDecimal.ZERO : previous.getElectricityReading()); if (waterUsage.compareTo(BigDecimal.ZERO) 0 || elecUsage.compareTo(BigDecimal.ZERO) 0) { throw new BusinessException(本期读数低于上期请核对抄表数据); } BigDecimal waterFee calcFee(WATER, waterUsage); BigDecimal elecFee calcFee(ELECTRIC, elecUsage); BigDecimal total waterFee.add(elecFee); BillDO bill BillDO.builder() .roomId(roomId).billMonth(month) .waterUsage(waterUsage).electricityUsage(elecUsage) .waterFee(waterFee).electricityFee(elecFee) .totalFee(total).status(0) .build(); billMapper.insert(bill); current.setIsSettled(1); readingMapper.update(current); return bill; } private BigDecimal calcFee(String meterType, BigDecimal usage) { ListPriceDO levels priceMapper.selectByType(meterType); BigDecimal fee BigDecimal.ZERO; BigDecimal remains usage; for (int i 0; i levels.size(); i) { PriceDO level levels.get(i); BigDecimal levelStart level.getStartNum(); BigDecimal levelEnd level.getEndNum(); BigDecimal levelUsage; if (i levels.size() - 1) { levelUsage remains; } else { levelUsage remains.min(levelEnd.subtract(levelStart)); } if (levelUsage.compareTo(BigDecimal.ZERO) 0) { continue; } fee fee.add(levelUsage.multiply(level.getUnitPrice())); remains remains.subtract(levelUsage); if (remains.compareTo(BigDecimal.ZERO) 0) { break; } } return fee; } }这段代码里有几个细节要特别注意。selectLastBefore查的是“比当前月份小”中离当前最近的一条抄表记录这是计算用量的正确姿势如果用“上个月固定值”去查一旦中间某个月漏抄表用量就会变成 0宿管就会找你麻烦。calcFee里的阶梯算法是在循环内逐段扣减阶梯用量而不是一次性取一个价这样支持任意多档位数据库加配置就行。负读数直接抛异常不让脏数据流到账单表。3.3 Controller 层与权限拦截控制器层按功能拆成ReadingController、BillController、PaymentController。以生成账单的请求为例Controller RequestMapping(/bill) public class BillController { Resource private BillService billService; PostMapping(/generate) ResponseBody public Result generate(RequestParam Long roomId, RequestParam String month) { try { BillDO bill billService.generateBill(roomId, month); return Result.ok(bill); } catch (BusinessException e) { return Result.fail(e.getMessage()); } } }权限拦截建议在spring-mvc.xml中配置mvc:interceptors写一个AdminInterceptor只拦截/admin/**路径。学生端的查询接口不拦截管理员端必须校验 session 中的role字段。登录密码用 MD5 加盐存储就够了这个项目不涉及高安全场景但盐值不要写死每个用户随机生成一个盐再哈希。4. 用 IDEA 与 Tomcat8 把项目跑通的完整路径4.1 从 zip 包到本地环境的三个前置条件先把压缩包解压后不要急着导入 IDEA。先确认本机环境JDK 必须是 8Tomcat 建议 8.5MySQL 用 5.7 或 8.0 均可。数据库连接串在jdbc.properties里jdbc.drivercom.mysql.cj.jdbc.Driver jdbc.urljdbc:mysql://localhost:3306/campus_water?useUnicodetruecharacterEncodingutf8useSSLfalseserverTimezoneAsia/ShanghaiallowPublicKeyRetrievaltrue jdbc.usernameroot jdbc.password123456serverTimezoneAsia/Shanghai是 MySQL 8.0 的必填项不写会报时区错误allowPublicKeyRetrievaltrue是为了解决 MySQL 8.0 使用 caching_sha2_password 认证时的连接失败问题。如果本机装的是 MySQL 5.7驱动可以换成 5.1.49注意 5.7 用户用useSSLfalse就够了。4.2 数据库初始化与 IDEA 配置数据库导入直接用命令行比在 Navicat 里点导入更不容易出错mysql -uroot -p sql/campus_water.sql导入后先执行几条 SQL 验证表是否齐全USE campus_water; SHOW TABLES; SELECT COUNT(*) FROM tb_room;在 IDEA 里按File - New - Project from Existing Sources导入解压后的目录选择 Maven 工程等待依赖下载完成。然后配置 TomcatRun - Edit Configurations - - Tomcat Server - LocalDeployment页签点 - Artifact - 选择 war explodedApplication context填/campus启动后访问http://localhost:8080/campus常见启动报错有两大类一是java.lang.ClassNotFoundException: org.springframework.web.context.ContextLoaderListener说明 spring-web 依赖没进 lib二是Invalid bound statement (not found)说明mapperLocations路径和实际 Mapper XML 存放位置不一致。这两种在日志里都是红色堆栈按上面的配置核对即可。4.3 用 Maven 打 war 包部署到独立 Tomcat如果你不想用 IDEA 内置 Tomcat用 Maven 命令行构建也可以mvn clean package -DskipTests cp target/campus-water.war /usr/local/tomcat8/webapps/ cd /usr/local/tomcat8/bin ./startup.sh启动完成后访问http://localhost:8080/campus-water/。用独立 Tomcat 部署时多注意一个细节campus-water.war部署后解压出的目录名是campus-water访问路径里必须带这个上下文名。如果希望访问根路径可以改成ROOT.war这是 Tomcat 的约定规则。部署后查看日志tail -f /usr/local/tomcat8/logs/catalina.out出现Deployment of web application archive has finished就说明部署成功。Tomcat 日志排查时最忌讳只看 IDEA 控制台catalina.out里才是完整堆栈。4.4 数据不一致时的排查顺序页面功能表现异常时按这个顺序排查先看浏览器开发者工具 Network 面板里请求是否返回 404 或 500再看 IDEA 控制台对应 SQL 日志最后直接拿 SQL 到数据库里执行一遍。SSM 项目里大量“页面白屏”问题本质上是 Mapper XML 里 resultType 和实体类字段不匹配MyBatis 默认开启驼峰映射需要在mybatis-config.xml里设置configuration settings setting namemapUnderscoreToCamelCase valuetrue/ /settings /configuration不开启的话数据库字段room_id映射不到实体属性roomId查询结果全是 null但又不会报错属于最隐蔽的坑。5. 抄表复核与幂等设计让水电费结算接口经得起反复调用5.1 用量突变检测毕业设计里少见的亮点功能水电费系统在真实使用中常遇到一个问题某个月宿舍没人住抄表员还是会录入读数两次读数相同用量是 0但极端情况是有时读了隔壁房间的表造成用量突然暴涨几倍。这种数据用肉眼核对很难发现可以在ReadingService里加一段突变检测public void checkAbnormal(ReadingDO current, ReadingDO previous) { if (previous null) { return; } BigDecimal elecUsage current.getElectricityReading() .subtract(previous.getElectricityReading()); BigDecimal monthAvg readingMapper.selectAvgUsage(current.getRoomId()); if (monthAvg ! null elecUsage.compareTo(monthAvg.multiply(new BigDecimal(3))) 0) { throw new BusinessException(该房间用电量超过月均3倍请核对读数); } }把抄表平均值作为基线超过 3 倍直接拒绝提交让管理员二次确认。这是实际项目中真正能抗住“垃圾数据”的设计答辩时把这段代码讲清楚比你贴十个增删改查方法都有说服力。5.2 用数据库唯一索引兜底重复出账管理端出账操作可能被用户连续点两次generateBill里虽然先查了exist但两个请求同时进来时两次查询都返回空然后都执行 insert唯一索引uk_room_month (room_id, bill_month)会直接让第二次 insert 抛DuplicateKeyException。在 Service 里捕住这个异常做幂等返回try { billMapper.insert(bill); } catch (DuplicateKeyException e) { return billMapper.selectByRoomAndMonth(roomId, month); }逻辑是数据库索引拦截了重复数据业务代码捕获冲突后把已存在的账单返回给前端。幂等设计的原则就是“接口被重复调用也只会产生一份账单”这个实践在支付、订单系统里对口可以当着面试题的素材讲。5.3 冲正而不是删除保留账单的审计链路万一确实录错了比如把 1200 度录成了 120 度正确的做法不是把账单删掉重新生成而是做“冲正”把原账单标记为作废再生成一张同月新账单并保留原记录UPDATE tb_bill SET status 2, remark 抄表有误作废重算 WHERE id #{billId}; INSERT INTO tb_bill (room_id, bill_month, water_usage, electricity_usage, water_fee, electricity_fee, total_fee, status, remark) SELECT room_id, bill_month, water_usage, electricity_usage, water_fee, electricity_fee, total_fee, 0, 冲正后重算 FROM tb_bill WHERE id #{billId};这样财务对账时每一分钱都能追溯来路而不是消失了。这里把原账单金额反号后写入新账单即可。对毕业设计而言这一条“作废原单并保留记录再生成新单”的思路已经达到了企业级系统的设计高度能直接把项目从“增删改查”拉高到“业务闭环”的层次后续不管在面试里聊“程序如何保证数据正确性”还是聊事务与幂等设计都有细节可以展开。本文还有配套的精品资源点击获取
分享:

看完干货,该让你的企业上线了

免费需求沟通 · 48 小时内出具建站方案 · 河南本地可上门