前言
最近因为工作原因对接工商银行微信聚合支付接口和二维码扫码支付接口,网上php教程太少,因此记录一下对接步骤。
程序开发之前要做的准备工作
1、阅读相关接口文档,熟悉工行支付接口调用流程,接口文档地址如下:工商银行接口文档。附:聚合支付接口,二维码扫码支付接口
2、准备相关的参数,通用参数为:
appid:工商银行appid
mer_id:工商银行商户账号,商户线下档案编号,特约商户12位,特约部门15位
store_code:e生活档案编号
icbc_pulic_key:网关公钥
private_key:应用私钥
public_key:应用公钥
encryptKey:AES加密密钥
2.1、相关参数来源
appid,mer_id,store_code,icbc_pulic_key皆为工行工作人员提供,
private_key应用公私钥对可点击下载javaSdk后自己生成。
Windows环境,以生成RSA算法为例:
进入到bin目录,双击keygen_rsa.bat文件,生成一对RSA公钥和密钥。
Linux环境,以生成RSA算法为例:
切换到bin目录,运行 ./keygen_rsa.sh命令,生成一对RSA公钥和密钥。
生成公私钥对后将公钥给工作人员配置。
注意还必须提供encryptKey(AES加密密钥)配置,用于算法的加解密。
3、和工作人员申请开通相关接口,工行是分接口开放的,接口必须配置到你的账户下,才能进行调用。
4、下载工行的phpsdk
注意事项:里面要求安装php_infosec扩展,此扩展是为了完成ca验证进行安装的,公众号聚合支付和二维码扫码支付接口一般用不到ca验证,只用RSA2和AES的话应该用不到,因为都是公开算法。
5、代码示例,使用代码类的时候,主要要先将你的SDK文件引入类中。
/**
* Created by PhpStorm.
* User: lv
* Date: 2020/3/10
* Time: 13:37
*/
/**
* 功能说明:自定义工商银行支付接入类
*/
class IcbcPay
{
function __construct()
{
//引入文件
require_once(ROOT_PATH . 'data/extend/icbc/icbc-api-sdk-cop-php/DefaultIcbcClient.php');
require_once(ROOT_PATH . 'data/extend/icbc/icbc-api-sdk-cop-php/UiIcbcClient.php');
}
public function index()
{
// 防止默认目录错误
}
/**
* 基本设置读取
*
* @return unknown
*/
public function getIcbcpayConfig()
{
// 工商银行appid(颁发的必填参数)
$icbc_config['appid'] = '';
// 工商银行商户账号(商户线下档案编号(特约商户12位,特约部门15位))
$icbc_config['mer_id'] = '';
// e生活档案编号
$icbc_config['store_code'] = '';
// 接口号,目前仅支持上送1.0.0.1
$icbc_config['interface_version'] = '1.0.0.1';
// 第三方应用ID;商户在微信公众号内接入时必送,此处设定为微信公众号的appid
$icbc_config['tp_app_id'] = '';
// 网关公钥
$icbc_config['icbc_pulic_key'] = '';
// 应用私钥
$icbc_config['private_key'] = '';
// 应用私钥
$icbc_config['encrypt_key'] = '';
return $icbc_config;
}
/**
* @param $msg_id //消息通讯唯一编号,每次调用独立生成,APP级唯一
* @param $tp_open_id //第三方用户标识;商户在微信公众号/支付宝生活号内接入时必送,上送用户在商户appid下的唯一标识。
* @param $out_trade_no //商户订单号;需保证商户系统唯一
* @param string $tran_type //交易类型 OfflinePay-线下支付,OnlinePay-线上支付
* @param $goods_body //商品描述 芬达
* @param $goods_detail //商品详情 {‘good_name’:’芬达橙味300ml罐装’,’good_id’:’1001’,’good_num’:’1’}
* @param $order_amount // 总金额(单位:分)
* @param $return_url // 支付成功回显页面。支付成功后,客户端引导跳转至该页面显示
* @param $notify_url // 支付结果通知地址;
* @param $mer_hint // 商家提示。目前暂无处理,后续可用于在交易页面回显给客户
* @param $attach // 附加数据。商户可上送定制信息
* @return string
* 当前接口参数类型全部为字符串类型,调用时注意!
*/
public function setAPV2Pay($msg_id, $tp_open_id, $out_trade_no, $tran_type = 'OnlinePay', $goods_body, $goods_detail, $order_amount, $return_url, $notify_url, $mer_hint, $attach)
{
$icbc_config = $this->getIcbcpayConfig();
//第三方应用ID;商户在微信公众号内接入时必送,上送微信分配的公众账号ID;商户通过支付宝生活号接入时必送,上送支付宝分配的应用ID。
$tp_app_id = $icbc_config['tp_app_id'];
//分期期数。目前仅支持1-不分期,注意必须是字符串类型
$install_times = '1';
//通知类型,表示在交易处理完成后把交易结果通知商户的处理模式。 取值“HS”:在交易完成后将通知信息,主动发送给商户,发送地址为notify_url指定地址; 取值“AG”:在交易完成后不通知商户
$notify_type = 'HS';
//结果发送类型,通知方式为HS时有效。取值“0”:无论支付成功或者失败,银行都向商户发送交易通知信息;取值“1”,银行只向商户发送交易成功的通知信息,注意必须是字符串类型
$result_type = '0';
$content = array(
"interface_version" => (string)$icbc_config['interface_version'],
"mer_id" => (string)$icbc_config['mer_id'],
"tp_app_id" => (string)$tp_app_id,
"tp_open_id" => (string)$tp_open_id,
"out_trade_no" => (string)$out_trade_no,
"tran_type" => (string)$tran_type,
"order_date" => (string)date("YmdHis", $_SERVER['REQUEST_TIME']),
"end_time" => (string)date("YmdHis", $_SERVER['REQUEST_TIME'] + 300), // order_date之后5分钟
"goods_body" => (string)$goods_body,
"goods_detail" => (string)$goods_detail,
"order_amount" => (string)bcmul($order_amount, 100), //总金额(单位分)
"spbill_create_ip" => (string)$_SERVER['REMOTE_ADDR'],
"install_times" => (string)$install_times,
"return_url" => (string)$return_url, // 支付回显
"notify_url" => (string)$notify_url, // 支付结果通知
"notify_type" => (string)$notify_type,
"result_type" => (string)$result_type,
);
if (!empty($attach)) $content['attach'] = $attach;
if (!empty($mer_hint)) $content['mer_hint'] = $mer_hint;
//接口调用地址
$serviceUrl = 'https://gw.open.icbc.com.cn/ui/aggregate/payment/request/V2';
//调用参数合集
$request = array(
"serviceUrl" => $serviceUrl,
"method" => 'POST',// 请求方法,只能是POST或GET
"isNeedEncrypt" => true,// 是否需要加密
"extraParams" => null,//其他参数,用数组类型array
"biz_content" => $content,
);
//组成form表单提交
$resp = $this->icbcClientForm($icbc_config, $request, $msg_id);
return $resp;
}
/**
* 工商银行获取支付二维码
* 当前接口参数类型全部为字符串类型,调用时注意。
*/
public function getPayQrCode($msg_id, $out_trade_no, $pay_expire, $order_amt, $notify_url, $attach)
{
$icbc_config = $this->getIcbcpayConfig();
//商户是否开启通知接口 0-否;1-是;非1按0处理,注意必须是字符串类型
$notify_flag = '1';
//扫码后是否需要跳转分行,0:否,1:是;非1按0处理,注意必须是字符串类型
$sp_flag = '0';
$content = array(
'mer_id' => (string)$icbc_config['mer_id'],
'out_trade_no' => (string)$out_trade_no,
'store_code' => (string)$icbc_config['store_code'],//e生活档案编号
'order_amt' => (string)bcmul($order_amt, 100), //总金额(单位分)
'trade_date' => (string)date("Ymd", $_SERVER['REQUEST_TIME']), //订单生成日期
'trade_time' => (string)date("His", $_SERVER['REQUEST_TIME']), //订单生成时间
'tporder_create_ip' => (string)$_SERVER['REMOTE_ADDR'],
'notify_url' => (string)$notify_url, // 支付结果通知
'notify_flag' => (string)$notify_flag,
'sp_flag' => (string)$sp_flag,
'pay_expire' => (string)$pay_expire,//二维码过期时间
);
if (!empty($attach)) $content['attach'] = $attach;
//接口调用地址
$serviceUrl = 'https://gw.open.icbc.com.cn/api/qrcode/V2/generate';
//调用参数合集
$request = array(
"serviceUrl" => $serviceUrl,
"method" => 'POST',// 请求方法,只能是POST或GET
"isNeedEncrypt" => false,// 是否需要加密
"extraParams" => null,//其他参数,用数组类型array
"biz_content" => $content,
);
//进行curl提交,并获取返回值
$resp = $this->icbcClientCurl($icbc_config, $request, $msg_id);
return $resp;
}
/**
* 建立请求,提交信息,公众号聚合支付 构造form表单
* @param $icbc_config
* @param $json_content
* @param $msg_id
*/
public function icbcClientForm($icbc_config, $request, $msg_id)
{
//APP的编号,应用在API开放平台注册时生成
$appId = $icbc_config['appid'];
//应用私钥
$privateKey = $icbc_config['private_key'];
//签名类型,CA-工行颁发的证书认证,RSA-RSAWithSha1,RSA2-RSAWithSha256,缺省为RSA2,
$signType = \IcbcConstants::$SIGN_TYPE_RSA2;
//字符集 ,缺省为UTF-8
$charset = 'UTF-8';
//请求参数格式,仅支持json
$format = 'json';
//网关公钥
$icbcPulicKey = $icbc_config['icbc_pulic_key'];
//AES加密密钥,缺省为空
$encryptKey = $icbc_config['encrypt_key'];
//加密方式,本接口参数需进行AES加密
$encryptType = 'AES';
//采用ca认证方式时,需上送证书,使用此签名算法的时候,php需要安装扩展php_infosec.dll,工行sdk提供的是php7.0以上的版本
$ca = '';
//当签名类型为CA时,通过该字段上送证书密码,缺省为空
$password = '';
//初始化链接请求构造
$client = new \UiIcbcClient($appId, $privateKey, $signType, $charset, $format,
$icbcPulicKey, $encryptKey, $encryptType, $ca, $password);
//请求接口下单
$resp = $client->buildPostForm($request, $msg_id, '');
return $resp;
}
/**
* 建立请求,提交信息,构造curl请求
* @param $icbc_config
* @param $json_content
* @param $msg_id
*/
public function icbcClientCurl($icbc_config, $request, $msg_id)
{
//APP的编号,应用在API开放平台注册时生成
$appId = $icbc_config['appid'];
//应用私钥
$privateKey = $icbc_config['private_key'];
//签名类型,CA-工行颁发的证书认证,RSA-RSAWithSha1,RSA2-RSAWithSha256,缺省为RSA2,
$signType = \IcbcConstants::$SIGN_TYPE_RSA2;
//字符集 ,缺省为UTF-8
$charset = 'UTF-8';
//请求参数格式,仅支持json
$format = 'json';
//网关公钥
$icbcPulicKey = $icbc_config['icbc_pulic_key'];
//AES加密密钥,缺省为空
$encryptKey = $icbc_config['encrypt_key'];
//加密方式,本接口参数需进行AES加密
$encryptType = 'AES';
//采用ca认证方式时,需上送证书,使用此签名算法的时候,php需要安装扩展php_infosec.dll,工行sdk提供的是php7.0以上的版本
$ca = '';
//当签名类型为CA时,通过该字段上送证书密码,缺省为空
$password = '';
//初始化链接请求构造 此处注意,工行官网sdk文件中提供的【IcbcSignature::verify】方法缺失参数,需要补上$password参数,否则无法调用成功。
$client = new \DefaultIcbcClient($appId, $privateKey, $signType, $charset, $format,
$icbcPulicKey, $encryptKey, $encryptType, $ca, $password);
//请求接口下单
$resp = $client->execute($request, $msg_id, '');
$resp = $resp ? json_decode($resp, true) : '';
return $resp;
}
/**
* * 回调签名验证
* @param $postArr 工行返回参数
* @param $notify_url 回调url
* @return int|void
* @throws Exception
*/
public function icbcCheckSign($postArr, $notify_url)
{
$icbc_config = $this->getIcbcpayConfig();
//工行以数组返回
$postParame = $postArr;
$sign = $postParame['sign'];
unset($postParame['sign']);
//进行验签操作
$WebUtils = new \WebUtils();
$IcbcSignature = new \IcbcSignature();
$path = parse_url($notify_url, PHP_URL_PATH);
$content = $WebUtils->buildOrderedSignStr($path, $postParame);
//签名类型,CA-工行颁发的证书认证,RSA-RSAWithSha1,RSA2-RSAWithSha256,缺省为RSA2,
$signType = \IcbcConstants::$SIGN_TYPE_RSA;
//字符集 ,缺省为UTF-8
$charset = \IcbcConstants::$CHARSET_UTF8;
//当签名类型为CA时,通过该字段上送证书密码,缺省为空
$password = '';
//网关公钥
$icbcPulicKey = $icbc_config['icbc_pulic_key'];
(boolean)$signVerified = $IcbcSignature->verify($content, $signType, $icbcPulicKey, $charset, $sign, $password);
return $signVerified;
}
/**
* 请求最原始的demo
*/
public function demo()
{
include_once('data\extend\icbc\icbc-api-sdk-cop-php/UiIcbcClient.php');
$msg_id = 'aa' . rand(10000000, 999999999);
$content = array(
"interface_version" => "1.0.0.1",
"mer_id" => 'mer_id',
"tp_app_id" => 'tp_app_id',
"tp_open_id" => 'tp_open_id',
"out_trade_no" => '12311212',
"tran_type" => "OfflinePay",
"order_date" => date("YmdHis", $_SERVER['REQUEST_TIME']),
"end_time" => date("YmdHis", $_SERVER['REQUEST_TIME'] + 300), // order_date之后5分钟
"goods_body" => '12',
"goods_detail" => "{'good_name':1,'good_id':2,'good_num':'1'}",
"order_amount" => bcmul(0.01, 100), //总金额(单位分)
"spbill_create_ip" => $_SERVER['REMOTE_ADDR'],
"install_times" => "1",
"return_url" => "http://ip/index/pay/successful", // 支付回显
"notify_url" => "http://ip/index/pay/notify", // 支付结果通知
"notify_type" => "HS",
"result_type" => "0",
);
$request = array(
"serviceUrl" => 'https://gw.open.icbc.com.cn/api/qrcode/V2/generate',
"method" => 'POST',
"isNeedEncrypt" => true,
"extraParams" => null,
"biz_content" => $content,
);
$client = new \UiIcbcClient('appid',
'私钥key',
\IcbcConstants::$SIGN_TYPE_RSA2,
'UTF-8',
'json',
'网关公钥',
'AES加密key',
'AES',
'',
'');
$resp = $client->buildPostForm($request, $msg_id, '');
echo $resp;
die;
}
}
6、坑点注意
6.1、工行官网sdk文件【DefaultIcbcClient.php】中的【execute】方法提供的【IcbcSignature::verify】方法缺失参数,需要补上$this->password参数,否则无法调用成功。
完整为:
//解析响应
$passed = IcbcSignature::verify($respBizContentStr, IcbcConstants::$SIGN_TYPE_RSA, $this->icbcPulicKey, $this->charset, $sign,$this->password);
6.2、【AES.php】文件中,@mcrypt_module_open系列方法在从PHP 7.2起已经弃用,报错之后需要加上错误抑制符,或者重写替换方案。
6.3、低版本的工行SDK【IcbcConstants.php】文件中时区设置错误,需要修改为东八区上海,正确代码如下:
/** Date默认时区 **/
//public static $DATE_TIMEZONE = "Etc/GMT+8";//java版GMT+8
public static $DATE_TIMEZONE = "Asia/Shanghai";//东八区
6.4、关于下单时间
目前工行有4套测试环境,每套测试环境上的系统时间于现实时间不符,当报错为下单时间无效的错误时,请使用工行开发人员提供的小工具获取测试环境的时间,用测试环境的时间来替换订单有效时间。