应使用适配器模式封装支付回调逻辑,将微信、支付宝等渠道的验签、字段提取、响应生成隔离到独立类中,统一接口约束verify/getOrderId/buildSuccessResponse等方法,避免控制器内混杂if-else与重复校验。

为什么不能直接在控制器里写支付回调逻辑
第三方支付通知(如微信、支付宝)的回调接口,本质是外部系统向你发起的 HTTP 请求,参数格式、签名方式、响应规则各不相同。如果每个渠道都在 PayController 里硬编码处理,很快就会出现:重复校验逻辑、混杂的 if-else 判断、难以测试、新增渠道要改主流程——这不是封装,是打补丁。
适配器模式在这里的核心价值不是“设计感”,而是把「渠道差异」隔离到独立类中,让主流程只关心「这笔通知是否合法、是否已处理、要不要更新订单状态」。
- 微信回调用
XML传参,需解析 + 验证sign字段 - 支付宝回调用
GET或POST的form数据,验签靠alipay_rsa_public_key和sign_type - 银联回调可能要求先调用
queryOrder接口反查,再确认结果 - 所有渠道都要求「响应成功字符串」(如微信要
"success",支付宝要"success"或空响应),但失败时不能抛异常,必须返回特定内容
定义统一的支付通知适配器接口
先明确契约:不管哪个渠道,只要实现了这个接口,就能被主流程无差别调用。重点约束三件事:验签、提取业务字段、生成响应体。
interface PaymentNotifyAdapter
{
public function verify(): bool;
public function getOrderId(): string;
public function getTradeNo(): string;
public function getAmount(): float;
public function getNotifyId(): string;
public function buildSuccessResponse(): string;
public function buildFailureResponse(string $reason): string;
}
注意 getNotifyId():这是关键字段,用于幂等控制。微信叫 result_notify_id(v3)或 nonce_str + timestamp 组合,支付宝叫 notify_id,必须从原始请求中准确提取,不能用订单号代替。
立即学习“PHP免费学习笔记(深入)”;
- 不要在接口里定义
handle()方法——业务处理(如查库、改状态、发消息)不属于适配器职责 -
buildSuccessResponse()返回值必须是纯字符串,且不含换行、空格、BOM 头,否则微信会重试 - 所有实现类构造函数应只接收
array $rawInput和必要配置(如公钥路径),避免依赖全局容器或 Request 对象
微信支付 v3 回调适配器的关键实现细节
微信 v3 回调是 JSON + AES-GCM 解密 + 平台证书验签,比 v2 复杂得多。很多封装失败就栽在解密和验签顺序上。
- 必须先用平台证书验证
Wechatpay-Signature请求头,再解密body字段,不能反过来 - 解密后得到的是标准 JSON,但字段名是驼峰(
outTradeNo),不是下划线,别硬套 Laravel 的request()->all() -
getNotifyId()应返回data['id'](即通知事件 ID),这是微信幂等去重的唯一依据 -
buildSuccessResponse()必须返回空字符串'',不是"success"—— 这是 v3 文档明确写的,错一个字符都会触发重试
示例片段(省略证书加载和解密):
public function verify(): bool
{
$signature = $_SERVER['HTTP_WECHATPAY_SIGNATURE'] ?? '';
$timestamp = $_SERVER['HTTP_WECHATPAY_TIMESTAMP'] ?? '';
$nonce = $_SERVER['HTTP_WECHATPAY_NONCE'] ?? '';
$body = file_get_contents('php://input');
return WechatPayV3Signer::verify($signature, $timestamp, $nonce, $body, $this->platformCert);
}
public function buildSuccessResponse(): string
{
return ''; // 注意:不是 "success"
}
支付宝回调适配器如何避开验签陷阱
支付宝回调最常出问题的是验签失败,原因几乎全是参数拼接不一致。它的验签逻辑要求:将所有非空参数按字典序排序,拼成 key=value&key=value 字符串,再用 RSA 公钥验证 sign 字段。
- 必须过滤掉
sign和sign_type字段本身,否则验签必挂 - 参数值要原样参与拼接,不能 urldecode —— 支付宝回调里的中文是 rawurlencode 后传的,拼串时保持原样
-
getNotifyId()取$params['notify_id']即可,但要注意该字段在沙箱环境可能为空,需降级用out_trade_no+gmt_create组合 - 响应时若用
echo 'success',确保输出前没任何echo、var_dump、错误日志,否则响应体含 HTML 或警告信息,支付宝认为失败
验签核心逻辑示意:
public function verify(): bool
{
$params = $this->rawInput;
unset($params['sign'], $params['sign_type']);
ksort($params);
$stringToBeSigned = http_build_query($params, '', '&', PHP_QUERY_RFC3986);
return openssl_verify($stringToBeSigned, base64_decode($params['sign']), $this->publicKey, OPENSSL_ALGO_SHA256) === 1;
}
真正难的不是写适配器,是确保每个渠道的 verify() 在任何边界条件下(空参数、时间偏移、证书过期、签名截断)都返回明确的布尔值,且不抛异常。一旦某个渠道适配器崩了,整个回调入口就不可用——所以务必给每个适配器加单元测试,用真实回调 payload 快照做回归验证。



















