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

742 lines
28 KiB
PHP
Executable File
Raw Permalink Blame History

This file contains ambiguous Unicode characters!

This file contains ambiguous Unicode characters that may be confused with others in your current locale. If your use case is intentional and legitimate, you can safely ignore this warning. Use the Escape button to highlight these characters.

<?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;
}
}
?>