Java电费管理系统源码部署与阶梯电价计费实战

发布时间:2026/10/29 2:35:18
Java电费管理系统源码部署与阶梯电价计费实战

简介:面向计算机专业毕业设计与Java Web实战学习的电费管理系统项目源码,围绕居民小区或企业的抄表、计费、收费场景,提供用户管理、电费计算、在线缴费、数据统计和欠费提醒等模块,适合用于毕业设计参考、课程项目改造及Spring Boot全栈练习。压缩包共158个文件,约1.78MB,以52个Java后端源码、24个JavaScript脚本、19个HTML页面、14个CSS样式以及XML、properties配置为主,分别对应业务逻辑、页面交互、界面布局与框架配置,并体现MVC分层和数据库映射思路。已有117人学习,说明该案例对中小型管理系统开发具有一定参考价值,也便于观察完整业务闭环。读者可据此了解Spring Boot、MyBatis、MySQL与Spring Security的整合方式,梳理阶梯电价与峰谷电价计算、JWT身份认证、在线支付、报表导出和定时提醒的实现路径,也可借助现有目录结构完成功能扩展、代码重构与答辩材料准备。

1. 拿到一份 Java 电费管理系统源码,先别急着双击运行

很多人下载完毕业设计电费管理系统(java).zip的第一反应,是解压、找 main 方法、双击运行,然后被一屏红色堆栈劝退,转头就判定“这包是坏的”。真实情况是,这类 Java Web 电费项目的启动门槛几乎全在环境侧:JDK 版本、MySQL 版本、数据库名、连接串密码、Tomcat 端口,任意一处对不上,框架都不会给你一句人话提示,而是直接抛ClassNotFoundException或者Access denied。这份资源真正值钱的地方不是界面,而是它把“用户—电表—抄表—计费—缴费—统计”这条链完整串了起来,里面包含阶梯电价分段累加、欠费状态流转、月度报表汇总这些答辩必问的点。它适合三类人:缺一个完整业务闭环交毕设的、想拿现成 Java Web 项目练手熟悉分层结构的、需要一套可改的计费逻辑做二次开发的。下面按“先认清结构、再跑通环境、最后改得动业务”的顺序往下拆。

2. 技术栈与目录结构:这份包到底由哪几层拼起来

拿到一个不明底细的 Java 项目,最忌讳的就是先改代码。先花二十分钟把技术栈和分层摸清楚,后面所有报错你都能定位到具体一层。这一章的目标是让你在不动一行业务代码的前提下,说清楚这个包用的是哪套框架、请求从哪进、数据落到哪张表。

2.1 从构建文件和依赖列表反推技术栈

市面上这类电费管理系统的常见做法是两种组合:一种是 SSM(Spring + SpringMVC + MyBatis)配 JSP 页面,另一种是 SpringBoot 单体配 Thymeleaf 或前后端分离的 Vue。两者在目录结构上差别很大,判断依据就是你解压后第一眼看到的东西。

# 在解压后的项目根目录执行,先确认构建方式 ls -l # 三种典型输出对应的结论: # 1) 看到 pom.xml -> Maven 工程,SSM 或 SpringBoot 都可能 # 2) 看到 build.gradle -> Gradle 工程,改依赖要看 build.gradle # 3) 只有 WEB-INF/lib/*.jar -> 老式非构建工具工程,依赖要手工往 lib 里丢

确认是 Maven 之后,别去一个个翻 jar 包,直接用依赖树把框架版本打出来。这一步决定了你后面该装 JDK 8 还是 JDK 17、该配 Tomcat 8 还是 Tomcat 10。

# 只过滤出关键框架坐标,避免几百行依赖看花眼 mvn -q dependency:tree \ -Dincludes=org.springframework:*,org.mybatis:*,mysql:*,javax.servlet:* \ 2>/dev/null | head -40

-Dincludes是坐标过滤器,支持groupId:artifactId通配;head -40只是防止输出过长,实际排查时可以把过滤条件放宽。重点看三件事:Spring 的大版本(4.x 还是 5.x,直接决定能不能上 JDK 17)、servlet 包是javax.servlet还是jakarta.servlet(这一条决定 Tomcat 版本,是最常见的翻车点)、MySQL 驱动是mysql:mysql-connector-java还是com.mysql:mysql-connector-j(后者对应 MySQL 8 的新驱动,连接串参数要求不一样)。

如果没有 pom.xml,就得换一种查法:直接看WEB-INF/web.xml里的 servlet 版本声明,以及WEB-INF/lib下 jar 的文件名。jar 名里通常带版本号,比如spring-webmvc-5.3.x.jar,这比猜靠谱得多。这一步做完,你手上应该有一张清单:JDK 版本、Servlet 规范版本、Spring 版本、MySQL 驱动版本。四个数字对齐了,后面 80% 的启动异常不会发生。

2.2 目录分层与一条缴费请求的完整链路

国内这类毕设项目的分层高度趋同,基本是 controller / service / service.impl / dao 或 mapper / entity / util 这几层,前端资源在webapp或resources/static下。与其从头读代码,不如用关键字把入口类捞出来,再顺着入口往下走。

# 定位所有控制器,看清系统对外暴露了哪些业务入口 grep -rn "class .*Controller" --include=*.java . | head -20 # 定位缴费相关逻辑,中英文关键字都给上,防止命名风格不统一 grep -rn "缴费\|pay\|Payment\|recharge" --include=*.java . | head -20

grep -rn的-r是递归,-n是带行号,--include限定只搜 Java 文件,能避开前端资源和编译产物。第二行同时搜中英文,是因为不少毕设代码的注释是中文、方法名是英文,只搜一种会漏。

摸清入口之后,把一条“用户缴费”请求的路径画出来,比读十页文档有效:

环节典型类名职责出问题时的表现
登录拦截LoginInterceptor / Filter校验 session,未登录跳登录页登录成功后又被踢回登录页
控制层BillController / PayController接参、校验、调 service页面 404 或参数为 null
业务层BillServiceImpl事务、计费、状态判断金额算错、重复扣费
持久层BillMapper / BillDaoSQL 执行与结果映射查不到数据但表里明明有
实体层Bill / Meter / User字段与表结构对应字段名对不上导致全 null

顺着这张表,你会很快发现一个关键点:电费系统里最容易被改坏的不是页面,是业务层的计费方法和状态更新语句。这两处一旦逻辑写死或者少加事务,答辩演示时就会出现“点两次缴费扣两次钱”这种当场社死的场面。

2.3 表关系先理清,再动手改字段

电费系统的表数量通常不多,但关系必须理清,否则加一个字段就会引发连锁报错。常见做法是围绕四张核心表展开:用户表存户号、姓名、联系方式、所属片区;电表表存表号、户号、安装地址、初始读数;抄表记录表存表号、抄表日期、本期读数、上期读数;账单表存户号、账期、用电量、应收金额、缴费状态。有的包会把电价配置单独抽一张表,有的直接写在代码常量里,这一点在 4.1 会细说。

判断表关系最快的办法不是看 ER 图(这种包多半没有),而是看建表语句里的外键和索引。

# 从 sql 脚本里快速提取建表语句的表名和主外键 grep -niE "create table|primary key|foreign key|key \`" *.sql | head -40

-i忽略大小写,-E启用扩展正则,这样CREATE TABLE和create table都能命中。重点确认两件事:户号在账单表里是冗余存储还是靠外键关联(冗余存储意味着改户号要手工同步,是个隐性坑);抄表记录和账单之间有没有唯一约束(没有唯一约束就挡不住同一账期重复出账)。

理完这三件事,你就具备了动手的前提:知道该装什么版本、知道业务入口在哪、知道数据长什么样。

3. 环境搭建与数据库落地:JDK、MySQL、连接串三处对齐

环境这一章是整个落地过程里最容易卡住新手的部分,因为它没有“正确答案”,只有“和这个包匹配的答案”。同一个项目,别人机器上能跑,你机器上跑不起来,九成是版本或连接配置的差异。下面按从外到内的顺序,把三处必须对齐的地方逐个落实。

3.1 JDK 与容器版本对齐,别用新版本硬套老项目

先确认本机有什么,再对照 2.1 里查到的框架版本做匹配。

java -version # 看运行环境,通常是 1.8 / 11 / 17 javac -version # 看编译环境,和上面不一致会导致编译期奇怪报错 mvn -version # 顺带确认 Maven 用的哪个 JDK

如果依赖树里显示 Spring 是 4.x 或 5.0/5.1 这种早期 5.x,稳妥选择是 JDK 8;Spring 5.3 可以上 JDK 11 甚至 17,但前提是没用到被移除的内部 API。这里有一条血泪经验:不要因为本机装的是 JDK 17 就硬着头皮上,老 SSM 项目里大量javax.*引用在 JDK 17 加 Tomcat 10 的组合下会集体失效,因为 Tomcat 10 把命名空间整体换成了jakarta.*。改这个的成本,远超装一个 JDK 8。

pom.xml 里的编译级别是另一处要核对的地方:

<properties> <!-- 源码级别和字节码级别要保持一致,只改一个会编译报错 --> <maven.compiler.source>1.8</maven.compiler.source> <maven.compiler.target>1.8</maven.compiler.target> <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding> </properties>

source决定编译器按哪个语言版本校验语法,target决定生成什么版本的字节码,两者不一致时 javac 会直接报错。sourceEncoding单独拎出来说,是因为它决定了源码里的中文注释和中文常量按什么编码读,不写 UTF-8 而本机默认是 GBK 时,中文常量进去就是乱码,排查起来极其费劲。

提示:在 IDE 里导入项目后,还要再检查一遍 IDE 自己的编译级别设置。命令行mvn能过、IDE 里一堆红的情况,基本都是这一处没同步。

3.2 建库、导数据与连接串的三点对齐

数据库这块,先建库再导脚本,顺序反了会报“表不存在”。

-- 字符集用 utf8mb4,户名、地址里出现生僻字或符号时不会截断 CREATE DATABASE IF NOT EXISTS electric_fee DEFAULT CHARACTER SET utf8mb4 DEFAULT COLLATE utf8mb4_general_ci; -- 先切到目标库,再执行脚本内容,否则表会建到默认库里 USE electric_fee; SOURCE /你的路径/electric_fee.sql;

utf8mb4_general_ci是兼容性最稳的排序规则,老的 MySQL 5.7 也认。SOURCE是 mysql 客户端命令,用 Navicat 之类的图形工具时对应“运行 SQL 文件”,但一定要先选中目标库再运行,这是新手最常忽略的一步。

导完表,接着把连接串改对。配置文件的位置不确定,用关键字找:

# 数据源配置可能藏在 properties / yml / xml 任何一处 grep -rniE "jdbc:mysql|datasource|jdbc.username" \ --include=*.properties --include=*.yml --include=*.xml . | head -20

找到之后按下面这份配置逐项核对:

# MySQL 8 驱动类名,5.x 驱动是 com.mysql.jdbc.Driver,写错直接连不上 jdbc.driver=com.mysql.cj.jdbc.Driver # 四个关键参数:时区、编码、SSL、公钥检索 jdbc.url=jdbc:mysql://127.0.0.1:3306/electric_fee?useUnicode=true&characterEncoding=utf8&serverTimezone=Asia/Shanghai&useSSL=false&allowPublicKeyRetrieval=true jdbc.username=root jdbc.password=换成你自己的密码

逐项解释一下这几个参数为什么不能省。serverTimezone不写,MySQL 8 驱动会按服务器默认时区解释时间,账单日期可能整体偏移一天,演示时“本月账单”查不出来就是这么来的。useSSL=false是关掉本地自签证书的连接加密,只用于本机开发,生产环境必须打开。allowPublicKeyRetrieval=true是 MySQL 8 默认认证插件下的常见补救,不加会报Public Key Retrieval is not allowed。characterEncoding=utf8和建库时的utf8mb4是配套的,只改一边,中文仍然可能变问号。

需要严格对齐的就三样:库名(electric_fee要和建库语句一致)、账号密码(要和你本机 MySQL 实际账号一致)、端口(默认 3306,改过就要跟着改)。这三样对上了,数据源初始化基本不会出问题。

3.3 启动方式与首次登录验证

打包和启动分两种,看 2.1 的结论走。

# 跳过测试打包,毕设项目的单元测试往往依赖真实数据库,跑测试反而卡住 mvn clean package -DskipTests # SpringBoot 形态:直接跑 jar,端口可以在命令行覆盖 java -jar target/electric-fee-*.jar --server.port=8080 # SSM 形态:把 war 丢进 Tomcat 的 webapps 目录,然后启动 Tomcat cp target/*.war $TOMCAT_HOME/webapps/ $TOMCAT_HOME/bin/startup.sh

-DskipTests跳过测试执行但仍然编译测试代码,如果连测试代码都编不过,换成-Dmaven.test.skip=true连编译一起跳过。--server.port是 SpringBoot 的命令行参数覆盖,端口被占用时不用改配置文件。SSM 形态下要注意 war 包名就是访问路径的一部分,electric-fee.war丢进去,访问地址是http://localhost:8080/electric-fee/,少写这一段就是 404。

启动成功的判断标准不是“没报错”,而是日志里出现明确的启动完成标志:SpringBoot 会打印Started ... in x seconds,Tomcat 会打印Server startup in xxx ms。看到这两行再去浏览器验证。

首次登录的账号密码,通常写在 sql 脚本的 user 表插入语句里。密码字段有两种可能:明文,或者 MD5 之类的摘要。判断方法是打开登录相关的 service 方法,看它是直接equals比较,还是先做了一次哈希再比较。

# 看登录逻辑怎么处理密码,决定你该往表里塞明文还是密文 grep -rn -A 15 "login" --include=*ServiceImpl.java . | head -40

-A 15表示命中行之后再多显示 15 行,足够覆盖整个方法体。如果是摘要比较,而脚本里插的是明文,登录必然失败,这时候要么用同样的算法生成一条密文更新进库,要么临时把比较逻辑改成明文,验证完再改回去。

4. 电费计算这条主线:阶梯电价、抄表出账与状态流转

前面三章解决的是“能跑起来”,这一章解决的是“跑起来之后算得对”。电费系统跟普通增删改查项目的本质区别就在计费逻辑上,答辩时老师最爱追问的也是这里:电价调整怎么办、跨档怎么算、重复出账怎么防。这三问对应三处代码,改明白了,这个包才算真的被你吃透。

4.1 阶梯电价做成配置表,而不是写死在代码里

先看原包怎么处理的。用关键字定位计费入口:

# 找计费相关的常量和方法,单价、档位这类字眼最容易命中 grep -rn "单价\|unitPrice\|price\|阶梯\|tier" --include=*.java . | head -30

如果命中的是private static final double PRICE = 0.5;这种写法,说明单价是硬编码的,换成任何一档电价都要改代码重编译。这种写法在答辩现场被问到“下个月电价调了怎么办”时会很尴尬。我的习惯是把它抽成一张配置表,代码只认表不认常量。

-- 阶梯电价配置表:一行代表一个档位在一段时间内的单价 CREATE TABLE t_price_tier ( id BIGINT PRIMARY KEY AUTO_INCREMENT, tier_no INT NOT NULL COMMENT '档位序号,从1开始,必须连续', start_kwh DECIMAL(10,2) NOT NULL COMMENT '本档起始度数(含)', end_kwh DECIMAL(10,2) DEFAULT NULL COMMENT '本档结束度数(含),NULL表示无上限', unit_price DECIMAL(10,4) NOT NULL COMMENT '本档单价,元/度', effective_from DATE NOT NULL COMMENT '生效日期,支持按时间切换电价方案', created_at DATETIME DEFAULT CURRENT_TIMESTAMP, UNIQUE KEY uk_tier_eff (tier_no, effective_from) );

几个设计点值得说清楚。单价用DECIMAL(10,4)而不是DOUBLE,因为金额计算里浮点误差会在累加时被放大,出现“差了三分钱”这种查不出原因的账。end_kwh允许为 NULL 表示最后一档无上限,比写一个 999999 更语义化。effective_from是为了支持电价调整,查询时取“生效日期不晚于账期”的最新一版方案,历史账单不受影响。唯一键(tier_no, effective_from)防止同一档位同一生效日期被插两次。

对应的初始数据按三段式居民阶梯抄一份,实际项目里换成业务方给的标准即可:

INSERT INTO t_price_tier (tier_no, start_kwh, end_kwh, unit_price, effective_from) VALUES (1, 0.00, 180.00, 0.5000, '2024-01-01'), (2, 180.00, 400.00, 0.5500, '2024-01-01'), (3, 400.00, NULL, 0.8000, '2024-01-01');

注意档位之间的边界是“左闭右开”还是“左开右闭”,这里用的是上一档结束度数等于下一档起始度数,计算时靠区间裁剪保证不重复计费,4.2 的代码会体现这一点。

4.2 抄表到出账:分段累加的实现与两个易错参数

计费方法本身不到二十行,但写错的方式有很多种。下面这版是我一般会用的写法:

/** * 按阶梯电价计算电费 * @param usage 本期用电量 = 本期抄见读数 - 上期抄见读数 * @param tiers 电价档位列表,调用前必须按 start_kwh 升序排好 */ public BigDecimal calcFee(BigDecimal usage, List<PriceTier> tiers) { if (usage == null || usage.signum() <= 0) { return BigDecimal.ZERO; // 用电量为 0 或负数(换表、倒走)直接返回,避免负数账单 } BigDecimal fee = BigDecimal.ZERO; for (PriceTier tier : tiers) { // 裁剪出本档实际计入的电量:下限以下的部分不算,上限以上的部分留给下一档 BigDecimal upper = tier.getEndKwh() == null ? usage : tier.getEndKwh(); BigDecimal inTier = usage.min(upper).subtract(tier.getStartKwh()); if (inTier.signum() <= 0) { break; // 用电量还没到本档起点,后面的档更不用算 } fee = fee.add(inTier.multiply(tier.getUnitPrice())); } // 只在最终结果上做一次舍入,避免每档各舍一次导致累计偏差 return fee.setScale(2, RoundingMode.HALF_UP); }

逐行解释关键点。usage.min(upper)是区间裁剪的核心:当用电量没超过本档上限时取用电量本身,超过时取上限,这样每档只会拿到落在自己区间内那一段电量。subtract(tier.getStartKwh())扣掉本档起点的部分,因为起点以下已经由前一档算过了。signum() <= 0提前跳出,既省循环也避免出现负数段。最后一步setScale(2, HALF_UP)放在循环外面,是这条逻辑里最容易被忽略的细节——如果每档都舍入一次,三档累加下来可能比正确值多出几分钱,账目对不上时非常难查。

两个必须校验的入参。第一,tiers必须按start_kwh升序,传进来是乱序的话,区间裁剪会直接失效,算出来的金额可能比实际少一半,而且不报错,属于典型的静默错误。第二,usage必须是本期读数减上期读数后的净用量,如果原包里直接把本期读数当用量传进去,那从第二个月开始所有账单都会虚高。这一处值得在出账前加一行日志把上期读数、本期读数、净用量都打出来,第一版跑通后再去掉。

出账的整体流程对应到代码上大概是三步:先查该户该账期是否已有账单(有就跳过,防重复出账),再查最近两条抄表记录算净用量,最后调计费方法写账单。中间任何一步失败都要能回滚,所以这个方法在 service 层必须带事务注解。

4.3 缴费与欠费状态流转:别让用户点两次扣两次钱

状态流转是电费系统里第二容易出问题的地方。典型状态是:0 未出账、1 已出账待缴、2 已缴清、3 逾期欠费。从 1 到 2 的这次跃迁,如果只写一句UPDATE t_bill SET status = 2 WHERE id = ?,用户在网络慢的时候连点两下“缴费”,就会生成两条缴费流水,账面上收了两笔钱。防这个的标准做法是带状态条件的更新,再加一个版本号兜底:

-- 只有当前状态确实还是“待缴”时才更新成功 UPDATE t_bill SET status = 2, pay_time = NOW(), pay_channel= #{channel}, version = version + 1 WHERE id = #{id} AND status = 1 AND version = #{version};

执行完拿返回值判断,affectedRows == 1才算缴费成功,等于 0 说明这笔账单已经被处理过(重复提交或并发),此时应该给前端返回“该账单已缴费,请勿重复操作”,而不是当成失败抛出异常。这一条在答辩演示时特别有用,你可以主动演示连点两次,让老师看到第二次被正确拦下。

version字段是乐观锁,需要在账单表里加上,默认值 0。它在状态条件之外又加了一层保护,作用场景是同一状态下并发修改金额之类的操作,普通缴费场景其实靠status = 1就够,但加上版本号成本很低,习惯性带上没坏处。

还有一个容易被漏掉的状态:逾期。批量把状态 1 改成 3 的任务,判断条件是账期加缴费截止天数小于今天。这个任务必须可重复执行且结果一致,也就是今天跑一次和跑十次结果相同,写法上仍然靠WHERE status = 1 AND due_date < CURDATE()这种带状态的更新来保证,而不是先查出来再逐条改。

5. 排查清单:这套电费系统跑不起来时,八成栽在这几处

前面几章是按顺序推的,实际动手时不会这么顺,往往是启动阶段报一个错、跑起来之后又冒一个错。这一章按“启动期”和“运行期”分开列,每条都按现象、原因、解决三段写,方便你直接对号入座。

5.1 启动期:容器版本、库连接与脚本导入

现象一:启动就抛java.lang.ClassNotFoundException: javax.servlet.Filter或jakarta.servlet.*找不到。原因基本锁定在 Tomcat 版本和 servlet 命名空间不匹配。项目编译时用的是javax.servlet,而你部署到了 Tomcat 10 及以上,Tomcat 10 已经把命名空间整体换成了jakarta.*,类名对不上自然加载失败。解决方式二选一:换 Tomcat 8.5 或 9 部署,这是成本最低的;或者把项目里所有javax.servlet引用改成jakarta.servlet并同步升级依赖,改动面大,非必要不做。判断当前用的是哪个命名空间,直接看依赖树里 servlet 那个坐标的 groupId 就够了。

现象二:启动日志里出现Access denied for user 'root'@'localhost',或者连接超时。原因通常是连接串里的账号密码和本机 MySQL 不一致,或者端口不是 3306。还有一种隐蔽情况:本机装了多个 MySQL 实例,3306 被另一个实例占用,你改的那个配置文件根本没生效。解决方式是先用命令行直连验证:mysql -u root -p -h 127.0.0.1 -P 3306,能进去说明账号密码没问题,再回头核对配置文件路径是否被正确加载。SpringBoot 项目里配置文件可能在resources下也可能在启动目录下,外部配置优先级更高,这点要留意。

现象三:导入 sql 脚本时报ERROR 1067 Invalid default value或ERROR 1146 Table doesn't exist。前一个错的原因是脚本里用了DEFAULT '0000-00-00 00:00:00'这类零日期,而当前 MySQL 的sql_mode开了严格模式,不允许零日期。解决方式是临时放宽会话级严格模式:SET SESSION sql_mode='NO_ENGINE_SUBSTITUTION';再执行导入,导入完可以恢复。后一个错的原因是脚本里有跨库引用或者没先USE目标库,表建到了别的库里,解决办法是在脚本开头显式加上USE electric_fee;。这两个错都属于“脚本没问题、环境不匹配”,别急着去改脚本。

5.2 运行期:登录回跳、中文乱码与金额偏差

现象四:账号密码明明对,登录成功后页面又跳回登录页,像什么都没发生。原因多半在拦截器或过滤器的路径匹配上。常见两种写法错误:一是拦截器把登录接口本身也拦了,登录请求返回的 session 还没写进去就被重定向;二是静态资源路径没放进白名单,页面加载 js、css 时被拦截,浏览器拿不到资源,看起来像是被踢回登录页。解决办法是先看拦截器配置里的排除路径,把登录接口和/static/**、/css/**、/js/**这类资源路径都加进白名单;再确认 session 是否真的写进去了,在登录方法里打印一下 session id,前后对比就能定位。

现象五:页面上的中文变成问号,或者账单里的户名显示成乱码。这是个三处联动的问题,只改一处不解决问题:数据库和表的字符集要utf8mb4、连接串要有characterEncoding=utf8、Tomcat 的server.xml里 Connector 要配URIEncoding="UTF-8"。三处都对了还乱码,就看 JSP 页面头部有没有<%@ page contentType="text/html;charset=UTF-8" %>,以及 IDE 的文件编码是不是 UTF-8。排查顺序建议从数据库往上倒推:先在 mysql 客户端里SELECT一下,看库里存的是不是已经乱了,如果库里就是乱的,问题在写入端,跟页面无关。

现象六:账单金额和手工算的差几分钱,或者第一期就对不上。差几分钱,原因几乎都是用了double做金额累加,浮点误差在分段计算里被放大。解决办法是全链路换成BigDecimal,并且只在最终结果上做一次舍入,中间过程不舍入。第一期就对不上,更可能是取数取错了:把本期抄见读数直接当用电量用了,忘了减上期读数。这一处要在出账逻辑里加日志,把上期读数、本期读数、净用量、各档金额逐档打出来,对着手算结果一段一段比,比猜快得多。

6. 从能跑到能用:把演示效果做扎实的三处改造

跑到能登录、能出账、能缴费,只完成了六成。剩下四成在于演示时能不能稳定复现、被追问时能不能接得住。我一般会做三件事,成本都不高,但效果肉眼可见。

第一件是准备一份可重复执行的初始化数据脚本。演示最怕的是现场点了几下,数据被改乱了,第二次演示状态不对。做法是把用户、电表、连续三个月的抄表记录和账单写成一份脚本,里面用INSERT ... ON DUPLICATE KEY UPDATE或者先TRUNCATE再插入,保证每次执行后系统都回到同一个演示状态。

-- 演示前重置到固定状态,字段顺序与表结构保持一致 TRUNCATE TABLE t_bill; TRUNCATE TABLE t_meter_reading; -- 造三个月连续抄表记录,让趋势图有形状而不是一条直线 INSERT INTO t_meter_reading (meter_no, read_date, current_kwh) VALUES ('M1001', '2024-01-05', 120.00), ('M1001', '2024-02-05', 340.00), ('M1001', '2024-03-05', 720.00);

TRUNCATE会重置自增 id 且不可回滚,只适合演示库,别在有真实数据的环境执行。三条读数对应的月度用量分别是 120、220、380,正好跨越第一档、第二档和第二到第三档,演示阶梯电价的三个区间都覆盖到了,比随便填几个数字有说服力。

第二件是给报表加一个明确的验证方法,而不是只看页面出图了就算完。月度汇总的验证思路很简单:把账单表按账期GROUP BY求和,和你页面上显示的总应收对一遍,两个数一致才算报表逻辑正确。

-- 报表页面的总数应该等于这条 SQL 的结果,对不上就是聚合条件写错了 SELECT DATE_FORMAT(bill_month, '%Y-%m') AS ym, COUNT(*) AS bill_cnt, SUM(receivable_amount) AS total_receivable, SUM(CASE WHEN status = 2 THEN receivable_amount ELSE 0 END) AS paid_amount FROM t_bill GROUP BY ym ORDER BY ym;

SUM(CASE WHEN ...)这种条件聚合是报表里最实用的写法,一次查询就能把应收和实收都算出来,不用跑两条 SQL 再在 Java 里拼。对不上时的排查顺序是:先确认bill_month是不是每行都有值(有 NULL 会导致分组漏数据),再确认金额字段有没有被重复计入(关联查询没加去重条件时很常见)。

第三件是把默认账号和密钥类的东西从代码里挪出去。演示前改一次密码、换个端口,都能体现你对配置管理的理解,成本却只有几分钟。

我自己的教训来自一次现场演示:前一周调试时为了图快,把计费方法里的一档单价临时改成了常量,演示当天老师问“如果第二档电价调整,你需要改几个文件”,我当场翻了三处代码才找齐。从那以后我每次接手这类项目,都强制先走一遍“把可配置项从代码里抽出来”的流程,抽完再谈功能。希望帮到你。

本文还有配套的精品资源,点击获取