Django汽车租赁系统开发实战:从数据建模到部署避坑
2026/9/14 4:04:06 网站建设 项目流程

简介:面向毕业设计场景的汽车租赁管理网站完整源码包,基于Python与Django构建后端,配合Vue实现管理界面,覆盖管理员与用户双角色,业务上打通汽车信息管理、租赁归还流程、商品购物车及订单处理等模块,从用户注册登录到租车下单,再到后台订单管理,形成完整闭环。包内共744个文件,其中162个SVG图标与162个JavaScript文件支撑前端交互,45个Python源码与44个Vue组件构成主要业务逻辑,附带MySQL数据库脚本、运行脚本及部署说明,压缩包约12.24MB。安装与启动批处理文件让本地环境搭建更便捷,适合计算机专业学生参考课程设计或毕业设计。目前已有120人学习下载,对需要理解Django+MySQL+前后端分离架构的开发者有较好参考价值。

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_length=11, blank=True) address = models.CharField(max_length=200, blank=True) # 继承 Django 自带的用户模型,自带的 is_staff 可以作为管理员标记 class CarBrand(models.Model): name = models.CharField(max_length=50, unique=True) def __str__(self): return self.name class CarInfo(models.Model): brand = models.ForeignKey(CarBrand, on_delete=models.CASCADE) name = models.CharField(max_length=100) plate_number = models.CharField(max_length=20, unique=True) daily_rent = models.DecimalField(max_digits=8, decimal_places=2) status_choices = (('available', '可租'), ('rented', '已租'), ('maintain', '维修')) status = models.CharField(max_length=10, choices=status_choices, default='available') image = models.ImageField(upload_to='cars/', blank=True) description = models.TextField(blank=True)

这段代码把车辆状态设计成字符串枚举而非布尔值,原因是租赁业务里除了“可租/已租”,还需要“维修中”这种停滞状态。如果用布尔字段,后续扩展状态会面临迁移成本。plate_number加唯一约束,防止同一辆车被录入两次。

租赁记录和归还记录则需要把时间、金额、关联对象都记下来:

class RentalRecord(models.Model): user = models.ForeignKey(User, on_delete=models.CASCADE) car = models.ForeignKey(CarInfo, on_delete=models.CASCADE) start_date = models.DateField() end_date = models.DateField() total_amount = models.DecimalField(max_digits=10, decimal_places=2) status_choices = (('pending', '待确认'), ('confirmed', '已确认'), ('returned', '已归还'), ('cancelled', '已取消')) status = models.CharField(max_length=10, choices=status_choices, default='pending') create_time = models.DateTimeField(auto_now_add=True) class ReturnRecord(models.Model): rental = models.OneToOneField(RentalRecord, on_delete=models.CASCADE) return_date = models.DateField() actual_days = models.IntegerField() overdue_days = models.IntegerField(default=0) overdue_fee = models.DecimalField(max_digits=8, decimal_places=2, default=0) note = models.TextField(blank=True)

ReturnRecordOneToOneField关联租赁单,因为一次租赁只对应一次归还。actual_daysoverdue_days虽然能通过日期计算,但单独存字段的好处是:一旦管理员手工调整了归还日期,历史数据不会被公式改变,后续对账时才说得清逾期费是哪里来的。

2.2 购物车与订单的冗余设计

汽车商品这块,购物车表属于典型的“临时数据”,可以随时清空重建;订单表则必须快照商品信息,不能只存外键。原因很直接:商品价格或名称后续改变,老订单里的金额也会被拖走,这对财务数据是灾难。

class Cart(models.Model): user = models.ForeignKey(User, on_delete=models.CASCADE) product = models.ForeignKey('CarProduct', on_delete=models.CASCADE) count = models.PositiveIntegerField(default=1) class Meta: unique_together = ('user', 'product') class Order(models.Model): order_no = models.CharField(max_length=32, unique=True) user = models.ForeignKey(User, on_delete=models.CASCADE) product_name = models.CharField(max_length=100) # 冗余商品名 product_price = models.DecimalField(max_digits=8, decimal_places=2) # 冗余单价 count = models.PositiveIntegerField() total_price = models.DecimalField(max_digits=10, decimal_places=2) status_choices = (('unpaid', '待付款'), ('paid', '已付款'), ('shipped', '已发货'), ('done', '已完成')) status = models.CharField(max_length=10, choices=status_choices, default='unpaid') create_time = models.DateTimeField(auto_now_add=True)

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_staffis_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} 不是可租状态,跳过', level='warning') 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( rental=rental, return_date=actual_return_date, actual_days=actual_days, overdue_days=overdue_days, overdue_fee=overdue_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.adminSimpleListFilter或自定义视图,否则只能默认取当天日期。

4. 用户端实战:选车、租赁、购物车与订单生成

4.1 用户注册登录与权限控制

用户端不走 Django admin,需要自己写视图和模板。注册登录用 Django 内置的authenticatelogin,密码加密由框架处理,不需要手动加盐。权限控制通过@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, username=username, password=password) 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(status='available') 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, pk=car_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( user=request.user, car=car, start_date=start_date, end_date=end_date, total_amount=total, status='pending', ) # 注意:这里不立刻改车辆状态,等管理员确认后才改 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(user=request.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_no=order_no, user=request.user, product_name=product.name, product_price=product.price, count=item.count, total_price=product.price * item.count, status='unpaid', )) 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_no=order_no, user=request.user) if order.status != 'unpaid': return JsonResponse({'code': 400, 'msg': '订单状态不可支付'}) order.status = 'paid' order.save(update_fields=['status']) return JsonResponse({'code': 200, 'msg': '支付成功'})

注意这里查询条件里带了user=request.user,否则用户可以拿着别人订单号去改状态。虽然这种系统没有支付回调,但作为毕设,代码里把“越权访问”堵上,答辩时能解释清楚,就是加分项。

5. 运行与部署:bat 脚本启动、MySQL 配置与常见坑

5.1 配套的 bat 脚本是干什么用的

项目自带的安装.bat2-run.bat3-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里改数据库配置。关键点是安装mysqlclientpymysql,前者编译依赖多,后者纯 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.pyUSE_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,否则图片永远 404:

from django.conf.urls.static import static from django.conf import settings urlpatterns += static(settings.MEDIA_URL, document_root=settings.MEDIA_ROOT)

一键启动脚本里如果直接执行runserver,端口被占用会报错,可以先netstat -ano | findstr :8000查端口,然后taskkill /F /PID 进程号清理旧进程,再重新跑。bat 脚本里加一句taskkill /F /IM python.exe虽然简单粗暴,但会把所有 Python 进程杀掉,容易误伤其他项目,不建议。

最后提一个数据层面的验证技巧:租赁和归还流程走完后,打开 admin 看三张表——RentalRecord的状态是returnedReturnRecord存在对应记录、CarInfo.statusavailable。三者状态一致,说明事务联动没有漏。如果哪一步断了,优先检查是不是用了queryset.update()而绕过 Django 信号或自定义save_model逻辑。

本文还有配套的精品资源,点击获取

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询