ARTICLE · INTELLIGENCE

战地情报 · 详情页

来自尧图项目组的一线实战观察与深度解析

智慧停车场管理系统设计:并发计费与数据库建模实战避坑指南

智慧停车场管理系统设计:并发计费与数据库建模实战避坑指南

简介:这份智慧停车场管理系统设计与实现资料包,面向计算机相关专业毕业生、课程设计学生及初级开发人员,提供从选题背景、需求分析、数据库设计到前后端编码的完整参考方案。压缩包共1211个文件,约146.81MB,主要包含Java/JSP页面、Java源码与class文件、SQL数据库脚本、docx/doc论文文档,同时包含VB工程文件(frm/vbp/bas)、大量png/jpg/gif界面截图与演示图、xml/yml/properties等工程配置及SVN版本文件,目录层级丰富,便于对照学习。目前已有159人学习下载。从中可以提取智慧停车场系统的车牌识别、无线通信、智能导引、车位预约、停车费用自动计算、无感支付、车位监控等模块实现思路。资料同时覆盖用户、车辆、财务等常见管理模块,获取数据库表结构、前后端交互方式与项目部署配置,既能支撑毕业设计论文撰写,也为后续功能扩展与二次开发提供基础。

1. 智慧停车场管理系统:毕设里低估了并发计费的隐藏难度

从表面看,这套系统做的是“入场抬杆、出场收费”两件事,数据库多建几张表、后台加几个管理页面就完事。但真正跑起来你会发现,高峰期同一条车道上连续几辆车排队入场,相机抓拍结果还没落库,下一帧识别又推回来了,入场记录瞬间多两条,账户余额和实收对不上。更麻烦的是,出口收费员拿着人工计时的手表,跟系统金额差了几毛钱,要么是出场精度问题,要么是跨天规则写错了边界。

这类“智慧停车场管理系统的设计与实现(论文+源码)”项目,本质是给车牌识别、停车订单、计费规则、月卡会员和车位余量之间搭一套可靠的状态流转闭环。适合两类人看:一类是毕业设计选了停车场方向,需要同时交付论文和可运行源码;另一类是刚入行的后端工程师,想用一个不算复杂的业务场景练手,把并发扣减、状态机设计和数据库建模串起来。接下来按“业务拆解 → 技术选型 → 数据库 → 核心功能 → 避坑 → 验证”这条路线,把我做这类项目时踩过的坑和沉淀下来的方案一次讲透。

2. 系统拆解与技术选型:前后端分离里的“最小可用”方案

2.1 智慧停车场到底在管哪些业务:从入场到出场的五步闭环

停车场系统的核心不是“停车”,而是“车辆状态流转”。一辆车从入口到出口,至少要经历五个状态:识别入场、分配车位、计时计费、出场校验、支付释放。整个系统的所有模块都是围绕这五个状态展开的。

入场时,相机抓拍车牌,系统先判断这辆车是临时车、月卡车还是预约车。临时车直接分配一个空闲车位并开启计时;月卡车校验有效期后放行,不产生计费订单;预约车则校验预约时段和车位预留情况。出场时反过来,先查入场记录,再根据车辆类型决定是否计费、按什么规则计费,最后完成支付或扣款、释放车位。

这个闭环里有三个容易遗漏的点。第一,入场记录必须关联到车位,否则“剩余车位数量”就无从算起;第二,出场时不能只靠车牌找订单,还需要考虑同一车牌重复入场、多次进出的情况;第三,异常车辆——比如识别失败、无入场记录的出场车——必须有一套人工处理通道。很多毕设翻车都是因为只做了“理想路径”的增删改查,把这些边界场景遗漏了。

2.2 技术栈怎么选:Spring Boot + Vue 为主线的理由

这类系统的主流技术路线是前后端分离,后端 Spring Boot,前端 Vue,数据库 MySQL,再配一个 Redis 做并发控制。也有一些用 SSM + JSP 的,优点是代码聚在一起、部署一个包就能跑,缺点是页面和业务耦合较深,答辩展示时不好讲“前后端分离”的亮点。我一般倾向于 Spring Boot + Vue,工作量不大,而且论文里可以把“前后端交互”单独写成一小章。

下面是三种常见组合的对比,方便你按自己的时间做决定。

技术组合优势主要成本适合场景
Spring Boot + Vue + MySQL + Redis前后端分离,并发处理有亮点,答辩好讲需要单独部署前端和后端,联调工作量略大时间相对充裕,想拿高分
SSM + JSP + MySQL代码量紧凑,结构简单前端展示能力弱,状态管理容易写乱时间紧张,以跑通为主
Django + Django模板 + MySQLPython 上手快,后台管理自带国内参考案例较少,招聘场景不吃香Python 基础更好的人

无论选哪条技术路线,住宿里缺不了三个模块:车牌识别对接、停车订单管理、计费规则引擎。其中计费规则引擎是真正的业务核心,单纯把费率写死在代码里是行不通的,因为答辩老师大概率会问“跨天停车怎么收费”“免费时长怎么算”“月卡过期当天怎么处理”。这些规则需要设计成数据库可配置的结构,后面第 3 章会给出具体的建表方案。

2.3 论文怎么写才配得上源码:设计与实现的对应关系

“论文+源码”这种交付形式,常见的坑是论文写得像需求文档,源码又和论文对不上。我见过不少同学的程序实现了预约停车,但论文里一个字没提,答辩时只能现场口头补,非常被动。稳妥的做法是让论文的每一章都能在源码里找到对应落点。

需求分析对应业务闭环:要把临时车、月卡车、预约车三种角色的处理流程讲清楚。概要设计对应系统架构和模块划分:画清楚管理端、用户端、硬件对接层之间的调用关系。详细设计与数据库设计对应表结构和接口设计:建表语句、核心接口的输入输出参数。系统实现对应最大的几个功能:入场登记、出场计费、余位扣减、月卡管理。测试章节则对应你实际做过的验证步骤,比如下面第 6 章模拟入场的完整流程,就是可以直接写进测试章节的真实操作记录。

3. 数据库建模:五张核心表把停车状态管住

3.1 车位表、车辆表与入场记录表的建表语句

停车场系统的表数量不需要太多,但每张表的字段设计要能支撑状态流转。我最常用的核心表有五张:车位表、车辆表、入场记录表、计费规则表、月卡表。先看前三张的建表语句。

-- 车位表:记录车位的物理位置与当前状态 CREATE TABLE parking_space ( id BIGINT PRIMARY KEY AUTO_INCREMENT COMMENT '车位ID', space_no VARCHAR(16) NOT NULL COMMENT '车位编号,如 A-001', area_name VARCHAR(32) DEFAULT NULL COMMENT '区域名,如地面/负一层', status TINYINT NOT NULL DEFAULT 0 COMMENT '0空闲 1占用 2锁定 3故障', vehicle_id BIGINT DEFAULT NULL COMMENT '当前占用车辆ID,空表示空闲', create_time DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, update_time DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, UNIQUE KEY uk_space_no (space_no) ) COMMENT='车位信息表'; -- 车辆表:保存已登记车辆基础信息 CREATE TABLE vehicle ( id BIGINT PRIMARY KEY AUTO_INCREMENT COMMENT '车辆ID', plate_no VARCHAR(16) NOT NULL COMMENT '标准车牌号,统一大小写', plate_color TINYINT NOT NULL DEFAULT 0 COMMENT '0蓝 1绿 2黄 3白', owner_name VARCHAR(32) DEFAULT NULL COMMENT '车主姓名', phone VARCHAR(20) DEFAULT NULL COMMENT '联系电话', vehicle_type TINYINT NOT NULL DEFAULT 0 COMMENT '0临时 1月卡 2预约', create_time DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, UNIQUE KEY uk_plate_no (plate_no) ) COMMENT='车辆信息表'; -- 入场记录表:一次停车会话的主记录 CREATE TABLE parking_record ( id BIGINT PRIMARY KEY AUTO_INCREMENT COMMENT '记录ID', record_no VARCHAR(32) NOT NULL COMMENT '业务单号,如IN20250101001', vehicle_id BIGINT NOT NULL COMMENT '车辆ID', space_id BIGINT DEFAULT NULL COMMENT '分配的车位ID', entry_time DATETIME NOT NULL COMMENT '入场时间', exit_time DATETIME DEFAULT NULL COMMENT '出场时间,空表示在场', status TINYINT NOT NULL DEFAULT 0 COMMENT '0在场 1已完成 2异常离场', amount DECIMAL(10,2) DEFAULT 0.00 COMMENT '实收金额', create_time DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, INDEX idx_vehicle_entry (vehicle_id, entry_time), INDEX idx_status (status) ) COMMENT='停车入场记录表';

三张表的分工是:车位表管“哪里可以停”,车辆表管“谁的车”,入场记录表管“停了多少次、停了多久”。注意几个容易被忽略的设计点:车牌号加了唯一索引,因为同一辆车的车牌必须全局唯一,否则后面查月卡和查在场记录都会出现歧义;入场记录表建了idx_vehicle_entry组合索引,这是出场时查询“某辆车最近一条在场记录”的核心索引;车位状态用 0/1/2/3 四个数字表示,而不是直接用字符串,因为后续做余位统计时COUNT(*)加GROUP BY status的效率更高。

3.2 计费规则表与月卡表:让金额计算不再写死在代码里

计费规则是这类系统里最容易出问题的表。如果把“每半小时 2 元”写死在代码里,改一次费率就要改代码重新部署,非常被动。正确的做法是把计费规则设计成可配置的数据行,后端只做计算引擎。下面给出计费规则表和月卡表的参考结构。

-- 计费规则表:按车辆类型和时段配置费率 CREATE TABLE charging_rule ( id BIGINT PRIMARY KEY AUTO_INCREMENT COMMENT '规则ID', rule_name VARCHAR(32) NOT NULL COMMENT '规则名,如临停首小时', vehicle_type TINYINT NOT NULL DEFAULT 0 COMMENT '0临时 1月卡 2预约', time_mode TINYINT NOT NULL DEFAULT 0 COMMENT '0按自然日 1按进入起24小时', free_minutes INT NOT NULL DEFAULT 0 COMMENT '免费时长,单位分钟', unit_minutes INT NOT NULL DEFAULT 30 COMMENT '计费单元,单位分钟', unit_price DECIMAL(10,2) NOT NULL DEFAULT 0.00 COMMENT '每个计费单元价格(元)', max_day_price DECIMAL(10,2) DEFAULT NULL COMMENT '单日封顶金额,空表示不封顶', status TINYINT NOT NULL DEFAULT 1 COMMENT '1启用 0停用', create_time DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ) COMMENT='计费规则表'; -- 月卡表:会员车有效期管理 CREATE TABLE monthly_card ( id BIGINT PRIMARY KEY AUTO_INCREMENT COMMENT '月卡ID', vehicle_id BIGINT NOT NULL COMMENT '车辆ID', card_no VARCHAR(32) NOT NULL COMMENT '卡号', start_date DATE NOT NULL COMMENT '生效日期', end_date DATE NOT NULL COMMENT '失效日期', monthly_fee DECIMAL(10,2) NOT NULL DEFAULT 0.00 COMMENT '月缴金额', status TINYINT NOT NULL DEFAULT 1 COMMENT '1有效 0停用 2过期', create_time DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, UNIQUE KEY uk_card_no (card_no), INDEX idx_vehicle_date (vehicle_id, start_date, end_date) ) COMMENT='月卡信息表';

计费的计算逻辑是:先读charging_rule里对应的vehicle_type,取到free_minutes、unit_minutes、unit_price三个核心参数,然后算出停车总时长。比如免费 15 分钟、每 30 分钟收 2 元,停 80 分钟就是80 - 15 = 65分钟为计费时长,65 / 30 = 2个完整单元向上取整为 3 个单元,金额 6 元。这里“向上取整”和“是否包含免费时长”必须放在规则表里,否则代码写死一个版本后,后期改需求就要返工。

3.3 数据库设计里最容易被答辩老师问倒的三个点

设计这套表时会遇到三个经典的追问,提前想清楚能少踩不少坑。第一个是“入场记录表为什么不用车牌号直接做主键”,因为同一辆车完全可以多次入场,比如上午进去、中午出来、下午又进去,用车牌做唯一键会让第二次入场写不进去,所以必须用自增主键加车牌索引。第二个是“车辆表和入场记录表为什么分开”,因为车辆属性是静态的——颜色、车主、车牌,入场记录是动态的——入场时间、出场时间、车位、金额,混在一张表里会导致更新异常,这是第三范式最基本的拆分逻辑。第三个是“删除数据到底用 DELETE 还是状态字段”,停车场系统里的车位、车辆、月卡都需要留审计痕迹,推荐用状态字段做逻辑删除,只有停车记录表里的异常数据才允许物理删除。

4. 核心功能落地:车牌识别对接与计费计算

4.1 车牌识别的接入方式:本地SDK与云端API的取舍

车牌识别是停车场系统的入口前置条件,识别不准,后面全乱。常见做法有两种:一种是在本地服务器部署车牌识别算法库的 SDK,图片不出场,识别结果直接回调后端接口,优点是低延迟、不依赖带宽,缺点是部署环境要满足依赖;另一种是云端 API 识别,把相机抓拍的图片传上去,等回调返回车牌号和置信度,优点是接入简单,但每次请求都耗时且按次计费。

毕设和中小型项目一般选本地 SDK,因为不需要额外申请密钥,而且离线环境也能跑通。接入时重点关注识别结果里的plate_no、plate_color、confidence三个字段。其中confidence是置信度,低于阈值(比如 0.8)的识别结果不要直接落库,而应该进入人工处理队列,让岗亭值班员在界面上确认。这看起来是小事,但高峰期相机角度偏移时,置信度往往会骤降,没有这层兜底就会出现大量错车记录。

4.2 入场登记接口:一次“先查后写”避免重复抬杆

入场登记的完整逻辑是:收到相机识别结果 → 校验车牌格式 → 查询车辆是否存在 → 若存在且为月卡则校验有效期 → 分配空闲车位 → 写入入场记录 → 更新车位状态 → 返回是否抬杆。下面这个接口是这套流程的核心。

@PostMapping("/api/entry") public Result entry(@RequestBody EntryRequest request) { // 步骤1:对识别结果做归一化,统一大写、去掉分隔符 String plateNo = normalizePlate(request.getPlateNo()); // 步骤2:基本格式校验,不合法直接返回 if (!isValidPlate(plateNo)) { return Result.fail("车牌格式不正确"); } // 步骤3:检查车辆是否已经在场内,防止重复入场 Vehicle vehicle = vehicleMapper.selectByPlateNo(plateNo); List<ParkingRecord> activeRecords = parkingRecordMapper .selectByVehicleAndStatus(vehicle == null ? null : vehicle.getId(), 0); if (!activeRecords.isEmpty()) { return Result.fail("该车已在场内,请勿重复入场"); } // 步骤4:分配空闲车位,使用FOR UPDATE锁定该行,防止并发分配同一车位 ParkingSpace space = parkingSpaceMapper.selectOneFreeSpaceForUpdate(); if (space == null) { return Result.fail("车位已满,请等待"); } // 步骤5:如果是月卡,校验有效期,过期则按临时车处理 if (vehicle != null && vehicle.getVehicleType() == 1) { MonthlyCard card = monthlyCardMapper.selectValidByVehicleId(vehicle.getId()); if (card == null) { vehicle.setVehicleType(0); // 月卡过期降级为临时车 vehicleMapper.updateById(vehicle); } } // 步骤6:创建入场记录并更新车位占用状态 ParkingRecord record = new ParkingRecord(); record.setRecordNo(generateRecordNo()); record.setVehicleId(vehicle == null ? addNewVehicle(plateNo, request.getPlateColor()) : vehicle.getId()); record.setSpaceId(space.getId()); record.setEntryTime(new Date()); record.setStatus(0); parkingRecordMapper.insert(record); space.setStatus(1); space.setVehicleId(record.getVehicleId()); parkingSpaceMapper.updateById(space); return Result.ok("入场成功,请通行"); }

这段代码把“重复入场”和“车位分配”两道雷拆掉了。selectByVehicleAndStatus查的是状态为 0 的在场记录,只要还有在场记录就拒绝第二次入场,这样相机连拍几帧也不会重复落单。selectOneFreeSpaceForUpdate是在数据库层面用SELECT ... FOR UPDATE锁住空闲车位,两个并发请求同时进来时,只有先到的事务能拿到车位行锁,后到的会等待或查不到空闲车位,从根上避免了一辆车同时占两个车位。

参数上需要注意vehicleType的降级处理:月卡到期当天,出场时如果闸机不校验,就会产生“已过期还能抬杆”的漏洞。我的习惯是入场和出场两处都校验月卡状态,入场时将过期月卡变成临时车并重新关联计费规则。这个逻辑不做,测试阶段很难发现,等正式上线之后,一个月卡客户一年多没续费还能进场,后台报表金额就会对不上。

4.3 出场计费接口:按停车时长动态计算金额

出场计费比入场复杂,因为它涉及规则匹配、时长计算、金额精度和订单生成四个环节。下面给出出场流程的代码骨架。

@PostMapping("/api/exit") public Result exit(@RequestBody ExitRequest request) { String plateNo = normalizePlate(request.getPlateNo()); // 步骤1:查询该车牌最晚一条在场记录 ParkingRecord record = parkingRecordMapper .selectLatestActiveByPlateNo(plateNo); if (record == null) { return Result.fail("未找到入场记录,请走人工通道"); } // 步骤2:取出对应车辆类型,确定是否免费(月卡不计费) Vehicle vehicle = vehicleMapper.selectById(record.getVehicleId()); BigDecimal payableAmount = BigDecimal.ZERO; if (vehicle.getVehicleType() != 1) { // 步骤3:按车辆类型查计费规则 ChargingRule rule = chargingRuleMapper .selectEnableByVehicleType(vehicle.getVehicleType()); long totalMinutes = Duration.between( record.getEntryTime().toInstant(), new Date().toInstant()).toMinutes(); long billableMinutes = totalMinutes - rule.getFreeMinutes(); if (billableMinutes > 0) { // 按计费单元向上取整计算金额 BigDecimal units = BigDecimal.valueOf( Math.ceil(billableMinutes / (double) rule.getUnitMinutes())); payableAmount = units.multiply(rule.getUnitPrice()); // 判断单日封顶金额 if (rule.getMaxDayPrice() != null) { payableAmount = payableAmount.min(rule.getMaxDayPrice()); } } } // 步骤4:生成出场订单,更新记录状态与释放车位 record.setExitTime(new Date()); record.setAmount(payableAmount); record.setStatus(1); parkingRecordMapper.updateById(record); ParkingSpace space = parkingSpaceMapper.selectById(record.getSpaceId()); space.setStatus(0); space.setVehicleId(null); parkingSpaceMapper.updateById(space); return Result.ok(payableAmount.toPlainString()); }

出场逻辑里最难的是“边界时间计算”。Duration.between得到的是精确到分钟的总时长,但入场时间是 23:50、出场时间是次日 00:10 这种跨天场景,分钟数不能直接减,否则会出现负数。更稳妥的做法是先换算成时间戳毫秒数再除以 60000,最后用向上取整计算计费单元数。这里的Math.ceil决定了“不足一个计费单元按一个单元算”,是行业里的默认规则。

封顶金额的判断必须放在单元计费之后。如果先判断封顶再算单元,就会出现一种漏洞:停 12 小时应该封顶 20 元,但因为前 2 小时已经按单元收了 10 元,后面逻辑以为没超封顶就继续累加,最终金额超了。所以我的顺序是:先算完整单元金额,再跟maxDayPrice取最小值。

4.4 剩余车位的并发扣减:从悲观锁到Redis原子操作

车位余量是停车场系统里高频访问的数据,入口大屏和用户端小程序的剩余车位展示都靠它,而且入场每次都会变更。如果直接用数据库行做update parking_space set status=1 where id=?,高并发下会出现余量不一致的问题。我在第 2 章提过 Redis 是推荐选项,这里展开说为什么。

入场扣减余量的核心要求是“减到 0 就不能再减了”。用数据库做,必须依赖SELECT ... FOR UPDATE锁住汇总行,锁等待时间会拖慢响应;用 Redis 的DECR命令做原子自减,减完顺手看一眼返回值,如果是负数,再用INCR加回去并返回“车位已满”,效率高得多。代码示例:

// 初始化时用总车位数写入Redis // redisTemplate.opsForValue().set("parking:remain", totalSpaceCount); // 入场时原子扣减,返回值小于0说明已经扣超了 Long remain = redisTemplate.opsForValue().decrement("parking:remain"); if (remain < 0) { redisTemplate.opsForValue().increment("parking:remain"); return Result.fail("车位已满,请等待"); } // 出场时原子增加 redisTemplate.opsForValue().increment("parking:remain");

这种做法唯一的隐患是 Redis 里余量和数据库里真实车位状态可能临时不一致。比如某辆车入场后程序崩溃,Redis 扣了但数据库没改。所以我的习惯是以数据库为准做定期校准:每小时用COUNT(*)统计status=0的空闲车位数,刷新一次 Redis 缓存。这个定时任务在论文的系统实现章节里也算一个功能点,答辩时能多一个亮点。

5. 避坑实录:计费对不上、并发翻车、论文被反问的三个教训

5.1 同一辆车重复入场拉出一条订单:在“在场状态”上栽跟头

现象:测试时用同一个车牌连续调用两次入场接口,第 1 次返回成功,第 2 次也返回成功,数据库里出现两条状态为“在场”的入场记录。

原因:入场接口只判断了“车辆表里有没有这个车牌”,没有判断“该车牌当前有没有未出场的记录”。相机在高频率抓拍时,同一辆车的识别结果会以几乎相同的时间戳推送两次,第二次请求完全绕过了校验逻辑。

解决:入场前先按车牌查在场记录,只要存在状态为 0 的记录就拒绝本次入场。同时给parking_record表加一个业务上的确认机制:在车辆在场状态下,同一车牌只有一条status=0的数据。实际编码时用selectLatestActiveByPlateNo这个查询,命中即拒绝。

5.2 临停金额总比人工少几毛:分钟取整规则没定清楚

现象:手动计算停车费 15 元,系统只算了 14 元。停 65 分钟、免费 15 分钟、按 30 分钟 2 元计费,人工习惯按“不足半小时按半小时算”,但代码里用了Math.floor向下取整,65 减 15 等于 50 分钟,50 除以 30 向下取整只算了 1 个单元。

原因:计费逻辑里的取整方向没和业务约定对齐。规则表里只配置了unit_minutes,没有明确是向上取整还是向下取整。

解决:把取整方向直接写进代码注释和论文的详细设计里,统一用Math.ceil向上取整。另外在charging_rule表增加一个round_type字段(0向下 1向上),把取整方式也变成可配置项。这样后期改费率时,规则调整不需要动代码。

5.3 新能源车牌识别后少一位:字符规范化没有做

现象:绿牌新能源车入场,识别结果返回“粤AD12345”,但库里却存成了“粤AD1234 5”或者小写字母混入,后面出场按原车牌查不到记录,只能人工放行。

原因:不同识别 SDK 对新能源车牌最后一位字母的处理不一致,有的返回完整位,有的把汉字和字母混在一个字段里,导致车牌长度不稳定。再加上代码里没有做toUpperCase()和空格去除。

解决:写一个normalizePlate方法,统一去掉空格和特殊字符,英文字母全部转大写。同时可以用正则校验车牌格式:^[京津冀晋蒙辽吉黑沪苏浙皖闽赣鲁豫鄂湘粤桂琼川贵云陕甘青宁新藏][A-Z][A-HJ-NP-Z0-9]{5,6}$。注意第 7 位新能源车牌可以多一位,正则要放宽长度。

5.4 月卡过期自动抬杆:闸机放行条件没做二次判断

现象:一张月卡 1 月 31 日到期,2 月 1 日进场时道闸照常抬杆,后台也没有任何提醒。用户自己不发现,管理员月底对账时才发现少收了一个月费用。

原因:闸机放行条件是“车牌存在 + 入场记录创建成功”,而不是“车辆处于正常月卡有效期”。入场接口里的月卡过期降级逻辑没有生效,或根本没有写。

解决:入场时先查月卡有效日期,end_date小于当前日期的直接按临时车处理,更新vehicle_type为 0。出场时再做一次校验,如果发现该车本应是月卡但已过期,按临停计费并把费用补收。两处校验不能只做一处,入口校验是防进场,出口校验是防查不到记录。

6. 快速验证:用命令行模拟一次完整入场出场

6.1 用 curl 模拟相机识别推送

不依赖真实相机硬件,直接调用后端接口就能完成端到端验证。先启动后端服务,然后用下面的命令模拟一次入场。

curl -X POST http://localhost:8080/api/entry \ -H "Content-Type: application/json" \ -d '{"plateNo":"粤B12345","plateColor":0}' curl -X POST http://localhost:8080/api/exit \ -H "Content-Type: application/json" \ -d '{"plateNo":"粤B12345","plateColor":0}'

第一条命令返回“入场成功,请通行”,第二条命令返回实收金额。如果第二条返回“未找到入场记录”,说明入场的数据没有正确落库,优先检查parking_record表里是否生成了记录,以及vehicle_id是否被正确关联。

6.2 一条龙状态检查:从数据库确认状态流转

接口返回成功不等于状态流转正确,还要直接查数据库确认。我用得最多的一组排查 SQL:

-- 1. 查车辆是否生成 SELECT * FROM vehicle WHERE plate_no = '粤B12345'; -- 2. 查入场记录状态:应为0在场,出场后应为1已完成 SELECT record_no, entry_time, exit_time, status, amount FROM parking_record WHERE vehicle_id = (SELECT id FROM vehicle WHERE plate_no = '粤B12345'); -- 3. 查车位状态:入场后为1占用,出场后应恢复为0空闲 SELECT space_no, status FROM parking_space WHERE id = (SELECT space_id FROM parking_record WHERE record_no = 'IN20250101001');

这三个查询分别验证了车辆表、入场记录表和车位表的数据联动。如果入场后车位状态还是 0,说明parking_space更新代码没有执行;如果出场后金额是 0,说明charging_rule表里没有配置对应vehicle_type的规则。测试时我给每条记录都写清record_no,查起来一目了然。

6.3 场景边界怎么测:一张表覆盖所有流转

简单的出入测试只能说明代码能跑,谈不上可靠。我习惯准备一张场景测试表,把异常和边界情况全列进去,逐一跑通再收工。

场景输入预期结果
正常临时车蓝牌入场→出场生成停车订单,金额按规则计算
重复入场同一车牌连续调两次入场第二次返回“该车已在场内”
月卡过期入场过期月卡绑定车牌入场自动降级为临时车并开始计费
跨天收费入场23:50,出场次日00:10按总时长 20 分钟计算,无负值
全额封顶停车 12 小时金额不超过max_day_price
无入场记录出场直接调用出场接口返回“未找到入场记录,请走人工通道”

这组测试跑完,代码里 90% 的边界问题都会暴露出来。剩下的 10% 属于真实硬件对接问题,比如专用相机推送的数据格式、图片传输超时,这些在开发环境里只能用 Mock 模拟,真正联调时再补一轮。

做完这些验证,整套系统的核心闭环就算真正立住了。我做了这么多套类似方案,最大的教训是:停车场系统的代码量不大,但每写一条更新语句都要问自己一句“如果这条语句执行到一半程序停了,数据会不会不一致”。把这种习惯带到论文代码审查里,答辩时老师提什么刁钻问题都能接得住。希望帮到你。

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

RELATED READING

延伸阅读

更多一线实战笔记与深度复盘,助您持续精进