webman迁移

This commit is contained in:
2026-03-18 11:22:12 +08:00
parent dab3b3148f
commit ea77c7b3a1
623 changed files with 38163 additions and 106 deletions

View File

@@ -22,6 +22,26 @@ class ClickCaptcha
'static/fonts/zhttfs/SourceHanSansCN-Normal.ttf',
];
/** 当项目字体不存在时的系统字体回退路径(支持中文,需 .ttf 格式) */
private function getFontFallbacks(): array
{
$fallbacks = [];
if (str_starts_with(strtoupper(PHP_OS_FAMILY), 'WIN')) {
$winFonts = (getenv('SystemRoot') ?: getenv('WINDIR') ?: 'C:\\Windows') . '\\Fonts\\';
$fallbacks = [
$winFonts . 'simhei.ttf', // 黑体
$winFonts . 'msyh.ttc', // 微软雅黑
$winFonts . 'simsun.ttc', // 宋体
];
} else {
$fallbacks = [
'/usr/share/fonts/truetype/wqy/wqy-microhei.ttf',
'/usr/share/fonts/truetype/wqy/wqy-microhei.ttc',
];
}
return $fallbacks;
}
private array $iconDict = [
'aeroplane' => '飞机',
'apple' => '苹果',
@@ -58,23 +78,116 @@ class ClickCaptcha
Db::name('captcha')->where('expire_time', '<', time())->delete();
}
/** 将 public 下的相对路径转为绝对路径public_path 无尾部分隔符时也能正确拼接) */
private function path(string $relativePath): string
{
return Filesystem::fsFit(rtrim(public_path(), '/\\') . DIRECTORY_SEPARATOR . ltrim($relativePath, '/\\'));
}
/**
* 获取可用的字体路径,优先使用项目字体,不存在时回退到系统字体或配置中的 font_path
*/
protected function getFontPath(): string
{
$customFont = $this->config['font_path'] ?? config('buildadmin.click_captcha.font_path', '');
if (!empty($customFont)) {
$fullPath = Filesystem::fsFit($customFont);
if (is_file($fullPath)) {
return $fullPath;
}
}
foreach ($this->fontPaths as $path) {
$fullPath = $this->path($path);
if (is_file($fullPath)) {
return $fullPath;
}
}
foreach ($this->getFontFallbacks() as $path) {
$fullPath = Filesystem::fsFit($path);
if (is_file($fullPath)) {
return $fullPath;
}
}
throw new \RuntimeException(
'点选验证码需要中文字体文件。请将 SourceHanSansCN-Normal.ttf 放入 public/static/fonts/zhttfs/ 目录,' .
'或在 config/buildadmin.php 的 click_captcha.font_path 中指定系统字体路径(如 Windows: C:\\Windows\\Fonts\\simhei.ttf'
);
}
/** 当无背景图时创建简易纯色背景300x150 */
private function createFallbackBg(): string
{
$w = 300;
$h = 150;
$img = imagecreatetruecolor($w, $h);
imagefill($img, 0, 0, imagecolorallocate($img, 245, 245, 245));
$dir = runtime_path('captcha');
if (!is_dir($dir)) {
mkdir($dir, 0755, true);
}
$path = $dir . DIRECTORY_SEPARATOR . 'fallback_bg_' . uniqid() . '.png';
imagepng($img, $path);
imagedestroy($img);
return $path;
}
/** 获取存在的背景图路径列表 */
private function getAvailableBgPaths(): array
{
$available = [];
foreach ($this->bgPaths as $p) {
if (is_file($this->path($p))) {
$available[] = $p;
}
}
return $available;
}
/** 获取存在的图标名列表(文件存在的才加入) */
private function getAvailableIcons(): array
{
$available = [];
foreach (array_keys($this->iconDict) as $name) {
$iconPath = $this->path('static/images/captcha/click/icons/' . $name . '.png');
if (is_file($iconPath)) {
$available[] = $name;
}
}
return $available;
}
public function creat(string $id): array
{
$imagePath = Filesystem::fsFit(public_path() . $this->bgPaths[mt_rand(0, count($this->bgPaths) - 1)]);
$fontPath = Filesystem::fsFit(public_path() . $this->fontPaths[mt_rand(0, count($this->fontPaths) - 1)]);
$randPoints = $this->randPoints($this->config['length'] + $this->config['confuse_length']);
$availableBgs = $this->getAvailableBgPaths();
$imagePath = !empty($availableBgs)
? $this->path($availableBgs[mt_rand(0, count($availableBgs) - 1)])
: $this->createFallbackBg();
$fontPath = $this->getFontPath();
$randPoints = $this->randPoints($this->config['length'] + $this->config['confuse_length']);
$lang = config('translation.locale', config('lang.default_lang', 'zh-cn'));
$lang = config('translation.locale', config('lang.default_lang', 'zh-cn'));
$isZhCn = str_starts_with(strtolower(str_replace('_', '-', (string)$lang)), 'zh-cn');
foreach ($randPoints as $v) {
$tmp['size'] = rand(15, 30);
if (isset($this->iconDict[$v])) {
$tmp['icon'] = true;
$tmp['name'] = $v;
$tmp['text'] = $lang == 'zh-cn' ? "<{$this->iconDict[$v]}>" : "<$v>";
$iconInfo = getimagesize(Filesystem::fsFit(public_path() . 'static/images/captcha/click/icons/' . $v . '.png'));
$tmp['width'] = $iconInfo[0];
$tmp['height'] = $iconInfo[1];
$iconPath = $this->path('static/images/captcha/click/icons/' . $v . '.png');
if (!is_file($iconPath)) {
$fontArea = imagettfbbox($tmp['size'], 0, $fontPath, $this->iconDict[$v]);
$textWidth = $fontArea[2] - $fontArea[0];
$textHeight = $fontArea[1] - $fontArea[7];
$tmp['icon'] = false;
$tmp['text'] = $isZhCn ? $this->iconDict[$v] : $v;
$tmp['width'] = $textWidth;
$tmp['height'] = $textHeight;
} else {
$tmp['icon'] = true;
$tmp['name'] = $v;
$tmp['text'] = $isZhCn ? "<{$this->iconDict[$v]}>" : "<$v>";
$iconInfo = getimagesize($iconPath);
$tmp['width'] = $iconInfo[0];
$tmp['height'] = $iconInfo[1];
}
} else {
$fontArea = imagettfbbox($tmp['size'], 0, $fontPath, $v);
$textWidth = $fontArea[2] - $fontArea[0];
@@ -184,7 +297,7 @@ class ClickCaptcha
protected function iconCover($bgImg, $iconImgData): void
{
$iconImage = imagecreatefrompng(Filesystem::fsFit(public_path() . 'static/images/captcha/click/icons/' . $iconImgData['name'] . '.png'));
$iconImage = imagecreatefrompng($this->path('static/images/captcha/click/icons/' . $iconImgData['name'] . '.png'));
$trueColorImage = imagecreatetruecolor($iconImgData['width'], $iconImgData['height']);
imagecopy($trueColorImage, $bgImg, 0, 0, $iconImgData['x'], $iconImgData['y'], $iconImgData['width'], $iconImgData['height']);
imagecopy($trueColorImage, $iconImage, 0, 0, 0, 0, $iconImgData['width'], $iconImgData['height']);
@@ -201,14 +314,22 @@ class ClickCaptcha
}
if (in_array('icon', $this->config['mode'])) {
$icon = array_keys($this->iconDict);
shuffle($icon);
$icon = array_slice($icon, 0, $length);
$arr = array_merge($arr, $icon);
$availableIcons = $this->getAvailableIcons();
if (!empty($availableIcons)) {
shuffle($availableIcons);
$icon = array_slice($availableIcons, 0, $length);
$arr = array_merge($arr, $icon);
}
}
shuffle($arr);
return array_slice($arr, 0, $length);
$result = array_slice($arr, 0, $length);
if (empty($result)) {
for ($i = 0; $i < $length; $i++) {
$result[] = mb_substr($this->config['zhSet'], mt_rand(0, mb_strlen($this->config['zhSet'], 'utf-8') - 1), 1, 'utf-8');
}
}
return $result;
}
private function randPosition(array $textArr, int $imgW, int $imgH, int $fontW, int $fontH, bool $isIcon): array