这是一份可公开阅读的架构记录,说明一张图片如何从订阅源进入媒体库,最终出现在网页照片墙中。
为保护部署环境,本文使用逻辑路径和组件名称,不包含服务器地址、账号、本机绝对路径、内部端口、密钥或具体模型配置。
一句话总览
图片订阅源
│
▼
RSS 图片摄取插件 ── 下载 / 识别 / 校验 ──► 共享媒体存储
│ │
└──────────── 写入 SQLite 元数据 ◄──────┘
│
▼
缩略图与 manifest
│
▼
浏览器 ◄── 照片墙 API ◄── Next.js 前端
整条链路坚持三个原则:
- 插件负责生产,前端只读消费。
- 数据库保存“图片是什么”,媒体目录保存“图片文件在哪里”。
- 原图、缩略图、数据库任意一层异常时,页面都应尽量降级,而不是直接崩溃。
重构前为什么容易出问题
早期版本可以工作,但数据契约比较脆弱:
- 一个作品的多张图片路径和哈希塞在 JSON 数组中,数组错位后难以判断每张图属于谁。
- 下载临时文件位于公开照片目录内,处理未完成的文件也可能被目录扫描发现。
- 数据库记录、磁盘文件与缩略图之间缺少统一健康检查。
- 摄取失败时,临时文件可能残留;缩略图并发刷新也可能互相覆盖。
- 数据库不可用时,页面虽然能扫描文件夹,但前端对降级状态缺少清晰约定。
- 观看数和收藏数只在首次摄取时写入,之后不会变化。
- 部署脚本可能在新版本尚未验证时就停止旧服务。
这次重构没有改变“RSS → 插件 → 数据库与媒体目录 → API → 前端”的大方向,而是把每个边界变得更明确、可检查和可恢复。
各组件的职责
| 组件 | 职责 | 是否写数据 |
|---|---|---|
| RSS 聚合服务 | 把榜单或订阅内容转换为 RSS/Atom | 否 |
| 图片摄取插件 | 拉取条目、下载图片、识别内容、归档、写库 | 是 |
| 共享媒体存储 | 保存原图与相册目录 | 是,由插件写入 |
| SQLite 图片库 | 保存作品级和图片级元数据 | 是,由插件写入 |
| 缩略图生成器 | 生成 WebP 缩略图和索引清单 | 是 |
| 照片墙 API | 只读数据库和缩略图清单,组装前端数据 | 否 |
| 照片墙页面 | 展示、筛选、排序和查看详情 | 否 |
| 健康检查与修复工具 | 检查一致性;在明确授权时修复 | 仅修复模式写入 |
摄取阶段:一张图如何进入系统
拉取订阅
插件按配置周期读取多个 RSS 源。当前运行配置中,摄取循环每 30 分钟执行一次。
每个条目会解析出作品标识、标题、作者、原始链接、图片 URL、标签、摘要、观看数、收藏数、发布时间和来源。过长的多图条目会在下载前被跳过,避免把长篇漫画误当作普通插画批量摄取。
先登记作品,再处理图片
新作品先以 pending 状态写入数据库,并取得一个单调递增的展示编号。这个编号用于稳定排序和文件命名,不依赖文件修改时间。
pending ──处理成功──► ready
│
└──处理失败──► failed ──重试──► ready
ready ──文件被明确删除──► deleted
deleted 会保留作品标识,防止下一轮订阅又把用户主动删除的图片重新摄取回来。
私有暂存区
下载中的文件先进入公开照片墙目录之外的暂存区,而且每个作品使用独立运行子目录。这样未下载完成的文件不会被前端扫描,并发任务也不会碰撞临时文件名。
插件会为每张图片计算 SHA-256,并保留原始来源 URL。图片下载完成后才交给内容识别流程,得到简短描述、内容分类和必要的展示元数据。
原子归档与失败回滚
识别成功后,插件把同一作品的图片从暂存区移动到最终相册目录,再一次性提交数据库中的 ready 状态。
- 目标文件已存在时拒绝覆盖;
- 同批次任意移动失败时,已移动文件会回滚;
- 数据库尚未成功提交时发生异常,会清理本轮创建的文件;
- 清理操作只能触及暂存区或照片墙根目录。
因此,前端看到的 ready 应代表“元数据已完成、原图已落盘、逐图记录已建立”。
数据库 v7:从作品数组改成逐图记录
SQLite 仍是插件与前端之间的数据契约,但 schema 已升级到 v7。
items:作品级数据
一行代表一个作品或一组相关图片,保存标题、作者、来源、标签、原始链接、描述、分类、热度、展示编号以及摄取和推送状态。
兼容旧版本的路径、哈希和来源 URL 数组暂时保留,目的是支持迁移和回滚,而不是继续把它当作唯一真相。
item_images:图片级数据
这是重构的核心。一张图片对应一行,主键是“作品标识 + 图片序号”,保存:
- 原始图片 URL;
- 本地路径和相对存储键;
- SHA-256;
- MIME 类型、宽高、文件大小和修改时间;
- 当前状态。
这样即使一个作品有多张图,也能独立判断某一张图是否丢失、错位或需要重新生成缩略图。
photowall_data_issues:待决定问题
无法安全自动修复的问题会进入单独的问题表。既没有可信来源链接、又无法用哈希唯一匹配的历史图片不会被强行猜测或覆盖,而是保持原状等待人工决定。
观看数和收藏数如何更新
热度数据不再只在首次摄取时写死。
插件会复用摄取轮次已经读取到的 RSS 快照,按配置每 60 分钟同步一次已入库作品的观看数与收藏数。同一作品如果同时出现在多个订阅中,本轮先合并重复项,再使用该轮可见的较高计数。
这一步只更新热度字段,不会改变图片路径、摄取状态或推送状态。它依赖订阅源仍能返回该作品,因此适合更新近期仍在订阅窗口中的内容,不等同于对所有历史作品永久实时轮询。
缩略图流水线
照片墙一次可能展示很多图片,列表使用 WebP 缩略图,打开详情时再请求原图。
只要本轮有新作品进入 ready,插件就会在后台请求一次缩略图刷新。连续到达的请求会合并,生成器本身还有进程锁:
- 同一时间只允许一个实例写缩略图目录;
- 过期锁会被识别并清理;
- 生成中的文件先写临时名,成功后再原子替换;
- manifest 没有变化时不重复改写;
- 已失去来源的旧缩略图会被清理。
manifest 按原图公开 URL 索引,记录缩略图 URL 和原图宽高。URL 附带基于修改时间的版本参数,让浏览器缓存随文件变化自动失效。
API 只有在缩略图 URL、宽度和高度都有效时,才把图片加入正常展示集,避免破图进入页面。
照片墙 API 如何组织数据
前端通过 GET /api/photowall 获取数据。API 使用只读方式打开 SQLite,并要求数据库至少为 schema v7。
读取顺序是:
- 优先读取
item_images中状态为ready的逐图记录; - 历史作品还没有逐图记录时,临时回退到旧路径数组;
- 检查原图是否真实存在;
- 检查 manifest 是否有可用缩略图;
- 合并作品元数据、推送文案、图片尺寸和公开 URL。
API 会生成真实文件夹相册、“所有图片”总览、来源与分类虚拟相册、收藏与观看热度视图,以及筛选所需的统计数据。
相册总览只返回少量预览图,完整照片流只传一次;请求特定相册时也可以按相册 ID 单独返回,减少重复数据。
三级降级
正常:SQLite + 原图 + 缩略图 manifest
↓ 数据库不可用
降级:扫描照片墙文件夹 + manifest
↓ 共享存储也不可用
兜底:前端内置的静态相册数据
降级响应不缓存,正常响应使用短时间私有缓存并允许后台重新验证。
浏览器端如何展示
导航栏在用户准备进入照片墙时会提前请求一次 API。同一页面生命周期内,共享请求会避免多个组件重复拉取数据;请求失败时缓存会清空,下一次仍可重试。
页面拿到数据后在浏览器内完成排序、来源与内容筛选、关键词搜索、两种视图切换和分批渲染。Hero 会根据收藏、观看和内容分类计算不同展示组并定时轮换。
因此,当热度同步后某张图超过原 Hero 第一名,用户下一次取得新 API 数据时,Hero 会重新排序。已经长时间打开的页面不会主动轮询,需要重新进入或刷新。
新图多久能在页面看到
等待下一轮 RSS 摄取(最多约 30 分钟)
+ 下载、识别和入库
+ 缩略图生成
+ API 短缓存或页面刷新
所以新图通常会在一次摄取周期之后出现。插件刚完成缩略图刷新时,重新进入照片墙最容易取得最新结果;已经打开的旧页面需要刷新。
健康检查、修复与备份
统一健康检查默认只读,会验证共享存储、公开媒体映射、SQLite 完整性和 schema、登记文件、缩略图、manifest、暂存残留、待决定问题、API 数据源,以及抽样原图和缩略图访问。
深度模式还会逐张计算 SHA-256,适合维护或迁移后使用。
修复工具默认先做 dry-run 并输出报告。真正写入前必须完成数据库备份,变更过的图片也会另存备份。自动修复只处理能够通过可信来源或唯一哈希确认的情况;来源丢失严重、无法唯一确认归属的图片只登记问题,不自动处理。
安全部署顺序
新的生产启动流程遵循“先验证新版本,再停止旧版本”:
- 检查运行环境与共享存储挂载;
- 清理数据库中已经不存在的照片路径;
- 生成并校验缩略图;
- 执行离线健康检查;
- 构建 production bundle;
- 前五步全部成功后,才停止旧服务;
- 启动新服务并等待可访问;
- 执行在线照片墙检查。
任一步失败都会中止流程。构建或离线检查失败时,旧服务仍保持运行,避免一次更新直接造成网站离线。
常见故障定位
新图没有出现
依次检查:作品是否进入 ready、逐图记录是否存在、原图是否落盘、缩略图是否生成、manifest 是否更新、API 是否仍在短缓存中。
页面有相册但缺少元数据
这通常意味着 API 进入了扫描文件夹降级模式。图片还能显示,但作者、来源、标签和热度等数据库字段不可用。
图片变成 404
检查共享存储挂载、公开媒体映射、数据库本地路径和磁盘文件是否一致。不要直接修改数据库路径来猜测文件归属。
热度没有变化
先确认热度同步已启用,再确认作品仍存在于订阅快照中。历史作品离开订阅窗口后,不保证继续更新。
重构带来的实际收益
- 每张图片都有独立、可校验的身份,不再只依赖多个平行 JSON 数组。
- 下载中间态与公开目录隔离,失败任务不会污染照片墙。
- 文件移动、缩略图和 manifest 都采用临时文件加原子替换。
- 插件、生成器和部署流程都有并发保护。
- 数据库损坏、存储暂时不可用时,页面仍有明确降级路径。
- 观看和收藏数据能够随订阅快照继续更新。
- 健康检查把数据库、文件、缩略图、API 串成了一套可重复验证的流程。
- 自动修复有备份、有报告,也明确知道哪些数据不应该擅自处理。
这套方案并不追求把所有组件做成复杂服务,而是让一个个人网站的图片管线具备足够清晰的边界、可恢复性和日常可维护性。
