先聊一个我实际见过很多次的场景:一家做司机外包、代驾调度或者商务用车租赁的公司,手里握着几十上百个司机,订单完结后对司机到底干得好不好,基本靠客户投诉和调度员印象,客户说“还行”就是5分,说“不靠谱”就是差评,司机的收入、排单、奖惩没有统一依据。等到司机数量上来、客户类型变多,管理就会失控。市面上的评分系统要么太重,要么不是为“人”这个服务对象设计的。于是我做了这套基于Python和Django的汽车司机租赁评分管理系统,核心是把司机当作一个持续波动的服务实体来管理,通过订单闭环采集多维度评分,再用滚动窗口聚合出每个司机的近期表现分,最终反哺运营的派单、预警和奖惩决策。
这篇文章不是只讲建几个模型、跑通一个Django项目,而是完整还原从需求分析到数据建模,到评分逻辑落地、运营后台配置,再到部署前安全处理的整个思考过程。适合两种读者:一种是刚学完Python基础、想用Django做一个真正完整项目的同学,另一种是公司内部需要一套小型车辆服务管理系统、正在为技术选型纠结的开发者。如果你只是想找一个能跑的Demo抄,我会尽量把代码写完整;但更希望你看懂每一步的取舍,因为这套系统真正值钱的地方不在那几个模型,而在“订单状态怎么流转”“评分怎么防止刷分”“沉淀下来的分数怎么被运营使用”这些容易忽略的细节。
1. 司机租赁管理,为什么评分会成为业务核心
先别急着开IDE,这一节我希望你脑子里先过一遍业务。很多项目做着做着就变形,根本原因是一开始没搞清楚系统到底要为谁服务、解决谁的什么问题。
1.1 三类角色背后的三套诉求
这套系统最基础的角色就三个:平台运营方(调度和管理人员)、司机、客户(叫车或租司机的一方)。三类人的诉求完全不同,系统设计时必须分别照顾到:
- 运营方要的是“可控”:想知道哪些司机最近表现下滑、哪些司机适合派给重要客户、司机被投诉后能不能快速触发干预。没有一个量化的分数,这些全靠拍脑袋。
- 司机要的是“公平”:评分必须跟着具体服务订单走,评价维度要透明,不能是客户随口一句“我不满意”就影响收入。司机希望能看到自己每一单的评价明细和综合分变化。
- 客户要的是“省心”:能提前选一个靠谱司机,服务完成后能方便地对服务态度、开车安全度、是否守时给出结构化反馈。
这三套诉求互相咬合,落到系统上就是一个评分闭环:客户下单,平台派司机,司机提供租赁或代驾服务,服务完成后客户按多个维度给司机打分,分数经过聚合形成司机的动态信用,运营再拿这个信用去决定未来的派单优先级和预警对象。
1.2 司机评分和商品评分,本质上是两码事
很多人把司机评分当成“给酒店打个分”来做,这是我最想纠正的认知偏差。一个商品上架后的质量是固定的,早期评价可以代表它的长期水平;但司机的服务状态是动态变化的,人会有状态波动、态度变化、技能成长。一个司机去年是全年度优秀,不代表他最近一个月没有服务下滑、没有客户投诉。
所以评分机制一定不能做“全部历史平均”。我之前见过一个方案,数据库里存了司机入行以来所有订单的评价,算出终身平均分挂在档案上,一个干了三年的老司机哪怕最近连续被投诉,综合分还是被早期的高分拉得很高,运营根本看不出问题。这种评分对管理毫无帮助。
正确的做法是滚动窗口评分,比如只取最近20单或30单做加权平均,让分数既能反映近况,又不会因为一两单偶然差评大幅跳动。这一点在后面的评分引擎部分我会给出具体实现。
1.3 第一版要打哪些分,维度怎么定
评分维度不能太感性。系统第一版我建议用五个固定维度,每个都是1到5分:
| 维度 | 权重 | 说明 |
|---|---|---|
| 服务态度 | 30% | 沟通是否礼貌、是否主动配合客户需求 |
| 准时守约 | 25% | 是否准点到达、是否按约定线路行驶 |
| 行车安全 | 25% | 有无急加速急刹车、是否遵守交规 |
| 车辆状况 | 20% | 车内整洁度、车况是否与描述一致 |
| 综合评价 | 不计入加权 | 客户可选填的主观印象和备注 |
综合维度不一定作为加权项,因为客户对“印象”的评分主观性很强,而且容易重复。我建议把综合分作为一个独立展示字段放在前台,用来让客户和运营快速感受口碑,但真正的派单决策使用加权平均分。
维度权重先不要做成后台可随意调整的动态规则表。第一版就用一个Python常量字典,放在配置或者服务模块里,运营真想调整时改配置发布一次即可。做成复杂的规则表意味着你要额外做规则版本管理、生效区间、异常兜底,对一个小团队的内部系统来说,性价比很低。
需要模型API调用? 免费领10W Token,多模型网关一键接入 Claude、DeepSeek 等主流模型。
2. Django项目骨架与开发环境:从零跑通比想象中多踩三个坑
我默认读者已经装好了Python 3.10以上版本。如果还没装,这里只说一句:安装时务必勾选Add Python to PATH,否则后面在命令行里输入python会提示找不到命令。这个坑在Windows上尤其常见,不少初学者卡在第一步就放弃了。
2.1 为什么这种系统选Django而不是Flask
司机租赁评分系统不是一个简单的API服务,它包含了用户体系、后台管理、数据建模、页面渲染、权限控制等一整套Web能力。如果选Flask,用户认证、ORM、表单校验、Admin后台都要自己从零拼,工作量会大很多;而Django内置了Admin后台和完整的ORM,天然适合这种“业务逻辑清晰、需要后台支撑的管理类系统”。
另一个实用考量是Django的ORM对复杂查询支持得很好。比如后面要算“近30单的加权平均分”“最近一周投诉最多的司机”,用Django的ORM写出来可读性很强,出了问题也容易排查。Python的Web框架选择很多,但作为一套要长期维护的内部管理工具,Django的成熟生态和丰富文档能替团队省掉大量隐性成本。
2.2 项目创建的具体流程和目录规划
我通常会把所有项目放在一个专用目录下,用虚拟环境隔离依赖。这里给出完整命令序列:
bash复制mkdir driver_lease_project
cd driver_lease_project
python -m venv venv
# Windows激活虚拟环境
venv\Scripts\activate
# macOS/Linux激活虚拟环境
source venv/bin/activate
# 安装Django 4.2 LTS版本
pip install django==4.2
# 创建项目
django-admin startproject config .
# 创建两个业务app:accounts处理用户与档案,lease处理订单与评分
python manage.py startapp accounts
python manage.py startapp lease
这里有几个跟普通教程不一样的细节。第一,项目目录我命名为config而不是driver_lease,因为driver_lease后面会被当成业务app名使用,Django项目根配置和业务模块重名会让import产生混乱。第二,我把accounts和lease拆成了两个app,划分标准是领域:账号体系管“谁是谁”,业务体系管“干了什么”。这样后面加新功能时,比如要增加司机排班,直接在lease旁边加一个schedule app就行。
2.3 settings.py里必须提前处理的配置
如果你跟着做过Django教程,应该会看到settings.py里有默认的INSTALLED_APPS。把新创建的app加进去,另外强烈建议在项目一开始就设置好这三项:
python复制# config/settings.py
INSTALLED_APPS = [
'django.contrib.admin',
'django.contrib.auth',
'django.contrib.contenttypes',
'django.contrib.sessions',
'django.contrib.messages',
'django.contrib.staticfiles',
'accounts', # 用户与档案
'lease', # 订单与评分
]
AUTH_USER_MODEL = 'accounts.User'
LANGUAGE_CODE = 'zh-hans'
TIME_ZONE = 'Asia/Shanghai'
USE_I18N = True
USE_TZ = True
这里最关键的一行是AUTH_USER_MODEL。Django默认的User模型不包含user_type(角色类型)和phone(手机号)这两个司机租赁场景的刚需字段,所以要自定义用户模型。而Django官方文档明确建议:自定义用户模型一定要在第一次makemigrations之前配置好,否则中途再换用户模型会导致迁移链非常混乱,轻则重置数据库,重则要把项目早期迁移全部推倒重来。
有个容易被新手忽略的点:我已经设置了USE_TZ = True,上面又写了TIME_ZONE = 'Asia/Shanghai'。Django的规则是,数据库里统一存UTC时间,展示时按当前时区转成本地时间。理由很简单:如果以后服务器跑到海外或用了云厂商的UTC默认配置,所有业务时间戳都不会出现混乱。
3. 数据模型设计:把司机、订单、评分翻译成一张张表
模型设计是这套系统的地基,地基歪了后面所有查询和业务逻辑都别扭。我在设计时没有一上来就堆字段,而是先把一条核心数据链路画出来:客户和司机都继承自用户基类,一次服务生成一个租赁订单,订单完结后生成一条评分记录,评分记录经过聚合计算回写到司机的档案分数上。
3.1 自定义用户模型和司机档案
accounts/models.py,定义用户模型和档案模型:
python复制# accounts/models.py
from django.db import models
from django.contrib.auth.models import AbstractUser
class User(AbstractUser):
USER_TYPE_CHOICES = (
('platform', '平台运营'),
('driver', '司机'),
('client', '客户'),
)
user_type = models.CharField('用户类型', max_length=20, choices=USER_TYPE_CHOICES)
phone = models.CharField('手机号', max_length=20, unique=True)
id_card_no = models.CharField('身份证号', max_length=18, blank=True)
class Meta:
db_table = 'auth_user_ext'
def __str__(self):
return f'{self.username}({self.get_user_type_display()})'
这里有一个细节值得多说两句:phone设置了unique=True,因为司机注册、客户下单、运营找回账号都需要手机号作为强凭证。身份证号则不要求必填,只在司机实名认证时写入,避免全平台账号体系一启动就背上敏感信息采集的合规压力。
司机档案放在扩展表里而不是直接把字段堆到User上,是因为用户体系的字段(登录名、密码、手机号)和业务身份的字段(驾龄、车牌、接单状态)变更频率完全不一样。派单时主要看状态和评分,没必要每次都把用户大表整个拖出来。
python复制# accounts/models.py
class DriverProfile(models.Model):
DRIVER_STATUS_CHOICES = (
('available', '可接单'),
('busy', '服务中'),
('offline', '已下线'),
('suspended', '已停用'),
)
user = models.OneToOneField(
User, on_delete=models.CASCADE,
related_name='driver_profile', verbose_name='关联用户'
)
real_name = models.CharField('真实姓名', max_length=50)
license_no = models.CharField('驾驶证号', max_length=50, unique=True)
driving_years = models.PositiveIntegerField('驾龄/年', default=0)
car_model = models.CharField('车辆型号', max_length=50, blank=True)
car_plate = models.CharField('车牌号', max_length=20, blank=True)
status = models.CharField('司机状态', max_length=20, choices=DRIVER_STATUS_CHOICES, default='offline')
# 冗余聚合分,避免频繁大表聚合查询
avg_score = models.DecimalField('滚动均分', max_digits=3, decimal_places=2, default=0)
score_cnt = models.PositiveIntegerField('参与评分的订单数', default=0)
remark = models.CharField('运营备注', max_length=255, blank=True)
created_at = models.DateTimeField('创建时间', auto_now_add=True)
updated_at = models.DateTimeField('更新时间', auto_now=True)
为什么不加司机档案模型,也可以直接给User加字段?从功能上看当然可行,但从职责划分上会很脏。司机有服务状态、接单能力评估、排班偏好,这些是“业务身份”不是“平台账号”,放在一个表里未来一旦要支持一个司机兼职做客户,或者一个账号切换多个身份,你的用户表会被业务字段塞到爆炸。
3.2 租赁订单模型,职责不只是记录一次交易
很多初学者设计订单表,就是记录谁租了谁的车、几点到几点、多少钱,完事。但司机租赁的订单还要承担“可评价”的边界判定:没完成的订单能不能评?司机被放鸽子后能不能申诉差评?这些问题的答案都藏在订单状态机里。
lease/models.py,订单模型:
python复制# lease/models.py
import uuid
from django.db import models
from django.conf import settings
class LeaseOrder(models.Model):
ORDER_STATUS_CHOICES = (
('pending', '待服务'),
('in_service', '服务中'),
('completed', '已完成'),
('cancelled', '已取消'),
('disputed', '争议中'),
)
order_no = models.UUIDField('订单号', default=uuid.uuid4, unique=True, editable=False)
client = models.ForeignKey(
settings.AUTH_USER_MODEL,
on_delete=models.PROTECT,
related_name='client_orders',
verbose_name='客户'
)
driver = models.ForeignKey(
settings.AUTH_USER_MODEL,
on_delete=models.PROTECT,
related_name='driver_orders',
verbose_name='司机'
)
driver_name_snapshot = models.CharField('司机姓名快照', max_length=50, blank=True)
car_plate_snapshot = models.CharField('车牌快照', max_length=20, blank=True)
service_start = models.DateTimeField('服务开始时间')
service_end = models.DateTimeField('服务结束时间', null=True, blank=True)
fee_amount = models.DecimalField('订单金额', max_digits=10, decimal_places=2, default=0)
status = models.CharField('订单状态', max_length=20, choices=ORDER_STATUS_CHOICES, default='pending')
is_rated = models.BooleanField('是否已评价', default=False)
created_at = models.DateTimeField('创建时间', auto_now_add=True)
这里有两个细节,解释一下为什么这么设计:
第一个是on_delete=models.PROTECT。订单关联了客户和司机,如果某个员工账号因为离职被删除,订单记录如果跟着级联删除,历史纠纷和审计链路就断了。PROTECT会用外键完整性拦住删除操作,逼着你把账号标记为“停用”而不是物理删除,这是管理系统最稳妥的做法。
第二个是driver_name_snapshot和car_plate_snapshot这两个“快照字段”。司机改名字、换车牌是会发生的事,但历史订单里当时服务的司机是谁、开的是哪台车,这个事实不能跟着司机档案变动。下单时把关键信息复制一份到订单上,后续统计“上个月这台车被投诉了几次”时,才不会被档案更新污染。
3.3 评分表,用OneToOne锁死一单一评
评分记录不需要单独维护一个“评价单号”,因为它的生命周期完全依附于订单。一张订单只允许被评一次,所以评分表用OneToOneField关联订单比普通ForeignKey更合适,数据库层面直接杜绝了重复评分:
python复制# lease/models.py
class Rating(models.Model):
RATING_STATUS_CHOICES = (
('normal', '正常'),
('flagged', '已标记争议'),
('hidden', '已隐藏'),
)
order = models.OneToOneField(
LeaseOrder, on_delete=models.CASCADE,
related_name='rating', verbose_name='关联订单'
)
driver = models.ForeignKey(
settings.AUTH_USER_MODEL,
on_delete=models.CASCADE,
related_name='driver_ratings',
verbose_name='被评司机'
)
client = models.ForeignKey(
settings.AUTH_USER_MODEL,
on_delete=models.SET_NULL,
null=True, blank=True,
related_name='client_ratings',
verbose_name='评价客户'
)
service_score = models.PositiveSmallIntegerField('服务态度分', default=5)
punctuality_score = models.PositiveSmallIntegerField('准时守约分', default=5)
safety_score = models.PositiveSmallIntegerField('行车安全分', default=5)
vehicle_score = models.PositiveSmallIntegerField('车辆状况分', default=5)
comment = models.TextField('文字评价', blank=True)
status = models.CharField('评价状态', max_length=20, choices=RATING_STATUS_CHOICES, default='normal')
created_at = models.DateTimeField('评价时间', auto_now_add=True)
class Meta:
verbose_name = '服务评分'
verbose_name_plural = '服务评分'
ordering = ['-created_at']
客户评价字段为什么要保留外键并且允许为空?因为有的企业客户下单人是行政助理,真正乘坐司机车的是老板;或者平台做匿名评价保护时,需要在业务上隐藏评价者身份。员工被删除时用SET_NULL保留评分记录本身,评价历史不会被抹掉。
字段默认值设为5有一个运营层面的考虑:评分页如果客户忘记点某个维度的星星,默认按5分算,比默认0分对司机友好得多。司机整体评分偏低会打击接单意愿,这在小规模车队里是很现实的团队管理问题。当然,如果希望客户必须显式选择每项打分,可以在前端设置必填校验,后台字段默认值只作为兜底方案。
3.4 先有鸡还是先有蛋:迁移顺序怎么定
数据模型设计完不等于能直接跑,迁移顺序是Django项目里非常容易踩的坑。因为LeaseOrder的client字段外键指向自定义的accounts.User,所以必须先让accounts完成迁移并注册到Django的ORM中,然后lease应用才能正确解析外键。
推荐操作顺序:
bash复制python manage.py makemigrations accounts
python manage.py makemigrations lease
python manage.py migrate
先只生成accounts的迁移,再生成lease的迁移。如果你用makemigrations不带app名称一口气全生成,有时候Django能自动理顺依赖,有时候会生成一个依赖顺序有问题的迁移文件。在两个app有外键关联时,分开执行是稳定不出错的做法。
4. 评分引擎与订单状态闭环:核心实现,不只是存一条记录
现在到了这个项目最有含金量的部分:怎么把“一笔订单完成了”这个业务事件,安全地转换成“一条评分入库、一个司机均分更新、可能触发一条风控预警”的结果。我建议把这些逻辑放在独立的服务层文件里,不要散落在views.py的函数中。
4.1 订单状态流转,用模型方法而不是视图直接改状态
如果视图里随便写LeaseOrder.objects.filter(id=id).update(status='completed'),会有一个问题:任何状态的订单都能被强行改成“已完成”。比如一个处于“已取消”状态的订单被误操作改成已完成,系统就会允许客户对它进行评分,业务上完全说不通。
正确做法是把状态流转封装成模型方法:
python复制# lease/models.py
class LeaseOrder(models.Model):
# ... 上面已定义字段省略 ...
def start_service(self):
if self.status != 'pending':
raise ValueError('只有待服务订单才能开始服务')
self.status = 'in_service'
self.save(update_fields=['status', 'updated_at'])
def complete_service(self):
if self.status != 'in_service':
raise ValueError('只有服务中的订单才能标记完成')
self.status = 'completed'
self.service_end = timezone.now()
self.save(update_fields=['status', 'service_end', 'updated_at'])
def cancel(self):
if self.status in ('completed', 'cancelled'):
raise ValueError('该状态订单不允许取消')
self.status = 'cancelled'
self.save(update_fields=['status', 'updated_at'])
每个状态流转方法里都先校验“当前状态是否允许执行这个动作”,不合法直接抛异常。状态机虽然只有五态,但把规则收口在模型层后,视图、管理命令、定时任务调用时都是同一套约束,不会再出现接口直接改库导致脏状态的问题。
4.2 提交评分的服务层逻辑
用户点击提交评分后,视图会向上图这样调用服务层。评分提交逻辑被单独抽成一个静态方法,方便管理后台和后续的手机端接口复用:
python复制# lease/services.py
from django.db import transaction
from django.core.exceptions import PermissionDenied
from .models import Rating, LeaseOrder, DriverScoreSnapshot
# 维度权重,第一版先写成常量
DIMENSION_WEIGHTS = {
'service_score': 0.30,
'punctuality_score': 0.25,
'safety_score': 0.25,
'vehicle_score': 0.20,
}
def _clamp_score(value):
"""防止调用方传入越界数值,强约束在1~5之间"""
try:
value = int(value)
except (TypeError, ValueError):
return 5
return max(1, min(5, value))
class RatingService:
@staticmethod
@transaction.atomic
def submit_rating(order, client_user, payload):
# 1. 订单必须是已完成状态
if order.status != 'completed':
raise PermissionDenied('只有已完成订单可以评价')
# 2. 只有下单客户本人(或员工代录)才能评价
if order.client_id != client_user.id:
raise PermissionDenied('无权评价该订单')
# 3. 防止重复评分(数据库层也有OneToOne兜底)
if hasattr(order, 'rating'):
raise PermissionDenied('该订单已评价')
rating = Rating.objects.create(
order=order,
driver_id=order.driver_id,
client=client_user,
service_score=_clamp_score(payload.get('service_score', 5)),
punctuality_score=_clamp_score(payload.get('punctuality_score', 5)),
safety_score=_clamp_score(payload.get('safety_score', 5)),
vehicle_score=_clamp_score(payload.get('vehicle_score', 5)),
comment=payload.get('comment', '')[:500],
)
# 标记订单已评价,避免列表页再接一次查询判断
order.is_rated = True
order.save(update_fields=['is_rated', 'updated_at'])
# 聚合更新司机最新评分
RatingService.recalc_driver_avg_score(order.driver_id)
return rating
@staticmethod
def recalc_driver_avg_score(driver_id, recent_n=30):
"""最近N单滚动加权平均分"""
recent_ratings = list(
Rating.objects
.filter(driver_id=driver_id, status='normal')
.order_by('-created_at')[:recent_n]
)
if not recent_ratings:
return 0
total_raw = 0
for r in recent_ratings:
raw = (
r.service_score * DIMENSION_WEIGHTS['service_score'] +
r.punctuality_score * DIMENSION_WEIGHTS['punctuality_score'] +
r.safety_score * DIMENSION_WEIGHTS['safety_score'] +
r.vehicle_score * DIMENSION_WEIGHTS['vehicle_score']
)
total_raw += raw
avg = round(total_raw / len(recent_ratings), 2)
profile = DriverProfile.objects.get(user_id=driver_id)
profile.avg_score = avg
profile.score_cnt = len(recent_ratings)
profile.save(update_fields=['avg_score', 'score_cnt', 'updated_at'])
return avg
这段实现我实际跑过很多遍,有三点经验分享:
第一,第2步的权限校验为什么是order.client_id != client_user.id而不是比较对象?Django的client_id指的是当前订单里存储的外键主键,避免为了权限判断再加载一次完整的User对象,这样省一次数据库查询。在大表高频场景下,凡是能比ID就绝不整表加载。
第二,聚合更新是实时的,不是异步任务。单个评分产生的计算量很小,在订单量没到日均几万之前,实时更新司机档案上的聚合分完全撑得住,而且数据一致性最好。如果订单量真的大了,可以把这个动作改成通过消息队列异步处理,或者每天凌晨用定时任务批量重算,但那至少是系统在日均千单以后才需要考虑的事。
第三,评分插件里维度打分用的是PositiveSmallIntegerField,它本身不能限制最大值是5,所以_clamp_score这个函数非常重要。无论前端还是管理员操作,只要传入越界分数,强制收敛到1~5。这是接口层防御,不是前端校验的替代品。
4.3 最近30单滚动均分的微妙之处
为什么要30单而不是10单、100单?这是我在实际运营中观察出来的:10单太敏感,司机一天接三单,十单就是三天的表现,偶发投诉会让分数大幅下跌;100单太迟钝,司机出问题后运营可能要过半个月才在分数上看到变化;30单对于代驾、租赁场景,大约是司机一到两周的工作量,既能反映近期趋势,又有一定稳定性。
不过这里有一个坑:如果直接用order_by('-created_at')[:30]取最近的30条已评价订单,那么司机刚入行、只服务了5单时,30单滚动区间里实际只有5条数据。这时候评分偏向早期单量不足时的波动,展示出来可能是一个4.95分或3.8分这种误导性很强的分数。我建议前端展示时,如果score_cnt小于10,旁边必须标注“新司机评分样本不足”,或者直接不显示具体分数,只显示“收录中”状态。
4.4 风控与防作弊,这个系统处理“烂评价”的思路
评分系统上线最怕的不是没人评,而是被恶意利用。这里我整理了一套按优先级排序的防御策略,先在服务层实现能代码化的部分:
- 只有“已完成”订单可以评价,评价入口关闭前先检查订单状态。
OneToOneField保证了数据库层无法重复评分。- 评论文本做敏感词过滤,主要是拦截联系方式、外链和辱骂内容。
- 司机可以对评价发起申诉,运营将评分标记为
flagged后,该评分暂不参与滚动聚合。 - 运营可以手动隐藏评价,打上
hidden状态后司机端列表不再展示。 - 同一客户对同一个司机的评价,如果一周内超过3次且近几次都是差评,后台自动给运营弹一个疑似恶意评价的提示。
最后一条需要额外说明:客户真实体验差连续给差评是完全合理的,不能机械地判定为恶意。这个提示的作用是提醒运营去人工看一下具体订单和聊天记录,而不是自动删除评价。
5. Admin后台与运营看板:让评分数据真正用起来
系统做完,数据躺在数据库里没有任何价值。Django自带Admin后台是这套评分系统“从能用变成好用”的关键杠杆,小团队甚至可以不写任何前端页面,直接在Admin里完成日常运营。
5.1 用Admin管理司机、订单和评分记录
lease/admin.py里可以这样配置:
python复制# lease/admin.py
from django.contrib import admin
from .models import LeaseOrder, Rating
from accounts.models import DriverProfile
@admin.register(LeaseOrder)
class LeaseOrderAdmin(admin.ModelAdmin):
list_display = ('order_no', 'client', 'driver', 'status', 'fee_amount', 'is_rated', 'created_at')
list_filter = ('status', 'is_rated', 'created_at')
search_fields = ('order_no', 'client__username', 'driver__username')
date_hierarchy = 'created_at'
raw_id_fields = ('client', 'driver')
readonly_fields = ('order_no', 'driver_name_snapshot', 'car_plate_snapshot')
actions = ['mark_as_completed']
@admin.action(description='将选中订单强制标记为已完成')
def mark_as_completed(self, request, queryset):
count = 0
for order in queryset:
try:
order.complete_service()
count += 1
except ValueError:
pass
self.message_user(request, f'成功标记 {count} 条订单')
有几个配置项真的就是为管理场景准备的:
raw_id_fields = ('client', 'driver')。司机和客户表数据量一大,外键默认的下拉框会把所有用户一次性加载到页面上,浏览器直接卡死。raw_id_fields会把下拉框变成输入框,管理员手动输入用户ID,或者点击放大镜搜索,对千级以上的用户表是刚需。
readonly_fields保护快照字段,防止运营不小心改掉历史订单上存储的司机姓名和车牌,审计数据不允许后台随便改。
date_hierarchy = 'created_at'会在Admin列表页顶部生成一个日期层级筛选器,按年、月、日逐步下钻,查看当天的待处理订单非常方便。
5.2 一键停用低分司机的Action操作
运营每天打开后台,最想做的事情是:把最近表现差的司机拉出来看一眼。DriverProfile列表页需要支持按评分排序。
accounts/admin.py:
python复制# accounts/admin.py
from django.contrib import admin
from django.contrib.auth.admin import UserAdmin as BaseUserAdmin
from django.contrib.auth.models import Group
from django.contrib.auth.admin import GroupAdmin as BaseGroupAdmin
from .models import User, DriverProfile
@admin.register(User)
class UserAdmin(BaseUserAdmin):
list_display = ('username', 'phone', 'user_type', 'is_staff', 'date_joined')
list_filter = ('user_type', 'is_staff', 'is_active')
fieldsets = (
(None, {'fields': ('username', 'password')}),
('个人资料', {'fields': ('phone', 'user_type', 'id_card_no')}),
('权限', {'fields': ('is_active', 'is_staff', 'is_superuser', 'groups')}),
)
search_fields = ('username', 'phone')
@admin.register(DriverProfile)
class DriverProfileAdmin(admin.ModelAdmin):
list_display = ('real_name', 'user', 'status', 'avg_score', 'score_cnt', 'driving_years', 'updated_at')
list_filter = ('status', 'avg_score')
search_fields = ('real_name', 'license_no', 'user__phone')
actions = ['suspend_drivers', 'available_drivers']
@admin.action(description='停用选中司机')
def suspend_drivers(self, request, queryset):
count = 0
for profile in queryset:
if profile.status != 'suspended':
profile.status = 'suspended'
profile.save(update_fields=['status', 'updated_at'])
count += 1
self.message_user(request, f'已停用 {count} 位司机')
@admin.action(description='将选中司机设为可接单')
def available_drivers(self, request, queryset):
updated = queryset.filter(status='suspended').update(status='available')
self.message_user(request, f'已恢复 {updated} 位司机')
后台操作的核心思想是“犯错留痕、恢复也留痕”。所有状态操作通过Action封装,不会出现运营直接改字段导致数据和状态机不一致的情况。低分司机不是自动停用,而是由人工判断后一键操作,在这个尺度上保留人审,比做成完全自动的风控更合适,毕竟司机服务问题往往需要结合具体投诉内容判断。
5.3 评分趋势看板:不写React也能做内部可视化
管理团队不一定需要花里胡哨的前端图表,但确实需要看评分趋势。第一版我建议不要引入前端图表库,直接在Django里写简单聚合统计视图就够了。
比如一个只显示文本数据的内部看板:
python复制# lease/views.py
from django.shortcuts import render
from django.utils import timezone
from datetime import timedelta
from django.db.models import Avg, Count
from .models import Rating, LeaseOrder
def dashboard(request):
week_ago = timezone.now() - timedelta(days=7)
low_score_drivers = DriverProfile.objects.filter(
avg_score__gte=0.01, avg_score__lt=4.0
).order_by('avg_score')[:20]
recent_ratings_count = Rating.objects.filter(
created_at__gte=week_ago, status='normal'
).count()
today_orders = LeaseOrder.objects.filter(
created_at__date=timezone.localdate()
).count()
context = {
'low_score_drivers': low_score_drivers,
'recent_ratings_count': recent_ratings_count,
'today_orders': today_orders,
}
return render(request, 'lease/dashboard.html', context)
在真实的公司内部,看板的意义首先是“今天有没有异常”,而不是数据可视化炫技。用Django模板加一个简单的表格页面,列出低于4分的司机、近7天评价数量、今日订单数,就能满足90%的需求。
如果你后期要做趋势图,也不一定要立刻上ECharts。第一版可以每天定时把聚合数据写到一个统计表里,然后用Django模板输出图表数据JSON,前端用开源的Chart.js绘制折线图,半小时就能搞定。
6. 从开发机到可用系统:部署与安全配置的最后一公里
项目在本地跑通,跟真正给运营团队用起来,中间还有几个坎。我整理了自己在部署这类项目时反复踩过的三个坑,以及对应的解决方案。
6.1 环境变量与敏感信息:密钥别写死在代码里
开发环境里你可以在settings.py里直接写SECRET_KEY='xxx',但代码一旦上传到公司Git仓库或者多人协作,这个密钥就泄露了。攻击者拿到SECRET_KEY可以伪造会话、篡改密码重置链接,整个系统的认证体系等于裸奔。
我推荐在项目根目录加一个.env文件,然后让settings.py读取:
bash复制pip install python-dotenv==1.0.0
项目根目录创建.env:
ini复制DJANGO_SECRET_KEY=your-real-secret-key-here
DJANGO_DEBUG=False
DJANGO_ALLOWED_HOSTS=your-company-domain.com,www.your-company-domain.com
DJANGO_DB_NAME=driver_lease
DJANGO_DB_USER=lease_user
DJANGO_DB_PASSWORD=complex-password
settings.py读取:
python复制# config/settings.py
import os
from dotenv import load_dotenv
load_dotenv()
SECRET_KEY = os.environ['DJANGO_SECRET_KEY']
DEBUG = os.environ.get('DJANGO_DEBUG', 'False') == 'True'
ALLOWED_HOSTS = os.environ.get('DJANGO_ALLOWED_HOSTS', '').split(',')
.env文件绝对不要提交到Git仓库,在.gitignore里加一行.env。这样换一台新开发机部署时,只需要复制一份.env模板并填上对应环境的密钥,代码仓库本身不包含任何环境差异。
6.2 DEBUG=False之后:静态文件与ALLOWED_HOSTS的连锁反应
本地调试设置DEBUG=True时没有感觉,一旦DEBUG=False,Django默认不再处理静态文件,页面上的CSS、JS、后端上传的图片全部404。同时任何不在ALLOWED_HOSTS里的Host请求都会直接报错。
解决办法是执行收集静态文件命令,然后在Nginx层托管:
bash复制python manage.py collectstatic --noinput
settings.py也需要配置STATIC_ROOT:
python复制STATIC_URL = '/static/'
STATIC_ROOT = BASE_DIR / 'staticfiles'
部署时Nginx把/static/开头的请求映射到staticfiles目录,其余请求反向代理给Django进程。这里最容易犯的错误是把collectstatic忘记了,上线后管理后台全部裸奔。我建议把它写进部署流程文档的必做清单里,每次发版先跑一遍再reload服务。
6.3 数据库选择:从SQLite切到PostgreSQL
Django默认SQLite作为本地开发数据库,零配置、开箱即用。但如果公司真的要同时几个人在线使用,SQLite的写锁问题和单机性能会成为瓶颈。
我把生产环境切成PostgreSQL的原因是:
- Django的JSONField等高级功能在PostgreSQL上支持得最好。
- 并发写入不会出现
database is locked。 - 后续即使要做数据分片、水平扩展,PostgreSQL生态的迁移路径也更清晰。
用PostgreSQL需要增改几个依赖和配置:
bash复制pip install psycopg2-binary==2.9.9
python复制DATABASES = {
'default': {
'ENGINE': 'django.db.backends.postgresql',
'NAME': os.environ['DJANGO_DB_NAME'],
'USER': os.environ['DJANGO_DB_USER'],
'PASSWORD': os.environ['DJANGO_DB_PASSWORD'],
'HOST': os.environ.get('DJANGO_DB_HOST', '127.0.0.1'),
'PORT': os.environ.get('DJANGO_DB_PORT', '5432'),
'CONN_MAX_AGE': 60,
'OPTIONS': {'connect_timeout': 5},
}
}
CONN_MAX_AGE: 60表示数据库连接可以复用60秒,减少每次请求都重新握手连接的开销。在长连接池的配合下,小规模管理系统的性能不需要再做额外优化。
6.4 一些我建议加的运营小功能
这里补充两个不需要单独写成一个H2章节但很实用的功能,它们能在真实运营中帮你省下大量人力。
第一个是定时发送低分报表。用Django自带的management command,可以注册一条自定义命令来统计本周低分司机,然后配合crontab每天早晨发送邮件给运营负责人:
python复制# lease/management/commands/send_daily_rating_report.py
from django.core.management.base import BaseCommand
from django.utils import timezone
from datetime import timedelta
from lease.models import Rating
from accounts.models import DriverProfile
class Command(BaseCommand):
help = '每天早上统计低分司机情况'
def handle(self, *args, **options):
week_ago = timezone.now() - timedelta(days=7)
new_low_score = DriverProfile.objects.filter(
avg_score__gte=0.01,
avg_score__lt=4.0,
updated_at__gte=week_ago
)
# 实际场景这里会把统计结果通过邮件或企业微信机器人推送
self.stdout.write(f'低分司机数量: {new_low_score.count()}')
第二个是评价后自动检查风险词。在RatingService.submit_rating里,提交评论后对文本做一次正则检查,如果命中“拒绝给钱”“私下加价”“绕路”这类词组,自动把评分状态置为flagged并提醒运营人工处理。这能显著缩短从客户投诉到平台响应的间隔。
这两个功能都不复杂,但它们决定了系统是“记录工具”还是“管理工具”。评分管理系统的终极价值是帮运营把注意力放到真正出问题的人和订单上,而不是让他们每天去翻一堆表格。
最后说一点个人体会。我在实际项目交付时发现,代码部分最耗时的从来不是模型或视图,而是业务规则的边界:什么样的订单才能被评价、司机申诉后怎么处理已聚合的分数、运营改状态的权限怎么收口。这些逻辑必须想清楚后落到模型和服务层里,往前端堆弹窗解决不了根本问题。这套系统目前是在一家小型车辆服务公司内部跑着的状态,日均处理订单百余单,评分数据稳定运转,运营已经养成了每天早上看低分司机报表的习惯。如果你正在做类似项目,建议也先把“订单状态下谁能评价”这一条最核心的业务规则想透,再动手写代码,后边会顺很多。
