
随着同城外卖、校园配送、商家自营配送等业务不断发展,外卖系统已经不再只是简单的“商品展示+下单+支付”。
一个相对完整的外卖系统,通常还需要与支付服务、配送服务、短信服务、地图服务以及其他第三方业务系统进行接口对接。
因此,在外卖系统定制开发过程中,“外卖系统对接三方接口”已经成为比较常见的技术需求。
尤其是订单、支付和配送三个环节,如果系统之间无法实现稳定的数据互通,就容易出现支付成功但订单状态没有更新、骑手配送状态不同步、订单重复推送等问题。
本文就从系统架构、接口设计、订单同步、支付回调以及配送状态同步几个方面,对外卖系统对接三方接口的实现方案进行介绍。

传统外卖系统可以将业务全部放在自己的服务器中完成。
例如:
用户选择商品 → 提交订单 → 系统生成订单 → 用户支付 → 商家接单 → 骑手配送 → 订单完成。
但实际业务中,很多环节需要依赖外部服务。
例如:
因此,一个比较合理的系统架构应该将自身业务系统与第三方服务进行解耦。
可以采用下面这种结构:
┌──────────────┐
│ 用户端 │
│ H5 / 小程序 │
└──────┬───────┘
│
▼
┌───────────────┐
│ 外卖业务系统 │
│ PHP + MySQL │
└───────┬───────┘
│
┌─────────────┼─────────────┐
│ │ │
▼ ▼ ▼
┌─────────┐ ┌─────────┐ ┌─────────┐
│支付接口 │ │配送接口 │ │地图接口 │
└─────────┘ └─────────┘ └─────────┘这样做的好处是,即使某一个第三方服务发生变化,也不会直接影响整个外卖系统的核心业务。
以一个普通外卖订单为例,整个流程可以拆分为几个步骤。
用户提交订单
↓
系统创建本地订单
↓
生成待支付订单
↓
调用支付接口
↓
用户完成支付
↓
支付平台回调
↓
系统验证支付结果
↓
更新订单为已支付
↓
通知商家接单
↓
创建配送订单
↓
第三方配送平台接单
↓
同步配送状态
↓
骑手完成配送
↓
更新本地订单为已完成这里有一个非常重要的设计原则:
第三方接口返回成功,并不代表整个业务流程已经完成。
系统必须以自己的订单状态作为最终业务依据。
首先需要建立自己的订单表。
例如MySQL可以设计为:
CREATE TABLE `orders` (
`id` BIGINT UNSIGNED NOT NULL AUTO_INCREMENT,
`order_no` VARCHAR(64) NOT NULL,
`user_id` BIGINT NOT NULL,
`shop_id` BIGINT NOT NULL,
`total_amount` DECIMAL(10,2) NOT NULL DEFAULT 0.00,
`pay_amount` DECIMAL(10,2) NOT NULL DEFAULT 0.00,
`pay_status` TINYINT NOT NULL DEFAULT 0,
`order_status` TINYINT NOT NULL DEFAULT 0,
`delivery_status` TINYINT NOT NULL DEFAULT 0,
`third_order_no` VARCHAR(128) DEFAULT NULL,
`created_at` DATETIME NOT NULL,
`updated_at` DATETIME NOT NULL,
PRIMARY KEY (`id`),
UNIQUE KEY `uk_order_no` (`order_no`)
);其中:
pay_status
0 = 未支付
1 = 已支付
2 = 已退款
order_status
0 = 待支付
1 = 待接单
2 = 配送中
3 = 已完成
4 = 已取消
delivery_status
0 = 未创建
1 = 已创建
2 = 配送中
3 = 已送达
4 = 配送异常实际项目中可以根据具体业务增加更多状态。
例如:
商家已接单
骑手已接单
骑手已到店
骑手取餐
骑手送达
用户确认收货订单号是整个系统与第三方进行数据关联的重要字段。
例如PHP可以这样生成订单号:
function createOrderNo()
{
return date('YmdHis') . mt_rand(100000, 999999);
}
$orderNo = createOrderNo();
echo $orderNo;生成结果类似:
20260901172535123456建议不要直接使用数据库自增ID作为对外订单号。
可以使用:
业务订单号
+
数据库ID
+
第三方订单号分别管理。
例如:
本地订单号:20260901172535123456
数据库ID:10235
第三方订单号:THIRD202609010001这样后续排查接口问题会更加方便。
支付接口一般分为两个阶段:
第一阶段是创建支付订单。
第二阶段是接收支付回调。
例如后端创建支付订单:
$order = [
'order_no' => $orderNo,
'amount' => '39.90',
'title' => '外卖订单'
];
$response = http_post(
'https://api.example.com/pay/create',
$order
);
$result = json_decode($response, true);
if ($result['code'] === 200) {
$payUrl = $result['data']['pay_url'];
echo json_encode([
'code' => 200,
'pay_url' => $payUrl
]);
}前端获取支付地址之后,可以引导用户完成支付。
例如:
async function payOrder(orderNo) {
const result = await fetch('/api/order/pay', {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify({
order_no: orderNo
})
});
const data = await result.json();
if (data.code === 200) {
window.location.href = data.pay_url;
}
}但是,支付成功以后,不能仅仅根据前端页面显示来判断订单是否支付成功。
真正可靠的方式是等待支付平台的服务器回调。
例如:
public function payNotify()
{
$data = $_POST;
if (!$this->verifySign($data)) {
return 'SIGN_ERROR';
}
$orderNo = $data['order_no'];
$status = $data['status'];
if ($status !== 'SUCCESS') {
return 'FAIL';
}
$order = $this->findOrder($orderNo);
if (!$order) {
return 'ORDER_NOT_FOUND';
}
if ($order['pay_status'] == 1) {
return 'SUCCESS';
}
$this->updateOrder($orderNo, [
'pay_status' => 1,
'order_status' => 1
]);
return 'SUCCESS';
}这里需要特别注意“幂等处理”。
因为第三方平台可能重复发送支付回调。
例如:
第一次回调 → SUCCESS
第二次回调 → SUCCESS
第三次回调 → SUCCESS如果没有幂等机制,就可能造成:
重复发货
重复增加余额
重复创建配送订单
重复发送通知因此应该先判断订单当前状态。
if ($order['pay_status'] == 1) {
return 'SUCCESS';
}已经处理过的订单直接返回成功即可。
当订单完成支付,并且商家确认接单以后,就可以进入配送流程。
系统可以调用第三方配送接口:
$deliveryData = [
'order_no' => $order['order_no'],
'shop' => [
'name' => '示例餐厅',
'address' => '示例商家地址',
'phone' => '13800000000'
],
'customer' => [
'name' => '用户',
'address' => '用户收货地址',
'phone' => '13900000000'
],
'goods' => [
[
'name' => '套餐',
'quantity' => 1
]
]
];
$response = http_post(
'https://api.example.com/delivery/order/create',
$deliveryData
);
$result = json_decode($response, true);第三方返回成功以后:
if ($result['code'] === 200) {
$thirdOrderNo = $result['data']['order_no'];
updateOrder($order['order_no'], [
'third_order_no' => $thirdOrderNo,
'delivery_status' => 1
]);
}这样本地系统就可以保存第三方配送订单编号。
这是外卖系统对接三方接口非常重要的一点。
本地系统一般有:
order_no第三方配送系统一般有:
third_order_no两个订单号不能混为一谈。
建议数据库设计成:
本地订单号
第三方订单号
第三方服务商
第三方订单状态例如:
ALTER TABLE `orders`
ADD COLUMN `third_provider` VARCHAR(50) DEFAULT NULL,
ADD COLUMN `third_order_no` VARCHAR(128) DEFAULT NULL,
ADD COLUMN `third_status` VARCHAR(50) DEFAULT NULL;这样以后更换配送服务商时,也不会影响本地订单结构。
配送订单创建以后,第三方平台可能不断产生状态变化。
例如:
待接单
↓
骑手已接单
↓
骑手到店
↓
骑手取货
↓
配送中
↓
已送达第三方可以通过Webhook向你的服务器发送通知。
例如:
public function deliveryNotify()
{
$data = json_decode(
file_get_contents('php://input'),
true
);
if (!$this->verifySign($data)) {
return 'SIGN_ERROR';
}
$thirdOrderNo = $data['order_no'];
$status = $data['status'];
$order = findOrderByThirdNo($thirdOrderNo);
if (!$order) {
return 'ORDER_NOT_FOUND';
}
updateOrder($order['order_no'], [
'third_status' => $status,
'delivery_status' => mapDeliveryStatus($status)
]);
return 'SUCCESS';
}其中:
function mapDeliveryStatus($status)
{
$map = [
'WAITING' => 1,
'ACCEPTED' => 2,
'DELIVERING' => 3,
'COMPLETED' => 4
];
return $map[$status] ?? 0;
}这样第三方系统的状态就可以转换成自己系统能够理解的状态。
不同平台的状态定义可能完全不同。
例如:
平台A:
10 = 待接单
20 = 已接单
30 = 配送中
40 = 已完成平台B可能是:
WAITING
ACCEPTED
DELIVERING
COMPLETED如果业务代码直接使用第三方状态,后期更换第三方服务时,就需要修改大量业务代码。
更合理的方式是建立统一状态层。
第三方状态
↓
状态转换层
↓
系统统一状态
↓
用户端 / 商家端 / 管理后台例如:
function normalizeDeliveryStatus($provider, $status)
{
$maps = [
'provider_a' => [
'10' => 'WAITING',
'20' => 'ACCEPTED',
'30' => 'DELIVERING',
'40' => 'COMPLETED'
],
'provider_b' => [
'WAITING' => 'WAITING',
'ACCEPTED' => 'ACCEPTED',
'DELIVERING' => 'DELIVERING',
'COMPLETED' => 'COMPLETED'
]
];
return $maps[$provider][$status] ?? 'UNKNOWN';
}这样系统内部只需要处理统一状态。
如果外卖系统未来可能接入多个第三方服务,可以进一步使用接口适配器。
例如:
interface DeliveryProvider
{
public function createOrder($order);
public function cancelOrder($order);
public function queryOrder($order);
public function calculateFee($order);
}然后不同配送服务分别实现:
class ProviderA implements DeliveryProvider
{
public function createOrder($order)
{
// 调用服务商A接口
}
public function cancelOrder($order)
{
// 调用服务商A取消接口
}
public function queryOrder($order)
{
// 查询服务商A订单
}
public function calculateFee($order)
{
// 服务商A配送费计算
}
}另一个服务商:
class ProviderB implements DeliveryProvider
{
public function createOrder($order)
{
// 调用服务商B接口
}
public function cancelOrder($order)
{
// 调用服务商B接口
}
public function queryOrder($order)
{
// 查询服务商B订单
}
public function calculateFee($order)
{
// 服务商B配送费计算
}
}业务层不需要关心具体服务商。
$provider = DeliveryFactory::make($providerName);
$result = $provider->createOrder($order);这种方式非常适合需要对接多个第三方配送服务的外卖系统。
第三方接口一般会涉及订单、金额、用户、地址等业务数据。
因此接口不能简单地接收请求。
可以通过:
时间戳
+
随机数
+
业务参数
+
密钥生成签名。
例如:
$params = [
'order_no' => $orderNo,
'timestamp' => time()
];
ksort($params);
$string = http_build_query($params);
$sign = hash_hmac(
'sha256',
$string,
$secret
);请求时:
order_no
timestamp
sign服务端收到请求以后重新计算签名。
$serverSign = hash_hmac(
'sha256',
$string,
$secret
);
if (!hash_equals($serverSign, $requestSign)) {
return 'SIGN_ERROR';
}这样可以降低接口被伪造调用的风险。
现实环境中,第三方接口并不是百分之百稳定。
可能出现:
网络超时
接口响应慢
服务器异常
返回格式错误
服务商维护因此不能简单写成:
$result = http_post($url, $data);然后失败以后直接提示用户。
可以增加超时时间和重试机制。
例如:
function requestWithRetry($url, $data, $maxRetry = 3)
{
for ($i = 0; $i < $maxRetry; $i++) {
try {
$result = http_post($url, $data);
if ($result) {
return $result;
}
} catch (Exception $e) {
if ($i === $maxRetry - 1) {
throw $e;
}
sleep(1);
}
}
return false;
}但是需要注意:
“重试”并不是所有接口都适合。
例如创建配送订单这种操作,如果第一次请求已经成功,只是响应没有返回,再次创建可能造成重复订单。
因此最好结合业务幂等号使用。
例如:
$idempotentKey = hash(
'sha256',
$order['order_no'] . '_delivery'
);提交第三方接口时:
$deliveryData['request_id'] = $idempotentKey;第三方服务可以根据这个唯一请求号判断:
第一次请求:
request_id = ABC123
第二次请求:
request_id = ABC123如果已经创建过,就直接返回原来的配送订单。
如果第三方接口支持幂等机制,这种设计应该优先使用。
外卖系统在实际运营过程中,接口问题是比较难排查的一类问题。
例如:
用户说已经支付
商家说没有收到订单这时候后台需要知道:
什么时候调用了支付接口?
发送了什么参数?
第三方返回了什么?
有没有收到回调?
回调验证是否成功?
订单最终修改成了什么状态?因此建议建立接口日志表。
CREATE TABLE `api_logs` (
`id` BIGINT UNSIGNED NOT NULL AUTO_INCREMENT,
`provider` VARCHAR(50) NOT NULL,
`api_name` VARCHAR(100) NOT NULL,
`request_id` VARCHAR(100) DEFAULT NULL,
`request_data` TEXT,
`response_data` TEXT,
`status` VARCHAR(30) DEFAULT NULL,
`created_at` DATETIME NOT NULL,
PRIMARY KEY (`id`)
);记录:
接口名称
请求时间
请求参数
响应结果
订单号
第三方订单号
请求ID
错误信息出现问题时,管理员就可以直接查看接口日志。
这是外卖系统开发过程中非常重要的一点。
不推荐:
fetch('https://third-party.com/api/order', {
method: 'POST',
body: JSON.stringify(order)
});因为这样可能暴露:
API Key
Secret
业务参数
第三方接口地址正确的方式应该是:
前端
↓
自己的业务API
↓
业务服务器
↓
第三方接口例如:
fetch('/api/order/create', {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify({
shop_id: 1001,
goods: cart
})
});然后由PHP后端完成第三方接口调用。
这样能够更好地保护第三方接口密钥。
随着系统功能越来越多,可以将第三方服务统一管理。
例如:
Service
│
├── Payment
│ ├── ProviderA
│ └── ProviderB
│
├── Delivery
│ ├── ProviderA
│ └── ProviderB
│
├── Map
│ ├── ProviderA
│ └── ProviderB
│
└── Sms
├── ProviderA
└── ProviderB业务代码只需要调用:
$paymentService->create($order);
$deliveryService->create($order);
$mapService->geocode($address);
$smsService->send($phone, $message);而不需要在订单业务代码里面直接写大量第三方接口请求。
这种架构更加方便维护和扩展。
综合来看,可以设计成:
用户端
H5 / 小程序 / APP
│
▼
API业务层
│
┌────────────┼────────────┐
│ │ │
▼ ▼ ▼
订单服务 支付服务 配送服务
│ │ │
│ ▼ ▼
│ 第三方支付 第三方配送
│
├──────────────┐
│ │
▼ ▼
MySQL Redis
│
▼
接口日志
│
▼
管理后台通过这样的架构,可以把自己的核心业务和第三方服务进行隔离。
在实际开发过程中,建议重点考虑以下几个方面。
第三方服务出现异常时,自己的订单系统不能跟着直接瘫痪。
支付成功、订单状态、配送状态等数据需要保持一致。
支付回调、配送回调、订单创建等接口都应该考虑重复请求。
所有涉及订单、支付、用户等数据的接口,都应该进行身份认证和签名校验。
建议完整记录请求、响应、订单号和错误信息。
第三方接口不能无限等待,需要设置合理的请求超时时间。
对于可安全重试的接口,可以增加自动重试。
不要让第三方状态直接渗透到核心业务系统。
如果未来可能更换第三方服务,应提前设计适配器层。
API Key、Secret、支付密钥等信息不能直接写入前端代码。

外卖系统对接三方接口,本质上是解决不同系统之间的数据通信和业务协同问题。
其中订单、支付和配送属于最核心的三个环节。
比较合理的技术方案应该是:
统一订单中心
+
支付服务层
+
配送服务层
+
第三方接口适配器
+
Webhook回调
+
幂等机制
+
签名验证
+
接口日志
+
异常重试通过这种方式,可以让外卖系统具备更好的扩展能力。
当业务需要接入新的支付服务、配送服务或其他第三方系统时,只需要增加对应的接口适配器,而不需要大规模修改原有订单业务。
对于企业而言,如果后期还需要增加校园外卖、同城配送、商家自配送、跑腿配送等业务,也可以在统一订单中心的基础上继续扩展。
因此,在进行外卖系统定制开发时,与其只考虑“能不能对接三方”,更应该从系统架构层面考虑“如何让三方接口稳定、安全、可扩展地接入自己的业务系统”。
原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。
如有侵权,请联系 cloudcommunity@tencent.com 删除。