ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

Java电费管理系统毕业设计:从技术选型到答辩避坑的完整实现路径

Java电费管理系统毕业设计:从技术选型到答辩避坑的完整实现路径 简介这是一份面向高校计算机专业毕业设计场景的Java电费管理系统完整源码包适合正在准备毕设或需要Java Web实战项目的学生与开发者。系统围绕居民小区、企业电费自动化管理展开涵盖用户管理、电费计算、在线缴费、数据统计与缴费提醒等核心模块采用Spring Boot结合MyBatis、MySQL与Spring Security前端使用HTML5、CSS3与JavaScript构建响应式界面并集成JWT认证、第三方支付与报表导出等实用功能。资源包共158个文件以52个java源码、24个js脚本、19个html页面、14个css样式及10个xml配置为主另含字体、图片与Maven构建文件压缩包约1.78MB目录结构清晰便于按模块阅读与二次开发。目前已有115人学习下载可作为毕设选题参考、课程设计模板或Java全栈练手项目帮助读者快速理解MVC分层设计、数据库建模与权限控制等关键实现思路。1. 电费管理系统毕业设计从能跑起来到能答辩的完整路径很多同学拿到「毕业设计电费管理系统(java).zip」这类题目时第一反应是去搜一套现成源码改改交差。但真正做过一轮的人都知道能跑起来和能答辩通过是两回事——导师问一句「你这个抄表数据怎么保证不重复计费」如果答不上来源码再完整也白搭。电费管理系统本质上是一个典型的 CRUD 加业务规则的信息管理系统核心链路是用户/住户建档 → 电表读数录入 → 阶梯电价计算 → 账单生成 → 缴费状态更新。它涉及 Java 后端、数据库设计、前后端交互、定时任务几个模块适合作为软件工程、计算机科学与技术专业的本科毕业设计选题。这篇文章面向正在做这个题目的同学也面向需要快速搭出一套可演示系统的 Java 初学者把选型、建表、核心算法、接口设计和答辩前必须搞清楚的坑一条一条讲透。2. 技术选型与工程骨架为什么用 Spring Boot MyBatis-Plus 而不是裸 Servlet2.1 选型逻辑毕业设计要的是「可解释的复杂度」很多同学纠结用 JSP Servlet 还是 Spring Boot。我的建议很明确如果你的 Java 基础还停留在「能写类和方法」的阶段用 Spring Boot MyBatis-Plus Thymeleaf或前后端分离的 Vue是性价比最高的方案。原因有三点第一Spring Boot 的自动配置让你不用手写 web.xml 和一堆 XML 映射文件省下来的时间可以花在业务逻辑上第二MyBatis-Plus 提供了代码生成器和通用 CRUD实体类建好之后基础增删改查几乎不用写 SQL第三答辩时你能说清楚「控制层 → 服务层 → 数据层」的分层结构这比裸 Servlet 里一堆 if-else 要有说服力得多。热搜词里出现的「mybatisplus根据java实体类生成创建表的sql语句」其实指向一个很实用的技巧MyBatis-Plus 本身不直接生成 DDL但你可以用它的代码生成器反向拿到实体类再手写建表语句或者用 JPA 的ddl-auto先跑一遍拿到 SQL 再迁移。毕业设计里我一般会直接手写建表 SQL因为这样字段类型、索引、注释都可控答辩时也方便解释。2.2 工程骨架搭建从零到能启动的最小步骤先确认本地 Java 环境。热搜里「java 环境配置」「java环境变量使用多个jdk」是高频问题毕业设计阶段建议统一用 JDK 17Spring Boot 3.x 的最低要求不要在同一台机器上混用 JDK 8 和 17否则java -version和 IDE 里编译用的版本不一致会出现「编译通过但运行报 UnsupportedClassVersionError」的经典翻车。# 确认当前 JDK 版本必须是 17 或以上 java -version # 如果机器上有多个 JDK用 JAVA_HOME 显式指定 export JAVA_HOME/usr/lib/jvm/java-17-openjdk-amd64 export PATH$JAVA_HOME/bin:$PATH上面两行是 Linux/macOS 的写法Windows 下在系统环境变量里改JAVA_HOME指向 JDK 17 的安装目录然后把%JAVA_HOME%\bin放到 Path 最前面。改完之后一定要新开一个终端再执行java -version老终端里的环境变量不会刷新。接下来用 Spring Initializr 或 IDE 自带的 Spring Boot 项目向导建工程依赖勾选Spring Web、MyBatis-Plus如果向导里没有就手动加、MySQL Driver、Lombok。建好之后在application.yml里配数据源spring: datasource: url: jdbc:mysql://localhost:3306/electric_billing?useUnicodetruecharacterEncodingutf8serverTimezoneAsia/Shanghai username: root password: your_password driver-class-name: com.mysql.cj.jdbc.Driver mybatis-plus: configuration: map-underscore-to-camel-case: true global-config: db-config: id-type: autoserverTimezoneAsia/Shanghai这个参数不加插入时间字段时可能差 8 小时账单日期就会错位。map-underscore-to-camel-case让数据库的user_name自动映射到 Java 的userName省掉大量Results注解。id-type: auto表示主键用数据库自增毕业设计里够用不需要上雪花算法。2.3 包结构约定答辩时导师最爱问的分层我一般会按这个结构组织包controller放接口入口service和service.impl放业务逻辑mapper放 MyBatis-Plus 的 Mapper 接口entity放数据库实体dto放前端传参和返回的对象config放全局配置common放统一返回体和异常处理。这个结构不是必须的但它的好处是导师问「你的业务逻辑写在哪」时你能直接指到service.impl下的具体类而不是在一堆工具类里翻。提示不要把业务计算逻辑写在 Controller 里。阶梯电价计算、账单生成这些必须放在 Service 层否则单元测试没法写答辩演示时也没法单独验证计算是否正确。3. 数据库设计与核心表电费系统的四张主表和两个易错字段3.1 表结构设计住户、电表、读数、账单电费管理系统的数据模型可以抽象成四张核心表。住户表存用户基本信息电表表存表具信息和所属住户读数表存每次抄表的度数账单表存按月生成的费用记录。这四张表的关系是一个住户可以有一块或多块电表一块电表有多条读数记录一个住户一个月生成一条账单。-- 住户表 CREATE TABLE t_user ( id BIGINT PRIMARY KEY AUTO_INCREMENT, user_name VARCHAR(50) NOT NULL COMMENT 住户姓名, phone VARCHAR(20) COMMENT 联系电话, address VARCHAR(200) COMMENT 用电地址, create_time DATETIME DEFAULT CURRENT_TIMESTAMP ) ENGINEInnoDB DEFAULT CHARSETutf8mb4 COMMENT住户表; -- 电表表 CREATE TABLE t_meter ( id BIGINT PRIMARY KEY AUTO_INCREMENT, meter_no VARCHAR(30) NOT NULL UNIQUE COMMENT 电表编号, user_id BIGINT NOT NULL COMMENT 所属住户, install_date DATE COMMENT 安装日期, status TINYINT DEFAULT 1 COMMENT 1正常 0停用, INDEX idx_user_id (user_id) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4 COMMENT电表表; -- 抄表读数表 CREATE TABLE t_reading ( id BIGINT PRIMARY KEY AUTO_INCREMENT, meter_id BIGINT NOT NULL COMMENT 电表ID, read_date DATE NOT NULL COMMENT 抄表日期, current_reading DECIMAL(12,2) NOT NULL COMMENT 本次读数, previous_reading DECIMAL(12,2) NOT NULL COMMENT 上次读数, usage_amount DECIMAL(12,2) GENERATED ALWAYS AS (current_reading - previous_reading) STORED COMMENT 用电量, UNIQUE KEY uk_meter_date (meter_id, read_date), INDEX idx_meter_id (meter_id) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4 COMMENT抄表读数表; -- 账单表 CREATE TABLE t_bill ( id BIGINT PRIMARY KEY AUTO_INCREMENT, user_id BIGINT NOT NULL, bill_month VARCHAR(7) NOT NULL COMMENT 账期 yyyy-MM, total_usage DECIMAL(12,2) COMMENT 总用电量, total_amount DECIMAL(12,2) COMMENT 应缴金额, pay_status TINYINT DEFAULT 0 COMMENT 0未缴 1已缴, pay_time DATETIME COMMENT 缴费时间, UNIQUE KEY uk_user_month (user_id, bill_month) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4 COMMENT账单表;这里有两个字段设计值得单独说。第一个是t_reading里的usage_amount我用了 MySQL 的生成列GENERATED ALWAYS AS这样用电量由数据库自动算Java 代码里不用再手动减避免「上次读数填错导致用电量为负」时还要在代码里补判断。第二个是t_bill的uk_user_month唯一索引这是防止同一个月重复生成账单的关键——定时任务如果因为重启跑了两次第二次插入会直接报唯一键冲突而不是悄悄生成两条账单让住户交两次钱。3.2 阶梯电价参数表把规则从代码里抽出来阶梯电价是电费系统的核心业务规则常见做法是分三档第一档每月 0-180 度单价较低第二档 181-400 度单价上浮第三档 400 度以上单价最高。很多同学直接把这三个区间和单价硬编码在 Java 的 if-else 里这是答辩时容易被追问的点——「如果电价调整了你要改代码重新部署吗」。更稳妥的做法是加一张电价配置表CREATE TABLE t_price_config ( id BIGINT PRIMARY KEY AUTO_INCREMENT, tier_no INT NOT NULL COMMENT 档位序号, start_usage DECIMAL(12,2) NOT NULL COMMENT 起始度数, end_usage DECIMAL(12,2) COMMENT 结束度数NULL表示无上限, unit_price DECIMAL(8,4) NOT NULL COMMENT 单价 元/度, effective_date DATE NOT NULL COMMENT 生效日期 ) ENGINEInnoDB DEFAULT CHARSETutf8mb4 COMMENT阶梯电价配置表; INSERT INTO t_price_config (tier_no, start_usage, end_usage, unit_price, effective_date) VALUES (1, 0, 180, 0.5200, 2024-01-01), (2, 180, 400, 0.5700, 2024-01-01), (3, 400, NULL, 0.8200, 2024-01-01);注意end_usage和下一档的start_usage是衔接的计算时用「左闭右开」区间即第一档是[0, 180)第二档是[180, 400)第三档是[400, ∞)。这样不会出现 180 度被算两次的情况。effective_date字段是为了支持电价调整查询时取生效日期小于等于账期的最后一条配置。3.3 索引与约束两个容易被忽略的细节第一个细节是t_reading上的uk_meter_date唯一索引。抄表数据最怕重复录入同一块表同一天录两次用电量就会翻倍。加了唯一索引之后重复插入会抛异常Service 层捕获后提示「该电表今日已抄表」比在代码里先查再插要可靠——并发场景下先查再插仍然可能插重。第二个细节是金额字段用DECIMAL而不是DOUBLE。浮点数做金额计算会出现0.1 0.2 0.30000000000000004这种问题账单金额差几分钱住户投诉起来很难解释。Java 侧对应用BigDecimal并且在做除法时指定保留位数和舍入模式。注意DECIMAL(12,2)表示总共 12 位数字其中 2 位小数整数部分最多 10 位。电费场景下够用但如果你要存很大的工业用电量可以调到DECIMAL(14,2)。4. 阶梯电价计算与账单生成核心算法和定时任务落地4.1 阶梯电价计算BigDecimal 的正确用法阶梯电价的计算逻辑是把总用电量按档位拆开每段乘以对应单价最后求和。比如用了 300 度第一档 180 度按 0.52 算剩下 120 度按 0.57 算。这个逻辑用代码实现时最容易翻车的地方是 BigDecimal 的比较和除法。public BigDecimal calculateFee(BigDecimal totalUsage, ListPriceConfig configs) { BigDecimal total BigDecimal.ZERO; for (PriceConfig config : configs) { BigDecimal start config.getStartUsage(); BigDecimal end config.getEndUsage() null ? totalUsage : config.getEndUsage().min(totalUsage); if (totalUsage.compareTo(start) 0) { break; } // 本档实际用电量 min(总用量, 档位上限) - 档位下限 BigDecimal usageInTier end.subtract(start); if (usageInTier.compareTo(BigDecimal.ZERO) 0) { continue; } total total.add(usageInTier.multiply(config.getUnitPrice())); } // 最终金额保留两位小数四舍五入 return total.setScale(2, RoundingMode.HALF_UP); }这段代码有几个关键点。第一compareTo而不是equals因为BigDecimal的equals会比较精度new BigDecimal(180.0).equals(new BigDecimal(180))返回 false用compareTo才是数值比较。第二end取config.getEndUsage().min(totalUsage)保证最后一档不会超出实际用量。第三setScale(2, RoundingMode.HALF_UP)在最后一步统一做中间过程不截断避免多次舍入累积误差。参数说明totalUsage是本月总用电量从读数表汇总得到configs是按tier_no升序排列的电价配置列表查询时用effective_date 账期过滤。如果配置表里某档的end_usage为 NULL表示无上限代码里用totalUsage兜底。4.2 账单生成定时任务每月 1 号跑什么账单生成一般放在每月 1 号凌晨执行汇总上个月所有电表的读数按住户维度合并调用上面的计算逻辑写入账单表。Spring Boot 里用Scheduled注解就能实现Component public class BillGenerateTask { Autowired private ReadingMapper readingMapper; Autowired private BillService billService; // 每月1号凌晨2点执行 Scheduled(cron 0 0 2 1 * ?) public void generateMonthlyBill() { // 计算上一个账期如当前是2025-03则账期为2025-02 String billMonth YearMonth.now().minusMonths(1).toString(); ListLong userIds readingMapper.selectDistinctUserIdsByMonth(billMonth); for (Long userId : userIds) { try { billService.generateBill(userId, billMonth); } catch (Exception e) { // 单个住户失败不影响其他住户记录日志后续人工处理 log.error(生成账单失败 userId{}, month{}, userId, billMonth, e); } } } }cron 0 0 2 1 * ?的含义是秒 0、分 0、时 2、日 1、月任意、周任意即每月 1 号 02:00 执行。放在凌晨 2 点是为了避开白天演示和调试时间。循环里对每个住户单独 try-catch是因为某个住户数据异常比如读数缺失不应该导致整个月的账单都生成不了。billService.generateBill内部要先查账单表是否已存在该住户该账期的记录存在就跳过这是配合前面唯一索引的双保险。4.3 手动触发与幂等答辩演示时的后悔药定时任务在答辩现场没法等所以必须提供一个手动触发的接口让导师说「你演示一下生成账单」时你能立刻跑。这个接口同时要保证幂等——重复调用不会生成重复账单。PostMapping(/bill/generate) public Result generate(RequestParam String billMonth) { // 先检查该账期是否已生成过 Long count billMapper.selectCount( new LambdaQueryWrapperBill().eq(Bill::getBillMonth, billMonth)); if (count 0) { return Result.fail(该账期已生成账单如需重新生成请先删除); } billService.batchGenerate(billMonth); return Result.success(生成完成); }这里用selectCount做前置检查配合数据库唯一索引双重保证不会重复。如果导师要求「重新生成」就提供一个删除接口按账期删掉再跑而不是让生成逻辑去覆盖——覆盖逻辑写不好容易把已缴费状态也冲掉。提示演示前提前准备好两三个月的抄表数据否则账单生成出来全是 0 度演示效果很差。数据可以用 SQL 脚本批量插入注意读数要递增。5. 避坑与排查电费系统开发中最容易翻车的五个点5.1 抄表读数倒挂导致用电量为负现象账单金额出现负数或者用电量字段是负值。原因录入本次读数时填得比上次读数小生成列current_reading - previous_reading直接算出负数。解决在 Service 层插入读数前先查该电表最近一条读数校验current_reading previous_reading不满足就抛业务异常提示「本次读数不能小于上次读数」。数据库层面也可以加 CHECK 约束但 MySQL 8.0.16 之前不支持 CHECK所以代码校验更通用。5.2 定时任务重复执行生成双份账单现象同一住户同一账期出现两条账单。原因服务重启或集群部署时定时任务被触发多次而生成逻辑没有做幂等。解决账单表加uk_user_month唯一索引生成前先查是否已存在插入时捕获DuplicateKeyException并跳过。如果部署了多个实例需要引入分布式锁或把定时任务单独部署毕业设计阶段单机部署加唯一索引就够了。5.3 时间字段差 8 小时导致账期错位现象3 月 1 号凌晨生成的账单账期算成了 3 月而不是 2 月。原因数据库连接串没配serverTimezone或者 JVM 时区是 UTCYearMonth.now()拿到的日期比北京时间晚 8 小时。解决连接串加serverTimezoneAsia/Shanghai启动参数加-Duser.timezoneAsia/Shanghai两处都配才保险。5.4 BigDecimal 除法抛 ArithmeticException现象计算电费时抛Non-terminating decimal expansion异常。原因BigDecimal.divide()不指定精度时遇到除不尽的情况比如 1/3会直接抛异常。解决所有除法都指定精度和舍入模式写成a.divide(b, 2, RoundingMode.HALF_UP)。电费计算里除法用得少但均摊费用、计算平均电价时会遇到。5.5 前端传参日期格式不匹配导致 400现象前端提交抄表日期后端返回 400 Bad Request。原因前端传的是2025-03-01字符串后端实体类字段是LocalDateSpring 默认的日期转换器对某些格式不认。解决在 DTO 字段上加JsonFormat(pattern yyyy-MM-dd)和DateTimeFormat(pattern yyyy-MM-dd)前者管 JSON 反序列化后者管表单参数绑定两个都加才覆盖所有场景。6. 答辩前必须能讲清的三个技术点和一套自测方法走到这里系统基本能跑通了。但毕业设计和课程作业的区别在于你得能讲清楚为什么这么做。我见过太多同学系统跑得挺顺导师一问「你这个阶梯电价如果政策调整了怎么办」就卡住。下面三个点是我建议每个做电费管理系统的同学都提前准备好的。第一个点是阶梯电价的可配置化。你要能说清楚电价规则存在t_price_config表里按effective_date支持多版本计算时取账期对应的生效版本。这样政策调整只需要插一条新配置不用改代码重新部署。如果导师追问「跨档位怎么算」你就把 4.1 里那段 BigDecimal 循环逻辑讲一遍重点说「左闭右开」和「最后统一舍入」。第二个点是账单生成的幂等性。你要能说清楚唯一索引uk_user_month是数据库层的兜底Service 层的前置查询是应用层的拦截两层配合保证重复执行不会产生重复账单。如果导师问「集群部署怎么办」你就说毕业设计是单机部署生产环境需要引入分布式锁或把定时任务独立部署这个边界要主动交代不要硬撑。第三个点是金额计算的精度处理。你要能说清楚数据库用DECIMALJava 用BigDecimal除法指定精度最后一步setScale(2, HALF_UP)。如果导师问「为什么不用 double」你就举0.1 0.2的例子这是最直观的。自测方法我一般用一套「三查一跑」查读数是否有倒挂SQL 查current_reading previous_reading的记录、查账单是否有重复按user_id bill_month分组 count 大于 1、查金额是否有负数total_amount 0然后手动触发一次账单生成看日志有没有异常、数据库记录数是否符合预期。这套查完基本能覆盖 90% 的演示翻车场景。最后说个我自己的习惯答辩前一天把数据库清空从建表脚本开始完整跑一遍包括插入测试数据、生成账单、模拟缴费。这一步能暴露很多「开发时数据一直在所以没发现」的问题比如建表脚本漏了某个字段、初始化数据没准备。我吃过这个亏凌晨两点发现演示数据没了只能临时手写 SQL 补。希望帮到你。本文还有配套的精品资源点击获取
返回列表