This commit is contained in:
2026-01-14 16:57:35 +08:00
parent 804baa64b2
commit ec265be26b
38 changed files with 585 additions and 294 deletions

View File

@@ -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: "注册失败,请稍后再试"

View File

@@ -0,0 +1 @@
export * from './auth-action';

View File

@@ -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<typeof DictionaryActionInputDtoSchema>;
export type ActionInputLookUpDictionary = z.infer<typeof schemaActionInputLookUpDictionary>;
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;

View File

@@ -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<DictionaryActionOutputDto> => {
export const actionLookUpDictionary = async (dto: ActionInputLookUpDictionary): Promise<ActionOutputLookUpDictionary> => {
try {
return {
message: 'success',
success: true,
data: await lookUpService(validateDictionaryActionInput(dto))
data: await serviceLookUp(validateActionInputLookUpDictionary(dto))
};
} catch (e) {
if (e instanceof ValidateError) {

View File

@@ -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;

View File

@@ -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<SelectLastLookUpResultOutputDto> {
export async function repoSelectLastLookUpResult(dto: RepoInputSelectLastLookUpResult): Promise<RepoOutputSelectLastLookUpResult> {
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({

View File

@@ -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;

View File

@@ -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,

View File

@@ -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<ActionOutputGetFoldersWithTotalPairsByUserId> {
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.'
};
}
}

View File

@@ -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<typeof schemaActionInputCreatePair>;
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<typeof schemaActionInputUpdatePairById>;
export const validateActionInputUpdatePairById = generateValidator(schemaActionInputUpdatePairById);
export type ActionOutputGetFoldersWithTotalPairsByUserId = {
message: string,
success: boolean,
data?: TSharedFolderWithTotalPairs[];
};

View File

@@ -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;
}

View File

@@ -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;
}

View File

@@ -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;
}

View File

@@ -0,0 +1,2 @@
export * from './folder-aciton';
export * from './folder-action-dto';

View File

@@ -1 +0,0 @@
"use server";

View File

@@ -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,
},
});
}

View File

@@ -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;