当我们在讨论外卖系统源码时,往往最核心、最具挑战的部分不是用户注册或菜品展示,而是订单配送引擎。它决定了用户下单后,系统如何找到最合适的骑手,以及如何实时追踪位置。

本文将抛开理论的堆砌,直接进入实战。我们将基于高性能PHP协程框架Hyperf,手写两个核心模块:一个是智能派单算法的简单实现,另一个是多支付网关的策略模式集成。通过这两段代码,带大家透视海外外卖系统“源码”背后的设计逻辑。

一、环境准备与技术选型

在开始写代码之前,我们需要明确:传统的PHP-FPM模式在处理WebSocket长连接和实时地理位置计算时显得力不从心。因此,我们选择Hyperf作为基础骨架。它基于Swoole常驻内存,支持协程,能让我们以同步代码的写法获得异步I/O的高性能 。

// composer.json 核心依赖
{
    "require": {
        "php": ">=7.4",
        "hyperf/http-server": "~2.2",
        "hyperf/websocket-server": "~2.2",
        "hyperf/db": "~2.2",
        "hyperf/redis": "~2.2",
        "hyperf/contract": "~2.2"
    }
}

二、代码示例一:智能抢单模式的WebSocket广播

在海外外卖模式中,“抢单”比“派单”更常见(尤其是众包模式)。当用户下单后,系统需要将订单推送给附近的骑手。这里我们利用Hyperf的WebSocket服务实现广播。

场景设定:
  1. 骑手端App与后端维持WebSocket长连接,连接时上传自己的经纬度。

  2. 后端维护一个“附近骑手”的Redis有序集合(Geo数据结构)。

  3. 新订单产生时,从Redis中查找范围内的骑手,并通过WebSocket广播新订单提醒。

<?php
declare(strict_types=1);

namespace App\Service;

use Hyperf\Redis\Redis;
use Hyperf\WebSocketServer\Sender;
use App\Model\Order;

class DispatchService
{
    /**
     * @var Redis
     */
    protected $redis;

    /**
     * @var Sender
     */
    protected $sender;

    public function __construct(Redis $redis, Sender $sender)
    {
        $this->redis = $redis;
        $this->sender = $sender;
    }

    /**
     * 当新订单创建时,寻找附近骑手并推送
     * @param Order $order
     * @return bool
     */
    public function dispatchNewOrder(Order $order)
    {
        // 1. 获取订单餐厅的经纬度
        $restaurantLng = $order->restaurant_longitude;
        $restaurantLat = $order->restaurant_latitude;

        // 2. 在Redis GEO中查询半径5公里内的在线骑手
        // 骑手位置存储在 key "rider:geo" 中,member 为 rider_id
        $nearbyRiders = $this->redis->geoRadius(
            'rider:geo', 
            $restaurantLng, 
            $restaurantLat, 
            5, 
            'km', 
            ['WITHDIST', 'ASC']
        );

        if (empty($nearbyRiders)) {
            // 没有骑手,触发等待机制或加小费逻辑
            return false;
        }

        // 3. 组装要推送的订单简要信息
        $pushData = [
            'event' => 'new_order_broadcast',
            'order_id' => $order->id,
            'restaurant' => $order->restaurant_name,
            'pickup_address' => $order->restaurant_address,
            'estimated_fee' => $order->delivery_fee,
            'target_distance' => '5km以内' // 实际应用中需计算
        ];

        // 4. 遍历骑手,通过WebSocket发送消息
        // 假设我们在连接时,将 fd 与 rider_id 的关系存放在了 Redis String 中
        foreach ($nearbyRiders as $rider) {
            $riderId = $rider[0]; // geoRadius 返回的 member
            // 从Redis获取该骑手对应的WebSocket连接文件描述符 (fd)
            $fd = $this->redis->get(sprintf('rider_fd_%s', $riderId));
            if ($fd) {
                // 使用 Sender 组件推送
                $this->sender->push((int)$fd, json_encode($pushData));
            }
        }

        // 5. 记录日志,触发超时未接单的后续处理
        $this->redis->setex(sprintf('order:dispatch:%s', $order->id), 120, json_encode($nearbyRiders));

        return true;
    }
}

这段代码展示了如何利用Redis的高性能Geo功能,结合WebSocket实现低延迟的“抢单”广播。在实际系统中,还需要考虑“同一订单推送给多人,谁先抢到”的锁机制,通常可以结合Redis的原子性操作实现 。

三、代码示例二:多支付网关的“策略模式”集成

海外支付渠道多样,且随时可能接入新的本地支付方式。我们需要在代码设计上保证开闭原则——对扩展开放,对修改关闭。这里采用策略模式来设计支付模块。

场景设定:

后台需要支持切换支付渠道,如PayPal、Stripe、Momo(东南亚电子钱包)。

1. 定义支付策略接口

<?php
namespace App\Contract;

interface PaymentStrategyInterface
{
    /**
     * 创建支付单
     * @param float $amount 金额
     * @param string $currency 货币代码
     * @param array $orderInfo 订单信息
     * @return array 包含支付跳转链接或凭据
     */
    public function createPayment(float $amount, string $currency, array $orderInfo): array;

    /**
     * 处理回调验证
     * @param array $callbackData 回调数据
     * @return bool
     */
    public function verifyCallback(array $callbackData): bool;
}

2. 实现具体的支付类(以Stripe为例)

<?php
namespace App\Service\Payment;

use App\Contract\PaymentStrategyInterface;
use Stripe\StripeClient;
use Hyperf\Logger\LoggerFactory;

class StripePayment implements PaymentStrategyInterface
{
    protected $stripe;
    protected $logger;

    public function __construct(LoggerFactory $loggerFactory)
    {
        // 从配置文件读取密钥
        $this->stripe = new StripeClient(config('payment.stripe.secret_key'));
        $this->logger = $loggerFactory->get('payment', 'stripe');
    }

    public function createPayment(float $amount, string $currency, array $orderInfo): array
    {
        try {
            // Stripe 使用最小货币单位(分),所以乘以100
            $amountInCents = (int)($amount * 100);

            $paymentIntent = $this->stripe->paymentIntents->create([
                'amount' => $amountInCents,
                'currency' => strtolower($currency),
                'metadata' => [
                    'order_id' => $orderInfo['order_id'],
                    'user_id' => $orderInfo['user_id']
                ],
                'description' => sprintf('Order #%s payment', $orderInfo['order_id'])
            ]);

            // 记录日志
            $this->logger->info('Stripe payment created', [
                'order_id' => $orderInfo['order_id'],
                'intent_id' => $paymentIntent->id
            ]);

            // 返回客户端秘钥,供前端完成支付
            return [
                'status' => 'success',
                'client_secret' => $paymentIntent->client_secret,
                'payment_intent_id' => $paymentIntent->id
            ];

        } catch (\Exception $e) {
            $this->logger->error('Stripe payment error: ' . $e->getMessage());
            return [
                'status' => 'error',
                'message' => $e->getMessage()
            ];
        }
    }

    public function verifyCallback(array $callbackData): bool
    {
        // 验证 Stripe 的 Webhook 签名
        // 实际应用中需要根据 Stripe 官方文档验证签名
        if (empty($callbackData['type']) || $callbackData['type'] !== 'payment_intent.succeeded') {
            return false;
        }
        // ... 具体验证逻辑
        return true;
    }
}

3. 支付上下文(控制器调用)

<?php
namespace App\Controller;

use Hyperf\Di\Annotation\Inject;
use Hyperf\HttpServer\Annotation\Controller;
use Hyperf\HttpServer\Annotation\PostMapping;
use Psr\Container\ContainerInterface;

#[Controller]
class PaymentController
{
    #[Inject]
    protected ContainerInterface $container;

    private function getPaymentStrategy(string $channel): PaymentStrategyInterface
    {
        // 根据配置映射类名
        $map = [
            'stripe' => \App\Service\Payment\StripePayment::class,
            'paypal' => \App\Service\Payment\PaypalPayment::class,
            'momo' => \App\Service\Payment\MomoPayment::class,
        ];

        if (!isset($map[$channel])) {
            throw new \Exception("Unsupported payment channel");
        }

        // 从容器中获取实例(依赖自动注入)
        return $this->container->get($map[$channel]);
    }

    #[PostMapping(path: "/api/v1/pay/create")]
    public function createPay()
    {
        $channel = $this->request->input('channel', 'stripe');
        $amount = $this->request->input('amount');
        $currency = $this->request->input('currency', 'usd');
        $orderId = $this->request->input('order_id');

        try {
            $strategy = $this->getPaymentStrategy($channel);
            $result = $strategy->createPayment((float)$amount, $currency, [
                'order_id' => $orderId,
                'user_id' => auth()->id()
            ]);

            return $this->response->json($result);
        } catch (\Throwable $e) {
            return $this->response->json(['status' => 'error', 'msg' => $e->getMessage()]);
        }
    }
}

通过这种设计,以后如果要接入“先买后付”(BNPL)服务如Klarna,只需新增一个KlarnaPayment类实现接口,然后在getPaymentStrategy映射中添加一项即可,完全无需修改现有的业务逻辑代码  。

四、总结与扩展

通过上述两个代码示例,我们窥见了海外外卖系统源码中的冰山一角。

  1. 配送模块的核心在于利用Redis+WebSocket维持海量连接和实时通信,解决“找骑手”的问题。

  2. 支付模块的核心在于利用设计模式解耦多变的外部渠道,保证核心业务逻辑的稳定。

  3. 性能保障:PHP通过Hyperf等协程框架,彻底解决了I/O阻塞问题,使得在海外高延迟网络环境下(如调用第三方地图API、支付API)依然能保持出色的吞吐量 。

Logo

openvela 操作系统专为 AIoT 领域量身定制,以轻量化、标准兼容、安全性和高度可扩展性为核心特点。openvela 以其卓越的技术优势,已成为众多物联网设备和 AI 硬件的技术首选,涵盖了智能手表、运动手环、智能音箱、耳机、智能家居设备以及机器人等多个领域。

更多推荐