简介:这份资源是面向高校计算机相关专业学生与Java初学者的一套毕业设计完整项目源码,主题为基于Java的电费管理系统,可用于课程设计、毕业设计参考或自学练手。压缩包共158个文件,整体约1.78MB,以52个java源文件为核心,配合24个js、19个html、14个css构建前端页面,另有xml配置、properties参数文件、字体图标资源及Maven构建脚本等,结构完整、便于二次开发。系统围绕用户管理、电费计算、缴费管理、数据统计与缴费提醒等模块展开,采用Spring Boot结合MyBatis与MySQL实现后端,前端使用Bootstrap等框架搭建响应式界面,并集成Spring Security与JWT完成认证授权,涵盖阶梯电价、峰谷电价等计费逻辑,以及报表导出与短信提醒等实用功能。目前已有117人学习下载,适合需要完整项目案例、想理解MVC分层设计与前后端协作流程的读者参考借鉴。1. 从一份 Java 毕设资源说起:电费管理系统到底能跑出什么如果你正在找一份能直接跑起来的 Java 毕业设计,又不想碰那种“只有登录页和一张静态表格”的演示壳子,那这份电费管理系统值得花时间拆一拆。它解决的是一个很具体的场景:小区、园区或宿舍楼的电表抄表、费用计算、缴费记录和欠费提醒,全部塞进一个 Java Web 项目里。适合谁?正在做毕设但缺一个完整业务闭环的计算机专业学生,以及想拿一个中小型管理系统练手 CRUD、权限和报表逻辑的初级开发者。我见过太多毕设项目把“管理”两个字做成了摆设,但这个系统至少把用户、电表、费率、账单、缴费这几条线串起来了,跑通之后你能清楚看到一个费用从产生到结清的全过程。2. 技术栈与目录结构:先看清骨架再动手2.1 技术选型:为什么是这套组合从项目标题里的.zip和java能判断,这是一个典型的 Java Web 工程。结合这类毕设项目的常见做法,技术栈大概率落在SSM(Spring + Spring MVC + MyBatis)或Spring Boot + MyBatis上。我拿到源码后第一件事就是看pom.xml或lib目录,确认是 Maven 管理还是传统 Web 项目。如果是 Maven 项目,pom.xml里会集中声明依赖。常见配置如下:<!-- pom.xml 核心依赖片段 --> <dependencies> <!-- Spring 核心:IoC 容器和事务管理 --> <dependency> <groupId>org.springframework</groupId> <artifactId>spring-context</artifactId> <version>5.3.x</version> </dependency> <!-- Spring MVC:处理 Web 请求和视图解析 --> <dependency> <groupId>org.springframework</groupId> <artifactId>spring-webmvc</artifactId> <version>5.3.x</version> </dependency> <!-- MyBatis:SQL 映射与数据库操作 --> <dependency> <groupId>org.mybatis</groupId> <artifactId>mybatis</artifactId> <version>3.5.x</version> </dependency> <!-- MySQL 驱动 --> <dependency> <groupId>mysql</groupId> <artifactId>mysql-connector-java</artifactId> <version>8.0.x</version> </dependency> <!-- JSP 与 JSTL:前端页面渲染 --> <dependency> <groupId>javax.servlet</groupId> <artifactId>jstl</artifactId> <version>1.2</version> </dependency> </dependencies>这段配置说明项目走的是传统三层架构:Controller 接请求,Service 写业务,DAO 层用 MyBatis 操作数据库。参数上要注意 Spring 和 MyBatis 的版本兼容,5.3.x 配 3.5.x 是稳妥组合,MySQL 驱动 8.0 以上要改 JDBC URL 里的时区和 SSL 参数。如果项目是 Spring Boot 版本,那pom.xml里会看到spring-boot-starter-web和mybatis-spring-boot-starter,启动类上会有@SpringBootApplication。两种形态的改造思路不同,后面章节会分开说。2.2 目录结构:每个包在干什么解压后典型的目录长这样:electricity-fee-system/ ├── src/ │ ├── main/ │ │ ├── java/ │ │ │ └── com/example/efee/ │ │ │ ├── controller/ # 接收前端请求 │ │ │ ├── service/ # 业务逻辑 │ │ │ │ └── impl/ │ │ │ ├── mapper/ # MyBatis 接口 │ │ │ ├── entity/ # 数据库实体类 │ │ │ ├── vo/ # 前端展示对象 │ │ │ └── util/ # 工具类 │ │ ├── resources/ │ │ │ ├── mapper/ # MyBatis XML 映射文件 │ │ │ ├── application.yml # 或 spring.xml │ │ │ └── db.properties # 数据库连接配置 │ │ └── webapp/ │ │ ├── WEB-INF/ │ │ │ ├── web.xml # 传统 Web 项目入口 │ │ │ └── views/ # JSP 页面 │ │ └── static/ # CSS、JS、图片 │ └── test/ # 测试代码 ├── sql/ │ └── electricity_fee.sql # 建表与初始数据 └── pom.xmlcontroller包负责把 HTTP 请求参数转成 Java 对象,service包写费用计算和状态流转,mapper包是 MyBatis 接口,对应的 SQL 写在resources/mapper下的 XML 里。entity和数据库表一一对应,vo是给前端页面用的组合对象,比如账单列表里要同时显示用户名和电表编号,就会用一个BillVO来装。提示:拿到源码先别急着改代码,把sql目录下的脚本在 MySQL 里执行一遍,确认表结构和初始数据都进去了,后面排查问题时能少一半玄学。2.3 数据库表设计:费用流转的数据底座电费管理系统的核心表通常有这几张:表名作用关键字段user系统用户(管理员/住户)id, username, password, role, room_idroom房间或住户信息id, room_no, owner_name, phonemeter电表信息id, meter_no, room_id, install_datereading抄表记录id, meter_id, read_date, current_reading, last_readingfee_rate费率配置id, rate_name, price_per_kwh, effective_datebill账单id, room_id, period, usage_kwh, amount, statuspayment缴费记录id, bill_id, pay_date, pay_amount, operatorreading表里存的是本次抄表读数和上次读数,差值就是用电量。bill表根据用电量和费率算出金额,status字段标记“未缴/已缴/逾期”。payment表记录每一笔缴费,和bill是一对多关系,支持分次缴费的场景。这套表结构不算复杂,但覆盖了从抄表到出账再到缴费的完整链路。二次开发时如果想加“阶梯电价”,只需要在fee_rate和计算逻辑里动刀,不用推翻整个设计。3. 核心模块拆解:抄表、算费、出账的代码链路3.1 抄表录入:从表单到数据库抄表是费用计算的起点。前端页面提交电表编号和本次读数,后端要做的第一件事是查出上次读数,算出用电量,然后存一条reading记录。// ReadingController.java 抄表录入接口 @PostMapping("/reading/add") public String addReading(@RequestParam("meterNo") String meterNo, @RequestParam("currentReading") BigDecimal currentReading, Model model) { // 1. 根据电表编号查最近一次抄表记录 Reading lastReading = readingService.getLastReadingByMeterNo(meterNo); if (lastReading == null) { model.addAttribute("msg", "该电表暂无历史抄表记录,请先初始化"); return "reading/add"; } // 2. 校验本次读数不能小于上次读数 if (currentReading.compareTo(lastReading.getCurrentReading()) < 0) { model.addAttribute("msg", "本次读数不能小于上次读数"); return "reading/add"; } // 3. 组装新记录并保存 Reading reading = new Reading(); reading.setMeterNo(meterNo); reading.setLastReading(lastReading.getCurrentReading()); reading.setCurrentReading(currentReading); reading.setReadDate(new Date()); readingService.saveReading(reading); return "redirect:/reading/list"; }这段代码的逻辑很直白:先查历史,再校验,最后落库。参数上currentReading用BigDecimal而不是double,是因为电费计算涉及金额,浮点数会有精度问题,这是很多毕设项目容易翻车的地方。getLastReadingByMeterNo在 Service 层会调用 Mapper 的selectLastByMeterNo,SQL 里用ORDER BY read_date DESC LIMIT 1取最近一条。如果项目是 Spring Boot 版本,@PostMapping和@RequestParam的用法一样,只是不需要在web.xml里配 DispatcherServlet,启动类会自动扫描 Controller。3.2 费用计算:费率匹配与金额生成抄表记录有了,下一步是出账。出账的核心是根据用电量和当前生效的费率算出金额。// BillService.java 出账逻辑 public Bill generateBill(Long roomId, String period) { // 1. 查该房间在指定周期的抄表记录 Reading reading = readingMapper.selectByRoomAndPeriod(roomId, period); if (reading == null) { throw new BusinessException("该周期无抄表记录,无法出账"); } // 2. 计算用电量 = 本次读数 - 上次读数 BigDecimal usage = reading.getCurrentReading() .subtract(reading.getLastReading()); // 3. 查当前生效的费率 FeeRate rate = feeRateMapper.selectCurrentRate(); if (rate == null) { throw new BusinessException("未配置费率,请先在费率管理中设置"); } // 4. 金额 = 用电量 × 单价,保留两位小数 BigDecimal amount = usage.multiply(rate.getPricePerKwh()) .setScale(2, RoundingMode.HALF_UP); // 5. 组装账单对象 Bill bill = new Bill(); bill.setRoomId(roomId); bill.setPeriod(period); bill.setUsageKwh(usage); bill.setAmount(amount); bill.setStatus("UNPAID"); // 初始状态:未缴 billMapper.insert(bill); return bill; }这里有几个关键点。第一,用电量用subtract而不是直接减,保证BigDecimal的精度。第二,setScale(2, RoundingMode.HALF_UP)是金额保留两位小数的标准做法,四舍五入模式要显式指定,否则默认的RoundingMode在某些场景下会抛异常。第三,费率查询用selectCurrentRate,SQL 里按effective_date倒序取第一条,支持费率调整后新账单自动用新价格。period参数一般是"2025-01"这种格式,对应抄表记录的月份。如果项目里没有period字段,可能是用read_date的范围来筛选,改造时要注意统一。3.3 缴费与状态流转:账单从“未缴”到“已缴”账单生成后,住户缴费,系统要更新账单状态并记录缴费流水。// PaymentService.java 缴费处理 @Transactional public void payBill(Long billId, BigDecimal payAmount, String operator) { // 1. 查账单,校验状态 Bill bill = billMapper.selectById(billId); if (bill == null) { throw new BusinessException("账单不存在"); } if ("PAID".equals(bill.getStatus())) { throw new BusinessException("该账单已缴清,请勿重复操作"); } // 2. 校验缴费金额 if (payAmount.compareTo(bill.getAmount()) < 0) { throw new BusinessException("缴费金额不足,当前应缴:" + bill.getAmount()); } // 3. 写缴费记录 Payment payment = new Payment(); payment.setBillId(billId); payment.setPayAmount(payAmount); payment.setPayDate(new Date()); payment.setOperator(operator); paymentMapper.insert(payment); // 4. 更新账单状态 bill.setStatus("PAID"); billMapper.updateStatus(bill); }@Transactional注解保证缴费记录插入和账单状态更新在同一个事务里,要么都成功,要么都回滚。这是缴费模块最不能省的地方,否则会出现“钱交了但账单还是未缴”的脏数据。参数operator记录操作人,方便后续对账。如果项目支持分次缴费,payAmount可以小于bill.getAmount(),状态改成PARTIAL,这里按全额缴费的简单场景处理。3.4 欠费提醒与报表查询欠费提醒的逻辑是查bill表里status = 'UNPAID'且period超过当前月份的记录,在首页或单独页面展示。报表查询则是按房间、按周期聚合用电量和金额,SQL 里用GROUP BY实现。<!-- BillMapper.xml 按房间统计用电量 --> <select id="sumUsageByRoom" resultType="map"> SELECT r.room_no AS roomNo, SUM(b.usage_kwh) AS totalUsage, SUM(b.amount) AS totalAmount FROM bill b JOIN room r ON b.room_id = r.id WHERE b.period BETWEEN #{startPeriod} AND #{endPeriod} GROUP BY r.room_no ORDER BY totalUsage DESC </select>这个查询返回一个List<Map<String, Object>>,前端用 JSTL 遍历渲染成表格。startPeriod和endPeriod是查询参数,对应页面上的时间范围选择器。如果数据量大,可以在period和room_id上建联合索引,查询速度会明显提升。4. 本地跑起来:环境、配置与启动步骤4.1 环境准备:JDK、MySQL、Tomcat 一个不能少传统 SSM 项目需要 JDK 8 或 11、MySQL 5.7 或 8.0、Tomcat 8.5 或 9.0。Spring Boot 项目内置 Tomcat,只需要 JDK 和 MySQL。我一般先确认 JDK 版本:java -version # 输出 java version "1.8.0_xxx" 或 "11.0.x"MySQL 安装后建一个库,字符集用utf8mb4:CREATE DATABASE electricity_fee DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci;然后把sql/electricity_fee.sql导入:mysql -u root -p electricity_fee < sql/electricity_fee.sql导入完成后用SHOW TABLES;确认表都建好了,再看一眼fee_rate表里有没有初始费率数据,没有的话出账会直接报错。4.2 数据库连接配置:改对这三处找到db.properties或application.yml,改数据库连接信息:# db.properties 传统 SSM 项目 jdbc.driver=com.mysql.cj.jdbc.Driver jdbc.url=jdbc:mysql://localhost:3306/electricity_fee?useUnicode=true&characterEncoding=utf8&serverTimezone=Asia/Shanghai&useSSL=false jdbc.username=root jdbc.password=你的密码如果是 Spring Boot 的application.yml:spring: datasource: driver-class-name: com.mysql.cj.jdbc.Driver url: jdbc:mysql://localhost:3306/electricity_fee?useUnicode=true&characterEncoding=utf8&serverTimezone=Asia/Shanghai&useSSL=false username: root password: 你的密码serverTimezone=Asia/Shanghai不加的话,MySQL 8.0 会报时区错误。useSSL=false是本地开发关掉 SSL 警告,生产环境要按实际情况配置。4.3 启动与访问:看到登录页就算成功一半传统 SSM 项目在 IDEA 里配置 Tomcat,把项目部署上去,启动后访问http://localhost:8080/项目名/。Spring Boot 项目直接运行启动类的main方法,控制台看到Started Application in x seconds就说明起来了。默认账号一般在sql脚本的user表里,常见的是admin/123456或admin/admin。登录后先别急着点菜单,按这个顺序走一遍:费率管理 → 房间管理 → 电表管理 → 抄表录入 → 生成账单 → 缴费。这条链路走通,说明核心业务没问题。注意:如果登录后页面报 500,先看控制台异常。最常见的是 MyBatis 映射文件没被扫描到,检查mybatis.mapper-locations配置或SqlSessionFactoryBean的mapperLocations属性。5. 避坑与排查:那些让我加班到凌晨的坑5.1 中文乱码:从数据库到页面的全链路排查现象:抄表记录里的备注、房间名显示成???或乱码。原因:数据库字符集、JDBC URL、页面编码三者不一致。常见的是数据库建库时用了latin1,或者 JDBC URL 里没加characterEncoding=utf8。解决:先确认数据库和表的字符集是utf8mb4,再检查 JDBC URL 带上useUnicode=true&characterEncoding=utf8,最后在 JSP 页面顶部加<%@ page contentType="text/html;charset=UTF-8" language="java" %>。三处都对齐,乱码基本消失。5.2 费率查不到:出账时抛“未配置费率”现象:点“生成账单”按钮,页面提示“未配置费率”。原因:fee_rate表是空的,或者effective_date设置成了未来时间,selectCurrentRate查不到生效记录。解决:往fee_rate表插一条effective_date小于等于今天的记录,比如INSERT INTO fee_rate (rate_name, price_per_kwh, effective_date) VALUES ('居民用电', 0.55, '2025-01-01');。如果项目用的是逻辑删除,还要确认deleted字段是 0。5.3 账单重复生成:同一周期点了两次出账现象:同一个房间同一个周期出现两条账单。原因:出账接口没有做幂等校验,每次点击都插一条新记录。解决:在generateBill方法开头加一次查询,如果该房间该周期已有账单,直接返回已有账单或抛业务异常。SQL 层面可以在bill表的(room_id, period)上建唯一索引,数据库兜底。5.4 缴费后状态没变:事务没生效现象:缴费记录插入了,但账单状态还是“未缴”。原因:@Transactional注解没生效,常见于 Service 类没有被 Spring 代理,或者异常被 catch 后没重新抛出。解决:确认@Transactional加在 public 方法上,且类被 Spring 管理。如果方法里用了 try-catch,确保异常能抛出去触发回滚。Spring Boot 项目还要确认启动类上有@EnableTransactionManagement(虽然自动配置通常已经开了)。5.5 页面 404:Controller 没扫描到现象:访问某个功能页面报 404,控制台没有异常。原因:Controller 类不在 Spring 扫描的包路径下,或者@Controller写成了@Component。解决:检查spring.xml或启动类上的@ComponentScan范围,确保 Controller 包被包含。传统 SSM 项目还要确认web.xml里 DispatcherServlet 的contextConfigLocation指向了正确的配置文件。6. 二次开发:从能跑到好用,我一般改这三处6.1 加阶梯电价:改计算逻辑和费率表阶梯电价是电费系统最常见的改造需求。我的做法是在fee_rate表加tier1_limit、tier1_price、tier2_limit、tier2_price等字段,然后在generateBill里分段计算。// 阶梯电价计算示例 BigDecimal usage = reading.getCurrentReading().subtract(reading.getLastReading()); BigDecimal amount = BigDecimal.ZERO; BigDecimal tier1Limit = rate.getTier1Limit(); // 第一档上限,如 200 度 if (usage.compareTo(tier1Limit) <= 0) { amount = usage.multiply(rate.getTier1Price()); } else { // 第一档部分 amount = tier1Limit.multiply(rate.getTier1Price()); // 超出部分按第二档算 BigDecimal remaining = usage.subtract(tier1Limit); amount = amount.add(remaining.multiply(rate.getTier2Price())); } amount = amount.setScale(2, RoundingMode.HALF_UP);改造时注意tier1Limit和tier2Price这些字段要在FeeRate实体和 Mapper XML 里同步加上,否则查出来是 null。验证方法是造一条用电量刚好跨档的抄表记录,看金额是不是按分段算出来的。6.2 加导出功能:用 POI 把账单导成 Excel报表页面加一个“导出”按钮,用 Apache POI 实现。先在pom.xml加依赖:<dependency> <groupId>org.apache.poi</groupId> <artifactId>poi-ooxml</artifactId> <version>5.2.3</version> </dependency>然后在 Controller 里写导出接口:@GetMapping("/bill/export") public void exportBill(HttpServletResponse response) throws IOException { List<BillVO> list = billService.listAllBills(); XSSFWorkbook workbook = new XSSFWorkbook(); XSSFSheet sheet = workbook.createSheet("账单列表"); // 表头 XSSFRow header = sheet.createRow(0); header.createCell(0).setCellValue("房间号"); header.createCell(1).setCellValue("周期"); header.createCell(2).setCellValue("用电量"); header.createCell(3).setCellValue("金额"); header.createCell(4).setCellValue("状态"); // 数据行 int rowNum = 1; for (BillVO vo : list) { XSSFRow row = sheet.createRow(rowNum++); row.createCell(0).setCellValue(vo.getRoomNo()); row.createCell(1).setCellValue(vo.getPeriod()); row.createCell(2).setCellValue(vo.getUsageKwh().doubleValue()); row.createCell(3).setCellValue(vo.getAmount().doubleValue()); row.createCell(4).setCellValue(vo.getStatus()); } response.setContentType("application/vnd.openxmlformats-officedocument.spreadsheetml.sheet"); response.setHeader("Content-Disposition", "attachment;filename=bills.xlsx"); workbook.write(response.getOutputStream()); workbook.close(); }XSSFWorkbook对应.xlsx格式,HSSFWorkbook对应.xls。数据量大时用SXSSFWorkbook避免内存溢出。导出文件名用英文,中文文件名在某些浏览器上会乱码。6.3 加权限控制:从“登录就行”到“按角色分菜单”原项目可能只有登录校验,没有细粒度权限。我一般用 Spring Security 或简单的拦截器实现。轻量做法是自定义拦截器:public class AuthInterceptor implements HandlerInterceptor { @Override public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception { HttpSession session = request.getSession(); User user = (User) session.getAttribute("loginUser"); if (user == null) { response.sendRedirect(request.getContextPath() + "/login"); return false; } // 管理员才能访问用户管理和费率管理 String uri = request.getRequestURI(); if ((uri.contains("/user/") || uri.contains("/rate/")) && !"ADMIN".equals(user.getRole())) { response.sendRedirect(request.getContextPath() + "/403"); return false; } return true; } }然后在springmvc.xml里注册拦截器,指定拦截路径和排除路径。验证方法是分别用管理员和普通住户账号登录,看菜单和接口访问是否符合预期。6.4 一个我踩过的坑:改完费率后旧账单被重算有次我改费率表,顺手把generateBill里的费率查询逻辑改成了“实时查最新费率”,结果重新生成历史账单时,旧账单金额全变了。后来我强制自己:账单一旦生成,金额就冻结,费率调整只影响新账单。具体做法是在bill表加rate_snapshot字段,出账时把当时的单价存进去,后续查询和展示都用这个快照值。从那以后我每次改费率相关逻辑,都强制走一遍“改费率 → 生成新账单 → 查历史账单”的验证流程,确认旧数据没被波及。希望帮到你。本文还有配套的精品资源,点击获取
简介:这份资源是面向高校计算机相关专业学生与Java初学者的一套毕业设计完整项目源码,主题为基于Java的电费管理系统,可用于课程设计、毕业设计参考或自学练手。压缩包共158个文件,整体约1.78MB,以52个java源文件为核心,配合24个js、19个html、14个css构建前端页面,另有xml配置、properties参数文件、字体图标资源及Maven构建脚本等,结构完整、便于二次开发。系统围绕用户管理、电费计算、缴费管理、数据统计与缴费提醒等模块展开,采用Spring Boot结合MyBatis与MySQL实现后端,前端使用Bootstrap等框架搭建响应式界面,并集成Spring Security与JWT完成认证授权,涵盖阶梯电价、峰谷电价等计费逻辑,以及报表导出与短信提醒等实用功能。目前已有117人学习下载,适合需要完整项目案例、想理解MVC分层设计与前后端协作流程的读者参考借鉴。1. 从一份 Java 毕设资源说起:电费管理系统到底能跑出什么如果你正在找一份能直接跑起来的 Java 毕业设计,又不想碰那种“只有登录页和一张静态表格”的演示壳子,那这份电费管理系统值得花时间拆一拆。它解决的是一个很具体的场景:小区、园区或宿舍楼的电表抄表、费用计算、缴费记录和欠费提醒,全部塞进一个 Java Web 项目里。适合谁?正在做毕设但缺一个完整业务闭环的计算机专业学生,以及想拿一个中小型管理系统练手 CRUD、权限和报表逻辑的初级开发者。我见过太多毕设项目把“管理”两个字做成了摆设,但这个系统至少把用户、电表、费率、账单、缴费这几条线串起来了,跑通之后你能清楚看到一个费用从产生到结清的全过程。2. 技术栈与目录结构:先看清骨架再动手2.1 技术选型:为什么是这套组合从项目标题里的.zip和java能判断,这是一个典型的 Java Web 工程。结合这类毕设项目的常见做法,技术栈大概率落在SSM(Spring + Spring MVC + MyBatis)或Spring Boot + MyBatis上。我拿到源码后第一件事就是看pom.xml或lib目录,确认是 Maven 管理还是传统 Web 项目。如果是 Maven 项目,pom.xml里会集中声明依赖。常见配置如下:<!-- pom.xml 核心依赖片段 --> <dependencies> <!-- Spring 核心:IoC 容器和事务管理 --> <dependency> <groupId>org.springframework</groupId> <artifactId>spring-context</artifactId> <version>5.3.x</version> </dependency> <!-- Spring MVC:处理 Web 请求和视图解析 --> <dependency> <groupId>org.springframework</groupId> <artifactId>spring-webmvc</artifactId> <version>5.3.x</version> </dependency> <!-- MyBatis:SQL 映射与数据库操作 --> <dependency> <groupId>org.mybatis</groupId> <artifactId>mybatis</artifactId> <version>3.5.x</version> </dependency> <!-- MySQL 驱动 --> <dependency> <groupId>mysql</groupId> <artifactId>mysql-connector-java</artifactId> <version>8.0.x</version> </dependency> <!-- JSP 与 JSTL:前端页面渲染 --> <dependency> <groupId>javax.servlet</groupId> <artifactId>jstl</artifactId> <version>1.2</version> </dependency> </dependencies>这段配置说明项目走的是传统三层架构:Controller 接请求,Service 写业务,DAO 层用 MyBatis 操作数据库。参数上要注意 Spring 和 MyBatis 的版本兼容,5.3.x 配 3.5.x 是稳妥组合,MySQL 驱动 8.0 以上要改 JDBC URL 里的时区和 SSL 参数。如果项目是 Spring Boot 版本,那pom.xml里会看到spring-boot-starter-web和mybatis-spring-boot-starter,启动类上会有@SpringBootApplication。两种形态的改造思路不同,后面章节会分开说。2.2 目录结构:每个包在干什么解压后典型的目录长这样:electricity-fee-system/ ├── src/ │ ├── main/ │ │ ├── java/ │ │ │ └── com/example/efee/ │ │ │ ├── controller/ # 接收前端请求 │ │ │ ├── service/ # 业务逻辑 │ │ │ │ └── impl/ │ │ │ ├── mapper/ # MyBatis 接口 │ │ │ ├── entity/ # 数据库实体类 │ │ │ ├── vo/ # 前端展示对象 │ │ │ └── util/ # 工具类 │ │ ├── resources/ │ │ │ ├── mapper/ # MyBatis XML 映射文件 │ │ │ ├── application.yml # 或 spring.xml │ │ │ └── db.properties # 数据库连接配置 │ │ └── webapp/ │ │ ├── WEB-INF/ │ │ │ ├── web.xml # 传统 Web 项目入口 │ │ │ └── views/ # JSP 页面 │ │ └── static/ # CSS、JS、图片 │ └── test/ # 测试代码 ├── sql/ │ └── electricity_fee.sql # 建表与初始数据 └── pom.xmlcontroller包负责把 HTTP 请求参数转成 Java 对象,service包写费用计算和状态流转,mapper包是 MyBatis 接口,对应的 SQL 写在resources/mapper下的 XML 里。entity和数据库表一一对应,vo是给前端页面用的组合对象,比如账单列表里要同时显示用户名和电表编号,就会用一个BillVO来装。提示:拿到源码先别急着改代码,把sql目录下的脚本在 MySQL 里执行一遍,确认表结构和初始数据都进去了,后面排查问题时能少一半玄学。2.3 数据库表设计:费用流转的数据底座电费管理系统的核心表通常有这几张:表名作用关键字段user系统用户(管理员/住户)id, username, password, role, room_idroom房间或住户信息id, room_no, owner_name, phonemeter电表信息id, meter_no, room_id, install_datereading抄表记录id, meter_id, read_date, current_reading, last_readingfee_rate费率配置id, rate_name, price_per_kwh, effective_datebill账单id, room_id, period, usage_kwh, amount, statuspayment缴费记录id, bill_id, pay_date, pay_amount, operatorreading表里存的是本次抄表读数和上次读数,差值就是用电量。bill表根据用电量和费率算出金额,status字段标记“未缴/已缴/逾期”。payment表记录每一笔缴费,和bill是一对多关系,支持分次缴费的场景。这套表结构不算复杂,但覆盖了从抄表到出账再到缴费的完整链路。二次开发时如果想加“阶梯电价”,只需要在fee_rate和计算逻辑里动刀,不用推翻整个设计。3. 核心模块拆解:抄表、算费、出账的代码链路3.1 抄表录入:从表单到数据库抄表是费用计算的起点。前端页面提交电表编号和本次读数,后端要做的第一件事是查出上次读数,算出用电量,然后存一条reading记录。// ReadingController.java 抄表录入接口 @PostMapping("/reading/add") public String addReading(@RequestParam("meterNo") String meterNo, @RequestParam("currentReading") BigDecimal currentReading, Model model) { // 1. 根据电表编号查最近一次抄表记录 Reading lastReading = readingService.getLastReadingByMeterNo(meterNo); if (lastReading == null) { model.addAttribute("msg", "该电表暂无历史抄表记录,请先初始化"); return "reading/add"; } // 2. 校验本次读数不能小于上次读数 if (currentReading.compareTo(lastReading.getCurrentReading()) < 0) { model.addAttribute("msg", "本次读数不能小于上次读数"); return "reading/add"; } // 3. 组装新记录并保存 Reading reading = new Reading(); reading.setMeterNo(meterNo); reading.setLastReading(lastReading.getCurrentReading()); reading.setCurrentReading(currentReading); reading.setReadDate(new Date()); readingService.saveReading(reading); return "redirect:/reading/list"; }这段代码的逻辑很直白:先查历史,再校验,最后落库。参数上currentReading用BigDecimal而不是double,是因为电费计算涉及金额,浮点数会有精度问题,这是很多毕设项目容易翻车的地方。getLastReadingByMeterNo在 Service 层会调用 Mapper 的selectLastByMeterNo,SQL 里用ORDER BY read_date DESC LIMIT 1取最近一条。如果项目是 Spring Boot 版本,@PostMapping和@RequestParam的用法一样,只是不需要在web.xml里配 DispatcherServlet,启动类会自动扫描 Controller。3.2 费用计算:费率匹配与金额生成抄表记录有了,下一步是出账。出账的核心是根据用电量和当前生效的费率算出金额。// BillService.java 出账逻辑 public Bill generateBill(Long roomId, String period) { // 1. 查该房间在指定周期的抄表记录 Reading reading = readingMapper.selectByRoomAndPeriod(roomId, period); if (reading == null) { throw new BusinessException("该周期无抄表记录,无法出账"); } // 2. 计算用电量 = 本次读数 - 上次读数 BigDecimal usage = reading.getCurrentReading() .subtract(reading.getLastReading()); // 3. 查当前生效的费率 FeeRate rate = feeRateMapper.selectCurrentRate(); if (rate == null) { throw new BusinessException("未配置费率,请先在费率管理中设置"); } // 4. 金额 = 用电量 × 单价,保留两位小数 BigDecimal amount = usage.multiply(rate.getPricePerKwh()) .setScale(2, RoundingMode.HALF_UP); // 5. 组装账单对象 Bill bill = new Bill(); bill.setRoomId(roomId); bill.setPeriod(period); bill.setUsageKwh(usage); bill.setAmount(amount); bill.setStatus("UNPAID"); // 初始状态:未缴 billMapper.insert(bill); return bill; }这里有几个关键点。第一,用电量用subtract而不是直接减,保证BigDecimal的精度。第二,setScale(2, RoundingMode.HALF_UP)是金额保留两位小数的标准做法,四舍五入模式要显式指定,否则默认的RoundingMode在某些场景下会抛异常。第三,费率查询用selectCurrentRate,SQL 里按effective_date倒序取第一条,支持费率调整后新账单自动用新价格。period参数一般是"2025-01"这种格式,对应抄表记录的月份。如果项目里没有period字段,可能是用read_date的范围来筛选,改造时要注意统一。3.3 缴费与状态流转:账单从“未缴”到“已缴”账单生成后,住户缴费,系统要更新账单状态并记录缴费流水。// PaymentService.java 缴费处理 @Transactional public void payBill(Long billId, BigDecimal payAmount, String operator) { // 1. 查账单,校验状态 Bill bill = billMapper.selectById(billId); if (bill == null) { throw new BusinessException("账单不存在"); } if ("PAID".equals(bill.getStatus())) { throw new BusinessException("该账单已缴清,请勿重复操作"); } // 2. 校验缴费金额 if (payAmount.compareTo(bill.getAmount()) < 0) { throw new BusinessException("缴费金额不足,当前应缴:" + bill.getAmount()); } // 3. 写缴费记录 Payment payment = new Payment(); payment.setBillId(billId); payment.setPayAmount(payAmount); payment.setPayDate(new Date()); payment.setOperator(operator); paymentMapper.insert(payment); // 4. 更新账单状态 bill.setStatus("PAID"); billMapper.updateStatus(bill); }@Transactional注解保证缴费记录插入和账单状态更新在同一个事务里,要么都成功,要么都回滚。这是缴费模块最不能省的地方,否则会出现“钱交了但账单还是未缴”的脏数据。参数operator记录操作人,方便后续对账。如果项目支持分次缴费,payAmount可以小于bill.getAmount(),状态改成PARTIAL,这里按全额缴费的简单场景处理。3.4 欠费提醒与报表查询欠费提醒的逻辑是查bill表里status = 'UNPAID'且period超过当前月份的记录,在首页或单独页面展示。报表查询则是按房间、按周期聚合用电量和金额,SQL 里用GROUP BY实现。<!-- BillMapper.xml 按房间统计用电量 --> <select id="sumUsageByRoom" resultType="map"> SELECT r.room_no AS roomNo, SUM(b.usage_kwh) AS totalUsage, SUM(b.amount) AS totalAmount FROM bill b JOIN room r ON b.room_id = r.id WHERE b.period BETWEEN #{startPeriod} AND #{endPeriod} GROUP BY r.room_no ORDER BY totalUsage DESC </select>这个查询返回一个List<Map<String, Object>>,前端用 JSTL 遍历渲染成表格。startPeriod和endPeriod是查询参数,对应页面上的时间范围选择器。如果数据量大,可以在period和room_id上建联合索引,查询速度会明显提升。4. 本地跑起来:环境、配置与启动步骤4.1 环境准备:JDK、MySQL、Tomcat 一个不能少传统 SSM 项目需要 JDK 8 或 11、MySQL 5.7 或 8.0、Tomcat 8.5 或 9.0。Spring Boot 项目内置 Tomcat,只需要 JDK 和 MySQL。我一般先确认 JDK 版本:java -version # 输出 java version "1.8.0_xxx" 或 "11.0.x"MySQL 安装后建一个库,字符集用utf8mb4:CREATE DATABASE electricity_fee DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci;然后把sql/electricity_fee.sql导入:mysql -u root -p electricity_fee < sql/electricity_fee.sql导入完成后用SHOW TABLES;确认表都建好了,再看一眼fee_rate表里有没有初始费率数据,没有的话出账会直接报错。4.2 数据库连接配置:改对这三处找到db.properties或application.yml,改数据库连接信息:# db.properties 传统 SSM 项目 jdbc.driver=com.mysql.cj.jdbc.Driver jdbc.url=jdbc:mysql://localhost:3306/electricity_fee?useUnicode=true&characterEncoding=utf8&serverTimezone=Asia/Shanghai&useSSL=false jdbc.username=root jdbc.password=你的密码如果是 Spring Boot 的application.yml:spring: datasource: driver-class-name: com.mysql.cj.jdbc.Driver url: jdbc:mysql://localhost:3306/electricity_fee?useUnicode=true&characterEncoding=utf8&serverTimezone=Asia/Shanghai&useSSL=false username: root password: 你的密码serverTimezone=Asia/Shanghai不加的话,MySQL 8.0 会报时区错误。useSSL=false是本地开发关掉 SSL 警告,生产环境要按实际情况配置。4.3 启动与访问:看到登录页就算成功一半传统 SSM 项目在 IDEA 里配置 Tomcat,把项目部署上去,启动后访问http://localhost:8080/项目名/。Spring Boot 项目直接运行启动类的main方法,控制台看到Started Application in x seconds就说明起来了。默认账号一般在sql脚本的user表里,常见的是admin/123456或admin/admin。登录后先别急着点菜单,按这个顺序走一遍:费率管理 → 房间管理 → 电表管理 → 抄表录入 → 生成账单 → 缴费。这条链路走通,说明核心业务没问题。注意:如果登录后页面报 500,先看控制台异常。最常见的是 MyBatis 映射文件没被扫描到,检查mybatis.mapper-locations配置或SqlSessionFactoryBean的mapperLocations属性。5. 避坑与排查:那些让我加班到凌晨的坑5.1 中文乱码:从数据库到页面的全链路排查现象:抄表记录里的备注、房间名显示成???或乱码。原因:数据库字符集、JDBC URL、页面编码三者不一致。常见的是数据库建库时用了latin1,或者 JDBC URL 里没加characterEncoding=utf8。解决:先确认数据库和表的字符集是utf8mb4,再检查 JDBC URL 带上useUnicode=true&characterEncoding=utf8,最后在 JSP 页面顶部加<%@ page contentType="text/html;charset=UTF-8" language="java" %>。三处都对齐,乱码基本消失。5.2 费率查不到:出账时抛“未配置费率”现象:点“生成账单”按钮,页面提示“未配置费率”。原因:fee_rate表是空的,或者effective_date设置成了未来时间,selectCurrentRate查不到生效记录。解决:往fee_rate表插一条effective_date小于等于今天的记录,比如INSERT INTO fee_rate (rate_name, price_per_kwh, effective_date) VALUES ('居民用电', 0.55, '2025-01-01');。如果项目用的是逻辑删除,还要确认deleted字段是 0。5.3 账单重复生成:同一周期点了两次出账现象:同一个房间同一个周期出现两条账单。原因:出账接口没有做幂等校验,每次点击都插一条新记录。解决:在generateBill方法开头加一次查询,如果该房间该周期已有账单,直接返回已有账单或抛业务异常。SQL 层面可以在bill表的(room_id, period)上建唯一索引,数据库兜底。5.4 缴费后状态没变:事务没生效现象:缴费记录插入了,但账单状态还是“未缴”。原因:@Transactional注解没生效,常见于 Service 类没有被 Spring 代理,或者异常被 catch 后没重新抛出。解决:确认@Transactional加在 public 方法上,且类被 Spring 管理。如果方法里用了 try-catch,确保异常能抛出去触发回滚。Spring Boot 项目还要确认启动类上有@EnableTransactionManagement(虽然自动配置通常已经开了)。5.5 页面 404:Controller 没扫描到现象:访问某个功能页面报 404,控制台没有异常。原因:Controller 类不在 Spring 扫描的包路径下,或者@Controller写成了@Component。解决:检查spring.xml或启动类上的@ComponentScan范围,确保 Controller 包被包含。传统 SSM 项目还要确认web.xml里 DispatcherServlet 的contextConfigLocation指向了正确的配置文件。6. 二次开发:从能跑到好用,我一般改这三处6.1 加阶梯电价:改计算逻辑和费率表阶梯电价是电费系统最常见的改造需求。我的做法是在fee_rate表加tier1_limit、tier1_price、tier2_limit、tier2_price等字段,然后在generateBill里分段计算。// 阶梯电价计算示例 BigDecimal usage = reading.getCurrentReading().subtract(reading.getLastReading()); BigDecimal amount = BigDecimal.ZERO; BigDecimal tier1Limit = rate.getTier1Limit(); // 第一档上限,如 200 度 if (usage.compareTo(tier1Limit) <= 0) { amount = usage.multiply(rate.getTier1Price()); } else { // 第一档部分 amount = tier1Limit.multiply(rate.getTier1Price()); // 超出部分按第二档算 BigDecimal remaining = usage.subtract(tier1Limit); amount = amount.add(remaining.multiply(rate.getTier2Price())); } amount = amount.setScale(2, RoundingMode.HALF_UP);改造时注意tier1Limit和tier2Price这些字段要在FeeRate实体和 Mapper XML 里同步加上,否则查出来是 null。验证方法是造一条用电量刚好跨档的抄表记录,看金额是不是按分段算出来的。6.2 加导出功能:用 POI 把账单导成 Excel报表页面加一个“导出”按钮,用 Apache POI 实现。先在pom.xml加依赖:<dependency> <groupId>org.apache.poi</groupId> <artifactId>poi-ooxml</artifactId> <version>5.2.3</version> </dependency>然后在 Controller 里写导出接口:@GetMapping("/bill/export") public void exportBill(HttpServletResponse response) throws IOException { List<BillVO> list = billService.listAllBills(); XSSFWorkbook workbook = new XSSFWorkbook(); XSSFSheet sheet = workbook.createSheet("账单列表"); // 表头 XSSFRow header = sheet.createRow(0); header.createCell(0).setCellValue("房间号"); header.createCell(1).setCellValue("周期"); header.createCell(2).setCellValue("用电量"); header.createCell(3).setCellValue("金额"); header.createCell(4).setCellValue("状态"); // 数据行 int rowNum = 1; for (BillVO vo : list) { XSSFRow row = sheet.createRow(rowNum++); row.createCell(0).setCellValue(vo.getRoomNo()); row.createCell(1).setCellValue(vo.getPeriod()); row.createCell(2).setCellValue(vo.getUsageKwh().doubleValue()); row.createCell(3).setCellValue(vo.getAmount().doubleValue()); row.createCell(4).setCellValue(vo.getStatus()); } response.setContentType("application/vnd.openxmlformats-officedocument.spreadsheetml.sheet"); response.setHeader("Content-Disposition", "attachment;filename=bills.xlsx"); workbook.write(response.getOutputStream()); workbook.close(); }XSSFWorkbook对应.xlsx格式,HSSFWorkbook对应.xls。数据量大时用SXSSFWorkbook避免内存溢出。导出文件名用英文,中文文件名在某些浏览器上会乱码。6.3 加权限控制:从“登录就行”到“按角色分菜单”原项目可能只有登录校验,没有细粒度权限。我一般用 Spring Security 或简单的拦截器实现。轻量做法是自定义拦截器:public class AuthInterceptor implements HandlerInterceptor { @Override public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception { HttpSession session = request.getSession(); User user = (User) session.getAttribute("loginUser"); if (user == null) { response.sendRedirect(request.getContextPath() + "/login"); return false; } // 管理员才能访问用户管理和费率管理 String uri = request.getRequestURI(); if ((uri.contains("/user/") || uri.contains("/rate/")) && !"ADMIN".equals(user.getRole())) { response.sendRedirect(request.getContextPath() + "/403"); return false; } return true; } }然后在springmvc.xml里注册拦截器,指定拦截路径和排除路径。验证方法是分别用管理员和普通住户账号登录,看菜单和接口访问是否符合预期。6.4 一个我踩过的坑:改完费率后旧账单被重算有次我改费率表,顺手把generateBill里的费率查询逻辑改成了“实时查最新费率”,结果重新生成历史账单时,旧账单金额全变了。后来我强制自己:账单一旦生成,金额就冻结,费率调整只影响新账单。具体做法是在bill表加rate_snapshot字段,出账时把当时的单价存进去,后续查询和展示都用这个快照值。从那以后我每次改费率相关逻辑,都强制走一遍“改费率 → 生成新账单 → 查历史账单”的验证流程,确认旧数据没被波及。希望帮到你。本文还有配套的精品资源,点击获取