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