微信特约商进件

80 阅读13分钟

准备资料

商户mchid

商户API v3密钥(微信服务商-账户中心-API安全 api v3密钥 pay.weixin.qq.com/index.php/c…)

证书编号 (apiclient_cert.pem证书解析后获得)

支付平台公钥(接口获取)

<?php
declare (strict_types=1);

namespace app\common\service;

use app\common\enum\wechat\ActivitiesRateEnum;
use app\common\enum\wechat\ContactType;
use app\common\enum\wechat\SubjectTypeEnum;
use app\common\model\Region;
use app\common\model\SubMerchant;
use app\common\model\WxServerSetting;

/**
 * 微信服务商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 __construct($store_id)
    {

        $this->store_id = $store_id;

        // 查询微信服务器设置
        $wechat_sp = WxServerSetting::where(['store_id' => 10001])->find();
        // 如果存在证书
        if ($wechat_sp->cert_pem) {
            // 设置公钥路径
            $this->public_key_path = root_path('static/wechat') . 'public_key.pem';
        }
        // 如果存在私钥
        if ($wechat_sp->key_pem) {
            // 设置商户私钥路径
            $this->mch_private_key = root_path('static/wechat') . 'apiclient_key.pem';
        }
        // 设置商户号
        $this->mch_id = '1695987447';
//        $this->mch_id = $wechat_sp->sp_mch_id;
        // 设置商户API密钥
        $this->mch_api_key = "zhaotonsoutekejiyouxiangonsi2233";
        // 设置序列号
        $this->serial_no = "4CD591572D80EBFDC8E41C0CCAEF82E1BCDBA40A";
        // 设置服务应用ID
        $this->service_app_id = "wxb4b7d70412c85bb2";

    }

//    public function getW()
//    {
//        // 设置参数
//// 商户号
//        $merchantId = '1679974523';
//
//// 从本地文件中加载「商户API私钥」,「商户API私钥」会用来生成请求的签名
//        $merchantPrivateKeyFilePath = "file:///" . root_path('static/wechat') . 'apiclient_key.pem';
//        $merchantPrivateKeyInstance = Rsa::from($merchantPrivateKeyFilePath, Rsa::KEY_TYPE_PRIVATE);
//
//// 「商户API证书」的「证书序列号」
//        $merchantCertificateSerial = $this->serial_no;
//
//// 从本地文件中加载「微信支付平台证书」,用来验证微信支付应答的签名
//        $platformCertificateFilePath = "file:///" . root_path('static/wechat') . 'cert.pem';
//        $platformPublicKeyInstance = Rsa::from($platformCertificateFilePath, Rsa::KEY_TYPE_PUBLIC);
//
//// 从「微信支付平台证书」中获取「证书序列号」
//        $platformCertificateSerial = PemUtil::parseCertificateSerialNo($platformCertificateFilePath);
//
//// 构造一个 APIv3 客户端实例
//        $instance = Builder::factory([
//            'mchid' => $merchantId,
//            'serial' => $merchantCertificateSerial,
//            'privateKey' => $merchantPrivateKeyInstance,
//            'certs' => [
//                $platformCertificateSerial => $platformPublicKeyInstance,
//            ],
//        ]);
//
//// 发送请求
//        $resp = $instance->chain('v3/certificates')->get(
//        /** @see https://docs.guzzlephp.org/en/stable/request-options.html#debug */
//        // ['debug' => true] // 调试模式
//        );
//        echo (string)$resp->getBody(), PHP_EOL;
//    }

    /*
     * 微信特约商户进件接口
     */
    public function subApplyment(array $params, $user)
    {
        $subMerchant = new SubMerchant();
        $sub_merchant = $subMerchant->where('store_id', $this->store_id)->find();
        $sub_merchant_data = [
            'store_id' => $this->store_id,
            'type' => 1,
            'admin_id' => $user->user_id,
            'business_code' => $this->getBusinessCode(),
        ];
        if (ContactType::LEGAL == $params['contact_info']['contact_type']) {
            $sub_merchant_data['contact_type'] = ContactType::LEGAL;
            $sub_merchant_data['contact_name'] = $params['contact_info']['contact_name'];
            $sub_merchant_data['contact_id_number'] = $params['contact_info']['contact_id_number'];
            $sub_merchant_data['mobile_phone'] = $params['contact_info']['mobile_phone'];
            $sub_merchant_data['contact_email'] = $params['contact_info']['contact_email'];
        } else {
            $sub_merchant_data['contact_id_doc_type'] = $params['contact_info']['contact_id_doc_type'];
            $sub_merchant_data['contact_id_doc_copy'] = $params['contact_info']['contact_id_doc_copy'];
            $sub_merchant_data['contact_id_doc_copy_back'] = $params['contact_info']['contact_id_doc_copy_back'];
            $sub_merchant_data['contact_period_begin'] = $params['contact_info']['contact_period_begin'];
            $sub_merchant_data['contact_period_end'] = $params['contact_info']['contact_period_end'];
            $sub_merchant_data['business_authorization_letter'] = $params['contact_info']['business_authorization_letter'];
        }
        $sub_merchant_data['subject_type'] = $params['subject_info']['subject_type'];
        $sub_merchant_data['license_copy'] = $params['subject_info']['business_license_info']['license_copy'];
        $sub_merchant_data['license_number'] = $params['subject_info']['business_license_info']['license_number'];
        $sub_merchant_data['merchant_name'] = $params['subject_info']['business_license_info']['merchant_name'];
        $sub_merchant_data['legal_person'] = $params['subject_info']['business_license_info']['legal_person'];
        $sub_merchant_data['license_address'] = $params['subject_info']['business_license_info']['license_address'] ?? "";
        $sub_merchant_data['period_begin'] = $params['subject_info']['business_license_info']['period_begin'] ?? "";
        $sub_merchant_data['period_end'] = $params['subject_info']['business_license_info']['period_end'] ?? "";

        $sub_merchant_data['merchant_shortname'] = $params['business_info']['merchant_shortname'];
        $sub_merchant_data['service_phone'] = $params['business_info']['service_phone'];
        $sub_merchant_data['sales_scenes_type'] = $params['business_info']['sales_info']['sales_scenes_type'];
        $sub_merchant_data['qualification_type'] = $params['settlement_info']['qualification_type'];
        $sub_merchant_data['activities_additions'] = $params['settlement_info']['activities_additions'];
        $sub_merchant_data['activities_id'] = '20191030111cff5b5e';
        $sub_merchant_data['activities_rate'] = ActivitiesRateEnum::PUBLIC_RATE;
        $sub_merchant_data['bank_account_type'] = $params['bank_account_info']['bank_account_type'];
        $sub_merchant_data['account_name'] = $params['bank_account_info']['account_name'];
        $sub_merchant_data['account_bank'] = $params['bank_account_info']['account_bank'];
        $sub_merchant_data['bank_address_code'] = Region::getById($params['district_id']) ? Region::getById($params['district_id'])['code'] : $params['bank_address_code'];
        $sub_merchant_data['account_number'] = $params['bank_account_info']['account_number'];
        $sub_merchant_data['id_doc_type'] = $params['subject_info']['identity_info']['id_doc_type'];
        $sub_merchant_data['id_card_copy'] = $params['subject_info']['identity_info']['id_card_info']['id_card_copy'];
        $sub_merchant_data['id_card_national'] = $params['subject_info']['identity_info']['id_card_info']['id_card_national'];
        $sub_merchant_data['id_card_name'] = $params['subject_info']['identity_info']['id_card_info']['id_card_name'];
        $sub_merchant_data['id_card_number'] = $params['subject_info']['identity_info']['id_card_info']['id_card_number'];
        $sub_merchant_data['card_period_begin'] = $params['subject_info']['identity_info']['id_card_info']['card_period_begin'];
        $sub_merchant_data['card_period_end'] = $params['subject_info']['identity_info']['id_card_info']['card_period_end'];
        $sub_merchant_data['id_card_address'] = $params['subject_info']['identity_info']['id_card_info']['id_card_address'];
        $sub_merchant_data['owner'] = $params['subject_info']['identity_info']['owner'];
        if (!$sub_merchant_data['owner']) {
            $sub_merchant_data['ubo_id_doc_type'] = $params['subject_info']['identity_info']['ubo_info']['ubo_id_doc_type'];
            $sub_merchant_data['ubo_id_doc_copy'] = $params['subject_info']['identity_info']['ubo_info']['ubo_id_doc_copy'];
            $sub_merchant_data['ubo_id_doc_copy_back'] = $params['subject_info']['identity_info']['ubo_info']['ubo_id_doc_copy_back'];
            $sub_merchant_data['ubo_id_doc_name'] = $params['subject_info']['identity_info']['ubo_info']['ubo_id_doc_name'];
            $sub_merchant_data['ubo_id_doc_number'] = $params['subject_info']['identity_info']['ubo_info']['ubo_id_doc_number'];
            $sub_merchant_data['ubo_id_doc_address'] = $params['subject_info']['identity_info']['ubo_info']['ubo_id_doc_address'];
            $sub_merchant_data['ubo_period_begin'] = $params['subject_info']['identity_info']['ubo_info']['ubo_period_begin'];
            $sub_merchant_data['ubo_period_end'] = $params['subject_info']['identity_info']['ubo_info']['ubo_period_end'];
        }
        if ($sub_merchant) {
            $sub_merchant->where('store_id', $this->store_id)->update($sub_merchant_data);
        } else {
            $sub_merchant = SubMerchant::create($sub_merchant_data);
        }
        // 参数准备
        $data = [
//            //业务申请编号
            'business_code' => $this->getBusinessCode(),
            //超级管理员信息
            'contact_info' => [
                'contact_type' => $params['contact_info']['contact_type'],//超级管理员类型LEGAL:经营者/法人- SUPER:经办人
                'contact_name' => $this->getEncrypt($params['contact_info']['contact_name']),//超级管理员姓名
                'contact_id_number' => $this->getEncrypt($params['contact_info']['contact_id_number']),//超级管理员身份证件号码
                'mobile_phone' => $this->getEncrypt($params['contact_info']['mobile_phone']),//联系手机
                'contact_email' => $this->getEncrypt($params['contact_info']['contact_email']),//联系邮箱
//                'contact_id_doc_type' => $params['contact_info']['contact_id_doc_type'],//超级管理员证件类型
//                'contact_id_doc_copy' => $this->mediaUpload($params['contact_info']['contact_id_doc_copy']),//超级管理员证件正面照片
//                'contact_id_doc_copy_back' => $this->mediaUpload($params['contact_info']['contact_id_doc_copy_back']),//超级管理员证件反面照片
//                'contact_period_begin' => $params['contact_info']['contact_period_begin'],//超级管理员证件有效期开始时间
//                'contact_period_end' => $params['contact_info']['contact_period_end'],//超级管理员证件有效期结束时间
//                'business_authorization_letter' => $this->mediaUpload(['contact_info']['business_authorization_letter']),//业务办理授权函
            ],
            //主体资料
            'subject_info' => [
                //主体类型SUBJECT_TYPE_INDIVIDUAL(个体户)SUBJECT_TYPE_ENTERPRISE(企业)SUBJECT_TYPE_INSTITUTIONS(党政、机关及事业单位)SUBJECT_TYPE_OTHERS(其他组织)
                'subject_type' => $params['subject_info']['subject_type'],
                //营业执照
                'business_license_info' => [
                    'license_copy' => $this->mediaUpload($params['subject_info']['business_license_info']['license_copy']),//营业执照照片-1张
                    'license_number' => $params['subject_info']['business_license_info']['license_number'],//注册号/统一社会信用代码-格式须为18位数字|大写字母
                    'merchant_name' => $params['subject_info']['business_license_info']['merchant_name'],//商户名称
                    'legal_person' => $params['subject_info']['business_license_info']['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' => $params['subject_info']['identity_info']['id_doc_type'],//证件类型
                    'owner' => $params['subject_info']['identity_info']['owner'],//经营者/法人是否为受益人
                    //身份证信息
                    'id_card_info' => [
                        'id_card_copy' => $this->mediaUpload($params['subject_info']['identity_info']['id_card_info']['id_card_copy']),
                        'id_card_national' => $this->mediaUpload($params['subject_info']['identity_info']['id_card_info']['id_card_national']),
                        'id_card_name' => $this->getEncrypt($params['subject_info']['identity_info']['id_card_info']['id_card_name']),
                        'id_card_number' => $this->getEncrypt($params['subject_info']['identity_info']['id_card_info']['id_card_number']),
                        'card_period_begin' => $params['subject_info']['identity_info']['id_card_info']['card_period_begin'],
                        'card_period_end' => $params['subject_info']['identity_info']['id_card_info']['card_period_end'],
                        'id_card_address' => $this->getEncrypt($params['subject_info']['identity_info']['id_card_info']['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['business_info']['merchant_shortname'],
                'service_phone' => $params['business_info']['service_phone'],
                'sales_info' => [
                    'sales_scenes_type' => $params['business_info']['sales_info']['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_info['activities_id'] = '20191030111cff5b5e',
//        $settlement_info['activities_rate'] = ActivitiesRateEnum::PUBLIC_RATE,
//                'settlement_id' => $params['settlement_info']['settlement_id'],
                'qualification_type' => $params['settlement_info']['qualification_type'],
                'activities_additions' => $params['settlement_info']['activities_additions'],
                'activities_id' => '20191030111cff5b5e',
                'activities_rate' => ActivitiesRateEnum::PUBLIC_RATE,
//                'qualifications' => $params['settlement_info']['qualifications']
            ],


            //结算银行账户
            'bank_account_info' => [
                'bank_account_type' => $params['bank_account_info']['bank_account_type'],
                'account_name' => $this->getEncrypt($params['bank_account_info']['account_name']),
                'account_bank' => $params['bank_account_info']['account_bank'],
                'bank_address_code' => Region::getById($params['district_id']) ? Region::getById($params['district_id'])['code'] : $params['bank_address_code'],
//                'bank_name' => $params['bank_account_info']['bank_name'],
                'account_number' => $this->getEncrypt($params['bank_account_info']['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 = '7E516EBA4EF81D464E28434EE82EF62D72056949';
        foreach ($params['business_info']['sales_info']['sales_scenes_type'] as $v) {
            if ($v == 'SALES_SCENES_MINI_PROGRAM') {  // 小程序
                $data['business_info']['sales_info']['mini_program_info']['mini_program_appid'] = $params['business_info']['sales_info']['mini_program_info']['mini_program_appid'];
            }
            if ($v == 'SALES_SCENES_MP') { // 公众号
                $data['business_info']['sales_info']['mp_info'] = $params['business_info']['sales_info']['mp_info'];
                foreach ($params['business_info']['sales_info']['mp_info']['mp_pics'] as $key => $item) {
                    $data['business_info']['sales_info']['mp_info']['mp_pics'][$key] = $this->mediaUpload($item);
                }


            }
            if ($v == 'SALES_SCENES_APP') { // app

//                $business_info['sales_info']['app_info']['app_sub_appid'] = $wechat->app_id;
            }
        }
        $data ['settlement_info']['settlement_id'] = '';
        if ($params['subject_info']['subject_type'] == SubjectTypeEnum::SUBJECT_TYPE_INDIVIDUAL) {
            $data ['settlement_info']['settlement_id'] = '719';
        } else if ($params['subject_info']['subject_type'] == SubjectTypeEnum::SUBJECT_TYPE_ENTERPRISE) {
            $data ['settlement_info']['settlement_id'] = '716';
        } else {
            $data ['settlement_info']['settlement_id'] = '716';
        }
        //$serial_no = $this->serial_no_new;


        $bodyData = json_encode($data);
        // 获取认证信息
        $authorization = $this->getAuthorization($url, 'POST', $bodyData);
        $header = [
            'Content-Type:application/json',
            'Accept:application/json',
            'User-Agent:*/*',
            'Authorization:' . $authorization,
            'Wechatpay-Serial:' . $serial_no
        ];
//        dd($header);
        $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' => ''];
//        }
        $data['sub_merchant'] = $sub_merchant;
        return $data;
//        if ($applyment_id = $data['applyment_id']) {
//            return 'xxxxx';
//            return json(['code' => '200', 'msg' => '资料提交成功', 'data' => $applyment_id]);
//        }
    }

    /**
     * 业务编号
     * @return string
     */
    public function getBusinessCode()
    {
        return date('Ymd') . substr((string)time(), -5) . substr(microtime(), 2, 5) . sprintf('%02d', rand(0, 99));
    }

    /**
     * 敏感字符加密
     * @param $str
     * @return string
     * @throws Exception
     */
//    private function getEncrypt($str)
//    {
////        dd($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;
//    }
    private function getEncrypt($str)
    {
        //$str是待加密字符串
        $public_key_path = $this->public_key_path;
        $public_key = file_get_contents($public_key_path);
        $encrypted = '';
        if (openssl_public_encrypt($str, $encrypted, $public_key, OPENSSL_PKCS1_OAEP_PADDING)) {
            //base64编码
            $sign = base64_encode($encrypted);
        } else {
            throw new Exception('encrypt failed');
        }
        return $sign;
    }

    /**
     * 上传文件
     */
    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 $media_id;
    }

    /**
     * 获取认证信息
     * @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 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;
    }

    /**
     * 获取证书编号(官方案例-已改造)
     * @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);
    }

    /**
     * 获取支付平台证书
     * @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);

            file_put_contents($public_key_path, $content);
        } else {
            $content = file_get_contents($public_key_path);

        }
        return $content;
    }

    /**
     * 获取微信支付平台证书
     */
    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;
    }

    /**
     * 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');
    }

    /**
     * 进件查询
     */
    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 getError()
    {
        return $this->error;
    }

}

入库

SET NAMES utf8mb4;
SET FOREIGN_KEY_CHECKS = 0;

-- ----------------------------
-- Table structure for yoshop_sub_merchant
-- ----------------------------
DROP TABLE IF EXISTS `yoshop_sub_merchant`;
CREATE TABLE `yoshop_sub_merchant`  (
  `id` int NOT NULL AUTO_INCREMENT,
  `admin_id` int NULL DEFAULT NULL,
  `type` tinyint NULL DEFAULT 1 COMMENT '商户类型,1:微信,2:支付宝',
  `business_code` varchar(30) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL COMMENT '业务申请编号',
  `contact_name` varchar(5) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL COMMENT '超级管理员姓名',
  `contact_id_number` varchar(22) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL COMMENT '超级管理员身份证件号码',
  `mobile_phone` varchar(20) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL COMMENT '联系手机',
  `contact_email` varchar(50) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL COMMENT '联系邮箱',
  `subject_type` varchar(40) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL COMMENT '主体类型',
  `id_doc_type` varchar(50) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL COMMENT '证件类型',
  `id_card_copy` varchar(260) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL COMMENT '身份证人像面照片',
  `id_card_copy_img` varchar(200) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL COMMENT '身份证人像面照片',
  `id_card_national` varchar(260) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL COMMENT '身份证国徽面照片',
  `id_card_national_img` varchar(200) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL COMMENT '身份证国徽面照片',
  `id_card_name` varchar(10) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL COMMENT '身份证姓名',
  `id_card_number` varchar(22) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL COMMENT '身份证号码',
  `card_period_begin` char(10) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL COMMENT '身份证有效期开始时间-示例值:2026-06-06',
  `card_period_end` char(10) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL COMMENT '身份证有效期结束时间',
  `merchant_shortname` varchar(50) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL COMMENT '商户简称',
  `service_phone` varchar(20) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL COMMENT '客服电话',
  `sales_scenes_type` varchar(20) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL COMMENT '经营场景类型',
  `biz_store_name` varchar(50) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL COMMENT '线下场所名称',
  `biz_address_code` varchar(10) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL COMMENT '线下场所省市编码-示例值:440305',
  `biz_store_address` varchar(100) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL COMMENT '线下场所地址-示例值:南山区xx大厦x层xxxx室',
  `store_entrance_pic` text CHARACTER SET utf8 COLLATE utf8_general_ci NULL COMMENT '线下场所门头照片',
  `store_entrance_pic_img` text CHARACTER SET utf8 COLLATE utf8_general_ci NULL COMMENT '线下场所门头照片',
  `indoor_pic` text CHARACTER SET utf8 COLLATE utf8_general_ci NULL COMMENT '线下场所内部照片',
  `indoor_pic_img` text CHARACTER SET utf8 COLLATE utf8_general_ci NULL COMMENT '线下场所内部照片',
  `settlement_id` varchar(5) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL COMMENT '入驻结算规则ID-719:个体,716:企业',
  `qualification_type` varchar(20) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL COMMENT '所属行业',
  `activities_id` varchar(20) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL COMMENT '优惠费率活动ID',
  `activities_rate` float(10, 1) NULL DEFAULT NULL COMMENT '优惠费率活动值',
  `qualifications` text CHARACTER SET utf8 COLLATE utf8_general_ci NULL COMMENT '特殊资质图片,最多可上传5张照片《食品经营许可证》或《餐饮服务许可证》',
  `qualifications_img` text CHARACTER SET utf8 COLLATE utf8_general_ci NULL COMMENT '特殊资质图片,最多可上传5张照片《食品经营许可证》或《餐饮服务许可证》',
  `bank_account_type` varchar(50) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL COMMENT '结算银行账户类型',
  `account_name` varchar(50) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL COMMENT '开户名称-选择“对公银行账户”时,开户名称必须与营业执照上的“商户名称”一致',
  `account_bank` varchar(10) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL COMMENT '开户银行',
  `bank_address_code` varchar(10) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL COMMENT '开户银行省市编码',
  `bank_name` varchar(50) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL COMMENT '开户银行全称(含支行)',
  `account_number` varchar(50) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL COMMENT '银行账号',
  `state` tinyint NULL DEFAULT 0 COMMENT '审核状态,0:待审核,1:确认审核,2:审核失败',
  `created_time` datetime NULL DEFAULT NULL,
  `updated_time` datetime NULL DEFAULT NULL,
  `merchant_name` varchar(50) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL COMMENT '商户名称',
  `legal_person` varchar(50) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL COMMENT '法人姓名',
  `license_number` varchar(50) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL COMMENT '注册号/统一社会信用代码',
  `license_copy` varchar(300) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL COMMENT '营业执照',
  `license_copy_img` varchar(260) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL COMMENT '营业执照',
  `biz_sub_appid` varchar(255) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL,
  `id_card_address` varchar(260) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL COMMENT '身份证居住地址-主体类型为企业时,需要填写',
  `contact_type` varchar(10) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL COMMENT '超级管理员类型',
  `contact_id_doc_type` varchar(30) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL COMMENT '超级管理员证件类型',
  `contact_id_doc_copy` varchar(260) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL COMMENT '超级管理员证件正面照片',
  `contact_id_doc_copy_back` varchar(260) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL COMMENT '超级管理员证件反面照片',
  `contact_period_begin` varchar(128) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL COMMENT '超级管理员证件有效期开始时间',
  `contact_period_end` varchar(128) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL COMMENT '超级管理员证件有效期结束时间',
  `business_authorization_letter` varchar(128) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL COMMENT '业务办理授权函',
  `contact_id_doc_copy_img` varchar(255) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL,
  `contact_id_doc_copy_back_img` varchar(255) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL,
  `business_authorization_letter_img` varchar(255) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL,
  `applyment_id` varchar(255) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL,
  `store_id` int NULL DEFAULT NULL,
  `license_address` varchar(255) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL,
  `period_begin` varchar(255) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL,
  `period_end` varchar(255) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL,
  `activities_additions` varchar(255) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL,
  `owner` varchar(255) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL,
  `ubo_id_doc_type` varchar(255) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL,
  `ubo_id_doc_copy` varchar(255) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL,
  `ubo_id_doc_copy_back` varchar(255) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL,
  `ubo_id_doc_name` varchar(255) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL,
  `ubo_id_doc_number` varchar(255) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL,
  `ubo_id_doc_address` varchar(255) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL,
  `ubo_period_begin` varchar(255) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL,
  `ubo_period_end` varchar(255) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL,
  PRIMARY KEY (`id`) USING BTREE
) ENGINE = InnoDB AUTO_INCREMENT = 9 CHARACTER SET = utf8 COLLATE = utf8_general_ci ROW_FORMAT = Dynamic;

SET FOREIGN_KEY_CHECKS = 1;