You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 
 

11 KiB

设计:文章图片元数据、孤儿回收与优化流水线

日期:2026-04-18
状态:已定稿(与产品对话一致:优先 A 孤儿回收 + C 图片优化

1. 背景与目标

当前 POST /api/file/upload 将图片写入 public/assets/,返回 /public/assets/... URL;posts.bodyMarkdowncoverUrl 仅保存字符串,磁盘文件与数据库无引用关系。导致:

  • 删改文章后易产生 无人引用的孤儿文件,磁盘只增不减。
  • 缺少结构化元数据,难以做 压缩、限宽、多格式 等优化与后续 CDN 迁移。

本 spec 定义 站内托管图片 的元数据表、保存文章时 的引用同步、孤儿文件的审查与清理(默认 自动删除;可配置 自动清理;支持 手动 清理),以及 上传后优化 的首版约定。

2. 与既有 spec 的关系

  • 2026-04-18-article-edit-markdown-design.md 曾将「服务端图片压缩流水线」列为 非目标本 spec 取代该条:在保持「Markdown 内为 URL、复用上传接口形态」的前提下,增加元数据与优化流水线。
  • 该 spec 中的编辑器交互(分屏、![](url) 插入、MIME/大小限制)仍然有效;大小与 MIME 上限 若与本 spec 冲突,以 较严者 为准,或在实现计划中统一改为一处常量。

3. 架构取向(已定)

采用 元数据表 + 保存文章时建立/刷新引用(非「全库扫描目录差集」作为主真相)。外链图片 media_assets不参与 孤儿删除。

4. 数据模型

4.1 media_assets(名称实现时可微调,语义为「用户上传的站内媒体文件」)

字段 说明
id 主键
userId 所有者;所有查询与删除必须 按用户隔离
storageKey 相对存储根的路径或稳定键,与对外 URL 可逆映射(与现 public/assets 约定对齐)
mime 存储的主资源 MIME(优化后多为 image/webp
sizeBytes 主资源字节数
sha256 可选;用于去重或完整性
variantsJson 可选;记录衍生路径,如 { "original": "...", "webp": "..." },具体键名实现计划锁定
status 可选首版:ready;若上异步优化则 processing | ready | failed
createdAt 创建时间(用于宽限期)

首版post_media_refs 解析与 GC 以 canonical 对外 URLmedia_assets 主行一致为准;若存在 variantsJson,在实现计划中明确 哪一条为 Markdown 插入的默认 URL,避免同一逻辑资产被拆成多条无法追踪的记录。

4.2 post_media_refs

  • 多对多:(postId, assetId),带唯一约束。
  • 主路径:每次 post 创建/更新成功 后,根据当前 bodyMarkdown + coverUrl 解析出 本站前缀 下的图片 URL,解析为 assetId替换 该 post 的整行关联集合(实现可采用先删后插或 diff,以事务保证一致)。

真相来源:关联表为引用真相; 以 post 上冗余「URL 缓存列」作为主真相。

5. 引用解析规则

  • 范围coverUrl + bodyMarkdown 中出现的图片 URL。
  • 识别本站资源:与当前静态资源约定一致的前缀(例如 /public/assets/);实现计划锁定正则或解析器,避免误把外链当站内。
  • 外链:忽略,不创建、不删除 media_assets

6. 孤儿回收(A)

6.0 原则(已定)

  • 默认不自动删除:系统 不得 在无人操作的情况下静默删除孤儿文件;须先满足「可删除」条件,且仅在 自动清理已开启 时由定时任务执行删除。
  • 可审查:提供固定入口(见 6.5)列出 候选孤儿(含预览信息、体积、createdAt、成为孤儿的时间等),供用户确认。
  • 可配置自动清理:通过配置项 显式开启 后,定时任务才对「已过宽限期的候选」执行与手动相同的删除逻辑。
  • 可手动清理:在审查界面 按条删除批量删除当前筛选结果(实现计划定义二次确认文案与权限)。

6.1 孤儿定义

media_assets 中某行 不存在 任何 post_media_refs.assetId 指向它,且满足用户隔离。

6.2 宽限期(删除前置条件)

以下时刻起满宽限期后的记录,才进入 「可删除」 集合(审查列表可展示「未过宽限期」与「可删」状态,避免与自动任务语义混淆)。

场景 规则
从未被任何 post 引用 createdAt 起满 T 小时后才 可删T 默认 24,实现为常量或环境变量
曾被引用、保存后引用被移除 最后一次取消全部引用 起满 T2 后才 可删T2 默认 24 小时,可与 T 相同或独立配置

「最后一次取消全部引用」以实现为准:可在 ref 从非空变为空时更新 assetOrphanSince(需额外表字段),或在任务中根据 ref 不存在推断;实现计划必须选定一种可测试策略,避免误删编辑中未保存会话(数据库 ref 滞后是预期,由 T/T2 兜底)。

未过宽限期的孤儿 仍可在审查列表中展示(标注「冷却中」),但 不可 被手动或自动删除。

6.3 删除动作(手动与自动共用)

可删除 集合中的资产:

  1. 再次确认无 post_media_refs 引用(防竞态)。
  2. 删除磁盘(或对象存储)上 storageKeyvariantsJson 中列出的衍生文件。
  3. 删除 media_assets 行(首版硬删;若需审计可后续加软删或日志表,非首版必做)。

顺序与失败:单机首版可采用「删文件 → 删行」;若删文件失败应记录日志且 删行,便于重试。禁止删除仍被某 post_media_refs 引用的行。

6.4 自动清理配置与定时任务

配置项 约定
自动清理总开关 默认关闭。仅当开启时,定时任务才执行 6.3 的删除。配置载体首选用 环境变量(如 MEDIA_ORPHAN_AUTO_SWEEP_ENABLED=false),若项目已有全局配置表/管理后台,实现计划可合并为同一真相源。
执行周期 可配置(如每小时);默认仅在开关开启时注册调度。
范围 自动任务以 系统内部 job 身份运行,枚举 全站 满足「孤儿 + 已过宽限期」的 media_assets,对每一条调用与 手动删除 相同的 6.3 例程(含引用重检、用户隔离不得误删他人文件)。审查页仅展示 当前登录用户 自己的候选;不得 因「仅我的列表」而限制自动任务的全站职责(多租户下每行仍有 userId)。

关闭自动清理时:定时任务 不跑删除,或空转仅写指标;审查与手动删除 不受影响

6.5 审查入口(首版必做)

  • 位置:实现计划从 /me/... 中选具体路径(例如「文章」下子页或「设置」子区块),与现有 me 布局一致。
  • 能力:分页列表候选孤儿(可按「全部 / 仅可删 / 冷却中」筛选);展示缩略图或 URL、sizeBytescreatedAt、孤儿时长、宽限期状态。
  • 手动操作:单条删除、多选删除、(可选)「删除当前筛选下全部可删项」——均需 二次确认,且仅对 可删除 行生效。
  • APIGET 列表、POST 批量删除(body 含 assetIds 或「按筛选删除」的显式 flag),权限与 第 10 节 一致。

6.6 与「仅列表不删除」的观测

  • 可选:定时任务在 自动关闭 时仍记录 可删数量 到日志或指标,便于运维;非首版必做

7. 图片优化(C)

7.1 首版能力

  • 上传后:解码限最大宽度(默认 1920px,实现计划可配置)→ 输出 WebP(质量默认 82,可配置)。
  • Markdown 与上传 API 返回的 url 指向优化后的主资源(路径规则与现 public/assets 命名兼容或可迁移)。
  • 是否保留原图:首版 可不保留(仅 WebP),以减少复杂度与存储;若产品要求可回滚原图,则在 variantsJson 中保留 original 键并在实现计划中写清磁盘布局。

7.2 二期(非首版验收)

  • 多宽度衍生(w800 / w1200)与渲染侧 srcset / <picture>
  • 异步处理队列与 processing 状态(首版若同步优化在 10MB 内可接受则不必上队列)。

7.3 CDN

  • 首版无 CDN 前缀;DB 存相对 path 或稳定 storageKey,便于日后只改对外域名。

8. 上传接口演进

  • POST /api/file/upload:在写磁盘 同时 插入 media_assetsuserId 从会话取,与现站认证一致)。
  • 响应仍兼容现有 { files: [{ name, url, path, mimeType, size }, ...] }url 为插入 Markdown 的 canonical 地址;size/mimeType 反映 主存储资源(优化后)。
  • 实现计划需处理:优化失败时的回退(拒绝上传并提示 vs 保留原图),首版推荐 失败则整笔上传失败并 Toast,不留下半条记录。

9. Post 服务与事务

  • createPost / updatePost 成功提交正文后,在同一请求内或紧随其后的可靠步骤 调用「根据 markdown + cover 刷新 post_media_refs」。
  • 若刷新失败:实现计划定义 是否回滚整次保存重试队列;首版推荐 与 post 保存同事务或先 post 后 ref 且可重试修复,避免长期 ref 与正文不一致。

10. 安全与隔离

  • 所有 media_assetspost_media_refs 的读删改必须 校验 post 属于当前 user 且 asset 属于当前 user
  • GC 仅处理本应用创建的 storageKey 模式,禁止 删除不匹配前缀的路径。

11. 非目标(本 spec)

  • 非图片附件(PDF、ZIP)与完整「媒体库」浏览 UI(审查页仅面向孤儿候选,不是通用资源管理器)。
  • 第三方图床配置、用户级 CDN 绑定。
  • 跨实例分布式锁(单节点部署假设;多实例时需在实现计划中另加对象存储与锁,非本期必写)。

12. 测试与验收(实现阶段)

  • 上传 → DB 有 media_assets;保存带图文章 → post_media_refs 正确;删图改文保存 → ref 更新。
  • 默认:自动清理关闭时,即使存在可删孤儿,定时任务不得删除 磁盘或行。
  • 审查页:列表与筛选正确;冷却中项不可删;可删项可单条/批量手动删除,且二次确认生效。
  • 自动清理开启:仅对已过宽限期且无引用的记录执行删除,逻辑与手动删除一致。
  • 宽限期内 允许手动或自动删除刚上传未引用文件。
  • 大图超宽被限宽;输出为 WebP;体积小于原图(典型 JPEG/PNG 样例)。
  • 外链图片 URL 出现在正文中 → 无 DB 行、不被误删。

13. 后续流程

实现前另建 writing-plans 实现计划(迁移、上传与优化、引用同步、/me 审查页与删除 API、自动清理配置与可选 cron、Sharp 依赖与错误处理、与现有编辑器契约)。