架构实战:PHP构建同城跑腿配送系统全解析
本文探讨了使用PHP构建同城跑腿配送系统的关键技术方案。系统采用智能订单分配算法,综合考虑距离、负载、评分和技能等多维度因素实现最优派单;通过Swoole实现WebSocket实时位置追踪和消息推送;利用Redis GEO功能进行骑手位置管理。文章详细介绍了基于权重的骑手匹配算法和批量订单分配的KM算法实现,以及位置数据验证、心跳检测等核心功能。PHP凭借快速开发和丰富生态,结合合理架构设计,能够
同城跑腿配送系统作为O2O模式的典型代表,对后端服务的实时性、并发性和可扩展性提出了较高要求。PHP以其快速开发、生态丰富、部署简单的特点,在配送系统的初期建设和快速迭代中展现出独特优势。本文将深入探讨如何使用PHP构建一个高性能、高可用的同城跑腿配送系统。

一、智能订单分配系统实现
1.1 基于权重的骑手匹配算法
<?php
/**
* 订单分配服务类
* 实现多因子权重计算的智能派单
*/
class OrderDispatchService
{
private $riderModel;
private $orderModel;
private $mapService;
private $cache;
// 权重配置
const WEIGHT_DISTANCE = 0.4; // 距离权重
const WEIGHT_LOAD = 0.3; // 负载权重
const WEIGHT_RATING = 0.2; // 评分权重
const WEIGHT_SKILL = 0.1; // 技能权重
public function __construct()
{
$this->riderModel = new RiderModel();
$this->orderModel = new OrderModel();
$this->mapService = new MapService();
$this->cache = Cache::getInstance();
}
/**
* 智能分配订单
* @param array $order 订单信息
* @return array 分配结果
*/
public function dispatchOrder($order)
{
// 1. 获取附近可用骑手
$riders = $this->getAvailableRiders(
$order['pickup_location'],
$order['service_radius']
);
if (empty($riders)) {
return [
'success' => false,
'message' => '暂无可用骑手',
'code' => 1001
];
}
// 2. 计算每个骑手的综合得分
$riderScores = [];
foreach ($riders as $rider) {
$score = $this->calculateRiderScore($rider, $order);
$riderScores[$rider['id']] = $score;
}
// 3. 按得分排序并选择最优骑手
arsort($riderScores);
$bestRiderId = key($riderScores);
$bestRider = $this->riderModel->find($bestRiderId);
// 4. 执行分配
$result = $this->assignOrderToRider($order, $bestRider);
return [
'success' => $result,
'rider' => $bestRider,
'score' => $riderScores[$bestRiderId],
'message' => $result ? '分配成功' : '分配失败'
];
}
/**
* 获取附近可用骑手
*/
private function getAvailableRiders($location, $radius)
{
// 使用Redis GEO查询附近骑手
$redis = $this->cache->getRedis();
// 将骑手位置存入GEO集合
$key = 'riders:online';
// 执行GEO半径查询
$riderIds = $redis->geoRadius(
$key,
$location['lng'],
$location['lat'],
$radius,
'km',
['WITHDIST', 'ASC']
);
if (empty($riderIds)) {
return [];
}
// 获取骑手详细信息
$riders = [];
foreach ($riderIds as $item) {
$riderId = $item[0];
$distance = $item[1];
$rider = $this->riderModel->getRiderDetail($riderId);
if ($rider && $rider['status'] == 1) { // 状态:在线
$rider['distance'] = $distance;
$riders[] = $rider;
}
}
return $riders;
}
/**
* 计算骑手综合得分
*/
private function calculateRiderScore($rider, $order)
{
// 距离得分(距离越近得分越高)
$distanceScore = $this->calculateDistanceScore(
$rider['distance'],
$order['service_radius']
);
// 负载得分(订单越少得分越高)
$loadScore = $this->calculateLoadScore(
$rider['current_orders'],
$rider['max_orders']
);
// 评分得分(评分越高得分越高)
$ratingScore = $this->calculateRatingScore($rider['rating']);
// 技能匹配得分
$skillScore = $this->calculateSkillScore($rider, $order);
// 加权总分
$totalScore =
$distanceScore * self::WEIGHT_DISTANCE +
$loadScore * self::WEIGHT_LOAD +
$ratingScore * self::WEIGHT_RATING +
$skillScore * self::WEIGHT_SKILL;
return round($totalScore, 4);
}
/**
* 计算距离得分
*/
private function calculateDistanceScore($distance, $maxDistance)
{
// 归一化处理,距离越近得分越高
$normalized = 1 - min($distance / $maxDistance, 1);
// 使用sigmoid函数平滑处理
return 1 / (1 + exp(-10 * ($normalized - 0.5)));
}
/**
* 计算负载得分
*/
private function calculateLoadScore($current, $max)
{
if ($current >= $max) {
return 0;
}
$loadRatio = $current / $max;
// 负载越轻得分越高
return 1 - $loadRatio;
}
/**
* 计算评分得分
*/
private function calculateRatingScore($rating)
{
// 假设评分为1-5分,归一化到0-1
return $rating / 5;
}
/**
* 计算技能匹配得分
*/
private function calculateSkillScore($rider, $order)
{
// 订单类型需要匹配骑手技能
$requiredSkills = $order['required_skills'] ?? [];
if (empty($requiredSkills)) {
return 0.5; // 无特殊要求给基础分
}
$riderSkills = $rider['skills'] ?? [];
$matchCount = count(array_intersect($requiredSkills, $riderSkills));
if ($matchCount >= count($requiredSkills)) {
return 1; // 完全匹配
} elseif ($matchCount > 0) {
return 0.6; // 部分匹配
} else {
return 0; // 完全不匹配
}
}
/**
* 执行订单分配
*/
private function assignOrderToRider($order, $rider)
{
// 开启事务
$this->orderModel->beginTransaction();
try {
// 1. 更新订单状态
$orderUpdate = [
'status' => 2, // 已分配
'rider_id' => $rider['id'],
'assign_time' => date('Y-m-d H:i:s')
];
$this->orderModel->where('id', $order['id'])->update($orderUpdate);
// 2. 更新骑手状态
$riderUpdate = [
'current_orders' => $rider['current_orders'] + 1,
'status' => $rider['current_orders'] + 1 >= $rider['max_orders'] ? 2 : 1
// 状态:1空闲 2忙碌
];
$this->riderModel->where('id', $rider['id'])->update($riderUpdate);
// 3. 发送推送通知
$this->sendPushNotification($rider, $order);
// 4. 记录分配日志
$this->logDispatch($order['id'], $rider['id']);
// 提交事务
$this->orderModel->commit();
return true;
} catch (Exception $e) {
$this->orderModel->rollback();
Log::error('订单分配失败:' . $e->getMessage());
return false;
}
}
/**
* 发送推送通知
*/
private function sendPushNotification($rider, $order)
{
$pushData = [
'type' => 'new_order',
'order_id' => $order['id'],
'pickup_address' => $order['pickup_address'],
'delivery_address' => $order['delivery_address'],
'estimated_earnings' => $order['delivery_fee']
];
// 调用推送服务
$pushService = new PushService();
$pushService->sendToRider($rider['id'], '您有新的订单', $pushData);
}
/**
* 记录分配日志
*/
private function logDispatch($orderId, $riderId)
{
$logData = [
'order_id' => $orderId,
'rider_id' => $riderId,
'action' => 'assign',
'create_time' => date('Y-m-d H:i:s')
];
DB::table('dispatch_logs')->insert($logData);
}
}
1.2 批量订单分配优化算法
<?php
/**
* 批量订单分配优化器
* 使用KM算法(Kuhn-Munkres)解决最优分配问题
*/
class BatchOrderDispatcher
{
private $riders;
private $orders;
private $costMatrix;
private $rowCover;
private $colCover;
private $starMatrix;
private $primeMatrix;
/**
* 执行批量分配优化
* @param array $riders 骑手列表
* @param array $orders 订单列表
* @return array 最优分配方案
*/
public function dispatch($riders, $orders)
{
$this->riders = $riders;
$this->orders = $orders;
// 1. 构建成本矩阵
$this->buildCostMatrix();
// 2. 执行KM算法
$assignment = $this->kmAlgorithm();
// 3. 格式化分配结果
return $this->formatAssignment($assignment);
}
/**
* 构建成本矩阵
*/
private function buildCostMatrix()
{
$n = max(count($this->riders), count($this->orders));
$this->costMatrix = array_fill(0, $n, array_fill(0, $n, 0));
foreach ($this->riders as $i => $rider) {
foreach ($this->orders as $j => $order) {
// 计算分配成本(越小越好)
$cost = $this->calculateAssignmentCost($rider, $order);
$this->costMatrix[$i][$j] = $cost;
}
// 填充剩余列
for ($j = count($this->orders); $j < $n; $j++) {
$this->costMatrix[$i][$j] = 999999; // 虚拟订单,高成本
}
}
// 填充剩余行
for ($i = count($this->riders); $i < $n; $i++) {
for ($j = 0; $j < $n; $j++) {
$this->costMatrix[$i][$j] = 999999; // 虚拟骑手,高成本
}
}
}
/**
* 计算单个分配成本
*/
private function calculateAssignmentCost($rider, $order)
{
// 距离成本
$distance = $this->calculateDistance(
$rider['current_location'],
$order['pickup_location']
);
// 时间成本
$timeCost = $this->calculateTimeCost($rider, $order);
// 负载成本
$loadRatio = $rider['current_orders'] / $rider['max_orders'];
$loadCost = $loadRatio * 100;
// 技能匹配惩罚
$skillPenalty = $this->calculateSkillPenalty($rider, $order);
// 总成本(越小越好)
return $distance * 0.5 + $timeCost * 0.3 + $loadCost * 0.2 + $skillPenalty;
}
/**
* KM算法实现
*/
private function kmAlgorithm()
{
$n = count($this->costMatrix);
// 初始化标签
$labelR = array_fill(0, $n, 0); // 行标签
$labelC = array_fill(0, $n, 0); // 列标签
// 初始化行标签为行最大值
for ($i = 0; $i < $n; $i++) {
$labelR[$i] = max($this->costMatrix[$i]);
}
// 初始化匹配数组
$matchR = array_fill(0, $n, -1); // 行匹配的列
$matchC = array_fill(0, $n, -1); // 列匹配的行
for ($i = 0; $i < $n; $i++) {
$this->findAugmentingPath($i, $labelR, $labelC, $matchR, $matchC);
}
return ['matchR' => $matchR, 'matchC' => $matchC];
}
/**
* 寻找增广路径
*/
private function findAugmentingPath($startRow, &$labelR, &$labelC, &$matchR, &$matchC)
{
$n = count($this->costMatrix);
$slack = array_fill(0, $n, INF);
$prev = array_fill(0, $n, -1);
$inTreeR = array_fill(0, $n, false);
$inTreeC = array_fill(0, $n, false);
$inTreeR[$startRow] = true;
while (true) {
// 更新slack值
for ($j = 0; $j < $n; $j++) {
if (!$inTreeC[$j]) {
$gap = $labelR[$startRow] + $labelC[$j] - $this->costMatrix[$startRow][$j];
if ($gap < $slack[$j]) {
$slack[$j] = $gap;
$prev[$j] = $startRow;
}
}
}
// 寻找最小的slack
$minSlack = INF;
$minCol = -1;
for ($j = 0; $j < $n; $j++) {
if (!$inTreeC[$j] && $slack[$j] < $minSlack) {
$minSlack = $slack[$j];
$minCol = $j;
}
}
// 更新标签
for ($i = 0; $i < $n; $i++) {
if ($inTreeR[$i]) {
$labelR[$i] -= $minSlack;
}
}
for ($j = 0; $j < $n; $j++) {
if ($inTreeC[$j]) {
$labelC[$j] += $minSlack;
}
}
// 检查是否可以匹配
if ($matchC[$minCol] == -1) {
// 找到增广路径,进行匹配
$this->augment($minCol, $prev, $matchR, $matchC);
return;
} else {
// 继续扩展树
$matchedRow = $matchC[$minCol];
$inTreeC[$minCol] = true;
$inTreeR[$matchedRow] = true;
$startRow = $matchedRow;
// 重置slack
for ($j = 0; $j < $n; $j++) {
if (!$inTreeC[$j]) {
$gap = $labelR[$startRow] + $labelC[$j] - $this->costMatrix[$startRow][$j];
if ($gap < $slack[$j]) {
$slack[$j] = $gap;
$prev[$j] = $startRow;
}
}
}
}
}
}
/**
* 执行增广
*/
private function augment($col, $prev, &$matchR, &$matchC)
{
while ($col != -1) {
$row = $prev[$col];
$nextCol = $matchR[$row];
$matchR[$row] = $col;
$matchC[$col] = $row;
$col = $nextCol;
}
}
/**
* 格式化分配结果
*/
private function formatAssignment($assignment)
{
$result = [];
foreach ($assignment['matchR'] as $i => $j) {
if ($i < count($this->riders) && $j < count($this->orders)) {
$result[] = [
'rider' => $this->riders[$i],
'order' => $this->orders[$j],
'cost' => $this->costMatrix[$i][$j]
];
}
}
return $result;
}
/**
* 计算两点距离
*/
private function calculateDistance($loc1, $loc2)
{
$lat1 = $loc1['lat'];
$lng1 = $loc1['lng'];
$lat2 = $loc2['lat'];
$lng2 = $loc2['lng'];
$earthRadius = 6371; // 地球半径,单位公里
$dLat = deg2rad($lat2 - $lat1);
$dLng = deg2rad($lng2 - $lng1);
$a = sin($dLat/2) * sin($dLat/2) +
cos(deg2rad($lat1)) * cos(deg2rad($lat2)) *
sin($dLng/2) * sin($dLng/2);
$c = 2 * atan2(sqrt($a), sqrt(1-$a));
return $earthRadius * $c;
}
}
二、实时位置追踪与WebSocket推送
2.1 基于Swoole的WebSocket服务实现
<?php
/**
* WebSocket服务器类
* 使用Swoole实现实时位置推送
*/
class WebSocketServer
{
private $server;
private $redis;
private $config;
public function __construct()
{
$this->config = include('config/websocket.php');
$this->initRedis();
$this->initServer();
}
/**
* 初始化Redis连接
*/
private function initRedis()
{
$this->redis = new Redis();
$this->redis->connect(
$this->config['redis_host'],
$this->config['redis_port']
);
}
/**
* 初始化WebSocket服务器
*/
private function initServer()
{
// 创建WebSocket服务器
$this->server = new Swoole\WebSocket\Server(
$this->config['host'],
$this->config['port']
);
// 设置运行参数
$this->server->set([
'worker_num' => $this->config['worker_num'],
'task_worker_num' => $this->config['task_worker_num'],
'max_connections' => $this->config['max_connections'],
'heartbeat_check_interval' => $this->config['heartbeat_check_interval'],
'heartbeat_idle_time' => $this->config['heartbeat_idle_time'],
'log_file' => $this->config['log_file'],
'log_level' => $this->config['log_level']
]);
// 注册事件回调
$this->server->on('open', [$this, 'onOpen']);
$this->server->on('message', [$this, 'onMessage']);
$this->server->on('close', [$this, 'onClose']);
$this->server->on('task', [$this, 'onTask']);
$this->server->on('finish', [$this, 'onFinish']);
}
/**
* 启动服务器
*/
public function start()
{
echo "WebSocket服务器启动,监听 {$this->config['host']}:{$this->config['port']}\n";
$this->server->start();
}
/**
* 连接打开事件
*/
public function onOpen($server, $request)
{
$fd = $request->fd;
$userId = $request->get['user_id'] ?? '';
$userType = $request->get['user_type'] ?? '';
if (empty($userId) || empty($userType)) {
$server->close($fd);
return;
}
// 存储连接信息
$connectionInfo = [
'fd' => $fd,
'user_id' => $userId,
'user_type' => $userType,
'connect_time' => time(),
'last_heartbeat' => time()
];
// 保存到Redis
$this->redis->hSet('ws:connections', $fd, json_encode($connectionInfo));
// 根据用户类型保存索引
$key = "ws:{$userType}:{$userId}";
$this->redis->sAdd($key, $fd);
// 记录日志
Log::info("WebSocket连接建立", [
'fd' => $fd,
'user_id' => $userId,
'user_type' => $userType
]);
// 发送欢迎消息
$server->push($fd, json_encode([
'type' => 'connect',
'message' => '连接成功',
'time' => time()
]));
}
/**
* 消息接收事件
*/
public function onMessage($server, $frame)
{
$fd = $frame->fd;
$data = json_decode($frame->data, true);
if (!$data) {
return;
}
// 获取连接信息
$connectionInfo = $this->redis->hGet('ws:connections', $fd);
if (!$connectionInfo) {
return;
}
$connectionInfo = json_decode($connectionInfo, true);
switch ($data['type']) {
case 'heartbeat':
$this->handleHeartbeat($server, $fd, $connectionInfo);
break;
case 'location':
$this->handleLocation($server, $fd, $connectionInfo, $data);
break;
case 'status_change':
$this->handleStatusChange($server, $fd, $connectionInfo, $data);
break;
case 'order_accept':
$this->handleOrderAccept($server, $fd, $connectionInfo, $data);
break;
default:
// 未知消息类型
$server->push($fd, json_encode([
'type' => 'error',
'message' => '未知的消息类型'
]));
}
}
/**
* 处理心跳
*/
private function handleHeartbeat($server, $fd, $connectionInfo)
{
// 更新心跳时间
$connectionInfo['last_heartbeat'] = time();
$this->redis->hSet('ws:connections', $fd, json_encode($connectionInfo));
// 响应心跳
$server->push($fd, json_encode([
'type' => 'heartbeat',
'time' => time()
]));
}
/**
* 处理位置更新
*/
private function handleLocation($server, $fd, $connectionInfo, $data)
{
$location = $data['location'];
// 验证位置数据
if (!$this->validateLocation($location)) {
return;
}
// 保存位置到Redis GEO
$key = "riders:locations";
$this->redis->rawCommand(
'GEOADD',
$key,
$location['lng'],
$location['lat'],
$connectionInfo['user_id']
);
// 保存位置历史
$historyKey = "rider:location:history:{$connectionInfo['user_id']}";
$location['time'] = time();
$this->redis->lPush($historyKey, json_encode($location));
$this->redis->lTrim($historyKey, 0, 99); // 只保留最近100条
// 投递异步任务处理位置广播
$taskData = [
'type' => 'broadcast_location',
'user_id' => $connectionInfo['user_id'],
'location' => $location
];
$this->server->task(json_encode($taskData));
}
/**
* 验证位置数据
*/
private function validateLocation($location)
{
if (!isset($location['lat']) || !isset($location['lng'])) {
return false;
}
$lat = floatval($location['lat']);
$lng = floatval($location['lng']);
// 中国大致经纬度范围
if ($lat < 3.86 || $lat > 53.55) {
return false;
}
if ($lng < 73.66 || $lng > 135.05) {
return false;
}
return true;
}
/**
* 连接关闭事件
*/
public function onClose($server, $fd)
{
// 获取连接信息
$connectionInfo = $this->redis->hGet('ws:connections', $fd);
if ($connectionInfo) {
$connectionInfo = json_decode($connectionInfo, true);
// 删除用户索引
$key = "ws:{$connectionInfo['user_type']}:{$connectionInfo['user_id']}";
$this->redis->sRem($key, $fd);
// 删除连接信息
$this->redis->hDel('ws:connections', $fd);
Log::info("WebSocket连接关闭", [
'fd' => $fd,
'user_id' => $connectionInfo['user_id']
]);
}
}
/**
* 任务处理事件
*/
public function onTask($server, $taskId, $workerId, $data)
{
$taskData = json_decode
结语
使用PHP开发同城跑腿配送系统,既能发挥PHP快速开发的优势,又能通过合理架构设计满足高并发需求。关键在于选择合适的技术组件,建立完善的监控体系,并持续优化系统性能。随着业务发展,可以逐步引入Swoole、Hyperf等高性能方案,实现从传统PHP到现代化PHP的平滑演进。
在实际项目中,建议采用迭代开发模式,先实现核心订单流程,再逐步完善周边功能。同时要注重代码质量和规范,为后续维护和扩展打下良好基础。PHP在即时配送领域的应用仍有很大潜力,期待更多创新实践的出现。
更多推荐
所有评论(0)