TP6 多应用支付开发教程

基于 ThinkPHP 6 + Layui 构建 | 微信支付 & 支付宝支付完整指南

ThinkPHP 6 Layui 微信支付 支付宝支付 多应用架构

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 模式
  • ✅ 做好订单金额校验
  • ✅ 测试完整支付流程