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 class:
ApiCache(located atApiCache.php)cache mode:
Ordinary reading and writing(
get/set) - for internal or manual operationFull page caching(
page) - 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:
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">
缓存管理器 • 编辑后保存会自动压缩
</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>
Tutorial
/www/wwwroot/你的站/
├── Mcache/ ← 缓存根目录(自动创建)
│ ├── Mpage/ ← 页面缓存(分片2)
│ │ ├── a/
│ │ │ └── a3f.db
│ │ └── ...
│ ├── Mtrans/ ← 翻译缓存(分片1,单库)
│ │ └── single.db
│ └── Mbig/ ← 大缓存(分片3)
├── lib/
│ └── ApiCache.php ← 缓存系统文件
└── index.phpone sentence cheat sheet
| demand | code |
|---|---|
| Basic writing | $c->set('key', $data, 3600) |
| Basic reading | $c->get('key') |
| Delete | $c->delete('key') |
| Page cache | cache_page('key', 'Mpage', null, 600) |
| Translation cache | get_cache_instance('Mtrans', 1) |
| Large cache | get_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.