业务需求,只对接了微信统一下单,包括统一下单、支付结果查询、支付结果回调通知、退款申请、退款结果查询、退款结果通知。其他的支付也大差不差,值得参考一下。
一、准备工作
1.联系招行对接人员获取接口文档,接口文档里面会有APPID、APPSECRET、公钥、私钥等测试参数。
2.异步通知回调地址需要提前提供招行对接人员(测试环境和生产环境均需要),因为招行开通需要一点时间,避免影响开发。
3.对接微信统一下单接口,需要提供小程序/公众号APPID、小程序/公众号主体名称、支付授权目录给招行对接人员。
二、代码
1.安装SDK
招商银行聚合支付SDKhttps://gitee.com/tcmtang/cmb-pay-sdk2.使用
<?php
namespace app\common\service\pay;
use Cmb\Factory;
use Cmb\Kernel\Utils;
/**
* 招商银行聚合支付-微信统一下单
*/
class CmbPayService extends BasePayService
{
//实例
protected $app;
//支付成功回调地址
protected $notifyUrl = '/pay/cmbPayNotify';
//退款成功回调地址
protected $returnNotifyUrl = '/pay/cmbReturnNotify';
/**
* 初始化配置
*/
public function __construct($terminal,$userId = null)
{
$config = [
// APP ID
'appid' => '',
// APP SECRET
'secret' => '',
// 商户号
'mer_id' => '',
// 收银员 ID
'user_id' => '',
// 商户私钥
'private_key' => '',
// 招行公钥
'cmb_public_key' => '',
// 签名方法,默认使用 01(RSA2)
'sign_method' => '02', // 01(RSA2) or 02(SM2)
// sm2 签名程序目录,绝对路径,如果签名失败确认文件权限组、权限是否正确
// 建议将 ./bin 目录下的 sm2 与 sm2.exe 拷贝出来,然后给予 755 权限。因为 composer update 该扩展包时,文件权限组可能会被修改
// 我把 ./bin 目录复制到了 server/extend 目录下,然后修改 ./bin 目录名称为 sm2 ,并把目录下的文件都给了 755 权限
'bin_path' => root_path().'extend/sm2',
// 是否开启测试环境
'test' => true,
// 事件监听
'events' => [
// 将以下 Listener 类换成自己的 Listener 类即可
'listen' => [
// 请求前事件
\Cmb\Kernel\Events\BeforeRequest::class => [
// 支持两种监听方式
// function ($event) { var_dump($event->base);die; }
[new \Cmb\Kernel\Listeners\BeforeRequestListener(), 'handle']
],
// 请求完成事件
\Cmb\Kernel\Events\HttpResponseCreated::class => [
[new \Cmb\Kernel\Listeners\HttpResponseCreatedListener(), 'handle']
]
]
]
];
$this->app = Factory::payment($config);
}
/**
* @notes 微信统一下单(默认使用小程序支付)
*/
public function pay($from,$order)
{
try {
//支付参数
$params = [
'subAppId' => '',//小程序APPID
'orderId' => '',//商户订单号 商户端生成,要求此订单号在整个商户下唯一
'tradeType' => 'JSAPI',//交易类型 APP支付:APP 公众号支付:JSAPI 小程序支付:JSAPI
'body' => '',//商品描述
// 'goodsDetail' => '',//商品详细描述
// 'goodsTag' => '',//商品标记,代金券或立减优惠功能的参数
// 'attach' => '',//附加数据
'mchReserved' => '',//商户保留域 用于传输商户自定义数据,在支付结果查询和交易结果通知时原样返回
'payValidTime' => '',//支付数据的有效时间,单位为秒,应不小于60
'notifyUrl' => request()->domain(true) . '/' . $this->notifyUrl,//交易通知地址 支付结果将送到本地址
'txnAmt' => 0,//交易金额 单位为分,txnAmt=orderOrigAmt-orderCouponAmt
// 'orderOrigAmt' => 0,//订单原始金额 单位为分,和orderCouponAmt同时出现
// 'orderCouponAmt' => 0,//订单优惠金额 单位为分,和orderOrigAmt同时出现
'currencyCode' => 156,//交易币种 默认156,目前只支持人民币(156)
'spbillCreateIp' => request()->ip(),//终端IP 必须传正确的用户端IP
// 'limitPay' => '', //限定支付方式 no_credit--指定不能使用信用卡支付;为空表示无限制
// 'openId' => '',//用户标识 用户在Appid下的唯一标识。
'subOpenId' => '',//用户子标识 trade_type=JSAPI,此参数必传,用户在subAppId下的唯一标识。
// 'sceneInfo' => '',//场景信息 H5支付支付必填,公众号、APP支付和小程序支付不填
// 'encryptIdentity' => self::sign([
// 'need_check' => 'F',//是否校验身份,T是F否
// 'name' => '',//名字
// 'number' => '',//证件号
// 'mobile' => '',//手机号
// 'type' => 'IDCARD',//证件类型,只支持身份证
// ]),//实名支付信息
// 'policyNo' => '',//保单单号 保险实名支付对接中保信使用,由保险公司生成,须与实名认证上送中保信的“支付单号”一致。如果交易有上送此字段则交易成功后会通知中保信做见费出单操作。该字段和region需同时出现或同时不出现。
// 'region' => '',//地区码 缴税系统地区码按指引填写
// 'paymentNo' => '',//上海保险平台交易号 上海保险平台交易号
// 'encryptTerminalInfo' => '',//终端信息 商户侧受理终端信息
// 'limitPayer' => '',//指定支付者 上传此参数, 可限制用户只有是成年人才能支付.ADULT: 成年人。
// 'encryptTradeAddressInfo' => '',//交易地址信息 交易地址信息
];
//微信统一下单
$result = $this->app->polypay->onlinePay($params);
if ($result['returnCode'] !== 'SUCCESS' || $result['respCode'] !== 'SUCCESS') {
throw new \Exception($result['errCode'].'-'.$result['respMsg']);
}
return [
'config' => json_decode($result['biz_content'], true)['payData']
];
} catch (\Exception $e) {
throw new \Exception($e->getMessage());
}
}
/**
* @notes 支付查询
* 仅支持15天以内的交易查询,超过15天的交易发起查询将返回CMB_ORDERID_NOT_EXIST或NOT_ALLOW。
* 商户在一定时间内未收到支付结果通知的情况下必须主动发起支付结果查询。
* 支付结果查询应做以下处理:微信统一下单场景下建议商户在拉起支付5秒钟之后第一次发起查询,后续每5-10秒查询一次,查询10次之后还未支付,建议调关单接口关闭此笔订单并生成新的订单供用户再次支付。
* 若查询返回的returnCode和respCode为SUCCESS且tradeState为C(订单已关闭)、D(订单已撤销)、F(交易失败)、R(转入退款,表示已支付成功并发起了退款申请),或respCode未FAIL且errCode为ORDERID_INVALID(订单已失效),则商户无需再次发起查询。
*/
public function payQuery($orderSn)
{
$result = $this->app->polypay->orderQuery($orderSn);
if ($result['returnCode'] !== 'SUCCESS' || $result['respCode'] !== 'SUCCESS') {
throw new \Exception($result['errCode'].'-'.$result['respMsg']);
}
return $result;
}
/**
* @notes 敏感信息加密
*/
public function sign($data)
{
return Utils::sign(
$data,
$this->app['config']->get('private_key'),
$this->app['config']->get('sign_method', '01'),
$this->app['config']->get('bin_path')
);
}
/**
* @notes 退款申请
*/
public function refund($data)
{
if (!empty($data["transaction_id"])) {
$refundData = [
'orderId' => '',//商户退款订单号 商户端生成退款订单号,要求此订单号整个商户下唯一
'origOrderId' => '',//原交易商户订单号 原交易的商户订单号,此字段和原交易平台订单号字段至少要上送一个,若两个都上送,则以原交易平台订单号为准
// 'origOutOrderId' => '',//原外部商户订单号 支付宝先享后付订单的交易必须上送。支付宝先享后付-订单创建请求参数中的outOrderId
'origCmbOrderId' => '',//原交易平台订单号 原交易招行订单号,此字段和原交易商户订单号字段至少要上送一个,若两个都上送,则以此字段为准
'notifyUrl' => request()->domain(true) . '/' . $this->returnNotifyUrl,//交易通知地址 若为空则通知到原交易的通知地址
'txnAmt' => 0,//交易金额 原交易金额,单位为分
'refundAmt' => 0,//退款金额 单位为分,refundAmt=refundOrigAmt-refundCouponAmt;
// 'refundOrigAmt' => 0,//退单原始金额 单位为分,与refundCouponAmt同时出现
// 'refundCouponAmt' => 0,//退单优惠金额 单位为分,与refundOrigAmt同时出现
'currencyCode' => 156,//交易币种
// 'refundReason' => '',//退款原因
// 'mchReserved' => '',//商户保留域 用于传输商户自定义数据,在退款结果查询和退款结果通知时原样返回
// 'acqAddnData' => '',//银联收款方附加数据 银联通道且有商品优惠时出现
// 'goodsDetail' => '',//支付宝商品列表 支付宝通道且有商品优惠时出现
// 'queryOptions' => 156,//查询选项 商户通过上送该参数来定制同步需要额外返回的信息字段(支付宝),数组形式的字符串
// 'contractRefundReq' => '',//合约退款 json格式字符串
// 'encryptTradeAddressInfo' => '',//交易地址信息
];
return $this->app->polypay->refund($refundData);
} else {
return false;
}
}
/**
* @notes 退款查询
* 仅支持15天以内的查询,超过15天的交易发起查询将返回CMB_ORDERID_NOT_EXIST或NOT_ALLOW。
* 异步查询退款申请处理结果,查询机制建议30秒查一次,查询10分钟仍为P状态则停止查询并以T+1日对账单中状态为准。
*/
public function refundQuery($refundSn)
{
return $this->app->polypay->refundQuery($refundSn);
}
/**
* @notes 支付成功回调
* 只有成功支付的交易才会有支付结果通知,支付结果通知招行需要提前申请到商户回调地址的网络,正常需要2-3个工作日。
* 商户不得依赖招行的支付结果通知,必须在一定时间内未收到通知时主动发起支付结果查询。
*/
public function notify()
{
$response = $this->app->handlePaidNotify(function ($message, $fail) {
//回调数据
$bizContent = json_decode($message['biz_content'], true);
//支付成功逻辑处理
return true; // 返回处理完成
});
$response->send();
}
/**
* @notes 退款退款成功回调
* 只有退款成功的交易才会有退款结果通知
*/
public function returnNotify()
{
$response = $this->app->handlePaidNotify(function ($message, $fail) {
//回调数据
$bizContent = json_decode($message['biz_content'], true);
//退款成功逻辑处理
return true; // 返回处理完成
});
$response->send();
}
}
三、申请对接生产环境
1.必须实现微信统一下单接口、支付结果查询接口、退款申请接口、退款结果查询接口。这是招行要求必须实现的接口,填写测试用例需要用到。(商户不得依赖招行的支付结果通知,必须在一定时间内未收到通知时主动发起支付结果查询。)
2.填写验收测试用例,等待招行验收(需要用到账单详情截图和接口报文截图)。
3.测试用例验收通过之后,再根据招行对接人员提供的生产环境获取参数指引文档操作,拿到生产环境参数,这次支付对接就算完成了。
四、注意事项
1.在测试环境联调时支付的金额必须当天进行退款,测试环境隔日无法退款。
2.如果签名失败,请确认sm2文件权限组、权限是否正确
3.招行提供了加签结果校验的Web测试页面,测试地址为:http://api-polypay-uat.cs.cmburl.cn/signtest/4.必须实现定时任务主动查询未成功支付订单的支付结果,不能依赖招行的支付结果通知,否则无法验收通过。
5.特别特别要注意,小程序开启了发货信息管理功能的,将无法使用招行的聚合支付,一定要注意。