ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

Django实战:构建智能停车场收费系统,搞定车牌识别与计费状态机

Django实战:构建智能停车场收费系统,搞定车牌识别与计费状态机 简介基于Python与Django的智能停车场收费系统实现方案定位为计算机专业毕业设计、课程设计及停车场管理系统开发者的可直接参考的完整样板工程。资源包共235个文件压缩包大小3.82MB主体包括25个Python源码文件、15个HTML页面、15个JS脚本、10个CSS样式表、1个SQL数据库结构文件另含大量运行截图和数据库备份便于直观了解系统运行状态并及时恢复数据。整套方案覆盖车辆进出记录、停车费用计算、车牌自动识别、数据统计等核心业务流程车牌识别模块基于图像处理算法实现具备良好识别准确率数据库采用规范化设计确保数据存取高效性。项目代码注释详尽目录结构清晰前端基于Bootstrap等成熟组件构建界面风格统一可直接用于部署演示或作为二次开发基础。已有58人学习下载资源经本地编译验证技术评审超过95分适合需要快速搭建停车收费管理原型的研究者参考。1. 智能停车场收费系统为什么说瓶颈在“计费状态机”而不是车牌识别车牌识别准确率做到 99% 并不难难的是把识别结果变成一笔不会出错的收费记录。停车场收费系统的真正复杂度集中在两处一是车牌识别结果如何快速且稳定地进入数据库二是临时车、月租车、免费车这些计费状态如何在并发下正确流转。基于 Python 与 Django 实现这套系统意味着你选择了一条开发效率高、二次开发友好的路线但同时也必须正面处理 Django 默认的同步阻塞模型与硬件设备回调之间的节奏冲突。我接下来说的方案适合具备 Python 基础、想在 Django 项目实战中把车牌识别和数据库管理串起来的人。整套系统用一个 HTTP 服务接收识别设备推送的车牌事件用 Django ORM 完成车辆档案、入场记录、收费结算的闭环最后通过管理后台和 API 把数据消费掉。你可以用模拟数据先跑通再挂接真实设备也可以直接把识别模块替换成你自己的推理服务。2. 选型与结构Django 做业务中枢识别设备只负责“报车牌”2.1 为什么用 Django 而不是 Flask 或 FastAPI这是一个典型的“管理后台重、对外接口轻”的系统。停车收费需要大量的数据维护操作——车辆档案、月租套餐、费率表、优惠券、异常流水审核Django 自带 Admin 和 ORM可以直接把后台管理成本压到最低。如果选 Flask这些都要从零拼选 FastAPI异步性能好看但 Admin 生态远不如 Django 成熟。Django 的 ORM 在这个场景下的价值容易被低估。停车场收费涉及多张表的关联查询比如根据车牌找车辆档案、再关联套餐和剩余次数用 ORM 的select_related和prefetch_related能把查询次数从 N1 降下来。同时迁移机制让数据库结构变更可以走版本控制。项目实战时最怕的是改一张表导致线上数据不同步Django 的 migration 至少给了后悔药。2.2 识别设备接入的两种常见姿态市面上的车牌识别一体机绝大多数都支持 HTTP 推送或 SDK 回调。HTTP 推送是更通用的方案设备识别到车牌后POST 一条 JSON 到你的 Django 接口包含车牌号、识别时间、设备编号和一张抓拍图 URL。SDK 回调往往需要额外的转发服务不建议直接嵌进 Django 进程。我一般会这样设计接入层# parking/views.py import json import logging from django.views.decorators.csrf import csrf_exempt from django.http import JsonResponse from django.utils import timezone from .models import PassRecord, Device logger logging.getLogger(__name__) csrf_exempt def device_callback(request, device_code): if request.method ! POST: return JsonResponse({code: 1, msg: method not allowed}) try: payload json.loads(request.body) except json.JSONDecodeError: return JsonResponse({code: 1, msg: bad json}) # 设备侧一般会签名字段这里只做基础校验 plate_no payload.get(plate_no, ).strip() event_type payload.get(event_type, entry) # entry / exit recognize_time payload.get(recognize_time) image_url payload.get(image_url, ) if not plate_no: return JsonResponse({code: 1, msg: plate_no required}) try: device Device.objects.get(codedevice_code, is_activeTrue) except Device.DoesNotExist: logger.warning(funknown device: {device_code}) return JsonResponse({code: 1, msg: unknown device}) # 通过事务创建通行记录 from django.db import transaction with transaction.atomic(): record PassRecord.objects.create( devicedevice, plate_noplate_no, event_typeevent_type, recognize_timerecognize_time or timezone.now(), image_urlimage_url, ) return JsonResponse({code: 0, msg: ok, data: {record_id: record.id}})这段代码有两点值得注意。第一csrf_exempt是必须的设备回调不会带 Django 的 CSRF token但你要在设备侧配一个签名参数做身份校验不能裸奔。第二transaction.atomic()不是摆设因为等一下创建通行记录的同时很可能还要联动更新车位余量或车辆在场状态这两个操作必须同生共死。2.3 数据库表结构与 Django 模型设计停车场收费系统的核心表逃不出这几张设备表、车辆档案表、通行记录表、收费规则表、订单表。设备表记录每个出入口的设备编码和方向车辆档案表区分临时车、月租车、内部车通行记录表是流水账每次识别都写一条收费规则表存费率订单表在车辆出场结算时生成。# parking/models.py from django.db import models class Device(models.Model): code models.CharField(max_length50, uniqueTrue) name models.CharField(max_length100) direction models.CharField(max_length10, choices[(entry, 入口), (exit, 出口)]) is_active models.BooleanField(defaultTrue) class VehicleProfile(models.Model): plate_no models.CharField(max_length15, uniqueTrue, db_indexTrue) vehicle_type models.CharField( max_length10, choices[(temp, 临时车), (monthly, 月租车), (free, 免费车)], defaulttemp ) monthly_expire_at models.DateTimeField(nullTrue, blankTrue) class PassRecord(models.Model): device models.ForeignKey(Device, on_deletemodels.PROTECT) plate_no models.CharField(max_length15, db_indexTrue) event_type models.CharField(max_length10, choices[(entry, 入场), (exit, 出场)]) recognize_time models.DateTimeField(db_indexTrue) image_url models.URLField(blankTrue) class RateRule(models.Model): name models.CharField(max_length50) free_minutes models.IntegerField(default15) first_hour_fee models.DecimalField(max_digits6, decimal_places2) per_hour_fee models.DecimalField(max_digits6, decimal_places2) daily_cap models.DecimalField(max_digits6, decimal_places2, nullTrue, blankTrue) class ParkingOrder(models.Model): record models.ForeignKey(PassRecord, on_deletemodels.PROTECT) plate_no models.CharField(max_length15, db_indexTrue) entry_time models.DateTimeField() exit_time models.DateTimeField() duration_minutes models.IntegerField() total_fee models.DecimalField(max_digits8, decimal_places2) status models.CharField(max_length10, choices[(unpaid, 未支付), (paid, 已支付)], defaultunpaid)这里关键的决定是把PassRecord做成只插入不更新的流水表在场状态另外通过“最近一条入场记录没有对应出场记录”来推导或者用独立的在场表维护。不要试图在流水表上做繁重的更新操作后续并发纠正状态时会非常被动。VehicleProfile需要加db_index因为所有查询都是按车牌号走的没有任何场景需要扫全表。3. 把计费逻辑在 Django 里落地从入场到结算的完整闭环3.1 入场流程临时车、月租车如何分流入场事件进来后不是所有车都直接放行。月租车要判断有效期临时车要判断黑名单免费车要判断白名单。这一步如果放在设备回调里做同步判断很容易因为数据库查询慢导致设备端等待超时所以正确姿势是“先落库、再异步判断”。在 Django 里最简单的方式是直接同步处理因为回调接口通常只做判断不放行真正的道闸控制由设备侧自己决定。# parking/services.py from django.utils import timezone from .models import VehicleProfile, PassRecord def handle_entry(record: PassRecord): profile VehicleProfile.objects.filter(plate_norecord.plate_no).first() if not profile: # 未建档车辆直接按临时车处理 return {action: open, vehicle_type: temp} if profile.vehicle_type monthly: if profile.monthly_expire_at and profile.monthly_expire_at timezone.now(): return {action: open, vehicle_type: monthly} else: # 月租到期降级为临时车 return {action: open_with_warning, vehicle_type: temp, msg: monthly expired} if profile.vehicle_type free: return {action: open, vehicle_type: free} return {action: open, vehicle_type: temp}这里的业务规则是“月租到期不拦截但提醒收费”。因为在实际停车场里月租过期几天的车很常见硬拦会导致出入口拥堵不如放行后由出口按临时车计费。这个决策逻辑应该在 service 层而不是堆在视图函数里。视图只负责解析请求和处理异常业务规则集中在 service 层后面改规则时不用动接口代码。3.2 出场结算时长计算与分段计费出场计算的难点不在算钱而在“如何定义入场时间”。入场时间应该取最近一条没有对应出场的入场记录而不是设备推送的当前时间往前猜。如果在入场记录缺失的情况下出场需要走人工纠正流程而不是用默认时间瞎算。# parking/billing.py from datetime import timedelta from decimal import Decimal from .models import RateRule, PassRecord, ParkingOrder def calc_fee(entry_time, exit_time, rule: RateRule): duration exit_time - entry_time total_minutes int(duration.total_seconds() // 60) if total_minutes rule.free_minutes: return Decimal(0.00), total_minutes # 超过免费时长的部分按“首小时 后续每小时”计费 billable_minutes total_minutes - rule.free_minutes hours (billable_minutes 59) // 60 # 向上取整 if hours 1: fee rule.first_hour_fee else: fee rule.first_hour_fee (hours - 1) * rule.per_hour_fee if rule.daily_cap and fee rule.daily_cap: fee rule.daily_cap return fee, total_minutes def settle_exit(record: PassRecord): entry_record ( PassRecord.objects .filter(plate_norecord.plate_no, event_typeentry) .exclude(idrecord.id) .order_by(-recognize_time) .first() ) if not entry_record: raise ValueError(找不到对应入场记录需要人工处理) rule RateRule.objects.filter(namedefault).first() fee, minutes calc_fee(entry_record.recognize_time, record.recognize_time, rule) order ParkingOrder.objects.create( recordrecord, plate_norecord.plate_no, entry_timeentry_record.recognize_time, exit_timerecord.recognize_time, duration_minutesminutes, total_feefee, ) return order(billable_minutes 59) // 60这个写法是向上取整的经典技巧。停车计费超过一分钟就要按一小时算不能四舍五入。daily_cap是封顶费比如白天 8 小时封顶 30 元这里一天只按自然日算如果你的需求要跨天累计封顶还需要把分段逻辑改得更复杂。settle_exit在找不到入场记录时直接抛异常在接口层要捕获并通知值班人员。3.3 Django Admin 如何接管配置与查询Admin 是整个系统交付时最容易出彩的部分。把 RateRule、VehicleProfile、PassRecord 都注册进去运营人员不需要写 SQL 就能完成日常维护。关键是要把列表页的字段和过滤器配置好让查询效率有保障。# parking/admin.py from django.contrib import admin from .models import Device, VehicleProfile, PassRecord, RateRule, ParkingOrder admin.register(PassRecord) class PassRecordAdmin(admin.ModelAdmin): list_display (plate_no, event_type, device, recognize_time) list_filter (event_type, device__direction, recognize_time) search_fields (plate_no,) date_hierarchy recognize_time raw_id_fields (device,) admin.register(VehicleProfile) class VehicleProfileAdmin(admin.ModelAdmin): list_display (plate_no, vehicle_type, monthly_expire_at) list_filter (vehicle_type,) search_fields (plate_no,)date_hierarchy会在列表页顶部生成一个按日期钻取的导航对查询出场记录非常实用。raw_id_fields会把外键下拉框换成输入框因为设备表可能只有几十条但用户量大了之后外键下拉框会卡死浏览器。这些细节决定了后台在真实运营中好不好用。实时推送不在 Django 默认能力范围内可以后续引入 Django Channels 做 WebSocket。常见的做法是识别设备回调时把一条消息塞进 Redis 队列然后 Channels 层从 Redis 订阅并推送给前端大屏。Django 和 WebSocket 的整合坑很多尤其是和 Django 的会话机制、认证机制混在一起时建议只在独立端口跑 WebSocket不要和 WSGI 进程混布。4. 并发、状态一致性与数据修正这个方案最大的翻车点4.1 重复回调导致重复入场记录真实设备经常因为网络闪断把同一张车牌推送两次如果不去重就会产生两条入场记录出场结算时取到的是较早那条费用就会多算。解决思路不能依赖设备端保证服务端必须做幂等。# parking/views.py from django.db import transaction, IntegrityError csrf_exempt def device_callback(request, device_code): # ... 前面的解析代码不变 ... # 用设备号 设备本地事件序号做唯一键 event_id payload.get(event_id, ) if event_id: with transaction.atomic(): record, created PassRecord.objects.get_or_create( devicedevice, device_event_idevent_id, defaults{ plate_no: plate_no, event_type: event_type, recognize_time: recognize_time or timezone.now(), image_url: image_url, } ) if not created: return JsonResponse({code: 0, msg: duplicated})device_event_id是设备每次推送时自增的序号这个字段在模型中要加unique_together约束。比用时间戳去重靠谱得多因为设备时钟经常不准。处理重复回调时第二次请求直接返回成功但不创建记录让设备端不再重试。4.2 车牌识别错误的人工纠正流程识别错误是必然事件。相似字符“0”和“O”、“1”和“I”在低分辨率抓拍下经常混淆。一旦入场时识别错了出场时又识别对了系统会看成两辆不同的车导致找不到入场记录。我见过最有效的处理方式是在管理后台提供一个“合并记录”的操作。值班人员搜到两条记录后把错误的车牌号改成正确的然后重算订单。在 Django Admin 中写一个 action 就行# parking/admin.py from django.contrib import admin, messages def merge_plate_records(modeladmin, request, queryset): if queryset.count() ! 2: messages.error(request, 只能选择两条记录进行合并) return # 取第一条记录的入场时间第二条的出场时间 # 这里需要更严谨的判断逻辑此处只做示意 messages.success(request, 合并完成请检查订单)人工操作一定要留下操作日志。谁改的、什么时候改的、原车牌是什么、新车牌是什么这些都要有记录。停车场收费的客诉往往集中在“你多收我钱了”没有日志就意味着查无实据只能赔钱安抚。4.3 收费计算与并发扣费同一个车牌在极短时间内连续出场两次的概率极低但不代表没有。比如出口闸机抬杆后又落杆司机倒车再冲一次设备又推送了一条出场记录。这会导致同一辆车生成两张订单。处理方式是在创建订单时锁住入场记录或者在车辆档案上加一个“在场”标记。# parking/billing.py def settle_exit(record: PassRecord): entry_record ( PassRecord.objects .filter(plate_norecord.plate_no, event_typeentry) .exclude(idrecord.id) .order_by(-recognize_time) .first() ) if not entry_record: raise ValueError(找不到对应入场记录需要人工处理) # 使用 select_for_update 锁住入场记录 # 依赖数据库事务防止并发创建订单select_for_update是 PostgreSQL 和 MySQL 都支持的行级锁。只有在一个事务里执行时才有意义Django 的 ORM 调用它时必须用transaction.atomic()包起来否则锁会在查询结束后立刻释放。锁住入场记录后第二个请求再进来时要么等锁要么直接超时配合唯一约束可以兜底。行级锁是并发问题的兜底手段。如果入口和出口的并发量很高依赖锁会让接口响应变慢更优雅的方案是用 Redis 分布式锁但引入 Redis 又增加了运维成本。对于日均千辆级别的停车场数据库行锁完全够用不要为了架构好看而过度设计。5. 从开发到上线的 6 个坑基于 Django 实现停车场收费的实测避坑记录5.1 时区问题导致计费时长错乱现象车辆停了 1 小时 5 分钟系统显示停了 2 小时 5 分钟。原因设备推送的recognize_time是带时区偏移的字符串Django 的USE_TZTrue时直接存DateTimeField会被当成 UTC 时间。入场记录被加上了 8 小时偏差出场记录正常时长就多出 8 小时。解决在解析设备推送时把recognize_time字符串先转成 aware datetime。如果设备给的是2025-01-01 12:00:00这种无时区格式默认按本地时区处理from django.utils.timezone import make_aware import pytz naive_time datetime.strptime(payload[recognize_time], %Y-%m-%d %H:%M:%S) aware_time make_aware(naive_time, timezonepytz.timezone(Asia/Shanghai))5.2 数据库字符集导致车牌乱码现象新能源车牌中的“D”“F”正常但汉字“京”“沪”存入数据库变成问号。原因MySQL 表级字符集是 latin1或者 Django 连接串里没有指定 utf8mb4。解决创建数据库时指定字符集连接串用 MySQL 时注意Django 4.0 及以上版本不再自动把utf8mb4的varchar转成对应字段类型CREATE DATABASE parking_db CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;5.3 入口摄像头识别到“无牌车”导致死循环现象无牌车在入口处触发识别设备推送plate_no为空字符串系统拒收后设备反复推送。原因设备会根据服务端返回失败进行重试重试间隔很短。解决宁可创建一条plate_no无牌_20250101120000的记录也不要返回失败。等到人工发卡或扫码入场后再更新这条记录的车牌号。这比在设备端配置“无牌车不上传”要可靠因为设备端经常没这个选项。5.4 管理后台查询变慢现象运营人员筛选一个月内的入场记录页面转圈超过 10 秒。原因PassRecord表有几百万条流水PlateNo上的索引虽然能加速单点查询但list_filter的时间范围筛选走了full scan。解决给时间和车牌建联合索引同时限制 Admin 的默认查询范围class PassRecordAdmin(admin.ModelAdmin): def get_queryset(self, request): qs super().get_queryset(request) # 默认只看最近 90 天 threshold timezone.now() - timedelta(days90) return qs.filter(recognize_time__gtethreshold)5.5 设备时间和服务端时间不一致现象明明车在 12:00 入库入场记录显示 11:50导致出场时时长被多算 10 分钟。原因设备端 NTP 同步失败系统断电重启后时钟回退。解决信任服务端时间不信任设备时间。设备推送的时间只作为参考字段保存业务计算一律使用服务端接收到请求的时间。在复杂场景中入口和出口的时钟都在服务端统一比校对设备省心得多。5.6 免密支付回调重复通知现象对接停车场缴费平台时支付回调重复推送订单被重复标记为已支付。原因支付平台的“最终一致性”设计回调本身就是不保证一次性的。解决支付回调接口做幂等用transaction_id做唯一约束重复回调直接返回成功不再更新订单class PaymentCallback(models.Model): transaction_id models.CharField(max_length64, uniqueTrue) order models.ForeignKey(ParkingOrder, on_deletemodels.PROTECT) amount models.DecimalField(max_digits8, decimal_places2) paid_at models.DateTimeField()6. 进阶玩法从离线车牌识别到半离线计费如果前期的机场、园区类项目不需要实时上报识别记录或者设备部署点位和服务器之间网络质量不稳定可以考虑“设备本地识别 半离线计费”的方案。识别设备的 SDK 通常支持本地保存通行记录网络恢复后再批量补传。Django 这边的对接逻辑要稍微改一下设备回调接口收到的是批量数据每一条带一个本地事件序号服务端按这个序号做增量同步。判断哪些是增量数据最稳妥的做法是在设备端存last_uploaded_event_id每次补传时带上这个游标。服务端接收后先校验游标之前的事件是否已经存在再插入新数据。这种方式比在服务端按created_at过滤可靠得多因为批量补传中可能有设备本地手动调整过的数据。我会在服务端增加一个“设备心跳”接口让设备每隔 10 秒上报一次自身状态包括缓存队列长度和最近识别时间。一旦发现某台设备的队列长度持续增长基本可以判断是网络链路故障。硬件设备也是项目的一部分纯软件层面不监控设备在线状态等到出口排长队的时候运维压力会非常大。做这个项目给我最大的教训是车牌识别只是一瞬间的事数据管理才是贯穿始终的命题。不要把精力都花在调模型准确率上先想清楚异常数据怎么进、怎么改、怎么出。数据链路稳了哪怕识别率只有 95%人工纠正也能扛住数据链路乱了识别率 99% 也会被无牌车和错误车牌冲垮。希望这套 Django 实现方案能帮你少走一段弯路。以上。本文还有配套的精品资源点击获取
返回列表