1. 项目总体设计与技术选型
做充电桩预约管理系统,最先要考虑的不是写代码,而是想清楚一个问题:这套系统到底要管哪些事?
电动车充电和汽油车加油不一样,充电时间动辄半小时到几小时,用户不可能一直守在车旁边。在充电资源紧张的区域,到了充电站发现充电桩被占、排队遥遥无期,体验非常差。所以预约系统的核心价值在于:让用户提前锁定充电桩和充电时间,到站即充,不用傻等。而运营方则需要一个管理端,实时掌握设备状态、预约订单、用户信息、收入流水。
我拿到这个标题第一反应是:这是个标准的“管理后台 + 用户端 + 服务端”三段式结构。前端选了Vue + ElementUi做PC管理后台,用户端走微信小程序,后端用Node.js提供接口服务。这套选型非常务实,没有炫技成分,每个环节都有明确的理由。
Vue + ElementUi:ElementUi本身就是Vue 2时代最成熟的中后台组件库,表格、表单、弹窗、标签页这些管理端的高频组件开箱即用,开发效率极高。做出来的界面规整统一,完全够用。
微信小程序:充电场景天然移动化,用户不可能坐在PC前去预约充电桩。小程序对比原生App的最大优势是不用安装、用完即走,扫码就能用,特别契合充电这种低频但刚需的生活服务场景。
Node.js:与前端同为JavaScript技术栈,前后端可以共享一些工具函数和数据校验逻辑,联调成本低。对于这种并发量不算夸张的业务系统,Node.js的异步IO模型完全能够胜任。
技术栈确认之后,我建议整个系统按两个端来规划代码结构:
- PC管理端:负责充电桩信息维护、预约订单管理、用户管理、计费规则配置、数据统计;
- 小程序端:负责用户注册登录、充电桩搜索、预约下单、支付、充电状态查看、历史订单。
后端统一暴露RESTful接口,两端共用同一套接口。一个有意思的细节是,如果PC端使用Vue 3,对应组件库应选择Element Plus而非ElementUi——ElementUi是Vue 2专属组件库,不能直接用在Vue 3项目里。很多新手在这里踩过坑,后续要单独说明。
提示:这个项目的核心技术难点不在单个功能,而在“预约冲突处理”和“时间片管理”。同一时间同一个桩只能被一个用户预约,前后端必须双重校验,才能防止超卖。
2. 数据库设计:核心表结构与关键字段说明
数据库设计是整个系统的地基。我见过太多项目,功能开发到一半发现表结构不合理,加班改表,连带改接口、改前端,非常痛苦。先把表设计想清楚,后面每一步都顺。
2.1 充电桩信息表
存储充电桩的静态属性和动态状态。静态属性包括桩的编号、名称、位置(经纬度和地址描述)、功率类型(快充/慢充)、接口类型(国标/欧标等),动态状态包括空闲、使用中、预约中、故障等状态字段。
这张表最需要留意的是“状态字段”的维护策略。充电桩状态实时性强,如果每次状态变更都直接更新主表,在高频请求下会产生大量写操作。实际项目中常见做法是主表只存静态信息,状态用Redis缓存维护,或单独建一张运行状态表。但对于预约充电这种场景,业务量级通常不大,直接在主表上维护状态字段,配合定时任务做状态巡检,更简单直接。
2.2 用户表
这里要区分两种角色:小程序端用户和PC管理端的管理员。我建议拆成两张表,因为字段差异太大。用户表核心字段有:
- openid(微信唯一标识)
- 手机号
- 昵称、头像
- 余额或账户状态
- 注册时间
管理员表则负责后台登录,存用户名、密码(加盐哈希)、权限等级。两张表混在一起会非常乱,给后面的鉴权逻辑增加不必要的复杂度。
2.3 预约订单表
业务核心中的核心。字段包括预约单号、用户ID、充电桩ID、预约开始时间、预约结束时间、实际开始充电时间、实际结束时间、订单状态、支付金额、创建时间。
预约状态流转是重点:待支付 → 已预约 → 充电中 → 已完成,此外还有已取消和超时未支付自动关闭。每一个状态节点都要有对应的时间字段,方便统计和分析。比如“超时未支付自动关闭”就需要记录关闭时间,后续运营要分析有多少订单因为未支付被释放。
2.4 计费规则表
充电计费策略不是简单的一口价。常见策略有:按度数计费、按时间计费、峰谷电价(不同时段电价不同)、快慢充不同单价。建议把计费规则做成可配置的,运营方可以在后台维护,而不需要改代码。
表设计可以拆成两级:规则主表和规则明细表。主表定义规则名称、适用桩型、生效时间段;明细表定义具体阶梯价格或时段价格。比如快充桩在工作日9:00-12:00每度1.2元、12:00-18:00每度1.5元,这些都在明细表里存。
2.5 充电记录表
区别于预约订单,充电记录更偏向设备运行数据:本次充电度数、充电时长、开始SOC、结束SOC、费用明细。每次充电完成之后生成一条记录,用于用户端展示充电报告、运营端做能耗统计。
经验之谈:预约订单表和充电记录表虽然有关联,但不要合并成一张表。订单关注的是交易流程,记录关注的是设备运行数据,两者查询频率和写入时机完全不同,合并后很难维护。
3. 后端接口设计与Node.js核心逻辑
接口是系统的骨架。设计接口时最忌讳想到哪写到哪,没有统一规范。我在实际开发中始终保持一个原则:先定义返回结构,再定义接口清单,最后才动手写代码。
3.1 接口返回结构统一
给前端返回的数据结构必须统一,不然前端做数据解析时会非常痛苦。我最常用的结构是这样:
{ "code": 0, "message": "success", "data": {} }code为0表示成功,非0表示业务异常。前端只需要在axios拦截器里统一判断code,就能处理大部分异常场景,不需要每个接口单独做错误处理。
3.2 核心接口清单
充电桩相关:
- 获取充电桩列表(支持按距离、功率、状态筛选)
- 获取充电桩详情
- 获取充电桩在指定时间段内的预约情况
预约订单相关:
- 创建预约订单(核心接口,要做并发控制)
- 取消预约
- 确认开始充电
- 确认结束充电并结算
用户相关:
- 微信登录(获取openid)
- 绑定手机号
- 查询我的预约记录
- 查询我的充电记录
3.3 并发控制:防止同一个充电桩被重复预约
这是后端最需要注意的逻辑。用户A和用户B同时提交某个桩某个时间段的预约请求,必须保证只有一个人能成功。
Node.js虽然单线程,但面对数据库操作时有异步回调,如果写成“先查有没有冲突,再插入订单”,很可能两个请求都查询不到冲突,然后都插入成功——这就产生了重复预约。
我采用的是“乐观锁 + 唯一索引 + 事务”三重保障方案。
第一步,在预约订单表上给“充电桩ID + 预约开始时间”建立唯一索引。数据库层面先拦截物理重复。
第二步,创建订单时用事务包裹,先用SELECT ... FOR UPDATE锁定充电桩记录,再查询冲突订单,最后插入。这样两个并发请求到达时,后一个会等待前一个事务结束,再执行查询,此时已经能看到冲突记录,从而拒绝下单。
第三步,业务代码里再做好时间窗口判断:预约结束时间必须大于开始时间、预约时间不能与已有订单重叠等。三层校验叠加,基本可以杜绝超卖。
需要说明的是,这个项目属于轻量级业务,不推荐直接引入消息队列或分布式锁,会大幅增加系统复杂度。数据库层的锁机制足够解决这个问题,维护成本也低。
3.4 微信登录机制
小程序端用户首次进入时,通过wx.login()获取临时code,后端拿到code后调用微信接口换取openid。openid是用户在微信体系内的唯一标识,作为用户表的主键字段使用。
需要注意的一点:code换openid的接口需要在小程序后台配置服务器域名白名单,上线前必须把后端接口域名配置为HTTPS,否则真机调试会直接失败。这个细节经常被忽略,导致本地开发好好的,一上线就登录失败。
登录态维护方面,我采用JWT方案。后端在换取到openid后签发JWT,小程序端把token存到storage中,后续请求在请求头里带上Authorization: Bearer <token>,后端用中间件统一校验。
4. 微信小程序端与PC管理端方案设计
4.1 小程序端技术方案:原生还是uni-app
在真正动手写小程序之前,要先做一个关键决策:用微信原生语法,还是用uni-app跨端框架。
参考标题“vue+nodejs+ElementUi”,项目技术栈以Vue为核心。如果小程序端也要求用Vue语法,选择uni-app会非常自然——开发时写的还是Vue代码,但编译产物是各平台小程序。尤其当团队本来就会Vue时,上手成本极低。
但也要考虑实际情况:
- 原生小程序的性能和调试体验更好,真机兼容问题更少;
- uni-app对Vue 3的支持早已成熟,同一套业务代码后续还可编译到其他平台,扩展性强。
从我个人的建议来看,如果要做的是纯微信端产品,原生语法完全够了;但如果是商业项目,后续有拓展支付宝、抖音小程序甚至App的可能,直接用uni-app更省心。这里按uni-app方案展开,沿用Vue语法会让从PC端过来的开发者感到非常顺。
4.2 小程序端核心页面设计
首页:基于wx.getLocation()获取当前定位,调用后端接口拉取附近的充电桩列表,用地图标注或列表展示。充电桩状态用颜色区分:绿色空闲、橙色预约中、灰色故障。
预约页:用户选择充电桩后可查看该桩的状态时段表,挑选时间段提交预约。这里前端要限制:只能选择未来某个时间段,且不能跨天。这种限制能减轻后端的时间冲突处理压力,把简单问题在页面层解决。
充电页:用户到达充电桩,扫码或点击“开始充电”,后端创建充电任务,返回实时数据(电压、电流、度数)的轮询接口。页面用ECharts或uCharts展示充电趋势图。
个人中心:我的预约单、我的充电记录、余额充值、意见反馈。
4.3 PC管理端技术方案与页面布局
管理端使用Vue + ElementUi + Vuex + Vue Router + Axios。这套组合是PC中后台的标配,脚手架可以直接用Vue CLI或Vite创建。
布局采用经典的侧边栏菜单 + 顶部栏 + 内容区域结构。菜单项规划为:
- 仪表盘:核心数据卡片 + 充电量趋势图
- 充电桩管理:列表、新增、编辑、故障维护
- 预约管理:全部订单、待支付、进行中、已完成、已取消,支持按桩、按用户、按日期筛选
- 用户管理:用户列表、账户余额变动记录
- 计费管理:费率规则配置
- 数据统计:按日/周/月维度统计充电次数、营收、平均充电时长
- 系统设置:管理员账号、操作日志
这里的重点不太在页面UI本身,而在权限控制。不同管理员应该拥有不同权限,比如普通运营人员只能看订单,不能修改费率。ElementUi中可以用路由守卫配合动态路由实现,菜单根据当前用户的权限动态渲染。
4.4 接口联调与跨域处理
前后端分离开发中最常见的问题就是跨域。开发环境下,Vue默认跑在8080端口,Node.js跑在3000端口,前端请求后端必然跨域。
后端用Express或Koa框架时,直接安装并启用cors中间件,就能解决开发环境下的跨域问题。需要注意生产环境的配置:HTTPS域名下的小程序请求,不受浏览器跨域限制,但PC端管理后台如果部署在独立域名,依然要处理好跨域,或者用Nginx反向代理将/api路径转发到Node服务,这样前端请求同源地址,后端无需开启跨域。
5. 预约流程与充电业务流程实现
5.1 预约流程的完整状态流转
从用户角度,一次完整的预约流程大概是:
- 用户搜索到目标充电桩,查看可预约时间段;
- 选定时段提交预约,系统创建订单;
- 订单状态为待支付,创建后15分钟内需完成支付;
- 支付完成后状态变更为已预约;
- 到达充电站后,用户扫码或点击“开始充电”,状态变为充电中;
- 充电完成或用户主动结束,系统结算金额,状态变为已完成。
这里有一个容易被忽略的产品设计问题:预约是否要收费?我的方案是预约本身免费,但从“预约成功”那一刻起,用户的支付账户做配额占用(或预授权冻结),这样一来可最大限度地降低恶意预约的比例,同时预约爽约成本低,不会劝退真实用户。如果用户预约了但未在约定时间到达,超时15分钟后系统自动释放充电桩,同时记一次违约记录。连续多次违约的用户,系统限制其后续预约权限。
5.2 计费结算逻辑实现
结算并非简单的单价×度数。考虑到峰谷电价,实际逻辑可能是:
用户在峰期充电,电量30度,单价1.2元;充完电进入谷期时间后,单价变成0.8元,那么费用必须分段计算。
数据表里的sol记录每次充电的起止时间,在结算时后端需要把充电时长按照电价规则的时段切片,逐段计算费用累加。这个算法在后端实现时,提前把当天费率规则按时间排序,遍历充电时间区间,切割重合区间乘对应的单价。
这是个商业逻辑上的关键点。如果忽略峰谷分段计价,直接按结束时刻的单价统一计算,定价会出现明显偏差。后台需要设置“当前生效费率”,建议新增费率规则带时间戳并采用生效时间顺序排列,避免并发请求读到互斥规则。
5.3 充电桩状态实时同步
小程序端展示充电桩“使用中”“空闲”状态,背后的状态同步需要特别处理。常见方案有两种:
第一种:设备主动上报。充电桩硬件通过MQTT或HTTP轮询,每隔一段时间上报状态到后端,后端更新数据库,前端通过轮询或WebSocket获取最新状态。
第二种:业务驱动更新。开始充电时置为“使用中”,结束充电时置为“空闲”,预约成功但未开始的前置状态为“预约中”。
对于没有硬件基础、以Demo演示为主的情况,第二种就足够。但需要注意:系统异常退出、网络断开等情况可能导致状态卡死。建议增加一个定时巡检任务,每5分钟扫描一次所有“使用中”但超过最大充电时长的记录,强制置为结束状态。
6. 常见问题与排障速查
6.1 小程序真机预览一片空白
多半是域名白名单或HTTPS没配置。开发工具里勾选了“不校验合法域名”才能跑通,但真机上不生效。必须把后端接口部署到HTTPS域名,且在微信公众平台配置服务器域名,其中request合法域名填后端地址,uploadFile合法域名填文件上传地址。
6.2 管理端首次登录后页面空白
Vue项目最常见的原因:路由模式为history,但部署在Nginx下未配置try_files重写。用户刷新管理页面的某个子路径,Nginx找不到对应的文件,直接返回404。在Nginx的location /中配置:
try_files $uri $uri/ /index.html;这个问题上百个开发者踩过同一条坑,建议部署时直接写在配置里,省得排查。
6.3 同一个桩同时被两个人预约成功
参考上文的事务加唯一索引方案。如果用MySQL,还要注意:事务隔离级别必须是READ_COMMITTED或更高,在低隔离级别(如READ_UNCOMMITTED)下,即使加了SELECT ... FOR UPDATE,也可能读到未提交的数据,造成判断失误。
6.4 支付回调丢失
微信支付成功后,会异步通知后端服务器。如果后端在回调中处理订单状态时发生异常,或者网络超时导致回调没有送达,订单会一直挂在“待支付”。必须实现回调幂等处理,并在用户端提供一个“刷新订单状态”的操作,主动向微信查询支付结果。
7. 部署建议与踩坑记录
环境准备:Node.js选择14以上版本,MySQL选择5.7或8.0,小程序端用微信开发者工具。服务器用一台2核4GB内存的云主机就足够承载初期业务量。
前端打包:PC管理端执行npm run build,生成dist目录,通过Nginx部署为静态文件。小程序端在开发者工具中上传代码,到微信公众平台提交审核。
后端部署:建议用PM2做进程守护,保持Node服务常驻。如果业务量增长快,可以在前面加一层Nginx负载均衡,或者使用PM2的cluster模式启动多进程。
数据库运维:每天做好自动备份,至少保留最近7天的备份文件。数据是业务的生命线,主机崩溃时可以快速恢复。
整个项目做下来,我印象最深的是预约并发冲突的处理。技术上并没有高深的地方,数据库锁应用得当,配合合理的业务校验逻辑,问题完全可以控制在数据库层面解决。很多人一听到并发就想到上消息队列、上分布式锁,对小项目来说这都是过度设计。
另外一点感想是:技术选型只要不反主流,怎么选都不会错,真正的复杂度永远在业务逻辑里。计费规则、预约状态流转、爽约处理,这些业务细节占了项目80%的精力。把业务规则梳理清楚,代码实现反而是水到渠成的事情。
最后分享一个小技巧:开发这类管理系统时,建议先拟定一份契约文档,把接口地址、请求参数、返回字段先固定下来。前后端并行开发时联调效率能提升好几倍,不用等后端全部写好前端才能动工。这份契约文档本身,就是你在项目交流中拿得出手的产物。