From be02833b1142db8a55c43aabb1d51c0f61cbe34b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E9=BB=84=E8=87=AA=E8=BE=BE?= <980324341@qq.com> Date: Mon, 31 Aug 2026 19:42:18 +0800 Subject: [PATCH] =?UTF-8?q?task-19:=20=E6=8B=86=E5=88=86=20shop-match=20/?= =?UTF-8?q?=20product-risk=20=E6=A8=A1=E5=9D=97=20API=20=E8=87=B3=20types/?= =?UTF-8?q?modules/shop-match.ts=EF=BC=8Cjava-modules.ts=20re-export?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - product-risk 15 函数 + shop-match 17 函数 + 全套类型(含 ShopMatch 对 ProductRisk 的 type alias) - 保留模块的 PatrolDelete/QueryAsin/Withdraw alias 通过 import type 兜底引用 - QueryAsinCountryAsins 内联定义避免跨模块循环依赖 - 10 个测试:双模块 CRUD/preference/match/task 系列/progressBatch 双端/dashboard/skip-asins/download/export compat --- frontend-vue/src/shared/api/java-modules.ts | 534 +--------------- .../shared/api/types/modules/shop-match.ts | 597 ++++++++++++++++++ frontend-vue/tests/modules-shop-match.test.ts | 272 ++++++++ 3 files changed, 876 insertions(+), 527 deletions(-) create mode 100644 frontend-vue/src/shared/api/types/modules/shop-match.ts create mode 100644 frontend-vue/tests/modules-shop-match.test.ts diff --git a/frontend-vue/src/shared/api/java-modules.ts b/frontend-vue/src/shared/api/java-modules.ts index f8dbae53..92e91eca 100644 --- a/frontend-vue/src/shared/api/java-modules.ts +++ b/frontend-vue/src/shared/api/java-modules.ts @@ -18,6 +18,13 @@ export * from "./types/modules/similar-asin.ts"; export * from "./types/modules/appearance-patent.ts"; export * from "./types/modules/delete-brand.ts"; export * from "./types/modules/price-track.ts"; +export * from "./types/modules/shop-match.ts"; +import type { + ProductRiskCandidateVo, + ProductRiskDashboardVo, + ProductRiskMatchShopsVo, + ProductRiskShopQueueItem, +} from "./types/modules/shop-match.ts"; const JAVA_API_PREFIX = "/newApi/api"; @@ -152,509 +159,6 @@ export async function uploadTempFileToJava( } -/** 商品风险处理:备选店铺与匹配相关数据结构。 */ -export interface ProductRiskCandidateVo { - id: number; - shop_name: string; - created_at?: string; -} - -export interface ProductRiskShopQueueItem { - shopName: string; - matched: boolean; - shopId?: string; - platform?: string; - companyName?: string; - openStoreUrl?: string; - matchedUserId?: number; - matchStatus?: string; - matchMessage?: string; - queryAsins?: QueryAsinCountryAsins[]; - skipAsinsByCountry?: Record; - skip_asins_by_country?: Record; - skipAsinDetailsByCountry?: Record>; - skip_asin_details_by_country?: Record>; -} - -export interface ShopMatchResultRow { - asin?: string; - minimumPrice?: string; - minimum_price?: string; - status?: string; - done?: boolean; -} - -export interface ShopMatchSubmitShopPayload { - shopName?: string; - error?: string; - countries?: Record; - skipAsinsByCountry?: Record; - skip_asins_by_country?: Record; - skipAsinDetailsByCountry?: Record>; - skip_asin_details_by_country?: Record>; -} - -export interface ProductRiskMatchShopsVo { - items: ProductRiskShopQueueItem[]; -} - -export function listProductRiskCandidates() { - return unwrapJavaResponse( - get>( - `${JAVA_API_PREFIX}/product-risk-resolve/candidates`, - { - params: { user_id: getCurrentUserId() }, - }, - ), - ); -} - -export function addProductRiskCandidate(shopName: string) { - return unwrapJavaResponse( - post< - JavaApiResponse, - { user_id: number; shop_name: string } - >(`${JAVA_API_PREFIX}/product-risk-resolve/candidates`, { - user_id: getCurrentUserId(), - shop_name: shopName, - }), - ); -} - -export function deleteProductRiskCandidate(id: number) { - return unwrapJavaResponse( - del>( - `${JAVA_API_PREFIX}/product-risk-resolve/candidates/${id}`, - { - params: { user_id: getCurrentUserId() }, - }, - ), - ); -} - -export interface ProductRiskCountryPreferenceVo { - country_codes: string[]; -} - -export function getProductRiskCountryPreference() { - return unwrapJavaResponse( - get>( - `${JAVA_API_PREFIX}/product-risk-resolve/country-preference`, - { - params: { user_id: getCurrentUserId() }, - }, - ), - ); -} - -export function putProductRiskCountryPreference(countryCodes: string[]) { - return unwrapJavaResponse( - put< - JavaApiResponse, - { user_id: number; country_codes: string[] } - >(`${JAVA_API_PREFIX}/product-risk-resolve/country-preference`, { - user_id: getCurrentUserId(), - country_codes: countryCodes, - }), - ); -} - -export function matchProductRiskShops(shopNames: string[]) { - return unwrapJavaResponse( - post< - JavaApiResponse, - { user_id: number; shop_names: string[] } - >(`${JAVA_API_PREFIX}/product-risk-resolve/match-shops`, { - user_id: getCurrentUserId(), - shop_names: shopNames, - }), - ); -} - -export interface ProductRiskDashboardVo { - candidateCount: number; - processedTaskCount: number; - successTaskCount: number; - failedTaskCount: number; -} - -export interface ProductRiskHistoryItem { - resultId?: number; - taskId?: number; - shopName?: string; - shopId?: string; - platform?: string; - companyName?: string; - matched?: boolean; - matchStatus?: string; - matchMessage?: string; - taskStatus?: string; - success?: boolean; - error?: string; - outputFilename?: string; - downloadUrl?: string; - fileJobId?: number; - fileStatus?: string; - fileError?: string; - fileReady?: boolean; - scheduledAt?: string; - skipAsinsByCountry?: Record; - skip_asins_by_country?: Record; - skipAsinDetailsByCountry?: Record>; - skip_asin_details_by_country?: Record>; -} - -export interface ProductRiskHistoryVo { - items: ProductRiskHistoryItem[]; -} - -export interface ProductRiskTaskSummary { - id?: number; - taskNo?: string; - status?: string; - errorMessage?: string; - createdAt?: string; - updatedAt?: string; - finishedAt?: string; - scheduledAt?: string; - countryCodes?: string[]; - currentStageIndex?: number; - activeStageIndex?: number; - scheduleStages?: ShopMatchTaskStage[]; -} - -export interface ProductRiskTaskDetailVo { - task?: ProductRiskTaskSummary; - items?: ProductRiskHistoryItem[]; -} - -export interface ProductRiskTaskBatchVo { - items: ProductRiskTaskDetailVo[]; - missingTaskIds?: number[]; -} - -export interface ProductRiskPendingDeleteVo { - removed: boolean; -} - -export interface ProductRiskCreateTaskVo { - taskId: number; - items: ProductRiskHistoryItem[]; -} - -export type ShopMatchCandidateVo = ProductRiskCandidateVo; -export type ShopMatchCountryPreferenceVo = ProductRiskCountryPreferenceVo; -export type ShopMatchShopQueueItem = ProductRiskShopQueueItem; -export type ShopMatchDashboardVo = ProductRiskDashboardVo; -export type ShopMatchHistoryItem = ProductRiskHistoryItem; -export type ShopMatchHistoryVo = ProductRiskHistoryVo; -export type ShopMatchTaskDetailVo = ProductRiskTaskDetailVo; -export type ShopMatchTaskBatchVo = ProductRiskTaskBatchVo; - -export interface ShopMatchCreateTaskItem { - shopName: string; - matched: boolean; - shopId?: string; - platform?: string; - companyName?: string; - openStoreUrl?: string; - matchedUserId?: number; - matchStatus?: string; - matchMessage?: string; -} - -export interface ShopMatchCreateTaskResultItem { - resultId?: number; - taskId?: number; - shopName?: string; - shopId?: string; - platform?: string; - companyName?: string; - matched?: boolean; - matchStatus?: string; - matchMessage?: string; - taskStatus?: string; - success?: boolean; - error?: string; - outputFilename?: string; - downloadUrl?: string; - fileJobId?: number; - fileStatus?: string; - fileError?: string; - fileReady?: boolean; - scheduledAt?: string; -} - -export interface ShopMatchCreateTaskVo { - taskId: number; - items: ShopMatchCreateTaskResultItem[]; -} - -export interface ShopMatchTaskStage { - stageIndex?: number; - scheduledAt?: string; - status?: string; -} - -export function getProductRiskDashboard() { - return unwrapJavaResponse( - get>( - `${JAVA_API_PREFIX}/product-risk-resolve/dashboard`, - { - params: { user_id: getCurrentUserId() }, - }, - ), - ); -} - -export function getProductRiskHistory() { - return unwrapJavaResponse( - get>( - `${JAVA_API_PREFIX}/product-risk-resolve/history`, - { - params: { user_id: getCurrentUserId() }, - }, - ), - ); -} - -export function deleteProductRiskHistory(resultId: number) { - return unwrapJavaResponse( - del>( - `${JAVA_API_PREFIX}/product-risk-resolve/history/${resultId}`, - { - params: { user_id: getCurrentUserId() }, - }, - ), - ); -} - -export function createProductRiskTask(items: ProductRiskShopQueueItem[]) { - return unwrapJavaResponse( - post< - JavaApiResponse, - { user_id: number; items: ProductRiskShopQueueItem[] } - >(`${JAVA_API_PREFIX}/product-risk-resolve/tasks`, { - user_id: getCurrentUserId(), - items, - }), - ); -} - -export function deleteProductRiskTask(taskId: number) { - return unwrapJavaResponse( - del>( - `${JAVA_API_PREFIX}/product-risk-resolve/tasks/${taskId}`, - { - params: { user_id: getCurrentUserId() }, - }, - ), - ); -} - -export function deletePendingProductRiskShopResult(shopName: string) { - return unwrapJavaResponse( - del>( - `${JAVA_API_PREFIX}/product-risk-resolve/pending-shop-result`, - { - params: { user_id: getCurrentUserId(), shop_name: shopName }, - }, - ), - ); -} - -export function getProductRiskTasksBatch(taskIds: number[]) { - return unwrapJavaResponse( - post, { taskIds: number[] }>( - `${JAVA_API_PREFIX}/product-risk-resolve/tasks/batch`, - { taskIds }, - ), - ); -} - -export function getProductRiskResultDownloadUrl(resultId: number) { - return getJavaDownloadUrl( - `/product-risk-resolve/results/${resultId}/download`, - ); -} - -export function listShopMatchCandidates() { - return unwrapJavaResponse( - get>( - `${JAVA_API_PREFIX}/shop-match/candidates`, - { - params: { user_id: getCurrentUserId() }, - }, - ), - ); -} - -export function addShopMatchCandidate(shopName: string) { - return unwrapJavaResponse( - post< - JavaApiResponse, - { user_id: number; shop_name: string } - >(`${JAVA_API_PREFIX}/shop-match/candidates`, { - user_id: getCurrentUserId(), - shop_name: shopName, - }), - ); -} - -export function deleteShopMatchCandidate(id: number) { - return unwrapJavaResponse( - del>(`${JAVA_API_PREFIX}/shop-match/candidates/${id}`, { - params: { user_id: getCurrentUserId() }, - }), - ); -} - -export function getShopMatchCountryPreference() { - return unwrapJavaResponse( - get>( - `${JAVA_API_PREFIX}/shop-match/country-preference`, - { - params: { user_id: getCurrentUserId() }, - }, - ), - ); -} - -export function putShopMatchCountryPreference(countryCodes: string[]) { - return unwrapJavaResponse( - put< - JavaApiResponse, - { user_id: number; country_codes: string[] } - >(`${JAVA_API_PREFIX}/shop-match/country-preference`, { - user_id: getCurrentUserId(), - country_codes: countryCodes, - }), - ); -} - -export function matchShopMatchShops(shopNames: string[]) { - return unwrapJavaResponse( - post< - JavaApiResponse, - { user_id: number; shop_names: string[] } - >(`${JAVA_API_PREFIX}/shop-match/match-shops`, { - user_id: getCurrentUserId(), - shop_names: shopNames, - }), - ); -} - -export function getShopMatchDashboard() { - return unwrapJavaResponse( - get>( - `${JAVA_API_PREFIX}/shop-match/dashboard`, - { - params: { user_id: getCurrentUserId() }, - }, - ), - ); -} - -export function getShopMatchHistory() { - return unwrapJavaResponse( - get>( - `${JAVA_API_PREFIX}/shop-match/history`, - { - params: { user_id: getCurrentUserId() }, - }, - ), - ); -} - -export function deleteShopMatchHistory(resultId: number) { - return unwrapJavaResponse( - del>(`${JAVA_API_PREFIX}/shop-match/history/${resultId}`, { - params: { user_id: getCurrentUserId() }, - }), - ); -} - -export function createShopMatchTask( - items: ShopMatchCreateTaskItem[], - countryCodes: string[], - scheduleTimes?: string[], -) { - return unwrapJavaResponse( - post< - JavaApiResponse, - { user_id: number; items: ShopMatchCreateTaskItem[]; country_codes: string[]; schedule_times?: string[] } - >(`${JAVA_API_PREFIX}/shop-match/tasks`, { - user_id: getCurrentUserId(), - items, - country_codes: countryCodes, - schedule_times: scheduleTimes, - }), - ); -} - -export function deleteShopMatchTask(taskId: number) { - return unwrapJavaResponse( - del>(`${JAVA_API_PREFIX}/shop-match/tasks/${taskId}`, { - params: { user_id: getCurrentUserId() }, - }), - ); -} - -export function activateShopMatchTask(taskId: number, stageIndex: number) { - return unwrapJavaResponse( - post, undefined>( - `${JAVA_API_PREFIX}/shop-match/tasks/${taskId}/activate?user_id=${encodeURIComponent(String(getCurrentUserId()))}&stage_index=${encodeURIComponent(String(stageIndex))}`, - undefined, - ), - ); -} - -export function completeShopMatchTaskStage(taskId: number, stageIndex: number) { - return unwrapJavaResponse( - post, { stage_index: number }>( - `${JAVA_API_PREFIX}/shop-match/tasks/${taskId}/stage-finished?user_id=${encodeURIComponent(String(getCurrentUserId()))}`, - { stage_index: stageIndex }, - ), - ); -} - -export function getShopMatchTasksBatch(taskIds: number[]) { - return unwrapJavaResponse( - post, { taskIds: number[] }>( - `${JAVA_API_PREFIX}/shop-match/tasks/batch`, - { taskIds }, - ), - ); -} - -export function getShopMatchResultDownloadUrl(resultId: number) { - return getJavaDownloadUrl(`/shop-match/results/${resultId}/download`); -} - -export function getShopMatchTaskSkipAsinsPaginated( - taskId: number, - page: number = 1, - pageSize: number = 1000, - options: { - shopName?: string | string[]; - countryCode?: string | string[]; - } = {}, -) { - return unwrapJavaResponse( - get>( - `${JAVA_API_PREFIX}/shop-match/tasks/${taskId}/skip-asins/paginated`, - { - params: { - page, - page_size: pageSize, - ...(options.shopName ? { shop_name: options.shopName } : {}), - ...(options.countryCode ? { country_code: options.countryCode } : {}), - }, - }, - ), - ); -} - export type PatrolDeleteCandidateVo = ProductRiskCandidateVo; export type PatrolDeleteDashboardVo = ProductRiskDashboardVo; export type PatrolDeleteShopQueueItem = ProductRiskShopQueueItem; @@ -1445,30 +949,6 @@ export function deleteWithdrawHistory(resultId: number) { ); } -export function getShopMatchTaskProgressBatch(taskIds: number[]) { - return postTaskProgressBatch( - `${JAVA_API_PREFIX}/shop-match/tasks/progress/batch`, - taskIds, - ); -} - -export function getProductRiskTaskProgressBatch(taskIds: number[]) { - return postTaskProgressBatch( - `${JAVA_API_PREFIX}/product-risk-resolve/tasks/progress/batch`, - taskIds, - ); -} - -export interface ShopMatchSkipAsinPageVo { - page: number; - pageSize: number; - total: number; - totalPages: number; - queryAsins: QueryAsinCountryAsins[]; - skipAsinsByCountry: Record; - skipAsinDetailsByCountry: Record>; -} - // ========== 视频复刻 / 图生视频 ========== export interface ImageVideoDouyinCopyVo { diff --git a/frontend-vue/src/shared/api/types/modules/shop-match.ts b/frontend-vue/src/shared/api/types/modules/shop-match.ts new file mode 100644 index 00000000..345a4fbc --- /dev/null +++ b/frontend-vue/src/shared/api/types/modules/shop-match.ts @@ -0,0 +1,597 @@ +import { get, post, put, del, type JavaApiResponse, unwrapJavaResponse } from '../../http.ts' +import { buildJavaUrl, JAVA_API_PREFIX } from '../../url.ts' +import { getCurrentUserId } from '../../user.ts' +import { getJavaDownloadUrl } from '../../download-url.ts' +import { getTaskProgressCacheTtlMs, getTaskProgressTimeoutMs } from '../../../task-progress-config.ts' +import { createTaskProgressRequestCache } from '../../../task-progress-request-cache.ts' +import { API_ENDPOINTS } from '../../endpoints.ts' + +/** 与 query-asin 模块同构的共享行结构(避免跨模块循环依赖,类型内联) */ +export interface QueryAsinCountryAsins { + country: string; + asins: string[]; +} + +/** 商品风险处理:备选店铺与匹配相关数据结构。 */ +export interface ProductRiskCandidateVo { + id: number; + shop_name: string; + created_at?: string; +} + +export interface ProductRiskShopQueueItem { + shopName: string; + matched: boolean; + shopId?: string; + platform?: string; + companyName?: string; + openStoreUrl?: string; + matchedUserId?: number; + matchStatus?: string; + matchMessage?: string; + queryAsins?: QueryAsinCountryAsins[]; + skipAsinsByCountry?: Record; + skip_asins_by_country?: Record; + skipAsinDetailsByCountry?: Record>; + skip_asin_details_by_country?: Record>; +} + +export interface ShopMatchResultRow { + asin?: string; + minimumPrice?: string; + minimum_price?: string; + status?: string; + done?: boolean; +} + +export interface ShopMatchSubmitShopPayload { + shopName?: string; + error?: string; + countries?: Record; + skipAsinsByCountry?: Record; + skip_asins_by_country?: Record; + skipAsinDetailsByCountry?: Record>; + skip_asin_details_by_country?: Record>; +} + +export interface ProductRiskMatchShopsVo { + items: ProductRiskShopQueueItem[]; +} + +export interface ProductRiskCountryPreferenceVo { + country_codes: string[]; +} + +export interface ProductRiskDashboardVo { + candidateCount: number; + processedTaskCount: number; + successTaskCount: number; + failedTaskCount: number; +} + +export interface ProductRiskHistoryItem { + resultId?: number; + taskId?: number; + shopName?: string; + shopId?: string; + platform?: string; + companyName?: string; + matched?: boolean; + matchStatus?: string; + matchMessage?: string; + taskStatus?: string; + success?: boolean; + error?: string; + outputFilename?: string; + downloadUrl?: string; + fileJobId?: number; + fileStatus?: string; + fileError?: string; + fileReady?: boolean; + scheduledAt?: string; + skipAsinsByCountry?: Record; + skip_asins_by_country?: Record; + skipAsinDetailsByCountry?: Record>; + skip_asin_details_by_country?: Record>; +} + +export interface ProductRiskHistoryVo { + items: ProductRiskHistoryItem[]; +} + +export interface ProductRiskTaskSummary { + id?: number; + taskNo?: string; + status?: string; + errorMessage?: string; + createdAt?: string; + updatedAt?: string; + finishedAt?: string; + scheduledAt?: string; + countryCodes?: string[]; + currentStageIndex?: number; + activeStageIndex?: number; + scheduleStages?: ShopMatchTaskStage[]; +} + +export interface ProductRiskTaskDetailVo { + task?: ProductRiskTaskSummary; + items?: ProductRiskHistoryItem[]; +} + +export interface ProductRiskTaskBatchVo { + items: ProductRiskTaskDetailVo[]; + missingTaskIds?: number[]; +} + +export interface ProductRiskPendingDeleteVo { + removed: boolean; +} + +export interface ProductRiskCreateTaskVo { + taskId: number; + items: ProductRiskHistoryItem[]; +} + +export type ShopMatchCandidateVo = ProductRiskCandidateVo; +export type ShopMatchCountryPreferenceVo = ProductRiskCountryPreferenceVo; +export type ShopMatchShopQueueItem = ProductRiskShopQueueItem; +export type ShopMatchDashboardVo = ProductRiskDashboardVo; +export type ShopMatchHistoryItem = ProductRiskHistoryItem; +export type ShopMatchHistoryVo = ProductRiskHistoryVo; +export type ShopMatchTaskDetailVo = ProductRiskTaskDetailVo; +export type ShopMatchTaskBatchVo = ProductRiskTaskBatchVo; + +export interface ShopMatchCreateTaskItem { + shopName: string; + matched: boolean; + shopId?: string; + platform?: string; + companyName?: string; + openStoreUrl?: string; + matchedUserId?: number; + matchStatus?: string; + matchMessage?: string; +} + +export interface ShopMatchCreateTaskResultItem { + resultId?: number; + taskId?: number; + shopName?: string; + shopId?: string; + platform?: string; + companyName?: string; + matched?: boolean; + matchStatus?: string; + matchMessage?: string; + taskStatus?: string; + success?: boolean; + error?: string; + outputFilename?: string; + downloadUrl?: string; + fileJobId?: number; + fileStatus?: string; + fileError?: string; + fileReady?: boolean; + scheduledAt?: string; +} + +export interface ShopMatchCreateTaskVo { + taskId: number; + items: ShopMatchCreateTaskResultItem[]; +} + +export interface ShopMatchTaskStage { + stageIndex?: number; + scheduledAt?: string; + status?: string; +} + +export interface ShopMatchSkipAsinPageVo { + page: number; + pageSize: number; + total: number; + totalPages: number; + queryAsins: QueryAsinCountryAsins[]; + skipAsinsByCountry: Record; + skipAsinDetailsByCountry: Record>; +} + +const taskProgressResponseCache = createTaskProgressRequestCache({ + ttlMs: () => getTaskProgressCacheTtlMs(), + maxEntries: 100, + maxInflight: 16, +}); + +function normalizeTaskIds(taskIds: number[]) { + return Array.from( + new Set( + (taskIds || []) + .map((taskId) => Number(taskId)) + .filter((taskId) => Number.isFinite(taskId) && taskId > 0), + ), + ).sort((left, right) => left - right); +} + +function buildTaskProgressRequestKey(path: string, taskIds: number[]) { + return `${path}::${taskIds.join(",")}`; +} + +async function postTaskProgressBatch( + path: string, + taskIds: number[], +) { + const normalizedTaskIds = normalizeTaskIds(taskIds); + if (!normalizedTaskIds.length) { + return { items: [], missingTaskIds: [] } as T; + } + + const cacheKey = buildTaskProgressRequestKey(path, normalizedTaskIds); + const cached = taskProgressResponseCache.get(cacheKey); + if (cached !== undefined) { + return cached as T; + } + + const inflight = taskProgressResponseCache.getInflight(cacheKey); + if (inflight) { + return (await inflight) as T; + } + + const requestPromise = unwrapJavaResponse( + post, { taskIds: number[] }>(path, { taskIds: normalizedTaskIds }, { + timeout: getTaskProgressTimeoutMs(), + }), + ) + .then((data) => { + taskProgressResponseCache.set(cacheKey, data); + return data; + }) + .finally(() => { + taskProgressResponseCache.endInflight(cacheKey); + }); + + taskProgressResponseCache.startInflight(cacheKey, requestPromise); + return requestPromise; +} + +export function listProductRiskCandidates() { + return unwrapJavaResponse( + get>( + `${JAVA_API_PREFIX}/product-risk-resolve/candidates`, + { + params: { user_id: getCurrentUserId() }, + }, + ), + ); +} + +export function addProductRiskCandidate(shopName: string) { + return unwrapJavaResponse( + post< + JavaApiResponse, + { user_id: number; shop_name: string } + >(`${JAVA_API_PREFIX}/product-risk-resolve/candidates`, { + user_id: getCurrentUserId(), + shop_name: shopName, + }), + ); +} + +export function deleteProductRiskCandidate(id: number) { + return unwrapJavaResponse( + del>( + `${JAVA_API_PREFIX}/product-risk-resolve/candidates/${id}`, + { + params: { user_id: getCurrentUserId() }, + }, + ), + ); +} + +export function getProductRiskCountryPreference() { + return unwrapJavaResponse( + get>( + `${JAVA_API_PREFIX}/product-risk-resolve/country-preference`, + { + params: { user_id: getCurrentUserId() }, + }, + ), + ); +} + +export function putProductRiskCountryPreference(countryCodes: string[]) { + return unwrapJavaResponse( + put< + JavaApiResponse, + { user_id: number; country_codes: string[] } + >(`${JAVA_API_PREFIX}/product-risk-resolve/country-preference`, { + user_id: getCurrentUserId(), + country_codes: countryCodes, + }), + ); +} + +export function matchProductRiskShops(shopNames: string[]) { + return unwrapJavaResponse( + post< + JavaApiResponse, + { user_id: number; shop_names: string[] } + >(`${JAVA_API_PREFIX}/product-risk-resolve/match-shops`, { + user_id: getCurrentUserId(), + shop_names: shopNames, + }), + ); +} + +export function getProductRiskDashboard() { + return unwrapJavaResponse( + get>( + `${JAVA_API_PREFIX}/product-risk-resolve/dashboard`, + { + params: { user_id: getCurrentUserId() }, + }, + ), + ); +} + +export function getProductRiskHistory() { + return unwrapJavaResponse( + get>( + `${JAVA_API_PREFIX}/product-risk-resolve/history`, + { + params: { user_id: getCurrentUserId() }, + }, + ), + ); +} + +export function deleteProductRiskHistory(resultId: number) { + return unwrapJavaResponse( + del>( + `${JAVA_API_PREFIX}/product-risk-resolve/history/${resultId}`, + { + params: { user_id: getCurrentUserId() }, + }, + ), + ); +} + +export function createProductRiskTask(items: ProductRiskShopQueueItem[]) { + return unwrapJavaResponse( + post< + JavaApiResponse, + { user_id: number; items: ProductRiskShopQueueItem[] } + >(`${JAVA_API_PREFIX}/product-risk-resolve/tasks`, { + user_id: getCurrentUserId(), + items, + }), + ); +} + +export function deleteProductRiskTask(taskId: number) { + return unwrapJavaResponse( + del>( + `${JAVA_API_PREFIX}/product-risk-resolve/tasks/${taskId}`, + { + params: { user_id: getCurrentUserId() }, + }, + ), + ); +} + +export function deletePendingProductRiskShopResult(shopName: string) { + return unwrapJavaResponse( + del>( + `${JAVA_API_PREFIX}/product-risk-resolve/pending-shop-result`, + { + params: { user_id: getCurrentUserId(), shop_name: shopName }, + }, + ), + ); +} + +export function getProductRiskTasksBatch(taskIds: number[]) { + return unwrapJavaResponse( + post, { taskIds: number[] }>( + `${JAVA_API_PREFIX}/product-risk-resolve/tasks/batch`, + { taskIds }, + ), + ); +} + +export function getProductRiskResultDownloadUrl(resultId: number) { + return getJavaDownloadUrl( + `/product-risk-resolve/results/${resultId}/download`, + ); +} + +export function getProductRiskTaskProgressBatch(taskIds: number[]) { + return postTaskProgressBatch( + `${JAVA_API_PREFIX}/product-risk-resolve/tasks/progress/batch`, + taskIds, + ); +} + +export function listShopMatchCandidates() { + return unwrapJavaResponse( + get>( + `${JAVA_API_PREFIX}/shop-match/candidates`, + { + params: { user_id: getCurrentUserId() }, + }, + ), + ); +} + +export function addShopMatchCandidate(shopName: string) { + return unwrapJavaResponse( + post< + JavaApiResponse, + { user_id: number; shop_name: string } + >(`${JAVA_API_PREFIX}/shop-match/candidates`, { + user_id: getCurrentUserId(), + shop_name: shopName, + }), + ); +} + +export function deleteShopMatchCandidate(id: number) { + return unwrapJavaResponse( + del>(`${JAVA_API_PREFIX}/shop-match/candidates/${id}`, { + params: { user_id: getCurrentUserId() }, + }), + ); +} + +export function getShopMatchCountryPreference() { + return unwrapJavaResponse( + get>( + `${JAVA_API_PREFIX}/shop-match/country-preference`, + { + params: { user_id: getCurrentUserId() }, + }, + ), + ); +} + +export function putShopMatchCountryPreference(countryCodes: string[]) { + return unwrapJavaResponse( + put< + JavaApiResponse, + { user_id: number; country_codes: string[] } + >(`${JAVA_API_PREFIX}/shop-match/country-preference`, { + user_id: getCurrentUserId(), + country_codes: countryCodes, + }), + ); +} + +export function matchShopMatchShops(shopNames: string[]) { + return unwrapJavaResponse( + post< + JavaApiResponse, + { user_id: number; shop_names: string[] } + >(`${JAVA_API_PREFIX}/shop-match/match-shops`, { + user_id: getCurrentUserId(), + shop_names: shopNames, + }), + ); +} + +export function getShopMatchDashboard() { + return unwrapJavaResponse( + get>( + `${JAVA_API_PREFIX}/shop-match/dashboard`, + { + params: { user_id: getCurrentUserId() }, + }, + ), + ); +} + +export function getShopMatchHistory() { + return unwrapJavaResponse( + get>( + `${JAVA_API_PREFIX}/shop-match/history`, + { + params: { user_id: getCurrentUserId() }, + }, + ), + ); +} + +export function deleteShopMatchHistory(resultId: number) { + return unwrapJavaResponse( + del>(`${JAVA_API_PREFIX}/shop-match/history/${resultId}`, { + params: { user_id: getCurrentUserId() }, + }), + ); +} + +export function createShopMatchTask( + items: ShopMatchCreateTaskItem[], + countryCodes: string[], + scheduleTimes?: string[], +) { + return unwrapJavaResponse( + post< + JavaApiResponse, + { user_id: number; items: ShopMatchCreateTaskItem[]; country_codes: string[]; schedule_times?: string[] } + >(`${JAVA_API_PREFIX}/shop-match/tasks`, { + user_id: getCurrentUserId(), + items, + country_codes: countryCodes, + schedule_times: scheduleTimes, + }), + ); +} + +export function deleteShopMatchTask(taskId: number) { + return unwrapJavaResponse( + del>(`${JAVA_API_PREFIX}/shop-match/tasks/${taskId}`, { + params: { user_id: getCurrentUserId() }, + }), + ); +} + +export function activateShopMatchTask(taskId: number, stageIndex: number) { + return unwrapJavaResponse( + post, undefined>( + `${JAVA_API_PREFIX}/shop-match/tasks/${taskId}/activate?user_id=${encodeURIComponent(String(getCurrentUserId()))}&stage_index=${encodeURIComponent(String(stageIndex))}`, + undefined, + ), + ); +} + +export function completeShopMatchTaskStage(taskId: number, stageIndex: number) { + return unwrapJavaResponse( + post, { stage_index: number }>( + `${JAVA_API_PREFIX}/shop-match/tasks/${taskId}/stage-finished?user_id=${encodeURIComponent(String(getCurrentUserId()))}`, + { stage_index: stageIndex }, + ), + ); +} + +export function getShopMatchTasksBatch(taskIds: number[]) { + return unwrapJavaResponse( + post, { taskIds: number[] }>( + `${JAVA_API_PREFIX}/shop-match/tasks/batch`, + { taskIds }, + ), + ); +} + +export function getShopMatchResultDownloadUrl(resultId: number) { + return getJavaDownloadUrl(`/shop-match/results/${resultId}/download`); +} + +export function getShopMatchTaskSkipAsinsPaginated( + taskId: number, + page: number = 1, + pageSize: number = 1000, + options: { + shopName?: string | string[]; + countryCode?: string | string[]; + } = {}, +) { + return unwrapJavaResponse( + get>( + `${JAVA_API_PREFIX}/shop-match/tasks/${taskId}/skip-asins/paginated`, + { + params: { + page, + page_size: pageSize, + ...(options.shopName ? { shop_name: options.shopName } : {}), + ...(options.countryCode ? { country_code: options.countryCode } : {}), + }, + }, + ), + ); +} + +export function getShopMatchTaskProgressBatch(taskIds: number[]) { + return postTaskProgressBatch( + `${JAVA_API_PREFIX}/shop-match/tasks/progress/batch`, + taskIds, + ); +} diff --git a/frontend-vue/tests/modules-shop-match.test.ts b/frontend-vue/tests/modules-shop-match.test.ts new file mode 100644 index 00000000..58fb7d24 --- /dev/null +++ b/frontend-vue/tests/modules-shop-match.test.ts @@ -0,0 +1,272 @@ +import { test } from 'node:test' +import assert from 'node:assert/strict' +import { http } from '../src/shared/api/http.ts' +import { + listProductRiskCandidates, + addProductRiskCandidate, + deleteProductRiskCandidate, + getProductRiskCountryPreference, + putProductRiskCountryPreference, + matchProductRiskShops, + getProductRiskDashboard, + getProductRiskHistory, + deleteProductRiskHistory, + createProductRiskTask, + deleteProductRiskTask, + deletePendingProductRiskShopResult, + getProductRiskTasksBatch, + getProductRiskResultDownloadUrl, + getProductRiskTaskProgressBatch, + listShopMatchCandidates, + addShopMatchCandidate, + deleteShopMatchCandidate, + getShopMatchCountryPreference, + putShopMatchCountryPreference, + matchShopMatchShops, + getShopMatchDashboard, + getShopMatchHistory, + deleteShopMatchHistory, + createShopMatchTask, + deleteShopMatchTask, + activateShopMatchTask, + completeShopMatchTaskStage, + getShopMatchTasksBatch, + getShopMatchResultDownloadUrl, + getShopMatchTaskSkipAsinsPaginated, + getShopMatchTaskProgressBatch, +} from '../src/shared/api/types/modules/shop-match.ts' +import { + type ShopMatchCreateTaskItem, +} from '../src/shared/api/types/modules/shop-match.ts' + +function setupWindow() { + ;(globalThis as Record).window = { + localStorage: { getItem: () => '42' }, + location: { origin: 'http://localhost' }, + } +} + +function mockRequest( + t: Parameters[1] extends (t: infer T) => unknown ? T : never, + impl: (config: { url?: string; method?: string; params?: Record; data?: unknown; timeout?: number }) => Promise, +) { + t.mock.method(http, 'request', impl as never) +} + +const okResponse = (data: unknown) => Promise.resolve({ data: { success: true, message: 'ok', data } }) + +test('test_product_risk_candidates_crud', async (t) => { + setupWindow() + const calls: { url?: string; method?: string; params?: Record; data?: unknown }[] = [] + mockRequest(t, (config) => { + calls.push(config) + return okResponse({ id: 1, shop_name: 's1' }) + }) + await listProductRiskCandidates() + await addProductRiskCandidate('店铺A') + await deleteProductRiskCandidate(3) + assert.equal(calls[0].url, '/newApi/api/product-risk-resolve/candidates') + assert.deepEqual(calls[0].params, { user_id: 42 }) + assert.equal(calls[1].url, '/newApi/api/product-risk-resolve/candidates') + assert.equal(calls[1].method, 'POST') + assert.deepEqual(calls[1].data, { user_id: 42, shop_name: '店铺A' }) + assert.equal(calls[2].url, '/newApi/api/product-risk-resolve/candidates/3') + assert.deepEqual(calls[2].params, { user_id: 42 }) + assert.equal(calls[2].method, 'DELETE') +}) + +test('test_product_risk_preference_match', async (t) => { + setupWindow() + const calls: { url?: string; method?: string; params?: Record; data?: unknown }[] = [] + mockRequest(t, (config) => { + calls.push(config) + return okResponse({ country_codes: ['US'] }) + }) + await getProductRiskCountryPreference() + await putProductRiskCountryPreference(['US']) + await matchProductRiskShops(['店铺A']) + assert.equal(calls[0].url, '/newApi/api/product-risk-resolve/country-preference') + assert.deepEqual(calls[0].params, { user_id: 42 }) + assert.equal(calls[1].url, '/newApi/api/product-risk-resolve/country-preference') + assert.equal(calls[1].method, 'PUT') + assert.deepEqual(calls[1].data, { user_id: 42, country_codes: ['US'] }) + assert.equal(calls[2].url, '/newApi/api/product-risk-resolve/match-shops') + assert.deepEqual(calls[2].data, { user_id: 42, shop_names: ['店铺A'] }) +}) + +test('test_product_risk_task_apis', async (t) => { + setupWindow() + const calls: { url?: string; method?: string; params?: Record; data?: unknown }[] = [] + mockRequest(t, (config) => { + calls.push(config) + return okResponse({ taskId: 1, items: [] }) + }) + await createProductRiskTask([{ shopName: '店铺A', matched: true }]) + await deleteProductRiskTask(7) + await deletePendingProductRiskShopResult('店铺A') + await getProductRiskTasksBatch([5, 3]) + assert.equal(calls[0].url, '/newApi/api/product-risk-resolve/tasks') + assert.deepEqual(calls[0].data, { user_id: 42, items: [{ shopName: '店铺A', matched: true }] }) + assert.equal(calls[1].url, '/newApi/api/product-risk-resolve/tasks/7') + assert.equal(calls[1].method, 'DELETE') + assert.deepEqual(calls[1].params, { user_id: 42 }) + assert.equal(calls[2].url, '/newApi/api/product-risk-resolve/pending-shop-result') + assert.deepEqual(calls[2].params, { user_id: 42, shop_name: '店铺A' }) + assert.equal(calls[3].url, '/newApi/api/product-risk-resolve/tasks/batch') + assert.deepEqual(calls[3].data, { taskIds: [5, 3] }) +}) + +test('test_product_risk_dashboard_history_download', async (t) => { + setupWindow() + const calls: { url?: string; method?: string; params?: Record }[] = [] + mockRequest(t, (config) => { + calls.push(config) + return okResponse({ candidateCount: 0, processedTaskCount: 0, successTaskCount: 1, failedTaskCount: 0 }) + }) + await getProductRiskDashboard() + await getProductRiskHistory() + await deleteProductRiskHistory(9) + assert.equal(calls[0].url, '/newApi/api/product-risk-resolve/dashboard') + assert.deepEqual(calls[0].params, { user_id: 42 }) + assert.equal(calls[1].url, '/newApi/api/product-risk-resolve/history') + assert.deepEqual(calls[1].params, { user_id: 42 }) + assert.equal(calls[2].url, '/newApi/api/product-risk-resolve/history/9') + assert.equal(calls[2].method, 'DELETE') + assert.equal( + getProductRiskResultDownloadUrl(7), + 'http://localhost/newApi/api/product-risk-resolve/results/7/download?user_id=42', + ) +}) + +test('test_shop_match_candidates_crud', async (t) => { + setupWindow() + const calls: { url?: string; method?: string; params?: Record; data?: unknown }[] = [] + mockRequest(t, (config) => { + calls.push(config) + return okResponse({ id: 1, shop_name: 's1' }) + }) + await listShopMatchCandidates() + await addShopMatchCandidate('店铺A') + await deleteShopMatchCandidate(3) + assert.equal(calls[0].url, '/newApi/api/shop-match/candidates') + assert.deepEqual(calls[0].params, { user_id: 42 }) + assert.equal(calls[1].url, '/newApi/api/shop-match/candidates') + assert.equal(calls[1].method, 'POST') + assert.deepEqual(calls[1].data, { user_id: 42, shop_name: '店铺A' }) + assert.equal(calls[2].url, '/newApi/api/shop-match/candidates/3') + assert.equal(calls[2].method, 'DELETE') +}) + +test('test_shop_match_preference_match', async (t) => { + setupWindow() + const calls: { url?: string; method?: string; params?: Record; data?: unknown }[] = [] + mockRequest(t, (config) => { + calls.push(config) + return okResponse({ country_codes: ['US'] }) + }) + await getShopMatchCountryPreference() + await putShopMatchCountryPreference(['US']) + await matchShopMatchShops(['店铺A']) + assert.equal(calls[0].url, '/newApi/api/shop-match/country-preference') + assert.deepEqual(calls[0].params, { user_id: 42 }) + assert.equal(calls[1].url, '/newApi/api/shop-match/country-preference') + assert.equal(calls[1].method, 'PUT') + assert.deepEqual(calls[1].data, { user_id: 42, country_codes: ['US'] }) + assert.equal(calls[2].url, '/newApi/api/shop-match/match-shops') + assert.deepEqual(calls[2].data, { user_id: 42, shop_names: ['店铺A'] }) +}) + +test('test_shop_match_task_apis', async (t) => { + setupWindow() + const calls: { url?: string; method?: string; params?: Record; data?: unknown }[] = [] + mockRequest(t, (config) => { + calls.push(config) + return okResponse({ taskId: 1, items: [] }) + }) + const item: ShopMatchCreateTaskItem = { shopName: '店铺A', matched: true } + await createShopMatchTask([item], ['US'], ['2026-09-01 10:00']) + await deleteShopMatchTask(7) + await activateShopMatchTask(7, 1) + await completeShopMatchTaskStage(7, 1) + await getShopMatchTasksBatch([5, 3]) + assert.equal(calls[0].url, '/newApi/api/shop-match/tasks') + assert.equal(calls[0].method, 'POST') + assert.deepEqual(calls[0].data, { user_id: 42, items: [item], country_codes: ['US'], schedule_times: ['2026-09-01 10:00'] }) + assert.equal(calls[1].url, '/newApi/api/shop-match/tasks/7') + assert.equal(calls[1].method, 'DELETE') + assert.equal(calls[2].url, '/newApi/api/shop-match/tasks/7/activate?user_id=42&stage_index=1') + assert.equal(calls[2].method, 'POST') + assert.equal(calls[3].url, '/newApi/api/shop-match/tasks/7/stage-finished?user_id=42') + assert.deepEqual(calls[3].data, { stage_index: 1 }) + assert.equal(calls[4].url, '/newApi/api/shop-match/tasks/batch') + assert.deepEqual(calls[4].data, { taskIds: [5, 3] }) +}) + +test('test_shop_match_dashboard_history_skip_asins', async (t) => { + setupWindow() + const calls: { url?: string; method?: string; params?: Record }[] = [] + mockRequest(t, (config) => { + calls.push(config) + return okResponse({ candidateCount: 0, processedTaskCount: 0, successTaskCount: 1, failedTaskCount: 0 }) + }) + await getShopMatchDashboard() + await getShopMatchHistory() + await deleteShopMatchHistory(9) + await getShopMatchTaskSkipAsinsPaginated(7, 1, 1000, { shopName: '店铺A' }) + assert.equal(calls[0].url, '/newApi/api/shop-match/dashboard') + assert.deepEqual(calls[0].params, { user_id: 42 }) + assert.equal(calls[1].url, '/newApi/api/shop-match/history') + assert.equal(calls[2].url, '/newApi/api/shop-match/history/9') + assert.equal(calls[2].method, 'DELETE') + assert.equal(calls[3].url, '/newApi/api/shop-match/tasks/7/skip-asins/paginated') + assert.deepEqual(calls[3].params, { page: 1, page_size: 1000, shop_name: '店铺A' }) + assert.equal( + getShopMatchResultDownloadUrl(7), + 'http://localhost/newApi/api/shop-match/results/7/download?user_id=42', + ) +}) + +test('test_shop_match_product_risk_progress_batch', async (t) => { + setupWindow() + const calls: { url?: string; method?: string; data?: unknown; timeout?: number }[] = [] + mockRequest(t, (config) => { + calls.push(config) + return okResponse({ items: [], missingTaskIds: [] }) + }) + const shopMatch = await getShopMatchTaskProgressBatch([5, 3]) + const productRisk = await getProductRiskTaskProgressBatch([5, 3]) + assert.equal(calls[0].url, '/newApi/api/shop-match/tasks/progress/batch') + assert.deepEqual(calls[0].data, { taskIds: [3, 5] }) + assert.equal(calls[0].timeout, 10000) + assert.equal(calls[1].url, '/newApi/api/product-risk-resolve/tasks/progress/batch') + assert.deepEqual(calls[1].data, { taskIds: [3, 5] }) + assert.equal(calls[1].timeout, 10000) + assert.deepEqual(shopMatch, { items: [], missingTaskIds: [] }) + assert.deepEqual(productRisk, { items: [], missingTaskIds: [] }) +}) + +test('test_shop_match_product_risk_export_compat', async (t) => { + setupWindow() + const fromJavaModules = await import('../src/shared/api/java-modules.ts') + const fromShopMatch = await import('../src/shared/api/types/modules/shop-match.ts') + const productRiskNames = [ + 'listProductRiskCandidates', 'addProductRiskCandidate', 'deleteProductRiskCandidate', + 'getProductRiskCountryPreference', 'putProductRiskCountryPreference', 'matchProductRiskShops', + 'getProductRiskDashboard', 'getProductRiskHistory', 'deleteProductRiskHistory', + 'createProductRiskTask', 'deleteProductRiskTask', 'deletePendingProductRiskShopResult', + 'getProductRiskTasksBatch', 'getProductRiskResultDownloadUrl', 'getProductRiskTaskProgressBatch', + ] + const shopMatchNames = [ + 'listShopMatchCandidates', 'addShopMatchCandidate', 'deleteShopMatchCandidate', + 'getShopMatchCountryPreference', 'putShopMatchCountryPreference', 'matchShopMatchShops', + 'getShopMatchDashboard', 'getShopMatchHistory', 'deleteShopMatchHistory', + 'createShopMatchTask', 'deleteShopMatchTask', 'activateShopMatchTask', + 'completeShopMatchTaskStage', 'getShopMatchTasksBatch', 'getShopMatchResultDownloadUrl', + 'getShopMatchTaskSkipAsinsPaginated', 'getShopMatchTaskProgressBatch', + ] + for (const name of [...productRiskNames, ...shopMatchNames]) { + assert.equal(fromJavaModules[name], fromShopMatch[name], `${name} 应为同一引用`) + } + const candidate = { id: 1, shop_name: 's1' } + assert.equal(candidate.shop_name, 's1') +})