多彩编程 多彩编程MZPH · CODE BLOG
ARTICLE DETAIL

文章详情

深耕前端与后端开发技术的一线实战笔记与踩坑复盘。

Django网上租车系统设计揭秘:ORM、订单状态机与远程调试实战

Django网上租车系统设计揭秘:ORM、订单状态机与远程调试实战 如果你现在正在为毕设选题发愁或者手里已经拿到了一套“基于 Django 的网上租车系统”源码和文档我建议你先沉住气把里面到底有什么东西真正吃透。这类系统乍看只是“一个租车网站”但只要拆开看用户、车辆、订单、后台管理、状态流转这些模块之间的关联关系才是整套源码最值钱的部分。我这段时间帮人调试过好几套 Django 毕设项目租车系统的业务复杂度刚好卡在一个非常合适的点比博客系统有内容比电商系统简单又能把 Django 的 ORM、Admin、表单、权限认证这些东西全部用上。这篇文章我就按实际动手的顺序把网上租车系统的设计与实现拆开讲清楚中间会穿插大量调试心得尤其是远程调试和二次定制这两个最容易出问题的环节。1. 为什么网上租车系统适合拿来做 Django 毕设1.1 它解决的问题足够具体很多毕设选题的问题在于“大而空”。比如“校园信息管理系统”“企业办公平台”一听就知道功能边界模糊做到最后容易变成拼凑页面。网上租车系统不一样它的业务场景非常清晰用户想用车租车公司有车双方通过网站完成选车、下单、取车、还车、结算这一系列动作。一套合格的网上租车系统至少要覆盖三类角色的需求普通用户注册登录、浏览车辆、按条件筛选、查看车辆详情、提交租车订单、查看订单状态、取消订单。后台管理员管理车辆信息、管理车辆分类、审核和处理订单、查看用户列表。系统本身的约束同一辆车不能同时被预租、订单金额要能自动计算、超时未支付订单要能取消。这套需求里的每个点都能对应到 Django 的某个核心能力例如用户注册登录对应自带的认证系统车辆列表对应 ORM 查询和模板渲染订单提交对应表单校验和事务处理后台管理直接使用 Django Admin 就能完成。学生在答辩时能讲清楚“我为什么要这样设计”老师也能一眼看懂系统的业务逻辑。1.2 功能范围应该收敛到什么程度我见过不少同学拿到源码后第一反应是“功能太少了”想拼命往上加东西比如优惠券、积分、消息通知、在线支付。这个想法本身没问题但放在毕设场景里要克制。原因很简单功能越多出问题的概率越大答辩时被追问的深度也越深。一套合适的毕设版网上租车系统主线功能控制在 6 到 8 个模块就足够了用户注册与登录可结合 Django 自带认证改造车辆分类展示与关键词搜索车辆详情页与租车价格展示在线下单填写取车日期和还车日期我的订单用户查看订单位置和状态管理员后台的车辆管理管理员后台的订单管理基础的数据统计或最近订单展示支付部分我建议做“模拟支付”也就是把订单状态从“待支付”改成“已支付”不需要真正接入支付宝或微信支付。原因很实际申请商户号、配置回调、处理异步通知这些环节会消耗大量时间而且本地测试环境网络条件很难完全模拟线上场景。先把链路跑通代码里预留扩展接口答辩时告诉老师“支付模块可以用策略模式继续扩展”效果比硬接一个支付网关好得多。1.3 用 Django 而不是其他框架的真实理由这里我不唱高调直接说实际原因。用 Django 做租车系统收益最大的三个点分别是自带 Admin 后台、自带 ORM 和迁移机制、自带用户认证体系。如果一个系统用 Flask 或 FastAPI 从零写需要自己建用户表、自己设计登录逻辑、自己写后台管理页面。这些工作不是不能做但每一项都是在消耗宝贵的开发时间。而 Django 从startproject开始auth应用就已经存在迁移命令一行搞定Admin 页面直接注册模型就能生成增删改查界面。对于毕设项目来说这些内置能力能省掉大约三分之一的工作量。同时Django 的 ORM 对初学者非常友好查询车辆列表只需要一行Car.objects.filter(status1)之间的关系通过外键就能表达清楚。再加上模板语法简单后端渲染页面时不需要单独搭建前端工程。整体技术栈简单调试方便也更容易在答辩时讲明白。2. 数据建模把租车业务翻译成数据库表2.1 核心模型拆解租车系统的数据模型是整个项目的地基。我这次调试的源码里核心模型主要分成四个车辆分类Category、车辆信息Car、订单Order、用户User。用户模型直接基于 Django 的AbstractUser扩展不用自己从头写认证逻辑。扩展字段时可以加上手机号、驾驶证号、头像等但要注意一点不要修改username和password这两个字段的默认行为否则登录逻辑会变得非常复杂。车辆分类模型比较简单只有名称和创建时间。真正有设计含量的是车辆信息表字段大致如下class Category(models.Model): name models.CharField(max_length50, verbose_name分类名称) created_at models.DateTimeField(auto_now_addTrue) class Car(models.Model): STATUS_CHOICES ( (1, 可租), (2, 已租出), (3, 维修中), (4, 已下架), ) category models.ForeignKey(Category, on_deletemodels.SET_NULL, nullTrue, related_namecars) plate_no models.CharField(max_length20, uniqueTrue) brand models.CharField(max_length50) model_name models.CharField(max_length50) seats models.PositiveSmallIntegerField(default5) fuel_type models.CharField(max_length20) gearbox models.CharField(max_length20, help_text手动/自动) daily_rent models.DecimalField(max_digits10, decimal_places2) deposit models.DecimalField(max_digits10, decimal_places2) status models.PositiveSmallIntegerField(choicesSTATUS_CHOICES, default1) image models.ImageField(upload_tocars/, blankTrue, nullTrue) description models.TextField(blankTrue) created_at models.DateTimeField(auto_now_addTrue) def __str__(self): return f{self.brand} {self.model_name} ({self.plate_no})这里有几个细节值得展开说。image字段用upload_to指定上传目录在本地调试时媒体文件会保存到MEDIA_ROOT目录下需要在settings.py里配置MEDIA_URL和MEDIA_ROOT。另外图片字段不是必须的如果你手上没有足够的车辆图片可以用占位图或者在模板里做样式兜底不要因为这个卡主流程。2.2 订单模型是最容易设计错的一张表订单表比车辆表复杂得多因为它既要关联用户又要关联车辆还要记录租期、费用和状态。下面是我认为比较合理的订单模型结构class Order(models.Model): STATUS_CHOICES ( (1, 待支付), (2, 待取车), (3, 租赁中), (4, 待还车), (5, 已完成), (6, 已取消), ) order_no models.CharField(max_length32, uniqueTrue) user models.ForeignKey(settings.AUTH_USER_MODEL, on_deletemodels.PROTECT, related_nameorders) car models.ForeignKey(Car, on_deletemodels.PROTECT, related_nameorders) start_date models.DateField() end_date models.DateField() total_amount models.DecimalField(max_digits10, decimal_places2) deposit models.DecimalField(max_digits10, decimal_places2) status models.PositiveSmallIntegerField(choicesSTATUS_CHOICES, default1) created_at models.DateTimeField(auto_now_addTrue) updated_at models.DateTimeField(auto_nowTrue) def save(self, *args, **kwargs): if not self.order_no: self.order_no datetime.now().strftime(%Y%m%d%H%M%S) str(random.randint(1000, 9999)) super().save(*args, **kwargs)订单金额的计算逻辑一般写在视图或 form 的校验阶段不建议在save()里写复杂业务计算因为模型层一旦耦合业务后续想改价格规则就会很痛苦。我的做法是视图层负责计算模型层只负责存储和基础默认值。还有一个容易忽略的问题是on_delete的取值。用户和车辆都用PROTECT意思是如果这个用户或车辆已经被订单引用就不能被直接删除。这样设计虽然会让管理员删数据的时候遇到阻碍但能防止出现“订单还在车辆已经没了”的脏数据。对于毕设演示来说PROTECT带来的提示反而是一个可以讲的亮点。2.3 状态字段用整型而不是字符串不少初学者喜欢把状态字段写成字符串比如status models.CharField(max_length20)值直接填已支付。这种写法最大的问题是数据冗余严重一个状态可能有多种写法比如“已支付”“已付款”“支付成功”后台统计时容易出错。我强烈建议状态字段一律用整数加上choices。比如订单状态就是 1 到 6 的数字车辆状态就是 1 到 4 的数字。模板里展示的时候再用过滤器映射成中文或者在get_status_display()方法里直接获取中文描述。这样做的好处有三个数据整洁、查询效率高、代码可读性强。同样时间字段要注意auto_now_add和auto_now的区别。auto_now_add只在创建时写入一次适合created_atauto_now每次保存都会更新适合updated_at。这两个属性非常容易搞混如果写反了会产生“数据变了一次结果创建时间跟着变”的诡异问题。3. 租车主流程的实现细节3.1 浏览器到订单的完整链路一个用户从进入网站到完成下单中间经过的步骤大致是访问车辆列表页点击某一辆车进入详情页在详情页选择取车和还车日期点击“立即预订”填写订单表单确认金额后提交订单。这个流程里有两个核心环节最值得认真实现。第一个是车辆列表的筛选。好的列表页应该有分类筛选、关键词搜索和分页。用 Django 自带的ListView就能做得比较干净from django.views.generic import ListView class CarListView(ListView): model Car template_name car/list.html context_object_name cars paginate_by 9 def get_queryset(self): queryset Car.objects.filter(status1).select_related(category) keyword self.request.GET.get(keyword, ).strip() category_id self.request.GET.get(category) if keyword: queryset queryset.filter(brand__icontainskeyword) if category_id: queryset queryset.filter(category_idcategory_id) return queryset.order_by(-created_at)我特意在代码里加了select_related(category)这是个非常细节的优化。车辆列表页如果一次展示 9 辆车每辆车都要显示分类名称正常情况下会触发 9 次分类查询。加上select_related之后Django 会用一次 JOIN 把分类信息带出来查询次数从 10 次降到 1 次。这个细节在项目代码里未必看得出来但答辩时如果被问到“你怎么优化查询性能”完全可以直接拿出来讲。3.2 订单状态流转是一张状态机订单不是建好了就完事了。从用户提交订单开始状态会随着业务动作不断变化这是一张清晰的状态机当前状态触发动作下一个状态待支付用户模拟支付待取车待取车管理员确认车辆已交付租赁中租赁中用户归还车辆待还车待还车管理员确认车辆检查完毕已完成待支付超时未支付 / 用户取消已取消待取车用户取消已取消状态变化的核心原则是每一步状态变更都要有明确的触发源。这个源码里比较聪明的一点是把状态变更封装在视图层函数里而不是让每个页面都随便改订单状态。下单时还有一个并发问题值得注意。假如两个用户同时看到同一辆“可租”的车并同时提交订单如果代码不做保护就会出现一辆车被两次预订的情况。解决思路是在创建订单时对车辆行加锁from django.db import transaction from django.core.exceptions import ValidationError def create_order(request, car_id): with transaction.atomic(): car Car.objects.select_for_update().get(pkcar_id) if car.status ! 1: raise ValidationError(该车辆当前不可预订) order Order.objects.create( userrequest.user, carcar, start_daterequest.POST.get(start_date), end_daterequest.POST.get(end_date), total_amountcalculate_amount(car, request.POST), ) car.status 2 car.save() return redirect(order_detail, order_idorder.id)select_for_update会在数据库层面锁住这一行记录直到事务结束。这是应对并发问题最直接有效的办法也是答辩时一个非常加分的知识点。3.3 Django Admin 作为管理后台的妙处很多同学一听到后台管理就头大想着要自己写一个登录页、列表页、表单页。其实 Django 自带了一个功能强大的 Admin 后台只需要注册模型就能拥有完整的增删改查界面。网上租车系统的后台完全可以靠它撑起来。from django.contrib import admin from .models import Category, Car, Order admin.register(Car) class CarAdmin(admin.ModelAdmin): list_display (brand, model_name, plate_no, daily_rent, status) list_filter (status, category) search_fields (brand, model_name, plate_no) list_editable (status,) readonly_fields (created_at,)这段配置非常实用。list_display决定后台列表页显示哪些列list_filter让管理员能按状态和分类筛选search_fields让搜索框可以直接搜品牌、车型和车牌号list_editable可以让状态在列表页直接下拉修改不用进入详情页面。这种演示效果比自己做页面省力而且看起来很专业。订单模型也要注册到 Admin 里注册后就能直接在后台看到所有的订单、用户和车辆状态。如果想让订单号只读可以把order_no加进readonly_fields防止管理员手改订单号导致数据错乱。4. 项目从源码到跑通的落地路径4.1 环境搭建从固定版本开始拿到源码第一步不是看代码而是把环境搭好。环境搭建最怕的是两个问题Python 版本不一致、依赖库版本冲突。网上租车系统一般基于 Django 3.2 或 4.x 开发建议直接用虚拟环境安装。你在项目根目录下会看到一个requirements.txt文件打开看一下里面应该有 Django、Pillow 这类依赖。安装顺序按标准流程来python -m venv venv source venv/bin/activate # Windows 下执行 venv\Scripts\activate pip install -r requirements.txt为什么要用虚拟环境因为不同项目对 Django 的版本要求不同全局环境很容易装成“新项目把旧项目的库顶掉”的灾难现场。虚拟环境相当于给每个项目单独开了一个房间互不干扰发现问题直接删掉重建十分钟就能恢复。4.2 数据库迁移不是跑一次就完很多同学对 Django 的迁移机制理解不深以为执行python manage.py migrate就万事大吉。实际上迁移文件是跟着模型代码走的。如果源码里带了migrations目录迁移文件是现成的执行 migrate 即可。如果模型被改动过就得先生成迁移文件再执行python manage.py makemigrations python manage.py migrate python manage.py createsuperuser这里有一个非常常见的坑迁移顺序乱了或者数据库里已经有一张表迁移又尝试重复创建。遇到这种情况不要慌优先检查django_migrations表里的记录和实际模型是否对得上。如果某张表的迁移记录和代码对不上可以按需删除对应迁移记录再重新 migrate。这个操作比较敏感做之前最好先备份。创建超级管理员之后用python manage.py runserver启动项目访问http://127.0.0.1:8000/admin/登录后台再把车辆数据录进去。如果不想手录也可以准备一个fixtures/目录用python manage.py loaddata init_data.json把预置数据导进去演示的时候效率高很多。4.3 先把一条演示路径走顺我调试过很多毕设项目总结出一个经验拿到项目以后先不要急着看每一个功能而是先完整走通一条“用户下单”路径。也就是注册一个测试账号浏览车辆列表点进详情页提交订单模拟支付到后台查看订单。这条路径走通了项目的主干也就活着了。剩下的问题无论出现在哪里顺着这条路径一步一个断点去排查定位速度会非常快。我很推荐把这段流程当成你的习惯性自检每次改动代码后都把这条主路径重新走一遍能拦截大量低级错误。5. 远程调试和代码交付的这些事5.1 为什么毕设项目需要远程调试“远程调试”这个词听起来很高端实际上很接地气。很多同学平时在自己电脑上写代码等项目需要部署到服务器或者把代码交给别人的时候就会发现一个尴尬的问题你这里跑得好的代码换一个环境就起不来。远程调试的典型场景有三种一是项目要部署到云服务器上二是你帮别人看代码对方的环境和你不一致三是答辩演示时用自己的电脑不稳定需要一套能随时启动的服务器环境。基于 Django 的网上租车系统远程调试核心思路并不复杂把代码放到服务器上在服务器上创建虚拟环境、安装依赖、迁移数据库、启动开发服务器然后通过本地的 PyCharm 连接服务器的解释器实现“本地编辑代码、服务器执行程序”的效果。这样做的好处是你的开发环境和线上环境保持一致不会再出现“本地没毛病、服务器一堆错”的窘境。5.2 PyCharm 远程解释器配置的实操过程假设你已经有一台服务器这台服务器上安装了 Python 3.10 或更高版本并且已经用 git 拉取了源码。接下来要做的是在 PyCharm 里配置远程解释器。以 PyCharm Professional 为例配置流程大致是打开File - Settings - Project - Python Interpreter。点击齿轮按钮选择Add Interpreter - On SSH。填入服务器地址、用户名和认证方式建议优先使用密钥认证。在下一步选择服务器上的虚拟环境路径如果没有就新建一个。配置完成后PyCharm 会同步代码到服务器并自动安装依赖。配置完成之后本地编辑文件保存时 PyCharm 会同步到服务器运行和调试都走服务器的 Python 环境。你可以在代码里正常打断点PyCharm 会通过远程调试协议暂停在对应的行查看变量和执行堆栈。这个功能对排查线上环境问题特别有效。有一点要注意远程调试时Django 项目的ALLOWED_HOSTS必须包含服务器的域名或 IP否则访问会报DisallowedHost错误。同时本地和服务器的时间应该保持一致如果服务器时间和本地时间相差太大会造成登录态失效、数据库时间写错之类的莫名问题。5.3 换环境部署最容易踩的配置项除了ALLOWED_HOSTS部署环境里还有几个配置项是高频坑位。第一个是DEBUG。本地调试可以设成True服务器上一旦设成True错误页面会把路径、配置、源码行号全部暴露出来非常不安全。合理做法是设置环境变量来控制。第二个是静态文件。Django 开发服务器会自动处理静态文件但换到服务器上或者是用python manage.py runserver 0.0.0.0:8000给外部访问时静态文件路径配置不对页面就会出现“有 HTML 没有 CSS”的情况。最省事的方式是在settings.py里配好STATIC_URL和STATICFILES_DIRS确保模板能加载到样式。第三个是数据库。远程环境一般会用 MySQL 或 PostgreSQL和本地默认的 SQLite 有差异。如果你把源码里的数据库连接从 SQLite 改成 MySQL需要装对应的数据库驱动并且注意 MySQL 的字符集和时区设置。这个改造如果时间不够我建议演示时先用轻量的数据库别在数据库切换上挖坑。远程调试本质上是一个工程习惯而不是一个神秘技巧。养成“代码在服务器上也能跑通”的习惯你的项目从源码到交付的可信度会高很多。6. 二次定制从一套源码改造成自己的项目6.1 加字段相对容易迁移要谨慎拿到别人的源码最常遇到的需求就是“我想把某个功能改一下”。比如给车辆增加一个“是否新能源”的字段或者给订单增加一个“取车门店”的字段。加字段本身不难难的是字段加完之后怎么保证系统不崩。最好的方式是先改模型再执行makemigrations和migrate。这两个命令会自动生成增量迁移文件不会影响已有的数据。这里有一个很容易犯的错误直接在数据库里手动加列而不是通过 Django 迁移。手动加列会导致 Django 的迁移记录和真实数据库结构不一致后续migrate命令就会报错。所以我强烈建议改模型结构一律走迁移命令宁可多花五分钟也不要去手动操作数据库。6.2 新模块怎么加才不会破坏原项目如果你想在租车系统里加一个“新闻公告”模块方法非常清晰先在项目里新建一个 app比如python manage.py startapp news然后在settings.py的INSTALLED_APPS里注册再写模型、视图、模板。整个过程和原模块完全解耦不会影响租车的核心流程。这也体现了一个好的源码设计的重要性。原项目如果把所有代码堆在一个views.py里、所有模板都放在一个目录下二次定制就会非常痛苦。合理的项目应该按功能拆分 app比如users、cars、orders、news各管各的。你在改造时尽量延续这种风格而不是把新代码乱塞进已有模块。6.3 文档和讲解顺序要跟着代码走源码包里如果有文档这份文档不要只当摆设。文档的作用不只是“交差”更是你答辩时的提词器。在我看来文档应该包含四块内容项目背景和需求分析、数据库设计说明、核心业务流程说明、运行部署说明。调试项目时我习惯把文档和代码对照着看每看一个模块就在文档里标记一下对应的模型类和视图函数。这样的好处是当老师随机指着一个页面问“这个功能是怎么实现的”时你能立刻在代码里定位到相关文件而不是满项目翻找。对于答辩来说这种从容的定位能力比背十页讲解稿都管用。7. 远程调试之外我遇到过的高频坑和答辩现场经验7.1 时间比较、金额计算和并发更新的坑租车系统里最容易出错的其实是“看似简单”的日期和金额计算。订单会记录start_date和end_date计算租车天数时必须用(end_date - start_date).days。如果结束日期等于开始日期天数就是 0这种边界情况要特别处理否则会出现租一天结果费用为 0 的 bug。金额字段用DecimalField而不是FloatField这一点很重要。浮点数在计算机里是有精度问题的0.1 加 0.2 可能得到 0.30000000000000004。金额一旦涉及小数计算必须用Decimal类型并且数据库字段也声明为decimal才能保证金额准确。并发更新的坑我前面提过。状态变更时如果不加锁可能出现管理员在后台改车辆状态用户同时在下单两边互相覆盖的情况。再加上事务保护才能保证数据一致。这个知识点如果在答辩时被问到“如果两个人同时租同一辆车怎么办”可以直接回答“我用数据库行锁和事务来保证”比空谈理论有力得多。7.2 演示现场最怕出问题的三个场景我见过太多演示现场翻车总结起来有三个高频场景。第一个是数据库没有预置数据现场打开车辆列表是空的。这个问题最好解决提前录好 10 到 20 辆车的数据确保图片、价格、状态都有内容。第二个是媒体文件路径错误。车辆图片在本地能显示一到服务器就变成裂图。主要原因是没有配置MEDIA_URL或者服务器上媒体目录不存在。演示前一定要在真实环境里重新启动项目把图片、登录状态、订单流程完整走一遍。第三个是端口和访问地址不对。如果用远程服务器演示runserver 0.0.0.0:8000默认是允许外部访问的但防火墙或安全组没有放行 8000 端口就会一直连接不上。这个必须在演示前确认不要现场去查防火墙规则。7.3 我觉得比较顺的答辩讲解顺序最后分享一套我比较推荐的讲解顺序不需要背稿但要有清晰的思路。先讲业务背景在租车场景下用户、车辆、订单之间存在什么关系系统解决了什么问题。再讲技术选型为什么用 DjangoORM 怎么帮助我快速建模Admin 后台能不能支撑管理需求。然后按数据流讲用户看到车辆列表点进详情页提交订单后台处理订单。这段要拿着代码说讲到CarListVie时就切到列表页代码讲到订单状态时就切到Order模型。被追问的时候优先谈事务和并发控制这是最容易体现“你确实写过代码而不是只调包”的地方。再展示你对select_related、DecimalField、迁移机制的理解基本就可以稳住场面了。7.4 最后聊聊我实际操作中的体会调试这套网上租车系统的过程里我最大的感受是毕业设计源码拿到手不要急着改先花两个小时把项目结构、模型关系、主流程跑通再决定改哪里。很多人一上来就加功能结果连原有的订单状态都能搞乱最终返工成本更高。如果你打算在此基础上做定制记住一条原则核心流程尽量少动扩展功能尽量新增独立模块。租车的下单闭环已经足够完整你要做的个性化提升可以是前端样式、Excel 导出、数据可视化、前后端分离接口等不必去重构已经稳定的核心逻辑。还有一个小技巧调试任何 Django 项目时多利用print()配合日志定位问题。比如在某一个查询语句后面打印一下queryset的数量立刻就能知道是数据没查到还是模板没渲染。不要小看这种土办法它就是最快的远程调试方式。
返回列表