| 1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889909192939495969798991001011021031041051061071081091101111121131141151161171181191201211221231241251261271281291301311321331341351361371381391401411421431441451461471481491501511521531541551561571581591601611621631641651661671681691701711721731741751761771781791801811821831841851861871881891901911921931941951961971981992002012022032042052062072082092102112122132142152162172182192202212222232242252262272282292302312322332342352362372382392402412422432442452462472482492502512522532542552562572582592602612622632642652662672682692702712722732742752762772782792802812822832842852862872882892902912922932942952962972982993003013023033043053063073083093103113123133143153163173183193203213223233243253263273283293303313323333343353363373383393403413423433443453463473483493503513523533543553563573583593603613623633643653663673683693703713723733743753763773783793803813823833843853863873883893903913923933943953963973983994004014024034044054064074084094104114124134144154164174184194204214224234244254264274284294304314324334344354364374384394404414424434444454464474484494504514524534544554564574584594604614624634644654664674684694704714724734744754764774784794804814824834844854864874884894904914924934944954964974984995005015025035045055065075085095105115125135145155165175185195205215225235245255265275285295305315325335345355365375385395405415425435445455465475485495505515525535545555565575585595605615625635645655665675685695705715725735745755765775785795805815825835845855865875885895905915925935945955965975985996006016026036046056066076086096106116126136146156166176186196206216226236246256266276286296306316326336346356366376386396406416426436446456466476486496506516526536546556566576586596606616626636646656666676686696706716726736746756766776786796806816826836846856866876886896906916926936946956966976986997007017027037047057067077087097107117127137147157167177187197207217227237247257267277287297307317327337347357367377387397407417427437447457467477487497507517527537547557567577587597607617627637647657667677687697707717727737747757767777787797807817827837847857867877887897907917927937947957967977987998008018028038048058068078088098108118128138148158168178188198208218228238248258268278288298308318328338348358368378388398408418428438448458468478488498508518528538548558568578588598608618628638648658668678688698708718728738748758768778788798808818828838848858868878888898908918928938948958968978988999009019029039049059069079089099109119129139149159169179189199209219229239249259269279289299309319329339349359369379389399409419429439449459469479489499509519529539549559569579589599609619629639649659669679689699709719729739749759769779789799809819829839849859869879889899909919929939949959969979989991000100110021003100410051006100710081009101010111012101310141015101610171018101910201021102210231024102510261027102810291030103110321033103410351036103710381039104010411042104310441045104610471048104910501051105210531054105510561057105810591060106110621063106410651066106710681069107010711072107310741075107610771078107910801081108210831084108510861087108810891090109110921093109410951096109710981099110011011102110311041105110611071108110911101111111211131114111511161117111811191120112111221123112411251126112711281129113011311132113311341135113611371138113911401141114211431144114511461147114811491150115111521153115411551156115711581159116011611162116311641165116611671168116911701171117211731174117511761177117811791180118111821183118411851186118711881189119011911192119311941195119611971198119912001201120212031204120512061207120812091210121112121213121412151216121712181219122012211222122312241225122612271228122912301231123212331234123512361237123812391240124112421243124412451246124712481249125012511252125312541255125612571258125912601261126212631264126512661267126812691270127112721273127412751276127712781279128012811282128312841285128612871288128912901291129212931294129512961297129812991300130113021303130413051306130713081309131013111312131313141315131613171318131913201321132213231324132513261327132813291330133113321333133413351336133713381339134013411342134313441345134613471348134913501351135213531354135513561357135813591360136113621363136413651366136713681369137013711372137313741375137613771378137913801381138213831384138513861387138813891390139113921393139413951396139713981399140014011402140314041405140614071408140914101411141214131414141514161417141814191420142114221423142414251426142714281429143014311432143314341435143614371438143914401441144214431444144514461447144814491450145114521453145414551456145714581459146014611462146314641465146614671468146914701471147214731474147514761477147814791480148114821483148414851486148714881489149014911492149314941495149614971498149915001501150215031504150515061507150815091510151115121513151415151516151715181519152015211522152315241525152615271528152915301531153215331534153515361537153815391540154115421543154415451546154715481549155015511552155315541555155615571558155915601561156215631564156515661567156815691570157115721573157415751576157715781579158015811582158315841585158615871588158915901591159215931594159515961597159815991600160116021603160416051606160716081609161016111612161316141615161616171618161916201621162216231624162516261627162816291630163116321633163416351636163716381639164016411642164316441645164616471648164916501651165216531654165516561657165816591660166116621663166416651666166716681669167016711672167316741675167616771678167916801681168216831684168516861687168816891690169116921693169416951696169716981699170017011702170317041705170617071708170917101711171217131714171517161717171817191720172117221723172417251726172717281729173017311732173317341735173617371738173917401741174217431744174517461747174817491750175117521753175417551756175717581759176017611762176317641765176617671768176917701771177217731774177517761777177817791780178117821783178417851786178717881789179017911792179317941795179617971798179918001801180218031804180518061807180818091810181118121813181418151816181718181819182018211822182318241825182618271828182918301831183218331834183518361837183818391840184118421843184418451846184718481849185018511852185318541855185618571858185918601861186218631864186518661867186818691870187118721873187418751876187718781879188018811882188318841885188618871888188918901891189218931894189518961897189818991900190119021903190419051906190719081909191019111912191319141915191619171918191919201921192219231924192519261927192819291930193119321933193419351936193719381939194019411942194319441945194619471948194919501951195219531954195519561957195819591960196119621963196419651966196719681969197019711972197319741975197619771978197919801981198219831984198519861987198819891990199119921993199419951996199719981999200020012002200320042005200620072008200920102011201220132014201520162017201820192020202120222023202420252026202720282029203020312032203320342035203620372038203920402041204220432044204520462047204820492050205120522053205420552056205720582059206020612062206320642065206620672068206920702071207220732074207520762077207820792080208120822083208420852086208720882089209020912092209320942095209620972098209921002101210221032104210521062107210821092110211121122113211421152116211721182119212021212122212321242125212621272128212921302131213221332134213521362137213821392140214121422143214421452146214721482149215021512152215321542155215621572158215921602161216221632164216521662167216821692170217121722173217421752176217721782179218021812182218321842185218621872188218921902191219221932194219521962197219821992200220122022203220422052206220722082209221022112212221322142215221622172218221922202221222222232224222522262227222822292230223122322233223422352236223722382239224022412242224322442245224622472248224922502251225222532254225522562257225822592260226122622263226422652266226722682269227022712272227322742275227622772278227922802281228222832284228522862287228822892290229122922293229422952296229722982299230023012302230323042305230623072308230923102311231223132314231523162317231823192320232123222323232423252326232723282329233023312332233323342335233623372338233923402341234223432344234523462347234823492350235123522353235423552356235723582359236023612362236323642365236623672368236923702371237223732374237523762377237823792380238123822383238423852386238723882389239023912392239323942395239623972398239924002401240224032404240524062407240824092410241124122413241424152416241724182419242024212422242324242425242624272428242924302431243224332434243524362437243824392440244124422443244424452446244724482449245024512452245324542455245624572458245924602461246224632464246524662467246824692470247124722473247424752476247724782479248024812482248324842485248624872488248924902491249224932494249524962497249824992500250125022503250425052506250725082509251025112512251325142515251625172518251925202521252225232524252525262527252825292530253125322533253425352536253725382539254025412542254325442545254625472548254925502551255225532554255525562557255825592560256125622563256425652566256725682569257025712572257325742575257625772578257925802581258225832584258525862587258825892590259125922593259425952596259725982599260026012602260326042605260626072608260926102611261226132614261526162617261826192620262126222623262426252626262726282629263026312632263326342635263626372638263926402641264226432644264526462647264826492650265126522653265426552656265726582659266026612662266326642665266626672668266926702671267226732674267526762677267826792680268126822683268426852686268726882689269026912692269326942695269626972698269927002701270227032704270527062707270827092710271127122713271427152716271727182719272027212722272327242725272627272728272927302731273227332734273527362737273827392740274127422743274427452746274727482749275027512752275327542755275627572758275927602761276227632764276527662767276827692770277127722773277427752776277727782779278027812782278327842785278627872788278927902791279227932794279527962797279827992800280128022803280428052806280728082809281028112812281328142815281628172818281928202821282228232824282528262827282828292830283128322833283428352836283728382839284028412842284328442845284628472848284928502851285228532854285528562857285828592860286128622863286428652866286728682869287028712872287328742875287628772878287928802881288228832884288528862887288828892890289128922893289428952896289728982899290029012902290329042905290629072908290929102911291229132914291529162917291829192920292129222923292429252926292729282929293029312932293329342935293629372938293929402941294229432944294529462947294829492950295129522953295429552956295729582959296029612962296329642965296629672968296929702971297229732974297529762977297829792980298129822983298429852986298729882989299029912992299329942995299629972998299930003001300230033004300530063007300830093010301130123013301430153016301730183019302030213022302330243025302630273028302930303031303230333034303530363037303830393040304130423043304430453046304730483049305030513052305330543055305630573058305930603061306230633064306530663067306830693070307130723073307430753076307730783079308030813082308330843085308630873088308930903091309230933094309530963097309830993100310131023103310431053106310731083109311031113112311331143115311631173118311931203121312231233124312531263127312831293130313131323133313431353136313731383139314031413142314331443145314631473148314931503151315231533154315531563157315831593160316131623163316431653166316731683169317031713172317331743175317631773178317931803181318231833184318531863187318831893190319131923193319431953196319731983199320032013202320332043205320632073208320932103211321232133214321532163217321832193220322132223223322432253226322732283229323032313232323332343235323632373238323932403241324232433244324532463247324832493250325132523253325432553256325732583259326032613262326332643265326632673268326932703271327232733274327532763277327832793280328132823283328432853286328732883289329032913292329332943295329632973298329933003301330233033304330533063307330833093310331133123313331433153316331733183319332033213322332333243325332633273328332933303331333233333334333533363337333833393340334133423343334433453346334733483349335033513352335333543355335633573358335933603361336233633364336533663367336833693370337133723373337433753376337733783379338033813382338333843385338633873388338933903391339233933394339533963397339833993400340134023403340434053406340734083409341034113412341334143415341634173418341934203421342234233424342534263427342834293430343134323433343434353436343734383439344034413442344334443445344634473448344934503451345234533454345534563457345834593460346134623463346434653466346734683469347034713472347334743475347634773478347934803481348234833484348534863487348834893490349134923493349434953496349734983499350035013502350335043505350635073508350935103511351235133514351535163517351835193520352135223523352435253526352735283529353035313532353335343535353635373538353935403541354235433544354535463547354835493550355135523553355435553556355735583559356035613562356335643565356635673568356935703571357235733574357535763577357835793580358135823583358435853586358735883589359035913592359335943595359635973598359936003601360236033604360536063607360836093610361136123613361436153616361736183619362036213622362336243625362636273628362936303631363236333634363536363637363836393640364136423643364436453646364736483649365036513652365336543655365636573658365936603661366236633664366536663667366836693670367136723673367436753676367736783679368036813682368336843685368636873688368936903691369236933694369536963697369836993700370137023703370437053706370737083709371037113712371337143715371637173718371937203721372237233724372537263727372837293730373137323733373437353736373737383739374037413742374337443745374637473748374937503751375237533754375537563757375837593760376137623763376437653766376737683769377037713772377337743775377637773778377937803781378237833784378537863787378837893790379137923793379437953796379737983799380038013802380338043805380638073808380938103811381238133814381538163817381838193820382138223823382438253826382738283829383038313832383338343835383638373838383938403841384238433844384538463847384838493850385138523853385438553856385738583859386038613862386338643865386638673868386938703871387238733874387538763877387838793880388138823883388438853886388738883889389038913892389338943895389638973898389939003901390239033904390539063907390839093910391139123913391439153916391739183919392039213922392339243925392639273928392939303931393239333934393539363937393839393940394139423943394439453946394739483949395039513952395339543955395639573958395939603961396239633964396539663967396839693970397139723973397439753976397739783979398039813982398339843985398639873988398939903991399239933994399539963997399839994000400140024003400440054006400740084009401040114012401340144015401640174018401940204021402240234024402540264027402840294030403140324033403440354036403740384039404040414042404340444045404640474048404940504051405240534054405540564057405840594060406140624063406440654066406740684069407040714072407340744075407640774078407940804081408240834084408540864087408840894090409140924093409440954096409740984099410041014102410341044105410641074108410941104111411241134114411541164117411841194120412141224123412441254126412741284129413041314132413341344135413641374138413941404141414241434144414541464147414841494150415141524153415441554156415741584159416041614162416341644165416641674168416941704171417241734174417541764177417841794180418141824183418441854186418741884189419041914192419341944195419641974198419942004201420242034204420542064207420842094210421142124213421442154216421742184219422042214222422342244225422642274228422942304231423242334234423542364237423842394240424142424243424442454246424742484249425042514252425342544255425642574258425942604261426242634264426542664267426842694270427142724273427442754276427742784279428042814282428342844285428642874288428942904291429242934294429542964297429842994300430143024303430443054306430743084309431043114312431343144315431643174318431943204321432243234324432543264327432843294330433143324333433443354336433743384339434043414342434343444345434643474348434943504351435243534354435543564357435843594360436143624363436443654366436743684369437043714372437343744375437643774378437943804381438243834384438543864387438843894390439143924393439443954396439743984399440044014402440344044405440644074408440944104411441244134414441544164417441844194420442144224423442444254426442744284429443044314432443344344435443644374438443944404441444244434444444544464447444844494450445144524453445444554456445744584459446044614462446344644465446644674468446944704471447244734474447544764477447844794480448144824483448444854486448744884489449044914492449344944495449644974498449945004501450245034504450545064507450845094510451145124513451445154516451745184519452045214522452345244525452645274528452945304531453245334534453545364537453845394540454145424543454445454546454745484549455045514552455345544555455645574558455945604561456245634564456545664567456845694570457145724573457445754576457745784579458045814582458345844585458645874588458945904591459245934594459545964597459845994600460146024603460446054606460746084609461046114612461346144615461646174618461946204621462246234624462546264627462846294630463146324633463446354636463746384639464046414642464346444645464646474648464946504651465246534654465546564657465846594660466146624663466446654666466746684669467046714672467346744675467646774678467946804681468246834684468546864687468846894690469146924693469446954696469746984699470047014702470347044705470647074708470947104711471247134714471547164717471847194720472147224723472447254726472747284729473047314732473347344735473647374738473947404741474247434744474547464747474847494750475147524753475447554756475747584759476047614762476347644765476647674768476947704771477247734774477547764777477847794780478147824783478447854786478747884789479047914792479347944795479647974798479948004801480248034804480548064807480848094810481148124813481448154816481748184819482048214822482348244825482648274828482948304831483248334834483548364837483848394840484148424843484448454846484748484849485048514852485348544855485648574858485948604861486248634864486548664867486848694870487148724873487448754876487748784879488048814882488348844885488648874888488948904891489248934894489548964897489848994900490149024903490449054906490749084909491049114912491349144915491649174918491949204921492249234924492549264927492849294930493149324933493449354936493749384939494049414942494349444945494649474948494949504951495249534954495549564957495849594960496149624963496449654966496749684969497049714972497349744975497649774978497949804981498249834984498549864987498849894990499149924993499449954996499749984999500050015002500350045005500650075008500950105011501250135014501550165017501850195020502150225023502450255026502750285029503050315032503350345035503650375038503950405041504250435044504550465047504850495050505150525053505450555056505750585059506050615062506350645065506650675068506950705071507250735074507550765077507850795080508150825083508450855086508750885089509050915092509350945095509650975098509951005101510251035104510551065107510851095110511151125113511451155116511751185119512051215122512351245125512651275128512951305131513251335134513551365137513851395140514151425143514451455146514751485149515051515152515351545155515651575158515951605161516251635164516551665167516851695170517151725173517451755176517751785179518051815182518351845185518651875188518951905191519251935194519551965197519851995200520152025203520452055206520752085209521052115212521352145215521652175218521952205221522252235224522552265227522852295230523152325233523452355236523752385239524052415242524352445245524652475248524952505251525252535254525552565257525852595260526152625263526452655266526752685269527052715272527352745275527652775278527952805281528252835284528552865287528852895290529152925293529452955296529752985299530053015302530353045305530653075308530953105311531253135314531553165317531853195320532153225323532453255326532753285329533053315332533353345335533653375338533953405341534253435344534553465347534853495350535153525353535453555356535753585359536053615362536353645365536653675368536953705371537253735374537553765377537853795380538153825383538453855386538753885389539053915392539353945395539653975398539954005401540254035404540554065407540854095410541154125413541454155416541754185419542054215422542354245425542654275428542954305431543254335434543554365437543854395440544154425443544454455446544754485449545054515452545354545455545654575458545954605461546254635464546554665467546854695470547154725473547454755476547754785479548054815482548354845485548654875488548954905491549254935494549554965497549854995500550155025503550455055506550755085509551055115512551355145515551655175518551955205521552255235524552555265527552855295530553155325533553455355536553755385539554055415542554355445545554655475548554955505551555255535554555555565557555855595560556155625563556455655566556755685569557055715572557355745575557655775578557955805581558255835584558555865587558855895590559155925593559455955596559755985599560056015602560356045605560656075608560956105611561256135614561556165617561856195620562156225623562456255626562756285629563056315632563356345635563656375638563956405641564256435644564556465647564856495650565156525653565456555656565756585659566056615662566356645665566656675668566956705671567256735674567556765677567856795680568156825683568456855686568756885689569056915692569356945695569656975698569957005701570257035704570557065707570857095710571157125713571457155716571757185719572057215722572357245725572657275728572957305731573257335734573557365737573857395740574157425743574457455746574757485749575057515752575357545755575657575758575957605761576257635764576557665767576857695770577157725773577457755776577757785779578057815782578357845785578657875788578957905791579257935794579557965797579857995800580158025803580458055806580758085809581058115812581358145815581658175818581958205821582258235824582558265827582858295830583158325833583458355836583758385839584058415842584358445845584658475848584958505851585258535854585558565857585858595860586158625863586458655866586758685869587058715872587358745875587658775878587958805881588258835884588558865887588858895890589158925893589458955896589758985899590059015902590359045905590659075908590959105911591259135914591559165917591859195920592159225923592459255926592759285929593059315932593359345935593659375938593959405941594259435944594559465947594859495950595159525953595459555956595759585959596059615962596359645965596659675968596959705971597259735974597559765977597859795980598159825983598459855986598759885989599059915992599359945995599659975998599960006001600260036004600560066007600860096010601160126013601460156016601760186019602060216022602360246025602660276028602960306031603260336034 |
- use crate::steam_auth::{SteamGuardAccount, Confirmation};
- use crate::api_client::{ApiConfig, initialize_api_client, get_api_client_for_async, get_http_client};
- use crate::app_logger;
- use crate::config::AppConfig;
- use crate::machine::generate_machine_code;
- use crate::storage::{get_storage, UserInfo};
- use crate::heartbeat::{
- start_heartbeat_service,
- stop_heartbeat_service,
- get_heartbeat_service_status,
- is_heartbeat_running,
- execute_heartbeat,
- execute_key_sync,
- update_heartbeat_config,
- get_heartbeat_config,
- HeartbeatConfig,
- };
- use serde::{Deserialize, Serialize};
- use serde_json;
- use std::collections::HashMap;
- use std::fs;
- use std::path::PathBuf;
- use std::sync::{Mutex, OnceLock};
- use std::time::Duration;
- use chrono::{Datelike, Utc};
- // 导出 reqwest::StatusCode 供子模块使用
- pub use reqwest::StatusCode;
- // 加密相关导入
- use base64::{Engine as _, engine::general_purpose};
- use hmac::{Hmac, Mac};
- use sha2::Sha256;
- use aes::Aes256;
- use ctr::Ctr64BE;
- use aes::cipher::{KeyIvInit, StreamCipher};
- use rsa::{RsaPublicKey, pkcs8::DecodePublicKey, pkcs1v15::Pkcs1v15Encrypt, oaep::Oaep};
- use rand;
- use rand::rngs::OsRng;
- use std::collections::VecDeque;
- use std::time::{SystemTime, UNIX_EPOCH, Instant};
- // 新的安全通信模块导入
- pub mod crypto;
- use crate::commands::crypto::{
- aes_encrypt,
- aes_decrypt,
- get_stored_key_version,
- get_current_public_key,
- initialize_key_manager,
- refresh_key_if_needed,
- get_key_manager_state,
- validate_stored_key,
- has_valid_hmac_key,
- clear_hmac_key,
- store_hmac_key_from_api,
- sync_hmac_key as crypto_sync_hmac_key,
- generate_hmac_signature,
- rsa_encrypt_aes_key as crypto_rsa_encrypt_aes_key,
- store_rsa_key_segments,
- retrieve_rsa_key_segments,
- has_stored_rsa_key as crypto_has_stored_rsa_key,
- clear_stored_rsa_key as crypto_clear_stored_rsa_key,
- KeyManagerState,
- };
- use log::{info, error, warn, debug};
- mod buff_sweep_decode;
- mod buff_sweep_runner;
- // ========== 本地 nonce 去重缓存 ==========
- // 防止 Rust 端重试时短时间内使用同一个 nonce(已被后端防重放拒绝过),导致雪崩
- lazy_static::lazy_static! {
- static ref RECENT_NONCES: std::sync::Mutex<VecDeque<(String, Instant)>> = std::sync::Mutex::new(VecDeque::new());
- }
- const NONCE_CACHE_TTL_SECS: u64 = 10; // 10 秒内相同 nonce 不重发
- /// 生成一个唯一的随机 nonce(64位hex = 32字节),并检查本地缓存
- fn generate_unique_nonce() -> String {
- let nonce: String = (0..32u8).map(|_| rand::random::<u8>()).map(|b| format!("{:02x}", b)).collect();
- let mut cache = RECENT_NONCES.lock().unwrap();
- // 清理过期条目(超过 TTL 的)
- let now = Instant::now();
- cache.retain(|(_, instant)| now.duration_since(*instant).as_secs() < NONCE_CACHE_TTL_SECS);
- // 如果 nonce 已在缓存中(极罕见),重新生成一次
- if cache.iter().any(|(n, _)| n == &nonce) {
- let nonce2: String = (0..32u8).map(|_| rand::random::<u8>()).map(|b| format!("{:02x}", b)).collect();
- cache.push_back((nonce2.clone(), now));
- return nonce2;
- }
- cache.push_back((nonce.clone(), now));
- nonce
- }
- /// 记录一个 nonce(请求发出后调用,即使失败了也要记录,避免同一 nonce 在 TTL 内重发)
- fn record_nonce(nonce: &str) {
- if let Ok(mut cache) = RECENT_NONCES.lock() {
- let now = Instant::now();
- cache.retain(|(_, instant)| now.duration_since(*instant).as_secs() < NONCE_CACHE_TTL_SECS);
- cache.push_back((nonce.to_string(), now));
- }
- }
- #[tauri::command]
- pub async fn generate_steam_code(account_data: String) -> Result<String, String> {
- let account: SteamGuardAccount = serde_json::from_str(&account_data)
- .map_err(|e| format!("Failed to parse account data: {}", e))?;
- account.generate_code()
- .map_err(|e| format!("Failed to generate code: {}", e))
- }
- #[tauri::command]
- pub async fn fetch_confirmations(account_data: String) -> Result<String, String> {
- let account: SteamGuardAccount = serde_json::from_str(&account_data)
- .map_err(|e| format!("Failed to parse account data: {}", e))?;
- let confirmations = account.fetch_confirmations().await
- .map_err(|e| format!("Failed to fetch confirmations: {}", e))?;
- serde_json::to_string(&confirmations)
- .map_err(|e| format!("Failed to serialize confirmations: {}", e))
- }
- #[tauri::command]
- pub async fn accept_confirmation(account_data: String, confirmation_data: String) -> Result<bool, String> {
- let account: SteamGuardAccount = serde_json::from_str(&account_data)
- .map_err(|e| format!("Failed to parse account data: {}", e))?;
- let confirmation: Confirmation = serde_json::from_str(&confirmation_data)
- .map_err(|e| format!("Failed to parse confirmation data: {}", e))?;
- account.accept_confirmation(&confirmation).await
- .map_err(|e| format!("Failed to accept confirmation: {}", e))
- }
- #[tauri::command]
- pub async fn deny_confirmation(account_data: String, confirmation_data: String) -> Result<bool, String> {
- let account: SteamGuardAccount = serde_json::from_str(&account_data)
- .map_err(|e| format!("Failed to parse account data: {}", e))?;
- let confirmation: Confirmation = serde_json::from_str(&confirmation_data)
- .map_err(|e| format!("Failed to parse confirmation data: {}", e))?;
- account.deny_confirmation(&confirmation).await
- .map_err(|e| format!("Failed to deny confirmation: {}", e))
- }
- #[tauri::command]
- pub fn save_steam_account(account_data: String, account_name: String) -> Result<(), String> {
- let app_data_dir = dirs::data_dir()
- .ok_or("Failed to get app data directory")?
- .join("YourAppName")
- .join("steam_accounts");
- fs::create_dir_all(&app_data_dir)
- .map_err(|e| format!("Failed to create directory: {}", e))?;
- let file_path = app_data_dir.join(format!("{}.json", account_name));
- // 简单的加密(生产环境应该使用更强的加密)
- let encrypted_data = simple_encrypt(&account_data);
- fs::write(&file_path, encrypted_data)
- .map_err(|e| format!("Failed to save account: {}", e))
- }
- #[tauri::command]
- pub fn load_steam_accounts() -> Result<String, String> {
- let app_data_dir = dirs::data_dir()
- .ok_or("Failed to get app data directory")?
- .join("YourAppName")
- .join("steam_accounts");
- if !app_data_dir.exists() {
- return Ok("[]".to_string());
- }
- let mut accounts = Vec::new();
- for entry in fs::read_dir(&app_data_dir)
- .map_err(|e| format!("Failed to read directory: {}", e))?
- {
- let entry = entry.map_err(|e| format!("Failed to read entry: {}", e))?;
- let path = entry.path();
- if path.extension().and_then(|s| s.to_str()) == Some("json") {
- let encrypted_data = fs::read(&path)
- .map_err(|e| format!("Failed to read file: {}", e))?;
- let account_data = simple_decrypt(&encrypted_data);
- accounts.push(account_data);
- }
- }
- Ok(format!("[{}]", accounts.join(",")))
- }
- // 简单的加密/解密函数(生产环境应该使用更强的加密)
- fn simple_encrypt(data: &str) -> Vec<u8> {
- data.as_bytes().iter().map(|&b| b.wrapping_add(42)).collect()
- }
- fn simple_decrypt(data: &[u8]) -> String {
- let bytes: Vec<u8> = data.iter().map(|&b| b.wrapping_sub(42)).collect();
- String::from_utf8_lossy(&bytes).to_string()
- }
- // ==================== Project-local steam-msg storage commands ====================
- #[tauri::command]
- pub async fn save_steam_guard_file(base64Data: String, filename: String) -> Result<String, String> {
- use base64::{engine::general_purpose, Engine as _};
- // Determine project root (assume current_dir is inside the project, fall back to current_dir)
- let cwd = std::env::current_dir().map_err(|e| format!("Failed to get current dir: {}", e))?;
- let project_root = cwd; // use current dir; in typical dev setup this is project root
- let steam_msg_dir: PathBuf = project_root.join("steam-msg");
- fs::create_dir_all(&steam_msg_dir).map_err(|e| format!("Failed to create steam-msg dir: {}", e))?;
- let target_path = steam_msg_dir.join(&filename);
- let decoded = general_purpose::STANDARD
- .decode(&base64Data)
- .map_err(|e| format!("Failed to decode base64 data: {}", e))?;
- fs::write(&target_path, &decoded).map_err(|e| format!("Failed to write steam guard file: {}", e))?;
- Ok(target_path.to_string_lossy().to_string())
- }
- /// 统一 accounts 文件名(所有账号信息存于一个 JSON)
- const ACCOUNTS_JSON: &str = "accounts.json";
- /// 获取 steam-msg 目录。
- /// 路径为 `AppData\Local\steam-msg\{当前登录用户名}\`,实现多用户数据隔离。
- /// 若新目录下无数据而旧扁平目录下有数据,则一次性迁移过来。
- fn get_steam_msg_dir() -> Result<PathBuf, String> {
- // 获取 AppData\Local\steam-msg 基础路径
- let base = dirs::data_local_dir()
- .or_else(|| std::env::current_dir().ok())
- .ok_or_else(|| "无法获取可写目录".to_string())?;
- let base_steam_msg = base.join("steam-msg");
- // 获取当前登录用户名,拼入子目录
- let username = get_current_username_internal()
- .unwrap_or_else(|_| "guest".to_string());
- let user_dir = base_steam_msg.join(&username);
- // 迁移逻辑:若新目录下尚无任何 .maFile(说明从未迁移过),
- // 而旧扁平目录下存在 accounts.json,则一次性迁移所有数据过来
- let has_existing_files = user_dir.join(ACCOUNTS_JSON).exists()
- || std::fs::read_dir(&user_dir)
- .and_then(|mut d| d.next().transpose())
- .is_ok();
- if !has_existing_files && base_steam_msg.join(ACCOUNTS_JSON).exists() {
- if let Err(e) = fs::create_dir_all(&user_dir) {
- log::warn!("[get_steam_msg_dir] 创建用户目录失败: {}", e);
- } else {
- // 复制 accounts.json 和所有 .maFile
- if let Ok(entries) = fs::read_dir(&base_steam_msg) {
- for entry in entries.flatten() {
- let name = entry.file_name();
- let name_str = name.to_string_lossy();
- if name_str.ends_with(".maFile") || name_str == ACCOUNTS_JSON {
- let dest = user_dir.join(&name);
- if !dest.exists() {
- if let Err(e) = fs::copy(entry.path(), &dest) {
- log::warn!("[get_steam_msg_dir] 迁移文件失败 {:?}: {}", entry.path(), e);
- }
- }
- }
- }
- }
- log::info!("[get_steam_msg_dir] 已从旧目录迁移数据到 {}", user_dir.display());
- }
- }
- Ok(user_dir)
- }
- /// 将用户名中不能作为文件名的字符替换为下划线,避免 Windows 等平台路径错误或崩溃
- fn sanitize_username_for_filename(username: &str) -> String {
- let mut s = username.to_string();
- for c in ['\\', '/', ':', '*', '?', '"', '<', '>', '|'] {
- s = s.replace(c, "_");
- }
- if s.is_empty() {
- s = "steam_account".to_string();
- }
- s
- }
- #[tauri::command]
- pub async fn save_steam_account_to_project(
- steamUsername: String,
- steamPassword: String,
- steamGuardPath: String,
- isUsAccount: Option<bool>,
- ) -> Result<(), String> {
- use serde_json::{json, Value};
- let steam_msg_dir = get_steam_msg_dir()?;
- fs::create_dir_all(&steam_msg_dir).map_err(|e| format!("创建目录失败: {}", e))?;
- let safe_name = sanitize_username_for_filename(&steamUsername);
- let mafile_dest = steam_msg_dir.join(format!("{}.maFile", safe_name));
- if !steamGuardPath.is_empty() {
- let src = PathBuf::from(&steamGuardPath);
- if src.exists() {
- fs::copy(&src, &mafile_dest).map_err(|e| format!("复制 maFile 失败: {}", e))?;
- }
- }
- let steam_guard_path_str = mafile_dest.to_string_lossy().to_string();
- let accounts_file = steam_msg_dir.join(ACCOUNTS_JSON);
- let mut root: Value = if accounts_file.exists() {
- let text = fs::read_to_string(&accounts_file).map_err(|e| format!("读取账号文件失败: {}", e))?;
- serde_json::from_str(&text).unwrap_or_else(|_| json!({ "accounts": {} }))
- } else {
- json!({ "accounts": {} })
- };
- let accounts = root.get_mut("accounts").and_then(|a| a.as_object_mut()).ok_or("账号数据结构异常")?;
- let is_us = isUsAccount.unwrap_or(true);
- let entry = json!({
- "steam_account": steamUsername,
- "steam_password": steamPassword,
- "steam_guard_path": steam_guard_path_str,
- "is_us_account": is_us
- });
- accounts.insert(steamUsername.clone(), entry);
- let content = serde_json::to_string_pretty(&root).map_err(|e| format!("序列化失败: {}", e))?;
- fs::write(&accounts_file, content).map_err(|e| format!("写入账号文件失败: {}", e))?;
- Ok(())
- }
- #[tauri::command]
- pub async fn list_project_steam_accounts() -> Result<String, String> {
- let steam_msg_dir = get_steam_msg_dir()?;
- let accounts_file = steam_msg_dir.join(ACCOUNTS_JSON);
- if !accounts_file.exists() {
- return Ok("[]".to_string());
- }
- let text = fs::read_to_string(&accounts_file).map_err(|e| format!("Read accounts file failed: {}", e))?;
- let root: serde_json::Value = serde_json::from_str(&text).unwrap_or(serde_json::json!({ "accounts": {} }));
- let accounts_obj = root
- .get("accounts")
- .and_then(|a| a.as_object())
- .map(|m| m.values().cloned().collect::<Vec<_>>())
- .unwrap_or_default();
- serde_json::to_string(&accounts_obj).map_err(|e| format!("Serialize failed: {}", e))
- }
- #[tauri::command]
- pub async fn delete_project_steam_account(steamUsername: String) -> Result<(), String> {
- let steam_msg_dir = get_steam_msg_dir()?;
- let accounts_file = steam_msg_dir.join(ACCOUNTS_JSON);
- if accounts_file.exists() {
- let text = fs::read_to_string(&accounts_file).map_err(|e| format!("Read accounts file failed: {}", e))?;
- let mut root: serde_json::Value = serde_json::from_str(&text).unwrap_or(serde_json::json!({ "accounts": {} }));
- if let Some(accounts) = root.get_mut("accounts").and_then(|a| a.as_object_mut()) {
- accounts.remove(&steamUsername);
- }
- let content = serde_json::to_string_pretty(&root).map_err(|e| format!("Serialize failed: {}", e))?;
- fs::write(&accounts_file, content).map_err(|e| format!("Failed to write accounts json: {}", e))?;
- }
- let safe_name = sanitize_username_for_filename(&steamUsername);
- let mafile_path = steam_msg_dir.join(format!("{}.maFile", safe_name));
- if mafile_path.exists() {
- let _ = fs::remove_file(&mafile_path);
- }
- Ok(())
- }
- /// 更新账号的是否美国账号属性
- #[tauri::command]
- pub async fn update_account_is_us_account(
- steamUsername: String,
- isUsAccount: bool,
- ) -> Result<(), String> {
- let steam_msg_dir = get_steam_msg_dir()?;
- let accounts_file = steam_msg_dir.join(ACCOUNTS_JSON);
- if !accounts_file.exists() {
- return Err("账号文件不存在".to_string());
- }
- let text = fs::read_to_string(&accounts_file).map_err(|e| format!("读取账号文件失败: {}", e))?;
- let mut root: serde_json::Value =
- serde_json::from_str(&text).unwrap_or(serde_json::json!({ "accounts": {} }));
- let accounts = root
- .get_mut("accounts")
- .and_then(|a| a.as_object_mut())
- .ok_or("账号数据结构异常")?;
- let entry = accounts
- .get_mut(&steamUsername)
- .and_then(|v| v.as_object_mut())
- .ok_or_else(|| format!("未找到账号 {}", steamUsername))?;
- entry.insert(
- "is_us_account".to_string(),
- serde_json::json!(isUsAccount),
- );
- let content =
- serde_json::to_string_pretty(&root).map_err(|e| format!("序列化失败: {}", e))?;
- fs::write(&accounts_file, content).map_err(|e| format!("写入账号文件失败: {}", e))?;
- Ok(())
- }
- /// 更新账号的充值卡价比例
- #[tauri::command]
- pub async fn update_account_card_rate(
- steamUsername: String,
- cardRate: Option<f64>,
- ) -> Result<(), String> {
- let steam_msg_dir = get_steam_msg_dir()?;
- let accounts_file = steam_msg_dir.join(ACCOUNTS_JSON);
- if !accounts_file.exists() {
- return Err("账号文件不存在".to_string());
- }
- let text = fs::read_to_string(&accounts_file).map_err(|e| format!("读取账号文件失败: {}", e))?;
- let mut root: serde_json::Value =
- serde_json::from_str(&text).unwrap_or(serde_json::json!({ "accounts": {} }));
- let accounts = root
- .get_mut("accounts")
- .and_then(|a| a.as_object_mut())
- .ok_or("账号数据结构异常")?;
- let entry = accounts
- .get_mut(&steamUsername)
- .and_then(|v| v.as_object_mut())
- .ok_or_else(|| format!("未找到账号 {}", steamUsername))?;
- match cardRate {
- Some(rate) => {
- entry.insert(
- "card_rate".to_string(),
- serde_json::json!(rate),
- );
- }
- None => {
- entry.remove("card_rate");
- }
- }
- let content =
- serde_json::to_string_pretty(&root).map_err(|e| format!("序列化失败: {}", e))?;
- fs::write(&accounts_file, content).map_err(|e| format!("写入账号文件失败: {}", e))?;
- Ok(())
- }
- /// 更新账号的是否参与扫货状态
- #[tauri::command]
- pub async fn update_account_is_active(
- steamUsername: String,
- isActive: bool,
- ) -> Result<(), String> {
- let steam_msg_dir = get_steam_msg_dir()?;
- let accounts_file = steam_msg_dir.join(ACCOUNTS_JSON);
- if !accounts_file.exists() {
- return Err("账号文件不存在".to_string());
- }
- let text = fs::read_to_string(&accounts_file).map_err(|e| format!("读取账号文件失败: {}", e))?;
- let mut root: serde_json::Value =
- serde_json::from_str(&text).unwrap_or(serde_json::json!({ "accounts": {} }));
- let accounts = root
- .get_mut("accounts")
- .and_then(|a| a.as_object_mut())
- .ok_or("账号数据结构异常")?;
- let entry = accounts
- .get_mut(&steamUsername)
- .and_then(|v| v.as_object_mut())
- .ok_or_else(|| format!("未找到账号 {}", steamUsername))?;
- entry.insert(
- "is_active".to_string(),
- serde_json::json!(isActive),
- );
- let content =
- serde_json::to_string_pretty(&root).map_err(|e| format!("序列化失败: {}", e))?;
- fs::write(&accounts_file, content).map_err(|e| format!("写入账号文件失败: {}", e))?;
- Ok(())
- }
- /// 将指定账号的 maFile 复制到用户选择的路径(下载 maFile)
- #[tauri::command]
- pub async fn export_mafile_to_path(steam_username: String, dest_path: String) -> Result<(), String> {
- let steam_msg_dir = get_steam_msg_dir()?;
- let safe_name = sanitize_username_for_filename(&steam_username);
- let src = steam_msg_dir.join(format!("{}.maFile", safe_name));
- if !src.exists() {
- return Err(format!("未找到账号 {} 的 maFile", steam_username));
- }
- let dest = PathBuf::from(&dest_path);
- fs::copy(&src, &dest).map_err(|e| format!("复制 maFile 失败: {}", e))?;
- Ok(())
- }
- /// 在 `accounts` 对象中解析与后台绑定名、界面传入名一致的存储键(精确匹配 → 忽略大小写 → 匹配条目内 `steam_account`)。
- fn resolve_accounts_storage_key(
- accounts: &serde_json::Map<String, serde_json::Value>,
- hint: &str,
- ) -> Option<String> {
- let hint = hint.trim();
- if hint.is_empty() {
- return None;
- }
- if accounts.contains_key(hint) {
- return Some(hint.to_string());
- }
- for (k, v) in accounts {
- if k.trim().eq_ignore_ascii_case(hint) {
- return Some(k.clone());
- }
- if let Some(sa) = v.get("steam_account").and_then(|x| x.as_str()) {
- if sa.trim().eq_ignore_ascii_case(hint) {
- return Some(k.clone());
- }
- }
- }
- None
- }
- /// 与界面「查询余额」按钮同源:读 `accounts.json`、POST 本地 steamAPI `wallet-balance`(含代理重试)。
- pub(super) async fn steam_wallet_balance_fetch(lookup_username: &str) -> Result<String, String> {
- use serde_json::Value;
- let steam_msg_dir = get_steam_msg_dir()?;
- let accounts_file = steam_msg_dir.join(ACCOUNTS_JSON);
- if !accounts_file.exists() {
- return Err("本地账号文件不存在".to_string());
- }
- let text = fs::read_to_string(&accounts_file).map_err(|e| format!("读取账号文件失败: {}", e))?;
- let root: Value = serde_json::from_str(&text).map_err(|e| format!("账号文件格式错误: {}", e))?;
- let accounts = root.get("accounts").and_then(|a| a.as_object()).ok_or("账号文件结构错误")?;
- let storage_key = resolve_accounts_storage_key(accounts, lookup_username).ok_or_else(|| {
- format!(
- "未找到账号 {}(请确认 accounts.json 中键名或 steam_account 与后台绑定一致)",
- lookup_username
- )
- })?;
- let obj = accounts
- .get(&storage_key)
- .ok_or_else(|| format!("未找到账号条目 {}", storage_key))?;
- let steam_login_secure = obj
- .get("steamLoginSecure")
- .and_then(|v| v.as_str())
- .filter(|s| !s.is_empty())
- .ok_or_else(|| format!("账号 {} 暂无登录凭证,请先进行「账号验证绑定」", lookup_username))?;
- let client = get_http_client();
- let api_url = "http://localhost:5000/wallet-balance";
- let request_body = serde_json::json!({
- "SteamLoginSecure": steam_login_secure
- });
- let (status, body) = post_steam_api_with_proxy_retry(
- &client,
- api_url,
- &request_body,
- "wallet-balance",
- )
- .await?;
- if !status.is_success() {
- return Err(format!("steamAPI 余额接口返回错误: {} {}", status.as_u16(), body));
- }
- let json: Value = serde_json::from_str(&body).map_err(|e| format!("响应解析失败: {}", e))?;
- let amount = json
- .get("amount")
- .and_then(|v| v.as_str())
- .unwrap_or("0")
- .to_string();
- let currency = json
- .get("currency")
- .and_then(|v| v.as_str())
- .unwrap_or("USD")
- .to_string();
- let out = serde_json::json!({ "amount": amount, "currency": currency });
- serde_json::to_string(&out).map_err(|e| format!("序列化失败: {}", e))
- }
- /// 通过本地 steamAPI 获取账号余额(由 steamAPI 统一处理 Steam 出网代理)
- #[tauri::command]
- pub async fn get_steam_wallet_balance(steam_username: String) -> Result<String, String> {
- steam_wallet_balance_fetch(&steam_username).await
- }
- /// IP 代理配置文件名(与 accounts.json 同目录,仅存本地)
- const PROXY_CONFIG_JSON: &str = "proxy_config.json";
- static API_PROXY_CACHE: OnceLock<Mutex<Option<String>>> = OnceLock::new();
- #[derive(Debug, Clone, Copy, PartialEq, Eq)]
- enum ProxyModeKind {
- None,
- Api,
- Account,
- }
- #[derive(Debug, Clone)]
- struct ProxySelection {
- mode: ProxyModeKind,
- proxy_url: Option<String>,
- }
- #[derive(Debug, Default, Deserialize)]
- struct StoredProxyConfig {
- #[serde(rename = "effectiveBrandId")]
- effective_brand_id: Option<String>,
- #[serde(rename = "effectiveMode")]
- effective_mode: Option<String>,
- configs: Option<std::collections::HashMap<String, StoredBrandConfig>>,
- }
- #[derive(Debug, Default, Deserialize, Clone)]
- struct StoredBrandConfig {
- #[serde(rename = "apiUrl")]
- api_url: Option<String>,
- #[serde(rename = "serverHost")]
- server_host: Option<String>,
- #[serde(rename = "serverPort")]
- server_port: Option<u16>,
- #[serde(rename = "serverUsername")]
- server_username: Option<String>,
- #[serde(rename = "serverPassword")]
- server_password: Option<String>,
- }
- #[derive(Debug, Deserialize)]
- struct ProxyTestRequest {
- mode: String,
- protocol: String,
- #[serde(rename = "apiUrl")]
- api_url: Option<String>,
- #[serde(rename = "serverHost")]
- server_host: Option<String>,
- #[serde(rename = "serverPort")]
- server_port: Option<u16>,
- #[serde(rename = "serverUsername")]
- server_username: Option<String>,
- #[serde(rename = "serverPassword")]
- server_password: Option<String>,
- }
- #[derive(Debug, Serialize)]
- struct ProxyTestResponse {
- success: bool,
- logs: Vec<String>,
- error: Option<String>,
- }
- fn api_proxy_cache() -> &'static Mutex<Option<String>> {
- API_PROXY_CACHE.get_or_init(|| Mutex::new(None))
- }
- fn clear_api_proxy_cache() {
- if let Ok(mut cache) = api_proxy_cache().lock() {
- *cache = None;
- }
- }
- /// 扫货多路并行时,每路 worker 使用独立缓存,避免多路共用一个 API 代理出口。
- fn clear_sweep_worker_api_proxy_cache(slot: &std::sync::Arc<Mutex<Option<String>>>) {
- if let Ok(mut g) = slot.lock() {
- *g = None;
- }
- }
- fn is_proxy_related_failure(
- status: Option<reqwest::StatusCode>,
- err_msg: Option<&str>,
- body: Option<&str>,
- ) -> bool {
- if let Some(s) = status {
- if s == reqwest::StatusCode::PROXY_AUTHENTICATION_REQUIRED
- || s == reqwest::StatusCode::BAD_GATEWAY
- || s == reqwest::StatusCode::SERVICE_UNAVAILABLE
- || s == reqwest::StatusCode::GATEWAY_TIMEOUT
- || s == reqwest::StatusCode::TOO_MANY_REQUESTS
- {
- return true;
- }
- }
- let mut text = String::new();
- if let Some(msg) = err_msg {
- text.push_str(msg);
- text.push(' ');
- }
- if let Some(resp) = body {
- text.push_str(resp);
- }
- let text = text.to_lowercase();
- [
- "proxy",
- "tunnel",
- "timed out",
- "timeout",
- "connection refused",
- "connection reset",
- "failed to connect",
- "unreachable",
- "network is unreachable",
- "name or service not known",
- ]
- .iter()
- .any(|token| text.contains(token))
- }
- /// 解析单行代理 URL
- fn normalize_proxy_url(raw: &str) -> Option<String> {
- let value = raw.trim().trim_matches('"').trim_matches('\'');
- if value.is_empty() {
- return None;
- }
- if value.starts_with("http://") || value.starts_with("https://") {
- return Some(value.to_string());
- }
- if value.contains(':') {
- return Some(format!("http://{}", value));
- }
- None
- }
- /// 解析 API 返回的多行代理列表(每行一个 IP:Port)
- fn parse_multi_line_proxies(body: &str) -> Vec<String> {
- let trimmed = body.trim();
- if trimmed.is_empty() {
- return Vec::new();
- }
- let mut proxies = Vec::new();
- for line in trimmed.lines() {
- let line = line.trim();
- if line.is_empty() {
- continue;
- }
- // 跳过 JSON 响应
- if line.starts_with('{') || line.starts_with('[') || line.starts_with('{') {
- continue;
- }
- // 尝试解析 IP:Port 格式
- if let Some(proxy) = normalize_proxy_url(line) {
- proxies.push(proxy);
- }
- }
- proxies
- }
- fn normalize_proxy_url_with_protocol(raw: &str, protocol: &str) -> Option<String> {
- let value = raw.trim().trim_matches('"').trim_matches('\'');
- if value.is_empty() {
- return None;
- }
- if value.starts_with("http://") || value.starts_with("https://") || value.starts_with("socks5://") {
- return Some(value.to_string());
- }
- if !value.contains(':') {
- return None;
- }
- let scheme = if protocol.eq_ignore_ascii_case("socks5") {
- "socks5"
- } else {
- "http"
- };
- Some(format!("{}://{}", scheme, value))
- }
- // ============================================================================
- // 代理池:支持批量获取代理,自动从 API 拉取并缓存
- // ============================================================================
- /// 代理池容量阈值:少于此数量时自动从 API 获取新的代理
- const PROXY_POOL_LOW_THRESHOLD: usize = 20;
- /// 代理池每次从 API 获取的数量
- const PROXY_POOL_FETCH_COUNT: usize = 100;
- /// 代理池全局状态
- static PROXY_POOL: OnceLock<Mutex<ProxyPool>> = OnceLock::new();
- /// 保护扫货成功文件写入的全局锁
- static SWEEP_SUCCESS_FILE_LOCK: OnceLock<Mutex<()>> = OnceLock::new();
- fn get_sweep_success_file_lock() -> &'static Mutex<()> {
- SWEEP_SUCCESS_FILE_LOCK.get_or_init(|| Mutex::new(()))
- }
- /// 获取代理池实例(线程安全)
- fn get_proxy_pool() -> &'static Mutex<ProxyPool> {
- PROXY_POOL.get_or_init(|| Mutex::new(ProxyPool::new()))
- }
- /// 代理池结构
- struct ProxyPool {
- /// 可用的代理列表
- proxies: Vec<String>,
- /// 正在使用的代理(避免重复使用)
- in_use: HashMap<String, usize>, // proxy_url -> 使用计数
- }
- impl ProxyPool {
- fn new() -> Self {
- Self {
- proxies: Vec::new(),
- in_use: HashMap::new(),
- }
- }
- /// 获取一个可用代理(优先获取最少使用的)
- fn get_proxy(&mut self) -> Option<String> {
- if self.proxies.is_empty() {
- return None;
- }
- // 按使用次数排序,取最少的那个
- self.proxies.sort_by(|a, b| {
- let count_a = self.in_use.get(a).unwrap_or(&0);
- let count_b = self.in_use.get(b).unwrap_or(&0);
- count_a.cmp(count_b)
- });
- if let Some(proxy) = self.proxies.pop() {
- *self.in_use.entry(proxy.clone()).or_insert(0) += 1;
- return Some(proxy);
- }
- None
- }
- /// 归还代理(成功使用后调用,减少使用计数)
- fn return_proxy(&mut self, proxy: &str) {
- if let Some(count) = self.in_use.get_mut(proxy) {
- if *count > 0 {
- *count -= 1;
- }
- }
- }
- /// 标记代理为失效(被封禁等),从池中移除
- fn mark_proxy_invalid(&mut self, proxy: &str) {
- self.proxies.retain(|p| p != proxy);
- self.in_use.remove(proxy);
- }
- /// 添加多个代理到池中
- fn add_proxies(&mut self, proxies: Vec<String>) {
- for proxy in proxies {
- if !self.in_use.contains_key(&proxy) && !self.proxies.contains(&proxy) {
- self.proxies.push(proxy);
- }
- }
- }
- /// 检查是否需要补充代理
- fn needs_refill(&self) -> bool {
- self.proxies.len() < PROXY_POOL_LOW_THRESHOLD
- }
- /// 获取当前可用代理数量
- fn available_count(&self) -> usize {
- self.proxies.len()
- }
- }
- fn parse_proxy_api_response(body: &str) -> Option<String> {
- let trimmed = body.trim();
- if trimmed.is_empty() {
- return None;
- }
- if let Ok(json) = serde_json::from_str::<serde_json::Value>(trimmed) {
- if let Some(s) = json.as_str() {
- return normalize_proxy_url(s);
- }
- if let Some(obj) = json.as_object() {
- for key in ["proxyUrl", "proxy_url", "url", "proxy"] {
- if let Some(v) = obj.get(key).and_then(|x| x.as_str()) {
- if let Some(proxy) = normalize_proxy_url(v) {
- return Some(proxy);
- }
- }
- }
- let host = obj
- .get("ip")
- .and_then(|x| x.as_str())
- .or_else(|| obj.get("host").and_then(|x| x.as_str()))
- .or_else(|| obj.get("server").and_then(|x| x.as_str()));
- let port = obj
- .get("port")
- .and_then(|x| x.as_u64())
- .or_else(|| obj.get("serverPort").and_then(|x| x.as_u64()));
- if let (Some(h), Some(p)) = (host, port) {
- return normalize_proxy_url(&format!("{}:{}", h, p));
- }
- }
- }
- for line in trimmed.lines() {
- let candidate = line.trim().split(',').next().unwrap_or("").trim();
- if let Some(proxy) = normalize_proxy_url(candidate) {
- return Some(proxy);
- }
- }
- None
- }
- fn parse_proxy_api_response_by_protocol(body: &str, protocol: &str) -> Option<String> {
- let trimmed = body.trim();
- if trimmed.is_empty() {
- return None;
- }
- if let Ok(json) = serde_json::from_str::<serde_json::Value>(trimmed) {
- if let Some(s) = json.as_str() {
- return normalize_proxy_url_with_protocol(s, protocol);
- }
- if let Some(obj) = json.as_object() {
- for key in ["proxyUrl", "proxy_url", "url", "proxy"] {
- if let Some(v) = obj.get(key).and_then(|x| x.as_str()) {
- if let Some(proxy) = normalize_proxy_url_with_protocol(v, protocol) {
- return Some(proxy);
- }
- }
- }
- let host = obj
- .get("ip")
- .and_then(|x| x.as_str())
- .or_else(|| obj.get("host").and_then(|x| x.as_str()))
- .or_else(|| obj.get("server").and_then(|x| x.as_str()));
- let port = obj
- .get("port")
- .and_then(|x| x.as_u64())
- .or_else(|| obj.get("serverPort").and_then(|x| x.as_u64()));
- if let (Some(h), Some(p)) = (host, port) {
- return normalize_proxy_url_with_protocol(&format!("{}:{}", h, p), protocol);
- }
- }
- }
- for line in trimmed.lines() {
- let candidate = line.trim().split(',').next().unwrap_or("").trim();
- if let Some(proxy) = normalize_proxy_url_with_protocol(candidate, protocol) {
- return Some(proxy);
- }
- }
- None
- }
- fn clip_text(value: &str, max_chars: usize) -> String {
- value.chars().take(max_chars).collect::<String>()
- }
- async fn fetch_public_ip_via_client(client: &reqwest::Client) -> Result<String, String> {
- let ip_services = [
- "https://api.ipify.org?format=json",
- "https://httpbin.org/ip",
- "https://ifconfig.me/ip",
- ];
- let mut last_error = String::new();
- for url in ip_services {
- let resp = match client.get(url).send().await {
- Ok(v) => v,
- Err(e) => {
- last_error = format!("请求 {} 失败: {}", url, e);
- continue;
- }
- };
- let status = resp.status();
- let body = resp
- .text()
- .await
- .map_err(|e| format!("读取 IP 检测响应失败: {}", e))?;
- if !status.is_success() {
- last_error = format!("请求 {} 返回 {}: {}", url, status.as_u16(), clip_text(&body, 220));
- continue;
- }
- if let Ok(json) = serde_json::from_str::<serde_json::Value>(&body) {
- if let Some(ip) = json.get("ip").and_then(|v| v.as_str()) {
- return Ok(ip.to_string());
- }
- if let Some(origin) = json.get("origin").and_then(|v| v.as_str()) {
- return Ok(origin.to_string());
- }
- }
- let plain_ip = body.trim().to_string();
- if !plain_ip.is_empty() {
- return Ok(plain_ip);
- }
- }
- if last_error.is_empty() {
- Err("无法从公网 IP 检测服务获取结果".to_string())
- } else {
- Err(last_error)
- }
- }
- fn build_account_proxy_url(req: &ProxyTestRequest) -> Result<String, String> {
- let host = req.server_host.clone().unwrap_or_default().trim().to_string();
- let port = req.server_port.unwrap_or_default();
- if host.is_empty() || port == 0 {
- return Err("账号密码方式配置不完整:请填写代理服务器和端口".to_string());
- }
- let scheme = if req.protocol.eq_ignore_ascii_case("socks5") {
- "socks5"
- } else {
- "http"
- };
- let username = req.server_username.clone().unwrap_or_default();
- let password = req.server_password.clone().unwrap_or_default();
- if username.trim().is_empty() {
- Ok(format!("{}://{}:{}", scheme, host, port))
- } else {
- Ok(format!("{}://{}:{}@{}:{}", scheme, username, password, host, port))
- }
- }
- #[tauri::command]
- pub async fn test_proxy_configuration(payload: String) -> Result<String, String> {
- let req: ProxyTestRequest =
- serde_json::from_str(&payload).map_err(|e| format!("测试参数格式错误: {}", e))?;
- let mut logs: Vec<String> = Vec::new();
- logs.push(format!(
- "开始测试,模式: {},协议: {}",
- req.mode,
- req.protocol
- ));
- let test_result: Result<(), String> = async {
- let protocol = req.protocol.to_lowercase();
- if protocol != "http" && protocol != "socks5" {
- return Err("不支持的代理协议,仅支持 http/socks5".to_string());
- }
- let proxy_url = if req.mode.eq_ignore_ascii_case("api") {
- let api_url = req.api_url.clone().unwrap_or_default();
- if api_url.trim().is_empty() {
- return Err("API 方式配置不完整:请填写 API 地址".to_string());
- }
- logs.push(format!("步骤1/3:请求代理 API -> {}", api_url));
- let client = reqwest::Client::builder()
- .timeout(Duration::from_secs(12))
- .build()
- .map_err(|e| format!("创建请求客户端失败: {}", e))?;
- let resp = client
- .get(&api_url)
- .send()
- .await
- .map_err(|e| format!("请求代理 API 失败: {}", e))?;
- let status = resp.status();
- let body = resp
- .text()
- .await
- .map_err(|e| format!("读取代理 API 响应失败: {}", e))?;
- logs.push(format!("代理 API 响应状态: {}", status.as_u16()));
- logs.push(format!("代理 API 响应内容: {}", clip_text(&body, 500)));
- if !status.is_success() {
- return Err(format!(
- "代理 API 返回错误: {} {}",
- status.as_u16(),
- clip_text(&body, 220)
- ));
- }
- let parsed = parse_proxy_api_response_by_protocol(&body, &protocol)
- .ok_or_else(|| "代理 API 返回内容无法解析为代理地址,请检查接口文档".to_string())?;
- logs.push(format!("步骤2/3:解析到代理地址 -> {}", parsed));
- parsed
- } else if req.mode.eq_ignore_ascii_case("account") {
- logs.push("步骤1/3:组装账号密码代理地址".to_string());
- let built = build_account_proxy_url(&req)?;
- logs.push(format!("步骤2/3:已生成代理地址 -> {}", built));
- built
- } else {
- return Err("不支持的测试模式,仅支持 api/account".to_string());
- };
- logs.push("步骤3/3:通过代理请求公网 IP 检测服务".to_string());
- let proxy = reqwest::Proxy::all(&proxy_url)
- .map_err(|e| format!("代理地址格式错误,无法创建客户端: {}", e))?;
- let test_client = reqwest::Client::builder()
- .proxy(proxy)
- .timeout(Duration::from_secs(16))
- .build()
- .map_err(|e| format!("创建代理测试客户端失败: {}", e))?;
- let ip = fetch_public_ip_via_client(&test_client).await?;
- logs.push(format!("代理可用,出口 IP: {}", ip));
- Ok(())
- }
- .await;
- let response = match test_result {
- Ok(()) => ProxyTestResponse {
- success: true,
- logs,
- error: None,
- },
- Err(err) => {
- logs.push(format!("测试失败: {}", err));
- ProxyTestResponse {
- success: false,
- logs,
- error: Some(err),
- }
- }
- };
- serde_json::to_string(&response).map_err(|e| format!("序列化测试结果失败: {}", e))
- }
- fn load_stored_proxy_config() -> Result<StoredProxyConfig, String> {
- let steam_msg_dir = get_steam_msg_dir()?;
- let path = steam_msg_dir.join(PROXY_CONFIG_JSON);
- if !path.exists() {
- return Ok(StoredProxyConfig::default());
- }
- let raw = fs::read_to_string(&path).map_err(|e| format!("读取代理配置失败: {}", e))?;
- serde_json::from_str::<StoredProxyConfig>(&raw).map_err(|e| format!("代理配置格式错误: {}", e))
- }
- /// 获取当前生效的代理 API URL(供 Worker 独立代理池使用)
- pub fn get_effective_proxy_api_url() -> Option<String> {
- let stored = load_stored_proxy_config().ok()?;
- let mode = stored.effective_mode?;
- if mode != "api" {
- return None;
- }
- let brand_id = stored.effective_brand_id?;
- let configs = stored.configs?;
- let brand = configs.get(&brand_id)?;
- let api_url = brand.api_url.clone()?;
- if api_url.trim().is_empty() {
- return None;
- }
- Some(api_url)
- }
- /// WorkerProxyPool 专用的获取代理函数(获取一个代理)
- pub async fn fetch_one_proxy_for_worker() -> Result<String, String> {
- let api_url = get_effective_proxy_api_url()
- .ok_or_else(|| "未配置代理 API".to_string())?;
-
- let client = reqwest::Client::builder()
- .timeout(Duration::from_secs(12))
- .build()
- .map_err(|e| format!("创建代理API客户端失败: {}", e))?;
- let resp = client
- .get(&api_url)
- .send()
- .await
- .map_err(|e| format!("请求代理API失败: {}", e))?;
-
- let status = resp.status();
- let body = resp.text().await.map_err(|e| format!("读取代理API响应失败: {}", e))?;
-
- if !status.is_success() {
- return Err(format!("代理API返回错误: {} {}", status.as_u16(), body));
- }
- let proxies = parse_multi_line_proxies(&body);
- if proxies.is_empty() {
- return Err("代理API返回内容无法解析".to_string());
- }
- // 返回第一个代理
- Ok(proxies.into_iter().next().unwrap())
- }
- async fn fetch_api_proxy_url(api_url: &str) -> Result<String, String> {
- let client = reqwest::Client::builder()
- .timeout(Duration::from_secs(12))
- .build()
- .map_err(|e| format!("创建代理API客户端失败: {}", e))?;
- let resp = client
- .get(api_url)
- .send()
- .await
- .map_err(|e| format!("请求代理API失败: {}", e))?;
- let status = resp.status();
- let body = resp.text().await.map_err(|e| format!("读取代理API响应失败: {}", e))?;
- if !status.is_success() {
- return Err(format!("代理API返回错误: {} {}", status.as_u16(), body));
- }
- // 解析多行代理列表
- let proxies = parse_multi_line_proxies(&body);
- let proxies_count = proxies.len();
- if proxies.is_empty() {
- // 降级:尝试旧解析方式
- return parse_proxy_api_response(&body)
- .ok_or_else(|| "代理API返回内容无法解析为 HTTP 代理".to_string());
- }
- // 添加到代理池
- let pool = get_proxy_pool();
- if let Ok(mut guard) = pool.lock() {
- guard.add_proxies(proxies);
- info!(
- "[proxy-pool] 从 API 获取了 {} 个代理,池中现有 {} 个可用",
- proxies_count,
- guard.available_count()
- );
- }
- // 从池中获取一个代理
- let pool = get_proxy_pool();
- if let Ok(mut guard) = pool.lock() {
- if let Some(proxy) = guard.get_proxy() {
- return Ok(proxy);
- }
- }
- Err("代理池为空,无法获取代理".to_string())
- }
- async fn resolve_local_http_proxy(force_refresh_api: bool) -> Result<ProxySelection, String> {
- resolve_local_http_proxy_for_sweep(force_refresh_api, None).await
- }
- /// `cache_slot`:`Some` 时使用该互斥缓存(扫货每路 worker 独立);`None` 时使用全局 `API_PROXY_CACHE`。
- async fn resolve_local_http_proxy_for_sweep(
- force_refresh_api: bool,
- cache_slot: Option<&std::sync::Arc<Mutex<Option<String>>>>,
- ) -> Result<ProxySelection, String> {
- let stored = load_stored_proxy_config()?;
- let mode_raw = stored.effective_mode.unwrap_or_default();
- let mode = match mode_raw.as_str() {
- "api" => ProxyModeKind::Api,
- "account" => ProxyModeKind::Account,
- _ => ProxyModeKind::None,
- };
- if mode == ProxyModeKind::None {
- return Ok(ProxySelection {
- mode,
- proxy_url: None,
- });
- }
- let brand_id = stored.effective_brand_id.unwrap_or_default();
- let brand = stored
- .configs
- .as_ref()
- .and_then(|map| map.get(&brand_id))
- .cloned()
- .ok_or_else(|| "未找到当前生效品牌的代理配置".to_string())?;
- if mode == ProxyModeKind::Account {
- let host = brand.server_host.unwrap_or_default();
- let port = brand.server_port.unwrap_or_default();
- if host.trim().is_empty() || port == 0 {
- return Err("账号密码代理配置不完整:请检查代理服务器和端口".to_string());
- }
- let user = brand.server_username.unwrap_or_default();
- let pass = brand.server_password.unwrap_or_default();
- let proxy_url = if user.is_empty() {
- format!("http://{}:{}", host, port)
- } else {
- format!("http://{}:{}@{}:{}", user, pass, host, port)
- };
- return Ok(ProxySelection {
- mode,
- proxy_url: Some(proxy_url),
- });
- }
- if !force_refresh_api {
- if let Some(slot) = cache_slot {
- if let Ok(cache) = slot.lock() {
- if let Some(cached) = cache.as_ref() {
- return Ok(ProxySelection {
- mode,
- proxy_url: Some(cached.clone()),
- });
- }
- }
- } else if let Ok(cache) = api_proxy_cache().lock() {
- if let Some(cached) = cache.as_ref() {
- return Ok(ProxySelection {
- mode,
- proxy_url: Some(cached.clone()),
- });
- }
- }
- }
- let api_url = brand.api_url.unwrap_or_default();
- if api_url.trim().is_empty() {
- return Err("API代理配置不完整:请填写 API 地址".to_string());
- }
- // 检查代理池是否需要补充(少于阈值时自动补充)
- let pool = get_proxy_pool();
- let should_refill = if let Ok(guard) = pool.lock() {
- guard.needs_refill()
- } else {
- true
- };
- if should_refill {
- info!("[proxy-pool] 代理池数量不足(<{}),自动从 API 补充", PROXY_POOL_LOW_THRESHOLD);
- // 预先调用 API 补充代理池(异步,带重试机制)
- let api_url_clone = api_url.clone();
- tokio::spawn(async move {
- refill_proxy_pool_with_retry(&api_url_clone, 3).await;
- });
- }
- // 从池中获取代理,如果没有则调用 API
- let proxy_url = get_proxy_from_pool_or_fetch(&api_url).await?;
- if let Some(slot) = cache_slot {
- if let Ok(mut cache) = slot.lock() {
- *cache = Some(proxy_url.clone());
- }
- } else if let Ok(mut cache) = api_proxy_cache().lock() {
- *cache = Some(proxy_url.clone());
- }
- Ok(ProxySelection {
- mode,
- proxy_url: Some(proxy_url),
- })
- }
- /// 带重试机制的代理池补充(异步)
- async fn refill_proxy_pool_with_retry(api_url: &str, max_retries: usize) {
- for attempt in 1..=max_retries {
- match fetch_api_proxy_url(api_url).await {
- Ok(_) => {
- info!("[proxy-pool] 异步补充代理成功");
- return;
- }
- Err(e) => {
- if attempt < max_retries {
- warn!("[proxy-pool] 异步补充代理失败 (尝试 {}/{}): {},2秒后重试",
- attempt, max_retries, e);
- tokio::time::sleep(Duration::from_secs(2)).await;
- } else {
- error!("[proxy-pool] 异步补充代理最终失败 (尝试 {}/{}): {}",
- attempt, max_retries, e);
- }
- }
- }
- }
- }
- /// 从代理池获取代理,如果没有则调用 API 获取
- async fn get_proxy_from_pool_or_fetch(api_url: &str) -> Result<String, String> {
- // 先尝试从池中获取
- {
- let pool = get_proxy_pool();
- if let Ok(mut guard) = pool.lock() {
- if let Some(proxy) = guard.get_proxy() {
- return Ok(proxy);
- }
- }
- }
- // 池为空,直接调用 API 获取
- fetch_api_proxy_url(api_url).await
- }
- async fn post_steam_api_once(
- client: &reqwest::Client,
- api_url: &str,
- base_body: &serde_json::Value,
- _proxy_url: Option<&str>,
- ) -> Result<(reqwest::StatusCode, String), String> {
- // 不再传递代理信息给 localhost:5000,让 steamAPI 服务直连 Steam
- // 代理只用于客户端直接访问 Steam 的接口(如 render、buylisting)
- let resp = client
- .post(api_url)
- .json(base_body)
- .send()
- .await
- .map_err(|e| format!("HTTP 请求失败: {}", e))?;
- let status = resp.status();
- let response_text = resp.text().await.map_err(|e| format!("读取响应失败: {}", e))?;
- Ok((status, response_text))
- }
- async fn post_steam_api_with_proxy_retry(
- client: &reqwest::Client,
- api_url: &str,
- base_body: &serde_json::Value,
- label: &str,
- ) -> Result<(reqwest::StatusCode, String), String> {
- // 调用 localhost:5000 接口,不使用代理(直连模式)
- // 代理仅用于客户端直接访问 Steam 的接口(如 render、buylisting)
- log::info!(
- "[api:{}] 直连 localhost:5000(不使用代理)",
- label
- );
- let resp = client
- .post(api_url)
- .json(base_body)
- .send()
- .await
- .map_err(|e| {
- log::error!("[api:{}] HTTP 请求失败: {}", label, e);
- format!("HTTP 请求失败: {}", e)
- })?;
- let status = resp.status();
- let response_text = resp.text().await.map_err(|e| {
- log::error!("[api:{}] 读取响应失败: {}", label, e);
- format!("读取响应失败: {}", e)
- })?;
- Ok((status, response_text))
- }
- /// 读取本地 IP 代理配置(存于 steam-msg 目录,不上传服务器)
- #[tauri::command]
- pub async fn read_proxy_config() -> Result<String, String> {
- let steam_msg_dir = get_steam_msg_dir()?;
- let path = steam_msg_dir.join(PROXY_CONFIG_JSON);
- if !path.exists() {
- return Ok("{}".to_string());
- }
- fs::read_to_string(&path).map_err(|e| format!("读取代理配置失败: {}", e))
- }
- /// 写入本地 IP 代理配置(存于 steam-msg 目录,不上传服务器)
- #[tauri::command]
- pub async fn write_proxy_config(json: String) -> Result<(), String> {
- let steam_msg_dir = get_steam_msg_dir()?;
- fs::create_dir_all(&steam_msg_dir).map_err(|e| format!("创建目录失败: {}", e))?;
- let path = steam_msg_dir.join(PROXY_CONFIG_JSON);
- fs::write(&path, json).map_err(|e| format!("写入代理配置失败: {}", e))?;
- clear_api_proxy_cache();
- Ok(())
- }
- /// 标记代理为失效(被封禁等),会从代理池中移除
- #[tauri::command]
- pub fn invalidate_proxy(proxy_url: String) -> Result<(), String> {
- let pool = get_proxy_pool();
- if let Ok(mut guard) = pool.lock() {
- guard.mark_proxy_invalid(&proxy_url);
- info!("[proxy-pool] 标记代理失效: {}", proxy_url);
- Ok(())
- } else {
- Err("代理池锁定失败".to_string())
- }
- }
- /// 获取代理池状态(用于调试)
- #[tauri::command]
- pub fn get_proxy_pool_status() -> String {
- let pool = get_proxy_pool();
- if let Ok(guard) = pool.lock() {
- let count = guard.available_count();
- let response = serde_json::json!({
- "code": 200,
- "msg": "ok",
- "data": {
- "available_count": count,
- "low_threshold": PROXY_POOL_LOW_THRESHOLD,
- "needs_refill": guard.needs_refill()
- }
- });
- serde_json::to_string(&response).unwrap_or_else(|_| r#"{"code":500,"msg":"序列化失败"}"#.to_string())
- } else {
- r#"{"code":500,"msg":"代理池锁定失败"}"#.to_string()
- }
- }
- /// 手动触发代理池补充(用于调试或主动刷新)
- #[tauri::command]
- pub async fn refill_proxy_pool() -> Result<(), String> {
- let stored = load_stored_proxy_config()?;
- let mode_raw = stored.effective_mode.unwrap_or_default();
- if mode_raw != "api" {
- return Err("当前代理模式不是 API 模式,无法手动补充".to_string());
- }
- let brand_id = stored.effective_brand_id.unwrap_or_default();
- let brand = stored
- .configs
- .as_ref()
- .and_then(|map| map.get(&brand_id))
- .cloned()
- .ok_or_else(|| "未找到当前生效品牌的代理配置".to_string())?;
- let api_url = brand.api_url.unwrap_or_default();
- if api_url.trim().is_empty() {
- return Err("API代理配置不完整".to_string());
- }
- fetch_api_proxy_url(&api_url).await?;
- info!("[proxy-pool] 手动触发代理池补充完成");
- Ok(())
- }
- /// 调用本地 steamAPI `/login` 刷新 session 并写回 `accounts.json`(与 `verify_steam_login` 同源逻辑)。
- /// 供扫货启动时对已绑定账号预热 token,避免购买阶段因过期凭证失败。
- pub(super) async fn steam_account_relogin_persist(steam_username: &str) -> Result<String, String> {
- use serde_json::Value;
- log::info!("[steam_relogin] 开始: {}", steam_username);
- let steam_msg_dir = get_steam_msg_dir()?;
- let accounts_file = steam_msg_dir.join(ACCOUNTS_JSON);
- if !accounts_file.exists() {
- log::error!(
- "[steam_relogin] 账号文件不存在: {}",
- accounts_file.to_string_lossy()
- );
- return Err(format!(
- "Accounts file not found: {}",
- accounts_file.to_string_lossy()
- ));
- }
- let text =
- fs::read_to_string(&accounts_file).map_err(|e| format!("Failed to read accounts file: {}", e))?;
- let mut root: Value = serde_json::from_str(&text).map_err(|e| format!("Invalid accounts json: {}", e))?;
- let storage_key = {
- let accounts_ro = root
- .get("accounts")
- .and_then(|a| a.as_object())
- .ok_or("Invalid accounts structure")?;
- resolve_accounts_storage_key(accounts_ro, steam_username).ok_or_else(|| {
- format!(
- "Account {} not found in local storage(accounts 键或 steam_account 字段)",
- steam_username
- )
- })?
- };
- let accounts = root
- .get_mut("accounts")
- .and_then(|a| a.as_object_mut())
- .ok_or("Invalid accounts structure")?;
- let mut obj: Value = accounts
- .get(&storage_key)
- .cloned()
- .ok_or_else(|| format!("账号条目缺失 {}", storage_key))?;
- let login_username = obj
- .get("steam_account")
- .and_then(|v| v.as_str())
- .map(|s| s.trim())
- .filter(|s| !s.is_empty())
- .unwrap_or(storage_key.as_str())
- .to_string();
- let steam_password = obj
- .get("steam_password")
- .and_then(|v| v.as_str())
- .unwrap_or("")
- .to_string();
- let steam_guard_path = obj
- .get("steam_guard_path")
- .and_then(|v| v.as_str())
- .unwrap_or("")
- .to_string();
- log::info!(
- "[steam_relogin] lookup={} storage_key={} login_username={} maFile 路径长度: {}",
- steam_username,
- storage_key,
- login_username,
- steam_guard_path.len()
- );
- let client = get_http_client();
- let api_url = "http://localhost:5000/login";
- let request_body = serde_json::json!({
- "Username": login_username,
- "Password": steam_password,
- "MaFilePath": steam_guard_path
- });
- log::info!("[steam_relogin] POST {}", api_url);
- let (status, resp_body) = match post_steam_api_with_proxy_retry(&client, api_url, &request_body, "login").await {
- Ok(result) => result,
- Err(e) => {
- log::error!("[steam_relogin] HTTP 失败: {}", e);
- obj["status"] = serde_json::Value::String("fail".to_string());
- if let Some(acc) = root.get_mut("accounts").and_then(|a| a.as_object_mut()) {
- acc.insert(storage_key.clone(), obj);
- }
- let _ = fs::write(&accounts_file, serde_json::to_string_pretty(&root).unwrap_or_default());
- return Err(format!("HTTP request failed: {}", e));
- }
- };
- log::info!("[steam_relogin] 状态码: {}", status);
- if !status.is_success() {
- let err_body = resp_body;
- log::error!(
- "[steam_relogin] 非成功, 响应(前500): {}",
- &err_body.chars().take(500).collect::<String>()
- );
- obj["status"] = serde_json::Value::String("fail".to_string());
- if let Some(acc) = root.get_mut("accounts").and_then(|a| a.as_object_mut()) {
- acc.insert(storage_key.clone(), obj);
- }
- let _ = fs::write(&accounts_file, serde_json::to_string_pretty(&root).unwrap_or_default());
- return Err(format!(
- "Steam 登录接口返回错误: {} - {}",
- status,
- if err_body.len() > 200 {
- format!("{}...", &err_body[..200])
- } else {
- err_body
- }
- ));
- }
- log::info!(
- "[steam_relogin] 响应长度: {}, 前200: {}",
- resp_body.len(),
- &resp_body.chars().take(200).collect::<String>()
- );
- let resp_json: Value = serde_json::from_str(&resp_body).map_err(|e| {
- log::error!(
- "[steam_relogin] JSON 解析失败: {}, 原始(前300): {}",
- e,
- &resp_body.chars().take(300).collect::<String>()
- );
- obj["status"] = serde_json::Value::String("fail".to_string());
- if let Some(acc) = root.get_mut("accounts").and_then(|a| a.as_object_mut()) {
- acc.insert(storage_key.clone(), obj.clone());
- }
- let _ = fs::write(&accounts_file, serde_json::to_string_pretty(&root).unwrap_or_default());
- format!("Failed to parse response JSON: {}", e)
- })?;
- if resp_json.get("encrypted").is_some() && resp_json.get("session").is_none() {
- log::error!("[steam_relogin] 加密响应且无 session");
- return Err(
- "Steam 登录接口返回了加密数据,当前仅支持明文 session 格式,请检查 localhost:5000 服务配置"
- .to_string(),
- );
- }
- if let Some(session) = resp_json.get("session") {
- if let Some(session_id) = session.get("sessionId").and_then(|v| v.as_str()) {
- obj["sessionId"] = serde_json::Value::String(session_id.to_string());
- }
- if let Some(access_token) = session.get("accessToken").and_then(|v| v.as_str()) {
- obj["accessToken"] = serde_json::Value::String(access_token.to_string());
- }
- if let Some(refresh_token) = session.get("refreshToken").and_then(|v| v.as_str()) {
- obj["refreshToken"] = serde_json::Value::String(refresh_token.to_string());
- }
- if let Some(steam_id) = session.get("steamId").and_then(|v| v.as_i64()) {
- obj["steamId"] = serde_json::Value::Number(serde_json::Number::from(steam_id));
- } else if let Some(steam_id_s) = session.get("steamId").and_then(|v| v.as_str()) {
- if let Ok(n) = steam_id_s.parse::<i64>() {
- obj["steamId"] = serde_json::Value::Number(serde_json::Number::from(n));
- } else {
- obj["steamId"] = serde_json::Value::String(steam_id_s.to_string());
- }
- }
- }
- if let Some(cookies) = resp_json.get("cookies").and_then(|v| v.as_array()) {
- for c in cookies {
- if let Some(name) = c.get("name").and_then(|n| n.as_str()) {
- if name == "steamLoginSecure" {
- if let Some(value) = c.get("value").and_then(|v| v.as_str()) {
- obj["steamLoginSecure"] = serde_json::Value::String(value.to_string());
- }
- }
- if name == "sessionid" {
- if let Some(value) = c.get("value").and_then(|v| v.as_str()) {
- obj["sessionId"] = serde_json::Value::String(value.to_string());
- }
- }
- }
- }
- }
- obj["status"] = serde_json::Value::String("success".to_string());
- let steam_id = obj
- .get("steamId")
- .and_then(|v| match v {
- serde_json::Value::Number(n) => n.as_i64().map(|i| i.to_string()),
- serde_json::Value::String(s) => Some(s.clone()),
- _ => None,
- })
- .unwrap_or_default();
- if let Some(acc) = root.get_mut("accounts").and_then(|a| a.as_object_mut()) {
- acc.insert(storage_key.clone(), obj);
- }
- let content = serde_json::to_string_pretty(&root).map_err(|e| format!("Serialize failed: {}", e))?;
- fs::write(&accounts_file, &content).map_err(|e| {
- log::error!("[steam_relogin] 写入 accounts 失败: {}", e);
- format!("Failed to write accounts json: {}", e)
- })?;
- log::info!(
- "[steam_relogin] 已写回 accounts: {}",
- accounts_file.to_string_lossy()
- );
- Ok(steam_id)
- }
- /// 扫货启动时 Steam 会话准备:与界面「查询余额 → 失败再重新登录 → 再查余额」一致,减少无效 /login 耗时。
- pub(super) async fn steam_account_sweep_startup_auth(lookup_username: &str) -> Result<(), String> {
- match steam_wallet_balance_fetch(lookup_username).await {
- Ok(_) => {
- log::info!(
- "[sweep_steam_auth] {} 余额探活成功(同界面「查询余额」),会话有效,跳过 /login",
- lookup_username
- );
- Ok(())
- }
- Err(e_first) => {
- log::info!(
- "[sweep_steam_auth] {} 余额探活失败,将执行与「重新登录」相同的 /login: {}",
- lookup_username,
- e_first
- );
- steam_account_relogin_persist(lookup_username).await?;
- if let Err(e2) = steam_wallet_balance_fetch(lookup_username).await {
- log::warn!(
- "[sweep_steam_auth] {} 已写回登录态,但登录后余额探活仍失败(可继续扫货,与界面一致仅提示): {}",
- lookup_username,
- e2
- );
- }
- Ok(())
- }
- }
- }
- #[tauri::command]
- pub async fn verify_steam_login(steamUsername: String) -> Result<String, String> {
- log::info!("[verify_steam_login] 开始验证账号: {}", steamUsername);
- let steam_id = steam_account_relogin_persist(&steamUsername).await?;
- if let Err(e) = steam_wallet_balance_fetch(&steamUsername).await {
- log::warn!("[verify_steam_login] 与界面一致:登录后余额探活失败(仍视为登录成功): {}", e);
- }
- if steam_id.is_empty() {
- log::warn!("[verify_steam_login] 未能解析 steamId,请检查 session.steamId");
- } else {
- log::info!("[verify_steam_login] steam_id: {}", steam_id);
- }
- let result = serde_json::json!({ "success": true, "steam_id": steam_id });
- Ok(result.to_string())
- }
- // ==================== 后端API调用命令 ====================
- #[tauri::command]
- pub async fn initialize_backend_api(base_url: String, software_code: String) -> Result<(), String> {
- let config = ApiConfig {
- base_url,
- timeout: 30,
- software_code,
- };
- initialize_api_client(config)?;
- Ok(())
- }
- #[tauri::command]
- pub async fn user_register(username: String, password: String, email: String, card_code: String, _machine_code: Option<String>) -> Result<String, String> {
- let mut client = get_api_client_for_async()?;
- // 注册时不传递机器码,让其为None
- let result = client.register(&username, &password, &email, &card_code, "").await?;
- serde_json::to_string(&result)
- .map_err(|e| format!("序列化响应失败: {}", e))
- }
- #[tauri::command]
- pub async fn user_login(username: String, password: String, machine_code: String) -> Result<String, String> {
- let mut client = get_api_client_for_async()?;
- let result = client.login(&username, &password, &machine_code).await?;
-
- // 登录成功后,启动心跳服务
- if let Some(ref data) = result.data {
- let api_url = get_api_base_url()?;
- let session_id = data.session_id.as_deref().unwrap_or("").to_string();
-
- if !session_id.is_empty() {
- info!("[Login] 登录成功,启动心跳服务");
- // 启动心跳服务(包括密钥同步定时任务)
- start_heartbeat_service(api_url, session_id, machine_code).await
- .map_err(|e| format!("启动心跳服务失败: {}", e))?;
- }
- }
- serde_json::to_string(&result)
- .map_err(|e| format!("序列化响应失败: {}", e))
- }
- #[tauri::command]
- pub async fn user_unbind(username: String, password: String) -> Result<String, String> {
- let mut client = get_api_client_for_async()?;
- let result = client.unbind(&username, &password).await?;
- serde_json::to_string(&result)
- .map_err(|e| format!("序列化响应失败: {}", e))
- }
- #[tauri::command]
- pub async fn user_recharge(username: String, password: String, card_code: String) -> Result<String, String> {
- let mut client = get_api_client_for_async()?;
- let result = client.recharge(&username, &password, &card_code).await?;
- serde_json::to_string(&result)
- .map_err(|e| format!("序列化响应失败: {}", e))
- }
- #[tauri::command]
- pub async fn user_heartbeat(user_id: i32) -> Result<String, String> {
- let mut client = get_api_client_for_async()?;
- let result = client.heartbeat(user_id).await?;
- serde_json::to_string(&result)
- .map_err(|e| format!("序列化响应失败: {}", e))
- }
- #[tauri::command]
- pub async fn user_logout(user_id: i32, session_id: String) -> Result<String, String> {
- // 扫货远程停止由前端在调用本接口前通过 `stop_all_processes` 完成。
- // 1. 停止心跳服务
- info!("[Logout] 停止心跳服务");
- stop_heartbeat_service().await;
- let mut client = get_api_client_for_async()?;
- let result = client.logout(user_id, &session_id).await?;
- serde_json::to_string(&result)
- .map_err(|e| format!("序列化响应失败: {}", e))
- }
- #[tauri::command]
- #[allow(dead_code)]
- pub async fn validate_session(session_id: String) -> Result<String, String> {
- let mut client = get_api_client_for_async()?;
- let result = client.validate_session(&session_id).await?;
- serde_json::to_string(&result)
- .map_err(|e| format!("序列化响应失败: {}", e))
- }
- #[tauri::command]
- pub async fn activate_card(user_id: i32, card_code: String) -> Result<String, String> {
- let mut client = get_api_client_for_async()?;
- let result = client.activate_card(user_id, &card_code).await?;
- serde_json::to_string(&result)
- .map_err(|e| format!("序列化响应失败: {}", e))
- }
- #[tauri::command]
- pub async fn get_steam_account_limit(user_id: i32) -> Result<String, String> {
- let mut client = get_api_client_for_async()?;
- let result = client.get_steam_account_limit(user_id).await?;
- serde_json::to_string(&result).map_err(|e| format!("序列化响应失败: {}", e))
- }
- #[tauri::command]
- pub async fn get_steam_accounts(user_id: i32) -> Result<String, String> {
- let mut client = get_api_client_for_async()?;
- let result = client.get_steam_accounts(user_id).await?;
- serde_json::to_string(&result)
- .map_err(|e| format!("序列化响应失败: {}", e))
- }
- #[tauri::command]
- pub async fn create_steam_account(user_id: i32, steam_username: String, steam_id: Option<String>) -> Result<String, String> {
- let mut client = get_api_client_for_async()?;
- let result = client.create_steam_account(user_id, &steam_username, steam_id.as_deref()).await?;
- serde_json::to_string(&result)
- .map_err(|e| format!("序列化响应失败: {}", e))
- }
- #[tauri::command]
- pub async fn bind_steam_account(user_id: i32, steam_username: String, steam_id: String) -> Result<String, String> {
- if steam_id.trim().is_empty() {
- log::warn!("[bind_steam_account] steam_id 为空,跳过后端绑定");
- return Err("steam_id 为空,请先完成 Steam 登录验证".to_string());
- }
- log::info!("[bind_steam_account] 开始: user_id={}, steam_username={}, steam_id={}", user_id, steam_username, steam_id);
- let mut client = get_api_client_for_async().map_err(|e| {
- log::error!("[bind_steam_account] 获取 API 客户端失败: {:?}", e);
- format!("获取 API 客户端失败: {:?}", e)
- })?;
- let result = client.bind_steam_account(user_id, &steam_username, &steam_id).await.map_err(|e| {
- log::error!("[bind_steam_account] 请求失败: {:?}", e);
- let msg = match &e {
- crate::error::ClientError::ApiBusinessError { message, .. } => message.clone(),
- crate::error::ClientError::ServerError { message, .. } => message.clone(),
- _ => format!("{:?}", e),
- };
- msg
- })?;
- let json = serde_json::to_string(&result).map_err(|e| {
- log::error!("[bind_steam_account] 序列化响应失败: {}", e);
- format!("序列化响应失败: {}", e)
- })?;
- log::info!("[bind_steam_account] 成功");
- Ok(json)
- }
- #[tauri::command]
- pub async fn update_steam_account(user_id: i32, account_id: i32, updates: String) -> Result<String, String> {
- let mut client = get_api_client_for_async()?;
- let updates_json: serde_json::Value = serde_json::from_str(&updates)
- .map_err(|e| format!("无效的更新JSON: {}", e))?;
- let result = client.update_steam_account(user_id, account_id, updates_json).await?;
- serde_json::to_string(&result)
- .map_err(|e| format!("序列化响应失败: {}", e))
- }
- #[tauri::command]
- pub async fn delete_steam_account(user_id: i32, account_id: i32) -> Result<String, String> {
- let mut client = get_api_client_for_async()?;
- let result = client.delete_steam_account(user_id, account_id).await?;
- serde_json::to_string(&result)
- .map_err(|e| format!("序列化响应失败: {}", e))
- }
- #[tauri::command]
- pub async fn get_announcements(page: i32, page_size: i32, keyword: Option<String>) -> Result<String, String> {
- let mut client = get_api_client_for_async()?;
- let result = client.get_announcements(page, page_size, keyword.as_deref()).await?;
- serde_json::to_string(&result)
- .map_err(|e| format!("序列化响应失败: {}", e))
- }
- #[tauri::command]
- pub async fn health_check() -> Result<String, String> {
- let client = get_api_client_for_async()?;
- let result = client.health_check().await?;
- serde_json::to_string(&result)
- .map_err(|e| format!("序列化响应失败: {}", e))
- }
- #[tauri::command]
- pub async fn get_public_key() -> Result<String, String> {
- let client = get_api_client_for_async()?;
- let result = client.get_public_key().await?;
- serde_json::to_string(&result)
- .map_err(|e| format!("序列化响应失败: {}", e))
- }
- #[tauri::command]
- pub async fn get_client_configs() -> Result<String, String> {
- let mut client = get_api_client_for_async()?;
- let result = client.get_client_configs().await?;
- serde_json::to_string(&result)
- .map_err(|e| format!("序列化响应失败: {}", e))
- }
- #[tauri::command]
- #[allow(dead_code)]
- pub fn clear_api_cache() -> Result<(), String> {
- // 清除API客户端的缓存
- // 这里需要根据实际的API客户端实现来清除缓存
- // 目前暂时返回成功,具体实现需要根据业务逻辑调整
- println!("清除API缓存的命令已调用");
- Ok(())
- }
- // ==================== 配置管理命令 ====================
- #[tauri::command]
- pub fn get_app_config() -> Result<String, String> {
- let config = AppConfig::load()
- .map_err(|e| format!("Failed to load config: {}", e))?;
- serde_json::to_string(&config)
- .map_err(|e| format!("Failed to serialize config: {}", e))
- }
- #[tauri::command]
- pub fn update_api_url(new_url: String) -> Result<(), String> {
- let mut config = AppConfig::load()
- .map_err(|e| format!("Failed to load config: {}", e))?;
- config.update_api_url(new_url)
- .map_err(|e| format!("Failed to update API URL: {}", e))?;
- // 重新初始化API客户端
- let api_config = ApiConfig {
- base_url: config.api.base_url.clone(),
- timeout: config.api.timeout,
- software_code: config.api.software_code.clone(),
- };
- initialize_api_client(api_config)
- .map_err(|e| format!("Failed to reinitialize API client: {}", e))
- }
- #[tauri::command]
- pub fn get_api_base_url() -> Result<String, String> {
- let config = AppConfig::load()
- .map_err(|e| format!("Failed to load config: {}", e))?;
- Ok(config.api.base_url)
- }
- // ==================== 机器码和存储功能 ====================
- #[tauri::command]
- pub fn get_machine_code() -> Result<String, String> {
- generate_machine_code()
- .map_err(|e| format!("生成机器码失败: {}", e))
- }
- #[tauri::command]
- pub fn store_user_info(user_info: String) -> Result<(), String> {
- let user_info: UserInfo = serde_json::from_str(&user_info)
- .map_err(|e| format!("解析用户信息失败: {}", e))?;
- let storage = get_storage()
- .map_err(|e| format!("获取存储失败: {}", e))?;
- storage.set_user_info(&user_info)
- .map_err(|e| format!("保存用户信息失败: {}", e))
- }
- #[tauri::command]
- pub fn get_stored_user_info() -> Result<String, String> {
- let storage = get_storage()
- .map_err(|e| format!("获取存储失败: {}", e))?;
- let user_info = storage.get_user_info()
- .map_err(|e| format!("读取用户信息失败: {}", e))?;
- serde_json::to_string(&user_info)
- .map_err(|e| format!("序列化用户信息失败: {}", e))
- }
- /// 获取当前登录用户名
- #[tauri::command]
- pub fn get_current_username() -> Result<String, String> {
- let storage = get_storage()
- .map_err(|e| format!("获取存储失败: {}", e))?;
- match storage.get_user_info() {
- Ok(user_info) => Ok(user_info.username),
- Err(_) => Ok("guest".to_string()),
- }
- }
- /// 从后端获取用户信息(安全加密调用,符合技术方案)
- #[tauri::command]
- pub async fn get_user_profile(
- session_id: String,
- machine_code: String,
- ) -> Result<serde_json::Value, String> {
- let api_url = get_api_base_url()?;
- secure_get_user_profile_call(&api_url, &session_id, &machine_code).await
- }
- #[tauri::command]
- pub fn get_system_username() -> Result<String, String> {
- // 尝试读取环境变量(Unix/Windows常用变量)
- if let Ok(u) = std::env::var("USER") {
- if !u.is_empty() {
- return Ok(u)
- }
- }
- if let Ok(u) = std::env::var("USERNAME") {
- if !u.is_empty() {
- return Ok(u)
- }
- }
- // 环境变量中没有找到用户名时返回 unknown
- Ok("unknown".to_string())
- }
- #[tauri::command]
- pub fn clear_stored_user_info() -> Result<(), String> {
- let storage = get_storage()
- .map_err(|e| format!("获取存储失败: {}", e))?;
- storage.remove("user_info")
- .map_err(|e| format!("清除用户信息失败: {}", e))
- }
- #[tauri::command]
- pub fn minimize_window() -> Result<(), String> {
- // 在Tauri中最小化窗口
- // 这里需要使用 Tauri's window API,但目前我们返回成功
- Ok(())
- }
- #[tauri::command]
- pub fn close_window() -> Result<(), String> {
- // 在Tauri中关闭窗口
- // 这里需要使用 Tauri's window API,但目前我们返回成功
- Ok(())
- }
- /// 登出 / 认证失效 / 关窗等场景:取消本地扫货 tokio 任务,并在有会话时尽力通知后端 `selection-stop` 与 `sweep-stop`。
- #[derive(Debug, Deserialize)]
- #[serde(rename_all = "camelCase")]
- pub struct StopAllProcessesArgs {
- pub session_id: Option<String>,
- pub machine_code: Option<String>,
- #[serde(default)]
- pub trace_id: Option<String>,
- }
- #[tauri::command]
- pub async fn stop_all_processes(args: StopAllProcessesArgs) -> Result<(), String> {
- // 本地扫货为多 worker 并行,统一用全局 SWEEP_CANCEL;无需逐任务 abort。
- info!("[stop_all_processes] 取消本地扫货并尽力通知后端停止选品/扫货");
- buff_sweep_runner::request_cancel();
- if let (Some(sid), Some(mid)) = (args.session_id, args.machine_code) {
- let s = sid.trim();
- let m = mid.trim();
- if !s.is_empty() && !m.is_empty() {
- match buff_secure_selection_stop_decrypt(s, m).await {
- Ok(_) => info!("[stop_all_processes] 已通知后端 selection-stop"),
- Err(e) => warn!("[stop_all_processes] 通知后端 selection-stop 失败(可忽略): {}", e),
- }
- match buff_secure_sweep_stop_decrypt(s, m).await {
- Ok(_) => info!("[stop_all_processes] 已通知后端 sweep-stop"),
- Err(e) => warn!("[stop_all_processes] 通知后端 sweep-stop 失败(可忽略): {}", e),
- }
- }
- }
- Ok(())
- }
- #[derive(serde::Serialize, serde::Deserialize)]
- pub struct LoginRequest {
- pub username: String,
- pub password: String,
- pub machine_code: String,
- }
- #[derive(serde::Serialize, serde::Deserialize)]
- pub struct LoginResponse {
- pub user_id: i32,
- pub username: String,
- pub expire_time: String,
- pub days_remaining: i32,
- pub email: Option<String>,
- pub session_id: Option<String>,
- }
- /// 校验服务端响应签名(response_signature)
- ///
- /// 按文档约定:
- /// sign_string = "{response_timestamp}:{response_nonce}:{encrypted_data}:{key_version}"
- fn verify_response_signature(
- response_json: &serde_json::Value,
- encrypted_data: &str,
- key_version: i32,
- ) -> Result<(), String> {
- use std::time::{SystemTime, UNIX_EPOCH};
- let response_timestamp = response_json
- .get("response_timestamp")
- .and_then(|v| v.as_i64())
- .ok_or_else(|| "响应缺少 response_timestamp 字段".to_string())?;
- let response_nonce = response_json
- .get("response_nonce")
- .and_then(|v| v.as_str())
- .ok_or_else(|| "响应缺少 response_nonce 字段".to_string())?;
- let response_signature = response_json
- .get("response_signature")
- .and_then(|v| v.as_str())
- .ok_or_else(|| "响应缺少 response_signature 字段".to_string())?;
- // 时间戳容忍范围 ±5 分钟
- let now = SystemTime::now()
- .duration_since(UNIX_EPOCH)
- .map_err(|e| format!("Time error: {}", e))?
- .as_secs() as i64;
- let diff = (now - response_timestamp).abs();
- if diff > 300 {
- return Err("响应时间戳超出允许范围,请重试登录".to_string());
- }
- // 构建签名字符串,服务端用 SERVER_SECRET 签名,客户端用相同密钥验签
- let sign_string = format!(
- "{}:{}:{}:{}",
- response_timestamp, response_nonce, encrypted_data, key_version
- );
- let server_secret = get_server_secret_key();
- let expected_signature = generate_hmac_signature(&sign_string, server_secret.as_str())
- .map_err(|e| format!("生成响应签名失败: {}", e))?;
- if expected_signature != response_signature {
- return Err("响应签名验证失败,可能存在中间人攻击风险".to_string());
- }
- Ok(())
- }
- // AES 加解密使用 crypto 模块的 aes_encrypt / aes_decrypt
- #[tauri::command]
- pub async fn secure_user_login(request: LoginRequest) -> Result<LoginResponse, String> {
- use base64::{Engine as _, engine::general_purpose};
- use serde_json;
- use std::time::{SystemTime, UNIX_EPOCH};
- // 1. 从Rust后端分段存储中重组RSA公钥
- let public_key_pem = retrieve_rsa_key_segments()?;
- // 2. 生成随机AES密钥
- let aes_key = (0..32).map(|_| rand::random::<u8>()).collect::<Vec<u8>>();
- // 3. 生成时间戳和 nonce(需在加密请求体前生成,供 request_timestamp 与签名使用)
- let timestamp = SystemTime::now()
- .duration_since(UNIX_EPOCH)
- .map_err(|e| format!("Time error: {}", e))?
- .as_secs() as i64;
- let nonce: String = (0..32u8).map(|_| rand::random::<u8>()).map(|b| format!("{:02x}", b)).collect();
- // 4. AES加密请求参数(包含原始机器特征 + 业务时间戳 + 软件标识,供多软件共用用户表)
- let config = AppConfig::load().map_err(|e| format!("加载配置失败: {}", e))?;
- let request_body_plain = serde_json::json!({
- "username": request.username,
- "password": request.password,
- "raw_machine_info": request.machine_code,
- "request_timestamp": timestamp,
- "software_code": config.api.software_code,
- });
- let request_json = request_body_plain.to_string();
- let encrypted_params = aes_encrypt(request_json.as_bytes(), &aes_key)?;
- let encrypted_params_base64 = general_purpose::STANDARD.encode(&encrypted_params);
- // 5. RSA加密AES密钥
- let public_key_parsed = RsaPublicKey::from_public_key_pem(&public_key_pem)
- .map_err(|e| format!("Failed to parse public key: {}", e))?;
- let mut rng = OsRng;
- // 使用OAEP填充,与后端保持一致
- let padding = Oaep::new::<sha2::Sha256>();
- let encrypted_aes_key = public_key_parsed.encrypt(&mut rng, padding, &aes_key)
- .map_err(|e| format!("Failed to encrypt AES key: {}", e))?;
- let encrypted_aes_key_base64 = general_purpose::STANDARD.encode(&encrypted_aes_key);
- // 6. 获取RSA密钥版本
- let key_version = get_stored_key_version().ok_or_else(|| "RSA公钥版本不存在".to_string())?;
- // 7. 生成签名
- let signature_data = format!("{}:{}:{}:{}:{}", timestamp, nonce, encrypted_params_base64, encrypted_aes_key_base64, key_version);
- type HmacSha256 = Hmac<Sha256>;
- let mut mac = HmacSha256::new_from_slice(get_secret_key().as_bytes())
- .map_err(|e| format!("HMAC init failed: {}", e))?;
- mac.update(signature_data.as_bytes());
- let signature_bytes = mac.finalize().into_bytes();
- let signature = general_purpose::STANDARD.encode(&signature_bytes);
- // 8. 发送HTTP请求
- let client = get_http_client();
- let api_url = format!("{}/api/v1/auth/login", get_api_base_url()?);
- let request_body = serde_json::json!({
- "encrypted_params": encrypted_params_base64,
- "aes_key": encrypted_aes_key_base64,
- "key_version": key_version,
- "timestamp": timestamp,
- "nonce": nonce,
- "signature": signature
- });
- let response = client
- .post(api_url)
- .json(&request_body)
- .send()
- .await
- .map_err(|e| format!("HTTP request failed: {}", e))?;
- if !response.status().is_success() {
- return Err(format!("HTTP request failed with status: {}", response.status()));
- }
- let response_json: serde_json::Value = response.json()
- .await
- .map_err(|e| format!("Failed to parse response: {}", e))?;
- // 检查响应状态
- if let Some(code) = response_json.get("code") {
- if code != 200 {
- let msg = response_json.get("msg")
- .and_then(|m| m.as_str())
- .unwrap_or("Unknown error");
- return Err(format!("API error: {}", msg));
- }
- }
- // 8. 解密前先校验响应签名,然后解密响应数据
- if let Some(encrypted_data) = response_json.get("encrypted_data").and_then(|d| d.as_str()) {
- // 先验证响应签名
- verify_response_signature(&response_json, encrypted_data, key_version)?;
- let encrypted_bytes = general_purpose::STANDARD
- .decode(encrypted_data)
- .map_err(|e| format!("Failed to decode encrypted data: {}", e))?;
- // 从响应数据中提取IV(前16字节,与公告API保持一致)
- let response_iv = &encrypted_bytes[0..16];
- let encrypted_payload = &encrypted_bytes[16..];
- // AES解密payload部分
- let mut cipher = Ctr64BE::<Aes256>::new_from_slices(&aes_key, response_iv)
- .map_err(|e| format!("AES cipher init failed: {}", e))?;
- let mut decrypted_data = encrypted_payload.to_vec();
- cipher.apply_keystream(&mut decrypted_data);
- let decrypted_json: serde_json::Value = serde_json::from_slice(&decrypted_data)
- .map_err(|e| format!("Failed to parse decrypted data: {}", e))?;
- // 检查响应状态码
- let code = decrypted_json.get("code")
- .and_then(|c| c.as_i64())
- .unwrap_or(500) as i32;
- if code != 200 {
- let msg = decrypted_json.get("msg")
- .and_then(|m| m.as_str())
- .unwrap_or("Unknown error");
- // 401/403 直接返回后端友好提示,便于登录页展示「卡密已过期」等
- if code == 401 || code == 403 {
- return Err(msg.to_string());
- }
- return Err(format!("API error {}: {}", code, msg));
- }
- // 提取用户信息:兼容后端在 root 或 data 子对象中返回用户字段
- let payload = if let Some(d) = decrypted_json.get("data") {
- d
- } else {
- &decrypted_json
- };
- let user_id = payload.get("user_id")
- .and_then(|id| id.as_i64())
- .unwrap_or(0) as i32;
- let username = payload.get("username")
- .and_then(|u| u.as_str())
- .unwrap_or("")
- .to_string();
- let expire_time = payload.get("expire_time")
- .and_then(|et| et.as_str())
- .unwrap_or("")
- .to_string();
- let days_remaining = payload.get("days_remaining")
- .and_then(|d| d.as_i64())
- .unwrap_or(0) as i32;
- let email = payload.get("email")
- .and_then(|e| e.as_str())
- .map(|s| s.to_string());
- let session_id = payload.get("session_id")
- .and_then(|s| s.as_str())
- .map(|s| s.to_string());
- Ok(LoginResponse {
- user_id,
- username,
- expire_time,
- days_remaining,
- email,
- session_id,
- })
- } else {
- Err("No encrypted_data in response".to_string())
- }
- }
- #[derive(serde::Serialize, serde::Deserialize)]
- pub struct RegisterRequest {
- pub username: String,
- pub password: String,
- pub email: String,
- pub card_code: String,
- }
- #[derive(serde::Serialize, serde::Deserialize)]
- pub struct RegisterResponse {
- pub user_id: i32,
- pub username: String,
- pub message: String,
- }
- #[tauri::command]
- pub async fn secure_user_register(request: RegisterRequest) -> Result<RegisterResponse, String> {
- use base64::{Engine as _, engine::general_purpose};
- use serde_json;
- use std::time::{SystemTime, UNIX_EPOCH};
- // 1. 从Rust后端分段存储中重组RSA公钥
- let public_key_pem = retrieve_rsa_key_segments()?;
- // 2. 生成随机AES密钥
- let aes_key = (0..32).map(|_| rand::random::<u8>()).collect::<Vec<u8>>();
- // 3. AES加密请求参数(含 software_code,供多软件共用用户表)
- let app_config = AppConfig::load().map_err(|e| format!("加载配置失败: {}", e))?;
- let request_data = serde_json::json!({
- "username": request.username,
- "password": request.password,
- "email": request.email,
- "card_code": request.card_code,
- "software_code": app_config.api.software_code,
- });
- let request_json = request_data.to_string();
- let encrypted_params = aes_encrypt(&request_json.as_bytes(), &aes_key)?;
- let encrypted_params_base64 = general_purpose::STANDARD.encode(&encrypted_params);
- // 4. RSA加密AES密钥
- let public_key_parsed = RsaPublicKey::from_public_key_pem(&public_key_pem)
- .map_err(|e| format!("Failed to parse public key: {}", e))?;
- let mut rng = OsRng;
- // 使用OAEP填充,与后端保持一致
- let padding = Oaep::new::<sha2::Sha256>();
- let encrypted_aes_key = public_key_parsed.encrypt(&mut rng, padding, &aes_key)
- .map_err(|e| format!("Failed to encrypt AES key: {}", e))?;
- let encrypted_aes_key_base64 = general_purpose::STANDARD.encode(&encrypted_aes_key);
- // 5. 生成时间戳和nonce
- let timestamp = SystemTime::now()
- .duration_since(UNIX_EPOCH)
- .map_err(|e| format!("Time error: {}", e))?
- .as_secs() as i64;
- let nonce: String = (0..32u8).map(|_| rand::random::<u8>()).map(|b| format!("{:02x}", b)).collect();
- // 6. 获取RSA密钥版本
- let key_version = get_stored_key_version().ok_or_else(|| "RSA公钥版本不存在".to_string())?;
- // 7. 生成签名
- let signature_data = format!("{}:{}:{}:{}:{}", timestamp, nonce, encrypted_params_base64, encrypted_aes_key_base64, key_version);
- type HmacSha256 = Hmac<Sha256>;
- let mut mac = HmacSha256::new_from_slice(get_secret_key().as_bytes())
- .map_err(|e| format!("HMAC init failed: {}", e))?;
- mac.update(signature_data.as_bytes());
- let signature_bytes = mac.finalize().into_bytes();
- let signature = general_purpose::STANDARD.encode(&signature_bytes);
- // 8. 发送HTTP请求
- let client = get_http_client();
- let api_url = format!("{}/api/v1/auth/register", get_api_base_url()?);
- let request_body = serde_json::json!({
- "encrypted_params": encrypted_params_base64,
- "aes_key": encrypted_aes_key_base64,
- "key_version": key_version,
- "timestamp": timestamp,
- "nonce": nonce,
- "signature": signature
- });
- let response = client
- .post(api_url)
- .json(&request_body)
- .send()
- .await
- .map_err(|e| format!("HTTP request failed: {}", e))?;
- if !response.status().is_success() {
- return Err(format!("HTTP request failed with status: {}", response.status()));
- }
- let response_json: serde_json::Value = response.json()
- .await
- .map_err(|e| format!("Failed to parse response: {}", e))?;
- // 检查响应状态
- if let Some(code) = response_json.get("code") {
- if code != 200 {
- let msg = response_json.get("msg")
- .and_then(|m| m.as_str())
- .unwrap_or("Unknown error");
- return Err(format!("API error: {}", msg));
- }
- }
- // 8. 解密前先校验响应签名,然后解密响应数据
- if let Some(encrypted_data) = response_json.get("encrypted_data").and_then(|d| d.as_str()) {
- verify_response_signature(&response_json, encrypted_data, key_version)?;
- let encrypted_bytes = general_purpose::STANDARD
- .decode(encrypted_data)
- .map_err(|e| format!("Failed to decode encrypted data: {}", e))?;
- // 从响应数据中提取IV(前16字节,与公告API保持一致)
- let response_iv = &encrypted_bytes[0..16];
- let encrypted_payload = &encrypted_bytes[16..];
- // AES解密payload部分(与后端 IV + AES-CTR 约定一致)
- let mut cipher = Ctr64BE::<Aes256>::new_from_slices(&aes_key, response_iv)
- .map_err(|e| format!("AES cipher init failed: {}", e))?;
- let mut decrypted_data = encrypted_payload.to_vec();
- cipher.apply_keystream(&mut decrypted_data);
- let decrypted_json: serde_json::Value = serde_json::from_slice(&decrypted_data)
- .map_err(|e| format!("Failed to parse decrypted data: {}", e))?;
- // 检查响应状态码
- let code = decrypted_json.get("code")
- .and_then(|c| c.as_i64())
- .unwrap_or(500) as i32;
- if code != 200 {
- let msg = decrypted_json.get("msg")
- .and_then(|m| m.as_str())
- .unwrap_or("Unknown error");
- return Err(format!("API error {}: {}", code, msg));
- }
- // 提取注册结果(后端返回格式为 { "code", "msg", "data": { "user_id", "username", ... } })
- let data_obj = decrypted_json.get("data");
- let user_id = data_obj
- .and_then(|d| d.get("user_id").and_then(|id| id.as_i64()))
- .or_else(|| decrypted_json.get("user_id").and_then(|id| id.as_i64()))
- .unwrap_or(0) as i32;
- let username = data_obj
- .and_then(|d| d.get("username").and_then(|u| u.as_str()))
- .or_else(|| decrypted_json.get("username").and_then(|u| u.as_str()))
- .unwrap_or("")
- .to_string();
- let message = decrypted_json.get("msg")
- .and_then(|m| m.as_str())
- .or_else(|| decrypted_json.get("message").and_then(|m| m.as_str()))
- .unwrap_or("注册成功")
- .to_string();
- Ok(RegisterResponse {
- user_id,
- username,
- message,
- })
- } else {
- Err("No encrypted_data in response".to_string())
- }
- }
- /// 找回密码请求(明文 POST,仅用户名和邮箱)
- #[derive(serde::Serialize, serde::Deserialize)]
- pub struct ForgotPasswordRequest {
- pub username: String,
- pub email: String,
- }
- #[tauri::command]
- pub async fn forgot_password_request(username: String, email: String) -> Result<String, String> {
- log::info!("[Rust forgot_password] Step 1 - 收到请求: username='{}', email='{}'", username, email);
- let api_url = format!("{}/api/v1/auth/forgot-password", get_api_base_url()?);
- let client = get_http_client();
- let body = serde_json::json!({ "username": username.trim(), "email": email.trim() });
- log::info!("[Rust forgot_password] Step 2 - 发送请求体: {}", body);
- let response = client
- .post(&api_url)
- .json(&body)
- .send()
- .await
- .map_err(|e| {
- log::error!("[Rust forgot_password] Step 3 - 请求失败: {}", e);
- format!("请求失败: {}", e)
- })?;
- log::info!("[Rust forgot_password] Step 4 - 收到响应状态: {}", response.status());
- let status = response.status();
- let text = response.text().await.map_err(|e| format!("读取响应失败: {}", e))?;
- log::info!("[Rust forgot_password] Step 5 - 响应内容: {}", text);
- if !status.is_success() {
- return Err(format!("请求失败: {}", text));
- }
- let json: serde_json::Value = serde_json::from_str(&text).unwrap_or(serde_json::json!({}));
- let msg = json.get("msg").and_then(|m| m.as_str()).unwrap_or("提交成功,请查收邮箱");
- log::info!("[Rust forgot_password] Step 6 - 返回消息: {}", msg);
- Ok(msg.to_string())
- }
- #[derive(serde::Serialize, serde::Deserialize)]
- pub struct UnbindRequest {
- pub username: String,
- pub password: String,
- /// 与登录时一致的 raw 机器信息(前端从 authStore.machineCode 传入,保证与后端存储的 machine_code 一致)
- #[serde(default)]
- pub machine_code: Option<String>,
- }
- #[derive(serde::Serialize, serde::Deserialize)]
- pub struct UnbindResponse {
- pub message: String,
- pub success: bool,
- }
- #[tauri::command]
- pub async fn secure_user_unbind(request: UnbindRequest) -> Result<UnbindResponse, String> {
- use base64::{Engine as _, engine::general_purpose};
- use serde_json;
- use std::time::{SystemTime, UNIX_EPOCH};
- // 1. 从Rust后端分段存储中重组RSA公钥
- let public_key_pem = retrieve_rsa_key_segments()?;
- // 2. 生成随机AES密钥
- let aes_key = (0..32).map(|_| rand::random::<u8>()).collect::<Vec<u8>>();
- // 3. 使用与登录时一致的 raw_machine_info:优先用前端传入的 machine_code(来自 authStore),否则本机实时生成
- let raw_machine_info = request
- .machine_code
- .filter(|s| !s.is_empty())
- .unwrap_or_else(|| generate_machine_code().unwrap_or_else(|_| String::new()));
- // 4. AES加密请求参数(含 username、password、raw_machine_info)
- let request_data = serde_json::json!({
- "username": request.username,
- "password": request.password,
- "raw_machine_info": raw_machine_info
- });
- let request_json = request_data.to_string();
- let encrypted_params = aes_encrypt(&request_json.as_bytes(), &aes_key)?;
- let encrypted_params_base64 = general_purpose::STANDARD.encode(&encrypted_params);
- // 4. RSA加密AES密钥
- let public_key_parsed = RsaPublicKey::from_public_key_pem(&public_key_pem)
- .map_err(|e| format!("Failed to parse public key: {}", e))?;
- let mut rng = OsRng;
- // 使用OAEP填充,与后端保持一致
- let padding = Oaep::new::<sha2::Sha256>();
- let encrypted_aes_key = public_key_parsed.encrypt(&mut rng, padding, &aes_key)
- .map_err(|e| format!("Failed to encrypt AES key: {}", e))?;
- let encrypted_aes_key_base64 = general_purpose::STANDARD.encode(&encrypted_aes_key);
- // 5. 生成时间戳和nonce
- let timestamp = SystemTime::now()
- .duration_since(UNIX_EPOCH)
- .map_err(|e| format!("Time error: {}", e))?
- .as_secs() as i64;
- let nonce: String = (0..32u8).map(|_| rand::random::<u8>()).map(|b| format!("{:02x}", b)).collect();
- // 6. 获取RSA密钥版本
- let key_version = get_stored_key_version().ok_or_else(|| "RSA公钥版本不存在".to_string())?;
- // 7. 生成签名
- let signature_data = format!("{}:{}:{}:{}:{}", timestamp, nonce, encrypted_params_base64, encrypted_aes_key_base64, key_version);
- type HmacSha256 = Hmac<Sha256>;
- let mut mac = HmacSha256::new_from_slice(get_secret_key().as_bytes())
- .map_err(|e| format!("HMAC init failed: {}", e))?;
- mac.update(signature_data.as_bytes());
- let signature_bytes = mac.finalize().into_bytes();
- let signature = general_purpose::STANDARD.encode(&signature_bytes);
- // 8. 发送HTTP请求
- let client = get_http_client();
- let api_url = format!("{}/api/v1/auth/unbind", get_api_base_url()?);
- let request_body = serde_json::json!({
- "encrypted_params": encrypted_params_base64,
- "aes_key": encrypted_aes_key_base64,
- "key_version": key_version,
- "timestamp": timestamp,
- "nonce": nonce,
- "signature": signature
- });
- let response = client
- .post(api_url)
- .json(&request_body)
- .send()
- .await
- .map_err(|e| format!("HTTP request failed: {}", e))?;
- if !response.status().is_success() {
- return Err(format!("HTTP request failed with status: {}", response.status()));
- }
- let response_json: serde_json::Value = response.json()
- .await
- .map_err(|e| format!("Failed to parse response: {}", e))?;
- // 检查响应状态
- if let Some(code) = response_json.get("code") {
- if code != 200 {
- let msg = response_json.get("msg")
- .and_then(|m| m.as_str())
- .unwrap_or("Unknown error");
- return Err(format!("API error: {}", msg));
- }
- }
- // 8. 解密前先校验响应签名,然后解密响应数据
- if let Some(encrypted_data) = response_json.get("encrypted_data").and_then(|d| d.as_str()) {
- verify_response_signature(&response_json, encrypted_data, key_version)?;
- let encrypted_bytes = general_purpose::STANDARD
- .decode(encrypted_data)
- .map_err(|e| format!("Failed to decode encrypted data: {}", e))?;
- if encrypted_bytes.len() < 16 {
- return Err("Encrypted data too short".to_string());
- }
- // 提取前16字节作为IV,后面的作为加密数据(与后端create_secure_response保持一致)
- let response_iv = &encrypted_bytes[0..16];
- let encrypted_payload = &encrypted_bytes[16..];
- // AES解密响应数据
- let mut cipher = Ctr64BE::<Aes256>::new_from_slices(&aes_key, response_iv)
- .map_err(|e| format!("AES cipher init failed: {}", e))?;
- let mut decrypted_data = encrypted_payload.to_vec();
- cipher.apply_keystream(&mut decrypted_data);
- let decrypted_json: serde_json::Value = serde_json::from_slice(&decrypted_data)
- .map_err(|e| format!("Failed to parse decrypted data: {}", e))?;
- // 检查响应状态码
- let code = decrypted_json.get("code")
- .and_then(|c| c.as_i64())
- .unwrap_or(500) as i32;
- if code != 200 {
- let msg = decrypted_json.get("msg")
- .and_then(|m| m.as_str())
- .unwrap_or("Unknown error");
- return Err(format!("API error {}: {}", code, msg));
- }
- // 提取解绑结果
- let message = decrypted_json.get("message")
- .and_then(|m| m.as_str())
- .unwrap_or("解绑成功")
- .to_string();
- let success = decrypted_json.get("success")
- .and_then(|s| s.as_bool())
- .unwrap_or(true);
- Ok(UnbindResponse {
- message,
- success,
- })
- } else {
- Err("No encrypted_data in response".to_string())
- }
- }
- #[derive(serde::Serialize, serde::Deserialize)]
- pub struct ChangePasswordRequest {
- pub username: String,
- pub old_password: String,
- pub new_password: String,
- }
- #[derive(serde::Serialize, serde::Deserialize)]
- pub struct ChangePasswordResponse {
- pub message: String,
- pub success: bool,
- }
- #[tauri::command]
- pub async fn secure_user_change_password(request: ChangePasswordRequest) -> Result<ChangePasswordResponse, String> {
- use base64::{Engine as _, engine::general_purpose};
- use serde_json;
- use std::time::{SystemTime, UNIX_EPOCH};
- let public_key_pem = retrieve_rsa_key_segments()?;
- let aes_key = (0..32).map(|_| rand::random::<u8>()).collect::<Vec<u8>>();
- let request_data = serde_json::json!({
- "username": request.username,
- "old_password": request.old_password,
- "new_password": request.new_password
- });
- let request_json = request_data.to_string();
- let encrypted_params = aes_encrypt(&request_json.as_bytes(), &aes_key)?;
- let encrypted_params_base64 = general_purpose::STANDARD.encode(&encrypted_params);
- let public_key_parsed = RsaPublicKey::from_public_key_pem(&public_key_pem)
- .map_err(|e| format!("Failed to parse public key: {}", e))?;
- let mut rng = OsRng;
- let padding = Oaep::new::<sha2::Sha256>();
- let encrypted_aes_key = public_key_parsed.encrypt(&mut rng, padding, &aes_key)
- .map_err(|e| format!("Failed to encrypt AES key: {}", e))?;
- let encrypted_aes_key_base64 = general_purpose::STANDARD.encode(&encrypted_aes_key);
- let timestamp = SystemTime::now()
- .duration_since(UNIX_EPOCH)
- .map_err(|e| format!("Time error: {}", e))?
- .as_secs() as i64;
- let nonce: String = (0..32u8).map(|_| rand::random::<u8>()).map(|b| format!("{:02x}", b)).collect();
- let key_version = get_stored_key_version().ok_or_else(|| "RSA公钥版本不存在".to_string())?;
- let signature_data = format!("{}:{}:{}:{}:{}", timestamp, nonce, encrypted_params_base64, encrypted_aes_key_base64, key_version);
- type HmacSha256 = Hmac<Sha256>;
- let mut mac = HmacSha256::new_from_slice(get_secret_key().as_bytes())
- .map_err(|e| format!("HMAC init failed: {}", e))?;
- mac.update(signature_data.as_bytes());
- let signature_bytes = mac.finalize().into_bytes();
- let signature = general_purpose::STANDARD.encode(&signature_bytes);
- let client = get_http_client();
- let api_url = format!("{}/api/v1/auth/change-password", get_api_base_url()?);
- let request_body = serde_json::json!({
- "encrypted_params": encrypted_params_base64,
- "aes_key": encrypted_aes_key_base64,
- "key_version": key_version,
- "timestamp": timestamp,
- "nonce": nonce,
- "signature": signature
- });
- let response = client
- .post(api_url)
- .json(&request_body)
- .send()
- .await
- .map_err(|e| format!("HTTP request failed: {}", e))?;
- if !response.status().is_success() {
- return Err(format!("HTTP request failed with status: {}", response.status()));
- }
- let response_json: serde_json::Value = response.json()
- .await
- .map_err(|e| format!("Failed to parse response: {}", e))?;
- if let Some(code) = response_json.get("code") {
- if code != 200 {
- let msg = response_json.get("msg")
- .and_then(|m| m.as_str())
- .unwrap_or("Unknown error");
- return Err(format!("API error: {}", msg));
- }
- }
- if let Some(encrypted_data) = response_json.get("encrypted_data").and_then(|d| d.as_str()) {
- verify_response_signature(&response_json, encrypted_data, key_version)?;
- let encrypted_bytes = general_purpose::STANDARD
- .decode(encrypted_data)
- .map_err(|e| format!("Failed to decode encrypted data: {}", e))?;
- if encrypted_bytes.len() < 16 {
- return Err("Encrypted data too short".to_string());
- }
- let response_iv = &encrypted_bytes[0..16];
- let encrypted_payload = &encrypted_bytes[16..];
- let mut cipher = Ctr64BE::<Aes256>::new_from_slices(&aes_key, response_iv)
- .map_err(|e| format!("AES cipher init failed: {}", e))?;
- let mut decrypted_data = encrypted_payload.to_vec();
- cipher.apply_keystream(&mut decrypted_data);
- let decrypted_json: serde_json::Value = serde_json::from_slice(&decrypted_data)
- .map_err(|e| format!("Failed to parse decrypted data: {}", e))?;
- let code = decrypted_json.get("code")
- .and_then(|c| c.as_i64())
- .unwrap_or(500) as i32;
- if code != 200 {
- let msg = decrypted_json.get("msg")
- .and_then(|m| m.as_str())
- .unwrap_or("Unknown error");
- return Err(format!("API error {}: {}", code, msg));
- }
- let message = decrypted_json.get("msg")
- .and_then(|m| m.as_str())
- .unwrap_or("密码修改成功")
- .to_string();
- let success = code == 200;
- Ok(ChangePasswordResponse {
- message,
- success,
- })
- } else {
- Err("No encrypted_data in response".to_string())
- }
- }
- #[derive(serde::Serialize, serde::Deserialize)]
- pub struct RechargeRequest {
- pub username: String,
- pub password: String,
- pub card_code: String,
- }
- #[derive(serde::Serialize, serde::Deserialize)]
- pub struct RechargeResponse {
- pub message: String,
- pub success: bool,
- }
- #[tauri::command]
- pub async fn secure_user_recharge(request: RechargeRequest) -> Result<RechargeResponse, String> {
- use base64::{Engine as _, engine::general_purpose};
- use serde_json;
- use std::time::{SystemTime, UNIX_EPOCH};
- // 1. 从Rust后端分段存储中重组RSA公钥
- let public_key_pem = retrieve_rsa_key_segments()?;
- // 2. 生成随机AES密钥
- let aes_key = (0..32).map(|_| rand::random::<u8>()).collect::<Vec<u8>>();
- // 3. AES加密请求参数(含 software_code,供多软件共用用户表)
- let app_config = AppConfig::load().map_err(|e| format!("加载配置失败: {}", e))?;
- let request_data = serde_json::json!({
- "username": request.username,
- "password": request.password,
- "card_code": request.card_code,
- "software_code": app_config.api.software_code,
- });
- let request_json = request_data.to_string();
- let encrypted_params = aes_encrypt(&request_json.as_bytes(), &aes_key)?;
- let encrypted_params_base64 = general_purpose::STANDARD.encode(&encrypted_params);
- // 4. RSA加密AES密钥
- let public_key_parsed = RsaPublicKey::from_public_key_pem(&public_key_pem)
- .map_err(|e| format!("Failed to parse public key: {}", e))?;
- let mut rng = OsRng;
- // 使用OAEP填充,与后端保持一致
- let padding = Oaep::new::<sha2::Sha256>();
- let encrypted_aes_key = public_key_parsed.encrypt(&mut rng, padding, &aes_key)
- .map_err(|e| format!("Failed to encrypt AES key: {}", e))?;
- let encrypted_aes_key_base64 = general_purpose::STANDARD.encode(&encrypted_aes_key);
- // 5. 生成时间戳和nonce
- let timestamp = SystemTime::now()
- .duration_since(UNIX_EPOCH)
- .map_err(|e| format!("Time error: {}", e))?
- .as_secs() as i64;
- let nonce: String = (0..32u8).map(|_| rand::random::<u8>()).map(|b| format!("{:02x}", b)).collect();
- // 6. 获取RSA密钥版本
- let key_version = get_stored_key_version().ok_or_else(|| "RSA公钥版本不存在".to_string())?;
- // 7. 生成签名
- let signature_data = format!("{}:{}:{}:{}:{}", timestamp, nonce, encrypted_params_base64, encrypted_aes_key_base64, key_version);
- type HmacSha256 = Hmac<Sha256>;
- let mut mac = HmacSha256::new_from_slice(get_secret_key().as_bytes())
- .map_err(|e| format!("HMAC init failed: {}", e))?;
- mac.update(signature_data.as_bytes());
- let signature_bytes = mac.finalize().into_bytes();
- let signature = general_purpose::STANDARD.encode(&signature_bytes);
- // 8. 发送HTTP请求
- let client = get_http_client();
- let api_url = format!("{}/api/v1/auth/recharge", get_api_base_url()?);
- let request_body = serde_json::json!({
- "encrypted_params": encrypted_params_base64,
- "aes_key": encrypted_aes_key_base64,
- "key_version": key_version,
- "timestamp": timestamp,
- "nonce": nonce,
- "signature": signature
- });
- let response = client
- .post(api_url)
- .json(&request_body)
- .send()
- .await
- .map_err(|e| format!("HTTP request failed: {}", e))?;
- if !response.status().is_success() {
- return Err(format!("HTTP request failed with status: {}", response.status()));
- }
- let response_json: serde_json::Value = response.json()
- .await
- .map_err(|e| format!("Failed to parse response: {}", e))?;
- // 检查响应状态
- if let Some(code) = response_json.get("code") {
- if code != 200 {
- let msg = response_json.get("msg")
- .and_then(|m| m.as_str())
- .unwrap_or("Unknown error");
- return Err(format!("API error: {}", msg));
- }
- }
- // 8. 解密前先校验响应签名,然后解密响应数据
- if let Some(encrypted_data) = response_json.get("encrypted_data").and_then(|d| d.as_str()) {
- verify_response_signature(&response_json, encrypted_data, key_version)?;
- let encrypted_bytes = general_purpose::STANDARD
- .decode(encrypted_data)
- .map_err(|e| format!("Failed to decode encrypted data: {}", e))?;
- if encrypted_bytes.len() < 16 {
- return Err("Invalid encrypted data length".to_string());
- }
- // 提取前16字节作为IV,后面的作为加密数据(与后端create_secure_response保持一致)
- let response_iv = &encrypted_bytes[0..16];
- let encrypted_payload = &encrypted_bytes[16..];
- // AES解密响应数据
- let mut cipher = Ctr64BE::<Aes256>::new_from_slices(&aes_key, response_iv)
- .map_err(|e| format!("AES cipher init failed: {}", e))?;
- let mut decrypted_data = encrypted_payload.to_vec();
- cipher.apply_keystream(&mut decrypted_data);
- let decrypted_json: serde_json::Value = serde_json::from_slice(&decrypted_data)
- .map_err(|e| format!("Failed to parse decrypted data: {}", e))?;
- // 检查响应状态码
- let code = decrypted_json.get("code")
- .and_then(|c| c.as_i64())
- .unwrap_or(500) as i32;
- if code != 200 {
- let msg = decrypted_json.get("msg")
- .and_then(|m| m.as_str())
- .unwrap_or("Unknown error");
- return Err(format!("API error {}: {}", code, msg));
- }
- // 提取充值结果
- let message = decrypted_json.get("message")
- .and_then(|m| m.as_str())
- .unwrap_or("充值成功")
- .to_string();
- let success = decrypted_json.get("success")
- .and_then(|s| s.as_bool())
- .unwrap_or(true);
- Ok(RechargeResponse {
- message,
- success,
- })
- } else {
- Err("No encrypted_data in response".to_string())
- }
- }
- /// 安全获取用户信息(单次):AES+RSA 加密请求,验证并解密响应
- async fn secure_get_user_profile_call_inner(
- api_url: &str,
- session_id: &str,
- machine_code: &str,
- ) -> Result<serde_json::Value, String> {
- use std::time::{SystemTime, UNIX_EPOCH};
- let public_key_pem = retrieve_rsa_key_segments()?;
- let key_version = get_stored_key_version().ok_or_else(|| "RSA公钥版本不存在".to_string())?;
- let aes_key: Vec<u8> = (0..32).map(|_| rand::random::<u8>()).collect();
- let request_data = serde_json::json!({
- "session_id": session_id,
- "machine_code": machine_code
- });
- let request_json = request_data.to_string();
- let encrypted_params_bytes = aes_encrypt(request_json.as_bytes(), &aes_key)
- .map_err(|e| format!("AES加密失败: {}", e))?;
- let encrypted_params_base64 = general_purpose::STANDARD.encode(&encrypted_params_bytes);
- let encrypted_aes_key_base64 = crypto_rsa_encrypt_aes_key(&aes_key, &public_key_pem)
- .map_err(|e| format!("RSA加密AES密钥失败: {}", e))?;
- let timestamp = SystemTime::now()
- .duration_since(UNIX_EPOCH)
- .map_err(|e| format!("Time error: {}", e))?
- .as_secs() as i64;
- let nonce: String = (0..32u8).map(|_| rand::random::<u8>()).map(|b| format!("{:02x}", b)).collect();
- let signature_data = format!(
- "{}:{}:{}:{}:{}",
- timestamp, nonce, encrypted_params_base64, encrypted_aes_key_base64, key_version
- );
- let signature = generate_hmac_signature(&signature_data, &get_secret_key())
- .map_err(|e| format!("HMAC签名失败: {}", e))?;
- let client = get_http_client();
- let url = format!("{}/api/v1/user/profile", api_url.trim_end_matches('/'));
- let request_body = serde_json::json!({
- "encrypted_params": encrypted_params_base64,
- "aes_key": encrypted_aes_key_base64,
- "key_version": key_version,
- "timestamp": timestamp,
- "nonce": nonce,
- "signature": signature
- });
- let response = client
- .post(&url)
- .json(&request_body)
- .send()
- .await
- .map_err(|e| format!("请求失败: {}", e))?;
- if !response.status().is_success() {
- return Err(format!("请求失败: {}", response.status()));
- }
- let response_json: serde_json::Value = response
- .json()
- .await
- .map_err(|e| format!("解析响应失败: {}", e))?;
- if let Some(code) = response_json.get("code") {
- if code != 200 {
- let msg = response_json
- .get("msg")
- .and_then(|m| m.as_str())
- .unwrap_or("未知错误");
- return Err(format!("API error: {}", msg));
- }
- }
- let encrypted_data = response_json
- .get("encrypted_data")
- .and_then(|d| d.as_str())
- .ok_or("响应中缺少 encrypted_data")?;
- verify_response_signature(&response_json, encrypted_data, key_version)?;
- let encrypted_bytes = general_purpose::STANDARD
- .decode(encrypted_data)
- .map_err(|e| format!("Base64解码失败: {}", e))?;
- if encrypted_bytes.len() < 16 {
- return Err("加密数据长度不足".to_string());
- }
- let decrypted_bytes = aes_decrypt(&encrypted_bytes, &aes_key)
- .map_err(|e| format!("AES解密失败: {}", e))?;
- let decrypted_json: serde_json::Value = serde_json::from_slice(&decrypted_bytes)
- .map_err(|e| format!("解析解密数据失败: {}", e))?;
- let code = decrypted_json.get("code").and_then(|c| c.as_i64()).unwrap_or(500) as i32;
- if code != 200 {
- let msg = decrypted_json
- .get("msg")
- .and_then(|m| m.as_str())
- .unwrap_or("未知错误");
- return Err(format!("{}: {}", code, msg));
- }
- Ok(decrypted_json)
- }
- /// 安全获取用户信息:AES+RSA 加密请求,验证并解密响应
- pub async fn secure_get_user_profile_call(
- api_url: &str,
- session_id: &str,
- machine_code: &str,
- ) -> Result<serde_json::Value, String> {
- secure_get_user_profile_call_inner(api_url, session_id, machine_code).await
- }
- /// 安全密钥同步(单次):POST /api/v1/keys/sync(已登录),返回解密后的 public_key、key_version
- async fn secure_keys_sync_call_inner(
- api_url: &str,
- session_id: &str,
- machine_code: &str,
- ) -> Result<serde_json::Value, String> {
- use std::time::{SystemTime, UNIX_EPOCH};
- let public_key_pem = retrieve_rsa_key_segments()?;
- let key_version = get_stored_key_version().ok_or_else(|| "RSA公钥版本不存在".to_string())?;
- let aes_key: Vec<u8> = (0..32).map(|_| rand::random::<u8>()).collect();
- let request_data = serde_json::json!({
- "session_id": session_id,
- "machine_code": machine_code
- });
- let request_json = request_data.to_string();
- let encrypted_params_bytes = aes_encrypt(request_json.as_bytes(), &aes_key)
- .map_err(|e| format!("AES加密失败: {}", e))?;
- let encrypted_params_base64 = general_purpose::STANDARD.encode(&encrypted_params_bytes);
- let encrypted_aes_key_base64 = crypto_rsa_encrypt_aes_key(&aes_key, &public_key_pem)
- .map_err(|e| format!("RSA加密AES密钥失败: {}", e))?;
- let timestamp = SystemTime::now()
- .duration_since(UNIX_EPOCH)
- .map_err(|e| format!("Time error: {}", e))?
- .as_secs() as i64;
- let nonce: String = (0..32u8).map(|_| rand::random::<u8>()).map(|b| format!("{:02x}", b)).collect();
- let signature_data = format!(
- "{}:{}:{}:{}:{}",
- timestamp, nonce, encrypted_params_base64, encrypted_aes_key_base64, key_version
- );
- let signature = generate_hmac_signature(&signature_data, &get_secret_key())
- .map_err(|e| format!("HMAC签名失败: {}", e))?;
- let client = get_http_client();
- let url = format!("{}/api/v1/keys/sync", api_url.trim_end_matches('/'));
- let request_body = serde_json::json!({
- "encrypted_params": encrypted_params_base64,
- "aes_key": encrypted_aes_key_base64,
- "key_version": key_version,
- "timestamp": timestamp,
- "nonce": nonce,
- "signature": signature
- });
- let response = client
- .post(&url)
- .json(&request_body)
- .send()
- .await
- .map_err(|e| format!("密钥同步请求失败: {}", e))?;
- if !response.status().is_success() {
- return Err(format!("密钥同步响应错误: {}", response.status()));
- }
- let response_json: serde_json::Value = response
- .json()
- .await
- .map_err(|e| format!("解析密钥同步响应失败: {}", e))?;
- if let Some(code) = response_json.get("code") {
- if code != 200 {
- let msg = response_json
- .get("msg")
- .and_then(|m| m.as_str())
- .unwrap_or("未知错误");
- return Err(format!("API error: {}", msg));
- }
- }
- let encrypted_data = response_json
- .get("encrypted_data")
- .and_then(|d| d.as_str())
- .ok_or("响应中缺少 encrypted_data")?;
- verify_response_signature(&response_json, encrypted_data, key_version)?;
- let encrypted_bytes = general_purpose::STANDARD
- .decode(encrypted_data)
- .map_err(|e| format!("Base64解码失败: {}", e))?;
- if encrypted_bytes.len() < 16 {
- return Err("加密数据长度不足".to_string());
- }
- let decrypted_bytes = aes_decrypt(&encrypted_bytes, &aes_key)
- .map_err(|e| format!("AES解密失败: {}", e))?;
- let decrypted_json: serde_json::Value = serde_json::from_slice(&decrypted_bytes)
- .map_err(|e| format!("解析解密数据失败: {}", e))?;
- let code = decrypted_json.get("code").and_then(|c| c.as_i64()).unwrap_or(500) as i32;
- if code != 200 {
- let msg = decrypted_json
- .get("msg")
- .and_then(|m| m.as_str())
- .unwrap_or("未知错误");
- return Err(format!("{}: {}", code, msg));
- }
- Ok(decrypted_json)
- }
- /// 安全密钥同步:POST /api/v1/keys/sync(已登录),返回解密后的 public_key、key_version
- pub async fn secure_keys_sync_call(
- api_url: &str,
- session_id: &str,
- machine_code: &str,
- ) -> Result<serde_json::Value, String> {
- secure_keys_sync_call_inner(api_url, session_id, machine_code).await
- }
- /// 安全密钥同步并更新本地存储(供 heartbeat 密钥同步任务调用)
- pub async fn secure_keys_sync_and_store(
- api_url: &str,
- session_id: &str,
- machine_code: &str,
- ) -> Result<bool, String> {
- let decrypted = secure_keys_sync_call(api_url, session_id, machine_code).await?;
- let public_key = decrypted
- .get("public_key")
- .and_then(|v| v.as_str())
- .ok_or("响应缺少 public_key")?;
- let key_version = decrypted
- .get("key_version")
- .and_then(|v| v.as_i64())
- .ok_or("响应缺少 key_version")? as i32;
- store_rsa_key_segments(public_key.to_string(), key_version);
- Ok(true)
- }
- /// 获取客户端版本号
- fn get_client_version() -> String {
- // 从 Cargo.toml 的 package.version 获取
- env!("CARGO_PKG_VERSION").to_string()
- }
- /// 构建并发送单次安全心跳请求,返回原始响应 JSON(解密前的)。
- /// 需要外部保证 nonce 唯一(用 generate_unique_nonce),并在请求前 record_nonce。
- async fn do_secure_heartbeat_request(
- api_url: &str,
- session_id: &str,
- machine_code: &str,
- nonce: &str,
- ) -> Result<serde_json::Value, String> {
- let public_key_pem = retrieve_rsa_key_segments()?;
- let key_version = get_stored_key_version().ok_or_else(|| "RSA公钥版本不存在".to_string())?;
- let config = AppConfig::load().map_err(|e| format!("加载配置失败: {}", e))?;
- let aes_key: Vec<u8> = (0..32).map(|_| rand::random::<u8>()).collect();
- let request_data = serde_json::json!({
- "session_id": session_id,
- "machine_code": machine_code,
- "software_code": config.api.software_code,
- "client_version": get_client_version(),
- "platform": "windows",
- "channel": "stable",
- });
- let request_json = request_data.to_string();
- let encrypted_params_bytes = aes_encrypt(request_json.as_bytes(), &aes_key)
- .map_err(|e| format!("AES加密失败: {}", e))?;
- let encrypted_params_base64 = general_purpose::STANDARD.encode(&encrypted_params_bytes);
- let encrypted_aes_key_base64 = crypto_rsa_encrypt_aes_key(&aes_key, &public_key_pem)
- .map_err(|e| format!("RSA加密AES密钥失败: {}", e))?;
- let timestamp = std::time::SystemTime::now()
- .duration_since(std::time::UNIX_EPOCH)
- .map_err(|e| format!("Time error: {}", e))?
- .as_secs() as i64;
- let signature_data = format!(
- "{}:{}:{}:{}:{}",
- timestamp, nonce, encrypted_params_base64, encrypted_aes_key_base64, key_version
- );
- let signature = generate_hmac_signature(&signature_data, &get_secret_key())
- .map_err(|e| format!("HMAC签名失败: {}", e))?;
- let client = get_http_client();
- let url = format!("{}/api/v1/heartbeat", api_url.trim_end_matches('/'));
- let request_body = serde_json::json!({
- "encrypted_params": encrypted_params_base64,
- "aes_key": encrypted_aes_key_base64,
- "key_version": key_version,
- "timestamp": timestamp,
- "nonce": nonce,
- "signature": signature
- });
- let response = client
- .post(&url)
- .json(&request_body)
- .send()
- .await
- .map_err(|e| format!("心跳请求失败: {}", e))?;
- let status = response.status();
- // 401/403:认证问题,原样返回给前端
- if status.as_u16() == 401 || status.as_u16() == 403 {
- let body = response.text().await.unwrap_or_else(|_| "{}".to_string());
- let body_json: serde_json::Value = serde_json::from_str(&body).unwrap_or(serde_json::json!({}));
- let code = body_json.get("code").and_then(|c| c.as_i64()).unwrap_or(status.as_u16() as i64) as i32;
- let msg = body_json.get("msg").and_then(|m| m.as_str()).unwrap_or(
- if status.as_u16() == 401 { "会话已过期,请重新登录" } else { "权限不足" }
- ).to_string();
- return Ok(serde_json::json!({ "code": code, "msg": msg, "data": {} }));
- }
- // 400 Bad Request:可能是 nonce 冲突(后端防重放拒绝),需要重试
- if status.as_u16() == 400 {
- return Err("400".to_string());
- }
- if !status.is_success() {
- return Err(format!("心跳响应错误: {}", status));
- }
- let response_json: serde_json::Value = response.json().await
- .map_err(|e| format!("解析心跳响应失败: {}", e))?;
- if let Some(code) = response_json.get("code") {
- if code != 200 {
- let msg = response_json.get("msg").and_then(|m| m.as_str()).unwrap_or("未知错误");
- return Err(format!("API error: {}", msg));
- }
- }
- let encrypted_data = response_json
- .get("encrypted_data")
- .and_then(|d| d.as_str())
- .ok_or("响应中缺少 encrypted_data")?;
- verify_response_signature(&response_json, encrypted_data, key_version)?;
- let encrypted_bytes = general_purpose::STANDARD.decode(encrypted_data)
- .map_err(|e| format!("Base64解码失败: {}", e))?;
- if encrypted_bytes.len() < 16 {
- return Err("加密数据长度不足".to_string());
- }
- let decrypted_bytes = aes_decrypt(&encrypted_bytes, &aes_key)
- .map_err(|e| format!("AES解密失败: {}", e))?;
- let decrypted_json: serde_json::Value = serde_json::from_slice(&decrypted_bytes)
- .map_err(|e| format!("解析解密数据失败: {}", e))?;
- Ok(decrypted_json)
- }
- /// 安全心跳调用(内部重试逻辑):
- /// 1. 生成唯一 nonce
- /// 2. 记录 nonce 后发请求
- /// 3. 收到 400(nonce 冲突)则重试一次(新 nonce + 重新签名)
- async fn secure_heartbeat_call_inner(
- api_url: &str,
- session_id: &str,
- machine_code: &str,
- ) -> Result<serde_json::Value, String> {
- // 第一次尝试
- let nonce1 = generate_unique_nonce();
- record_nonce(&nonce1);
- let result = do_secure_heartbeat_request(api_url, session_id, machine_code, &nonce1).await;
- match result {
- Ok(resp) => Ok(resp),
- Err(e) if e == "400" => {
- // Nonce 冲突,后端拒绝。生成新 nonce,重试一次
- warn!("[Heartbeat] 心跳被后端防重放拒绝(nonce冲突),重试...");
- let nonce2 = generate_unique_nonce();
- record_nonce(&nonce2);
- do_secure_heartbeat_request(api_url, session_id, machine_code, &nonce2).await
- }
- Err(e) => Err(e),
- }
- }
- /// 通用安全 POST:构建加密请求体、发送 POST、验证并解密响应。
- /// path 为相对路径,如 "/api/v1/buff/filter-options";request_data 需包含 session_id、machine_code 及业务字段。
- pub async fn secure_post_decrypt(
- api_url: &str,
- path: &str,
- request_data: &serde_json::Value,
- ) -> Result<serde_json::Value, String> {
- use std::time::{SystemTime, UNIX_EPOCH};
- let nonce1 = generate_unique_nonce();
- record_nonce(&nonce1);
- let result = do_secure_post_request(api_url, path, request_data, &nonce1).await;
- match result {
- Ok(resp) => Ok(resp),
- Err(e) if e == "400" => {
- // Nonce 冲突(防重放拒绝),生成新 nonce 重试一次
- warn!("[SecurePost] 接口 {} 被防重放拒绝,重试...", path);
- let nonce2 = generate_unique_nonce();
- record_nonce(&nonce2);
- do_secure_post_request(api_url, path, request_data, &nonce2).await
- }
- Err(e) => Err(e),
- }
- }
- async fn do_secure_post_request(
- api_url: &str,
- path: &str,
- request_data: &serde_json::Value,
- nonce: &str,
- ) -> Result<serde_json::Value, String> {
- use std::time::{SystemTime, UNIX_EPOCH};
- let public_key_pem = retrieve_rsa_key_segments()?;
- let key_version = get_stored_key_version().ok_or_else(|| "RSA公钥版本不存在".to_string())?;
- let aes_key: Vec<u8> = (0..32).map(|_| rand::random::<u8>()).collect();
- let request_json = request_data.to_string();
- let encrypted_params_bytes = aes_encrypt(request_json.as_bytes(), &aes_key)
- .map_err(|e| format!("AES加密失败: {}", e))?;
- let encrypted_params_base64 = general_purpose::STANDARD.encode(&encrypted_params_bytes);
- let encrypted_aes_key_base64 = crypto_rsa_encrypt_aes_key(&aes_key, &public_key_pem)
- .map_err(|e| format!("RSA加密AES密钥失败: {}", e))?;
- let timestamp = SystemTime::now()
- .duration_since(UNIX_EPOCH)
- .map_err(|e| format!("Time error: {}", e))?
- .as_secs() as i64;
- let signature_data = format!(
- "{}:{}:{}:{}:{}",
- timestamp, nonce, encrypted_params_base64, encrypted_aes_key_base64, key_version
- );
- let signature = generate_hmac_signature(&signature_data, &get_secret_key())
- .map_err(|e| format!("HMAC签名失败: {}", e))?;
- let client = get_http_client();
- let url = format!("{}{}", api_url.trim_end_matches('/'), path);
- let request_body = serde_json::json!({
- "encrypted_params": encrypted_params_base64,
- "aes_key": encrypted_aes_key_base64,
- "key_version": key_version,
- "timestamp": timestamp,
- "nonce": nonce,
- "signature": signature
- });
- let response = client
- .post(&url)
- .json(&request_body)
- .send()
- .await
- .map_err(|e| format!("请求失败: {}", e))?;
- let status = response.status();
- if status.as_u16() == 401 || status.as_u16() == 403 {
- let body = response.text().await.unwrap_or_else(|_| "{}".to_string());
- let body_json: serde_json::Value = serde_json::from_str(&body).unwrap_or(serde_json::json!({}));
- let code = body_json.get("code").and_then(|c| c.as_i64()).unwrap_or(status.as_u16() as i64) as i32;
- let msg = body_json.get("msg").and_then(|m| m.as_str()).unwrap_or(
- if status.as_u16() == 401 { "会话已过期,请重新登录" } else { "权限不足" }
- ).to_string();
- return Ok(serde_json::json!({ "code": code, "msg": msg, "data": serde_json::Value::Null }));
- }
- // 400 Bad Request:可能是 nonce 冲突(后端防重放拒绝)
- if status.as_u16() == 400 {
- return Err("400".to_string());
- }
- if !status.is_success() {
- return Err(format!("响应错误: {}", status));
- }
- let response_json: serde_json::Value = response.json().await
- .map_err(|e| format!("解析响应失败: {}", e))?;
- // 业务错误(如 403 今日次数上限)仍返回 Ok,由前端根据 code 展示 msg
- let encrypted_data = response_json.get("encrypted_data").and_then(|d| d.as_str());
- let decrypted_json = if let Some(encrypted_data) = encrypted_data {
- verify_response_signature(&response_json, encrypted_data, key_version)?;
- let encrypted_bytes = general_purpose::STANDARD
- .decode(encrypted_data)
- .map_err(|e| format!("Base64解码失败: {}", e))?;
- if encrypted_bytes.len() < 16 {
- return Err("加密数据长度不足".to_string());
- }
- let decrypted_bytes = aes_decrypt(&encrypted_bytes, &aes_key)
- .map_err(|e| format!("AES解密失败: {}", e))?;
- serde_json::from_slice::<serde_json::Value>(&decrypted_bytes)
- .map_err(|e| format!("解析解密数据失败: {}", e))?
- } else {
- response_json.clone()
- };
- Ok(decrypted_json)
- }
- /// 安全心跳调用:AES+RSA 加密请求,验证并解密响应(供 heartbeat 模块使用)
- pub async fn secure_heartbeat_call(
- api_url: &str,
- session_id: &str,
- machine_code: &str,
- ) -> Result<serde_json::Value, String> {
- secure_heartbeat_call_inner(api_url, session_id, machine_code).await
- }
- /// 供前端状态栏调用的安全心跳命令(与后台心跳同一套加密流程,避免 fetch_public_key 竞态和解密错误)
- #[tauri::command]
- pub async fn secure_heartbeat(session_id: String, machine_code: String) -> Result<String, String> {
- let config = AppConfig::load().map_err(|e| format!("加载配置失败: {}", e))?;
- let api_url = config.api.base_url.trim_end_matches('/');
- let decrypted = secure_heartbeat_call_inner(api_url, &session_id, &machine_code).await?;
- // 后端解密后为 { code, msg, config?, user_info?, session_info? },转为前端期望的 { code, msg, data: { config, session_info, user_info } }
- let code = decrypted.get("code").and_then(|c| c.as_i64()).unwrap_or(200) as i32;
- let msg = decrypted
- .get("msg")
- .and_then(|m| m.as_str())
- .unwrap_or("成功")
- .to_string();
- let data = serde_json::json!({
- "config": decrypted.get("config"),
- "session_info": decrypted.get("session_info"),
- "user_info": decrypted.get("user_info"),
- });
- let response = serde_json::json!({ "code": code, "msg": msg, "data": data });
- serde_json::to_string(&response).map_err(|e| format!("序列化心跳响应失败: {}", e))
- }
- // HMAC 签名密钥:按年份写死,与后端一致(CLIENT_SECRET = buff-client-secret-{年})
- fn get_secret_key() -> String {
- format!("buff-client-secret-{}", Utc::now().year())
- }
- fn get_server_secret_key() -> String {
- format!("buff-server-secret-{}", Utc::now().year())
- }
- // ---------- Buff 选品等安全 POST 命令(与后端 @secure_endpoint 对应) ----------
- #[tauri::command]
- pub async fn secure_buff_filter_options(
- session_id: String,
- machine_code: String,
- game: Option<String>,
- ) -> Result<String, String> {
- let config = AppConfig::load().map_err(|e| format!("加载配置失败: {}", e))?;
- let api_url = config.api.base_url.trim_end_matches('/');
- let request_data = serde_json::json!({
- "session_id": session_id,
- "machine_code": machine_code,
- "game": game.unwrap_or_else(|| "csgo".to_string()),
- });
- let decrypted = secure_post_decrypt(api_url, "/api/v1/buff/filter-options", &request_data).await?;
- serde_json::to_string(&decrypted).map_err(|e| format!("序列化失败: {}", e))
- }
- #[tauri::command]
- pub async fn secure_buff_goods_attr_list(
- session_id: String,
- machine_code: String,
- goods_ids: Vec<i64>,
- ) -> Result<String, String> {
- let config = AppConfig::load().map_err(|e| format!("加载配置失败: {}", e))?;
- let api_url = config.api.base_url.trim_end_matches('/');
- let request_data = serde_json::json!({
- "session_id": session_id,
- "machine_code": machine_code,
- "goods_ids": goods_ids,
- });
- let decrypted = secure_post_decrypt(api_url, "/api/v1/buff/goods-attr-list", &request_data).await?;
- serde_json::to_string(&decrypted).map_err(|e| format!("序列化失败: {}", e))
- }
- #[tauri::command]
- pub async fn secure_buff_selection_config(
- session_id: String,
- machine_code: String,
- action: String,
- name: Option<String>,
- config_json: Option<String>,
- is_default: Option<bool>,
- ) -> Result<String, String> {
- let config = AppConfig::load().map_err(|e| format!("加载配置失败: {}", e))?;
- let api_url = config.api.base_url.trim_end_matches('/');
- let mut request_data = serde_json::json!({
- "session_id": session_id,
- "machine_code": machine_code,
- "action": action,
- });
- if let Some(n) = name {
- request_data["name"] = serde_json::Value::String(n);
- }
- if let Some(c) = config_json {
- request_data["config_json"] = serde_json::Value::String(c);
- }
- if let Some(d) = is_default {
- request_data["is_default"] = serde_json::Value::Bool(d);
- }
- let decrypted = secure_post_decrypt(api_url, "/api/v1/buff/secure/selection-config", &request_data).await?;
- serde_json::to_string(&decrypted).map_err(|e| format!("序列化失败: {}", e))
- }
- #[tauri::command]
- pub async fn secure_buff_selection_start(
- session_id: String,
- machine_code: String,
- config_id: Option<i64>,
- ) -> Result<String, String> {
- let config = AppConfig::load().map_err(|e| format!("加载配置失败: {}", e))?;
- let api_url = config.api.base_url.trim_end_matches('/');
- let mut request_data = serde_json::json!({
- "session_id": session_id,
- "machine_code": machine_code,
- });
- if let Some(id) = config_id {
- request_data["config_id"] = serde_json::json!(id);
- }
- let decrypted = secure_post_decrypt(api_url, "/api/v1/buff/secure/selection-start", &request_data).await?;
- serde_json::to_string(&decrypted).map_err(|e| format!("序列化失败: {}", e))
- }
- #[tauri::command]
- pub async fn secure_buff_selection_log(
- session_id: String,
- machine_code: String,
- ) -> Result<String, String> {
- let config = AppConfig::load().map_err(|e| format!("加载配置失败: {}", e))?;
- let api_url = config.api.base_url.trim_end_matches('/');
- let request_data = serde_json::json!({
- "session_id": session_id,
- "machine_code": machine_code,
- });
- let decrypted = secure_post_decrypt(api_url, "/api/v1/buff/secure/selection-log", &request_data).await?;
- serde_json::to_string(&decrypted).map_err(|e| format!("序列化失败: {}", e))
- }
- #[tauri::command]
- pub async fn secure_buff_selection_result(
- session_id: String,
- machine_code: String,
- selection_request_id: Option<i64>,
- ) -> Result<String, String> {
- let config = AppConfig::load().map_err(|e| format!("加载配置失败: {}", e))?;
- let api_url = config.api.base_url.trim_end_matches('/');
- let mut request_data = serde_json::json!({
- "session_id": session_id,
- "machine_code": machine_code,
- });
- if let Some(sid) = selection_request_id {
- if sid > 0 {
- request_data["selection_request_id"] = serde_json::json!(sid);
- }
- }
- let decrypted = secure_post_decrypt(api_url, "/api/v1/buff/secure/selection-result", &request_data).await?;
- serde_json::to_string(&decrypted).map_err(|e| format!("序列化失败: {}", e))
- }
- async fn buff_secure_selection_stop_decrypt(
- session_id: &str,
- machine_code: &str,
- ) -> Result<serde_json::Value, String> {
- let config = AppConfig::load().map_err(|e| format!("加载配置失败: {}", e))?;
- let api_url = config.api.base_url.trim_end_matches('/');
- let request_data = serde_json::json!({
- "session_id": session_id,
- "machine_code": machine_code,
- });
- secure_post_decrypt(api_url, "/api/v1/buff/secure/selection-stop", &request_data).await
- }
- async fn buff_secure_sweep_stop_decrypt(
- session_id: &str,
- machine_code: &str,
- ) -> Result<serde_json::Value, String> {
- let config = AppConfig::load().map_err(|e| format!("加载配置失败: {}", e))?;
- let api_url = config.api.base_url.trim_end_matches('/');
- let request_data = serde_json::json!({
- "session_id": session_id,
- "machine_code": machine_code,
- });
- secure_post_decrypt(api_url, "/api/v1/buff/secure/sweep-stop", &request_data).await
- }
- #[tauri::command]
- pub async fn secure_buff_selection_stop(
- session_id: String,
- machine_code: String,
- ) -> Result<String, String> {
- let decrypted = buff_secure_selection_stop_decrypt(&session_id, &machine_code).await?;
- serde_json::to_string(&decrypted).map_err(|e| format!("序列化失败: {}", e))
- }
- #[tauri::command]
- pub async fn secure_keys_hmac(
- session_id: String,
- machine_code: String,
- ) -> Result<String, String> {
- let config = AppConfig::load().map_err(|e| format!("加载配置失败: {}", e))?;
- let api_url = config.api.base_url.trim_end_matches('/');
- let request_data = serde_json::json!({
- "session_id": session_id,
- "machine_code": machine_code,
- });
- let decrypted = secure_post_decrypt(api_url, "/api/v1/keys/hmac/secure", &request_data).await?;
- if let (Some(200), Some(data)) = (decrypted.get("code").and_then(|c| c.as_i64()), decrypted.get("data")) {
- if let (Some(hmac_key), Some(expires_at_val)) = (data.get("hmac_key").and_then(|v| v.as_str()), data.get("expires_at")) {
- let expires_at_u64: u64 = if let Some(s) = expires_at_val.as_str() {
- chrono::DateTime::parse_from_rfc3339(s)
- .map(|dt| dt.timestamp().max(0) as u64)
- .or_else(|_| {
- chrono::NaiveDateTime::parse_from_str(s, "%Y-%m-%dT%H:%M:%S")
- .map(|naive| naive.and_utc().timestamp().max(0) as u64)
- })
- .unwrap_or_else(|_| s.parse::<u64>().unwrap_or(0))
- } else if let Some(n) = expires_at_val.as_i64() {
- (n as u64).max(0)
- } else {
- 0
- };
- if !hmac_key.is_empty() && expires_at_u64 > 0 {
- store_hmac_key_from_api(hmac_key.to_string(), expires_at_u64);
- }
- }
- }
- serde_json::to_string(&decrypted).map_err(|e| format!("序列化失败: {}", e))
- }
- #[tauri::command]
- pub async fn store_rsa_public_key(public_key: String, key_version: i32) -> Result<(), String> {
- // 使用 crypto/key_storage 统一存储,与 initialize_key_manager 同一套存储
- store_rsa_key_segments(public_key.clone(), key_version);
- // 调试:验证存储和重组是否正确(生产环境请移除)
- #[cfg(debug_assertions)]
- {
- println!("RSA公钥已安全存储,版本: {}", key_version);
- match retrieve_rsa_key_segments() {
- Ok(retrieved) if retrieved == public_key => {
- println!("✅ RSA公钥存储和重组验证成功");
- }
- Ok(retrieved) => {
- println!("❌ RSA公钥存储验证失败: 内容不匹配");
- println!(" 原始长度: {}, 重组长度: {}", public_key.len(), retrieved.len());
- println!(" 原始前缀: {}", &public_key[..50.min(public_key.len())]);
- println!(" 重组前缀: {}", &retrieved[..50.min(retrieved.len())]);
- }
- Err(e) => {
- println!("❌ RSA公钥存储验证失败: {}", e);
- }
- }
- }
- Ok(())
- }
- #[tauri::command]
- pub async fn has_stored_rsa_key() -> Result<bool, String> {
- Ok(crypto_has_stored_rsa_key())
- }
- #[tauri::command]
- pub async fn clear_stored_rsa_key() -> Result<(), String> {
- crypto_clear_stored_rsa_key();
- Ok(())
- }
- #[tauri::command]
- pub async fn rsa_encrypt_aes_key_by_stored_key(aes_key: String) -> Result<String, String> {
- use base64::{Engine as _, engine::general_purpose};
- use rsa::{RsaPublicKey, pkcs8::DecodePublicKey, pkcs1v15::Pkcs1v15Encrypt, oaep::Oaep};
- use rand::thread_rng;
- // 从Rust后端分段存储中重组RSA公钥
- let public_key_pem = retrieve_rsa_key_segments()?;
- let aes_key_bytes = general_purpose::STANDARD.decode(&aes_key)
- .map_err(|e| format!("Failed to decode AES key: {}", e))?;
- if aes_key_bytes.len() != 32 {
- return Err("Invalid AES key length".to_string());
- }
- let public_key_parsed = RsaPublicKey::from_public_key_pem(&public_key_pem)
- .map_err(|e| format!("Failed to parse public key: {}", e))?;
- let mut rng = thread_rng();
- // 使用OAEP填充,与后端保持一致
- let padding = Oaep::new::<sha2::Sha256>();
- let encrypted_aes_key = public_key_parsed.encrypt(&mut rng, padding, &aes_key_bytes)
- .map_err(|e| format!("Failed to encrypt AES key: {}", e))?;
- Ok(general_purpose::STANDARD.encode(&encrypted_aes_key))
- }
- #[tauri::command]
- pub async fn rsa_encrypt_aes_key(aes_key: String, public_key: String) -> Result<String, String> {
- use base64::{Engine as _, engine::general_purpose};
- // 解码AES密钥
- let aes_key_bytes = general_purpose::STANDARD.decode(&aes_key)
- .map_err(|e| format!("Failed to decode AES key: {}", e))?;
- if aes_key_bytes.len() != 32 {
- return Err("Invalid AES key length".to_string());
- }
- // 解析RSA公钥
- use rsa::{RsaPublicKey, pkcs8::DecodePublicKey};
- let public_key_parsed = RsaPublicKey::from_public_key_pem(&public_key)
- .map_err(|e| format!("Failed to parse public key: {}", e))?;
- // 使用RSA加密AES密钥
- use rsa::pkcs1v15::Pkcs1v15Encrypt;
- let mut rng = rand::thread_rng();
- // 使用OAEP填充,与后端保持一致
- let padding = Oaep::new::<sha2::Sha256>();
- let encrypted_aes_key = public_key_parsed.encrypt(&mut rng, padding, &aes_key_bytes)
- .map_err(|e| format!("Failed to encrypt AES key: {}", e))?;
- // Base64编码
- Ok(general_purpose::STANDARD.encode(&encrypted_aes_key))
- }
- #[derive(serde::Serialize, serde::Deserialize)]
- pub struct AnnouncementRequest {
- pub page: i32,
- pub page_size: i32,
- pub keyword: Option<String>,
- }
- /// Tauri 2 invoke 传参格式为 { request: AnnouncementRequest },需用包装结构体接收
- #[derive(serde::Serialize, serde::Deserialize)]
- pub struct SecureGetAnnouncementsArgs {
- pub request: AnnouncementRequest,
- }
- #[derive(serde::Serialize, serde::Deserialize)]
- pub struct AnnouncementResponse {
- pub data: Vec<serde_json::Value>,
- pub total: i32,
- pub page: i32,
- pub page_size: i32,
- }
- #[tauri::command]
- pub async fn secure_get_announcements(args: SecureGetAnnouncementsArgs) -> Result<AnnouncementResponse, String> {
- let request = args.request;
- // 公告为公开数据,使用 GET /api/v1/announcements 明文接口,避免 POST 安全端点的 422 等问题
- let base = get_api_base_url()?;
- let url = format!(
- "{}/api/v1/announcements?page={}&page_size={}",
- base.trim_end_matches('/'),
- request.page,
- request.page_size
- );
- let client = get_http_client();
- let response = client
- .get(&url)
- .send()
- .await
- .map_err(|e| format!("HTTP request failed: {}", e))?;
- if !response.status().is_success() {
- return Err(format!("HTTP request failed with status: {}", response.status()));
- }
- let response_json: serde_json::Value = response
- .json()
- .await
- .map_err(|e| format!("Failed to parse response: {}", e))?;
- let code = response_json.get("code").and_then(|c| c.as_i64()).unwrap_or(0);
- if code != 200 {
- let msg = response_json
- .get("msg")
- .and_then(|m| m.as_str())
- .unwrap_or("Unknown error");
- return Err(format!("API error: {}", msg));
- }
- // 后端 GET 返回: { "code": 200, "msg": "...", "data": { "announcements": [...], "total", "page", "page_size" } }
- let data_obj = response_json.get("data").and_then(|d| d.as_object());
- let data = data_obj
- .and_then(|o| o.get("announcements"))
- .and_then(|a| a.as_array())
- .cloned()
- .unwrap_or_default();
- let total = data_obj
- .and_then(|o| o.get("total"))
- .and_then(|t| t.as_i64())
- .unwrap_or(0) as i32;
- let page = data_obj
- .and_then(|o| o.get("page"))
- .and_then(|p| p.as_i64())
- .unwrap_or(1) as i32;
- let page_size = data_obj
- .and_then(|o| o.get("page_size"))
- .and_then(|ps| ps.as_i64())
- .unwrap_or(10) as i32;
- Ok(AnnouncementResponse {
- data,
- total,
- page,
- page_size,
- })
- }
- #[tauri::command]
- pub fn get_rsa_key_version() -> Result<i32, String> {
- get_stored_key_version().ok_or_else(|| "RSA公钥版本不存在".to_string())
- }
- // ==================== 新的安全通信命令 ====================
- /// 初始化安全客户端(仅 RSA 公钥;HMAC 使用按年份写死的静态密钥,无需同步)
- #[tauri::command]
- pub async fn initialize_security_client(
- api_url: String,
- _username: Option<String>,
- machine_code: String,
- ) -> Result<bool, String> {
- use crate::commands::crypto::initialize_key_manager;
- info!("[SecurityClient] 初始化安全客户端");
- let key_result = initialize_key_manager(&api_url, false, Some(&machine_code)).await;
- if let Err(e) = key_result {
- error!("[SecurityClient] RSA密钥初始化失败: {}", e);
- return Err(format!("RSA密钥初始化失败: {}", e));
- }
- info!("[SecurityClient] 安全客户端初始化成功");
- Ok(true)
- }
- /// 检查安全客户端是否已初始化
- #[tauri::command]
- pub fn check_security_initialized() -> Result<bool, String> {
- Ok(crate::commands::crypto::has_stored_rsa_key())
- }
- /// 获取安全客户端状态
- #[tauri::command]
- pub fn get_security_status() -> Result<serde_json::Value, String> {
- Ok(serde_json::json!({
- "key_manager": crate::commands::crypto::get_key_manager_status(),
- "hmac_key": crate::commands::crypto::get_hmac_key_status(),
- "timestamp": std::time::SystemTime::now()
- .duration_since(std::time::UNIX_EPOCH)
- .unwrap_or_default()
- .as_secs(),
- }))
- }
- /// 刷新安全密钥(如果需要)
- #[tauri::command]
- pub async fn refresh_security_keys(api_url: String) -> Result<bool, String> {
- use crate::commands::crypto::refresh_key_if_needed;
- info!("[SecurityClient] 刷新安全密钥");
- refresh_key_if_needed(&api_url).await
- }
- /// 重置安全客户端
- #[tauri::command]
- pub fn reset_security_client() -> Result<(), String> {
- use crate::commands::crypto::clear_hmac_key;
- info!("[SecurityClient] 重置安全客户端");
- clear_hmac_key();
- Ok(())
- }
- /// 同步HMAC密钥(调用后端API)
- #[tauri::command]
- pub async fn sync_hmac_key(
- username: String,
- machine_code: String,
- ) -> Result<serde_json::Value, String> {
- use serde_json::Value;
- info!("[SecurityClient] 同步HMAC密钥: {}", username);
- // 获取API基础URL
- let config = AppConfig::load()
- .map_err(|e| format!("加载配置失败: {}", e))?;
-
- let api_url = config.api.base_url;
- let url = format!("{}/api/v1/keys/hmac", api_url);
- // 发送请求
- let client = get_http_client();
- let response = client
- .post(&url)
- .json(&serde_json::json!({
- "username": username,
- "machine_code": machine_code
- }))
- .send()
- .await
- .map_err(|e| format!("HTTP请求失败: {}", e))?;
- // 解析响应
- let response_json: Value = response
- .json()
- .await
- .map_err(|e| format!("响应解析失败: {}", e))?;
- info!("[SecurityClient] HMAC密钥同步响应: {:?}", response_json);
- Ok(response_json)
- }
- /// 获取密钥管理器状态(供前端/心跳使用)
- #[tauri::command]
- pub fn get_key_manager_status() -> Result<serde_json::Value, String> {
- Ok(crate::commands::crypto::get_key_manager_status())
- }
- /// 获取 HMAC 密钥状态(供前端/心跳使用)
- #[tauri::command]
- pub fn get_hmac_key_status() -> Result<serde_json::Value, String> {
- Ok(crate::commands::crypto::get_hmac_key_status())
- }
- // ============ 心跳管理命令 ============
- /// 获取心跳状态
- #[tauri::command]
- pub fn get_heartbeat_state_command() -> Result<serde_json::Value, String> {
- Ok(get_heartbeat_service_status())
- }
- /// 停止心跳服务
- #[tauri::command]
- pub async fn stop_heartbeat_command() -> Result<(), String> {
- stop_heartbeat_service().await;
- Ok(())
- }
- /// 获取心跳配置
- #[tauri::command]
- pub fn get_heartbeat_config_command() -> Result<serde_json::Value, String> {
- let config = get_heartbeat_config();
- Ok(serde_json::json!({
- "interval_seconds": config.interval_seconds,
- "max_failures": config.max_failures,
- "renewal_threshold": config.renewal_threshold,
- "extension_duration": config.extension_duration,
- }))
- }
- /// 读取用户选择的 txt 文件内容(用于 BUFF 选品排除/指定商品 id)
- #[tauri::command]
- pub fn read_text_file(path: String) -> Result<String, String> {
- fs::read_to_string(&path).map_err(|e| format!("读取文件失败: {}", e))
- }
- // ============ 扫货日志(仅本地 JSONL 文件 + 后端最近一次请求单元数据,不落库日志正文) ============
- #[derive(Debug, Clone, Serialize, Deserialize)]
- struct LocalSweepLogEntry {
- created_at: Option<String>,
- goods_id: String,
- goods_name: String,
- result: String,
- message: String,
- target_price_usd: Option<f64>,
- purchase_price_usd: Option<f64>,
- #[serde(default)]
- purchase_wear: Option<f64>,
- #[serde(default)]
- listing_id: Option<String>,
- #[serde(default)]
- line_color: Option<String>,
- #[serde(default)]
- diag_level: Option<String>,
- // API 详细日志字段
- #[serde(default)]
- api_name: Option<String>,
- #[serde(default)]
- steam_account: Option<String>,
- #[serde(default)]
- method: Option<String>,
- #[serde(default)]
- url: Option<String>,
- #[serde(default)]
- request_headers: Option<serde_json::Value>,
- #[serde(default)]
- request_body: Option<serde_json::Value>,
- #[serde(default)]
- response_status: Option<u16>,
- #[serde(default)]
- response_body: Option<String>,
- #[serde(default)]
- elapsed_ms: Option<u64>,
- #[serde(default)]
- error: Option<String>,
- }
- /// 去掉扫货日志 `【线程N】` 前缀后再做关键词着色,避免影响成功/命中判断。
- fn sweep_log_message_for_color_rules(message: &str) -> &str {
- let s = message.trim();
- if !s.starts_with('【') {
- return s;
- }
- if let Some(i) = s.find('】') {
- let rest = &s[i + '】'.len_utf8()..];
- return rest.trim_start();
- }
- s
- }
- fn sweep_log_line_color_hex(result: &str, message: &str, diag_level: Option<&str>) -> String {
- if result == "diag" {
- return match diag_level {
- Some("warn") => "#fa8c16".to_string(),
- Some("error") => "#ff4d4f".to_string(),
- _ => "#595959".to_string(),
- };
- }
- let msg = sweep_log_message_for_color_rules(message);
- if result == "failed" {
- return "#fa8c16".to_string();
- }
- if result == "success" {
- return "#ff4d4f".to_string();
- }
- if !msg.is_empty() && (msg.contains("购买成功") || msg.contains("扫货成功")) {
- return "#ff4d4f".to_string();
- }
- if result == "matched" {
- return "#52c41a".to_string();
- }
- if result == "scan" {
- return "#8c8c8c".to_string();
- }
- if msg.contains("磨损命中目标区间") {
- return "#52c41a".to_string();
- }
- if msg.contains("价格命中候选") && msg.contains("磨损查询") {
- return "#52c41a".to_string();
- }
- "#000000".to_string()
- }
- fn hydrate_sweep_log_line_colors(logs: &mut [LocalSweepLogEntry]) {
- for e in logs.iter_mut() {
- if e.line_color.as_deref().unwrap_or("").is_empty() {
- e.line_color = Some(sweep_log_line_color_hex(
- &e.result,
- &e.message,
- e.diag_level.as_deref(),
- ));
- }
- }
- }
- fn local_sweep_log_file_path(request_id: i64) -> Result<PathBuf, String> {
- let steam_msg_dir = get_steam_msg_dir()?;
- Ok(steam_msg_dir.join(format!("{}.txt", request_id)))
- }
- fn discover_latest_local_sweep_request_id() -> Option<i64> {
- let dir = match get_steam_msg_dir() {
- Ok(d) => d,
- Err(e) => {
- log::error!("[discover_latest_local_sweep_request_id] 获取目录失败: {}", e);
- return None;
- }
- };
- log::info!("[discover_latest_local_sweep_request_id] 扫描目录: {:?}", dir);
- // 检查目录是否存在
- if !dir.exists() {
- log::error!("[discover_latest_local_sweep_request_id] 目录不存在: {:?}", dir);
- return None;
- }
- // 检查是否是目录
- if !dir.is_dir() {
- log::error!("[discover_latest_local_sweep_request_id] 路径不是目录: {:?}", dir);
- return None;
- }
- let entries = match fs::read_dir(&dir) {
- Ok(e) => e,
- Err(e) => {
- log::error!("[discover_latest_local_sweep_request_id] 读取目录失败: {} (目录: {:?})", e, dir);
- return None;
- }
- };
- let mut max_id: Option<i64> = None;
- for entry in entries.flatten() {
- let path = entry.path();
- if !path.is_file() {
- continue;
- }
- let name = match path.file_name().and_then(|n| n.to_str()) {
- Some(n) => n,
- None => continue,
- };
- let stem = match name.strip_suffix(".txt") {
- Some(s) => s,
- None => continue,
- };
- match stem.parse::<i64>() {
- Ok(id) => {
- log::info!("[discover_latest_local_sweep_request_id] 发现日志文件: {} -> id={}", name, id);
- max_id = Some(max_id.map(|m| m.max(id)).unwrap_or(id));
- }
- Err(_) => {
- // 非数字命名的 txt 文件(如 accounts.json),忽略
- }
- }
- }
- log::info!("[discover_latest_local_sweep_request_id] 最大 request_id: {:?}", max_id);
- max_id
- }
- fn read_local_sweep_meta_started_at(request_id: i64) -> Option<String> {
- let path = local_sweep_log_file_path(request_id).ok()?;
- let content = fs::read_to_string(&path).ok()?;
- let first = content.lines().next()?.trim();
- if !first.starts_with('{') {
- return None;
- }
- let v: serde_json::Value = serde_json::from_str(first).ok()?;
- v.get("meta")?
- .get("started_at")?
- .as_str()
- .map(|s| s.to_string())
- }
- fn read_local_sweep_logs_from_file(request_id: i64) -> Result<Vec<LocalSweepLogEntry>, String> {
- fn extract_field(raw: &str, start_marker: &str, end_marker: Option<&str>) -> Option<String> {
- let start = raw.find(start_marker)? + start_marker.len();
- let tail = &raw[start..];
- let end = match end_marker {
- Some(marker) => tail.find(marker)?,
- None => tail.len(),
- };
- Some(tail[..end].trim().to_string())
- }
- let path = local_sweep_log_file_path(request_id)?;
- if !path.exists() {
- return Ok(Vec::new());
- }
- let content = fs::read_to_string(&path).map_err(|e| format!("读取日志文件失败: {}", e))?;
- let mut out: Vec<LocalSweepLogEntry> = Vec::new();
- for line in content.lines() {
- let raw = line.trim();
- if raw.is_empty() {
- continue;
- }
- if raw.starts_with('{') {
- // 跳过 meta 行
- if let Ok(value) = serde_json::from_str::<serde_json::Value>(raw) {
- if value.get("meta").is_some() {
- continue;
- }
- }
- // 解析为 LocalSweepLogEntry,失败时记录日志
- match serde_json::from_str::<LocalSweepLogEntry>(raw) {
- Ok(mut entry) => {
- if entry.line_color.as_deref().unwrap_or("").is_empty() {
- entry.line_color = Some(sweep_log_line_color_hex(
- &entry.result,
- &entry.message,
- entry.diag_level.as_deref(),
- ));
- }
- out.push(entry);
- }
- Err(e) => {
- log::warn!(
- "[read_local_sweep_logs_from_file] JSON 行解析失败: {} (前50字符: {:.50?})",
- e, raw
- );
- }
- }
- continue;
- }
- if raw.starts_with("扫货日志(本地文件为准)") {
- continue;
- }
- if raw.starts_with("created_at\t") {
- continue;
- }
- if raw.starts_with("时间=") {
- let created_at = extract_field(raw, "时间=", Some(" | 饰品ID="));
- let goods_id = extract_field(raw, " | 饰品ID=", Some(" | 饰品名称=")).unwrap_or_default();
- let goods_name = extract_field(raw, " | 饰品名称=", Some(" | 结果=")).unwrap_or_default();
- let result = extract_field(raw, " | 结果=", Some(" | 目标价USD=")).unwrap_or_default();
- let target_raw = extract_field(raw, " | 目标价USD=", Some(" | 成交价USD=")).unwrap_or_default();
- let purchase_raw = extract_field(raw, " | 成交价USD=", Some(" | 说明=")).unwrap_or_default();
- let message = extract_field(raw, " | 说明=", None).unwrap_or_default();
- let mut entry = LocalSweepLogEntry {
- created_at: created_at.and_then(|v| if v.is_empty() { None } else { Some(v) }),
- goods_id,
- goods_name,
- result,
- message,
- target_price_usd: if target_raw.is_empty() {
- None
- } else {
- target_raw.parse::<f64>().ok()
- },
- purchase_price_usd: if purchase_raw.is_empty() {
- None
- } else {
- purchase_raw.parse::<f64>().ok()
- },
- purchase_wear: None,
- listing_id: None,
- line_color: None,
- diag_level: None,
- api_name: None,
- steam_account: None,
- method: None,
- url: None,
- request_headers: None,
- request_body: None,
- response_status: None,
- response_body: None,
- elapsed_ms: None,
- error: None,
- };
- entry.line_color = Some(sweep_log_line_color_hex(
- &entry.result,
- &entry.message,
- None,
- ));
- out.push(entry);
- continue;
- }
- let cols: Vec<&str> = raw.splitn(7, '\t').collect();
- if cols.len() != 7 {
- continue;
- }
- let target_price_usd = if cols[4].trim().is_empty() {
- None
- } else {
- cols[4].trim().parse::<f64>().ok()
- };
- let purchase_price_usd = if cols[5].trim().is_empty() {
- None
- } else {
- cols[5].trim().parse::<f64>().ok()
- };
- let mut entry = LocalSweepLogEntry {
- created_at: if cols[0].trim().is_empty() {
- None
- } else {
- Some(cols[0].trim().to_string())
- },
- goods_id: cols[1].trim().to_string(),
- goods_name: cols[2].trim().to_string(),
- result: cols[3].trim().to_string(),
- message: cols[6].trim().to_string(),
- target_price_usd,
- purchase_price_usd,
- purchase_wear: None,
- listing_id: None,
- line_color: None,
- diag_level: None,
- api_name: None,
- steam_account: None,
- method: None,
- url: None,
- request_headers: None,
- request_body: None,
- response_status: None,
- response_body: None,
- elapsed_ms: None,
- error: None,
- };
- entry.line_color = Some(sweep_log_line_color_hex(
- &entry.result,
- &entry.message,
- None,
- ));
- out.push(entry);
- }
- hydrate_sweep_log_line_colors(&mut out);
- log::info!("[read_local_sweep_logs_from_file] request_id={} 总行数={} 解析成功={} 解析失败={}",
- request_id, content.lines().count(), out.len(),
- content.lines().filter(|l| !l.trim().is_empty()).count() - out.len());
- Ok(out)
- }
- /// 单笔扫货成功明细立即写入 `buff_sweep_request_item`(`append_only`,不删历史行)。
- /// 本地扫货在购买成功分支里应第一时间 `await` 本函数。
- pub async fn buff_sweep_append_success_item_inner(
- session_id: &str,
- machine_code: &str,
- sweep_request_id: i64,
- selection_request_id: i64,
- item: serde_json::Value,
- ) -> Result<(), String> {
- if session_id.trim().is_empty() || machine_code.trim().is_empty() {
- return Err("会话或机器码为空".to_string());
- }
- if sweep_request_id <= 0 || selection_request_id <= 0 {
- return Err("sweep_request_id 或 selection_request_id 无效".to_string());
- }
- let config = AppConfig::load().map_err(|e| format!("加载配置失败: {}", e))?;
- let api_url = config.api.base_url.trim_end_matches('/');
- let request_data = serde_json::json!({
- "session_id": session_id,
- "machine_code": machine_code,
- "sweep_request_id": sweep_request_id,
- "selection_request_id": selection_request_id,
- "append_only": true,
- "items": [item],
- });
- let decrypted =
- secure_post_decrypt(api_url, "/api/v1/buff/secure/sweep-item-client-sync", &request_data).await?;
- let code = decrypted.get("code").and_then(|v| v.as_i64()).unwrap_or(500);
- if code != 200 {
- let msg = decrypted
- .get("msg")
- .and_then(|v| v.as_str())
- .unwrap_or("扫货成功明细同步失败");
- return Err(msg.to_string());
- }
- Ok(())
- }
- /// 供前端或其它调用方传入单条成功明细 JSON(字段与后端 `sweep-item-client-sync` 中单条一致)。
- #[tauri::command]
- pub async fn secure_buff_sweep_append_success_item(
- session_id: String,
- machine_code: String,
- sweep_request_id: i64,
- selection_request_id: i64,
- item_json: String,
- ) -> Result<String, String> {
- let item: serde_json::Value =
- serde_json::from_str(&item_json).map_err(|e| format!("item_json 解析失败: {}", e))?;
- buff_sweep_append_success_item_inner(
- &session_id,
- &machine_code,
- sweep_request_id,
- selection_request_id,
- item,
- )
- .await?;
- serde_json::to_string(&serde_json::json!({
- "code": 200,
- "msg": "ok",
- "data": null
- }))
- .map_err(|e| format!("序列化失败: {}", e))
- }
- /// 客户端版本检查(安全端点,登录前可调用)
- #[tauri::command]
- pub async fn secure_client_version_check() -> Result<String, String> {
- let config = AppConfig::load().map_err(|e| format!("加载配置失败: {}", e))?;
- let api_url = config.api.base_url.trim_end_matches('/');
- let platform = if cfg!(target_os = "windows") {
- "windows"
- } else if cfg!(target_os = "macos") {
- "macos"
- } else {
- "linux"
- };
- let request_data = serde_json::json!({
- "client_version": env!("CARGO_PKG_VERSION"),
- "platform": platform,
- "channel": "stable"
- });
- let decrypted = secure_post_decrypt(api_url, "/api/v1/client/version/check", &request_data).await?;
- serde_json::to_string(&decrypted).map_err(|e| format!("序列化失败: {}", e))
- }
- #[tauri::command]
- pub async fn secure_buff_selection_item_delete(
- session_id: String,
- machine_code: String,
- item_id: i64,
- ) -> Result<String, String> {
- let config = AppConfig::load().map_err(|e| format!("加载配置失败: {}", e))?;
- let api_url = config.api.base_url.trim_end_matches('/');
- let request_data = serde_json::json!({
- "session_id": session_id,
- "machine_code": machine_code,
- "item_id": item_id,
- });
- let decrypted =
- secure_post_decrypt(api_url, "/api/v1/buff/secure/selection-item-delete", &request_data).await?;
- serde_json::to_string(&decrypted).map_err(|e| format!("序列化失败: {}", e))
- }
- /// 批量导入选品数据(JSON文件方式)。
- ///
- /// 1. 读取本地多个JSON文件,合并内容
- /// 2. 调用后端接口,先删除当前用户所有选品数据,再批量插入新数据
- /// 3. JSON中的价格为CNY,转换为USD:USD = CNY / steam_card_ratio + 0.01(保留2位小数)
- #[tauri::command]
- pub async fn batch_import_buff_selection_items(
- session_id: String,
- machine_code: String,
- file_paths: Vec<String>,
- steam_card_ratio: f64,
- ) -> Result<String, String> {
- use std::collections::HashMap;
- if session_id.trim().is_empty() || machine_code.trim().is_empty() {
- return Err("会话或机器码为空".to_string());
- }
- if file_paths.is_empty() {
- return Err("至少需要选择一个JSON文件".to_string());
- }
- if steam_card_ratio <= 0.0 {
- return Err("Steam卡价比例必须大于0".to_string());
- }
- // 合并多个JSON文件内容
- let mut merged_payload: HashMap<String, serde_json::Value> = HashMap::new();
- for path in &file_paths {
- let content = std::fs::read_to_string(path)
- .map_err(|e| format!("读取文件失败 {}: {}", path, e))?;
- let payload: serde_json::Value = serde_json::from_str(&content)
- .map_err(|e| format!("JSON解析失败 {}: {}", path, e))?;
- if let serde_json::Value::Object(obj) = payload {
- for (k, v) in obj {
- merged_payload.insert(k, v);
- }
- } else {
- return Err(format!("JSON文件格式错误(非对象): {}", path));
- }
- }
- if merged_payload.is_empty() {
- return Err("JSON文件中没有有效数据".to_string());
- }
- let goods_ids: Vec<String> = merged_payload.keys().cloned().collect();
- let config = AppConfig::load().map_err(|e| format!("加载配置失败: {}", e))?;
- let api_url = config.api.base_url.trim_end_matches('/');
- // 先调用后端接口获取这些 goods_id 对应的元数据(goods_name, icon_url, steam_link)
- let request_data = serde_json::json!({
- "session_id": session_id,
- "machine_code": machine_code,
- "goods_ids": goods_ids,
- });
- let decrypted = secure_post_decrypt(api_url, "/api/v1/buff/goods-attr-list", &request_data).await?;
- // 解析 goods 元数据
- let goods_meta_list = decrypted
- .get("data")
- .and_then(|d| d.as_array())
- .map(|arr| {
- arr.iter()
- .filter_map(|item| {
- let goods_id = item.get("goods_id")?.as_str()?.to_string();
- let name = item.get("name").and_then(|n| n.as_str()).unwrap_or("").to_string();
- let icon_url = item.get("icon_url").and_then(|u| u.as_str()).unwrap_or("").to_string();
- let steam_link = item
- .get("steam_link")
- .and_then(|l| l.as_str())
- .unwrap_or("")
- .to_string();
- Some((goods_id, name, icon_url, steam_link))
- })
- .collect::<Vec<_>>()
- })
- .unwrap_or_default();
- let goods_meta_map: HashMap<String, (String, String, String)> = goods_meta_list
- .into_iter()
- .map(|(id, name, icon, link)| (id, (name, icon, link)))
- .collect();
- // 组装批量导入的数据
- let mut items_to_insert: Vec<serde_json::Value> = Vec::new();
- for (goods_id, value) in &merged_payload {
- let price_str = value
- .get("price")
- .and_then(|p| p.as_str().map(|s| s.to_string()))
- .or_else(|| value.get("price").and_then(|p| p.as_f64().map(|f| format!("{}", f))))
- .ok_or_else(|| format!("goods_id {} 缺少有效的 price 字段", goods_id))?;
- let price_cny = price_str.parse::<f64>()
- .map_err(|_| format!("goods_id {} price 字段不是有效数字: {}", goods_id, price_str))?;
- let min_wear = value
- .get("min_paintwear")
- .and_then(|p| p.as_str().map(|s| s.to_string()))
- .or_else(|| value.get("min_paintwear").and_then(|p| p.as_f64().map(|f| format!("{}", f))))
- .unwrap_or_else(|| "0".to_string());
- let max_wear = value
- .get("max_paintwear")
- .and_then(|p| p.as_str().map(|s| s.to_string()))
- .or_else(|| value.get("max_paintwear").and_then(|p| p.as_f64().map(|f| format!("{}", f))))
- .unwrap_or_else(|| "0".to_string());
- let wear_range = format!("{}-{}", min_wear, max_wear);
- // CNY 保留2位小数
- let price_cny_rounded = (price_cny * 100.0).round() / 100.0;
- // USD = CNY / ratio + 0.01,保留2位小数
- let price_usd = ((price_cny_rounded / steam_card_ratio) * 100.0).round() / 100.0 + 0.01;
- // 优先从 JSON 文件的 name 字段获取饰品名称,如果没有则用后端返回的数据
- let goods_name_from_json = value
- .get("name")
- .and_then(|n| n.as_str())
- .map(|s| s.to_string())
- .unwrap_or_default();
- let (goods_name_from_api, icon_url, steam_link) = goods_meta_map
- .get(goods_id)
- .cloned()
- .unwrap_or_else(|| (String::new(), String::new(), String::new()));
- // 优先使用 JSON 中的名称,其次使用后端接口返回的名称
- let goods_name = if !goods_name_from_json.is_empty() {
- goods_name_from_json
- } else {
- goods_name_from_api
- };
- items_to_insert.push(serde_json::json!({
- "goods_id": goods_id,
- "goods_name": goods_name,
- "icon_url": icon_url,
- "steam_link": steam_link,
- "price_cny": price_cny_rounded,
- "price_usd": price_usd,
- "wear_range": wear_range,
- }));
- }
- // 调用后端批量导入接口
- let import_request_data = serde_json::json!({
- "session_id": session_id,
- "machine_code": machine_code,
- "items": items_to_insert,
- });
- let import_decrypted =
- secure_post_decrypt(api_url, "/api/v1/buff/secure/selection-batch-import", &import_request_data).await?;
- serde_json::to_string(&import_decrypted).map_err(|e| format!("序列化失败: {}", e))
- }
- /// 扫货:获取后台已绑定 Steam 用户名(与本地 accounts.json 取交集后用于扫货)。
- #[tauri::command]
- pub async fn secure_buff_sweep_bound_steam_usernames(
- session_id: String,
- machine_code: String,
- ) -> Result<String, String> {
- if session_id.trim().is_empty() || machine_code.trim().is_empty() {
- return Err("会话或机器码为空".to_string());
- }
- let config = AppConfig::load().map_err(|e| format!("加载配置失败: {}", e))?;
- let api_url = config.api.base_url.trim_end_matches('/');
- let request_data = serde_json::json!({
- "session_id": session_id,
- "machine_code": machine_code,
- });
- let decrypted = secure_post_decrypt(
- api_url,
- "/api/v1/buff/secure/sweep-bound-steam-usernames",
- &request_data,
- )
- .await?;
- serde_json::to_string(&decrypted).map_err(|e| format!("序列化失败: {}", e))
- }
- #[tauri::command]
- pub async fn secure_buff_sweep_start(session_id: String, machine_code: String) -> Result<String, String> {
- let config = AppConfig::load().map_err(|e| format!("加载配置失败: {}", e))?;
- let api_url = config.api.base_url.trim_end_matches('/');
- let request_data = serde_json::json!({
- "session_id": session_id,
- "machine_code": machine_code,
- "client_local": true
- });
- let decrypted = secure_post_decrypt(api_url, "/api/v1/buff/secure/sweep-start", &request_data).await?;
- serde_json::to_string(&decrypted).map_err(|e| format!("序列化失败: {}", e))
- }
- /// 启动客户端本地扫货循环(需先调用 `secure_buff_sweep_start` 成功并拿到 request_id / selection_request_id)。
- #[tauri::command]
- pub async fn spawn_buff_sweep_local_runner(
- session_id: String,
- machine_code: String,
- sweep_request_id: i64,
- selection_request_id: i64,
- sweep_worker_count: Option<u32>,
- selected_accounts: Option<Vec<String>>,
- account_card_rate_map: Option<std::collections::HashMap<String, f64>>,
- ) -> Result<(), String> {
- // 扫货启动时,确保用户文件和目录存在
- ensure_sweep_data_files_exist()?;
- buff_sweep_runner::spawn_runner(
- session_id,
- machine_code,
- sweep_request_id,
- selection_request_id,
- sweep_worker_count.unwrap_or(3).max(1),
- selected_accounts.unwrap_or_default(),
- account_card_rate_map.unwrap_or_default(),
- )
- .await
- }
- /// 确保扫货数据文件和目录存在(扫货启动前调用)
- fn ensure_sweep_data_files_exist() -> Result<(), String> {
- let steam_msg_dir = get_steam_msg_dir()?;
- fs::create_dir_all(&steam_msg_dir).map_err(|e| format!("创建 steam-msg 目录失败: {}", e))?;
- // 确保用户文件存在(空文件或空结构)
- let username = get_current_username_internal().unwrap_or_else(|_| "guest".to_string());
- let user_file_path = sweep_success_items_path()?;
- if !user_file_path.exists() {
- let initial_data = LocalSweepSuccessData::default();
- let content = serde_json::to_string_pretty(&initial_data)
- .map_err(|e| format!("序列化失败: {}", e))?;
- fs::write(&user_file_path, content).map_err(|e| format!("创建用户文件失败: {}", e))?;
- log::info!("[ensure] 已创建用户扫货数据文件: {:?}", user_file_path);
- }
- log::info!("[ensure] steam-msg 目录: {:?}", steam_msg_dir);
- log::info!("[ensure] 用户扫货数据文件: {:?}", user_file_path);
- Ok(())
- }
- #[tauri::command]
- pub async fn secure_buff_sweep_stop(session_id: String, machine_code: String) -> Result<String, String> {
- buff_sweep_runner::request_cancel();
-
- // 等待扫货任务检测到取消并退出(最多等待 3 秒)
- match tokio::time::timeout(
- tokio::time::Duration::from_secs(3),
- buff_sweep_runner::wait_for_stop()
- ).await {
- Ok(_) => info!("[sweep-stop] 扫货任务已停止"),
- Err(_) => warn!("[sweep-stop] 等待扫货任务停止超时,强制继续"),
- }
-
- let decrypted = buff_secure_sweep_stop_decrypt(&session_id, &machine_code).await?;
- serde_json::to_string(&decrypted).map_err(|e| format!("序列化失败: {}", e))
- }
- #[tauri::command]
- pub async fn secure_buff_sweep_result(
- session_id: String,
- machine_code: String,
- page: Option<i64>,
- page_size: Option<i64>,
- ) -> Result<String, String> {
- if session_id.trim().is_empty() || machine_code.trim().is_empty() {
- let resp = serde_json::json!({
- "code": 400,
- "msg": "请求参数错误,请检查后重试",
- "data": null
- });
- return serde_json::to_string(&resp).map_err(|e| format!("序列化失败: {}", e));
- }
- let config = AppConfig::load().map_err(|e| format!("加载配置失败: {}", e))?;
- let api_url = config.api.base_url.trim_end_matches('/');
- let mut request_data = serde_json::json!({
- "session_id": session_id,
- "machine_code": machine_code,
- });
- if let Some(p) = page {
- request_data["page"] = serde_json::json!(p);
- }
- if let Some(ps) = page_size {
- request_data["page_size"] = serde_json::json!(ps);
- }
- let decrypted = secure_post_decrypt(api_url, "/api/v1/buff/secure/sweep-result", &request_data).await?;
- serde_json::to_string(&decrypted).map_err(|e| format!("序列化失败: {}", e))
- }
- /// 扫货日志:正文仅来自本地 `steam-msg/{request_id}.txt`(JSONL,可含 `line_color`)。
- /// 扫货进程不再向 `/buff/secure/sweep-log-client-sync` 上传日志正文;元数据仍来自 `/buff/secure/sweep-result`(ID/状态/时间)。
- #[tauri::command]
- pub async fn secure_buff_sweep_log(
- session_id: String,
- machine_code: String,
- last_log_count: Option<i64>,
- limit: Option<i64>,
- ) -> Result<String, String> {
- if session_id.trim().is_empty() || machine_code.trim().is_empty() {
- let resp = serde_json::json!({
- "code": 400,
- "msg": "请求参数错误,请检查后重试",
- "data": null
- });
- return serde_json::to_string(&resp).map_err(|e| format!("序列化失败: {}", e));
- }
- let config = AppConfig::load().map_err(|e| format!("加载配置失败: {}", e))?;
- let api_url = config.api.base_url.trim_end_matches('/');
- let sweep_result_data = {
- let request_data = serde_json::json!({
- "session_id": session_id.clone(),
- "machine_code": machine_code.clone(),
- "page": 1,
- "page_size": 1,
- });
- match secure_post_decrypt(api_url, "/api/v1/buff/secure/sweep-result", &request_data).await {
- Ok(v) => v,
- Err(e) => {
- warn!("[sweep-log] 获取 sweep-result 失败,将尝试仅本地日志: {}", e);
- serde_json::json!({ "code": 500 })
- }
- }
- };
- let mut request_id_opt: Option<i64> = None;
- let mut status_opt: Option<String> = None;
- let mut started_at_opt: Option<String> = None;
- let mut ended_at_opt: Option<String> = None;
- if sweep_result_data.get("code").and_then(|v| v.as_i64()) == Some(200) {
- if let Some(data) = sweep_result_data.get("data") {
- request_id_opt = data.get("request_id").and_then(|v| v.as_i64());
- status_opt = data.get("status").and_then(|v| v.as_str()).map(|s| s.to_string());
- started_at_opt = data.get("started_at").and_then(|v| v.as_str()).map(|s| s.to_string());
- ended_at_opt = data.get("ended_at").and_then(|v| v.as_str()).map(|s| s.to_string());
- }
- }
- if request_id_opt.is_none() {
- request_id_opt = discover_latest_local_sweep_request_id();
- }
- let Some(request_id) = request_id_opt else {
- let resp = serde_json::json!({
- "code": 200,
- "msg": "ok",
- "data": {
- "request_id": null,
- "status": null,
- "started_at": null,
- "ended_at": null,
- "log_count": 0,
- "logs": []
- }
- });
- return serde_json::to_string(&resp).map_err(|e| format!("序列化失败: {}", e));
- };
- if started_at_opt.is_none() {
- started_at_opt = read_local_sweep_meta_started_at(request_id);
- }
- let all_logs = read_local_sweep_logs_from_file(request_id).unwrap_or_default();
- let total_log_count = all_logs.len();
- let cursor = last_log_count.unwrap_or(-1);
- let mut logs = if cursor >= 0 {
- let start = if (cursor as usize) > total_log_count {
- 0
- } else {
- cursor as usize
- };
- all_logs[start..].to_vec()
- } else {
- all_logs
- };
- logs.reverse();
- let effective_limit = limit.unwrap_or(200).clamp(1, 500) as usize;
- if logs.len() > effective_limit {
- logs.truncate(effective_limit);
- }
- let resp = serde_json::json!({
- "code": 200,
- "msg": "ok",
- "data": {
- "request_id": request_id,
- "status": status_opt,
- "started_at": started_at_opt,
- "ended_at": ended_at_opt,
- "log_count": total_log_count as i64,
- "logs": logs
- }
- });
- serde_json::to_string(&resp).map_err(|e| format!("序列化失败: {}", e))
- }
- /// 纯本地读取扫货日志(不再调用后端 API)。
- /// 从 `steam-msg/{sweep_request_id}.txt` 读取日志,返回与 `secure_buff_sweep_log` 相同格式的 JSON。
- #[tauri::command]
- pub async fn read_local_sweep_logs(
- last_log_count: Option<i64>,
- limit: Option<i64>,
- ) -> Result<String, String> {
- // 调试信息(会返回给前端)
- let mut debug_info = serde_json::json!({});
- // 获取 steam-msg 目录路径
- let steam_msg_dir_path = match get_steam_msg_dir() {
- Ok(p) => p,
- Err(e) => {
- log::error!("[read_local_sweep_logs] 获取目录失败: {}", e);
- let resp = serde_json::json!({
- "code": 500,
- "msg": format!("获取目录失败: {}", e),
- "data": {
- "request_id": null,
- "status": null,
- "started_at": null,
- "ended_at": null,
- "log_count": 0,
- "logs": [],
- "debug": {
- "error": format!("get_steam_msg_dir failed: {}", e)
- }
- }
- });
- return serde_json::to_string(&resp).map_err(|e| format!("序列化失败: {}", e));
- }
- };
- debug_info["steam_msg_dir"] = serde_json::json!(steam_msg_dir_path.to_string_lossy().to_string());
- debug_info["steam_msg_dir_exists"] = serde_json::json!(steam_msg_dir_path.exists());
- if steam_msg_dir_path.exists() {
- debug_info["steam_msg_dir_is_dir"] = serde_json::json!(steam_msg_dir_path.is_dir());
- }
- log::info!("[read_local_sweep_logs] steam-msg 目录: {:?}", steam_msg_dir_path);
- let request_id = discover_latest_local_sweep_request_id();
- debug_info["discovered_request_id"] = serde_json::json!(request_id);
- log::info!("[read_local_sweep_logs] 发现的最新 request_id: {:?}", request_id);
- let Some(request_id) = request_id else {
- log::warn!("[read_local_sweep_logs] 未找到扫货日志文件");
- let resp = serde_json::json!({
- "code": 200,
- "msg": "ok",
- "data": {
- "request_id": null,
- "status": null,
- "started_at": null,
- "ended_at": null,
- "log_count": 0,
- "logs": [],
- "debug": debug_info
- }
- });
- return serde_json::to_string(&resp).map_err(|e| format!("序列化失败: {}", e));
- };
- let log_file_path = local_sweep_log_file_path(request_id)?;
- debug_info["log_file_path"] = serde_json::json!(log_file_path.to_string_lossy().to_string());
- debug_info["log_file_exists"] = serde_json::json!(log_file_path.exists());
- log::info!("[read_local_sweep_logs] 日志文件路径: {:?}", log_file_path);
- log::info!("[read_local_sweep_logs] 日志文件是否存在: {}", log_file_path.exists());
- if log_file_path.exists() {
- if let Ok(metadata) = fs::metadata(&log_file_path) {
- debug_info["log_file_size"] = serde_json::json!(metadata.len());
- log::info!("[read_local_sweep_logs] 日志文件大小: {} bytes", metadata.len());
- }
- }
- let started_at = read_local_sweep_meta_started_at(request_id);
- debug_info["started_at"] = serde_json::json!(started_at);
- let all_logs = read_local_sweep_logs_from_file(request_id).unwrap_or_default();
- debug_info["total_logs_parsed"] = serde_json::json!(all_logs.len());
- log::info!("[read_local_sweep_logs] 读取到的日志条数: {}", all_logs.len());
- let total_log_count = all_logs.len();
- let cursor = last_log_count.unwrap_or(-1);
- let mut logs = if cursor >= 0 {
- let start = if (cursor as usize) > total_log_count {
- 0
- } else {
- cursor as usize
- };
- all_logs[start..].to_vec()
- } else {
- all_logs
- };
- logs.reverse();
- let effective_limit = limit.unwrap_or(200).clamp(1, 500) as usize;
- if logs.len() > effective_limit {
- logs.truncate(effective_limit);
- }
- // status: 根据本地日志判断扫货是否进行中
- // 如果日志文件存在但没有 "ended" 相关记录,视为 running
- let has_ended = logs.iter().any(|l| {
- l.result == "failed" && l.message.contains("扫货已结束")
- || l.message.contains("扫货已完成")
- || l.message.contains("扫货已停止")
- });
- let status = if has_ended { "completed" } else { "running" };
- let ended_at_val = if has_ended { started_at.clone() } else { None::<String> };
- log::info!("[read_local_sweep_logs] 返回日志条数: {}", logs.len());
- let resp = serde_json::json!({
- "code": 200,
- "msg": "ok",
- "data": {
- "request_id": request_id,
- "status": status,
- "started_at": started_at,
- "ended_at": ended_at_val,
- "log_count": total_log_count as i64,
- "logs": logs,
- "debug": debug_info
- }
- });
- serde_json::to_string(&resp).map_err(|e| format!("序列化失败: {}", e))
- }
- /// 获取扫货 Worker 日志文件列表
- #[tauri::command]
- pub async fn get_sweep_worker_log_files(
- request_id: Option<i64>,
- ) -> Result<String, String> {
- let request_id = match request_id {
- Some(id) if id > 0 => id,
- _ => discover_latest_local_sweep_request_id()
- .ok_or_else(|| "未找到扫货记录".to_string())?,
- };
-
- let steam_msg_dir = get_steam_msg_dir()
- .map_err(|e| format!("获取目录失败: {}", e))?;
-
- // 查找所有匹配的日志文件 {request_id}-{n}.txt
- let pattern = format!("{}-", request_id);
- let mut worker_files: Vec<serde_json::Value> = Vec::new();
-
- if let Ok(entries) = std::fs::read_dir(&steam_msg_dir) {
- for entry in entries.flatten() {
- if let Some(name) = entry.file_name().to_str() {
- // 主日志文件: {request_id}-{worker_index}.txt
- // 例如: 306-1.txt
- if name.starts_with(&pattern) && name.ends_with(".txt") {
- let file_name_stem = name.trim_end_matches(".txt");
-
- // 排除备份文件(如果存在旧的备份文件)
- let parts: Vec<&str> = file_name_stem.split('-').collect();
- if parts.len() >= 3 {
- // 检查是否是旧格式的备份文件: {request_id}-{worker_index}-{backup_index}
- if parts[1].parse::<u32>().is_ok() && parts[2].parse::<u32>().is_ok() {
- // 这是旧备份文件,跳过
- continue;
- }
- }
-
- // 主日志文件: {request_id}-{worker_index}.txt
- if let Some(idx) = file_name_stem.strip_prefix(&pattern)
- .and_then(|s| s.parse::<u32>().ok())
- {
- let path = entry.path();
- let size = std::fs::metadata(&path)
- .map(|m| m.len())
- .unwrap_or(0);
- worker_files.push(serde_json::json!({
- "worker_index": idx,
- "file_name": name,
- "file_size": size,
- }));
- }
- }
- }
- }
- }
-
- // 按 worker 序号排序
- worker_files.sort_by_key(|v| v.get("worker_index").and_then(|x| x.as_u64()).unwrap_or(0));
-
- let resp = serde_json::json!({
- "code": 200,
- "msg": "ok",
- "data": {
- "request_id": request_id,
- "worker_count": worker_files.len(),
- "workers": worker_files,
- "rotation_threshold": 1000,
- "note": "每个 Worker 日志文件最多保留 1000 条记录,超出后自动清空",
- }
- });
-
- serde_json::to_string(&resp).map_err(|e| format!("序列化失败: {}", e))
- }
- /// 读取指定 Worker 的扫货日志
- #[tauri::command]
- pub async fn read_sweep_worker_log(
- request_id: i64,
- worker_index: u32,
- last_log_count: Option<i64>,
- limit: Option<i64>,
- ) -> Result<String, String> {
- let steam_msg_dir = get_steam_msg_dir()
- .map_err(|e| format!("获取目录失败: {}", e))?;
-
- let file_name = format!("{}-{}.txt", request_id, worker_index);
- let file_path = steam_msg_dir.join(&file_name);
-
- if !file_path.exists() {
- return Err(format!("Worker {} 日志文件不存在", worker_index));
- }
-
- // 读取文件内容
- let content = std::fs::read_to_string(&file_path)
- .map_err(|e| format!("读取日志文件失败: {}", e))?;
-
- // 解析日志条目
- let mut all_logs: Vec<LocalSweepLogEntry> = Vec::new();
- for line in content.lines() {
- let raw = line.trim();
- if raw.is_empty() {
- continue;
- }
-
- if let Ok(v) = serde_json::from_str::<serde_json::Value>(raw) {
- let entry = LocalSweepLogEntry {
- created_at: v.get("created_at").and_then(|x| x.as_str()).map(|s| s.to_string()),
- goods_id: v.get("goods_id").and_then(|x| x.as_str()).map(|s| s.to_string()).unwrap_or_default(),
- goods_name: v.get("goods_name").and_then(|x| x.as_str()).map(|s| s.to_string()).unwrap_or_default(),
- result: v.get("result").and_then(|x| x.as_str()).map(|s| s.to_string()).unwrap_or_default(),
- message: v.get("message").and_then(|x| x.as_str()).map(|s| s.to_string()).unwrap_or_default(),
- target_price_usd: v.get("target_price_usd").and_then(|x| x.as_f64()),
- purchase_price_usd: v.get("purchase_price_usd").and_then(|x| x.as_f64()),
- purchase_wear: v.get("purchase_wear").and_then(|x| x.as_f64()),
- listing_id: v.get("listing_id").and_then(|x| x.as_str()).map(|s| s.to_string()),
- line_color: v.get("line_color").and_then(|x| x.as_str()).map(|s| s.to_string()),
- diag_level: v.get("diag_level").and_then(|x| x.as_str()).map(|s| s.to_string()),
- api_name: v.get("api_name").and_then(|x| x.as_str()).map(|s| s.to_string()),
- steam_account: v.get("steam_account").and_then(|x| x.as_str()).map(|s| s.to_string()),
- method: v.get("method").and_then(|x| x.as_str()).map(|s| s.to_string()),
- url: v.get("url").and_then(|x| x.as_str()).map(|s| s.to_string()),
- error: v.get("error").and_then(|x| x.as_str()).map(|s| s.to_string()),
- elapsed_ms: v.get("elapsed_ms").and_then(|x| x.as_i64()).map(|x| x as u64),
- request_headers: None,
- request_body: None,
- response_status: v.get("response_status").and_then(|x| x.as_i64()).map(|x| x as u16),
- response_body: None,
- };
- all_logs.push(entry);
- }
- }
-
- hydrate_sweep_log_line_colors(&mut all_logs);
-
- let total_log_count = all_logs.len();
- let cursor = last_log_count.unwrap_or(-1);
- let logs: Vec<LocalSweepLogEntry> = if cursor >= 0 {
- let start = if (cursor as usize) > total_log_count {
- 0
- } else {
- cursor as usize
- };
- let lim = limit.unwrap_or(1000) as usize;
- all_logs[start..].iter().take(lim).cloned().collect()
- } else {
- let lim = limit.unwrap_or(1000) as usize;
- all_logs.iter().take(lim).cloned().collect()
- };
-
- let resp = serde_json::json!({
- "code": 200,
- "msg": "ok",
- "data": {
- "request_id": request_id,
- "worker_index": worker_index,
- "log_count": total_log_count as i64,
- "logs": logs,
- }
- });
-
- serde_json::to_string(&resp).map_err(|e| format!("序列化失败: {}", e))
- }
- /// 读取指定 Worker 的备份扫货日志
- #[tauri::command]
- pub async fn read_sweep_worker_backup_log(
- request_id: i64,
- worker_index: u32,
- backup_index: u32,
- last_log_count: Option<i64>,
- limit: Option<i64>,
- ) -> Result<String, String> {
- let steam_msg_dir = get_steam_msg_dir()
- .map_err(|e| format!("获取目录失败: {}", e))?;
-
- let file_name = format!("{}-{}-{}.txt", request_id, worker_index, backup_index);
- let file_path = steam_msg_dir.join(&file_name);
-
- if !file_path.exists() {
- return Err(format!("Worker {} 备份 {} 日志文件不存在", worker_index, backup_index));
- }
-
- // 读取文件内容
- let content = std::fs::read_to_string(&file_path)
- .map_err(|e| format!("读取日志文件失败: {}", e))?;
-
- // 解析日志条目
- let mut all_logs: Vec<LocalSweepLogEntry> = Vec::new();
- for line in content.lines() {
- let raw = line.trim();
- if raw.is_empty() {
- continue;
- }
-
- if let Ok(v) = serde_json::from_str::<serde_json::Value>(raw) {
- let entry = LocalSweepLogEntry {
- created_at: v.get("created_at").and_then(|x| x.as_str()).map(|s| s.to_string()),
- goods_id: v.get("goods_id").and_then(|x| x.as_str()).map(|s| s.to_string()).unwrap_or_default(),
- goods_name: v.get("goods_name").and_then(|x| x.as_str()).map(|s| s.to_string()).unwrap_or_default(),
- result: v.get("result").and_then(|x| x.as_str()).map(|s| s.to_string()).unwrap_or_default(),
- message: v.get("message").and_then(|x| x.as_str()).map(|s| s.to_string()).unwrap_or_default(),
- target_price_usd: v.get("target_price_usd").and_then(|x| x.as_f64()),
- purchase_price_usd: v.get("purchase_price_usd").and_then(|x| x.as_f64()),
- purchase_wear: v.get("purchase_wear").and_then(|x| x.as_f64()),
- listing_id: v.get("listing_id").and_then(|x| x.as_str()).map(|s| s.to_string()),
- line_color: v.get("line_color").and_then(|x| x.as_str()).map(|s| s.to_string()),
- diag_level: v.get("diag_level").and_then(|x| x.as_str()).map(|s| s.to_string()),
- api_name: v.get("api_name").and_then(|x| x.as_str()).map(|s| s.to_string()),
- steam_account: v.get("steam_account").and_then(|x| x.as_str()).map(|s| s.to_string()),
- method: v.get("method").and_then(|x| x.as_str()).map(|s| s.to_string()),
- url: v.get("url").and_then(|x| x.as_str()).map(|s| s.to_string()),
- error: v.get("error").and_then(|x| x.as_str()).map(|s| s.to_string()),
- elapsed_ms: v.get("elapsed_ms").and_then(|x| x.as_i64()).map(|x| x as u64),
- request_headers: None,
- request_body: None,
- response_status: v.get("response_status").and_then(|x| x.as_i64()).map(|x| x as u16),
- response_body: None,
- };
- all_logs.push(entry);
- }
- }
-
- hydrate_sweep_log_line_colors(&mut all_logs);
-
- let total_log_count = all_logs.len();
- let cursor = last_log_count.unwrap_or(-1);
- let logs: Vec<LocalSweepLogEntry> = if cursor >= 0 {
- let start = if (cursor as usize) > total_log_count {
- 0
- } else {
- cursor as usize
- };
- let lim = limit.unwrap_or(1000) as usize;
- all_logs[start..].iter().take(lim).cloned().collect()
- } else {
- let lim = limit.unwrap_or(1000) as usize;
- all_logs.iter().take(lim).cloned().collect()
- };
-
- let resp = serde_json::json!({
- "code": 200,
- "msg": "ok",
- "data": {
- "request_id": request_id,
- "worker_index": worker_index,
- "backup_index": backup_index,
- "log_count": total_log_count as i64,
- "logs": logs,
- "note": "这是历史备份日志",
- }
- });
-
- serde_json::to_string(&resp).map_err(|e| format!("序列化失败: {}", e))
- }
- // ============ 本地扫货成功数据存储(不再依赖后端数据库) ============
- // 数据结构:以 listing_id 为 key,便于记录同一饰品的多次购买(每次购买磨损不同)
- /// 扫货成功数据结构(以 listing_id 为 key)
- #[derive(Debug, Clone, Serialize, Deserialize)]
- struct LocalSweepSuccessItem {
- /// 扫货请求ID
- sweep_request_id: i64,
- /// 选品请求ID
- selection_request_id: i64,
- /// 选品记录ID
- selection_item_id: i64,
- /// 商品ID
- goods_id: String,
- /// 商品名称
- goods_name: String,
- /// 图标URL
- icon_url: Option<String>,
- /// 磨损区间
- wear_range: Option<String>,
- /// 购买时的磨损值
- purchase_wear: Option<f64>,
- /// 目标价格(USD)
- target_price_usd: Option<f64>,
- /// 成交价格(USD)
- purchase_price_usd: Option<f64>,
- /// 成交价格(人民币)
- purchase_price_cny: Option<f64>,
- /// 购买账号
- steam_account: Option<String>,
- /// Listing ID(唯一标识一次购买)
- listing_id: Option<String>,
- /// 创建时间
- created_at: String,
- }
- impl LocalSweepSuccessItem {
- fn new(
- sweep_request_id: i64,
- selection_request_id: i64,
- selection_item_id: i64,
- goods_id: String,
- goods_name: String,
- icon_url: Option<String>,
- wear_range: Option<String>,
- purchase_wear: Option<f64>,
- target_price_usd: Option<f64>,
- purchase_price_usd: Option<f64>,
- purchase_price_cny: Option<f64>,
- steam_account: Option<String>,
- listing_id: Option<String>,
- // 购买时间(由调用方在发起购买前生成,确保排序正确)
- created_at: Option<String>,
- ) -> Self {
- Self {
- sweep_request_id,
- selection_request_id,
- selection_item_id,
- goods_id,
- goods_name,
- icon_url,
- wear_range,
- purchase_wear,
- target_price_usd,
- purchase_price_usd,
- purchase_price_cny,
- steam_account,
- listing_id,
- created_at: created_at.unwrap_or_else(|| {
- chrono::Local::now().format("%Y-%m-%d %H:%M:%S").to_string()
- }),
- }
- }
- }
- /// 本地扫货成功数据外层结构
- #[derive(Debug, Clone, Serialize, Deserialize, Default)]
- struct LocalSweepSuccessData {
- /// 以 listing_id 为 key 的购买记录(同一饰品的多次购买各自独立记录)
- items: HashMap<String, LocalSweepSuccessItem>,
- /// 总数
- total: i64,
- }
- /// 获取扫货成功数据文件路径(按用户名存储)
- fn sweep_success_items_path() -> Result<PathBuf, String> {
- let username = get_current_username_internal().unwrap_or_else(|_| "guest".to_string());
- let steam_msg_dir = get_steam_msg_dir()?;
- let filename = format!("{}_sweep_success_items.json", sanitize_username_for_filename(&username));
- Ok(steam_msg_dir.join(filename))
- }
- /// 内部函数:获取当前用户名(不返回 Result)
- fn get_current_username_internal() -> Result<String, String> {
- let storage = get_storage()
- .map_err(|e| format!("获取存储失败: {}", e))?;
- match storage.get_user_info() {
- Ok(user_info) => Ok(user_info.username),
- Err(_) => Err("未找到用户信息".to_string()),
- }
- }
- /// 读取本地所有扫货成功数据(返回 Vec 格式供兼容性使用)
- fn load_all_local_sweep_success_items() -> Result<Vec<LocalSweepSuccessItem>, String> {
- let path = sweep_success_items_path()?;
- if !path.exists() {
- return Ok(Vec::new());
- }
- let content = fs::read_to_string(&path).map_err(|e| format!("读取扫货成功数据失败: {}", e))?;
- if content.trim().is_empty() {
- return Ok(Vec::new());
- }
- // 尝试解析新格式(items 为数组,按时间倒序存储)
- #[derive(Debug, Deserialize)]
- struct SweepSuccessDataArray {
- items: Vec<LocalSweepSuccessItem>,
- total: i64,
- }
- if let Ok(data) = serde_json::from_str::<SweepSuccessDataArray>(&content) {
- // 新格式:数组已按时间倒序,直接返回
- return Ok(data.items);
- }
- // 尝试解析旧格式(items 为 HashMap)
- if let Ok(data) = serde_json::from_str::<LocalSweepSuccessData>(&content) {
- let mut items: Vec<LocalSweepSuccessItem> = data.items.into_values().collect();
- items.sort_by(|a, b| b.created_at.cmp(&a.created_at));
- return Ok(items);
- }
- // 兼容最旧格式(纯数组)
- let items: Vec<LocalSweepSuccessItem> =
- serde_json::from_str(&content).map_err(|e| format!("解析扫货成功数据失败: {}", e))?;
- Ok(items)
- }
- /// 追加单条扫货成功数据到本地(以 listing_id 为 key,每次购买独立记录)
- fn append_local_sweep_success_item(item: LocalSweepSuccessItem) -> Result<(), String> {
- let path = sweep_success_items_path()?;
- let steam_msg_dir = get_steam_msg_dir()?;
- fs::create_dir_all(&steam_msg_dir).map_err(|e| format!("创建目录失败: {}", e))?;
- app_logger::debug(&format!(
- "[sweep] [DEBUG] 开始写入文件: path={}, listing_id={:?}",
- path.display(),
- item.listing_id
- ));
- // 获取文件锁,防止并发写入导致数据覆盖
- let _guard = get_sweep_success_file_lock()
- .lock()
- .map_err(|e| format!("获取文件锁失败: {}", e))?;
- // 读取现有数据
- let mut data = if path.exists() {
- let content = fs::read_to_string(&path).unwrap_or_default();
- app_logger::debug(&format!(
- "[sweep] [DEBUG] 读取现有文件成功, 内容长度={}",
- content.len()
- ));
- if content.trim().is_empty() {
- LocalSweepSuccessData::default()
- } else if let Ok(d) = serde_json::from_str::<LocalSweepSuccessData>(&content) {
- // 新格式(items 为 HashMap)
- d
- } else {
- // 尝试解析新数组格式 { "items": [...], "total": N }
- #[derive(Deserialize)]
- struct SweepSuccessDataArray {
- items: Vec<LocalSweepSuccessItem>,
- total: i64,
- }
- if let Ok(array_data) = serde_json::from_str::<SweepSuccessDataArray>(&content) {
- let mut map = HashMap::new();
- for i in array_data.items {
- let key = i.listing_id.clone().unwrap_or_else(|| i.goods_id.clone());
- map.insert(key, i);
- }
- LocalSweepSuccessData {
- items: map,
- total: array_data.total,
- }
- } else {
- // 旧格式数据(goods_id 为 key 或纯数组),转换为新格式(listing_id 为 key)
- if let Ok(items) = serde_json::from_str::<Vec<LocalSweepSuccessItem>>(&content) {
- let mut map = HashMap::new();
- for i in items {
- let key = i.listing_id.clone().unwrap_or_else(|| i.goods_id.clone());
- map.insert(key, i);
- }
- let total = map.len() as i64;
- LocalSweepSuccessData {
- items: map,
- total,
- }
- } else if let Ok(data_old) = serde_json::from_str::<LocalSweepSuccessData>(&content) {
- // 旧格式(goods_id 为 key),迁移时用 goods_id 作为 listing_id 兜底
- let mut map = HashMap::new();
- for (k, v) in data_old.items {
- let key = v.listing_id.clone().unwrap_or(k);
- map.insert(key, v);
- }
- let total = map.len() as i64;
- LocalSweepSuccessData {
- items: map,
- total,
- }
- } else {
- LocalSweepSuccessData::default()
- }
- }
- }
- } else {
- app_logger::debug("[sweep] [DEBUG] 文件不存在,将创建新文件");
- LocalSweepSuccessData::default()
- };
- // 使用 listing_id 作为 key 存储/更新(每次购买独立记录)
- let listing_id = item.listing_id.clone().unwrap_or_else(|| item.goods_id.clone());
- let prev_count = data.items.len();
- app_logger::debug(&format!(
- "[sweep] [DEBUG] 插入前: listing_id={}, prev_count={}, key_exists={}",
- listing_id,
- prev_count,
- data.items.contains_key(&listing_id)
- ));
- data.items.insert(listing_id.clone(), item.clone());
- let new_count = data.items.len();
- data.total = data.items.len() as i64;
- // 记录是否新增成功
- if new_count > prev_count {
- info!(
- "[sweep] 新增购买记录: listing_id={}, 当前总数={}",
- listing_id, new_count
- );
- } else {
- warn!(
- "[sweep] 更新购买记录: listing_id={}, 可能存在重复",
- listing_id
- );
- }
- // 按购买时间倒序排列后写入文件(最新在前)
- let content = serialize_sweep_success_data_ordered(&data)
- .map_err(|e| format!("序列化扫货成功数据失败: {}", e))?;
- app_logger::debug(&format!(
- "[sweep] [DEBUG] 序列化后内容长度={}",
- content.len()
- ));
- fs::write(&path, content).map_err(|e| format!("写入扫货成功数据失败: {}", e))?;
- app_logger::debug(&format!(
- "[sweep] [DEBUG] 文件写入成功: {} 条记录",
- new_count
- ));
- // 释放锁(_guard 在此作用域结束时自动释放)
- // 同时写入以 steam_account 为名称的文件
- if let Some(ref account) = item.steam_account {
- if !account.is_empty() {
- if let Err(e) = append_to_steam_account_file(account, &item) {
- error!("[sweep] 写入账号 {} 文件失败: {}", account, e);
- } else {
- info!("[sweep] 写入账号 {} 文件成功", account);
- }
- }
- }
- Ok(())
- }
- /// 获取以 Steam 账号为名称的扫货成功数据文件路径
- fn steam_account_sweep_items_path(account: &str) -> Result<PathBuf, String> {
- let steam_msg_dir = get_steam_msg_dir()?;
- let filename = format!("{}_sweep_items.json", sanitize_username_for_filename(account));
- Ok(steam_msg_dir.join(filename))
- }
- /// 追加数据到以 Steam 账号为名称的文件(以 listing_id 为 key)
- fn append_to_steam_account_file(account: &str, item: &LocalSweepSuccessItem) -> Result<(), String> {
- let path = steam_account_sweep_items_path(account)?;
- let steam_msg_dir = get_steam_msg_dir()?;
- fs::create_dir_all(&steam_msg_dir).map_err(|e| format!("创建目录失败: {}", e))?;
- debug!(
- "[sweep] [account_file] 开始写入: account={}, listing_id={:?}",
- account,
- item.listing_id
- );
- // 读取现有数据
- let mut data = if path.exists() {
- let content = fs::read_to_string(&path).unwrap_or_default();
- debug!(
- "[sweep] [account_file] 读取现有文件成功: {} bytes",
- content.len()
- );
- if content.trim().is_empty() {
- LocalSweepSuccessData::default()
- } else if let Ok(d) = serde_json::from_str::<LocalSweepSuccessData>(&content) {
- // 新格式(items 为 HashMap)
- d
- } else {
- // 尝试解析新数组格式 { "items": [...], "total": N }
- #[derive(Deserialize)]
- struct SweepSuccessDataArray {
- items: Vec<LocalSweepSuccessItem>,
- total: i64,
- }
- if let Ok(array_data) = serde_json::from_str::<SweepSuccessDataArray>(&content) {
- let mut map = HashMap::new();
- for i in array_data.items {
- let key = i.listing_id.clone().unwrap_or_else(|| i.goods_id.clone());
- map.insert(key, i);
- }
- LocalSweepSuccessData {
- items: map,
- total: array_data.total,
- }
- } else {
- // 旧格式数据(goods_id 为 key 或纯数组),转换为新格式(listing_id 为 key)
- if let Ok(items) = serde_json::from_str::<Vec<LocalSweepSuccessItem>>(&content) {
- let mut map = HashMap::new();
- for i in items {
- let key = i.listing_id.clone().unwrap_or_else(|| i.goods_id.clone());
- map.insert(key, i);
- }
- let total = map.len() as i64;
- LocalSweepSuccessData {
- items: map,
- total,
- }
- } else if let Ok(data_old) = serde_json::from_str::<LocalSweepSuccessData>(&content) {
- let mut map = HashMap::new();
- for (k, v) in data_old.items {
- let key = v.listing_id.clone().unwrap_or(k);
- map.insert(key, v);
- }
- let total = map.len() as i64;
- LocalSweepSuccessData {
- items: map,
- total,
- }
- } else {
- LocalSweepSuccessData::default()
- }
- }
- }
- } else {
- debug!("[sweep] [account_file] 文件不存在,将创建新文件");
- LocalSweepSuccessData::default()
- };
- // 使用 listing_id 作为 key 存储/更新(每次购买独立记录)
- let listing_id = item.listing_id.clone().unwrap_or_else(|| item.goods_id.clone());
- let prev_count = data.items.len();
- debug!(
- "[sweep] [account_file] 插入前: listing_id={}, prev_count={}, key_exists={}",
- listing_id,
- prev_count,
- data.items.contains_key(&listing_id)
- );
- data.items.insert(listing_id.clone(), item.clone());
- let new_count = data.items.len();
- data.total = data.items.len() as i64;
- if new_count > prev_count {
- info!(
- "[sweep] [account_file] 新增记录: account={}, listing_id={}, 当前总数={}",
- account, listing_id, new_count
- );
- } else {
- warn!(
- "[sweep] [account_file] 更新记录: account={}, listing_id={}",
- account, listing_id
- );
- }
- // 按购买时间倒序排列后写入文件(最新在前)
- let content = serialize_sweep_success_data_ordered(&data)
- .map_err(|e| format!("序列化数据失败: {}", e))?;
- fs::write(&path, content).map_err(|e| format!("写入账号文件失败: {}", e))?;
- debug!(
- "[sweep] [account_file] 写入成功: {} 条记录",
- new_count
- );
- Ok(())
- }
- /// 将扫货成功数据序列化为按购买时间倒序的格式(最新在前)
- fn serialize_sweep_success_data_ordered(data: &LocalSweepSuccessData) -> Result<String, String> {
- // 将 HashMap 转换为 Vec 并按 created_at 倒序排列(最新在前)
- let mut items_vec: Vec<_> = data.items.iter().collect();
- items_vec.sort_by(|a, b| b.1.created_at.cmp(&a.1.created_at));
- // 提取排序后的数据(只取 value,忽略 key)
- let ordered_items: Vec<&LocalSweepSuccessItem> = items_vec.into_iter().map(|(_, v)| v).collect();
- // 使用数组格式输出(数组能保证顺序)
- #[derive(Serialize)]
- struct SweepSuccessDataArray<'a> {
- items: Vec<&'a LocalSweepSuccessItem>,
- total: i64,
- }
- let array_data = SweepSuccessDataArray {
- items: ordered_items,
- total: data.total,
- };
- serde_json::to_string_pretty(&array_data).map_err(|e| format!("JSON 序列化失败: {}", e))
- }
- /// 读取本地扫货成功数据(分页)
- #[tauri::command]
- pub async fn read_local_sweep_success_items(
- page: Option<i64>,
- page_size: Option<i64>,
- ) -> Result<String, String> {
- let page = page.unwrap_or(1).max(1);
- let page_size = page_size.unwrap_or(10).clamp(1, 100);
- let all_items = load_all_local_sweep_success_items().unwrap_or_default();
- // 按时间倒序
- let total = all_items.len() as i64;
- let start = ((page - 1) * page_size) as usize;
- let end = (start + page_size as usize).min(all_items.len());
- let items: Vec<LocalSweepSuccessItem> = if start < all_items.len() {
- all_items[start..end].to_vec()
- } else {
- Vec::new()
- };
- let resp = serde_json::json!({
- "code": 200,
- "msg": "ok",
- "data": {
- "page": page,
- "page_size": page_size,
- "total": total,
- "items": items
- }
- });
- serde_json::to_string(&resp).map_err(|e| format!("序列化失败: {}", e))
- }
- /// 添加本地扫货成功数据(供外部调用)
- #[tauri::command]
- pub async fn add_local_sweep_success_item(item_json: String) -> Result<String, String> {
- let item: LocalSweepSuccessItem =
- serde_json::from_str(&item_json).map_err(|e| format!("解析扫货成功数据失败: {}", e))?;
- append_local_sweep_success_item(item)?;
- Ok(serde_json::json!({"code": 200, "msg": "添加成功"}).to_string())
- }
- /// 添加演示用的扫货成功数据
- #[tauri::command]
- pub async fn add_demo_sweep_success_item() -> Result<String, String> {
- let demo_item = LocalSweepSuccessItem::new(
- 1, // sweep_request_id
- 1, // selection_request_id
- 1, // selection_item_id
- "12345".to_string(), // goods_id
- "AK-47 | 血腥运动 (略有磨损)".to_string(), // goods_name
- Some("https://community.steamart.io/skins/730/ak47_bloodsport_in_field-worn.webp".to_string()), // icon_url
- Some("略有磨损".to_string()), // wear_range
- Some(0.12345678), // purchase_wear
- Some(10.50), // target_price_usd
- Some(9.80), // purchase_price_usd
- Some(73.50), // purchase_price_cny (9.80 * 7.5)
- Some("demo_steam_account".to_string()), // steam_account
- Some("999999999999999999".to_string()), // listing_id
- None, // created_at(演示数据使用自动生成的时间)
- );
- append_local_sweep_success_item(demo_item)?;
- info!("[demo] 添加演示扫货成功数据成功");
- Ok(serde_json::json!({"code": 200, "msg": "演示数据添加成功"}).to_string())
- }
- /// 异步追加扫货成功数据(供扫货进程内部调用)
- pub async fn append_sweep_success_item_async(
- sweep_request_id: i64,
- selection_request_id: i64,
- selection_item_id: i64,
- goods_id: String,
- goods_name: String,
- icon_url: Option<String>,
- wear_range: Option<String>,
- purchase_wear: Option<f64>,
- target_price_usd: Option<f64>,
- purchase_price_usd: Option<f64>,
- purchase_price_cny: Option<f64>,
- steam_account: Option<String>,
- listing_id: Option<String>,
- // 购买时间(由调用方在发起购买前生成,确保排序正确)
- created_at: Option<String>,
- ) {
- let item = LocalSweepSuccessItem::new(
- sweep_request_id,
- selection_request_id,
- selection_item_id,
- goods_id,
- goods_name,
- icon_url,
- wear_range,
- purchase_wear,
- target_price_usd,
- purchase_price_usd,
- purchase_price_cny,
- steam_account,
- listing_id,
- created_at,
- );
- if let Err(e) = append_local_sweep_success_item(item) {
- app_logger::error(&format!("[sweep] 写入本地扫货成功数据失败: {}", e));
- } else {
- app_logger::info("[sweep] 写入本地扫货成功数据成功");
- }
- }
- /// 读取指定 Steam 账号的扫货成功数据(从 {account}_sweep_items.json)
- #[tauri::command]
- pub async fn read_steam_account_sweep_items(
- account: String,
- ) -> Result<String, String> {
- let path = steam_account_sweep_items_path(&account)?;
- if !path.exists() {
- let resp = serde_json::json!({
- "code": 200,
- "msg": "ok",
- "data": {
- "items": [],
- "total": 0
- }
- });
- return serde_json::to_string(&resp).map_err(|e| format!("序列化失败: {}", e));
- }
- let content = fs::read_to_string(&path).map_err(|e| format!("读取文件失败: {}", e))?;
- if content.trim().is_empty() {
- let resp = serde_json::json!({
- "code": 200,
- "msg": "ok",
- "data": {
- "items": [],
- "total": 0
- }
- });
- return serde_json::to_string(&resp).map_err(|e| format!("序列化失败: {}", e));
- }
- // 尝试解析新格式(items 为数组,按时间倒序存储)
- #[derive(Debug, Deserialize)]
- struct SweepSuccessDataArray {
- items: Vec<LocalSweepSuccessItem>,
- total: i64,
- }
- if let Ok(data) = serde_json::from_str::<SweepSuccessDataArray>(&content) {
- // 新格式:数组已按时间倒序,直接使用
- let resp = serde_json::json!({
- "code": 200,
- "msg": "ok",
- "data": {
- "items": data.items,
- "total": data.total
- }
- });
- return serde_json::to_string(&resp).map_err(|e| format!("序列化失败: {}", e));
- }
- // 尝试解析旧格式(items 为 HashMap)
- if let Ok(data) = serde_json::from_str::<LocalSweepSuccessData>(&content) {
- let mut items: Vec<LocalSweepSuccessItem> = data.items.into_values().collect();
- items.sort_by(|a, b| b.created_at.cmp(&a.created_at));
- let resp = serde_json::json!({
- "code": 200,
- "msg": "ok",
- "data": {
- "items": items,
- "total": data.total
- }
- });
- return serde_json::to_string(&resp).map_err(|e| format!("序列化失败: {}", e));
- }
- // 兼容旧格式(纯数组)
- let items: Vec<LocalSweepSuccessItem> =
- serde_json::from_str(&content).map_err(|e| format!("解析数据失败: {}", e))?;
- let total = items.len() as i64;
- let resp = serde_json::json!({
- "code": 200,
- "msg": "ok",
- "data": {
- "items": items,
- "total": total
- }
- });
- serde_json::to_string(&resp).map_err(|e| format!("序列化失败: {}", e))
- }
- /// 获取应用日志文件路径(exe 同目录下 logs/app_YYYY-MM-DD.log)
- #[tauri::command]
- pub async fn get_app_log_path() -> Result<String, String> {
- crate::app_logger::get_current_log_path()
- .ok_or_else(|| "日志系统未初始化".to_string())
- }
|