[开发]FlintHub 附件体系改造方案 v3(最终稿)· 2026-09-19

👑Lv.11 元老 🌞 骨干会员
2026-10-26 11:03:46

一、施工范围(你的 7 条要求 → 落地口径)

#要求性质落地口径
1统一收敛层地基84 处路径拼接全部收敛到统一 API(视图 55 + PHP 26 + 硬编码 3)。必须先做,否则分月与迁移无落点。
2全类型分月地基附件 / 编辑器正文图 / 头像 / 博客封面 + seed 头像 → 全部进 YYYY-MM/。seed 头像保持 seed_avatars/YYYY-MM/ 前缀(见 3.4)。
3缩略图体系呈现thumb_ 400×300(附件卡片)+ md_ 800×600(正文图);头像不生成缩略图。
4旧数据迁移一次性文件 + 全部字段 + 全部正文 URL 一次性彻底迁移;迁移后不存在新旧并存(见 4.4)。
5附件卡片 UI体验公共组件 + Modern 主题样式 + 系统内置类型图标 + 大小格式化 + 下载按钮 + 移动端适配。仅 Modern 主题(Xenforo 不在本次范围)。
6下载计数功能表结构(走补丁清单)+ 原子自增;仅非图片计数;卡片展示「下载 N 次」。
7灯箱体验全页连读多图切换 + 预载;只在详情页使用,不做全局;其他页面图片直接用缩略图。

二、地基一:统一收敛层(60 处 → 1 套 API)

31视图层 UPLOAD_URL 拼接
26PHP 层 UPLOAD_PATH 拼接(12 文件)
3硬编码 /assets/uploads/
60合计,全部收敛

2.1 统一 API(app/Helpers/Upload.php)

/** 相对路径 → 可访问 URL
 *  '2026-09/ab12….png'          → '/assets/uploads/2026-09/ab12….png'
 *  'seed_avatars/2026-09/1.svg' → '/assets/uploads/seed_avatars/2026-09/1.svg'
 *  'covers/2026-09/x.svg'       → '/assets/uploads/covers/2026-09/x.svg'
 *  'ab12….png'                  → '/assets/uploads/ab12….png'   ← 兼容旧值(迁移前/漏网数据)
 *  'http(s)://…'                → 原样返回(外链不动)
 *  ''                           → ''                             ← 便于视图直接输出 */
public static function url(string $rel): string

/** 相对路径 → 绝对文件系统路径
 *  禁止 '..';白名单正则(可选命名空间 + 可选年月目录 + 安全文件名)+ realpath 前缀校验;
 *  非法或文件不存在 → null。所有 unlink / readfile / file_exists / is_file 一律走它。 */
public static function path(string $rel): ?string

/** 缩略图 URL:存在缩略图文件则返回,否则回退 url()(零 404)
 *  $prefix: 'thumb_' = 400×300(附件卡片)、'md_' = 800×600(正文图) */
public static function thumbUrl(string $rel, string $prefix = 'thumb_'): string

/** 字节数友好格式化:1536 → '1.5 KB'、2516582 → '2.4 MB' */
public static function formatSize(int $bytes): string

/** 生成相对路径(内部):'2026-09/.' */
private static function makeRelPath(string $ext): string

2.2 视图入口(app/Core/TemplateCompiler.php)

public function uploadUrl(string $rel): string                      // → Upload::url($rel)
public function uploadThumb(string $rel, string $p = 'thumb_'): string

2.3 改造范式(前后对比)

场景改造前改造后
普通图片<?php echo $this->e(\UPLOAD_URL . $avatarPath); ?><?php echo $this->e($this->uploadUrl($avatarPath)); ?>
CSS 背景图background-image:url('<?php echo $this->cssValue(\UPLOAD_URL . $cover); ?>')background-image:url('<?php echo $this->cssValue($this->uploadUrl($cover)); ?>')
附件缩略图<img src="/attachment/{id}"><img src="/attachment/{id}?s=thumb">
附件下载直链<?php echo $this->e(\UPLOAD_URL . $att['filename']); ?><?php echo $this->e($this->url('/attachment/' . (int)$att['id'])); ?>
e() 包裹必须保留(值来自数据库)。收敛只针对「路径拼接」这一件事,转义职责不变(项目视图规范 = 赋值时转义)。

2.4 视图层 31 处清单

文件行号类型
app/Views/forum/index.php60, 93, 150, 182头像 ×4
app/Views/forum/category.php70, 101头像 ×2
app/Views/home/index_default.php48, 82, 140头像 ×3
app/Views/home/index_default.php127博客封面(现未走 cssValue,一并修正)
app/Views/blog/index.php50, 63封面 cssValue + 头像
app/Views/blog/detail.php47, 70封面 + 评论头像
app/Views/blog/edit.php63当前封面预览
app/Views/thread/show.php89, 239头像 ×2
app/Views/thread/edit.php105下载直链 → /attachment/{id}
app/Views/post/edit.php41下载直链 → /attachment/{id}
app/Views/profile/index.php25头像(此处原本未加 \ 前缀)
app/Views/search/index.php121头像
app/Views/tags/show.php32头像
app/Views/_components/right_sidebar.php62, 158头像 ×2
plugins/dice/views/index.php80头像
plugins/floor_reply/views/list.php42头像
plugins/quiz_duel/views/index.php45, 88头像 ×2
plugins/red_packet/views/detail.php73头像
plugins/user_level/views/like_list.php60头像
plugins/user_profile/views/front.php27头像

2.5 PHP 层 26 处清单

文件 : 行用途改造
app/Helpers/Upload.php:84,86,87,129落盘 + 缩略图路径makeRelPath() / path()
app/Controllers/AttachmentController.php:85下载读文件basename() → Upload::path()
app/Controllers/ApiController.php:278,280,281远程拉图落盘走 makeRelPath()
app/Controllers/BlogController.php:283,288,293,328封面替换 / 删除Upload::path() + 顺带删缩略图
app/Controllers/ProfileController.php:100,101旧头像删除Upload::path()
app/Helpers/Settings.php:822,930孤儿清理 + deleteAttachment同上(清理已递归,需回归)
app/Helpers/CoverTemplate.php:94covers 目录→ covers/YYYY-MM/
app/Models/Thread.php:742,843硬删帖清附件白名单放行 / + path()
app/Models/User.php:213,219seed 头像目录与返回值→ seed_avatars/YYYY-MM/(见 3.4)
app/Models/User.php:614级联删附件path()
plugins/content_review/Plugin.php:138,205审核通过 / 驳回删头像path()
plugins/points_mall/FrontController.php:204已用 realpath 前缀校验替换为 path()(等价,统一入口)
plugins/ai_assistant/config.php:22,23插件自定义 UPLOAD_PATH / URL改为引用核心常量,消除双份定义
cli/seed_content.php:281,285seed 头像生成(独立实现)同步 seed_avatars/YYYY-MM/
assets/js/editor.js:2046 + editor.php:2046硬编码前缀判定无需改(分月后 URL 仍含 /assets/uploads/),但需回归

三、地基二:全类型按月分目录

3.1 目录结构

assets/uploads/
├── 2026-09/
│   ├── ab12cd34….png          附件 / 编辑器正文图 / 用户头像 / 博客封面
│   ├── thumb_ab12cd34….png    缩略图 400×300(附件卡片)
│   └── md_ab12cd34….png       中图   800×600(正文内嵌图)
├── covers/
│   └── 2026-09/
│       └── tpl_a1b2c3.svg     模板生成封面(CoverTemplate)
└── seed_avatars/
    └── 2026-09/
        └── 1.svg              默认种子头像(按用户注册月份)

3.2 各类型处理表

类型入口分月存库字段(迁移后)视图取值缩略图
表单附件Upload::file() ← Settings::handleAttachments / ThreadController / PostController是attachments.filename = 2026-09/…/attachment/{id}?s=thumbthumb_
编辑器正文图POST /api/upload是不入库(URL 在正文里)正文 HTML(渲染期改写)md_
远程拉图/api/fetchImage是不入库同上md_
用户头像ProfileController::upload_avatar是users.avatar = 2026-09/…uploadUrl($avatar)不需要
博客封面上传BlogController 的 Upload::file()是blogs.cover_image = 2026-09/…uploadUrl($cover)可选(列表页省流量)
模板生成封面CoverTemplate::saveCover()是blogs.cover_image = covers/2026-09/…svg同上SVG 无需
默认种子头像User::defaultAvatarPath() / cli/seed_content.php::writeAvatar()是users.avatar = seed_avatars/2026-09/{id}.svg同上不需要

3.3 缩略图档位

前缀尺寸用途生成时机
thumb_400×300附件卡片上传时同步(现有逻辑)
md_800×600正文内嵌图(新增)上传时同步(复用 makeThumbnail() 的 $maxW/$maxH)
头像不生成缩略图(本身就是 96×96 的小 SVG 或用户上传的小图,再缩无意义)。

两档各自独立判定「原图 ≤ 目标尺寸则 skip」→ 小图不产生冗余文件,thumbUrl() 自动回退原图。

3.4 seed_avatars 分月设计

你的要求是「seed 头像也应该进 YYYY-MM/,因为默认注册的就是 seed 头像」。确认它的生成逻辑后,落地口径如下:

路径形态:seed_avatars/2026-09/{id}.svg —— 保留 seed_avatars/ 前缀,在其内部再按月分层。

为什么保留前缀(而不是直接放 2026-09/1.svg):

  • plugins/task_center/Plugin.php:491 用 strpos($av, 'seed_avatars/') !== 0 靠前缀判定「是否为默认头像」(任务中心用它区分「已设置头像」)。 保留前缀 → 该判断零改动;若去掉前缀,任务中心的判断会全部失效(所有用户被误判为「未设置头像」)。
  • 语义清晰:一眼可辨「系统生成的默认头像」vs「用户上传的头像」,便于清理与统计。

月份来源:用户注册月份(users.created_at)。

  • 注册时生成 → 当月即注册月,直接 date('Y-m') 即可;同时加一个可选参数 ?string $createdAt = null 供迁移/回填显式指定,未传则取当前月。
  • 迁移老数据时 → 按该用户 created_at 分配月份,确定性保持(同一用户恒定同一路径,不会漂移)。

确定性不受影响:现有逻辑是 if (!is_file($file)) file_put_contents(...) —— 路径变了,但「同 id 恒同文件、已存在则复用」的语义完全保留。

现状规模:assets/uploads/seed_avatars/ 现有 281 个 SVG,迁移时逐个按所属用户的 created_at 归月。

四、旧数据一次性彻底迁移

4.1 脚本:cli/migrate_uploads_by_month.php

用法:
  php cli/migrate_uploads_by_month.php --dry-run      # 只出报告,零改动(默认)
  php cli/migrate_uploads_by_month.php --yes          # 真正执行
  php cli/migrate_uploads_by_month.php --only=files   # 只移文件不改数据(分阶段用)

迁移范围(7 类):
  ① assets/uploads/*.ext              → assets/uploads/YYYY-MM/
     (thumb_ / md_ 前缀文件跟随其原图所在月份)
  ② assets/uploads/covers/*.svg       → covers/YYYY-MM/
  ③ assets/uploads/seed_avatars/*.svg → seed_avatars/YYYY-MM/(按用户 created_at)
  ④ attachments.filename              纯文件名 → 'YYYY-MM/文件名'
  ⑤ users.avatar                      同上;seed 头像 → 'seed_avatars/YYYY-MM/{id}.svg'
  ⑥ blogs.cover_image                 同上;模板封面 → 'covers/YYYY-MM/x.svg'
  ⑦ 正文 / 内容中的 URL(6 个位置):
       topic_index.excerpt_images        (逗号分隔完整 URL)
       extern/ 下 topic 正文 txt          (按 topic.extern_path 定位)
       extern/ 下 reply 正文 txt          (按 reply.extern_path 定位)
       blogs.content
       blog_comments.content
       settings 表所有值(custom_css 等可能引用图片)
  输出日志 data/runtime/migrate_uploads_<时间戳>.log(逐条 src → dst)

月份判定优先级

  1. 有库记录 → 用记录时间(attachments.created_at / users.created_at / blogs.created_at)
  2. 无记录(编辑器图、孤儿文件) → 文件 mtime
  3. thumb_ / md_ 前缀 → 跟随原图月份(保证同组文件同目录)

4.2 安全设计(缺一不可)

  • 默认 dry-run,必须显式 --yes 才动文件
  • 幂等:路径已含 YYYY-MM/ 的跳过,重复执行结果一致
  • 精确替换:正文 URL 按「迁移清单中的文件名」逐个 preg_quote 匹配,不用宽泛正则(防 ab12.png 误伤 ab12.png.bak)
  • 先备份:脚本启动即检查当日备份是否存在,无则拒绝执行并打印备份命令
  • 失败即停:单个文件移动失败(跨盘 / 占用)立即中止,打印已完成条数便于人工回滚
  • 只移不删:任何情况下不删除文件

4.3 操作步骤(等你把生产数据覆盖到本地后)

  1. 你把服务器的 assets/uploads/ 与 data/ 覆盖到本地。注意 main_index.sqlite 是 WAL → 必须连 -wal / -shm 一起拷,否则读不到表。
  2. 停站(或确认无人访问)。迁移期间不接受写入。
  3. 全量备份到 .workbuddy-ai/backup/20260919_附件迁移/:assets/uploads/ + data/meta/ + data/bucket/ + data/extern/。
  4. 我先在备份副本上完整演练一遍(dry-run → 实际执行 → 抽样验证),确认无误。
  5. 对正式数据跑 --dry-run,把报告给你确认(重点看「无法判定月份的文件」数量与抽样)。
  6. 你确认后执行 --yes,完成后输出对账(文件数 / 目录数 / DB 改动行数)。
  7. 回归验证:抽 10 个附件下载、10 张正文图、5 个头像、5 个 seed 头像、5 个封面;跑孤儿清理确认不误删。

4.4 「迁移后无新旧并存」的口径

按你的要求,迁移是一次性彻底的:迁移完成后,所有数据里的路径都是新布局(含 YYYY-MM/),不存在「一部分在根目录、一部分在子目录」的混合状态。

因此迁移后:

  • 视图走 Upload::url() 输出的全是新路径;
  • Upload::url() 里对「无斜杠旧值」的兼容分支保留但不再是常态路径——它是防御性兜底(万一有漏网数据或第三方插件直写了旧格式),不是设计上的「两套并存」;
  • 迁移脚本的验收标准就是:二次 dry-run 报告里「待迁移项 = 0」,即再跑一次不会有任何改动。

五、正文图缩略图(渲染期)

落点:app/Helpers/Content.php:121-126 的 if ($tag === 'img') 分支(净化循环内)。

if ($tag === 'img') {
    $node->removeAttribute('srcset');
    $node->removeAttribute('sizes');
    $node->setAttribute('loading', 'lazy');
    $node->setAttribute('decoding', 'async');

    // 站内上传图 → 换中图 + 记录原图供灯箱使用
    $src = $node->getAttribute('src');
    if (preg_match('#^/assets/uploads/(.+)$#', $src, $m)) {
        $rel = $m[1];
        $md  = Upload::thumbUrl($rel, 'md_');       // 无 md_ 则回退原图
        if ($md !== Upload::url($rel)) {            // 确认确实换了图才写 data-fullurl
            $node->setAttribute('data-fullurl', $src);
            $node->setAttribute('src', $md);
        }
    }
}
为什么在渲染期做:① 净化本就在渲染期跑(ThreadController:233 → Content::formatPostContent())→ 存量帖立即生效、无需重存、不改数据;② 迁移脚本因此不必处理「正文里是否要换缩略图」,只改目录; ③ 将来调尺寸/策略只改这一处。
⚠️ 配套必改:净化属性白名单不含 data-fullurl (Content.php:53 $allowedAttrs = ['href','src','alt','title','target','rel','class','width','height','style'])。
必须放行该属性,并对它的值做协议白名单校验(仅 http(s):// 或 / 开头),否则等于开了个属性注入口子。

正文图不包 <a>:thread.js 已有 .post-text-content img 全局点击委托;且 data-action 不在净化白名单里,加了也会被删。

六、非图片附件下载次数

6.1 表结构(必须走补丁清单)

ALTER TABLE attachments ADD COLUMN download_count INTEGER NOT NULL DEFAULT 0
⚠️ 必须加进 Schema::ensureColumnPatches() 的 $columnPatches 清单(app/SplitDB/Schema.php:610)。 Schema::bootstrap() 只在 data/meta 不存在时执行,存量站升级不会重跑 → 只改 CREATE TABLE 对老站完全无效。

6.2 计数语义

场景请求计数
非图片附件下载/attachment/{id}(非 image/*)+1
图片原图查看/attachment/{id}(image/*)不计
图片缩略图/attachment/{id}?s=thumb不计
编辑页下载改造后统一走 /attachment/{id}+1(原先绕过权限校验,现纳入)
UPDATE attachments SET download_count = download_count + 1 WHERE id = :id   -- 原子自增,不做 read-modify-write

6.3 i18n

  • 新增键 attachment.download_count / attachment.download,三语同步(lang/{zh,en,zh_tw}.php,基线各 1224 键)
  • I18n::get() 缺键是 return $key; → 不报错,只在页面把键名当文案吐出来,必须跑全量比对

七、附件卡片 UI(公共组件 + Modern 样式)

7.1 组件收敛:2 处渲染 → 1 份代码

新增 app/Views/_components/attachment_card.php(模板引擎已支持 {include} → $this->include($tpl, $extra)), 入参 att / variant(main | post)/ showDownloads。

调用点:app/Views/thread/show.php(2 处:主楼 + 回帖)→ 收敛为 1 份逻辑。 组件只输出语义类 .fh-attach-*,样式由 modern.css 提供。

7.2 视觉设计(Modern)

图片卡

  • 栅格:display:grid; grid-template-columns:repeat(auto-fill,minmax(120px,1fr)); gap:8px
  • 缩略图:width:100%; aspect-ratio:4/3; object-fit:cover(去掉 200×150 硬编码)
  • hover:scale(1.03) + 主色描边;prefers-reduced-motion 下禁用
  • 文件名单行省略;src="/attachment/{id}?s=thumb"

文件卡

┌────────────────────────────────────────────────┐
│ ┌────┐  project-spec-2026.pdf                 │
│ │PDF │  2.4 MB · 下载 37 次                    │
│ └────┘                        [ ↓ 下载 ]      │
└────────────────────────────────────────────────┘
  • 类型图标(复用系统内置图标,不新增资源): pdf→file-pdf、zip/rar/7z→file-archive、doc/docx→file-word、 xls/xlsx→file-excel、txt/md→file-alt、其他 →file
  • 大小走 Upload::formatSize(),替代现有 2048.0 KB
  • 下载按钮复用 .mn-btn.mn-btn-sm + download 图标

7.3 移动端(@media (max-width:768px))

  • 图片栅格降为 minmax(100px,1fr),480 以下 2 列
  • 文件卡:图标 + 文件名占满,按钮不压缩;文件名 word-break:break-all(已有 .thread-file-name 可复用)
  • 触控目标高度 ≥ 40px

7.4 主题范围

仅 Modern 主题。Xenforo 主题不在本次改造范围(将单独删除),其视图与 CSS 不做任何改动,也不进收敛清单。

八、灯箱(全页连读 + 预载,仅详情页)

8.1 加载范围(按你的要求)

口径
使用范围仅详情页(帖子详情 + 博客详情),不做全局,不进 layouts/main.php
容器收敛为公共组件 app/Views/_components/image_lightbox.php,由 2 个详情页视图各自 include(不是布局层)→ 代码从 2 份变 1 份,加载范围不变
JS从 thread.js 拆出 assets/js/lightbox.js,仍只在详情页加载。好处:博客详情页不必再为了灯箱加载 thread.js(它含引用 / 附件 / 折叠等帖子专属逻辑)
其他页面图片直接用缩略图(thumbUrl()),不提供灯箱 —— 符合你的要求
关闭方式统一统一为 data-action="close-lightbox" 事件委托(现在 Modern 详情页用的是内联 onclick,不一致)

8.2 多图切换设计(全页连读)

能力实现
图组收集打开时现算:document.querySelectorAll('[data-fullurl]') 按 DOM 顺序全页收集,可跨楼层连续翻阅。不可缓存成静态数组——htmx 翻页只替换 #replies,静态数组会失效
预载打开时预载下一张;每次切换后预载新的上一张与下一张(new Image().src = url);已缓存则瞬切无白屏
PC 操作← / → 键切换;左右悬浮箭头按钮;顶部 3 / 8 计数器
移动端操作未放大时左右滑动 = 切换(阈值 50px,横向位移需大于纵向);放大后单指拖动 = 平移(沿用现有 lbScale > 1 分支,天然不冲突)
循环首张往前 → 末张;末张往后 → 首张
状态重置每次切换重置 lbScale=1 / lbTx=0 / lbTy=0(沿用现有逻辑)
CSS.image-lightbox img { max-width:92vw; max-height:88vh }(现为 60vw/60vh,多图阅读偏小)+ 箭头 / 计数器样式 + 移动端微调
与现有手势零冲突:现有 touchstart 仅在 lbScale > 1 时进入 pan 模式;未放大时 lbTouch 为 null → 左右滑动完全空闲。切换逻辑只在「未放大」分支接管,不触碰现有缩放 / 平移代码。
哪些图会被卷进多图浏览:只有带 data-fullurl 的图片。① 正文图 → 由 Content 渲染期自动加;② 附件区图片 → 视图加。 头像等小图不加该属性,避免被卷进多图连读。

九、改动文件汇总

文件类型改动
app/Helpers/Upload.php改url() / path() / thumbUrl() / formatSize() / makeRelPath();file() 分月;makeThumbnail() 双档
app/Core/TemplateCompiler.php改新增 uploadUrl() / uploadThumb()
app/Helpers/Content.php改img 分支换 md_ + 写 data-fullurl;放行该属性并做协议校验
app/SplitDB/Schema.php改$columnPatches 加 attachments.download_count
app/Controllers/AttachmentController.php改?s=thumb 分支 + immutable 缓存头 + 非图片计数 + Upload::path()
app/Controllers/ApiController.php改upload / fetchImage 分月
app/Controllers/{Thread,Post,Blog,Profile}Controller.php改上传调用点 + 封面 / 头像删除走统一路径解析
app/Helpers/Settings.php改handleAttachments / deleteAttachment / cleanupOrphanAttachments 适配
app/Helpers/CoverTemplate.php改covers/YYYY-MM/
app/Models/User.php改defaultAvatarPath() 加可选 $createdAt,路径 → seed_avatars/YYYY-MM/;级联删附件走 path()
app/Models/Thread.php改附件删除白名单放行 / + path()
cli/seed_content.php改writeAvatar() 同步 seed_avatars/YYYY-MM/
plugins/content_review/Plugin.php、points_mall/FrontController.php、ai_assistant/config.php改统一路径解析 / 消除重复常量定义
app/Views/_components/attachment_card.php新增附件卡公共组件
app/Views/_components/image_lightbox.php新增灯箱公共组件(含箭头 / 计数器)
app/Views/ 12 个文件改55 处拼接收敛 + 附件区改组件 + 灯箱容器改 include
plugins/*/views/ 6 个文件改7 处头像拼接收敛
assets/js/lightbox.js新增灯箱 + 多图切换 / 预载 / 键盘 / 滑动(仅详情页加载)
assets/js/thread.js改移除灯箱逻辑,保留引用 / 附件 / 折叠
assets/css/modern.css改附件卡样式 + 灯箱尺寸 / 箭头 / 计数器 + 移动端断点
cli/migrate_uploads_by_month.php新增迁移脚本
lang/{zh,en,zh_tw}.php改新增附件 / 灯箱相关键(三语同步)
version.php、update/14/、增量补丁X.Y.Z.zip改/新增升版 + 发布包

十、施工顺序(P0 → P7,逐段可验证)

阶段内容验证点
P0准备:备份 assets/uploads/ + data/{meta,bucket,extern};记录基线(文件数、附件行数、seed 头像 281 个、目录结构)备份可读、基线入档
P1收敛层:Upload::url/path/thumbUrl/formatSize + TemplateCompiler::uploadUrl/uploadThumb。纯新增,不动调用点php -l 通过;单元验证:新/旧值兼容、路径穿越(../)拦截、协议校验
P2拼接收敛:60 处全部替换为统一入口全库 grep UPLOAD_URL / UPLOAD_PATH 仅剩 config.php 定义处;页面无 404
P3分月落盘:makeRelPath() 分月 + md_ 档位 + covers/YYYY-MM/ + seed_avatars/YYYY-MM/新上传落入对应 YYYY-MM/;缩略图同目录;旧文件仍可访问(迁移前)
P4下载计数:Schema 清单 + 非图片原子自增存量库补列成功;下载 +1;图片查看不计
P5视图与交互:附件卡组件 + 灯箱组件 + lightbox.js + 双主题 CSS + 正文图缩略图Modern 主题正常;多图切换 / 预载 / 键盘 / 滑动可用;清缓存后生效
P6迁移:脚本编写 → 备份副本演练 → 你确认 → 对正式数据执行dry-run 报告确认;执行后抽样回归;二次 dry-run 应为「待迁移项 = 0」
P7发布:i18n 三语同步;升 FLINTHUB_VERSION;出 update/14/ + zip三语零差异;发布包结构符合约定

十一、风险与应对

#风险后果应对
1迁移不可逆(移文件 + 改库 + 改正文)附件 / 图片全站失效默认 dry-run;先备份;先在备份副本上完整演练;精确替换不用宽泛正则;失败即停;只移不删
260 处拼接漏改局部 404收敛层先行;改完全库 grep UPLOAD_URL/UPLOAD_PATH 应为 0 命中(除常量定义)
3seed 头像分月破坏 task_center 的「是否已设头像」判定所有用户被误判未设头像保留 seed_avatars/ 前缀 → strpos($av,'seed_avatars/') 判断零改动(这也是选择该目录形态的原因)
4老站 ensureColumnPatches 漏加字段下载计数报错清单 + marker 验证;用存量库实测
5净化放行 data-fullurl 引入属性注入潜在 XSS对该值做协议白名单(仅 http(s):// 或 / 开头)
6正文图 md_ 不存在图裂thumbUrl() 内 file_exists() 判定,不存在回退原图(makeThumbnail() 返回值不可信)
7灯箱图组在 htmx 翻页后失效切换范围不对打开时现算图组,不缓存静态数组
8移动端滑动切换与平移冲突体验混乱严格用 lbScale > 1 分支:放大 = 平移,未放大 = 切换
9i18n 三语不同步页面吐键名跑「模板引用键 ↔ 三语字典」全量比对
10改视图 / CSS 后未清页面缓存以为没改对PageCache::invalidate()(data/runtime/pages/*.html)
11本机 Edit 偶发「报成功但未落盘」漏改导致 500关键改动后回读验证,不只信返回值
12本机无 Chromium / 无真机移动端与灯箱手势未实机验证用「HTML 结构计数 + CSS 特异性分析 + JS 分支静态推演」验证;必要时你用手机实测

十二、开工前最后确认

你的 7 条要求已全部落入方案,无待定项。仅一处需要你明确(不影响其他部分):

范围已锁定:仅 Modern 主题。Xenforo 主题(assets/Xenforo/)不在本次改造范围,将单独删除 —— 因此它不进收敛清单、不改视图、不改 CSS。

⚠️ 时序提醒:在 Xenforo 被删除之前,其视图仍在用旧的 UPLOAD_URL . $x 拼接。若先执行了分月落盘(P3)而 Xenforo 尚未删除,该主题下的头像/封面会 404 —— 属预期,删除主题即消失。建议在 P3 之前先删掉 Xenforo,避免这段窗口。

施工起点:P0 备份 → P1 收敛层。收敛层是纯新增(不改任何调用点), 可独立完成并验证,零回归风险。你可以让我先做 P1 停下来看效果,再继续 P2 之后的接线。
轻量级、高性能、零 MySQL 依赖的PHP社区系统。
| 瀏覽 5 次 | 回覆 0 次

全部回覆 (0)

暫無回覆

請 登入