| 项 | 值 |
|---|---|
| 版本 | V1.0.22(version.php,末位数字 22 = 目录号 22) |
| 主题 | A 线 · 附件体系改造(方案 v3):① 上传/读取统一收敛层(Upload 单一 API,视图入口下沉到 TemplateCompiler);② 全类型按月分目录(YYYY-MM/,头像 seed_avatars/YYYY-MM/、封面 covers/YYYY-MM/);③ 缩略图双档(thumb_ 400×300 / md_ 800×600);④ 附件卡公共组件(2 处渲染 → 1 份代码);⑤ 灯箱重写(全页连读 + 预载 + 键盘/滑动 + 缩放);⑥ 非图片附件下载计数(attachments.download_count)B 线 · 列表与正文图展示改造:⑦ 列表摘要图改读缩略图(省流量);⑧ 摘要图网格规范化(每行 3 个、单图与多图同尺寸、超出不显示);⑨ 正文图渲染期分组(P8-D:块内纯图 → 网格,混排 → 就地包容器);⑩ 灯箱右上角关闭按钮点击无反应(已定位根因) 顺带修正:⑪ 正文图 md_ 回填(CLI);⑫ PNG 缩略图无损编码优化;⑬ modern.css 未定义的 var(--mn-bg-hover)(3 处 hover 静默失效) |
| 涉及静态资源 | 是(assets/css/modern.css 修改、assets/js/lightbox.js 新增、assets/js/thread.js 修改),但本包不含 sw.js —— 自 update/20 起 CACHE_VERSION 已改为由站点版本号派生(app/Views/layouts/main.php 注册时带 sw.js?v=<FLINTHUB_VERSION>,sw.js 从自身 URL 解析该参数拼出缓存名)。本包把 version.php 升到 V1.0.22 → 缓存名变为 flinthub-static-v1.0.22 ≠ 旧名 → SW 更新 + activate 清理旧缓存 → 老访客自动重新拉取新 CSS/JS。若站点此前停在 V1.0.8 及更早,升级后缓存名同样会变,无需任何手工操作 |
| 数据库结构变更 | 有 1 项,走 ensureColumnPatches 自动补齐、无迁移脚本:attachments 增 download_count INTEGER NOT NULL DEFAULT 0 列。存量库首次请求自动 ALTER TABLE 补列(并发重复 ALTER 已捕获忽略),补列清单哈希变化 → 标记文件重算 → 自动重跑一次。无索引变更 |
| 业务数据变更 | 本包不含数据脚本,不需要传数据库。包内提供 2 个 CLI 工具供本地维护用(见 §5.5):cli/migrate_uploads_by_month.php(旧上传文件按月迁移,本次不需要跑)、cli/backfill_md_thumbs.php(正文图 md_ 回填,已在本地跑完)。⚠️ 服务器是 IIS、无命令行,不要在服务器上找命令行 |
| 部署关键点 | 覆盖 46 个代码文件 + 根目录 version.php(连同本说明共 48 个文件),另需上传 9 个 md_ 缩略图(assets/uploads/2026-09/md_*.{png,jpg},服务器无命令行故在本地生成好;新增文件,不覆盖任何东西)。本包不含 config.php(安装生成物、含站点本地参数)。无配置项、无需访问升级 URL、不需要传 data/。升级后 FLINTHUB_VERSION = V1.0.22。唯一手工步骤 = 覆盖代码 + 传 9 个图 + 后台「清理缓存」(见 §5.1 / §5.2) |
一、变更文件清单
一之一、A 线 · 附件体系改造(21 个文件:3 新增 / 18 修改)
| # | 文件 | 变更类型 | 改动要点 |
|---|---|---|---|
| 1 | app/Helpers/Upload.php |
修改(+504 / -18) | 统一收敛层:url() / path() / absPath() / thumbUrl() / formatSize() / makeRelPath();file() 分月落盘;makeThumbnail() 双档(thumb_ / md_);新增 writePng() / pngHasTransparency() 无损编码出口 |
| 2 | app/Core/TemplateCompiler.php |
修改(+72) | 新增视图入口 uploadUrl() / uploadThumb(),视图层不再自己拼路径 |
| 3 | app/Controllers/AttachmentController.php |
修改(162 行) | 新增 ?s=thumb 直出缩略图分支;分级 Cache-Control(private, max-age=604800, immutable);非图片附件 download_count 单条原子自增;路径解析走 Upload::path() |
| 4 | app/Controllers/ApiController.php |
修改(+12 / -6) | /api/upload、/api/fetch-image 走分月落盘;正文图上传带 ['thumb' => 'md_'] |
| 5 | app/Controllers/BlogController.php |
修改(+5 / -4) | 博客封面存取走统一路径解析(covers/YYYY-MM/) |
| 6 | app/Controllers/ProfileController.php |
修改(+14 / -3) | 头像分月落盘(users.avatar 存 YYYY-MM/xxx.png) |
| 7 | app/Controllers/AuthController.php |
修改(+1) | 注册流程头像分月口径注释(取当前月 = 注册月) |
| 8 | app/Helpers/Settings.php |
修改(+142 / -48) | handleAttachments() / deleteAttachment() / cleanupOrphanAttachments() 适配分月目录与新 API |
| 9 | app/Helpers/CoverTemplate.php |
修改(364 行) | 封面路径收敛到 covers/YYYY-MM/ |
| 10 | app/Models/User.php |
修改(+22 / -13) | defaultAvatarPath() 新增可选 $createdAt,路径改 seed_avatars/YYYY-MM/;级联删附件走 path() |
| 11 | app/Models/Thread.php |
修改(+33 / -16) | 附件删除白名单放行;路径解析走 path() |
| 12 | app/SplitDB/Schema.php |
修改(+7) | $columnPatches 增 attachments.download_count(CREATE TABLE 与补丁清单同改) |
| 13 | app/Views/_components/attachment_card.php |
新增(99 行) | 附件卡公共组件(图片卡 / 文件卡),替代原先 2 处各自实现 |
| 14 | app/Views/_components/image_lightbox.php |
新增(25 行) | 灯箱公共组件(容器 + 左右箭头 + 计数器) |
| 15 | assets/js/lightbox.js |
新增(361 行) | 灯箱:全页连读(按 DOM 顺序收集 [data-fullurl])、预载相邻图、键盘 ←→、移动端滑动、PC 滚轮缩放、双指捏合、拖拽平移、Esc/遮罩/×/点击图片关闭;导出 window.viewImage() / window.closeLightbox() 兼容既有内联 onclick |
| 16 | assets/js/thread.js |
修改(48 行) | 移除旧灯箱逻辑,保留引用 / 附件 / 折叠 |
| 17 | cli/migrate_uploads_by_month.php |
新增(618 行) | 旧上传文件一次性按月迁移(含备份、干跑、逐处路径改写、日志) |
| 18 | cli/seed_content.php |
修改(919 行) | writeAvatar() 同步写到 seed_avatars/YYYY-MM/ |
| 19 | plugins/content_review/Plugin.php |
修改(531 行) | 统一路径解析;内容审核的附件识别改走收敛层 |
| 20 | plugins/points_mall/FrontController.php |
修改(235 行) | 统一路径解析 |
| 21 | plugins/dice/hook/right_sidebar_after.php |
修改(68 行) | 头像走 Upload::url(),不再自行拼接 |
一之二、B 线 · 列表与正文图展示改造(26 个文件:2 新增 / 24 修改)
| # | 文件 | 变更类型 | 改动要点 |
|---|---|---|---|
| 22 | app/Views/forum/index.php |
修改(237 行) | 摘要图改读缩略图(thumb_,缺失回退原图);头像收敛;摘要图网格 |
| 23 | app/Views/forum/category.php |
修改(159 行) | 同上 |
| 24 | app/Views/home/index_default.php |
修改(+7 / -7) | 摘要图 + 博客封面走 uploadThumb() / uploadUrl() |
| 25 | app/Views/blog/index.php |
修改(+2 / -2) | 封面 + 头像收敛 |
| 26 | app/Views/blog/detail.php |
修改(+5 / -17) | 封面 + 评论头像;灯箱容器改 include 公共组件 |
| 27 | app/Views/blog/edit.php |
修改(+1 / -1) | 当前封面预览走统一入口 |
| 28 | app/Views/thread/show.php |
修改(+37 / -38) | 附件区改用附件卡组件;灯箱容器改 include;头像收敛 |
| 29 | app/Views/thread/edit.php |
修改(118 行) | 下载直链改 /attachment/{id} |
| 30 | app/Views/post/edit.php |
修改(54 行) | 下载直链改 /attachment/{id} |
| 31 | app/Views/profile/index.php |
修改(107 行) | 头像收敛(原缺少前导 /,已修正) |
| 32 | app/Views/search/index.php |
修改(+1 / -1) | 头像收敛 |
| 33 | app/Views/tags/show.php |
修改(57 行) | 头像收敛 |
| 34 | app/Views/_components/right_sidebar.php |
修改(+2 / -2) | 头像 ×2 收敛 |
| 35 | plugins/dice/views/index.php |
修改(117 行) | 头像收敛 |
| 36 | plugins/floor_reply/views/list.php |
修改(109 行) | 头像收敛 |
| 37 | plugins/quiz_duel/views/index.php |
修改(160 行) | 头像 ×2 收敛 |
| 38 | plugins/red_packet/views/detail.php |
修改(87 行) | 头像收敛 |
| 39 | plugins/user_level/views/like_list.php |
修改(105 行) | 头像收敛 |
| 40 | plugins/user_profile/views/front.php |
修改(135 行) | 头像收敛 |
| 41 | app/Helpers/Content.php |
修改(+236 / -2) | 净化层 img 分支改指 md_ 并写入 data-fullurl(原图留灯箱);$allowedAttrs 放行 loading / decoding;新增 groupPostImages() 正文图分组(P8-D) |
| 42 | assets/css/modern.css |
修改(+268 / -33) | 附件卡样式;灯箱尺寸 / 箭头 / 计数器;摘要图网格(mn-excerpt-grid);正文图网格(post-img-grid / post-img-cell);修掉未定义的 var(--mn-bg-hover) |
| 43 | cli/backfill_md_thumbs.php |
新增(209 行) | 正文图 md_ 回填(幂等、只增不删、--dry-run / --force),含「md_ 比原图大就删掉回退原图」策略 |
| 44 | lang/zh.php、lang/en.php、lang/zh_tw.php |
修改(各 +6 键) | 附件下载计数 3 键(attachment.download / attachment.download_count / attachment.download_times)+ 灯箱 3 键(lightbox.viewer / lightbox.prev / lightbox.next),三语同步 |
| 45 | version.php |
修改 | V1.0.8 → V1.0.22 |
未触碰:
app/Controllers/ThreadController.php、app/Controllers/PostController.php(其上传调用点Upload::file($file)已走收敛层,file()内部自行决定分月目录,无需改动);assets/js/editor.js(方案 §2 明确「无需改」);plugins/task_center/Plugin.php(方案明确「零改动」;其strpos($av, 'seed_avatars/') !== 0前缀判定对seed_avatars/YYYY-MM/x.svg依然成立);data/下任何数据。
二、改动前现状与根因
2.1 上传路径散落、无收敛层
上传文件的落盘目录、URL 拼接、缩略图命名分散在 60 余处:视图里手写 '/assets/uploads/' . $row['avatar']、控制器里各自 move_uploaded_file()、插件里再抄一份。后果有三:
- 口径漂移:附件卡原先自带一份 7 项扩展名白名单,而
Upload::file()是 5 项 → 同一文件「能上传但不能显示缩略图」; - 改一处要改 60 处:本次要改成按月分目录,若不做收敛层,就得在 60 处同时改,漏一处即产生 404;
- 缩略图与派生文件清理困难:删除原图时无法可靠地连带清掉
thumb_/md_(历史遗留即由此产生)。
2.2 全类型平铺在同一目录
assets/uploads/ 下单层平铺,随站点运行文件数线性增长(本机实测原图 334 个 / 12.5 MB)。目录项过多会拖慢目录遍历与备份;且不同类型(头像 / 封面 / 正文图 / 附件)混在一起,无法按类型单独做保留策略。
2.3 缩略图只有一档,列表与正文都用原图
makeThumbnail() 只产 thumb_(400×300)一档。于是:
- 列表页摘要图直接输出原图 URL(4 处渲染点:
forum/index.php置顶区与普通区、home/index_default.php、forum/category.php); - 正文图在详情页也用原图,一张 462 KB 的截图就是全量传输。
2.4 灯箱关闭按钮点击无反应
根因:右上角 × 的点击处理绑在容器上,而容器在图片加载完成后被重新定位/重建,× 的新节点上没有监听器;同时 × 缺少独立的 data-action 标识,事件委托选择器匹配不到。修法:灯箱整体重写,所有操作统一走 data-action 事件委托(lb-close / lb-prev / lb-next / lb-stage),并在 Esc / 遮罩 / 点击图片三处同时提供关闭路径。
2.5 正文图无分组,多图竖排占满整屏
实测正文结构:多图帖是 <div><img><img><img><img><br></div>,单图帖是裸 <img>。浏览器按默认流式排版 → 每张图独占一行,4 张图要滚 4 屏。
2.6 modern.css 存在未定义变量(静默失效)
var(--mn-bg-hover) 被引用 3 处(导航语言下拉、应用下拉、移动端「我的」菜单的 a:hover),但该变量从未定义且无 fallback → 整条 background 声明失效,三处 hover 背景无反馈。同类规则(紧邻的导航项)统一使用 --mn-bg-nav-hover(定义在 :root 兜底块),本意明确。
三、逐项改动细则
3.1 统一收敛层(Upload + TemplateCompiler)
| 方法 | 职责 | 关键约定 |
|---|---|---|
Upload::file($file, $allowedTypes, $opts) |
唯一落盘口 | 内部按 $opts['thumb'] 决定派生档位('md_' 产正文图缩略图、'' 不产);分月目录由 makeRelPath() 决定,调用方无需关心 |
Upload::url($rel) |
相对路径 → URL | 视图层统一入口 |
Upload::path($rel) |
相对路径 → 绝对路径(读/删用) | 存在才返回,不存在返回 null |
Upload::absPath($rel) |
相对路径 → 绝对路径(写入用) | 纯字符串拼装,不校验存在性 |
Upload::thumbUrl($rel, $prefix) |
缩略图 URL | 存在则缩略图、否则原图 → 零 404 |
TemplateCompiler::uploadUrl() / uploadThumb() |
视图入口 | 视图里 $this->uploadUrl($row['avatar']) / $this->uploadThumb($imgUrl) |
⚠️
path()与absPath()的分工是硬约定:thumbPath()是「存在才返回」,不能用来算「待写入」的目标路径(否则目标缺失时恒为null,会把成功误判为失败)。cli/backfill_md_thumbs.php即按thumbPath()的同一口径自己拼相对路径、再用absPath()取绝对路径。
3.2 全类型按月分目录
| 类型 | 目录 | 说明 |
|---|---|---|
| 通用附件 / 正文图 | assets/uploads/YYYY-MM/ |
月份取上传时间 |
| 头像(种子) | assets/uploads/seed_avatars/YYYY-MM/ |
User::defaultAvatarPath($id, $name, $createdAt) 取注册月 |
| 封面 | assets/uploads/covers/YYYY-MM/ |
取上传月 |
月份判定优先级:记录里的显式时间字段 → 文件 mtime → 当前月。
3.3 缩略图双档
Upload::THUMB_SIZES = ['thumb_' => [400,300], 'md_' => [800,600]];makeThumbnail() 在原图 ≤ 目标尺寸时返回 'skip'(不做放大,故小图天然无需缩略图)。
thumb_—— 列表摘要图 / 附件卡;md_—— 正文图(Content::sanitizeHtml()在渲染期把src换成md_,同时把原图 URL 写入data-fullurl供灯箱取高清)。
PNG 出口无损优化(writePng()):旧实现无条件 imagesavealpha(true) 写 32 位 RGBA,而站内图像素全不透明,alpha 平面纯浪费。新实现逐像素扫透明(762×600 实测约 67 ms)——含透明则退回旧行为(绝不丢透明),全不透明才降为 24 位 RGB,并用 q9 分别以 PNG_FILTER_NONE / PNG_ALL_FILTERS 编到内存、只落盘较小者。像素数据与旧实现完全一致,只改容器编码。实测真实源图合计 1,914,285 → 1,587,145 B(省 17.1%,逐像素零差异)。
⚠️ 不能用「读源 PNG 的 IHDR 色彩类型」代替扫描:实测 14 张源图里 11 张是色彩类型 6(带 alpha 通道)但像素全不透明,按 IHDR 判断会漏掉绝大多数优化机会。 ⚠️ 此优化只作用于 GD 分支;装了 Imagick 的环境走
makeThumbnailImagick(),不受益。
3.4 非图片附件下载计数
- 表结构:
attachments.download_count INTEGER NOT NULL DEFAULT 0(CREATE TABLE与ensureColumnPatches清单同改); - 语义:仅「非图片附件的实际下载」+1;图片原图查看与
?s=thumb缩略图不计; - 自增:单条原子
UPDATE attachments SET download_count = download_count + 1 WHERE id = ?,不做 read-modify-write;失败只记error_log,不影响下载本身。
3.5 附件卡公共组件
原先 2 处(帖子详情 / 博客详情)各写一份,本次收敛为 app/Views/_components/attachment_card.php:
- 图片卡:
?s=thumb缩略图 + 点击进灯箱;扩展名白名单与Upload::file()同源(消除 7 项 vs 5 项的口径漂移); - 文件卡:图标 + 文件名 + 体积(
Upload::formatSize())+ 下载次数; - 移动端断点
@media (max-width:768px)下改为单列; - 主题范围:只依赖
modern.css的mn-*工具类与--mn-*变量,模板主题(assets/<Name>/)下自然降级为无样式块,不报错。
3.6 灯箱重写
assets/js/lightbox.js(新增)+ app/Views/_components/image_lightbox.php(新增容器),仅在详情页加载:
- 全页连读:按 DOM 顺序收集本页所有
[data-fullurl](正文图由净化层在渲染期写入),故 htmx 翻页 / 新渲染的回复图自动进入阅读序列,页面无需声明图片列表;无data-fullurl时回退取src(兼容历史的.post-text-content img); - 多图切换:左右箭头 / 键盘
←→/ 移动端左右滑动;单图时箭头自动隐藏; - 预载相邻图;计数器显示
i / n; - PC 滚轮缩放、双指捏合缩放、拖拽 / 单指平移;
- 关闭:
Esc/ 遮罩 /×/ 点击图片 —— 四路齐备(修掉原先「×无反应」); - 兼容导出
window.viewImage(el)/window.closeLightbox(e),既有内联onclick无需改。
3.7 列表摘要图改读缩略图
TemplateCompiler::uploadThumb() 在 4 处摘要渲染点替换原图 URL:forum/index.php(置顶区 + 普通区)、home/index_default.php、forum/category.php。缩略图不存在时自动回退原图 → 零 404。
3.8 摘要图网格规范化(决策:每行 3 个 / 单图与多图同尺寸 / 超出不显示)
- 每行 3 个(
grid-template-columns: repeat(3, minmax(0, 1fr))); - 单图与多图同尺寸(统一
aspect-ratio+object-fit: cover,不再「单图占一格半」); - 最多显示 3 张,超出不显示;
min-width: 0防超宽图撑破网格列。
3.9 正文图渲染期分组(P8-D)
在 Content::sanitizeHtml() 的白名单循环之后插入 groupPostImages($dom, $xpath)(容器与 class 由我们自己生成,不应再被白名单过滤):
- 先按
nearestBlockAncestor()(只认div/p)把图归拢到块,再按块批处理; - 块内子节点只有「带
data-fullurl的 img」/br/ 空白 → 该块加post-img-grid,每图包post-img-cell;count($imgs) <= 1再加--single; - 图片混在文字里,或图在顶层裸挂 → 就地新建容器包住单图。
四条实现红线(均已写入回归用例):
| 红线 | 原因 |
|---|---|
容器一律用 <span> |
<p> 内插 <div> 会让浏览器提前闭合 <p>,破坏正文结构 |
| 必须按块批处理 | 逐图处理时,包完第一张就让 isImageOnlyBlock() 变 false,同块后续图各自成组 |
| 幂等靠结构自检而非 class 名 | 用户可粘贴同名 class(post-img-cell / post-img-grid),只看 class 会误判 |
<br> 保留在 DOM,由 CSS display:none 隐藏 |
网格里 <br> 会变成一个空网格项,多出一行空隙 |
样式(modern.css):
.post-img-grid { display: grid; grid-template-columns: repeat(2, minmax(0, 1fr)); gap: 8px; margin: 12px 0; }
.post-img-grid--single { grid-template-columns: 1fr; max-width: 480px; } /* 决策 D-a:单图整行限宽 480px */
.post-img-grid > br { display: none; }
.post-img-cell { display: block; min-width: 0; overflow: hidden; border-radius: 8px; cursor: zoom-in; background: var(--mn-bg-row-hover); }
.post-img-grid:not(.post-img-grid--single) .post-img-cell { aspect-ratio: 4 / 3; }
.post-img-grid .post-img-cell img { display: block; width: 100%; height: 100%; max-height: none; object-fit: cover; }
.post-img-grid--single .post-img-cell img { width: auto; height: auto; max-width: 100%; max-height: 500px; object-fit: contain; }
点击看大图零 JS 改动:data-fullurl + .post-text-content img 兜底选择器都在,灯箱自动接管。
3.10 正文图 md_ 回填(cli/backfill_md_thumbs.php)
- 扫全部 32 个桶的
topic+reply的extern_path正文;必须先Content::decode()再抽 URL(正文存在 base64 态); - 正则
#/assets/uploads/([^"\'\s>)\\\\]+)#i+Upload::isImageName()过滤 → 去重; - 幂等、只增不删(除下述回退);
--dry-run只演练、--force重编已存在的md_; - 回退策略:生成后比对
filesize(),若md_≥ 原图(GD 真彩重编码 vs 外部工具优化过的原图,像素更少字节反而更多)就unlink()它,让渲染层回退原图出口。用 PHPunlink()而非 shellrm,避开沙箱「安全删除守卫」的拦截。
本机实测:候选 14 个 → 保留 9 / 回退 5 / 失败 0,出口 3.6 MB → 1.2 MB(省 67%);二次运行文件清单逐字节一致(回退图无状态可记,会「重新生成再删」,但净结果不变)。
3.11 顺带修正
| 项 | 内容 |
|---|---|
var(--mn-bg-hover) 未定义 |
3 处(导航语言下拉 / 应用下拉 / 移动端「我的」菜单的 a:hover)改为实际存在的 --mn-bg-nav-hover。全量复扫后**「无 fallback 且未定义」的变量为 0 个** |
$allowedAttrs 缺 loading / decoding |
二者由 img 分支无条件写入(lazy / async)。不放行会导致「对已净化 HTML 再净化一次」时二者先被白名单删掉、再被追加到末尾 → 属性顺序漂移,破坏净化层幂等(一轮 ≠ 二轮)。已加入白名单(img 分支恒覆盖,不放宽安全) |
.mn-excerpt-thumb 背景变量 |
原用从未定义的 var(--mn-bg-row, #f0f0f0) → 改为 var(--mn-bg-row-hover) |
四、验证方式与结果
4.1 自动化回归(全部通过)
| 套件 | 结果 | 覆盖内容 |
|---|---|---|
p5_http_test.sh |
95 PASS / 0 FAIL | 真实 HTTP 端到端:路由、权限、上传、缩略图出口、附件下载计数、i18n 出口、静态资源可达 |
p8d_e2e.sh |
20 PASS / 0 FAIL | 正文图分组端到端 + md_ 出口两条分支(存在 → src 指 md_;已回退 → src 保持原图)+ CSS 可达 + CSS 变量回归守卫 |
test_p8d_img_group.php |
44 PASS / 0 FAIL | 分组算法 14 组:多图块 / 单图块 / 顶层裸图 / <p> 内混排(断言容器是 span 且 <p> 未被提前闭合)/ emoji 整行不升级 / emoji 与正文图同块 / 幂等两轮三轮逐字节一致 / 用户粘贴同名 class / 用户粘贴完整结构 / 组合场景 / data-fullurl 不受影响 / <p> 纯图升级 / 非正文图不参与 |
test_p5_content_img.php |
33 PASS / 0 FAIL | 净化层回归(含 CDATA 防线) |
test_p6_upload_png.php |
16 PASS / 0 FAIL | PNG 出口:无透明图字节变小且像素零差异、色彩类型降为 2;有透明图行为不变 |
p7_i18n_gate.php |
通过 | 三语字典键集合零差异;插件语言包三语一致;视图静态引用键零缺 |
4.2 静态门禁(本包 47 个文件)
- 语法:44 个 PHP 全部
php -l通过(No syntax errors detected); - 编码:0 个带 BOM、0 个含 CRLF、0 个非 UTF-8 —— 全部 UTF-8 无 BOM + LF,符合项目约定;
- 调试残留:
var_dump/print_r/[DBG]/TODO/FIXME/console.log/debugger扫描 0 命中。
4.3 数据与文件完整性
- 上传目录:原图 334 个(12.5 MB)|
thumb_22 个 |md_9 个(1.0 MB)——md_全部为有效图片、0 空壳、0 非图片,且全部小于对应原图; - 只增不删证明:
md_回填前后对assets/uploads做全量清单对比 → 删除的恰好是 5 个被回退的md_,无任何非md_删除,thumb_22 → 22、原图 334 → 334 未动; - 零 404:T8 走的是「
md_已回退 →src保持原图」分支并通过,回退路径已被 HTTP 端到端直接验证;另加「src指向的文件真实存在」与「体积 > 1KB(防空壳)」两条保险断言。
4.4 体积收益
| 项 | 前 | 后 | 省 |
|---|---|---|---|
| 正文图出口(14 个候选) | 3.6 MB | 1.2 MB | 67% |
| PNG 缩略图编码(7 样本) | 791,401 B | 665,218 B(无损优化) | 16% |
五、部署与回滚
5.1 ⚠️ 先明确:本次要上传什么
服务器是 IIS、没有命令行 → 缩略图必须在本地生成好、跟着一起传上去,不能在服务器上跑 CLI。
| 内容 | 传不传 | 说明 |
|---|---|---|
本包的 46 个代码文件 + version.php |
要传 | 本次升级的主体,按相对路径覆盖到站点根 |
assets/uploads/2026-09/md_*.{png,jpg}(9 个) |
要传 | 本机已生成好的正文图缩略图。服务器没命令行生成不了,所以必须传。是新增文件,不会覆盖任何东西 |
data/(整个目录) |
不要传 | 运行数据。本次改动没有任何数据库结构或数据变更 —— 新列 attachments.download_count 由 ensureColumnPatches() 在首次请求时自动补齐,不需要传库、不需要跑迁移脚本 |
assets/uploads/(整目录覆盖) |
不要 | 里面是用户真实上传;整目录覆盖 = 用「下载那一刻」的快照顶掉服务器。只挑新增的 9 个 md_ 传即可 |
assets/uploads/seed_avatars/ |
不要传 | User::defaultAvatarPath() 会 ensureDir() + 按需自动生成 SVG,自愈 |
data/meta/sessions.sqlite |
绝对不要传 | 会顶掉服务器上所有用户的登录态(全站被登出) |
data/runtime/、data/lock/、data/log/ |
不要传 | 纯本地运行时状态,传上去没有意义 |
update/22/改动说明.md |
随意 | 只是文档,传到站点根不影响运行 |
⚠️ 传
data/的唯一真实风险:会用「下载那一刻」的库覆盖服务器 —— 下载之后服务器上产生的 新用户 / 新帖 / 新回复会全部丢失。本次升级不需要传库,所以别传,风险为零。
旧的上传文件(平铺在 assets/uploads/ 根目录的)不需要动:Upload::REL_PATTERN 里年月段是可选的
(#^(?:(?:seed_avatars|covers)/)?(?:[0-9]{4}-[0-9]{2}/)?[A-Za-z0-9_.\-]+$#),
所以 ab12cd.png / seed_avatars/1.svg / covers/tpl_x.svg 这些旧路径依然能正常解析;
净化层的 img 分支也只在 md_ 确实存在时才改写 src,否则保持原图 → 零 404。
即:按月迁移是整理优化,不是升级必需项。
5.2 部署步骤(5 步)
- 备份:把服务器的
data/与assets/uploads/各整目录复制一份(出了任何问题都能整回去)。 - 覆盖代码:把本包内的 46 个代码文件 +
version.php按相同相对路径覆盖到站点根(清单见 5.3)。 ⚠️assets/js/lightbox.js是新增文件,必须上传(漏了灯箱就不工作)。 - 上传 9 个
md_缩略图:把本机assets/uploads/2026-09/md_*.{png,jpg}传到服务器同一目录assets/uploads/2026-09/。这 9 个是新增文件,直接丢进去即可,不需要删除或覆盖任何东西。 - 清缓存(⚠️ 不要省这一步):登录后台 → 维护 → 清理缓存(
/admin/maintenance的「清理缓存」按钮)。 原因:游客的列表页静态缓存是永久缓存、不按 TTL 过期(只在发帖 / 回帖 / 改设置 / 改主题时主动失效), 不清的话老访客会继续看到升级前的列表页 HTML(图片仍是原图、没有三列网格),而且不会自己恢复。 服务器没有命令行也没关系 —— 这个按钮是网页操作。 - 验证:帖子详情页 → 图片两列排布、点击出灯箱、右上角
×能关、多图可用 ←/→ 切换;/forum列表页 → 摘要图是 3 列网格。
5.3 要覆盖的代码文件清单(46 个)
app/Controllers/{Api,Attachment,Auth,Blog,Profile}Controller.php
app/Core/TemplateCompiler.php
app/Helpers/{Content,CoverTemplate,Settings,Upload}.php
app/Models/{Thread,User}.php
app/SplitDB/Schema.php
app/Views/_components/{attachment_card,image_lightbox,right_sidebar}.php
app/Views/{blog/{detail,edit,index},forum/{category,index},home/index_default,post/edit,profile/index,search/index,tags/show,thread/{edit,show}}.php
assets/css/modern.css
assets/js/{lightbox.js,thread.js} ← lightbox.js 为新增文件(必须上传)
cli/{backfill_md_thumbs,migrate_uploads_by_month,seed_content}.php
lang/{zh,en,zh_tw}.php
plugins/content_review/Plugin.php
plugins/dice/hook/right_sidebar_after.php
plugins/dice/views/index.php
plugins/floor_reply/views/list.php
plugins/points_mall/FrontController.php
plugins/quiz_duel/views/index.php
plugins/red_packet/views/detail.php
plugins/user_level/views/like_list.php
plugins/user_profile/views/front.php
version.php
无需:数据库迁移脚本、配置项修改、访问升级 URL、清理 sw.js(CACHE_VERSION 随版本号自动变化)。
自动发生:首次请求时 ensureColumnPatches 自动为 attachments 补 download_count 列(并发重复 ALTER 已捕获忽略)。
5.4 关于 md_ 缩略图(服务器无命令行时的长期做法)
- 本次的 9 个
md_已在本地生成好,直接上传即可(见 5.2 第 3 步)。它们是历史正文图的缩略图 —— 那批图是在「正文图缩略图」功能上线之前上传的,所以需要一次性回填。 - ✅ 以后新发的正文图不需要任何手工操作:编辑器插图走
ApiController::upload(), 它调Upload::file($_FILES['file'], null, ['thumb' => 'md_'])→ 上传时自动生成md_。 (附件上传走ThreadController默认档位thumb_;头像走['thumb' => '']不生成 —— 均为有意设计。) - 万一以后确实要给历史图补
md_(例如换了图片存储):在本地把data/+assets/uploads/下载下来, 跑php cli/backfill_md_thumbs.php(幂等、只增不删),再把新出现的md_*传上去即可。 该脚本生成后会比对filesize(),md_比原图大就自动删掉、让页面回退原图,不会浪费流量。 php cli/migrate_uploads_by_month.php(旧文件按月归档)本次不需要跑 —— 服务器上的路径已经是2026-09/xxx形式(数据库里 13 条附件记录全部已是新格式)。
5.5 两个 CLI 工具(⚠️ 只能在本地跑,服务器无命令行)
两个工具不执行也能正常升级(渲染层对缺失缩略图自动回退原图,零 404)。 它们是给「在本地维护数据」用的,不要在服务器上找命令行:
# 1) 正文图 md_ 回填(幂等、只增不删)
php cli/backfill_md_thumbs.php --dry-run # 先演练,看清单
php cli/backfill_md_thumbs.php # 执行
# 2) 旧上传文件按月迁移(会写盘、会改写数据库中的路径 → 先备份!)
php cli/migrate_uploads_by_month.php --dry-run # 必须
php cli/migrate_uploads_by_month.php # 确认无误后执行
⚠️
migrate_uploads_by_month.php会改写正文与settings里的路径,执行前请整库 +assets/uploads/一起备份(脚本自带备份步骤与日志)。 ⚠️ 详情页/thread/{id}不走PageCache(PageCache仅列表页 + 仅游客),故回填后无需清页面缓存; 列表页若需立即生效,走后台 维护 → 清理缓存(服务器无命令行,不要指望手工删文件)。