1. 项目背景与核心需求
微信支付Native支付模式是商户系统按微信支付协议生成支付二维码,用户再用微信"扫一扫"完成支付的模式。这种支付方式特别适合PC网站、线下门店等场景,相比JSAPI支付省去了微信内嵌浏览器的限制。
在Vue+Laravel技术栈中实现微信支付Native模式,需要解决几个核心问题:
- 前端(Vue)如何安全获取支付二维码
- 后端(Laravel)如何与微信支付API交互
- 如何确保支付结果通知的可靠性
- 支付流程中的异常处理机制
我最近在一个电商项目中完整实现了这套流程,期间踩过不少坑,特别是支付结果异步通知的处理和二维码刷新机制上。下面就把完整的实现方案和避坑经验分享给大家。
2. 开发环境准备
2.1 基础环境配置
首先确保你的开发环境满足以下要求:
- PHP 7.4+ (Laravel 8+要求)
- Composer 2.0+
- Node.js 14+
- Vue CLI 4.5+
- MySQL 5.7+
建议使用Laravel Sail或Homestead作为开发环境,可以避免很多环境兼容性问题。我在Windows和Mac上都测试过,但最终生产环境选择了Linux,因为微信支付的证书文件在Windows上有时会出现权限问题。
2.2 微信支付商户平台配置
在开始编码前,需要在微信支付商户平台完成以下配置:
- 申请Native支付权限
- 设置API密钥(32位随机字符串)
- 下载商户证书(apiclient_cert.pem和apiclient_key.pem)
- 配置支付通知URL(这个后面会重点讲)
特别注意:API密钥不要使用简单字符串,建议用密码生成器创建。我就曾经因为使用简单密钥导致测试环境被恶意调用。
3. Laravel后端实现
3.1 支付SDK集成
推荐使用官方提供的wechatpay/wechatpay包:
bash复制composer require wechatpay/wechatpay
然后在config/services.php中添加配置:
php复制'wechatpay' => [
'app_id' => env('WECHAT_APP_ID'),
'mch_id' => env('WECHAT_MCH_ID'),
'key' => env('WECHAT_KEY'),
'cert_path' => storage_path('wechatpay/apiclient_cert.pem'),
'key_path' => storage_path('wechatpay/apiclient_key.pem'),
'notify_url' => env('WECHAT_NOTIFY_URL'),
],
证书文件建议放在storage目录下,不要放在public目录中,避免被直接下载。我遇到过因为证书路径配置错误导致签名验证失败的问题,调试了整整一天才发现是路径大小写问题。
3.2 创建支付订单API
在Laravel中创建一个支付接口:
php复制use WeChatPay\Builder;
use WeChatPay\Util\PemUtil;
public function createOrder(Request $request)
{
$order = [
'mchid' => config('services.wechatpay.mch_id'),
'out_trade_no' => 'ORDER'.time(),
'appid' => config('services.wechatpay.app_id'),
'description' => '测试商品',
'notify_url' => config('services.wechatpay.notify_url'),
'amount' => [
'total' => 1, // 单位是分
'currency' => 'CNY'
]
];
$instance = Builder::factory([
'mchid' => config('services.wechatpay.mch_id'),
'serial' => '你的证书序列号', // 在商户平台可查
'privateKey' => PemUtil::loadPrivateKey(file_get_contents(config('services.wechatpay.key_path'))),
'certs' => [
'你的证书序列号' => PemUtil::loadCertificate(file_get_contents(config('services.wechatpay.cert_path'))),
],
]);
$resp = $instance->chain('v3/pay/transactions/native')->post(['json' => $order]);
return response()->json([
'code_url' => json_decode($resp->getBody(), true)['code_url']
]);
}
这里有几个关键点需要注意:
- out_trade_no必须是唯一的,建议使用业务ID+时间戳
- amount的单位是分,不是元
- 证书序列号可以在商户平台的"账户中心->API安全"中找到
3.3 支付结果通知处理
支付结果通知是微信支付最重要的环节之一,也是最容易出问题的地方。创建一个专门的控制器处理通知:
php复制use Symfony\Component\HttpFoundation\Response;
public function notify(Request $request)
{
$inWechatpaySignature = $request->header('Wechatpay-Signature');
$inWechatpayTimestamp = $request->header('Wechatpay-Timestamp');
$inWechatpayNonce = $request->header('Wechatpay-Nonce');
$inWechatpaySerial = $request->header('Wechatpay-Serial');
$body = $request->getContent();
$apiv3Key = config('services.wechatpay.key');
$merchantId = config('services.wechatpay.mch_id');
// 验证签名
$verified = \WeChatPay\Crypto\AesGcm::decrypt(
$inWechatpaySignature,
$inWechatpayNonce,
$inWechatpayTimestamp.$body,
$apiv3Key
);
if (!$verified) {
return response()->json(['code' => 'FAIL', 'message' => '签名验证失败'], Response::HTTP_BAD_REQUEST);
}
$data = json_decode($body, true);
// 处理业务逻辑
$order = Order::where('trade_no', $data['out_trade_no'])->first();
if ($order && $order->status === 'pending') {
$order->update([
'status' => $data['trade_state'] === 'SUCCESS' ? 'paid' : 'failed',
'paid_at' => now(),
'transaction_id' => $data['transaction_id']
]);
}
return response()->json(['code' => 'SUCCESS', 'message' => '']);
}
重要提示:通知接口必须返回HTTP 200状态码和特定的JSON响应,否则微信会认为通知失败,会持续重试。我曾经因为返回格式不对导致微信重试了8次。
4. Vue前端实现
4.1 获取支付二维码
在Vue组件中调用后端接口获取支付二维码:
javascript复制import axios from 'axios';
export default {
data() {
return {
qrcodeUrl: '',
timer: null,
orderStatus: 'pending'
}
},
methods: {
async createPayment() {
try {
const { data } = await axios.post('/api/payment/create');
this.qrcodeUrl = `https://api.qrserver.com/v1/create-qr-code/?size=150x150&data=${encodeURIComponent(data.code_url)}`;
this.checkPaymentStatus();
} catch (error) {
console.error('创建支付失败:', error);
}
},
async checkPaymentStatus() {
this.timer = setInterval(async () => {
const { data } = await axios.get(`/api/order/status/${this.orderId}`);
if (data.status === 'paid') {
clearInterval(this.timer);
this.orderStatus = 'paid';
this.$router.push('/payment/success');
}
}, 3000);
}
},
beforeUnmount() {
clearInterval(this.timer);
}
}
这里使用了第三方服务生成二维码,实际项目中建议使用qrcode.js等库在客户端生成,避免依赖外部服务。
4.2 支付状态轮询
由于微信支付的通知可能有延迟,前端需要实现状态轮询。但要注意:
- 轮询间隔建议3-5秒,太频繁会给服务器造成压力
- 支付成功或组件销毁时要清除定时器
- 轮询次数应该有上限(比如20次后停止)
我曾经遇到过用户扫码支付后关闭页面,但定时器还在运行的问题,导致服务器负载升高。
5. 常见问题与解决方案
5.1 证书相关问题
问题现象:调用API时返回"证书验证失败"或"签名错误"
解决方案:
- 确保证书文件路径正确
- 检查证书文件权限(特别是Linux环境)
- 确认商户平台配置的IP白名单包含服务器IP
- 检查系统时间是否正确(证书验证依赖时间)
5.2 支付结果通知问题
问题现象:支付成功后没有收到通知,或通知重复发送
解决方案:
- 确保通知接口URL能外网访问(开发时可用ngrok)
- 正确处理并返回微信期望的响应格式
- 实现通知去重逻辑(通过transaction_id)
5.3 二维码过期处理
问题现象:用户扫码时提示二维码已过期
解决方案:
- 前端检测二维码创建时间,超过2小时自动刷新
- 后端生成新订单号(微信支付要求订单号唯一)
- 在UI上明确提示用户二维码有效期
6. 上线前的检查清单
- [ ] 所有API调用都添加了异常处理
- [ ] 支付通知接口进行了压力测试
- [ ] 证书文件已从代码仓库中排除
- [ ] 敏感配置(如API密钥)使用环境变量
- [ ] 前端实现了支付超时处理
- [ ] 日志系统记录了完整的支付流程
- [ ] 准备了手动对账流程
在实际项目中,我们因为忽略了第7点,导致上线第一天就有几笔支付状态不一致,花了很多时间手动核对。建议提前准备对账脚本,定期与微信支付账单核对。
