☰
Java+SSM+微信小程序毕业设计:电动车智能充电平台实战
2026/9/29 7:37:27 网站建设 项目流程

简介:本资源为基于SSM框架的电动车智能充电服务平台微信小程序毕业设计全套资料,面向计算机相关专业需要完成毕业设计的学生及Java初学者。项目采用Java技术栈与MySQL数据库,实现首页、个人中心、用户管理、充电桩管理、电池商品管理、托送服务管理、我的钱包、充值信息、消费信息、购买订单、配送信息、服务订单及系统管理等功能模块,区分管理员与普通用户两类角色权限,注册登录后可进行后台操作。压缩包共1572个文件,涵盖160个Java源码、235个js脚本、164个vue组件、122个wxss与120个wxml小程序页面文件,以及png、svg等图片素材和sql脚本、docx文档、ppt演示文稿,整体约25.86MB,目录结构清晰。已有89人学习下载。读者可获得完整可运行的源码工程、数据库脚本、配套说明文档与答辩PPT,便于快速理解SSM与微信小程序的整合开发思路,对照模块划分完成功能扩展与二次开发,为毕业设计选题、编码实现与答辩准备提供参考。

1. 从一份 SSM 小程序毕设包说起:它到底能跑出什么

电动车智能充电服务平台,说白了就是把「找桩、扫码、充电、计费、结算」这条链路搬到微信小程序里,后台用 SSM(Spring + SpringMVC + MyBatis)扛业务。这个标题里塞了四个关键词:Java、SSM、微信小程序、毕业设计,任何一个单拎出来都是热搜常客,凑在一起就是计算机毕业设计里最典型的一类选题——业务闭环清晰、技术栈主流、答辩时老师一听就懂。

我带过几届学生的毕设,这类题目最大的价值不在「智能」两个字,而在于它把 Java 后端和微信小程序前端完整串了一遍:后端要处理用户、充电桩、订单、钱包四张核心表,前端要处理登录态、扫码、支付回调、实时状态刷新。你把这套跑通,等于把 SSM 的增删改查、事务、拦截器,以及小程序的请求封装、缓存、页面栈全过了一遍。适合谁?适合 Java 基础刚学完、想找一个能写进简历、答辩不慌的本科或专科毕业生,也适合想拿一个完整项目练手 SSM 的转行者。

我一般会先跟人说清楚:这个包不是拿来直接交差的,是拿来拆的。拆开看它怎么分层、怎么配、怎么把充电订单的状态机跑对,比直接改个名字交上去值钱得多。下面按「先立住原理、再动手复现、最后避坑」的顺序讲。

2. SSM 后端骨架怎么搭:从四张核心表到能跑通的接口

2.1 先定表结构,别急着写 Controller

很多人一上来就打开 IDEA 建工程,结果写到一半发现订单和充电桩的关系没想清楚,回头改表改到崩溃。我的习惯是先把 ER 想明白,落到 SQL 上。电动车充电平台的核心就四张表:用户表、充电桩表、充电订单表、钱包流水表。下面是我常用的建表脚本,字段做了精简但保留了关键约束。

-- 用户表:微信登录靠 openid 唯一标识 CREATE TABLE `user` ( `id` INT PRIMARY KEY AUTO_INCREMENT, `openid` VARCHAR(64) NOT NULL UNIQUE COMMENT '微信openid,登录唯一键', `nickname` VARCHAR(64) DEFAULT '', `balance` DECIMAL(10,2) DEFAULT 0.00 COMMENT '钱包余额,单位元', `create_time` DATETIME DEFAULT CURRENT_TIMESTAMP ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; -- 充电桩表:status 用 0空闲 1占用 2故障 三态 CREATE TABLE `charger` ( `id` INT PRIMARY KEY AUTO_INCREMENT, `code` VARCHAR(32) NOT NULL UNIQUE COMMENT '桩编号,扫码得到', `location` VARCHAR(128) COMMENT '安装位置', `power` DECIMAL(6,2) COMMENT '功率kW', `price_per_hour` DECIMAL(6,2) COMMENT '每小时单价', `status` TINYINT DEFAULT 0 ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; -- 订单表:状态机是灵魂,0进行中 1已完成 2已取消 CREATE TABLE `order` ( `id` INT PRIMARY KEY AUTO_INCREMENT, `user_id` INT NOT NULL, `charger_id` INT NOT NULL, `start_time` DATETIME, `end_time` DATETIME, `cost` DECIMAL(10,2) DEFAULT 0.00, `status` TINYINT DEFAULT 0, INDEX `idx_user` (`user_id`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; -- 钱包流水:每次扣费/充值都留痕,方便对账 CREATE TABLE `wallet_log` ( `id` INT PRIMARY KEY AUTO_INCREMENT, `user_id` INT NOT NULL, `amount` DECIMAL(10,2) COMMENT '正数充值,负数扣费', `type` TINYINT COMMENT '1充值 2消费', `create_time` DATETIME DEFAULT CURRENT_TIMESTAMP ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;

逻辑说明:openid加唯一索引是因为微信登录拿到的 openid 就是天然主键,重复插入会直接报错,省得你在代码里查一遍。order表的status用 TINYINT 而不是字符串,是为了后面用状态机判断时比较快,也省空间。wallet_log单独一张表,是因为钱的事必须留痕,答辩时老师最爱问「你怎么保证余额和流水对得上」,这张表就是答案。

参数说明:price_per_hour用 DECIMAL 不用 FLOAT,涉及金额一律 DECIMAL,这是血泪经验,FLOAT 算几次就会出现 0.30000000000000004 这种鬼东西。power同理。字符集统一 utf8mb4,别用 utf8,否则用户昵称里带 emoji 直接插入失败。

2.2 MyBatis 映射与 Service 事务边界

表建好之后,SSM 的套路是 Mapper 接口 + XML 映射。这里最容易翻车的是事务。充电订单的「结束充电」动作要同时做三件事:改订单状态、改充电桩状态、扣用户余额。这三步必须在一个事务里,否则扣了钱没改桩状态,桩就永远占用了。

@Service public class OrderServiceImpl implements OrderService { @Autowired private OrderMapper orderMapper; @Autowired private ChargerMapper chargerMapper; @Autowired private UserMapper userMapper; // 结束充电:改订单、释放桩、扣费,三步一个事务 @Override @Transactional(rollbackFor = Exception.class) public void finishCharge(Integer orderId) { Order order = orderMapper.selectById(orderId); if (order == null || order.getStatus() != 0) { throw new RuntimeException("订单状态异常,无法结束"); } // 1. 计算费用:按小时计费,不足一小时按一小时 long minutes = Duration.between(order.getStartTime(), LocalDateTime.now()).toMinutes(); long hours = Math.max(1, (minutes + 59) / 60); Charger charger = chargerMapper.selectById(order.getChargerId()); BigDecimal cost = charger.getPricePerHour().multiply(BigDecimal.valueOf(hours)); // 2. 扣余额,余额不足直接抛异常回滚 User user = userMapper.selectById(order.getUserId()); if (user.getBalance().compareTo(cost) < 0) { throw new RuntimeException("余额不足"); } userMapper.deductBalance(user.getId(), cost); // 3. 改订单状态和桩状态 orderMapper.finish(orderId, cost, LocalDateTime.now()); chargerMapper.updateStatus(order.getChargerId(), 0); } }

逻辑说明:@Transactional(rollbackFor = Exception.class)里的rollbackFor必须写,因为 Spring 默认只对 RuntimeException 回滚,你抛个受检异常它就不回滚了,这是新手最常踩的坑。费用计算用Math.max(1, ...)保证最低收一小时,业务上合理,也避免出现 0 元订单。

参数说明:deductBalance建议在 XML 里写成UPDATE user SET balance = balance - #{cost} WHERE id = #{id} AND balance >= #{cost},用 SQL 层面的条件更新防并发超扣,比在 Java 里先查再改安全得多。返回影响行数为 0 就说明余额不够,Service 里判断一下抛异常即可。

2.3 微信登录与 openid 换取

小程序端调wx.login拿到 code,传给后端,后端拿 code 去微信接口换 openid。这一步是前后端联调的第一个卡点,很多人卡在「code 已使用」上。

@RestController @RequestMapping("/api/auth") public class AuthController { @Value("${wx.appid}") private String appid; @Value("${wx.secret}") private String secret; @PostMapping("/login") public Result login(@RequestBody LoginDTO dto) { // code 只能用一次,且5分钟内有效 String url = "https://api.weixin.qq.com/sns/jscode2session" + "?appid=" + appid + "&secret=" + secret + "&js_code=" + dto.getCode() + "&grant_type=authorization_code"; String resp = HttpUtil.get(url); JSONObject json = JSON.parseObject(resp); String openid = json.getString("openid"); if (openid == null) { return Result.fail("登录失败:" + json.getString("errmsg")); } User user = userService.getOrCreateByOpenid(openid); // 生成 token 返回,后续请求带 token String token = JwtUtil.sign(user.getId()); return Result.ok(token); } }

逻辑说明:jscode2session这个接口的 code 是一次性的,前端如果重复用同一个 code 调两次,第二次必报 40163。所以前端拿到 code 后要立刻发请求,别缓存。getOrCreateByOpenid做的是「查不到就插入」,保证第一次登录自动注册。

参数说明:appid和secret千万别硬编码在代码里,放application.properties或配置中心,提交代码前检查一遍,泄露了别人能拿你的小程序额度。token 用 JWT 自包含用户 id,后端拦截器解析即可,不用存 session,小程序这种无状态场景更合适。

3. 小程序端怎么接:请求封装、扫码与状态刷新

3.1 请求封装与登录态维护

小程序原生wx.request用起来很啰嗦,每个页面都写一遍 header 和错误处理不现实。标准做法是封一层request.js,统一带 token、统一处理 401。

// utils/request.js const BASE_URL = 'https://your-domain.com/api'; function request(options) { return new Promise((resolve, reject) => { wx.request({ url: BASE_URL + options.url, method: options.method || 'GET', data: options.data || {}, header: { 'content-type': 'application/json', 'token': wx.getStorageSync('token') || '' }, success(res) { if (res.statusCode === 401) { // token 失效,清缓存回登录页 wx.removeStorageSync('token'); wx.reLaunch({ url: '/pages/login/login' }); return reject(new Error('未登录')); } if (res.data.code !== 200) { wx.showToast({ title: res.data.msg, icon: 'none' }); return reject(new Error(res.data.msg)); } resolve(res.data.data); }, fail(err) { wx.showToast({ title: '网络异常', icon: 'none' }); reject(err); } }); }); } module.exports = { request };

逻辑说明:把 token 放在 header 里而不是每次拼 URL,是因为后端拦截器统一从 header 取,干净。401 直接reLaunch回登录页,避免用户在失效状态下继续操作。res.data.code !== 200这层是业务错误码,和 HTTP 状态码分开,后端返回统一结构{code, msg, data}。

参数说明:BASE_URL上线必须是 https,微信小程序正式环境不允许 http。开发阶段可以在开发者工具里勾「不校验合法域名」,但真机预览会失败,所以尽早配好域名和证书。

3.2 扫码充电的完整链路

扫码是这类小程序的入口动作。wx.scanCode拿到桩编号,然后调后端「开始充电」接口,后端把桩状态改成占用、生成订单。

// pages/scan/scan.js const { request } = require('../../utils/request'); Page({ data: { charging: false, orderId: null }, onScan() { wx.scanCode({ onlyFromCamera: true, success: async (res) => { const chargerCode = res.result; // 二维码里存的是桩编号 try { const data = await request({ url: '/order/start', method: 'POST', data: { chargerCode } }); this.setData({ charging: true, orderId: data.orderId }); this.startPolling(); // 开始轮询充电状态 } catch (e) { // request 里已经 toast 过了,这里不用重复提示 } } }); }, // 每 5 秒查一次订单状态,充电结束或余额不足时停止 startPolling() { this.timer = setInterval(async () => { const data = await request({ url: '/order/status?id=' + this.data.orderId }); if (data.status !== 0) { clearInterval(this.timer); this.setData({ charging: false }); wx.showToast({ title: '充电已结束', icon: 'success' }); } }, 5000); }, onUnload() { if (this.timer) clearInterval(this.timer); // 页面卸载必须清定时器 } });

逻辑说明:onlyFromCamera: true强制只能拍照扫码,防止用户从相册选一张假二维码。轮询间隔 5 秒是折中,太短费电费流量,太长用户觉得卡。onUnload清定时器是必须的,否则页面关了定时器还在跑,内存泄漏,小程序虽然会回收但体验上会出问题。

参数说明:二维码内容建议直接存桩编号字符串,别存 JSON,扫出来还要解析一层容易出错。轮询接口要轻量,只返回 status 和 cost,别把整个订单对象返回。

3.3 支付与余额扣减的时序

充电结束后的支付有两种做法:预充值扣余额,或者微信支付直接付。毕设里推荐预充值模式,逻辑简单、闭环清晰。用户先充值到钱包,充电结束从余额扣。充值走微信支付,回调里加余额。

@PostMapping("/pay/notify") public String payNotify(HttpServletRequest request) { // 微信支付回调是 XML 格式,不是 JSON String xml = HttpUtil.readBody(request); Map<String, String> map = WxPayUtil.xmlToMap(xml); // 验签,防止伪造回调 if (!WxPayUtil.verifySign(map, apiKey)) { return "<xml><return_code><![CDATA[FAIL]]></return_code></xml>"; } if ("SUCCESS".equals(map.get("result_code"))) { String outTradeNo = map.get("out_trade_no"); BigDecimal amount = new BigDecimal(map.get("total_fee")).divide(new BigDecimal(100)); walletService.recharge(outTradeNo, amount); // 幂等处理在里面 } return "<xml><return_code><![CDATA[SUCCESS]]></return_code></xml>"; }

逻辑说明:微信支付回调是 XML 不是 JSON,很多人第一次接会懵。验签必须做,否则别人构造一个请求就能给你加余额。recharge里要做幂等,因为微信可能重复回调同一笔,用out_trade_no做唯一键,重复的直接返回成功不再加钱。

参数说明:total_fee单位是分,要除以 100 转成元。返回给微信的必须是 XML 格式的 SUCCESS,返回 JSON 微信会认为你没收到,一直重试。

4. 避坑与排查:这类毕设最容易翻车的五个点

4.1 现象:小程序真机请求全部失败,开发者工具正常

原因:开发者工具默认不校验域名,真机校验。你的BASE_URL要么是 http,要么域名没在小程序后台配 request 合法域名。

解决:登录小程序管理后台,开发设置里把后端域名加到 request 合法域名,必须是 https 且已备案。开发阶段真机调试可以在手机端开「调试模式」临时绕过,但答辩演示前一定配好,别赌。

4.2 现象:充电结束后余额扣了,但充电桩状态还是「占用」

原因:Service 方法没加@Transactional,或者加了但异常被 catch 吞了没往外抛,事务没触发回滚。

解决:确认@Transactional(rollbackFor = Exception.class)加在 public 方法上,且同类内部调用不生效(Spring AOP 代理问题)。扣余额和改桩状态必须在同一个 Service 方法里,别拆到两个 Service 互相调。

4.3 现象:并发扫码同一个桩,两个人都开始充电了

原因:start接口先查桩状态再改,查和改之间有窗口,两个请求都查到空闲。

解决:用条件更新,UPDATE charger SET status = 1 WHERE id = ? AND status = 0,判断影响行数,为 0 就说明被别人抢了,直接返回「该桩已被占用」。这是乐观锁思路,比 synchronized 靠谱,分布式下也成立。

4.4 现象:微信登录偶尔报 40163 code been used

原因:前端把同一个 code 发了两次请求,或者用户快速点了两次登录按钮。

解决:前端登录按钮点击后立即置灰,拿到 code 后只发一次。后端对同一个 code 的重复请求可以做短时缓存,第二次直接返回第一次的结果,但更简单的是前端防抖。

4.5 现象:金额计算出 0.30000000000000004 这种数

原因:用了 double 或 float 做金额运算。

解决:所有金额字段用 DECIMAL,Java 里用 BigDecimal,且new BigDecimal(0.1)这种构造是错的,要用new BigDecimal("0.1")字符串构造。除法必须指定精度和舍入模式,divide(new BigDecimal("100"), 2, RoundingMode.HALF_UP)。

5. 让这套毕设拿高分的一个技巧:把状态机画出来

答辩时老师最容易被问倒的地方是「你这个订单状态怎么流转的」。代码写得再花,说不清状态流转就是硬伤。我的习惯是在文档里放一张状态机表,比画图还清楚,老师一看就知道你想过边界。

当前状态触发动作目标状态前置条件
无扫码开始充电进行中(0)桩空闲且余额大于最低阈值
进行中(0)用户手动结束已完成(1)无
进行中(0)余额耗尽自动结束已完成(1)余额扣至 0
进行中(0)桩故障上报已取消(2)不扣费
已完成(1)再次结束拒绝状态非法

这张表往文档里一放,配合代码里的if (order.getStatus() != 0) throw ...,逻辑闭环就立住了。再进一步,你可以在order表加一个status变更日志表,每次状态变化插一条,答辩演示时能拿出「这笔订单从开始到结束经历了什么」的完整轨迹,比口头说强十倍。

我自己的教训是:第一次做这类项目时只顾着把功能跑通,状态判断散落在各个 Controller 里,后来加一个「取消订单」功能,改了五个地方还漏了一个,测试时出现「已取消的订单还能结束充电」这种玄学 bug。后来统一收口到 Service 的状态机方法里,所有状态变更只走一个入口,问题就没了。如果你时间只够做一件事,把状态流转收口,比多写两个页面值钱。

希望帮到你。

本文还有配套的精品资源,点击获取

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询