简介:这份资源是面向高校计算机专业毕业设计场景的完整项目包,基于SSM框架开发电动车智能充电服务平台微信小程序,适合正在准备Java方向毕设、需要可运行源码与配套文档的本科生或自学者。项目围绕充电桩管理、电池商品、托送服务、钱包充值、消费与购买订单、配送信息、服务订单等模块展开,区分管理员与普通用户两类角色,覆盖注册登录、信息浏览与后台管理等典型业务链路。压缩包共1572个文件,约25.86MB,包含160个Java源文件、122个wxss与120个wxml小程序页面文件、164个vue前端组件、235个js脚本,以及sql脚本、properties配置、docx文档与ppt答辩材料等,便于理解前后端结构与数据库设计。目前已有90人学习下载,可作为毕设选题参考、代码二次开发与答辩准备的实践素材。
1. 电动车智能充电服务平台:从 SSM 后端到微信小程序的完整落地路径
校园、小区、产业园里电动车充电桩越铺越多,但真正让运营方头疼的不是硬件,而是「谁在充、充了多久、扣了多少钱、桩子是不是离线」这一整套账。基于 SSM 的电动车智能充电服务平台微信小程序,本质就是把充电桩的计费、订单、设备状态和用户管理搬到微信生态里:后端用 Spring + SpringMVC + MyBatis 扛住业务逻辑和数据库读写,前端用微信小程序做扫码充电、余额充值、订单查询。它适合做 Java 毕业设计的同学,也适合想快速搭一套充电桩管理后台的开发者。这篇笔记按「后端怎么分层、小程序怎么对接、参数怎么设、坑在哪」的顺序拆开讲,源码和文档只是结果,真正值钱的是这套结构你能不能自己复现出来。
2. SSM 后端分层:Controller、Service、Mapper 各管什么
2.1 为什么充电业务适合用 SSM 而不是一把梭
充电平台的核心链路是「用户扫码 → 校验设备 → 创建订单 → 计费 → 结束订单 → 扣款」,这条链路里既有事务要求,又有设备状态并发问题。SSM 的分层刚好能把这件事拆干净:Controller 只负责接收小程序请求和返回 JSON,Service 层处理业务规则和事务边界,Mapper 层只做 SQL 映射。常见做法是把充电订单的创建和余额扣减放在同一个 Service 方法里,用@Transactional保证要么都成功要么都回滚,避免出现「订单建了钱没扣」或者「钱扣了订单没建」的玄学问题。
如果换成纯 Servlet 或者把逻辑全塞进 Controller,后期加一个「按时长阶梯计费」的规则就要改一堆地方。SSM 的好处是计费规则集中在 Service,Mapper 只关心表字段,改需求时影响面可控。这也是为什么大量 Java 毕业设计仍然选 SSM 作为骨架——它不是最新,但结构清晰,答辩时讲得清楚。
2.2 三层结构的目录与依赖配置
一个能跑起来的 SSM 工程,目录结构建议这样组织,别把所有类平铺在一个包下:
src/main/java/com/charging/ ├── controller/ # 小程序接口入口 │ ├── UserController.java │ ├── DeviceController.java │ └── OrderController.java ├── service/ │ ├── UserService.java │ ├── DeviceService.java │ └── impl/ │ ├── UserServiceImpl.java │ └── OrderServiceImpl.java ├── mapper/ # MyBatis 接口 │ ├── UserMapper.java │ └── OrderMapper.java ├── entity/ # 与表对应的实体 │ ├── User.java │ ├── Device.java │ └── ChargeOrder.java └── util/ # 统一返回、时间工具 └── Result.java src/main/resources/ ├── mapper/ # XML 映射文件 │ ├── UserMapper.xml │ └── OrderMapper.xml ├── spring-dao.xml ├── spring-service.xml ├── spring-mvc.xml └── jdbc.propertiesjdbc.properties里数据库连接是第一个要调对的地方,参数写错后面全是白忙:
jdbc.driver=com.mysql.cj.jdbc.Driver jdbc.url=jdbc:mysql://localhost:3306/charging_db?useUnicode=true&characterEncoding=utf8&serverTimezone=Asia/Shanghai jdbc.username=root jdbc.password=你的密码serverTimezone必须显式指定,否则 MySQL 8 下订单时间会差 8 小时,这个坑我在第一次跑订单列表时踩过,页面上显示的时间全是凌晨,排查半天才发现是时区没配。characterEncoding=utf8保证中文设备名不乱码。连接池参数在spring-dao.xml里配,初始连接数 5、最大 20 对毕业设计演示足够,别一上来就设 100,本地数据库扛不住反而启动报错。
2.3 用 MyBatis 映射充电订单表的完整示例
订单表是这套系统的核心,字段设计直接决定后面计费好不好写。下面这张表结构是我一般会用的版本:
| 字段名 | 类型 | 说明 |
|---|---|---|
| id | bigint | 主键,自增 |
| order_no | varchar(32) | 订单号,唯一 |
| user_id | bigint | 用户 ID |
| device_id | bigint | 充电桩 ID |
| start_time | datetime | 开始充电时间 |
| end_time | datetime | 结束充电时间 |
| duration | int | 充电时长(分钟) |
| amount | decimal(10,2) | 订单金额 |
| status | tinyint | 0 进行中 1 已完成 2 已取消 |
对应的实体和 Mapper 接口:
public class ChargeOrder { private Long id; private String orderNo; private Long userId; private Long deviceId; private Date startTime; private Date endTime; private Integer duration; private BigDecimal amount; private Integer status; // getter/setter 省略 }public interface OrderMapper { int insertOrder(ChargeOrder order); ChargeOrder selectByOrderNo(String orderNo); int updateOrderStatus(@Param("orderNo") String orderNo, @Param("status") Integer status, @Param("endTime") Date endTime, @Param("duration") Integer duration, @Param("amount") BigDecimal amount); List<ChargeOrder> selectByUserId(@Param("userId") Long userId); }XML 里写 SQL 时,updateOrderStatus用动态 SQL 只更新非空字段,避免把没传的字段覆盖成 null:
<update id="updateOrderStatus"> update charge_order <set> <if test="status != null">status = #{status},</if> <if test="endTime != null">end_time = #{endTime},</if> <if test="duration != null">duration = #{duration},</if> <if test="amount != null">amount = #{amount},</if> </set> where order_no = #{orderNo} </update>这里order_no建唯一索引,防止重复提交生成两条订单。amount用BigDecimal而不是double,钱的计算用浮点数迟早出精度问题,这是血泪经验。Service 层结束订单时先查订单状态,只有status=0才允许更新,避免用户重复点击「结束充电」导致金额被扣两次。
3. 微信小程序对接:登录、扫码、订单列表怎么串起来
3.1 小程序登录获取手机号与后端会话打通
小程序端第一步是拿到用户身份。常见做法是wx.login拿 code,后端用 code 换 openid,再结合手机号授权把用户落到user表。注意wx.getUserInfo已经拿不到真实昵称头像,别在这上面浪费时间,用手机号授权按钮更实际。
// 小程序端:登录并获取手机号 wx.login({ success(res) { if (res.code) { wx.request({ url: 'https://你的域名/api/user/login', method: 'POST', data: { code: res.code }, success(r) { // 后端返回自定义 token,存本地 wx.setStorageSync('token', r.data.data.token) } }) } } })后端UserController收到 code 后调用微信接口换 openid,查库不存在就插入新用户,返回一个自己生成的 token(可以用 UUID 存 Redis 或直接 JWT)。参数上要注意:微信接口返回的session_key不要下发给小程序,留在服务端。token 有效期设 7 天,过期让小程序重新登录。这里容易翻车的是域名必须备案且配到小程序后台的 request 合法域名,本地调试时在开发者工具里勾「不校验合法域名」能省很多事,但上线前一定要配好。
3.2 扫码充电的完整请求链路
扫码充电是小程序的核心交互。用户扫充电桩上的二维码,二维码里带device_no,小程序解析后调后端创建订单接口:
// 扫码后发起充电 wx.scanCode({ success(res) { const deviceNo = res.result // 二维码内容即设备编号 wx.request({ url: 'https://你的域名/api/order/start', method: 'POST', header: { 'token': wx.getStorageSync('token') }, data: { deviceNo: deviceNo }, success(r) { if (r.data.code === 200) { wx.showToast({ title: '开始充电' }) } else { wx.showToast({ title: r.data.msg, icon: 'none' }) } } }) } })后端start接口要做四件事:校验 token、查设备是否存在且在线、查用户余额是否大于起步金额、插入订单并把设备状态改为「使用中」。这四步必须在一个事务里,尤其是「查余额」和「扣款」之间不能有间隙,否则并发下会出现余额扣成负数。我一般会在device表加一个status字段,用update device set status=1 where id=? and status=0这种带条件的更新来判断设备是否被抢占,返回影响行数为 0 就说明别人先占了,直接给小程序返回「设备使用中」。
3.3 订单列表与分页参数怎么设
订单列表接口按用户 ID 查,分页参数用pageNum和pageSize,默认每页 10 条。小程序端用onReachBottom做上拉加载:
@RequestMapping("/list") public Result list(@RequestParam Long userId, @RequestParam(defaultValue = "1") Integer pageNum, @RequestParam(defaultValue = "10") Integer pageSize) { PageHelper.startPage(pageNum, pageSize); List<ChargeOrder> list = orderMapper.selectByUserId(userId); PageInfo<ChargeOrder> page = new PageInfo<>(list); return Result.success(page); }PageHelper的startPage必须紧挨着查询语句,中间不能插别的数据库操作,否则分页会作用到错误的 SQL 上,这是很隐蔽的一个坑。返回给小程序的时间字段建议在后端格式化成yyyy-MM-dd HH:mm,别让小程序自己解析时间戳,省得两端时区不一致又出问题。
4. 计费规则与设备状态:参数怎么定、并发怎么防
4.1 按时长阶梯计费的实现与参数
充电计费常见两种:按小时固定价,或者阶梯价。阶梯价更贴近实际运营,比如前 60 分钟 1 元,之后每 30 分钟 0.5 元。实现时把规则写成配置,别硬编码在代码里:
public BigDecimal calcAmount(int duration) { BigDecimal amount = BigDecimal.ZERO; if (duration <= 60) { amount = new BigDecimal("1.00"); } else { amount = new BigDecimal("1.00"); int extra = duration - 60; int units = (int) Math.ceil(extra / 30.0); // 不足 30 分钟按 30 分钟算 amount = amount.add(new BigDecimal("0.50").multiply(new BigDecimal(units))); } return amount; }Math.ceil保证不足一个计费单元也按一个单元收,这是运营方普遍接受的规则。duration由结束时间减开始时间得到,单位分钟。金额计算全程用BigDecimal,multiply和add都不要用double中转。如果要做「封顶价」,在最后加一个if (amount.compareTo(max) > 0) amount = max;即可。
4.2 设备在线状态与心跳检测
设备状态不能只靠数据库字段,真实场景里桩子可能断网但数据库还写着「在线」。常见做法是加一张device_heartbeat表或者用 Redis 存设备最后心跳时间,超过 3 分钟没心跳就判定离线。查询设备时先看心跳时间:
select d.*, h.last_time from device d left join device_heartbeat h on d.id = h.device_id where d.device_no = #{deviceNo}如果last_time为空或者距当前超过 180 秒,直接返回「设备离线」。这个 180 秒是可调参数,心跳间隔一般设 60 秒,容忍两次丢包。别把阈值设得太小,网络抖动一下设备就掉线,用户扫码全是失败,体验很差。
4.3 并发充电请求的防重与锁
同一用户快速连点两次扫码,或者两个用户同时扫同一个桩,都会产生并发问题。防重有三层:前端按钮点击后置灰、后端用订单号唯一索引兜底、设备状态用条件更新抢占。订单号生成建议用「时间戳 + 用户 ID 后四位 + 随机数」,别用纯自增,避免被猜到。
String orderNo = System.currentTimeMillis() + String.format("%04d", userId % 10000) + (int)(Math.random() * 9000 + 1000);插入订单时如果唯一索引冲突会抛DuplicateKeyException,在 Service 里捕获后返回「请勿重复提交」。设备抢占用前面说的条件更新,两步配合基本能挡住绝大多数并发。分布式锁对毕业设计来说属于过度设计,本地单机部署用数据库条件更新就够了,别为了炫技引入 Redis 锁反而增加调试成本。
5. 避坑与排查:这套系统最容易翻车的 5 个地方
5.1 小程序请求全部 400 或跨域报错
现象是小程序开发者工具里请求返回 400,或者提示不在合法域名列表。原因通常是后端接口路径没配好,或者spring-mvc.xml里没开注解驱动导致参数绑定失败。解决:确认@RequestMapping路径和前端url完全一致,检查<mvc:annotation-driven/>是否配置,本地调试勾选「不校验合法域名」,上线前把域名备案并加到小程序后台。
5.2 订单时间差 8 小时
现象是订单列表里开始时间显示成凌晨。原因是数据库连接串没写serverTimezone,或者实体里用了java.util.Date但 JDBC 按 UTC 解析。解决:连接串加serverTimezone=Asia/Shanghai,实体时间字段统一用java.util.Date并在返回前用SimpleDateFormat格式化,别混用LocalDateTime和Date。
5.3 余额扣成负数
现象是并发充电时用户余额被扣穿。原因是「查余额」和「扣余额」分成了两条 SQL,中间被其他请求插入。解决:扣款用update user set balance = balance - #{amount} where id = #{id} and balance >= #{amount},判断影响行数,为 0 就回滚整个事务并返回「余额不足」。
5.4 分页查询返回总数不对
现象是订单列表总数一直是当前页条数。原因是PageHelper.startPage之后又执行了别的查询,或者PageInfo包装的不是startPage紧挨着的那次查询结果。解决:startPage和查询语句之间不要插入任何 Mapper 调用,PageInfo直接包装查询返回的 List。
5.5 设备状态卡在「使用中」不释放
现象是用户充完电,设备状态还是 1,别人扫不了。原因是结束订单时只更新了订单表,忘了同步更新设备表。解决:结束订单的 Service 方法里,更新订单状态和把device.status改回 0 放在同一个事务,任何一步失败都回滚。再加一个定时任务,扫描超过 24 小时还是「使用中」的订单强制结束,作为兜底。
6. 从能跑到好用:接口幂等与日志排查的两个进阶习惯
把系统跑起来只是第一步,真正让这套充电平台「敢给人用」的是两件事:接口幂等和可排查的日志。接口幂等我在订单创建上一般会加一个客户端生成的requestId,小程序每次点击生成一个 UUID 随请求带上,后端用 Redis 或数据库唯一索引记录这个requestId,重复请求直接返回上次结果。这样即使用户网络卡顿重发,也不会多扣一次钱。参数上requestId有效期设 5 分钟,过期自动清理,别让它无限堆积。
日志这块,SSM 默认的 Log4j 或 Logback 配置只打 INFO,出问题时根本看不出哪条 SQL 慢了。我习惯在logback.xml里把 Mapper 包的日志级别单独调到 DEBUG,这样每条 SQL 和参数都会打出来:
<logger name="com.charging.mapper" level="DEBUG"/>调完之后记得改回 INFO,不然生产环境日志量会爆炸。排查订单问题时,按order_no去日志里搜,能看到从创建到结束的完整链路,比在数据库里翻表快得多。还有一个习惯是给每个接口的返回加统一的Result包装,code、msg、data三个字段固定,小程序端只判断code,出问题时后端日志里也能一眼看出是业务失败还是系统异常。
这套基于 SSM 的电动车智能充电服务平台,结构不复杂,但每个环节都有能踩的坑。我的习惯是先把订单链路跑通,再补设备心跳和并发防护,最后才做界面美化。顺序反了的话,界面再好看,一并发就崩,答辩时被问一句「两个人同时扫同一个桩会怎样」就答不上来。希望帮到你。
本文还有配套的精品资源,点击获取