基于Django的智能停车场收费系统:车牌识别与计费逻辑实现

简介:这是一套面向计算机相关专业毕业设计场景的停车场收费管理系统完整实现方案,采用Python与Django框架开发,并集成车牌识别模块,适合需要完成毕设课题或学习Web系统开发的学习者参考。资源包共235个文件,约3.82MB,以25个py源码文件、15个html模板、15个js脚本与10个css样式文件为主体,辅以sql数据库脚本、md说明文档及图片素材,覆盖后端逻辑、前端页面与数据存储各层。已有58人学习下载。项目围绕车辆进出记录、费用计算、数据统计与自动化车牌识别等核心业务展开,数据库采用规范化设计,识别模块基于成熟图像处理算法实现,代码注释详尽、目录结构清晰,便于理解系统架构与实现细节。学习者可借此掌握Django Web开发、数据库设计与集成、图像识别技术应用等实践技能,并作为同类管理系统开发的参考范例。

1. 从一张 Excel 收费表说起:这套 Django 停车场系统到底能跑多远

如果你正在做毕业设计,选题是停车场收费管理,大概率经历过这样的场景:用 Excel 记车牌、算时长、手填金额,答辩前老师问一句「进出场时间怎么保证不重复计费」,你就开始冒汗。这套基于 Python 与 Django 的智能停车场收费系统,核心就是把这个手工流程换成一套能跑起来的 Web 应用——车牌识别负责把车「认出来」,Django 负责把进出场记录、费率规则、收费流水管起来,数据库负责让这些数据不丢、可查、可对账。

它适合三类人:一是毕业设计需要完整可演示系统的同学,二是想拿一个真实业务练 Django 增删改查和 ORM 的初学者,三是需要给小型车场做一套轻量管理后台的开发者。整套东西不依赖重型框架,Python 装好、Django 跑起来、数据库连上,就能在本地把「车进场—识别—计费—出场—放行」这条链路走通。下面我按实际拆项目的顺序,把选型理由、建表逻辑、识别接入和踩坑点一层层摊开。

2. 环境与数据模型:先把 Django 项目和停车业务表搭对

2.1 为什么用 Django 而不是 Flask 做这套系统

停车场收费系统的本质是「一堆有状态的数据 + 一套规则计算 + 一个能看的后台」。Django 自带 ORM、Admin 后台、迁移工具和表单校验,这四样东西恰好覆盖了停车业务里最费时间的部分。用 Flask 你得自己拼 SQLAlchemy、自己写后台页面、自己做表单验证,毕业设计周期里这些重复劳动会吃掉大半时间。

Django 的 Admin 还有一个隐性好处:答辩现场老师让你「查一下今天所有未缴费的车」,你直接进 Admin 按条件筛就行,不用临时改代码。常见做法是把车辆、车位、进出场记录、费率规则拆成独立模型,用外键关联,这样计费逻辑只依赖记录表,不会和车辆信息耦合死。

选型上还有一个现实考量:Django 的models.py写清楚字段类型和约束后,makemigrations和migrate两条命令就能把表建好,数据库管理这块基本不用手写 DDL。对新手来说,这比直接怼 MySQL 建表语句稳得多。

2.2 建项目与建 App 的完整命令

先确认 Python 版本,建议 3.8 及以上,Django 用 4.x 稳定版。下面这套命令我一般会完整走一遍,注意虚拟环境一定要建,不然后面装依赖会污染系统 Python。

# 创建项目目录并进入 mkdir parking_system && cd parking_system # 创建虚拟环境(Windows 用 python -m venv venv) python3 -m venv venv source venv/bin/activate # 安装 Django 和数据库驱动 pip install django mysqlclient pillow # 创建 Django 项目,注意结尾的点,避免多一层目录 django-admin startproject parking . # 创建业务 App,命名 parking_app python manage.py startapp parking_app

startproject结尾那个点很关键,不加的话会在当前目录下再套一层parking/,后面路径全要改。mysqlclient是连 MySQL 用的,如果你先用 SQLite 练手,可以暂时不装,Django 默认就是 SQLite,零配置能跑。

2.3 停车业务四张核心表怎么设计

数据模型是这套系统的地基,设计错了后面计费全是坑。我一般拆成车辆表、车位表、进出场记录表、费率规则表四张。下面这段models.py可以直接抄,字段注释写清楚了每个字段的业务含义。

from django.db import models class Vehicle(models.Model): """车辆信息表:一个车牌对应一条记录""" plate_number = models.CharField(max_length=10, unique=True, verbose_name="车牌号") owner_name = models.CharField(max_length=20, blank=True, verbose_name="车主姓名") created_at = models.DateTimeField(auto_now_add=True) def __str__(self): return self.plate_number class ParkingSpace(models.Model): """车位表:记录车位编号和占用状态""" space_no = models.CharField(max_length=10, unique=True, verbose_name="车位编号") is_occupied = models.BooleanField(default=False, verbose_name="是否占用") class RateRule(models.Model): """费率规则表:按小时计费,支持免费时长""" name = models.CharField(max_length=30, verbose_name="规则名称") free_minutes = models.IntegerField(default=15, verbose_name="免费时长(分钟)") price_per_hour = models.DecimalField(max_digits=6, decimal_places=2, verbose_name="每小时单价") daily_cap = models.DecimalField(max_digits=8, decimal_places=2, default=50, verbose_name="每日封顶") class EntryRecord(models.Model): """进出场记录表:计费的核心依据""" vehicle = models.ForeignKey(Vehicle, on_delete=models.CASCADE, verbose_name="车辆") space = models.ForeignKey(ParkingSpace, null=True, blank=True, on_delete=models.SET_NULL) entry_time = models.DateTimeField(verbose_name="进场时间") exit_time = models.DateTimeField(null=True, blank=True, verbose_name="出场时间") fee = models.DecimalField(max_digits=8, decimal_places=2, default=0, verbose_name="应收费用") is_paid = models.BooleanField(default=False, verbose_name="是否已缴费")

plate_number加了unique=True,防止同一车牌重复建档;EntryRecord里exit_time允许为空,因为车还在场内时没有出场时间,计费逻辑要判断这个字段是否为 None。on_delete的选择也有讲究:车辆删了记录跟着删用CASCADE,车位删了记录保留但置空用SET_NULL,这样历史流水不会因为删个车位就消失。

建完模型执行迁移:

python manage.py makemigrations parking_app python manage.py migrate python manage.py createsuperuser

makemigrations生成迁移文件,migrate真正建表,createsuperuser建后台账号。三步顺序不能乱,跳过makemigrations直接migrate会发现表根本没建。

3. 车牌识别接入:从图片到车牌号的那一段怎么落地

3.1 识别方案选型:本地库还是调接口

车牌识别这块,毕业设计里常见两条路:一是用本地 OCR 库,比如 HyperLPR、EasyOCR 或 PaddleOCR 的车牌模型;二是调第三方识别接口。本地库的好处是不依赖网络、答辩现场不怕断网,缺点是环境配置容易翻车,尤其是 CPU 版推理库的依赖冲突。第三方接口识别率高,但要联网、要申请 key,答辩时网络一抖就尴尬。

我的建议是本地库为主,选 HyperLPR3 或 PaddleOCR,前者专门做中文车牌,开箱即用程度高。如果你只是要演示流程,甚至可以先写一个「模拟识别」函数,把预置车牌号返回,把计费链路先跑通,再替换成真实识别。这样开发节奏不会被环境问题卡死。

3.2 识别模块与 Django 的对接方式

识别代码不要直接塞进视图函数里,单独建一个recognition.py,对外只暴露一个recognize_plate(image_path)函数。这样以后换识别库,只改这一个文件,视图层不动。下面是一个基于 HyperLPR3 的封装示例:

# parking_app/recognition.py import hyperlpr3 as lpr3 # 单例加载模型,避免每次调用都重新初始化 _catcher = lpr3.LicensePlateCatcher(detect_level=lpr3.DETECT_LEVEL_LOW) def recognize_plate(image_path): """ 输入图片路径,返回识别到的车牌号字符串 识别失败返回 None """ try: results = _catcher(image_path) if results and len(results) > 0: # results 结构为 [(车牌号, 置信度, 类型, 框), ...] plate, confidence, _, _ = results[0] if confidence > 0.7: # 置信度阈值,低于此值视为不可信 return plate return None except Exception as e: print(f"识别异常: {e}") return None

detect_level设成 LOW 是为了在 CPU 上跑得快,答辩演示够用;confidence > 0.7这个阈值是血泪经验,设太低会把模糊图片识别成乱七八糟的字符,设太高又频繁识别失败,0.7 是个平衡点。模型用单例加载,因为初始化模型很耗时,放在函数里每次调用都加载会让接口慢到无法忍受。

3.3 视图层:识别、建档、开闸一条龙

视图函数负责把识别结果落到数据库,并返回是否放行。下面这个vehicle_entry视图处理进场流程:

# parking_app/views.py from django.http import JsonResponse from django.utils import timezone from .models import Vehicle, EntryRecord, ParkingSpace from .recognition import recognize_plate def vehicle_entry(request): """车辆进场:识别车牌 -> 建档 -> 分配车位 -> 生成进场记录""" if request.method != "POST": return JsonResponse({"code": 405, "msg": "仅支持 POST"}) image_path = request.POST.get("image_path") plate = recognize_plate(image_path) if not plate: return JsonResponse({"code": 400, "msg": "车牌识别失败,请重试"}) # 车牌不存在则自动建档 vehicle, created = Vehicle.objects.get_or_create(plate_number=plate) # 检查是否已有未出场的记录,防止重复进场 if EntryRecord.objects.filter(vehicle=vehicle, exit_time__isnull=True).exists(): return JsonResponse({"code": 409, "msg": "该车已在场内"}) # 找一个空闲车位并占用 space = ParkingSpace.objects.filter(is_occupied=False).first() if space: space.is_occupied = True space.save() EntryRecord.objects.create( vehicle=vehicle, space=space, entry_time=timezone.now() ) return JsonResponse({"code": 200, "msg": "进场成功", "plate": plate})

get_or_create一行搞定「有则取、无则建」,省掉先查再建的两次数据库操作。exit_time__isnull=True这个查询是防重复进场的关键,没有它同一辆车可以刷无数次进场记录,计费直接乱套。车位用filter(is_occupied=False).first()拿第一个空闲的,简单但够用,真实场景可以按区域或就近分配。

3.4 出场计费:时长、免费时段、封顶怎么算

出场视图是整个系统逻辑最密的地方,计费规则要处理免费时长、按小时向上取整、每日封顶三个条件。下面这段代码把计费逻辑单独抽成函数,方便单元测试:

# parking_app/views.py 续 import math from decimal import Decimal def calculate_fee(entry_time, exit_time, rule): """根据进出场时间和费率规则计算费用""" duration = exit_time - entry_time minutes = duration.total_seconds() / 60 # 未超过免费时长,不收费 if minutes <= rule.free_minutes: return Decimal("0.00") # 扣除免费时长后按小时向上取整 billable_minutes = minutes - rule.free_minutes hours = math.ceil(billable_minutes / 60) fee = hours * rule.price_per_hour # 应用每日封顶 if fee > rule.daily_cap: fee = rule.daily_cap return fee def vehicle_exit(request): """车辆出场:计算费用 -> 释放车位 -> 更新记录""" plate = request.POST.get("plate") vehicle = Vehicle.objects.filter(plate_number=plate).first() if not vehicle: return JsonResponse({"code": 404, "msg": "无此车辆记录"}) record = EntryRecord.objects.filter(vehicle=vehicle, exit_time__isnull=True).first() if not record: return JsonResponse({"code": 404, "msg": "无在场记录"}) rule = RateRule.objects.first() # 简化处理,取第一条规则 now = timezone.now() fee = calculate_fee(record.entry_time, now, rule) record.exit_time = now record.fee = fee record.save() # 释放车位 if record.space: record.space.is_occupied = False record.space.save() return JsonResponse({"code": 200, "msg": "出场成功", "fee": str(fee)})

math.ceil是向上取整,停 61 分钟按 2 小时算,这是停车场行业惯例,用round会少收钱。Decimal类型不能和 float 直接混算,所以单价和封顶都用Decimal,返回时转成字符串避免 JSON 序列化报错。费率规则这里简化成取第一条,真实项目应该按车型或时段匹配,但毕业设计演示够用。

4. 避坑与排查:这套系统最容易翻车的五个地方

4.1 车牌识别库装不上或一跑就崩

现象:pip install hyperlpr3卡在编译,或者 import 时报缺少libGL.so之类的动态库。原因:OCR 类库依赖 OpenCV 和底层图像库,不同系统缺的包不一样,Windows 和 Linux 报错还不同。解决:Linux 下先apt install libgl1 libglib2.0-0,Windows 下装opencv-python而不是opencv-contrib-python,两者混装会冲突。实在搞不定就退回模拟识别函数,先把业务链路跑通。

4.2 时区导致计费差好几个小时

现象:明明停了 1 小时,算出来 9 小时,费用离谱。原因:Django 默认USE_TZ=True用 UTC 存时间,timezone.now()返回的是 UTC 时间,如果你在模板或计算里又按本地时间理解,就会差 8 小时。解决:在settings.py里设TIME_ZONE = 'Asia/Shanghai'和USE_TZ = True,所有时间统一用timezone.now(),不要用datetime.now(),两者混用必出时区 bug。

4.3 重复进场导致计费翻倍

现象:同一辆车短时间内生成多条未出场记录,出场时只结算了一条,其他记录永远挂着。原因:进场视图没做「是否已在场内」校验,或者校验条件写错。解决:进场前必须查EntryRecord.objects.filter(vehicle=vehicle, exit_time__isnull=True).exists(),为真直接拒绝。这个校验要放在创建记录之前,顺序错了等于没校验。

4.4 数据库迁移报「表已存在」或字段对不上

现象:改了模型字段后migrate报错,或者新字段在数据库里查不到。原因:手动改过数据库表结构,或者迁移文件没提交、多台机器迁移状态不一致。解决:开发阶段实在乱了就删掉migrations目录下除__init__.py外的文件,删库重建,重新makemigrations和migrate。生产环境不能这么干,要用--fake谨慎处理,但毕业设计阶段重建最省事。

4.5 Admin 后台看不到新模型

现象:模型建好了,migrate也跑了,但登录 Admin 找不到对应的表。原因:忘了在admin.py里注册模型。解决:在parking_app/admin.py里加admin.site.register(Vehicle)等注册语句,或者用@admin.register装饰器。这个坑新手几乎必踩,因为 Django 不会自动把模型暴露到后台。

5. 进阶技巧:用 Admin 自定义和单元测试把系统验一遍

5.1 让 Admin 后台直接显示计费关键字段

默认 Admin 只显示__str__返回的内容,查记录很不方便。我一般会自定义list_display和筛选器,让后台一眼能看出哪些车没缴费、哪些还在场内:

# parking_app/admin.py from django.contrib import admin from .models import Vehicle, EntryRecord, ParkingSpace, RateRule @admin.register(EntryRecord) class EntryRecordAdmin(admin.ModelAdmin): list_display = ("vehicle", "entry_time", "exit_time", "fee", "is_paid") list_filter = ("is_paid", "exit_time") search_fields = ("vehicle__plate_number",) date_hierarchy = "entry_time"

list_display决定列表页显示哪些列,list_filter右侧生成筛选面板,search_fields里的双下划线是跨外键查询,vehicle__plate_number表示按关联车辆的车牌搜。date_hierarchy顶部生成日期钻取条,查某天的流水特别快。这几行加上去,答辩演示时老师随便点几下就能看到数据关系,比翻代码有说服力。

5.2 用单元测试验证计费逻辑

计费函数是纯逻辑,最适合写测试。下面这个测试覆盖免费、正常计费、封顶三种情况:

# parking_app/tests.py from django.test import TestCase from datetime import timedelta from django.utils import timezone from decimal import Decimal from .models import RateRule from .views import calculate_fee class FeeTest(TestCase): def setUp(self): self.rule = RateRule.objects.create( name="标准", free_minutes=15, price_per_hour=Decimal("5.00"), daily_cap=Decimal("50.00") ) def test_free_within_15min(self): entry = timezone.now() exit_ = entry + timedelta(minutes=10) self.assertEqual(calculate_fee(entry, exit_, self.rule), Decimal("0.00")) def test_one_hour_charge(self): entry = timezone.now() exit_ = entry + timedelta(minutes=70) # 扣15分钟免费,剩55分钟按1小时 self.assertEqual(calculate_fee(entry, exit_, self.rule), Decimal("5.00")) def test_daily_cap(self): entry = timezone.now() exit_ = entry + timedelta(hours=20) # 远超封顶 self.assertEqual(calculate_fee(entry, exit_, self.rule), Decimal("50.00"))

跑python manage.py test就能验证。测试用例的价值在于:你改计费逻辑时,跑一遍就知道有没有把原来的规则改坏。我见过太多人改了一行取整方式,结果免费时长判断跟着失效,有测试就不会出这种事。

5.3 一个我踩过的坑和现在的习惯

早期做这套系统时,我把计费逻辑直接写在视图函数里,没有抽成独立函数,结果想写测试时发现根本没法单独调用,只能起服务发请求测,效率极低。后来养成习惯:凡是纯计算逻辑,一律抽成不依赖 request 的函数,视图只负责取参数、调函数、存结果。从那以后我每次写计费、排序、格式转换这类逻辑,都强制先问自己「这个能不能单独测」,能就抽出来。这套停车场系统里,calculate_fee和recognize_plate都是这么拆出来的,改起来心里有底。希望这套拆解能帮到你,把毕业设计稳稳跑通。

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