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

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

【包含功能】

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

laravel安装类库

bash 复制代码
composer require intervention/image

thinkphp安装类库

bash 复制代码
composer require topthink/think-image

使用前定义全局常量

php 复制代码
<?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 复制代码
<?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文件配置,使其自动加载

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

2、thinkphp版(app\common.php)

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 '';
        }
    }
}
相关推荐
爱勇宝3 小时前
第 1 章:别把“需求文档”当成真正的需求
前端·后端·程序员
IT_陈寒7 小时前
闭包陷阱让我加了两天班,JavaScript你真行
前端·人工智能·后端
易协同低代码9 小时前
通达OA核心类库TD类深度解析
后端
Gopher_HBo9 小时前
Go语言学习笔记(十八)Gin处理Session
后端
谭光志9 小时前
工具塞满上下文窗口怎么办?深度拆解 AI Agent Tool Search 按需加载实现原理
前端·后端·ai编程
她说..9 小时前
Java 默认值设置方式
java·开发语言·后端·springboot
foggyprojects10 小时前
从0开始,一句话启动AI DataAgent
后端·数据分析·ai编程
郡杰10 小时前
一些基础和问题解决
后端
陈随易10 小时前
前端项目部署只要30秒
前端·后端·程序员
YIAN10 小时前
从零手写文件读取 MCP 服务:一文吃透 Model Context Protocol 全链路通信原理
前端·后端·mcp