帝国CMS附件对接七牛云:PHP同步删除与重试队列实现
简介这是基于PHP开发的帝国CMS7.5七牛云图片附件同步、上传删除插件专为帝国CMS站点管理员与二次开发人员设计用于实现图片附件自动同步至七牛云存储、快速上传、智能删除及CDN加速分发有效减轻主服务器压力并提升图片访问体验。压缩包共23个文件其中22个为PHP源码文件涵盖图片同步上传、删除检测、API通信、配置管理等模块另附1个使用说明txt便于快速部署。目前已有316人学习下载。插件完整展示了使用file_put_contents、unlink处理文件通过curl与七牛云API交互利用json_decode解析响应并通过try-catch实现异常处理等关键PHP技术。读者可基于源码理解帝国CMS与七牛云存储的集成流程掌握附件同步、删除联动及敏感配置管理的方法适合希望为CMS系统扩展云存储能力、优化站点图片处理效率的开发者参考学习。1. 帝国CMS 附件多了之后磁盘、备份、迁移三座山一起压过来帝国CMS 站点跑到一定规模最先崩的往往不是数据库而是附件目录。图片堆在服务器本地磁盘备份要整盘拷、迁移要整目录传、高并发访问时 Nginx 和 PHP-FPM 全被静态文件拖住。这个基于 PHP 的帝国CMS 7.5 七牛云图片附件同步、上传删除插件解决的就是把附件层搬到对象存储后台传图时同步到七牛删除附件时联动删云端页面访问走 CDN 加速。它最有价值的不是上传那段代码而是“同步删除”和“失败重试”这套一致性机制——很多同类插件只做到把图丢上云本地删了云端残留半年后两边完全对不上账。适合正在跑帝国CMS 7.5、图片量大、准备从 FTP 附件模式切到对象存储的运维和 PHP 开发者。下面按源码实现路径直接拆。2. 插件底层原理PHP 生成上传凭证、签名规则与 key 映射2.1 为什么“同步删除”比“定时清理”可靠七牛云本身没有“和本地目录自动同步”的能力它只认 HTTP API。插件要做的事就是给帝国CMS 的附件操作补上两个事件上传成功后触发远端写入删除被调用时触发远端删除。定时清理方案的问题在于图片可能在文章未发布时上传、在批量替换时变成孤儿文件定时任务很难枚举出“哪些本地文件在云端没有对应对象”。插件采用事件驱动删除动作发生在帝国CMS 执行附件删除的那一刻删远端失败才进重试队列兜底。这个“直删 重试”的结构保证了大部分场景下本地和云端是同一份数据而不是靠定期扫描去弥补。2.2 PHP 生成七牛上传凭证的完整实现七牛上传接口不直接认 AccessKey/SecretKey而是要求客户端携带一个短期有效的 Upload Token。这个 token 的生成逻辑在插件里属于核心工具函数用 PHP 的hash_hmac就能实现?php function base64_urlsafe(string $data): string { return rtrim(strtr(base64_encode($data), /, -_), ); } function qiniu_upload_token( string $accessKey, string $secretKey, string $bucket, string $key, int $expires 3600 ): string { $policy [ scope $bucket . : . $key, // 限定只能传这个 key deadline time() $expires, // 凭证绝对过期时间 insertOnly 1, // 同名文件不覆盖 ]; $encodedPolicy base64_urlsafe(json_encode($policy, JSON_UNESCAPED_SLASHES)); $sign hash_hmac(sha1, $encodedPolicy, $secretKey, true); return $accessKey . : . base64_urlsafe($sign) . : . $encodedPolicy; }核心点在于签名对象是 URL-safe Base64 编码后的策略字符串而不是原始 JSON。json_encode必须加JSON_UNESCAPED_SLASHES否则默认会把/转义成\/导致签名字符串和七牛服务端解码结果不一致出现token auth failed。参数表参数取值示例说明scopemy-bucket:article/2025/06/a.jpg带 key 时只允许上传指定对象只写 bucket 名则允许客户端自定 keydeadline当前时间戳 3600token 过期时间建议不超过 3600 秒insertOnly0 或 11 拒绝覆盖同名对象帝国CMS 替换附件时要设 0expires3600插件内部换算成 deadline 的偏移量2.3 key 映射规则本地文件路径怎么变成云端对象名云端 key 相当于对象存储里的“文件路径”。插件里必须有一个统一的映射函数上传、删除、回源三处都调用它否则就会出现“上传走的是带前缀的 key删除时把前缀丢了”这种低级错误。常见映射规则是取帝国CMS 附件根目录之后的相对路径再拼接插件配置的QINIU_PREFIX。例如本地文件是/www/wwwroot/site/d/file/article/2025/06/a.jpg附件根目录是/www/wwwroot/site/d/file/配置前缀为article那么云端 key 就是article/2025/06/a.jpg。这里要特别注意 Windows 与 Linux 的目录分隔符差异DIRECTORY_SEPARATOR在 Linux 下是/生产环境基本没问题但本地调试如果是 Windows路径替换时要把\统一转成/。3. 核心 PHP 实现cURL 上传、远端删除联动与失败重试队列3.1 cURL 上传用 CURLFile 代替裸 HTTP 流PHP 上传文件到七牛成熟做法是用curl加CURLFile以multipart/form-data方式 POST 给上传域名。不要在 PHP 5 时代的老写法$filePathPHP 7 之后已经不再支持。?php function qiniu_upload_file( string $localFile, string $token, string $key, string $zone ): string { $mime (new finfo(FILEINFO_MIME_TYPE))-file($localFile); $host $zone ? https://upload- . $zone . .qiniup.com : https://upload.qiniup.com; $post [ token $token, key $key, file new CURLFile($localFile, $mime, basename($localFile)), ]; $ch curl_init(); curl_setopt($ch, CURLOPT_URL, $host); curl_setopt($ch, CURLOPT_POST, true); curl_setopt($ch, CURLOPT_POSTFIELDS, $post); curl_setopt($ch, CURLOPT_RETURNTRANSFER, true); curl_setopt($ch, CURLOPT_CONNECTTIMEOUT, 10); curl_setopt($ch, CURLOPT_TIMEOUT, 60); $resp curl_exec($ch); $httpCode curl_getinfo($ch, CURLINFO_HTTP_CODE); curl_close($ch); $result json_decode($resp, true); // 返回数组而不是对象字段缺失时更好判断 if ($httpCode ! 200 || !isset($result[key])) { throw new RuntimeException( 七牛上传失败 HTTP . $httpCode . resp . $resp ); } return $result[key]; }CURLOPT_CONNECTTIMEOUT设置 10 秒是防止七牛域名不可达时 PHP-FPM 进程被长时间挂住CURLOPT_TIMEOUT60 秒对常规图片足够但如果你经常传 5MB 以上的大图建议提到 120。finfo读出的 MIME 只能保证 Content-Type 正确不能替代扩展名白名单校验这一点后面第 5 章会展开。json_decode($resp, true)这里故意返回数组因为七牛出错时返回的 JSON 结构里可能没有key字段用数组方式判断比对象方式少一层“试图读取不存在属性”的报错这也是 PHP 里处理接口返回数据的常见取舍。3.2 删除联动先远端后本地删除失败不进 unlink删除动作的顺序很关键。常见错误是先unlink本地文件再去调七牛 delete结果云端删除失败本地文件已经没了想重试都拿不回原始数据。正确做法是先调七牛管理接口远端删除成功后再删本地。?php function qiniu_delete_file( string $accessKey, string $secretKey, string $bucket, string $key, string $zone z1 ): bool { $encodedKey base64_urlsafe($bucket . : . $key); $entry /delete/ . $encodedKey; $sign hash_hmac(sha1, $entry . \n, $secretKey, true); $encodedSign base64_urlsafe($sign); $auth $accessKey . : . $encodedSign; $ch curl_init(https://rs- . $zone . .qiniu.com . $entry); curl_setopt($ch, CURLOPT_POST, true); curl_setopt($ch, CURLOPT_RETURNTRANSFER, true); curl_setopt($ch, CURLOPT_HTTPHEADER, [Authorization: QBox . $auth]); $resp curl_exec($ch); $httpCode curl_getinfo($ch, CURLINFO_HTTP_CODE); curl_close($ch); return $httpCode 200; }业务调用层组合如下// 先删远端远端成功才删本地避免远端残留后本地不可恢复 $delOk qiniu_delete_file($accessKey, $secretKey, $bucket, $key, $zone); if ($delOk) { if (is_file($localFile)) { unlink($localFile); } } else { retryPush([type delete, key $key, local $localFile]); }删除接口的签名主体是/delete/encodedKey加一个换行符注意hash_hmac的第二个参数必须以\n结尾。rs-z1.qiniu.com对应华北区域华东是rs.qiniu.com华南是rs-z2.qiniu.com如果你的 bucket 不在华北这个域名要跟着换。3.3 PHP 错误处理与重试队列try-catch 不是终点curl_exec本身返回false表示网络层失败HTTP 返回非 200 表示业务层失败这两种情况都要纳入错误处理。插件里用 try-catch 把上传和删除包起来外层捕获RuntimeException后写日志并推入重试队列这是 PHP 错误处理的标准做法。如果服务器上有 Redis用 list 结构就能撑起一个轻量重试队列$payload json_encode([type delete, key $key], JSON_UNESCAPED_SLASHES); $redis-lpush(qiniu:retry, $payload);消费脚本用brpop阻塞取任务失败次数超过 3 次就单独记入死信列表不再死循环重试。你们系统里如果已经引入 Redis Stream也可以平移到消费者组语义但对单个帝国CMS 站点来说list 加一个每分钟跑一次的 cron 脚本已经足够不需要引入消费组那么重的依赖。4. 帝国CMS 7.5 接入e/class/connect.php 的钩子位置与附件地址替换4.1 先定位帝国CMS 的附件处理入口帝国CMS 7.5 的附件上传、删除逻辑集中在/e/class/connect.php。插件 zip 里的使用说明.txt 一般会明确告诉你 patch 哪两个位置附件上传成功后的回调处以及附件删除函数内部。上传处调用qiniu_upload_file删除处先调qiniu_delete_file删除成功再删本地。// e/class/connect.php 中删除附件函数改造示意 function DelFileQ($file) { $key qiniu_local_path_to_key($file); // 与上传时一致的 key 映射 if (qiniu_delete_file($key)) { if (is_file($file)) { unlink($file); } } else { qiniu_retry_push([type delete, file $file, key $key]); } }提示帝国CMS 二开普遍是直接改核心文件patch 前务必把 connect.php 复制一份备份以后帝国CMS 升级时这个文件会被覆盖需要重新打补丁。这段改动的关键不是函数名本身而是“删除顺序”和“失败兜底”。很多二开方案只加一行qiniu_delete($file)就完事完全不判断返回值云端删除失败时本地照样 unlink数据就分叉了。4.2 附件地址前缀与 CDN 域名替换插件接管上传后文件已经进了七牛但帝国CMS 正文里存的图片地址还是本地相对路径。要让页面直接加载 CDN 地址需要改两处。第一处是后台的附件地址前缀把它改成七牛 CDN 域名例如https://img.example.com/。第二处是历史数据文章表里的titlepic、正文中的图片路径需要批量替换UPDATE phome_ecms_news SET titlepic REPLACE(titlepic, https://www.example.com/d/file/, https://img.example.com/);表名前缀phome_以你实际安装为准要做替换前先SELECT出来抽查确认替换前后路径拼接是对的再执行UPDATE。正文里的图片地址分布在不同字段正文内容一般存在newstext字段也要一并处理但要注意不要把已经改成 CDN 域名的地址二次替换成错误结果。4.3 配置管理AccessKey 与 SecretKey 不落库、不进模板插件的配置项建议单独放一个文件避免和帝国CMS 后台配置搅在一起// qiniu_config.php 放在非 web 可访问目录服务端 require 读取 ?php define(QINIU_AK, 你的 AccessKey); define(QINIU_SK, 你的 SecretKey); // 只在服务端读取绝不回显到页面 define(QINIU_BUCKET, my-article-images); define(QINIU_ZONE, z1); // z0 华东 z1 华北 z2 华南 define(QINIU_CDN, https://img.example.com); define(QINIU_PREFIX, article); // 云端 key 统一前缀配置项示例说明QINIU_AK一串 AccessKey用于签名可被 token 机制限制QINIU_SK一串 SecretKey泄漏等于 bucket 沦陷必须文件权限 600QINIU_BUCKETmy-article-images与七牛控制台创建的空间名一致QINIU_ZONEz1决定 upload/rs 使用哪个区域域名QINIU_CDNhttps://img.example.com绑定七牛 CDN 或自定义域名QINIU_PREFIXarticle所有 key 的前缀建议按业务分目录SecretKey 不要写进数据库也不要通过后台表单回显。浏览器里能看到的任何变量都有可能被开发者工具扒出来一旦 SK 泄露等于把整个 bucket 的读写权限交给了对方。5. 部署与排错php 上传漏洞、凭证泄露、本地上传失败与 sitemap 图片地址处理5.1 服务端 key 生成与扩展名校验这类插件最容易出问题的不是七牛接口而是上传入口本身。客户端传什么文件名就用什么文件名是典型的 PHP 上传漏洞来源。插件里必须做两层校验第一层是扩展名白名单第二层是finfo读 MIME 类型。?php function check_image_file(string $fileName, string $tmpFile): bool { $ext strtolower(pathinfo($fileName, PATHINFO_EXTENSION)); if (!in_array($ext, [jpg, jpeg, png, gif, webp], true)) { return false; } $mime (new finfo(FILEINFO_MIME_TYPE))-file($tmpFile); if (!in_array($mime, [image/jpeg, image/png, image/gif, image/webp], true)) { return false; } return true; }同时云端 key 必须由服务端生成不能接受前端传上来的路径。常见做法是date(Y/m) . / . uniqid(, true) . . . $ext这样既避免目录穿越又天然按月份分目录后面做生命周期管理也方便。5.2 凭证安全token 期限、AK 授权范围上传凭证不要给太长有效期600 秒足够完成一次上传。管理凭证的权限要收缩在七牛控制台给这个 AK/SK 只分配目标 bucket 的读写权限不要用主账号的 AK/SK。插件上传凭证的scope最好用bucket:key形式把客户端能上传的对象固定死一旦允许客户端自定 key攻击者就能把任意路径写入你的 bucket。5.3 常见失败对照表部署过程中最容易遇到的四类问题现象原因处理方案上传返回 401 token auth failed服务器时间偏差超过 10 分钟或 token 已过期同步服务器时间token 有效期调到 3600 秒内删除返回 404 no such filekey 映射不一致删除时少了前缀对比上传、删除两处的 key 生成函数本地已删云端还有残余删除失败且未进重试队列检查日志跑一遍第 6 章的一致性脚本页面图片 404CDN 域名前缀配错附件地址没替换干净检查 QINIU_CDN 是否带斜杠历史文章字段是否 UPDATE5.4 sitemap 与伪静态规则图片地址不能留在本地域名帝国CMS 生成 sitemap 时图片地址默认取当前站点域名如果你的站点域名没换成 CDN搜索引擎抓到的图片 URL 还是本地路径。处理办法是改 sitemap 生成脚本里的域名常量或在生成后用正则把/d/file/替换成https://img.example.com/。不建议用 nginx rewrite 把/d/file/全部 301 到七牛。原因是本地历史文件可能没有及时迁移规则一旦生效用户访问一个本地不存在的文件会被 301 到云端也找不到的地址白白增加一次跳转。正确的顺序是先批量迁移历史文件到七牛再改附件前缀最后重新生成 sitemap。6. 进阶qshell 核对云端文件、一致性校验与删除风暴限流6.1 用 qshell 拉取云端全量清单对账qshell 是七牛官方命令行工具。要验证插件到底有没有把每个附件都传上去与其在后台一张张数不如直接拉全量清单和本地目录做 diff。qshell account AccessKey SecretKey AccountName qshell list bucket2 -o cloud_list.txt find d/file -type f | sort local_list.txtbucket2命令导出的文件里第一列是云端 key。本地用find只输出相对路径再统一加article/前缀比对。首次跑这个对比大概率会发现两类问题一是历史图片根本没同步过二是删除操作没有联动云端。这两类文件数量超过预期时说明插件的钩子没有完整覆盖或删除重试队列失效了。6.2 一致性校验脚本的循环骨架发现差异后要用脚本收口。脚本逻辑不用复杂核心就是本地有云端没有时补传本地没有云端有时不处理因为云端多出来的文件可能是历史数据迁移时留下的直接删有风险。?php foreach ($localRows as $local) { $key qiniu_local_path_to_key($local); if (!qiniu_stat_key($key)) { // 云端没有对应对象补传或至少记日志告警 $token qiniu_upload_token($accessKey, $secretKey, $bucket, $key); qiniu_upload_file($local, $token, $key, $zone); } }qiniu_stat_key对应的是七牛的stat接口只查询对象元信息不产生流量费用。补传时重新生成 token不要复用一小时前的旧 token。6.3 删除风暴限流与运维巡检帝国CMS 后台批量删除几十篇文章时删除钩子会被同步调用几十次瞬间打几十个 delete 请求到七牛管理接口。对象存储一般都有频控管理接口的限流阈值比上传接口低所以删除操作必须限速。最廉价的做法是每删一个文件后睡 200 到 300 毫秒usleep(250000); // 把删除速率压到每分钟 200-300 次更规整的方案是把删除任务推进 Redis list由 cron 脚本异步消费消费循环里同样做速率控制。这样后台批量操作不会被七牛接口耗时卡住本地 unlink 也会延迟到云端删成功之后执行。把 qshell list 导出的云端清单和数据库附件表每天比对一次补传失败日志里出现的 key基本能保证本地与云端长期一致。这一条建议直接加进运维巡检脚本。本文还有配套的精品资源点击获取