Django汽车租赁系统开发实战:从数据建模到部署避坑 简介面向毕业设计场景的汽车租赁管理网站完整源码包基于Python与Django构建后端配合Vue实现管理界面覆盖管理员与用户双角色业务上打通汽车信息管理、租赁归还流程、商品购物车及订单处理等模块从用户注册登录到租车下单再到后台订单管理形成完整闭环。包内共744个文件其中162个SVG图标与162个JavaScript文件支撑前端交互45个Python源码与44个Vue组件构成主要业务逻辑附带MySQL数据库脚本、运行脚本及部署说明压缩包约12.24MB。安装与启动批处理文件让本地环境搭建更便捷适合计算机专业学生参考课程设计或毕业设计。目前已有120人学习下载对需要理解DjangoMySQL前后端分离架构的开发者有较好参考价值。1. 为什么汽车租赁系统用 Django 而不是 Flask 或 SSM一个毕业生把“汽车租赁管理网站”做成 Python Django而不是国内更常见的 SSM 组合通常不是因为赶时髦而是因为租赁业务的状态流转比普通 CRUD 更依赖“事务一致性”用户提交租赁单、管理员确认、车辆从“可租”变成“租赁中”、归还后重新置为“可租”这一串动作只要中间断一步车就有可能被重复租出去。Django 的 ORM 默认包裹事务、admin 后台几乎零代码生成、自带用户体系和权限框架能把这类业务的后台管理部分从“写接口”变成“配页面”所以拿来当毕设或企业内部系统骨架非常合适。这篇文章以一个完整的汽车租赁管理网站为例拆开讲它的数据模型、管理员端功能、用户端租车与下单流程以及本地启动和部署时的坑。代码基于 Python 3 Django数据库用 MySQL。适合正在做 Django 课程设计、想快速理解租赁业务闭环、或者打算把毕设改造成作品集的人。2. 数据建模用户、车辆、租赁单与订单的关系设计2.1 从业务需求反推业务表这套系统的核心角色只有两个管理员和用户但业务对象却有六类用户、汽车品牌、汽车信息、汽车租赁、汽车归还、汽车商品、商品类型、订单、购物车。穿起来看租赁和购买是两条独立链路但共用同一套用户表。先看租赁链路用户租车生成一条租赁记录记录里要关联用户和车辆车辆归还要单独记录因为归还时可能产生逾期费用或车辆损坏描述不能直接写在租赁记录里覆盖原状态。购买链路则是用户把汽车商品加入购物车结算时生成订单订单里要有商品快照和总价。用 Django 的models.py组织这些关系常见做法是拆成 app 或全放一个models.py。对毕设规模来说一个文件足够但建议按业务分模块写# models.py from django.db import models from django.contrib.auth.models import AbstractUser class User(AbstractUser): phone models.CharField(max_length11, blankTrue) address models.CharField(max_length200, blankTrue) # 继承 Django 自带的用户模型自带的 is_staff 可以作为管理员标记 class CarBrand(models.Model): name models.CharField(max_length50, uniqueTrue) def __str__(self): return self.name class CarInfo(models.Model): brand models.ForeignKey(CarBrand, on_deletemodels.CASCADE) name models.CharField(max_length100) plate_number models.CharField(max_length20, uniqueTrue) daily_rent models.DecimalField(max_digits8, decimal_places2) status_choices ((available, 可租), (rented, 已租), (maintain, 维修)) status models.CharField(max_length10, choicesstatus_choices, defaultavailable) image models.ImageField(upload_tocars/, blankTrue) description models.TextField(blankTrue)这段代码把车辆状态设计成字符串枚举而非布尔值原因是租赁业务里除了“可租/已租”还需要“维修中”这种停滞状态。如果用布尔字段后续扩展状态会面临迁移成本。plate_number加唯一约束防止同一辆车被录入两次。租赁记录和归还记录则需要把时间、金额、关联对象都记下来class RentalRecord(models.Model): user models.ForeignKey(User, on_deletemodels.CASCADE) car models.ForeignKey(CarInfo, on_deletemodels.CASCADE) start_date models.DateField() end_date models.DateField() total_amount models.DecimalField(max_digits10, decimal_places2) status_choices ((pending, 待确认), (confirmed, 已确认), (returned, 已归还), (cancelled, 已取消)) status models.CharField(max_length10, choicesstatus_choices, defaultpending) create_time models.DateTimeField(auto_now_addTrue) class ReturnRecord(models.Model): rental models.OneToOneField(RentalRecord, on_deletemodels.CASCADE) return_date models.DateField() actual_days models.IntegerField() overdue_days models.IntegerField(default0) overdue_fee models.DecimalField(max_digits8, decimal_places2, default0) note models.TextField(blankTrue)ReturnRecord用OneToOneField关联租赁单因为一次租赁只对应一次归还。actual_days和overdue_days虽然能通过日期计算但单独存字段的好处是一旦管理员手工调整了归还日期历史数据不会被公式改变后续对账时才说得清逾期费是哪里来的。2.2 购物车与订单的冗余设计汽车商品这块购物车表属于典型的“临时数据”可以随时清空重建订单表则必须快照商品信息不能只存外键。原因很直接商品价格或名称后续改变老订单里的金额也会被拖走这对财务数据是灾难。class Cart(models.Model): user models.ForeignKey(User, on_deletemodels.CASCADE) product models.ForeignKey(CarProduct, on_deletemodels.CASCADE) count models.PositiveIntegerField(default1) class Meta: unique_together (user, product) class Order(models.Model): order_no models.CharField(max_length32, uniqueTrue) user models.ForeignKey(User, on_deletemodels.CASCADE) product_name models.CharField(max_length100) # 冗余商品名 product_price models.DecimalField(max_digits8, decimal_places2) # 冗余单价 count models.PositiveIntegerField() total_price models.DecimalField(max_digits10, decimal_places2) status_choices ((unpaid, 待付款), (paid, 已付款), (shipped, 已发货), (done, 已完成)) status models.CharField(max_length10, choicesstatus_choices, defaultunpaid) create_time models.DateTimeField(auto_now_addTrue)unique_together保证同一用户对同一商品只会有一行购物车数据重复加入只是count 1。订单编号用order_no而不是自增 id目的是对外隐藏业务量也方便后续接入支付回调——支付平台一般要求商户订单号唯一且不超过 32 位我用time.strftime(%Y%m%d%H%M%S) str(user_id)拼再加随机数防并发撞号。2.3 为什么不直接用 Django 自带的 Group 做管理员Django admin 自带用户模型有is_staff和is_superuser完全可以直接区分管理员和普通用户不需要另建 admin 表。在settings.py里把AUTH_USER_MODEL指向自定义User再通过user.is_staff判定角色。这套系统里的“管理员功能”其实走的就是 Django admin 自动生成的界面但业务上很多操作不能直接在 admin 列表页完成比如“确认租赁”要同时把车辆状态改成已租。所以数据模型里专门留了状态字段后面用 admin action 或自定义视图去处理联动更新。提示自定义用户模型后第一次python manage.py makemigrations之前必须设置好AUTH_USER_MODEL一旦已经有迁移记录再改会很麻烦。3. 管理员端功能实现品牌管理、车辆上下架与归还处理3.1 用 Django admin 快速搭出管理后台这套系统的“管理员功能有个人中心、用户管理、汽车品牌管理、汽车信息管理、汽车租赁管理、汽车归还管理、商品类型管理、汽车商品管理、系统管理、订单管理”如果全部手写视图和模板工作量会翻好几倍。Django 的方案是先把模型注册进 admin再用自定义ModelAdmin控制列表展示和操作。# admin.py from django.contrib import admin from .models import CarBrand, CarInfo, RentalRecord, ReturnRecord, Order admin.register(CarBrand) class CarBrandAdmin(admin.ModelAdmin): list_display (id, name) search_fields (name,) admin.register(CarInfo) class CarInfoAdmin(admin.ModelAdmin): list_display (id, name, brand, plate_number, daily_rent, status) list_filter (brand, status) search_fields (name, plate_number) list_editable (status,)list_editable可以直接在列表页下拉修改车辆状态不用进入详情页适合管理员快速上下架车辆。list_filter按品牌和状态过滤车辆数量多的时候非常方便。3.2 租赁确认的联动事务管理员确认一条租赁单时不能只改RentalRecord.status还必须把对应CarInfo.status改成rented。这两个操作必须在一个事务里完成否则会出现“租赁单已确认但车辆仍可租”的脏数据。Django 的transaction.atomic()提供事务块或者通过重写 admin 的save_model方法实现# admin.py from django.db import transaction from django.contrib import admin from .models import RentalRecord, CarInfo admin.register(RentalRecord) class RentalRecordAdmin(admin.ModelAdmin): list_display (id, user, car, start_date, end_date, total_amount, status) actions [confirm_rental] admin.action(description确认选中的租赁单) def confirm_rental(self, request, queryset): with transaction.atomic(): for record in queryset: if record.status ! pending: continue # 只处理待确认的单子 car record.car if car.status ! available: self.message_user(request, f车辆 {car.plate_number} 不是可租状态跳过, levelwarning) continue record.status confirmed record.save() car.status rented car.save(update_fields[status]) self.message_user(request, f已确认 {queryset.count()} 条租赁单)这段代码是典型的 admin action 写法。事务包裹了状态修改遇到车辆不可租就跳过而不是直接报错避免一条脏数据中断整批操作。update_fields只更新status字段减少 SQL 更新范围。3.3 归还处理计算逾期费并回滚车辆状态归还流程比确认租赁更复杂核心逻辑是根据实际归还日期计算租赁天数对比计划结束日期算出逾期天数再按日租金的某个比例算逾期费。业务上通常要求“先登记归还记录再改租赁单状态为已归还最后把车辆状态改回可租”。# 在视图或 service 函数中 from datetime import date from django.db import transaction def process_return(rental, actual_return_date): with transaction.atomic(): plan_end rental.end_date actual_days (actual_return_date - rental.start_date).days overdue_days max(0, (actual_return_date - plan_end).days) overdue_fee overdue_days * rental.car.daily_rent * 0.5 # 逾期日租金50%作为罚款 ReturnRecord.objects.create( rentalrental, return_dateactual_return_date, actual_daysactual_days, overdue_daysoverdue_days, overdue_feeoverdue_fee, note逾期归还 if overdue_days 0 else ) rental.status returned rental.save(update_fields[status]) rental.car.status available rental.car.save(update_fields[status])这里有两个容易踩坑的地方。第一actual_days如果用(actual_return_date - start_date).days会把还车当天也算进去如果业务规定“当天还不算钱”就得days 1或按小时计费。第二逾期费率是写死的 0.5如果用在真实项目里建议把这个比例做成系统配置项而不是藏在代码常量中。提示Django admin 的actions可以直接跑这类批量业务但如果需要弹窗输入“实际归还日期”就得搭配django.contrib.admin的SimpleListFilter或自定义视图否则只能默认取当天日期。4. 用户端实战选车、租赁、购物车与订单生成4.1 用户注册登录与权限控制用户端不走 Django admin需要自己写视图和模板。注册登录用 Django 内置的authenticate和login密码加密由框架处理不需要手动加盐。权限控制通过login_required装饰器防止未登录用户直接访问租赁或购物车接口。# views.py from django.contrib.auth import authenticate, login from django.contrib.auth.decorators import login_required from django.shortcuts import render, redirect def login_view(request): if request.method POST: username request.POST.get(username) password request.POST.get(password) user authenticate(request, usernameusername, passwordpassword) if user: login(request, user) return redirect(car_list) else: return render(request, login.html, {error: 用户名或密码错误}) return render(request, login.html) login_required def car_list(request): cars CarInfo.objects.filter(statusavailable) return render(request, car_list.html, {cars: cars})login_view里没有区分用户还是管理员因为登录成功后request.user.is_staff可以决定跳转目标。普通用户跳转列表页管理员跳转/admin/。注意authenticate需要配合 Django 的ModelBackend如果你的AUTH_USER_MODEL自定义后没设置 backend默认也能用但不建议重写save_model里对密码明文赋值——要set_password才行。4.2 租赁下单时校验车辆可用性用户提交租赁表单时服务端必须再次校验车辆状态不能信任前端传的car_id。常见攻击是对car_id取已租车辆导致重复下单。校验逻辑放在RentalRecord.objects.create之前# views.py from django.shortcuts import get_object_or_404 from django.utils import timezone from .models import CarInfo, RentalRecord login_required def rent_car(request, car_id): car get_object_or_404(CarInfo, pkcar_id) if car.status ! available: return render(request, error.html, {msg: 该车辆已被租出或维修中}) start_date request.POST.get(start_date) end_date request.POST.get(end_date) # 日期合法性校验 if start_date end_date: return render(request, error.html, {msg: 结束日期必须晚于开始日期}) days (end_date - start_date).days total days * car.daily_rent RentalRecord.objects.create( userrequest.user, carcar, start_datestart_date, end_dateend_date, total_amounttotal, statuspending, ) # 注意这里不立刻改车辆状态等管理员确认后才改 return redirect(rental_success)这里刻意不在提交后立刻把车改成“已租”而是维持“可租”直到管理员确认。这样设计符合实际线下业务用户提交意愿不等于租车成功管理员可能因为押金未付或证照不符而拒绝拒绝后状态回到“可租”不需要复杂的反向操作。代价是存在多用户同时提交同一辆车的可能但从毕设和中小系统角度看这种程度的一致性足够不必上分布式锁。4.3 购物车与订单生成的事务性操作购物车是用户端的典型复合操作后续加购、结算涉及多表联动。结算时要把购物车里的每一条商品读出来逐条写订单记录然后清空购物车。这三个动作必须在一个事务中执行否则会出现“订单已经生成但购物车里东西还在”或“购物车清了但订单缺失”的情况。# views.py from django.db import transaction from django.http import JsonResponse from .models import Cart, Order, CarProduct import time, random login_required def checkout(request): if request.method ! POST: return JsonResponse({code: 400, msg: 请求方式错误}) cart_items Cart.objects.filter(userrequest.user).select_related(product) if not cart_items.exists(): return JsonResponse({code: 400, msg: 购物车为空}) order_list [] with transaction.atomic(): for item in cart_items: product item.product order_no time.strftime(%Y%m%d%H%M%S) str(random.randint(1000, 9999)) order_list.append(Order( order_noorder_no, userrequest.user, product_nameproduct.name, product_priceproduct.price, countitem.count, total_priceproduct.price * item.count, statusunpaid, )) Order.objects.bulk_create(order_list) # 批量创建减少数据库往返 cart_items.delete() # 清空购物车 return JsonResponse({code: 200, msg: 下单成功, orders: [o.order_no for o in order_list]})select_related是 Django ORM 里必须养成的习惯避免每次item.product都发一条 SQL。bulk_create在数据量大时性能提升明显毕设数据量小看不出差距但代码风格上是对的。清空购物车用cart_items.delete()这条 QuerySet 在事务里会直接翻译成一条DELETE WHERE语句不用循环删。4.4 订单状态的更新方式订单从“待付款”到“已付款”再到“已完成”最简单的做法是在模板里放一个“模拟支付”按钮用户点击后调一个视图更新状态。真实项目里这里是接入微信或支付宝回调回调里验签后更新订单。login_required def pay_order(request, order_no): order get_object_or_404(Order, order_noorder_no, userrequest.user) if order.status ! unpaid: return JsonResponse({code: 400, msg: 订单状态不可支付}) order.status paid order.save(update_fields[status]) return JsonResponse({code: 200, msg: 支付成功})注意这里查询条件里带了userrequest.user否则用户可以拿着别人订单号去改状态。虽然这种系统没有支付回调但作为毕设代码里把“越权访问”堵上答辩时能解释清楚就是加分项。5. 运行与部署bat 脚本启动、MySQL 配置与常见坑5.1 配套的 bat 脚本是干什么用的项目自带的安装.bat、2-run.bat、3-build.bat等文件看起来像是前后端分离项目里同时存在 Vue 和 Django 的产物。从文件名推断2-run.bat是启动后端 Django 服务3-build.bat是构建前端 Vue 项目运行.bat可能是直接跑或一键启动。在 Windows 上做毕设演示这类脚本的核心目的只有两个字省事。一段典型的2-run.bat内容大概是echo off cd /d %~dp0 python manage.py runserver 0.0.0.0:8000%~dp0表示当前 bat 文件所在目录这样无论从哪个路径双击都能切到项目根目录。0.0.0.0可以允许局域网内其他电脑通过 IP 访问演示时方便拿手机或另一台电脑看效果。如果后端是 Django前端是 Vue则3-build.bat一般执行echo off cd /d %~dp0 npm install npm run build安装.bat通常是先装 Python 依赖再装前端依赖echo off pip install -r requirements.txt cd frontend npm install注意bat 脚本里不要用pause结尾再跑服务否则关掉弹窗会把 Python 进程一起杀掉。生产演示时建议用python manage.py runserver后另开窗口而不是依赖 bat 阻塞式运行。5.2 MySQL 配置与初始化Django 默认用 SQLite但这个项目明确用 MySQL需要在settings.py里改数据库配置。关键点是安装mysqlclient或pymysql前者编译依赖多后者纯 Python 但要在__init__.py里 patch。# settings.py DATABASES { default: { ENGINE: django.db.backends.mysql, NAME: car_rental, USER: root, PASSWORD: your_password, HOST: 127.0.0.1, PORT: 3306, OPTIONS: {charset: utf8mb4}, } }utf8mb4是必须的否则用户输入 emoji 时utf8字符集会报Incorrect string value错误。MySQL 字符集默认可能是utf8mb4_general_ci排序规则影响不大但建库时统一用utf8mb4最稳。初始化步骤mysql -uroot -p CREATE DATABASE car_rental CHARACTER SET utf8mb4; python manage.py makemigrations python manage.py migrate python manage.py createsuperuser python manage.py runserver如果makemigrations检测不到模型变化先检查app是否注册进INSTALLED_APPS。如果是单文件项目不需要启动单独 app。5.3 常见报错与排查方法第一类报错是ModuleNotFoundError: No module named MySQLdb说明没装mysqlclient。Windows 上装mysqlclient经常卡编译建议直接pip install mysqlclient新版自带 whl或者用 pip 源替换清华源。第二类是django.db.utils.OperationalError: (1045, Access denied for user)原因是 MySQL 用户名密码不对或没有远端访问权限。本地开发直接确认USER是 root密码别用特殊字符如因为settings.py里的字符串不需要转义但某些 bat 脚本拼接时会出现引号问题。第三类是时区问题。settings.py中USE_TZ True时DateTimeField返回的是UTC时间如果你在模板里直接用{{ record.create_time }}看到的可能比北京时间早 8 小时。解决方法是设置TIME_ZONE Asia/Shanghai且USE_TZ False或者保留USE_TZ True在模板里用{{ record.create_time|localtime }}过滤。5.4 让演示更顺利的小技巧答辩演示时最容易翻车的是数据库连不上和静态文件加载失败。Django 的 admin 自带 CSS不用额外处理但如果你写的前端页面引用了本地图片、CSS必须在settings.py配置MEDIA_URL /media/ MEDIA_ROOT os.path.join(BASE_DIR, media) STATIC_URL /static/ STATICFILES_DIRS [os.path.join(BASE_DIR, static)]开发环境运行时还要在urls.py里加serve否则图片永远 404from django.conf.urls.static import static from django.conf import settings urlpatterns static(settings.MEDIA_URL, document_rootsettings.MEDIA_ROOT)一键启动脚本里如果直接执行runserver端口被占用会报错可以先netstat -ano | findstr :8000查端口然后taskkill /F /PID 进程号清理旧进程再重新跑。bat 脚本里加一句taskkill /F /IM python.exe虽然简单粗暴但会把所有 Python 进程杀掉容易误伤其他项目不建议。最后提一个数据层面的验证技巧租赁和归还流程走完后打开 admin 看三张表——RentalRecord的状态是returned、ReturnRecord存在对应记录、CarInfo.status是available。三者状态一致说明事务联动没有漏。如果哪一步断了优先检查是不是用了queryset.update()而绕过 Django 信号或自定义save_model逻辑。本文还有配套的精品资源点击获取