new-sp-api-schedule.ts 27 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675
  1. /**
  2. * Amazon SP-API 数据采集调度器(新版)
  3. *
  4. * 相较旧版 sp-api-schedule.ts,本版将四个采集步骤拆分为独立的公共方法:
  5. * - collectListings 采集 Listing 数据
  6. * - collectOrders 采集订单数据
  7. * - collectReports 采集报表数据
  8. * - parseReports 解析/检查未完成报表
  9. *
  10. * 每个方法均接受可选的 ProgressCallback,适合通过 SSE 向前端推送实时进度。
  11. * 定时任务(每天凌晨 1 点)调用上述方法时不传 callback,仅打印日志。
  12. *
  13. * 使用方式:
  14. * import { newSpApiScheduler } from './new-sp-api-schedule.ts';
  15. * // 仅手动触发,不启动 cron(由旧版 spApiScheduler 维护 cron)
  16. * await newSpApiScheduler.collectListings(shopId, progressCallback);
  17. */
  18. import nodeCron from 'npm:node-cron';
  19. // ===== 调度器类 =====
  20. class NewSpApiScheduler {
  21. #cronTask = null;
  22. #reportCronTask = null;
  23. #lastRunTime = null;
  24. // ========== 私有工具 ==========
  25. #delay(ms) {
  26. return new Promise(resolve => setTimeout(resolve, ms));
  27. }
  28. #calculateNextRunTime() {
  29. const now = new Date();
  30. const next = new Date(now);
  31. next.setHours(1, 0, 0, 0);
  32. if (next <= now) next.setDate(next.getDate() + 1);
  33. return next;
  34. }
  35. /**
  36. * 查询活跃的 Amazon 店铺列表
  37. * @param shopId 若传入则只返回该店铺
  38. */
  39. async #getActiveAmazonShops(shopId) {
  40. const Parse = globalThis.Parse;
  41. const query = new Parse.Query('Shop');
  42. query.equalTo('platform', 'amazon');
  43. query.equalTo('status', 'active');
  44. if (shopId) query.equalTo('objectId', shopId);
  45. query.limit(1000);
  46. return await query.find({ useMasterKey: true });
  47. }
  48. // ========== 单店铺内部实现 ==========
  49. /**
  50. * 执行单个店铺的 Listing 采集
  51. * 通过 /api/amazon/forward 调用 SP-API,内部 cleanListings 处理全部分页
  52. */
  53. async #doCollectListings(shopId, config, marketplaceId, progress) {
  54. const sellerId = config.SpApiConfig.sellerID;
  55. marketplaceId = marketplaceId || 'ATVPDKIKX0DER';
  56. const Parse = globalThis.Parse;
  57. let lastUpdatedAfter;
  58. try {
  59. const shopPointer = Parse.Object.extend('Shop').createWithoutData(shopId);
  60. const query = new Parse.Query('Listing');
  61. query.equalTo('shop', shopPointer);
  62. query.descending('lastUpdatedDate');
  63. query.limit(1);
  64. const latestListing = await query.first({ useMasterKey: true });
  65. if (latestListing && latestListing.get('lastUpdatedDate')) {
  66. lastUpdatedAfter = latestListing.get('lastUpdatedDate').toISOString();
  67. progress?.({ type: 'info', message: `增量采集,使用最新 lastUpdatedDate: ${lastUpdatedAfter}` });
  68. } else {
  69. const oneYearAgo = new Date(Date.now() - 365 * 24 * 60 * 60 * 1000);
  70. lastUpdatedAfter = oneYearAgo.toISOString();
  71. progress?.({ type: 'info', message: `无历史数据,从一年前开始全量采集: ${lastUpdatedAfter}` });
  72. }
  73. } catch (error) {
  74. const oneYearAgo = new Date(Date.now() - 365 * 24 * 60 * 60 * 1000);
  75. lastUpdatedAfter = oneYearAgo.toISOString();
  76. progress?.({ type: 'warn', message: `查询 Listing 历史失败,默认使用一年前: ${lastUpdatedAfter}` });
  77. }
  78. 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}`;
  79. progress?.({ type: 'info', message: '正在调用亚马逊 Listing API(cleanListings 内部处理分页,请耐心等待)...' });
  80. console.log(`[New SP-API Scheduler] 店铺 ${shopId} 请求 Listing API,path: ${path}`);
  81. const response = await fetch('http://localhost:3000/api/amazon/forward', {
  82. method: 'POST',
  83. headers: { 'Content-Type': 'application/json', 'shop-objectid': shopId },
  84. body: JSON.stringify({ path, method: 'GET', functionName: 'cleanListings' })
  85. });
  86. const result = await response.json();
  87. if (!result.success) {
  88. throw new Error(result.message || 'Listing 采集失败');
  89. }
  90. const processed = result.data?.processed || 0;
  91. console.log(`[New SP-API Scheduler] 店铺 ${shopId} Listing 采集完成: ${processed} 条记录`);
  92. return { processed };
  93. }
  94. /**
  95. * 执行单个店铺的订单采集(增量)
  96. * 通过 /api/amazon/forward 调用 SP-API,内部 cleanOrders 处理全部分页
  97. */
  98. async #doCollectOrders(shopId, config, marketplaceId, progress) {
  99. marketplaceId = marketplaceId || 'ATVPDKIKX0DER';
  100. const Parse = globalThis.Parse;
  101. let createdAfter;
  102. try {
  103. const shopPointer = Parse.Object.extend('Shop').createWithoutData(shopId);
  104. const query = new Parse.Query('Order');
  105. query.equalTo('shop', shopPointer);
  106. query.descending('orderDate');
  107. query.limit(1);
  108. const latestOrder = await query.first({ useMasterKey: true });
  109. if (latestOrder && latestOrder.get('orderDate')) {
  110. createdAfter = latestOrder.get('orderDate').toISOString();
  111. progress?.({ type: 'info', message: `增量采集,使用最新 orderDate: ${createdAfter}` });
  112. } else {
  113. const ninetyDaysAgo = new Date(Date.now() - 90 * 24 * 60 * 60 * 1000);
  114. createdAfter = ninetyDaysAgo.toISOString();
  115. progress?.({ type: 'info', message: `无历史数据,从90天前开始采集: ${createdAfter}` });
  116. }
  117. } catch (error) {
  118. const ninetyDaysAgo = new Date(Date.now() - 90 * 24 * 60 * 60 * 1000);
  119. createdAfter = ninetyDaysAgo.toISOString();
  120. progress?.({ type: 'warn', message: `查询 Order 历史失败,默认使用90天前: ${createdAfter}` });
  121. }
  122. progress?.({ type: 'info', message: '正在调用亚马逊 Orders API(cleanOrders 内部处理分页,请耐心等待)...' });
  123. console.log(`[New SP-API Scheduler] 店铺 ${shopId} 请求 Orders API,createdAfter: ${createdAfter}`);
  124. const response = await fetch('http://localhost:3000/api/amazon/forward', {
  125. method: 'POST',
  126. headers: { 'Content-Type': 'application/json', 'shop-objectid': shopId },
  127. body: JSON.stringify({
  128. path: `/orders/v0/orders?MarketplaceIds=${marketplaceId}&CreatedAfter=${createdAfter}`,
  129. method: 'GET',
  130. functionName: 'cleanOrders'
  131. })
  132. });
  133. const result = await response.json();
  134. if (!result.success) {
  135. throw new Error(result.message || '订单采集失败');
  136. }
  137. const processed = result.data?.processed || 0;
  138. console.log(`[New SP-API Scheduler] 店铺 ${shopId} 订单采集完成: ${processed} 条记录`);
  139. return { processed };
  140. }
  141. /**
  142. * 执行单个店铺的报表创建与保存
  143. */
  144. async #doCollectReports(shopId, config, marketplaceId, progress) {
  145. const Parse = globalThis.Parse;
  146. const reportTypes = ['GET_FBA_FULFILLMENT_CUSTOMER_RETURNS_DATA'];
  147. for (const reportType of reportTypes) {
  148. try {
  149. // 1. 查询上次 dataEndTime 作为本次起点
  150. let dataStartTime;
  151. try {
  152. const q = new Parse.Query('Reports');
  153. q.equalTo('shop', shopId);
  154. q.equalTo('reportType', reportType);
  155. q.descending('dataEndTime');
  156. q.limit(1);
  157. const last = await q.first({ useMasterKey: true });
  158. dataStartTime = last?.get('dataEndTime')
  159. ? last.get('dataEndTime').toISOString()
  160. : new Date(Date.now() - 90 * 24 * 60 * 60 * 1000).toISOString();
  161. } catch (e) {
  162. dataStartTime = new Date(Date.now() - 90 * 24 * 60 * 60 * 1000).toISOString();
  163. }
  164. const dataEndTime = new Date().toISOString();
  165. progress?.({ type: 'info', message: `创建报表 ${reportType},时间范围: ${dataStartTime} ~ ${dataEndTime}` });
  166. console.log(`[New SP-API Scheduler] 店铺 ${shopId} 创建报表 ${reportType}, range: ${dataStartTime} ~ ${dataEndTime}`);
  167. // 2. 创建报表
  168. const createRes = await fetch('http://localhost:3000/api/amazon/forward', {
  169. method: 'POST',
  170. headers: { 'Content-Type': 'application/json', 'shop-objectid': shopId },
  171. body: JSON.stringify({
  172. path: '/reports/2021-06-30/reports',
  173. method: 'POST',
  174. body: { reportType, marketplaceIds: [marketplaceId], dataStartTime, dataEndTime }
  175. })
  176. });
  177. const createResult = await createRes.json();
  178. const reportId = createResult?.data?.reportId;
  179. if (!reportId) {
  180. progress?.({ type: 'error', message: `创建报表失败 ${reportType}: ${JSON.stringify(createResult)}` });
  181. console.error(`[New SP-API Scheduler] 创建报表失败 ${reportType}:`, JSON.stringify(createResult));
  182. continue;
  183. }
  184. progress?.({ type: 'info', message: `报表已创建 reportId: ${reportId},等待查询状态...` });
  185. console.log(`[New SP-API Scheduler] 报表已创建, reportId: ${reportId}`);
  186. await this.#delay(3000);
  187. // 3. 查询报表状态
  188. const getRes = await fetch('http://localhost:3000/api/amazon/forward', {
  189. method: 'POST',
  190. headers: { 'Content-Type': 'application/json', 'shop-objectid': shopId },
  191. body: JSON.stringify({
  192. path: `/reports/2021-06-30/reports/${reportId}`,
  193. method: 'GET'
  194. })
  195. });
  196. const reportData = await getRes.json();
  197. const reportInfo = reportData?.data || {};
  198. // 4. 存入 Reports 表
  199. const ReportObj = Parse.Object.extend('Reports');
  200. const rpt = new ReportObj();
  201. rpt.set('shop', { __type: 'Pointer', className: 'Shop', objectId: shopId });
  202. rpt.set('reportId', reportId);
  203. rpt.set('reportType', reportType);
  204. rpt.set('marketplaceIds', reportInfo.marketplaceIds || []);
  205. rpt.set('dataStartTime', reportInfo.dataStartTime ? new Date(reportInfo.dataStartTime) : null);
  206. rpt.set('dataEndTime', reportInfo.dataEndTime ? new Date(reportInfo.dataEndTime) : null);
  207. rpt.set('createdTime', reportInfo.createdTime ? new Date(reportInfo.createdTime) : null);
  208. rpt.set('processingStartTime', reportInfo.processingStartTime ? new Date(reportInfo.processingStartTime) : null);
  209. rpt.set('processingEndTime', reportInfo.processingEndTime ? new Date(reportInfo.processingEndTime) : null);
  210. rpt.set('processingStatus', reportInfo.processingStatus || 'IN_QUEUE');
  211. rpt.set('reportDocumentId', reportInfo.reportDocumentId || null);
  212. rpt.set('isParsed', false);
  213. await rpt.save(null, { useMasterKey: true });
  214. progress?.({ type: 'success', message: `报表 ${reportType} 已保存,processingStatus: ${reportInfo.processingStatus || 'IN_QUEUE'}` });
  215. console.log(`[New SP-API Scheduler] 报表已保存, status: ${reportInfo.processingStatus}`);
  216. await this.#delay(2000);
  217. } catch (error) {
  218. progress?.({ type: 'error', message: `报表 ${reportType} 处理失败: ${error.message}` });
  219. console.error(`[New SP-API Scheduler] 报表 ${reportType} 处理失败: ${error.message}`);
  220. }
  221. }
  222. }
  223. /**
  224. * 检查并更新 processingStatus 不为 DONE 的报表
  225. */
  226. async #doCheckPendingReports(shopId, progress) {
  227. const Parse = globalThis.Parse;
  228. const query = new Parse.Query('Reports');
  229. query.notEqualTo('processingStatus', 'DONE');
  230. if (shopId) query.equalTo('shop', shopId);
  231. query.limit(1000);
  232. const pendingReports = await query.find({ useMasterKey: true });
  233. if (pendingReports.length === 0) {
  234. progress?.({ type: 'info', message: '没有状态未完成的报表,跳过状态更新' });
  235. console.log('[New SP-API Scheduler] 没有未完成的报表');
  236. return;
  237. }
  238. progress?.({ type: 'info', message: `发现 ${pendingReports.length} 个未完成报表,逐一查询最新状态...` });
  239. console.log(`[New SP-API Scheduler] 发现 ${pendingReports.length} 个未完成报表`);
  240. for (const report of pendingReports) {
  241. try {
  242. const reportId = report.get('reportId');
  243. const sid = report.get('shop').id;
  244. if (!reportId || !sid) continue;
  245. const getRes = await fetch('http://localhost:3000/api/amazon/forward', {
  246. method: 'POST',
  247. headers: { 'Content-Type': 'application/json', 'shop-objectid': sid },
  248. body: JSON.stringify({ path: `/reports/2021-06-30/reports/${reportId}`, method: 'GET' })
  249. });
  250. const reportData = await getRes.json();
  251. const reportInfo = reportData?.data || {};
  252. const newStatus = reportInfo.processingStatus || report.get('processingStatus');
  253. report.set('processingStatus', newStatus);
  254. if (reportInfo.reportDocumentId) report.set('reportDocumentId', reportInfo.reportDocumentId);
  255. await report.save(null, { useMasterKey: true });
  256. progress?.({ type: 'info', message: `报表 ${reportId} 状态更新为: ${newStatus}` });
  257. console.log(`[New SP-API Scheduler] 报表 ${reportId} 状态更新为: ${newStatus}`);
  258. await this.#delay(10000);
  259. } catch (error) {
  260. progress?.({ type: 'error', message: `更新报表状态失败: ${error.message}` });
  261. console.error(`[New SP-API Scheduler] 更新报表状态失败: ${error.message}`);
  262. }
  263. }
  264. }
  265. /**
  266. * 解析 isParsed=false & processingStatus=DONE 的报表
  267. */
  268. async #doProcessUnparsedReports(shopId, progress) {
  269. const Parse = globalThis.Parse;
  270. const query = new Parse.Query('Reports');
  271. query.equalTo('shop', shopId);
  272. query.equalTo('isParsed', false);
  273. query.equalTo('processingStatus', 'DONE');
  274. query.limit(1000);
  275. const unparsedReports = await query.find();
  276. if (unparsedReports.length === 0) {
  277. progress?.({ type: 'info', message: '没有待解析的报表(状态 DONE 且未解析)' });
  278. console.log(`[New SP-API Scheduler] 店铺 ${shopId} 没有待解析的报表`);
  279. return { processed: 0 };
  280. }
  281. progress?.({ type: 'info', message: `发现 ${unparsedReports.length} 个待解析报表` });
  282. console.log(`[New SP-API Scheduler] 发现 ${unparsedReports.length} 个待解析报表`);
  283. let totalProcessed = 0;
  284. for (const report of unparsedReports) {
  285. try {
  286. const reportDocumentId = report.get('reportDocumentId');
  287. const reportType = report.get('reportType');
  288. if (!reportDocumentId) {
  289. progress?.({ type: 'warn', message: `报表 ${report.id} 缺少 reportDocumentId,跳过` });
  290. continue;
  291. }
  292. progress?.({ type: 'info', message: `正在解析报表 ${reportDocumentId} (${reportType})` });
  293. console.log(`[New SP-API Scheduler] 解析报表 ${reportDocumentId} (${reportType})`);
  294. const response = await fetch('http://localhost:3000/api/amazon/processReport', {
  295. method: 'POST',
  296. headers: { 'Content-Type': 'application/json', 'shop-objectid': shopId },
  297. body: JSON.stringify({ reportDocumentId, reportType, shopId })
  298. });
  299. const result = await response.json();
  300. if (result.success) {
  301. const parsed = result.data?.processResult?.processed || 0;
  302. report.set('isParsed', true);
  303. report.set('parsedAt', new Date());
  304. report.set('parsedRecords', parsed);
  305. await report.save(null, { useMasterKey: true });
  306. totalProcessed += parsed;
  307. progress?.({ type: 'success', message: `报表 ${reportDocumentId} 解析完成: ${parsed} 条记录`, processed: parsed });
  308. console.log(`[New SP-API Scheduler] 报表 ${reportDocumentId} 解析完成: ${parsed} 条记录`);
  309. } else {
  310. progress?.({ type: 'warn', message: `报表 ${reportDocumentId} 解析未成功: ${result.message}` });
  311. console.warn(`[New SP-API Scheduler] 报表 ${reportDocumentId} 解析未成功: ${result.message}`);
  312. }
  313. await this.#delay(10000);
  314. } catch (error) {
  315. progress?.({ type: 'error', message: `报表解析异常: ${error.message}` });
  316. console.error(`[New SP-API Scheduler] 报表解析异常: ${error.message}`);
  317. }
  318. }
  319. return { processed: totalProcessed };
  320. }
  321. // ========== 构造函数 ==========
  322. constructor() {
  323. console.log('[New SP-API Scheduler] 初始化 Amazon SP-API 数据采集调度器(新版)');
  324. }
  325. // ========== 公共采集方法 ==========
  326. /**
  327. * 采集所有活跃店铺(或指定店铺)的 Listing 数据
  328. * @param shopId 可选,不传则采集全部活跃店铺
  329. * @param progress 可选进度回调,适合 SSE 实时推送
  330. */
  331. async collectListings(shopId, progress) {
  332. const shops = await this.#getActiveAmazonShops(shopId);
  333. if (shops.length === 0) {
  334. const msg = shopId ? `未找到店铺 ${shopId} 或店铺未激活` : '没有找到活跃的 Amazon 店铺';
  335. progress?.({ type: 'warn', message: msg });
  336. console.warn(`[New SP-API Scheduler] ${msg}`);
  337. return { success: true, message: msg, processed: 0, errorCount: 0, errors: [] };
  338. }
  339. let totalProcessed = 0;
  340. let errorCount = 0;
  341. const errors = [];
  342. for (const shop of shops) {
  343. const sid = shop.id;
  344. const shopName = shop.get('name');
  345. const config = shop.get('config');
  346. const marketplaceId = shop.get('marketplaceId') || 'ATVPDKIKX0DER';
  347. if (!config || !config.SpApiConfig) {
  348. const msg = `店铺【${shopName}】(${sid}) 缺少 SpApiConfig 配置,跳过`;
  349. progress?.({ type: 'error', message: msg });
  350. console.error(`[New SP-API Scheduler] ${msg}`);
  351. errors.push(msg);
  352. errorCount++;
  353. continue;
  354. }
  355. if (config.SpApiConfig.listingEnabled === false) {
  356. progress?.({ type: 'warn', message: `店铺【${shopName}】未配置区域 Seller ID,跳过 Listing` });
  357. continue;
  358. }
  359. progress?.({ type: 'info', message: `开始采集店铺【${shopName}】的 Listing 数据` });
  360. console.log(`[New SP-API Scheduler] 开始采集店铺 ${shopName} (${sid}) 的 Listing 数据`);
  361. try {
  362. const result = await this.#doCollectListings(sid, config, marketplaceId, progress);
  363. progress?.({ type: 'success', message: `店铺【${shopName}】Listing 采集完成`, processed: result.processed });
  364. totalProcessed += result.processed;
  365. await this.#delay(2000);
  366. } catch (error) {
  367. const msg = `店铺【${shopName}】Listing 采集失败: ${error.message}`;
  368. progress?.({ type: 'error', message: msg });
  369. console.error(`[New SP-API Scheduler] ${msg}`);
  370. errors.push(msg);
  371. errorCount++;
  372. }
  373. }
  374. const message = `Listing 采集完成: 成功 ${shops.length - errorCount}/${shops.length} 个店铺,共 ${totalProcessed} 条记录`;
  375. return { success: errorCount === 0, message, processed: totalProcessed, errorCount, errors };
  376. }
  377. /**
  378. * 采集所有活跃店铺(或指定店铺)的订单数据(增量)
  379. * @param shopId 可选
  380. * @param progress 可选进度回调
  381. */
  382. async collectOrders(shopId, progress) {
  383. const shops = await this.#getActiveAmazonShops(shopId);
  384. if (shops.length === 0) {
  385. const msg = shopId ? `未找到店铺 ${shopId} 或店铺未激活` : '没有找到活跃的 Amazon 店铺';
  386. progress?.({ type: 'warn', message: msg });
  387. console.warn(`[New SP-API Scheduler] ${msg}`);
  388. return { success: true, message: msg, processed: 0, errorCount: 0, errors: [] };
  389. }
  390. let totalProcessed = 0;
  391. let errorCount = 0;
  392. const errors = [];
  393. for (const shop of shops) {
  394. const sid = shop.id;
  395. const shopName = shop.get('name');
  396. const config = shop.get('config');
  397. const marketplaceId = shop.get('marketplaceId') || 'ATVPDKIKX0DER';
  398. if (!config || !config.SpApiConfig) {
  399. const msg = `店铺【${shopName}】(${sid}) 缺少 SpApiConfig 配置,跳过`;
  400. progress?.({ type: 'error', message: msg });
  401. console.error(`[New SP-API Scheduler] ${msg}`);
  402. errors.push(msg);
  403. errorCount++;
  404. continue;
  405. }
  406. progress?.({ type: 'info', message: `开始采集店铺【${shopName}】的订单数据` });
  407. console.log(`[New SP-API Scheduler] 开始采集店铺 ${shopName} (${sid}) 的订单数据`);
  408. try {
  409. const result = await this.#doCollectOrders(sid, config, marketplaceId, progress);
  410. progress?.({ type: 'success', message: `店铺【${shopName}】订单采集完成`, processed: result.processed });
  411. totalProcessed += result.processed;
  412. await this.#delay(2000);
  413. } catch (error) {
  414. const msg = `店铺【${shopName}】订单采集失败: ${error.message}`;
  415. progress?.({ type: 'error', message: msg });
  416. console.error(`[New SP-API Scheduler] ${msg}`);
  417. errors.push(msg);
  418. errorCount++;
  419. }
  420. }
  421. const message = `订单采集完成: 成功 ${shops.length - errorCount}/${shops.length} 个店铺,共 ${totalProcessed} 条记录`;
  422. return { success: errorCount === 0, message, processed: totalProcessed, errorCount, errors };
  423. }
  424. /**
  425. * 为所有活跃店铺(或指定店铺)创建并保存报表
  426. * @param shopId 可选
  427. * @param progress 可选进度回调
  428. */
  429. async collectReports(shopId, progress) {
  430. const shops = await this.#getActiveAmazonShops(shopId);
  431. if (shops.length === 0) {
  432. const msg = shopId ? `未找到店铺 ${shopId} 或店铺未激活` : '没有找到活跃的 Amazon 店铺';
  433. progress?.({ type: 'warn', message: msg });
  434. console.warn(`[New SP-API Scheduler] ${msg}`);
  435. return { success: true, message: msg, processed: 0, errorCount: 0, errors: [] };
  436. }
  437. let successCount = 0;
  438. let errorCount = 0;
  439. const errors = [];
  440. for (const shop of shops) {
  441. const sid = shop.id;
  442. const shopName = shop.get('name');
  443. const config = shop.get('config');
  444. const marketplaceId = shop.get('marketplaceId') || 'ATVPDKIKX0DER';
  445. if (!config || !config.SpApiConfig) {
  446. const msg = `店铺【${shopName}】(${sid}) 缺少 SpApiConfig 配置,跳过`;
  447. progress?.({ type: 'error', message: msg });
  448. console.error(`[New SP-API Scheduler] ${msg}`);
  449. errors.push(msg);
  450. errorCount++;
  451. continue;
  452. }
  453. progress?.({ type: 'info', message: `开始采集店铺【${shopName}】的报表数据` });
  454. console.log(`[New SP-API Scheduler] 开始采集店铺 ${shopName} (${sid}) 的报表数据`);
  455. try {
  456. await this.#doCollectReports(sid, config, marketplaceId, progress);
  457. progress?.({ type: 'success', message: `店铺【${shopName}】报表采集完成` });
  458. successCount++;
  459. await this.#delay(2000);
  460. } catch (error) {
  461. const msg = `店铺【${shopName}】报表采集失败: ${error.message}`;
  462. progress?.({ type: 'error', message: msg });
  463. console.error(`[New SP-API Scheduler] ${msg}`);
  464. errors.push(msg);
  465. errorCount++;
  466. }
  467. }
  468. const message = `报表采集完成: 成功 ${successCount}/${shops.length} 个店铺`;
  469. return { success: errorCount === 0, message, processed: successCount, errorCount, errors };
  470. }
  471. /**
  472. * 检查未完成报表状态并解析已完成的报表
  473. * @param shopId 可选
  474. * @param progress 可选进度回调
  475. */
  476. async parseReports(shopId, progress) {
  477. const shops = await this.#getActiveAmazonShops(shopId);
  478. if (shops.length === 0) {
  479. const msg = shopId ? `未找到店铺 ${shopId} 或店铺未激活` : '没有找到活跃的 Amazon 店铺';
  480. progress?.({ type: 'warn', message: msg });
  481. console.warn(`[New SP-API Scheduler] ${msg}`);
  482. return { success: true, message: msg, processed: 0, errorCount: 0, errors: [] };
  483. }
  484. // 先全局检查并更新 pending 状态
  485. progress?.({ type: 'info', message: '检查所有未完成报表的最新状态...' });
  486. try {
  487. await this.#doCheckPendingReports(shopId, progress);
  488. } catch (error) {
  489. progress?.({ type: 'warn', message: `检查未完成报表状态时出错: ${error.message}` });
  490. console.warn(`[New SP-API Scheduler] 检查未完成报表状态出错: ${error.message}`);
  491. }
  492. let totalProcessed = 0;
  493. let errorCount = 0;
  494. const errors = [];
  495. for (const shop of shops) {
  496. const sid = shop.id;
  497. const shopName = shop.get('name');
  498. progress?.({ type: 'info', message: `开始解析店铺【${shopName}】的已完成报表` });
  499. console.log(`[New SP-API Scheduler] 开始解析店铺 ${shopName} (${sid}) 的已完成报表`);
  500. try {
  501. const result = await this.#doProcessUnparsedReports(sid, progress);
  502. progress?.({ type: 'success', message: `店铺【${shopName}】报表解析完成: ${result.processed} 条记录`, processed: result.processed });
  503. totalProcessed += result.processed;
  504. await this.#delay(2000);
  505. } catch (error) {
  506. const msg = `店铺【${shopName}】报表解析失败: ${error.message}`;
  507. progress?.({ type: 'error', message: msg });
  508. console.error(`[New SP-API Scheduler] ${msg}`);
  509. errors.push(msg);
  510. errorCount++;
  511. }
  512. }
  513. const message = `报表解析完成: 成功 ${shops.length - errorCount}/${shops.length} 个店铺,共解析 ${totalProcessed} 条记录`;
  514. return { success: errorCount === 0, message, processed: totalProcessed, errorCount, errors };
  515. }
  516. // ========== 定时任务管理 ==========
  517. /**
  518. * 启动定时任务
  519. * - 凌晨 1 点:采集 Listing / Order / Report
  520. * - 凌晨 2 点:解析已完成报表
  521. */
  522. async start() {
  523. if (this.#cronTask) {
  524. console.log('[New SP-API Scheduler] 定时任务已在运行中,跳过重复启动');
  525. return;
  526. }
  527. this.#cronTask = nodeCron.schedule('0 1 * * *', async () => {
  528. console.log('[New SP-API Scheduler] ===== 凌晨1点:开始每日数据采集 =====');
  529. this.#lastRunTime = new Date();
  530. try {
  531. await this.collectListings(undefined, undefined);
  532. await this.collectOrders(undefined, undefined);
  533. await this.collectReports(undefined, undefined);
  534. } catch (e) {
  535. console.error('[New SP-API Scheduler] 每日采集任务失败:', e.message);
  536. }
  537. console.log('[New SP-API Scheduler] ===== 每日数据采集完成 =====');
  538. }, { timezone: 'Asia/Shanghai' });
  539. this.#reportCronTask = nodeCron.schedule('0 2 * * *', async () => {
  540. console.log('[New SP-API Scheduler] ===== 凌晨2点:开始报表解析 =====');
  541. try {
  542. await this.parseReports(undefined, undefined);
  543. } catch (e) {
  544. console.error('[New SP-API Scheduler] 报表解析任务失败:', e.message);
  545. }
  546. console.log('[New SP-API Scheduler] ===== 报表解析完成 =====');
  547. }, { timezone: 'Asia/Shanghai' });
  548. console.log('[New SP-API Scheduler] 定时任务已启动(凌晨1点采集,凌晨2点解析)');
  549. }
  550. /**
  551. * 停止定时任务
  552. */
  553. stop() {
  554. if (this.#cronTask) {
  555. this.#cronTask.stop();
  556. this.#cronTask = null;
  557. console.log('[New SP-API Scheduler] 采集定时任务已停止');
  558. }
  559. if (this.#reportCronTask) {
  560. this.#reportCronTask.stop();
  561. this.#reportCronTask = null;
  562. console.log('[New SP-API Scheduler] 报表解析定时任务已停止');
  563. }
  564. }
  565. /**
  566. * 获取调度器状态
  567. */
  568. getStatus() {
  569. return {
  570. isRunning: !!(this.#cronTask || this.#reportCronTask),
  571. lastRunTime: this.#lastRunTime,
  572. nextRunTime: this.#calculateNextRunTime()
  573. };
  574. }
  575. }
  576. // 导出单例
  577. export const newSpApiScheduler = new NewSpApiScheduler();