sp-api-schedule.ts 24 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676
  1. /**
  2. * Amazon SP-API 数据采集定时任务
  3. *
  4. * 功能:
  5. * - 每天凌晨1点自动执行数据采集
  6. * - 支持多店铺数据同步
  7. * - 采集 Listing、订单、报表数据
  8. * - 自动解析未处理的报表
  9. *
  10. * 使用方式:
  11. * import { spApiScheduler } from './modules/sp-api-schedule.ts';
  12. * await spApiScheduler.start();
  13. */
  14. import nodeCron from 'npm:node-cron';
  15. class SpApiScheduler {
  16. // 私有属性(先声明)
  17. #isRunning = false;
  18. #lastRunTime = null;
  19. #cronTask= null;
  20. #reportCronTask = null;
  21. #cronEnabled = true;
  22. // ========== 私有工具方法(最优先声明,避免调用时未定义) ==========
  23. /**
  24. * 延迟函数
  25. * @private
  26. * @param {number} ms - 延迟毫秒数
  27. * @returns {Promise<void>} 延迟 Promise
  28. */
  29. #delay(ms) {
  30. return new Promise(resolve => setTimeout(resolve, ms));
  31. }
  32. /**
  33. * 计算下次执行时间(凌晨1点)
  34. * @private
  35. * @returns {Date} 下次执行时间
  36. */
  37. #calculateNextRunTime() {
  38. const now = new Date();
  39. const next = new Date(now);
  40. next.setHours(1, 0, 0, 0);
  41. if (next <= now) {
  42. next.setDate(next.getDate() + 1);
  43. }
  44. return next;
  45. }
  46. /**
  47. * 获取所有活跃的 Amazon 店铺
  48. * @private
  49. * @returns {Promise<any[]>} 活跃店铺列表
  50. */
  51. async #getActiveAmazonShops(shopId) {
  52. const Parse = globalThis.Parse
  53. const query = new Parse.Query('Shop');
  54. query.equalTo('platform', 'amazon');
  55. query.equalTo('status', 'active');
  56. if(shopId) {
  57. query.equalTo('objectId', shopId);
  58. }
  59. query.limit(1000);
  60. return await query.find();
  61. }
  62. /**
  63. * 记录执行日志
  64. * @private
  65. * @param {any} logData - 日志数据对象
  66. * @returns {Promise<void>}
  67. */
  68. async #logExecution(logData) {
  69. try {
  70. const Parse = globalThis.Parse
  71. const TaskLog = Parse.Object.extend('TaskExecutionLog');
  72. const log = new TaskLog();
  73. log.set('taskName', logData.taskName);
  74. log.set('startTime', logData.startTime);
  75. log.set('endTime', logData.endTime);
  76. log.set('duration', logData.duration);
  77. log.set('successCount', logData.successCount);
  78. log.set('errorCount', logData.errorCount);
  79. log.set('errors', logData.errors);
  80. log.set('status', logData.status);
  81. await log.save(null, { useMasterKey: true });
  82. console.log('[SP-API Scheduler] 执行日志已保存');
  83. } catch (error) {
  84. console.error('[SP-API Scheduler] 保存执行日志失败:', error.message);
  85. }
  86. }
  87. /**
  88. * 采集 Listing 数据
  89. * @private
  90. * @param {string} shopId - 店铺ID
  91. * @param {any} config - 店铺配置
  92. * @param {string} marketplaceId - 市场ID
  93. * @returns {Promise<void>}
  94. */
  95. async #collectListings(shopId, config, marketplaceId) {
  96. const sellerId = config.SpApiConfig.sellerID;
  97. marketplaceId = marketplaceId || 'ATVPDKIKX0DER';
  98. const Parse = globalThis.Parse;
  99. let lastUpdatedAfter;
  100. try {
  101. const shopPointer = Parse.Object.extend('Shop').createWithoutData(shopId);
  102. const query = new Parse.Query('Listing');
  103. query.equalTo('shop', shopPointer);
  104. query.descending('lastUpdatedDate');
  105. query.limit(1);
  106. const latestListing = await query.first({ useMasterKey: true });
  107. if (latestListing && latestListing.get('lastUpdatedDate')) {
  108. lastUpdatedAfter = latestListing.get('lastUpdatedDate').toISOString();
  109. console.log(`[SP-API Scheduler] 店铺 ${shopId} 使用最新的 lastUpdatedDate: ${lastUpdatedAfter}`);
  110. } else {
  111. const oneYearAgo = new Date(Date.now() - 365 * 24 * 60 * 60 * 1000);
  112. lastUpdatedAfter = oneYearAgo.toISOString();
  113. console.log(`[SP-API Scheduler] 店铺 ${shopId} 没有Listing数据,使用一年前的时间: ${lastUpdatedAfter}`);
  114. }
  115. } catch (error) {
  116. const oneYearAgo = new Date(Date.now() - 365 * 24 * 60 * 60 * 1000);
  117. lastUpdatedAfter = oneYearAgo.toISOString();
  118. console.log(`[SP-API Scheduler] 店铺 ${shopId} 查询Listing失败,使用一年前的时间: ${lastUpdatedAfter}, 错误: ${error.message}`);
  119. }
  120. 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}`
  121. const response = await fetch('http://localhost:3000/api/amazon/forward', {
  122. method: 'POST',
  123. headers: { 'Content-Type': 'application/json', 'shop-objectid': shopId },
  124. body: JSON.stringify({
  125. path: path,
  126. method: 'GET',
  127. functionName: "cleanListings"
  128. })
  129. });
  130. const result = await response.json();
  131. if (!result.success) {
  132. throw new Error(result.message || 'Listing 采集失败');
  133. }
  134. console.log(`[SP-API Scheduler] 店铺 ${shopId} Listing 数据采集完成: ${result.processed || 0} 条记录`);
  135. }
  136. /**
  137. * 采集订单数据(增量同步)
  138. * @private
  139. * @param {string} shopId - 店铺ID
  140. * @param {any} config - 店铺配置
  141. * @param {string[]} marketplaceIds - 市场ID列表
  142. * @param {any} shop - 店铺对象
  143. * @returns {Promise<void>}
  144. */
  145. async #collectOrders(shopId, config, marketplaceId) {
  146. marketplaceId = marketplaceId || 'ATVPDKIKX0DER';
  147. const Parse = globalThis.Parse;
  148. let createdAfter;
  149. try {
  150. const shopPointer = Parse.Object.extend('Shop').createWithoutData(shopId);
  151. const query = new Parse.Query('Order');
  152. query.equalTo('shop', shopPointer);
  153. query.descending('orderDate');
  154. query.limit(1);
  155. const latestOrder = await query.first({ useMasterKey: true });
  156. if (latestOrder && latestOrder.get('orderDate')) {
  157. createdAfter = latestOrder.get('orderDate').toISOString();
  158. console.log(`[SP-API Scheduler] 店铺 ${shopId} 使用最新的 orderDate: ${createdAfter}`);
  159. } else {
  160. const ninetyDaysAgo = new Date(Date.now() - 90 * 24 * 60 * 60 * 1000);
  161. createdAfter = ninetyDaysAgo.toISOString();
  162. console.log(`[SP-API Scheduler] 店铺 ${shopId} 没有Order数据,使用90天前的时间: ${createdAfter}`);
  163. }
  164. } catch (error) {
  165. const ninetyDaysAgo = new Date(Date.now() - 90 * 24 * 60 * 60 * 1000);
  166. createdAfter = ninetyDaysAgo.toISOString();
  167. console.log(`[SP-API Scheduler] 店铺 ${shopId} 查询Order失败,使用90天前的时间: ${createdAfter}, 错误: ${error.message}`);
  168. }
  169. const response = await fetch('http://localhost:3000/api/amazon/forward', {
  170. method: 'POST',
  171. headers: { 'Content-Type': 'application/json', "shop-objectid": shopId },
  172. body: JSON.stringify({
  173. path: `/orders/v0/orders?MarketplaceIds=${marketplaceId}&CreatedAfter=${createdAfter}`,
  174. method: 'GET',
  175. functionName: 'cleanOrders'
  176. })
  177. });
  178. const result = await response.json();
  179. if (!result.success) {
  180. throw new Error(result.message || '订单采集失败');
  181. }
  182. console.log(`[SP-API Scheduler] 店铺 ${shopId} 订单数据采集完成: ${result.processed || 0} 条记录`);
  183. }
  184. /**
  185. * 采集报表数据
  186. * @private
  187. * @param {string} shopId - 店铺ID
  188. * @param {any} config - 店铺配置
  189. * @param {string[]} marketplaceId - 市场ID列表
  190. * @returns {Promise<void>}
  191. */
  192. async #collectReports(shopId, config, marketplaceId) {
  193. const Parse = globalThis.Parse;
  194. const reportTypes = ['GET_FBA_FULFILLMENT_CUSTOMER_RETURNS_DATA'];
  195. for (const reportType of reportTypes) {
  196. try {
  197. // 1. 查询上次 dataEndTime 作为本次 dataStartTime
  198. let dataStartTime;
  199. try {
  200. const q = new Parse.Query('Reports');
  201. q.equalTo('shop', shopId);
  202. q.equalTo('reportType', reportType);
  203. // q.equalTo('marketplaceIds', [marketplaceId);
  204. q.descending('dataEndTime');
  205. q.limit(1);
  206. const last = await q.first({ useMasterKey: true });
  207. if (last && last.get('dataEndTime')) {
  208. dataStartTime = last.get('dataEndTime').toISOString();
  209. } else {
  210. dataStartTime = new Date(Date.now() - 90 * 24 * 60 * 60 * 1000).toISOString();
  211. }
  212. } catch (e) {
  213. dataStartTime = new Date(Date.now() - 90 * 24 * 60 * 60 * 1000).toISOString();
  214. }
  215. const dataEndTime = new Date().toISOString();
  216. console.log(`[SP-API Scheduler] 店铺 ${shopId} 创建报表 ${reportType}, range: ${dataStartTime} ~ ${dataEndTime}`);
  217. // 2. 创建报表
  218. const createRes = await fetch('http://localhost:3000/api/amazon/forward', {
  219. method: 'POST',
  220. headers: { 'Content-Type': 'application/json', 'shop-objectid': shopId },
  221. body: JSON.stringify({
  222. path: '/reports/2021-06-30/reports',
  223. method: 'POST',
  224. body: { reportType, marketplaceIds: [marketplaceId], dataStartTime, dataEndTime }
  225. })
  226. });
  227. const createResult = await createRes.json();
  228. const reportId = createResult?.data?.reportId;
  229. if (!reportId) {
  230. console.error(`[SP-API Scheduler] 创建报表失败 ${reportType}:`, JSON.stringify(createResult));
  231. continue;
  232. }
  233. console.log(`[SP-API Scheduler] 报表已创建, reportId: ${reportId}`);
  234. await this.#delay(3000);
  235. // 3. 获取报表状态
  236. const getRes = await fetch('http://localhost:3000/api/amazon/forward', {
  237. method: 'POST',
  238. headers: { 'Content-Type': 'application/json', 'shop-objectid': shopId },
  239. body: JSON.stringify({
  240. path: `/reports/2021-06-30/reports/${reportId}`,
  241. method: 'GET'
  242. })
  243. });
  244. const reportData = await getRes.json();
  245. const reportInfo = reportData?.data || {};
  246. // 4. 存入 Reports 表
  247. const ReportObj = Parse.Object.extend('Reports');
  248. const rpt = new ReportObj();
  249. rpt.set('shop', {
  250. __type: 'Pointer',
  251. className: 'Shop',
  252. objectId: shopId
  253. });
  254. rpt.set('reportId', reportId);
  255. rpt.set('reportType', reportType);
  256. rpt.set('marketplaceIds', reportInfo.marketplaceIds || []);
  257. rpt.set('dataStartTime', reportInfo.dataStartTime ? new Date(reportInfo.dataStartTime) : null);
  258. rpt.set('dataEndTime', reportInfo.dataEndTime ? new Date(reportInfo.dataEndTime) : null);
  259. rpt.set('createdTime', reportInfo.createdTime ? new Date(reportInfo.createdTime) : null);
  260. rpt.set('processingStartTime', reportInfo.processingStartTime ? new Date(reportInfo.processingStartTime) : null);
  261. rpt.set('processingEndTime', reportInfo.processingEndTime ? new Date(reportInfo.processingEndTime) : null);
  262. rpt.set('processingStatus', reportInfo.processingStatus || 'IN_QUEUE');
  263. rpt.set('reportDocumentId', reportInfo.reportDocumentId || null);
  264. rpt.set('isParsed', false);
  265. await rpt.save(null, { useMasterKey: true });
  266. console.log(`[SP-API Scheduler] 报表已保存到 Reports 表, status: ${reportInfo.processingStatus}`);
  267. await this.#delay(2000);
  268. } catch (error) {
  269. console.error(`[SP-API Scheduler] 报表 ${reportType} 处理失败: ${error.message}`);
  270. }
  271. }
  272. }
  273. /**
  274. * 处理未解析的报表
  275. * @private
  276. * @param {string} shopId - 店铺ID
  277. * @returns {Promise<void>}
  278. */
  279. async #processUnparsedReports(shopId) {
  280. const Parse = globalThis.Parse
  281. const query = new Parse.Query('Reports');
  282. query.equalTo('shop', shopId);
  283. query.equalTo('isParsed', false);
  284. query.equalTo('processingStatus', 'DONE');
  285. query.limit(1000);
  286. const unparsedReports = await query.find();
  287. if (unparsedReports.length === 0) {
  288. console.log(`[SP-API Scheduler] 店铺 ${shopId} 没有未解析的报表`);
  289. return;
  290. }
  291. console.log(`[SP-API Scheduler] 发现 ${unparsedReports.length} 个未解析报表`);
  292. for (const report of unparsedReports) {
  293. try {
  294. const reportDocumentId = report.get('reportDocumentId');
  295. const reportType = report.get('reportType');
  296. const response = await fetch('http://localhost:3000/api/amazon/processReport', {
  297. method: 'POST',
  298. headers: { 'Content-Type': 'application/json', "shop-objectid": shopId },
  299. body: JSON.stringify({
  300. reportDocumentId,
  301. reportType,
  302. shopId
  303. })
  304. });
  305. const result = await response.json();
  306. if (result.success) {
  307. report.set('isParsed', true);
  308. report.set('parsedAt', new Date());
  309. report.set('parsedRecords', result.processed || 0);
  310. await report.save(null, { useMasterKey: true });
  311. console.log(`[SP-API Scheduler] 报表 ${reportDocumentId} 解析完成: ${result.processed} 条记录`);
  312. }
  313. await this.#delay(10000);
  314. } catch (error) {
  315. console.error(`[SP-API Scheduler] 报表解析失败: ${error.message}`);
  316. }
  317. }
  318. }
  319. /**
  320. * 检查并更新未完成的报表状态
  321. * 每天凌晨2点执行:查询 Reports 表中 status 不为 DONE 的报表,
  322. * 通过 reportId 获取最新状态并更新,然后解析已完成的报表
  323. * @private
  324. * @returns {Promise<void>}
  325. */
  326. async #checkPendingReports(shopid) {
  327. const Parse = globalThis.Parse;
  328. console.log('[SP-API Scheduler] ========================================');
  329. console.log('[SP-API Scheduler] 开始检查未完成的报表状态');
  330. console.log('[SP-API Scheduler] 执行时间:', new Date().toISOString());
  331. console.log('[SP-API Scheduler] ========================================');
  332. try {
  333. // 1. 查询所有 processingStatus 不为 DONE 的报表
  334. const query = new Parse.Query('Reports');
  335. query.notEqualTo('processingStatus', 'DONE');
  336. if(shopid) {
  337. query.equalTo('shop', shopid);
  338. }
  339. query.limit(1000);
  340. const pendingReports = await query.find({ useMasterKey: true });
  341. if (pendingReports.length === 0) {
  342. console.log('[SP-API Scheduler] 没有未完成的报表');
  343. return;
  344. }
  345. console.log(`[SP-API Scheduler] 发现 ${pendingReports.length} 个未完成报表`);
  346. // 2. 逐个通过 reportId 获取最新状态
  347. const shopIdsToProcess = new Set();
  348. for (const report of pendingReports) {
  349. try {
  350. const reportId = report.get('reportId');
  351. const shopId = report.get('shop').id;
  352. if (!reportId || !shopId) continue;
  353. console.log(`[SP-API Scheduler] 检查报表 ${reportId} 状态...`);
  354. const getRes = await fetch('http://localhost:3000/api/amazon/forward', {
  355. method: 'POST',
  356. headers: { 'Content-Type': 'application/json', 'shop-objectid': shopId },
  357. body: JSON.stringify({
  358. path: `/reports/2021-06-30/reports/${reportId}`,
  359. method: 'GET'
  360. })
  361. });
  362. const reportData = await getRes.json();
  363. const reportInfo = reportData?.data || {};
  364. // 3. 更新 Reports 表
  365. const newStatus = reportInfo.processingStatus || report.get('processingStatus');
  366. report.set('processingStatus', newStatus);
  367. if (reportInfo.reportDocumentId) {
  368. report.set('reportDocumentId', reportInfo.reportDocumentId);
  369. }
  370. await report.save(null, { useMasterKey: true });
  371. console.log(`[SP-API Scheduler] 报表 ${reportId} 状态更新为: ${newStatus}`);
  372. if (newStatus === 'DONE') {
  373. shopIdsToProcess.add(shopId);
  374. }
  375. await this.#delay(10000);
  376. } catch (error) {
  377. console.error(`[SP-API Scheduler] 更新报表状态失败: ${error.message}`);
  378. }
  379. }
  380. // 4. 对有新完成报表的店铺执行 processUnparsedReports
  381. for (const shopId of shopIdsToProcess) {
  382. try {
  383. console.log(`[SP-API Scheduler] 解析店铺 ${shopId} 的已完成报表`);
  384. await this.#processUnparsedReports(shopId);
  385. } catch (error) {
  386. console.error(`[SP-API Scheduler] 店铺 ${shopId} 报表解析失败: ${error.message}`);
  387. }
  388. }
  389. console.log('[SP-API Scheduler] 未完成报表检查完毕');
  390. } catch (error) {
  391. console.error(`[SP-API Scheduler] 检查未完成报表失败: ${error.message}`);
  392. }
  393. }
  394. /**
  395. * 处理单个店铺的数据采集
  396. * @private
  397. * @param {any} shop - 店铺对象
  398. * @returns {Promise<void>}
  399. */
  400. async #processShop(shop){
  401. const shopId = shop.id;
  402. const shopName = shop.get('name');
  403. const config = shop.get('config');
  404. const marketplaceId = shop.get('marketplaceId');
  405. if (!config || !config.SpApiConfig) {
  406. throw new Error('店铺配置缺失');
  407. }
  408. // 1. 采集 Listing 数据
  409. try {
  410. console.log(`[SP-API Scheduler] 采集店铺 ${shopName} 的 Listing 数据`);
  411. await this.#collectListings(shopId, config, marketplaceId);
  412. await this.#delay(3000);
  413. } catch (error) {
  414. console.error(`[SP-API Scheduler] Listing 采集失败: ${error.message}`);
  415. }
  416. // 2. 采集订单数据(增量同步)
  417. try {
  418. console.log(`[SP-API Scheduler] 采集店铺 ${shopName} 的订单数据`);
  419. await this.#collectOrders(shopId, config, marketplaceId);
  420. await this.#delay(3000);
  421. } catch (error) {
  422. console.error(`[SP-API Scheduler] 订单采集失败: ${error.message}`);
  423. }
  424. // 3. 采集报表数据
  425. try {
  426. console.log(`[SP-API Scheduler] 采集店铺 ${shopName} 的报表数据`);
  427. await this.#collectReports(shopId, config, marketplaceId);
  428. await this.#delay(3000);
  429. } catch (error) {
  430. console.error(`[SP-API Scheduler] 报表采集失败: ${error.message}`);
  431. }
  432. // 4. 处理未解析的报表
  433. try {
  434. console.log(`[SP-API Scheduler] 处理店铺 ${shopName} 的未解析报表`);
  435. await this.#checkPendingReports(shopId);
  436. } catch (error) {
  437. console.error(`[SP-API Scheduler] 报表处理失败: ${error.message}`);
  438. }
  439. // 5. 更新店铺同步状态
  440. shop.set('lastSyncTime', new Date());
  441. shop.set('syncStatus', 'completed');
  442. await shop.save(null, { useMasterKey: true });
  443. }
  444. /**
  445. * 执行数据采集任务(核心方法:移到调用方之前)
  446. * @private
  447. * @returns {Promise<any>} 执行结果对象,包含成功数、失败数、耗时等信息
  448. */
  449. async #executeDataCollection(shopId) {
  450. if (this.#isRunning) {
  451. console.log('[SP-API Scheduler] 任务正在执行中,跳过本次调度');
  452. return {
  453. success: false,
  454. message: '任务正在执行中',
  455. successCount: 0,
  456. errorCount: 0,
  457. duration: 0,
  458. errors: []
  459. };
  460. }
  461. this.#isRunning = true;
  462. const startTime = Date.now();
  463. const errors = [];
  464. let successCount = 0;
  465. let errorCount = 0;
  466. console.log('[SP-API Scheduler] ========================================');
  467. console.log('[SP-API Scheduler] 开始执行每日数据采集任务');
  468. console.log('[SP-API Scheduler] 执行时间:', new Date().toISOString());
  469. console.log('[SP-API Scheduler] ========================================');
  470. try {
  471. // 1. 获取所有 Amazon 平台的活跃店铺
  472. const shops = await this.#getActiveAmazonShops(shopId);
  473. if (shops.length === 0) {
  474. console.log('[SP-API Scheduler] 没有找到活跃的 Amazon 店铺');
  475. return {
  476. success: true,
  477. message: '没有活跃店铺需要处理',
  478. successCount: 0,
  479. errorCount: 0,
  480. duration: Date.now() - startTime,
  481. errors: []
  482. };
  483. }
  484. console.log(`[SP-API Scheduler] 发现 ${shops.length} 个 Amazon 店铺`);
  485. // 2. 循环处理每个店铺
  486. for (const shop of shops) {
  487. try {
  488. console.log(`\n[SP-API Scheduler] ----------------------------------------`);
  489. console.log(`[SP-API Scheduler] 开始处理店铺: ${shop.get('name')} (${shop.id})`);
  490. await this.#processShop(shop);
  491. successCount++;
  492. console.log(`[SP-API Scheduler] 店铺 ${shop.get('name')} 处理完成`);
  493. await this.#delay(2000);
  494. } catch (error) {
  495. errorCount++;
  496. const errorMsg = `店铺 ${shop.get('name')} 处理失败: ${error.message}`;
  497. errors.push(errorMsg);
  498. console.error(`[SP-API Scheduler] ${errorMsg}`);
  499. }
  500. }
  501. // 3. 记录执行日志
  502. const duration = Date.now() - startTime;
  503. await this.#logExecution({
  504. taskName: 'sp-api-daily-sync',
  505. startTime: new Date(startTime),
  506. endTime: new Date(),
  507. duration,
  508. successCount,
  509. errorCount,
  510. errors,
  511. status: errorCount === 0 ? 'success' : (successCount > 0 ? 'partial_success' : 'failed')
  512. });
  513. this.#lastRunTime = new Date();
  514. const message = `数据采集完成: 成功 ${successCount}/${shops.length} 个店铺,耗时 ${Math.round(duration / 1000)}秒`;
  515. console.log(`\n[SP-API Scheduler] ========================================`);
  516. console.log(`[SP-API Scheduler] ${message}`);
  517. console.log(`[SP-API Scheduler] ========================================\n`);
  518. return {
  519. success: errorCount === 0,
  520. message,
  521. successCount,
  522. errorCount,
  523. duration,
  524. errors
  525. };
  526. } catch (error) {
  527. const duration = Date.now() - startTime;
  528. const errorMsg = `数据采集任务执行失败: ${error.message}`;
  529. console.error(`[SP-API Scheduler] ${errorMsg}`);
  530. return {
  531. success: false,
  532. message: errorMsg,
  533. successCount,
  534. errorCount: errorCount + 1,
  535. duration,
  536. errors: [...errors, errorMsg]
  537. };
  538. } finally {
  539. this.#isRunning = false;
  540. }
  541. }
  542. // ========== 公共方法(后声明,因为依赖前面的私有方法) ==========
  543. /**
  544. * 构造函数
  545. * 初始化 Amazon SP-API 数据采集调度器
  546. */
  547. constructor() {
  548. console.log('[SP-API Scheduler] 初始化 Amazon SP-API 数据采集调度器');
  549. }
  550. /**
  551. * 启动定时任务
  552. * 每天凌晨1点自动执行数据采集
  553. * @returns {Promise<void>}
  554. */
  555. async start() {
  556. if (this.#cronTask) {
  557. console.log('[SP-API Scheduler] 定时任务已在运行中');
  558. return;
  559. }
  560. // 每天凌晨1点执行 (Asia/Shanghai 时区)
  561. this.#cronTask = nodeCron.schedule('0 1 * * *', async () => {
  562. await this.#executeDataCollection();
  563. }, {
  564. timezone: "Asia/Shanghai"
  565. });
  566. console.log('[SP-API Scheduler] 定时任务已启动,将在每天凌晨1点执行');
  567. // 每天凌晨2点检查未完成的报表状态并解析
  568. this.#reportCronTask = nodeCron.schedule('0 2 * * *', async () => {
  569. await this.#checkPendingReports();
  570. }, {
  571. timezone: "Asia/Shanghai"
  572. });
  573. console.log('[SP-API Scheduler] 报表状态检查任务已启动,将在每天凌晨2点执行');
  574. // 测试模式:每5分钟执行一次(开发时可取消注释)
  575. // this.#cronTask = nodeCron.schedule('*/5 * * * *', async () => {
  576. // await this.#executeDataCollection();
  577. // });
  578. // console.log('[SP-API Scheduler] 测试模式:每5分钟执行一次');
  579. }
  580. /**
  581. * 停止定时任务
  582. * @returns {void}
  583. */
  584. stop(){
  585. if (this.#cronTask) {
  586. this.#cronTask.stop();
  587. this.#cronTask = null;
  588. console.log('[SP-API Scheduler] 数据采集定时任务已停止');
  589. }
  590. if (this.#reportCronTask) {
  591. this.#reportCronTask.stop();
  592. this.#reportCronTask = null;
  593. console.log('[SP-API Scheduler] 报表状态检查定时任务已停止');
  594. }
  595. }
  596. /**
  597. * 获取调度器状态
  598. * @returns {object} 调度器状态对象
  599. */
  600. getStatus(){
  601. const nextRunTime = this.#calculateNextRunTime();
  602. return {
  603. isRunning: this.#isRunning,
  604. lastRunTime: this.#lastRunTime,
  605. nextRunTime,
  606. cronEnabled: this.#cronEnabled
  607. };
  608. }
  609. /**
  610. * 手动触发数据采集
  611. * @returns {Promise<any>} 执行结果对象
  612. */
  613. async triggerManually(shopId) {
  614. console.log('[SP-API Scheduler] 手动触发数据采集任务');
  615. return await this.#executeDataCollection(shopId); // 此时#executeDataCollection已声明
  616. }
  617. }
  618. // 导出单例
  619. export const spApiScheduler = new SpApiScheduler();