<?php
/**
 * 芋头CMS
 * ============================================================================
 * 版权所有 2016-2028 沅陵县乌托邦网络服务有限公司，并保留所有权利。
 * 网站地址: http://www.yutoucms.cn
 * ----------------------------------------------------------------------------
 * 如果商业用途务必到官方购买正版授权, 以免引起不必要的法律纠纷.
 * ============================================================================
 * Author: 芋头 <mxgsf@qq.com>
 * Date: 2026-5-20
 */



/**
 * 获取站点配置
 */
function getSiteConfig($key = '', $default = '')
{
    static $configs = null;
    if ($configs === null) {
        $db = \Core\Db\DB::getInstance();
        $rows = $db->fetchAll("SELECT config_key, config_value FROM {$db->table('config')}");
        $configs = [];
        foreach ($rows as $row) {
            $configs[$row['config_key']] = $row['config_value'];
        }
    }
    if ($key === '') {
        return $configs;
    }
    return $configs[$key] ?? $default;
}

function cmsIsLocalDebugRequest(array $server = null)
{
    $server = $server === null ? $_SERVER : $server;
    $remoteAddress = trim((string) ($server['REMOTE_ADDR'] ?? ''));
    if (!in_array($remoteAddress, ['127.0.0.1', '::1'], true)) { return false; }
    $host = strtolower(trim((string) ($server['HTTP_HOST'] ?? '')));
    $host = preg_replace('/:\d+$/', '', $host);
    return in_array($host, ['localhost', '127.0.0.1', '[::1]', '::1'], true);
}

/**
 * 检查 GIF 是否为动图，动图不做 WebP 转换，避免丢帧。
 */
function cmsIsAnimatedGif($filePath)
{
    if (!is_file($filePath) || !is_readable($filePath)) {
        return false;
    }
    $handle = fopen($filePath, 'rb');
    if (!$handle) {
        return false;
    }
    $chunk = '';
    $frames = 0;
    while (!feof($handle) && $frames < 2) {
        $chunk .= fread($handle, 1024 * 100);
        $frames += preg_match_all('#\x00\x21\xF9\x04.{4}\x00[\x2C\x21]#s', $chunk, $matches);
        if (strlen($chunk) > 1024 * 200) {
            $chunk = substr($chunk, -1024 * 50);
        }
    }
    fclose($handle);
    return $frames > 1;
}

function cmsSafeImageExtension($mimeType, $originalName = '')
{
    $extensions = [
        'image/jpeg' => 'jpg',
        'image/jpg' => 'jpg',
        'image/png' => 'png',
        'image/gif' => 'gif',
        'image/webp' => 'webp',
        'image/bmp' => 'bmp',
        'image/x-ms-bmp' => 'bmp',
        'image/svg+xml' => 'svg',
        'image/x-icon' => 'ico',
        'image/vnd.microsoft.icon' => 'ico',
    ];
    if (isset($extensions[$mimeType])) { return $extensions[$mimeType]; }
    $originalExt = strtolower(pathinfo((string) $originalName, PATHINFO_EXTENSION));
    return in_array($originalExt, array_values($extensions), true) ? $originalExt : 'jpg';
}

/**
 * 统一保存上传图片：优先转为 WebP，失败时自动回退原格式。
 */
function cmsSaveUploadedImage($tmpPath, $originalName, $targetDir, $baseName = 'img', $options = [])
{
    $quality = isset($options['quality']) ? intval($options['quality']) : 82;
    if ($quality <= 0 || $quality > 100) {
        $quality = 82;
    }
    if (!is_dir($targetDir) && !mkdir($targetDir, 0755, true) && !is_dir($targetDir)) {
        return ['success' => false, 'message' => '创建上传目录失败'];
    }
    $finfo = finfo_open(FILEINFO_MIME_TYPE);
    $mimeType = $finfo ? finfo_file($finfo, $tmpPath) : '';
    if ($finfo) {
        finfo_close($finfo);
    }
    $originalExt = cmsSafeImageExtension($mimeType, $originalName);
    $safeBaseName = preg_replace('/[^A-Za-z0-9_\-]/', '_', (string) $baseName);
    $safeBaseName = trim($safeBaseName, '_');
    if ($safeBaseName === '') {
        $safeBaseName = 'img';
    }
    $fileToken = $safeBaseName . '_' . time() . '_' . substr(md5(uniqid('', true)), 0, 10);
    $targetDir = rtrim($targetDir, '/\\') . DIRECTORY_SEPARATOR;

    $canConvertToWebp = function_exists('imagewebp') && function_exists('imagecreatefromjpeg') && function_exists('imagecreatefrompng');
    $shouldKeepOriginal = false;
    if ($mimeType === 'image/gif' && cmsIsAnimatedGif($tmpPath)) {
        $shouldKeepOriginal = true;
    }
    if ($mimeType === 'image/svg+xml' || $mimeType === 'image/x-icon' || $mimeType === 'image/vnd.microsoft.icon') {
        $shouldKeepOriginal = true;
    }

    if (!$shouldKeepOriginal && $canConvertToWebp) {
        $image = null;
        if ($mimeType === 'image/jpeg' || $mimeType === 'image/jpg') {
            $image = @imagecreatefromjpeg($tmpPath);
        } elseif ($mimeType === 'image/png') {
            $image = @imagecreatefrompng($tmpPath);
        } elseif ($mimeType === 'image/webp' && function_exists('imagecreatefromwebp')) {
            $image = @imagecreatefromwebp($tmpPath);
        } elseif ($mimeType === 'image/gif' && function_exists('imagecreatefromgif')) {
            $image = @imagecreatefromgif($tmpPath);
        } elseif ($mimeType === 'image/bmp' && function_exists('imagecreatefrombmp')) {
            $image = @imagecreatefrombmp($tmpPath);
        } elseif ($mimeType === 'image/x-ms-bmp' && function_exists('imagecreatefrombmp')) {
            $image = @imagecreatefrombmp($tmpPath);
        }

        if ($image) {
            imagepalettetotruecolor($image);
            imagealphablending($image, false);
            imagesavealpha($image, true);
            $targetName = $fileToken . '.webp';
            $targetPath = $targetDir . $targetName;
            $saved = @imagewebp($image, $targetPath, $quality);
            imagedestroy($image);
            if ($saved && is_file($targetPath)) {
                return [
                    'success' => true,
                    'filename' => $targetName,
                    'path' => $targetPath,
                    'mime' => 'image/webp',
                    'converted' => true,
                    'extension' => 'webp'
                ];
            }
        }
    }

    $targetName = $fileToken . '.' . $originalExt;
    $targetPath = $targetDir . $targetName;
    if (!move_uploaded_file($tmpPath, $targetPath)) {
        return ['success' => false, 'message' => '图片保存失败'];
    }

    return [
        'success' => true,
        'filename' => $targetName,
        'path' => $targetPath,
        'mime' => $mimeType,
        'converted' => false,
        'extension' => $originalExt
    ];
}

/**
 * 检查数据表字段是否存在
 */
function cmsVersionCompare($left, $right)
{
    $left = trim((string) $left);
    $right = trim((string) $right);
    if ($left === '' && $right === '') {
        return 0;
    }
    if ($left === '') {
        return -1;
    }
    if ($right === '') {
        return 1;
    }
    return version_compare($left, $right);
}

function cmsFetchRemoteUpdateInfo($timeout = 3)
{
    static $cache = [];
    $timeout = max(1, intval($timeout));
    if (array_key_exists($timeout, $cache)) {
        return $cache[$timeout];
    }

    $baseUrl = rtrim((string) (defined('LICENSE_CENTER_URL') ? LICENSE_CENTER_URL : 'http://yutou.wtbidc.cn/license-center'), '/');
    $url = $baseUrl . '/api.php?action=check_update';
    $response = '';

    if (function_exists('curl_init')) {
        $ch = curl_init($url);
        curl_setopt_array($ch, [
            CURLOPT_RETURNTRANSFER => true,
            CURLOPT_TIMEOUT => $timeout,
            CURLOPT_CONNECTTIMEOUT => min($timeout, 2),
            CURLOPT_SSL_VERIFYPEER => false,
            CURLOPT_SSL_VERIFYHOST => false,
        ]);
        $response = (string) curl_exec($ch);
        curl_close($ch);
    } elseif (ini_get('allow_url_fopen')) {
        $context = stream_context_create([
            'http' => ['timeout' => $timeout],
            'ssl' => ['verify_peer' => false, 'verify_peer_name' => false],
        ]);
        $response = (string) @file_get_contents($url, false, $context);
    }

    $decoded = $response !== '' ? json_decode($response, true) : null;
    $data = is_array($decoded) && isset($decoded['data']) && is_array($decoded['data']) ? $decoded['data'] : null;
    if (!is_array($data) || trim((string) ($data['latest_version'] ?? '')) === '') {
        $cache[$timeout] = null;
        return null;
    }

    $cache[$timeout] = [
        'latest_version' => trim((string) ($data['latest_version'] ?? '')),
        'changelog' => trim((string) ($data['changelog'] ?? '')),
        'download_url' => trim((string) ($data['download_url'] ?? '')),
        'current_version' => trim((string) ($data['current_version'] ?? '')),
    ];
    return $cache[$timeout];
}

function cmsResolveUploadAssetPath($pathOrUrl)
{
    $raw = trim((string) $pathOrUrl);
    if ($raw === '') {
        return '';
    }
    $path = parse_url($raw, PHP_URL_PATH);
    if (!is_string($path) || $path === '') {
        $path = $raw;
    }
    $relative = ltrim(str_replace(['/', '\\'], DIRECTORY_SEPARATOR, $path), DIRECTORY_SEPARATOR);
    if ($relative === '') {
        return '';
    }
    $root = realpath(dirname(__DIR__, 2));
    if ($root === false) {
        return '';
    }
    $candidate = realpath($root . DIRECTORY_SEPARATOR . $relative);
    if ($candidate === false || strpos($candidate, $root . DIRECTORY_SEPARATOR) !== 0 || !is_file($candidate)) {
        return '';
    }
    return $candidate;
}

function cmsCreateImageResourceFromFile($filePath, $mimeType)
{
    if (!is_file($filePath)) {
        return null;
    }
    switch (strtolower((string) $mimeType)) {
        case 'image/jpeg':
        case 'image/jpg':
            return function_exists('imagecreatefromjpeg') ? @imagecreatefromjpeg($filePath) : null;
        case 'image/png':
            return function_exists('imagecreatefrompng') ? @imagecreatefrompng($filePath) : null;
        case 'image/webp':
            return function_exists('imagecreatefromwebp') ? @imagecreatefromwebp($filePath) : null;
        case 'image/gif':
            return function_exists('imagecreatefromgif') ? @imagecreatefromgif($filePath) : null;
        case 'image/bmp':
        case 'image/x-ms-bmp':
            return function_exists('imagecreatefrombmp') ? @imagecreatefrombmp($filePath) : null;
        default:
            return null;
    }
}

function cmsNormalizeWatermarkPosition($position)
{
    $position = trim((string) $position);
    $utf8Map = [
        "\xE5\x8F\xB3\xE4\xB8\x8B" => 'bottom-right',
        "\xE5\xB7\xA6\xE4\xB8\x8B" => 'bottom-left',
        "\xE5\x8F\xB3\xE4\xB8\x8A" => 'top-right',
        "\xE5\xB7\xA6\xE4\xB8\x8A" => 'top-left',
        "\xE5\xB1\x85\xE4\xB8\xAD" => 'center',
    ];
    if (isset($utf8Map[$position])) {
        return $utf8Map[$position];
    }
    $map = [
        '右下' => 'bottom-right',
        '左下' => 'bottom-left',
        '右上' => 'top-right',
        '左上' => 'top-left',
        '居中' => 'center',
    ];
    return $map[$position] ?? 'bottom-right';
}

function cmsFindWatermarkFontFile()
{
    static $fontFile = null;
    if ($fontFile !== null) {
        return $fontFile;
    }
    $candidates = [
        dirname(__DIR__, 2) . '/public/fonts/msyh.ttf',
        dirname(__DIR__, 2) . '/public/fonts/simhei.ttf',
        'C:\\Windows\\Fonts\\msyh.ttc',
        'C:\\Windows\\Fonts\\msyh.ttf',
        'C:\\Windows\\Fonts\\simhei.ttf',
        'C:\\Windows\\Fonts\\arial.ttf',
        '/usr/share/fonts/truetype/wqy/wqy-microhei.ttc',
        '/usr/share/fonts/truetype/dejavu/DejaVuSans.ttf',
    ];
    foreach ($candidates as $candidate) {
        if (is_file($candidate)) {
            $fontFile = $candidate;
            return $fontFile;
        }
    }
    $fontFile = '';
    return $fontFile;
}

function cmsBuildWatermarkConfig(array $options = [])
{
    $image = trim((string) getSiteConfig('watermark_image', ''));
    $text = trim((string) getSiteConfig('watermark_text', ''));
    $alpha = isset($options['watermark_alpha']) ? floatval($options['watermark_alpha']) : floatval(getSiteConfig('watermark_alpha', '0.5'));
    if ($alpha < 0) {
        $alpha = 0;
    } elseif ($alpha > 1) {
        $alpha = 1;
    }
    return [
        'enabled' => ($image !== '' || $text !== '') && function_exists('imagecreatetruecolor'),
        'image' => $image,
        'text' => $text,
        'position' => cmsNormalizeWatermarkPosition(getSiteConfig('watermark_position', '右下')),
        'alpha' => $alpha,
        'font' => cmsFindWatermarkFontFile(),
    ];
}

function cmsAdjustWatermarkOpacity($image, $opacity)
{
    $opacity = max(0, min(1, floatval($opacity)));
    if ($opacity >= 0.999) {
        return $image;
    }
    $width = imagesx($image);
    $height = imagesy($image);
    $adjusted = imagecreatetruecolor($width, $height);
    imagealphablending($adjusted, false);
    imagesavealpha($adjusted, true);
    $transparent = imagecolorallocatealpha($adjusted, 0, 0, 0, 127);
    imagefilledrectangle($adjusted, 0, 0, $width, $height, $transparent);
    imagecopy($adjusted, $image, 0, 0, 0, 0, $width, $height);
    for ($x = 0; $x < $width; $x++) {
        for ($y = 0; $y < $height; $y++) {
            $rgba = imagecolorat($adjusted, $x, $y);
            $alpha = ($rgba >> 24) & 0x7F;
            $red = ($rgba >> 16) & 0xFF;
            $green = ($rgba >> 8) & 0xFF;
            $blue = $rgba & 0xFF;
            $newAlpha = 127 - (127 - $alpha) * $opacity;
            $color = imagecolorallocatealpha($adjusted, $red, $green, $blue, max(0, min(127, intval(round($newAlpha)))));
            imagesetpixel($adjusted, $x, $y, $color);
        }
    }
    return $adjusted;
}

function cmsScaleWatermarkImage($image, $targetWidth, $targetHeight)
{
    $srcWidth = imagesx($image);
    $srcHeight = imagesy($image);
    if ($srcWidth <= 0 || $srcHeight <= 0 || ($srcWidth <= $targetWidth && $srcHeight <= $targetHeight)) {
        return $image;
    }
    $ratio = max(0.05, min(1, min($targetWidth / $srcWidth, $targetHeight / $srcHeight)));
    $newWidth = max(1, intval(round($srcWidth * $ratio)));
    $newHeight = max(1, intval(round($srcHeight * $ratio)));
    $scaled = imagecreatetruecolor($newWidth, $newHeight);
    imagealphablending($scaled, false);
    imagesavealpha($scaled, true);
    $transparent = imagecolorallocatealpha($scaled, 0, 0, 0, 127);
    imagefilledrectangle($scaled, 0, 0, $newWidth, $newHeight, $transparent);
    imagecopyresampled($scaled, $image, 0, 0, 0, 0, $newWidth, $newHeight, $srcWidth, $srcHeight);
    return $scaled;
}

function cmsCalculateWatermarkBox($canvasWidth, $canvasHeight, $blockWidth, $blockHeight, $position, $margin = 24)
{
    $margin = max(8, intval($margin));
    switch ($position) {
        case 'top-left':
            return ['x' => $margin, 'y' => $margin];
        case 'top-right':
            return ['x' => max($margin, $canvasWidth - $blockWidth - $margin), 'y' => $margin];
        case 'bottom-left':
            return ['x' => $margin, 'y' => max($margin, $canvasHeight - $blockHeight - $margin)];
        case 'center':
            return [
                'x' => max($margin, intval(round(($canvasWidth - $blockWidth) / 2))),
                'y' => max($margin, intval(round(($canvasHeight - $blockHeight) / 2))),
            ];
        case 'bottom-right':
        default:
            return [
                'x' => max($margin, $canvasWidth - $blockWidth - $margin),
                'y' => max($margin, $canvasHeight - $blockHeight - $margin),
            ];
    }
}

function cmsApplyConfiguredWatermark(&$image, array $config)
{
    $isGdImage = is_object($image) && class_exists('GdImage', false) && $image instanceof GdImage;
    if (empty($config['enabled']) || (!is_resource($image) && !$isGdImage)) {
        return false;
    }

    $canvasWidth = imagesx($image);
    $canvasHeight = imagesy($image);
    if ($canvasWidth < 80 || $canvasHeight < 80) {
        return false;
    }

    $wmImage = null;
    $wmWidth = 0;
    $wmHeight = 0;
    $wmPath = cmsResolveUploadAssetPath($config['image'] ?? '');
    if ($wmPath !== '') {
        $wmInfo = @getimagesize($wmPath);
        $wmMime = is_array($wmInfo) ? ($wmInfo['mime'] ?? '') : '';
        $wmImage = cmsCreateImageResourceFromFile($wmPath, $wmMime);
        if ($wmImage) {
            $scaled = cmsScaleWatermarkImage($wmImage, intval($canvasWidth * 0.28), intval($canvasHeight * 0.22));
            if ($scaled !== $wmImage) {
                imagedestroy($wmImage);
                $wmImage = $scaled;
            }
            $adjusted = cmsAdjustWatermarkOpacity($wmImage, $config['alpha'] ?? 0.5);
            if ($adjusted !== $wmImage) {
                imagedestroy($wmImage);
                $wmImage = $adjusted;
            }
            $wmWidth = imagesx($wmImage);
            $wmHeight = imagesy($wmImage);
        }
    }

    $text = trim((string) ($config['text'] ?? ''));
    $font = trim((string) ($config['font'] ?? ''));
    $textMetrics = null;
    if ($text !== '' && $font !== '' && function_exists('imagettfbbox') && function_exists('imagettftext')) {
        $fontSize = max(14, intval(round(min($canvasWidth, $canvasHeight) / 22)));
        $bbox = @imagettfbbox($fontSize, 0, $font, $text);
        if (is_array($bbox)) {
            $minX = min($bbox[0], $bbox[2], $bbox[4], $bbox[6]);
            $maxX = max($bbox[0], $bbox[2], $bbox[4], $bbox[6]);
            $minY = min($bbox[1], $bbox[3], $bbox[5], $bbox[7]);
            $maxY = max($bbox[1], $bbox[3], $bbox[5], $bbox[7]);
            $textMetrics = [
                'font_size' => $fontSize,
                'left' => $minX,
                'top' => $minY,
                'width' => max(1, $maxX - $minX),
                'height' => max(1, $maxY - $minY),
            ];
        }
    }

    if (!$wmImage && !$textMetrics) {
        return false;
    }

    $gap = ($wmImage && $textMetrics) ? 10 : 0;
    $blockWidth = max($wmWidth, intval($textMetrics['width'] ?? 0));
    $blockHeight = $wmHeight + $gap + intval($textMetrics['height'] ?? 0);
    $box = cmsCalculateWatermarkBox($canvasWidth, $canvasHeight, $blockWidth, $blockHeight, $config['position'] ?? 'bottom-right');

    imagealphablending($image, true);
    imagesavealpha($image, true);

    if ($wmImage) {
        $imgX = $box['x'] + intval(round(($blockWidth - $wmWidth) / 2));
        $imgY = $box['y'];
        imagecopy($image, $wmImage, $imgX, $imgY, 0, 0, $wmWidth, $wmHeight);
    }

    if ($textMetrics) {
        $textX = $box['x'] + intval(round(($blockWidth - $textMetrics['width']) / 2)) - $textMetrics['left'];
        $textY = $box['y'] + $wmHeight + $gap - $textMetrics['top'];
        $opacity = max(0, min(1, floatval($config['alpha'] ?? 0.5)));
        $textAlpha = max(0, min(127, 127 - intval(round(127 * $opacity))));
        $shadowAlpha = max($textAlpha, 70);
        $shadow = imagecolorallocatealpha($image, 15, 23, 42, $shadowAlpha);
        $color = imagecolorallocatealpha($image, 255, 255, 255, $textAlpha);
        @imagettftext($image, $textMetrics['font_size'], 0, $textX + 1, $textY + 1, $shadow, $font, $text);
        @imagettftext($image, $textMetrics['font_size'], 0, $textX, $textY, $color, $font, $text);
    }

    if ($wmImage) {
        imagedestroy($wmImage);
    }
    return true;
}

function cmsSaveGdImageToPath($image, $targetPath, $mimeType, $quality)
{
    switch (strtolower((string) $mimeType)) {
        case 'image/jpeg':
        case 'image/jpg':
            imageinterlace($image, true);
            return @imagejpeg($image, $targetPath, max(1, min(100, intval($quality))));
        case 'image/png':
            imagealphablending($image, false);
            imagesavealpha($image, true);
            $pngQuality = 9 - intval(round(max(0, min(100, intval($quality))) / 100 * 9));
            return @imagepng($image, $targetPath, max(0, min(9, $pngQuality)));
        case 'image/gif':
            return @imagegif($image, $targetPath);
        case 'image/webp':
            return function_exists('imagewebp') ? @imagewebp($image, $targetPath, max(1, min(100, intval($quality)))) : false;
        case 'image/bmp':
        case 'image/x-ms-bmp':
            return function_exists('imagebmp') ? @imagebmp($image, $targetPath) : false;
        default:
            return false;
    }
}

function cmsStoreUploadedImage($tmpPath, $originalName, $targetDir, $baseName = 'img', $options = [])
{
    if (empty($options['apply_watermark'])) {
        return cmsSaveUploadedImage($tmpPath, $originalName, $targetDir, $baseName, $options);
    }
    $quality = isset($options['quality']) ? intval($options['quality']) : 82;
    if ($quality <= 0 || $quality > 100) {
        $quality = 82;
    }
    if (!is_dir($targetDir) && !mkdir($targetDir, 0755, true) && !is_dir($targetDir)) {
        return ['success' => false, 'message' => '创建上传目录失败'];
    }

    $finfo = finfo_open(FILEINFO_MIME_TYPE);
    $mimeType = $finfo ? finfo_file($finfo, $tmpPath) : '';
    if ($finfo) {
        finfo_close($finfo);
    }

    $originalExt = cmsSafeImageExtension($mimeType, $originalName);
    $safeBaseName = preg_replace('/[^A-Za-z0-9_\-]/', '_', (string) $baseName);
    $safeBaseName = trim($safeBaseName, '_');
    if ($safeBaseName === '') {
        $safeBaseName = 'img';
    }
    $fileToken = $safeBaseName . '_' . time() . '_' . substr(md5(uniqid('', true)), 0, 10);
    $targetDir = rtrim($targetDir, '/\\') . DIRECTORY_SEPARATOR;

    $canConvertToWebp = function_exists('imagewebp') && function_exists('imagecreatefromjpeg') && function_exists('imagecreatefrompng');
    $shouldKeepOriginal = ($mimeType === 'image/gif' && cmsIsAnimatedGif($tmpPath))
        || $mimeType === 'image/svg+xml'
        || $mimeType === 'image/x-icon'
        || $mimeType === 'image/vnd.microsoft.icon';
    $image = !$shouldKeepOriginal ? cmsCreateImageResourceFromFile($tmpPath, $mimeType) : null;
    $watermarkApplied = false;

    if ($image) {
        imagepalettetotruecolor($image);
        imagealphablending($image, false);
        imagesavealpha($image, true);
        if (!empty($options['apply_watermark'])) {
            $watermarkApplied = cmsApplyConfiguredWatermark($image, cmsBuildWatermarkConfig($options));
        }
        if ($canConvertToWebp) {
            $targetName = $fileToken . '.webp';
            $targetPath = $targetDir . $targetName;
            $saved = @imagewebp($image, $targetPath, $quality);
            if ($saved && is_file($targetPath)) {
                imagedestroy($image);
                return [
                    'success' => true,
                    'filename' => $targetName,
                    'path' => $targetPath,
                    'mime' => 'image/webp',
                    'converted' => true,
                    'extension' => 'webp',
                    'watermarked' => $watermarkApplied
                ];
            }
        }
    }

    $targetName = $fileToken . '.' . $originalExt;
    $targetPath = $targetDir . $targetName;
    if ($image) {
        $saved = cmsSaveGdImageToPath($image, $targetPath, $mimeType, $quality);
        imagedestroy($image);
        if ($saved && is_file($targetPath)) {
            return [
                'success' => true,
                'filename' => $targetName,
                'path' => $targetPath,
                'mime' => $mimeType,
                'converted' => false,
                'extension' => $originalExt,
                'watermarked' => $watermarkApplied
            ];
        }
    }

    if (!move_uploaded_file($tmpPath, $targetPath)) {
        return ['success' => false, 'message' => '图片保存失败'];
    }

    return [
        'success' => true,
        'filename' => $targetName,
        'path' => $targetPath,
        'mime' => $mimeType,
        'converted' => false,
        'extension' => $originalExt,
        'watermarked' => false
    ];
}

function cmsTableHasColumn($table, $column)
{
    static $columnCache = [];
    $db = \Core\Db\DB::getInstance();
    $tableName = $db->table($table);
    $cacheKey = $tableName . '.' . $column;
    if (array_key_exists($cacheKey, $columnCache)) {
        return $columnCache[$cacheKey];
    }
    $rows = $db->fetchAll("SHOW COLUMNS FROM {$tableName}");
    $exists = false;
    foreach ($rows as $row) {
        if (($row['Field'] ?? '') === $column) {
            $exists = true;
            break;
        }
    }
    $columnCache[$cacheKey] = $exists;
    return $exists;
}

/**
 * 确保数据表字段存在
 */
function cmsEnsureTableColumn($table, $column, $alterSql)
{
    if (cmsTableHasColumn($table, $column)) {
        return true;
    }
    $db = \Core\Db\DB::getInstance();
    $db->query($alterSql);
    return true;
}

/**
 * 生成URL
 */
function buildUrl($path = '', $params = [])
{
    static $base = null;

    if ($base === null) {
        $db = \Core\Db\DB::getInstance();
        $siteRow = $db->fetch("SELECT config_value FROM {$db->table('config')} WHERE config_key = 'site_url'");
        $base = rtrim($siteRow['config_value'] ?? 'http://' . $_SERVER['HTTP_HOST'], '/');
    }

    $useRewriteUrls = cmsRewriteModeEnabled();
    if (!$useRewriteUrls) {
        $url = $base . '/public/index.php?';
        if ($path) $url .= 'm=' . $path;
        if ($params) $url .= '&' . http_build_query($params);
    } else {
        $url = $base . '/' . ltrim($path, '/');
        if ($params) $url .= '?' . http_build_query($params);
    }
    
    return $url;
}

/**
 * 生成栏目URL
 */
function channelUrl($channel, $page = 1)
{
    $base = getSiteConfig('site_url', 'http://' . $_SERVER['HTTP_HOST']);
    $url = $base . cmsBuildChannelUrl($channel, $page > 1 ? ['page' => $page] : []);
    if ($page > 1) {
        return $url;
    }
    return $url;
}

/**
 * 生成文章URL
 */
function articleUrl($article)
{
    $base = getSiteConfig('site_url', 'http://' . $_SERVER['HTTP_HOST']);
    $url = $base . cmsBuildFrontendUrl('article', intval($article['id'] ?? 0));
    return $url;
}

/**
 * 生成产品URL
 */
function productUrl($product)
{
    $base = getSiteConfig('site_url', 'http://' . $_SERVER['HTTP_HOST']);
    $url = $base . cmsBuildFrontendUrl('product', intval($product['id'] ?? 0));
    return $url;
}

/**
 * 生成前台相对URL，兼容动态/伪静态模式
 */
if (!function_exists('cmsBuildFrontendUrl')) {
    function cmsBuildFrontendUrl($type, $id = 0, $dir = '', $extraParams = [], $redirectUrl = '')
    {
        $type = trim((string) $type);
        $id = intval($id);
        $dir = trim((string) $dir, '/');
        $redirectUrl = trim((string) $redirectUrl);

        if ($type === 'channel' && $redirectUrl !== '') {
            return $redirectUrl;
        }

        if ($type === 'page' && $id <= 0) {
            $db = \Core\Db\DB::getInstance();
            $pageRow = $db->fetch("SELECT id FROM {$db->table('page')} WHERE status = 1 ORDER BY sort_order ASC, id ASC LIMIT 1");
            $id = intval($pageRow['id'] ?? 0);
            if ($id <= 0) {
                return '/';
            }
        }

        $urlMode = getSiteConfig('url_mode', 'dynamic');
        $useRewriteUrls = strtolower(trim((string) $urlMode)) !== 'dynamic' && cmsBoolConfigEnabled('rewrite_auto_enabled', false);
        if (!$useRewriteUrls) {
            switch ($type) {
                case 'home':
                case 'index':
                    $url = '/public/index.php?m=index';
                    break;
                case 'channel':
                    $url = '/public/index.php?m=channel&id=' . $id;
                    break;
                case 'article':
                    $url = $id > 0 ? '/public/index.php?m=article&id=' . $id : '/public/index.php?m=article';
                    break;
                case 'product':
                    $url = $id > 0 ? '/public/index.php?m=product&id=' . $id : '/public/index.php?m=product';
                    break;
                case 'search':
                    $url = '/public/index.php?m=search';
                    break;
                case 'guestbook':
                    $url = '/public/index.php?m=guestbook';
                    break;
                case 'page':
                    $url = '/public/index.php?m=page&id=' . $id;
                    break;
                case 'image':
                    $url = $id > 0 ? '/public/index.php?m=image&id=' . $id : '/public/index.php?m=image';
                    break;
                case 'download':
                    $url = $id > 0 ? '/public/index.php?m=download&id=' . $id : '/public/index.php?m=download';
                    break;
                case 'video':
                    $url = $id > 0 ? '/public/index.php?m=video&id=' . $id : '/public/index.php?m=video';
                    break;
                case 'recruit':
                    $url = $id > 0 ? '/public/index.php?m=recruit&id=' . $id : '/public/index.php?m=recruit';
                    break;
                default:
                    $url = '/public/index.php';
                    break;
            }
        } else {
            switch ($type) {
                case 'home':
                case 'index':
                    $url = '/';
                    break;
                case 'channel':
                    $url = $dir !== '' ? '/' . $dir . '/' : '/channel/' . $id . '/';
                    break;
                case 'article':
                    $url = $id > 0 ? '/article/' . $id . '.html' : '/article/';
                    break;
                case 'product':
                    $url = $id > 0 ? '/product/' . $id . '.html' : '/product/';
                    break;
                case 'search':
                    $url = '/search/';
                    break;
                case 'guestbook':
                    $url = '/guestbook/';
                    break;
                case 'page':
                    $url = '/page/' . $id . '.html';
                    break;
                case 'image':
                    $url = $id > 0 ? '/image/' . $id . '.html' : '/image/';
                    break;
                case 'download':
                    $url = $id > 0 ? '/download/' . $id . '.html' : '/download/';
                    break;
                case 'video':
                    $url = $id > 0 ? '/video/' . $id . '.html' : '/video/';
                    break;
                case 'recruit':
                    $url = $id > 0 ? '/recruit/' . $id . '.html' : '/recruit/';
                    break;
                default:
                    $url = '/';
                    break;
            }
        }

        if (!empty($extraParams) && is_array($extraParams)) {
            $joiner = strpos($url, '?') !== false ? '&' : '?';
            $url .= $joiner . http_build_query($extraParams);
        }

        return $url;
    }
}

if (!function_exists('cmsBuildChannelUrl')) {
    function cmsBuildChannelUrl($channel, $extraParams = [])
    {
        if (!is_array($channel)) {
            return cmsBuildFrontendUrl('channel', 0, '', $extraParams);
        }

        $redirectUrl = (string) ($channel['redirect_url'] ?? '');
        if (intval($channel['channel_type'] ?? 0) === 3) {
            $redirectUrl = '';
        }

        return cmsBuildFrontendUrl(
            'channel',
            intval($channel['id'] ?? 0),
            (string) ($channel['channel_dir'] ?? ''),
            $extraParams,
            $redirectUrl
        );
    }
}

if (!function_exists('cmsResolveBuilderFrontendUrl')) {
    function cmsResolveBuilderFrontendUrl($builderType, $refId = 0, $fallbackUrl = '')
    {
        $builderType = trim((string) $builderType);
        $refId = intval($refId);
        $fallbackUrl = trim((string) $fallbackUrl);
        $db = \Core\Db\DB::getInstance();

        switch ($builderType) {
            case 'home':
                return cmsBuildFrontendUrl('home');

            case 'page':
                if ($refId > 0) {
                    return cmsBuildFrontendUrl('page', $refId);
                }
                return $fallbackUrl !== '' ? $fallbackUrl : cmsBuildFrontendUrl('home');

            case 'article_list':
                return cmsBuildFrontendUrl('article');

            case 'article_detail':
                $row = $db->fetch("SELECT id FROM {$db->table('article')} WHERE status = 1 ORDER BY id DESC LIMIT 1");
                return cmsBuildFrontendUrl('article', intval($row['id'] ?? 0));

            case 'product_list':
                return cmsBuildFrontendUrl('product');

            case 'product_detail':
                $row = $db->fetch("SELECT id FROM {$db->table('product')} WHERE status = 1 ORDER BY id DESC LIMIT 1");
                return cmsBuildFrontendUrl('product', intval($row['id'] ?? 0));

            case 'image_list':
                return cmsBuildFrontendUrl('image');

            case 'image_detail':
                if ($refId > 0) {
                    return cmsBuildFrontendUrl('image', $refId);
                }
                $row = $db->fetch("SELECT id FROM {$db->table('image')} WHERE status = 1 ORDER BY id DESC LIMIT 1");
                return cmsBuildFrontendUrl('image', intval($row['id'] ?? 0));

            case 'download_list':
                return cmsBuildFrontendUrl('download');

            case 'download_detail':
                $row = $db->fetch("SELECT id FROM {$db->table('download')} WHERE status = 1 ORDER BY id DESC LIMIT 1");
                return cmsBuildFrontendUrl('download', intval($row['id'] ?? 0));

            case 'video_list':
                return cmsBuildFrontendUrl('video');

            case 'video_detail':
                $row = $db->fetch("SELECT id FROM {$db->table('video')} WHERE status = 1 ORDER BY id DESC LIMIT 1");
                return cmsBuildFrontendUrl('video', intval($row['id'] ?? 0));

            case 'recruit_list':
                return cmsBuildFrontendUrl('recruit');

            case 'recruit_detail':
                $row = $db->fetch("SELECT id FROM {$db->table('recruit')} WHERE status = 1 ORDER BY id DESC LIMIT 1");
                return cmsBuildFrontendUrl('recruit', intval($row['id'] ?? 0));
        }

        return $fallbackUrl !== '' ? $fallbackUrl : cmsBuildFrontendUrl('home');
    }
}

/**
 * 获取栏目名称
 */
function getChannelName($channelId)
{
    static $channels = null;
    if ($channels === null) {
        $db = \Core\Db\DB::getInstance();
        $rows = $db->fetchAll("SELECT id, channel_name FROM {$db->table('channel')}");
        $channels = [];
        foreach ($rows as $row) {
            $channels[$row['id']] = $row['channel_name'];
        }
    }
    return $channels[$channelId] ?? '';
}

/**
 * 截取字符串
 */
function msubstr($str, $start = 0, $length = 100, $suffix = '...')
{
    if (function_exists('mb_substr')) {
        $s = mb_substr($str, $start, $length, 'utf-8');
    } else {
        $len = strlen($str);
        $s = '';
        for ($i = $start; $i < $len && $length > 0; $i++) {
            if (ord($str[$i]) > 127) {
                $s .= $str[$i] . $str[++$i] . $str[++$i];
                $length--;
            } else {
                $s .= $str[$i];
                $length--;
            }
        }
    }
    if ($start == 0 && strlen($s) < strlen($str)) {
        $s .= $suffix;
    }
    return $s;
}

/**
 * 格式化日期
 */
function formatDate($time, $format = 'Y-m-d H:i')
{
    if (empty($time)) {
        return '';
    }
    return date($format, $time);
}

/**
 * 格式化文件大小
 */
function formatSize($bytes)
{
    $units = ['B', 'KB', 'MB', 'GB', 'TB'];
    $i = 0;
    while ($bytes >= 1024 && $i < 4) {
        $bytes /= 1024;
        $i++;
    }
    return round($bytes, 2) . ' ' . $units[$i];
}

/**
 * 获取文章标记样式
 */
function getFlagStyle($flag)
{
    $styles = [
        'top' => ['text' => '置顶', 'color' => '#e74c3c'],
        'recommend' => ['text' => '推荐', 'color' => '#f39c12'],
        'hot' => ['text' => '热门', 'color' => '#e67e22'],
    ];
    return $styles[$flag] ?? ['text' => '', 'color' => ''];
}

/**
 * 生成缩略图HTML
 */
function getLitpicHtml($litpic, $alt = '', $width = 0, $height = 0)
{
    if (empty($litpic)) {
        return '<span class="no-pic">暂无图片</span>';
    }

    $style = '';
    if ($width > 0) $style .= "width:{$width}px;";
    if ($height > 0) $style .= "height:{$height}px;";

    // data:image 开头的 base64 图片直接输出，不转义
    if (strpos($litpic, 'data:') === 0) {
        return '<img src="' . $litpic . '" alt="' . htmlspecialchars($alt) . '"' . ($style ? ' style="' . $style . '"' : '') . '>';
    }

    return '<img src="' . htmlspecialchars($litpic) . '" alt="' . htmlspecialchars($alt) . '"' . ($style ? ' style="' . $style . '"' : '') . '>';
}

/**
 * 生成SEO信息
 */
function getSeoInfo($title = '', $keywords = '', $description = '')
{
    $siteName = getSiteConfig('site_name', '企业CMS');
    $siteTitle = getSiteConfig('site_title', $siteName);
    $siteKeywords = getSiteConfig('site_keywords', '');
    $siteDescription = getSiteConfig('site_description', '');
    
    $pageTitle = $title ? $title . ' - ' . $siteTitle : $siteTitle;
    $pageKeywords = $keywords ?: $siteKeywords;
    $pageDescription = $description ?: $siteDescription;
    
    return [
        'title' => $pageTitle,
        'keywords' => $pageKeywords,
        'description' => $pageDescription,
    ];
}

/**
 * 输出分页
 */
function showPage($page, $totalPages, $url = '')
{
    if ($totalPages <= 1) {
        return '';
    }
    
    $html = '<div class="pagination">';
    
    // 上一页
    if ($page > 1) {
        $html .= '<a href="' . $url . '?page=' . ($page - 1) . '" class="prev">上一页</a>';
    }
    
    // 页码
    $start = max(1, $page - 2);
    $end = min($totalPages, $page + 2);
    
    if ($start > 1) {
        $html .= '<a href="' . $url . '?page=1">1</a>';
        if ($start > 2) {
            $html .= '<span class="ellipsis">...</span>';
        }
    }
    
    for ($i = $start; $i <= $end; $i++) {
        $active = ($i == $page) ? ' class="active"' : '';
        $html .= '<a href="' . $url . '?page=' . $i . '"' . $active . '>' . $i . '</a>';
    }
    
    if ($end < $totalPages) {
        if ($end < $totalPages - 1) {
            $html .= '<span class="ellipsis">...</span>';
        }
        $html .= '<a href="' . $url . '?page=' . $totalPages . '">' . $totalPages . '</a>';
    }
    
    // 下一页
    if ($page < $totalPages) {
        $html .= '<a href="' . $url . '?page=' . ($page + 1) . '" class="next">下一页</a>';
    }
    
    $html .= '</div>';
    
    return $html;
}

/**
 * 获取会员信息
 */
function getMemberInfo($memberId)
{
    $db = \Core\Db\DB::getInstance();
    return $db->fetch("SELECT * FROM {$db->table('member')} WHERE id = ?", [$memberId]);
}

/**
 * 检查会员登录状态
 */
function checkMemberLogin()
{
    if (!isset($_SESSION['member_id']) || empty($_SESSION['member_id'])) {
        header('Location: /member/login.php');
        exit;
    }
}

/**
 * 记录操作日志
 */
function addLog($action, $module = '', $description = '', $content = '')
{
    $db = \Core\Db\DB::getInstance();
    $adminId = $_SESSION['admin_id'] ?? 0;
    $adminName = $_SESSION['admin_name'] ?? '系统';
    $db->insert('log', [
        'user_id' => $adminId,
        'username' => $adminName,
        'action' => $action,
        'module' => $module,
        'content' => $description,
        'ip' => $_SERVER['REMOTE_ADDR'] ?? '',
        'user_agent' => $_SERVER['HTTP_USER_AGENT'] ?? '',
        'create_time' => time()
    ]);
}

/**
 * 获取后台 CSRF 令牌
 */
function getAdminCsrfToken()
{
    if (empty($_SESSION['admin_csrf_token'])) {
        $_SESSION['admin_csrf_token'] = bin2hex(random_bytes(32));
    }
    return $_SESSION['admin_csrf_token'];
}

/**
 * 校验后台 CSRF 令牌
 */
function verifyAdminCsrfToken($token = null)
{
    if ($token === null) {
        $token = isset($_POST['csrf_token']) ? $_POST['csrf_token'] : '';
        if ($token === '' && isset($_SERVER['HTTP_X_CSRF_TOKEN'])) {
            $token = $_SERVER['HTTP_X_CSRF_TOKEN'];
        }
    }
    return !empty($token) && isset($_SESSION['admin_csrf_token']) && hash_equals($_SESSION['admin_csrf_token'], $token);
}

/**
 * 后台 CSRF 校验失败时统一输出
 */
  function requireAdminCsrfToken($json = false, $token = null)
  {
      if (verifyAdminCsrfToken($token)) {
          return true;
      }
      http_response_code(403);
      if ($json) {
        while (ob_get_level() > 0) { ob_end_clean(); }
        header('Content-Type: application/json; charset=utf-8');
        echo json_encode(['code' => 403, 'msg' => '非法请求，CSRF校验失败']);
    } else {
        echo '<script>alert("非法请求，CSRF校验失败");history.back();</script>';
    }
    exit;
}

if (!function_exists('cmsBuilderTargetMatchesRequest')) {
    function cmsBuilderTargetMatchesRequest($builderPageType, $builderRefId, $module, $id)
    {
        $builderPageType = trim((string) $builderPageType);
        $module = trim((string) $module);
        $id = intval($id);
        $builderRefId = intval($builderRefId);

        $map = [
            'article_list' => ['module' => 'article', 'detail' => false],
            'article_detail' => ['module' => 'article', 'detail' => true],
            'product_list' => ['module' => 'product', 'detail' => false],
            'product_detail' => ['module' => 'product', 'detail' => true],
            'image_list' => ['module' => 'image', 'detail' => false],
            'image_detail' => ['module' => 'image', 'detail' => true],
            'download_list' => ['module' => 'download', 'detail' => false],
            'download_detail' => ['module' => 'download', 'detail' => true],
            'video_list' => ['module' => 'video', 'detail' => false],
            'video_detail' => ['module' => 'video', 'detail' => true],
            'recruit_list' => ['module' => 'recruit', 'detail' => false],
            'recruit_detail' => ['module' => 'recruit', 'detail' => true],
            'page' => ['module' => 'page', 'detail' => true],
            'home' => ['module' => 'index', 'detail' => false],
        ];

        if (!isset($map[$builderPageType])) {
            return false;
        }

        $target = $map[$builderPageType];
        if ($target['module'] !== $module && !($target['module'] === 'index' && $module === '')) {
            return false;
        }

        if (!empty($target['detail'])) {
            return $id > 0 && ($builderRefId === 0 || $builderRefId === $id);
        }

        return $id <= 0;
    }
}

if (!function_exists('cmsBuilderAuthSecret')) {
    function cmsBuilderAuthSecret()
    {
        $base = (string) getSiteConfig('site_url', ($_SERVER['HTTP_HOST'] ?? 'enterprisecms'));
        $license = (string) getSiteConfig('license_code', '');
        $name = (string) getSiteConfig('site_name', 'enterprisecms');
        return hash('sha256', $base . '|' . $license . '|' . $name . '|builder-auth');
    }
}

if (!function_exists('cmsBuildBuilderAccessToken')) {
    function cmsBuildBuilderAccessToken($builderType, $refId = 0, $expiresAt = 0)
    {
        $builderType = trim((string) $builderType);
        $refId = intval($refId);
        $expiresAt = intval($expiresAt);
        if ($expiresAt <= 0) {
            $expiresAt = time() + 7200;
        }
        $payload = $builderType . '|' . $refId . '|' . $expiresAt;
        $signature = hash_hmac('sha256', $payload, cmsBuilderAuthSecret());
        return base64_encode($payload . '|' . $signature);
    }
}

if (!function_exists('cmsVerifyBuilderAccessToken')) {
    function cmsVerifyBuilderAccessToken($token, $builderType, $refId = 0)
    {
        $token = trim((string) $token);
        if ($token === '') {
            return false;
        }
        $decoded = base64_decode($token, true);
        if (!is_string($decoded) || $decoded === '') {
            return false;
        }
        $parts = explode('|', $decoded);
        if (count($parts) !== 4) {
            return false;
        }
        $tokenType = trim((string) ($parts[0] ?? ''));
        $tokenRefId = intval($parts[1] ?? 0);
        $expiresAt = intval($parts[2] ?? 0);
        $signature = (string) ($parts[3] ?? '');
        if ($tokenType !== trim((string) $builderType) || $tokenRefId !== intval($refId)) {
            return false;
        }
        if ($expiresAt < time()) {
            return false;
        }
        $payload = $tokenType . '|' . $tokenRefId . '|' . $expiresAt;
        $expected = hash_hmac('sha256', $payload, cmsBuilderAuthSecret());
        return hash_equals($expected, $signature);
    }
}

/**
 * 获取会员端 CSRF 令牌
 */
function getMemberCsrfToken()
{
    if (empty($_SESSION['member_csrf_token'])) {
        $_SESSION['member_csrf_token'] = bin2hex(random_bytes(32));
    }
    return $_SESSION['member_csrf_token'];
}

/**
 * 校验会员端 CSRF 令牌
 */
function verifyMemberCsrfToken($token = null)
{
    if ($token === null) {
        $token = isset($_POST['csrf_token']) ? $_POST['csrf_token'] : '';
        if ($token === '' && isset($_SERVER['HTTP_X_CSRF_TOKEN'])) {
            $token = $_SERVER['HTTP_X_CSRF_TOKEN'];
        }
    }
    return !empty($token) && isset($_SESSION['member_csrf_token']) && hash_equals($_SESSION['member_csrf_token'], $token);
}

/**
 * 会员端 CSRF 校验失败时统一输出
 */
function requireMemberCsrfToken($json = false)
{
    if (verifyMemberCsrfToken()) {
        return true;
    }
    http_response_code(403);
    if ($json) {
        while (ob_get_level() > 0) { ob_end_clean(); }
        header('Content-Type: application/json; charset=utf-8');
        echo json_encode(['code' => 403, 'msg' => '非法请求，CSRF校验失败']);
    } else {
        echo '<script>alert("非法请求，CSRF校验失败");history.back();</script>';
    }
    exit;
}

/**
 * 发送站内消息通知管理员
 * @param string $title   消息标题
 * @param string $content 消息内容
 * @param string $fromName 发送者名称
 */
function sendAdminMessage($title, $content, $fromName = '系统通知')
{
    $db = \Core\Db\DB::getInstance();
    $db->insert('message', [
        'from_id'   => 0,
        'from_name' => $fromName,
        'to_id'     => 0,
        'to_name'   => '管理员',
        'title'     => $title,
        'content'   => $content,
        'is_read'   => 0,
        'create_time' => time()
    ]);
}

/**
 * 记录页面浏览（每次访问自动累加）
 */
function cmsEnsureFavoriteTable($db)
{
    return true;
}

function cmsEnsureMemberMessageTable($db)
{
    return true;
}

function sendMemberMessage($memberId, $title, $content, $fromName = '系统通知')
{
    $memberId = intval($memberId);
    $title = trim((string) $title);
    $content = trim((string) $content);
    if ($memberId <= 0 || $title === '' || $content === '') { return false; }
    $db = \Core\Db\DB::getInstance();
    cmsEnsureMemberMessageTable($db);
    return $db->insert('member_message', [
        'from_id' => 0,
        'from_name' => mb_substr(trim((string) $fromName), 0, 50, 'UTF-8'),
        'to_id' => $memberId,
        'to_name' => '',
        'title' => mb_substr($title, 0, 200, 'UTF-8'),
        'content' => mb_substr($content, 0, 5000, 'UTF-8'),
        'is_read' => 0,
        'read_time' => 0,
        'create_time' => time(),
    ]);
}

function trackPageView()
{
    $db = \Core\Db\DB::getInstance();
    
    // 获取今日开始时间戳
    $todayStart = strtotime(date('Y-m-d'));
    
    // 今日浏览量+1
    $db->query("UPDATE {$db->table('config')} SET config_value = config_value + 1 WHERE config_key = 'today_views'");
    
    // 如果今日浏览量记录的时间不是今天，重置为1
    $lastDate = getSiteConfig('views_date', '');
    $today = date('Y-m-d');
    if ($lastDate != $today) {
        $db->query("UPDATE {$db->table('config')} SET config_value = 1 WHERE config_key = 'today_views'");
        $db->query("UPDATE {$db->table('config')} SET config_value = ? WHERE config_key = 'views_date'", [$today]);
    }
    
    // 总浏览量+1
    $db->query("UPDATE {$db->table('config')} SET config_value = config_value + 1 WHERE config_key = 'total_views'");
}

/**
 * 获取站点浏览量
 */
function getSiteViews()
{
    $today = date('Y-m-d');
    $lastDate = getSiteConfig('views_date', '');
    
    // 如果记录日期不是今天，今日浏览量需要重置
    if ($lastDate != $today) {
        return [
            'today_views' => 0,
            'total_views' => intval(getSiteConfig('total_views', 0))
        ];
    }
    
    return [
        'today_views' => intval(getSiteConfig('today_views', 0)),
        'total_views' => intval(getSiteConfig('total_views', 0))
    ];
}

if (!function_exists('cmsBoolConfigEnabled')) {
    function cmsBoolConfigEnabled($key, $default = false)
    {
        $value = strtolower(trim((string) getSiteConfig($key, $default ? '1' : '0')));
        return in_array($value, ['1', 'true', 'on', 'yes', 'enabled', 'static'], true);
    }
}

if (!function_exists('cmsRewriteModeEnabled')) {
    function cmsRewriteModeEnabled()
    {
        $mode = strtolower(trim((string) getSiteConfig('url_mode', 'dynamic')));
        if ($mode === 'dynamic') {
            return false;
        }
        return cmsBoolConfigEnabled('rewrite_auto_enabled', false);
    }
}

if (!function_exists('cmsGenerateRewriteRules')) {
    function cmsGenerateRewriteRules($basePath = '')
    {
        $basePath = trim((string) $basePath);
        $basePath = $basePath === '' ? '/' : '/' . trim($basePath, '/') . '/';
        $rootIndex = $basePath . 'index.php';

        $apache = "RewriteEngine On\n"
            . "RewriteBase " . $basePath . "\n"
            . "RewriteCond %{REQUEST_FILENAME} !-f\n"
            . "RewriteCond %{REQUEST_FILENAME} !-d\n"
            . "RewriteRule ^ " . ltrim($rootIndex, '/') . " [L,QSA]\n";

        $nginx = "try_files \$uri \$uri/ {$rootIndex}?\$query_string;\n";

        return [
            'apache' => $apache,
            'nginx' => $nginx
        ];
    }
}

if (!function_exists('cmsWriteRewriteConfigFiles')) {
    function cmsWriteRewriteConfigFiles()
    {
        $basePath = trim(parse_url((string) getSiteConfig('site_url', ''), PHP_URL_PATH), '/');
        $rules = cmsGenerateRewriteRules($basePath);
        $root = rtrim(str_replace('\\', '/', dirname(__DIR__, 2)), '/');
        file_put_contents($root . '/.htaccess', (string) ($rules['apache'] ?? ''));
        file_put_contents($root . '/nginx.htaccess', (string) ($rules['nginx'] ?? ''));
        return [
            'apache' => $root . '/.htaccess',
            'nginx' => $root . '/nginx.htaccess'
        ];
    }
}

if (!function_exists('cmsNormalizeRelativeUrl')) {
    function cmsNormalizeRelativeUrl($url)
    {
        $url = trim((string) $url);
        if ($url === '') {
            return '/';
        }
        if (preg_match('#^https?://#i', $url)) {
            $path = parse_url($url, PHP_URL_PATH);
            $query = parse_url($url, PHP_URL_QUERY);
            return $path . ($query ? '?' . $query : '');
        }
        return strpos($url, '/') === 0 ? $url : '/' . $url;
    }
}

if (!function_exists('cmsResolveStaticRoute')) {
    function cmsResolveStaticRoute($requestPath)
    {
        $requestPath = trim((string) $requestPath);
        $requestPath = preg_replace('#^/public/#', '/', $requestPath);
        $requestPath = '/' . ltrim($requestPath, '/');
        $pathOnly = parse_url($requestPath, PHP_URL_PATH);
        $pathOnly = rtrim((string) $pathOnly, '/');
        if ($pathOnly === '') {
            return ['module' => 'index', 'action' => 'index', 'id' => 0, 'keyword' => '', 'dir' => ''];
        }
        if ($pathOnly === '/search') {
            return ['module' => 'search', 'action' => 'index', 'id' => 0, 'keyword' => trim((string) ($_GET['keyword'] ?? '')), 'dir' => ''];
        }
        if ($pathOnly === '/guestbook') {
            return ['module' => 'guestbook', 'action' => 'index', 'id' => 0, 'keyword' => '', 'dir' => ''];
        }
        $detailMap = [
            '#^/article/([0-9]+)\.html$#i' => 'article',
            '#^/product/([0-9]+)\.html$#i' => 'product',
            '#^/page/([0-9]+)\.html$#i' => 'page',
            '#^/image/([0-9]+)\.html$#i' => 'image',
            '#^/download/([0-9]+)\.html$#i' => 'download',
            '#^/video/([0-9]+)\.html$#i' => 'video',
            '#^/recruit/([0-9]+)\.html$#i' => 'recruit',
        ];
        foreach ($detailMap as $pattern => $module) {
            if (preg_match($pattern, $pathOnly, $matches)) {
                return ['module' => $module, 'action' => 'index', 'id' => intval($matches[1]), 'keyword' => '', 'dir' => ''];
            }
        }
        $listMap = [
            '/article' => 'article',
            '/product' => 'product',
            '/image' => 'image',
            '/download' => 'download',
            '/video' => 'video',
            '/recruit' => 'recruit',
        ];
        if (isset($listMap[$pathOnly])) {
            return ['module' => $listMap[$pathOnly], 'action' => 'index', 'id' => 0, 'keyword' => '', 'dir' => ''];
        }
        $dir = trim($pathOnly, '/');
        if ($dir !== '') {
            return ['module' => 'channel', 'action' => 'index', 'id' => 0, 'keyword' => '', 'dir' => $dir];
        }
        return ['module' => 'index', 'action' => 'index', 'id' => 0, 'keyword' => '', 'dir' => ''];
    }
}

if (!function_exists('cmsInternalLinkDictionary')) {
    function cmsInternalLinkDictionary($limit = 300)
    {
        static $cache = null;
        if ($cache !== null) {
            return $cache;
        }
        $db = \Core\Db\DB::getInstance();
        $rows = [];
        $sources = [
            ['table' => 'article', 'type' => 'article', 'sql' => "SELECT id, title, channel_id, create_time FROM {$db->table('article')} WHERE status = 1 ORDER BY create_time DESC, id DESC LIMIT " . intval($limit)],
            ['table' => 'product', 'type' => 'product', 'sql' => "SELECT id, title, channel_id, create_time FROM {$db->table('product')} WHERE status = 1 ORDER BY create_time DESC, id DESC LIMIT " . intval($limit)],
            ['table' => 'page', 'type' => 'page', 'sql' => "SELECT id, page_title AS title, channel_id, create_time FROM {$db->table('page')} WHERE status = 1 ORDER BY create_time DESC, id DESC LIMIT " . intval($limit)],
        ];
        foreach ($sources as $source) {
            foreach ($db->fetchAll($source['sql']) as $row) {
                $title = trim((string) ($row['title'] ?? ''));
                if ($title === '' || mb_strlen($title, 'UTF-8') < 2) {
                    continue;
                }
                $rows[] = [
                    'keyword' => $title,
                    'url' => cmsBuildFrontendUrl($source['type'], intval($row['id'] ?? 0)),
                    'type' => $source['type'],
                    'id' => intval($row['id'] ?? 0),
                ];
            }
        }
        usort($rows, function ($a, $b) {
            return mb_strlen($b['keyword'], 'UTF-8') <=> mb_strlen($a['keyword'], 'UTF-8');
        });
        $cache = $rows;
        return $cache;
    }
}

if (!function_exists('cmsInjectInternalLinks')) {
    function cmsInjectInternalLinks($html, $currentType = '', $currentId = 0)
    {
        if (!cmsBoolConfigEnabled('seo_internal_link_enabled', false)) {
            return (string) $html;
        }
        $html = (string) $html;
        if ($html === '' || stripos($html, '<a ') === false && stripos($html, '<p') === false && stripos($html, '<div') === false) {
            return $html;
        }
        $maxLinks = max(1, intval(getSiteConfig('seo_internal_link_max', 3)));
        $dictionary = cmsInternalLinkDictionary();
        if (empty($dictionary)) {
            return $html;
        }
        $linkCount = 0;
        $parts = preg_split('/(<a\b[^>]*>.*?<\/a>)/is', $html, -1, PREG_SPLIT_DELIM_CAPTURE);
        foreach ($parts as $index => $part) {
            if ($linkCount >= $maxLinks) {
                break;
            }
            if (preg_match('/^<a\b/i', $part)) {
                continue;
            }
            foreach ($dictionary as $item) {
                if ($linkCount >= $maxLinks) {
                    break;
                }
                if ($item['type'] === $currentType && intval($item['id']) === intval($currentId)) {
                    continue;
                }
                $keyword = trim((string) $item['keyword']);
                if ($keyword === '' || mb_strlen($keyword, 'UTF-8') < 2) {
                    continue;
                }
                if (mb_stripos(strip_tags($part), $keyword, 0, 'UTF-8') === false) {
                    continue;
                }
                $pattern = '/' . preg_quote($keyword, '/') . '/u';
                $replacement = '<a href="' . htmlspecialchars($item['url'], ENT_QUOTES, 'UTF-8') . '" class="cms-auto-internal-link">' . $keyword . '</a>';
                $newPart = preg_replace($pattern, $replacement, $part, 1, $replaceCount);
                if ($replaceCount > 0 && $newPart !== null) {
                    $parts[$index] = $newPart;
                    $part = $newPart;
                    $linkCount++;
                }
            }
        }
        return implode('', $parts);
    }
}

if (!function_exists('cmsApplyInternalLinksToRow')) {
    function cmsApplyInternalLinksToRow(array $row, $type, $contentField = 'content')
    {
        if (!isset($row[$contentField])) {
            return $row;
        }
        $row[$contentField] = cmsInjectInternalLinks((string) $row[$contentField], (string) $type, intval($row['id'] ?? 0));
        return $row;
    }
}

if (!function_exists('cmsRepairInternalHref')) {
    function cmsRepairInternalHref($href)
    {
        $href = trim((string) $href);
        if ($href === '' || strpos($href, 'javascript:') === 0 || strpos($href, '#') === 0 || strpos($href, 'mailto:') === 0 || strpos($href, 'tel:') === 0) {
            return ['href' => $href, 'changed' => false];
        }

        $siteUrl = rtrim((string) getSiteConfig('site_url', ''), '/');
        $isAbsoluteInternal = false;
        if (preg_match('#^https?://#i', $href)) {
            if ($siteUrl === '' || stripos($href, $siteUrl) !== 0) {
                return ['href' => $href, 'changed' => false];
            }
            $isAbsoluteInternal = true;
        }

        $relative = cmsNormalizeRelativeUrl($href);
        $newHref = $relative;
        $db = \Core\Db\DB::getInstance();

        if (preg_match('#^/public/index\.php\?m=(article|product|page|image|download|video|recruit)&id=([0-9]+)#i', $relative, $matches)) {
            $type = strtolower($matches[1]);
            $id = intval($matches[2]);
            if (cmsContentExistsByType($type, $id)) {
                $newHref = cmsBuildFrontendUrl($type, $id);
            }
        } elseif (preg_match('#^/(article|product|page|image|download|video|recruit)/([0-9]+)\.html$#i', parse_url($relative, PHP_URL_PATH), $matches)) {
            $type = strtolower($matches[1]);
            $id = intval($matches[2]);
            if (cmsContentExistsByType($type, $id)) {
                $newHref = cmsBuildFrontendUrl($type, $id);
            }
        } elseif (preg_match('#^/public/index\.php\?m=channel&id=([0-9]+)#i', $relative, $matches)) {
            $channelId = intval($matches[1]);
            $channel = $db->fetch("SELECT id, channel_dir, redirect_url FROM {$db->table('channel')} WHERE id = ? AND is_show = 1 LIMIT 1", [$channelId]);
            if ($channel) {
                $newHref = cmsBuildChannelUrl($channel);
            }
        } elseif (preg_match('#^/channel/([0-9]+)/?$#i', parse_url($relative, PHP_URL_PATH), $matches)) {
            $channelId = intval($matches[1]);
            $channel = $db->fetch("SELECT id, channel_dir, redirect_url FROM {$db->table('channel')} WHERE id = ? AND is_show = 1 LIMIT 1", [$channelId]);
            if ($channel) {
                $newHref = cmsBuildChannelUrl($channel);
            }
        } elseif (preg_match('#^/public/index\.php\?m=(search|guestbook)$#i', $relative, $matches)) {
            $newHref = cmsBuildFrontendUrl(strtolower($matches[1]), 0);
        } elseif (preg_match('#^/(search|guestbook)/?$#i', parse_url($relative, PHP_URL_PATH), $matches)) {
            $newHref = cmsBuildFrontendUrl(strtolower($matches[1]), 0);
        }

        if ($isAbsoluteInternal && $newHref !== $relative) {
            $newHref = cmsAbsoluteUrl($newHref);
        }

        return [
            'href' => $newHref,
            'changed' => $newHref !== $href
        ];
    }
}

if (!function_exists('cmsRepairContentInternalLinks')) {
    function cmsRepairContentInternalLinks($content)
    {
        $content = (string) $content;
        if ($content === '') {
            return ['content' => $content, 'count' => 0];
        }

        $count = 0;
        $newContent = preg_replace_callback(
            '#(<a\b[^>]*\shref=)(["\'])([^"\']+)(\2)#i',
            function ($matches) use (&$count) {
                $result = cmsRepairInternalHref($matches[3]);
                if (!empty($result['changed'])) {
                    $count++;
                    return $matches[1] . $matches[2] . $result['href'] . $matches[4];
                }
                return $matches[0];
            },
            $content
        );

        return [
            'content' => $newContent !== null ? $newContent : $content,
            'count' => $count
        ];
    }
}

if (!function_exists('cmsDetectBrokenLinks')) {
    function cmsDetectBrokenLinks($limitPerTable = 100)
    {
        $db = \Core\Db\DB::getInstance();
        $tables = [
            'article' => ['pk' => 'id', 'title' => 'title', 'content' => 'content', 'status' => 'status'],
            'product' => ['pk' => 'id', 'title' => 'title', 'content' => 'content', 'status' => 'status'],
            'page' => ['pk' => 'id', 'title' => 'title', 'content' => 'content', 'status' => 'status'],
            'download' => ['pk' => 'id', 'title' => 'title', 'content' => 'content', 'status' => 'status'],
            'video' => ['pk' => 'id', 'title' => 'title', 'content' => 'content', 'status' => 'status'],
            'recruit' => ['pk' => 'id', 'title' => 'title', 'content' => 'content', 'status' => 'status'],
            'image' => ['pk' => 'id', 'title' => 'title', 'content' => 'content', 'status' => 'status'],
        ];
        $results = [];
        foreach ($tables as $table => $meta) {
            $columns = $db->fetchAll("SHOW COLUMNS FROM {$db->table($table)}");
            $fieldMap = [];
            foreach ($columns as $column) {
                $fieldName = strtolower((string) ($column['Field'] ?? ''));
                if ($fieldName !== '') {
                    $fieldMap[$fieldName] = true;
                }
            }

            $titleFieldCandidates = [$meta['title'], 'title', 'name', 'page_title', 'pro_name', 'art_title'];
            $contentFieldCandidates = [$meta['content'], 'content', 'details', 'description', 'body'];
            $titleField = '';
            $contentField = '';

            foreach ($titleFieldCandidates as $candidate) {
                $candidate = strtolower((string) $candidate);
                if ($candidate !== '' && isset($fieldMap[$candidate])) {
                    $titleField = $candidate;
                    break;
                }
            }
            foreach ($contentFieldCandidates as $candidate) {
                $candidate = strtolower((string) $candidate);
                if ($candidate !== '' && isset($fieldMap[$candidate])) {
                    $contentField = $candidate;
                    break;
                }
            }

            if ($contentField === '') {
                continue;
            }

            $selectTitle = $titleField !== '' ? $titleField . ' AS title' : "'' AS title";
            $rows = $db->fetchAll("SELECT {$meta['pk']} AS id, {$selectTitle}, {$contentField} AS content FROM {$db->table($table)} WHERE {$meta['status']} = 1 ORDER BY id DESC LIMIT " . intval($limitPerTable));
            foreach ($rows as $row) {
                preg_match_all('/<a\b[^>]*href=["\']([^"\']+)["\']/i', (string) ($row['content'] ?? ''), $matches);
                foreach (($matches[1] ?? []) as $href) {
                    $href = trim((string) $href);
                    if ($href === '' || strpos($href, 'javascript:') === 0 || strpos($href, '#') === 0 || strpos($href, 'mailto:') === 0 || strpos($href, 'tel:') === 0) {
                        continue;
                    }
                    $issue = cmsInspectBrokenLink($href);
                    if ($issue !== null) {
                        $results[] = [
                            'table' => $table,
                            'id' => intval($row['id'] ?? 0),
                            'title' => (string) ($row['title'] ?? ''),
                            'href' => $href,
                            'issue' => $issue['issue'],
                            'suggestion' => $issue['suggestion'],
                        ];
                    }
                }
            }
        }
        return $results;
    }
}

if (!function_exists('cmsInspectBrokenLink')) {
    function cmsInspectBrokenLink($href)
    {
        $href = trim((string) $href);
        $relative = cmsNormalizeRelativeUrl($href);
        if (preg_match('#^https?://#i', $href)) {
            $siteUrl = rtrim((string) getSiteConfig('site_url', ''), '/');
            if ($siteUrl !== '' && stripos($href, $siteUrl) !== 0) {
                return null;
            }
        }
        if (preg_match('#^/public/index\.php\?m=([a-z_]+)&id=([0-9]+)#i', $relative, $matches)) {
            $module = strtolower($matches[1]);
            $id = intval($matches[2]);
            if (!cmsContentExistsByType($module, $id)) {
                return ['issue' => '内容不存在', 'suggestion' => '目标 ' . $module . ':' . $id . ' 已不存在，建议替换或删除该链接'];
            }
            if (cmsRewriteModeEnabled()) {
                return ['issue' => '仍在使用动态链接', 'suggestion' => '可替换为 ' . cmsBuildFrontendUrl($module, $id)];
            }
            return null;
        }
        if (preg_match('#^/(article|product|page|image|download|video|recruit)/([0-9]+)\.html$#i', parse_url($relative, PHP_URL_PATH), $matches)) {
            $module = strtolower($matches[1]);
            $id = intval($matches[2]);
            if (!cmsContentExistsByType($module, $id)) {
                return ['issue' => '伪静态目标不存在', 'suggestion' => '目标 ' . $module . ':' . $id . ' 不存在，建议修正或删除'];
            }
            return null;
        }
        if (preg_match('#^/([a-zA-Z0-9_-]+)/?$#', parse_url($relative, PHP_URL_PATH), $matches)) {
            $dir = strtolower($matches[1]);
            $db = \Core\Db\DB::getInstance();
            $row = $db->fetch("SELECT id FROM {$db->table('channel')} WHERE channel_dir = ? AND is_show = 1 LIMIT 1", [$dir]);
            if (!$row) {
                return ['issue' => '栏目目录不存在', 'suggestion' => '栏目目录 ' . $dir . ' 不存在，建议改为有效栏目链接'];
            }
            return null;
        }
        return null;
    }
}

if (!function_exists('cmsContentExistsByType')) {
    function cmsContentExistsByType($type, $id)
    {
        $type = strtolower(trim((string) $type));
        $id = intval($id);
        if ($id <= 0) {
            return false;
        }
        $map = [
            'article' => 'article',
            'product' => 'product',
            'page' => 'page',
            'image' => 'image',
            'download' => 'download',
            'video' => 'video',
            'recruit' => 'recruit',
        ];
        if (!isset($map[$type])) {
            return false;
        }
        $db = \Core\Db\DB::getInstance();
        $row = $db->fetch("SELECT id FROM {$db->table($map[$type])} WHERE id = ? LIMIT 1", [$id]);
        return !empty($row);
    }
}

if (!function_exists('cmsBaseSiteUrl')) {
    function cmsBaseSiteUrl()
    {
        return rtrim((string) getSiteConfig('site_url', 'http://' . ($_SERVER['HTTP_HOST'] ?? 'localhost')), '/');
    }
}

if (!function_exists('cmsAbsoluteUrl')) {
    function cmsAbsoluteUrl($path)
    {
        $path = (string) $path;
        if (preg_match('#^https?://#i', $path)) {
            return $path;
        }
        return cmsBaseSiteUrl() . (strpos($path, '/') === 0 ? $path : '/' . $path);
    }
}

if (!function_exists('cmsCollectSitemapUrls')) {
    function cmsCollectSitemapUrls($limitPerType = 500)
    {
        $db = \Core\Db\DB::getInstance();
        $items = [];
        $push = function ($loc, $lastmod = null, $changefreq = 'weekly', $priority = '0.6') use (&$items) {
            $loc = trim((string) $loc);
            if ($loc === '') {
                return;
            }
            $items[$loc] = [
                'loc' => $loc,
                'lastmod' => $lastmod,
                'changefreq' => $changefreq,
                'priority' => $priority
            ];
        };

        $push(cmsAbsoluteUrl('/'), date('c'), 'daily', '1.0');

        $channels = $db->fetchAll("SELECT id, channel_dir, update_time FROM {$db->table('channel')} WHERE is_show = 1 ORDER BY id DESC LIMIT " . intval($limitPerType));
        foreach ($channels as $row) {
            $push(
                cmsAbsoluteUrl(cmsBuildChannelUrl($row)),
                !empty($row['update_time']) ? date('c', intval($row['update_time'])) : null,
                'weekly',
                '0.8'
            );
        }

        $map = [
            'article' => ['table' => 'article', 'freq' => 'daily', 'priority' => '0.8'],
            'product' => ['table' => 'product', 'freq' => 'weekly', 'priority' => '0.8'],
            'page' => ['table' => 'page', 'freq' => 'monthly', 'priority' => '0.7'],
            'image' => ['table' => 'image', 'freq' => 'weekly', 'priority' => '0.7'],
            'download' => ['table' => 'download', 'freq' => 'weekly', 'priority' => '0.7'],
            'video' => ['table' => 'video', 'freq' => 'weekly', 'priority' => '0.7'],
            'recruit' => ['table' => 'recruit', 'freq' => 'weekly', 'priority' => '0.7'],
        ];
        foreach ($map as $type => $meta) {
            $rows = $db->fetchAll("SELECT id, create_time, update_time FROM {$db->table($meta['table'])} WHERE status = 1 ORDER BY id DESC LIMIT " . intval($limitPerType));
            foreach ($rows as $row) {
                $timestamp = intval($row['update_time'] ?? 0) ?: intval($row['create_time'] ?? 0);
                $push(
                    cmsAbsoluteUrl(cmsBuildFrontendUrl($type, intval($row['id'] ?? 0))),
                    $timestamp > 0 ? date('c', $timestamp) : null,
                    $meta['freq'],
                    $meta['priority']
                );
            }
        }

        return array_values($items);
    }
}

if (!function_exists('cmsGenerateSitemapXml')) {
    function cmsGenerateSitemapXml($limitPerType = 500)
    {
        $urls = cmsCollectSitemapUrls($limitPerType);
        $xml = "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n";
        $xml .= "<urlset xmlns=\"http://www.sitemaps.org/schemas/sitemap/0.9\">\n";
        foreach ($urls as $item) {
            $xml .= "  <url>\n";
            $xml .= "    <loc>" . htmlspecialchars($item['loc'], ENT_XML1, 'UTF-8') . "</loc>\n";
            if (!empty($item['lastmod'])) {
                $xml .= "    <lastmod>" . htmlspecialchars($item['lastmod'], ENT_XML1, 'UTF-8') . "</lastmod>\n";
            }
            if (!empty($item['changefreq'])) {
                $xml .= "    <changefreq>" . htmlspecialchars($item['changefreq'], ENT_XML1, 'UTF-8') . "</changefreq>\n";
            }
            if (!empty($item['priority'])) {
                $xml .= "    <priority>" . htmlspecialchars($item['priority'], ENT_XML1, 'UTF-8') . "</priority>\n";
            }
            $xml .= "  </url>\n";
        }
        $xml .= "</urlset>\n";
        return $xml;
    }
}

if (!function_exists('cmsGenerateRobotsTxt')) {
    function cmsGenerateRobotsTxt()
    {
        $lines = [
            'User-agent: *',
            'Allow: /',
            'Disallow: /admin/',
            'Disallow: /core/',
            'Disallow: /install/',
            'Disallow: /license-center/',
        ];
        $extra = trim((string) getSiteConfig('robots_extra_rules', ''));
        if ($extra !== '') {
            $lines[] = '';
            $lines[] = $extra;
        }
        $lines[] = '';
        $lines[] = 'Sitemap: ' . cmsAbsoluteUrl('/sitemap.xml');
        return implode("\n", $lines) . "\n";
    }
}

if (!function_exists('cmsEnsureIndexNowKey')) {
    function cmsEnsureIndexNowKey()
    {
        $key = trim((string) getSiteConfig('indexnow_key', ''));
        if ($key === '') {
            $key = substr(hash('sha256', uniqid('indexnow_', true)), 0, 32);
            $db = \Core\Db\DB::getInstance();
            $exists = $db->fetch("SELECT id FROM {$db->table('config')} WHERE config_key = 'indexnow_key' LIMIT 1");
            if ($exists) {
                $db->update('config', ['config_value' => $key], 'config_key = ?', ['indexnow_key']);
            } else {
                $db->insert('config', [
                    'config_key' => 'indexnow_key',
                    'config_value' => $key,
                    'config_type' => 'text',
                    'config_group' => 'basic'
                ]);
            }
        }
        return $key;
    }
}

if (!function_exists('cmsWriteSeoStaticFiles')) {
    function cmsWriteSeoStaticFiles()
    {
        $root = rtrim(str_replace('\\', '/', dirname(__DIR__, 2)), '/');
        $sitemapPath = $root . '/sitemap.xml';
        $robotsPath = $root . '/robots.txt';
        file_put_contents($sitemapPath, cmsGenerateSitemapXml());
        file_put_contents($robotsPath, cmsGenerateRobotsTxt());

        $indexNowEnabled = cmsBoolConfigEnabled('indexnow_enabled', false);
        $keyPath = '';
        if ($indexNowEnabled) {
            $key = cmsEnsureIndexNowKey();
            $keyPath = $root . '/' . $key . '.txt';
            file_put_contents($keyPath, $key);
        }
        return [
            'sitemap' => $sitemapPath,
            'robots' => $robotsPath,
            'indexnow_key_file' => $keyPath
        ];
    }
}

if (!function_exists('cmsPushUrlsToIndexNow')) {
    function cmsPushUrlsToIndexNow(array $urls)
    {
        if (!cmsBoolConfigEnabled('indexnow_enabled', false)) {
            return ['success' => false, 'message' => 'IndexNow 未开启'];
        }
        $urls = array_values(array_unique(array_filter(array_map('trim', $urls))));
        if (empty($urls)) {
            return ['success' => false, 'message' => '没有可推送的 URL'];
        }
        $key = cmsEnsureIndexNowKey();
        $payload = json_encode([
            'host' => parse_url(cmsBaseSiteUrl(), PHP_URL_HOST),
            'key' => $key,
            'keyLocation' => cmsAbsoluteUrl('/' . $key . '.txt'),
            'urlList' => array_values(array_map('cmsAbsoluteUrl', $urls))
        ], JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES);
        $ch = curl_init('https://api.indexnow.org/indexnow');
        curl_setopt_array($ch, [
            CURLOPT_POST => true,
            CURLOPT_POSTFIELDS => $payload,
            CURLOPT_RETURNTRANSFER => true,
            CURLOPT_HTTPHEADER => ['Content-Type: application/json; charset=utf-8'],
            CURLOPT_TIMEOUT => 20
        ]);
        $response = curl_exec($ch);
        $httpCode = intval(curl_getinfo($ch, CURLINFO_HTTP_CODE));
        $error = curl_error($ch);
        curl_close($ch);
        return [
            'success' => $httpCode >= 200 && $httpCode < 300,
            'message' => $error !== '' ? $error : ('HTTP ' . $httpCode),
            'response' => $response
        ];
    }
}

if (!function_exists('cmsPushUrlsToBaidu')) {
    function cmsPushUrlsToBaidu(array $urls)
    {
        if (!cmsBoolConfigEnabled('baidu_push_enabled', false)) {
            return ['success' => false, 'message' => '百度主动推送未开启'];
        }
        $token = trim((string) getSiteConfig('baidu_push_token', ''));
        if ($token === '') {
            return ['success' => false, 'message' => '未配置百度推送 token'];
        }
        $site = parse_url(cmsBaseSiteUrl(), PHP_URL_HOST);
        if ($site === '') {
            return ['success' => false, 'message' => '站点域名解析失败'];
        }
        $urls = array_values(array_unique(array_filter(array_map(function ($url) {
            return cmsAbsoluteUrl($url);
        }, $urls))));
        if (empty($urls)) {
            return ['success' => false, 'message' => '没有可推送的 URL'];
        }
        $api = 'http://data.zz.baidu.com/urls?site=' . urlencode($site) . '&token=' . urlencode($token);
        $ch = curl_init($api);
        curl_setopt_array($ch, [
            CURLOPT_POST => true,
            CURLOPT_POSTFIELDS => implode("\n", $urls),
            CURLOPT_RETURNTRANSFER => true,
            CURLOPT_HTTPHEADER => ['Content-Type: text/plain'],
            CURLOPT_TIMEOUT => 20
        ]);
        $response = curl_exec($ch);
        $httpCode = intval(curl_getinfo($ch, CURLINFO_HTTP_CODE));
        $error = curl_error($ch);
        curl_close($ch);
        return [
            'success' => $httpCode >= 200 && $httpCode < 300,
            'message' => $error !== '' ? $error : ('HTTP ' . $httpCode),
            'response' => $response
        ];
    }
}
