本文目录导读:

在 PHP 中实现组合支付(即一次订单同时使用多种支付方式,如余额 + 微信,或支付宝 + 银行卡)通常有两种架构思路:
核心难点:支付平台的接口只支持单一支付方式,组合支付需要自己记账,并将多个支付平台的流水关联到同一个订单上。
以下是几种常见的实现方案,从简单到复杂:
钱包余额 + 第三方支付(最常用)
这是最常见的组合支付场景(比如余额不够,用微信补差价)。
核心逻辑:
- 拆分计算:在创建订单时,计算
余额抵扣和需第三方支付的金额。 - 先锁余额:提交订单时,先冻结或扣除用户余额(转入平台暂存账户)。
- 再付差额:生成一个实际的第三方支付单,金额为差额部分。
- 异步回调处理:第三方支付回调成功后,将订单置为已支付状态。
核心代码逻辑示例:
<?php
class PaymentService {
private $db; // PDO实例
private $user;
private $order;
public function __construct($db, $order, $user) {
$this->db = $db;
$this->order = $order;
$this->user = $user;
}
/**
* 计算拆分金额
*/
public function calculateSplit($totalAmount, $useBalanceAmount = 0) {
$balance = $this->user['balance'];
// 用户实际抵扣金额不能超过余额和订单总额
$deduct = min($balance, $useBalanceAmount, $totalAmount);
// 第三方支付金额 = 总金额 - 余额抵扣
$externalAmount = $totalAmount - $deduct;
return ['balance_pay' => $deduct, 'external_pay' => $externalAmount];
}
/**
* 步骤1:提交订单并冻结余额
*/
public function lockBalanceAndCreateOrder($totalAmount, $useBalanceAmount) {
$split = $this->calculateSplit($totalAmount, $useBalanceAmount);
// 开启事务
$this->db->beginTransaction();
try {
// 1. 扣减用户余额,增加冻结金额
// UPDATE users SET balance = balance - :deduct, frozen_balance = frozen_balance + :deduct WHERE id = :uid
// 2. 创建订单状态为 'PENDING_PAY'(待支付)
// 订单记录中保存 balance_pay 和 external_pay 两个字段
$this->db->commit();
return ['order_id' => $orderId, 'external_pay' => $split['external_pay']];
} catch (\Exception $e) {
$this->db->rollBack();
throw $e;
}
}
/**
* 步骤2:发起第三方支付(仅支付差额)
*/
public function payWithWechat($orderId, $externalAmount) {
if ($externalAmount <= 0) {
// 如果差额为0,直接调用内部回调完成
return $this->handleInternalSuccess($orderId);
}
// 调用微信支付接口,金额为 $externalAmount(注意单位换算)
$wechatResponse = WechatPay::unifiedOrder([
'out_trade_no' => $orderId . '_EXT', // 生成本地唯一子单号
'total_fee' => $externalAmount * 100,
]);
return $wechatResponse;
}
/**
* 步骤3:微信支付回调
*/
public function handleWechatCallback($orderId) {
// 验证签名...
$this->db->beginTransaction();
try {
// 1. 解锁冻结余额(将 frozen_balance 减掉)
// 2. 计算总支付完成:余额部分 + 微信部分
// 3. 订单状态更新为 PAID
// 4. 冻结金额清零
$this->db->commit();
} catch (\Exception $e) {
$this->db->rollBack();
}
}
/**
* 内部成功处理(当余额充足无需外部支付时)
*/
private function handleInternalSuccess($orderId) {
// 直接将冻结余额清除,订单置为已支付
}
}
平台“订单拆单”模式(多个第三方支付)
适用于大型商城,比如一笔 1000 元的订单,用支付宝付 500,微信付 500(通常较少见,因为用户体验不佳)。
实现思路:
- 父订单 + 子订单:
- 数据库设计:
orders父表(记录总金额),order_payments子表(记录每种支付方式的子支付单)。 - 下单时生成多个子支付单(金额、支付渠道)。
- 数据库设计:
- 回调聚合判断:
- 外部支付回调时,更新对应子支付单状态。
- 判断所有子支付单是否全部“已支付”。
- 如果全部支付完成,将主订单状态更新为“已支付”。
数据结构建议:
orders (
id, user_id, total_amount, status (PENDING/PAID),
paid_amount (已累计支付金额)
)
order_payments_sub (
id, order_id, channel (alipay/wechat/balance),
amount, status (PENDING/PAID), external_trade_no
)
聚合判断核心逻辑:
public function onSubPaymentCallback($orderId) {
// 更新该子单状态为已支付
// 聚合查询
$subPayments = $this->db->query(
"SELECT SUM(CASE WHEN status='PAID' THEN amount ELSE 0 END) as paid,
SUM(amount) as total
FROM order_payments_sub WHERE order_id = :oid"
);
if ($subPayments['paid'] >= $subPayments['total']) {
// 所有子单都支付完成,更新主订单
$this->db->update("UPDATE orders SET status='PAID' WHERE id=:oid");
}
}
预付卡 / 积分 + 余额 + 第三方
这是方案一的扩展版,核心公式: 实付金额 = 订单金额 - 优惠券金额 - 积分抵扣 - 余额 -(第三方支付)
关键点:
- 金额精度:PHP 浮点数运算会有精度丢失,一定要用
Decimal类或string类型存储金额。 - 最小单位:与第三方对接时,注意人民币是
分,数据库存储建议用分或Decimal(10,2)。 - 并发问题:用户可能同时发起两次组合支付,导致余额重复扣减。必须使用数据库行级锁(
SELECT ... FOR UPDATE)锁定用户余额行,并在事务中进行扣减。
-- 伪代码:锁定用户余额行 SELECT balance FROM users WHERE id = ? FOR UPDATE;
总结建议
| 场景 | 推荐方案 |
|---|---|
| 个人项目 / 中小系统 | 方案一(余额 + 外部支付),逻辑简单,容易维护 |
| 大型电商 / SaaS | 方案二(订单拆分子支付单),支持任意多种组合,扩展性强 |
| 多币种 / 多账户 | 方案三(复杂冲抵),需要引入规则引擎来管理抵扣顺序 |
最重要提示:组合支付中,“先把钱收齐”再发货/放款是底层原则,对于第三方支付回调,必须做幂等处理(防止回调多次导致重复入账)。