简介:一套基于Python与Django框架的智能停车场收费管理系统,面向计算机相关专业毕业设计及Web系统开发者,提供集成车牌识别与数据库管理的完整实现方案。方案覆盖车辆进出记录、费用计算、数据统计分析和自动化车牌识别等核心业务流程,既能支撑实际场景,也可作为教学案例学习。压缩包共235个文件,容量约3.82MB,包括Python源码、前端页面、SQL数据库脚本以及图片素材等,各类型文件按模块组织,便于快速定位与复用;当前已有58人浏览学习。项目代码经过本地编译验证,在技术评审中获得超过95分的成绩,并受到教学助理严格审核,确认符合学术与实践双重标准。车牌识别功能基于图像处理算法实现,识别准确率较高,数据库采用规范化设计,保证存储与检索效率;整套系统注释详尽、文档结构清晰,适合毕业设计直接参考,也可作为停车场管理系统二次开发的基础范例。
1. 智能停车场收费系统该用什么技术栈:为什么选 Python + Django 做后端
车开到闸机口,摄像头抓拍,两秒内识别车牌、抬杆、开始计费;离场时同样识别车牌、自动算费、生成账单。这套流程最终以数据为中心运转,但真正的工程量不止是让模型认出车牌,而是把识别结果落进数据库之后,后端如何保证每笔停车费不多收、不少收、不重收。正因为如此,Python 与 Django 在收费系统落地里被大量使用:识别侧有 OpenCV/ONNX 和 HyperLPR 等开源方案支撑,业务侧 Django 自带 ORM、迁移工具和 Admin 后台,一个小团队两周内就能把计费闭环跑通。本文面向要把它部署到现场、或者正在做 Django 项目实战的工程师,按车牌识别接入、数据库建模、结算实现、避坑和验证五个阶段展开。
2. 车牌识别与 Django 服务如何协作:接口方案、线程模型和最小架构
2.1 识别进程独立于 Web 服务:本地 HTTP 服务的必要性
主流的车牌识别实现(比如 HyperLPR、基于 YOLO 的定制模型)和 Django 业务服务耦合在同一个进程里,是从根上埋雷。识别一张图要 100 到 300 毫秒,受 CPU/GPU 算力影响;Django 同步视图在这个时间段内会一直占用工作线程。本地开发感觉不到,生产环境里 10 个并发入场请求就能把 gunicorn 的默认 worker 全部拖住,后面的请求开始排队。
常见做法是把识别引擎包装成一个独立 HTTP 服务,只监听 127.0.0.1 的某个端口,Django 通过 requests 调用它获取车牌结果。这样两个好处:识别服务可以单独重新部署,升级模型权重不影响收费接口;Django 端可以把识别调用放进异步任务,Web 工作线程不会被慢 I/O 卡死。我通常用 FastAPI 做这层壳,请求模型简单,性能也够。
# recognition_server.py from fastapi import FastAPI, UploadFile import numpy as np import cv2 app = FastAPI() engine = load_plate_engine() # 识别引擎只初始化一次 @app.post("/plate") async def plate(file: UploadFile): data = await file.read() img = cv2.imdecode(np.frombuffer(data, np.uint8), cv2.IMREAD_COLOR) result = engine(img) plates = [{"plate_no": r["code"], "conf": float(r["confidence"])} for r in result if r.get("code")] return {"plates": plates}逻辑说明:load_plate_engine()在进程启动时加载模型权重,不要在请求里反复创建。cv2.imdecode把上传的图片字节流解码成 numpy 数组,识别引擎返回车牌字符串和置信度。真实项目中,模型文件约几十到几百 MB,每次重新加载耗时数十秒,复用是必须的。
参数说明:UploadFile是 FastAPI 的上传类型,Django 端发送 multipart/form-data 即可。返回的confidence在后续过滤中使用;如果一帧画面里有多辆车识别出多个车牌,Django 端按置信度排序取最高者,而不是直接取第一个,否则出口容易把旁边车道经过的车算进来。
2.2 接入方式:Django 主动查询还是回调推送,按车道并发选型
对外暴露的调用关系有两种可选:同步查询和异步回调。同步查询是 Django 收到摄像头触发请求后,立即调用识别服务拿结果再落库,实现最简单,但识别耗时会被下游感知;异步回调是识别服务自己部署摄像头侧,分析完成后通过 POST 回调 Django 的接口,Django 负责落库。车道数量多时,回调方式不容易阻塞 Web 服务,但需要操心回调地址暴露、请求校验和顺序问题。
对于 10 条车道以下的车场,我通常选同步查询加超时兜底。回调方案在多个摄像头同时触发时,到达 Django 的顺序和实际车辆进场顺序不一定一致,后续出场计费会依赖一个不可靠的顺序,这是给自己挖坑。同步查询里 Django 段代码并不复杂:
# parking/views.py import requests from django.conf import settings def recognize_plate(image_bytes): resp = requests.post( settings.RECOGNITION_URL, files={"file": ("plate.jpg", image_bytes, "image/jpeg")}, timeout=settings.RECOGNITION_TIMEOUT, ) resp.raise_for_status() plates = resp.json().get("plates", []) plates.sort(key=lambda x: x["conf"], reverse=True) return plates[0]["plate_no"] if plates else None逻辑说明:settings.RECOGNITION_URL必须做成配置项。实际部署时,Django 所在机器和识别服务经常不在一台服务器上,测试环境用127.0.0.1:8001,生产环境改成内网地址,不要硬编码在视图里。
参数说明:timeout同时控制连接和读取超时,识别服务变慢时,这个参数决定你是否放弃请求。我一般设 3 到 5 秒;超过 5 秒意味着识别服务已经高负载,此时放行并且标记PENDING让后台补录,比让车主堵在闸机口强。
2.3 入场上报的最小 Django 接口:请求返回、落库与幂等
调用识别之后,Django 要写一条入场记录。入场记录要面对重复请求:网线闪断重发、摄像头重复抓拍、客户端重试,同一个车牌可能被 POST 很多次。如果每次都新建一条入场记录,出场时就会匹配出多个status="IN"的记录,收费逻辑直接乱了。
用get_or_create做成幂等接口:
# parking/views.py from django.views.decorators.http import require_POST from django.http import JsonResponse from django.utils import timezone from parking.models import ParkingRecord @require_POST def entry_api(request): plate_no = recognize_plate(request.FILES["image"].read()) if not plate_no: return JsonResponse({"ok": True, "plate_no": None, "level": "warning"}, status=201) obj, created = ParkingRecord.objects.get_or_create( plate_no=plate_no, status="IN", entry_source=request.POST.get("source", "camera"), defaults={ "entry_image": request.FILES["image"], "entry_time": timezone.now(), } ) if not created: # 重复入场请求,返回已有记录,前端不需要再抬杆 pass return JsonResponse({"ok": True, "plate_no": obj.plate_no, "record_id": obj.id})逻辑说明:get_or_create的查询条件只放在前三个参数;如果把entry_time放进查询条件,每次都因为时间不同而创建新记录,等于幂等失效,这是最常见的重复入场事故。
参数说明:entry_source用来区分车道来源。多进多出的车场,同一辆车可能在东门和西门各请求一次;只按车牌判断就会把误入其他入口的车当成新入场。字段建议写成"A_ENTRY"、"B_EXIT"这类出入口标识,出场时能跟踪车辆动线。
3. 数据库管理的核心:车辆、计费规则与停车记录的三表设计
3.1 业务字段梳理:车牌号、入场时间、计费规则 ID 与状态位
停车场计费不是简单收银,需要把车辆、入场记录和计费规则分开建模。我把核心拆成三张表:车辆表、停车记录表和费率规则表。车辆表保存车牌和月租状态;停车记录表是一进一出对应一条账单;费率规则表存不同区域的收费标准。
| 表名 | 核心字段 | 说明 |
|---|---|---|
| Vehicle | plate_no, is_monthly, expire_time | 车牌是逻辑主键,月租车和临时车区分 |
| ParkingRecord | plate_no, entry_time, exit_time, fee_rule, status | 一进一出一条记录 |
| FeeRule | name, free_minutes, unit_price, unit_step, cap_amount | 价格策略,避免写死在代码里 |
为什么要独立一张 FeeRule?因为同一个车场不同区域收费不一样,地面和地下、普通车位和充电车位价格不同,或者营销活动定义了新价格。费率做成数据库行,运营人员在 Django admin 里改价格,应用不用发版。这是把“数据库管理”的价值体现出来的关键设计。
这里有个新手常犯错误:把费用计算字段直接加到停车记录表里,每次出场时用 Python 代码临时算一遍。规则调整后,历史账单无法追溯,财务对账非常痛苦。正确做法是费用计算放在一个 service 层,它依赖记录字段和费率表,输出结算结果,并且把使用过的费率快照存下来。
3.2 Django 模型定义与字段边界:时间字段用 naive 还是 aware
把模型用 Django 代码写出来,最有争议的总是时间字段。Django 默认USE_TZ=True时,DateTimeField 存 UTC,取出来会转成本地时间;如果代码里混用datetime.now()和timezone.now(),数据库时区转换会出现八小时偏差。
# parking/models.py from django.db import models from django.utils import timezone class FeeRule(models.Model): name = models.CharField(max_length=64) free_minutes = models.PositiveIntegerField(default=0, help_text="免费分钟数") unit_price = models.DecimalField(max_digits=6, decimal_places=2, help_text="单位价格,元") unit_step = models.PositiveIntegerField(default=60, help_text="计费步长,分钟") cap_amount = models.DecimalField(max_digits=6, decimal_places=2, null=True, blank=True, help_text="单次封顶,元") is_active = models.BooleanField(default=True) class ParkingRecord(models.Model): plate_no = models.CharField(max_length=20, db_index=True) entry_time = models.DateTimeField(default=timezone.now) exit_time = models.DateTimeField(null=True, blank=True) fee_rule = models.ForeignKey(FeeRule, null=True, on_delete=models.PROTECT) status = models.CharField( max_length=10, choices=[("IN", "在场"), ("OUT", "已出场"), ("PENDING", "待人工")], default="IN") entry_source = models.CharField(max_length=32, default="") entry_image = models.ImageField(upload_to="entries/") total_amount = models.DecimalField(max_digits=7, decimal_places=2, null=True, blank=True)逻辑说明:on_delete=models.PROTECT比CASCADE安全。费率规则一旦被停车记录引用,不允许直接删除,否则历史账单的金额来源变成悬空引用。Django 在删规则前会抛ProtectedError,提示先处理关联记录。
参数说明:max_length=20不是给当前车牌留的,大陆车牌最长 8 位,但新能源车牌、挂车车牌格式更多,预留扩展。金额字段必须用DecimalField,不要用FloatField,浮点数在 Python 里存在精度误差,做月度财务核对时会产生分以下误差。
3.3 查询与删除对象:ORM 的常见误区和索引设计
Django 中查询和删除对象是高频操作,中文检索里有一大批开发者搜“django 执行查询-删除对象”。实际项目里,痛点集中在按时间倒序取最近记录、批量删除历史数据。
# 反例:order_by("-entry_time") 没走索引时全表扫描 # records = ParkingRecord.objects.order_by("-entry_time")[:20] # 正例:在 Meta 中声明组合索引 class ParkingRecord(models.Model): # 字段省略 class Meta: indexes = [ models.Index(fields=["-entry_time", "status"]), ]逻辑说明:组合索引把排序字段和过滤字段放在同一个索引里。status="IN" AND order_by -entry_time的查询可以直接用这个索引完成排序,避免 filesort 和回表。数据量到十万条时,有没有这个索引,查询耗时会从 200 毫秒降到 5 毫秒。
删除对象时,queryset.delete()是一句批量 SQL;Python 循环里一个个删会变成 N 条 SQL,性能差异非常明显。
# 清理 90 天前已出场的历史记录 old_count = ParkingRecord.objects.filter( entry_time__lt=timezone.now() - timezone.timedelta(days=90), status="OUT", ).delete() print(f"删除 {old_count[0]} 条")逻辑说明:delete()返回的是一个二元组,第一个元素是总共删除的行数。生产环境做这类清理,最好先count()看一下规模,再事务里执行,并且跑之前导出备份,这是给误操作留的后悔药。
参数说明:queryset.delete()级联删除关联的外键对象,但采用的是SET NULL还是CASCADE,取决于外键定义。清理历史记录时,如果entry_image挂在 ImageField,数据库删除了行,文件系统里的图片文件还需要另行清理,这部分不能靠 ORM 解决。
4. 收费计算落地:从入场到出场的结算流程和并发一致性
4.1 计费规则引擎:免费时长、时段价、封顶价的实现
计费规则最典型的组合是:入场后 30 分钟免费,超出后按小时计费,每 60 分钟 5 元,单日封顶 20 元。还有一些场库按半小时计费、分时段价格。实现上,我习惯把计费函数写成纯函数,不碰数据库,方便单元测试反复灌数据。
# parking/fee_calculator.py from decimal import Decimal, ROUND_DOWN import math def calc_fee(rule, entry_time, exit_time): total_minutes = max(0, int((exit_time - entry_time).total_seconds() / 60)) if total_minutes <= rule.free_minutes: return Decimal("0.00") billable = total_minutes - rule.free_minutes units = math.ceil(billable / rule.unit_step) amount = rule.unit_price * units if rule.cap_amount is not None: amount = min(amount, rule.cap_amount) return amount.quantize(Decimal("0.01"), rounding=ROUND_DOWN)逻辑说明:先扣免费时长,剩余分钟用math.ceil向上取整到计费步长,最后做封顶。ROUND_DOWN表示 5.5999 元落成 5.59 元,这个舍入规则要和支付端保持一致。
参数说明:unit_step默认 60 表示按小时;按半小时计费时改成 30,unit_price对应半小时单价。free_minutes统一用分钟,不要用小时和分钟混着传。这个函数没有任何 I/O,后面接 Django 视图、Celery 任务或命令行脚本都一样。
4.2 出场结算的 Django 事务:select_for_update 防止重复结算
出场结算最隐蔽的问题,是两个出口的摄像头同时请求同一个车牌。例如一辆车从 A 口出场,识别服务返回车牌,同时 B 口也拍到了这辆车,两个请求先后打到 Django。如果不加锁,两条请求会同时读到status="IN"的记录,各自计算费用,返回给前后两个闸机,后台记录也变成两条出场记录。
解决办法是用事务配合select_for_update():
# parking/views.py from django.db import transaction from parking.models import ParkingRecord from parking.fee_calculator import calc_fee @transaction.atomic def exit_api(plate_no): try: rec = ParkingRecord.objects.select_for_update().get( plate_no=plate_no, status="IN") except ParkingRecord.DoesNotExist: return {"ok": False, "reason": "no_entry"} rec.exit_time = timezone.now() rec.total_amount = calc_fee(rec.fee_rule, rec.entry_time, rec.exit_time) rec.status = "OUT" rec.save() return {"ok": True, "amount": rec.total_amount}逻辑说明:select_for_update()生成SELECT ... FOR UPDATE,在事务提交前锁住这一行。两个并发请求里,后一个必须等前一个提交后才会读取,此时status已经变成OUT,get()抛出DoesNotExist,接口明确返回no_entry拒绝重复结算。
参数说明:@transaction.atomic必须与select_for_update()配对。没有事务时,锁在查询结束后立刻释放,相当于没锁。另一个容易踩的坑是:select_for_update()不能用在含prefetch_related的查询上,锁只对主表生效,关联表是另一套处理方式。
4.3 跨天跨月边界:时区设置与 23:59 出场的金额校验
跨天计费出错大多源自时区设置。Django 默认USE_TZ=True,数据库里存 UTC,取出转本地时区。如果 MySQL 版本旧且时区表缺失,连接时可能报Unknown or incorrect time zone。稳妥做法是在settings.DATABASES里指定init_command:
DATABASES = { "default": { "ENGINE": "django.db.backends.mysql", "NAME": "parking", "USER": "parking", "PASSWORD": os.environ["DB_PASSWORD"], "HOST": "127.0.0.1", "OPTIONS": {"init_command": "SET time_zone = '+08:00'"}, "CONN_MAX_AGE": 3600, "CONN_HEALTH_CHECKS": True, } }逻辑说明:init_command在每次新连接建立时执行,强制使用东八区。CONN_MAX_AGE=3600让连接复用,降低握手开销;配合CONN_HEALTH_CHECKS,Django 会先探测连接有效性再查询。
场景验证:入场时间 11 月 30 日 08:00,出场时间 12 月 1 日 01:30,免费时长 30 分钟,小时单价 5 元,封顶 20 元。测试代码覆盖这组跨天数据:
from django.test import TestCase from django.utils import timezone from parking.models import FeeRule from parking.fee_calculator import calc_fee from datetime import datetime class CrossDayTestCase(TestCase): def test_cross_day_billing(self): rule = FeeRule(free_minutes=30, unit_step=60, unit_price=5, cap_amount=20) entry = timezone.make_aware(datetime(2024, 11, 30, 8, 0)) exit_ = timezone.make_aware(datetime(2024, 12, 1, 1, 30)) # 总时长 17.5 小时,扣除 0.5 小时免费,应计 17 小时 amount = calc_fee(rule, entry, exit_) self.assertEqual(amount, Decimal("20.00"))逻辑说明:测试用例没有经过数据库,完整绕过了时区问题,因此它验证的是计算逻辑本身。真正容易踩坑的是代码里用datetime.now()而不是timezone.now(),Linux 服务器时区为 UTC 时,本地时间会差 8 小时,账单就按错误的时长计算了。
参数说明:如果cap_amount=None,要单独测超过 24 小时、无封顶的用例,确认它按小时累计且不会溢出。测试前先执行date +"%Z %z"检查服务器时区,这是免费的知识点。
5. 系统避坑:车牌识别误判、环境冲突与数据库连接的 5 个血泪经验
5.1 车牌省份简称识别错误:数据集与后处理的关系
现象:训练和单元测试都正常,现场一上线,“粤A12345”经常被识别成“湘A12345”。蓝底白字的“粤”和“湘”骨架相似,低分辨率或强逆光时,模型在最后的字符分类上给出相近概率。
原因:车牌汉字是闭集分类,识别模型训练数据里省份样本分布不均匀。广东样本多,模型偏向输出常见类别;某些省份样本少,输出概率低且容易混淆。
解决:不要一上来就重训练模型,先加后处理和业务兜底。识别服务里维护一个《省份简称白名单》,对置信度低于 0.8 的汉字单独走一个字符分类器再判一次。Django 端维护“场内车牌集合”,如果最终识别车牌不在场内,但恰好有“省份简称不同、其余字符相同”的车牌在场内,就按场内车牌结算,并在后台标记疑似误识别。这个兜底能把误识别导致的计费投诉减少大半。
5.2 Django 视图同步调用识别服务导致请求超时
现象:高峰期入场请求频繁 502,Django 日志显示 gunicorn worker 排队到 30 秒以上,识别服务 CPU 占用接近 100%。
原因:摄像头触发接口同步等待识别结果。识别服务是单进程,请求排队后,一张图最坏处理两秒,Web 工作线程被全部占住,新请求进不来。
解决:gunicorn 加 worker 和线程数只能缓解,治标不治本:
gunicorn parking.wsgi:application --workers 4 --threads 2 --timeout 30 --bind 0.0.0.0:8000参数说明:--workers 4启动 4 个进程,--threads 2每个进程开 2 个线程,吞吐量提升到 8 个并发线程;--timeout 30超过 30 秒的请求会被强制终止。要彻底解决,还得在识别服务前做队列或限制并发,否则高峰期进来的图片流量会把识别服务压垮,Django 只是从“卡死”变成“排队等卡死”。
5.3 NumPy/OpenCV 版本冲突与 Python 环境变量配置问题
现象:用同一份 requirements.txt,Windows 本地运行正常,Linux 服务器上 import cv2 报错ImportError: numpy.core.multiarray failed to import。新人在配置环境时经常卡在这一步。
原因:OpenCV 轮子依赖特定 numpy 的 ABI。本地 Python 3.9 配 numpy 1.x,服务器 Python 3.11,pip 自动装最新 numpy 2.x,旧版 OpenCV 和新版 numpy 二进制不兼容。
解决:锁版本,不要写numpy>=1.24这种宽范围。我验证过的组合是:
pip install numpy==1.24.4 opencv-python==4.8.1.78 python -c "import numpy, cv2; print(numpy.__version__, cv2.__version__)"这一步要检查两点:python命令指向哪个解释器,安装的命令是否真的装进当前解释器。很多翻车现场是命令行里python指向系统 3.6,pip3却指向 3.11,包装进了错误的 site-packages;VSCode 里选择了 Python 解释器,但终端里python是另一套路径。执行which python确认路径,再用虚拟环境隔离。
5.4 数据库连接泄漏:Django 连接复用与 MySQL 超时设置
现象:系统跑了两天后,日志随机出现OperationalError: 2006, MySQL server has gone away,部分页面开始 500。
原因:Django 默认每个线程维护持久连接。MySQL 的wait_timeout默认 8 小时,空闲连接被服务端关掉,Django 不知道连接已失效,下一次查询执行就会报错。
解决:两端配合。MySQL 侧把wait_timeout调大,Django 侧设置CONN_MAX_AGE和CONN_HEALTH_CHECKS,我在第 4.3 节的数据库配置里给出了这两项。Django 4.1 之后,CONN_HEALTH_CHECKS会在执行请求前验证连接,失效就重建,基本不需要自己写中间件了。注意一点:CONN_MAX_AGE不要设为 0,否则失去连接复用意义。
5.5 免费时段跨天的计费误差:时间的舍入规则
现象:车辆 23:45 入场,次日 00:25 出场,规则写明“免费 30 分钟”,账单却显示收费 5 元,车主投诉。
原因:总停车时长 40 分钟,扣除 30 分钟免费,剩余 10 分钟,按“不足一小时按一小时”计费,收 5 元是预期行为;但有些实现把“不足一小时不计费”当规则,剩余 10 分钟被当成 0,漏收。这两种解读在业务侧需要事先定清楚。
解决:把规则显式表达为“入场后 N 分钟内出场免费,超出后按整计费单位向上取整”。示例里 40 分钟总时长、扣 30 分钟免费、剩余 10 分钟按一小时收费,是正确结果。如果担心客诉,可以加一个grace_minutes宽容参数,比如在免费时段边界外再给 5 分钟缓冲,但要在费率规则表里显式记录,不能靠代码临时打折。
6. 进阶技巧:用 Mock 车牌识别把计费逻辑在本地完整验证
6.1 用 unittest.mock 替换识别服务,让测试不依赖硬件
接入真实摄像头和识别服务之前,可以用patch替换recognize_plate函数,固定返回一个车牌,把入场、出场、计费整条链路在本地跑通。
# parking/tests/test_flow.py from unittest.mock import patch from django.test import TestCase from django.urls import reverse from parking.models import ParkingRecord class EntryExitFlowTest(TestCase): @patch("parking.views.recognize_plate", return_value="粤A12345") def test_entry_then_exit(self, mock_recognize): # 模拟入口摄像头请求 entry_resp = self.client.post(reverse("entry_api"), { "image": SIMPLE_JPG_BYTES, "source": "A_ENTRY", }) self.assertEqual(entry_resp.status_code, 200) rec = ParkingRecord.objects.get(plate_no="粤A12345", status="IN") self.assertEqual(rec.entry_source, "A_ENTRY") exit_resp = self.client.post(reverse("exit_api"), {"plate_no": "粤A12345"}) self.assertEqual(exit_resp.status_code, 200) self.assertGreater(Decimal(exit_resp.json()["amount"]), 0)逻辑说明:patch把视图里的识别函数替换成固定返回值,测试完全绕开识别服务。任何安装了 Django 的机器都能跑这套测试,CI 里不需要摄像头、不需要 GPU、不需要识别服务进程。
参数说明:SIMPLE_JPG_BYTES是一张最小尺寸的 JPEG 纯色图,约 1 KB,放在测试目录下读取为字节串。图片内容不需要有车牌,因为识别函数被替换了,但请求里必须包含有效图片,否则代码走到request.FILES["image"]时会 KeyError。
6.2 用并发脚本验证开锁逻辑,避免只靠单元测试
单元测试默认在事务里执行,同一测试内模拟并发并不真实。要验证select_for_update()是否真的挡住重复出场,我习惯本地把 gunicorn 跑起来,然后并发请求出口接口:
from concurrent.futures import ThreadPoolExecutor def fire_exit(plate): try: return requests.post("http://127.0.0.1:8000/api/exit/", data={"plate_no": plate}, timeout=5).status_code except Exception: return 500 with ThreadPoolExecutor(max_workers=50) as pool: results = list(pool.map(fire_exit, ["粤A00001"] * 50)) print(results.count(200), results.count(500))我一般会把 50 个请求全部指向同一个车牌,然后登录数据库看这条记录,status必须只剩OUT,并且只生成一笔账单。如果看到 200 状态码数量大于 1,说明锁没生效,回到代码里检查事务装饰器和外键查询方式。
这套流程做完,入场、计费、结算、异常兜底四个环节都有验证手段,再往后就是接摄像头设备侧的联调,后端基本不用再动。我最开始做这类项目时,直接在视图里同步调识别服务,上线第一个晚高峰就翻车了。把识别服务拆开、把费率做成数据库配置、把计费函数写成纯函数,这些改动看起来简单,却把后端从“不稳定能用”变成了“敢接财务对账”。希望这个方案能帮你少走一段弯路。
本文还有配套的精品资源,点击获取