ARTICLE DETAIL

建站实战干货

来自一线的建站与推广经验沉淀,每一条都经过真实交付验证。

车位预约小程序源码实战:预约冲突、状态管理与上线细节

2026/9/12 14:13:11 拓冰建站 浏览量
车位预约小程序源码实战:预约冲突、状态管理与上线细节 简介一套基于微信开发者工具、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(idreservation_id) if reservation.status ! occupied: raise Exception(订单状态异常无法出场) reservation.status completed reservation.save() spot Spot.get(idreservation.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 表示车位 IDvalue 存用户 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_row2, values_onlyTrue): 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强制重启但体验偏激进通常只在有大版本改动时启用。回到车位预约这个场景入场核销、车位状态流转、预约冲突检测每一个环节出问题都会直接导致线下投诉所以更新策略宁可晚一点也要保证线上跑的是经过验证的最新代码。本文还有配套的精品资源点击获取