ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

PHP微信支付与退款全链路实战:从下单到退款到账的完整实现

PHP微信支付与退款全链路实战:从下单到退款到账的完整实现 简介这份PHP微信支付与退款类资源面向需要为电商或在线服务网站接入微信支付的开发者尤其适合希望绕开官方SDK、以轻量方式理解底层交互流程的中初级PHP程序员。压缩包共3个文件均为php源码整体约7KB涵盖统一下单、签名生成、退款申请与异步回调处理等核心环节结构紧凑便于快速阅读。资源围绕JSAPI支付展开先获取预支付订单拿到prepay_id再结合AppID、商户号与支付密钥生成JSAPI签名前端通过JSSDK唤起微信客户端完成付款退款部分则演示提交退款申请、查询退款状态以及解析回调XML并更新业务状态。示例代码把预支付、签名、退款请求与通知处理串成完整链路读者可据此快速集成到自己的项目中同时理解敏感信息加密与密钥保管的注意事项。目前已有1008人学习下载适合作为微信支付入门的参考实现。1. PHP微信支付和退款类从下单到退款到账这条链路到底怎么跑通做 PHP 电商项目的同学几乎都绕不开微信支付这一关。下单、支付、回调、退款四个环节里任何一个出问题用户就会在客服群里炸锅。我见过太多项目支付能跑通退款一调就报「签名错误」或者「订单号不存在」最后只能人工转账了事。这篇笔记就把 PHP 环境下微信支付和退款类的完整落地路径拆开讲清楚从商户配置、下单接口、异步回调验签到退款申请、退款回调、对账排查每一步都给可复现的代码和参数说明。适合谁看如果你正在用 PHP 做商城、知识付费、会员系统需要接入微信支付并支持原路退款这篇能让你少走至少两天的弯路。如果你只是想知道「微信支付接口」大概长什么样那可能收获有限因为这里全是实操细节和踩坑记录。下面按「先跑通支付再搞定退款最后处理异常」的顺序推进中间会重点讲签名、证书、回调这三个最容易翻车的地方。2. 支付链路先跑通统一下单、签名与异步回调2.1 选型官方 SDK 还是自己封装PHP 接微信支付常见做法有三种直接用微信官方提供的 PHP SDK、用社区维护的 EasyWeChat、或者自己按文档封装。官方 SDK 更新慢但胜在稳定EasyWeChat 封装度高适合快速开发自己封装最灵活但签名和证书处理容易出错。我一般会自己封装一个WxPay类原因有两个一是项目里往往还要接支付宝、银联统一抽象层更好维护二是退款涉及证书路径、双向认证自己控制更清楚。下面这个类结构是经过多个项目验证的核心方法就四个unifiedOrder、notify、refund、refundNotify。?php class WxPay { private $appId; private $mchId; private $apiKey; private $certPath; private $keyPath; public function __construct(array $config) { $this-appId $config[app_id]; $this-mchId $config[mch_id]; $this-apiKey $config[api_key]; $this-certPath $config[cert_path]; // apiclient_cert.pem $this-keyPath $config[key_path]; // apiclient_key.pem } // 生成签名参数按字典序拼接末尾追加 key再 MD5 转大写 public function sign(array $params): string { ksort($params); $str ; foreach ($params as $k $v) { if ($v ! $k ! sign) { $str . $k . . $v . ; } } $str . key . $this-apiKey; return strtoupper(md5($str)); } }这段代码的关键在sign方法微信要求参数按 ASCII 字典序排序空值不参与签名最后拼上key再 MD5。很多人签名报错八成是排序没做、或者把sign字段自己也拼进去了。参数说明api_key是商户平台里设置的 APIv2 密钥32 位cert_path和key_path是退款时才需要的证书文件支付下单用不到。2.2 统一下单最小可跑通的请求统一下单接口是https://api.mch.weixin.qq.com/pay/unifiedorder请求体是 XML 格式。虽然现在微信推 APIv3但 APIv2 在存量项目里依然大量使用而且退款接口 APIv2 更简单所以这里以 v2 为主。public function unifiedOrder(array $order): array { $params [ appid $this-appId, mch_id $this-mchId, nonce_str bin2hex(random_bytes(16)), body $order[body], // 商品描述如 会员充值 out_trade_no $order[trade_no], // 商户订单号32 位内 total_fee $order[fee], // 金额单位分 spbill_create_ip $_SERVER[REMOTE_ADDR], notify_url $order[notify_url], // 异步回调地址必须公网可访问 trade_type NATIVE, // NATIVE 扫码 / JSAPI 公众号 / APP ]; $params[sign] $this-sign($params); $xml $this-toXml($params); $resp $this-post($xml, https://api.mch.weixin.qq.com/pay/unifiedorder); return $this-fromXml($resp); }逻辑说明trade_type决定支付场景NATIVE 返回二维码链接JSAPI 需要额外传openid。total_fee单位是分别传成元这是新手最常见的翻车点。notify_url必须是公网 HTTPS 或 HTTP 地址本地开发可以用内网穿透工具临时映射但上线前一定要换成正式域名。参数怎么调out_trade_no建议用「业务前缀 时间戳 随机数」生成保证全局唯一且可追溯。body不要带特殊字符微信对中文编码敏感建议统一 UTF-8。2.3 异步回调验签和幂等一个都不能少支付成功后微信会 POST 一个 XML 到你的notify_url。这里有两个必须做的事验签和幂等处理。public function notify(): string { $raw file_get_contents(php://input); $data $this-fromXml($raw); // 1. 验签防止伪造回调 $sign $data[sign] ?? ; unset($data[sign]); if ($this-sign($data) ! $sign) { return $this-replyXml(FAIL, 签名校验失败); } // 2. 幂等同一笔订单可能回调多次 $tradeNo $data[out_trade_no]; if ($this-orderAlreadyPaid($tradeNo)) { return $this-replyXml(SUCCESS, OK); } // 3. 业务处理更新订单状态、发货、加积分 $this-markOrderPaid($tradeNo, $data[transaction_id]); return $this-replyXml(SUCCESS, OK); }逻辑说明验签时要把sign字段剔除后再计算否则永远对不上。幂等判断建议用数据库唯一索引兜底不要只靠查询高并发下会有竞态。返回给微信的必须是 XMLSUCCESS才会停止重试否则微信会按 15s、15s、30s、3m、10m 的间隔持续回调。提示回调处理一定要写日志把原始 XML 和验签结果都记下来。出问题时日志是唯一的后悔药。3. 退款链路证书、双向认证与退款回调3.1 退款为什么比支付更容易出错退款接口是https://api.mch.weixin.qq.com/secapi/pay/refund和支付最大的区别是它需要双向认证也就是必须带上商户证书。很多人在支付阶段顺风顺水一到退款就报curl: (58) unable to load client key或者证书序列号不匹配根源都在证书配置。证书有两个文件apiclient_cert.pem和apiclient_key.pem从商户平台下载。注意这两个文件是配套的不能混用不同商户的。路径建议用绝对路径相对路径在 CLI 和 FPM 下表现不一致容易踩坑。3.2 退款请求参数与证书配置public function refund(array $refund): array { $params [ appid $this-appId, mch_id $this-mchId, nonce_str bin2hex(random_bytes(16)), out_trade_no $refund[trade_no], // 原支付订单号 out_refund_no $refund[refund_no], // 退款单号唯一 total_fee $refund[total_fee], // 原订单总额分 refund_fee $refund[refund_fee], // 本次退款金额分 notify_url $refund[notify_url], // 退款结果回调 ]; $params[sign] $this-sign($params); $xml $this-toXml($params); // 关键退款必须带证书 $resp $this-postWithCert( $xml, https://api.mch.weixin.qq.com/secapi/pay/refund, $this-certPath, $this-keyPath ); return $this-fromXml($resp); }逻辑说明total_fee是原订单金额refund_fee是本次退款金额支持部分退款。out_refund_no必须唯一重复提交同一退款单号微信会返回原结果这其实是一种幂等保护。postWithCert里要设置CURLOPT_SSLCERT和CURLOPT_SSLKEY并且CURLOPT_VERBOSE打开方便排查。参数怎么调如果要做多次部分退款每次的out_refund_no都要不同但out_trade_no保持原订单号。退款金额累计不能超过原订单总额否则微信直接拒绝。3.3 退款回调解密与状态判断退款回调的 XML 里有一个req_info字段是加密的需要用 API 密钥解密。public function refundNotify(): string { $raw file_get_contents(php://input); $data $this-fromXml($raw); // req_info 用 AES-256-ECB 解密密钥是 api_key 的 MD5 $key md5($this-apiKey); $decrypted openssl_decrypt( base64_decode($data[req_info]), AES-256-ECB, $key, OPENSSL_RAW_DATA ); $info $this-fromXml($decrypted); // 判断退款状态 if ($info[refund_status] SUCCESS) { $this-markRefundSuccess($info[out_refund_no]); } return $this-replyXml(SUCCESS, OK); }逻辑说明req_info的解密密钥不是api_key本身而是它的 MD5 值这是微信文档里容易看漏的一行。解密后是 XML字段包括out_refund_no、refund_status、refund_fee等。refund_status为SUCCESS才算真正到账PROCESSING表示还在处理需要等回调或主动查询。注意退款回调也可能重复幂等逻辑同样不能省。建议用out_refund_no做唯一索引。4. 避坑与排查退款报错、签名失败、回调丢失怎么查4.1 签名错误九成是排序或编码问题现象调用接口返回签名错误但参数看起来都对。原因微信要求参数按 ASCII 字典序排序且空值不参与签名。PHP 的ksort默认是按字符串比较但如果数组里混了数字键或者值里有前后空格就会出错。另外中文参数必须 UTF-8 编码GBK 环境下会直接签名失败。解决签名前先ksort($params)然后遍历时用if ($v || $k sign) continue;跳过空值和 sign 本身。中文参数统一用mb_convert_encoding转 UTF-8。调试时把拼接前的字符串打出来和微信官方签名工具对比。4.2 退款报「证书不存在」或「curl 58」现象退款接口返回curl: (58) unable to load client key或者证书序列号不匹配。原因证书路径不对、文件权限不足、或者 cert 和 key 不配套。FPM 运行用户和 CLI 用户不同相对路径会指向不同目录。解决用绝对路径检查文件权限至少 644属主是 PHP 运行用户。确认 cert 和 key 是同一商户下载的。如果用了宝塔面板注意 PHP 的open_basedir限制证书目录要在允许范围内。4.3 回调收不到先查网络再查返回格式现象支付成功但订单状态没变日志里没有回调记录。原因notify_url不可公网访问、返回的不是 XML、或者返回了FAIL。微信对回调的响应格式很严格必须是 XML 且return_code为SUCCESS。解决先用curl从外网访问notify_url确认可达。回调处理里不要输出任何额外内容echo、var_dump都会污染响应。返回用replyXml(SUCCESS, OK)统一封装。如果用了框架注意路由是否被 CSRF 中间件拦截。4.4 退款金额对不上单位与累计校验现象退款报退款金额超限或到账金额和预期不符。原因total_fee和refund_fee单位是分但数据库里可能存的是元。多次部分退款时累计退款额没做校验。解决统一在数据库存分展示时再除 100。退款前先查该订单已退金额加上本次不超过total_fee。建议在退款表上加唯一索引防止并发重复退款。4.5 回调验签失败别把 sign 自己算进去现象回调验签永远失败但下单时签名正常。原因回调的 XML 里包含sign字段验签时必须先unset($data[sign])再计算。另外回调 XML 可能带 CDATA解析时要处理好。解决用SimpleXMLElement解析后转数组剔除sign再验签。如果用了fromXml自定义方法确保 CDATA 被正确提取。验签失败时把原始 XML 和计算出的签名都记日志对比一次就能定位。5. 进阶技巧用对账文件和主动查询兜住最后一公里支付和退款都跑通后还有一个绕不开的问题回调可能丢失尤其是服务器迁移、网络抖动的时候。这时候不能干等得有两套兜底机制。第一套是主动查询。支付用orderquery退款用refundquery都是 APIv2 接口签名方式和下单一致。建议写一个定时任务每 5 分钟扫描一次「支付中」和「退款中」的订单超过 10 分钟没回调的就主动查一次。查询结果里trade_state为SUCCESS就补更新订单refund_status为SUCCESS就补更新退款。// 主动查询订单状态 public function queryOrder(string $tradeNo): array { $params [ appid $this-appId, mch_id $this-mchId, out_trade_no $tradeNo, nonce_str bin2hex(random_bytes(16)), ]; $params[sign] $this-sign($params); $resp $this-post($this-toXml($params), https://api.mch.weixin.qq.com/pay/orderquery); return $this-fromXml($resp); }第二套是对账文件。微信每天会生成对账单通过downloadbill接口下载。对账单是 CSV 格式包含当天所有交易和退款记录。我一般会写一个每日凌晨的任务下载前一天的账单和本地订单表逐笔核对差异记录单独告警。这一步能发现很多回调层面发现不了的问题比如金额不一致、订单号对不上。对账文件下载有个坑如果当天没有交易微信返回的是NO_BILL_EXIST而不是文件代码里要判断这个字符串别直接当 CSV 解析。最后说一个我自己的习惯所有微信支付相关的操作不管是下单、回调、退款还是查询全部写进一张wx_pay_log表字段包括请求参数、响应内容、耗时、IP、时间。这张表在排查问题时比任何日志工具都好用因为它是结构化的可以直接 SQL 查询。上线前我会用沙箱环境跑一遍完整链路包括一笔正常支付、一笔全额退款、一笔部分退款、一次回调丢失后的主动查询。这套流程跑通基本就能安心睡觉了。希望帮到你。本文还有配套的精品资源点击获取
返回列表