简介:这是一份基于微信小程序的停车场预约管理系统完整毕设项目包,面向计算机相关专业学生和开发者,适用于毕业设计、课程设计、大作业或初期项目演示,也可作为小程序前端与后端服务整合的实战参考。压缩包共1126个文件,约30.58MB,涵盖Java后端逻辑、Vue管理端页面、小程序前端wxml与wxss脚本、SQL数据库脚本、png与svg界面素材以及mp4运行视频等,目录按功能模块划分,便于按需检索源码和配置。目前已有254人学习。项目已通过验证,可直接运行学习,配套视频演示了实际效果,数据库脚本可快速初始化环境,适合在此基础上进行二次开发,例如扩充停车位策略或预约规则;使用前将解压路径改为英文名可避免解析问题。
1. 这个毕设项目到底在做什么
打开压缩包之前,先别急着点运行视频。基于微信小程序的停车场预约管理系统,本质是把停车场的车位状态从「线下到门口碰运气」搬到手机端:用户打开小程序,看到哪些车位可约、剩余多少、价格多少,选一个时间段提交预约,到现场扫码或报号入场,离场后自动释放车位。管理端再在后台维护车位、审核订单、看统计。它解决的痛点是停车场的潮汐空置——白天写字楼车位不够,晚上又全空着,预约制能把固定时段的使用权提前锁定,商户也愿意为确定性付费。
适合谁做?两类人。一类是计算机/软件类毕设选题,需要同时展示小程序前端、后端接口、数据库设计和订单状态流转,这套题麻雀虽小五脏俱全;另一类是真有停车场资源、想低成本试水预约运营的个体商户,小程序免安装、微信生态内直接打开,比做App和H5都省事。标题里已经写明配套了源码、数据库文件和运行视频,意味着你拿到的是一套可导入的完整工程,而不是散落的代码片段。下面我从数据库怎么设计、接口怎么写、小程序端怎么接步骤讲清楚,顺便把最容易翻车的几个坑提前点出来。
2. 停车场预约系统的数据库设计:五张表把状态机立住
数据库是一套预约系统的地基。很多毕设翻车不是代码写错,而是表结构设计得太随意——车位状态放一个字段里硬改,订单和用户混在一张表,时间用字符串存,回头一跑报表全是脏数据。我一般会按「用户、车位、订单、时段、配置」拆成五张核心表,先想清楚状态流转再建表。
2.1 表结构怎么拆:车位、订单、用户各司其职
用户表最简单,微信小程序登录拿 openid 作为唯一标识。注意不要自己建用户名密码体系,小程序端用户没有输入账号密码的习惯,直接openid当主键逻辑上的用户 ID 即可,昵称头像通过wx.getUserProfile()拿,存下来用于订单展示和客服联系。
车位列,我通常设计为parking_space:id、code(车位编号,如 A-01)、section(区域)、type(普通/充电/无障碍)、status(空闲/占用/禁用)、price_per_hour(该车位按时段可不同价,简单起见放基础价)。这里要特别注意——车位的物理状态和预约状态是两回事:物理状态表示车位上此刻有没有车,预约状态表示未来某段时间被谁预订了。只用一个字段会出大问题,后面避坑章单独说。
订单表是核心,字段包括:id、user_openid、space_id、start_time、end_time、status(待支付/已预约/进行中/已完成/已取消/已过期)、pay_amount、create_time。这里的status不是乱写的,它是一条状态机:用户提交预约 → 待支付;支付成功 → 已预约;到达入场时间扫码 → 进行中;离场释放 → 已完成;用户主动取消 → 已取消;超过开始时间 15 分钟未入场且未取消 → 已过期。
时段表很多人会忽略,但预约系统的核心其实是它。停车不是全天任意时段都能约,停车场有营业时间、不同时段价格也不同。我建time_slot表:id、start、end、price_rate。比如 8:00-18:00 是高峰价,18:00-次日8:00 是夜间价。前端选择时间段时,默认按时段表生成可选列表,避免用户提交一个凌晨三点的预约让你处理起来措手不及。
最后一张是配置表parking_config,存营业时间、超时宽限分钟数、取消预约截止提前量等参数。把「硬编码在代码里的业务规则」挪到配置表里,改运营策略不用重新发版,演示和答辩时也能现场改参数给老师看效果。
2.2 建表语句与三个必注意的边界
以下建表 SQL 是 MySQL 方言,字段按上述设计收敛到最简,你可以直接导入后自行扩展。注意engine=InnoDB和utf8mb4字符集,前者保证订单事务,后者保证中文字段不出现乱码。
CREATE DATABASE IF NOT EXISTS parking_reserve DEFAULT CHARSET utf8mb4 COLLATE utf8mb4_general_ci; USE parking_reserve; CREATE TABLE user ( id INT PRIMARY KEY AUTO_INCREMENT, openid VARCHAR(64) NOT NULL UNIQUE COMMENT '微信openid', nickname VARCHAR(64) DEFAULT '', phone VARCHAR(20) DEFAULT '', create_time DATETIME DEFAULT CURRENT_TIMESTAMP ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; CREATE TABLE parking_space ( id INT PRIMARY KEY AUTO_INCREMENT, code VARCHAR(20) NOT NULL UNIQUE COMMENT '车位编号', section VARCHAR(50) DEFAULT '', type TINYINT DEFAULT 0 COMMENT '0普通 1充电 2无障碍', status TINYINT DEFAULT 0 COMMENT '0空闲 1占用 2禁用', price_per_hour DECIMAL(6,2) DEFAULT 5.00 ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; CREATE TABLE parking_order ( id INT PRIMARY KEY AUTO_INCREMENT, order_no VARCHAR(32) NOT NULL UNIQUE COMMENT '业务订单号', user_openid VARCHAR(64) NOT NULL, space_id INT NOT NULL, space_code VARCHAR(20) DEFAULT '', start_time DATETIME NOT NULL, end_time DATETIME NOT NULL, status TINYINT NOT NULL DEFAULT 0 COMMENT '0待支付 1已预约 2进行中 3已完成 4已取消 5已过期', pay_amount DECIMAL(8,2) DEFAULT 0, create_time DATETIME DEFAULT CURRENT_TIMESTAMP, KEY idx_start_end (start_time, end_time), KEY idx_user (user_openid) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; CREATE TABLE time_slot ( id INT PRIMARY KEY AUTO_INCREMENT, start TIME NOT NULL, end TIME NOT NULL, price_rate DECIMAL(4,2) DEFAULT 1.00 COMMENT '价格倍率' ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; CREATE TABLE parking_config ( id INT PRIMARY KEY AUTO_INCREMENT, config_key VARCHAR(50) NOT NULL UNIQUE, config_value VARCHAR(255) DEFAULT '' ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;建表时三个边界值得提。第一,order_no不能做成自增 ID 对外暴露,订单号最好是时间戳+随机数的业务编号,同时加唯一索引防止重复单。第二,start_time和end_time必须建复合索引,这是预约冲突查询最频繁的过滤条件,不建索引,车位多了以后每次预约都全表扫,后面会越来越卡。第三,用户表里openid加唯一约束,同一个微信号重复授权登录时走更新逻辑而不是再插一条,这是小程序端反复进入页面都会触发的路径,不处理会累积脏数据。
数据库这层设计走通后,预约系统就不再是「改一个字符串字段」,而是明确的状态机:每个状态何时进入、由哪个动作触发、超时了谁来扫。状态机立住了,后端接口写起来就是顺着状态图填代码,不会东改一块西补一块。
3. 后端预约接口:先写对时间冲突的 SQL 再写业务逻辑
后端是预约系统的引擎,接口设计按资源维度拆:车位查询、下单预约、支付回调、入场确认、取消预约、离场释放、订单列表。毕设项目不需要上微服务,单体应用加一张订单表就够,但时间冲突的查询必须写对,这是整套系统能不能防住「同一车位同一时段被两个人约走」的关键。
3.1 可预约车位列表:SQL 怎么排除冲突时段
前端进入小程序首页时,要展示当前可预约的车位。这里的「可预约」不是车位物理空闲就行,而是车位在用户所选时间段内没有任何未取消的预约。最稳妥的做法是前端先传startTime和endTime,后端用一条 NOT EXISTS 子查询过滤掉已被占用的车位:
SELECT s.id, s.code, s.section, s.type, s.price_per_hour FROM parking_space s WHERE s.status = 0 -- 车位不在禁用/占用状态 AND NOT EXISTS ( SELECT 1 FROM parking_order o WHERE o.space_id = s.id AND o.status IN (0, 1, 2) -- 待支付、已预约、进行中都算占用 AND o.start_time < #{endTime} AND o.end_time > #{startTime} ) ORDER BY s.code;这段 SQL 是整套预约系统的命门,多解释两句。o.start_time < #{endTime} AND o.end_time > #{startTime}是区间重叠判断的标准写法,叫「交集非空」:已存在的预约时间段和新请求时间段只要有一点点重叠,就会被 NOT EXISTS 挡住。很多新手写成start_time BETWEEN ? AND ? OR end_time BETWEEN ? AND ?,这只能判断端点,漏掉了「已存在预约完全包含新请求」的情况——比如已有预约是 10:00-14:00,新请求是 11:00-12:00,两个端点都不在范围内,但实际是冲突的。用上面的写法一网打尽。
status IN (0, 1, 2)也要动脑筋:待支付和已预约的订单当然占住未来时段,进行中的订单说明车位上现在有车,也要排除。只有已完成、已取消、已过期的订单才不再参与冲突计算。这一条写错,会出现「明明有位子却约不了」或者「约了之后到现场发现车位上停着车」的尴尬场面。
3.2 创建预约:用带条件的 UPDATE 代替先查后插
列表查询写对了,创建预约这步还会遇到并发问题。用户 A 和用户 B 同时提交同一车位的预约请求,后端如果先 SELECT 判断无冲突再 INSERT,两个请求可能都读到「无冲突」,先后插入成功——这就是经典的并发超卖。解决思路很简单:把判断和写入合并成一条带条件 UPDATE,或者用数据库行锁。
我的做法是给车位表加一个预约锁字段,下单时先执行条件更新:
UPDATE parking_space SET status = 1 -- 临时置为占用,作为并发锁 WHERE id = #{spaceId} AND status = 0;如果UPDATE影响行数为 1,说明抢锁成功,可以继续创建订单;如果为 0,说明车位已被别人锁定或处于禁用状态,直接返回「车位已被预约」。锁住后再插入订单记录,订单状态置为待支付,同时开启支付倒计时。这里有个细节:锁不能一直占着,用户在支付页停留太久,锁要能释放。我在订单表里加一个lock_expire_time(锁过期时间,通常 5 分钟),定时任务每 30 秒扫一次,把超时未支付的订单置为已取消,同时把车位状态恢复为 0。
另一种常见做法是直接用事务 +SELECT ... FOR UPDATE锁行,原理一样,但毕设项目里理解UPDATE条件锁更直观,答辩时也容易讲清楚。要注意的是,锁和订单创建必须在同一个事务里,事务提交前任何异常都要回滚 UPDATE,否则车位会被锁死。Spring Boot 里就是@Transactional包住这两个操作,异常直接向外抛。
订单创建成功后的支付环节,毕设项目一般接微信支付会碰到商户号申请门槛,很多同学用模拟支付代替——点击「确认支付」直接走支付成功回调。这里建议后端把支付接口和支付成功回调接口分开预留,真实微信支付接入时只需替换支付实现类,前端那部分不用动。
4. 微信小程序端:从登录态到车位列表的关键代码
小程序端是用户直接接触的界面,很多同学把精力花在页面样式上,结果在登录态和请求封装上翻了车。微信小程序的网络请求天生和普通网页不同:域名必须备案并配置到小程序后台白名单,开发阶段还要在开发者工具里勾选「不校验合法域名」。这套项目跑本地时,后端接口是http://localhost:8080,一定要在详情 → 本地设置里勾掉域名校验,否则所有请求都会被拦截,页面空白一片。
4.1 登录态处理:wx.login 拿 code,换 openid
小程序不像网页端可以自己输账号密码。微信的登录流程是固定的:前端调wx.login()拿到临时code,传给后端;后端拿code调微信的接口换openid和session_key;后端再返回自定义的登录态 token 给小程序的storage里存着。
// utils/auth.js function login() { return new Promise((resolve, reject) => { wx.login({ success: async (res) => { if (res.code) { try { const { token } = await request({ url: '/api/auth/login', method: 'POST', data: { code: res.code } }); wx.setStorageSync('token', token); resolve(token); } catch (err) { reject(err); } } else { reject(new Error('wx.login 获取 code 失败')); } }, fail: reject }); }); }这段代码里request是我封装好的请求函数,统一在 header 里带上token。注意wx.login的code只能使用一次,后端换完openid后这个 code 就失效了,所以前端一定要等后端返回成功再继续后续操作,不要在 success 回调里重复调用wx.login。另外,session_key是敏感信息,不要返回给前端,后端换完就自己存着,后续解密手机号等操作要用。
4.2 车位列表渲染:从接口数据到页面可视
拿到车位列表后,前端渲染没有太复杂的逻辑,但有一个常见问题:车位的状态是动态的,用户停留在页面期间可能被其他人约走。所以首页车位列表不能只加载一次,要在onShow生命周期重新拉取。下面是一个典型写法:
// pages/index/index.js Page({ data: { spaceList: [], loading: false, selectedDate: '', timeOptionIndex: 0 }, onShow() { this.loadSpaces(); }, async loadSpaces() { this.setData({ loading: true }); try { const { date, startTime, endTime } = this.buildTimeRange(); const list = await request({ url: '/api/space/available', data: { date, startTime, endTime } }); this.setData({ spaceList: list }); } finally { this.setData({ loading: false }); } }, buildTimeRange() { // 从预约时段面板读当前选中项,拼成完整时间戳 return { date: this.data.selectedDate, startTime: `${this.data.selectedDate} ${slots[this.data.timeOptionIndex].start}`, endTime: `${this.data.selectedDate} ${slots[this.data.timeOptionIndex].end}` }; } });这里的时间处理是另一个易错点。new Date()在小程序端取到的是用户手机本地时间,如果手机时间和服务器时间不一致,提交预约时会直接导致时间判断错乱。我的做法是:后端返回一个当前服务器时间戳,前端所有时间计算都以它为准,展示给用户的相对时间再本地格式化。请求封装里顺手加一个getServerTime接口,登录成功后拉一次缓存到 globalData 里,页面里需要「当前时间」的地方都用它,避免本地时钟不准引发预约边界问题。
页面布局上,车位列表用wx:for渲染每个卡片,展示车位编号、区域、类型图标和价格,点击后跳转到预约确认页。列表滚动加载更多在小程序的实现和网页不同——用scroll-view的bindscrolltolower触发分页加载,每次请求页码加 1 追加到数组末尾,避免一次性渲染几百个车位把页面卡住。
5. 预约流程的五个常见坑:从启动到跑通的排查实录
再完善的代码,部署到实际环境都会冒出新问题。我梳理了这套系统从启动到上线最常遇到的 5 个坑,每条都按「现象 → 原因 → 解决」讲清楚,你照着项目跑一遍时能省不少排查时间。
5.1 前端请求全部失败,页面白屏
现象是小程序页面能打开,但列表永远转圈,打开调试面板看到所有请求都是fail状态。原因有两个,一是开发工具没勾选「不校验合法域名」,二是后端服务没启动或端口不对。解决:先在浏览器直接访问后端接口地址,确认返回 JSON;再检查小程序工具里的本地设置勾选状态;最后确认请求的 baseURL 是不是http://127.0.0.1:8080这种不带空格的地址。注意千万别用localhost之外能上网的 IP,小程序工具和手机真机调试时访问的地址不同,真机必须用局域网 IP。
5.2 数据库导入后中文乱码
项目压缩包里通常带.sql文件,用 Navicat 导入后表里中文全变成???。多数情况是建库字符集不是utf8mb4,而.sql文件本身是 UTF-8 编码。解决:重新建库时显式指定DEFAULT CHARSET utf8mb4 COLLATE utf8mb4_general_ci,导入前在 Navicat 的连接属性里把编码设为 UTF-8。如果已经导入,删库重建最干净,别想着 ALTER TABLE 一条条改,容易漏。
5.3 时间判断差 8 小时,预约时段错乱
后端存储的start_time比前端选的时间早或晚 8 小时。这是因为 JSON 传输时时间格式化带了时区:后端用的是GMT而前端默认GMT+8。解决:统一时间传输格式,最简单的是前后端都传「时间戳」而不是格式化字符串,数据库再转成DATETIME存储。另一种方案是后端写一个全局 Jackson 配置,规定LocalDateTime序列化格式为yyyy-MM-dd HH:mm:ss,前端new Date("2024-05-20 14:00:00".replace(/-/g, "/"))转换,避免 Safari 不认横杠时间串的坑。
5.4 并发预约同一车位,双双提示成功
两个用户同时点提交,后端都返回了成功,但一个车位只该有一个订单。这就是我前面说的并发问题,如果后端下单接口是用「先 SELECT 再 INSERT」实现,就会踩中。解决:按第 3 章讲的改成条件 UPDATE 加事务。你可以在后端接口里加一个Thread.sleep(200)模拟延迟,然后用两个浏览器同时请求,复现这个 bug 给答辩老师看并发处理的必要性——顺便展示你如何用条件锁解决。
5.5 用户取消预约后,车位仍显示不可约
订单状态改为已取消后,车位列表里还是看不到这个车位。原因是列表查询的 SQL 里只过滤了订单状态在 0/1/2,但车位表的status字段还停留在 1(占用),没有被还原。解决:取消预约的接口里,在订单状态更新的同一个事务中,把parking_space.status改回 0。这里的关键又是事务——任何订单状态的变更,只要影响车位的可约性,都必须和车位状态的更新放在同一事务里,否则就会出现状态不一致的脏数据。
6. 跑通之后怎么验证:把预约状态机压测一遍
项目跑通只是第一步,真正能证明系统健壮性的方法是模拟真实用户把每一条状态流转路径都走一遍。我建议你按下面的清单操作:先注册两个微信号(或同一个微信号开两个开发者工具实例),同时访问同一车位,验证只有一人能预约成功;再约一个时段,等它过期不支付,确认系统自动释放车位;然后预约并支付,到入场时间扫码入场,确认状态从已预约变成进行中;离场后确认车位秒回空闲。把每一步的订单状态、车位状态、数据库记录三者对照,任何一步对不上就是代码里埋着 bug。
进阶一点,可以给后端写一个简单的压力测试脚本,模拟 50 个并发预约同一车位,统计成功数和失败数。成功数一定是 1,失败数一定是 49,任何多余的成功数说明并发控制失效。这个测试结果写进毕设论文的测试章节,比贴十张页面截图有说服力得多。我在给自己的项目做验收时还养成了一个习惯:每改一个功能,先把数据库里所有订单状态重置为初始,再从头走一遍完整预约流程,确保没有因为新增功能破坏旧链路。
这套系统的价值不在于技术栈有多新,而在于把「预约」这个业务逻辑闭环得完整且自洽。你拿到压缩包后,我给你的建议是别急着改界面,花半天时间把数据库的每张表、每段 SQL 和每条状态流转读懂,再按上面的排查清单跑通,然后从「加一个预约提醒推送」或「接入真实地图选车位」这种小功能入手改造。等你把它的状态机吃透了,改造成校园自习室预约、医院号源预约,都只是换皮换字段的事。我当年调试别人的项目时,最深的感触就是:先复现,再理解,最后动手改,顺序千万别反。希望帮到你。
本文还有配套的精品资源,点击获取