支付流程
用户下单 → 创建支付单 → 调用支付渠道 → 用户支付 → 异步回调 → 更新订单
数据库设计
-- 支付单
CREATE TABLE payments (
id BIGINT PRIMARY KEY AUTO_INCREMENT,
payment_no VARCHAR(32) UNIQUE,
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(32) UNIQUE,
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);
}
}
}
}
总结
| 要点 | 说明 |
|---|---|
| 幂等性 | 回调可能多次,需要幂等处理 |
| 签名验证 | 必须验证回调签名 |
| 事务 | 支付单和订单状态要在事务中更新 |
| 对账 | 每日对账,发现差异及时处理 |
支付系统要求高可靠性,每个环节都要考虑异常情况。