☰
SpringBoot+Vue停车场管理系统毕设跑通指南:环境搭建、数据库设计与前后端联调
2026/10/7 16:35:42 网站建设 项目流程

简介:这是一套基于Java、Spring Boot、Vue与MySQL开发的停车场管理系统完整毕业设计资源,面向高校计算机相关专业学生及需要课程设计、期末大作业参考的开发者,可直接用于毕设答辩或作为企业级项目学习案例。压缩包共852个文件,约43.56MB,涵盖113个Java后端源码、59个Vue前端组件、161个JavaScript脚本、44个HTML页面、52个CSS样式及1个SQL数据库脚本,另附论文文档与部署说明,结构清晰、模块分明。系统实现车位管理、车辆出入登记、数据统计等核心功能,前后端分离,界面美观、操作便捷,并经过严格调试确保可运行。目前已有86人学习下载。读者可从中获得完整源码、数据库脚本、论文参考及Maven构建配置,快速理解Spring Boot与Vue的整合思路,掌握数据库表结构设计与前后端交互逻辑,适合作为学习模板或二次开发基础。

1. 停车场管理系统:从一份毕设源码里拆出能跑通的技术栈

很多同学拿到“基于java+springboot+vue+mysql的停车场管理系统”这类毕设项目时,第一反应是解压、找README、照着文档敲命令,结果卡在数据库连不上、前端跨域报错、SpringBoot版本冲突这些地方,三天都跑不起来。这个标题背后其实是一套非常典型的Java全栈组合:SpringBoot做后端接口,Vue做前端页面,MySQL存业务数据,核心业务是车辆进出场、车位状态管理、计费规则和订单记录。它适合正在做毕设的计算机专业学生,也适合想用一个完整项目练手SpringBoot+Vue前后端分离的初级Java工程师。这篇文章不讲空泛的架构图,而是按“环境怎么搭、表怎么建、接口怎么写、前后端怎么联调、坑怎么绕”的顺序,把一套停车场管理系统从源码到跑通的路径拆开讲清楚。你跟着走一遍,至少能拿到一个能演示、能答辩、能继续改的版本。

2. 环境准备与项目结构:先把SpringBoot和Vue的版本对齐

2.1 为什么版本对齐比写代码更先要做

停车场管理系统这类毕设项目,翻车最多的地方不是业务逻辑,而是环境。SpringBoot 2.x和3.x的差异很大,Vue 2和Vue 3的写法也不一样,MySQL 5.7和8.0在驱动类和连接串上也有区别。你拿到源码后,第一件事不是急着mvn spring-boot:run,而是先看pom.xml里的SpringBoot版本、package.json里的Vue版本、以及数据库脚本里的建表语句用的是哪个MySQL版本。常见做法是:如果源码用SpringBoot 2.7.x,就配JDK 8或11;如果用SpringBoot 3.x,就必须JDK 17起步。Vue这边,如果package.json里是vue: ^2.6.x,你就别用Vue 3的createApp写法去改它。

我一般会先把三个东西确认清楚:JDK版本、Node版本、MySQL版本。JDK用java -version看,Node用node -v看,MySQL用mysql --version看。这三个版本对不上,后面全是玄学问题。

2.2 后端项目结构:controller、service、mapper三层怎么分

一个标准的SpringBoot停车场管理系统后端,目录结构通常是这样:

src/main/java/com/parking/ ├── controller/ // 接口层,接收前端请求 ├── service/ // 业务逻辑层,处理计费、车位分配 ├── mapper/ // 数据访问层,MyBatis或MyBatis-Plus ├── entity/ // 数据库实体类 ├── config/ // 跨域配置、拦截器配置 └── ParkingApplication.java

controller层只做参数校验和调用service,不要在里面写业务计算。service层是停车场系统的核心,比如“车辆入场时分配车位”“出场时按小时计费”都写在这里。mapper层用MyBatis-Plus的话,很多单表操作可以直接继承BaseMapper,不用手写SQL。

// 示例:车辆入场接口 @RestController @RequestMapping("/api/vehicle") public class VehicleController { @Autowired private VehicleService vehicleService; @PostMapping("/entry") public Result entry(@RequestBody EntryDTO dto) { // 参数校验:车牌号不能为空 if (dto.getPlateNumber() == null || dto.getPlateNumber().isEmpty()) { return Result.error("车牌号不能为空"); } return vehicleService.vehicleEntry(dto); } }

这段代码的逻辑是:前端传车牌号和入场时间,后端先做基础校验,再交给service处理。Result是统一返回体,包含code、msg、data三个字段。参数说明:EntryDTO里至少要有plateNumber和entryTime,如果系统支持预约车位,还要加parkingSpaceId。

2.3 前端项目结构:Vue页面和API请求怎么组织

Vue前端一般用vue-cli或vite搭建,目录结构如下:

src/ ├── api/ // 封装axios请求 ├── views/ // 页面组件 ├── router/ // 路由配置 ├── store/ // Vuex状态管理(可选) └── main.js

api目录下按模块建文件,比如vehicle.js、parkingSpace.js、order.js。每个文件里封装对应的请求方法,页面组件只调用这些方法,不直接写axios.get。

// src/api/vehicle.js import request from '@/utils/request' export function vehicleEntry(data) { return request({ url: '/api/vehicle/entry', method: 'post', data: data }) }

request是封装好的axios实例,里面统一配了baseURL和拦截器。baseURL一般指向后端地址,比如http://localhost:8080。这样做的目的是:后端地址变了,只改一个地方,不用每个页面都改。

提示:如果前端启动后请求报404,先看baseURL有没有写对,再看后端接口的@RequestMapping路径是否匹配。跨域问题在开发阶段用proxy解决,生产环境用Nginx反向代理。

3. 数据库设计:停车场管理系统的表结构和建表脚本

3.1 核心表:车位表、车辆表、订单表怎么设计

停车场管理系统的数据库设计,核心就三张表:parking_space(车位表)、vehicle(车辆进出记录表)、order(订单表)。如果要做会员或月卡,再加member表。车位表里要有车位编号、区域、状态(空闲/占用/维修)。车辆表里要有车牌号、入场时间、出场时间、车位ID。订单表里要有车牌号、计费金额、支付状态、创建时间。

CREATE TABLE `parking_space` ( `id` INT NOT NULL AUTO_INCREMENT, `space_no` VARCHAR(20) NOT NULL COMMENT '车位编号', `area` VARCHAR(50) DEFAULT NULL COMMENT '区域', `status` TINYINT DEFAULT 0 COMMENT '0空闲 1占用 2维修', PRIMARY KEY (`id`), UNIQUE KEY `uk_space_no` (`space_no`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; CREATE TABLE `vehicle` ( `id` INT NOT NULL AUTO_INCREMENT, `plate_number` VARCHAR(20) NOT NULL COMMENT '车牌号', `space_id` INT DEFAULT NULL COMMENT '车位ID', `entry_time` DATETIME DEFAULT NULL, `exit_time` DATETIME DEFAULT NULL, PRIMARY KEY (`id`), KEY `idx_plate` (`plate_number`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; CREATE TABLE `order` ( `id` INT NOT NULL AUTO_INCREMENT, `plate_number` VARCHAR(20) NOT NULL, `amount` DECIMAL(10,2) DEFAULT 0.00 COMMENT '金额', `pay_status` TINYINT DEFAULT 0 COMMENT '0未支付 1已支付', `create_time` DATETIME DEFAULT CURRENT_TIMESTAMP, PRIMARY KEY (`id`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;

建表时注意:space_no加唯一索引,防止重复车位编号。plate_number加普通索引,因为查询车辆记录时经常按车牌号查。金额字段用DECIMAL,不要用FLOAT,避免精度问题。

3.2 计费规则表:按小时还是按次

如果系统支持不同计费规则,建议单独建一张billing_rule表:

CREATE TABLE `billing_rule` ( `id` INT NOT NULL AUTO_INCREMENT, `rule_name` VARCHAR(50) NOT NULL, `free_minutes` INT DEFAULT 15 COMMENT '免费时长(分钟)', `hourly_rate` DECIMAL(10,2) DEFAULT 5.00 COMMENT '每小时费率', `daily_max` DECIMAL(10,2) DEFAULT 50.00 COMMENT '每日封顶', PRIMARY KEY (`id`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;

free_minutes控制免费停车时长,hourly_rate是超出后的每小时费用,daily_max是单日最高收费。计费逻辑写在service层:出场时间减入场时间,先扣免费时长,再按小时向上取整,最后和daily_max比较取小值。

3.3 数据库连接配置:application.yml里几个必调参数

SpringBoot的数据库配置在application.yml或application.properties里:

spring: datasource: url: jdbc:mysql://localhost:3306/parking_db?useUnicode=true&characterEncoding=utf8&serverTimezone=Asia/Shanghai username: root password: your_password driver-class-name: com.mysql.cj.jdbc.Driver

serverTimezone必须配,否则MySQL 8.0会报时区错误。characterEncoding=utf8防止中文乱码。如果用的是MySQL 5.7,驱动类可以写com.mysql.jdbc.Driver,但建议统一用com.mysql.cj.jdbc.Driver,兼容性更好。

注意:数据库名、用户名、密码要和本地实际环境一致。如果导入SQL脚本时报错,先检查脚本里的CREATE DATABASE语句有没有指定字符集,建议手动改成utf8mb4。

4. 后端接口实现:车辆进出场和计费逻辑怎么写

4.1 车辆入场:分配车位和更新状态

车辆入场的业务逻辑分三步:查车牌是否已在场内、找空闲车位、插入车辆记录并更新车位状态。这三步要放在一个事务里,任何一步失败都回滚。

@Service public class VehicleServiceImpl implements VehicleService { @Autowired private ParkingSpaceMapper spaceMapper; @Autowired private VehicleMapper vehicleMapper; @Transactional(rollbackFor = Exception.class) @Override public Result vehicleEntry(EntryDTO dto) { // 1. 检查是否已在场内 Vehicle exist = vehicleMapper.selectByPlateAndExitNull(dto.getPlateNumber()); if (exist != null) { return Result.error("该车辆已在场内"); } // 2. 找空闲车位 ParkingSpace space = spaceMapper.selectOneFreeSpace(); if (space == null) { return Result.error("车位已满"); } // 3. 插入车辆记录 Vehicle vehicle = new Vehicle(); vehicle.setPlateNumber(dto.getPlateNumber()); vehicle.setSpaceId(space.getId()); vehicle.setEntryTime(new Date()); vehicleMapper.insert(vehicle); // 4. 更新车位状态 space.setStatus(1); spaceMapper.updateById(space); return Result.success("入场成功"); } }

@Transactional保证事务一致性。selectByPlateAndExitNull是自定义查询,查exit_time IS NULL的记录。selectOneFreeSpace用LIMIT 1查第一个空闲车位。参数说明:EntryDTO里的plateNumber要做去空格和转大写处理,避免“京A12345”和“京a12345”被当成两个车。

4.2 车辆出场:计费算法和时间差处理

出场逻辑比入场复杂,核心是算钱。用java.time.Duration算时间差,比手动减毫秒更可靠。

@Override @Transactional(rollbackFor = Exception.class) public Result vehicleExit(String plateNumber) { Vehicle vehicle = vehicleMapper.selectByPlateAndExitNull(plateNumber); if (vehicle == null) { return Result.error("未找到在场记录"); } // 计算停车时长 LocalDateTime entry = vehicle.getEntryTime().toInstant() .atZone(ZoneId.systemDefault()).toLocalDateTime(); LocalDateTime exit = LocalDateTime.now(); long minutes = Duration.between(entry, exit).toMinutes(); // 计费:免费15分钟,超出按小时向上取整 BillingRule rule = billingRuleMapper.selectDefault(); BigDecimal amount = BigDecimal.ZERO; if (minutes > rule.getFreeMinutes()) { long chargeMinutes = minutes - rule.getFreeMinutes(); long hours = (chargeMinutes + 59) / 60; // 向上取整 amount = rule.getHourlyRate().multiply(BigDecimal.valueOf(hours)); if (amount.compareTo(rule.getDailyMax()) > 0) { amount = rule.getDailyMax(); } } // 更新车辆出场时间 vehicle.setExitTime(new Date()); vehicleMapper.updateById(vehicle); // 释放车位 ParkingSpace space = spaceMapper.selectById(vehicle.getSpaceId()); space.setStatus(0); spaceMapper.updateById(space); // 生成订单 Order order = new Order(); order.setPlateNumber(plateNumber); order.setAmount(amount); order.setPayStatus(0); orderMapper.insert(order); return Result.success(amount); }

Duration.between算出的minutes是总停车分钟数。(chargeMinutes + 59) / 60是向上取整的常用写法,比如61分钟算2小时。BigDecimal做金额运算,避免double精度丢失。参数说明:freeMinutes和hourlyRate从billing_rule表读,方便后台调整。

4.3 分页查询和条件筛选:MyBatis-Plus怎么配

车辆记录和订单列表一般需要分页。用MyBatis-Plus的话,先配分页插件:

@Configuration public class MybatisPlusConfig { @Bean public MybatisPlusInterceptor mybatisPlusInterceptor() { MybatisPlusInterceptor interceptor = new MybatisPlusInterceptor(); interceptor.addInnerInterceptor(new PaginationInnerInterceptor(DbType.MYSQL)); return interceptor; } }

然后service里这样写:

public Page<VehicleVO> pageQuery(int pageNum, int pageSize, String plateNumber) { Page<Vehicle> page = new Page<>(pageNum, pageSize); LambdaQueryWrapper<Vehicle> wrapper = new LambdaQueryWrapper<>(); if (plateNumber != null && !plateNumber.isEmpty()) { wrapper.like(Vehicle::getPlateNumber, plateNumber); } wrapper.orderByDesc(Vehicle::getEntryTime); vehicleMapper.selectPage(page, wrapper); // 转换成VO返回 return page.convert(this::toVO); }

PaginationInnerInterceptor必须加,否则selectPage不会真正分页。LambdaQueryWrapper用方法引用写条件,避免字段名写错。orderByDesc按入场时间倒序,最新的记录排前面。

提示:如果分页查出来total为0但数据明明存在,检查分页插件有没有被Spring扫描到。MybatisPlusConfig要放在@SpringBootApplication同级或子包下。

5. 前后端联调与避坑:跨域、时间格式、接口404怎么排查

5.1 跨域配置:开发阶段用CorsConfig还是proxy

前后端分离项目,前端跑在localhost:8081,后端跑在localhost:8080,浏览器会拦跨域请求。两种解法:后端加CORS配置,或者前端配proxy。我一般两个都配,开发阶段用proxy,生产环境用Nginx。

后端CORS配置:

@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("*")比allowedOrigins("*")更安全,因为后者在allowCredentials(true)时会报错。maxAge(3600)表示预检请求缓存1小时,减少OPTIONS请求次数。

前端proxy配置(vue.config.js):

module.exports = { devServer: { proxy: { '/api': { target: 'http://localhost:8080', changeOrigin: true } } } }

changeOrigin: true让后端收到的请求头里的Host是后端地址,避免一些框架的Host校验。

5.2 时间格式:前端显示和数据库存储不一致

常见现象:数据库里存的是2024-01-15 10:30:00,前端显示成2024-01-15T10:30:00.000+08:00。原因是SpringBoot默认用Jackson序列化Date,格式是ISO 8601。解决方式是在application.yml里配:

spring: jackson: date-format: yyyy-MM-dd HH:mm:ss time-zone: GMT+8

或者在实体类的Date字段上加@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss", timezone = "GMT+8")。推荐后者,因为只影响需要格式化的字段,不会全局改。

5.3 接口404:从URL、方法、参数三个方向查

接口404是联调阶段最高频的问题。排查顺序:先看前端请求的URL和后端@RequestMapping是否完全一致,包括大小写和斜杠;再看请求方法(GET/POST/PUT/DELETE)是否匹配;最后看参数是@RequestParam还是@RequestBody,前端传的是query还是body。

常见翻车场景:后端写@PostMapping("/entry"),前端用axios.get('/api/vehicle/entry'),方法不匹配,报405而不是404,但浏览器控制台可能显示成404。另一个场景:后端类上写了@RequestMapping("/vehicle"),方法上写了@PostMapping("/entry"),完整路径是/vehicle/entry,前端却请求/api/vehicle/entry,少了/api前缀。要么后端加server.servlet.context-path=/api,要么前端baseURL改成http://localhost:8080。

5.4 避坑清单:5条血泪经验

现象一:启动报“Failed to configure a DataSource”。原因:application.yml里数据库配置没写全,或者spring.datasource.url拼写错误。 解决:检查url、username、password、driver-class-name四项是否都有值,url里数据库名是否和实际一致。

现象二:前端请求返回“Access-Control-Allow-Origin”错误。原因:后端CORS没配,或者配了但allowedOrigins写了具体域名而前端端口变了。 解决:开发阶段用allowedOriginPatterns("*"),或者前端配proxy走同源。

现象三:车辆出场计费金额总是0。原因:entry_time存的是Date,取出来转LocalDateTime时区不对,导致Duration算出来是负数或0。 解决:统一用LocalDateTime存时间,或者Date转Instant时指定ZoneId.systemDefault()。

现象四:MyBatis-Plus分页查不出总数。原因:分页插件没注册,或者selectPage的Page对象泛型不对。 解决:确认MybatisPlusConfig里有PaginationInnerInterceptor,且Page<Vehicle>的泛型是实体类。

现象五:Vue打包后放到SpringBoot的static目录,刷新页面404。原因:Vue是单页应用,路由用history模式,刷新时请求的是后端路径,后端没有对应接口。 解决:后端加一个配置,把所有非/api开头的请求转发到index.html;或者Vue路由改用hash模式。

@Controller public class IndexController { @RequestMapping(value = "/{path:[^\\.]*}") public String forward() { return "forward:/index.html"; } }

这段代码把所有不带点号的路径转发到index.html,让Vue路由接管。注意不要拦截/api开头的请求,否则接口全挂。

6. 从能跑到能答辩:几个让项目加分的小技巧

6.1 用定时任务自动释放超时占用车位

停车场系统有个现实问题:车辆入场后一直不出场,车位永远被占。可以加一个定时任务,每天凌晨检查入场超过24小时且未出场的记录,自动标记为异常并释放车位。

@Component public class ParkingScheduledTask { @Autowired private VehicleMapper vehicleMapper; @Autowired private ParkingSpaceMapper spaceMapper; @Scheduled(cron = "0 0 2 * * ?") public void releaseTimeoutSpaces() { List<Vehicle> timeoutList = vehicleMapper.selectTimeoutVehicles(24); for (Vehicle v : timeoutList) { ParkingSpace space = spaceMapper.selectById(v.getSpaceId()); if (space != null) { space.setStatus(0); spaceMapper.updateById(space); } } } }

@Scheduled(cron = "0 0 2 * * ?")表示每天凌晨2点执行。selectTimeoutVehicles(24)查入场超过24小时的记录。这个功能在答辩时很加分,因为它体现了对真实业务场景的考虑。

6.2 用ECharts做车位使用率看板

前端加一个数据看板,用ECharts画车位使用率饼图和今日收入柱状图。后端提供一个统计接口:

@GetMapping("/statistics") public Result statistics() { long total = spaceMapper.selectCount(null); long occupied = spaceMapper.selectCount( new LambdaQueryWrapper<ParkingSpace>().eq(ParkingSpace::getStatus, 1) ); BigDecimal todayIncome = orderMapper.selectTodayIncome(); Map<String, Object> map = new HashMap<>(); map.put("total", total); map.put("occupied", occupied); map.put("free", total - occupied); map.put("todayIncome", todayIncome); return Result.success(map); }

前端用echarts的pie和bar组件渲染。这个看板不需要太复杂,但能让答辩老师一眼看到“数据可视化”能力。

6.3 接口文档和测试:用Postman跑一遍全流程

答辩前一定要用Postman把“入场→查询→出场→支付”全流程跑一遍。建一个Collection,按顺序调接口,检查每一步的返回值和数据库状态。常见问题:入场后车位状态没变、出场后订单没生成、支付后状态没更新。这些都要在Postman里验证。

我自己的习惯是:每改一次service层的计费逻辑,就用Postman重新跑一遍出场接口,对比金额和预期是否一致。这个习惯帮我省了很多答辩现场的尴尬。

6.4 论文里怎么描述技术选型

论文的技术选型章节,不要只写“用了SpringBoot和Vue”,要写清楚为什么选。比如:SpringBoot的自动配置和起步依赖减少了XML配置,适合快速开发中小型管理系统;Vue的组件化和双向绑定提高了前端开发效率;MySQL的InnoDB引擎支持事务,适合停车计费这种需要数据一致性的场景。把这些理由写进去,比堆技术名词更有说服力。

希望帮到你。

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

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

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

立即咨询