常用自定义函数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 '';
        }
    }
}
相关推荐
Zfox_42 分钟前
【Go】环境搭建与基本使用
开发语言·后端·golang
w***H6501 小时前
Springboot项目:使用MockMvc测试get和post接口(含单个和多个请求参数场景)
java·spring boot·后端
a***13141 小时前
Spring Boot 条件注解:@ConditionalOnProperty 完全解析
java·spring boot·后端
tgethe1 小时前
Java注解
java·后端
武子康1 小时前
大数据-170 Elasticsearch 7.3.0 三节点集群实战:目录/参数/启动到联机
大数据·后端·elasticsearch
稚辉君.MCA_P8_Java1 小时前
DeepSeek Java 多线程打印的12种实现方法
java·linux·后端·架构·maven
JienDa1 小时前
JienDa聊PHP:电商系统实战架构深度解析与优化策略
开发语言·架构·php
w***48821 小时前
Springboot 3项目整合Knife4j接口文档(接口分组详细教程)
java·spring boot·后端
k***45991 小时前
SpringBoot【实用篇】- 测试
java·spring boot·后端