Laravel 最优雅的支付系统

摘要

本文全面解析 Laravel 支付系统的构建方法,包括架构设计、多支付网关集成、最佳实践和扩展功能。通过本文,您将学习:

  • Laravel 支付系统的核心架构和设计模式
  • 如何使用 Omnipay、Laravel Pay 和 Cashier 等流行支付包
  • 构建统一支付网关抽象层的具体实现
  • 支付系统的安全考虑、性能优化和可维护性
  • 如何集成多种支付方式(支付宝、微信支付、PayPal 等)
  • 订阅支付、支付分账和多币种支持等高级功能

本文适合希望在 Laravel 项目中构建或优化支付系统的开发者,提供了详细的技术指南和实战示例。

1. 引言

在现代 Web 应用中,支付系统是电子商务和订阅服务的核心组件。一个优雅的支付系统不仅要功能完善,还要易于集成、扩展和维护。Laravel 作为 PHP 生态中最流行的框架之一,提供了丰富的工具和包来构建高质量的支付系统。

本文将介绍如何在 Laravel 中构建一个最优雅的支付系统,包括架构设计、常用包推荐、具体实现和最佳实践。如果您还不熟悉 Laravel 的基本概念,可以参考我们的 Laravel 12 教程 - Web 开发实战入门 文章,了解 Laravel 的核心功能和使用方法。

2. Laravel 支付系统架构设计

2.1 Laravel 支付系统核心架构

Laravel 支付系统采用分层架构设计,核心组件包括:

  • 接口层:处理 HTTP 请求和响应,包括前端集成和支付回调
  • 业务层:实现支付业务逻辑,包括订单管理、支付流程控制和状态管理
  • 网关抽象层:统一不同支付渠道的接口,处理协议转换和错误处理
  • 数据访问层:负责数据库操作,包括订单、支付记录和退款数据的存储和查询
  • 通知系统:处理支付渠道的异步通知,确保支付状态的实时同步
  • 安全层:实现支付验证、签名验证和防欺诈措施

2.2 设计模式应用

1. 策略模式:用于不同支付方式的实现,通过统一接口隔离不同支付网关的具体实现细节

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
// 策略接口
interface PaymentGatewayInterface {
public function create(array $params);
public function refund(array $params);
public function verify(array $data);
}

// 具体策略实现
class AlipayGateway implements PaymentGatewayInterface {
public function create(array $params) {
// 支付宝支付实现
}

public function refund(array $params) {
// 支付宝退款实现
}

public function verify(array $data) {
// 支付宝签名验证
}
}

class WechatGateway implements PaymentGatewayInterface {
public function create(array $params) {
// 微信支付实现
}

public function refund(array $params) {
// 微信退款实现
}

public function verify(array $data) {
// 微信签名验证
}
}

2. 工厂模式:用于创建不同支付网关实例,根据配置或用户选择动态创建相应的支付网关

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
class PaymentGatewayFactory {
public static function create($gateway) {
$config = config('payment.' . $gateway);

switch (strtolower($gateway)) {
case 'alipay':
return new AlipayGateway($config);
case 'wechat':
return new WechatGateway($config);
case 'paypal':
return new PayPalGateway($config);
default:
throw new \InvalidArgumentException("Unsupported gateway: {$gateway}");
}
}
}

3. 观察者模式:用于处理支付状态变化的事件,如支付成功后发送通知、更新订单状态等

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
// 支付状态变化事件
class PaymentStatusChanged {
use SerializesModels;

public $payment;
public $oldStatus;
public $newStatus;

public function __construct($payment, $oldStatus, $newStatus) {
$this->payment = $payment;
$this->oldStatus = $oldStatus;
$this->newStatus = $newStatus;
}
}

// 事件监听器
class PaymentNotificationListener implements ShouldQueue {
use InteractsWithQueue;

public function handle(PaymentStatusChanged $event) {
if ($event->newStatus === 'succeeded') {
// 发送支付成功通知
$this->sendNotification($event->payment);

// 更新订单状态
$event->payment->order->update(['status' => 'paid']);
}
}

protected function sendNotification($payment) {
// 实现通知逻辑
}
}

// 注册监听器
Event::listen(PaymentStatusChanged::class, PaymentNotificationListener::class);

4. 仓库模式:用于数据访问层的抽象,简化数据操作并提高可测试性

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
interface PaymentRepositoryInterface {
public function create(array $data);
public function findByOrderNo($orderNo);
public function updateStatus($id, $status);
public function getStatistics($startDate, $endDate);
}

class PaymentRepository implements PaymentRepositoryInterface {
public function create(array $data) {
return Payment::create($data);
}

public function findByOrderNo($orderNo) {
return Payment::where('order_no', $orderNo)->first();
}

public function updateStatus($id, $status) {
return Payment::where('id', $id)->update(['status' => $status]);
}

public function getStatistics($startDate, $endDate) {
return Payment::whereBetween('created_at', [$startDate, $endDate])
->selectRaw('DATE(created_at) as date, SUM(amount) as total_amount, COUNT(*) as count')
->groupBy('date')
->get();
}
}

2.3 架构最佳实践

1. 模块划分

  • 将支付系统划分为独立的模块,每个模块负责特定的功能
  • 使用 Laravel 服务提供者注册服务,实现依赖注入
  • 遵循单一职责原则,确保每个类只负责一项功能

2. 配置管理

  • 使用环境变量存储敏感信息,如 API 密钥和商户信息
  • 提供统一的配置文件,支持不同环境的配置切换
  • 使用配置缓存,提高系统性能

3. 错误处理

  • 实现统一的错误处理机制,捕获和记录支付过程中的异常
  • 为不同的错误类型提供明确的错误码和错误信息
  • 实现重试机制,处理网络波动等临时错误

4. 日志记录

  • 记录所有支付相关的操作和错误,便于审计和问题排查
  • 使用不同的日志级别,区分普通操作和异常情况
  • 实现日志轮转,避免日志文件过大

5. 监控告警

  • 监控支付系统的运行状态,如支付成功率、响应时间等
  • 设置告警阈值,当指标异常时及时通知相关人员
  • 实现健康检查接口,便于外部系统监控

3. Laravel 推荐的支付包

3.1 Laravel Omnipay 集成

Omnipay 是一个功能强大的 PHP 支付处理库,提供了统一的支付网关接口,支持 100+ 种支付网关。

核心特性

  • 统一 API 接口:所有支付网关使用相同的接口,简化代码实现
  • 丰富的网关支持:支持支付宝、微信支付、PayPal、Stripe 等主流支付方式
  • 灵活的扩展机制:易于添加自定义支付网关
  • 完善的文档:提供详细的使用文档和示例
  • 活跃的社区:持续维护和更新

安装与配置

安装核心包

1
composer require league/omnipay

安装具体网关

1
2
3
4
5
6
7
8
9
10
11
# 支付宝
composer require omnipay/alipay

# 微信支付
composer require omnipay/wechatpay

# PayPal
composer require omnipay/paypal

# Stripe
composer require omnipay/stripe

配置示例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
// config/omnipay.php
return [
'gateways' => [
'alipay' => [
'driver' => 'Alipay_AopPage',
'options' => [
'app_id' => env('ALIPAY_APP_ID'),
'private_key' => env('ALIPAY_PRIVATE_KEY'),
'alipay_public_key' => env('ALIPAY_PUBLIC_KEY'),
'notify_url' => env('APP_URL') . '/payment/alipay/notify',
'return_url' => env('APP_URL') . '/payment/alipay/return',
'sign_type' => 'RSA2',
'sandbox' => env('ALIPAY_SANDBOX', false),
],
],
'wechat' => [
'driver' => 'WechatPay_Native',
'options' => [
'app_id' => env('WECHAT_APP_ID'),
'mch_id' => env('WECHAT_MCH_ID'),
'key' => env('WECHAT_KEY'),
'notify_url' => env('APP_URL') . '/payment/wechat/notify',
'cert_path' => env('WECHAT_CERT_PATH'),
'key_path' => env('WECHAT_KEY_PATH'),
'sandbox' => env('WECHAT_SANDBOX', false),
],
],
'paypal' => [
'driver' => 'PayPal_Express',
'options' => [
'username' => env('PAYPAL_USERNAME'),
'password' => env('PAYPAL_PASSWORD'),
'signature' => env('PAYPAL_SIGNATURE'),
'returnUrl' => env('APP_URL') . '/payment/paypal/return',
'cancelUrl' => env('APP_URL') . '/payment/paypal/cancel',
'testMode' => env('PAYPAL_SANDBOX', false),
],
],
],
];

服务提供者

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
// app/Providers/OmnipayServiceProvider.php
use IlluminateupporterviceProvider;
use Omnipay\Omnipay;

class OmnipayServiceProvider extends ServiceProvider
{
public function register()
{
$this->app->singleton('omnipay', function ($app) {
$gateways = config('omnipay.gateways', []);

foreach ($gateways as $name => $config) {
$gateway = Omnipay::create($config['driver']);
$gateway->initialize($config['options']);
Omnipay::registerGateway($name, $gateway);
}

return Omnipay::getInstance();
});
}

public function boot()
{
$this->publishes([
__DIR__.'/../Config/omnipay.php' => config_path('omnipay.php'),
]);
}
}

使用示例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
// 发起支付
use Omnipay\Omnipay;

$gateway = Omnipay::create('Alipay_AopPage');
$gateway->initialize([
'app_id' => config('omnipay.gateways.alipay.options.app_id'),
'private_key' => config('omnipay.gateways.alipay.options.private_key'),
'alipay_public_key' => config('omnipay.gateways.alipay.options.alipay_public_key'),
'notify_url' => config('omnipay.gateways.alipay.options.notify_url'),
'return_url' => config('omnipay.gateways.alipay.options.return_url'),
'sign_type' => 'RSA2',
'sandbox' => config('omnipay.gateways.alipay.options.sandbox'),
]);

$response = $gateway->purchase([
'out_trade_no' => 'ORD'.date('YmdHis').mt_rand(1000, 9999),
'total_amount' => '99.99',
'subject' => '测试商品',
'body' => '测试商品描述',
])->send();

if ($response->isRedirect()) {
// 重定向到支付页面
$response->redirect();
} else {
// 支付失败
echo $response->getMessage();
}

// 处理回调
public function alipayNotify(Request $request)
{
$gateway = Omnipay::create('Alipay_AopPage');
$gateway->initialize(config('omnipay.gateways.alipay.options'));

$response = $gateway->completePurchase([
'request_params' => $request->all()
])->send();

if ($response->isSuccessful() && $response->isTradeStatusOk()) {
// 支付成功,处理业务逻辑
$orderNo = $request->input('out_trade_no');
$transactionId = $request->input('trade_no');
$amount = $request->input('total_amount');

// 更新订单状态等

return 'success';
}

return 'fail';
}

3.2 Laravel Pay 集成

Laravel Pay 是基于 Omnipay 开发的 Laravel 支付扩展包,提供了更符合 Laravel 风格的 API,简化了支付集成流程。

核心特性

  • Laravel 风格 API:使用链式调用和门面模式,代码更简洁优雅
  • 内置支付记录:自动管理支付记录,支持查询和统计
  • 统一配置:使用 Laravel 配置文件,支持环境变量
  • 丰富的网关支持:支持支付宝、微信支付、PayPal 等主流支付方式
  • 完善的文档:提供详细的使用文档和示例

安装与配置

安装

1
composer require yansongda/laravel-pay

发布配置

1
php artisan vendor:publish --provider="Yansongda\LaravelPay\PayServiceProvider"

配置示例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
// config/pay.php
return [
'default' => [
'strategy' => \Yansongda\Pay\Strategies\Strategy::class,
'gateways' => [
'alipay',
'wechat',
],
],

'alipay' => [
'default' => [
'app_id' => env('ALIPAY_APP_ID'),
'app_secret_cert' => env('ALIPAY_APP_SECRET_CERT'),
'app_public_cert_path' => env('ALIPAY_APP_PUBLIC_CERT_PATH'),
'alipay_public_cert_path' => env('ALIPAY_PUBLIC_CERT_PATH'),
'alipay_root_cert_path' => env('ALIPAY_ROOT_CERT_PATH'),
'notify_url' => env('APP_URL') . '/payment/alipay/notify',
'return_url' => env('APP_URL') . '/payment/alipay/return',
'mode' => env('ALIPAY_MODE', 'dev'), // dev 或 prod
],
],

'wechat' => [
'default' => [
'app_id' => env('WECHAT_APP_ID'),
'mch_id' => env('WECHAT_MCH_ID'),
'key' => env('WECHAT_KEY'),
'cert_path' => env('WECHAT_CERT_PATH'),
'key_path' => env('WECHAT_KEY_PATH'),
'notify_url' => env('APP_URL') . '/payment/wechat/notify',
],
],
];

使用示例

1. 发起支付宝支付

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
use Yansongda\Pay\Pay;

// 电脑网站支付
$order = [
'out_trade_no' => 'ORD'.date('YmdHis').mt_rand(1000, 9999),
'total_amount' => '99.99',
'subject' => '测试商品',
];

$pay = Pay::alipay()->web($order);

return $pay->send(); // 直接跳转

// 手机网站支付
$pay = Pay::alipay()->wap($order);

// APP 支付
$pay = Pay::alipay()->app($order);

2. 发起微信支付

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
// 扫码支付
$order = [
'out_trade_no' => 'ORD'.date('YmdHis').mt_rand(1000, 9999),
'total_fee' => 9999, // 单位:分
'body' => '测试商品',
'trade_type' => 'NATIVE',
];

$pay = Pay::wechat()->scan($order);

return $pay->send(); // 返回二维码链接

// JSAPI 支付
$order = [
'out_trade_no' => 'ORD'.date('YmdHis').mt_rand(1000, 9999),
'total_fee' => 9999,
'body' => '测试商品',
'trade_type' => 'JSAPI',
'openid' => $openid, // 用户的 openid
];

$pay = Pay::wechat()->jsapi($order);

// APP 支付
$pay = Pay::wechat()->app($order);

3. 处理回调

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
// 支付宝回调
public function alipayNotify(Request $request)
{
$data = $request->all();

try {
$result = Pay::alipay()->callback($data);

// 验证成功,处理业务逻辑
$orderNo = $result->out_trade_no;
$transactionId = $result->trade_no;
$amount = $result->total_amount;

// 更新订单状态等

return $result->success(); // 返回 success 字符串
} catch (\Exception $e) {
// 验证失败,记录错误
Log::error('支付宝回调验证失败: ' . $e->getMessage());
return 'fail';
}
}

// 微信支付回调
public function wechatNotify(Request $request)
{
$data = $request->all();

try {
$result = Pay::wechat()->callback($data);

// 验证成功,处理业务逻辑
$orderNo = $result->out_trade_no;
$transactionId = $result->transaction_id;
$amount = $result->total_fee / 100; // 转换为元

// 更新订单状态等

return $result->success(); // 返回 XML 格式的 success
} catch (\Exception $e) {
// 验证失败,记录错误
Log::error('微信支付回调验证失败: ' . $e->getMessage());
return Pay::wechat()->callback()->fail(); // 返回 XML 格式的 fail
}
}

4. 退款

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
// 支付宝退款
$refundOrder = [
'out_trade_no' => '原订单号',
'refund_amount' => '99.99',
'out_request_no' => 'REFUND'.date('YmdHis').mt_rand(1000, 9999),
'refund_reason' => '退款原因',
];

$result = Pay::alipay()->refund($refundOrder);

// 微信支付退款
$refundOrder = [
'out_trade_no' => '原订单号',
'out_refund_no' => 'REFUND'.date('YmdHis').mt_rand(1000, 9999),
'total_fee' => 9999, // 单位:分
'refund_fee' => 9999, // 单位:分
'refund_desc' => '退款原因',
];

$result = Pay::wechat()->refund($refundOrder);

3.3 Laravel Cashier 集成

Cashier 是 Laravel 官方提供的订阅计费管理包,专为处理订阅服务设计,支持 Stripe 和 Paddle 两种支付服务。

核心特性

  • 官方维护:与 Laravel 框架深度集成,定期更新
  • 订阅管理:支持订阅创建、更新、取消、暂停等操作
  • 发票管理:自动生成和发送发票,支持发票历史查询
  • 付款处理:自动处理定期付款,支持付款失败重试
  • 优惠码支持:支持折扣码和促销活动
  • Webhook 处理:自动处理支付服务的 Webhook 通知
  • 税费计算:支持不同地区的税费计算

安装与配置

安装 Stripe 版本

1
2
composer require laravel/cashier
php artisan migrate

安装 Paddle 版本

1
2
composer require laravel/cashier-paddle
php artisan migrate

配置 Stripe

1. 设置环境变量

1
2
3
4
STRIPE_KEY=pk_test_your_stripe_key
STRIPE_SECRET=sk_test_your_stripe_secret
CASHIER_CURRENCY=CNY
CASHIER_MODEL=App\Models\User

2. 配置用户模型

1
2
3
4
5
6
7
8
9
10
11
12
13
// app/Models/User.php
use Laravel\Cashier\Billable;

class User extends Authenticatable
{
use Billable;

// 可选:自定义订阅相关字段
protected $casts = [
'trial_ends_at' => 'datetime',
'subscription_ends_at' => 'datetime',
];
}

3. 配置 Webhook

1
2
3
4
// routes/web.php
use Laravel\Cashier\Http\Controllers\WebhookController;

Route::post('/stripe/webhook', [WebhookController::class, 'handleWebhook']);

使用示例

1. 创建订阅

1
2
3
4
5
6
7
8
9
10
11
12
13
14
// 基本订阅
$user = User::find(1);
$subscription = $user->newSubscription('default', 'price_monthly')
->create($paymentMethod);

// 带试用期的订阅
$subscription = $user->newSubscription('default', 'price_monthly')
->trialDays(14)
->create($paymentMethod);

// 带优惠券的订阅
$subscription = $user->newSubscription('default', 'price_monthly')
->withCoupon('SUMMER20')
->create($paymentMethod);

2. 管理订阅

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
// 取消订阅
$user->subscription('default')->cancel();

// 立即取消订阅
$user->subscription('default')->cancelNow();

// 暂停订阅
$user->subscription('default')->pause();

// 恢复订阅
$user->subscription('default')->resume();

// 更新订阅计划
$user->subscription('default')->swap('price_yearly');

// 检查订阅状态
if ($user->subscribed('default')) {
// 用户已订阅
}

if ($user->subscription('default')->onGracePeriod()) {
// 用户在宽限期内
}

if ($user->subscription('default')->onTrial()) {
// 用户在试用期内
}

3. 发票管理

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
// 获取用户的发票
$invoices = $user->invoices();

// 获取最新发票
$latestInvoice = $user->latestInvoice();

// 下载发票 PDF
return $user->downloadInvoice($invoiceId, [
'vendor' => 'Your Company',
'product' => 'Your Product',
'street' => '123 Main St',
'location' => 'City, State ZIP',
'phone' => '555-555-5555',
'email' => 'contact@yourcompany.com',
]);

4. 付款方式管理

1
2
3
4
5
6
7
8
9
10
11
// 添加付款方式
$user->addPaymentMethod($paymentMethod);

// 获取付款方式
$paymentMethods = $user->paymentMethods();

// 设置默认付款方式
$user->updateDefaultPaymentMethod($paymentMethod);

// 删除付款方式
$user->deletePaymentMethod($paymentMethodId);

5. 一次性付款

1
2
3
4
5
6
7
// 处理一次性付款
$user->charge(1000, $paymentMethod); // 金额以最小货币单位表示

// 带描述的一次性付款
$user->charge(1000, $paymentMethod, [
'description' => 'One-time payment',
]);

6. 处理 Webhook

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
// 自定义 Webhook 处理
class CustomWebhookController extends WebhookController
{
public function handleCustomerSubscriptionUpdated(array $payload)
{
// 处理订阅更新事件
parent::handleCustomerSubscriptionUpdated($payload);
}

public function handleInvoicePaymentSucceeded(array $payload)
{
// 处理发票付款成功事件
parent::handleInvoicePaymentSucceeded($payload);
}

public function handleInvoicePaymentFailed(array $payload)
{
// 处理发票付款失败事件
parent::handleInvoicePaymentFailed($payload);
}
}

4. 构建优雅的 Laravel 支付系统

4.1 Laravel 支付系统目录结构

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
app/
├── Http/
│ ├── Controllers/
│ │ └── PaymentController.php
│ ├── Middleware/
│ └── Requests/
│ └── PaymentRequest.php
├── Models/
│ ├── Order.php
│ ├── Payment.php
│ └── Refund.php
├── Services/
│ └── Payment/
│ ├── Gateways/
│ │ ├── AlipayGateway.php
│ │ ├── WechatGateway.php
│ │ └── PayPalGateway.php
│ ├── Contracts/
│ │ └── GatewayInterface.php
│ ├── Factory.php
│ ├── PaymentService.php
│ └── Events/
│ ├── PaymentCreated.php
│ ├── PaymentSucceeded.php
│ └── PaymentFailed.php
└── Providers/
└── PaymentServiceProvider.php

4.2 Laravel 支付网关抽象层

首先,定义支付网关接口:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
<?php

namespace App\Services\Payment\Contracts;

interface GatewayInterface
{
/**
* Laravel 支付 - 创建支付
*/
public function create(array $params);

/**
* Laravel 支付 - 处理支付回调
*/
public function handleCallback($data);

/**
* Laravel 支付 - 退款
*/
public function refund(array $params);

/**
* Laravel 支付 - 查询支付状态
*/
public function query(array $params);
}

然后,实现具体的支付网关:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
<?php

namespace App\Services\Payment\Gateways;

use App\Services\Payment\Contracts\GatewayInterface;
use Omnipay\Omnipay;

class AlipayGateway implements GatewayInterface
{
protected $gateway;

public function __construct()
{
$this->gateway = Omnipay::create('Alipay_AopApp');
$this->gateway->setAppId(config('payment.alipay.app_id'));
$this->gateway->setPrivateKey(config('payment.alipay.private_key'));
$this->gateway->setAppId(config('payment.alipay.app_id'));
$this->gateway->setAlipayPublicKey(config('payment.alipay.public_key'));
$this->gateway->setSignType('RSA2');
$this->gateway->setNotifyUrl(route('payment.alipay.notify'));
$this->gateway->setReturnUrl(route('payment.alipay.return'));
}

public function create(array $params)
{
$order = [
'out_trade_no' => $params['order_no'],
'total_amount' => $params['amount'],
'subject' => $params['subject'],
'body' => $params['body'] ?? $params['subject'],
];

$response = $this->gateway->purchase($order)->send();

return $response->getData();
}

public function handleCallback($data)
{
// 处理支付宝回调
$response = $this->gateway->completePurchase([
'request_params' => $data
])->send();

if ($response->isSuccessful() && $response->isTradeStatusOk()) {
return [
'success' => true,
'order_no' => $data['out_trade_no'],
'transaction_id' => $data['trade_no'],
'amount' => $data['total_amount'],
];
}

return ['success' => false];
}

public function refund(array $params)
{
$response = $this->gateway->refund([
'out_trade_no' => $params['order_no'],
'refund_amount' => $params['amount'],
'out_request_no' => $params['refund_no'],
])->send();

return $response->getData();
}

public function query(array $params)
{
$response = $this->gateway->query([
'out_trade_no' => $params['order_no'],
])->send();

return $response->getData();
}
}

4.3 Laravel 支付工厂

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
<?php

namespace App\Services\Payment;

use App\Services\Payment\Gateways\AlipayGateway;
use App\Services\Payment\Gateways\WechatGateway;
use App\Services\Payment\Gateways\PayPalGateway;

class Factory
{
public static function make($gateway)
{
switch (strtolower($gateway)) {
case 'alipay':
return new AlipayGateway();
case 'wechat':
return new WechatGateway();
case 'paypal':
return new PayPalGateway();
default:
throw new \InvalidArgumentException("Unsupported gateway: {$gateway}");
}
}
}

4.4 Laravel 支付服务

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
<?php

namespace App\Services\Payment;

use App\Services\Payment\Events\PaymentCreated;
use App\Services\Payment\Events\PaymentSucceeded;
use App\Services\Payment\Events\PaymentFailed;
use App\Models\Order;
use App\Models\Payment;

class PaymentService
{
/**
* Laravel 支付 - 创建支付
*/
public function createPayment($orderId, $gateway, $amount, $extra = [])
{
$order = Order::findOrFail($orderId);

$payment = Payment::create([
'order_id' => $order->id,
'gateway' => $gateway,
'amount' => $amount,
'order_no' => $this->generateOrderNo(),
'status' => 'pending',
'extra' => $extra,
]);

event(new PaymentCreated($payment));

$gatewayInstance = Factory::make($gateway);
$result = $gatewayInstance->create([
'order_no' => $payment->order_no,
'amount' => $amount,
'subject' => $order->subject,
'body' => $order->body,
...$extra,
]);

$payment->update(['payload' => $result]);

return [
'payment' => $payment,
'gateway_data' => $result,
];
}

/**
* Laravel 支付 - 处理支付回调
*/
public function handleCallback($gateway, $data)
{
$gatewayInstance = Factory::make($gateway);
$result = $gatewayInstance->handleCallback($data);

if ($result['success']) {
$payment = Payment::where('order_no', $result['order_no'])->first();

if ($payment && $payment->status === 'pending') {
$payment->update([
'status' => 'succeeded',
'transaction_id' => $result['transaction_id'],
'completed_at' => now(),
]);

$payment->order->update(['status' => 'paid']);

event(new PaymentSucceeded($payment));
}
} else {
event(new PaymentFailed($data));
}

return $result;
}

/**
* Laravel 支付 - 处理退款
*/
public function refund($paymentId, $amount, $reason = '')
{
$payment = Payment::findOrFail($paymentId);

if ($payment->status !== 'succeeded') {
throw new \Exception('Only succeeded payments can be refunded');
}

$refundNo = $this->generateRefundNo();

$gatewayInstance = Factory::make($payment->gateway);
$result = $gatewayInstance->refund([
'order_no' => $payment->order_no,
'amount' => $amount,
'refund_no' => $refundNo,
'reason' => $reason,
]);

$refund = $payment->refunds()->create([
'refund_no' => $refundNo,
'amount' => $amount,
'reason' => $reason,
'status' => $result['success'] ? 'succeeded' : 'failed',
'payload' => $result,
]);

if ($result['success']) {
$payment->update(['refunded_amount' => $payment->refunded_amount + $amount]);
}

return $refund;
}

/**
* Laravel 支付 - 生成订单号
*/
protected function generateOrderNo()
{
return date('YmdHis') . str_pad(mt_rand(1, 9999), 4, '0', STR_PAD_LEFT);
}

/**
* Laravel 支付 - 生成退款单号
*/
protected function generateRefundNo()
{
return 'REFUND_' . date('YmdHis') . str_pad(mt_rand(1, 9999), 4, '0', STR_PAD_LEFT);
}
}

4.5 Laravel 支付模型定义

Order 模型

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
<?php

namespace App\Models;

use Illuminate\Database\Eloquent\Model;

class Order extends Model
{
protected $fillable = [
'user_id',
'subject',
'body',
'amount',
'status',
'extra',
];

protected $casts = [
'extra' => 'array',
];

public function payments()
{
return $this->hasMany(Payment::class);
}
}

Payment 模型

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
<?php

namespace App\Models;

use Illuminate\Database\Eloquent\Model;

class Payment extends Model
{
const STATUS_PENDING = 'pending';
const STATUS_SUCCEEDED = 'succeeded';
const STATUS_FAILED = 'failed';
const STATUS_REFUNDED = 'refunded';

protected $fillable = [
'order_id',
'gateway',
'order_no',
'transaction_id',
'amount',
'refunded_amount',
'status',
'payload',
'extra',
'completed_at',
];

protected $casts = [
'payload' => 'array',
'extra' => 'array',
'completed_at' => 'datetime',
];

public function order()
{
return $this->belongsTo(Order::class);
}

public function refunds()
{
return $this->hasMany(Refund::class);
}
}

Refund 模型

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
<?php

namespace App\Models;

use Illuminate\Database\Eloquent\Model;

class Refund extends Model
{
const STATUS_PENDING = 'pending';
const STATUS_SUCCEEDED = 'succeeded';
const STATUS_FAILED = 'failed';

protected $fillable = [
'payment_id',
'refund_no',
'amount',
'reason',
'status',
'payload',
'completed_at',
];

protected $casts = [
'payload' => 'array',
'completed_at' => 'datetime',
];

public function payment()
{
return $this->belongsTo(Payment::class);
}
}

4.6 Laravel 支付控制器

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
<?php

namespace App\Http\Controllers;

use App\Http\Requests\PaymentRequest;
use App\Services\Payment\PaymentService;
use Illuminate\Http\Request;

class PaymentController extends Controller
{
protected $paymentService;

public function __construct(PaymentService $paymentService)
{
$this->paymentService = $paymentService;
}

public function create(PaymentRequest $request)
{
$data = $request->validated();

$result = $this->paymentService->createPayment(
$data['order_id'],
$data['gateway'],
$data['amount'],
$data['extra'] ?? []
);

return response()->json($result);
}

public function alipayNotify(Request $request)
{
$result = $this->paymentService->handleCallback('alipay', $request->all());

return $result['success'] ? 'success' : 'fail';
}

public function wechatNotify(Request $request)
{
$data = $request->all();
$result = $this->paymentService->handleCallback('wechat', $data);

return $result['success'] ?
'<xml><return_code><![CDATA[SUCCESS]]></return_code><return_msg><![CDATA[OK]]></return_msg></xml>' :
'<xml><return_code><![CDATA[FAIL]]></return_code><return_msg><![CDATA[FAIL]]></return_msg></xml>';
}

public function refund(Request $request)
{
$refund = $this->paymentService->refund(
$request->input('payment_id'),
$request->input('amount'),
$request->input('reason')
);

return response()->json($refund);
}
}

4.7 Laravel 支付事件定义

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
<?php

namespace App\Services\Payment\Events;

use App\Models\Payment;
use Illuminate\Queue\SerializesModels;

class PaymentCreated
{
use SerializesModels;

public $payment;

public function __construct(Payment $payment)
{
$this->payment = $payment;
}
}

class PaymentSucceeded
{
use SerializesModels;

public $payment;

public function __construct(Payment $payment)
{
$this->payment = $payment;
}
}

class PaymentFailed
{
use SerializesModels;

public $data;

public function __construct($data)
{
$this->data = $data;
}
}

4.8 Laravel 支付服务提供者

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
<?php

namespace App\Providers;

use Illuminate\Support\ServiceProvider;
use App\Services\Payment\PaymentService;

class PaymentServiceProvider extends ServiceProvider
{
/**
* Register services.
*
* @return void
*/
public function register()
{
$this->app->singleton(PaymentService::class, function ($app) {
return new PaymentService();
});
}

/**
* Bootstrap services.
*
* @return void
*/
public function boot()
{
$this->publishes([
__DIR__.'/../Config/payment.php' => config_path('payment.php'),
]);
}
}

4.9 Laravel 支付配置文件

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
<?php

return [
'alipay' => [
'app_id' => env('ALIPAY_APP_ID'),
'private_key' => env('ALIPAY_PRIVATE_KEY'),
'public_key' => env('ALIPAY_PUBLIC_KEY'),
'sandbox' => env('ALIPAY_SANDBOX', false),
],

'wechat' => [
'app_id' => env('WECHAT_APP_ID'),
'mch_id' => env('WECHAT_MCH_ID'),
'key' => env('WECHAT_KEY'),
'cert_path' => env('WECHAT_CERT_PATH'),
'key_path' => env('WECHAT_KEY_PATH'),
'sandbox' => env('WECHAT_SANDBOX', false),
],

'paypal' => [
'client_id' => env('PAYPAL_CLIENT_ID'),
'secret' => env('PAYPAL_SECRET'),
'sandbox' => env('PAYPAL_SANDBOX', false),
],
];

5. Laravel 支付系统最佳实践

5.1 Laravel 支付系统安全考虑

  • 使用 HTTPS:确保所有支付相关的请求都通过 HTTPS 传输,防止中间人攻击
  • 验证签名:严格验证支付网关的回调签名,使用官方提供的 SDK 或验证工具
  • 防重放攻击:使用唯一的订单号和时间戳防止重复支付,设置合理的订单过期时间
  • 数据加密:敏感信息如支付凭证应加密存储,使用 Laravel 的加密功能或环境变量
  • 权限控制:限制支付相关操作的访问权限,使用 Laravel 的中间件和策略进行授权
  • 输入验证:对所有支付相关的输入进行严格验证,使用 Laravel 的表单验证功能
  • 日志记录:记录所有支付操作和异常,便于审计和问题排查
  • 定期安全审计:定期检查支付系统的安全配置和依赖包的安全更新

安全实现示例

签名验证实现:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
// 在支付回调处理中验证签名
public function verifySignature($data, $signature) {
// 获取配置的密钥
$key = config('payment.' . $this->gateway . '.key');

// 根据支付网关类型选择不同的签名验证方法
switch ($this->gateway) {
case 'alipay':
return $this->verifyAlipaySignature($data, $signature, $key);
case 'wechat':
return $this->verifyWechatSignature($data, $signature, $key);
default:
return false;
}
}

// 支付宝签名验证
protected function verifyAlipaySignature($data, $signature, $key) {
// 移除 sign 参数
unset($data['sign']);
unset($data['sign_type']);

// 按照支付宝要求排序参数
ksort($data);

// 生成待签名字符串
$stringToSign = http_build_query($data, '', '&');

// 使用 RSA2 算法验证签名
$publicKey = openssl_pkey_get_public($key);
$result = openssl_verify($stringToSign, base64_decode($signature), $publicKey, OPENSSL_ALGO_SHA256);

openssl_free_key($publicKey);
return $result === 1;
}

5.2 Laravel 支付系统性能优化

  • 使用队列:将支付处理和通知处理放入队列,使用 Laravel 的队列系统异步处理
  • 缓存配置:缓存支付网关配置,减少重复加载,使用 Laravel 的缓存系统
  • 数据库索引:为支付表的关键字段添加索引,如订单号、交易号、状态等
  • 批量操作:对于批量退款等操作,使用批量处理,减少数据库查询次数
  • 连接池:对于高并发场景,使用数据库连接池和 Redis 连接池
  • 代码优化:优化支付处理逻辑,减少不必要的计算和查询
  • 资源限制:合理设置 PHP 和服务器的资源限制,避免性能瓶颈

性能优化实现示例

使用队列处理支付通知:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
// 在支付成功事件监听器中使用队列
class PaymentSucceededListener implements ShouldQueue
{
use InteractsWithQueue;

public function handle(PaymentSucceeded $event)
{
$payment = $event->payment;

// 发送邮件通知
Mail::to($payment->order->user->email)->send(
new PaymentSuccessMail($payment)
);

// 发送短信通知
$this->sendSmsNotification($payment);

// 更新订单状态
$payment->order->update(['status' => 'paid']);
}

protected function sendSmsNotification($payment)
{
// 实现短信发送逻辑
}
}

// 注册队列监听器
Event::listen(PaymentSucceeded::class, PaymentSucceededListener::class);

缓存支付网关配置:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
// 在支付网关工厂中使用缓存
class PaymentGatewayFactory
{
public static function create($gateway)
{
// 尝试从缓存获取配置
$configKey = "payment.gateway.{$gateway}";
$config = Cache::remember($configKey, 3600, function () use ($gateway) {
return config("payment.{$gateway}");
});

// 根据配置创建支付网关
switch ($gateway) {
case 'alipay':
return new AlipayGateway($config);
case 'wechat':
return new WechatGateway($config);
default:
throw new \InvalidArgumentException("Unsupported gateway: {$gateway}");
}
}
}

5.3 Laravel 支付系统可维护性

  • 代码规范:遵循 Laravel 代码风格和 PSR 规范
  • 文档完善:为支付系统编写详细的文档
  • 日志记录:记录所有支付相关的操作和错误
  • 监控告警:监控支付系统的运行状态和异常
  • 测试覆盖:编写单元测试和集成测试

6. Laravel 集成多种支付方式

6.1 Laravel 支付前端集成

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
<template>
<div class="payment-methods">
<div
v-for="method in paymentMethods"
:key="method.id"
class="payment-method"
@click="selectPaymentMethod(method)"
>
<img :src="method.icon" :alt="method.name">
<span>{{ method.name }}</span>
</div>

<button v-if="selectedMethod" @click="submitPayment" class="submit-btn">
确认支付
</button>
</div>
</template>

<script>
export default {
data() {
return {
paymentMethods: [
{ id: 'alipay', name: '支付宝', icon: '/icons/alipay.png' },
{ id: 'wechat', name: '微信支付', icon: '/icons/wechat.png' },
{ id: 'paypal', name: 'PayPal', icon: '/icons/paypal.png' },
],
selectedMethod: null,
orderId: 1,
amount: 99.99,
};
},
methods: {
selectPaymentMethod(method) {
this.selectedMethod = method;
},
async submitPayment() {
try {
const response = await axios.post('/api/payments', {
order_id: this.orderId,
gateway: this.selectedMethod.id,
amount: this.amount,
});

const { payment, gateway_data } = response.data;

// 根据支付方式处理跳转或唤起支付
if (this.selectedMethod.id === 'alipay') {
// 支付宝 H5 支付
window.location.href = gateway_data.url;
} else if (this.selectedMethod.id === 'wechat') {
// 微信支付,显示二维码
this.showWechatQrCode(gateway_data.code_url);
} else if (this.selectedMethod.id === 'paypal') {
// PayPal 支付
window.location.href = gateway_data.redirect_url;
}
} catch (error) {
console.error('支付失败:', error);
}
},
showWechatQrCode(codeUrl) {
// 显示微信支付二维码
this.$alert(
`<img src="https://api.qrserver.com/v1/create-qr-code/?size=200x200&data=${encodeURIComponent(codeUrl)}" />`,
'微信支付',
{
html: true,
confirmButtonText: '已支付',
callback: (action) => {
if (action === 'confirm') {
this.checkPaymentStatus();
}
}
}
);
},
async checkPaymentStatus() {
// 轮询检查支付状态
// ...
},
},
};
</script>

6.2 Laravel 支付后端路由

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
<?php

use Illuminate\Support\Facades\Route;
use App\Http\Controllers\PaymentController;

Route::prefix('api')->group(function () {
Route::post('payments', [PaymentController::class, 'create']);
Route::post('payments/refund', [PaymentController::class, 'refund']);
});

// 支付回调路由
Route::prefix('payment')->group(function () {
Route::post('alipay/notify', [PaymentController::class, 'alipayNotify']);
Route::get('alipay/return', [PaymentController::class, 'alipayReturn']);
Route::post('wechat/notify', [PaymentController::class, 'wechatNotify']);
Route::post('paypal/notify', [PaymentController::class, 'paypalNotify']);
});

7. Laravel 支付扩展功能

7.1 Laravel 订阅支付

对于需要定期扣费的订阅服务,可以使用 Laravel Cashier:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
<?php

use Laravel\Cashier\Billable;

class User extends Authenticatable
{
use Billable;
}

// 创建订阅
$user->newSubscription('default', 'price_monthly')->create($paymentMethod);

// 取消订阅
$user->subscription('default')->cancel();

// 恢复订阅
$user->subscription('default')->resume();

7.2 Laravel 支付分账

对于需要分账的场景,可以扩展支付网关:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
<?php

class AlipayGateway extends Gateway
{
// ...

public function transfer(array $params)
{
$response = $this->gateway->transfer([
'out_biz_no' => $params['order_no'],
'trans_amount' => $params['amount'],
'payee_type' => $params['type'],
'payee_account' => $params['account'],
'payer_show_name' => $params['payer_name'],
'payee_real_name' => $params['payee_name'],
])->send();

return $response->getData();
}
}

7.3 Laravel 支付多币种支持

对于需要支持多币种的场景,可以在支付服务中添加币种转换:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
<?php

class PaymentService
{
// ...

public function createPayment($orderId, $gateway, $amount, $currency = 'CNY', $extra = [])
{
// 币种转换
if ($currency !== 'CNY' && in_array($gateway, ['alipay', 'wechat'])) {
$amount = $this->convertCurrency($amount, $currency, 'CNY');
}

// 后续处理...
}

protected function convertCurrency($amount, $from, $to)
{
// 调用汇率转换 API 或使用固定汇率
// ...
return $convertedAmount;
}
}

8. Laravel 支付系统总结

构建一个优雅的 Laravel 支付系统需要考虑以下几个方面:

  1. 架构设计:使用合理的设计模式和目录结构,确保系统的可扩展性和可维护性
  2. 网关抽象:通过抽象层统一不同支付渠道的接口,简化集成和切换
  3. 安全性:严格验证支付回调,防止欺诈和攻击
  4. 用户体验:提供流畅的支付流程和清晰的错误提示
  5. 监控和日志:记录所有支付相关的操作和错误,便于排查问题
  6. 测试覆盖:编写充分的测试用例,确保系统的稳定性

通过本文介绍的方案,你可以构建一个功能完善、易于扩展、安全可靠的 Laravel 支付系统,为你的应用提供优雅的支付体验。

9. Laravel 支付参考资料

官方文档

第三方支付库

支付网关文档

相关教程

技术博客和资源

安全资源