_appid = $app_id; $this->_appsecret = $app_secret; } /** * 小程序发送订阅消息 * @author Devil * @blog http://gong.gg/ * @version 1.0.0 * @date 2022-04-06 * @desc description * @param [string] $params['page'] [页面地址] * @param [string] $params['scene'] [参数] * @return [string] [成功返回文件流, 失败则空] */ public function MiniSubscribeMessage($params = []) { // 请求参数 $p = [ [ 'checked_type' => 'empty', 'key_name' => 'touser', 'error_msg' => MyLang('common_extend.base.wechat.touser_openid_empty_tips'), ], [ 'checked_type' => 'empty', 'key_name' => 'template_id', 'error_msg' => MyLang('common_extend.base.wechat.template_id_empty_tips'), ], [ 'checked_type' => 'empty', 'key_name' => 'data', 'error_msg' => MyLang('common_extend.base.wechat.data_empty_tips'), ], ]; $ret = ParamsChecked($params, $p); if($ret !== true) { return DataReturn($ret, -1); } // 获取access_token $access_token = $this->GetMiniAccessToken(); if($access_token === false) { return DataReturn(MyLang('common_extend.base.common.access_token_request_fail_tips'), -1); } // 发送订阅消息 $url = 'https://api.weixin.qq.com/cgi-bin/message/subscribe/send?access_token='.$access_token; $data = [ 'touser' => $params['touser'], 'template_id' => $params['template_id'], 'data' => $params['data'], ]; // 跳转页面,可以不传 仅限本小程序内的页面。支持带参数,(示例index?foo=bar) if(isset($params['page']) && !empty($params['page'])){ $data['page'] = $params['page']; } // 跳转小程序类型:developer为开发版;trial为体验版;formal为正式版;默认为正式版 if(isset($params['miniprogram_state']) && !empty($params['miniprogram_state'])){ $data['miniprogram_state'] = $params['miniprogram_state']; } // 进入小程序查看的语言类型,支持zh_CN(简中)、en_US(英文)、zh_HK(繁中)、zh_TW(繁中),默认为zh_CN if(isset($params['lang']) && !empty($params['lang'])){ $data['lang'] = $params['lang']; } $res = $this->HttpRequestPost($url, $data, false); if(!empty($res)) { if(stripos($res, 'errcode') === false) { return DataReturn(MyLang('send_success'), 0, $res); } $res = json_decode($res, true); $msg = isset($res['errmsg']) ? $res['errmsg'] : MyLang('send_fail'); if($msg == 'ok'){ return DataReturn(MyLang('send_success'), 0, $res); }else{ $msg = isset($res['errcode']) ? $res['errcode'].':'.$msg : $msg; } } else { $msg = MyLang('send_fail').'2'; } return DataReturn($msg, -1); } /** * 小程序发货信息录入 * @author Shon Wu * @blog https://github.com/mantoufan/ * @version 1.0.0 * @date 2023-08-21 * @desc description * @param [string] $params['order_model'] [订单模式] * @param [string] $params['trade_no'] [支付平台交易号] * @param [string] $params['buyer_user'] [支付平台用户账号] * @param [string] $params['goods_title'] [商品标题] * @optional [string] $params['express_name'] [快递公司名称] * @optional [string] $params['express_number'] [快递单号] * @optional [string] $params['tel'] [收货联系人手机号] * @return [array|null] [成功原样返回微信小程序接口响应, 失败则空] */ public function MiniUploadShippingInfo($params = []) { // 请求参数 $p = [ [ 'checked_type' => 'empty', 'key_name' => 'trade_no', 'error_msg' => MyLang('common_extend.base.wechat.trade_no_empty_tips'), ], [ 'checked_type' => 'empty', 'key_name' => 'buyer_user', 'error_msg' => MyLang('common_extend.base.wechat.buyer_user_empty_tips'), ], [ 'checked_type' => 'empty', 'key_name' => 'goods_title', 'error_msg' => MyLang('common_extend.base.wechat.goods_title_empty_tips'), ], ]; $ret = ParamsChecked($params, $p); if($ret !== true) { return DataReturn($ret, -1); } // 获取access_token $access_token = $this->GetMiniAccessToken(); if($access_token === false) { return DataReturn(MyLang('common_extend.base.common.access_token_request_fail_tips'), -1); } // 是否已开通发货管理 $key = 'wechat_is_trade_managed_'.$this->_appid; $trade_managed = MyCache($key); if(empty($trade_managed)) { $url = 'https://api.weixin.qq.com/wxa/sec/order/is_trade_managed?access_token='.$access_token; $trade_managed = $this->HttpRequestPost($url, ['appid'=>$this->_appid], true); MyCache($key, $trade_managed); } if(empty($trade_managed) || !isset($trade_managed['errcode']) || $trade_managed['errcode'] != 0 || !isset($trade_managed['is_trade_managed']) || $trade_managed['is_trade_managed'] != 1) { return DataReturn(MyLang('common_extend.base.common.not_opened_trade_managed_msg'), 0); } // 订单模式类型 // 订单模式(0快递, 1同城, 2自提, 3虚拟) $order_model_list = [ // 商城订单模式:快递 <-> 微信小程序:实体物流配送采用快递公司进行实体物流配送形式 0 => 1, // 商城订单模式:同城服务 <-> 微信小程序:同城配送 1 => 2, // 商城订单模式:自提型 <-> 微信小程序:用户自提 2 => 4, // 商城订单模式:虚拟销售 <-> 微信小程序:虚拟商品 3 => 3, ]; $logistics_type = array_key_exists($params['order_model'], $order_model_list) ? $order_model_list[$params['order_model']] : null; // 展示模式表示为无效的订单,不处理 if($logistics_type === null) { return DataReturn(MyLang('common_extend.base.wechat.no_match_logistics_mode'), 0); } // 商城参数转换为微信小程序参数 if(!is_array($params['goods_title'])) { $params['goods_title'] = explode(',', $params['goods_title']); } $shipping_list = array_map(function($item) { return ['item_desc'=>$item]; }, $params['goods_title']); // 物流发货匹配快递信息 if($logistics_type === 1) { if(!empty($params['express_name']) && !empty($params['express_number'])) { // 当商城为销售型时,传入快递公司编码和快递单号,传入收件人和发件人手机号供顺丰使用 $express_res = $this->GetMiniDeliveryIdByName($params['express_name']); if($express_res['code'] == 0) { $consignor_tel = empty($params['consignor_tel']) ? '' : mb_substr($params['consignor_tel'], 0, 3, 'utf-8').'****'.mb_substr($params['consignor_tel'], -4, null, 'utf-8'); $receiver_tel = empty($params['receiver_tel']) ? '' : mb_substr($params['receiver_tel'], 0, 3, 'utf-8').'****'.mb_substr($params['receiver_tel'], -4, null, 'utf-8'); foreach($shipping_list as &$v) { $v['express_company'] = $express_res['data']; $v['tracking_no'] = $params['express_number']; if(!empty($consignor_tel) || !empty($receiver_tel)) { $v['contact'] = [ 'consignor_contact' => $consignor_tel, 'receiver_contact' => $receiver_tel, ]; } } } else { // 没有匹配到快递则使用同城类型 $logistics_type = 2; } } else { // 没有匹配到快递则使用同城类型 $logistics_type = 2; } } // 非快递模式物流信息只能为一项 if($logistics_type != 1 && count($shipping_list) > 1) { $shipping_list = [$shipping_list[0]]; } // 请求参数 $data = [ 'order_key' => [ // 使用微信支付单号 'order_number_type' => 2, // 原支付交易对应的微信订单号 'transaction_id' => $params['trade_no'], ], // 发货模式:1 统一发货,2 分拆发货 'delivery_mode' => (count($shipping_list) == 1) ? 1 : 2, // 物流信息列表 'shipping_list' => $shipping_list, // 分拆发货模式时、是否已全部发货 true | false 'is_all_delivered' => true, // 物流模式,发货方式枚举值:1、实体物流配送采用快递公司进行实体物流配送形式 2、同城配送 3、虚拟商品,虚拟商品,例如话费充值,点卡等,无实体配送形式 4、用户自提 'logistics_type' => $logistics_type, // 支付者,支付者信息 'payer' => ['openid' => $params['buyer_user']], // 上传时间 'upload_time' => date('Y-m-d\TH:i:sP'), ]; // 录入发货信息接口 $url = 'https://api.weixin.qq.com/wxa/sec/order/upload_shipping_info?access_token='.$access_token; $res = $this->HttpRequestPost($url, $data, true); $code = isset($res['errcode']) ? $res['errcode'] : ''; $msg = isset($res['errmsg']) ? $res['errmsg'] : MyLang('send_fail'); // 这几种情况则视为正常,不影响业务 // 10060001 支付单不存在 // 10060002 支付单已完成发货 // 10060003 无法继续发货 // 10060023 发货信息未更新 // 10060004 支付单处于不可发货的状态 if(in_array($code, [10060001, 10060002, 10060003, 10060023, 10060004]) || $code == 0 && $msg == 'ok') { return DataReturn(MyLang('send_success'), 0, $res); } else { $msg = ($code !== '') ? $code.':'.$msg : $msg; } return DataReturn($msg, -1); } /** * 获取微信小程序中的快递公司编码:根据快递公司名称 * @author Shon Wu * @blog https://github.com/mantoufan/ * @version 1.0.0 * @date 2023-08-21 * @desc description * @param [string] $express_name [快递公司名称] * @return [string|null] [成功返回快递公司编码, 失败则空] */ function GetMiniDeliveryIdByName($express_name) { // 获取access_token $access_token = $this->GetMiniAccessToken(); if($access_token === false) { return DataReturn(MyLang('common_extend.base.common.access_token_request_fail_tips'), -1); } // 获取运力id列表接口 $url = 'https://api.weixin.qq.com/cgi-bin/express/delivery/open_msg/get_delivery_list?access_token='.$access_token; // 需要 POST {} 给接口 $res = $this->HttpRequestPost($url, (object)[], true); $msg = isset($res['errmsg']) ? $res['errmsg'] : MyLang('get_fail'); if(isset($res['errcode']) && $res['errcode'] == 0) { $delivery_list = $res['delivery_list']; $delivery_names = array_column($delivery_list, 'delivery_name'); $delivery_index = array_search($express_name, $delivery_names); if($delivery_index === false) { // 精确匹配失败后,如果公司名称包含快递、物流、速运、速递和邮政,进行模糊匹配。例如:顺丰快递 - 顺丰速运,韵达快递 - 韵达速递 // 关键词的笛卡尔积,例如:顺丰快递未匹配,则依次查找顺丰速运、顺丰速递、顺丰物流、顺丰,直到找到一个匹配编码为止 $to_be_replaceds = [ '快递' => ['速运', '速递', '物流', ''], '物流' => ['快递', '速运', '速递', ''], '速运' => ['快递', '速递', '物流', ''], '速递' => ['快递', '速运', '物流', ''], // EMS速递 - EMS '邮政' => ['邮政快递', '国际邮政'], // 邮政包裹 - 邮政快递包裹 / 国际邮政包裹 ]; foreach($to_be_replaceds as $pattern => $replaceds) { if(stripos($express_name, $pattern) !== false) { foreach($replaceds as $replaced) { $express_name_replaced = str_replace($pattern, $replaced, $express_name); $delivery_index = array_search($express_name_replaced, $delivery_names); if($delivery_index !== false) { break 2; } } break; } } } if($delivery_index !== false) { return DataReturn(MyLang('get_success'), 0, $delivery_list[$delivery_index]['delivery_id']); } // 无匹配物流公司编码 $msg = MyLang('common_extend.base.wechat.no_match_logistics_company_code'); } else { $msg = isset($res['errcode']) ? $res['errcode'].':'.$msg : $msg; } return DataReturn($msg, -1); } /** * 检验数据的真实性,并且获取解密后的明文 * @author Devil * @blog http://gong.gg/ * @version 1.0.0 * @datetime 2017-12-30T18:20:53+0800 * @param [string] $encrypted_data [加密的用户数据] * @param [string] $iv [与用户数据一同返回的初始向量] * @param [string] $openid [解密后的原文] * @return [array|string] [成功返回用户信息数组, 失败返回错误信息] */ public function DecryptData($encrypted_data, $iv, $openid) { // 登录授权session $login_key = 'wechat_user_login_'.$openid; $session_data = MyCache($login_key); if(empty($session_data)) { return DataReturn(MyLang('common_extend.base.common.session_key_empty_tips'), -1); } // iv长度 if(strlen($iv) != 24) { return DataReturn(MyLang('common_extend.base.common.iv_error_tips'), -1); } // 加密函数 if(!function_exists('openssl_decrypt')) { return DataReturn(MyLang('openssl_no_support_tips'), -1); } $result = openssl_decrypt(base64_decode($encrypted_data), "AES-128-CBC", base64_decode($session_data['session_key']), 1, base64_decode($iv)); $data = json_decode($result, true); if($data == NULL) { return DataReturn(MyLang('common_extend.base.common.please_try_again_tips'), -1); } if($data['watermark']['appid'] != $this->_appid) { return DataReturn(MyLang('appid_mismatch_tips'), -1); } return DataReturn('success', 0, $data); } /** * 用户授权 * @author Devil * @blog http://gong.gg/ * @version 1.0.0 * @date 2018-11-06 * @desc description * @param [array] $params [输入参数] */ public function GetAuthSessionKey($params = []) { if(empty($params['authcode'])) { return DataReturn(MyLang('common_extend.base.common.auth_code_empty_tips'), -1); } // 请求获取session_key $url = 'https://api.weixin.qq.com/sns/jscode2session?appid='.$this->_appid.'&secret='.$this->_appsecret.'&js_code='.$params['authcode'].'&grant_type=authorization_code'; $result = $this->HttpRequestGet($url); if(empty($result)) { return DataReturn(MyLang('common_extend.base.common.auth_api_request_fail_tips'), -1); } if(!empty($result['openid'])) { // 缓存SessionKey $key = 'wechat_user_login_'.$result['openid']; // 缓存存储 MyCache($key, $result); return DataReturn(MyLang('auth_success'), 0, $result); } $msg = empty($result['errmsg']) ? MyLang('common_extend.base.common.auth_api_request_error_tips') : $result['errmsg']; return DataReturn($msg, -1); } /** * 二维码创建 * @author Devil * @blog http://gong.gg/ * @version 1.0.0 * @datetime 2018-01-02T19:53:10+0800 * @param [string] $params['page'] [页面地址] * @param [string] $params['scene'] [参数] * @return [string] [成功返回文件流, 失败则空] */ public function MiniQrCodeCreate($params) { // 请求参数 $p = [ [ 'checked_type' => 'empty', 'key_name' => 'page', 'error_msg' => MyLang('common_extend.base.common.page_empty_tips'), ], [ 'checked_type' => 'length', 'checked_data' => '1,32', 'key_name' => 'scene', 'error_msg' => MyLang('common_extend.base.common.scene_empty_tips'), ], ]; $ret = ParamsChecked($params, $p); if($ret !== true) { return DataReturn($ret, -1); } // 获取access_token $access_token = $this->GetMiniAccessToken(); if($access_token === false) { return DataReturn(MyLang('common_extend.base.common.access_token_request_fail_tips'), -1); } // 获取二维码 $url = 'https://api.weixin.qq.com/wxa/getwxacodeunlimit?access_token='.$access_token; $data = [ 'page' => $params['page'], 'scene' => $params['scene'], 'width' => empty($params['width']) ? 1000 : intval($params['width']), ]; $res = $this->HttpRequestPost($url, $data, false); if(!empty($res)) { if(stripos($res, 'errcode') === false) { return DataReturn(MyLang('get_success'), 0, $res); } $res = json_decode($res, true); $msg = isset($res['errmsg']) ? $res['errmsg'] : MyLang('common_extend.base.common.get_qrcode_fail_tips'); } else { $msg = MyLang('common_extend.base.common.get_qrcode_fail_tips'); } return DataReturn($msg, -1); } /** * 获取用户手机号码 * @author Devil * @blog http://gong.gg/ * @version 1.0.0 * @date 2022-04-26 * @desc description * @param [string] $code [临时code] */ public function GetUserPhoneNumber($code) { // 获取access_token $access_token = $this->GetMiniAccessToken(); if($access_token === false) { return DataReturn(MyLang('common_extend.base.common.access_token_request_fail_tips'), -1); } // 获取手机 $url = 'https://api.weixin.qq.com/wxa/business/getuserphonenumber?access_token='.$access_token; $data = [ 'code' => $code, ]; $res = $this->HttpRequestPost($url, $data); if(!empty($res)) { if(isset($res['errcode']) && $res['errcode'] == 0 && !empty($res['phone_info'])) { return DataReturn('success', 0, $res['phone_info']['purePhoneNumber']); } return DataReturn($res['errmsg'].'('.$res['errcode'].')', -1); } return DataReturn(MyLang('api_request_fail_tips'), -1); } /** * 小程序获取access_token * @author Devil * @blog http://gong.gg/ * @version 1.0.0 * @date 2020-08-26 * @desc description */ public function GetMiniAccessToken() { return $this->GetAccessToken(); } /** * 获取微信环境签名配置信息 * @author Devil * @blog http://gong.gg/ * @version 1.0.0 * @date 2020-08-26 * @desc description * @param [array] $params [输入参数] */ public function GetSignPackage($params = []) { $access_token = $this->GetAccessToken(); if(!empty($access_token)) { // 获取 ticket $ticket = $this->GetTicket($access_token); // 注意 URL 一定要动态获取,不能 hardcode. $url = empty($params['url']) ? __MY_VIEW_URL__ : urldecode(htmlspecialchars_decode($params['url'])); $timestamp = time(); $nonce_str = $this->CreateNonceStr(); // 这里参数的顺序要按照 key 值 ASCII 码升序排序 $string = "jsapi_ticket={$ticket}&noncestr={$nonce_str}×tamp={$timestamp}&url={$url}"; return [ 'appId' => $this->_appid, 'nonceStr' => $nonce_str, 'timestamp' => $timestamp, 'url' => $url, 'signature' => sha1($string), 'rawString' => $string ]; } return []; } /** * 签名随机字符串创建 * @author Devil * @blog http://gong.gg/ * @version 1.0.0 * @date 2020-08-26 * @desc description * @param [int] $length [长度] */ public function CreateNonceStr($length = 16) { $chars = 'abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789'; $str = ''; for($i = 0; $i < $length; $i++) { $str .= substr($chars, mt_rand(0, strlen($chars) - 1), 1); } return $str; } /** * 公共获取access_token * @author Devil * @blog http://gong.gg/ * @version 1.0.0 * @date 2020-08-26 * @desc description */ public function GetAccessToken() { // 缓存key $key = $this->_appid.'_access_token'; $result = MyCache($key); if(!empty($result)) { if($result['expires_in'] > time()) { return $result['access_token']; } } // 网络请求 $url = 'https://api.weixin.qq.com/cgi-bin/token?grant_type=client_credential&appid='.$this->_appid.'&secret='.$this->_appsecret; $result = $this->HttpRequestGet($url); if(!empty($result) && !empty($result['access_token'])) { // 缓存存储 $result['expires_in'] += time(); MyCache($key, $result); return $result['access_token']; } return false; } /** * 获取授权页ticket * @author Devil * @blog http://gong.gg/ * @version 1.0.0 * @date 2020-08-26 * @desc description * @param [string] $access_token [access_token] * @param [string] $type [类型(默认jsapi)] */ public function GetTicket($access_token, $type = 'jsapi') { // 缓存key $key = $this->_appid.'_get_ticket'; $result = MyCache($key); if(!empty($result)) { if($result['expires_in'] > time()) { return $result['ticket']; } } // 网络请求 $url = 'https://api.weixin.qq.com/cgi-bin/ticket/getticket?access_token='.$access_token.'&type='.$type; $result = $this->HttpRequestGet($url); if(!empty($result) && !empty($result['ticket'])) { // 缓存存储 $result['expires_in'] += time(); MyCache($key, $result); return $result['ticket']; } return false; } /** * get请求 * @author Devil * @blog http://gong.gg/ * @version 1.0.0 * @datetime 2018-01-03T19:21:38+0800 * @param [string] $url [url地址] * @return [array] [返回数据] */ public function HttpRequestGet($url) { $curl = curl_init(); curl_setopt($curl, CURLOPT_RETURNTRANSFER, true); curl_setopt($curl, CURLOPT_TIMEOUT, 500); curl_setopt($curl, CURLOPT_SSL_VERIFYPEER, false); curl_setopt($curl, CURLOPT_SSL_VERIFYHOST, false); curl_setopt($curl, CURLOPT_URL, $url); $res = curl_exec($curl); curl_close($curl); return json_decode($res, true); } /** * curl模拟post * @author Devil * @blog http://gong.gg/ * @version 1.0.0 * @datetime 2017-09-25T09:10:46+0800 * @param [string] $url [请求地址] * @param [array] $data [发送的post数据] * @param [array] $is_parsing [是否需要解析数据] * @return [array] [返回的数据] */ public function HttpRequestPost($url, $data, $is_parsing = true) { $curl = curl_init(); curl_setopt($curl, CURLOPT_RETURNTRANSFER, true); curl_setopt($curl, CURLOPT_TIMEOUT, 500); curl_setopt($curl, CURLOPT_SSL_VERIFYPEER, false); curl_setopt($curl, CURLOPT_SSL_VERIFYHOST, false); curl_setopt($curl, CURLOPT_URL, $url); curl_setopt($curl, CURLOPT_POSTFIELDS, json_encode($data, JSON_UNESCAPED_UNICODE)); curl_setopt($curl, CURLOPT_POST, true); $res = curl_exec($curl); curl_close($curl); if($is_parsing === true) { return json_decode($res, true); } return $res; } } ?>