简介:为ThinkPHP5项目打造的微信与支付宝支付集成资源包,主要面向需要快速接入主流支付渠道的PHP开发者。资源完整呈现商户配置、SDK调用、下单支付、回调验签等关键环节,同时兼顾网银支付对接思路。压缩包共2321个文件,其中PHP源码1545个,另含配置文件、测试脚本与说明文档,整体仅4.76MB,便于部署和二次开发。目前已有925人学习,适合作为支付接口开发的自学参考或项目模板。内容针对微信支付、支付宝支付分别梳理商户号、密钥、回调地址等配置方式,并详解统一下单、预支付标识生成、支付结果通知等流程;作者还总结了安全传输与签名校验的注意事项。资源内附带phpunit测试用例和配置模板,可帮助开发者快速验证支付逻辑,目录结构清晰,便于按支付渠道分类检索,能有效缩短开发周期、降低接入门槛。无论是个人开发者自学还是团队集成复用,都能从中受益。
1. 把微信支付和支付宝支付接进TP5,真正难的不是调接口
做商城项目的时候,我把微信支付和支付宝支付接进TP5后台,前前后后折腾了快两周。官方给的旧版Demo要么停留在ThinkPHP 3时代,要么引一堆SDK依赖,都塞不进现有的老项目。这份项目把支付收口成一个TP5扩展:业务层只调Pay::driver('wechat')->pay($order)这一行,统一下单、签名、异步回调、退款全部收敛进驱动。微信、支付宝、网银支付三种方式共用同一套业务接口,几百行代码替代官方文档的几十页说明。实际接完才会发现,真正花时间的不是调接口,而是签名格式、证书、回调这三个环节——这套代码把三座山挪走了。适合手上有TP5老项目要做支付集成、或者外包商城要快速加收银台的开发,新手按章节顺序走一遍也能跑通。
2. 架构设计:驱动模式把微信、支付宝、网银收进同一套接口
2.1 目录结构与加载方式:extend 下怎么让 TP5 自动识别
这套代码没有把支付逻辑平铺在两个控制器里,而是在extend目录下做了一套轻量驱动框架。ThinkPHP 5 的extend目录自带命名空间自动加载能力,只要类名和文件路径对齐,控制器里use pay\Pay;就能直接实例化。目录结构大致长这样:
extend/pay/ ├── Pay.php // 统一入口,负责驱动分发 ├── Driver.php // 驱动抽象类,定义支付、退款、回调、查询四个契约 ├── exception/ │ └── PayException.php // 支付异常统一抛出 └── driver/ ├── Wechat.php // 微信支付驱动:统一下单、XML 解析、退款证书 ├── Alipay.php // 支付宝驱动:页面支付、RSA2 签名、表单跳转 └── Bank.php // 网银支付驱动:表单 POST 跳转类渠道的示例 application/ ├── config/pay.php // 微信、支付宝、网银的商户配置 ├── controller/Payment.php // 收银台、回调入口、退款入口 └── model/Order.php // 订单模型,金额单位统一为分看目录就能明白它的设计意图:业务层只依赖pay\Pay,不直接 new 任何一个渠道类。装新渠道就是往driver/目录丢一个类再改配置,订单控制器不需要动。这种分层对老项目改造特别友好,我的习惯是先跑通一个渠道再复制驱动模板,基本不会把现有业务拖下水。
2.2 统一入口与驱动抽象:业务层永远只调 Pay::driver()
核心的分发逻辑在extend/pay/Pay.php,整个类只有十几行,做两件事:拼接驱动类名、单例缓存实例。用ucfirst($name)把wechat变成Wechat,再映射到\pay\driver\Wechat这个类,存在则复用,不存在就抛异常。
<?php namespace pay; class Pay { protected static $drivers = []; public static function driver($name) { $class = '\\pay\\driver\\' . ucfirst($name); if (!class_exists($class)) { throw new \InvalidArgumentException("支付驱动 {$name} 不存在"); } if (!isset(self::$drivers[$name])) { self::$drivers[$name] = new $class(); } return self::$drivers[$name]; } }参数说明:$name是配置里约定的渠道标识,wechat、alipay、bank三选一;实际项目里也可以让客户端传一个channel字段,收银台拿到后调用Pay::driver($channel),前端和后端都只认字符串,不感知具体类。单例缓存的意义在于同一个请求里多次调用Pay::driver('wechat')不会重复初始化对象和加载证书。
Driver.php是抽象基类,它把支付渠道的差异收敛成四个动词:下单pay、退款refund、异步通知notify、主动查单query。每个驱动必须实现这四个方法,但对订单模块而言,它永远只面对这四个接口签名。
<?php namespace pay; abstract class Driver { protected $config; public function __construct(array $config = []) { $this->config = $config; } abstract public function pay(array $order); abstract public function refund(array $order); abstract public function notify(); abstract public function query(array $order); }参数说明:$order是业务层传入的订单数据,约定至少包含order_no(商户订单号)、amount(金额,单位分,整数)、subject(商品标题)。金额在驱动入口统一为分,微信直接用,支付宝在内部除以 100 转成元。这个约定是后来被金额差 100 倍坑过之后才立下的规矩。
2.3 协议差异收敛:XML、表单跳转、验签方式的统一策略
微信走的是 POST XML,支付宝走的是 GET 表单自动提交,网银又是另一套 POST 表单跳转协议。业务层不可能为每个渠道写一套收银台模板,所以驱动内部必须把差异吞掉。三个渠道的现实差异先列清楚:
| 维度 | 微信支付 | 支付宝 | 网银支付(示例) |
|---|---|---|---|
| 下单通信 | POST XML 到统一下单接口 | URL 参数签名跳转网关 | 表单 POST 跳转银行网关 |
| 签名算法 | MD5 或 HMAC-SHA256 | RSA2(SHA256withRSA) | 银行各自定义,多为 MD5 |
| 金额单位 | 分 | 元(两位小数) | 分或元,需配置确认 |
| 异步通知 | POST XML | POST 表单参数 | 同步 + 异步双通知 |
| 退款 | 需加载商户证书 | 仅签名,无需证书 | 部分银行需要证书 |
支付成功后统一返回一个数组:['pay_url' => '二维码内容或跳转地址', 'type' => 'wechat']。收银台只需要判断type决定展示二维码还是直接window.location跳转。回调入口也做了同样的事:控制器从路由参数里读type,调用对应驱动的notify(),驱动内部解析原始数据、验签、归一化成统一的订单状态结构,再交给业务层处理。这样一来,就算哪天要接第五个支付渠道,收银台和订单模块一行都不用改。
3. 核心实现拆解:统一下单、RSA2签名到异步回调的完整链路
3.1 微信统一下单:参数组包、MD5 签名与 XML 通信
微信支付的关键在统一下单接口,所有trade_type的入口都是它。Native 扫码、JSAPI 公众号支付、H5 支付,只是参数和后续跳转方式不同,下单这一步完全共用。下面的代码是driver/Wechat.php里pay()的骨架:
public function pay(array $order) { $params = [ 'appid' => $this->config['appid'], 'mch_id' => $this->config['mch_id'], 'nonce_str' => $this->nonceStr(), 'body' => $order['subject'], 'out_trade_no' => $order['order_no'], 'total_fee' => $order['amount'], // 单位:分 'spbill_create_ip' => request()->ip(), 'notify_url' => $this->config['notify_url'], 'trade_type' => $this->config['trade_type'], // NATIVE / JSAPI 'product_id' => $order['order_no'], ]; $params['sign'] = $this->sign($params); $xml = $this->arrayToXml($params); $response = $this->postXml($this->unifiedOrderUrl, $xml); $result = $this->xmlToArray($response); if ($result['return_code'] !== 'SUCCESS' || $result['result_code'] !== 'SUCCESS') { throw new \Exception('微信下单失败:' . $result['return_msg']); } // NATIVE 模式下返回 code_url,用它生成二维码给用户扫 return ['pay_url' => $result['code_url'], 'type' => 'wechat']; }sign()方法的实现值得单独说。微信的签名规则是所有非空参数按键名 ASCII 升序排列,拼成key1=value1&key2=value2,末尾再拼上&key=商户密钥,做 MD5 后转大写。注意参与签名的值不能做 URL 编码,密钥也不是配置里的 APIv3 密钥,而是 V2 接口的 API 密钥(在商户平台「API 安全」里设置)。
protected function sign(array $params) { ksort($params); $string = ''; foreach ($params as $key => $value) { if ($value === '' || $value === null) { continue; } $string .= $key . '=' . $value . '&'; } $string = rtrim($string, '&') . '&key=' . $this->config['key']; return strtoupper(md5($string)); }签名踩得最多的坑是nonce_str每次都一样,微信会直接拒绝;另外arrayToXml时必须把sign节点放在最后,否则部分版本的服务端校验会报签名错误。XML 解析用simplexml_load_string($response, 'SimpleXMLElement', LIBXML_NOCDATA),不然CDATA包裹的return_msg会丢内容,这是排查支付失败订单时最容易忽略的细节。
3.2 支付宝页面支付:RSA2 签名串与跳转 URL 的生成
支付宝的电脑网站支付走的是alipay.trade.page.pay,下单本质上就是拼一个带签名的跳转 URL。和微信最大的区别是:参数拼进 URL 之前要签名,签名基于原始字符串,不是编码后的字符串。驱动里的pay()大致是这副样子:
public function pay(array $order) { $bizContent = [ 'out_trade_no' => $order['order_no'], 'total_amount' => number_format($order['amount'] / 100, 2, '.', ''), 'subject' => $order['subject'], 'product_code' => 'FAST_INSTANT_TRADE_PAY', ]; $params = [ 'app_id' => $this->config['app_id'], 'method' => 'alipay.trade.page.pay', 'charset' => 'utf-8', 'sign_type' => 'RSA2', 'timestamp' => date('Y-m-d H:i:s'), 'version' => '1.0', 'notify_url' => $this->config['notify_url'], 'return_url' => $this->config['return_url'], 'biz_content' => json_encode($bizContent, JSON_UNESCAPED_UNICODE), ]; $params['sign'] = $this->rsa2Sign($params); return ['pay_url' => $this->gateway . '?' . http_build_query($params), 'type' => 'alipay']; }这里有两个细节会在联调时反复折磨人。第一,金额必须是元,且最多两位小数,所以total_amount = amount / 100之后要number_format,直接除再拼字符串会出浮点精度问题,比如 0.29 变成 0.29000000000000004。第二,biz_content用JSON_UNESCAPED_UNICODE编码,如果中文被转成\uXXXX,签名串和实际请求参数对不上,验签必失败。
rsa2Sign()的拼装要求和微信完全不同。支付宝要求所有参数按 ASCII 排序,过滤空值,拼成key1=value1&key2=value2,然后直接用私钥做OPENSSL_ALGO_SHA256签名。这里不能用http_build_query的输出直接签,因为它的空格会被编码成+,而支付宝验签时会把空格还原成%20,两边一对比就出错。我在驱动里是手工遍历参数拼原串,保证和验签侧完全一致。
protected function rsa2Sign(array $params) { ksort($params); $string = ''; foreach ($params as $key => $value) { if ($value === '' || $value === null) { continue; } $string .= $key . '=' . $value . '&'; } $string = rtrim($string, '&'); openssl_sign($string, $sign, $this->config['private_key'], OPENSSL_ALGO_SHA256); return base64_encode($sign); }配置里的private_key是商户应用私钥,public_key是支付宝公钥,两个都是 PEM 格式字符串。私钥要注意是 PKCS1 还是 PKCS8,商户后台下载时选错了,openssl_sign直接返回 false,而且不报任何 PHP 异常。我一般写配置时顺手在注释里标清楚格式,避免换机器部署时玄学报错。
3.3 异步回调处理:验签、金额比对与幂等更新
支付成功之后,微信和支付宝都会向notify_url发异步通知。这是整个支付集成里最容易翻车的部分:验签没过、金额没比对、回调重复到达、更新顺序错了,都会导致订单状态混乱。驱动统一提供一个notify(),把两家渠道的差异在入口处理掉,业务层看到的已经是清洗后的数据。
public function notify() { // 微信是 XML,支付宝是 POST 表单,驱动内各自解析成 $data if (!$this->verifySign($data)) { return 'FAIL'; } // 归一化:无论微信还是支付宝,最终都有 out_trade_no 和 amount(单位分) $orderNo = $data['out_trade_no']; $amount = $data['amount']; Db::startTrans(); try { $order = Db::name('orders') ->where('order_no', $orderNo) ->lock(true) ->find(); if (!$order || $order['status'] == 1) { Db::commit(); return $this->successText(); // 已处理的订单直接返回成功,渠道方停止重发 } if ($order['amount'] != $amount) { throw new \Exception("金额不一致:订单 {$orderNo} 应收 {$order['amount']} 实收 {$amount}"); } Db::name('orders')->where('order_no', $orderNo)->update([ 'status' => 1, 'paid_at' => time(), ]); Db::commit(); return $this->successText(); } catch (\Exception $e) { Db::rollback(); \think\Log::record('支付回调异常:' . $e->getMessage(), 'pay'); return $this->failText(); } }lock(true)是 TP5 查询构造器里的排他锁,配合事务使用,能在并发场景下避免两个回调同时读到“待支付”状态。实际生产中渠道方超时后会重发通知,同一个订单可能收到三五次回调,没有锁的话第一次更新后第二次又进事务,库存逻辑容易被重复执行。加了锁之后,后到的请求会等第一个事务提交,再读到status == 1直接返回成功,这就是幂等处理的核心思路。
verifySign在微信侧是重新算一遍签名比对,支付宝侧是用支付宝公钥验签。这里要特别注意:验签原始数据是渠道方发过来的原始报文,不能拿 PHP 解析后的参数重新拼接再去验,微信的验签要用原始 XML 里所有字段重新组包,支付宝则用原始 POST 参数按签名规则拼串。我见过有人把解析后的数组json_encode再去验签,结果自然是 FAIL。
3.4 退款与订单查询:证书加载和回滚思路
退款接口的差异在微信和支付宝之间特别明显。支付宝退款只是再签一次名,走alipay.trade.refund,和下单几乎同构;微信退款必须加载商户证书,否则接口直接报“证书序列号不存在”。微信退款的驱动代码需要额外处理 curl 的证书参数:
public function refund(array $order) { $params = [ 'appid' => $this->config['appid'], 'mch_id' => $this->config['mch_id'], 'nonce_str' => $this->nonceStr(), 'out_trade_no' => $order['order_no'], 'out_refund_no' => $order['refund_no'], // 退款单号,业务侧生成 'total_fee' => $order['amount'], 'refund_fee' => $order['refund_amount'], ]; $params['sign'] = $this->sign($params); $xml = $this->arrayToXml($params); $response = $this->postXml($this->refundUrl, $xml, [ CURLOPT_SSLCERT => $this->config['cert_path'], CURLOPT_SSLKEY => $this->config['key_path'], ]); // 解析 response,校验 return_code / result_code,然后记录 flow_no }cert_path指apiclient_cert.pem,key_path指apiclient_key.pem,两份文件建议放在项目cert/目录下,用ROOT_PATH拼绝对路径。证书有效期通常是 5 年,但我在维护项目时发现,财务在商户平台重新下载证书后只更新了本地后台没同步服务器,结果生产退款挂了半天。退款接口没有异步通知,必须靠返回的result_code判断结果,所以退款状态要么主动调用查询接口对账,要么在失败时把接口返回值完整记日志。这套驱动里query()就是为对账准备的,按订单号查渠道真实状态,回写本地表。
4. 部署与联调:密钥配置、数据表和一次真实支付测试
4.1 配置文件与密钥准备
项目里的application/config/pay.php是所有渠道参数的集中地。配置文件建议不要写进版本库,至少私钥部分要单独用config_pay.php.local这类文件覆盖。字段结构如下:
return [ 'wechat' => [ 'appid' => 'wx1234567890abcdef', // 公众号/小程序 AppID 'mch_id' => '1600000000', // 微信商户号 'key' => 'V2接口密钥,32位字符串', // 商户平台 API 安全里设置 'trade_type' => 'NATIVE', // NATIVE 扫码 / JSAPI 公众号 'notify_url' => 'https://pay.example.com/index/payment/notify/type/wechat', 'cert_path' => ROOT_PATH . 'cert/apiclient_cert.pem', 'key_path' => ROOT_PATH . 'cert/apiclient_key.pem', ], 'alipay' => [ 'app_id' => '2021000000000000', 'private_key' => '-----BEGIN RSA PRIVATE KEY-----\n...', 'public_key' => '-----BEGIN PUBLIC KEY-----\n...', 'gateway' => 'https://openapi.alipay.com/gateway.do', 'notify_url' => 'https://pay.example.com/index/payment/notify/type/alipay', 'return_url' => 'https://pay.example.com/index/payment/return', ], ];notify_url必须是一个外网能访问到的 HTTPS 地址,微信支付侧会校验收到的回调地址不能是 IP,也不能是没备案的域名。支付宝的沙箱环境网关换成https://openapi.alipaydev.com/gateway.do,密钥也要在沙箱应用里单独生成,和正式环境完全不互通。配置完成后写一个小脚本把密钥内容打印出来前几位和后几位,确认换行、头尾标记都完整,再进联调。
4.2 订单、流水与日志表设计
支付集成不能只盯着一张订单表,至少要有订单、流水、日志这三层数据。订单表只记录业务状态,流水表记录渠道侧的交易号、回调原文和验签结果,日志表给排查用。精简后的建表语句,能覆盖回调、退款、对账三类场景:
CREATE TABLE `orders` ( `id` int(11) NOT NULL AUTO_INCREMENT, `order_no` varchar(32) NOT NULL COMMENT '业务订单号', `amount` int(11) NOT NULL COMMENT '金额,单位分', `channel` varchar(16) NOT NULL COMMENT 'wechat / alipay / bank', `status` tinyint(4) NOT NULL DEFAULT '0' COMMENT '0待支付 1已支付 2已退款', `paid_at` int(11) DEFAULT NULL COMMENT '支付时间戳', `created_at` int(11) DEFAULT NULL, PRIMARY KEY (`id`), UNIQUE KEY `uk_order_no` (`order_no`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='业务订单表'; CREATE TABLE `payment_logs` ( `id` int(11) NOT NULL AUTO_INCREMENT, `order_no` varchar(32) NOT NULL COMMENT '关联订单号', `channel` varchar(16) NOT NULL, `transaction_id` varchar(64) DEFAULT NULL COMMENT '渠道交易号', `raw_data` text COMMENT '回调原始报文,原样记录', `result` varchar(8) DEFAULT NULL COMMENT 'success / fail', `created_at` int(11) DEFAULT NULL, PRIMARY KEY (`id`), KEY `idx_order_no` (`order_no`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='支付回调流水表';流水表和订单表分离的原因是:一笔订单可能收到多次回调、多次退款尝试,如果都写进订单表会造成字段爆炸,而且原始报文是排查问题的第一手证据。对账时把两张表按order_no关联,查出“渠道侧有支付记录但本地订单未更新”的异常单,就是一个完整的对账脚本。回调日志我习惯直接用 TP5 的Log::record,记录渠道、订单号、原始报文、验签结果、处理结果,一行 JSON,排查问题时grep order_no就能把整条链路拉出来。
4.3 从沙箱到真实支付的联调顺序
联调顺序我建议严格按“支付宝沙箱 → 微信小额真实支付 → 回调幂等测试”三步走,每一步都有对应的验证手段。第一步,在支付宝开放平台创建沙箱应用,把gateway切到沙箱地址,用沙箱买家账号完成一笔真实的下单流程,验证签名、跳转、异步回调三条链路。沙箱的异步通知是真实的,能直接看到notify_url是否收到 POST。
第二步,微信支付没有官方沙箱,我的做法是配一个最低价商品,真实支付一块钱,支付成功后立刻走退款流程把资金回退,既验证了下单也验证了证书配置。这一步要提前确认notify_url的域名已经通过了微信支付侧的校验,否则接口返回成功但回调根本打不进来。本地调试时用内网穿透把回调地址映射到开发机,微信侧看到的是合法域名,请求能真正打到本地代码里。
第三步,模拟重复回调。用 curl 往本地回调地址发一个之前记录下来的真实回调报文,第一次应该返回success,第二次同样返回success但业务表只更新一次,流水表多一条记录。这个测试能直接暴露幂等逻辑是否生效:
curl -X POST "https://pay.example.com/index/payment/notify/type/wechat" \ -H "Content-Type: application/xml" \ -d '<xml><out_trade_no>20250101001</out_trade_no>...</xml>'这个假回调用没有正确签名的报文,会被验签拦截返回 FAIL,属于正常现象;要测真正链路,得从日志里把渠道原始回调原文抠出来,原样重放。重放时观察数据库状态和流水表,就能确认幂等和验签都在真实工作。
5. 避坑笔记:回调、证书、金额单位三座山的翻车复盘
5.1 回调类问题:地址校验、日志黑匣子与重复通知
现象一:微信下单返回成功,但异步通知一直没有到达,后台订单停留在待支付。原因:notify_url里配的是http://或者是 IP 地址,微信支付侧对回调地址有严格校验,要求必须是 HTTPS 且域名与商户平台配置一致。解决:检查配置里的notify_url是否使用备案过的 HTTPS 域名;本地联调时用内网穿透映射到开发机,穿透工具的临时域名通常能通过校验。
现象二:线上某笔订单渠道侧显示已支付,本地订单状态没变,翻日志发现回调入口完全没记录。原因:回调入口没有第一步就写日志,代码在解析或验签阶段直接抛异常,PHP 又没开display_errors,异常全被吞了。解决:notify()方法第一行先记录原始报文,再把整个处理包进try-catch,catch里记录异常栈并返回FAIL。那条日志就是后续排查的入口,没有它等于让渠道方黑匣子。
现象三:同一个订单渠道方通知了三次,库存被扣了三次。原因:回调处理没有幂等,线程 A 先查订单是待支付,线程 B 同时查到也是待支付,两边都执行了更新。解决:用lock(true)配合事务,先锁行再查状态,已支付订单直接返回成功。注意锁一定要加在事务内部,TP5 里Db::startTrans()之后再用lock(true),否则锁的边界不对。
5.2 密钥、证书与金额单位:三处最隐蔽的翻车点
现象四:页面显示订单 1 元,微信回调里的total_fee是 100,支付宝回调里的total_amount是 1.00,订单状态判断直接失败。原因:金额单位没有在驱动入口统一,微信要分、支付宝要元,业务层混用。解决:业务层全部用“分”和整数运算,微信驱动直接用$order['amount'],支付宝驱动number_format($order['amount'] / 100, 2),不让单位换算散落在业务代码里。
现象五:支付宝验签一直返回 FAIL。原因:配置里把“应用私钥”和“支付宝公钥”填反了,或者密钥字符串从后台复制下来时带了换行符和-----BEGIN头尾被存进数据表。解决:在config/pay.php里载入密钥文件而不是粘贴字符串;如果必须放字符串,PHP 读进来后先str_replace("\n", '')再包回 PEM 头尾。确认sign_type是 RSA2,RSA 本身是 SHA1 签名,接口会明确拒绝。
现象六:微信退款报“证书序列号不存在”。原因:商户平台重新下载了证书,但服务器上的apiclient_cert.pem和apiclient_key.pem没有同步更新,或者配置路径指向了旧文件。解决:证书下载后第一时间同步所有服务器,在配置里用ROOT_PATH拼绝对路径,部署时写一个校验脚本检查证书有效期;退款接口调用后把响应里return_code和result_code都写日志,不要只记成功。顺便说一句,证书文件不要放 web 可访问目录,cert/目录要加访问限制或放 webroot 之外。
6. 二次开发:三天扩展一个新支付驱动的标准化动作
6.1 新的支付渠道如何接入
这套驱动最大的价值在于接入成本恒定。新增一个渠道,不是从零写一套支付逻辑,而是复制driver/Bank.php这样一个模板,实现四个方法:pay、refund、notify、query。一个新渠道的骨架代码不超过 80 行,核心是参数映射和签名算法:
<?php namespace pay\driver; use pay\Driver; class Other extends Driver { public function pay(array $order) { // 1. 把 $order 里的 order_no / amount / subject 映射成渠道参数 // 2. 按渠道规则签名 // 3. 返回 ['pay_url' => '跳转地址或二维码内容', 'type' => 'other'] } public function notify() { // 1. 记录原始报文 // 2. 验签 // 3. 归一化成 ['out_trade_no' => ..., 'amount' => ...] // 4. 调公共的订单更新逻辑 } public function refund(array $order) { // 按渠道文档组请求参数并签名 } public function query(array $order) { // 主动查询渠道订单状态,用于对账 } }接入新渠道时,三处改动就够了:config/pay.php增加一个渠道配置块;extend/pay/Driver.php不用动;收银台模板里如果新渠道需要不同的展示形式,比如银联需要表单自动提交,就在收银台判断type == 'other'时输出一个自动提交的 HTML 表单。业务层其他代码零改动。网银支付这一类渠道虽然同步回调和异步回调的参数名和微信完全不一样,但驱动隔离了这些差异,订单模型永远只面对统一的out_trade_no和amount。
6.2 一个对账验证的收尾习惯
扩展完驱动之后,我做联调时会先画一张参数映射表:业务字段对应渠道字段、金额单位、签名方式、回调字段。表画完,代码基本已经完成了一半。再准备一份模拟回调报文,按渠道文档构造签名,用 curl 重放一遍,观察流水表和订单表的更新结果。真正上线前强制走一遍query()对账,支付和退款各测一次,确认渠道状态和本地状态能对上。
从那以后我每次接支付渠道都强制先画三张清单:参数映射、回调校验、日志字段,写完才动手写驱动代码。这套流程救过我很多次,尤其是几个月后要维护一个半年没碰过的支付项目时,资料比记忆可靠得多。希望帮到你。
本文还有配套的精品资源,点击获取