<?php
// +----------------------------------------------------------------------
// | Bwsaas
// +----------------------------------------------------------------------
// | Copyright (c) 2015~2020 http://www.buwangyun.com All rights reserved.
// +----------------------------------------------------------------------
// | Licensed ( http://www.apache.org/licenses/LICENSE-2.0 )
// +----------------------------------------------------------------------
// | Gitee ( https://gitee.com/buwangyun/bwsaas )
// +----------------------------------------------------------------------
// | Author: buwangyun <hnlg666@163.com>
// +----------------------------------------------------------------------
// | Date: 2020-9-28 10:55:00
// +----------------------------------------------------------------------

namespace bw;


use addons\epay\library\Service;
use app\admin\model\Miniqrcode;
use app\admin\model\Xftts;
use app\common\library\Upload;
use think\Cache;
use think\File;
use app\common\model\Attachment;
use addons\xftts\library\Tts;
use addons\xftts\library\WebSocket\Client;
use addons\xftts\library\WebSocket\Exception;
use think\Response;
use traits\CacheTrait;

/** 商城工具类
 * Class Common
 * @package app\bwmall\model
 */
class Common
{

    use CacheTrait;

    /**
     * 获取图片完整连接
     */
    public static function getImagesFullUrl($value = '')
    {
        if (stripos($value, 'http') === 0 || $value === '' || stripos($value, 'data:image') === 0) {
            return $value;
        } else {
            $upload = \think\Config::get('upload');
            if (!empty($upload['cdnurl'])) {
                return  $upload['cdnurl'] . $value;
            } else {
                return self::getHttpLocation() . $value;
            }
        }
    }

    /**
     * 获取当前地址
     * @return string
     */
    public static function getHttpLocation() {
        $http_type = ((isset($_SERVER['HTTPS']) && $_SERVER['HTTPS'] == 'on') || (isset($_SERVER['HTTP_X_FORWARDED_PROTO']) && $_SERVER['HTTP_X_FORWARDED_PROTO'] == 'https')) ? 'https://' : 'http://';
        return $http_type . $_SERVER['HTTP_HOST'];
    }

    /**
     * 时间戳 - 精确到毫秒
     * @return float
     */
    public static function getMillisecond() {
        list($t1, $t2) = explode(' ', microtime());
        return (float)sprintf('%.0f',(floatval($t1)+floatval($t2))*1000);
    }



    /**
     * 判断文件是否存在，支持本地及远程文件
     * @param  String  $file 文件路径
     * @return Boolean
     */
    public static function check_file_exists($file){
// 屏蔽域名不存在等访问问题的警告
        error_reporting(E_ALL ^ (E_WARNING|E_NOTICE));
        // 远程文件
        if(strtolower(substr($file, 0, 4))=='http'){

            $header = get_headers($file, true);

            return isset($header[0]) && (strpos($header[0], '200') || strpos($header[0], '304'));

            // 本地文件
        }else{
            return file_exists($file);
        }

    }

    /**将目录下的文件保存到框架文件系统
     * @param $file_path 原文件全路径（物理=>[绝对|相对]）
     * @param $file_name 源文件名
     * @return \app\common\model\attachment|\think\Model
     * @throws \app\common\exception\UploadException
     */
    public static function setFastAdminFile($file_path,$file_name){
        //保存到第三方文件
        //name 原文件名 type 文件类型  tmp_name 原目录 error =0 size 文件大小
        $fi = new \finfo(FILEINFO_MIME_TYPE);
        $mime_type = $fi->file($file_path);
        $temp = [
            'name'=> $file_name,
            'size'=> filesize($file_path),
            'tmp_name'=>$file_path,
            'error'=>0,
            'type'=>$mime_type
        ];
        $file = (new File($file_path))->isTest(true)->setUploadInfo($temp);

        $category = $_POST["category"] ?? 'code';
        $_POST["category"] = 'code';
        $upload = new Upload($file,$category);
        $res = $upload->upload();
        $_POST["category"] = $category;
        return $res;
    }

    public $temp_url = 'uploads/qrcode';//临时，目录

    public $path = '';

    /**将目录下的文件保存到框架文件系统
     * @param $file_path 原文件全路径（物理=>[绝对|相对]）
     * @param $file_name 源文件名
     * @return \app\common\model\attachment|\think\Model
     * @throws \app\common\exception\UploadException
     */
    public  function setFastAdminFileByUrl($url,$fileName){
        ob_start();
        readfile($url);
        $res = ob_get_contents();//文件二进制流
        ob_end_clean();
        $outfile = $this->temp_url . '/';  //本地缓存地址
        if (!file_exists('./' . $outfile . $this->path)) mkdir('./' . $outfile . $this->path, 0777, true);
        $filepath = './' . $outfile . $this->path . '/' . $fileName;

        file_put_contents($filepath, $res);
        //保存到fastadmin框架
        $attachment = Common::setFastAdminFile($filepath, $fileName);
        // TODO: 生成后删除源文件
        @unlink($filepath);
        return $attachment;
    }




    /**
     * 字符串命名风格转换
     * type 0 将Java风格转换为C的风格 1 将C风格转换为Java的风格
     * @access public
     * @param string $name 字符串
     * @param integer $type 转换类型
     * @param bool $ucfirst 首字母是否大写（驼峰规则）
     * @return string
     */
    public static function parseName($name, $type = 0, $ucfirst = true)
    {
        if ($type) {
            $name = preg_replace_callback('/_([a-zA-Z])/', function ($match) {
                return strtoupper($match[1]);
            }, $name);
            return $ucfirst ? ucfirst($name) : lcfirst($name);
        }

        return strtolower(trim(preg_replace("/[A-Z]/", "_\\0", $name), "_"));
    }


    public static function toreplace($str, $find)//$str是你需要操作的字符串,$find是你指定的字符串
    {
        if (strpos($str, $find) === false) return false;

        $a = explode($find, $str);
        return $a[0] . $find;
    }


    /**
     * 无限级归类
     *
     * @param array $list 归类的数组
     * @param string $id 父级ID
     * @param string $pid 父级PID
     * @param string $child key
     * @param string $root 顶级
     *
     * @return array
     */
    public static function tree(array $list, string $pk = 'id', string $pid = 'pid', string $child = 'child', int $root = 0): array
    {
        $tree = [];  //最终得到的树形数据

        if (is_array($list)) {
            $refer = [];

            //基于数组的指针(引用) 并 同步改变数组
            foreach ($list as $key => $val) {
                $list[$key][$child] = [];
                $refer[$val[$pk]] = &$list[$key];  //以主键为下标，值为列表数据的引用
            }


            foreach ($list as $key => $val) {
                //是否存在parent
                $parentId = isset($val[$pid]) ? $val[$pid] : $root;  //取出父级id

                //如果是根节点，直接放入根层级(实际放入的是一个组装好的树分支)
                if ($root == $parentId) {
                    $tree[$val[$pk]] = &$list[$key];
                } else {  //如果是其他节点，通过引用传入树分支


                    if (isset($refer[$parentId])) {

                        $refer[$parentId][$child][] = &$list[$key];  //1  3  4

                    }
                }

            }
            // die;
        }
        //var_dump(array_values($tree));
        return array_values($tree);
    }


    /**得到关系树中的全部用户id（递归）
     * @param array $list tree数据来源
     * @param string $name 需要获取的数据名
     * @param string $child 树子节点名
     * @return array
     */
    public static function getTreeItem(array $list, string $name = 'pid', string $child = 'child')
    {
        $item = [];
        //遍历关系
        foreach ($list as &$value) {
            $item[] = $value[$name];
            if ($value[$child]) $item = array_merge($item, self::getTreeItem($value[$child], $name, $child));//合并两个数组
        }
        return $item;
    }


    /**
     * 排列组合
     *
     * @param array $input 排列的数组
     *
     * @return array
     */
    static public function arrayArrange(array $input): array
    {
        $temp = [];
        $result = array_shift($input);  //挤出数组第一个元素
        while ($item = array_shift($input))  //循环每次挤出数组的一个元素，直到数组元素全部挤出
        {
            $temp = $result;
            $result = [];
            foreach ($temp as $v) {
                foreach ($item as $val) {
                    $result[] = array_merge_recursive($v, $val);

                }
            }
        }
        return $result;
    }

    /**
     * 富文本base64解码
     */
    public static function r_text_decode($text)
    {
        $text = base64_decode($text);
        return htmlspecialchars_decode(urldecode($text));
    }

    /**得到小程序太阳码
     * @param $path
     * @param string $scene
     * @param bool $cache
     * @return attachment|array|false|\PDOStatement|string|\think\Model
     * @throws \EasyWeChat\Kernel\Exceptions\InvalidArgumentException
     * @throws \EasyWeChat\Kernel\Exceptions\RuntimeException
     * @throws \app\common\exception\UploadException
     * @throws \think\db\exception\DataNotFoundException
     * @throws \think\db\exception\ModelNotFoundException
     * @throws \think\exception\DbException
     */
    public static function getMiniappCode($scene, $page, $width = 430, $auto_color = false, $line_color = ["r" => "0", "g" => "0", "b" => "0"], $is_hyaline = true, $outType = null, $check_path = true, $env_version = 'release',$cache = true){
        // 写入到文件
        $file_name = md5( $page.$scene.'MiniCode') . '.png';
        if($cache){
            $file_info = Attachment::where('filename',$file_name)->find();
            if($file_info){
                $file_info['full_url'] = cdnurl($file_info['url'],true);
                return $file_info;
            }
        }
        if (empty($page)) {
            $page = 'pages/index/index';
        }
//        $wechat = new \addons\shopro\library\Wechat('wxMiniProgram');
//        $content = $wechat->getApp()->app_code->getUnlimit($scene, [
//            'page' => $path,
//            'is_hyaline' => true,
//        ]);

                    // 实例对应的接口对象
          $scheme = new \WeMini\Qrcode(Service::wechatConfig());
          $content =  $scheme->createMiniScene($scene, $page, $width, $auto_color, $line_color ? ( is_array($line_color) ? $line_color : json_decode($line_color, true))  : ["r" => "0", "g" => "0", "b" => "0"], $is_hyaline, null, $check_path, $env_version);



        if ($content instanceof \EasyWeChat\Kernel\Http\StreamResponse || is_string($content)) {
            $filePath = ROOT_PATH . 'public/uploads/qrcode/' . $file_name;
            if(is_string($content)){
                file_put_contents(ROOT_PATH . 'public/uploads/qrcode/'.$file_name, $content);
            }else{
                $content->saveAs(ROOT_PATH . 'public/uploads/qrcode', $file_name);
            }

            //保存到fastadmin框架
            $attachment = Common::setFastAdminFile($filePath, $file_name);
            $attachment['full_url'] = cdnurl($attachment['url'],true);
            // TODO: 生成后删除源文件
            @unlink($filePath);
            return $attachment;

        } else {
            var_dump(gettype($content));
            // 小程序码获取失败
            $msg = isset($content['errcode']) ? $content['errcode'] : '-';
            $msg .= isset($content['errmsg']) ? $content['errmsg'] : '';
            \think\Log::write('wxacode-error' . $msg);
            throw new \Exception($msg);
        }
    }


    /**得到小程序二维码(调用次数上限为10万)
     * @param $path
     * @param bool $cache
     * @return attachment|array|false|\PDOStatement|string|\think\Model
     * @throws \EasyWeChat\Kernel\Exceptions\InvalidArgumentException
     * @throws \EasyWeChat\Kernel\Exceptions\RuntimeException
     * @throws \app\common\exception\UploadException
     * @throws \think\db\exception\DataNotFoundException
     * @throws \think\db\exception\ModelNotFoundException
     * @throws \think\exception\DbException
     */
    public static function getMiniappQrCode($path,$getContent=false,$cache = true){
        // 写入到文件
        $file_name = md5( $path.'MiniQrCode') . '.png';
        if($cache){
            $file_info = Attachment::where('filename',$file_name)->find();
            if($file_info){
                $file_info['full_url'] = cdnurl($file_info['url'],true);
                $file_info['common_content'] = null;
                if($getContent){
                    //得到返解析的内容
                    $qrcode = new \Zxing\QrReader( $file_info['full_url']);   //绝对路径
                    $file_info['common_content'] = $qrcode->text(); //返回二维码的内容
                }
                return $file_info;
            }
        }
        if (empty($path)) {
            $path = 'pages/index/index';
        }
        $wechat = new \addons\shopro\library\Wechat('wxMiniProgram');
        $content = $wechat->getApp()->app_code->getQrCode($path);
        if ($content instanceof \EasyWeChat\Kernel\Http\StreamResponse) {
            $filePath = ROOT_PATH . 'public/uploads/qrcode/' . $file_name;
            $content->saveAs(ROOT_PATH . 'public/uploads/qrcode', $file_name);
            //保存到fastadmin框架
            $attachment = Common::setFastAdminFile($filePath, $file_name);
            $attachment['full_url'] = cdnurl($attachment['url'],true);
            $attachment['common_content'] = null;
            if($getContent) {
                //得到返解析的内容
                $qrcode = new \Zxing\QrReader($attachment['full_url']);   //绝对路径
                $attachment['common_content'] = $qrcode->text(); //返回二维码的内容
            }
            // TODO: 生成后删除源文件
            @unlink($filePath);
            return $attachment;

        } else {
            // 小程序码获取失败
            $msg = isset($content['errcode']) ? $content['errcode'] : '-';
            $msg .= isset($content['errmsg']) ? $content['errmsg'] : '';
            \think\Log::write('wxacode-error' . $msg);
            throw new \Exception($msg);
        }
    }



    /**生成二维码
     * @param $params
     * @throws \Endroid\QrCode\Exception\InvalidPathException
     * @throws \app\common\exception\UploadException
     */
    public static function getQrcode($params,$stream = false,$cache = true,$response = false){
        if($response){
            return self::getQRcodeResponse($params);
        }
        $qrCode = \addons\qrcode\library\Service::qrcode($params);
        if($stream)return $qrCode;
        // 写入到文件
        $file_name = md5(implode('', $params).'Qrcode') . '.png';
        if($cache){
            $file_info = Attachment::where('filename',$file_name)->find();
            if($file_info){
                $file_info['full_url'] = cdnurl($file_info['url'],true);
                return $file_info;
            }
        }
        $filePath = ROOT_PATH . 'public/uploads/qrcode/' . $file_name;
        $qrCode->writeFile($filePath);
        //保存到fastadmin框架
        $attachment = Common::setFastAdminFile($filePath, $file_name);
        // TODO: 生成后删除源文件
        @unlink($filePath);
        $attachment['full_url'] = cdnurl($attachment['url'],true);
        return $attachment;
    }

    /**生成一维码
     * @param $params
     * @throws \Endroid\QrCode\Exception\InvalidPathException
     * @throws \app\common\exception\UploadException
     */
    public static function getBarcode($params,$stream = false,$cache = true){
        $qrCode = \addons\barcode\library\Service::barcode($params);
        if($stream)return $qrCode;
        // 写入到文件
        $file_name = md5(implode('', $params).'Barcode') . '.png';
        if($cache){
            $file_info = Attachment::where('filename',$file_name)->find();
            if($file_info){
                $file_info['full_url'] = cdnurl($file_info['url'],true);
                return $file_info;
            }
        }
        $outfile =  'uploads/barcode/';
        if (!file_exists('./' . $outfile )) mkdir('./' . $outfile, 0777, true);
        $filePath = './' . $outfile  . '/' . $file_name;
        file_put_contents($filePath, $qrCode);
        //保存到fastadmin框架
        $attachment = Common::setFastAdminFile($filePath, $file_name);
        // TODO: 生成后删除源文件
        @unlink($filePath);
        $attachment['full_url'] = cdnurl($attachment['url'],true);
        return $attachment;
    }


    /**执行字符串模板替换
     * @param $template
     * @param array $params
     * @param string $expression
     */
    public static function parsePrintTemplateString($template,$params=[],$expression = '{{KEYWORD}}'){
        foreach ($params as $name => $value)
        {
            //得到需要替换的字符串
            $replace_name = str_replace("KEYWORD",$name,$expression);
            //执行模板字符串替换
            $template = str_replace($replace_name,$value,$template);
        }
        return $template;
    }




    /**
     * 返回多层栏目
     * @param $data 操作的数组
     * @param int $pid 一级PID的值
     * @param string $html 栏目名称前缀
     * @param string $fieldPri 唯一键名，如果是表则是表的主键
     * @param string $fieldPid 父ID键名
     * @param int $level 不需要传参数（执行时调用）
     * @return array
     */
    static public function channelLevel($data, $pid = 0, $html = "&nbsp;", $fieldPri = 'cid', $fieldPid = 'pid', $level = 1)
    {
        if (empty($data)) {
            return array();
        }
        $arr = array();
        foreach ($data as $v) {
            if ($v[$fieldPid] == $pid) {
                $arr[$v[$fieldPri]] = $v;
                $arr[$v[$fieldPri]]['_level'] = $level;
                $arr[$v[$fieldPri]]['_html'] = str_repeat($html, $level - 1);
                $arr[$v[$fieldPri]]["_data"] = self::channelLevel($data, $v[$fieldPri], $html, $fieldPri, $fieldPid, $level + 1);
            }
        }
        return $arr;
    }

    /**
     * 获得所有子栏目
     * @param $data 栏目数据
     * @param int $pid 操作的栏目
     * @param string $html 栏目名前字符
     * @param string $fieldPri 表主键
     * @param string $fieldPid 父id
     * @param int $level 等级
     * @return array
     */
    static public function channelList($data, $pid = 0, $html = "&nbsp;", $fieldPri = 'cid', $fieldPid = 'pid', $level = 1)
    {
        $data = self::_channelList($data, $pid, $html, $fieldPri, $fieldPid, $level);
        if (empty($data))
            return $data;
        foreach ($data as $n => $m) {
            if ($m['_level'] == 1)
                continue;
            $data[$n]['_first'] = false;
            $data[$n]['_end'] = false;
            if (!isset($data[$n - 1]) || $data[$n - 1]['_level'] != $m['_level']) {
                $data[$n]['_first'] = true;
            }
            if (isset($data[$n + 1]) && $data[$n]['_level'] > $data[$n + 1]['_level']) {
                $data[$n]['_end'] = true;
            }
        }
        //更新key为栏目主键
        $category = array();
        foreach ($data as $d) {
            $category[$d[$fieldPri]] = $d;
        }
        return $category;
    }

    //只供channelList方法使用
    static private function _channelList($data, $pid = 0, $html = "&nbsp;", $fieldPri = 'cid', $fieldPid = 'pid', $level = 1)
    {
        if (empty($data))
            return array();
        $arr = array();
        foreach ($data as $v) {
            $id = $v[$fieldPri];
            if ($v[$fieldPid] == $pid) {
                $v['_level'] = $level;
                $v['_html'] = str_repeat($html, $level - 1);
                array_push($arr, $v);
                $tmp = self::_channelList($data, $id, $html, $fieldPri, $fieldPid, $level + 1);
                $arr = array_merge($arr, $tmp);
            }
        }
        return $arr;
    }

    /**
     * 获得展示状态的树 数据 例如  ├─ xxx
     * @param $data 数据
     * @param $title 字段名
     * @param string $fieldPri 主键id
     * @param string $fieldPid 父id
     * @return array
     */
    static public function exhibition_tree($data, $title, $fieldPri = 'cid', $fieldPid = 'pid')
    {
        if (!is_array($data) || empty($data))
            return array();
        $arr = self::channelList($data, 0, '', $fieldPri, $fieldPid);
        foreach ($arr as $k => $v) {
            $str = "";
            if ($v['_level'] > 2) {
                for ($i = 1; $i < $v['_level'] - 1; $i++) {
                    $str .= "&emsp;│";
                }
            }
            if ($v['_level'] != 1) {
                $t = $title ? $v[$title] : "";
                if (isset($arr[$k + 1]) && $arr[$k + 1]['_level'] >= $arr[$k]['_level']) {
                    $arr[$k]['_name'] = $str . "&emsp;├─ " . $v['_html'] . $t;
                } else {
                    $arr[$k]['_name'] = $str . "&emsp;└─ " . $v['_html'] . $t;
                }
            } else {
                $arr[$k]['_name'] = $v[$title];
            }
        }
        //设置主键为$fieldPri
        $data = array();
        foreach ($arr as $d) {
            $data[$d[$fieldPri]] = $d;
        }
        return $data;
    }

    /**
     * 获得所有父级栏目
     * @param $data 栏目数据
     * @param $sid 子栏目
     * @param string $fieldPri 唯一键名，如果是表则是表的主键
     * @param string $fieldPid 父ID键名
     * @return array
     */
    static public function parentChannel($data, $sid, $fieldPri = 'cid', $fieldPid = 'pid')
    {
        if (empty($data)) {
            return $data;
        } else {
            $arr = array();
            foreach ($data as $v) {
                if ($v[$fieldPri] == $sid) {
                    $arr[] = $v;
                    $_n = self::parentChannel($data, $v[$fieldPid], $fieldPri, $fieldPid);
                    if (!empty($_n)) {
                        $arr = array_merge($arr, $_n);
                    }
                }
            }
            return $arr;
        }
    }

    /**
     * 判断$s_cid是否是$d_cid的子栏目
     * @param $data 栏目数据
     * @param $sid 子栏目id
     * @param $pid 父栏目id
     * @param string $fieldPri 主键
     * @param string $fieldPid 父id字段
     * @return bool
     */
    static function isChild($data, $sid, $pid, $fieldPri = 'cid', $fieldPid = 'pid')
    {
        $_data = self::channelList($data, $pid, '', $fieldPri, $fieldPid);
        foreach ($_data as $c) {
            //目标栏目为源栏目的子栏目
            if ($c[$fieldPri] == $sid)
                return true;
        }
        return false;
    }

    /**
     * 检测是不否有子栏目
     * @param $data 栏目数据
     * @param $cid 要判断的栏目cid
     * @param string $fieldPid 父id表字段名
     * @return bool
     */
    static function hasChild($data, $cid, $fieldPid = 'pid')
    {
        foreach ($data as $d) {
            if ($d[$fieldPid] == $cid) return true;
        }
        return false;
    }

    /**
     * 递归实现迪卡尔乘积
     * @param $arr 操作的数组
     * @param array $tmp
     * @return array
     */
    static function descarte($arr, $tmp = array())
    {
        static $n_arr = array();
        foreach (array_shift($arr) as $v) {
            $tmp[] = $v;
            if ($arr) {
                self::descarte($arr, $tmp);
            } else {
                $n_arr[] = $tmp;
            }
            array_pop($tmp);
        }
        return $n_arr;
    }


    /**得到可复用的小程序二维码
     * @param $link_id
     * @param bool $cache
     * @return Miniqrcode|array|false|\PDOStatement|string|\think\Model
     * @throws \Exception
     */
    public static function getLinkMiniQrCode($link_id,$mini_path=null,$type = 'mock',$cache = true){
        try{
         $miniqrcode =  Miniqrcode::where("link_id",$link_id)->where("type",$type)->where("value","not null")->find();
         if($miniqrcode && $miniqrcode['value']){
             $miniqrcode['status'] = '2';
             $miniqrcode->save();
             return $miniqrcode;
         }
        //判断是否有空余小程序二维码
         $miniqrcode  =  Miniqrcode::where("status","1")->where("value","not null")->where("value","<>","")->find();

        //如果有，则更新二维码为使用中，保存新的连接信息并返回
        if($miniqrcode  && $miniqrcode['value']){
            $miniqrcode['status'] = '2';
            $miniqrcode['link_id'] = $link_id;
            $miniqrcode['type'] = $type;
            $miniqrcode->save();
            return Miniqrcode::where('id',$miniqrcode['id'])->find();
        }else{
        //如果没有，则
            $count = Miniqrcode::count();
            $miniqrcode_limit = config("site.miniqrcode_limit");//练车小程序码url
            $mock_minicode_url = $mini_path ?: config("site.mock_minicode_url");//练车码生成上限数量
            //1,判断是否超出使用数量，如果超出则抛异常
            if($count >= $miniqrcode_limit)throw new \Exception("已超出能生成的小程序二维码上限！");
           //如果未超出
               //1插入小程序二维码表
            $miniqrcode     = new Miniqrcode;
            $miniqrcode->link_id     = $link_id;
            $miniqrcode['type'] = $type;
            $miniqrcode['status'] = '2';
            $miniqrcode->save();
             // 获取自增ID
            $miniqrcode_id = $miniqrcode->id;
            //拼接二维码内容
            $path = "{$mock_minicode_url}?link_id={$miniqrcode_id}";
               //2生成小程序二维码，并逆解析内容:找到能解析出内容的二维码
            while(true)
            {
                $attachment = Common::getMiniappQrCode($path,true,$cache);
                $image = $attachment['url'];
                $value = $attachment['common_content'];
                //解析不出二维码，存入码表,循环下一张
                if(!$value){
                    //原码更新成失败码
                    $error_miniqrcode  =  Miniqrcode::where("id",$miniqrcode_id)->find();
                    $error_miniqrcode['link_id']     = 0;
                    $error_miniqrcode['status'] = '1';
                    $error_miniqrcode['type'] = 'other';
                    $error_miniqrcode['path'] = $path;
                    $error_miniqrcode['image'] = $image;
                    $error_miniqrcode['value'] = $value;
                    $error_miniqrcode->save();

                    //1插入新的小程序二维码
                    $new_miniqrcode     = new Miniqrcode;
                    $new_miniqrcode->link_id     = $link_id;
                    $new_miniqrcode['type'] = $type;
                    $new_miniqrcode['status'] = '2';
                    $new_miniqrcode->save();

                    //赋值新自增ID
                    $miniqrcode_id = $new_miniqrcode->id;
                    //拼接新二维码内容
                    $path = "{$mock_minicode_url}?link_id={$miniqrcode_id}";
                    //开始新一轮循环，直到获取到能解析的二维码为止

                }else{
                //解析得出二维码直接返回
                    //3返回表信息
                    $miniqrcode  =  Miniqrcode::where("id",$miniqrcode_id)->find();
                    $miniqrcode['path'] = $path;
                    $miniqrcode['image'] = $image;
                    $miniqrcode['value'] = $value;
                    $miniqrcode->save();
                    break;
                }
            }

            return $miniqrcode;
        }

        }catch (\Throwable $e){
            throw new \Exception($e->getMessage());
        }

    }

    /** 释放被占用的小程序二维码
     * @param $link_id
     * @param string $type
     * @return bool
     * @throws \think\db\exception\DataNotFoundException
     * @throws \think\db\exception\ModelNotFoundException
     * @throws \think\exception\DbException
     */
    public static function freeLinkMiniQrCode($link_id,$type = 'mock'){
        $miniqrcode =  Miniqrcode::where("link_id",$link_id)->where("type",$type)->find();
        if(!$miniqrcode)return true;
        $miniqrcode['status']  = '1';
        $miniqrcode['link_id']  = 0;
        $miniqrcode['type']  = 'other';
        $miniqrcode->save();
        return true;
    }




    /**射线与边是否有交点
     * @param $poi
     * @param $sPoi
     * @param $ePoi
     */
    public static function isRayIntersectsSegment($poi, $sPoi, $ePoi){


    // 排除 与射线平行、重合、是一个点的情况
    if($sPoi[0] == $ePoi[0])return false;
    //# 排除 线段在射线上边
    if($sPoi[0] > $poi[0] && $ePoi[0] > $poi[0])return false;
    // # 排除 线段在射线下边
    if($sPoi[0] < $poi[0] && $ePoi[0] < $poi[0])return false;
    // # 排除 交点为下端点
    if($sPoi[0] == $poi[0] && $ePoi[0] > $poi[0])return false;
    // # 排除 交点为上端点
    if($ePoi[0] == $poi[0] && $sPoi[0] > $poi[0])return false;
    //  # 排除 线段在射线左边
    if($sPoi[1] < $poi[1] && $ePoi[1] < $poi[1])return false;
    // # 求交，相似多边形性质
        //规避除零异常
        $meddle_v = ($sPoi[0] - $ePoi[0]) * ($sPoi[1] - $ePoi[1]);
        if($meddle_v == 1)return false;
       $xseg = $ePoi[1] + ($poi[0] - $ePoi[0]) / ($sPoi[0] - $ePoi[0]) * ($sPoi[1] - $ePoi[1]);
    //  # 交点在射线起点的左侧
    if($xseg < $poi[1])return false;
    // #  排除上述情况之后真正有效的点
    return true;
    }


    /**地理围栏算法实现-单围栏（射线法：多变遍历）
     * @param $poi
     * @param $poly
     * @return bool
     */
    public static function poiInPoly($poi, $poly,$count_check=true){
       // # 交点个数
        $count = 0 ;
       // # 逐个二维数组进行判断
        $last_index = count($poly)-1; //最后一个数组下标
        foreach ($poly as $index=>$epoly)
        {
            //最后一个节点
           if($last_index == $index){
               $e_poi = $poly[$index];
               $s_poi = $poly[0];
           }else{
               //正常节点
               $e_poi = $poly[$index];
               $s_poi = $poly[$index + 1];
           }

            //刚好压在点上
            if($poi[0] == $e_poi[0] && $poi[1] == $e_poi[1])return true;
            if($poi[0] == $s_poi[0] && $poi[1] == $s_poi[1])return true;

            if(self::isRayIntersectsSegment($poi, $e_poi, $s_poi))$count += 1;
        }
        if($count_check)return $count % 2 != 0;
        return $count;
    }


    /**地理围栏算法实现-多围栏（射线法：多变遍历）
     * @param $poi
     * @param $polys
     */
    public static function poiInPolyArray($poi, $polys){
        // # 交点个数
        //记录下包含围栏数
        $contain = [];
        foreach ($polys as $poly)
        {
            if(is_string($poly))$poly = json_decode($poly, true);
            if(self::poiInPoly($poi, $poly))$contain[] = $poly;
        }
        return $contain;
    }


    /**
     * 得到时间标识（当天6点前算当天）
     */
    public static function getTimeflag($change=false,$tomorrow_s=""){
        //得到当前时间
        $time = time();
        if($tomorrow_s){
            $tomorrow_string = $tomorrow_s;
        }else{
            $tomorrow_string = config("site.queue_tomorrow_string") ?: "6:0:0";
        }

        //得到当前六点的时间
        $format_six_time = date("Y-m-d {$tomorrow_string}",$time);
        //得到当前六点的时间戳
        $time_six = strtotime($format_six_time);
        //如果小于六点取前一天的六点
        if($time < $time_six){
            $time_six = strtotime("{$format_six_time} -1 day");
        } //如果大于六点取今天的六点的时间戳
        //如果是取范围则返回当天时间范围
        if($change){
            $format_time_end = date("Y-m-d H:i:s",$time_six);
            $time_end = strtotime("{$format_time_end} +1 day") - 1; //59分秒时
            return ['time_start'=>$time_six,'time_end'=>$time_end];
        }
        //不是取范围则直接返回
        return $time_six;
    }


    /**
     * 下划线转驼峰
     * 思路:
     * step1.原字符串转小写,原字符串中的分隔符用空格替换,在字符串开头加上分隔符
     * step2.将字符串中每个单词的首字母转换为大写,再去空格,去字符串首部附加的分隔符.
     */
    public static function  camelize($uncamelized_words,$separator='_')
    {
        $uncamelized_words = $separator. str_replace($separator, " ", strtolower($uncamelized_words));
        return ltrim(str_replace(" ", "", ucwords($uncamelized_words)), $separator );
    }

    /**
     * 驼峰命名转下划线命名
     * 思路:
     * 小写和大写紧挨一起的地方,加上分隔符,然后全部转小写
     */
    public static function uncamelize($camelCaps,$separator='_')
    {
        return strtolower(preg_replace('/([a-z])([A-Z])/', "$1" . $separator . "$2", $camelCaps));
    }

    /**讯飞语音合成
     * @param $params
     * @return array
     */
    public static function xftts($params)
    {
        if (!isset($params['text']) || empty($params['text'])) {
            throw new\Exception('请输入合成文字');
        }
        $config = get_addon_config('xftts');

        $tts = new Tts();
        $url = $tts->createAuthUrl($config['APIKey'], $config['APISecret'], time());
        $client = new Client($url);
        $message = $tts->createMsg(
            $config['APPID'],
            $params['text']."，，，，，，，，，，，，，，，，，，，，，，，，，，",
            isset($params['ent']) ? $params['ent'] : $config['ent'],
            isset($params['aue']) ? $params['aue'] : $config['aue'],
            isset($params['auf']) ? $params['auf'] : $config['auf'],
            isset($params['vcn']) ? $params['vcn'] : $config['vcn'],
            isset($params['speed']) ? $params['speed'] : $config['speed'],
            isset($params['volume']) ? $params['volume'] : $config['volume'],
            isset($params['pitch']) ? $params['pitch'] : $config['pitch'],
            isset($params['bgs']) ? $params['bgs'] : $config['bgs'],
            isset($params['tte']) ? $params['tte'] : $config['tte'],
            isset($params['reg']) ? $params['reg'] : $config['reg'],
            isset($params['ram']) ? $params['ram'] : $config['ram'],
            isset($params['rdn']) ? $params['rdn'] : $config['rdn']
        );
        $params = array_merge($config,$params);
        try {
            $client->send(json_encode($message, true));
            $date = date('YmdHis', time());
            $file_name = $date . ($params['aue'] === 'raw' ? '.pcm' : '.mp3');
            $folder = '/uploads/tts/';
            //判断文件夹是否存在
            if (!is_dir(ROOT_PATH . 'public' . $folder)) {
                @mkdir(ROOT_PATH . 'public' . $folder);
            }
            $audio_file = fopen(ROOT_PATH . 'public' . $folder . $file_name, 'ab');
            $response = $client->receive();
            $response = json_decode($response, true);
            do {
                if ($response['code']) {
                    throw new \Exception($response['msg']);
                }
                //返回的音频需要进行base64解码
                $audio = base64_decode($response['data']['audio']);
                fwrite($audio_file, $audio);
                // 第一次消息就收到结束标志的情况
                if ($response['data']['status'] == 2) {
                    fclose($audio_file);
                    $client->close();
                    $url = $folder . $file_name;
                    $filesize = filesize(ROOT_PATH . 'public' . $url);
                    //保存语音数据
                    $params['speech_url'] = $url;
                    $params['filesize'] = $filesize;
                    $params['admin_id'] = 1;
                    $result = (new Xftts())->allowField(true)->save($params);


                    return [
                        'code' => 0,
                        'msg'  => '合成成功',
                        'data' => [
                            'url'      => $url,
                            'full_url'      => cdnurl($url,true),
                            'filesize' => $filesize,
                            'object'      => Xftts::where('speech_url',$url)->find(),
                        ]
                    ];
                }
                //继续接收消息
                $response = $client->receive();
                $response = json_decode($response, true);
            } while ($response['data']['status'] != 2);
            fclose($audio_file);

            $url = $folder . $file_name;
            $filesize = filesize(ROOT_PATH . 'public' . $url);
            //保存语音数据
            $params['speech_url'] = $url;
            $params['filesize'] = $filesize;
            $params['admin_id'] = 1;
            $result = (new Xftts())->allowField(true)->save($params);

            return [
                'code' => 0,
                'msg'  => '合成成功',
                'data' => [
                    'url'      => $url,
                    'full_url'      => cdnurl($url,true),
                    'filesize' => $filesize,
                    'object'      => Xftts::where('speech_url',$url)->find(),
                ]
            ];
        } catch (Exception $e) {
            throw new \Exception($e->getMessage(),1);
//            return [
//                'code' => 1,
//                'msg'  => $e->getMessage(),
//            ];
        } finally {
            $client->close();
        }
    }

    /**得到音频信息  composer require james-heinrich/getid3
     * @param $localRelativePath
     * @return array
     * @throws \getid3_exception
     */
    public static function getAudioInfo($fullPath){

        $remotefilename = $fullPath;
        if ($fp_remote = fopen($remotefilename, 'rb')) {
            $localtempfilename = tempnam('/tmp', 'getID3');
            if ($fp_local = fopen($localtempfilename, 'wb')) {
                while ($buffer = fread($fp_remote, 8192)) {
                    fwrite($fp_local, $buffer);
                }
                fclose($fp_local);
                // 初始化getID3引擎
                $getID3 = new \getID3;
                $ThisFileInfo = $getID3->analyze($localtempfilename);
                // 删除临时文件
                unlink($localtempfilename);
            }
            fclose($fp_remote);
        }
     return $ThisFileInfo;
    }



    /**
     * 得到语音播报内容（讯飞语音）
     */
    public static function getAudioContent($template_content,$params=[],$expression = '{{KEYWORD}}'){

        //解析获取文本内容
        $content = self::parsePrintTemplateString($template_content,$params,$expression);
        //判断是否存在当前文件
        $xftts = Xftts::where("text",$content)->find();
        //如果存在，则继续往下走
        if(!$xftts){
            //如果不存在，则生成文件继续往下走
            $audio_info = self::xftts(['text'=> $content]);
            $xftts = $audio_info['data']['object'];
        }
        $tts_key = "xftts_{$content}";
        //判断是否存在当前播放秒数
        $content_seconds = Cache::get($tts_key);
        if(!$content_seconds){
            //不存在当前秒数则解析当前秒数并存入缓存
            $audio_info = self::getAudioInfo(cdnurl($xftts['speech_url'],true));
//            var_dump($audio_info['playtime_seconds']);die;
            Cache::set($tts_key,$audio_info['playtime_seconds'],3600);
            $content_seconds = $audio_info['playtime_seconds'];
        }
        $xftts['content_seconds'] = $content_seconds;

        return $xftts;
    }

    /**    生成并直接返回二维码http响应而不存储
     * @param $text
     * @param $label
     * @param $params
     * @return Response|\think\response\Json|\think\response\Jsonp|\think\response\Redirect|\think\response\View|\think\response\Xml
     * @throws \Endroid\QrCode\Exception\InvalidPathException
     */
    public static function getQRcodeResponse($params=[]){

        $params = array_intersect_key($params, array_flip(['text', 'size', 'padding', 'errorlevel', 'foreground', 'background', 'logo', 'logosize', 'logopath', 'label', 'labelfontsize', 'labelalignment']));

//        $params['text'] = $text;
//        $params['label'] = $label;

        $qrCode = \addons\qrcode\library\Service::qrcode($params);

        $mimetype = 'image/png';

        $response = Response::create()->header("Content-Type", $mimetype);

        // 直接显示二维码
        header('Content-Type: ' . $qrCode->getContentType());

        // 设置缓存过期时间（例如，30天）
        $expiresTime = strtotime('+30 days');
        header('Expires: ' . gmdate('D, d M Y H:i:s', $expiresTime) . ' GMT');
        header('Cache-Control: max-age=' . (30 * 24 * 60 * 60));


        $response->content($qrCode->writeString());
        return $response;
    }


}