fix: 删书彻底清理文件与DB + 阅读位置跨设备同步

删除不彻底的根因是库里 file_path 存了旧 Windows 相对路径(uploads\xxx.epub),Linux 容器里 os.path.exists 永远为假:

- 新增 _upload_file() 归一化路径(取文件名挂到当前 uploads 目录),读/删都走它,老库迁移的文件也能定位;文件删除挪到后台任务,失败不影响接口

- 删除后对 SQLite 执行 VACUUM,让 db 文件真正缩小

- 删除时一并清理 reading_progress

阅读位置原来只存 localStorage,换设备丢失:

- 新增 reading_progress 表 + GET/PUT /books/{id}/progress 接口

- 前端打开时优先取服务端位置(本地兜底并双端拉齐),翻页防抖同步到服务端,关闭时把防抖窗口内的最后一页立即刷上去
This commit is contained in:
2026-10-27 21:42:04 +08:00
parent 5febc02099
commit 956323007d
6 changed files with 136 additions and 16 deletions
+21 -2
View File
@@ -184,7 +184,16 @@ export default function EpubReader({ book, onClose, onAsk }: {
setToc(entries);
setFontSize(initialFont);
const host = viewerRef.current!;
const savedCfi = parseSavedCfi(readSaved(positionKey));
// 阅读位置优先取服务端(跨设备同步),本地 localStorage 只做离线兜底
const localCfi = parseSavedCfi(readSaved(positionKey));
let savedCfi = localCfi;
try {
const progress = await api.getReadingProgress(book.id);
if (progress?.cfi) {
savedCfi = progress.cfi;
if (localCfi !== progress.cfi) save(positionKey, progress.cfi); // 双端拉齐
}
} catch { /* 拉取失败不阻断打开,用本地位置 */ }
restoringPositionRef.current = true;
cfiRef.current = savedCfi;
setContinuous(false);
@@ -255,7 +264,12 @@ export default function EpubReader({ book, onClose, onAsk }: {
cfiRef.current = start.cfi;
if (restoringPositionRef.current) return;
window.clearTimeout(saveTimer);
saveTimer = window.setTimeout(() => { if (!disposed) save(positionKey, start.cfi!); }, 300);
saveTimer = window.setTimeout(() => {
if (disposed) return;
save(positionKey, start.cfi!);
// 同步到服务端(跨设备);失败静默,下次 relocate 会再试
void api.saveReadingProgress(book.id, start.cfi!).catch(() => {});
}, 300);
});
rendition.on("selected", (cfi: string, contents: Contents) => {
if (disposed) return;
@@ -299,6 +313,11 @@ export default function EpubReader({ book, onClose, onAsk }: {
return () => {
disposed = true;
controller.abort();
// 关书页时把防抖窗口里的位置立即刷到服务端,避免「最后一页丢失」
if (cfiRef.current && !restoringPositionRef.current) {
save(positionKey, cfiRef.current);
void api.saveReadingProgress(book.id, cfiRef.current).catch(() => {});
}
window.clearTimeout(saveTimer);
observer?.disconnect();
renditionRef.current = null;
+10
View File
@@ -27,6 +27,12 @@ export interface ReadingNote {
updated_at: string;
}
export interface ReadingProgress {
book_id: string;
cfi: string;
updated_at: string;
}
export interface Chapter {
id: string;
book_id: string;
@@ -161,6 +167,10 @@ export const api = {
return res.json() as Promise<Book>;
},
deleteBook: (id: string) => request<{ deleted: string }>(`/books/${id}`, { method: "DELETE" }),
// 阅读位置(跨设备同步):服务端存最近位置,本地 localStorage 只做离线兜底
getReadingProgress: (bookId: string) => request<ReadingProgress | null>(`/books/${bookId}/progress`),
saveReadingProgress: (bookId: string, cfi: string) =>
request<ReadingProgress>(`/books/${bookId}/progress`, { method: "PUT", body: JSON.stringify({ cfi }) }),
// Conversations
listConversations: (bookId?: string, kind?: string) => {