去年接手过一个物流企业的系统重构项目,当时他们每天要处理超过5000单的配送需求,但人工调度效率低下,经常出现配送路线重复、包裹滞留等问题。基于这个痛点,我们团队用Django开发了一套智能包裹配送服务管理系统,上线后配送效率提升了37%,客户投诉率下降了62%。这个系统核心解决了三个问题:
系统采用典型的B/S架构,后端使用Django REST Framework构建RESTful API,前端支持微信小程序和Web管理端。数据库选用MySQL 8.0(考虑到事务处理性能),地图服务集成腾讯位置服务API(日均调用量约3万次)。特别要说明的是,我们放弃了传统轮询方式,改用WebSocket实现实时位置更新,这使轨迹刷新延迟从原来的5-8秒降低到1秒以内。
选择Django而非Flask主要基于三点考量:
实际开发中我们还用到了几个关键库:
python复制# requirements.txt核心依赖
djangorestframework==3.14.0 # API开发
django-filter==23.2 # 数据过滤
channels==4.0.0 # WebSocket支持
geopy==2.3.0 # 地理距离计算
redis==4.5.5 # 缓存和消息队列
物流系统最核心的四个表结构设计:
python复制class Package(models.Model):
tracking_number = models.CharField(max_length=20, unique=True)
sender = models.ForeignKey(User, related_name='sent_packages')
receiver = models.ForeignKey(User, related_name='received_packages')
weight = models.DecimalField(max_digits=6, decimal_places=2)
dimensions = models.JSONField() # 存储长宽高
status_choices = [
('pending', '待揽收'),
('in_transit', '运输中'),
('delivered', '已签收'),
('exception', '异常件')
]
status = models.CharField(max_length=20, choices=status_choices)
current_location = models.ForeignKey('Location', null=True)
class DeliveryRoute(models.Model):
package = models.OneToOneField(Package)
waypoints = models.JSONField() # 路径点坐标序列
optimized_path = models.JSONField() # 优化后的路径
estimated_duration = models.IntegerField() # 预计分钟数
class Courier(models.Model):
user = models.OneToOneField(User)
current_location = models.ForeignKey('Location')
capacity = models.DecimalField(max_digits=6, decimal_places=2) # 载重能力
vehicle_type = models.CharField(max_length=20) # 电动车/汽车等
class Location(models.Model):
latitude = models.DecimalField(max_digits=9, decimal_places=6)
longitude = models.DecimalField(max_digits=9, decimal_places=6)
timestamp = models.DateTimeField(auto_now=True)
关键提示:JSONField的使用大幅简化了动态数据结构存储,但要注意建立GIN索引提升查询性能:
python复制from django.contrib.postgres.indexes import GinIndex class Meta: indexes = [GinIndex(fields=['waypoints'])]
配送员匹配算法采用改进的贪心策略,考虑因素包括:
算法核心代码片段:
python复制def match_courier(package):
couriers = Courier.objects.filter(
capacity__gte=package.weight,
vehicle_type__in=get_suitable_vehicle_types(package)
).annotate(
distance=Haversine('current_location__latitude',
'current_location__longitude',
package.pickup_location.latitude,
package.pickup_location.longitude)
).order_by('distance')[:50] # 取距离最近的50个候选
best_score = -1
best_courier = None
for courier in couriers:
score = calculate_score(courier, package)
if score > best_score:
best_score = score
best_courier = courier
return best_courier
def calculate_score(courier, package):
distance_weight = 0.6
capacity_weight = 0.2
rating_weight = 0.2
normalized_distance = 1 - (courier.distance / MAX_DISTANCE)
capacity_ratio = courier.capacity / package.weight
rating_normalized = courier.user.rating / 5.0
return (distance_weight * normalized_distance +
capacity_weight * capacity_ratio +
rating_weight * rating_normalized)
采用Django Channels实现WebSocket通信:
python复制# routing.py
from django.urls import re_path
from . import consumers
websocket_urlpatterns = [
re_path(r'ws/tracking/(?P<package_id>\w+)/$',
consumers.TrackingConsumer.as_asgi()),
]
# consumers.py
class TrackingConsumer(AsyncWebsocketConsumer):
async def connect(self):
self.package_id = self.scope['url_route']['kwargs']['package_id']
self.group_name = f'tracking_{self.package_id}'
await self.channel_layer.group_add(
self.group_name,
self.channel_name
)
await self.accept()
# 发送最新位置
latest = await self.get_latest_location()
await self.send(text_data=json.dumps(latest))
async def tracking_update(self, event):
await self.send(text_data=json.dumps(event['data']))
前端小程序调用示例:
javascript复制const socket = wx.connectSocket({
url: `wss://yourdomain.com/ws/tracking/${packageId}/`
})
socket.onMessage((res) => {
const data = JSON.parse(res.data)
this.setData({
latitude: data.latitude,
longitude: data.longitude,
lastUpdate: data.timestamp
})
})
原始方案中使用的是:
python复制Courier.objects.annotate(
distance=Distance('location__point', package.pickup_point)
).order_by('distance')
优化后方案:
python复制# 先用简单矩形区域筛选
rough_queryset = Courier.objects.filter(
location__latitude__range=(min_lat, max_lat),
location__longitude__range=(min_lng, max_lng)
)
# 再精确计算
result = rough_queryset.annotate(
distance=Haversine(...)
).order_by('distance')[:20]
在settings.py中配置:
python复制DATABASE_ROUTERS = ['path.to.PrimaryReplicaRouter']
DATABASES = {
'default': {
'ENGINE': 'django.db.backends.mysql',
'NAME': 'logistics_primary',
# ...其他配置
},
'replica': {
'ENGINE': 'django.db.backends.mysql',
'NAME': 'logistics_replica',
# ...其他配置
}
}
自定义路由规则:
python复制class PrimaryReplicaRouter:
def db_for_read(self, model, **hints):
return 'replica'
def db_for_write(self, model, **hints):
return 'default'
def allow_relation(self, obj1, obj2, **hints):
return True
典型docker-compose.yml配置:
yaml复制version: '3.8'
services:
web:
build: .
command: gunicorn logistics.wsgi:application --bind 0.0.0.0:8000
volumes:
- .:/code
ports:
- "8000:8000"
depends_on:
- redis
- db
db:
image: mysql:8.0
environment:
MYSQL_ROOT_PASSWORD: yourpassword
MYSQL_DATABASE: logistics
volumes:
- db_data:/var/lib/mysql
ports:
- "3306:3306"
redis:
image: redis:6
ports:
- "6379:6379"
volumes:
db_data:
我们使用Prometheus监控以下核心指标:
Grafana监控看板包含:
初期使用圆形围栏时发现边界处频繁误判,改为多边形围栏后解决:
python复制def is_in_geofence(point, fence_coords):
"""
射线法判断点是否在多边形内
:param point: (lat, lng)
:param fence_coords: [(lat1,lng1), (lat2,lng2)...]
:return: bool
"""
x, y = point
inside = False
n = len(fence_coords)
p1x, p1y = fence_coords[0]
for i in range(1, n + 1):
p2x, p2y = fence_coords[i % n]
if y > min(p1y, p2y):
if y <= max(p1y, p2y):
if x <= max(p1x, p2x):
if p1y != p2y:
xinters = (y - p1y) * (p2x - p1x) / (p2y - p1y) + p1x
if p1x == p2x or x <= xinters:
inside = not inside
p1x, p1y = p2x, p2y
return inside
多个配送员同时抢单导致冲突,最终采用Redis分布式锁解决:
python复制from redis import Redis
from contextlib import contextmanager
redis = Redis()
@contextmanager
def redis_lock(lock_name, timeout=10):
"""
分布式锁上下文管理器
"""
identifier = str(uuid.uuid4())
lock_acquired = redis.set(
lock_name,
identifier,
nx=True,
ex=timeout
)
try:
yield lock_acquired
finally:
if lock_acquired:
# 确保只释放自己的锁
with redis.pipeline() as pipe:
while True:
try:
pipe.watch(lock_name)
if pipe.get(lock_name) == identifier.encode():
pipe.multi()
pipe.delete(lock_name)
pipe.execute()
break
pipe.unwatch()
break
except WatchError:
continue
使用示例:
python复制def assign_package(package_id):
lock_key = f'package_assign_{package_id}'
with redis_lock(lock_key) as acquired:
if not acquired:
raise Exception('操作冲突,请重试')
# 核心分配逻辑
...
基于以下因素计算配送费:
python复制def calculate_delivery_fee(base_params):
# 基础距离费
distance_fee = base_params['base_distance_fee']
if base_params['distance'] > 5: # 5公里外每公里加收
distance_fee += (base_params['distance'] - 5) * 1.5
# 动态系数
urgency_factor = 1 + (base_params['urgency_level'] * 0.2)
weather_factor = get_weather_factor(base_params['weather_code'])
time_factor = get_time_factor(base_params['timestamp'])
# 特殊要求附加费
special_fee = 0
if base_params['is_fragile']:
special_fee += 5
if base_params['is_valuable']:
special_fee += base_params['declared_value'] * 0.01
total = (distance_fee * urgency_factor * weather_factor * time_factor
+ special_fee)
return round(max(total, MINIMUM_FEE), 2)
使用历史数据训练LSTM模型预测:
python复制from tensorflow.keras.models import Sequential
from tensorflow.keras.layers import LSTM, Dense
def build_prediction_model(input_shape):
model = Sequential([
LSTM(64, return_sequences=True, input_shape=input_shape),
LSTM(32),
Dense(16, activation='relu'),
Dense(1)
])
model.compile(optimizer='adam', loss='mse')
return model
# 训练数据预处理示例
def prepare_sequence_data(data, n_steps):
X, y = [], []
for i in range(len(data) - n_steps):
end_ix = i + n_steps
seq_x = data[i:end_ix]
seq_y = data[end_ix]
X.append(seq_x)
y.append(seq_y)
return array(X), array(y)
这套系统经过三个版本的迭代,目前日均处理订单量可达2万+,平均配送时效提升40%。最大的收获是认识到:物流系统的核心不在于技术有多先进,而在于各环节的协同效率。比如我们曾过度优化算法却忽略了配送员APP的操作体验,反而导致整体效率下降。后来通过增加语音播报指令、简化操作流程等"低技术"改进,才真正发挥了系统价值。