首页 / 资讯中心 / 文章详情

基于Django与MySQL的停车场预约计费系统:数据库设计与并发事务实践

基于Django与MySQL的停车场预约计费系统:数据库设计与并发事务实践 ★ FEATURED ARTICLE
简介一套基于PythonDjangoMySql开发的停车场预约停车计费系统毕业设计源码包面向计算机相关专业毕业生及需要完成同类课设的开发者。系统采用管理员与用户双角色用户可注册登录、按楼层/区域查询车位信息、选择车位预约并自动检测时间冲突还能登记管理个人车辆、查询预约记录与停车记录、发布留言管理员可维护用户及区域车位信息办理车辆停车、离开并自动结算费用同时完成预约审核、新闻公告发布与留言处理。资源内含2000个文件以js、html、css等前端资源为主另有少量Python源码文件与数据库脚本压缩包整体5.98MB。已有227人学习下载。除完整项目代码外还提供了数据库脚本及用户、区域、停车位等核心实体设计便于快速建表跑通项目理解预约冲突检测、订单审核、计费结算等业务逻辑适合毕业设计参考或基于Django2.2、Python3.7、MySQL环境二次开发。1. 停车场预约计费系统为什么毕业设计选它最稳每年毕业季总有一批人卡在选题上既要工作量达标又要技术栈能写清楚还要答辩时能现场演示不翻车。停车场预约停车计费系统恰好卡在这个平衡点上——业务规则一眼能懂但真做起来涉及预约状态流转、计费策略、MySQL事务与并发扣费往深里挖全是数据库和Django的硬功夫。一个能跑的预约计费系统面上看是个CRUD骨子里是把数据库设计、ORM查询、事务隔离和流程状态机全过了一遍正是答辩时最有话可说的那种项目。这套系统最值钱的地方不在界面而在三张核心表的设计和计费流程的完整闭环。预约、入场、出场、扣费每一步都在和数据库打交道任何一环断了系统就露怯。我的建议是把重心压在业务逻辑和数据库设计上前端简单够用就行别把时间耗在CSS调样式上。新手能按步骤把环境搭起来跑通全流程熟手则可以把预约冲突、锁车延迟扣费等边界场景挖深这些才是拉开档次的地方。2. 先把数据库建明白停车场的钱都压在表结构里2.1 三张核心表车位、预约订单、计费流水怎么设计这个系统的地基是MySQL数据库脚本而脚本的灵魂是表关系。停车场预约计费本质上就三件事车位在不在、被谁约了、停了多久该收多少钱。围绕这三件事核心表通常逃不开车位表、预约订单表和计费流水表。车位数固定订单表负责记录每一次预约计费流水表负责把停车时间换算成金额三张表用外键串起来就是一个最小可用闭环。拿车位表来说常见字段是车位编号、区域、是否启用、当前状态。这里有个新手常踩的坑把车位状态直接写死成“空闲/占用”。一旦加了预约功能车位状态就至少要拆成三种空闲、已被预约、已入场占用。而且“已被预约”和“已占用”是两个概念——预约了没到场车位其实还闲着到了场没出场车位才算真占用。如果只用一个字段表达后面的冲突判断和计费逻辑全部要打补丁。预约订单表是业务核心建议字段至少覆盖订单号、用户ID或用户手机号、车位ID、预约入场时间、预约出场时间、实际入场时间、实际出场时间、订单状态、支付状态、应付金额、实付金额。其中订单号务必要用业务号不要用自增ID裸奔因为订单号会打在小票和短信里而自增ID会把停车场每天的单量暴露出去。计费流水表稍微轻量一些但也不能省。每次扣费或者预扣款都得有一条流水记录字段包含关联订单号、计费开始时间、结束时间、时长分钟数、计费规则版本、金额、流水类型预扣/实扣/退款。之所以要把计费规则版本单独存下来是因为停车场调价太常见了如果不存版本事后对账时你根本说不清这笔钱是按哪套价格算出来的。实际的订单生命周期是这样流转的用户发起预约写入一条预约订单状态置为“已预约”同时把车位标记为“已被预约”用户到场入场系统比对实际入场时间和预约时间更新车位的状态为“已占用”用户出场时系统根据入场时间和出场时间计算费用生成流水并更新订单金额和支付状态。整个过程所有状态变化都落在数据库里才能在答辩的时候拿查询语句展示每一步的可追溯性。2.2 MySQL脚本的写法表结构、索引、初始化数据一锅端数据库脚本文件一般拆成三部分建表语句、索引和初始化数据。建表时的几个细节直接决定后面的开发体验。第一主键定成自增INT是省事但如果考虑以后分库分表就要犹豫一下纯毕业设计自增ID完全够用别在这上面过度设计。第二时间字段建议用DATETIME而不是TIMESTAMP虽然TIMESTAMP省一点存储但DATETIME没有2038年的问题且直观显示格式调试时少折腾。第三金额字段千万、千万、千万不要用FLOAT或DOUBLE。MySQL的浮点数存在精度丢失问题FLOAT算出来的金额可能差出几分钱。计费系统的金额用DECIMAL(10,2)精确到分这是对账不出乱子的大前提。第四订单状态这类字段用TINYINT配注释比直接用字符串省空间也更规范但注释一定要写清楚否则三个月后你自己都不知道0代表什么。索引设计上订单表的查询模式很固定按用户查订单、按车位查预约时间、按状态筛选超时订单。所以最常见的两个复合索引是用户ID创建时间联合索引、车位ID预约时间段联合索引。后面这个索引直接服务于冲突检测——判断一个车位在某个时间段是否已被预约一条带上车位的范围查询就可以扫索引完成。初始化数据的量也别太抠这是你后面做演示和调接口的弹药库。车位列30-50个分布到三四个区域比如A区普通车位、B区充电车位、C区VIP车位。每类车位计费规则还不一样这样你在写业务逻辑时天然就要处理多规则答辩时这就是一个可以讲的点。用户表放三五个测试账号密码用Django的make_password生成哈希存进去别用明文不仅是安全问题答辩时老师如果问你密码怎么存的这是一个加分的细节。数据库脚本建完后建议先用一条SQL验证所有表能对上SELECT COUNT(*) FROM information_schema.TABLES WHERE TABLE_SCHEMAparking_db确认表数量对不对再逐个表SHOW CREATE TABLE看结构。3. Django项目骨架与核心业务实现预约冲突怎么判断最省事3.1 创建项目和应用目录按功能拆别全堆在models.py里Django项目从结构上就该把业务拆开常见做法是建一个users应用管用户、parking应用管车位、orders应用管预约订单和计费。如果嫌应用太多也至少把订单和计费单独拆一个应用出来因为这两个模块的业务逻辑最重拆开写views和services都好维护。纯堆在一个models.py里到了写计费逻辑的时候文件几百行往上走你自己都想重新来过。创建项目的基本命令是老生常谈但这里有一个容易被忽略的细节虚拟环境一定要先建不要全局装Django。全局环境装出来的项目依赖版本是公开的哪天你把项目移到别的机器上装了一堆不兼容的包会花整整一天在解决环境冲突上。用python -m venv venv建虚拟环境然后pip install django mysqlclient再django-admin startproject parking_project这是最稳的起手式。settings.py里需要配三处缺一不可。第一处INSTALLED_APPS里把新创建的应用加进去。第二处DATABASES配置改成MySQL连接ENGINE填django.db.backends.mysqlNAME、USER、PASSWORD、HOST、PORT一个都不能少。第三处时区和语言。这点尤其重要TIME_ZONE要配成Asia/ShanghaiUSE_TZ设为True否则Django默认存UTC时间你的预约时间会整体偏移8小时入场出场的时间全部对不上计费结果直接乱套。多个Django版本之间的写法差异也要留意。新版Django把urls配置改成了path语法老教程里的url正则导入已经变了。另外Django 4.0以上对MySQL版本有要求MySQL 5.7勉强能跑8.0是舒适区。如果你用的是Django 5.x建议配MySQL 8.0以上省得遇到不明不白的兼容报错。3.2 预约冲突检测用时间区间重叠查询替代逐条遍历预约功能里最核心的一段业务逻辑就是判断车位在用户想要的时间段里是否已经被约走。新手最容易想到的做法把该车位的所有预约记录查出来逐条遍历比较时间。这个做法在数据量小的时候能用订单一多效率就惨不忍睹而且代码写得又臭又长。真正高效的方案是用SQL的时间区间重叠判断。两条预约冲突的条件是新预约的开始时间小于已有预约的结束时间且新预约的结束时间大于已有预约的开始时间。用Django ORM写就是Q对象加两个条件做ANDfrom django.db.models import Q from django.utils import timezone def check_carport_conflict(carport_id, start_time, end_time, exclude_order_idNone): 检查车位在指定时间段内是否已有预约冲突 queryset Order.objects.filter( carport_idcarport_id, status__in[ORDER_STATUS_PREORDERED, ORDER_STATUS_PARKED], ).filter( Q(pre_start_time__ltend_time) Q(pre_end_time__gtstart_time) ) if exclude_order_id: queryset queryset.exclude(idexclude_order_id) return queryset.exists()这段代码的逻辑核心是那个Q对象组合查询。两个条件分别是已有预约的开始时间必须早于新预约的结束时间已有预约的结束时间必须晚于新预约的开始时间。只要两者同时成立时间区间就存在重叠冲突就成立。exclude_order_id参数是用来排除自身的修改预约时传当前订单ID否则自己跟自己永远冲突。queryset.exists()比queryset.count()更高效因为只要查到第一条就跑路不需要统计总数。这个方案还有一个隐藏好处因为索引设计时已经给车位ID和预约时间段建了联合索引这个查询能直接走索引扫几百条预约数据毫秒级返回。如果换成逐条遍历每次请求都要把所有记录加载到Python内存里做比较随着订单增长性能会以肉眼可见的速度劣化。这是答辩时值得主动讲的一个点——你不仅做了功能还考虑了查询效率。3.3 计费逻辑如何落成代码入场、出场、超时三个入口计费系统最怕的是逻辑散落各处入场算一次出场算一次超时任务又算一次三处逻辑不完全一致最后账单就会偶尔对不上。我的做法是把计费规则抽成独立模块所有入口统一调用一个计费函数这样只要改一处所有入口都生效。计费规则常见的有三种按时计费、按次计费、分段计费。停车场一般用的是分段计费比如首小时10元之后每小时5元24小时封顶40元。抽成函数大概长这样def calc_parking_fee(start_time, end_time, unit_price5, free_minutes15): 分段计费规则首小时10元后续每小时unit_price24小时封顶40元 if not start_time or not end_time: return 0 duration_minutes (end_time - start_time).total_seconds() / 60 if duration_minutes free_minutes: return 0 hours (duration_minutes - free_minutes) / 60 if hours 1: total 10 else: total 10 math.ceil((hours - 1)) * unit_price return min(total, 40)参数里free_minutes是免费时长一般停车场15分钟内免费代码里先扣掉免费时段再做费用计算。math.ceil的作用是把不满一小时的部分向上取整比如停了2小时10分钟按3小时算。封顶逻辑必须放在最后因为向上取整可能导致费用超过封顶值min函数直接截断。实际的项目里unit_price和封顶值建议做成数据库表不要写死在代码里。答辩时被问“如果停车场改价怎么办”直接说改数据库配置即可一句话就能把关机逻辑和运营需求解耦这件事讲清楚。入场、出场、超时三个入口调用这个函数的方式不同。用户入场时系统要做两件事把订单状态从“已预约”改成“已入场”把实际入场时间写进订单。出场时算费用、生成流水、把订单状态更新成“已完成”车位状态恢复成“空闲”。超时则是一个定时任务每分钟扫一次预约时间已过但还没入场的订单自动标记为“已过期”并把车位释放出来。定时任务可以用Django的celery也可以更轻量地用crontab跑一个manage.py命令毕设用后者足够还能避开celery环境的坑。4. 预约接口与后台管理从URL到JSON响应全链路走通4.1 视图函数写业务Serializer只做数据校验Django开发接口有两条路线一是传统得用View加JsonResponse手写二是走Django REST Framework用Serializer和ModelViewSet。毕业设计如果想省事且答辩时好讲建议直接用DRF代码量能压缩一半以上而且自带的Browsable API界面在演示时相当加分老师在浏览器里就能直接测试接口。预约接口的完整链路是这样用户POST一个预约请求携带车位ID、预约入场时间、预约出场时间服务端先做参数校验再查车位是否存在且可用然后做冲突检测最后创建订单返回结果。DRF的Serializer可以帮我们省掉很多手写校验代码class OrderCreateSerializer(serializers.ModelSerializer): class Meta: model Order fields [id, carport, pre_start_time, pre_end_time, status] read_only_fields [id, status] def validate_pre_end_time(self, value): start_time self.initial_data.get(pre_start_time) if start_time and value start_time: raise serializers.ValidationError(出场时间必须晚于入场时间) return valueread_only_fields把id和status标记为只读客户端无法伪造订单状态。validate_pre_end_time是Serializer内置的字段级校验钩子在进入视图之前就拦住“结束时间早于开始时间”这种非法请求。这里的关键点是Django的ModelSerializer在create时会自动调用create方法但如果我们想插入一段自定义逻辑就需要重写视图里的perform_create。视图这边用ModelViewSet加权限控制是比较快的一条路径。ListAPIView负责用户查询自己的预约记录CreateAPIView负责新建预约这样天然支持GET和POST的分工。查询当前用户订单时要注意perform_create或get_queryset里把queryset限定成当前登录用户否则任何用户都能看到别人的订单和车位信息这是接口安全层面最低级但也最常见的漏洞。4.2 数据库事务与并发抢车位同一秒两个用户约同一个车位预约系统天然面临并发问题。两个用户同时提交同一个车位同一时间段的预约请求如果没有并发控制数据库里会插进两条互相冲突的预约订单。表面上看起来两条请求都通过了冲突检测——因为它们各自读到的是没有其它预约的旧状态。解决这个问题最常用的方案是MySQL的事务加锁。Django的transaction.atomic块配合select_for_update可以给车位记录加行级锁让后一个请求必须等前一个请求提交后才能读取。代码长这样from django.db import transaction def create_order(request): carport_id request.data.get(carport) start_time request.data.get(pre_start_time) end_time request.data.get(pre_end_time) with transaction.atomic(): carport Carport.objects.select_for_update().get(idcarport_id) conflict check_carport_conflict(carport_id, start_time, end_time) if conflict: return JsonResponse({code: 1, msg: 该车位已被预约}, status409) order Order.objects.create( carport_idcarport_id, userrequest.user, pre_start_timestart_time, pre_end_timeend_time, statusORDER_STATUS_PREORDERED, ) carport.status CARPORT_PREORDERED carport.save() return JsonResponse({code: 0, data: {order_id: order.id}})select_for_update是Django ORM里对应MySQL行级锁的写法。当这条查询执行时对应的车位记录会被锁定直到当前事务commit或rollback。第二个并发请求走到这里时会被数据库阻塞住等前一个事务结束才开始执行。这个机制保证冲突检测和订单创建是在同一次数据库快照下完成的原子操作不会出现“先读后写”的中间状态。需要特别注意的是select_for_update必须在事务内使用所以整体包了一层transaction.atomic。这里有一个经常被忽略的细节加锁顺序尽量一致否则并发下可能死锁。比如都先锁车位再创建订单不要一个请求先锁车位另一个请求先锁订单再锁车位。订单量大了以后MySQL的锁等待超时也是一个坑InnoDB默认锁等待50秒如果业务高峰期排队时间过长会报Lock wait timeout exceeded解决思路是缩短事务内操作时长把耗时计算放到事务外面。4.3 后台管理站点的配置几分钟把运营端搞定Django自带Admin后台是毕业设计里省时间的大杀器。把三张核心表注册进admin.py几分钟就能得到一个可以操作的运营后台不用写一行前端代码。注册的代码基本上长这样from django.contrib import admin admin.register(Order) class OrderAdmin(admin.ModelAdmin): list_display (order_no, user, carport, pre_start_time, pre_end_time, status, amount) list_filter (status, pre_start_time) search_fields (order_no, user__username, carport__carport_no)list_display控制了列表页显示哪些列list_filter提供右侧的状态和时间筛选器search_fields允许按订单号、用户名和车位号做模糊搜索。这三个配置是Admin最常用的三个属性配上之后运营人员日常查单、看状态、排查纠纷的效率能高一倍。还有几个小技巧可以顺手加上date_hierarchy设为pre_start_time列表页顶部会出现时间轴向导航ordering设为(-create_time,)让最新订单排前面如果金额字段要可编辑就加到list_editable里但注意list_editable在做了分页后会有限制踩过一次就知道。Admin的显示选项虽然好用但要注意权限问题。默认Django Admin不限制访问任何人登录admin后台都能看到所有数据。毕业设计可以演示完再把Debug关掉但正式一点的做法是至少把ADMIN_ENABLED做成开关或者给staff用户分配只读权限。这个点答辩时也可能被问到提前想好措辞。5. 避坑指南身份验证、时区、编码、乱扣费四道坎5.1 身份验证踩坑手写session还是用JWTDjango默认的身份验证体系是session登录之后在浏览器里保持状态管理后台直接就能用。但如果要做前后端分离或者供小程序调用session的方案就显得笨重——小程序端没有cookie机制跨域请求带session也费劲。常见的替代方案是JWTDjango REST Framework配SimpleJWT库两三个接口就能搞定登录发令牌和刷新令牌。毕设如果做的是纯网页版把Django的默认session用起来就够了不必引入JWT。如果纠结后面前端用Vue还是小程序直接上JWT比较省心。坑点在于Django默认的认证后端只认User模型如果你改了用户模型扩展了字段必须在settings.py里写AUTH_USER_MODEL指向新模型而且要在第一次migrate之前配置好否则中途改造会让人想打人。另外一个高发坑Django自带的User模型用户名字段是唯一索引如果打算用手机号做登录标识就得扩展或替换用户模型要么用用户名做登录把手机号存到profile表里要么重写用户模型把USERNAME_FIELD改成手机号。后者更合理但migration历史一多改动成本会上升。我的建议是动手之前想清楚别再登录方式上临时变。5.2 时区和时间存储的坑预约时间整体偏移8小时时区问题几乎每个Django项目都会遇到一次。数据库里存的时间看起来没问题查出来也正常但前端传到后端的时间总是“慢8小时”或“快8小时”。根源在于USE_TZ设置和前端传参格式不一致。如果USE_TZTrueDjango默认存UTC时间展示时按当前时区转本地时间。如果你的MySQL连接没有配置time_zone或者前端传来的时间是本地的无时区字符串Django会按照系统时区解析一旦系统是UTC时间就偏了。排查这个问题时先看settings.py里的TIME_ZONE和USE_TZ再把MySQL的time_zone查出来SELECT NOW()。最终极的解决办法就是TIME_ZONEAsia/Shanghai并且所有传给前端的接口统一格式化输出不用ISO标准串。前面提到的订单表里所有时间字段都是DateTimeFieldDjango的ORM在写入时会自动做时区转换所以重点控制好输入侧就行。5.3 中文显示成乱码从建库语句就要治本MySQL建库时没用utf8mb4中文存进去或者查出来全是问号和乱码是中文开发者遇到频率最高的MySQL问题。Django连接MySQL时默认字符集是从连接参数里取的如果建库时用的latin1或者utf8mb3中文字段存进去就可能现实成????。utf8mb3的问题在于它虽然名字叫utf8但实际只支持到三字节一些生僻字和表情符号存不进去。解决方法是建库时直接写死字符集CREATE DATABASE parking_db DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci。然后在settings.py里OPTIONS配置加一行charsetutf8mb4确保Django连接也用这个字符集。已经建成latin1的库要用ALTER DATABASE加ALTER TABLE把字符集整体转过来顺带把所有VARCHAR字段也转了。另一个隐藏的点是中文排序问题utf8mb4_unicode_ci对拼音的排序不友好但毕设场景影响不大。5.4 计费时间计算的坑免费时段、跨天与封顶边界计费的边界条件比想象中多。我把常见的三类坑列出来每一条都是真实项目里踩过的。第一类是免费时长放进去之后停车时长怎么被计算。比如免费15分钟用户停了15分零30秒按时长是刚好过免费线但分钟数算出来可能因为四舍五入变回15分钟导致免费。正确做法是算总秒数再做减法不要用datetime相减后取分钟数做舍入。第二类是跨天计费。用户晚上23点入场凌晨1点出场如果按自然日分成两天计算每天的免费时段和封顶都要重新算一遍。大多数停车场按“连续停车时长”计费不跨天拆分但按天封顶的规则怎么处理就需要提前定义。常见的做法是按“入场时间到出场时间的总跨度”计算中间跨了几天就按几天封顶叠加。这个规则可以在后台配置里写清楚避免和用户纠纷。第三类是封顶边界。停车23小时50分钟离24小时还差10分钟但按小时向上取整可能已经达到封顶值了此时费用是40元还是更高如果代码里先算分段金额再做min截断结果是40元但如果规则写的是“超过24小时重新计时”那23小时50分就应该按不到24小时计算不封顶。这两种规则在真实停车场都存在关键是代码里要注释清楚你选择的是哪种答辩时被追问规则的时候才不至于含糊。6. 部署到服务器之前用一条慢查询日志把项目验收一遍项目写完之后别急着打包交差先花几个小时做一轮自查。我最常用的一招是开启MySQL慢查询日志模拟用户操作走一遍全流程然后看哪些查询超过了1秒。开启慢查询的命令是SET GLOBAL slow_query_logON同时把long_query_time设成1秒然后在跑完业务之后去查慢查询列表逐个分析有没有没建索引的全表扫描。这轮自查通常会暴露两类问题一类是Django的N1查询——查询订单列表时每个订单又去查一次用户信息和车位信息循环几百次就把接口拖慢了。解决办法是视图里用select_related(user, carport)把关联表JOIN进来把几十次查询合并成一次。另一类是没走索引的时间范围查询booking时间没有索引查询超时订单时全表扫描。这两种问题在答辩现场都有说头因为它们都来源于真实业务而非虚构的优化。验证完业务之后检查一遍线上环境配置。Debug改成False是必须的ALLOWED_HOSTS配成域名或IPSECRET_KEY不要用默认值。静态文件的处理毕设可以不深究但至少要知道runserver只适合开发环境部署用gunicorn加nginx才是常规操作。这些点写进项目README里方便后续接手的人快速了解。最后做一轮“删库重来”的演练。这是我想给你的最后一个习惯——从数据库脚本重新建库、导入初始数据、启动Django、跑通全流程。很多项目交付时代码在自己电脑上能跑换个环境就歇菜原因不是代码有bug而是环境依赖没写清楚。requirements.txt里把Django版本、DRF版本、mysqlclient版本都锁死数据库脚本在干净实例上能一键重建。这套流程走完你对这个项目的掌握程度基本就能覆盖答辩中九成的追问。希望帮到你。本文还有配套的精品资源点击获取
阅读完成 · 觉得有帮助?
咨询建站