From 8faa798401021c279b23f1552ef4c00bdbfa150c Mon Sep 17 00:00:00 2001 From: Caihaohan Date: Tue, 27 Jan 2026 20:14:43 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20=E6=B7=BB=E5=8A=A0=E6=89=B9=E9=87=8F?= =?UTF-8?q?=E5=86=99=E5=85=A5=E5=85=B3=E9=94=AE=E8=AF=8D=20API?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/app/api/keyword-cloud/keywords/route.ts | 131 ++++++++++++++++++++ 1 file changed, 131 insertions(+) create mode 100644 src/app/api/keyword-cloud/keywords/route.ts diff --git a/src/app/api/keyword-cloud/keywords/route.ts b/src/app/api/keyword-cloud/keywords/route.ts new file mode 100644 index 0000000..e85e2ee --- /dev/null +++ b/src/app/api/keyword-cloud/keywords/route.ts @@ -0,0 +1,131 @@ +import { NextResponse } from 'next/server'; +import { prisma } from '@/lib/prisma'; +import { BatchKeywordsRequestSchema } from '@/lib/validations'; +import { upsertQuarter, createKeywords, logKeywordCloudError } from '@/hooks/useKeywordCloud'; +import crypto from 'crypto'; + +export const dynamic = 'force-dynamic'; + +/** + * POST /api/keyword-cloud/keywords + * 批量写入关键词(n8n 工作流使用) + */ +export async function POST(request: Request) { + try { + // 1. 验证 API Key + const body = await request.json(); + const { apiKey, ...requestData } = body; + + if (!apiKey) { + return NextResponse.json( + { + success: false, + error: 'Missing API key', + }, + { status: 401 } + ); + } + + const expectedApiKey = process.env.WEBHOOK_API_KEY; + if (!expectedApiKey) { + console.error('WEBHOOK_API_KEY not configured'); + return NextResponse.json( + { + success: false, + error: 'Server configuration error', + }, + { status: 500 } + ); + } + + // 使用 timing-safe 比较防止时序攻击 + try { + const apiKeyBuffer = Buffer.from(apiKey, 'utf-8'); + const expectedBuffer = Buffer.from(expectedApiKey, 'utf-8'); + + if (apiKeyBuffer.length !== expectedBuffer.length || + !crypto.timingSafeEqual(apiKeyBuffer, expectedBuffer)) { + return NextResponse.json( + { + success: false, + error: 'Invalid API key', + }, + { status: 401 } + ); + } + } catch (error) { + return NextResponse.json( + { + success: false, + error: 'Authentication failed', + }, + { status: 401 } + ); + } + + // 2. 验证请求数据 + const validationResult = BatchKeywordsRequestSchema.safeParse(requestData); + + if (!validationResult.success) { + return NextResponse.json( + { + success: false, + error: 'Validation failed', + details: validationResult.error.errors, + }, + { status: 400 } + ); + } + + const { quarter, keywords } = validationResult.data; + + // 3. 创建或更新季度记录 + const quarterData = await upsertQuarter(quarter, { + title: `${quarter.replace('-', '年')}季度`, + titleEn: quarter.replace('-', ' '), + }); + + // 4. 批量创建关键词 + const result = await createKeywords(quarterData.id, keywords); + + // 5. 记录错误 + for (const error of result.errors) { + await logKeywordCloudError({ + quarter, + keyword: error.word, + errorType: 'DB_ERROR', + errorMessage: error.error, + }); + } + + // 6. 返回结果 + return NextResponse.json({ + success: true, + created: result.created, + failed: result.failed, + errors: result.errors, + }); + } catch (error) { + console.error('Error creating keywords:', error); + + // 记录未捕获的错误 + try { + await logKeywordCloudError({ + quarter: 'unknown', + errorType: 'API_ERROR', + errorMessage: error instanceof Error ? error.message : 'Unknown error', + rawData: { error }, + }); + } catch (logError) { + console.error('Failed to log error:', logError); + } + + return NextResponse.json( + { + success: false, + error: 'Failed to create keywords', + }, + { status: 500 } + ); + } +}