Django停车场管理系统开发复盘:事务与并发控制实践
当时我看到毕设题目列表里“停车场管理系统”的时候第一反应是这题太普通了。身边好几个同学都选过xx管理系统听起来就像数据库课设的换皮版本。但真拿到需求开始做Django停车场管理系统之后我发现自己的想法完全错了——这套系统里最难的不是增删改查而是“车位状态、停车记录、费用结算”之间永远保持一致。如果一个管理系统的核心对象在状态流转中前后矛盾再花哨的页面都撑不过答辩。这篇就当作一次个人项目复盘把从选题、项目初始化、数据建模到入场/离场核心业务、后台演示、上线前排坑的完整过程写下来。如果你是正在准备计算机毕业设计的同学尤其是打算用Django做管理系统类项目但不知道从哪里下手的这篇能帮你少走很多弯路。里面的模型设计、事务处理和几个“答辩必问坑”我都是实际踩过之后才总结出来的。1. 选题复盘停车场管理系统是典型的练手但有深度的业务题目1.1 一个听上去普通实际上吃业务逻辑的选题管理系统类毕设有一个常见毛病功能之间彼此独立比如“用户管理”和“订单管理”没有强关联增删改查写完就结束了。停车场系统不一样它的核心是一条完整的状态链路车辆入场验证车辆分配空闲车位车位从“空闲”变“占用”车辆在场车辆不能重复入场车位不能被第二辆车占用车辆离场计算停车时长算费用停车记录从“进行中”变“已完成”车位从“占用”变“空闲”统计查询按天看收入、看车位占用率这里每一步都和上一步的结果有关状态一旦对不上系统就乱了。比如一辆车出场后车位没有释放就会造成“明明显示占用但停车场空着一个位子”的尴尬情况。也正因为有这种状态联动论文里可以写出“状态机设计”“事务一致性”“并发控制”这些相对有分量的词回答答辩问题的时候就有了支撑。评阅老师问“你这个系统的难点是什么”你至少可以讲清楚“如何避免两个入口同时把同一车位分配给两辆车”“如何保证手动删除操作不会留下脏数据”而不是只会说“我实现了登录注册”。1.2 为什么我最终选择Django而不是Flask或Spring选型阶段我也犹豫过。身边同学用Spring Boot的不少但作为一个需要一个人完成全部工作的毕设我最终选了Django主要原因是Django自带Admin后台运营管理页面不用从零写省下的时间可以用来打磨核心流程。ORM、迁移、认证、表单、模板、CSRF防护这一套都内置对于3到4个月内的个人开发来说非常友好。Flask虽然轻但很多模块要自己拼数据库迁移、用户认证、Admin这些组件选型本身就会耗掉大量时间而这些时间本可以花在真正的主线任务上。Spring Boot社区强大但配置和知识面要求更高短期上手成本明显高于Django。简单说如果你想在毕设里最大程度展示“我能完整做一个软件项目”而不是“我会框架A还是框架B”Django是短期内最容易形成完整交付物的选择。注意Django版本在写这篇项目复盘时用的是4.x系列Python建议3.10以上。依赖安装直接pip install django装最新稳定版即可毕设场景不需要追求绝对版本组合但Python版本和Django版本不兼容的坑很常见尽量在同一台开发机上保持一致。2. 环境搭建与项目初始化startproject之后我做了哪三件事2.1 app目录拆分方案很多新手拿到题目会直接写一个app把所有模块塞进去文件少的时候还好一旦加入车位管理、停车记录、收入统计、用户角色这些功能models.py会迅速膨胀到上千行。我第一版就是这种单app结构后来为了让代码看起来更像正经项目重新拆成了三个核心appdjango-admin startproject parking_system . python manage.py startapp account python manage.py startapp parking python manage.py startapp financeaccount用户体系。系统里不只有超级管理员还有月卡用户账号体系需要从Django默认User扩展。parking车位、车辆、停车记录这是一切状态流转的主战场。finance收入统计、费用流水。也可以和parking放一起但独立出来能让论文里的“模块划分”更清晰。如果你觉得三个app太碎至少也要把“用户相关”和“业务相关”区分开。这样后面扩展新功能时不会动不动就改一堆文件。2.2 一开始就替换AUTH_USER_MODEL这是我特别想提醒后来人的一个点。Django的默认User表只有用户名、邮箱、密码这类字段而停车场系统往往需要绑定手机号和车牌号还要区分用户类型。最标准的做法是继承AbstractUser扩展用户模型。from django.contrib.auth.models import AbstractUser from django.db import models class User(AbstractUser): USER_TYPE_CHOICES ( (admin, 管理员), (operator, 运营人员), (member, 月卡用户), ) user_type models.CharField(用户类型, max_length20, choicesUSER_TYPE_CHOICES, defaultmember) phone models.CharField(手机号, max_length20, blankTrue) plate_number models.CharField(默认车牌号, max_length20, blankTrue, help_text月卡用户绑定车牌后该车辆离场免计费) membership_expire models.DateTimeField(月卡有效期截止时间, nullTrue, blankTrue) class Meta: verbose_name 用户 verbose_name_plural verbose_name def __str__(self): return f{self.username}({self.user_type})改完模型后必须要在settings.py里声明AUTH_USER_MODEL account.User用AbstractUser而不是直接建一张User表是因为Django内置的认证、Admin登录、Session都会默认关联这个模型。如果你在项目跑起来一段时间后再换User模型migrate很容易出现依赖错乱到时候哭都来不及。2.3 settings中需要提前配好的几项首轮开发就把这些配好后边省很多事# settings.py USE_TZ True TIME_ZONE Asia/Shanghai这个太重要了。Django默认开启USE_TZ数据库里存的是UTC时间如果你直接使用datetime.datetime.now()写入入场时间很可能比北京时间少8小时。时间错了计费就全错。正确做法是代码里一律用django.utils.timezone.now()。还有静态文件、语言和时区LANGUAGE_CODE zh-hans TIME_ZONE Asia/Shanghai STATIC_URL /static/ MEDIA_URL /media/ MEDIA_ROOT BASE_DIR / mediaMEDIA配置虽然本项目不一定用得上但毕设演示如果有图片上传需求没有提前配好会到处翻配置文件。3. 数据建模把“入场离场”翻译成几张有用的表3.1 表结构设计的起点是业务流程而不是页面我最初设计表的时候犯了一个典型错误照着网页上的输入框去建表。入场表单里填车牌、选车位就只想到“停车记录表”。结果做到出场结算时发现费用规则不知道放哪查历史停车记录又缺状态字段只能反复加列。建议先画一张业务图再动手建表核心表可以收敛为这样五类车位表ParkingSlot车位在哪个区、什么编号、当前是否空闲车辆表Vehicle车牌号、车辆类型、归属账号停车记录表ParkingRecord什么时候入场、哪个车、停在哪个车位、什么时候出场、收多少钱收费配置表FeeConfig免费时长、基础价格、超时单价、封顶价用户账号表User人员身份、手机号、月卡车绑定以及有效期finance如果做就基于ParkingRecord去按天汇总不一定需要单独表。3.2 核心模型代码与字段选择理由下面是我在实际项目中调过好几轮的版本把字段直接关系说清楚。# parking/models.py from django.db import models from django.conf import settings class ParkingSlot(models.Model): status_choices ( (FREE, 空闲), (OCCUPIED, 占用), ) slot_no models.CharField(车位编号, max_length20, uniqueTrue) area models.CharField(区域, max_length50, defaultA区) status models.CharField(状态, max_length20, choicesstatus_choices, defaultFREE) created_at models.DateTimeField(创建时间, auto_now_addTrue) class Meta: ordering [slot_no] verbose_name 车位 verbose_name_plural verbose_name def __str__(self): return f{self.slot_no}({self.area})车位表最核心的字段就是status。这里要注意车位编号最好唯一并且区域和编号分开存方便以后扩展“A区/月卡区/访客区”这类分区策略。class Vehicle(models.Model): vehicle_type_choices ( (CAR, 小型车), (SUV, SUV), (TRUCK, 货车), (MOTOR, 摩托车), ) plate_number models.CharField(车牌号, max_length20, uniqueTrue) owner models.ForeignKey( settings.AUTH_USER_MODEL, on_deletemodels.SET_NULL, nullTrue, blankTrue, related_namevehicles, verbose_name所属用户, ) vehicle_type models.CharField(车辆类型, max_length20, choicesvehicle_type_choices, defaultCAR) created_at models.DateTimeField(创建时间, auto_now_addTrue) def __str__(self): return self.plate_number车牌之所以要单独建一张Vehicle表而不是在停车记录里直接用字符串保存车牌是因为同一辆车会多次进场停放的车辆和账号需要关联。如果只在记录里放字符串根本无法维护“车辆与月卡用户”的归属关系。class ParkingRecord(models.Model): status_choices ( (PARKING, 停车中), (FINISHED, 已离场), (CANCELLED, 已取消), ) vehicle models.ForeignKey(Vehicle, on_deletemodels.PROTECT, related_nameparking_records, verbose_name车辆) slot models.ForeignKey(ParkingSlot, on_deletemodels.PROTECT, related_nameparking_records, verbose_name车位) entry_time models.DateTimeField(入场时间) exit_time models.DateTimeField(离场时间, nullTrue, blankTrue) fee_amount models.DecimalField(应收费用, max_digits10, decimal_places2, default0) status models.CharField(状态, max_length20, choicesstatus_choices, defaultPARKING) created_at models.DateTimeField(创建时间, auto_now_addTrue) class Meta: ordering [-entry_time] verbose_name 停车记录 verbose_name_plural verbose_name def __str__(self): return f{self.vehicle_id}-{self.entry_time:%Y-%m-%d %H:%M} def save(self, *args, **kwargs): # 如果离场时间被设置自动计算费用 if self.exit_time and self.status FINISHED and self.fee_amount 0: self.fee_amount self.calculate_fee() super().save(*args, **kwargs)外键用on_deletemodels.PROTECT的目的一张停车记录一旦产生它关联的车位和车辆不应该被随便删掉因为这会引发历史账单不可追溯。Django里CASCADE看起来很省心但在这种财务相关系统里很容易造成数据被静默清空。收费配置class FeeConfig(models.Model): 收费策略整个系统一般只有一行生效配置。 name models.CharField(策略名称, max_length100, default临时车收费标准) free_minutes models.IntegerField(免费分钟数, default15) first_hour_price models.DecimalField(首小时价格, max_digits6, decimal_places2, default5) hourly_price models.DecimalField(超过一小时后每小时单价, max_digits6, decimal_places2, default2) daily_cap models.DecimalField(单次24小时封顶价, max_digits8, decimal_places2, default20) updated_at models.DateTimeField(更新时间, auto_nowTrue) class Meta: verbose_name 收费配置 verbose_name_plural verbose_name这里我没有做成多条分时段规则表原因是毕业设计场景下“入场时长×超出费用”已经足够体现业务逻辑再复杂一点容易把自己绕进去。如果你想做得更漂亮可以扩展成白天晚上不同单价但工作量会成倍上涨。3.3 停车记录和状态机的关系停车记录的状态变化是最值得写进论文的PARKING入场成功后生成记录车位占用中FINISHED车辆正常离场车位释放CANCELLED管理员发现入场记录错误后取消车位释放一个已取消的记录不能出现在收入统计里否则账就平不了。可以设计一个最常用的查询条件组合比如“查询今天还在场的车”ParkingRecord.objects.filter(entry_time__datetoday, statusPARKING)在MySQL里对entry_time这种字段做日期函数过滤会导致索引失效建议加一个entry_date字段或者在过滤时使用时间区间。为了毕设演示记录量不大时问题不明显但论文里如果能提到“避免在日期字段上使用函数导致索引失效”会显得思考更完整。4. 业务代码入场、车位分配与离场计费的事务实现这一章是整个系统的核心也是答辩老师最容易深挖的地方。4.1 入场流程的代码为什么需要用事务入场业务的步骤是查询车辆是否已在场内查询一个空闲车位创建一条停车记录把车位状态改成占用如果没有事务步骤2和步骤4之间的间隙另一个请求可能也查到这个空车位然后创建第二条记录。看代码是这样# parking/views.py from django.db import transaction from django.shortcuts import render, redirect from django.utils import timezone from django.contrib import messages from .models import ParkingSlot, ParkingRecord, Vehicle transaction.atomic def car_entry(request): 入口岗亭管理员提交车牌号后自动分配车位。 if request.method ! POST: return render(request, parking/entry.html) plate_number request.POST.get(plate_number, ).strip().upper() if not plate_number: messages.error(request, 车牌号不能为空) return redirect(parking:car_entry) # 防止同一辆车重复入场 if ParkingRecord.objects.filter(vehicle__plate_numberplate_number, statusPARKING).exists(): messages.warning(request, 该车辆尚未离场不能再次入场) return redirect(parking:car_entry) vehicle, _ Vehicle.objects.get_or_create(plate_numberplate_number) # 锁定空闲车位避免并发冲突 slot ParkingSlot.objects.select_for_update().filter(statusFREE).order_by(slot_no).first() if not slot: messages.error(request, 停车场当前没有空闲车位) return redirect(parking:car_entry) ParkingRecord.objects.create( vehiclevehicle, slotslot, entry_timetimezone.now(), statusPARKING ) slot.status OCCUPIED slot.save(update_fields[status]) messages.success(request, f车辆{plate_number}已入场分配车位{slot.slot_no}) return redirect(parking:car_entry)为什么这里要select_for_update()它的作用是给那一条车位记录加行锁。如果A车和B车同时提交入场B车的查询会一直等A车的事务提交后才能继续从而保证同一个车位不会被同时分配给两辆车。注意transaction.atomic和select_for_update必须配合事务使用。如果只在普通查询上加select_for_update但没有包在一个事务里锁不会像预期那样保持到后续更新完成。另一个容易被新手忽略的地方是不要直接get_or_create之后不保存。Vehicle是唯一车牌号建立的第一次插入没问题但如果车牌在另一台电脑上提交get_or_create也可能遇到并发插入导致的唯一键冲突。毕设一般并发量低不用过于焦虑但论文里可以讨论这个点。4.2 离场流程先算钱再释放车位离场比入场要谨慎。核心步骤顺序不能乱找到一条状态为PARKING的记录设置离场时间为当前时间计算费用更新停车记录和车位状态记录费用可以被后续统计查询transaction.atomic def car_exit(request, record_id): 管理员在出口确认放行按实际停车时长结算费用。 record ParkingRecord.objects.select_for_update().filter( idrecord_id, statusPARKING ).first() if not record: messages.error(request, 该停车记录不存在或已经离场) return redirect(parking:index) record.exit_time timezone.now() record.status FINISHED # 如果该车绑定的用户月卡在有效期内直接免费否则正常计费 if record.vehicle.owner and record.vehicle.owner.membership_expire: if record.vehicle.owner.membership_expire timezone.now(): record.fee_amount 0 else: record.fee_amount record.calculate_fee() else: record.fee_amount record.calculate_fee() record.save() record.slot.status FREE record.slot.save(update_fields[status]) messages.success(request, f车牌{record.vehicle.plate_number}离场应收{record.fee_amount}元) return render(request, parking/exit_result.html, {record: record})4.3 计费函数的算法设计停车计费里面有一个很容易踩的坑小时向上取整。官方一点说就是“不足1小时按1小时计算”。我在ParkingRecord模型中实现了calculate_fee()方法from decimal import Decimal import math # 放在ParkingRecord里作为方法 def calculate_fee(self): 规则 1. 进场时间起 free_minutes 内免费 2. 首小时按 first_hour_price 计费 3. 超过首小时后按 hourly_price 累加不足1小时按1小时 4. 如果费用超过 daily_cap按 daily_cap 封顶。 适合毕设的单条费率策略正式商业地库可按白天/夜晚分时段扩展。 config FeeConfig.objects.first() if not config or not self.exit_time: return Decimal(0.00) delta self.exit_time - self.entry_time total_minutes delta.total_seconds() / 60.0 if total_minutes config.free_minutes: return Decimal(0.00) # 去掉免费时长后真正要计费的分钟数 billable_minutes total_minutes - config.free_minutes # 如果计费分钟数小于等于60分钟只收首小时费用 if billable_minutes 60: fee config.first_hour_price else: # 超出首小时之后的部分按向上取整计算小时数 extra_minutes billable_minutes - 60 extra_hours math.ceil(extra_minutes / 60) fee config.first_hour_price extra_hours * config.hourly_price # 单次停车封顶 if fee config.daily_cap: fee config.daily_cap return Decimal(fee).quantize(Decimal(0.01))这样算出来的费用会保留两位小数适合数据库里的DecimalField。我在调试时发现如果直接用浮点数存钱会出现0.30000000000000004这种奇怪结果所以金额一律用Decimal。4.4 这里还延伸了几道Django面试常考题很多同学会去搜“django相关面试题目”做完这个系统以后你会发现很多面试题几乎都练过Django中ORM的N1查询问题。如果要列出最近100条停车记录并显示每辆车的车牌号直接用ParkingRecord.objects.all()循环访问record.vehicle.plate_number会产生100多次额外查询。正确做法是select_related(vehicle, slot)。查询今天所有离场车辆的收入。可以用aggregate聚合from django.db.models import Sum, Count from django.db.models.functions import TruncDate stats ( ParkingRecord.objects .filter(statusFINISHED, exit_time__date2025-01-01) .aggregate(total_amountSum(fee_amount), total_countCount(id)) )按天看收入趋势daily_report ( ParkingRecord.objects .filter(statusFINISHED) .annotate(dayTruncDate(exit_time)) .values(day) .annotate(totalSum(fee_amount), countCount(id)) .order_by(-day)[:30] )4.5 删除对象时如何避免把车位一起“带偏”热搜词里有“django执行查询-删除对象”这里我专门讲一下。Django模型默认的delete会按照外键关系级联处理但这颗糖有时很危险。比如在Admin里删除一条“停车中”的停车记录机器默认只会把这条记录删掉车位状态不会跟着更新。于是车位就一直显示占用实际车还在场内但记录已经没了。解决办法是覆写save或者为停车记录增加独立方法并在需要删除时调用自定义处理# parking/models.py def release_slot_and_cancel(self): 取消一条停车记录并释放车位。 该方法不是真正从数据库删记录而是把状态改成已取消。 with transaction.atomic(): if self.status PARKING: self.status CANCELLED self.exit_time timezone.now() self.fee_amount 0 self.save() self.slot.status FREE self.slot.save(update_fields[status])在系统里我鼓励用“状态取消”而不是物理删除历史数据。财务系统动不动就删记录会让审计变成灾难而且论文里也不好讲“完整性”。如果你确实要在Admin后台提供删除功能自定义ModelAdmin的delete方法可以这样写# parking/admin.py from django.contrib import admin from .models import ParkingRecord, ParkingSlot, Vehicle, FeeConfig admin.register(ParkingRecord) class ParkingRecordAdmin(admin.ModelAdmin): list_display (vehicle, slot, entry_time, exit_time, fee_amount, status) list_filter (status, entry_time) def delete_queryset(self, request, queryset): for record in queryset: record.release_slot_and_cancel()通过这种自定义而不是直接queryset.delete()能防止把车位状态搞乱。5. 运维与后台页面让管理系统看起来不像“半成品”5.1 自带Admin的后台可以撑住大部分运营管理需求很多人的毕设误区是觉得Django Admin是“不干活”的默认页面。实际上你只要花一点时间配置Admin的演示效果完全不输手搭后台。我的建议是保留Admin给管理员用前台页面做岗位操作两者职责分开。下面是实际配置过的核心代码# parking/admin.py admin.register(ParkingSlot) class ParkingSlotAdmin(admin.ModelAdmin): list_display (slot_no, area, status) list_filter (area, status) list_editable (status,) search_fields (slot_no,) admin.register(Vehicle) class VehicleAdmin(admin.ModelAdmin): list_display (plate_number, owner, vehicle_type, created_at) search_fields (plate_number,) admin.register(FeeConfig) class FeeConfigAdmin(admin.ModelAdmin): list_display (name, free_minutes, first_hour_price, hourly_price, daily_cap, updated_at)Admin里开启list_editable之后可以直接在列表页把车位状态改成空闲/占用演示入场出场时非常直观。5.2 页面层级设计入口、出口、监控台前台页面我给系统设计了四块首页/监控台用表格或卡片展示总车位数、空闲车位、在场车辆、今日收入入场登记页输入车牌后台自动分配车位出场结算页列出在场车辆支持点击“结算离场”历史记录页支持按车牌、日期筛选这个页面设计不需要多炫但逻辑上让操作人员能傻瓜式工作。做一个后台页面时核心的一点是“操作完要有反馈”。所以我在每个视图都加了messages.success或messages.error用户提交后页面顶部会弹出提示比干巴巴回到一个空白页面体验好很多。举一个快速统计面板的例子# 首页视图 def dashboard(request): today_start timezone.now().replace(hour0, minute0, second0, microsecond0) slot_total ParkingSlot.objects.count() slot_free ParkingSlot.objects.filter(statusFREE).count() car_inside ParkingRecord.objects.filter(statusPARKING).count() today_income ParkingRecord.objects.filter( statusFINISHED, exit_time__gtetoday_start ).aggregate(totalSum(fee_amount))[total] or 0 recent_records ParkingRecord.objects.select_related(vehicle, slot).order_by(-entry_time)[:10] context { slot_total: slot_total, slot_free: slot_free, car_inside: car_inside, today_income: today_income, recent_records: recent_records, } return render(request, parking/dashboard.html, context)强调一下列表查询用了select_related(vehicle, slot)。这里如果在模板里对每条记录都打印record.vehicle.plate_number性能会差很多。答辩演示时数据量小看不出来但面试或论文中都会被问到。5.3 权限控制不是登录了就能干所有事停车场系统有两类使用场景前台入口的“收费员/管理员”录入车辆“月卡用户”查看自己车辆的停车记录先做最简单的登录校验Django提供了装饰器from django.contrib.auth.decorators import login_required login_required def dashboard(request): # ... return render(request, parking/dashboard.html, context)其实更好的做法是做一个自定义装饰器区分普通用户和管理员from functools import wraps from django.shortcuts import redirect def admin_required(view_func): wraps(view_func) def _wrapped_view(request, *args, **kwargs): if not request.user.is_authenticated: return redirect(account:login) if request.user.user_type not in [admin, operator] and not request.user.is_superuser: return redirect(account:no_permission) return view_func(request, *args, **kwargs) return _wrapped_view在视图上直接标admin_required看到这个装饰器的人就知道不是所有登录用户都能入场登记。这是“角色控制”最基本的体现。月卡用户查看自己的记录时要在视图里做一层归属过滤queryset ParkingRecord.objects.filter(vehicle__ownerrequest.user)如果不加这个filter普通用户只要改URL里的id就能看到别的车历史订单这对答辩来说是很掉分的漏洞。6. 答辩前必须处理掉的那些隐患与线上事故6.1 手写当前时间造成的“早八小时”事故我在开发过程中一度手动把“2025-01-01 09:00”塞进测试数据结果Admin里看历史记录时全部变成凌晨1点。排查后发现是自己为了省事用了datetime.datetime(2025, 1, 1, 9)而Django在USE_TZTrue的情况下会自动转成UTC时区显示。正确写法是from django.utils import timezone now timezone.now()如果只是想造一条入场时间用timezone.make_aware(datetime(2025, 1, 1, 9, 0))。6.2 SQLite与MySQL的环境差异开发时我用默认SQLite非常顺滑。后来为了部署到服务器上演示切换到MySQL马上遇到两个问题中文乱码迁移不兼容如果你一开始确定要部署到MySQL建议在settings里写成这样DATABASES { default: { ENGINE: django.db.backends.mysql, NAME: parking_db, USER: parking_user, PASSWORD: your_password, HOST: 127.0.0.1, PORT: 3306, OPTIONS: { charset: utf8mb4, }, } }utf8mb4不只是为了中文还为了万一将来存放emoji或生僻字时不至于报错。所有旧表在新环境里重新migrate就干净了不建议试图从SQLite直接迁移数据文件到MySQL底层兼容性问题太多。6.3 演示时最容易出现的“账号没权限”尴尬答辩演示时你需要准备两类账号超级管理员createsuperuser创建的账号普通月卡用户用来演示“登录后只能看自己的记录”很多同学忘记创建普通用户现场直接用Admin账号演示所有功能结果被老师问“普通用户看到的是什么界面”时一时半会儿切不过去。提前在项目里做一个初始化用户脚本或写死固定的测试账号是种省力办法。你只要保证每次演示前数据库里都有admin用户一些已完成的停车记录且时间分散在最近一个月几辆正在场内的车6.4 清空测试数据时带来的连锁反应有一次我想把所有模拟数据清掉直接调用了Vehicle.objects.all().delete()。因为当时Vehicle外键关联停车记录时用了models.CASCADE字段没有改成PROTECT所以导致所有车辆对应的停车记录、车位的状态全被清掉——车位表的数据倒是保留了但所有正在停车的记录都没了。这个教训让我彻底明白了为什么“业务关键外键要用PROTECT而不是CASCADE”。真正上线时删除操作必须克制。毕业设计阶段如果你觉得数据乱了宁可用flush命令清空整个开发库再重新跑模拟数据也不要粗暴级联删除。6.5 与真实停车场系统的差距以及如何应对答辩如果答辩老师问“你这个系统和商业停车场系统有什么差距”你可以说真实系统常常接入了摄像头车牌识别而本毕设采用人工输入车牌模拟入口操作但车牌识别后端的核心逻辑仍然是精确匹配和记录关联。真实系统有更多收费规则比如白天晚上分段计价、24小时封顶、跨天计算本项目实现了基础计费策略同时把收费配置独立成表方便后续扩展。真实系统会有车位传感器本设计用数据库状态作为“虚拟传感器”从软件层面保证了状态一致。这样说既不会过度吹嘘自己也能说明你明白自己做了什么以及未来怎么做。如果老师进一步追问“车辆识别进来后如果车牌没识别清楚怎么办”你可以说系统允许人工复核修改车牌后再入场核心逻辑是入场记录必须绑定一个明确的Vehicle。如果不能确定车牌就不能创建入场记录避免脏数据。6.6 测试账号与数据生成技巧毕设演示最怕“现场造数据”。我写了一个简单的管理命令用于生成一辆在库车和一批模拟历史记录。部署到演示环境后只需要先跑迁移再执行一次生成命令界面上就有能展示三四十条记录的数据。# parking/management/commands/generate_demo.py from django.core.management.base import BaseCommand from django.utils import timezone from parking.models import ParkingSlot, Vehicle, ParkingRecord from random import randint, choice from datetime import timedelta class Command(BaseCommand): help 生成停车场演示数据 def handle(self, *args, **kwargs): # 先创建10个车位 for i in range(1, 11): ParkingSlot.objects.get_or_create( slot_nofA{i:02d}, defaults{area: A区} ) # 创建3辆在场车辆 for plate in [京A12345, 沪B67890, 粤C11223]: vehicle, _ Vehicle.objects.get_or_create(plate_numberplate) slot ParkingSlot.objects.filter(statusFREE).first() if slot: ParkingRecord.objects.create( vehiclevehicle, slotslot, entry_timetimezone.now() - timedelta(hoursrandint(1, 5)), statusPARKING ) slot.status OCCUPIED slot.save(update_fields[status]) self.stdout.write(self.style.SUCCESS(演示数据生成完成))这个管理命令文件位置是app/management/commands/generate_demo.py注意必须在parking目录下手动创建management/commands包并且每个目录都要有__init__.py不然后续命令找不到。最后再分享一个个人经验毕设项目不要一味堆功能真正重要的是把核心链路做完整再把异常情况想清楚。停车场系统在管理类题目里看起来不起眼但“事务”“并发锁”“状态机”这些关键词一旦你真正实践过不管是写论文还是面试被问到都能讲出几分底气。答辩时我听到的最有价值的问题就是“你这个停车位如果同时在两台电脑上被入场会怎样”那一刻我知道提前研究select_for_update的功夫没有白费。如果你也准备用Django做一个具有完整业务链路的毕设建议从入场、离场、计费这条主线入手。先把模型字段定稳再去补页面最后补权限和统计。你在写代码过程中遇到的每个状态错乱和异常记录都会变成答辩时最好的素材。