Hongmu Notes
Home Summary of pitfalls Highly efficient web page caching page code
Summary of pitfalls

Highly efficient web page caching page code

Highly efficient web page caching page code

I studied it before,High-performance PHP cache system design: intelligent cleaning and counting management

But I found that this can only cache individual html, not some data that you want to cache.

At the same time, in order to solve the limitations of inode.

So I tried to put the cache files into the db database to achieve efficient storage.

1. System Overview

  • core classApiCache(located at ApiCache.php

  • cache mode

    • Ordinary reading and writingget / set) - for internal or manual operation

    • Full page cachingpage) - Capture the entire output and exit

  • Cleanup mechanism: Standalone CLI script ClearCache.php, to achieve low-frequency elimination + over-limit deletion

Caching code:

<?php
// ============================================================
//  ApiCache.php - 高性能 SQLite 分片缓存(V6.4 定稿版)
//  分片:1=单库  2=4096(默认)  3=65536
//  兼容:PHP 5.6 ~ 8.x
//
//  V6.4 变更(相对 V6.3):
//    - 消除冗余的 $dbName 判空(构造函数已保证)
//    - 魔数 500 → 常量 CHUNK_SIZE
//    - page() 错误类型数组 → 常量 FATAL_ERRORS
// ============================================================

if (!defined('CACHE_ROOT')) {
    throw new Exception("必须定义 CACHE_ROOT 常量");
}
if (!defined('CACHE_DEFAULT_SHARD')) {
    define('CACHE_DEFAULT_SHARD', 2);
}
if (!defined('CACHE_DEFAULT_COUNT')) {
    define('CACHE_DEFAULT_COUNT', 30);
}

class ApiCache
{
    const COMPRESS_THRESHOLD = 200;
    const COMPRESS_LEVEL = 6;
    const CHUNK_SIZE = 500;   // 批量查询/删除时每批条数

    // 致命错误类型(page() 用于判断是否缓存)
    const FATAL_ERRORS = array(
        E_ERROR, E_PARSE, E_CORE_ERROR, E_COMPILE_ERROR, E_USER_ERROR
    );

    private static $countConfig = null;

    private $dirName;
    private $root;
    private $level1;
    private $level2;
    private $dbName;
    private $dbPool = array();
    private $probability = 0;

    public function __construct($dirName, $shardType = null, $dbName = null)
    {
        // 清洗 dirName,防止路径穿越
        $clean = preg_replace('/[^a-zA-Z0-9_\-]/', '', (string)$dirName);
        if ($clean === '') {
            throw new Exception("无效的缓存目录名: " . $dirName);
        }
        $this->dirName = $clean;
        $this->root = rtrim(CACHE_ROOT, '/') . '/' . $clean . '/';

        if (!is_dir($this->root)) {
            if (!mkdir($this->root, 0755, true)) {
                throw new Exception("无法创建目录: " . $this->root);
            }
        }

        if ($shardType === null) $shardType = CACHE_DEFAULT_SHARD;
        $map = array(
            1 => array(0, 0),
            2 => array(1, 3),
            3 => array(2, 4),
        );
        if (!isset($map[$shardType])) {
            throw new Exception("无效分片类型: " . $shardType);
        }
        list($this->level1, $this->level2) = $map[$shardType];

        self::loadCountConfig();
        $this->probability = isset(self::$countConfig[$clean])
            ? max(0, min(100, (int)self::$countConfig[$clean]))
            : (int) CACHE_DEFAULT_COUNT;

        if ($this->level1 === 0) {
            $name = ($dbName === null || $dbName === '') ? 'single' : (string)$dbName;
            $name = preg_replace('/[^a-zA-Z0-9_\-]/', '', $name);
            $this->dbName = ($name === '') ? 'single' : $name;
        } else {
            $this->dbName = null;
        }
    }

    // ============================================================
    //  配置
    // ============================================================
    private static function loadCountConfig()
    {
        if (self::$countConfig !== null) return;
        $file = rtrim(CACHE_ROOT, '/') . '/_config.php';
        self::$countConfig = file_exists($file) ? (array)@include $file : array();
    }

    // ============================================================
    //  编解码
    // ============================================================
    private function decodeContent($blob)
    {
        if ($blob === null || $blob === '') return false;
        $raw = @gzuncompress($blob);
        $content = ($raw !== false) ? $raw : $blob;
        return ($content === '') ? false : $content;
    }

    private function encodeContent($data)
    {
        if (is_array($data) || is_object($data)) {
            $flags = JSON_UNESCAPED_UNICODE;
            if (defined('JSON_INVALID_UTF8_IGNORE')) $flags |= JSON_INVALID_UTF8_IGNORE;
            $json = json_encode($data, $flags);
            $data = ($json !== false) ? $json : serialize($data);
        }
        $data = (string) $data;
        if ($data === '') return false;
        return (strlen($data) < self::COMPRESS_THRESHOLD)
            ? $data
            : gzcompress($data, self::COMPRESS_LEVEL);
    }

    private function isExpired($expireTime, $now = null)
    {
        if ($expireTime <= 0) return false;
        if ($now === null) $now = time();
        return $expireTime < $now;
    }

    /**
     * 写入单条记录(set 和 setMulti 共用)
     * UPDATE 优先(保留 access_count / first_time),不存在则 INSERT
     */
    private function writeEntry($db, $md5, $compressed, $expire, $now)
    {
        try {
            $stmt = $db->prepare("UPDATE cache SET content = ?, expire_time = ? WHERE md5 = ?");
            $stmt->execute(array($compressed, $expire, $md5));
            if ($stmt->rowCount() > 0) return true;

            $stmt = $db->prepare("INSERT INTO cache (md5, content, access_count, first_time, expire_time) VALUES (?, ?, 0, ?, ?)");
            return $stmt->execute(array($md5, $compressed, $now, $expire));
        } catch (PDOException $e) {
            try {
                $stmt = $db->prepare("UPDATE cache SET content = ?, expire_time = ? WHERE md5 = ?");
                $stmt->execute(array($compressed, $expire, $md5));
                return true;
            } catch (Exception $e2) {
                error_log("ApiCache writeEntry error: " . $e2->getMessage());
                return false;
            }
        }
    }

    // ============================================================
    //  基础读写
    // ============================================================
    public function get($key)
    {
        $md5 = md5($key);
        $db = $this->getDb($md5);
        if (!$db) return false;

        try {
            $stmt = $db->prepare("SELECT content, expire_time FROM cache WHERE md5 = ?");
            $stmt->execute(array($md5));
            $row = $stmt->fetch(PDO::FETCH_ASSOC);
            if (!$row) return false;

            if ($this->isExpired($row['expire_time'])) {
                $del = $db->prepare("DELETE FROM cache WHERE md5 = ?");
                $del->execute(array($md5));
                return false;
            }

            if ($this->probability > 0) {
                if ($this->probability >= 100 || mt_rand(1, 100) <= $this->probability) {
                    $upd = $db->prepare("UPDATE cache SET access_count = access_count + 1 WHERE md5 = ?");
                    $upd->execute(array($md5));
                }
            }

            return $this->decodeContent($row['content']);
        } catch (Exception $e) {
            error_log("ApiCache get error: " . $e->getMessage());
            return false;
        }
    }

    public function set($key, $data, $ttl = 0)
    {
        $md5 = md5($key);
        $db = $this->getDb($md5);
        if (!$db) return false;

        $compressed = $this->encodeContent($data);
        if ($compressed === false) return false;

        $expire = ($ttl > 0) ? time() + $ttl : 0;
        return $this->writeEntry($db, $md5, $compressed, $expire, time());
    }

    public function delete($key)
    {
        $md5 = md5($key);
        $db = $this->getDb($md5);
        if (!$db) return false;
        try {
            $stmt = $db->prepare("DELETE FROM cache WHERE md5 = ?");
            $stmt->execute(array($md5));
            return $stmt->rowCount() > 0;
        } catch (Exception $e) {
            error_log("ApiCache delete error: " . $e->getMessage());
            return false;
        }
    }

    // ============================================================
    //  批量方法(仅单库模式支持)
    //
    //  ⚠️ 为什么限制:
    //    - 分片2/3 下数据天然分散,批量无法"摊薄"连接开销
    //    - 数据量小时批量 ≈ 逐条,数据量大时内存扛不住
    //    - 分片模式下请直接用 get / set / delete 循环
    // ============================================================

    /**
     * 批量读取(仅单库)
     * @return array  ['key' => 'value', ...],未命中的 key 不在结果里
     */
    public function getMulti($keys)
    {
        $db = $this->getSingleModeDb('getMulti');
        if (!$db) return array();

        if (empty($keys) || !is_array($keys)) return array();

        // md5 => 原 key
        $md5ToKey = array();
        foreach ($keys as $key) {
            $md5 = md5($key);
            if (!isset($md5ToKey[$md5])) $md5ToKey[$md5] = $key;
        }

        $result = array();
        $now = time();

        foreach (array_chunk(array_keys($md5ToKey), self::CHUNK_SIZE) as $chunk) {
            try {
                $ph = implode(',', array_fill(0, count($chunk), '?'));
                $stmt = $db->prepare("SELECT md5, content, expire_time FROM cache WHERE md5 IN ($ph)");
                $stmt->execute($chunk);

                foreach ($stmt->fetchAll(PDO::FETCH_ASSOC) as $row) {
                    if ($this->isExpired($row['expire_time'], $now)) continue;
                    $value = $this->decodeContent($row['content']);
                    if ($value === false) continue;
                    $result[$md5ToKey[$row['md5']]] = $value;
                }
            } catch (Exception $e) {
                error_log("ApiCache getMulti error: " . $e->getMessage());
            }
        }

        return $result;
    }

    /**
     * 批量写入(仅单库)
     * @return int  成功写入条数
     */
    public function setMulti($items, $ttl = 0)
    {
        $db = $this->getSingleModeDb('setMulti');
        if (!$db) return 0;

        if (empty($items) || !is_array($items)) return 0;

        $expire = ($ttl > 0) ? time() + $ttl : 0;
        $now = time();
        $success = 0;

        try {
            $db->beginTransaction();
            foreach ($items as $key => $value) {
                $compressed = $this->encodeContent($value);
                if ($compressed === false) continue;

                $md5 = md5($key);
                if ($this->writeEntry($db, $md5, $compressed, $expire, $now)) {
                    $success++;
                }
            }
            $db->commit();
        } catch (Exception $e) {
            if ($db->inTransaction()) $db->rollBack();
            error_log("ApiCache setMulti error: " . $e->getMessage());
        }

        return $success;
    }

    /**
     * 批量删除(仅单库)
     * @return int  成功删除条数
     */
    public function deleteMulti($keys)
    {
        $db = $this->getSingleModeDb('deleteMulti');
        if (!$db) return 0;

        if (empty($keys) || !is_array($keys)) return 0;

        $md5s = array();
        foreach ($keys as $key) {
            $md5s[] = md5($key);
        }

        $total = 0;
        foreach (array_chunk($md5s, self::CHUNK_SIZE) as $chunk) {
            try {
                $ph = implode(',', array_fill(0, count($chunk), '?'));
                $stmt = $db->prepare("DELETE FROM cache WHERE md5 IN ($ph)");
                $stmt->execute($chunk);
                $total += $stmt->rowCount();
            } catch (Exception $e) {
                error_log("ApiCache deleteMulti error: " . $e->getMessage());
            }
        }

        return $total;
    }

    // ============================================================
    //  全页面缓存
    // ============================================================
    public function page($key, $ttl = 0)
    {
        $content = $this->get($key);
        if ($content !== false) {
            echo $content;
            exit;
        }

        if (function_exists('error_clear_last')) error_clear_last();

        ob_start(function($buffer) use ($key, $ttl) {
            try {
                if ($buffer === '') return $buffer;

                $err = error_get_last();
                if ($err !== null && in_array($err['type'], self::FATAL_ERRORS, true)) {
                    return $buffer;
                }

                if (stripos($buffer, 'Fatal error') !== false ||
                    stripos($buffer, 'Parse error') !== false) {
                    return $buffer;
                }

                $this->set($key, $buffer, $ttl);
            } catch (Exception $e) {
                error_log('ApiCache page flush failed: ' . $e->getMessage());
            }
            return $buffer;
        });
    }

    // ============================================================
    //  内部工具
    // ============================================================

    /**
     * 获取单库连接(仅单库模式有效)
     *
     * 三个批量方法共用,同时完成"单库检查 + 获取连接"两步。
     *
     * @param  string $method  调用方方法名(仅用于日志)
     * @return PDO|false
     */
    private function getSingleModeDb($method)
    {
        if ($this->level1 !== 0) {
            error_log("ApiCache: $method 仅支持单库模式(分片1),当前为分片模式");
            return false;
        }
        return $this->getDbByFile($this->root . $this->dbName . '.db', $this->root);
    }

    private function shardPaths($md5)
    {
        if ($this->level1 === 0) {
            // 单库模式:$this->dbName 由构造函数保证有效
            return array('file' => $this->root . $this->dbName . '.db', 'dir' => $this->root);
        }
        $dir1 = substr($md5, 0, $this->level1);
        $dir2 = substr($md5, 0, $this->level2);
        $dir = $this->root . $dir1;
        return array('file' => $dir . '/' . $dir2 . '.db', 'dir' => $dir);
    }

    private function getDbByFile($dbFile, $dirPath)
    {
        if (isset($this->dbPool[$dbFile])) return $this->dbPool[$dbFile];
        $pdo = $this->openDb($dbFile, $dirPath);
        if ($pdo) $this->dbPool[$dbFile] = $pdo;
        return $pdo;
    }

    private function getDb($md5)
    {
        $p = $this->shardPaths($md5);
        return $this->getDbByFile($p['file'], $p['dir']);
    }

    /**
     * 打开数据库连接
     *
     * 只保留"0KB 空文件删除"(安全)。
     * 打开失败直接返回 false,不重试、不删主库。
     * 坏库交给清理脚本处理。
     */
    private function openDb($dbFile, $dirPath)
    {
        if (!is_dir($dirPath)) {
            if (!mkdir($dirPath, 0755, true)) {
                error_log("无法创建目录: $dirPath");
                return false;
            }
        }

        // 只删"真正的空文件":0 字节 且 无 WAL
        if (file_exists($dbFile) && filesize($dbFile) === 0
            && !file_exists($dbFile . '-wal')) {
            @unlink($dbFile);
        }

        try {
            $pdo = new PDO("sqlite:" . $dbFile);
            $pdo->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION);
            $pdo->exec("CREATE TABLE IF NOT EXISTS cache (
                md5 TEXT PRIMARY KEY,
                content BLOB,
                access_count INTEGER DEFAULT 0,
                first_time INTEGER,
                expire_time INTEGER DEFAULT 0
            )");
            $pdo->exec("CREATE INDEX IF NOT EXISTS idx_access_count ON cache(access_count)");
            $pdo->exec("CREATE INDEX IF NOT EXISTS idx_expire_time ON cache(expire_time)");
            $pdo->exec("PRAGMA journal_mode = WAL");
            $pdo->exec("PRAGMA busy_timeout = 5000");
            $pdo->exec("PRAGMA synchronous = OFF");
            return $pdo;
        } catch (PDOException $e) {
            error_log("SQLite open failed: $dbFile - " . $e->getMessage());
            return false;
        }
    }
}

// ============================================================
//  全局辅助函数
// ============================================================
function get_cache_instance($dirName, $shardType = null, $dbName = null)
{
    static $instances = array();
    $key = $dirName . '|' . $shardType . '|' . ($dbName !== null ? $dbName : '');
    if (!isset($instances[$key])) {
        $instances[$key] = new ApiCache($dirName, $shardType, $dbName);
    }
    return $instances[$key];
}

function cache_page($key, $dirName = 'Mpage', $shardType = null, $ttl = 0)
{
    $cache = get_cache_instance($dirName, $shardType);
    $cache->page($key, $ttl);
}

Clean up the code:

 隐藏内容:评论后查看
#!/usr/bin/env php
<?php
if (php_sapi_name() !== 'cli') {
    die("仅允许 CLI 运行\n");
}

// ---------- 配置区域 ----------
define('CACHE_ROOT', '/www/wwwroot/musictxt.4s5.cn/Mpage');

// 清理任务列表
$CLEAN_TASKS = [
    [
        'path'        => CACHE_ROOT . '/WpObject',
        'max_records' => 200000,   // 超过此数量则强制淘汰最冷数据
    ],
];
// ---------- 执行清理 ----------

echo "[" . date('Y-m-d H:i:s') . "] 开始清理(过期删除 + 超限冷淘汰)...\n";
$totalExpired = 0;
$totalOverLimit = 0;

foreach ($CLEAN_TASKS as $task) {
    $path = $task['path'];
    echo "\n--- 清理任务: $path ---\n";
    if (!is_dir($path)) {
        echo "  目录不存在,跳过\n";
        continue;
    }

    $dbFiles = getDbFiles($path);
    if (empty($dbFiles)) {
        echo "  没有找到 .db 文件\n";
        continue;
    }
    echo "  发现 " . count($dbFiles) . " 个 .db 文件\n";

    // 第一步:删除所有已过期的记录
    $deletedExpired = 0;
    $fileCount = 0;
    foreach ($dbFiles as $dbFile) {
        $fileCount++;
        $deleted = cleanExpiredRecords($dbFile);
        if ($deleted > 0) {
            echo "  过期清理 $dbFile : 删除 $deleted 条\n";
        }
        $deletedExpired += $deleted;
        if ($fileCount % 10 == 0) {
            echo "  已处理 $fileCount 个文件...\n";
        }
    }
    echo "第一步完成,共删除过期缓存 $deletedExpired 条\n";
    $totalExpired += $deletedExpired;

    // 第二步:如果总数仍超限,按访问量删除最冷数据
    $totalRecords = countAllRecords($dbFiles);
    $max = $task['max_records'];
    if ($totalRecords > $max) {
        $need = $totalRecords - $max;
        echo "记录数 $totalRecords 超过限制($max),需删除 $need 条最冷数据\n";
        $deletedCold = deleteColdestRecords($dbFiles, $need);
        echo "第二步删除 $deletedCold 条\n";
        $totalOverLimit += $deletedCold;
    } else {
        echo "记录数 $totalRecords 未超限 $max\n";
    }

    // 清理空目录(可选)
    removeEmptyDirs($path, false);
}

echo "\n[" . date('Y-m-d H:i:s') . "] 清理完成\n";
echo "汇总:过期删除 $totalExpired 条,超限冷淘汰 $totalOverLimit 条\n";

// ---------- 辅助函数 ----------

/**
 * 递归获取指定目录下所有 .db 文件
 */
function getDbFiles($root) {
    $files = [];
    if (!is_dir($root)) return $files;
    $iter = new RecursiveIteratorIterator(
        new RecursiveDirectoryIterator($root, RecursiveDirectoryIterator::SKIP_DOTS)
    );
    foreach ($iter as $file) {
        if ($file->isFile() && $file->getExtension() === 'db') {
            $files[] = $file->getPathname();
        }
    }
    return $files;
}

/**
 * 删除某个 .db 文件中所有已过期的记录(基于 expire_time)
 * 分批处理,避免长事务锁表
 */
function cleanExpiredRecords($dbFile) {
    try {
        $pdo = new PDO("sqlite:" . $dbFile);
        $pdo->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION);
        $now = time();

        $limit = 1000;
        $lastMd5 = '';
        $totalDeleted = 0;

        do {
            $stmt = $pdo->prepare("SELECT md5, expire_time FROM cache WHERE md5 > ? ORDER BY md5 LIMIT ?");
            $stmt->execute([$lastMd5, $limit]);
            $rows = $stmt->fetchAll();
            if (empty($rows)) break;

            $toDelete = [];
            foreach ($rows as $row) {
                if ($row['expire_time'] > 0 && $row['expire_time'] < $now) {
                    $toDelete[] = $row['md5'];
                }
            }
            if (!empty($toDelete)) {
                $in = implode(',', array_fill(0, count($toDelete), '?'));
                $del = $pdo->prepare("DELETE FROM cache WHERE md5 IN ($in)");
                $del->execute($toDelete);
                $totalDeleted += $del->rowCount();
            }
            $lastMd5 = end($rows)['md5'];
        } while (true);

        unset($pdo);
        return $totalDeleted;
    } catch (Exception $e) {
        error_log("清理失败 $dbFile: " . $e->getMessage());
        return 0;
    }
}

/**
 * 统计所有 .db 文件中的总记录数
 */
function countAllRecords($dbFiles) {
    $total = 0;
    foreach ($dbFiles as $dbFile) {
        try {
            $pdo = new PDO("sqlite:" . $dbFile);
            $total += (int) $pdo->query("SELECT COUNT(*) FROM cache")->fetchColumn();
            unset($pdo);
        } catch (Exception $e) {
            // 忽略损坏的数据库
        }
    }
    return $total;
}

/**
 * 在多个 .db 文件中按比例删除访问量最低的记录
 */
function deleteColdestRecords($dbFiles, $need) {
    // 先统计每个文件的记录数
    $dbStats = [];
    $totalRecords = 0;
    foreach ($dbFiles as $dbFile) {
        try {
            $pdo = new PDO("sqlite:" . $dbFile);
            $count = (int) $pdo->query("SELECT COUNT(*) FROM cache")->fetchColumn();
            $dbStats[$dbFile] = $count;
            $totalRecords += $count;
            unset($pdo);
        } catch (Exception $e) {
            // 忽略
        }
    }
    if ($totalRecords == 0) return 0;

    $deleted = 0;
    // 按比例分配删除名额
    foreach ($dbStats as $dbFile => $count) {
        if ($count == 0) continue;
        $deleteCount = (int) ceil($need * ($count / $totalRecords));
        if ($deleteCount <= 0) continue;
        try {
            $pdo = new PDO("sqlite:" . $dbFile);
            $pdo->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION);
            $del = $pdo->prepare("DELETE FROM cache ORDER BY access_count ASC, first_time ASC LIMIT ?");
            $del->execute([$deleteCount]);
            $deleted += $del->rowCount();
            unset($pdo);
        } catch (Exception $e) {
            // 忽略
        }
    }

    // 如果还没删够,补删(每个文件再删一条,直到满足需求)
    if ($deleted < $need) {
        $remaining = $need - $deleted;
        foreach ($dbFiles as $dbFile) {
            if ($remaining <= 0) break;
            try {
                $pdo = new PDO("sqlite:" . $dbFile);
                $del = $pdo->prepare("DELETE FROM cache ORDER BY access_count ASC, first_time ASC LIMIT 1");
                $del->execute();
                if ($del->rowCount() > 0) {
                    $deleted++;
                    $remaining--;
                }
                unset($pdo);
            } catch (Exception $e) {
                // 忽略
            }
        }
    }
    return $deleted;
}

/**
 * 删除空目录(保留根目录本身)
 */
function removeEmptyDirs($dir, $isRoot = true) {
    if (!is_dir($dir)) return;
    foreach (scandir($dir) as $item) {
        if ($item === '.' || $item === '..') continue;
        $path = $dir . DIRECTORY_SEPARATOR . $item;
        if (is_dir($path)) {
            removeEmptyDirs($path, false);
        }
    }
    if (!$isRoot && count(scandir($dir)) == 2) {
        @rmdir($dir);
    }
}

View cached scripts

The cached script can be viewed in the browser.

<?php
// ============================================================
//  缓存管理器 - 安全增强版
//  放置路径:CACHE_ROOT 目录下(例如 /Mcache/index.php)
// ============================================================

// ---------- 安全配置 ----------
// 建议在外部配置文件定义,这里作为示例
// 若未设置环境变量,则使用默认(并提示错误)
if (!defined('CACHE_ROOT')) {
    define('CACHE_ROOT', __DIR__);
}

// 密码哈希(请使用 password_hash('你的密码', PASSWORD_DEFAULT) 生成后替换)
// 例如:$hash = '$2y$10$ABCDEFGHIJKLMNOPQRSTUVWXYZ...';
$ADMIN_HASH = getenv('CACHE_ADMIN_HASH') ?: '';  // 从环境变量读取
if (empty($ADMIN_HASH)) {
    // 如果没有设置,则默认一个测试密码(但会警告)
    $ADMIN_HASH = '$2y$10$N6lQZ.UoMvlYIWrJ3AbjAu6.8pDZ1dY2rVfXcNlD43hW1A0wGk0C'; // 对应 "admin123"
    // 生产环境务必在环境变量中设置!
}
define('ADMIN_HASH', $ADMIN_HASH);

define('PAGE_SIZE', 20);
define('MAX_LOGIN_ATTEMPTS', 5);
define('LOCKOUT_TIME', 1800); // 30分钟

// ---------- 会话安全 ----------
ini_set('session.cookie_httponly', 1);
if (isset($_SERVER['HTTPS']) && $_SERVER['HTTPS'] === 'on') {
    ini_set('session.cookie_secure', 1);
}
ini_set('session.cookie_samesite', 'Strict');
session_start();

// ---------- 登录逻辑 ----------
function isAuthenticated() {
    return isset($_SESSION['cache_manager_auth']) && $_SESSION['cache_manager_auth'] === true;
}

// 登录失败计数
if (!isset($_SESSION['login_attempts'])) {
    $_SESSION['login_attempts'] = 0;
}
if (!isset($_SESSION['lockout_until'])) {
    $_SESSION['lockout_until'] = 0;
}

// 登出
if (isset($_GET['logout'])) {
    unset($_SESSION['cache_manager_auth']);
    unset($_SESSION['login_attempts']);
    unset($_SESSION['lockout_until']);
    session_destroy();
    header('Location: ?');
    exit;
}

// 处理登录
if (isset($_POST['password'])) {
    $now = time();
    // 检查是否锁定
    if ($_SESSION['lockout_until'] > $now) {
        $remain = $_SESSION['lockout_until'] - $now;
        $loginError = "登录尝试过多,请等待 " . ceil($remain/60) . " 分钟后再试。";
    } else {
        // 重置锁定
        if ($_SESSION['lockout_until'] > 0) {
            $_SESSION['login_attempts'] = 0;
            $_SESSION['lockout_until'] = 0;
        }
        $inputPass = $_POST['password'];
        // 验证密码(使用 password_verify)
        if (password_verify($inputPass, ADMIN_HASH)) {
            $_SESSION['cache_manager_auth'] = true;
            $_SESSION['login_attempts'] = 0;
            header('Location: ?');
            exit;
        } else {
            $_SESSION['login_attempts']++;
            if ($_SESSION['login_attempts'] >= MAX_LOGIN_ATTEMPTS) {
                $_SESSION['lockout_until'] = time() + LOCKOUT_TIME;
                $loginError = "登录尝试过多,已锁定 30 分钟。";
            } else {
                $loginError = "密码错误,还剩 " . (MAX_LOGIN_ATTEMPTS - $_SESSION['login_attempts']) . " 次尝试机会。";
            }
        }
    }
}

if (!isAuthenticated()) {
    ?>
    <!DOCTYPE html>
    <html><head><meta charset="utf-8"><title>缓存管理器登录</title></head>
    <body style="font-family:sans-serif;max-width:400px;margin:100px auto;text-align:center;">
        <h2>登录</h2>
        <?php if (isset($loginError)) echo "<p style='color:red;'>$loginError</p>"; ?>
        <form method="post">
            <input type="password" name="password" placeholder="密码" style="width:100%;padding:8px;margin:10px 0;">
            <button type="submit">登录</button>
        </form>
    </body></html>
    <?php
    exit;
}

// ---------- CSRF 保护 ----------
function generateCsrfToken() {
    if (empty($_SESSION['csrf_token'])) {
        $_SESSION['csrf_token'] = bin2hex(random_bytes(32));
    }
    return $_SESSION['csrf_token'];
}

function verifyCsrfToken($token) {
    return isset($_SESSION['csrf_token']) && hash_equals($_SESSION['csrf_token'], $token);
}

// 生成 token(用于表单)
$csrfToken = generateCsrfToken();

// ---------- 工具函数 ----------
function h($str) { return htmlspecialchars($str, ENT_QUOTES, 'UTF-8'); }
function decompress($data) { $raw = @gzuncompress($data); return ($raw !== false) ? $raw : $data; }
function compress($data) { return (strlen($data) < 200) ? $data : gzcompress($data, 6); }
function getParam($name, $default = '') { return isset($_GET[$name]) ? $_GET[$name] : $default; }

// ---------- 路径安全函数 ----------
function safePath($path) {
    $path = str_replace(['..', '\\'], '', $path);
    $fullPath = __DIR__ . '/' . ltrim($path, '/');
    $realPath = realpath($fullPath);
    if ($realPath === false) return false;
    // 必须位于 CACHE_ROOT 内
    $rootReal = realpath(__DIR__);
    if (strpos($realPath, $rootReal) !== 0) {
        return false;
    }
    return $realPath;
}

// ---------- 请求处理 ----------
// 仅允许 POST 修改操作
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
    // 验证 CSRF token
    if (!isset($_POST['csrf_token']) || !verifyCsrfToken($_POST['csrf_token'])) {
        die('CSRF 验证失败,请刷新页面重试。');
    }
    // 处理编辑
    if (isset($_POST['action']) && $_POST['action'] === 'edit') {
        $dbFile = basename($_POST['file'] ?? '');
        $editMd5 = $_POST['md5'] ?? '';
        $editContent = $_POST['content'] ?? '';
        $currentDir = $_POST['dir'] ?? '';
        $page = (int)($_POST['page'] ?? 1);
        if ($dbFile && $editMd5) {
            $fullDir = safePath($currentDir);
            if (!$fullDir) {
                die('非法目录');
            }
            $fullDbPath = $fullDir . '/' . $dbFile;
            if (!file_exists($fullDbPath) || pathinfo($fullDbPath, PATHINFO_EXTENSION) !== 'db') {
                die('非法数据库文件');
            }
            try {
                $pdo = new PDO("sqlite:" . $fullDbPath);
                $pdo->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION);
                $stmt = $pdo->prepare("SELECT content FROM cache WHERE md5 = ?");
                $stmt->execute([$editMd5]);
                if ($stmt->fetch()) {
                    $compressed = compress($editContent);
                    $upd = $pdo->prepare("UPDATE cache SET content = ? WHERE md5 = ?");
                    $upd->execute([$compressed, $editMd5]);
                    $msg = '记录更新成功';
                } else {
                    $msg = '记录不存在,无法更新';
                }
            } catch (Exception $e) {
                $msg = '更新失败:' . $e->getMessage();
            }
            header('Location: ?dir=' . urlencode($currentDir) . '&file=' . urlencode($dbFile) . '&page=' . $page . '&msg=' . urlencode($msg));
            exit;
        }
    }
    // 处理删除
    if (isset($_POST['action']) && $_POST['action'] === 'delete') {
        $dbFile = basename($_POST['file'] ?? '');
        $editMd5 = $_POST['md5'] ?? '';
        $currentDir = $_POST['dir'] ?? '';
        $page = (int)($_POST['page'] ?? 1);
        if ($dbFile && $editMd5) {
            $fullDir = safePath($currentDir);
            if (!$fullDir) {
                die('非法目录');
            }
            $fullDbPath = $fullDir . '/' . $dbFile;
            if (!file_exists($fullDbPath) || pathinfo($fullDbPath, PATHINFO_EXTENSION) !== 'db') {
                die('非法数据库文件');
            }
            try {
                $pdo = new PDO("sqlite:" . $fullDbPath);
                $pdo->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION);
                $del = $pdo->prepare("DELETE FROM cache WHERE md5 = ?");
                $del->execute([$editMd5]);
                $msg = '记录已删除';
            } catch (Exception $e) {
                $msg = '删除失败:' . $e->getMessage();
            }
            header('Location: ?dir=' . urlencode($currentDir) . '&file=' . urlencode($dbFile) . '&page=' . $page . '&msg=' . urlencode($msg));
            exit;
        }
    }
}

// ---------- GET 参数处理 ----------
$currentDir = str_replace(['..', '\\'], '', getParam('dir', ''));
$fullDir = __DIR__ . '/' . $currentDir;
if (!is_dir($fullDir)) { $currentDir = ''; $fullDir = __DIR__; }

$dbFile = basename(getParam('file', ''));
$fullDbPath = $fullDir . '/' . $dbFile;
if ($dbFile && (!file_exists($fullDbPath) || !is_file($fullDbPath) || pathinfo($fullDbPath, PATHINFO_EXTENSION) !== 'db')) {
    $dbFile = '';
}

$page = max(1, (int)getParam('page', 1));
$offset = ($page - 1) * PAGE_SIZE;
$msg = getParam('msg', '');
if ($msg) $msg = urldecode($msg);

// 编辑表单(GET 方式仅用于展示表单,不修改数据)
$editMd5 = getParam('md5', '');
$action = getParam('action');
$showEditForm = ($action === 'edit_form' && $editMd5 && $dbFile);
?>
<!DOCTYPE html>
<html lang="zh-CN">
<head>
    <meta charset="utf-8">
    <title>缓存管理器</title>
    <style>
        body { font-family: "Segoe UI", Arial, sans-serif; margin: 20px; background: #f5f7fa; }
        .container { max-width: 1200px; margin: 0 auto; background: #fff; padding: 20px; border-radius: 8px; box-shadow: 0 2px 8px rgba(0,0,0,0.1); }
        h1 { font-size: 24px; border-bottom: 2px solid #eee; padding-bottom: 10px; }
        .breadcrumb { margin: 15px 0; font-size: 14px; }
        .breadcrumb a { color: #3498db; text-decoration: none; }
        .msg { padding: 10px; background: #d4edda; color: #155724; border: 1px solid #c3e6cb; border-radius: 4px; margin: 10px 0; }
        .msg.error { background: #f8d7da; color: #721c24; border-color: #f5c6cb; }
        .nav { margin: 10px 0; }
        .nav a { display: inline-block; padding: 6px 12px; background: #3498db; color: #fff; border-radius: 4px; text-decoration: none; margin-right: 5px; }
        .nav a:hover { background: #2980b9; }
        table { width: 100%; border-collapse: collapse; margin-top: 15px; font-size: 14px; }
        th, td { padding: 8px 12px; text-align: left; border-bottom: 1px solid #ddd; }
        th { background: #f8f9fa; font-weight: 600; }
        tr:hover { background: #f1f5f9; }
        .file-icon { color: #3498db; font-weight: bold; }
        .dir-icon { color: #f39c12; font-weight: bold; }
        .actions a { margin-right: 8px; color: #3498db; text-decoration: none; }
        .actions a:hover { text-decoration: underline; }
        .actions a.delete { color: #e74c3c; }
        .pagination { margin-top: 20px; text-align: center; }
        .pagination a, .pagination span { display: inline-block; padding: 5px 12px; margin: 0 3px; border: 1px solid #ddd; border-radius: 4px; text-decoration: none; color: #333; }
        .pagination .active { background: #3498db; color: #fff; border-color: #3498db; }
        .pagination a:hover { background: #eee; }
        .content-preview { max-height: 150px; overflow: auto; background: #f8f9fa; padding: 6px; border-radius: 4px; font-family: monospace; font-size: 12px; white-space: pre-wrap; word-break: break-all; }
        .edit-form { margin-top: 20px; border-top: 2px solid #eee; padding-top: 20px; }
        .edit-form textarea { width: 100%; height: 300px; font-family: monospace; font-size: 14px; padding: 8px; border: 1px solid #ccc; border-radius: 4px; }
        .edit-form .btn { padding: 8px 20px; background: #2ecc71; color: #fff; border: none; border-radius: 4px; cursor: pointer; }
        .edit-form .btn:hover { background: #27ae60; }
        .edit-form .btn.cancel { background: #95a5a6; }
        .edit-form .btn.cancel:hover { background: #7f8c8d; }
        .badge { display: inline-block; padding: 2px 8px; border-radius: 12px; font-size: 12px; font-weight: bold; }
        .badge-expired { background: #e74c3c; color: #fff; }
        .badge-active { background: #2ecc71; color: #fff; }
        .badge-never { background: #95a5a6; color: #fff; }
        .footer { margin-top: 30px; text-align: center; color: #999; font-size: 12px; }
        .logout { float: right; margin-top: 10px; }
        .csrf-warning { color: #e74c3c; font-size: 13px; margin-top: 5px; }
    </style>
</head>
<body>
<div class="container">
    <h1> 缓存管理器
        <span class="logout"><a href="?logout=1" style="color:#e74c3c; text-decoration:none;">退出</a></span>
    </h1>

    <?php if ($msg): ?>
        <div class="msg <?= strpos($msg, '失败') !== false || strpos($msg, '错误') !== false ? 'error' : '' ?>"><?= h($msg) ?></div>
    <?php endif; ?>

    <div class="breadcrumb">
        <a href="?">根目录</a>
        <?php
        if ($currentDir) {
            $parts = explode('/', $currentDir);
            $path = '';
            foreach ($parts as $part) {
                $path .= $part . '/';
                echo ' / <a href="?dir=' . urlencode(rtrim($path, '/')) . '">' . h($part) . '</a>';
            }
        }
        if ($dbFile) echo ' / <strong>' . h($dbFile) . '</strong>';
        ?>
    </div>

    <?php if ($dbFile): ?>
        <?php
        try {
            $pdo = new PDO("sqlite:" . $fullDbPath);
            $pdo->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION);
            $tableExists = $pdo->query("SELECT name FROM sqlite_master WHERE type='table' AND name='cache'")->fetch();
            if (!$tableExists) {
                echo '<p style="color:red;">此数据库中没有 cache 表。</p>';
            } else {
                $totalStmt = $pdo->query("SELECT COUNT(*) FROM cache");
                $total = $totalStmt->fetchColumn();
                $stmt = $pdo->prepare("SELECT md5, content, access_count, first_time, expire_time FROM cache ORDER BY first_time DESC LIMIT ? OFFSET ?");
                $stmt->bindValue(1, PAGE_SIZE, PDO::PARAM_INT);
                $stmt->bindValue(2, $offset, PDO::PARAM_INT);
                $stmt->execute();
                $rows = $stmt->fetchAll(PDO::FETCH_ASSOC);

                echo '<div class="nav">';
                echo '<a href="?dir=' . urlencode($currentDir) . '">⬅ 返回目录</a>';
                echo ' <span style="margin-left:20px;">总记录数:' . $total . '</span>';
                echo '</div>';

                if ($total == 0) {
                    echo '<p>此缓存数据库为空。</p>';
                } else {
                    echo '<table>';
                    echo '<tr><th>MD5</th><th>访问数</th><th>首次时间</th><th>过期时间</th><th>内容预览</th><th>操作</th></tr>';
                    foreach ($rows as $row) {
                        $expire = $row['expire_time'];
                        $now = time();
                        $status = $expire == 0 ? '<span class="badge badge-never">永不过期</span>' :
                                  ($expire < $now ? '<span class="badge badge-expired">已过期</span>' : '<span class="badge badge-active">有效</span>');

                        $raw = decompress($row['content']);
                        $preview = mb_strlen($raw) > 200 ? mb_substr($raw, 0, 200) . '...' : $raw;
                        $preview = h($preview);
                        ?>
                        <tr>
                            <td style="font-family:monospace; font-size:12px;"><?= h($row['md5']) ?></td>
                            <td><?= h($row['access_count']) ?></td>
                            <td><?= date('Y-m-d H:i:s', $row['first_time']) ?></td>
                            <td><?= $row['expire_time'] ? date('Y-m-d H:i:s', $row['expire_time']) : '0(永久)' ?> <?= $status ?></td>
                            <td><div class="content-preview"><?= $preview ?></div></td>
                            <td class="actions">
                                <a href="?dir=<?= urlencode($currentDir) ?>&file=<?= urlencode($dbFile) ?>&page=<?= $page ?>&action=edit_form&md5=<?= h($row['md5']) ?>#edit-form">编辑</a>
                                <a href="#" class="delete" onclick="deleteRecord('<?= h($row['md5']) ?>')">删除</a>
                            </td>
                        </tr>
                        <?php
                    }
                    echo '</table>';

                    // 分页
                    if ($total > PAGE_SIZE) {
                        $totalPages = ceil($total / PAGE_SIZE);
                        echo '<div class="pagination">';
                        if ($page > 1) echo '<a href="?dir=' . urlencode($currentDir) . '&file=' . urlencode($dbFile) . '&page=' . ($page-1) . '">上一页</a>';
                        for ($i = 1; $i <= $totalPages; $i++) {
                            echo $i == $page ? '<span class="active">' . $i . '</span>' :
                                 '<a href="?dir=' . urlencode($currentDir) . '&file=' . urlencode($dbFile) . '&page=' . $i . '">' . $i . '</a>';
                        }
                        if ($page < $totalPages) echo '<a href="?dir=' . urlencode($currentDir) . '&file=' . urlencode($dbFile) . '&page=' . ($page+1) . '">下一页</a>';
                        echo '</div>';
                    }

                    // ---------- 编辑表单(仅显示,实际提交用 POST) ----------
                    if ($showEditForm) {
                        $stmt = $pdo->prepare("SELECT content FROM cache WHERE md5 = ?");
                        $stmt->execute([$editMd5]);
                        $row = $stmt->fetch(PDO::FETCH_ASSOC);
                        if ($row) {
                            $decrypted = decompress($row['content']);
                            ?>
                            <div class="edit-form" id="edit-form">
                                <h3>编辑记录 (MD5: <?= h($editMd5) ?>)</h3>
                                <form method="post" action="">
                                    <input type="hidden" name="csrf_token" value="<?= $csrfToken ?>">
                                    <input type="hidden" name="action" value="edit">
                                    <input type="hidden" name="dir" value="<?= h($currentDir) ?>">
                                    <input type="hidden" name="file" value="<?= h($dbFile) ?>">
                                    <input type="hidden" name="page" value="<?= $page ?>">
                                    <input type="hidden" name="md5" value="<?= h($editMd5) ?>">
                                    <textarea name="content" rows="15"><?= h($decrypted) ?></textarea>
                                    <br>
                                    <button type="submit" class="btn">保存</button>
                                    <a href="?dir=<?= urlencode($currentDir) ?>&file=<?= urlencode($dbFile) ?>&page=<?= $page ?>" class="btn cancel">取消</a>
                                </form>
                            </div>
                            <?php
                        } else {
                            echo '<p style="color:red;">错误:记录不存在</p>';
                        }
                    }
                }
            }
        } catch (PDOException $e) {
            echo '<p style="color:red;">数据库错误:' . h($e->getMessage()) . '</p>';
        }
        ?>
    <?php else: ?>
        <!-- 目录浏览 -->
        <?php
        $dirs = glob($fullDir . '/*', GLOB_ONLYDIR);
        $files = glob($fullDir . '/*.db');
        ?>
        <div class="nav">
            <?php if ($currentDir): ?>
                <a href="?dir=<?= urlencode(dirname($currentDir)) ?>">⬅ 上一级</a>
            <?php endif; ?>
            <a href="?"> 根目录</a>
        </div>
        <?php if (empty($dirs) && empty($files)): ?>
            <p>此目录为空</p>
        <?php else: ?>
            <table>
                <tr><th>名称</th><th>类型</th><th>操作</th></tr>
                <?php foreach ($dirs as $dir): ?>
                    <?php $name = basename($dir); ?>
                    <tr>
                        <td><span class="dir-icon"></span> <a href="?dir=<?= urlencode($currentDir ? $currentDir . '/' . $name : $name) ?>"><?= h($name) ?></a></td>
                        <td>目录</td>
                        <td><a href="?dir=<?= urlencode($currentDir ? $currentDir . '/' . $name : $name) ?>">进入</a></td>
                    </tr>
                <?php endforeach; ?>
                <?php foreach ($files as $file): ?>
                    <?php $name = basename($file); ?>
                    <tr>
                        <td><span class="file-icon">️</span> <a href="?dir=<?= urlencode($currentDir) ?>&file=<?= urlencode($name) ?>"><?= h($name) ?></a></td>
                        <td>SQLite 数据库</td>
                        <td><a href="?dir=<?= urlencode($currentDir) ?>&file=<?= urlencode($name) ?>">查看</a></td>
                    </tr>
                <?php endforeach; ?>
            </table>
        <?php endif; ?>
    <?php endif; ?>

    <div class="footer">
        缓存管理器 &bull; 编辑后保存会自动压缩
    </div>
</div>

<script>
// 删除操作使用 POST + CSRF 保护
function deleteRecord(md5) {
    if (!confirm('确定删除此记录吗?')) return;
    var form = document.createElement('form');
    form.method = 'POST';
    form.action = '';
    var fields = {
        'csrf_token': '<?= $csrfToken ?>',
        'action': 'delete',
        'dir': '<?= h($currentDir) ?>',
        'file': '<?= h($dbFile) ?>',
        'page': '<?= $page ?>',
        'md5': md5
    };
    for (var key in fields) {
        var input = document.createElement('input');
        input.type = 'hidden';
        input.name = key;
        input.value = fields[key];
        form.appendChild(input);
    }
    document.body.appendChild(form);
    form.submit();
}
</script>

</body>
</html>

20260817210451658-image

Tutorial

/www/wwwroot/你的站/
├── Mcache/              ← 缓存根目录(自动创建)
│   ├── Mpage/           ← 页面缓存(分片2)
│   │   ├── a/
│   │   │   └── a3f.db
│   │   └── ...
│   ├── Mtrans/          ← 翻译缓存(分片1,单库)
│   │   └── single.db
│   └── Mbig/            ← 大缓存(分片3)
├── lib/
│   └── ApiCache.php     ← 缓存系统文件
└── index.php

one sentence cheat sheet

demandcode
Basic writing$c->set('key', $data, 3600)
Basic reading$c->get('key')
Delete$c->delete('key')
Page cachecache_page('key', 'Mpage', null, 600)
Translation cacheget_cache_instance('Mtrans', 1)
Large cacheget_cache_instance('Mbig', 3)
Determine hit$v = $c->get('k'); if ($v !== false) {...}

This set of plug-ins is essentially a "local Redis without network overhead", suitable for scenarios with limited resources but wanting to greatly improve response speed.

微信赞赏

WeChat

支付宝赞赏

Alipay

✍️ Author: Hong Mu

webmaster · Thanks for reading, stay tuned for more exciting content

Author homepage View home page →

Related articles

Browser caching increases the speed of secondary visits to your website

Browser caching increases the speed of secondary visits to your website Summary of pitfalls

Use browser caching of official words: If users will visit your website multiple times, browser caching of static resources can save users time. Cache headers should be applied to all cacheable static resources, not just to a small subset of static resources (for example, images). Cacheable resources include JS and CSS files, image files, and other binary object files (media files, PDF files, etc.). Normally, HTML doesn’t…
👁 202

Recommended reading

(Adaptive mobile phone) Electronic eye electronic monitoring equipment pbootcms website template Monitoring and security electronic probe website source code download 0368

(Adaptive mobile phone) Electronic eye electronic monitoring equipment pbootcms website template Monitoring and security electronic probe website source code download 0368 Practical Collection pbootcms Template

An adaptive mobile phone electronic eye monitoring and security electronic equipment PbootCMS website template. The design style is technological and safe, suitable for displaying surveillance probes, security systems and intelligent hardware. It helps security technology companies display their products online and attract home and business customers. Template display Installation instructions Website backend:/admin.php Account: admin Password: admin Unzip password: www.4s5.cn Related...
👁 63
(Adaptive mobile phone) Industrial wastewater treatment equipment pbootcms website template Environmental protection technology website source code download 0848

(Adaptive mobile phone) Industrial wastewater treatment equipment pbootcms website template Environmental protection technology website source code download 0848 Practical Collection pbootcms Template

An industrial wastewater treatment equipment and environmental protection technology PbootCMS website template that supports PC and WAP. The design style is professional and technological, suitable for displaying wastewater treatment technology, equipment products and engineering cases. Helps environmental engineering companies to display solutions online and attract industrial corporate customers. Template display Installation instructions Website backend:/admin.php Account: admin Password: admin Unzip password: www.4…
👁 51
Specialty gourmet catering franchise website template 0167

Specialty gourmet catering franchise website template 0167 Practical Collection Yiyou template

This set of eyoucms templates is suitable for specialty food and catering franchise industries. The design style is fashionable and appetizing, and can display specialty foods, catering brands, franchise policies and store images. It helps catering chain brands attract franchisees and consumers online. Template display Installation instructions Website backend: /login.php Account: admin Password: admin Related articles Summary of common problems in the installation of Yiyou CMS Yiyou CMS (Ey...
👁 39
Locomotive collector keyword deduplication processing (website keyword optimization plug-in)

Locomotive collector keyword deduplication processing (website keyword optimization plug-in) Practical Collection High-speed rail collection

When collecting web pages, the keywords of some websites may be repeated. Or some word segmentation plug-ins may cause multiple word segmentation results to appear repeatedly. At this time, you can use the keyword deduplication processing plug-in! This plug-in can greatly simplify the collection of keywords, which is of great significance to website optimization. PHP plugin! As shown in the picture, above are the test results!
👁 263
Refrigeration equipment machinery enterprise website template 0659

Refrigeration equipment machinery enterprise website template 0659 Practical Collection Yiyou template

This set of eyoucms templates is suitable for refrigeration equipment and machinery companies. The design style is professional and industrial, and can display refrigeration equipment products, mechanical systems, technical parameters and industrial applications. It helps refrigeration companies display their products online and attract commercial and industrial customers. Template display Installation instructions Website backend: /login.php Account: admin Password: admin Related articles Yiyou CMS installation FAQ summary Yiyou CMS...
👁 48
(PC+WAP) doors and windows customized pbootcms website template aluminum alloy doors and windows website source code download 0126

(PC+WAP) doors and windows customized pbootcms website template aluminum alloy doors and windows website source code download 0126 Practical Collection pbootcms Template

A PbootCMS website template about door and window customization and aluminum alloy doors and windows industry, supporting PC and WAP access. The design style is modern and simple, which can well display the door and window product series and installation cases. It is a good choice for door and window brands or aluminum alloy processing companies to build official brand websites and improve online customer acquisition capabilities. Template display Installation instructions Website backend:/admin.php Account: admin Password: admin Unzip...
👁 41