简介:一份基于Java的电费管理系统毕业设计源码包,面向高校计算机专业毕业生及Java后端学习者,常用于毕业设计、课程项目或综合实践参考。压缩包共158个文件、约1.78MB,以52个Java源文件、24个JavaScript脚本、19个HTML页面、14个CSS样式及10个XML配置为主,涵盖Spring Boot后端、MyBatis持久层、前端页面与数据库脚本等完整代码。系统实现了用户认证与权限控制、阶梯/峰谷电费计算、在线缴费(第三方支付SDK)、月度报表统计、逾期提醒等核心模块,分层目录对应MVC架构,便于快速运行和二次开发。资源内还包含计费算法实现、RESTful API接口以及Apache POI导出Excel等细节,可直接对照学习。目前已有115人学习浏览,对需要完整毕设项目参考的同学具有一定价值。
1. 电费管理系统要解决的核心问题:从手工抄表到线上缴费的迁移
做毕业设计选管理系统,很容易撞上“管理员维护一堆数据”这种类型。电费管理系统乍看就是一个 CRUD,但真动手时,缴费金额该用哪种数据类型、阶梯电价怎么和数据库表联动、抄表记录怎么对账,每个点都够让人返工一次。这套完整方案基于 Java 技术栈,采用 Spring Boot + MyBatis + MySQL 实现,覆盖电表管理、抄表录入、阶梯计费、账单生成、在线缴费、欠费提醒和统计报表的完整闭环。它的价值不在于代码量多,而在于把一条容易做散的业务线串成了标准状态流。适合正在选题或者已经拿到类似题目的学生参考,也适合想快速搭一个内部电费管理工具给物业或宿管使用的从业者,照着改就能落地。
2. 总体架构设计:Spring Boot 与 MyBatis 的分层结构、七张核心表
2.1 技术选型:为什么毕业设计优先选 Spring Boot 而不是 SSH 或 SSM
我经手过不少管理系统项目,负责任地说,Spring Boot 是最稳妥的选择。理由很简单:它把 Spring 的配置复杂度收掉了,一个java -jar就能启动,数据库连接、事务管理、定时任务这类高频能力都有起步依赖,不用自己拼配置。相比早年 SSM(Spring + Spring MVC + MyBatis)时代要手写大量 XML,Spring Boot 的“约定优于配置”对课程设计和毕业设计都友好得多,调试成本低,遇到问题也容易在社区里搜到答案。
持久层用 MyBatis 而不是 Spring Data JPA,是我比较坚持的一点。电费管理有大量按月份、按用户状态进行统计的查询,SQL 直接写在 Mapper XML 里,复杂报表的调优路径是清晰的;而 JPA 的自带方法在联表统计时会让你去猜底层 SQL,像个黑匣子。前端我不推荐拆成前后端分离,当前资源采用服务端模板加原生 JS 的方式,后台管理系统页面数量不多,用渲染模板更快,答辩时导师对后端渲染与请求流程的理解成本也更低。权限方案走拦截器或安全框架二选一,前者实现简单,后者需要配置过滤器链,按毕设难度选拦截器就够交差。
2.2 项目目录结构:从 Controller 到 Mapper 的四层职责
一套合格的 Java 毕设源码,目录结构和业务分层是导师最先翻看的部分。当前项目压缩包内源码按标准 Spring Boot 结构整理,我建议你拿到后先按下面的骨架对照,确认分层没乱:
src/main/java/com/university/elecbill/ ├── controller/ # 接口层,接收请求、返回视图或 JSON │ ├── UserController.java │ ├── MeterController.java │ ├── BillController.java │ └── PaymentController.java ├── service/ # 业务层,接口与实现分离 │ ├── BillService.java │ └── impl/BillServiceImpl.java ├── mapper/ # MyBatis Mapper 接口,与 XML 一一对应 │ ├── UserMapper.java │ ├── MeterMapper.java │ └── BillMapper.java ├── entity/ # 数据表实体,字段与数据库列一一映射 │ ├── User.java │ ├── Meter.java │ ├── MeterRecord.java │ └── Bill.java ├── dto/ # 界面与接口的传输对象,避免直接暴露实体 │ ├── BillQueryDTO.java │ └── StatisticResult.java ├── config/ # Web 拦截器、定时任务配置 │ ├── WebConfig.java │ └── ScheduleConfig.java └── common/ # 统一返回结果、异常处理、工具类 ├── Result.java └── BusinessException.javaController 层只负责参数接收和调用 Service,不写业务逻辑;Service 层处理状态流转与金额计算;Mapper 层只做数据读写。这条约定看着简单,但很多毕设翻车就翻在把数据校验、金额计算、SQL 拼接全堆在 Controller 里,一旦导师追问某个字段流转就会答得语无伦次。
除了源码,毕业设计资源包内还应该包含开题报告、毕业论文和答辩 PPT。论文里的用例图、ER 图和时序图要与上述分层对应,比如用例图里的“抄表录入”对应 MeterController + MeterRecord 表,时序图要画出抄表到生成账单的整个过程。这套资源包里这几项是一起见到的,正好省去自己东拼西凑的时间。
2.3 数据库设计:七张核心表的字段与关联关系
电费管理系统的数据模型比一般管理系统多了一层“抄表—账单—缴费”的流转关系。当前方案用七张表把这条链路撑起来,核心表设计如下:
| 表名 | 关键字段 | 说明 |
|---|---|---|
| user | id, username, password, role, phone, status | 用户表,role 区分管理员与普通用户 |
| meter | id, meter_no, user_id, address, install_date | 电表表,meter_no 加唯一约束 |
| meter_record | id, meter_id, year, month, current_reading, pre_reading, delta | 抄表记录,一张电表每月一条 |
| tariff_config | id, config_key, config_value, remark | 电价配置表,存放阶梯档位与单价 |
| bill | id, record_id, user_id, total_amount, base_amount, tier_amount, status | 账单表,status 区分待缴/已缴/作废 |
| payment | id, bill_id, user_id, pay_amount, pay_time, pay_type | 缴费记录表,记录每次实际缴费 |
| operation_log | id, operator_id, action, target_id, create_time | 操作日志,用于后台追溯 |
设计上需要注意三个点。一是meter_record表建议对meter_id + year + month加联合唯一索引,确保同一张电表一个月只产生一条抄表记录,业务上也就天然拦截了重复抄表;二是bill表和meter_record是一对一的关系,账单生成后不可随意修改,如果抄表数据错误,应通过“作废旧账单并重新生成”的方式处理,而不是直接 UPDATE 金额字段;三是电费单价不应写死在代码里,而要放到tariff_config表,后面章节会专门讲为什么这么做。
这七张表之间,user与meter是一对多,meter与meter_record是一对多,meter_record与bill是一对一,bill与payment是一对多。ER 图画出来是一个四级嵌套结构,答辩时顺着这条线讲,导师能很快 get 到你的主设计思路。
3. 核心模块实现:阶梯电价计算、批量账单生成与缴费状态流转
3.1 阶梯电价计算:把档位规则配置化而不是写死在代码里
电价计算是整套系统最容易出彩、也最容易翻车的模块。常见的场景是:月用电量在 200 度以内按基础电价,201 到 400 度部分按第二档电价,超过 400 度部分按第三档电价。很多人一上来就在 Service 里写下if (delta <= 200) ... else if这类硬编码,问题在于电价如果调整,改代码重打包再部署,演示时恰好被发现价格变了,场面会很尴尬。
我的做法是从业务上就把“计算规则”和“业务逻辑”拆开。电价档位存放在tariff_config表中,计算时取出配置,再按统一的方法计算:
public BigDecimal calculateAmount(int delta, TariffConfig config) { // 第一档:0 <= delta <= firstLimit,按基础单价 if (delta <= config.getFirstLimit()) { return config.getBasePrice() .multiply(BigDecimal.valueOf(delta)); } // 第二档:firstLimit < delta <= secondLimit if (delta <= config.getSecondLimit()) { BigDecimal firstPart = config.getBasePrice() .multiply(BigDecimal.valueOf(config.getFirstLimit())); BigDecimal secondPart = config.getSecondPrice() .multiply(BigDecimal.valueOf(delta - config.getFirstLimit())); return firstPart.add(secondPart); } // 第三档:delta > secondLimit,分三段累加 BigDecimal firstPart = config.getBasePrice() .multiply(BigDecimal.valueOf(config.getFirstLimit())); BigDecimal secondPart = config.getSecondPrice() .multiply(BigDecimal.valueOf(config.getSecondLimit() - config.getFirstLimit())); BigDecimal thirdPart = config.getThirdPrice() .multiply(BigDecimal.valueOf(delta - config.getSecondLimit())); return firstPart.add(secondPart).add(thirdPart); }这里有几个设计点值得说。第一,delta(本月用电量)的来源是抄表模块,由“本月止码 - 上月止码”计算得来,所以这个方法只接收整数增量,内部不查库,职责单一。第二,金额计算全程使用BigDecimal,用BigDecimal.valueOf(delta)而不是new BigDecimal(delta),避免浮点构造精度丢失。第三,如果将来要支持峰谷电价,不需要改动这个方法,只需在配置表里增加峰时段单价字段,再在 Service 层多传一个时段参数即可,扩展路径是清晰的。
答辩时导师很可能问“阶梯电价的边界值你是如何处理的”,这个问题在计算逻辑里其实已经给出答案:第一档的判断条件是delta <= firstLimit,第二档的进入条件是delta > firstLimit && delta <= secondLimit,也就是档位区间是左闭右闭且不重不漏。在毕业论文里最好用一张表明确写出每档的用电量区间和对应单价,比纯文字描述直观得多。
3.2 抄表与账单生成:月度批处理任务的幂等设计
抄表模块的输入是抄表员录入的本月止码,输出是一条meter_record记录和一张bill账单。为了防止重复生成账单,我用状态标志加数据库唯一索引双重保险。具体流程是:抄表时先读取pre_reading(上月止码),计算delta后插入抄表记录;账单生成任务扫描当月已有抄表记录、但没有账单的meter_record,逐个计算金额并创建账单。
为了让抄表和账单两个动作解耦,账单生成通过定时任务批量触发,而不是在抄表时直接生成。定时任务用@Scheduled注解即可,每天凌晨执行一次:
@Scheduled(cron = "0 30 1 * * ?") public void generateMonthlyBill() { // 只处理上个月已经抄表、且尚未生成账单的记录 List<MeterRecord> pending = meterRecordMapper.findPendingBills(); for (MeterRecord record : pending) { // 幂等检查:账单表存在 record_id 即跳过 if (billMapper.existsByRecordId(record.getId())) { continue; } Bill bill = new Bill(); bill.setRecordId(record.getId()); bill.setUserId(record.getUserId()); bill.setTotalAmount(billingService.calculateAmount(record.getDelta(), config)); bill.setStatus(BillStatus.PENDING); billMapper.insert(bill); } }这段代码的关键在幂等检查。定时任务每天跑一次,如果前一天已经生成过账单但状态没更新,防止重复生成就会出错。当前实现通过billMapper.existsByRecordId(record.getId())先查再插,配合数据库层面对record_id加唯一约束,双保险下来基本不会出现重复账单。这是我在这个项目里最建议你保留的设计,因为很多同学的方案里只有一个简单的for循环生成,一旦任务被调用两次,系统里就会出现一户多账,而且很难凭肉眼在演示时发现。
注意,定时任务的 cron 表达式不要写得过于密集。电费账单按月生成,每天凌晨跑一次足够,写得太频繁反而容易在高峰期碰到数据库连接池不够用的问题。同时,generateMonthlyBill内部要加try-catch,单条记录失败不能中断整个批次,失败记录写入日志表,方便后续人工排查。
3.3 缴费与欠费提醒:账单状态机的流转与控制
账单从生成到销账,状态是单向流转的:待缴费(PENDING)→ 已缴费(PAID),异常情况下待缴费可以变为 已作废(CANCELLED)。缴费操作是整个系统里唯一修改账单金额的入口,所以这个模块的状态流转必须设计严密。
我习惯在 Service 层里用状态机的方式控制,而不是随意写if (bill.getStatus() == 1)这种魔法值。核心逻辑是这样的:缴费请求进来后,先校验账单状态必须是 PENDING,再创建支付记录、更新账单状态为 PAID,两个动作放进同一个事务。如果状态不对就抛业务异常,前端拿到错误提示后重新刷新账单列表。这样一来,支付记录和账单状态永远保持一致,不会出现“钱扣了状态还没改”的中间态。
欠费提醒模块不需要实时推送,用定时扫描就好。电费系统的欠费提醒粒度是月,松耦合完全够用。扫描逻辑是:找出状态为 PENDING 且账单日期距今超过缴费期限的账单,再按用户维度聚合成提醒列表,通过站内信或者短信发送。这一块做成接口而不是直接用定时任务发短信,原因是答辩时导师通常会问“如果短信服务挂了怎么办”,只要回答“提醒任务只把待提醒数据写入消息表,由第三方接口异步消费,失败可重推”,就能体现你对异常场景的思考。
在页面功能上,前台用户能看到“我的账单”和“待缴金额”,后台管理员能看到“全部账单”和“缴费明细”。这里权限控制要特别注意:普通用户请求账单列表时,Service 层必须强制把查询条件加上当前登录用户的 ID,不能只靠前端传入参数,否则改一下 URL 就能看别人的账单,这是后面避坑章节会重点展开的问题。
3.4 统计报表:用 SQL 聚合而不是内存拼接
统计报表是电费系统里让代码量瞬间膨胀的地方。按月统计应收总额、实收总额、欠费总额,按电表统计用电量排行,这些需求如果都靠 Java 循环统计,几十户时没问题,几百户时性能就开始拉胯。正确的做法是让数据库做好聚合,后端只组装展示结构。
拿“应收、实收、欠费”这张核心报表举例:
SELECT DATE_FORMAT(b.create_time, '%Y-%m') AS bill_month, SUM(b.total_amount) AS total_bill, SUM(CASE WHEN p.id IS NOT NULL THEN p.pay_amount ELSE 0 END) AS total_paid FROM bill b LEFT JOIN payment p ON p.bill_id = b.id WHERE b.create_time BETWEEN #{start} AND #{end} GROUP BY bill_month ORDER BY bill_month DESC;这里用LEFT JOIN而不是INNER JOIN是故意的:未缴费的账单在payment表里没有对应记录,如果INNER JOIN,这些账单会被直接过滤掉,欠费金额就永远统计不出来。CASE WHEN做条件聚合,把已缴金额和账单总额放在同一行返回,后端拿到的结果集直接转成图表数据,不用二次遍历拼装。
对应到项目资源里的报表页面,前端用表格分页展示数据列表,再用简单的柱状图展示近六个月的用电趋势。如果论文里需要写“系统的可扩展性”,这里就是个很好的切入点:目前是月度报表,将来如果要支持按年汇总,只需把DATE_FORMAT的粒度改成'%Y'再调GROUP BY就行,查询结构不用动。
4. 最容易翻车的五个细节:金额精度、阶梯边界、任务重复与权限绕过
4.1 金额精度问题:数据库字段是 DECIMAL,代码里却用 double
现象:电费账单金额显示成 98.9999999997,或者缴费金额和计算金额差了 0.01,怎么对都对不上。
原因:Java 代码里用了double或者Float存储金额。浮点数在二进制下无法精确表示 0.1,多次计算误差累计,最终会体现在账单上。数据库层面如果用的是DOUBLE类型,存储时也已经发生了精度损耗。
解决:代码层统一用BigDecimal,并且用BigDecimal.valueOf(double)或BigDecimal(String)初始化;数据库字段类型用DECIMAL(10, 2),由数据库保证两位小数存储;MyBatis映射时确认resultMap里金额字段的jdbcType为DECIMAL。我一般还会在查询金额汇总的 SQL 里用ROUND(SUM(amount), 2)做一道兜底。
4.2 阶梯电价边界:300 度整的电费比 301 度还高,用户投诉不断
现象:用户本月用电量恰好等于第二档上限时,账单金额反而比相邻月份多出几十块,明显异常。
原因:计算方法的区间判断条件写错,比如写成了delta < secondLimit而不是delta <= secondLimit,导致整百的边界被推到下一档;或者档位区间重叠,有一段电量被重复计费。
解决:先明确业务规则,通常是“第一档 = 第一档电量 × 第一档单价,第二档 = 第二档电量 × 第二档单价,以此类推”,边界值归入低收入区间。然后把重点放在测试上:写几个边界测试用例,分别测 200、201、400、401 度,断言金额和阶梯系数正确,再测一次整百临界值。无论是用 JUnit 写单元测试还是在论文附录里放测试表,都比在页面上肉眼验证可靠得多。
4.3 定时任务重复执行:深夜系统自动生成双份账单
现象:某天早上用户查账单,发现同一个月出现了两张相同金额的账单记录。
原因:定时任务被触发两次,第一次执行时账单已生成,第二次执行时没有做幂等检查;或者应用部署在多个实例上,每个实例的@Scheduled都在跑同一批任务。这是我在多个项目里看到的通病,尤其是把服务同时部署在开发和测试两套环境时最明显。
解决:参考第 3 章的做法,在生成账单的任务开头检查meter_record是否已有对应bill,有则跳过;数据库层对bill.record_id加唯一索引,这是最后一道防线。多实例部署场景可以用数据库乐观锁记录任务执行状态:任务表里存last_exec_time,每次执行前用UPDATE ... WHERE last_exec_time = ?抢占,更新失败则说明其他实例已在执行。
4.4 权限绕过:普通用户直接改 URL 就能访问管理后台
现象:普通用户登录后,在浏览器地址栏手动输入/admin/bill/list,直接看到了全系统所有用户的账单列表和缴费情况。
原因:页面菜单做了权限控制,但后端接口没有拦截。只要接口存在,绕过前端 UI 直接构造 URL 请求就能打到 Controller,这在只做前端隐藏的毕设里非常常见。
解决:在拦截器里校验当前登录用户的角色,后台路径统一以/admin/**开头,拦截器对所有该前缀的请求检查session.getAttribute("role"),非管理员直接返回 403。同时数据级别校验不能省:列表查询的 Service 方法里,如果是普通用户,强制用SecurityUtils.getUserId()覆盖查询条件,这个细节能让你在设计说明里多写两页“安全性设计”。
4.5 数据库乱码与时间差八小时:配置清单里最容易忽略的两行
现象:插入中文用户名后页面显示乱码,账单生成时间比实际时间差了 8 个小时。
原因:JDBC 连接串没有指定characterEncoding和serverTimezone,应用服务器与数据库服务器的时区不一致。MySQL 8 默认时区与本地时区不同就会导致时间偏移,乱码则基本是连接串缺了编码参数。
解决:application.yml里把这两项写全,并且保持统一。
spring: datasource: url: jdbc:mysql://localhost:3306/elec_bill?useUnicode=true&characterEncoding=utf8&serverTimezone=Asia/Shanghai&useSSL=false username: root password: your_password jackson: date-format: yyyy-MM-dd HH:mm:ss time-zone: Asia/Shanghai注意characterEncoding=utf8是必需项,缺了它在本地环境可能没反应,但换一台数据库版本不同的机器就会复现乱码。答辩演示时建议把这份配置原样保留,不要在演示机上临时改。
5. 验收与答辩:五分钟启动项目,十五分钟讲透设计
5.1 拿到项目后按这个顺序跑通,再谈优化
对着这份资源做验收,我的建议是不要先看代码,先把环境准备好,按“建库 → 改配置 → 启动 → 跑流程”的顺序走一遍。步骤如下:
# 1. 初始化数据库 mysql -uroot -p < sql/elec_bill.sql # 2. 修改 application.yml 中的数据库账号密码 # 3. 启动服务 mvn spring-boot:run启动成功后,用管理员账号登录后台,依次执行这个验收序列:新增一个用户 → 给用户绑定电表 → 为该电表录入本月抄表止码 → 查看系统自动生成的账单 → 对账单发起缴费 → 查看统计报表里的当月应收、实收数据 → 退出管理员账号,用普通用户登录,确认只能看到自己的账单。这套流程走下来没有问题,说明核心链路是通的。
答辩前的演示要控制节奏,不要上来就点菜单。先讲业务痛点(手工抄表、电费计算易错、催缴繁琐),再讲你如何用状态流解决,最后再演示。演示步骤不需要多,5 步以内最好,每一步对应一个模块,比如新建电表对应电表管理、录入抄表对应抄表模块、缴费跳转对应支付流程。这样 5 分钟讲完,逻辑非常紧凑。
5.2 答辩最常见的四类追问与应对要点
| 提问方向 | 典型问法 | 回答要点 |
|---|---|---|
| 技术选型 | 为什么不用 Vue 做前后端分离? | 管理系统页面密集、交互简单,服务端模板开发效率更高;资源包定位是课程设计而非企业级项目,降低架构复杂度不等于能力不足 |
| 业务规则 | 电价调整了怎么办? | 电价档位和单价都存数据库配置表,计算时读取,不修改代码;演示时可以主动打开配置表给导师看 |
| 数据安全 | 用户怎么防止越权查看账单? | 拦截器加后端强制用户 ID 过滤,不是只靠前端隐藏按钮;普通用户查询账单时,Service 层使用会话中的用户 ID 做条件拼接 |
| 可靠性 | 定时任务重复执行会重复生成账单吗? | bill 表对 record_id 加唯一索引,配合任务内幂等判断,后插入的重复记录会被数据库拒绝 |
这四类问题覆盖了毕设答辩 80% 的追问点。回答时要先说结果,再讲一句关键实现。比如“金额用什么类型”直接说“BigDecimal,数据库配 DECIMAL(10,2),避免浮点误差”,不用展开太多细节,导师感兴趣自然会追问。
5.3 我踩过的坑和现在固定遵守的验收习惯
说句老实话,我做类似管理系统时也吃过亏。有一回演示前一天,我把某个月份的电费算出来,页面显示金额怎么都对不上电表读数,最后发现是抄表记录里把上月止码当成当月止码录进去了,而代码里对delta < 0的情况直接取了绝对值。这种诡异结果很难靠肉眼发现,只能靠构造异常数据来测。自那以后,我每次做管理系统都会强制走一遍这条验收清单:数据精度检查、边界值用例检查、权限越权检查、定时任务幂等检查。清单不用太长,能覆盖最容易出问题的四个场景就行。这四条走完,项目里大部分隐性坑都能提前暴露。希望这套方案的思路能帮到你,让你在交毕设之前就避开我当时返工熬夜的弯路。
本文还有配套的精品资源,点击获取