Your Name 478562c312
Some checks failed
Lint / build (20) (push) Has been cancelled
Playwright Tests / test (push) Has been cancelled
feat: migrate product planning workspace
2026-03-26 16:38:48 +08:00

110 lines
3.7 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 type { ProductPlanningRowsResponse, TaobaoReviewRow } from "@/lib/product-planning/types";
import { getResolvedProductPlanningApiConfig } from "@/lib/product-planning/server-config";
import {
isProductPlanningEnabled,
PRODUCT_PLANNING_DISABLED_MESSAGE,
} from "@/lib/product-planning/server-feature";
import { getProductPlanningToken } from "@/lib/product-planning/server-auth";
import { ensureHttpsUrl } from "@/lib/product-planning/utils";
export const runtime = "nodejs";
function normalizeMediaUrl(value: unknown) {
if (typeof value === "string") {
return ensureHttpsUrl(value);
}
if (value && typeof value === "object") {
const candidate = (value as Record<string, unknown>).url;
if (typeof candidate === "string") {
return ensureHttpsUrl(candidate);
}
}
return "";
}
function jsonError(status: number, error: string) {
return NextResponse.json({ success: false, error }, { status });
}
export async function GET(request: NextRequest) {
const token = await getProductPlanningToken(request);
if (!token?.sub) {
return jsonError(401, "未登录");
}
if (!(await isProductPlanningEnabled())) {
return jsonError(403, PRODUCT_PLANNING_DISABLED_MESSAGE);
}
const itemId = String(request.nextUrl.searchParams.get("id") || "").trim();
const page = Math.max(
Number.parseInt(String(request.nextUrl.searchParams.get("page") || "1"), 10) || 1,
1
);
if (!itemId) {
return jsonError(400, "缺少商品ID参数");
}
const config = await getResolvedProductPlanningApiConfig();
const apiKey = config.taobaoApiKey;
const apiSecret = config.taobaoApiSecret;
if (!apiKey || !apiSecret) {
return jsonError(500, "缺少商品策划工具淘宝 API Key / Secret,请前往系统设置配置");
}
const apiUrl =
`${config.oneboundBaseUrl}/taobao/item_review/?key=${encodeURIComponent(apiKey)}` +
`&secret=${encodeURIComponent(apiSecret)}&num_iid=${encodeURIComponent(itemId)}` +
`&page=${page}&version=1&cache=no&lang=zh-CN`;
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();
if (data?.error_code && data.error_code !== "0000") {
return jsonError(502, data.error || data.reason || "淘宝评价接口返回错误");
}
const rows: TaobaoReviewRow[] = Array.isArray(data?.items?.item)
? data.items.item.map((item: any) => ({
itemId,
date: String(item?.rate_date || "").trim(),
content: String(item?.rate_content || "").trim(),
appendComment: String(item?.add_feedback || "").trim() || undefined,
sku: String(item?.auction_sku || "").trim() || undefined,
userId: String(item?.display_user_nick || "").trim() || undefined,
images: Array.isArray(item?.pics)
? item.pics.map((image: unknown) => normalizeMediaUrl(image)).filter(Boolean)
: [],
videoUrls: item?.video ? [normalizeMediaUrl(item.video)].filter(Boolean) : [],
}))
: [];
const payload: ProductPlanningRowsResponse<TaobaoReviewRow> = {
success: true,
itemId,
page,
totalPages: Number.parseInt(String(data?.items?.totalpage || "1"), 10) || 1,
rows,
};
return NextResponse.json(payload);
} catch (error) {
console.error("[ProductPlanning][Reviews] request failed:", error);
return jsonError(502, "淘宝评价接口调用失败");
}
}