deploy-runtime-routes.js 194 KB

1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889909192939495969798991001011021031041051061071081091101111121131141151161171181191201211221231241251261271281291301311321331341351361371381391401411421431441451461471481491501511521531541551561571581591601611621631641651661671681691701711721731741751761771781791801811821831841851861871881891901911921931941951961971981992002012022032042052062072082092102112122132142152162172182192202212222232242252262272282292302312322332342352362372382392402412422432442452462472482492502512522532542552562572582592602612622632642652662672682692702712722732742752762772782792802812822832842852862872882892902912922932942952962972982993003013023033043053063073083093103113123133143153163173183193203213223233243253263273283293303313323333343353363373383393403413423433443453463473483493503513523533543553563573583593603613623633643653663673683693703713723733743753763773783793803813823833843853863873883893903913923933943953963973983994004014024034044054064074084094104114124134144154164174184194204214224234244254264274284294304314324334344354364374384394404414424434444454464474484494504514524534544554564574584594604614624634644654664674684694704714724734744754764774784794804814824834844854864874884894904914924934944954964974984995005015025035045055065075085095105115125135145155165175185195205215225235245255265275285295305315325335345355365375385395405415425435445455465475485495505515525535545555565575585595605615625635645655665675685695705715725735745755765775785795805815825835845855865875885895905915925935945955965975985996006016026036046056066076086096106116126136146156166176186196206216226236246256266276286296306316326336346356366376386396406416426436446456466476486496506516526536546556566576586596606616626636646656666676686696706716726736746756766776786796806816826836846856866876886896906916926936946956966976986997007017027037047057067077087097107117127137147157167177187197207217227237247257267277287297307317327337347357367377387397407417427437447457467477487497507517527537547557567577587597607617627637647657667677687697707717727737747757767777787797807817827837847857867877887897907917927937947957967977987998008018028038048058068078088098108118128138148158168178188198208218228238248258268278288298308318328338348358368378388398408418428438448458468478488498508518528538548558568578588598608618628638648658668678688698708718728738748758768778788798808818828838848858868878888898908918928938948958968978988999009019029039049059069079089099109119129139149159169179189199209219229239249259269279289299309319329339349359369379389399409419429439449459469479489499509519529539549559569579589599609619629639649659669679689699709719729739749759769779789799809819829839849859869879889899909919929939949959969979989991000100110021003100410051006100710081009101010111012101310141015101610171018101910201021102210231024102510261027102810291030103110321033103410351036103710381039104010411042104310441045104610471048104910501051105210531054105510561057105810591060106110621063106410651066106710681069107010711072107310741075107610771078107910801081108210831084108510861087108810891090109110921093109410951096109710981099110011011102110311041105110611071108110911101111111211131114111511161117111811191120112111221123112411251126112711281129113011311132113311341135113611371138113911401141114211431144114511461147114811491150115111521153115411551156115711581159116011611162116311641165116611671168116911701171117211731174117511761177117811791180118111821183118411851186118711881189119011911192119311941195119611971198119912001201120212031204120512061207120812091210121112121213121412151216121712181219122012211222122312241225122612271228122912301231123212331234123512361237123812391240124112421243124412451246124712481249125012511252125312541255125612571258125912601261126212631264126512661267126812691270127112721273127412751276127712781279128012811282128312841285128612871288128912901291129212931294129512961297129812991300130113021303130413051306130713081309131013111312131313141315131613171318131913201321132213231324132513261327132813291330133113321333133413351336133713381339134013411342134313441345134613471348134913501351135213531354135513561357135813591360136113621363136413651366136713681369137013711372137313741375137613771378137913801381138213831384138513861387138813891390139113921393139413951396139713981399140014011402140314041405140614071408140914101411141214131414141514161417141814191420142114221423142414251426142714281429143014311432143314341435143614371438143914401441144214431444144514461447144814491450145114521453145414551456145714581459146014611462146314641465146614671468146914701471147214731474147514761477147814791480148114821483148414851486148714881489149014911492149314941495149614971498149915001501150215031504150515061507150815091510151115121513151415151516151715181519152015211522152315241525152615271528152915301531153215331534153515361537153815391540154115421543154415451546154715481549155015511552155315541555155615571558155915601561156215631564156515661567156815691570157115721573157415751576157715781579158015811582158315841585158615871588158915901591159215931594159515961597159815991600160116021603160416051606160716081609161016111612161316141615161616171618161916201621162216231624162516261627162816291630163116321633163416351636163716381639164016411642164316441645164616471648164916501651165216531654165516561657165816591660166116621663166416651666166716681669167016711672167316741675167616771678167916801681168216831684168516861687168816891690169116921693169416951696169716981699170017011702170317041705170617071708170917101711171217131714171517161717171817191720172117221723172417251726172717281729173017311732173317341735173617371738173917401741174217431744174517461747174817491750175117521753175417551756175717581759176017611762176317641765176617671768176917701771177217731774177517761777177817791780178117821783178417851786178717881789179017911792179317941795179617971798179918001801180218031804180518061807180818091810181118121813181418151816181718181819182018211822182318241825182618271828182918301831183218331834183518361837183818391840184118421843184418451846184718481849185018511852185318541855185618571858185918601861186218631864186518661867186818691870187118721873187418751876187718781879188018811882188318841885188618871888188918901891189218931894189518961897189818991900190119021903190419051906190719081909191019111912191319141915191619171918191919201921192219231924192519261927192819291930193119321933193419351936193719381939194019411942194319441945194619471948194919501951195219531954195519561957195819591960196119621963196419651966196719681969197019711972197319741975197619771978197919801981198219831984198519861987198819891990199119921993199419951996199719981999200020012002200320042005200620072008200920102011201220132014201520162017201820192020202120222023202420252026202720282029203020312032203320342035203620372038203920402041204220432044204520462047204820492050205120522053205420552056205720582059206020612062206320642065206620672068206920702071207220732074207520762077207820792080208120822083208420852086208720882089209020912092209320942095209620972098209921002101210221032104210521062107210821092110211121122113211421152116211721182119212021212122212321242125212621272128212921302131213221332134213521362137213821392140214121422143214421452146214721482149215021512152215321542155215621572158215921602161216221632164216521662167216821692170217121722173217421752176217721782179218021812182218321842185218621872188218921902191219221932194219521962197219821992200220122022203220422052206220722082209221022112212221322142215221622172218221922202221222222232224222522262227222822292230223122322233223422352236223722382239224022412242224322442245224622472248224922502251225222532254225522562257225822592260226122622263226422652266226722682269227022712272227322742275227622772278227922802281228222832284228522862287228822892290229122922293229422952296229722982299230023012302230323042305230623072308230923102311231223132314231523162317231823192320232123222323232423252326232723282329233023312332233323342335233623372338233923402341234223432344234523462347234823492350235123522353235423552356235723582359236023612362236323642365236623672368236923702371237223732374237523762377237823792380238123822383238423852386238723882389239023912392239323942395239623972398239924002401240224032404240524062407240824092410241124122413241424152416241724182419242024212422242324242425242624272428242924302431243224332434243524362437243824392440244124422443244424452446244724482449245024512452245324542455245624572458245924602461246224632464246524662467246824692470247124722473247424752476247724782479248024812482248324842485248624872488248924902491249224932494249524962497249824992500250125022503250425052506250725082509251025112512251325142515251625172518251925202521252225232524252525262527252825292530253125322533253425352536253725382539254025412542254325442545254625472548254925502551255225532554255525562557255825592560256125622563256425652566256725682569257025712572257325742575257625772578257925802581258225832584258525862587258825892590259125922593259425952596259725982599260026012602260326042605260626072608260926102611261226132614261526162617261826192620262126222623262426252626262726282629263026312632263326342635263626372638263926402641264226432644264526462647264826492650265126522653265426552656265726582659266026612662266326642665266626672668266926702671267226732674267526762677267826792680268126822683268426852686268726882689269026912692269326942695269626972698269927002701270227032704270527062707270827092710271127122713271427152716271727182719272027212722272327242725272627272728272927302731273227332734273527362737273827392740274127422743274427452746274727482749275027512752275327542755275627572758275927602761276227632764276527662767276827692770277127722773277427752776277727782779278027812782278327842785278627872788278927902791279227932794279527962797279827992800280128022803280428052806280728082809281028112812281328142815281628172818281928202821282228232824282528262827282828292830283128322833283428352836283728382839284028412842284328442845284628472848284928502851285228532854285528562857285828592860286128622863286428652866286728682869287028712872287328742875287628772878287928802881288228832884288528862887288828892890289128922893289428952896289728982899290029012902290329042905290629072908290929102911291229132914291529162917291829192920292129222923292429252926292729282929293029312932293329342935293629372938293929402941294229432944294529462947294829492950295129522953295429552956295729582959296029612962296329642965296629672968296929702971297229732974297529762977297829792980298129822983298429852986298729882989299029912992299329942995299629972998299930003001300230033004300530063007300830093010301130123013301430153016301730183019302030213022302330243025302630273028302930303031303230333034303530363037303830393040304130423043304430453046304730483049305030513052305330543055305630573058305930603061306230633064306530663067306830693070307130723073307430753076307730783079308030813082308330843085308630873088308930903091309230933094309530963097309830993100310131023103310431053106310731083109311031113112311331143115311631173118311931203121312231233124312531263127312831293130313131323133313431353136313731383139314031413142314331443145314631473148314931503151315231533154315531563157315831593160316131623163316431653166316731683169317031713172317331743175317631773178317931803181318231833184318531863187318831893190319131923193319431953196319731983199320032013202320332043205320632073208320932103211321232133214321532163217321832193220322132223223322432253226322732283229323032313232323332343235323632373238323932403241324232433244324532463247324832493250325132523253325432553256325732583259326032613262326332643265326632673268326932703271327232733274327532763277327832793280328132823283328432853286328732883289329032913292329332943295329632973298329933003301330233033304330533063307330833093310331133123313331433153316331733183319332033213322332333243325332633273328332933303331333233333334333533363337333833393340334133423343334433453346334733483349335033513352335333543355335633573358335933603361336233633364336533663367336833693370337133723373337433753376337733783379338033813382338333843385338633873388338933903391339233933394339533963397339833993400340134023403340434053406340734083409341034113412341334143415341634173418341934203421342234233424342534263427342834293430343134323433343434353436343734383439344034413442344334443445344634473448344934503451345234533454345534563457345834593460346134623463346434653466346734683469347034713472347334743475347634773478347934803481348234833484348534863487348834893490349134923493349434953496349734983499350035013502350335043505350635073508350935103511351235133514351535163517351835193520352135223523352435253526352735283529353035313532353335343535353635373538353935403541354235433544354535463547354835493550355135523553355435553556355735583559356035613562356335643565356635673568356935703571357235733574357535763577357835793580358135823583358435853586358735883589359035913592359335943595359635973598359936003601360236033604360536063607360836093610361136123613361436153616361736183619362036213622362336243625362636273628362936303631363236333634363536363637363836393640364136423643364436453646364736483649365036513652365336543655365636573658365936603661366236633664366536663667366836693670367136723673367436753676367736783679368036813682368336843685368636873688368936903691369236933694369536963697369836993700370137023703370437053706370737083709371037113712371337143715371637173718371937203721372237233724372537263727372837293730373137323733373437353736373737383739374037413742374337443745374637473748374937503751375237533754375537563757375837593760376137623763376437653766376737683769377037713772377337743775377637773778377937803781378237833784378537863787378837893790379137923793379437953796379737983799380038013802380338043805380638073808380938103811381238133814381538163817381838193820382138223823382438253826382738283829383038313832383338343835383638373838383938403841384238433844384538463847384838493850385138523853385438553856385738583859386038613862386338643865386638673868386938703871387238733874387538763877387838793880388138823883388438853886388738883889389038913892389338943895389638973898389939003901390239033904390539063907390839093910391139123913391439153916391739183919392039213922392339243925392639273928392939303931393239333934393539363937393839393940394139423943394439453946394739483949395039513952395339543955395639573958395939603961396239633964396539663967396839693970397139723973397439753976397739783979398039813982398339843985398639873988398939903991399239933994399539963997399839994000400140024003400440054006400740084009401040114012401340144015401640174018401940204021402240234024402540264027402840294030403140324033403440354036403740384039404040414042404340444045404640474048404940504051405240534054405540564057405840594060406140624063406440654066406740684069407040714072407340744075407640774078407940804081408240834084408540864087408840894090409140924093409440954096409740984099410041014102410341044105410641074108410941104111411241134114411541164117411841194120412141224123412441254126412741284129413041314132413341344135413641374138413941404141414241434144414541464147414841494150415141524153415441554156415741584159416041614162416341644165416641674168416941704171417241734174417541764177417841794180418141824183418441854186418741884189419041914192419341944195419641974198419942004201420242034204420542064207420842094210421142124213421442154216421742184219422042214222422342244225422642274228422942304231423242334234423542364237423842394240424142424243424442454246424742484249425042514252425342544255425642574258425942604261426242634264426542664267426842694270427142724273427442754276427742784279428042814282428342844285428642874288428942904291429242934294429542964297429842994300430143024303430443054306430743084309431043114312431343144315431643174318431943204321432243234324432543264327432843294330433143324333433443354336433743384339434043414342434343444345434643474348434943504351435243534354435543564357435843594360436143624363436443654366436743684369437043714372437343744375437643774378437943804381438243834384438543864387438843894390439143924393439443954396439743984399440044014402440344044405440644074408440944104411441244134414441544164417441844194420442144224423442444254426442744284429443044314432443344344435443644374438443944404441444244434444444544464447444844494450445144524453445444554456445744584459446044614462446344644465446644674468446944704471447244734474447544764477447844794480448144824483448444854486448744884489449044914492449344944495449644974498449945004501450245034504450545064507450845094510451145124513451445154516451745184519452045214522452345244525452645274528452945304531453245334534453545364537453845394540454145424543454445454546454745484549455045514552455345544555455645574558455945604561456245634564456545664567456845694570457145724573457445754576457745784579458045814582458345844585458645874588458945904591459245934594459545964597459845994600460146024603460446054606460746084609461046114612461346144615461646174618461946204621462246234624462546264627462846294630463146324633463446354636463746384639464046414642464346444645464646474648464946504651465246534654465546564657465846594660466146624663466446654666466746684669467046714672467346744675467646774678467946804681468246834684468546864687468846894690469146924693469446954696469746984699470047014702470347044705470647074708470947104711471247134714471547164717471847194720472147224723472447254726472747284729473047314732473347344735473647374738473947404741474247434744474547464747
  1. // backend/api/routes.ts
  2. import express4 from "npm:express";
  3. import { getStoredRelayAuth, isUsableAuth } from "../src/relay/relay-runtime.js";
  4. import relayCredentialRouter from "./relay/credential-routes.js";
  5. // backend/modules/fmode-amazon-sp-api/src/routes.ts
  6. import { Router } from "express";
  7. // backend/modules/fmode-amazon-sp-api/src/marketplace-helper.ts
  8. var ENDPOINTS = {
  9. NA: "https://sellingpartnerapi-na.amazon.com",
  10. EU: "https://sellingpartnerapi-eu.amazon.com",
  11. FE: "https://sellingpartnerapi-fe.amazon.com"
  12. };
  13. var SANDBOX_ENDPOINTS = {
  14. NA: "https://sandbox.sellingpartnerapi-na.amazon.com",
  15. EU: "https://sandbox.sellingpartnerapi-eu.amazon.com",
  16. FE: "https://sandbox.sellingpartnerapi-fe.amazon.com"
  17. };
  18. var MARKETPLACE_REGION_MAP = {
  19. // 北美
  20. "A2EUQ1WTGCTBG2": "NA",
  21. // 加拿大
  22. "ATVPDKIKX0DER": "NA",
  23. // 美国
  24. "A1AM78C64UM0Y8": "NA",
  25. // 墨西哥
  26. "A2Q3Y263D00KWC": "NA",
  27. // 巴西
  28. // 欧洲
  29. "A1RKKUPIHCS9HS": "EU",
  30. // 西班牙
  31. "A1F83G8C2ARO7P": "EU",
  32. // 英国
  33. "A13V1IB3VIYZZH": "EU",
  34. // 法国
  35. "A1805IZSGTT6HS": "EU",
  36. // 荷兰
  37. "A1PA6795UKMFR9": "EU",
  38. // 德国
  39. "APJ6JRA9NG5V4": "EU",
  40. // 意大利
  41. "A2NODRKZP88ZB9": "EU",
  42. // 瑞典
  43. "AE08WJ6EMKS27": "EU",
  44. // 埃及
  45. "A1C3SOZRARQ6R3": "EU",
  46. // 波兰
  47. "AMEN7PMS3EDWL": "EU",
  48. // 比利时
  49. "A28R8C7NBKEWEA": "EU",
  50. // 爱尔兰
  51. "A33AVAJ2PDY3EV": "EU",
  52. // 土耳其
  53. "A17E79C6D8DWNP": "EU",
  54. // 沙特阿拉伯
  55. "A2VIGQ35RCS4UG": "EU",
  56. // 阿联酋
  57. "ARBP9OOSHTCHU": "EU",
  58. // 埃及 (重复?)
  59. "A21TJRUUN4KGV": "EU",
  60. // 印度
  61. // 远东
  62. "A1VC38T7YXB528": "FE",
  63. // 日本
  64. "A39IBJ37TRP1C6": "FE",
  65. // 澳大利亚
  66. "A19VAU5U5O7RUS": "FE"
  67. // 新加坡
  68. };
  69. var getEndpointByMarketplaceId = (marketplaceId, isSandbox = false) => {
  70. const region = MARKETPLACE_REGION_MAP[marketplaceId] || "NA";
  71. const endpoints = isSandbox ? SANDBOX_ENDPOINTS : ENDPOINTS;
  72. return endpoints[region];
  73. };
  74. var getEndpointByRegion = (regionCode, isSandbox = false) => {
  75. const endpoints = isSandbox ? SANDBOX_ENDPOINTS : ENDPOINTS;
  76. const awsRegionMap = {
  77. "us-east-1": "NA",
  78. "eu-west-1": "EU",
  79. "us-west-2": "FE"
  80. };
  81. const regionKey = awsRegionMap[regionCode] || regionCode;
  82. if (endpoints[regionKey]) {
  83. return endpoints[regionKey];
  84. }
  85. return endpoints.NA;
  86. };
  87. // backend/modules/fmode-amazon-sp-api/src/client.ts
  88. var SpApiClient = class {
  89. // 配置在构造函数中现在是可选的,但可以传递用于遗留/静态用法
  90. // 然而,对于基于 Parse 的多租户用法,我们依赖于方法参数。
  91. // 为了保持向后兼容性并支持新的动态模式,我们可以使用混合方法。
  92. // 对于动态用法,我们将主要不使用 this.config。
  93. staticConfig;
  94. // RDT Token 缓存 (内存缓存,按 shopId 和 resourceKey 存储)
  95. rdtCache = /* @__PURE__ */ new Map();
  96. constructor(config) {
  97. this.staticConfig = config;
  98. }
  99. /**
  100. * 获取基础 URL
  101. */
  102. getBaseUrl(marketplaceId, isSandbox, region) {
  103. if (marketplaceId) {
  104. return getEndpointByMarketplaceId(marketplaceId, isSandbox);
  105. }
  106. if (region) {
  107. return getEndpointByRegion(region, isSandbox);
  108. }
  109. if (this.staticConfig) {
  110. const REGION_ENDPOINTS = {
  111. "us-east-1": "https://sellingpartnerapi-na.amazon.com",
  112. "eu-west-1": "https://sellingpartnerapi-eu.amazon.com",
  113. "us-west-2": "https://sellingpartnerapi-fe.amazon.com"
  114. };
  115. const SANDBOX_ENDPOINTS2 = {
  116. "us-east-1": "https://sandbox.sellingpartnerapi-na.amazon.com",
  117. "eu-west-1": "https://sandbox.sellingpartnerapi-eu.amazon.com",
  118. "us-west-2": "https://sandbox.sellingpartnerapi-fe.amazon.com"
  119. };
  120. const endpoints = this.staticConfig.sandbox ? SANDBOX_ENDPOINTS2 : REGION_ENDPOINTS;
  121. return endpoints[this.staticConfig.region] || endpoints["us-east-1"];
  122. }
  123. throw new Error("\u65E0\u6CD5\u786E\u5B9A SP-API \u7AEF\u70B9\uFF0CMarketplaceId \u4E3A\u5FC5\u586B\u9879\u3002");
  124. }
  125. /**
  126. * 获取 LWA Access Token
  127. * 先从 Shop 表获取缓存的 token,如果不存在或过期则刷新
  128. */
  129. async getAccessToken(context) {
  130. if (context) {
  131. const { shopId, config } = context;
  132. return this.refreshAccessToken(
  133. shopId,
  134. config.clientId,
  135. config.clientSecret,
  136. config.refreshToken
  137. );
  138. }
  139. throw new Error("\u7F3A\u5C11 SpApiClient \u914D\u7F6E\u3002");
  140. }
  141. /**
  142. * 刷新 Token 逻辑
  143. * 1. 先从 Shop 表的 config.SpApiConfig.accessToken 获取
  144. * 2. 检查是否过期(通过 config.SpApiConfig.accessTokenExpiresAt)
  145. * 3. 如果不存在或过期,则请求新的 token
  146. * 4. 将新 token 和过期时间(50分钟后)保存回 Shop 表
  147. */
  148. async refreshAccessToken(shopId, clientId, clientSecret, refreshToken) {
  149. try {
  150. const Parse = globalThis.Parse;
  151. if (!Parse) {
  152. console.warn("[SP-API] Parse SDK not available, will fetch new token");
  153. }
  154. if (Parse) {
  155. try {
  156. const shopQuery = new Parse.Query("Shop");
  157. const shop = await shopQuery.get(shopId, { useMasterKey: true });
  158. if (shop) {
  159. const config = shop.get("config") || {};
  160. const spApiConfig = config.SpApiConfig || {};
  161. const cachedToken = spApiConfig.accessToken;
  162. const expiresAt = spApiConfig.accessTokenExpiresAt;
  163. if (cachedToken && expiresAt) {
  164. const now = /* @__PURE__ */ new Date();
  165. const expiryDate = new Date(expiresAt);
  166. if (expiryDate > now) {
  167. const remainingMinutes = Math.floor(
  168. (expiryDate.getTime() - now.getTime()) / 1e3 / 60
  169. );
  170. console.log(`[SP-API] \u4F7F\u7528\u7F13\u5B58\u7684 Access Token (\u5269\u4F59 ${remainingMinutes} \u5206\u949F)`);
  171. return cachedToken;
  172. } else {
  173. console.log("[SP-API] \u7F13\u5B58\u7684 Access Token \u5DF2\u8FC7\u671F\uFF0C\u6B63\u5728\u5237\u65B0...");
  174. }
  175. } else {
  176. console.log("[SP-API] \u672A\u627E\u5230\u7F13\u5B58\u7684 Access Token\uFF0C\u6B63\u5728\u83B7\u53D6\u65B0\u7684...");
  177. }
  178. }
  179. } catch (error) {
  180. console.warn("[SP-API] \u4ECE Shop \u8868\u83B7\u53D6 token \u5931\u8D25:", error.message);
  181. }
  182. }
  183. console.log("[SP-API] \u6B63\u5728\u5237\u65B0 Access Token...");
  184. const params = new URLSearchParams({
  185. grant_type: "refresh_token",
  186. refresh_token: refreshToken,
  187. client_id: clientId,
  188. client_secret: clientSecret
  189. // scope: 'sellingpartnerapi::migration​'
  190. });
  191. const response = await fetch("https://api.amazon.com/auth/o2/token", {
  192. method: "POST",
  193. headers: {
  194. "Content-Type": "application/x-www-form-urlencoded"
  195. },
  196. body: params
  197. });
  198. if (!response.ok) {
  199. const errorText = await response.text();
  200. throw new Error(
  201. `Token refresh failed: ${response.status} ${response.statusText} - ${errorText}`
  202. );
  203. }
  204. const data = await response.json();
  205. console.log("[SP-API] \u5237\u65B0 Access Token \u6210\u529F\u3002");
  206. if (Parse) {
  207. try {
  208. const shopQuery = new Parse.Query("Shop");
  209. const shop = await shopQuery.get(shopId, { useMasterKey: true });
  210. if (shop) {
  211. const config = shop.get("config") || {};
  212. const spApiConfig = config.SpApiConfig || {};
  213. const expiresAt = /* @__PURE__ */ new Date();
  214. expiresAt.setMinutes(expiresAt.getMinutes() + 50);
  215. spApiConfig.accessToken = data.access_token;
  216. spApiConfig.accessTokenExpiresAt = expiresAt.toISOString();
  217. config.SpApiConfig = spApiConfig;
  218. shop.set("config", config);
  219. await shop.save(null, { useMasterKey: true });
  220. console.log(
  221. `[SP-API] Access Token \u5DF2\u4FDD\u5B58\u5230 Shop \u8868 (\u8FC7\u671F\u65F6\u95F4: ${expiresAt.toISOString()})`
  222. );
  223. }
  224. } catch (error) {
  225. console.error("[SP-API] \u4FDD\u5B58 token \u5230 Shop \u8868\u5931\u8D25:", error.message);
  226. }
  227. }
  228. return data.access_token;
  229. } catch (error) {
  230. console.error("[SP-API] \u5237\u65B0 token \u5931\u8D25:", error);
  231. throw error;
  232. }
  233. }
  234. /**
  235. * 获取 RDT Token (带缓存)
  236. * @param shopId 店铺ID
  237. * @param accessToken LWA Access Token
  238. * @param method HTTP 方法
  239. * @param path API 路径
  240. * @param dataElements 可选的数据元素列表
  241. * @param marketplaceId 市场ID
  242. * @param isSandbox 是否沙箱环境
  243. * @param region AWS 区域
  244. */
  245. async getRestrictedDataToken(accessToken, baseUrl, method, path, dataElements) {
  246. console.log(`[SP-API] \u6B63\u5728\u83B7\u53D6 RDT Token for ${method} ${path}...`);
  247. const rdtEndpoint = `${baseUrl}/tokens/2021-03-01/restrictedDataToken`;
  248. const cleanPath = path.startsWith("/") ? path : `/${path}`;
  249. const requestBody = {
  250. restrictedResources: [
  251. {
  252. method,
  253. path: cleanPath
  254. }
  255. ]
  256. };
  257. console.log(`[SP-API] RDT \u8BF7\u6C42\u8BE6\u60C5:`, {
  258. endpoint: rdtEndpoint,
  259. requestBody: JSON.stringify(requestBody, null, 2),
  260. accessToken: accessToken.substring(0, 20) + "..."
  261. });
  262. try {
  263. const response = await fetch(rdtEndpoint, {
  264. method: "POST",
  265. headers: {
  266. "x-amz-access-token": accessToken,
  267. "Content-Type": "application/json"
  268. },
  269. body: JSON.stringify(requestBody)
  270. });
  271. const responseText = await response.text();
  272. console.log(`[SP-API] RDT \u54CD\u5E94\u72B6\u6001: ${response.status}`, responseText);
  273. if (!response.ok) {
  274. let errorData;
  275. try {
  276. errorData = JSON.parse(responseText);
  277. } catch (e) {
  278. errorData = responseText;
  279. }
  280. if (response.status === 403) {
  281. throw new Error(
  282. `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)}`
  283. );
  284. } else if (response.status === 400) {
  285. throw new Error(`RDT \u8BF7\u6C42\u53C2\u6570\u9519\u8BEF: ${JSON.stringify(errorData)}`);
  286. } else if (response.status === 500) {
  287. throw new Error(
  288. `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`
  289. );
  290. }
  291. throw new Error(
  292. `RDT request failed: ${response.status} ${response.statusText} - ${JSON.stringify(errorData)}`
  293. );
  294. }
  295. const data = JSON.parse(responseText);
  296. console.log(`[SP-API] RDT Token \u83B7\u53D6\u6210\u529F (\u6709\u6548\u671F: ${data.expiresIn} \u79D2)`);
  297. return data.restrictedDataToken;
  298. } catch (error) {
  299. console.error("[SP-API] \u83B7\u53D6 RDT Token \u5931\u8D25:", error.message);
  300. throw error;
  301. }
  302. }
  303. /**
  304. * 发送 SP-API 请求
  305. */
  306. async request(options) {
  307. const accessToken = await this.getAccessToken(options.context);
  308. let marketplaceId = options.context?.marketplaceId;
  309. if (!marketplaceId && options.query) {
  310. const key = Object.keys(options.query).find(
  311. (k) => k.toLowerCase() === "marketplaceids" || k.toLowerCase() === "marketplaceid"
  312. );
  313. if (key) {
  314. const val = options.query[key];
  315. marketplaceId = Array.isArray(val) ? val[0] : val?.split(",")[0];
  316. }
  317. }
  318. const isSandbox = options.context?.config.sandbox || this.staticConfig?.sandbox;
  319. const region = options.context?.config.region || this.staticConfig?.region;
  320. const baseUrl = this.getBaseUrl(marketplaceId, isSandbox, region);
  321. let urlString = baseUrl.replace(/\/$/, "") + (options.path.startsWith("/") ? options.path : "/" + options.path);
  322. const queryParts = [];
  323. if (options.query) {
  324. Object.entries(options.query).forEach(([key, value]) => {
  325. if (value !== void 0 && value !== null) {
  326. if ((key === "nextToken" || key === "pageToken") && typeof value === "string") {
  327. try {
  328. let valToUse = value;
  329. if (value.includes("%")) {
  330. try {
  331. valToUse = decodeURIComponent(value);
  332. } catch (e) {
  333. }
  334. }
  335. queryParts.push(`${key}=${encodeURIComponent(valToUse)}`);
  336. } catch (e) {
  337. queryParts.push(`${key}=${encodeURIComponent(value)}`);
  338. }
  339. } else {
  340. const encodedVal = encodeURIComponent(String(value)).replace(/%2C/g, ",");
  341. queryParts.push(`${key}=${encodedVal}`);
  342. }
  343. }
  344. });
  345. }
  346. if (queryParts.length > 0) {
  347. urlString += (urlString.includes("?") ? "&" : "?") + queryParts.join("&");
  348. }
  349. let rdtToken;
  350. if (options.requiresRdt && options.context) {
  351. console.log(`[SP-API] \u68C0\u6D4B\u5230\u9700\u8981 RDT Token \u7684 API: ${options.path}`);
  352. try {
  353. rdtToken = await this.getRestrictedDataToken(
  354. accessToken,
  355. baseUrl,
  356. options.method,
  357. options.path,
  358. options.rdtDataElements
  359. );
  360. console.log("[SP-API] RDT Token \u83B7\u53D6\u6210\u529F");
  361. } catch (error) {
  362. console.error("[SP-API] \u83B7\u53D6 RDT Token \u5931\u8D25\uFF0C\u5C06\u4F7F\u7528\u666E\u901A Access Token:", error.message);
  363. }
  364. }
  365. const headers = {
  366. "x-amz-access-token": rdtToken || accessToken,
  367. // 优先使用 RDT Token
  368. "Content-Type": "application/json",
  369. "User-Agent": "fmode-amazon-sp-api/1.0",
  370. ...options.headers || {}
  371. };
  372. if (rdtToken) {
  373. console.log("[SP-API] \u4F7F\u7528 RDT Token \u8BBF\u95EE\u53D7\u9650\u6570\u636E");
  374. }
  375. let retries = 0;
  376. const maxRetries = 3;
  377. while (retries < maxRetries) {
  378. try {
  379. console.log(`[SP-API] \u8BF7\u6C42: ${options.method} ${urlString}`, {
  380. query: options.query,
  381. shopId: options.context?.shopId
  382. });
  383. const fetchOptions = {
  384. method: options.method,
  385. headers
  386. };
  387. if (options.body && options.method !== "GET") {
  388. fetchOptions.body = JSON.stringify(options.body);
  389. }
  390. const response = await fetch(urlString, fetchOptions);
  391. if (!response.ok) {
  392. if (response.status === 429) {
  393. retries++;
  394. if (retries >= maxRetries)
  395. throw new Error(`Rate limit exceeded after ${maxRetries} retries`);
  396. const retryAfterHeader = response.headers.get("retry-after");
  397. const retryAfter = retryAfterHeader ? parseInt(retryAfterHeader, 10) : 0;
  398. const delay = retryAfter ? retryAfter * 1e3 : Math.pow(2, retries) * 1e3;
  399. console.log(`[SP-API] \u901F\u7387\u9650\u5236\u3002${delay}ms \u540E\u91CD\u8BD5... (\u5C1D\u8BD5 ${retries}/${maxRetries})`);
  400. await new Promise((resolve) => setTimeout(resolve, delay));
  401. continue;
  402. }
  403. if (response.status >= 500) {
  404. retries++;
  405. if (retries >= maxRetries)
  406. throw new Error(`Server error ${response.status} after ${maxRetries} retries`);
  407. const delay = Math.pow(2, retries) * 1e3;
  408. console.log(
  409. `[SP-API] \u670D\u52A1\u5668\u9519\u8BEF ${response.status}\u3002${delay}ms \u540E\u91CD\u8BD5... (\u5C1D\u8BD5 ${retries}/${maxRetries})`
  410. );
  411. await new Promise((resolve) => setTimeout(resolve, delay));
  412. continue;
  413. }
  414. const errorText = await response.text();
  415. let errorData;
  416. try {
  417. errorData = JSON.parse(errorText);
  418. } catch (e) {
  419. errorData = errorText;
  420. }
  421. console.error(`[SP-API] \u9519\u8BEF: ${options.method} ${urlString}`, {
  422. status: response.status,
  423. data: errorData,
  424. statusText: response.statusText
  425. });
  426. console.error(errorData);
  427. const error = new Error(
  428. `Request failed with status ${response.status}: ${JSON.stringify(errorData)}`
  429. );
  430. error.response = {
  431. status: response.status,
  432. data: errorData,
  433. headers: response.headers
  434. };
  435. throw error;
  436. }
  437. const responseText = await response.text();
  438. if (!responseText.trim())
  439. return {};
  440. return JSON.parse(responseText);
  441. } catch (error) {
  442. if (error.response) {
  443. throw error;
  444. }
  445. console.error(`[SP-API] \u7F51\u7EDC/\u672A\u77E5\u9519\u8BEF: ${error.message}`);
  446. throw error;
  447. }
  448. }
  449. throw new Error("\u8D85\u8FC7\u6700\u5927\u91CD\u8BD5\u6B21\u6570");
  450. }
  451. };
  452. // backend/modules/fmode-amazon-sp-api/src/parse-init.ts
  453. var isInitialized = false;
  454. var getParse = async () => {
  455. let Parse = globalThis.Parse;
  456. if (!Parse) {
  457. try {
  458. const mod = await import("parse/lib/node/Parse.js");
  459. Parse = mod.default || mod;
  460. } catch {
  461. const mod2 = await import("parse");
  462. Parse = mod2.default || mod2;
  463. }
  464. const appId = process.env.PARSE_APP_ID || "TARGET_PARSE_APP_ID";
  465. const serverURL = process.env.PARSE_SERVER_URL || "http://localhost:3000/parse";
  466. const masterKey = process.env.PARSE_MASTER_KEY || "TARGET_PARSE_MASTER_KEY";
  467. Parse.initialize(appId, void 0, masterKey);
  468. Parse.serverURL = serverURL;
  469. globalThis.Parse = Parse;
  470. isInitialized = true;
  471. }
  472. return Parse;
  473. };
  474. // backend/modules/fmode-amazon-sp-api/src/middleware/auth.ts
  475. var shopAuthMiddleware = async (req, res, next) => {
  476. if (req.path === "/test") {
  477. return next();
  478. }
  479. try {
  480. const shopId = req.headers["shop-objectid"] || req.headers["x-shop-objectid"];
  481. if (!shopId) {
  482. return res.status(400).json({
  483. success: false,
  484. message: "\u7F3A\u5C11\u5FC5\u9700\u7684 header: shop-objectId"
  485. });
  486. }
  487. const Parse = await getParse();
  488. const query = new Parse.Query("Shop");
  489. let shop;
  490. try {
  491. shop = await query.get(shopId, { useMasterKey: true });
  492. console.log("[SP-API] \u8BA4\u8BC1\u4E2D\u95F4\u4EF6:", shop.get("name"));
  493. } catch (error) {
  494. console.log(error);
  495. return res.status(400).json({
  496. success: false,
  497. message: `\u65E0\u6548\u7684 shopId: ${shopId}`
  498. });
  499. }
  500. const config = shop.get("config").SpApiConfig;
  501. console.log("[SP-API] \u5E97\u94FA\u51ED\u8BC1\u5DF2\u52A0\u8F7D");
  502. if (!config || !config.clientId || !config.clientSecret || !config.refreshToken) {
  503. return res.status(400).json({
  504. success: false,
  505. message: "\u7F3A\u5C11\u4E9A\u9A6C\u900A\u914D\u7F6E\u4FE1\u606F"
  506. });
  507. }
  508. req.spApiContext = {
  509. shopId,
  510. marketplaceId: shop.get("marketplaceId"),
  511. config
  512. };
  513. next();
  514. } catch (error) {
  515. console.error("[SP-API] \u8BA4\u8BC1\u4E2D\u95F4\u4EF6\u9519\u8BEF:", error);
  516. res.status(500).json({
  517. success: false,
  518. message: "\u5E97\u94FA\u8BA4\u8BC1\u671F\u95F4\u53D1\u751F\u5185\u90E8\u670D\u52A1\u5668\u9519\u8BEF",
  519. error: error.message
  520. });
  521. }
  522. };
  523. // backend/modules/fmode-amazon-sp-api/src/api/customerFeedback.ts
  524. var CustomerFeedbackApi = class {
  525. constructor(client) {
  526. this.client = client;
  527. }
  528. /**
  529. * 检索某件商品的十个最正面和十个最负面的评论主题。
  530. * GET /customerFeedback/2024-06-01/items/{asin}/reviews/topics
  531. */
  532. async getItemReviewTopics(params) {
  533. const { asin, ...queryParams } = params;
  534. return this.client.request({
  535. method: "GET",
  536. path: `/customerFeedback/2024-06-01/items/${asin}/reviews/topics`,
  537. query: queryParams
  538. });
  539. }
  540. /**
  541. * 该API返回请求ASIN的关联浏览节点。
  542. * GET /customerFeedback/2024-06-01/items/{asin}/browseNode
  543. */
  544. async getItemBrowseNode(params) {
  545. const { asin, ...queryParams } = params;
  546. return this.client.request({
  547. method: "GET",
  548. path: `/customerFeedback/2024-06-01/items/${asin}/browseNode`,
  549. query: queryParams
  550. });
  551. }
  552. /**
  553. * 检索浏览节点中十个最正面和十个最负面的评论主题。
  554. * GET /customerFeedback/2024-06-01/browseNodes/{browseNodeId}/reviews/topics
  555. */
  556. async getBrowseNodeReviewTopics(params) {
  557. const { browseNodeId, ...queryParams } = params;
  558. return this.client.request({
  559. method: "GET",
  560. path: `/customerFeedback/2024-06-01/browseNodes/${browseNodeId}/reviews/topics`,
  561. query: queryParams
  562. });
  563. }
  564. /**
  565. * 检索过去六个月商品的正面和负面评价趋势。
  566. * GET /customerFeedback/2024-06-01/items/{asin}/reviews/trends
  567. */
  568. async getItemReviewTrends(params) {
  569. const { asin, ...queryParams } = params;
  570. return this.client.request({
  571. method: "GET",
  572. path: `/customerFeedback/2024-06-01/items/${asin}/reviews/trends`,
  573. query: queryParams
  574. });
  575. }
  576. /**
  577. * 检索过去六个月浏览节点的正面和负面评价趋势。
  578. * GET /customerFeedback/2024-06-01/browseNodes/{browseNodeId}/reviews/trends
  579. */
  580. async getBrowseNodeReviewTrends(params) {
  581. const { browseNodeId, ...queryParams } = params;
  582. return this.client.request({
  583. method: "GET",
  584. path: `/customerFeedback/2024-06-01/browseNodes/${browseNodeId}/reviews/trends`,
  585. query: queryParams
  586. });
  587. }
  588. /**
  589. * 检索客户在浏览节点退货时提到的主题。
  590. * GET /customerFeedback/2024-06-01/browseNodes/{browseNodeId}/returns/topics
  591. */
  592. async getBrowseNodeReturnTopics(params) {
  593. const { browseNodeId, ...queryParams } = params;
  594. return this.client.request({
  595. method: "GET",
  596. path: `/customerFeedback/2024-06-01/browseNodes/${browseNodeId}/returns/topics`,
  597. query: queryParams
  598. });
  599. }
  600. /**
  601. * 检索客户在浏览节点退货时提到的话题趋势。
  602. * GET /customerFeedback/2024-06-01/browseNodes/{browseNodeId}/returns/trends
  603. */
  604. async getBrowseNodeReturnTrends(params) {
  605. const { browseNodeId, ...queryParams } = params;
  606. return this.client.request({
  607. method: "GET",
  608. path: `/customerFeedback/2024-06-01/browseNodes/${browseNodeId}/returns/trends`,
  609. query: queryParams
  610. });
  611. }
  612. };
  613. // backend/modules/fmode-amazon-sp-api/src/api/orders.ts
  614. var OrdersApi = class {
  615. constructor(client) {
  616. this.client = client;
  617. }
  618. /**
  619. * 获取订单列表
  620. * GET /orders/v0/orders
  621. */
  622. async getOrders(params) {
  623. return this.client.request({
  624. method: "GET",
  625. path: "/orders/v0/orders",
  626. query: params
  627. });
  628. }
  629. /**
  630. * 获取订单详情
  631. * GET /orders/v0/orders/{orderId}
  632. */
  633. async getOrder(orderId) {
  634. return this.client.request({
  635. method: "GET",
  636. path: `/orders/v0/orders/${orderId}`
  637. });
  638. }
  639. /**
  640. * 获取订单商品
  641. * GET /orders/v0/orders/{orderId}/orderItems
  642. */
  643. async getOrderItems(orderId, nextToken) {
  644. return this.client.request({
  645. method: "GET",
  646. path: `/orders/v0/orders/${orderId}/orderItems`,
  647. query: nextToken ? { NextToken: nextToken } : void 0
  648. });
  649. }
  650. };
  651. // backend/modules/fmode-amazon-sp-api/src/api/sales.ts
  652. var SalesApi = class {
  653. constructor(client) {
  654. this.client = client;
  655. }
  656. /**
  657. * 回报给定买方类型的给定区间内汇总订单指标
  658. * GET /sales/v1/orderMetrics
  659. */
  660. async getOrderMetrics(params) {
  661. return this.client.request({
  662. method: "GET",
  663. path: "/sales/v1/orderMetrics",
  664. query: params
  665. });
  666. }
  667. };
  668. // backend/modules/fmode-amazon-sp-api/src/api/listings.ts
  669. var ListingsApi = class {
  670. constructor(client) {
  671. this.client = client;
  672. }
  673. /**
  674. * 列出所有列表项
  675. * GET /listings/2021-08-01/items/{sellerId}
  676. */
  677. async searchListingsItems(params) {
  678. const { sellerId, ...queryParams } = params;
  679. return this.client.request({
  680. method: "GET",
  681. path: `/listings/2021-08-01/items/${sellerId}`,
  682. query: queryParams
  683. });
  684. }
  685. /**
  686. * 获取列表项详情
  687. * GET /listings/2021-08-01/items/{sellerId}/{sku}
  688. */
  689. async getListingsItem(params) {
  690. const { sellerId, sku, ...queryParams } = params;
  691. return this.client.request({
  692. method: "GET",
  693. path: `/listings/2021-08-01/items/${sellerId}/${sku}`,
  694. query: queryParams
  695. });
  696. }
  697. /**
  698. * Put (创建/更新) 列表项
  699. * PUT /listings/2021-08-01/items/{sellerId}/{sku}
  700. */
  701. async putListingsItem(sellerId, sku, marketplaceIds, body) {
  702. return this.client.request({
  703. method: "PUT",
  704. path: `/listings/2021-08-01/items/${sellerId}/${sku}`,
  705. query: { marketplaceIds: marketplaceIds.join(",") },
  706. body
  707. });
  708. }
  709. /**
  710. * 删除列表项
  711. * DELETE /listings/2021-08-01/items/{sellerId}/{sku}
  712. */
  713. async deleteListingsItem(sellerId, sku, marketplaceIds) {
  714. return this.client.request({
  715. method: "DELETE",
  716. path: `/listings/2021-08-01/items/${sellerId}/${sku}`,
  717. query: { marketplaceIds: marketplaceIds.join(",") }
  718. });
  719. }
  720. };
  721. // backend/modules/fmode-amazon-sp-api/src/api/externalFulfillment.ts
  722. var ExternalFulfillmentApi = class {
  723. constructor(client) {
  724. this.client = client;
  725. }
  726. /**
  727. * 退货列表
  728. * GET /externalFulfillment/2024-09-11/returns
  729. */
  730. async listReturns(params) {
  731. return this.client.request({
  732. method: "GET",
  733. path: "/externalFulfillment/2024-09-11/returns",
  734. query: params
  735. });
  736. }
  737. /**
  738. * 获取退货详情
  739. * GET /externalFulfillment/2024-09-11/returns/{returnId}
  740. */
  741. async getReturn(returnId) {
  742. return this.client.request({
  743. method: "GET",
  744. path: `/externalFulfillment/2024-09-11/returns/${returnId}`
  745. });
  746. }
  747. };
  748. // backend/modules/fmode-amazon-sp-api/src/api/catalogItems.ts
  749. var CatalogItemsApi = class {
  750. constructor(client) {
  751. this.client = client;
  752. }
  753. /**
  754. * 列表目录分类
  755. * GET /catalog/v0/categories
  756. */
  757. async listCatalogCategories(params) {
  758. return this.client.request({
  759. method: "GET",
  760. path: "/catalog/v0/categories",
  761. query: params
  762. });
  763. }
  764. /**
  765. * 搜索亚马逊目录商品
  766. * GET /catalog/2022-04-01/items
  767. */
  768. async searchCatalogItems(params) {
  769. return this.client.request({
  770. method: "GET",
  771. path: "/catalog/2022-04-01/items",
  772. query: params
  773. });
  774. }
  775. /**
  776. * 获取类目信息
  777. * GET /catalog/2022-04-01/items/{asin}
  778. */
  779. async getCatalogItem(params) {
  780. const { asin, ...queryParams } = params;
  781. return this.client.request({
  782. method: "GET",
  783. path: `/catalog/2022-04-01/items/${asin}`,
  784. query: queryParams
  785. });
  786. }
  787. };
  788. // backend/modules/fmode-amazon-sp-api/src/api/sellers.ts
  789. var SellersApi = class {
  790. constructor(client) {
  791. this.client = client;
  792. }
  793. /**
  794. * 返回卖家账户及其市场的信息
  795. * GET /sellers/v1/account
  796. */
  797. async getAccount() {
  798. return this.client.request({
  799. method: "GET",
  800. path: "/sellers/v1/account"
  801. });
  802. }
  803. /**
  804. * 返回卖家可以列出商品的市场列表以及卖家在这些市场的参与信息
  805. * GET /sellers/v1/marketplaceParticipations
  806. */
  807. async getMarketplaceParticipations() {
  808. return this.client.request({
  809. method: "GET",
  810. path: "/sellers/v1/marketplaceParticipations"
  811. });
  812. }
  813. };
  814. // backend/modules/fmode-amazon-sp-api/src/function.ts
  815. function parseNumber(value) {
  816. if (!value || value.trim() === "")
  817. return 0;
  818. const num = parseFloat(value.replace(/[^0-9.-]/g, ""));
  819. return isNaN(num) ? 0 : num;
  820. }
  821. function parseIntegerValue(value) {
  822. if (!value || value.trim() === "")
  823. return 0;
  824. const num = parseInt(value.replace(/[^0-9-]/g, ""));
  825. return isNaN(num) ? 0 : num;
  826. }
  827. function parseDate(value) {
  828. if (!value || value.trim() === "")
  829. return null;
  830. try {
  831. const date = new Date(value);
  832. return isNaN(date.getTime()) ? null : date;
  833. } catch (error) {
  834. return null;
  835. }
  836. }
  837. async function runCloudFunction(data, context, functionId) {
  838. if (!functionId) {
  839. throw new Error("functionId is required");
  840. }
  841. try {
  842. console.log(`[SP-API] Running cloud function: ${functionId}`);
  843. const response = await fetch("http://localhost:3000/api/functions", {
  844. method: "POST",
  845. headers: {
  846. "Content-Type": "application/json"
  847. },
  848. body: JSON.stringify({
  849. id: functionId,
  850. data,
  851. context
  852. })
  853. });
  854. if (!response.ok) {
  855. const errorData = await response.json().catch(() => ({ message: response.statusText }));
  856. throw new Error(`Cloud function failed: ${errorData.message || response.statusText}`);
  857. }
  858. const result = await response.json();
  859. console.log(`[SP-API] Cloud function ${functionId} executed successfully`);
  860. return result;
  861. } catch (error) {
  862. console.error(`[SP-API] Cloud function ${functionId} execution failed:`, error.message);
  863. throw error;
  864. }
  865. }
  866. var functions = {
  867. /**
  868. * 处理 Amazon 报表文件
  869. * 下载、解析并存储报表数据到数据库
  870. * @param data 包含报表 URL 或报表信息的数据
  871. * @param context 上下文信息
  872. */
  873. async processReportFile(data, context) {
  874. const Parse = await getParse();
  875. if (!data || !data.reportDocumentUrl && !data.url) {
  876. console.warn("[SP-API] processReportFile \u7F3A\u5C11\u62A5\u8868 URL");
  877. return { success: false, message: "\u7F3A\u5C11\u62A5\u8868 URL", processed: 0 };
  878. }
  879. const shopId = context?.shopId;
  880. if (!shopId) {
  881. console.warn("[SP-API] processReportFile \u4E0A\u4E0B\u6587\u4E2D\u7F3A\u5C11 shopId");
  882. return { success: false, message: "\u7F3A\u5C11 shopId", processed: 0 };
  883. }
  884. const reportUrl = data.reportDocumentUrl || data.url;
  885. const reportType = data.reportType || "UNKNOWN";
  886. const reportId = data.reportId || "unknown";
  887. const compressionAlgorithm = data.compressionAlgorithm;
  888. console.log(`[SP-API] \u6B63\u5728\u5904\u7406\u62A5\u8868\u6587\u4EF6: ${reportType}`, {
  889. reportId,
  890. shopId,
  891. compressionAlgorithm: compressionAlgorithm || "NONE"
  892. });
  893. try {
  894. console.log("[SP-API] \u6B63\u5728\u4E0B\u8F7D\u62A5\u8868\u6587\u4EF6...");
  895. const response = await fetch(reportUrl);
  896. if (!response.ok) {
  897. throw new Error(`\u4E0B\u8F7D\u62A5\u8868\u5931\u8D25: ${response.status} ${response.statusText}`);
  898. }
  899. let reportContent;
  900. if (compressionAlgorithm && compressionAlgorithm !== "NONE") {
  901. console.log(`[SP-API] \u68C0\u6D4B\u5230\u538B\u7F29\u6587\u4EF6\uFF0C\u538B\u7F29\u7B97\u6CD5: ${compressionAlgorithm}`);
  902. const compressedData = await response.arrayBuffer();
  903. console.log(`[SP-API] \u538B\u7F29\u6587\u4EF6\u4E0B\u8F7D\u6210\u529F\uFF0C\u5927\u5C0F: ${compressedData.byteLength} \u5B57\u8282`);
  904. reportContent = await this.decompressReportData(compressedData, compressionAlgorithm);
  905. console.log(`[SP-API] \u6587\u4EF6\u89E3\u538B\u6210\u529F\uFF0C\u89E3\u538B\u540E\u5927\u5C0F: ${reportContent.length} \u5B57\u7B26`);
  906. } else {
  907. reportContent = await response.text();
  908. console.log(`[SP-API] \u62A5\u8868\u6587\u4EF6\u4E0B\u8F7D\u6210\u529F\uFF0C\u5927\u5C0F: ${reportContent.length} \u5B57\u7B26`);
  909. }
  910. let parsedData = [];
  911. let tableName = "Report";
  912. switch (reportType) {
  913. case "GET_MERCHANT_LISTINGS_DATA":
  914. parsedData = await this.parseMerchantListingsReport(reportContent);
  915. tableName = "Product";
  916. break;
  917. case "GET_FBA_FULFILLMENT_CUSTOMER_RETURNS_DATA":
  918. parsedData = await this.parseReturnsReport(reportContent);
  919. tableName = "ReturnRecord";
  920. break;
  921. case "GET_FLAT_FILE_ORDERS_RECONCILIATION_DATA":
  922. parsedData = await this.parseOrdersReconciliationReport(reportContent);
  923. tableName = "OrderReconciliation";
  924. break;
  925. case "GET_SALES_AND_TRAFFIC_REPORT":
  926. parsedData = await this.parseSalesTrafficReport(reportContent);
  927. tableName = "SalesTraffic";
  928. break;
  929. default:
  930. parsedData = await this.parseGenericReport(reportContent, reportType);
  931. tableName = "GenericReport";
  932. }
  933. if (parsedData.length === 0) {
  934. console.warn("[SP-API] \u62A5\u8868\u89E3\u6790\u540E\u6CA1\u6709\u6570\u636E");
  935. return { success: true, message: "\u62A5\u8868\u4E3A\u7A7A", processed: 0 };
  936. }
  937. const shopPointer = Parse.Object.extend("Shop").createWithoutData(shopId);
  938. const objectsToSave = [];
  939. for (const item of parsedData) {
  940. const reportObj = new Parse.Object(tableName);
  941. reportObj.set("shop", shopPointer);
  942. Object.keys(item).forEach((key) => {
  943. if (item[key] !== null && item[key] !== void 0 && item[key] !== "") {
  944. reportObj.set(key, item[key]);
  945. }
  946. });
  947. objectsToSave.push(reportObj);
  948. }
  949. const batchSize = 100;
  950. let totalSaved = 0;
  951. for (let i = 0; i < objectsToSave.length; i += batchSize) {
  952. const batch = objectsToSave.slice(i, i + batchSize);
  953. try {
  954. await Parse.Object.saveAll(batch, { useMasterKey: true });
  955. totalSaved += batch.length;
  956. console.log(`[SP-API] \u5DF2\u4FDD\u5B58 ${totalSaved}/${objectsToSave.length} \u6761\u8BB0\u5F55\u5230 ${tableName} \u8868`);
  957. } catch (error) {
  958. console.error(`[SP-API] \u4FDD\u5B58\u6279\u6B21 ${i}-${i + batch.length} \u5931\u8D25:`, error.message);
  959. }
  960. }
  961. console.log(`[SP-API] \u62A5\u8868\u5904\u7406\u5B8C\u6210: ${reportType}\uFF0C\u5171\u5904\u7406 ${totalSaved} \u6761\u8BB0\u5F55`);
  962. return {
  963. success: true,
  964. processed: totalSaved,
  965. reportType,
  966. reportId,
  967. tableName
  968. };
  969. } catch (error) {
  970. console.error("[SP-API] \u5904\u7406\u62A5\u8868\u6587\u4EF6\u5931\u8D25:", error.message);
  971. return {
  972. success: false,
  973. message: error.message,
  974. processed: 0
  975. };
  976. }
  977. },
  978. /**
  979. * 解析商品列表报表 (GET_MERCHANT_LISTINGS_DATA)
  980. */
  981. async parseMerchantListingsReport(content) {
  982. const lines = content.split("\n");
  983. if (lines.length < 2)
  984. return [];
  985. const headers = lines[0].split(" ");
  986. const results = [];
  987. for (let i = 1; i < lines.length; i++) {
  988. const line = lines[i].trim();
  989. if (!line)
  990. continue;
  991. const values = line.split(" ");
  992. const item = {};
  993. const rawData = {};
  994. headers.forEach((header, index) => {
  995. const value = values[index] || "";
  996. const cleanHeader = header.trim().replace(/[^a-zA-Z0-9_-]/g, "_").toLowerCase();
  997. rawData[cleanHeader] = value;
  998. rawData[header.trim()] = value;
  999. });
  1000. item.item_name = rawData["item-name"] || rawData["item_name"] || rawData["product-name"] || "";
  1001. item.listing_id = rawData["listing-id"] || rawData["listing_id"] || "";
  1002. item.seller_sku = rawData["seller-sku"] || rawData["seller_sku"] || rawData["sku"] || "";
  1003. item.asin1 = rawData["asin1"] || "";
  1004. item.asin2 = rawData["asin2"] || "";
  1005. item.asin3 = rawData["asin3"] || "";
  1006. item.product_id = rawData["product-id"] || rawData["product_id"] || "";
  1007. item.product_id_type = rawData["product-id-type"] || rawData["product_id_type"] || "1";
  1008. if (i == 1) {
  1009. console.log(item, rawData);
  1010. }
  1011. item.price = parseNumber(rawData["price"] || rawData["list-price"] || rawData["your-price"]);
  1012. item.quantity = parseIntegerValue(rawData["quantity"] || rawData["available-quantity"]);
  1013. item.pending_quantity = parseIntegerValue(rawData["pending-quantity"] || rawData["pending_quantity"]);
  1014. item.business_price = parseNumber(rawData["business-price"] || rawData["business_price"]);
  1015. item.quantity_price_type = rawData["quantity-price-type"] || rawData["quantity_price_type"] || "";
  1016. item.open_date = parseDate(rawData["open-date"] || rawData["open_date"] || rawData["date-created"]);
  1017. item.fulfillment_channel = rawData["fulfillment-channel"] || rawData["fulfillment_channel"] || "";
  1018. item.will_ship_internationally = rawData["will-ship-internationally"] || rawData["will_ship_internationally"] || "";
  1019. item.expedited_shipping = rawData["expedited-shipping"] || rawData["expedited_shipping"] || "";
  1020. item.zshop_shipping_fee = parseNumber(rawData["zshop-shipping-fee"] || rawData["zshop_shipping_fee"]);
  1021. item.merchant_shipping_group = rawData["merchant-shipping-group"] || rawData["merchant_shipping_group"] || "";
  1022. item.item_description = rawData["item-description"] || rawData["item_description"] || rawData["description"] || "";
  1023. item.item_note = rawData["item-note"] || rawData["item_note"] || "";
  1024. item.item_condition = rawData["item-condition"] || rawData["item_condition"] || rawData["condition"] || "";
  1025. item.zshop_category1 = rawData["zshop-category1"] || rawData["zshop_category1"] || "";
  1026. item.zshop_browse_path = rawData["zshop-browse-path"] || rawData["zshop_browse_path"] || "";
  1027. item.zshop_storefront_feature = rawData["zshop-storefront-feature"] || rawData["zshop_storefront_feature"] || "";
  1028. item.zshop_boldface = rawData["zshop-boldface"] || rawData["zshop_boldface"] || "";
  1029. item.image_url = rawData["image-url"] || rawData["image_url"] || rawData["main-image-url"] || "";
  1030. item.item_is_marketplace = rawData["item-is-marketplace"] || rawData["item_is_marketplace"] || "";
  1031. item.license_plate_number = rawData["license-plate-number"] || rawData["license_plate_number"] || "";
  1032. if (item.seller_sku || item.asin1 || item.item_name) {
  1033. results.push(item);
  1034. }
  1035. }
  1036. return results;
  1037. },
  1038. /**
  1039. * 解析退货报表 (GET_FBA_FULFILLMENT_CUSTOMER_RETURNS_DATA)
  1040. */
  1041. async parseReturnsReport(content) {
  1042. const lines = content.split("\n");
  1043. if (lines.length < 2)
  1044. return [];
  1045. const headers = lines[0].split(" ");
  1046. const results = [];
  1047. for (let i = 1; i < lines.length; i++) {
  1048. const line = lines[i].trim();
  1049. if (!line)
  1050. continue;
  1051. const values = line.split(" ");
  1052. const item = {};
  1053. headers.forEach((header, index) => {
  1054. const value = values[index] || "";
  1055. const cleanHeader = header.trim().replace(/[^a-zA-Z0-9]/g, "_");
  1056. if (header.includes("date") || header.includes("Date")) {
  1057. item[cleanHeader] = value ? new Date(value) : null;
  1058. } else if (header.includes("quantity") || header.includes("Quantity")) {
  1059. item[cleanHeader] = value ? parseInt(value) : 0;
  1060. } else {
  1061. item[cleanHeader] = value;
  1062. }
  1063. });
  1064. item.returnDate = item.return_date || null;
  1065. item.orderId = item.order_id || item.amazon_order_id || "";
  1066. item.sku = item.sku || item.seller_sku || "";
  1067. item.asin = item.asin || "";
  1068. item.fnsku = item.fnsku || "";
  1069. item.product_name = item.product_name || "";
  1070. item.fulfillment_center_id = item.fulfillment_center_id || "";
  1071. item.detailed_disposition = item.detailed_disposition || "";
  1072. item.reason = item.reason || "";
  1073. item.license_plate_number = item.license_plate_number || "";
  1074. item.customer_comments = item.customer_comments || "";
  1075. results.push(item);
  1076. }
  1077. return results;
  1078. },
  1079. /**
  1080. * 解析订单对账报表 (GET_FLAT_FILE_ORDERS_RECONCILIATION_DATA)
  1081. */
  1082. async parseOrdersReconciliationReport(content) {
  1083. const lines = content.split("\n");
  1084. if (lines.length < 2)
  1085. return [];
  1086. const headers = lines[0].split(" ");
  1087. const results = [];
  1088. for (let i = 1; i < lines.length; i++) {
  1089. const line = lines[i].trim();
  1090. if (!line)
  1091. continue;
  1092. const values = line.split(" ");
  1093. const item = {};
  1094. headers.forEach((header, index) => {
  1095. const value = values[index] || "";
  1096. const cleanHeader = header.trim().replace(/[^a-zA-Z0-9]/g, "_");
  1097. if (header.includes("amount") || header.includes("Amount") || header.includes("price") || header.includes("Price")) {
  1098. item[cleanHeader] = value ? parseFloat(value) : 0;
  1099. } else if (header.includes("quantity") || header.includes("Quantity")) {
  1100. item[cleanHeader] = value ? parseInt(value) : 0;
  1101. } else if (header.includes("date") || header.includes("Date")) {
  1102. item[cleanHeader] = value ? new Date(value) : null;
  1103. } else {
  1104. item[cleanHeader] = value;
  1105. }
  1106. });
  1107. results.push(item);
  1108. }
  1109. return results;
  1110. },
  1111. /**
  1112. * 解析销售流量报表 (GET_SALES_AND_TRAFFIC_REPORT)
  1113. */
  1114. async parseSalesTrafficReport(content) {
  1115. try {
  1116. const jsonData = JSON.parse(content);
  1117. const results = [];
  1118. if (jsonData.salesAndTrafficByDate) {
  1119. jsonData.salesAndTrafficByDate.forEach((item) => {
  1120. results.push({
  1121. date: new Date(item.date),
  1122. orderedProductSales: parseFloat(item.salesByDate?.orderedProductSales?.amount || 0),
  1123. orderedProductSalesB2B: parseFloat(item.salesByDate?.orderedProductSalesB2B?.amount || 0),
  1124. unitsOrdered: parseInt(item.salesByDate?.unitsOrdered || 0),
  1125. unitsOrderedB2B: parseInt(item.salesByDate?.unitsOrderedB2B || 0),
  1126. totalOrderItems: parseInt(item.salesByDate?.totalOrderItems || 0),
  1127. totalOrderItemsB2B: parseInt(item.salesByDate?.totalOrderItemsB2B || 0),
  1128. averageSalesPerOrderItem: parseFloat(item.salesByDate?.averageSalesPerOrderItem?.amount || 0),
  1129. averageUnitsPerOrderItem: parseFloat(item.salesByDate?.averageUnitsPerOrderItem || 0),
  1130. sessions: parseInt(item.trafficByDate?.sessions || 0),
  1131. sessionPercentage: parseFloat(item.trafficByDate?.sessionPercentage || 0),
  1132. pageViews: parseInt(item.trafficByDate?.pageViews || 0),
  1133. pageViewsPercentage: parseFloat(item.trafficByDate?.pageViewsPercentage || 0),
  1134. buyBoxPercentage: parseFloat(item.trafficByDate?.buyBoxPercentage || 0)
  1135. });
  1136. });
  1137. }
  1138. return results;
  1139. } catch (error) {
  1140. return this.parseGenericReport(content, "GET_SALES_AND_TRAFFIC_REPORT");
  1141. }
  1142. },
  1143. /**
  1144. * 通用报表解析器(CSV/TSV 格式)
  1145. */
  1146. async parseGenericReport(content, reportType) {
  1147. const lines = content.split("\n");
  1148. if (lines.length < 2)
  1149. return [];
  1150. const delimiter = lines[0].includes(" ") ? " " : ",";
  1151. const headers = lines[0].split(delimiter);
  1152. const results = [];
  1153. for (let i = 1; i < lines.length; i++) {
  1154. const line = lines[i].trim();
  1155. if (!line)
  1156. continue;
  1157. const values = line.split(delimiter);
  1158. const item = {};
  1159. headers.forEach((header, index) => {
  1160. const value = values[index] || "";
  1161. const cleanHeader = header.trim().replace(/[^a-zA-Z0-9]/g, "_");
  1162. if (value.match(/^\d{4}-\d{2}-\d{2}/) || value.match(/^\d{2}\/\d{2}\/\d{4}/)) {
  1163. item[cleanHeader] = new Date(value);
  1164. } else if (value.match(/^\d+\.\d+$/)) {
  1165. item[cleanHeader] = parseFloat(value);
  1166. } else if (value.match(/^\d+$/)) {
  1167. item[cleanHeader] = parseInt(value);
  1168. } else {
  1169. item[cleanHeader] = value;
  1170. }
  1171. });
  1172. item.reportType = reportType;
  1173. results.push(item);
  1174. }
  1175. return results;
  1176. },
  1177. /**
  1178. * 解压缩报表数据
  1179. * @param compressedData 压缩的二进制数据
  1180. * @param compressionAlgorithm 压缩算法 (GZIP)
  1181. * @returns 解压后的文本内容
  1182. */
  1183. async decompressReportData(compressedData, compressionAlgorithm) {
  1184. try {
  1185. if (compressionAlgorithm.toUpperCase() === "GZIP") {
  1186. const zlib = await import("zlib");
  1187. const util = await import("util");
  1188. const buffer = Buffer.from(compressedData);
  1189. const gunzip = util.promisify(zlib.gunzip);
  1190. const decompressed = await gunzip(buffer);
  1191. return decompressed.toString("utf-8");
  1192. } else {
  1193. throw new Error(`\u4E0D\u652F\u6301\u7684\u538B\u7F29\u7B97\u6CD5: ${compressionAlgorithm}`);
  1194. }
  1195. } catch (error) {
  1196. console.error(`[SP-API] \u89E3\u538B\u7F29\u5931\u8D25:`, error.message);
  1197. throw new Error(`\u89E3\u538B\u7F29\u5931\u8D25: ${error.message}`);
  1198. }
  1199. },
  1200. // 清洗Orders数据 并且通过Parse将数据存储到数据库里面
  1201. async cleanOrders(data, context) {
  1202. const Parse = await getParse();
  1203. if (!data || !data.payload || !data.payload.Orders) {
  1204. console.warn("[SP-API] cleanOrders \u63A5\u6536\u5230\u65E0\u6548\u7684\u6570\u636E\u7ED3\u6784:", JSON.stringify(data).substring(0, 200));
  1205. return { success: false, message: "\u65E0\u6548\u7684\u6570\u636E\u7ED3\u6784", processed: 0 };
  1206. }
  1207. const shopId = context?.shopId;
  1208. if (!shopId) {
  1209. console.warn("[SP-API] cleanOrders \u4E0A\u4E0B\u6587\u4E2D\u7F3A\u5C11 shopId");
  1210. return { success: false, message: "\u7F3A\u5C11 shopId", processed: 0 };
  1211. }
  1212. const orders = data.payload.Orders;
  1213. console.log("[SP-API] \u6B63\u5728\u6E05\u6D17\u8BA2\u5355...", orders.length, "\u4E2A\u8BA2\u5355\uFF0C\u5E97\u94FAID:", shopId);
  1214. const objectsToSave = [];
  1215. const shopPointer = Parse.Object.extend("Shop").createWithoutData(shopId);
  1216. const orderIds = orders.map((o) => o.AmazonOrderId);
  1217. let existingOrders = [];
  1218. try {
  1219. const query = new Parse.Query("Order");
  1220. query.containedIn("platformOrderId", orderIds);
  1221. query.equalTo("shop", shopPointer);
  1222. query.limit(1e3);
  1223. existingOrders = await query.find({ useMasterKey: true });
  1224. } catch (error) {
  1225. console.warn("[SP-API] \u67E5\u8BE2\u73B0\u6709\u8BA2\u5355\u8B66\u544A (\u5C06\u4F5C\u4E3A\u65B0\u8BA2\u5355\u7EE7\u7EED):", error);
  1226. existingOrders = [];
  1227. }
  1228. const existingMap = /* @__PURE__ */ new Map();
  1229. existingOrders.forEach((o) => existingMap.set(o.get("platformOrderId"), o));
  1230. for (const orderData of orders) {
  1231. let orderObj = existingMap.get(orderData.AmazonOrderId);
  1232. if (!orderObj) {
  1233. orderObj = new Parse.Object("Order");
  1234. orderObj.set("platformOrderId", orderData.AmazonOrderId);
  1235. orderObj.set("shop", shopPointer);
  1236. }
  1237. orderObj.set("orderDate", new Date(orderData.PurchaseDate));
  1238. orderObj.set("status", orderData.OrderStatus);
  1239. orderObj.set("marketplaceId", orderData.MarketplaceId);
  1240. if (orderData.OrderTotal && orderData.OrderTotal.Amount) {
  1241. orderObj.set("totalAmount", parseFloat(orderData.OrderTotal.Amount));
  1242. } else {
  1243. if (orderObj.isNew()) {
  1244. orderObj.set("totalAmount", 0);
  1245. }
  1246. }
  1247. if (orderData.BuyerInfo) {
  1248. orderObj.set("buyerInfo", orderData.BuyerInfo);
  1249. }
  1250. if (orderData.ShippingAddress) {
  1251. orderObj.set("shippingAddress", orderData.ShippingAddress);
  1252. orderObj.set("customerRegion", orderData.ShippingAddress.CountryCode);
  1253. }
  1254. if (orderObj.isNew()) {
  1255. orderObj.set("items", []);
  1256. }
  1257. orderObj.set("lastUpdateDate", new Date(orderData.LastUpdateDate));
  1258. orderObj.set("fulfillmentChannel", orderData.FulfillmentChannel);
  1259. orderObj.set("orderType", orderData.OrderType);
  1260. if (orderData.ShippingAddress) {
  1261. orderObj.set("shippingCountry", orderData.ShippingAddress.CountryCode);
  1262. orderObj.set("shippingState", orderData.ShippingAddress.StateOrRegion);
  1263. orderObj.set("shippingCity", orderData.ShippingAddress.City);
  1264. }
  1265. orderObj.set("isPrime", orderData.IsPrime || false);
  1266. orderObj.set("isBusinessOrder", orderData.IsBusinessOrder || false);
  1267. if (orderData.EarliestShipDate) {
  1268. orderObj.set("earliestShipDate", new Date(orderData.EarliestShipDate));
  1269. }
  1270. if (orderData.LatestShipDate) {
  1271. orderObj.set("latestShipDate", new Date(orderData.LatestShipDate));
  1272. }
  1273. objectsToSave.push(orderObj);
  1274. }
  1275. if (objectsToSave.length > 0) {
  1276. try {
  1277. await Parse.Object.saveAll(objectsToSave, { useMasterKey: true });
  1278. console.log(`[SP-API] \u6210\u529F\u4FDD\u5B58/\u66F4\u65B0 ${objectsToSave.length} \u4E2A\u8BA2\u5355\u3002`);
  1279. } catch (error) {
  1280. console.error("[SP-API] \u4FDD\u5B58\u8BA2\u5355\u9519\u8BEF:", error);
  1281. for (const obj of objectsToSave) {
  1282. try {
  1283. await obj.save(null, { useMasterKey: true });
  1284. } catch (e) {
  1285. console.error("\u4FDD\u5B58\u5355\u4E2A\u5BF9\u8C61\u9519\u8BEF:", obj.toJSON(), e);
  1286. }
  1287. }
  1288. return { success: false, message: "\u4FDD\u5B58\u5230\u6570\u636E\u5E93\u9519\u8BEF (\u90E8\u5206\u5931\u8D25\u8BF7\u68C0\u67E5\u65E5\u5FD7)", error };
  1289. }
  1290. }
  1291. const nextToken = data?.payload?.NextToken || data?.NextToken;
  1292. if (nextToken && context?.client) {
  1293. const currentNextToken = String(nextToken).trim();
  1294. const previousNextToken = context.previousNextToken;
  1295. if (previousNextToken && previousNextToken === currentNextToken) {
  1296. console.warn("[SP-API] \u68C0\u6D4B\u5230\u91CD\u590D\u7684 NextToken\uFF0C\u505C\u6B62\u5206\u9875\u4EE5\u9632\u6B62\u6B7B\u5FAA\u73AF\u3002");
  1297. return { success: true, processed: objectsToSave.length, hasNext: false, message: "Duplicate NextToken detected" };
  1298. }
  1299. try {
  1300. const originalPath = context.requestPath || "";
  1301. const [basePath, rawQuery] = originalPath.split("?");
  1302. const keysToRemove = ["NextToken", "nextToken", "pageToken", "offset", "page", "startIndex"];
  1303. let newQueryString = "";
  1304. if (rawQuery) {
  1305. const urlParams = new URLSearchParams(rawQuery);
  1306. const parts = [];
  1307. urlParams.forEach((value, key) => {
  1308. if (!keysToRemove.includes(key)) {
  1309. parts.push(`${key}=${encodeURIComponent(value)}`);
  1310. }
  1311. });
  1312. newQueryString = parts.join("&");
  1313. }
  1314. if (context.requestQuery) {
  1315. const extraParts = [];
  1316. Object.entries(context.requestQuery).forEach(([k, v]) => {
  1317. if (v === void 0 || v === null)
  1318. return;
  1319. if (!keysToRemove.includes(k) && !newQueryString.includes(`${k}=`)) {
  1320. const valStr = Array.isArray(v) ? v.join(",") : String(v);
  1321. extraParts.push(`${k}=${encodeURIComponent(valStr)}`);
  1322. }
  1323. });
  1324. if (extraParts.length > 0) {
  1325. newQueryString = newQueryString ? `${newQueryString}&${extraParts.join("&")}` : extraParts.join("&");
  1326. }
  1327. }
  1328. const encodedToken = encodeURIComponent(currentNextToken);
  1329. const nextPath = `${basePath}?NextToken=${encodedToken}`;
  1330. await new Promise((resolve) => setTimeout(resolve, 3e4));
  1331. const nextResult = await context.client.request({
  1332. method: "GET",
  1333. path: nextPath,
  1334. context: {
  1335. shopId: context.shopId,
  1336. marketplaceId: context.marketplaceId,
  1337. config: context.config
  1338. }
  1339. });
  1340. context.requestCount = (context.requestCount || 1) + 1;
  1341. context.previousNextToken = currentNextToken;
  1342. return await this.cleanOrders(nextResult, context);
  1343. } catch (e) {
  1344. console.error("[SP-API] \u83B7\u53D6\u4E0B\u4E00\u9875\u8BA2\u5355\u9519\u8BEF:", e);
  1345. }
  1346. }
  1347. return { success: true, processed: objectsToSave.length, hasNext: !!nextToken };
  1348. },
  1349. /**
  1350. * 清洗 Listings 数据并存储到数据库
  1351. * 对应 Parse Schema: Listing
  1352. * 字段设计:
  1353. * - shop: Pointer<Shop> 必填
  1354. * - sku: String (sku) - 卖家SKU
  1355. * - asin: String (summaries[0].asin) - 亚马逊ASIN 必填
  1356. * - title: String (summaries[0].itemName) - 商品标题 必填
  1357. * - mainImage: String (summaries[0].mainImage.link) - 主图链接
  1358. * - productType: String (summaries[0].productType) - 产品类型
  1359. * - status: String (summaries[0].status[0]) - 状态
  1360. * - createdDate: Date (summaries[0].createdDate)
  1361. * - lastUpdatedDate: Date (summaries[0].lastUpdatedDate)
  1362. * - marketplaceId: String (summaries[0].marketplaceId)
  1363. * - attributes: Object (attributes) - 其他属性
  1364. * - parentSku: String (attributes?.child_parent_sku_relationship?.[0]?.parent_sku) - 父SKU
  1365. * - issues: Array (issues) - 刊登问题
  1366. */
  1367. async cleanListings(data, context) {
  1368. const Parse = await getParse();
  1369. if (typeof Parse === "undefined") {
  1370. console.error("[SP-API] cleanListings \u5931\u8D25: \u5168\u5C40 Parse \u5BF9\u8C61\u4E0D\u5B58\u5728");
  1371. return { success: false, message: "\u5168\u5C40 Parse \u5BF9\u8C61\u4E0D\u5B58\u5728", processed: 0 };
  1372. }
  1373. if (!data || !data.items || !Array.isArray(data.items)) {
  1374. console.warn("[SP-API] cleanListings \u63A5\u6536\u5230\u65E0\u6548\u7684\u6570\u636E\u7ED3\u6784");
  1375. return { success: false, message: "\u65E0\u6548\u7684\u6570\u636E\u7ED3\u6784", processed: 0 };
  1376. }
  1377. const shopId = context?.shopId;
  1378. if (!shopId) {
  1379. console.warn("[SP-API] cleanListings \u4E0A\u4E0B\u6587\u4E2D\u7F3A\u5C11 shopId");
  1380. return { success: false, message: "\u7F3A\u5C11 shopId", processed: 0 };
  1381. }
  1382. const requestCount = context?.requestCount || 1;
  1383. console.log(`[SP-API] cleanListings \u7B2C ${requestCount} \u6B21\u5FAA\u73AF\u8BF7\u6C42`);
  1384. const items = data.items;
  1385. console.log(
  1386. `[SP-API] \u6B63\u5728\u6E05\u6D17 ${items.length} \u6761 Listing...`,
  1387. data.numberOfResults,
  1388. data.pagination
  1389. );
  1390. const shopPointer = Parse.Object.extend("Shop").createWithoutData(shopId);
  1391. const objectsToSave = [];
  1392. const skus = items.map((item) => item.sku);
  1393. console.log(`[SP-API] \u63D0\u53D6\u5230 ${skus.length} \u4E2A SKU \u7528\u4E8E\u67E5\u8BE2`, skus, context.requestCount);
  1394. let existingListings = [];
  1395. try {
  1396. const query = new Parse.Query("Listing");
  1397. query.containedIn("sku", skus);
  1398. query.equalTo("shop", shopPointer);
  1399. query.limit(data.numberOfResults);
  1400. existingListings = await query.find({ useMasterKey: true });
  1401. } catch (error) {
  1402. console.warn("[SP-API] \u67E5\u8BE2\u73B0\u6709 Listing \u8B66\u544A (\u5C06\u4F5C\u4E3A\u65B0 Listing \u7EE7\u7EED):", error);
  1403. }
  1404. const existingMap = /* @__PURE__ */ new Map();
  1405. existingListings.forEach((l) => existingMap.set(l.get("asin"), l));
  1406. for (const item of items) {
  1407. if (!item.summaries || item.summaries.length === 0)
  1408. continue;
  1409. let listingObj = existingMap.get(item.summaries[0].asin);
  1410. if (!listingObj) {
  1411. listingObj = new Parse.Object("Listing");
  1412. listingObj.set("sku", item.sku);
  1413. listingObj.set("shop", shopPointer);
  1414. }
  1415. if (item.attributes) {
  1416. const parentageList = item.attributes.parentage_level;
  1417. if (Array.isArray(parentageList)) {
  1418. const isChild = parentageList.some((p) => p.value === "child");
  1419. if (isChild) {
  1420. const relationships = item.attributes.child_parent_sku_relationship;
  1421. if (Array.isArray(relationships) && relationships.length > 0) {
  1422. const rel = relationships[0];
  1423. if (rel && rel.parent_sku) {
  1424. listingObj.set("parentSku", rel.parent_sku);
  1425. }
  1426. }
  1427. }
  1428. }
  1429. }
  1430. if (item.summaries && item.summaries.length > 0) {
  1431. const summary = item.summaries[0];
  1432. listingObj.set("asin", summary.asin);
  1433. listingObj.set("title", summary.itemName);
  1434. listingObj.set("productType", summary.productType);
  1435. listingObj.set("marketplaceId", summary.marketplaceId);
  1436. if (summary.status && summary.status.length > 0) {
  1437. listingObj.set("status", summary.status[0]);
  1438. }
  1439. if (summary.createdDate) {
  1440. listingObj.set("createdDate", new Date(summary.createdDate));
  1441. }
  1442. if (summary.lastUpdatedDate) {
  1443. listingObj.set("lastUpdatedDate", new Date(summary.lastUpdatedDate));
  1444. }
  1445. if (summary.mainImage && summary.mainImage.link) {
  1446. listingObj.set("mainImage", summary.mainImage.link);
  1447. }
  1448. }
  1449. if (item.attributes) {
  1450. listingObj.set("attributes", item.attributes);
  1451. }
  1452. if (item.issues) {
  1453. listingObj.set("issues", item.issues);
  1454. }
  1455. if (item.attributes?.list_price && item.attributes?.list_price.length > 0) {
  1456. const listPrice = item.attributes?.list_price[0];
  1457. if (listPrice.value) {
  1458. listingObj.set("price", parseFloat(listPrice.value));
  1459. listingObj.set("currency", listPrice.currency);
  1460. }
  1461. }
  1462. if (item.fulfillmentAvailability && item.fulfillmentAvailability.length > 0) {
  1463. const fa = item.fulfillmentAvailability[0];
  1464. listingObj.set("quantity", fa.quantity);
  1465. }
  1466. objectsToSave.push(listingObj);
  1467. }
  1468. if (objectsToSave.length > 0) {
  1469. try {
  1470. await Parse.Object.saveAll(objectsToSave, { useMasterKey: true });
  1471. console.log(`[SP-API] \u6210\u529F\u4FDD\u5B58/\u66F4\u65B0 ${objectsToSave.length} \u6761 Listing\u3002`);
  1472. } catch (error) {
  1473. console.error("[SP-API] \u4FDD\u5B58 Listing \u9519\u8BEF:", error);
  1474. }
  1475. }
  1476. if (context.requestCount === 50) {
  1477. console.log(data.pagination, data.pagination.nextToken, context?.client);
  1478. }
  1479. if (data.pagination && data.pagination.nextToken && context?.client) {
  1480. const currentNextToken = data.pagination.nextToken;
  1481. const previousNextToken = context.previousNextToken;
  1482. if (previousNextToken && previousNextToken === currentNextToken) {
  1483. console.warn("[SP-API] \u68C0\u6D4B\u5230\u91CD\u590D\u7684 nextToken\uFF0C\u505C\u6B62\u5206\u9875\u4EE5\u9632\u6B62\u6B7B\u5FAA\u73AF\u3002");
  1484. return { success: true, processed: objectsToSave.length, hasNext: false, message: "Duplicate nextToken detected" };
  1485. }
  1486. const pageSize = items.length || 10;
  1487. const maxRequests = Math.ceil((data.numberOfResults || 1e4) / pageSize) + 10;
  1488. if (requestCount >= maxRequests) {
  1489. console.warn(`[SP-API] \u5DF2\u8FBE\u5230\u6700\u5927\u8BF7\u6C42\u6B21\u6570 ${maxRequests}\uFF0C\u505C\u6B62\u5206\u9875\u3002`);
  1490. return { success: true, processed: objectsToSave.length, hasNext: true, message: `Reached max requests limit (${maxRequests})` };
  1491. }
  1492. console.log(`[SP-API] \u6B63\u5728\u83B7\u53D6\u4E0B\u4E00\u9875 Listing... (\u7B2C ${requestCount + 1} \u9875)`);
  1493. try {
  1494. const originalPath = context.requestPath || "";
  1495. const [basePath, rawQuery] = originalPath.split("?");
  1496. let newQueryString = "";
  1497. const keysToRemove = ["pageToken", "nextToken", "offset", "page", "startIndex"];
  1498. if (rawQuery) {
  1499. newQueryString = rawQuery.split("&").filter(Boolean).filter((part) => !keysToRemove.includes(decodeURIComponent(part.split("=")[0] || ""))).join("&");
  1500. }
  1501. if (context.requestQuery) {
  1502. const extraParts = [];
  1503. Object.entries(context.requestQuery).forEach(([k, v]) => {
  1504. if (v === void 0 || v === null)
  1505. return;
  1506. if (!keysToRemove.includes(k)) {
  1507. if (!newQueryString.includes(`${k}=`)) {
  1508. const valStr = Array.isArray(v) ? v.join(",") : String(v);
  1509. extraParts.push(`${k}=${encodeURIComponent(valStr)}`);
  1510. }
  1511. }
  1512. });
  1513. if (extraParts.length > 0) {
  1514. newQueryString = newQueryString ? `${newQueryString}&${extraParts.join("&")}` : extraParts.join("&");
  1515. }
  1516. }
  1517. const tokenVal = currentNextToken.trim();
  1518. console.log(`[SP-API] NextToken (Page ${requestCount + 1}):`, tokenVal.substring(0, 50) + "...");
  1519. const encodedToken = encodeURIComponent(tokenVal);
  1520. const marketplaceQuery = context.marketplaceId ? `marketplaceIds=${encodeURIComponent(String(context.marketplaceId))}&` : "";
  1521. const nextPath = `${basePath}?${marketplaceQuery}pageToken=${encodedToken}`;
  1522. console.log("[SP-API] Next Path:", nextPath.substring(0, 200) + "...");
  1523. await new Promise((resolve) => setTimeout(resolve, 3e3));
  1524. const nextResult = await context.client.request({
  1525. method: "GET",
  1526. path: nextPath,
  1527. context: {
  1528. shopId,
  1529. marketplaceId: context.marketplaceId,
  1530. config: context.config
  1531. }
  1532. });
  1533. context.requestCount = requestCount + 1;
  1534. context.previousNextToken = currentNextToken;
  1535. return await this.cleanListings(nextResult, context);
  1536. } catch (error) {
  1537. console.error("[SP-API] \u83B7\u53D6\u4E0B\u4E00\u9875\u9519\u8BEF:", error.message);
  1538. return {
  1539. success: true,
  1540. processed: objectsToSave.length,
  1541. hasNext: true,
  1542. error: error.message,
  1543. message: "\u5206\u9875\u8BF7\u6C42\u5931\u8D25\uFF0C\u5DF2\u4FDD\u5B58\u5F53\u524D\u9875\u6570\u636E"
  1544. };
  1545. }
  1546. }
  1547. return { success: true, processed: objectsToSave.length, hasNext: false };
  1548. }
  1549. };
  1550. // backend/modules/fmode-amazon-sp-api/src/routes.ts
  1551. var sendResponse = (res, data) => {
  1552. res.json({
  1553. success: true,
  1554. data,
  1555. timestamp: (/* @__PURE__ */ new Date()).toISOString()
  1556. });
  1557. };
  1558. var asyncHandler = (fn) => (req, res, next) => {
  1559. Promise.resolve(fn(req, res, next)).catch(next);
  1560. };
  1561. var createSpApiRouter = (staticClient) => {
  1562. const router5 = Router();
  1563. router5.use(shopAuthMiddleware);
  1564. const getClient = (req) => {
  1565. return staticClient || new SpApiClient();
  1566. };
  1567. const createScopedApi = (ApiClass, req) => {
  1568. const baseClient = getClient(req);
  1569. const scopedClient = {
  1570. request: async (options) => {
  1571. return baseClient.request({
  1572. ...options,
  1573. context: {
  1574. shopId: req.spApiContext?.shopId,
  1575. marketplaceId: req.spApiContext?.marketplaceId,
  1576. config: req.spApiContext?.config
  1577. }
  1578. });
  1579. }
  1580. };
  1581. return new ApiClass(scopedClient);
  1582. };
  1583. router5.get(
  1584. "/test",
  1585. asyncHandler(async (req, res) => {
  1586. sendResponse(res, { message: "SP-API \u8DEF\u7531\u6D4B\u8BD5\u6210\u529F" });
  1587. })
  1588. );
  1589. router5.get(
  1590. "/customerFeedback/items/:asin/reviews/topics",
  1591. asyncHandler(async (req, res) => {
  1592. const api = createScopedApi(CustomerFeedbackApi, req);
  1593. const result = await api.getItemReviewTopics({ ...req.query, asin: req.params.asin });
  1594. sendResponse(res, result);
  1595. })
  1596. );
  1597. router5.get(
  1598. "/customerFeedback/items/:asin/browseNode",
  1599. asyncHandler(async (req, res) => {
  1600. const api = createScopedApi(CustomerFeedbackApi, req);
  1601. const result = await api.getItemBrowseNode({ ...req.query, asin: req.params.asin });
  1602. sendResponse(res, result);
  1603. })
  1604. );
  1605. router5.get(
  1606. "/customerFeedback/browseNodes/:browseNodeId/reviews/topics",
  1607. asyncHandler(async (req, res) => {
  1608. const api = createScopedApi(CustomerFeedbackApi, req);
  1609. const result = await api.getBrowseNodeReviewTopics({
  1610. ...req.query,
  1611. browseNodeId: req.params.browseNodeId
  1612. });
  1613. sendResponse(res, result);
  1614. })
  1615. );
  1616. router5.get(
  1617. "/customerFeedback/items/:asin/reviews/trends",
  1618. asyncHandler(async (req, res) => {
  1619. const api = createScopedApi(CustomerFeedbackApi, req);
  1620. const result = await api.getItemReviewTrends({ ...req.query, asin: req.params.asin });
  1621. sendResponse(res, result);
  1622. })
  1623. );
  1624. router5.get(
  1625. "/customerFeedback/browseNodes/:browseNodeId/reviews/trends",
  1626. asyncHandler(async (req, res) => {
  1627. const api = createScopedApi(CustomerFeedbackApi, req);
  1628. const result = await api.getBrowseNodeReviewTrends({
  1629. ...req.query,
  1630. browseNodeId: req.params.browseNodeId
  1631. });
  1632. sendResponse(res, result);
  1633. })
  1634. );
  1635. router5.get(
  1636. "/customerFeedback/browseNodes/:browseNodeId/returns/topics",
  1637. asyncHandler(async (req, res) => {
  1638. const api = createScopedApi(CustomerFeedbackApi, req);
  1639. const result = await api.getBrowseNodeReturnTopics({
  1640. ...req.query,
  1641. browseNodeId: req.params.browseNodeId
  1642. });
  1643. sendResponse(res, result);
  1644. })
  1645. );
  1646. router5.get(
  1647. "/customerFeedback/browseNodes/:browseNodeId/returns/trends",
  1648. asyncHandler(async (req, res) => {
  1649. const api = createScopedApi(CustomerFeedbackApi, req);
  1650. const result = await api.getBrowseNodeReturnTrends({
  1651. ...req.query,
  1652. browseNodeId: req.params.browseNodeId
  1653. });
  1654. sendResponse(res, result);
  1655. })
  1656. );
  1657. router5.get(
  1658. "/orders",
  1659. asyncHandler(async (req, res) => {
  1660. const api = createScopedApi(OrdersApi, req);
  1661. const result = await api.getOrders(req.query);
  1662. console.log("result", result);
  1663. sendResponse(res, result);
  1664. })
  1665. );
  1666. router5.get(
  1667. "/orders/:orderId",
  1668. asyncHandler(async (req, res) => {
  1669. const api = createScopedApi(OrdersApi, req);
  1670. const result = await api.getOrder(req.params.orderId);
  1671. sendResponse(res, result);
  1672. })
  1673. );
  1674. router5.get(
  1675. "/orders/:orderId/items",
  1676. asyncHandler(async (req, res) => {
  1677. const api = createScopedApi(OrdersApi, req);
  1678. const result = await api.getOrderItems(req.params.orderId, req.query.NextToken);
  1679. sendResponse(res, result);
  1680. })
  1681. );
  1682. router5.get(
  1683. "/sales/orderMetrics",
  1684. asyncHandler(async (req, res) => {
  1685. const api = createScopedApi(SalesApi, req);
  1686. const result = await api.getOrderMetrics(req.query);
  1687. sendResponse(res, result);
  1688. })
  1689. );
  1690. router5.get(
  1691. "/listings/items/:sellerId",
  1692. asyncHandler(async (req, res) => {
  1693. const api = createScopedApi(ListingsApi, req);
  1694. const result = await api.searchListingsItems({
  1695. ...req.query,
  1696. sellerId: req.params.sellerId
  1697. });
  1698. sendResponse(res, result);
  1699. })
  1700. );
  1701. router5.get(
  1702. "/listings/items/:sellerId/:sku",
  1703. asyncHandler(async (req, res) => {
  1704. const api = createScopedApi(ListingsApi, req);
  1705. const result = await api.getListingsItem({
  1706. ...req.query,
  1707. sellerId: req.params.sellerId,
  1708. sku: req.params.sku
  1709. });
  1710. sendResponse(res, result);
  1711. })
  1712. );
  1713. router5.put(
  1714. "/listings/items/:sellerId/:sku",
  1715. asyncHandler(async (req, res) => {
  1716. const api = createScopedApi(ListingsApi, req);
  1717. const marketplaceIds = req.query.marketplaceIds?.split(",") || [];
  1718. const result = await api.putListingsItem(
  1719. req.params.sellerId,
  1720. req.params.sku,
  1721. marketplaceIds,
  1722. req.body
  1723. );
  1724. sendResponse(res, result);
  1725. })
  1726. );
  1727. router5.delete(
  1728. "/listings/items/:sellerId/:sku",
  1729. asyncHandler(async (req, res) => {
  1730. const api = createScopedApi(ListingsApi, req);
  1731. const marketplaceIds = req.query.marketplaceIds?.split(",") || [];
  1732. const result = await api.deleteListingsItem(
  1733. req.params.sellerId,
  1734. req.params.sku,
  1735. marketplaceIds
  1736. );
  1737. sendResponse(res, result);
  1738. })
  1739. );
  1740. router5.get(
  1741. "/externalFulfillment/returns",
  1742. asyncHandler(async (req, res) => {
  1743. const api = createScopedApi(ExternalFulfillmentApi, req);
  1744. const result = await api.listReturns(req.query);
  1745. sendResponse(res, result);
  1746. })
  1747. );
  1748. router5.get(
  1749. "/externalFulfillment/returns/:returnId",
  1750. asyncHandler(async (req, res) => {
  1751. const api = createScopedApi(ExternalFulfillmentApi, req);
  1752. const result = await api.getReturn(req.params.returnId);
  1753. sendResponse(res, result);
  1754. })
  1755. );
  1756. router5.get(
  1757. "/catalog/categories",
  1758. asyncHandler(async (req, res) => {
  1759. const api = createScopedApi(CatalogItemsApi, req);
  1760. const result = await api.listCatalogCategories(req.query);
  1761. sendResponse(res, result);
  1762. })
  1763. );
  1764. router5.get(
  1765. "/catalog/items",
  1766. asyncHandler(async (req, res) => {
  1767. const api = createScopedApi(CatalogItemsApi, req);
  1768. const result = await api.searchCatalogItems(req.query);
  1769. sendResponse(res, result);
  1770. })
  1771. );
  1772. router5.get(
  1773. "/catalog/items/:asin",
  1774. asyncHandler(async (req, res) => {
  1775. const api = createScopedApi(CatalogItemsApi, req);
  1776. const result = await api.getCatalogItem({ ...req.query, asin: req.params.asin });
  1777. sendResponse(res, result);
  1778. })
  1779. );
  1780. router5.get(
  1781. "/sellers/account",
  1782. asyncHandler(async (req, res) => {
  1783. const api = createScopedApi(SellersApi, req);
  1784. const result = await api.getAccount();
  1785. sendResponse(res, result);
  1786. })
  1787. );
  1788. router5.get(
  1789. "/sellers/marketplaceParticipations",
  1790. asyncHandler(async (req, res) => {
  1791. const api = createScopedApi(SellersApi, req);
  1792. const result = await api.getMarketplaceParticipations();
  1793. sendResponse(res, result);
  1794. })
  1795. );
  1796. router5.post(
  1797. "/forward",
  1798. asyncHandler(async (req, res) => {
  1799. const {
  1800. path,
  1801. method = "GET",
  1802. query,
  1803. body,
  1804. functionName,
  1805. functionId,
  1806. requiresRdt,
  1807. rdtDataElements
  1808. } = req.body;
  1809. if (!path) {
  1810. res.status(400).json({
  1811. success: false,
  1812. message: "Missing required parameter: path"
  1813. });
  1814. return;
  1815. }
  1816. const client = getClient(req);
  1817. const result = await client.request({
  1818. method: method.toUpperCase(),
  1819. path,
  1820. query,
  1821. body,
  1822. requiresRdt,
  1823. rdtDataElements,
  1824. context: {
  1825. shopId: req.spApiContext?.shopId,
  1826. marketplaceId: req.spApiContext?.marketplaceId,
  1827. config: req.spApiContext?.config
  1828. }
  1829. });
  1830. console.log(result, result?.numberOfResults);
  1831. if (functionId) {
  1832. try {
  1833. console.log(`[SP-API] Executing cloud function: ${functionId}`);
  1834. const cloudResult = await runCloudFunction(
  1835. result,
  1836. {
  1837. ...req.spApiContext,
  1838. requestPath: path,
  1839. requestQuery: query,
  1840. requestBody: body,
  1841. requestCount: 1
  1842. },
  1843. functionId
  1844. );
  1845. sendResponse(res, {
  1846. apiResult: result,
  1847. cloudFunctionResult: cloudResult
  1848. });
  1849. return;
  1850. } catch (error) {
  1851. console.error(`[SP-API] Error executing cloud function ${functionId}:`, error.message);
  1852. res.status(500).json({
  1853. success: false,
  1854. message: `Cloud function execution failed: ${error.message}`,
  1855. data: result
  1856. });
  1857. return;
  1858. }
  1859. }
  1860. if (functionName && functions[functionName]) {
  1861. try {
  1862. console.log(`[SP-API] Executing function: ${functionName}`);
  1863. await functions[functionName](result, {
  1864. ...req.spApiContext,
  1865. client,
  1866. requestPath: path,
  1867. requestQuery: query,
  1868. requestCount: 1
  1869. });
  1870. } catch (error) {
  1871. console.error(`[SP-API] Error executing function ${functionName}:`, error);
  1872. }
  1873. }
  1874. sendResponse(res, result);
  1875. })
  1876. );
  1877. router5.post(
  1878. "/processReport",
  1879. asyncHandler(async (req, res) => {
  1880. const { reportDocumentId, reportType } = req.body;
  1881. if (!reportDocumentId) {
  1882. res.status(400).json({
  1883. success: false,
  1884. message: "Missing required parameter: reportDocumentId"
  1885. });
  1886. return;
  1887. }
  1888. const client = getClient(req);
  1889. try {
  1890. console.log(`[SP-API] \u6B63\u5728\u83B7\u53D6\u62A5\u8868\u6587\u6863: ${reportDocumentId}`);
  1891. const documentResult = await client.request({
  1892. method: "GET",
  1893. path: `/reports/2021-06-30/documents/${reportDocumentId}`,
  1894. context: {
  1895. shopId: req.spApiContext?.shopId,
  1896. marketplaceId: req.spApiContext?.marketplaceId,
  1897. config: req.spApiContext?.config
  1898. }
  1899. });
  1900. if (!documentResult || !documentResult.url) {
  1901. res.status(404).json({
  1902. success: false,
  1903. message: "Report document not found or URL not available"
  1904. });
  1905. return;
  1906. }
  1907. console.log(`[SP-API] \u6B63\u5728\u5904\u7406\u62A5\u8868\u6587\u4EF6: ${documentResult.url}`);
  1908. console.log(`[SP-API] \u538B\u7F29\u7B97\u6CD5: ${documentResult.compressionAlgorithm || "NONE"}`);
  1909. const processResult = await functions.processReportFile(
  1910. {
  1911. reportDocumentUrl: documentResult.url,
  1912. reportType: reportType || "UNKNOWN",
  1913. reportId: reportDocumentId,
  1914. compressionAlgorithm: documentResult.compressionAlgorithm
  1915. },
  1916. {
  1917. shopId: req.spApiContext?.shopId,
  1918. marketplaceId: req.spApiContext?.marketplaceId,
  1919. config: req.spApiContext?.config
  1920. }
  1921. );
  1922. sendResponse(res, {
  1923. documentInfo: {
  1924. reportDocumentId,
  1925. url: documentResult.url,
  1926. reportType: reportType || "UNKNOWN"
  1927. },
  1928. processResult
  1929. });
  1930. } catch (error) {
  1931. console.error(`[SP-API] \u5904\u7406\u62A5\u8868\u6587\u6863\u5931\u8D25:`, error.message);
  1932. res.status(500).json({
  1933. success: false,
  1934. message: `Failed to process report document: ${error.message}`,
  1935. reportDocumentId
  1936. });
  1937. }
  1938. })
  1939. );
  1940. router5.use((error, _req, res, _next) => {
  1941. const status = Number(error?.response?.status || error?.status || 500);
  1942. const upstream = error?.response?.data;
  1943. res.status(status).json({
  1944. success: false,
  1945. status,
  1946. code: upstream?.errors?.[0]?.code || error?.code || "SP_API_ERROR",
  1947. message: upstream?.errors?.[0]?.message || error?.message || "SP-API request failed",
  1948. details: upstream?.errors?.[0]?.details || ""
  1949. });
  1950. });
  1951. return router5;
  1952. };
  1953. // backend/api/module/schedule/routes-schedule.ts
  1954. import express3 from "npm:express";
  1955. // backend/api/module/schedule/sp-api-schedule.ts
  1956. import nodeCron from "npm:node-cron";
  1957. var SpApiScheduler = class {
  1958. // 私有属性(先声明)
  1959. #isRunning = false;
  1960. #lastRunTime = null;
  1961. #cronTask = null;
  1962. #reportCronTask = null;
  1963. #cronEnabled = true;
  1964. // ========== 私有工具方法(最优先声明,避免调用时未定义) ==========
  1965. /**
  1966. * 延迟函数
  1967. * @private
  1968. * @param {number} ms - 延迟毫秒数
  1969. * @returns {Promise<void>} 延迟 Promise
  1970. */
  1971. #delay(ms) {
  1972. return new Promise((resolve) => setTimeout(resolve, ms));
  1973. }
  1974. /**
  1975. * 计算下次执行时间(凌晨1点)
  1976. * @private
  1977. * @returns {Date} 下次执行时间
  1978. */
  1979. #calculateNextRunTime() {
  1980. const now = /* @__PURE__ */ new Date();
  1981. const next = new Date(now);
  1982. next.setHours(1, 0, 0, 0);
  1983. if (next <= now) {
  1984. next.setDate(next.getDate() + 1);
  1985. }
  1986. return next;
  1987. }
  1988. /**
  1989. * 获取所有活跃的 Amazon 店铺
  1990. * @private
  1991. * @returns {Promise<any[]>} 活跃店铺列表
  1992. */
  1993. async #getActiveAmazonShops(shopId) {
  1994. const Parse = globalThis.Parse;
  1995. const query = new Parse.Query("Shop");
  1996. query.equalTo("platform", "amazon");
  1997. query.equalTo("status", "active");
  1998. if (shopId) {
  1999. query.equalTo("objectId", shopId);
  2000. }
  2001. query.limit(1e3);
  2002. return await query.find({ useMasterKey: true });
  2003. }
  2004. /**
  2005. * 记录执行日志
  2006. * @private
  2007. * @param {any} logData - 日志数据对象
  2008. * @returns {Promise<void>}
  2009. */
  2010. async #logExecution(logData) {
  2011. try {
  2012. const Parse = globalThis.Parse;
  2013. const TaskLog = Parse.Object.extend("TaskExecutionLog");
  2014. const log = new TaskLog();
  2015. log.set("taskName", logData.taskName);
  2016. log.set("startTime", logData.startTime);
  2017. log.set("endTime", logData.endTime);
  2018. log.set("duration", logData.duration);
  2019. log.set("successCount", logData.successCount);
  2020. log.set("errorCount", logData.errorCount);
  2021. log.set("errors", logData.errors);
  2022. log.set("status", logData.status);
  2023. await log.save(null, { useMasterKey: true });
  2024. console.log("[SP-API Scheduler] \u6267\u884C\u65E5\u5FD7\u5DF2\u4FDD\u5B58");
  2025. } catch (error) {
  2026. console.error("[SP-API Scheduler] \u4FDD\u5B58\u6267\u884C\u65E5\u5FD7\u5931\u8D25:", error.message);
  2027. }
  2028. }
  2029. /**
  2030. * 采集 Listing 数据
  2031. * @private
  2032. * @param {string} shopId - 店铺ID
  2033. * @param {any} config - 店铺配置
  2034. * @param {string} marketplaceId - 市场ID
  2035. * @returns {Promise<void>}
  2036. */
  2037. async #collectListings(shopId, config, marketplaceId) {
  2038. const sellerId = config.SpApiConfig.sellerID;
  2039. marketplaceId = marketplaceId || "ATVPDKIKX0DER";
  2040. const Parse = globalThis.Parse;
  2041. let lastUpdatedAfter;
  2042. try {
  2043. const shopPointer = Parse.Object.extend("Shop").createWithoutData(shopId);
  2044. const query = new Parse.Query("Listing");
  2045. query.equalTo("shop", shopPointer);
  2046. query.descending("lastUpdatedDate");
  2047. query.limit(1);
  2048. const latestListing = await query.first({ useMasterKey: true });
  2049. if (latestListing && latestListing.get("lastUpdatedDate")) {
  2050. lastUpdatedAfter = latestListing.get("lastUpdatedDate").toISOString();
  2051. console.log(`[SP-API Scheduler] \u5E97\u94FA ${shopId} \u4F7F\u7528\u6700\u65B0\u7684 lastUpdatedDate: ${lastUpdatedAfter}`);
  2052. } else {
  2053. const oneYearAgo = new Date(Date.now() - 365 * 24 * 60 * 60 * 1e3);
  2054. lastUpdatedAfter = oneYearAgo.toISOString();
  2055. console.log(`[SP-API Scheduler] \u5E97\u94FA ${shopId} \u6CA1\u6709Listing\u6570\u636E,\u4F7F\u7528\u4E00\u5E74\u524D\u7684\u65F6\u95F4: ${lastUpdatedAfter}`);
  2056. }
  2057. } catch (error) {
  2058. const oneYearAgo = new Date(Date.now() - 365 * 24 * 60 * 60 * 1e3);
  2059. lastUpdatedAfter = oneYearAgo.toISOString();
  2060. 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}`);
  2061. }
  2062. 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}`;
  2063. const response = await fetch("http://localhost:3000/api/amazon/forward", {
  2064. method: "POST",
  2065. headers: { "Content-Type": "application/json", "shop-objectid": shopId },
  2066. body: JSON.stringify({
  2067. path,
  2068. method: "GET",
  2069. functionName: "cleanListings"
  2070. })
  2071. });
  2072. const result = await response.json();
  2073. if (!result.success) {
  2074. throw new Error(result.message || "Listing \u91C7\u96C6\u5931\u8D25");
  2075. }
  2076. console.log(`[SP-API Scheduler] \u5E97\u94FA ${shopId} Listing \u6570\u636E\u91C7\u96C6\u5B8C\u6210: ${result.processed || 0} \u6761\u8BB0\u5F55`);
  2077. }
  2078. /**
  2079. * 采集订单数据(增量同步)
  2080. * @private
  2081. * @param {string} shopId - 店铺ID
  2082. * @param {any} config - 店铺配置
  2083. * @param {string[]} marketplaceIds - 市场ID列表
  2084. * @param {any} shop - 店铺对象
  2085. * @returns {Promise<void>}
  2086. */
  2087. async #collectOrders(shopId, config, marketplaceId) {
  2088. marketplaceId = marketplaceId || "ATVPDKIKX0DER";
  2089. const Parse = globalThis.Parse;
  2090. let createdAfter;
  2091. try {
  2092. const shopPointer = Parse.Object.extend("Shop").createWithoutData(shopId);
  2093. const query = new Parse.Query("Order");
  2094. query.equalTo("shop", shopPointer);
  2095. query.descending("orderDate");
  2096. query.limit(1);
  2097. const latestOrder = await query.first({ useMasterKey: true });
  2098. if (latestOrder && latestOrder.get("orderDate")) {
  2099. createdAfter = latestOrder.get("orderDate").toISOString();
  2100. console.log(`[SP-API Scheduler] \u5E97\u94FA ${shopId} \u4F7F\u7528\u6700\u65B0\u7684 orderDate: ${createdAfter}`);
  2101. } else {
  2102. const ninetyDaysAgo = new Date(Date.now() - 90 * 24 * 60 * 60 * 1e3);
  2103. createdAfter = ninetyDaysAgo.toISOString();
  2104. console.log(`[SP-API Scheduler] \u5E97\u94FA ${shopId} \u6CA1\u6709Order\u6570\u636E,\u4F7F\u752890\u5929\u524D\u7684\u65F6\u95F4: ${createdAfter}`);
  2105. }
  2106. } catch (error) {
  2107. const ninetyDaysAgo = new Date(Date.now() - 90 * 24 * 60 * 60 * 1e3);
  2108. createdAfter = ninetyDaysAgo.toISOString();
  2109. console.log(`[SP-API Scheduler] \u5E97\u94FA ${shopId} \u67E5\u8BE2Order\u5931\u8D25,\u4F7F\u752890\u5929\u524D\u7684\u65F6\u95F4: ${createdAfter}, \u9519\u8BEF: ${error.message}`);
  2110. }
  2111. const response = await fetch("http://localhost:3000/api/amazon/forward", {
  2112. method: "POST",
  2113. headers: { "Content-Type": "application/json", "shop-objectid": shopId },
  2114. body: JSON.stringify({
  2115. path: `/orders/v0/orders?MarketplaceIds=${marketplaceId}&CreatedAfter=${createdAfter}`,
  2116. method: "GET",
  2117. functionName: "cleanOrders"
  2118. })
  2119. });
  2120. const result = await response.json();
  2121. if (!result.success) {
  2122. throw new Error(result.message || "\u8BA2\u5355\u91C7\u96C6\u5931\u8D25");
  2123. }
  2124. console.log(`[SP-API Scheduler] \u5E97\u94FA ${shopId} \u8BA2\u5355\u6570\u636E\u91C7\u96C6\u5B8C\u6210: ${result.processed || 0} \u6761\u8BB0\u5F55`);
  2125. }
  2126. /**
  2127. * 采集报表数据
  2128. * @private
  2129. * @param {string} shopId - 店铺ID
  2130. * @param {any} config - 店铺配置
  2131. * @param {string[]} marketplaceId - 市场ID列表
  2132. * @returns {Promise<void>}
  2133. */
  2134. async #collectReports(shopId, config, marketplaceId) {
  2135. const Parse = globalThis.Parse;
  2136. const reportTypes = ["GET_FBA_FULFILLMENT_CUSTOMER_RETURNS_DATA"];
  2137. for (const reportType of reportTypes) {
  2138. try {
  2139. let dataStartTime;
  2140. try {
  2141. const q = new Parse.Query("Reports");
  2142. q.equalTo("shop", shopId);
  2143. q.equalTo("reportType", reportType);
  2144. q.descending("dataEndTime");
  2145. q.limit(1);
  2146. const last = await q.first({ useMasterKey: true });
  2147. if (last && last.get("dataEndTime")) {
  2148. dataStartTime = last.get("dataEndTime").toISOString();
  2149. } else {
  2150. dataStartTime = new Date(Date.now() - 90 * 24 * 60 * 60 * 1e3).toISOString();
  2151. }
  2152. } catch (e) {
  2153. dataStartTime = new Date(Date.now() - 90 * 24 * 60 * 60 * 1e3).toISOString();
  2154. }
  2155. const dataEndTime = (/* @__PURE__ */ new Date()).toISOString();
  2156. console.log(`[SP-API Scheduler] \u5E97\u94FA ${shopId} \u521B\u5EFA\u62A5\u8868 ${reportType}, range: ${dataStartTime} ~ ${dataEndTime}`);
  2157. const createRes = await fetch("http://localhost:3000/api/amazon/forward", {
  2158. method: "POST",
  2159. headers: { "Content-Type": "application/json", "shop-objectid": shopId },
  2160. body: JSON.stringify({
  2161. path: "/reports/2021-06-30/reports",
  2162. method: "POST",
  2163. body: { reportType, marketplaceIds: [marketplaceId], dataStartTime, dataEndTime }
  2164. })
  2165. });
  2166. const createResult = await createRes.json();
  2167. const reportId = createResult?.data?.reportId;
  2168. if (!reportId) {
  2169. console.error(`[SP-API Scheduler] \u521B\u5EFA\u62A5\u8868\u5931\u8D25 ${reportType}:`, JSON.stringify(createResult));
  2170. continue;
  2171. }
  2172. console.log(`[SP-API Scheduler] \u62A5\u8868\u5DF2\u521B\u5EFA, reportId: ${reportId}`);
  2173. await this.#delay(3e3);
  2174. const getRes = await fetch("http://localhost:3000/api/amazon/forward", {
  2175. method: "POST",
  2176. headers: { "Content-Type": "application/json", "shop-objectid": shopId },
  2177. body: JSON.stringify({
  2178. path: `/reports/2021-06-30/reports/${reportId}`,
  2179. method: "GET"
  2180. })
  2181. });
  2182. const reportData = await getRes.json();
  2183. const reportInfo = reportData?.data || {};
  2184. const ReportObj = Parse.Object.extend("Reports");
  2185. const rpt = new ReportObj();
  2186. rpt.set("shop", {
  2187. __type: "Pointer",
  2188. className: "Shop",
  2189. objectId: shopId
  2190. });
  2191. rpt.set("reportId", reportId);
  2192. rpt.set("reportType", reportType);
  2193. rpt.set("marketplaceIds", reportInfo.marketplaceIds || []);
  2194. rpt.set("dataStartTime", reportInfo.dataStartTime ? new Date(reportInfo.dataStartTime) : null);
  2195. rpt.set("dataEndTime", reportInfo.dataEndTime ? new Date(reportInfo.dataEndTime) : null);
  2196. rpt.set("createdTime", reportInfo.createdTime ? new Date(reportInfo.createdTime) : null);
  2197. rpt.set("processingStartTime", reportInfo.processingStartTime ? new Date(reportInfo.processingStartTime) : null);
  2198. rpt.set("processingEndTime", reportInfo.processingEndTime ? new Date(reportInfo.processingEndTime) : null);
  2199. rpt.set("processingStatus", reportInfo.processingStatus || "IN_QUEUE");
  2200. rpt.set("reportDocumentId", reportInfo.reportDocumentId || null);
  2201. rpt.set("isParsed", false);
  2202. await rpt.save(null, { useMasterKey: true });
  2203. console.log(`[SP-API Scheduler] \u62A5\u8868\u5DF2\u4FDD\u5B58\u5230 Reports \u8868, status: ${reportInfo.processingStatus}`);
  2204. await this.#delay(2e3);
  2205. } catch (error) {
  2206. console.error(`[SP-API Scheduler] \u62A5\u8868 ${reportType} \u5904\u7406\u5931\u8D25: ${error.message}`);
  2207. }
  2208. }
  2209. }
  2210. /**
  2211. * 处理未解析的报表
  2212. * @private
  2213. * @param {string} shopId - 店铺ID
  2214. * @returns {Promise<void>}
  2215. */
  2216. async #processUnparsedReports(shopId) {
  2217. const Parse = globalThis.Parse;
  2218. const query = new Parse.Query("Reports");
  2219. query.equalTo("shop", shopId);
  2220. query.equalTo("isParsed", false);
  2221. query.equalTo("processingStatus", "DONE");
  2222. query.limit(1e3);
  2223. const unparsedReports = await query.find();
  2224. if (unparsedReports.length === 0) {
  2225. console.log(`[SP-API Scheduler] \u5E97\u94FA ${shopId} \u6CA1\u6709\u672A\u89E3\u6790\u7684\u62A5\u8868`);
  2226. return;
  2227. }
  2228. console.log(`[SP-API Scheduler] \u53D1\u73B0 ${unparsedReports.length} \u4E2A\u672A\u89E3\u6790\u62A5\u8868`);
  2229. for (const report of unparsedReports) {
  2230. try {
  2231. const reportDocumentId = report.get("reportDocumentId");
  2232. const reportType = report.get("reportType");
  2233. const response = await fetch("http://localhost:3000/api/amazon/processReport", {
  2234. method: "POST",
  2235. headers: { "Content-Type": "application/json", "shop-objectid": shopId },
  2236. body: JSON.stringify({
  2237. reportDocumentId,
  2238. reportType,
  2239. shopId
  2240. })
  2241. });
  2242. const result = await response.json();
  2243. if (result.success) {
  2244. report.set("isParsed", true);
  2245. report.set("parsedAt", /* @__PURE__ */ new Date());
  2246. report.set("parsedRecords", result.processed || 0);
  2247. await report.save(null, { useMasterKey: true });
  2248. console.log(`[SP-API Scheduler] \u62A5\u8868 ${reportDocumentId} \u89E3\u6790\u5B8C\u6210: ${result.processed} \u6761\u8BB0\u5F55`);
  2249. }
  2250. await this.#delay(1e4);
  2251. } catch (error) {
  2252. console.error(`[SP-API Scheduler] \u62A5\u8868\u89E3\u6790\u5931\u8D25: ${error.message}`);
  2253. }
  2254. }
  2255. }
  2256. /**
  2257. * 检查并更新未完成的报表状态
  2258. * 每天凌晨2点执行:查询 Reports 表中 status 不为 DONE 的报表,
  2259. * 通过 reportId 获取最新状态并更新,然后解析已完成的报表
  2260. * @private
  2261. * @returns {Promise<void>}
  2262. */
  2263. async #checkPendingReports(shopid) {
  2264. const Parse = globalThis.Parse;
  2265. console.log("[SP-API Scheduler] ========================================");
  2266. console.log("[SP-API Scheduler] \u5F00\u59CB\u68C0\u67E5\u672A\u5B8C\u6210\u7684\u62A5\u8868\u72B6\u6001");
  2267. console.log("[SP-API Scheduler] \u6267\u884C\u65F6\u95F4:", (/* @__PURE__ */ new Date()).toISOString());
  2268. console.log("[SP-API Scheduler] ========================================");
  2269. try {
  2270. const query = new Parse.Query("Reports");
  2271. query.notEqualTo("processingStatus", "DONE");
  2272. if (shopid) {
  2273. query.equalTo("shop", shopid);
  2274. }
  2275. query.limit(1e3);
  2276. const pendingReports = await query.find({ useMasterKey: true });
  2277. if (pendingReports.length === 0) {
  2278. console.log("[SP-API Scheduler] \u6CA1\u6709\u672A\u5B8C\u6210\u7684\u62A5\u8868");
  2279. return;
  2280. }
  2281. console.log(`[SP-API Scheduler] \u53D1\u73B0 ${pendingReports.length} \u4E2A\u672A\u5B8C\u6210\u62A5\u8868`);
  2282. const shopIdsToProcess = /* @__PURE__ */ new Set();
  2283. for (const report of pendingReports) {
  2284. try {
  2285. const reportId = report.get("reportId");
  2286. const shopId = report.get("shop").id;
  2287. if (!reportId || !shopId)
  2288. continue;
  2289. console.log(`[SP-API Scheduler] \u68C0\u67E5\u62A5\u8868 ${reportId} \u72B6\u6001...`);
  2290. const getRes = await fetch("http://localhost:3000/api/amazon/forward", {
  2291. method: "POST",
  2292. headers: { "Content-Type": "application/json", "shop-objectid": shopId },
  2293. body: JSON.stringify({
  2294. path: `/reports/2021-06-30/reports/${reportId}`,
  2295. method: "GET"
  2296. })
  2297. });
  2298. const reportData = await getRes.json();
  2299. const reportInfo = reportData?.data || {};
  2300. const newStatus = reportInfo.processingStatus || report.get("processingStatus");
  2301. report.set("processingStatus", newStatus);
  2302. if (reportInfo.reportDocumentId) {
  2303. report.set("reportDocumentId", reportInfo.reportDocumentId);
  2304. }
  2305. await report.save(null, { useMasterKey: true });
  2306. console.log(`[SP-API Scheduler] \u62A5\u8868 ${reportId} \u72B6\u6001\u66F4\u65B0\u4E3A: ${newStatus}`);
  2307. if (newStatus === "DONE") {
  2308. shopIdsToProcess.add(shopId);
  2309. }
  2310. await this.#delay(1e4);
  2311. } catch (error) {
  2312. console.error(`[SP-API Scheduler] \u66F4\u65B0\u62A5\u8868\u72B6\u6001\u5931\u8D25: ${error.message}`);
  2313. }
  2314. }
  2315. for (const shopId of shopIdsToProcess) {
  2316. try {
  2317. console.log(`[SP-API Scheduler] \u89E3\u6790\u5E97\u94FA ${shopId} \u7684\u5DF2\u5B8C\u6210\u62A5\u8868`);
  2318. await this.#processUnparsedReports(shopId);
  2319. } catch (error) {
  2320. console.error(`[SP-API Scheduler] \u5E97\u94FA ${shopId} \u62A5\u8868\u89E3\u6790\u5931\u8D25: ${error.message}`);
  2321. }
  2322. }
  2323. console.log("[SP-API Scheduler] \u672A\u5B8C\u6210\u62A5\u8868\u68C0\u67E5\u5B8C\u6BD5");
  2324. } catch (error) {
  2325. console.error(`[SP-API Scheduler] \u68C0\u67E5\u672A\u5B8C\u6210\u62A5\u8868\u5931\u8D25: ${error.message}`);
  2326. }
  2327. }
  2328. /**
  2329. * 处理单个店铺的数据采集
  2330. * @private
  2331. * @param {any} shop - 店铺对象
  2332. * @returns {Promise<void>}
  2333. */
  2334. async #processShop(shop) {
  2335. const shopId = shop.id;
  2336. const shopName = shop.get("name");
  2337. const config = shop.get("config");
  2338. const marketplaceId = shop.get("marketplaceId");
  2339. if (!config || !config.SpApiConfig) {
  2340. throw new Error("\u5E97\u94FA\u914D\u7F6E\u7F3A\u5931");
  2341. }
  2342. if (config.SpApiConfig.listingEnabled !== false) {
  2343. try {
  2344. console.log(`[SP-API Scheduler] \u91C7\u96C6\u5E97\u94FA ${shopName} \u7684 Listing \u6570\u636E`);
  2345. await this.#collectListings(shopId, config, marketplaceId);
  2346. await this.#delay(3e3);
  2347. } catch (error) {
  2348. console.error(`[SP-API Scheduler] Listing \u91C7\u96C6\u5931\u8D25: ${error.message}`);
  2349. }
  2350. }
  2351. try {
  2352. console.log(`[SP-API Scheduler] \u91C7\u96C6\u5E97\u94FA ${shopName} \u7684\u8BA2\u5355\u6570\u636E`);
  2353. await this.#collectOrders(shopId, config, marketplaceId);
  2354. await this.#delay(3e3);
  2355. } catch (error) {
  2356. console.error(`[SP-API Scheduler] \u8BA2\u5355\u91C7\u96C6\u5931\u8D25: ${error.message}`);
  2357. }
  2358. try {
  2359. console.log(`[SP-API Scheduler] \u91C7\u96C6\u5E97\u94FA ${shopName} \u7684\u62A5\u8868\u6570\u636E`);
  2360. await this.#collectReports(shopId, config, marketplaceId);
  2361. await this.#delay(3e3);
  2362. } catch (error) {
  2363. console.error(`[SP-API Scheduler] \u62A5\u8868\u91C7\u96C6\u5931\u8D25: ${error.message}`);
  2364. }
  2365. try {
  2366. console.log(`[SP-API Scheduler] \u5904\u7406\u5E97\u94FA ${shopName} \u7684\u672A\u89E3\u6790\u62A5\u8868`);
  2367. await this.#checkPendingReports(shopId);
  2368. } catch (error) {
  2369. console.error(`[SP-API Scheduler] \u62A5\u8868\u5904\u7406\u5931\u8D25: ${error.message}`);
  2370. }
  2371. shop.set("lastSyncTime", /* @__PURE__ */ new Date());
  2372. shop.set("syncStatus", "completed");
  2373. await shop.save(null, { useMasterKey: true });
  2374. }
  2375. /**
  2376. * 执行数据采集任务(核心方法:移到调用方之前)
  2377. * @private
  2378. * @returns {Promise<any>} 执行结果对象,包含成功数、失败数、耗时等信息
  2379. */
  2380. async #executeDataCollection(shopId) {
  2381. if (this.#isRunning) {
  2382. console.log("[SP-API Scheduler] \u4EFB\u52A1\u6B63\u5728\u6267\u884C\u4E2D\uFF0C\u8DF3\u8FC7\u672C\u6B21\u8C03\u5EA6");
  2383. return {
  2384. success: false,
  2385. message: "\u4EFB\u52A1\u6B63\u5728\u6267\u884C\u4E2D",
  2386. successCount: 0,
  2387. errorCount: 0,
  2388. duration: 0,
  2389. errors: []
  2390. };
  2391. }
  2392. this.#isRunning = true;
  2393. const startTime = Date.now();
  2394. const errors = [];
  2395. let successCount = 0;
  2396. let errorCount = 0;
  2397. console.log("[SP-API Scheduler] ========================================");
  2398. console.log("[SP-API Scheduler] \u5F00\u59CB\u6267\u884C\u6BCF\u65E5\u6570\u636E\u91C7\u96C6\u4EFB\u52A1");
  2399. console.log("[SP-API Scheduler] \u6267\u884C\u65F6\u95F4:", (/* @__PURE__ */ new Date()).toISOString());
  2400. console.log("[SP-API Scheduler] ========================================");
  2401. try {
  2402. const shops = await this.#getActiveAmazonShops(shopId);
  2403. if (shops.length === 0) {
  2404. console.log("[SP-API Scheduler] \u6CA1\u6709\u627E\u5230\u6D3B\u8DC3\u7684 Amazon \u5E97\u94FA");
  2405. return {
  2406. success: true,
  2407. message: "\u6CA1\u6709\u6D3B\u8DC3\u5E97\u94FA\u9700\u8981\u5904\u7406",
  2408. successCount: 0,
  2409. errorCount: 0,
  2410. duration: Date.now() - startTime,
  2411. errors: []
  2412. };
  2413. }
  2414. console.log(`[SP-API Scheduler] \u53D1\u73B0 ${shops.length} \u4E2A Amazon \u5E97\u94FA`);
  2415. for (const shop of shops) {
  2416. try {
  2417. console.log(`
  2418. [SP-API Scheduler] ----------------------------------------`);
  2419. console.log(`[SP-API Scheduler] \u5F00\u59CB\u5904\u7406\u5E97\u94FA: ${shop.get("name")} (${shop.id})`);
  2420. await this.#processShop(shop);
  2421. successCount++;
  2422. console.log(`[SP-API Scheduler] \u5E97\u94FA ${shop.get("name")} \u5904\u7406\u5B8C\u6210`);
  2423. await this.#delay(2e3);
  2424. } catch (error) {
  2425. errorCount++;
  2426. const errorMsg = `\u5E97\u94FA ${shop.get("name")} \u5904\u7406\u5931\u8D25: ${error.message}`;
  2427. errors.push(errorMsg);
  2428. console.error(`[SP-API Scheduler] ${errorMsg}`);
  2429. }
  2430. }
  2431. const duration = Date.now() - startTime;
  2432. await this.#logExecution({
  2433. taskName: "sp-api-daily-sync",
  2434. startTime: new Date(startTime),
  2435. endTime: /* @__PURE__ */ new Date(),
  2436. duration,
  2437. successCount,
  2438. errorCount,
  2439. errors,
  2440. status: errorCount === 0 ? "success" : successCount > 0 ? "partial_success" : "failed"
  2441. });
  2442. this.#lastRunTime = /* @__PURE__ */ new Date();
  2443. const message = `\u6570\u636E\u91C7\u96C6\u5B8C\u6210: \u6210\u529F ${successCount}/${shops.length} \u4E2A\u5E97\u94FA\uFF0C\u8017\u65F6 ${Math.round(duration / 1e3)}\u79D2`;
  2444. console.log(`
  2445. [SP-API Scheduler] ========================================`);
  2446. console.log(`[SP-API Scheduler] ${message}`);
  2447. console.log(`[SP-API Scheduler] ========================================
  2448. `);
  2449. return {
  2450. success: errorCount === 0,
  2451. message,
  2452. successCount,
  2453. errorCount,
  2454. duration,
  2455. errors
  2456. };
  2457. } catch (error) {
  2458. const duration = Date.now() - startTime;
  2459. const errorMsg = `\u6570\u636E\u91C7\u96C6\u4EFB\u52A1\u6267\u884C\u5931\u8D25: ${error.message}`;
  2460. console.error(`[SP-API Scheduler] ${errorMsg}`);
  2461. return {
  2462. success: false,
  2463. message: errorMsg,
  2464. successCount,
  2465. errorCount: errorCount + 1,
  2466. duration,
  2467. errors: [...errors, errorMsg]
  2468. };
  2469. } finally {
  2470. this.#isRunning = false;
  2471. }
  2472. }
  2473. // ========== 公共方法(后声明,因为依赖前面的私有方法) ==========
  2474. /**
  2475. * 构造函数
  2476. * 初始化 Amazon SP-API 数据采集调度器
  2477. */
  2478. constructor() {
  2479. console.log("[SP-API Scheduler] \u521D\u59CB\u5316 Amazon SP-API \u6570\u636E\u91C7\u96C6\u8C03\u5EA6\u5668");
  2480. }
  2481. /**
  2482. * 启动定时任务
  2483. * 每天凌晨1点自动执行数据采集
  2484. * @returns {Promise<void>}
  2485. */
  2486. async start() {
  2487. if (this.#cronTask) {
  2488. console.log("[SP-API Scheduler] \u5B9A\u65F6\u4EFB\u52A1\u5DF2\u5728\u8FD0\u884C\u4E2D");
  2489. return;
  2490. }
  2491. this.#cronTask = nodeCron.schedule("0 1 * * *", async () => {
  2492. await this.#executeDataCollection();
  2493. }, {
  2494. timezone: "Asia/Shanghai"
  2495. });
  2496. console.log("[SP-API Scheduler] \u5B9A\u65F6\u4EFB\u52A1\u5DF2\u542F\u52A8\uFF0C\u5C06\u5728\u6BCF\u5929\u51CC\u66681\u70B9\u6267\u884C");
  2497. this.#reportCronTask = nodeCron.schedule("0 2 * * *", async () => {
  2498. await this.#checkPendingReports();
  2499. }, {
  2500. timezone: "Asia/Shanghai"
  2501. });
  2502. 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");
  2503. }
  2504. /**
  2505. * 停止定时任务
  2506. * @returns {void}
  2507. */
  2508. stop() {
  2509. if (this.#cronTask) {
  2510. this.#cronTask.stop();
  2511. this.#cronTask = null;
  2512. console.log("[SP-API Scheduler] \u6570\u636E\u91C7\u96C6\u5B9A\u65F6\u4EFB\u52A1\u5DF2\u505C\u6B62");
  2513. }
  2514. if (this.#reportCronTask) {
  2515. this.#reportCronTask.stop();
  2516. this.#reportCronTask = null;
  2517. console.log("[SP-API Scheduler] \u62A5\u8868\u72B6\u6001\u68C0\u67E5\u5B9A\u65F6\u4EFB\u52A1\u5DF2\u505C\u6B62");
  2518. }
  2519. }
  2520. /**
  2521. * 获取调度器状态
  2522. * @returns {object} 调度器状态对象
  2523. */
  2524. getStatus() {
  2525. const nextRunTime = this.#calculateNextRunTime();
  2526. return {
  2527. isRunning: this.#isRunning,
  2528. lastRunTime: this.#lastRunTime,
  2529. nextRunTime,
  2530. cronEnabled: this.#cronEnabled
  2531. };
  2532. }
  2533. /**
  2534. * 手动触发数据采集
  2535. * @returns {Promise<any>} 执行结果对象
  2536. */
  2537. async triggerManually(shopId) {
  2538. console.log("[SP-API Scheduler] \u624B\u52A8\u89E6\u53D1\u6570\u636E\u91C7\u96C6\u4EFB\u52A1");
  2539. return await this.#executeDataCollection(shopId);
  2540. }
  2541. };
  2542. var spApiScheduler = new SpApiScheduler();
  2543. // backend/api/module/schedule/new-sp-api-schedule.ts
  2544. import nodeCron2 from "npm:node-cron";
  2545. var NewSpApiScheduler = class {
  2546. #cronTask = null;
  2547. #reportCronTask = null;
  2548. #lastRunTime = null;
  2549. // ========== 私有工具 ==========
  2550. #delay(ms) {
  2551. return new Promise((resolve) => setTimeout(resolve, ms));
  2552. }
  2553. #calculateNextRunTime() {
  2554. const now = /* @__PURE__ */ new Date();
  2555. const next = new Date(now);
  2556. next.setHours(1, 0, 0, 0);
  2557. if (next <= now)
  2558. next.setDate(next.getDate() + 1);
  2559. return next;
  2560. }
  2561. /**
  2562. * 查询活跃的 Amazon 店铺列表
  2563. * @param shopId 若传入则只返回该店铺
  2564. */
  2565. async #getActiveAmazonShops(shopId) {
  2566. const Parse = globalThis.Parse;
  2567. const query = new Parse.Query("Shop");
  2568. query.equalTo("platform", "amazon");
  2569. query.equalTo("status", "active");
  2570. if (shopId)
  2571. query.equalTo("objectId", shopId);
  2572. query.limit(1e3);
  2573. return await query.find({ useMasterKey: true });
  2574. }
  2575. // ========== 单店铺内部实现 ==========
  2576. /**
  2577. * 执行单个店铺的 Listing 采集
  2578. * 通过 /api/amazon/forward 调用 SP-API,内部 cleanListings 处理全部分页
  2579. */
  2580. async #doCollectListings(shopId, config, marketplaceId, progress) {
  2581. const sellerId = config.SpApiConfig.sellerID;
  2582. marketplaceId = marketplaceId || "ATVPDKIKX0DER";
  2583. const Parse = globalThis.Parse;
  2584. let lastUpdatedAfter;
  2585. try {
  2586. const shopPointer = Parse.Object.extend("Shop").createWithoutData(shopId);
  2587. const query = new Parse.Query("Listing");
  2588. query.equalTo("shop", shopPointer);
  2589. query.descending("lastUpdatedDate");
  2590. query.limit(1);
  2591. const latestListing = await query.first({ useMasterKey: true });
  2592. if (latestListing && latestListing.get("lastUpdatedDate")) {
  2593. lastUpdatedAfter = latestListing.get("lastUpdatedDate").toISOString();
  2594. progress?.({ type: "info", message: `\u589E\u91CF\u91C7\u96C6\uFF0C\u4F7F\u7528\u6700\u65B0 lastUpdatedDate: ${lastUpdatedAfter}` });
  2595. } else {
  2596. const oneYearAgo = new Date(Date.now() - 365 * 24 * 60 * 60 * 1e3);
  2597. lastUpdatedAfter = oneYearAgo.toISOString();
  2598. progress?.({ type: "info", message: `\u65E0\u5386\u53F2\u6570\u636E\uFF0C\u4ECE\u4E00\u5E74\u524D\u5F00\u59CB\u5168\u91CF\u91C7\u96C6: ${lastUpdatedAfter}` });
  2599. }
  2600. } catch (error) {
  2601. const oneYearAgo = new Date(Date.now() - 365 * 24 * 60 * 60 * 1e3);
  2602. lastUpdatedAfter = oneYearAgo.toISOString();
  2603. progress?.({ type: "warn", message: `\u67E5\u8BE2 Listing \u5386\u53F2\u5931\u8D25\uFF0C\u9ED8\u8BA4\u4F7F\u7528\u4E00\u5E74\u524D: ${lastUpdatedAfter}` });
  2604. }
  2605. 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}`;
  2606. 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..." });
  2607. console.log(`[New SP-API Scheduler] \u5E97\u94FA ${shopId} \u8BF7\u6C42 Listing API\uFF0Cpath: ${path}`);
  2608. const response = await fetch("http://localhost:3000/api/amazon/forward", {
  2609. method: "POST",
  2610. headers: { "Content-Type": "application/json", "shop-objectid": shopId },
  2611. body: JSON.stringify({ path, method: "GET", functionName: "cleanListings" })
  2612. });
  2613. const result = await response.json();
  2614. if (!result.success) {
  2615. throw new Error(result.message || "Listing \u91C7\u96C6\u5931\u8D25");
  2616. }
  2617. const processed = result.data?.processed || 0;
  2618. console.log(`[New SP-API Scheduler] \u5E97\u94FA ${shopId} Listing \u91C7\u96C6\u5B8C\u6210: ${processed} \u6761\u8BB0\u5F55`);
  2619. return { processed };
  2620. }
  2621. /**
  2622. * 执行单个店铺的订单采集(增量)
  2623. * 通过 /api/amazon/forward 调用 SP-API,内部 cleanOrders 处理全部分页
  2624. */
  2625. async #doCollectOrders(shopId, config, marketplaceId, progress) {
  2626. marketplaceId = marketplaceId || "ATVPDKIKX0DER";
  2627. const Parse = globalThis.Parse;
  2628. let createdAfter;
  2629. try {
  2630. const shopPointer = Parse.Object.extend("Shop").createWithoutData(shopId);
  2631. const query = new Parse.Query("Order");
  2632. query.equalTo("shop", shopPointer);
  2633. query.descending("orderDate");
  2634. query.limit(1);
  2635. const latestOrder = await query.first({ useMasterKey: true });
  2636. if (latestOrder && latestOrder.get("orderDate")) {
  2637. createdAfter = latestOrder.get("orderDate").toISOString();
  2638. progress?.({ type: "info", message: `\u589E\u91CF\u91C7\u96C6\uFF0C\u4F7F\u7528\u6700\u65B0 orderDate: ${createdAfter}` });
  2639. } else {
  2640. const ninetyDaysAgo = new Date(Date.now() - 90 * 24 * 60 * 60 * 1e3);
  2641. createdAfter = ninetyDaysAgo.toISOString();
  2642. progress?.({ type: "info", message: `\u65E0\u5386\u53F2\u6570\u636E\uFF0C\u4ECE90\u5929\u524D\u5F00\u59CB\u91C7\u96C6: ${createdAfter}` });
  2643. }
  2644. } catch (error) {
  2645. const ninetyDaysAgo = new Date(Date.now() - 90 * 24 * 60 * 60 * 1e3);
  2646. createdAfter = ninetyDaysAgo.toISOString();
  2647. progress?.({ type: "warn", message: `\u67E5\u8BE2 Order \u5386\u53F2\u5931\u8D25\uFF0C\u9ED8\u8BA4\u4F7F\u752890\u5929\u524D: ${createdAfter}` });
  2648. }
  2649. 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..." });
  2650. console.log(`[New SP-API Scheduler] \u5E97\u94FA ${shopId} \u8BF7\u6C42 Orders API\uFF0CcreatedAfter: ${createdAfter}`);
  2651. const response = await fetch("http://localhost:3000/api/amazon/forward", {
  2652. method: "POST",
  2653. headers: { "Content-Type": "application/json", "shop-objectid": shopId },
  2654. body: JSON.stringify({
  2655. path: `/orders/v0/orders?MarketplaceIds=${marketplaceId}&CreatedAfter=${createdAfter}`,
  2656. method: "GET",
  2657. functionName: "cleanOrders"
  2658. })
  2659. });
  2660. const result = await response.json();
  2661. if (!result.success) {
  2662. throw new Error(result.message || "\u8BA2\u5355\u91C7\u96C6\u5931\u8D25");
  2663. }
  2664. const processed = result.data?.processed || 0;
  2665. console.log(`[New SP-API Scheduler] \u5E97\u94FA ${shopId} \u8BA2\u5355\u91C7\u96C6\u5B8C\u6210: ${processed} \u6761\u8BB0\u5F55`);
  2666. return { processed };
  2667. }
  2668. /**
  2669. * 执行单个店铺的报表创建与保存
  2670. */
  2671. async #doCollectReports(shopId, config, marketplaceId, progress) {
  2672. const Parse = globalThis.Parse;
  2673. const reportTypes = ["GET_FBA_FULFILLMENT_CUSTOMER_RETURNS_DATA"];
  2674. for (const reportType of reportTypes) {
  2675. try {
  2676. let dataStartTime;
  2677. try {
  2678. const q = new Parse.Query("Reports");
  2679. q.equalTo("shop", shopId);
  2680. q.equalTo("reportType", reportType);
  2681. q.descending("dataEndTime");
  2682. q.limit(1);
  2683. const last = await q.first({ useMasterKey: true });
  2684. dataStartTime = last?.get("dataEndTime") ? last.get("dataEndTime").toISOString() : new Date(Date.now() - 90 * 24 * 60 * 60 * 1e3).toISOString();
  2685. } catch (e) {
  2686. dataStartTime = new Date(Date.now() - 90 * 24 * 60 * 60 * 1e3).toISOString();
  2687. }
  2688. const dataEndTime = (/* @__PURE__ */ new Date()).toISOString();
  2689. progress?.({ type: "info", message: `\u521B\u5EFA\u62A5\u8868 ${reportType}\uFF0C\u65F6\u95F4\u8303\u56F4: ${dataStartTime} ~ ${dataEndTime}` });
  2690. console.log(`[New SP-API Scheduler] \u5E97\u94FA ${shopId} \u521B\u5EFA\u62A5\u8868 ${reportType}, range: ${dataStartTime} ~ ${dataEndTime}`);
  2691. const createRes = await fetch("http://localhost:3000/api/amazon/forward", {
  2692. method: "POST",
  2693. headers: { "Content-Type": "application/json", "shop-objectid": shopId },
  2694. body: JSON.stringify({
  2695. path: "/reports/2021-06-30/reports",
  2696. method: "POST",
  2697. body: { reportType, marketplaceIds: [marketplaceId], dataStartTime, dataEndTime }
  2698. })
  2699. });
  2700. const createResult = await createRes.json();
  2701. const reportId = createResult?.data?.reportId;
  2702. if (!reportId) {
  2703. progress?.({ type: "error", message: `\u521B\u5EFA\u62A5\u8868\u5931\u8D25 ${reportType}: ${JSON.stringify(createResult)}` });
  2704. console.error(`[New SP-API Scheduler] \u521B\u5EFA\u62A5\u8868\u5931\u8D25 ${reportType}:`, JSON.stringify(createResult));
  2705. continue;
  2706. }
  2707. progress?.({ type: "info", message: `\u62A5\u8868\u5DF2\u521B\u5EFA reportId: ${reportId}\uFF0C\u7B49\u5F85\u67E5\u8BE2\u72B6\u6001...` });
  2708. console.log(`[New SP-API Scheduler] \u62A5\u8868\u5DF2\u521B\u5EFA, reportId: ${reportId}`);
  2709. await this.#delay(3e3);
  2710. const getRes = await fetch("http://localhost:3000/api/amazon/forward", {
  2711. method: "POST",
  2712. headers: { "Content-Type": "application/json", "shop-objectid": shopId },
  2713. body: JSON.stringify({
  2714. path: `/reports/2021-06-30/reports/${reportId}`,
  2715. method: "GET"
  2716. })
  2717. });
  2718. const reportData = await getRes.json();
  2719. const reportInfo = reportData?.data || {};
  2720. const ReportObj = Parse.Object.extend("Reports");
  2721. const rpt = new ReportObj();
  2722. rpt.set("shop", { __type: "Pointer", className: "Shop", objectId: shopId });
  2723. rpt.set("reportId", reportId);
  2724. rpt.set("reportType", reportType);
  2725. rpt.set("marketplaceIds", reportInfo.marketplaceIds || []);
  2726. rpt.set("dataStartTime", reportInfo.dataStartTime ? new Date(reportInfo.dataStartTime) : null);
  2727. rpt.set("dataEndTime", reportInfo.dataEndTime ? new Date(reportInfo.dataEndTime) : null);
  2728. rpt.set("createdTime", reportInfo.createdTime ? new Date(reportInfo.createdTime) : null);
  2729. rpt.set("processingStartTime", reportInfo.processingStartTime ? new Date(reportInfo.processingStartTime) : null);
  2730. rpt.set("processingEndTime", reportInfo.processingEndTime ? new Date(reportInfo.processingEndTime) : null);
  2731. rpt.set("processingStatus", reportInfo.processingStatus || "IN_QUEUE");
  2732. rpt.set("reportDocumentId", reportInfo.reportDocumentId || null);
  2733. rpt.set("isParsed", false);
  2734. await rpt.save(null, { useMasterKey: true });
  2735. progress?.({ type: "success", message: `\u62A5\u8868 ${reportType} \u5DF2\u4FDD\u5B58\uFF0CprocessingStatus: ${reportInfo.processingStatus || "IN_QUEUE"}` });
  2736. console.log(`[New SP-API Scheduler] \u62A5\u8868\u5DF2\u4FDD\u5B58, status: ${reportInfo.processingStatus}`);
  2737. await this.#delay(2e3);
  2738. } catch (error) {
  2739. progress?.({ type: "error", message: `\u62A5\u8868 ${reportType} \u5904\u7406\u5931\u8D25: ${error.message}` });
  2740. console.error(`[New SP-API Scheduler] \u62A5\u8868 ${reportType} \u5904\u7406\u5931\u8D25: ${error.message}`);
  2741. }
  2742. }
  2743. }
  2744. /**
  2745. * 检查并更新 processingStatus 不为 DONE 的报表
  2746. */
  2747. async #doCheckPendingReports(shopId, progress) {
  2748. const Parse = globalThis.Parse;
  2749. const query = new Parse.Query("Reports");
  2750. query.notEqualTo("processingStatus", "DONE");
  2751. if (shopId)
  2752. query.equalTo("shop", shopId);
  2753. query.limit(1e3);
  2754. const pendingReports = await query.find({ useMasterKey: true });
  2755. if (pendingReports.length === 0) {
  2756. progress?.({ type: "info", message: "\u6CA1\u6709\u72B6\u6001\u672A\u5B8C\u6210\u7684\u62A5\u8868\uFF0C\u8DF3\u8FC7\u72B6\u6001\u66F4\u65B0" });
  2757. console.log("[New SP-API Scheduler] \u6CA1\u6709\u672A\u5B8C\u6210\u7684\u62A5\u8868");
  2758. return;
  2759. }
  2760. progress?.({ type: "info", message: `\u53D1\u73B0 ${pendingReports.length} \u4E2A\u672A\u5B8C\u6210\u62A5\u8868\uFF0C\u9010\u4E00\u67E5\u8BE2\u6700\u65B0\u72B6\u6001...` });
  2761. console.log(`[New SP-API Scheduler] \u53D1\u73B0 ${pendingReports.length} \u4E2A\u672A\u5B8C\u6210\u62A5\u8868`);
  2762. for (const report of pendingReports) {
  2763. try {
  2764. const reportId = report.get("reportId");
  2765. const sid = report.get("shop").id;
  2766. if (!reportId || !sid)
  2767. continue;
  2768. const getRes = await fetch("http://localhost:3000/api/amazon/forward", {
  2769. method: "POST",
  2770. headers: { "Content-Type": "application/json", "shop-objectid": sid },
  2771. body: JSON.stringify({ path: `/reports/2021-06-30/reports/${reportId}`, method: "GET" })
  2772. });
  2773. const reportData = await getRes.json();
  2774. const reportInfo = reportData?.data || {};
  2775. const newStatus = reportInfo.processingStatus || report.get("processingStatus");
  2776. report.set("processingStatus", newStatus);
  2777. if (reportInfo.reportDocumentId)
  2778. report.set("reportDocumentId", reportInfo.reportDocumentId);
  2779. await report.save(null, { useMasterKey: true });
  2780. progress?.({ type: "info", message: `\u62A5\u8868 ${reportId} \u72B6\u6001\u66F4\u65B0\u4E3A: ${newStatus}` });
  2781. console.log(`[New SP-API Scheduler] \u62A5\u8868 ${reportId} \u72B6\u6001\u66F4\u65B0\u4E3A: ${newStatus}`);
  2782. await this.#delay(1e4);
  2783. } catch (error) {
  2784. progress?.({ type: "error", message: `\u66F4\u65B0\u62A5\u8868\u72B6\u6001\u5931\u8D25: ${error.message}` });
  2785. console.error(`[New SP-API Scheduler] \u66F4\u65B0\u62A5\u8868\u72B6\u6001\u5931\u8D25: ${error.message}`);
  2786. }
  2787. }
  2788. }
  2789. /**
  2790. * 解析 isParsed=false & processingStatus=DONE 的报表
  2791. */
  2792. async #doProcessUnparsedReports(shopId, progress) {
  2793. const Parse = globalThis.Parse;
  2794. const query = new Parse.Query("Reports");
  2795. query.equalTo("shop", shopId);
  2796. query.equalTo("isParsed", false);
  2797. query.equalTo("processingStatus", "DONE");
  2798. query.limit(1e3);
  2799. const unparsedReports = await query.find();
  2800. if (unparsedReports.length === 0) {
  2801. progress?.({ type: "info", message: "\u6CA1\u6709\u5F85\u89E3\u6790\u7684\u62A5\u8868\uFF08\u72B6\u6001 DONE \u4E14\u672A\u89E3\u6790\uFF09" });
  2802. console.log(`[New SP-API Scheduler] \u5E97\u94FA ${shopId} \u6CA1\u6709\u5F85\u89E3\u6790\u7684\u62A5\u8868`);
  2803. return { processed: 0 };
  2804. }
  2805. progress?.({ type: "info", message: `\u53D1\u73B0 ${unparsedReports.length} \u4E2A\u5F85\u89E3\u6790\u62A5\u8868` });
  2806. console.log(`[New SP-API Scheduler] \u53D1\u73B0 ${unparsedReports.length} \u4E2A\u5F85\u89E3\u6790\u62A5\u8868`);
  2807. let totalProcessed = 0;
  2808. for (const report of unparsedReports) {
  2809. try {
  2810. const reportDocumentId = report.get("reportDocumentId");
  2811. const reportType = report.get("reportType");
  2812. if (!reportDocumentId) {
  2813. progress?.({ type: "warn", message: `\u62A5\u8868 ${report.id} \u7F3A\u5C11 reportDocumentId\uFF0C\u8DF3\u8FC7` });
  2814. continue;
  2815. }
  2816. progress?.({ type: "info", message: `\u6B63\u5728\u89E3\u6790\u62A5\u8868 ${reportDocumentId} (${reportType})` });
  2817. console.log(`[New SP-API Scheduler] \u89E3\u6790\u62A5\u8868 ${reportDocumentId} (${reportType})`);
  2818. const response = await fetch("http://localhost:3000/api/amazon/processReport", {
  2819. method: "POST",
  2820. headers: { "Content-Type": "application/json", "shop-objectid": shopId },
  2821. body: JSON.stringify({ reportDocumentId, reportType, shopId })
  2822. });
  2823. const result = await response.json();
  2824. if (result.success) {
  2825. const parsed = result.data?.processResult?.processed || 0;
  2826. report.set("isParsed", true);
  2827. report.set("parsedAt", /* @__PURE__ */ new Date());
  2828. report.set("parsedRecords", parsed);
  2829. await report.save(null, { useMasterKey: true });
  2830. totalProcessed += parsed;
  2831. progress?.({ type: "success", message: `\u62A5\u8868 ${reportDocumentId} \u89E3\u6790\u5B8C\u6210: ${parsed} \u6761\u8BB0\u5F55`, processed: parsed });
  2832. console.log(`[New SP-API Scheduler] \u62A5\u8868 ${reportDocumentId} \u89E3\u6790\u5B8C\u6210: ${parsed} \u6761\u8BB0\u5F55`);
  2833. } else {
  2834. progress?.({ type: "warn", message: `\u62A5\u8868 ${reportDocumentId} \u89E3\u6790\u672A\u6210\u529F: ${result.message}` });
  2835. console.warn(`[New SP-API Scheduler] \u62A5\u8868 ${reportDocumentId} \u89E3\u6790\u672A\u6210\u529F: ${result.message}`);
  2836. }
  2837. await this.#delay(1e4);
  2838. } catch (error) {
  2839. progress?.({ type: "error", message: `\u62A5\u8868\u89E3\u6790\u5F02\u5E38: ${error.message}` });
  2840. console.error(`[New SP-API Scheduler] \u62A5\u8868\u89E3\u6790\u5F02\u5E38: ${error.message}`);
  2841. }
  2842. }
  2843. return { processed: totalProcessed };
  2844. }
  2845. // ========== 构造函数 ==========
  2846. constructor() {
  2847. console.log("[New SP-API Scheduler] \u521D\u59CB\u5316 Amazon SP-API \u6570\u636E\u91C7\u96C6\u8C03\u5EA6\u5668\uFF08\u65B0\u7248\uFF09");
  2848. }
  2849. // ========== 公共采集方法 ==========
  2850. /**
  2851. * 采集所有活跃店铺(或指定店铺)的 Listing 数据
  2852. * @param shopId 可选,不传则采集全部活跃店铺
  2853. * @param progress 可选进度回调,适合 SSE 实时推送
  2854. */
  2855. async collectListings(shopId, progress) {
  2856. const shops = await this.#getActiveAmazonShops(shopId);
  2857. if (shops.length === 0) {
  2858. 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";
  2859. progress?.({ type: "warn", message: msg });
  2860. console.warn(`[New SP-API Scheduler] ${msg}`);
  2861. return { success: true, message: msg, processed: 0, errorCount: 0, errors: [] };
  2862. }
  2863. let totalProcessed = 0;
  2864. let errorCount = 0;
  2865. const errors = [];
  2866. for (const shop of shops) {
  2867. const sid = shop.id;
  2868. const shopName = shop.get("name");
  2869. const config = shop.get("config");
  2870. const marketplaceId = shop.get("marketplaceId") || "ATVPDKIKX0DER";
  2871. if (!config || !config.SpApiConfig) {
  2872. const msg = `\u5E97\u94FA\u3010${shopName}\u3011(${sid}) \u7F3A\u5C11 SpApiConfig \u914D\u7F6E\uFF0C\u8DF3\u8FC7`;
  2873. progress?.({ type: "error", message: msg });
  2874. console.error(`[New SP-API Scheduler] ${msg}`);
  2875. errors.push(msg);
  2876. errorCount++;
  2877. continue;
  2878. }
  2879. if (config.SpApiConfig.listingEnabled === false) {
  2880. progress?.({ type: "warn", message: `\u5E97\u94FA\u3010${shopName}\u3011\u672A\u914D\u7F6E\u533A\u57DF Seller ID\uFF0C\u8DF3\u8FC7 Listing` });
  2881. continue;
  2882. }
  2883. progress?.({ type: "info", message: `\u5F00\u59CB\u91C7\u96C6\u5E97\u94FA\u3010${shopName}\u3011\u7684 Listing \u6570\u636E` });
  2884. console.log(`[New SP-API Scheduler] \u5F00\u59CB\u91C7\u96C6\u5E97\u94FA ${shopName} (${sid}) \u7684 Listing \u6570\u636E`);
  2885. try {
  2886. const result = await this.#doCollectListings(sid, config, marketplaceId, progress);
  2887. progress?.({ type: "success", message: `\u5E97\u94FA\u3010${shopName}\u3011Listing \u91C7\u96C6\u5B8C\u6210`, processed: result.processed });
  2888. totalProcessed += result.processed;
  2889. await this.#delay(2e3);
  2890. } catch (error) {
  2891. const msg = `\u5E97\u94FA\u3010${shopName}\u3011Listing \u91C7\u96C6\u5931\u8D25: ${error.message}`;
  2892. progress?.({ type: "error", message: msg });
  2893. console.error(`[New SP-API Scheduler] ${msg}`);
  2894. errors.push(msg);
  2895. errorCount++;
  2896. }
  2897. }
  2898. const message = `Listing \u91C7\u96C6\u5B8C\u6210: \u6210\u529F ${shops.length - errorCount}/${shops.length} \u4E2A\u5E97\u94FA\uFF0C\u5171 ${totalProcessed} \u6761\u8BB0\u5F55`;
  2899. return { success: errorCount === 0, message, processed: totalProcessed, errorCount, errors };
  2900. }
  2901. /**
  2902. * 采集所有活跃店铺(或指定店铺)的订单数据(增量)
  2903. * @param shopId 可选
  2904. * @param progress 可选进度回调
  2905. */
  2906. async collectOrders(shopId, progress) {
  2907. const shops = await this.#getActiveAmazonShops(shopId);
  2908. if (shops.length === 0) {
  2909. 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";
  2910. progress?.({ type: "warn", message: msg });
  2911. console.warn(`[New SP-API Scheduler] ${msg}`);
  2912. return { success: true, message: msg, processed: 0, errorCount: 0, errors: [] };
  2913. }
  2914. let totalProcessed = 0;
  2915. let errorCount = 0;
  2916. const errors = [];
  2917. for (const shop of shops) {
  2918. const sid = shop.id;
  2919. const shopName = shop.get("name");
  2920. const config = shop.get("config");
  2921. const marketplaceId = shop.get("marketplaceId") || "ATVPDKIKX0DER";
  2922. if (!config || !config.SpApiConfig) {
  2923. const msg = `\u5E97\u94FA\u3010${shopName}\u3011(${sid}) \u7F3A\u5C11 SpApiConfig \u914D\u7F6E\uFF0C\u8DF3\u8FC7`;
  2924. progress?.({ type: "error", message: msg });
  2925. console.error(`[New SP-API Scheduler] ${msg}`);
  2926. errors.push(msg);
  2927. errorCount++;
  2928. continue;
  2929. }
  2930. progress?.({ type: "info", message: `\u5F00\u59CB\u91C7\u96C6\u5E97\u94FA\u3010${shopName}\u3011\u7684\u8BA2\u5355\u6570\u636E` });
  2931. console.log(`[New SP-API Scheduler] \u5F00\u59CB\u91C7\u96C6\u5E97\u94FA ${shopName} (${sid}) \u7684\u8BA2\u5355\u6570\u636E`);
  2932. try {
  2933. const result = await this.#doCollectOrders(sid, config, marketplaceId, progress);
  2934. progress?.({ type: "success", message: `\u5E97\u94FA\u3010${shopName}\u3011\u8BA2\u5355\u91C7\u96C6\u5B8C\u6210`, processed: result.processed });
  2935. totalProcessed += result.processed;
  2936. await this.#delay(2e3);
  2937. } catch (error) {
  2938. const msg = `\u5E97\u94FA\u3010${shopName}\u3011\u8BA2\u5355\u91C7\u96C6\u5931\u8D25: ${error.message}`;
  2939. progress?.({ type: "error", message: msg });
  2940. console.error(`[New SP-API Scheduler] ${msg}`);
  2941. errors.push(msg);
  2942. errorCount++;
  2943. }
  2944. }
  2945. const message = `\u8BA2\u5355\u91C7\u96C6\u5B8C\u6210: \u6210\u529F ${shops.length - errorCount}/${shops.length} \u4E2A\u5E97\u94FA\uFF0C\u5171 ${totalProcessed} \u6761\u8BB0\u5F55`;
  2946. return { success: errorCount === 0, message, processed: totalProcessed, errorCount, errors };
  2947. }
  2948. /**
  2949. * 为所有活跃店铺(或指定店铺)创建并保存报表
  2950. * @param shopId 可选
  2951. * @param progress 可选进度回调
  2952. */
  2953. async collectReports(shopId, progress) {
  2954. const shops = await this.#getActiveAmazonShops(shopId);
  2955. if (shops.length === 0) {
  2956. 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";
  2957. progress?.({ type: "warn", message: msg });
  2958. console.warn(`[New SP-API Scheduler] ${msg}`);
  2959. return { success: true, message: msg, processed: 0, errorCount: 0, errors: [] };
  2960. }
  2961. let successCount = 0;
  2962. let errorCount = 0;
  2963. const errors = [];
  2964. for (const shop of shops) {
  2965. const sid = shop.id;
  2966. const shopName = shop.get("name");
  2967. const config = shop.get("config");
  2968. const marketplaceId = shop.get("marketplaceId") || "ATVPDKIKX0DER";
  2969. if (!config || !config.SpApiConfig) {
  2970. const msg = `\u5E97\u94FA\u3010${shopName}\u3011(${sid}) \u7F3A\u5C11 SpApiConfig \u914D\u7F6E\uFF0C\u8DF3\u8FC7`;
  2971. progress?.({ type: "error", message: msg });
  2972. console.error(`[New SP-API Scheduler] ${msg}`);
  2973. errors.push(msg);
  2974. errorCount++;
  2975. continue;
  2976. }
  2977. progress?.({ type: "info", message: `\u5F00\u59CB\u91C7\u96C6\u5E97\u94FA\u3010${shopName}\u3011\u7684\u62A5\u8868\u6570\u636E` });
  2978. console.log(`[New SP-API Scheduler] \u5F00\u59CB\u91C7\u96C6\u5E97\u94FA ${shopName} (${sid}) \u7684\u62A5\u8868\u6570\u636E`);
  2979. try {
  2980. await this.#doCollectReports(sid, config, marketplaceId, progress);
  2981. progress?.({ type: "success", message: `\u5E97\u94FA\u3010${shopName}\u3011\u62A5\u8868\u91C7\u96C6\u5B8C\u6210` });
  2982. successCount++;
  2983. await this.#delay(2e3);
  2984. } catch (error) {
  2985. const msg = `\u5E97\u94FA\u3010${shopName}\u3011\u62A5\u8868\u91C7\u96C6\u5931\u8D25: ${error.message}`;
  2986. progress?.({ type: "error", message: msg });
  2987. console.error(`[New SP-API Scheduler] ${msg}`);
  2988. errors.push(msg);
  2989. errorCount++;
  2990. }
  2991. }
  2992. const message = `\u62A5\u8868\u91C7\u96C6\u5B8C\u6210: \u6210\u529F ${successCount}/${shops.length} \u4E2A\u5E97\u94FA`;
  2993. return { success: errorCount === 0, message, processed: successCount, errorCount, errors };
  2994. }
  2995. /**
  2996. * 检查未完成报表状态并解析已完成的报表
  2997. * @param shopId 可选
  2998. * @param progress 可选进度回调
  2999. */
  3000. async parseReports(shopId, progress) {
  3001. const shops = await this.#getActiveAmazonShops(shopId);
  3002. if (shops.length === 0) {
  3003. 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";
  3004. progress?.({ type: "warn", message: msg });
  3005. console.warn(`[New SP-API Scheduler] ${msg}`);
  3006. return { success: true, message: msg, processed: 0, errorCount: 0, errors: [] };
  3007. }
  3008. progress?.({ type: "info", message: "\u68C0\u67E5\u6240\u6709\u672A\u5B8C\u6210\u62A5\u8868\u7684\u6700\u65B0\u72B6\u6001..." });
  3009. try {
  3010. await this.#doCheckPendingReports(shopId, progress);
  3011. } catch (error) {
  3012. progress?.({ type: "warn", message: `\u68C0\u67E5\u672A\u5B8C\u6210\u62A5\u8868\u72B6\u6001\u65F6\u51FA\u9519: ${error.message}` });
  3013. console.warn(`[New SP-API Scheduler] \u68C0\u67E5\u672A\u5B8C\u6210\u62A5\u8868\u72B6\u6001\u51FA\u9519: ${error.message}`);
  3014. }
  3015. let totalProcessed = 0;
  3016. let errorCount = 0;
  3017. const errors = [];
  3018. for (const shop of shops) {
  3019. const sid = shop.id;
  3020. const shopName = shop.get("name");
  3021. progress?.({ type: "info", message: `\u5F00\u59CB\u89E3\u6790\u5E97\u94FA\u3010${shopName}\u3011\u7684\u5DF2\u5B8C\u6210\u62A5\u8868` });
  3022. console.log(`[New SP-API Scheduler] \u5F00\u59CB\u89E3\u6790\u5E97\u94FA ${shopName} (${sid}) \u7684\u5DF2\u5B8C\u6210\u62A5\u8868`);
  3023. try {
  3024. const result = await this.#doProcessUnparsedReports(sid, progress);
  3025. progress?.({ type: "success", message: `\u5E97\u94FA\u3010${shopName}\u3011\u62A5\u8868\u89E3\u6790\u5B8C\u6210: ${result.processed} \u6761\u8BB0\u5F55`, processed: result.processed });
  3026. totalProcessed += result.processed;
  3027. await this.#delay(2e3);
  3028. } catch (error) {
  3029. const msg = `\u5E97\u94FA\u3010${shopName}\u3011\u62A5\u8868\u89E3\u6790\u5931\u8D25: ${error.message}`;
  3030. progress?.({ type: "error", message: msg });
  3031. console.error(`[New SP-API Scheduler] ${msg}`);
  3032. errors.push(msg);
  3033. errorCount++;
  3034. }
  3035. }
  3036. 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`;
  3037. return { success: errorCount === 0, message, processed: totalProcessed, errorCount, errors };
  3038. }
  3039. // ========== 定时任务管理 ==========
  3040. /**
  3041. * 启动定时任务
  3042. * - 凌晨 1 点:采集 Listing / Order / Report
  3043. * - 凌晨 2 点:解析已完成报表
  3044. */
  3045. async start() {
  3046. if (this.#cronTask) {
  3047. console.log("[New SP-API Scheduler] \u5B9A\u65F6\u4EFB\u52A1\u5DF2\u5728\u8FD0\u884C\u4E2D\uFF0C\u8DF3\u8FC7\u91CD\u590D\u542F\u52A8");
  3048. return;
  3049. }
  3050. this.#cronTask = nodeCron2.schedule("0 1 * * *", async () => {
  3051. console.log("[New SP-API Scheduler] ===== \u51CC\u66681\u70B9\uFF1A\u5F00\u59CB\u6BCF\u65E5\u6570\u636E\u91C7\u96C6 =====");
  3052. this.#lastRunTime = /* @__PURE__ */ new Date();
  3053. try {
  3054. await this.collectListings(void 0, void 0);
  3055. await this.collectOrders(void 0, void 0);
  3056. await this.collectReports(void 0, void 0);
  3057. } catch (e) {
  3058. console.error("[New SP-API Scheduler] \u6BCF\u65E5\u91C7\u96C6\u4EFB\u52A1\u5931\u8D25:", e.message);
  3059. }
  3060. console.log("[New SP-API Scheduler] ===== \u6BCF\u65E5\u6570\u636E\u91C7\u96C6\u5B8C\u6210 =====");
  3061. }, { timezone: "Asia/Shanghai" });
  3062. this.#reportCronTask = nodeCron2.schedule("0 2 * * *", async () => {
  3063. console.log("[New SP-API Scheduler] ===== \u51CC\u66682\u70B9\uFF1A\u5F00\u59CB\u62A5\u8868\u89E3\u6790 =====");
  3064. try {
  3065. await this.parseReports(void 0, void 0);
  3066. } catch (e) {
  3067. console.error("[New SP-API Scheduler] \u62A5\u8868\u89E3\u6790\u4EFB\u52A1\u5931\u8D25:", e.message);
  3068. }
  3069. console.log("[New SP-API Scheduler] ===== \u62A5\u8868\u89E3\u6790\u5B8C\u6210 =====");
  3070. }, { timezone: "Asia/Shanghai" });
  3071. 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");
  3072. }
  3073. /**
  3074. * 停止定时任务
  3075. */
  3076. stop() {
  3077. if (this.#cronTask) {
  3078. this.#cronTask.stop();
  3079. this.#cronTask = null;
  3080. console.log("[New SP-API Scheduler] \u91C7\u96C6\u5B9A\u65F6\u4EFB\u52A1\u5DF2\u505C\u6B62");
  3081. }
  3082. if (this.#reportCronTask) {
  3083. this.#reportCronTask.stop();
  3084. this.#reportCronTask = null;
  3085. console.log("[New SP-API Scheduler] \u62A5\u8868\u89E3\u6790\u5B9A\u65F6\u4EFB\u52A1\u5DF2\u505C\u6B62");
  3086. }
  3087. }
  3088. /**
  3089. * 获取调度器状态
  3090. */
  3091. getStatus() {
  3092. return {
  3093. isRunning: !!(this.#cronTask || this.#reportCronTask),
  3094. lastRunTime: this.#lastRunTime,
  3095. nextRunTime: this.#calculateNextRunTime()
  3096. };
  3097. }
  3098. };
  3099. var newSpApiScheduler = new NewSpApiScheduler();
  3100. // backend/api/module/schedule/sorftime-api-schedule.ts
  3101. import nodeCron3 from "npm:node-cron";
  3102. function unwrapRelayData(result) {
  3103. let data = result?.data ?? result?.Data ?? result;
  3104. if (data && typeof data === "object" && !Array.isArray(data)) {
  3105. data = data.data ?? data.Data ?? data;
  3106. }
  3107. return data;
  3108. }
  3109. function relaySucceeded(result) {
  3110. const code = Number(result?.code ?? result?.Code ?? 200);
  3111. return code === 0 || code === 200;
  3112. }
  3113. function siteCode(marketplaceId, domain) {
  3114. const byMarketplace = {
  3115. ATVPDKIKX0DER: "us",
  3116. A2EUQ1WTGCTBG2: "ca",
  3117. A1AM78C64UM0Y8: "mx",
  3118. A2Q3Y263D00KWC: "br",
  3119. A1F83G8C2ARO7P: "uk",
  3120. A1PA6795UKMFR9: "de",
  3121. A13V1IB3VIYZZH: "fr",
  3122. A1RKKUPIHCS9HS: "es",
  3123. APJ6JRA9NG5V4: "it",
  3124. A1VC38T7YXB528: "jp",
  3125. A39IBJ37TRP1C6: "au",
  3126. A2VIGQ35RCS4UG: "ae",
  3127. A17E79C6D8DWNP: "sa",
  3128. A21TJRUUN4KGV: "in"
  3129. };
  3130. return byMarketplace[marketplaceId] || String(domain || "");
  3131. }
  3132. async function upsertSorftimeProduct(raw, shopId, shopName, marketplaceId, domain) {
  3133. const Parse = globalThis.Parse;
  3134. const asin = String(raw?.Asin || raw?.ASIN || raw?.asin || "").trim().toUpperCase();
  3135. if (!asin)
  3136. return false;
  3137. const shop = Parse.Object.extend("Shop").createWithoutData(shopId);
  3138. const common = {
  3139. asin,
  3140. parentAsin: raw.ParentAsin || raw.ParentASIN || "",
  3141. title: raw.Title || raw.title || "",
  3142. photo: raw.Photo || raw.photo || "",
  3143. imageUrl: Array.isArray(raw.Photo) ? raw.Photo[0] : raw.Photo || "",
  3144. price: Number(raw.Price || 0),
  3145. salesPrice: Number(raw.SalesPrice || raw.Price || 0),
  3146. brand: raw.Brand || "",
  3147. sellerId: raw.BuyboxSellerId || "",
  3148. ratings: Number(raw.Ratings || 0),
  3149. rating: Number(raw.Ratings || 0),
  3150. ratingsCount: Number(raw.RatingsCount || 0),
  3151. category: raw.Category || "",
  3152. bsrCategory: raw.BsrCategory || [],
  3153. rank: Number(raw.Rank || 0),
  3154. listingSalesVolumeOfMonth: Number(raw.ListingSalesVolumeOfMonth || 0),
  3155. ListingSalesVolumeOfMonth: Number(raw.ListingSalesVolumeOfMonth || 0),
  3156. listingSalesOfMonth: Number(raw.ListingSalesOfMonth || 0),
  3157. marketplaceId,
  3158. domain: String(domain || ""),
  3159. site: siteCode(marketplaceId, domain),
  3160. storeName: shopName || "",
  3161. shopName: shopName || "",
  3162. shopId,
  3163. shop,
  3164. source: "sorftime",
  3165. onlineDate: String(raw.OnlineDate || "").slice(0, 10),
  3166. rawData: raw
  3167. };
  3168. for (const className of ["Product", "ProductDetail", "SorftimeProduct"]) {
  3169. const query = new Parse.Query(className);
  3170. query.equalTo("asin", asin);
  3171. if (className !== "SorftimeProduct")
  3172. query.equalTo("shop", shop);
  3173. let object = null;
  3174. try {
  3175. object = await query.first({ useMasterKey: true });
  3176. } catch (error) {
  3177. const message = String(error?.message || error || "");
  3178. if (!message.includes("does not exist") && !message.includes("non-existent class"))
  3179. throw error;
  3180. }
  3181. if (!object)
  3182. object = new Parse.Object(className);
  3183. for (const [key, value] of Object.entries(common)) {
  3184. if (value !== void 0 && value !== null)
  3185. object.set(key, value);
  3186. }
  3187. await object.save(null, { useMasterKey: true });
  3188. }
  3189. return true;
  3190. }
  3191. async function upsertSorftimeReview(raw, asin, shopId) {
  3192. const Parse = globalThis.Parse;
  3193. const reviewId = String(raw?.ReviewId || raw?.Id || raw?.ID || raw?.id || "").trim();
  3194. const uniqueId = reviewId || `${asin}:${raw?.ReviewsDate || raw?.ReviewDate || raw?.Date || ""}:${raw?.ProfileName || raw?.Author || ""}`;
  3195. const query = new Parse.Query("SorftimeReviews");
  3196. query.equalTo("reviewId", uniqueId);
  3197. let object = null;
  3198. try {
  3199. object = await query.first({ useMasterKey: true });
  3200. } catch (error) {
  3201. const message = String(error?.message || error || "");
  3202. if (!message.includes("does not exist") && !message.includes("non-existent class"))
  3203. throw error;
  3204. }
  3205. if (!object)
  3206. object = new Parse.Object("SorftimeReviews");
  3207. object.set("reviewId", uniqueId);
  3208. object.set("asin", asin);
  3209. object.set("star", Number(raw?.Star || raw?.Rating || raw?.rating || 0));
  3210. object.set("rating", Number(raw?.Star || raw?.Rating || raw?.rating || 0));
  3211. object.set("title", raw?.Title || raw?.ReviewTitle || "");
  3212. object.set("content", raw?.Content || raw?.ReviewContent || raw?.Body || "");
  3213. object.set("reviewDate", raw?.ReviewsDate || raw?.ReviewDate || raw?.Date || "");
  3214. object.set("author", raw?.ConsumerName || raw?.ProfileName || raw?.Author || "");
  3215. object.set("verified", Boolean(raw?.IsVP || raw?.OnlyPurchase || raw?.VerifiedPurchase));
  3216. object.set("shop", Parse.Object.extend("Shop").createWithoutData(shopId));
  3217. object.set("rawData", raw);
  3218. await object.save(null, { useMasterKey: true });
  3219. }
  3220. var SorftimeScheduler = class {
  3221. // 私有属性(先声明)
  3222. #isRunning = false;
  3223. #lastRunTime = null;
  3224. #categoryProductsCronTask = null;
  3225. #keywordsCronTask = null;
  3226. #marketTrendCronTask = null;
  3227. #shopProductsCronTask = null;
  3228. #productDetailCronTask = null;
  3229. #cloudFunctionCronTask = null;
  3230. #reviewsCronTask = null;
  3231. #cronEnabled = true;
  3232. // ========== 私有工具方法(最优先声明,避免调用时未定义) ==========
  3233. /**
  3234. * 延迟函数
  3235. * @private
  3236. * @param {number} ms - 延迟毫秒数
  3237. * @returns {Promise<void>} 延迟 Promise
  3238. */
  3239. #delay(ms) {
  3240. return new Promise((resolve) => setTimeout(resolve, ms));
  3241. }
  3242. /**
  3243. * 获取所有叶子节点类目
  3244. * @private
  3245. * @returns {Promise<any[]>} 叶子节点类目列表
  3246. */
  3247. async #getLeafCategories() {
  3248. const Parse = globalThis.Parse;
  3249. const query = new Parse.Query("SelfCategory");
  3250. query.equalTo("isLeaf", true);
  3251. query.limit(1e4);
  3252. return await query.find({ useMasterKey: true });
  3253. }
  3254. /**
  3255. * 根据 shopId 或 nodeIds 筛选类目
  3256. * - nodeIds 优先:直接按 nodeId 数组过滤 SelfCategory
  3257. * - shopId:读取店铺的 nodeIds 字段后过滤 SelfCategory
  3258. * - 两者均未传:返回全部叶子节点类目
  3259. * @private
  3260. * @param {string} shopId - 可选
  3261. * @param {string[]} nodeIds - 可选
  3262. * @returns {Promise<any[]>} 类目列表
  3263. */
  3264. async #getLeafCategoriesByFilter(shopId, nodeIds) {
  3265. const Parse = globalThis.Parse;
  3266. if (nodeIds && nodeIds.length > 0) {
  3267. const query = new Parse.Query("SelfCategory");
  3268. query.containedIn("nodeId", nodeIds);
  3269. query.limit(1e4);
  3270. return await query.find({ useMasterKey: true });
  3271. }
  3272. if (shopId) {
  3273. const shopQuery = new Parse.Query("Shop");
  3274. const shop = await shopQuery.get(shopId, { useMasterKey: true });
  3275. const shopNodeIds = shop.get("nodeIds") || [];
  3276. if (shopNodeIds.length > 0) {
  3277. const catQuery = new Parse.Query("SelfCategory");
  3278. catQuery.containedIn("nodeId", shopNodeIds);
  3279. catQuery.limit(1e4);
  3280. return await catQuery.find({ useMasterKey: true });
  3281. }
  3282. }
  3283. return await this.#getLeafCategories();
  3284. }
  3285. /**
  3286. * 获取活跃的 Amazon 店铺列表
  3287. * @private
  3288. * @param {string} shopId - 可选,传入则只返回该店铺
  3289. * @returns {Promise<any[]>} 店铺列表
  3290. */
  3291. async #getActiveAmazonShops(shopId) {
  3292. const Parse = globalThis.Parse;
  3293. const query = new Parse.Query("Shop");
  3294. query.equalTo("platform", "amazon");
  3295. query.equalTo("status", "active");
  3296. if (shopId)
  3297. query.equalTo("objectId", shopId);
  3298. query.limit(1e3);
  3299. return await query.find({ useMasterKey: true });
  3300. }
  3301. /**
  3302. * 记录执行日志
  3303. * @private
  3304. * @param {any} logData - 日志数据对象
  3305. * @returns {Promise<void>}
  3306. */
  3307. async #logExecution(logData) {
  3308. try {
  3309. const Parse = globalThis.Parse;
  3310. const TaskLog = Parse.Object.extend("TaskExecutionLog");
  3311. const log = new TaskLog();
  3312. log.set("taskName", logData.taskName);
  3313. log.set("startTime", logData.startTime);
  3314. log.set("endTime", logData.endTime);
  3315. log.set("duration", logData.duration);
  3316. log.set("successCount", logData.successCount);
  3317. log.set("errorCount", logData.errorCount);
  3318. log.set("errors", logData.errors);
  3319. log.set("status", logData.status);
  3320. await log.save(null, { useMasterKey: true });
  3321. console.log("[Sorftime Scheduler] \u6267\u884C\u65E5\u5FD7\u5DF2\u4FDD\u5B58");
  3322. } catch (error) {
  3323. console.error("[Sorftime Scheduler] \u4FDD\u5B58\u6267\u884C\u65E5\u5FD7\u5931\u8D25:", error.message);
  3324. }
  3325. }
  3326. /**
  3327. * 采集类目热销产品(每月1号凌晨执行)
  3328. * @private
  3329. * @param {string} nodeId - 类目节点ID
  3330. * @param {number} domain - 站点域名代码
  3331. * @returns {Promise<void>}
  3332. */
  3333. async #collectCategoryProducts(nodeId, domain, progress) {
  3334. console.log(`[Sorftime Scheduler] \u5F00\u59CB\u91C7\u96C6\u7C7B\u76EE ${nodeId} \u7684\u70ED\u9500\u4EA7\u54C1 (domain: ${domain})`);
  3335. const functionId = "ZmYNPsoX9X";
  3336. for (let page = 1; page <= 4; page++) {
  3337. try {
  3338. progress?.({ type: "info", message: `\u7C7B\u76EE ${nodeId} \u7B2C ${page}/4 \u9875\u70ED\u9500\u4EA7\u54C1\u91C7\u96C6\u4E2D...` });
  3339. const requestBody = {
  3340. path: "/api/CategoryProducts",
  3341. method: "POST",
  3342. query: { domain },
  3343. body: {
  3344. NodeId: nodeId,
  3345. Page: page,
  3346. Range: 400
  3347. },
  3348. functionId
  3349. };
  3350. const result = await relayClient.forwardSorftime({
  3351. path: requestBody.path,
  3352. method: requestBody.method,
  3353. query: requestBody.query,
  3354. body: requestBody.body
  3355. });
  3356. if (!relaySucceeded(result)) {
  3357. throw new Error(result.message || `\u7B2C${page}\u9875\u6570\u636E\u91C7\u96C6\u5931\u8D25`);
  3358. }
  3359. console.log(`[Sorftime Scheduler] \u7C7B\u76EE ${nodeId} \u7B2C${page}\u9875\u6570\u636E\u91C7\u96C6\u5B8C\u6210`);
  3360. await this.#delay(2e3);
  3361. } catch (error) {
  3362. console.error(`[Sorftime Scheduler] \u7C7B\u76EE ${nodeId} \u7B2C${page}\u9875\u91C7\u96C6\u5931\u8D25:`, error.message);
  3363. throw error;
  3364. }
  3365. }
  3366. }
  3367. /**
  3368. * 采集类目关键词(每周一凌晨1点执行)
  3369. * @private
  3370. * @param {string} nodeId - 类目节点ID
  3371. * @param {number} domain - 站点域名代码
  3372. * @returns {Promise<void>}
  3373. */
  3374. async #collectCategoryKeywords(nodeId, domain, progress) {
  3375. console.log(`[Sorftime Scheduler] \u5F00\u59CB\u91C7\u96C6\u7C7B\u76EE ${nodeId} \u7684\u5173\u952E\u8BCD (domain: ${domain})`);
  3376. const functionId = "KNN4L19aoi";
  3377. try {
  3378. const requestBody = {
  3379. path: "/api/CategoryRequestKeyword",
  3380. method: "POST",
  3381. query: { domain },
  3382. body: {
  3383. Nodeid: nodeId,
  3384. PageIndex: 1,
  3385. PageSize: 100
  3386. },
  3387. functionId
  3388. };
  3389. const result = await relayClient.forwardSorftime({
  3390. path: requestBody.path,
  3391. method: requestBody.method,
  3392. query: requestBody.query,
  3393. body: requestBody.body
  3394. });
  3395. if (!relaySucceeded(result)) {
  3396. throw new Error(result.message || "\u5173\u952E\u8BCD\u91C7\u96C6\u5931\u8D25");
  3397. }
  3398. console.log(`[Sorftime Scheduler] \u7C7B\u76EE ${nodeId} \u5173\u952E\u8BCD\u91C7\u96C6\u5B8C\u6210`);
  3399. } catch (error) {
  3400. console.error(`[Sorftime Scheduler] \u7C7B\u76EE ${nodeId} \u5173\u952E\u8BCD\u91C7\u96C6\u5931\u8D25:`, error.message);
  3401. throw error;
  3402. }
  3403. }
  3404. /**
  3405. * 通过 SellerId 分页采集单个店铺的产品数据
  3406. * @private
  3407. * @param {string} shopId - 店铺 Parse objectId
  3408. * @param {string} sellerId - 亚马逊 SellerId
  3409. * @param {number} domain - 站点代码(1=US)
  3410. * @param {Function} progress - 可选进度回调
  3411. * @returns {Promise<{processed: number, pages: number}>}
  3412. */
  3413. async #doCollectShopProducts(shopId, shopName, sellerId, domain, marketplaceId, progress) {
  3414. const functionId = "GSjAsvw9FK";
  3415. let page = 1;
  3416. let hasMore = true;
  3417. let totalProcessed = 0;
  3418. progress?.({ type: "info", message: `SellerId: ${sellerId}\uFF0C\u5F00\u59CB\u5206\u9875\u91C7\u96C6\u4EA7\u54C1\u6570\u636E...` });
  3419. console.log(`[Sorftime Scheduler] \u5E97\u94FA ${shopId} SellerId: ${sellerId} \u5F00\u59CB\u91C7\u96C6`);
  3420. while (hasMore) {
  3421. progress?.({ type: "info", message: `\u6B63\u5728\u91C7\u96C6\u7B2C ${page} \u9875...` });
  3422. console.log(`[Sorftime Scheduler] \u5E97\u94FA ${shopId} \u91C7\u96C6\u7B2C ${page} \u9875`);
  3423. const result = await relayClient.forwardSorftime({
  3424. path: "/api/ProductQuery",
  3425. method: "POST",
  3426. query: { domain },
  3427. body: {
  3428. Query: 1,
  3429. QueryType: 5,
  3430. Pattern: sellerId,
  3431. Page: page
  3432. }
  3433. });
  3434. if (!relaySucceeded(result)) {
  3435. throw new Error(`\u7B2C ${page} \u9875\u91C7\u96C6\u5931\u8D25: ${result.message || result.Message || JSON.stringify(result)}`);
  3436. }
  3437. const payload = unwrapRelayData(result);
  3438. const items = payload?.Items || payload?.Products || (Array.isArray(payload) ? payload : []);
  3439. const count = Array.isArray(items) ? items.length : 0;
  3440. let saved = 0;
  3441. for (const item of items) {
  3442. if (await upsertSorftimeProduct(item, shopId, shopName, item?.MarketplaceId || marketplaceId || "", domain))
  3443. saved++;
  3444. }
  3445. totalProcessed += count;
  3446. progress?.({ type: "info", message: `\u7B2C ${page} \u9875\u83B7\u53D6 ${count} \u6761\u4EA7\u54C1\uFF0C\u5165\u5E93 ${saved} \u6761`, processed: totalProcessed });
  3447. 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`);
  3448. const pageCount = Number(payload?.PageCount || 0);
  3449. if (count === 0 || pageCount > 0 && page >= pageCount || page >= 100) {
  3450. hasMore = false;
  3451. } else {
  3452. page++;
  3453. await this.#delay(2e3);
  3454. }
  3455. }
  3456. return { processed: totalProcessed, pages: page };
  3457. }
  3458. /**
  3459. * 采集类目市场趋势(每月1号凌晨1点执行)
  3460. * @private
  3461. * @param {string} nodeId - 类目节点ID
  3462. * @param {number} domain - 站点域名代码
  3463. * @returns {Promise<void>}
  3464. */
  3465. async #collectCategoryMarketTrend(nodeId, domain, progress) {
  3466. console.log(`[Sorftime Scheduler] \u5F00\u59CB\u91C7\u96C6\u7C7B\u76EE ${nodeId} \u7684\u5E02\u573A\u8D8B\u52BF (domain: ${domain})`);
  3467. const functionId = "OCOKi93GjS";
  3468. const trendIndexes = [0, 1, 2, 3, 4, 5];
  3469. for (const TrendIndex of trendIndexes) {
  3470. try {
  3471. progress?.({ type: "info", message: `\u7C7B\u76EE ${nodeId} \u8D8B\u52BF\u7C7B\u578B ${TrendIndex}/5 \u91C7\u96C6\u4E2D...` });
  3472. const requestBody = {
  3473. path: "/api/CategoryTrend",
  3474. method: "POST",
  3475. query: { domain },
  3476. body: {
  3477. NodeId: nodeId,
  3478. TrendIndex
  3479. },
  3480. functionId
  3481. };
  3482. const result = await relayClient.forwardSorftime({
  3483. path: requestBody.path,
  3484. method: requestBody.method,
  3485. query: requestBody.query,
  3486. body: requestBody.body
  3487. });
  3488. if (!relaySucceeded(result)) {
  3489. throw new Error(result.message || `\u5E02\u573A\u8D8B\u52BF\u7C7B\u578B ${TrendIndex} \u91C7\u96C6\u5931\u8D25`);
  3490. }
  3491. console.log(`[Sorftime Scheduler] \u7C7B\u76EE ${nodeId} \u5E02\u573A\u8D8B\u52BF\u7C7B\u578B ${TrendIndex} \u91C7\u96C6\u5B8C\u6210`);
  3492. await this.#delay(1e3);
  3493. } catch (error) {
  3494. console.error(`[Sorftime Scheduler] \u7C7B\u76EE ${nodeId} \u5E02\u573A\u8D8B\u52BF\u7C7B\u578B ${TrendIndex} \u91C7\u96C6\u5931\u8D25:`, error.message);
  3495. throw error;
  3496. }
  3497. }
  3498. }
  3499. /**
  3500. * 执行类目市场趋势采集任务(每月1号凌晨1点)
  3501. * @private
  3502. * @returns {Promise<any>} 执行结果对象
  3503. */
  3504. async #executeCategoryMarketTrendCollection(shopId, nodeIds, progress) {
  3505. if (this.#isRunning) {
  3506. console.log("[Sorftime Scheduler] \u4EFB\u52A1\u6B63\u5728\u6267\u884C\u4E2D\uFF0C\u8DF3\u8FC7\u672C\u6B21\u8C03\u5EA6");
  3507. return {
  3508. success: false,
  3509. message: "\u4EFB\u52A1\u6B63\u5728\u6267\u884C\u4E2D",
  3510. successCount: 0,
  3511. errorCount: 0,
  3512. duration: 0,
  3513. errors: []
  3514. };
  3515. }
  3516. this.#isRunning = true;
  3517. const startTime = Date.now();
  3518. const errors = [];
  3519. let successCount = 0;
  3520. let errorCount = 0;
  3521. console.log("[Sorftime Scheduler] ========================================");
  3522. console.log("[Sorftime Scheduler] \u5F00\u59CB\u6267\u884C\u7C7B\u76EE\u5E02\u573A\u8D8B\u52BF\u91C7\u96C6\u4EFB\u52A1");
  3523. console.log("[Sorftime Scheduler] \u6267\u884C\u65F6\u95F4:", (/* @__PURE__ */ new Date()).toISOString());
  3524. console.log("[Sorftime Scheduler] ========================================");
  3525. try {
  3526. const categories = await this.#getLeafCategoriesByFilter(shopId, nodeIds);
  3527. if (categories.length === 0) {
  3528. console.log("[Sorftime Scheduler] \u6CA1\u6709\u627E\u5230\u53F6\u5B50\u8282\u70B9\u7C7B\u76EE\u6570\u636E");
  3529. return {
  3530. success: true,
  3531. message: "\u6CA1\u6709\u53F6\u5B50\u8282\u70B9\u7C7B\u76EE\u9700\u8981\u5904\u7406",
  3532. successCount: 0,
  3533. errorCount: 0,
  3534. duration: Date.now() - startTime,
  3535. errors: []
  3536. };
  3537. }
  3538. console.log(`[Sorftime Scheduler] \u53D1\u73B0 ${categories.length} \u4E2A\u53F6\u5B50\u8282\u70B9\u7C7B\u76EE`);
  3539. for (const category of categories) {
  3540. try {
  3541. const nodeId = category.get("nodeId");
  3542. const categoryName = category.get("name");
  3543. const domain = category.get("domain") || 1;
  3544. console.log(`
  3545. [Sorftime Scheduler] ----------------------------------------`);
  3546. console.log(`[Sorftime Scheduler] \u5F00\u59CB\u5904\u7406\u7C7B\u76EE\u5E02\u573A\u8D8B\u52BF: ${categoryName} (${nodeId}, domain: ${domain})`);
  3547. progress?.({ type: "info", message: `\u5F00\u59CB\u91C7\u96C6\u7C7B\u76EE\u3010${categoryName}\u3011\u5E02\u573A\u8D8B\u52BF` });
  3548. await this.#collectCategoryMarketTrend(nodeId, domain, progress);
  3549. successCount++;
  3550. progress?.({ type: "success", message: `\u7C7B\u76EE\u3010${categoryName}\u3011\u5E02\u573A\u8D8B\u52BF\u91C7\u96C6\u5B8C\u6210`, processed: successCount });
  3551. console.log(`[Sorftime Scheduler] \u7C7B\u76EE ${categoryName} \u5E02\u573A\u8D8B\u52BF\u91C7\u96C6\u5B8C\u6210`);
  3552. await this.#delay(2e3);
  3553. } catch (error) {
  3554. errorCount++;
  3555. const errorMsg = `\u7C7B\u76EE ${category.get("name")} \u5E02\u573A\u8D8B\u52BF\u91C7\u96C6\u5931\u8D25: ${error.message}`;
  3556. errors.push(errorMsg);
  3557. progress?.({ type: "error", message: errorMsg });
  3558. console.error(`[Sorftime Scheduler] ${errorMsg}`);
  3559. }
  3560. }
  3561. const duration = Date.now() - startTime;
  3562. await this.#logExecution({
  3563. taskName: "sorftime-category-trend-monthly",
  3564. startTime: new Date(startTime),
  3565. endTime: /* @__PURE__ */ new Date(),
  3566. duration,
  3567. successCount,
  3568. errorCount,
  3569. errors,
  3570. status: errorCount === 0 ? "success" : successCount > 0 ? "partial_success" : "failed"
  3571. });
  3572. this.#lastRunTime = /* @__PURE__ */ new Date();
  3573. 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`;
  3574. console.log(`
  3575. [Sorftime Scheduler] ========================================`);
  3576. console.log(`[Sorftime Scheduler] ${message}`);
  3577. console.log(`[Sorftime Scheduler] ========================================
  3578. `);
  3579. return {
  3580. success: errorCount === 0,
  3581. message,
  3582. successCount,
  3583. errorCount,
  3584. duration,
  3585. errors
  3586. };
  3587. } catch (error) {
  3588. const duration = Date.now() - startTime;
  3589. const errorMsg = `\u7C7B\u76EE\u5E02\u573A\u8D8B\u52BF\u91C7\u96C6\u4EFB\u52A1\u6267\u884C\u5931\u8D25: ${error.message}`;
  3590. console.error(`[Sorftime Scheduler] ${errorMsg}`);
  3591. return {
  3592. success: false,
  3593. message: errorMsg,
  3594. successCount,
  3595. errorCount: errorCount + 1,
  3596. duration,
  3597. errors: [...errors, errorMsg]
  3598. };
  3599. } finally {
  3600. this.#isRunning = false;
  3601. }
  3602. }
  3603. /**
  3604. * 执行类目热销产品采集任务(每月1号)
  3605. * @private
  3606. * @returns {Promise<any>} 执行结果对象,包含成功数、失败数、耗时等信息
  3607. */
  3608. async #executeCategoryProductsCollection(shopId, nodeIds, progress) {
  3609. if (this.#isRunning) {
  3610. console.log("[Sorftime Scheduler] \u4EFB\u52A1\u6B63\u5728\u6267\u884C\u4E2D\uFF0C\u8DF3\u8FC7\u672C\u6B21\u8C03\u5EA6");
  3611. return {
  3612. success: false,
  3613. message: "\u4EFB\u52A1\u6B63\u5728\u6267\u884C\u4E2D",
  3614. successCount: 0,
  3615. errorCount: 0,
  3616. duration: 0,
  3617. errors: []
  3618. };
  3619. }
  3620. this.#isRunning = true;
  3621. const startTime = Date.now();
  3622. const errors = [];
  3623. let successCount = 0;
  3624. let errorCount = 0;
  3625. console.log("[Sorftime Scheduler] ========================================");
  3626. console.log("[Sorftime Scheduler] \u5F00\u59CB\u6267\u884C\u7C7B\u76EE\u70ED\u9500\u4EA7\u54C1\u91C7\u96C6\u4EFB\u52A1");
  3627. console.log("[Sorftime Scheduler] \u6267\u884C\u65F6\u95F4:", (/* @__PURE__ */ new Date()).toISOString());
  3628. console.log("[Sorftime Scheduler] ========================================");
  3629. try {
  3630. const categories = await this.#getLeafCategoriesByFilter(shopId, nodeIds);
  3631. if (categories.length === 0) {
  3632. console.log("[Sorftime Scheduler] \u6CA1\u6709\u627E\u5230\u7C7B\u76EE\u6570\u636E");
  3633. return {
  3634. success: true,
  3635. message: "\u6CA1\u6709\u7C7B\u76EE\u9700\u8981\u5904\u7406",
  3636. successCount: 0,
  3637. errorCount: 0,
  3638. duration: Date.now() - startTime,
  3639. errors: []
  3640. };
  3641. }
  3642. console.log(`[Sorftime Scheduler] \u53D1\u73B0 ${categories.length} \u4E2A\u7C7B\u76EE`);
  3643. for (const category of categories) {
  3644. try {
  3645. const nodeId = category.get("nodeId");
  3646. const categoryName = category.get("name");
  3647. const domain = category.get("domain") || 1;
  3648. console.log(`
  3649. [Sorftime Scheduler] ----------------------------------------`);
  3650. console.log(`[Sorftime Scheduler] \u5F00\u59CB\u5904\u7406\u7C7B\u76EE: ${categoryName} (${nodeId}, domain: ${domain})`);
  3651. progress?.({ type: "info", message: `\u5F00\u59CB\u91C7\u96C6\u7C7B\u76EE\u3010${categoryName}\u3011\u70ED\u9500\u4EA7\u54C1` });
  3652. await this.#collectCategoryProducts(nodeId, domain, progress);
  3653. successCount++;
  3654. progress?.({ type: "success", message: `\u7C7B\u76EE\u3010${categoryName}\u3011\u70ED\u9500\u4EA7\u54C1\u91C7\u96C6\u5B8C\u6210`, processed: successCount });
  3655. console.log(`[Sorftime Scheduler] \u7C7B\u76EE ${categoryName} \u5904\u7406\u5B8C\u6210`);
  3656. await this.#delay(3e3);
  3657. } catch (error) {
  3658. errorCount++;
  3659. const errorMsg = `\u7C7B\u76EE ${category.get("name")} \u5904\u7406\u5931\u8D25: ${error.message}`;
  3660. errors.push(errorMsg);
  3661. progress?.({ type: "error", message: errorMsg });
  3662. console.error(`[Sorftime Scheduler] ${errorMsg}`);
  3663. }
  3664. }
  3665. const duration = Date.now() - startTime;
  3666. await this.#logExecution({
  3667. taskName: "sorftime-category-products-monthly",
  3668. startTime: new Date(startTime),
  3669. endTime: /* @__PURE__ */ new Date(),
  3670. duration,
  3671. successCount,
  3672. errorCount,
  3673. errors,
  3674. status: errorCount === 0 ? "success" : successCount > 0 ? "partial_success" : "failed"
  3675. });
  3676. this.#lastRunTime = /* @__PURE__ */ new Date();
  3677. 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`;
  3678. console.log(`
  3679. [Sorftime Scheduler] ========================================`);
  3680. console.log(`[Sorftime Scheduler] ${message}`);
  3681. console.log(`[Sorftime Scheduler] ========================================
  3682. `);
  3683. return {
  3684. success: errorCount === 0,
  3685. message,
  3686. successCount,
  3687. errorCount,
  3688. duration,
  3689. errors
  3690. };
  3691. } catch (error) {
  3692. const duration = Date.now() - startTime;
  3693. const errorMsg = `\u7C7B\u76EE\u70ED\u9500\u4EA7\u54C1\u91C7\u96C6\u4EFB\u52A1\u6267\u884C\u5931\u8D25: ${error.message}`;
  3694. console.error(`[Sorftime Scheduler] ${errorMsg}`);
  3695. return {
  3696. success: false,
  3697. message: errorMsg,
  3698. successCount,
  3699. errorCount: errorCount + 1,
  3700. duration,
  3701. errors: [...errors, errorMsg]
  3702. };
  3703. } finally {
  3704. this.#isRunning = false;
  3705. }
  3706. }
  3707. /**
  3708. * 执行类目关键词采集任务(每周一)
  3709. * @private
  3710. * @returns {Promise<any>} 执行结果对象,包含成功数、失败数、耗时等信息
  3711. */
  3712. async #executeCategoryKeywordsCollection(shopId, nodeIds, progress) {
  3713. if (this.#isRunning) {
  3714. console.log("[Sorftime Scheduler] \u4EFB\u52A1\u6B63\u5728\u6267\u884C\u4E2D\uFF0C\u8DF3\u8FC7\u672C\u6B21\u8C03\u5EA6");
  3715. return {
  3716. success: false,
  3717. message: "\u4EFB\u52A1\u6B63\u5728\u6267\u884C\u4E2D",
  3718. successCount: 0,
  3719. errorCount: 0,
  3720. duration: 0,
  3721. errors: []
  3722. };
  3723. }
  3724. this.#isRunning = true;
  3725. const startTime = Date.now();
  3726. const errors = [];
  3727. let successCount = 0;
  3728. let errorCount = 0;
  3729. console.log("[Sorftime Scheduler] ========================================");
  3730. console.log("[Sorftime Scheduler] \u5F00\u59CB\u6267\u884C\u7C7B\u76EE\u5173\u952E\u8BCD\u91C7\u96C6\u4EFB\u52A1");
  3731. console.log("[Sorftime Scheduler] \u6267\u884C\u65F6\u95F4:", (/* @__PURE__ */ new Date()).toISOString());
  3732. console.log("[Sorftime Scheduler] ========================================");
  3733. try {
  3734. const categories = await this.#getLeafCategoriesByFilter(shopId, nodeIds);
  3735. if (categories.length === 0) {
  3736. console.log("[Sorftime Scheduler] \u6CA1\u6709\u627E\u5230\u53F6\u5B50\u8282\u70B9\u7C7B\u76EE\u6570\u636E");
  3737. return {
  3738. success: true,
  3739. message: "\u6CA1\u6709\u53F6\u5B50\u8282\u70B9\u7C7B\u76EE\u9700\u8981\u5904\u7406",
  3740. successCount: 0,
  3741. errorCount: 0,
  3742. duration: Date.now() - startTime,
  3743. errors: []
  3744. };
  3745. }
  3746. console.log(`[Sorftime Scheduler] \u53D1\u73B0 ${categories.length} \u4E2A\u53F6\u5B50\u8282\u70B9\u7C7B\u76EE`);
  3747. for (const category of categories) {
  3748. try {
  3749. const nodeId = category.get("nodeId");
  3750. const categoryName = category.get("name");
  3751. const domain = category.get("domain") || 1;
  3752. console.log(`
  3753. [Sorftime Scheduler] ----------------------------------------`);
  3754. console.log(`[Sorftime Scheduler] \u5F00\u59CB\u5904\u7406\u7C7B\u76EE: ${categoryName} (${nodeId}, domain: ${domain})`);
  3755. progress?.({ type: "info", message: `\u5F00\u59CB\u91C7\u96C6\u7C7B\u76EE\u3010${categoryName}\u3011\u5173\u952E\u8BCD` });
  3756. await this.#collectCategoryKeywords(nodeId, domain, progress);
  3757. successCount++;
  3758. progress?.({ type: "success", message: `\u7C7B\u76EE\u3010${categoryName}\u3011\u5173\u952E\u8BCD\u91C7\u96C6\u5B8C\u6210`, processed: successCount });
  3759. console.log(`[Sorftime Scheduler] \u7C7B\u76EE ${categoryName} \u5173\u952E\u8BCD\u91C7\u96C6\u5B8C\u6210`);
  3760. await this.#delay(2e3);
  3761. } catch (error) {
  3762. errorCount++;
  3763. const errorMsg = `\u7C7B\u76EE ${category.get("name")} \u5173\u952E\u8BCD\u91C7\u96C6\u5931\u8D25: ${error.message}`;
  3764. errors.push(errorMsg);
  3765. progress?.({ type: "error", message: errorMsg });
  3766. console.error(`[Sorftime Scheduler] ${errorMsg}`);
  3767. }
  3768. }
  3769. const duration = Date.now() - startTime;
  3770. await this.#logExecution({
  3771. taskName: "sorftime-category-keywords-weekly",
  3772. startTime: new Date(startTime),
  3773. endTime: /* @__PURE__ */ new Date(),
  3774. duration,
  3775. successCount,
  3776. errorCount,
  3777. errors,
  3778. status: errorCount === 0 ? "success" : successCount > 0 ? "partial_success" : "failed"
  3779. });
  3780. this.#lastRunTime = /* @__PURE__ */ new Date();
  3781. 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`;
  3782. console.log(`
  3783. [Sorftime Scheduler] ========================================`);
  3784. console.log(`[Sorftime Scheduler] ${message}`);
  3785. console.log(`[Sorftime Scheduler] ========================================
  3786. `);
  3787. return {
  3788. success: errorCount === 0,
  3789. message,
  3790. successCount,
  3791. errorCount,
  3792. duration,
  3793. errors
  3794. };
  3795. } catch (error) {
  3796. const duration = Date.now() - startTime;
  3797. const errorMsg = `\u7C7B\u76EE\u5173\u952E\u8BCD\u91C7\u96C6\u4EFB\u52A1\u6267\u884C\u5931\u8D25: ${error.message}`;
  3798. console.error(`[Sorftime Scheduler] ${errorMsg}`);
  3799. return {
  3800. success: false,
  3801. message: errorMsg,
  3802. successCount,
  3803. errorCount: errorCount + 1,
  3804. duration,
  3805. errors: [...errors, errorMsg]
  3806. };
  3807. } finally {
  3808. this.#isRunning = false;
  3809. }
  3810. }
  3811. /**
  3812. * 执行云函数调用任务(每两小时)
  3813. * 调用指定的云函数进行数据处理
  3814. * @private
  3815. * @returns {Promise<any>} 执行结果对象
  3816. */
  3817. async #executeCloudFunctionInvocation() {
  3818. console.log("[Sorftime Scheduler] ========================================");
  3819. console.log("[Sorftime Scheduler] \u5F00\u59CB\u6267\u884C\u4E91\u51FD\u6570\u8C03\u7528\u4EFB\u52A1");
  3820. console.log("[Sorftime Scheduler] \u6267\u884C\u65F6\u95F4:", (/* @__PURE__ */ new Date()).toISOString());
  3821. console.log("[Sorftime Scheduler] ========================================");
  3822. const startTime = Date.now();
  3823. try {
  3824. const response = await fetch("http://localhost:3000/api/functions", {
  3825. method: "POST",
  3826. headers: {
  3827. "Content-Type": "application/json"
  3828. },
  3829. body: JSON.stringify({
  3830. id: "Z4z6SB4o9e"
  3831. })
  3832. });
  3833. const duration = Date.now() - startTime;
  3834. if (!response.ok) {
  3835. const errorMsg = `\u4E91\u51FD\u6570\u8C03\u7528\u5931\u8D25\uFF0C\u72B6\u6001\u7801: ${response.status}`;
  3836. console.error(`[Sorftime Scheduler] ${errorMsg}`);
  3837. return {
  3838. success: false,
  3839. message: errorMsg,
  3840. duration,
  3841. errors: [errorMsg]
  3842. };
  3843. }
  3844. const result = await response.json();
  3845. const message = `\u4E91\u51FD\u6570\u8C03\u7528\u5B8C\u6210\uFF0C\u8017\u65F6 ${Math.round(duration / 1e3)}\u79D2`;
  3846. console.log(`
  3847. [Sorftime Scheduler] ========================================`);
  3848. console.log(`[Sorftime Scheduler] ${message}`);
  3849. console.log(`[Sorftime Scheduler] ========================================
  3850. `);
  3851. return {
  3852. success: true,
  3853. message,
  3854. duration,
  3855. data: result,
  3856. errors: []
  3857. };
  3858. } catch (error) {
  3859. const duration = Date.now() - startTime;
  3860. const errorMsg = `\u4E91\u51FD\u6570\u8C03\u7528\u4EFB\u52A1\u6267\u884C\u5931\u8D25: ${error.message}`;
  3861. console.error(`[Sorftime Scheduler] ${errorMsg}`);
  3862. return {
  3863. success: false,
  3864. message: errorMsg,
  3865. duration,
  3866. errors: [errorMsg]
  3867. };
  3868. }
  3869. }
  3870. /**
  3871. * 执行产品评论采集任务(每天凌晨1点)
  3872. * 根据 Product 表中的 ASIN,采集对应的评论信息
  3873. * @private
  3874. * @returns {Promise<any>} 执行结果对象
  3875. */
  3876. async #executeProductReviewsCollection(progress) {
  3877. console.log("[Sorftime Scheduler] ========================================");
  3878. console.log("[Sorftime Scheduler] \u5F00\u59CB\u6267\u884C\u4EA7\u54C1\u8BC4\u8BBA\u91C7\u96C6\u4EFB\u52A1");
  3879. console.log("[Sorftime Scheduler] \u6267\u884C\u65F6\u95F4:", (/* @__PURE__ */ new Date()).toISOString());
  3880. console.log("[Sorftime Scheduler] ========================================");
  3881. const startTime = Date.now();
  3882. let successCount = 0;
  3883. let errorCount = 0;
  3884. const errors = [];
  3885. progress?.({ type: "info", message: "\u5F00\u59CB\u6267\u884C\u4EA7\u54C1\u8BC4\u8BBA\u91C7\u96C6\u4EFB\u52A1" });
  3886. try {
  3887. const Parse = globalThis.Parse;
  3888. const productQuery = new Parse.Query("Product");
  3889. productQuery.limit(1e4);
  3890. const products = await productQuery.find({ useMasterKey: true });
  3891. if (products.length === 0) {
  3892. console.log("[Sorftime Scheduler] \u6CA1\u6709\u627E\u5230 Product \u6570\u636E");
  3893. progress?.({ type: "info", message: "\u6CA1\u6709 Product \u6570\u636E\u9700\u8981\u5904\u7406" });
  3894. return {
  3895. success: true,
  3896. message: "\u6CA1\u6709 Product \u6570\u636E\u9700\u8981\u5904\u7406",
  3897. successCount: 0,
  3898. errorCount: 0,
  3899. duration: Date.now() - startTime,
  3900. errors: []
  3901. };
  3902. }
  3903. console.log(`[Sorftime Scheduler] \u53D1\u73B0 ${products.length} \u4E2A Product \u8BB0\u5F55`);
  3904. progress?.({ type: "info", message: `\u5171\u53D1\u73B0 ${products.length} \u4E2A\u4EA7\u54C1\u9700\u8981\u68C0\u67E5\u8BC4\u8BBA` });
  3905. for (const product of products) {
  3906. try {
  3907. const asin = product.get("asin");
  3908. if (!asin) {
  3909. console.log("[Sorftime Scheduler] \u8DF3\u8FC7\u65E0 ASIN \u7684 Product");
  3910. continue;
  3911. }
  3912. progress?.({ type: "info", message: `\u5F00\u59CB\u5904\u7406 ASIN: ${asin}` });
  3913. const shopRelation = product.get("shop");
  3914. if (!shopRelation) {
  3915. console.log(`[Sorftime Scheduler] Product \u65E0\u5173\u8054 Shop\uFF0CASIN: ${asin}`);
  3916. continue;
  3917. }
  3918. const shop = await shopRelation.fetch({ useMasterKey: true });
  3919. const shopDomain = shop.get("domain") || 1;
  3920. let queryStartDt = "2025-01-01";
  3921. const reviewQuery = new Parse.Query("SorftimeReviews");
  3922. reviewQuery.equalTo("asin", asin);
  3923. reviewQuery.descending("updatedAt");
  3924. reviewQuery.limit(1);
  3925. let latestReview = null;
  3926. try {
  3927. latestReview = await reviewQuery.first({ useMasterKey: true });
  3928. } catch (error) {
  3929. const message2 = String(error?.message || error || "");
  3930. if (!message2.includes("does not exist") && !message2.includes("non-existent class"))
  3931. throw error;
  3932. }
  3933. if (latestReview) {
  3934. const updateAt = latestReview.get("updatedAt");
  3935. if (updateAt) {
  3936. const date = new Date(updateAt);
  3937. queryStartDt = date.toISOString().split("T")[0];
  3938. }
  3939. }
  3940. console.log(`[Sorftime Scheduler] \u5F00\u59CB\u91C7\u96C6\u8BC4\u8BBA\uFF0CASIN: ${asin}, \u67E5\u8BE2\u8D77\u59CB\u65E5\u671F: ${queryStartDt}`);
  3941. let result;
  3942. try {
  3943. result = await relayClient.forwardSorftime({
  3944. path: "/api/ProductReviewsQuery",
  3945. method: "POST",
  3946. query: {
  3947. domain: shopDomain,
  3948. shopId: shop.id
  3949. },
  3950. body: {
  3951. ASIN: asin,
  3952. PageIndex: 1,
  3953. OnlyPurchase: 1,
  3954. Star: "1,2,3,4,5",
  3955. QueryStartDt: queryStartDt
  3956. }
  3957. });
  3958. } catch (error) {
  3959. const errorMsg = `\u8BF7\u6C42\u8BC4\u8BBA\u6570\u636E\u5931\u8D25\uFF0CASIN: ${asin}, ${error.message || error}`;
  3960. errors.push(errorMsg);
  3961. errorCount++;
  3962. console.error(`[Sorftime Scheduler] ${errorMsg}`);
  3963. continue;
  3964. }
  3965. const payload = unwrapRelayData(result);
  3966. const reviews = payload?.Reviews || payload?.Items || payload?.List || (Array.isArray(payload) ? payload : []);
  3967. if (relaySucceeded(result)) {
  3968. for (const review of reviews)
  3969. await upsertSorftimeReview(review, asin, shop.id);
  3970. successCount++;
  3971. const statusText = reviews.length ? `\u5165\u5E93 ${reviews.length} \u6761` : "\u65E0\u65B0\u589E\u8BC4\u8BBA";
  3972. console.log(`[Sorftime Scheduler] \u8BC4\u8BBA\u6570\u636E\u91C7\u96C6\u6210\u529F\uFF0CASIN: ${asin}\uFF0C${statusText}`);
  3973. progress?.({ type: "success", message: `\u8BC4\u8BBA\u6570\u636E\u91C7\u96C6\u6210\u529F\uFF0CASIN: ${asin}\uFF0C${statusText}`, processed: successCount });
  3974. } else {
  3975. const errorMsg = `\u8BC4\u8BBA\u6570\u636E\u83B7\u53D6\u5931\u8D25\uFF0CASIN: ${asin}`;
  3976. errors.push(errorMsg);
  3977. errorCount++;
  3978. console.error(`[Sorftime Scheduler] ${errorMsg}`);
  3979. progress?.({ type: "error", message: errorMsg });
  3980. }
  3981. await this.#delay(500);
  3982. } catch (error) {
  3983. errorCount++;
  3984. const errorMsg = `\u5904\u7406 Product \u5931\u8D25: ${error.message}`;
  3985. errors.push(errorMsg);
  3986. console.error(`[Sorftime Scheduler] ${errorMsg}`);
  3987. progress?.({ type: "error", message: errorMsg });
  3988. }
  3989. }
  3990. const duration = Date.now() - startTime;
  3991. 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`;
  3992. console.log(`
  3993. [Sorftime Scheduler] ========================================`);
  3994. console.log(`[Sorftime Scheduler] ${message}`);
  3995. console.log(`[Sorftime Scheduler] ========================================
  3996. `);
  3997. progress?.({ type: errorCount === 0 ? "success" : "warning", message });
  3998. return {
  3999. success: errorCount === 0,
  4000. message,
  4001. successCount,
  4002. errorCount,
  4003. duration,
  4004. errors
  4005. };
  4006. } catch (error) {
  4007. const duration = Date.now() - startTime;
  4008. const errorMsg = `\u4EA7\u54C1\u8BC4\u8BBA\u91C7\u96C6\u4EFB\u52A1\u6267\u884C\u5931\u8D25: ${error.message}`;
  4009. console.error(`[Sorftime Scheduler] ${errorMsg}`);
  4010. progress?.({ type: "error", message: errorMsg });
  4011. return {
  4012. success: false,
  4013. message: errorMsg,
  4014. successCount: 0,
  4015. errorCount: 1,
  4016. duration,
  4017. errors: [errorMsg]
  4018. };
  4019. }
  4020. }
  4021. // ========== 公共方法(后声明,因为依赖前面的私有方法) ==========
  4022. /**
  4023. * 构造函数
  4024. * 初始化 Sorftime API 数据采集调度器
  4025. */
  4026. constructor() {
  4027. console.log("[Sorftime Scheduler] \u521D\u59CB\u5316 Sorftime API \u6570\u636E\u91C7\u96C6\u8C03\u5EA6\u5668");
  4028. }
  4029. /**
  4030. * 启动定时任务
  4031. * @returns {Promise<void>}
  4032. */
  4033. async start() {
  4034. if (this.#categoryProductsCronTask || this.#keywordsCronTask || this.#marketTrendCronTask || this.#shopProductsCronTask || this.#productDetailCronTask || this.#cloudFunctionCronTask || this.#reviewsCronTask) {
  4035. console.log("[Sorftime Scheduler] \u5B9A\u65F6\u4EFB\u52A1\u5DF2\u5728\u8FD0\u884C\u4E2D");
  4036. return;
  4037. }
  4038. this.#categoryProductsCronTask = nodeCron3.schedule("0 0 1 * *", async () => {
  4039. await this.#executeCategoryProductsCollection(void 0, void 0, void 0);
  4040. }, {
  4041. timezone: "Asia/Shanghai"
  4042. });
  4043. 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");
  4044. this.#marketTrendCronTask = nodeCron3.schedule("0 1 1 * *", async () => {
  4045. await this.#executeCategoryMarketTrendCollection(void 0, void 0, void 0);
  4046. }, {
  4047. timezone: "Asia/Shanghai"
  4048. });
  4049. 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");
  4050. this.#keywordsCronTask = nodeCron3.schedule("0 1 * * 1", async () => {
  4051. await this.#executeCategoryKeywordsCollection(void 0, void 0, void 0);
  4052. }, {
  4053. timezone: "Asia/Shanghai"
  4054. });
  4055. 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");
  4056. if (process.env.ENABLE_SORFTIME_REVIEW_SCHEDULES === "true") {
  4057. this.#reviewsCronTask = nodeCron3.schedule("0 1 * * *", async () => {
  4058. console.log("[Sorftime Scheduler] ===== \u51CC\u66681\u70B9\uFF1A\u5F00\u59CB\u4EA7\u54C1\u8BC4\u8BBA\u91C7\u96C6 =====");
  4059. try {
  4060. await this.#executeProductReviewsCollection(void 0);
  4061. } catch (e) {
  4062. console.error("[Sorftime Scheduler] \u4EA7\u54C1\u8BC4\u8BBA\u91C7\u96C6\u5931\u8D25:", e.message);
  4063. }
  4064. console.log("[Sorftime Scheduler] ===== \u4EA7\u54C1\u8BC4\u8BBA\u91C7\u96C6\u5B8C\u6210 =====");
  4065. }, { timezone: "Asia/Shanghai" });
  4066. 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");
  4067. } else {
  4068. console.log("[Sorftime Scheduler] \u4EA7\u54C1\u8BC4\u8BBA\u91C7\u96C6\u5B9A\u65F6\u4EFB\u52A1\u5DF2\u7981\u7528");
  4069. }
  4070. this.#shopProductsCronTask = nodeCron3.schedule("0 2 * * *", async () => {
  4071. console.log("[Sorftime Scheduler] ===== \u51CC\u66682\u70B9\uFF1A\u5F00\u59CB\u5E97\u94FA\u4EA7\u54C1\u91C7\u96C6 =====");
  4072. try {
  4073. await this.collectShopProducts(void 0, void 0);
  4074. } catch (e) {
  4075. console.error("[Sorftime Scheduler] \u5E97\u94FA\u4EA7\u54C1\u91C7\u96C6\u5931\u8D25:", e.message);
  4076. }
  4077. console.log("[Sorftime Scheduler] ===== \u5E97\u94FA\u4EA7\u54C1\u91C7\u96C6\u5B8C\u6210 =====");
  4078. }, { timezone: "Asia/Shanghai" });
  4079. 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");
  4080. if (process.env.ENABLE_LEGACY_CLOUD_FUNCTION_SCHEDULES === "true") {
  4081. this.#cloudFunctionCronTask = nodeCron3.schedule("0 */2 * * *", async () => {
  4082. try {
  4083. await this.#executeCloudFunctionInvocation();
  4084. } catch (e) {
  4085. console.error("[Sorftime Scheduler] \u9057\u7559\u4E91\u51FD\u6570\u8C03\u7528\u5931\u8D25:", e.message);
  4086. }
  4087. }, { timezone: "Asia/Shanghai" });
  4088. } else {
  4089. console.log("[Sorftime Scheduler] \u9057\u7559\u52A8\u6001\u4E91\u51FD\u6570\u5B9A\u65F6\u4EFB\u52A1\u5DF2\u7981\u7528");
  4090. }
  4091. }
  4092. /**
  4093. * 停止定时任务
  4094. * @returns {void}
  4095. */
  4096. stop() {
  4097. if (this.#categoryProductsCronTask) {
  4098. this.#categoryProductsCronTask.stop();
  4099. this.#categoryProductsCronTask = null;
  4100. console.log("[Sorftime Scheduler] \u7C7B\u76EE\u70ED\u9500\u4EA7\u54C1\u5B9A\u65F6\u4EFB\u52A1\u5DF2\u505C\u6B62");
  4101. }
  4102. if (this.#marketTrendCronTask) {
  4103. this.#marketTrendCronTask.stop();
  4104. this.#marketTrendCronTask = null;
  4105. console.log("[Sorftime Scheduler] \u7C7B\u76EE\u5E02\u573A\u8D8B\u52BF\u5B9A\u65F6\u4EFB\u52A1\u5DF2\u505C\u6B62");
  4106. }
  4107. if (this.#keywordsCronTask) {
  4108. this.#keywordsCronTask.stop();
  4109. this.#keywordsCronTask = null;
  4110. console.log("[Sorftime Scheduler] \u7C7B\u76EE\u5173\u952E\u8BCD\u5B9A\u65F6\u4EFB\u52A1\u5DF2\u505C\u6B62");
  4111. }
  4112. if (this.#shopProductsCronTask) {
  4113. this.#shopProductsCronTask.stop();
  4114. this.#shopProductsCronTask = null;
  4115. console.log("[Sorftime Scheduler] \u5E97\u94FA\u4EA7\u54C1\u91C7\u96C6\u5B9A\u65F6\u4EFB\u52A1\u5DF2\u505C\u6B62");
  4116. }
  4117. if (this.#productDetailCronTask) {
  4118. this.#productDetailCronTask.stop();
  4119. this.#productDetailCronTask = null;
  4120. console.log("[Sorftime Scheduler] \u4EA7\u54C1\u8BE6\u60C5\u540C\u6B65\u5B9A\u65F6\u4EFB\u52A1\u5DF2\u505C\u6B62");
  4121. }
  4122. if (this.#cloudFunctionCronTask) {
  4123. this.#cloudFunctionCronTask.stop();
  4124. this.#cloudFunctionCronTask = null;
  4125. console.log("[Sorftime Scheduler] \u4E91\u51FD\u6570\u8C03\u7528\u5B9A\u65F6\u4EFB\u52A1\u5DF2\u505C\u6B62");
  4126. }
  4127. if (this.#reviewsCronTask) {
  4128. this.#reviewsCronTask.stop();
  4129. this.#reviewsCronTask = null;
  4130. console.log("[Sorftime Scheduler] \u4EA7\u54C1\u8BC4\u8BBA\u91C7\u96C6\u5B9A\u65F6\u4EFB\u52A1\u5DF2\u505C\u6B62");
  4131. }
  4132. }
  4133. /**
  4134. * 获取调度器状态
  4135. * @returns {object} 调度器状态对象
  4136. */
  4137. getStatus() {
  4138. return {
  4139. isRunning: this.#isRunning,
  4140. lastRunTime: this.#lastRunTime,
  4141. cronEnabled: this.#cronEnabled,
  4142. tasks: {
  4143. categoryProducts: {
  4144. enabled: !!this.#categoryProductsCronTask,
  4145. schedule: "\u6BCF\u67081\u53F7\u51CC\u6668"
  4146. },
  4147. marketTrend: {
  4148. enabled: !!this.#marketTrendCronTask,
  4149. schedule: "\u6BCF\u67081\u53F7\u51CC\u66681\u70B9"
  4150. },
  4151. keywords: {
  4152. enabled: !!this.#keywordsCronTask,
  4153. schedule: "\u6BCF\u5468\u4E00\u51CC\u66681\u70B9"
  4154. },
  4155. shopProducts: {
  4156. enabled: !!this.#shopProductsCronTask,
  4157. schedule: "\u6BCF\u5929\u51CC\u66682\u70B9"
  4158. },
  4159. productDetails: {
  4160. enabled: !!this.#productDetailCronTask,
  4161. schedule: "\u6BCF\u5C0F\u65F6"
  4162. },
  4163. cloudFunction: {
  4164. enabled: !!this.#cloudFunctionCronTask,
  4165. schedule: "\u6BCF\u4E24\u5C0F\u65F6"
  4166. },
  4167. productReviews: {
  4168. enabled: !!this.#reviewsCronTask,
  4169. schedule: "\u6BCF\u5929\u51CC\u66681\u70B9"
  4170. }
  4171. }
  4172. };
  4173. }
  4174. /**
  4175. * 手动触发类目热销产品采集
  4176. * @returns {Promise<any>} 执行结果对象
  4177. */
  4178. async triggerCategoryProducts(shopId, nodeIds, progress) {
  4179. console.log("[Sorftime Scheduler] \u624B\u52A8\u89E6\u53D1\u7C7B\u76EE\u70ED\u9500\u4EA7\u54C1\u91C7\u96C6\u4EFB\u52A1");
  4180. return await this.#executeCategoryProductsCollection(shopId, nodeIds, progress);
  4181. }
  4182. /**
  4183. * 手动触发类目关键词采集
  4184. * @returns {Promise<any>} 执行结果对象
  4185. */
  4186. async triggerCategoryKeywords(shopId, nodeIds, progress) {
  4187. console.log("[Sorftime Scheduler] \u624B\u52A8\u89E6\u53D1\u7C7B\u76EE\u5173\u952E\u8BCD\u91C7\u96C6\u4EFB\u52A1");
  4188. return await this.#executeCategoryKeywordsCollection(shopId, nodeIds, progress);
  4189. }
  4190. /**
  4191. * 手动触发类目市场趋势采集
  4192. * @returns {Promise<any>} 执行结果对象
  4193. */
  4194. async triggerCategoryTrends(shopId, nodeIds, progress) {
  4195. console.log("[Sorftime Scheduler] \u624B\u52A8\u89E6\u53D1\u7C7B\u76EE\u5E02\u573A\u8D8B\u52BF\u91C7\u96C6\u4EFB\u52A1");
  4196. return await this.#executeCategoryMarketTrendCollection(shopId, nodeIds, progress);
  4197. }
  4198. /**
  4199. * 手动触发产品评论采集
  4200. * @returns {Promise<any>} 执行结果对象
  4201. */
  4202. async triggerProductReviews(progress) {
  4203. console.log("[Sorftime Scheduler] \u624B\u52A8\u89E6\u53D1\u4EA7\u54C1\u8BC4\u8BBA\u91C7\u96C6\u4EFB\u52A1");
  4204. return await this.#executeProductReviewsCollection(progress);
  4205. }
  4206. /**
  4207. * 按需触发单个类目的全量数据采集(热销产品 + 关键词 + 市场趋势)
  4208. * 前端选中某个类目但数据库无数据时调用
  4209. * @param {string} nodeId - 类目节点ID
  4210. * @param {number} domain - 站点域名代码(默认1=美国)
  4211. * @returns {Promise<any>} 执行结果对象
  4212. */
  4213. async triggerSingleCategory(nodeId, domain = 1, progress) {
  4214. const startTime = Date.now();
  4215. const errors = [];
  4216. let tasks = { products: false, keywords: false, trends: false };
  4217. console.log(`[Sorftime Scheduler] \u6309\u9700\u89E6\u53D1\u5355\u7C7B\u76EE\u91C7\u96C6: nodeId=${nodeId}, domain=${domain}`);
  4218. try {
  4219. try {
  4220. progress?.({ type: "info", message: `\u5F00\u59CB\u91C7\u96C6\u7C7B\u76EE ${nodeId} \u70ED\u9500\u4EA7\u54C1` });
  4221. await this.#collectCategoryProducts(nodeId, domain, progress);
  4222. tasks.products = true;
  4223. progress?.({ type: "success", message: `\u7C7B\u76EE ${nodeId} \u70ED\u9500\u4EA7\u54C1\u91C7\u96C6\u5B8C\u6210` });
  4224. console.log(`[Sorftime Scheduler] \u5355\u7C7B\u76EE ${nodeId} \u70ED\u9500\u4EA7\u54C1\u91C7\u96C6\u5B8C\u6210`);
  4225. } catch (error) {
  4226. const errMsg = error?.message || String(error);
  4227. progress?.({ type: "error", message: `\u70ED\u9500\u4EA7\u54C1\u91C7\u96C6\u5931\u8D25: ${errMsg}` });
  4228. errors.push(`\u70ED\u9500\u4EA7\u54C1\u91C7\u96C6\u5931\u8D25: ${errMsg}`);
  4229. console.error(`[Sorftime Scheduler] \u70ED\u9500\u4EA7\u54C1\u91C7\u96C6\u5931\u8D25:`, error);
  4230. }
  4231. await new Promise((resolve) => setTimeout(resolve, 500));
  4232. try {
  4233. progress?.({ type: "info", message: `\u5F00\u59CB\u91C7\u96C6\u7C7B\u76EE ${nodeId} \u5173\u952E\u8BCD` });
  4234. await this.#collectCategoryKeywords(nodeId, domain, progress);
  4235. tasks.keywords = true;
  4236. progress?.({ type: "success", message: `\u7C7B\u76EE ${nodeId} \u5173\u952E\u8BCD\u91C7\u96C6\u5B8C\u6210` });
  4237. console.log(`[Sorftime Scheduler] \u5355\u7C7B\u76EE ${nodeId} \u5173\u952E\u8BCD\u91C7\u96C6\u5B8C\u6210`);
  4238. } catch (error) {
  4239. const errMsg = error?.message || String(error);
  4240. progress?.({ type: "error", message: `\u5173\u952E\u8BCD\u91C7\u96C6\u5931\u8D25: ${errMsg}` });
  4241. errors.push(`\u5173\u952E\u8BCD\u91C7\u96C6\u5931\u8D25: ${errMsg}`);
  4242. console.error(`[Sorftime Scheduler] \u5173\u952E\u8BCD\u91C7\u96C6\u5931\u8D25:`, error);
  4243. }
  4244. await new Promise((resolve) => setTimeout(resolve, 500));
  4245. try {
  4246. progress?.({ type: "info", message: `\u5F00\u59CB\u91C7\u96C6\u7C7B\u76EE ${nodeId} \u5E02\u573A\u8D8B\u52BF` });
  4247. await this.#collectCategoryMarketTrend(nodeId, domain, progress);
  4248. tasks.trends = true;
  4249. progress?.({ type: "success", message: `\u7C7B\u76EE ${nodeId} \u5E02\u573A\u8D8B\u52BF\u91C7\u96C6\u5B8C\u6210` });
  4250. console.log(`[Sorftime Scheduler] \u5355\u7C7B\u76EE ${nodeId} \u5E02\u573A\u8D8B\u52BF\u91C7\u96C6\u5B8C\u6210`);
  4251. } catch (error) {
  4252. const errMsg = error?.message || String(error);
  4253. progress?.({ type: "error", message: `\u5E02\u573A\u8D8B\u52BF\u91C7\u96C6\u5931\u8D25: ${errMsg}` });
  4254. errors.push(`\u5E02\u573A\u8D8B\u52BF\u91C7\u96C6\u5931\u8D25: ${errMsg}`);
  4255. console.error(`[Sorftime Scheduler] \u5E02\u573A\u8D8B\u52BF\u91C7\u96C6\u5931\u8D25:`, error);
  4256. }
  4257. const duration = Date.now() - startTime;
  4258. const allSuccess = errors.length === 0;
  4259. const successCount = Object.values(tasks).filter(Boolean).length;
  4260. if (this.#logExecution) {
  4261. this.#logExecution({
  4262. taskName: `trigger-single-category-${nodeId}`,
  4263. startTime: new Date(startTime),
  4264. endTime: /* @__PURE__ */ new Date(),
  4265. duration,
  4266. successCount,
  4267. errorCount: errors.length,
  4268. errors,
  4269. status: allSuccess ? "success" : "partial_success"
  4270. }).catch((err) => console.error("[Sorftime Scheduler] Log execution error:", err));
  4271. }
  4272. const result = {
  4273. success: allSuccess,
  4274. 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`,
  4275. nodeId,
  4276. domain,
  4277. tasks,
  4278. duration,
  4279. errors: errors.length > 0 ? errors : void 0
  4280. };
  4281. console.log(`[Sorftime Scheduler] \u91C7\u96C6\u7ED3\u679C:`, result);
  4282. return result;
  4283. } catch (err) {
  4284. const duration = Date.now() - startTime;
  4285. const errMsg = err?.message || String(err);
  4286. console.error(`[Sorftime Scheduler] \u91C7\u96C6\u5F02\u5E38:`, err);
  4287. return {
  4288. success: false,
  4289. message: `\u7C7B\u76EE ${nodeId} \u91C7\u96C6\u5F02\u5E38: ${errMsg}`,
  4290. nodeId,
  4291. domain,
  4292. tasks,
  4293. duration,
  4294. errors: [errMsg]
  4295. };
  4296. }
  4297. }
  4298. /**
  4299. * 采集所有活跃 Amazon 店铺(或指定店铺)的产品数据
  4300. * 通过 SellerId (QueryType=5) 分页请求 /api/ProductQuery
  4301. * @param {string} shopId - 可选,不传则采集全部活跃 Amazon 店铺
  4302. * @param {Function} progress - 可选进度回调,适合 SSE 实时推送
  4303. * @returns {Promise<any>} 执行结果
  4304. */
  4305. async collectShopProducts(shopId, progress) {
  4306. const shops = await this.#getActiveAmazonShops(shopId);
  4307. if (shops.length === 0) {
  4308. 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";
  4309. progress?.({ type: "warn", message: msg });
  4310. console.warn(`[Sorftime Scheduler] ${msg}`);
  4311. return { success: true, message: msg, processed: 0, errorCount: 0, errors: [] };
  4312. }
  4313. let totalProcessed = 0;
  4314. let errorCount = 0;
  4315. const errors = [];
  4316. for (const shop of shops) {
  4317. const sid = shop.id;
  4318. const shopName = shop.get("name");
  4319. const config = shop.get("config");
  4320. const domain = Number(shop.get("domain") || 1);
  4321. const marketplaceId = shop.get("marketplaceId") || "";
  4322. if (!config || !config.SpApiConfig || !config.SpApiConfig.sellerID) {
  4323. const msg = `\u5E97\u94FA\u3010${shopName}\u3011(${sid}) \u7F3A\u5C11 SpApiConfig.sellerID \u914D\u7F6E\uFF0C\u8DF3\u8FC7`;
  4324. progress?.({ type: "error", message: msg });
  4325. console.error(`[Sorftime Scheduler] ${msg}`);
  4326. errors.push(msg);
  4327. errorCount++;
  4328. continue;
  4329. }
  4330. if (config.SpApiConfig.listingEnabled === false) {
  4331. 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` });
  4332. continue;
  4333. }
  4334. const sellerId = config.SpApiConfig.sellerID;
  4335. progress?.({ type: "info", message: `\u5F00\u59CB\u91C7\u96C6\u5E97\u94FA\u3010${shopName}\u3011\u7684\u4EA7\u54C1\u6570\u636E (SellerId: ${sellerId})` });
  4336. console.log(`[Sorftime Scheduler] \u5F00\u59CB\u91C7\u96C6\u5E97\u94FA ${shopName} (${sid}) \u4EA7\u54C1\u6570\u636E`);
  4337. try {
  4338. const result = await this.#doCollectShopProducts(sid, shopName, sellerId, domain, marketplaceId, progress);
  4339. 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 });
  4340. console.log(`[Sorftime Scheduler] \u5E97\u94FA ${shopName} \u4EA7\u54C1\u91C7\u96C6\u5B8C\u6210: ${result.processed} \u6761`);
  4341. totalProcessed += result.processed;
  4342. await this.#delay(2e3);
  4343. } catch (error) {
  4344. const msg = `\u5E97\u94FA\u3010${shopName}\u3011\u4EA7\u54C1\u91C7\u96C6\u5931\u8D25: ${error.message}`;
  4345. progress?.({ type: "error", message: msg });
  4346. console.error(`[Sorftime Scheduler] ${msg}`);
  4347. errors.push(msg);
  4348. errorCount++;
  4349. }
  4350. }
  4351. 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`;
  4352. return { success: errorCount === 0, message, processed: totalProcessed, errorCount, errors };
  4353. }
  4354. };
  4355. var sorftimeScheduler = new SorftimeScheduler();
  4356. // backend/api/module/schedule/routes-schedule.ts
  4357. (async () => {
  4358. try {
  4359. await spApiScheduler.start();
  4360. console.log("[API Routes] Amazon SP-API\u5B9A\u65F6\u4EFB\u52A1\u5DF2\u542F\u52A8");
  4361. } catch (error) {
  4362. console.error("[API Routes] Amazon SP-API\u5B9A\u65F6\u4EFB\u52A1\u542F\u52A8\u5931\u8D25:", error);
  4363. }
  4364. if (process.env.ENABLE_SORFTIME_SCHEDULES === "true") {
  4365. try {
  4366. await sorftimeScheduler.start();
  4367. console.log("[API Routes] Sorftime\u5B9A\u65F6\u4EFB\u52A1\u5DF2\u542F\u52A8");
  4368. } catch (error) {
  4369. console.error("[API Routes] Sorftime\u5B9A\u65F6\u4EFB\u52A1\u542F\u52A8\u5931\u8D25:", error);
  4370. }
  4371. } else {
  4372. console.log("[API Routes] Sorftime\u5B9A\u65F6\u4EFB\u52A1\u5DF2\u7981\u7528");
  4373. }
  4374. })();
  4375. var router3 = express3.Router();
  4376. console.log("\u52A0\u8F7Dschedule\u8DEF\u7531 /api/schedule/");
  4377. router3.use(express3.json({
  4378. charset: "utf-8",
  4379. // 额外配置:确保解析URL编码的请求体也支持中文
  4380. type: "application/json"
  4381. }));
  4382. router3.get("/sp-api-schedule/status", (req, res) => {
  4383. res.json({
  4384. success: true,
  4385. data: {
  4386. spApi: spApiScheduler.getStatus()
  4387. }
  4388. });
  4389. });
  4390. router3.get("/sp-api-schedule/start", async (req, res) => {
  4391. console.log("[API Routes] \u542F\u52A8\u5B9A\u65F6\u4EFB\u52A1");
  4392. try {
  4393. const result = await spApiScheduler.start();
  4394. res.json({
  4395. success: true,
  4396. message: "\u5B9A\u65F6\u5668\u6267\u884C\u9648\u529F",
  4397. data: result
  4398. });
  4399. } catch (error) {
  4400. res.status(500).json({
  4401. success: false,
  4402. message: error.message
  4403. });
  4404. }
  4405. });
  4406. router3.post("/sp-api-schedule/trigger", async (req, res) => {
  4407. try {
  4408. const shopId = req.body.shopId;
  4409. const result = await spApiScheduler.triggerManually(shopId);
  4410. res.json({
  4411. success: result.success,
  4412. message: result.message,
  4413. data: result
  4414. });
  4415. } catch (error) {
  4416. res.status(500).json({
  4417. success: false,
  4418. message: error.message
  4419. });
  4420. }
  4421. });
  4422. function createSseResponse(res) {
  4423. res.setHeader("Content-Type", "text/event-stream; charset=utf-8");
  4424. res.setHeader("Cache-Control", "no-cache");
  4425. res.setHeader("Connection", "keep-alive");
  4426. res.setHeader("X-Accel-Buffering", "no");
  4427. res.setHeader("Access-Control-Allow-Origin", "*");
  4428. res.setHeader("Access-Control-Allow-Methods", "GET, POST, OPTIONS");
  4429. res.setHeader("Access-Control-Allow-Headers", "Content-Type");
  4430. res.flushHeaders();
  4431. const send = (event, data) => {
  4432. if (!res.writableEnded) {
  4433. res.write(`event: ${event}
  4434. data: ${JSON.stringify(data)}
  4435. `);
  4436. }
  4437. };
  4438. const heartbeatTimer = setInterval(() => {
  4439. if (!res.writableEnded)
  4440. res.write(": heartbeat\n\n");
  4441. }, 15e3);
  4442. const close = () => {
  4443. clearInterval(heartbeatTimer);
  4444. if (!res.writableEnded)
  4445. res.end();
  4446. };
  4447. return { send, close };
  4448. }
  4449. router3.post("/sp-api-schedule/collect-listing", async (req, res) => {
  4450. const { shopId } = req.body || {};
  4451. const { send, close } = createSseResponse(res);
  4452. req.on("aborted", () => close());
  4453. try {
  4454. 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"}` });
  4455. const result = await newSpApiScheduler.collectListings(
  4456. shopId || void 0,
  4457. (event) => send("progress", event)
  4458. );
  4459. send("done", { success: result.success, message: result.message, processed: result.processed, errorCount: result.errorCount, errors: result.errors });
  4460. } catch (error) {
  4461. send("error", { success: false, message: error.message || "\u91C7\u96C6 Listing \u6570\u636E\u65F6\u53D1\u751F\u672A\u77E5\u9519\u8BEF" });
  4462. } finally {
  4463. close();
  4464. }
  4465. });
  4466. router3.post("/sp-api-schedule/collect-order", async (req, res) => {
  4467. const { shopId } = req.body || {};
  4468. const { send, close } = createSseResponse(res);
  4469. req.on("aborted", () => close());
  4470. try {
  4471. 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"}` });
  4472. const result = await newSpApiScheduler.collectOrders(
  4473. shopId || void 0,
  4474. (event) => send("progress", event)
  4475. );
  4476. send("done", { success: result.success, message: result.message, processed: result.processed, errorCount: result.errorCount, errors: result.errors });
  4477. } catch (error) {
  4478. send("error", { success: false, message: error.message || "\u91C7\u96C6\u8BA2\u5355\u6570\u636E\u65F6\u53D1\u751F\u672A\u77E5\u9519\u8BEF" });
  4479. } finally {
  4480. close();
  4481. }
  4482. });
  4483. router3.post("/sp-api-schedule/collect-report", async (req, res) => {
  4484. const { shopId } = req.body || {};
  4485. const { send, close } = createSseResponse(res);
  4486. req.on("aborted", () => close());
  4487. try {
  4488. 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"}` });
  4489. const result = await newSpApiScheduler.collectReports(
  4490. shopId || void 0,
  4491. (event) => send("progress", event)
  4492. );
  4493. send("done", { success: result.success, message: result.message, processed: result.processed, errorCount: result.errorCount, errors: result.errors });
  4494. } catch (error) {
  4495. send("error", { success: false, message: error.message || "\u91C7\u96C6\u62A5\u8868\u6570\u636E\u65F6\u53D1\u751F\u672A\u77E5\u9519\u8BEF" });
  4496. } finally {
  4497. close();
  4498. }
  4499. });
  4500. router3.post("/sp-api-schedule/parse-report", async (req, res) => {
  4501. const { shopId } = req.body || {};
  4502. const { send, close } = createSseResponse(res);
  4503. req.on("aborted", () => close());
  4504. try {
  4505. send("progress", { type: "info", message: `\u5F00\u59CB\u89E3\u6790\u62A5\u8868${shopId ? "\uFF0C\u5E97\u94FA: " + shopId : "\uFF08\u5168\u90E8\u6D3B\u8DC3\u5E97\u94FA\uFF09"}` });
  4506. const result = await newSpApiScheduler.parseReports(
  4507. shopId || void 0,
  4508. (event) => send("progress", event)
  4509. );
  4510. send("done", { success: result.success, message: result.message, processed: result.processed, errorCount: result.errorCount, errors: result.errors });
  4511. } catch (error) {
  4512. send("error", { success: false, message: error.message || "\u89E3\u6790\u62A5\u8868\u65F6\u53D1\u751F\u672A\u77E5\u9519\u8BEF" });
  4513. } finally {
  4514. close();
  4515. }
  4516. });
  4517. router3.get("/sorftime-schedule/status", (req, res) => {
  4518. res.json({
  4519. success: true,
  4520. data: {
  4521. sorftime: sorftimeScheduler.getStatus()
  4522. }
  4523. });
  4524. });
  4525. router3.post("/sorftime-schedule/trigger-products", async (req, res) => {
  4526. const { shopId, nodeIds } = req.body || {};
  4527. const { send, close } = createSseResponse(res);
  4528. req.on("aborted", () => close());
  4529. try {
  4530. send("progress", { type: "info", message: "\u5F00\u59CB\u91C7\u96C6\u7C7B\u76EE\u70ED\u9500\u4EA7\u54C1..." });
  4531. const result = await sorftimeScheduler.triggerCategoryProducts(
  4532. shopId || void 0,
  4533. nodeIds || void 0,
  4534. (event) => send("progress", event)
  4535. );
  4536. send("done", { success: result.success, message: result.message, successCount: result.successCount, errorCount: result.errorCount, errors: result.errors });
  4537. } catch (error) {
  4538. send("error", { success: false, message: error.message || "\u91C7\u96C6\u7C7B\u76EE\u70ED\u9500\u4EA7\u54C1\u65F6\u53D1\u751F\u672A\u77E5\u9519\u8BEF" });
  4539. } finally {
  4540. close();
  4541. }
  4542. });
  4543. router3.post("/sorftime-schedule/trigger-keywords", async (req, res) => {
  4544. const { shopId, nodeIds } = req.body || {};
  4545. const { send, close } = createSseResponse(res);
  4546. req.on("aborted", () => close());
  4547. try {
  4548. send("progress", { type: "info", message: "\u5F00\u59CB\u91C7\u96C6\u7C7B\u76EE\u5173\u952E\u8BCD..." });
  4549. const result = await sorftimeScheduler.triggerCategoryKeywords(
  4550. shopId || void 0,
  4551. nodeIds || void 0,
  4552. (event) => send("progress", event)
  4553. );
  4554. send("done", { success: result.success, message: result.message, successCount: result.successCount, errorCount: result.errorCount, errors: result.errors });
  4555. } catch (error) {
  4556. send("error", { success: false, message: error.message || "\u91C7\u96C6\u7C7B\u76EE\u5173\u952E\u8BCD\u65F6\u53D1\u751F\u672A\u77E5\u9519\u8BEF" });
  4557. } finally {
  4558. close();
  4559. }
  4560. });
  4561. router3.post("/sorftime-schedule/trigger-category-trends", async (req, res) => {
  4562. const { shopId, nodeIds } = req.body || {};
  4563. const { send, close } = createSseResponse(res);
  4564. req.on("aborted", () => close());
  4565. try {
  4566. send("progress", { type: "info", message: "\u5F00\u59CB\u91C7\u96C6\u7C7B\u76EE\u5E02\u573A\u8D8B\u52BF..." });
  4567. const result = await sorftimeScheduler.triggerCategoryTrends(
  4568. shopId || void 0,
  4569. nodeIds || void 0,
  4570. (event) => send("progress", event)
  4571. );
  4572. send("done", { success: result.success, message: result.message, successCount: result.successCount, errorCount: result.errorCount, errors: result.errors });
  4573. } catch (error) {
  4574. send("error", { success: false, message: error.message || "\u91C7\u96C6\u7C7B\u76EE\u5E02\u573A\u8D8B\u52BF\u65F6\u53D1\u751F\u672A\u77E5\u9519\u8BEF" });
  4575. } finally {
  4576. close();
  4577. }
  4578. });
  4579. router3.post("/sorftime-schedule/trigger-single-category", async (req, res) => {
  4580. const { nodeId, domain } = req.body || {};
  4581. if (!nodeId) {
  4582. return res.status(400).json({ success: false, message: "\u7F3A\u5C11 nodeId \u53C2\u6570" });
  4583. }
  4584. const { send, close } = createSseResponse(res);
  4585. req.on("aborted", () => close());
  4586. try {
  4587. 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` });
  4588. const result = await sorftimeScheduler.triggerSingleCategory(
  4589. nodeId,
  4590. domain || 1,
  4591. (event) => send("progress", event)
  4592. );
  4593. send("done", { success: result.success, message: result.message, tasks: result.tasks, duration: result.duration, errors: result.errors });
  4594. } catch (error) {
  4595. send("error", { success: false, message: error.message || "\u91C7\u96C6\u5355\u7C7B\u76EE\u6570\u636E\u65F6\u53D1\u751F\u672A\u77E5\u9519\u8BEF" });
  4596. } finally {
  4597. close();
  4598. }
  4599. });
  4600. router3.post("/sorftime-schedule/collect-reviews", async (req, res) => {
  4601. const { send, close } = createSseResponse(res);
  4602. req.on("aborted", () => close());
  4603. try {
  4604. send("progress", { type: "info", message: "\u5F00\u59CB\u91C7\u96C6\u4EA7\u54C1\u8BC4\u8BBA\u6570\u636E..." });
  4605. const result = await sorftimeScheduler.triggerProductReviews(
  4606. (event) => send("progress", event)
  4607. );
  4608. send("done", { success: result.success, message: result.message, successCount: result.successCount, errorCount: result.errorCount, errors: result.errors });
  4609. } catch (error) {
  4610. send("error", { success: false, message: error.message || "\u91C7\u96C6\u4EA7\u54C1\u8BC4\u8BBA\u65F6\u53D1\u751F\u672A\u77E5\u9519\u8BEF" });
  4611. } finally {
  4612. close();
  4613. }
  4614. });
  4615. router3.post("/sorftime-schedule/collect-shop-products", async (req, res) => {
  4616. const { shopId } = req.body || {};
  4617. const { send, close } = createSseResponse(res);
  4618. req.on("aborted", () => close());
  4619. try {
  4620. 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"}` });
  4621. const result = await sorftimeScheduler.collectShopProducts(
  4622. shopId || void 0,
  4623. (event) => send("progress", event)
  4624. );
  4625. send("done", { success: result.success, message: result.message, processed: result.processed, errorCount: result.errorCount, errors: result.errors });
  4626. } catch (error) {
  4627. send("error", { success: false, message: error.message || "\u91C7\u96C6\u5E97\u94FA\u4EA7\u54C1\u65F6\u53D1\u751F\u672A\u77E5\u9519\u8BEF" });
  4628. } finally {
  4629. close();
  4630. }
  4631. });
  4632. router3.get("/test", (req, res) => {
  4633. res.json({
  4634. message: "\u6D4B\u8BD5 sp-api-schedule",
  4635. body: req.body
  4636. });
  4637. });
  4638. var routes_schedule_default = router3;
  4639. // backend/api/routes.ts
  4640. var router4 = express4.Router();
  4641. console.log("[API Routes] Initializing Jianhen customer instance...");
  4642. router4.use((req, res, next) => {
  4643. console.log(`[API Router] Hit: ${req.method} ${req.url} (Original: ${req.originalUrl})`);
  4644. next();
  4645. });
  4646. router4.get("/ping", (req, res) => {
  4647. res.json({ message: "pong", instance: "jianhen-voc" });
  4648. });
  4649. function isInternalRequest(req) {
  4650. const address = String(req.socket?.remoteAddress || "");
  4651. return !req.headers["x-external-request"] && (address === "127.0.0.1" || address === "::1" || address.endsWith(":127.0.0.1"));
  4652. }
  4653. async function requireApiUser(req, res, next) {
  4654. if (isInternalRequest(req))
  4655. return next();
  4656. const token = String(req.headers["x-parse-session-token"] || "").trim();
  4657. if (!token)
  4658. return res.status(401).json({ success: false, message: "\u9700\u8981\u767B\u5F55" });
  4659. try {
  4660. const Parse = globalThis.Parse;
  4661. const query = new Parse.Query("_Session");
  4662. query.equalTo("sessionToken", token);
  4663. query.include("user");
  4664. const session = await query.first({ useMasterKey: true });
  4665. const user = session?.get("user");
  4666. if (!user || user.get("status") === "disabled") {
  4667. return res.status(401).json({ success: false, message: "\u4F1A\u8BDD\u65E0\u6548" });
  4668. }
  4669. req.currentUser = user;
  4670. next();
  4671. } catch {
  4672. res.status(401).json({ success: false, message: "\u4F1A\u8BDD\u65E0\u6548" });
  4673. }
  4674. }
  4675. function requireAdmin(req, res, next) {
  4676. if (isInternalRequest(req) || req.currentUser?.get?.("isAdmin") === true)
  4677. return next();
  4678. res.status(403).json({ success: false, message: "\u9700\u8981\u7BA1\u7406\u5458\u6743\u9650" });
  4679. }
  4680. function requireUserExceptHealth(req, res, next) {
  4681. if (req.path === "/health")
  4682. return next();
  4683. return requireApiUser(req, res, next);
  4684. }
  4685. function requireAdminExceptToken(req, res, next) {
  4686. if (req.path === "/token")
  4687. return next();
  4688. return requireAdmin(req, res, next);
  4689. }
  4690. router4.use("/amazon", requireApiUser, createSpApiRouter());
  4691. router4.use("/relay", requireApiUser, requireAdminExceptToken, relayCredentialRouter);
  4692. console.log("[API Routes] \u2705 Company relay credential routes mounted");
  4693. try {
  4694. router4.use("/schedule", requireApiUser, requireAdmin, routes_schedule_default);
  4695. } catch (e) {
  4696. console.warn("[API Routes] scheduleRouter \u52A0\u8F7D\u5931\u8D25", e);
  4697. }
  4698. router4.get("/", (req, res) => {
  4699. res.json({
  4700. message: "Jianhen VOC Instance API Routes Loaded",
  4701. version: "1.0.1-customer-relay",
  4702. timestamp: (/* @__PURE__ */ new Date()).toISOString(),
  4703. availableRoutes: [
  4704. "/api/amazon",
  4705. "/api/relay/token",
  4706. "/api/relay/status",
  4707. "/api/relay/credential",
  4708. "/api/schedule"
  4709. ],
  4710. removedModules: [
  4711. "fmode-sorftime-api (direct upstream)",
  4712. "fmode-tikhub-api (direct upstream)",
  4713. "fmode-tikhub-api-customize",
  4714. "fmode-eccang-api"
  4715. ],
  4716. relay: {
  4717. companyBase: "https://server.fmode.cn",
  4718. sorftimeUpstream: "/api/voc-ecom/forward",
  4719. tikhubUpstream: "/api/voc-social/*"
  4720. }
  4721. });
  4722. });
  4723. var routes_default = router4;
  4724. export {
  4725. routes_default as default
  4726. };