代码展示:PHP搭建海外版外卖系统源码解析

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

本文将抛开理论的堆砌,直接进入实战。我们将基于高性能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)依然能保持出色的吞吐量 。

相关推荐
feifeigo1232 小时前
matlab画图工具
开发语言·matlab
唐璜Taro2 小时前
Vue3 + TypeScript 后台管理系统完整方案
前端·javascript·typescript
大大水瓶2 小时前
Tomcat
java·tomcat
dustcell.2 小时前
haproxy七层代理
java·开发语言·前端
norlan_jame2 小时前
C-PHY与D-PHY差异
c语言·开发语言
游离态指针2 小时前
以为发消息=下单成功?RabbitMQ从0到秒杀实战的完整踩坑笔记
java
李慕婉学姐2 小时前
Springboot智慧社区系统设计与开发6n99s526(程序+源码+数据库+调试部署+开发环境)带论文文档1万字以上,文末可获取,系统界面在最后面。
数据库·spring boot·后端
掘金酱2 小时前
「寻找年味」 沸点活动|获奖名单公示🎊
前端·人工智能·后端
颜酱2 小时前
栈的经典应用:从基础到进阶,解决LeetCode高频栈类问题
javascript·后端·算法