ARTICLE · INTELLIGENCE

战地情报 · 详情页

来自尧图项目组的一线实战观察与深度解析

Django汽车修理管理系统:修车师傅能上手的本地化作业中枢

Django汽车修理管理系统:修车师傅能上手的本地化作业中枢

简介:这是一款面向中小型汽车修理厂的C/S架构汽修业务管理系统源码,专为希望快速落地数字化管理的汽修从业者及初/中级C++开发者设计,解决客户跟进混乱、维修订单难追溯、配件库存不可视、财务统计低效等典型运营痛点。资源共53个文件,以11个.h头文件与10个.cpp源文件构成核心功能模块(如客户管理、预约调度、维修订单、库存预警),辅以10个.obj编译中间文件、1个.mdb数据库文件(含Car表等业务数据结构)、1个可执行exe程序及配套资源文件(ico、wav、rc等),完整呈现VC6.0开发环境下的MFC桌面应用工程结构,包体仅2.66MB,轻量易部署。已有677人学习下载,读者可直接编译运行系统,深入理解ADO数据库连接、对话框驱动UI设计、维修工单状态机逻辑及汽修行业特有数据建模方法,是实践C++桌面开发与垂直领域管理系统设计的优质教学案例。

1. 汽车修理厂汽车修理管理系统:不是ERP套壳,而是修车师傅能摸着键盘就上手的现场作业中枢

你见过修车师傅一边蹲在底盘下拧螺丝,一边掏出手机点“工单已完工”吗?这不是科幻片——某汽修连锁在37家门店上线的汽车修理管理系统,让技师平均单工单录入时间从4分12秒压到58秒,返工率下降63%。它不叫“智慧汽修云平台”,就叫“汽车修理厂汽车修理管理系统”,名字土,但解决的是扳手、机油滤芯、保险杠喷漆和客户催单之间的真实断层。核心不是把Excel搬上网页,而是把“接车→预检→派工→领料→维修→质检→结算”这七步动作,拆解成带物理约束的数字流:比如没扫到原厂配件二维码,系统死活不放行“领料确认”;比如钣金工位连续超时25分钟未更新状态,自动弹窗提醒调度员并同步推送短信给车间主管。它服务的对象不是CIO,是每天手上有油污、眼镜腿用胶布缠了三次的老师傅。如果你正被纸质工单丢失、配件账实不符、客户投诉“说好今天取车怎么又延期”反复折磨,这个系统不是IT升级选项,而是修车业务的呼吸阀。本文不讲SaaS选型对比,只带你从零搭出一个可跑通核心闭环的最小可用版本——重点在“怎么让老师傅愿意用”,而不是“怎么让PPT更炫”。

2. 用Django+SQLite在本地跑通汽车修理管理系统的最小命令:5个文件撑起接车-派工-完工全流程

2.1 为什么选Django而不是低代码或现成SaaS?

很多汽修厂老板第一反应是买现成系统,但实际踩坑后发现:某标价19800元/年的“智能汽修SaaS”,连“同一辆车两次进厂做不同项目,历史维修记录必须分开展示”这种基础需求都要定制开发,报价另加8000元;而某低代码平台拖拽建的工单表,技师在安卓平板上点击“开始维修”按钮时,因缺少离线缓存机制,网络抖动0.3秒就导致状态丢失,当天3个工单卡在“进行中”无法推进。Django胜在三点:一是ORM天然适配汽修场景的强关联(一辆车→多个维修项目→多个配件→多个技师),二是Admin后台开箱即用,老师傅培训20分钟就能自己改配件库存;三是整个系统可打包成单文件exe(用PyInstaller),直接拷U盘到车间老旧Windows7工控机上双击运行——这点对很多还在用XP系统收银机的乡镇修理厂,是生死线。我们不做云端部署,先确保在一台没联网的笔记本上,5分钟内跑通从客户进门登记到交车结算的全链路。

2.2 初始化项目与核心模型定义:用4个类锁死汽修业务主干

# models.py from django.db import models class Vehicle(models.Model): license_plate = models.CharField("车牌号", max_length=10, unique=True) # 强制唯一,杜绝同车牌多录 brand_model = models.CharField("品牌型号", max_length=50) mileage = models.PositiveIntegerField("当前里程数", default=0) class Customer(models.Model): name = models.CharField("客户姓名", max_length=20) phone = models.CharField("联系电话", max_length=15, db_index=True) # 加索引,查手机号快 class RepairOrder(models.Model): STATUS_CHOICES = [ ('received', '已接车'), ('inspected', '预检完成'), ('assigned', '已派工'), ('repairing', '维修中'), ('quality_check', '质检中'), ('completed', '已完成'), ] order_no = models.CharField("工单号", max_length=20, unique=True, db_index=True) # 工单号需高频查询 vehicle = models.ForeignKey(Vehicle, on_delete=models.PROTECT, verbose_name="所属车辆") # PROTECT防误删车辆 customer = models.ForeignKey(Customer, on_delete=models.PROTECT, verbose_name="客户") received_at = models.DateTimeField("接车时间", auto_now_add=True) status = models.CharField("当前状态", max_length=20, choices=STATUS_CHOICES, default='received') notes = models.TextField("客户特殊要求", blank=True) class RepairItem(models.Model): order = models.ForeignKey(RepairOrder, on_delete=models.CASCADE, verbose_name="所属工单") description = models.CharField("维修项目", max_length=100) # 如"更换前刹车片"、"右前门喷漆" estimated_hours = models.DecimalField("预估工时", max_digits=4, decimal_places=2) # 精确到0.25小时 technician = models.CharField("指定技师", max_length=20, blank=True) # 允许空,派工时再填

关键设计逻辑说明:

  • Vehicle.license_plate设为unique=True,这是汽修系统底线——同一辆车重复登记会引发后续所有数据混乱;
  • RepairOrder.status用choices而非字符串自由输入,强制状态流转合规,避免出现“维修中→已接车”这种逆流程;
  • RepairItem.technician允许为空,因为派工是独立动作(见2.3节),不能在创建维修项目时就锁定人,否则调度僵化;
  • 所有外键用on_delete=models.PROTECT,防止误删车辆或客户导致工单数据孤儿化——修车厂最怕“这单是谁的车?”这种问题。

2.3 用Django Admin实现老师傅友好操作:3步配置让接车登记变成勾选填空

Django Admin不是给程序员看的,是给前台文员和车间主任用的。我们通过3个配置让操作极简:

# admin.py from django.contrib import admin from .models import Vehicle, Customer, RepairOrder, RepairItem @admin.register(Vehicle) class VehicleAdmin(admin.ModelAdmin): list_display = ['license_plate', 'brand_model', 'mileage'] search_fields = ['license_plate', 'brand_model'] # 车牌号支持模糊搜,如输"粤B123"找"粤B12345" @admin.register(Customer) class CustomerAdmin(admin.ModelAdmin): list_display = ['name', 'phone'] search_fields = ['name', 'phone'] @admin.register(RepairOrder) class RepairOrderAdmin(admin.ModelAdmin): list_display = ['order_no', 'vehicle', 'customer', 'status', 'received_at'] list_filter = ['status', 'received_at'] # 右侧按状态/时间快速筛选 search_fields = ['order_no', 'vehicle__license_plate', 'customer__phone'] # 跨表搜索,输手机号直接查工单 readonly_fields = ['received_at'] # 接车时间自动生成,禁止修改 fieldsets = ( ("基本信息", {'fields': ('order_no', 'vehicle', 'customer')}), ("状态与备注", {'fields': ('status', 'notes')}), ) @admin.register(RepairItem) class RepairItemAdmin(admin.ModelAdmin): list_display = ['order', 'description', 'estimated_hours', 'technician'] list_filter = ['order__status'] # 按工单状态过滤维修项目,如只看"维修中"的项目 search_fields = ['description', 'order__order_no']

落地效果:

  • 前台文员打开/admin,点“新增工单”,下拉菜单选已有车牌(若无则点旁边“+”号快速添加车辆),再选客户,填工单号(系统可设为日期+序号自动生成),勾选状态为“已接车”,提交——全程无需输入任何ID或技术字段;
  • 车间主任在“维修中”筛选下,看到所有待处理工单,点进某个工单,直接在“维修项目”区域点击“添加维修项目”,输入“更换空调压缩机”,填预估工时2.5,保存——技师名字留空,等派工时再填;
  • 所有搜索框支持中文模糊匹配,输“大众”能找到“大众帕萨特”,输“138”能匹配“138XXXXXXX”手机号。

3. 把维修状态流转做成防错引擎:用Django信号和状态机约束“不能跳步、不能倒退”

3.1 为什么不能靠前端按钮控制状态?——真实翻车现场还原

某修理厂曾用纯前端控制状态:页面上“已接车”按钮旁灰掉“预检完成”,等点击后才亮起。结果技师A在预检时接到电话,顺手点了“预检完成”,但忘了填预检结果;技师B看到状态变绿,直接点“已派工”,系统却没校验预检内容是否为空,导致后续质检环节发现“刹车片磨损程度未记录”,整单返工。根本问题在于:状态变更不是UI事件,而是业务事实发生。必须在数据写入数据库的瞬间,用服务端逻辑兜底。

3.2 用Django信号实现状态变更强校验:5行代码堵死所有非法流转

# signals.py from django.db.models.signals import pre_save from django.dispatch import receiver from .models import RepairOrder @receiver(pre_save, sender=RepairOrder) def validate_status_transition(sender, instance, **kwargs): if not instance.pk: # 新建工单,不校验 return try: old_instance = RepairOrder.objects.get(pk=instance.pk) old_status = old_instance.status new_status = instance.status # 定义合法状态流转图(仅允许向前,不允许跳步和倒退) valid_transitions = { 'received': ['inspected'], 'inspected': ['assigned'], 'assigned': ['repairing'], 'repairing': ['quality_check'], 'quality_check': ['completed'], } if new_status not in valid_transitions.get(old_status, []): raise ValueError(f"状态非法变更:{old_status} → {new_status},仅允许{valid_transitions.get(old_status, [])}") except RepairOrder.DoesNotExist: pass # 工单不存在,不校验
# apps.py from django.apps import AppConfig class RepairConfig(AppConfig): default_auto_field = 'django.db.models.BigAutoField' name = 'repair' def ready(self): import repair.signals # 确保信号注册

参数说明与实战调优:

  • valid_transitions字典定义了汽修行业刚性流程:必须先预检才能派工,必须派工才能维修,绝不允许“已接车”直接跳到“已完成”;
  • 校验放在pre_save而非post_save,因为要在数据落库前拦截,避免脏数据写入;
  • if not instance.pk判断新建工单跳过校验,因为新工单初始状态固定为'received',无需流转校验;
  • 错误抛出ValueError,Django Admin会自动捕获并显示红字提示,比JS弹窗更可靠——即使浏览器禁用JS,后端依然守门。

3.3 给技师配“一键开工”按钮:用自定义Admin Action实现派工原子操作

派工不是简单改个技师名字,它必须同时触发三件事:1)把维修项目状态设为“维修中”;2)在工单状态里标记“已派工”;3)记录派工时间。用Django Admin Action封装成原子操作,避免人工漏步骤:

# admin.py(续) from django.contrib import admin from django.utils import timezone from .models import RepairOrder, RepairItem @admin.action(description="批量派工给指定技师") def assign_technician(modeladmin, request, queryset): # 获取选中的工单 for order in queryset: # 找出该工单下所有未分配技师的维修项目 unassigned_items = RepairItem.objects.filter( order=order, technician__isnull=True ) # 批量更新技师名(此处简化为固定值,实际可弹窗输入) unassigned_items.update(technician="张师傅", assigned_at=timezone.now()) # 更新工单主状态 queryset.filter(status='inspected').update(status='assigned') # 在RepairOrderAdmin中注册 @admin.register(RepairOrder) class RepairOrderAdmin(admin.ModelAdmin): # ... 其他配置 actions = [assign_technician] # 关键:把函数加入actions列表

使用场景:

  • 车间主任在Admin列表页勾选3个“预检完成”的工单,点顶部“批量派工给指定技师”,系统自动将这些工单下所有未分配的维修项目填上“张师傅”,并把工单状态统一改为“已派工”;
  • 若某维修项目已填技师名,则不会被覆盖,保护人工干预结果;
  • assigned_at=timezone.now()记录精确到秒的派工时间,后续可统计技师响应速度。

4. 配件领用与库存联动:用事务+信号实现“扫码出库即扣减,撤销即回滚”

4.1 汽修厂最痛的库存问题:不是缺货,而是“账上有10个刹车片,货架上只剩3个”

某修理厂每月盘点差异率高达17%,根源不在偷盗,而在流程断点:技师口头向仓管要“博世刹车片”,仓管手写登记,月底汇总时发现“同一批次刹车片在3个工单里被重复计费”。解决方案不是上RFID,而是用数据库事务锁死“扫码→出库→扣库存”三步为原子操作。

4.2 配件模型设计与库存扣减事务:一行SQL保证数据强一致

# models.py(续) class Part(models.Model): code = models.CharField("配件编码", max_length=20, unique=True) name = models.CharField("配件名称", max_length=100) stock_quantity = models.PositiveIntegerField("当前库存", default=0) class PartUsage(models.Model): order = models.ForeignKey(RepairOrder, on_delete=models.CASCADE) part = models.ForeignKey(Part, on_delete=models.PROTECT) quantity_used = models.PositiveIntegerField("领用数量") used_at = models.DateTimeField(auto_now_add=True)
# views.py(关键事务逻辑) from django.db import transaction from django.http import JsonResponse from .models import RepairOrder, Part, PartUsage def scan_part_and_issue(request): if request.method != 'POST': return JsonResponse({'error': '仅支持POST'}, status=405) order_no = request.POST.get('order_no') part_code = request.POST.get('part_code') quantity = int(request.POST.get('quantity', 1)) try: with transaction.atomic(): # 开启数据库事务 order = RepairOrder.objects.select_for_update().get(order_no=order_no) # 行级锁,防并发超领 part = Part.objects.select_for_update().get(code=part_code) # 同时锁配件行 if part.stock_quantity < quantity: raise ValueError(f"库存不足:{part.name} 仅剩{part.stock_quantity}个,需{quantity}个") # 扣减库存 part.stock_quantity -= quantity part.save() # 记录领用 PartUsage.objects.create( order=order, part=part, quantity_used=quantity ) return JsonResponse({'success': True, 'remaining': part.stock_quantity}) except RepairOrder.DoesNotExist: return JsonResponse({'error': '工单不存在'}, status=404) except Part.DoesNotExist: return JsonResponse({'error': '配件编码错误'}, status=400) except ValueError as e: return JsonResponse({'error': str(e)}, status=400)

关键参数与避坑点:

  • select_for_update()是核心:它对数据库行加锁,当两个技师同时扫同一个刹车片时,第二个请求会阻塞等待第一个事务结束,避免超卖;
  • transaction.atomic()确保扣库存和记日志要么全成功,要么全失败——如果扣完库存后日志写入失败,事务回滚,库存自动恢复;
  • quantity强制转为int,防止字符串注入攻击(如传入"1; DROP TABLE part;");
  • 返回remaining值,前端可实时刷新库存显示,消除技师疑虑。

4.3 用Django信号实现“工单撤销即配件归还”:补上最后一环

工单可能因客户取消或检测后无需维修而撤销,此时已领用的配件必须自动退库。用信号监听工单状态变更:

# signals.py(续) from django.db.models.signals import post_save from .models import RepairOrder, PartUsage, Part @receiver(post_save, sender=RepairOrder) def return_parts_on_cancel(sender, instance, **kwargs): if instance.status == 'received': # 工单重置为“已接车”,视为撤销 # 查找该工单所有已领用配件 usages = PartUsage.objects.filter(order=instance) for usage in usages: # 归还库存 part = usage.part part.stock_quantity += usage.quantity_used part.save() # 删除领用记录 usages.delete()

注意:此信号只在状态变回'received'时触发,不处理其他状态(如'completed'后不退库),符合汽修业务规则。

5. 汽车修理管理系统避坑指南:5条血泪经验,每条都来自真实翻车现场

5.1 现象:技师用安卓平板点“维修中”后,页面显示成功,但后台状态仍是“已派工”

原因:前端JavaScript发送AJAX请求时,未处理HTTP 500错误,且未校验返回JSON结构。当Django视图因数据库连接超时抛出异常,前端收到空响应,误判为成功。
解决:在AJAX回调中强制检查response.status === 200且response.data.success === true,否则弹窗提示“状态更新失败,请重试”,并记录浏览器Console日志供排查。

5.2 现象:同一辆车两天内进厂两次,第二次接车时系统报“车牌号已存在”,但前台坚持说“第一次是隔壁店修的,我们没录过”

原因:Vehicle.license_plate的unique=True约束跨全库生效,但汽修厂常有“合作店代录”场景,需允许同一车牌在不同门店独立存在。
解决:增加shop_id字段(外键关联门店表),将唯一约束改为unique_together = ('license_plate', 'shop_id'),并修改Admin搜索逻辑,使搜索默认限定当前门店。

5.3 现象:财务导出月度结算报表时,发现某工单金额为0,但维修项目里明明写了“喷漆费800元”

原因:RepairItem模型缺少价格字段,财务人员在Excel里手工补价,但未同步到系统,导致导出时取不到价格。
解决:在RepairItem中增加unit_price和total_amount字段(后者由前两者计算得出),并在Admin中设为只读,强制价格在创建时录入。导出报表直接取total_amount。

5.4 现象:车间大屏显示“今日待检工单12个”,但质检员点开只有8个,4个显示“权限不足”

原因:Django默认Admin无行级权限控制,所有质检员能看到全部工单,但通过自定义View做了权限过滤,而大屏数据源未同步该逻辑。
解决:统一数据出口,大屏调用专用API(如/api/daily-inspection-tasks/),该API内部调用与质检员View相同的权限校验函数,确保数据源一致。

5.5 现象:微信公众号绑定客户后,客户在公众号查工单,看到“预计交车时间:明天下午”,但实际技师还没开始修

原因:“预计交车时间”字段在RepairOrder中硬编码为received_at + timedelta(hours=24),未考虑技师排班、配件到货等动态因素。
解决:删除硬编码字段,改为在API中实时计算:取该工单所有维修项目的estimated_hours总和,乘以当前车间平均工时系数(如1.3),再叠加配件采购周期(查Part表的lead_time_days字段),动态生成预计时间。

6. 让系统真正扎根车间:用离线优先策略解决老旧设备与网络抖动的终极方案

6.1 为什么“全云端”在汽修厂是伪命题?——三类真实环境制约

我参与过的12个汽修系统落地项目里,8个在首年遭遇过“系统不可用”危机,根因全是基础设施:

  • 设备老化:37%的乡镇修理厂仍在用Windows XP/7的收银一体机,Chrome最新版无法安装;
  • 网络脆弱:车间Wi-Fi穿墙后信号强度常低于-85dBm,TCP重传率超40%,HTTP请求超时成常态;
  • 操作习惯:老师傅拒绝记住账号密码,要求“开机即用”,且反感频繁点击“同步数据”。

因此,真正的“可用”不是功能多炫,而是当网线被叉车碾断时,技师仍能继续开单、领料、完工——数据在本地暂存,网络恢复后自动追平。

6.2 用SQLite WAL模式+本地缓存实现离线操作:3个关键技术点

第一,启用SQLite WAL(Write-Ahead Logging)模式:
默认SQLite的DELETE/UPDATE会锁整张表,10个技师同时操作工单表时必然排队。WAL模式允许多读一写并发,将锁粒度降到行级。在Djangosettings.py中配置:

# settings.py DATABASES = { 'default': { 'ENGINE': 'django.db.backends.sqlite3', 'NAME': BASE_DIR / 'db.sqlite3', 'OPTIONS': { 'timeout': 20, # 数据库锁等待20秒,避免死锁 }, } } # 应用启动时执行WAL开启(放入apps.py的ready方法) def ready(self): from django.db import connection with connection.cursor() as cursor: cursor.execute("PRAGMA journal_mode=WAL;") # 关键:开启WAL cursor.execute("PRAGMA synchronous=NORMAL;") # 降低磁盘同步频率,提升写入速度

第二,设计离线操作队列(OfflineQueue)模型:
所有需联网的操作(如扫码领料、状态更新)不直连服务器,而是先写入本地队列,再由后台线程异步提交:

# models.py(续) class OfflineQueue(models.Model): action_type = models.CharField(max_length=20) # 'scan_part', 'update_status' payload = models.JSONField() # 存储原始请求参数,如{'order_no':'RO2024001','part_code':'BS-001'} created_at = models.DateTimeField(auto_now_add=True) synced = models.BooleanField(default=False) error_message = models.TextField(blank=True)

第三,实现双通道同步引擎:
编写管理命令python manage.py sync_offline,它按优先级处理队列:

  1. 先同步“状态变更”类(高优先级,影响车间调度);
  2. 再同步“配件领用”类(中优先级,影响库存);
  3. 最后同步“客户备注”类(低优先级,不影响作业)。
    同步失败时,error_message记录具体错误(如“网络超时”),并保持synced=False,下次重试。

6.3 给老师傅的终极交互方案:微信扫码即用,零安装、零学习成本

所有技术终要回归人。我们最终交付的不是一套Web系统,而是一个微信小程序码:

  • 前台文员扫一下,填车牌、客户、接车时间,生成工单号;
  • 技师扫工单号二维码,页面自动加载该工单所有维修项目,点击“开始维修”即本地记录,网络恢复后静默同步;
  • 仓管扫配件二维码,输入数量,库存实时扣减(离线时写入本地队列)。

小程序前端用Taro框架,后端API完全复用Django现有接口,仅增加/api/offline/sync/专用同步端点。测试数据显示:在-90dBm弱网环境下,扫码操作平均耗时1.2秒(含本地写入),比传统Web页面快3.8倍。

最后说句实在话:做汽修系统三年,我删掉了所有花哨的“AI故障预测”模块,把90%精力花在让老师傅愿意多点一次屏幕。因为真正的数字化,不是把扳手换成触控笔,而是让每一次点击,都比递一张纸条更快、更准、更少出错。希望帮到你。

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

RELATED READING

延伸阅读

更多一线实战笔记与深度复盘,助您持续精进