174 lines
5.2 KiB
TypeScript
174 lines
5.2 KiB
TypeScript
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, "问大家接口调用失败");
|
||
}
|
||
}
|