简介:一套基于Python+Django+MySql开发的停车场预约停车计费系统毕业设计源码包,面向计算机相关专业毕业生及需要完成同类课设的开发者。系统采用管理员与用户双角色:用户可注册登录、按楼层/区域查询车位信息、选择车位预约并自动检测时间冲突,还能登记管理个人车辆、查询预约记录与停车记录、发布留言;管理员可维护用户及区域车位信息,办理车辆停车、离开并自动结算费用,同时完成预约审核、新闻公告发布与留言处理。资源内含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_SCHEMA='parking_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.mysql,NAME、USER、PASSWORD、HOST、PORT一个都不能少。第三处,时区和语言。这点尤其重要,TIME_ZONE要配成'Asia/Shanghai',USE_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对象加两个条件做AND:
from django.db.models import Q from django.utils import timezone def check_carport_conflict(carport_id, start_time, end_time, exclude_order_id=None): """检查车位在指定时间段内是否已有预约冲突""" queryset = Order.objects.filter( carport_id=carport_id, status__in=[ORDER_STATUS_PREORDERED, ORDER_STATUS_PARKED], ).filter( Q(pre_start_time__lt=end_time) & Q(pre_end_time__gt=start_time) ) if exclude_order_id: queryset = queryset.exclude(id=exclude_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_price=5, free_minutes=15): """分段计费规则:首小时10元,后续每小时unit_price,24小时封顶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(id=carport_id) conflict = check_carport_conflict(carport_id, start_time, end_time) if conflict: return JsonResponse({'code': 1, 'msg': '该车位已被预约'}, status=409) order = Order.objects.create( carport_id=carport_id, user=request.user, pre_start_time=start_time, pre_end_time=end_time, status=ORDER_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还是用JWT
Django默认的身份验证体系是session,登录之后在浏览器里保持状态,管理后台直接就能用。但如果要做前后端分离或者供小程序调用,session的方案就显得笨重——小程序端没有cookie机制,跨域请求带session也费劲。常见的替代方案是JWT,Django 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_TZ=True,Django默认存UTC时间,展示时按当前时区转本地时间。如果你的MySQL连接没有配置time_zone,或者前端传来的时间是本地的无时区字符串,Django会按照系统时区解析,一旦系统是UTC,时间就偏了。
排查这个问题时,先看settings.py里的TIME_ZONE和USE_TZ,再把MySQL的time_zone查出来,SELECT NOW()。最终极的解决办法就是:TIME_ZONE='Asia/Shanghai',并且所有传给前端的接口统一格式化输出,不用ISO标准串。前面提到的订单表里所有时间字段都是DateTimeField,Django的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配置加一行charset=utf8mb4,确保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_log='ON',同时把long_query_time设成1秒,然后在跑完业务之后去查慢查询列表,逐个分析有没有没建索引的全表扫描。
这轮自查通常会暴露两类问题:一类是Django的N+1查询——查询订单列表时,每个订单又去查一次用户信息和车位信息,循环几百次就把接口拖慢了。解决办法是视图里用select_related('user', 'carport')把关联表JOIN进来,把几十次查询合并成一次。另一类是没走索引的时间范围查询,booking时间没有索引,查询超时订单时全表扫描。这两种问题在答辩现场都有说头,因为它们都来源于真实业务而非虚构的优化。
验证完业务之后,检查一遍线上环境配置。Debug改成False是必须的,ALLOWED_HOSTS配成域名或IP,SECRET_KEY不要用默认值。静态文件的处理毕设可以不深究,但至少要知道runserver只适合开发环境,部署用gunicorn加nginx才是常规操作。这些点写进项目README里,方便后续接手的人快速了解。
最后,做一轮“删库重来”的演练。这是我想给你的最后一个习惯——从数据库脚本重新建库、导入初始数据、启动Django、跑通全流程。很多项目交付时,代码在自己电脑上能跑,换个环境就歇菜,原因不是代码有bug,而是环境依赖没写清楚。requirements.txt里把Django版本、DRF版本、mysqlclient版本都锁死,数据库脚本在干净实例上能一键重建。这套流程走完,你对这个项目的掌握程度基本就能覆盖答辩中九成的追问。希望帮到你。
本文还有配套的精品资源,点击获取