常用自定义函数laravel版+thinkphp版

推荐使用php8.2+版本,php8.2以下版本不支持Random\RandomException,需修改随机字符串生成方法

【包含功能】

  • 格式化数字/浮点数
  • 字符串处理(截取、过滤、修复HTML)
  • 数组处理(树形结构、去重、转换)
  • 文件操作(删除、安全检测、图片处理)
  • 随机字符串生成
  • 隐私数据处理
  • JSON输出封装
  • 时间/距离计算
  • 图片处理(裁剪、转base64)

    laravel安装类库

    composer require intervention/image

    thinkphp安装类库

    composer require topthink/think-image

    使用前定义全局常量

    <?php
    defined('UPLOAD_PATH') or define('UPLOAD_PATH', '***');
    defined('UPLOAD_IMAGE_PATH') or define('UPLOAD_IMAGE_PATH', '***');
    defined('UPLOAD_VIDEO_PATH') or define('UPLOAD_VIDEO_PATH', '***');
    defined('UPLOAD_FILE_PATH') or define('UPLOAD_FILE_PATH', '***');
    defined('UPLOAD_RICH_PATH') or define('UPLOAD_RICH_PATH', '***');
    defined('UPLOAD_TEMP_PATH') or define('UPLOAD_TEMP_PATH', '***');

    1、laravel版(app\Helpers\helpers.php)

    <?php
    
    use Intervention\Image\ImageManager;
    use Intervention\Image\Drivers\Gd\Driver;
    use Random\RandomException;
    
    /*
     |------------------------------------------------------------------------------------
     | 格式化数字
     |------------------------------------------------------------------------------------
     | @param  mixed $number 待格式化数字,支持int、float、string类型
     | @param  int   $length 数字长度
     | @return int           格式化后的数值
     |------------------------------------------------------------------------------------
     */
    if (!function_exists('fnumber')) {
        function fnumber(mixed $number, int $length = 10, int $default = 0): int
        {
            $type = gettype($number);
            if ('boolean' === $type) {
                return $number ? 1 : 0;
            } elseif ('double' === $type) {
                $intValue = (int)$number;
                $strValue = (string)abs($intValue);
                return ($length >= strlen($strValue)) ? $intValue : $default;
            } elseif ('string' === $type) {
                if (preg_match('/^([-+]?)(\d+)(\.\d+)?$/', $number, $matches)) {
                    $symbol = $matches[1];
                    $value = $matches[2];
                    if ($length >= strlen($value)) {
                        return (int)($symbol . $value);
                    }
                }
                return $default;
            } elseif ('integer' === $type) {
                $strValue = (string)abs($number);
                return ($length >= strlen($strValue)) ? (int)$number : $default;
            } else {
                return $default;
            }
        }
    }
    /*
     |------------------------------------------------------------------------------------
     | 格式化浮点数
     |------------------------------------------------------------------------------------
     | @param  mixed        $number  待格式化数字,支持int、float、string类型
     | @param  int          $length  数字长度
     | @param  boolean      $numeric 是否输出数值类型
     | @param  boolean      $symbol  是否允许负数
     | @return float|string          格式化后的数值
     |------------------------------------------------------------------------------------
     */
    if (!function_exists('formatFloat')) {
        function formatFloat(mixed $number, int $length = 2, bool $numeric = true, bool $symbol = false): float|string
        {
            if (is_bool($number)) {
                $number = $number ? 1 : 0;
            } elseif (is_numeric($number) || is_string($number)) {
                $strValue = (string)$number;
                $pattern = $symbol ? '/^([-+])?(\d+)(\.\d+)?$/' : '/^(\d+)(\.\d+)?$/';
                if (preg_match($pattern, $strValue, $matches)) {
                    $number = $matches[0];
                } else {
                    $number = 0;
                }
            } else {
                $number = 0;
            }
            $formatted = number_format((float)$number, $length, '.', '');
            return $numeric ? (float)$formatted : $formatted;
        }
    }
    /*
     |------------------------------------------------------------------------------------
     | 格式化数字
     |------------------------------------------------------------------------------------
     | @param  int   $number 待格式化数字
     | @param  int   $suffix 后辍
     | @return mixed         格式化后的数字,int、string类型
     |------------------------------------------------------------------------------------
     */
    if (!function_exists('formatNumber')) {
        function formatNumber(int $number, string $suffix = ''): string|int
        {
            $number = fnumber($number);
            if ($number > 10000) {
                return intval($number / 10000) . '万' . $suffix;
            } elseif ($number > 1000) {
                return intval($number / 1000) . '千' . $suffix;
            } else {
                return $number;
            }
        }
    }
    /*
     |------------------------------------------------------------------------------------
     | 过滤html、script、css标签
     |------------------------------------------------------------------------------------
     | @param mixed   $str  待过滤字符串,支持int、float、string类型
     | @param int     $mode 过滤模式:0-过滤全部; 1-过滤script+css;2-保留基本标签
     | @return string       过滤后的字符串
     |------------------------------------------------------------------------------------
     */
    
    if (!function_exists('filterTags')) {
        function filterTags(mixed $str, int $mode = 0): string
        {
            if (is_string($str)) {
                $str = trim($str);
            } elseif (is_int($str) || is_float($str)) {
                $str = (string)$str;
            } else {
                return '';
            }
            if (isEmpty($str)) {
                return '';
            }
            if (!mb_check_encoding($str, 'UTF-8')) {
                $str = mb_convert_encoding($str, 'UTF-8', 'auto');
            }
            $str = htmlspecialchars_decode($str, ENT_QUOTES | ENT_HTML5);
            $str = preg_replace([
                '@<script[^>]*?>.*?</script>@si',
                '@<iframe[^>]*?>.*?</iframe>@si'
            ], '', $str);
            switch ($mode) {
                case 0:
                    $str = preg_replace('@<style[^>]*?>.*?</style>@si', '', $str);
                    $str = strip_tags($str);
                    break;
                case 1:
                    $str = preg_replace('@<style[^>]*?>.*?</style>@si', '', $str);
                    $str = fixHtml($str);
                    break;
                case 2:
                    $str = preg_replace('@<style[^>]*?>.*?</style>@si', '', $str);
                    $allowedTags = '<p><br><img><span><div><strong><em><b><i><u><ul><ol><li><h1><h2><h3><h4><h5><h6>';
                    $str = strip_tags($str, $allowedTags);
                    break;
                default:
                    $str = fixHtml($str);
                    break;
            }
            return trim($str);
        }
    }
    /*
     |------------------------------------------------------------------------------------
     | 截取字符串(支持中英文混合)
     |------------------------------------------------------------------------------------
     | @param  string $str  待截取字符串
     | @param  int    $len  截取长度(汉字算2个长度,英文、数字、符号算1个字符)
     | @param  string $tail 缀尾符
     | @return string       截取的字符串
     |------------------------------------------------------------------------------------
     */
    if (!function_exists('cutString')) {
        function cutString(string $str, int $len = 30, string $tail = '...'): string
        {
            $str = absTrim(filterTags($str));
            if (isEmpty($str) || $len <= 0) {
                return '';
            }
            $result = '';
            $count = 0;
            $slen = mb_strlen($str, 'UTF-8');
            for ($i = 0; $i < $slen; $i++) {
                $char = mb_substr($str, $i, 1, 'UTF-8');
                if (preg_match('/[\x{4e00}-\x{9fa5}]/u', $char)) {
                    $charSize = 2;
                } else {
                    $charSize = 1;
                }
                if ($count + $charSize > $len) {
                    break;
                }
                $result .= $char;
                $count += $charSize;
            }
            if ($slen > mb_strlen($result, 'UTF-8')) {
                $result .= $tail;
            }
            return $result;
        }
    }
    /*
     |------------------------------------------------------------------------------------
     | 修复html代码
     |------------------------------------------------------------------------------------
     | @param  string $html 待修复的HTML代码
     | @return string       修复的html代码
     |------------------------------------------------------------------------------------
     */
    if (!function_exists('fixHtml')) {
        function fixHtml(string $html): string
        {
            if (isEmpty($html)) {
                return '';
            }
            // 自闭合标签列表
            $selfClosingTags = ['meta', 'img', 'br', 'link', 'area', 'input', 'hr', 'col'];
            // 匹配所有开始标签
            preg_match_all('#<([a-z1-6]+)(?:\s+[^>]*)?>#i', $html, $startMatches);
            $startTags = $startMatches[1] ?? [];
            // 匹配所有结束标签
            preg_match_all('#</([a-z1-6]+)>#i', $html, $endMatches);
            $endTags = $endMatches[1] ?? [];
            // 标签栈
            $stack = [];
            // 找出需要关闭的标签
            foreach ($startTags as $tag) {
                if (!in_array(strtolower($tag), $selfClosingTags)) {
                    $stack[] = $tag;
                }
            }
            // 与结束标签匹配
            foreach ($endTags as $tag) {
                $pos = array_search($tag, $stack);
                if ($pos !== false) {
                    array_splice($stack, $pos, 1);
                }
            }
            // 剩余在栈中的标签需要关闭
            while (!isEmpty($stack)) {
                $tag = array_pop($stack);
                $html .= "</$tag>";
            }
            return $html;
        }
    }
    /*
     |------------------------------------------------------------------------------------
     | 过滤空格、换行符
     |------------------------------------------------------------------------------------
     | @param  string $str 待过滤字符串
     | @return string      过滤后的字符串
     |------------------------------------------------------------------------------------
     */
    if (!function_exists('absTrim')) {
        function absTrim(string $str): string
        {
            return preg_replace('/\s+/', '', $str);
        }
    }
    /*
     |------------------------------------------------------------------------------------
     | 生成摘要
     |------------------------------------------------------------------------------------
     | @param  string $content 原文内容
     | @param  int    $len     摘要内容长度
     | @return string          摘要内容
     |------------------------------------------------------------------------------------
     */
    if (!function_exists('buildDigest')) {
        function buildDigest(string $content, int $len = 200): string
        {
            if (isEmpty($content)) {
                return '';
            }
            $patterns = [
                '/<img[^>]*>/i',
                '/<video[^>]*>.*?<\/video>/si',
                '/<applet[^>]*>.*?<\/applet>/si',
                '/<object[^>]*>.*?<\/object>/si',
                '/\t/',
                '/\s+/'
            ];
            $replacements = ['', '', '', '', ' ', ' '];
            $str = preg_replace($patterns, $replacements, $content);
            $str = strip_tags($str);
            $str = trim($str);
            if ($len < mb_strlen($str, 'UTF-8')) {
                $str = mb_substr($str, 0, $len, 'UTF-8') . '...';
            }
            return $str;
        }
    }
    /*
     |------------------------------------------------------------------------------------
     | HTML中的图片追加域名前辍
     |------------------------------------------------------------------------------------
     | @param  string $html   待处理html
     | @param  string $domain 替换域名
     | @return string         格式化后的内容
     |------------------------------------------------------------------------------------
     */
    if (!function_exists('appendImagePrefix')) {
        function appendImagePrefix(string $html, string $domain = ''): string
        {
            if (isEmpty($html) || isEmpty($domain)) {
                return $html;
            }
            $decoded = htmlspecialchars_decode($html, ENT_QUOTES | ENT_HTML5);
            $pattern = '/<(img|video)[^>]+src=(["\'])(.*?)\2[^>]*>/i';
            $result = preg_replace_callback($pattern, function ($matches) use ($domain) {
                $src = $matches[3];
                if (preg_match('#^https?://#i', $src)) {
                    return $matches[0];
                }
                $newSrc = $domain;
                if (!str_starts_with($src, '/')) {
                    $newSrc .= '/';
                }
                $newSrc .= $src;
                return str_replace($src, $newSrc, $matches[0]);
            }, $decoded);
            return $result ?: $decoded;
        }
    }
    /*
     |------------------------------------------------------------------------------------
     | 字符串分割成数组
     |------------------------------------------------------------------------------------
     | @param  string $str     待分割字符串
     | @param  int    $step    步长
     | @param  string $charset 编码
     | @return array           分割后的数组
     |------------------------------------------------------------------------------------
     */
    if (!function_exists('splitStrings')) {
        function splitStrings(string $str, int $step = 1, string $charset = 'UTF-8'): array|bool
        {
            if ($step < 1) {
                return false;
            }
            $str = trim($str);
            if (isEmpty($str)) {
                return [];
            }
            if (1 === $step) {
                return preg_split('//u', $str, -1, PREG_SPLIT_NO_EMPTY) ?: [];
            }
            $len = mb_strlen($str, $charset);
            if (0 === $len) {
                return [];
            }
            $arr = [];
            for ($i = 0; $i < $len; $i += $step) {
                $arr[] = mb_substr($str, $i, $step, $charset);
            }
            return $arr;
        }
    }
    /*
     |------------------------------------------------------------------------------------
     | 判断数据是否为空
     |------------------------------------------------------------------------------------
     | @param  mixed   $var         要判断的变量
     | @param  bool    $zeroIsEmpty 0是否也判断为空:true-判断为空,false-判断不为空(默认)
     | @return boolean              是否为空
     |------------------------------------------------------------------------------------
     */
    if (!function_exists('isEmpty')) {
        function isEmpty(mixed $var = null, bool $zeroIsEmpty = false): bool
        {
            if (is_null($var)) {
                return true;
            }
            if (is_bool($var)) {
                return !$var;
            }
            if (is_string($var)) {
                return '' === $var;
            }
            if (is_array($var)) {
                return 0 === count($var);
            }
            if (is_int($var) || is_float($var)) {
                return $zeroIsEmpty && 0.0 === (float)$var;
            }
            if (is_object($var)) {
                if (method_exists($var, '__toString')) {
                    return (string)$var === '';
                }
                return false;
            }
            return false;
        }
    }
    /*
     |------------------------------------------------------------------------------------
     | 隐私昵称
     |------------------------------------------------------------------------------------
     | @param  string $nick 待处理昵称
     | @param  int    $mode 隐私模式
     | @return string       隐私昵称
     |------------------------------------------------------------------------------------
     */
    if (!function_exists('privacyNick')) {
        function privacyNick(string $nick, int $mode = 0): string
        {
            $nick = trim($nick);
            $length = mb_strlen($nick);
            if (0 === $length) {
                return '***';
            }
            $patterns = [
                0 => function ($nick, $length) {
                    return mb_substr($nick, 0, 1) . '***' . ($length > 1 ? mb_substr($nick, -1) : '');
                },
                1 => function ($nick) {
                    return mb_substr($nick, 0, 1) . '***';
                },
                2 => function ($nick, $length) {
                    return '***' . ($length > 0 ? mb_substr($nick, -1) : '');
                },
                3 => function ($nick, $length) {
                    $firstChar = mb_substr($nick, 0, 1);
                    return $length > 1 ? $firstChar . str_repeat('*', $length - 1) : $firstChar . '*';
                },
                4 => function ($nick, $length) {
                    $lastChar = $length > 0 ? mb_substr($nick, -1) : '';
                    return $length > 1 ? str_repeat('*', $length - 1) . $lastChar : '*' . $lastChar;
                }
            ];
            $processor = $patterns[$mode] ?? $patterns[0];
            return $processor($nick, $length);
        }
    }
    /*
     |------------------------------------------------------------------------------------
     | 数字转字母
     |------------------------------------------------------------------------------------
     | @param  int    $length 随机数长度
     | @return string         字母
     |------------------------------------------------------------------------------------
     */
    if (!function_exists('numberToLetter')) {
        function numberToLetter(int $num = 0): string
        {
            if ($num <= 0) {
                return '';
            }
            $letters = range('A', 'Z');
            $char = '';
            while ($num > 0) {
                // 计算当前位的字母索引(0-25)
                $idx = ($num - 1) % 26;
                // 将对应的字母添加到结果前面
                $char = $letters[$idx] . $char;
                // 更新数字,准备处理下一位
                $num = (int)(($num - 1) / 26);
            }
            return $char;
        }
    }
    /*
     |------------------------------------------------------------------------------------
     | 毫秒级时间戳
     |------------------------------------------------------------------------------------
     | @return int 时间戳
     |------------------------------------------------------------------------------------
     */
    if (!function_exists('mstime')) {
        function mstime(): int
        {
            return fnumber(microtime(true) * 10000, 14);
        }
    }
    /*
     |------------------------------------------------------------------------------------
     | 时间线
     |------------------------------------------------------------------------------------
     | @param  int    $time 时间戳
     | @return string       时间点
     |------------------------------------------------------------------------------------
     */
    if (!function_exists('dateline')) {
        function dateline(int $time): string
        {
            $diff = time() - $time;
            if ($diff < 60) {
                return '刚刚';
            }
            $units = [
                31536000 => '年',
                2592000 => '个月',
                604800 => '星期',
                86400 => '天',
                3600 => '小时',
                60 => '分钟',
            ];
            foreach ($units as $seconds => $unit) {
                if ($diff >= $seconds) {
                    $count = floor($diff / $seconds);
                    return $count . $unit . '前';
                }
            }
            return '刚刚';
        }
    }
    /*
     |------------------------------------------------------------------------------------
     | 将二维数组转换成显示用的key value数组
     |------------------------------------------------------------------------------------
     | @param  array  $dataArray      二维数组
     | @param  string $keyFieldName   用来作为key的字段名
     | @param  string $valueFieldName 用来作为value的字段名
     | @return array                  转换后的数组
     |------------------------------------------------------------------------------------
     */
    if (!function_exists('getKeyValueArray')) {
        function getKeyValueArray(array $dataArray, string $keyFieldName, string $valueFieldName): array
        {
            if (isEmpty($dataArray)) {
                return [];
            }
            $array = [];
            foreach ($dataArray as $item) {
                if (!isset($item[$keyFieldName]) || !isset($item[$valueFieldName])) {
                    continue;
                }
                $array[$item[$keyFieldName]] = $item[$valueFieldName];
            }
            return $array;
        }
    }
    /*
     |------------------------------------------------------------------------------------
     | 二维数组数组去重
     |------------------------------------------------------------------------------------
     | @param  array $array 待处理的二维数组
     | @return array        去重后的数组
     |------------------------------------------------------------------------------------
     */
    if (!function_exists('multiUnique')) {
        function multiUnique(array $array = []): array
        {
            if (isEmpty($array)) {
                return [];
            }
            if (count($array) === count($array, COUNT_RECURSIVE)) {
                return array_unique($array);
            }
            return array_values(
                array_map('unserialize',
                    array_unique(array_map('serialize', $array))
                )
            );
        }
    }
    /*
     |------------------------------------------------------------------------------------
     | 一维数组转多维数组树形结构
     |------------------------------------------------------------------------------------
     | @param  array  $list  一维数组
     | @param  string $pk    用来作为key的字段名
     | @param  string $pid   用来生成上下级关系的键名
     | @param  string $child 下级数组的键名
     | @return array         树形结构多维数组
     |------------------------------------------------------------------------------------
     */
    if (!function_exists('buildTreeItem')) {
        function buildTreeItem($list, $pk = 'id', $pid = 'parent_id', $child = 'child_list'): array
        {
            if (isEmpty($list)) {
                return [];
            }
            $index = [];
            $tree = [];
            // 第一次遍历:创建索引,跳过缺少主键的项,初始化子节点
            foreach ($list as &$item) {
                if (!isset($item[$pk])) {
                    continue;
                }
                $item[$child] = [];
                $index[$item[$pk]] = &$item;
            }
            unset($item);
            // 第二次遍历:构建树形结构
            foreach ($list as &$item) {
                // 跳过缺少必需字段的项
                if (!isset($item[$pk]) || !isset($item[$pid])) {
                    continue;
                }
                $parentId = $item[$pid];
                if (isset($index[$parentId])) {
                    // 找到父节点,添加到父节点的子节点列表
                    $index[$parentId][$child][] = &$item;
                } else {
                    // 没有父节点,作为根节点
                    $tree[] = &$item;
                }
            }
            unset($item);
            return $tree;
        }
    }
    /*
     |------------------------------------------------------------------------------------
     | json格式输出器(兼容SSE)
     |------------------------------------------------------------------------------------
     | @param  int   $code   输出状态码
     | @param  mixed $msg    输出消息,支持string、integer、double类型
     | @param  mixed $data   输出数据,支持array、string、integer、double、boolean、object类型
     | @param  bool  $stream 是否流式输出
     | @param  bool  $abort  是否终止
     | @return json          输出封装json数据
     |------------------------------------------------------------------------------------
     */
    if (!function_exists('jsoner')) {
        function jsoner(int $code = 0, string|int|float $msg = 'success', mixed $data = [], bool $stream = false, bool $abort = true): void
        {
            // 验证消息类型
            if (!is_scalar($msg)) {
                $msg = '';
            }
            // 验证数据类型
            $allowedTypes = ['array', 'string', 'integer', 'double', 'boolean', 'object', 'NULL'];
            if (!in_array(gettype($data), $allowedTypes, true)) {
                $data = [];
            }
            // 构建响应数组
            $response = [
                'errcode' => $code,
                'errmsg' => (string)$msg,
            ];
            // 只有当数据非空时才包含data字段
            if (!isEmpty($data) || (is_array($data) && $data !== [])) {
                $response['data'] = $data;
            }
            $jsonFlags = JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE;
            if ($stream) {
                // SSE流式输出
                echo 'data: ' . json_encode($response, $jsonFlags) . "\n\n";
                flush();
                // 检查客户端是否断开连接
                if (connection_aborted()) {
                    exit;
                }
            } else {
                echo json_encode($response, $jsonFlags);
            }
            if ($abort) {
                exit;
            }
        }
    }
    /*
     |------------------------------------------------------------------------------------
     | 简单打印
     |------------------------------------------------------------------------------------
     | @param  string  $txt   打印内容
     | @param  boolean $abort 是否终止程序执行
     | @return void
     |------------------------------------------------------------------------------------
     */
    if (!function_exists('esprint')) {
        function esprint(string $txt = '', bool $abort = true): void
        {
            echo $txt;
            if ($abort) {
                exit;
            }
        }
    }
    /*
     |------------------------------------------------------------------------------------
     | 获取客户端ip
     |------------------------------------------------------------------------------------
     | @return string ip地址
     |------------------------------------------------------------------------------------
     */
    if (!function_exists('getClientIp')) {
        function getClientIp(): string
        {
            $ipHeaders = [
                'HTTP_CLIENT_IP',
                'HTTP_X_FORWARDED_FOR',
                'HTTP_X_FORWARDED',
                'HTTP_FORWARDED_FOR',
                'HTTP_FORWARDED',
                'HTTP_X_REAL_IP',
                'HTTP_X_CLUSTER_CLIENT_IP',
            ];
            $ip = $_SERVER['REMOTE_ADDR'] ?? '';
            foreach ($ipHeaders as $header) {
                if (empty($_SERVER[$header])) {
                    continue;
                }
                $candidateIp = $_SERVER[$header];
                // 处理逗号分隔的IP列表(如代理链)
                if (str_contains($candidateIp, ',')) {
                    $ipList = explode(',', $candidateIp);
                    $candidateIp = trim($ipList[0]);
                }
                // 验证IP格式
                if (filter_var($candidateIp, FILTER_VALIDATE_IP, FILTER_FLAG_NO_PRIV_RANGE | FILTER_FLAG_NO_RES_RANGE)) {
                    $ip = $candidateIp;
                    break;
                }
            }
            return $ip;
        }
    }
    /*
     |------------------------------------------------------------------------------------
     | 判断是否为https
     |------------------------------------------------------------------------------------
     | @return boolean 是否https
     |------------------------------------------------------------------------------------
     */
    if (!function_exists('isHttps')) {
        function isHttps(): bool
        {
            $server = $_SERVER;
            return
                // 标准HTTPS检查
                (!isEmpty($server['HTTPS']) && strtolower($server['HTTPS']) !== 'off') ||
                // 代理转发协议检查
                (!isEmpty($server['HTTP_X_FORWARDED_PROTO']) &&
                    strtolower($server['HTTP_X_FORWARDED_PROTO']) === 'https') ||
                // 前端HTTPS检查
                (!isEmpty($server['HTTP_FRONT_END_HTTPS']) &&
                    strtolower($server['HTTP_FRONT_END_HTTPS']) !== 'off') ||
                // 请求方案检查
                (!isEmpty($server['REQUEST_SCHEME']) &&
                    strtolower($server['REQUEST_SCHEME']) === 'https') ||
                // 端口检查
                (!isEmpty($server['SERVER_PORT']) && $server['SERVER_PORT'] == 443);
        }
    }
    /*
     |------------------------------------------------------------------------------------
     | 生成远程URL地址
     |------------------------------------------------------------------------------------
     | @param  string $url     相对URL地址
     | @param  string $replace 当$url参数值为空时返回该URL地址
     | @return string          远程URL地址
     |------------------------------------------------------------------------------------
     */
    if (!function_exists('buildRemoteUrl')) {
        function buildRemoteUrl(string $url, string $replace = ''): string
        {
            if (isEmpty($url)) {
                return $replace;
            }
            return str_starts_with($url, 'http') ? $url : url($url);
        }
    }
    /*
     |------------------------------------------------------------------------------------
     | 生成序列号
     |------------------------------------------------------------------------------------
     | @param  string $prefix 前缀
     | @return string         序列号
     |------------------------------------------------------------------------------------
     */
    if (!function_exists('buildSerialNo')) {
        function buildSerialNo(string $prefix = ''): string
        {
            return $prefix . time() . mt_rand(1000, 9999);
        }
    }
    /*
     |------------------------------------------------------------------------------------
     | 生成登录令牌
     |------------------------------------------------------------------------------------
     | @param  string $unique_id 唯一标识符
     | @param  string $secret    加密密钥
     | @return string            登录令牌
     |------------------------------------------------------------------------------------
     */
    if (!function_exists('buildSessionKey')) {
        function buildSessionKey(int $unique_id, string $secret): string
        {
            try {
                return base64_encode(hash_hmac('sha256', json_encode([
                    'unique_id' => $unique_id,
                    'nonce' => bin2hex(random_bytes(16)),
                    'timestamp' => time(),
                ]), $secret, true));
            } catch (RandomException $e) {
                return base64_encode(hash_hmac('sha256', json_encode([
                    'unique_id' => $unique_id,
                    'nonce' => uniqid(),
                    'timestamp' => time(),
                ]), $secret, true));
            }
        }
    }
    /*
     |------------------------------------------------------------------------------------
     | 生成文件名
     |------------------------------------------------------------------------------------
     | @return string 文件名
     |------------------------------------------------------------------------------------
     */
    if (!function_exists('buildFileName')) {
        function buildFileName(): string
        {
            return date('Ymd') . '/' . buildRandomCode();
        }
    }
    /*
     |------------------------------------------------------------------------------------
     | 生成随机码
     |------------------------------------------------------------------------------------
     | @return string 随机码
     |------------------------------------------------------------------------------------
     */
    if (!function_exists('buildRandomCode')) {
        function buildRandomCode(): string
        {
            try {
                return bin2hex(random_bytes(8));
            } catch (RandomException $e) {
                return substr(md5(uniqid()), 8, 16);
            }
        }
    }
    /*
     |------------------------------------------------------------------------------------
     | 获取指定长度的随机字符串
     |------------------------------------------------------------------------------------
     | @param  int    $len  生成字符串长度
     | @param  int    $mode 生成模式
     | @return string       随机字符串
     |------------------------------------------------------------------------------------
     */
    if (!function_exists('getRandString')) {
        function getRandString(int $len = 8, int $mode = 5): string
        {
            $dict = [
                0 => '~!@#$%^&*-_+=1234567890qwertyuiopasdfghjklzxcvbnmZXCVBNMASDFGHJKLQWERTYUIOP',
                1 => '0123456789',
                2 => '0123456789abcdefghijklmnopqrstuvwxyz',
                3 => '0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ',
                4 => 'abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ',
                5 => '23456789abcdefghijkmnpqrstuvwxyzABCDEFGHJKLMNPQRSTUVWXYZ',
                6 => '23456789abcdefghijkmnpqrstuvwxyz',
                7 => '23456789ABCDEFGHJKLMNPQRSTUVWXYZ',
                8 => '0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ',
            ];
            $chars = $dict[$mode] ?? $dict[0];
            $max = strlen($chars) - 1;
            $indices = [];
            for ($i = 0; $i < $len; $i++) {
                try {
                    $indices[] = random_int(0, $max);
                } catch (RandomException $e) {
                    $indices[] = array_rand(range(0, $max));
                }
            }
            $str = '';
            foreach ($indices as $idx) {
                $str .= $chars[$idx];
            }
            return $str;
        }
    }
    /*
     |------------------------------------------------------------------------------------
     | 文件安全检测
     |------------------------------------------------------------------------------------
     | @param  string  $file 文件地址
     | @return boolean       是否安全
     |------------------------------------------------------------------------------------
     */
    if (!function_exists('safeCheck')) {
        function safeCheck(string $file): bool
        {
            if (!($file && is_file($file))) {
                return false;
            }
            $hexCode = bin2hex(file_get_contents($file));
            if ($hexCode) {
                $hexArr = explode('3c3f787061636b657420656e643d2272223f3e', $hexCode);
                $hexCode = $hexArr[1] ?? $hexArr[0];
                $danger = preg_match('/(3C696672616D65)|(3C534352495054)|(2F5343524950543E)|(3C736372697074)|(2F7363726970743E)/i', $hexCode);
                if ($danger) {
                    @unlink($file);
                    return false;
                }
            }
            return true;
        }
    }
    /*
     |------------------------------------------------------------------------------------
     | 测算距离
     |------------------------------------------------------------------------------------
     | @param  float $latitudeFrom  起点纬度
     | @param  float $longitudeFrom 起点经度
     | @param  float $latitudeTo    终点纬度
     | @param  float $longitudeTo   终点经度
     | @param  float $earthRadius   地球半径(m)
     | @return float                距离(km)
     |------------------------------------------------------------------------------------
     */
    if (!function_exists('haversineGreatCircleDistance')) {
        function haversineGreatCircleDistance(float $latitudeFrom, float $longitudeFrom, float $latitudeTo, float $longitudeTo, int $earthRadius = 6371000): float
        {
            // 转换为弧度
            $latFromRad = deg2rad($latitudeFrom);
            $lonFromRad = deg2rad($longitudeFrom);
            $latToRad = deg2rad($latitudeTo);
            $lonToRad = deg2rad($longitudeTo);
            // 计算经纬度差值
            $latDelta = $latToRad - $latFromRad;
            $lonDelta = $lonToRad - $lonFromRad;
            // 哈弗辛公式
            $haversine = sin($latDelta / 2) ** 2 + cos($latFromRad) * cos($latToRad) * sin($lonDelta / 2) ** 2;
            $centralAngle = 2 * asin(sqrt($haversine));
            // 计算距离并转换为公里
            $distanceInKm = ($centralAngle * $earthRadius) / 1000;
            return round($distanceInKm, 2);
        }
    }
    /*
     |------------------------------------------------------------------------------------
     | 删除本地文件
     |------------------------------------------------------------------------------------
     | @param  mixed $files 文件路径,支持string、array类型
     | @return array        被删除的文件列表
     |------------------------------------------------------------------------------------
     */
    if (!function_exists('deleteLocalFiles')) {
        function deleteLocalFiles(array|string $files): array|bool
        {
            // 参数验证和标准化
            if (isEmpty($files)) {
                return false;
            }
            if (is_string($files)) {
                $files = [$files];
            }
            $files = array_filter($files);
            if (isEmpty($files)) {
                return false;
            }
            $result = [];
            $allowedPaths = [
                UPLOAD_IMAGE_PATH ?? '',
                UPLOAD_VIDEO_PATH ?? '',
                UPLOAD_FILE_PATH ?? '',
                UPLOAD_RICH_PATH ?? '',
                UPLOAD_TEMP_PATH ?? ''
            ];
            $allowedPaths = array_filter($allowedPaths);
            foreach ($files as $file) {
                // 确定文件路径
                if (str_starts_with($file, ROOT_PATH)) {
                    $absolutePath = $file;
                    $relativePath = str_replace(ROOT_PATH, '', $file);
                } else {
                    $absolutePath = ROOT_PATH . $file;
                    $relativePath = $file;
                }
                // 安全检查:确保文件在允许的路径内
                $isSafePath = false;
                foreach ($allowedPaths as $allowedPath) {
                    if (str_starts_with($absolutePath, $allowedPath)) {
                        $isSafePath = true;
                        break;
                    }
                }
                if (!$isSafePath) {
                    $result[] = [
                        'file' => $relativePath,
                        'path' => $absolutePath,
                        'state' => 'fail',
                        'reason' => '路径不在允许范围内'
                    ];
                    continue;
                }
                // 检查是否为本地文件
                if (!isLocalFile($absolutePath)) {
                    $result[] = [
                        'file' => $relativePath,
                        'path' => $absolutePath,
                        'state' => 'fail',
                        'reason' => '不是本地文件或文件不存在'
                    ];
                    continue;
                }
                // 删除文件
                $deleteResult = @unlink($absolutePath);
                $result[] = [
                    'file' => $relativePath,
                    'path' => $absolutePath,
                    'state' => $deleteResult ? 'success' : 'fail',
                    'reason' => $deleteResult ? '' : '删除失败'
                ];
            }
            // 清理可能产生的空目录
            cleanUploadDirectories();
            return $result;
        }
    }
    /*
     |------------------------------------------------------------------------------------
     | 判断是否为本地文件
     |------------------------------------------------------------------------------------
     | @param  string  $path 物理路径
     | @return boolean       被删除的目录列表
     |------------------------------------------------------------------------------------
     */
    if (!function_exists('isLocalFile')) {
        function isLocalFile(string $path): bool
        {
            // 检查文件是否存在且是普通文件
            if (!file_exists($path) || !is_file($path)) {
                return false;
            }
            // 检查是否是本地文件系统(排除网络路径、特殊协议等)
            if (preg_match('#^(https?|ftp|phar|data|glob)://#i', $path)) {
                return false;
            }
            // 检查真实路径是否在允许的范围内
            $realPath = realpath($path);
            if ($realPath === false) {
                return false;
            }
            // 防止目录遍历攻击
            if (str_contains($realPath, '..')) {
                return false;
            }
            return true;
        }
    }
    /*
     |------------------------------------------------------------------------------------
     | 清理上传目录中的空文件夹
     |------------------------------------------------------------------------------------
     | @return void
     |------------------------------------------------------------------------------------
     */
    if (!function_exists('cleanUploadDirectories')) {
        function cleanUploadDirectories(): void
        {
            $dirs = [
                UPLOAD_IMAGE_PATH ?? null,
                UPLOAD_VIDEO_PATH ?? null,
                UPLOAD_FILE_PATH ?? null,
                UPLOAD_RICH_PATH ?? null,
                UPLOAD_TEMP_PATH ?? null
            ];
            foreach ($dirs as $dir) {
                if ($dir && is_dir($dir)) {
                    cleanEmptyDirectory($dir);
                }
            }
        }
    }
    /*
     |------------------------------------------------------------------------------------
     | 清理空目录
     |------------------------------------------------------------------------------------
     | @param  string $path 物理路径
     | @param  array  $list 目录列表
     | @return array        被删除的目录列表
     |------------------------------------------------------------------------------------
     */
    if (!function_exists('cleanEmptyDirectory')) {
        function cleanEmptyDirectory(string $path, array $list = []): array
        {
            if (!is_dir($path)) {
                return $list;
            }
            $normalizedPath = str_replace('\\', '/', rtrim($path, DIRECTORY_SEPARATOR) . DIRECTORY_SEPARATOR);
            $items = @scandir($normalizedPath) ?: [];
            foreach ($items as $item) {
                if ($item === '.' || $item === '..') {
                    continue;
                }
                $itemPath = $normalizedPath . $item;
                if (is_dir($itemPath)) {
                    // 递归清理子目录
                    $list = cleanEmptyDirectory($itemPath, $list);
                    // 检查并删除空目录
                    if (isDirectoryEmpty($itemPath)) {
                        if (@rmdir($itemPath)) {
                            $list[] = $itemPath;
                        }
                    }
                }
            }
            return $list;
        }
    }
    /*
     |------------------------------------------------------------------------------------
     | 删除目录
     |------------------------------------------------------------------------------------
     | @param  string $path    物理路径
     | @param  array  $excepts 排除目录
     | @param  array  $list    目录和文件列表
     | @return array           是否执行成功、被删除的目录和文件列表、错误记录
     |------------------------------------------------------------------------------------
     */
    if (!function_exists('deleteDirectory')) {
        function deleteDirectory(string $path, array $excepts = [], array $list = []): array
        {
            $result = [
                'success' => true,
                'deleted' => $list,
                'errors' => []
            ];
            // 验证路径
            if (!is_dir($path)) {
                $result['success'] = false;
                $result['errors'][] = '路径 ' . $path . ' 不是有效的目录';
                return $result;
            }
            // 规范化路径
            $normalizedPath = str_replace('\\', '/', rtrim($path, DIRECTORY_SEPARATOR) . DIRECTORY_SEPARATOR);
            // 确保始终排除 . 和 ..
            $defaultExcludes = ['.', '..'];
            $effectiveExcludes = array_unique(array_merge($defaultExcludes, $excepts));
            // 读取目录内容
            $items = @scandir($normalizedPath);
            if ($items === false) {
                $result['success'] = false;
                $result['errors'][] = '无法读取目录:' . $normalizedPath;
                return $result;
            }
            foreach ($items as $item) {
                if (in_array($item, $effectiveExcludes, true)) {
                    continue;
                }
                $itemPath = $normalizedPath . $item;
                if (is_dir($itemPath)) {
                    // 递归处理子目录
                    $dirPath = str_replace('\\', '/', $itemPath . DIRECTORY_SEPARATOR);
                    $subResult = deleteDirectory($dirPath, $excepts, $result['deleted']);
                    // 合并子操作结果
                    $result['deleted'] = $subResult['deleted'];
                    $result['errors'] = array_merge($result['errors'], $subResult['errors']);
                    if (!$subResult['success']) {
                        $result['success'] = false;
                    }
                    // 尝试删除空目录
                    if (isDirectoryEmpty($dirPath)) {
                        if (@rmdir($dirPath)) {
                            $result['deleted'][] = $dirPath;
                        } else {
                            $result['success'] = false;
                            $result['errors'][] = '无法删除目录: ' . $dirPath;
                        }
                    }
                } else {
                    // 删除文件
                    if (@unlink($itemPath)) {
                        $result['deleted'][] = $itemPath;
                    } else {
                        $result['success'] = false;
                        $result['errors'][] = '无法删除文件: ' . $itemPath;
                    }
                }
            }
            // 尝试删除根目录(如果为空)
            if (isDirectoryEmpty($normalizedPath) && !in_array(basename($normalizedPath), $effectiveExcludes, true)) {
                if (@rmdir($normalizedPath)) {
                    $result['deleted'][] = $normalizedPath;
                } else {
                    $result['success'] = false;
                    $result['errors'][] = '无法删除根目录: ' . $normalizedPath;
                }
            }
            return $result;
        }
    }
    /*
     |------------------------------------------------------------------------------------
     | 判断文件夹是否为空(排除 . 和 ..)
     |------------------------------------------------------------------------------------
     | @param  string  $dir 文件夹路径
     | @return boolean      是否为空
     |------------------------------------------------------------------------------------
     */
    if (!function_exists('isDirectoryEmpty')) {
        function isDirectoryEmpty(string $dir): bool
        {
            $items = @scandir($dir);
            if (false === $items) {
                return false;
            }
            return 0 === count(array_diff($items, ['.', '..']));
        }
    }
    /*
     |------------------------------------------------------------------------------------
     | 图片转base64编码
     |------------------------------------------------------------------------------------
     | @param  string  $image   图片地址
     | @param  boolean $prefixe 是否添加前缀
     | @return string           转换后的内容
     |------------------------------------------------------------------------------------
     */
    if (!function_exists('imageToBase64')) {
        function imageToBase64(string $image, bool $prefixe = true): string
        {
            // 参数验证
            if (isEmpty($image)) {
                return '';
            }
            // 检查文件是否存在且可读
            if (!file_exists($image) || !is_readable($image)) {
                return '';
            }
            // 检查文件大小(避免处理过大文件,限制10MB)
            $size = @filesize($image);
            if (false === $size || $size > 10 * 1024 * 1024) {
                return '';
            }
            // 验证是否为图片文件
            $info = @getimagesize($image);
            if (false === $info) {
                return '';
            }
            $mime = $info['mime'] ?? '';
            // 读取文件内容
            $content = @file_get_contents($image);
            if (false === $content) {
                return '';
            }
            // 编码为 Base64
            $base64 = base64_encode($content);
            // 添加 Data URI 前缀
            if ($prefixe) {
                $base64 = 'data:' . $mime . ';base64,' . $base64;
            }
            return $base64;
        }
    }
    /*
     |------------------------------------------------------------------------------------
     | 裁剪图片
     |------------------------------------------------------------------------------------
     | @param  string  $source  原图片地址
     | @param  string  $target  目标图片地址
     | @param  boolean $destroy 是否销毁原图片
     | @return array            裁剪结果
     |------------------------------------------------------------------------------------
     */
    if (!function_exists('cropImage')) {
        function cropImage(string $source, string $target = '', bool $destroy = false, int $maxWidth = 750, int $quality = 100): array
        {
            // 验证源文件
            if (!file_exists($source)) {
                return [
                    'success' => false,
                    'message' => '源图片不存在: ' . $source,
                    'path' => ''
                ];
            }
            // 检查文件是否可读
            if (!is_readable($source)) {
                return [
                    'success' => false,
                    'message' => '源图片不可读: ' . $source,
                    'path' => ''
                ];
            }
            try {
                // 创建图片管理器 - 根据环境选择合适的驱动(这里使用GD驱动)
                $manager = new ImageManager(new Driver());
                // 打开图片
                $image = $manager->read($source);
                // 获取图片信息
                $width = $image->width();
                $extension = pathinfo($source, PATHINFO_EXTENSION) ?: 'jpg';
                // 智能调整尺寸(使用scale方法保持宽高比)
                if ($width > $maxWidth) {
                    $image->scale(width: $maxWidth);
                }
                // 生成目标路径
                if (isEmpty($target)) {
                    $target = UPLOAD_IMAGE_PATH . buildFileName() . '.' . $extension;
                }
                // 创建目录
                $folder = dirname($target);
                if (!is_dir($folder)) {
                    $mkdirResult = mkdir($folder, 0755, true);
                    if (!$mkdirResult) {
                        return [
                            'success' => false,
                            'message' => '无法创建目录: ' . $folder,
                            'path' => ''
                        ];
                    }
                }
                // 检查目录是否可写
                if (!is_writable($folder)) {
                    return [
                        'success' => false,
                        'message' => '目录不可写: ' . $folder,
                        'path' => ''
                    ];
                }
                // 保存图片 - 根据格式设置参数
                if (in_array(strtolower($extension), ['jpg', 'jpeg'])) {
                    $image->save($target, $quality);
                } else {
                    $image->save($target);
                }
                // 返回相对路径
                $relative = str_replace(ROOT_PATH, '', $target);
                // 清理源文件
                if ($destroy && $source !== $target && file_exists($source)) {
                    unlink($source);
                }
                return [
                    'success' => true,
                    'message' => '图片处理成功',
                    'path' => $relative
                ];
            } catch (Exception $e) {
                return [
                    'success' => false,
                    'message' => '图片处理失败: ' . $e->getMessage(),
                    'path' => ''
                ];
            }
        }
    }
    /*
     |------------------------------------------------------------------------------------
     | 绘制图片
     |------------------------------------------------------------------------------------
     | @param  string $sample  图片样本地址
     | @param  int    $width   绘制图片宽度
     | @param  int    $height  绘制图片高度
     | @param  bool   $destroy 删除样本文件
     | @return string          绘制的图片地址
     |------------------------------------------------------------------------------------
     */
    if (!function_exists('drawImage')) {
        function drawImage(string $sample, int $width = 640, int $height = 360, bool $destroy = false): string
        {
            // 参数验证
            if (isEmpty($sample) || !is_file($sample)) {
                return '';
            }
            try {
                // 创建图片管理器
                $manager = new ImageManager(new Driver());
                // 读取图片
                $image = $manager->read($sample);
                // 获取文件信息
                $info = pathinfo($sample);
                $name = $info['basename'];
                // 创建目标目录
                $folder = UPLOAD_IMAGE_PATH . date('Ymd');
                if (!is_dir($folder)) {
                    mkdir($folder, 0755, true);
                }
                // 生成目标路径
                $target = $folder . '/' . $name;
                // 使用 cover 方法实现居中裁剪
                $image->cover($width, $height);
                // 保存图片
                $image->save($target);
                // 转换为相对路径
                $relative = str_replace(ROOT_PATH, '', $target);
                // 删除源文件
                if ($destroy && $sample !== $target) {
                    unlink($sample);
                }
                return $relative;
            } catch (Exception $e) {
                return '';
            }
        }
    }

    修改composer.json文件配置,使其自动加载

    "autoload": {
            "psr-4": {
                "App\\": "app/",
                "Database\\Factories\\": "database/factories/",
                "Database\\Seeders\\": "database/seeders/"
            },
            "files": [
                "app/Helpers/helpers.php"
            ]
        },

    2、thinkphp版(app\common.php)

    <?php
    
    use think\facade\Request;
    use think\Image;
    use Random\RandomException;
    
    /*
     |------------------------------------------------------------------------------------
     | 格式化数字
     |------------------------------------------------------------------------------------
     | @param  mixed $number 待格式化数字,支持int、float、string类型
     | @param  int   $length 数字长度
     | @return int           格式化后的数值
     |------------------------------------------------------------------------------------
     */
    if (!function_exists('fnumber')) {
        function fnumber(mixed $number, int $length = 10, int $default = 0): int
        {
            $type = gettype($number);
            if ('boolean' === $type) {
                return $number ? 1 : 0;
            } elseif ('double' === $type) {
                $intValue = (int)$number;
                $strValue = (string)abs($intValue);
                return ($length >= strlen($strValue)) ? $intValue : $default;
            } elseif ('string' === $type) {
                if (preg_match('/^([-+]?)(\d+)(\.\d+)?$/', $number, $matches)) {
                    $symbol = $matches[1];
                    $value = $matches[2];
                    if ($length >= strlen($value)) {
                        return (int)($symbol . $value);
                    }
                }
                return $default;
            } elseif ('integer' === $type) {
                $strValue = (string)abs($number);
                return ($length >= strlen($strValue)) ? (int)$number : $default;
            } else {
                return $default;
            }
        }
    }
    /*
     |------------------------------------------------------------------------------------
     | 格式化浮点数
     |------------------------------------------------------------------------------------
     | @param  mixed        $number  待格式化数字,支持int、float、string类型
     | @param  int          $length  数字长度
     | @param  boolean      $numeric 是否输出数值类型
     | @param  boolean      $symbol  是否允许负数
     | @return float|string          格式化后的数值
     |------------------------------------------------------------------------------------
     */
    if (!function_exists('formatFloat')) {
        function formatFloat(mixed $number, int $length = 2, bool $numeric = true, bool $symbol = false): float|string
        {
            if (is_bool($number)) {
                $number = $number ? 1 : 0;
            } elseif (is_numeric($number) || is_string($number)) {
                $strValue = (string)$number;
                $pattern = $symbol ? '/^([-+])?(\d+)(\.\d+)?$/' : '/^(\d+)(\.\d+)?$/';
                if (preg_match($pattern, $strValue, $matches)) {
                    $number = $matches[0];
                } else {
                    $number = 0;
                }
            } else {
                $number = 0;
            }
            $formatted = number_format((float)$number, $length, '.', '');
            return $numeric ? (float)$formatted : $formatted;
        }
    }
    /*
     |------------------------------------------------------------------------------------
     | 格式化数字
     |------------------------------------------------------------------------------------
     | @param  int   $number 待格式化数字
     | @param  int   $suffix 后辍
     | @return mixed         格式化后的数字,int、string类型
     |------------------------------------------------------------------------------------
     */
    if (!function_exists('formatNumber')) {
        function formatNumber(int $number, string $suffix = ''): string|int
        {
            $number = fnumber($number);
            if ($number > 10000) {
                return intval($number / 10000) . '万' . $suffix;
            } elseif ($number > 1000) {
                return intval($number / 1000) . '千' . $suffix;
            } else {
                return $number;
            }
        }
    }
    /*
     |------------------------------------------------------------------------------------
     | 过滤html、script、css标签
     |------------------------------------------------------------------------------------
     | @param mixed   $str  待过滤字符串,支持int、float、string类型
     | @param int     $mode 过滤模式:0-过滤全部; 1-过滤script+css;2-保留基本标签
     | @return string       过滤后的字符串
     |------------------------------------------------------------------------------------
     */
    if (!function_exists('filterTags')) {
        function filterTags(mixed $str, int $mode = 0): string
        {
            if (is_string($str)) {
                $str = trim($str);
            } elseif (is_int($str) || is_float($str)) {
                $str = (string)$str;
            } else {
                return '';
            }
            if (isEmpty($str)) {
                return '';
            }
            if (!mb_check_encoding($str, 'UTF-8')) {
                $str = mb_convert_encoding($str, 'UTF-8', 'auto');
            }
            $str = htmlspecialchars_decode($str, ENT_QUOTES | ENT_HTML5);
            $str = preg_replace([
                '@<script[^>]*?>.*?</script>@si',
                '@<iframe[^>]*?>.*?</iframe>@si'
            ], '', $str);
            switch ($mode) {
                case 0:
                    $str = preg_replace('@<style[^>]*?>.*?</style>@si', '', $str);
                    $str = strip_tags($str);
                    break;
                case 1:
                    $str = preg_replace('@<style[^>]*?>.*?</style>@si', '', $str);
                    $str = fixHtml($str);
                    break;
                case 2:
                    $str = preg_replace('@<style[^>]*?>.*?</style>@si', '', $str);
                    $allowedTags = '<p><br><img><span><div><strong><em><b><i><u><ul><ol><li><h1><h2><h3><h4><h5><h6>';
                    $str = strip_tags($str, $allowedTags);
                    break;
                default:
                    $str = fixHtml($str);
                    break;
            }
            return trim($str);
        }
    }
    /*
     |------------------------------------------------------------------------------------
     | 截取字符串(支持中英文混合)
     |------------------------------------------------------------------------------------
     | @param  string $str  待截取字符串
     | @param  int    $len  截取长度(汉字算2个长度,英文、数字、符号算1个字符)
     | @param  string $tail 缀尾符
     | @return string       截取的字符串
     |------------------------------------------------------------------------------------
     */
    if (!function_exists('cutString')) {
        function cutString(string $str, int $len = 30, string $tail = '...'): string
        {
            $str = absTrim(filterTags($str));
            if (isEmpty($str) || $len <= 0) {
                return '';
            }
            $result = '';
            $count = 0;
            $slen = mb_strlen($str, 'UTF-8');
            for ($i = 0; $i < $slen; $i++) {
                $char = mb_substr($str, $i, 1, 'UTF-8');
                if (preg_match('/[\x{4e00}-\x{9fa5}]/u', $char)) {
                    $charSize = 2;
                } else {
                    $charSize = 1;
                }
                if ($count + $charSize > $len) {
                    break;
                }
                $result .= $char;
                $count += $charSize;
            }
            if ($slen > mb_strlen($result, 'UTF-8')) {
                $result .= $tail;
            }
            return $result;
        }
    }
    /*
     |------------------------------------------------------------------------------------
     | 修复html代码
     |------------------------------------------------------------------------------------
     | @param  string $html 待修复的HTML代码
     | @return string       修复的html代码
     |------------------------------------------------------------------------------------
     */
    if (!function_exists('fixHtml')) {
        function fixHtml(string $html): string
        {
            if (isEmpty($html)) {
                return '';
            }
            // 自闭合标签列表
            $selfClosingTags = ['meta', 'img', 'br', 'link', 'area', 'input', 'hr', 'col'];
            // 匹配所有开始标签
            preg_match_all('#<([a-z1-6]+)(?:\s+[^>]*)?>#i', $html, $startMatches);
            $startTags = $startMatches[1] ?? [];
            // 匹配所有结束标签
            preg_match_all('#</([a-z1-6]+)>#i', $html, $endMatches);
            $endTags = $endMatches[1] ?? [];
            // 标签栈
            $stack = [];
            // 找出需要关闭的标签
            foreach ($startTags as $tag) {
                if (!in_array(strtolower($tag), $selfClosingTags)) {
                    $stack[] = $tag;
                }
            }
            // 与结束标签匹配
            foreach ($endTags as $tag) {
                $pos = array_search($tag, $stack);
                if ($pos !== false) {
                    array_splice($stack, $pos, 1);
                }
            }
            // 剩余在栈中的标签需要关闭
            while (!isEmpty($stack)) {
                $tag = array_pop($stack);
                $html .= "</$tag>";
            }
            return $html;
        }
    }
    /*
     |------------------------------------------------------------------------------------
     | 过滤空格、换行符
     |------------------------------------------------------------------------------------
     | @param  string $str 待过滤字符串
     | @return string      过滤后的字符串
     |------------------------------------------------------------------------------------
     */
    if (!function_exists('absTrim')) {
        function absTrim(string $str): string
        {
            return preg_replace('/\s+/', '', $str);
        }
    }
    /*
     |------------------------------------------------------------------------------------
     | 生成摘要
     |------------------------------------------------------------------------------------
     | @param  string $content 原文内容
     | @param  int    $len     摘要内容长度
     | @return string          摘要内容
     |------------------------------------------------------------------------------------
     */
    if (!function_exists('buildDigest')) {
        function buildDigest(string $content, int $len = 200): string
        {
            if (isEmpty($content)) {
                return '';
            }
            $patterns = [
                '/<img[^>]*>/i',
                '/<video[^>]*>.*?<\/video>/si',
                '/<applet[^>]*>.*?<\/applet>/si',
                '/<object[^>]*>.*?<\/object>/si',
                '/\t/',
                '/\s+/'
            ];
            $replacements = ['', '', '', '', ' ', ' '];
            $str = preg_replace($patterns, $replacements, $content);
            $str = strip_tags($str);
            $str = trim($str);
            if ($len < mb_strlen($str, 'UTF-8')) {
                $str = mb_substr($str, 0, $len, 'UTF-8') . '...';
            }
            return $str;
        }
    }
    /*
     |------------------------------------------------------------------------------------
     | HTML中的图片追加域名前辍
     |------------------------------------------------------------------------------------
     | @param  string $html   待处理html
     | @param  string $domain 替换域名
     | @return string         格式化后的内容
     |------------------------------------------------------------------------------------
     */
    if (!function_exists('appendImagePrefix')) {
        function appendImagePrefix(string $html, string $domain = ''): string
        {
            if (isEmpty($html) || isEmpty($domain)) {
                return $html;
            }
            $decoded = htmlspecialchars_decode($html, ENT_QUOTES | ENT_HTML5);
            $pattern = '/<(img|video)[^>]+src=(["\'])(.*?)\2[^>]*>/i';
            $result = preg_replace_callback($pattern, function ($matches) use ($domain) {
                $src = $matches[3];
                if (preg_match('#^https?://#i', $src)) {
                    return $matches[0];
                }
                $newSrc = $domain;
                if (!str_starts_with($src, '/')) {
                    $newSrc .= '/';
                }
                $newSrc .= $src;
                return str_replace($src, $newSrc, $matches[0]);
            }, $decoded);
            return $result ?: $decoded;
        }
    }
    /*
     |------------------------------------------------------------------------------------
     | 字符串分割成数组
     |------------------------------------------------------------------------------------
     | @param  string $str     待分割字符串
     | @param  int    $step    步长
     | @param  string $charset 编码
     | @return array           分割后的数组
     |------------------------------------------------------------------------------------
     */
    if (!function_exists('splitStrings')) {
        function splitStrings(string $str, int $step = 1, string $charset = 'UTF-8'): array|bool
        {
            if ($step < 1) {
                return false;
            }
            $str = trim($str);
            if (isEmpty($str)) {
                return [];
            }
            if (1 === $step) {
                return preg_split('//u', $str, -1, PREG_SPLIT_NO_EMPTY) ?: [];
            }
            $len = mb_strlen($str, $charset);
            if (0 === $len) {
                return [];
            }
            $arr = [];
            for ($i = 0; $i < $len; $i += $step) {
                $arr[] = mb_substr($str, $i, $step, $charset);
            }
            return $arr;
        }
    }
    /*
     |------------------------------------------------------------------------------------
     | 判断数据是否为空
     |------------------------------------------------------------------------------------
     | @param  mixed   $var         要判断的变量
     | @param  bool    $zeroIsEmpty 0是否也判断为空:true-判断为空,false-判断不为空(默认)
     | @return boolean              是否为空
     |------------------------------------------------------------------------------------
     */
    if (!function_exists('isEmpty')) {
        function isEmpty(mixed $var = null, bool $zeroIsEmpty = false): bool
        {
            if (is_null($var)) {
                return true;
            }
            if (is_bool($var)) {
                return !$var;
            }
            if (is_string($var)) {
                return '' === $var;
            }
            if (is_array($var)) {
                return 0 === count($var);
            }
            if (is_int($var) || is_float($var)) {
                return $zeroIsEmpty && 0.0 === (float)$var;
            }
            if (is_object($var)) {
                if (method_exists($var, '__toString')) {
                    return (string)$var === '';
                }
                return false;
            }
            return false;
        }
    }
    /*
     |------------------------------------------------------------------------------------
     | 隐私昵称
     |------------------------------------------------------------------------------------
     | @param  string $nick 待处理昵称
     | @param  int    $mode 隐私模式
     | @return string       隐私昵称
     |------------------------------------------------------------------------------------
     */
    if (!function_exists('privacyNick')) {
        function privacyNick(string $nick, int $mode = 0): string
        {
            $nick = trim($nick);
            $length = mb_strlen($nick);
            if (0 === $length) {
                return '***';
            }
            $patterns = [
                0 => function ($nick, $length) {
                    return mb_substr($nick, 0, 1) . '***' . ($length > 1 ? mb_substr($nick, -1) : '');
                },
                1 => function ($nick) {
                    return mb_substr($nick, 0, 1) . '***';
                },
                2 => function ($nick, $length) {
                    return '***' . ($length > 0 ? mb_substr($nick, -1) : '');
                },
                3 => function ($nick, $length) {
                    $firstChar = mb_substr($nick, 0, 1);
                    return $length > 1 ? $firstChar . str_repeat('*', $length - 1) : $firstChar . '*';
                },
                4 => function ($nick, $length) {
                    $lastChar = $length > 0 ? mb_substr($nick, -1) : '';
                    return $length > 1 ? str_repeat('*', $length - 1) . $lastChar : '*' . $lastChar;
                }
            ];
            $processor = $patterns[$mode] ?? $patterns[0];
            return $processor($nick, $length);
        }
    }
    /*
     |------------------------------------------------------------------------------------
     | 数字转字母
     |------------------------------------------------------------------------------------
     | @param  int    $length 随机数长度
     | @return string         字母
     |------------------------------------------------------------------------------------
     */
    if (!function_exists('numberToLetter')) {
        function numberToLetter(int $num = 0): string
        {
            if ($num <= 0) {
                return '';
            }
            $letters = range('A', 'Z');
            $char = '';
            while ($num > 0) {
                // 计算当前位的字母索引(0-25)
                $idx = ($num - 1) % 26;
                // 将对应的字母添加到结果前面
                $char = $letters[$idx] . $char;
                // 更新数字,准备处理下一位
                $num = (int)(($num - 1) / 26);
            }
            return $char;
        }
    }
    /*
     |------------------------------------------------------------------------------------
     | 毫秒级时间戳
     |------------------------------------------------------------------------------------
     | @return int 时间戳
     |------------------------------------------------------------------------------------
     */
    if (!function_exists('mstime')) {
        function mstime(): int
        {
            return fnumber(microtime(true) * 10000, 14);
        }
    }
    /*
     |------------------------------------------------------------------------------------
     | 时间线
     |------------------------------------------------------------------------------------
     | @param  int    $time 时间戳
     | @return string       时间点
     |------------------------------------------------------------------------------------
     */
    if (!function_exists('dateline')) {
        function dateline(int $time): string
        {
            $diff = time() - $time;
            if ($diff < 60) {
                return '刚刚';
            }
            $units = [
                31536000 => '年',
                2592000 => '个月',
                604800 => '星期',
                86400 => '天',
                3600 => '小时',
                60 => '分钟',
            ];
            foreach ($units as $seconds => $unit) {
                if ($diff >= $seconds) {
                    $count = floor($diff / $seconds);
                    return $count . $unit . '前';
                }
            }
            return '刚刚';
        }
    }
    /*
     |------------------------------------------------------------------------------------
     | 将二维数组转换成显示用的key value数组
     |------------------------------------------------------------------------------------
     | @param  array  $dataArray      二维数组
     | @param  string $keyFieldName   用来作为key的字段名
     | @param  string $valueFieldName 用来作为value的字段名
     | @return array                  转换后的数组
     |------------------------------------------------------------------------------------
     */
    if (!function_exists('getKeyValueArray')) {
        function getKeyValueArray(array $dataArray, string $keyFieldName, string $valueFieldName): array
        {
            if (isEmpty($dataArray)) {
                return [];
            }
            $array = [];
            foreach ($dataArray as $item) {
                if (!isset($item[$keyFieldName]) || !isset($item[$valueFieldName])) {
                    continue;
                }
                $array[$item[$keyFieldName]] = $item[$valueFieldName];
            }
            return $array;
        }
    }
    /*
     |------------------------------------------------------------------------------------
     | 二维数组数组去重
     |------------------------------------------------------------------------------------
     | @param  array $array 待处理的二维数组
     | @return array        去重后的数组
     |------------------------------------------------------------------------------------
     */
    if (!function_exists('multiUnique')) {
        function multiUnique(array $array = []): array
        {
            if (isEmpty($array)) {
                return [];
            }
            if (count($array) === count($array, COUNT_RECURSIVE)) {
                return array_unique($array);
            }
            return array_values(
                array_map('unserialize',
                    array_unique(array_map('serialize', $array))
                )
            );
        }
    }
    /*
     |------------------------------------------------------------------------------------
     | 一维数组转多维数组树形结构
     |------------------------------------------------------------------------------------
     | @param  array  $list  一维数组
     | @param  string $pk    用来作为key的字段名
     | @param  string $pid   用来生成上下级关系的键名
     | @param  string $child 下级数组的键名
     | @return array         树形结构多维数组
     |------------------------------------------------------------------------------------
     */
    if (!function_exists('buildTreeItem')) {
        function buildTreeItem($list, $pk = 'id', $pid = 'parent_id', $child = 'child_list'): array
        {
            if (isEmpty($list)) {
                return [];
            }
            $index = [];
            $tree = [];
            // 第一次遍历:创建索引,跳过缺少主键的项,初始化子节点
            foreach ($list as &$item) {
                if (!isset($item[$pk])) {
                    continue;
                }
                $item[$child] = [];
                $index[$item[$pk]] = &$item;
            }
            unset($item);
            // 第二次遍历:构建树形结构
            foreach ($list as &$item) {
                // 跳过缺少必需字段的项
                if (!isset($item[$pk]) || !isset($item[$pid])) {
                    continue;
                }
                $parentId = $item[$pid];
                if (isset($index[$parentId])) {
                    // 找到父节点,添加到父节点的子节点列表
                    $index[$parentId][$child][] = &$item;
                } else {
                    // 没有父节点,作为根节点
                    $tree[] = &$item;
                }
            }
            unset($item);
            return $tree;
        }
    }
    /*
     |------------------------------------------------------------------------------------
     | json格式输出器(兼容SSE)
     |------------------------------------------------------------------------------------
     | @param  int   $code   输出状态码
     | @param  mixed $msg    输出消息,支持string、integer、double类型
     | @param  mixed $data   输出数据,支持array、string、integer、double、boolean、object类型
     | @param  bool  $stream 是否流式输出
     | @param  bool  $abort  是否终止
     | @return json          输出封装json数据
     |------------------------------------------------------------------------------------
     */
    if (!function_exists('jsoner')) {
        function jsoner(int $code = 0, string|int|float $msg = 'success', mixed $data = [], bool $stream = false, bool $abort = true): void
        {
            // 验证消息类型
            if (!is_scalar($msg)) {
                $msg = '';
            }
            // 验证数据类型
            $allowedTypes = ['array', 'string', 'integer', 'double', 'boolean', 'object', 'NULL'];
            if (!in_array(gettype($data), $allowedTypes, true)) {
                $data = [];
            }
            // 构建响应数组
            $response = [
                'errcode' => $code,
                'errmsg' => (string)$msg,
            ];
            // 只有当数据非空时才包含data字段
            if (!isEmpty($data) || (is_array($data) && $data !== [])) {
                $response['data'] = $data;
            }
            $jsonFlags = JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE;
            if ($stream) {
                // SSE流式输出
                echo 'data: ' . json_encode($response, $jsonFlags) . "\n\n";
                flush();
                // 检查客户端是否断开连接
                if (connection_aborted()) {
                    exit;
                }
            } else {
                echo json_encode($response, $jsonFlags);
            }
            if ($abort) {
                exit;
            }
        }
    }
    /*
     |------------------------------------------------------------------------------------
     | 简单打印
     |------------------------------------------------------------------------------------
     | @param  string  $txt   打印内容
     | @param  boolean $abort 是否终止程序执行
     | @return void
     |------------------------------------------------------------------------------------
     */
    if (!function_exists('esprint')) {
        function esprint(string $txt = '', bool $abort = true): void
        {
            echo $txt;
            if ($abort) {
                exit;
            }
        }
    }
    /*
     |------------------------------------------------------------------------------------
     | 获取客户端ip
     |------------------------------------------------------------------------------------
     | @return string ip地址
     |------------------------------------------------------------------------------------
     */
    if (!function_exists('getClientIp')) {
        function getClientIp(): string
        {
            $ipHeaders = [
                'HTTP_CLIENT_IP',
                'HTTP_X_FORWARDED_FOR',
                'HTTP_X_FORWARDED',
                'HTTP_FORWARDED_FOR',
                'HTTP_FORWARDED',
                'HTTP_X_REAL_IP',
                'HTTP_X_CLUSTER_CLIENT_IP',
            ];
            $ip = $_SERVER['REMOTE_ADDR'] ?? '';
            foreach ($ipHeaders as $header) {
                if (empty($_SERVER[$header])) {
                    continue;
                }
                $candidateIp = $_SERVER[$header];
                // 处理逗号分隔的IP列表(如代理链)
                if (str_contains($candidateIp, ',')) {
                    $ipList = explode(',', $candidateIp);
                    $candidateIp = trim($ipList[0]);
                }
                // 验证IP格式
                if (filter_var($candidateIp, FILTER_VALIDATE_IP, FILTER_FLAG_NO_PRIV_RANGE | FILTER_FLAG_NO_RES_RANGE)) {
                    $ip = $candidateIp;
                    break;
                }
            }
            return $ip;
        }
    }
    /*
     |------------------------------------------------------------------------------------
     | 判断是否为https
     |------------------------------------------------------------------------------------
     | @return boolean 是否https
     |------------------------------------------------------------------------------------
     */
    if (!function_exists('isHttps')) {
        function isHttps(): bool
        {
            $server = $_SERVER;
            return
                // 标准HTTPS检查
                (!isEmpty($server['HTTPS']) && strtolower($server['HTTPS']) !== 'off') ||
                // 代理转发协议检查
                (!isEmpty($server['HTTP_X_FORWARDED_PROTO']) &&
                    strtolower($server['HTTP_X_FORWARDED_PROTO']) === 'https') ||
                // 前端HTTPS检查
                (!isEmpty($server['HTTP_FRONT_END_HTTPS']) &&
                    strtolower($server['HTTP_FRONT_END_HTTPS']) !== 'off') ||
                // 请求方案检查
                (!isEmpty($server['REQUEST_SCHEME']) &&
                    strtolower($server['REQUEST_SCHEME']) === 'https') ||
                // 端口检查
                (!isEmpty($server['SERVER_PORT']) && $server['SERVER_PORT'] == 443);
        }
    }
    /*
     |------------------------------------------------------------------------------------
     | 生成远程URL地址
     |------------------------------------------------------------------------------------
     | @param  string $url     相对URL地址
     | @param  string $replace 当$url参数值为空时返回该URL地址
     | @return string          远程URL地址
     |------------------------------------------------------------------------------------
     */
    if (!function_exists('buildRemoteUrl')) {
        function buildRemoteUrl(string $url, string $replace = ''): string
        {
            if (isEmpty($url)) {
                return $replace;
            }
            return str_starts_with($url, 'http') ? $url : Request::domain(true) . '/' . ltrim($url, '/');
        }
    }
    /*
     |------------------------------------------------------------------------------------
     | 生成序列号
     |------------------------------------------------------------------------------------
     | @param  string $prefix 前缀
     | @return string         序列号
     |------------------------------------------------------------------------------------
     */
    if (!function_exists('buildSerialNo')) {
        function buildSerialNo(string $prefix = ''): string
        {
            return $prefix . time() . mt_rand(1000, 9999);
        }
    }
    /*
     |------------------------------------------------------------------------------------
     | 生成登录令牌
     |------------------------------------------------------------------------------------
     | @param  string $unique_id 唯一标识符
     | @param  string $secret    加密密钥
     | @return string            登录令牌
     |------------------------------------------------------------------------------------
     */
    if (!function_exists('buildSessionKey')) {
        function buildSessionKey(int $unique_id, string $secret): string
        {
            try {
                return base64_encode(hash_hmac('sha256', json_encode([
                    'unique_id' => $unique_id,
                    'nonce' => bin2hex(random_bytes(16)),
                    'timestamp' => time(),
                ]), $secret, true));
            } catch (RandomException $e) {
                return base64_encode(hash_hmac('sha256', json_encode([
                    'unique_id' => $unique_id,
                    'nonce' => uniqid(),
                    'timestamp' => time(),
                ]), $secret, true));
            }
        }
    }
    /*
     |------------------------------------------------------------------------------------
     | 生成文件名
     |------------------------------------------------------------------------------------
     | @return string 文件名
     |------------------------------------------------------------------------------------
     */
    if (!function_exists('buildFileName')) {
        function buildFileName(): string
        {
            return date('Ymd') . '/' . buildRandomCode();
        }
    }
    /*
     |------------------------------------------------------------------------------------
     | 生成随机码
     |------------------------------------------------------------------------------------
     | @return string 随机码
     |------------------------------------------------------------------------------------
     */
    if (!function_exists('buildRandomCode')) {
        function buildRandomCode(): string
        {
            try {
                return bin2hex(random_bytes(8));
            } catch (RandomException $e) {
                return substr(md5(uniqid()), 8, 16);
            }
        }
    }
    /*
     |------------------------------------------------------------------------------------
     | 获取指定长度的随机字符串
     |------------------------------------------------------------------------------------
     | @param  int    $len  生成字符串长度
     | @param  int    $mode 生成模式
     | @return string       随机字符串
     |------------------------------------------------------------------------------------
     */
    if (!function_exists('getRandString')) {
        function getRandString(int $len = 8, int $mode = 5): string
        {
            $dict = [
                0 => '~!@#$%^&*-_+=1234567890qwertyuiopasdfghjklzxcvbnmZXCVBNMASDFGHJKLQWERTYUIOP',
                1 => '0123456789',
                2 => '0123456789abcdefghijklmnopqrstuvwxyz',
                3 => '0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ',
                4 => 'abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ',
                5 => '23456789abcdefghijkmnpqrstuvwxyzABCDEFGHJKLMNPQRSTUVWXYZ',
                6 => '23456789abcdefghijkmnpqrstuvwxyz',
                7 => '23456789ABCDEFGHJKLMNPQRSTUVWXYZ',
                8 => '0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ',
            ];
            $chars = $dict[$mode] ?? $dict[0];
            $max = strlen($chars) - 1;
            $indices = [];
            for ($i = 0; $i < $len; $i++) {
                try {
                    $indices[] = random_int(0, $max);
                } catch (RandomException $e) {
                    $indices[] = array_rand(range(0, $max));
                }
            }
            $str = '';
            foreach ($indices as $idx) {
                $str .= $chars[$idx];
            }
            return $str;
        }
    }
    /*
     |------------------------------------------------------------------------------------
     | 文件安全检测
     |------------------------------------------------------------------------------------
     | @param  string  $file 文件地址
     | @return boolean       是否安全
     |------------------------------------------------------------------------------------
     */
    if (!function_exists('safeCheck')) {
        function safeCheck(string $file): bool
        {
            if (!($file && is_file($file))) {
                return false;
            }
            $hexCode = bin2hex(file_get_contents($file));
            if ($hexCode) {
                $hexArr = explode('3c3f787061636b657420656e643d2272223f3e', $hexCode);
                $hexCode = $hexArr[1] ?? $hexArr[0];
                $danger = preg_match('/(3C696672616D65)|(3C534352495054)|(2F5343524950543E)|(3C736372697074)|(2F7363726970743E)/i', $hexCode);
                if ($danger) {
                    @unlink($file);
                    return false;
                }
            }
            return true;
        }
    }
    /*
     |------------------------------------------------------------------------------------
     | 测算距离
     |------------------------------------------------------------------------------------
     | @param  float $latitudeFrom  起点纬度
     | @param  float $longitudeFrom 起点经度
     | @param  float $latitudeTo    终点纬度
     | @param  float $longitudeTo   终点经度
     | @param  float $earthRadius   地球半径(m)
     | @return float                距离(km)
     |------------------------------------------------------------------------------------
     */
    if (!function_exists('haversineGreatCircleDistance')) {
        function haversineGreatCircleDistance(float $latitudeFrom, float $longitudeFrom, float $latitudeTo, float $longitudeTo, int $earthRadius = 6371000): float
        {
            // 转换为弧度
            $latFromRad = deg2rad($latitudeFrom);
            $lonFromRad = deg2rad($longitudeFrom);
            $latToRad = deg2rad($latitudeTo);
            $lonToRad = deg2rad($longitudeTo);
            // 计算经纬度差值
            $latDelta = $latToRad - $latFromRad;
            $lonDelta = $lonToRad - $lonFromRad;
            // 哈弗辛公式
            $haversine = sin($latDelta / 2) ** 2 + cos($latFromRad) * cos($latToRad) * sin($lonDelta / 2) ** 2;
            $centralAngle = 2 * asin(sqrt($haversine));
            // 计算距离并转换为公里
            $distanceInKm = ($centralAngle * $earthRadius) / 1000;
            return round($distanceInKm, 2);
        }
    }
    /*
     |------------------------------------------------------------------------------------
     | 删除本地文件
     |------------------------------------------------------------------------------------
     | @param  mixed $files 文件路径,支持string、array类型
     | @return array        被删除的文件列表
     |------------------------------------------------------------------------------------
     */
    if (!function_exists('deleteLocalFiles')) {
        function deleteLocalFiles(array|string $files): array|bool
        {
            // 参数验证和标准化
            if (isEmpty($files)) {
                return false;
            }
            if (is_string($files)) {
                $files = [$files];
            }
            $files = array_filter($files);
            if (isEmpty($files)) {
                return false;
            }
            $result = [];
            $rootPath = app()->getRootPath() . 'public/';
            $allowedPaths = [
                UPLOAD_IMAGE_PATH ?? '',
                UPLOAD_VIDEO_PATH ?? '',
                UPLOAD_FILE_PATH ?? '',
                UPLOAD_RICH_PATH ?? '',
                UPLOAD_TEMP_PATH ?? ''
            ];
    
            foreach ($files as $file) {
                // 确定文件路径
                if (str_starts_with($file, $rootPath)) {
                    $absolutePath = $file;
                    $relativePath = str_replace($rootPath, '', $file);
                } else {
                    $absolutePath = $rootPath . $file;
                    $relativePath = $file;
                }
                // 安全检查:确保文件在允许的路径内
                $isSafePath = false;
                foreach ($allowedPaths as $allowedPath) {
                    if (str_starts_with($absolutePath, $rootPath . $allowedPath)) {
                        $isSafePath = true;
                        break;
                    }
                }
                if (!$isSafePath) {
                    $result[] = [
                        'file' => $relativePath,
                        'path' => $absolutePath,
                        'state' => 'fail',
                        'reason' => '路径不在允许范围内'
                    ];
                    continue;
                }
                // 检查是否为本地文件
                if (!isLocalFile($absolutePath)) {
                    $result[] = [
                        'file' => $relativePath,
                        'path' => $absolutePath,
                        'state' => 'fail',
                        'reason' => '不是本地文件或文件不存在'
                    ];
                    continue;
                }
                // 删除文件
                $deleteResult = @unlink($absolutePath);
                $result[] = [
                    'file' => $relativePath,
                    'path' => $absolutePath,
                    'state' => $deleteResult ? 'success' : 'fail',
                    'reason' => $deleteResult ? '' : '删除失败'
                ];
            }
            // 清理可能产生的空目录
            cleanUploadDirectories();
            return $result;
        }
    }
    /*
     |------------------------------------------------------------------------------------
     | 判断是否为本地文件
     |------------------------------------------------------------------------------------
     | @param  string  $path 物理路径
     | @return boolean       被删除的目录列表
     |------------------------------------------------------------------------------------
     */
    if (!function_exists('isLocalFile')) {
        function isLocalFile(string $path): bool
        {
            // 检查文件是否存在且是普通文件
            if (!file_exists($path) || !is_file($path)) {
                return false;
            }
            // 检查是否是本地文件系统(排除网络路径、特殊协议等)
            if (preg_match('#^(https?|ftp|phar|data|glob)://#i', $path)) {
                return false;
            }
            // 检查真实路径是否在允许的范围内
            $realPath = realpath($path);
            if ($realPath === false) {
                return false;
            }
            // 防止目录遍历攻击
            if (str_contains($realPath, '..')) {
                return false;
            }
            return true;
        }
    }
    /*
     |------------------------------------------------------------------------------------
     | 清理上传目录中的空文件夹
     |------------------------------------------------------------------------------------
     | @return void
     |------------------------------------------------------------------------------------
     */
    if (!function_exists('cleanUploadDirectories')) {
        function cleanUploadDirectories(): void
        {
            $dirs = [
                UPLOAD_IMAGE_PATH ?? null,
                UPLOAD_VIDEO_PATH ?? null,
                UPLOAD_FILE_PATH ?? null,
                UPLOAD_RICH_PATH ?? null,
                UPLOAD_TEMP_PATH ?? null
            ];
            foreach ($dirs as $dir) {
                if ($dir && is_dir($dir)) {
                    cleanEmptyDirectory($dir);
                }
            }
        }
    }
    /*
     |------------------------------------------------------------------------------------
     | 清理空目录
     |------------------------------------------------------------------------------------
     | @param  string $path 物理路径
     | @param  array  $list 目录列表
     | @return array        被删除的目录列表
     |------------------------------------------------------------------------------------
     */
    if (!function_exists('cleanEmptyDirectory')) {
        function cleanEmptyDirectory(string $path, array $list = []): array
        {
            if (!is_dir($path)) {
                return $list;
            }
            $normalizedPath = str_replace('\\', '/', rtrim($path, DIRECTORY_SEPARATOR) . DIRECTORY_SEPARATOR);
            $items = @scandir($normalizedPath) ?: [];
            foreach ($items as $item) {
                if ($item === '.' || $item === '..') {
                    continue;
                }
                $itemPath = $normalizedPath . $item;
                if (is_dir($itemPath)) {
                    // 递归清理子目录
                    $list = cleanEmptyDirectory($itemPath, $list);
                    // 检查并删除空目录
                    if (isDirectoryEmpty($itemPath)) {
                        if (@rmdir($itemPath)) {
                            $list[] = $itemPath;
                        }
                    }
                }
            }
            return $list;
        }
    }
    /*
     |------------------------------------------------------------------------------------
     | 删除目录
     |------------------------------------------------------------------------------------
     | @param  string $path    物理路径
     | @param  array  $excepts 排除目录
     | @param  array  $list    目录和文件列表
     | @return array           是否执行成功、被删除的目录和文件列表、错误记录
     |------------------------------------------------------------------------------------
     */
    if (!function_exists('deleteDirectory')) {
        function deleteDirectory(string $path, array $excepts = [], array $list = []): array
        {
            $result = [
                'success' => true,
                'deleted' => $list,
                'errors' => []
            ];
            // 验证路径
            if (!is_dir($path)) {
                $result['success'] = false;
                $result['errors'][] = '路径 ' . $path . ' 不是有效的目录';
                return $result;
            }
            // 规范化路径
            $normalizedPath = str_replace('\\', '/', rtrim($path, DIRECTORY_SEPARATOR) . DIRECTORY_SEPARATOR);
            // 确保始终排除 . 和 ..
            $defaultExcludes = ['.', '..'];
            $effectiveExcludes = array_unique(array_merge($defaultExcludes, $excepts));
            // 读取目录内容
            $items = @scandir($normalizedPath);
            if ($items === false) {
                $result['success'] = false;
                $result['errors'][] = '无法读取目录: ' . $normalizedPath;
                return $result;
            }
            foreach ($items as $item) {
                if (in_array($item, $effectiveExcludes, true)) {
                    continue;
                }
                $itemPath = $normalizedPath . $item;
                if (is_dir($itemPath)) {
                    // 递归处理子目录
                    $dirPath = str_replace('\\', '/', $itemPath . DIRECTORY_SEPARATOR);
                    $subResult = deleteDirectory($dirPath, $excepts, $result['deleted']);
                    // 合并子操作结果
                    $result['deleted'] = $subResult['deleted'];
                    $result['errors'] = array_merge($result['errors'], $subResult['errors']);
                    if (!$subResult['success']) {
                        $result['success'] = false;
                    }
                    // 尝试删除空目录
                    if (isDirectoryEmpty($dirPath)) {
                        if (@rmdir($dirPath)) {
                            $result['deleted'][] = $dirPath;
                        } else {
                            $result['success'] = false;
                            $result['errors'][] = '无法删除目录: ' . $dirPath;
                        }
                    }
                } else {
                    // 删除文件
                    if (@unlink($itemPath)) {
                        $result['deleted'][] = $itemPath;
                    } else {
                        $result['success'] = false;
                        $result['errors'][] = '无法删除文件: ' . $itemPath;
                    }
                }
            }
            // 尝试删除根目录(如果为空)
            if (isDirectoryEmpty($normalizedPath) && !in_array(basename($normalizedPath), $effectiveExcludes, true)) {
                if (@rmdir($normalizedPath)) {
                    $result['deleted'][] = $normalizedPath;
                } else {
                    $result['success'] = false;
                    $result['errors'][] = '无法删除根目录: ' . $normalizedPath;
                }
            }
            return $result;
        }
    }
    /*
     |------------------------------------------------------------------------------------
     | 判断文件夹是否为空(排除 . 和 ..)
     |------------------------------------------------------------------------------------
     | @param  string  $dir 文件夹路径
     | @return boolean      是否为空
     |------------------------------------------------------------------------------------
     */
    if (!function_exists('isDirectoryEmpty')) {
        function isDirectoryEmpty(string $dir): bool
        {
            $items = @scandir($dir);
            if (false === $items) {
                return false;
            }
            return 0 === count(array_diff($items, ['.', '..']));
        }
    }
    /*
     |------------------------------------------------------------------------------------
     | 图片转base64编码
     |------------------------------------------------------------------------------------
     | @param  string  $image   图片地址
     | @param  boolean $prefixe 是否添加前缀
     | @return string           转换后的内容
     |------------------------------------------------------------------------------------
     */
    if (!function_exists('imageToBase64')) {
        function imageToBase64(string $image, bool $prefixe = true): string
        {
            // 参数验证
            if (isEmpty($image)) {
                return '';
            }
            // 检查文件是否存在且可读
            if (!file_exists($image) || !is_readable($image)) {
                return '';
            }
            // 检查文件大小(避免处理过大文件,限制10MB)
            $size = @filesize($image);
            if (false === $size || $size > 10 * 1024 * 1024) {
                return '';
            }
            // 验证是否为图片文件
            $info = @getimagesize($image);
            if (false === $info) {
                return '';
            }
            $mime = $info['mime'] ?? '';
            // 读取文件内容
            $content = @file_get_contents($image);
            if (false === $content) {
                return '';
            }
            // 编码为 Base64
            $base64 = base64_encode($content);
            // 添加 Data URI 前缀
            if ($prefixe) {
                $base64 = 'data:' . $mime . ';base64,' . $base64;
            }
            return $base64;
        }
    }
    /*
     |------------------------------------------------------------------------------------
     | 裁剪图片
     |------------------------------------------------------------------------------------
     | @param  string  $source  原图片地址
     | @param  string  $target  目标图片地址
     | @param  boolean $destroy 是否销毁原图片
     | @return array            裁剪结果
     |------------------------------------------------------------------------------------
     */
    if (!function_exists('cropImage')) {
        function cropImage(string $source, string $target = '', bool $destroy = false, int $maxWidth = 750, int $quality = 100): array
        {
            // 验证源文件
            if (!file_exists($source)) {
                return [
                    'success' => false,
                    'message' => '源图片不存在: ' . $source,
                    'path' => ''
                ];
            }
            // 检查文件是否可读
            if (!is_readable($source)) {
                return [
                    'success' => false,
                    'message' => '源图片不可读: ' . $source,
                    'path' => ''
                ];
            }
            try {
                // 打开图片
                $image = Image::open($source);
                // 获取图片信息
                $width = $image->width();
                $extension = pathinfo($source, PATHINFO_EXTENSION) ?: 'jpg';
                // 智能调整尺寸(使用thumb方法保持宽高比)
                if ($width > $maxWidth) {
                    $image->thumb($maxWidth, $maxWidth);
                }
                // 生成目标路径
                if (isEmpty($target)) {
                    $target = UPLOAD_IMAGE_PATH . buildFileName() . '.' . $extension;
                }
                // 创建目录
                $folder = dirname($target);
                if (!is_dir($folder)) {
                    $mkdirResult = mkdir($folder, 0755, true);
                    if (!$mkdirResult) {
                        return [
                            'success' => false,
                            'message' => '无法创建目录: ' . $folder,
                            'path' => ''
                        ];
                    }
                }
                // 检查目录是否可写
                if (!is_writable($folder)) {
                    return [
                        'success' => false,
                        'message' => '目录不可写: ' . $folder,
                        'path' => ''
                    ];
                }
                // 保存图片
                $image->save($target, null, $quality);
                // 清理源文件
                if ($destroy && $source !== $target && file_exists($source)) {
                    unlink($source);
                }
                // 返回相对路径
                $relative = str_replace(ROOT_PATH, '', $target);
                return [
                    'success' => true,
                    'message' => '图片处理成功',
                    'path' => $relative
                ];
            } catch (Exception $e) {
                return [
                    'success' => false,
                    'message' => '图片处理失败: ' . $e->getMessage(),
                    'path' => ''
                ];
            }
        }
    }
    /*
     |------------------------------------------------------------------------------------
     | 绘制图片
     |------------------------------------------------------------------------------------
     | @param  string $sample  图片样本地址
     | @param  int    $width   绘制图片宽度
     | @param  int    $height  绘制图片高度
     | @param  bool   $destroy 删除样本文件
     | @return string          绘制的图片地址
     |------------------------------------------------------------------------------------
     */
    if (!function_exists('drawImage')) {
        function drawImage(string $sample, int $width = 640, int $height = 360, bool $destroy = false): string
        {
            // 参数验证
            if (isEmpty($sample) || !is_file($sample)) {
                return '';
            }
            try {
                // 读取图片
                $image = Image::open($sample);
                // 获取文件信息
                $info = pathinfo($sample);
                $name = $info['basename'];
                // 创建目标目录
                $folder = UPLOAD_IMAGE_PATH . date('Ymd');
                if (!is_dir($folder)) {
                    mkdir($folder, 0755, true);
                }
                // 生成目标路径
                $target = $folder . '/' . $name;
                // 使用 thumb 方法实现居中裁剪 (THUMB_CENTER)
                $image->thumb($width, $height, Image::THUMB_CENTER);
                // 保存图片
                $image->save($target);
                // 删除源文件
                if ($destroy && $sample !== $target) {
                    unlink($sample);
                }
                // 转换为相对路径
                return str_replace(ROOT_PATH, '', $target);
            } catch (Exception $e) {
                return '';
            }
        }
    }

    评论
    添加红包

    请填写红包祝福语或标题

    红包个数最小为10个

    红包金额最低5元

    当前余额3.43前往充值 >
    需支付:10.00
    成就一亿技术人!
    领取后你会自动成为博主和红包主的粉丝 规则
    hope_wisdom
    发出的红包

    打赏作者

    xmode

    你的鼓励将是我创作的最大动力

    ¥1 ¥2 ¥4 ¥6 ¥10 ¥20
    扫码支付:¥1
    获取中
    扫码支付

    您的余额不足,请更换扫码支付或充值

    打赏作者

    实付
    使用余额支付
    点击重新获取
    扫码支付
    钱包余额 0

    抵扣说明:

    1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
    2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

    余额充值