You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
543 lines
20 KiB
543 lines
20 KiB
<?php
|
|
|
|
namespace service;
|
|
|
|
use app\admin\model\order\StoreOrder;
|
|
use app\admin\model\order\StoreOrderCartInfo;
|
|
use app\admin\model\user\User;
|
|
use EasyWeChat\Foundation\Application;
|
|
use EasyWeChat\Payment\Order;
|
|
use think\exception\ValidateException;
|
|
use think\Url;
|
|
use app\admin\model\wechat\WechatMessage;
|
|
use app\wap\model\user\ToutiaoUser;
|
|
use service\SystemConfigService;
|
|
use service\AdminException;
|
|
use service\HookService;
|
|
use Exception;
|
|
use EasyWeChat\Encryption\EncryptionException;
|
|
use think\Cache;
|
|
use think\Log;
|
|
|
|
/**快手小程序接口
|
|
* Class WechatMinService
|
|
* @package service
|
|
*/
|
|
class TouMiniProgramService
|
|
{
|
|
|
|
protected static $instance;
|
|
const DY_LINK = 'https://developer.toutiao.com/api/apps/v2/';
|
|
const CODE2_SESSION_URL = self::DY_LINK . 'jscode2session';
|
|
const ACCESS_TOKEN_URL = self::DY_LINK . 'token';
|
|
const CREATE_ORDER_URL = 'https://developer.toutiao.com/api/apps/ecpay/v1/create_order';
|
|
const PUSH_ORDER_URL ='https://developer.toutiao.com/api/apps/order/v2/push';
|
|
const REFUND_ORDER_URL ='https://developer.toutiao.com/api/apps/ecpay/v1/create_refund';
|
|
const QRCODE_URL='https://open.douyin.com/api/apps/v1/qrcode/create/';
|
|
const CLIENT_TOKEN_URL='https://open.douyin.com/oauth/client_token/';
|
|
protected $appid;
|
|
protected $appSecret;
|
|
protected $paySalt;
|
|
protected $token;
|
|
public $data = null;
|
|
public function __construct()
|
|
{
|
|
$toutiao = SystemConfigService::more(['site_url', 'toutiao_mini_appid', 'toutiao_mini_secret', 'toutiao_pay_salt', 'toutiao_token']);
|
|
$this->appid = isset($toutiao['toutiao_mini_appid']) ? trim($toutiao['toutiao_mini_appid']) : '';
|
|
$this->appSecret = isset($toutiao['toutiao_mini_secret']) ? trim($toutiao['toutiao_mini_secret']) : '';
|
|
$this->paySalt = isset($toutiao['toutiao_pay_salt']) ? trim($toutiao['toutiao_pay_salt']) : '';
|
|
$this->token = isset($toutiao['toutiao_token']) ? trim($toutiao['toutiao_token']) : '';
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
* 获得用户信息 根据code 获取session_key
|
|
* @param array|string $openid
|
|
* @return $userInfo
|
|
*/
|
|
public function getUserInfo($code)
|
|
{
|
|
try {
|
|
$dat = array(
|
|
'appid' => $this->appid,
|
|
'secret' => $this->appSecret,
|
|
'code' => $code,
|
|
"anonymous_code" => ''
|
|
);
|
|
if (isset($code)) {
|
|
$res = self::jsonPost(self::CODE2_SESSION_URL, json_encode($dat));
|
|
$data = json_decode($res, true);
|
|
if ($data['err_no'] == 0) {
|
|
return $data['data'];
|
|
} else {
|
|
throw new ValidateException("获取失败");
|
|
}
|
|
} else {
|
|
throw new ValidateException("获取失败");
|
|
}
|
|
} catch (\Throwable $e) {
|
|
throw new ValidateException($e->getMessage());
|
|
}
|
|
}
|
|
|
|
|
|
/**
|
|
* 加密数据解密
|
|
* @param $sessionKey
|
|
* @param $iv
|
|
* @param $encryptData
|
|
* @return $userInfo
|
|
*/
|
|
public static function encryptor($sessionKey, $iv, $encrypted)
|
|
{
|
|
try {
|
|
$decrypted = openssl_decrypt(
|
|
base64_decode($encrypted, true),
|
|
'aes-128-cbc',
|
|
base64_decode($sessionKey, true),
|
|
OPENSSL_RAW_DATA | OPENSSL_NO_PADDING,
|
|
base64_decode($iv, true)
|
|
);
|
|
} catch (Exception $e) {
|
|
throw new EncryptionException($e->getMessage(), EncryptionException::ERROR_DECRYPT_AES);
|
|
}
|
|
|
|
if (is_null($result = json_decode(self::decode($decrypted), true))) {
|
|
throw new EncryptionException('ILLEGAL_BUFFER', EncryptionException::ILLEGAL_BUFFER);
|
|
}
|
|
return $result;
|
|
}
|
|
public static function decode($decrypted)
|
|
{
|
|
$pad = ord(substr($decrypted, -1));
|
|
if ($pad < 1 || $pad > 32) {
|
|
$pad = 0;
|
|
}
|
|
return substr($decrypted, 0, (strlen($decrypted) - $pad));
|
|
}
|
|
|
|
/**
|
|
* 生成支付订单对象
|
|
* @param $openid
|
|
* @param $out_trade_no
|
|
* @param $total_fee
|
|
* @param $attach
|
|
* @param $body
|
|
* @param string $detail
|
|
* @param string $trade_type
|
|
* @param array $options
|
|
* @return Order
|
|
*/
|
|
protected static function paymentOrder($openid, $out_trade_no, $total_fee, $attach, $body, $detail = '', $trade_type = 'JSAPI', $options = [])
|
|
{
|
|
$total_fee = bcmul($total_fee, 100, 0);
|
|
$order = array_merge(compact('openid', 'out_trade_no', 'total_fee', 'attach', 'body', 'detail', 'trade_type'), $options);
|
|
if ($order['detail'] == '') unset($order['detail']);
|
|
return new Order($order);
|
|
}
|
|
|
|
/**
|
|
* 订单推送到抖音
|
|
* @param $data array 订单数据
|
|
* @note order_status 与 status须保持一致,但类型不同
|
|
* @return array
|
|
*/
|
|
public function pushOrder($order_id)
|
|
{
|
|
$orderInfo = StoreOrder::where(['order_id' => $order_id])->find();
|
|
$cart = StoreOrderCartInfo::where('oid', $orderInfo['id'])->find();
|
|
$cartInfo = (array)(json_decode($cart['cart_info']));
|
|
$userInfo = ToutiaoUser::where('uid', $cartInfo['uid'])->find();
|
|
$productInfo = (array)$cartInfo['productInfo'];
|
|
$openid = $userInfo['openid']; //获取下单用户openid
|
|
//组装商品
|
|
$item_list = [[
|
|
'item_code' => (string)$productInfo['id'],
|
|
'img' => $productInfo['image'],
|
|
'title' => $productInfo['store_name'],
|
|
'amount' => $cartInfo['cart_num'],
|
|
'price' => (int)($orderInfo['pay_price']*100)
|
|
]]; //参数对应请查看官方文档,注意字段类型
|
|
// 组装订单
|
|
$detail = [
|
|
'order_id' => $orderInfo['order_id'],
|
|
'create_time' => $orderInfo['add_time'],
|
|
'status' => "已支付",
|
|
'amount' => $orderInfo['total_num'],
|
|
'total_price' => (int)($orderInfo['total_price']*100),
|
|
'detail_url' => "pages/store/detail?id=" . $productInfo['id'],
|
|
'item_list' => $item_list
|
|
];
|
|
$param = [
|
|
'access_token' => $this->get_token(),
|
|
'app_name' => "douyin",
|
|
'open_id' => $openid,
|
|
'update_time' => $this->getMillisecond(),
|
|
'order_detail' => json_encode($detail),
|
|
'order_type' => 0,
|
|
'order_status' => 1
|
|
];
|
|
$this->jsonPost(self::PUSH_ORDER_URL, json_encode($param)); //请求
|
|
}
|
|
|
|
|
|
public function getMillisecond()
|
|
{
|
|
list($t1, $t2) = explode(' ', microtime());
|
|
return (float)sprintf('%.0f', (floatval($t1) + floatval($t2)) * 1000);
|
|
}
|
|
/**头条小程序二维码生成接口不限量永久
|
|
* @param $scene
|
|
* @param null $page
|
|
* @param null $width
|
|
* @param null $autoColor
|
|
* @param array $lineColor
|
|
* @return \Psr\Http\Message\StreamInterface
|
|
*/
|
|
public function appCodeUnlimitService($scene, $page = null ,$width = 430, $autoColor = false, $lineColor = ['r' => 0, 'g' => 0, 'b' => 0])
|
|
{
|
|
$data = [
|
|
'app_name' => 'douyin',
|
|
'appid' => $this->appid, //订单号
|
|
'path' =>$page.'?'.$scene , //金额 单位:分
|
|
];
|
|
$json = json_encode($data, 320);
|
|
$res = json_decode(self::clientjsonPost(self::QRCODE_URL, $json),true);
|
|
if($res['err_no']!=0)throw new ValidateException($res['err_msg']/* .'可点击右上角分享' */);
|
|
return json_decode($res, true)['data'];
|
|
}
|
|
|
|
/**
|
|
* 获得jsSdk支付参数
|
|
* @param $openid
|
|
* @param $out_trade_no
|
|
* @param $total_fee
|
|
* @param $attach
|
|
* @param $body
|
|
* @param string $detail
|
|
* @param string $trade_type
|
|
* @param array $options
|
|
* @return array|string
|
|
*/
|
|
public function jsPay( $out_trade_no, $total_fee, $attach, $body, $detail = '', $trade_type = 'JSAPI', $options = [])
|
|
{
|
|
$price = $total_fee * 100;
|
|
$data = [
|
|
'app_id' => $this->appid,
|
|
'out_order_no' => $out_trade_no, //订单号
|
|
'total_amount' => $price, //金额 单位:分
|
|
'body' => $body, //支付的内容
|
|
'subject' => $body, //支付的标题
|
|
'valid_time' => 3600,
|
|
'cp_extra' => $attach,
|
|
'notify_url' => SystemConfigService::get('site_url') . Url::build('wap/Toutiao/notify'),
|
|
];
|
|
$data['sign'] = self::generate_sign($data);
|
|
$json = json_encode($data, 320);
|
|
$res = self::jsonPost(self::CREATE_ORDER_URL, $json);
|
|
return json_decode($res, true)['data'];
|
|
}
|
|
/**
|
|
* 签名
|
|
*/
|
|
public function generate_sign($map)
|
|
{
|
|
$rList = [];
|
|
foreach ($map as $k => $v) {
|
|
if ($k == "other_settle_params" || $k == "app_id" || $k == "sign" || $k == "thirdparty_id")
|
|
continue;
|
|
|
|
$value = trim(strval($v));
|
|
if (is_array($v)) {
|
|
$value = self::arrayToStr($v);
|
|
}
|
|
|
|
$len = strlen($value);
|
|
if ($len > 1 && substr($value, 0, 1) == "\"" && substr($value, $len - 1) == "\"")
|
|
$value = substr($value, 1, $len - 1);
|
|
$value = trim($value);
|
|
if ($value == "" || $value == "null")
|
|
continue;
|
|
$rList[] = $value;
|
|
}
|
|
$rList[] = $this->paySalt;
|
|
sort($rList, SORT_STRING);
|
|
return md5(implode('&', $rList));
|
|
}
|
|
public static function arrayToStr($map)
|
|
{
|
|
$isMap = self::isArrMap($map);
|
|
$result = "";
|
|
if ($isMap) {
|
|
$result = "map[";
|
|
}
|
|
$keyArr = array_keys($map);
|
|
if ($isMap) {
|
|
sort($keyArr);
|
|
}
|
|
$paramsArr = array();
|
|
foreach ($keyArr as $k) {
|
|
$v = $map[$k];
|
|
if ($isMap) {
|
|
if (is_array($v)) {
|
|
$paramsArr[] = sprintf("%s:%s", $k, self::arrayToStr($v));
|
|
} else {
|
|
$paramsArr[] = sprintf("%s:%s", $k, trim(strval($v)));
|
|
}
|
|
} else {
|
|
if (is_array($v)) {
|
|
$paramsArr[] = self::arrayToStr($v);
|
|
} else {
|
|
$paramsArr[] = trim(strval($v));
|
|
}
|
|
}
|
|
}
|
|
|
|
$result = sprintf("%s%s", $result, join(" ", $paramsArr));
|
|
if (!$isMap) {
|
|
$result = sprintf("[%s]", $result);
|
|
} else {
|
|
$result = sprintf("%s]", $result);
|
|
}
|
|
return $result;
|
|
}
|
|
|
|
public static function isArrMap($map)
|
|
{
|
|
foreach ($map as $k => $v) {
|
|
if (is_string($k)) {
|
|
return true;
|
|
}
|
|
}
|
|
return false;
|
|
}
|
|
/**
|
|
* 获得验签
|
|
*/
|
|
public function callbackSign($data)
|
|
{
|
|
if (!isset($data['msg_signature']) || !isset($data['timestamp']) || !isset($data['nonce']) || !isset($data['msg'])) {
|
|
return false;
|
|
}
|
|
$arr = [
|
|
$this->token, (string)$data['timestamp'], (string)$data['nonce'], (string)$data['msg']
|
|
];
|
|
Log::write($arr);
|
|
sort($arr, SORT_STRING);
|
|
|
|
$join_str = implode('', $arr);
|
|
return sha1($join_str);
|
|
}
|
|
/**
|
|
* 获得token
|
|
*/
|
|
public function get_token()
|
|
{
|
|
try {
|
|
$param = ['appid' => $this->appid, 'secret' => $this->appSecret, 'grant_type' => "client_credential"];
|
|
$access_token = Cache::get('dy_accessToken');
|
|
if (empty($access_token)) {
|
|
$data = $this->jsonPost(self::ACCESS_TOKEN_URL,json_encode($param));
|
|
|
|
$data=(array)json_decode($data);
|
|
if ($data['err_no'] == 0) {
|
|
$access_token = ((array)$data['data'])['access_token'];
|
|
Cache::set('dy_accessToken', $access_token, ((array)$data['data'])['expires_in']);
|
|
}
|
|
}
|
|
return $access_token;
|
|
} catch (\Throwable $e) {
|
|
throw new ValidateException($e->getMessage());
|
|
}
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
|
* [createRefund 订单退款]
|
|
* @param [type] $order [订单相关信息]
|
|
* @return [type] [description]
|
|
* $order = array(
|
|
* order_sn' => '', // 订单编号
|
|
* 'refund_sn' => '', // 退款编号
|
|
* 'total_amount' => '', // 订单金额(分)
|
|
* 'body' => '', // 退款原因
|
|
*/
|
|
|
|
public function createRefund($order)
|
|
{
|
|
$params = [
|
|
'app_id' => $this->appid, // 是 小程序 id
|
|
'out_order_no' => (string)$order['order_id'], // 是 商户分配订单号,标识进行退款的订单
|
|
'out_refund_no' => (string)'refund_'.$order['order_id'], // 是 商户分配退款号
|
|
'refund_amount' => (int)($order['refund_price']*100), // 是 退款金额,单位[分]
|
|
'reason' => $order['refund_reason_wap_explain'] ?? '用户申请退款', // 是 退款理由,长度上限 100
|
|
'cp_extra' => '', // 否 开发者自定义字段,回调原样回传
|
|
'notify_url' => '', // 否 商户自定义回调地址
|
|
// 'sign' => '', // 是 开发者对核心字段签名, 签名方式见文档, 防止传输过程中出现意外
|
|
'thirdparty_id' => '', // 否,服务商模式接入必传 第三方平台服务商 id,非服务商模式留空
|
|
'disable_msg' => 1, // 否 是否屏蔽担保支付消息,1-屏蔽
|
|
'msg_page' => '', // 否 担保支付消息跳转页
|
|
|
|
];
|
|
$params['sign']=self::generate_sign( $params);
|
|
!empty($order['cp_extra']) && $params['cp_extra'] = $order['cp_extra'];
|
|
!empty($order['all_settle']) && $params['all_settle'] = $order['all_settle'];
|
|
!empty($config['thirdparty_id']) && $params['thirdparty_id'] = $config['thirdparty_id'];
|
|
if (!empty($config['msg_page'])) {
|
|
$params['disable_msg'] = 0;
|
|
$params['msg_page'] = $config['msg_page'];
|
|
}
|
|
$params['sign'] = self::generate_sign($params);
|
|
$url = self::REFUND_ORDER_URL;
|
|
$response = $this->jsonPost($url, json_encode($params));
|
|
$result = json_decode($response, true);
|
|
return $result;
|
|
}
|
|
|
|
/**
|
|
* [queryRefund 退款查询]
|
|
* @param [type] $refundSn [开发者侧的订单号, 不可重复]
|
|
* @return [type] [description]
|
|
|
|
*/
|
|
|
|
public function queryRefund($refundSn)
|
|
{
|
|
$params = [
|
|
'app_id' => $this->appid, // 小程序 AppID
|
|
'out_refund_no' => $refundSn, // 开发者侧的退款号
|
|
// 'sign' => '', // 开发者对核心字段签名, 签名方式见文档, 防止传输过程中出现意外
|
|
// 'thirdparty_id' => '', // 服务商模式接入必传 第三方平台服务商 id,非服务商模式留空
|
|
];
|
|
!empty($config['thirdparty_id']) && $params['thirdparty_id'] = $config['thirdparty_id'];
|
|
$params['sign'] = self::generate_sign($params);
|
|
$url = self::$queryRefundUrl;
|
|
$response = Http::post($url, json_encode($params));
|
|
$result = json_decode($response, true);
|
|
return $result;
|
|
|
|
}
|
|
|
|
/**
|
|
* [notifyRefund 退款回调验证]
|
|
* @return [array] [返回数组格式的notify数据]
|
|
*/
|
|
public static function notifyRefund()
|
|
{
|
|
$data = $_POST; // 获取回调数据
|
|
$config = self::$config;
|
|
if (!$data || empty($data['status']))
|
|
die('暂无回调信息');
|
|
$result = json_decode($data['msg'], true); // 进行签名验证
|
|
// 判断签名是否正确 判断支付状态
|
|
if ($result && $data['status']!='FAIL') {
|
|
return $data;
|
|
} else {
|
|
return false;
|
|
}
|
|
}
|
|
|
|
public function get_client_token(){
|
|
try {
|
|
$param = ['client_key' => $this->appid, 'client_secret' => $this->appSecret, 'grant_type' => "client_credential"];
|
|
$access_token = Cache::get('dy_clientToken');
|
|
if (empty($access_token)) {
|
|
$data = $this->jsonPost(self::CLIENT_TOKEN_URL,$param,0,'multipart/form-data');
|
|
$data=(array)json_decode($data);
|
|
$data=(array)($data['data']);
|
|
if ($data['error_code'] == 0) {
|
|
$access_token = $data['access_token'];
|
|
Cache::set('dy_clientToken', $access_token, ($data['expires_in']));
|
|
}
|
|
}
|
|
return $access_token;
|
|
} catch (\Throwable $e) {
|
|
throw new ValidateException($e->getMessage());
|
|
}
|
|
}
|
|
|
|
public function jsonPost($url, $data = NULL, $times = 0,$type="application/json; charset=utf-8")
|
|
{
|
|
|
|
$curl = curl_init();
|
|
curl_setopt($curl, CURLOPT_URL, $url);
|
|
curl_setopt($curl, CURLOPT_SSL_VERIFYPEER, false);
|
|
curl_setopt($curl, CURLOPT_SSL_VERIFYHOST, false);
|
|
curl_setopt($curl, CURLOPT_POST, 1);
|
|
curl_setopt($curl, CURLOPT_TIMEOUT, 2); //超时时间2秒
|
|
curl_setopt($curl, CURLOPT_POSTFIELDS, $data);
|
|
curl_setopt($curl, CURLOPT_HEADER, 0);
|
|
curl_setopt($curl, CURLOPT_HTTPHEADER, array(
|
|
'Content-Type: '.$type,
|
|
'Cache-Control: no-cache',
|
|
'Pragma: no-cache'
|
|
));
|
|
curl_setopt($curl, CURLOPT_RETURNTRANSFER, 1);
|
|
$res = curl_exec($curl);
|
|
curl_close($curl);
|
|
return $res;
|
|
}
|
|
public function clientjsonPost($url, $data = NULL, $times = 0)
|
|
{
|
|
|
|
$curl = curl_init();
|
|
curl_setopt($curl, CURLOPT_URL, $url);
|
|
curl_setopt($curl, CURLOPT_SSL_VERIFYPEER, false);
|
|
curl_setopt($curl, CURLOPT_SSL_VERIFYHOST, false);
|
|
curl_setopt($curl, CURLOPT_POST, 1);
|
|
curl_setopt($curl, CURLOPT_TIMEOUT, 2); //超时时间2秒
|
|
curl_setopt($curl, CURLOPT_POSTFIELDS, $data);
|
|
curl_setopt($curl, CURLOPT_HEADER, 0);
|
|
curl_setopt($curl, CURLOPT_HTTPHEADER, array(
|
|
'Content-Type: application/json; charset=utf-8',
|
|
'Cache-Control: no-cache',
|
|
'Pragma: no-cache',
|
|
'access-token:'.$this->get_client_token()
|
|
));
|
|
curl_setopt($curl, CURLOPT_RETURNTRANSFER, 1);
|
|
$res = curl_exec($curl);
|
|
curl_close($curl);
|
|
return $res;
|
|
}
|
|
|
|
public static function curlGet($url = '', $options = array())
|
|
{
|
|
$ch = curl_init($url);
|
|
curl_setopt($ch, CURLOPT_RETURNTRANSFER, 1);
|
|
curl_setopt($ch, CURLOPT_TIMEOUT, 30);
|
|
if (!empty($options)) {
|
|
curl_setopt_array($ch, $options);
|
|
}
|
|
//https请求 不验证证书和host
|
|
curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, false);
|
|
curl_setopt($ch, CURLOPT_SSL_VERIFYHOST, false);
|
|
$data = curl_exec($ch);
|
|
curl_close($ch);
|
|
return $data;
|
|
}
|
|
public function curlPost($url = '', $postData = '', $options = array())
|
|
{
|
|
if (is_array($postData)) {
|
|
$postData = http_build_query($postData);
|
|
}
|
|
$ch = curl_init();
|
|
curl_setopt($ch, CURLOPT_URL, $url);
|
|
curl_setopt($ch, CURLOPT_RETURNTRANSFER, 1);
|
|
curl_setopt($ch, CURLOPT_POST, 1);
|
|
curl_setopt($ch, CURLOPT_POSTFIELDS, $postData);
|
|
curl_setopt($ch, CURLOPT_TIMEOUT, 30); //设置cURL允许执行的最长秒数
|
|
if (!empty($options)) {
|
|
curl_setopt_array($ch, $options);
|
|
}
|
|
//https请求 不验证证书和host
|
|
curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, false);
|
|
curl_setopt($ch, CURLOPT_SSL_VERIFYHOST, false);
|
|
$data = curl_exec($ch);
|
|
curl_close($ch);
|
|
return $data;
|
|
}
|
|
}
|
|
|