简介:这是一套面向高校计算机相关专业毕业设计与课程设计场景的停车场微信小程序完整项目源码,采用Java后端配合微信小程序前端与MySQL数据库实现,适合需要完成小程序类毕设或课程作业的学生参考与二次开发。系统划分管理员、商家、用户三类角色,覆盖车主管理、停车场信息维护、预约停车、取消预约、进场停车、商场收费、留言板及系统管理等核心业务模块,功能链路较为完整。压缩包共1242个文件,约15.84MB,包含119个Java源文件、175个JavaScript脚本、134个Vue组件、86个WXML模板与88个WXSS样式,以及2个SQL数据库脚本和若干PNG、SVG、JPG等界面素材,前后端代码与数据库文件齐备。项目基于JDK1.8、Maven3.3、Tomcat7与MySQL5.7环境,配套Eclipse或IDEA及微信开发者工具即可运行。目前已有50人学习下载,可作为毕业设计选题落地的参考方案,帮助读者理解角色权限划分、预约与收费流程设计及前后端接口组织方式。
1. 停车场微信小程序:一个被低估的毕业设计选题
很多同学选毕业设计题目时,第一反应是"停车场管理系统太老套了",于是转头去做推荐算法或者大模型应用,结果中期答辩被导师一句"你的创新点在哪"问得哑口无言。但实际情况恰恰相反——停车场微信小程序这个方向,看起来朴素,落地时却能把 Java 后端、小程序前端、MySQL 数据库设计、业务状态机四条线全部串起来,工作量扎实、演示效果直观、答辩时经得起追问。我带过几届学生的毕设,凡是选这个方向的,最后系统跑通率明显高于那些追热点的。
这个选题的核心链路是:车主用微信小程序查车位、预约、缴费,管理员用后台管理车位和订单,Java 后端负责业务逻辑和数据库读写。它解决的是真实场景里"进场找不到位、出场排队缴费"的痛点,适合有一定 Java 和前端基础、想在两个月内做出一个能演示、能讲清楚、能写进简历的完整系统的同学。下面我按实际开发顺序,把选型、建表、接口、联调、避坑一条条拆开讲。
2. 技术选型与项目骨架:为什么是这套组合
2.1 后端为什么选 SpringBoot 而不是 Servlet
毕业设计里最常见的翻车点,是用原生 Servlet + JSP 硬写,写到一半发现跨域、JSON 序列化、事务管理全要自己处理,时间全耗在造轮子上。SpringBoot 的价值在于它把 Web 层、数据访问层、事务、参数校验都封装好了,你只需要关注停车场本身的业务逻辑。
我一般推荐的技术栈是:SpringBoot 2.7.x + MyBatis-Plus + MySQL 8.0 + Maven。MyBatis-Plus 相比原生 MyBatis,单表增删改查不用写 XML,分页插件开箱即用,对毕设这种以单表操作为主的项目非常合适。下面是最小可运行的pom.xml依赖片段:
<!-- pom.xml 核心依赖,版本按本地 Maven 仓库实际情况调整 --> <dependencies> <!-- Web 层:提供 REST 接口和内置 Tomcat --> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-web</artifactId> </dependency> <!-- 数据访问:MyBatis-Plus 简化单表 CRUD --> <dependency> <groupId>com.baomidou</groupId> <artifactId>mybatis-plus-boot-starter</artifactId> <version>3.5.3.1</version> </dependency> <!-- MySQL 驱动 --> <dependency> <groupId>com.mysql</groupId> <artifactId>mysql-connector-j</artifactId> <scope>runtime</scope> </dependency> <!-- Lombok:省掉 getter/setter,注意 IDE 要装插件 --> <dependency> <groupId>org.projectlombok</groupId> <artifactId>lombok</artifactId> <optional>true</optional> </dependency> </dependencies>逻辑说明:spring-boot-starter-web负责把 Controller 暴露成 HTTP 接口;mybatis-plus-boot-starter负责把实体类映射到数据库表;Lombok 用注解生成样板代码。参数上要注意 MyBatis-Plus 3.5.x 和 SpringBoot 2.7.x 是兼容的,如果你用 SpringBoot 3.x,必须换成 MyBatis-Plus 3.5.3 以上版本,否则会因为 Jakarta EE 包名变更启动报错——这是新手最容易踩的版本坑。
2.2 小程序端为什么用原生而不是 uni-app
小程序端有两种主流选择:微信原生开发(WXML + WXSS + JS)和 uni-app 跨端框架。毕设场景我建议用原生,原因很实际:原生没有编译层,报错信息直接对应源码行号,调试时用微信开发者工具的"调试器"能直接看到网络请求和 Storage 变化;而 uni-app 多一层编译,出问题时你分不清是框架的锅还是自己的锅。原生开发的核心文件结构如下:
miniprogram/ ├── pages/ │ ├── index/ # 首页:车位列表与查询 │ ├── reserve/ # 预约页:选车位、选时段 │ ├── order/ # 订单页:缴费与历史记录 │ └── mine/ # 个人中心:车辆信息、余额 ├── utils/ │ └── request.js # 统一封装 wx.request,处理 token 和错误码 └── app.js # 全局登录态与 baseUrl 配置utils/request.js是整个前端的地基,必须统一封装,否则每个页面都写一遍wx.request,后期改接口地址会改到崩溃。下面是一个可直接抄的封装:
// utils/request.js 统一请求封装 const BASE_URL = 'http://localhost:8080/api'; // 开发阶段指向本地后端 function request(options) { return new Promise((resolve, reject) => { wx.request({ url: BASE_URL + options.url, method: options.method || 'GET', data: options.data || {}, header: { 'content-type': 'application/json', // 登录后把 token 存进 Storage,每次请求带上 'Authorization': wx.getStorageSync('token') || '' }, success(res) { // 后端约定 code=200 为成功,其余为业务异常 if (res.data.code === 200) { resolve(res.data.data); } else { wx.showToast({ title: res.data.msg || '请求失败', icon: 'none' }); reject(res.data); } }, fail(err) { wx.showToast({ title: '网络异常', icon: 'none' }); reject(err); } }); }); } module.exports = { request };逻辑说明:用 Promise 包一层,页面里就能用await request({...})的写法,避免回调地狱。参数上BASE_URL在开发阶段指向localhost,但真机调试时手机访问不到电脑的 localhost,必须换成电脑的局域网 IP,并且后端要允许跨域——这一点后面避坑章节会细说。
2.3 数据库选 MySQL 8.0 的理由与字符集设置
MySQL 是毕设的默认答案,但版本和字符集有讲究。用 8.0 而不是 5.7,是因为 8.0 默认字符集是utf8mb4,能存 emoji,而且窗口函数、CTE 在写统计报表时很方便。建库时一定要显式指定字符集,否则从 5.7 迁移过来的同学容易遇到中文乱码:
-- 建库语句,字符集和排序规则必须显式指定 CREATE DATABASE parking_db DEFAULT CHARACTER SET utf8mb4 DEFAULT COLLATE utf8mb4_general_ci;参数说明:utf8mb4是真正的四字节 UTF-8,能覆盖所有中文和特殊符号;utf8mb4_general_ci排序不区分大小写,适合车牌号、用户名这类字段的查询。如果你的 MySQL 是 8.0.30 以上,排序规则可以换成utf8mb4_0900_ai_ci,性能更好,但毕设用 general_ci 完全够用,不必纠结。
3. 数据库表设计与核心接口实现
3.1 五张核心表:从车位到订单的完整建模
停车场系统的表设计不需要多,但每张表的字段要能支撑完整业务流。我一般会建五张核心表:用户表、车辆表、车位表、预约表、订单表。这里重点讲车位表和订单表,因为这两张表的设计直接决定了业务逻辑顺不顺。
车位表的关键是"状态"字段,它决定了车位能不能被预约:
-- 车位表:status 是业务核心,0空闲 1已预约 2已占用 3维护中 CREATE TABLE t_parking_spot ( id BIGINT PRIMARY KEY AUTO_INCREMENT, spot_no VARCHAR(20) NOT NULL COMMENT '车位编号,如 A-001', area VARCHAR(20) NOT NULL COMMENT '区域,如 A区', status TINYINT NOT NULL DEFAULT 0 COMMENT '0空闲 1已预约 2已占用 3维护', hourly_rate DECIMAL(6,2) NOT NULL DEFAULT 5.00 COMMENT '每小时费率', create_time DATETIME DEFAULT CURRENT_TIMESTAMP, UNIQUE KEY uk_spot_no (spot_no) ) COMMENT '车位表';订单表则要记录金额、时长和支付状态,字段设计如下:
-- 订单表:关联用户、车位、预约,记录完整计费信息 CREATE TABLE t_order ( id BIGINT PRIMARY KEY AUTO_INCREMENT, order_no VARCHAR(32) NOT NULL COMMENT '订单号,用时间戳+随机数生成', user_id BIGINT NOT NULL, spot_id BIGINT NOT NULL, start_time DATETIME NOT NULL COMMENT '入场时间', end_time DATETIME DEFAULT NULL COMMENT '出场时间', duration INT DEFAULT 0 COMMENT '停车分钟数', amount DECIMAL(8,2) DEFAULT 0.00 COMMENT '订单金额', pay_status TINYINT DEFAULT 0 COMMENT '0未支付 1已支付 2已退款', create_time DATETIME DEFAULT CURRENT_TIMESTAMP, UNIQUE KEY uk_order_no (order_no), KEY idx_user (user_id) ) COMMENT '订单表';逻辑说明:spot_no加唯一索引,防止同一车位被重复录入;order_no用唯一索引保证订单号不重复;idx_user加速"查我的订单"这个高频查询。参数上hourly_rate用DECIMAL(6,2)而不是FLOAT,因为金额计算绝不能用浮点数,否则会出现 0.1+0.2 不等于 0.3 的经典问题,计费时对不上账。
3.2 车位查询与预约接口:状态机是灵魂
车位预约的本质是一个状态流转:空闲 → 已预约 → 已占用 → 空闲。这个流转必须用数据库事务 + 行锁保证原子性,否则两个用户同时点"预约"同一个车位,会双双成功,这就是典型的超卖问题。下面是用 MyBatis-Plus 实现的预约接口核心逻辑:
// SpotServiceImpl.java 预约核心方法 @Service public class SpotServiceImpl implements SpotService { @Autowired private SpotMapper spotMapper; @Autowired private OrderMapper orderMapper; @Override @Transactional(rollbackFor = Exception.class) // 开启事务,任何异常都回滚 public void reserve(Long spotId, Long userId) { // 1. 悲观锁查询车位,SELECT ... FOR UPDATE 锁住这一行 ParkingSpot spot = spotMapper.selectByIdForUpdate(spotId); if (spot == null) { throw new BizException("车位不存在"); } // 2. 校验状态,只有空闲才能预约 if (spot.getStatus() != 0) { throw new BizException("该车位已被占用或维护中"); } // 3. 更新车位状态为已预约 spot.setStatus(1); spotMapper.updateById(spot); // 4. 生成预约订单 Order order = new Order(); order.setOrderNo(generateOrderNo()); order.setUserId(userId); order.setSpotId(spotId); order.setStartTime(new Date()); order.setPayStatus(0); orderMapper.insert(order); } }逻辑说明:@Transactional保证"改状态"和"插订单"要么都成功要么都失败;selectByIdForUpdate对应的 SQL 是SELECT * FROM t_parking_spot WHERE id = ? FOR UPDATE,它会在事务提交前锁住这一行,第二个并发请求会阻塞等待,等第一个事务提交后读到状态已经是 1,从而抛出"已被占用"。参数上rollbackFor = Exception.class很关键,默认 Spring 只对运行时异常回滚,加上这个配置后受检异常也会回滚,避免状态改了但订单没插进去的脏数据。
3.3 计费逻辑:按分钟还是按小时
计费规则是答辩时老师最爱追问的点。常见做法有两种:按小时向上取整,或者按分钟精确计费。我一般推荐"首小时按小时计,超出部分按分钟计",既符合真实停车场规则,又能体现你的业务思考。核心代码如下:
// 计费工具类:根据入场出场时间计算金额 public class FeeCalculator { /** * @param start 入场时间 * @param end 出场时间 * @param hourlyRate 每小时费率 * @return 应付金额 */ public static BigDecimal calc(Date start, Date end, BigDecimal hourlyRate) { long minutes = (end.getTime() - start.getTime()) / (1000 * 60); if (minutes <= 0) { return BigDecimal.ZERO; } // 不足 60 分钟按 1 小时算 if (minutes <= 60) { return hourlyRate; } // 超出部分按分钟折算,保留两位小数,四舍五入 BigDecimal extraMinutes = new BigDecimal(minutes - 60); BigDecimal extraFee = extraMinutes .divide(new BigDecimal(60), 4, RoundingMode.HALF_UP) .multiply(hourlyRate); return hourlyRate.add(extraFee).setScale(2, RoundingMode.HALF_UP); } }逻辑说明:先用毫秒差算出总分钟数,前 60 分钟直接收一小时费用,超出部分按分钟数 / 60 × 费率计算。参数上divide的第二个参数 4 表示中间计算保留 4 位小数,最后setScale(2)保留两位,避免中间步骤精度丢失导致最终金额差几分钱。这个细节答辩时讲出来,老师会认为你真的算过账。
4. 前后端联调与部署:从本地跑通到能演示
4.1 跨域与真机调试:两个必过的坎
本地开发时,小程序开发者工具请求localhost:8080一般没问题,但真机预览时手机会去请求手机自己的 localhost,必然失败。解决办法是把BASE_URL换成电脑的局域网 IP,比如http://192.168.1.100:8080/api,同时后端要开启跨域。SpringBoot 里加一个配置类即可:
// CorsConfig.java 允许小程序跨域请求 @Configuration public class CorsConfig implements WebMvcConfigurer { @Override public void addCorsMappings(CorsRegistry registry) { registry.addMapping("/**") .allowedOriginPatterns("*") // 开发阶段放开,上线要收紧 .allowedMethods("GET", "POST", "PUT", "DELETE") .allowCredentials(true) .maxAge(3600); } }逻辑说明:allowedOriginPatterns("*")在 SpringBoot 2.4 以后替代了allowedOrigins("*"),因为后者和allowCredentials(true)不能同时用。参数上maxAge(3600)表示预检请求结果缓存一小时,减少 OPTIONS 请求次数。注意真机调试时手机和电脑必须在同一个 WiFi 下,这是物理前提,不是代码能解决的。
4.2 用 Postman 先测接口再写页面
很多同学的习惯是页面写完再联调,结果一出错不知道是前端传参错了还是后端逻辑错了。我的做法是后端接口写完后,先用 Postman 把每个接口跑一遍,确认返回结构正确,再写小程序页面。这样前端出问题时能快速定位是请求封装的问题还是页面逻辑的问题。测试预约接口时,重点看三件事:返回的code是不是 200、data里有没有订单号、数据库里车位状态有没有变成 1。三步都对,接口才算通。
4.3 打包部署:把 jar 跑起来
毕设演示时,后端要能独立运行。用 Maven 打包成可执行 jar:
# 在项目根目录执行,跳过测试加快打包速度 mvn clean package -DskipTests # 打包完成后在 target 目录找到 jar,直接运行 java -jar parking-backend-1.0.0.jar --spring.profiles.active=prod参数说明:-DskipTests跳过单元测试,毕设阶段测试用例不全,跳过能省时间;--spring.profiles.active=prod指定生产配置文件,里面把数据库地址、端口写成正式环境的值。运行前确认 MySQL 服务已启动、数据库已建好、application-prod.yml里的账号密码正确,否则启动会报Communications link failure。
5. 避坑与常见问题排查
5.1 中文乱码:从数据库到响应全链路排查
现象:小程序页面显示的车位区域名是问号或方块。原因通常有三层:数据库字符集不是 utf8mb4、JDBC 连接串没指定编码、HTTP 响应头没设 charset。解决顺序是从下往上查:先确认建库语句带了utf8mb4,再检查连接串jdbc:mysql://localhost:3306/parking_db?useUnicode=true&characterEncoding=utf8,最后在application.yml里加server.servlet.encoding.charset=utf-8和force=true。三层都对了,乱码必然消失。
5.2 预约超卖:并发测试才暴露的问题
现象:用 JMeter 或手写多线程同时请求预约接口,发现同一个车位生成了两条订单。原因是没有加锁,两个线程同时读到状态 0。解决办法就是 3.2 节讲的SELECT ... FOR UPDATE悲观锁,或者用乐观锁版本号。毕设里推荐悲观锁,实现简单、逻辑直观,答辩时也好讲。测试时开 10 个线程打同一个车位,只有 1 个成功、9 个返回"已被占用",才算过关。
5.3 订单金额对不上:浮点数的锅
现象:停车 90 分钟,费率 5 元/小时,算出来是 7.49 或 7.51 而不是 7.5。原因是用了double做金额运算。解决方法是全程用BigDecimal,数据库字段用DECIMAL,Java 实体用BigDecimal,计算时指定精度和舍入模式。这个坑我在 3.3 节的代码里已经规避了,但如果你自己写计费逻辑,一定要检查有没有混入float或double。
5.4 小程序登录态丢失:Storage 没同步
现象:用户登录后,切到订单页又提示未登录。原因是登录成功后只把 token 存在了内存变量里,没写进wx.setStorageSync,页面跳转后变量被重置。解决办法是登录成功后立即wx.setStorageSync('token', res.token),请求封装里每次从 Storage 读。另外要注意 token 过期时间,毕设可以简单设为 7 天,过期后跳回登录页重新授权。
5.5 真机预览白屏:域名校验没关
现象:开发者工具里一切正常,真机预览白屏或请求全部失败。原因是微信小程序真机环境默认校验合法域名,而你的后端是 IP 地址,不在白名单里。开发阶段在开发者工具"详情 → 本地设置"里勾选"不校验合法域名、web-view、TLS 版本以及 HTTPS 证书",真机预览时也要在手机端开启调试模式。这是开发阶段的临时手段,正式上线必须配置 HTTPS 域名,但毕设演示用调试模式完全够。
6. 让答辩加分:把状态机和计费规则讲成亮点
很多同学觉得停车场系统没亮点,其实亮点不在功能多,而在你把边界情况处理得多细。答辩时老师最容易被两个点打动:一是并发预约的锁机制,二是计费规则的完整性。前者体现你懂数据库事务,后者体现你懂业务。
先说状态机。你可以画一张车位状态流转图(用文字描述即可):空闲 → 预约中 → 已占用 → 待结算 → 空闲,每个箭头对应一个接口和一次数据库状态更新。讲的时候强调"任何一次状态变更都在事务里,且变更前必须校验当前状态",这句话一说,老师就知道你不是只会 CRUD。
再说计费。除了 3.3 节的基础规则,你可以加两个进阶规则:一是免费时长,比如入场 15 分钟内离场不收费;二是每日封顶,比如一天最多收 40 元。这两个规则用代码实现都不难,但能让你的系统看起来像真实产品。下面是一个带免费时长和封顶的计费方法:
// 进阶计费:15 分钟免费,每日封顶 40 元 public static BigDecimal calcAdvanced(Date start, Date end, BigDecimal hourlyRate) { long minutes = (end.getTime() - start.getTime()) / (1000 * 60); // 免费时长判断 if (minutes <= 15) { return BigDecimal.ZERO; } BigDecimal fee; if (minutes <= 60) { fee = hourlyRate; } else { BigDecimal extra = new BigDecimal(minutes - 60) .divide(new BigDecimal(60), 4, RoundingMode.HALF_UP) .multiply(hourlyRate); fee = hourlyRate.add(extra); } // 每日封顶 40 元 BigDecimal cap = new BigDecimal("40.00"); if (fee.compareTo(cap) > 0) { fee = cap; } return fee.setScale(2, RoundingMode.HALF_UP); }逻辑说明:先判断免费时长,再走分段计费,最后用compareTo判断是否超过封顶值。参数上封顶值写成常量或配置项更好,方便调整。答辩时你可以现场改封顶值演示,老师会觉得你的系统是可配置的,而不是写死的。
验证方法上,我建议你准备一组测试用例:停车 10 分钟(应免费)、停车 60 分钟(应 5 元)、停车 90 分钟(应 7.5 元)、停车 10 小时(应 40 元封顶)。把这四个用例跑一遍,结果全对,计费模块就算稳了。这组用例也可以写进论文的测试章节,比空泛的"系统运行正常"有说服力得多。
最后说个我自己的习惯:每次改完计费或状态机代码,我都会把数据库清空重新跑一遍完整流程——从注册、查车位、预约、入场、出场到支付,走一遍看有没有状态卡死。这个习惯帮我抓出过好几次"订单支付了但车位没释放"的 bug。做毕设最怕的就是演示当天流程走不通,提前多跑几遍完整链路,比事后补救靠谱得多。希望帮到你。
本文还有配套的精品资源,点击获取