本文由 FlintHub 开发文档归档发布。原始文档日期见标题前缀,来源:8月13日备份/SoSite-插件仓库开发计划.md。
SoSite 插件仓库开发计划
文档日期:2026-08-09
目标程序:SoSite V5.0.0(自研 MVC,PHP 8.2 / MySQL 5.7-8.0,零 Composer 依赖)
前置依据:SoSite-三功能落地方案-在线升级-插件仓库-定时任务.md(§2.2 插件仓库机制拆解、§7 P1 插件仓库、§8 风险与安全措施)
参考对象:bbs1.org 插件市场(远程 JSON 源 + sha256 强校验 + 安装后默认停用 + 注册表同步)
说明:本文为插件仓库功能的完整开发计划(设计 → 实现 → 测试 → 发布全流程),落地前需按实际代码结构复核函数签名与路由注册方式。
一、背景与目标
1.1 现状
| 项 | 现状 |
|---|
| 插件安装 | 仅本地手动放置 plugins/{dir}/ + 后台启停;无市场、无远程安装 |
| 插件来源 | 手工复制/上传 zip 解压,无来源校验、无版本管理 |
| 插件更新 | 手动覆盖文件,无差异检测、无备份回滚 |
| 插件卸载 | 后台卸载(调 Plugin.php::uninstall() + 删注册),但插件文件目录保留 |
1.2 目标
- 后台新增插件市场页:浏览 / 搜索 / 一键安装 / 更新 / 卸载 / 重新安装
- 支持远程市场源(自建 HTTP 源或 Gitee Release),feed JSON 协议下发插件元数据与安装包
- 全程安全路径:仅管理员 + CSRF + sha256 强校验 + 解压路径白名单 + 安装后默认停用 + 备份回滚 + AuditLog 日志
- 不改变现有 14 个插件的目录结构、plugin.json 格式、生命周期机制(向后兼容)
- 为后续"系统插件"(
system 标志防停用失去管理入口)预留字段
二、现状分析(必须遵守的既有约束)
2.1 插件系统现状(2026-08-09 核对)
| 项 | 现状 |
|---|
| 插件总数 | 14 个(announcements / content_review / daily_checkin / dice / edit_info / friend_links / icon_generator / invite / post_favorite / qa / quiz_duel / red_packet / seo / user_profile),11 启用 / 3 停用 |
| 目录结构 | plugins/{dir}/:plugin.json + 可选 Plugin.php + hook/.php + views/.php + assets/* |
| 清单文件 | plugin.json:name / version / author / description / activated / permissions / hooks |
| 生命周期 | Plugin.php::activate(): bool(false 回滚)/ deactivate() / uninstall() |
| 缓存 | plugins/plugins_cache.php(var_export 生成,json_decode 读取;原子写入 tmp+rename + opcache 失效) |
| 命名空间 | Plugin\{Dir}(Autoloader 映射),类文件 {Xxx}Controller.php |
| 视图 | A+ 规则:view('plugins/xxx/yyy') → plugins/xxx/views/yyy.php(resolveTemplate 插件目录优先,回落 app/Views) |
| 资源 | 直接 /plugins/{dir}/assets/...(勿用 asset(),避免 assets/plugins/ 双份漂移) |
| 后台管理 | app/Controllers/Admin/PluginController.php(列表/启用/禁用/编辑配置/卸载)+ app/Views/admin/plugins.php |
| 路由 | hook route_register(前台)/ admin_route_register(后台),$router->get/post |
2.2 实现前必须复核的接口
app/Helpers/Plugin.php 的 loadPlugins() / saveCache() / refresh()(缓存重建挂载点)Settings::get/update(存放市场地址、更新源地址、最后检查时间)AuditLog::log($action, $targetType, $targetId, $detail)(安装/卸载审计)Csrf::token()/verifyOrDie()(后台表单)- Router 后台路由注册(
admin_route_register hook,URL 不带 /admin 前缀) Admin\BaseController 鉴权(isAdmin + 二次密码验证)- PHP 是否启用
zip 扩展(phpstudy 默认开启;未开启需 extension=php_zip.dll) protected/ 目录可写性(备份目录)
三、总体架构
┌─────────────────────────┐ ┌────────────────────────────────┐
│ 市场服务端(发布方) │ HTTP │ SoSite 站点(客户端) │
│ feed.json(元数据) │ ──────► │ 后台"插件市场"页 │
│ files/{plugin}-{ver}.zip│ GET │ PluginMarket.php(安装器) │
│ 发布脚本(打包+sha256) │ │ 校验→下载→解压→备份→落盘→停用 │
└─────────────────────────┘ └────────────────────────────────┘
3.1 市场源(发布方)
- 静态托管即可:
feed.json + files/{plugin_id}-{version}.zip - 可放自建服务器 / Gitee Release / 对象存储(需 HTTPS + 证书)
- 不含插件源码明文:zip 内是完整插件目录(代码 + plugin.json + hook + views + assets)
3.2 站点(客户端)
- 新增
app/Helpers/PluginMarket.php(核心安装器) - 新增
app/Controllers/Admin/PluginMarketController.php(后台路由) - 新增
app/Views/admin/plugin_market.php(市场页) - 新增
protected/backups/plugins/(安装/升级备份目录) - 新增
protected/plugins-market-state.json(市场缓存 + 已安装元数据,运行时生成)
四、市场协议设计(feed.json 规范)
4.1 feed.json 结构
{
"schema": 1,
"generated_at": "2026-08-09T00:00:00+08:00",
"source": "https://market.example.com/",
"plugins": [
{
"id": "daily_checkin",
"name": "每日签到",
"version": "1.0.0",
"author": "SoSite",
"description": "用户每日签到获取积分",
"homepage": "https://example.com/plugin/daily_checkin",
"license": "MIT",
"tags": ["签到", "积分"],
"category": "utility",
"system": false,
"requires": {
"sosite": ">=5.0.0",
"php": ">=8.0",
"plugins": []
},
"install": {
"archive": "files/daily_checkin-1.0.0.zip",
"sha256": "a1b2c3...64位hex",
"size": 20480,
"files": 12
},
"updated_at": "2026-08-09T00:00:00+08:00",
"changelog": "v1.0.0 首次发布"
}
]
}
4.2 字段说明
| 字段 | 必填 | 说明 |
|---|
schema | ✅ | 协议版本号,客户端不兼容则拒绝 |
generated_at | ✅ | 生成时间,客户端缓存判断用 |
source | ✅ | 源地址,客户端校验同源(防中间人篡改 feed 指向) |
plugins[].id | ✅ | 插件目录名(小写下划线,与 plugins/{id}/ 一致) |
plugins[].version | ✅ | 语义化版本 X.Y.Z |
plugins[].system | ❌ | 系统插件标志(预留:true 时禁止停用/卸载) |
plugins[].requires.sosite | ❌ | SoSite 版本约束(>=5.0.0,version_compare 校验) |
plugins[].requires.php | ❌ | PHP 版本约束 |
plugins[].requires.plugins | ❌ | 依赖的其他插件 id 列表(安装前检查已启用) |
plugins[].install.archive | ✅ | zip 相对路径(相对 source) |
plugins[].install.sha256 | ✅ | zip 文件 sha256(强校验) |
plugins[].install.size | ❌ | 文件大小(下载前预检) |
4.3 客户端拉取
- 后台"插件市场"页首次打开 → 拉取 feed.json → 缓存到
protected/plugins-market-state.json(含拉取时间) - 缓存 TTL 默认 5 分钟,过期自动刷新;刷新失败回退旧缓存(页面提示"缓存已过期")
- feed.json 拉取用 cURL(
CURLOPT_SSL_VERIFYPEER=true 正规证书,不关 SSL 校验)
五、客户端模块设计:PluginMarket.php
5.1 方法清单
namespace app\Helpers;
class PluginMarket
{
// —— 市场源 ——
public static function getSourceUrl(): string; // 读 Settings 市场地址
public static function fetchFeed(bool $force = false): array; // 拉取+缓存 feed
public static function getCachedFeed(): array; // 读缓存(失败回退)
public static function clearCache(): void; // 清市场缓存
// —— 安装/更新/卸载 ——
public static function install(string $pluginId): array; // 安装(核心)
public static function update(string $pluginId): array; // 更新(= 安装到已存在目录)
public static function uninstall(string $pluginId): array; // 卸载(调 Plugin::uninstall + 删目录)
public static function reinstall(string $pluginId): array; // 重新安装(备份→删→装)
// —— 校验 ——
public static function validateFeedItem(array $item): bool; // 元数据白名单校验
public static function checkRequirements(array $item): array; // 版本/依赖检查
public static function verifyZip(string $zipPath, string $sha256): bool; // sha256 校验
// —— 解压安全 ——
public static function extractZip(string $zipPath, string $dest): array; // 安全解压
// —— 备份 ——
public static function backupPlugin(string $pluginId): ?string; // 备份到 protected/backups/plugins/
// —— 辅助 ——
public static function compareVersions(string $remote, string $local): int; // version_compare
public static function log(string $action, string $pluginId, string $detail): void; // AuditLog
}
5.2 核心安装流程(install 详细步骤)
1. 后台 POST /admin/plugin-market/install(CSRF 校验 + BaseController 鉴权)
2. fetchFeed() 拉取/读取缓存,按 id 找插件项;不存在 → 返回错误
3. validateFeedItem():字段白名单、sha256 格式、archive 相对路径(拒绝绝对路径/../)
4. checkRequirements():sosite/php/依赖插件版本检查,不满足 → 返回错误(不下载)
5. 下载 zip → protected/tmp/(file_get_contents/cURL,超时 60s,校验 size 上限如 20MB)
6. sha256_file(zip) 与 feed 的 sha256 比对;不一致 → 删除临时 zip,返回错误
7. 并发锁:flock(protected/plugins-market.lock, LOCK_EX|LOCK_NB),占用中 → "正在处理其他安装"
8. 解压安全:逐 entry 校验落点(见 5.3),解压到 protected/tmp/{pluginId}-{rand}/
9. 校验解压结果:plugin.json 存在且 id/name 与 feed 一致;目录名必须 = pluginId
10. 备份现有插件(如已存在):copy 到 protected/backups/plugins/{pluginId}-{时间戳}/
11. 落盘:rmdir 旧目录(如存在)→ 原子移入 plugins/{pluginId}/
12. opcache_invalidate(全部新文件, true)
13. Plugin::refresh() 重建插件缓存
14. 写市场状态:本地记录 {id, version, sha256, installed_at, source}
15. AuditLog::log('plugin_install', 'plugin', 0, "安装 {name} v{version}")
16. 释放锁;删除临时文件
17. 返回成功(提示"已安装,默认停用,请到插件管理启用")
5.3 解压安全(zip 路径穿越防护)
| 校验 | 规则 |
|---|
| entry 路径 | 拒绝含 ../、绝对路径(/ 开头、盘符)的 entry |
| 符号链接 | 拒绝 entry 为 symlink(ZipArchive::STAT_NAME 类型判断) |
| 落点校验 | 逐 entry realpath 后必须落在目标目录内(strpos(real, dest) === 0) |
| 大小上限 | 单 entry ≤ 20MB,总解压 ≤ 50MB(防 zip 炸弹) |
| 条目上限 | ≤ 500 个文件 |
| 压缩比 | 解压后大小/压缩后大小 ≤ 100(防 zip bomb) |
| 顶层目录 | 强制插件内容以 plugins/{id}/ 为根(zip 内可带或不带顶层目录,归一化处理) |
5.4 失败回滚
- 任何步骤失败:若已备份旧版本 → 恢复备份(rmdir 新 → 移回备份);未落盘前失败直接清理临时文件
- 备份保留:
protected/backups/plugins/ 保留最近 3 份(旧备份自动清理),供手动恢复 - 回滚后
Plugin::refresh() 重建缓存,保证插件状态一致
六、后台管理页设计
6.1 路由
// hook/admin_route_register.php(新增插件市场钩子,或并入核心)
$router->get('/admin/plugin-market', ['\app\Controllers\Admin\PluginMarketController', 'index']);
$router->post('/admin/plugin-market/install', ['\app\Controllers\Admin\PluginMarketController', 'install']);
$router->post('/admin/plugin-market/update', ['\app\Controllers\Admin\PluginMarketController', 'update']);
$router->post('/admin/plugin-market/uninstall',['\app\Controllers\Admin\PluginMarketController', 'uninstall']);
$router->post('/admin/plugin-market/reinstall',['\app\Controllers\Admin\PluginMarketController', 'reinstall']);
$router->post('/admin/plugin-market/refresh', ['\app\Controllers\Admin\PluginMarketController', 'refresh']);
6.2 市场页布局(app/Views/admin/plugin_market.php)
┌─ 插件市场 ─────────────────────────────────────────────┐
│ [市场地址: https://...] [刷新] [搜索框] [分类筛选] │
├───────────────────────────────────────────────────────┤
│ 插件卡片列表(每项): │
│ 名称 / 版本 / 作者 / 描述 / 标签 / 分类 │
│ [安装] [更新] [重新安装] [卸载] ← 按本地状态动态显示 │
│ 本地状态:未安装 / 已装 v1.0.0(停用) / 已装并启用 │
│ 可更新提示:远端 v1.1.0 > 本地 v1.0.0(绿色徽标) │
│ sha256 摘要(前 8 位展示,点击展开完整值) │
└───────────────────────────────────────────────────────┘
6.3 状态判断逻辑
| 本地状态 | 按钮显示 |
|---|
| 未安装 | [安装] |
| 已安装(本地版本 < 远端) | [更新] [重新安装] [卸载] |
| 已安装(本地版本 = 远端) | [重新安装] [卸载] |
| 已安装且 system=true | [更新] 仅(禁停用/禁卸载,预留) |
- 本地版本读取:
Plugin::getPlugin($id)['version'](缓存) - 已安装元数据(sha256/来源/时间)读
protected/plugins-market-state.json 中的 installed 段
6.4 交互安全
- 所有 POST 带 CSRF;按钮点击后 confirm 提示(卸载/重新安装二次确认)
- 安装/更新/卸载均为同步阻塞执行(插件规模小、秒级完成),页面顶部提示"处理中请勿关闭"
- 操作结果 flash 提示(成功/失败原因)
七、发布脚本(发布方工具)
7.1 打包脚本 tools/plugin-publish.php(发布方运行,不进站点)
用法:php plugin-publish.php [--source https://market.example.com/]
步骤:
1. 读取 plugins/{id}/plugin.json,校验 id/name/version 与参数一致
2. 校验目录结构(plugin.json 必含 name;views/assets/hook 存在性提示)
3. 生成 zip:仅打包插件目录内容(排除 .git/README 可选保留)
4. 计算 sha256 + size + 文件数
5. 更新 feed.json(按 id 合并/替换该插件条目,保留其他插件)
6. 输出:feed.json + files/{id}-{version}.zip + 校验信息
7.2 发布流程(人工)
改插件代码 → 提交版本号到 plugin.json → 跑发布脚本
→ 产出 zip + 更新 feed.json
→ 上传 feed.json 与 zip 到市场源(保持目录结构)
→ 站点后台"刷新"拉取新 feed → 显示可更新
7.3 市场源目录规范
market/
├── feed.json
└── files/
├── daily_checkin-1.0.0.zip
└── seo-1.2.0.zip
八、安全设计(全部强制)
| 风险 | 措施 |
|---|
| 未授权操作 | 仅管理员:Admin\BaseController 鉴权 + 全部 POST Csrf::verifyOrDie |
| 恶意/被篡改安装包 | sha256 强校验(下载后 hash_file 与 feed 比对,不符即弃) |
| feed 被篡改 | HTTPS + 证书校验;source 同源校验;schema 版本校验 |
| zip 解压路径穿越 | 逐 entry 落点校验、拒绝 ..//绝对路径/符号链接/zip 炸弹(见 5.3) |
| 未审查代码立即执行 | 安装后默认停用(落盘后 plugin.json activated:false,须后台手动启用) |
| 安装失败 | 全流程备份 + 失败自动回滚 + 备份保留 3 份 |
| 恶意插件删库 | 安装/卸载全程 AuditLog 审计;卸载时由插件自身 uninstall() 负责删表(框架不越权删数据) |
| 并发安装 | flock 互斥锁,重复触发拒绝 |
| 临时文件残留 | 安装结束清理 protected/tmp/ 临时 zip/解压目录 |
| 大文件/流量 | 下载前 size 预检(≤20MB)、超时 60s、失败中止 |
| 版本错配 | requires.sosite/php version_compare 校验;依赖插件检查 |
九、边界情况与错误处理(穷举)
9.1 网络与源
| 情况 | 处理 |
|---|
| feed.json 拉取失败(DNS/超时/5xx) | 回退缓存;无缓存 → 页面提示"市场不可达",不崩溃 |
| feed.json 格式非法(JSON 解析失败/schema 不符) | 拒绝使用,保留旧缓存,提示"市场数据异常" |
| 下载 zip 超时/中断 | 删除临时文件,返回"下载失败,请重试" |
| zip 下载后 sha256 不符 | 删临时文件,返回"校验失败,安装包可能被篡改"(AuditLog 记录告警) |
| 市场源 HTTP(非 HTTPS) | 允许但页面黄条提示"非加密源,建议 HTTPS"(不强制) |
| 证书校验失败 | 视为不可达,提示检查证书 |
9.2 插件安装/更新
| 情况 | 处理 |
|---|
| 插件 id 在本地已存在(重复安装) | 提示"已安装,如需覆盖请用更新/重新安装";或 install 检测到存在自动转 update(二选一,默认提示) |
| 插件 id 与 zip 内 plugin.json 的 id 不一致 | 拒绝(防伪装包) |
| zip 解压后缺 plugin.json / 无 name 字段 | 拒绝,清理临时目录 |
| zip 内容不是单插件(多目录/顶层混乱) | 归一化失败 → 拒绝 |
| 安装磁盘空间不足 | file_put_contents 失败 → 回滚 + 提示空间不足 |
| 插件目录已存在且被占用(Windows 文件锁) | 备份/替换失败 → 提示"目录被占用,稍后重试"(Windows 特有) |
plugins_cache.php 写入失败(目录只读) | 安装回滚 + 提示"插件目录不可写" |
安装后启用时 activate() 返回 false | 保持停用,提示"安装成功但启用失败:原因"(插件自身错误) |
| 更新时本地插件有本地修改 | 更新前自动备份;页面提示"更新将覆盖本地修改,备份已保留在 protected/backups/plugins/" |
9.3 依赖与版本
| 情况 | 处理 |
|---|
SoSite 版本不满足 requires.sosite | 安装前拒绝,提示所需版本 |
PHP 版本不满足 requires.php | 安装前拒绝 |
| 依赖插件未安装/未启用 | 安装前检查 requires.plugins,缺失则提示先装依赖 |
| 依赖循环(A 依赖 B,B 依赖 A) | feed 校验时检测环,拒绝 |
| 远端版本 < 本地版本(降级) | 市场页不显示"更新",仅显示"重新安装"(提示版本低于本地) |
9.4 卸载
| 情况 | 处理 |
|---|
| 卸载已启用插件 | 先 Plugin::deactivate() 再卸载;插件 uninstall() 清数据 |
插件 uninstall() 抛异常 | 捕获记录,继续删注册(文件保留,提示数据清理失败) |
| 卸载 system 插件 | 拒绝(预留 system 标志) |
| 卸载后文件残留 | 提示用户手动删除 plugins/{id}/(或提供"删除文件"二次确认选项) |
| 卸载正在被其他插件依赖的插件 | 检查 requires.plugins 反向依赖,提示先卸载依赖方 |
9.5 并发与一致性
| 情况 | 处理 |
|---|
| 并发安装(两个管理员同时操作) | flock 互斥锁,第二个直接拒绝 |
| 安装过程中页面刷新 | 同步阻塞 + 结果 flash;刷新后按实际状态显示(幂等判断) |
| 安装后缓存与磁盘不一致 | 每步操作后 Plugin::refresh() 强制重建;异常路径也刷 |
十、测试计划
10.1 单元/脚本测试(本地)
| 测试项 | 方法 |
|---|
| feed 解析 | 构造合法/非法 feed JSON → validateFeedItem / fetchFeed |
| sha256 校验 | 正确/篡改 zip → verifyZip 结果 |
| 解压安全 | 构造含 ../、绝对路径、symlink、超大文件、超多条目的 zip → extractZip 全部拒绝 |
| 版本比较 | version_compare 边界(1.0.0 vs 1.0.1、1.1 vs 1.10) |
| 依赖检查 | requires 满足/不满足/依赖缺失/循环依赖 |
10.2 集成测试(真实站点)
| 场景 | 步骤 | 预期 |
|---|
| 全新安装 | 发布一个测试插件 zip → 市场页安装 | 落盘 plugins/{id}/,activated=false,缓存刷新,AuditLog 记录 |
| 安装后启用 | 后台插件管理启用 | 插件功能可用 |
| 更新 | 发布 v1.0.1 → 市场页更新 | 备份旧版,新版本生效,缓存刷新 |
| 回滚验证 | 构造一个激活时抛异常的插件 | 安装/更新失败自动回滚到备份 |
| 卸载 | 卸载测试插件 | 数据表删除(插件 uninstall 内)、注册移除 |
| 并发 | 两个请求同时安装 | 第二个被锁拒绝 |
| 权限 | 非管理员访问安装接口 | 重定向登录/拒绝 |
| CSRF | 无 token 提交安装 | verifyOrDie 终止 |
| 路径穿越 | 恶意 zip | 解压被拒,无文件落盘 |
| 停用插件更新 | 更新已停用插件 | 更新后保持停用 |
10.3 回归
- 现有 14 个插件逐个启用/访问主页面,确认 A+ 模板解析、资源加载、hooks 正常(插件仓库不改变任何现有机制)
- 安装新插件后
php -l 全部新文件 + 页面访问 + 后台启用
十一、分期实施计划
| 阶段 | 内容 | 涉及文件 | 预估 |
|---|
| P0 基础 | 市场协议文档定稿 + PluginMarket.php 核心(fetchFeed/校验/下载/sha256/解压/落盘/备份回滚)+ flock 锁 + AuditLog | app/Helpers/PluginMarket.php、发布脚本 | 1.5~2 天 |
| P1 后台 | PluginMarketController + 市场页视图(列表/搜索/分类/状态/按钮)+ 路由 hook + Settings 市场地址 | 控制器、视图、hook、settings 配置 | 1 天 |
| P2 安全加固 | 解压路径穿越全套校验、zip 炸弹防护、依赖/版本检查、system 标志预留、反向依赖检查 | PluginMarket.php 扩展 | 1 天 |
| P3 发布侧 | 发布脚本完善(打包/feed 更新/校验输出)+ 示例测试插件 + 市场源部署文档 | tools/plugin-publish.php、示例 | 0.5~1 天 |
| P4 测试收尾 | 10.x 测试计划全部执行 + 回归 14 插件 + 部署文档(市场地址配置、HTTPS 建议) | 文档、修复 | 1 天 |
合计约 5~6 天(单人)。与在线升级(P2)相互独立,本计划为插件仓库专项。
十二、部署与配置
12.1 站点侧配置
- 后台"设置 → 插件"新增字段:市场地址(
Settings::set('plugin_market_url', 'https://market.example.com/feed.json')) - 默认空 = 市场页提示"未配置市场源"
- 可选:市场检查间隔(默认 5 分钟缓存)
12.2 服务器要求
- PHP 需启用
zip 扩展(phpstudy 默认开;php -m | grep zip 验证) plugins/、protected/ 目录需可写(安装/备份)- 出站 HTTP/HTTPS 可达市场源
- 建议:Nginx 下
protected/ 已配置 deny all(交接文档 P3 待办,若未配需补 location ~ ^/protected/ { deny all; },否则备份 zip 可被下载)
12.3 与现有 update 包的关系
- 插件仓库是独立能力,不替代 update 手动包(那是核心程序升级通道)
- 插件市场分发包保持现有 manifest 格式(plugin.json 结构不变),14 个现有插件可直接作为首批市场内容发布
十三、附录
13.1 关键新增文件总览
app/Helpers/PluginMarket.php # 插件市场安装器(核心)
app/Controllers/Admin/PluginMarketController.php
app/Views/admin/plugin_market.php # 后台市场页
app/Views/admin/_sidebar.php # 侧边栏入口(复用 admin_sidebar_links hook)
tools/plugin-publish.php # 发布脚本(发布方工具,不入站点)
protected/plugins-market-state.json # 市场缓存 + 已安装元数据(运行时生成)
protected/backups/plugins/ # 安装/升级备份目录
protected/plugins-market.lock # 并发锁文件
13.2 Settings 新增键
| 键 | 默认 | 用途 |
|---|
plugin_market_url | '' | 市场 feed 地址 |
plugin_market_last_check | 0 | 上次拉取时间(缓存 TTL) |
13.3 待复核清单(实现前)
- [ ]
zip 扩展确认 - [ ] Router admin_route_register 挂载点
- [ ]
Settings::get/update 签名 - [ ]
AuditLog::log 签名($action/$targetType/$targetId/$detail) - [ ]
Plugin::refresh() 与 plugins_cache.php 结构 - [ ]
protected/ Nginx 访问隔离 - [ ] Windows 文件锁行为(目录占用时的备份/替换)