refactor: 修复 modules 三层架构违规
- auth: actionDeleteAccount 改用 service+repo,forgot-password 完整三层实现 - card: serviceCheckCardOwnership 替代直接调用 repository - deck: 移除 service 层的 use server 指令 - dictionary: 数据转换逻辑从 repository 移到 service - ocr: 认证移到 action 层,跨模块调用改用 service - translator: genIPA/genLanguage 改用 service 层
This commit is contained in:
@@ -166,6 +166,9 @@
|
|||||||
"resetPasswordFailed": "Reset-E-Mail konnte nicht gesendet werden",
|
"resetPasswordFailed": "Reset-E-Mail konnte nicht gesendet werden",
|
||||||
"resetPasswordEmailSent": "Reset-E-Mail erfolgreich gesendet",
|
"resetPasswordEmailSent": "Reset-E-Mail erfolgreich gesendet",
|
||||||
"resetPasswordEmailSentHint": "Wir haben einen Link zum Zurücksetzen Ihres Passworts an Ihre E-Mail-Adresse gesendet. Bitte überprüfen Sie Ihren Posteingang.",
|
"resetPasswordEmailSentHint": "Wir haben einen Link zum Zurücksetzen Ihres Passworts an Ihre E-Mail-Adresse gesendet. Bitte überprüfen Sie Ihren Posteingang.",
|
||||||
|
"verifyYourEmail": "E-Mail bestätigen",
|
||||||
|
"verificationEmailSent": "Bestätigungs-E-Mail gesendet",
|
||||||
|
"verificationEmailSentHint": "Wir haben eine Bestätigungs-E-Mail an {email} gesendet. Bitte klicken Sie auf den Link in der E-Mail, um Ihr Konto zu bestätigen.",
|
||||||
"checkYourEmail": "Überprüfen Sie Ihre E-Mail",
|
"checkYourEmail": "Überprüfen Sie Ihre E-Mail",
|
||||||
"backToLogin": "Zurück zur Anmeldung",
|
"backToLogin": "Zurück zur Anmeldung",
|
||||||
"resetPassword": "Passwort zurücksetzen",
|
"resetPassword": "Passwort zurücksetzen",
|
||||||
|
|||||||
@@ -157,6 +157,9 @@
|
|||||||
"resetPasswordFailed": "Failed to send reset email",
|
"resetPasswordFailed": "Failed to send reset email",
|
||||||
"resetPasswordEmailSent": "Reset email sent successfully",
|
"resetPasswordEmailSent": "Reset email sent successfully",
|
||||||
"resetPasswordEmailSentHint": "We've sent a password reset link to your email address. Please check your inbox.",
|
"resetPasswordEmailSentHint": "We've sent a password reset link to your email address. Please check your inbox.",
|
||||||
|
"verifyYourEmail": "Verify Your Email",
|
||||||
|
"verificationEmailSent": "Verification email sent",
|
||||||
|
"verificationEmailSentHint": "We've sent a verification email to {email}. Please click the link in the email to verify your account.",
|
||||||
"checkYourEmail": "Check Your Email",
|
"checkYourEmail": "Check Your Email",
|
||||||
"backToLogin": "Back to Login",
|
"backToLogin": "Back to Login",
|
||||||
"resetPassword": "Reset Password",
|
"resetPassword": "Reset Password",
|
||||||
|
|||||||
@@ -166,6 +166,9 @@
|
|||||||
"resetPasswordFailed": "Échec de l'envoi de l'e-mail de réinitialisation",
|
"resetPasswordFailed": "Échec de l'envoi de l'e-mail de réinitialisation",
|
||||||
"resetPasswordEmailSent": "E-mail de réinitialisation envoyé avec succès",
|
"resetPasswordEmailSent": "E-mail de réinitialisation envoyé avec succès",
|
||||||
"resetPasswordEmailSentHint": "Nous avons envoyé un lien de réinitialisation de mot de passe à votre adresse e-mail. Veuillez vérifier votre boîte de réception.",
|
"resetPasswordEmailSentHint": "Nous avons envoyé un lien de réinitialisation de mot de passe à votre adresse e-mail. Veuillez vérifier votre boîte de réception.",
|
||||||
|
"verifyYourEmail": "Vérifier votre e-mail",
|
||||||
|
"verificationEmailSent": "E-mail de vérification envoyé",
|
||||||
|
"verificationEmailSentHint": "Nous avons envoyé un e-mail de vérification à {email}. Veuillez cliquer sur le lien dans l'e-mail pour vérifier votre compte.",
|
||||||
"checkYourEmail": "Vérifiez votre e-mail",
|
"checkYourEmail": "Vérifiez votre e-mail",
|
||||||
"backToLogin": "Retour à la connexion",
|
"backToLogin": "Retour à la connexion",
|
||||||
"resetPassword": "Réinitialiser le mot de passe",
|
"resetPassword": "Réinitialiser le mot de passe",
|
||||||
|
|||||||
@@ -166,6 +166,9 @@
|
|||||||
"resetPasswordFailed": "Impossibile inviare email di reset",
|
"resetPasswordFailed": "Impossibile inviare email di reset",
|
||||||
"resetPasswordEmailSent": "Email di reset inviata con successo",
|
"resetPasswordEmailSent": "Email di reset inviata con successo",
|
||||||
"resetPasswordEmailSentHint": "Abbiamo inviato un link per reimpostare la password al tuo indirizzo email. Controlla la tua casella di posta.",
|
"resetPasswordEmailSentHint": "Abbiamo inviato un link per reimpostare la password al tuo indirizzo email. Controlla la tua casella di posta.",
|
||||||
|
"verifyYourEmail": "Verifica la tua Email",
|
||||||
|
"verificationEmailSent": "Email di verifica inviata",
|
||||||
|
"verificationEmailSentHint": "Abbiamo inviato un'email di verifica a {email}. Clicca sul link nell'email per verificare il tuo account.",
|
||||||
"checkYourEmail": "Controlla la tua Email",
|
"checkYourEmail": "Controlla la tua Email",
|
||||||
"backToLogin": "Torna al Login",
|
"backToLogin": "Torna al Login",
|
||||||
"resetPassword": "Reimposta Password",
|
"resetPassword": "Reimposta Password",
|
||||||
|
|||||||
@@ -157,6 +157,9 @@
|
|||||||
"resetPasswordFailed": "リセットメールの送信に失敗しました",
|
"resetPasswordFailed": "リセットメールの送信に失敗しました",
|
||||||
"resetPasswordEmailSent": "リセットメールを送信しました",
|
"resetPasswordEmailSent": "リセットメールを送信しました",
|
||||||
"resetPasswordEmailSentHint": "パスワードリセット用のリンクをメールでお送りしました。受信トレイをご確認ください。",
|
"resetPasswordEmailSentHint": "パスワードリセット用のリンクをメールでお送りしました。受信トレイをご確認ください。",
|
||||||
|
"verifyYourEmail": "メールアドレスを確認",
|
||||||
|
"verificationEmailSent": "確認メールを送信しました",
|
||||||
|
"verificationEmailSentHint": "{email} に確認メールを送信しました。メール内のリンクをクリックしてアカウントを確認してください。",
|
||||||
"checkYourEmail": "メールをご確認ください",
|
"checkYourEmail": "メールをご確認ください",
|
||||||
"backToLogin": "ログインに戻る",
|
"backToLogin": "ログインに戻る",
|
||||||
"resetPassword": "パスワードをリセット",
|
"resetPassword": "パスワードをリセット",
|
||||||
|
|||||||
@@ -166,6 +166,9 @@
|
|||||||
"resetPasswordFailed": "재설정 이메일 전송 실패",
|
"resetPasswordFailed": "재설정 이메일 전송 실패",
|
||||||
"resetPasswordEmailSent": "재설정 이메일이 전송되었습니다",
|
"resetPasswordEmailSent": "재설정 이메일이 전송되었습니다",
|
||||||
"resetPasswordEmailSentHint": "비밀번호 재설정 링크를 이메일로 보냈습니다. 받은 편지함을 확인해주세요.",
|
"resetPasswordEmailSentHint": "비밀번호 재설정 링크를 이메일로 보냈습니다. 받은 편지함을 확인해주세요.",
|
||||||
|
"verifyYourEmail": "이메일 인증",
|
||||||
|
"verificationEmailSent": "인증 이메일이 전송되었습니다",
|
||||||
|
"verificationEmailSentHint": "{email}로 인증 이메일을 보냈습니다. 이메일의 링크를 클릭하여 계정을 인증해주세요.",
|
||||||
"checkYourEmail": "이메일을 확인하세요",
|
"checkYourEmail": "이메일을 확인하세요",
|
||||||
"backToLogin": "로그인으로 돌아가기",
|
"backToLogin": "로그인으로 돌아가기",
|
||||||
"resetPassword": "비밀번호 재설정",
|
"resetPassword": "비밀번호 재설정",
|
||||||
|
|||||||
@@ -166,6 +166,9 @@
|
|||||||
"resetPasswordFailed": "ئەسلىگە قايتۇرۇش ئېلخېتى ئەۋەتىش مەغلۇپ بولدى",
|
"resetPasswordFailed": "ئەسلىگە قايتۇرۇش ئېلخېتى ئەۋەتىش مەغلۇپ بولدى",
|
||||||
"resetPasswordEmailSent": "ئەسلىگە قايتۇرۇش ئېلخېتى مۇۋەپپەقىيەتلىك ئەۋەتىلدى",
|
"resetPasswordEmailSent": "ئەسلىگە قايتۇرۇش ئېلخېتى مۇۋەپپەقىيەتلىك ئەۋەتىلدى",
|
||||||
"resetPasswordEmailSentHint": "پارولنى ئەسلىگە قايتۇرۇش ئۇلانمىسىنى ئېلخەت ئادرېسىڭىزغا ئەۋەتتۇق. ئېلخەت ساندۇقىڭىزنى تەكشۈرۈڭ.",
|
"resetPasswordEmailSentHint": "پارولنى ئەسلىگە قايتۇرۇش ئۇلانمىسىنى ئېلخەت ئادرېسىڭىزغا ئەۋەتتۇق. ئېلخەت ساندۇقىڭىزنى تەكشۈرۈڭ.",
|
||||||
|
"verifyYourEmail": "ئېلخەتنى دەلىللەش",
|
||||||
|
"verificationEmailSent": "دەلىللەش ئېلخېتى ئەۋەتىلدى",
|
||||||
|
"verificationEmailSentHint": "{email} غا دەلىللەش ئېلخېتى ئەۋەتتۇق. ئېلخەتتىكى ئۇلانمىنى چېكىپ ھېساباتىڭىزنى دەلىللەڭ.",
|
||||||
"checkYourEmail": "ئېلخېتىڭىزنى تەكشۈرۈڭ",
|
"checkYourEmail": "ئېلخېتىڭىزنى تەكشۈرۈڭ",
|
||||||
"backToLogin": "كىرىشكە قايتىش",
|
"backToLogin": "كىرىشكە قايتىش",
|
||||||
"resetPassword": "پارولنى ئەسلىگە قايتۇرۇش",
|
"resetPassword": "پارولنى ئەسلىگە قايتۇرۇش",
|
||||||
|
|||||||
@@ -157,6 +157,9 @@
|
|||||||
"resetPasswordFailed": "发送重置邮件失败",
|
"resetPasswordFailed": "发送重置邮件失败",
|
||||||
"resetPasswordEmailSent": "重置邮件已发送",
|
"resetPasswordEmailSent": "重置邮件已发送",
|
||||||
"resetPasswordEmailSentHint": "我们已向您的邮箱发送了密码重置链接,请查收。",
|
"resetPasswordEmailSentHint": "我们已向您的邮箱发送了密码重置链接,请查收。",
|
||||||
|
"verifyYourEmail": "验证您的邮箱",
|
||||||
|
"verificationEmailSent": "验证邮件已发送",
|
||||||
|
"verificationEmailSentHint": "我们已向 {email} 发送了验证邮件,请点击邮件中的链接完成验证。",
|
||||||
"checkYourEmail": "请查收邮件",
|
"checkYourEmail": "请查收邮件",
|
||||||
"backToLogin": "返回登录",
|
"backToLogin": "返回登录",
|
||||||
"resetPassword": "重置密码",
|
"resetPassword": "重置密码",
|
||||||
|
|||||||
@@ -1,7 +1,6 @@
|
|||||||
"use client";
|
"use client";
|
||||||
|
|
||||||
import { useState } from "react";
|
import { useState } from "react";
|
||||||
import { authClient } from "@/lib/auth-client";
|
|
||||||
import Link from "next/link";
|
import Link from "next/link";
|
||||||
import { toast } from "sonner";
|
import { toast } from "sonner";
|
||||||
import { useTranslations } from "next-intl";
|
import { useTranslations } from "next-intl";
|
||||||
@@ -9,6 +8,7 @@ import { Card, CardBody } from "@/design-system/base/card";
|
|||||||
import { Input } from "@/design-system/base/input";
|
import { Input } from "@/design-system/base/input";
|
||||||
import { PrimaryButton } from "@/design-system/base/button";
|
import { PrimaryButton } from "@/design-system/base/button";
|
||||||
import { VStack } from "@/design-system/layout/stack";
|
import { VStack } from "@/design-system/layout/stack";
|
||||||
|
import { actionRequestPasswordReset } from "@/modules/auth/forgot-password-action";
|
||||||
|
|
||||||
export default function ForgotPasswordPage() {
|
export default function ForgotPasswordPage() {
|
||||||
const t = useTranslations("auth");
|
const t = useTranslations("auth");
|
||||||
@@ -23,16 +23,13 @@ export default function ForgotPasswordPage() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
setLoading(true);
|
setLoading(true);
|
||||||
const { error } = await authClient.requestPasswordReset({
|
const result = await actionRequestPasswordReset({ email });
|
||||||
email,
|
|
||||||
redirectTo: "/reset-password",
|
|
||||||
});
|
|
||||||
|
|
||||||
if (error) {
|
if (!result.success) {
|
||||||
toast.error(error.message ?? t("resetPasswordFailed"));
|
toast.error(result.message);
|
||||||
} else {
|
} else {
|
||||||
setSent(true);
|
setSent(true);
|
||||||
toast.success(t("resetPasswordEmailSent"));
|
toast.success(result.message);
|
||||||
}
|
}
|
||||||
setLoading(false);
|
setLoading(false);
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -18,6 +18,7 @@ export default function SignUpPage() {
|
|||||||
const [email, setEmail] = useState("");
|
const [email, setEmail] = useState("");
|
||||||
const [password, setPassword] = useState("");
|
const [password, setPassword] = useState("");
|
||||||
const [loading, setLoading] = useState(false);
|
const [loading, setLoading] = useState(false);
|
||||||
|
const [verificationSent, setVerificationSent] = useState(false);
|
||||||
|
|
||||||
const searchParams = useSearchParams();
|
const searchParams = useSearchParams();
|
||||||
const redirectTo = searchParams.get("redirect");
|
const redirectTo = searchParams.get("redirect");
|
||||||
@@ -26,10 +27,10 @@ export default function SignUpPage() {
|
|||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!isPending && session?.user?.username && !redirectTo) {
|
if (!isPending && session?.user?.username && !redirectTo && !verificationSent) {
|
||||||
router.push("/folders");
|
router.push("/folders");
|
||||||
}
|
}
|
||||||
}, [session, isPending, router, redirectTo]);
|
}, [session, isPending, router, redirectTo, verificationSent]);
|
||||||
|
|
||||||
const handleSignUp = async () => {
|
const handleSignUp = async () => {
|
||||||
if (!username || !email || !password) {
|
if (!username || !email || !password) {
|
||||||
@@ -49,12 +50,38 @@ export default function SignUpPage() {
|
|||||||
toast.error(error.message ?? t("signUpFailed"));
|
toast.error(error.message ?? t("signUpFailed"));
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
router.push(redirectTo ?? "/folders");
|
setVerificationSent(true);
|
||||||
|
toast.success(t("verificationEmailSent"));
|
||||||
} finally {
|
} finally {
|
||||||
setLoading(false);
|
setLoading(false);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
if (verificationSent) {
|
||||||
|
return (
|
||||||
|
<div className="flex justify-center items-center min-h-screen">
|
||||||
|
<Card className="w-96">
|
||||||
|
<CardBody>
|
||||||
|
<VStack gap={4} align="center" justify="center">
|
||||||
|
<h1 className="text-2xl font-bold text-center w-full">
|
||||||
|
{t("verifyYourEmail")}
|
||||||
|
</h1>
|
||||||
|
<p className="text-center text-gray-600">
|
||||||
|
{t("verificationEmailSentHint", { email })}
|
||||||
|
</p>
|
||||||
|
<Link
|
||||||
|
href="/login"
|
||||||
|
className="text-primary-500 hover:underline"
|
||||||
|
>
|
||||||
|
{t("backToLogin")}
|
||||||
|
</Link>
|
||||||
|
</VStack>
|
||||||
|
</CardBody>
|
||||||
|
</Card>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="flex justify-center items-center min-h-screen">
|
<div className="flex justify-center items-center min-h-screen">
|
||||||
<Card className="w-96">
|
<Card className="w-96">
|
||||||
|
|||||||
@@ -5,7 +5,6 @@ import { headers } from "next/headers";
|
|||||||
import { redirect } from "next/navigation";
|
import { redirect } from "next/navigation";
|
||||||
import { ValidateError } from "@/lib/errors";
|
import { ValidateError } from "@/lib/errors";
|
||||||
import { createLogger } from "@/lib/logger";
|
import { createLogger } from "@/lib/logger";
|
||||||
import { prisma } from "@/lib/db";
|
|
||||||
import {
|
import {
|
||||||
ActionInputGetUserProfileByUsername,
|
ActionInputGetUserProfileByUsername,
|
||||||
ActionInputSignIn,
|
ActionInputSignIn,
|
||||||
@@ -20,7 +19,8 @@ import {
|
|||||||
import {
|
import {
|
||||||
serviceGetUserProfileByUsername,
|
serviceGetUserProfileByUsername,
|
||||||
serviceSignIn,
|
serviceSignIn,
|
||||||
serviceSignUp
|
serviceSignUp,
|
||||||
|
serviceDeleteAccount
|
||||||
} from "./auth-service";
|
} from "./auth-service";
|
||||||
|
|
||||||
// Re-export types for use in components
|
// Re-export types for use in components
|
||||||
@@ -194,75 +194,11 @@ export async function actionDeleteAccount(): Promise<ActionOutputDeleteAccount>
|
|||||||
return { success: false, message: "Unauthorized" };
|
return { success: false, message: "Unauthorized" };
|
||||||
}
|
}
|
||||||
|
|
||||||
const userId = session.user.id;
|
const result = await serviceDeleteAccount({ userId: session.user.id });
|
||||||
|
|
||||||
await prisma.$transaction(async (tx) => {
|
if (!result.success) {
|
||||||
// Delete in correct order to avoid foreign key constraints
|
return { success: false, message: "Failed to delete account" };
|
||||||
// 1. Revlogs (depend on cards)
|
}
|
||||||
await tx.revlog.deleteMany({
|
|
||||||
where: { card: { note: { userId } } }
|
|
||||||
});
|
|
||||||
|
|
||||||
// 2. Cards (depend on notes and decks)
|
|
||||||
await tx.card.deleteMany({
|
|
||||||
where: { note: { userId } }
|
|
||||||
});
|
|
||||||
|
|
||||||
// 3. Notes (depend on note types)
|
|
||||||
await tx.note.deleteMany({
|
|
||||||
where: { userId }
|
|
||||||
});
|
|
||||||
|
|
||||||
// 4. Note types
|
|
||||||
await tx.noteType.deleteMany({
|
|
||||||
where: { userId }
|
|
||||||
});
|
|
||||||
|
|
||||||
// 5. Deck favorites
|
|
||||||
await tx.deckFavorite.deleteMany({
|
|
||||||
where: { userId }
|
|
||||||
});
|
|
||||||
|
|
||||||
// 6. Decks
|
|
||||||
await tx.deck.deleteMany({
|
|
||||||
where: { userId }
|
|
||||||
});
|
|
||||||
|
|
||||||
// 7. Follows (both as follower and following)
|
|
||||||
await tx.follow.deleteMany({
|
|
||||||
where: {
|
|
||||||
OR: [
|
|
||||||
{ followerId: userId },
|
|
||||||
{ followingId: userId }
|
|
||||||
]
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
// 8. Dictionary lookups
|
|
||||||
await tx.dictionaryLookUp.deleteMany({
|
|
||||||
where: { userId }
|
|
||||||
});
|
|
||||||
|
|
||||||
// 9. Translation history
|
|
||||||
await tx.translationHistory.deleteMany({
|
|
||||||
where: { userId }
|
|
||||||
});
|
|
||||||
|
|
||||||
// 10. Sessions
|
|
||||||
await tx.session.deleteMany({
|
|
||||||
where: { userId }
|
|
||||||
});
|
|
||||||
|
|
||||||
// 11. Accounts
|
|
||||||
await tx.account.deleteMany({
|
|
||||||
where: { userId }
|
|
||||||
});
|
|
||||||
|
|
||||||
// 12. Finally, delete the user
|
|
||||||
await tx.user.delete({
|
|
||||||
where: { id: userId }
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
return { success: true, message: "Account deleted successfully" };
|
return { success: true, message: "Account deleted successfully" };
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
|
|||||||
@@ -25,3 +25,12 @@ export type RepoInputFindUserById = {
|
|||||||
export type RepoInputFindUserByEmail = {
|
export type RepoInputFindUserByEmail = {
|
||||||
email: string;
|
email: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// Delete user cascade types
|
||||||
|
export type RepoInputDeleteUserCascade = {
|
||||||
|
userId: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type RepoOutputDeleteUserCascade = {
|
||||||
|
success: boolean;
|
||||||
|
};
|
||||||
|
|||||||
@@ -1,11 +1,16 @@
|
|||||||
import { prisma } from "@/lib/db";
|
import { prisma } from "@/lib/db";
|
||||||
|
import { createLogger } from "@/lib/logger";
|
||||||
import {
|
import {
|
||||||
RepoInputFindUserByEmail,
|
RepoInputFindUserByEmail,
|
||||||
RepoInputFindUserById,
|
RepoInputFindUserById,
|
||||||
RepoInputFindUserByUsername,
|
RepoInputFindUserByUsername,
|
||||||
RepoOutputUserProfile
|
RepoInputDeleteUserCascade,
|
||||||
|
RepoOutputUserProfile,
|
||||||
|
RepoOutputDeleteUserCascade
|
||||||
} from "./auth-repository-dto";
|
} from "./auth-repository-dto";
|
||||||
|
|
||||||
|
const log = createLogger("auth-repository");
|
||||||
|
|
||||||
export async function repoFindUserByUsername(dto: RepoInputFindUserByUsername): Promise<RepoOutputUserProfile> {
|
export async function repoFindUserByUsername(dto: RepoInputFindUserByUsername): Promise<RepoOutputUserProfile> {
|
||||||
const user = await prisma.user.findUnique({
|
const user = await prisma.user.findUnique({
|
||||||
where: { username: dto.username },
|
where: { username: dto.username },
|
||||||
@@ -62,3 +67,68 @@ export async function repoFindUserByEmail(dto: RepoInputFindUserByEmail): Promis
|
|||||||
|
|
||||||
return user;
|
return user;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export async function repoDeleteUserCascade(dto: RepoInputDeleteUserCascade): Promise<RepoOutputDeleteUserCascade> {
|
||||||
|
const { userId } = dto;
|
||||||
|
|
||||||
|
log.info("Starting cascade delete for user", { userId });
|
||||||
|
|
||||||
|
await prisma.$transaction(async (tx) => {
|
||||||
|
await tx.revlog.deleteMany({
|
||||||
|
where: { card: { note: { userId } } }
|
||||||
|
});
|
||||||
|
|
||||||
|
await tx.card.deleteMany({
|
||||||
|
where: { note: { userId } }
|
||||||
|
});
|
||||||
|
|
||||||
|
await tx.note.deleteMany({
|
||||||
|
where: { userId }
|
||||||
|
});
|
||||||
|
|
||||||
|
await tx.noteType.deleteMany({
|
||||||
|
where: { userId }
|
||||||
|
});
|
||||||
|
|
||||||
|
await tx.deckFavorite.deleteMany({
|
||||||
|
where: { userId }
|
||||||
|
});
|
||||||
|
|
||||||
|
await tx.deck.deleteMany({
|
||||||
|
where: { userId }
|
||||||
|
});
|
||||||
|
|
||||||
|
await tx.follow.deleteMany({
|
||||||
|
where: {
|
||||||
|
OR: [
|
||||||
|
{ followerId: userId },
|
||||||
|
{ followingId: userId }
|
||||||
|
]
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
await tx.dictionaryLookUp.deleteMany({
|
||||||
|
where: { userId }
|
||||||
|
});
|
||||||
|
|
||||||
|
await tx.translationHistory.deleteMany({
|
||||||
|
where: { userId }
|
||||||
|
});
|
||||||
|
|
||||||
|
await tx.session.deleteMany({
|
||||||
|
where: { userId }
|
||||||
|
});
|
||||||
|
|
||||||
|
await tx.account.deleteMany({
|
||||||
|
where: { userId }
|
||||||
|
});
|
||||||
|
|
||||||
|
await tx.user.delete({
|
||||||
|
where: { id: userId }
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
log.info("Cascade delete completed for user", { userId });
|
||||||
|
|
||||||
|
return { success: true };
|
||||||
|
}
|
||||||
|
|||||||
@@ -38,3 +38,11 @@ export type ServiceOutputUserProfile = {
|
|||||||
createdAt: Date;
|
createdAt: Date;
|
||||||
updatedAt: Date;
|
updatedAt: Date;
|
||||||
} | null;
|
} | null;
|
||||||
|
|
||||||
|
export type ServiceInputDeleteAccount = {
|
||||||
|
userId: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type ServiceOutputDeleteAccount = {
|
||||||
|
success: boolean;
|
||||||
|
};
|
||||||
|
|||||||
@@ -1,15 +1,18 @@
|
|||||||
import { auth } from "@/auth";
|
import { auth } from "@/auth";
|
||||||
import {
|
import {
|
||||||
repoFindUserByUsername,
|
repoFindUserByUsername,
|
||||||
repoFindUserById
|
repoFindUserById,
|
||||||
|
repoDeleteUserCascade
|
||||||
} from "./auth-repository";
|
} from "./auth-repository";
|
||||||
import {
|
import {
|
||||||
ServiceInputGetUserProfileByUsername,
|
ServiceInputGetUserProfileByUsername,
|
||||||
ServiceInputGetUserProfileById,
|
ServiceInputGetUserProfileById,
|
||||||
ServiceInputSignIn,
|
ServiceInputSignIn,
|
||||||
ServiceInputSignUp,
|
ServiceInputSignUp,
|
||||||
|
ServiceInputDeleteAccount,
|
||||||
ServiceOutputAuth,
|
ServiceOutputAuth,
|
||||||
ServiceOutputUserProfile
|
ServiceOutputUserProfile,
|
||||||
|
ServiceOutputDeleteAccount
|
||||||
} from "./auth-service-dto";
|
} from "./auth-service-dto";
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -92,3 +95,7 @@ export async function serviceGetUserProfileByUsername(dto: ServiceInputGetUserPr
|
|||||||
export async function serviceGetUserProfileById(dto: ServiceInputGetUserProfileById): Promise<ServiceOutputUserProfile> {
|
export async function serviceGetUserProfileById(dto: ServiceInputGetUserProfileById): Promise<ServiceOutputUserProfile> {
|
||||||
return await repoFindUserById(dto);
|
return await repoFindUserById(dto);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export async function serviceDeleteAccount(dto: ServiceInputDeleteAccount): Promise<ServiceOutputDeleteAccount> {
|
||||||
|
return await repoDeleteUserCascade({ userId: dto.userId });
|
||||||
|
}
|
||||||
|
|||||||
12
src/modules/auth/forgot-password-action-dto.ts
Normal file
12
src/modules/auth/forgot-password-action-dto.ts
Normal file
@@ -0,0 +1,12 @@
|
|||||||
|
import { z } from "zod";
|
||||||
|
|
||||||
|
export const schemaActionInputForgotPassword = z.object({
|
||||||
|
email: z.string().email("请输入有效的邮箱地址"),
|
||||||
|
});
|
||||||
|
|
||||||
|
export type ActionInputForgotPassword = z.infer<typeof schemaActionInputForgotPassword>;
|
||||||
|
|
||||||
|
export interface ActionOutputForgotPassword {
|
||||||
|
success: boolean;
|
||||||
|
message: string;
|
||||||
|
}
|
||||||
35
src/modules/auth/forgot-password-action.ts
Normal file
35
src/modules/auth/forgot-password-action.ts
Normal file
@@ -0,0 +1,35 @@
|
|||||||
|
"use server";
|
||||||
|
|
||||||
|
import { createLogger } from "@/lib/logger";
|
||||||
|
import { validate } from "@/utils/validate";
|
||||||
|
import { ValidateError } from "@/lib/errors";
|
||||||
|
import {
|
||||||
|
schemaActionInputForgotPassword,
|
||||||
|
type ActionInputForgotPassword,
|
||||||
|
type ActionOutputForgotPassword,
|
||||||
|
} from "./forgot-password-action-dto";
|
||||||
|
import { serviceRequestPasswordReset } from "./forgot-password-service";
|
||||||
|
|
||||||
|
const log = createLogger("forgot-password-action");
|
||||||
|
|
||||||
|
export async function actionRequestPasswordReset(
|
||||||
|
input: unknown
|
||||||
|
): Promise<ActionOutputForgotPassword> {
|
||||||
|
try {
|
||||||
|
const dto = validate(input, schemaActionInputForgotPassword) as ActionInputForgotPassword;
|
||||||
|
|
||||||
|
return await serviceRequestPasswordReset({ email: dto.email });
|
||||||
|
} catch (e) {
|
||||||
|
if (e instanceof ValidateError) {
|
||||||
|
return {
|
||||||
|
success: false,
|
||||||
|
message: e.message,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
log.error("Password reset request failed", { error: e });
|
||||||
|
return {
|
||||||
|
success: false,
|
||||||
|
message: "发送重置邮件失败,请稍后重试",
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
7
src/modules/auth/forgot-password-repository-dto.ts
Normal file
7
src/modules/auth/forgot-password-repository-dto.ts
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
export type RepoInputFindUserByEmail = {
|
||||||
|
email: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type RepoOutputFindUserByEmail = {
|
||||||
|
id: string;
|
||||||
|
} | null;
|
||||||
19
src/modules/auth/forgot-password-repository.ts
Normal file
19
src/modules/auth/forgot-password-repository.ts
Normal file
@@ -0,0 +1,19 @@
|
|||||||
|
import { prisma } from "@/lib/db";
|
||||||
|
import { createLogger } from "@/lib/logger";
|
||||||
|
import {
|
||||||
|
RepoInputFindUserByEmail,
|
||||||
|
RepoOutputFindUserByEmail
|
||||||
|
} from "./forgot-password-repository-dto";
|
||||||
|
|
||||||
|
const log = createLogger("forgot-password-repository");
|
||||||
|
|
||||||
|
export async function repoFindUserByEmail(dto: RepoInputFindUserByEmail): Promise<RepoOutputFindUserByEmail> {
|
||||||
|
log.debug("Finding user by email", { email: dto.email });
|
||||||
|
|
||||||
|
const user = await prisma.user.findUnique({
|
||||||
|
where: { email: dto.email },
|
||||||
|
select: { id: true },
|
||||||
|
});
|
||||||
|
|
||||||
|
return user;
|
||||||
|
}
|
||||||
8
src/modules/auth/forgot-password-service-dto.ts
Normal file
8
src/modules/auth/forgot-password-service-dto.ts
Normal file
@@ -0,0 +1,8 @@
|
|||||||
|
export type ServiceInputRequestPasswordReset = {
|
||||||
|
email: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type ServiceOutputRequestPasswordReset = {
|
||||||
|
success: boolean;
|
||||||
|
message: string;
|
||||||
|
};
|
||||||
34
src/modules/auth/forgot-password-service.ts
Normal file
34
src/modules/auth/forgot-password-service.ts
Normal file
@@ -0,0 +1,34 @@
|
|||||||
|
import { auth } from "@/auth";
|
||||||
|
import { createLogger } from "@/lib/logger";
|
||||||
|
import { repoFindUserByEmail } from "./forgot-password-repository";
|
||||||
|
import {
|
||||||
|
ServiceInputRequestPasswordReset,
|
||||||
|
ServiceOutputRequestPasswordReset
|
||||||
|
} from "./forgot-password-service-dto";
|
||||||
|
|
||||||
|
const log = createLogger("forgot-password-service");
|
||||||
|
|
||||||
|
export async function serviceRequestPasswordReset(dto: ServiceInputRequestPasswordReset): Promise<ServiceOutputRequestPasswordReset> {
|
||||||
|
log.info("Processing password reset request", { email: dto.email });
|
||||||
|
|
||||||
|
const user = await repoFindUserByEmail({ email: dto.email });
|
||||||
|
|
||||||
|
if (!user) {
|
||||||
|
return {
|
||||||
|
success: false,
|
||||||
|
message: "该邮箱未注册",
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
await auth.api.requestPasswordReset({
|
||||||
|
body: {
|
||||||
|
email: dto.email,
|
||||||
|
redirectTo: "/reset-password",
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
return {
|
||||||
|
success: true,
|
||||||
|
message: "重置密码邮件已发送,请检查您的邮箱",
|
||||||
|
};
|
||||||
|
}
|
||||||
@@ -42,8 +42,8 @@ import {
|
|||||||
serviceGetCardStats,
|
serviceGetCardStats,
|
||||||
serviceDeleteCard,
|
serviceDeleteCard,
|
||||||
serviceGetCardByIdWithNote,
|
serviceGetCardByIdWithNote,
|
||||||
|
serviceCheckCardOwnership,
|
||||||
} from "./card-service";
|
} from "./card-service";
|
||||||
import { repoGetCardDeckOwnerId } from "./card-repository";
|
|
||||||
import { CardQueue } from "../../../generated/prisma/enums";
|
import { CardQueue } from "../../../generated/prisma/enums";
|
||||||
|
|
||||||
const log = createLogger("card-action");
|
const log = createLogger("card-action");
|
||||||
@@ -161,8 +161,7 @@ async function checkCardOwnership(cardId: bigint): Promise<boolean> {
|
|||||||
const session = await auth.api.getSession({ headers: await headers() });
|
const session = await auth.api.getSession({ headers: await headers() });
|
||||||
if (!session?.user?.id) return false;
|
if (!session?.user?.id) return false;
|
||||||
|
|
||||||
const ownerId = await repoGetCardDeckOwnerId(cardId);
|
return serviceCheckCardOwnership({ cardId, userId: session.user.id });
|
||||||
return ownerId === session.user.id;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
async function getCurrentUserId(): Promise<string | null> {
|
async function getCurrentUserId(): Promise<string | null> {
|
||||||
|
|||||||
@@ -34,6 +34,13 @@ export interface ServiceInputGetCardStats {
|
|||||||
deckId: number;
|
deckId: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface ServiceInputCheckCardOwnership {
|
||||||
|
cardId: bigint;
|
||||||
|
userId: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export type ServiceOutputCheckCardOwnership = boolean;
|
||||||
|
|
||||||
export type ServiceOutputCard = {
|
export type ServiceOutputCard = {
|
||||||
id: bigint;
|
id: bigint;
|
||||||
noteId: bigint;
|
noteId: bigint;
|
||||||
@@ -94,11 +101,17 @@ export type ServiceOutputReviewResult = {
|
|||||||
|
|
||||||
export const SM2_CONFIG = {
|
export const SM2_CONFIG = {
|
||||||
LEARNING_STEPS: [1, 10],
|
LEARNING_STEPS: [1, 10],
|
||||||
|
RELEARNING_STEPS: [10],
|
||||||
GRADUATING_INTERVAL_GOOD: 1,
|
GRADUATING_INTERVAL_GOOD: 1,
|
||||||
GRADUATING_INTERVAL_EASY: 4,
|
GRADUATING_INTERVAL_EASY: 4,
|
||||||
EASY_INTERVAL: 4,
|
EASY_INTERVAL: 4,
|
||||||
MINIMUM_FACTOR: 1300,
|
MINIMUM_FACTOR: 1300,
|
||||||
DEFAULT_FACTOR: 2500,
|
DEFAULT_FACTOR: 2500,
|
||||||
|
MAXIMUM_INTERVAL: 36500,
|
||||||
|
EASY_BONUS: 1.3,
|
||||||
|
HARD_INTERVAL: 1.2,
|
||||||
|
NEW_INTERVAL: 0.0,
|
||||||
|
INTERVAL_MODIFIER: 1.0,
|
||||||
FACTOR_ADJUSTMENTS: {
|
FACTOR_ADJUSTMENTS: {
|
||||||
1: -200,
|
1: -200,
|
||||||
2: -150,
|
2: -150,
|
||||||
|
|||||||
@@ -11,6 +11,7 @@ import {
|
|||||||
repoGetCardStats,
|
repoGetCardStats,
|
||||||
repoDeleteCard,
|
repoDeleteCard,
|
||||||
repoGetCardsByNoteId,
|
repoGetCardsByNoteId,
|
||||||
|
repoGetCardDeckOwnerId,
|
||||||
} from "./card-repository";
|
} from "./card-repository";
|
||||||
import {
|
import {
|
||||||
RepoInputUpdateCard,
|
RepoInputUpdateCard,
|
||||||
@@ -23,11 +24,13 @@ import {
|
|||||||
ServiceInputGetNewCards,
|
ServiceInputGetNewCards,
|
||||||
ServiceInputGetCardsByDeckId,
|
ServiceInputGetCardsByDeckId,
|
||||||
ServiceInputGetCardStats,
|
ServiceInputGetCardStats,
|
||||||
|
ServiceInputCheckCardOwnership,
|
||||||
ServiceOutputCard,
|
ServiceOutputCard,
|
||||||
ServiceOutputCardWithNote,
|
ServiceOutputCardWithNote,
|
||||||
ServiceOutputCardStats,
|
ServiceOutputCardStats,
|
||||||
ServiceOutputScheduledCard,
|
ServiceOutputScheduledCard,
|
||||||
ServiceOutputReviewResult,
|
ServiceOutputReviewResult,
|
||||||
|
ServiceOutputCheckCardOwnership,
|
||||||
ReviewEase,
|
ReviewEase,
|
||||||
SM2_CONFIG,
|
SM2_CONFIG,
|
||||||
} from "./card-service-dto";
|
} from "./card-service-dto";
|
||||||
@@ -50,7 +53,11 @@ function calculateNextReviewTime(intervalDays: number): Date {
|
|||||||
return new Date(now + intervalDays * 86400 * 1000);
|
return new Date(now + intervalDays * 86400 * 1000);
|
||||||
}
|
}
|
||||||
|
|
||||||
function scheduleNewCard(ease: ReviewEase, factor: number): {
|
function clampInterval(interval: number): number {
|
||||||
|
return Math.min(Math.max(1, interval), SM2_CONFIG.MAXIMUM_INTERVAL);
|
||||||
|
}
|
||||||
|
|
||||||
|
function scheduleNewCard(ease: ReviewEase, currentFactor: number): {
|
||||||
type: CardType;
|
type: CardType;
|
||||||
queue: CardQueue;
|
queue: CardQueue;
|
||||||
ivl: number;
|
ivl: number;
|
||||||
@@ -63,21 +70,53 @@ function scheduleNewCard(ease: ReviewEase, factor: number): {
|
|||||||
queue: CardQueue.LEARNING,
|
queue: CardQueue.LEARNING,
|
||||||
ivl: 0,
|
ivl: 0,
|
||||||
due: Math.floor(Date.now() / 1000) + SM2_CONFIG.LEARNING_STEPS[0] * 60,
|
due: Math.floor(Date.now() / 1000) + SM2_CONFIG.LEARNING_STEPS[0] * 60,
|
||||||
newFactor: Math.max(SM2_CONFIG.MINIMUM_FACTOR, factor + SM2_CONFIG.FACTOR_ADJUSTMENTS[1]),
|
newFactor: currentFactor,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
const ivl = SM2_CONFIG.INITIAL_INTERVALS[ease];
|
if (ease === 2) {
|
||||||
|
if (SM2_CONFIG.LEARNING_STEPS.length >= 2) {
|
||||||
|
const avgStep = (SM2_CONFIG.LEARNING_STEPS[0] + SM2_CONFIG.LEARNING_STEPS[1]) / 2;
|
||||||
|
return {
|
||||||
|
type: CardType.LEARNING,
|
||||||
|
queue: CardQueue.LEARNING,
|
||||||
|
ivl: 0,
|
||||||
|
due: Math.floor(Date.now() / 1000) + avgStep * 60,
|
||||||
|
newFactor: currentFactor,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
return {
|
||||||
|
type: CardType.LEARNING,
|
||||||
|
queue: CardQueue.LEARNING,
|
||||||
|
ivl: 0,
|
||||||
|
due: Math.floor(Date.now() / 1000) + SM2_CONFIG.LEARNING_STEPS[0] * 60,
|
||||||
|
newFactor: currentFactor,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
if (ease === 3) {
|
||||||
|
return {
|
||||||
|
type: CardType.LEARNING,
|
||||||
|
queue: CardQueue.LEARNING,
|
||||||
|
ivl: 0,
|
||||||
|
due: Math.floor(Date.now() / 1000) + SM2_CONFIG.LEARNING_STEPS[0] * 60,
|
||||||
|
newFactor: currentFactor,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
const ivl = SM2_CONFIG.EASY_INTERVAL;
|
||||||
|
const newFactor = SM2_CONFIG.DEFAULT_FACTOR + SM2_CONFIG.FACTOR_ADJUSTMENTS[4];
|
||||||
|
|
||||||
return {
|
return {
|
||||||
type: CardType.REVIEW,
|
type: CardType.REVIEW,
|
||||||
queue: CardQueue.REVIEW,
|
queue: CardQueue.REVIEW,
|
||||||
ivl,
|
ivl,
|
||||||
due: calculateDueDate(ivl),
|
due: calculateDueDate(ivl),
|
||||||
newFactor: Math.max(SM2_CONFIG.MINIMUM_FACTOR, factor + SM2_CONFIG.FACTOR_ADJUSTMENTS[ease]),
|
newFactor: Math.max(SM2_CONFIG.MINIMUM_FACTOR, newFactor),
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
function scheduleLearningCard(ease: ReviewEase, factor: number, left: number): {
|
function scheduleLearningCard(ease: ReviewEase, currentFactor: number, left: number): {
|
||||||
type: CardType;
|
type: CardType;
|
||||||
queue: CardQueue;
|
queue: CardQueue;
|
||||||
ivl: number;
|
ivl: number;
|
||||||
@@ -85,45 +124,88 @@ function scheduleLearningCard(ease: ReviewEase, factor: number, left: number): {
|
|||||||
newFactor: number;
|
newFactor: number;
|
||||||
newLeft: number;
|
newLeft: number;
|
||||||
} {
|
} {
|
||||||
|
const steps = SM2_CONFIG.LEARNING_STEPS;
|
||||||
|
const totalSteps = steps.length;
|
||||||
|
|
||||||
if (ease === 1) {
|
if (ease === 1) {
|
||||||
return {
|
return {
|
||||||
type: CardType.LEARNING,
|
type: CardType.LEARNING,
|
||||||
queue: CardQueue.LEARNING,
|
queue: CardQueue.LEARNING,
|
||||||
ivl: 0,
|
ivl: 0,
|
||||||
due: Math.floor(Date.now() / 1000) + SM2_CONFIG.LEARNING_STEPS[0] * 60,
|
due: Math.floor(Date.now() / 1000) + steps[0] * 60,
|
||||||
newFactor: Math.max(SM2_CONFIG.MINIMUM_FACTOR, factor + SM2_CONFIG.FACTOR_ADJUSTMENTS[1]),
|
newFactor: currentFactor,
|
||||||
newLeft: SM2_CONFIG.LEARNING_STEPS.length * 1000 + SM2_CONFIG.LEARNING_STEPS.length,
|
newLeft: totalSteps * 1000,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
const stepIndex = Math.floor(left % 1000);
|
const stepIndex = Math.floor(left % 1000);
|
||||||
if (ease === 2 && stepIndex < SM2_CONFIG.LEARNING_STEPS.length - 1) {
|
|
||||||
const nextStep = stepIndex + 1;
|
if (ease === 2) {
|
||||||
|
if (stepIndex === 0 && steps.length >= 2) {
|
||||||
|
const avgStep = (steps[0] + steps[1]) / 2;
|
||||||
|
return {
|
||||||
|
type: CardType.LEARNING,
|
||||||
|
queue: CardQueue.LEARNING,
|
||||||
|
ivl: 0,
|
||||||
|
due: Math.floor(Date.now() / 1000) + avgStep * 60,
|
||||||
|
newFactor: currentFactor,
|
||||||
|
newLeft: left,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
if (stepIndex < steps.length - 1) {
|
||||||
|
const nextStep = stepIndex + 1;
|
||||||
|
return {
|
||||||
|
type: CardType.LEARNING,
|
||||||
|
queue: CardQueue.LEARNING,
|
||||||
|
ivl: 0,
|
||||||
|
due: Math.floor(Date.now() / 1000) + steps[nextStep] * 60,
|
||||||
|
newFactor: currentFactor,
|
||||||
|
newLeft: nextStep * 1000 + (totalSteps - nextStep),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (ease === 3) {
|
||||||
|
if (stepIndex < steps.length - 1) {
|
||||||
|
const nextStep = stepIndex + 1;
|
||||||
|
return {
|
||||||
|
type: CardType.LEARNING,
|
||||||
|
queue: CardQueue.LEARNING,
|
||||||
|
ivl: 0,
|
||||||
|
due: Math.floor(Date.now() / 1000) + steps[nextStep] * 60,
|
||||||
|
newFactor: currentFactor,
|
||||||
|
newLeft: nextStep * 1000 + (totalSteps - nextStep),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
const ivl = SM2_CONFIG.GRADUATING_INTERVAL_GOOD;
|
||||||
return {
|
return {
|
||||||
type: CardType.LEARNING,
|
type: CardType.REVIEW,
|
||||||
queue: CardQueue.LEARNING,
|
queue: CardQueue.REVIEW,
|
||||||
ivl: 0,
|
ivl,
|
||||||
due: Math.floor(Date.now() / 1000) + SM2_CONFIG.LEARNING_STEPS[nextStep] * 60,
|
due: calculateDueDate(ivl),
|
||||||
newFactor: Math.max(SM2_CONFIG.MINIMUM_FACTOR, factor + SM2_CONFIG.FACTOR_ADJUSTMENTS[2]),
|
newFactor: SM2_CONFIG.DEFAULT_FACTOR,
|
||||||
newLeft: nextStep * 1000 + (SM2_CONFIG.LEARNING_STEPS.length - nextStep),
|
newLeft: 0,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
const ivl = ease === 4 ? SM2_CONFIG.GRADUATING_INTERVAL_EASY : SM2_CONFIG.GRADUATING_INTERVAL_GOOD;
|
const ivl = SM2_CONFIG.GRADUATING_INTERVAL_EASY;
|
||||||
|
const newFactor = SM2_CONFIG.DEFAULT_FACTOR + SM2_CONFIG.FACTOR_ADJUSTMENTS[4];
|
||||||
|
|
||||||
return {
|
return {
|
||||||
type: CardType.REVIEW,
|
type: CardType.REVIEW,
|
||||||
queue: CardQueue.REVIEW,
|
queue: CardQueue.REVIEW,
|
||||||
ivl,
|
ivl,
|
||||||
due: calculateDueDate(ivl),
|
due: calculateDueDate(ivl),
|
||||||
newFactor: Math.max(SM2_CONFIG.MINIMUM_FACTOR, factor + SM2_CONFIG.FACTOR_ADJUSTMENTS[ease]),
|
newFactor: Math.max(SM2_CONFIG.MINIMUM_FACTOR, newFactor),
|
||||||
newLeft: 0,
|
newLeft: 0,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
function scheduleReviewCard(
|
function scheduleReviewCard(
|
||||||
ease: ReviewEase,
|
ease: ReviewEase,
|
||||||
ivl: number,
|
currentIvl: number,
|
||||||
factor: number,
|
currentFactor: number,
|
||||||
lapses: number,
|
lapses: number,
|
||||||
): {
|
): {
|
||||||
type: CardType;
|
type: CardType;
|
||||||
@@ -134,27 +216,34 @@ function scheduleReviewCard(
|
|||||||
newLapses: number;
|
newLapses: number;
|
||||||
} {
|
} {
|
||||||
if (ease === 1) {
|
if (ease === 1) {
|
||||||
|
const newFactor = Math.max(SM2_CONFIG.MINIMUM_FACTOR, currentFactor + SM2_CONFIG.FACTOR_ADJUSTMENTS[1]);
|
||||||
|
const newIvl = Math.max(1, Math.floor(currentIvl * SM2_CONFIG.NEW_INTERVAL));
|
||||||
return {
|
return {
|
||||||
type: CardType.RELEARNING,
|
type: CardType.RELEARNING,
|
||||||
queue: CardQueue.LEARNING,
|
queue: CardQueue.LEARNING,
|
||||||
ivl: 0,
|
ivl: newIvl,
|
||||||
due: Math.floor(Date.now() / 1000) + SM2_CONFIG.LEARNING_STEPS[0] * 60,
|
due: Math.floor(Date.now() / 1000) + SM2_CONFIG.RELEARNING_STEPS[0] * 60,
|
||||||
newFactor: Math.max(SM2_CONFIG.MINIMUM_FACTOR, factor + SM2_CONFIG.FACTOR_ADJUSTMENTS[1]),
|
newFactor,
|
||||||
newLapses: lapses + 1,
|
newLapses: lapses + 1,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
const newFactor = Math.max(SM2_CONFIG.MINIMUM_FACTOR, factor + SM2_CONFIG.FACTOR_ADJUSTMENTS[ease]);
|
let newFactor: number;
|
||||||
const factorMultiplier = newFactor / 1000;
|
let newIvl: number;
|
||||||
let newIvl = Math.floor(ivl * factorMultiplier);
|
|
||||||
|
|
||||||
if (ease === 2) {
|
if (ease === 2) {
|
||||||
newIvl = Math.max(1, Math.floor(newIvl * 1.2));
|
newFactor = Math.max(SM2_CONFIG.MINIMUM_FACTOR, currentFactor + SM2_CONFIG.FACTOR_ADJUSTMENTS[2]);
|
||||||
} else if (ease === 4) {
|
newIvl = Math.floor(currentIvl * SM2_CONFIG.HARD_INTERVAL * SM2_CONFIG.INTERVAL_MODIFIER);
|
||||||
newIvl = Math.floor(newIvl * 1.3);
|
} else if (ease === 3) {
|
||||||
|
newFactor = currentFactor;
|
||||||
|
newIvl = Math.floor(currentIvl * (currentFactor / 1000) * SM2_CONFIG.INTERVAL_MODIFIER);
|
||||||
|
} else {
|
||||||
|
newIvl = Math.floor(currentIvl * (currentFactor / 1000) * SM2_CONFIG.EASY_BONUS * SM2_CONFIG.INTERVAL_MODIFIER);
|
||||||
|
newFactor = Math.max(SM2_CONFIG.MINIMUM_FACTOR, currentFactor + SM2_CONFIG.FACTOR_ADJUSTMENTS[4]);
|
||||||
}
|
}
|
||||||
|
|
||||||
newIvl = Math.max(1, newIvl);
|
newIvl = clampInterval(newIvl);
|
||||||
|
newIvl = Math.max(currentIvl + 1, newIvl);
|
||||||
|
|
||||||
return {
|
return {
|
||||||
type: CardType.REVIEW,
|
type: CardType.REVIEW,
|
||||||
@@ -237,7 +326,9 @@ export async function serviceAnswerCard(
|
|||||||
due: result.due,
|
due: result.due,
|
||||||
factor: result.newFactor,
|
factor: result.newFactor,
|
||||||
reps: card.reps + 1,
|
reps: card.reps + 1,
|
||||||
left: result.type === CardType.LEARNING ? SM2_CONFIG.LEARNING_STEPS.length * 1000 + SM2_CONFIG.LEARNING_STEPS.length : 0,
|
left: result.type === CardType.LEARNING
|
||||||
|
? SM2_CONFIG.LEARNING_STEPS.length * 1000
|
||||||
|
: 0,
|
||||||
mod: Math.floor(Date.now() / 1000),
|
mod: Math.floor(Date.now() / 1000),
|
||||||
};
|
};
|
||||||
scheduled = {
|
scheduled = {
|
||||||
@@ -284,7 +375,9 @@ export async function serviceAnswerCard(
|
|||||||
factor: result.newFactor,
|
factor: result.newFactor,
|
||||||
reps: card.reps + 1,
|
reps: card.reps + 1,
|
||||||
lapses: result.newLapses,
|
lapses: result.newLapses,
|
||||||
left: result.type === CardType.RELEARNING ? SM2_CONFIG.LEARNING_STEPS.length * 1000 + SM2_CONFIG.LEARNING_STEPS.length : 0,
|
left: result.type === CardType.RELEARNING
|
||||||
|
? SM2_CONFIG.RELEARNING_STEPS.length * 1000
|
||||||
|
: 0,
|
||||||
mod: Math.floor(Date.now() / 1000),
|
mod: Math.floor(Date.now() / 1000),
|
||||||
};
|
};
|
||||||
scheduled = {
|
scheduled = {
|
||||||
@@ -382,3 +475,11 @@ export async function serviceDeleteCard(cardId: bigint): Promise<void> {
|
|||||||
log.info("Deleting card", { cardId: cardId.toString() });
|
log.info("Deleting card", { cardId: cardId.toString() });
|
||||||
await repoDeleteCard(cardId);
|
await repoDeleteCard(cardId);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export async function serviceCheckCardOwnership(
|
||||||
|
input: ServiceInputCheckCardOwnership,
|
||||||
|
): Promise<ServiceOutputCheckCardOwnership> {
|
||||||
|
log.debug("Checking card ownership", { cardId: input.cardId.toString() });
|
||||||
|
const ownerId = await repoGetCardDeckOwnerId(input.cardId);
|
||||||
|
return ownerId === input.userId;
|
||||||
|
}
|
||||||
|
|||||||
@@ -1,5 +1,3 @@
|
|||||||
"use server";
|
|
||||||
|
|
||||||
import { createLogger } from "@/lib/logger";
|
import { createLogger } from "@/lib/logger";
|
||||||
import {
|
import {
|
||||||
ServiceInputCreateDeck,
|
ServiceInputCreateDeck,
|
||||||
|
|||||||
@@ -1,5 +1,3 @@
|
|||||||
import { TSharedItem } from "@/shared/dictionary-type";
|
|
||||||
|
|
||||||
export type RepoInputCreateDictionaryLookUp = {
|
export type RepoInputCreateDictionaryLookUp = {
|
||||||
userId?: string;
|
userId?: string;
|
||||||
text: string;
|
text: string;
|
||||||
@@ -8,7 +6,29 @@ export type RepoInputCreateDictionaryLookUp = {
|
|||||||
dictionaryItemId?: number;
|
dictionaryItemId?: number;
|
||||||
};
|
};
|
||||||
|
|
||||||
export type RepoOutputSelectLastLookUpResult = TSharedItem & {id: number} | null;
|
export type RepoOutputSelectLastLookUpResultEntry = {
|
||||||
|
id: number;
|
||||||
|
itemId: number;
|
||||||
|
ipa: string | null;
|
||||||
|
definition: string;
|
||||||
|
partOfSpeech: string | null;
|
||||||
|
example: string;
|
||||||
|
createdAt: Date;
|
||||||
|
updatedAt: Date;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type RepoOutputSelectLastLookUpResultItem = {
|
||||||
|
id: number;
|
||||||
|
frequency: number;
|
||||||
|
standardForm: string;
|
||||||
|
queryLang: string;
|
||||||
|
definitionLang: string;
|
||||||
|
createdAt: Date;
|
||||||
|
updatedAt: Date;
|
||||||
|
entries: RepoOutputSelectLastLookUpResultEntry[];
|
||||||
|
};
|
||||||
|
|
||||||
|
export type RepoOutputSelectLastLookUpResult = RepoOutputSelectLastLookUpResultItem | null;
|
||||||
|
|
||||||
export type RepoInputCreateDictionaryItem = {
|
export type RepoInputCreateDictionaryItem = {
|
||||||
standardForm: string;
|
standardForm: string;
|
||||||
|
|||||||
@@ -1,6 +1,5 @@
|
|||||||
import { stringNormalize } from "@/utils/string";
|
import { stringNormalize } from "@/utils/string";
|
||||||
import {
|
import {
|
||||||
RepoInputCreateDictionaryEntry,
|
|
||||||
RepoInputCreateDictionaryEntryWithoutItemId,
|
RepoInputCreateDictionaryEntryWithoutItemId,
|
||||||
RepoInputCreateDictionaryItem,
|
RepoInputCreateDictionaryItem,
|
||||||
RepoInputCreateDictionaryLookUp,
|
RepoInputCreateDictionaryLookUp,
|
||||||
@@ -30,22 +29,12 @@ export async function repoSelectLastLookUpResult(dto: RepoInputSelectLastLookUpR
|
|||||||
createdAt: 'desc'
|
createdAt: 'desc'
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
if (result && result.dictionaryItem) {
|
|
||||||
const item = result.dictionaryItem;
|
if (!result?.dictionaryItem) {
|
||||||
return {
|
return null;
|
||||||
id: item.id,
|
|
||||||
standardForm: item.standardForm,
|
|
||||||
entries: item.entries.filter(v => !!v).map(v => {
|
|
||||||
return {
|
|
||||||
ipa: v.ipa || undefined,
|
|
||||||
definition: v.definition,
|
|
||||||
partOfSpeech: v.partOfSpeech || undefined,
|
|
||||||
example: v.example
|
|
||||||
};
|
|
||||||
})
|
|
||||||
};
|
|
||||||
}
|
}
|
||||||
return null;
|
|
||||||
|
return result.dictionaryItem;
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function repoCreateLookUp(content: RepoInputCreateDictionaryLookUp) {
|
export async function repoCreateLookUp(content: RepoInputCreateDictionaryLookUp) {
|
||||||
|
|||||||
@@ -2,9 +2,23 @@ import { executeDictionaryLookup } from "@/lib/bigmodel/dictionary/orchestrator"
|
|||||||
import { repoCreateLookUp, repoCreateLookUpWithItemAndEntries, repoSelectLastLookUpResult } from "./dictionary-repository";
|
import { repoCreateLookUp, repoCreateLookUpWithItemAndEntries, repoSelectLastLookUpResult } from "./dictionary-repository";
|
||||||
import { ServiceInputLookUp } from "./dictionary-service-dto";
|
import { ServiceInputLookUp } from "./dictionary-service-dto";
|
||||||
import { createLogger } from "@/lib/logger";
|
import { createLogger } from "@/lib/logger";
|
||||||
|
import { RepoOutputSelectLastLookUpResultItem } from "./dictionary-repository-dto";
|
||||||
|
|
||||||
const log = createLogger("dictionary-service");
|
const log = createLogger("dictionary-service");
|
||||||
|
|
||||||
|
function transformRawItemToSharedItem(rawItem: RepoOutputSelectLastLookUpResultItem) {
|
||||||
|
return {
|
||||||
|
id: rawItem.id,
|
||||||
|
standardForm: rawItem.standardForm,
|
||||||
|
entries: rawItem.entries.map(entry => ({
|
||||||
|
ipa: entry.ipa ?? undefined,
|
||||||
|
definition: entry.definition,
|
||||||
|
partOfSpeech: entry.partOfSpeech ?? undefined,
|
||||||
|
example: entry.example
|
||||||
|
}))
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
export const serviceLookUp = async (dto: ServiceInputLookUp) => {
|
export const serviceLookUp = async (dto: ServiceInputLookUp) => {
|
||||||
const {
|
const {
|
||||||
text,
|
text,
|
||||||
@@ -27,7 +41,6 @@ export const serviceLookUp = async (dto: ServiceInputLookUp) => {
|
|||||||
definitionLang
|
definitionLang
|
||||||
);
|
);
|
||||||
|
|
||||||
// 使用事务确保数据一致性
|
|
||||||
repoCreateLookUpWithItemAndEntries(
|
repoCreateLookUpWithItemAndEntries(
|
||||||
{
|
{
|
||||||
standardForm: response.standardForm,
|
standardForm: response.standardForm,
|
||||||
@@ -47,18 +60,20 @@ export const serviceLookUp = async (dto: ServiceInputLookUp) => {
|
|||||||
|
|
||||||
return response;
|
return response;
|
||||||
} else {
|
} else {
|
||||||
|
const transformedResult = transformRawItemToSharedItem(lastLookUpResult);
|
||||||
|
|
||||||
repoCreateLookUp({
|
repoCreateLookUp({
|
||||||
userId: userId,
|
userId: userId,
|
||||||
text: text,
|
text: text,
|
||||||
queryLang: queryLang,
|
queryLang: queryLang,
|
||||||
definitionLang: definitionLang,
|
definitionLang: definitionLang,
|
||||||
dictionaryItemId: lastLookUpResult.id
|
dictionaryItemId: transformedResult.id
|
||||||
}).catch(error => {
|
}).catch(error => {
|
||||||
log.error("Failed to save dictionary data", { error: error instanceof Error ? error.message : String(error) });
|
log.error("Failed to save dictionary data", { error: error instanceof Error ? error.message : String(error) });
|
||||||
});
|
});
|
||||||
return {
|
return {
|
||||||
standardForm: lastLookUpResult.standardForm,
|
standardForm: transformedResult.standardForm,
|
||||||
entries: lastLookUpResult.entries
|
entries: transformedResult.entries
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
@@ -1,5 +1,7 @@
|
|||||||
"use server";
|
"use server";
|
||||||
|
|
||||||
|
import { headers } from "next/headers";
|
||||||
|
import { auth } from "@/auth";
|
||||||
import { validate } from "@/utils/validate";
|
import { validate } from "@/utils/validate";
|
||||||
import { ValidateError } from "@/lib/errors";
|
import { ValidateError } from "@/lib/errors";
|
||||||
import { createLogger } from "@/lib/logger";
|
import { createLogger } from "@/lib/logger";
|
||||||
@@ -13,8 +15,17 @@ export async function actionProcessOCR(
|
|||||||
input: unknown
|
input: unknown
|
||||||
): Promise<ActionOutputProcessOCR> {
|
): Promise<ActionOutputProcessOCR> {
|
||||||
try {
|
try {
|
||||||
|
const session = await auth.api.getSession({ headers: await headers() });
|
||||||
|
if (!session?.user?.id) {
|
||||||
|
log.warn("Unauthorized OCR attempt");
|
||||||
|
return { success: false, message: "Unauthorized" };
|
||||||
|
}
|
||||||
|
|
||||||
const validatedInput = validate(input, schemaActionInputProcessOCR);
|
const validatedInput = validate(input, schemaActionInputProcessOCR);
|
||||||
return serviceProcessOCR(validatedInput);
|
return serviceProcessOCR({
|
||||||
|
...validatedInput,
|
||||||
|
userId: session.user.id,
|
||||||
|
});
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
if (e instanceof ValidateError) {
|
if (e instanceof ValidateError) {
|
||||||
return { success: false, message: e.message };
|
return { success: false, message: e.message };
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ export const schemaServiceInputProcessOCR = z.object({
|
|||||||
deckId: z.number().int().positive("Deck ID must be positive"),
|
deckId: z.number().int().positive("Deck ID must be positive"),
|
||||||
sourceLanguage: z.string().optional(),
|
sourceLanguage: z.string().optional(),
|
||||||
targetLanguage: z.string().optional(),
|
targetLanguage: z.string().optional(),
|
||||||
|
userId: z.string().min(1, "User ID is required"),
|
||||||
});
|
});
|
||||||
|
|
||||||
export type ServiceInputProcessOCR = z.infer<typeof schemaServiceInputProcessOCR>;
|
export type ServiceInputProcessOCR = z.infer<typeof schemaServiceInputProcessOCR>;
|
||||||
|
|||||||
@@ -1,12 +1,8 @@
|
|||||||
"use server";
|
|
||||||
|
|
||||||
import { executeOCR } from "@/lib/bigmodel/ocr/orchestrator";
|
import { executeOCR } from "@/lib/bigmodel/ocr/orchestrator";
|
||||||
import { repoGetUserIdByDeckId } from "@/modules/deck/deck-repository";
|
import { serviceCheckOwnership } from "@/modules/deck/deck-service";
|
||||||
import { repoCreateNote, repoJoinFields } from "@/modules/note/note-repository";
|
import { serviceCreateNote } from "@/modules/note/note-service";
|
||||||
import { repoCreateCard } from "@/modules/card/card-repository";
|
import { serviceCreateCard } from "@/modules/card/card-service";
|
||||||
import { repoGetNoteTypesByUserId, repoCreateNoteType } from "@/modules/note-type/note-type-repository";
|
import { serviceGetNoteTypesByUserId, serviceCreateNoteType } from "@/modules/note-type/note-type-service";
|
||||||
import { auth } from "@/auth";
|
|
||||||
import { headers } from "next/headers";
|
|
||||||
import { createLogger } from "@/lib/logger";
|
import { createLogger } from "@/lib/logger";
|
||||||
import type { ServiceInputProcessOCR, ServiceOutputProcessOCR } from "./ocr-service-dto";
|
import type { ServiceInputProcessOCR, ServiceOutputProcessOCR } from "./ocr-service-dto";
|
||||||
import { NoteKind } from "../../../generated/prisma/enums";
|
import { NoteKind } from "../../../generated/prisma/enums";
|
||||||
@@ -16,7 +12,7 @@ const log = createLogger("ocr-service");
|
|||||||
const VOCABULARY_NOTE_TYPE_NAME = "Vocabulary (OCR)";
|
const VOCABULARY_NOTE_TYPE_NAME = "Vocabulary (OCR)";
|
||||||
|
|
||||||
async function getOrCreateVocabularyNoteType(userId: string): Promise<number> {
|
async function getOrCreateVocabularyNoteType(userId: string): Promise<number> {
|
||||||
const existingTypes = await repoGetNoteTypesByUserId({ userId });
|
const existingTypes = await serviceGetNoteTypesByUserId({ userId });
|
||||||
const existing = existingTypes.find((nt) => nt.name === VOCABULARY_NOTE_TYPE_NAME);
|
const existing = existingTypes.find((nt) => nt.name === VOCABULARY_NOTE_TYPE_NAME);
|
||||||
|
|
||||||
if (existing) {
|
if (existing) {
|
||||||
@@ -47,7 +43,7 @@ async function getOrCreateVocabularyNoteType(userId: string): Promise<number> {
|
|||||||
|
|
||||||
const css = ".card { font-family: Arial; font-size: 20px; text-align: center; color: black; background-color: white; }";
|
const css = ".card { font-family: Arial; font-size: 20px; text-align: center; color: black; background-color: white; }";
|
||||||
|
|
||||||
const noteTypeId = await repoCreateNoteType({
|
const noteTypeId = await serviceCreateNoteType({
|
||||||
name: VOCABULARY_NOTE_TYPE_NAME,
|
name: VOCABULARY_NOTE_TYPE_NAME,
|
||||||
kind: NoteKind.STANDARD,
|
kind: NoteKind.STANDARD,
|
||||||
css,
|
css,
|
||||||
@@ -63,19 +59,17 @@ async function getOrCreateVocabularyNoteType(userId: string): Promise<number> {
|
|||||||
export async function serviceProcessOCR(
|
export async function serviceProcessOCR(
|
||||||
input: ServiceInputProcessOCR
|
input: ServiceInputProcessOCR
|
||||||
): Promise<ServiceOutputProcessOCR> {
|
): Promise<ServiceOutputProcessOCR> {
|
||||||
log.info("Processing OCR request", { deckId: input.deckId });
|
log.info("Processing OCR request", { deckId: input.deckId, userId: input.userId });
|
||||||
|
|
||||||
const session = await auth.api.getSession({ headers: await headers() });
|
const isOwner = await serviceCheckOwnership({
|
||||||
if (!session?.user?.id) {
|
deckId: input.deckId,
|
||||||
log.warn("Unauthorized OCR attempt");
|
userId: input.userId
|
||||||
return { success: false, message: "Unauthorized" };
|
});
|
||||||
}
|
|
||||||
|
|
||||||
const deckOwner = await repoGetUserIdByDeckId(input.deckId);
|
if (!isOwner) {
|
||||||
if (deckOwner !== session.user.id) {
|
|
||||||
log.warn("Deck ownership mismatch", {
|
log.warn("Deck ownership mismatch", {
|
||||||
deckId: input.deckId,
|
deckId: input.deckId,
|
||||||
userId: session.user.id
|
userId: input.userId
|
||||||
});
|
});
|
||||||
return {
|
return {
|
||||||
success: false,
|
success: false,
|
||||||
@@ -110,33 +104,28 @@ export async function serviceProcessOCR(
|
|||||||
const sourceLanguage = ocrResult.detectedSourceLanguage || input.sourceLanguage || "Unknown";
|
const sourceLanguage = ocrResult.detectedSourceLanguage || input.sourceLanguage || "Unknown";
|
||||||
const targetLanguage = ocrResult.detectedTargetLanguage || input.targetLanguage || "Unknown";
|
const targetLanguage = ocrResult.detectedTargetLanguage || input.targetLanguage || "Unknown";
|
||||||
|
|
||||||
const noteTypeId = await getOrCreateVocabularyNoteType(session.user.id);
|
const noteTypeId = await getOrCreateVocabularyNoteType(input.userId);
|
||||||
|
|
||||||
let pairsCreated = 0;
|
let pairsCreated = 0;
|
||||||
for (const pair of ocrResult.pairs) {
|
for (const pair of ocrResult.pairs) {
|
||||||
try {
|
try {
|
||||||
const now = Date.now();
|
const { id: noteId } = await serviceCreateNote({
|
||||||
const noteId = await repoCreateNote({
|
|
||||||
noteTypeId,
|
noteTypeId,
|
||||||
userId: session.user.id,
|
userId: input.userId,
|
||||||
fields: [pair.word, pair.definition, sourceLanguage, targetLanguage],
|
fields: [pair.word, pair.definition, sourceLanguage, targetLanguage],
|
||||||
tags: ["ocr"],
|
tags: ["ocr"],
|
||||||
});
|
});
|
||||||
|
|
||||||
await repoCreateCard({
|
await serviceCreateCard({
|
||||||
id: BigInt(now + pairsCreated),
|
|
||||||
noteId,
|
noteId,
|
||||||
deckId: input.deckId,
|
deckId: input.deckId,
|
||||||
ord: 0,
|
ord: 0,
|
||||||
due: pairsCreated + 1,
|
|
||||||
});
|
});
|
||||||
|
|
||||||
await repoCreateCard({
|
await serviceCreateCard({
|
||||||
id: BigInt(now + pairsCreated + 10000),
|
|
||||||
noteId,
|
noteId,
|
||||||
deckId: input.deckId,
|
deckId: input.deckId,
|
||||||
ord: 1,
|
ord: 1,
|
||||||
due: pairsCreated + 1,
|
|
||||||
});
|
});
|
||||||
|
|
||||||
pairsCreated++;
|
pairsCreated++;
|
||||||
|
|||||||
@@ -7,8 +7,7 @@ import {
|
|||||||
} from "./translator-action-dto";
|
} from "./translator-action-dto";
|
||||||
import { ValidateError } from "@/lib/errors";
|
import { ValidateError } from "@/lib/errors";
|
||||||
import { createLogger } from "@/lib/logger";
|
import { createLogger } from "@/lib/logger";
|
||||||
import { serviceTranslateText } from "./translator-service";
|
import { serviceTranslateText, serviceGenIPA, serviceGenLanguage } from "./translator-service";
|
||||||
import { getAnswer } from "@/lib/bigmodel/llm";
|
|
||||||
|
|
||||||
const log = createLogger("translator-action");
|
const log = createLogger("translator-action");
|
||||||
|
|
||||||
@@ -40,67 +39,12 @@ export const actionTranslateText = async (
|
|||||||
* @deprecated 保留此函数以支持旧代码(text-speaker 功能)
|
* @deprecated 保留此函数以支持旧代码(text-speaker 功能)
|
||||||
*/
|
*/
|
||||||
export const genIPA = async (text: string) => {
|
export const genIPA = async (text: string) => {
|
||||||
return (
|
return serviceGenIPA({ text });
|
||||||
"[" +
|
|
||||||
(
|
|
||||||
await getAnswer(
|
|
||||||
`
|
|
||||||
<text>${text}</text>
|
|
||||||
|
|
||||||
请生成以上文本的严式国际音标
|
|
||||||
然后直接发给我
|
|
||||||
不要附带任何说明
|
|
||||||
不要擅自增减符号
|
|
||||||
不许用"/"或者"[]"包裹
|
|
||||||
`.trim(),
|
|
||||||
)
|
|
||||||
)
|
|
||||||
.replaceAll("[", "")
|
|
||||||
.replaceAll("]", "") +
|
|
||||||
"]"
|
|
||||||
);
|
|
||||||
};
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @deprecated 保留此函数以支持旧代码(text-speaker 功能)
|
* @deprecated 保留此函数以支持旧代码(text-speaker 功能)
|
||||||
*/
|
*/
|
||||||
export const genLanguage = async (text: string) => {
|
export const genLanguage = async (text: string) => {
|
||||||
const language = await getAnswer([
|
return serviceGenLanguage({ text });
|
||||||
{
|
|
||||||
role: "system",
|
|
||||||
content: `
|
|
||||||
你是一个语言检测工具。请识别文本的语言并返回语言名称。
|
|
||||||
|
|
||||||
返回语言的标准英文名称,例如:
|
|
||||||
- 中文: Chinese
|
|
||||||
- 英语: English
|
|
||||||
- 日语: Japanese
|
|
||||||
- 韩语: Korean
|
|
||||||
- 法语: French
|
|
||||||
- 德语: German
|
|
||||||
- 意大利语: Italian
|
|
||||||
- 葡萄牙语: Portuguese
|
|
||||||
- 西班牙语: Spanish
|
|
||||||
- 俄语: Russian
|
|
||||||
- 阿拉伯语: Arabic
|
|
||||||
- 印地语: Hindi
|
|
||||||
- 泰语: Thai
|
|
||||||
- 越南语: Vietnamese
|
|
||||||
- 等等...
|
|
||||||
|
|
||||||
如果无法识别语言,返回 "Unknown"
|
|
||||||
|
|
||||||
规则:
|
|
||||||
1. 只返回语言的标准英文名称
|
|
||||||
2. 首字母大写,其余小写
|
|
||||||
3. 不要附带任何说明
|
|
||||||
4. 不要擅自增减符号
|
|
||||||
`.trim()
|
|
||||||
},
|
|
||||||
{
|
|
||||||
role: "user",
|
|
||||||
content: `<text>${text}</text>`
|
|
||||||
}
|
|
||||||
]);
|
|
||||||
return language.trim();
|
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -10,3 +10,17 @@ export type ServiceInputTranslateText = {
|
|||||||
};
|
};
|
||||||
|
|
||||||
export type ServiceOutputTranslateText = TSharedTranslationResult;
|
export type ServiceOutputTranslateText = TSharedTranslationResult;
|
||||||
|
|
||||||
|
// DTO types for deprecated genIPA function
|
||||||
|
export type ServiceInputGenIPA = {
|
||||||
|
text: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type ServiceOutputGenIPA = string;
|
||||||
|
|
||||||
|
// DTO types for deprecated genLanguage function
|
||||||
|
export type ServiceInputGenLanguage = {
|
||||||
|
text: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type ServiceOutputGenLanguage = string;
|
||||||
|
|||||||
@@ -1,6 +1,14 @@
|
|||||||
import { executeTranslation } from "@/lib/bigmodel/translator/orchestrator";
|
import { executeTranslation } from "@/lib/bigmodel/translator/orchestrator";
|
||||||
|
import { getAnswer } from "@/lib/bigmodel/llm";
|
||||||
import { repoCreateTranslationHistory, repoSelectLatestTranslation } from "./translator-repository";
|
import { repoCreateTranslationHistory, repoSelectLatestTranslation } from "./translator-repository";
|
||||||
import { ServiceInputTranslateText, ServiceOutputTranslateText } from "./translator-service-dto";
|
import {
|
||||||
|
ServiceInputTranslateText,
|
||||||
|
ServiceOutputTranslateText,
|
||||||
|
ServiceInputGenIPA,
|
||||||
|
ServiceOutputGenIPA,
|
||||||
|
ServiceInputGenLanguage,
|
||||||
|
ServiceOutputGenLanguage,
|
||||||
|
} from "./translator-service-dto";
|
||||||
import { createLogger } from "@/lib/logger";
|
import { createLogger } from "@/lib/logger";
|
||||||
|
|
||||||
const log = createLogger("translator-service");
|
const log = createLogger("translator-service");
|
||||||
@@ -71,3 +79,72 @@ export const serviceTranslateText = async (
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export const serviceGenIPA = async (
|
||||||
|
dto: ServiceInputGenIPA
|
||||||
|
): Promise<ServiceOutputGenIPA> => {
|
||||||
|
const { text } = dto;
|
||||||
|
return (
|
||||||
|
"[" +
|
||||||
|
(
|
||||||
|
await getAnswer(
|
||||||
|
`
|
||||||
|
<text>${text}</text>
|
||||||
|
|
||||||
|
请生成以上文本的严式国际音标
|
||||||
|
然后直接发给我
|
||||||
|
不要附带任何说明
|
||||||
|
不要擅自增减符号
|
||||||
|
不许用"/"或者"[]"包裹
|
||||||
|
`.trim(),
|
||||||
|
)
|
||||||
|
)
|
||||||
|
.replaceAll("[", "")
|
||||||
|
.replaceAll("]", "") +
|
||||||
|
"]"
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export const serviceGenLanguage = async (
|
||||||
|
dto: ServiceInputGenLanguage
|
||||||
|
): Promise<ServiceOutputGenLanguage> => {
|
||||||
|
const { text } = dto;
|
||||||
|
const language = await getAnswer([
|
||||||
|
{
|
||||||
|
role: "system",
|
||||||
|
content: `
|
||||||
|
你是一个语言检测工具。请识别文本的语言并返回语言名称。
|
||||||
|
|
||||||
|
返回语言的标准英文名称,例如:
|
||||||
|
- 中文: Chinese
|
||||||
|
- 英语: English
|
||||||
|
- 日语: Japanese
|
||||||
|
- 韩语: Korean
|
||||||
|
- 法语: French
|
||||||
|
- 德语: German
|
||||||
|
- 意大利语: Italian
|
||||||
|
- 葡萄牙语: Portuguese
|
||||||
|
- 西班牙语: Spanish
|
||||||
|
- 俄语: Russian
|
||||||
|
- 阿拉伯语: Arabic
|
||||||
|
- 印地语: Hindi
|
||||||
|
- 泰语: Thai
|
||||||
|
- 越南语: Vietnamese
|
||||||
|
- 等等...
|
||||||
|
|
||||||
|
如果无法识别语言,返回 "Unknown"
|
||||||
|
|
||||||
|
规则:
|
||||||
|
1. 只返回语言的标准英文名称
|
||||||
|
2. 首字母大写,其余小写
|
||||||
|
3. 不要附带任何说明
|
||||||
|
4. 不要擅自增减符号
|
||||||
|
`.trim()
|
||||||
|
},
|
||||||
|
{
|
||||||
|
role: "user",
|
||||||
|
content: `<text>${text}</text>`
|
||||||
|
}
|
||||||
|
]);
|
||||||
|
return language.trim();
|
||||||
|
};
|
||||||
|
|||||||
Reference in New Issue
Block a user