本文目录导读:

在 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 | 方案二(订单拆分子支付单),支持任意多种组合,扩展性强 |
| 多币种 / 多账户 | 方案三(复杂冲抵),需要引入规则引擎来管理抵扣顺序 |
最重要提示:组合支付中,“先把钱收齐”再发货/放款是底层原则,对于第三方支付回调,必须做幂等处理(防止回调多次导致重复入账)。
版权声明:NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
CBA直播
CBA直播
CBA直播
CBA直播
CBA直播
CBA直播
CBA直播
CBA直播
CBA直播
CBA直播
CBA直播
CBA直播
CBA直播
CBA直播
CBA直播
CBA直播
CBA直播
CBA直播
足球直播
足球直播
足球直播
足球直播
足球直播
足球直播
足球直播
足球直播
足球直播
足球直播
足球直播
足球直播
足球直播
足球直播
足球直播
足球直播
足球直播
足球直播
英超直播
英超直播
英超直播
英超直播
英超直播
英超直播
英超直播
英超直播
英超直播
英超直播
英超直播
英超直播
英超直播
英超直播
英超直播
英超直播
英超直播
英超直播