
1. 项目概述与业务需求拆解如果你最近正在翻计算机毕设选题列表十有八九会撞见这个题目基于 SpringBoot 的电力营销系统。后面通常还跟着一串关键词Java、SpringBoot、电力营销管理平台、全流程管理系统。我的建议是这个题不但能选而且是个越做越有内容的题。它不是那种表面花哨、实际只有增删改查的空壳也不像个别冷门方向那样做完了答辩老师都提不出问题、甚至看不懂你在做什么。电力营销系统最大的优势在于业务逻辑真实、数据链路完整、技术选型贴合企业主流论文好写、系统好讲演示的时候也拿得出手。这个系统本质上要解决什么问题咱们用大白话说电力公司要给用户建档立户记录每个月用了多少电按电价算出电费把账单发出去、把钱收回来如果没交清还得记一笔欠费台账最后再把这一堆数据汇总成报表。这整条流程——从客户档案、计量管理、抄表录入、电费计算到账单生成、缴费核销、账务统计——就是一个完整的电力营销业务闭环。你作为开发者的任务就是用 Java 和 SpringBoot 把这套流程从线下搬到线上让管理员能维护用户档案抄表员能批量录入电表读数系统自动算费财务确认到账领导能看统计报表。有人可能会问这不就是个普通的业务管理系统吗和图书管理、商品管理有什么区别区别大了。商品管理系统通常只有一套简单的库存和订单流转而电力营销系统天然带着阶梯电价、按需计费、欠费催缴、账实核对这些特殊逻辑。举个例子一个居民用户一个月用了 300 度电和用了 600 度电单价不是一乘了事而是套用电价规则超出阶梯的部分按更高的价格计算。这类业务规则一旦在代码里落地项目深度立刻不一样。适合谁来参考如果你是计算机专业本科生、正在为毕设发愁或者你是研究生、想在这个方向上加一点算法或大数据分析做创新点这篇内容都能给你一条完整的实施路径。我还见过一些自学 Java 想做个像样项目放进简历的伙伴也适合按这个方向练手。它覆盖了工程搭建、权限安全、复杂查询、报表聚合、前后端联调这些日常工作中最常用的技能学完不是只有毕业那一下有用面试聊项目的时候也很有料。2. 总体架构与工程结构设计2.1 单体架构就够了别给自己找事先说一个很多同学容易踩的坑看着网上那些分布式电商项目用 Nacos、Gateway、OpenFeign就也想把自己的毕设拆成微服务架构。我的意见很直接电力营销系统单体应用完全够用甚至更合适。毕设评审看重的是业务流程是否完整、核心逻辑是否合理、代码质量是否规范而不是你拆了几个服务。你要真把 Spring Cloud 全家桶塞进来且不说部署到答辩演示环境有多麻烦单是一个本地服务注册发现调试就足够让你在最后一周崩溃两回。单体 SpringBoot 工程配合模块化包结构已经能把客户的思路表达得很清楚。控制层只做参数接收和结果返回业务层专注事务和规则计算数据层用 MyBatis-Plus 操作数据库前端要么用 Vue 做前后端分离要么用 Thymeleaf 做传统的服务端渲染。我通常建议毕设选前后端分离因为后面简历上能写独立完成前后端全栈开发而且 Vue 的界面效果比 JSP 时代好看太多答辩演示的视觉分很重要。2.2 后端工程结构怎么组织才显得专业有些同学的项目结构是 controller 一层下面全是揉在一起的 service 类一个类几千行答辩老师一打开就皱眉头。这里我给你一个可以直接套用的结构模板power-marketing/ ├── src/main/java/com/example/powermarketing/ │ ├── PowerMarketingApplication.java │ ├── config/ # 配置类跨域、MyBatis-Plus分页、拦截器注册 │ ├── common/ # 统一返回结果、异常处理、常量定义 │ │ ├── Result.java │ │ ├── ResultCode.java │ │ ├── BusinessException.java │ │ └── GlobalExceptionHandler.java │ ├── entity/ # 数据库实体 │ │ ├── User.java │ │ ├── Customer.java │ │ ├── MeterPoint.java │ │ ├── MeterReading.java │ │ └── ElectricityBill.java │ ├── mapper/ # MyBatis-Plus 的 Mapper 接口 │ ├── service/ # 业务接口 │ │ ├── CustomerService.java │ │ ├── MeterReadingService.java │ │ └── impl/ # 业务实现类 │ ├── controller/ # REST 接口 │ │ ├── AuthController.java │ │ ├── CustomerController.java │ │ └── BillController.java │ └── util/ # 工具类JWT工具、日期工具、电价计算器 └── src/main/resources/ ├── application.yml └── mapper/ # 自定义 XML SQL复杂报表查询放这里每个实体和对应的 Controller 一一对应Service 接口和 Impl 分离这虽然不是必须的但答辩老师看到这种结构时第一印象就是这学生是认真练过的。统一返回结果Result尤其重要我见过很多同学接口返回各种乱七八糟的 Map 或者裸 JSON前端解析时一头雾水统一成{ code, message, data }之后所有接口风格一致联调效率高很多。2.3 数据库设计是电力营销系统的灵魂说实话这个项目的很多代码是在数据库表设计完成的那一刻就定了大半。表没设计好后面写一万行代码都是在补窟窿。我按业务模块给你列一下核心表权限模块sys_user用户表字段包含 id、username、passwordBCrypt 加密存储、real_name、role_id、status。不要分什么管理员表、抄表员表统一放一张用户表用角色区分。sys_role角色表预置超级管理员、抄表员、财务员、普通管理员几种角色。sys_menu / sys_role_menu菜单权限表。如果毕设时间紧可以简化成角色字段直接判断权限不做细粒度菜单管理但这么写论文的时候权限设计一章会显得单薄所以建议做一套简化版 RBAC。档案与业务模块customer客户档案表这是整个系统的主数据。包含用户编号、姓名、身份证号脱敏展示、联系电话、用电地址、供电单位、户表关系、建档日期。meter_point计量点表一个客户可以有一个或多个计量点比如一个商铺装了总表和分表。字段有计量点编号、所属用户、电表型号、倍率、安装日期、状态。meter_reading抄表记录表记录某个月某个计量点抄得的电表示数、抄表日期、抄表人、数据来源。这里有一条经验录的是表计示数而不是电量电量是算出来存到账单里的这样能保证原始抄表数据可追溯。tariff电价表用来配置不同用电类别居民、一般工商业、农业的目录电价和阶梯阈值。electricity_bill电费账单表每月的计费结果。字段要有账单月份、用户编号、计量点编号、抄表示数、倍率、电量、电费金额、优惠金额、应收金额、实收金额、缴费状态、生成时间。payment_record缴费记录表每一笔缴费流水。统计与日志operation_log操作日志表记录谁在什么时间干了什么毕设里做审计线索很好用。reconcile_statistics这个可以不建表用 SQL 联表聚合出一张月度汇总也可以答辩讲清楚你的统计口径就行。我把核心表关系说清楚用户和角色是多对一客户和计量点是一对多计量点和抄表记录是一对多月度抄表记录通过计算生成电费账单账单和缴费记录是一对多。建议在建表之前先用 PowerDesigner 或者 Navicat 的模型绘制功能画一张 ER 图这张图直接可以贴进毕业论文开题报告里还能放缩小版一份设计图多个地方用很划算。3. 核心功能模块与实现细节3.1 登录认证与权限拦截JWT Spring 拦截器电力营销系统涉及电费、用户档案这些敏感业务登录认证这块不能糊弄。我推荐用 JWT 做无状态认证好处是可以和 Vue 前端完美配合token存在浏览器本地存储中每次请求在请求头带上后端用拦截器统一校验。先看看配置文件里怎么注册拦截器Configuration public class WebMvcConfig implements WebMvcConfigurer { Resource private AuthInterceptor authInterceptor; Override public void addInterceptors(InterceptorRegistry registry) { registry.addInterceptor(authInterceptor) .addPathPatterns(/**) .excludePathPatterns( /auth/login, /auth/register, /error, /doc.html, /webjars/**, /favicon.ico ); } }登录接口生成 token 的代码也比较固定Service public class AuthServiceImpl implements AuthService { Resource private SysUserMapper sysUserMapper; Resource private JwtUtil jwtUtil; Override public String login(LoginRequest request) { LambdaQueryWrapperSysUser wrapper Wrappers.lambdaQuery(); wrapper.eq(SysUser::getUsername, request.getUsername()); SysUser user sysUserMapper.selectOne(wrapper); if (user null) { throw new BusinessException(用户名或密码错误); } if (!BCrypt.checkpw(request.getPassword(), user.getPassword())) { throw new BusinessException(用户名或密码错误); } if (user.getStatus() ! 1) { throw new BusinessException(账号已被禁用); } // 生成 tokenuserId 和 roleId 放进载荷 return jwtUtil.generateToken(user.getId(), user.getRoleId()); } }这里有个细节登录失败提示统一写成用户名或密码错误不要分别提示用户不存在密码错误防账号探测属于安全常识答辩老师问起来你能说出这个理由印象分会不一样。另外密码必须用 BCrypt 加密千万不要 MD5 或者明文存我在面试中见过太多在校生项目密码直接明文存数据库这是一个非常扎眼的低级失误。权限控制方面我在拦截器里拿到用户角色后可以简单粗暴地做角色匹配Component public class AuthInterceptor implements HandlerInterceptor { Override public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception { if (request.getMethod().equals(OPTIONS)) { return true; // 放行预检请求避免跨域问题 } String token request.getHeader(Authorization); if (token ! null token.startsWith(Bearer )) { token token.substring(7); } if (token null || token.isEmpty()) { throw new BusinessException(401, 未登录或登录已过期); } // 校验 token 并解析出 userId、roleId 放入 request attribute Claims claims jwtUtil.parseToken(token); if (claims null) { throw new BusinessException(401, token 无效); } request.setAttribute(userId, claims.get(userId)); request.setAttribute(roleId, claims.get(roleId)); return true; } }如果你想做到按角色区分接口访问就在拦截器里维护一个接口路径-允许角色的映射表或者用RequireRole注解加在 Controller 方法上配合切面做校验。能把这层做了论文里可以专门写一节基于 RBAC 的权限控制设计答辩就能讲一段。3.2 抄表计算模块为什么存示数而不是直接存电量抄表流程是电力营销的核心数据源。抄表员在系统里选择客户的计量点填入本次电表示数系统自动计算本次用电量。听起来简单但实际上有大学问。我之前说过数据库里应当保存的是表计示数电表上拍的读数而不是用电量。这背后的原因一言以蔽之示数是原始凭证电量是派生物。如果存的是电量一旦电价政策调整或者要核对某个月的明细你没有任何办法回溯验证到底有没有算错但如果存了抄表读数任何时候都能用电量 (本次示数 - 上次示数) 乘以互感器倍率重新验算。电量计算逻辑我写成一个独立的工具类Component public class ElectricityCalculator { /** * 计算单个月用电量 * param currentReading 本次示数 * param lastReading 上次示数 * param rate 互感器倍率直读表为1 * return 用电量千瓦时 */ public BigDecimal calculateUsage(BigDecimal currentReading, BigDecimal lastReading, BigDecimal rate) { BigDecimal usage currentReading.subtract(lastReading); if (usage.compareTo(BigDecimal.ZERO) 0) { throw new BusinessException(当前示数小于上次示数请检查抄表数据); } return usage.multiply(rate).setScale(2, RoundingMode.HALF_UP); } /** * 阶梯电价计算 * param usage 用电量 * param tariffVo 电价配置对象 * return 应收电费 */ public BigDecimal calculateAmount(BigDecimal usage, TariffVo tariffVo) { BigDecimal amount BigDecimal.ZERO; if (tariffVo.getType() 1) { // 居民阶梯第一档用量内按目录电价超出部分按分档加价 BigDecimal firstTier tariffVo.getFirstTierLimit(); // 比如 240 BigDecimal firstTierPrice tariffVo.getFirstTierPrice(); BigDecimal secondTierPrice tariffVo.getSecondTierPrice(); if (usage.compareTo(firstTier) 0) { amount usage.multiply(firstTierPrice); } else { BigDecimal firstPart firstTier.multiply(firstTierPrice); BigDecimal secondPart usage.subtract(firstTier).multiply(secondTierPrice); amount firstPart.add(secondPart); } } else { // 一般工商业及其他单一制电价直接相乘 amount usage.multiply(tariffVo.getSinglePrice()); } return amount.setScale(2, RoundingMode.HALF_UP); } }这里我建议把计算逻辑从 Service 中独立出来一方面方便写单元测试另一方面答辩时你可以展示针对核心计算模块编写了单元测试这是论文中的功能测试章节很好的素材。测试用例也不难写比如上面阶梯电价的用例240 度以内每度 0.52 元超过部分每度 0.57 元如果用电 300 度结果为 2400.52 600.57 124.8 34.2 159.0 元。把这些断言写进测试类答辩前跑一遍绿色通过截图放论文里比什么都好使。3.3 电费账单生成流程化管理的关键账单生成为什么要强调流程因为在真实的电力营销系统里一个月的账单流程通常是抄表数据录入到可用状态然后启动批量计费计费完成后生成账单草稿财务审核后正式发布用户缴费后核销。每笔电费记录从头到尾都有状态。我在设计表结构时给electricity_bill增加了一个bill_status字段1-计费完成待发布2-已发布待缴费3-已缴费4-已冲正。批量生成账单的核心方法一般是这样的思路Transactional(rollbackFor Exception.class) public void generateBill(String billMonth) { // 1. 查出当月已经完成抄表的所有计量点 ListMeterPointVO points meterPointMapper.selectNeedBillingList(billMonth); if (CollectionUtils.isEmpty(points)) { throw new BusinessException(当月没有可计费的抄表数据); } // 2. 遍历每个计量点取当前示数、上次示数、倍率、电价配置 for (MeterPointVO point : points) { MeterReading current meterReadingMapper.selectCurrent(point.getId(), billMonth); MeterReading last meterReadingMapper.selectLastBefore(point.getId(), billMonth); BigDecimal usage calculator.calculateUsage(current.getReading(), last.getReading(), point.getRate()); BigDecimal amount calculator.calculateAmount(usage, tariffMapper.selectByCustomerType(point.getType())); ElectricityBill bill new ElectricityBill(); bill.setCustomerId(point.getCustomerId()); bill.setBillMonth(billMonth); bill.setUsageAmount(usage); bill.setPayableAmount(amount); bill.setBillStatus(1); bill.setCreateTime(new Date()); electricityBillMapper.insert(bill); } }注意方法上面那个Transactional注解批量生成账单是一个典型的强事务操作要么全部生成成功要么失败回滚绝不能出现半个月数据生成了、下半月失败的情况。写论文时可以专门讲一下什么是事务的一致性保证计费数据不重不漏。3.4 报表统计让数据开口说话最后一个核心模块就是统计报表。如果前面全是录数据、算电费那系统只是一个业务记录工具而报表功能才让它有了管理平台的样子。我建议至少做三个维度的统计月度应收实收统计按供电单位或用电类别分组统计某个月应收多少、实收多少、回收率是多少。欠费台账列出所有未结清的账单按欠费金额降序排列能导出 Excel。用电趋势对比某个客户近 12 个月用电量的折线趋势。报表数据不建议在 Java 内存里慢慢聚合直接写 SQL 聚合是最高效的方式。我用一个自定义 XML 查询来处理月度统计MyBatis-Plus 也支持自定义 SQL在 Mapper 接口里写方法XML 里写查询select idselectMonthlySummary resultTypecom.example.powermarketing.entity.vo.MonthlySummaryVO SELECT DATE_FORMAT(create_time, %Y-%m) AS month, SUM(CASE WHEN bill_status IN (2,3) THEN payable_amount ELSE 0 END) AS receivable_amount, SUM(CASE WHEN bill_status 3 THEN actual_amount ELSE 0 END) AS collected_amount, COUNT(CASE WHEN bill_status 3 THEN 1 ELSE NULL END) AS collected_bill_count, COUNT(*) AS total_bill_count FROM electricity_bill GROUP BY DATE_FORMAT(create_time, %Y-%m) ORDER BY month DESC /select这种统计 SQL 就是常说的用数据讲故事的能力答辩老师一定会问你这报表怎么统计的你能把 SQL 语句下意识地讲清楚聚合逻辑这一关基本稳过。另外前端图表建议用 ECharts柱状图看销量、折线图看趋势两三行配置就能出一个漂亮的界面。4. 实操过程与部署调试要点4.1 本地环境准备版本选择要一致做这个项目之前先把环境对齐别在版本不搭的问题上浪费人生。我推荐一套稳妥组合组件版本说明JDK1.8 或 11别用 17 以上部分旧依赖会踩坑SpringBoot2.7.x稳定官方维护期长不要上 3.xMySQL5.7 或 8.08.0 记得驱动用com.mysql.cj.jdbc.DriverRedis5.x如果只做 JWT可以不引入减少复杂度Node.js16.x 或 18.x前端 Vue 项目打包需要IDEIDEA 2022功能全学生授权免费SpringBoot 3.x 现在很流行但如果你没经验我建议老老实实 2.7。SpringBoot 3 强制要求 JDK 17有些第三方 starter 版本跟不上查问题查到凌晨的滋味不好受。4.2 后端项目创建和配置文件直接在 IDEA 里用 Spring Initializr 创建工程选上 Web、MyBatis、MySQL Driver、Validation 这几个依赖。然后打开application.yml把数据源配好server: port: 8080 spring: datasource: url: jdbc:mysql://localhost:3306/power_marketing?useUnicodetruecharacterEncodingutf8useSSLfalseserverTimezoneAsia/Shanghai username: root password: 123456 driver-class-name: com.mysql.cj.jdbc.Driver mybatis-plus: mapper-locations: classpath:mapper/*.xml configuration: log-impl: org.apache.ibatis.logging.stdout.StdOutImpl map-underscore-to-camel-case: true global-config: db-config: id-type: auto logic-delete-field: deleted logic-delete-value: 1 logic-not-delete-value: 0这里map-underscore-to-camel-case设置为 true数据库的user_name字段就能自动映射到 Java 实体的userName少写很多映射代码。打印 SQL 日志的配置建议保留开发阶段排查问题全靠它上线前再关掉就是。4.3 接口联调与演示数据的准备前后端分离开发时联调是必走的一关。前端 Vue 开发服务器默认在 8080 之外的端口比如 5173直接请求后端会触发跨域。我在配置类里写了个跨域过滤器放行所有来源Configuration public class CorsConfig implements WebMvcConfigurer { Override public void addCorsMappings(CorsRegistry registry) { registry.addMapping(/**) .allowedOriginPatterns(*) .allowedMethods(GET, POST, PUT, DELETE, OPTIONS) .allowedHeaders(*) .allowCredentials(true) .maxAge(3600); } }这个配置有个坑如果后端开启了 JWT 拦截器跨域预检请求 OPTIONS 会被拦截器拦住前端就报 401。所以拦截器里要像前面代码那样if (request.getMethod().equals(OPTIONS)) return true;直接放行。这个坑我见过太多人踩写出来提醒一下。演示数据这块我的经验是提前写一个 SQL 脚本造好一套真实感比较强的模拟数据比如十个客户分布在居民、商业、农业等不同类别两个月抄表记录五笔已缴、三笔欠费这样演示的时候一进系统就有内容可看不需要现场录入。答辩的时候现场敲数据很尴尬老师等着看结果越急越容易出bug。4.4 打包部署一台机器跑通前后端最终交付的时候有两种常见方式。如果你只做了后端 Vue 前端最省事的做法是把前端构建产物放进 SpringBoot 的静态资源目录然后打成一个 jar 包一条命令java -jar power-marketing.jar全部启动。做法是在 Vue 项目里执行npm run build将生成的dist目录文件复制到src/main/resources/static下注意前端接口地址要写成相对路径/api再用 Nginx 或后端转发都没有问题。如果你觉得这样不够专业范儿也可以用 Docker 起一个 Nginx 容器跑前端静态文件再起一个容器跑 jar 包。但对毕设来说单 jar 包最简单直接答辩机器上只要装好 JDK、MySQL 就能运行也不用现场演示 Docker 拉镜像那套流程尽量减少不确定因素。5. 常见问题与排查技巧做这个项目从零到一一定会碰到几个重复率极高的问题我逐个给你说一下怎么排查免得你边写边怀疑人生。5.1 MyBatis-Plus 分页失效很多人配置了分页插件但查询时总是不生效Page对象返回的记录数和总条数不正常。原因十有八九是你只引入了分页插件依赖忘了在配置类注册PaginationInnerInterceptorConfiguration public class MybatisPlusConfig { Bean public MybatisPlusInterceptor mybatisPlusInterceptor() { MybatisPlusInterceptor interceptor new MybatisPlusInterceptor(); interceptor.addInnerInterceptor(new PaginationInnerInterceptor(DbType.MYSQL)); return interceptor; } }同时还要注意分页查询的 Mapper 方法不能自己写死 SQL 里的LIMIT否则和分页插件叠加起来会冲突查出来的数据莫名其妙变少。5.2 日期时间格式前后端不一致后端返回LocalDateTime默认的格式是2024-05-21T10:00:00这个带 T 的格式前端展示很难看。在配置文件中加一行 jackson 配置就可以统一spring: jackson: date-format: yyyy-MM-dd HH:mm:ss time-zone: GMT8还有一些场景需要格式化后返回比如前端图表需要月格式2024-05这时直接在 SQL 里用DATE_FORMAT处理最省事别在 Java 层循环格式化。5.3 数据删不掉外键约束报错设计客户档案和计量点表时我建议加外键约束但在删除客户时如果该客户存在账单数据会抛外键约束异常。这个问题有两种解法一种是删除前先检查是否有业务数据有则提示该用户存在历史账单不能删除另一种是逻辑删除就是前面配置里那个logic-delete-field删除时只把deleted标记改成 1不影响历史数据。毕设项目我更推荐逻辑删除既避免了物理外键的麻烦也能在答辩时说出软删除的设计思想。5.4 token 过期了还在操作JWT 无状态的特点就是服务端不保存会话状态token 过期时间一般设置 2 小时或者 24 小时过期后前端请求返回 401。这个逻辑本身没问题但很多初学者处理不好前端拿到 401 之后没有跳转回登录页而是一直卡在报错状态。所以前端 axios 的响应拦截器里要加一段逻辑遇到 401 就清掉本地 token跳回/login。这也是一个可以写进论文的前端统一鉴权处理小亮点。6. 自己动手做一遍的几条硬经验最后说点掏心窝子的。这个题目我前后见过太多人做差别最大的其实不在于谁写的代码多而在于谁能把整套业务串起来、讲清楚。我自己带过几个同学弄类似的毕设给我的体会是先把业务流程图画出来再写代码比什么都有用。你在论文里画一张从档案建立到缴费核销的泳道图答辩的时候拿着激光笔顺着走一遍老师基本就会“哦这个系统逻辑挺清楚”。流程图不一定要多么精美的工具ProcessOn、Draw.io 都行关键是你的思路要像流水一样顺畅。第二个经验是核心计算模块一定单独写类。一是方便测试二是代码结构清晰。别把电价计算逻辑直接写在 Service 里和别的事情搅和在一起不然改一个 bug 会把另一个功能弄炸。我第一次做类似项目的时候图省事把阶梯电价直接写死在 Controller 里后来改电价标准的时候差点把登录接口都搞坏了。这事的教训就是再简单的逻辑只要将来可能变化就应该把它放在独立的地方用一个明确的接口把它包装起来。第三个是给答辩准备的演示建议提前准备三张要点卡片——第一张是系统解决了什么业务痛点第二张是核心功能演示路径第三张是技术难点和解决方案比如事务、权限、报表优化。演示的时候不要先吹牛再翻车而是打开系统按着实际流程走一遍从登录到录入抄表数据再生成账单最后看报表截图全流程走完就是一次完美的演示。现场就不要动数据库改数据了我在另一个项目里亲眼见过同学在答辩现场手动 INSERT 一条用户数据结果 SQL 写错直接白屏三分钟没恢复回来老师只能在旁边干等着。你如果时间还剩两周以上完全可以把这套系统做出来。先从数据库脚本开始建库建表再搭 SpringBoot 后端把登录和客户管理跑通然后补上抄表和计费这两个核心模块最后用 Vue 写页面并做完统计报表。收尾时打包成一个 jar 包在干净的电脑上试着跑一遍全流程确认无误后把截图整理到论文里这个项目就算稳稳落地了。