2026-05-27 13:16:37 +08:00

174 lines
5.2 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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<TaobaoQaRow> = {
success: true,
itemId,
page,
totalPages,
rows,
};
return NextResponse.json(payload);
} catch (error) {
logger.error("request_failed", error);
return jsonError(502, "问大家接口调用失败");
}
}