☰
停车场管理系统实战:计费规则、并发控制与数据库设计
2026/10/15 18:07:49 网站建设 项目流程

简介:面向高校计算机相关专业毕业设计或课程设计,一份停车场管理系统毕业设计资料囊括了从硬件交互到软件实现的一体化方案。内容覆盖车牌识别、车位状态管理、停车计费、预约与异常报警等核心模块,并涉及数据库表设计与索引优化、Spring Boot/Django后端、MySQL/MongoDB存储、OpenCV与TensorFlow识别算法以及支付接口对接等关键技术点,适合作为综合实践项目的参考与二次开发基础。

资源包共592个文件,压缩包大小约142.93MB,主要包含Java源码与class文件、XML配置、前端HTML/JS页面、数据库SQL脚本及依赖jar包等,目录结构完整清晰,便于按模块查阅和学习。目前已有2528人学习浏览,说明该方案在实际下载使用中具有一定参考价值。通过梳理项目源码、配置和文档,读者可以理解系统各层职责,并在此基础上扩展功能或改进实现。

1. 毕业设计-停车场管理系统:难点不在界面,而在计费边界与并发

停车场管理系统是毕业设计里出镜率极高的题目,但和外观相反,真正卡住人的从来不是页面漂不漂亮,而是免费时长、跨天费率、日封顶、并发抢车位这些看不见的分支逻辑。一个入场一个出场看似两个按钮,落地时要同时处理“车满了怎么办”“同一辆车重复进场怎么办”“前端显示金额和后端结算金额不一致怎么办”等一系列问题。本文按一线工程师做这类管理系统的习惯,把选型理由、六张核心表、进出场与计费实现、Vue前端联动讲透,再给五个高频翻车现场。适合正在选题、或已经开工想少走弯路的同学;也适合想用停车场管理系统证明自己能独立做完一个完整业务闭环的新手。

2. 技术选型与架构:为什么Spring Boot + Vue + MySQL是稳妥答案

2.1 选型逻辑:Servlet、SSM还是Spring Boot,写给想标新立异的人

我见过不少同学在选型上纠结很久,最后要么选了最原始的Servlet+JSP,要么选了配置繁琐的SSM。先说结论:如果你不是冲着“我就要用某某技术”去答辩,最稳的答案就是Spring Boot + Vue + MySQL。原因很实际:Spring Boot把配置和部署都收拢到约定里,一个人两周左右就能写出完整项目;MyBatis Plus处理单表CRUD非常省事;前端用Vue做页面和接口联调,整体节奏是毕业设计能驾驭的。

下面是三种后端方案的直观对比,方便你判断自己该走哪条路。

方案上手速度答辩解释成本代码量与维护适合场景
Servlet + JSP快低代码组织较散时间极紧,只要页面能跑
SSM(Spring MVC + Spring + MyBatis)中中配置文件多想展示经典JavaEE功底
Spring Boot + MyBatis Plus快低约定优于配置,代码量最少多数人的最优选择

我一般会建议选Spring Boot这条线,不是因为SSM不能做,而是毕业设计周期就那么长,把时间省下来打磨计费规则和演示效果,比在XML配置文件里折腾更有价值。前端用Vue 3配合Element Plus,组件现成,表格、表单、弹窗都能快速搭好,而且组件库的样式本身就有“成品感”,答辩时观感好很多。

2.2 六张表撑起一个停车场:数据库设计与建表SQL

数据库设计是停车场管理系统的地基,很多同学一上来就只建一张停车记录表,结果做到统计报表时发现字段不够用,只能回头改表。我建议从一开始就按六张表来设计:车位表、车辆表、停车记录表、计费规则表、系统用户表、支付订单表。这六张表基本覆盖了进出场、收费、登录、统计四类核心诉求。

-- 1. 车位表 CREATE TABLE parking_space ( id BIGINT PRIMARY KEY AUTO_INCREMENT, space_no VARCHAR(20) NOT NULL COMMENT '车位编号,如 A-01', area_id VARCHAR(20) DEFAULT 'A' COMMENT '区域编号', status TINYINT NOT NULL DEFAULT 0 COMMENT '0空闲 1占用', type TINYINT NOT NULL DEFAULT 0 COMMENT '0普通 1新能源', UNIQUE KEY uk_space_no (space_no) ) COMMENT '车位表'; -- 2. 车辆表 CREATE TABLE vehicle ( id BIGINT PRIMARY KEY AUTO_INCREMENT, plate_no VARCHAR(20) NOT NULL UNIQUE COMMENT '车牌号', owner_name VARCHAR(50) COMMENT '车主姓名', phone VARCHAR(20) COMMENT '联系电话', vehicle_type TINYINT NOT NULL DEFAULT 0 COMMENT '0临时车 1月卡', create_time DATETIME DEFAULT CURRENT_TIMESTAMP ) COMMENT '车辆表'; -- 3. 停车记录表 CREATE TABLE parking_record ( id BIGINT PRIMARY KEY AUTO_INCREMENT, record_no VARCHAR(30) NOT NULL COMMENT '业务单号', plate_no VARCHAR(20) NOT NULL COMMENT '车牌号', space_id BIGINT COMMENT '车位ID', enter_time DATETIME NOT NULL COMMENT '入场时间', exit_time DATETIME COMMENT '出场时间', total_minutes INT COMMENT '停车时长(分钟)', total_amount DECIMAL(10,2) COMMENT '应收金额', status TINYINT NOT NULL DEFAULT 0 COMMENT '0在场 1已离场 2异常', create_time DATETIME DEFAULT CURRENT_TIMESTAMP, UNIQUE KEY uk_record_no (record_no), KEY idx_plate (plate_no), KEY idx_status (status) ) COMMENT '停车记录表'; -- 4. 计费规则表 CREATE TABLE fee_rule ( id BIGINT PRIMARY KEY AUTO_INCREMENT, rule_name VARCHAR(50) NOT NULL COMMENT '规则名称', vehicle_type TINYINT NOT NULL DEFAULT 0 COMMENT '0临时车 1月卡', free_minutes INT NOT NULL DEFAULT 15 COMMENT '免费分钟数', first_period_minutes INT NOT NULL DEFAULT 60 COMMENT '首时段分钟数', first_period_fee DECIMAL(10,2) NOT NULL DEFAULT 5.00 COMMENT '首时段费用', hourly_fee DECIMAL(10,2) NOT NULL DEFAULT 3.00 COMMENT '超出后每小时费用', day_cap DECIMAL(10,2) NOT NULL DEFAULT 20.00 COMMENT '单日封顶金额' ) COMMENT '计费规则表'; -- 5. 系统用户表 CREATE TABLE sys_user ( id BIGINT PRIMARY KEY AUTO_INCREMENT, username VARCHAR(30) NOT NULL UNIQUE, password VARCHAR(100) NOT NULL COMMENT '存储加密后的密码', role TINYINT NOT NULL DEFAULT 1 COMMENT '0管理员 1收费员' ) COMMENT '系统用户表'; -- 6. 支付订单表 CREATE TABLE pay_order ( id BIGINT PRIMARY KEY AUTO_INCREMENT, order_no VARCHAR(30) NOT NULL COMMENT '订单号', record_id BIGINT NOT NULL COMMENT '停车记录ID', amount DECIMAL(10,2) NOT NULL COMMENT '支付金额', pay_type TINYINT NOT NULL DEFAULT 0 COMMENT '0现金 1扫码 2月卡扣减', pay_time DATETIME DEFAULT CURRENT_TIMESTAMP, status TINYINT NOT NULL DEFAULT 0 COMMENT '0未支付 1已支付 2已退款', UNIQUE KEY uk_order_no (order_no) ) COMMENT '支付订单表';

建表时有几个点要注意。金额一律用DECIMAL而不是FLOAT,因为浮点数在累加和比较时会出现精度漂移,演示时出现“应收20.00实收19.99”非常尴尬。状态字段用TINYINT配合注释,比直接存字符串更省空间,也更容易扩展;后端可以用枚举类映射这些状态值。车位编号加了唯一索引,业务上编号必须不重复。

另外这些表之间没有物理外键,只保留逻辑关联,比如parking_record里的space_id对应parking_space的id。这是后端项目里常见的做法,因为物理外键在删除和更新时容易造成连锁限制,毕业设计阶段反而增加麻烦。逻辑关联足够支撑联表查询。

2.3 后端工程目录结构:哪个包放什么一眼能说清

目录结构直接影响答辩时讲解的清晰度。我习惯把后端工程按controller、service、mapper、entity、common、config六层组织,前端那部分单独放在vue目录下。这样的分层在面试和答辩里都是标准答案:controller只做参数接收和结果返回,service放业务规则,mapper管数据库操作,entity对应表结构。

parking-system/ ├── src/main/java/com/example/parking │ ├── controller/ # 接口层:入场、出场、统计等接口 │ ├── service/ # 业务层:计费、车位分配、订单生成 │ ├── mapper/ # 数据库访问:单表CRUD和自定义SQL │ ├── entity/ # 实体类:对应六张表 │ ├── common/ # 统一返回体、异常处理、常量 │ └── config/ # 跨域、拦截器等配置 ├── src/main/resources │ └── application.yml # 数据库连接、端口等配置 └── vue-parking/ # 前端工程 ├── src/api/ # axios请求封装 ├── src/views/ # 页面组件 └── src/router/ # 路由配置

说明一下每层的边界:controller里不要出现“计算金额”这种逻辑,它只负责把前端传进来的参数交给service,再把service返回的数据包成统一结构回给前端。service是核心,计费、分配车位、生成订单都在这里做。mapper层用MyBatis Plus的BaseMapper就能覆盖大部分单表操作,复杂的统计SQL再手写。

这段结构是我在多个管理系统项目里验证过的,最大的好处是分工清楚,写完一个功能后要改接口还是改逻辑,定位到对应层就行。答辩时老师问“某个功能怎么实现的”,你能明确说出“从controller到service到mapper的完整链路”,印象分会好很多。

3. 核心功能从零落地:车辆进出登记与计费规则

3.1 车辆入场:一个接口串起车位分配与记录落库

车辆入场是停车场管理系统第一个要写透的功能。它不只是往数据库插一条记录,而是要完成三件事:检查同一辆车是否已经在场内、分配一个空闲车位、写入一条“在场”状态的停车记录。这三件事必须放在一个事务里,否则可能出现记录写进去了但车位没占用、或者车位占用了但记录查不到的情况。

@Transactional public ParkingRecord enter(String plateNo, Long spaceId) { // 1. 检查是否已有在场记录,防止同一辆车重复入场 LambdaQueryWrapper<ParkingRecord> wrapper = new LambdaQueryWrapper<>(); wrapper.eq(ParkingRecord::getPlateNo, plateNo) .eq(ParkingRecord::getStatus, 0); if (parkingRecordMapper.selectCount(wrapper) > 0) { throw new BizException("该车牌已在场内,请勿重复入场"); } // 2. 没有指定车位时,自动分配一个空闲车位 if (spaceId == null) { spaceId = allocateFreeSpace(); } // 3. 生成业务单号,格式:R + 时间戳 + 随机三位数 String recordNo = "R" + System.currentTimeMillis() + String.format("%03d", new Random().nextInt(1000)); // 4. 插入停车记录 ParkingRecord record = new ParkingRecord(); record.setRecordNo(recordNo); record.setPlateNo(plateNo); record.setSpaceId(spaceId); record.setEnterTime(LocalDateTime.now()); record.setStatus(0); parkingRecordMapper.insert(record); return record; } private Long allocateFreeSpace() { // 原子操作:把第一个空闲车位置为占用,防止并发环境下两个请求拿到同一个车位 int rows = parkingSpaceMapper.update(null, new LambdaUpdateWrapper<ParkingSpace>() .eq(ParkingSpace::getStatus, 0) .set(ParkingSpace::getStatus, 1) .last("LIMIT 1")); if (rows == 0) { throw new BizException("车位已满"); } return parkingSpaceMapper.selectOne( new LambdaQueryWrapper<ParkingSpace>() .eq(ParkingSpace::getStatus, 1) .orderByAsc(ParkingSpace::getId) .last("LIMIT 1")).getId(); }

这里最关键的一步在allocateFreeSpace方法里。常见错误是先select查一个空闲车位,再update把它改成占用,这样两个人同时入场时可能查到同一个车位,产生超卖。正确做法是直接用带条件的UPDATE把空闲车位改成占用,然后通过影响行数判断是否成功。update返回1说明抢到了,返回0说明车位已经被别人占走,继续抛“车位已满”。

@Transactional注解保证这个方法的任何一步失败,前面已经执行的数据库操作都会回滚。比如记录插入失败时,车位状态也会撤销,不会留下脏数据。recordNo不要依赖数据库自增id来做业务单号,因为自增id在演示时看起来像“1、2、3”,容易被老师追问编号规则,用时间戳加随机数更贴近真实系统。

3.2 阶梯计费到底怎么算:半小时一小时容易,跨天别算错

计费规则是停车场管理系统里最容易翻车的部分。简单的乘法谁都写得出来,但真实停车场有免费时长、首时段、超出后按小时计费、单日封顶,还有最要命的跨天场景。我见过不少同学把规则写成“超过免费时长后,按每小时价格乘以总小时数”,结果一台车停了两天两夜,费用算出来是一笔天文数字,完全不符合实际。

public BigDecimal computeFee(LocalDateTime enter, LocalDateTime exit, FeeRule rule) { long minutes = Duration.between(enter, exit).toMinutes(); if (minutes <= rule.getFreeMinutes()) { return BigDecimal.ZERO; } // 思路:按自然日切段,每一段独立计算,最后累加 BigDecimal total = BigDecimal.ZERO; LocalDateTime cursor = enter; while (cursor.isBefore(exit)) { LocalDateTime dayEnd = cursor.toLocalDate().plusDays(1).atStartOfDay(); LocalDateTime segEnd = dayEnd.isBefore(exit) ? dayEnd : exit; total = total.add(calcSegment(cursor, segEnd, rule)); cursor = segEnd; } return total; } private BigDecimal calcSegment(LocalDateTime start, LocalDateTime end, FeeRule rule) { long minutes = Duration.between(start, end).toMinutes(); // 第一时段(比如首小时)内按首时段价格收费 if (minutes <= rule.getFirstPeriodMinutes()) { return BigDecimal.valueOf(rule.getFirstPeriodFee()); } // 超出部分按小时向上取整 long extraMinutes = minutes - rule.getFirstPeriodMinutes(); int extraHours = (int) Math.ceil(extraMinutes / 60.0); BigDecimal fee = BigDecimal.valueOf(rule.getFirstPeriodFee()) .add(BigDecimal.valueOf(rule.getHourlyFee()) .multiply(BigDecimal.valueOf(extraHours))); // 单日封顶:超过封顶价只收封顶价 BigDecimal cap = BigDecimal.valueOf(rule.getDayCap()); return fee.compareTo(cap) > 0 ? cap : fee; }

这个方法的核心是按自然日把停车时长切成多段。比如22:00入场,第二天02:00出场,会被切成22:00到24:00、00:00到02:00两段,每段分别计算后再累加。这样做的好处是每段时长最多24小时,不会出现“停了两天直接乘以48小时”的错乱,而且单日封顶能正确作用在每一段上。

计费规则的参数全部放在fee_rule表里,不要在代码里写死。答辩时如果要调整价格,改数据库就能生效,比改代码重启方便得多。常见配置是临时车免费15分钟、首小时5元、超出后每小时3元、单日封顶20元;月卡车可以把免费分钟数设成很大,或者单独用vehicleType字段在service里判断直接返回零费用。还要注意一个容易被忽略的点:向上取整用的是Math.ceil而不是直接除以60取整,也就是说停了1小时01分,要按2小时收费,这才是真实停车场的逻辑。

3.3 出场结算:别在前端自己算钱,后端重算才是准的

出场结算的逻辑比入场更敏感,因为牵扯到钱。我强烈建议出场时不要信任前端传来的金额,而是用后端接口根据入场时间和当前时间重新计算。实现的步骤是:先按plateNo查到在场记录,再查对应的计费规则,计算费用,生成支付订单,最后更新停车记录的状态和车位状态。

@Transactional public PayOrder settle(String plateNo, Integer payType) { // 1. 找到在场记录 LambdaQueryWrapper<ParkingRecord> wrapper = new LambdaQueryWrapper<>(); wrapper.eq(ParkingRecord::getPlateNo, plateNo) .eq(ParkingRecord::getStatus, 0); ParkingRecord record = parkingRecordMapper.selectOne(wrapper); if (record == null) { throw new BizException("未找到在场记录"); } // 2. 查计费规则并计算金额 FeeRule rule = feeRuleMapper.selectOne( new LambdaQueryWrapper<FeeRule>() .eq(FeeRule::getVehicleType, 0)); LocalDateTime exitTime = LocalDateTime.now(); long minutes = Duration.between(record.getEnterTime(), exitTime).toMinutes(); BigDecimal amount = computeFee(record.getEnterTime(), exitTime, rule); // 3. 更新记录并生成支付订单 record.setExitTime(exitTime); record.setTotalMinutes((int) minutes); record.setTotalAmount(amount); record.setStatus(1); parkingRecordMapper.updateById(record); PayOrder order = new PayOrder(); order.setOrderNo("P" + System.currentTimeMillis()); order.setRecordId(record.getId()); order.setAmount(amount); order.setPayType(payType); order.setStatus(1); payOrderMapper.insert(order); // 4. 释放车位 parkingSpaceMapper.update(null, new LambdaUpdateWrapper<ParkingSpace>() .eq(ParkingSpace::getId, record.getSpaceId()) .set(ParkingSpace::getStatus, 0)); return order; }

settle方法里的一个关键点是金额计算放在后端,而不是让前端把算好的金额传过来。前端的系统时间可能不准,用户也可能修改本地时间,后端以服务器时间为准才能保证计费一致。出场完成后再释放车位,顺序不能反,否则车位先空闲了,结算却失败,会造成车辆还在场但车位已经显示空位的错乱。

4. 前端页面与后端对接:Vue实现车位看板与计费展示

4.1 统一返回体和axios封装:前后端联调少吵架的基础

前后端对接最大的痛点是返回格式不统一,有的接口返回对象,有的接口直接返回字符串,前端处理起来极容易出错。我习惯在后端定义一个统一的Result类,所有接口都返回同一个结构,前端再用axios拦截器统一处理。

@Data public class Result<T> { private int code; // 0成功,非0失败 private String message; // 提示信息 private T data; // 业务数据 public static <T> Result<T> ok(T data) { Result<T> r = new Result<>(); r.code = 0; r.message = "ok"; r.data = data; return r; } public static <T> Result<T> error(String message) { Result<T> r = new Result<>(); r.code = 500; r.message = message; return r; } }
import axios from 'axios' import { ElMessage } from 'element-plus' const request = axios.create({ baseURL: '/api', timeout: 10000 }) // 响应拦截器:统一处理返回体和错误提示 request.interceptors.response.use( (resp) => { const body = resp.data if (body.code !== 0) { ElMessage.error(body.message) return Promise.reject(new Error(body.message)) } return body.data }, (err) => { ElMessage.error('网络异常,请稍后重试') return Promise.reject(err) } ) export default request

这样封装之后,前端调用接口时拿到的直接就是业务数据,不用每处都判断一次code。错误提示也统一由拦截器弹出,页面代码里不用到处写try catch。baseURL设成/api,后端再配置一下跨域或使用网关转发,本地联调时基本不会遇到跨域拦截的问题。

4.2 车位占用看板的轮询刷新:前端这样写更省事

车位看板是停车场管理系统里最有展示效果的功能,一进去就能看到整个停车场的车位占用情况。我的做法是用一个响应式的车位列表,配合setInterval每5秒请求一次后端接口。毕设场景下并发量不大,轮询完全够用,比引入WebSocket要简单得多,还能在答辩时讲清楚“为什么轮询而不是长连接”这个技术取舍。

import { ref, onMounted, onUnmounted } from 'vue' import request from '../api/request' const spaceList = ref([]) let timer = null async function fetchSpaceList() { const data = await request.get('/parking/space/list') spaceList.value = data } onMounted(() => { fetchSpaceList() timer = setInterval(fetchSpaceList, 5000) }) onUnmounted(() => { clearInterval(timer) })

特别注意清理定时器这步。Vue组件卸载后,如果setInterval还在跑,会继续请求接口并尝试更新已经不存在的组件数据,控制台会不断报错。所以在onUnmounted里必须clearInterval。另一个细节是不要在组件激活时就立刻请求,先把数据加载出来再启动轮询,避免页面闪一下空数据。

轮询间隔设为5秒是比较合适的选择。太短会频繁请求后端造成无谓压力,太长则车位状态更新不及时,演示时可能出现“车已经开走了,看板还显示占用”的尴尬。如果想更平滑,可以在收到数据后对比前后两次的差异再做局部更新,但对毕业设计来说整体刷新已经足够。

4.3 收费展示与结算金额:前端计时器不能自己累加

很多同学做停车时长展示时会写一个前端定时器,每秒把秒数加一,页面看起来确实在走。这个做法有一个隐藏问题:页面只要被切到后台,定时器就被浏览器挂起,回到前台时显示的时长会比实际少一大截。更稳妥的做法是后端返回入场时间,前端只负责计算当前时间与入场时间的差值。

import { ref, onMounted, onUnmounted } from 'vue' const durationText = ref('0小时0分') let timer = null function startTimer(enterTime) { clearInterval(timer) timer = setInterval(() => { const diff = Date.now() - new Date(enterTime).getTime() const minutes = Math.floor(diff / 60000) durationText.value = `${Math.floor(minutes / 60)}小时${minutes % 60}分` }, 1000) } function stopTimer() { clearInterval(timer) } onMounted(() => startTimer(record.enterTime)) onUnmounted(stopTimer)

这段代码里的startTimer接收的是后端下发的enterTime字符串,每次刷新页面都会重新用当前时间减去入场时间,所以无论页面怎么切换、手机锁屏多久,重新回到页面后的时长都是对的。展示金额也同理,前端不要自己乘单价,而是展示后端根据“当前时刻”返回的预估金额,每隔一段时间刷新一次。真正结算时再调用出场接口让后端重算,这样前端展示和后端收费最终能对上。

5. 停车场管理系统避坑指南:五个高频翻车现场

5.1 并发抢车位:两个请求同时拿到最后一个空闲车位

现象:演示时有人同时操作两个页面入场,系统显示车位还有空余,但两张入场单竟然分配到了同一个车位。原因:先select一个空闲车位,再update改成占用,两步之间有间隙,另一个请求也select到了同一条空闲记录。解决:用UPDATE加条件“status=0”再去更新,影响行数为1才算成功。我在入场接口的allocateFreeSpace里用的就是这种方式,这也是数据库层面最便宜且有效的防超卖方案。

5.2 跨天计费算错:夜班场景一算一个准

现象:22:00入场、次日03:00出场的车,收费金额比预期高很多,甚至出现“封顶失效”的情况。原因:按“总小时数乘以单价”计算,中间经过的自然日没有切分,单日封顶根本起不到作用。解决:统一走计费器,按自然日把时长切成多段分别计算再累加,每段独立应用首时段价格、小时价格和单日封顶。这个逻辑在3.2里已经完整写出来了,关键是所有出场结算都调用同一个方法,不要自己另写一套临时算法。

5.3 数据库时间比本地时间早8小时

现象:前端录入入场时间后,列表里显示的时间比北京时间少8小时,计费结果跟着全部错乱。原因:MySQL连接串里没有设置serverTimezone,或者数据库时区配置成了UTC。解决:在JDBC连接参数里显式指定时区,同时确认服务器和数据库的时区设置一致。这个问题的特征非常明显,只要发现所有时间字段集体“少8小时”,基本就是时区问题,不是代码逻辑问题。

5.4 前端计时器误差:页面放后台一小时,回来才发现时间没走

现象:笔记本盖上盖子再打开,页面上停车时长几乎没变,但后端结算时金额却涨了很多。原因:浏览器为了省电会挂起后台标签页的定时器,前端累加逻辑因此失真。解决:前端不做秒数累加,只根据入场时间和当前时间计算差值,每次刷新都用Date.now()和入场时间重算。我在4.3里的写法就是基于这个思路,展示和时间相关的功能时优先依赖后端时间戳,不要依赖前端自增。

5.5 车牌号不规范:统计报表被脏数据毁掉

现象:演示时手滑把一个车牌录成“浙A123”,后面还有个空格,导致这位“车主”的停车记录在报表里单独成行,图表多出一个诡异的数据点。原因:入场接口没有做车牌格式校验,数据库也允许了脏数据入库。解决:在后端service里用正则校验车牌格式,比如普通燃油车牌照的常见模式,同时入库前统一trim掉首尾空格。管理员后台预留一个“异常记录修正”入口,允许在特殊情况下手工修改车牌并记录操作日志,这样既保住了演示数据的干净度,又能在答辩时体现异常处理的设计。

6. 让演示现场效果翻倍的三个动作:模拟数据、部署脚本与演示节奏

6.1 一个SQL脚本生成过去一周的模拟停车记录

空荡荡的统计报表在答辩时非常减分。我的习惯是用一个存储过程生成过去7天的模拟停车记录,让收入趋势图、车位利用率图都呈现自然的“工作日高、周末低”形态。生成数据时要注意两点:时间要分布在过去7天而不是集中在今天;金额要和计费规则保持一致,不要随便写数字,否则老师按你的费率一验算就穿帮。

DROP PROCEDURE IF EXISTS gen_demo_data; DELIMITER $$ CREATE PROCEDURE gen_demo_data() BEGIN DECLARE i INT DEFAULT 0; DECLARE v_enter DATETIME; WHILE i < 300 DO -- 生成过去7天内的随机入场时间,覆盖白天和晚上 SET v_enter = DATE_ADD( DATE_SUB(CURDATE(), INTERVAL FLOOR(RAND() * 7) DAY), INTERVAL FLOOR(RAND() * 900) MINUTE); INSERT INTO parking_record (record_no, plate_no, space_id, enter_time, exit_time, total_minutes, total_amount, status) VALUES (CONCAT('R', DATE_FORMAT(v_enter, '%Y%m%d%H%i%s'), FLOOR(RAND() * 1000)), CONCAT(ELT(1 + FLOOR(RAND() * 3), 'A', 'B', 'C'), LPAD(FLOOR(RAND() * 99999), 5, '0')), FLOOR(1 + RAND() * 20), v_enter, DATE_ADD(v_enter, INTERVAL 30 + FLOOR(RAND() * 240) MINUTE), TIMESTAMPDIFF(MINUTE, v_enter, DATE_ADD(v_enter, INTERVAL 30 + FLOOR(RAND() * 240) MINUTE)), ROUND(TIMESTAMPDIFF(MINUTE, v_enter, DATE_ADD(v_enter, INTERVAL 30 + FLOOR(RAND() * 240) MINUTE)) / 60 * 3, 2), 1); SET i = i + 1; END WHILE; END$$ DELIMITER ; CALL gen_demo_data();

执行前先确认已有数据可以清掉,否则统计会叠加旧数据。这段脚本生成300条记录足够让图表饱满,又不会让数据库查询变慢。如果你想让数据更接近真实停车场,可以在生成时对小时做一个加权:早上8点到10点和傍晚6点到8点之间多生成一些记录,凌晨少生成一些,这样折线图的峰谷形状非常自然。

6.2 部署命令与演示顺序:让系统在你的电脑上稳定跑完二十分钟

答辩现场最怕的是设备临时出状况,所以部署方式一定要简单且可重复。我一般是后端打包成单个jar包,前端构建后把静态文件放到后端工程的static目录里,这样只启动一个jar就能跑起整个系统,不用在演示电脑上装Node环境。

# 前端构建,产物默认输出到 dist 目录 npm run build # 后端打包,跳过测试减少等待时间 mvn clean package -DskipTests # 启动后端,启动后访问 http://localhost:8080 即可看到页面 java -jar target/parking-system.jar

部署完成后要按照固定的演示顺序走一遍:先打开车位看板展示实时状态,再演示入场功能、出场结算功能,最后打开统计报表页面展示数据图表。如果模拟数据脚本是在答辩前生成好的,记得把数据里的时间范围向前推一周,不要生成到“今天”,否则老师问“为什么这一条记录显示刚刚入场”时不好解释。我自己的习惯是每次答辩前都会重置一遍演示数据,清空停车记录表,重新调用存储过程。这个习惯救过我很多次,因为开发过程中留下的测试数据往往状态混乱,演示到一半出现“车牌已在场内”“车位已满”这类报错会非常影响节奏。希望这篇笔记能帮你在毕业设计这条路上少踩几个坑。

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

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

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

立即咨询