☰
停车场微信小程序源码:车位预约+支付闭环全解析
2026/10/23 14:04:51 网站建设 项目流程

简介:一套面向停车场运营场景的微信小程序源码工程,适合计算机相关专业毕业设计、小程序开发入门与二次开发实践,可快速搭建包含车位展示、地图定位等模块的移动端应用原型。压缩包共收录393个文件,以png图片、json配置、js逻辑、wxml页面结构、wxss样式及vue组件为主,另有map地图文件、scss/less样式辅助以及md说明、license授权信息,整体4.11MB,目录层级清晰,便于按页面或功能模块检索学习。已有469人浏览/下载,属于轻量级高复用的小程序参考项目。资料内含完整移动端页面源码与配套逻辑,重点覆盖地图组件使用、页面交互与样式适配,可直接导入开发工具运行调试;结合现有代码还能理解uniapp项目结构、数据绑定与组件化开发思路,为后续扩展或毕业设计答辩提供可展示的工程基础。

1. 停车场微信小程序源码拆解:一个能直接答辩、能上线的车位预约闭环

凌晨一点,物业在群里说出口道闸不抬杆,后面积了七八台车。你打开手机想远程看一眼车位状态是不是没同步——这个念头,就是这套停车场系统微信小程序源码存在的意义。它不是一个只有静态页面的壳子,而是一条完整的业务闭环:车主在小程序里查空闲车位、预约锁位、支付离场费用,后端用 Java 处理车位状态流转和微信支付回调,MySQL 落表。这几年毕业设计里出现频率很高的一条真实业务线,拿来改改就能当毕设主项目,或者拆成模块塞进外包项目里直接复用。

适合两类人:一类是拿它做课程设计、毕业设计的学生,答辩时能讲清楚“并发锁位”和“支付回调幂等”就够有深度了;另一类是刚接单的开发者,需要一个可运行、可二开的停车场小程序底座,省掉从零画原型和设计表的时间。整个源码包分小程序前端和 Java 后端两大块,数据库脚本、接口文档都在压缩包里,按后文步骤跑起来,二十分钟内能看到第一个真实车位页面。

2. 整体架构与技术栈:三层分离,前端不背锅

2.1 前后端分离怎么分:小程序、Java 后端、MySQL 的关系

这套项目走的是最标准的微信小程序 + Spring Boot 单体后端结构,没有引入微服务那套复杂东西,但层次非常清晰。小程序端只负责展示和交互,所有业务判定都交给后端接口:用户点“预约”按钮,前端把车位 ID 传给后端,后端判断车位是否空闲、能不能预约、预约后多久过期,再把结果回给前端。好处是业务逻辑集中,后续如果要加安卓壳或者接第三方硬件,只动接口调用,不用重写判断逻辑。

前端用微信小程序原生框架,WXML + WXSS + JavaScript,直接在微信开发者工具里打开就能编译。后端是典型的 Spring Boot 分层结构:Controller 只做参数收口,Service 放业务规则,Mapper 用 MyBatis-Plus 操作 MySQL。我一般接这种带小程序的源码项目,第一件事就是确认后端提供的接口跟小程序里 request 的 url 是否能对上,这套源码的接口路径是统一过一遍的,省掉不少联调时间。

parking-lot/ ├── backend/ # Java 后端工程 │ ├── src/main/java/ │ │ └── com/parking/ │ │ ├── controller/ # 接口层:登录、车位、订单、支付 │ │ ├── service/ # 业务层:锁位、计费、回调处理 │ │ ├── mapper/ # 数据访问,配合 MyBatis 注解与 XML │ │ ├── entity/ # 数据库实体:车位、订单、用户 │ │ ├── config/ # 拦截器、全局异常、支付配置 │ │ └── common/ # 统一响应体、分页工具 │ └── src/main/resources/ │ ├── application.yml # 数据源、端口、微信 appid 配置 │ └── mapper/*.xml # 手写 SQL 存放目录 ├── miniprogram/ # 微信小程序前端 │ ├── pages/ │ │ ├── index/ # 首页:车位列表、空闲状态 │ │ ├── reserve/ # 预约、入场、结算支付 │ │ ├── wallet/ # 钱包、订单记录、发票信息 │ │ └── mine/ # 我的车牌、个人设置 │ ├── utils/request.js # 封装 wx.request,统一带 token │ └── app.js └── sql/ └── parking.sql # 建库建表脚本,含初始数据

2.2 目录结构与核心文件:拿到源码先看哪几个文件

拿到压缩包后建议按这个顺序读代码,能少走弯路。先开sql/parking.sql把表结构过一遍,因为停车场的核心就在车位状态和订单状态两个字段上,表设计看明白了,业务代码基本猜得八九不离十。然后看backend/src/main/resources/application.yml,改 MySQL 连接串和微信小程序 appid 配置,这是启动后端的前置条件。最后再翻miniprogram/utils/request.js,小程序端所有请求都从这里发出,统一处理了 token 注入和错误提示。

这里要特别强调一个关键点:小程序端utils/request.js里用的 baseUrl 不是线上域名,而是一段本地开发用的http://127.0.0.1:8080之类的地址。微信开发者工具默认不允许请求 http 接口,需要在“详情 - 本地设置”里勾选“不校验合法域名”,否则一启动就是白屏或者报 url not in domain list。这个问题排在第 5 章的避坑清单里,但建议现在就知道,因为几乎每个新手都会在这里卡一次。

server: port: 8080 spring: datasource: url: jdbc:mysql://localhost:3306/parking?useUnicode=true&characterEncoding=utf8&serverTimezone=Asia/Shanghai username: root password: 123456 wx: appid: 你的小程序appid secret: 你的小程序secret mch-id: 微信支付商户号 pay-key: 支付API密钥

把 application.yml 里的wx段单独拎出来说,是因为它直接决定了微信登录和支付能不能跑通。appid和secret是小程序维度的,用户在开发者后台“开发管理 - 开发设置”里能找到;mch-id和pay-key是微信支付商户号维度的,如果不涉及真实商户,支付流程可以用测试商户号或者把支付按钮的调用暂时注释掉,不影响车位预约主流程联调。

2.3 数据库设计:五张表撑起一套计费闭环

这套核心表控制在八张以内,删掉日志和审核冗余表后,核心业务其实就五张,特别适合拿去答辩。用户表、车牌表、车位表、预约记录表、订单表,五张表相互咬合,支撑起从预约到支付的全部状态流转。

表名关键字段状态值设计作用
memberid, openid, nickname, phone无小程序用户主体
carid, member_id, plate_no无绑定车牌,出场校验用
parking_spaceid, lot_id, status, position1空闲 / 2预约 / 3占用车位状态流转核心
reservationid, space_id, member_id, enter_time, expire_time0进行中 / 1已完成 / 2已取消预约锁位记录
pay_orderid, reservation_id, order_no, amount, status0待支付 / 1已支付 / 2已退款支付与计费落地

parking_space.status是整套系统最敏感的一个字段,车位状态没变化,后面结算、入场校验全都会跟着乱。源码里所有修改状态的操作都放在事务内执行,这样设计是为了避免并发请求把车位状态改丢。reservation.expire_time是预约超时时间,超过这个时间没入场,车位自动释放回空闲,这种机制在真实停车场里非常常见。

注意:改表结构或者加索引时,优先保证parking_space.status和pay_order.status这两个字段上有索引。它们出现在 where 条件里的频率最高,加索引对高并发场景的收益明显。

3. 核心后端拆解:并发锁位、计时计费、支付回调解读

3.1 车位查询与锁位:不让两个用户抢到同一个车位

停车场业务最简单的玩法就是“查列表 - 点预约 - 缴费离场”,但这套源码有意思的地方在于把“锁位”单独做了一步。查询空闲车位的接口很常规,一条带状态的 SQL 加一个 limit 控制一次查询量,避免小程序端一次拉太多数据。

SELECT id, lot_id, position, status FROM parking_space WHERE lot_id = #{lotId} AND status = 1 LIMIT #{limit}

这条 SQL 的逻辑很简单,但要注意status = 1这个条件必须和业务层配合,不能只靠前端隐藏按钮来控制。用户在页面看到空闲车位后,如果犹豫了十几秒再点预约,期间车位可能已经被别人锁走。所以后端的预约接口必须做真正的状态校验和行级锁,这也是这套源码里含金量比较高的部分。

@Transactional public ReserveResult lockSpace(Long spaceId, Long memberId) { // 第一层校验,先确认车位存在且空闲 ParkingSpace space = parkingSpaceMapper.selectById(spaceId); if (space == null || space.getStatus() != ParkingSpace.STATUS_FREE) { throw new BizException("车位状态异常,请刷新后重试"); } // 行级锁:把这条车位记录锁住,防止并发同时读到空闲状态 ParkingSpace locked = parkingSpaceMapper.selectByIdForUpdate(spaceId); if (locked.getStatus() != ParkingSpace.STATUS_FREE) { throw new BizException("手慢了,车位被预约了"); } // 第二层:更新车位状态为预约 locked.setStatus(ParkingSpace.STATUS_RESERVED); parkingSpaceMapper.updateById(locked); // 生成预约单,15 分钟内未入场自动释放 Reservation reservation = new Reservation(); reservation.setSpaceId(spaceId); reservation.setMemberId(memberId); reservation.setEnterTime(LocalDateTime.now()); reservation.setExpireTime(LocalDateTime.now().plusMinutes(15)); reservationMapper.insert(reservation); return new ReserveResult(reservation.getId(), reservation.getExpireTime()); }

这段代码的核心思路是“先读再校验再写”,并且借助selectByIdForUpdate把行锁拿到手里。注解@Transactional保证这三个步骤要么全部成功、要么全部回滚,不会出现“车位改成预约了但预约单没生成”的脏状态。这里说明一下selectByIdForUpdate的原理:它在数据库层面给选中的行加上排他锁,直到事务提交才释放,另一个线程来读同一条记录时只能等待,从根上避免两个用户同时提交预约。如果去掉这层锁,只靠状态判断,并发高的时候大概率会出现超卖——两个用户都以为自己预约成功了。

提示:selectByIdForUpdate必须放在事务里才生效。如果你在 Service 方法上没有加@Transactional,锁不会持续到提交阶段,相当于白写。

3.2 计费规则实现:不足 15 分钟按 15 分钟,封顶逻辑是这样算的

计费是停车场系统的门面功能,用户可能不在乎你的表结构设计得漂亮不漂亮,但一定在乎多收没多收钱。这套源码的计费规则可配置,按单价、计费单位、每日封顶三个参数控制,适合不同场地。核心公式是:停车时长除以计费单位向上取整,再乘以单价,最后跟封顶值取较小的一方。

public BigDecimal calculateAmount(LocalDateTime enterTime, LocalDateTime leaveTime) { long minutes = Duration.between(enterTime, leaveTime).toMinutes(); // 不足一个计费周期按一个周期算 long periods = (minutes + CHARGING_UNIT - 1) / CHARGING_UNIT; BigDecimal amount = unitPrice.multiply(BigDecimal.valueOf(periods)); // 每日封顶逻辑:超过封顶金额后不再累加 if (amount.compareTo(dailyCap) > 0) { amount = dailyCap; } // 金额保留两位小数,避免前端出现 0.30000000000000004 这种问题 return amount.setScale(2, RoundingMode.HALF_UP); }

(minutes + CHARGING_UNIT - 1) / CHARGING_UNIT这个写法是取整除法的向上取整技巧,比Math.ceil更直观。参数说明:CHARGING_UNIT是计费单位分钟,比如 15;unitPrice是单位价格,用BigDecimal而不是double,避免浮点数精度翻车;dailyCap是每日封顶金额。有点良心的停车场都会配封顶价,不然停一天几十块的计费谁受得了。这套源码把三个参数写进了数据字典,运营人员可在后台配置,不用改代码。

计费相关的另一个点是“预约超时释放”。用户预约后 15 分钟内不入场,车位要自动释放回空闲状态,不然一个车位会被一个没有到场的人占住一整天。源码实现方式是定时任务扫描 reservation 表,将超过 expire_time 且 status 还是 0 的记录改为已取消,同时把对应 parking_space 的 status 改回 1:

@Scheduled(fixedDelay = 30000) public void releaseExpiredReservation() { List<Reservation> list = reservationMapper.selectExpired(); for (Reservation item : list) { item.setStatus(Reservation.STATUS_CANCELLED); reservationMapper.updateById(item); ParkingSpace space = parkingSpaceMapper.selectById(item.getSpaceId()); space.setStatus(ParkingSpace.STATUS_FREE); parkingSpaceMapper.updateById(space); } }

这套实现看起来直接,但要注意任务执行频率:fixedDelay = 30000表示上一次任务执行完成后间隔 30 秒再跑下一次。如果业务量很大,定时频率太密会把数据库拖慢,太疏又会让用户产生“明明预约过期了车位还锁着”的投诉。常见做法是把扫描范围限制在本小时内到期的记录,配合 expire_time 上的索引,查询压力可控。

3.3 支付回调解读:幂等处理防微信重复回调

支付部分是这套系统里最能看出代码功底的地方。微信支付回调机制是“不确定性的”,同一个支付结果可能会推送多次,后端如果处理不当,用户支付成功后订单状态被更新两遍,甚至出现金额翻倍。

@Transactional public String handleWechatPayNotify(String orderNo, String transactionId) { PayOrder order = payOrderMapper.selectByOrderNo(orderNo); // 已经处理过,直接返回成功,防止重复回调 if (order.getStatus() == PayOrder.STATUS_PAID) { return WxPayResult.success(); } // 比对数据库中的金额和微信回调里的金额,不一致直接拒绝 if (!order.getAmount().equals(callbackAmount)) { return WxPayResult.fail("金额不一致"); } order.setStatus(PayOrder.STATUS_PAID); order.setTransactionId(transactionId); order.setPaidTime(LocalDateTime.now()); payOrderMapper.updateById(order); // 支付成功,释放车位并记录入场 parkingSpaceMapper.finishReservation(order.getReservationId()); return WxPayResult.success(); }

这段代码最关键的是第一层判断:order.getStatus() == PayOrder.STATUS_PAID时直接返回成功。微信收到成功响应后就不会再重发回调,而这一层判断同时承担了幂等和防重复更新两个职责。但这里有一个真实的血泪经验:如果只判断状态再更新,在高并发下两个回调线程可能同时读到 status 为 0,然后同时去执行更新。更稳的写法是直接执行一条带条件的 UPDATE 并检查影响行数,比如UPDATE pay_order SET status = 1 WHERE order_no = ? AND status = 0,如果影响行数为 0 说明已经处理过。源码里两层都做了,事务加条件更新,单看哪一层都不容易出问题,合起来才稳妥。

4. 小程序前端实战:登录、车位状态轮询与支付拉起

4.1 微信登录:code 换 token,别在前端碰 appSecret

小程序端的登录逻辑跟普通网页登录不一样,关键区别在于:小程序不能直接拿appid + appSecret去微信接口换 openid,那样会把密钥暴露在用户手机里,任何人反编译小程序都能拿到你的 secret,风险很大。正确的姿势是前端先用wx.login拿一个临时凭证 code,再把 code 传给后端,由后端拿着 code 加自己的 appSecret 去微信服务器换 openid 和 session_key。

// 登录:wx.login 拿 code,后端换 openid,前端只存 token const login = async () => { const { code } = await wx.login(); const res = await request({ url: '/api/wx/login', method: 'POST', data: { code } }); if (res.code === 0) { wx.setStorageSync('token', res.data.token); wx.setStorageSync('memberId', res.data.memberId); } else { wx.showToast({ title: '登录失败', icon: 'none' }); } };

后端处理这个接口时,实际上就是拿前端传来的 code 去调微信的jscode2session接口,换取 openid 后在 member 表里找到或创建用户,再生成一个自定义 token 返回前端。之后的所有请求都在 header 里带上这个 token,后端拦截器解析 token 得到用户身份。要注意 token 的有效期设计,源码里默认是七天,如果用户长期不用再打开需要重新登录,这是常见做法,不要设计成永不过期,安全上容易留下隐患。

4.2 车位列表与状态轮询:10 秒一次的实时感

用户打开首页需要看到当前哪些车位是空闲的、哪些被预约、哪些被占用。小程序端不能像 App 一样用长连接推送,最常见的实现就是轮询:每隔若干秒请求一次车位列表,把最新的状态渲染到页面上。

// 车位状态轮询,每 10 秒拉一次全量列表 const startPolling = () => { if (this.pollTimer) { clearInterval(this.pollTimer); } this.pollTimer = setInterval(async () => { const res = await request({ url: '/api/parking/list', method: 'GET' }); if (res.code === 0) { // 全量替换,避免 setData 增量更新带来脏数据 this.setData({ spaceList: res.data.list }); } }, 10000); }; const stopPolling = () => { if (this.pollTimer) { clearInterval(this.pollTimer); this.pollTimer = null; } };

这里有个很容易被新手忽略的点:轮询定时器在页面隐藏或者退出时必须清除,否则页面走了定时器还在后台跑,既浪费用户流量,还可能在你已经离开页面后弹报错。源码里在onShow里启动轮询,在onHide和onUnload里停止,逻辑闭环。每次轮询全量替换列表数据,数据量不算大,最多几十个车位,性能上完全没问题;如果你要接一个几百车位的大场,建议改成只返回状态变化的车位增量数据,或者加一个 version 字段做状态对比,减少 setData 的开销。

4.3 支付拉起:wx.requestPayment 的标准姿势

小程序端调起微信支付的代码其实很薄,真正有含金量的是后端把统一下单参数算好签名返回给前端。前端拿到参数后,调用微信提供的wx.requestPayment,这一步不需要你做任何金额计算,金额在后端算好并签名,小程序端只是把这些参数原样传给微信支付。

const pay = async (orderNo) => { const res = await request({ url: '/api/pay/unifiedOrder', method: 'POST', data: { orderNo } }); if (res.code !== 0) { wx.showToast({ title: res.msg || '下单失败', icon: 'none' }); return; } wx.requestPayment({ timeStamp: res.data.timeStamp, nonceStr: res.data.nonceStr, package: res.data.packageValue, signType: 'RSA', paySign: res.data.paySign, success: () => { wx.showToast({ title: '支付成功' }); // 刷新订单状态 this.fetchOrderDetail(orderNo); }, fail: () => { wx.showToast({ title: '已取消支付', icon: 'none' }); } }); };

这段代码的注意点集中在参数名上:后端返回的参数timeStamp是字符串类型,而小程序端要求的是字符串,别传成数字,类型不一致微信会直接报requestPayment:fail。package是关键词,不能直接作为变量名,所以代码里用packageValue接收再转交给wx.requestPayment。signType的值取决于后端签名方式,现在微信支付 APIv3 很多场景默认 RSA。如果支付签名不对,优先去后端日志里看微信返回的具体错误码,比在前端瞎猜快得多。

5. 本地运行与常见问题排查:从白屏到计费异常的五个坑

5.1 坑一:小程序请求直接被平台拦截,报 url not in domain list

现象:小程序编译后,页面能打开,但所有请求都失败,控制台提示url not in domain list。原因:微信小程序安全策略默认不允许请求 http 接口或未配置的域名。解决:在微信开发者工具里点击“详情”,找到“本地设置”,勾选“不校验合法域名、web-view(业务域名)、TLS 版本以及 HTTPS 证书”。如果真机预览依然报错,需要在小程序后台的“开发管理 - 服务器域名”里配置 request 合法域名,本地开发工具勾选不校验只对模拟器有效。

5.2 坑二:登录态一重启就失效,每次打开小程序都要重新登录

现象:后端明明启动正常,数据库里有用户数据,但每次冷启动小程序后 token 都是空的,或者登录后接口依然返回 401。原因:常见有两种,一种是前端把 token 存在内存变量里而不是wx.setStorageSync,小程序一旦切后台或被回收,变量被重置;另一种是后端把 token 存内存 Map,后端一重启所有 token 就全没了。解决:前端统一走utils/request.js,每次从 storage 读取 token 注入 header;后端用 Redis 存 token 并设置过期时间,或者用 JWT 无状态方案,让后端重启不影响用户登录态。

5.3 坑三:并发预约同一车位,出现两个成功订单

现象:两个用户同时点同一个空余车位的预约按钮,两个人都收到“预约成功”的提示,但车位只有一个。原因:去掉selectByIdForUpdate之后,两个请求同时读到 status 为 1 的空闲状态,同时走了后面的更新逻辑,数据库层面没有互相等待。解决:给预约接口加回行级锁,或者用乐观锁UPDATE parking_space SET status = 2 WHERE id = ? AND status = 1,再检查影响行数,如果为 0 说明车位已被抢走,直接返回失败。这是并发类 Bug 里最容易现场演示的典型案例,也是答辩时最容易被追问的点。

5.4 坑四:支付回调重复执行,用户被扣两次钱或者订单状态被重复更新

现象:用户明明只支付了一次,后台订单记录里却出现重复的状态流转,或者钱包余额被扣减了两次。原因:微信支付回调在网络抖动时会自动重试,回调接口收到同一条支付结果多次,后端每次都没有做幂等判断,直接执行业务逻辑。解决:如 3.3 节所写,处理回调前先查订单当前状态,已经支付的直接返回成功;另外给 pay_order 表的order_no加唯一约束,从数据库层面兜底,哪怕代码逻辑有漏洞,第二次插入也会被数据库拒绝。

5.5 坑五:MySQL 中文乱码和时区差八小时

现象:页面上显示的车位位置和用户昵称全是问号,数据库里直接看也是乱码;或者订单创建时间比本地时间早了八个小时。原因:MySQL 连接串里没带characterEncoding=utf8和serverTimezone=Asia/Shanghai,MySQL 服务端默认字符集不是 utf8mb4,时区用了系统默认 UTC。解决:像第 2 章 application.yml 里写的那样,在数据源连接串中显式加上useUnicode=true&characterEncoding=utf8&serverTimezone=Asia/Shanghai,同时把 MySQL 表结构字符集统一改成 utf8mb4。改完这些参数后,记得重启后端服务并重新执行建表脚本,否则已经建好的表不会自动迁移。

提示:这几个坑在换数据库、换部署环境时最容易复发,即使之前跑通过,换到另一台机器上也可能重新踩一遍。每个项目拿到手先按这五条过一遍,能省出大半天的排查时间。

6. 二次开发建议:给这套系统加月卡、加场、加车牌识别

6.1 先加一张月卡表,区分免费和计费入口

停车场系统最常见的二开需求就是加月卡会员。思路不复杂:新增一张 member_card 表,记录车牌号、生效日期、失效日期;出场结算时,先判断当前车牌是否在有效月卡范围内,如果是就直接放行并生成一条零金额订单,否则走原有的按次计费逻辑。

public boolean hasActiveMonthlyCard(String plateNo) { LocalDate today = LocalDate.now(); MonthlyCard card = monthlyCardMapper.selectActive(plateNo, today); return card != null; }

这个接口对接出场逻辑时,注意月卡判断要在计费计算之前做,而且要区分“月卡车辆入场未绑定车牌”的情况。我一般会在 entance 接口里强制校验车辆和车牌是否绑定,如果车牌没绑,入场时就提示用户先去小程序绑定,避免出场时月卡判断找不到对应关系。

6.2 往无人值守的方向改:对接车牌识别相机

如果手头有硬件条件,可以继续把项目改成无人值守模式。方�象是:道闸相机识别到车牌后,调用后端出场接口查询该车牌是否有预约、是否在免费时段、是否月卡。这套源码的接口设计是按“车位 ID”维度做的预约,改造时可以先加一个 plate_no 字段到 reservation 表,让预约记录和车牌强关联,再给入场出场接口各加一个按车牌查询的版本。硬件相机厂商的接口各不相同,但后端只需要暴露一个“根据车牌查询订单并结算”的接口,剩下的是在硬件端把识别结果拼装成请求。

从那以后,我每次拿到电商、停车、预约类带支付闭环的项目,都会强制先走一遍支付幂等核对、并发锁位检查、数据库连接参数确认这三件事,再动手改业务代码。这套停车场系统微信小程序源码的价值不在于代码量有多大,而在于它把真实业务里最常见的坑都过了一遍,照着源码里的写法扩展新功能比自己从零搭要省力得多。希望帮到你。

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

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

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

立即咨询