推荐使用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 '';
}
}
}