简介:这份资源是面向高校计算机专业毕业设计场景的完整项目包,主题为基于SSM框架的电动车智能充电服务平台微信小程序,适合正在准备Java方向毕设、需要可运行源码与配套文档的本科或高职学生参考。项目以Java为核心技术栈,后端采用SSM整合MySQL实现数据持久化,前端为微信小程序,功能覆盖首页、个人中心、用户管理、充电桩管理、电池商品管理、托送服务管理、我的钱包、充值信息、消费信息、购买订单、配送信息、服务订单及系统管理等模块,并区分管理员与普通用户两类角色权限。压缩包共1572个文件,约25.86MB,包含160个java源文件、235个js脚本、164个vue组件、122个wxss与120个wxml小程序页面文件,以及png、svg等界面素材,另附docx文档、ppt演示与sql脚本,便于理解表结构与整体设计。目前已有90人学习下载,可作为毕设选题落地、代码研读与答辩材料整理的参考。
1. 电动车智能充电服务平台:一个 SSM + 微信小程序的毕设选题到底在做什么
校园里电动车充电桩前排队的场景,几乎每个做过这类毕业设计的人都见过。这个选题要解决的核心问题很具体:把充电桩的启停、计费、订单、故障上报这几件事,从人工插卡搬到微信小程序上,让用户扫码就能充电、后台能看到每台桩的状态。技术栈是 SSM(Spring + SpringMVC + MyBatis)做后端,微信小程序做用户端,MySQL 存数据。它适合正在找 Java 毕业设计选题、想做一个「有真实业务闭环、能演示、能答辩」的同学,也适合想借这个项目把 SSM 三层架构、微信登录、订单状态机这几块串起来练一遍的人。这个方向不算新,但胜在业务清晰、模块边界好切、答辩时能讲的东西多,源码和文档也容易整理成一套完整交付物。
2. 需求拆解与技术选型:为什么是 SSM 而不是 SpringBoot
2.1 三类角色和它们各自要干的事
先把业务角色理清楚,后面写代码才不会乱。这个平台通常有三类使用者:普通用户、充电桩管理员、平台运营方。用户端在微信小程序里,能做的事包括注册登录、绑定手机号、查看附近充电桩、扫码或选桩启动充电、查看充电进度和费用、在线支付、查历史订单、报故障。管理员端一般在 PC 后台,负责充电桩的增删改查、价格策略配置、订单管理、故障工单处理、数据统计。运营方看的是整体报表和收益。
把这三类角色的功能列成表,是动手前最值钱的一步:
| 角色 | 核心功能 | 对应后端模块 |
|---|---|---|
| 用户 | 登录、找桩、充电、支付、订单、报障 | 用户模块、充电模块、订单模块 |
| 管理员 | 桩管理、价格配置、工单处理 | 桩管理模块、计费模块、工单模块 |
| 运营 | 收益统计、使用报表 | 统计模块 |
这张表决定了你后面要建几张核心表、写几个 Controller。很多同学一上来就写代码,写到一半发现订单状态和充电状态对不上,就是这一步没做。
2.2 SSM 三层架构在这个项目里怎么落
SSM 的价值在于分层清晰,答辩时老师最爱问的就是「你的 Controller、Service、Mapper 各负责什么」。在这个项目里,分层可以这样切:
- Controller 层:接收小程序发来的请求,做参数校验,返回统一 JSON 格式。比如
/charge/start、/order/list。 - Service 层:业务逻辑核心,充电启动要判断桩是否空闲、用户余额是否够、生成订单;充电结束要算费用、改订单状态、释放桩。
- Mapper 层:MyBatis 映射,负责和 MySQL 打交道,写 SQL 或 XML。
为什么不用 SpringBoot?不是不能用,而是 SSM 的 XML 配置和分层结构在答辩时更容易讲清楚「框架原理」,而且很多学校的教学大纲就是按 SSM 走的,老师看着熟悉。如果你自己更熟 SpringBoot,完全可以换,业务逻辑一模一样,只是配置方式不同。常见做法是把 Spring 的 bean 扫描、事务管理、MyBatis 的 SqlSessionFactory 都写在applicationContext.xml里,Web 层单独配spring-mvc.xml。
2.3 数据库表设计的最小集合
表不用多,但几张核心表必须设计对。下面是最小可用集合:
-- 用户表 CREATE TABLE `user` ( `id` INT PRIMARY KEY AUTO_INCREMENT, `openid` VARCHAR(64) NOT NULL COMMENT '微信openid,唯一标识', `nickname` VARCHAR(64), `phone` VARCHAR(20) COMMENT '绑定手机号', `balance` DECIMAL(10,2) DEFAULT 0.00 COMMENT '账户余额', `create_time` DATETIME DEFAULT CURRENT_TIMESTAMP, UNIQUE KEY `uk_openid` (`openid`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; -- 充电桩表 CREATE TABLE `charging_pile` ( `id` INT PRIMARY KEY AUTO_INCREMENT, `pile_no` VARCHAR(32) NOT NULL COMMENT '桩编号,扫码用', `location` VARCHAR(128) COMMENT '位置描述', `status` TINYINT DEFAULT 0 COMMENT '0空闲 1使用中 2故障 3离线', `power` DECIMAL(5,2) COMMENT '功率kW', `price_per_kwh` DECIMAL(6,2) COMMENT '每度电价格' ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; -- 充电订单表 CREATE TABLE `charge_order` ( `id` INT PRIMARY KEY AUTO_INCREMENT, `order_no` VARCHAR(32) NOT NULL, `user_id` INT NOT NULL, `pile_id` INT NOT NULL, `start_time` DATETIME, `end_time` DATETIME, `kwh` DECIMAL(8,2) COMMENT '充电度数', `amount` DECIMAL(10,2) COMMENT '订单金额', `status` TINYINT DEFAULT 0 COMMENT '0充电中 1已完成 2已取消 3异常', UNIQUE KEY `uk_order_no` (`order_no`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;user表的openid必须加唯一索引,这是微信登录的根基。charging_pile的status字段是整个系统状态流转的核心,充电启动和结束都要改它。charge_order的status是订单状态机,后面会专门讲。注意金额字段用DECIMAL不要用FLOAT,浮点算钱迟早出问题,这是血泪经验。
3. 后端核心功能实现:从微信登录到充电订单状态机
3.1 微信小程序登录换取 openid 的完整链路
微信登录是这个小程序的第一道坎,也是答辩必问点。流程是:小程序端调用wx.login()拿到临时code,把code发给后端,后端拿code+appid+secret去微信服务器换openid和session_key,然后后端生成自己的登录态(一般用 token)返回给小程序。
后端换 openid 的代码大致长这样:
// UserController.java @PostMapping("/wxLogin") @ResponseBody public Result wxLogin(@RequestBody Map<String, String> params) { String code = params.get("code"); // 拼接微信接口地址,appid和secret从配置文件读 String url = "https://api.weixin.qq.com/sns/jscode2session" + "?appid=" + appId + "&secret=" + appSecret + "&js_code=" + code + "&grant_type=authorization_code"; // 用HttpClient发GET请求,解析返回JSON String resp = HttpUtil.get(url); JSONObject json = JSON.parseObject(resp); String openid = json.getString("openid"); if (openid == null) { return Result.fail("登录失败"); } // 查库,没有就注册 User user = userService.findByOpenid(openid); if (user == null) { user = new User(); user.setOpenid(openid); userService.insert(user); } // 生成token返回 String token = JwtUtil.createToken(user.getId()); return Result.ok(token); }逻辑说明:code只能用一次,五分钟过期,所以不能缓存复用。appid和secret千万别硬编码在代码里,放配置文件,答辩时老师看到硬编码会扣分。session_key涉及解密用户信息,如果只是拿 openid 做登录,可以不存。参数上,grant_type固定是authorization_code,写错就换不到。
提示:微信开发者工具里测试时,
code每次调用wx.login()都会变,不要手动构造。真机上wx.login()不需要用户授权,静默就能拿到 code。
3.2 充电启动与结束的状态流转
充电业务的核心是状态机。一台桩从空闲到使用中,再到空闲,中间订单从充电中到已完成,每一步都要保证数据一致。启动充电的 Service 逻辑:
// ChargeServiceImpl.java @Transactional public Result startCharge(Integer userId, String pileNo) { // 1. 查桩,加行锁防止并发 ChargingPile pile = pileMapper.selectByNoForUpdate(pileNo); if (pile == null) return Result.fail("桩不存在"); if (pile.getStatus() != 0) return Result.fail("桩不可用"); // 2. 查用户余额 User user = userMapper.selectById(userId); if (user.getBalance().compareTo(new BigDecimal("1.00")) < 0) { return Result.fail("余额不足"); } // 3. 改桩状态为使用中 pile.setStatus(1); pileMapper.updateById(pile); // 4. 生成订单 ChargeOrder order = new ChargeOrder(); order.setOrderNo(OrderNoUtil.gen()); order.setUserId(userId); order.setPileId(pile.getId()); order.setStartTime(new Date()); order.setStatus(0); orderMapper.insert(order); return Result.ok(order.getOrderNo()); }逻辑说明:selectByNoForUpdate是关键,它对应 SQL 里的SELECT ... FOR UPDATE,给这行加排他锁,防止两个用户同时抢同一台桩。@Transactional保证改桩状态和插订单在同一个事务里,要么都成功要么都回滚。余额判断这里只判断了大于 1 元,实际项目里应该按预估充电量冻结一部分金额,但毕设做到这个程度够用。
结束充电的逻辑反过来:算充电度数(可以用桩上报的,也可以按时间估算)、算金额、扣余额、改订单状态为 1、改桩状态为 0。这里最容易翻车的是「充电中途用户退出小程序」,订单一直挂在充电中。解决办法是加一个定时任务,扫描超过设定时长还没结束的订单,自动结算或标记异常。
3.3 订单列表与分页查询
订单列表是用户端高频功能,用 MyBatis 分页插件(PageHelper)最省事:
<!-- ChargeOrderMapper.xml --> <select id="selectByUser" resultType="ChargeOrder"> SELECT o.*, p.pile_no, p.location FROM charge_order o LEFT JOIN charging_pile p ON o.pile_id = p.id WHERE o.user_id = #{userId} ORDER BY o.start_time DESC </select>// Service层调用 PageHelper.startPage(pageNum, pageSize); List<ChargeOrder> list = orderMapper.selectByUser(userId); PageInfo<ChargeOrder> pageInfo = new PageInfo<>(list); return Result.ok(pageInfo);逻辑说明:PageHelper.startPage必须紧跟在查询方法前一行,中间不能插别的数据库操作,否则分页会串。LEFT JOIN是为了把桩编号和位置带出来,前端展示更友好。参数pageNum从 1 开始,pageSize建议不超过 20,小程序下拉加载体验好。
4. 微信小程序端对接:登录、扫码与充电页面的关键实现
4.1 小程序登录与手机号获取
小程序端登录分两步:先wx.login()换 token,再按需获取手机号。获取手机号用的是<button open-type="getPhoneNumber">,用户点击后拿到加密的encryptedData和iv,发给后端解密。注意这个能力需要小程序有对应权限,个人开发者账号可能拿不到,毕设演示时可以用模拟数据代替。
// pages/login/login.js Page({ data: { token: '' }, onLoad() { this.wxLogin(); }, wxLogin() { wx.login({ success: (res) => { if (res.code) { wx.request({ url: 'http://localhost:8080/user/wxLogin', method: 'POST', data: { code: res.code }, success: (resp) => { // 存token到本地 wx.setStorageSync('token', resp.data.data); this.setData({ token: resp.data.data }); } }); } } }); }, // 获取手机号按钮回调 getPhone(e) { if (e.detail.errMsg !== 'getPhoneNumber:ok') return; wx.request({ url: 'http://localhost:8080/user/bindPhone', method: 'POST', header: { 'token': wx.getStorageSync('token') }, data: { encryptedData: e.detail.encryptedData, iv: e.detail.iv }, success: (resp) => { wx.showToast({ title: '绑定成功' }); } }); } });逻辑说明:wx.setStorageSync把 token 存本地,后续所有请求都在 header 里带上。e.detail.errMsg判断用户是否同意授权,不同意就别发请求。后端解密用微信提供的 AES 工具类,session_key是登录时拿到的,要存起来或缓存。
4.2 扫码启动充电的页面逻辑
扫码用wx.scanCode,扫到桩上的二维码(内容就是桩编号),然后调后端启动接口:
// pages/scan/scan.js Page({ scanAndCharge() { wx.scanCode({ success: (res) => { const pileNo = res.result; // 二维码内容即桩编号 wx.request({ url: 'http://localhost:8080/charge/start', method: 'POST', header: { 'token': wx.getStorageSync('token') }, data: { pileNo: pileNo }, success: (resp) => { if (resp.data.code === 200) { // 跳转到充电中页面,带上订单号 wx.navigateTo({ url: '/pages/charging/charging?orderNo=' + resp.data.data }); } else { wx.showToast({ title: resp.data.msg, icon: 'none' }); } } }); } }); } });逻辑说明:res.result就是二维码里的字符串,生成二维码时把桩编号写进去即可。启动成功后跳转到充电中页面,用orderNo去轮询充电进度。轮询用setInterval,每 5 秒查一次订单状态,充电结束就清除定时器并跳转支付。
4.3 充电进度轮询与支付
充电中页面要展示已充电度数、已用时间、预估费用。这些数据后端可以按时间实时算,也可以等桩上报。毕设里用时间估算最简单:
// pages/charging/charging.js Page({ data: { kwh: 0, amount: 0, status: 0 }, onLoad(options) { this.orderNo = options.orderNo; this.timer = setInterval(() => { this.queryProgress(); }, 5000); }, queryProgress() { wx.request({ url: 'http://localhost:8080/charge/progress', data: { orderNo: this.orderNo }, success: (resp) => { const d = resp.data.data; this.setData({ kwh: d.kwh, amount: d.amount, status: d.status }); if (d.status === 1) { clearInterval(this.timer); // 跳转支付 wx.navigateTo({ url: '/pages/pay/pay?orderNo=' + this.orderNo }); } } }); }, onUnload() { clearInterval(this.timer); // 页面卸载必须清定时器 } });逻辑说明:onUnload里清定时器是必须的,否则页面关了请求还在发,真机上会报错。支付环节毕设一般用模拟支付,点确认就把订单状态改成已支付,真实接微信支付需要商户号,个人做不了。
5. 避坑与排查:这类毕设最容易翻车的 5 个地方
5.1 现象:两个用户同时扫同一台桩,都启动成功
原因:启动充电时没加锁,两个请求同时查到桩状态是空闲,都通过了判断。解决:在 Mapper 里用SELECT ... FOR UPDATE加行锁,或者用 Redis 分布式锁。毕设里行锁足够,注意FOR UPDATE必须在事务里才生效。
5.2 现象:小程序请求后端一直 404 或跨域
原因:本地开发时小程序请求localhost没问题,但真机调试时手机访问不到电脑的 localhost;另外微信开发者工具默认校验合法域名。解决:真机调试用电脑局域网 IP,开发者工具里勾选「不校验合法域名」。后端跨域在 Controller 上加@CrossOrigin或配全局 CORS。
5.3 现象:订单金额算出来是 0.30000000000000004
原因:用了double或float算钱。解决:金额一律用BigDecimal,数据库用DECIMAL。BigDecimal比较大小用compareTo不要用equals,equals会比较精度。
5.4 现象:充电中途关掉小程序,订单永远停在充电中
原因:没有超时处理机制。解决:加定时任务(Spring 的@Scheduled),每分钟扫一次超过设定时长(比如 4 小时)还在充电中的订单,自动结算或标记异常。这是答辩时能加分的点。
5.5 现象:微信登录偶尔失败,提示 code 无效
原因:code被重复使用,或者前端把wx.login的 code 缓存了。解决:每次登录都重新调wx.login(),code 不缓存。另外检查appid和secret是否配对,secret 重置过要同步更新配置。
6. 让这个毕设从「能跑」到「能拿高分」的几个技巧
先说一个容易被忽略的点:把状态流转画成一张图放进论文。充电桩状态(空闲/使用中/故障/离线)和订单状态(充电中/已完成/已取消/异常)之间的转换关系,用表格或状态图列清楚,答辩时老师一看就知道你想明白了。这比堆代码有用得多。
再讲一个实操技巧:给关键接口加日志。在 Service 层用log.info记录每次充电启动、结束、扣费的入参和结果,出问题时能快速定位。很多同学答辩演示时突然某个功能不对,现场翻代码找半天,有日志的话一眼就能看出是参数错了还是数据状态不对。
关于数据统计模块,如果时间紧,不用做复杂的图表,用一条 SQL 出结果就行:
-- 统计每个充电桩的总充电度数和总收益 SELECT p.pile_no, p.location, IFNULL(SUM(o.kwh), 0) AS total_kwh, IFNULL(SUM(o.amount), 0) AS total_amount, COUNT(o.id) AS order_count FROM charging_pile p LEFT JOIN charge_order o ON p.id = o.pile_id AND o.status = 1 GROUP BY p.id ORDER BY total_amount DESC;这条 SQL 用LEFT JOIN保证没有订单的桩也能显示,IFNULL处理空值,status = 1只统计已完成的订单。放到后台报表页,答辩时演示「哪个桩最赚钱」,比干讲架构有说服力。
最后说文档和 PPT。文档别写成代码注释的堆砌,重点写三块:需求分析(为什么做这个)、系统设计(表结构和模块划分)、测试(关键功能的测试用例和结果)。PPT 控制在 15 页以内,把状态流转图、核心表设计、一个完整业务流程(扫码到支付)讲透就够了。我自己的习惯是,答辩前把整个流程在真机上走三遍,每一步截图存好,现场演示万一网络出问题,直接翻截图也能讲下去。
希望帮到你。
本文还有配套的精品资源,点击获取