同城跑腿配送系统作为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在即时配送领域的应用仍有很大潜力,期待更多创新实践的出现。

Logo

腾讯云面向开发者汇聚海量精品云计算使用和开发经验,营造开放的云计算技术生态圈。

更多推荐