阅读视图
从“苦读教程”到“听 AI 播客”
路虽远,行则将至:写在蒋羿抵达山海关之际
语音输入法的爆发前夜
用了一周豆包,我还是换回了微信输入法
死磕两晚:一篇硬核教程背后的“体力活”
聊聊 Claude Code:它不是工具,它是实习生
国内也能用!Claude Code 完整上手指南
体检、投篮、搞 PC 版同步 - 周六琐记
开源免费:我的公众号封面图生成器
到账 24.87 元
devlog:让 Claude Code 帮你记录每一天的工作
来自自恋狂的瞎折腾
作为一个超级丰满的女子,依然阻挡不住自己的自恋,要把照片放到每一个地方。啊,你问为什么?还不是因为自恋嘛!
Moe-Counter Baby版
这个东西其实用了很久了,直到前段时间重建之后才发现这个东西竟然也更新了。并且增加了很多样式,为了拥抱这些样式直接重新构建了一版。原来的版本依赖有点问题,于是顺手给改了以下,提交了一份新的代码:https://gitee.com/obaby/moe-counter
主要问题在于sqllite依赖的问题,修改package.json 改成下面的版本就行了:
"dependencies": {
"better-sqlite3": "^12.6.2",
"compression": "^1.8.0",
"dotenv": "^16.5.0",
"express": "^4.21.2",
"image-size": "^0.8.3",
"mime-types": "^2.1.35",
"mongoose": "^8.15.1",
"pug": "^3.0.3",
"zod": "^3.25.61"
},
重新编译:
# 1. 配置镜像源 pnpm config set registry https://registry.npmmirror.com export npm_config_disturl=https://npmmirror.com/dist export npm_config_node_gyp_mirror=https://npmmirror.com/dist # 2. 删除旧的依赖并重新安装(使用新版本) rm -rf node_modules pnpm-lock.yaml # 3. 重新安装依赖(会自动安装 better-sqlite3@12.6.2) pnpm install # 4. 如果安装后还是找不到 bindings,手动编译 cd node_modules/.pnpm/better-sqlite3@*/node_modules/better-sqlite3 npm run build-release cd ~/Moe-Counter
顺手把里面应用的jsdeliver的链接也给换了,毕竟这个东西在国内访问问题还是蛮多的。
自恋版计数器:
当然啦,为了做这些图也着实废了一番功夫:

最开始是上面的,生成一堆,但是分辨率不高。还有个问题那就是ai不识数,总是会少数字。
后来尝试单个生成,但是ai的数字分辨能力依然跟智障一样。生成了很多乱七八糟的数字:
至于怎么安装主题,那就更简单了,把图片按照0-9整成gif,弄个文件件放到theme下面,重启服务就ok啦。
UserAgent 系统版本号
浏览器的ua在某个时间点之后,操作系统的版本号就固定在了某个特定的值:
windows是Windows NT 10.0
mac是Intel Mac OS X 10_15_7
其实,这个现在已经变成了一个通用的作法,参考:https://bugs.webkit.org/show_bug.cgi?id=216593
这个东西以后大概率也不会更新了,为了让ua显示准确数值,之前修复了win11的识别问题,昨天有点时间,又处理了下osx的识别。但是在mac下目前safari无法获取系统版本信息,暂时没什么解决方案。
主要方法也简单:
1.在提交表单的时候判断操作系统,针对win 和mac 传递一个隐藏字段
////////////////////////////////////////////////////////////////////////////////////////////////
// macOS 主版本号与版本名称映射(15 Sequoia 2024-09-16,26 Tahoe 2025-09-15)
// By:obaby
// 2026-02-04
// https://oba.by
// https://zhongxiaojie.cn
////////////////////////////////////////////////////////////////////////////////////////////////
var macVersionNames = {
10: "macOS 10",
11: "Big Sur",
12: "Monterey",
13: "Ventura",
14: "Sonoma",
15: "Sequoia",
26: "Tahoe"
};
function setOsVersionInput(elementId, value) {
var el = document.getElementById(elementId);
if (el !== null) el.value = value;
}
try {
navigator.userAgentData.getHighEntropyValues(["platformVersion"])
.then(ua => {
var platform = navigator.userAgentData.platform;
if (platform === "Windows") {
var major = parseInt(ua.platformVersion.split('.')[0], 10);
if (major >= 13) {
setOsVersionInput("comment_windows_version", "win11");
console.log("Windows 11 or later");
} else if (major > 0) {
setOsVersionInput("comment_windows_version", "win10");
console.log("Windows 10");
} else {
console.log("Before Windows 10");
}
} else if (platform === "macOS" || platform === "Mac OS") {
var parts = ua.platformVersion.split('.');
var major = parseInt(parts[0], 10);
var minor = parseInt(parts[1] || '0', 10);
var versionLabel = major >= 11 ? (macVersionNames[major] || "macOS " + major) : "macOS 10." + minor;
// setOsVersionInput("comment_mac_version", versionLabel);
setOsVersionInput("comment_windows_version", "Mac OS X " + major + "_" + minor);
console.log("Value: " + "Mac OS X " + major + "_" + minor);
console.log("macOS:", versionLabel, "(" + ua.platformVersion + ")");
// 15 及之后单独标记,便于统计/兼容判断
if (major >= 15) {
setOsVersionInput("comment_mac_15plus", "1");
console.log("macOS 15+ (Sequoia or later):", versionLabel, "(" + ua.platformVersion + ")");
} else {
console.log("macOS:", versionLabel, "(" + ua.platformVersion + ")");
}
} else {
console.log("Not Windows or macOS:", platform);
}
});
} catch (e) {
console.log("OS version detection: Not Supported");
}
2.在wp接受到评论的时候,提前针对ua进行处理,针对传递的特殊的ua进行替换
// 钩子函数,在评论提交前调用
add_filter('pre_comment_user_agent', 'block_specific_user_agent');
function block_specific_user_agent($user_agent) {
// 这里设置你想要阻止的用户代理字符串
// $blocked_ua = 'BadBot/1.0';
// // 如果用户代理匹配,返回一个空字符串来阻止评论
// if (strpos($user_agent, $blocked_ua) !== false) {
// return '';
// }
if (isset($_POST['comment_windows_version']) && $user_agent) {
$raw = trim($_POST['comment_windows_version']);
// 白名单校验,防止 XSS/脏数据:只允许 'win11' 或 "Mac OS X 主_次" 格式
if ($raw === 'win11') {
$user_agent = str_replace('Windows NT 10.0', 'Windows NT 11.0', $user_agent);
} elseif (preg_match('/^Mac OS X\s+(\d+)(?:[_\.](\d+))?$/i', $raw, $m) && strlen($raw) <= 32) {
$major = (int)$m[1];
if ($major > 10) {
$allowed = true;
$user_agent = str_replace('Intel Mac OS X 10_15_7', $raw, $user_agent);
}
}
}
// 否则,返回原始的用户代理字符串
return $user_agent;
}
显示归属地的插件无需任何修改,会自动显示正确的版本。
The post 来自自恋狂的瞎折腾 appeared first on obaby 𝐢𝐧⃝ void.
👗V👗
感觉《2077》在电脑硬盘上已经躺了很长的时间,前几天加载存档的时候发现上个存档竟然还停留在22年,在记忆里似乎并没有那么久没玩过。再次加载完游戏,一切那么陌生又熟悉。似乎V也已经等了我很久了。
这20年发售的游戏,到现在已经6年了。这6年时间里,自己仅仅玩了36个小时,整体进度才不到20%。之前的剧情都已经淡忘了,记不清楚之前经历过什么,连现在的任务都不清楚。只是隐约记得,V出现在这个叫做夜之城的地方,脑子里的芯片里还住着另外一个人-银手。剩下的都记不清楚了,只是直到V要不惜一切代价的活下去,用尽所有的方式。
我的如同金鱼般的的记忆力,无法支撑我写长的游戏记录文章。然而,不管是看电影还是玩游戏,有时候难免会带入到主角的视角,或许从某个程度上来说,rpg游戏的走向也是玩家的游戏走向,甚至不知不觉中就影响了游戏的发展进程。当然有的时候也会刻意选择一些与自己想法完全相反的选项,而这时,多数的目的是为了达成另外一个结局。
6年的时间,我没能让游戏有任何一个结局。年轻的时候,拿到一款游戏总是能快速的达成一个结局,哪怕不是最好的,cod系列的游戏,普通难度的通关时间一般都在80个小时左右,作为一个手残党,能两周左右通关也确实需要付出点努力。当时,玩游戏的时候,有个大学同学总是说,玩游戏当然是选择老兵难度-困难。最高难度,三枪毙命对我来说着实有些扛不住。几年前还能跟朋友一起玩l4d最高难度,通宵的玩,看着那一波波的僵尸袭来,尤其是witcher的笑声,不管听到多少次总是感觉那个快速袭来的僵尸就在身后。如果awp不能远距离解决掉,最后大概率就是别开膛破肚。后来,偶尔还是连线上的服务器,只是再也找不回原来的感觉了。当然,我更喜欢的是里面换的洛丽塔的小裙子,打僵尸的时候都可可爱爱萌萌哒。
现在把她重新拾起来,没有结束,总是有点始乱终弃的感觉。虽然完成了换装游戏,服饰确多数没有达到自己的预期。经过几天的折腾,总算是有能看过眼的了。
这种换装方式,其实依然是属于外挂的范畴了,虽然是叫做mod。内心总是有种感觉,哪怕是游戏里也要打扮的美美哒,如果不美,那就酷酷的。
我在游戏里的选择,决定了V的未来,然而,现实中自己的人生也得自己去选择。多数时候其实跟游戏里区别并不大,在没有攻略的情况下,对于每个选择,并不知道这个选择是对是错。
最近乱七八糟的事情视在太多,甚至连闺蜜圈迭代目前都暂停了。没有时间去维护或者开发更多的功能,公司的屎山代码,每天都在爆出各种问题和bug,处理起来焦头烂额。年关将近,乱七八糟的事情也实在是多。想清闲也是片刻闲暇都没有。
昨天下班的时候,宝子的小姨说,宝子姥姥手疼,都没法正常生活了。下午的时候,宝子的小姨跟她一起去医院检查了以下,抽了积液,注射了药物。晚上带着宝子,从小姨家接姥姥回家。宝子上课外托管回来的时候已经有些晚了,路上提到宝子的晚饭,从餐厅买的水饺。
让姥姥尝试一个,她一口回绝:『我不吃,外面的东西要加各种调料,大油什么的,我不放心不吃!』
『你就试一个』我继续劝。
『不吃,不能顾此失彼,我好不容易降血脂,又吃大油,不吃。本来明天要买里脊的,炸肉,我现在买不了了,等周末吧』
我答复:『没事,不用你管了,我明天去买,全部我自己炸行了,不用你管了』
『本来明天要包水饺的,也不包了吧,现在这个状况,三天不能碰水。等过年能包就行了』
『咱们能不包了吗?直接买现成的?』我问。
『不行,买的不放心,我还是得自己包』她继续坚持。
到门口宝子跟姥姥下车之后,我跟对象去停车。
『现在这个无解了,犟种无药可救』我说。
『现在也没必要可怜她,都是他自己找的』对象答到。
『要不这样吧,咱们买现成的,她自己吃自己包,吃几个包几个。我是无所谓的,我觉得买的速冻的也挺好吃的』
『也只好如此了』
相对于V的宏大叙事,我的生活就在这中鸡毛蒜皮的小事中慢慢蹉跎。你不让她干,她忍不住,自己去干了,又开始埋怨我们不关心她,不带她去看医生。宝子姥姥总是说什么自己小姐身子,丫鬟命。现在没人让她当丫鬟,很多事情也完全可以不用干。只是,可能是出于父母的本能去做了一些事情。
只是现在,我不想在过多干预这种事情了,都是自己的选择。付出自然是令人感动,但是那种自残式的付出,大可不必。自己身体收到残害,还道德绑架自己的子女。
小的时候,总是有各种狂妄的梦想。现在,这些梦想却都成空了。平平无奇,作为一个普通人,在这个泥淖里挣扎,尽量不让沼泽吞没自己。
宝子报了寒假的托管班,第一天就交到了新的朋友。应该在一起也玩的蛮开心的,我经常跟宝子说,咱俩换换吧。我去替你上学,你去上班。真的挺羡慕现在宝子的生活的。有时候虽然慢吞吞的,我也会叨叨。但是,跟自己小的时候完全不一样。
有时候也在想,自己的人生什么时候变得这么无趣了。单调枯燥,一成不变有时候也蛮好的,唯一和游戏里一样的。
那就是充满了利益利用和各种欺骗。只是,自己不用跟V一样真的去出生入死。
而至于游戏的终点,所有人都一样,那就是死亡。
这么一想反而也就释然了,这瞬间的得失又有什么关系呢。未来的路,说长也长,说短也短。
人生,能有几个6年。
The post 👗V👗 appeared first on obaby 𝐢𝐧⃝ void.
👠高跟鞋👠
经常看到acves玩各种游戏的记录,虽然很多游戏自己没玩过,看这种游戏记录的时候,敬业多多少少的能直到故事的梗概和大致的时间线。玩游戏这件事情,其实也需要天赋,这个东西,自己确并没有多少。作为一个手残党,多数游戏纯粹就是靠时长耗过去,一次次的失败,一次次读档,最终也就过去了。
不过有的时候,单纯靠读档也解决不了问题,还是得靠搜攻略,或者再退一步那就是上修改器了,《黑神话悟空》通关就是纯粹靠修改器,魂类游戏也着实不是自己的强项(咱就是说,你有擅长的游戏吗?!)。
多年以前买的《2077》只是偶尔进去转悠转悠,硬生生的给完成了换装游戏,有时间就进去跑各种服装店。然而,游戏里面的服装说实话一言难尽,真的挺丑的。那么多商店,竟然连双好看的鞋子都没有。
打开gog看到除了新的dlc,购买之后发现竟然有个redmod支持的功能。过段现在之后,去找各种mod。从3dm下载各种mod 以及管理工具导入之后,却发现貌似没什么效果,启动之后mod列表是空的。后来有试了各种其他的管理工具,依然是同样的问题,但是工具却能显示出来。尝试漩涡,以及mo2之后,忽然发现莫斯mo2是可以的,安装的mod通过指令添加物品之后确实能够看到了。
不过mo2的管理感觉也有问题,后来不知道哪里出问题之后连游戏都进不去了。又花了一个小时重新修复游戏,这80g的安装包,下载也属实是花了不少的时间。修复之后换回漩涡,冲洗安装编译mod,这次看到了编译窗口,貌似感觉是正常了。然而,换了鞋子之后模型出问题了:
小裙子挺好看的,但是有点穿模,没法从前面看。应该用的身体的模型也得换掉。
mod这个东西,的确提供了很多个性化定制的能力,也引入了一些问题。因为开了摸个身体模型,导致游戏直接卡在了加载页面。为了解决这个问题,来回删除重装了无数次的mod包括手工删除。
有时候对于那种喜欢的鞋子,真的挺没抵抗力的。哪怕看别人穿,感觉也是赏心悦目。
有时候甚至让人上瘾,哪怕买了之后从来都没穿过,就那么扔着,甚至很多都从来没机会穿过。
拍写真的时候,化妆师小姐姐说,鞋子好好看啊,不过我穿不了这么高的。
The post 👠高跟鞋👠 appeared first on obaby 𝐢𝐧⃝ void.
死缠烂打
有时候其实挺不理解,那些销售们能盯着一个人进行轮番轰炸,哪怕已经被告知自己对这些东西不高兴了。
不知道是不是因为经济形势问题,还是连大厂也没单了,能盯上我这种小人物。并且骚扰电话犹如附骨之蛆,拦都拦不住,如同逐臭的苍蝇一样,赶都赶不走,也可能是因为我就是那臭吧。
这半年陆续街道百度地图的电话,问是不是继续用,在第一次打电话的时候告知他们不用了,答复说如果不用了,那可以把ak什么的删除掉。
后来又打了几次,实在受不了就给删除了,结果天几天又打,这脑子是有毛病还是咋滴?
另外一个就是交通银行信用卡,各种额度提升,让办理分期。每次打电话都是拒绝,结果过不了几天又开始开,最近已经到了拒接之后,第二天接着打,甚至一天打无数个!纯粹就是tmd脑子有病。
妈的,这些傻屌再打直接注销这个破b信用卡,艹!
The post 死缠烂打 appeared first on obaby 𝐢𝐧⃝ void.
浅谈WordPress静态化
以前在使用php 7.4的时候,并没有刻意尝试将页面静态化。然而,在升级到8.4之后由于一系列的问题,导致php响应异常缓慢,哪怕是开启了object cache。这就让人挺奇怪。不过在后期解决掉配置文件长度异常以及主题频繁的更新提示之后页面恢复正常了。
测速的时候,多数测速点基本都在1s以内。然而,与旧版一样,扛不住快速测试,快速测试的情况下,前面测速点的正常,后面的就会让php-fpm跑满cpu。于是,在之前的一篇文章提到了解决wp健康问题(响应超时)的问题,也是为了解决速度太慢的问题。本质上也是用的静态化的处理逻辑。
然而,这个东西存在问题,那就是我有多个域名,在数据更新之后怎么同时更新所有域名的缓存数据就成了问题。之前想的是通过wp插件,检测到变化删除缓存文件的方式。但是php-fpm由于权限问题导致删除失败。我又不想给php进程太高的权限,所以,后来尝试在nginx中进行操作,不过需要编译lua模块。
我不想编译,于是干脆换了 openresty:
OpenResty® 是一个基于 Nginx 与 Lua 的高性能 Web 平台,其内部集成了大量精良的 Lua 库、第三方模块以及大多数的依赖项。用于方便地搭建能够处理超高并发、扩展性极高的动态 Web 应用、Web 服务和动态网关。
OpenResty® 通过汇聚各种设计精良的 Nginx 模块(主要由 OpenResty 团队自主开发),从而将 Nginx 有效地变成一个强大的通用 Web 应用平台。这样,Web 开发人员和系统工程师可以使用 Lua 脚本语言调动 Nginx 支持的各种 C 以及 Lua 模块,快速构造出足以胜任 10K 乃至 1000K 以上单机并发连接的高性能 Web 应用系统。
这么一来就简单了,安装openresty替换掉原来的nginx:
1.更新缓存配置,所有的域名使用同一个缓存配置wordpress-php-with-cache-auto-purge-allinone.conf:
# WordPress PHP 处理 + FastCGI 页面缓存 + 自动清除缓存(无需插件)
# 基于 wordpress-php-with-cache.conf,添加自动缓存清除功能
# 每个站点独立缓存(缓存键包含 $host)
location ~ [^/]\.php(/|$)
{
try_files $uri =404;
fastcgi_pass unix:/run/php/php8.4-fpm.sock;
# ------ 页面缓存:跳过后台、登录、订阅等 ------
set $skip_cache 0;
if ($request_uri ~* "/wp-admin/|/wp-login\.php|/xmlrpc\.php|wp-.*\.php|/feed/|sitemap(_index)?.xml|/cart/|/checkout/|/my-account/") {
set $skip_cache 1;
}
if ($http_cookie ~* "comment_author|wordpress_[a-f0-9]+|wp-postpass|wordpress_logged_in|woocommerce_") {
set $skip_cache 1;
}
fastcgi_cache_bypass $skip_cache;
fastcgi_no_cache $skip_cache;
# ------ FastCGI 缓存(依赖 nginx.conf 中 fastcgi_cache_path ALLINONE)------
# 注意:缓存键包含 $host,每个站点独立缓存
fastcgi_cache ALLINONE;
fastcgi_cache_key $scheme$request_method$host$request_uri;
fastcgi_cache_valid 200 301 302 60m;
fastcgi_cache_use_stale error timeout updating http_500 http_503;
fastcgi_cache_lock on;
fastcgi_cache_lock_timeout 5s;
fastcgi_index index.php;
include fastcgi.conf;
# ------ 检测工具要求的客户端缓存响应头 ------
add_header X-Cache-Status $upstream_cache_status;
add_header X-Cache-Enabled "1";
add_header Cache-Control "public, max-age=3600";
}
缓存清理配置cache-purge-lua-allinone.conf:
# OpenResty Lua 缓存清除配置(ALLINONE 缓存版本)
# 在 server 块中添加此配置以启用缓存清除功能
# 适用于 ALLINONE 缓存(缓存键不包含 host,所有域名共享)
# 缓存清除 location(使用 Lua 脚本)
# 支持两种方式:
# 1. /purge/路径 - 清除指定路径的缓存
# 2. /purge-all - 清除全部缓存
# 3. PURGE 方法 - 使用 HTTP PURGE 方法清除当前请求的缓存
location ~ ^/purge(/.*)$ {
# 只允许本地或内网访问(安全考虑)
allow 127.0.0.1;
allow ::1;
allow 10.0.0.0/8;
allow 172.16.0.0/12;
allow 192.168.0.0/16;
deny all;
# 使用 Lua 脚本处理
content_by_lua_block {
local cache_purge = require "cache_purge_allinone"
cache_purge.handle_purge_request()
}
access_log off;
}
# 清除全部缓存 location
location = /purge-all {
# 只允许本地或内网访问(安全考虑)
allow 127.0.0.1;
allow ::1;
allow 10.0.0.0/8;
allow 172.16.0.0/12;
allow 192.168.0.0/16;
deny all;
# 使用 Lua 脚本处理
content_by_lua_block {
local cache_purge = require "cache_purge_allinone"
cache_purge.handle_purge_request()
}
access_log off;
}
# 获取缓存路径列表 location
location = /cache-paths {
# 只允许本地或内网访问(安全考虑)
allow 127.0.0.1;
allow ::1;
allow 10.0.0.0/8;
allow 172.16.0.0/12;
allow 192.168.0.0/16;
deny all;
# 使用 Lua 脚本处理
content_by_lua_block {
local cache_purge = require "cache_purge_allinone"
cache_purge.handle_cache_paths_request()
}
access_log off;
}
2.lua脚本部分:auto_cache_purge_allinone.lua
-- OpenResty 自动缓存清除脚本(ALLINONE 缓存)
-- 自动检测评论提交并清除缓存
-- 缓存规则:fastcgi_cache_key $scheme$request_method$host$request_uri(包含 host,每个站点独立缓存)
-- 缓存目录:/var/cache/nginx/allinone/
local _M = {}
-- 统一的缓存目录
local cache_path = '/var/cache/nginx/allinone'
-- 计算缓存文件路径
local function get_cache_file_path(cache_key_md5)
local level1 = string.sub(cache_key_md5, -1)
local level2 = string.sub(cache_key_md5, -3, -2)
return cache_path .. '/' .. level1 .. '/' .. level2 .. '/' .. cache_key_md5
end
-- 计算缓存键的 MD5(包含 host)
-- 缓存键格式:$scheme$request_method$host$request_uri
local function get_cache_key_md5(scheme, method, host, uri)
local cache_key_string = scheme .. method .. host .. uri
return ngx.md5(cache_key_string)
end
-- 删除缓存文件
local function delete_cache_file(file_path)
local command = 'rm -f "' .. file_path .. '" 2>/dev/null'
local ok = os.execute(command)
return ok == 0
end
-- 清除指定 URL 的缓存
local function purge_url(scheme, host, uri)
local method = 'GET'
local cache_key_md5 = get_cache_key_md5(scheme, method, host, uri)
local cache_file = get_cache_file_path(cache_key_md5)
local deleted = delete_cache_file(cache_file)
-- 也尝试删除匹配的文件(处理查询参数等情况)
local level1 = string.sub(cache_key_md5, -1)
local level2 = string.sub(cache_key_md5, -3, -2)
local cache_dir = cache_path .. '/' .. level1 .. '/' .. level2
local command = 'find "' .. cache_dir .. '" -name "' .. cache_key_md5 .. '*" -delete 2>/dev/null'
os.execute(command)
return deleted
end
-- 从评论提交请求中提取文章 ID
local function extract_post_id_from_request()
-- 尝试从请求体中获取 post_id
ngx.req.read_body()
local body = ngx.req.get_body_data()
if body then
-- 解析 POST 数据
local post_id = string.match(body, ".*comment_post_ID=(%d+)")
if post_id then
return tonumber(post_id)
end
end
-- 尝试从 referer 中提取
local referer = ngx.var.http_referer
if referer then
-- 从 URL 中提取文章 ID(WordPress 的 URL 结构)
local post_id = string.match(referer, ".*/%?p=(%d+)")
if post_id then
return tonumber(post_id)
end
end
return nil
end
-- 从 referer 中提取文章路径
local function extract_post_path()
-- 从 referer 获取(评论来源页面就是文章页面)
local referer = ngx.var.http_referer
if referer then
local path = string.match(referer, ".*https?://[^/]+(.+)")
if path then
-- 移除查询参数和锚点
path = string.match(path, "^([^?#]+)")
-- 移除尾部斜杠(除了根路径)
if path ~= "/" then
path = string.match(path, "^(.+)/$") or path
end
if path and path ~= "" then
return path
end
end
end
return nil
end
-- 实际的缓存清除操作(在异步定时器中执行)
local function do_purge_cache(scheme, host, post_path, request_uri)
-- 如果获取到文章信息,清除文章缓存
if post_path and post_path ~= "" and post_path ~= "/" then
local deleted = purge_url(scheme, host, post_path)
if deleted then
ngx.log(ngx.ERR, "[Auto Cache Purge ALLINONE] 已清除文章缓存: ", post_path)
end
-- 也尝试带尾部斜杠的版本
purge_url(scheme, host, post_path .. "/")
else
-- 如果获取不到文章信息,记录日志
ngx.log(ngx.ERR, "[Auto Cache Purge ALLINONE] 无法获取文章信息,仅清除首页缓存")
end
-- 无论是否获取到文章信息,都清除首页缓存
local home_deleted = purge_url(scheme, host, "/")
if home_deleted then
ngx.log(ngx.ERR, "[Auto Cache Purge ALLINONE] 已清除首页缓存")
end
end
-- 自动检测评论相关操作并清除缓存
function _M.auto_purge_on_comment()
ngx.log(ngx.ERR, "[Auto Cache Purge ALLINONE] log 阶段开始执行")
-- 检查是否已标记为跳过(例如在 header_filter 阶段已检测到需要忽略的请求)
if ngx.ctx and ngx.ctx.skip_cache_purge then
ngx.log(ngx.ERR, "[Auto Cache Purge ALLINONE] 跳过:已标记为 skip_cache_purge")
return
end
-- 只处理 POST 请求
if ngx.var.request_method ~= "POST" then
ngx.log(ngx.ERR, "[Auto Cache Purge ALLINONE] 跳过:不是 POST 请求,method=", ngx.var.request_method)
return
end
local request_uri = ngx.var.request_uri or ""
ngx.log(ngx.ERR, "[Auto Cache Purge ALLINONE] log 阶段,request_uri: ", request_uri)
local is_comment_action = false
local comment_id = nil
local post_id = nil
local body = nil
-- 检测前端评论提交
if string.find(request_uri, "wp%-comments%-post%.php") then
-- 检查状态码(302 重定向表示评论提交成功)
local status = ngx.status
ngx.log(ngx.ERR, "[Auto Cache Purge ALLINONE] wp-comments-post.php 请求,status: ", status)
if status ~= 302 and status ~= 200 then
return
end
is_comment_action = true
end
-- 检测后台 AJAX 评论操作(删除、批准、垃圾评论、回复评论等)
if string.find(request_uri, "admin%-ajax%.php") then
ngx.log(ngx.ERR, "[Auto Cache Purge ALLINONE] 检测到 admin-ajax.php 请求")
-- 首先尝试从上下文获取请求体(可能在 header_filter 阶段已读取)
if ngx.ctx and ngx.ctx.request_body then
body = ngx.ctx.request_body
ngx.log(ngx.ERR, "[Auto Cache Purge ALLINONE] 从上下文获取请求体,长度: ", string.len(body))
else
-- 安全地读取请求体(避免重复读取)
ngx.log(ngx.ERR, "[Auto Cache Purge ALLINONE] 尝试读取请求体")
local ok, err = pcall(function()
ngx.req.read_body()
body = ngx.req.get_body_data()
end)
if not ok then
ngx.log(ngx.ERR, "[Auto Cache Purge ALLINONE] 读取请求体失败: ", err or "unknown error")
return
end
if body then
ngx.log(ngx.ERR, "[Auto Cache Purge ALLINONE] 成功读取请求体,长度: ", string.len(body))
else
ngx.log(ngx.ERR, "[Auto Cache Purge ALLINONE] 请求体为空")
end
end
-- 检查 referer 和 body,如果不包含 /wp-admin/ 则跳过
local referer = ngx.var.http_referer or ""
ngx.log(ngx.ERR, "[Auto Cache Purge ALLINONE] referer: ", referer)
if not string.match(referer, ".*/wp%-admin/") then
ngx.log(ngx.ERR, "[Auto Cache Purge ALLINONE] 跳过:referer 不包含 /wp-admin/, referer=", referer)
return
end
if not body then
ngx.log(ngx.ERR, "[Auto Cache Purge ALLINONE] 跳过:body 为空")
return
end
-- 记录 body 内容(用于调试,只记录前 500 个字符)
local body_preview = string.sub(body, 1, 500)
ngx.log(ngx.ERR, "[Auto Cache Purge ALLINONE] admin-ajax.php 请求,body 预览: ", body_preview)
-- 检测请求动作,忽略 php_probe_realtime
local action_match = string.match(body, ".*action=([^&]+)")
ngx.log(ngx.ERR, "[Auto Cache Purge ALLINONE] action_match: ", action_match or "nil")
if action_match == "php_probe_realtime" then
ngx.log(ngx.ERR, "[Auto Cache Purge ALLINONE] 跳过:php_probe_realtime 请求")
return
end
-- 检测评论相关操作
local action_detected = false
if string.match(body, ".*action=delete%-comment") then
action_detected = true
ngx.log(ngx.ERR, "[Auto Cache Purge ALLINONE] 检测到 action=delete-comment")
elseif string.match(body, ".*action=trash%-comment") then
action_detected = true
ngx.log(ngx.ERR, "[Auto Cache Purge ALLINONE] 检测到 action=trash-comment")
elseif string.match(body, ".*action=untrash%-comment") then
action_detected = true
ngx.log(ngx.ERR, "[Auto Cache Purge ALLINONE] 检测到 action=untrash-comment")
elseif string.match(body, ".*action=spam%-comment") then
action_detected = true
ngx.log(ngx.ERR, "[Auto Cache Purge ALLINONE] 检测到 action=spam-comment")
elseif string.match(body, ".*action=unspam%-comment") then
action_detected = true
ngx.log(ngx.ERR, "[Auto Cache Purge ALLINONE] 检测到 action=unspam-comment")
elseif string.match(body, ".*action=approve%-comment") then
action_detected = true
ngx.log(ngx.ERR, "[Auto Cache Purge ALLINONE] 检测到 action=approve-comment")
elseif string.match(body, ".*action=unapprove%-comment") then
action_detected = true
ngx.log(ngx.ERR, "[Auto Cache Purge ALLINONE] 检测到 action=unapprove-comment")
elseif string.match(body, ".*action=replyto%-comment") then
action_detected = true
ngx.log(ngx.ERR, "[Auto Cache Purge ALLINONE] 检测到 action=replyto-comment")
end
if action_detected then
is_comment_action = true
-- 提取评论 ID 和文章 ID
comment_id = string.match(body, ".*comment_ID=(%d+)") or string.match(body, ".*id=(%d+)")
post_id = string.match(body, ".*comment_post_ID=(%d+)")
-- 记录检测到的评论操作
ngx.log(ngx.ERR, "[Auto Cache Purge ALLINONE] 检测到后台评论操作: action=", action_match or "unknown", ", comment_id=", comment_id or "nil", ", post_id=", post_id or "nil")
else
ngx.log(ngx.ERR, "[Auto Cache Purge ALLINONE] 未检测到评论相关操作")
end
end
if not is_comment_action then
ngx.log(ngx.ERR, "[Auto Cache Purge ALLINONE] 跳过:不是评论相关操作")
return
end
local scheme = ngx.var.scheme or "https"
local host = ngx.var.host or ngx.var.http_host or ""
-- 提取文章路径
local post_path = nil
-- 如果是后台 AJAX 操作,尝试从请求参数或 referer 中提取文章信息
if string.find(request_uri, "admin%-ajax%.php") and (comment_id or post_id) then
-- 使用之前读取的 body(避免重复读取)
if body then
-- 尝试从 _url 参数中提取文章路径
local url_param = string.match(body, ".*_url=([^&]+)")
if url_param then
url_param = ngx.unescape_uri(url_param)
-- 提取路径部分
local extracted_path = string.match(url_param, ".*https?://[^/]+(.+)")
if extracted_path then
extracted_path = string.match(extracted_path, "^([^?#]+)")
-- 如果不是后台页面,使用这个路径
if extracted_path and not string.match(extracted_path, ".*wp%-admin") then
post_path = extracted_path
end
end
end
-- 如果还没有找到路径,尝试从其他参数中提取
if not post_path or post_path == "" then
-- 尝试从 referer 中提取(如果 referer 是文章页面)
local referer = ngx.var.http_referer
if referer and not string.match(referer, ".*wp%-admin") then
post_path = string.match(referer, ".*https?://[^/]+(.+)")
if post_path then
post_path = string.match(post_path, "^([^?#]+)")
end
end
end
end
else
-- 前端评论提交,从 referer 提取文章路径
post_path = extract_post_path()
end
-- 使用异步定时器执行缓存清除操作
-- ngx.timer.at(0, handler) 会在当前请求处理完成后立即在后台执行
local ok, err = ngx.timer.at(0, function(premature, scheme_arg, host_arg, post_path_arg, request_uri_arg)
if premature then
-- 定时器被提前取消(不应该发生,因为 delay=0)
ngx.log(ngx.WARN, "[Auto Cache Purge ALLINONE] 定时器被提前取消")
return
end
-- 在异步上下文中执行实际的缓存清除
local purge_ok, purge_err = pcall(do_purge_cache, scheme_arg, host_arg, post_path_arg, request_uri_arg)
if not purge_ok then
ngx.log(ngx.ERR, "[Auto Cache Purge ALLINONE] 异步清除缓存错误: ", purge_err)
end
end, scheme, host, post_path, request_uri)
if not ok then
-- 如果创建定时器失败(例如定时器池已满),回退到同步执行
ngx.log(ngx.ERR, "[Auto Cache Purge ALLINONE] 无法创建异步定时器,使用同步执行: ", err)
do_purge_cache(scheme, host, post_path, request_uri)
end
end
-- 在 rewrite 阶段执行(可以读取请求体)
-- 提前读取请求体并保存到上下文,供 log 阶段使用
function _M.rewrite()
-- 只处理 POST 请求
if ngx.var.request_method ~= "POST" then
return
end
local request_uri = ngx.var.request_uri or ""
-- 只处理 admin-ajax.php 和 wp-comments-post.php 请求
if string.find(request_uri, "admin%-ajax%.php") or string.find(request_uri, "wp%-comments%-post%.php") then
-- 检查 referer(对于 admin-ajax.php)
if string.find(request_uri, "admin%-ajax%.php") then
local referer = ngx.var.http_referer or ""
if not string.find(referer, "/wp%-admin/") then
ngx.ctx.skip_cache_purge = true
return
end
end
-- 在 rewrite 阶段读取请求体(这个阶段允许读取)
local ok, err = pcall(function()
ngx.req.read_body()
local body = ngx.req.get_body_data()
if body then
-- 对于 admin-ajax.php,检查是否为需要忽略的请求
if string.find(request_uri, "admin%-ajax%.php") then
local action_match = string.match(body, ".*action=([^&]+)")
if action_match == "php_probe_realtime" then
ngx.ctx.skip_cache_purge = true
return
end
end
-- 保存到上下文,供 log 阶段使用
ngx.ctx.request_body = body
ngx.log(ngx.ERR, "[Auto Cache Purge ALLINONE] rewrite 阶段已保存请求体,长度: ", string.len(body))
else
ngx.log(ngx.ERR, "[Auto Cache Purge ALLINONE] rewrite 阶段:请求体为空")
end
end)
if not ok then
ngx.log(ngx.ERR, "[Auto Cache Purge ALLINONE] rewrite 阶段读取请求体出错: ", err or "unknown error")
end
end
end
-- 在响应头过滤阶段执行(可以访问响应头)
-- 注意:header_filter 阶段无法读取请求体,只能做简单的标记
function _M.header_filter()
-- header_filter 阶段不再需要做任何处理
-- 所有检查都在 rewrite 阶段完成
end
-- 在日志阶段执行(确保在响应完成后)
function _M.log()
-- 使用 pcall 包装,避免错误导致请求失败
local ok, err = pcall(function()
_M.auto_purge_on_comment()
end)
if not ok then
ngx.log(ngx.ERR, "[Auto Cache Purge ALLINONE] log 阶段执行错误: ", err)
end
end
return _M
cache_purge_allinone.lua:
-- OpenResty Lua 缓存清除脚本(ALLINONE 缓存版本,支持独立缓存)
-- 支持独立缓存清除(缓存键包含 host,每个站点独立缓存)
-- 缓存键格式:$scheme$request_method$host$request_uri
local _M = {}
-- 统一的缓存目录(ALLINONE 缓存)
local cache_path = "/var/cache/nginx/allinone"
-- 计算缓存文件路径(基于 Nginx levels=1:2)
local function get_cache_file_path(cache_key_md5)
local level1 = string.sub(cache_key_md5, -1) -- 最后1位
local level2 = string.sub(cache_key_md5, -3, -2) -- 倒数第3-2位
return cache_path .. "/" .. level1 .. "/" .. level2 .. "/" .. cache_key_md5
end
-- 计算缓存键的 MD5(包含 host,每个站点独立缓存)
-- 缓存键格式:$scheme$request_method$host$request_uri
local function get_cache_key_md5(scheme, method, host, uri)
local cache_key_string = scheme .. method .. host .. uri
return ngx.md5(cache_key_string)
end
-- 删除缓存文件
local function delete_cache_file(file_path)
-- 使用 shell 命令删除(更可靠)
local command = "rm -f \"" .. file_path .. "\" 2>/dev/null"
local ok = os.execute(command)
if ok == 0 then
return true
else
ngx.log(ngx.ERR, "[Cache Purge ALLINONE] 删除缓存文件失败: ", file_path)
return false
end
end
-- 清除指定 URL 的缓存(包含 host,每个站点独立缓存)
function _M.purge_url(scheme, host, uri)
-- 计算缓存键(包含 host):$scheme$request_method$host$request_uri
local method = "GET"
local cache_key_md5 = get_cache_key_md5(scheme, method, host, uri)
-- 获取缓存文件路径
local cache_file = get_cache_file_path(cache_key_md5)
-- 删除缓存文件
local deleted = delete_cache_file(cache_file)
-- 也尝试删除匹配的文件(处理查询参数等情况)
local level1 = string.sub(cache_key_md5, -1)
local level2 = string.sub(cache_key_md5, -3, -2)
local cache_dir = cache_path .. "/" .. level1 .. "/" .. level2
-- 使用 shell 命令删除匹配的文件
local command = "find \"" .. cache_dir .. "\" -name \"" .. cache_key_md5 .. "*\" -delete 2>/dev/null"
local result = os.execute(command)
if result == 0 then
deleted = true
end
-- 也尝试带尾部斜杠的版本(如果原路径没有)
if uri ~= "/" and not string.match(uri, "/$") then
local uri_with_slash = uri .. "/"
local cache_key_md5_slash = get_cache_key_md5(scheme, method, host, uri_with_slash)
local cache_file_slash = get_cache_file_path(cache_key_md5_slash)
delete_cache_file(cache_file_slash)
local level1_slash = string.sub(cache_key_md5_slash, -1)
local level2_slash = string.sub(cache_key_md5_slash, -3, -2)
local cache_dir_slash = cache_path .. "/" .. level1_slash .. "/" .. level2_slash
local command_slash = "find \"" .. cache_dir_slash .. "\" -name \"" .. cache_key_md5_slash .. "*\" -delete 2>/dev/null"
os.execute(command_slash)
end
-- 也尝试不带尾部斜杠的版本(如果原路径有)
if uri ~= "/" and string.match(uri, "/$") then
local uri_without_slash = string.match(uri, "^(.+)/$") or uri
local cache_key_md5_no_slash = get_cache_key_md5(scheme, method, host, uri_without_slash)
local cache_file_no_slash = get_cache_file_path(cache_key_md5_no_slash)
delete_cache_file(cache_file_no_slash)
local level1_no_slash = string.sub(cache_key_md5_no_slash, -1)
local level2_no_slash = string.sub(cache_key_md5_no_slash, -3, -2)
local cache_dir_no_slash = cache_path .. "/" .. level1_no_slash .. "/" .. level2_no_slash
local command_no_slash = "find \"" .. cache_dir_no_slash .. "\" -name \"" .. cache_key_md5_no_slash .. "*\" -delete 2>/dev/null"
os.execute(command_no_slash)
end
return deleted
end
-- 清除全部缓存
function _M.purge_all()
ngx.log(ngx.ERR, "[Cache Purge ALLINONE] 开始清除全部缓存,目录: " .. cache_path)
-- 测试:先检查当前用户和权限
local whoami_cmd = "whoami 2>&1"
local whoami_handle = io.popen(whoami_cmd)
if whoami_handle then
local user = whoami_handle:read("*a")
whoami_handle:close()
ngx.log(ngx.ERR, "[Cache Purge ALLINONE] 当前用户: " .. (user or "unknown"))
end
-- 测试:检查缓存目录权限
local ls_cmd = "ls -la '" .. cache_path .. "' 2>&1"
local ls_handle = io.popen(ls_cmd)
if ls_handle then
local ls_output = ls_handle:read("*a")
ls_handle:close()
ngx.log(ngx.ERR, "[Cache Purge ALLINONE] 目录权限: " .. ls_output)
end
-- 方法1: 直接使用 rm 命令
local cmd = "rm -rf '" .. cache_path .. "'/* 2>&1"
ngx.log(ngx.ERR, "[Cache Purge ALLINONE] 执行命令: " .. cmd)
local handle = io.popen(cmd)
if handle then
local output = handle:read("*a")
local success, exit_reason, exit_code = handle:close()
ngx.log(ngx.ERR, "[Cache Purge ALLINONE] 命令输出: " .. (output or "无输出"))
ngx.log(ngx.ERR, "[Cache Purge ALLINONE] 命令结果: success=" .. tostring(success) .. ", exit_code=" .. tostring(exit_code))
if exit_code == 0 then
ngx.log(ngx.ERR, "[Cache Purge ALLINONE] 成功清除全部缓存")
return true, 1
end
else
ngx.log(ngx.ERR, "[Cache Purge ALLINONE] 无法执行命令: io.popen 失败")
end
-- 方法2: 如果上面的失败了,尝试使用 os.execute
ngx.log(ngx.ERR, "[Cache Purge ALLINONE] 尝试 os.execute 方法")
local result = os.execute(cmd)
ngx.log(ngx.ERR, "[Cache Purge ALLINONE] os.execute 结果: " .. tostring(result))
if result == 0 then
ngx.log(ngx.ERR, "[Cache Purge ALLINONE] os.execute 成功清除全部缓存")
return true, 1
end
ngx.log(ngx.ERR, "[Cache Purge ALLINONE] 所有删除方法都失败了")
return false, 0
end
-- 处理缓存路径请求
function _M.handle_cache_paths_request()
local success, result = _M.get_cache_paths()
if success then
ngx.status = 200
ngx.header["Content-Type"] = "application/json"
-- 限制返回数量
local limit = 100
local limited_paths = {}
for i = 1, math.min(limit, #result) do
table.insert(limited_paths, result[i])
end
-- 构建简单的 JSON 响应
local json_paths = {}
for _, path in ipairs(limited_paths) do
table.insert(json_paths, string.format('{"path":"%s","size":%d,"mtime":%d,"md5":"%s"}',
path.path,
path.size,
path.mtime,
path.md5
))
end
local response = string.format('{"status":"success","paths":[%s],"total_count":%d,"has_more":%s,"limit":%d}',
table.concat(json_paths, ','),
#result,
#result > limit and "true" or "false",
limit
)
ngx.say(response)
else
ngx.status = 500
ngx.header["Content-Type"] = "application/json"
local error_msg = type(result) == "string" and result or "获取缓存路径失败"
ngx.say(string.format('{"status":"error","message":"%s"}', error_msg))
ngx.log(ngx.ERR, "[Cache Purge ALLINONE] 获取缓存路径失败: " .. error_msg)
end
ngx.exit(ngx.status)
end
-- 处理 PURGE 请求
function _M.handle_purge_request()
local request_uri = ngx.var.request_uri or ""
-- 检查是否是清除全部缓存的请求
if string.match(request_uri, "^/purge%-all") then
local success, file_count = _M.purge_all()
if success then
ngx.status = 200
ngx.header["Content-Type"] = "application/json"
ngx.say("{\"status\":\"success\",\"message\":\"已清除全部缓存\",\"deleted_count\":" .. file_count .. "}")
else
ngx.status = 500
ngx.header["Content-Type"] = "application/json"
ngx.say("{\"status\":\"error\",\"message\":\"清除全部缓存失败,请检查 Nginx 错误日志获取详细信息\"}")
end
ngx.exit(ngx.status)
return
end
-- 从请求 URI 中提取路径(/purge/xxx -> xxx)
local uri = string.match(request_uri, "^/purge(/.*)$")
if not uri or uri == "" then
uri = "/"
end
-- 移除查询参数
uri = string.match(uri, "^([^?]+)") or uri
-- 强制使用 https(因为实际缓存都是 https 的)
local scheme = "https"
-- 获取 host(从 Host 头或 server_name 获取)
local host = ngx.var.http_host or ngx.var.host or ngx.var.server_name or ""
-- 如果 host 为空,记录错误
if host == "" then
ngx.log(ngx.ERR, "[Cache Purge ALLINONE] 无法获取 host,清除失败")
ngx.status = 400
ngx.header["Content-Type"] = "application/json"
ngx.say("{\"status\":\"error\",\"message\":\"无法获取 host 信息\"}")
ngx.exit(ngx.status)
end
-- 清除缓存(包含 host,每个站点独立缓存)
local success = _M.purge_url(scheme, host, uri)
if success then
ngx.status = 200
ngx.header["Content-Type"] = "application/json"
ngx.say("{\"status\":\"success\",\"message\":\"已清除缓存: " .. scheme .. "://" .. host .. uri .. " (独立缓存)\"}")
ngx.log(ngx.ERR, "[Cache Purge ALLINONE] 成功清除缓存: " .. scheme .. "://" .. host .. uri)
else
ngx.status = 404
ngx.header["Content-Type"] = "application/json"
ngx.say("{\"status\":\"error\",\"message\":\"清除失败: " .. scheme .. "://" .. host .. uri .. " (可能缓存不存在)\"}")
ngx.log(ngx.ERR, "[Cache Purge ALLINONE] 清除缓存失败: " .. scheme .. "://" .. host .. uri)
end
ngx.exit(ngx.status)
end
-- 获取缓存路径列表
function _M.get_cache_paths()
ngx.log(ngx.ERR, "[Cache Purge ALLINONE] 开始获取缓存路径列表,目录: " .. cache_path)
local paths = {}
local total_count = 0
-- 直接尝试使用 find 命令,如果目录不存在,find 会返回错误
-- 使用 -maxdepth 限制深度,提高性能
local cmd = "find '" .. cache_path .. "' -maxdepth 3 -type f 2>&1 | head -100"
ngx.log(ngx.ERR, "[Cache Purge ALLINONE] 执行命令: " .. cmd)
local handle = io.popen(cmd)
if not handle then
ngx.log(ngx.ERR, "[Cache Purge ALLINONE] io.popen 执行失败")
return false, "无法执行 find 命令"
end
local output = handle:read("*a")
local success, exit_reason, exit_code = handle:close()
ngx.log(ngx.ERR, "[Cache Purge ALLINONE] 命令输出长度: " .. (output and string.len(output) or 0))
ngx.log(ngx.ERR, "[Cache Purge ALLINONE] 命令结果: success=" .. tostring(success) .. ", exit_code=" .. tostring(exit_code))
-- 检查输出中是否包含错误信息
if output and string.find(output, "No such file or directory") then
ngx.log(ngx.ERR, "[Cache Purge ALLINONE] 缓存目录不存在")
return false, "缓存目录不存在: " .. cache_path
end
if not output or output == "" then
ngx.log(ngx.ERR, "[Cache Purge ALLINONE] find 命令无输出,可能是目录为空或不存在")
-- 返回空列表而不是错误
return true, {}
end
-- 解析输出
for line in output:gmatch("[^\r\n]+") do
if line ~= "" and not string.find(line, "^find:") then -- 跳过错误行
local full_path = line:gsub("^%s+", ""):gsub("%s+$", "") -- 去除首尾空格
if full_path ~= "" and string.find(full_path, cache_path) then
-- 提取相对路径和文件名
local relative_path = full_path:gsub("^" .. cache_path:gsub("%-", "%%-"):gsub("%.", "%%.") .. "/", "")
local md5 = relative_path:match("([^/]+)$") or relative_path
-- 尝试获取文件大小和修改时间
local stat_cmd = "stat -c '%s %Y' '" .. full_path .. "' 2>/dev/null"
local stat_handle = io.popen(stat_cmd)
local size = 0
local mtime = 0
if stat_handle then
local stat_output = stat_handle:read("*a")
stat_handle:close()
if stat_output then
local stat_size, stat_mtime = stat_output:match("(%d+)%s+(%d+)")
if stat_size then size = tonumber(stat_size) or 0 end
if stat_mtime then mtime = tonumber(stat_mtime) or 0 end
end
end
table.insert(paths, {
path = full_path,
size = size,
mtime = mtime,
md5 = md5
})
total_count = total_count + 1
-- 限制返回数量
if total_count >= 100 then
break
end
end
end
end
-- 按修改时间倒序排序
table.sort(paths, function(a, b) return a.mtime > b.mtime end)
ngx.log(ngx.ERR, "[Cache Purge ALLINONE] 成功获取 " .. #paths .. " 个缓存文件路径")
return true, paths
end
return _M
3.openresty nginx.conf 配置添加下面的内容 在http中添加:
http
{
# Lua 模块路径配置(必须)
lua_package_path "/usr/local/openresty/nginx/conf/lua/?.lua;;";
# ALLINONE
fastcgi_cache_path /var/cache/nginx/allinone
levels=1:2
keys_zone=ALLINONE:64m
max_size=512m
inactive=60m
use_temp_path=off;
4.修改vhost配置文件添加conf引入zhongxiaojie.com.conf:
include cache-purge-lua-allinone.conf; include wordpress-php-with-cache-auto-purge-allinone.conf;
5.wp插件nginx-cache-purge-multi-domain.php 如果只有一个域名,就保留一个就可以了:
<?php
/**
* Plugin Name: Nginx FastCGI Cache Purge Multi-Domain
* Description: 当有新评论提交或文章更新时,自动清除多个域名的 Nginx FastCGI 缓存(每个站点独立缓存)
* Version: 3.1
* Author: obaby
*/
// 防止直接访问
if (!defined("ABSPATH")) {
exit;
}
class Nginx_Cache_Purge_Multi_Domain {
// 统一的缓存目录(ALLINONE 缓存,但每个站点独立缓存)
private $cache_path = "/var/cache/nginx/allinone";
// 需要清除缓存的所有域名列表(可通过 WordPress 选项覆盖)
private $default_domains = array(
"zhongxiaojie.com",
"oba.by",
);
// 选项名称
private $option_name = "nginx_cache_purge_multi_domain";
// 是否启用调试日志
private $debug = true;
// 异步清除队列
private $purge_queue = array();
// 缓存键计算缓存(避免重复计算)
private $cache_key_cache = array();
// 文件系统访问检查缓存
private $fs_access_cache = null;
public function __construct() {
// 注册 hooks
add_action("comment_post", array($this, "purge_cache_on_comment"), 10, 2);
add_action("wp_set_comment_status", array($this, "purge_cache_on_comment_status"), 10, 2);
add_action("save_post", array($this, "purge_cache_on_post_save"), 10, 1);
// 异步处理队列(在请求结束后执行)
add_action("shutdown", array($this, "process_purge_queue"), 999);
// 添加管理菜单
add_action("admin_menu", array($this, "add_admin_menu"));
// 处理 AJAX 请求
add_action("wp_ajax_nginx_cache_purge_all", array($this, "ajax_purge_all_cache"));
add_action("wp_ajax_nginx_cache_get_info", array($this, "ajax_get_cache_info"));
add_action("wp_ajax_nginx_cache_get_paths", array($this, "ajax_get_cache_paths"));
if ($this->debug) {
$this->log("插件初始化完成");
}
}
/**
* 获取配置的域名列表
*/
private function get_domains() {
$options = get_option($this->option_name, array());
if (isset($options["domains"]) && is_array($options["domains"]) && !empty($options["domains"])) {
return $options["domains"];
}
return $this->default_domains;
}
/**
* 添加到清除队列(异步处理)
*/
private function add_to_purge_queue($scheme, $host, $path) {
$key = $scheme . "|" . $host . "|" . $path;
if (!isset($this->purge_queue[$key])) {
$this->purge_queue[$key] = array(
"scheme" => $scheme,
"host" => $host,
"path" => $path,
);
}
}
/**
* 批量添加到清除队列
*/
private function add_paths_to_queue($paths, $scheme = "https") {
$domains = $this->get_domains();
foreach ($domains as $domain) {
foreach ($paths as $path) {
$this->add_to_purge_queue($scheme, $domain, $path);
}
}
}
/**
* 处理清除队列(异步执行)
*/
public function process_purge_queue() {
if (empty($this->purge_queue)) {
return;
}
// 如果支持 fastcgi_finish_request,立即结束响应,在后台处理
if (function_exists("fastcgi_finish_request")) {
fastcgi_finish_request();
}
if ($this->debug) {
$this->log(sprintf("开始异步处理清除队列,共 %d 个任务", count($this->purge_queue)));
}
$success_count = 0;
$fail_count = 0;
foreach ($this->purge_queue as $item) {
$result = $this->purge_url_for_domain($item["scheme"], $item["host"], $item["path"]);
if ($result) {
$success_count++;
} else {
$fail_count++;
}
}
if ($this->debug) {
$this->log(sprintf("清除队列处理完成:成功 %d,失败 %d", $success_count, $fail_count));
}
// 清空队列
$this->purge_queue = array();
}
/**
* 评论提交后清除缓存
*/
public function purge_cache_on_comment($comment_id, $comment_approved) {
if ($this->debug) {
$this->log(sprintf("Hook 触发: comment_id=%d, approved=%s", $comment_id, $comment_approved));
}
// 只处理已批准的评论
if ($comment_approved != 1) {
return;
}
$comment = get_comment($comment_id);
if (!$comment) {
return;
}
$post_id = $comment->comment_post_ID;
$post = get_post($post_id);
if (!$post) {
return;
}
// 获取需要清除的路径
$paths = $this->get_post_purge_paths($post_id);
// 添加到异步队列
$this->add_paths_to_queue($paths);
if ($this->debug) {
$this->log(sprintf("评论提交:已添加到清除队列,post_id=%d,路径数=%d", $post_id, count($paths)));
}
}
/**
* 评论状态变更时清除缓存
*/
public function purge_cache_on_comment_status($comment_id, $status) {
if (!in_array($status, array("approve", "spam", "trash"))) {
return;
}
$comment = get_comment($comment_id);
if (!$comment) {
return;
}
$post_id = $comment->comment_post_ID;
$paths = $this->get_post_purge_paths($post_id);
$this->add_paths_to_queue($paths);
}
/**
* 文章保存时清除缓存
*/
public function purge_cache_on_post_save($post_id) {
// 跳过自动保存和修订
if (defined("DOING_AUTOSAVE") && DOING_AUTOSAVE) {
return;
}
if (wp_is_post_revision($post_id)) {
return;
}
$post = get_post($post_id);
if (!$post || $post->post_status != "publish") {
return;
}
$paths = $this->get_post_purge_paths($post_id);
$this->add_paths_to_queue($paths);
if ($this->debug) {
$this->log(sprintf("文章保存:已添加到清除队列,post_id=%d,路径数=%d", $post_id, count($paths)));
}
}
/**
* 获取文章相关的清除路径列表
*/
private function get_post_purge_paths($post_id) {
$paths = array();
// 文章页面
$post_url = get_permalink($post_id);
if ($post_url) {
$parsed = parse_url($post_url);
if (isset($parsed["path"])) {
$paths[] = $parsed["path"];
}
}
// 首页
$paths[] = "/";
// 分类页
$categories = get_the_category($post_id);
foreach ($categories as $category) {
$cat_url = get_category_link($category->term_id);
if ($cat_url) {
$parsed = parse_url($cat_url);
if (isset($parsed["path"])) {
$paths[] = $parsed["path"];
}
}
}
return array_unique($paths);
}
/**
* 清除指定域名和路径的缓存
*/
private function purge_url_for_domain($scheme, $host, $path) {
if (empty($host) || empty($path)) {
return false;
}
// 方法1:尝试 HTTP PURGE(优先,更快)
if ($this->purge_via_http($scheme, $host, $path)) {
return true;
}
// 方法2:直接删除缓存文件
return $this->purge_via_file_delete($scheme, $host, $path);
}
/**
* 通过 HTTP 请求清除缓存
*/
private function purge_via_http($scheme, $host, $path) {
$purge_url = "http://127.0.0.1/purge" . $path; // 使用本地回环,避免外部网络开销
$args = array(
"method" => "GET",
"timeout" => 1, // 减少超时时间,快速失败
"headers" => array(
"Host" => $host,
),
"sslverify" => false,
"blocking" => true, // 必须阻塞,否则无法判断结果
);
$response = wp_remote_request($purge_url, $args);
if (is_wp_error($response)) {
if ($this->debug) {
$this->log(sprintf("HTTP PURGE 失败: %s - %s", $purge_url, $response->get_error_message()));
}
return false;
}
$code = wp_remote_retrieve_response_code($response);
// 200 或 404 都算成功
if ($code == 200 || $code == 404) {
if ($this->debug) {
$this->log(sprintf("HTTP PURGE 成功: %s (code=%d)", $purge_url, $code));
}
return true;
}
return false;
}
/**
* 通过删除缓存文件清除缓存
*/
private function purge_via_file_delete($scheme, $host, $path) {
// 检查文件系统访问权限(缓存结果)
if ($this->fs_access_cache === null) {
$this->fs_access_cache = $this->check_fs_access();
}
if (!$this->fs_access_cache) {
// 无法访问文件系统,尝试 shell 命令
return $this->purge_via_shell($scheme, $host, $path);
}
$deleted = false;
$path_variants = $this->get_path_variants($path);
foreach ($path_variants as $path_variant) {
$cache_files = $this->get_cache_files($scheme, $host, $path_variant);
foreach ($cache_files as $cache_file) {
if (@file_exists($cache_file) && @unlink($cache_file)) {
$deleted = true;
if ($this->debug) {
$this->log(sprintf("文件删除成功: %s", $cache_file));
}
}
}
// 也尝试删除匹配的文件(处理查询参数等情况)
$cache_dir = dirname($cache_files[0]);
if (@is_dir($cache_dir)) {
$md5 = $this->get_cache_key_md5($scheme, $host, $path_variant);
$files = @glob($cache_dir . "/" . $md5 . "*");
if ($files && is_array($files)) {
foreach ($files as $file) {
if (@is_file($file) && @unlink($file)) {
$deleted = true;
}
}
}
}
}
// 如果文件删除失败,尝试 shell 命令
if (!$deleted) {
$deleted = $this->purge_via_shell($scheme, $host, $path);
}
return $deleted;
}
/**
* 检查文件系统访问权限
*/
private function check_fs_access() {
$old_error_handler = set_error_handler(function($errno, $errstr) {
if (strpos($errstr, "open_basedir") !== false) {
return true;
}
return false;
}, E_WARNING);
$can_access = @is_dir($this->cache_path);
restore_error_handler();
return $can_access;
}
/**
* 获取路径变体(处理尾部斜杠等)
*/
private function get_path_variants($path) {
$variants = array(
$path,
rtrim($path, "/"),
$path . "/",
);
// 去重
return array_unique($variants);
}
/**
* 计算缓存键的 MD5(带缓存)
*/
private function get_cache_key_md5($scheme, $host, $path) {
$key = $scheme . "|" . $host . "|" . $path;
if (!isset($this->cache_key_cache[$key])) {
$cache_key_string = $scheme . "GET" . $host . $path;
$this->cache_key_cache[$key] = md5($cache_key_string);
}
return $this->cache_key_cache[$key];
}
/**
* 获取缓存文件路径列表
*/
private function get_cache_files($scheme, $host, $path) {
$md5 = $this->get_cache_key_md5($scheme, $host, $path);
$level1 = substr($md5, -1);
$level2 = substr($md5, -3, 2);
$cache_file = $this->cache_path . "/" . $level1 . "/" . $level2 . "/" . $md5;
return array($cache_file);
}
/**
* 通过 shell 命令删除缓存文件
*/
private function purge_via_shell($scheme, $host, $path) {
// 检查 shell 命令是否可用
if (!function_exists("exec")) {
return false;
}
$disabled_functions = explode(",", ini_get("disable_functions"));
if (in_array("exec", $disabled_functions)) {
return false;
}
$deleted = false;
$path_variants = $this->get_path_variants($path);
foreach ($path_variants as $path_variant) {
$md5 = $this->get_cache_key_md5($scheme, $host, $path_variant);
$level1 = substr($md5, -1);
$level2 = substr($md5, -3, 2);
$cache_file = $this->cache_path . "/" . $level1 . "/" . $level2 . "/" . $md5;
// 删除精确匹配的文件
$command = sprintf("rm -f %s 2>/dev/null", escapeshellarg($cache_file));
@exec($command, $output, $return_var);
if ($return_var === 0) {
$deleted = true;
}
// 删除匹配的文件(处理查询参数)
$cache_dir = dirname($cache_file);
$glob_command = sprintf("rm -f %s/%s* 2>/dev/null", escapeshellarg($cache_dir), escapeshellarg($md5));
@exec($glob_command, $glob_output, $glob_return_var);
if ($glob_return_var === 0) {
$deleted = true;
}
}
return $deleted;
}
/**
* 日志记录(统一入口)
*/
private function log($message) {
if ($this->debug && function_exists("error_log")) {
error_log("[Nginx Cache Purge Multi-Domain] " . $message);
}
}
/**
* 添加管理菜单
*/
public function add_admin_menu() {
add_management_page(
"Nginx 缓存管理",
"Nginx 缓存管理",
"manage_options",
"nginx-cache-purge-tools",
array($this, "render_tools_page")
);
}
/**
* 渲染工具页面
*/
public function render_tools_page() {
if (!current_user_can("manage_options")) {
wp_die("您没有权限访问此页面");
}
// 获取缓存信息
$cache_info = $this->get_cache_info();
$sample_md5 = $this->get_sample_cache_md5();
?>
<div class="wrap">
<h1>Nginx 缓存管理工具</h1>
<div class="card" style="max-width: 800px;">
<h2>缓存信息</h2>
<table class="form-table">
<tr>
<th scope="row">缓存目录</th>
<td>
<code><?php echo esc_html($this->cache_path); ?></code>
<?php if (!$cache_info["accessible"]): ?>
<span style="color: red;">(无法访问)</span>
<?php endif; ?>
</td>
</tr>
<tr>
<th scope="row">缓存大小</th>
<td>
<?php if ($cache_info["accessible"]): ?>
<strong><?php echo esc_html($this->format_bytes($cache_info["size"])); ?></strong>
<?php else: ?>
<span style="color: red;">无法获取</span>
<?php endif; ?>
</td>
</tr>
<tr>
<th scope="row">缓存文件数</th>
<td>
<?php if ($cache_info["accessible"]): ?>
<strong><?php echo number_format($cache_info["file_count"]); ?></strong> 个文件
<?php else: ?>
<span style="color: red;">无法获取</span>
<?php endif; ?>
</td>
</tr>
<tr>
<th scope="row">缓存 MD5 示例</th>
<td>
<code><?php echo esc_html($sample_md5["md5"]); ?></code>
<br>
<small style="color: #666;">
缓存键: <code><?php echo esc_html($sample_md5["key"]); ?></code>
<br>
文件路径: <code><?php echo esc_html($sample_md5["path"]); ?></code>
</small>
</td>
</tr>
</table>
<p>
<button type="button" id="refresh-cache-info" class="button">刷新信息</button>
<button type="button" id="purge-all-cache" class="button button-primary" style="margin-left: 10px;">清除全部缓存</button>
<button type="button" id="show-cache-paths" class="button" style="margin-left: 10px;">查看缓存文件</button>
</p>
<div id="cache-action-message" style="margin-top: 15px;"></div>
</div>
<div class="card" id="cache-paths-section" style="max-width: 800px; display: none; margin-top: 20px;">
<h2>缓存文件列表</h2>
<div id="cache-paths-controls" style="margin-bottom: 15px;">
<label for="cache-paths-limit">显示数量:</label>
<select id="cache-paths-limit">
<option value="20">20</option>
<option value="50" selected>50</option>
<option value="100">100</option>
<option value="200">200</option>
</select>
<button type="button" id="refresh-cache-paths" class="button" style="margin-left: 10px;">刷新列表</button>
</div>
<div id="cache-paths-content">
<p class="description">正在加载缓存文件列表...</p>
</div>
<div id="cache-paths-message" style="margin-top: 15px;"></div>
</div>
</div>
<script type="text/javascript">
jQuery(document).ready(function($) {
// 刷新缓存信息
$('#refresh-cache-info').on('click', function() {
var $btn = $(this);
var $msg = $('#cache-action-message');
$btn.prop('disabled', true).text('刷新中...');
$msg.html('<div class="notice notice-info"><p>正在刷新缓存信息...</p></div>');
$.ajax({
url: ajaxurl,
type: 'POST',
data: {
action: 'nginx_cache_get_info',
nonce: '<?php echo wp_create_nonce("nginx_cache_tools"); ?>'
},
success: function(response) {
if (response.success) {
var info = response.data;
$msg.html('<div class="notice notice-success"><p>刷新成功!缓存大小: ' + info.size_formatted + ', 文件数: ' + info.file_count_formatted + '</p></div>');
// 更新页面显示
if (info.accessible) {
$('th:contains("缓存大小")').next('td').html('<strong>' + info.size_formatted + '</strong>');
$('th:contains("缓存文件数")').next('td').html('<strong>' + info.file_count_formatted + '</strong> 个文件');
}
} else {
$msg.html('<div class="notice notice-error"><p>刷新失败: ' + (response.data || '未知错误') + '</p></div>');
}
},
error: function() {
$msg.html('<div class="notice notice-error"><p>请求失败,请重试</p></div>');
},
complete: function() {
$btn.prop('disabled', false).text('刷新信息');
}
});
});
// 清除全部缓存
$('#purge-all-cache').on('click', function() {
if (!confirm('确定要清除全部缓存吗?此操作不可恢复!')) {
return;
}
var $btn = $(this);
var $msg = $('#cache-action-message');
$btn.prop('disabled', true).text('清除中...');
$msg.html('<div class="notice notice-info"><p>正在清除全部缓存,请稍候...</p></div>');
$.ajax({
url: ajaxurl,
type: 'POST',
data: {
action: 'nginx_cache_purge_all',
nonce: '<?php echo wp_create_nonce("nginx_cache_tools"); ?>'
},
success: function(response) {
if (response.success) {
$msg.html('<div class="notice notice-success"><p>' + (response.data.message || '缓存清除成功!') + '</p></div>');
// 自动刷新缓存信息
setTimeout(function() {
$('#refresh-cache-info').trigger('click');
}, 1000);
} else {
$msg.html('<div class="notice notice-error"><p>清除失败: ' + (response.data || '未知错误') + '</p></div>');
}
},
error: function() {
$msg.html('<div class="notice notice-error"><p>请求失败,请重试</p></div>');
},
complete: function() {
$btn.prop('disabled', false).text('清除全部缓存');
}
});
});
// 显示缓存文件列表
$('#show-cache-paths').on('click', function() {
var $section = $('#cache-paths-section');
if ($section.is(':visible')) {
$section.hide();
$(this).text('查看缓存文件');
} else {
$section.show();
$(this).text('隐藏缓存文件');
loadCachePaths();
}
});
// 刷新缓存文件列表
$('#refresh-cache-paths').on('click', function() {
loadCachePaths();
});
// 当限制数量改变时重新加载
$('#cache-paths-limit').on('change', function() {
loadCachePaths();
});
function loadCachePaths() {
var $content = $('#cache-paths-content');
var $msg = $('#cache-paths-message');
var limit = $('#cache-paths-limit').val();
$content.html('<p class="description">正在加载缓存文件列表...</p>');
$msg.empty();
$.ajax({
url: ajaxurl,
type: 'POST',
data: {
action: 'nginx_cache_get_paths',
limit: limit,
nonce: '<?php echo wp_create_nonce("nginx_cache_tools"); ?>'
},
success: function(response) {
if (response.success) {
var data = response.data;
var html = '';
if (data.paths && data.paths.length > 0) {
html += '<table class="widefat striped">';
html += '<thead><tr>';
html += '<th>缓存文件路径</th>';
html += '<th>文件大小</th>';
html += '<th>修改时间</th>';
html += '<th>MD5</th>';
html += '</tr></thead>';
html += '<tbody>';
data.paths.forEach(function(item) {
html += '<tr>';
html += '<td><code style="word-break: break-all;">' + item.path + '</code></td>';
html += '<td>' + formatBytes(item.size) + '</td>';
html += '<td>' + new Date(item.mtime * 1000).toLocaleString() + '</td>';
html += '<td><code>' + item.md5 + '</code></td>';
html += '</tr>';
});
html += '</tbody></table>';
if (data.has_more) {
html += '<p class="description">显示前 ' + data.limit + ' 个文件,共 ' + data.total_count + ' 个缓存文件。</p>';
} else {
html += '<p class="description">共 ' + data.total_count + ' 个缓存文件。</p>';
}
} else {
html = '<p class="description">没有找到缓存文件。</p>';
}
$content.html(html);
} else {
$content.html('<p class="description">加载失败。</p>');
$msg.html('<div class="notice notice-error"><p>加载缓存路径失败: ' + (response.data || '未知错误') + '</p></div>');
}
},
error: function() {
$content.html('<p class="description">请求失败。</p>');
$msg.html('<div class="notice notice-error"><p>请求失败,请重试</p></div>');
}
});
}
function formatBytes(bytes) {
if (bytes === 0) return '0 B';
var k = 1024;
var sizes = ['B', 'KB', 'MB', 'GB'];
var i = Math.floor(Math.log(bytes) / Math.log(k));
return parseFloat((bytes / Math.pow(k, i)).toFixed(2)) + ' ' + sizes[i];
}
});
</script>
<?php
}
/**
* AJAX: 获取缓存信息
*/
public function ajax_get_cache_info() {
check_ajax_referer("nginx_cache_tools", "nonce");
if (!current_user_can("manage_options")) {
wp_send_json_error("权限不足");
}
$info = $this->get_cache_info();
wp_send_json_success(array(
"accessible" => $info["accessible"],
"size" => $info["size"],
"size_formatted" => $this->format_bytes($info["size"]),
"file_count" => $info["file_count"],
"file_count_formatted" => number_format($info["file_count"]),
));
}
/**
* AJAX: 清除全部缓存
*/
public function ajax_purge_all_cache() {
check_ajax_referer("nginx_cache_tools", "nonce");
if (!current_user_can("manage_options")) {
wp_send_json_error("权限不足");
}
$result = $this->purge_all_cache();
if ($result["success"]) {
wp_send_json_success(array(
"message" => $result["message"],
"deleted_count" => $result["deleted_count"],
));
} else {
wp_send_json_error($result["message"]);
}
}
/**
* AJAX: 获取缓存路径列表
*/
public function ajax_get_cache_paths() {
check_ajax_referer("nginx_cache_tools", "nonce");
if (!current_user_can("manage_options")) {
wp_send_json_error("权限不足");
}
$paths = $this->get_cache_paths();
if (isset($paths['error'])) {
wp_send_json_error($paths['error']);
}
// 数据已经由 Lua 脚本处理,直接返回
wp_send_json_success(array(
"paths" => $paths,
"total_count" => count($paths),
"has_more" => false, // Lua 脚本已经限制了数量
"limit" => 100,
));
}
/**
* 获取缓存信息
*/
private function get_cache_info() {
$info = array(
"accessible" => false,
"size" => 0,
"file_count" => 0,
);
// 检查文件系统访问权限
if ($this->fs_access_cache === null) {
$this->fs_access_cache = $this->check_fs_access();
}
if (!$this->fs_access_cache) {
// 尝试使用 shell 命令获取信息
return $this->get_cache_info_via_shell();
}
// 使用 PHP 文件系统函数
if (@is_dir($this->cache_path)) {
$info["accessible"] = true;
$info["size"] = $this->get_dir_size($this->cache_path);
$info["file_count"] = $this->count_files($this->cache_path);
}
return $info;
}
/**
* 通过 shell 命令获取缓存信息
*/
private function get_cache_info_via_shell() {
$info = array(
"accessible" => false,
"size" => 0,
"file_count" => 0,
);
if (!function_exists("exec")) {
return $info;
}
$disabled_functions = explode(",", ini_get("disable_functions"));
if (in_array("exec", $disabled_functions)) {
return $info;
}
// 获取目录大小
$size_command = sprintf("du -sb %s 2>/dev/null | cut -f1", escapeshellarg($this->cache_path));
@exec($size_command, $size_output, $size_return);
if ($size_return === 0 && !empty($size_output)) {
$info["size"] = intval($size_output[0]);
$info["accessible"] = true;
}
// 获取文件数量
$count_command = sprintf("find %s -type f 2>/dev/null | wc -l", escapeshellarg($this->cache_path));
@exec($count_command, $count_output, $count_return);
if ($count_return === 0 && !empty($count_output)) {
$info["file_count"] = intval(trim($count_output[0]));
}
return $info;
}
/**
* 获取目录大小(递归)
*/
private function get_dir_size($dir) {
$size = 0;
if (!@is_dir($dir)) {
return 0;
}
$files = @scandir($dir);
if ($files === false) {
return 0;
}
foreach ($files as $file) {
if ($file === "." || $file === "..") {
continue;
}
$path = $dir . "/" . $file;
if (@is_file($path)) {
$size += @filesize($path);
} elseif (@is_dir($path)) {
$size += $this->get_dir_size($path);
}
}
return $size;
}
/**
* 统计文件数量(递归)
*/
private function count_files($dir) {
$count = 0;
if (!@is_dir($dir)) {
return 0;
}
$files = @scandir($dir);
if ($files === false) {
return 0;
}
foreach ($files as $file) {
if ($file === "." || $file === "..") {
continue;
}
$path = $dir . "/" . $file;
if (@is_file($path)) {
$count++;
} elseif (@is_dir($path)) {
$count += $this->count_files($path);
}
}
return $count;
}
/**
* 获取示例缓存 MD5
*/
private function get_sample_cache_md5() {
$scheme = "https";
$method = "GET";
$host = $this->get_domains()[0] ?? "example.com";
$path = "/";
$cache_key_string = $scheme . $method . $host . $path;
$md5 = md5($cache_key_string);
$level1 = substr($md5, -1);
$level2 = substr($md5, -3, 2);
$file_path = $this->cache_path . "/" . $level1 . "/" . $level2 . "/" . $md5;
return array(
"key" => $cache_key_string,
"md5" => $md5,
"path" => $file_path,
);
}
/**
* 获取缓存路径列表(通过 Lua 脚本)
*/
private function get_cache_paths() {
// 获取域名列表,使用第一个域名作为 Host
$domains = $this->get_domains();
if (empty($domains)) {
return array("error" => "未配置域名");
}
$host = $domains[0];
$url = "http://127.0.0.1/cache-paths";
$args = array(
"method" => "GET",
"timeout" => 30,
"headers" => array(
"Host" => $host,
),
"sslverify" => false,
"blocking" => true,
);
$response = wp_remote_request($url, $args);
if (is_wp_error($response)) {
return array("error" => "HTTP 请求失败: " . $response->get_error_message());
}
$code = wp_remote_retrieve_response_code($response);
$body = wp_remote_retrieve_body($response);
if ($code != 200) {
return array("error" => "请求失败 (HTTP {$code}): " . $body);
}
$json = json_decode($body, true);
if (!$json || !isset($json["status"]) || $json["status"] != "success") {
return array("error" => "响应格式错误: " . $body);
}
return $json["paths"] ?? array();
}
/**
* 清除全部缓存
*/
private function purge_all_cache() {
$result = array(
"success" => false,
"message" => "",
"deleted_count" => 0,
);
// 优先使用 HTTP 请求调用 Lua 脚本(与现有机制一致,避免权限问题)
$http_result = $this->purge_all_cache_via_http();
if ($http_result["success"]) {
return $http_result;
}
// 如果 HTTP 请求失败,回退到直接文件操作
if ($this->debug) {
$this->log("HTTP 清除全部缓存失败,尝试使用文件系统方式: " . $http_result["message"]);
}
// 检查文件系统访问权限
if ($this->fs_access_cache === null) {
$this->fs_access_cache = $this->check_fs_access();
}
// 使用 shell 命令(更快更可靠)
if (function_exists("exec")) {
$disabled_functions = explode(",", ini_get("disable_functions"));
if (!in_array("exec", $disabled_functions)) {
return $this->purge_all_cache_via_shell();
}
}
// 回退到 PHP 文件系统函数
if ($this->fs_access_cache) {
return $this->purge_all_cache_via_php();
}
$result["message"] = "无法清除缓存,请检查权限或 Nginx 配置";
return $result;
}
/**
* 通过 HTTP 请求调用 Lua 脚本清除全部缓存
*/
private function purge_all_cache_via_http() {
$result = array(
"success" => false,
"message" => "",
"deleted_count" => 0,
);
// 获取域名列表,使用第一个域名作为 Host(清除全部缓存只需要调用一次)
$domains = $this->get_domains();
if (empty($domains)) {
$result["message"] = "未配置域名";
return $result;
}
$host = $domains[0]; // 使用第一个域名
$purge_url = "http://127.0.0.1/purge-all"; // 使用本地回环,避免外部网络开销
$args = array(
"method" => "GET",
"timeout" => 30, // 清除全部缓存可能需要较长时间
"headers" => array(
"Host" => $host,
),
"sslverify" => false,
"blocking" => true, // 必须阻塞,否则无法判断结果
);
$response = wp_remote_request($purge_url, $args);
if (is_wp_error($response)) {
$result["message"] = "HTTP 请求失败: " . $response->get_error_message();
if ($this->debug) {
$this->log(sprintf("HTTP PURGE-ALL 失败: %s - %s", $purge_url, $response->get_error_message()));
}
return $result;
}
$code = wp_remote_retrieve_response_code($response);
$body = wp_remote_retrieve_body($response);
// 解析 JSON 响应
$json = json_decode($body, true);
if ($code == 200 && $json && isset($json["status"]) && $json["status"] == "success") {
$result["success"] = true;
$result["deleted_count"] = isset($json["deleted_count"]) ? intval($json["deleted_count"]) : 0;
$result["message"] = isset($json["message"]) ? $json["message"] : "缓存清除成功";
if ($this->debug) {
$this->log(sprintf("HTTP PURGE-ALL 成功: %s (code=%d, deleted=%d)", $purge_url, $code, $result["deleted_count"]));
}
} else {
$error_msg = isset($json["message"]) ? $json["message"] : "未知错误";
$result["message"] = sprintf("清除失败 (HTTP %d): %s", $code, $error_msg);
if ($this->debug) {
$this->log(sprintf("HTTP PURGE-ALL 失败: %s (code=%d) - %s", $purge_url, $code, $error_msg));
}
}
return $result;
}
/**
* 通过 shell 命令清除全部缓存
*/
private function purge_all_cache_via_shell() {
$result = array(
"success" => false,
"message" => "",
"deleted_count" => 0,
);
// 先统计文件数量
$count_command = sprintf("find %s -type f 2>/dev/null | wc -l", escapeshellarg($this->cache_path));
@exec($count_command, $count_output, $count_return);
$file_count_before = ($count_return === 0 && !empty($count_output)) ? intval(trim($count_output[0])) : 0;
// 删除所有缓存文件
$delete_command = sprintf("find %s -type f -delete 2>/dev/null", escapeshellarg($this->cache_path));
@exec($delete_command, $delete_output, $delete_return);
if ($delete_return === 0) {
$result["success"] = true;
$result["deleted_count"] = $file_count_before;
$result["message"] = sprintf("成功清除 %d 个缓存文件", $file_count_before);
if ($this->debug) {
$this->log(sprintf("清除全部缓存成功,删除 %d 个文件", $file_count_before));
}
} else {
$result["message"] = "清除缓存失败,请检查权限";
}
return $result;
}
/**
* 通过 PHP 文件系统函数清除全部缓存
*/
private function purge_all_cache_via_php() {
$result = array(
"success" => false,
"message" => "",
"deleted_count" => 0,
);
if (!@is_dir($this->cache_path)) {
$result["message"] = "缓存目录不存在";
return $result;
}
$deleted_count = 0;
$this->delete_dir_contents($this->cache_path, $deleted_count);
$result["success"] = true;
$result["deleted_count"] = $deleted_count;
$result["message"] = sprintf("成功清除 %d 个缓存文件", $deleted_count);
if ($this->debug) {
$this->log(sprintf("清除全部缓存成功,删除 %d 个文件", $deleted_count));
}
return $result;
}
/**
* 递归删除目录内容(保留目录结构)
*/
private function delete_dir_contents($dir, &$deleted_count) {
if (!@is_dir($dir)) {
return;
}
$files = @scandir($dir);
if ($files === false) {
return;
}
foreach ($files as $file) {
if ($file === "." || $file === "..") {
continue;
}
$path = $dir . "/" . $file;
if (@is_file($path)) {
if (@unlink($path)) {
$deleted_count++;
}
} elseif (@is_dir($path)) {
$this->delete_dir_contents($path, $deleted_count);
// 删除空目录
@rmdir($path);
}
}
}
/**
* 格式化字节大小
*/
private function format_bytes($bytes, $precision = 2) {
$units = array("B", "KB", "MB", "GB", "TB");
$bytes = max($bytes, 0);
$pow = floor(($bytes ? log($bytes) : 0) / log(1024));
$pow = min($pow, count($units) - 1);
$bytes /= pow(1024, $pow);
return round($bytes, $precision) . " " . $units[$pow];
}
}
// 初始化插件
if (class_exists("Nginx_Cache_Purge_Multi_Domain")) {
new Nginx_Cache_Purge_Multi_Domain();
}
6.重启openresty ,启用wp插件:
systemclt reload openresty
实际效果 快速测试:
系统负载 btop:
lighthouse:
缓存管理:
The post 浅谈WordPress静态化 appeared first on obaby 𝐢𝐧⃝ void.



























