refactor: 完全重构为 Anki 兼容数据结构
- 用 Deck 替换 Folder - 用 Note + Card 替换 Pair (双向复习) - 添加 NoteType (卡片模板) - 添加 Revlog (复习历史) - 实现 SM-2 间隔重复算法 - 更新所有前端页面 - 添加数据库迁移
This commit is contained in:
327
src/modules/deck/deck-action.ts
Normal file
327
src/modules/deck/deck-action.ts
Normal file
@@ -0,0 +1,327 @@
|
||||
"use server";
|
||||
|
||||
import { auth } from "@/auth";
|
||||
import { headers } from "next/headers";
|
||||
import { createLogger } from "@/lib/logger";
|
||||
import { ValidateError } from "@/lib/errors";
|
||||
import { Visibility } from "../../../generated/prisma/enums";
|
||||
import {
|
||||
ActionInputCreateDeck,
|
||||
ActionInputUpdateDeck,
|
||||
ActionInputDeleteDeck,
|
||||
ActionInputGetDeckById,
|
||||
ActionInputGetPublicDecks,
|
||||
ActionInputSearchPublicDecks,
|
||||
ActionInputGetPublicDeckById,
|
||||
ActionInputToggleDeckFavorite,
|
||||
ActionInputCheckDeckFavorite,
|
||||
ActionOutputCreateDeck,
|
||||
ActionOutputUpdateDeck,
|
||||
ActionOutputDeleteDeck,
|
||||
ActionOutputGetDeckById,
|
||||
ActionOutputGetDecksByUserId,
|
||||
ActionOutputGetPublicDecks,
|
||||
ActionOutputDeck,
|
||||
ActionOutputPublicDeck,
|
||||
ActionOutputSearchPublicDecks,
|
||||
ActionOutputGetPublicDeckById,
|
||||
ActionOutputToggleDeckFavorite,
|
||||
ActionOutputCheckDeckFavorite,
|
||||
ActionOutputGetUserFavoriteDecks,
|
||||
validateActionInputCreateDeck,
|
||||
validateActionInputUpdateDeck,
|
||||
validateActionInputDeleteDeck,
|
||||
validateActionInputGetDeckById,
|
||||
validateActionInputGetPublicDecks,
|
||||
validateActionInputSearchPublicDecks,
|
||||
validateActionInputGetPublicDeckById,
|
||||
validateActionInputToggleDeckFavorite,
|
||||
validateActionInputCheckDeckFavorite,
|
||||
} from "./deck-action-dto";
|
||||
import {
|
||||
serviceCreateDeck,
|
||||
serviceUpdateDeck,
|
||||
serviceDeleteDeck,
|
||||
serviceGetDeckById,
|
||||
serviceGetDecksByUserId,
|
||||
serviceGetPublicDecks,
|
||||
serviceCheckOwnership,
|
||||
serviceSearchPublicDecks,
|
||||
serviceGetPublicDeckById,
|
||||
serviceToggleDeckFavorite,
|
||||
serviceCheckDeckFavorite,
|
||||
serviceGetUserFavoriteDecks,
|
||||
} from "./deck-service";
|
||||
|
||||
const log = createLogger("deck-action");
|
||||
|
||||
async function checkDeckOwnership(deckId: number): Promise<boolean> {
|
||||
const session = await auth.api.getSession({ headers: await headers() });
|
||||
if (!session?.user?.id) return false;
|
||||
return serviceCheckOwnership({ deckId, userId: session.user.id });
|
||||
}
|
||||
|
||||
export async function actionCreateDeck(input: ActionInputCreateDeck): Promise<ActionOutputCreateDeck> {
|
||||
try {
|
||||
const session = await auth.api.getSession({ headers: await headers() });
|
||||
if (!session?.user?.id) {
|
||||
return { success: false, message: "Unauthorized" };
|
||||
}
|
||||
|
||||
const validatedInput = validateActionInputCreateDeck(input);
|
||||
const result = await serviceCreateDeck({
|
||||
name: validatedInput.name,
|
||||
desc: validatedInput.desc,
|
||||
userId: session.user.id,
|
||||
visibility: validatedInput.visibility as Visibility | undefined,
|
||||
});
|
||||
|
||||
return result;
|
||||
} catch (e) {
|
||||
if (e instanceof ValidateError) {
|
||||
return { success: false, message: e.message };
|
||||
}
|
||||
log.error("Failed to create deck", { error: e });
|
||||
return { success: false, message: "Unknown error occurred" };
|
||||
}
|
||||
}
|
||||
|
||||
export async function actionUpdateDeck(input: ActionInputUpdateDeck): Promise<ActionOutputUpdateDeck> {
|
||||
try {
|
||||
const validatedInput = validateActionInputUpdateDeck(input);
|
||||
|
||||
const isOwner = await checkDeckOwnership(validatedInput.deckId);
|
||||
if (!isOwner) {
|
||||
return { success: false, message: "You do not have permission to update this deck" };
|
||||
}
|
||||
|
||||
return serviceUpdateDeck({
|
||||
deckId: validatedInput.deckId,
|
||||
name: validatedInput.name,
|
||||
desc: validatedInput.desc,
|
||||
visibility: validatedInput.visibility as Visibility | undefined,
|
||||
collapsed: validatedInput.collapsed,
|
||||
});
|
||||
} catch (e) {
|
||||
if (e instanceof ValidateError) {
|
||||
return { success: false, message: e.message };
|
||||
}
|
||||
log.error("Failed to update deck", { error: e });
|
||||
return { success: false, message: "Unknown error occurred" };
|
||||
}
|
||||
}
|
||||
|
||||
export async function actionDeleteDeck(input: ActionInputDeleteDeck): Promise<ActionOutputDeleteDeck> {
|
||||
try {
|
||||
const validatedInput = validateActionInputDeleteDeck(input);
|
||||
|
||||
const isOwner = await checkDeckOwnership(validatedInput.deckId);
|
||||
if (!isOwner) {
|
||||
return { success: false, message: "You do not have permission to delete this deck" };
|
||||
}
|
||||
|
||||
return serviceDeleteDeck({ deckId: validatedInput.deckId });
|
||||
} catch (e) {
|
||||
if (e instanceof ValidateError) {
|
||||
return { success: false, message: e.message };
|
||||
}
|
||||
log.error("Failed to delete deck", { error: e });
|
||||
return { success: false, message: "Unknown error occurred" };
|
||||
}
|
||||
}
|
||||
|
||||
export async function actionGetDeckById(input: ActionInputGetDeckById): Promise<ActionOutputGetDeckById> {
|
||||
try {
|
||||
const validatedInput = validateActionInputGetDeckById(input);
|
||||
const result = await serviceGetDeckById({ deckId: validatedInput.deckId });
|
||||
|
||||
if (!result.success || !result.data) {
|
||||
return { success: false, message: result.message };
|
||||
}
|
||||
|
||||
return {
|
||||
success: true,
|
||||
message: result.message,
|
||||
data: {
|
||||
...result.data,
|
||||
visibility: result.data.visibility as "PRIVATE" | "PUBLIC",
|
||||
},
|
||||
};
|
||||
} catch (e) {
|
||||
log.error("Failed to get deck", { error: e });
|
||||
return { success: false, message: "Unknown error occurred" };
|
||||
}
|
||||
}
|
||||
|
||||
export async function actionGetDecksByUserId(userId: string): Promise<ActionOutputGetDecksByUserId> {
|
||||
try {
|
||||
const result = await serviceGetDecksByUserId({ userId });
|
||||
|
||||
if (!result.success || !result.data) {
|
||||
return { success: false, message: result.message };
|
||||
}
|
||||
|
||||
return {
|
||||
success: true,
|
||||
message: result.message,
|
||||
data: result.data.map((deck) => ({
|
||||
...deck,
|
||||
visibility: deck.visibility as "PRIVATE" | "PUBLIC",
|
||||
})),
|
||||
};
|
||||
} catch (e) {
|
||||
log.error("Failed to get decks", { error: e });
|
||||
return { success: false, message: "Unknown error occurred" };
|
||||
}
|
||||
}
|
||||
|
||||
export async function actionGetPublicDecks(input: ActionInputGetPublicDecks = {}): Promise<ActionOutputGetPublicDecks> {
|
||||
try {
|
||||
const validatedInput = validateActionInputGetPublicDecks(input);
|
||||
const result = await serviceGetPublicDecks(validatedInput);
|
||||
|
||||
if (!result.success || !result.data) {
|
||||
return { success: false, message: result.message };
|
||||
}
|
||||
|
||||
return {
|
||||
success: true,
|
||||
message: result.message,
|
||||
data: result.data.map((deck) => ({
|
||||
...deck,
|
||||
visibility: deck.visibility as "PRIVATE" | "PUBLIC",
|
||||
})),
|
||||
};
|
||||
} catch (e) {
|
||||
if (e instanceof ValidateError) {
|
||||
return { success: false, message: e.message };
|
||||
}
|
||||
log.error("Failed to get public decks", { error: e });
|
||||
return { success: false, message: "Unknown error occurred" };
|
||||
}
|
||||
}
|
||||
|
||||
export async function actionGetPublicDeckById(input: ActionInputGetPublicDeckById): Promise<ActionOutputGetPublicDeckById> {
|
||||
try {
|
||||
const validatedInput = validateActionInputGetPublicDeckById(input);
|
||||
const result = await serviceGetPublicDeckById(validatedInput);
|
||||
|
||||
if (!result.success || !result.data) {
|
||||
return { success: false, message: result.message };
|
||||
}
|
||||
|
||||
return {
|
||||
success: true,
|
||||
message: result.message,
|
||||
data: {
|
||||
...result.data,
|
||||
visibility: result.data.visibility as "PRIVATE" | "PUBLIC",
|
||||
},
|
||||
};
|
||||
} catch (e) {
|
||||
if (e instanceof ValidateError) {
|
||||
return { success: false, message: e.message };
|
||||
}
|
||||
log.error("Failed to get public deck", { error: e });
|
||||
return { success: false, message: "Unknown error occurred" };
|
||||
}
|
||||
}
|
||||
|
||||
export async function actionSearchPublicDecks(input: ActionInputSearchPublicDecks): Promise<ActionOutputSearchPublicDecks> {
|
||||
try {
|
||||
const validatedInput = validateActionInputSearchPublicDecks(input);
|
||||
const result = await serviceSearchPublicDecks(validatedInput);
|
||||
|
||||
if (!result.success || !result.data) {
|
||||
return { success: false, message: result.message };
|
||||
}
|
||||
|
||||
return {
|
||||
success: true,
|
||||
message: result.message,
|
||||
data: result.data.map((deck) => ({
|
||||
...deck,
|
||||
visibility: deck.visibility as "PRIVATE" | "PUBLIC",
|
||||
})),
|
||||
};
|
||||
} catch (e) {
|
||||
if (e instanceof ValidateError) {
|
||||
return { success: false, message: e.message };
|
||||
}
|
||||
log.error("Failed to search public decks", { error: e });
|
||||
return { success: false, message: "Unknown error occurred" };
|
||||
}
|
||||
}
|
||||
|
||||
export async function actionToggleDeckFavorite(input: ActionInputToggleDeckFavorite): Promise<ActionOutputToggleDeckFavorite> {
|
||||
try {
|
||||
const session = await auth.api.getSession({ headers: await headers() });
|
||||
if (!session?.user?.id) {
|
||||
return { success: false, message: "Unauthorized" };
|
||||
}
|
||||
|
||||
const validatedInput = validateActionInputToggleDeckFavorite(input);
|
||||
const result = await serviceToggleDeckFavorite({
|
||||
deckId: validatedInput.deckId,
|
||||
userId: session.user.id,
|
||||
});
|
||||
|
||||
return result;
|
||||
} catch (e) {
|
||||
if (e instanceof ValidateError) {
|
||||
return { success: false, message: e.message };
|
||||
}
|
||||
log.error("Failed to toggle deck favorite", { error: e });
|
||||
return { success: false, message: "Unknown error occurred" };
|
||||
}
|
||||
}
|
||||
|
||||
export async function actionCheckDeckFavorite(input: ActionInputCheckDeckFavorite): Promise<ActionOutputCheckDeckFavorite> {
|
||||
try {
|
||||
const session = await auth.api.getSession({ headers: await headers() });
|
||||
if (!session?.user?.id) {
|
||||
return { success: true, message: "Not logged in", data: { isFavorited: false, favoriteCount: 0 } };
|
||||
}
|
||||
|
||||
const validatedInput = validateActionInputCheckDeckFavorite(input);
|
||||
const result = await serviceCheckDeckFavorite({
|
||||
deckId: validatedInput.deckId,
|
||||
userId: session.user.id,
|
||||
});
|
||||
|
||||
return result;
|
||||
} catch (e) {
|
||||
if (e instanceof ValidateError) {
|
||||
return { success: false, message: e.message };
|
||||
}
|
||||
log.error("Failed to check deck favorite", { error: e });
|
||||
return { success: false, message: "Unknown error occurred" };
|
||||
}
|
||||
}
|
||||
|
||||
export async function actionGetUserFavoriteDecks(): Promise<ActionOutputGetUserFavoriteDecks> {
|
||||
try {
|
||||
const session = await auth.api.getSession({ headers: await headers() });
|
||||
if (!session?.user?.id) {
|
||||
return { success: false, message: "Unauthorized" };
|
||||
}
|
||||
|
||||
const result = await serviceGetUserFavoriteDecks(session.user.id);
|
||||
|
||||
if (!result.success || !result.data) {
|
||||
return { success: false, message: result.message };
|
||||
}
|
||||
|
||||
return {
|
||||
success: true,
|
||||
message: result.message,
|
||||
data: result.data.map((deck) => ({
|
||||
...deck,
|
||||
visibility: deck.visibility as "PRIVATE" | "PUBLIC",
|
||||
})),
|
||||
};
|
||||
} catch (e) {
|
||||
log.error("Failed to get user favorite decks", { error: e });
|
||||
return { success: false, message: "Unknown error occurred" };
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user