简介:一套基于微信开发者工具、Java与MySQL实现的车位预约微信小程序完整源码,适合微信小程序学习者和毕业设计、课程设计人员参考。项目包含用户端与管理端:用户可登录后查看停车场、资讯、停车记录、个人中心并提交预约;管理员可管理用户、停车场、停车记录与资讯信息。资源包共981个文件,大小约9.87MB,除png、svg、gif等大量图片素材外,还包含js、java、xml、sql等前后端代码与数据库脚本,以及wxml、wxss等小程序关键文件,目录结构完整清晰。源码已在测试环境验证可正常运行,下载后可直接导入微信开发者工具,并结合Java后端与MySQL数据库完成部署,便于快速掌握小程序开发流程、前后端数据交互及功能模块设计,适用于课程设计、项目实战或二次开发。该资源已有530人浏览学习,人气稳定。
1. 从“提前锁车位”到真实预约系统,一份车位预约源码真正要解决的三个问题
做车位预约小程序,卡住多数人的不是wx.request怎么写,而是“预约”这个动作背后的状态一致性。你点下“预约”按钮,前端高兴地提示成功,后台车位其实已经被别人占走;或者你在停车场出口被道闸拦住,因为系统里根本没有你这笔订单的记录。这类问题在真正的微信小程序开发里几乎每天都会遇到,而一份完整的车位预约项目实例,核心价值就在于把“页面代码”和“业务逻辑”对齐。本篇围绕“车位预约小程序源码”展开,从项目骨架、核心功能实现到小型车场的轻量替代方案,逐层拆解。话题覆盖了前端开发、微信小程序项目实例、数据库项目实例、stm32项目实例之外最常见的一个疑问:拿到源码后怎么改成自己能跑、敢上线的版本。
适合的人群有三类:一是刚开始做微信小程序毕设或作品集,需要完整功能参考的开发者;二是公司内部停车系统需要小程序端,但不想直接买 SaaS 的团队;三是已有服务端,只缺一套能在微信里稳定运行的前端交互层的人。这个标题里真正值钱的不是.rar里的文件,而是“车位预约”这个场景逼出来的设计决策。
2. 先把“预约”这件事想清楚:车位预约小程序的核心模型和目录设计
2.1 核心业务模型:预约单与车位状态的一致性
任何预约系统都绕不开两个实体:车位和预约单。车位的状态只有三种:空闲、被占用、被锁定。空闲与占用是物理事实,被锁定则是一个业务状态——用户提交预约后、实际入场之前,车位应该被“锁住”一段时间,防止其他用户重复预约。
在微信小程序端,用户能感知到的所有操作都围绕预约单展开:选择车位、提交预约、取消预约、入场、出场。而后端真正要保证的只有一条规则:同一时间片内,一个车位只能存在一张有效预约单。这个约束听起来简单,落地时却要处理并发:两个用户同时提交同一个车位的预约,数据库里先到先得,这需要事务或唯一索引;而小程序端往往没有“实时推送”,用户看到的车位状态可能是 30 秒前的快照,这也解释了为什么很多项目里要加一个“状态轮询”。
从源码的常见组织方式来看,一套完整的车位预约项目实例通常分成三个部分:
- 微信小程序端:页面、组件、工具函数、请求封装
- 服务端接口:预约管理、车位管理、用户身份、支付或入场凭证
- 管理后台:车场管理员用的车位录入、预约记录查询界面
parking-reservation/ ├── miniprogram/ # 小程序端 │ ├── pages/ │ │ ├── index/ # 首页:车位列表 + 状态 │ │ ├── reserve/ # 预约页:选时间、提交 │ │ ├── my/ # 我的预约:取消、入场码 │ │ └── pay/ # 支付结果页(如果接入微信支付) │ ├── utils/ │ │ ├── request.js # wx.request 封装 │ │ └── format.js # 时间格式化 │ └── app.json ├── server/ # 服务端 │ ├── routes/ │ ├── models/ │ └── utils/ └── docs/这个目录的意义不只是组织文件,它决定了一个新成员接手时能不能在 10 分钟内找到“取消预约”的逻辑在哪。很多项目实例把业务判断写在页面里,小程序端直接操作数据库(通过云开发),这种写法在原型阶段很快,但一旦要支持“用户 A 取消后车位立即释放”这种逻辑,就不得不在每个页面里复制一份判断,出错率极高。
2.2 页面与接口的对应关系:从index到reserve的调用链
真实的车位预约流程中,用户从进入小程序到完成预约,至少要经过三个页面。同样,源码里最值得读的也是这三个页面的事件链路。
首页pages/index/index负责加载车位列表并渲染状态。它的核心工作是调用getParkingList接口:
前端调用示例
// miniprogram/utils/request.js const request = (url, method = 'GET', data = {}) => { return new Promise((resolve, reject) => { wx.request({ url: `${BASE_URL}${url}`, method, data, header: { 'Content-Type': 'application/json' }, success: (res) => { if (res.statusCode === 200) { resolve(res.data) } else { reject(new Error(`请求失败:${res.statusCode}`)) } }, fail: (err) => reject(err) }) }) } // miniprogram/pages/index/index.js Page({ data: { spots: [], // 车位列表 loading: false }, onShow() { this.loadSpots() }, async loadSpots() { this.setData({ loading: true }) try { const res = await request('/api/spots') this.setData({ spots: res.data }) } catch (err) { wx.showToast({ title: '车位加载失败', icon: 'none' }) } finally { this.setData({ loading: false }) } } })BASE_URL需要根据你的部署环境去改:本地调试用http://127.0.0.1:3000,真机预览则必须是 HTTPS 域名,且要配置在小程序后台的 request 合法域名里。很多第一次做微信小程序开发的人在这一步卡住——代码没错,接口也能通,但模拟器正常、真机请求全部失败,原因就是没配合法域名或没开“不校验合法域名”调试选项。
预约页pages/reserve/reserve.js的核心是提交预约
// 提交预约 async submitReservation() { const { spotId, startTime, endTime } = this.data if (!spotId || !startTime || !endTime) { wx.showToast({ title: '请选择完整的时间段', icon: 'none' }) return } const res = await request('/api/reserve', 'POST', { spotId, startTime, endTime }) if (res.code === 0) { wx.redirectTo({ url: `/pages/my/my` }) } else { wx.showToast({ title: res.msg || '预约失败', icon: 'none' }) } }这里有一个容易忽略的细节:startTime和endTime建议传时间戳而非格式化字符串。时间戳天然可比大小,后端做时间重叠校验时不需要再解析字符串。另一个细节是成功后用wx.redirectTo而不是wx.navigateTo,避免用户从“我的预约”返回时又回到预约页,重复提交。
2.3 预约冲突检测:为什么不能只靠前端判断
前端在选择时间段时当然可以做个简单的重叠判断,但这个判断只能用来改善体验,不能作为业务保障。以车位资源有限的小型停车场为例,两个用户同时提交完全相同的预约段,前端各自在自己的手机上看车位都还是空闲状态,此时只能由后端决定谁成功。
常见的后端冲突检测做法是查询该车位在目标时间段内是否存在有效预约:
SELECT COUNT(*) FROM reservation WHERE spot_id = ? AND status IN ('active', 'pending') AND start_time < ? -- 已有的开始时间早于新的结束时间 AND end_time > ? -- 已有的结束时间晚于新的开始时间这段 SQL 表达的是“时间重叠”的经典判定:start_time < new_end且end_time > new_start。如果COUNT(*) > 0,说明冲突,直接拒绝新预约。需要注意的是status必须排除已取消的记录,否则用户取消后无法再约同一个车位。
源码实例里如果用了事务,通常会在INSERT前执行上面的查询,但更稳的做法是给(spot_id, start_time, end_time, status)建联合唯一索引,从数据库层面兜底。实际项目中这两种手段会同时使用:臃肿的逻辑放在服务端代码里以便返回友好提示,数据库索引作为最后一道防线。
3. 把核心代码跑起来:车位状态变更、入场与离场的完整实现
3.1 车位状态机的代码设计:不可跳过的中间状态
车位状态直接映射到页面 UI,也映射到接口权限。一个常见的状态机设计如下:
| 状态 | 含义 | 触发动作 | 可执行操作 |
|---|---|---|---|
free | 空闲 | 无 | 预约 |
locked | 被预约但未入场 | 提交预约成功 | 取消预约、入场 |
occupied | 已入场 | 入场核销成功 | 出场 |
maintenance | 维护中/不可用 | 管理员设置 | 无 |
在代码实现里,状态机不应该散落在各个接口里。很多简单的项目实例会拿一个if...else写在接口里,比如“当状态是 free 且操作是预约,则改为 locked”,这个写法直观,但维护起来会随着状态增多迅速失控。常见的做法是把状态流转抽成一个单独的函数:
# server/services/spot_service.py from enum import Enum class SpotStatus(Enum): FREE = 'free' LOCKED = 'locked' OCCUPIED = 'occupied' MAINTENANCE = 'maintenance' # 允许的转移表:key 为 (当前状态, 操作),value 为目标状态 TRANSITIONS = { (SpotStatus.FREE, 'reserve'): SpotStatus.LOCKED, (SpotStatus.LOCKED, 'cancel'): SpotStatus.FREE, (SpotStatus.LOCKED, 'enter'): SpotStatus.OCCUPIED, (SpotStatus.OCCUPIED, 'exit'): SpotStatus.FREE, } def transition(current_status: SpotStatus, action: str) -> SpotStatus: new_status = TRANSITIONS.get((current_status, action)) if new_status is None: raise ValueError(f"不允许的状态转移:{current_status} -> {action}") return new_status这段代码的核心价值在于:不合法的操作会在入口处直接报错,而不是跑到数据库层才暴露出问题。enter和exit在TRANSITIONS里的映射说明了一个容易被忽略的业务规则:cancel 动作只对 locked 状态有效,已经入场的订单只能走 exit 流程。如果不做这样的约束,用户入场后点击取消预约,车位状态会回到 free,但车辆还在场内,后续计费全部错乱。
3.2 开锁与入场:小程序端如何拿到“入场凭证”
入场不是用户点一下“入场”就完事,通常要经过道闸或人工确认。对于没有硬件的纯软件项目实例,最常见的做法是生成一个入场二维码或核销码,由车场管理员扫码后确认核销。
小程序端展示核销码的页面,核心逻辑比较简单:
// miniprogram/pages/my/my.js Page({ data: { reservation: null, qrcode: '' // 核销码,一般由后端生成 }, onShow() { this.loadCurrentReservation() }, async loadCurrentReservation() { const res = await request('/api/reservation/current') if (res.code === 0 && res.data) { this.setData({ reservation: res.data, qrcode: res.data.verifyCode }) } } })对应地,服务端生成verifyCode时需要保证两点:一是全局唯一;二是不可猜测。简单时间戳拼接可读性好,但容易被遍历。更常见的做法是用uuid去掉横线后取前 16 位,或者用Redis INCR生成自增号再加随机盐混淆。如果接入的是微信支付,这笔预约单还可以关联一个out_trade_no,这样入场核销和支付结算可以共用同一笔订单状态。
在真正的小程序开发里,“入场凭证”这个环节还涉及一个体验问题:用户入场后停留在“待入场”页面,按钮应该变成“已入场”且不可点击。常规做法是onShow时重新拉取状态,但如果用户一直停留在当前页面不切换,状态不会自动更新。比较好的处理是加一个定时轮询,每 15 秒查询一次订单状态:
// 每 15 秒检查一次订单状态 startPolling() { this.timer = setInterval(async () => { const res = await request(`/api/reservation/${this.data.reservation.id}`) if (res.data.status === 'occupied') { clearInterval(this.timer) this.setData({ reservation: res.data }) wx.showToast({ title: '已入场', icon: 'success' }) } }, 15000) }, onUnload() { if (this.timer) clearInterval(this.timer) }这里的定时器必须在onUnload清理,否则页面已经销毁、定时器仍然在跑,会造成重复请求甚至内存泄漏。对刚上手前端开发的人来说,这是一个很容易忽略但面试和联调时都会被问到的点。
3.3 出场与释放:防止“车位永远被占用”
出场逻辑比入场更要求严谨,因为出场后车位要释放,订单要结算,两者必须同步完成。最常见的错误是只更新了车位状态为 free,没有把订单状态置为已完成,导致后续数据统计出现“已完成的订单”和“空闲的车位”同时存在但无法对应。
一个简单可靠的服务端实现是:
# server/routes/spot.py @router.post('/api/spot/exit') def exit_spot(request): reservation_id = request.json.get('reservationId') # 开启事务,保证订单状态与车位状态同时更新 with db.transaction(): reservation = Reservation.get(id=reservation_id) if reservation.status != 'occupied': raise Exception('订单状态异常,无法出场') reservation.status = 'completed' reservation.save() spot = Spot.get(id=reservation.spot_id) spot.status = 'free' spot.save() return {'code': 0, 'msg': '出场成功'}事务在这里不是可有可无。如果先释放车位、更新订单状态时崩溃,会出现车位空闲但订单还占着的脏数据;反过来则车位永远占用。两个写操作要么同时成功,要么同时回滚,这是车位预约系统里少数据一致性要求最高的路径。
从订单表设计角度,reservation表至少需要这些字段:id、spot_id、user_id、start_time、end_time、status、verify_code、created_at。在此基础上可以扩展actual_start_time和actual_end_time,记录真实入场和出场时间,用于超时计费等场景。
4. 后台管理怎么做:用 3 条 Redis 命令实现小型车场的轻量替代方案
4.1 为什么不用数据库而用 Redis:预约锁和实时状态是两回事
不少拿到项目实例的人会问:既然数据库已经存了车位状态,为什么还要引入 Redis?原因在于数据库的行锁和事务设计目标是持久化,而车位的“实时锁定”是一个短时间、高并发、可以丢失的状态。
以预约为例:用户提交预约到真正入场,中间有 15 到 30 分钟的空档。这个时间段里车位必须被标记为“有人预约”,但显然不适合把这种临时状态写进数据库的spots表——如果用户取消,要反向更新一次。更进一步,如果在用户查看首页的时候,每次都要去数据库SELECT十次状态,压力虽然不大,但代码复杂度会上升。
小型车场可以用三条 Redis 命令完成锁定与释放:
# 1. 尝试锁定车位,setnx 只有在 key 不存在时才成功 SETNX spot:lock:{spotId} {userId} # 2. 给锁加上过期时间,防止用户占着车位不放 EXPIRE spot:lock:{spotId} 1800 # 3. 用户取消或超时后,删除锁 DEL spot:lock:{spotId}逻辑说明:SETNX是 Redis 里最常用于实现分布式锁的命令,key 表示车位 ID,value 存用户 ID,返回 1 表示抢锁成功,返回 0 表示车位已被别人预约。EXPIRE必须紧跟着SETNX执行,如果中间崩溃,锁会变成永不过期。DEL操作用在用户主动取消、管理员强制释放、入场完成三种场景。
在 Node.js 的ioredis或其他 Redis 客户端里,这两条要整体执行,否则会有原子性问题:
// server/utils/redis.js const Redis = require('ioredis') const redis = new Redis({ host: '127.0.0.1', port: 6379 }) async function tryLock(spotId, userId) { const result = await redis.set(`spot:lock:${spotId}`, userId, 'NX', 'EX', 1800) return result === 'OK' } async function releaseLock(spotId) { await redis.del(`spot:lock:${spotId}`) }tryLock把SETNX和EXPIRE合并为一条SET key value NX EX命令,在高版本 Redis 中可以直接原子执行。这里还没有处理“锁被其他人误删”的问题,但要解决也很简单:DEL前先GET一下 value 是否等于当前用户 ID,或者在 Lua 脚本里做判断。对于小型车场项目,这个方案已经足够。
4.2 管理后台的批量录入门店:Excel 导入和车位编号生成
后台管理员的第一件事不是画页面,而是批量录入车位。一个 200 个车位的停车场,一个一个手填不太现实,常见做法是提供 Excel 导入:
# server/utils/import_spots.py import openpyxl def import_spots_from_excel(file_path): workbook = openpyxl.load_workbook(file_path) sheet = workbook.active spots = [] for row in sheet.iter_rows(min_row=2, values_only=True): area, number = row[0], row[1] spot_code = f"{area}-{number:03d}" # 如 A-001 spots.append({"code": spot_code, "area": area}) return spots这里number:03d会让编号统一三位数,排序时不会出现A-10排在A-2前面的字符串排序问题。批量导入的校验逻辑至少包括:重复编号判断、区域名称是否在预设范围内、导入条数是否超限。
管理后台和小程序端共用同一套服务端接口,但权限不同。常见做法是给管理员单独签发一个角色标识,在服务端中间件里判断请求来源。小程序端用户登录拿到openid,管理员登录拿到admin_token,两个体系彼此独立,互不干扰。
5. 上线前必查的 5 个细节:权限、版本兼容、自定义组件和常见坑
5.1 预约权限控制:同一台手机、同一辆车不能重复占位
车位预约小程序在真实运营中会遇到一个高频问题:一个用户预约成功后,在过期时间前能不能再预约另一个车位?业务上通常是不允许的,否则一个人可以锁多个车位、实际只停一辆车。
服务端做这个校验很简单:
# 判断当前用户是否有未完成的有效预约 active_reservation = Reservation.query.filter( Reservation.user_id == current_user_id, Reservation.status.in_(['pending', 'occupied']) ).first() if active_reservation: return {'code': 1, 'msg': '您已有未完成的预约'}需要特别说明的是判断条件里的状态列表:pending是已提交但未入场,occupied是已入场但未出场。这两个状态都说明用户“正在占用资源”,除非后台管理员强制释放,否则不能再发起新的预约。
如果不做这个限制,恶意用户可以用同一账号把整个停车场全部锁光,造成事实上的拒绝服务。这是项目实例里最容易遗漏、但又是真实运营中第一优先级的功能。
5.2app.json里的页面注册顺序和导航栏配置
微信小程序的app.json中pages数组第一项是启动页。对于车位预约项目,启动页通常设置为车位列表首页。这里有一个不太被注意的坑:如果第一个页面是pages/index/index,而用户上次使用停留在“我的预约”页,下次打开小程序会直接进入首页,这本身没问题,但如果首页依赖onLoad里的参数才能展示数据,就会导致白屏。
一个常用的缓解方案是让首页的onShow每次去做数据刷新,而不是只在onLoad里加载。前面的代码示例也体现了这一点。另一个实践是,如果首页数据加载慢,不要用页面跳转,而是在app.json里开启lazyCodeLoading: "requiredComponents"来减少首包加载时间:
{ "lazyCodeLoading": "requiredComponents", "pages": [ "pages/index/index", "pages/reserve/reserve", "pages/my/my", "pages/pay/pay" ] }lazyCodeLoading的作用是按需注入代码,而不是一次性把整个项目的 JS 全部下载下来,对提升首次进入速度有直观效果。不过这个配置只对基础库 2.11.1 以上版本生效,低版本基础库会自动忽略。
5.3 真机与模拟器行为不一致:车位状态刷新和倒计时的差异
车位预约页面上通常会显示预约剩余有效时间,常见实现是setInterval每秒更新页面数据。但小程序在页面切到后台时,定时器会被系统挂起,回前台后定时器恢复执行,但倒计时没有刷新,显示的是切后台之前的剩余秒数。
解决方式是在onShow里重新计算:
onShow() { this.updateCountdown() }, updateCountdown() { const remain = this.data.expireTime - Date.now() this.setData({ countdown: Math.max(0, Math.ceil(remain / 1000)) }) }expireTime来自后端返回的时间戳,每次onShow都用当前时间重新计算,而不是依赖定时器累减。定时器只负责在页面可见时推动 UI 刷新,时间计算永远以服务端时间为准。这样即使在后台停留 10 分钟再切回来,倒计时也是准的。
5.4 版本兼容:低版本基础库下的wx.login与getUserProfile
微信小程序的登录方式和用户信息授权经历过多轮调整。老项目中常见的wx.getUserInfo直接弹窗授权方式已经被收回,现在必须使用getUserProfile按钮触发。一份相对完整的项目源码里应该体现这个差异。
对于要兼容旧版本项目的开发者,可以封装一层兼容调用:
function getUserProfile() { if (wx.getUserProfile) { return new Promise((resolve) => { wx.getUserProfile({ desc: '用于完善会员资料', success: (res) => resolve(res.userInfo), fail: () => resolve(null) }) }) } // 低版本基础库降级处理 return new Promise((resolve) => { wx.getUserInfo({ success: (res) => resolve(res.userInfo), fail: () => resolve(null) }) }) }getUserProfile必须由用户点击按钮触发,不能在onLoad里直接调用,否则会直接触发 fail 回调。这是审查源码时最容易发现的问题:很多从旧项目复制的代码,授权弹窗在低版本上能用,到新版本上就不弹了。
5.5 版本更新检查:用updateManager让用户自动更新
车位预约类小程序更新频率不低,比如车位区域调整、预约规则修改。如果用户一直使用旧版本页面,可能出现“页面显示有车位,提交预约却提示失败”的不一致问题。一个通用做法是在app.js的onLaunch里接入更新检查:
// app.js onLaunch() { if (wx.canIUse('getUpdateManager')) { const updateManager = wx.getUpdateManager() updateManager.onUpdateReady(function () { wx.showModal({ title: '更新提示', content: '新版本已经准备好,是否重启应用?', success(res) { if (res.confirm) { updateManager.applyUpdate() } } }) }) } }这里有个容易被问到的细节:onUpdateReady的触发条件是“新版本下载完成”,但下载是后台静默进行的,不提示用户。如果用户一直不点重启,旧版本会一直使用到小程序被微信主动销毁。所以有些团队会在onCheckForUpdate回调里拿到hasUpdate后,直接调用applyUpdate强制重启,但体验偏激进,通常只在有大版本改动时启用。
回到车位预约这个场景:入场核销、车位状态流转、预约冲突检测,每一个环节出问题都会直接导致线下投诉,所以更新策略宁可晚一点,也要保证线上跑的是经过验证的最新代码。
本文还有配套的精品资源,点击获取