尧图网络科技YAOTU DIGITAL 获取报价
获取报价
首页 / 资讯中心 / 文章详情

Django开发智能停车场收费系统:数据建模、车牌识别与事务处理实战

发布时间:2026/9/29 19:25:28

资讯中心
01
ARTICLE

Django开发智能停车场收费系统:数据建模、车牌识别与事务处理实战

Django开发智能停车场收费系统:数据建模、车牌识别与事务处理实战
简介一套基于Python与Django的智能停车场收费系统实现方案集成车牌识别与数据库管理以zip压缩包发布适合计算机相关专业毕业设计选题也可供停车场管理系统开发者研究参考。系统完成车辆进出记录、费用计算、数据统计分析和自动化车牌识别等核心业务流程技术难度适中代码已通过编译验证并获得超过95分的评审成绩。文件共235个压缩包大小3.82MB内容以python源码、前端html/js/css页面、数据库脚本与备份文件为主另有大量jpg系统截图与界面预览图以及少量字体样式资源整体目录结构清晰便于检索。源码模块划分完整代码注释较详细数据库设计规范车牌识别基于成熟图像处理算法实现。已有58人学习或浏览可作为课程设计或毕业设计的完整参考范例通过学习本项目可掌握Django Web开发、规范化数据库设计与图像识别应用等实践技能。资源源于网络分享仅供学习交流使用请勿商用。1. 智能停车场收费系统的核心不只是识别而是识别之后的账怎么算做停车场收费系统最容易犯的错是把精力全砸在“车牌识别准不准”上。识别率当然重要但真正决定这套系统能不能长期跑下去的是识别结果落库之后的链路是否可靠。基于Python与Django的智能停车场收费系统方案核心就是让车牌识别与数据库管理形成一条闭环识别设备推送结果Django负责清洗、校验、计费、落账每一笔出入场都对应一条可追溯的数据库记录。如果读完只想记住一句话先设计好数据表再谈识别算法后面能省掉一半返工。这篇笔记面向正在用Django做管理系统的开发者也适合刚起步做Django项目新手、打算接识别设备做实际场地的从业者按搭建相似系统的常规顺序把从数据模型到设备联调的完整路径讲清楚。2. Django数据模型设计先把这几张表想清楚后面少改一半代码做这类系统最忌讳一上来就写识别回调接口。等接口写完开始算钱才发现车型、车位、收费规则没有一个合理的落点只能推倒重来。我一般会先花半天时间用Django创建app并把这五张核心表定下来停车场表、车辆表、停车记录表、收费规则表、支付流水表。2.1 停车场、车辆与停车记录三张表先撑起主体停车场和车辆是基础数据停车记录是业务主体。按 Django 项目实战的常见写法先把这三张表建好from django.db import models from django.utils import timezone class ParkingLot(models.Model): 停车场单停车场起步字段设计时先考虑多停车场扩展 name models.CharField(max_length64, verbose_name停车场名称) total_spaces models.PositiveIntegerField(default100, verbose_name总车位数) address models.CharField(max_length128, blankTrue, verbose_name地址) created_at models.DateTimeField(auto_now_addTrue) class Meta: db_table parking_lot verbose_name 停车场 class Vehicle(models.Model): 车辆档案车牌号唯一黑白名单后续可加字段 plate_number models.CharField(max_length20, uniqueTrue, db_indexTrue, verbose_name车牌号) plate_color models.CharField(max_length8, blankTrue, verbose_name车牌颜色) owner_phone models.CharField(max_length16, blankTrue, verbose_name车主电话) created_at models.DateTimeField(auto_now_addTrue) class Meta: db_table vehicle verbose_name 车辆档案 class ParkingRecord(models.Model): 停车记录入场生成一条出场结算后更新同一行 STATUS_PARKING parking STATUS_FINISHED finished STATUS_EXCEPTION exception STATUS_CHOICES [ (STATUS_PARKING, 在场), (STATUS_FINISHED, 已离场), (STATUS_EXCEPTION, 异常), ] lot models.ForeignKey(ParkingLot, on_deletemodels.PROTECT, verbose_name所属停车场) vehicle models.ForeignKey(Vehicle, on_deletemodels.PROTECT, verbose_name车辆) entry_image models.CharField(max_length256, blankTrue, verbose_name入场抓拍图片路径) entry_time models.DateTimeField(defaulttimezone.now, verbose_name入场时间) exit_time models.DateTimeField(nullTrue, blankTrue, verbose_name出场时间) status models.CharField(max_length16, choicesSTATUS_CHOICES, defaultSTATUS_PARKING, db_indexTrue, verbose_name状态) fee models.DecimalField(max_digits8, decimal_places2, default0, verbose_name应收金额) class Meta: db_table parking_record indexes [ models.Index(fields[status, entry_time], nameidx_status_entry_time), models.Index(fields[vehicle, status], nameidx_vehicle_status), ]这段模型有几个关键选择值得说明。on_deletemodels.PROTECT的意思是如果停车记录还没删除关联的车辆和停车场不允许被删除这对财务类数据非常重要——误删一条车辆记录可能导致一批历史流水同时被级联清掉。图片为什么用CharField存路径而不是ImageField是因为抓拍图是摄像头直接上传到服务器或对象存储Django的ImageField会自动处理上传和校验但在高并发写场景下会拖慢事务而且设备端通常已经把图传到指定目录业务端只需要记录路径。索引方面status entry_time是出场查询最常用的组合条件比如“查今天所有在场车辆”或“查昨晚出场但没支付的记录”vehicle status用来快速判断某辆车当前是否在场这也是防重复入场的核心查询。如果你在小车场几千条数据里感觉不到索引差异等数据到几十万条时这两个索引就是能不能秒开页面和会不会锁表卡死的区别。榜额另外说一句fee字段坚持用DecimalField而不是FloatField。停车费涉及金钱计算浮点数的二进制表示会让 0.10.2 不等于 0.3线上算错几毛钱很难发现但财务对不上账的时候会非常头疼。2.2 收费规则用表驱动别把价格写死在代码里停车场收费规则几乎一定会变开业期间免费两小时、三个月后改成首小时五元、每晚十点后封顶二十元。如果把规则写在业务函数里每一次调价都要改代码重新部署而且容易改出边界 bug。更常见的做法是单独建一张收费规则表把规则做成数据class ChargeRule(models.Model): 收费规则同一停车场可配多条规则按时间段生效 lot models.ForeignKey(ParkingLot, on_deletemodels.CASCADE, related_namecharge_rules, verbose_name所属停车场) name models.CharField(max_length32, verbose_name规则名称) free_minutes models.PositiveIntegerField(default0, verbose_name免费时长(分钟)) first_hour_fee models.DecimalField(max_digits6, decimal_places2, verbose_name首小时费用) hourly_fee models.DecimalField(max_digits6, decimal_places2, verbose_name超首小时后每小时费用) daily_cap models.DecimalField(max_digits6, decimal_places2, nullTrue, blankTrue, verbose_name24小时封顶) effective_from models.DateTimeField(defaulttimezone.now, verbose_name生效时间) effective_to models.DateTimeField(nullTrue, blankTrue, verbose_name失效时间) class Meta: db_table charge_rule verbose_name 收费规则 class PaymentRecord(models.Model): 支付流水一条停车记录对应最多一条成功支付流水 record models.OneToOneField(ParkingRecord, on_deletemodels.PROTECT, verbose_name停车记录) amount models.DecimalField(max_digits8, decimal_places2, verbose_name实收金额) method models.CharField(max_length16, verbose_name支付方式) transaction_id models.CharField(max_length64, blankTrue, verbose_name第三方流水号) paid_at models.DateTimeField(auto_now_addTrue, verbose_name支付时间) class Meta: db_table payment_record verbose_name 支付流水收费规则表里加生效时间和失效时间是为了支持未来“按时间段切换价格”的需求。同一停车场在不同时段可以有多条规则结算时按入场时间找当时生效的那条。daily_cap可为空没有封顶的停车场就不填。支付流水表用OneToOneField关联停车记录避免同一条记录被重复支付也方便财务对账时按流水号倒查。2.3 表结构变更交给 Django Migration 管理模型定稿后接下来第一步就是生成迁移文件并执行迁移python manage.py makemigrations parking python manage.py migratemakemigrations会比较当前模型与历史迁移文件的差异自动生成一个 migration 文件migrate则把变更应用到数据库。这里有两个实际提醒。第一不要用migrate --fake跳过真实迁移。常见场景是在开发环境改完模型直接migrate后发现线上也要同步有人图省事就--fake结果数据库表结构和迁移记录对不上后续加字段时永远提示冲突整条迁移链直接废掉。第二给已有数据表加非空字段时migration 会停下来问你要默认值。如果表里有存量数据必须给一个合理的默认值或先设 nullable否则线上执行会失败或直接锁表。生产环境建议先在测试库跑一遍migrate确认锁表时间和数据迁移量可接受再在窗口期内操作正式库。3. 车牌识别模块接入设备回调、数据清洗与置信度过滤模型层就绪后下一个重头戏是把车牌识别设备接入 Django。这里说的接入不是去训练识别模型而是对接市面上一体机的输出结果。常见设备像臻识车牌识别一体机配置工具里能调出识别结果推送地址你只需要把 Djang 接口地址配置到设备端即可真正的复杂度在回调数据的处理和边界判断。3.1 识别设备的两种接入方式回调优先SDK兜底车牌识别一体机的接入方式行业中基本分两类。第一类是 HTTP 回调设备识别到车牌后把识别结果、抓拍时间、置信度、抓拍图地址打包 POST 到你配置的接口业务系统处理完再返回响应。第二类是 SDK设备厂家提供动态库或 Python 包业务系统主动连接设备取流识别或读取结果。两者怎么选我一般优先选 HTTP 回调原因是链路最短、最容易排查。设备端把报文发过来Django 里一个视图函数就能处理失败时设备会按自己的策略重发不需要你维护长连接会话。SDK 方式适合识别算法需要直接跑在业务进程里的场景或者对抓帧时机有强控制要求的项目但代价是你得管理设备连接状态、断线重连和 SDK 版本兼容排障成本明显更高。大多数出入口道闸场景一体机内置识别算法Django 端用 HTTP 回调就够了。下表对比两类方式的关键差异对比项HTTP 回调SDK 接入部署方式设备主动推送业务端只暴露接口业务端引入 SDK主动连接设备断线处理设备端负责缓存重发业务端需自行实现重连并发能力天然适应多路设备同时回调受连接数和 SDK 自身限制排查难度抓包看 POST 报文即可需要查 SDK 日志和连接状态适用场景出入口道闸标准一体机定制采集、实时视频流分析如果是新项目起步选 HTTP 回调最省事。接入前先到一体机的配置界面把“识别结果推送”指向http://你的服务器IP:8000/api/device/callback/顺便确认推送的报文格式是 JSON 还是 form这决定了接口解析方式。3.2 封装一个回调接口先验格式再谈业务无论设备报文长什么样Django 端都应该先做一个统一的入口视图把解析、校验和业务处理拆开。下面是用 Django 视图类实现的一个回调接口模板import json import re import logging from django.http import JsonResponse from django.views import View from django.views.decorators.csrf import csrf_exempt from django.utils.decorators import method_decorator logger logging.getLogger(parking.device) # 普通蓝牌中文省份 英文字母 5~6位字母数字 PLATE_PATTERN re.compile(r^[\u4e00-\u9fa5][A-Z][A-Z0-9]{5,6}$) method_decorator(csrf_exempt, namedispatch) class DeviceCallbackView(View): def post(self, request): # 设备是服务端调用不走浏览器关闭 CSRF 是行业常规 try: payload json.loads(request.body) except (json.JSONDecodeError, TypeError): logger.warning(invalid json from device: %s, request.body[:200]) return JsonResponse({code: 400, msg: invalid json}) plate str(payload.get(plate, )).strip().upper() confidence float(payload.get(confidence, 0) or 0) lot_id payload.get(lot_id) direction payload.get(direction, entry) # entry / exit capture_time payload.get(capture_time, ) # 格式校验车牌不符合结构直接丢弃不给业务层填脏数据的机会 if not PLATE_PATTERN.match(plate): logger.info(plate format rejected: %s, plate) return JsonResponse({code: 200, msg: ignored}) # 置信度过滤过低的结果往往是把其他文字误识别成车牌 if confidence 0.75: logger.info(low confidence ignored: %s %.2f, plate, confidence) return JsonResponse({code: 200, msg: ignored}) # 到这一步才进入业务层处理入场或出场逻辑 # handle_entry_event(plateplate, lot_idlot_id, # directiondirection, capture_timecapture_time) return JsonResponse({code: 0, msg: ok})这段代码的逻辑顺序是有讲究的解析 JSON 在前格式校验其次置信度过滤再其次业务处理最后。设备回调可能在高峰期每秒进来几十条每个环节都要快速拒绝无效数据减少对数据库的压力。csrf_exempt是因为设备端是服务端调用没有浏览器会话不开 CSRF 反而绕开了对请求头 token 的额外依赖如果对安全性有更高要求可以在报文里约定一个签名头在视图里验签即可。置信度阈值 0.75 是我常用的起始值。设太高会把新能源绿牌、双层车牌这类识别难度大的结果漏掉设太低又容易把广告牌上的文字识别成车牌混进来。调试阶段可以先打到 0.6观察三天识别日志找到误识别最高发的那批结果再往上调阈值。这属于典型的“玄学”参数不同场地、不同光照条件差异很大一定要根据实际日志调而不是抄默认配置。3.3 识别结果的后处理车牌清洗与重复识别去重设备返回的车牌号并不总是干净的。常见情况是字符串里带横杠、点、空格比如“京A·12345”或“京A-12345”还有可能在置信度字段后面跟一串算法版本号。如果不做清洗数据库里存进去的就是脏数据后面按车牌精确查询时非常容易翻车。清洗函数一般长这样def normalize_plate(raw: str) - str: 清洗设备原始输出去掉非中文字母数字字符统一转大写 raw raw.strip().upper() return re.sub(r[^\u4e00-\u9fa5A-Z0-9], , raw)清洗规则很简单只保留中文、大写字母和数字。处理完后再用之前的PLATE_PATTERN做一次校验清洗后格式仍然不对的就丢弃。注意不要直接改原始字段先把清洗后的值赋给新变量后续所有业务逻辑都使用清洗后的值。重复识别是另一个高频坑。设备在识别不稳定或连续抓拍时可能对同一辆车在几秒内推送两三次相同结果。如果不做去重入场会生成两条在场记录出场会生成两条结算记录财务直接对不上。常见做法是加一个短时窗口去重from django.core.cache import cache def is_duplicate_event(plate: str, direction: str) - bool: 同一方向同一车牌在5秒内只认第一次 key fplate_event:{plate}:{direction} if cache.get(key): return True cache.set(key, 1, timeout5) return False这里用 Django 的缓存框架默认可以落到 LocMemCache 或 Redis。5 秒窗口适合道闸场景车辆从触发识别到完全通过道闸通常也就几秒窗口太短拦不住设备重发窗口太长又可能把真实的新入场误判成重复。等设备稳定后可以把窗口缩到 3 秒。更严谨的做法是让设备在每次回调时带一个递增的message_id数据库表里建唯一索引天然幂等——但这个前提是设备协议支持一般在接口文档里确认。4. 收费计算与数据库事务入场、结算这一条链路上的坑停车场系统里数据库事务是保证账目“不错、不重、不丢”的关键。识别接口只管把车牌报过来业务层要做的是一套完整的入场、出场、计费事务流程。这一章的代码都不是什么高级技巧但每行都对应实际踩过的数据一致性问题值得逐条细看。4.1 入场处理防止重复入场也要防止车位超卖入场事件到后端时第一件事是查这辆车当前是否已经在场。如果已经在场说明设备重复推送直接拒绝即可。同时还要处理车位已满的情况这两件事必须放在同一个数据库事务里from django.db import transaction, models from django.utils import timezone class EntryDuplicateError(Exception): pass class LotFullError(Exception): pass transaction.atomic def create_entry_record(plate, lot_id, entry_image): # 先锁停车场的行防止并发下车位总数被多笔请求同时修改 lot ParkingLot.objects.select_for_update().get(pklot_id) vehicle, _ Vehicle.objects.get_or_create(plate_numberplate) # 同一车牌不能重复在场 exists ParkingRecord.objects.filter( vehiclevehicle, statusparking ).exists() if exists: raise EntryDuplicateError(f车辆已在场: {plate}) # 车位余量判断 if lot.total_spaces - lot.occupied_count 0: raise LotFullError(f车位已满: {lot.name}) record ParkingRecord.objects.create( lotlot, vehiclevehicle, entry_imageentry_image, entry_timetimezone.now(), statusparking, ) # F表达式直接在数据库层原子加一避免并发读改写 ParkingLot.objects.filter(pklot.pk).update( occupied_countmodels.F(occupied_count) 1 ) return record这个函数里有三个容易忽略的细节。select_for_update()会对停车场这行记录加写锁保证后续的余量判断和更新在事务内不会被打断。get_or_create是按车牌查车辆档案如果是新车牌就建一条这里要求plate_number有唯一约束否则并发时会撞出重复车辆记录。最后的occupied_count更新用的是F(occupied_count) 1而不是先把值读出来再加回去这样可以避免两个并发请求都读到同一个旧值把数量加错。事务的边界也要克制。transaction.atomic只包住了数据库写操作不要在事务里执行外部 HTTP 请求或图片下载否则锁的持有时间会被人为拉长高并发时直接拖垮整个停车场的入场登记。4.2 出场计费规则计算与封顶逻辑出场结算时要根据入场时间和当前时间算出时长再套用收费规则计算金额。这块逻辑看起来简单但“向上取整”和“封顶”两个细节处理不好用户就会来投诉。from decimal import Decimal def calculate_fee(rule, entry_time, exit_time): 按收费规则计算停车费返回 Decimal 金额 if entry_time is None or exit_time is None: return Decimal(0) duration_seconds (exit_time - entry_time).total_seconds() if duration_seconds 0: return Decimal(0) minutes int(duration_seconds // 60) # 免费时段不用计费 if minutes rule.free_minutes: return Decimal(0) # 首小时收费 if minutes 60: return rule.first_hour_fee # 超出首小时后按小时向上取整满61分钟按2小时算 extra_hours (minutes - 60 59) // 60 fee rule.first_hour_fee extra_hours * rule.hourly_fee # 24小时封顶 if rule.daily_cap and fee rule.daily_cap: fee rule.daily_cap return fee.quantize(Decimal(0.01))注意extra_hours的计算用了(minutes - 60 59) // 60这是向上取整的经典写法。比如停车 61 分钟(61 - 60 59) // 60 60 // 60 1也就是首小时之外再算 1 小时共 2 小时费用。如果直接写minutes // 6061 分钟会被截断成 1 小时少收一小时的钱。daily_cap的封顶判断放在最后且只在填了封顶值时才生效。这里再多提一句如果rule.first_hour_fee是DecimalFieldrule.hourly_fee也是DecimalField那整个算式里的加减乘除都保持在Decimal上不会出现浮点误差。如果中途不小心和 Python 原生的float相乘结果会退化成浮点所以实际开发时我习惯在函数开头显式做个转换。4.3 用 select_for_update 锁住结算记录防止重复扣费出场结算的高危场景是同一辆车的出场事件被设备以极高频率重复推送或者收费员手动操作和自动推送同时发生。如果不用锁两个请求可能同时读到一条“在场”记录同时结算生成两条流水用户被扣两次钱。Django 里处理方案是select_for_update()transaction.atomic def settle_exit(record_id): # 锁住这条停车记录其他请求必须等当前事务完成 record ParkingRecord.objects.select_for_update().get(pkrecord_id) # 二次判断状态已经被结算过的直接返回 if record.status ! parking: return record rule ChargeRule.objects.filter( lotrecord.lot, effective_from__ltetimezone.now() ).order_by(-effective_from).first() exit_time timezone.now() record.exit_time exit_time record.fee calculate_fee(rule, record.entry_time, exit_time) record.status finished record.save() # 释放一个车位 ParkingLot.objects.filter(pkrecord.lot_id).update( occupied_countmodels.F(occupied_count) - 1 ) return record这段代码的锁顺序是先锁ParkingRecord行再更新ParkingLot的占用数。多事务并发时如果锁的顺序不统一比如一个事务先锁记录再锁停车场另一个事务先锁停车场再锁记录就会形成死锁数据库会随机杀掉一个事务并报死锁错误。所以先锁停车记录再释放车位这个顺序在项目里要保持一致绝不能一处先锁车辆、另一处先锁停车场。至于状态二次判断是因为select_for_update()拿到锁之后等待中的事务可能已经在前一个事务提交后把记录改成了“finished”如果不再判断一次就会出现重复结算。事务范围同样要克制。settle_exit只做状态更新和车位释放不要把支付回调、消息推送、图片归档塞进来。支付成功回调再单独处理PaymentRecord的创建这样可以随时重试支付环节而不影响停车记录本身。5. 避坑与常见问题排查这五条踩坑记录每一条都值半天时间设备联调和上线初期最容易出问题的往往不是什么大架构问题而是几类看起来很小、排查起来却极其耗时的细节。下面五条按“现象 → 原因 → 解决”的顺序写都是我或同行在相似项目里真实处理过的。5.1 车牌字段里出现横杠、点、空格导致查询比对失败现象数据库里车牌号存的是“京A·12345”报价接口按用户输入“京A12345”查永远查不到这条车。原因一体机识别结果自带间隔符“·”或者报文里的车牌字段拼接时带了横杠和空格。清洗逻辑只做了strip()没有把中间字符过滤掉。解决把车牌清洗和正则校验放在回调入口处所有进入业务逻辑的车牌都必须经过normalize_plate()同时给Vehicle.plate_number建唯一约束从源头杜绝脏数据落库。plate normalize_plate(payload.get(plate, )) if not PLATE_PATTERN.match(plate): return JsonResponse({code: 200, msg: ignored})这个坑的隐蔽点在于设备刚上线时数据量小脏数据不影响多少功能等车牌数量过万后所有按车牌查询的接口都开始随机出问题排查时要翻到几个月前的老数据才能发现源头。5.2 高并发出场时报 “Lock wait timeout exceeded”现象早晚高峰出口排队多辆车同时出场Django 日志里频繁出现Lock wait timeout exceeded; try restarting transaction部分车被多扣或出场失败。原因事务里既锁了ParkingRecord又锁了ParkingLot但各接口加锁顺序不统一形成死锁后 MySQL 随机回滚一个事务另一个常见原因是事务里做了耗时操作锁持时间过长其他请求等待超时。解决全局统一定义加锁顺序推荐“先业务表后统计表”也就是先锁ParkingRecord再锁ParkingLot。同时确保事务代码块内不调用外部 HTTP、不下载图片、不执行识别——这些全部放到回调入口处提前完成让事务保持毫秒级。排查这种问题最快的方式是打开 MySQL 的死锁日志SHOW ENGINE INNODB STATUS;看LATEST DETECTED DEADLOCK段里面明确列出了两个事务锁冲突的表和行照着调整代码里的锁顺序即可。死锁不可怕可怕的是不调整代码只重启服务第二天同一时间继续翻车。5.3 设备没有校时计费结果比实际多了十几分钟现象有用户投诉停车 59 分钟被收了 2 小时的费用系统显示的停车时长比你手工记录的多了十几分钟。原因出入口一体机在无 NTP 环境运行数周后设备时钟慢慢漂移用设备回调里的capture_time作为入场时间而出场时间是服务器时间服务器和设备的时钟不一致入场时间被推前十几分钟时长跨越了整小时边界。解决回调接口不要信任设备时间入场时间和出场时间一律以 Django 服务器时间为准也就是用timezone.now()。如果设备报文里带了抓拍时间只作为日志字段保存不参与计费计算。同时在一体机配置工具里开启 NTP 校时指向内网时间服务器。需要额外注意时区配置。Django 项目里保持USE_TZ True和TIME_ZONE Asia/Shanghai所有模型时间字段统一用timezone.now()写入不要混用datetime.now()。一旦模型里既有datetime.now()写入的字段又有timezone.now()写入的字段日期差值就会莫名其妙多出 8 小时而且只在某些时段触发极其难查。5.4 同一辆车出口反复触发识别系统重复结算了两次现象出口收费屏显示该车已完成支付但数据库里同一个ParkingRecord下出现了两条支付流水或者出场记录被自动结算后收费员又手动操作了一遍。原因设备重复推了相同结果Django 端虽然加了缓存去重但缓存用的是单机LocMemCache在多进程或多机器部署下各进程独立缓存去重失效。解决把去重缓存切换到 Redis让所有应用进程共享同一份 key同时入口处增加一次幂等判断出场记录从“parking”到“finished”的更新用条件更新而不是无条件更新updated ParkingRecord.objects.filter( pkrecord_id, statusparking ).update(statusfinished, exit_timetimezone.now()) if updated 0: # 说明记录已被其他请求处理过 return这种“乐观更新”配合缓存窗口是双保险。缓存负责拦截高频重复请求数据库条件更新负责兜底并发边界。两套机制缺一不可只靠缓存的话并发请求打到不同进程时还是会出问题。5.5 联调时设备日志显示发送成功Django 端一个请求都没收到现象一体机配置推送地址后设备日志显示 HTTP 200但 Django 日志里没有任何记录。原因一是回调地址配置成了localhost或127.0.0.1设备在独立网段请求发到设备自己的本机回环地址二是开发环境 Django 未加入ALLOWED_HOSTS请求被拦截三是端口没开或防火墙拦截。解决先在 Django 机器上用curl模拟设备请求验证接口本身通不通curl -X POST http://127.0.0.1:8000/api/device/callback/ \ -H Content-Type: application/json \ -d {plate:京A12345,confidence:0.9,lot_id:1,direction:entry}接口返回正常后再配置设备。回调地址用 Django 机器的局域网 IP不要用localhost。如果设备和开发机跨网段先在同一网段联调确认协议没问题后再部署到有公网地址的测试服务器上。排查这类问题时最有效的做法是逐步缩短链路先 curl 自测、再同网段设备测试、最后跨网段联调每步都能确认一个环节。6. 进阶玩法用 Channels 推送入场事件到岗亭大屏基础链路跑通后最常被现场人员提的需求是岗亭大屏要实时显示“某车牌入场了”“剩余车位数还剩多少”。如果靠前端定时轮询车流量大时会白白消耗接口请求而且有秒级延迟。用 Django Channels 的 WebSocket 能力可以做到后端一旦有数据就实时前端推送这也是热词里经常搜到“django websocket实现后台有数据前端推送”的实际场景。6.1 用 Channels 按停车场维度推送事件# consumers.py import json from channels.generic.websocket import AsyncWebsocketConsumer class ParkingEventConsumer(AsyncWebsocketConsumer): async def connect(self): self.lot_id self.scope[url_route][kwargs][lot_id] self.group_name flot_{self.lot_id} await self.channel_layer.group_add(self.group_name, self.channel_name) await self.accept() async def disconnect(self, code): await self.channel_layer.group_discard(self.group_name, self.channel_name) async def parking_event(self, event): # 这个方法名要和 group_send 的 type 参数对应 await self.send(text_datajson.dumps(event[data]))业务侧在车位状态变化时调用channel_layer.group_send推送事件即可。比如入场成功后from channels.layers import get_channel_layer from asgiref.sync import async_to_sync channel_layer get_channel_layer() async_to_sync(channel_layer.group_send)( flot_{lot_id}, { type: parking.event, data: {plate: plate, action: entry, ts: timezone.now().isoformat()}, }, )前端用WebSocket连接ws://host/ws/parking/{lot_id}/收到消息后直接更新大屏上的车牌列表和剩余车位数不用刷新页面也无需轮询。部署时记得把 ASGI 应用作为入口用 Daphne 或 Uvicorn 启动别再只跑 WSGI 的runserver。6.2 高频车位查询用缓存扛住大屏如果每秒钟都要显示剩余车位数直接查ParkingLot表虽然也能跑但高并发时会产生很多无谓查询。更稳妥的做法是把车位余量写进 Redis并设置一个短过期时间from django.core.cache import cache def get_remaining_spaces(lot_id): key flot_remaining:{lot_id} value cache.get(key) if value is None: lot ParkingLot.objects.get(pklot_id) value lot.total_spaces - lot.occupied_count cache.set(key, value, timeout3) # 3秒过期保证新鲜度 return value入场和出场事务里同步更新occupied_count之后顺手把lot_remaining:{lot_id}这个键删掉下次读取时就会自动回源数据库重新计算。3 秒过期时间对道闸场景完全够用总量不高的停车场甚至可以设到 10 秒减少数据库压力。这套方案的价值在于核心收费链路全部落在 Django 模型和数据库事务上WebSocket 和 Redis 只是锦上添花的展示层。项目实际推进时我习惯先把入场、结算、支付流水这三条链路跑通再去做实时推送否则前端越华丽后端账目越乱上线后越煎熬。希望这篇笔记能把你在方向选择上省下的时间花到真正值得打磨的计费正确性上去毕竟停车场的账一天错几笔就是几百块收入对不上这种体验谁做谁知道。本文还有配套的精品资源点击获取
02
RELATED NEWS

相关资讯

更多网站建设与数字化升级内容

03
WHY YAOTU

想打造同款高转化官网?

懂行业、懂生意,从建站到增长一站式陪跑

◈

场景化定制

不做模板站,围绕你的业务场景量身设计,小众不撞款。

◐

营销型架构

以转化目标组织内容与路径,让官网真正带来询盘。

▲

全周期服务

设计、开发、运营、运维一体,上线只是开始。

免费获取你的建站方案

留下需求,专属顾问 24 小时内为你输出方案建议。