diff --git a/src/app/(features)/dictionary/SearchResult.tsx b/src/app/(features)/dictionary/SearchResult.tsx index cf351a8..9c6846b 100644 --- a/src/app/(features)/dictionary/SearchResult.tsx +++ b/src/app/(features)/dictionary/SearchResult.tsx @@ -1,18 +1,19 @@ import { Plus, RefreshCw } from "lucide-react"; import { toast } from "sonner"; import { authClient } from "@/lib/auth-client"; -import { Folder } from "../../../../generated/prisma/browser"; import { DictionaryEntry } from "./DictionaryEntry"; import { useTranslations } from "next-intl"; import { performDictionaryLookup } from "./utils"; import { TSharedItem } from "@/shared"; +import { TSharedFolder } from "@/shared/folder-type"; +import { actionCreatePair } from "@/modules/folder"; interface SearchResultProps { searchResult: TSharedItem; searchQuery: string; queryLang: string; definitionLang: string; - folders: Folder[]; + folders: TSharedFolder[]; selectedFolderId: number | null; onFolderSelect: (folderId: number | null) => void; onResultUpdate: (newResult: TSharedItem) => void; @@ -66,12 +67,12 @@ export function SearchResult({ } const entry = searchResult.entries[0]; - createPair({ + actionCreatePair({ text1: searchResult.standardForm, text2: entry.definition, language1: queryLang, language2: definitionLang, - ipa1: isDictWordResponse(searchResult) && (entry as DictWordEntry).ipa ? (entry as DictWordEntry).ipa : undefined, + ipa1: entry.ipa, folderId: selectedFolderId, }) .then(() => { diff --git a/src/app/(features)/dictionary/page.tsx b/src/app/(features)/dictionary/page.tsx index 1a9c1cd..877b887 100644 --- a/src/app/(features)/dictionary/page.tsx +++ b/src/app/(features)/dictionary/page.tsx @@ -3,13 +3,15 @@ import { useState, useEffect } from "react"; import Container from "@/components/ui/Container"; import { authClient } from "@/lib/auth-client"; -import { Folder } from "../../../../generated/prisma/browser"; import { SearchForm } from "./SearchForm"; import { SearchResult } from "./SearchResult"; import { useTranslations } from "next-intl"; import { POPULAR_LANGUAGES } from "./constants"; import { performDictionaryLookup } from "./utils"; import { TSharedItem } from "@/shared"; +import { actionGetFoldersByUserId } from "@/modules/folder"; +import { TSharedFolder } from "@/shared/folder-type"; +import { toast } from "sonner"; export default function DictionaryPage() { const t = useTranslations("dictionary"); @@ -20,20 +22,24 @@ export default function DictionaryPage() { const [queryLang, setQueryLang] = useState("english"); const [definitionLang, setDefinitionLang] = useState("chinese"); const [selectedFolderId, setSelectedFolderId] = useState(null); - const [folders, setFolders] = useState([]); + const [folders, setFolders] = useState([]); const { data: session } = authClient.useSession(); // 加载用户的文件夹列表 useEffect(() => { if (session) { - getFoldersByUserId(session.user.id as string) + actionGetFoldersByUserId(session.user.id as string) + .then(result => { + if (!result.success || !result.data) throw result.message; + return result.data; + }) .then((loadedFolders) => { setFolders(loadedFolders); // 如果有文件夹且未选择,默认选择第一个 if (loadedFolders.length > 0 && !selectedFolderId) { setSelectedFolderId(loadedFolders[0].id); } - }); + }).catch(e => toast.error); } }, [session, selectedFolderId]); diff --git a/src/app/(features)/dictionary/utils.ts b/src/app/(features)/dictionary/utils.ts index 857f77c..ab7bde2 100644 --- a/src/app/(features)/dictionary/utils.ts +++ b/src/app/(features)/dictionary/utils.ts @@ -1,14 +1,14 @@ import { toast } from "sonner"; -import { lookUpDictionaryAction } from "@/modules/dictionary/dictionary-action"; -import { DictionaryActionInputDto, DictionaryActionOutputDto } from "@/modules/dictionary"; +import { actionLookUpDictionary } from "@/modules/dictionary/dictionary-action"; +import { ActionInputLookUpDictionary, ActionOutputLookUpDictionary } from "@/modules/dictionary"; import { TSharedItem } from "@/shared"; export async function performDictionaryLookup( - options: DictionaryActionInputDto, + options: ActionInputLookUpDictionary, t?: (key: string) => string ): Promise { const { text, queryLang, definitionLang, forceRelook = false, userId } = options; - const result = await lookUpDictionaryAction({ + const result = await actionLookUpDictionary({ text, queryLang, definitionLang, diff --git a/src/app/auth/AuthForm.tsx b/src/app/auth/AuthForm.tsx index 00a9706..b79c29a 100644 --- a/src/app/auth/AuthForm.tsx +++ b/src/app/auth/AuthForm.tsx @@ -2,11 +2,11 @@ import { useState, useActionState, startTransition } from "react"; import { useTranslations } from "next-intl"; -import { signInAction, signUpAction, SignUpState } from "@/modules/user/user-action"; import Container from "@/components/ui/Container"; import Input from "@/components/ui/Input"; import { LightButton } from "@/components/ui/buttons"; import { authClient } from "@/lib/auth-client"; +import { signInAction, signUpAction, SignUpState } from "@/modules/auth"; interface AuthFormProps { redirectTo?: string; @@ -225,7 +225,7 @@ export default function AuthForm({ redirectTo }: AuthFormProps) { className="w-full mt-4 py-2 flex items-center justify-center gap-2" > - + {t(mode === 'signin' ? 'signInWithGitHub' : 'signUpWithGitHub')} diff --git a/src/app/folders/FoldersClient.tsx b/src/app/folders/FoldersClient.tsx index f3e4fe4..000e781 100644 --- a/src/app/folders/FoldersClient.tsx +++ b/src/app/folders/FoldersClient.tsx @@ -8,23 +8,17 @@ import { Trash2, } from "lucide-react"; import { useEffect, useState } from "react"; -import { logger } from "@/lib/logger"; import { useRouter } from "next/navigation"; -import { Folder } from "../../../generated/prisma/browser"; -import { - createFolder, - deleteFolderById, - getFoldersWithTotalPairsByUserId, - renameFolderById, -} from "@/lib/server/services/folderService"; import { useTranslations } from "next-intl"; import { toast } from "sonner"; import PageLayout from "@/components/ui/PageLayout"; import PageHeader from "@/components/ui/PageHeader"; import CardList from "@/components/ui/CardList"; +import { actionCreateFolder, actionDeleteFolderById, actionGetFoldersWithTotalPairsByUserId, actionRenameFolderById } from "@/modules/folder"; +import { TSharedFolderWithTotalPairs } from "@/shared/folder-type"; interface FolderProps { - folder: Folder & { total: number }; + folder: TSharedFolderWithTotalPairs; refresh: () => void; } @@ -62,7 +56,15 @@ const FolderCard = ({ folder, refresh }: FolderProps) => { e.stopPropagation(); const newName = prompt("Input a new name.")?.trim(); if (newName && newName.length > 0) { - renameFolderById(folder.id, newName).then(refresh); + actionRenameFolderById(folder.id, newName) + .then(result => { + if (result.success) { + refresh(); + } + else { + toast.error(result.message); + } + }); } }} className="p-2 text-gray-400 hover:text-gray-600 hover:bg-gray-100 rounded-lg transition-colors" @@ -74,7 +76,15 @@ const FolderCard = ({ folder, refresh }: FolderProps) => { e.stopPropagation(); const confirm = prompt(t("confirmDelete", { name: folder.name })); if (confirm === folder.name) { - deleteFolderById(folder.id).then(refresh); + actionDeleteFolderById(folder.id) + .then(result => { + if (result.success) { + refresh(); + } + else { + toast.error(result.message); + } + }); } }} className="p-2 text-gray-400 hover:text-red-500 hover:bg-red-50 rounded-lg transition-colors" @@ -87,33 +97,37 @@ const FolderCard = ({ folder, refresh }: FolderProps) => { ); }; -export default function FoldersClient({ userId }: { userId: string }) { +export default function FoldersClient({ userId }: { userId: string; }) { const t = useTranslations("folders"); - const [folders, setFolders] = useState<(Folder & { total: number })[]>( + const [folders, setFolders] = useState( [], ); const [loading, setLoading] = useState(false); useEffect(() => { setLoading(true); - getFoldersWithTotalPairsByUserId(userId) + actionGetFoldersWithTotalPairsByUserId(userId) .then((folders) => { - setFolders(folders); - setLoading(false); - }) - .catch((error) => { - logger.error("加载文件夹失败", error); - toast.error("加载出错,请重试。"); + if (folders.success && folders.data) { + setFolders(folders.data); + setLoading(false); + } }); }, [userId]); const updateFolders = async () => { - try { - const updatedFolders = await getFoldersWithTotalPairsByUserId(userId); - setFolders(updatedFolders); - } catch (error) { - logger.error("更新文件夹失败", error); - } + setLoading(true); + await actionGetFoldersWithTotalPairsByUserId(userId) + .then(async result => { + if (!result.success) toast.error(result.message); + else await actionGetFoldersWithTotalPairsByUserId(userId) + .then((folders) => { + if (folders.success && folders.data) { + setFolders(folders.data); + } + }); + }); + setLoading(false); }; return ( @@ -127,11 +141,14 @@ export default function FoldersClient({ userId }: { userId: string }) { if (!folderName) return; setLoading(true); try { - await createFolder({ - name: folderName, - userId: userId, - }); - await updateFolders(); + await actionCreateFolder(userId, folderName) + .then(result => { + if (result.success) { + updateFolders(); + } else { + toast.error(result.message); + } + }); } finally { setLoading(false); } diff --git a/src/app/folders/[folder_id]/InFolder.tsx b/src/app/folders/[folder_id]/InFolder.tsx index 40e8e22..68901f5 100644 --- a/src/app/folders/[folder_id]/InFolder.tsx +++ b/src/app/folders/[folder_id]/InFolder.tsx @@ -3,30 +3,20 @@ import { ArrowLeft, Plus } from "lucide-react"; import { useEffect, useState } from "react"; import { redirect, useRouter } from "next/navigation"; -import { - createPair, - deletePairById, - getPairsByFolderId, -} from "@/lib/server/services/pairService"; import AddTextPairModal from "./AddTextPairModal"; import TextPairCard from "./TextPairCard"; import { useTranslations } from "next-intl"; import PageLayout from "@/components/ui/PageLayout"; import { GreenButton } from "@/components/ui/buttons"; -import { logger } from "@/lib/logger"; import { IconButton } from "@/components/ui/buttons"; import CardList from "@/components/ui/CardList"; +import { actionCreatePair, actionDeletePairById, actionGetPairsByFolderId } from "@/modules/folder"; +import { TSharedPair } from "@/shared/folder-type"; +import { toast } from "sonner"; -export interface TextPair { - id: number; - text1: string; - text2: string; - language1: string; - language2: string; -} -export default function InFolder({ folderId }: { folderId: number }) { - const [textPairs, setTextPairs] = useState([]); +export default function InFolder({ folderId }: { folderId: number; }) { + const [textPairs, setTextPairs] = useState([]); const [loading, setLoading] = useState(true); const [openAddModal, setAddModal] = useState(false); const router = useRouter(); @@ -35,25 +25,26 @@ export default function InFolder({ folderId }: { folderId: number }) { useEffect(() => { const fetchTextPairs = async () => { setLoading(true); - try { - const data = await getPairsByFolderId(folderId); - setTextPairs(data as TextPair[]); - } catch (error) { - logger.error("获取文本对失败", error); - } finally { - setLoading(false); - } + await actionGetPairsByFolderId(folderId) + .then(result => { + if (!result.success || !result.data) throw result.message; + return result.data; + }).then(setTextPairs) + .catch(toast.error) + .finally(() => { + setLoading(false); + }); }; fetchTextPairs(); }, [folderId]); const refreshTextPairs = async () => { - try { - const data = await getPairsByFolderId(folderId); - setTextPairs(data as TextPair[]); - } catch (error) { - logger.error("获取文本对失败", error); - } + await actionGetPairsByFolderId(folderId) + .then(result => { + if (!result.success || !result.data) throw result.message; + return result.data; + }).then(setTextPairs) + .catch(toast.error); }; return ( @@ -123,8 +114,11 @@ export default function InFolder({ folderId }: { folderId: number }) { key={textPair.id} textPair={textPair} onDel={() => { - deletePairById(textPair.id); - refreshTextPairs(); + actionDeletePairById(textPair.id) + .then(result => { + if (!result.success) throw result.message; + }).then(refreshTextPairs) + .catch(toast.error); }} refreshTextPairs={refreshTextPairs} /> @@ -143,7 +137,7 @@ export default function InFolder({ folderId }: { folderId: number }) { language1: string, language2: string, ) => { - await createPair({ + await actionCreatePair({ text1: text1, text2: text2, language1: language1, @@ -155,4 +149,4 @@ export default function InFolder({ folderId }: { folderId: number }) { /> ); -} +}; diff --git a/src/app/folders/[folder_id]/TextPairCard.tsx b/src/app/folders/[folder_id]/TextPairCard.tsx index c3e394e..a23ac90 100644 --- a/src/app/folders/[folder_id]/TextPairCard.tsx +++ b/src/app/folders/[folder_id]/TextPairCard.tsx @@ -1,13 +1,13 @@ import { Edit, Trash2 } from "lucide-react"; -import { TextPair } from "./InFolder"; -import { updatePairById } from "@/lib/server/services/pairService"; import { useState } from "react"; import UpdateTextPairModal from "./UpdateTextPairModal"; import { useTranslations } from "next-intl"; -import { UpdatePairInput } from "@/lib/server/services/types"; +import { TSharedPair } from "@/shared/folder-type"; +import { actionUpdatePairById, ActionInputUpdatePairById } from "@/modules/folder"; +import { toast } from "sonner"; interface TextPairCardProps { - textPair: TextPair; + textPair: TSharedPair; onDel: () => void; refreshTextPairs: () => void; } @@ -66,8 +66,8 @@ export default function TextPairCard({ setOpenUpdateModal(false)} - onUpdate={async (id: number, data: UpdatePairInput) => { - await updatePairById(id, data); + onUpdate={async (id: number, data: ActionInputUpdatePairById) => { + await actionUpdatePairById(id, data).then(result => result.success ? toast.success(result.message) : toast.error(result.message)); setOpenUpdateModal(false); refreshTextPairs(); }} diff --git a/src/app/folders/[folder_id]/UpdateTextPairModal.tsx b/src/app/folders/[folder_id]/UpdateTextPairModal.tsx index 1720845..6a4be55 100644 --- a/src/app/folders/[folder_id]/UpdateTextPairModal.tsx +++ b/src/app/folders/[folder_id]/UpdateTextPairModal.tsx @@ -3,15 +3,15 @@ import Input from "@/components/ui/Input"; import { LocaleSelector } from "@/components/ui/LocaleSelector"; import { X } from "lucide-react"; import { useRef, useState } from "react"; -import { UpdatePairInput } from "@/lib/server/services/types"; -import { TextPair } from "./InFolder"; import { useTranslations } from "next-intl"; +import { TSharedPair } from "@/shared/folder-type"; +import { ActionInputUpdatePairById } from "@/modules/folder"; interface UpdateTextPairModalProps { isOpen: boolean; onClose: () => void; - textPair: TextPair; - onUpdate: (id: number, tp: UpdatePairInput) => void; + textPair: TSharedPair; + onUpdate: (id: number, tp: ActionInputUpdatePairById) => void; } export default function UpdateTextPairModal({ diff --git a/src/app/folders/[folder_id]/page.tsx b/src/app/folders/[folder_id]/page.tsx index 99258b3..3e75005 100644 --- a/src/app/folders/[folder_id]/page.tsx +++ b/src/app/folders/[folder_id]/page.tsx @@ -1,9 +1,9 @@ import { redirect } from "next/navigation"; import { getTranslations } from "next-intl/server"; import InFolder from "./InFolder"; -import { getUserIdByFolderId } from "@/lib/server/services/folderService"; import { auth } from "@/auth"; import { headers } from "next/headers"; +import { actionGetUserIdByFolderId } from "@/modules/folder"; export default async function FoldersPage({ params, }: { @@ -17,7 +17,7 @@ export default async function FoldersPage({ redirect("/folders"); } if (!session) redirect(`/auth?redirect=/folders/${folder_id}`); - if ((await getUserIdByFolderId(Number(folder_id))) !== session.user.id) { + if ((await actionGetUserIdByFolderId(Number(folder_id))).data !== session.user.id) { return

{t("unauthorized")}

; } return ; diff --git a/src/lib/bigmodel/dictionary/orchestrator.ts b/src/lib/bigmodel/dictionary/orchestrator.ts index b720787..3c61d64 100644 --- a/src/lib/bigmodel/dictionary/orchestrator.ts +++ b/src/lib/bigmodel/dictionary/orchestrator.ts @@ -1,4 +1,4 @@ -import { LookUpServiceOutputDto } from "@/modules/dictionary/dictionary-service-dto"; +import { ServiceOutputLookUp } from "@/modules/dictionary/dictionary-service-dto"; import { analyzeInput } from "./stage1-inputAnalysis"; import { determineSemanticMapping } from "./stage2-semanticMapping"; import { generateStandardForm } from "./stage3-standardForm"; @@ -9,7 +9,7 @@ export async function executeDictionaryLookup( text: string, queryLang: string, definitionLang: string -): Promise { +): Promise { try { // ========== 阶段 1:输入分析 ========== console.log("[阶段1] 开始输入分析..."); @@ -74,7 +74,7 @@ export async function executeDictionaryLookup( console.log("[阶段4] 词条生成完成:", entriesResult); // ========== 组装最终结果 ========== - const finalResult: LookUpServiceOutputDto = { + const finalResult: ServiceOutputLookUp = { standardForm: standardFormResult.standardForm, entries: entriesResult.entries, }; diff --git a/src/modules/user/user-action.ts b/src/modules/auth/auth-action.ts similarity index 96% rename from src/modules/user/user-action.ts rename to src/modules/auth/auth-action.ts index 737b777..7712fc4 100644 --- a/src/modules/user/user-action.ts +++ b/src/modules/auth/auth-action.ts @@ -67,6 +67,9 @@ export async function signUpAction(prevState: SignUpState, formData: FormData) { redirect(redirectTo || "/"); } catch (error) { + if (error instanceof Error && error.message.includes('NEXT_REDIRECT')) { + throw error; + } return { success: false, message: "注册失败,请稍后再试" diff --git a/src/modules/auth/index.ts b/src/modules/auth/index.ts new file mode 100644 index 0000000..9cf54ed --- /dev/null +++ b/src/modules/auth/index.ts @@ -0,0 +1 @@ +export * from './auth-action'; \ No newline at end of file diff --git a/src/modules/dictionary/dictionary-action-dto.ts b/src/modules/dictionary/dictionary-action-dto.ts index 881a4af..17d21a1 100644 --- a/src/modules/dictionary/dictionary-action-dto.ts +++ b/src/modules/dictionary/dictionary-action-dto.ts @@ -1,29 +1,21 @@ -import { ValidateError } from "@/lib/errors"; import { TSharedItem } from "@/shared"; +import { LENGTH_MAX_DICTIONARY_TEXT, LENGTH_MAX_LANGUAGE, LENGTH_MIN_DICTIONARY_TEXT, LENGTH_MIN_LANGUAGE } from "@/shared/constant"; +import { generateValidator } from "@/utils/validate"; import z from "zod"; -const DictionaryActionInputDtoSchema = z.object({ - text: z.string().min(1, 'Empty text.').max(30, 'Text too long.'), - queryLang: z.string().min(1, 'Query lang too short.').max(20, 'Query lang too long.'), +const schemaActionInputLookUpDictionary = z.object({ + text: z.string().min(LENGTH_MIN_DICTIONARY_TEXT).max(LENGTH_MAX_DICTIONARY_TEXT), + queryLang: z.string().min(LENGTH_MIN_LANGUAGE).max(LENGTH_MAX_LANGUAGE), forceRelook: z.boolean(), - definitionLang: z.string().min(1, 'Definition lang too short.').max(20, 'Definition lang too long.'), + definitionLang: z.string().min(LENGTH_MIN_LANGUAGE).max(LENGTH_MAX_LANGUAGE), userId: z.string().optional() }); -export type DictionaryActionInputDto = z.infer; +export type ActionInputLookUpDictionary = z.infer; -export const validateDictionaryActionInput = (dto: DictionaryActionInputDto): DictionaryActionInputDto => { - const result = DictionaryActionInputDtoSchema.safeParse(dto); - if (result.success) return result.data; +export const validateActionInputLookUpDictionary = generateValidator(schemaActionInputLookUpDictionary); - const errorMessages = result.error.issues.map((issue) => - `${issue.path.join('.')}: ${issue.message}` - ).join('; '); - - throw new ValidateError(`Validation failed: ${errorMessages}`); -}; - -export type DictionaryActionOutputDto = { +export type ActionOutputLookUpDictionary = { message: string, success: boolean; data?: TSharedItem; diff --git a/src/modules/dictionary/dictionary-action.ts b/src/modules/dictionary/dictionary-action.ts index c508d2a..4d389be 100644 --- a/src/modules/dictionary/dictionary-action.ts +++ b/src/modules/dictionary/dictionary-action.ts @@ -1,15 +1,15 @@ "use server"; -import { DictionaryActionInputDto, DictionaryActionOutputDto, validateDictionaryActionInput } from "./dictionary-action-dto"; +import { ActionInputLookUpDictionary, ActionOutputLookUpDictionary, validateActionInputLookUpDictionary } from "./dictionary-action-dto"; import { ValidateError } from "@/lib/errors"; -import { lookUpService } from "./dictionary-service"; +import { serviceLookUp } from "./dictionary-service"; -export const lookUpDictionaryAction = async (dto: DictionaryActionInputDto): Promise => { +export const actionLookUpDictionary = async (dto: ActionInputLookUpDictionary): Promise => { try { return { message: 'success', success: true, - data: await lookUpService(validateDictionaryActionInput(dto)) + data: await serviceLookUp(validateActionInputLookUpDictionary(dto)) }; } catch (e) { if (e instanceof ValidateError) { diff --git a/src/modules/dictionary/dictionary-repository-dto.ts b/src/modules/dictionary/dictionary-repository-dto.ts index add7e7a..ad97e3d 100644 --- a/src/modules/dictionary/dictionary-repository-dto.ts +++ b/src/modules/dictionary/dictionary-repository-dto.ts @@ -1,6 +1,6 @@ import { TSharedItem } from "@/shared"; -export type CreateDictionaryLookUpInputDto = { +export type RepoInputCreateDictionaryLookUp = { userId?: string; text: string; queryLang: string; @@ -8,15 +8,15 @@ export type CreateDictionaryLookUpInputDto = { dictionaryItemId?: number; }; -export type SelectLastLookUpResultOutputDto = TSharedItem & {id: number} | null; +export type RepoOutputSelectLastLookUpResult = TSharedItem & {id: number} | null; -export type CreateDictionaryItemInputDto = { +export type RepoInputCreateDictionaryItem = { standardForm: string; queryLang: string; definitionLang: string; }; -export type CreateDictionaryEntryInputDto = { +export type RepoInputCreateDictionaryEntry = { itemId: number; ipa?: string; definition: string; @@ -24,14 +24,14 @@ export type CreateDictionaryEntryInputDto = { example: string; }; -export type CreateDictionaryEntryWithoutItemIdInputDto = { +export type RepoInputCreateDictionaryEntryWithoutItemId = { ipa?: string; definition: string; partOfSpeech?: string; example: string; }; -export type SelectLastLookUpResultInputDto = { +export type RepoInputSelectLastLookUpResult = { text: string, queryLang: string, definitionLang: string; diff --git a/src/modules/dictionary/dictionary-repository.ts b/src/modules/dictionary/dictionary-repository.ts index fb41555..dcbcf5c 100644 --- a/src/modules/dictionary/dictionary-repository.ts +++ b/src/modules/dictionary/dictionary-repository.ts @@ -1,15 +1,15 @@ import { stringNormalize } from "@/utils/string"; import { - CreateDictionaryEntryInputDto, - CreateDictionaryEntryWithoutItemIdInputDto, - CreateDictionaryItemInputDto, - CreateDictionaryLookUpInputDto, - SelectLastLookUpResultInputDto, - SelectLastLookUpResultOutputDto, + RepoInputCreateDictionaryEntry, + RepoInputCreateDictionaryEntryWithoutItemId, + RepoInputCreateDictionaryItem, + RepoInputCreateDictionaryLookUp, + RepoInputSelectLastLookUpResult, + RepoOutputSelectLastLookUpResult, } from "./dictionary-repository-dto"; import prisma from "@/lib/db"; -export async function selectLastLookUpResult(dto: SelectLastLookUpResultInputDto): Promise { +export async function repoSelectLastLookUpResult(dto: RepoInputSelectLastLookUpResult): Promise { const result = await prisma.dictionaryLookUp.findFirst({ where: { normalizedText: stringNormalize(dto.text), @@ -48,16 +48,16 @@ export async function selectLastLookUpResult(dto: SelectLastLookUpResultInputDto return null; } -export async function createLookUp(content: CreateDictionaryLookUpInputDto) { +export async function repoCreateLookUp(content: RepoInputCreateDictionaryLookUp) { return (await prisma.dictionaryLookUp.create({ data: { ...content, normalizedText: stringNormalize(content.text) } })).id; } -export async function createLookUpWithItemAndEntries( - itemData: CreateDictionaryItemInputDto, - lookUpData: CreateDictionaryLookUpInputDto, - entries: CreateDictionaryEntryWithoutItemIdInputDto[] +export async function repoCreateLookUpWithItemAndEntries( + itemData: RepoInputCreateDictionaryItem, + lookUpData: RepoInputCreateDictionaryLookUp, + entries: RepoInputCreateDictionaryEntryWithoutItemId[] ) { return await prisma.$transaction(async (tx) => { const item = await tx.dictionaryItem.create({ diff --git a/src/modules/dictionary/dictionary-service-dto.ts b/src/modules/dictionary/dictionary-service-dto.ts index bc90218..eecd2ee 100644 --- a/src/modules/dictionary/dictionary-service-dto.ts +++ b/src/modules/dictionary/dictionary-service-dto.ts @@ -1,6 +1,6 @@ import { TSharedItem } from "@/shared"; -export type LookUpServiceInputDto = { +export type ServiceInputLookUp = { text: string, queryLang: string, definitionLang: string, @@ -8,4 +8,4 @@ export type LookUpServiceInputDto = { userId?: string; }; -export type LookUpServiceOutputDto = TSharedItem; +export type ServiceOutputLookUp = TSharedItem; diff --git a/src/modules/dictionary/dictionary-service.ts b/src/modules/dictionary/dictionary-service.ts index 5591e5a..864fc18 100644 --- a/src/modules/dictionary/dictionary-service.ts +++ b/src/modules/dictionary/dictionary-service.ts @@ -1,8 +1,8 @@ import { executeDictionaryLookup } from "@/lib/bigmodel/dictionary"; -import { createLookUp, createLookUpWithItemAndEntries, selectLastLookUpResult } from "./dictionary-repository"; -import { LookUpServiceInputDto } from "./dictionary-service-dto"; +import { repoCreateLookUp, repoCreateLookUpWithItemAndEntries, repoSelectLastLookUpResult } from "./dictionary-repository"; +import { ServiceInputLookUp } from "./dictionary-service-dto"; -export const lookUpService = async (dto: LookUpServiceInputDto) => { +export const serviceLookUp = async (dto: ServiceInputLookUp) => { const { text, queryLang, @@ -11,7 +11,7 @@ export const lookUpService = async (dto: LookUpServiceInputDto) => { forceRelook } = dto; - const lastLookUpResult = await selectLastLookUpResult({ + const lastLookUpResult = await repoSelectLastLookUpResult({ text, queryLang, definitionLang, @@ -25,7 +25,7 @@ export const lookUpService = async (dto: LookUpServiceInputDto) => { ); // 使用事务确保数据一致性 - createLookUpWithItemAndEntries( + repoCreateLookUpWithItemAndEntries( { standardForm: response.standardForm, queryLang, @@ -44,7 +44,7 @@ export const lookUpService = async (dto: LookUpServiceInputDto) => { return response; } else { - createLookUp({ + repoCreateLookUp({ userId: userId, text: text, queryLang: queryLang, diff --git a/src/modules/folder/folder-aciton.ts b/src/modules/folder/folder-aciton.ts index e69de29..5b001a0 100644 --- a/src/modules/folder/folder-aciton.ts +++ b/src/modules/folder/folder-aciton.ts @@ -0,0 +1,202 @@ +"use server"; + +import { ValidateError } from "@/lib/errors"; +import { ActionInputCreatePair, ActionInputUpdatePairById, ActionOutputGetFoldersWithTotalPairsByUserId, validateActionInputCreatePair, validateActionInputUpdatePairById } from "./folder-action-dto"; +import { repoCreateFolder, repoCreatePair, repoDeleteFolderById, repoDeletePairById, repoGetFoldersByUserId, repoGetFoldersWithTotalPairsByUserId, repoGetPairsByFolderId, repoGetUserIdByFolderId, repoRenameFolderById, repoUpdatePairById } from "./folder-repository"; +import { validate } from "@/utils/validate"; +import z from "zod"; +import { LENGTH_MAX_FOLDER_NAME, LENGTH_MIN_FOLDER_NAME } from "@/shared/constant"; + +export async function actionGetPairsByFolderId(folderId: number) { + try { + return { + success: true, + message: 'success', + data: await repoGetPairsByFolderId(folderId) + }; + } catch (e) { + console.log(e); + return { + success: false, + message: 'Unknown error occured.' + }; + } +} + +export async function actionUpdatePairById(id: number, dto: ActionInputUpdatePairById) { + try { + const validatedDto = validateActionInputUpdatePairById(dto); + await repoUpdatePairById(id, validatedDto); + return { + success: true, + message: 'success', + }; + } catch (e) { + console.log(e); + return { + success: false, + message: 'Unknown error occured.' + }; + } +} + +export async function actionGetUserIdByFolderId(folderId: number) { + try { + return { + success: true, + message: 'success', + data: await repoGetUserIdByFolderId(folderId) + }; + } catch (e) { + console.log(e); + return { + success: false, + message: 'Unknown error occured.' + }; + } +} + +export async function actionDeleteFolderById(folderId: number) { + try { + await repoDeleteFolderById(folderId); + return { + success: true, + message: 'success', + }; + } catch (e) { + console.log(e); + return { + success: false, + message: 'Unknown error occured.' + }; + } +} + +export async function actionDeletePairById(id: number) { + try { + await repoDeletePairById(id); + return { + success: true, + message: 'success' + }; + } catch (e) { + console.log(e); + return { + success: false, + message: 'Unknown error occured.' + }; + } +} + +export async function actionGetFoldersWithTotalPairsByUserId(id: string): Promise { + try { + return { + success: true, + message: 'success', + data: await repoGetFoldersWithTotalPairsByUserId(id) + }; + } catch (e) { + console.log(e); + return { + success: false, + message: 'Unknown error occured.' + }; + } +} + +export async function actionGetFoldersByUserId(userId: string) { + try { + return { + success: true, + message: 'success', + data: await repoGetFoldersByUserId(userId) + }; + } catch (e) { + console.log(e); + return { + success: false, + message: 'Unknown error occured.' + }; + } +} + +export async function actionCreatePair(dto: ActionInputCreatePair) { + try { + const validatedDto = validateActionInputCreatePair(dto); + await repoCreatePair(validatedDto); + return { + success: true, + message: 'success' + }; + } catch (e) { + if (e instanceof ValidateError) { + return { + success: false, + message: e.message + }; + } + console.log(e); + return { + success: false, + message: 'Unknown error occured.' + }; + } +} + +export async function actionCreateFolder(userId: string, folderName: string) { + try { + const validatedFolderName = validate(folderName, + z.string() + .trim() + .min(LENGTH_MIN_FOLDER_NAME) + .max(LENGTH_MAX_FOLDER_NAME)); + await repoCreateFolder({ + name: validatedFolderName, + userId: userId + }); + return { + success: true, + message: 'success' + }; + } catch (e) { + if (e instanceof ValidateError) { + return { + success: false, + message: e.message + }; + } + console.log(e); + return { + success: false, + message: 'Unknown error occured.' + }; + } +} + +export async function actionRenameFolderById(id: number, newName: string) { + try { + const validatedNewName = validate( + newName, + z.string() + .min(LENGTH_MIN_FOLDER_NAME) + .max(LENGTH_MAX_FOLDER_NAME) + .trim()); + await repoRenameFolderById(id, validatedNewName); + return { + success: true, + message: 'success' + }; + } catch (e) { + if (e instanceof ValidateError) { + return { + success: false, + message: e.message + }; + } + console.log(e); + return { + success: false, + message: 'Unknown error occured.' + }; + } +} diff --git a/src/modules/folder/folder-action-dto.ts b/src/modules/folder/folder-action-dto.ts index e69de29..6566e02 100644 --- a/src/modules/folder/folder-action-dto.ts +++ b/src/modules/folder/folder-action-dto.ts @@ -0,0 +1,34 @@ +import { LENGTH_MAX_FOLDER_NAME, LENGTH_MAX_IPA, LENGTH_MAX_LANGUAGE, LENGTH_MAX_PAIR_TEXT, LENGTH_MIN_FOLDER_NAME, LENGTH_MIN_IPA, LENGTH_MIN_LANGUAGE, LENGTH_MIN_PAIR_TEXT } from "@/shared/constant"; +import { TSharedFolderWithTotalPairs } from "@/shared/folder-type"; +import { generateValidator } from "@/utils/validate"; +import z from "zod"; + +export const schemaActionInputCreatePair = z.object({ + text1: z.string().min(LENGTH_MIN_PAIR_TEXT).max(LENGTH_MAX_PAIR_TEXT), + text2: z.string().min(LENGTH_MIN_PAIR_TEXT).max(LENGTH_MAX_PAIR_TEXT), + language1: z.string().min(LENGTH_MIN_LANGUAGE).max(LENGTH_MAX_LANGUAGE), + language2: z.string().min(LENGTH_MIN_LANGUAGE).max(LENGTH_MAX_LANGUAGE), + ipa1: z.string().min(LENGTH_MIN_IPA).max(LENGTH_MAX_IPA).optional(), + ipa2: z.string().min(LENGTH_MIN_IPA).max(LENGTH_MAX_IPA).optional(), + folderId: z.int() +}); +export type ActionInputCreatePair = z.infer; +export const validateActionInputCreatePair = generateValidator(schemaActionInputCreatePair); + +export const schemaActionInputUpdatePairById = z.object({ + text1: z.string().min(LENGTH_MIN_PAIR_TEXT).max(LENGTH_MAX_PAIR_TEXT).optional(), + text2: z.string().min(LENGTH_MIN_PAIR_TEXT).max(LENGTH_MAX_PAIR_TEXT).optional(), + language1: z.string().min(LENGTH_MIN_LANGUAGE).max(LENGTH_MAX_LANGUAGE).optional(), + language2: z.string().min(LENGTH_MIN_LANGUAGE).max(LENGTH_MAX_LANGUAGE).optional(), + ipa1: z.string().min(LENGTH_MIN_IPA).max(LENGTH_MAX_IPA).optional(), + ipa2: z.string().min(LENGTH_MIN_IPA).max(LENGTH_MAX_IPA).optional(), + folderId: z.int().optional() +}); +export type ActionInputUpdatePairById = z.infer; +export const validateActionInputUpdatePairById = generateValidator(schemaActionInputUpdatePairById); + +export type ActionOutputGetFoldersWithTotalPairsByUserId = { + message: string, + success: boolean, + data?: TSharedFolderWithTotalPairs[]; +}; diff --git a/src/modules/folder/folder-repository-dto.ts b/src/modules/folder/folder-repository-dto.ts new file mode 100644 index 0000000..844ef14 --- /dev/null +++ b/src/modules/folder/folder-repository-dto.ts @@ -0,0 +1,23 @@ +export interface RepoInputCreateFolder { + name: string; + userId: string; +} + +export interface RepoInputCreatePair { + text1: string; + text2: string; + language1: string; + language2: string; + ipa1?: string; + ipa2?: string; + folderId: number; +} + +export interface RepoInputUpdatePair { + text1?: string; + text2?: string; + language1?: string; + language2?: string; + ipa1?: string; + ipa2?: string; +} diff --git a/src/modules/folder/folder-repository.ts b/src/modules/folder/folder-repository.ts index e69de29..3d6d5db 100644 --- a/src/modules/folder/folder-repository.ts +++ b/src/modules/folder/folder-repository.ts @@ -0,0 +1,120 @@ +import prisma from "@/lib/db"; +import { RepoInputCreateFolder, RepoInputCreatePair, RepoInputUpdatePair } from "./folder-repository-dto"; + +export async function repoCreatePair(data: RepoInputCreatePair) { + return (await prisma.pair.create({ + data: data, + })).id; +} + +export async function repoDeletePairById(id: number) { + await prisma.pair.delete({ + where: { + id: id, + }, + }); +} + +export async function repoUpdatePairById( + id: number, + data: RepoInputUpdatePair, +) { + await prisma.pair.update({ + where: { + id: id, + }, + data: data, + }); +} + +export async function repoGetPairCountByFolderId(folderId: number) { + return prisma.pair.count({ + where: { + folderId: folderId, + }, + }); +} + +export async function repoGetPairsByFolderId(folderId: number) { + return (await prisma.pair.findMany({ + where: { + folderId: folderId, + }, + })).map(pair => { + return { + text1:pair.text1, + text2: pair.text2, + language1: pair.language1, + language2: pair.language2, + ipa1: pair.ipa1, + ipa2: pair.ipa2, + id: pair.id, + folderId: pair.folderId + } + }); +} + +export async function repoGetFoldersByUserId(userId: string) { + return (await prisma.folder.findMany({ + where: { + userId: userId, + }, + }))?.map(v => { + return { + id: v.id, + name: v.name, + userId: v.userId + }; + }); +} + +export async function repoRenameFolderById(id: number, newName: string) { + await prisma.folder.update({ + where: { + id: id, + }, + data: { + name: newName, + }, + }); +} + +export async function repoGetFoldersWithTotalPairsByUserId(userId: string) { + const folders = await prisma.folder.findMany({ + where: { userId }, + include: { + _count: { + select: { pairs: true }, + }, + }, + }); + return folders.map(folder => ({ + id: folder.id, + name: folder.name, + userId: folder.userId, + total: folder._count?.pairs ?? 0, + })); +} + +export async function repoCreateFolder(folder: RepoInputCreateFolder) { + await prisma.folder.create({ + data: folder, + }); +} + +export async function repoDeleteFolderById(id: number) { + await prisma.folder.delete({ + where: { + id: id, + }, + }); +} + +export async function repoGetUserIdByFolderId(id: number) { + const folder = await prisma.folder.findUnique({ + where: { + id: id, + }, + }); + return folder?.userId; +} diff --git a/src/modules/folder/folder-service.ts b/src/modules/folder/folder-service.ts index 9ce74d3..e69de29 100644 --- a/src/modules/folder/folder-service.ts +++ b/src/modules/folder/folder-service.ts @@ -1,68 +0,0 @@ -import { CreateFolderInput, UpdateFolderInput } from "../translator/translator-dto"; -import prisma from "@/lib/db"; - -export async function getFoldersByUserId(userId: string) { - return prisma.folder.findMany({ - where: { - userId: userId, - }, - }); -} - -export async function renameFolderById(id: number, newName: string) { - return prisma.folder.update({ - where: { - id: id, - }, - data: { - name: newName, - }, - }); -} - -export async function getFoldersWithTotalPairsByUserId(userId: string) { - const folders = await prisma.folder.findMany({ - where: { userId }, - include: { - _count: { - select: { pairs: true }, - }, - }, - }); - return folders.map(folder => ({ - ...folder, - total: folder._count?.pairs ?? 0, - })); -} - -export async function createFolder(folder: CreateFolderInput) { - return prisma.folder.create({ - data: folder, - }); -} - -export async function deleteFolderById(id: number) { - return prisma.folder.delete({ - where: { - id: id, - }, - }); -} - -export async function updateFolderById(id: number, data: UpdateFolderInput) { - return prisma.folder.update({ - where: { - id: id, - }, - data: data, - }); -} - -export async function getUserIdByFolderId(id: number) { - const folder = await prisma.folder.findUnique({ - where: { - id: id, - }, - }); - return folder?.userId; -} diff --git a/src/modules/folder/index.ts b/src/modules/folder/index.ts new file mode 100644 index 0000000..019dbb5 --- /dev/null +++ b/src/modules/folder/index.ts @@ -0,0 +1,2 @@ +export * from './folder-aciton'; +export * from './folder-action-dto'; \ No newline at end of file diff --git a/src/modules/pair/pair-action-dto.ts b/src/modules/pair/pair-action-dto.ts deleted file mode 100644 index e69de29..0000000 diff --git a/src/modules/pair/pair-action.ts b/src/modules/pair/pair-action.ts deleted file mode 100644 index 6ae8d13..0000000 --- a/src/modules/pair/pair-action.ts +++ /dev/null @@ -1 +0,0 @@ -"use server"; \ No newline at end of file diff --git a/src/modules/pair/pair-repository.ts b/src/modules/pair/pair-repository.ts deleted file mode 100644 index 6d4b63b..0000000 --- a/src/modules/pair/pair-repository.ts +++ /dev/null @@ -1,44 +0,0 @@ -import { CreatePairInput, UpdatePairInput } from "../translator/translator-dto"; -import prisma from "@/lib/db"; - -export async function createPair(data: CreatePairInput) { - return (await prisma.pair.create({ - data: data, - })).id; -} - -export async function deletePairById(id: number) { - await prisma.pair.delete({ - where: { - id: id, - }, - }); -} - -export async function updatePairById( - id: number, - data: UpdatePairInput, -) { - await prisma.pair.update({ - where: { - id: id, - }, - data: data, - }); -} - -export async function getPairCountByFolderId(folderId: number) { - return prisma.pair.count({ - where: { - folderId: folderId, - }, - }); -} - -export async function getPairsByFolderId(folderId: number) { - return prisma.pair.findMany({ - where: { - folderId: folderId, - }, - }); -} diff --git a/src/modules/pair/pair-service-dto.ts b/src/modules/pair/pair-service-dto.ts deleted file mode 100644 index e69de29..0000000 diff --git a/src/modules/pair/pair-service.ts b/src/modules/pair/pair-service.ts deleted file mode 100644 index e69de29..0000000 diff --git a/src/modules/translator/translator-dto.ts b/src/modules/translator/translator-dto.ts index 5ac0671..0df3689 100644 --- a/src/modules/translator/translator-dto.ts +++ b/src/modules/translator/translator-dto.ts @@ -1,40 +1,3 @@ -/** - * Service 层的自定义业务类型 - * - * 这些类型用于替换 Prisma 生成的类型,提高代码的可维护性和抽象层次 - */ - -// Folder 相关 -export interface CreateFolderInput { - name: string; - userId: string; -} - -export interface UpdateFolderInput { - name?: string; -} - -// Pair 相关 -export interface CreatePairInput { - text1: string; - text2: string; - language1: string; - language2: string; - ipa1?: string; - ipa2?: string; - folderId: number; -} - -export interface UpdatePairInput { - text1?: string; - text2?: string; - language1?: string; - language2?: string; - ipa1?: string; - ipa2?: string; -} - -// Translation 相关 export interface CreateTranslationHistoryInput { userId?: string; sourceText: string; @@ -50,7 +13,6 @@ export interface TranslationHistoryQuery { targetLanguage: string; } -// 翻译相关 - 统一翻译函数 export interface TranslateTextInput { sourceText: string; targetLanguage: string; diff --git a/src/modules/user/user-action-dto.ts b/src/modules/user/user-action-dto.ts deleted file mode 100644 index e69de29..0000000 diff --git a/src/modules/user/user-repository.ts b/src/modules/user/user-repository.ts deleted file mode 100644 index e69de29..0000000 diff --git a/src/modules/user/user-service-dto.ts b/src/modules/user/user-service-dto.ts deleted file mode 100644 index e69de29..0000000 diff --git a/src/modules/user/user-service.ts b/src/modules/user/user-service.ts deleted file mode 100644 index e69de29..0000000 diff --git a/src/shared/constant.ts b/src/shared/constant.ts new file mode 100644 index 0000000..a081c2d --- /dev/null +++ b/src/shared/constant.ts @@ -0,0 +1,14 @@ +export const LENGTH_MAX_DICTIONARY_TEXT = 30; +export const LENGTH_MIN_DICTIONARY_TEXT = 1; + +export const LENGTH_MAX_LANGUAGE = 20; +export const LENGTH_MIN_LANGUAGE = 1; + +export const LENGTH_MAX_PAIR_TEXT = 50; +export const LENGTH_MIN_PAIR_TEXT = 1; + +export const LENGTH_MAX_IPA = 150; +export const LENGTH_MIN_IPA = 1; + +export const LENGTH_MAX_FOLDER_NAME = 20; +export const LENGTH_MIN_FOLDER_NAME = 1; \ No newline at end of file diff --git a/src/shared/folder-related-type.ts b/src/shared/folder-related-type.ts deleted file mode 100644 index 4950db4..0000000 --- a/src/shared/folder-related-type.ts +++ /dev/null @@ -1,3 +0,0 @@ -export type TSharedPair = { - -}; \ No newline at end of file diff --git a/src/shared/folder-type.ts b/src/shared/folder-type.ts new file mode 100644 index 0000000..cd2b6aa --- /dev/null +++ b/src/shared/folder-type.ts @@ -0,0 +1,23 @@ +export type TSharedFolder = { + id: number, + name: string, + userId: string; +}; + +export type TSharedFolderWithTotalPairs = { + id: number, + name: string, + userId: string, + total: number; +}; + +export type TSharedPair = { + text1: string; + text2: string; + language1: string; + language2: string; + ipa1: string | null; + ipa2: string | null; + id: number; + folderId: number; +}; \ No newline at end of file diff --git a/src/utils/validate.ts b/src/utils/validate.ts new file mode 100644 index 0000000..de61a7e --- /dev/null +++ b/src/utils/validate.ts @@ -0,0 +1,13 @@ +import { ValidateError } from "@/lib/errors"; +import z from "zod"; + +export const validate = (dto: T, schema: U) => { + const result = schema.safeParse(dto); + if (result.success) return result.data as z.infer; + const errorMessages = result.error.issues.map((issue) => + `${issue.path.join('.')}: ${issue.message}` + ).join('; '); + throw new ValidateError(`Validation failed: ${errorMessages}`); +}; + +export const generateValidator = (schema: T) => (dto: T) => validate(dto, schema);