import { NextRequest, NextResponse } from "next/server"; import { z } from "zod"; import { createLogger } from "@/lib/observability/logger"; import { getResolvedProductPlanningApiConfig } from "@/lib/product-planning/server-config"; import { isProductPlanningEnabled, PRODUCT_PLANNING_DISABLED_MESSAGE, } from "@/lib/product-planning/server-feature"; import type { ProductPlanningRowsResponse, TaobaoQaRow } from "@/lib/product-planning/types"; import { getProductPlanningToken } from "@/lib/product-planning/server-auth"; import { normalizeText, truncateText, uniqueBy } from "@/lib/product-planning/utils"; export const runtime = "nodejs"; const logger = createLogger("product-planning-taobao-qa-api"); const requestSchema = z.object({ itemId: z.string().trim().min(1), page: z.number().int().min(1).max(20).default(1), }); function jsonError(status: number, error: string) { return NextResponse.json({ success: false, error }, { status }); } function cleanAnswerContent(value: unknown) { return normalizeText(value).replace( /\d{4}-\d{2}-\d{2}\s\d{2}:\d{2}:\d{2}(?:\s*[::]\s*[^::\n]+)?[::]?/g, "" ); } function formatQaRows(itemId: string, payload: any): TaobaoQaRow[] { const groups = Array.isArray(payload?.data?.list) ? payload.data.list : Array.isArray(payload?.list) ? payload.list : []; const rows: TaobaoQaRow[] = []; for (const group of groups) { const questions = Array.isArray(group) ? group : [group]; for (const question of questions) { const questionText = normalizeText(question?.title || question?.question); const questionDetail = normalizeText(question?.content); const answers = Array.isArray(question?.answerList) ? question.answerList : Array.isArray(question?.subFeeds) ? question.subFeeds.map((answer: any) => ({ content: answer?.title || "", official: answer?.official || false, user: answer?.user, date: answer?.date || answer?.gmtCreate || "", })) : []; if (answers.length === 0) { rows.push({ itemId, question: questionText || questionDetail || "未命名问题", questionDetail: questionDetail && questionDetail !== questionText ? questionDetail : undefined, answer: "", answerUser: undefined, answerDate: undefined, isOfficial: false, }); continue; } for (const answer of answers) { rows.push({ itemId, question: questionText || questionDetail || "未命名问题", questionDetail: questionDetail && questionDetail !== questionText ? questionDetail : undefined, answer: truncateText(cleanAnswerContent(answer?.content), 240), answerUser: normalizeText(answer?.userNick || answer?.display_user_nick || answer?.user?.nick) || undefined, answerDate: normalizeText(answer?.date || answer?.gmtCreate) || undefined, isOfficial: Boolean(answer?.official), }); } } } return uniqueBy(rows, (row) => [ row.itemId, row.question, row.questionDetail || "", row.answer, row.answerUser || "", row.answerDate || "", row.isOfficial ? "1" : "0", ].join("::") ); } export async function POST(request: NextRequest) { const authToken = await getProductPlanningToken(request); if (!authToken?.sub) { return jsonError(401, "未登录"); } if (!(await isProductPlanningEnabled())) { return jsonError(403, PRODUCT_PLANNING_DISABLED_MESSAGE); } let body: unknown; try { body = await request.json(); } catch { return jsonError(400, "请求体不是合法 JSON"); } const parsed = requestSchema.safeParse(body); if (!parsed.success) { return jsonError(400, "请求参数不合法"); } const config = await getResolvedProductPlanningApiConfig(); const qaApiToken = config.qaToken; if (!qaApiToken) { return jsonError(500, "缺少商品策划工具问大家 Token,请前往系统设置配置"); } const { itemId, page } = parsed.data; const apiUrl = `${config.qaApiUrl}?itemId=${encodeURIComponent(itemId)}` + `&page=${page}&token=${encodeURIComponent(qaApiToken)}`; try { const response = await fetch(apiUrl, { signal: AbortSignal.timeout(15000), headers: { Accept: "application/json", }, cache: "no-store", }); if (!response.ok) { return jsonError(502, `问大家接口请求失败:${response.status}`); } const data = await response.json(); const rows = formatQaRows(itemId, data); const totalPages = Number.parseInt( String( data?.data?.totalPages || data?.data?.pageCount || data?.pageCount || data?.pagination?.pageCount || 1 ), 10 ) || 1; const payload: ProductPlanningRowsResponse = { success: true, itemId, page, totalPages, rows, }; return NextResponse.json(payload); } catch (error) { logger.error("request_failed", error); return jsonError(502, "问大家接口调用失败"); } }