vr-shopxo-plugin/shopxo/extend/base/Wechat.php

742 lines
28 KiB
PHP
Raw Permalink Normal View History

<?php
// +----------------------------------------------------------------------
// | ShopXO 国内领先企业级B2C免费开源电商系统
// +----------------------------------------------------------------------
// | Copyright (c) 2011~2099 http://shopxo.net All rights reserved.
// +----------------------------------------------------------------------
// | Licensed ( https://opensource.org/licenses/mit-license.php )
// +----------------------------------------------------------------------
// | Author: Devil
// +----------------------------------------------------------------------
namespace base;
/**
* 微信驱动
* @author Devil
* @blog http://gong.gg/
* @version 1.0.0
* @date 2018-06-28
* @desc 支持所有文件存储到硬盘
*/
class Wechat
{
// appid
private $_appid;
// appsecret
private $_appsecret;
/**
* 构造方法
* @author Devil
* @blog http://gong.gg/
* @version 1.0.0
* @datetime 2017-12-30T18:04:05+0800
* @param [string] $app_id [应用appid]
* @param [string] $app_secret [应用密钥]
*/
public function __construct($app_id, $app_secret)
{
$this->_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}&timestamp={$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;
}
}
?>