// backend/api/routes.ts import express4 from "npm:express"; import { getStoredRelayAuth, isUsableAuth } from "../src/relay/relay-runtime.js"; import relayCredentialRouter from "./relay/credential-routes.js"; // backend/modules/fmode-amazon-sp-api/src/routes.ts import { Router } from "express"; // backend/modules/fmode-amazon-sp-api/src/marketplace-helper.ts var ENDPOINTS = { NA: "https://sellingpartnerapi-na.amazon.com", EU: "https://sellingpartnerapi-eu.amazon.com", FE: "https://sellingpartnerapi-fe.amazon.com" }; var SANDBOX_ENDPOINTS = { NA: "https://sandbox.sellingpartnerapi-na.amazon.com", EU: "https://sandbox.sellingpartnerapi-eu.amazon.com", FE: "https://sandbox.sellingpartnerapi-fe.amazon.com" }; var MARKETPLACE_REGION_MAP = { // 北美 "A2EUQ1WTGCTBG2": "NA", // 加拿大 "ATVPDKIKX0DER": "NA", // 美国 "A1AM78C64UM0Y8": "NA", // 墨西哥 "A2Q3Y263D00KWC": "NA", // 巴西 // 欧洲 "A1RKKUPIHCS9HS": "EU", // 西班牙 "A1F83G8C2ARO7P": "EU", // 英国 "A13V1IB3VIYZZH": "EU", // 法国 "A1805IZSGTT6HS": "EU", // 荷兰 "A1PA6795UKMFR9": "EU", // 德国 "APJ6JRA9NG5V4": "EU", // 意大利 "A2NODRKZP88ZB9": "EU", // 瑞典 "AE08WJ6EMKS27": "EU", // 埃及 "A1C3SOZRARQ6R3": "EU", // 波兰 "AMEN7PMS3EDWL": "EU", // 比利时 "A28R8C7NBKEWEA": "EU", // 爱尔兰 "A33AVAJ2PDY3EV": "EU", // 土耳其 "A17E79C6D8DWNP": "EU", // 沙特阿拉伯 "A2VIGQ35RCS4UG": "EU", // 阿联酋 "ARBP9OOSHTCHU": "EU", // 埃及 (重复?) "A21TJRUUN4KGV": "EU", // 印度 // 远东 "A1VC38T7YXB528": "FE", // 日本 "A39IBJ37TRP1C6": "FE", // 澳大利亚 "A19VAU5U5O7RUS": "FE" // 新加坡 }; var getEndpointByMarketplaceId = (marketplaceId, isSandbox = false) => { const region = MARKETPLACE_REGION_MAP[marketplaceId] || "NA"; const endpoints = isSandbox ? SANDBOX_ENDPOINTS : ENDPOINTS; return endpoints[region]; }; var getEndpointByRegion = (regionCode, isSandbox = false) => { const endpoints = isSandbox ? SANDBOX_ENDPOINTS : ENDPOINTS; const awsRegionMap = { "us-east-1": "NA", "eu-west-1": "EU", "us-west-2": "FE" }; const regionKey = awsRegionMap[regionCode] || regionCode; if (endpoints[regionKey]) { return endpoints[regionKey]; } return endpoints.NA; }; // backend/modules/fmode-amazon-sp-api/src/client.ts var SpApiClient = class { // 配置在构造函数中现在是可选的,但可以传递用于遗留/静态用法 // 然而,对于基于 Parse 的多租户用法,我们依赖于方法参数。 // 为了保持向后兼容性并支持新的动态模式,我们可以使用混合方法。 // 对于动态用法,我们将主要不使用 this.config。 staticConfig; // RDT Token 缓存 (内存缓存,按 shopId 和 resourceKey 存储) rdtCache = /* @__PURE__ */ new Map(); constructor(config) { this.staticConfig = config; } /** * 获取基础 URL */ getBaseUrl(marketplaceId, isSandbox, region) { if (marketplaceId) { return getEndpointByMarketplaceId(marketplaceId, isSandbox); } if (region) { return getEndpointByRegion(region, isSandbox); } if (this.staticConfig) { const REGION_ENDPOINTS = { "us-east-1": "https://sellingpartnerapi-na.amazon.com", "eu-west-1": "https://sellingpartnerapi-eu.amazon.com", "us-west-2": "https://sellingpartnerapi-fe.amazon.com" }; const SANDBOX_ENDPOINTS2 = { "us-east-1": "https://sandbox.sellingpartnerapi-na.amazon.com", "eu-west-1": "https://sandbox.sellingpartnerapi-eu.amazon.com", "us-west-2": "https://sandbox.sellingpartnerapi-fe.amazon.com" }; const endpoints = this.staticConfig.sandbox ? SANDBOX_ENDPOINTS2 : REGION_ENDPOINTS; return endpoints[this.staticConfig.region] || endpoints["us-east-1"]; } throw new Error("\u65E0\u6CD5\u786E\u5B9A SP-API \u7AEF\u70B9\uFF0CMarketplaceId \u4E3A\u5FC5\u586B\u9879\u3002"); } /** * 获取 LWA Access Token * 先从 Shop 表获取缓存的 token,如果不存在或过期则刷新 */ async getAccessToken(context) { if (context) { const { shopId, config } = context; return this.refreshAccessToken( shopId, config.clientId, config.clientSecret, config.refreshToken ); } throw new Error("\u7F3A\u5C11 SpApiClient \u914D\u7F6E\u3002"); } /** * 刷新 Token 逻辑 * 1. 先从 Shop 表的 config.SpApiConfig.accessToken 获取 * 2. 检查是否过期(通过 config.SpApiConfig.accessTokenExpiresAt) * 3. 如果不存在或过期,则请求新的 token * 4. 将新 token 和过期时间(50分钟后)保存回 Shop 表 */ async refreshAccessToken(shopId, clientId, clientSecret, refreshToken) { try { const Parse = globalThis.Parse; if (!Parse) { console.warn("[SP-API] Parse SDK not available, will fetch new token"); } if (Parse) { try { const shopQuery = new Parse.Query("Shop"); const shop = await shopQuery.get(shopId, { useMasterKey: true }); if (shop) { const config = shop.get("config") || {}; const spApiConfig = config.SpApiConfig || {}; const cachedToken = spApiConfig.accessToken; const expiresAt = spApiConfig.accessTokenExpiresAt; if (cachedToken && expiresAt) { const now = /* @__PURE__ */ new Date(); const expiryDate = new Date(expiresAt); if (expiryDate > now) { const remainingMinutes = Math.floor( (expiryDate.getTime() - now.getTime()) / 1e3 / 60 ); console.log(`[SP-API] \u4F7F\u7528\u7F13\u5B58\u7684 Access Token (\u5269\u4F59 ${remainingMinutes} \u5206\u949F)`); return cachedToken; } else { console.log("[SP-API] \u7F13\u5B58\u7684 Access Token \u5DF2\u8FC7\u671F\uFF0C\u6B63\u5728\u5237\u65B0..."); } } else { console.log("[SP-API] \u672A\u627E\u5230\u7F13\u5B58\u7684 Access Token\uFF0C\u6B63\u5728\u83B7\u53D6\u65B0\u7684..."); } } } catch (error) { console.warn("[SP-API] \u4ECE Shop \u8868\u83B7\u53D6 token \u5931\u8D25:", error.message); } } console.log("[SP-API] \u6B63\u5728\u5237\u65B0 Access Token..."); const params = new URLSearchParams({ grant_type: "refresh_token", refresh_token: refreshToken, client_id: clientId, client_secret: clientSecret // scope: 'sellingpartnerapi::migration​' }); const response = await fetch("https://api.amazon.com/auth/o2/token", { method: "POST", headers: { "Content-Type": "application/x-www-form-urlencoded" }, body: params }); if (!response.ok) { const errorText = await response.text(); throw new Error( `Token refresh failed: ${response.status} ${response.statusText} - ${errorText}` ); } const data = await response.json(); console.log("[SP-API] \u5237\u65B0 Access Token \u6210\u529F\u3002"); if (Parse) { try { const shopQuery = new Parse.Query("Shop"); const shop = await shopQuery.get(shopId, { useMasterKey: true }); if (shop) { const config = shop.get("config") || {}; const spApiConfig = config.SpApiConfig || {}; const expiresAt = /* @__PURE__ */ new Date(); expiresAt.setMinutes(expiresAt.getMinutes() + 50); spApiConfig.accessToken = data.access_token; spApiConfig.accessTokenExpiresAt = expiresAt.toISOString(); config.SpApiConfig = spApiConfig; shop.set("config", config); await shop.save(null, { useMasterKey: true }); console.log( `[SP-API] Access Token \u5DF2\u4FDD\u5B58\u5230 Shop \u8868 (\u8FC7\u671F\u65F6\u95F4: ${expiresAt.toISOString()})` ); } } catch (error) { console.error("[SP-API] \u4FDD\u5B58 token \u5230 Shop \u8868\u5931\u8D25:", error.message); } } return data.access_token; } catch (error) { console.error("[SP-API] \u5237\u65B0 token \u5931\u8D25:", error); throw error; } } /** * 获取 RDT Token (带缓存) * @param shopId 店铺ID * @param accessToken LWA Access Token * @param method HTTP 方法 * @param path API 路径 * @param dataElements 可选的数据元素列表 * @param marketplaceId 市场ID * @param isSandbox 是否沙箱环境 * @param region AWS 区域 */ async getRestrictedDataToken(accessToken, baseUrl, method, path, dataElements) { console.log(`[SP-API] \u6B63\u5728\u83B7\u53D6 RDT Token for ${method} ${path}...`); const rdtEndpoint = `${baseUrl}/tokens/2021-03-01/restrictedDataToken`; const cleanPath = path.startsWith("/") ? path : `/${path}`; const requestBody = { restrictedResources: [ { method, path: cleanPath } ] }; console.log(`[SP-API] RDT \u8BF7\u6C42\u8BE6\u60C5:`, { endpoint: rdtEndpoint, requestBody: JSON.stringify(requestBody, null, 2), accessToken: accessToken.substring(0, 20) + "..." }); try { const response = await fetch(rdtEndpoint, { method: "POST", headers: { "x-amz-access-token": accessToken, "Content-Type": "application/json" }, body: JSON.stringify(requestBody) }); const responseText = await response.text(); console.log(`[SP-API] RDT \u54CD\u5E94\u72B6\u6001: ${response.status}`, responseText); if (!response.ok) { let errorData; try { errorData = JSON.parse(responseText); } catch (e) { errorData = responseText; } if (response.status === 403) { throw new Error( `RDT \u6743\u9650\u4E0D\u8DB3: \u5E94\u7528\u53EF\u80FD\u6CA1\u6709\u8BBF\u95EE PII \u6570\u636E\u7684\u6743\u9650\u3002\u9519\u8BEF\u8BE6\u60C5: ${JSON.stringify(errorData)}` ); } else if (response.status === 400) { throw new Error(`RDT \u8BF7\u6C42\u53C2\u6570\u9519\u8BEF: ${JSON.stringify(errorData)}`); } else if (response.status === 500) { throw new Error( `Amazon \u5185\u90E8\u670D\u52A1\u5668\u9519\u8BEF: ${JSON.stringify(errorData)}\u3002\u5EFA\u8BAE\u7A0D\u540E\u91CD\u8BD5\u6216\u68C0\u67E5 API \u8DEF\u5F84\u662F\u5426\u6B63\u786E\u3002` ); } throw new Error( `RDT request failed: ${response.status} ${response.statusText} - ${JSON.stringify(errorData)}` ); } const data = JSON.parse(responseText); console.log(`[SP-API] RDT Token \u83B7\u53D6\u6210\u529F (\u6709\u6548\u671F: ${data.expiresIn} \u79D2)`); return data.restrictedDataToken; } catch (error) { console.error("[SP-API] \u83B7\u53D6 RDT Token \u5931\u8D25:", error.message); throw error; } } /** * 发送 SP-API 请求 */ async request(options) { const accessToken = await this.getAccessToken(options.context); let marketplaceId = options.context?.marketplaceId; if (!marketplaceId && options.query) { const key = Object.keys(options.query).find( (k) => k.toLowerCase() === "marketplaceids" || k.toLowerCase() === "marketplaceid" ); if (key) { const val = options.query[key]; marketplaceId = Array.isArray(val) ? val[0] : val?.split(",")[0]; } } const isSandbox = options.context?.config.sandbox || this.staticConfig?.sandbox; const region = options.context?.config.region || this.staticConfig?.region; const baseUrl = this.getBaseUrl(marketplaceId, isSandbox, region); let urlString = baseUrl.replace(/\/$/, "") + (options.path.startsWith("/") ? options.path : "/" + options.path); const queryParts = []; if (options.query) { Object.entries(options.query).forEach(([key, value]) => { if (value !== void 0 && value !== null) { if ((key === "nextToken" || key === "pageToken") && typeof value === "string") { try { let valToUse = value; if (value.includes("%")) { try { valToUse = decodeURIComponent(value); } catch (e) { } } queryParts.push(`${key}=${encodeURIComponent(valToUse)}`); } catch (e) { queryParts.push(`${key}=${encodeURIComponent(value)}`); } } else { const encodedVal = encodeURIComponent(String(value)).replace(/%2C/g, ","); queryParts.push(`${key}=${encodedVal}`); } } }); } if (queryParts.length > 0) { urlString += (urlString.includes("?") ? "&" : "?") + queryParts.join("&"); } let rdtToken; if (options.requiresRdt && options.context) { console.log(`[SP-API] \u68C0\u6D4B\u5230\u9700\u8981 RDT Token \u7684 API: ${options.path}`); try { rdtToken = await this.getRestrictedDataToken( accessToken, baseUrl, options.method, options.path, options.rdtDataElements ); console.log("[SP-API] RDT Token \u83B7\u53D6\u6210\u529F"); } catch (error) { console.error("[SP-API] \u83B7\u53D6 RDT Token \u5931\u8D25\uFF0C\u5C06\u4F7F\u7528\u666E\u901A Access Token:", error.message); } } const headers = { "x-amz-access-token": rdtToken || accessToken, // 优先使用 RDT Token "Content-Type": "application/json", "User-Agent": "fmode-amazon-sp-api/1.0", ...options.headers || {} }; if (rdtToken) { console.log("[SP-API] \u4F7F\u7528 RDT Token \u8BBF\u95EE\u53D7\u9650\u6570\u636E"); } let retries = 0; const maxRetries = 3; while (retries < maxRetries) { try { console.log(`[SP-API] \u8BF7\u6C42: ${options.method} ${urlString}`, { query: options.query, shopId: options.context?.shopId }); const fetchOptions = { method: options.method, headers }; if (options.body && options.method !== "GET") { fetchOptions.body = JSON.stringify(options.body); } const response = await fetch(urlString, fetchOptions); if (!response.ok) { if (response.status === 429) { retries++; if (retries >= maxRetries) throw new Error(`Rate limit exceeded after ${maxRetries} retries`); const retryAfterHeader = response.headers.get("retry-after"); const retryAfter = retryAfterHeader ? parseInt(retryAfterHeader, 10) : 0; const delay = retryAfter ? retryAfter * 1e3 : Math.pow(2, retries) * 1e3; console.log(`[SP-API] \u901F\u7387\u9650\u5236\u3002${delay}ms \u540E\u91CD\u8BD5... (\u5C1D\u8BD5 ${retries}/${maxRetries})`); await new Promise((resolve) => setTimeout(resolve, delay)); continue; } if (response.status >= 500) { retries++; if (retries >= maxRetries) throw new Error(`Server error ${response.status} after ${maxRetries} retries`); const delay = Math.pow(2, retries) * 1e3; console.log( `[SP-API] \u670D\u52A1\u5668\u9519\u8BEF ${response.status}\u3002${delay}ms \u540E\u91CD\u8BD5... (\u5C1D\u8BD5 ${retries}/${maxRetries})` ); await new Promise((resolve) => setTimeout(resolve, delay)); continue; } const errorText = await response.text(); let errorData; try { errorData = JSON.parse(errorText); } catch (e) { errorData = errorText; } console.error(`[SP-API] \u9519\u8BEF: ${options.method} ${urlString}`, { status: response.status, data: errorData, statusText: response.statusText }); console.error(errorData); const error = new Error( `Request failed with status ${response.status}: ${JSON.stringify(errorData)}` ); error.response = { status: response.status, data: errorData, headers: response.headers }; throw error; } const responseText = await response.text(); if (!responseText.trim()) return {}; return JSON.parse(responseText); } catch (error) { if (error.response) { throw error; } console.error(`[SP-API] \u7F51\u7EDC/\u672A\u77E5\u9519\u8BEF: ${error.message}`); throw error; } } throw new Error("\u8D85\u8FC7\u6700\u5927\u91CD\u8BD5\u6B21\u6570"); } }; // backend/modules/fmode-amazon-sp-api/src/parse-init.ts var isInitialized = false; var getParse = async () => { let Parse = globalThis.Parse; if (!Parse) { try { const mod = await import("parse/lib/node/Parse.js"); Parse = mod.default || mod; } catch { const mod2 = await import("parse"); Parse = mod2.default || mod2; } const appId = process.env.PARSE_APP_ID || "TARGET_PARSE_APP_ID"; const serverURL = process.env.PARSE_SERVER_URL || "http://localhost:3000/parse"; const masterKey = process.env.PARSE_MASTER_KEY || "TARGET_PARSE_MASTER_KEY"; Parse.initialize(appId, void 0, masterKey); Parse.serverURL = serverURL; globalThis.Parse = Parse; isInitialized = true; } return Parse; }; // backend/modules/fmode-amazon-sp-api/src/middleware/auth.ts var shopAuthMiddleware = async (req, res, next) => { if (req.path === "/test") { return next(); } try { const shopId = req.headers["shop-objectid"] || req.headers["x-shop-objectid"]; if (!shopId) { return res.status(400).json({ success: false, message: "\u7F3A\u5C11\u5FC5\u9700\u7684 header: shop-objectId" }); } const Parse = await getParse(); const query = new Parse.Query("Shop"); let shop; try { shop = await query.get(shopId, { useMasterKey: true }); console.log("[SP-API] \u8BA4\u8BC1\u4E2D\u95F4\u4EF6:", shop.get("name")); } catch (error) { console.log(error); return res.status(400).json({ success: false, message: `\u65E0\u6548\u7684 shopId: ${shopId}` }); } const config = shop.get("config").SpApiConfig; console.log("[SP-API] \u5E97\u94FA\u51ED\u8BC1\u5DF2\u52A0\u8F7D"); if (!config || !config.clientId || !config.clientSecret || !config.refreshToken) { return res.status(400).json({ success: false, message: "\u7F3A\u5C11\u4E9A\u9A6C\u900A\u914D\u7F6E\u4FE1\u606F" }); } req.spApiContext = { shopId, marketplaceId: shop.get("marketplaceId"), config }; next(); } catch (error) { console.error("[SP-API] \u8BA4\u8BC1\u4E2D\u95F4\u4EF6\u9519\u8BEF:", error); res.status(500).json({ success: false, message: "\u5E97\u94FA\u8BA4\u8BC1\u671F\u95F4\u53D1\u751F\u5185\u90E8\u670D\u52A1\u5668\u9519\u8BEF", error: error.message }); } }; // backend/modules/fmode-amazon-sp-api/src/api/customerFeedback.ts var CustomerFeedbackApi = class { constructor(client) { this.client = client; } /** * 检索某件商品的十个最正面和十个最负面的评论主题。 * GET /customerFeedback/2024-06-01/items/{asin}/reviews/topics */ async getItemReviewTopics(params) { const { asin, ...queryParams } = params; return this.client.request({ method: "GET", path: `/customerFeedback/2024-06-01/items/${asin}/reviews/topics`, query: queryParams }); } /** * 该API返回请求ASIN的关联浏览节点。 * GET /customerFeedback/2024-06-01/items/{asin}/browseNode */ async getItemBrowseNode(params) { const { asin, ...queryParams } = params; return this.client.request({ method: "GET", path: `/customerFeedback/2024-06-01/items/${asin}/browseNode`, query: queryParams }); } /** * 检索浏览节点中十个最正面和十个最负面的评论主题。 * GET /customerFeedback/2024-06-01/browseNodes/{browseNodeId}/reviews/topics */ async getBrowseNodeReviewTopics(params) { const { browseNodeId, ...queryParams } = params; return this.client.request({ method: "GET", path: `/customerFeedback/2024-06-01/browseNodes/${browseNodeId}/reviews/topics`, query: queryParams }); } /** * 检索过去六个月商品的正面和负面评价趋势。 * GET /customerFeedback/2024-06-01/items/{asin}/reviews/trends */ async getItemReviewTrends(params) { const { asin, ...queryParams } = params; return this.client.request({ method: "GET", path: `/customerFeedback/2024-06-01/items/${asin}/reviews/trends`, query: queryParams }); } /** * 检索过去六个月浏览节点的正面和负面评价趋势。 * GET /customerFeedback/2024-06-01/browseNodes/{browseNodeId}/reviews/trends */ async getBrowseNodeReviewTrends(params) { const { browseNodeId, ...queryParams } = params; return this.client.request({ method: "GET", path: `/customerFeedback/2024-06-01/browseNodes/${browseNodeId}/reviews/trends`, query: queryParams }); } /** * 检索客户在浏览节点退货时提到的主题。 * GET /customerFeedback/2024-06-01/browseNodes/{browseNodeId}/returns/topics */ async getBrowseNodeReturnTopics(params) { const { browseNodeId, ...queryParams } = params; return this.client.request({ method: "GET", path: `/customerFeedback/2024-06-01/browseNodes/${browseNodeId}/returns/topics`, query: queryParams }); } /** * 检索客户在浏览节点退货时提到的话题趋势。 * GET /customerFeedback/2024-06-01/browseNodes/{browseNodeId}/returns/trends */ async getBrowseNodeReturnTrends(params) { const { browseNodeId, ...queryParams } = params; return this.client.request({ method: "GET", path: `/customerFeedback/2024-06-01/browseNodes/${browseNodeId}/returns/trends`, query: queryParams }); } }; // backend/modules/fmode-amazon-sp-api/src/api/orders.ts var OrdersApi = class { constructor(client) { this.client = client; } /** * 获取订单列表 * GET /orders/v0/orders */ async getOrders(params) { return this.client.request({ method: "GET", path: "/orders/v0/orders", query: params }); } /** * 获取订单详情 * GET /orders/v0/orders/{orderId} */ async getOrder(orderId) { return this.client.request({ method: "GET", path: `/orders/v0/orders/${orderId}` }); } /** * 获取订单商品 * GET /orders/v0/orders/{orderId}/orderItems */ async getOrderItems(orderId, nextToken) { return this.client.request({ method: "GET", path: `/orders/v0/orders/${orderId}/orderItems`, query: nextToken ? { NextToken: nextToken } : void 0 }); } }; // backend/modules/fmode-amazon-sp-api/src/api/sales.ts var SalesApi = class { constructor(client) { this.client = client; } /** * 回报给定买方类型的给定区间内汇总订单指标 * GET /sales/v1/orderMetrics */ async getOrderMetrics(params) { return this.client.request({ method: "GET", path: "/sales/v1/orderMetrics", query: params }); } }; // backend/modules/fmode-amazon-sp-api/src/api/listings.ts var ListingsApi = class { constructor(client) { this.client = client; } /** * 列出所有列表项 * GET /listings/2021-08-01/items/{sellerId} */ async searchListingsItems(params) { const { sellerId, ...queryParams } = params; return this.client.request({ method: "GET", path: `/listings/2021-08-01/items/${sellerId}`, query: queryParams }); } /** * 获取列表项详情 * GET /listings/2021-08-01/items/{sellerId}/{sku} */ async getListingsItem(params) { const { sellerId, sku, ...queryParams } = params; return this.client.request({ method: "GET", path: `/listings/2021-08-01/items/${sellerId}/${sku}`, query: queryParams }); } /** * Put (创建/更新) 列表项 * PUT /listings/2021-08-01/items/{sellerId}/{sku} */ async putListingsItem(sellerId, sku, marketplaceIds, body) { return this.client.request({ method: "PUT", path: `/listings/2021-08-01/items/${sellerId}/${sku}`, query: { marketplaceIds: marketplaceIds.join(",") }, body }); } /** * 删除列表项 * DELETE /listings/2021-08-01/items/{sellerId}/{sku} */ async deleteListingsItem(sellerId, sku, marketplaceIds) { return this.client.request({ method: "DELETE", path: `/listings/2021-08-01/items/${sellerId}/${sku}`, query: { marketplaceIds: marketplaceIds.join(",") } }); } }; // backend/modules/fmode-amazon-sp-api/src/api/externalFulfillment.ts var ExternalFulfillmentApi = class { constructor(client) { this.client = client; } /** * 退货列表 * GET /externalFulfillment/2024-09-11/returns */ async listReturns(params) { return this.client.request({ method: "GET", path: "/externalFulfillment/2024-09-11/returns", query: params }); } /** * 获取退货详情 * GET /externalFulfillment/2024-09-11/returns/{returnId} */ async getReturn(returnId) { return this.client.request({ method: "GET", path: `/externalFulfillment/2024-09-11/returns/${returnId}` }); } }; // backend/modules/fmode-amazon-sp-api/src/api/catalogItems.ts var CatalogItemsApi = class { constructor(client) { this.client = client; } /** * 列表目录分类 * GET /catalog/v0/categories */ async listCatalogCategories(params) { return this.client.request({ method: "GET", path: "/catalog/v0/categories", query: params }); } /** * 搜索亚马逊目录商品 * GET /catalog/2022-04-01/items */ async searchCatalogItems(params) { return this.client.request({ method: "GET", path: "/catalog/2022-04-01/items", query: params }); } /** * 获取类目信息 * GET /catalog/2022-04-01/items/{asin} */ async getCatalogItem(params) { const { asin, ...queryParams } = params; return this.client.request({ method: "GET", path: `/catalog/2022-04-01/items/${asin}`, query: queryParams }); } }; // backend/modules/fmode-amazon-sp-api/src/api/sellers.ts var SellersApi = class { constructor(client) { this.client = client; } /** * 返回卖家账户及其市场的信息 * GET /sellers/v1/account */ async getAccount() { return this.client.request({ method: "GET", path: "/sellers/v1/account" }); } /** * 返回卖家可以列出商品的市场列表以及卖家在这些市场的参与信息 * GET /sellers/v1/marketplaceParticipations */ async getMarketplaceParticipations() { return this.client.request({ method: "GET", path: "/sellers/v1/marketplaceParticipations" }); } }; // backend/modules/fmode-amazon-sp-api/src/function.ts function parseNumber(value) { if (!value || value.trim() === "") return 0; const num = parseFloat(value.replace(/[^0-9.-]/g, "")); return isNaN(num) ? 0 : num; } function parseIntegerValue(value) { if (!value || value.trim() === "") return 0; const num = parseInt(value.replace(/[^0-9-]/g, "")); return isNaN(num) ? 0 : num; } function parseDate(value) { if (!value || value.trim() === "") return null; try { const date = new Date(value); return isNaN(date.getTime()) ? null : date; } catch (error) { return null; } } async function runCloudFunction(data, context, functionId) { if (!functionId) { throw new Error("functionId is required"); } try { console.log(`[SP-API] Running cloud function: ${functionId}`); const response = await fetch("http://localhost:3000/api/functions", { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ id: functionId, data, context }) }); if (!response.ok) { const errorData = await response.json().catch(() => ({ message: response.statusText })); throw new Error(`Cloud function failed: ${errorData.message || response.statusText}`); } const result = await response.json(); console.log(`[SP-API] Cloud function ${functionId} executed successfully`); return result; } catch (error) { console.error(`[SP-API] Cloud function ${functionId} execution failed:`, error.message); throw error; } } var functions = { /** * 处理 Amazon 报表文件 * 下载、解析并存储报表数据到数据库 * @param data 包含报表 URL 或报表信息的数据 * @param context 上下文信息 */ async processReportFile(data, context) { const Parse = await getParse(); if (!data || !data.reportDocumentUrl && !data.url) { console.warn("[SP-API] processReportFile \u7F3A\u5C11\u62A5\u8868 URL"); return { success: false, message: "\u7F3A\u5C11\u62A5\u8868 URL", processed: 0 }; } const shopId = context?.shopId; if (!shopId) { console.warn("[SP-API] processReportFile \u4E0A\u4E0B\u6587\u4E2D\u7F3A\u5C11 shopId"); return { success: false, message: "\u7F3A\u5C11 shopId", processed: 0 }; } const reportUrl = data.reportDocumentUrl || data.url; const reportType = data.reportType || "UNKNOWN"; const reportId = data.reportId || "unknown"; const compressionAlgorithm = data.compressionAlgorithm; console.log(`[SP-API] \u6B63\u5728\u5904\u7406\u62A5\u8868\u6587\u4EF6: ${reportType}`, { reportId, shopId, compressionAlgorithm: compressionAlgorithm || "NONE" }); try { console.log("[SP-API] \u6B63\u5728\u4E0B\u8F7D\u62A5\u8868\u6587\u4EF6..."); const response = await fetch(reportUrl); if (!response.ok) { throw new Error(`\u4E0B\u8F7D\u62A5\u8868\u5931\u8D25: ${response.status} ${response.statusText}`); } let reportContent; if (compressionAlgorithm && compressionAlgorithm !== "NONE") { console.log(`[SP-API] \u68C0\u6D4B\u5230\u538B\u7F29\u6587\u4EF6\uFF0C\u538B\u7F29\u7B97\u6CD5: ${compressionAlgorithm}`); const compressedData = await response.arrayBuffer(); console.log(`[SP-API] \u538B\u7F29\u6587\u4EF6\u4E0B\u8F7D\u6210\u529F\uFF0C\u5927\u5C0F: ${compressedData.byteLength} \u5B57\u8282`); reportContent = await this.decompressReportData(compressedData, compressionAlgorithm); console.log(`[SP-API] \u6587\u4EF6\u89E3\u538B\u6210\u529F\uFF0C\u89E3\u538B\u540E\u5927\u5C0F: ${reportContent.length} \u5B57\u7B26`); } else { reportContent = await response.text(); console.log(`[SP-API] \u62A5\u8868\u6587\u4EF6\u4E0B\u8F7D\u6210\u529F\uFF0C\u5927\u5C0F: ${reportContent.length} \u5B57\u7B26`); } let parsedData = []; let tableName = "Report"; switch (reportType) { case "GET_MERCHANT_LISTINGS_DATA": parsedData = await this.parseMerchantListingsReport(reportContent); tableName = "Product"; break; case "GET_FBA_FULFILLMENT_CUSTOMER_RETURNS_DATA": parsedData = await this.parseReturnsReport(reportContent); tableName = "ReturnRecord"; break; case "GET_FLAT_FILE_ORDERS_RECONCILIATION_DATA": parsedData = await this.parseOrdersReconciliationReport(reportContent); tableName = "OrderReconciliation"; break; case "GET_SALES_AND_TRAFFIC_REPORT": parsedData = await this.parseSalesTrafficReport(reportContent); tableName = "SalesTraffic"; break; default: parsedData = await this.parseGenericReport(reportContent, reportType); tableName = "GenericReport"; } if (parsedData.length === 0) { console.warn("[SP-API] \u62A5\u8868\u89E3\u6790\u540E\u6CA1\u6709\u6570\u636E"); return { success: true, message: "\u62A5\u8868\u4E3A\u7A7A", processed: 0 }; } const shopPointer = Parse.Object.extend("Shop").createWithoutData(shopId); const objectsToSave = []; for (const item of parsedData) { const reportObj = new Parse.Object(tableName); reportObj.set("shop", shopPointer); Object.keys(item).forEach((key) => { if (item[key] !== null && item[key] !== void 0 && item[key] !== "") { reportObj.set(key, item[key]); } }); objectsToSave.push(reportObj); } const batchSize = 100; let totalSaved = 0; for (let i = 0; i < objectsToSave.length; i += batchSize) { const batch = objectsToSave.slice(i, i + batchSize); try { await Parse.Object.saveAll(batch, { useMasterKey: true }); totalSaved += batch.length; console.log(`[SP-API] \u5DF2\u4FDD\u5B58 ${totalSaved}/${objectsToSave.length} \u6761\u8BB0\u5F55\u5230 ${tableName} \u8868`); } catch (error) { console.error(`[SP-API] \u4FDD\u5B58\u6279\u6B21 ${i}-${i + batch.length} \u5931\u8D25:`, error.message); } } console.log(`[SP-API] \u62A5\u8868\u5904\u7406\u5B8C\u6210: ${reportType}\uFF0C\u5171\u5904\u7406 ${totalSaved} \u6761\u8BB0\u5F55`); return { success: true, processed: totalSaved, reportType, reportId, tableName }; } catch (error) { console.error("[SP-API] \u5904\u7406\u62A5\u8868\u6587\u4EF6\u5931\u8D25:", error.message); return { success: false, message: error.message, processed: 0 }; } }, /** * 解析商品列表报表 (GET_MERCHANT_LISTINGS_DATA) */ async parseMerchantListingsReport(content) { const lines = content.split("\n"); if (lines.length < 2) return []; const headers = lines[0].split(" "); const results = []; for (let i = 1; i < lines.length; i++) { const line = lines[i].trim(); if (!line) continue; const values = line.split(" "); const item = {}; const rawData = {}; headers.forEach((header, index) => { const value = values[index] || ""; const cleanHeader = header.trim().replace(/[^a-zA-Z0-9_-]/g, "_").toLowerCase(); rawData[cleanHeader] = value; rawData[header.trim()] = value; }); item.item_name = rawData["item-name"] || rawData["item_name"] || rawData["product-name"] || ""; item.listing_id = rawData["listing-id"] || rawData["listing_id"] || ""; item.seller_sku = rawData["seller-sku"] || rawData["seller_sku"] || rawData["sku"] || ""; item.asin1 = rawData["asin1"] || ""; item.asin2 = rawData["asin2"] || ""; item.asin3 = rawData["asin3"] || ""; item.product_id = rawData["product-id"] || rawData["product_id"] || ""; item.product_id_type = rawData["product-id-type"] || rawData["product_id_type"] || "1"; if (i == 1) { console.log(item, rawData); } item.price = parseNumber(rawData["price"] || rawData["list-price"] || rawData["your-price"]); item.quantity = parseIntegerValue(rawData["quantity"] || rawData["available-quantity"]); item.pending_quantity = parseIntegerValue(rawData["pending-quantity"] || rawData["pending_quantity"]); item.business_price = parseNumber(rawData["business-price"] || rawData["business_price"]); item.quantity_price_type = rawData["quantity-price-type"] || rawData["quantity_price_type"] || ""; item.open_date = parseDate(rawData["open-date"] || rawData["open_date"] || rawData["date-created"]); item.fulfillment_channel = rawData["fulfillment-channel"] || rawData["fulfillment_channel"] || ""; item.will_ship_internationally = rawData["will-ship-internationally"] || rawData["will_ship_internationally"] || ""; item.expedited_shipping = rawData["expedited-shipping"] || rawData["expedited_shipping"] || ""; item.zshop_shipping_fee = parseNumber(rawData["zshop-shipping-fee"] || rawData["zshop_shipping_fee"]); item.merchant_shipping_group = rawData["merchant-shipping-group"] || rawData["merchant_shipping_group"] || ""; item.item_description = rawData["item-description"] || rawData["item_description"] || rawData["description"] || ""; item.item_note = rawData["item-note"] || rawData["item_note"] || ""; item.item_condition = rawData["item-condition"] || rawData["item_condition"] || rawData["condition"] || ""; item.zshop_category1 = rawData["zshop-category1"] || rawData["zshop_category1"] || ""; item.zshop_browse_path = rawData["zshop-browse-path"] || rawData["zshop_browse_path"] || ""; item.zshop_storefront_feature = rawData["zshop-storefront-feature"] || rawData["zshop_storefront_feature"] || ""; item.zshop_boldface = rawData["zshop-boldface"] || rawData["zshop_boldface"] || ""; item.image_url = rawData["image-url"] || rawData["image_url"] || rawData["main-image-url"] || ""; item.item_is_marketplace = rawData["item-is-marketplace"] || rawData["item_is_marketplace"] || ""; item.license_plate_number = rawData["license-plate-number"] || rawData["license_plate_number"] || ""; if (item.seller_sku || item.asin1 || item.item_name) { results.push(item); } } return results; }, /** * 解析退货报表 (GET_FBA_FULFILLMENT_CUSTOMER_RETURNS_DATA) */ async parseReturnsReport(content) { const lines = content.split("\n"); if (lines.length < 2) return []; const headers = lines[0].split(" "); const results = []; for (let i = 1; i < lines.length; i++) { const line = lines[i].trim(); if (!line) continue; const values = line.split(" "); const item = {}; headers.forEach((header, index) => { const value = values[index] || ""; const cleanHeader = header.trim().replace(/[^a-zA-Z0-9]/g, "_"); if (header.includes("date") || header.includes("Date")) { item[cleanHeader] = value ? new Date(value) : null; } else if (header.includes("quantity") || header.includes("Quantity")) { item[cleanHeader] = value ? parseInt(value) : 0; } else { item[cleanHeader] = value; } }); item.returnDate = item.return_date || null; item.orderId = item.order_id || item.amazon_order_id || ""; item.sku = item.sku || item.seller_sku || ""; item.asin = item.asin || ""; item.fnsku = item.fnsku || ""; item.product_name = item.product_name || ""; item.fulfillment_center_id = item.fulfillment_center_id || ""; item.detailed_disposition = item.detailed_disposition || ""; item.reason = item.reason || ""; item.license_plate_number = item.license_plate_number || ""; item.customer_comments = item.customer_comments || ""; results.push(item); } return results; }, /** * 解析订单对账报表 (GET_FLAT_FILE_ORDERS_RECONCILIATION_DATA) */ async parseOrdersReconciliationReport(content) { const lines = content.split("\n"); if (lines.length < 2) return []; const headers = lines[0].split(" "); const results = []; for (let i = 1; i < lines.length; i++) { const line = lines[i].trim(); if (!line) continue; const values = line.split(" "); const item = {}; headers.forEach((header, index) => { const value = values[index] || ""; const cleanHeader = header.trim().replace(/[^a-zA-Z0-9]/g, "_"); if (header.includes("amount") || header.includes("Amount") || header.includes("price") || header.includes("Price")) { item[cleanHeader] = value ? parseFloat(value) : 0; } else if (header.includes("quantity") || header.includes("Quantity")) { item[cleanHeader] = value ? parseInt(value) : 0; } else if (header.includes("date") || header.includes("Date")) { item[cleanHeader] = value ? new Date(value) : null; } else { item[cleanHeader] = value; } }); results.push(item); } return results; }, /** * 解析销售流量报表 (GET_SALES_AND_TRAFFIC_REPORT) */ async parseSalesTrafficReport(content) { try { const jsonData = JSON.parse(content); const results = []; if (jsonData.salesAndTrafficByDate) { jsonData.salesAndTrafficByDate.forEach((item) => { results.push({ date: new Date(item.date), orderedProductSales: parseFloat(item.salesByDate?.orderedProductSales?.amount || 0), orderedProductSalesB2B: parseFloat(item.salesByDate?.orderedProductSalesB2B?.amount || 0), unitsOrdered: parseInt(item.salesByDate?.unitsOrdered || 0), unitsOrderedB2B: parseInt(item.salesByDate?.unitsOrderedB2B || 0), totalOrderItems: parseInt(item.salesByDate?.totalOrderItems || 0), totalOrderItemsB2B: parseInt(item.salesByDate?.totalOrderItemsB2B || 0), averageSalesPerOrderItem: parseFloat(item.salesByDate?.averageSalesPerOrderItem?.amount || 0), averageUnitsPerOrderItem: parseFloat(item.salesByDate?.averageUnitsPerOrderItem || 0), sessions: parseInt(item.trafficByDate?.sessions || 0), sessionPercentage: parseFloat(item.trafficByDate?.sessionPercentage || 0), pageViews: parseInt(item.trafficByDate?.pageViews || 0), pageViewsPercentage: parseFloat(item.trafficByDate?.pageViewsPercentage || 0), buyBoxPercentage: parseFloat(item.trafficByDate?.buyBoxPercentage || 0) }); }); } return results; } catch (error) { return this.parseGenericReport(content, "GET_SALES_AND_TRAFFIC_REPORT"); } }, /** * 通用报表解析器(CSV/TSV 格式) */ async parseGenericReport(content, reportType) { const lines = content.split("\n"); if (lines.length < 2) return []; const delimiter = lines[0].includes(" ") ? " " : ","; const headers = lines[0].split(delimiter); const results = []; for (let i = 1; i < lines.length; i++) { const line = lines[i].trim(); if (!line) continue; const values = line.split(delimiter); const item = {}; headers.forEach((header, index) => { const value = values[index] || ""; const cleanHeader = header.trim().replace(/[^a-zA-Z0-9]/g, "_"); if (value.match(/^\d{4}-\d{2}-\d{2}/) || value.match(/^\d{2}\/\d{2}\/\d{4}/)) { item[cleanHeader] = new Date(value); } else if (value.match(/^\d+\.\d+$/)) { item[cleanHeader] = parseFloat(value); } else if (value.match(/^\d+$/)) { item[cleanHeader] = parseInt(value); } else { item[cleanHeader] = value; } }); item.reportType = reportType; results.push(item); } return results; }, /** * 解压缩报表数据 * @param compressedData 压缩的二进制数据 * @param compressionAlgorithm 压缩算法 (GZIP) * @returns 解压后的文本内容 */ async decompressReportData(compressedData, compressionAlgorithm) { try { if (compressionAlgorithm.toUpperCase() === "GZIP") { const zlib = await import("zlib"); const util = await import("util"); const buffer = Buffer.from(compressedData); const gunzip = util.promisify(zlib.gunzip); const decompressed = await gunzip(buffer); return decompressed.toString("utf-8"); } else { throw new Error(`\u4E0D\u652F\u6301\u7684\u538B\u7F29\u7B97\u6CD5: ${compressionAlgorithm}`); } } catch (error) { console.error(`[SP-API] \u89E3\u538B\u7F29\u5931\u8D25:`, error.message); throw new Error(`\u89E3\u538B\u7F29\u5931\u8D25: ${error.message}`); } }, // 清洗Orders数据 并且通过Parse将数据存储到数据库里面 async cleanOrders(data, context) { const Parse = await getParse(); if (!data || !data.payload || !data.payload.Orders) { console.warn("[SP-API] cleanOrders \u63A5\u6536\u5230\u65E0\u6548\u7684\u6570\u636E\u7ED3\u6784:", JSON.stringify(data).substring(0, 200)); return { success: false, message: "\u65E0\u6548\u7684\u6570\u636E\u7ED3\u6784", processed: 0 }; } const shopId = context?.shopId; if (!shopId) { console.warn("[SP-API] cleanOrders \u4E0A\u4E0B\u6587\u4E2D\u7F3A\u5C11 shopId"); return { success: false, message: "\u7F3A\u5C11 shopId", processed: 0 }; } const orders = data.payload.Orders; console.log("[SP-API] \u6B63\u5728\u6E05\u6D17\u8BA2\u5355...", orders.length, "\u4E2A\u8BA2\u5355\uFF0C\u5E97\u94FAID:", shopId); const objectsToSave = []; const shopPointer = Parse.Object.extend("Shop").createWithoutData(shopId); const orderIds = orders.map((o) => o.AmazonOrderId); let existingOrders = []; try { const query = new Parse.Query("Order"); query.containedIn("platformOrderId", orderIds); query.equalTo("shop", shopPointer); query.limit(1e3); existingOrders = await query.find({ useMasterKey: true }); } catch (error) { console.warn("[SP-API] \u67E5\u8BE2\u73B0\u6709\u8BA2\u5355\u8B66\u544A (\u5C06\u4F5C\u4E3A\u65B0\u8BA2\u5355\u7EE7\u7EED):", error); existingOrders = []; } const existingMap = /* @__PURE__ */ new Map(); existingOrders.forEach((o) => existingMap.set(o.get("platformOrderId"), o)); for (const orderData of orders) { let orderObj = existingMap.get(orderData.AmazonOrderId); if (!orderObj) { orderObj = new Parse.Object("Order"); orderObj.set("platformOrderId", orderData.AmazonOrderId); orderObj.set("shop", shopPointer); } orderObj.set("orderDate", new Date(orderData.PurchaseDate)); orderObj.set("status", orderData.OrderStatus); orderObj.set("marketplaceId", orderData.MarketplaceId); if (orderData.OrderTotal && orderData.OrderTotal.Amount) { orderObj.set("totalAmount", parseFloat(orderData.OrderTotal.Amount)); } else { if (orderObj.isNew()) { orderObj.set("totalAmount", 0); } } if (orderData.BuyerInfo) { orderObj.set("buyerInfo", orderData.BuyerInfo); } if (orderData.ShippingAddress) { orderObj.set("shippingAddress", orderData.ShippingAddress); orderObj.set("customerRegion", orderData.ShippingAddress.CountryCode); } if (orderObj.isNew()) { orderObj.set("items", []); } orderObj.set("lastUpdateDate", new Date(orderData.LastUpdateDate)); orderObj.set("fulfillmentChannel", orderData.FulfillmentChannel); orderObj.set("orderType", orderData.OrderType); if (orderData.ShippingAddress) { orderObj.set("shippingCountry", orderData.ShippingAddress.CountryCode); orderObj.set("shippingState", orderData.ShippingAddress.StateOrRegion); orderObj.set("shippingCity", orderData.ShippingAddress.City); } orderObj.set("isPrime", orderData.IsPrime || false); orderObj.set("isBusinessOrder", orderData.IsBusinessOrder || false); if (orderData.EarliestShipDate) { orderObj.set("earliestShipDate", new Date(orderData.EarliestShipDate)); } if (orderData.LatestShipDate) { orderObj.set("latestShipDate", new Date(orderData.LatestShipDate)); } objectsToSave.push(orderObj); } if (objectsToSave.length > 0) { try { await Parse.Object.saveAll(objectsToSave, { useMasterKey: true }); console.log(`[SP-API] \u6210\u529F\u4FDD\u5B58/\u66F4\u65B0 ${objectsToSave.length} \u4E2A\u8BA2\u5355\u3002`); } catch (error) { console.error("[SP-API] \u4FDD\u5B58\u8BA2\u5355\u9519\u8BEF:", error); for (const obj of objectsToSave) { try { await obj.save(null, { useMasterKey: true }); } catch (e) { console.error("\u4FDD\u5B58\u5355\u4E2A\u5BF9\u8C61\u9519\u8BEF:", obj.toJSON(), e); } } return { success: false, message: "\u4FDD\u5B58\u5230\u6570\u636E\u5E93\u9519\u8BEF (\u90E8\u5206\u5931\u8D25\u8BF7\u68C0\u67E5\u65E5\u5FD7)", error }; } } const nextToken = data?.payload?.NextToken || data?.NextToken; if (nextToken && context?.client) { const currentNextToken = String(nextToken).trim(); const previousNextToken = context.previousNextToken; if (previousNextToken && previousNextToken === currentNextToken) { console.warn("[SP-API] \u68C0\u6D4B\u5230\u91CD\u590D\u7684 NextToken\uFF0C\u505C\u6B62\u5206\u9875\u4EE5\u9632\u6B62\u6B7B\u5FAA\u73AF\u3002"); return { success: true, processed: objectsToSave.length, hasNext: false, message: "Duplicate NextToken detected" }; } try { const originalPath = context.requestPath || ""; const [basePath, rawQuery] = originalPath.split("?"); const keysToRemove = ["NextToken", "nextToken", "pageToken", "offset", "page", "startIndex"]; let newQueryString = ""; if (rawQuery) { const urlParams = new URLSearchParams(rawQuery); const parts = []; urlParams.forEach((value, key) => { if (!keysToRemove.includes(key)) { parts.push(`${key}=${encodeURIComponent(value)}`); } }); newQueryString = parts.join("&"); } if (context.requestQuery) { const extraParts = []; Object.entries(context.requestQuery).forEach(([k, v]) => { if (v === void 0 || v === null) return; if (!keysToRemove.includes(k) && !newQueryString.includes(`${k}=`)) { const valStr = Array.isArray(v) ? v.join(",") : String(v); extraParts.push(`${k}=${encodeURIComponent(valStr)}`); } }); if (extraParts.length > 0) { newQueryString = newQueryString ? `${newQueryString}&${extraParts.join("&")}` : extraParts.join("&"); } } const encodedToken = encodeURIComponent(currentNextToken); const nextPath = `${basePath}?NextToken=${encodedToken}`; await new Promise((resolve) => setTimeout(resolve, 3e4)); const nextResult = await context.client.request({ method: "GET", path: nextPath, context: { shopId: context.shopId, marketplaceId: context.marketplaceId, config: context.config } }); context.requestCount = (context.requestCount || 1) + 1; context.previousNextToken = currentNextToken; return await this.cleanOrders(nextResult, context); } catch (e) { console.error("[SP-API] \u83B7\u53D6\u4E0B\u4E00\u9875\u8BA2\u5355\u9519\u8BEF:", e); } } return { success: true, processed: objectsToSave.length, hasNext: !!nextToken }; }, /** * 清洗 Listings 数据并存储到数据库 * 对应 Parse Schema: Listing * 字段设计: * - shop: Pointer 必填 * - sku: String (sku) - 卖家SKU * - asin: String (summaries[0].asin) - 亚马逊ASIN 必填 * - title: String (summaries[0].itemName) - 商品标题 必填 * - mainImage: String (summaries[0].mainImage.link) - 主图链接 * - productType: String (summaries[0].productType) - 产品类型 * - status: String (summaries[0].status[0]) - 状态 * - createdDate: Date (summaries[0].createdDate) * - lastUpdatedDate: Date (summaries[0].lastUpdatedDate) * - marketplaceId: String (summaries[0].marketplaceId) * - attributes: Object (attributes) - 其他属性 * - parentSku: String (attributes?.child_parent_sku_relationship?.[0]?.parent_sku) - 父SKU * - issues: Array (issues) - 刊登问题 */ async cleanListings(data, context) { const Parse = await getParse(); if (typeof Parse === "undefined") { console.error("[SP-API] cleanListings \u5931\u8D25: \u5168\u5C40 Parse \u5BF9\u8C61\u4E0D\u5B58\u5728"); return { success: false, message: "\u5168\u5C40 Parse \u5BF9\u8C61\u4E0D\u5B58\u5728", processed: 0 }; } if (!data || !data.items || !Array.isArray(data.items)) { console.warn("[SP-API] cleanListings \u63A5\u6536\u5230\u65E0\u6548\u7684\u6570\u636E\u7ED3\u6784"); return { success: false, message: "\u65E0\u6548\u7684\u6570\u636E\u7ED3\u6784", processed: 0 }; } const shopId = context?.shopId; if (!shopId) { console.warn("[SP-API] cleanListings \u4E0A\u4E0B\u6587\u4E2D\u7F3A\u5C11 shopId"); return { success: false, message: "\u7F3A\u5C11 shopId", processed: 0 }; } const requestCount = context?.requestCount || 1; console.log(`[SP-API] cleanListings \u7B2C ${requestCount} \u6B21\u5FAA\u73AF\u8BF7\u6C42`); const items = data.items; console.log( `[SP-API] \u6B63\u5728\u6E05\u6D17 ${items.length} \u6761 Listing...`, data.numberOfResults, data.pagination ); const shopPointer = Parse.Object.extend("Shop").createWithoutData(shopId); const objectsToSave = []; const skus = items.map((item) => item.sku); console.log(`[SP-API] \u63D0\u53D6\u5230 ${skus.length} \u4E2A SKU \u7528\u4E8E\u67E5\u8BE2`, skus, context.requestCount); let existingListings = []; try { const query = new Parse.Query("Listing"); query.containedIn("sku", skus); query.equalTo("shop", shopPointer); query.limit(data.numberOfResults); existingListings = await query.find({ useMasterKey: true }); } catch (error) { console.warn("[SP-API] \u67E5\u8BE2\u73B0\u6709 Listing \u8B66\u544A (\u5C06\u4F5C\u4E3A\u65B0 Listing \u7EE7\u7EED):", error); } const existingMap = /* @__PURE__ */ new Map(); existingListings.forEach((l) => existingMap.set(l.get("asin"), l)); for (const item of items) { if (!item.summaries || item.summaries.length === 0) continue; let listingObj = existingMap.get(item.summaries[0].asin); if (!listingObj) { listingObj = new Parse.Object("Listing"); listingObj.set("sku", item.sku); listingObj.set("shop", shopPointer); } if (item.attributes) { const parentageList = item.attributes.parentage_level; if (Array.isArray(parentageList)) { const isChild = parentageList.some((p) => p.value === "child"); if (isChild) { const relationships = item.attributes.child_parent_sku_relationship; if (Array.isArray(relationships) && relationships.length > 0) { const rel = relationships[0]; if (rel && rel.parent_sku) { listingObj.set("parentSku", rel.parent_sku); } } } } } if (item.summaries && item.summaries.length > 0) { const summary = item.summaries[0]; listingObj.set("asin", summary.asin); listingObj.set("title", summary.itemName); listingObj.set("productType", summary.productType); listingObj.set("marketplaceId", summary.marketplaceId); if (summary.status && summary.status.length > 0) { listingObj.set("status", summary.status[0]); } if (summary.createdDate) { listingObj.set("createdDate", new Date(summary.createdDate)); } if (summary.lastUpdatedDate) { listingObj.set("lastUpdatedDate", new Date(summary.lastUpdatedDate)); } if (summary.mainImage && summary.mainImage.link) { listingObj.set("mainImage", summary.mainImage.link); } } if (item.attributes) { listingObj.set("attributes", item.attributes); } if (item.issues) { listingObj.set("issues", item.issues); } if (item.attributes?.list_price && item.attributes?.list_price.length > 0) { const listPrice = item.attributes?.list_price[0]; if (listPrice.value) { listingObj.set("price", parseFloat(listPrice.value)); listingObj.set("currency", listPrice.currency); } } if (item.fulfillmentAvailability && item.fulfillmentAvailability.length > 0) { const fa = item.fulfillmentAvailability[0]; listingObj.set("quantity", fa.quantity); } objectsToSave.push(listingObj); } if (objectsToSave.length > 0) { try { await Parse.Object.saveAll(objectsToSave, { useMasterKey: true }); console.log(`[SP-API] \u6210\u529F\u4FDD\u5B58/\u66F4\u65B0 ${objectsToSave.length} \u6761 Listing\u3002`); } catch (error) { console.error("[SP-API] \u4FDD\u5B58 Listing \u9519\u8BEF:", error); } } if (context.requestCount === 50) { console.log(data.pagination, data.pagination.nextToken, context?.client); } if (data.pagination && data.pagination.nextToken && context?.client) { const currentNextToken = data.pagination.nextToken; const previousNextToken = context.previousNextToken; if (previousNextToken && previousNextToken === currentNextToken) { console.warn("[SP-API] \u68C0\u6D4B\u5230\u91CD\u590D\u7684 nextToken\uFF0C\u505C\u6B62\u5206\u9875\u4EE5\u9632\u6B62\u6B7B\u5FAA\u73AF\u3002"); return { success: true, processed: objectsToSave.length, hasNext: false, message: "Duplicate nextToken detected" }; } const pageSize = items.length || 10; const maxRequests = Math.ceil((data.numberOfResults || 1e4) / pageSize) + 10; if (requestCount >= maxRequests) { console.warn(`[SP-API] \u5DF2\u8FBE\u5230\u6700\u5927\u8BF7\u6C42\u6B21\u6570 ${maxRequests}\uFF0C\u505C\u6B62\u5206\u9875\u3002`); return { success: true, processed: objectsToSave.length, hasNext: true, message: `Reached max requests limit (${maxRequests})` }; } console.log(`[SP-API] \u6B63\u5728\u83B7\u53D6\u4E0B\u4E00\u9875 Listing... (\u7B2C ${requestCount + 1} \u9875)`); try { const originalPath = context.requestPath || ""; const [basePath, rawQuery] = originalPath.split("?"); let newQueryString = ""; const keysToRemove = ["pageToken", "nextToken", "offset", "page", "startIndex"]; if (rawQuery) { newQueryString = rawQuery.split("&").filter(Boolean).filter((part) => !keysToRemove.includes(decodeURIComponent(part.split("=")[0] || ""))).join("&"); } if (context.requestQuery) { const extraParts = []; Object.entries(context.requestQuery).forEach(([k, v]) => { if (v === void 0 || v === null) return; if (!keysToRemove.includes(k)) { if (!newQueryString.includes(`${k}=`)) { const valStr = Array.isArray(v) ? v.join(",") : String(v); extraParts.push(`${k}=${encodeURIComponent(valStr)}`); } } }); if (extraParts.length > 0) { newQueryString = newQueryString ? `${newQueryString}&${extraParts.join("&")}` : extraParts.join("&"); } } const tokenVal = currentNextToken.trim(); console.log(`[SP-API] NextToken (Page ${requestCount + 1}):`, tokenVal.substring(0, 50) + "..."); const encodedToken = encodeURIComponent(tokenVal); const marketplaceQuery = context.marketplaceId ? `marketplaceIds=${encodeURIComponent(String(context.marketplaceId))}&` : ""; const nextPath = `${basePath}?${marketplaceQuery}pageToken=${encodedToken}`; console.log("[SP-API] Next Path:", nextPath.substring(0, 200) + "..."); await new Promise((resolve) => setTimeout(resolve, 3e3)); const nextResult = await context.client.request({ method: "GET", path: nextPath, context: { shopId, marketplaceId: context.marketplaceId, config: context.config } }); context.requestCount = requestCount + 1; context.previousNextToken = currentNextToken; return await this.cleanListings(nextResult, context); } catch (error) { console.error("[SP-API] \u83B7\u53D6\u4E0B\u4E00\u9875\u9519\u8BEF:", error.message); return { success: true, processed: objectsToSave.length, hasNext: true, error: error.message, message: "\u5206\u9875\u8BF7\u6C42\u5931\u8D25\uFF0C\u5DF2\u4FDD\u5B58\u5F53\u524D\u9875\u6570\u636E" }; } } return { success: true, processed: objectsToSave.length, hasNext: false }; } }; // backend/modules/fmode-amazon-sp-api/src/routes.ts var sendResponse = (res, data) => { res.json({ success: true, data, timestamp: (/* @__PURE__ */ new Date()).toISOString() }); }; var asyncHandler = (fn) => (req, res, next) => { Promise.resolve(fn(req, res, next)).catch(next); }; var createSpApiRouter = (staticClient) => { const router5 = Router(); router5.use(shopAuthMiddleware); const getClient = (req) => { return staticClient || new SpApiClient(); }; const createScopedApi = (ApiClass, req) => { const baseClient = getClient(req); const scopedClient = { request: async (options) => { return baseClient.request({ ...options, context: { shopId: req.spApiContext?.shopId, marketplaceId: req.spApiContext?.marketplaceId, config: req.spApiContext?.config } }); } }; return new ApiClass(scopedClient); }; router5.get( "/test", asyncHandler(async (req, res) => { sendResponse(res, { message: "SP-API \u8DEF\u7531\u6D4B\u8BD5\u6210\u529F" }); }) ); router5.get( "/customerFeedback/items/:asin/reviews/topics", asyncHandler(async (req, res) => { const api = createScopedApi(CustomerFeedbackApi, req); const result = await api.getItemReviewTopics({ ...req.query, asin: req.params.asin }); sendResponse(res, result); }) ); router5.get( "/customerFeedback/items/:asin/browseNode", asyncHandler(async (req, res) => { const api = createScopedApi(CustomerFeedbackApi, req); const result = await api.getItemBrowseNode({ ...req.query, asin: req.params.asin }); sendResponse(res, result); }) ); router5.get( "/customerFeedback/browseNodes/:browseNodeId/reviews/topics", asyncHandler(async (req, res) => { const api = createScopedApi(CustomerFeedbackApi, req); const result = await api.getBrowseNodeReviewTopics({ ...req.query, browseNodeId: req.params.browseNodeId }); sendResponse(res, result); }) ); router5.get( "/customerFeedback/items/:asin/reviews/trends", asyncHandler(async (req, res) => { const api = createScopedApi(CustomerFeedbackApi, req); const result = await api.getItemReviewTrends({ ...req.query, asin: req.params.asin }); sendResponse(res, result); }) ); router5.get( "/customerFeedback/browseNodes/:browseNodeId/reviews/trends", asyncHandler(async (req, res) => { const api = createScopedApi(CustomerFeedbackApi, req); const result = await api.getBrowseNodeReviewTrends({ ...req.query, browseNodeId: req.params.browseNodeId }); sendResponse(res, result); }) ); router5.get( "/customerFeedback/browseNodes/:browseNodeId/returns/topics", asyncHandler(async (req, res) => { const api = createScopedApi(CustomerFeedbackApi, req); const result = await api.getBrowseNodeReturnTopics({ ...req.query, browseNodeId: req.params.browseNodeId }); sendResponse(res, result); }) ); router5.get( "/customerFeedback/browseNodes/:browseNodeId/returns/trends", asyncHandler(async (req, res) => { const api = createScopedApi(CustomerFeedbackApi, req); const result = await api.getBrowseNodeReturnTrends({ ...req.query, browseNodeId: req.params.browseNodeId }); sendResponse(res, result); }) ); router5.get( "/orders", asyncHandler(async (req, res) => { const api = createScopedApi(OrdersApi, req); const result = await api.getOrders(req.query); console.log("result", result); sendResponse(res, result); }) ); router5.get( "/orders/:orderId", asyncHandler(async (req, res) => { const api = createScopedApi(OrdersApi, req); const result = await api.getOrder(req.params.orderId); sendResponse(res, result); }) ); router5.get( "/orders/:orderId/items", asyncHandler(async (req, res) => { const api = createScopedApi(OrdersApi, req); const result = await api.getOrderItems(req.params.orderId, req.query.NextToken); sendResponse(res, result); }) ); router5.get( "/sales/orderMetrics", asyncHandler(async (req, res) => { const api = createScopedApi(SalesApi, req); const result = await api.getOrderMetrics(req.query); sendResponse(res, result); }) ); router5.get( "/listings/items/:sellerId", asyncHandler(async (req, res) => { const api = createScopedApi(ListingsApi, req); const result = await api.searchListingsItems({ ...req.query, sellerId: req.params.sellerId }); sendResponse(res, result); }) ); router5.get( "/listings/items/:sellerId/:sku", asyncHandler(async (req, res) => { const api = createScopedApi(ListingsApi, req); const result = await api.getListingsItem({ ...req.query, sellerId: req.params.sellerId, sku: req.params.sku }); sendResponse(res, result); }) ); router5.put( "/listings/items/:sellerId/:sku", asyncHandler(async (req, res) => { const api = createScopedApi(ListingsApi, req); const marketplaceIds = req.query.marketplaceIds?.split(",") || []; const result = await api.putListingsItem( req.params.sellerId, req.params.sku, marketplaceIds, req.body ); sendResponse(res, result); }) ); router5.delete( "/listings/items/:sellerId/:sku", asyncHandler(async (req, res) => { const api = createScopedApi(ListingsApi, req); const marketplaceIds = req.query.marketplaceIds?.split(",") || []; const result = await api.deleteListingsItem( req.params.sellerId, req.params.sku, marketplaceIds ); sendResponse(res, result); }) ); router5.get( "/externalFulfillment/returns", asyncHandler(async (req, res) => { const api = createScopedApi(ExternalFulfillmentApi, req); const result = await api.listReturns(req.query); sendResponse(res, result); }) ); router5.get( "/externalFulfillment/returns/:returnId", asyncHandler(async (req, res) => { const api = createScopedApi(ExternalFulfillmentApi, req); const result = await api.getReturn(req.params.returnId); sendResponse(res, result); }) ); router5.get( "/catalog/categories", asyncHandler(async (req, res) => { const api = createScopedApi(CatalogItemsApi, req); const result = await api.listCatalogCategories(req.query); sendResponse(res, result); }) ); router5.get( "/catalog/items", asyncHandler(async (req, res) => { const api = createScopedApi(CatalogItemsApi, req); const result = await api.searchCatalogItems(req.query); sendResponse(res, result); }) ); router5.get( "/catalog/items/:asin", asyncHandler(async (req, res) => { const api = createScopedApi(CatalogItemsApi, req); const result = await api.getCatalogItem({ ...req.query, asin: req.params.asin }); sendResponse(res, result); }) ); router5.get( "/sellers/account", asyncHandler(async (req, res) => { const api = createScopedApi(SellersApi, req); const result = await api.getAccount(); sendResponse(res, result); }) ); router5.get( "/sellers/marketplaceParticipations", asyncHandler(async (req, res) => { const api = createScopedApi(SellersApi, req); const result = await api.getMarketplaceParticipations(); sendResponse(res, result); }) ); router5.post( "/forward", asyncHandler(async (req, res) => { const { path, method = "GET", query, body, functionName, functionId, requiresRdt, rdtDataElements } = req.body; if (!path) { res.status(400).json({ success: false, message: "Missing required parameter: path" }); return; } const client = getClient(req); const result = await client.request({ method: method.toUpperCase(), path, query, body, requiresRdt, rdtDataElements, context: { shopId: req.spApiContext?.shopId, marketplaceId: req.spApiContext?.marketplaceId, config: req.spApiContext?.config } }); console.log(result, result?.numberOfResults); if (functionId) { try { console.log(`[SP-API] Executing cloud function: ${functionId}`); const cloudResult = await runCloudFunction( result, { ...req.spApiContext, requestPath: path, requestQuery: query, requestBody: body, requestCount: 1 }, functionId ); sendResponse(res, { apiResult: result, cloudFunctionResult: cloudResult }); return; } catch (error) { console.error(`[SP-API] Error executing cloud function ${functionId}:`, error.message); res.status(500).json({ success: false, message: `Cloud function execution failed: ${error.message}`, data: result }); return; } } if (functionName && functions[functionName]) { try { console.log(`[SP-API] Executing function: ${functionName}`); await functions[functionName](result, { ...req.spApiContext, client, requestPath: path, requestQuery: query, requestCount: 1 }); } catch (error) { console.error(`[SP-API] Error executing function ${functionName}:`, error); } } sendResponse(res, result); }) ); router5.post( "/processReport", asyncHandler(async (req, res) => { const { reportDocumentId, reportType } = req.body; if (!reportDocumentId) { res.status(400).json({ success: false, message: "Missing required parameter: reportDocumentId" }); return; } const client = getClient(req); try { console.log(`[SP-API] \u6B63\u5728\u83B7\u53D6\u62A5\u8868\u6587\u6863: ${reportDocumentId}`); const documentResult = await client.request({ method: "GET", path: `/reports/2021-06-30/documents/${reportDocumentId}`, context: { shopId: req.spApiContext?.shopId, marketplaceId: req.spApiContext?.marketplaceId, config: req.spApiContext?.config } }); if (!documentResult || !documentResult.url) { res.status(404).json({ success: false, message: "Report document not found or URL not available" }); return; } console.log(`[SP-API] \u6B63\u5728\u5904\u7406\u62A5\u8868\u6587\u4EF6: ${documentResult.url}`); console.log(`[SP-API] \u538B\u7F29\u7B97\u6CD5: ${documentResult.compressionAlgorithm || "NONE"}`); const processResult = await functions.processReportFile( { reportDocumentUrl: documentResult.url, reportType: reportType || "UNKNOWN", reportId: reportDocumentId, compressionAlgorithm: documentResult.compressionAlgorithm }, { shopId: req.spApiContext?.shopId, marketplaceId: req.spApiContext?.marketplaceId, config: req.spApiContext?.config } ); sendResponse(res, { documentInfo: { reportDocumentId, url: documentResult.url, reportType: reportType || "UNKNOWN" }, processResult }); } catch (error) { console.error(`[SP-API] \u5904\u7406\u62A5\u8868\u6587\u6863\u5931\u8D25:`, error.message); res.status(500).json({ success: false, message: `Failed to process report document: ${error.message}`, reportDocumentId }); } }) ); router5.use((error, _req, res, _next) => { const status = Number(error?.response?.status || error?.status || 500); const upstream = error?.response?.data; res.status(status).json({ success: false, status, code: upstream?.errors?.[0]?.code || error?.code || "SP_API_ERROR", message: upstream?.errors?.[0]?.message || error?.message || "SP-API request failed", details: upstream?.errors?.[0]?.details || "" }); }); return router5; }; // backend/api/module/schedule/routes-schedule.ts import express3 from "npm:express"; // backend/api/module/schedule/sp-api-schedule.ts import nodeCron from "npm:node-cron"; var SpApiScheduler = class { // 私有属性(先声明) #isRunning = false; #lastRunTime = null; #cronTask = null; #reportCronTask = null; #cronEnabled = true; // ========== 私有工具方法(最优先声明,避免调用时未定义) ========== /** * 延迟函数 * @private * @param {number} ms - 延迟毫秒数 * @returns {Promise} 延迟 Promise */ #delay(ms) { return new Promise((resolve) => setTimeout(resolve, ms)); } /** * 计算下次执行时间(凌晨1点) * @private * @returns {Date} 下次执行时间 */ #calculateNextRunTime() { const now = /* @__PURE__ */ new Date(); const next = new Date(now); next.setHours(1, 0, 0, 0); if (next <= now) { next.setDate(next.getDate() + 1); } return next; } /** * 获取所有活跃的 Amazon 店铺 * @private * @returns {Promise} 活跃店铺列表 */ async #getActiveAmazonShops(shopId) { const Parse = globalThis.Parse; const query = new Parse.Query("Shop"); query.equalTo("platform", "amazon"); query.equalTo("status", "active"); if (shopId) { query.equalTo("objectId", shopId); } query.limit(1e3); return await query.find({ useMasterKey: true }); } /** * 记录执行日志 * @private * @param {any} logData - 日志数据对象 * @returns {Promise} */ async #logExecution(logData) { try { const Parse = globalThis.Parse; const TaskLog = Parse.Object.extend("TaskExecutionLog"); const log = new TaskLog(); log.set("taskName", logData.taskName); log.set("startTime", logData.startTime); log.set("endTime", logData.endTime); log.set("duration", logData.duration); log.set("successCount", logData.successCount); log.set("errorCount", logData.errorCount); log.set("errors", logData.errors); log.set("status", logData.status); await log.save(null, { useMasterKey: true }); console.log("[SP-API Scheduler] \u6267\u884C\u65E5\u5FD7\u5DF2\u4FDD\u5B58"); } catch (error) { console.error("[SP-API Scheduler] \u4FDD\u5B58\u6267\u884C\u65E5\u5FD7\u5931\u8D25:", error.message); } } /** * 采集 Listing 数据 * @private * @param {string} shopId - 店铺ID * @param {any} config - 店铺配置 * @param {string} marketplaceId - 市场ID * @returns {Promise} */ async #collectListings(shopId, config, marketplaceId) { const sellerId = config.SpApiConfig.sellerID; marketplaceId = marketplaceId || "ATVPDKIKX0DER"; const Parse = globalThis.Parse; let lastUpdatedAfter; try { const shopPointer = Parse.Object.extend("Shop").createWithoutData(shopId); const query = new Parse.Query("Listing"); query.equalTo("shop", shopPointer); query.descending("lastUpdatedDate"); query.limit(1); const latestListing = await query.first({ useMasterKey: true }); if (latestListing && latestListing.get("lastUpdatedDate")) { lastUpdatedAfter = latestListing.get("lastUpdatedDate").toISOString(); console.log(`[SP-API Scheduler] \u5E97\u94FA ${shopId} \u4F7F\u7528\u6700\u65B0\u7684 lastUpdatedDate: ${lastUpdatedAfter}`); } else { const oneYearAgo = new Date(Date.now() - 365 * 24 * 60 * 60 * 1e3); lastUpdatedAfter = oneYearAgo.toISOString(); console.log(`[SP-API Scheduler] \u5E97\u94FA ${shopId} \u6CA1\u6709Listing\u6570\u636E,\u4F7F\u7528\u4E00\u5E74\u524D\u7684\u65F6\u95F4: ${lastUpdatedAfter}`); } } catch (error) { const oneYearAgo = new Date(Date.now() - 365 * 24 * 60 * 60 * 1e3); lastUpdatedAfter = oneYearAgo.toISOString(); console.log(`[SP-API Scheduler] \u5E97\u94FA ${shopId} \u67E5\u8BE2Listing\u5931\u8D25,\u4F7F\u7528\u4E00\u5E74\u524D\u7684\u65F6\u95F4: ${lastUpdatedAfter}, \u9519\u8BEF: ${error.message}`); } let path = `/listings/2021-08-01/items/${sellerId}?marketplaceIds=${marketplaceId}&includedData=summaries,attributes,issues&withStatus=BUYABLE,DISCOVERABLE&sortBy=lastUpdatedDate&sortOrder=ASC&pageSize=10&lastUpdatedAfter=${lastUpdatedAfter}`; const response = await fetch("http://localhost:3000/api/amazon/forward", { method: "POST", headers: { "Content-Type": "application/json", "shop-objectid": shopId }, body: JSON.stringify({ path, method: "GET", functionName: "cleanListings" }) }); const result = await response.json(); if (!result.success) { throw new Error(result.message || "Listing \u91C7\u96C6\u5931\u8D25"); } console.log(`[SP-API Scheduler] \u5E97\u94FA ${shopId} Listing \u6570\u636E\u91C7\u96C6\u5B8C\u6210: ${result.processed || 0} \u6761\u8BB0\u5F55`); } /** * 采集订单数据(增量同步) * @private * @param {string} shopId - 店铺ID * @param {any} config - 店铺配置 * @param {string[]} marketplaceIds - 市场ID列表 * @param {any} shop - 店铺对象 * @returns {Promise} */ async #collectOrders(shopId, config, marketplaceId) { marketplaceId = marketplaceId || "ATVPDKIKX0DER"; const Parse = globalThis.Parse; let createdAfter; try { const shopPointer = Parse.Object.extend("Shop").createWithoutData(shopId); const query = new Parse.Query("Order"); query.equalTo("shop", shopPointer); query.descending("orderDate"); query.limit(1); const latestOrder = await query.first({ useMasterKey: true }); if (latestOrder && latestOrder.get("orderDate")) { createdAfter = latestOrder.get("orderDate").toISOString(); console.log(`[SP-API Scheduler] \u5E97\u94FA ${shopId} \u4F7F\u7528\u6700\u65B0\u7684 orderDate: ${createdAfter}`); } else { const ninetyDaysAgo = new Date(Date.now() - 90 * 24 * 60 * 60 * 1e3); createdAfter = ninetyDaysAgo.toISOString(); console.log(`[SP-API Scheduler] \u5E97\u94FA ${shopId} \u6CA1\u6709Order\u6570\u636E,\u4F7F\u752890\u5929\u524D\u7684\u65F6\u95F4: ${createdAfter}`); } } catch (error) { const ninetyDaysAgo = new Date(Date.now() - 90 * 24 * 60 * 60 * 1e3); createdAfter = ninetyDaysAgo.toISOString(); console.log(`[SP-API Scheduler] \u5E97\u94FA ${shopId} \u67E5\u8BE2Order\u5931\u8D25,\u4F7F\u752890\u5929\u524D\u7684\u65F6\u95F4: ${createdAfter}, \u9519\u8BEF: ${error.message}`); } const response = await fetch("http://localhost:3000/api/amazon/forward", { method: "POST", headers: { "Content-Type": "application/json", "shop-objectid": shopId }, body: JSON.stringify({ path: `/orders/v0/orders?MarketplaceIds=${marketplaceId}&CreatedAfter=${createdAfter}`, method: "GET", functionName: "cleanOrders" }) }); const result = await response.json(); if (!result.success) { throw new Error(result.message || "\u8BA2\u5355\u91C7\u96C6\u5931\u8D25"); } console.log(`[SP-API Scheduler] \u5E97\u94FA ${shopId} \u8BA2\u5355\u6570\u636E\u91C7\u96C6\u5B8C\u6210: ${result.processed || 0} \u6761\u8BB0\u5F55`); } /** * 采集报表数据 * @private * @param {string} shopId - 店铺ID * @param {any} config - 店铺配置 * @param {string[]} marketplaceId - 市场ID列表 * @returns {Promise} */ async #collectReports(shopId, config, marketplaceId) { const Parse = globalThis.Parse; const reportTypes = ["GET_FBA_FULFILLMENT_CUSTOMER_RETURNS_DATA"]; for (const reportType of reportTypes) { try { let dataStartTime; try { const q = new Parse.Query("Reports"); q.equalTo("shop", shopId); q.equalTo("reportType", reportType); q.descending("dataEndTime"); q.limit(1); const last = await q.first({ useMasterKey: true }); if (last && last.get("dataEndTime")) { dataStartTime = last.get("dataEndTime").toISOString(); } else { dataStartTime = new Date(Date.now() - 90 * 24 * 60 * 60 * 1e3).toISOString(); } } catch (e) { dataStartTime = new Date(Date.now() - 90 * 24 * 60 * 60 * 1e3).toISOString(); } const dataEndTime = (/* @__PURE__ */ new Date()).toISOString(); console.log(`[SP-API Scheduler] \u5E97\u94FA ${shopId} \u521B\u5EFA\u62A5\u8868 ${reportType}, range: ${dataStartTime} ~ ${dataEndTime}`); const createRes = await fetch("http://localhost:3000/api/amazon/forward", { method: "POST", headers: { "Content-Type": "application/json", "shop-objectid": shopId }, body: JSON.stringify({ path: "/reports/2021-06-30/reports", method: "POST", body: { reportType, marketplaceIds: [marketplaceId], dataStartTime, dataEndTime } }) }); const createResult = await createRes.json(); const reportId = createResult?.data?.reportId; if (!reportId) { console.error(`[SP-API Scheduler] \u521B\u5EFA\u62A5\u8868\u5931\u8D25 ${reportType}:`, JSON.stringify(createResult)); continue; } console.log(`[SP-API Scheduler] \u62A5\u8868\u5DF2\u521B\u5EFA, reportId: ${reportId}`); await this.#delay(3e3); const getRes = await fetch("http://localhost:3000/api/amazon/forward", { method: "POST", headers: { "Content-Type": "application/json", "shop-objectid": shopId }, body: JSON.stringify({ path: `/reports/2021-06-30/reports/${reportId}`, method: "GET" }) }); const reportData = await getRes.json(); const reportInfo = reportData?.data || {}; const ReportObj = Parse.Object.extend("Reports"); const rpt = new ReportObj(); rpt.set("shop", { __type: "Pointer", className: "Shop", objectId: shopId }); rpt.set("reportId", reportId); rpt.set("reportType", reportType); rpt.set("marketplaceIds", reportInfo.marketplaceIds || []); rpt.set("dataStartTime", reportInfo.dataStartTime ? new Date(reportInfo.dataStartTime) : null); rpt.set("dataEndTime", reportInfo.dataEndTime ? new Date(reportInfo.dataEndTime) : null); rpt.set("createdTime", reportInfo.createdTime ? new Date(reportInfo.createdTime) : null); rpt.set("processingStartTime", reportInfo.processingStartTime ? new Date(reportInfo.processingStartTime) : null); rpt.set("processingEndTime", reportInfo.processingEndTime ? new Date(reportInfo.processingEndTime) : null); rpt.set("processingStatus", reportInfo.processingStatus || "IN_QUEUE"); rpt.set("reportDocumentId", reportInfo.reportDocumentId || null); rpt.set("isParsed", false); await rpt.save(null, { useMasterKey: true }); console.log(`[SP-API Scheduler] \u62A5\u8868\u5DF2\u4FDD\u5B58\u5230 Reports \u8868, status: ${reportInfo.processingStatus}`); await this.#delay(2e3); } catch (error) { console.error(`[SP-API Scheduler] \u62A5\u8868 ${reportType} \u5904\u7406\u5931\u8D25: ${error.message}`); } } } /** * 处理未解析的报表 * @private * @param {string} shopId - 店铺ID * @returns {Promise} */ async #processUnparsedReports(shopId) { const Parse = globalThis.Parse; const query = new Parse.Query("Reports"); query.equalTo("shop", shopId); query.equalTo("isParsed", false); query.equalTo("processingStatus", "DONE"); query.limit(1e3); const unparsedReports = await query.find(); if (unparsedReports.length === 0) { console.log(`[SP-API Scheduler] \u5E97\u94FA ${shopId} \u6CA1\u6709\u672A\u89E3\u6790\u7684\u62A5\u8868`); return; } console.log(`[SP-API Scheduler] \u53D1\u73B0 ${unparsedReports.length} \u4E2A\u672A\u89E3\u6790\u62A5\u8868`); for (const report of unparsedReports) { try { const reportDocumentId = report.get("reportDocumentId"); const reportType = report.get("reportType"); const response = await fetch("http://localhost:3000/api/amazon/processReport", { method: "POST", headers: { "Content-Type": "application/json", "shop-objectid": shopId }, body: JSON.stringify({ reportDocumentId, reportType, shopId }) }); const result = await response.json(); if (result.success) { report.set("isParsed", true); report.set("parsedAt", /* @__PURE__ */ new Date()); report.set("parsedRecords", result.processed || 0); await report.save(null, { useMasterKey: true }); console.log(`[SP-API Scheduler] \u62A5\u8868 ${reportDocumentId} \u89E3\u6790\u5B8C\u6210: ${result.processed} \u6761\u8BB0\u5F55`); } await this.#delay(1e4); } catch (error) { console.error(`[SP-API Scheduler] \u62A5\u8868\u89E3\u6790\u5931\u8D25: ${error.message}`); } } } /** * 检查并更新未完成的报表状态 * 每天凌晨2点执行:查询 Reports 表中 status 不为 DONE 的报表, * 通过 reportId 获取最新状态并更新,然后解析已完成的报表 * @private * @returns {Promise} */ async #checkPendingReports(shopid) { const Parse = globalThis.Parse; console.log("[SP-API Scheduler] ========================================"); console.log("[SP-API Scheduler] \u5F00\u59CB\u68C0\u67E5\u672A\u5B8C\u6210\u7684\u62A5\u8868\u72B6\u6001"); console.log("[SP-API Scheduler] \u6267\u884C\u65F6\u95F4:", (/* @__PURE__ */ new Date()).toISOString()); console.log("[SP-API Scheduler] ========================================"); try { const query = new Parse.Query("Reports"); query.notEqualTo("processingStatus", "DONE"); if (shopid) { query.equalTo("shop", shopid); } query.limit(1e3); const pendingReports = await query.find({ useMasterKey: true }); if (pendingReports.length === 0) { console.log("[SP-API Scheduler] \u6CA1\u6709\u672A\u5B8C\u6210\u7684\u62A5\u8868"); return; } console.log(`[SP-API Scheduler] \u53D1\u73B0 ${pendingReports.length} \u4E2A\u672A\u5B8C\u6210\u62A5\u8868`); const shopIdsToProcess = /* @__PURE__ */ new Set(); for (const report of pendingReports) { try { const reportId = report.get("reportId"); const shopId = report.get("shop").id; if (!reportId || !shopId) continue; console.log(`[SP-API Scheduler] \u68C0\u67E5\u62A5\u8868 ${reportId} \u72B6\u6001...`); const getRes = await fetch("http://localhost:3000/api/amazon/forward", { method: "POST", headers: { "Content-Type": "application/json", "shop-objectid": shopId }, body: JSON.stringify({ path: `/reports/2021-06-30/reports/${reportId}`, method: "GET" }) }); const reportData = await getRes.json(); const reportInfo = reportData?.data || {}; const newStatus = reportInfo.processingStatus || report.get("processingStatus"); report.set("processingStatus", newStatus); if (reportInfo.reportDocumentId) { report.set("reportDocumentId", reportInfo.reportDocumentId); } await report.save(null, { useMasterKey: true }); console.log(`[SP-API Scheduler] \u62A5\u8868 ${reportId} \u72B6\u6001\u66F4\u65B0\u4E3A: ${newStatus}`); if (newStatus === "DONE") { shopIdsToProcess.add(shopId); } await this.#delay(1e4); } catch (error) { console.error(`[SP-API Scheduler] \u66F4\u65B0\u62A5\u8868\u72B6\u6001\u5931\u8D25: ${error.message}`); } } for (const shopId of shopIdsToProcess) { try { console.log(`[SP-API Scheduler] \u89E3\u6790\u5E97\u94FA ${shopId} \u7684\u5DF2\u5B8C\u6210\u62A5\u8868`); await this.#processUnparsedReports(shopId); } catch (error) { console.error(`[SP-API Scheduler] \u5E97\u94FA ${shopId} \u62A5\u8868\u89E3\u6790\u5931\u8D25: ${error.message}`); } } console.log("[SP-API Scheduler] \u672A\u5B8C\u6210\u62A5\u8868\u68C0\u67E5\u5B8C\u6BD5"); } catch (error) { console.error(`[SP-API Scheduler] \u68C0\u67E5\u672A\u5B8C\u6210\u62A5\u8868\u5931\u8D25: ${error.message}`); } } /** * 处理单个店铺的数据采集 * @private * @param {any} shop - 店铺对象 * @returns {Promise} */ async #processShop(shop) { const shopId = shop.id; const shopName = shop.get("name"); const config = shop.get("config"); const marketplaceId = shop.get("marketplaceId"); if (!config || !config.SpApiConfig) { throw new Error("\u5E97\u94FA\u914D\u7F6E\u7F3A\u5931"); } if (config.SpApiConfig.listingEnabled !== false) { try { console.log(`[SP-API Scheduler] \u91C7\u96C6\u5E97\u94FA ${shopName} \u7684 Listing \u6570\u636E`); await this.#collectListings(shopId, config, marketplaceId); await this.#delay(3e3); } catch (error) { console.error(`[SP-API Scheduler] Listing \u91C7\u96C6\u5931\u8D25: ${error.message}`); } } try { console.log(`[SP-API Scheduler] \u91C7\u96C6\u5E97\u94FA ${shopName} \u7684\u8BA2\u5355\u6570\u636E`); await this.#collectOrders(shopId, config, marketplaceId); await this.#delay(3e3); } catch (error) { console.error(`[SP-API Scheduler] \u8BA2\u5355\u91C7\u96C6\u5931\u8D25: ${error.message}`); } try { console.log(`[SP-API Scheduler] \u91C7\u96C6\u5E97\u94FA ${shopName} \u7684\u62A5\u8868\u6570\u636E`); await this.#collectReports(shopId, config, marketplaceId); await this.#delay(3e3); } catch (error) { console.error(`[SP-API Scheduler] \u62A5\u8868\u91C7\u96C6\u5931\u8D25: ${error.message}`); } try { console.log(`[SP-API Scheduler] \u5904\u7406\u5E97\u94FA ${shopName} \u7684\u672A\u89E3\u6790\u62A5\u8868`); await this.#checkPendingReports(shopId); } catch (error) { console.error(`[SP-API Scheduler] \u62A5\u8868\u5904\u7406\u5931\u8D25: ${error.message}`); } shop.set("lastSyncTime", /* @__PURE__ */ new Date()); shop.set("syncStatus", "completed"); await shop.save(null, { useMasterKey: true }); } /** * 执行数据采集任务(核心方法:移到调用方之前) * @private * @returns {Promise} 执行结果对象,包含成功数、失败数、耗时等信息 */ async #executeDataCollection(shopId) { if (this.#isRunning) { console.log("[SP-API Scheduler] \u4EFB\u52A1\u6B63\u5728\u6267\u884C\u4E2D\uFF0C\u8DF3\u8FC7\u672C\u6B21\u8C03\u5EA6"); return { success: false, message: "\u4EFB\u52A1\u6B63\u5728\u6267\u884C\u4E2D", successCount: 0, errorCount: 0, duration: 0, errors: [] }; } this.#isRunning = true; const startTime = Date.now(); const errors = []; let successCount = 0; let errorCount = 0; console.log("[SP-API Scheduler] ========================================"); console.log("[SP-API Scheduler] \u5F00\u59CB\u6267\u884C\u6BCF\u65E5\u6570\u636E\u91C7\u96C6\u4EFB\u52A1"); console.log("[SP-API Scheduler] \u6267\u884C\u65F6\u95F4:", (/* @__PURE__ */ new Date()).toISOString()); console.log("[SP-API Scheduler] ========================================"); try { const shops = await this.#getActiveAmazonShops(shopId); if (shops.length === 0) { console.log("[SP-API Scheduler] \u6CA1\u6709\u627E\u5230\u6D3B\u8DC3\u7684 Amazon \u5E97\u94FA"); return { success: true, message: "\u6CA1\u6709\u6D3B\u8DC3\u5E97\u94FA\u9700\u8981\u5904\u7406", successCount: 0, errorCount: 0, duration: Date.now() - startTime, errors: [] }; } console.log(`[SP-API Scheduler] \u53D1\u73B0 ${shops.length} \u4E2A Amazon \u5E97\u94FA`); for (const shop of shops) { try { console.log(` [SP-API Scheduler] ----------------------------------------`); console.log(`[SP-API Scheduler] \u5F00\u59CB\u5904\u7406\u5E97\u94FA: ${shop.get("name")} (${shop.id})`); await this.#processShop(shop); successCount++; console.log(`[SP-API Scheduler] \u5E97\u94FA ${shop.get("name")} \u5904\u7406\u5B8C\u6210`); await this.#delay(2e3); } catch (error) { errorCount++; const errorMsg = `\u5E97\u94FA ${shop.get("name")} \u5904\u7406\u5931\u8D25: ${error.message}`; errors.push(errorMsg); console.error(`[SP-API Scheduler] ${errorMsg}`); } } const duration = Date.now() - startTime; await this.#logExecution({ taskName: "sp-api-daily-sync", startTime: new Date(startTime), endTime: /* @__PURE__ */ new Date(), duration, successCount, errorCount, errors, status: errorCount === 0 ? "success" : successCount > 0 ? "partial_success" : "failed" }); this.#lastRunTime = /* @__PURE__ */ new Date(); const message = `\u6570\u636E\u91C7\u96C6\u5B8C\u6210: \u6210\u529F ${successCount}/${shops.length} \u4E2A\u5E97\u94FA\uFF0C\u8017\u65F6 ${Math.round(duration / 1e3)}\u79D2`; console.log(` [SP-API Scheduler] ========================================`); console.log(`[SP-API Scheduler] ${message}`); console.log(`[SP-API Scheduler] ======================================== `); return { success: errorCount === 0, message, successCount, errorCount, duration, errors }; } catch (error) { const duration = Date.now() - startTime; const errorMsg = `\u6570\u636E\u91C7\u96C6\u4EFB\u52A1\u6267\u884C\u5931\u8D25: ${error.message}`; console.error(`[SP-API Scheduler] ${errorMsg}`); return { success: false, message: errorMsg, successCount, errorCount: errorCount + 1, duration, errors: [...errors, errorMsg] }; } finally { this.#isRunning = false; } } // ========== 公共方法(后声明,因为依赖前面的私有方法) ========== /** * 构造函数 * 初始化 Amazon SP-API 数据采集调度器 */ constructor() { console.log("[SP-API Scheduler] \u521D\u59CB\u5316 Amazon SP-API \u6570\u636E\u91C7\u96C6\u8C03\u5EA6\u5668"); } /** * 启动定时任务 * 每天凌晨1点自动执行数据采集 * @returns {Promise} */ async start() { if (this.#cronTask) { console.log("[SP-API Scheduler] \u5B9A\u65F6\u4EFB\u52A1\u5DF2\u5728\u8FD0\u884C\u4E2D"); return; } this.#cronTask = nodeCron.schedule("0 1 * * *", async () => { await this.#executeDataCollection(); }, { timezone: "Asia/Shanghai" }); console.log("[SP-API Scheduler] \u5B9A\u65F6\u4EFB\u52A1\u5DF2\u542F\u52A8\uFF0C\u5C06\u5728\u6BCF\u5929\u51CC\u66681\u70B9\u6267\u884C"); this.#reportCronTask = nodeCron.schedule("0 2 * * *", async () => { await this.#checkPendingReports(); }, { timezone: "Asia/Shanghai" }); console.log("[SP-API Scheduler] \u62A5\u8868\u72B6\u6001\u68C0\u67E5\u4EFB\u52A1\u5DF2\u542F\u52A8\uFF0C\u5C06\u5728\u6BCF\u5929\u51CC\u66682\u70B9\u6267\u884C"); } /** * 停止定时任务 * @returns {void} */ stop() { if (this.#cronTask) { this.#cronTask.stop(); this.#cronTask = null; console.log("[SP-API Scheduler] \u6570\u636E\u91C7\u96C6\u5B9A\u65F6\u4EFB\u52A1\u5DF2\u505C\u6B62"); } if (this.#reportCronTask) { this.#reportCronTask.stop(); this.#reportCronTask = null; console.log("[SP-API Scheduler] \u62A5\u8868\u72B6\u6001\u68C0\u67E5\u5B9A\u65F6\u4EFB\u52A1\u5DF2\u505C\u6B62"); } } /** * 获取调度器状态 * @returns {object} 调度器状态对象 */ getStatus() { const nextRunTime = this.#calculateNextRunTime(); return { isRunning: this.#isRunning, lastRunTime: this.#lastRunTime, nextRunTime, cronEnabled: this.#cronEnabled }; } /** * 手动触发数据采集 * @returns {Promise} 执行结果对象 */ async triggerManually(shopId) { console.log("[SP-API Scheduler] \u624B\u52A8\u89E6\u53D1\u6570\u636E\u91C7\u96C6\u4EFB\u52A1"); return await this.#executeDataCollection(shopId); } }; var spApiScheduler = new SpApiScheduler(); // backend/api/module/schedule/new-sp-api-schedule.ts import nodeCron2 from "npm:node-cron"; var NewSpApiScheduler = class { #cronTask = null; #reportCronTask = null; #lastRunTime = null; // ========== 私有工具 ========== #delay(ms) { return new Promise((resolve) => setTimeout(resolve, ms)); } #calculateNextRunTime() { const now = /* @__PURE__ */ new Date(); const next = new Date(now); next.setHours(1, 0, 0, 0); if (next <= now) next.setDate(next.getDate() + 1); return next; } /** * 查询活跃的 Amazon 店铺列表 * @param shopId 若传入则只返回该店铺 */ async #getActiveAmazonShops(shopId) { const Parse = globalThis.Parse; const query = new Parse.Query("Shop"); query.equalTo("platform", "amazon"); query.equalTo("status", "active"); if (shopId) query.equalTo("objectId", shopId); query.limit(1e3); return await query.find({ useMasterKey: true }); } // ========== 单店铺内部实现 ========== /** * 执行单个店铺的 Listing 采集 * 通过 /api/amazon/forward 调用 SP-API,内部 cleanListings 处理全部分页 */ async #doCollectListings(shopId, config, marketplaceId, progress) { const sellerId = config.SpApiConfig.sellerID; marketplaceId = marketplaceId || "ATVPDKIKX0DER"; const Parse = globalThis.Parse; let lastUpdatedAfter; try { const shopPointer = Parse.Object.extend("Shop").createWithoutData(shopId); const query = new Parse.Query("Listing"); query.equalTo("shop", shopPointer); query.descending("lastUpdatedDate"); query.limit(1); const latestListing = await query.first({ useMasterKey: true }); if (latestListing && latestListing.get("lastUpdatedDate")) { lastUpdatedAfter = latestListing.get("lastUpdatedDate").toISOString(); progress?.({ type: "info", message: `\u589E\u91CF\u91C7\u96C6\uFF0C\u4F7F\u7528\u6700\u65B0 lastUpdatedDate: ${lastUpdatedAfter}` }); } else { const oneYearAgo = new Date(Date.now() - 365 * 24 * 60 * 60 * 1e3); lastUpdatedAfter = oneYearAgo.toISOString(); progress?.({ type: "info", message: `\u65E0\u5386\u53F2\u6570\u636E\uFF0C\u4ECE\u4E00\u5E74\u524D\u5F00\u59CB\u5168\u91CF\u91C7\u96C6: ${lastUpdatedAfter}` }); } } catch (error) { const oneYearAgo = new Date(Date.now() - 365 * 24 * 60 * 60 * 1e3); lastUpdatedAfter = oneYearAgo.toISOString(); progress?.({ type: "warn", message: `\u67E5\u8BE2 Listing \u5386\u53F2\u5931\u8D25\uFF0C\u9ED8\u8BA4\u4F7F\u7528\u4E00\u5E74\u524D: ${lastUpdatedAfter}` }); } const path = `/listings/2021-08-01/items/${sellerId}?marketplaceIds=${marketplaceId}&includedData=summaries,attributes,issues&withStatus=BUYABLE,DISCOVERABLE&sortBy=lastUpdatedDate&sortOrder=ASC&pageSize=10&lastUpdatedAfter=${lastUpdatedAfter}`; progress?.({ type: "info", message: "\u6B63\u5728\u8C03\u7528\u4E9A\u9A6C\u900A Listing API\uFF08cleanListings \u5185\u90E8\u5904\u7406\u5206\u9875\uFF0C\u8BF7\u8010\u5FC3\u7B49\u5F85\uFF09..." }); console.log(`[New SP-API Scheduler] \u5E97\u94FA ${shopId} \u8BF7\u6C42 Listing API\uFF0Cpath: ${path}`); const response = await fetch("http://localhost:3000/api/amazon/forward", { method: "POST", headers: { "Content-Type": "application/json", "shop-objectid": shopId }, body: JSON.stringify({ path, method: "GET", functionName: "cleanListings" }) }); const result = await response.json(); if (!result.success) { throw new Error(result.message || "Listing \u91C7\u96C6\u5931\u8D25"); } const processed = result.data?.processed || 0; console.log(`[New SP-API Scheduler] \u5E97\u94FA ${shopId} Listing \u91C7\u96C6\u5B8C\u6210: ${processed} \u6761\u8BB0\u5F55`); return { processed }; } /** * 执行单个店铺的订单采集(增量) * 通过 /api/amazon/forward 调用 SP-API,内部 cleanOrders 处理全部分页 */ async #doCollectOrders(shopId, config, marketplaceId, progress) { marketplaceId = marketplaceId || "ATVPDKIKX0DER"; const Parse = globalThis.Parse; let createdAfter; try { const shopPointer = Parse.Object.extend("Shop").createWithoutData(shopId); const query = new Parse.Query("Order"); query.equalTo("shop", shopPointer); query.descending("orderDate"); query.limit(1); const latestOrder = await query.first({ useMasterKey: true }); if (latestOrder && latestOrder.get("orderDate")) { createdAfter = latestOrder.get("orderDate").toISOString(); progress?.({ type: "info", message: `\u589E\u91CF\u91C7\u96C6\uFF0C\u4F7F\u7528\u6700\u65B0 orderDate: ${createdAfter}` }); } else { const ninetyDaysAgo = new Date(Date.now() - 90 * 24 * 60 * 60 * 1e3); createdAfter = ninetyDaysAgo.toISOString(); progress?.({ type: "info", message: `\u65E0\u5386\u53F2\u6570\u636E\uFF0C\u4ECE90\u5929\u524D\u5F00\u59CB\u91C7\u96C6: ${createdAfter}` }); } } catch (error) { const ninetyDaysAgo = new Date(Date.now() - 90 * 24 * 60 * 60 * 1e3); createdAfter = ninetyDaysAgo.toISOString(); progress?.({ type: "warn", message: `\u67E5\u8BE2 Order \u5386\u53F2\u5931\u8D25\uFF0C\u9ED8\u8BA4\u4F7F\u752890\u5929\u524D: ${createdAfter}` }); } progress?.({ type: "info", message: "\u6B63\u5728\u8C03\u7528\u4E9A\u9A6C\u900A Orders API\uFF08cleanOrders \u5185\u90E8\u5904\u7406\u5206\u9875\uFF0C\u8BF7\u8010\u5FC3\u7B49\u5F85\uFF09..." }); console.log(`[New SP-API Scheduler] \u5E97\u94FA ${shopId} \u8BF7\u6C42 Orders API\uFF0CcreatedAfter: ${createdAfter}`); const response = await fetch("http://localhost:3000/api/amazon/forward", { method: "POST", headers: { "Content-Type": "application/json", "shop-objectid": shopId }, body: JSON.stringify({ path: `/orders/v0/orders?MarketplaceIds=${marketplaceId}&CreatedAfter=${createdAfter}`, method: "GET", functionName: "cleanOrders" }) }); const result = await response.json(); if (!result.success) { throw new Error(result.message || "\u8BA2\u5355\u91C7\u96C6\u5931\u8D25"); } const processed = result.data?.processed || 0; console.log(`[New SP-API Scheduler] \u5E97\u94FA ${shopId} \u8BA2\u5355\u91C7\u96C6\u5B8C\u6210: ${processed} \u6761\u8BB0\u5F55`); return { processed }; } /** * 执行单个店铺的报表创建与保存 */ async #doCollectReports(shopId, config, marketplaceId, progress) { const Parse = globalThis.Parse; const reportTypes = ["GET_FBA_FULFILLMENT_CUSTOMER_RETURNS_DATA"]; for (const reportType of reportTypes) { try { let dataStartTime; try { const q = new Parse.Query("Reports"); q.equalTo("shop", shopId); q.equalTo("reportType", reportType); q.descending("dataEndTime"); q.limit(1); const last = await q.first({ useMasterKey: true }); dataStartTime = last?.get("dataEndTime") ? last.get("dataEndTime").toISOString() : new Date(Date.now() - 90 * 24 * 60 * 60 * 1e3).toISOString(); } catch (e) { dataStartTime = new Date(Date.now() - 90 * 24 * 60 * 60 * 1e3).toISOString(); } const dataEndTime = (/* @__PURE__ */ new Date()).toISOString(); progress?.({ type: "info", message: `\u521B\u5EFA\u62A5\u8868 ${reportType}\uFF0C\u65F6\u95F4\u8303\u56F4: ${dataStartTime} ~ ${dataEndTime}` }); console.log(`[New SP-API Scheduler] \u5E97\u94FA ${shopId} \u521B\u5EFA\u62A5\u8868 ${reportType}, range: ${dataStartTime} ~ ${dataEndTime}`); const createRes = await fetch("http://localhost:3000/api/amazon/forward", { method: "POST", headers: { "Content-Type": "application/json", "shop-objectid": shopId }, body: JSON.stringify({ path: "/reports/2021-06-30/reports", method: "POST", body: { reportType, marketplaceIds: [marketplaceId], dataStartTime, dataEndTime } }) }); const createResult = await createRes.json(); const reportId = createResult?.data?.reportId; if (!reportId) { progress?.({ type: "error", message: `\u521B\u5EFA\u62A5\u8868\u5931\u8D25 ${reportType}: ${JSON.stringify(createResult)}` }); console.error(`[New SP-API Scheduler] \u521B\u5EFA\u62A5\u8868\u5931\u8D25 ${reportType}:`, JSON.stringify(createResult)); continue; } progress?.({ type: "info", message: `\u62A5\u8868\u5DF2\u521B\u5EFA reportId: ${reportId}\uFF0C\u7B49\u5F85\u67E5\u8BE2\u72B6\u6001...` }); console.log(`[New SP-API Scheduler] \u62A5\u8868\u5DF2\u521B\u5EFA, reportId: ${reportId}`); await this.#delay(3e3); const getRes = await fetch("http://localhost:3000/api/amazon/forward", { method: "POST", headers: { "Content-Type": "application/json", "shop-objectid": shopId }, body: JSON.stringify({ path: `/reports/2021-06-30/reports/${reportId}`, method: "GET" }) }); const reportData = await getRes.json(); const reportInfo = reportData?.data || {}; const ReportObj = Parse.Object.extend("Reports"); const rpt = new ReportObj(); rpt.set("shop", { __type: "Pointer", className: "Shop", objectId: shopId }); rpt.set("reportId", reportId); rpt.set("reportType", reportType); rpt.set("marketplaceIds", reportInfo.marketplaceIds || []); rpt.set("dataStartTime", reportInfo.dataStartTime ? new Date(reportInfo.dataStartTime) : null); rpt.set("dataEndTime", reportInfo.dataEndTime ? new Date(reportInfo.dataEndTime) : null); rpt.set("createdTime", reportInfo.createdTime ? new Date(reportInfo.createdTime) : null); rpt.set("processingStartTime", reportInfo.processingStartTime ? new Date(reportInfo.processingStartTime) : null); rpt.set("processingEndTime", reportInfo.processingEndTime ? new Date(reportInfo.processingEndTime) : null); rpt.set("processingStatus", reportInfo.processingStatus || "IN_QUEUE"); rpt.set("reportDocumentId", reportInfo.reportDocumentId || null); rpt.set("isParsed", false); await rpt.save(null, { useMasterKey: true }); progress?.({ type: "success", message: `\u62A5\u8868 ${reportType} \u5DF2\u4FDD\u5B58\uFF0CprocessingStatus: ${reportInfo.processingStatus || "IN_QUEUE"}` }); console.log(`[New SP-API Scheduler] \u62A5\u8868\u5DF2\u4FDD\u5B58, status: ${reportInfo.processingStatus}`); await this.#delay(2e3); } catch (error) { progress?.({ type: "error", message: `\u62A5\u8868 ${reportType} \u5904\u7406\u5931\u8D25: ${error.message}` }); console.error(`[New SP-API Scheduler] \u62A5\u8868 ${reportType} \u5904\u7406\u5931\u8D25: ${error.message}`); } } } /** * 检查并更新 processingStatus 不为 DONE 的报表 */ async #doCheckPendingReports(shopId, progress) { const Parse = globalThis.Parse; const query = new Parse.Query("Reports"); query.notEqualTo("processingStatus", "DONE"); if (shopId) query.equalTo("shop", shopId); query.limit(1e3); const pendingReports = await query.find({ useMasterKey: true }); if (pendingReports.length === 0) { progress?.({ type: "info", message: "\u6CA1\u6709\u72B6\u6001\u672A\u5B8C\u6210\u7684\u62A5\u8868\uFF0C\u8DF3\u8FC7\u72B6\u6001\u66F4\u65B0" }); console.log("[New SP-API Scheduler] \u6CA1\u6709\u672A\u5B8C\u6210\u7684\u62A5\u8868"); return; } progress?.({ type: "info", message: `\u53D1\u73B0 ${pendingReports.length} \u4E2A\u672A\u5B8C\u6210\u62A5\u8868\uFF0C\u9010\u4E00\u67E5\u8BE2\u6700\u65B0\u72B6\u6001...` }); console.log(`[New SP-API Scheduler] \u53D1\u73B0 ${pendingReports.length} \u4E2A\u672A\u5B8C\u6210\u62A5\u8868`); for (const report of pendingReports) { try { const reportId = report.get("reportId"); const sid = report.get("shop").id; if (!reportId || !sid) continue; const getRes = await fetch("http://localhost:3000/api/amazon/forward", { method: "POST", headers: { "Content-Type": "application/json", "shop-objectid": sid }, body: JSON.stringify({ path: `/reports/2021-06-30/reports/${reportId}`, method: "GET" }) }); const reportData = await getRes.json(); const reportInfo = reportData?.data || {}; const newStatus = reportInfo.processingStatus || report.get("processingStatus"); report.set("processingStatus", newStatus); if (reportInfo.reportDocumentId) report.set("reportDocumentId", reportInfo.reportDocumentId); await report.save(null, { useMasterKey: true }); progress?.({ type: "info", message: `\u62A5\u8868 ${reportId} \u72B6\u6001\u66F4\u65B0\u4E3A: ${newStatus}` }); console.log(`[New SP-API Scheduler] \u62A5\u8868 ${reportId} \u72B6\u6001\u66F4\u65B0\u4E3A: ${newStatus}`); await this.#delay(1e4); } catch (error) { progress?.({ type: "error", message: `\u66F4\u65B0\u62A5\u8868\u72B6\u6001\u5931\u8D25: ${error.message}` }); console.error(`[New SP-API Scheduler] \u66F4\u65B0\u62A5\u8868\u72B6\u6001\u5931\u8D25: ${error.message}`); } } } /** * 解析 isParsed=false & processingStatus=DONE 的报表 */ async #doProcessUnparsedReports(shopId, progress) { const Parse = globalThis.Parse; const query = new Parse.Query("Reports"); query.equalTo("shop", shopId); query.equalTo("isParsed", false); query.equalTo("processingStatus", "DONE"); query.limit(1e3); const unparsedReports = await query.find(); if (unparsedReports.length === 0) { progress?.({ type: "info", message: "\u6CA1\u6709\u5F85\u89E3\u6790\u7684\u62A5\u8868\uFF08\u72B6\u6001 DONE \u4E14\u672A\u89E3\u6790\uFF09" }); console.log(`[New SP-API Scheduler] \u5E97\u94FA ${shopId} \u6CA1\u6709\u5F85\u89E3\u6790\u7684\u62A5\u8868`); return { processed: 0 }; } progress?.({ type: "info", message: `\u53D1\u73B0 ${unparsedReports.length} \u4E2A\u5F85\u89E3\u6790\u62A5\u8868` }); console.log(`[New SP-API Scheduler] \u53D1\u73B0 ${unparsedReports.length} \u4E2A\u5F85\u89E3\u6790\u62A5\u8868`); let totalProcessed = 0; for (const report of unparsedReports) { try { const reportDocumentId = report.get("reportDocumentId"); const reportType = report.get("reportType"); if (!reportDocumentId) { progress?.({ type: "warn", message: `\u62A5\u8868 ${report.id} \u7F3A\u5C11 reportDocumentId\uFF0C\u8DF3\u8FC7` }); continue; } progress?.({ type: "info", message: `\u6B63\u5728\u89E3\u6790\u62A5\u8868 ${reportDocumentId} (${reportType})` }); console.log(`[New SP-API Scheduler] \u89E3\u6790\u62A5\u8868 ${reportDocumentId} (${reportType})`); const response = await fetch("http://localhost:3000/api/amazon/processReport", { method: "POST", headers: { "Content-Type": "application/json", "shop-objectid": shopId }, body: JSON.stringify({ reportDocumentId, reportType, shopId }) }); const result = await response.json(); if (result.success) { const parsed = result.data?.processResult?.processed || 0; report.set("isParsed", true); report.set("parsedAt", /* @__PURE__ */ new Date()); report.set("parsedRecords", parsed); await report.save(null, { useMasterKey: true }); totalProcessed += parsed; progress?.({ type: "success", message: `\u62A5\u8868 ${reportDocumentId} \u89E3\u6790\u5B8C\u6210: ${parsed} \u6761\u8BB0\u5F55`, processed: parsed }); console.log(`[New SP-API Scheduler] \u62A5\u8868 ${reportDocumentId} \u89E3\u6790\u5B8C\u6210: ${parsed} \u6761\u8BB0\u5F55`); } else { progress?.({ type: "warn", message: `\u62A5\u8868 ${reportDocumentId} \u89E3\u6790\u672A\u6210\u529F: ${result.message}` }); console.warn(`[New SP-API Scheduler] \u62A5\u8868 ${reportDocumentId} \u89E3\u6790\u672A\u6210\u529F: ${result.message}`); } await this.#delay(1e4); } catch (error) { progress?.({ type: "error", message: `\u62A5\u8868\u89E3\u6790\u5F02\u5E38: ${error.message}` }); console.error(`[New SP-API Scheduler] \u62A5\u8868\u89E3\u6790\u5F02\u5E38: ${error.message}`); } } return { processed: totalProcessed }; } // ========== 构造函数 ========== constructor() { console.log("[New SP-API Scheduler] \u521D\u59CB\u5316 Amazon SP-API \u6570\u636E\u91C7\u96C6\u8C03\u5EA6\u5668\uFF08\u65B0\u7248\uFF09"); } // ========== 公共采集方法 ========== /** * 采集所有活跃店铺(或指定店铺)的 Listing 数据 * @param shopId 可选,不传则采集全部活跃店铺 * @param progress 可选进度回调,适合 SSE 实时推送 */ async collectListings(shopId, progress) { const shops = await this.#getActiveAmazonShops(shopId); if (shops.length === 0) { const msg = shopId ? `\u672A\u627E\u5230\u5E97\u94FA ${shopId} \u6216\u5E97\u94FA\u672A\u6FC0\u6D3B` : "\u6CA1\u6709\u627E\u5230\u6D3B\u8DC3\u7684 Amazon \u5E97\u94FA"; progress?.({ type: "warn", message: msg }); console.warn(`[New SP-API Scheduler] ${msg}`); return { success: true, message: msg, processed: 0, errorCount: 0, errors: [] }; } let totalProcessed = 0; let errorCount = 0; const errors = []; for (const shop of shops) { const sid = shop.id; const shopName = shop.get("name"); const config = shop.get("config"); const marketplaceId = shop.get("marketplaceId") || "ATVPDKIKX0DER"; if (!config || !config.SpApiConfig) { const msg = `\u5E97\u94FA\u3010${shopName}\u3011(${sid}) \u7F3A\u5C11 SpApiConfig \u914D\u7F6E\uFF0C\u8DF3\u8FC7`; progress?.({ type: "error", message: msg }); console.error(`[New SP-API Scheduler] ${msg}`); errors.push(msg); errorCount++; continue; } if (config.SpApiConfig.listingEnabled === false) { progress?.({ type: "warn", message: `\u5E97\u94FA\u3010${shopName}\u3011\u672A\u914D\u7F6E\u533A\u57DF Seller ID\uFF0C\u8DF3\u8FC7 Listing` }); continue; } progress?.({ type: "info", message: `\u5F00\u59CB\u91C7\u96C6\u5E97\u94FA\u3010${shopName}\u3011\u7684 Listing \u6570\u636E` }); console.log(`[New SP-API Scheduler] \u5F00\u59CB\u91C7\u96C6\u5E97\u94FA ${shopName} (${sid}) \u7684 Listing \u6570\u636E`); try { const result = await this.#doCollectListings(sid, config, marketplaceId, progress); progress?.({ type: "success", message: `\u5E97\u94FA\u3010${shopName}\u3011Listing \u91C7\u96C6\u5B8C\u6210`, processed: result.processed }); totalProcessed += result.processed; await this.#delay(2e3); } catch (error) { const msg = `\u5E97\u94FA\u3010${shopName}\u3011Listing \u91C7\u96C6\u5931\u8D25: ${error.message}`; progress?.({ type: "error", message: msg }); console.error(`[New SP-API Scheduler] ${msg}`); errors.push(msg); errorCount++; } } const message = `Listing \u91C7\u96C6\u5B8C\u6210: \u6210\u529F ${shops.length - errorCount}/${shops.length} \u4E2A\u5E97\u94FA\uFF0C\u5171 ${totalProcessed} \u6761\u8BB0\u5F55`; return { success: errorCount === 0, message, processed: totalProcessed, errorCount, errors }; } /** * 采集所有活跃店铺(或指定店铺)的订单数据(增量) * @param shopId 可选 * @param progress 可选进度回调 */ async collectOrders(shopId, progress) { const shops = await this.#getActiveAmazonShops(shopId); if (shops.length === 0) { const msg = shopId ? `\u672A\u627E\u5230\u5E97\u94FA ${shopId} \u6216\u5E97\u94FA\u672A\u6FC0\u6D3B` : "\u6CA1\u6709\u627E\u5230\u6D3B\u8DC3\u7684 Amazon \u5E97\u94FA"; progress?.({ type: "warn", message: msg }); console.warn(`[New SP-API Scheduler] ${msg}`); return { success: true, message: msg, processed: 0, errorCount: 0, errors: [] }; } let totalProcessed = 0; let errorCount = 0; const errors = []; for (const shop of shops) { const sid = shop.id; const shopName = shop.get("name"); const config = shop.get("config"); const marketplaceId = shop.get("marketplaceId") || "ATVPDKIKX0DER"; if (!config || !config.SpApiConfig) { const msg = `\u5E97\u94FA\u3010${shopName}\u3011(${sid}) \u7F3A\u5C11 SpApiConfig \u914D\u7F6E\uFF0C\u8DF3\u8FC7`; progress?.({ type: "error", message: msg }); console.error(`[New SP-API Scheduler] ${msg}`); errors.push(msg); errorCount++; continue; } progress?.({ type: "info", message: `\u5F00\u59CB\u91C7\u96C6\u5E97\u94FA\u3010${shopName}\u3011\u7684\u8BA2\u5355\u6570\u636E` }); console.log(`[New SP-API Scheduler] \u5F00\u59CB\u91C7\u96C6\u5E97\u94FA ${shopName} (${sid}) \u7684\u8BA2\u5355\u6570\u636E`); try { const result = await this.#doCollectOrders(sid, config, marketplaceId, progress); progress?.({ type: "success", message: `\u5E97\u94FA\u3010${shopName}\u3011\u8BA2\u5355\u91C7\u96C6\u5B8C\u6210`, processed: result.processed }); totalProcessed += result.processed; await this.#delay(2e3); } catch (error) { const msg = `\u5E97\u94FA\u3010${shopName}\u3011\u8BA2\u5355\u91C7\u96C6\u5931\u8D25: ${error.message}`; progress?.({ type: "error", message: msg }); console.error(`[New SP-API Scheduler] ${msg}`); errors.push(msg); errorCount++; } } const message = `\u8BA2\u5355\u91C7\u96C6\u5B8C\u6210: \u6210\u529F ${shops.length - errorCount}/${shops.length} \u4E2A\u5E97\u94FA\uFF0C\u5171 ${totalProcessed} \u6761\u8BB0\u5F55`; return { success: errorCount === 0, message, processed: totalProcessed, errorCount, errors }; } /** * 为所有活跃店铺(或指定店铺)创建并保存报表 * @param shopId 可选 * @param progress 可选进度回调 */ async collectReports(shopId, progress) { const shops = await this.#getActiveAmazonShops(shopId); if (shops.length === 0) { const msg = shopId ? `\u672A\u627E\u5230\u5E97\u94FA ${shopId} \u6216\u5E97\u94FA\u672A\u6FC0\u6D3B` : "\u6CA1\u6709\u627E\u5230\u6D3B\u8DC3\u7684 Amazon \u5E97\u94FA"; progress?.({ type: "warn", message: msg }); console.warn(`[New SP-API Scheduler] ${msg}`); return { success: true, message: msg, processed: 0, errorCount: 0, errors: [] }; } let successCount = 0; let errorCount = 0; const errors = []; for (const shop of shops) { const sid = shop.id; const shopName = shop.get("name"); const config = shop.get("config"); const marketplaceId = shop.get("marketplaceId") || "ATVPDKIKX0DER"; if (!config || !config.SpApiConfig) { const msg = `\u5E97\u94FA\u3010${shopName}\u3011(${sid}) \u7F3A\u5C11 SpApiConfig \u914D\u7F6E\uFF0C\u8DF3\u8FC7`; progress?.({ type: "error", message: msg }); console.error(`[New SP-API Scheduler] ${msg}`); errors.push(msg); errorCount++; continue; } progress?.({ type: "info", message: `\u5F00\u59CB\u91C7\u96C6\u5E97\u94FA\u3010${shopName}\u3011\u7684\u62A5\u8868\u6570\u636E` }); console.log(`[New SP-API Scheduler] \u5F00\u59CB\u91C7\u96C6\u5E97\u94FA ${shopName} (${sid}) \u7684\u62A5\u8868\u6570\u636E`); try { await this.#doCollectReports(sid, config, marketplaceId, progress); progress?.({ type: "success", message: `\u5E97\u94FA\u3010${shopName}\u3011\u62A5\u8868\u91C7\u96C6\u5B8C\u6210` }); successCount++; await this.#delay(2e3); } catch (error) { const msg = `\u5E97\u94FA\u3010${shopName}\u3011\u62A5\u8868\u91C7\u96C6\u5931\u8D25: ${error.message}`; progress?.({ type: "error", message: msg }); console.error(`[New SP-API Scheduler] ${msg}`); errors.push(msg); errorCount++; } } const message = `\u62A5\u8868\u91C7\u96C6\u5B8C\u6210: \u6210\u529F ${successCount}/${shops.length} \u4E2A\u5E97\u94FA`; return { success: errorCount === 0, message, processed: successCount, errorCount, errors }; } /** * 检查未完成报表状态并解析已完成的报表 * @param shopId 可选 * @param progress 可选进度回调 */ async parseReports(shopId, progress) { const shops = await this.#getActiveAmazonShops(shopId); if (shops.length === 0) { const msg = shopId ? `\u672A\u627E\u5230\u5E97\u94FA ${shopId} \u6216\u5E97\u94FA\u672A\u6FC0\u6D3B` : "\u6CA1\u6709\u627E\u5230\u6D3B\u8DC3\u7684 Amazon \u5E97\u94FA"; progress?.({ type: "warn", message: msg }); console.warn(`[New SP-API Scheduler] ${msg}`); return { success: true, message: msg, processed: 0, errorCount: 0, errors: [] }; } progress?.({ type: "info", message: "\u68C0\u67E5\u6240\u6709\u672A\u5B8C\u6210\u62A5\u8868\u7684\u6700\u65B0\u72B6\u6001..." }); try { await this.#doCheckPendingReports(shopId, progress); } catch (error) { progress?.({ type: "warn", message: `\u68C0\u67E5\u672A\u5B8C\u6210\u62A5\u8868\u72B6\u6001\u65F6\u51FA\u9519: ${error.message}` }); console.warn(`[New SP-API Scheduler] \u68C0\u67E5\u672A\u5B8C\u6210\u62A5\u8868\u72B6\u6001\u51FA\u9519: ${error.message}`); } let totalProcessed = 0; let errorCount = 0; const errors = []; for (const shop of shops) { const sid = shop.id; const shopName = shop.get("name"); progress?.({ type: "info", message: `\u5F00\u59CB\u89E3\u6790\u5E97\u94FA\u3010${shopName}\u3011\u7684\u5DF2\u5B8C\u6210\u62A5\u8868` }); console.log(`[New SP-API Scheduler] \u5F00\u59CB\u89E3\u6790\u5E97\u94FA ${shopName} (${sid}) \u7684\u5DF2\u5B8C\u6210\u62A5\u8868`); try { const result = await this.#doProcessUnparsedReports(sid, progress); progress?.({ type: "success", message: `\u5E97\u94FA\u3010${shopName}\u3011\u62A5\u8868\u89E3\u6790\u5B8C\u6210: ${result.processed} \u6761\u8BB0\u5F55`, processed: result.processed }); totalProcessed += result.processed; await this.#delay(2e3); } catch (error) { const msg = `\u5E97\u94FA\u3010${shopName}\u3011\u62A5\u8868\u89E3\u6790\u5931\u8D25: ${error.message}`; progress?.({ type: "error", message: msg }); console.error(`[New SP-API Scheduler] ${msg}`); errors.push(msg); errorCount++; } } const message = `\u62A5\u8868\u89E3\u6790\u5B8C\u6210: \u6210\u529F ${shops.length - errorCount}/${shops.length} \u4E2A\u5E97\u94FA\uFF0C\u5171\u89E3\u6790 ${totalProcessed} \u6761\u8BB0\u5F55`; return { success: errorCount === 0, message, processed: totalProcessed, errorCount, errors }; } // ========== 定时任务管理 ========== /** * 启动定时任务 * - 凌晨 1 点:采集 Listing / Order / Report * - 凌晨 2 点:解析已完成报表 */ async start() { if (this.#cronTask) { console.log("[New SP-API Scheduler] \u5B9A\u65F6\u4EFB\u52A1\u5DF2\u5728\u8FD0\u884C\u4E2D\uFF0C\u8DF3\u8FC7\u91CD\u590D\u542F\u52A8"); return; } this.#cronTask = nodeCron2.schedule("0 1 * * *", async () => { console.log("[New SP-API Scheduler] ===== \u51CC\u66681\u70B9\uFF1A\u5F00\u59CB\u6BCF\u65E5\u6570\u636E\u91C7\u96C6 ====="); this.#lastRunTime = /* @__PURE__ */ new Date(); try { await this.collectListings(void 0, void 0); await this.collectOrders(void 0, void 0); await this.collectReports(void 0, void 0); } catch (e) { console.error("[New SP-API Scheduler] \u6BCF\u65E5\u91C7\u96C6\u4EFB\u52A1\u5931\u8D25:", e.message); } console.log("[New SP-API Scheduler] ===== \u6BCF\u65E5\u6570\u636E\u91C7\u96C6\u5B8C\u6210 ====="); }, { timezone: "Asia/Shanghai" }); this.#reportCronTask = nodeCron2.schedule("0 2 * * *", async () => { console.log("[New SP-API Scheduler] ===== \u51CC\u66682\u70B9\uFF1A\u5F00\u59CB\u62A5\u8868\u89E3\u6790 ====="); try { await this.parseReports(void 0, void 0); } catch (e) { console.error("[New SP-API Scheduler] \u62A5\u8868\u89E3\u6790\u4EFB\u52A1\u5931\u8D25:", e.message); } console.log("[New SP-API Scheduler] ===== \u62A5\u8868\u89E3\u6790\u5B8C\u6210 ====="); }, { timezone: "Asia/Shanghai" }); console.log("[New SP-API Scheduler] \u5B9A\u65F6\u4EFB\u52A1\u5DF2\u542F\u52A8\uFF08\u51CC\u66681\u70B9\u91C7\u96C6\uFF0C\u51CC\u66682\u70B9\u89E3\u6790\uFF09"); } /** * 停止定时任务 */ stop() { if (this.#cronTask) { this.#cronTask.stop(); this.#cronTask = null; console.log("[New SP-API Scheduler] \u91C7\u96C6\u5B9A\u65F6\u4EFB\u52A1\u5DF2\u505C\u6B62"); } if (this.#reportCronTask) { this.#reportCronTask.stop(); this.#reportCronTask = null; console.log("[New SP-API Scheduler] \u62A5\u8868\u89E3\u6790\u5B9A\u65F6\u4EFB\u52A1\u5DF2\u505C\u6B62"); } } /** * 获取调度器状态 */ getStatus() { return { isRunning: !!(this.#cronTask || this.#reportCronTask), lastRunTime: this.#lastRunTime, nextRunTime: this.#calculateNextRunTime() }; } }; var newSpApiScheduler = new NewSpApiScheduler(); // backend/api/module/schedule/sorftime-api-schedule.ts import nodeCron3 from "npm:node-cron"; function unwrapRelayData(result) { let data = result?.data ?? result?.Data ?? result; if (data && typeof data === "object" && !Array.isArray(data)) { data = data.data ?? data.Data ?? data; } return data; } function relaySucceeded(result) { const code = Number(result?.code ?? result?.Code ?? 200); return code === 0 || code === 200; } function siteCode(marketplaceId, domain) { const byMarketplace = { ATVPDKIKX0DER: "us", A2EUQ1WTGCTBG2: "ca", A1AM78C64UM0Y8: "mx", A2Q3Y263D00KWC: "br", A1F83G8C2ARO7P: "uk", A1PA6795UKMFR9: "de", A13V1IB3VIYZZH: "fr", A1RKKUPIHCS9HS: "es", APJ6JRA9NG5V4: "it", A1VC38T7YXB528: "jp", A39IBJ37TRP1C6: "au", A2VIGQ35RCS4UG: "ae", A17E79C6D8DWNP: "sa", A21TJRUUN4KGV: "in" }; return byMarketplace[marketplaceId] || String(domain || ""); } async function upsertSorftimeProduct(raw, shopId, shopName, marketplaceId, domain) { const Parse = globalThis.Parse; const asin = String(raw?.Asin || raw?.ASIN || raw?.asin || "").trim().toUpperCase(); if (!asin) return false; const shop = Parse.Object.extend("Shop").createWithoutData(shopId); const common = { asin, parentAsin: raw.ParentAsin || raw.ParentASIN || "", title: raw.Title || raw.title || "", photo: raw.Photo || raw.photo || "", imageUrl: Array.isArray(raw.Photo) ? raw.Photo[0] : raw.Photo || "", price: Number(raw.Price || 0), salesPrice: Number(raw.SalesPrice || raw.Price || 0), brand: raw.Brand || "", sellerId: raw.BuyboxSellerId || "", ratings: Number(raw.Ratings || 0), rating: Number(raw.Ratings || 0), ratingsCount: Number(raw.RatingsCount || 0), category: raw.Category || "", bsrCategory: raw.BsrCategory || [], rank: Number(raw.Rank || 0), listingSalesVolumeOfMonth: Number(raw.ListingSalesVolumeOfMonth || 0), ListingSalesVolumeOfMonth: Number(raw.ListingSalesVolumeOfMonth || 0), listingSalesOfMonth: Number(raw.ListingSalesOfMonth || 0), marketplaceId, domain: String(domain || ""), site: siteCode(marketplaceId, domain), storeName: shopName || "", shopName: shopName || "", shopId, shop, source: "sorftime", onlineDate: String(raw.OnlineDate || "").slice(0, 10), rawData: raw }; for (const className of ["Product", "ProductDetail", "SorftimeProduct"]) { const query = new Parse.Query(className); query.equalTo("asin", asin); if (className !== "SorftimeProduct") query.equalTo("shop", shop); let object = null; try { object = await query.first({ useMasterKey: true }); } catch (error) { const message = String(error?.message || error || ""); if (!message.includes("does not exist") && !message.includes("non-existent class")) throw error; } if (!object) object = new Parse.Object(className); for (const [key, value] of Object.entries(common)) { if (value !== void 0 && value !== null) object.set(key, value); } await object.save(null, { useMasterKey: true }); } return true; } async function upsertSorftimeReview(raw, asin, shopId) { const Parse = globalThis.Parse; const reviewId = String(raw?.ReviewId || raw?.Id || raw?.ID || raw?.id || "").trim(); const uniqueId = reviewId || `${asin}:${raw?.ReviewsDate || raw?.ReviewDate || raw?.Date || ""}:${raw?.ProfileName || raw?.Author || ""}`; const query = new Parse.Query("SorftimeReviews"); query.equalTo("reviewId", uniqueId); let object = null; try { object = await query.first({ useMasterKey: true }); } catch (error) { const message = String(error?.message || error || ""); if (!message.includes("does not exist") && !message.includes("non-existent class")) throw error; } if (!object) object = new Parse.Object("SorftimeReviews"); object.set("reviewId", uniqueId); object.set("asin", asin); object.set("star", Number(raw?.Star || raw?.Rating || raw?.rating || 0)); object.set("rating", Number(raw?.Star || raw?.Rating || raw?.rating || 0)); object.set("title", raw?.Title || raw?.ReviewTitle || ""); object.set("content", raw?.Content || raw?.ReviewContent || raw?.Body || ""); object.set("reviewDate", raw?.ReviewsDate || raw?.ReviewDate || raw?.Date || ""); object.set("author", raw?.ConsumerName || raw?.ProfileName || raw?.Author || ""); object.set("verified", Boolean(raw?.IsVP || raw?.OnlyPurchase || raw?.VerifiedPurchase)); object.set("shop", Parse.Object.extend("Shop").createWithoutData(shopId)); object.set("rawData", raw); await object.save(null, { useMasterKey: true }); } var SorftimeScheduler = class { // 私有属性(先声明) #isRunning = false; #lastRunTime = null; #categoryProductsCronTask = null; #keywordsCronTask = null; #marketTrendCronTask = null; #shopProductsCronTask = null; #productDetailCronTask = null; #cloudFunctionCronTask = null; #reviewsCronTask = null; #cronEnabled = true; // ========== 私有工具方法(最优先声明,避免调用时未定义) ========== /** * 延迟函数 * @private * @param {number} ms - 延迟毫秒数 * @returns {Promise} 延迟 Promise */ #delay(ms) { return new Promise((resolve) => setTimeout(resolve, ms)); } /** * 获取所有叶子节点类目 * @private * @returns {Promise} 叶子节点类目列表 */ async #getLeafCategories() { const Parse = globalThis.Parse; const query = new Parse.Query("SelfCategory"); query.equalTo("isLeaf", true); query.limit(1e4); return await query.find({ useMasterKey: true }); } /** * 根据 shopId 或 nodeIds 筛选类目 * - nodeIds 优先:直接按 nodeId 数组过滤 SelfCategory * - shopId:读取店铺的 nodeIds 字段后过滤 SelfCategory * - 两者均未传:返回全部叶子节点类目 * @private * @param {string} shopId - 可选 * @param {string[]} nodeIds - 可选 * @returns {Promise} 类目列表 */ async #getLeafCategoriesByFilter(shopId, nodeIds) { const Parse = globalThis.Parse; if (nodeIds && nodeIds.length > 0) { const query = new Parse.Query("SelfCategory"); query.containedIn("nodeId", nodeIds); query.limit(1e4); return await query.find({ useMasterKey: true }); } if (shopId) { const shopQuery = new Parse.Query("Shop"); const shop = await shopQuery.get(shopId, { useMasterKey: true }); const shopNodeIds = shop.get("nodeIds") || []; if (shopNodeIds.length > 0) { const catQuery = new Parse.Query("SelfCategory"); catQuery.containedIn("nodeId", shopNodeIds); catQuery.limit(1e4); return await catQuery.find({ useMasterKey: true }); } } return await this.#getLeafCategories(); } /** * 获取活跃的 Amazon 店铺列表 * @private * @param {string} shopId - 可选,传入则只返回该店铺 * @returns {Promise} 店铺列表 */ async #getActiveAmazonShops(shopId) { const Parse = globalThis.Parse; const query = new Parse.Query("Shop"); query.equalTo("platform", "amazon"); query.equalTo("status", "active"); if (shopId) query.equalTo("objectId", shopId); query.limit(1e3); return await query.find({ useMasterKey: true }); } /** * 记录执行日志 * @private * @param {any} logData - 日志数据对象 * @returns {Promise} */ async #logExecution(logData) { try { const Parse = globalThis.Parse; const TaskLog = Parse.Object.extend("TaskExecutionLog"); const log = new TaskLog(); log.set("taskName", logData.taskName); log.set("startTime", logData.startTime); log.set("endTime", logData.endTime); log.set("duration", logData.duration); log.set("successCount", logData.successCount); log.set("errorCount", logData.errorCount); log.set("errors", logData.errors); log.set("status", logData.status); await log.save(null, { useMasterKey: true }); console.log("[Sorftime Scheduler] \u6267\u884C\u65E5\u5FD7\u5DF2\u4FDD\u5B58"); } catch (error) { console.error("[Sorftime Scheduler] \u4FDD\u5B58\u6267\u884C\u65E5\u5FD7\u5931\u8D25:", error.message); } } /** * 采集类目热销产品(每月1号凌晨执行) * @private * @param {string} nodeId - 类目节点ID * @param {number} domain - 站点域名代码 * @returns {Promise} */ async #collectCategoryProducts(nodeId, domain, progress) { console.log(`[Sorftime Scheduler] \u5F00\u59CB\u91C7\u96C6\u7C7B\u76EE ${nodeId} \u7684\u70ED\u9500\u4EA7\u54C1 (domain: ${domain})`); const functionId = "ZmYNPsoX9X"; for (let page = 1; page <= 4; page++) { try { progress?.({ type: "info", message: `\u7C7B\u76EE ${nodeId} \u7B2C ${page}/4 \u9875\u70ED\u9500\u4EA7\u54C1\u91C7\u96C6\u4E2D...` }); const requestBody = { path: "/api/CategoryProducts", method: "POST", query: { domain }, body: { NodeId: nodeId, Page: page, Range: 400 }, functionId }; const result = await relayClient.forwardSorftime({ path: requestBody.path, method: requestBody.method, query: requestBody.query, body: requestBody.body }); if (!relaySucceeded(result)) { throw new Error(result.message || `\u7B2C${page}\u9875\u6570\u636E\u91C7\u96C6\u5931\u8D25`); } console.log(`[Sorftime Scheduler] \u7C7B\u76EE ${nodeId} \u7B2C${page}\u9875\u6570\u636E\u91C7\u96C6\u5B8C\u6210`); await this.#delay(2e3); } catch (error) { console.error(`[Sorftime Scheduler] \u7C7B\u76EE ${nodeId} \u7B2C${page}\u9875\u91C7\u96C6\u5931\u8D25:`, error.message); throw error; } } } /** * 采集类目关键词(每周一凌晨1点执行) * @private * @param {string} nodeId - 类目节点ID * @param {number} domain - 站点域名代码 * @returns {Promise} */ async #collectCategoryKeywords(nodeId, domain, progress) { console.log(`[Sorftime Scheduler] \u5F00\u59CB\u91C7\u96C6\u7C7B\u76EE ${nodeId} \u7684\u5173\u952E\u8BCD (domain: ${domain})`); const functionId = "KNN4L19aoi"; try { const requestBody = { path: "/api/CategoryRequestKeyword", method: "POST", query: { domain }, body: { Nodeid: nodeId, PageIndex: 1, PageSize: 100 }, functionId }; const result = await relayClient.forwardSorftime({ path: requestBody.path, method: requestBody.method, query: requestBody.query, body: requestBody.body }); if (!relaySucceeded(result)) { throw new Error(result.message || "\u5173\u952E\u8BCD\u91C7\u96C6\u5931\u8D25"); } console.log(`[Sorftime Scheduler] \u7C7B\u76EE ${nodeId} \u5173\u952E\u8BCD\u91C7\u96C6\u5B8C\u6210`); } catch (error) { console.error(`[Sorftime Scheduler] \u7C7B\u76EE ${nodeId} \u5173\u952E\u8BCD\u91C7\u96C6\u5931\u8D25:`, error.message); throw error; } } /** * 通过 SellerId 分页采集单个店铺的产品数据 * @private * @param {string} shopId - 店铺 Parse objectId * @param {string} sellerId - 亚马逊 SellerId * @param {number} domain - 站点代码(1=US) * @param {Function} progress - 可选进度回调 * @returns {Promise<{processed: number, pages: number}>} */ async #doCollectShopProducts(shopId, shopName, sellerId, domain, marketplaceId, progress) { const functionId = "GSjAsvw9FK"; let page = 1; let hasMore = true; let totalProcessed = 0; progress?.({ type: "info", message: `SellerId: ${sellerId}\uFF0C\u5F00\u59CB\u5206\u9875\u91C7\u96C6\u4EA7\u54C1\u6570\u636E...` }); console.log(`[Sorftime Scheduler] \u5E97\u94FA ${shopId} SellerId: ${sellerId} \u5F00\u59CB\u91C7\u96C6`); while (hasMore) { progress?.({ type: "info", message: `\u6B63\u5728\u91C7\u96C6\u7B2C ${page} \u9875...` }); console.log(`[Sorftime Scheduler] \u5E97\u94FA ${shopId} \u91C7\u96C6\u7B2C ${page} \u9875`); const result = await relayClient.forwardSorftime({ path: "/api/ProductQuery", method: "POST", query: { domain }, body: { Query: 1, QueryType: 5, Pattern: sellerId, Page: page } }); if (!relaySucceeded(result)) { throw new Error(`\u7B2C ${page} \u9875\u91C7\u96C6\u5931\u8D25: ${result.message || result.Message || JSON.stringify(result)}`); } const payload = unwrapRelayData(result); const items = payload?.Items || payload?.Products || (Array.isArray(payload) ? payload : []); const count = Array.isArray(items) ? items.length : 0; let saved = 0; for (const item of items) { if (await upsertSorftimeProduct(item, shopId, shopName, item?.MarketplaceId || marketplaceId || "", domain)) saved++; } totalProcessed += count; progress?.({ type: "info", message: `\u7B2C ${page} \u9875\u83B7\u53D6 ${count} \u6761\u4EA7\u54C1\uFF0C\u5165\u5E93 ${saved} \u6761`, processed: totalProcessed }); console.log(`[Sorftime Scheduler] \u5E97\u94FA ${shopId} \u7B2C ${page} \u9875\u83B7\u53D6 ${count} \u6761\u4EA7\u54C1\uFF0C\u5165\u5E93 ${saved} \u6761\uFF0C\u7D2F\u8BA1 ${totalProcessed} \u6761`); const pageCount = Number(payload?.PageCount || 0); if (count === 0 || pageCount > 0 && page >= pageCount || page >= 100) { hasMore = false; } else { page++; await this.#delay(2e3); } } return { processed: totalProcessed, pages: page }; } /** * 采集类目市场趋势(每月1号凌晨1点执行) * @private * @param {string} nodeId - 类目节点ID * @param {number} domain - 站点域名代码 * @returns {Promise} */ async #collectCategoryMarketTrend(nodeId, domain, progress) { console.log(`[Sorftime Scheduler] \u5F00\u59CB\u91C7\u96C6\u7C7B\u76EE ${nodeId} \u7684\u5E02\u573A\u8D8B\u52BF (domain: ${domain})`); const functionId = "OCOKi93GjS"; const trendIndexes = [0, 1, 2, 3, 4, 5]; for (const TrendIndex of trendIndexes) { try { progress?.({ type: "info", message: `\u7C7B\u76EE ${nodeId} \u8D8B\u52BF\u7C7B\u578B ${TrendIndex}/5 \u91C7\u96C6\u4E2D...` }); const requestBody = { path: "/api/CategoryTrend", method: "POST", query: { domain }, body: { NodeId: nodeId, TrendIndex }, functionId }; const result = await relayClient.forwardSorftime({ path: requestBody.path, method: requestBody.method, query: requestBody.query, body: requestBody.body }); if (!relaySucceeded(result)) { throw new Error(result.message || `\u5E02\u573A\u8D8B\u52BF\u7C7B\u578B ${TrendIndex} \u91C7\u96C6\u5931\u8D25`); } console.log(`[Sorftime Scheduler] \u7C7B\u76EE ${nodeId} \u5E02\u573A\u8D8B\u52BF\u7C7B\u578B ${TrendIndex} \u91C7\u96C6\u5B8C\u6210`); await this.#delay(1e3); } catch (error) { console.error(`[Sorftime Scheduler] \u7C7B\u76EE ${nodeId} \u5E02\u573A\u8D8B\u52BF\u7C7B\u578B ${TrendIndex} \u91C7\u96C6\u5931\u8D25:`, error.message); throw error; } } } /** * 执行类目市场趋势采集任务(每月1号凌晨1点) * @private * @returns {Promise} 执行结果对象 */ async #executeCategoryMarketTrendCollection(shopId, nodeIds, progress) { if (this.#isRunning) { console.log("[Sorftime Scheduler] \u4EFB\u52A1\u6B63\u5728\u6267\u884C\u4E2D\uFF0C\u8DF3\u8FC7\u672C\u6B21\u8C03\u5EA6"); return { success: false, message: "\u4EFB\u52A1\u6B63\u5728\u6267\u884C\u4E2D", successCount: 0, errorCount: 0, duration: 0, errors: [] }; } this.#isRunning = true; const startTime = Date.now(); const errors = []; let successCount = 0; let errorCount = 0; console.log("[Sorftime Scheduler] ========================================"); console.log("[Sorftime Scheduler] \u5F00\u59CB\u6267\u884C\u7C7B\u76EE\u5E02\u573A\u8D8B\u52BF\u91C7\u96C6\u4EFB\u52A1"); console.log("[Sorftime Scheduler] \u6267\u884C\u65F6\u95F4:", (/* @__PURE__ */ new Date()).toISOString()); console.log("[Sorftime Scheduler] ========================================"); try { const categories = await this.#getLeafCategoriesByFilter(shopId, nodeIds); if (categories.length === 0) { console.log("[Sorftime Scheduler] \u6CA1\u6709\u627E\u5230\u53F6\u5B50\u8282\u70B9\u7C7B\u76EE\u6570\u636E"); return { success: true, message: "\u6CA1\u6709\u53F6\u5B50\u8282\u70B9\u7C7B\u76EE\u9700\u8981\u5904\u7406", successCount: 0, errorCount: 0, duration: Date.now() - startTime, errors: [] }; } console.log(`[Sorftime Scheduler] \u53D1\u73B0 ${categories.length} \u4E2A\u53F6\u5B50\u8282\u70B9\u7C7B\u76EE`); for (const category of categories) { try { const nodeId = category.get("nodeId"); const categoryName = category.get("name"); const domain = category.get("domain") || 1; console.log(` [Sorftime Scheduler] ----------------------------------------`); console.log(`[Sorftime Scheduler] \u5F00\u59CB\u5904\u7406\u7C7B\u76EE\u5E02\u573A\u8D8B\u52BF: ${categoryName} (${nodeId}, domain: ${domain})`); progress?.({ type: "info", message: `\u5F00\u59CB\u91C7\u96C6\u7C7B\u76EE\u3010${categoryName}\u3011\u5E02\u573A\u8D8B\u52BF` }); await this.#collectCategoryMarketTrend(nodeId, domain, progress); successCount++; progress?.({ type: "success", message: `\u7C7B\u76EE\u3010${categoryName}\u3011\u5E02\u573A\u8D8B\u52BF\u91C7\u96C6\u5B8C\u6210`, processed: successCount }); console.log(`[Sorftime Scheduler] \u7C7B\u76EE ${categoryName} \u5E02\u573A\u8D8B\u52BF\u91C7\u96C6\u5B8C\u6210`); await this.#delay(2e3); } catch (error) { errorCount++; const errorMsg = `\u7C7B\u76EE ${category.get("name")} \u5E02\u573A\u8D8B\u52BF\u91C7\u96C6\u5931\u8D25: ${error.message}`; errors.push(errorMsg); progress?.({ type: "error", message: errorMsg }); console.error(`[Sorftime Scheduler] ${errorMsg}`); } } const duration = Date.now() - startTime; await this.#logExecution({ taskName: "sorftime-category-trend-monthly", startTime: new Date(startTime), endTime: /* @__PURE__ */ new Date(), duration, successCount, errorCount, errors, status: errorCount === 0 ? "success" : successCount > 0 ? "partial_success" : "failed" }); this.#lastRunTime = /* @__PURE__ */ new Date(); const message = `\u7C7B\u76EE\u5E02\u573A\u8D8B\u52BF\u91C7\u96C6\u5B8C\u6210: \u6210\u529F ${successCount}/${categories.length} \u4E2A\u7C7B\u76EE\uFF0C\u8017\u65F6 ${Math.round(duration / 1e3)}\u79D2`; console.log(` [Sorftime Scheduler] ========================================`); console.log(`[Sorftime Scheduler] ${message}`); console.log(`[Sorftime Scheduler] ======================================== `); return { success: errorCount === 0, message, successCount, errorCount, duration, errors }; } catch (error) { const duration = Date.now() - startTime; const errorMsg = `\u7C7B\u76EE\u5E02\u573A\u8D8B\u52BF\u91C7\u96C6\u4EFB\u52A1\u6267\u884C\u5931\u8D25: ${error.message}`; console.error(`[Sorftime Scheduler] ${errorMsg}`); return { success: false, message: errorMsg, successCount, errorCount: errorCount + 1, duration, errors: [...errors, errorMsg] }; } finally { this.#isRunning = false; } } /** * 执行类目热销产品采集任务(每月1号) * @private * @returns {Promise} 执行结果对象,包含成功数、失败数、耗时等信息 */ async #executeCategoryProductsCollection(shopId, nodeIds, progress) { if (this.#isRunning) { console.log("[Sorftime Scheduler] \u4EFB\u52A1\u6B63\u5728\u6267\u884C\u4E2D\uFF0C\u8DF3\u8FC7\u672C\u6B21\u8C03\u5EA6"); return { success: false, message: "\u4EFB\u52A1\u6B63\u5728\u6267\u884C\u4E2D", successCount: 0, errorCount: 0, duration: 0, errors: [] }; } this.#isRunning = true; const startTime = Date.now(); const errors = []; let successCount = 0; let errorCount = 0; console.log("[Sorftime Scheduler] ========================================"); console.log("[Sorftime Scheduler] \u5F00\u59CB\u6267\u884C\u7C7B\u76EE\u70ED\u9500\u4EA7\u54C1\u91C7\u96C6\u4EFB\u52A1"); console.log("[Sorftime Scheduler] \u6267\u884C\u65F6\u95F4:", (/* @__PURE__ */ new Date()).toISOString()); console.log("[Sorftime Scheduler] ========================================"); try { const categories = await this.#getLeafCategoriesByFilter(shopId, nodeIds); if (categories.length === 0) { console.log("[Sorftime Scheduler] \u6CA1\u6709\u627E\u5230\u7C7B\u76EE\u6570\u636E"); return { success: true, message: "\u6CA1\u6709\u7C7B\u76EE\u9700\u8981\u5904\u7406", successCount: 0, errorCount: 0, duration: Date.now() - startTime, errors: [] }; } console.log(`[Sorftime Scheduler] \u53D1\u73B0 ${categories.length} \u4E2A\u7C7B\u76EE`); for (const category of categories) { try { const nodeId = category.get("nodeId"); const categoryName = category.get("name"); const domain = category.get("domain") || 1; console.log(` [Sorftime Scheduler] ----------------------------------------`); console.log(`[Sorftime Scheduler] \u5F00\u59CB\u5904\u7406\u7C7B\u76EE: ${categoryName} (${nodeId}, domain: ${domain})`); progress?.({ type: "info", message: `\u5F00\u59CB\u91C7\u96C6\u7C7B\u76EE\u3010${categoryName}\u3011\u70ED\u9500\u4EA7\u54C1` }); await this.#collectCategoryProducts(nodeId, domain, progress); successCount++; progress?.({ type: "success", message: `\u7C7B\u76EE\u3010${categoryName}\u3011\u70ED\u9500\u4EA7\u54C1\u91C7\u96C6\u5B8C\u6210`, processed: successCount }); console.log(`[Sorftime Scheduler] \u7C7B\u76EE ${categoryName} \u5904\u7406\u5B8C\u6210`); await this.#delay(3e3); } catch (error) { errorCount++; const errorMsg = `\u7C7B\u76EE ${category.get("name")} \u5904\u7406\u5931\u8D25: ${error.message}`; errors.push(errorMsg); progress?.({ type: "error", message: errorMsg }); console.error(`[Sorftime Scheduler] ${errorMsg}`); } } const duration = Date.now() - startTime; await this.#logExecution({ taskName: "sorftime-category-products-monthly", startTime: new Date(startTime), endTime: /* @__PURE__ */ new Date(), duration, successCount, errorCount, errors, status: errorCount === 0 ? "success" : successCount > 0 ? "partial_success" : "failed" }); this.#lastRunTime = /* @__PURE__ */ new Date(); const message = `\u7C7B\u76EE\u70ED\u9500\u4EA7\u54C1\u91C7\u96C6\u5B8C\u6210: \u6210\u529F ${successCount}/${categories.length} \u4E2A\u7C7B\u76EE\uFF0C\u8017\u65F6 ${Math.round(duration / 1e3)}\u79D2`; console.log(` [Sorftime Scheduler] ========================================`); console.log(`[Sorftime Scheduler] ${message}`); console.log(`[Sorftime Scheduler] ======================================== `); return { success: errorCount === 0, message, successCount, errorCount, duration, errors }; } catch (error) { const duration = Date.now() - startTime; const errorMsg = `\u7C7B\u76EE\u70ED\u9500\u4EA7\u54C1\u91C7\u96C6\u4EFB\u52A1\u6267\u884C\u5931\u8D25: ${error.message}`; console.error(`[Sorftime Scheduler] ${errorMsg}`); return { success: false, message: errorMsg, successCount, errorCount: errorCount + 1, duration, errors: [...errors, errorMsg] }; } finally { this.#isRunning = false; } } /** * 执行类目关键词采集任务(每周一) * @private * @returns {Promise} 执行结果对象,包含成功数、失败数、耗时等信息 */ async #executeCategoryKeywordsCollection(shopId, nodeIds, progress) { if (this.#isRunning) { console.log("[Sorftime Scheduler] \u4EFB\u52A1\u6B63\u5728\u6267\u884C\u4E2D\uFF0C\u8DF3\u8FC7\u672C\u6B21\u8C03\u5EA6"); return { success: false, message: "\u4EFB\u52A1\u6B63\u5728\u6267\u884C\u4E2D", successCount: 0, errorCount: 0, duration: 0, errors: [] }; } this.#isRunning = true; const startTime = Date.now(); const errors = []; let successCount = 0; let errorCount = 0; console.log("[Sorftime Scheduler] ========================================"); console.log("[Sorftime Scheduler] \u5F00\u59CB\u6267\u884C\u7C7B\u76EE\u5173\u952E\u8BCD\u91C7\u96C6\u4EFB\u52A1"); console.log("[Sorftime Scheduler] \u6267\u884C\u65F6\u95F4:", (/* @__PURE__ */ new Date()).toISOString()); console.log("[Sorftime Scheduler] ========================================"); try { const categories = await this.#getLeafCategoriesByFilter(shopId, nodeIds); if (categories.length === 0) { console.log("[Sorftime Scheduler] \u6CA1\u6709\u627E\u5230\u53F6\u5B50\u8282\u70B9\u7C7B\u76EE\u6570\u636E"); return { success: true, message: "\u6CA1\u6709\u53F6\u5B50\u8282\u70B9\u7C7B\u76EE\u9700\u8981\u5904\u7406", successCount: 0, errorCount: 0, duration: Date.now() - startTime, errors: [] }; } console.log(`[Sorftime Scheduler] \u53D1\u73B0 ${categories.length} \u4E2A\u53F6\u5B50\u8282\u70B9\u7C7B\u76EE`); for (const category of categories) { try { const nodeId = category.get("nodeId"); const categoryName = category.get("name"); const domain = category.get("domain") || 1; console.log(` [Sorftime Scheduler] ----------------------------------------`); console.log(`[Sorftime Scheduler] \u5F00\u59CB\u5904\u7406\u7C7B\u76EE: ${categoryName} (${nodeId}, domain: ${domain})`); progress?.({ type: "info", message: `\u5F00\u59CB\u91C7\u96C6\u7C7B\u76EE\u3010${categoryName}\u3011\u5173\u952E\u8BCD` }); await this.#collectCategoryKeywords(nodeId, domain, progress); successCount++; progress?.({ type: "success", message: `\u7C7B\u76EE\u3010${categoryName}\u3011\u5173\u952E\u8BCD\u91C7\u96C6\u5B8C\u6210`, processed: successCount }); console.log(`[Sorftime Scheduler] \u7C7B\u76EE ${categoryName} \u5173\u952E\u8BCD\u91C7\u96C6\u5B8C\u6210`); await this.#delay(2e3); } catch (error) { errorCount++; const errorMsg = `\u7C7B\u76EE ${category.get("name")} \u5173\u952E\u8BCD\u91C7\u96C6\u5931\u8D25: ${error.message}`; errors.push(errorMsg); progress?.({ type: "error", message: errorMsg }); console.error(`[Sorftime Scheduler] ${errorMsg}`); } } const duration = Date.now() - startTime; await this.#logExecution({ taskName: "sorftime-category-keywords-weekly", startTime: new Date(startTime), endTime: /* @__PURE__ */ new Date(), duration, successCount, errorCount, errors, status: errorCount === 0 ? "success" : successCount > 0 ? "partial_success" : "failed" }); this.#lastRunTime = /* @__PURE__ */ new Date(); const message = `\u7C7B\u76EE\u5173\u952E\u8BCD\u91C7\u96C6\u5B8C\u6210: \u6210\u529F ${successCount}/${categories.length} \u4E2A\u7C7B\u76EE\uFF0C\u8017\u65F6 ${Math.round(duration / 1e3)}\u79D2`; console.log(` [Sorftime Scheduler] ========================================`); console.log(`[Sorftime Scheduler] ${message}`); console.log(`[Sorftime Scheduler] ======================================== `); return { success: errorCount === 0, message, successCount, errorCount, duration, errors }; } catch (error) { const duration = Date.now() - startTime; const errorMsg = `\u7C7B\u76EE\u5173\u952E\u8BCD\u91C7\u96C6\u4EFB\u52A1\u6267\u884C\u5931\u8D25: ${error.message}`; console.error(`[Sorftime Scheduler] ${errorMsg}`); return { success: false, message: errorMsg, successCount, errorCount: errorCount + 1, duration, errors: [...errors, errorMsg] }; } finally { this.#isRunning = false; } } /** * 执行云函数调用任务(每两小时) * 调用指定的云函数进行数据处理 * @private * @returns {Promise} 执行结果对象 */ async #executeCloudFunctionInvocation() { console.log("[Sorftime Scheduler] ========================================"); console.log("[Sorftime Scheduler] \u5F00\u59CB\u6267\u884C\u4E91\u51FD\u6570\u8C03\u7528\u4EFB\u52A1"); console.log("[Sorftime Scheduler] \u6267\u884C\u65F6\u95F4:", (/* @__PURE__ */ new Date()).toISOString()); console.log("[Sorftime Scheduler] ========================================"); const startTime = Date.now(); try { const response = await fetch("http://localhost:3000/api/functions", { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ id: "Z4z6SB4o9e" }) }); const duration = Date.now() - startTime; if (!response.ok) { const errorMsg = `\u4E91\u51FD\u6570\u8C03\u7528\u5931\u8D25\uFF0C\u72B6\u6001\u7801: ${response.status}`; console.error(`[Sorftime Scheduler] ${errorMsg}`); return { success: false, message: errorMsg, duration, errors: [errorMsg] }; } const result = await response.json(); const message = `\u4E91\u51FD\u6570\u8C03\u7528\u5B8C\u6210\uFF0C\u8017\u65F6 ${Math.round(duration / 1e3)}\u79D2`; console.log(` [Sorftime Scheduler] ========================================`); console.log(`[Sorftime Scheduler] ${message}`); console.log(`[Sorftime Scheduler] ======================================== `); return { success: true, message, duration, data: result, errors: [] }; } catch (error) { const duration = Date.now() - startTime; const errorMsg = `\u4E91\u51FD\u6570\u8C03\u7528\u4EFB\u52A1\u6267\u884C\u5931\u8D25: ${error.message}`; console.error(`[Sorftime Scheduler] ${errorMsg}`); return { success: false, message: errorMsg, duration, errors: [errorMsg] }; } } /** * 执行产品评论采集任务(每天凌晨1点) * 根据 Product 表中的 ASIN,采集对应的评论信息 * @private * @returns {Promise} 执行结果对象 */ async #executeProductReviewsCollection(progress) { console.log("[Sorftime Scheduler] ========================================"); console.log("[Sorftime Scheduler] \u5F00\u59CB\u6267\u884C\u4EA7\u54C1\u8BC4\u8BBA\u91C7\u96C6\u4EFB\u52A1"); console.log("[Sorftime Scheduler] \u6267\u884C\u65F6\u95F4:", (/* @__PURE__ */ new Date()).toISOString()); console.log("[Sorftime Scheduler] ========================================"); const startTime = Date.now(); let successCount = 0; let errorCount = 0; const errors = []; progress?.({ type: "info", message: "\u5F00\u59CB\u6267\u884C\u4EA7\u54C1\u8BC4\u8BBA\u91C7\u96C6\u4EFB\u52A1" }); try { const Parse = globalThis.Parse; const productQuery = new Parse.Query("Product"); productQuery.limit(1e4); const products = await productQuery.find({ useMasterKey: true }); if (products.length === 0) { console.log("[Sorftime Scheduler] \u6CA1\u6709\u627E\u5230 Product \u6570\u636E"); progress?.({ type: "info", message: "\u6CA1\u6709 Product \u6570\u636E\u9700\u8981\u5904\u7406" }); return { success: true, message: "\u6CA1\u6709 Product \u6570\u636E\u9700\u8981\u5904\u7406", successCount: 0, errorCount: 0, duration: Date.now() - startTime, errors: [] }; } console.log(`[Sorftime Scheduler] \u53D1\u73B0 ${products.length} \u4E2A Product \u8BB0\u5F55`); progress?.({ type: "info", message: `\u5171\u53D1\u73B0 ${products.length} \u4E2A\u4EA7\u54C1\u9700\u8981\u68C0\u67E5\u8BC4\u8BBA` }); for (const product of products) { try { const asin = product.get("asin"); if (!asin) { console.log("[Sorftime Scheduler] \u8DF3\u8FC7\u65E0 ASIN \u7684 Product"); continue; } progress?.({ type: "info", message: `\u5F00\u59CB\u5904\u7406 ASIN: ${asin}` }); const shopRelation = product.get("shop"); if (!shopRelation) { console.log(`[Sorftime Scheduler] Product \u65E0\u5173\u8054 Shop\uFF0CASIN: ${asin}`); continue; } const shop = await shopRelation.fetch({ useMasterKey: true }); const shopDomain = shop.get("domain") || 1; let queryStartDt = "2025-01-01"; const reviewQuery = new Parse.Query("SorftimeReviews"); reviewQuery.equalTo("asin", asin); reviewQuery.descending("updatedAt"); reviewQuery.limit(1); let latestReview = null; try { latestReview = await reviewQuery.first({ useMasterKey: true }); } catch (error) { const message2 = String(error?.message || error || ""); if (!message2.includes("does not exist") && !message2.includes("non-existent class")) throw error; } if (latestReview) { const updateAt = latestReview.get("updatedAt"); if (updateAt) { const date = new Date(updateAt); queryStartDt = date.toISOString().split("T")[0]; } } console.log(`[Sorftime Scheduler] \u5F00\u59CB\u91C7\u96C6\u8BC4\u8BBA\uFF0CASIN: ${asin}, \u67E5\u8BE2\u8D77\u59CB\u65E5\u671F: ${queryStartDt}`); let result; try { result = await relayClient.forwardSorftime({ path: "/api/ProductReviewsQuery", method: "POST", query: { domain: shopDomain, shopId: shop.id }, body: { ASIN: asin, PageIndex: 1, OnlyPurchase: 1, Star: "1,2,3,4,5", QueryStartDt: queryStartDt } }); } catch (error) { const errorMsg = `\u8BF7\u6C42\u8BC4\u8BBA\u6570\u636E\u5931\u8D25\uFF0CASIN: ${asin}, ${error.message || error}`; errors.push(errorMsg); errorCount++; console.error(`[Sorftime Scheduler] ${errorMsg}`); continue; } const payload = unwrapRelayData(result); const reviews = payload?.Reviews || payload?.Items || payload?.List || (Array.isArray(payload) ? payload : []); if (relaySucceeded(result)) { for (const review of reviews) await upsertSorftimeReview(review, asin, shop.id); successCount++; const statusText = reviews.length ? `\u5165\u5E93 ${reviews.length} \u6761` : "\u65E0\u65B0\u589E\u8BC4\u8BBA"; console.log(`[Sorftime Scheduler] \u8BC4\u8BBA\u6570\u636E\u91C7\u96C6\u6210\u529F\uFF0CASIN: ${asin}\uFF0C${statusText}`); progress?.({ type: "success", message: `\u8BC4\u8BBA\u6570\u636E\u91C7\u96C6\u6210\u529F\uFF0CASIN: ${asin}\uFF0C${statusText}`, processed: successCount }); } else { const errorMsg = `\u8BC4\u8BBA\u6570\u636E\u83B7\u53D6\u5931\u8D25\uFF0CASIN: ${asin}`; errors.push(errorMsg); errorCount++; console.error(`[Sorftime Scheduler] ${errorMsg}`); progress?.({ type: "error", message: errorMsg }); } await this.#delay(500); } catch (error) { errorCount++; const errorMsg = `\u5904\u7406 Product \u5931\u8D25: ${error.message}`; errors.push(errorMsg); console.error(`[Sorftime Scheduler] ${errorMsg}`); progress?.({ type: "error", message: errorMsg }); } } const duration = Date.now() - startTime; const message = `\u4EA7\u54C1\u8BC4\u8BBA\u91C7\u96C6\u5B8C\u6210: \u6210\u529F ${successCount}/${products.length} \u4E2A\u4EA7\u54C1\uFF0C\u8017\u65F6 ${Math.round(duration / 1e3)}\u79D2`; console.log(` [Sorftime Scheduler] ========================================`); console.log(`[Sorftime Scheduler] ${message}`); console.log(`[Sorftime Scheduler] ======================================== `); progress?.({ type: errorCount === 0 ? "success" : "warning", message }); return { success: errorCount === 0, message, successCount, errorCount, duration, errors }; } catch (error) { const duration = Date.now() - startTime; const errorMsg = `\u4EA7\u54C1\u8BC4\u8BBA\u91C7\u96C6\u4EFB\u52A1\u6267\u884C\u5931\u8D25: ${error.message}`; console.error(`[Sorftime Scheduler] ${errorMsg}`); progress?.({ type: "error", message: errorMsg }); return { success: false, message: errorMsg, successCount: 0, errorCount: 1, duration, errors: [errorMsg] }; } } // ========== 公共方法(后声明,因为依赖前面的私有方法) ========== /** * 构造函数 * 初始化 Sorftime API 数据采集调度器 */ constructor() { console.log("[Sorftime Scheduler] \u521D\u59CB\u5316 Sorftime API \u6570\u636E\u91C7\u96C6\u8C03\u5EA6\u5668"); } /** * 启动定时任务 * @returns {Promise} */ async start() { if (this.#categoryProductsCronTask || this.#keywordsCronTask || this.#marketTrendCronTask || this.#shopProductsCronTask || this.#productDetailCronTask || this.#cloudFunctionCronTask || this.#reviewsCronTask) { console.log("[Sorftime Scheduler] \u5B9A\u65F6\u4EFB\u52A1\u5DF2\u5728\u8FD0\u884C\u4E2D"); return; } this.#categoryProductsCronTask = nodeCron3.schedule("0 0 1 * *", async () => { await this.#executeCategoryProductsCollection(void 0, void 0, void 0); }, { timezone: "Asia/Shanghai" }); console.log("[Sorftime Scheduler] \u7C7B\u76EE\u70ED\u9500\u4EA7\u54C1\u5B9A\u65F6\u4EFB\u52A1\u5DF2\u542F\u52A8\uFF0C\u5C06\u5728\u6BCF\u67081\u53F7\u51CC\u6668\u6267\u884C"); this.#marketTrendCronTask = nodeCron3.schedule("0 1 1 * *", async () => { await this.#executeCategoryMarketTrendCollection(void 0, void 0, void 0); }, { timezone: "Asia/Shanghai" }); console.log("[Sorftime Scheduler] \u7C7B\u76EE\u5E02\u573A\u8D8B\u52BF\u5B9A\u65F6\u4EFB\u52A1\u5DF2\u542F\u52A8\uFF0C\u5C06\u5728\u6BCF\u67081\u53F7\u51CC\u66681\u70B9\u6267\u884C"); this.#keywordsCronTask = nodeCron3.schedule("0 1 * * 1", async () => { await this.#executeCategoryKeywordsCollection(void 0, void 0, void 0); }, { timezone: "Asia/Shanghai" }); console.log("[Sorftime Scheduler] \u7C7B\u76EE\u5173\u952E\u8BCD\u5B9A\u65F6\u4EFB\u52A1\u5DF2\u542F\u52A8\uFF0C\u5C06\u5728\u6BCF\u5468\u4E00\u51CC\u66681\u70B9\u6267\u884C"); if (process.env.ENABLE_SORFTIME_REVIEW_SCHEDULES === "true") { this.#reviewsCronTask = nodeCron3.schedule("0 1 * * *", async () => { console.log("[Sorftime Scheduler] ===== \u51CC\u66681\u70B9\uFF1A\u5F00\u59CB\u4EA7\u54C1\u8BC4\u8BBA\u91C7\u96C6 ====="); try { await this.#executeProductReviewsCollection(void 0); } catch (e) { console.error("[Sorftime Scheduler] \u4EA7\u54C1\u8BC4\u8BBA\u91C7\u96C6\u5931\u8D25:", e.message); } console.log("[Sorftime Scheduler] ===== \u4EA7\u54C1\u8BC4\u8BBA\u91C7\u96C6\u5B8C\u6210 ====="); }, { timezone: "Asia/Shanghai" }); console.log("[Sorftime Scheduler] \u4EA7\u54C1\u8BC4\u8BBA\u91C7\u96C6\u5B9A\u65F6\u4EFB\u52A1\u5DF2\u542F\u52A8\uFF0C\u5C06\u5728\u6BCF\u5929\u51CC\u66681\u70B9\u6267\u884C"); } else { console.log("[Sorftime Scheduler] \u4EA7\u54C1\u8BC4\u8BBA\u91C7\u96C6\u5B9A\u65F6\u4EFB\u52A1\u5DF2\u7981\u7528"); } this.#shopProductsCronTask = nodeCron3.schedule("0 2 * * *", async () => { console.log("[Sorftime Scheduler] ===== \u51CC\u66682\u70B9\uFF1A\u5F00\u59CB\u5E97\u94FA\u4EA7\u54C1\u91C7\u96C6 ====="); try { await this.collectShopProducts(void 0, void 0); } catch (e) { console.error("[Sorftime Scheduler] \u5E97\u94FA\u4EA7\u54C1\u91C7\u96C6\u5931\u8D25:", e.message); } console.log("[Sorftime Scheduler] ===== \u5E97\u94FA\u4EA7\u54C1\u91C7\u96C6\u5B8C\u6210 ====="); }, { timezone: "Asia/Shanghai" }); console.log("[Sorftime Scheduler] \u5E97\u94FA\u4EA7\u54C1\u91C7\u96C6\u5B9A\u65F6\u4EFB\u52A1\u5DF2\u542F\u52A8\uFF0C\u5C06\u5728\u6BCF\u5929\u51CC\u66682\u70B9\u6267\u884C"); if (process.env.ENABLE_LEGACY_CLOUD_FUNCTION_SCHEDULES === "true") { this.#cloudFunctionCronTask = nodeCron3.schedule("0 */2 * * *", async () => { try { await this.#executeCloudFunctionInvocation(); } catch (e) { console.error("[Sorftime Scheduler] \u9057\u7559\u4E91\u51FD\u6570\u8C03\u7528\u5931\u8D25:", e.message); } }, { timezone: "Asia/Shanghai" }); } else { console.log("[Sorftime Scheduler] \u9057\u7559\u52A8\u6001\u4E91\u51FD\u6570\u5B9A\u65F6\u4EFB\u52A1\u5DF2\u7981\u7528"); } } /** * 停止定时任务 * @returns {void} */ stop() { if (this.#categoryProductsCronTask) { this.#categoryProductsCronTask.stop(); this.#categoryProductsCronTask = null; console.log("[Sorftime Scheduler] \u7C7B\u76EE\u70ED\u9500\u4EA7\u54C1\u5B9A\u65F6\u4EFB\u52A1\u5DF2\u505C\u6B62"); } if (this.#marketTrendCronTask) { this.#marketTrendCronTask.stop(); this.#marketTrendCronTask = null; console.log("[Sorftime Scheduler] \u7C7B\u76EE\u5E02\u573A\u8D8B\u52BF\u5B9A\u65F6\u4EFB\u52A1\u5DF2\u505C\u6B62"); } if (this.#keywordsCronTask) { this.#keywordsCronTask.stop(); this.#keywordsCronTask = null; console.log("[Sorftime Scheduler] \u7C7B\u76EE\u5173\u952E\u8BCD\u5B9A\u65F6\u4EFB\u52A1\u5DF2\u505C\u6B62"); } if (this.#shopProductsCronTask) { this.#shopProductsCronTask.stop(); this.#shopProductsCronTask = null; console.log("[Sorftime Scheduler] \u5E97\u94FA\u4EA7\u54C1\u91C7\u96C6\u5B9A\u65F6\u4EFB\u52A1\u5DF2\u505C\u6B62"); } if (this.#productDetailCronTask) { this.#productDetailCronTask.stop(); this.#productDetailCronTask = null; console.log("[Sorftime Scheduler] \u4EA7\u54C1\u8BE6\u60C5\u540C\u6B65\u5B9A\u65F6\u4EFB\u52A1\u5DF2\u505C\u6B62"); } if (this.#cloudFunctionCronTask) { this.#cloudFunctionCronTask.stop(); this.#cloudFunctionCronTask = null; console.log("[Sorftime Scheduler] \u4E91\u51FD\u6570\u8C03\u7528\u5B9A\u65F6\u4EFB\u52A1\u5DF2\u505C\u6B62"); } if (this.#reviewsCronTask) { this.#reviewsCronTask.stop(); this.#reviewsCronTask = null; console.log("[Sorftime Scheduler] \u4EA7\u54C1\u8BC4\u8BBA\u91C7\u96C6\u5B9A\u65F6\u4EFB\u52A1\u5DF2\u505C\u6B62"); } } /** * 获取调度器状态 * @returns {object} 调度器状态对象 */ getStatus() { return { isRunning: this.#isRunning, lastRunTime: this.#lastRunTime, cronEnabled: this.#cronEnabled, tasks: { categoryProducts: { enabled: !!this.#categoryProductsCronTask, schedule: "\u6BCF\u67081\u53F7\u51CC\u6668" }, marketTrend: { enabled: !!this.#marketTrendCronTask, schedule: "\u6BCF\u67081\u53F7\u51CC\u66681\u70B9" }, keywords: { enabled: !!this.#keywordsCronTask, schedule: "\u6BCF\u5468\u4E00\u51CC\u66681\u70B9" }, shopProducts: { enabled: !!this.#shopProductsCronTask, schedule: "\u6BCF\u5929\u51CC\u66682\u70B9" }, productDetails: { enabled: !!this.#productDetailCronTask, schedule: "\u6BCF\u5C0F\u65F6" }, cloudFunction: { enabled: !!this.#cloudFunctionCronTask, schedule: "\u6BCF\u4E24\u5C0F\u65F6" }, productReviews: { enabled: !!this.#reviewsCronTask, schedule: "\u6BCF\u5929\u51CC\u66681\u70B9" } } }; } /** * 手动触发类目热销产品采集 * @returns {Promise} 执行结果对象 */ async triggerCategoryProducts(shopId, nodeIds, progress) { console.log("[Sorftime Scheduler] \u624B\u52A8\u89E6\u53D1\u7C7B\u76EE\u70ED\u9500\u4EA7\u54C1\u91C7\u96C6\u4EFB\u52A1"); return await this.#executeCategoryProductsCollection(shopId, nodeIds, progress); } /** * 手动触发类目关键词采集 * @returns {Promise} 执行结果对象 */ async triggerCategoryKeywords(shopId, nodeIds, progress) { console.log("[Sorftime Scheduler] \u624B\u52A8\u89E6\u53D1\u7C7B\u76EE\u5173\u952E\u8BCD\u91C7\u96C6\u4EFB\u52A1"); return await this.#executeCategoryKeywordsCollection(shopId, nodeIds, progress); } /** * 手动触发类目市场趋势采集 * @returns {Promise} 执行结果对象 */ async triggerCategoryTrends(shopId, nodeIds, progress) { console.log("[Sorftime Scheduler] \u624B\u52A8\u89E6\u53D1\u7C7B\u76EE\u5E02\u573A\u8D8B\u52BF\u91C7\u96C6\u4EFB\u52A1"); return await this.#executeCategoryMarketTrendCollection(shopId, nodeIds, progress); } /** * 手动触发产品评论采集 * @returns {Promise} 执行结果对象 */ async triggerProductReviews(progress) { console.log("[Sorftime Scheduler] \u624B\u52A8\u89E6\u53D1\u4EA7\u54C1\u8BC4\u8BBA\u91C7\u96C6\u4EFB\u52A1"); return await this.#executeProductReviewsCollection(progress); } /** * 按需触发单个类目的全量数据采集(热销产品 + 关键词 + 市场趋势) * 前端选中某个类目但数据库无数据时调用 * @param {string} nodeId - 类目节点ID * @param {number} domain - 站点域名代码(默认1=美国) * @returns {Promise} 执行结果对象 */ async triggerSingleCategory(nodeId, domain = 1, progress) { const startTime = Date.now(); const errors = []; let tasks = { products: false, keywords: false, trends: false }; console.log(`[Sorftime Scheduler] \u6309\u9700\u89E6\u53D1\u5355\u7C7B\u76EE\u91C7\u96C6: nodeId=${nodeId}, domain=${domain}`); try { try { progress?.({ type: "info", message: `\u5F00\u59CB\u91C7\u96C6\u7C7B\u76EE ${nodeId} \u70ED\u9500\u4EA7\u54C1` }); await this.#collectCategoryProducts(nodeId, domain, progress); tasks.products = true; progress?.({ type: "success", message: `\u7C7B\u76EE ${nodeId} \u70ED\u9500\u4EA7\u54C1\u91C7\u96C6\u5B8C\u6210` }); console.log(`[Sorftime Scheduler] \u5355\u7C7B\u76EE ${nodeId} \u70ED\u9500\u4EA7\u54C1\u91C7\u96C6\u5B8C\u6210`); } catch (error) { const errMsg = error?.message || String(error); progress?.({ type: "error", message: `\u70ED\u9500\u4EA7\u54C1\u91C7\u96C6\u5931\u8D25: ${errMsg}` }); errors.push(`\u70ED\u9500\u4EA7\u54C1\u91C7\u96C6\u5931\u8D25: ${errMsg}`); console.error(`[Sorftime Scheduler] \u70ED\u9500\u4EA7\u54C1\u91C7\u96C6\u5931\u8D25:`, error); } await new Promise((resolve) => setTimeout(resolve, 500)); try { progress?.({ type: "info", message: `\u5F00\u59CB\u91C7\u96C6\u7C7B\u76EE ${nodeId} \u5173\u952E\u8BCD` }); await this.#collectCategoryKeywords(nodeId, domain, progress); tasks.keywords = true; progress?.({ type: "success", message: `\u7C7B\u76EE ${nodeId} \u5173\u952E\u8BCD\u91C7\u96C6\u5B8C\u6210` }); console.log(`[Sorftime Scheduler] \u5355\u7C7B\u76EE ${nodeId} \u5173\u952E\u8BCD\u91C7\u96C6\u5B8C\u6210`); } catch (error) { const errMsg = error?.message || String(error); progress?.({ type: "error", message: `\u5173\u952E\u8BCD\u91C7\u96C6\u5931\u8D25: ${errMsg}` }); errors.push(`\u5173\u952E\u8BCD\u91C7\u96C6\u5931\u8D25: ${errMsg}`); console.error(`[Sorftime Scheduler] \u5173\u952E\u8BCD\u91C7\u96C6\u5931\u8D25:`, error); } await new Promise((resolve) => setTimeout(resolve, 500)); try { progress?.({ type: "info", message: `\u5F00\u59CB\u91C7\u96C6\u7C7B\u76EE ${nodeId} \u5E02\u573A\u8D8B\u52BF` }); await this.#collectCategoryMarketTrend(nodeId, domain, progress); tasks.trends = true; progress?.({ type: "success", message: `\u7C7B\u76EE ${nodeId} \u5E02\u573A\u8D8B\u52BF\u91C7\u96C6\u5B8C\u6210` }); console.log(`[Sorftime Scheduler] \u5355\u7C7B\u76EE ${nodeId} \u5E02\u573A\u8D8B\u52BF\u91C7\u96C6\u5B8C\u6210`); } catch (error) { const errMsg = error?.message || String(error); progress?.({ type: "error", message: `\u5E02\u573A\u8D8B\u52BF\u91C7\u96C6\u5931\u8D25: ${errMsg}` }); errors.push(`\u5E02\u573A\u8D8B\u52BF\u91C7\u96C6\u5931\u8D25: ${errMsg}`); console.error(`[Sorftime Scheduler] \u5E02\u573A\u8D8B\u52BF\u91C7\u96C6\u5931\u8D25:`, error); } const duration = Date.now() - startTime; const allSuccess = errors.length === 0; const successCount = Object.values(tasks).filter(Boolean).length; if (this.#logExecution) { this.#logExecution({ taskName: `trigger-single-category-${nodeId}`, startTime: new Date(startTime), endTime: /* @__PURE__ */ new Date(), duration, successCount, errorCount: errors.length, errors, status: allSuccess ? "success" : "partial_success" }).catch((err) => console.error("[Sorftime Scheduler] Log execution error:", err)); } const result = { success: allSuccess, message: allSuccess ? `\u7C7B\u76EE ${nodeId} \u5168\u91CF\u91C7\u96C6\u5B8C\u6210\uFF0C\u8017\u65F6 ${Math.round(duration / 1e3)}\u79D2` : `\u7C7B\u76EE ${nodeId} \u90E8\u5206\u91C7\u96C6\u5931\u8D25\uFF08\u6210\u529F ${successCount}/3 \u4E2A\u4EFB\u52A1\uFF09`, nodeId, domain, tasks, duration, errors: errors.length > 0 ? errors : void 0 }; console.log(`[Sorftime Scheduler] \u91C7\u96C6\u7ED3\u679C:`, result); return result; } catch (err) { const duration = Date.now() - startTime; const errMsg = err?.message || String(err); console.error(`[Sorftime Scheduler] \u91C7\u96C6\u5F02\u5E38:`, err); return { success: false, message: `\u7C7B\u76EE ${nodeId} \u91C7\u96C6\u5F02\u5E38: ${errMsg}`, nodeId, domain, tasks, duration, errors: [errMsg] }; } } /** * 采集所有活跃 Amazon 店铺(或指定店铺)的产品数据 * 通过 SellerId (QueryType=5) 分页请求 /api/ProductQuery * @param {string} shopId - 可选,不传则采集全部活跃 Amazon 店铺 * @param {Function} progress - 可选进度回调,适合 SSE 实时推送 * @returns {Promise} 执行结果 */ async collectShopProducts(shopId, progress) { const shops = await this.#getActiveAmazonShops(shopId); if (shops.length === 0) { const msg = shopId ? `\u672A\u627E\u5230\u5E97\u94FA ${shopId} \u6216\u5E97\u94FA\u672A\u6FC0\u6D3B` : "\u6CA1\u6709\u627E\u5230\u6D3B\u8DC3\u7684 Amazon \u5E97\u94FA"; progress?.({ type: "warn", message: msg }); console.warn(`[Sorftime Scheduler] ${msg}`); return { success: true, message: msg, processed: 0, errorCount: 0, errors: [] }; } let totalProcessed = 0; let errorCount = 0; const errors = []; for (const shop of shops) { const sid = shop.id; const shopName = shop.get("name"); const config = shop.get("config"); const domain = Number(shop.get("domain") || 1); const marketplaceId = shop.get("marketplaceId") || ""; if (!config || !config.SpApiConfig || !config.SpApiConfig.sellerID) { const msg = `\u5E97\u94FA\u3010${shopName}\u3011(${sid}) \u7F3A\u5C11 SpApiConfig.sellerID \u914D\u7F6E\uFF0C\u8DF3\u8FC7`; progress?.({ type: "error", message: msg }); console.error(`[Sorftime Scheduler] ${msg}`); errors.push(msg); errorCount++; continue; } if (config.SpApiConfig.listingEnabled === false) { progress?.({ type: "warn", message: `\u5E97\u94FA\u3010${shopName}\u3011\u672A\u914D\u7F6E\u533A\u57DF Seller ID\uFF0C\u8DF3\u8FC7 Sorftime \u5E97\u94FA\u4EA7\u54C1\u91C7\u96C6` }); continue; } const sellerId = config.SpApiConfig.sellerID; progress?.({ type: "info", message: `\u5F00\u59CB\u91C7\u96C6\u5E97\u94FA\u3010${shopName}\u3011\u7684\u4EA7\u54C1\u6570\u636E (SellerId: ${sellerId})` }); console.log(`[Sorftime Scheduler] \u5F00\u59CB\u91C7\u96C6\u5E97\u94FA ${shopName} (${sid}) \u4EA7\u54C1\u6570\u636E`); try { const result = await this.#doCollectShopProducts(sid, shopName, sellerId, domain, marketplaceId, progress); progress?.({ type: "success", message: `\u5E97\u94FA\u3010${shopName}\u3011\u4EA7\u54C1\u91C7\u96C6\u5B8C\u6210: \u5171 ${result.pages} \u9875 ${result.processed} \u6761`, processed: result.processed }); console.log(`[Sorftime Scheduler] \u5E97\u94FA ${shopName} \u4EA7\u54C1\u91C7\u96C6\u5B8C\u6210: ${result.processed} \u6761`); totalProcessed += result.processed; await this.#delay(2e3); } catch (error) { const msg = `\u5E97\u94FA\u3010${shopName}\u3011\u4EA7\u54C1\u91C7\u96C6\u5931\u8D25: ${error.message}`; progress?.({ type: "error", message: msg }); console.error(`[Sorftime Scheduler] ${msg}`); errors.push(msg); errorCount++; } } const message = `\u5E97\u94FA\u4EA7\u54C1\u91C7\u96C6\u5B8C\u6210: \u6210\u529F ${shops.length - errorCount}/${shops.length} \u4E2A\u5E97\u94FA\uFF0C\u5171 ${totalProcessed} \u6761\u8BB0\u5F55`; return { success: errorCount === 0, message, processed: totalProcessed, errorCount, errors }; } }; var sorftimeScheduler = new SorftimeScheduler(); // backend/api/module/schedule/routes-schedule.ts (async () => { try { await spApiScheduler.start(); console.log("[API Routes] Amazon SP-API\u5B9A\u65F6\u4EFB\u52A1\u5DF2\u542F\u52A8"); } catch (error) { console.error("[API Routes] Amazon SP-API\u5B9A\u65F6\u4EFB\u52A1\u542F\u52A8\u5931\u8D25:", error); } if (process.env.ENABLE_SORFTIME_SCHEDULES === "true") { try { await sorftimeScheduler.start(); console.log("[API Routes] Sorftime\u5B9A\u65F6\u4EFB\u52A1\u5DF2\u542F\u52A8"); } catch (error) { console.error("[API Routes] Sorftime\u5B9A\u65F6\u4EFB\u52A1\u542F\u52A8\u5931\u8D25:", error); } } else { console.log("[API Routes] Sorftime\u5B9A\u65F6\u4EFB\u52A1\u5DF2\u7981\u7528"); } })(); var router3 = express3.Router(); console.log("\u52A0\u8F7Dschedule\u8DEF\u7531 /api/schedule/"); router3.use(express3.json({ charset: "utf-8", // 额外配置:确保解析URL编码的请求体也支持中文 type: "application/json" })); router3.get("/sp-api-schedule/status", (req, res) => { res.json({ success: true, data: { spApi: spApiScheduler.getStatus() } }); }); router3.get("/sp-api-schedule/start", async (req, res) => { console.log("[API Routes] \u542F\u52A8\u5B9A\u65F6\u4EFB\u52A1"); try { const result = await spApiScheduler.start(); res.json({ success: true, message: "\u5B9A\u65F6\u5668\u6267\u884C\u9648\u529F", data: result }); } catch (error) { res.status(500).json({ success: false, message: error.message }); } }); router3.post("/sp-api-schedule/trigger", async (req, res) => { try { const shopId = req.body.shopId; const result = await spApiScheduler.triggerManually(shopId); res.json({ success: result.success, message: result.message, data: result }); } catch (error) { res.status(500).json({ success: false, message: error.message }); } }); function createSseResponse(res) { res.setHeader("Content-Type", "text/event-stream; charset=utf-8"); res.setHeader("Cache-Control", "no-cache"); res.setHeader("Connection", "keep-alive"); res.setHeader("X-Accel-Buffering", "no"); res.setHeader("Access-Control-Allow-Origin", "*"); res.setHeader("Access-Control-Allow-Methods", "GET, POST, OPTIONS"); res.setHeader("Access-Control-Allow-Headers", "Content-Type"); res.flushHeaders(); const send = (event, data) => { if (!res.writableEnded) { res.write(`event: ${event} data: ${JSON.stringify(data)} `); } }; const heartbeatTimer = setInterval(() => { if (!res.writableEnded) res.write(": heartbeat\n\n"); }, 15e3); const close = () => { clearInterval(heartbeatTimer); if (!res.writableEnded) res.end(); }; return { send, close }; } router3.post("/sp-api-schedule/collect-listing", async (req, res) => { const { shopId } = req.body || {}; const { send, close } = createSseResponse(res); req.on("aborted", () => close()); try { send("progress", { type: "info", message: `\u5F00\u59CB\u91C7\u96C6 Listing \u6570\u636E${shopId ? "\uFF0C\u5E97\u94FA: " + shopId : "\uFF08\u5168\u90E8\u6D3B\u8DC3\u5E97\u94FA\uFF09"}` }); const result = await newSpApiScheduler.collectListings( shopId || void 0, (event) => send("progress", event) ); send("done", { success: result.success, message: result.message, processed: result.processed, errorCount: result.errorCount, errors: result.errors }); } catch (error) { send("error", { success: false, message: error.message || "\u91C7\u96C6 Listing \u6570\u636E\u65F6\u53D1\u751F\u672A\u77E5\u9519\u8BEF" }); } finally { close(); } }); router3.post("/sp-api-schedule/collect-order", async (req, res) => { const { shopId } = req.body || {}; const { send, close } = createSseResponse(res); req.on("aborted", () => close()); try { send("progress", { type: "info", message: `\u5F00\u59CB\u91C7\u96C6\u8BA2\u5355\u6570\u636E${shopId ? "\uFF0C\u5E97\u94FA: " + shopId : "\uFF08\u5168\u90E8\u6D3B\u8DC3\u5E97\u94FA\uFF09"}` }); const result = await newSpApiScheduler.collectOrders( shopId || void 0, (event) => send("progress", event) ); send("done", { success: result.success, message: result.message, processed: result.processed, errorCount: result.errorCount, errors: result.errors }); } catch (error) { send("error", { success: false, message: error.message || "\u91C7\u96C6\u8BA2\u5355\u6570\u636E\u65F6\u53D1\u751F\u672A\u77E5\u9519\u8BEF" }); } finally { close(); } }); router3.post("/sp-api-schedule/collect-report", async (req, res) => { const { shopId } = req.body || {}; const { send, close } = createSseResponse(res); req.on("aborted", () => close()); try { send("progress", { type: "info", message: `\u5F00\u59CB\u91C7\u96C6\u62A5\u8868\u6570\u636E${shopId ? "\uFF0C\u5E97\u94FA: " + shopId : "\uFF08\u5168\u90E8\u6D3B\u8DC3\u5E97\u94FA\uFF09"}` }); const result = await newSpApiScheduler.collectReports( shopId || void 0, (event) => send("progress", event) ); send("done", { success: result.success, message: result.message, processed: result.processed, errorCount: result.errorCount, errors: result.errors }); } catch (error) { send("error", { success: false, message: error.message || "\u91C7\u96C6\u62A5\u8868\u6570\u636E\u65F6\u53D1\u751F\u672A\u77E5\u9519\u8BEF" }); } finally { close(); } }); router3.post("/sp-api-schedule/parse-report", async (req, res) => { const { shopId } = req.body || {}; const { send, close } = createSseResponse(res); req.on("aborted", () => close()); try { send("progress", { type: "info", message: `\u5F00\u59CB\u89E3\u6790\u62A5\u8868${shopId ? "\uFF0C\u5E97\u94FA: " + shopId : "\uFF08\u5168\u90E8\u6D3B\u8DC3\u5E97\u94FA\uFF09"}` }); const result = await newSpApiScheduler.parseReports( shopId || void 0, (event) => send("progress", event) ); send("done", { success: result.success, message: result.message, processed: result.processed, errorCount: result.errorCount, errors: result.errors }); } catch (error) { send("error", { success: false, message: error.message || "\u89E3\u6790\u62A5\u8868\u65F6\u53D1\u751F\u672A\u77E5\u9519\u8BEF" }); } finally { close(); } }); router3.get("/sorftime-schedule/status", (req, res) => { res.json({ success: true, data: { sorftime: sorftimeScheduler.getStatus() } }); }); router3.post("/sorftime-schedule/trigger-products", async (req, res) => { const { shopId, nodeIds } = req.body || {}; const { send, close } = createSseResponse(res); req.on("aborted", () => close()); try { send("progress", { type: "info", message: "\u5F00\u59CB\u91C7\u96C6\u7C7B\u76EE\u70ED\u9500\u4EA7\u54C1..." }); const result = await sorftimeScheduler.triggerCategoryProducts( shopId || void 0, nodeIds || void 0, (event) => send("progress", event) ); send("done", { success: result.success, message: result.message, successCount: result.successCount, errorCount: result.errorCount, errors: result.errors }); } catch (error) { send("error", { success: false, message: error.message || "\u91C7\u96C6\u7C7B\u76EE\u70ED\u9500\u4EA7\u54C1\u65F6\u53D1\u751F\u672A\u77E5\u9519\u8BEF" }); } finally { close(); } }); router3.post("/sorftime-schedule/trigger-keywords", async (req, res) => { const { shopId, nodeIds } = req.body || {}; const { send, close } = createSseResponse(res); req.on("aborted", () => close()); try { send("progress", { type: "info", message: "\u5F00\u59CB\u91C7\u96C6\u7C7B\u76EE\u5173\u952E\u8BCD..." }); const result = await sorftimeScheduler.triggerCategoryKeywords( shopId || void 0, nodeIds || void 0, (event) => send("progress", event) ); send("done", { success: result.success, message: result.message, successCount: result.successCount, errorCount: result.errorCount, errors: result.errors }); } catch (error) { send("error", { success: false, message: error.message || "\u91C7\u96C6\u7C7B\u76EE\u5173\u952E\u8BCD\u65F6\u53D1\u751F\u672A\u77E5\u9519\u8BEF" }); } finally { close(); } }); router3.post("/sorftime-schedule/trigger-category-trends", async (req, res) => { const { shopId, nodeIds } = req.body || {}; const { send, close } = createSseResponse(res); req.on("aborted", () => close()); try { send("progress", { type: "info", message: "\u5F00\u59CB\u91C7\u96C6\u7C7B\u76EE\u5E02\u573A\u8D8B\u52BF..." }); const result = await sorftimeScheduler.triggerCategoryTrends( shopId || void 0, nodeIds || void 0, (event) => send("progress", event) ); send("done", { success: result.success, message: result.message, successCount: result.successCount, errorCount: result.errorCount, errors: result.errors }); } catch (error) { send("error", { success: false, message: error.message || "\u91C7\u96C6\u7C7B\u76EE\u5E02\u573A\u8D8B\u52BF\u65F6\u53D1\u751F\u672A\u77E5\u9519\u8BEF" }); } finally { close(); } }); router3.post("/sorftime-schedule/trigger-single-category", async (req, res) => { const { nodeId, domain } = req.body || {}; if (!nodeId) { return res.status(400).json({ success: false, message: "\u7F3A\u5C11 nodeId \u53C2\u6570" }); } const { send, close } = createSseResponse(res); req.on("aborted", () => close()); try { send("progress", { type: "info", message: `\u5F00\u59CB\u91C7\u96C6\u7C7B\u76EE ${nodeId} \u5168\u91CF\u6570\u636E\uFF08\u70ED\u9500\u4EA7\u54C1 + \u5173\u952E\u8BCD + \u5E02\u573A\u8D8B\u52BF\uFF09` }); const result = await sorftimeScheduler.triggerSingleCategory( nodeId, domain || 1, (event) => send("progress", event) ); send("done", { success: result.success, message: result.message, tasks: result.tasks, duration: result.duration, errors: result.errors }); } catch (error) { send("error", { success: false, message: error.message || "\u91C7\u96C6\u5355\u7C7B\u76EE\u6570\u636E\u65F6\u53D1\u751F\u672A\u77E5\u9519\u8BEF" }); } finally { close(); } }); router3.post("/sorftime-schedule/collect-reviews", async (req, res) => { const { send, close } = createSseResponse(res); req.on("aborted", () => close()); try { send("progress", { type: "info", message: "\u5F00\u59CB\u91C7\u96C6\u4EA7\u54C1\u8BC4\u8BBA\u6570\u636E..." }); const result = await sorftimeScheduler.triggerProductReviews( (event) => send("progress", event) ); send("done", { success: result.success, message: result.message, successCount: result.successCount, errorCount: result.errorCount, errors: result.errors }); } catch (error) { send("error", { success: false, message: error.message || "\u91C7\u96C6\u4EA7\u54C1\u8BC4\u8BBA\u65F6\u53D1\u751F\u672A\u77E5\u9519\u8BEF" }); } finally { close(); } }); router3.post("/sorftime-schedule/collect-shop-products", async (req, res) => { const { shopId } = req.body || {}; const { send, close } = createSseResponse(res); req.on("aborted", () => close()); try { send("progress", { type: "info", message: `\u5F00\u59CB\u91C7\u96C6\u5E97\u94FA\u4EA7\u54C1\u6570\u636E${shopId ? "\uFF0C\u5E97\u94FA: " + shopId : "\uFF08\u5168\u90E8\u6D3B\u8DC3\u5E97\u94FA\uFF09"}` }); const result = await sorftimeScheduler.collectShopProducts( shopId || void 0, (event) => send("progress", event) ); send("done", { success: result.success, message: result.message, processed: result.processed, errorCount: result.errorCount, errors: result.errors }); } catch (error) { send("error", { success: false, message: error.message || "\u91C7\u96C6\u5E97\u94FA\u4EA7\u54C1\u65F6\u53D1\u751F\u672A\u77E5\u9519\u8BEF" }); } finally { close(); } }); router3.get("/test", (req, res) => { res.json({ message: "\u6D4B\u8BD5 sp-api-schedule", body: req.body }); }); var routes_schedule_default = router3; // backend/api/routes.ts var router4 = express4.Router(); console.log("[API Routes] Initializing Jianhen customer instance..."); router4.use((req, res, next) => { console.log(`[API Router] Hit: ${req.method} ${req.url} (Original: ${req.originalUrl})`); next(); }); router4.get("/ping", (req, res) => { res.json({ message: "pong", instance: "jianhen-voc" }); }); function isInternalRequest(req) { const address = String(req.socket?.remoteAddress || ""); return !req.headers["x-external-request"] && (address === "127.0.0.1" || address === "::1" || address.endsWith(":127.0.0.1")); } async function requireApiUser(req, res, next) { if (isInternalRequest(req)) return next(); const token = String(req.headers["x-parse-session-token"] || "").trim(); if (!token) return res.status(401).json({ success: false, message: "\u9700\u8981\u767B\u5F55" }); try { const Parse = globalThis.Parse; const query = new Parse.Query("_Session"); query.equalTo("sessionToken", token); query.include("user"); const session = await query.first({ useMasterKey: true }); const user = session?.get("user"); if (!user || user.get("status") === "disabled") { return res.status(401).json({ success: false, message: "\u4F1A\u8BDD\u65E0\u6548" }); } req.currentUser = user; next(); } catch { res.status(401).json({ success: false, message: "\u4F1A\u8BDD\u65E0\u6548" }); } } function requireAdmin(req, res, next) { if (isInternalRequest(req) || req.currentUser?.get?.("isAdmin") === true) return next(); res.status(403).json({ success: false, message: "\u9700\u8981\u7BA1\u7406\u5458\u6743\u9650" }); } function requireUserExceptHealth(req, res, next) { if (req.path === "/health") return next(); return requireApiUser(req, res, next); } function requireAdminExceptToken(req, res, next) { if (req.path === "/token") return next(); return requireAdmin(req, res, next); } router4.use("/amazon", requireApiUser, createSpApiRouter()); router4.use("/relay", requireApiUser, requireAdminExceptToken, relayCredentialRouter); console.log("[API Routes] \u2705 Company relay credential routes mounted"); try { router4.use("/schedule", requireApiUser, requireAdmin, routes_schedule_default); } catch (e) { console.warn("[API Routes] scheduleRouter \u52A0\u8F7D\u5931\u8D25", e); } router4.get("/", (req, res) => { res.json({ message: "Jianhen VOC Instance API Routes Loaded", version: "1.0.1-customer-relay", timestamp: (/* @__PURE__ */ new Date()).toISOString(), availableRoutes: [ "/api/amazon", "/api/relay/token", "/api/relay/status", "/api/relay/credential", "/api/schedule" ], removedModules: [ "fmode-sorftime-api (direct upstream)", "fmode-tikhub-api (direct upstream)", "fmode-tikhub-api-customize", "fmode-eccang-api" ], relay: { companyBase: "https://server.fmode.cn", sorftimeUpstream: "/api/voc-ecom/forward", tikhubUpstream: "/api/voc-social/*" } }); }); var routes_default = router4; export { routes_default as default };