支付系统设计:对接微信支付宝

32 阅读3分钟

支付流程

用户下单 → 创建支付单 → 调用支付渠道 → 用户支付 → 异步回调 → 更新订单

数据库设计

-- 支付单  
CREATE TABLE payments (  
    id BIGINT PRIMARY KEY AUTO_INCREMENT,  
    payment_no VARCHAR(32UNIQUE,  
    order_id BIGINT,  
    order_no VARCHAR(32),  
    user_id BIGINT,  
    amount DECIMAL(10,2),  
    channel VARCHAR(20),          -- wechat/alipay  
    channel_trade_no VARCHAR(64), -- 渠道交易号  
    status TINYINT DEFAULT 0,  
    paid_at TIMESTAMP NULL,  
    created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,  
    INDEX idx_order (order_id),  
    INDEX idx_channel_trade (channel_trade_no)  
);  
  
-- 退款单  
CREATE TABLE refunds (  
    id BIGINT PRIMARY KEY AUTO_INCREMENT,  
    refund_no VARCHAR(32UNIQUE,  
    payment_id BIGINT,  
    order_id BIGINT,  
    amount DECIMAL(10,2),  
    reason VARCHAR(255),  
    channel_refund_no VARCHAR(64),  
    status TINYINT DEFAULT 0,  
    created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP  
);

统一支付接口

<?php  
interface PaymentGateway  
{  
    public function pay(Payment $payment): array;  
    public function query(string $paymentNo): array;  
    public function refund(Refund $refund): array;  
    public function verifyNotify(array $data): bool;  
}

微信支付

<?php  
class WechatPayment implements PaymentGateway  
{  
    private $config;  
      
    public function __construct(array $config)  
    {  
        $this->config = $config;  
    }  
      
    // JSAPI 支付(公众号/小程序)  
    public function pay(Payment $payment): array  
    {  
        $params = [  
            'appid' => $this->config['appid'],  
            'mchid' => $this->config['mchid'],  
            'description' => $payment->description,  
            'out_trade_no' => $payment->payment_no,  
            'notify_url' => $this->config['notify_url'],  
            'amount' => [  
                'total' => (int)($payment->amount * 100),  
                'currency' => 'CNY'  
            ],  
            'payer' => [  
                'openid' => $payment->openid  
            ]  
        ];  
          
        $response$this->request('POST''/v3/pay/transactions/jsapi', $params);  
          
        // 返回前端调起支付的参数  
        return $this->buildJsApiParams($response['prepay_id']);  
    }  
      
    private function buildJsApiParams(string $prepayId): array  
    {  
        $params = [  
            'appId' => $this->config['appid'],  
            'timeStamp' => (string)time(),  
            'nonceStr' => $this->generateNonceStr(),  
            'package' => "prepay_id={$prepayId}",  
            'signType' => 'RSA',  
        ];  
          
        $params['paySign'] = $this->sign($params);  
          
        return $params;  
    }  
      
    // 验证回调  
    public function verifyNotify(array $data): bool  
    {  
        $signature = $_SERVER['HTTP_WECHATPAY_SIGNATURE'] ?? '';  
        $timestamp = $_SERVER['HTTP_WECHATPAY_TIMESTAMP'] ?? '';  
        $nonce = $_SERVER['HTTP_WECHATPAY_NONCE'] ?? '';  
        $body = file_get_contents('php://input');  
          
        $message"{$timestamp}\n{$nonce}\n{$body}\n";  
          
        return $this->verifySignature($message, $signature);  
    }  
      
    // 退款  
    public function refund(Refund $refund): array  
    {  
        $params = [  
            'out_trade_no' => $refund->payment->payment_no,  
            'out_refund_no' => $refund->refund_no,  
            'amount' => [  
                'refund' => (int)($refund->amount * 100),  
                'total' => (int)($refund->payment->amount * 100),  
                'currency' => 'CNY'  
            ]  
        ];  
          
        return $this->request('POST''/v3/refund/domestic/refunds', $params);  
    }  
      
    private function request(string $method, string $uri, array $params): array  
    {  
        $body = json_encode($params);  
        $timestamp = time();  
        $nonce$this->generateNonceStr();  
          
        $signature$this->sign("{$method}\n{$uri}\n{$timestamp}\n{$nonce}\n{$body}\n");  
          
        $response = Http::withHeaders([  
            'Authorization' => "WECHATPAY2-SHA256-RSA2048 mchid=\"{$this->config['mchid']}\",nonce_str=\"{$nonce}\",timestamp=\"{$timestamp}\",serial_no=\"{$this->config['serial_no']}\",signature=\"{$signature}\"",  
            'Content-Type' => 'application/json'  
        ])->send($method"https://api.mch.weixin.qq.com{$uri}", ['body' => $body]);  
          
        return $response->json();  
    }  
}

支付宝支付

<?php  
class AlipayPayment implements PaymentGateway  
{  
    private $config;  
      
    public function pay(Payment $payment): array  
    {  
        $params = [  
            'app_id' => $this->config['app_id'],  
            'method' => 'alipay.trade.create',  
            'charset' => 'utf-8',  
            'sign_type' => 'RSA2',  
            'timestamp' => date('Y-m-d H:i:s'),  
            'version' => '1.0',  
            'notify_url' => $this->config['notify_url'],  
            'biz_content' => json_encode([  
                'out_trade_no' => $payment->payment_no,  
                'total_amount' => $payment->amount,  
                'subject' => $payment->description,  
                'buyer_id' => $payment->buyer_id  
            ])  
        ];  
          
        $params['sign'] = $this->sign($params);  
          
        $response = Http::asForm()->post('https://openapi.alipay.com/gateway.do', $params);  
          
        return $response->json();  
    }  
      
    public function verifyNotify(array $data): bool  
    {  
        $sign = $data['sign'] ?? '';  
        unset($data['sign'], $data['sign_type']);  
          
        ksort($data);  
        $content = urldecode(http_build_query($data));  
          
        return openssl_verify(  
            $content,  
            base64_decode($sign),  
            $this->config['alipay_public_key'],  
            OPENSSL_ALGO_SHA256  
        ) === 1;  
    }  
      
    public function refund(Refund $refund): array  
    {  
        $params = [  
            'app_id' => $this->config['app_id'],  
            'method' => 'alipay.trade.refund',  
            'charset' => 'utf-8',  
            'sign_type' => 'RSA2',  
            'timestamp' => date('Y-m-d H:i:s'),  
            'version' => '1.0',  
            'biz_content' => json_encode([  
                'out_trade_no' => $refund->payment->payment_no,  
                'refund_amount' => $refund->amount,  
                'out_request_no' => $refund->refund_no  
            ])  
        ];  
          
        $params['sign'] = $this->sign($params);  
          
        return Http::asForm()->post('https://openapi.alipay.com/gateway.do', $params)->json();  
    }  
}

支付服务

<?php  
class PaymentService  
{  
    private array $gateways;  
      
    public function __construct()  
    {  
        $this->gateways = [  
            'wechat' => new WechatPayment(config('payment.wechat')),  
            'alipay' => new AlipayPayment(config('payment.alipay'))  
        ];  
    }  
      
    // 创建支付  
    public function create(Order $order, string $channel): array  
    {  
        $payment = Payment::create([  
            'payment_no' => $this->generatePaymentNo(),  
            'order_id' => $order->id,  
            'order_no' => $order->order_no,  
            'user_id' => $order->user_id,  
            'amount' => $order->pay_amount,  
            'channel' => $channel,  
            'status' => PaymentStatus::PENDING  
        ]);  
          
        $gateway$this->gateways[$channel];  
          
        return $gateway->pay($payment);  
    }  
      
    // 处理回调  
    public function handleNotify(string $channel, array $data): bool  
    {  
        $gateway$this->gateways[$channel];  
          
        if (!$gateway->verifyNotify($data)) {  
            Log::error('支付回调验签失败', $data);  
            return false;  
        }  
          
        $paymentNo = $data['out_trade_no'] ?? $data['out_trade_no'];  
        $payment = Payment::where('payment_no', $paymentNo)->first();  
          
        if (!$payment) {  
            Log::error('支付单不存在', ['payment_no' => $paymentNo]);  
            return false;  
        }  
          
        // 幂等处理  
        if ($payment->status === PaymentStatus::PAID) {  
            return true;  
        }  
          
        DB::transaction(function () use ($payment, $data, $channel) {  
            // 更新支付单  
            $payment->update([  
                'status' => PaymentStatus::PAID,  
                'channel_trade_no' => $data['transaction_id'] ?? $data['trade_no'],  
                'paid_at' => now()  
            ]);  
              
            // 更新订单  
            $order = Order::find($payment->order_id);  
            $order->update([  
                'status' => OrderStatus::PAID,  
                'pay_time' => now()  
            ]);  
              
            // 确认扣减库存  
            $this->confirmInventory($order);  
              
            // 发送通知  
            event(new OrderPaid($order));  
        });  
          
        return true;  
    }  
      
    // 退款  
    public function refund(Order $order, float $amount, string $reason): Refund  
    {  
        $payment = Payment::where('order_id', $order->id)  
            ->where('status', PaymentStatus::PAID)  
            ->firstOrFail();  
          
        $refund = Refund::create([  
            'refund_no' => $this->generateRefundNo(),  
            'payment_id' => $payment->id,  
            'order_id' => $order->id,  
            'amount' => $amount,  
            'reason' => $reason,  
            'status' => RefundStatus::PENDING  
        ]);  
          
        $gateway$this->gateways[$payment->channel];  
        $result = $gateway->refund($refund);  
          
        if ($result['code'] === 'SUCCESS' || $result['code'] === '10000') {  
            $refund->update([  
                'status' => RefundStatus::SUCCESS,  
                'channel_refund_no' => $result['refund_id'] ?? $result['trade_no']  
            ]);  
        }  
          
        return $refund;  
    }  
}

回调接口

<?php  
// routes/api.php  
Route::post('/payment/notify/wechat', [PaymentController::class'wechatNotify']);  
Route::post('/payment/notify/alipay', [PaymentController::class'alipayNotify']);  
  
class PaymentController  
{  
    public function wechatNotify(Request $request)  
    {  
        $data = json_decode(file_get_contents('php://input'), true);  
          
        $success$this->paymentService->handleNotify('wechat', $data);  
          
        return response()->json([  
            'code' => $success'SUCCESS' 'FAIL',  
            'message' => $success'成功' '失败'  
        ]);  
    }  
      
    public function alipayNotify(Request $request)  
    {  
        $data = $request->all();  
          
        $success$this->paymentService->handleNotify('alipay', $data);  
          
        return $success'success' : 'fail';  
    }  
}

对账

<?php  
class ReconciliationService  
{  
    // 每日对账  
    public function daily(string $date): void  
    {  
        // 1. 下载账单  
        $wechatBills$this->downloadWechatBill($date);  
        $alipayBills$this->downloadAlipayBill($date);  
          
        // 2. 本地支付记录  
        $localPayments = Payment::whereDate('paid_at', $date)  
            ->where('status', PaymentStatus::PAID)  
            ->get()  
            ->keyBy('payment_no');  
          
        // 3. 对比  
        foreach ($wechatBills as $bill) {  
            $local = $localPayments[$bill['out_trade_no']] ?? null;  
              
            if (!$local) {  
                // 本地缺失,需要补单  
                $this->handleMissing($bill);  
            } elseif ($local->amount != $bill['amount']) {  
                // 金额不一致  
                $this->handleAmountMismatch($local, $bill);  
            }  
        }  
    }  
}

总结

要点说明
幂等性回调可能多次,需要幂等处理
签名验证必须验证回调签名
事务支付单和订单状态要在事务中更新
对账每日对账,发现差异及时处理

支付系统要求高可靠性,每个环节都要考虑异常情况。