refactor: 完全重构为 Anki 兼容数据结构
- 用 Deck 替换 Folder - 用 Note + Card 替换 Pair (双向复习) - 添加 NoteType (卡片模板) - 添加 Revlog (复习历史) - 实现 SM-2 间隔重复算法 - 更新所有前端页面 - 添加数据库迁移
This commit is contained in:
@@ -1,18 +1,69 @@
|
||||
"use server";
|
||||
|
||||
import { executeOCR } from "@/lib/bigmodel/ocr/orchestrator";
|
||||
import { repoCreatePair, repoGetUserIdByFolderId } from "@/modules/folder/folder-repository";
|
||||
import { repoGetUserIdByDeckId } from "@/modules/deck/deck-repository";
|
||||
import { repoCreateNote, repoJoinFields } from "@/modules/note/note-repository";
|
||||
import { repoCreateCard } from "@/modules/card/card-repository";
|
||||
import { repoGetNoteTypesByUserId, repoCreateNoteType } from "@/modules/note-type/note-type-repository";
|
||||
import { auth } from "@/auth";
|
||||
import { headers } from "next/headers";
|
||||
import { createLogger } from "@/lib/logger";
|
||||
import type { ServiceInputProcessOCR, ServiceOutputProcessOCR } from "./ocr-service-dto";
|
||||
import { NoteKind } from "../../../generated/prisma/enums";
|
||||
|
||||
const log = createLogger("ocr-service");
|
||||
|
||||
const VOCABULARY_NOTE_TYPE_NAME = "Vocabulary (OCR)";
|
||||
|
||||
async function getOrCreateVocabularyNoteType(userId: string): Promise<number> {
|
||||
const existingTypes = await repoGetNoteTypesByUserId({ userId });
|
||||
const existing = existingTypes.find((nt) => nt.name === VOCABULARY_NOTE_TYPE_NAME);
|
||||
|
||||
if (existing) {
|
||||
return existing.id;
|
||||
}
|
||||
|
||||
const fields = [
|
||||
{ name: "Word", ord: 0, sticky: false, rtl: false, font: "Arial", size: 20, media: [] },
|
||||
{ name: "Definition", ord: 1, sticky: false, rtl: false, font: "Arial", size: 20, media: [] },
|
||||
{ name: "Source Language", ord: 2, sticky: false, rtl: false, font: "Arial", size: 16, media: [] },
|
||||
{ name: "Target Language", ord: 3, sticky: false, rtl: false, font: "Arial", size: 16, media: [] },
|
||||
];
|
||||
|
||||
const templates = [
|
||||
{
|
||||
name: "Word → Definition",
|
||||
ord: 0,
|
||||
qfmt: "{{Word}}",
|
||||
afmt: "{{FrontSide}}<hr id=answer>{{Definition}}",
|
||||
},
|
||||
{
|
||||
name: "Definition → Word",
|
||||
ord: 1,
|
||||
qfmt: "{{Definition}}",
|
||||
afmt: "{{FrontSide}}<hr id=answer>{{Word}}",
|
||||
},
|
||||
];
|
||||
|
||||
const css = ".card { font-family: Arial; font-size: 20px; text-align: center; color: black; background-color: white; }";
|
||||
|
||||
const noteTypeId = await repoCreateNoteType({
|
||||
name: VOCABULARY_NOTE_TYPE_NAME,
|
||||
kind: NoteKind.STANDARD,
|
||||
css,
|
||||
fields,
|
||||
templates,
|
||||
userId,
|
||||
});
|
||||
|
||||
log.info("Created vocabulary note type", { noteTypeId, userId });
|
||||
return noteTypeId;
|
||||
}
|
||||
|
||||
export async function serviceProcessOCR(
|
||||
input: ServiceInputProcessOCR
|
||||
): Promise<ServiceOutputProcessOCR> {
|
||||
log.info("Processing OCR request", { folderId: input.folderId });
|
||||
log.info("Processing OCR request", { deckId: input.deckId });
|
||||
|
||||
const session = await auth.api.getSession({ headers: await headers() });
|
||||
if (!session?.user?.id) {
|
||||
@@ -20,15 +71,15 @@ export async function serviceProcessOCR(
|
||||
return { success: false, message: "Unauthorized" };
|
||||
}
|
||||
|
||||
const folderOwner = await repoGetUserIdByFolderId(input.folderId);
|
||||
if (folderOwner !== session.user.id) {
|
||||
log.warn("Folder ownership mismatch", {
|
||||
folderId: input.folderId,
|
||||
const deckOwner = await repoGetUserIdByDeckId(input.deckId);
|
||||
if (deckOwner !== session.user.id) {
|
||||
log.warn("Deck ownership mismatch", {
|
||||
deckId: input.deckId,
|
||||
userId: session.user.id
|
||||
});
|
||||
return {
|
||||
success: false,
|
||||
message: "You don't have permission to modify this folder"
|
||||
message: "You don't have permission to modify this deck"
|
||||
};
|
||||
}
|
||||
|
||||
@@ -59,19 +110,38 @@ export async function serviceProcessOCR(
|
||||
const sourceLanguage = ocrResult.detectedSourceLanguage || input.sourceLanguage || "Unknown";
|
||||
const targetLanguage = ocrResult.detectedTargetLanguage || input.targetLanguage || "Unknown";
|
||||
|
||||
const noteTypeId = await getOrCreateVocabularyNoteType(session.user.id);
|
||||
|
||||
let pairsCreated = 0;
|
||||
for (const pair of ocrResult.pairs) {
|
||||
try {
|
||||
await repoCreatePair({
|
||||
folderId: input.folderId,
|
||||
language1: sourceLanguage,
|
||||
language2: targetLanguage,
|
||||
text1: pair.word,
|
||||
text2: pair.definition,
|
||||
const now = Date.now();
|
||||
const noteId = await repoCreateNote({
|
||||
noteTypeId,
|
||||
userId: session.user.id,
|
||||
fields: [pair.word, pair.definition, sourceLanguage, targetLanguage],
|
||||
tags: ["ocr"],
|
||||
});
|
||||
|
||||
await repoCreateCard({
|
||||
id: BigInt(now + pairsCreated),
|
||||
noteId,
|
||||
deckId: input.deckId,
|
||||
ord: 0,
|
||||
due: pairsCreated + 1,
|
||||
});
|
||||
|
||||
await repoCreateCard({
|
||||
id: BigInt(now + pairsCreated + 10000),
|
||||
noteId,
|
||||
deckId: input.deckId,
|
||||
ord: 1,
|
||||
due: pairsCreated + 1,
|
||||
});
|
||||
|
||||
pairsCreated++;
|
||||
} catch (error) {
|
||||
log.error("Failed to create pair", {
|
||||
log.error("Failed to create note/card", {
|
||||
word: pair.word,
|
||||
error
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user