<?php
namespace app\service;
/**
* 微信服务商V3
*/
class WxpaymchService
{
// 错误信息
private $error = '';
// 商户mchid
private $mch_id = '';
// 商户API v3密钥(微信服务商-账户中心-API安全 api v3密钥 https://pay.weixin.qq.com/index.php/core/cert/api_cert)
private $mch_api_key = '';
// 证书编号 (apiclient_cert.pem证书解析后获得)
private $serial_no = '';
// 私钥 apiclient_key.pem(微信服务商-账户中心-API安全 自行下载 https://pay.weixin.qq.com/index.php/core/cert/api_cert)
private $mch_private_key = '';
// 支付平台公钥(接口获取)
private $public_key_path = 'cert_ficates_v3.pem';
/*
* 微信特约商户进件接口
*/
public function subApplyment( array $params)
{
// 参数准备
$data = [
//业务申请编号
'business_code' => $params['business_code'],
//超级管理员信息
'contact_info' => [
'contact_type' => $params['contact_type'],//超级管理员类型LEGAL:经营者/法人- SUPER:经办人
'contact_name' => $this->getEncrypt($params['contact_name']),//超级管理员姓名
'contact_id_number' => $this->getEncrypt($params['contact_id_number']),//超级管理员身份证件号码
'mobile_phone' => $this->getEncrypt($params['mobile_phone']),//联系手机
'contact_email' => $this->getEncrypt($params['contact_email']),//联系邮箱
'contact_id_doc_type' => $params['contact_id_doc_type'],//超级管理员证件类型
'contact_id_doc_copy' => $params['contact_id_doc_copy'],//超级管理员证件正面照片
'contact_id_doc_copy_back' => $params['contact_id_doc_copy_back'],//超级管理员证件反面照片
'contact_period_begin' => $params['contact_period_begin'],//超级管理员证件有效期开始时间
'contact_period_end' => $params['contact_period_end'],//超级管理员证件有效期结束时间
'business_authorization_letter' => $params['business_authorization_letter'],//业务办理授权函
],
//主体资料
'subject_info' => [
//主体类型SUBJECT_TYPE_INDIVIDUAL(个体户)SUBJECT_TYPE_ENTERPRISE(企业)SUBJECT_TYPE_INSTITUTIONS(党政、机关及事业单位)SUBJECT_TYPE_OTHERS(其他组织)
'subject_type' => $params['subject_type'],
//营业执照
'business_license_info' => [
'license_copy' => $params['license_copy'],//营业执照照片-1张
'license_number' => $params['license_number'],//注册号/统一社会信用代码-格式须为18位数字|大写字母
'merchant_name' => $params['merchant_name'],//商户名称
'legal_person' => $params['legal_person'],//法人姓名
/*'license_address' => '上海市松江工业区俞塘路512号4幢3层B319室',//注册地址
'period_begin' => '2019-12-11',//有效期限开始日期-2019-12-11
'period_end' => '2039-12-10',//有效期限结束日期-2039-12-10*/
],
//经营者/法人身份证件
'identity_info' => [
'id_doc_type' => 'IDENTIFICATION_TYPE_IDCARD',//证件类型
'owner' => true,//经营者/法人是否为受益人
//身份证信息
'id_card_info' => [
'id_card_copy' => $params['id_card_copy'],
'id_card_national' => $params['id_card_national'],
'id_card_name' => $this->getEncrypt($params['id_card_name']),
'id_card_number' => $this->getEncrypt($params['id_card_number']),
'card_period_begin' => $params['card_period_begin'],
'card_period_end' => $params['card_period_end'],
'id_card_address' => $this->getEncrypt($params['id_card_address']),
],
],
//-最终受益人信息列表(UBO)-仅企业需要填写。
/*'ubo_info_list' => [
[
'ubo_id_doc_address' => $this->getEncrypt($params['id_card_address']),
'ubo_id_doc_copy' => $params['id_card_copy'],
'ubo_id_doc_copy_back' => $params['id_card_national'],
'ubo_id_doc_name' => $this->getEncrypt($params['id_card_name']),
'ubo_id_doc_number' => $this->getEncrypt($params['id_card_number']),
'ubo_id_doc_type' => 'IDENTIFICATION_TYPE_IDCARD',//证件类型
'ubo_period_begin' => $params['card_period_begin'],
'ubo_period_end' => $params['card_period_end'],
]
],*/
],
//经营资料
'business_info' => [
'merchant_shortname' => $params['merchant_shortname'],
'service_phone' => $params['service_phone'],
'sales_info' => [
'sales_scenes_type' => [$params['sales_scenes_type']],
//线下门店场景
'biz_store_info' => [
'biz_store_name' => $params['biz_store_name'],
'biz_address_code' => $params['biz_address_code'],
'biz_store_address' => $params['biz_store_address'],
'store_entrance_pic' => [$params['store_entrance_pic']],
'indoor_pic' => [$params['indoor_pic']],
//'biz_sub_appid' => $params['biz_sub_appid'],
],
],
],
//结算规则
'settlement_info' => [
'settlement_id' => $params['settlement_id'],
'qualification_type' => $params['qualification_type'],
'activities_id' => $params['activities_id'],
'activities_rate' => (string)$params['activities_rate'],
'qualifications'=>[$params['qualifications']]
],
//结算银行账户
'bank_account_info' => [
'bank_account_type' => $params['bank_account_type'],
'account_name' => $this->getEncrypt($params['account_name']),
'account_bank' => $params['account_bank'],
'bank_address_code' => $params['bank_address_code'],
'bank_name' => $params['bank_name'],
'account_number' => $this->getEncrypt($params['account_number']),
],
//补充材料
/*'addition_info'=>[
'legal_person_commitment'=>$params['legal_person_commitment'],
'business_addition_pics'=>$params['business_addition_pics'],
],*/
];
//没有优惠费率
if($params['qualifications']==''){
unset($data['settlement_info']['qualifications']);
//unset($data['settlement_info']['activities_id']);
}
if($params['contact_type']=='LEGAL'){
unset($data['contact_info']['contact_id_doc_type'],
$data['contact_info']['contact_id_doc_copy'],
$data['contact_info']['contact_id_doc_copy_back'],
$data['contact_info']['contact_period_begin'],
$data['contact_info']['contact_period_end'],
$data['contact_info']['business_authorization_letter']
);
}
$url = 'https://api.mch.weixin.qq.com/v3/applyment4sub/applyment/';
// 获取支付平台证书编码(也可以用接口中返回的serial_no 来源:https://api.mch.weixin.qq.com/v3/certificates)
$serial_no = $this->parseSerialNo($this->getCertFicates());
//$serial_no = $this->serial_no_new;
$bodyData = json_encode($data);
//dump($bodyData);
// 获取认证信息
$authorization = $this->getAuthorization($url, 'POST', $bodyData);
$header = [
'Content-Type:application/json',
'Accept:application/json',
'User-Agent:*/*',
'Authorization:' . $authorization,
'Wechatpay-Serial:' . $serial_no
];
$json = $this->getCurl('POST', $url, $bodyData, $header);
$data = json_decode($json, true);
if (isset($data['code']) && isset($data['message'])) {
return ['code' => '201', 'msg' => '[subApplyment]请求错误 code:' . $data['code'] . ' msg:' . $data['message'], 'data' => ''];
}
if (empty($applyment_id = $data['applyment_id'])) {
return ['code' => '202', 'msg' => '[subApplyment]返回错误', 'data' => ''];
}
return ['code' => '200', 'msg' => '', 'data' => $applyment_id];
}
/**
* 进件查询
*/
public function queryApplyment($business_code)
{
$url = 'https://api.mch.weixin.qq.com/v3/applyment4sub/applyment/business_code/' . $business_code;
// 获取认证信息
$authorization = $this->getAuthorization($url);
$header = [
'Content-Type:application/json',
'Accept:application/json',
'User-Agent:*/*',
'Authorization:' . $authorization
];
$json = $this->getCurl('GET', $url, '', $header);
$data = json_decode($json, true);
if (isset($data['code']) && isset($data['message'])) {
// $this->error = '[queryApplyment]请求错误 code:' . $data['code'] . ' msg:' . $data['message'];
return ['code' => '201', 'msg' => '[queryApplyment]请求错误 code:' . $data['code'] . ' msg:' . $data['message'], 'data' => $data];
}
return ['code' => '200', 'msg' => '', 'data' => $data];
}
/**
* 上传文件
*/
public function mediaUpload($filepath)
{
// 上传图片
$filename = date("YmdHis").rand(100,999).'.png';
//$filepath = __DIR__ . '/' . $filename;
/*if (!file_exists($filepath)) {
//$this->error = '[mediaUpload]文件找不到';
return ['code' => '201', 'msg' => '缺少img参数2', 'data' => $filepath];
}*/
$url = 'https://api.mch.weixin.qq.com/v3/merchant/media/upload';
//$fi = new \finfo(FILEINFO_MIME_TYPE);
//$mime_type = $fi->file($filepath);
$mime_type = 'image/png';
$meta = [
'filename' => $filename,
'sha256' => hash_file('sha256', $filepath)
];
// 获取认证信息
$authorization = $this->getAuthorization($url, 'POST', json_encode($meta));
$boundary = uniqid();
$header = [
'Accept:application/json',
'User-Agent:*/*',
'Content-Type:multipart/form-data;boundary=' . $boundary,
'Authorization:' . $authorization
];
// 组合参数
$boundaryStr = "--{$boundary}\r\n";
$out = $boundaryStr;
$out .= 'Content-Disposition: form-data; name="meta"' . "\r\n";
$out .= 'Content-Type: application/json' . "\r\n";
$out .= "\r\n";
$out .= json_encode($meta) . "\r\n";
$out .= $boundaryStr;
$out .= 'Content-Disposition: form-data; name="file"; filename="' . $filename . '"' . "\r\n";
$out .= 'Content-Type: ' . $mime_type . ';' . "\r\n";
$out .= "\r\n";
$out .= file_get_contents($filepath) . "\r\n";
$out .= "--{$boundary}--\r\n";
$json = $this->getCurl('POST', $url, $out, $header);
$data = json_decode($json, true);
if (isset($data['code']) && isset($data['message'])) {
return ['code' => '201', 'msg' => '[mediaUpload]请求错误 code:' . $data['code'] . ' msg:' . $data['message'], 'data' => ''];
}
if (empty($media_id = $data['media_id'])) {
return ['code' => '201', 'msg' => '[mediaUpload]返回错误', 'data' => $data];
}
return ['code' => '200', 'msg' => '', 'data' => $media_id];
}
/**
* 获取微信支付平台证书
*/
public function certFicates()
{
$url = 'https://api.mch.weixin.qq.com/v3/certificates';
// 获取认证信息
$authorization = $this->getAuthorization($url);
$header = [
'Content-Type:application/json',
'Accept:application/json',
'User-Agent:*/*',
'Authorization:' . $authorization
];
$json = $this->getCurl('GET', $url, '', $header);
$data = json_decode($json, true);
if (isset($data['code']) && isset($data['message'])) {
$this->error = '[certFicates]请求错误 code:' . $data['code'] . ' msg:' . $data['message'];
return false;
}
if (empty($cfdata = $data['data'][0])) {
$this->error = '[certFicates]返回错误';
return false;
}
return $cfdata;
}
/**
* 获取认证信息
* @param string $url
* @param string $http_method
* @param string $body
* @return string
* @throws Exception
*/
private function getAuthorization($url, $http_method = 'GET', $body = '')
{
if (!in_array('sha256WithRSAEncryption', \openssl_get_md_methods(true))) {
throw new \Exception("当前PHP环境不支持SHA256withRSA");
}
//私钥地址
$mch_private_key = $this->mch_private_key;
//商户号
$merchant_id = $this->mch_id;
//当前时间戳
$timestamp = time();
//随机字符串
$nonce = $this->getNonceStr();
//证书编号
$serial_no = $this->serial_no;
$url_parts = parse_url($url);
$canonical_url = ($url_parts['path'] . (!empty($url_parts['query']) ? "?${url_parts['query']}" : ""));
$message = $http_method . "\n" .
$canonical_url . "\n" .
$timestamp . "\n" .
$nonce . "\n" .
$body . "\n";
openssl_sign($message, $raw_sign, \openssl_get_privatekey(\file_get_contents($mch_private_key)), 'sha256WithRSAEncryption');
$sign = base64_encode($raw_sign);
$schema = 'WECHATPAY2-SHA256-RSA2048';
$token = sprintf('mchid="%s",nonce_str="%s",timestamp="%d",serial_no="%s",signature="%s"',
$merchant_id, $nonce, $timestamp, $serial_no, $sign);
return $schema . ' ' . $token;
}
/**
* 敏感字符加密
* @param $str
* @return string
* @throws Exception
*/
private function getEncrypt($str)
{
static $content;
if (empty($content)) {
$content = $this->getCertFicates();
}
$encrypted = '';
if (openssl_public_encrypt($str, $encrypted, $content, OPENSSL_PKCS1_OAEP_PADDING)) {
//base64编码
$sign = base64_encode($encrypted);
}
else {
throw new \Exception('encrypt failed');
}
return $sign;
}
/**
* 获取支付平台证书
* @return false|string-private
*/
public function getCertFicates()
{
$public_key_path = $this->public_key_path;
if (!file_exists($public_key_path)) {
$cfData = $this->certFicates();
//dump($cfData);
$content = $this->decryptToString($cfData['encrypt_certificate']['associated_data'], $cfData['encrypt_certificate']['nonce'], $cfData['encrypt_certificate']['ciphertext'], $this->mch_api_key);
// dump($content);die;
file_put_contents($public_key_path, $content);
}
else {
$content = file_get_contents($public_key_path);
}
return $content;
}
/**
* 业务编号
* @return string
*/
public function getBusinessCode()
{
return date('Ymd') . substr(time(), -5) . substr(microtime(), 2, 5) . sprintf('%02d', rand(0, 99));
}
/**
* 随机字符串
* @param int $length
* @return string
*/
private function getNonceStr($length = 16)
{
// 密码字符集,可任意添加你需要的字符
$chars = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789";
$str = "";
for ($i = 0; $i < $length; $i++) {
$str .= $chars[mt_rand(0, strlen($chars) - 1)];
}
return $str;
}
/**
* @param string $method
* @param string $url
* @param array|string $data
* @param array $headers
* @param int $timeout
* @return bool|string
*/
private function getCurl($method = 'GET', $url, $data, $headers = [], $timeout = 10)
{
$curl = curl_init();
curl_setopt($curl, CURLOPT_URL, $url);
curl_setopt($curl, CURLOPT_HEADER, false);
curl_setopt($curl, CURLOPT_RETURNTRANSFER, 1);
curl_setopt($curl, CURLOPT_SSL_VERIFYPEER, false);
curl_setopt($curl, CURLOPT_TIMEOUT, $timeout);
if (!empty($headers)) {
curl_setopt($curl, CURLOPT_HTTPHEADER, $headers);
}
if ($method == 'POST') {
curl_setopt($curl, CURLOPT_POST, TRUE);
curl_setopt($curl, CURLOPT_POSTFIELDS, $data);
}
else {
}
$result = curl_exec($curl);
curl_close($curl);
return $result;
}
/**
* Decrypt AEAD_AES_256_GCM ciphertext(官方案例-已改造)
*
* @param string $associatedData AES GCM additional authentication data
* @param string $nonceStr AES GCM nonce
* @param string $ciphertext AES GCM cipher text
*
* @return string|bool Decrypted string on success or FALSE on failure
*/
private function decryptToString($associatedData, $nonceStr, $ciphertext, $aesKey)
{
$auth_tag_length_byte = 16;
$ciphertext = \base64_decode($ciphertext);
if (strlen($ciphertext) <= $auth_tag_length_byte) {
return false;
}
// ext-sodium (default installed on >= PHP 7.2)
if (function_exists('\sodium_crypto_aead_aes256gcm_is_available') &&
\sodium_crypto_aead_aes256gcm_is_available()) {
return \sodium_crypto_aead_aes256gcm_decrypt($ciphertext, $associatedData, $nonceStr, $aesKey);
}
// ext-libsodium (need install libsodium-php 1.x via pecl)
if (function_exists('\Sodium\crypto_aead_aes256gcm_is_available') &&
\Sodium\crypto_aead_aes256gcm_is_available()) {
return \Sodium\crypto_aead_aes256gcm_decrypt($ciphertext, $associatedData, $nonceStr, $aesKey);
}
// openssl (PHP >= 7.1 support AEAD)
if (PHP_VERSION_ID >= 70100 && in_array('aes-256-gcm', \openssl_get_cipher_methods())) {
$ctext = substr($ciphertext, 0, -$auth_tag_length_byte);
$authTag = substr($ciphertext, -$auth_tag_length_byte);
return \openssl_decrypt($ctext, 'aes-256-gcm', $aesKey, \OPENSSL_RAW_DATA, $nonceStr,
$authTag, $associatedData);
}
throw new \Exception('AEAD_AES_256_GCM需要PHP 7.1以上或者安装libsodium-php');
}
/**
* 获取证书编号(官方案例-已改造)
* @param $certificate
* @return string
*/
public function parseSerialNo($certificate)
{
$info = \openssl_x509_parse($certificate);
if (!isset($info['serialNumber']) && !isset($info['serialNumberHex'])) {
throw new \InvalidArgumentException('证书格式错误');
}
$serialNo = '';
// PHP 7.0+ provides serialNumberHex field
if (isset($info['serialNumberHex'])) {
$serialNo = $info['serialNumberHex'];
}
else {
// PHP use i2s_ASN1_INTEGER in openssl to convert serial number to string,
// i2s_ASN1_INTEGER may produce decimal or hexadecimal format,
// depending on the version of openssl and length of data.
if (\strtolower(\substr($info['serialNumber'], 0, 2)) == '0x') { // HEX format
$serialNo = \substr($info['serialNumber'], 2);
}
else { // DEC format
$value = $info['serialNumber'];
$hexvalues = ['0', '1', '2', '3', '4', '5', '6', '7',
'8', '9', 'A', 'B', 'C', 'D', 'E', 'F'];
while ($value != '0') {
$serialNo = $hexvalues[\bcmod($value, '16')] . $serialNo;
$value = \bcdiv($value, '16', 0);
}
}
}
return \strtoupper($serialNo);
}
public function getError()
{
return $this->error;
}
}
进件表sql
SET FOREIGN_KEY_CHECKS=0;
-- ----------------------------
-- Table structure for wj_sub_merchant
-- ----------------------------
DROP TABLE IF EXISTS `wj_sub_merchant`;
CREATE TABLE `wj_sub_merchant` (
`id` int(11) NOT NULL AUTO_INCREMENT,
`admin_id` int(11) DEFAULT NULL,
`type` tinyint(2) DEFAULT '1' COMMENT '商户类型,1:微信,2:支付宝',
`business_code` varchar(30) DEFAULT NULL COMMENT '业务申请编号',
`contact_name` varchar(5) DEFAULT NULL COMMENT '超级管理员姓名',
`contact_id_number` varchar(22) DEFAULT NULL COMMENT '超级管理员身份证件号码',
`mobile_phone` varchar(20) DEFAULT NULL COMMENT '联系手机',
`contact_email` varchar(50) DEFAULT NULL COMMENT '联系邮箱',
`subject_type` varchar(40) DEFAULT NULL COMMENT '主体类型',
`id_doc_type` varchar(50) DEFAULT NULL COMMENT '证件类型',
`id_card_copy` varchar(260) DEFAULT NULL COMMENT '身份证人像面照片',
`id_card_copy_img` varchar(200) DEFAULT NULL COMMENT '身份证人像面照片',
`id_card_national` varchar(260) DEFAULT NULL COMMENT '身份证国徽面照片',
`id_card_national_img` varchar(200) DEFAULT NULL COMMENT '身份证国徽面照片',
`id_card_name` varchar(10) DEFAULT NULL COMMENT '身份证姓名',
`id_card_number` varchar(22) DEFAULT NULL COMMENT '身份证号码',
`card_period_begin` char(10) DEFAULT NULL COMMENT '身份证有效期开始时间-示例值:2026-06-06',
`card_period_end` char(10) DEFAULT NULL COMMENT '身份证有效期结束时间',
`merchant_shortname` varchar(50) DEFAULT NULL COMMENT '商户简称',
`service_phone` varchar(20) DEFAULT NULL COMMENT '客服电话',
`sales_scenes_type` varchar(20) DEFAULT NULL COMMENT '经营场景类型',
`biz_store_name` varchar(50) DEFAULT NULL COMMENT '线下场所名称',
`biz_address_code` varchar(10) DEFAULT NULL COMMENT '线下场所省市编码-示例值:440305',
`biz_store_address` varchar(100) DEFAULT NULL COMMENT '线下场所地址-示例值:南山区xx大厦x层xxxx室',
`store_entrance_pic` text COMMENT '线下场所门头照片',
`store_entrance_pic_img` text COMMENT '线下场所门头照片',
`indoor_pic` text COMMENT '线下场所内部照片',
`indoor_pic_img` text COMMENT '线下场所内部照片',
`settlement_id` varchar(5) DEFAULT NULL COMMENT '入驻结算规则ID-719:个体,716:企业',
`qualification_type` varchar(20) DEFAULT NULL COMMENT '所属行业',
`activities_id` varchar(20) DEFAULT NULL COMMENT '优惠费率活动ID',
`activities_rate` float(10,1) DEFAULT NULL COMMENT '优惠费率活动值',
`qualifications` text COMMENT '特殊资质图片,最多可上传5张照片《食品经营许可证》或《餐饮服务许可证》',
`qualifications_img` text COMMENT '特殊资质图片,最多可上传5张照片《食品经营许可证》或《餐饮服务许可证》',
`bank_account_type` varchar(50) DEFAULT NULL COMMENT '结算银行账户类型',
`account_name` varchar(50) DEFAULT NULL COMMENT '开户名称-选择“对公银行账户”时,开户名称必须与营业执照上的“商户名称”一致',
`account_bank` varchar(10) DEFAULT NULL COMMENT '开户银行',
`bank_address_code` varchar(10) DEFAULT NULL COMMENT '开户银行省市编码',
`bank_name` varchar(50) DEFAULT NULL COMMENT '开户银行全称(含支行)',
`account_number` varchar(50) DEFAULT NULL COMMENT '银行账号',
`state` tinyint(5) DEFAULT '0' COMMENT '审核状态,0:待审核,1:确认审核,2:审核失败',
`created_time` datetime DEFAULT NULL,
`updated_time` datetime DEFAULT NULL,
`merchant_name` varchar(50) DEFAULT NULL COMMENT '商户名称',
`legal_person` varchar(50) DEFAULT NULL COMMENT '法人姓名',
`license_number` varchar(50) DEFAULT NULL COMMENT '注册号/统一社会信用代码',
`license_copy` varchar(300) DEFAULT NULL COMMENT '营业执照',
`license_copy_img` varchar(260) DEFAULT NULL COMMENT '营业执照',
`biz_sub_appid` varchar(255) DEFAULT NULL,
`id_card_address` varchar(260) DEFAULT NULL COMMENT '身份证居住地址-主体类型为企业时,需要填写',
`contact_type` varchar(10) DEFAULT NULL COMMENT '超级管理员类型',
`contact_id_doc_type` varchar(30) DEFAULT NULL COMMENT '超级管理员证件类型',
`contact_id_doc_copy` varchar(260) DEFAULT NULL COMMENT '超级管理员证件正面照片',
`contact_id_doc_copy_back` varchar(260) DEFAULT NULL COMMENT '超级管理员证件反面照片',
`contact_period_begin` varchar(128) DEFAULT NULL COMMENT '超级管理员证件有效期开始时间',
`contact_period_end` varchar(128) DEFAULT NULL COMMENT '超级管理员证件有效期结束时间',
`business_authorization_letter` varchar(128) DEFAULT NULL COMMENT '业务办理授权函',
`contact_id_doc_copy_img` varchar(255) DEFAULT NULL,
`contact_id_doc_copy_back_img` varchar(255) DEFAULT NULL,
`business_authorization_letter_img` varchar(255) DEFAULT NULL,
`applyment_id` varchar(255) DEFAULT NULL,
PRIMARY KEY (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=6 DEFAULT CHARSET=utf8;