浅谈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:

缓存管理:

 


You may also like

8 comments

  1. Level 1
    WebView 4.0 WebView 4.0 Android 16 Android 16 cn中国–广东–广州

    太强了,神器openrestry也会,我那时搞自定义负载均衡搞过一下,难度很大,搞到一半项目就没做了

  2. Level 1
    Google Chrome 137.0.7151.115 Google Chrome 137.0.7151.115 Android 16 Android 16 cn中国–四川–成都

    好复杂的感觉,我原来想过直接用脚本定期保存所有页面,实现静态,不知道可不可行

  3. Level 1
    Google Chrome 144.0.0.0 Google Chrome 144.0.0.0 Windows 10 x64 Edition Windows 10 x64 Edition unknownHong Kong–Hong Kong–Hong Kong

    近期在搞个很久前就想搞的站,从wordpress到typecho来回搭建车站了几次,最终还是选择了简单的typecho,WP优点多缺点也多,但对于我这种没技术的还是ty省心些。

发表回复

您的邮箱地址不会被公开。 必填项已用 * 标注