PHP 怎么组合支付

wen PHP项目 1

本文目录导读:

PHP 怎么组合支付

  1. 方案一:钱包余额 + 第三方支付(最常用)
  2. 方案二:平台“订单拆单”模式(多个第三方支付)
  3. 方案三:预付卡 / 积分 + 余额 + 第三方
  4. 总结建议

在 PHP 中实现组合支付(即一次订单同时使用多种支付方式,如余额 + 微信,或支付宝 + 银行卡)通常有两种架构思路:

核心难点:支付平台的接口只支持单一支付方式,组合支付需要自己记账,并将多个支付平台的流水关联到同一个订单上。

以下是几种常见的实现方案,从简单到复杂:


钱包余额 + 第三方支付(最常用)

这是最常见的组合支付场景(比如余额不够,用微信补差价)。

核心逻辑

  1. 拆分计算:在创建订单时,计算 余额抵扣需第三方支付 的金额。
  2. 先锁余额:提交订单时,先冻结或扣除用户余额(转入平台暂存账户)。
  3. 再付差额:生成一个实际的第三方支付单,金额为差额部分。
  4. 异步回调处理:第三方支付回调成功后,将订单置为已支付状态。

核心代码逻辑示例

<?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(通常较少见,因为用户体验不佳)。

实现思路

  1. 父订单 + 子订单
    • 数据库设计:orders 父表(记录总金额),order_payments 子表(记录每种支付方式的子支付单)。
    • 下单时生成多个子支付单(金额、支付渠道)。
  2. 回调聚合判断
    • 外部支付回调时,更新对应子支付单状态。
    • 判断所有子支付单是否全部“已支付”。
    • 如果全部支付完成,将主订单状态更新为“已支付”。

数据结构建议

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 方案二(订单拆分子支付单),支持任意多种组合,扩展性强
多币种 / 多账户 方案三(复杂冲抵),需要引入规则引擎来管理抵扣顺序

最重要提示:组合支付中,“先把钱收齐”再发货/放款是底层原则,对于第三方支付回调,必须做幂等处理(防止回调多次导致重复入账)。

抱歉,评论功能暂时关闭!