最近在帮几个做毕设的朋友看项目,发现一个选题出现频率特别高:Python + Django 的 4S 店客户管理系统。说实话,这个题我能理解为什么这么多人选——4S 店这个业务场景足够典型,客户管理、车辆档案、维修记录、销售跟进,全是经典的信息管理系统功能,做出来容易讲清楚,也方便演示。而这个系统最终做成什么样,直接决定了答辩时是“优秀”还是“及格”。
这篇文章我就把整个项目从需求分析到数据库设计、从核心代码到部署上线完整梳理一遍。不管你是拿它做毕业设计,还是想练手 Django 项目,或者真的是帮某个门店做内部工具,都可以照着这个思路走。我会把每一步的“为什么这么做”也讲清楚,而不是只甩代码。
1. 4S店客户管理系统到底在解决什么问题
1.1 4S店业务场景里的管理痛点
4S店这个词大家都不陌生,它集成了汽车销售、售后服务、配件供应和信息反馈四条业务线。但真正进过4S店后台,或者跟销售顾问、售后经理聊过,你会发现他们日常的工作流远没有外人想象的那么“高大上”——大量客户信息还堆在Excel表格里,销售跟进靠微信聊天记录,维修历史散落在不同系统里,客户来了还要重复问一遍上次保养做了什么。
这种状况带来的问题很实际:
- 销售顾问离职,他手里跟到一半的客户线索全部丢失,新接手的人要从头聊。
- 客户到店做保养,前台要翻半天纸质档案才能确认上次换的是哪个品牌的机油。
- 售后回访缺乏数据支撑,不知道哪些客户已经超过半年没回店,流失了也没人知道。
- 潜在客户的跟进状态不透明,老板想看看今天销售团队跟进了多少客户,只能一个个问,数据还可能对不上。
客户管理系统的作用,就是把上面这些混乱的线下流程搬到一个统一的 Web 系统里。每个客户一条档案,车辆信息、维修记录、跟进记录都挂在这条档案下,任何人打开系统都能看到完整的历史。
1.2 这个选题为什么适合做项目实战
从技术角度来说,这个选题覆盖了 Web 开发的核心知识面:用户认证、数据建模、增删改查、表关联查询、搜索分页、表单验证,每一个都是实际项目里绕不开的东西。而且难度曲线很平缓,不会一上来就让人劝退。
从交付角度来说,一套完整的系统 = 后端接口 + 前端页面 + 数据库设计 + 部署方案。Django 在这块的开发效率非常高,它的 ORM 可以直接把数据库表映射成 Python 类,自带 Admin 后台可以临时当数据管理工具用,内置的模板引擎做页面也够用。一个完整的系统,纯一个人开发的话,有效工作时间三到五天就能做出来。
我自己做这个项目的时候,还顺手把说明文档(也就是标题里那个 LW)整理了。写文档这件事很多人不重视,但实际做下来发现,把每个表的设计意图、每个接口的调用方式写清楚,对自己理清逻辑也有很大帮助。答辩或者给客户演示的时候,拿一份结构清晰的文档出来,比口头讲半天要有说服力得多。
需要模型API调用? 免费领10W Token,多模型网关一键接入 Claude、DeepSeek 等主流模型。
2. 为什么选了Django而不是Flask
2.1 Django三大让我省下大量时间的特性
做这种管理系统,技术选型一般会在 Flask 和 Django 之间纠结。我个人的建议是:凡是涉及用户登录后台、数据模型多、页面多、需要快速交付的项目,直接选 Django,不用犹豫。
第一个理由是它自带 Admin 后台。客户表、车辆表、工单表建好之后,Django Admin 会自动生成一套可用的增删改查界面。开发阶段测试数据根本不用写脚本去造,直接在后台点几下就能填进去。就算系统上线了,临时要改个什么数据,登录 Admin 后台也比写 SQL 方便。
第二个理由是 ORM 写起来太顺手。Django 的 ORM 支持链式查询、跨表查询、聚合统计,一条 Customer.objects.filter(last_visit__lt=date).count() 就能统计出三个月没到店的客户数量。这要是在 Flask 里,得自己拼 SQL、处理连接池、管理事务,开发效率差了一大截。
第三个理由是 Django 自带用户认证和 CSRF 防护。登录、登出、session、密码加密这些都是内置的,不需要引第三方库。做管理系统登录功能是刚需,这块能省时间是实打实的。
2.2 项目目录结构和基础配置
我用 Django 4.x 版本搭的项目,创建完项目之后,核心目录结构是这样的:
text复制4s_crm/
├── manage.py
├── requirements.txt
├── config/
│ ├── settings.py
│ ├── urls.py
│ └── wsgi.py
├── apps/
│ ├── accounts/ # 用户登录、权限
│ ├── customers/ # 客户信息、车辆档案
│ ├── service/ # 维修保养工单
│ └── sales/ # 销售线索、跟进记录
├── static/ # 静态资源
├── templates/ # 前端页面模板
└── db.sqlite3
有一个实用建议是:用 startapp 创建的第一个应用不要放在项目根目录,而是统一放到 apps/ 包下面管理。项目小的时候感觉无所谓,一旦功能多了,按业务拆分成多个 app,维护起来会舒服很多。具体做法是在 apps/ 目录下建 __init__.py,以后每个子应用用 apps.customers 这种方式导入,同时在 settings.py 里把 app 路径写成全路径。
3. 数据库设计:五张核心表怎么拆出来的
3.1 数据建模的思路
数据库设计是这个项目最重要的环节。很多初学的人一上来就直接建客户表,把姓名、电话、地址、车牌号、车型、保险日期全塞在一张表里。这样确实简单,但后面一定会遇到问题——比如一个客户名下有两辆车,或者一辆车做过三次维修,这些数据往哪放?硬塞在同一张表里,要么产生大量冗余字段,要么只能放弃记录。
正确的做法是从业务关系入手,先理清楚实体之间的关系。4S店客户管理场景里,核心实体有四个:用户(店里的员工)、客户、车辆、工单/跟进记录。关系是这样的:
- 一个用户负责多个客户(销售顾问可以跟进多个潜在客户)。
- 一个客户拥有多辆车(经常有客户增购第二辆车,或者家里有两台车都在这里保养)。
- 一辆车对应多条维修工单记录。
- 一个客户有多条销售跟进记录。
这本质上就是典型的一对多关系。遵循“把重复出现的信息拆成子表,用外键关联”这个原则,我设计了以下五张核心表。
3.2 models.py完整代码和字段说明
下面是完整的模型代码,包含了系统里最主要的几张表:
python复制from django.db import models
from django.contrib.auth.models import User
class Customer(models.Model):
"""客户基本信息表"""
name = models.CharField("客户姓名", max_length=50)
phone = models.CharField("联系电话", max_length=11, unique=True)
gender = models.CharField("性别", max_length=10, choices=(
("male", "男"), ("female", "女")), blank=True)
age = models.IntegerField("年龄", null=True, blank=True)
address = models.CharField("通讯地址", max_length=200, blank=True)
source = models.CharField("客户来源", max_length=50, blank=True)
level = models.CharField("客户等级", max_length=20, choices=(
("A", "A(高意向)"), ("B", "B(中意向)"), ("C", "C(低意向)")), default="C")
owner = models.ForeignKey(
User, on_delete=models.SET_NULL, null=True, blank=True,
verbose_name="归属销售顾问", related_name="customers")
remark = models.TextField("备注", blank=True)
created_at = models.DateTimeField("创建时间", auto_now_add=True)
updated_at = models.DateTimeField("更新时间", auto_now=True)
class Meta:
verbose_name = "客户"
ordering = ["-created_at"]
def __str__(self):
return f"{self.name}-{self.phone}"
class Vehicle(models.Model):
"""车辆信息表(一个客户可能有多辆车)"""
customer = models.ForeignKey(
Customer, on_delete=models.CASCADE, verbose_name="所属客户",
related_name="vehicles")
plate_number = models.CharField("车牌号", max_length=20)
brand = models.CharField("品牌", max_length=20)
model_name = models.CharField("车型", max_length=50)
vin = models.CharField("车架号", max_length=30, blank=True)
purchase_date = models.DateField("购车日期", null=True, blank=True)
insurance_expire_date = models.DateField("保险到期日", null=True, blank=True)
class Meta:
verbose_name = "车辆"
unique_together = ["customer", "plate_number"]
def __str__(self):
return f"{self.plate_number}-{self.brand}{self.model_name}"
class MaintainOrder(models.Model):
"""维修/保养工单表"""
vehicle = models.ForeignKey(
Vehicle, on_delete=models.CASCADE, verbose_name="车辆",
related_name="orders")
order_no = models.CharField("工单号", max_length=30, unique=True)
order_type = models.CharField("工单类型", max_length=10, choices=(
("maintain", "保养"), ("repair", "维修")), default="maintain")
service_item = models.TextField("服务项目")
cost = models.DecimalField("费用", max_digits=10, decimal_places=2, default=0)
technician = models.CharField("技师姓名", max_length=30, blank=True)
status = models.CharField("状态", max_length=20, choices=(
("pending", "待处理"), ("processing", "处理中"),
("completed", "已完成"), ("cancelled", "已取消")), default="pending")
finished_at = models.DateTimeField("完工时间", null=True, blank=True)
created_at = models.DateTimeField("创建时间", auto_now_add=True)
class Meta:
verbose_name = "工单"
ordering = ["-created_at"]
def __str__(self):
return self.order_no
class FollowUpRecord(models.Model):
"""销售跟进记录表"""
customer = models.ForeignKey(
Customer, on_delete=models.CASCADE, verbose_name="客户",
related_name="follow_records")
follow_time = models.DateTimeField("跟进时间", auto_now_add=True)
follow_type = models.CharField("跟进方式", max_length=20, choices=(
("phone", "电话"), ("visit", "到店"), ("wechat", "微信"), ("other", "其他")),
default="phone")
content = models.TextField("跟进内容")
next_plan = models.CharField("下次跟进计划", max_length=200, blank=True)
follow_by = models.ForeignKey(
User, on_delete=models.SET_NULL, null=True, verbose_name="跟进人")
class Meta:
verbose_name = "跟进记录"
ordering = ["-follow_time"]
几个关键的建模决策点说明一下:
Customer.phone设置了unique=True,电话号码是客户身份最重要的标识,同一个人如果录两次,系统应该直接拦截。Vehicle.customer用了on_delete=models.CASCADE,客户被删掉的时候,他名下的车辆和工单也一并删除。这在真实业务里可能有点激进,但对于课程设计级别的系统来说可以接受。有更高要求的话,可以改成保护模式并提示先处理关联数据。Customer.owner用了SET_NULL,员工离职时把客户归属置为未分配,而不是把客户一起删掉。Vehicle表里设置了unique_together = ["customer", "plate_number"],保证同一个客户名下不会重复录同一块车牌。
另外说明文档(LW)里我把每张表的字段类型、约束、用途都列成了表格,后面写设计报告的时候直接复制就行,这个习惯建议保留。
4. 核心功能模块的实现过程
4.1 登录和权限控制
管理系统跟普通展示网站不一样,不是所有人都能访问。我这里的做法是用 Django 自带的 LoginView 配合 login_required 装饰器,把整个系统内部页面都保护起来。
python复制# config/urls.py
from django.contrib.auth import views as auth_views
from django.urls import path, include
from django.contrib import admin
urlpatterns = [
path('admin/', admin.site.urls),
path('accounts/login/', auth_views.LoginView.as_view(template_name='login.html'),
name='login'),
path('accounts/logout/', auth_views.LogoutView.as_view(), name='logout'),
path('', include('apps.customers.urls')),
]
然后用一个自定义的装饰器,判断用户是否属于销售或售后角色:
python复制from functools import wraps
from django.shortcuts import redirect
def role_required(role_name):
"""根据用户所属用户组限制访问"""
def decorator(view_func):
@wraps(view_func)
def _wrapped_view(request, *args, **kwargs):
if not request.user.is_authenticated:
return redirect('login')
# 简单方案:用 is_staff 标识内部员工
if not request.user.is_staff:
return redirect('login')
return view_func(request, *args, **kwargs)
return _wrapped_view
return decorator
实际操作里我建议使用 is_staff 或者独立的用户组来做权限控制,而不是给每个功能单独写权限判断。小系统把用户分成“普通用户”和“管理员”基本就够了,权限搞太细反而增加工作量。
4.2 客户信息增删改查与搜索分页
客户列表页是整个系统的核心页面,我给它加了三个功能:关键词搜索(名字或电话)、状态筛选(客户等级)、分页展示。这三样加在一起,就是一个非常典型的企业后台列表页逻辑。
python复制from django.shortcuts import render, get_object_or_404, redirect
from django.core.paginator import Paginator
from django.contrib.auth.decorators import login_required
from .models import Customer, Vehicle, FollowUpRecord
from .forms import CustomerForm, VehicleForm, FollowUpForm
@login_required
def customer_list(request):
customers = Customer.objects.select_related("owner").all()
keyword = request.GET.get("keyword", "")
level = request.GET.get("level", "")
if keyword:
customers = customers.filter(name__icontains=keyword) | \
customers.filter(phone__icontains=keyword)
if level:
customers = customers.filter(level=level)
paginator = Paginator(customers, 10)
page_number = request.GET.get("page")
page_obj = paginator.get_page(page_number)
return render(request, "customers/customer_list.html", {
"page_obj": page_obj, "keyword": keyword, "level": level,
})
@login_required
def customer_add(request):
if request.method == "POST":
form = CustomerForm(request.POST)
if form.is_valid():
customer = form.save(commit=False)
customer.owner = request.user
customer.save()
return redirect("customer_list")
else:
form = CustomerForm()
return render(request, "customers/customer_form.html", {"form": form})
@login_required
def customer_detail(request, pk):
customer = get_object_or_404(Customer, pk=pk)
vehicles = customer.vehicles.all()
follow_records = customer.follow_records.all()
return render(request, "customers/customer_detail.html", {
"customer": customer,
"vehicles": vehicles,
"follow_records": follow_records,
})
注意我用了 select_related("owner"),这是为了避免在列表页循环时逐个查用户表产生 N+1 次查询。数据量小的时候不明显,客户一多,这行代码能省掉大量数据库往返。
模板部分,Django 自带模板引擎完全够用,不需要另外引入 Vue 或 React。我做了简单的 Bootstrap 页面,表格展示客户列表,搜索框和筛选下拉框放在上面,底部是 Bootstrap 风格的分页组件。
4.3 业务关联:维修记录的录入逻辑
维修工单不能孤立存在,它必须挂到一辆车下面,而车又必须在某个客户名下。所以新增工单的流程是:
加车辆 → 然后录工单 →,先选客户 → 再选该客户下的车辆 → 填服务项目和费用。
这里我用了 Django Form 实现级联选择:
python复制class MaintainOrderForm(forms.ModelForm):
customer = forms.ModelChoiceField(
queryset=Customer.objects.all(), label="选择客户",
empty_label="--- 请先选择客户 ---")
vehicle = forms.ModelChoiceField(
queryset=Vehicle.objects.all(), label="选择车辆",
empty_label="--- 请选择该客户车辆 ---")
class Meta:
model = MaintainOrder
fields = ["order_type", "service_item", "cost", "technician", "status"]
def clean(self):
cleaned_data = super().clean()
customer = cleaned_data.get("customer")
vehicle = cleaned_data.get("vehicle")
if customer and vehicle:
if vehicle.customer_id != customer.id:
self.add_error("vehicle", "所选车辆不属于该客户,请重新选择")
return cleaned_data
页面里通过 JavaScript 监听客户下拉框的变化,动态刷新车辆选项。这属于最基础的前后端交互,Django 视图提供接口返回该客户名下的车辆 JSON 数据即可。逻辑不难,但这一步做完,整个系统就有了“业务逻辑”的味道,看起来不再是单纯的 CRUD。
5. 常见问题与排查技巧实录
5.1 Django开发和部署中的踩坑清单
做这个项目的时候,我把遇到的报错顺手记录了一下。这些问题很多 Django 初学者都会反复碰到,干脆整理成一张速查表。
| 报错信息 | 原因 | 解决办法 |
|---|---|---|
ImportError: cannot import name 'xxx' from 'yyy' |
models.py 里循环引用 | 检查两个 model 之间的 import,尽量使用字符串引用 'app.ModelName' |
FieldError: Cannot resolve keyword 'xxx' |
ORM 字段名写错或关联链路过深 | 检查过滤字段是否存在于模型中,外链需要小写模型名+字段 |
TemplateDoesNotExist |
templates 目录路径没配对 |
确认 app 的 templates 目录在 settings.py 中已注册,且文件名和视图里返回的一致 |
Forbidden (CSRF cookie not set) |
表单里少了 {% csrf_token %} |
在所有 POST 表单模板中添加 {% csrf_token %} |
NoReverseMatch at /xxx/ |
url 命名空间或参数个数不对 | 检查 {% url %} 写法与 urls.py 中的 name 和参数是否一致 |
OperationalError: no such table: xxx |
没跑迁移 | 依次执行 python manage.py makemigrations 和 python manage.py migrate |
Bad Request (400) 部署后出现 |
ALLOWED_HOSTS 未包含域名/IP |
在 settings.py 里把服务器IP或域名加进 ALLOWED_HOSTS |
这里面最常见的其实不是代码本身报错,而是忘记迁移数据表。每次修改完 models.py 里的字段,一定要记住先 makemigrations 再 migrate,这是个肌肉记忆。
5.2 数据库选型与生产部署的建议
本地开发我用的是 Django 自带的 SQLite,零配置,文件即数据库。但到了部署环境,我更推荐换成 PostgreSQL。SQLite 在并发写入和数据量大的场景下会吃力,特别是客户系统这种需要持续记录操作日志的业务,PostgreSQL 要稳妥很多。
切换数据库配置只需改动 settings.py 里的 DATABASES 字典:
python复制DATABASES = {
"default": {
"ENGINE": "django.db.backends.postgresql",
"NAME": "crm_db",
"USER": "crm_user",
"PASSWORD": "你的密码",
"HOST": "127.0.0.1",
"PORT": "5432",
}
}
迁移方式也很简单,先在测试环境跑一遍数据迁移,再用 dumpdata 和 loaddata 把数据从 SQLite 导入 PostgreSQL,基本是无痛的。
部署的话,我用的是经典的 Nginx + Gunicorn + Django 组合。静态文件交给 Nginx 处理,动态请求通过 Gunicorn 转发给 Django。有一个特别容易踩的坑是:部署时忘记把 DEBUG = True 改成 False,结果项目一上线就报错,而且错误页面直接暴露了源代码路径,这是很大的安全隐患。改成 False 之后,还要执行 python manage.py collectstatic 把所有静态文件收集起来,再在 settings.py 里配 STATIC_ROOT。
5.3 为什么一定要有说明文档(LW)
最后说下标题里那个 “LW”。开始做项目时我也觉得文档是凑数用的,但后来发现写文档的过程能逼你重新审视代码逻辑。写数据库设计说明时,要解释清楚为什么用外键而不是冗余字段;写系统使用说明时,要设身处地考虑一个普通业务员怎么可能用得顺手。
我把说明文档分成几个部分:项目概述、需求分析、功能设计、数据库设计、核心代码说明、系统测试、使用说明。这些内容其实完全可以在开发过程中顺便积累,而不是最后赶工。等你答辩或演示的时候,拿一份图文并茂的文档出来,比在台上空口讲“我这个功能实现了”要有说服力得多。
结语
这个项目做下来,最大的收获不是“会了 Django”,而是完整地走了一遍从需求分析到数据库设计、从编码实现到部署上线的全流程。很多细节只有自己动手才会发现——比如为什么外键要用 CASCADE 而不是 SET_NULL,为什么列表页要写 select_related,为什么部署后页面样式全丢了。这些问题在网上都有答案,但只有踩过坑,才能真正理解那些答案背后在说什么。
如果你准备拿这个题目做毕设或者练手,建议按照我上面说的顺序来:先理清业务关系和数据模型,再用 Django Admin 快速搭出后台,然后把客户页面和工单页面逐个完善,最后再考虑部署。流程走通之后,你会发现一个系统的雏形很快就出来了,剩下的只是在上面不断打磨细节而已。
