refactor: Next.js 15兼容性改进和数据库模型标准化

This commit is contained in:
2026-02-01 16:53:09 +08:00
parent a33be23281
commit 76eefdc151
3 changed files with 81 additions and 66 deletions
+37 -28
View File
@@ -8,22 +8,23 @@ datasource db {
url = env("DATABASE_URL") url = env("DATABASE_URL")
} }
model external_links { model ExternalLink {
id String @id id String @id @default(cuid())
type LinkType type LinkType
url String url String
title String? title String?
projectId String projectId String
projects projects @relation(fields: [projectId], references: [id], onDelete: Cascade) project Project @relation(fields: [projectId], references: [id], onDelete: Cascade)
@@unique([projectId, url]) @@unique([projectId, url])
@@index([projectId], map: "idx_link_projectId") @@index([projectId], map: "idx_link_projectId")
@@index([type], map: "idx_link_type") @@index([type], map: "idx_link_type")
@@index([type, url], map: "idx_link_type_url") @@index([type, url], map: "idx_link_type_url")
@@index([url], map: "idx_link_url") @@index([url], map: "idx_link_url")
@@map("external_links")
} }
model keyword_cloud_error_logs { model KeywordCloudErrorLog {
id Int @id @default(autoincrement()) id Int @id @default(autoincrement())
quarter String quarter String
keyword String? keyword String?
@@ -34,9 +35,10 @@ model keyword_cloud_error_logs {
@@index([errorType], map: "idx_keywordCloudErrorLog_errorType") @@index([errorType], map: "idx_keywordCloudErrorLog_errorType")
@@index([quarter], map: "idx_keywordCloudErrorLog_quarter") @@index([quarter], map: "idx_keywordCloudErrorLog_quarter")
@@map("keyword_cloud_error_logs")
} }
model keywords { model Keyword {
id Int @id @default(autoincrement()) id Int @id @default(autoincrement())
word String word String
trendScore Int trendScore Int
@@ -47,16 +49,17 @@ model keywords {
detailPointsEn Json? detailPointsEn Json?
visualConfig Json visualConfig Json
createdAt DateTime @default(now()) createdAt DateTime @default(now())
updatedAt DateTime updatedAt DateTime @updatedAt
quarters quarters @relation(fields: [quarterId], references: [id], onDelete: Cascade) quarter Quarter @relation(fields: [quarterId], references: [id], onDelete: Cascade)
@@index([quarterId], map: "idx_keyword_quarterId") @@index([quarterId], map: "idx_keyword_quarterId")
@@index([trendScore], map: "idx_keyword_trendScore") @@index([trendScore], map: "idx_keyword_trendScore")
@@index([word], map: "idx_keyword_word") @@index([word], map: "idx_keyword_word")
@@map("keywords")
} }
model project_discovery_tasks { model ProjectDiscoveryTask {
id String @id id String @id @default(cuid())
status TaskStatus @default(PENDING) status TaskStatus @default(PENDING)
sourceUrl String sourceUrl String
sourceType String @default("manual") sourceType String @default("manual")
@@ -69,26 +72,28 @@ model project_discovery_tasks {
createdAt DateTime @default(now()) createdAt DateTime @default(now())
startedAt DateTime? startedAt DateTime?
completedAt DateTime? completedAt DateTime?
updatedAt DateTime updatedAt DateTime @updatedAt
projects projects? @relation(fields: [projectId], references: [id]) project Project? @relation(fields: [projectId], references: [id])
@@index([projectId], map: "idx_task_project_id") @@index([projectId], map: "idx_task_project_id")
@@index([sourceUrl], map: "idx_task_source_url") @@index([sourceUrl], map: "idx_task_source_url")
@@index([status, createdAt], map: "idx_task_status_created") @@index([status, createdAt], map: "idx_task_status_created")
@@map("project_discovery_tasks")
} }
model project_tags { model ProjectTag {
projectId String projectId String
tagId String tagId String
projects projects @relation(fields: [projectId], references: [id], onDelete: Cascade) project Project @relation(fields: [projectId], references: [id], onDelete: Cascade)
tags tags @relation(fields: [tagId], references: [id], onDelete: Cascade) tag Tag @relation(fields: [tagId], references: [id], onDelete: Cascade)
@@id([projectId, tagId]) @@id([projectId, tagId])
@@index([tagId]) @@index([tagId])
@@map("project_tags")
} }
model projects { model Project {
id String @id id String @id @default(cuid())
name String name String
nameEn String? nameEn String?
slug String @unique slug String @unique
@@ -99,19 +104,20 @@ model projects {
status ProjectStatus @default(ACTIVE) status ProjectStatus @default(ACTIVE)
source String? source String?
createdAt DateTime @default(now()) createdAt DateTime @default(now())
updatedAt DateTime updatedAt DateTime @updatedAt
embedding Unsupported("vector")? embedding Unsupported("vector")?
embeddingUpdatedAt DateTime? embeddingUpdatedAt DateTime?
external_links external_links[] links ExternalLink[]
project_discovery_tasks project_discovery_tasks[] projectDiscoveryTasks ProjectDiscoveryTask[]
project_tags project_tags[] tags ProjectTag[]
@@index([embedding], map: "idx_project_embedding_cosine") @@index([embedding], map: "idx_project_embedding_cosine")
@@index([slug], map: "idx_project_slug") @@index([slug], map: "idx_project_slug")
@@index([status, createdAt], map: "idx_project_status_createdAt") @@index([status, createdAt], map: "idx_project_status_createdAt")
@@map("projects")
} }
model quarters { model Quarter {
id Int @id @default(autoincrement()) id Int @id @default(autoincrement())
quarter String @unique quarter String @unique
title String title String
@@ -121,25 +127,27 @@ model quarters {
displayOrder Int @default(0) displayOrder Int @default(0)
isActive Boolean @default(true) isActive Boolean @default(true)
createdAt DateTime @default(now()) createdAt DateTime @default(now())
updatedAt DateTime updatedAt DateTime @updatedAt
keywords keywords[] keywords Keyword[]
@@index([displayOrder], map: "idx_quarter_displayOrder") @@index([displayOrder], map: "idx_quarter_displayOrder")
@@index([quarter], map: "idx_quarter_quarter") @@index([quarter], map: "idx_quarter_quarter")
@@map("quarters")
} }
model tags { model Tag {
id String @id id String @id @default(cuid())
name String @unique name String @unique
nameEn String? nameEn String?
slug String @unique slug String @unique
createdAt DateTime @default(now()) createdAt DateTime @default(now())
project_tags project_tags[] projects ProjectTag[]
@@index([slug], map: "idx_tag_slug") @@index([slug], map: "idx_tag_slug")
@@map("tags")
} }
model visual_style_rules { model VisualStyleRule {
id Int @id @default(autoincrement()) id Int @id @default(autoincrement())
name String @unique name String @unique
minScore Int minScore Int
@@ -151,10 +159,11 @@ model visual_style_rules {
priority Int @default(0) priority Int @default(0)
enabled Boolean @default(true) enabled Boolean @default(true)
createdAt DateTime @default(now()) createdAt DateTime @default(now())
updatedAt DateTime updatedAt DateTime @updatedAt
@@index([enabled], map: "idx_visualStyleRule_enabled") @@index([enabled], map: "idx_visualStyleRule_enabled")
@@index([minScore, maxScore], map: "idx_visualStyleRule_scoreRange") @@index([minScore, maxScore], map: "idx_visualStyleRule_scoreRange")
@@map("visual_style_rules")
} }
enum LinkType { enum LinkType {
+36 -27
View File
@@ -1,41 +1,44 @@
import { KeywordCloud } from './components/KeywordCloud'; import { KeywordCloud } from "./components/KeywordCloud";
import { getTranslations } from 'next-intl/server'; import { getTranslations } from "next-intl/server";
interface PageProps { interface PageProps {
params: { params?: Promise<{
locale: string; locale?: string | string[];
}; }>;
searchParams: { searchParams?: Promise<{
quarter?: string; quarter?: string | string[];
}; }>;
} }
export async function generateMetadata({ export async function generateMetadata({ params }: PageProps) {
params const resolvedParams = (await params) ?? {};
}: { const rawLocale = resolvedParams.locale;
params: Promise<{ locale: string }> const locale = (Array.isArray(rawLocale) ? rawLocale[0] : rawLocale) ?? "zh";
}) { const t = await getTranslations("keywordCloud");
const { locale } = await params;
const t = await getTranslations('keywordCloud');
return { return {
title: t('metaTitle'), title: t("metaTitle"),
description: t('metaDescription'), description: t("metaDescription"),
}; };
} }
export default async function KeywordCloudPage({ searchParams, params }: PageProps) { export default async function KeywordCloudPage({ searchParams, params }: PageProps) {
const { locale } = await params; const resolvedParams = (await params) ?? {};
const t = await getTranslations('keywordCloud'); const rawLocale = resolvedParams.locale;
const locale = (Array.isArray(rawLocale) ? rawLocale[0] : rawLocale) ?? "zh";
const resolvedSearchParams = (await searchParams) ?? {};
const t = await getTranslations("keywordCloud");
// 如果 URL 中有 quarter 参数,使用它;否则使用默认季度 // 如果 URL 中有 quarter 参数,使用它;否则使用默认季度
const quarter = searchParams.quarter || '2024-Q1'; const rawQuarter = resolvedSearchParams.quarter;
const quarter = (Array.isArray(rawQuarter) ? rawQuarter[0] : rawQuarter) || "2024-Q1";
const texts = { const texts = {
loading: t('loading'), loading: t("loading"),
loadFailed: t('loadFailed'), loadFailed: t("loadFailed"),
retry: t('retry'), retry: t("retry"),
hotKeyword: t('hotKeyword'), hotKeyword: t("hotKeyword"),
}; };
return ( return (
@@ -43,13 +46,19 @@ export default async function KeywordCloudPage({ searchParams, params }: PagePro
{/* 页面标题 */} {/* 页面标题 */}
<header className="relative z-10 pt-16 pb-8 text-center max-w-4xl mx-auto px-4"> <header className="relative z-10 pt-16 pb-8 text-center max-w-4xl mx-auto px-4">
<div className="inline-block bg-accent dark:bg-purple-700 border-2 border-black px-3 py-1 font-display font-bold text-xs mb-4 shadow-hard-sm rotate-[-2deg]"> <div className="inline-block bg-accent dark:bg-purple-700 border-2 border-black px-3 py-1 font-display font-bold text-xs mb-4 shadow-hard-sm rotate-[-2deg]">
{t('badge')} {t("badge")}
</div> </div>
<h1 className="font-display text-5xl md:text-7xl font-bold mb-6 tracking-tighter leading-tight"> <h1 className="font-display text-5xl md:text-7xl font-bold mb-6 tracking-tighter leading-tight">
{t('title')} <span className="text-transparent bg-clip-text bg-gradient-to-r from-blue-500 to-purple-500" style={{ WebkitTextStroke: '1.5px black' }}>{t('titleHighlight')}</span> {t("title")}{" "}
<span
className="text-transparent bg-clip-text bg-gradient-to-r from-blue-500 to-purple-500"
style={{ WebkitTextStroke: "1.5px black" }}
>
{t("titleHighlight")}
</span>
</h1> </h1>
<p className="text-lg md:text-xl max-w-2xl mx-auto font-medium text-gray-700 dark:text-gray-300 mb-8"> <p className="text-lg md:text-xl max-w-2xl mx-auto font-medium text-gray-700 dark:text-gray-300 mb-8">
{t('subtitle')} {t("subtitle")}
</p> </p>
</header> </header>
@@ -1,25 +1,22 @@
import { NextResponse } from 'next/server'; import { NextResponse } from "next/server";
import { getKeywordsByQuarter } from '@/hooks/useKeywordCloud'; import { getKeywordsByQuarter } from "@/hooks/useKeywordCloud";
export const dynamic = 'force-dynamic'; export const dynamic = "force-dynamic";
/** /**
* GET /api/keyword-cloud/keywords/[quarter] * GET /api/keyword-cloud/keywords/[quarter]
* 获取指定季度的关键词 * 获取指定季度的关键词
*/ */
export async function GET( export async function GET(request: Request, { params }: { params: Promise<{ quarter: string }> }) {
request: Request,
{ params }: { params: { quarter: string } }
) {
try { try {
const { quarter } = params; const { quarter } = await params;
// 验证 quarter 格式 // 验证 quarter 格式
if (!/^\d{4}-Q[1-4]$/.test(quarter)) { if (!/^\d{4}-Q[1-4]$/.test(quarter)) {
return NextResponse.json( return NextResponse.json(
{ {
success: false, success: false,
error: 'Invalid quarter format. Expected: YYYY-QN', error: "Invalid quarter format. Expected: YYYY-QN",
}, },
{ status: 400 } { status: 400 }
); );
@@ -47,11 +44,11 @@ export async function GET(
keywords: data.keywords, keywords: data.keywords,
}); });
} catch (error) { } catch (error) {
console.error('Error fetching keywords:', error); console.error("Error fetching keywords:", error);
return NextResponse.json( return NextResponse.json(
{ {
success: false, success: false,
error: 'Failed to fetch keywords', error: "Failed to fetch keywords",
}, },
{ status: 500 } { status: 500 }
); );