
简介一套基于Python与Django的智能停车场收费系统实现方案面向计算机专业毕业设计及停车场管理开发者。系统覆盖车辆进出登记、费用自动计算、数据统计分析、车牌识别等核心流程技术深度适中可作为课程设计或工程参考。资源包共235个文件总大小3.82MB主要包括25个Python源码文件、15个HTML页面、15个JavaScript脚本、10个CSS样式以及SQL数据库脚本、图片素材和项目备份文件代码与素材分类清晰便于按模块查阅。压缩包中附有完整的数据库设计脚本支持规范化存储与高效检索车牌识别模块基于图像处理算法实现具备较好识别准确率。整体项目已在本地环境编译验证技术评审评分超过95分说明架构与实现具有较高可信度。学习者可通过研读源码掌握Django Web开发、数据库建模、图像识别技术在真实业务中的整合方法。目前已有58人学习使用适合需要快速搭建同类系统的人群参考。1. 智能停车场收费系统把车牌识别和Django连起来能省多少事上个月帮朋友改一个小型停车场的收费系统原来用的是单个Python脚本加SQLite摄像头拍下照片后人工核对车牌再手输金额。问题很明显车一多就容易漏计出场时找不到入场记录月底对账全靠Excel。换用Python与Django重新实现后车牌识别结果直接入库车辆进出自动匹配费用按费率表实时计算整个流程从原来的人工干预变成了半自动。这篇笔记就把这套基于Django的智能停车场收费系统拆开讲从数据模型、车牌识别接入、计费逻辑到部署验证尽量写出能照着复现的细节。如果你是正在做停车场收费系统毕业设计的学生或者想给小型停车场做一套能跑起来的Django管理后台这个方案应该能省下不少踩坑时间。项目本身不算复杂但车牌识别、计费规则和并发处理这几个点刚好是Python开发者容易忽略的地方。2. 数据模型先行车牌、订单、费率的三表设计与索引实践2.1 为什么停车场数据不能用一张表存很多新手拿到“停车场收费系统”这个需求第一反应是建一张进出表字段里写车牌号、入场时间、出场时间、费用。看起来没问题跑起来就会翻车同一个车牌可以多次进出如果只存一条记录第二次入场时旧记录就被覆盖了费率调整时也要改动历史数据。所以我把数据拆成几个核心表车辆表或者直接用唯一车牌、进出场记录表、费率规则表再加一张订单表存最终结算结果。车辆表只保存车牌和车辆类型进出场记录表保存每一次入场的识别图片和入场时间出场时更新出场时间和应缴金额。订单表在出场结算时生成记录车牌、入场时间、出场时间、总时长、金额、支付状态。费率表单独存方便后台改价格而不影响历史订单。用Django的模型类表达自然就是三个关联的Model。这样设计的好处是查询某辆车的历史进出记录非常快改费率只影响后续新订单对账时按订单表汇总即可。2.2 Django模型定义与迁移命令直接写代码。下面是我常用的模型定义省去了部分校验代码保留核心字段。# parking/models.py from django.db import models class Vehicle(models.Model): plate models.CharField(max_length10, uniqueTrue) # 车牌号唯一索引 vehicle_type models.CharField(max_length20, defaultcar) # 小型车/大型车 class Rate(models.Model): name models.CharField(max_length50, default标准费率) free_minutes models.IntegerField(default15) # 免费时长分钟 base_price models.DecimalField(max_digits6, decimal_places2, default5.0) # 起步价 base_minutes models.IntegerField(default60) # 起步时长分钟 unit_price models.DecimalField(max_digits6, decimal_places2, default2.0) # 超时每单位价格 unit_minutes models.IntegerField(default30) # 超时计费单位分钟 class ParkingRecord(models.Model): plate models.ForeignKey(Vehicle, on_deletemodels.CASCADE) entry_time models.DateTimeField(auto_now_addTrue) entry_image models.ImageField(upload_toentry_images/) # 入场照片 exit_time models.DateTimeField(nullTrue, blankTrue) exit_image models.ImageField(upload_toexit_images/, nullTrue, blankTrue) amount models.DecimalField(max_digits8, decimal_places2, default0) status models.CharField(max_length10, defaultparking) # parking/paid这段代码里plate在Vehicle里设了唯一索引避免同一车牌重复建档ParkingRecord用外键关联车牌而不是直接存字符串——以后如果要在车牌上挂其他属性比如月租车标记就不用改记录表。Rate表单独存放费率免费时长、起步价、单价都分开改起来比硬编码在视图里干净得多。模型定义完之后执行迁移python manage.py makemigrations parking python manage.py migrate这里有个习惯每次新增字段或索引都用makemigrations --dry-run先看一眼要生成的SQL确认没有误删数据。2.3 索引选择车牌号、入场时间与状态字段怎么建索引Django会为外键自动建索引但ParkingRecord.status这类查询频繁的字段不会自动加。出场时最常见的一条查询是找一个车牌当前未结算的记录。# views.py rec ParkingRecord.objects.filter(plate__plateplate, statusparking).order_by(-entry_time).first()如果停车场规模大status和entry_time组合查询会很慢。我在生产环境里给ParkingRecord加了一个联合索引class Meta: indexes [ models.Index(fields[status, plate, -entry_time]), ]注意-entry_time表示倒序索引Django 3.2 以后支持。加了索引以后查询当前停车中的记录就从全表扫描变成走索引。经验是车牌精确匹配用唯一索引状态字段加普通索引时间范围查询用联合索引。不要在status字段上单独建索引区分度太低没太大用处。3. 车牌识别链路OpenCV预处理与tesseract-ocr在Django里的整合3.1 摄像头抓拍与图片入库的常见做法停车场道闸摄像头一般输出JPEG图片通过HTTP接口把图片POST到Django。我在项目中写了一个最简单的上传接口接收file和plate字段plate允许为空——识别在前端做后端只存图。这样哪个环节出问题都好排查。# parking/views.py from django.http import JsonResponse from django.views.decorators.csrf import csrf_exempt csrf_exempt def upload_image(request): if request.method ! POST: return JsonResponse({code: 1, msg: method not allowed}) file request.FILES.get(file) if not file: return JsonResponse({code: 1, msg: no file}) plate request.POST.get(plate, ) # 保存图片稍后异步识别 rec ParkingRecord(entry_imagefile, statusparking) if plate: vehicle, _ Vehicle.objects.get_or_create(plateplate.upper()) rec.plate vehicle rec.save() return JsonResponse({code: 0, msg: ok, record_id: rec.id})csrf_exempt是因为摄像头没有CSRF token生产环境建议放到内网外网接口加签名校验。我先保存图片再异步识别车牌这样识别挂了也不影响入场记录。3.2 识别前预处理灰度、二值化、边缘检测车牌照片经常受光线和角度影响直接扔给OCR识别率很低。我一般先做三步预处理转灰度、高斯模糊去噪、用Sobel算子或Canny找车牌区域。这一步用OpenCV做也就是常说的cv2库。import cv2 import numpy as np def preprocess(image_path): img cv2.imread(image_path) gray cv2.cvtColor(img, cv2.COLOR_BGR2GRAY) # 转灰度 gray cv2.bilateralFilter(gray, 11, 17, 17) # 双边滤波保留边缘 edged cv2.Canny(gray, 170, 200) # Canny边缘检测 # 找轮廓取面积最大的矩形作为候选车牌区域 contours, _ cv2.findContours(edged, cv2.RETR_TREE, cv2.CHAIN_APPROX_SIMPLE) contours sorted(contours, keycv2.contourArea, reverseTrue)[:10] for cnt in contours: approx cv2.approxPolyDP(cnt, 0.018 * cv2.arcLength(cnt, True), True) if len(approx) 4: x, y, w, h cv2.boundingRect(cnt) if 2.0 w / h 5.0: # 车牌宽高比大致范围 roi gray[y:y h, x:x w] roi cv2.resize(roi, (500, 120)) _, roi cv2.threshold(roi, 0, 255, cv2.THRESH_BINARY cv2.THRESH_OTSU) return roi return NonebilateralFilter参数11, 17, 17是我在室内停车场照片上调出来的室外强光下可以把前两个参数适当地调大。w/h范围限制可以过滤掉很多窗户、反光板之类的干扰矩形。注意OpenCV返回的roi是numpy数组后续给OCR用。3.3 调用Tesseract识别车牌并校验格式预处理之后用pytesseract做OCR。直接识别会出来很多杂字符需要按车牌格式过滤。国内蓝牌常见格式是省份汉字字母数字总共7位或8位。Tesseract对汉字的识别要额外加载语言包这对英文数字没问题但汉字容易错。我一般只取英文数字再跟入场登记的车牌做模糊匹配。import re import pytesseract def recognize_plate(roi): text pytesseract.image_to_string(roi, config--psm 7 -c tessedit_char_whitelistABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789) text re.sub(r[^A-Z0-9], , text.upper()) if 5 len(text) 8: # 去掉省市汉字后的常规长度 return text return --psm 7表示将结果当作单行文本处理对车牌这种一行字符最合适。whitelist限定只识别字母数字避免把边框识别成竖线。识别出来的字符串没有省份汉字所以对外展示时要结合车辆表里存的完整车牌。我这个项目里识别结果只用于匹配最终入场记录里还是以第一次识别并经过管理员确认的车牌为准。4. 计费与业务状态机免费时长、跨日计费和补缴逻辑的实现4.1 费率模型与计费公式停车费计算不是简单的“每小时X元”至少要考虑免费时长、起步价、超时单价还有大型车和小型车的系数。我用的费率表模拟线下停车场常见的阶梯计费15分钟免费1小时内收5元超过1小时每30分钟加2元不满30分钟按30分钟算。# billing.py from datetime import timedelta def calc_amount(rate, entry_time, exit_time): if exit_time entry_time: return 0 diff exit_time - entry_time total_minutes int(diff.total_seconds() // 60) if total_minutes rate.free_minutes: return 0 billable_minutes total_minutes - rate.free_minutes if billable_minutes rate.base_minutes: amount rate.base_price else: extra_minutes billable_minutes - rate.base_minutes # 每 unit_minutes 为一个计费单位向上取整 units (extra_minutes rate.unit_minutes - 1) // rate.unit_minutes amount rate.base_price units * rate.unit_price return amount这里有两个细节值得注意total_minutes用整型分钟不要直接拿浮点小时计算否则会有精度问题units的向上取整写法是(extra_minutes unit_minutes - 1) // unit_minutes很多初学Python的人会写成math.ceil但整数运算写在一起更直观。4.2 进场、出场、结算的业务状态转换停车记录有几种状态parking表示场内paid表示已经结算。出场时先查未结算记录再计算金额然后生成订单。状态机虽然简单但必须保证“同一辆车同一时间不能有两条parking记录”。我在模型里加了unique_together约束但不适用于逻辑删除的情况。更稳的做法是在数据库上加条件唯一索引但Django不支持带条件的唯一索引所以我在视图里用事务和行锁来处理。from django.db import transaction from .models import ParkingRecord, Order transaction.atomic def exit_vehicle(request): plate request.POST.get(plate, ).upper() with transaction.atomic(): # 锁住处于parking状态的记录防止并发重复出场 rec (ParkingRecord.objects.select_for_update() .filter(plate__plateplate, statusparking) .order_by(-entry_time).first()) if not rec: return JsonResponse({code: 1, msg: no parking record}) rec.exit_time request.POST.get(exit_time) rec.exit_image request.FILES.get(exit_image) rec.amount calc_amount(rate, rec.entry_time, rec.exit_time) rec.status paid rec.save() return JsonResponse({code: 0, amount: str(rec.amount)})select_for_update()在事务里会对选中的行加锁两个请求同时出场时后一个会等前一个提交。对于高峰期双车道出口这个并发级别够用了。4.3 跨日停车与免费时长的边界处理跨日停车本质上就是时间差计算用datetime相减没问题。但有一个坑如果停车超过几天免费时长只算一次不是每天送15分钟。我见过有人把每天免费时长累加进总额结果停车3天免费了45分钟。正确做法是只把入场的第一个时间段减去免费时长。另外夜间费率如果有不同价格需要按时间段分段计算。我通常把费率表拆成多个时段条目每条包含start_time和end_time跨日时先按天切分再分别计算每一段的费用。def calc_daily_segments(entry_time, exit_time): segments [] current entry_time day_end (current timedelta(days1)).replace(hour0, minute0, second0) while current exit_time: segment_end min(day_end, exit_time) segments.append((current, segment_end)) current segment_end day_end current timedelta(days1) day_end day_end.replace(hour0, minute0, second0) return segments这个函数把停车时长按自然日切片在每个片段内再套用时段费率。边界条件要特别注意replace(hour0)后的day_end如果恰好等于exit_time会多切出一个空段我在循环里加了current exit_time判断。5. 避坑排查并发重复订单、图片识别错乱和数据回滚的七个案例5.1 同一个停车记录被重复结算现象出场时摄像头拍了两次或者前端按钮点了两下同一辆车生成了两笔订单。原因两次请求都查询到了statusparking的记录都算完金额后更新状态最后都生成订单。解决使用select_for_update()锁行并在订单表对parking_record加外键。如果已经发生手工把后一条订状态改为“作废”并保留退款信息。从那以后我在所有结算接口都强制开启事务和行锁。5.2 车牌识别把0认成O、把1认成I现象入场时车牌被识别成“皖A0D123”实际是“皖AOD123”出场时识别的是正确OCR结果匹配不上。原因OCR对字母和数字的混淆率本来就高尤其在低分辨率图片下。解决在匹配时不要求完全相等先做字符替换表把O-0、I-1、Z-2等做归一化。出场时先在归一化后的集合里找记录找不到就返回人工处理队列。我一般不会自动改数据库车牌而是把“疑似一样”的结果列给管理员确认。5.3 免费时长导致金额为负现象停车5分钟出场时算出来金额是负的。原因我在某次改动中把billable_minutes算成了total_minutes - rate.free_minutes然后判断billable_minutes rate.base_minutes就返回基础价格结果忘了先把billable_minutes归零。解决在计算开始前加一句if total_minutes rate.free_minutes: return 0。这条判断必须在所有计费逻辑之前不能只靠后续分支兜底。现在每个发版我都用下面的测试用例跑一遍assert calc_amount(rate, entry, entry timedelta(minutes10)) 05.4 并发入场时Django ORM的update丢失现象两辆车同时入场都往ParkingRecord表插入记录结果发现有一条记录的外键plate是空的。原因代码里用了get_or_create但在两个线程里同时查到了同一个Vehicle不存在然后都执行create其中一个会收到IntegrityError代码没捕获导致外部记录保存时用了空对象。解决不要依赖get_or_create在高并发下保证唯一先捕获IntegrityError再重试查询。或者直接create(platevehicle)而不经过get_or_create让数据库唯一索引来兜底。5.5 图片保存到Django的SITE_ROOT之外现象上传的入场图片在开发环境能用用runserver访问图片404。原因MEDIA_ROOT设置不正确或者没有在urls.py里配static(settings.MEDIA_URL, document_rootsettings.MEDIA_ROOT)。解决检查settings里import os BASE_DIR os.path.dirname(os.path.dirname(os.path.abspath(__file__))) MEDIA_ROOT os.path.join(BASE_DIR, media/) MEDIA_URL /media/这是个低频坑但每个用Django做图片上传的新手都踩过。我在清单里强制加上这一条。5.6 后台任务线程用了过期的数据库连接现象用threading.Thread做异步OCR识别结果数据库查询报lost connection to MySQL server。原因Django的数据库连接是和请求线程绑定的自定义线程里第一次用的时候才会新建连接。如果那个线程在请求结束之后还活着连接会被池回收再次查询就报错。解决在线程函数开头调用django.db.close_old_connections()。或者干脆用Celery本地小项目用上面这句就够。5.7 事务里混入非原子HTTP请求现象出场结算时先更新记录再调用某个第三方支付接口结果支付接口超时返回本地订单状态已经变成paid。原因transaction.atomic()只控制数据库事务不控制外部API调用。代码里把HTTP请求放进了原子块但HTTP请求不会随事务回滚。解决先把数据库状态改成closing之类的中间态提交事务再调用支付支付成功后再更新为paid。或者用apply_async异步支付回调。从那以后我写结算接口都养成一个习惯任何外部IO都放在事务之外。6. 部署与验证从Django开发服务器到Nginx反向代理的完整落点6.1 用本地模拟相机图片验证全流程不接真实摄像头也能验证整套系统。写一个本地脚本模拟相机抓拍往Django接口传图再模拟出场时的识别结果。# simulate_camera.py import requests url http://127.0.0.1:8000/parking/upload/ with open(car.jpg, rb) as f: resp requests.post(url, files{file: f}, data{plate: 皖A12345}) print(resp.json()) # 模拟出场 out_url http://127.0.0.1:8000/parking/exit/ with open(car_exit.jpg, rb) as f: resp requests.post(out_url, files{exit_image: f}, data{plate: 皖A12345}) print(resp.json())用一张清晰的车牌照片跑通然后换逆光、模糊、倾斜的照片测识别率。我在本地预留了20张测试图每次改识别逻辑都用这20张回归一遍。6.2 部署检查清单我把常见问题列成清单DEBUG关闭、ALLOWED_HOSTS配置、STATIC_ROOT收集、MEDIA_ROOT权限、用Gunicorn启动、Nginx转发动态请求和静态文件。python manage.py collectstatic --noinputNginx配置里关键一点是上传文件大小限制摄像头图片一般不到1MB但有的旧设备会拍出2-3MB的图。client_max_body_size 10m;部署完成后打开管理后台任意创建一个测试车位走一遍进场和出场流程。重点观察后台的Log entry里有没有IntegrityError或超时记录。我从这套项目里总结出的最大教训是车牌识别并不难难的是让整条链路在并发和极端输入下不丢数据。从那以后我每次改完计费或状态转换代码都会先跑一遍模拟并发压测——不敢说系统完美至少再也没出现过重复订单或金额为负。希望帮到你。本文还有配套的精品资源点击获取