当我们在讨论外卖系统源码时,往往最核心、最具挑战的部分不是用户注册或菜品展示,而是订单配送引擎。它决定了用户下单后,系统如何找到最合适的骑手,以及如何实时追踪位置。
本文将抛开理论的堆砌,直接进入实战。我们将基于高性能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服务实现广播。
场景设定:
-
骑手端App与后端维持WebSocket长连接,连接时上传自己的经纬度。
-
后端维护一个"附近骑手"的Redis有序集合(Geo数据结构)。
-
新订单产生时,从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映射中添加一项即可,完全无需修改现有的业务逻辑代码 。
四、总结与扩展
通过上述两个代码示例,我们窥见了海外外卖系统源码中的冰山一角。
-
配送模块 的核心在于利用Redis+WebSocket维持海量连接和实时通信,解决"找骑手"的问题。
-
支付模块 的核心在于利用设计模式解耦多变的外部渠道,保证核心业务逻辑的稳定。
-
性能保障:PHP通过Hyperf等协程框架,彻底解决了I/O阻塞问题,使得在海外高延迟网络环境下(如调用第三方地图API、支付API)依然能保持出色的吞吐量 。