Step 1 环境准备与依赖安装
前置条件:
PHP >= 7.4 | Composer | MySQL 5.7+ | 已开通微信支付商户号 | 已开通支付宝商户
1.1 安装 ThinkPHP 6 多应用版
# 使用 composer 创建 TP6 多应用项目 composer create-project topthink/think tp6-payment # 进入项目目录 cd tp6-payment # 安装多应用扩展(如果未自动安装) composer require topthink/think-multi-app
1.2 安装支付 SDK
# 安装支付 SDK(推荐使用 yansongda/pay) composer require yansongda/pay ^3.0 # 该 SDK 同时支持微信支付和支付宝支付
提示:
yansongda/pay 是一个优秀的 PHP 支付 SDK,同时支持微信支付(V2/V3)和支付宝,API 统一,非常适合多应用架构。
1.3 配置 .env 环境变量
# .env 文件 - 支付相关配置 APP_DEBUG = true [WECHAT] PAY_APP_ID = wx1234567890abcdef PAY_MCH_ID = 1600000000 PAY_MCH_SECRET_KEY = your_mch_secret_key PAY_CERTS_PATH = /path/to/certs/ PAY_NOTIFY_URL = https://your-domain.com/api/payment/wechatNotify [ALIPAY] PAY_APP_ID = 2021001234567890 PAY_ALI_PUBLIC_KEY = /path/to/ali_public_key.pem PAY_ALI_PRIVATE_KEY = /path/to/app_private_key.pem PAY_NOTIFY_URL = https://your-domain.com/api/payment/alipayNotify PAY_RETURN_URL = https://your-domain.com/index/order/success
1.4 数据库表结构
-- 订单表 CREATE TABLE `order` ( `id` int(11) unsigned NOT NULL AUTO_INCREMENT, `order_no` varchar(64) NOT NULL COMMENT '订单号', `user_id` int(11) NOT NULL COMMENT '用户ID', `amount` decimal(10,2) NOT NULL DEFAULT '0.00' COMMENT '订单金额', `pay_type` tinyint(1) NOT NULL DEFAULT '0' COMMENT '支付方式:1微信 2支付宝', `status` tinyint(1) NOT NULL DEFAULT '0' COMMENT '状态:0待支付 1已支付 2已退款', `transaction_id` varchar(64) DEFAULT NULL COMMENT '第三方支付流水号', `paid_at` datetime DEFAULT NULL COMMENT '支付时间', `create_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP, `update_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, PRIMARY KEY (`id`), UNIQUE KEY `uk_order_no` (`order_no`), KEY `idx_user_id` (`user_id`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='订单表';
Step 2 多应用项目结构
ThinkPHP 6 多应用模式下,每个应用拥有独立的控制器、模型、视图等目录。支付相关代码建议放在公共模块的 Service 层,供多个应用调用。
2.1 目录结构总览
📁 thinkphp6/
├── 📁 app/
│ ├── 📁 admin/ ← 后台管理应用
│ │ ├── 📁 controller/
│ │ ├── 📁 model/
│ │ └── 📁 view/
│ ├── 📁 api/ ← API接口应用
│ │ ├── 📁 controller/
│ │ │ └── 📄 Payment.php ← 支付控制器
│ │ └── 📄 common.php
│ ├── 📁 index/ ← 前台应用
│ │ ├── 📁 controller/
│ │ │ └── 📄 Order.php
│ │ └── 📁 view/
│ └── 📁 common/ ← 公共模块
│ ├── 📁 service/
│ │ ├── 📄 WechatPayService.php ← 微信支付服务
│ │ └── 📄 AlipayService.php ← 支付宝服务
│ └── 📁 model/
│ └── 📄 Order.php
├── 📁 config/
│ ├── 📄 pay.php ← 支付配置
│ └── 📄 app.php ← 多应用配置
├── 📁 route/
│ └── 📄 api.php
├── 📄 composer.json
└── 📄 .env ← 环境变量
2.2 多应用路由配置
// config/app.php - 开启多应用模式
return [
// 开启多应用模式
'with_route' => true,
// 默认应用
'default_app' => 'index',
// 应用映射
'app_map' => [],
// 域名绑定
'domain_bind' => [],
// 禁止URL访问的应用列表
'deny_app_list' => ['common'],
];
2.3 应用路由定义
// route/api.php - API 应用路由
use think\facade\Route;
// 支付相关路由
Route::group('payment', function () {
// 创建支付订单
Route::post('create', 'Payment/create');
// 查询订单状态
Route::get('query/:order_no', 'Payment/query');
// 微信支付回调(不需要鉴权)
Route::post('wechatNotify', 'Payment/wechatNotify');
// 支付宝回调
Route::post('alipayNotify', 'Payment/alipayNotify');
// 支付宝同步回调
Route::get('alipayReturn', 'Payment/alipayReturn');
});
设计原则:
将支付逻辑封装为 Service 层,放在 common 公共模块中,这样 admin、api、index 等多个应用都可以复用支付功能,避免代码重复。
Step 3 支付配置文件
3.1 创建支付配置文件
<?php
// config/pay.php - 支付统一配置文件
return [
// 微信支付配置
'wechat' => [
'default' => [
// 必填 - 商户号
'mch_id' => env('WECHAT.PAY_MCH_ID'),
// 必填 - 商户密钥 (APIv2)
'mch_secret_key_v2' => env('WECHAT.PAY_MCH_SECRET_KEY'),
// 必填 - 商户API证书 (APIv3)
'mch_secret_cert' => runtime_path() . 'certs/apiclient_key.pem',
'mch_public_cert_path' => runtime_path() . 'certs/apiclient_cert.pem',
// 必填 - 公众号/小程序 APPID
'app_id' => env('WECHAT.PAY_APP_ID'),
// 异步通知地址
'notify_url' => env('WECHAT.PAY_NOTIFY_URL'),
// mp, mini, app, h5
'mp_app_id' => env('WECHAT.MP_APP_ID', ''),
'mini_app_id' => env('WECHAT.MINI_APP_ID', ''),
'app_app_id' => env('WECHAT.APP_APP_ID', ''),
],
],
// 支付宝配置
'alipay' => [
'default' => [
// 必填 - 支付宝应用 APPID
'app_id' => env('ALIPAY.PAY_APP_ID'),
// 必填 - 应用私钥
'ali_public_key' => env('ALIPAY.PAY_ALI_PUBLIC_KEY'),
// 必填 - 应用私钥文件路径
'app_public_cert_path' => runtime_path() . 'certs/appPublicCert.crt',
// 必填 - 支付宝公钥文件路径
'app_private_key_path' => runtime_path() . 'certs/appPrivateKey.pem',
// 异步通知地址
'notify_url' => env('ALIPAY.PAY_NOTIFY_URL'),
// 同步通知地址
'return_url' => env('ALIPAY.PAY_RETURN_URL'),
// 沙箱模式
'sandbox' => env('ALIPAY.SANDBOX', false),
],
],
];
3.2 证书文件放置
📁 runtime/certs/
├── 📄 apiclient_cert.pem ← 微信商户证书
├── 📄 apiclient_key.pem ← 微信商户密钥
├── 📄 wechat_public_cert.pem ← 微信平台证书
├── 📄 appPrivateKey.pem ← 支付宝应用私钥
├── 📄 appPublicCert.crt ← 支付宝应用公钥证书
└── 📄 alipayPublicCert.crt ← 支付宝公钥证书
安全警告:
证书文件务必不要提交到 Git 仓库!请在 .gitignore 中添加:
runtime/certs/
Step 4 微信支付服务封装
4.1 微信支付服务类
<?php
// app/common/service/WechatPayService.php
namespace app\common\service;
use Yansongda\Pay\Pay;
use think\facade\Log;
class WechatPayService
{
/**
* JSAPI 支付(公众号/小程序)
*/
public function jsapiPay(array $orderData, string $openid): array
{
try {
$params = [
'out_trade_no' => $orderData['order_no'],
'description' => $orderData['description'] ?? '商品购买',
'amount' => [
'total' => bcmul($orderData['amount'], 100, 0), // 转为分
],
'payer' => [
'openid' => $openid,
],
'notify_url' => config('pay.wechat.default.notify_url'),
];
$result = Pay::wechat()->mp($params);
Log::info('微信支付JSAPI下单成功', [
'order_no' => $orderData['order_no'],
'result' => $result->toArray(),
]);
return $result->toArray();
} catch (\Exception $e) {
Log::error('微信支付JSAPI下单失败', [
'order_no' => $orderData['order_no'],
'error' => $e->getMessage(),
]);
throw $e;
}
}
/**
* Native 支付(扫码支付)
*/
public function nativePay(array $orderData): array
{
try {
$params = [
'out_trade_no' => $orderData['order_no'],
'description' => $orderData['description'] ?? '商品购买',
'amount' => [
'total' => bcmul($orderData['amount'], 100, 0),
],
'notify_url' => config('pay.wechat.default.notify_url'),
];
$result = Pay::wechat()->native($params);
return [
'code_url' => $result->code_url ?? '',
];
} catch (\Exception $e) {
Log::error('微信支付Native下单失败', [
'error' => $e->getMessage(),
]);
throw $e;
}
}
/**
* H5 支付
*/
public function h5Pay(array $orderData): string
{
$params = [
'out_trade_no' => $orderData['order_no'],
'description' => $orderData['description'] ?? '商品购买',
'amount' => [
'total' => bcmul($orderData['amount'], 100, 0),
],
'scene_info' => [
'h5_info' => [
'type' => 'Wap',
],
],
'notify_url' => config('pay.wechat.default.notify_url'),
];
$result = Pay::wechat()->h5($params);
return $result->h5_url ?? '';
}
/**
* APP 支付
*/
public function appPay(array $orderData): array
{
$params = [
'out_trade_no' => $orderData['order_no'],
'description' => $orderData['description'] ?? '商品购买',
'amount' => [
'total' => bcmul($orderData['amount'], 100, 0),
],
'notify_url' => config('pay.wechat.default.notify_url'),
];
$result = Pay::wechat()->app($params);
return $result->toArray();
}
/**
* 处理微信支付回调
*/
public function handleNotify(): array
{
$result = Pay::wechat()->callback();
Log::info('微信支付回调数据', $result->toArray());
return $result->toArray();
}
/**
* 查询订单
*/
public function queryOrder(string $outTradeNo): array
{
$result = Pay::wechat()->find([
'out_trade_no' => $outTradeNo,
]);
return $result->toArray();
}
/**
* 申请退款
*/
public function refund(
string $outTradeNo,
string $outRefundNo,
float $totalAmount,
float $refundAmount
): array {
$params = [
'out_trade_no' => $outTradeNo,
'out_refund_no' => $outRefundNo,
'amount' => [
'total' => bcmul($totalAmount, 100, 0),
'refund' => bcmul($refundAmount, 100, 0),
],
];
$result = Pay::wechat()->refund($params);
return $result->toArray();
}
}
说明:
以上服务类封装了微信支付的常用场景:JSAPI(公众号/小程序)、Native(扫码)、H5、APP支付,以及回调处理和退款功能。
Step 5 支付宝支付服务封装
5.1 支付宝支付服务类
<?php
// app/common/service/AlipayService.php
namespace app\common\service;
use Yansongda\Pay\Pay;
use think\facade\Log;
class AlipayService
{
/**
* 电脑网站支付(PC端)
*/
public function webPay(array $orderData): string
{
try {
$params = [
'out_trade_no' => $orderData['order_no'],
'total_amount' => $orderData['amount'],
'subject' => $orderData['description'] ?? '商品购买',
];
$result = Pay::alipay()->web($params);
Log::info('支付宝电脑网站支付下单成功', [
'order_no' => $orderData['order_no'],
]);
return $result->getContent();
} catch (\Exception $e) {
Log::error('支付宝电脑网站支付下单失败', [
'error' => $e->getMessage(),
]);
throw $e;
}
}
/**
* 手机网站支付(H5端)
*/
public function wapPay(array $orderData): string
{
$params = [
'out_trade_no' => $orderData['order_no'],
'total_amount' => $orderData['amount'],
'subject' => $orderData['description'] ?? '商品购买',
];
$result = Pay::alipay()->wap($params);
return $result->getContent();
}
/**
* APP 支付
*/
public function appPay(array $orderData): string
{
$params = [
'out_trade_no' => $orderData['order_no'],
'total_amount' => $orderData['amount'],
'subject' => $orderData['description'] ?? '商品购买',
];
$result = Pay::alipay()->app($params);
return $result->getContent();
}
/**
* 处理支付宝异步通知
*/
public function handleNotify(): array
{
$result = Pay::alipay()->callback();
Log::info('支付宝异步通知数据', $result->toArray());
return $result->toArray();
}
/**
* 验证支付宝同步通知
*/
public function handleReturn(): array
{
$result = Pay::alipay()->find(request()->param());
return $result->toArray();
}
/**
* 查询订单
*/
public function queryOrder(string $outTradeNo): array
{
$result = Pay::alipay()->find([
'out_trade_no' => $outTradeNo,
]);
return $result->toArray();
}
/**
* 申请退款
*/
public function refund(string $outTradeNo, float $refundAmount): array
{
$params = [
'out_trade_no' => $outTradeNo,
'refund_amount' => $refundAmount,
];
$result = Pay::alipay()->refund($params);
return $result->toArray();
}
/**
* 关闭订单
*/
public function close(string $outTradeNo): array
{
$result = Pay::alipay()->close([
'out_trade_no' => $outTradeNo,
]);
return $result->toArray();
}
}
提示:
支付宝的 web() 和 wap() 方法返回的是 HTML 表单内容,前端需要直接输出该表单即可自动跳转到支付宝收银台。app() 返回的是 orderString,供 APP 端 SDK 调起支付。
Step 6 支付控制器开发
6.1 支付控制器
<?php
// app/api/controller/Payment.php
namespace app\api\controller;
use app\common\service\WechatPayService;
use app\common\service\AlipayService;
use app\common\model\Order;
use think\facade\Validate;
use think\Response;
class Payment
{
protected WechatPayService $wechatPay;
protected AlipayService $alipayPay;
public function __construct(
WechatPayService $wechatPay,
AlipayService $alipayPay
) {
$this->wechatPay = $wechatPay;
$this->alipayPay = $alipayPay;
}
/**
* 创建支付订单
*/
public function create(): Response
{
// 参数验证
$params = request()->post();
$validate = Validate::rule([
'order_no' => 'require|alphaDash',
'amount' => 'require|float|gt:0',
'pay_type' => 'require|in:1,2',
'scene' => 'require|in:jsapi,native,h5,app,web,wap',
'openid' => 'requireIf:scene,jsapi',
]);
if (!$validate->check($params)) {
return json(['code' => 400, 'msg' => $validate->getError()]);
}
// 创建订单记录
$order = Order::create([
'order_no' => $params['order_no'],
'user_id' => $params['user_id'] ?? 0,
'amount' => $params['amount'],
'pay_type' => $params['pay_type'],
'status' => 0,
]);
$orderData = [
'order_no' => $order->order_no,
'amount' => $order->amount,
'description' => $params['description'] ?? '商品购买',
];
try {
if ($params['pay_type'] == 1) {
$result = $this->handleWechatPay($params['scene'], $orderData, $params['openid'] ?? '');
} else {
$result = $this->handleAlipayPay($params['scene'], $orderData);
}
return json(['code' => 0, 'msg' => 'success', 'data' => $result]);
} catch (\Exception $e) {
$order->save(['status' => -1]);
return json(['code' => 500, 'msg' => '支付创建失败:' . $e->getMessage()]);
}
}
/**
* 处理微信支付
*/
private function handleWechatPay(string $scene, array $orderData, string $openid): array
{
switch ($scene) {
case 'jsapi':
return $this->wechatPay->jsapiPay($orderData, $openid);
case 'native':
return $this->wechatPay->nativePay($orderData);
case 'h5':
return ['url' => $this->wechatPay->h5Pay($orderData)];
case 'app':
return $this->wechatPay->appPay($orderData);
default:
throw new \Exception('不支持的支付场景');
}
}
/**
* 处理支付宝支付
*/
private function handleAlipayPay(string $scene, array $orderData): array
{
switch ($scene) {
case 'web':
return ['html' => $this->alipayPay->webPay($orderData)];
case 'wap':
return ['html' => $this->alipayPay->wapPay($orderData)];
case 'app':
return ['order_string' => $this->alipayPay->appPay($orderData)];
default:
throw new \Exception('不支持的支付场景');
}
}
/**
* 微信支付异步回调
*/
public function wechatNotify(): Response
{
try {
$data = $this->wechatPay->handleNotify();
$order = Order::where('order_no', $data['out_trade_no'])->find();
if ($order && $order->status == 0) {
$order->save([
'status' => 1,
'transaction_id' => $data['transaction_id'] ?? '',
'paid_at' => date('Y-m-d H:i:s'),
]);
}
return Pay::wechat()->success();
} catch (\Exception $e) {
\think\facade\Log::error('微信支付回调处理失败', ['error' => $e->getMessage()]);
return response('fail');
}
}
/**
* 支付宝异步回调
*/
public function alipayNotify(): Response
{
try {
$data = $this->alipayPay->handleNotify();
if ($data['trade_status'] == 'TRADE_SUCCESS' || $data['trade_status'] == 'TRADE_FINISHED') {
$order = Order::where('order_no', $data['out_trade_no'])->find();
if ($order && $order->status == 0) {
$order->save([
'status' => 1,
'transaction_id' => $data['trade_no'] ?? '',
'paid_at' => date('Y-m-d H:i:s'),
]);
}
}
return response('success');
} catch (\Exception $e) {
\think\facade\Log::error('支付宝回调处理失败', ['error' => $e->getMessage()]);
return response('fail');
}
}
/**
* 支付宝同步回调
*/
public function alipayReturn(): Response
{
try {
$data = $this->alipayPay->handleReturn();
return redirect('/index/order/success?order_no=' . $data['out_trade_no']);
} catch (\Exception $e) {
return redirect('/index/order/fail');
}
}
/**
* 查询订单状态
*/
public function query(string $order_no): Response
{
$order = Order::where('order_no', $order_no)->find();
if (!$order) {
return json(['code' => 404, 'msg' => '订单不存在']);
}
return json([
'code' => 0,
'data' => [
'order_no' => $order->order_no,
'amount' => $order->amount,
'status' => $order->status,
'pay_type' => $order->pay_type,
'transaction_id' => $order->transaction_id,
'paid_at' => $order->paid_at,
],
]);
}
}
6.2 订单模型
<?php
// app/common/model/Order.php
namespace app\common\model;
use think\Model;
class Order extends Model
{
protected $name = 'order';
protected $autoWriteTimestamp = 'datetime';
// 状态常量
const STATUS_PENDING = 0; // 待支付
const STATUS_PAID = 1; // 已支付
const STATUS_REFUNDED = 2; // 已退款
const STATUS_CLOSED = 3; // 已关闭
const STATUS_FAILED = -1; // 支付失败
// 支付类型
const PAY_TYPE_WECHAT = 1;
const PAY_TYPE_ALIPAY = 2;
/**
* 生成订单号
*/
public static function generateOrderNo(): string
{
return date('YmdHis') . str_pad(mt_rand(1, 999999), 6, '0', STR_PAD_LEFT);
}
/**
* 获取状态文字
*/
public function getStatusTextAttr($value, $data): string
{
$statusMap = [
self::STATUS_PENDING => '待支付',
self::STATUS_PAID => '已支付',
self::STATUS_REFUNDED => '已退款',
self::STATUS_CLOSED => '已关闭',
self::STATUS_FAILED => '支付失败',
];
return $statusMap[$data['status']] ?? '未知';
}
}
Step 7 前端支付页面(Layui)
7.1 支付选择页面
<!-- app/index/view/payment/index.html -->
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<title>收银台</title>
<link rel="stylesheet" href="/static/layui/css/layui.css">
<style>
.pay-container { max-width: 600px; margin: 50px auto; }
.pay-method {
padding: 20px; border: 2px solid #e8e8e8;
border-radius: 8px; margin-bottom: 15px;
cursor: pointer; transition: all 0.3s;
}
.pay-method:hover { border-color: #1E9FFF; background: #f0faff; }
.pay-method.selected { border-color: #1E9FFF; background: #e6f7ff; }
.pay-method img { width: 40px; height: 40px; vertical-align: middle; }
.pay-method .name { font-size: 18px; font-weight: bold; margin-left: 10px; }
.order-info { background: #f9f9f9; padding: 20px; border-radius: 8px; margin-bottom: 20px; }
.order-info .amount { font-size: 32px; color: #ff4d4f; font-weight: bold; }
</style>
</head>
<body>
<div class="pay-container">
<div class="layui-card">
<div class="layui-card-header"><h2>订单确认</h2></div>
<div class="layui-card-body">
<!-- 订单信息 -->
<div class="order-info">
<p>订单号:<span id="orderNo">{$order.order_no}</span></p>
<p>商品:{$order.description}</p>
<p>应付金额:<span class="amount">¥{$order.amount}</span></p>
</div>
<!-- 支付方式选择 -->
<h3>选择支付方式</h3>
<div class="pay-method" data-type="1" data-scene="native">
<img src="/static/images/wechat-pay.png">
<span class="name">微信支付(扫码)</span>
</div>
<div class="pay-method" data-type="1" data-scene="h5">
<img src="/static/images/wechat-pay.png">
<span class="name">微信支付(H5)</span>
</div>
<div class="pay-method" data-type="2" data-scene="web">
<img src="/static/images/alipay.png">
<span class="name">支付宝(电脑网站)</span>
</div>
<div class="pay-method" data-type="2" data-scene="wap">
<img src="/static/images/alipay.png">
<span class="name">支付宝(手机网站)</span>
</div>
<div style="margin-top:30px;text-align:center;">
<button class="layui-btn layui-btn-lg layui-btn-danger" id="payBtn">
<i class="layui-icon layui-icon-rmb"></i> 立即支付
</button>
</div>
</div>
</div>
</div>
<script src="/static/layui/layui.js"></script>
<script>
layui.use(['layer', 'jquery'], function(){
var layer = layui.layer;
var $ = layui.jquery;
var selectedType = null;
var selectedScene = null;
// 选择支付方式
$('.pay-method').on('click', function(){
$('.pay-method').removeClass('selected');
$(this).addClass('selected');
selectedType = $(this).data('type');
selectedScene = $(this).data('scene');
});
// 发起支付
$('#payBtn').on('click', function(){
if (!selectedType) {
layer.msg('请选择支付方式', {icon: 5});
return;
}
var loadIndex = layer.load(1);
$.ajax({
url: '/api/payment/create',
type: 'POST',
data: {
order_no: $('#orderNo').text(),
amount: '{$order.amount}',
pay_type: selectedType,
scene: selectedScene,
},
dataType: 'json',
success: function(res){
layer.close(loadIndex);
if (res.code === 0) {
handlePayResult(res.data, selectedType, selectedScene);
} else {
layer.msg(res.msg, {icon: 2});
}
},
error: function(){
layer.close(loadIndex);
layer.msg('网络错误', {icon: 2});
}
});
});
// 处理支付结果
function handlePayResult(data, payType, scene) {
if (payType == 1 && scene == 'native') {
// 微信扫码支付 - 显示二维码
layer.open({
type: 1,
title: '微信扫码支付',
area: ['400px', '450px'],
content: '<div style="text-align:center;padding:20px;">' +
'<div id="qrcode"></div>' +
'<p style="margin-top:10px;color:#666;">请使用微信扫描二维码</p>' +
'</div>',
success: function(){
new QRCode(document.getElementById('qrcode'), {
text: data.code_url,
width: 256, height: 256,
});
startPolling();
}
});
} else if (payType == 1 && scene == 'h5') {
window.location.href = data.url;
} else if (payType == 2) {
var div = document.createElement('div');
div.innerHTML = data.html;
document.body.appendChild(div);
div.querySelector('form').submit();
}
}
// 轮询订单状态
function startPolling() {
var timer = setInterval(function(){
$.get('/api/payment/query/{$order.order_no}', function(res){
if (res.code === 0 && res.data.status == 1) {
clearInterval(timer);
layer.closeAll();
layer.msg('支付成功!', {icon: 1});
setTimeout(function(){
window.location.href = '/index/order/success';
}, 1500);
}
});
}, 3000);
setTimeout(function(){ clearInterval(timer); }, 300000);
}
});
</script>
</body>
</html>
Step 8 支付回调通知处理
重要:
回调通知是支付系统中最关键的部分,必须做好以下安全措施:
- ✅ 验证签名,确保通知来源合法
- ✅ 幂等处理,防止重复通知导致重复发货
- ✅ 验证金额,确保与订单金额一致
- ✅ 使用事务,保证数据一致性
8.1 完善的回调处理(带事务和幂等)
<?php
// app/common/service/PaymentNotifyService.php
namespace app\common\service;
use app\common\model\Order;
use think\facade\Db;
use think\facade\Log;
class PaymentNotifyService
{
/**
* 处理支付成功通知
*/
public function handlePaid(
string $orderNo,
string $transactionId,
int $payType
): bool {
// 开启事务
Db::startTrans();
try {
// 加锁查询,防止并发
$order = Order::where('order_no', $orderNo)
->lock(true)
->find();
if (!$order) {
Log::error('回调订单不存在', ['order_no' => $orderNo]);
Db::rollback();
return false;
}
// 幂等检查:已支付则直接返回成功
if ($order->status == Order::STATUS_PAID) {
Log::info('订单已处理,跳过', ['order_no' => $orderNo]);
Db::commit();
return true;
}
// 只处理待支付状态
if ($order->status != Order::STATUS_PENDING) {
Log::warning('订单状态异常', [
'order_no' => $orderNo,
'status' => $order->status,
]);
Db::rollback();
return false;
}
// 更新订单为已支付
$order->save([
'status' => Order::STATUS_PAID,
'transaction_id' => $transactionId,
'pay_type' => $payType,
'paid_at' => date('Y-m-d H:i:s'),
]);
// 执行支付成功后的业务逻辑
$this->afterPaid($order);
Db::commit();
Log::info('支付回调处理成功', ['order_no' => $orderNo]);
return true;
} catch (\Exception $e) {
Db::rollback();
Log::error('支付回调处理异常', [
'order_no' => $orderNo,
'error' => $e->getMessage(),
]);
return false;
}
}
/**
* 支付成功后的业务逻辑
*/
private function afterPaid(Order $order): void
{
// 1. 给用户增加积分/余额等
// UserService::addPoints($order->user_id, $order->amount);
// 2. 发送支付成功通知
// NotificationService::sendPaidNotice($order);
// 3. 触发发货流程(如果是实物商品)
// DeliveryService::autoDeliver($order);
// 4. 记录支付日志
Db::name('pay_log')->insert([
'order_no' => $order->order_no,
'amount' => $order->amount,
'pay_type' => $order->pay_type,
'transaction_id' => $order->transaction_id,
'status' => 1,
'create_time' => date('Y-m-d H:i:s'),
]);
}
}
8.2 回调中间件(可选)
<?php
// app/api/middleware/PaymentNotify.php
namespace app\api\middleware;
use think\Request;
use think\Response;
use think\facade\Log;
class PaymentNotify
{
public function handle(Request $request, \Closure $next): Response
{
// 记录原始请求数据(方便排查问题)
Log::info('支付回调请求', [
'url' => $request->url(),
'method' => $request->method(),
'ip' => $request->ip(),
'body' => $request->getContent(),
'header' => $request->header(),
]);
$response = $next($request);
Log::info('支付回调响应', [
'status' => $response->getCode(),
'body' => $response->getContent(),
]);
return $response;
}
}
Step 9 测试与调试
9.1 沙箱环境配置
微信支付沙箱测试
- 1. 微信支付目前取消了沙箱环境,建议使用小额真实支付测试
- 2. 测试金额设置为 0.01 元
- 3. 确保回调地址可公网访问(可使用 ngrok 内网穿透)
- 4. 在微信商户平台配置好 API 密钥和证书
支付宝沙箱测试
- 1. 登录 支付宝开放平台
- 2. 进入「沙箱环境」获取测试 APPID
- 3. 下载沙箱版钱包 APP 进行测试
- 4. 使用沙箱提供的密钥和证书
- 5. 在配置中设置
'sandbox' => true
9.2 内网穿透(本地开发)
# 使用 ngrok 进行内网穿透 ngrok http 80 # 或者使用 cpolar(国内推荐) cpolar http 80 # 获得的公网地址如:https://abc123.ngrok.io # 将此地址配置为支付回调通知地址
9.3 常见问题排查
| 问题 | 可能原因 | 解决方案 |
|---|---|---|
| 签名验证失败 | 密钥配置错误 | 检查商户密钥/API密钥是否正确,注意区分V2和V3密钥 |
| 回调收不到通知 | 回调地址不可达 | 确认回调URL可公网访问,检查防火墙/安全组设置 |
| 证书加载失败 | 证书路径错误 | 使用绝对路径,确保文件有读取权限 |
| 订单号重复 | 订单号生成规则冲突 | 确保订单号全局唯一,建议时间戳+随机数 |
| 金额不匹配 | 单位转换错误 | 微信V3使用分为单位,支付宝使用元为单位 |
| appid与mch_id不匹配 | 配置混用 | 确认APPID和商户号属于同一商户 |
9.4 日志调试
<?php
// 开启支付 SDK 调试日志
// config/pay.php 中添加
return [
'wechat' => [
'default' => [
// ... 其他配置
'debug' => true,
],
],
'logger' => [
'enable' => true,
'file' => runtime_path() . 'log/pay.log',
'level' => 'debug',
],
];
// 在控制器中手动记录调试信息
\think\facade\Log::debug('支付调试', [
'request_params' => $params,
'response' => $result,
'config_used' => config('pay.wechat.default.mch_id'),
]);
恭喜你!
至此,你已经完成了 TP6 多应用下微信支付和支付宝支付的完整开发。
记得上线前做好以下检查:
- ✅ 切换到正式环境配置
- ✅ 配置正确的回调地址
- ✅ 部署正确的证书文件
- ✅ 关闭 debug 模式
- ✅ 做好订单金额校验
- ✅ 测试完整支付流程