ResourceController.cs 452 KB

12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485868788899091929394959697989910010110210310410510610710810911011111211311411511611711811912012112212312412512612712812913013113213313413513613713813914014114214314414514614714814915015115215315415515615715815916016116216316416516616716816917017117217317417517617717817918018118218318418518618718818919019119219319419519619719819920020120220320420520620720820921021121221321421521621721821922022122222322422522622722822923023123223323423523623723823924024124224324424524624724824925025125225325425525625725825926026126226326426526626726826927027127227327427527627727827928028128228328428528628728828929029129229329429529629729829930030130230330430530630730830931031131231331431531631731831932032132232332432532632732832933033133233333433533633733833934034134234334434534634734834935035135235335435535635735835936036136236336436536636736836937037137237337437537637737837938038138238338438538638738838939039139239339439539639739839940040140240340440540640740840941041141241341441541641741841942042142242342442542642742842943043143243343443543643743843944044144244344444544644744844945045145245345445545645745845946046146246346446546646746846947047147247347447547647747847948048148248348448548648748848949049149249349449549649749849950050150250350450550650750850951051151251351451551651751851952052152252352452552652752852953053153253353453553653753853954054154254354454554654754854955055155255355455555655755855956056156256356456556656756856957057157257357457557657757857958058158258358458558658758858959059159259359459559659759859960060160260360460560660760860961061161261361461561661761861962062162262362462562662762862963063163263363463563663763863964064164264364464564664764864965065165265365465565665765865966066166266366466566666766866967067167267367467567667767867968068168268368468568668768868969069169269369469569669769869970070170270370470570670770870971071171271371471571671771871972072172272372472572672772872973073173273373473573673773873974074174274374474574674774874975075175275375475575675775875976076176276376476576676776876977077177277377477577677777877978078178278378478578678778878979079179279379479579679779879980080180280380480580680780880981081181281381481581681781881982082182282382482582682782882983083183283383483583683783883984084184284384484584684784884985085185285385485585685785885986086186286386486586686786886987087187287387487587687787887988088188288388488588688788888989089189289389489589689789889990090190290390490590690790890991091191291391491591691791891992092192292392492592692792892993093193293393493593693793893994094194294394494594694794894995095195295395495595695795895996096196296396496596696796896997097197297397497597697797897998098198298398498598698798898999099199299399499599699799899910001001100210031004100510061007100810091010101110121013101410151016101710181019102010211022102310241025102610271028102910301031103210331034103510361037103810391040104110421043104410451046104710481049105010511052105310541055105610571058105910601061106210631064106510661067106810691070107110721073107410751076107710781079108010811082108310841085108610871088108910901091109210931094109510961097109810991100110111021103110411051106110711081109111011111112111311141115111611171118111911201121112211231124112511261127112811291130113111321133113411351136113711381139114011411142114311441145114611471148114911501151115211531154115511561157115811591160116111621163116411651166116711681169117011711172117311741175117611771178117911801181118211831184118511861187118811891190119111921193119411951196119711981199120012011202120312041205120612071208120912101211121212131214121512161217121812191220122112221223122412251226122712281229123012311232123312341235123612371238123912401241124212431244124512461247124812491250125112521253125412551256125712581259126012611262126312641265126612671268126912701271127212731274127512761277127812791280128112821283128412851286128712881289129012911292129312941295129612971298129913001301130213031304130513061307130813091310131113121313131413151316131713181319132013211322132313241325132613271328132913301331133213331334133513361337133813391340134113421343134413451346134713481349135013511352135313541355135613571358135913601361136213631364136513661367136813691370137113721373137413751376137713781379138013811382138313841385138613871388138913901391139213931394139513961397139813991400140114021403140414051406140714081409141014111412141314141415141614171418141914201421142214231424142514261427142814291430143114321433143414351436143714381439144014411442144314441445144614471448144914501451145214531454145514561457145814591460146114621463146414651466146714681469147014711472147314741475147614771478147914801481148214831484148514861487148814891490149114921493149414951496149714981499150015011502150315041505150615071508150915101511151215131514151515161517151815191520152115221523152415251526152715281529153015311532153315341535153615371538153915401541154215431544154515461547154815491550155115521553155415551556155715581559156015611562156315641565156615671568156915701571157215731574157515761577157815791580158115821583158415851586158715881589159015911592159315941595159615971598159916001601160216031604160516061607160816091610161116121613161416151616161716181619162016211622162316241625162616271628162916301631163216331634163516361637163816391640164116421643164416451646164716481649165016511652165316541655165616571658165916601661166216631664166516661667166816691670167116721673167416751676167716781679168016811682168316841685168616871688168916901691169216931694169516961697169816991700170117021703170417051706170717081709171017111712171317141715171617171718171917201721172217231724172517261727172817291730173117321733173417351736173717381739174017411742174317441745174617471748174917501751175217531754175517561757175817591760176117621763176417651766176717681769177017711772177317741775177617771778177917801781178217831784178517861787178817891790179117921793179417951796179717981799180018011802180318041805180618071808180918101811181218131814181518161817181818191820182118221823182418251826182718281829183018311832183318341835183618371838183918401841184218431844184518461847184818491850185118521853185418551856185718581859186018611862186318641865186618671868186918701871187218731874187518761877187818791880188118821883188418851886188718881889189018911892189318941895189618971898189919001901190219031904190519061907190819091910191119121913191419151916191719181919192019211922192319241925192619271928192919301931193219331934193519361937193819391940194119421943194419451946194719481949195019511952195319541955195619571958195919601961196219631964196519661967196819691970197119721973197419751976197719781979198019811982198319841985198619871988198919901991199219931994199519961997199819992000200120022003200420052006200720082009201020112012201320142015201620172018201920202021202220232024202520262027202820292030203120322033203420352036203720382039204020412042204320442045204620472048204920502051205220532054205520562057205820592060206120622063206420652066206720682069207020712072207320742075207620772078207920802081208220832084208520862087208820892090209120922093209420952096209720982099210021012102210321042105210621072108210921102111211221132114211521162117211821192120212121222123212421252126212721282129213021312132213321342135213621372138213921402141214221432144214521462147214821492150215121522153215421552156215721582159216021612162216321642165216621672168216921702171217221732174217521762177217821792180218121822183218421852186218721882189219021912192219321942195219621972198219922002201220222032204220522062207220822092210221122122213221422152216221722182219222022212222222322242225222622272228222922302231223222332234223522362237223822392240224122422243224422452246224722482249225022512252225322542255225622572258225922602261226222632264226522662267226822692270227122722273227422752276227722782279228022812282228322842285228622872288228922902291229222932294229522962297229822992300230123022303230423052306230723082309231023112312231323142315231623172318231923202321232223232324232523262327232823292330233123322333233423352336233723382339234023412342234323442345234623472348234923502351235223532354235523562357235823592360236123622363236423652366236723682369237023712372237323742375237623772378237923802381238223832384238523862387238823892390239123922393239423952396239723982399240024012402240324042405240624072408240924102411241224132414241524162417241824192420242124222423242424252426242724282429243024312432243324342435243624372438243924402441244224432444244524462447244824492450245124522453245424552456245724582459246024612462246324642465246624672468246924702471247224732474247524762477247824792480248124822483248424852486248724882489249024912492249324942495249624972498249925002501250225032504250525062507250825092510251125122513251425152516251725182519252025212522252325242525252625272528252925302531253225332534253525362537253825392540254125422543254425452546254725482549255025512552255325542555255625572558255925602561256225632564256525662567256825692570257125722573257425752576257725782579258025812582258325842585258625872588258925902591259225932594259525962597259825992600260126022603260426052606260726082609261026112612261326142615261626172618261926202621262226232624262526262627262826292630263126322633263426352636263726382639264026412642264326442645264626472648264926502651265226532654265526562657265826592660266126622663266426652666266726682669267026712672267326742675267626772678267926802681268226832684268526862687268826892690269126922693269426952696269726982699270027012702270327042705270627072708270927102711271227132714271527162717271827192720272127222723272427252726272727282729273027312732273327342735273627372738273927402741274227432744274527462747274827492750275127522753275427552756275727582759276027612762276327642765276627672768276927702771277227732774277527762777277827792780278127822783278427852786278727882789279027912792279327942795279627972798279928002801280228032804280528062807280828092810281128122813281428152816281728182819282028212822282328242825282628272828282928302831283228332834283528362837283828392840284128422843284428452846284728482849285028512852285328542855285628572858285928602861286228632864286528662867286828692870287128722873287428752876287728782879288028812882288328842885288628872888288928902891289228932894289528962897289828992900290129022903290429052906290729082909291029112912291329142915291629172918291929202921292229232924292529262927292829292930293129322933293429352936293729382939294029412942294329442945294629472948294929502951295229532954295529562957295829592960296129622963296429652966296729682969297029712972297329742975297629772978297929802981298229832984298529862987298829892990299129922993299429952996299729982999300030013002300330043005300630073008300930103011301230133014301530163017301830193020302130223023302430253026302730283029303030313032303330343035303630373038303930403041304230433044304530463047304830493050305130523053305430553056305730583059306030613062306330643065306630673068306930703071307230733074307530763077307830793080308130823083308430853086308730883089309030913092309330943095309630973098309931003101310231033104310531063107310831093110311131123113311431153116311731183119312031213122312331243125312631273128312931303131313231333134313531363137313831393140314131423143314431453146314731483149315031513152315331543155315631573158315931603161316231633164316531663167316831693170317131723173317431753176317731783179318031813182318331843185318631873188318931903191319231933194319531963197319831993200320132023203320432053206320732083209321032113212321332143215321632173218321932203221322232233224322532263227322832293230323132323233323432353236323732383239324032413242324332443245324632473248324932503251325232533254325532563257325832593260326132623263326432653266326732683269327032713272327332743275327632773278327932803281328232833284328532863287328832893290329132923293329432953296329732983299330033013302330333043305330633073308330933103311331233133314331533163317331833193320332133223323332433253326332733283329333033313332333333343335333633373338333933403341334233433344334533463347334833493350335133523353335433553356335733583359336033613362336333643365336633673368336933703371337233733374337533763377337833793380338133823383338433853386338733883389339033913392339333943395339633973398339934003401340234033404340534063407340834093410341134123413341434153416341734183419342034213422342334243425342634273428342934303431343234333434343534363437343834393440344134423443344434453446344734483449345034513452345334543455345634573458345934603461346234633464346534663467346834693470347134723473347434753476347734783479348034813482348334843485348634873488348934903491349234933494349534963497349834993500350135023503350435053506350735083509351035113512351335143515351635173518351935203521352235233524352535263527352835293530353135323533353435353536353735383539354035413542354335443545354635473548354935503551355235533554355535563557355835593560356135623563356435653566356735683569357035713572357335743575357635773578357935803581358235833584358535863587358835893590359135923593359435953596359735983599360036013602360336043605360636073608360936103611361236133614361536163617361836193620362136223623362436253626362736283629363036313632363336343635363636373638363936403641364236433644364536463647364836493650365136523653365436553656365736583659366036613662366336643665366636673668366936703671367236733674367536763677367836793680368136823683368436853686368736883689369036913692369336943695369636973698369937003701370237033704370537063707370837093710371137123713371437153716371737183719372037213722372337243725372637273728372937303731373237333734373537363737373837393740374137423743374437453746374737483749375037513752375337543755375637573758375937603761376237633764376537663767376837693770377137723773377437753776377737783779378037813782378337843785378637873788378937903791379237933794379537963797379837993800380138023803380438053806380738083809381038113812381338143815381638173818381938203821382238233824382538263827382838293830383138323833383438353836383738383839384038413842384338443845384638473848384938503851385238533854385538563857385838593860386138623863386438653866386738683869387038713872387338743875387638773878387938803881388238833884388538863887388838893890389138923893389438953896389738983899390039013902390339043905390639073908390939103911391239133914391539163917391839193920392139223923392439253926392739283929393039313932393339343935393639373938393939403941394239433944394539463947394839493950395139523953395439553956395739583959396039613962396339643965396639673968396939703971397239733974397539763977397839793980398139823983398439853986398739883989399039913992399339943995399639973998399940004001400240034004400540064007400840094010401140124013401440154016401740184019402040214022402340244025402640274028402940304031403240334034403540364037403840394040404140424043404440454046404740484049405040514052405340544055405640574058405940604061406240634064406540664067406840694070407140724073407440754076407740784079408040814082408340844085408640874088408940904091409240934094409540964097409840994100410141024103410441054106410741084109411041114112411341144115411641174118411941204121412241234124412541264127412841294130413141324133413441354136413741384139414041414142414341444145414641474148414941504151415241534154415541564157415841594160416141624163416441654166416741684169417041714172417341744175417641774178417941804181418241834184418541864187418841894190419141924193419441954196419741984199420042014202420342044205420642074208420942104211421242134214421542164217421842194220422142224223422442254226422742284229423042314232423342344235423642374238423942404241424242434244424542464247424842494250425142524253425442554256425742584259426042614262426342644265426642674268426942704271427242734274427542764277427842794280428142824283428442854286428742884289429042914292429342944295429642974298429943004301430243034304430543064307430843094310431143124313431443154316431743184319432043214322432343244325432643274328432943304331433243334334433543364337433843394340434143424343434443454346434743484349435043514352435343544355435643574358435943604361436243634364436543664367436843694370437143724373437443754376437743784379438043814382438343844385438643874388438943904391439243934394439543964397439843994400440144024403440444054406440744084409441044114412441344144415441644174418441944204421442244234424442544264427442844294430443144324433443444354436443744384439444044414442444344444445444644474448444944504451445244534454445544564457445844594460446144624463446444654466446744684469447044714472447344744475447644774478447944804481448244834484448544864487448844894490449144924493449444954496449744984499450045014502450345044505450645074508450945104511451245134514451545164517451845194520452145224523452445254526452745284529453045314532453345344535453645374538453945404541454245434544454545464547454845494550455145524553455445554556455745584559456045614562456345644565456645674568456945704571457245734574457545764577457845794580458145824583458445854586458745884589459045914592459345944595459645974598459946004601460246034604460546064607460846094610461146124613461446154616461746184619462046214622462346244625462646274628462946304631463246334634463546364637463846394640464146424643464446454646464746484649465046514652465346544655465646574658465946604661466246634664466546664667466846694670467146724673467446754676467746784679468046814682468346844685468646874688468946904691469246934694469546964697469846994700470147024703470447054706470747084709471047114712471347144715471647174718471947204721472247234724472547264727472847294730473147324733473447354736473747384739474047414742474347444745474647474748474947504751475247534754475547564757475847594760476147624763476447654766476747684769477047714772477347744775477647774778477947804781478247834784478547864787478847894790479147924793479447954796479747984799480048014802480348044805480648074808480948104811481248134814481548164817481848194820482148224823482448254826482748284829483048314832483348344835483648374838483948404841484248434844484548464847484848494850485148524853485448554856485748584859486048614862486348644865486648674868486948704871487248734874487548764877487848794880488148824883488448854886488748884889489048914892489348944895489648974898489949004901490249034904490549064907490849094910491149124913491449154916491749184919492049214922492349244925492649274928492949304931493249334934493549364937493849394940494149424943494449454946494749484949495049514952495349544955495649574958495949604961496249634964496549664967496849694970497149724973497449754976497749784979498049814982498349844985498649874988498949904991499249934994499549964997499849995000500150025003500450055006500750085009501050115012501350145015501650175018501950205021502250235024502550265027502850295030503150325033503450355036503750385039504050415042504350445045504650475048504950505051505250535054505550565057505850595060506150625063506450655066506750685069507050715072507350745075507650775078507950805081508250835084508550865087508850895090509150925093509450955096509750985099510051015102510351045105510651075108510951105111511251135114511551165117511851195120512151225123512451255126512751285129513051315132513351345135513651375138513951405141514251435144514551465147514851495150515151525153515451555156515751585159516051615162516351645165516651675168516951705171517251735174517551765177517851795180518151825183518451855186518751885189519051915192519351945195519651975198519952005201520252035204520552065207520852095210521152125213521452155216521752185219522052215222522352245225522652275228522952305231523252335234523552365237523852395240524152425243524452455246524752485249525052515252525352545255525652575258525952605261526252635264526552665267526852695270527152725273527452755276527752785279528052815282528352845285528652875288528952905291529252935294529552965297529852995300530153025303530453055306530753085309531053115312531353145315531653175318531953205321532253235324532553265327532853295330533153325333533453355336533753385339534053415342534353445345534653475348534953505351535253535354535553565357535853595360536153625363536453655366536753685369537053715372537353745375537653775378537953805381538253835384538553865387538853895390539153925393539453955396539753985399540054015402540354045405540654075408540954105411541254135414541554165417541854195420542154225423542454255426542754285429543054315432543354345435543654375438543954405441544254435444544554465447544854495450545154525453545454555456545754585459546054615462546354645465546654675468546954705471547254735474547554765477547854795480548154825483548454855486548754885489549054915492549354945495549654975498549955005501550255035504550555065507550855095510551155125513551455155516551755185519552055215522552355245525552655275528552955305531553255335534553555365537553855395540554155425543554455455546554755485549555055515552555355545555555655575558555955605561556255635564556555665567556855695570557155725573557455755576557755785579558055815582558355845585558655875588558955905591559255935594559555965597559855995600560156025603560456055606560756085609561056115612561356145615561656175618561956205621562256235624562556265627562856295630563156325633563456355636563756385639564056415642564356445645564656475648564956505651565256535654565556565657565856595660566156625663566456655666566756685669567056715672567356745675567656775678567956805681568256835684568556865687568856895690569156925693569456955696569756985699570057015702570357045705570657075708570957105711571257135714571557165717571857195720572157225723572457255726572757285729573057315732573357345735573657375738573957405741574257435744574557465747574857495750575157525753575457555756575757585759576057615762576357645765576657675768576957705771577257735774577557765777577857795780578157825783578457855786578757885789579057915792579357945795579657975798579958005801580258035804580558065807580858095810581158125813581458155816581758185819582058215822582358245825582658275828582958305831583258335834583558365837583858395840584158425843584458455846584758485849585058515852585358545855585658575858585958605861586258635864586558665867586858695870587158725873587458755876587758785879588058815882588358845885588658875888588958905891589258935894589558965897589858995900590159025903590459055906590759085909591059115912591359145915591659175918591959205921592259235924592559265927592859295930593159325933593459355936593759385939594059415942594359445945594659475948594959505951595259535954595559565957595859595960596159625963596459655966596759685969597059715972597359745975597659775978597959805981598259835984598559865987598859895990599159925993599459955996599759985999600060016002600360046005600660076008600960106011601260136014601560166017601860196020602160226023602460256026602760286029603060316032603360346035603660376038603960406041604260436044604560466047604860496050605160526053605460556056605760586059606060616062606360646065606660676068606960706071607260736074607560766077607860796080608160826083608460856086608760886089609060916092609360946095609660976098609961006101610261036104610561066107610861096110611161126113611461156116611761186119612061216122612361246125612661276128612961306131613261336134613561366137613861396140614161426143614461456146614761486149615061516152615361546155615661576158615961606161616261636164616561666167616861696170617161726173617461756176617761786179618061816182618361846185618661876188618961906191619261936194619561966197619861996200620162026203620462056206620762086209621062116212621362146215621662176218621962206221622262236224622562266227622862296230623162326233623462356236623762386239624062416242624362446245624662476248624962506251625262536254625562566257625862596260626162626263626462656266626762686269627062716272627362746275627662776278627962806281628262836284628562866287628862896290629162926293629462956296629762986299630063016302630363046305630663076308630963106311631263136314631563166317631863196320632163226323632463256326632763286329633063316332633363346335633663376338633963406341634263436344634563466347634863496350635163526353635463556356635763586359636063616362636363646365636663676368636963706371637263736374637563766377637863796380638163826383638463856386638763886389639063916392639363946395639663976398639964006401640264036404640564066407640864096410641164126413641464156416641764186419642064216422642364246425642664276428642964306431643264336434643564366437643864396440644164426443644464456446644764486449645064516452645364546455645664576458645964606461646264636464646564666467646864696470647164726473647464756476647764786479648064816482648364846485648664876488648964906491649264936494649564966497649864996500650165026503650465056506650765086509651065116512651365146515651665176518651965206521652265236524652565266527652865296530653165326533653465356536653765386539654065416542654365446545654665476548654965506551655265536554655565566557655865596560656165626563656465656566656765686569657065716572657365746575657665776578657965806581658265836584658565866587658865896590659165926593659465956596659765986599660066016602660366046605660666076608660966106611661266136614661566166617661866196620662166226623662466256626662766286629663066316632663366346635663666376638663966406641664266436644664566466647664866496650665166526653665466556656665766586659666066616662666366646665666666676668666966706671667266736674667566766677667866796680668166826683668466856686668766886689669066916692669366946695669666976698669967006701670267036704670567066707670867096710671167126713671467156716671767186719672067216722672367246725672667276728672967306731673267336734673567366737673867396740674167426743674467456746674767486749675067516752675367546755675667576758675967606761676267636764676567666767676867696770677167726773677467756776677767786779678067816782678367846785678667876788678967906791679267936794679567966797679867996800680168026803680468056806680768086809681068116812681368146815681668176818681968206821682268236824682568266827682868296830683168326833683468356836683768386839684068416842684368446845684668476848684968506851685268536854685568566857685868596860686168626863686468656866686768686869687068716872687368746875687668776878687968806881688268836884688568866887688868896890689168926893689468956896689768986899690069016902690369046905690669076908690969106911691269136914691569166917691869196920692169226923692469256926692769286929693069316932693369346935693669376938693969406941694269436944694569466947694869496950695169526953695469556956695769586959696069616962696369646965696669676968696969706971697269736974697569766977697869796980698169826983698469856986698769886989699069916992699369946995699669976998699970007001700270037004700570067007700870097010701170127013701470157016701770187019702070217022702370247025702670277028702970307031703270337034703570367037703870397040704170427043704470457046704770487049705070517052705370547055705670577058705970607061706270637064706570667067706870697070707170727073707470757076707770787079708070817082708370847085708670877088708970907091709270937094709570967097709870997100710171027103710471057106710771087109711071117112711371147115711671177118711971207121712271237124712571267127712871297130713171327133713471357136713771387139714071417142714371447145714671477148714971507151715271537154715571567157715871597160716171627163716471657166716771687169717071717172717371747175717671777178717971807181718271837184718571867187718871897190719171927193719471957196719771987199720072017202720372047205720672077208720972107211721272137214721572167217721872197220722172227223722472257226722772287229723072317232723372347235723672377238723972407241724272437244724572467247724872497250725172527253725472557256725772587259726072617262726372647265726672677268726972707271727272737274727572767277727872797280728172827283728472857286728772887289729072917292729372947295729672977298729973007301730273037304730573067307730873097310731173127313731473157316731773187319732073217322732373247325732673277328732973307331733273337334733573367337733873397340734173427343734473457346734773487349735073517352735373547355735673577358735973607361736273637364736573667367736873697370737173727373737473757376737773787379738073817382738373847385738673877388738973907391739273937394739573967397739873997400740174027403740474057406740774087409741074117412741374147415741674177418741974207421742274237424742574267427742874297430743174327433743474357436743774387439744074417442744374447445744674477448744974507451745274537454745574567457745874597460746174627463746474657466746774687469747074717472747374747475747674777478747974807481748274837484748574867487748874897490749174927493749474957496749774987499750075017502750375047505750675077508750975107511751275137514751575167517751875197520752175227523752475257526752775287529753075317532753375347535753675377538753975407541754275437544754575467547754875497550755175527553755475557556755775587559756075617562756375647565756675677568756975707571757275737574757575767577757875797580758175827583758475857586758775887589759075917592759375947595759675977598759976007601760276037604760576067607760876097610761176127613761476157616761776187619762076217622762376247625762676277628762976307631763276337634763576367637763876397640764176427643764476457646764776487649765076517652765376547655765676577658765976607661766276637664766576667667766876697670767176727673767476757676767776787679768076817682768376847685768676877688768976907691769276937694769576967697769876997700770177027703770477057706770777087709771077117712771377147715771677177718771977207721772277237724772577267727772877297730773177327733773477357736773777387739774077417742774377447745774677477748774977507751775277537754775577567757775877597760776177627763776477657766776777687769777077717772777377747775777677777778777977807781778277837784778577867787778877897790779177927793779477957796779777987799780078017802780378047805780678077808780978107811781278137814781578167817781878197820782178227823782478257826782778287829783078317832783378347835783678377838783978407841784278437844784578467847784878497850785178527853785478557856785778587859786078617862786378647865786678677868786978707871787278737874787578767877787878797880788178827883788478857886788778887889789078917892789378947895789678977898789979007901790279037904790579067907790879097910791179127913791479157916791779187919792079217922792379247925792679277928792979307931793279337934793579367937793879397940794179427943794479457946794779487949795079517952795379547955795679577958795979607961796279637964796579667967796879697970797179727973797479757976797779787979798079817982798379847985798679877988798979907991799279937994799579967997799879998000800180028003800480058006800780088009801080118012801380148015801680178018801980208021802280238024802580268027802880298030803180328033803480358036803780388039804080418042804380448045804680478048804980508051805280538054805580568057805880598060806180628063806480658066806780688069807080718072807380748075807680778078807980808081808280838084808580868087808880898090809180928093809480958096809780988099810081018102810381048105810681078108810981108111811281138114811581168117811881198120812181228123812481258126812781288129813081318132813381348135813681378138813981408141814281438144814581468147814881498150815181528153815481558156815781588159816081618162816381648165816681678168816981708171817281738174817581768177817881798180818181828183818481858186818781888189819081918192819381948195819681978198819982008201820282038204820582068207820882098210821182128213821482158216821782188219822082218222822382248225822682278228822982308231823282338234823582368237823882398240824182428243824482458246824782488249825082518252825382548255825682578258825982608261826282638264826582668267826882698270827182728273827482758276827782788279828082818282828382848285828682878288828982908291829282938294829582968297829882998300830183028303830483058306830783088309831083118312831383148315831683178318831983208321832283238324832583268327832883298330833183328333833483358336833783388339834083418342834383448345834683478348834983508351835283538354835583568357835883598360836183628363836483658366836783688369837083718372837383748375837683778378837983808381838283838384838583868387838883898390839183928393839483958396839783988399840084018402840384048405840684078408840984108411841284138414841584168417841884198420842184228423842484258426842784288429843084318432843384348435843684378438843984408441844284438444844584468447844884498450845184528453845484558456845784588459846084618462846384648465846684678468846984708471847284738474847584768477847884798480848184828483848484858486848784888489849084918492849384948495849684978498849985008501850285038504850585068507850885098510851185128513851485158516851785188519852085218522852385248525852685278528852985308531853285338534853585368537853885398540854185428543854485458546854785488549855085518552855385548555855685578558855985608561856285638564856585668567856885698570857185728573857485758576857785788579858085818582858385848585858685878588858985908591859285938594859585968597859885998600860186028603860486058606860786088609861086118612861386148615861686178618861986208621862286238624862586268627862886298630863186328633863486358636863786388639864086418642864386448645864686478648864986508651865286538654865586568657865886598660866186628663866486658666866786688669867086718672867386748675867686778678867986808681868286838684868586868687868886898690869186928693869486958696869786988699870087018702870387048705870687078708870987108711871287138714871587168717871887198720872187228723872487258726872787288729873087318732873387348735873687378738873987408741874287438744874587468747874887498750875187528753875487558756875787588759876087618762876387648765876687678768876987708771877287738774877587768777877887798780878187828783878487858786878787888789879087918792879387948795879687978798879988008801880288038804880588068807880888098810881188128813881488158816881788188819882088218822882388248825882688278828882988308831883288338834883588368837883888398840884188428843884488458846884788488849885088518852885388548855885688578858885988608861886288638864886588668867886888698870887188728873887488758876887788788879888088818882888388848885888688878888888988908891889288938894889588968897889888998900890189028903890489058906890789088909891089118912891389148915891689178918891989208921892289238924892589268927892889298930893189328933893489358936893789388939894089418942894389448945894689478948894989508951895289538954895589568957895889598960896189628963896489658966896789688969897089718972897389748975897689778978897989808981898289838984898589868987898889898990899189928993899489958996899789988999900090019002900390049005900690079008900990109011901290139014901590169017901890199020902190229023902490259026902790289029903090319032903390349035903690379038903990409041904290439044904590469047904890499050905190529053905490559056905790589059906090619062906390649065906690679068906990709071907290739074907590769077907890799080908190829083908490859086908790889089909090919092909390949095909690979098909991009101910291039104910591069107910891099110911191129113911491159116911791189119912091219122912391249125912691279128912991309131913291339134913591369137913891399140914191429143914491459146914791489149915091519152915391549155915691579158915991609161916291639164916591669167916891699170917191729173917491759176917791789179918091819182918391849185918691879188918991909191919291939194919591969197919891999200920192029203920492059206920792089209921092119212921392149215921692179218921992209221922292239224922592269227922892299230923192329233923492359236923792389239924092419242924392449245924692479248924992509251925292539254925592569257925892599260926192629263926492659266926792689269927092719272927392749275927692779278927992809281928292839284928592869287928892899290929192929293929492959296929792989299930093019302930393049305930693079308930993109311931293139314931593169317931893199320932193229323932493259326932793289329933093319332933393349335933693379338933993409341934293439344934593469347934893499350935193529353935493559356935793589359936093619362936393649365936693679368936993709371937293739374937593769377937893799380938193829383938493859386938793889389939093919392939393949395939693979398939994009401940294039404940594069407940894099410941194129413941494159416941794189419942094219422942394249425942694279428942994309431943294339434943594369437943894399440944194429443944494459446944794489449945094519452945394549455945694579458945994609461946294639464946594669467946894699470947194729473947494759476947794789479948094819482948394849485948694879488948994909491949294939494949594969497949894999500950195029503950495059506950795089509951095119512951395149515951695179518951995209521952295239524952595269527952895299530953195329533953495359536953795389539954095419542954395449545954695479548954995509551955295539554955595569557955895599560956195629563956495659566956795689569957095719572957395749575957695779578957995809581958295839584958595869587958895899590959195929593959495959596959795989599960096019602960396049605960696079608960996109611961296139614961596169617961896199620962196229623962496259626962796289629963096319632963396349635963696379638963996409641964296439644964596469647964896499650965196529653965496559656965796589659966096619662966396649665966696679668966996709671967296739674967596769677967896799680968196829683968496859686968796889689969096919692969396949695969696979698969997009701970297039704970597069707970897099710971197129713971497159716971797189719972097219722972397249725972697279728972997309731973297339734973597369737973897399740974197429743974497459746974797489749975097519752975397549755975697579758975997609761976297639764976597669767976897699770977197729773977497759776977797789779978097819782978397849785978697879788978997909791979297939794979597969797979897999800980198029803980498059806980798089809981098119812981398149815981698179818981998209821982298239824982598269827982898299830983198329833983498359836983798389839984098419842984398449845984698479848984998509851985298539854985598569857985898599860986198629863986498659866986798689869987098719872987398749875987698779878987998809881988298839884988598869887988898899890989198929893989498959896989798989899990099019902990399049905990699079908990999109911991299139914991599169917991899199920992199229923992499259926992799289929993099319932993399349935993699379938993999409941994299439944994599469947994899499950995199529953995499559956995799589959996099619962996399649965996699679968996999709971997299739974997599769977997899799980998199829983998499859986998799889989999099919992999399949995999699979998999910000100011000210003100041000510006100071000810009100101001110012100131001410015100161001710018100191002010021100221002310024100251002610027100281002910030100311003210033100341003510036100371003810039100401004110042100431004410045100461004710048100491005010051100521005310054100551005610057100581005910060100611006210063100641006510066100671006810069100701007110072100731007410075100761007710078100791008010081100821008310084100851008610087100881008910090100911009210093100941009510096100971009810099101001010110102101031010410105101061010710108101091011010111101121011310114101151011610117101181011910120101211012210123101241012510126101271012810129101301013110132101331013410135101361013710138101391014010141101421014310144101451014610147101481014910150101511015210153101541015510156101571015810159101601016110162101631016410165101661016710168101691017010171101721017310174101751017610177101781017910180101811018210183101841018510186101871018810189101901019110192101931019410195101961019710198101991020010201102021020310204102051020610207102081020910210102111021210213102141021510216102171021810219102201022110222102231022410225102261022710228102291023010231102321023310234102351023610237102381023910240102411024210243102441024510246102471024810249102501025110252102531025410255102561025710258102591026010261102621026310264102651026610267102681026910270102711027210273102741027510276102771027810279102801028110282102831028410285102861028710288102891029010291102921029310294102951029610297102981029910300103011030210303103041030510306103071030810309103101031110312103131031410315103161031710318103191032010321103221032310324103251032610327103281032910330103311033210333103341033510336103371033810339103401034110342103431034410345103461034710348103491035010351103521035310354103551035610357103581035910360103611036210363103641036510366103671036810369103701037110372103731037410375103761037710378103791038010381103821038310384103851038610387103881038910390103911039210393103941039510396103971039810399104001040110402104031040410405104061040710408104091041010411104121041310414104151041610417104181041910420104211042210423104241042510426104271042810429104301043110432
  1. using Aspose.Cells;
  2. using Aspose.Cells.Drawing;
  3. using Aspose.Words;
  4. using Aspose.Words.Tables;
  5. using Azure;
  6. using EyeSoft.Extensions;
  7. using Microsoft.AspNetCore.Http.Features;
  8. using Microsoft.Graph.Models;
  9. using Microsoft.Kiota.Abstractions;
  10. using Newtonsoft.Json.Serialization;
  11. using NodaTime;
  12. using NPOI.SS.Formula.Functions;
  13. using NPOI.SS.UserModel;
  14. using OASystem.API.OAMethodLib;
  15. using OASystem.API.OAMethodLib.Hotmail;
  16. using OASystem.API.OAMethodLib.HunYuanAPI;
  17. using OASystem.API.OAMethodLib.KiMiApi;
  18. using OASystem.API.OAMethodLib.QiYeWeChatAPI;
  19. using OASystem.API.OAMethodLib.QiYeWeChatAPI.AppNotice;
  20. using OASystem.API.OAMethodLib.SnovioAPI;
  21. using OASystem.API.OAMethodLib.TokenHubAI;
  22. using OASystem.API.OAMethodLib.TokenHubAI.Models;
  23. using OASystem.Domain.AesEncryption;
  24. using OASystem.Domain.Dtos.Groups;
  25. using OASystem.Domain.Entities.Groups;
  26. using OASystem.Domain.ViewModels.Groups;
  27. using OASystem.Domain.ViewModels.QiYeWeChat;
  28. using OASystem.Domain.ViewModels.Statistics;
  29. using OASystem.Infrastructure.Repositories.Groups;
  30. using Quartz.Logging;
  31. using Quartz.Util;
  32. using System.ComponentModel.DataAnnotations;
  33. using System.Data;
  34. using System.Diagnostics;
  35. using System.Security.AccessControl;
  36. using TencentCloud.Common;
  37. using static NodaTime.TimeZones.TzdbZone1970Location;
  38. using static OASystem.API.Controllers.PersonnelModuleController;
  39. using static OASystem.API.OAMethodLib.GeneralMethod;
  40. using static OASystem.API.OAMethodLib.Hotmail.HotmailService;
  41. using static OASystem.API.OAMethodLib.JWTHelper;
  42. using static OpenAI.GPT3.ObjectModels.Models;
  43. using Workbook = Aspose.Cells.Workbook;
  44. namespace OASystem.API.Controllers
  45. {
  46. /// <summary>
  47. /// 资料相关
  48. /// </summary>
  49. //[Authorize]
  50. [Route("api/[controller]/[action]")]
  51. public class ResourceController : ControllerBase
  52. {
  53. private readonly IMapper _mapper;
  54. private readonly ILogger<ResourceController> _logger;
  55. private readonly IConfiguration _config;
  56. private readonly IQiYeWeChatApiService _qiYeWeChatApiService;
  57. private readonly SqlSugarClient _sqlSugar;
  58. private readonly CarDataRepository _carDataRep;
  59. private readonly LocalGuideDataRepository _localGuideDataRep;
  60. private readonly ThreeCodeRepository _ThreeCodeRep;
  61. private readonly HotelDataRepository _hotelDataRep;
  62. private readonly ResItemInfoRepository _resItemInfoRep;
  63. private readonly SetDataRepository _setDataRepository;
  64. private readonly CountryFeeRepository _countryFeeRep;
  65. private readonly SetDataTypeRepository _setDataTypeRep;
  66. private readonly AirTicketAgentRepository _airTicketAgentRep;
  67. private readonly InvitationOfficialActivityDataRepository _InvitationOfficialActivityDataRep;
  68. private readonly OfficialActivitiesRepository _officialActivitiesRep;
  69. private readonly AskDataRepository _askDataRep;
  70. private readonly TicketBlackCodeRepository _ticketBlackCodeRep;
  71. private readonly TourClientListRepository _tourClientListRep;
  72. private readonly DelegationInfoRepository _delegationInfoRep;
  73. private readonly TranslatorLibraryRepository _translatorRep;
  74. private readonly MediaSuppliersRepository _mediaSupplierRep;
  75. private readonly List<int> _portIds;
  76. private readonly BasicInsuranceCostRepository _insuranceCostRep;
  77. private readonly GamesBudgetMasterRepository _gamesBudgetMasterRep;
  78. private readonly OverseaVehicleRepository _overseaVehicleRep;
  79. private readonly MaterialCostRepository _materialCostRep;
  80. private readonly HotmailService _hotmailService;
  81. private readonly ITokenHubService _tokenHubService;
  82. private readonly ISnovioService _snovioService;
  83. /// <summary>
  84. /// 签证费用归属省份静态数据
  85. /// </summary>
  86. private static readonly List<KeyValuePair<int, string>> _provinceData = new()
  87. {
  88. new(122, "四川"),
  89. new(132, "云南"),
  90. new(103, "重庆"),
  91. new(108, "贵州")
  92. };
  93. public ResourceController(
  94. IMapper mapper,
  95. IConfiguration config,
  96. ILogger<ResourceController> logger,
  97. IQiYeWeChatApiService qiYeWeChatApiService,
  98. HotmailService hotmailService,
  99. SqlSugarClient sqlSugar,
  100. CarDataRepository carDataRep,
  101. LocalGuideDataRepository localGuideDataRep,
  102. ThreeCodeRepository threeCodeRep,
  103. HotelDataRepository hotelDataRep,
  104. ResItemInfoRepository resItemInfoRep,
  105. SetDataRepository setDataRepository,
  106. CountryFeeRepository countryFeeRep,
  107. SetDataTypeRepository setDataTypeRep,
  108. AirTicketAgentRepository airTicketAgentRep,
  109. InvitationOfficialActivityDataRepository invitationOfficialActivityDataRep,
  110. OfficialActivitiesRepository officialActivitiesRep,
  111. AskDataRepository askDataRep,
  112. TicketBlackCodeRepository ticketBlackCodeRep,
  113. TourClientListRepository tourClientListRep,
  114. DelegationInfoRepository delegationInfoRep,
  115. TranslatorLibraryRepository translatorRep,
  116. MediaSuppliersRepository mediaSupplierRep,
  117. BasicInsuranceCostRepository insuranceCostRep,
  118. GamesBudgetMasterRepository gamesBudgetMasterRep,
  119. OverseaVehicleRepository overseaVehicleRep,
  120. MaterialCostRepository materialCostRep,
  121. ITokenHubService tokenHubService,
  122. ISnovioService snovioService
  123. )
  124. {
  125. _mapper = mapper;
  126. _config = config;
  127. _logger = logger;
  128. _qiYeWeChatApiService = qiYeWeChatApiService;
  129. _hotmailService = hotmailService;
  130. _sqlSugar = sqlSugar;
  131. _carDataRep = carDataRep;
  132. _localGuideDataRep = localGuideDataRep;
  133. _ThreeCodeRep = threeCodeRep;
  134. _hotelDataRep = hotelDataRep;
  135. _resItemInfoRep = resItemInfoRep;
  136. _setDataRepository = setDataRepository;
  137. _countryFeeRep = countryFeeRep;
  138. _setDataTypeRep = setDataTypeRep;
  139. _airTicketAgentRep = airTicketAgentRep;
  140. _InvitationOfficialActivityDataRep = invitationOfficialActivityDataRep;
  141. _officialActivitiesRep = officialActivitiesRep;
  142. _askDataRep = askDataRep;
  143. _ticketBlackCodeRep = ticketBlackCodeRep;
  144. _tourClientListRep = tourClientListRep;
  145. _delegationInfoRep = delegationInfoRep;
  146. _translatorRep = translatorRep;
  147. _mediaSupplierRep = mediaSupplierRep;
  148. _portIds = new List<int> { 1, 2, 3 };
  149. _insuranceCostRep = insuranceCostRep;
  150. _gamesBudgetMasterRep = gamesBudgetMasterRep;
  151. _overseaVehicleRep = overseaVehicleRep;
  152. _materialCostRep = materialCostRep;
  153. _tokenHubService = tokenHubService;
  154. _snovioService = snovioService;
  155. }
  156. #region 车公司资料板块
  157. /// <summary>
  158. /// 车公司信息查询
  159. /// </summary>
  160. /// <returns></returns>
  161. [HttpPost]
  162. [ProducesResponseType(typeof(JsonView), StatusCodes.Status200OK)]
  163. public async Task<IActionResult> QueryCarData(QueryCarDataDto dto)
  164. {
  165. Result LocalGuide = await _carDataRep.QueryCarData(dto);
  166. if (LocalGuide.Code == 0)
  167. {
  168. return Ok(JsonView(true, "查询成功", LocalGuide.Data));
  169. }
  170. else
  171. {
  172. return Ok(JsonView(false, LocalGuide.Msg));
  173. }
  174. }
  175. /// <summary>
  176. /// 车公司资料下拉框数据
  177. /// </summary>
  178. /// <returns></returns>
  179. [HttpPost]
  180. [ProducesResponseType(typeof(JsonView), StatusCodes.Status200OK)]
  181. public async Task<IActionResult> QueryCarSelect()
  182. {
  183. var carData = _carDataRep.QueryDto<Res_CarData, CarDataSelectView>().ToList();
  184. if (carData.Count == 0)
  185. {
  186. return Ok(JsonView(false, "暂无数据!"));
  187. }
  188. foreach (var item in carData) EncryptionProcessor.DecryptProperties(item);
  189. carData.Add(new CarDataSelectView { Id = 0, UnitArea = "全部" });
  190. carData = carData.Where((x, i) => carData.FindIndex(z => z.UnitArea == x.UnitArea) == i).ToList();
  191. carData = carData.OrderBy(x => x.Id).ToList();
  192. var data = new List<CarDataSelectView>();
  193. foreach (CarDataSelectView car in carData)
  194. {
  195. if (!string.IsNullOrWhiteSpace(car.UnitArea))
  196. {
  197. data.Add(car);
  198. }
  199. }
  200. return Ok(JsonView(true, "查询成功", data));
  201. }
  202. /// <summary>
  203. /// 根据Id查询车公司详细数据
  204. /// </summary>
  205. /// <returns></returns>
  206. [HttpPost]
  207. [ProducesResponseType(typeof(JsonView), StatusCodes.Status200OK)]
  208. public async Task<IActionResult> QuerCarDataDetailById(QueryCarDataDetailDto dto)
  209. {
  210. string sql = string.Format(@" Select * From Res_CarData With(Nolock) Where Id = {0} ", dto.Id);
  211. var info = _resItemInfoRep._sqlSugar.SqlQueryable<CarDataDetailDataView>(sql).First();
  212. if (info == null)
  213. {
  214. return Ok(JsonView(false, "未找到相关数据!"));
  215. }
  216. EncryptionProcessor.DecryptProperties(info);
  217. return Ok(JsonView(true, "查询成功", info));
  218. }
  219. /// <summary>
  220. /// 车公司信息添加
  221. /// </summary>
  222. /// <returns></returns>
  223. [HttpPost]
  224. [ProducesResponseType(typeof(JsonView), StatusCodes.Status200OK)]
  225. public async Task<IActionResult> AddCarData(AddCarDataDto dto)
  226. {
  227. if (string.IsNullOrEmpty(dto.UnitArea)) return Ok(JsonView(false, "请检查单位区域是否填写!"));
  228. if (string.IsNullOrEmpty(dto.UnitName)) return Ok(JsonView(false, "请检查单位名称是否填写!"));
  229. if (string.IsNullOrEmpty(dto.Contact)) return Ok(JsonView(false, "请检查单位联系人是否填写!"));
  230. if (string.IsNullOrEmpty(dto.ContactTel)) return Ok(JsonView(false, "请检查联系方式是否填写正确!"));
  231. var carDadaInfo = _carDataRep
  232. .QueryDto<Res_CarData, CarDataView>(a => a.UnitArea == dto.UnitArea &&
  233. a.UnitName == dto.UnitName &&
  234. a.Contact == dto.Contact &&
  235. a.ContactTel == dto.ContactTel
  236. )
  237. .First();
  238. if (carDadaInfo != null) return Ok(JsonView(false, "该信息已存在,请勿重复添加!"));
  239. var carData = _mapper.Map<Res_CarData>(dto);
  240. carData.LastUpdateUserId = dto.CreateUserId;
  241. carData.LastUpdateTime = DateTime.Now;
  242. EncryptionProcessor.EncryptProperties(carData);
  243. int id = await _carDataRep.AddAsyncReturnId(carData);
  244. if (id < 1) return Ok(JsonView(false, "添加失败!"));
  245. return Ok(JsonView(true, "添加成功", new { Id = id }));
  246. }
  247. /// <summary>
  248. /// 车公司信息修改
  249. /// </summary>
  250. /// <returns></returns>
  251. [HttpPost]
  252. [ProducesResponseType(typeof(JsonView), StatusCodes.Status200OK)]
  253. public async Task<IActionResult> UpCarData(UpCarDataDto dto)
  254. {
  255. if (string.IsNullOrEmpty(dto.UnitArea)) return Ok(JsonView(false, "请检查单位区域是否填写!"));
  256. if (string.IsNullOrEmpty(dto.UnitName)) return Ok(JsonView(false, "请检查单位名称是否填写!"));
  257. if (string.IsNullOrEmpty(dto.Contact)) return Ok(JsonView(false, "请检查单位联系人是否填写!"));
  258. if (string.IsNullOrEmpty(dto.ContactTel)) return Ok(JsonView(false, "请检查联系方式是否填写正确!"));
  259. var carData = _mapper.Map<Res_CarData>(dto);
  260. carData.LastUpdateTime = DateTime.Now;
  261. EncryptionProcessor.EncryptProperties(carData);
  262. var res = await _sqlSugar.Updateable(carData)
  263. .IgnoreColumns(x => new { x.CreateUserId, x.CreateTime, x.DeleteTime, x.DeleteUserId, x.IsDel })
  264. .ExecuteCommandAsync();
  265. if (res < 1) { return Ok(JsonView(false, "修改失败!")); }
  266. return Ok(JsonView(true, "修改成功"));
  267. }
  268. /// <summary>
  269. /// 车公司信息删除
  270. /// </summary>
  271. /// <returns></returns>
  272. [HttpPost]
  273. [ProducesResponseType(typeof(JsonView), StatusCodes.Status200OK)]
  274. public async Task<IActionResult> DelCarData(DelCarDataDto dto)
  275. {
  276. bool res = await _carDataRep.SoftDeleteByIdAsync<Res_CarData>(dto.Id.ToString(), dto.DeleteUserId);
  277. if (!res) { return Ok(JsonView(false, "删除失败!")); }
  278. return Ok(JsonView(true, "删除成功"));
  279. }
  280. #endregion
  281. #region 导游地接资料板块
  282. [HttpPost]
  283. [ProducesResponseType(typeof(JsonView), StatusCodes.Status200OK)]
  284. public async Task<IActionResult> LocalGuideBatchEncryption()
  285. {
  286. var infos = await _sqlSugar.Queryable<Res_LocalGuideData>().ToListAsync();
  287. foreach (var item in infos) EncryptionProcessor.EncryptProperties(item);
  288. await _sqlSugar.Updateable(infos).ExecuteCommandAsync();
  289. return Ok(JsonView(true));
  290. }
  291. /// <summary>
  292. /// 导游地接资料查询
  293. /// </summary>
  294. /// <param name="dto"></param>
  295. /// <returns></returns>
  296. [HttpPost]
  297. [ProducesResponseType(typeof(JsonView), StatusCodes.Status200OK)]
  298. public async Task<IActionResult> QueryLocalGuide(QueryLocalGuide dto)
  299. {
  300. Stopwatch stopwatch = Stopwatch.StartNew();
  301. Result LocalGuide = await _localGuideDataRep.QueryLocalGuide(dto);
  302. if (LocalGuide.Code == 0)
  303. {
  304. stopwatch.Stop();
  305. return Ok(JsonView(true, $"查询成功!耗时:{stopwatch.ElapsedMilliseconds} 毫秒", LocalGuide.Data));
  306. }
  307. stopwatch.Stop();
  308. return Ok(JsonView(false, LocalGuide.Msg + $" 耗时:{stopwatch.ElapsedMilliseconds} 毫秒"));
  309. }
  310. /// <summary>
  311. /// 导游地接资料下拉框数据
  312. /// </summary>
  313. /// <returns></returns>
  314. [HttpPost]
  315. [ProducesResponseType(typeof(JsonView), StatusCodes.Status200OK)]
  316. public async Task<IActionResult> QueryLocalGuideSelect()
  317. {
  318. var localGuides = _carDataRep.QueryDto<Res_LocalGuideData, QueryLocalGuideSelectView>().ToList();
  319. if (localGuides.Count == 0)
  320. {
  321. return Ok(JsonView(false, "暂无数据!"));
  322. }
  323. foreach (var item in localGuides) EncryptionProcessor.DecryptProperties(item);
  324. localGuides.Add(new QueryLocalGuideSelectView { Id = 0, UnitArea = "全部" });
  325. localGuides = localGuides.Where((x, i) => localGuides.FindIndex(z => z.UnitArea == x.UnitArea && !string.IsNullOrEmpty(z.UnitArea)) == i).ToList();
  326. localGuides = localGuides.OrderBy(x => x.Id).ToList();
  327. var data = new List<QueryLocalGuideSelectView>();
  328. foreach (QueryLocalGuideSelectView Local in localGuides)
  329. {
  330. if (!string.IsNullOrWhiteSpace(Local.UnitArea))
  331. {
  332. data.Add(Local);
  333. }
  334. }
  335. return Ok(JsonView(true, "查询成功", data));
  336. }
  337. /// <summary>
  338. /// 根据Id查询地接详细数据
  339. /// </summary>
  340. /// <returns></returns>
  341. [HttpPost]
  342. [ProducesResponseType(typeof(JsonView), StatusCodes.Status200OK)]
  343. public async Task<IActionResult> QueryLocalGuideDetailById(QueryLocalGuideDetailDto dto)
  344. {
  345. string sql = string.Format(@" Select * From Res_LocalGuideData With(Nolock) Where Id = {0} ", dto.Id);
  346. var info = _resItemInfoRep._sqlSugar.SqlQueryable<LocalGuideDetailDataView>(sql).First();
  347. if (info == null)
  348. {
  349. return Ok(JsonView(false, "未找到相关数据!"));
  350. }
  351. EncryptionProcessor.DecryptProperties(info);
  352. return Ok(JsonView(true, "查询成功", info));
  353. }
  354. /// <summary>
  355. /// 导游地接信息操作(Status:1.新增,2.修改)
  356. /// </summary>
  357. /// <returns></returns>
  358. [HttpPost]
  359. [ProducesResponseType(typeof(JsonView), StatusCodes.Status200OK)]
  360. public async Task<IActionResult> OperationLocalGuide(LocalGuideOperationDto dto)
  361. {
  362. if (string.IsNullOrEmpty(dto.UnitArea)) return Ok(JsonView(false, "请检查单位区域是否填写!"));
  363. if (string.IsNullOrEmpty(dto.UnitName)) return Ok(JsonView(false, "请检查单位名称是否填写!"));
  364. if (string.IsNullOrEmpty(dto.Contact)) return Ok(JsonView(false, "请检查单位联系人是否填写!"));
  365. if (string.IsNullOrEmpty(dto.ContactTel)) return Ok(JsonView(false, "请检查联系方式是否填写正确!"));
  366. Result result = await _localGuideDataRep.LocalGuideOperation(dto);
  367. if (result.Code != 0)
  368. {
  369. return Ok(JsonView(false, result.Msg));
  370. }
  371. return Ok(JsonView(true, result.Msg));
  372. }
  373. /// <summary>
  374. /// 导游地接信息操作(删除)
  375. /// </summary>
  376. /// <returns></returns>
  377. [HttpPost]
  378. [ProducesResponseType(typeof(JsonView), StatusCodes.Status200OK)]
  379. public async Task<IActionResult> DelLocalGuide(LocalGuideDelDto dto)
  380. {
  381. var res = await _localGuideDataRep.SoftDeleteByIdAsync<Res_LocalGuideData>(dto.Id.ToString(), dto.DeleteUserId);
  382. if (!res)
  383. {
  384. return Ok(JsonView(false, "删除失败"));
  385. }
  386. return Ok(JsonView(true, "删除成功!"));
  387. }
  388. #endregion
  389. #region 机场三字码信息
  390. /// <summary>
  391. /// 机场三字码查询
  392. /// </summary>
  393. /// <param name="dto"></param>
  394. /// <returns></returns>
  395. [HttpPost]
  396. [ProducesResponseType(typeof(JsonView), StatusCodes.Status200OK)]
  397. public async Task<IActionResult> QueryThreeCode(QueryThreeCodeDto dto)
  398. {
  399. try
  400. {
  401. Result LocalGuide = await _ThreeCodeRep.QueryThreeCode(dto);
  402. if (LocalGuide.Code == 0)
  403. {
  404. return Ok(JsonView(true, "查询成功", LocalGuide.Data));
  405. }
  406. else
  407. {
  408. return Ok(JsonView(false, LocalGuide.Msg));
  409. }
  410. }
  411. catch (Exception ex)
  412. {
  413. return Ok(JsonView(false, "程序错误!"));
  414. throw;
  415. }
  416. }
  417. /// <summary>
  418. /// 机场三字码数据城市下拉框数据
  419. /// </summary>
  420. /// <returns></returns>
  421. [HttpPost]
  422. [ProducesResponseType(typeof(JsonView), StatusCodes.Status200OK)]
  423. public async Task<IActionResult> QueryThreeCodeSelect()
  424. {
  425. try
  426. {
  427. var ThreeCode = _carDataRep.QueryDto<Res_ThreeCode, ThreeCodeSelectView>().ToList();
  428. if (ThreeCode.Count == 0)
  429. {
  430. return Ok(JsonView(false, "暂无数据!"));
  431. }
  432. ThreeCode.Add(new ThreeCodeSelectView { Id = 0, City = "全部" });
  433. ThreeCode = ThreeCode.Where((x, i) => ThreeCode.FindIndex(z => z.City == x.City && z.City != "") == i).ToList();
  434. ThreeCode = ThreeCode.OrderBy(x => x.Id).ToList();
  435. List<ThreeCodeSelectView> data = new List<ThreeCodeSelectView>();
  436. foreach (ThreeCodeSelectView _ThreeCode in ThreeCode)
  437. {
  438. if (!string.IsNullOrWhiteSpace(_ThreeCode.City))
  439. {
  440. data.Add(_ThreeCode);
  441. }
  442. }
  443. return Ok(JsonView(true, "查询成功", data));
  444. }
  445. catch (Exception)
  446. {
  447. return Ok(JsonView(false, "程序错误!"));
  448. throw;
  449. }
  450. }
  451. /// <summary>
  452. /// 机场三字码资料操作(Status:1.新增,2.修改)
  453. /// </summary>
  454. /// <param name="dto"></param>
  455. /// <returns></returns>
  456. [HttpPost]
  457. [ProducesResponseType(typeof(JsonView), StatusCodes.Status200OK)]
  458. public async Task<IActionResult> OperationThreeCode(ThreeCodeOperationDto dto)
  459. {
  460. try
  461. {
  462. if (dto.Three == "")
  463. {
  464. return Ok(JsonView(false, "请检查三字码是否填写!"));
  465. }
  466. if (dto.Country == "")
  467. {
  468. return Ok(JsonView(false, "请检查国家是否填写!"));
  469. }
  470. if (dto.City == "")
  471. {
  472. return Ok(JsonView(false, "请检查城市是否填写正确!"));
  473. }
  474. if (dto.AirPort == "")
  475. {
  476. return Ok(JsonView(false, "请检查机场是否填写正确!"));
  477. }
  478. Result result = await _ThreeCodeRep.ThreeCodeOperation(dto);
  479. if (result.Code != 0)
  480. {
  481. return Ok(JsonView(false, result.Msg));
  482. }
  483. return Ok(JsonView(true, result.Msg));
  484. }
  485. catch (Exception ex)
  486. {
  487. return Ok(JsonView(false, "程序错误!"));
  488. throw;
  489. }
  490. }
  491. /// <summary>
  492. /// 机场三字码资料操作(删除)
  493. /// </summary>
  494. /// <returns></returns>
  495. [HttpPost]
  496. [ProducesResponseType(typeof(JsonView), StatusCodes.Status200OK)]
  497. public async Task<IActionResult> DelThreeCode(ThreeCodeDelDto dto)
  498. {
  499. try
  500. {
  501. var res = await _ThreeCodeRep.SoftDeleteByIdAsync<Res_ThreeCode>(dto.Id.ToString(), dto.DeleteUserId);
  502. if (!res)
  503. {
  504. return Ok(JsonView(false, "删除失败"));
  505. }
  506. return Ok(JsonView(true, "删除成功!"));
  507. }
  508. catch (Exception ex)
  509. {
  510. return Ok(JsonView(false, "程序错误!"));
  511. throw;
  512. }
  513. }
  514. /// <summary>
  515. ///根据ID查询三字码信息
  516. /// </summary>
  517. /// <returns></returns>
  518. [HttpPost]
  519. public IActionResult QuerySingleThreeCode(QuerySingleThreeCode dto)
  520. {
  521. var jw = JsonView(false);
  522. var single = _sqlSugar.Queryable<Res_ThreeCode>().First(x => x.Id == dto.ID && x.IsDel == 0);
  523. if (single != null)
  524. {
  525. jw = JsonView(true, "获取成功!", new
  526. {
  527. single.Three,
  528. Four = single.Four?.Trim(),
  529. single.AirPort,
  530. single.AirPort_En,
  531. single.City,
  532. single.Country,
  533. single.Remark
  534. });
  535. }
  536. else
  537. {
  538. jw.Msg = "暂无!";
  539. }
  540. return Ok(jw);
  541. }
  542. #endregion
  543. #region 代理出票合作方资料
  544. /// <summary>
  545. /// 代理出票合作方资料
  546. /// </summary>
  547. /// <param name="dto"></param>
  548. /// <returns></returns>
  549. [HttpPost]
  550. [ProducesResponseType(typeof(JsonView), StatusCodes.Status200OK)]
  551. public async Task<IActionResult> QueryAirTicketAgent(DtoBase dto)
  552. {
  553. var res_AirTicketAgents = _airTicketAgentRep.Query<Res_AirTicketAgent>(a => a.IsDel == 0).ToList();
  554. if (res_AirTicketAgents.Count > 1)
  555. {
  556. res_AirTicketAgents = res_AirTicketAgents.OrderByDescending(a => a.CreateTime).ToList();
  557. if (dto.PageSize == 0 && dto.PageIndex == 0)
  558. {
  559. foreach (var item in res_AirTicketAgents) EncryptionProcessor.DecryptProperties(item);
  560. return Ok(JsonView(true, "查询成功!", res_AirTicketAgents));
  561. }
  562. else
  563. {
  564. int count = res_AirTicketAgents.Count;
  565. float totalPage = (float)count / dto.PageSize;//总页数
  566. if (totalPage == 0) totalPage = 1;
  567. else totalPage = (int)Math.Ceiling((double)totalPage);
  568. var _AirTicketAgent = new List<Res_AirTicketAgent>();
  569. for (int i = 0; i < dto.PageSize; i++)
  570. {
  571. var RowIndex = i + (dto.PageIndex - 1) * dto.PageSize;
  572. if (RowIndex < res_AirTicketAgents.Count)
  573. {
  574. EncryptionProcessor.DecryptProperties(res_AirTicketAgents[RowIndex]);
  575. _AirTicketAgent.Add(res_AirTicketAgents[RowIndex]);
  576. }
  577. else break;
  578. }
  579. return Ok(JsonView(true, "查询成功!", new { pageCount = count, totalPage = (int)totalPage, pageIndex = dto.PageIndex, pageSize = dto.PageSize, pageSource = _AirTicketAgent }));
  580. }
  581. }
  582. return Ok(JsonView(false, "暂无数据!"));
  583. }
  584. /// <summary>
  585. /// 代理出票合作方资料操作(Status:1.新增,2.修改)
  586. /// </summary>
  587. /// <param name="dto"></param>
  588. /// <returns></returns>
  589. [HttpPost]
  590. [ProducesResponseType(typeof(JsonView), StatusCodes.Status200OK)]
  591. public async Task<IActionResult> OpAirTicketAgent(OpAirTicketAgentDto dto)
  592. {
  593. if (string.IsNullOrEmpty(dto.Account)) return Ok(JsonView(false, "请检查代理商账户是否填写!"));
  594. if (string.IsNullOrEmpty(dto.Bank)) return Ok(JsonView(false, "请检查代理商银行是否填写!"));
  595. if (string.IsNullOrEmpty(dto.Name)) return Ok(JsonView(false, "请检查代理商名称是否填写正确!"));
  596. Result result = await _airTicketAgentRep.OpAirTicketAgent(dto);
  597. if (result.Code != 0) return Ok(JsonView(false, result.Msg));
  598. return Ok(JsonView(true, result.Msg));
  599. }
  600. /// <summary>
  601. /// 代理出票合作方资料操作(删除)
  602. /// </summary>
  603. /// <returns></returns>
  604. [HttpPost]
  605. [ProducesResponseType(typeof(JsonView), StatusCodes.Status200OK)]
  606. public async Task<IActionResult> DelAirTicketAgent(DelBaseDto dto)
  607. {
  608. var res = await _airTicketAgentRep.SoftDeleteByIdAsync<Res_AirTicketAgent>(dto.Id.ToString(), dto.DeleteUserId);
  609. if (!res) return Ok(JsonView(false, "删除失败"));
  610. return Ok(JsonView(true, "删除成功!"));
  611. }
  612. #endregion
  613. #region 酒店资料数据
  614. /// <summary>
  615. /// 酒店信息查询 Page
  616. /// </summary>
  617. /// <param name="dto"></param>
  618. /// <returns></returns>
  619. [HttpPost]
  620. [ProducesResponseType(typeof(JsonView), StatusCodes.Status200OK)]
  621. public async Task<IActionResult> QueryHotelData(QueryHotelDataDto dto)
  622. {
  623. string sqlWhere = string.Empty;
  624. if (!string.IsNullOrWhiteSpace(dto.Name))
  625. {
  626. sqlWhere += string.Format(@" And hd.Name like '%{0}%'", AesEncryptionHelper.Encrypt(dto.Name));
  627. }
  628. if (!string.IsNullOrWhiteSpace(dto.City) && dto.City != "全部")
  629. {
  630. sqlWhere += string.Format(@" And hd.City like '%{0}%'", AesEncryptionHelper.Encrypt(dto.City));
  631. }
  632. if (!string.IsNullOrWhiteSpace(dto.Contact))
  633. {
  634. sqlWhere += string.Format(@" And hd.Contact like '%{0}%'", AesEncryptionHelper.Encrypt(dto.Contact));
  635. }
  636. if (!string.IsNullOrWhiteSpace(dto.ContactPhone))
  637. {
  638. sqlWhere += string.Format(@" And hd.ContactPhone like '%{0}%'", AesEncryptionHelper.Encrypt(dto.ContactPhone));
  639. }
  640. sqlWhere += string.Format(@" And hd.IsDel={0}", 0);
  641. if (!string.IsNullOrEmpty(sqlWhere.Trim()))
  642. {
  643. Regex r = new Regex("And");
  644. sqlWhere = r.Replace(sqlWhere, "Where", 1);
  645. }
  646. string sql = string.Format(@"SELECT
  647. *
  648. FROM
  649. (
  650. SELECT
  651. ROW_NUMBER() OVER (
  652. ORDER BY
  653. hd.CreateTime DESC
  654. ) AS Row_Number,
  655. hd.Id,
  656. hd.City,
  657. hd.[Name],
  658. hd.Tel,
  659. hd.Fax,
  660. hd.Contact,
  661. hd.ContactPhone,
  662. u.CnName AS CreateUserName,
  663. hd.CreateTime
  664. FROM
  665. Res_HotelData hd
  666. WITH
  667. (NoLock)
  668. LEFT JOIN Sys_Users u
  669. WITH
  670. (NoLock) ON hd.CreateUserId = u.Id {0}
  671. ) Temp
  672. ", sqlWhere);
  673. if (dto.PortType == 1)
  674. {
  675. var HotelDataData = await _sqlSugar.SqlQueryable<HotelDataItemView>(sql).ToListAsync();
  676. if (HotelDataData.Count == 0)
  677. {
  678. return Ok(JsonView(false, "暂无数据"));
  679. }
  680. foreach (var item in HotelDataData)
  681. {
  682. EncryptionProcessor.DecryptProperties(item);
  683. }
  684. return Ok(JsonView(true, "操作成功", HotelDataData));
  685. }
  686. else if (dto.PortType == 2 || dto.PortType == 3)
  687. {
  688. RefAsync<int> total = 0;//REF和OUT不支持异步,想要真的异步这是最优解
  689. var data = await _sqlSugar.SqlQueryable<HotelDataItemView>(sql).ToPageListAsync(dto.PageIndex, dto.PageSize, total); //ToPageAsync
  690. foreach (var item in data) EncryptionProcessor.DecryptProperties(item);
  691. return Ok(JsonView(true, "操作成功", data, total));
  692. }
  693. else return Ok(JsonView(false, "请传入PortType参数!1:Web,2:Android,3:IOS"));
  694. }
  695. /// <summary>
  696. /// 酒店资料
  697. /// 详情
  698. /// add time:2024-05-14 11:57:10
  699. /// </summary>
  700. /// <returns></returns>
  701. [HttpPost]
  702. [ProducesResponseType(typeof(JsonView), StatusCodes.Status200OK)]
  703. public async Task<IActionResult> QueryHotelDataInfo(QueryHotelDataInfoDto dto)
  704. {
  705. var _view = await _hotelDataRep._Info(dto.PortType, dto.Id);
  706. if (_view.Code == 0)
  707. {
  708. return Ok(JsonView(true, _view.Msg, _view.Data));
  709. }
  710. return Ok(JsonView(false, _view.Msg));
  711. }
  712. /// <summary>
  713. /// 酒店资料下拉框数据
  714. /// </summary>
  715. /// <returns></returns>
  716. [HttpPost]
  717. [ProducesResponseType(typeof(JsonView), StatusCodes.Status200OK)]
  718. public async Task<IActionResult> QueryHotelDataSelect()
  719. {
  720. try
  721. {
  722. var HotelData = _carDataRep.QueryDto<Res_HotelData, QueryHotelDataSelect>().ToList();
  723. if (HotelData.Count == 0)
  724. {
  725. return Ok(JsonView(false, "暂无数据!"));
  726. }
  727. //解密
  728. foreach (var item in HotelData)
  729. {
  730. EncryptionProcessor.DecryptProperties(item);
  731. }
  732. HotelData.Add(new QueryHotelDataSelect { Id = 0, City = "全部" });
  733. HotelData = HotelData.Where((x, i) => HotelData.FindIndex(z => z.City == x.City && z.City != "") == i).ToList();
  734. HotelData = HotelData.OrderBy(x => x.Id).ToList();
  735. var data = new List<QueryHotelDataSelect>();
  736. foreach (var Hotel in HotelData)
  737. {
  738. if (!string.IsNullOrWhiteSpace(Hotel.City))
  739. {
  740. data.Add(Hotel);
  741. }
  742. }
  743. return Ok(JsonView(true, "查询成功", data));
  744. }
  745. catch (Exception)
  746. {
  747. return Ok(JsonView(false, "程序错误!"));
  748. throw;
  749. }
  750. }
  751. /// <summary>
  752. /// 酒店资料操作(Status:1.新增,2.修改)
  753. /// </summary>
  754. /// <param name="dto"></param>
  755. /// <returns></returns>
  756. [HttpPost]
  757. [ProducesResponseType(typeof(JsonView), StatusCodes.Status200OK)]
  758. public async Task<IActionResult> OperationHotelData(OperationHotelDto dto)
  759. {
  760. if (string.IsNullOrEmpty(dto.City)) return Ok(JsonView(false, "请检查酒店所在城市是否填写!"));
  761. if (string.IsNullOrEmpty(dto.Name)) return Ok(JsonView(false, "请检查酒店名称是否填写!"));
  762. if (string.IsNullOrEmpty(dto.Address)) return Ok(JsonView(false, "请检查酒店地址是否填写正确!"));
  763. if (string.IsNullOrEmpty(dto.Tel)) return Ok(JsonView(false, "请检查酒店联系方式是否填写正确!"));
  764. Result result = await _hotelDataRep.OperationHotelData(dto);
  765. if (result.Code != 0) return Ok(JsonView(false, result.Msg));
  766. return Ok(JsonView(true, result.Msg));
  767. }
  768. /// <summary>
  769. /// 酒店资料操作(删除)
  770. /// </summary>
  771. /// <returns></returns>
  772. [HttpPost]
  773. [ProducesResponseType(typeof(JsonView), StatusCodes.Status200OK)]
  774. public async Task<IActionResult> DelHotelData(DelHotelDataDto dto)
  775. {
  776. try
  777. {
  778. var res = await _hotelDataRep.SoftDeleteByIdAsync<Res_HotelData>(dto.Id.ToString(), dto.DeleteUserId);
  779. if (!res)
  780. {
  781. return Ok(JsonView(false, "删除失败"));
  782. }
  783. return Ok(JsonView(true, "删除成功!"));
  784. }
  785. catch (Exception ex)
  786. {
  787. return Ok(JsonView(false, "程序错误!"));
  788. throw;
  789. }
  790. }
  791. #endregion
  792. #region 签证费用资料
  793. /// <summary>
  794. /// 签证费用资料查询
  795. /// </summary>
  796. /// <param name="dto"></param>
  797. /// <returns></returns>
  798. [HttpPost]
  799. [ProducesResponseType(typeof(JsonView), StatusCodes.Status200OK)]
  800. public async Task<IActionResult> QueryCountryFeeCost(DtoBase dto)
  801. {
  802. var portType = dto.PortType;
  803. if (portType != 1 && portType != 2 && portType != 3)
  804. return Ok(JsonView(false, "请传入PortType参数!1:Web,2:Android,3:IOS"));
  805. var countryVisaFees = await _countryFeeRep
  806. .QueryDto<Res_CountryFeeCost, CountryFeeCostView>()
  807. .OrderByDescending(x => x.CreateTime)
  808. .ToListAsync();
  809. if (countryVisaFees == null || countryVisaFees.Count == 0)
  810. return Ok(JsonView(false, "暂无数据!"));
  811. return Ok(JsonView(true, "查询成功", countryVisaFees));
  812. }
  813. /// <summary>
  814. /// 签证费用资料查询 Page New
  815. /// </summary>
  816. /// <param name="dto"></param>
  817. /// <returns></returns>
  818. [HttpPost]
  819. [ProducesResponseType(typeof(JsonView), StatusCodes.Status200OK)]
  820. public async Task<IActionResult> QueryVisaCountryFeeCosts(QueryVisaCountryFeeCostsDto dto)
  821. {
  822. // 校验端口类型
  823. if (dto.PortType != 1 && dto.PortType != 2 && dto.PortType != 3)
  824. return Ok(JsonView(false, "请传入PortType参数!1:Web,2:Android,3:IOS"));
  825. var portType = dto.PortType;
  826. RefAsync<int> total = 0;
  827. var query = _countryFeeRep._sqlSugar
  828. .Queryable<Res_CountryFeeCost>()
  829. .Where(x => x.IsDel == 0)
  830. .WhereIF(dto.VisaFeeType >= 0, x => x.VisaFeeType == dto.VisaFeeType)
  831. .WhereIF(!string.IsNullOrEmpty(dto.CountryName), x => x.VisaCountry.Contains(dto.CountryName))
  832. .OrderByDescending(x => x.CreateTime);
  833. var countryVisaFees = await query.ToPageListAsync(dto.PageIndex, dto.PageSize, total);
  834. if (!countryVisaFees.Any()) return Ok(JsonView(true, "暂无数据!"));
  835. var view = _mapper.Map<List<CountryFeeCostView>>(countryVisaFees);
  836. return Ok(JsonView(true, "查询成功", view, total));
  837. }
  838. /// <summary>
  839. /// 签证费用资料操作(Status:1.新增,2.修改)
  840. /// </summary>
  841. /// <param name="dto"></param>
  842. /// <returns></returns>
  843. [HttpPost]
  844. [ProducesResponseType(typeof(JsonView), StatusCodes.Status200OK)]
  845. public async Task<IActionResult> OperationCountryFeeCost(OperationCountryFeeCostDto dto)
  846. {
  847. if (string.IsNullOrWhiteSpace(dto.VisaContinent) || string.IsNullOrWhiteSpace(dto.VisaCountry))
  848. return Ok(JsonView(false, "请检查州名和国家名是否填写!"));
  849. return Ok(await _countryFeeRep.OperationCountryFeeCost(dto));
  850. }
  851. /// <summary>
  852. /// 签证费用资料操作(删除)
  853. /// </summary>
  854. /// <returns></returns>
  855. [HttpPost]
  856. [ProducesResponseType(typeof(JsonView), StatusCodes.Status200OK)]
  857. public async Task<IActionResult> DelCountryFeeCost(DelCountryFeeCostDto dto)
  858. {
  859. var res = await _countryFeeRep.SoftDeleteByIdAsync<Res_CountryFeeCost>(dto.Id.ToString(), dto.DeleteUserId);
  860. if (!res) return Ok(JsonView(false, "删除失败"));
  861. return Ok(JsonView(true, "删除成功!"));
  862. }
  863. #endregion
  864. #region 签证费用资料 New
  865. /// <summary>
  866. /// 签证费用标准 Init
  867. /// </summary>
  868. /// <returns></returns>
  869. [HttpPost]
  870. [ProducesResponseType(typeof(JsonView), StatusCodes.Status200OK)]
  871. public async Task<IActionResult> VisaFeeStandardInit()
  872. {
  873. //签证费用类型数据
  874. var visaFeeTypeData = new List<KeyValuePair<int, string>>
  875. {
  876. new(0, "因公"),
  877. new(1, "因私")
  878. };
  879. //签证费用归属省份
  880. var data = new
  881. {
  882. VisaFeeType = visaFeeTypeData.Select(x => new { Id = x.Key, Text = x.Value }),
  883. Province = _provinceData.Select(x => new { Id = x.Key, Text = x.Value })
  884. };
  885. return Ok(JsonView(data));
  886. }
  887. /// <summary>
  888. /// 签证费用标准 Info
  889. /// </summary>
  890. /// <param name="dto"></param>
  891. /// <returns></returns>
  892. [HttpPost]
  893. [ProducesResponseType(typeof(JsonView), StatusCodes.Status200OK)]
  894. public async Task<IActionResult> VisaFeeStandardInfo(VisaFeeStandardInfoDto dto)
  895. {
  896. var portType = dto.PortType;
  897. if (portType != 1 && portType != 2 && portType != 3)
  898. return Ok(JsonView(false, "请传入PortType参数!1:Web,2:Android,3:IOS"));
  899. return Ok(await _countryFeeRep.InfoAsync(dto.Id));
  900. }
  901. /// <summary>
  902. /// 签证费用标准 List
  903. /// </summary>
  904. /// <param name="dto"></param>
  905. /// <returns></returns>
  906. [HttpPost]
  907. [ProducesResponseType(typeof(JsonView), StatusCodes.Status200OK)]
  908. public async Task<IActionResult> VisaFeeStandardList(VisaFeeStandardListDto dto)
  909. {
  910. return Ok(await _countryFeeRep.PageListAsync(dto));
  911. }
  912. /// <summary>
  913. /// 签证费用标准 Save
  914. /// </summary>
  915. /// <param name="dto"></param>
  916. /// <returns></returns>
  917. [HttpPost]
  918. [ProducesResponseType(typeof(JsonView), StatusCodes.Status200OK)]
  919. public async Task<IActionResult> VisaFeeStandardSave(VisaFeeStandardSaveDto dto)
  920. {
  921. return Ok(await _countryFeeRep.SaveAsync(dto));
  922. }
  923. /// <summary>
  924. /// 签证费用标准 SoftDel
  925. /// </summary>
  926. /// <param name="dto"></param>
  927. /// <returns></returns>
  928. [HttpPost]
  929. [ProducesResponseType(typeof(JsonView), StatusCodes.Status200OK)]
  930. public async Task<IActionResult> VisaFeeStandardSoftDel(VisaFeeStandardSoftDelDto dto)
  931. {
  932. return Ok(await _countryFeeRep.SoftDelAsync(dto.CurrUserId, dto.Id));
  933. }
  934. /// <summary>
  935. /// 根据省份获取签证费用报价
  936. /// </summary>
  937. /// <param name="dto"></param>
  938. /// <returns></returns>
  939. [HttpPost]
  940. [ProducesResponseType(typeof(JsonView), StatusCodes.Status200OK)]
  941. public async Task<IActionResult> VisaFeeQuoteByProvince(VisaFeesByProvinceDto dto)
  942. {
  943. //省份验证
  944. var provinceIds = _provinceData.Select(x => x.Key).ToList();
  945. if (!provinceIds.Contains(dto.ProvinceId))
  946. {
  947. return Ok(JsonView(false, "请选择有效的省份!"));
  948. }
  949. return Ok(await _countryFeeRep.VisaFeeQuoteAsync(dto.ProvinceId, dto.CountryName));
  950. }
  951. #endregion
  952. #region 物料信息、供应商维护
  953. #region 供应商
  954. /// <summary>
  955. /// 物料供应商查询
  956. /// </summary>
  957. /// <param name="_jsonDto">Json序列化</param>
  958. /// <returns></returns>
  959. [HttpPost]
  960. [ProducesResponseType(typeof(JsonView), StatusCodes.Status200OK)]
  961. public async Task<IActionResult> PostSearchItemVendor(JsonDtoBase _jsonDto)
  962. {
  963. if (string.IsNullOrEmpty(_jsonDto.Paras))
  964. {
  965. return Ok(JsonView(false, "参数为空"));
  966. }
  967. Search_ResItemVendorDto _ItemVendorDto = System.Text.Json.JsonSerializer.Deserialize<Search_ResItemVendorDto>(_jsonDto.Paras);
  968. if (_ItemVendorDto != null)
  969. {
  970. if (_ItemVendorDto.SearchType == 2) //获取列表
  971. {
  972. Res_ItemVendorListView rstList = _resItemInfoRep.GetVendorList(_ItemVendorDto);
  973. return Ok(rstList);
  974. }
  975. else
  976. {
  977. Res_ItemVendorView rstInfo = _resItemInfoRep.getVendorInfo(_ItemVendorDto);
  978. return Ok(rstInfo);
  979. }
  980. }
  981. else
  982. {
  983. return Ok(JsonView(false, "参数反序列化失败"));
  984. }
  985. //return Ok(JsonView(false));
  986. }
  987. /// <summary>
  988. /// 创建/编辑/删除供应商信息
  989. /// </summary>
  990. /// <param name="_dto"></param>
  991. /// <returns></returns>
  992. [HttpPost]
  993. [ProducesResponseType(typeof(JsonView), StatusCodes.Status200OK)]
  994. public async Task<IActionResult> PostEditItemVendor(Edit_ResItemVendorDto _dto)
  995. {
  996. bool rst = false;
  997. if (_dto.EditType >= 0)
  998. {
  999. if (string.IsNullOrEmpty(_dto.VendorFullName))
  1000. {
  1001. return Ok(JsonView(false, "全称未填写"));
  1002. }
  1003. if (string.IsNullOrEmpty(_dto.VendorLinker))
  1004. {
  1005. return Ok(JsonView(false, "联系人未填写"));
  1006. }
  1007. if (string.IsNullOrEmpty(_dto.VendorMobile))
  1008. {
  1009. return Ok(JsonView(false, "联系人手机号未填写"));
  1010. }
  1011. if (string.IsNullOrEmpty(_dto.BusRange))
  1012. {
  1013. return Ok(JsonView(false, "经营范围未选择"));
  1014. }
  1015. if (_dto.EditType == 0)
  1016. {
  1017. var checkEmpty = _resItemInfoRep.Query<Res_ItemVendor>(s => s.FullName == _dto.VendorFullName).First();
  1018. if (checkEmpty != null)
  1019. {
  1020. return Ok(JsonView(false, "已存在同名供应商"));
  1021. }
  1022. rst = await _resItemInfoRep.addVendorInfo(_dto);
  1023. }
  1024. else if (_dto.EditType == 1)
  1025. {
  1026. if (_dto.VendorId > 0)
  1027. {
  1028. Res_ItemVendor _entity = _mapper.Map<Res_ItemVendor>(_dto);
  1029. rst = await _resItemInfoRep.updVendorInfo(_entity);
  1030. }
  1031. else
  1032. {
  1033. return Ok(JsonView(false, "供应商不存在"));
  1034. }
  1035. }
  1036. }
  1037. else
  1038. {
  1039. if (_dto.VendorId < 1 || _dto.SysUserId < 1)
  1040. {
  1041. return Ok(JsonView(false, "用户Id或供应商Id不存在"));
  1042. }
  1043. Res_ItemVendor _entity = _mapper.Map<Res_ItemVendor>(_dto);
  1044. rst = await _resItemInfoRep.delVendorInfo(_entity);
  1045. }
  1046. return Ok(JsonView(rst));
  1047. }
  1048. #endregion
  1049. #region 物料信息
  1050. /// <summary>
  1051. /// 物料信息查询
  1052. /// </summary>
  1053. /// <param name="paras">Json序列化</param>
  1054. /// <returns></returns>
  1055. [HttpPost]
  1056. [ProducesResponseType(typeof(JsonView), StatusCodes.Status200OK)]
  1057. public async Task<IActionResult> PostSearchItemInfo(string paras)
  1058. {
  1059. if (string.IsNullOrEmpty(paras))
  1060. {
  1061. return Ok(JsonView(false, "参数为空"));
  1062. }
  1063. Search_ItemInfoDto _ItemInfoDto = System.Text.Json.JsonSerializer.Deserialize<Search_ItemInfoDto>(paras);
  1064. if (_ItemInfoDto != null)
  1065. {
  1066. if (_ItemInfoDto.SearchType == 2) //获取列表
  1067. {
  1068. Res_ItemInfoListView rstList = _resItemInfoRep.GetItemList(_ItemInfoDto);
  1069. return Ok(rstList);
  1070. }
  1071. else
  1072. {
  1073. Res_ItemInfoView rstInfo = _resItemInfoRep.getItemInfo(_ItemInfoDto);
  1074. return Ok(rstInfo);
  1075. }
  1076. }
  1077. else
  1078. {
  1079. return Ok(JsonView(false, "参数反序列化失败"));
  1080. }
  1081. //return Ok(JsonView(false));
  1082. }
  1083. /// <summary>
  1084. /// 创建/编辑/删除物料信息
  1085. /// </summary>
  1086. ///
  1087. /// <returns></returns>
  1088. [HttpPost]
  1089. [ProducesResponseType(typeof(JsonView), StatusCodes.Status200OK)]
  1090. public async Task<IActionResult> PostEditItemInfo(Edit_ResItemInfoDto _dto)
  1091. {
  1092. bool rst = false;
  1093. if (_dto.EditType >= 0)
  1094. {
  1095. if (_dto.VendorId < 1)
  1096. {
  1097. return Ok(JsonView(false, "未选择供应商"));
  1098. }
  1099. if (string.IsNullOrEmpty(_dto.ItemName))
  1100. {
  1101. return Ok(JsonView(false, "物料名称为空"));
  1102. }
  1103. if (_dto.ItemTypeId < 1)
  1104. {
  1105. return Ok(JsonView(false, "未选择物料类型"));
  1106. }
  1107. if (_dto.SysUserId < 1)
  1108. {
  1109. return Ok(JsonView(false, "当前操作用户Id为空"));
  1110. }
  1111. if (_dto.CurrRate <= 0)
  1112. {
  1113. return Ok(JsonView(false, "物料录入价格不能小于等于0"));
  1114. }
  1115. if (_dto.EditType == 0)
  1116. {
  1117. //判断物料名称、类型、供应商全部重复
  1118. var checkEmpty = _resItemInfoRep.Query<Res_ItemDetailInfo>(s => s.ItemName == _dto.ItemName && s.ItemTypeId == _dto.ItemTypeId && s.VendorId == _dto.VendorId).First();
  1119. if (checkEmpty != null)
  1120. {
  1121. return Ok(JsonView(false, "已存在重复物料信息"));
  1122. }
  1123. Res_ItemDetailInfo _entity = _mapper.Map<Res_ItemDetailInfo>(_dto);
  1124. DateTime dtNow = DateTime.Now;
  1125. _entity.CreateUserId = _dto.SysUserId;
  1126. _entity.IsDel = 0;
  1127. _entity.MaxRate = _dto.CurrRate;
  1128. _entity.MaxDt = dtNow;
  1129. _entity.CurrRate = _dto.CurrRate;
  1130. _entity.CurrDt = dtNow;
  1131. _entity.MinRate = _dto.CurrRate;
  1132. _entity.MinDt = dtNow;
  1133. rst = await _resItemInfoRep.AddAsync<Res_ItemDetailInfo>(_entity) > 0;
  1134. }
  1135. else if (_dto.EditType == 1)
  1136. {
  1137. if (_dto.ItemId > 0)
  1138. {
  1139. Res_ItemDetailInfo _entity = _mapper.Map<Res_ItemDetailInfo>(_dto);
  1140. _entity.Id = _dto.ItemId;
  1141. rst = await _resItemInfoRep.updItemInfo(_entity);
  1142. }
  1143. else
  1144. {
  1145. return Ok(JsonView(false, "供应商不存在"));
  1146. }
  1147. }
  1148. }
  1149. else
  1150. {
  1151. if (_dto.ItemId < 1 || _dto.SysUserId < 1)
  1152. {
  1153. return Ok(JsonView(false, "用户Id或物料信息Id不存在"));
  1154. }
  1155. Res_ItemDetailInfo _entity = _mapper.Map<Res_ItemDetailInfo>(_dto);
  1156. rst = await _resItemInfoRep.delItemInfo(_entity);
  1157. }
  1158. return Ok(JsonView(rst));
  1159. }
  1160. #endregion
  1161. #region 物料类型获取
  1162. #endregion
  1163. #endregion
  1164. #region 备忘录
  1165. /// <summary>
  1166. /// 备忘录查询
  1167. /// </summary>
  1168. /// <param name="_jsonDto">Json序列化</param>
  1169. /// <returns></returns>
  1170. [HttpPost]
  1171. [ProducesResponseType(typeof(JsonView), StatusCodes.Status200OK)]
  1172. public async Task<IActionResult> PostSearchMemo(JsonDtoBase _jsonDto)
  1173. {
  1174. if (string.IsNullOrEmpty(_jsonDto.Paras))
  1175. {
  1176. return Ok(JsonView(false, "参数为空"));
  1177. }
  1178. Search_ResMemoDto _memoDto = JsonConvert.DeserializeObject<Search_ResMemoDto>(_jsonDto.Paras);
  1179. if (_memoDto != null)
  1180. {
  1181. if (_memoDto.SearchType == 2)
  1182. {
  1183. //获取列表
  1184. string sqlWhere = string.Format(" Where IsDel=0 ");
  1185. #region SqlWhere
  1186. if (!string.IsNullOrEmpty(_memoDto.Abstracts))
  1187. {
  1188. sqlWhere += string.Format(@" And m.Abstracts Like '%{0}%' ", _memoDto.Abstracts);
  1189. }
  1190. if (!string.IsNullOrEmpty(_memoDto.Title))
  1191. {
  1192. sqlWhere += string.Format(@" And m.Title Like '%{0}%' ", _memoDto.Title);
  1193. }
  1194. if (_memoDto.ReadLevel > 0)
  1195. {
  1196. sqlWhere += string.Format(@" And m.ReadLevel = {0} ", _memoDto.ReadLevel);
  1197. }
  1198. #endregion
  1199. int currPIndex = (((_memoDto.PageIndex > 0) ? (_memoDto.PageIndex - 1) : 0) * _memoDto.PageSize) + 1;
  1200. int currPSize = (((_memoDto.PageIndex > 0) ? (_memoDto.PageIndex - 1) : 0) + 1) * _memoDto.PageSize;
  1201. string sql = string.Format(@" Select * From(Select ROW_NUMBER() Over(order By m.Id desc) as RowNumber,
  1202. m.Id as MemoId,d.DepName as DepartmentName,m.ReadLevel,m.Title,m.Abstracts,
  1203. m.LastedEditDt,m.LastedEditorId
  1204. From Res_Memo as m With(Nolock) Inner Join Sys_Users as u With(Nolock) On m.CreateUserId=u.Id
  1205. Inner Join Sys_Department as d With(Nolock) On u.DepId=d.Id {2}
  1206. ) as tb Where tb.RowNumber Between {0} And {1} ", currPIndex, currPSize, sqlWhere);
  1207. Res_MemoListView rst = new Res_MemoListView();
  1208. rst.CurrPageIndex = currPIndex;
  1209. rst.CurrPageSize = currPSize;
  1210. List<Res_MemoView> dataSource = _carDataRep._sqlSugar.SqlQueryable<Res_MemoView>(sql).ToList();
  1211. Dictionary<int, string> userDic = new Dictionary<int, string>();
  1212. foreach (var item in dataSource)
  1213. {
  1214. if (userDic.ContainsKey(item.LastedEditorId))
  1215. {
  1216. item.LastedEditor = userDic[item.LastedEditorId];
  1217. }
  1218. else
  1219. {
  1220. var _sysUser = _carDataRep.Query<Sys_Users>(s => s.Id == item.LastedEditorId).First();
  1221. userDic[item.LastedEditorId] = _sysUser.CnName;
  1222. item.LastedEditor = _sysUser.CnName;
  1223. }
  1224. }
  1225. rst.DataList = new List<Res_MemoView>(dataSource);
  1226. if (rst.DataList.Count > 0)
  1227. {
  1228. string sqlCount = string.Format(@" Select Id From Res_Memo as m With(Nolock) {0} ", sqlWhere);
  1229. int dataCount = _carDataRep._sqlSugar.SqlQueryable<Res_MemoInfo>(sqlCount).Count();
  1230. rst.DataCount = dataCount;
  1231. }
  1232. return Ok(JsonView(rst));
  1233. }
  1234. else
  1235. {
  1236. //获取对象
  1237. string sqlSingle = string.Format(@" Select
  1238. m.Id as MemoId,d.DepName as DepartmentName,m.ReadLevel,m.Title,m.Abstracts,
  1239. m.LastedEditDt,m.LastedEditor,m.MDFilePath
  1240. From Res_Memo as m With(Nolock) Inner Join Sys_Users as u With(Nolock) On m.CreateUserId=u.Id
  1241. Inner Join Sys_Department as d With(Nolock) On u.DepId=d.Id Where m.Id={0} ", _memoDto.MemoId);
  1242. Res_MemoView _result = _carDataRep._sqlSugar.SqlQueryable<Res_MemoView>(sqlSingle).First();
  1243. if (_result != null)
  1244. {
  1245. var _sysUser = _carDataRep.Query<Sys_Users>(s => s.Id == _result.LastedEditorId).First();
  1246. _result.LastedEditor = _sysUser.CnName;
  1247. _result.MarkDownContent = new IOOperatorHelper().Read(_result.MDFilePath);
  1248. return Ok(JsonView(_result));
  1249. }
  1250. }
  1251. }
  1252. else
  1253. {
  1254. return Ok(JsonView(false, "参数反序列化失败"));
  1255. }
  1256. return Ok(JsonView(false));
  1257. }
  1258. /// <summary>
  1259. /// 创建/编辑/删除备忘录信息
  1260. /// </summary>
  1261. /// <param name="_dto"></param>
  1262. /// <returns></returns>
  1263. [HttpPost]
  1264. [ProducesResponseType(typeof(JsonView), StatusCodes.Status200OK)]
  1265. public async Task<IActionResult> PostEditMemo(Edit_ResMemoDto _dto)
  1266. {
  1267. bool rst = false;
  1268. if (_dto.SysUserId < 1)
  1269. {
  1270. return Ok(JsonView(false, "操作人失效"));
  1271. }
  1272. var _sysUser = _resItemInfoRep.Query<Sys_Users>(s => s.Id == _dto.SysUserId).First();
  1273. if (_sysUser == null)
  1274. {
  1275. return Ok(JsonView(false, "操作人失效02"));
  1276. }
  1277. if (_dto.EditType >= 0)
  1278. {
  1279. if (_dto.ReadLevel < 1)
  1280. {
  1281. return Ok(JsonView(false, "未知的阅读等级"));
  1282. }
  1283. if (string.IsNullOrEmpty(_dto.Title.Trim()))
  1284. {
  1285. return Ok(JsonView(false, "标题不能为空"));
  1286. }
  1287. if (string.IsNullOrEmpty(_dto.Content.Trim()))
  1288. {
  1289. return Ok(JsonView(false, "正文内容不能为空"));
  1290. }
  1291. //新增备忘录
  1292. DateTime dtNow = DateTime.Now;
  1293. string dir = AppSettingsHelper.Get("MemoCurrPath");
  1294. string fileName = dtNow.ToString("yyyyMMddHHmmss") + _dto.Title + ".md";
  1295. string content = JsonConvert.SerializeObject(_dto.Content);
  1296. if (_dto.EditType == 0)//新增
  1297. {
  1298. string savePath = new IOOperatorHelper().Write_CoverFile(content, dir, fileName);
  1299. if (savePath.Length > 0)
  1300. {
  1301. Res_MemoInfo _insert = new Res_MemoInfo();
  1302. _insert.Abstracts = _dto.Abstracts;
  1303. _insert.Title = _dto.Title;
  1304. _insert.DepartmentId = _sysUser.DepId;
  1305. _insert.CreateUserId = _sysUser.Id;
  1306. _insert.LastedEditDt = dtNow;
  1307. _insert.LastedEditor = _sysUser.Id;
  1308. _insert.MDFilePath = savePath;
  1309. _insert.ReadLevel = _dto.ReadLevel;
  1310. _insert.Title = _dto.Title;
  1311. int result = await _resItemInfoRep.AddAsync(_insert);
  1312. return Ok(JsonView(result > 0));
  1313. }
  1314. else
  1315. {
  1316. return Ok(JsonView(false, "路径保存失败"));
  1317. }
  1318. }
  1319. else//修改
  1320. {
  1321. if (_dto.MemoId < 1)
  1322. {
  1323. return Ok(JsonView(false, "MemoId不存在"));
  1324. }
  1325. Res_MemoInfo _source = _resItemInfoRep.Query<Res_MemoInfo>(s => s.Id == _dto.MemoId).First();
  1326. if (_source == null)
  1327. {
  1328. return Ok(JsonView(false, "MemoInfo不存在"));
  1329. }
  1330. //修改
  1331. string sourcePath = _source.MDFilePath;
  1332. string recycDir = AppSettingsHelper.Get("MemoRecycleBinPath");
  1333. new IOOperatorHelper().MoveFile(sourcePath, recycDir);
  1334. string savePath = new IOOperatorHelper().Write_CoverFile(content, dir, fileName);
  1335. if (savePath.Length > 0)
  1336. {
  1337. var result = await _resItemInfoRep._sqlSugar.Updateable<Res_MemoInfo>()
  1338. .SetColumns(it => it.LastedEditDt == DateTime.Now)
  1339. .SetColumns(it => it.LastedEditor == _sysUser.Id)
  1340. .SetColumns(it => it.Abstracts == _dto.Abstracts)
  1341. .SetColumns(it => it.MDFilePath == savePath)
  1342. .SetColumns(it => it.ReadLevel == _dto.ReadLevel)
  1343. .SetColumns(it => it.Title == _dto.Title)
  1344. .Where(s => s.Id == _source.Id)
  1345. .ExecuteCommandAsync();
  1346. return Ok(JsonView(result > 0));
  1347. }
  1348. }
  1349. }
  1350. else
  1351. {
  1352. //删除
  1353. if (_dto.MemoId < 1)
  1354. {
  1355. return Ok(JsonView(false, "MemoId不存在"));
  1356. }
  1357. Res_MemoInfo _source = _resItemInfoRep.Query<Res_MemoInfo>(s => s.Id == _dto.MemoId).First();
  1358. if (_source == null)
  1359. {
  1360. return Ok(JsonView(false, "MemoInfo不存在"));
  1361. }
  1362. //修改
  1363. string sourcePath = _source.MDFilePath;
  1364. string recycDir = AppSettingsHelper.Get("MemoRecycleBinPath");
  1365. new IOOperatorHelper().MoveFile(sourcePath, recycDir);
  1366. var result = await _resItemInfoRep._sqlSugar.Updateable<Res_MemoInfo>()
  1367. .SetColumns(it => it.IsDel == 1)
  1368. .SetColumns(it => it.DeleteUserId == _sysUser.Id)
  1369. .SetColumns(it => it.DeleteTime == DateTime.Now.ToString("yyyy-MM-dd HH:mm:ss"))
  1370. .Where(s => s.Id == _source.Id)
  1371. .ExecuteCommandAsync();
  1372. return Ok(JsonView(result > 0));
  1373. }
  1374. return Ok(JsonView(rst));
  1375. }
  1376. #endregion
  1377. #region 商邀资料
  1378. /// <summary>
  1379. /// 商邀资料 基础数据源
  1380. /// </summary>
  1381. /// <param name="dto"></param>
  1382. /// <returns></returns>
  1383. [HttpPost]
  1384. [ProducesResponseType(typeof(JsonView), StatusCodes.Status200OK)]
  1385. public async Task<IActionResult> QueryIOAInitData(QueryIOAInitDataDto dto)
  1386. {
  1387. #region 参数验证
  1388. if (dto.PortType < 1) return Ok(JsonView(false, "请传入有效的PortType参数!"));
  1389. #endregion
  1390. var ExcludedKeyStr = new string[] { "快递费" };
  1391. var ClientKeyStr = new string[] { "邀请函翻译" };
  1392. List<Grp_DelegationInfo> _DelegationInfos = _sqlSugar.Queryable<Grp_DelegationInfo>().Where(it => it.IsDel == 0).OrderByDescending(it => it.JietuanTime).ToList();
  1393. List<Res_InvitationOfficialActivityData> _ioaDatas = _sqlSugar.Queryable<Res_InvitationOfficialActivityData>()
  1394. .Where(it => it.IsDel == 0)
  1395. .Select(x => new Res_InvitationOfficialActivityData
  1396. {
  1397. Country = x.Country,
  1398. UnitName = x.UnitName,
  1399. Contact = x.Contact,
  1400. Field = x.Field,
  1401. })
  1402. .ToList();
  1403. foreach (var item in _ioaDatas)
  1404. {
  1405. EncryptionProcessor.DecryptProperties(item);
  1406. }
  1407. foreach (var item in ExcludedKeyStr)
  1408. {
  1409. _ioaDatas = _ioaDatas.Where(x => string.IsNullOrWhiteSpace(x.UnitName) || !x.UnitName.Contains(item)).ToList();
  1410. }
  1411. foreach (var item in ClientKeyStr)
  1412. {
  1413. _ioaDatas = _ioaDatas.Where(x => string.IsNullOrWhiteSpace(x.Contact) || !x.Contact.Contains(item)).ToList();
  1414. }
  1415. List<Sys_Users> _Users = _sqlSugar.Queryable<Sys_Users>().Where(it => it.IsDel == 0).ToList();
  1416. var _countryData = _ioaDatas.Select(it => it.Country).Distinct().ToList(); _countryData.Remove("");
  1417. var _inviterData = _ioaDatas.Select(it => it.UnitName).Distinct().ToList(); _inviterData.Remove("");
  1418. var _contactData = _ioaDatas.Select(it => it.Contact).Distinct().ToList(); _contactData.Remove("");
  1419. var _domainData = _ioaDatas.Select(it => it.Field).Distinct().ToList(); _domainData.Remove("");
  1420. var _groupNameData = _DelegationInfos.Select(it => new { it.Id, it.TeamName }).ToList();
  1421. var _userNameData = _Users.Select(it => new { it.Id, it.CnName }).ToList();
  1422. var _data = new
  1423. {
  1424. CountryData = _countryData,
  1425. InviterData = _inviterData,
  1426. DomainData = _domainData,
  1427. ContactData = _contactData,
  1428. GroupNameData = _groupNameData,
  1429. UserNameData = _userNameData,
  1430. };
  1431. return Ok(JsonView(true, $"查询成功!", _data));
  1432. }
  1433. /// <summary>
  1434. /// 商邀资料查询
  1435. /// </summary>
  1436. /// <param name="dto"></param>
  1437. /// <returns></returns>
  1438. [HttpPost]
  1439. [ProducesResponseType(typeof(JsonView), StatusCodes.Status200OK)]
  1440. public async Task<IActionResult> QueryInvitationOfficialActivityData(QueryInvitationOfficialActivityDataDto dto)
  1441. {
  1442. var ExcludedKeyStr = new string[] { "快递费" };
  1443. var ClientKeyStr = new string[] { "邀请函翻译" };
  1444. var columns = "Id,UnitName,Contact,City";
  1445. try
  1446. {
  1447. #region 参数验证
  1448. if (dto.PageIndex < 1) return Ok(JsonView(false, "请传入有效的PageIndex参数!"));
  1449. if (dto.PageSize < 1) return Ok(JsonView(false, "请传入有效的PageSize参数!"));
  1450. #endregion
  1451. //string sqlWhere = string.Empty;
  1452. if (!string.IsNullOrWhiteSpace(dto.Country)) { columns += ",Country"; }
  1453. //if (!string.IsNullOrWhiteSpace(dto.UnitName)) { columns += ",UnitName"; }
  1454. //if (!string.IsNullOrWhiteSpace(dto.Contact)) { columns += ",Contact"; }
  1455. if (!string.IsNullOrWhiteSpace(dto.Delegation)) { columns += ",Delegation"; }
  1456. if (!string.IsNullOrWhiteSpace(dto.Field)) { columns += ",Field"; }
  1457. //if (dto.CreateUserId != 0 && !string.IsNullOrWhiteSpace(dto.CreateUserId.ToString())) { columns += ",CreateUserId"; }
  1458. //if (!string.IsNullOrWhiteSpace(dto.StartCreateTime) && !string.IsNullOrWhiteSpace(dto.EndCreateTime))
  1459. //{
  1460. // columns += ",CreateUserId";
  1461. //}
  1462. DateTime endTime = new DateTime();
  1463. var InvitationOfficialActivityDataList = _sqlSugar.Queryable<Res_InvitationOfficialActivityData>()
  1464. .Where(x => x.IsDel == 0)
  1465. .WhereIF(dto.CreateUserId != 0 && !string.IsNullOrWhiteSpace(dto.CreateUserId.ToString()), x => x.CreateUserId == dto.CreateUserId)
  1466. .WhereIF(DateTime.TryParse(dto.StartCreateTime, out DateTime startTime) && DateTime.TryParse(dto.EndCreateTime, out endTime), x => x.CreateTime >= startTime && x.CreateTime <= endTime)
  1467. .Select(columns)
  1468. .ToList();
  1469. foreach (var item in InvitationOfficialActivityDataList)
  1470. {
  1471. EncryptionProcessor.DecryptProperties(item);
  1472. }
  1473. InvitationOfficialActivityDataList = InvitationOfficialActivityDataList
  1474. .WhereIF(!string.IsNullOrWhiteSpace(dto.Country), x => (!string.IsNullOrWhiteSpace(x.Country) && x.Country.Contains(dto.Country)) || (!string.IsNullOrWhiteSpace(x.City) && x.City.Contains(dto.Country)))
  1475. .WhereIF(!string.IsNullOrWhiteSpace(dto.UnitName), x => !string.IsNullOrWhiteSpace(x.UnitName) && x.UnitName.Contains(dto.UnitName))
  1476. .WhereIF(!string.IsNullOrWhiteSpace(dto.Contact), x => !string.IsNullOrWhiteSpace(x.Contact) && x.Contact.Contains(dto.Contact))
  1477. .WhereIF(!string.IsNullOrWhiteSpace(dto.Delegation), x => !string.IsNullOrWhiteSpace(x.Delegation) && x.Delegation.Contains(dto.Delegation))
  1478. .WhereIF(!string.IsNullOrWhiteSpace(dto.Field), x => !string.IsNullOrWhiteSpace(x.Field) && x.Field.Contains(dto.Field))
  1479. .ToList();
  1480. foreach (var item in ExcludedKeyStr)
  1481. {
  1482. InvitationOfficialActivityDataList = InvitationOfficialActivityDataList.Where(x => string.IsNullOrWhiteSpace(x.UnitName) || !x.UnitName.Contains(item)).ToList();
  1483. //sqlWhere += $" And i.UnitName not like '%{item}%' ";
  1484. }
  1485. foreach (var item in ClientKeyStr)
  1486. {
  1487. InvitationOfficialActivityDataList = InvitationOfficialActivityDataList.Where(x => string.IsNullOrWhiteSpace(x.Contact) || !x.Contact.Contains(item)).ToList();
  1488. //sqlWhere += $" And i.Contact not like '%{item}%' ";
  1489. }
  1490. var ids = InvitationOfficialActivityDataList.Select(x => x.Id);
  1491. RefAsync<int> totalCount = 0;
  1492. var _ivitiesViews = await _sqlSugar.Queryable<Res_InvitationOfficialActivityData>()
  1493. .LeftJoin<Sys_Users>((a, b) => b.IsDel == 0 && a.CreateUserId == b.Id)
  1494. .Where((a, b) => ids.Contains(a.Id))
  1495. .OrderByDescending((a, b) => a.CreateTime)
  1496. .Select((a, b) => new InvitationOfficialActivityDataView
  1497. {
  1498. FaceBook = a.FaceBook,
  1499. Id = a.Id,
  1500. CreateUserId = a.CreateUserId,
  1501. IsDel = 0,
  1502. Address = a.Address,
  1503. Background = a.Background,
  1504. City = a.City,
  1505. Contact = a.Contact,
  1506. Country = a.Country,
  1507. CreateTime = a.CreateTime,
  1508. CreateUserName = b.CnName,
  1509. Delegation = a.Delegation,
  1510. Email = a.Email,
  1511. Fax = a.Fax,
  1512. Field = a.Field,
  1513. FilePath = a.FilePath,
  1514. Ins = a.Ins,
  1515. WeChat = a.WeChat,
  1516. UnitWeb = a.UnitWeb,
  1517. UnitName = a.UnitName,
  1518. UnitInfo = a.UnitInfo,
  1519. Tel = a.Tel,
  1520. SndFilePath = a.SndFilePath,
  1521. SndFileName = a.SndFileName,
  1522. Remark = a.Remark,
  1523. OtherInfo = a.OtherInfo,
  1524. Job = a.Job
  1525. })
  1526. .ToPageListAsync(dto.PageIndex, dto.PageSize, totalCount);
  1527. var allGroupIds = new HashSet<int>();
  1528. foreach (var item in _ivitiesViews)
  1529. {
  1530. EncryptionProcessor.DecryptProperties(item);
  1531. if (!string.IsNullOrEmpty(item.Delegation))
  1532. {
  1533. allGroupIds.UnionWith(item.Delegation.Split(',').Select(x =>
  1534. {
  1535. int id;
  1536. if (int.TryParse(x, out id)) return id;
  1537. return 0;
  1538. }).Where(id => id != 0));
  1539. }
  1540. }
  1541. var _DelegationInfos = _sqlSugar.Queryable<Grp_DelegationInfo>()
  1542. .Where(x => allGroupIds.Contains(x.Id) && x.IsDel == 0)
  1543. .ToList()
  1544. .GroupBy(x => x.Id)
  1545. .ToDictionary(group => group.Key, group => group.Select(g => g.TeamName));
  1546. foreach (var item in _ivitiesViews)
  1547. {
  1548. string groupNameStr = "";
  1549. if (!string.IsNullOrEmpty(item.Delegation))
  1550. {
  1551. var groupIds = item.Delegation.Split(',').Select(x =>
  1552. {
  1553. int id;
  1554. if (int.TryParse(x, out id)) return id;
  1555. return 0;
  1556. })
  1557. .Where(id => id != 0)
  1558. .ToArray();
  1559. foreach (var id in groupIds)
  1560. {
  1561. if (_DelegationInfos.ContainsKey(id))
  1562. {
  1563. groupNameStr += string.Join("", _DelegationInfos[id]) + ",";
  1564. }
  1565. }
  1566. if (groupNameStr.Length > 1)
  1567. {
  1568. groupNameStr = groupNameStr.Substring(0, groupNameStr.Length - 1);
  1569. }
  1570. }
  1571. item.DelegationStr = groupNameStr;
  1572. }
  1573. return Ok(JsonView(true, $"查询成功!", _ivitiesViews, totalCount));
  1574. }
  1575. catch (Exception ex)
  1576. {
  1577. return Ok(JsonView(false, ex.Message));
  1578. }
  1579. }
  1580. /// <summary>
  1581. /// 商邀资料查询-欧洲
  1582. /// </summary>
  1583. /// <returns></returns>
  1584. [HttpPost]
  1585. [ProducesResponseType(typeof(JsonView), StatusCodes.Status200OK)]
  1586. public async Task<IActionResult> QueryInvitationOfficialActivityData1()
  1587. {
  1588. try
  1589. {
  1590. // 定义欧洲国家数组
  1591. string[] europeanCountries = {
  1592. // 北欧国家
  1593. "瑞典", "芬兰", "挪威", "丹麦", "冰岛",
  1594. // 西欧国家
  1595. "英国", "爱尔兰", "法国", "荷兰", "比利时", "卢森堡", "摩纳哥",
  1596. // 中欧国家
  1597. "德国", "瑞士", "奥地利", "波兰", "捷克", "斯洛伐克", "匈牙利", "列支敦士登",
  1598. // 南欧国家
  1599. "意大利", "西班牙", "葡萄牙", "希腊", "马耳他", "圣马力诺", "梵蒂冈", "安道尔",
  1600. "克罗地亚", "斯洛文尼亚", "波斯尼亚和黑塞哥维那", "黑山", "塞尔维亚", "北马其顿", "阿尔巴尼亚", "保加利亚", "罗马尼亚",
  1601. // 东欧国家
  1602. "俄罗斯", "乌克兰", "白俄罗斯", "摩尔多瓦", "爱沙尼亚", "拉脱维亚", "立陶宛"
  1603. };
  1604. var InvitationOfficialActivityDataList = _sqlSugar.Queryable<Res_InvitationOfficialActivityData>()
  1605. .Where(x => x.IsDel == 0)
  1606. .Select(x => new Res_InvitationOfficialActivityData() { Id = x.Id, Country = x.Country })
  1607. .ToList();
  1608. foreach (var item in InvitationOfficialActivityDataList)
  1609. {
  1610. EncryptionProcessor.DecryptProperties(item);
  1611. }
  1612. var ids = InvitationOfficialActivityDataList
  1613. .WhereIF(europeanCountries.Any(), x => !string.IsNullOrWhiteSpace(x.Country) && europeanCountries.Contains(x.Country))
  1614. .Select(x => x.Id)
  1615. .ToList();
  1616. RefAsync<int> totalCount = 0;
  1617. var _ivitiesViews = await _sqlSugar.Queryable<Res_InvitationOfficialActivityData>()
  1618. .LeftJoin<Sys_Users>((a, b) => b.IsDel == 0 && a.CreateUserId == b.Id)
  1619. .Where((a, b) => ids.Contains(a.Id))
  1620. .OrderByDescending((a, b) => a.CreateTime)
  1621. .Select((a, b) => new InvitationOfficialActivityDataView1
  1622. {
  1623. Country = a.Country,
  1624. City = a.City,
  1625. UnitName = a.UnitName,
  1626. Field = a.Field,
  1627. Address = a.Address,
  1628. UnitInfo = a.UnitInfo,
  1629. Delegation = a.Delegation,
  1630. OtherInfo = a.OtherInfo
  1631. })
  1632. .ToListAsync();
  1633. var allGroupIds = new HashSet<int>();
  1634. foreach (var item in _ivitiesViews)
  1635. {
  1636. EncryptionProcessor.DecryptProperties(item);
  1637. if (!string.IsNullOrEmpty(item.Delegation))
  1638. {
  1639. allGroupIds.UnionWith(item.Delegation.Split(',').Select(x =>
  1640. {
  1641. int id;
  1642. if (int.TryParse(x, out id)) return id;
  1643. return 0;
  1644. }).Where(id => id != 0));
  1645. }
  1646. }
  1647. var _DelegationInfos = _sqlSugar.Queryable<Grp_DelegationInfo>()
  1648. .Where(x => allGroupIds.Contains(x.Id) && x.IsDel == 0)
  1649. .ToList()
  1650. .GroupBy(x => x.Id)
  1651. .ToDictionary(group => group.Key, group => group.Select(g => g.TeamName));
  1652. foreach (var item in _ivitiesViews)
  1653. {
  1654. string groupNameStr = "";
  1655. if (!string.IsNullOrEmpty(item.Delegation))
  1656. {
  1657. var groupIds = item.Delegation.Split(',').Select(x =>
  1658. {
  1659. int id;
  1660. if (int.TryParse(x, out id)) return id;
  1661. return 0;
  1662. })
  1663. .Where(id => id != 0)
  1664. .ToArray();
  1665. foreach (var id in groupIds)
  1666. {
  1667. if (_DelegationInfos.ContainsKey(id))
  1668. {
  1669. groupNameStr += string.Join("", _DelegationInfos[id]) + ",";
  1670. }
  1671. }
  1672. if (groupNameStr.Length > 1)
  1673. {
  1674. groupNameStr = groupNameStr.Substring(0, groupNameStr.Length - 1);
  1675. }
  1676. }
  1677. item.DelegationStr = groupNameStr;
  1678. }
  1679. //获取模板
  1680. string tempPath = (AppSettingsHelper.Get("ExcelBasePath") + "Template/商邀资料模板.xls");
  1681. var designer = new WorkbookDesigner();
  1682. designer.Workbook = new Workbook(tempPath);
  1683. var dt = CommonFun.ToDataTableArray(_ivitiesViews);
  1684. dt.TableName = $"OADataView";
  1685. designer.SetDataSource(dt);
  1686. designer.Process();
  1687. //文件名
  1688. string fileName = $"商邀资料{DateTime.Now.ToString("yyyyMMddHHmmss")}.xls";
  1689. designer.Workbook.Save(AppSettingsHelper.Get("ExcelBasePath") + "InvitationOfficialActivityExport/" + fileName);
  1690. string url = AppSettingsHelper.Get("ExcelBaseUrl") + "Office/Excel/InvitationOfficialActivityExport/" + fileName;
  1691. return Ok(JsonView(true, "成功", url));
  1692. }
  1693. catch (Exception ex)
  1694. {
  1695. return Ok(JsonView(false, ex.Message));
  1696. }
  1697. }
  1698. /// <summary>
  1699. /// 根据商邀资料Id查询信息
  1700. /// </summary>
  1701. /// <param name="id"></param>
  1702. /// <returns></returns>
  1703. [HttpGet("{id}")]
  1704. [ProducesResponseType(typeof(JsonView), StatusCodes.Status200OK)]
  1705. public async Task<IActionResult> QueryInvitationOfficialActivityById(int id)
  1706. {
  1707. if (id < 1) return Ok(JsonView(false, "Id参数错误!"));
  1708. return Ok(await _InvitationOfficialActivityDataRep.Info(id));
  1709. }
  1710. /// <summary>
  1711. /// 商邀资料操作(Status:1.新增,2.修改)
  1712. /// </summary>
  1713. /// <param name="dto"></param>
  1714. /// <returns></returns>
  1715. [HttpPost]
  1716. [ProducesResponseType(typeof(JsonView), StatusCodes.Status200OK)]
  1717. public async Task<IActionResult> OpInvitationOfficialActivity([FromForm] OpInvitationOfficialActivityDto dto)
  1718. {
  1719. return Ok(await _InvitationOfficialActivityDataRep.IOA_OP(dto));
  1720. }
  1721. /// <summary>
  1722. /// 商邀资料 删除文件
  1723. /// </summary>
  1724. /// <param name="id"></param>
  1725. /// <param name="fileName"></param>
  1726. /// <returns></returns>
  1727. [HttpDelete("{id}")]
  1728. [ProducesResponseType(typeof(JsonView), StatusCodes.Status200OK)]
  1729. public async Task<IActionResult> InvitationOfficialActivityDelFile(int id, string fileName)
  1730. {
  1731. if (id < 1) return Ok(JsonView(false, "请传入有效的Id"));
  1732. if (string.IsNullOrEmpty(fileName)) return Ok(JsonView(false, "文件名称不能为空!"));
  1733. var info = await _sqlSugar.Queryable<Res_InvitationOfficialActivityData>().FirstAsync(x => x.Id == id);
  1734. if (info == null) return Ok(JsonView(false, "该条数据不存在!"));
  1735. var files = new List<string>();
  1736. try
  1737. {
  1738. files = JsonConvert.DeserializeObject<List<string>>(info.SndFileName);
  1739. }
  1740. catch (Exception) { }
  1741. if (files != null && files.Count > 0 && files.Contains(fileName))
  1742. {
  1743. var filePath = $@"{AppSettingsHelper.Get("GrpFileBasePath")}/商邀相关文件/{fileName}";
  1744. if (System.IO.File.Exists(filePath))
  1745. {
  1746. System.IO.File.Delete(filePath);
  1747. }
  1748. //更改文件值
  1749. files.Remove(fileName);
  1750. var fileUpd = await _sqlSugar.Updateable<Res_InvitationOfficialActivityData>()
  1751. .SetColumns(x => x.SndFileName == JsonConvert.SerializeObject(files))
  1752. .Where(x => x.Id == id)
  1753. .ExecuteCommandAsync();
  1754. if (fileUpd > 0) return Ok(JsonView(true, "操作成功!"));
  1755. }
  1756. return Ok(JsonView(false, "操作失败!"));
  1757. }
  1758. /// <summary>
  1759. /// 删除商邀资料信息
  1760. /// </summary>
  1761. /// <param name="dto"></param>
  1762. /// <returns></returns>
  1763. [HttpPost]
  1764. [ProducesResponseType(typeof(JsonView), StatusCodes.Status200OK)]
  1765. public async Task<IActionResult> DelInvitationOfficialActivity(DelBaseDto dto)
  1766. {
  1767. var res = await _InvitationOfficialActivityDataRep.SoftDeleteByIdAsync<Res_InvitationOfficialActivityData>(dto.Id.ToString(), dto.DeleteUserId);
  1768. if (!res) return Ok(JsonView(false, "删除失败"));
  1769. var info = await _sqlSugar.Queryable<Res_InvitationOfficialActivityData>().FirstAsync(x => x.Id == dto.Id);
  1770. if (info == null) return Ok(JsonView(false, "该条数据不存在!"));
  1771. var files = new List<string>();
  1772. try
  1773. {
  1774. files = JsonConvert.DeserializeObject<List<string>>(info.SndFileName);
  1775. }
  1776. catch (Exception) { }
  1777. if (files != null && files.Count > 0)
  1778. {
  1779. foreach (var fileName in files)
  1780. {
  1781. var filePath = $@"{AppSettingsHelper.Get("GrpFileBasePath")}/商邀相关文件/{fileName}";
  1782. if (System.IO.File.Exists(filePath))
  1783. {
  1784. System.IO.File.Delete(filePath);
  1785. }
  1786. }
  1787. }
  1788. return Ok(JsonView(true, "删除成功!"));
  1789. }
  1790. /// <summary>
  1791. /// 商邀资料 ExcelExport
  1792. /// </summary>
  1793. /// <param name="dto"></param>
  1794. /// <returns></returns>
  1795. [HttpPost]
  1796. [ProducesResponseType(typeof(JsonView), StatusCodes.Status200OK)]
  1797. public async Task<IActionResult> InvitationOfficialActivityExcelExport(QueryInvitationOfficialActivityDataDto dto)
  1798. {
  1799. var ExcludedKeyStr = new string[] { "快递费" };
  1800. var ClientKeyStr = new string[] { "邀请函翻译" };
  1801. var columns = "Id,UnitName,Contact";
  1802. #region 参数验证
  1803. if (dto.PageIndex < 1) return Ok(JsonView(false, "请传入有效的PageIndex参数!"));
  1804. if (dto.PageSize < 1) return Ok(JsonView(false, "请传入有效的PageSize参数!"));
  1805. #endregion
  1806. if (!string.IsNullOrWhiteSpace(dto.Country)) { columns += ",Country"; }
  1807. if (!string.IsNullOrWhiteSpace(dto.Delegation)) { columns += ",Delegation"; }
  1808. if (!string.IsNullOrWhiteSpace(dto.Field)) { columns += ",Field"; }
  1809. DateTime endTime = new DateTime();
  1810. var InvitationOfficialActivityDataList = _sqlSugar.Queryable<Res_InvitationOfficialActivityData>()
  1811. .Where(x => x.IsDel == 0)
  1812. .WhereIF(dto.CreateUserId != 0 && !string.IsNullOrWhiteSpace(dto.CreateUserId.ToString()), x => x.CreateUserId == dto.CreateUserId)
  1813. .WhereIF(DateTime.TryParse(dto.StartCreateTime, out DateTime startTime) && DateTime.TryParse(dto.EndCreateTime, out endTime), x => x.CreateTime >= startTime && x.CreateTime <= endTime)
  1814. .Select(columns)
  1815. .ToList();
  1816. foreach (var item in InvitationOfficialActivityDataList)
  1817. {
  1818. EncryptionProcessor.DecryptProperties(item);
  1819. }
  1820. InvitationOfficialActivityDataList = InvitationOfficialActivityDataList
  1821. .WhereIF(!string.IsNullOrWhiteSpace(dto.Country), x => !string.IsNullOrWhiteSpace(x.Country) && x.Country.Contains(dto.Country))
  1822. .WhereIF(!string.IsNullOrWhiteSpace(dto.UnitName), x => !string.IsNullOrWhiteSpace(x.UnitName) && x.UnitName.Contains(dto.UnitName))
  1823. .WhereIF(!string.IsNullOrWhiteSpace(dto.Contact), x => !string.IsNullOrWhiteSpace(x.Contact) && x.Contact.Contains(dto.Contact))
  1824. .WhereIF(!string.IsNullOrWhiteSpace(dto.Delegation), x => !string.IsNullOrWhiteSpace(x.Delegation) && x.Delegation.Contains(dto.Delegation))
  1825. .WhereIF(!string.IsNullOrWhiteSpace(dto.Field), x => !string.IsNullOrWhiteSpace(x.Field) && x.Field.Contains(dto.Field))
  1826. .ToList();
  1827. foreach (var item in ExcludedKeyStr)
  1828. {
  1829. InvitationOfficialActivityDataList = InvitationOfficialActivityDataList.Where(x => string.IsNullOrWhiteSpace(x.UnitName) || !x.UnitName.Contains(item)).ToList();
  1830. //sqlWhere += $" And i.UnitName not like '%{item}%' ";
  1831. }
  1832. foreach (var item in ClientKeyStr)
  1833. {
  1834. InvitationOfficialActivityDataList = InvitationOfficialActivityDataList.Where(x => string.IsNullOrWhiteSpace(x.Contact) || !x.Contact.Contains(item)).ToList();
  1835. //sqlWhere += $" And i.Contact not like '%{item}%' ";
  1836. }
  1837. var ids = InvitationOfficialActivityDataList.Select(x => x.Id);
  1838. var _ivitiesViews = await _sqlSugar.Queryable<Res_InvitationOfficialActivityData>()
  1839. .LeftJoin<Sys_Users>((a, b) => b.IsDel == 0 && a.CreateUserId == b.Id)
  1840. .Where((a, b) => ids.Contains(a.Id))
  1841. .OrderByDescending((a, b) => a.CreateTime)
  1842. .Select((a, b) => new IOAInfoView
  1843. {
  1844. FaceBook = a.FaceBook,
  1845. Id = a.Id,
  1846. Address = a.Address,
  1847. Background = a.Background,
  1848. City = a.City,
  1849. Contact = a.Contact,
  1850. Country = a.Country,
  1851. Delegation = a.Delegation,
  1852. Email = a.Email,
  1853. Fax = a.Fax,
  1854. Field = a.Field,
  1855. Ins = a.Ins,
  1856. WeChat = a.WeChat,
  1857. UnitWeb = a.UnitWeb,
  1858. UnitName = a.UnitName,
  1859. UnitInfo = a.UnitInfo,
  1860. Tel = a.Tel,
  1861. SndFileName = a.SndFileName,
  1862. Remark = a.Remark,
  1863. OtherInfo = a.OtherInfo,
  1864. Job = a.Job
  1865. })
  1866. .ToListAsync();
  1867. if (!_ivitiesViews.Any()) return Ok(JsonView(false, $"您查询的数据暂无内容,不可导出!"));
  1868. var allGroupIds = new HashSet<int>();
  1869. foreach (var item in _ivitiesViews)
  1870. {
  1871. EncryptionProcessor.DecryptProperties(item);
  1872. if (!string.IsNullOrEmpty(item.Delegation))
  1873. {
  1874. allGroupIds.UnionWith(
  1875. item.Delegation.Split(',')
  1876. .Select(x =>
  1877. {
  1878. int id;
  1879. if (int.TryParse(x, out id)) return id;
  1880. return 0;
  1881. })
  1882. .Where(id => id != 0)
  1883. );
  1884. }
  1885. }
  1886. var _DelegationInfos = _sqlSugar.Queryable<Grp_DelegationInfo>()
  1887. .Where(x => allGroupIds.Contains(x.Id) && x.IsDel == 0)
  1888. .ToList()
  1889. .GroupBy(x => x.Id)
  1890. .ToDictionary(group => group.Key, group => group.Select(g => g.TeamName));
  1891. foreach (var item in _ivitiesViews)
  1892. {
  1893. string groupNameStr = "";
  1894. if (!string.IsNullOrEmpty(item.Delegation))
  1895. {
  1896. var groupIds = item.Delegation.Split(',').Select(x =>
  1897. {
  1898. int id;
  1899. if (int.TryParse(x, out id)) return id;
  1900. return 0;
  1901. })
  1902. .Where(id => id != 0)
  1903. .ToArray();
  1904. foreach (var id in groupIds)
  1905. {
  1906. if (_DelegationInfos.ContainsKey(id))
  1907. {
  1908. groupNameStr += string.Join("", _DelegationInfos[id]) + ",";
  1909. }
  1910. }
  1911. if (groupNameStr.Length > 1)
  1912. {
  1913. groupNameStr = groupNameStr.Substring(0, groupNameStr.Length - 1);
  1914. }
  1915. }
  1916. item.DelegationStr = groupNameStr;
  1917. }
  1918. //获取模板
  1919. string tempPath = (AppSettingsHelper.Get("ExcelBasePath") + "Template/商邀资料模板.xls");
  1920. var designer = new WorkbookDesigner();
  1921. designer.Workbook = new Workbook(tempPath);
  1922. var dt = CommonFun.ToDataTableArray(_ivitiesViews);
  1923. dt.TableName = $"OADataView";
  1924. designer.SetDataSource(dt);
  1925. designer.Process();
  1926. //文件名
  1927. string fileName = $"商邀资料{DateTime.Now.ToString("yyyyMMddHHmmss")}.xls";
  1928. designer.Workbook.Save(AppSettingsHelper.Get("ExcelBasePath") + "InvitationOfficialActivityExport/" + fileName);
  1929. string url = AppSettingsHelper.Get("ExcelBaseUrl") + "Office/Excel/InvitationOfficialActivityExport/" + fileName;
  1930. return Ok(JsonView(true, "成功", url));
  1931. }
  1932. /// <summary>
  1933. /// 商邀资料 导入文件(Excel)模板下载
  1934. /// </summary>
  1935. /// <returns></returns>
  1936. [HttpGet]
  1937. [ProducesResponseType(typeof(JsonView), StatusCodes.Status200OK)]
  1938. public async Task<IActionResult> InvitationOfficialActivityExcelTemplateExport()
  1939. {
  1940. //获取模板
  1941. string fileName = $"商邀资料上传文件模板.xls";
  1942. string url = AppSettingsHelper.Get("ExcelBaseUrl") + "Office/Excel/Template/" + fileName;
  1943. return Ok(JsonView(true, "成功", url));
  1944. }
  1945. /// <summary>
  1946. /// 商邀资料 上传Excel文件添加
  1947. /// </summary>
  1948. /// <param name="file"></param>
  1949. /// <param name="currUserId"></param>
  1950. /// <returns></returns>
  1951. [HttpPost]
  1952. [ProducesResponseType(typeof(JsonView), StatusCodes.Status200OK)]
  1953. public async Task<IActionResult> InvitationOfficialActivityExcelUpload(IFormFile file, int currUserId)
  1954. {
  1955. var fileName = file.FileName;
  1956. var filePostfix = fileName.Split(".")[1].ToUpper();
  1957. if (!filePostfix.Contains("XLS")) return Ok(JsonView(false, $"请传入Excel文件"));
  1958. if (file.Length < 1) return Ok(JsonView(false, $"请传入有效的文件"));
  1959. if (currUserId < 1) return Ok(JsonView(false, $"请传入有效的UserId"));
  1960. //保存文件
  1961. string fileDirPath = string.Format(@"{0}InvitationOfficialActivityExcelUpload/", AppSettingsHelper.Get("ExcelBasePath"));
  1962. if (!Directory.Exists(fileDirPath))
  1963. {
  1964. Directory.CreateDirectory(fileDirPath);
  1965. }
  1966. var filePath = Path.Combine(fileDirPath, fileName);
  1967. await using var stream = new FileStream(filePath, FileMode.Create);
  1968. await file.CopyToAsync(stream);
  1969. //读取保存的文件
  1970. Workbook workbook = new Workbook(filePath);
  1971. if (workbook == null) return Ok(JsonView(false, $"Excel文件不存在!"));
  1972. Worksheet worksheet = workbook.Worksheets[0];
  1973. if (worksheet == null) return Ok(JsonView(false, $"Excel文件工作簿不存在!"));
  1974. var infos = new List<Res_InvitationOfficialActivityData>();
  1975. var groupNames = await _sqlSugar.Queryable<Grp_DelegationInfo>().Where(x => x.IsDel == 0 && !string.IsNullOrEmpty(x.TeamName)).Select(x => new { x.Id, x.TeamName }).ToListAsync();
  1976. for (int row = 1; row < worksheet.Cells.MaxDataRow + 1; row++)
  1977. {
  1978. var groupIdsLabel = string.Empty;
  1979. var groupNames1 = worksheet.Cells[row, 14].StringValue ?? "-";
  1980. if (!string.IsNullOrEmpty(groupNames1))
  1981. {
  1982. if (groupNames1.Contains(";"))
  1983. {
  1984. var groupNameArray = groupNames1.Split(";").Where(x => !string.IsNullOrEmpty(x)).ToList();
  1985. var groupIds = groupNames.Where(x => groupNameArray.Contains(x.TeamName)).Select(x => x.Id).ToList();
  1986. if (groupIds.Any()) groupIdsLabel = string.Join(',', groupIds);
  1987. }
  1988. else groupIdsLabel = groupNames.Where(x => groupNames1.Equals(x.TeamName)).FirstOrDefault()?.Id.ToString() ?? string.Empty;
  1989. }
  1990. infos.Add(new Res_InvitationOfficialActivityData()
  1991. {
  1992. Country = worksheet.Cells[row, 0].StringValue ?? "-",
  1993. City = worksheet.Cells[row, 1].StringValue ?? "-",
  1994. UnitName = worksheet.Cells[row, 2].StringValue ?? "-",
  1995. Field = worksheet.Cells[row, 3].StringValue ?? "-",
  1996. Address = worksheet.Cells[row, 4].StringValue ?? "-",
  1997. UnitInfo = worksheet.Cells[row, 5].StringValue ?? "-",
  1998. Contact = worksheet.Cells[row, 6].StringValue ?? "-",
  1999. Job = worksheet.Cells[row, 7].StringValue ?? "-",
  2000. Tel = worksheet.Cells[row, 8].StringValue ?? "-",
  2001. Email = worksheet.Cells[row, 9].StringValue ?? "-",
  2002. WeChat = worksheet.Cells[row, 10].StringValue ?? "-",
  2003. FaceBook = worksheet.Cells[row, 11].StringValue ?? "-",
  2004. Ins = worksheet.Cells[row, 12].StringValue ?? "-",
  2005. Fax = worksheet.Cells[row, 13].StringValue ?? "-",
  2006. Delegation = groupIdsLabel,
  2007. SndFilePath = worksheet.Cells[row, 15].StringValue ?? "-",
  2008. OtherInfo = worksheet.Cells[row, 16].StringValue ?? "-",
  2009. Remark = $"[{DateTime.Now.ToString("yyyy-MM-dd HH:mm:ss")}] Excel上传文件添加! 文件存储地址[{filePath}]",
  2010. CreateUserId = currUserId,
  2011. IsDel = 0
  2012. });
  2013. }
  2014. //加密
  2015. foreach (var item in infos) EncryptionProcessor.EncryptProperties(item);
  2016. var addCount = await _sqlSugar.Insertable(infos).ExecuteCommandAsync();
  2017. if (addCount < 1) return Ok(JsonView(false, "文件上传添加失败!"));
  2018. return Ok(JsonView(true, $"上传文件添加成功!共添加{addCount}条!"));
  2019. }
  2020. #endregion
  2021. #region 商邀资料 AI
  2022. /// <summary>
  2023. /// 商邀资料AI 基础数据源
  2024. /// </summary>
  2025. /// <returns></returns>
  2026. [HttpGet]
  2027. [ProducesResponseType(typeof(JsonView), StatusCodes.Status200OK)]
  2028. public async Task<IActionResult> InvitationAIInit()
  2029. {
  2030. var itemNames = await InvitationAIInvName();
  2031. var unitNames = await InvitationAIClientName();
  2032. var countries = await InvitationAICountryName();
  2033. var industryNodes = IndustryTree.Build().Select(x => x.NameCn).ToList();
  2034. var orgScales = OrgScale.BuildInitialData().Select(x => x.Name).ToList();
  2035. var orgLevels = new List<string>() {
  2036. "全部",
  2037. "总部",
  2038. "分公司"
  2039. };
  2040. return Ok(JsonView(true, $"查询成功!", new
  2041. {
  2042. itemNames,
  2043. unitNames,
  2044. countries,
  2045. industryNodes,
  2046. orgScales,
  2047. orgLevels
  2048. }));
  2049. }
  2050. /// <summary>
  2051. /// 商邀资料AI 资料列表
  2052. /// </summary>
  2053. /// <returns></returns>
  2054. [HttpGet("{name}")]
  2055. [ProducesResponseType(typeof(JsonView), StatusCodes.Status200OK)]
  2056. public async Task<IActionResult> InvitationAIItemByName(string name)
  2057. {
  2058. if (string.IsNullOrWhiteSpace(name)) return Ok(JsonView(false, "名称无效"));
  2059. // 1. 获取主表记录
  2060. var info = await _sqlSugar.Queryable<Res_InvitationAI>()
  2061. .FirstAsync(x => x.IsDel == 0 && x.InvName == name);
  2062. var groupInfo = await _sqlSugar.Queryable<Grp_DelegationInfo>().FirstAsync(x => x.IsDel == 0 && x.TeamName.Equals(name));
  2063. string baseUrl = AppSettingsHelper.Get("OfficeBaseUrl")?.TrimEnd('/');
  2064. // 2. 分支处理:若无主表记录,尝试从团组表同步基本信息
  2065. if (info == null)
  2066. {
  2067. return Ok(JsonView(true, "暂无数据", new
  2068. {
  2069. Id = 0,
  2070. GroupId = groupInfo?.Id ?? 0,
  2071. InvName = name,
  2072. AiCrawledDetails = new List<InvitationAIInfo>(),
  2073. Entry = new
  2074. {
  2075. Objective = groupInfo?.VisitPurpose ?? "商务会谈",
  2076. OriginUnit = groupInfo?.ClientUnit ?? "",
  2077. TargetCountry = _delegationInfoRep.GroupSplitCountry(groupInfo?.VisitCountry ?? "")
  2078. }
  2079. }));
  2080. }
  2081. // 3. 数据填充(使用 ??= 语法确保 Null 安全)
  2082. info.EntryInfo ??= new EntryInfo();
  2083. if (string.IsNullOrEmpty(info.EntryInfo.Objective))
  2084. info.EntryInfo.Objective = groupInfo?.VisitPurpose ?? "商务会谈";
  2085. if (string.IsNullOrEmpty(info.EntryInfo.OriginUnit))
  2086. info.EntryInfo.OriginUnit = groupInfo?.ClientUnit ?? "";
  2087. if (info.EntryInfo.TargetCountry?.Any() != true)
  2088. info.EntryInfo.TargetCountry = _delegationInfoRep.GroupSplitCountry(groupInfo?.VisitCountry ?? "");
  2089. // 4. 排序:ThenByDescending 确保多级排序生效
  2090. info.AiCrawledDetails = info.AiCrawledDetails
  2091. .OrderByDescending(x => x.IsChecked)
  2092. .ThenByDescending(x => x.OperatedAt)
  2093. .ToList();
  2094. var details = info?.AiCrawledDetails ?? new List<InvitationAIInfo>();
  2095. if (details.Any())
  2096. {
  2097. foreach (var detail in details)
  2098. {
  2099. // 附件路径处理
  2100. if (detail.EmailInfo?.AttachmentPaths?.Any() == true)
  2101. {
  2102. detail.EmailInfo.AttachmentPaths = detail.EmailInfo.AttachmentPaths
  2103. .Select(p => p.StartsWith("http") ? p : $"{baseUrl}/{p.TrimStart('/')}")
  2104. .ToList();
  2105. }
  2106. // 官网地址注入新闻首位 (确保 PostUrl 已实例化)
  2107. if (!string.IsNullOrEmpty(detail.SiteUrl))
  2108. {
  2109. detail.PostUrl ??= new List<PostNewsItem>();
  2110. // 避免重复插入
  2111. if (!detail.PostUrl.Any(x => x.Description == "官网"))
  2112. {
  2113. detail.PostUrl.Insert(0, new PostNewsItem
  2114. {
  2115. Date = "-",
  2116. Description = "官网",
  2117. Url = detail.SiteUrl
  2118. });
  2119. }
  2120. }
  2121. }
  2122. }
  2123. return Ok(JsonView(true, "查询成功!", new
  2124. {
  2125. info.Id,
  2126. info.GroupId,
  2127. info.InvName,
  2128. AiCrawledDetails = details,
  2129. Entry = info.EntryInfo
  2130. }));
  2131. }
  2132. /// <summary>
  2133. /// 商邀资料AI 设置词条
  2134. /// </summary>
  2135. /// <returns></returns>
  2136. [HttpPost]
  2137. [ProducesResponseType(typeof(JsonView), StatusCodes.Status200OK)]
  2138. public async Task<IActionResult> InvitationAISetPrompt(InvitationAISetPromptDto dto)
  2139. {
  2140. // 基础校验
  2141. if (string.IsNullOrWhiteSpace(dto.InvName) || string.IsNullOrWhiteSpace(dto.Objective) || string.IsNullOrWhiteSpace(dto.OriginUnit) ||
  2142. dto.TargetCountry == null || dto.TargetCountry.Count == 0 || dto.Industries == null || dto.Industries.Count == 0 ||
  2143. dto.ScaleTypes == null || dto.ScaleTypes.Count == 0
  2144. )
  2145. return Ok(JsonView(false, "请传入有效的公务名称、单位名称、国家、出访目的、行业和规模类型!"));
  2146. var invName = dto.InvName;
  2147. var groupInfo = await _sqlSugar.Queryable<Grp_DelegationInfo>()
  2148. .Where(x => x.IsDel == 0 && x.TeamName.Equals(invName))
  2149. .Select(x => new {x.Id,x.TeamName, x.VisitPurpose })
  2150. .FirstAsync();
  2151. int groupId = groupInfo?.Id ?? 0;
  2152. #region 数据库操作
  2153. // 数据库信息获取方式
  2154. var dataInfo = await _sqlSugar.Queryable<Res_InvitationAI>().Where(x => x.IsDel == 0 && x.InvName.Equals( invName)).FirstAsync();
  2155. #region 词条信息
  2156. string operatorName = await _sqlSugar.Queryable<Sys_Users>().Where(x => x.IsDel == 0 && x.Id == dto.CurrUserId).Select(x => x.CnName).FirstAsync() ?? "-";
  2157. var entryInfo = new EntryInfo()
  2158. {
  2159. OriginUnit = dto.OriginUnit,
  2160. TargetCountry = dto.TargetCountry,
  2161. Objective = dto.Objective,
  2162. Industries = dto.Industries,
  2163. ScaleTypes = dto.ScaleTypes,
  2164. IsBackground = dto.IsBackground,
  2165. OrgLevel = dto.OrgLevel,
  2166. OtherConstraints = dto.OtherConstraints,
  2167. Operator = operatorName,
  2168. OperatedAt = DateTime.Now
  2169. };
  2170. #endregion
  2171. // 3.如果以上两种方式都没有查询到数据,则说明是新数据,需要添加到数据库
  2172. if (dataInfo == null)
  2173. {
  2174. // 3.1 新数据,需要添加到数据库
  2175. dataInfo = new Res_InvitationAI()
  2176. {
  2177. InvName = invName,
  2178. GroupId = groupId,
  2179. EntryInfo = entryInfo,
  2180. CreateUserId = dto.CurrUserId
  2181. };
  2182. var insert = await _sqlSugar.Insertable(dataInfo).ExecuteReturnIdentityAsync();
  2183. if (insert < 1)
  2184. {
  2185. return Ok(JsonView(false, $"词条信息新增失败!"));
  2186. }
  2187. dataInfo.Id = insert;
  2188. }
  2189. else
  2190. {
  2191. // 3.2 数据存在 则更新数据(覆盖原有的AI爬取详情,保留原有的其他字段不变)
  2192. dataInfo.EntryInfo = entryInfo;
  2193. var update = await _sqlSugar.Updateable(dataInfo).ExecuteCommandAsync();
  2194. if (update < 1)
  2195. {
  2196. return Ok(JsonView(false, $"词条信息更新失败!"));
  2197. }
  2198. }
  2199. #endregion
  2200. return Ok(JsonView(true, $"设置成功!", new
  2201. {
  2202. dataInfo.Id,
  2203. dataInfo.InvName,
  2204. dataInfo.GroupId,
  2205. Entry = dataInfo.EntryInfo,
  2206. }));
  2207. }
  2208. /// <summary>
  2209. /// 商邀资料AI 设置复选框选中 批量
  2210. /// </summary>
  2211. /// <param name="dto"></param>
  2212. /// <returns></returns>
  2213. [HttpPost]
  2214. public async Task<IActionResult> InvitationAISetChecked([FromBody] InvitationAISetCheckedDto dto)
  2215. {
  2216. // 1. 基础参数校验 (注意:这里移除了对 Guids.Any() 的强校验,允许空集合)
  2217. if (dto.Id < 1 || dto.CurrUserId < 1 || dto.Guids == null)
  2218. return Ok(JsonView(false, "请求参数不完整"));
  2219. // 2. 获取主记录
  2220. var invAiInfo = await _sqlSugar.Queryable<Res_InvitationAI>()
  2221. .FirstAsync(x => x.IsDel == 0 && x.Id == dto.Id);
  2222. if (invAiInfo?.AiCrawledDetails == null || !invAiInfo.AiCrawledDetails.Any())
  2223. return Ok(JsonView(false, "数据不存在或集合为空"));
  2224. // 3. 准备更新所需数据
  2225. var guidSet = dto.Guids.ToHashSet();
  2226. var now = DateTime.Now;
  2227. // 获取操作人姓名
  2228. string operatorName = await _sqlSugar.Queryable<Sys_Users>()
  2229. .Where(x => x.Id == dto.CurrUserId)
  2230. .Select(x => x.CnName)
  2231. .FirstAsync() ?? "-";
  2232. // 4. 单次遍历完成 重置 + 更新
  2233. // 如果 guidSet 为空,循环会将所有项的 IsChecked 置为 false
  2234. foreach (var item in invAiInfo.AiCrawledDetails)
  2235. {
  2236. if (guidSet.Contains(item.Guid))
  2237. {
  2238. item.IsChecked = true;
  2239. }
  2240. else
  2241. {
  2242. item.IsChecked = false;
  2243. }
  2244. item.OperatedAt = now;
  2245. item.Operator = operatorName;
  2246. }
  2247. // 5. 排序逻辑
  2248. // 即使是全量取消选中,也可以按最后操作时间排序,让最近变动的项在前
  2249. invAiInfo.AiCrawledDetails = invAiInfo.AiCrawledDetails
  2250. .OrderByDescending(x => x.OperatedAt)
  2251. .ToList();
  2252. // 6. 提交数据库
  2253. // 注意:SqlSugar 的 UpdateColumns 会序列化整个对象列表并覆盖数据库字段
  2254. await _sqlSugar.Updateable(invAiInfo)
  2255. .UpdateColumns(x => x.AiCrawledDetails)
  2256. .ExecuteCommandAsync();
  2257. return Ok(JsonView(true, guidSet.Any() ? "设置成功" : "已全部取消选中"));
  2258. }
  2259. /// <summary>
  2260. /// 商邀资料AI 保存
  2261. /// </summary>
  2262. /// <returns></returns>
  2263. [HttpPost]
  2264. [ProducesResponseType(typeof(JsonView), StatusCodes.Status200OK)]
  2265. public async Task<IActionResult> InvitationAISave(InvitationAISaveDto dto)
  2266. {
  2267. if (dto.Id < 1) return Ok(JsonView(false, "请先设置AI关键字"));
  2268. if (dto.CurrUserId < 1) return Ok(JsonView(false, "请传入用户Id"));
  2269. if (dto.AiCrawledDetail == null) return Ok(JsonView(false, "请选择保存的邀请方详情"));
  2270. var invAiInfo = await _sqlSugar.Queryable<Res_InvitationAI>()
  2271. .Where(x => x.Id == dto.Id)
  2272. .FirstAsync();
  2273. if (invAiInfo == null) return Ok(JsonView(false, "数据信息为空"));
  2274. var dataList = invAiInfo?.AiCrawledDetails;
  2275. if (dataList == null) return Ok(JsonView(false, "邀请方数据信息为空"));
  2276. var datas = dataList.Where(x => x.Guid != dto.AiCrawledDetail.Guid).ToList();
  2277. var editInfo = dto.AiCrawledDetail;
  2278. // 如果 Guid 为空,说明是新增数据,需要生成新的 Guid
  2279. if (string.IsNullOrEmpty(editInfo.Guid))
  2280. {
  2281. editInfo.Guid = Guid.NewGuid().ToString("N");
  2282. editInfo.Source = 2; // 标记为手动新增数据
  2283. editInfo.OperatedAt = DateTime.Now;
  2284. }
  2285. var opUserName = await _sqlSugar.Queryable<Sys_Users>().Where(x => x.IsDel == 0 && x.Id == dto.CurrUserId).Select(x => x.CnName).FirstAsync() ?? "-";
  2286. editInfo.Operator = opUserName;
  2287. editInfo.OperatedAt = DateTime.Now;
  2288. datas.Add(editInfo);
  2289. invAiInfo.AiCrawledDetails = datas.OrderByDescending(x => x.OperatedAt).ToList();
  2290. var editUpd = await _sqlSugar.Updateable(invAiInfo).UpdateColumns(x => x.AiCrawledDetails).ExecuteCommandAsync();
  2291. if (editUpd < 1)
  2292. {
  2293. return Ok(JsonView(true, "保存失败"));
  2294. }
  2295. return Ok(JsonView(true, "保存成功"));
  2296. }
  2297. /// <summary>
  2298. /// 商邀资料AI 单条删除
  2299. /// </summary>
  2300. /// <returns></returns>
  2301. [HttpPost]
  2302. [ProducesResponseType(typeof(JsonView), StatusCodes.Status200OK)]
  2303. public async Task<IActionResult> InvitationAISingleDel(InvitationAISingleDelDto dto)
  2304. {
  2305. int id = dto.Id;
  2306. string guid = dto.Guid;
  2307. // 基础校验
  2308. if (id < 1 || string.IsNullOrWhiteSpace(guid))
  2309. return Ok(JsonView(false, "请传入有效的Id和Guid!"));
  2310. var dataInfo = await _sqlSugar.Queryable<Res_InvitationAI>().Where(x => x.IsDel == 0 && x.Id == id).FirstAsync();
  2311. if (dataInfo == null)
  2312. return Ok(JsonView(false, "当前数据信息不存在!"));
  2313. var newDataInfos = dataInfo.AiCrawledDetails.Where(x => x.Guid != guid).ToList();
  2314. dataInfo.AiCrawledDetails = newDataInfos.OrderByDescending(x => x.OperatedAt).ToList();
  2315. var update = await _sqlSugar.Updateable(dataInfo).ExecuteCommandAsync();
  2316. if (update < 1)
  2317. {
  2318. return Ok(JsonView(false, $"删除失败!"));
  2319. }
  2320. return Ok(JsonView(true, $"删除成功!", new
  2321. {
  2322. dataInfo.Id,
  2323. dataInfo.InvName,
  2324. dataInfo.GroupId,
  2325. dataInfo.AiCrawledDetails
  2326. }));
  2327. }
  2328. /// <summary>
  2329. /// 商邀资料AI AI查询出的数据添加至商邀资料库
  2330. /// </summary>
  2331. /// <returns></returns>
  2332. [HttpPost]
  2333. [ProducesResponseType(typeof(JsonView), StatusCodes.Status200OK)]
  2334. public async Task<IActionResult> InvitationAIInsertResource([FromBody] InvitationAIInsertResourceDto dto)
  2335. {
  2336. // 基础校验
  2337. if (dto.Id < 1 || dto.CurrUserId < 1 || dto.Guids == null || !dto.Guids.Any())
  2338. return Ok(JsonView(false, "参数不完整,请选择有效的单位数据"));
  2339. // 获取主记录
  2340. var dataInfo = await _sqlSugar.Queryable<Res_InvitationAI>()
  2341. .FirstAsync(x => x.IsDel == 0 && x.Id == dto.Id);
  2342. if (dataInfo?.AiCrawledDetails == null)
  2343. return Ok(JsonView(false, "当前数据信息不存在!"));
  2344. // 筛选出待转正的 AI 数据 (使用 HashSet 优化匹配)
  2345. var guidSet = dto.Guids.ToHashSet();
  2346. var targetAiDetails = dataInfo.AiCrawledDetails
  2347. .Where(x => x.Source != 0 && guidSet.Contains(x.Guid))
  2348. .ToList();
  2349. if (!targetAiDetails.Any())
  2350. return Ok(JsonView(false, "未找到符合条件的 AI 获取信息,无法重复添加或来源错误!"));
  2351. // 准备入库数据(批量构建)
  2352. var now = DateTime.Now;
  2353. var opUserName = await _sqlSugar.Queryable<Sys_Users>().Where(x => x.IsDel == 0 && x.Id == dto.CurrUserId).Select(x => x.CnName).FirstAsync() ?? "-";
  2354. var rawLocalDatas = targetAiDetails.Select(item => {
  2355. // 修改内存中的状态为本地数据
  2356. item.Source = 0;
  2357. item.OperatedAt = now;
  2358. item.Operator = opUserName;
  2359. return new Res_InvitationOfficialActivityData()
  2360. {
  2361. // 批量加密操作
  2362. Country = AesEncryptionHelper.Encrypt(item.Region),
  2363. UnitName = AesEncryptionHelper.Encrypt(item.NameCn),
  2364. Address = AesEncryptionHelper.Encrypt(item.Address),
  2365. Field = AesEncryptionHelper.Encrypt(item.Scope),
  2366. Contact = AesEncryptionHelper.Encrypt(item.Contact),
  2367. Tel = AesEncryptionHelper.Encrypt(item.Phone),
  2368. Email = AesEncryptionHelper.Encrypt(item.Email),
  2369. UnitWeb = AesEncryptionHelper.Encrypt(item.SiteUrl),
  2370. LastUpdateUserId = dto.CurrUserId,
  2371. LastUpdateTime = now,
  2372. CreateUserId = dto.CurrUserId,
  2373. CreateTime = now
  2374. };
  2375. }).ToList();
  2376. // 开启事务执行双表更新
  2377. var result = await _sqlSugar.UseTranAsync(async () =>
  2378. {
  2379. // 插入本地资料库
  2380. await _sqlSugar.Insertable(rawLocalDatas).ExecuteCommandAsync();
  2381. // 更新 AI 主表的状态(全量覆盖 JSON 列)
  2382. await _sqlSugar.Updateable(dataInfo)
  2383. .UpdateColumns(x => x.AiCrawledDetails)
  2384. .ExecuteCommandAsync();
  2385. });
  2386. return Ok(result.IsSuccess
  2387. ? JsonView(true, "成功添加至资料库并更新来源状态")
  2388. : JsonView(false, $"操作失败:{result.ErrorMessage}"));
  2389. }
  2390. /// <summary>
  2391. /// 商邀资料AI TokenHub AI查询资料(SSE流式推送)
  2392. /// </summary>
  2393. [HttpPost]
  2394. public async Task InvitationAISearchStreamProgress([FromBody] InvitationAISearchDto dto)
  2395. {
  2396. HttpContext.InitializeSse();
  2397. try
  2398. {
  2399. await HttpContext.SendSseStepAsync(5, "正在加载配置...");
  2400. #region 1. 异步并行化验证 (Performance Boost)
  2401. var invAiInfo = await _sqlSugar.Queryable<Res_InvitationAI>().Where(x => x.IsDel == 0 && x.Id == dto.Id).FirstAsync();
  2402. var operatorName = await _sqlSugar.Queryable<Sys_Users>()
  2403. .Where(x => x.IsDel == 0 && x.Id == dto.CurrUserId)
  2404. .Select(x => x.CnName).FirstAsync();
  2405. if (invAiInfo?.EntryInfo == null)
  2406. {
  2407. await HttpContext.SendSseStepAsync(-1, "未找到有效的关键字配置。");
  2408. return;
  2409. }
  2410. var entryInfo = invAiInfo.EntryInfo;
  2411. //entryInfo.NeedCount = 5;
  2412. var targetCountrySet = new HashSet<string>(entryInfo.TargetCountry);
  2413. #endregion
  2414. await HttpContext.SendSseStepAsync(20, "正在解析本地典籍 (并行解密)...");
  2415. #region 2. 内存计算优化
  2416. // 仅查询必要字段,减少 DataReader 压力
  2417. var rawLocalDatas = await _sqlSugar.Queryable<Res_InvitationOfficialActivityData>()
  2418. .Where(x => x.IsDel == 0 && !string.IsNullOrEmpty(x.UnitName))
  2419. .Select(x => new { x.Country, x.UnitName, x.Address, x.Field, x.Contact, x.Tel, x.Email })
  2420. .ToListAsync();
  2421. // PLINQ 核心炼金:利用多核并行解密
  2422. var allDecrypted = rawLocalDatas.AsParallel().Select(item => new InvitationAIInfo
  2423. {
  2424. Guid = Guid.NewGuid().ToString("N"),
  2425. Source = 0, // 标识来源:本地
  2426. Region = AesEncryptionHelper.Decrypt(item.Country),
  2427. NameCn = AesEncryptionHelper.Decrypt(item.UnitName),
  2428. Address = AesEncryptionHelper.Decrypt(item.Address),
  2429. Scope = AesEncryptionHelper.Decrypt(item.Field),
  2430. Contact = AesEncryptionHelper.Decrypt(item.Contact),
  2431. Phone = AesEncryptionHelper.Decrypt(item.Tel),
  2432. Email = AesEncryptionHelper.Decrypt(item.Email),
  2433. OperatedAt = DateTime.Now,
  2434. Operator = operatorName,
  2435. }).ToList();
  2436. // 筛选符合国家的本地数据
  2437. var matchedCountries = allDecrypted.Where(x => targetCountrySet.Contains(x.Region)).ToList();
  2438. //var matchedCountries = new List<InvitationAIInfo>();
  2439. #endregion
  2440. #region 3. 混元 AI 协同炼金 (双阶段)
  2441. // --- 阶段 A: 行业匹配分析 ---
  2442. if (matchedCountries.Any())
  2443. {
  2444. await HttpContext.SendSseStepAsync(40, $"AI 正在执行 {entryInfo.TargetCountry.Count} 国的行业契合度分析...");
  2445. string industryQuestion = BuildIndustryPrompt(entryInfo, matchedCountries);
  2446. string industryRaw = string.Empty;
  2447. var reply = await _tokenHubService.ChatAsync(new ChatRequest()
  2448. {
  2449. Messages = new List<OASystem.API.OAMethodLib.TokenHubAI.Models.ChatMessage>()
  2450. {
  2451. new()
  2452. {
  2453. Content = industryQuestion
  2454. }
  2455. }
  2456. });
  2457. if (reply.IsSuccess) industryRaw = reply?.Content ?? string.Empty;
  2458. else
  2459. {
  2460. await HttpContext.SendSseStepAsync(-1, $"TokenHub AI 行业匹配分析失败: {reply?.ErrorMessage}");
  2461. }
  2462. var industryMatches = CleanAndParseJson<List<IndustryMatchResult>>(industryRaw) ?? new();
  2463. var matchedNames = new HashSet<string>(industryMatches.Select(x => x.TargetUnitName));
  2464. // 重新过滤:仅保留 AI 认为匹配的本地数据
  2465. matchedCountries = matchedCountries.Where(x => !string.IsNullOrEmpty(x.NameCn) && matchedNames.Contains(x.NameCn)).Distinct().ToList();
  2466. }
  2467. //重新实例 用于查看Snovio结果
  2468. //matchedCountries = new List<InvitationAIInfo>();
  2469. // --- 阶段 B: 差额补全 (Gap Filling) ---
  2470. var localInvDatas = new List<InvitationAIInfo>();
  2471. var aiTasks = new List<CountryAIPormptInfo>();
  2472. foreach (var country in entryInfo.TargetCountry)
  2473. {
  2474. var countryData = matchedCountries.Where(x => x.Region == country).Take(entryInfo.NeedCount).ToList();
  2475. localInvDatas.AddRange(countryData);
  2476. int gap = entryInfo.NeedCount - countryData.Count;
  2477. if (gap > 0) aiTasks.Add(new() { Country = country, Count = gap });
  2478. }
  2479. var hunyuanAIInvDatas = new List<InvitationAIInfo>();
  2480. if (aiTasks.Any())
  2481. {
  2482. // MVP:跨境差额改用 Snovio 公司库检索(替代混元 BuildHunyuanPrompt + ChatAsync)。
  2483. // 保留变量名 hunyuanAIInvDatas:下游融合(local + 第三方)与持久化逻辑零改动。
  2484. await HttpContext.SendSseStepAsync(60, $"Snovio 正在跨境检索 {string.Join(", ", aiTasks.Select(x => $"{x.Country}({x.Count}条)"))} 单位资料...");
  2485. // 行业映射:Snovio 严格校验 industries 取值,中文原词直透必 422,
  2486. // 经 SnovioIndustryMapper 转英文(未映射的丢弃,全空则不传该条件保召回)。
  2487. var snovioIndustries = SnovioIndustryMapper.MapToSnovio(entryInfo.Industries);
  2488. // 规模仅单值透传:多规模/解析失败返回 null,不传规模保召回。
  2489. var snovioSize = ParseSnovioSize(entryInfo.ScaleTypes);
  2490. // 逐国单次检索:Snovio 单页结果按 gap 截取,单国异常只记日志、不熔断整管。
  2491. // 逐条进度:60 起每条 +1(封顶 79,80 留给汇总),消息显示“正在检索{国家}第{index}名潜客”。
  2492. var snovioProgress = 60;
  2493. foreach (var task in aiTasks)
  2494. {
  2495. try
  2496. {
  2497. // 用 Kimi 将中文国家名翻译成英文,用于 location 二次校验(每国翻译一次)。
  2498. var countryEn = await TranslateCountryToEnglishAsync(task.Country, invAiInfo.InvName);
  2499. // 尽力凑满:逐页拉取公司,边取边做 location 校验与邮箱补全,只统计真正可用的公司。
  2500. // 有效公司不足 task.Count 时继续翻页,MaxSnovioPages 封顶控成本;
  2501. // Snovio 无更多页/无更多数据则提前停止。
  2502. const int MaxSnovioPages = 5;
  2503. var collected = 0; // 已成功收集的有效公司数(location 匹配且有邮箱)
  2504. for (var page = 1; page <= MaxSnovioPages && collected < task.Count; page++)
  2505. {
  2506. // Snovio 请求:国家按 country 精确限定。
  2507. var request = new SnovioCompanySearchRequest
  2508. {
  2509. Page = page,
  2510. Filters = new SnovioCompanySearchFilters
  2511. {
  2512. Company = new SnovioCompanyFilter
  2513. {
  2514. Industries = snovioIndustries.Any()
  2515. ? new SnovioStringListFilter { Include = snovioIndustries }
  2516. : null,
  2517. // Snovio size 为数组,单值包单元素数组;null 表示不传该条件。
  2518. Size = string.IsNullOrWhiteSpace(snovioSize) ? null : new List<string> { snovioSize },
  2519. },
  2520. Locations = new SnovioLocationFilter
  2521. {
  2522. Include = new SnovioLocationFilterItem
  2523. {
  2524. Locality = task.Country,
  2525. LocationType = "country"
  2526. }
  2527. }
  2528. },
  2529. };
  2530. // SearchCompaniesAsync = Start + 轮询等待(默认 2min/3s),同步返回最终结果。
  2531. var result = await _snovioService.SearchCompaniesAsync(request);
  2532. var raw = result?.Data?.Companies ?? new List<SnovioCompanySearchCompany>();
  2533. // Snovio 无更多数据则停,避免空转烧点。
  2534. if (!raw.Any()) break;
  2535. // 字段映射:Snovio 公司接口仅有稀疏字段,联系人/邮箱需按
  2536. // 公司域名 → 域名潜客 → 潜客邮箱三跳补全;电话 Snovio 未提供,保持空。
  2537. // Region 取请求国,保证后续按国计数(NeedCount)稳定;Source=1 保持第三方标识,
  2538. // 复用 InvitationAIInsertResource(Source != 0 可入库)逻辑。
  2539. foreach (var c in raw)
  2540. {
  2541. if (collected >= task.Count) break;
  2542. // 按 location 二次校验国家:Snovio 的 country 过滤为宽松匹配,可能混入别国数据,
  2543. // 仅采信 location 明确包含目标国家的公司,避免“标记为请求国实为别国”的公司入库。
  2544. if (!IsLocationMatchesCountry(c.Location, countryEn))
  2545. {
  2546. _logger.LogDebug("公务名称:{InvName};公司 location 与目标国不符,跳过;Name={Name};Location={Location};Country={Country}",
  2547. invAiInfo.InvName, c.Name, c.Location, task.Country);
  2548. continue;
  2549. }
  2550. var info = new InvitationAIInfo
  2551. {
  2552. Guid = Guid.NewGuid().ToString("N"),
  2553. Source = 1,
  2554. Region = task.Country,
  2555. Industry = c.Industry ?? entryInfo.Industries?.FirstOrDefault(),
  2556. NameCn = c.Name,
  2557. NameEn = c.Name,
  2558. Address = c.Location,
  2559. Scope = c.Industry,
  2560. SiteUrl = string.IsNullOrWhiteSpace(c.Domain) ? string.Empty : $"https://{c.Domain.Trim()}",
  2561. PostUrl = new List<PostNewsItem>(),
  2562. RecLevel = "Backup",
  2563. Operator = operatorName,
  2564. OperatedAt = DateTime.Now,
  2565. };
  2566. // 联系人/邮箱补全:逐个潜客取邮箱,该公司所有潜客均无邮箱则跳过该家。
  2567. var hasEmail = await EnrichSnovioContactAsync(c.Domain, info);
  2568. if (!hasEmail)
  2569. {
  2570. _logger.LogWarning("公务名称:{InvName};公司无可用邮箱,跳过;Name={Name}", invAiInfo.InvName, c.Name);
  2571. continue;
  2572. }
  2573. hunyuanAIInvDatas.Add(info);
  2574. collected++;
  2575. // 仅对成功收集的公司计数并推送:进度展示“已成功收集的有效数”。
  2576. snovioProgress = Math.Min(snovioProgress + 1, 79);
  2577. await HttpContext.SendSseStepAsync(snovioProgress, $"正在检索{task.Country}第{collected}名潜客...");
  2578. }
  2579. // 已到 Snovio 最后一页则停。
  2580. if (page >= (result?.Data?.TotalPages ?? page)) break;
  2581. }
  2582. }
  2583. catch (Exception ex)
  2584. {
  2585. // 单国失败继续下一国;全失败时下游按“本地有数据仍成功”兜底。
  2586. _logger.LogWarning(ex, "公务名称:{InvName};Snovio 检索失败;Country={Country}", invAiInfo.InvName, task.Country);
  2587. }
  2588. }
  2589. if (hunyuanAIInvDatas.Any())
  2590. {
  2591. await HttpContext.SendSseStepAsync(80,
  2592. $"Snovio 已完成跨境检索,共获取 {hunyuanAIInvDatas.Count} 条单位资料");
  2593. }
  2594. else
  2595. {
  2596. await HttpContext.SendSseStepAsync(80, "本次未检索到符合条件的境外单位,建议调整国家或行业范围。");
  2597. }
  2598. }
  2599. #endregion
  2600. await HttpContext.SendSseStepAsync(90, "正在同步成果至持久化仓库...");
  2601. #region 4. 数据融合与更新
  2602. var finalResult = localInvDatas.Concat(hunyuanAIInvDatas).ToList();
  2603. // 仅更新指定列,性能更优
  2604. invAiInfo.AiCrawledDetails = finalResult.OrderByDescending(x => x.OperatedAt).ToList();
  2605. bool isOk = await _sqlSugar.Updateable(invAiInfo)
  2606. .UpdateColumns(x => x.AiCrawledDetails)
  2607. .ExecuteCommandHasChangeAsync();
  2608. if (!isOk) await HttpContext.SendSseStepAsync(-1, $"数据持久化失败。");
  2609. #endregion
  2610. // 5. 终焉推送
  2611. string finalMsg = "操作成功!资料已全部就绪。";
  2612. // 如果 AI 全部失败,但本地有数据,仍可成功
  2613. if (!finalResult.Any())
  2614. {
  2615. finalMsg = "本次未检索到符合条件的境外单位,建议调整国家或行业范围。";
  2616. }
  2617. await HttpContext.SendSseStepAsync(100, finalMsg, new
  2618. {
  2619. invAiInfo.Id,
  2620. AiCrawledDetails = finalResult.OrderByDescending(x => x.Source).ThenBy(x => x.Region).ToList()
  2621. });
  2622. }
  2623. catch (TokenHubException ex)
  2624. {
  2625. await HttpContext.SendSseStepAsync(-1, $"TokenHub AI 调用失败: {ex.Message}");
  2626. }
  2627. catch (Exception ex)
  2628. {
  2629. _logger.LogError(ex, "SSE 管道熔断");
  2630. await HttpContext.SendSseStepAsync(-1, $"SSE 错误:{ex.InnerException.Message}({ex.Message})");
  2631. }
  2632. finally
  2633. {
  2634. await HttpContext.FinalizeSseAsync();
  2635. }
  2636. }
  2637. /// <summary>
  2638. /// 清洗并解析 AI 返回的 JSON 块
  2639. /// </summary>
  2640. private static T? CleanAndParseJson<T>(string rawResponse)
  2641. {
  2642. if (string.IsNullOrWhiteSpace(rawResponse)) return default;
  2643. string cleanJson = rawResponse.Trim();
  2644. // 自动剥离 Markdown 语法糖
  2645. if (cleanJson.Contains("```json"))
  2646. cleanJson = cleanJson.Split("```json")[1].Split("```")[0];
  2647. else if (cleanJson.Contains("```"))
  2648. cleanJson = cleanJson.Split("```")[1].Split("```")[0];
  2649. return JsonConvert.DeserializeObject<T>(cleanJson.Trim());
  2650. }
  2651. /// <summary>
  2652. /// 将规模选项(如“中型 (51-200人)”“巨型 (10001人以上)”“Self”,与 OrgScale 档位一致)解析为 Snovio Size。
  2653. /// MVP 策略:多规模/解析失败返回 null,即不传规模条件,避免 Snovio 过滤过严导致零召回。
  2654. /// </summary>
  2655. /// <param name="scaleTypes">词条中的规模集合</param>
  2656. /// <returns>单一 Snovio Size 字符串;无法唯一确定时返回 null</returns>
  2657. private static string? ParseSnovioSize(List<string>? scaleTypes)
  2658. {
  2659. if (scaleTypes == null || !scaleTypes.Any()) return null;
  2660. // Self 为 Snovio 自雇档,无数字,正则匹配不到,单独透传。
  2661. var trimmed = scaleTypes
  2662. .Where(x => !string.IsNullOrWhiteSpace(x))
  2663. .Select(x => x!.Trim())
  2664. .Distinct(StringComparer.OrdinalIgnoreCase)
  2665. .ToList();
  2666. if (trimmed.Count == 1 && trimmed[0].Equals("Self", StringComparison.OrdinalIgnoreCase))
  2667. return "Self";
  2668. var sizes = trimmed
  2669. .Select(x =>
  2670. {
  2671. // “10001人以上”类转 Snovio 的“10001+”写法。
  2672. var above = Regex.Match(x, @"(\d+)\s*人以上");
  2673. if (above.Success) return above.Groups[1].Value + "+";
  2674. return Regex.Match(x, @"(\d+\s*-\s*\d+\+?|\d+\+)") is { Success: true } m
  2675. ? m.Groups[1].Value.Replace(" ", string.Empty)
  2676. : string.Empty;
  2677. })
  2678. .Where(x => !string.IsNullOrEmpty(x))
  2679. .Distinct()
  2680. .ToList();
  2681. return sizes.Count == 1 ? sizes[0] : null;
  2682. }
  2683. /// <summary>
  2684. /// 判断公司 location 是否匹配目标国家。
  2685. /// Snovio 的 country 过滤为宽松匹配,location 可能为空或为别国;
  2686. /// 此方法用 location 字符串包含关系二次校验,命中则采信。
  2687. /// </summary>
  2688. /// <param name="location">Snovio 返回的公司 location(如 "New York, New York, United States")。</param>
  2689. /// <param name="country">目标国家(如 "United States")。</param>
  2690. private static bool IsLocationMatchesCountry(string? location, string? country)
  2691. {
  2692. if (string.IsNullOrWhiteSpace(location) || string.IsNullOrWhiteSpace(country))
  2693. return false;
  2694. var locationParts = location.Split(',').Select(x => x.Trim()).Where(x => x.Length > 0).ToList();
  2695. return locationParts.Any(x => string.Equals(x, country.Trim(), StringComparison.OrdinalIgnoreCase));
  2696. }
  2697. /// <summary>
  2698. /// 用 Kimi 将中文国家名翻译成英文国家名,用于 location 二次校验。
  2699. /// 翻译失败或返回空时回退原文本(若原文本已是英文则直接可用)。
  2700. /// </summary>
  2701. /// <param name="country">中文(或英文)国家名,如 "美国"。</param>
  2702. /// <param name="invName">公务名称,用于日志定位。</param>
  2703. private async Task<string> TranslateCountryToEnglishAsync(string country, string invName)
  2704. {
  2705. if (string.IsNullOrWhiteSpace(country))
  2706. return country;
  2707. // 已是纯英文则无需翻译
  2708. if (country.All(ch => char.IsLetter(ch) && ch < 128) && !ContainsCjk(country))
  2709. return country.Trim();
  2710. try
  2711. {
  2712. var messages = new List<SeedMessages>
  2713. {
  2714. new SeedMessages { Role = KimiRole.system, Content = "你是一个专业翻译。请将输入的中文国家/地区名称翻译成对应的英文国家/地区名称。只输出翻译结果本身,不要输出任何解释、引号或多余字符。" },
  2715. new SeedMessages { Role = KimiRole.user, Content = country.Trim() }
  2716. };
  2717. var result = await new KiMiApiClient().SeedMessageByFullConterObject(messages);
  2718. var translated = result?.Choices?.FirstOrDefault()?.Message?.Content?.Trim();
  2719. if (string.IsNullOrWhiteSpace(translated))
  2720. return country.Trim();
  2721. return translated;
  2722. }
  2723. catch (Exception ex)
  2724. {
  2725. _logger.LogWarning(ex, "公务名称:{InvName};Kimi 国家翻译失败,回退原文;Country={Country}", invName, country);
  2726. return country.Trim();
  2727. }
  2728. }
  2729. private static bool ContainsCjk(string text)
  2730. {
  2731. foreach (var ch in text)
  2732. {
  2733. if (ch >= 0x4E00 && ch <= 0x9FFF) return true;
  2734. }
  2735. return false;
  2736. }
  2737. /// <summary>
  2738. /// Snovio 联系人/邮箱补全:公司域名 → 域名潜客 → 潜客邮箱。
  2739. /// 每批最多并发 3 个潜客邮箱检索;先完成且有邮箱的立即采用并取消其余,
  2740. /// 无邮箱则继续等待同批下一个;整批失败再取下一批。全无则返回 false。
  2741. /// 联系人取“名 姓(职位)”,邮箱取首个有效;电话 Snovio 未提供,保持空。
  2742. /// 单公司失败只记日志、不抛异常,避免熔断整管 SSE。
  2743. /// </summary>
  2744. /// <param name="domain">公司域名(如 snov.io,不带协议)</param>
  2745. /// <param name="info">待补全的单位信息(原地写入 Contact/Email/Phone)</param>
  2746. /// <returns>是否成功获取到至少一个有效邮箱;false 表示该公司无可用邮箱。</returns>
  2747. private async Task<bool> EnrichSnovioContactAsync(string? domain, InvitationAIInfo info)
  2748. {
  2749. if (string.IsNullOrWhiteSpace(domain)) return false;
  2750. var sw = Stopwatch.StartNew();
  2751. var hasEmail = false;
  2752. try
  2753. {
  2754. // 域名潜客:MVP 取第 1 页,二期可按精英职位(Positions)过滤。
  2755. // 单次 45s 超时,避免 SSE 长时间挂起。
  2756. var prospects = await _snovioService.SearchDomainProspectsAsync(
  2757. new SnovioDomainProspectsSearchRequest { Domain = domain.Trim(), Page = 1 },
  2758. TimeSpan.FromSeconds(45));
  2759. var prospectList = (prospects?.Data ?? new List<SnovioDomainProspect>())
  2760. .Where(p => !string.IsNullOrWhiteSpace(ExtractSnovioProspectHash(p.SearchEmailsStart)))
  2761. .ToList();
  2762. if (!prospectList.Any()) return false;
  2763. const int MaxConcurrentProspectEmails = 3;
  2764. for (var offset = 0; offset < prospectList.Count; offset += MaxConcurrentProspectEmails)
  2765. {
  2766. var batch = prospectList.Skip(offset).Take(MaxConcurrentProspectEmails).ToList();
  2767. using var foundCts = new CancellationTokenSource();
  2768. var inFlight = batch
  2769. .Select(p => SearchProspectEmailSafeAsync(p, domain, info, foundCts.Token))
  2770. .ToList();
  2771. while (inFlight.Count > 0)
  2772. {
  2773. var finished = await Task.WhenAny(inFlight);
  2774. inFlight.Remove(finished);
  2775. var (prospect, email) = await finished;
  2776. if (email == null) continue;
  2777. foundCts.Cancel();
  2778. if (inFlight.Count > 0)
  2779. {
  2780. try { await Task.WhenAll(inFlight); }
  2781. catch (Exception) { }
  2782. }
  2783. info.Email = email.Email;
  2784. var name = $"{prospect.FirstName} {prospect.LastName}".Trim();
  2785. info.Contact = string.IsNullOrWhiteSpace(prospect.Position)
  2786. ? (string.IsNullOrWhiteSpace(name) ? (prospect.SourcePage ?? string.Empty) : name)
  2787. : (string.IsNullOrWhiteSpace(name) ? prospect.Position : $"{name}({prospect.Position})");
  2788. hasEmail = true;
  2789. return true;
  2790. }
  2791. }
  2792. return false;
  2793. }
  2794. catch (Exception ex) when (ex is TimeoutException)
  2795. {
  2796. _logger.LogWarning(ex, "公务名称:{InvName};Snovio 域名潜客检索超时,跳过该公司;Domain={Domain};Company={Name}",
  2797. info.NameCn, domain, info.NameCn);
  2798. return false;
  2799. }
  2800. catch (Exception ex) when (ex is TaskCanceledException or OperationCanceledException)
  2801. {
  2802. _logger.LogWarning(ex, "公务名称:{InvName};Snovio 域名潜客检索超时(HTTP),跳过该公司;Domain={Domain};Company={Name}",
  2803. info.NameCn, domain, info.NameCn);
  2804. return false;
  2805. }
  2806. catch (Exception ex)
  2807. {
  2808. _logger.LogWarning(ex, "Snovio 联系人补全失败;Domain={Domain}", domain);
  2809. return false;
  2810. }
  2811. finally
  2812. {
  2813. sw.Stop();
  2814. _logger.LogInformation(
  2815. "Snovio 潜客完整流程耗时 {ElapsedMs}ms;HasEmail={HasEmail};Domain={Domain};Company={Name}",
  2816. sw.ElapsedMilliseconds, hasEmail, domain, info.NameCn);
  2817. }
  2818. }
  2819. private async Task<(SnovioDomainProspect Prospect, SnovioProspectEmail? Email)> SearchProspectEmailSafeAsync(
  2820. SnovioDomainProspect prospect,
  2821. string domain,
  2822. InvitationAIInfo info,
  2823. CancellationToken cancellationToken)
  2824. {
  2825. var prospectHash = ExtractSnovioProspectHash(prospect.SearchEmailsStart);
  2826. var prospectName = $"{prospect.FirstName} {prospect.LastName}".Trim();
  2827. try
  2828. {
  2829. var emailResult = await _snovioService.SearchProspectEmailAsync(
  2830. prospectHash!,
  2831. null,
  2832. TimeSpan.FromSeconds(45),
  2833. cancellationToken: cancellationToken);
  2834. var email = emailResult?.Data?.Emails?.FirstOrDefault(x => !string.IsNullOrWhiteSpace(x.Email));
  2835. return (prospect, email);
  2836. }
  2837. catch (Exception ex) when (ex is OperationCanceledException && cancellationToken.IsCancellationRequested)
  2838. {
  2839. return (prospect, null);
  2840. }
  2841. catch (Exception ex) when (ex is TimeoutException)
  2842. {
  2843. _logger.LogWarning(ex, "公务名称:{InvName};Snovio 潜客邮箱检索超时,跳过该潜客;Domain={Domain};Company={Name};Prospect={Prospect}",
  2844. info.NameCn, domain, info.NameCn, prospectName);
  2845. return (prospect, null);
  2846. }
  2847. catch (Exception ex) when (ex is TaskCanceledException or OperationCanceledException)
  2848. {
  2849. _logger.LogWarning(ex, "公务名称:{InvName};Snovio 潜客邮箱检索超时(HTTP),跳过该潜客;Domain={Domain};Company={Name};Prospect={Prospect}",
  2850. info.NameCn, domain, info.NameCn, prospectName);
  2851. return (prospect, null);
  2852. }
  2853. catch (Exception ex)
  2854. {
  2855. _logger.LogWarning(ex, "Snovio 潜客邮箱检索失败;Domain={Domain};Prospect={Prospect}", domain, prospectName);
  2856. return (prospect, null);
  2857. }
  2858. }
  2859. /// <summary>
  2860. /// 从 search_emails_start 地址末段提取 prospect_hash。
  2861. /// </summary>
  2862. private static string? ExtractSnovioProspectHash(string? searchEmailsStart)
  2863. {
  2864. if (string.IsNullOrWhiteSpace(searchEmailsStart)) return null;
  2865. var trimmed = searchEmailsStart.Trim().TrimEnd('/');
  2866. var idx = trimmed.LastIndexOf('/');
  2867. return idx < 0 ? null : trimmed[(idx + 1)..];
  2868. }
  2869. /// <summary>
  2870. /// 商邀资料AI TokenHub AI续写(SSE流式推送)
  2871. /// </summary>
  2872. /// <param name="dto"></param>
  2873. /// <returns></returns>
  2874. [HttpPost]
  2875. public async Task InvitationAICompleteTextStream([FromBody] InvitationAICompleteTextDto dto)
  2876. {
  2877. HttpContext.InitializeSse();
  2878. try
  2879. {
  2880. // 初始化检查 (Progress: 5%)
  2881. var invAiInfo = await _sqlSugar.Queryable<Res_InvitationAI>().Where(x => x.Id == dto.Id).FirstAsync();
  2882. if (invAiInfo?.EntryInfo == null)
  2883. {
  2884. await HttpContext.SendSseStepAsync(-1, "请先设置关键字信息!");
  2885. return;
  2886. }
  2887. await HttpContext.SendSseStepAsync(10, "任务初始化完成,正在计算补齐缺口...");
  2888. // 任务拆解逻辑
  2889. var entryInfo = invAiInfo.EntryInfo;
  2890. var aiTasks = new List<CountryAIPormptInfo>();
  2891. int targetPerCountry = entryInfo.NeedCount;
  2892. foreach (var countryName in entryInfo.TargetCountry)
  2893. {
  2894. var countryDataCount = invAiInfo.AiCrawledDetails.Count(x => x.Region == countryName);
  2895. int aiNeedCount = targetPerCountry - countryDataCount;
  2896. if (aiNeedCount > 0) aiTasks.Add(new() { Country = countryName, Count = aiNeedCount });
  2897. }
  2898. if (!aiTasks.Any())
  2899. {
  2900. await HttpContext.SendSseStepAsync(100, "数据已满额,无需续写", invAiInfo.AiCrawledDetails);
  2901. return;
  2902. }
  2903. // Snovio 续写:去重集合沿用混元版语义(已有第三方数据的名称,避免重复补入)。
  2904. var existingNames = new HashSet<string>(
  2905. invAiInfo.AiCrawledDetails.Where(x => x.Source == 1 && !string.IsNullOrWhiteSpace(x.NameCn)).Select(x => x.NameCn),
  2906. StringComparer.OrdinalIgnoreCase);
  2907. await HttpContext.SendSseStepAsync(60, $"Snovio 正在跨境检索 {string.Join(", ", aiTasks.Select(x => $"{x.Country}({x.Count}条)"))} 单位资料...");
  2908. // 行业映射:Snovio 严格校验 industries 取值,中文原词直透必 422,
  2909. // 经 SnovioIndustryMapper 转英文(未映射的丢弃,全空则不传该条件保召回)。
  2910. var snovioIndustries = SnovioIndustryMapper.MapToSnovio(entryInfo.Industries);
  2911. // 规模仅单值透传:多规模/解析失败返回 null,不传规模保召回。
  2912. var snovioSize = ParseSnovioSize(entryInfo.ScaleTypes);
  2913. var operatorName = await _sqlSugar.Queryable<Sys_Users>()
  2914. .Where(x => x.Id == dto.CurrUserId)
  2915. .Select(x => x.CnName)
  2916. .FirstAsync() ?? "-";
  2917. // ====== 调用 Snovio ======
  2918. // 逐国检索:边取边做 location 校验与邮箱补全,只统计真正可用的公司。
  2919. // 逐条进度:60 起每条 +1(封顶 79,80 留给汇总),消息显示“正在检索{国家}第{index}名潜客”。
  2920. var hunyuanAIInvDatas = new List<InvitationAIInfo>();
  2921. var snovioProgress = 60;
  2922. foreach (var task in aiTasks)
  2923. {
  2924. try
  2925. {
  2926. // 用 Kimi 将中文国家名翻译成英文,用于 location 二次校验(每国翻译一次)。
  2927. var countryEn = await TranslateCountryToEnglishAsync(task.Country, invAiInfo.InvName);
  2928. // 翻页补足:有效公司不足 gap 时继续翻页,MaxSnovioPages 封顶控成本。
  2929. const int MaxSnovioPages = 5;
  2930. var collected = 0;
  2931. for (var page = 1; page <= MaxSnovioPages && collected < task.Count; page++)
  2932. {
  2933. // Snovio 请求:国家按 country 精确限定。
  2934. var request = new SnovioCompanySearchRequest
  2935. {
  2936. Page = page,
  2937. Filters = new SnovioCompanySearchFilters
  2938. {
  2939. Company = new SnovioCompanyFilter
  2940. {
  2941. Industries = snovioIndustries.Any()
  2942. ? new SnovioStringListFilter { Include = snovioIndustries }
  2943. : null,
  2944. // Snovio size 为数组,单值包单元素数组;null 表示不传该条件。
  2945. Size = string.IsNullOrWhiteSpace(snovioSize) ? null : new List<string> { snovioSize }
  2946. },
  2947. Locations = new SnovioLocationFilter
  2948. {
  2949. Include = new SnovioLocationFilterItem
  2950. {
  2951. Locality = task.Country,
  2952. LocationType = "country"
  2953. }
  2954. }
  2955. },
  2956. };
  2957. // SearchCompaniesAsync = Start + 轮询等待(默认 2min/3s),同步返回最终结果。
  2958. var result = await _snovioService.SearchCompaniesAsync(request);
  2959. var raw = result?.Data?.Companies ?? new List<SnovioCompanySearchCompany>();
  2960. // Snovio 无更多数据则停,避免空转烧点。
  2961. if (!raw.Any()) break;
  2962. // 字段映射:Snovio 公司接口仅有稀疏字段,联系人/邮箱需按
  2963. // 公司域名 → 域名潜客 → 潜客邮箱三跳补全;电话 Snovio 未提供,保持空。
  2964. // Region 取请求国,保证后续按国计数(NeedCount)稳定;Source=1 保持第三方标识,
  2965. // 复用 InvitationAIInsertResource(Source != 0 可入库)逻辑。
  2966. foreach (var c in raw)
  2967. {
  2968. if (collected >= task.Count) break;
  2969. // 客户端去重:跳过与已有第三方数据同名的公司(含本轮已收)。
  2970. if (string.IsNullOrWhiteSpace(c.Name) || existingNames.Contains(c.Name)) continue;
  2971. // 按 location 二次校验国家:Snovio 的 country 过滤为宽松匹配,可能混入别国数据,
  2972. // 仅采信 location 明确包含目标国家的公司,避免“标记为请求国实为别国”的公司入库。
  2973. if (!IsLocationMatchesCountry(c.Location, countryEn))
  2974. {
  2975. _logger.LogDebug("公务名称:{InvName};公司 location 与目标国不符,跳过;Name={Name};Location={Location};Country={Country}",
  2976. invAiInfo.InvName, c.Name, c.Location, task.Country);
  2977. continue;
  2978. }
  2979. var info = new InvitationAIInfo
  2980. {
  2981. Guid = Guid.NewGuid().ToString("N"),
  2982. Source = 1,
  2983. Region = task.Country,
  2984. Industry = entryInfo.Industries?.FirstOrDefault() ?? c.Industry,
  2985. NameCn = c.Name,
  2986. NameEn = c.Name,
  2987. Address = c.Location,
  2988. Scope = c.Industry,
  2989. SiteUrl = string.IsNullOrWhiteSpace(c.Domain) ? string.Empty : $"https://{c.Domain.Trim()}",
  2990. PostUrl = new List<PostNewsItem>(),
  2991. RecLevel = "Backup",
  2992. Operator = operatorName,
  2993. OperatedAt = DateTime.Now,
  2994. };
  2995. // 联系人/邮箱补全:逐个潜客取邮箱,该公司所有潜客均无邮箱则跳过该家。
  2996. var hasEmail = await EnrichSnovioContactAsync(c.Domain, info);
  2997. if (!hasEmail)
  2998. {
  2999. _logger.LogWarning("公务名称:{InvName};公司无可用邮箱,跳过;Name={Name}", invAiInfo.InvName, c.Name);
  3000. continue;
  3001. }
  3002. hunyuanAIInvDatas.Add(info);
  3003. existingNames.Add(c.Name);
  3004. collected++;
  3005. // 仅对成功收集的公司计数并推送:进度展示“已成功收集的有效数”。
  3006. snovioProgress = Math.Min(snovioProgress + 1, 79);
  3007. await HttpContext.SendSseStepAsync(snovioProgress, $"正在检索{task.Country}第{collected}名潜客...");
  3008. }
  3009. // 已到 Snovio 最后一页则停。
  3010. if (page >= (result?.Data?.TotalPages ?? page)) break;
  3011. }
  3012. }
  3013. catch (Exception ex)
  3014. {
  3015. // 单国失败继续下一国;全失败时下游按空结果提示。
  3016. _logger.LogWarning(ex, "公务名称:{InvName};Snovio 续写失败;Country={Country}", invAiInfo.InvName, task.Country);
  3017. }
  3018. }
  3019. await HttpContext.SendSseStepAsync(80, $"Snovio 已完成跨境检索,共获取 {hunyuanAIInvDatas.Count} 条单位资料");
  3020. if (!hunyuanAIInvDatas.Any())
  3021. {
  3022. await HttpContext.SendSseStepAsync(-1,"本次未检索到符合条件的境外单位,建议调整国家或行业范围。");
  3023. return;
  3024. }
  3025. // ====== 数据库写入 ======
  3026. invAiInfo.AiCrawledDetails ??= new();
  3027. invAiInfo.AiCrawledDetails.AddRange(hunyuanAIInvDatas);
  3028. invAiInfo.AiCrawledDetails =
  3029. invAiInfo.AiCrawledDetails.OrderByDescending(x => x.OperatedAt).ToList();
  3030. var updated = await _sqlSugar.Updateable(invAiInfo)
  3031. .UpdateColumns(x => x.AiCrawledDetails)
  3032. .ExecuteCommandAsync();
  3033. if (updated <= 0)
  3034. {
  3035. await HttpContext.SendSseStepAsync(-1, "数据库更新失败,请联系管理员");
  3036. return;
  3037. }
  3038. await HttpContext.SendSseStepAsync(100,
  3039. $"AI 续写成功!新增 {hunyuanAIInvDatas.Count} 条数据",
  3040. invAiInfo.AiCrawledDetails);
  3041. }
  3042. catch (TokenHubException ex)
  3043. {
  3044. await HttpContext.SendSseStepAsync(-1, $"TokenHub AI 调用失败: {ex.Message}");
  3045. }
  3046. catch (Exception ex)
  3047. {
  3048. _logger.LogError(ex, "SSE 续写异常");
  3049. await HttpContext.SendSseStepAsync(-1, $"炼金炸炉:{ex.Message}");
  3050. }
  3051. finally
  3052. {
  3053. await HttpContext.FinalizeSseAsync();
  3054. }
  3055. }
  3056. /// <summary>
  3057. /// 商邀资料AI 文件生成(基于TokenHub AI已爬取数据进行格式化输出,供用户下载使用)
  3058. /// </summary>
  3059. /// <returns></returns>
  3060. [HttpGet("{id}")]
  3061. [ProducesResponseType(typeof(JsonView), StatusCodes.Status200OK)]
  3062. public async Task<IActionResult> InvitationAIFileGenerator(int id)
  3063. {
  3064. var invAiInfo = await _sqlSugar.Queryable<Res_InvitationAI>()
  3065. .Where(x => x.Id == id)
  3066. .FirstAsync();
  3067. var dataList = invAiInfo?.AiCrawledDetails;
  3068. if (dataList == null)
  3069. return Ok(JsonView(false, "数据为空"));
  3070. Document doc = new Document();
  3071. DocumentBuilder builder = new DocumentBuilder(doc);
  3072. // A4页面设置
  3073. double usableWidth = 495;
  3074. foreach (Aspose.Words.Section sec in doc.Sections)
  3075. {
  3076. sec.PageSetup.PaperSize = Aspose.Words.PaperSize.A4;
  3077. sec.PageSetup.LeftMargin = 50;
  3078. sec.PageSetup.RightMargin = 50;
  3079. sec.PageSetup.TopMargin = 50;
  3080. sec.PageSetup.BottomMargin = 50;
  3081. }
  3082. // 卡片背景
  3083. Color cardBg = Color.FromArgb(252, 252, 252);
  3084. string fontName = "微软雅黑";
  3085. int count = 1;
  3086. foreach (var item in dataList)
  3087. {
  3088. // 标题
  3089. builder.ParagraphFormat.ClearFormatting();
  3090. builder.Font.ClearFormatting();
  3091. builder.Font.Name = fontName;
  3092. builder.Font.Size = 16;
  3093. builder.Font.Bold = true;
  3094. builder.Font.Color = Color.Black;
  3095. builder.Write($"{count++}. {item.NameCn}");
  3096. builder.InsertParagraph();
  3097. // 英文名
  3098. builder.Font.Size = 10;
  3099. builder.Font.Bold = false;
  3100. builder.Font.Color = Color.FromArgb(120, 120, 120);
  3101. builder.Write(item.NameEn ?? "");
  3102. builder.InsertParagraph();
  3103. builder.InsertParagraph();
  3104. // ====== 卡片表格 ======
  3105. Aspose.Words.Tables.Table table = builder.StartTable();
  3106. builder.CellFormat.Shading.BackgroundPatternColor = cardBg;
  3107. builder.CellFormat.VerticalAlignment = CellVerticalAlignment.Center;
  3108. builder.CellFormat.WrapText = true;
  3109. builder.CellFormat.LeftPadding = 12;
  3110. builder.CellFormat.RightPadding = 12;
  3111. builder.CellFormat.TopPadding = 8;
  3112. builder.CellFormat.BottomPadding = 8;
  3113. double labelW = 120;
  3114. AddSingleRow(builder, fontName, labelW, usableWidth - labelW, "详细地址", item.Address);
  3115. AddSingleRow(builder, fontName, labelW, usableWidth - labelW, "联系人", item.Contact);
  3116. AddSingleRow(builder, fontName, labelW, usableWidth - labelW, "联系电话", item.Phone);
  3117. AddSingleRow(builder, fontName, labelW, usableWidth - labelW, "电子邮箱", item.Email);
  3118. AddSingleLinkRow(builder, fontName, labelW, usableWidth - labelW, "官方网站", item.SiteUrl);
  3119. AddSingleLinkRow(builder, fontName, labelW, usableWidth - labelW, "最近三年动态", string.Join(", ", item.PostUrl.Select(p => p.Url)));
  3120. AddFullWidthRow(builder, fontName, labelW, usableWidth - labelW, "推荐等级", item.RecLevel);
  3121. AddFullWidthRow(builder, fontName, labelW, usableWidth - labelW, "经营范围", item.Scope);
  3122. AddFullWidthRow(builder, fontName, labelW, usableWidth - labelW, "对接建议", item.IntgAdvice);
  3123. // 表格样式
  3124. table.SetBorders(Aspose.Words.LineStyle.Single, 0.5, Color.FromArgb(230, 230, 230));
  3125. table.AutoFit(AutoFitBehavior.FixedColumnWidths);
  3126. table.AllowAutoFit = false;
  3127. table.PreferredWidth = PreferredWidth.FromPoints(usableWidth);
  3128. builder.EndTable();
  3129. builder.InsertParagraph();
  3130. builder.InsertParagraph();
  3131. }
  3132. // 保存逻辑
  3133. string fileName = $"{invAiInfo.InvName}_Professional.docx";
  3134. string filePath = Path.Combine(AppSettingsHelper.Get("InvitationAIAssistBasePath"), fileName);
  3135. doc.Save(filePath, Aspose.Words.SaveFormat.Docx);
  3136. return Ok(JsonView(true, "生成成功", new { Url = $"{AppSettingsHelper.Get("WordBaseUrl")}/{AppSettingsHelper.Get("InvitationAIAssistFtpPath")}/{fileName}" }));
  3137. }
  3138. #region 15.12 适配助手方法
  3139. private void AddSingleRow(DocumentBuilder builder, string font, double labelW, double contentW, string label, string value)
  3140. {
  3141. // 标签
  3142. builder.InsertCell();
  3143. builder.CellFormat.Width = labelW;
  3144. builder.ParagraphFormat.Alignment = ParagraphAlignment.Right;
  3145. builder.Font.Name = font;
  3146. builder.Font.Size = 9;
  3147. builder.Font.Bold = false;
  3148. builder.Font.Color = Color.FromArgb(120, 120, 120);
  3149. builder.Write(label);
  3150. // 内容
  3151. builder.InsertCell();
  3152. builder.CellFormat.Width = contentW;
  3153. builder.CellFormat.HorizontalMerge = CellMerge.First;
  3154. builder.ParagraphFormat.Alignment = ParagraphAlignment.Left;
  3155. builder.Font.Size = 10;
  3156. builder.Font.Bold = false;
  3157. builder.Font.Color = Color.FromArgb(60, 60, 60);
  3158. builder.Write(string.IsNullOrEmpty(value) ? "—" : value);
  3159. // 合并剩余列
  3160. builder.InsertCell();
  3161. builder.CellFormat.HorizontalMerge = CellMerge.Previous;
  3162. builder.InsertCell();
  3163. builder.CellFormat.HorizontalMerge = CellMerge.Previous;
  3164. builder.EndRow();
  3165. builder.CellFormat.HorizontalMerge = CellMerge.None;
  3166. }
  3167. private void AddSingleLinkRow(DocumentBuilder builder, string font, double labelW, double contentW, string label, string url)
  3168. {
  3169. builder.InsertCell();
  3170. builder.CellFormat.Width = labelW;
  3171. builder.ParagraphFormat.Alignment = ParagraphAlignment.Right;
  3172. builder.Font.Size = 9;
  3173. builder.Font.Bold = false;
  3174. builder.Font.Color = Color.FromArgb(120, 120, 120);
  3175. builder.Write(label);
  3176. builder.InsertCell();
  3177. builder.CellFormat.Width = contentW;
  3178. builder.CellFormat.HorizontalMerge = CellMerge.First;
  3179. builder.ParagraphFormat.Alignment = ParagraphAlignment.Left;
  3180. if (!string.IsNullOrEmpty(url) && url.StartsWith("http"))
  3181. {
  3182. builder.Font.Size = 10;
  3183. builder.Font.Color = Color.FromArgb(0, 102, 204);
  3184. builder.Font.Underline = Underline.None;
  3185. builder.InsertHyperlink(url, url, false);
  3186. }
  3187. else
  3188. {
  3189. builder.Font.Color = Color.FromArgb(60, 60, 60);
  3190. builder.Write("—");
  3191. }
  3192. builder.InsertCell();
  3193. builder.CellFormat.HorizontalMerge = CellMerge.Previous;
  3194. builder.InsertCell();
  3195. builder.CellFormat.HorizontalMerge = CellMerge.Previous;
  3196. builder.EndRow();
  3197. builder.CellFormat.HorizontalMerge = CellMerge.None;
  3198. }
  3199. private void AddFullWidthRow(DocumentBuilder builder, string font, double lw, double cw, string label, string value)
  3200. {
  3201. // 标签
  3202. builder.InsertCell();
  3203. builder.CellFormat.Width = lw;
  3204. builder.ParagraphFormat.Alignment = ParagraphAlignment.Right;
  3205. builder.Font.Size = 9;
  3206. builder.Font.Bold = false;
  3207. builder.Font.Color = Color.FromArgb(120, 120, 120);
  3208. builder.Write(label);
  3209. // 内容
  3210. builder.InsertCell();
  3211. builder.CellFormat.Width = cw;
  3212. builder.CellFormat.HorizontalMerge = CellMerge.First;
  3213. builder.ParagraphFormat.Alignment = ParagraphAlignment.Left;
  3214. builder.Font.Size = 10;
  3215. builder.Font.Bold = false;
  3216. builder.Font.Color = Color.FromArgb(60, 60, 60);
  3217. builder.Write(string.IsNullOrEmpty(value) ? "—" : value);
  3218. builder.InsertCell();
  3219. builder.CellFormat.HorizontalMerge = CellMerge.Previous;
  3220. builder.InsertCell();
  3221. builder.CellFormat.HorizontalMerge = CellMerge.Previous;
  3222. builder.EndRow();
  3223. builder.CellFormat.HorizontalMerge = CellMerge.None;
  3224. }
  3225. #endregion
  3226. /// <summary>
  3227. /// 商邀资料AI 生成邮件(SSE流式推送)
  3228. /// </summary>
  3229. /// <returns></returns>
  3230. [HttpPost]
  3231. public async Task InvitationAIGenerateEmailStream([FromBody] InvitationAIGenerateEmailDto dto)
  3232. {
  3233. HttpContext.InitializeSse();
  3234. try
  3235. {
  3236. // 1. 基础校验与资源加载 (Progress: 5%)
  3237. if (dto.Id < 1 || dto.Guids == null || !dto.Guids.Any())
  3238. {
  3239. await HttpContext.SendSseStepAsync(-1, "请求参数不完整,请选择单位");
  3240. return;
  3241. }
  3242. var invAiInfo = await _sqlSugar.Queryable<Res_InvitationAI>().Where(x => x.Id == dto.Id).FirstAsync();
  3243. if (invAiInfo?.AiCrawledDetails == null)
  3244. {
  3245. await HttpContext.SendSseStepAsync(-1, "基础商邀资料不存在");
  3246. return;
  3247. }
  3248. await HttpContext.SendSseStepAsync(10, "AI 正在分析考察团组背景与访问意图...");
  3249. // 2. 准备 AI 上下文数据 (Progress: 15%)
  3250. var clientInfoSources = invAiInfo.AiCrawledDetails.Where(x => dto.Guids.Contains(x.Guid)).ToList();
  3251. var clientInfosForAI = clientInfoSources.Select(x => new AICreateEmailInfo()
  3252. {
  3253. Guid = x.Guid,
  3254. NameCn = x.NameCn,
  3255. Scope = x.Scope
  3256. }).ToList();
  3257. var groupInfo = await _sqlSugar.Queryable<Grp_DelegationInfo>()
  3258. .Where(x => x.IsDel == 0 && x.Id == invAiInfo.GroupId)
  3259. .Select(x => new { x.TeamName, x.VisitPurpose, x.VisitDate })
  3260. .FirstAsync();
  3261. string operatorName = await _sqlSugar.Queryable<Sys_Users>()
  3262. .Where(x => x.Id == dto.CurrUserId).Select(x => x.CnName).FirstAsync() ?? "-";
  3263. // 3. 构建 Prompt 并调用 AI (Progress: 25%)
  3264. await HttpContext.SendSseStepAsync(30, $"AI 正在为 {clientInfosForAI.Count} 家单位撰写定制化正式邮件...");
  3265. // 构建生成邮件的 Prompt
  3266. string pormpt = BuildHunyuanEmailPrompt(
  3267. invAiInfo.EntryInfo?.OriginUnit ?? "",
  3268. groupInfo?.VisitPurpose ?? "",
  3269. groupInfo?.VisitDate.ToString("yyyy-MM-dd") ?? "",
  3270. clientInfosForAI
  3271. );
  3272. // 调用 AI (此处为阻塞式等待 AI 结果,若混元支持 Stream 可进一步拆解)
  3273. string aiResponse = string.Empty;
  3274. var reply = await _tokenHubService.ChatAsync(new ChatRequest()
  3275. {
  3276. Messages = new List<OASystem.API.OAMethodLib.TokenHubAI.Models.ChatMessage>()
  3277. {
  3278. new() { Content = pormpt }
  3279. }
  3280. });
  3281. if (reply.IsSuccess) aiResponse = reply?.Content ?? string.Empty;
  3282. else
  3283. {
  3284. await HttpContext.SendSseStepAsync(-1, $"TokenHub AI 生成邮件失败: {reply?.ErrorMessage}");
  3285. }
  3286. await HttpContext.SendSseStepAsync(80, "邮件初稿已生成,正在进行 HTML 格式校验与转义处理...");
  3287. // 4. 解析结果 (Progress: 85%)
  3288. var hunyuanAIEmailDatas = new List<AICreateEmailInfo>();
  3289. if (!string.IsNullOrWhiteSpace(aiResponse))
  3290. {
  3291. // 预处理:过滤 AI 可能返回的 Markdown 标记
  3292. string cleanJson = aiResponse.Trim();
  3293. if (cleanJson.StartsWith("```json"))
  3294. cleanJson = cleanJson.Substring(7, cleanJson.Length - 10).Trim();
  3295. else if (cleanJson.StartsWith("```"))
  3296. cleanJson = cleanJson.Substring(3, cleanJson.Length - 6).Trim();
  3297. try
  3298. {
  3299. // 解析并注入 Source 标识
  3300. hunyuanAIEmailDatas = JsonConvert.DeserializeObject<List<AICreateEmailInfo>>(cleanJson);
  3301. }
  3302. catch (JsonException ex)
  3303. {
  3304. // 记录日志并考虑 fallback 策略
  3305. _logger.LogError(ex, "Hunyuan AI 响应解析失败。原始数据:{Response}", aiResponse);
  3306. }
  3307. }
  3308. if (hunyuanAIEmailDatas == null || !hunyuanAIEmailDatas.Any())
  3309. {
  3310. await HttpContext.SendSseStepAsync(-1, "AI 格式解析失败,请尝试重新生成");
  3311. return;
  3312. }
  3313. // 5. 更新数据模型 (Progress: 90%)
  3314. foreach (var client in clientInfoSources)
  3315. {
  3316. var aiEmail = hunyuanAIEmailDatas.FirstOrDefault(x => x.Guid == client.Guid);
  3317. if (aiEmail != null)
  3318. {
  3319. client.EmailInfo.Status = 2; // 已生成
  3320. client.EmailInfo.EmailTitle = aiEmail.Subject;
  3321. client.EmailInfo.EmailContent = aiEmail.Content;
  3322. client.EmailInfo.Operator = operatorName;
  3323. client.EmailInfo.OperatedAt = DateTime.Now;
  3324. }
  3325. }
  3326. // 6. 数据库持久化 (Progress: 95%)
  3327. // 采用增量更新策略,避免直接 Where 过滤掉其他未选中的数据
  3328. var update = await _sqlSugar.Updateable(invAiInfo).UpdateColumns(x => x.AiCrawledDetails).ExecuteCommandAsync();
  3329. if (update > 0)
  3330. {
  3331. await HttpContext.SendSseStepAsync(100, "邮件全部生成完毕并已存入团组资料库", invAiInfo.AiCrawledDetails);
  3332. }
  3333. else
  3334. {
  3335. await HttpContext.SendSseStepAsync(-1, "数据库写入失败,请联系管理员");
  3336. }
  3337. }
  3338. catch (TokenHubException ex)
  3339. {
  3340. await HttpContext.SendSseStepAsync(-1, $"TokenHub AI 调用失败: {ex.Message}");
  3341. }
  3342. catch (Exception ex)
  3343. {
  3344. _logger.LogError(ex, "邮件生成异常");
  3345. await HttpContext.SendSseStepAsync(-1, $"生成失败:{ex.Message}");
  3346. }
  3347. finally
  3348. {
  3349. await HttpContext.FinalizeSseAsync();
  3350. }
  3351. }
  3352. /// <summary>
  3353. /// 混元邮件生成提示词构建
  3354. /// </summary>
  3355. /// <param name="originUnit">出访单位</param>
  3356. /// <param name="objective">出访室友/拜访目的</param>
  3357. /// <param name="visitDate">出访时间</param>
  3358. /// <param name="clientInfosForAI">需要生成邮件的客户信息列表</param>
  3359. /// <returns>最终构建的 System Prompt 字符串</returns>
  3360. private static string BuildHunyuanEmailPrompt(string originUnit, string objective,string visitDate, List<AICreateEmailInfo> clientInfosForAI)
  3361. {
  3362. return $@"
  3363. # Role
  3364. 你是一位精通国际政企关系的【首席联络官】。你具备极强的行业分析能力,能通过 [SourceEntity] 的名称自动检索并推导其行政职能与行业地位,并以此撰写具有战略高度、语调优雅的正式商务邮件。
  3365. # Intelligence Task: Source Profiling
  3366. 在生成邮件前,请先执行以下逻辑:
  3367. 1. **职能推导**:基于 [SourceEntity] 的名称,自动识别其在所属领域的具体行政职能与政策影响力。
  3368. 2. **战略对齐**:将其职能与全球大趋势(如:Sustainable Urbanization, Digital Transformation, Carbon Neutrality)挂钩,作为邮件第二段的叙事背景。
  3369. # [RICH_TEXT_STANDARDS_FOR_QUILL]
  3370. - **Prohibited Tags**: 绝对禁止包含 <html>, <body>, <head>, <!DOCTYPE>, <hr>, <style>。
  3371. - **Whitelisted Tags**: 仅允许使用 <h3> (用于主题), <p> (用于段落), <ul>, <li>, <strong>, <br>。
  3372. - **Structural Integrity**:
  3373. - 严禁出现裸露文本,所有段落必须由 <p> 包裹。
  3374. - 列表必须使用标准 <ul><li> 嵌套结构。
  3375. - **No Markdown**: 严禁在 Content 中出现 ###, **, --- 等 Markdown 符号。
  3376. # Reference Model (Few-Shot Example)
  3377. 在生成时,请严格参考以下范例的【语气】、【五段式结构】和【外交辞令】:
  3378. - Para 1: ""On behalf of [SourceEntity], I am writing to formally propose...""
  3379. - Para 2: ""As a pivotal megacity [AI根据Source地位补全]... we recognize [Target]’s leadership in...""
  3380. - Para 3: ""We are particularly keen to explore... [基于Target经营范围推导的3个技术点]...""
  3381. - Para 4: ""We propose a 2–3 day visit... during the week of [Logistics]...""
  3382. - Para 5: ""Enclosed please find the [PascalCase_File]...""
  3383. # Task
  3384. 根据 [TargetList] 中每个单位的【经营范围】,为 [SourceEntity] 生成独立的英文访问请求邮件。
  3385. # Inputs
  3386. - [SourceEntity]: [{originUnit}]
  3387. - [VisitPurpose]: [{objective}]
  3388. - [TargetList]: [{JsonConvert.SerializeObject(clientInfosForAI)}]
  3389. - [Logistics]: [{visitDate}]
  3390. # Execution Logic (Chain of Thought)
  3391. 1. **Scope-to-Focus Analysis**:
  3392. - 深入分析每个 Target 的【经营范围】。
  3393. - 自动推导 3 个与 [VisitPurpose] 高度对齐的专业考察点(如:Policy Frameworks, Technical Standards, Operational Case Studies)。
  3394. 2. **Modular Drafting**:
  3395. - 必须严格遵循参考范例的 5 段式逻辑。
  3396. - 针对不同职能属性(监管/技术/运营)动态微调邮件主题(Subject)。
  3397. 3. **No Personnel Reference**: 严禁提及“名单、人数、成员、审核中”等任何具体人员信息,保持机构对等对话的高度。
  3398. # Constraints & Standards
  3399. - **Tone**: Formal, Strategic, and Executive (庄重、具战略高度、执行力强)。
  3400. - **Naming Protocol**: 附件引用统一使用 `JointVisitAgenda`, `StrategicInquiryBrief` (PascalCase)。
  3401. - **Escaping**: 必须对 HTML 内部的所有双引号进行严格转义(\""),确保 JSON 字符串合法。
  3402. - **Output Format**: 仅输出一个合规的 JSON 数组,不包含任何解释性文字。
  3403. - **Field Mapping**:
  3404. - `Guid`: 原样保留。
  3405. - `NameCn`: 原样保留。
  3406. - `Scope`: 原样保留。
  3407. - `Subject`: 动态生成的英文主题。
  3408. - `Content`: 包含 QUILL的英文正文。
  3409. # JSON Structure Template
  3410. [
  3411. {{
  3412. ""Guid"": ""ID"",
  3413. ""NameCn"": ""Entity Name"",
  3414. ""Scope"": ""Original Scope Description"",
  3415. ""Subject"": ""Dynamic English Subject Line"",
  3416. ""Content"": ""<h3>Subject: ...</h3><p>Dear Leadership Team of <strong>[Target]</strong>,</p><p>On behalf of <strong>[SourceEntity]</strong>, I am writing to propose...</p><ul><li><strong>Focus Area:</strong> Technical analysis of...</li></ul><p>Sincerely,<br><strong>Office of the Chief Liaison</strong><br>[SourceEntity]</p>""
  3417. }}
  3418. ]";
  3419. }
  3420. /// <summary>
  3421. /// 商邀资料AI 邮件保存
  3422. /// </summary>
  3423. /// <returns></returns>
  3424. [HttpPost]
  3425. [ProducesResponseType(typeof(JsonView), StatusCodes.Status200OK)]
  3426. public async Task<IActionResult> InvitationAIEmailSave(InvitationAIEmailSaveDto dto)
  3427. {
  3428. if (dto.Id < 1) return Ok(JsonView(false, "请选择保存的团组"));
  3429. if (dto.CurrUserId < 1) return Ok(JsonView(false, "请传入用户Id"));
  3430. if (string.IsNullOrEmpty(dto.Guid)) return Ok(JsonView(false, "请传入Guid"));
  3431. if (string.IsNullOrEmpty(dto.EmailTitle)) return Ok(JsonView(false, "请传入邮件标题"));
  3432. if (string.IsNullOrEmpty(dto.EmailContent)) return Ok(JsonView(false, "请传入邮件内容"));
  3433. var invAiInfo = await _sqlSugar.Queryable<Res_InvitationAI>()
  3434. .Where(x => x.Id == dto.Id)
  3435. .FirstAsync();
  3436. if (invAiInfo == null) return Ok(JsonView(false, "数据信息为空"));
  3437. var dataList = invAiInfo?.AiCrawledDetails;
  3438. if (dataList == null) return Ok(JsonView(false, "邀请方数据信息为空"));
  3439. var editInfo = dataList.Where(x => x.Guid == dto.Guid).FirstOrDefault();
  3440. if (editInfo == null) return Ok(JsonView(false, "邀请方信息为空"));
  3441. var opUserName = await _sqlSugar.Queryable<Sys_Users>().Where(x => x.IsDel == 0 && x.Id == dto.CurrUserId).Select(x => x.CnName).FirstAsync() ?? "-";
  3442. editInfo.EmailInfo.EmailTitle = dto.EmailTitle;
  3443. editInfo.EmailInfo.EmailContent = dto.EmailContent;
  3444. editInfo.EmailInfo.Operator = opUserName;
  3445. editInfo.EmailInfo.OperatedAt = DateTime.Now;
  3446. var datas = dataList.Where(x => x.Guid != dto.Guid).ToList();
  3447. datas.Add(editInfo);
  3448. // 排序
  3449. invAiInfo.AiCrawledDetails = datas.OrderByDescending(x => x.OperatedAt).ToList();
  3450. var editUpd = await _sqlSugar.Updateable(invAiInfo).UpdateColumns(x => x.AiCrawledDetails).ExecuteCommandAsync();
  3451. if (editUpd < 1)
  3452. {
  3453. return Ok(JsonView(true, "邮件保存失败"));
  3454. }
  3455. return Ok(JsonView(true, "邮件保存成功"));
  3456. }
  3457. /// <summary>
  3458. /// 商邀资料AI 邮箱附件上传
  3459. /// </summary>
  3460. [HttpPost]
  3461. [ProducesResponseType(typeof(JsonView), StatusCodes.Status200OK)]
  3462. public async Task<IActionResult> InvitationAIFileSave([FromForm] InvitationAIFileSaveDto dto)
  3463. {
  3464. // 1. 炼金前置:严格参数校验
  3465. if (dto.Id < 1) return Ok(JsonView(false, "请选择保存的团组"));
  3466. if (dto.CurrUserId < 1) return Ok(JsonView(false, "请传入用户Id"));
  3467. if (string.IsNullOrEmpty(dto.Guid)) return Ok(JsonView(false, "请传入Guid"));
  3468. if (dto.Attachments == null || !dto.Attachments.Any()) return Ok(JsonView(false, "请传入附件"));
  3469. // 2. 数据获取:利用 SqlSugar 异步查询
  3470. var invAiInfo = await _sqlSugar.Queryable<Res_InvitationAI>()
  3471. .InSingleAsync(dto.Id);
  3472. if (invAiInfo?.AiCrawledDetails == null) return Ok(JsonView(false, "邀请方信息不存在"));
  3473. // 3. 业务定位:定位到具体需要编辑的 Guid 记录
  3474. var editInfo = invAiInfo.AiCrawledDetails.FirstOrDefault(x => x.Guid == dto.Guid);
  3475. if (editInfo == null) return Ok(JsonView(false, "未找到匹配的 Guid 记录"));
  3476. // 4. 追踪信息更新
  3477. var opUserName = await _sqlSugar.Queryable<Sys_Users>()
  3478. .Where(x => x.Id == dto.CurrUserId && x.IsDel == 0)
  3479. .Select(x => x.CnName)
  3480. .FirstAsync() ?? "-";
  3481. // 更新操作人与时间 (利用引用类型特性)
  3482. editInfo.Operator = opUserName;
  3483. editInfo.OperatedAt = DateTime.Now;
  3484. // 初始化 EmailInfo 确保不为 null
  3485. editInfo.EmailInfo ??= new EmailInfo();
  3486. editInfo.EmailInfo.Operator = opUserName;
  3487. editInfo.EmailInfo.OperatedAt = DateTime.Now;
  3488. editInfo.EmailInfo.AttachmentPaths ??= new List<string>();
  3489. // 5. 构建绝对路径与相对路径
  3490. string dirName = $"{editInfo.NameEn?.Trim() ?? "Default"}_{dto.Guid}";
  3491. string baseDir = AppSettingsHelper.Get("InvitationAIAssistBasePath");
  3492. string ftpBase = AppSettingsHelper.Get("InvitationAIAssistFtpPath");
  3493. string absolutePath = Path.Combine(baseDir, dirName);
  3494. string relativePathPrefix = $"{ftpBase}{dirName}/";
  3495. // 核心修复:确保父级目录递归创建,防止 DirectoryNotFoundException
  3496. if (!Directory.Exists(absolutePath))
  3497. {
  3498. Directory.CreateDirectory(absolutePath);
  3499. }
  3500. var newSavedRelativePaths = new List<string>();
  3501. // 6. 持续文件存储与实时验证
  3502. foreach (var file in dto.Attachments)
  3503. {
  3504. // 文件名安全过滤
  3505. string safeName = string.Join("_", file.FileName.Split(Path.GetInvalidFileNameChars()));
  3506. string fullPath = Path.Combine(absolutePath, safeName);
  3507. // 重名冲突处理:文件名(n).ext
  3508. if (System.IO.File.Exists(fullPath))
  3509. {
  3510. string fileNameOnly = Path.GetFileNameWithoutExtension(safeName);
  3511. string extension = Path.GetExtension(safeName);
  3512. int count = 1;
  3513. while (System.IO.File.Exists(fullPath))
  3514. {
  3515. safeName = $"{fileNameOnly}({count++}){extension}";
  3516. fullPath = Path.Combine(absolutePath, safeName);
  3517. }
  3518. }
  3519. try
  3520. {
  3521. // 写入流:使用作用域隔离,确保退出 using 时立即释放句柄
  3522. using (var stream = new FileStream(fullPath, FileMode.Create, FileAccess.Write, FileShare.None))
  3523. {
  3524. await file.CopyToAsync(stream);
  3525. await stream.FlushAsync(); // 强制落盘
  3526. }
  3527. // 实时验证:物理验证 + 逻辑校验
  3528. var fileInfo = new FileInfo(fullPath);
  3529. if (fileInfo.Exists && fileInfo.Length == file.Length)
  3530. {
  3531. newSavedRelativePaths.Add($"{relativePathPrefix}{safeName}");
  3532. }
  3533. }
  3534. catch (Exception ex)
  3535. {
  3536. // 发生 IO 异常时清理残余文件并抛出,触发外层处理
  3537. if (System.IO.File.Exists(fullPath)) System.IO.File.Delete(fullPath);
  3538. // 此处记录日志:
  3539. _logger.LogError(ex, "File Save Error");
  3540. continue; // 跳过失败的文件,继续下一个
  3541. }
  3542. }
  3543. if (!newSavedRelativePaths.Any()) return Ok(JsonView(false, "所有文件上传均失败"));
  3544. // 7. 数据合并与持久化:
  3545. // 将新路径与旧路径合并,并去重
  3546. var updatedPaths = editInfo.EmailInfo.AttachmentPaths;
  3547. updatedPaths.AddRange(newSavedRelativePaths);
  3548. editInfo.EmailInfo.AttachmentPaths = updatedPaths.Distinct().ToList();
  3549. // 重新排序整体列表
  3550. invAiInfo.AiCrawledDetails = invAiInfo.AiCrawledDetails
  3551. .OrderByDescending(x => x.OperatedAt)
  3552. .ToList();
  3553. // SqlSugar 高效部分列更新
  3554. var isSuccess = await _sqlSugar.Updateable(invAiInfo)
  3555. .UpdateColumns(x => x.AiCrawledDetails)
  3556. .ExecuteCommandAsync() > 0;
  3557. if (!isSuccess) return Ok(JsonView(false, "数据库更新失败"));
  3558. // 8. 返回前端数据 (支持 lowerCamelCase)
  3559. var officeBaseUrl = AppSettingsHelper.Get("OfficeBaseUrl");
  3560. var finalResultUrls = editInfo.EmailInfo.AttachmentPaths
  3561. .Select(path => $"{officeBaseUrl}{path}")
  3562. .ToList();
  3563. return Ok(JsonView(true, "邮件附件保存成功", finalResultUrls));
  3564. }
  3565. /// <summary>
  3566. /// 商邀资料AI 邮箱附件删除
  3567. /// </summary>
  3568. [HttpPost]
  3569. [ProducesResponseType(typeof(JsonView), StatusCodes.Status200OK)]
  3570. public async Task<IActionResult> InvitationAIFileDel([FromBody] InvitationAIFileDelDto dto)
  3571. {
  3572. // 1. 基础校验
  3573. if (dto.Id < 1) return Ok(JsonView(false, "请选择团组"));
  3574. if (string.IsNullOrEmpty(dto.Guid)) return Ok(JsonView(false, "请传入Guid"));
  3575. if (dto.AttachmentNames == null || !dto.AttachmentNames.Any()) return Ok(JsonView(false, "请传入待删除附件名称"));
  3576. // 2. 数据获取
  3577. var invAiInfo = await _sqlSugar.Queryable<Res_InvitationAI>().InSingleAsync(dto.Id);
  3578. if (invAiInfo?.AiCrawledDetails == null) return Ok(JsonView(false, "业务数据不存在"));
  3579. var editInfo = invAiInfo.AiCrawledDetails.FirstOrDefault(x => x.Guid == dto.Guid);
  3580. if (editInfo?.EmailInfo?.AttachmentPaths == null) return Ok(JsonView(false, "未找到对应的附件记录"));
  3581. // 3. 操作人追踪
  3582. var opUserName = await _sqlSugar.Queryable<Sys_Users>()
  3583. .Where(x => x.Id == dto.CurrUserId && x.IsDel == 0)
  3584. .Select(x => x.CnName).FirstAsync() ?? "System";
  3585. // 更新追踪状态
  3586. editInfo.Operator = editInfo.EmailInfo.Operator = opUserName;
  3587. editInfo.OperatedAt = editInfo.EmailInfo.OperatedAt = DateTime.Now;
  3588. // 4. 路径
  3589. string dirName = $"{editInfo.NameEn?.Trim() ?? "Default"}_{dto.Guid}";
  3590. string absoluteDir = Path.Combine(AppSettingsHelper.Get("InvitationAIAssistBasePath"), dirName);
  3591. // 获取当前数据库中的相对路径列表
  3592. var currentPaths = editInfo.EmailInfo.AttachmentPaths;
  3593. bool isChanged = false;
  3594. // 5. 核心删除逻辑
  3595. foreach (var fileName in dto.AttachmentNames)
  3596. {
  3597. // 查找数据库中是否存在包含该文件名的路径 (忽略大小写比较)
  3598. var targetRelativePath = currentPaths.FirstOrDefault(p => p.EndsWith("/" + fileName) || p.Equals(fileName));
  3599. if (targetRelativePath != null)
  3600. {
  3601. // A. 从数据库记录中移除
  3602. currentPaths.Remove(targetRelativePath);
  3603. isChanged = true;
  3604. // B. 物理文件删除逻辑
  3605. // 注意:fullPath 必须是 [基础路径] + [目录名] + [纯文件名]
  3606. string fullPath = Path.Combine(absoluteDir, fileName);
  3607. try
  3608. {
  3609. if (System.IO.File.Exists(fullPath))
  3610. {
  3611. System.IO.File.Delete(fullPath);
  3612. }
  3613. }
  3614. catch (Exception ex)
  3615. {
  3616. // 记录 IO 异常但不中断流程
  3617. _logger.LogWarning($"物理文件删除失败: {fullPath}, {ex.Message}");
  3618. }
  3619. }
  3620. }
  3621. if (!isChanged) return Ok(JsonView(false, "未找到匹配的可删除附件"));
  3622. // 6. 数据同步与持久化
  3623. editInfo.EmailInfo.AttachmentPaths = currentPaths;
  3624. // 排序 (利用引用类型,无需手动 Add/Remove)
  3625. invAiInfo.AiCrawledDetails = invAiInfo.AiCrawledDetails.OrderByDescending(x => x.OperatedAt).ToList();
  3626. var isUpd = await _sqlSugar.Updateable(invAiInfo)
  3627. .UpdateColumns(x => x.AiCrawledDetails)
  3628. .ExecuteCommandAsync() > 0;
  3629. if (!isUpd) return Ok(JsonView(false, "数据库记录更新失败"));
  3630. // 7. 返回剩余附件的完整访问地址
  3631. var officeBaseUrl = AppSettingsHelper.Get("OfficeBaseUrl");
  3632. var remainingUrls = editInfo.EmailInfo.AttachmentPaths
  3633. .Select(path => $"{officeBaseUrl}{path}")
  3634. .ToList();
  3635. return Ok(JsonView(true, "附件删除成功", remainingUrls));
  3636. }
  3637. /// <summary>
  3638. /// 商邀资料AI 发送邮件(SSE 流式推送)
  3639. /// </summary>
  3640. [HttpPost]
  3641. public async Task InvitationAISeedEmailStream([FromBody] InvitationAISeedEmailDto dto)
  3642. {
  3643. // 1. 初始化 SSE
  3644. HttpContext.InitializeSse();
  3645. try
  3646. {
  3647. await HttpContext.SendSseStepAsync(5, "正在准备发送邮件队列...");
  3648. #region 1. 参数与权限前置校验
  3649. if (dto.Id < 1 || dto.CurrUserId < 1 || dto.Guids == null || !dto.Guids.Any())
  3650. {
  3651. await HttpContext.SendSseStepAsync(-1, "参数验证失败,请检查选择的单位及用户状态。");
  3652. return;
  3653. }
  3654. // hotmail 配置信息验证
  3655. var hotmailConfig = await _hotmailService.GetUserMailConfig(dto.CurrUserId);
  3656. (bool verify,string msg) = _hotmailService.ConfigVerify(hotmailConfig);
  3657. if (!verify)
  3658. {
  3659. await HttpContext.SendSseStepAsync(-1, msg);
  3660. return;
  3661. }
  3662. // 获取商邀信息和用户信息
  3663. var invAiInfo = await _sqlSugar.Queryable<Res_InvitationAI>().FirstAsync(x => x.IsDel == 0 && x.Id == dto.Id);
  3664. var userInfo = await _sqlSugar.Queryable<Sys_Users>()
  3665. .Where(x => x.IsDel == 0 && x.Id == dto.CurrUserId)
  3666. .Select(x => new { x.Email, x.CnName })
  3667. .FirstAsync();
  3668. if (invAiInfo?.AiCrawledDetails == null)
  3669. {
  3670. await HttpContext.SendSseStepAsync(-1, "未找到有效的邀请方数据。");
  3671. return;
  3672. }
  3673. // 提取待发送的目标集合
  3674. var guidSet = new HashSet<string>(dto.Guids);
  3675. var seedInvInfos = invAiInfo.AiCrawledDetails.Where(x => guidSet.Contains(x.Guid)).ToList();
  3676. if (!seedInvInfos.Any())
  3677. {
  3678. await HttpContext.SendSseStepAsync(-1, "所选单位信息在原始库中不存在。");
  3679. return;
  3680. }
  3681. #endregion
  3682. await HttpContext.SendSseStepAsync(15, $"准备就绪,共计 {seedInvInfos.Count} 封邮件待发送...");
  3683. #region 2. 批量发送逻辑 (流式反馈)
  3684. int total = seedInvInfos.Count;
  3685. int current = 0;
  3686. var successCount = 0;
  3687. var failCount = 0;
  3688. foreach (var item in seedInvInfos)
  3689. {
  3690. current++;
  3691. // 计算进度:从 20% 到 90%
  3692. int progress = 20 + (int)((double)current / total * 70);
  3693. if (string.IsNullOrEmpty(item.EmailInfo?.EmailTitle) || string.IsNullOrEmpty(item.EmailInfo?.EmailContent))
  3694. {
  3695. await HttpContext.SendSseStepAsync(progress, $"跳过:{item.NameCn} (邮件标题或内容缺失)");
  3696. failCount++;
  3697. continue;
  3698. }
  3699. try
  3700. {
  3701. var req = new MailDto() {
  3702. Subject = item.EmailInfo.EmailTitle,
  3703. Content = item.EmailInfo.EmailContent,
  3704. To = item.Email,
  3705. AttachmentPaths = item.EmailInfo.AttachmentPaths
  3706. };
  3707. var res = await _hotmailService.SendMailAsync(hotmailConfig.UserName, req);
  3708. if (res.IsSuccess)
  3709. {
  3710. successCount++;
  3711. // 更新本地状态
  3712. item.EmailInfo.Status = 4; // 发送成功状态
  3713. item.EmailInfo.Operator = userInfo.CnName;
  3714. item.EmailInfo.OperatedAt = DateTime.Now;
  3715. await HttpContext.SendSseStepAsync(progress, $"成功:已向 {item.NameCn}({item.Email}) 发送邮件");
  3716. }
  3717. else
  3718. {
  3719. failCount++;
  3720. await HttpContext.SendSseStepAsync(progress, $"失败:{item.NameCn} 发送失败 ({res.Message})");
  3721. }
  3722. }
  3723. catch (Exception ex)
  3724. {
  3725. failCount++;
  3726. _logger.LogError(ex, $"企微邮件推送异常:{item.NameCn}");
  3727. await HttpContext.SendSseStepAsync(progress, $"异常:{item.NameCn} 连接超时");
  3728. }
  3729. }
  3730. #endregion
  3731. await HttpContext.SendSseStepAsync(95, "正在归档发送记录...");
  3732. #region 3. 数据同步与收尾
  3733. // 更新内存中的全量数据:排除旧的,加入已更新状态的
  3734. invAiInfo.AiCrawledDetails = invAiInfo.AiCrawledDetails
  3735. .Where(x => !guidSet.Contains(x.Guid))
  3736. .Concat(seedInvInfos)
  3737. .OrderByDescending(x => x.EmailInfo.OperatedAt)
  3738. .ToList();
  3739. // 精准更新 JSON 列
  3740. var updateOk = await _sqlSugar.Updateable(invAiInfo)
  3741. .UpdateColumns(x => x.AiCrawledDetails)
  3742. .ExecuteCommandHasChangeAsync();
  3743. if (!updateOk)
  3744. {
  3745. await HttpContext.SendSseStepAsync(-1, "记录保存失败,请检查数据库连接。");
  3746. return;
  3747. }
  3748. await HttpContext.SendSseStepAsync(100, $"任务完成!成功: {successCount}, 失败: {failCount}", new
  3749. {
  3750. Id = invAiInfo.Id,
  3751. SuccessCount = successCount,
  3752. FailCount = failCount
  3753. });
  3754. #endregion
  3755. }
  3756. catch (Exception ex)
  3757. {
  3758. _logger.LogError(ex, "邮件发送流发生崩溃");
  3759. await HttpContext.SendSseStepAsync(-1, $"系统错误:{ex.Message}");
  3760. }
  3761. finally
  3762. {
  3763. await HttpContext.FinalizeSseAsync();
  3764. }
  3765. }
  3766. #endregion
  3767. #region 商邀资料 AI - 无团组版
  3768. /// <summary>
  3769. /// 商邀资料AI-无团组版 基础数据源
  3770. /// </summary>
  3771. /// <returns></returns>
  3772. [HttpGet]
  3773. [ProducesResponseType(typeof(JsonView), StatusCodes.Status200OK)]
  3774. public async Task<IActionResult> InvitationAI_NoGroupInit()
  3775. {
  3776. var itemNames = await InvitationAI_NoGroupInvName();
  3777. var unitNames = await InvitationAIClientName();
  3778. var countries = await InvitationAICountryName();
  3779. var industryNodes = IndustryTree.Build().Select(x => x.NameCn).ToList();
  3780. var orgScales = OrgScale.BuildInitialData().Select(x => x.Name).ToList();
  3781. var orgLevels = new List<string>() {
  3782. "全部",
  3783. "总部",
  3784. "分公司"
  3785. };
  3786. return Ok(JsonView(true, $"查询成功!", new
  3787. {
  3788. itemNames,
  3789. unitNames,
  3790. countries,
  3791. industryNodes,
  3792. orgScales,
  3793. orgLevels
  3794. }));
  3795. }
  3796. /// <summary>
  3797. /// 商邀资料AI-无团组版 资料列表
  3798. /// </summary>
  3799. /// <returns></returns>
  3800. [HttpGet("{name}")]
  3801. [ProducesResponseType(typeof(JsonView), StatusCodes.Status200OK)]
  3802. public async Task<IActionResult> InvitationAI_NoGroupItemByName(string name)
  3803. {
  3804. if (string.IsNullOrWhiteSpace(name)) return Ok(JsonView(false, "名称无效"));
  3805. // 1. 获取主表记录
  3806. var info = await _sqlSugar.Queryable<Res_InvitationAI_NoGroup>()
  3807. .FirstAsync(x => x.IsDel == 0 && x.InvName == name);
  3808. // 2. 分支处理:若无主表记录,尝试从团组表同步基本信息
  3809. if (info == null)
  3810. {
  3811. return Ok(JsonView(true, "暂无数据", new
  3812. {
  3813. Id = 0,
  3814. InvName = name,
  3815. AiCrawledDetails = new List<InvitationAI_NoGroupInfo>(),
  3816. }));
  3817. }
  3818. // 3. 数据填充(使用 ??= 语法确保 Null 安全)
  3819. info.EntryInfo ??= new Entry_NoGroupInfo();
  3820. // 4. 排序:ThenByDescending 确保多级排序生效
  3821. info.AiCrawledDetails = info.AiCrawledDetails
  3822. .OrderByDescending(x => x.IsChecked)
  3823. .ThenByDescending(x => x.OperatedAt)
  3824. .ToList();
  3825. var details = info?.AiCrawledDetails ?? new List<InvitationAI_NoGroupInfo>();
  3826. string baseUrl = AppSettingsHelper.Get("OfficeBaseUrl")?.TrimEnd('/');
  3827. if (details.Any())
  3828. {
  3829. foreach (var detail in details)
  3830. {
  3831. // 附件路径处理
  3832. if (detail.EmailInfo?.AttachmentPaths?.Any() == true)
  3833. {
  3834. detail.EmailInfo.AttachmentPaths = detail.EmailInfo.AttachmentPaths
  3835. .Select(p => p.StartsWith("http") ? p : $"{baseUrl}/{p.TrimStart('/')}")
  3836. .ToList();
  3837. }
  3838. // 官网地址注入新闻首位 (确保 PostUrl 已实例化)
  3839. if (!string.IsNullOrEmpty(detail.SiteUrl))
  3840. {
  3841. detail.PostUrl ??= new List<PostNewsItem>();
  3842. // 避免重复插入
  3843. if (!detail.PostUrl.Any(x => x.Description == "官网"))
  3844. {
  3845. detail.PostUrl.Insert(0, new PostNewsItem
  3846. {
  3847. Date = "-",
  3848. Description = "官网",
  3849. Url = detail.SiteUrl
  3850. });
  3851. }
  3852. }
  3853. }
  3854. }
  3855. return Ok(JsonView(true, "查询成功!", new
  3856. {
  3857. info.Id,
  3858. info.InvName,
  3859. AiCrawledDetails = details,
  3860. Entry = info.EntryInfo
  3861. }));
  3862. }
  3863. /// <summary>
  3864. /// 商邀资料AI-无团组版 新增公务资料
  3865. /// </summary>
  3866. /// <returns></returns>
  3867. [HttpPost]
  3868. [ProducesResponseType(typeof(JsonView), StatusCodes.Status200OK)]
  3869. public async Task<IActionResult> InvitationAI_NoGroupAdd(InvitationAI_NoGroupAddDto dto)
  3870. {
  3871. // 基础校验
  3872. if (string.IsNullOrWhiteSpace(dto.InvName))
  3873. return Ok(JsonView(false, "请传入有效的公务名称!"));
  3874. if (dto.CurrUserId <= 0)
  3875. return Ok(JsonView(false, "用户身份无效"));
  3876. var invName = dto.InvName;
  3877. #region 数据库操作
  3878. // 数据库信息获取方式
  3879. var invExist = await _sqlSugar.Queryable<Res_InvitationAI_NoGroup>().AnyAsync(x => x.IsDel == 0 && x.InvName == invName);
  3880. if (invExist)
  3881. return Ok(JsonView(false, $"公务名称 [{invName}] 已存在,请更换"));
  3882. string operatorName = await _sqlSugar.Queryable<Sys_Users>().Where(x => x.IsDel == 0 && x.Id == dto.CurrUserId).Select(x => x.CnName).FirstAsync() ?? "-";
  3883. var dataInfo = new Res_InvitationAI_NoGroup()
  3884. {
  3885. InvName = invName,
  3886. EntryInfo = new Entry_NoGroupInfo()
  3887. {
  3888. Operator = operatorName,
  3889. OperatedAt = DateTime.Now
  3890. },
  3891. CreateUserId = dto.CurrUserId
  3892. };
  3893. var insert = await _sqlSugar.Insertable(dataInfo).ExecuteReturnIdentityAsync();
  3894. if (insert < 1)
  3895. {
  3896. return Ok(JsonView(false, $"词条信息新增失败!"));
  3897. }
  3898. #endregion
  3899. return Ok(JsonView(true, $"新增成功!", new
  3900. {
  3901. id = insert,
  3902. invName = dataInfo.InvName,
  3903. entry = dataInfo.EntryInfo,
  3904. }));
  3905. }
  3906. /// <summary>
  3907. /// 商邀资料AI-无团组版 设置词条
  3908. /// </summary>
  3909. /// <returns></returns>
  3910. [HttpPost]
  3911. [ProducesResponseType(typeof(JsonView), StatusCodes.Status200OK)]
  3912. public async Task<IActionResult> InvitationAI_NoGroupSetPrompt(InvitationAI_NoGroupSetPromptDto dto)
  3913. {
  3914. if (dto.Id < 1) return Ok(JsonView(false, "当前公务ID无效"));
  3915. if (string.IsNullOrWhiteSpace(dto.Objective)) return Ok(JsonView(false, "请输入出访目的"));
  3916. if (string.IsNullOrWhiteSpace(dto.OriginUnit)) return Ok(JsonView(false, "请输入单位名称"));
  3917. if (dto.TargetCountry == null || !dto.TargetCountry.Any()) return Ok(JsonView(false, "请选择目标国家"));
  3918. if (dto.Industries == null || !dto.Industries.Any()) return Ok(JsonView(false, "请选择所属行业"));
  3919. if (dto.ScaleTypes == null || !dto.ScaleTypes.Any()) return Ok(JsonView(false, "请选择规模类型"));
  3920. if (string.IsNullOrWhiteSpace(dto.VisitDate)) return Ok(JsonView(false, "请输入出访时间"));
  3921. // 数据库信息获取方式
  3922. var dataInfo = await _sqlSugar.Queryable<Res_InvitationAI_NoGroup>().Where(x => x.IsDel == 0 && x.Id == dto.Id).FirstAsync();
  3923. if (dataInfo == null)
  3924. return Ok(JsonView(false, "请先新增公务信息"));
  3925. string operatorName = await _sqlSugar.Queryable<Sys_Users>().Where(x => x.IsDel == 0 && x.Id == dto.CurrUserId).Select(x => x.CnName).FirstAsync() ?? "-";
  3926. dataInfo.EntryInfo = new Entry_NoGroupInfo()
  3927. {
  3928. VisitDate = dto.VisitDate?.Trim(),
  3929. OriginUnit = dto.OriginUnit?.Trim(),
  3930. TargetCountry = dto.TargetCountry,
  3931. Objective = dto.Objective?.Trim(),
  3932. Industries = dto.Industries,
  3933. ScaleTypes = dto.ScaleTypes,
  3934. IsBackground = dto.IsBackground,
  3935. OrgLevel = dto.OrgLevel,
  3936. OtherConstraints = dto.OtherConstraints,
  3937. Operator = operatorName,
  3938. OperatedAt = DateTime.Now
  3939. };
  3940. var updateResult = await _sqlSugar.Updateable(dataInfo)
  3941. .UpdateColumns(x => new { x.EntryInfo })
  3942. .ExecuteCommandHasChangeAsync();
  3943. if (!updateResult)
  3944. {
  3945. return Ok(JsonView(false, $"词条信息更新失败!"));
  3946. }
  3947. return Ok(JsonView(true, $"设置成功!", new
  3948. {
  3949. id = dataInfo.Id,
  3950. invName = dataInfo.InvName,
  3951. entry = dataInfo.EntryInfo
  3952. }));
  3953. }
  3954. /// <summary>
  3955. /// 商邀资料AI-无团组版 设置复选框选中 批量(支持传入空数组进行重置)
  3956. /// </summary>
  3957. /// <param name="dto"></param>
  3958. /// <returns></returns>
  3959. [HttpPost]
  3960. public async Task<IActionResult> InvitationAI_NoGroupSetChecked([FromBody] InvitationAI_NoGroupSetCheckedDto dto)
  3961. {
  3962. // 1. 基础参数校验 (注意:这里移除了对 Guids.Any() 的强校验,允许空集合)
  3963. if (dto.Id < 1 || dto.CurrUserId < 1 || dto.Guids == null)
  3964. return Ok(JsonView(false, "请求参数不完整"));
  3965. // 2. 获取主记录
  3966. var invAiInfo = await _sqlSugar.Queryable<Res_InvitationAI_NoGroup>()
  3967. .FirstAsync(x => x.IsDel == 0 && x.Id == dto.Id);
  3968. if (invAiInfo?.AiCrawledDetails == null || !invAiInfo.AiCrawledDetails.Any())
  3969. return Ok(JsonView(false, "数据不存在或集合为空"));
  3970. // 3. 准备更新所需数据
  3971. var guidSet = dto.Guids.ToHashSet();
  3972. var now = DateTime.Now;
  3973. // 获取操作人姓名
  3974. string operatorName = await _sqlSugar.Queryable<Sys_Users>()
  3975. .Where(x => x.Id == dto.CurrUserId)
  3976. .Select(x => x.CnName)
  3977. .FirstAsync() ?? "-";
  3978. // 4. 单次遍历完成 重置 + 更新
  3979. // 如果 guidSet 为空,循环会将所有项的 IsChecked 置为 false
  3980. foreach (var item in invAiInfo.AiCrawledDetails)
  3981. {
  3982. if (guidSet.Contains(item.Guid))
  3983. {
  3984. item.IsChecked = true;
  3985. }
  3986. else
  3987. {
  3988. item.IsChecked = false;
  3989. }
  3990. item.OperatedAt = now;
  3991. item.Operator = operatorName;
  3992. }
  3993. // 5. 排序逻辑
  3994. // 即使是全量取消选中,也可以按最后操作时间排序,让最近变动的项在前
  3995. invAiInfo.AiCrawledDetails = invAiInfo.AiCrawledDetails
  3996. .OrderByDescending(x => x.OperatedAt)
  3997. .ToList();
  3998. // 6. 提交数据库
  3999. // 注意:SqlSugar 的 UpdateColumns 会序列化整个对象列表并覆盖数据库字段
  4000. await _sqlSugar.Updateable(invAiInfo)
  4001. .UpdateColumns(x => x.AiCrawledDetails)
  4002. .ExecuteCommandAsync();
  4003. return Ok(JsonView(true, guidSet.Any() ? "设置成功" : "已全部取消选中"));
  4004. }
  4005. /// <summary>
  4006. /// 商邀资料AI-无团组版 保存
  4007. /// </summary>
  4008. /// <returns></returns>
  4009. [HttpPost]
  4010. [ProducesResponseType(typeof(JsonView), StatusCodes.Status200OK)]
  4011. public async Task<IActionResult> InvitationAI_NoGroupSave(InvitationAI_NoGroupSaveDto dto)
  4012. {
  4013. if (dto.Id < 1) return Ok(JsonView(false, "请先设置AI关键字"));
  4014. if (dto.CurrUserId < 1) return Ok(JsonView(false, "请传入用户Id"));
  4015. if (dto.AiCrawledDetail == null) return Ok(JsonView(false, "请选择保存的邀请方详情"));
  4016. var invAiInfo = await _sqlSugar.Queryable<Res_InvitationAI_NoGroup>()
  4017. .Where(x => x.Id == dto.Id)
  4018. .FirstAsync();
  4019. if (invAiInfo == null) return Ok(JsonView(false, "数据信息为空"));
  4020. var dataList = invAiInfo?.AiCrawledDetails;
  4021. if (dataList == null) return Ok(JsonView(false, "邀请方数据信息为空"));
  4022. var datas = dataList.Where(x => x.Guid != dto.AiCrawledDetail.Guid).ToList();
  4023. var editInfo = dto.AiCrawledDetail;
  4024. // 如果 Guid 为空,说明是新增数据,需要生成新的 Guid
  4025. if (string.IsNullOrEmpty(editInfo.Guid))
  4026. {
  4027. editInfo.Guid = Guid.NewGuid().ToString("N");
  4028. editInfo.Source = 2; // 标记为手动新增数据
  4029. editInfo.OperatedAt = DateTime.Now;
  4030. }
  4031. var opUserName = await _sqlSugar.Queryable<Sys_Users>().Where(x => x.IsDel == 0 && x.Id == dto.CurrUserId).Select(x => x.CnName).FirstAsync() ?? "-";
  4032. editInfo.Operator = opUserName;
  4033. editInfo.OperatedAt = DateTime.Now;
  4034. datas.Add(editInfo);
  4035. invAiInfo.AiCrawledDetails = datas.OrderByDescending(x => x.OperatedAt).ToList();
  4036. var editUpd = await _sqlSugar.Updateable(invAiInfo).UpdateColumns(x => x.AiCrawledDetails).ExecuteCommandAsync();
  4037. if (editUpd < 1)
  4038. {
  4039. return Ok(JsonView(true, "保存失败"));
  4040. }
  4041. return Ok(JsonView(true, "保存成功"));
  4042. }
  4043. /// <summary>
  4044. /// 商邀资料AI-无团组版 单条删除
  4045. /// </summary>
  4046. /// <returns></returns>
  4047. [HttpPost]
  4048. [ProducesResponseType(typeof(JsonView), StatusCodes.Status200OK)]
  4049. public async Task<IActionResult> InvitationAI_NoGroupSingleDel(InvitationAI_NoGroupSingleDelDto dto)
  4050. {
  4051. int id = dto.Id;
  4052. string guid = dto.Guid;
  4053. // 基础校验
  4054. if (id < 1 || string.IsNullOrWhiteSpace(guid))
  4055. return Ok(JsonView(false, "请传入有效的Id和Guid!"));
  4056. var dataInfo = await _sqlSugar.Queryable<Res_InvitationAI_NoGroup>().Where(x => x.IsDel == 0 && x.Id == id).FirstAsync();
  4057. if (dataInfo == null)
  4058. return Ok(JsonView(false, "当前数据信息不存在!"));
  4059. var newDataInfos = dataInfo.AiCrawledDetails.Where(x => x.Guid != guid).ToList();
  4060. dataInfo.AiCrawledDetails = newDataInfos.OrderByDescending(x => x.OperatedAt).ToList();
  4061. var update = await _sqlSugar.Updateable(dataInfo).ExecuteCommandAsync();
  4062. if (update < 1)
  4063. {
  4064. return Ok(JsonView(false, $"删除失败!"));
  4065. }
  4066. return Ok(JsonView(true, $"删除成功!", new
  4067. {
  4068. dataInfo.Id,
  4069. dataInfo.InvName,
  4070. dataInfo.AiCrawledDetails
  4071. }));
  4072. }
  4073. /// <summary>
  4074. /// 商邀资料AI-无团组版 AI查询出的数据添加至商邀资料库
  4075. /// </summary>
  4076. /// <returns></returns>
  4077. [HttpPost]
  4078. [ProducesResponseType(typeof(JsonView), StatusCodes.Status200OK)]
  4079. public async Task<IActionResult> InvitationAI_NoGroupInsertResource([FromBody] InvitationAI_NoGroupInsertResourceDto dto)
  4080. {
  4081. // 基础校验
  4082. if (dto.Id < 1 || dto.CurrUserId < 1 || dto.Guids == null || !dto.Guids.Any())
  4083. return Ok(JsonView(false, "参数不完整,请选择有效的单位数据"));
  4084. // 获取主记录
  4085. var dataInfo = await _sqlSugar.Queryable<Res_InvitationAI_NoGroup>()
  4086. .FirstAsync(x => x.IsDel == 0 && x.Id == dto.Id);
  4087. if (dataInfo?.AiCrawledDetails == null)
  4088. return Ok(JsonView(false, "当前数据信息不存在!"));
  4089. // 筛选出待转正的 AI 数据 (使用 HashSet 优化匹配)
  4090. var guidSet = dto.Guids.ToHashSet();
  4091. var targetAiDetails = dataInfo.AiCrawledDetails
  4092. .Where(x => x.Source != 0 && guidSet.Contains(x.Guid))
  4093. .ToList();
  4094. if (!targetAiDetails.Any())
  4095. return Ok(JsonView(false, "未找到符合条件的 AI 获取信息,无法重复添加或来源错误!"));
  4096. // 准备入库数据(批量构建)
  4097. var now = DateTime.Now;
  4098. var opUserName = await _sqlSugar.Queryable<Sys_Users>().Where(x => x.IsDel == 0 && x.Id == dto.CurrUserId).Select(x => x.CnName).FirstAsync() ?? "-";
  4099. var rawLocalDatas = targetAiDetails.Select(item => {
  4100. // 修改内存中的状态为本地数据
  4101. item.Source = 0;
  4102. item.OperatedAt = now;
  4103. item.Operator = opUserName;
  4104. return new Res_InvitationOfficialActivityData()
  4105. {
  4106. // 批量加密操作
  4107. Country = AesEncryptionHelper.Encrypt(item.Region),
  4108. UnitName = AesEncryptionHelper.Encrypt(item.NameCn),
  4109. Address = AesEncryptionHelper.Encrypt(item.Address),
  4110. Field = AesEncryptionHelper.Encrypt(item.Scope),
  4111. Contact = AesEncryptionHelper.Encrypt(item.Contact),
  4112. Tel = AesEncryptionHelper.Encrypt(item.Phone),
  4113. Email = AesEncryptionHelper.Encrypt(item.Email),
  4114. UnitWeb = AesEncryptionHelper.Encrypt(item.SiteUrl),
  4115. LastUpdateUserId = dto.CurrUserId,
  4116. LastUpdateTime = now,
  4117. CreateUserId = dto.CurrUserId,
  4118. CreateTime = now
  4119. };
  4120. }).ToList();
  4121. // 开启事务执行双表更新
  4122. var result = await _sqlSugar.UseTranAsync(async () =>
  4123. {
  4124. // 插入本地资料库
  4125. await _sqlSugar.Insertable(rawLocalDatas).ExecuteCommandAsync();
  4126. // 更新 AI 主表的状态(全量覆盖 JSON 列)
  4127. await _sqlSugar.Updateable(dataInfo)
  4128. .UpdateColumns(x => x.AiCrawledDetails)
  4129. .ExecuteCommandAsync();
  4130. });
  4131. return Ok(result.IsSuccess
  4132. ? JsonView(true, "操作成功")
  4133. : JsonView(false, $"操作失败:{result.ErrorMessage}"));
  4134. }
  4135. /// <summary>
  4136. /// 商邀资料AI-无团组版 TokenHub AI查询资料(SSE流式推送)
  4137. /// </summary>
  4138. [HttpPost]
  4139. public async Task InvitationAI_NoGroupSearchStreamProgress([FromBody] InvitationAI_NoGroupSearchDto dto)
  4140. {
  4141. HttpContext.InitializeSse();
  4142. try
  4143. {
  4144. await HttpContext.SendSseStepAsync(5, "正在加载配置...");
  4145. #region 1. 异步并行化验证 (Performance Boost)
  4146. var invAiInfo = await _sqlSugar.Queryable<Res_InvitationAI_NoGroup>().Where(x => x.IsDel == 0 && x.Id == dto.Id).FirstAsync();
  4147. var operatorName = await _sqlSugar.Queryable<Sys_Users>()
  4148. .Where(x => x.IsDel == 0 && x.Id == dto.CurrUserId)
  4149. .Select(x => x.CnName).FirstAsync();
  4150. if (invAiInfo?.EntryInfo == null)
  4151. {
  4152. await HttpContext.SendSseStepAsync(-1, "未找到有效的关键字配置。");
  4153. return;
  4154. }
  4155. var entryInfo = invAiInfo.EntryInfo;
  4156. var targetCountrySet = new HashSet<string>(entryInfo.TargetCountry);
  4157. #endregion
  4158. await HttpContext.SendSseStepAsync(20, "正在解析本地典籍 (并行解密)...");
  4159. #region 2. 内存计算优化
  4160. // 仅查询必要字段,减少 DataReader 压力
  4161. var rawLocalDatas = await _sqlSugar.Queryable<Res_InvitationOfficialActivityData>()
  4162. .Where(x => x.IsDel == 0 && !string.IsNullOrEmpty(x.UnitName))
  4163. .Select(x => new { x.Country, x.UnitName, x.Address, x.Field, x.Contact, x.Tel, x.Email })
  4164. .ToListAsync();
  4165. // PLINQ 核心炼金:利用多核并行解密
  4166. var allDecrypted = rawLocalDatas.AsParallel().Select(item => new InvitationAI_NoGroupInfo
  4167. {
  4168. Guid = Guid.NewGuid().ToString("N"),
  4169. Source = 0, // 标识来源:本地
  4170. Region = AesEncryptionHelper.Decrypt(item.Country),
  4171. NameCn = AesEncryptionHelper.Decrypt(item.UnitName),
  4172. Address = AesEncryptionHelper.Decrypt(item.Address),
  4173. Scope = AesEncryptionHelper.Decrypt(item.Field),
  4174. Contact = AesEncryptionHelper.Decrypt(item.Contact),
  4175. Phone = AesEncryptionHelper.Decrypt(item.Tel),
  4176. Email = AesEncryptionHelper.Decrypt(item.Email),
  4177. OperatedAt = DateTime.Now,
  4178. Operator = operatorName,
  4179. }).ToList();
  4180. // 筛选符合国家的本地数据
  4181. var matchedCountries = allDecrypted.Where(x => targetCountrySet.Contains(x.Region)).ToList();
  4182. #endregion
  4183. #region 3. 混元 AI 协同炼金 (双阶段)
  4184. // --- 阶段 A: 行业匹配分析 ---
  4185. if (matchedCountries.Any())
  4186. {
  4187. await HttpContext.SendSseStepAsync(40, $"AI 正在执行 {entryInfo.TargetCountry.Count} 国的行业契合度分析...");
  4188. string industryQuestion = BuildIndustryPrompt(entryInfo, matchedCountries);
  4189. string industryRaw = string.Empty;
  4190. var reply = await _tokenHubService.ChatAsync(new ChatRequest()
  4191. {
  4192. Messages = new List<OASystem.API.OAMethodLib.TokenHubAI.Models.ChatMessage>()
  4193. {
  4194. new() { Content = industryQuestion }
  4195. }
  4196. });
  4197. if (reply.IsSuccess) industryRaw = reply?.Content ?? string.Empty;
  4198. else
  4199. {
  4200. await HttpContext.SendSseStepAsync(-1, $"TokenHub AI 查询资料失败: {reply?.ErrorMessage}");
  4201. }
  4202. var industryMatches = CleanAndParseJson<List<IndustryMatchResult>>(industryRaw) ?? new();
  4203. var matchedNames = new HashSet<string>(industryMatches.Select(x => x.TargetUnitName));
  4204. // 重新过滤:仅保留 AI 认为匹配的本地数据
  4205. matchedCountries = matchedCountries.Where(x => !string.IsNullOrEmpty(x.NameCn) && matchedNames.Contains(x.NameCn)).Distinct().ToList();
  4206. }
  4207. // --- 阶段 B: 差额补全 (Gap Filling) ---
  4208. var localInvDatas = new List<InvitationAI_NoGroupInfo>();
  4209. var aiTasks = new List<CountryAIPormptInfo>();
  4210. foreach (var country in entryInfo.TargetCountry)
  4211. {
  4212. var countryData = matchedCountries.Where(x => x.Region == country).Take(entryInfo.NeedCount).ToList();
  4213. localInvDatas.AddRange(countryData);
  4214. int gap = entryInfo.NeedCount - countryData.Count;
  4215. if (gap > 0) aiTasks.Add(new() { Country = country, Count = gap });
  4216. }
  4217. var hunyuanAIInvDatas = new List<InvitationAI_NoGroupInfo>();
  4218. if (aiTasks.Any())
  4219. {
  4220. await HttpContext.SendSseStepAsync(60, $"Snovio 正在跨境检索 {string.Join(", ", aiTasks.Select(x => $"{x.Country}({x.Count}条)"))} 单位资料...");
  4221. var snovioIndustries = SnovioIndustryMapper.MapToSnovio(entryInfo.Industries);
  4222. var snovioSize = ParseSnovioSize(entryInfo.ScaleTypes);
  4223. var snovioProgress = 60;
  4224. foreach (var task in aiTasks)
  4225. {
  4226. try
  4227. {
  4228. var countryEn = await TranslateCountryToEnglishAsync(task.Country, invAiInfo.InvName);
  4229. const int MaxSnovioPages = 5;
  4230. var collected = 0;
  4231. for (var page = 1; page <= MaxSnovioPages && collected < task.Count; page++)
  4232. {
  4233. var request = new SnovioCompanySearchRequest
  4234. {
  4235. Page = page,
  4236. Filters = new SnovioCompanySearchFilters
  4237. {
  4238. Company = new SnovioCompanyFilter
  4239. {
  4240. Industries = snovioIndustries.Any()
  4241. ? new SnovioStringListFilter { Include = snovioIndustries }
  4242. : null,
  4243. Size = string.IsNullOrWhiteSpace(snovioSize) ? null : new List<string> { snovioSize },
  4244. },
  4245. Locations = new SnovioLocationFilter
  4246. {
  4247. Include = new SnovioLocationFilterItem
  4248. {
  4249. Locality = task.Country,
  4250. LocationType = "country"
  4251. }
  4252. }
  4253. },
  4254. };
  4255. var result = await _snovioService.SearchCompaniesAsync(request);
  4256. var raw = result?.Data?.Companies ?? new List<SnovioCompanySearchCompany>();
  4257. if (!raw.Any()) break;
  4258. foreach (var c in raw)
  4259. {
  4260. if (collected >= task.Count) break;
  4261. if (!IsLocationMatchesCountry(c.Location, countryEn))
  4262. {
  4263. _logger.LogDebug("公务名称:{InvName};公司 location 与目标国不符,跳过;Name={Name};Location={Location};Country={Country}",
  4264. invAiInfo.InvName, c.Name, c.Location, task.Country);
  4265. continue;
  4266. }
  4267. var info = new InvitationAI_NoGroupInfo
  4268. {
  4269. Guid = Guid.NewGuid().ToString("N"),
  4270. Source = 1,
  4271. Region = task.Country,
  4272. Industry = c.Industry ?? entryInfo.Industries?.FirstOrDefault(),
  4273. NameCn = c.Name,
  4274. NameEn = c.Name,
  4275. Address = c.Location,
  4276. Scope = c.Industry,
  4277. SiteUrl = string.IsNullOrWhiteSpace(c.Domain) ? string.Empty : $"https://{c.Domain.Trim()}",
  4278. PostUrl = new List<PostNewsItem>(),
  4279. RecLevel = "Backup",
  4280. Operator = operatorName,
  4281. OperatedAt = DateTime.Now,
  4282. };
  4283. var hasEmail = await EnrichSnovioContactAsync(c.Domain, info);
  4284. if (!hasEmail)
  4285. {
  4286. _logger.LogWarning("公务名称:{InvName};公司无可用邮箱,跳过;Name={Name}", invAiInfo.InvName, c.Name);
  4287. continue;
  4288. }
  4289. hunyuanAIInvDatas.Add(info);
  4290. collected++;
  4291. snovioProgress = Math.Min(snovioProgress + 1, 79);
  4292. await HttpContext.SendSseStepAsync(snovioProgress, $"正在检索{task.Country}第{collected}名潜客...");
  4293. }
  4294. if (page >= (result?.Data?.TotalPages ?? page)) break;
  4295. }
  4296. }
  4297. catch (Exception ex)
  4298. {
  4299. _logger.LogWarning(ex, "公务名称:{InvName};Snovio 检索失败;Country={Country}", invAiInfo.InvName, task.Country);
  4300. }
  4301. }
  4302. if (hunyuanAIInvDatas.Any())
  4303. {
  4304. await HttpContext.SendSseStepAsync(80,
  4305. $"Snovio 已完成跨境检索,共获取 {hunyuanAIInvDatas.Count} 条单位资料");
  4306. }
  4307. else
  4308. {
  4309. await HttpContext.SendSseStepAsync(80, "本次未检索到符合条件的境外单位,建议调整国家或行业范围。");
  4310. }
  4311. }
  4312. #endregion
  4313. await HttpContext.SendSseStepAsync(90, "正在同步成果至持久化仓库...");
  4314. #region 4. 数据融合与更新
  4315. var finalResult = localInvDatas.Concat(hunyuanAIInvDatas).ToList();
  4316. // 仅更新指定列,性能更优
  4317. invAiInfo.AiCrawledDetails = finalResult.OrderByDescending(x => x.OperatedAt).ToList();
  4318. bool isOk = await _sqlSugar.Updateable(invAiInfo)
  4319. .UpdateColumns(x => x.AiCrawledDetails)
  4320. .ExecuteCommandHasChangeAsync();
  4321. if (!isOk) await HttpContext.SendSseStepAsync(-1, $"数据持久化失败。");
  4322. #endregion
  4323. // 5. 终焉推送
  4324. string finalMsg = "操作成功!资料已全部就绪。";
  4325. if (!finalResult.Any())
  4326. {
  4327. finalMsg = "本次未检索到符合条件的境外单位,建议调整国家或行业范围。";
  4328. }
  4329. await HttpContext.SendSseStepAsync(100, finalMsg, new
  4330. {
  4331. invAiInfo.Id,
  4332. AiCrawledDetails = finalResult.OrderByDescending(x => x.Source).ThenBy(x => x.Region).ToList()
  4333. });
  4334. }
  4335. catch (TokenHubException ex)
  4336. {
  4337. await HttpContext.SendSseStepAsync(-1, $"TokenHub AI 调用失败: {ex.Message}");
  4338. }
  4339. catch (Exception ex)
  4340. {
  4341. _logger.LogError(ex, "SSE 管道熔断");
  4342. await HttpContext.SendSseStepAsync(-1, $"SSE 错误:{ex.InnerException.Message}({ex.Message})");
  4343. }
  4344. finally
  4345. {
  4346. await HttpContext.FinalizeSseAsync();
  4347. }
  4348. }
  4349. /// <summary>
  4350. /// 商邀资料AI-无团组版 TokenHub AI续写(SSE流式推送)
  4351. /// </summary>
  4352. /// <param name="dto"></param>
  4353. /// <returns></returns>
  4354. [HttpPost]
  4355. public async Task InvitationAI_NoGroupCompleteTextStream([FromBody] InvitationAI_NoGroupCompleteTextDto dto)
  4356. {
  4357. HttpContext.InitializeSse();
  4358. try
  4359. {
  4360. // 初始化检查 (Progress: 5%)
  4361. var invAiInfo = await _sqlSugar.Queryable<Res_InvitationAI_NoGroup>().Where(x => x.Id == dto.Id).FirstAsync();
  4362. if (invAiInfo?.EntryInfo == null)
  4363. {
  4364. await HttpContext.SendSseStepAsync(-1, "请先设置关键字信息!");
  4365. return;
  4366. }
  4367. await HttpContext.SendSseStepAsync(10, "任务初始化完成,正在计算补齐缺口...");
  4368. // 任务拆解逻辑
  4369. var entryInfo = invAiInfo.EntryInfo;
  4370. var aiTasks = new List<CountryAIPormptInfo>();
  4371. int targetPerCountry = entryInfo.NeedCount;
  4372. foreach (var countryName in entryInfo.TargetCountry)
  4373. {
  4374. var countryDataCount = invAiInfo.AiCrawledDetails.Count(x => x.Region == countryName);
  4375. int aiNeedCount = targetPerCountry - countryDataCount;
  4376. if (aiNeedCount > 0) aiTasks.Add(new() { Country = countryName, Count = aiNeedCount });
  4377. }
  4378. if (!aiTasks.Any())
  4379. {
  4380. await HttpContext.SendSseStepAsync(100, "数据已满额,无需续写", invAiInfo.AiCrawledDetails);
  4381. return;
  4382. }
  4383. var existingNames = new HashSet<string>(
  4384. invAiInfo.AiCrawledDetails.Where(x => x.Source == 1 && !string.IsNullOrWhiteSpace(x.NameCn)).Select(x => x.NameCn),
  4385. StringComparer.OrdinalIgnoreCase);
  4386. await HttpContext.SendSseStepAsync(60, $"Snovio 正在跨境检索 {string.Join(", ", aiTasks.Select(x => $"{x.Country}({x.Count}条)"))} 单位资料...");
  4387. var snovioIndustries = SnovioIndustryMapper.MapToSnovio(entryInfo.Industries);
  4388. var snovioSize = ParseSnovioSize(entryInfo.ScaleTypes);
  4389. var operatorName = await _sqlSugar.Queryable<Sys_Users>()
  4390. .Where(x => x.Id == dto.CurrUserId)
  4391. .Select(x => x.CnName)
  4392. .FirstAsync() ?? "-";
  4393. var hunyuanAIInvDatas = new List<InvitationAI_NoGroupInfo>();
  4394. var snovioProgress = 60;
  4395. foreach (var task in aiTasks)
  4396. {
  4397. try
  4398. {
  4399. var countryEn = await TranslateCountryToEnglishAsync(task.Country, invAiInfo.InvName);
  4400. const int MaxSnovioPages = 5;
  4401. var collected = 0;
  4402. for (var page = 1; page <= MaxSnovioPages && collected < task.Count; page++)
  4403. {
  4404. var request = new SnovioCompanySearchRequest
  4405. {
  4406. Page = page,
  4407. Filters = new SnovioCompanySearchFilters
  4408. {
  4409. Company = new SnovioCompanyFilter
  4410. {
  4411. Industries = snovioIndustries.Any()
  4412. ? new SnovioStringListFilter { Include = snovioIndustries }
  4413. : null,
  4414. Size = string.IsNullOrWhiteSpace(snovioSize) ? null : new List<string> { snovioSize }
  4415. },
  4416. Locations = new SnovioLocationFilter
  4417. {
  4418. Include = new SnovioLocationFilterItem
  4419. {
  4420. Locality = task.Country,
  4421. LocationType = "country"
  4422. }
  4423. }
  4424. },
  4425. };
  4426. var result = await _snovioService.SearchCompaniesAsync(request);
  4427. var raw = result?.Data?.Companies ?? new List<SnovioCompanySearchCompany>();
  4428. if (!raw.Any()) break;
  4429. foreach (var c in raw)
  4430. {
  4431. if (collected >= task.Count) break;
  4432. if (string.IsNullOrWhiteSpace(c.Name) || existingNames.Contains(c.Name)) continue;
  4433. if (!IsLocationMatchesCountry(c.Location, countryEn))
  4434. {
  4435. _logger.LogDebug("公务名称:{InvName};公司 location 与目标国不符,跳过;Name={Name};Location={Location};Country={Country}",
  4436. invAiInfo.InvName, c.Name, c.Location, task.Country);
  4437. continue;
  4438. }
  4439. var info = new InvitationAI_NoGroupInfo
  4440. {
  4441. Guid = Guid.NewGuid().ToString("N"),
  4442. Source = 1,
  4443. Region = task.Country,
  4444. Industry = entryInfo.Industries?.FirstOrDefault() ?? c.Industry,
  4445. NameCn = c.Name,
  4446. NameEn = c.Name,
  4447. Address = c.Location,
  4448. Scope = c.Industry,
  4449. SiteUrl = string.IsNullOrWhiteSpace(c.Domain) ? string.Empty : $"https://{c.Domain.Trim()}",
  4450. PostUrl = new List<PostNewsItem>(),
  4451. RecLevel = "Backup",
  4452. Operator = operatorName,
  4453. OperatedAt = DateTime.Now,
  4454. };
  4455. var hasEmail = await EnrichSnovioContactAsync(c.Domain, info);
  4456. if (!hasEmail)
  4457. {
  4458. _logger.LogWarning("公务名称:{InvName};公司无可用邮箱,跳过;Name={Name}", invAiInfo.InvName, c.Name);
  4459. continue;
  4460. }
  4461. hunyuanAIInvDatas.Add(info);
  4462. existingNames.Add(c.Name);
  4463. collected++;
  4464. snovioProgress = Math.Min(snovioProgress + 1, 79);
  4465. await HttpContext.SendSseStepAsync(snovioProgress, $"正在检索{task.Country}第{collected}名潜客...");
  4466. }
  4467. if (page >= (result?.Data?.TotalPages ?? page)) break;
  4468. }
  4469. }
  4470. catch (Exception ex)
  4471. {
  4472. _logger.LogWarning(ex, "公务名称:{InvName};Snovio 续写失败;Country={Country}", invAiInfo.InvName, task.Country);
  4473. }
  4474. }
  4475. await HttpContext.SendSseStepAsync(80, $"Snovio 已完成跨境检索,共获取 {hunyuanAIInvDatas.Count} 条单位资料");
  4476. if (!hunyuanAIInvDatas.Any())
  4477. {
  4478. await HttpContext.SendSseStepAsync(-1, "本次未检索到符合条件的境外单位,建议调整国家或行业范围。");
  4479. return;
  4480. }
  4481. invAiInfo.AiCrawledDetails ??= new();
  4482. invAiInfo.AiCrawledDetails.AddRange(hunyuanAIInvDatas);
  4483. invAiInfo.AiCrawledDetails =
  4484. invAiInfo.AiCrawledDetails.OrderByDescending(x => x.OperatedAt).ToList();
  4485. var updated = await _sqlSugar.Updateable(invAiInfo)
  4486. .UpdateColumns(x => x.AiCrawledDetails)
  4487. .ExecuteCommandAsync();
  4488. if (updated <= 0)
  4489. {
  4490. await HttpContext.SendSseStepAsync(-1, "数据库更新失败,请联系管理员");
  4491. return;
  4492. }
  4493. await HttpContext.SendSseStepAsync(100,
  4494. $"AI 续写成功!新增 {hunyuanAIInvDatas.Count} 条数据",
  4495. invAiInfo.AiCrawledDetails);
  4496. }
  4497. catch (TokenHubException ex)
  4498. {
  4499. await HttpContext.SendSseStepAsync(-1, $"TokenHub AI 调用失败: {ex.Message}");
  4500. }
  4501. catch (Exception ex)
  4502. {
  4503. _logger.LogError(ex, "SSE 续写异常");
  4504. await HttpContext.SendSseStepAsync(-1, $"炼金炸炉:{ex.Message}");
  4505. }
  4506. finally
  4507. {
  4508. await HttpContext.FinalizeSseAsync();
  4509. }
  4510. }
  4511. /// <summary>
  4512. /// 商邀资料AI-无团组版
  4513. /// 文件生成(基于TokenHub AI已爬取数据进行格式化输出,供用户下载使用)
  4514. /// </summary>
  4515. /// <returns></returns>
  4516. [HttpGet("{id}")]
  4517. [ProducesResponseType(typeof(JsonView), StatusCodes.Status200OK)]
  4518. public async Task<IActionResult> InvitationAI_NoGroupFileGenerator(int id)
  4519. {
  4520. var invAiInfo = await _sqlSugar.Queryable<Res_InvitationAI_NoGroup>()
  4521. .Where(x => x.Id == id)
  4522. .FirstAsync();
  4523. var dataList = invAiInfo?.AiCrawledDetails;
  4524. if (dataList == null)
  4525. return Ok(JsonView(false, "数据为空"));
  4526. Document doc = new Document();
  4527. DocumentBuilder builder = new DocumentBuilder(doc);
  4528. // A4页面设置
  4529. double usableWidth = 495;
  4530. foreach (Aspose.Words.Section sec in doc.Sections)
  4531. {
  4532. sec.PageSetup.PaperSize = Aspose.Words.PaperSize.A4;
  4533. sec.PageSetup.LeftMargin = 50;
  4534. sec.PageSetup.RightMargin = 50;
  4535. sec.PageSetup.TopMargin = 50;
  4536. sec.PageSetup.BottomMargin = 50;
  4537. }
  4538. // 卡片背景
  4539. Color cardBg = Color.FromArgb(252, 252, 252);
  4540. string fontName = "微软雅黑";
  4541. int count = 1;
  4542. foreach (var item in dataList)
  4543. {
  4544. // 标题
  4545. builder.ParagraphFormat.ClearFormatting();
  4546. builder.Font.ClearFormatting();
  4547. builder.Font.Name = fontName;
  4548. builder.Font.Size = 16;
  4549. builder.Font.Bold = true;
  4550. builder.Font.Color = Color.Black;
  4551. builder.Write($"{count++}. {item.NameCn}");
  4552. builder.InsertParagraph();
  4553. // 英文名
  4554. builder.Font.Size = 10;
  4555. builder.Font.Bold = false;
  4556. builder.Font.Color = Color.FromArgb(120, 120, 120);
  4557. builder.Write(item.NameEn ?? "");
  4558. builder.InsertParagraph();
  4559. builder.InsertParagraph();
  4560. // ====== 卡片表格 ======
  4561. Aspose.Words.Tables.Table table = builder.StartTable();
  4562. builder.CellFormat.Shading.BackgroundPatternColor = cardBg;
  4563. builder.CellFormat.VerticalAlignment = CellVerticalAlignment.Center;
  4564. builder.CellFormat.WrapText = true;
  4565. builder.CellFormat.LeftPadding = 12;
  4566. builder.CellFormat.RightPadding = 12;
  4567. builder.CellFormat.TopPadding = 8;
  4568. builder.CellFormat.BottomPadding = 8;
  4569. double labelW = 120;
  4570. AddSingleRow(builder, fontName, labelW, usableWidth - labelW, "详细地址", item.Address);
  4571. AddSingleRow(builder, fontName, labelW, usableWidth - labelW, "联系人", item.Contact);
  4572. AddSingleRow(builder, fontName, labelW, usableWidth - labelW, "联系电话", item.Phone);
  4573. AddSingleRow(builder, fontName, labelW, usableWidth - labelW, "电子邮箱", item.Email);
  4574. AddSingleLinkRow(builder, fontName, labelW, usableWidth - labelW, "官方网站", item.SiteUrl);
  4575. AddSingleLinkRow(builder, fontName, labelW, usableWidth - labelW, "最近三年动态", string.Join(", ", item.PostUrl.Select(p => p.Url)));
  4576. AddFullWidthRow(builder, fontName, labelW, usableWidth - labelW, "推荐等级", item.RecLevel);
  4577. AddFullWidthRow(builder, fontName, labelW, usableWidth - labelW, "经营范围", item.Scope);
  4578. AddFullWidthRow(builder, fontName, labelW, usableWidth - labelW, "对接建议", item.IntgAdvice);
  4579. // 表格样式
  4580. table.SetBorders(Aspose.Words.LineStyle.Single, 0.5, Color.FromArgb(230, 230, 230));
  4581. table.AutoFit(AutoFitBehavior.FixedColumnWidths);
  4582. table.AllowAutoFit = false;
  4583. table.PreferredWidth = PreferredWidth.FromPoints(usableWidth);
  4584. builder.EndTable();
  4585. builder.InsertParagraph();
  4586. builder.InsertParagraph();
  4587. }
  4588. // 保存逻辑
  4589. string fileName = $"{invAiInfo.InvName}(无团组版)_Professional.docx";
  4590. string filePath = Path.Combine(AppSettingsHelper.Get("InvitationAIAssistBasePath"), fileName);
  4591. doc.Save(filePath, Aspose.Words.SaveFormat.Docx);
  4592. return Ok(JsonView(true, "生成成功", new { Url = $"{AppSettingsHelper.Get("WordBaseUrl")}/{AppSettingsHelper.Get("InvitationAIAssistFtpPath")}/{fileName}" }));
  4593. }
  4594. /// <summary>
  4595. /// 商邀资料AI-无团组版
  4596. /// 生成邮件(SSE流式推送)
  4597. /// </summary>
  4598. /// <returns></returns>
  4599. [HttpPost]
  4600. public async Task InvitationAI_NoGroupGenerateEmailStream([FromBody] InvitationAI_NoGroupGenerateEmailDto dto)
  4601. {
  4602. HttpContext.InitializeSse();
  4603. try
  4604. {
  4605. // 1. 基础校验与资源加载 (Progress: 5%)
  4606. if (dto.Id < 1 || dto.Guids == null || !dto.Guids.Any())
  4607. {
  4608. await HttpContext.SendSseStepAsync(-1, "请求参数不完整,请选择单位");
  4609. return;
  4610. }
  4611. var invAiInfo = await _sqlSugar.Queryable<Res_InvitationAI_NoGroup>().Where(x => x.Id == dto.Id).FirstAsync();
  4612. if (invAiInfo?.AiCrawledDetails == null)
  4613. {
  4614. await HttpContext.SendSseStepAsync(-1, "基础商邀资料不存在");
  4615. return;
  4616. }
  4617. await HttpContext.SendSseStepAsync(10, "AI 正在分析考察团组背景与访问意图...");
  4618. // 2. 准备 AI 上下文数据 (Progress: 15%)
  4619. var clientInfoSources = invAiInfo.AiCrawledDetails.Where(x => dto.Guids.Contains(x.Guid)).ToList();
  4620. var clientInfosForAI = clientInfoSources.Select(x => new AICreateEmailInfo()
  4621. {
  4622. Guid = x.Guid,
  4623. NameCn = x.NameCn,
  4624. Scope = x.Scope
  4625. }).ToList();
  4626. string operatorName = await _sqlSugar.Queryable<Sys_Users>()
  4627. .Where(x => x.Id == dto.CurrUserId).Select(x => x.CnName).FirstAsync() ?? "-";
  4628. // 3. 构建 Prompt 并调用 AI (Progress: 25%)
  4629. await HttpContext.SendSseStepAsync(30, $"AI 正在为 {clientInfosForAI.Count} 家单位撰写定制化正式邮件...");
  4630. // 3.1 构建 Prompt
  4631. string pormpt = BuildHunyuanEmailPrompt(
  4632. invAiInfo.EntryInfo?.OriginUnit ?? "",
  4633. invAiInfo.EntryInfo?.Objective ?? "",
  4634. invAiInfo.EntryInfo?.VisitDate ?? "",
  4635. clientInfosForAI
  4636. );
  4637. // 调用 AI (此处为阻塞式等待 AI 结果,若混元支持 Stream 可进一步拆解)
  4638. string aiResponse = string.Empty;
  4639. var reply = await _tokenHubService.ChatAsync(new ChatRequest()
  4640. {
  4641. Messages = new List<OASystem.API.OAMethodLib.TokenHubAI.Models.ChatMessage>()
  4642. {
  4643. new() { Content = pormpt }
  4644. }
  4645. });
  4646. if (reply.IsSuccess) aiResponse = reply?.Content ?? string.Empty;
  4647. else
  4648. {
  4649. await HttpContext.SendSseStepAsync(-1, $"TokenHub AI 续写资料失败: {reply?.ErrorMessage}");
  4650. }
  4651. await HttpContext.SendSseStepAsync(80, "邮件初稿已生成,正在进行 HTML 格式校验与转义处理...");
  4652. // 4. 解析结果 (Progress: 85%)
  4653. var hunyuanAIEmailDatas = new List<AICreateEmailInfo>();
  4654. if (!string.IsNullOrWhiteSpace(aiResponse))
  4655. {
  4656. // 预处理:过滤 AI 可能返回的 Markdown 标记
  4657. string cleanJson = aiResponse.Trim();
  4658. if (cleanJson.StartsWith("```json"))
  4659. cleanJson = cleanJson.Substring(7, cleanJson.Length - 10).Trim();
  4660. else if (cleanJson.StartsWith("```"))
  4661. cleanJson = cleanJson.Substring(3, cleanJson.Length - 6).Trim();
  4662. try
  4663. {
  4664. // 解析并注入 Source 标识
  4665. hunyuanAIEmailDatas = JsonConvert.DeserializeObject<List<AICreateEmailInfo>>(cleanJson);
  4666. }
  4667. catch (JsonException ex)
  4668. {
  4669. // 记录日志并考虑 fallback 策略
  4670. _logger.LogError(ex, "Hunyuan AI 响应解析失败。原始数据:{Response}", aiResponse);
  4671. }
  4672. }
  4673. if (hunyuanAIEmailDatas == null || !hunyuanAIEmailDatas.Any())
  4674. {
  4675. await HttpContext.SendSseStepAsync(-1, "AI 格式解析失败,请尝试重新生成");
  4676. return;
  4677. }
  4678. // 5. 更新数据模型 (Progress: 90%)
  4679. foreach (var client in clientInfoSources)
  4680. {
  4681. var aiEmail = hunyuanAIEmailDatas.FirstOrDefault(x => x.Guid == client.Guid);
  4682. if (aiEmail != null)
  4683. {
  4684. client.EmailInfo.Status = 2; // 已生成
  4685. client.EmailInfo.EmailTitle = aiEmail.Subject;
  4686. client.EmailInfo.EmailContent = aiEmail.Content;
  4687. client.EmailInfo.Operator = operatorName;
  4688. client.EmailInfo.OperatedAt = DateTime.Now;
  4689. }
  4690. }
  4691. // 6. 数据库持久化 (Progress: 95%)
  4692. // 采用增量更新策略,避免直接 Where 过滤掉其他未选中的数据
  4693. var update = await _sqlSugar.Updateable(invAiInfo).UpdateColumns(x => x.AiCrawledDetails).ExecuteCommandAsync();
  4694. if (update > 0)
  4695. {
  4696. await HttpContext.SendSseStepAsync(100, "邮件全部生成完毕并已存入团组资料库", invAiInfo.AiCrawledDetails);
  4697. }
  4698. else
  4699. {
  4700. await HttpContext.SendSseStepAsync(-1, "数据库写入失败,请联系管理员");
  4701. }
  4702. }
  4703. catch (TokenHubException ex)
  4704. {
  4705. await HttpContext.SendSseStepAsync(-1, $"TokenHub AI 调用失败: {ex.Message}");
  4706. }
  4707. catch (Exception ex)
  4708. {
  4709. await HttpContext.SendSseStepAsync(-1, $"生成失败:{ex.Message}");
  4710. }
  4711. finally
  4712. {
  4713. await HttpContext.FinalizeSseAsync();
  4714. }
  4715. }
  4716. /// <summary>
  4717. /// 商邀资料AI-无团组版
  4718. /// 邮件保存
  4719. /// </summary>
  4720. /// <returns></returns>
  4721. [HttpPost]
  4722. [ProducesResponseType(typeof(JsonView), StatusCodes.Status200OK)]
  4723. public async Task<IActionResult> InvitationAI_NoGroupEmailSave(InvitationAI_NoGroupEmailSaveDto dto)
  4724. {
  4725. if (dto.Id < 1) return Ok(JsonView(false, "请选择保存的邮件资料"));
  4726. if (dto.CurrUserId < 1) return Ok(JsonView(false, "请传入用户Id"));
  4727. if (string.IsNullOrEmpty(dto.Guid)) return Ok(JsonView(false, "请传入Guid"));
  4728. if (string.IsNullOrEmpty(dto.EmailTitle)) return Ok(JsonView(false, "请传入邮件标题"));
  4729. if (string.IsNullOrEmpty(dto.EmailContent)) return Ok(JsonView(false, "请传入邮件内容"));
  4730. var invAiInfo = await _sqlSugar.Queryable<Res_InvitationAI_NoGroup>()
  4731. .Where(x => x.Id == dto.Id)
  4732. .FirstAsync();
  4733. if (invAiInfo == null) return Ok(JsonView(false, "数据信息为空"));
  4734. var dataList = invAiInfo?.AiCrawledDetails;
  4735. if (dataList == null) return Ok(JsonView(false, "邀请方数据信息为空"));
  4736. var editInfo = dataList.Where(x => x.Guid == dto.Guid).FirstOrDefault();
  4737. if (editInfo == null) return Ok(JsonView(false, "邀请方信息为空"));
  4738. var opUserName = await _sqlSugar.Queryable<Sys_Users>().Where(x => x.IsDel == 0 && x.Id == dto.CurrUserId).Select(x => x.CnName).FirstAsync() ?? "-";
  4739. editInfo.EmailInfo.EmailTitle = dto.EmailTitle;
  4740. editInfo.EmailInfo.EmailContent = dto.EmailContent;
  4741. editInfo.EmailInfo.Operator = opUserName;
  4742. editInfo.EmailInfo.OperatedAt = DateTime.Now;
  4743. var datas = dataList.Where(x => x.Guid != dto.Guid).ToList();
  4744. datas.Add(editInfo);
  4745. // 排序
  4746. invAiInfo.AiCrawledDetails = datas.OrderByDescending(x => x.OperatedAt).ToList();
  4747. var editUpd = await _sqlSugar.Updateable(invAiInfo).UpdateColumns(x => x.AiCrawledDetails).ExecuteCommandAsync();
  4748. if (editUpd < 1)
  4749. {
  4750. return Ok(JsonView(true, "邮件保存失败"));
  4751. }
  4752. return Ok(JsonView(true, "邮件保存成功"));
  4753. }
  4754. /// <summary>
  4755. /// 商邀资料AI-无团组版 邮箱附件上传
  4756. /// </summary>
  4757. [HttpPost]
  4758. [ProducesResponseType(typeof(JsonView), StatusCodes.Status200OK)]
  4759. public async Task<IActionResult> InvitationAI_NoGroupFileSave([FromForm] InvitationAI_NoGroupFileSaveDto dto)
  4760. {
  4761. // 1. 炼金前置:严格参数校验
  4762. if (dto.Id < 1) return Ok(JsonView(false, "请选择保存的公务资料"));
  4763. if (dto.CurrUserId < 1) return Ok(JsonView(false, "请传入用户Id"));
  4764. if (string.IsNullOrEmpty(dto.Guid)) return Ok(JsonView(false, "请传入Guid"));
  4765. if (dto.Attachments == null || !dto.Attachments.Any()) return Ok(JsonView(false, "请传入附件"));
  4766. // 2. 数据获取:利用 SqlSugar 异步查询
  4767. var invAiInfo = await _sqlSugar.Queryable<Res_InvitationAI_NoGroup>()
  4768. .InSingleAsync(dto.Id);
  4769. if (invAiInfo?.AiCrawledDetails == null) return Ok(JsonView(false, "邀请方信息不存在"));
  4770. // 3. 定位到具体需要编辑的 Guid 记录
  4771. var editInfo = invAiInfo.AiCrawledDetails.FirstOrDefault(x => x.Guid == dto.Guid);
  4772. if (editInfo == null) return Ok(JsonView(false, "未找到匹配的 Guid 记录"));
  4773. // 4. 追踪信息更新
  4774. var opUserName = await _sqlSugar.Queryable<Sys_Users>()
  4775. .Where(x => x.Id == dto.CurrUserId && x.IsDel == 0)
  4776. .Select(x => x.CnName)
  4777. .FirstAsync() ?? "-";
  4778. // 更新操作人与时间 (利用引用类型特性)
  4779. editInfo.Operator = opUserName;
  4780. editInfo.OperatedAt = DateTime.Now;
  4781. // 初始化 EmailInfo 确保不为 null
  4782. editInfo.EmailInfo ??= new EmailInfo();
  4783. editInfo.EmailInfo.Operator = opUserName;
  4784. editInfo.EmailInfo.OperatedAt = DateTime.Now;
  4785. editInfo.EmailInfo.AttachmentPaths ??= new List<string>();
  4786. // 5. 构建绝对路径与相对路径
  4787. string dirName = $"{editInfo.NameEn?.Trim() ?? "Default"}_{dto.Guid}";
  4788. string baseDir = AppSettingsHelper.Get("InvitationAIAssistBasePath");
  4789. string ftpBase = AppSettingsHelper.Get("InvitationAIAssistFtpPath");
  4790. string absolutePath = Path.Combine(baseDir, dirName);
  4791. string relativePathPrefix = $"{ftpBase}{dirName}/";
  4792. // 核心修复:确保父级目录递归创建,防止 DirectoryNotFoundException
  4793. if (!Directory.Exists(absolutePath))
  4794. {
  4795. Directory.CreateDirectory(absolutePath);
  4796. }
  4797. var newSavedRelativePaths = new List<string>();
  4798. // 6. 持续文件存储与实时验证
  4799. foreach (var file in dto.Attachments)
  4800. {
  4801. // 文件名安全过滤
  4802. string safeName = string.Join("_", file.FileName.Split(Path.GetInvalidFileNameChars()));
  4803. string fullPath = Path.Combine(absolutePath, safeName);
  4804. // 重名冲突处理:文件名(n).ext
  4805. if (System.IO.File.Exists(fullPath))
  4806. {
  4807. string fileNameOnly = Path.GetFileNameWithoutExtension(safeName);
  4808. string extension = Path.GetExtension(safeName);
  4809. int count = 1;
  4810. while (System.IO.File.Exists(fullPath))
  4811. {
  4812. safeName = $"{fileNameOnly}({count++}){extension}";
  4813. fullPath = Path.Combine(absolutePath, safeName);
  4814. }
  4815. }
  4816. try
  4817. {
  4818. // 写入流:使用作用域隔离,确保退出 using 时立即释放句柄
  4819. using (var stream = new FileStream(fullPath, FileMode.Create, FileAccess.Write, FileShare.None))
  4820. {
  4821. await file.CopyToAsync(stream);
  4822. await stream.FlushAsync(); // 强制落盘
  4823. }
  4824. // 实时验证:物理验证 + 逻辑校验
  4825. var fileInfo = new FileInfo(fullPath);
  4826. if (fileInfo.Exists && fileInfo.Length == file.Length)
  4827. {
  4828. newSavedRelativePaths.Add($"{relativePathPrefix}{safeName}");
  4829. }
  4830. }
  4831. catch (Exception ex)
  4832. {
  4833. // 发生 IO 异常时清理残余文件并抛出,触发外层处理
  4834. if (System.IO.File.Exists(fullPath)) System.IO.File.Delete(fullPath);
  4835. // 此处记录日志:
  4836. _logger.LogError(ex, "File Save Error");
  4837. continue; // 跳过失败的文件,继续下一个
  4838. }
  4839. }
  4840. if (!newSavedRelativePaths.Any()) return Ok(JsonView(false, "所有文件上传均失败"));
  4841. // 7. 数据合并与持久化:
  4842. // 将新路径与旧路径合并,并去重
  4843. var updatedPaths = editInfo.EmailInfo.AttachmentPaths;
  4844. updatedPaths.AddRange(newSavedRelativePaths);
  4845. editInfo.EmailInfo.AttachmentPaths = updatedPaths.Distinct().ToList();
  4846. // 重新排序整体列表
  4847. invAiInfo.AiCrawledDetails = invAiInfo.AiCrawledDetails
  4848. .OrderByDescending(x => x.OperatedAt)
  4849. .ToList();
  4850. // SqlSugar 高效部分列更新
  4851. var isSuccess = await _sqlSugar.Updateable(invAiInfo)
  4852. .UpdateColumns(x => x.AiCrawledDetails)
  4853. .ExecuteCommandAsync() > 0;
  4854. if (!isSuccess) return Ok(JsonView(false, "数据库更新失败"));
  4855. // 8. 返回前端数据 (支持 lowerCamelCase)
  4856. var officeBaseUrl = AppSettingsHelper.Get("OfficeBaseUrl");
  4857. var finalResultUrls = editInfo.EmailInfo.AttachmentPaths
  4858. .Select(path => $"{officeBaseUrl}{path}")
  4859. .ToList();
  4860. return Ok(JsonView(true, "邮件附件保存成功", finalResultUrls));
  4861. }
  4862. /// <summary>
  4863. /// 商邀资料AI-无团组版 邮箱附件删除
  4864. /// </summary>
  4865. [HttpPost]
  4866. [ProducesResponseType(typeof(JsonView), StatusCodes.Status200OK)]
  4867. public async Task<IActionResult> InvitationAI_NoGroupFileDel([FromBody] InvitationAI_NoGroupFileDelDto dto)
  4868. {
  4869. // 1. 基础校验
  4870. if (dto.Id < 1) return Ok(JsonView(false, "请选择要删除的邮件"));
  4871. if (string.IsNullOrEmpty(dto.Guid)) return Ok(JsonView(false, "请传入Guid"));
  4872. if (dto.AttachmentNames == null || !dto.AttachmentNames.Any()) return Ok(JsonView(false, "请传入待删除附件名称"));
  4873. // 2. 数据获取
  4874. var invAiInfo = await _sqlSugar.Queryable<Res_InvitationAI_NoGroup>().InSingleAsync(dto.Id);
  4875. if (invAiInfo?.AiCrawledDetails == null) return Ok(JsonView(false, "业务数据不存在"));
  4876. var editInfo = invAiInfo.AiCrawledDetails.FirstOrDefault(x => x.Guid == dto.Guid);
  4877. if (editInfo?.EmailInfo?.AttachmentPaths == null) return Ok(JsonView(false, "未找到对应的附件记录"));
  4878. // 3. 操作人追踪
  4879. var opUserName = await _sqlSugar.Queryable<Sys_Users>()
  4880. .Where(x => x.Id == dto.CurrUserId && x.IsDel == 0)
  4881. .Select(x => x.CnName).FirstAsync() ?? "System";
  4882. // 更新追踪状态
  4883. editInfo.Operator = editInfo.EmailInfo.Operator = opUserName;
  4884. editInfo.OperatedAt = editInfo.EmailInfo.OperatedAt = DateTime.Now;
  4885. // 4. 路径
  4886. string dirName = $"{editInfo.NameEn?.Trim() ?? "Default"}_{dto.Guid}";
  4887. string absoluteDir = Path.Combine(AppSettingsHelper.Get("InvitationAIAssistBasePath"), dirName);
  4888. // 获取当前数据库中的相对路径列表
  4889. var currentPaths = editInfo.EmailInfo.AttachmentPaths;
  4890. bool isChanged = false;
  4891. // 5. 核心删除逻辑
  4892. foreach (var fileName in dto.AttachmentNames)
  4893. {
  4894. // 查找数据库中是否存在包含该文件名的路径 (忽略大小写比较)
  4895. var targetRelativePath = currentPaths.FirstOrDefault(p => p.EndsWith("/" + fileName) || p.Equals(fileName));
  4896. if (targetRelativePath != null)
  4897. {
  4898. // A. 从数据库记录中移除
  4899. currentPaths.Remove(targetRelativePath);
  4900. isChanged = true;
  4901. // B. 物理文件删除逻辑
  4902. // 注意:fullPath 必须是 [基础路径] + [目录名] + [纯文件名]
  4903. string fullPath = Path.Combine(absoluteDir, fileName);
  4904. try
  4905. {
  4906. if (System.IO.File.Exists(fullPath))
  4907. {
  4908. System.IO.File.Delete(fullPath);
  4909. }
  4910. }
  4911. catch (Exception ex)
  4912. {
  4913. // 记录 IO 异常但不中断流程
  4914. _logger.LogWarning($"物理文件删除失败: {fullPath}, {ex.Message}");
  4915. }
  4916. }
  4917. }
  4918. if (!isChanged) return Ok(JsonView(false, "未找到匹配的可删除附件"));
  4919. // 6. 数据同步与持久化
  4920. editInfo.EmailInfo.AttachmentPaths = currentPaths;
  4921. // 排序 (利用引用类型,无需手动 Add/Remove)
  4922. invAiInfo.AiCrawledDetails = invAiInfo.AiCrawledDetails.OrderByDescending(x => x.OperatedAt).ToList();
  4923. var isUpd = await _sqlSugar.Updateable(invAiInfo)
  4924. .UpdateColumns(x => x.AiCrawledDetails)
  4925. .ExecuteCommandAsync() > 0;
  4926. if (!isUpd) return Ok(JsonView(false, "数据库记录更新失败"));
  4927. // 7. 返回剩余附件的完整访问地址
  4928. var officeBaseUrl = AppSettingsHelper.Get("OfficeBaseUrl");
  4929. var remainingUrls = editInfo.EmailInfo.AttachmentPaths
  4930. .Select(path => $"{officeBaseUrl}{path}")
  4931. .ToList();
  4932. return Ok(JsonView(true, "附件删除成功", remainingUrls));
  4933. }
  4934. /// <summary>
  4935. /// 商邀资料AI-无团组版 发送邮件(SSE 流式推送)
  4936. /// </summary>
  4937. [HttpPost]
  4938. public async Task InvitationAI_NoGroupSeedEmailStream([FromBody] InvitationAI_NoGroupSeedEmailDto dto)
  4939. {
  4940. // 1. 初始化 SSE
  4941. HttpContext.InitializeSse();
  4942. try
  4943. {
  4944. await HttpContext.SendSseStepAsync(5, "正在准备发送邮件队列...");
  4945. #region 1. 参数与权限前置校验
  4946. if (dto.Id < 1 || dto.CurrUserId < 1 || dto.Guids == null || !dto.Guids.Any())
  4947. {
  4948. await HttpContext.SendSseStepAsync(-1, "参数验证失败,请检查选择的单位及用户状态。");
  4949. return;
  4950. }
  4951. // hotmail 配置信息验证
  4952. var hotmailConfig = await _hotmailService.GetUserMailConfig(dto.CurrUserId);
  4953. (bool verify, string msg) = _hotmailService.ConfigVerify(hotmailConfig);
  4954. if (!verify)
  4955. {
  4956. await HttpContext.SendSseStepAsync(-1, msg);
  4957. return;
  4958. }
  4959. // 获取商邀信息和用户信息
  4960. var invAiInfo = await _sqlSugar.Queryable<Res_InvitationAI_NoGroup>().FirstAsync(x => x.IsDel == 0 && x.Id == dto.Id);
  4961. var userInfo = await _sqlSugar.Queryable<Sys_Users>()
  4962. .Where(x => x.IsDel == 0 && x.Id == dto.CurrUserId)
  4963. .Select(x => new { x.Email, x.CnName })
  4964. .FirstAsync();
  4965. if (invAiInfo?.AiCrawledDetails == null)
  4966. {
  4967. await HttpContext.SendSseStepAsync(-1, "未找到有效的邀请方数据。");
  4968. return;
  4969. }
  4970. // 提取待发送的目标集合
  4971. var guidSet = new HashSet<string>(dto.Guids);
  4972. var seedInvInfos = invAiInfo.AiCrawledDetails.Where(x => guidSet.Contains(x.Guid)).ToList();
  4973. if (!seedInvInfos.Any())
  4974. {
  4975. await HttpContext.SendSseStepAsync(-1, "所选单位信息在原始库中不存在。");
  4976. return;
  4977. }
  4978. #endregion
  4979. await HttpContext.SendSseStepAsync(15, $"准备就绪,共计 {seedInvInfos.Count} 封邮件待发送...");
  4980. #region 2. 批量发送逻辑 (流式反馈)
  4981. int total = seedInvInfos.Count;
  4982. int current = 0;
  4983. var successCount = 0;
  4984. var failCount = 0;
  4985. foreach (var item in seedInvInfos)
  4986. {
  4987. current++;
  4988. // 计算进度:从 20% 到 90%
  4989. int progress = 20 + (int)((double)current / total * 70);
  4990. if (string.IsNullOrEmpty(item.EmailInfo?.EmailTitle) || string.IsNullOrEmpty(item.EmailInfo?.EmailContent))
  4991. {
  4992. await HttpContext.SendSseStepAsync(progress, $"跳过:{item.NameCn} (邮件标题或内容缺失)");
  4993. failCount++;
  4994. continue;
  4995. }
  4996. try
  4997. {
  4998. var req = new MailDto()
  4999. {
  5000. Subject = item.EmailInfo.EmailTitle,
  5001. Content = item.EmailInfo.EmailContent,
  5002. To = item.Email,
  5003. AttachmentPaths = item.EmailInfo.AttachmentPaths
  5004. };
  5005. var res = await _hotmailService.SendMailAsync(hotmailConfig.UserName, req);
  5006. if (res.IsSuccess)
  5007. {
  5008. successCount++;
  5009. // 更新本地状态
  5010. item.EmailInfo.Status = 4; // 发送成功状态
  5011. item.EmailInfo.Operator = userInfo.CnName;
  5012. item.EmailInfo.OperatedAt = DateTime.Now;
  5013. await HttpContext.SendSseStepAsync(progress, $"成功:已向 {item.NameCn}({item.Email}) 发送邮件");
  5014. }
  5015. else
  5016. {
  5017. failCount++;
  5018. await HttpContext.SendSseStepAsync(progress, $"失败:{item.NameCn} 发送失败 ({res.Message})");
  5019. }
  5020. }
  5021. catch (Exception ex)
  5022. {
  5023. failCount++;
  5024. _logger.LogError(ex, $"企微邮件推送异常:{item.NameCn}");
  5025. await HttpContext.SendSseStepAsync(progress, $"异常:{item.NameCn} 连接超时");
  5026. }
  5027. }
  5028. #endregion
  5029. await HttpContext.SendSseStepAsync(95, "正在归档发送记录...");
  5030. #region 3. 数据同步与收尾
  5031. // 更新内存中的全量数据:排除旧的,加入已更新状态的
  5032. invAiInfo.AiCrawledDetails = invAiInfo.AiCrawledDetails
  5033. .Where(x => !guidSet.Contains(x.Guid))
  5034. .Concat(seedInvInfos)
  5035. .OrderByDescending(x => x.EmailInfo.OperatedAt)
  5036. .ToList();
  5037. // 精准更新 JSON 列
  5038. var updateOk = await _sqlSugar.Updateable(invAiInfo)
  5039. .UpdateColumns(x => x.AiCrawledDetails)
  5040. .ExecuteCommandHasChangeAsync();
  5041. if (!updateOk)
  5042. {
  5043. await HttpContext.SendSseStepAsync(-1, "记录保存失败,请检查数据库连接。");
  5044. return;
  5045. }
  5046. await HttpContext.SendSseStepAsync(100, $"任务完成!成功: {successCount}, 失败: {failCount}", new
  5047. {
  5048. Id = invAiInfo.Id,
  5049. SuccessCount = successCount,
  5050. FailCount = failCount
  5051. });
  5052. #endregion
  5053. }
  5054. catch (Exception ex)
  5055. {
  5056. _logger.LogError(ex, "邮件发送流发生崩溃");
  5057. await HttpContext.SendSseStepAsync(-1, $"系统错误:{ex.Message}");
  5058. }
  5059. finally
  5060. {
  5061. await HttpContext.FinalizeSseAsync();
  5062. }
  5063. }
  5064. #endregion
  5065. #region 公务出访
  5066. /// <summary>
  5067. /// 获取团组所有信息,绑定下拉框
  5068. /// </summary>
  5069. /// <param name="dto"></param>
  5070. /// <returns></returns>
  5071. [HttpPost]
  5072. [ProducesResponseType(typeof(JsonView), StatusCodes.Status200OK)]
  5073. public async Task<IActionResult> GetGroupAllList(OfficialActivitiesByDiIdDto dto)
  5074. {
  5075. //string groupSql = string.Format("Select * From Grp_DelegationInfo With(NoLock) Where IsDel = 0 Order By CreateTime Desc");
  5076. //List<Grp_DelegationInfo> _DelegationInfos = _sqlSugar.SqlQueryable< Grp_DelegationInfo >(groupSql).ToList();
  5077. var _groupData = await _delegationInfoRep.PostShareGroupInfos(1);
  5078. dynamic groupInfos = null;
  5079. if (_groupData.Code == 0) groupInfos = _groupData.Data;
  5080. var data = _sqlSugar.Queryable<Sys_SetData>().Where(a => a.IsDel == 0).ToList();
  5081. var data1 = data.Where(a => a.STid == 38).Select(x => new { x.Id, x.Name }).ToList();
  5082. var data2 = data.Where(a => a.STid == 101).Select(x => new { x.Id, x.Name }).ToList();
  5083. //张总安排未参与对接
  5084. if (data2.Any(x => x.Name.Equals("张总安排未参与对接")))
  5085. {
  5086. var zhangZong = data2.FirstOrDefault(x => x.Name.Equals("张总安排未参与对接"));
  5087. if (zhangZong != null) data2.Remove(zhangZong);
  5088. data2.Insert(0, zhangZong);
  5089. }
  5090. var data3 = data.Where(a => a.STid == 66).Select(x => new { x.Id, x.Name, x.Remark }).ToList();
  5091. var _DeleFile = _sqlSugar.Queryable<Grp_DeleFile>().Where(a => a.Diid == dto.DiId && a.IsDel == 0 && a.Category == 970).ToList();
  5092. var translatorData = _sqlSugar.Queryable<Res_TranslatorLibrary>().Where(a => a.IsDel == 0).ToList();
  5093. var translatorData1 = _mapper.Map<List<TranslatorView>>(translatorData);
  5094. foreach (var item in translatorData1) EncryptionProcessor.DecryptProperties(item);
  5095. return Ok(JsonView(true,
  5096. "查询成功!",
  5097. new
  5098. {
  5099. Delegation = groupInfos,
  5100. SetData = data1,
  5101. DataSource = data2,
  5102. currencyData = data3,
  5103. DeleFile = _DeleFile,
  5104. TranslatorData = translatorData1
  5105. }));
  5106. }
  5107. /// <summary>
  5108. /// 公务 团组名称 Page List
  5109. /// </summary>
  5110. /// <param name="dto"></param>
  5111. /// <returns></returns>
  5112. [HttpPost]
  5113. [ProducesResponseType(typeof(JsonView), StatusCodes.Status200OK)]
  5114. public async Task<IActionResult> OfficialActivitiesGroupNameList(OfficialActivitiesGroupNameListDto dto)
  5115. {
  5116. var sw = Stopwatch.StartNew();
  5117. if (!SharingStaticData.PortTypes.Contains(dto.PortType))
  5118. {
  5119. return Ok(JsonView(false, MsgTips.Port));
  5120. }
  5121. dto.PageIndex = dto.PageIndex == 0 ? 1 : dto.PageIndex;
  5122. dto.PageSize = dto.PageSize == 0 ? 10 : dto.PageSize;
  5123. RefAsync<int> total = 0;
  5124. // 只查询需要的字段,避免Select后再赋值,减少内存占用
  5125. var groupInfos = await _officialActivitiesRep._sqlSugar
  5126. .Queryable<Grp_DelegationInfo>()
  5127. .Where(x => x.IsDel == 0)
  5128. .WhereIF(!string.IsNullOrEmpty(dto.GroupName), x => x.TeamName.Contains(dto.GroupName))
  5129. .OrderByDescending(x => x.VisitDate)
  5130. .Select(x => new Web_ShareGroupInfoView()
  5131. {
  5132. Id = x.Id,
  5133. TeamName = x.TeamName,
  5134. TourCode = x.TourCode,
  5135. ClientName = x.ClientName,
  5136. VisitCountry = x.VisitCountry,
  5137. VisitStartDate = x.VisitStartDate,
  5138. VisitEndDate = x.VisitEndDate,
  5139. VisitDays = x.VisitDays,
  5140. VisitPNumber = x.VisitPNumber
  5141. })
  5142. .ToPageListAsync(dto.PageIndex, dto.PageSize, total);
  5143. if (groupInfos.Count > 0)
  5144. {
  5145. for (int i = 0; i < groupInfos.Count; i++)
  5146. {
  5147. var visitCountry = groupInfos[i].VisitCountry;
  5148. groupInfos[i].VisitCountry = !string.IsNullOrEmpty(visitCountry)
  5149. ? _delegationInfoRep.FormartTeamName(visitCountry)
  5150. : string.Empty;
  5151. }
  5152. }
  5153. sw.Stop();
  5154. return Ok(JsonView(true, $"操作成功!耗时:{sw.ElapsedMilliseconds}ms", groupInfos, total));
  5155. }
  5156. /// <summary>
  5157. /// 公务 翻译人员 Page List
  5158. /// </summary>
  5159. /// <param name="dto"></param>
  5160. /// <returns></returns>
  5161. [HttpPost]
  5162. [ProducesResponseType(typeof(JsonView), StatusCodes.Status200OK)]
  5163. public async Task<IActionResult> OfficialActivitiesTranslatorList(OfficialActivitiesTranslatorListDto dto)
  5164. {
  5165. var sw = Stopwatch.StartNew();
  5166. if (!SharingStaticData.PortTypes.Contains(dto.PortType))
  5167. {
  5168. return Ok(JsonView(false, MsgTips.Port));
  5169. }
  5170. dto.PageIndex = dto.PageIndex == 0 ? 1 : dto.PageIndex;
  5171. dto.PageSize = dto.PageSize == 0 ? 10 : dto.PageSize;
  5172. var translatorIds = new List<int>();
  5173. if (!string.IsNullOrEmpty(dto.TranslatorName))
  5174. {
  5175. var translators = await _sqlSugar.Queryable<Res_TranslatorLibrary>()
  5176. .Where(x => x.IsDel == 0 && !string.IsNullOrEmpty(x.Name))
  5177. .Select(x => new { x.Id, x.Name })
  5178. .ToListAsync();
  5179. if (translators.Any())
  5180. {
  5181. foreach (var item in translators)
  5182. {
  5183. var decryptedName = AesEncryptionHelper.Decrypt(item.Name);
  5184. if (!string.IsNullOrEmpty(decryptedName) && decryptedName.Contains(dto.TranslatorName))
  5185. {
  5186. translatorIds.Add(item.Id);
  5187. }
  5188. }
  5189. }
  5190. if (translatorIds.Count == 0)
  5191. {
  5192. sw.Stop();
  5193. return Ok(JsonView(true, $"暂无翻译人员信息!耗时: {sw.ElapsedMilliseconds}ms", Array.Empty<TranslatorView>(), 0));
  5194. }
  5195. }
  5196. RefAsync<int> total = 0;
  5197. var translatorInfos = await _officialActivitiesRep._sqlSugar
  5198. .Queryable<Res_TranslatorLibrary>()
  5199. .Where(x => x.IsDel == 0 && !string.IsNullOrEmpty(x.Name))
  5200. .WhereIF(translatorIds != null && translatorIds.Count > 0, x => translatorIds.Contains(x.Id))
  5201. .OrderByDescending(x => x.CreateTime)
  5202. .Select(x => new TranslatorView()
  5203. {
  5204. Id = x.Id,
  5205. Area = x.Area,
  5206. Name = x.Name,
  5207. Sex = x.Sex,
  5208. Tel = x.Tel,
  5209. Email = x.Email,
  5210. WechatNo = x.WechatNo,
  5211. OtherSocialAccounts = x.OtherSocialAccounts,
  5212. Language = x.Language,
  5213. Price = x.Price,
  5214. Currency = x.Currency,
  5215. })
  5216. .ToPageListAsync(dto.PageIndex, dto.PageSize, total);
  5217. for (int i = 0; i < translatorInfos.Count; i++)
  5218. {
  5219. EncryptionProcessor.DecryptProperties(translatorInfos[i]);
  5220. }
  5221. sw.Stop();
  5222. string msg = translatorInfos.Count == 0 ? $"暂无您查询的翻译人员信息!耗时: {sw.ElapsedMilliseconds}ms" : $"操作成功!耗时: {sw.ElapsedMilliseconds}ms";
  5223. return Ok(JsonView(true, msg, translatorInfos, total));
  5224. }
  5225. /// <summary>
  5226. /// 公务 绑定下拉框
  5227. /// </summary>
  5228. /// <param name="dto"></param>
  5229. /// <returns></returns>
  5230. [HttpPost]
  5231. [ProducesResponseType(typeof(JsonView), StatusCodes.Status200OK)]
  5232. public async Task<IActionResult> OfficialActivitiesInitDatabase(OfficialActivitiesByDiIdDto dto)
  5233. {
  5234. var data = _sqlSugar.Queryable<Sys_SetData>().Where(a => a.IsDel == 0).ToList();
  5235. var data1 = data.Where(a => a.STid == 38).Select(x => new { x.Id, x.Name }).ToList();
  5236. var data2 = data.Where(a => a.STid == 101).Select(x => new { x.Id, x.Name }).ToList();
  5237. //张总安排未参与对接
  5238. if (data2.Any(x => x.Name.Equals("张总安排未参与对接")))
  5239. {
  5240. var zhangZong = data2.FirstOrDefault(x => x.Name.Equals("张总安排未参与对接"));
  5241. if (zhangZong != null) data2.Remove(zhangZong);
  5242. data2.Insert(0, zhangZong);
  5243. }
  5244. var data3 = data.Where(a => a.STid == 66).Select(x => new { x.Id, x.Name, x.Remark }).ToList();
  5245. var _DeleFile = _sqlSugar.Queryable<Grp_DeleFile>().Where(a => a.Diid == dto.DiId && a.IsDel == 0 && a.Category == 970).ToList();
  5246. return Ok(JsonView(true,
  5247. "查询成功!",
  5248. new
  5249. {
  5250. SetData = data1,
  5251. DataSource = data2,
  5252. currencyData = data3,
  5253. DeleFile = _DeleFile
  5254. }));
  5255. }
  5256. /// <summary>
  5257. /// 公务List
  5258. /// </summary>
  5259. /// <param name="dto"></param>
  5260. /// <returns></returns>
  5261. [HttpPost]
  5262. [ProducesResponseType(typeof(JsonView), StatusCodes.Status200OK)]
  5263. public async Task<IActionResult> QueryOfficialActivitiesByDiId(OfficialActivitiesByDiIdDto dto)
  5264. {
  5265. return Ok(await _officialActivitiesRep.QueryOfficialActivitiesByDiId(dto));
  5266. }
  5267. /// <summary>
  5268. /// 根据公务出访数据Id查询数据
  5269. /// </summary>
  5270. /// <param name="dto"></param>
  5271. /// <returns></returns>
  5272. [HttpPost]
  5273. [ProducesResponseType(typeof(JsonView), StatusCodes.Status200OK)]
  5274. public async Task<IActionResult> QueryOfficialActivitiesById(OfficialActivitiesDiIdDto dto)
  5275. {
  5276. Result groupData = await _officialActivitiesRep.QueryOfficialActivitiesById(dto);
  5277. if (groupData.Code != 0)
  5278. {
  5279. return Ok(JsonView(false, groupData.Msg));
  5280. }
  5281. return Ok(JsonView(true, groupData.Msg, groupData.Data));
  5282. }
  5283. /// <summary>
  5284. /// 公务出访操作(Status:1.新增,2.修改)
  5285. /// </summary>
  5286. /// <param name="dto"></param>
  5287. /// <returns></returns>
  5288. [HttpPost]
  5289. [ProducesResponseType(typeof(JsonView), StatusCodes.Status200OK)]
  5290. public async Task<IActionResult> OpOfficialActivities(OpOfficialActivitiesDto dto)
  5291. {
  5292. Result groupData = await _officialActivitiesRep.OpOfficialActivities(dto);
  5293. if (groupData.Code != 0)
  5294. {
  5295. return Ok(JsonView(StatusCodes.Status400BadRequest, groupData.Msg, new { Id = 0 }));
  5296. }
  5297. try
  5298. {
  5299. //公务出访变更发送通知
  5300. await AppNoticeLibrary.SendUserMsg_GroupShare_ToOP(dto.DiId, dto.CreateUserId);
  5301. }
  5302. catch (Exception ex)
  5303. {
  5304. //抄送日志
  5305. }
  5306. return Ok(JsonView(true, groupData.Msg, groupData.Data));
  5307. }
  5308. /// <summary>
  5309. /// 上传文件(邮件截图)
  5310. /// </summary>
  5311. /// <param name="dto"></param>
  5312. /// <returns></returns>
  5313. [HttpPost]
  5314. [ProducesResponseType(typeof(JsonView), StatusCodes.Status200OK)]
  5315. public async Task<IActionResult> OfficialActivitiesUploadFiles([FromForm] OfficialActivitiesUploadFilesDto dto)
  5316. {
  5317. string networkPath = AppSettingsHelper.Get("GrpFileBaseUrl");
  5318. string localPath = AppSettingsHelper.Get("GrpFileBasePath");
  5319. string ptfPath = AppSettingsHelper.Get("GrpFileFtpPath");
  5320. if (dto.diId < 1 || dto.currUserId < 1)
  5321. {
  5322. return Ok(JsonView(false, "参数有误,上传失败!"));
  5323. }
  5324. if (dto.files == null || dto.files.Count < 1)
  5325. {
  5326. return Ok(JsonView(false, "文件为空,上传失败!"));
  5327. }
  5328. string localFileDir = $"{localPath}{ptfPath}";
  5329. List<string> fileUrls = new List<string>();
  5330. string[] failFiles = new string[] { };
  5331. foreach (var file in dto.files)
  5332. {
  5333. //文件名称
  5334. string[] fileNameArray = file.FileName.Split(".");
  5335. string projectFileName = $"{fileNameArray[0].ToString().Replace("-", "_").Replace("/", "_")}{DateTime.UtcNow:yyyyMMddHHmmss}.{fileNameArray[1]}";
  5336. //上传的文件的路径
  5337. string filePath = $@"{localPath}公务相关文件";
  5338. if (!Directory.Exists(filePath))
  5339. {
  5340. Directory.CreateDirectory(filePath);
  5341. }
  5342. var path = Path.Combine(filePath, projectFileName);
  5343. //using var stream = new FileStream(path, FileMode.Create);
  5344. //await file.CopyToAsync(stream);
  5345. try
  5346. {
  5347. using (FileStream fs = System.IO.File.Create(path))
  5348. {
  5349. file.CopyTo(fs);
  5350. fs.Flush();
  5351. }
  5352. fileUrls.Add($"/{ptfPath}公务相关文件/{projectFileName}");
  5353. }
  5354. catch (Exception ex)
  5355. {
  5356. failFiles.Append(file.FileName);
  5357. }
  5358. }
  5359. var oaInfo = await _sqlSugar.Queryable<Res_OfficialActivities>().Where(x => x.Id == dto.id).FirstAsync();
  5360. List<string> files = new List<string>();
  5361. var status = false;
  5362. var _oaInfo = new Res_OfficialActivities()
  5363. {
  5364. DiId = dto.diId,
  5365. ScreenshotOfMailUrl = JsonConvert.SerializeObject(fileUrls),
  5366. IsDel = 0,
  5367. CreateUserId = dto.currUserId,
  5368. CreateTime = DateTime.Now
  5369. };
  5370. if (oaInfo == null)
  5371. {
  5372. var id = await _sqlSugar.Insertable<Res_OfficialActivities>(_oaInfo)
  5373. .ExecuteReturnIdentityAsync();
  5374. if (id > 0)
  5375. {
  5376. status = true;
  5377. dto.id = id;
  5378. }
  5379. }
  5380. else
  5381. {
  5382. var oldFiles = string.IsNullOrEmpty(oaInfo.ScreenshotOfMailUrl)
  5383. ? new List<string>()
  5384. : JsonConvert.DeserializeObject<List<string>>(oaInfo.ScreenshotOfMailUrl);
  5385. if (oldFiles.Count > 0)
  5386. {
  5387. fileUrls.AddRange(oldFiles);
  5388. }
  5389. fileUrls = fileUrls.Distinct().ToList();
  5390. if (fileUrls.Count > 0)
  5391. {
  5392. var upd = await _sqlSugar.Updateable<Res_OfficialActivities>()
  5393. .SetColumns(x => x.ScreenshotOfMailUrl == JsonConvert.SerializeObject(fileUrls))
  5394. .Where(x => x.Id == dto.id)
  5395. .ExecuteCommandAsync();
  5396. if (upd > 0) status = true;
  5397. }
  5398. }
  5399. List<string> returnFileUrls = new List<string>();
  5400. fileUrls.ForEach(x =>
  5401. {
  5402. returnFileUrls.Add($"{networkPath}{x}");
  5403. });
  5404. if (status)
  5405. {
  5406. string msg = "操作成功!";
  5407. if (failFiles.Length > 1)
  5408. {
  5409. foreach (var item in failFiles)
  5410. {
  5411. msg += $"{failFiles}、";
  5412. }
  5413. msg = msg.Substring(0, msg.Length - 1);
  5414. msg += "等文件保存失败!";
  5415. }
  5416. return Ok(JsonView(true, msg, new { id = dto.id, fileUrls = returnFileUrls }));
  5417. }
  5418. return Ok(JsonView(false, "操作失败!"));
  5419. }
  5420. /// <summary>
  5421. /// 删除文件(邮件截图)
  5422. /// </summary>
  5423. /// <param name="dto"></param>
  5424. /// <returns></returns>
  5425. [HttpPost]
  5426. [ProducesResponseType(typeof(JsonView), StatusCodes.Status200OK)]
  5427. public async Task<IActionResult> OfficialActivitiesDelFile(OfficialActivitiesDelFileDto dto)
  5428. {
  5429. string networkPath = AppSettingsHelper.Get("GrpFileBaseUrl");
  5430. string localPath = AppSettingsHelper.Get("GrpFileBasePath");
  5431. string ptfPath = AppSettingsHelper.Get("GrpFileFtpPath");
  5432. if (dto.Id < 1 || string.IsNullOrEmpty(dto.FileName))
  5433. {
  5434. return Ok(JsonView(false, "参数有误,上传失败!"));
  5435. }
  5436. string localFileDir = $"{localPath}{ptfPath}";
  5437. List<string> fileUrls = new List<string>();
  5438. var oaInfo = await _sqlSugar.Queryable<Res_OfficialActivities>().Where(x => x.Id == dto.Id).FirstAsync();
  5439. if (oaInfo != null)
  5440. {
  5441. string urlJson = oaInfo.ScreenshotOfMailUrl ?? "";
  5442. if (!string.IsNullOrEmpty(urlJson))
  5443. {
  5444. List<string> urls = JsonConvert.DeserializeObject<List<string>>(urlJson);
  5445. var filePath = urls.Find(x => x.Contains(dto.FileName));
  5446. if (string.IsNullOrEmpty(filePath)) return Ok(JsonView(false, "文件不存在!"));
  5447. var updFile = urls.Remove(filePath);
  5448. if (updFile)
  5449. {
  5450. //删除文件
  5451. string fileUrl = AppSettingsHelper.Get("GrpFileBasePath").Replace(@"/Office/GrpFile", "") + filePath;
  5452. if (System.IO.File.Exists(fileUrl))
  5453. {
  5454. System.IO.File.Delete(fileUrl);
  5455. }
  5456. string urlJsonstr = JsonConvert.SerializeObject(urls);
  5457. var upd = await _sqlSugar.Updateable<Res_OfficialActivities>()
  5458. .SetColumns(x => x.ScreenshotOfMailUrl == urlJsonstr)
  5459. .Where(x => x.Id == dto.Id)
  5460. .ExecuteCommandAsync();
  5461. if (upd > 0) return Ok(JsonView(true, "操作成功!"));
  5462. }
  5463. }
  5464. }
  5465. return Ok(JsonView(false, "操作失败!"));
  5466. }
  5467. /// <summary>
  5468. /// 公务出访 确认、取消邀请
  5469. /// </summary>
  5470. /// <param name="dto"></param>
  5471. /// <returns></returns>
  5472. [HttpPost]
  5473. [ProducesResponseType(typeof(JsonView), StatusCodes.Status200OK)]
  5474. public async Task<IActionResult> OfficialActivitiesInviteOperation(OfficialActivitiesInviteOperationDto dto)
  5475. {
  5476. if (dto.Id < 1) return Ok(JsonView(false, "Id参数有误!"));
  5477. if (dto.Type < 0 || dto.Type > 1) return Ok(JsonView(false, "Type参数有误!"));
  5478. var upd = await _sqlSugar.Updateable<Res_OfficialActivities>()
  5479. .SetColumns(x => x.ConfirmTheInvitation == dto.Type)
  5480. .Where(x => x.Id == dto.Id)
  5481. .ExecuteCommandAsync();
  5482. if (upd > 0) return Ok(JsonView(true, "操作成功!"));
  5483. return Ok(JsonView(false, "操作失败!"));
  5484. }
  5485. /// <summary>
  5486. /// 上传文件
  5487. /// </summary>
  5488. /// <param name="file"></param>
  5489. /// <returns></returns>
  5490. [HttpPost]
  5491. [ProducesResponseType(typeof(JsonView), StatusCodes.Status200OK)]
  5492. public async Task<IActionResult> UploadOfficialActivities(IFormFile file)
  5493. {
  5494. try
  5495. {
  5496. int Type = int.Parse(Request.Headers["Type"]);//1公务方简介,2公务活动图片,3发票
  5497. int DiId = int.Parse(Request.Headers["DiId"]);
  5498. int CreateUserId = int.Parse(Request.Headers["CreateUserId"]);
  5499. if (file != null)
  5500. {
  5501. var fileDir = AppSettingsHelper.Get("GrpFileBasePath");
  5502. //文件名称
  5503. string projectFileName = file.FileName;
  5504. //上传的文件的路径
  5505. string filePath = "";
  5506. if (!Directory.Exists(fileDir))
  5507. {
  5508. Directory.CreateDirectory(fileDir);
  5509. }
  5510. //上传的文件的路径
  5511. filePath = fileDir + $@"\商邀相关文件\{projectFileName}";
  5512. using (FileStream fs = System.IO.File.Create(filePath))
  5513. {
  5514. file.CopyTo(fs);
  5515. fs.Flush();
  5516. }
  5517. Grp_DeleFile d = new Grp_DeleFile();
  5518. d.Diid = DiId;
  5519. d.Category = 970;
  5520. if (Type == 1) d.Kind = 1;
  5521. else if (Type == 2) d.Kind = 2;
  5522. else if (Type == 3) d.Kind = 3;
  5523. d.FileName = projectFileName;
  5524. d.FilePath = "";
  5525. d.CreateUserId = CreateUserId;
  5526. int id = await _sqlSugar.Insertable(d).ExecuteReturnIdentityAsync();
  5527. return Ok(JsonView(true, "上传成功!", projectFileName));
  5528. }
  5529. else
  5530. {
  5531. return Ok(JsonView(false, "上传失败!"));
  5532. }
  5533. }
  5534. catch (Exception ex)
  5535. {
  5536. return Ok(JsonView(false, "程序错误!"));
  5537. throw;
  5538. }
  5539. }
  5540. /// <summary>
  5541. /// 删除文件
  5542. /// </summary>
  5543. /// <param name="dto"></param>
  5544. /// <returns></returns>
  5545. [HttpPost]
  5546. [ProducesResponseType(typeof(JsonView), StatusCodes.Status200OK)]
  5547. public async Task<IActionResult> DelloadOfficialActivities(DelBaseDto dto)
  5548. {
  5549. try
  5550. {
  5551. var fileDir = AppSettingsHelper.Get("GrpFileBasePath");
  5552. Grp_DeleFile _DeleFile = await _sqlSugar.Queryable<Grp_DeleFile>().FirstAsync(a => a.Id == dto.Id);
  5553. if (_DeleFile != null)
  5554. {
  5555. string fileName = _DeleFile.FileName;
  5556. string filePath = fileDir + "/团组增减款项相关文件/" + fileName;
  5557. // 删除该文件
  5558. System.IO.File.Delete(filePath);
  5559. int id = await _sqlSugar.Updateable<Grp_DeleFile>().Where(a => a.Id == dto.Id).SetColumns(a => new Grp_DeleFile { IsDel = 1, DeleteUserId = dto.DeleteUserId, DeleteTime = DateTime.Now.ToString("yyyy-MM-dd HH:mm:ss") }).ExecuteCommandAsync();
  5560. return Ok(JsonView(true, "取消文件成功!"));
  5561. }
  5562. else
  5563. {
  5564. return Ok(JsonView(false, "取消文件失败!"));
  5565. }
  5566. }
  5567. catch (Exception ex)
  5568. {
  5569. return Ok(JsonView(false, "程序错误!"));
  5570. throw;
  5571. }
  5572. }
  5573. /// <summary>
  5574. /// 删除公务出访信息
  5575. /// </summary>
  5576. /// <param name="dto"></param>
  5577. /// <returns></returns>
  5578. [HttpPost]
  5579. [ProducesResponseType(typeof(JsonView), StatusCodes.Status200OK)]
  5580. public async Task<IActionResult> DelOfficialActivities(DelBaseDto dto)
  5581. {
  5582. _sqlSugar.BeginTran();
  5583. var res = await _officialActivitiesRep.SoftDeleteByIdAsync<Res_OfficialActivities>(dto.Id.ToString(), dto.DeleteUserId);
  5584. if (!res)
  5585. {
  5586. _sqlSugar.RollbackTran();
  5587. return Ok(JsonView(false, "删除失败"));
  5588. }
  5589. #region 删除公务出访
  5590. await _sqlSugar.Updateable<Grp_OfficialDutyLinkTranslator>()
  5591. .SetColumns(x => new Grp_OfficialDutyLinkTranslator()
  5592. {
  5593. DeleteUserId = dto.DeleteUserId,
  5594. DeleteTime = DateTime.Now.ToString("yyyy-MM-dd HH:mm:ss"),
  5595. IsDel = 1,
  5596. Remark = $"公务出访-->删除"
  5597. })
  5598. .Where(x => x.OfficialDutyId == dto.Id)
  5599. .ExecuteCommandAsync();
  5600. #endregion
  5601. _sqlSugar.CommitTran();
  5602. return Ok(JsonView(true, "删除成功!"));
  5603. }
  5604. /// <summary>
  5605. /// 公务出访
  5606. /// 请示范例提示
  5607. /// Add Time:2024-05-13 13:56:44
  5608. /// </summary>
  5609. /// <param name="dto"></param>
  5610. /// <returns></returns>
  5611. [HttpPost]
  5612. [ProducesResponseType(typeof(JsonView), StatusCodes.Status200OK)]
  5613. public async Task<IActionResult> PostOfficialActivitiesReqReqSampleTips(PostOfficialActivitiesReqReqSampleTipsDto dto)
  5614. {
  5615. var res = await _officialActivitiesRep.PostReqReqSampleTips(dto.country, dto.area, dto.client);
  5616. if (res.Code == 0)
  5617. {
  5618. return Ok(JsonView(true, "操作成功!", res.Data));
  5619. }
  5620. return Ok(JsonView(false, res.Msg));
  5621. }
  5622. /// <summary>
  5623. /// 公务出访 (省外办,市外办) File Downlaod
  5624. /// </summary>
  5625. /// <param name="dto"></param>
  5626. /// <returns></returns>
  5627. [HttpPost]
  5628. [ProducesResponseType(typeof(JsonView), StatusCodes.Status200OK)]
  5629. public async Task<IActionResult> OfficialActivitiesFileDownload(OfficialActivitiesFileDownload dto)
  5630. {
  5631. #region 参数验证
  5632. if (dto.FileType < 1 || dto.FileType > 2) return Ok(JsonView(false, "请传入有效的FileType参数. 1 省外办出访请示 2 市外办出访请示", ""));
  5633. if (dto.DiId < 1) return Ok(JsonView(false, "请传入有效的DiId参数.", ""));
  5634. #endregion
  5635. //团组基础信息
  5636. var groupInfo = _sqlSugar.Queryable<Grp_DelegationInfo>().Where(it => it.IsDel == 0 && it.Id == dto.DiId).First();
  5637. if (groupInfo == null) return Ok(JsonView(false, "该团组基本信息不存在", ""));
  5638. groupInfo.VisitCountry = groupInfo.VisitCountry.Replace("|", "、");
  5639. //团组公务信息
  5640. var obDatas = _sqlSugar.Queryable<Res_OfficialActivities>().Where(it => it.IsDel == 0 && it.DiId == dto.DiId).OrderBy(it => it.Date).ToList();
  5641. //if (obDatas.Count < 1) return Ok(JsonView(false, "请先录入公务信息!"));
  5642. //团组客户名单
  5643. var guestResult = _tourClientListRep._ItemByDiId(1, dto.DiId);
  5644. List<TourClientListByDiIdView> guestInfos = new List<TourClientListByDiIdView>();
  5645. if (guestResult.Result.Code == 0) guestInfos = guestResult.Result.Data as List<TourClientListByDiIdView>;
  5646. string visitCountrys = ""; //××国家(或地区)×天,××国家(或地区)×天
  5647. string countryStayStr = ""; // xx、xx、xx
  5648. //出入境费用 住宿类型
  5649. var dayCostDatas = _sqlSugar.Queryable<Grp_DayAndCost>().Where(it => it.IsDel == 0 && it.DiId == dto.DiId && it.Type == 1).ToList();
  5650. if (dayCostDatas.Count > 0)
  5651. {
  5652. List<int> nationalIds = dayCostDatas.Select(it => it.NationalTravelFeeId).ToList();
  5653. var nationalDatas = _sqlSugar.Queryable<Grp_NationalTravelFee>().Where(it => nationalIds.Contains(it.Id)).ToList();
  5654. var nationalDataGroupByCountry = nationalDatas.GroupBy(it => it.Country);
  5655. foreach (var item in nationalDataGroupByCountry)
  5656. {
  5657. visitCountrys += $"{item.Key}、";
  5658. int stayDays = nationalIds.Where(it => it == item.ToList()[0].Id).Count();
  5659. countryStayStr += $"{item.Key}{stayDays}天,";
  5660. }
  5661. if (visitCountrys.Length > 0) visitCountrys = visitCountrys.Substring(0, visitCountrys.Length - 1);
  5662. if (countryStayStr.Length > 0) countryStayStr = countryStayStr.Substring(0, countryStayStr.Length - 1);
  5663. }
  5664. else
  5665. {
  5666. visitCountrys = groupInfo.VisitCountry;
  5667. countryStayStr = GeneralMethod.GetCountryStandingTime(groupInfo.Id); //计算国家出访天数
  5668. }
  5669. //出访人数
  5670. int visitPeopleNum = groupInfo.VisitPNumber;
  5671. int visitDaysNum = groupInfo.VisitDays;
  5672. //出访单位
  5673. string obInfoStr = "";
  5674. foreach (var ob in obDatas) obInfoStr += @$"{ob.Client.Trim()}{ob.Job.Trim()}{ob.Contact.Trim()}、";
  5675. if (obInfoStr.Length > 0) obInfoStr = obInfoStr.Substring(0, obInfoStr.Length - 1);
  5676. obInfoStr = obInfoStr ?? "[公务出访未录入]";
  5677. //出访路线
  5678. string lineStr = GeneralMethod.GetGroupCityLine(groupInfo.Id, "—");
  5679. if (dto.FileType == 1)
  5680. {
  5681. //载入模板
  5682. string tempPath = AppSettingsHelper.Get("WordBasePath") + "Template/省外办出访请示 - 模板.docx";
  5683. var doc = new Document(tempPath);
  5684. DocumentBuilder builder = new DocumentBuilder(doc);
  5685. //键值对存放数据
  5686. Dictionary<string, string> dic = new Dictionary<string, string>();
  5687. //××(组团单位):接团客户信息团组
  5688. dic.Add("GroupClient", $"{groupInfo.ClientUnit}\r\n");
  5689. //关于××(职务、姓名)等×人赴××(国家或地区)进行×××(出访目的)的请示
  5690. string guestName = "";
  5691. string guestJob = "";
  5692. string guestInfoStr = "";
  5693. var guestFirstInfo = guestInfos.FirstOrDefault();
  5694. if (guestFirstInfo != null)
  5695. {
  5696. guestName = guestFirstInfo.LastName.Trim() + guestFirstInfo.FirstName.Trim();
  5697. guestJob = guestFirstInfo.Job.Trim();
  5698. guestInfoStr = $@"{guestJob}、{guestName}";
  5699. }
  5700. guestInfoStr = guestInfoStr ?? "[接团客户名单未录入]";
  5701. string askTitle = $@"关于{guestInfoStr}等{visitPeopleNum}人赴{visitCountrys}进行{groupInfo.VisitPurpose}的请示";
  5702. dic.Add("AskTitle", askTitle);
  5703. //应×××(邀请方名称+邀请人职务和姓名)的邀请,我单位拟派×××(职务、姓名)等×人(人数)于×××年×××月×××日赴×××(国家或地区)进行×××(出访目的)。现请示如下。
  5704. string visitDateStr = @$"{groupInfo.VisitDate.Year}年{groupInfo.VisitDate.Month}月{groupInfo.VisitDate.Day}日";
  5705. string askSubTitle = $@"应{obInfoStr}的邀请,我单位拟派{guestInfoStr}等{visitPeopleNum}人于{visitDateStr}赴{visitCountrys}进行{groupInfo.VisitPurpose}。现请示如下。";
  5706. dic.Add("AskSubTitle", askSubTitle);
  5707. //出访目的
  5708. dic.Add("VisitPurpose", groupInfo.VisitPurpose);
  5709. //出访任务
  5710. //(一)××(国家或地区)
  5711. //1.拜访×××(机构名称)×××(姓名、职务)洽谈(或调研、推动等)×××(项目或机构名称等)。(例:拜会×××经济与发展司司长×××,商讨“×××活动”相关筹备工作。)
  5712. //2.拜访×××(机构名称)×××(姓名、职务)洽谈(或调研、推动等)×××(项目或机构名称等)。(例:拜会×××经济与发展司司长×××,商讨“×××活动”相关筹备工作。)
  5713. string obBackgroundStr = "";//出访背景
  5714. string taskStr = ""; ; //出访任务
  5715. var countrys = obDatas.GroupBy(it => it.Country);
  5716. int countryIndex = 1;
  5717. foreach (var item in countrys)
  5718. {
  5719. string taskTitle = $"({GetToUpperNumber(countryIndex)}){item.Key}\r\n";
  5720. string taskContent = "";
  5721. string obBackgroundContent = "";
  5722. int obIndex = 1;
  5723. foreach (var obInfo in item.ToList())
  5724. {
  5725. string reqSmaple = "";
  5726. string settingStr = "";
  5727. if (!string.IsNullOrEmpty(obInfo.ReqSample)) reqSmaple = obInfo.ReqSample;
  5728. else reqSmaple = "[公务出访请示范例未录入]";
  5729. if (!string.IsNullOrEmpty(obInfo.Setting)) settingStr = obInfo.Setting;
  5730. else settingStr = "[公务方背景未录入]";
  5731. taskContent += $"{obIndex}. {reqSmaple}\r\n";
  5732. obBackgroundContent += $"{obIndex}. {obInfo.Client}:{settingStr}\r\n";
  5733. obIndex++;
  5734. }
  5735. taskStr += $"{taskTitle}{taskContent}";
  5736. obBackgroundStr += $"{taskTitle}{obBackgroundContent}";
  5737. countryIndex++;
  5738. }
  5739. //出访背景
  5740. if (obBackgroundStr.Length < 1) obBackgroundStr = "[公务出访背景未录入]";
  5741. dic.Add("OBSetting", obBackgroundStr);
  5742. //出访任务
  5743. if (taskStr.Length < 1) taskStr = "[公务出访任务未录入]";
  5744. dic.Add("TaskContent", taskStr);
  5745. //出访时间
  5746. //代表团拟于××年×月×日—×月×日出访,在外停留×天。其中,××国家(或地区)×天,××国家(或地区)×天。
  5747. string visitTimeQuantumStr = $"代表团拟于{groupInfo.VisitStartDate.Year}年{groupInfo.VisitStartDate.Month}月{groupInfo.VisitStartDate.Day}日—{groupInfo.VisitEndDate.Month}月{groupInfo.VisitEndDate.Day}日出访,在外停留{visitDaysNum}天。其中,{countryStayStr}。";
  5748. //出访时间
  5749. dic.Add("TimeQuantum", visitTimeQuantumStr);
  5750. //出访路线
  5751. //成都—××(出境城市名称)—××(转机不出机场)—××(公务所在城市)……—××(入境城市名称)—成都。(例:成都—法兰克福<转机不出机场>—巴黎—巴塞罗那—阿姆斯特丹<转机不出机场>-成都)
  5752. dic.Add("Line", lineStr);
  5753. //scheduling //行程安排
  5754. string schedulingStr = $"[OP行程单暂未生成]";
  5755. #region op行程 根据黑屏代码录入
  5756. var opTripView = GeneralMethod.GetBriefStroke(groupInfo.Id);
  5757. if (opTripView.Code != 0) schedulingStr = $"[{opTripView.Msg}]";
  5758. schedulingStr = "";
  5759. List<Grp_TravelList> travelList = new List<Grp_TravelList>();
  5760. travelList = opTripView.Data as List<Grp_TravelList>;
  5761. if (travelList != null)
  5762. {
  5763. foreach (var item in travelList)
  5764. {
  5765. schedulingStr += $"{item.Date}({item.WeekDay})\r\n{item.Trip}\r\n";
  5766. }
  5767. }
  5768. #endregion
  5769. dic.Add("Scheduling", schedulingStr);
  5770. #region 填充word模板书签内容
  5771. foreach (var key in dic.Keys)
  5772. {
  5773. builder.MoveToBookmark(key);
  5774. builder.Write(dic[key]);
  5775. }
  5776. #endregion
  5777. //获取word里所有表格
  5778. NodeCollection allTables = doc.GetChildNodes(NodeType.Table, true);
  5779. //获取所填表格的序数
  5780. Aspose.Words.Tables.Table tableOne = allTables[0] as Aspose.Words.Tables.Table;
  5781. var rowStart = tableOne.Rows[0]; //获取第1行
  5782. //循环赋值
  5783. for (int i = 0; i < guestInfos.Count; i++)
  5784. {
  5785. var guestInfo = guestInfos[i];
  5786. builder.MoveToCell(0, i + 1, 0, 0);
  5787. builder.Write(guestInfo.LastName + guestInfo.FirstName);
  5788. builder.MoveToCell(0, i + 1, 1, 0);
  5789. int sex = guestInfo.Sex;
  5790. string sexStr = string.Empty;
  5791. if (sex == 0) sexStr = "男";
  5792. else if (sex == 1) sexStr = "女";
  5793. else sexStr = "-";
  5794. builder.Write(sexStr);
  5795. builder.MoveToCell(0, i + 1, 2, 0);
  5796. builder.Write(guestInfo.CompanyFullName + guestInfo.Job);
  5797. builder.MoveToCell(0, i + 1, 3, 0);
  5798. //string birthDay = "";
  5799. string birthDayStr = string.Empty;
  5800. if (guestInfo.BirthDay != DateTime.MinValue)
  5801. {
  5802. birthDayStr = guestInfo.BirthDay.ToString("yyyy.MM");
  5803. }
  5804. builder.Write(birthDayStr);
  5805. }
  5806. //删除多余行
  5807. while (tableOne.Rows.Count > guestInfos.Count + 1)
  5808. {
  5809. tableOne.Rows.RemoveAt(guestInfos.Count + 1);
  5810. }
  5811. var fileDir = AppSettingsHelper.Get("WordBasePath");
  5812. string fileName = $"{groupInfo.TeamName}省外办出访请示{DateTime.Now.ToString("yyyyMMddHHmmss")}.docx";
  5813. string filePath = fileDir + $@"OfficialActivities/{fileName}";
  5814. doc.Save(filePath);
  5815. string Url = $@"{AppSettingsHelper.Get("WordBaseUrl")}Office/Word/OfficialActivities/{fileName}";
  5816. return Ok(JsonView(true, "操作成功!", Url));
  5817. }
  5818. else if (dto.FileType == 2)
  5819. {
  5820. //载入模板
  5821. string tempPath = AppSettingsHelper.Get("WordBasePath") + "Template/市外办出访请示 - 模板.docx";
  5822. var doc = new Document(tempPath);
  5823. DocumentBuilder builder = new DocumentBuilder(doc);
  5824. //键值对存放数据
  5825. var dic = new Dictionary<string, string>
  5826. {
  5827. //××(组团单位):接团客户信息团组
  5828. { "GroupClient", $"{groupInfo.ClientUnit}" },
  5829. { "GroupClient1", $"{groupInfo.ClientUnit}" }
  5830. };
  5831. //关于××(职务、姓名)等×人赴××(国家或地区)进行×××(出访目的)的请示
  5832. string guestName = "";
  5833. string guestJob = "";
  5834. string guestInfoStr = "";
  5835. var guestFirstInfo = guestInfos.FirstOrDefault();
  5836. if (guestFirstInfo != null)
  5837. {
  5838. guestName = guestFirstInfo.LastName.Trim() + guestFirstInfo.FirstName.Trim();
  5839. guestJob = guestFirstInfo.Job;
  5840. guestInfoStr = $@"{guestJob}、{guestName.Trim()}";
  5841. }
  5842. string reqTitle = $@"关于{guestInfoStr}等{visitPeopleNum}人赴{visitCountrys}进行{groupInfo.VisitPurpose}的请示";
  5843. dic.Add("ReqTitle", reqTitle);
  5844. //应×××(邀请方名称+邀请人职务和姓名)的邀请,我单位拟派×××(职务、姓名)等×人(人数)于×××年×××月×××日赴×××(国家或地区)进行×××(出访目的)。
  5845. string visitDateStr = @$"{groupInfo.VisitDate.Year}年{groupInfo.VisitDate.Month}月{groupInfo.VisitDate.Day}日";
  5846. string reqSubTitle = $@"应{obInfoStr}的邀请,我单位拟派{guestInfoStr}等{visitPeopleNum}人于{visitDateStr}赴{visitCountrys}进行{groupInfo.VisitPurpose}。";
  5847. dic.Add("ReqSubTitle", reqSubTitle);
  5848. /*
  5849. * 出访任务
  5850. * (一)××(国家或地区)
  5851. * 1.拜访×××(机构名称)×××(姓名、职务)洽谈(或调研、推动等)×××(项目或机构名称等)。(例:拜会×××经济与发展司司长×××,商讨“×××活动”相关筹备工作。)背景:拟拜访机构的优势、拟洽谈项目的前期进展情况等。(例:背景:由×××总领馆联合×××市政府共同举办的“×××活动”将于今年6月在×××举行。)
  5852. * 2.拜访×××(机构名称)×××(姓名、职务)洽谈(或调研等)×××(项目或机构名称等)。(例:拜会友城×××市市长×××,巩固和发展两市传统友谊,商谈两市未来互动交流合作项目。)背景:拟拜访机构的优势、拟洽谈项目的前期进展情况等。(例:背景:×××年×月,×××市与×××市正式缔结友好城市关系。在此框架下,两市开展了一系列友好互访和商务交流。并于×××年×月共同举办了“×××活动”。)
  5853. *
  5854. */
  5855. string taskStr = string.Empty;
  5856. List<string> countrys = obDatas.Where(it => !string.IsNullOrEmpty(it.Country)).Select(it => it.Country).ToList();
  5857. int countryIndex = 1;
  5858. foreach (var item in countrys)
  5859. {
  5860. string taskTitle = $"({GetToUpperNumber(countryIndex)}){item ?? "[公务出访国家未填写]"}\r\n";
  5861. string taskContent = "";
  5862. if (!string.IsNullOrEmpty(item))
  5863. {
  5864. var countryObDatas = obDatas.Where(it => !string.IsNullOrEmpty(it.Country) && it.Country.Contains(item)).OrderBy(it => it.Date).ToList();
  5865. int obIndex = 1;
  5866. if (countryObDatas.Count == 0) taskContent = "[公务出访未录入]\r\n";
  5867. else
  5868. {
  5869. foreach (var obInfo in countryObDatas)
  5870. {
  5871. taskContent += $"{obIndex}.{obInfo.ReqSample ?? "[公务出访请示范例未填写]"}\r\n背景:{obInfo.Setting ?? "[公务出访背景未填写]"}\r\n";
  5872. obIndex++;
  5873. }
  5874. }
  5875. }
  5876. taskStr += $"{taskTitle}{taskContent}";
  5877. countryIndex++;
  5878. }
  5879. if (string.IsNullOrEmpty(taskStr)) taskStr = "××××××";
  5880. //出访任务
  5881. dic.Add("TaskContent", taskStr);
  5882. //出访目的
  5883. dic.Add("VisitPurpose", groupInfo.VisitPurpose);
  5884. //出访信息
  5885. //代表团拟于××年×月×日—×月×日出访,在外停留×天。其中,××国家(或地区)×天,××国家(或地区)×天。出访路线:成都—××(出境城市名称)—××(转机不出机场)—××(公务所在城市)……—××(入境城市名称)—成都。(例:成都—法兰克福<转机不出机场>—巴黎—巴塞罗那—阿姆斯特丹<转机不出机场>-成都)出访费用:由××承担(注明由外方或上级机关承担,还是由派员单位在年度安排的预算经费中列支)。
  5886. string tripInfoStr = "";
  5887. tripInfoStr = $"代表团拟于{groupInfo.VisitStartDate.Year}年{groupInfo.VisitStartDate.Month}月{groupInfo.VisitStartDate.Day}日—{groupInfo.VisitEndDate.Month}月{groupInfo.VisitEndDate.Day}日出访,在外停留{visitDaysNum}天。其中,{countryStayStr}。出访路线:{lineStr}。出访费用:由××承担(注明由外方或上级机关承担,还是由派员单位在年度安排的预算经费中列支)";
  5888. dic.Add("TripInfo", tripInfoStr);
  5889. #region 填充word模板书签内容
  5890. foreach (var key in dic.Keys)
  5891. {
  5892. builder.MoveToBookmark(key);
  5893. builder.Write(dic[key]);
  5894. }
  5895. #endregion
  5896. //获取word里所有表格
  5897. NodeCollection allTables = doc.GetChildNodes(NodeType.Table, true);
  5898. //获取所填表格的序数
  5899. Aspose.Words.Tables.Table tableOne = allTables[0] as Aspose.Words.Tables.Table;
  5900. var rowStart = tableOne.Rows[0]; //获取第1行
  5901. //循环赋值
  5902. for (int i = 0; i < guestInfos.Count; i++)
  5903. {
  5904. var guestInfo = guestInfos[i];
  5905. builder.MoveToCell(0, i + 1, 0, 0);
  5906. builder.Write(guestInfo.LastName + guestInfo.FirstName);
  5907. builder.MoveToCell(0, i + 1, 1, 0);
  5908. int sex = guestInfo.Sex;
  5909. string sexStr = string.Empty;
  5910. if (sex == 0) sexStr = "男";
  5911. else if (sex == 1) sexStr = "女";
  5912. else sexStr = "未设置";
  5913. builder.Write(sexStr);
  5914. builder.MoveToCell(0, i + 1, 2, 0);
  5915. builder.Write(guestInfo.CompanyFullName + guestInfo.Job);
  5916. builder.MoveToCell(0, i + 1, 3, 0);
  5917. DateTime birthDay = guestInfo.BirthDay;
  5918. string birthDayStr = string.Empty;
  5919. if (birthDay != DateTime.MinValue)
  5920. {
  5921. birthDayStr = birthDay.ToString("yyyy.MM");
  5922. }
  5923. builder.Write(birthDayStr);
  5924. }
  5925. //删除多余行
  5926. while (tableOne.Rows.Count > guestInfos.Count + 1)
  5927. {
  5928. tableOne.Rows.RemoveAt(guestInfos.Count + 1);
  5929. }
  5930. var fileDir = AppSettingsHelper.Get("WordBasePath");
  5931. var fileName = $"{groupInfo.TeamName}市外办出访请示{DateTime.Now:yyyyMMddHHmmss}.docx";
  5932. var filePath = fileDir + $@"OfficialActivities/{fileName}";
  5933. doc.Save(filePath);
  5934. var url = $@"{AppSettingsHelper.Get("WordBaseUrl")}Office/Word/OfficialActivities/{fileName}";
  5935. return Ok(JsonView(true, "操作成功!", url));
  5936. }
  5937. return Ok(JsonView(true, "操作失败!", ""));
  5938. }
  5939. private static string GetToUpperNumber(int num)
  5940. {
  5941. string numStr = "";
  5942. if (num == 1) numStr = "一";
  5943. else if (num == 2) numStr = "二";
  5944. else if (num == 3) numStr = "三";
  5945. else if (num == 4) numStr = "四";
  5946. else if (num == 5) numStr = "五";
  5947. else if (num == 6) numStr = "六";
  5948. else if (num == 7) numStr = "七";
  5949. else if (num == 8) numStr = "八";
  5950. else if (num == 9) numStr = "九";
  5951. else if (num == 10) numStr = "十";
  5952. return numStr;
  5953. }
  5954. /// <summary>
  5955. /// 公务信息 - 导出
  5956. /// </summary>
  5957. /// <param name="dto"></param>
  5958. /// <returns></returns>
  5959. [HttpPost]
  5960. public IActionResult ExportOfficialActivitiesInfo(ExportOfficialActivitiesInfoDto dto)
  5961. {
  5962. var jw = JsonView(false);
  5963. if (!ModelState.IsValid)
  5964. {
  5965. jw.Data = ModelState;
  5966. return Ok(jw);
  5967. }
  5968. var startTime_bool = DateTime.TryParse(dto.StartTime, out DateTime startTime_parse);
  5969. var endTime_bool = DateTime.TryParse(dto.EndTime, out DateTime endTime_parse);
  5970. if (startTime_parse > endTime_parse)
  5971. {
  5972. jw.Msg = "开始实际不能大于结束时间!";
  5973. return Ok(jw);
  5974. }
  5975. if (startTime_bool && endTime_bool)
  5976. {
  5977. //处理时间模板
  5978. var query_DB = _sqlSugar.Queryable<Res_OfficialActivities>()
  5979. .LeftJoin<Grp_DelegationInfo>((a, b) => a.DiId == b.Id)
  5980. .LeftJoin<Sys_Users>((a, b,c) => a.CreateUserId == c.Id)
  5981. .Where((a, b, c) => a.DiId == dto.GroupId &&
  5982. a.IsDel == 0 &&
  5983. !string.IsNullOrEmpty(a.Client) &&
  5984. Convert.ToDateTime(a.Date) >= startTime_parse && Convert.ToDateTime(a.Date) <= endTime_parse)
  5985. .WhereIF(dto.State != -1, (a, b, c) => a.ConfirmTheInvitation == dto.State)
  5986. .Select((a, b, c) => new
  5987. {
  5988. b.TeamName,
  5989. a.Client,
  5990. a.ConfirmTheInvitation,
  5991. a.Contact,
  5992. ConfirmTheInvitationStr = a.ConfirmTheInvitation == 1 ? "是" : "否",
  5993. a.Date,
  5994. a.Nature,
  5995. c.CnName,
  5996. a.Address,
  5997. CreateTime = a.CreateTime.ToString("yyyy-MM-dd HH:mm:ss")
  5998. })
  5999. .OrderBy(a => a.Date)
  6000. .ToList();
  6001. if (query_DB.Count > 0)
  6002. {
  6003. //打开excel
  6004. var ftpPath = AppSettingsHelper.Get("ExcelFtpPath");
  6005. var fileBasePath = AppSettingsHelper.Get("ExcelBasePath");
  6006. //var fileName = "公务出访信息.docx";
  6007. //创建数据源Table
  6008. DataTable dtSource = new DataTable();
  6009. dtSource.TableName = "TB";
  6010. var firstElem = query_DB.First();
  6011. //遍历firstElem所有属性
  6012. foreach (var item in firstElem.GetType().GetProperties())
  6013. {
  6014. dtSource.Columns.Add(item.Name, item.PropertyType);
  6015. }
  6016. foreach (var item in query_DB)
  6017. {
  6018. DataRow dr = dtSource.NewRow();
  6019. //遍历dtSource所有列头
  6020. foreach (DataColumn column in dtSource.Columns)
  6021. {
  6022. dr[column.ColumnName] = item.GetType().GetProperty(column.ColumnName)?.GetValue(item)?.ToString();
  6023. }
  6024. dtSource.Rows.Add(dr);
  6025. }
  6026. WorkbookDesigner designer = new WorkbookDesigner();
  6027. designer.Workbook = new Workbook(fileBasePath + ("Template/公务出访信息.xlsx"));
  6028. var title = query_DB.FirstOrDefault()?.TeamName ?? "";
  6029. title += "-公务出访信息(" + startTime_parse.ToString("yyyy年MM月dd日") + "-" + endTime_parse.ToString("yyyy年MM月dd日") + ")";
  6030. designer.SetDataSource("title", title);
  6031. designer.SetDataSource(dtSource);
  6032. designer.Process();
  6033. var dirName = "ExportOfficialActivitiesInfo/";
  6034. var serverPath = $"{fileBasePath}{dirName}";
  6035. var exportSerevePath = serverPath + title + ".xlsx";
  6036. var exportFtpPaht = ftpPath + dirName + title + ".xlsx";
  6037. // 验证文件夹是否存在
  6038. if (!System.IO.Directory.Exists(serverPath))
  6039. {
  6040. Directory.CreateDirectory(serverPath);
  6041. }
  6042. //保存文件
  6043. designer.Workbook.Save(exportSerevePath);
  6044. jw.Data = AppSettingsHelper.Get("ExcelBaseUrl") + exportFtpPaht;
  6045. jw.Code = 200;
  6046. jw.Msg = "获取成功!";
  6047. }
  6048. else
  6049. {
  6050. jw.Msg = "无公务信息!";
  6051. }
  6052. }
  6053. else
  6054. {
  6055. jw.Data = "时间格式错误!";
  6056. }
  6057. return Ok(jw);
  6058. }
  6059. #endregion
  6060. #region 请示数据库
  6061. /// <summary>
  6062. /// 查询请示数据库初始化
  6063. /// </summary>
  6064. /// <param name="dto"></param>
  6065. /// <returns></returns>
  6066. [HttpPost]
  6067. [ProducesResponseType(typeof(JsonView), StatusCodes.Status200OK)]
  6068. public async Task<IActionResult> QueryAskData(QueryAskDataDto dto)
  6069. {
  6070. Result groupData = await _askDataRep.QueryAskData(dto);
  6071. if (groupData.Code != 0)
  6072. {
  6073. return Ok(JsonView(false, groupData.Msg));
  6074. }
  6075. return Ok(JsonView(true, groupData.Msg, groupData.Data));
  6076. }
  6077. /// <summary>
  6078. /// 根据Id查询请示数据库单条数据
  6079. /// </summary>
  6080. /// <param name="dto"></param>
  6081. /// <returns></returns>
  6082. [HttpPost]
  6083. [ProducesResponseType(typeof(JsonView), StatusCodes.Status200OK)]
  6084. public async Task<IActionResult> QueryAskDataById(QueryAskDataByIdDto dto)
  6085. {
  6086. Res_AskData _AskData = await _sqlSugar.Queryable<Res_AskData>().FirstAsync(a => a.IsDel == 0 && a.Id == dto.id);
  6087. if (_AskData == null)
  6088. {
  6089. return Ok(JsonView(true, "暂无数据!", _AskData));
  6090. }
  6091. return Ok(JsonView(true, "查询成功!", _AskData));
  6092. }
  6093. /// <summary>
  6094. /// 请示数据库操作(Status:1.新增,2.修改)
  6095. /// </summary>
  6096. /// <param name="dto"></param>
  6097. /// <returns></returns>
  6098. [HttpPost]
  6099. [ProducesResponseType(typeof(JsonView), StatusCodes.Status200OK)]
  6100. public async Task<IActionResult> OpAskData(OpAskDataDto dto)
  6101. {
  6102. Result groupData = await _askDataRep.OpAskData(dto);
  6103. if (groupData.Code != 0)
  6104. {
  6105. return Ok(JsonView(false, groupData.Msg));
  6106. }
  6107. return Ok(JsonView(true, groupData.Msg, groupData.Data));
  6108. }
  6109. /// <summary>
  6110. /// 删除请示资料信息
  6111. /// </summary>
  6112. /// <param name="dto"></param>
  6113. /// <returns></returns>
  6114. [HttpPost]
  6115. [ProducesResponseType(typeof(JsonView), StatusCodes.Status200OK)]
  6116. public async Task<IActionResult> DelAskData(DelBaseDto dto)
  6117. {
  6118. var res = await _askDataRep.SoftDeleteByIdAsync<Res_AskData>(dto.Id.ToString(), dto.DeleteUserId);
  6119. if (!res)
  6120. {
  6121. return Ok(JsonView(false, "删除失败"));
  6122. }
  6123. return Ok(JsonView(true, "删除成功!"));
  6124. }
  6125. /// <summary>
  6126. /// 请示资料信息
  6127. /// 省外办下载
  6128. /// </summary>
  6129. /// <param name="dto"></param>
  6130. /// <returns></returns>
  6131. [HttpPost]
  6132. [ProducesResponseType(typeof(JsonView), StatusCodes.Status200OK)]
  6133. public async Task<IActionResult> AskDataProvinceFAOPFile(AskDataProvinceFileDto dto)
  6134. {
  6135. if (dto.DataIdItem.Length < 1) return Ok(JsonView(false, $"请传入有效的数据Id集合!"));
  6136. if (dto.DiId < 1) return Ok(JsonView(false, $"请传入有效的数据DiId参数!"));
  6137. //大写数字序号
  6138. string[] num = { "一", "二", "三", "四", "五", "六", "七", "八", "九", "十" ,
  6139. "十一","十二","十三","十四","十五","十六","十七","十八","十九","二十",
  6140. "二十一","二十二","二十三","二十四","二十五","二十六","二十七","二十八","二十九","三十",
  6141. "三十一","三十二","三十三","三十四","三十五","三十六","三十七","三十八","三十九","四十"};
  6142. //数据源
  6143. var idItem = dto.DataIdItem.OrderBy(x => x);
  6144. var askDatas = await _sqlSugar.Queryable<Res_AskData>().Where(x => x.IsDel == 0 && idItem.Contains(x.Id)).ToListAsync();
  6145. //书签名
  6146. string Area = "",
  6147. Official = "";
  6148. //表格数据源
  6149. DataTable dt = new DataTable();
  6150. dt.Columns.Add("Country", typeof(string));
  6151. dt.Columns.Add("TalkCase", typeof(string));
  6152. int id = 0;
  6153. int TalkcaseOrder = 0;
  6154. int RowOrder = 0;
  6155. //填充数据
  6156. for (int i = 0; i < askDatas.Count; i++)
  6157. {
  6158. var ad = askDatas[i];
  6159. Area = Area + ad.Country + ad.Area + "、";
  6160. Official = Official + ad.UnitName + "、";
  6161. //填充表格数据源
  6162. DataRow dr = dt.NewRow();
  6163. if (i > 0)
  6164. {
  6165. var adtemp = askDatas[i - 1];
  6166. if (ad.Country == adtemp.Country)
  6167. {
  6168. TalkcaseOrder++;
  6169. dt.Rows[RowOrder]["TalkCase"] = dt.Rows[RowOrder]["TalkCase"] + "\r\n" + (1 + TalkcaseOrder).ToString() + "、" + ad.TalkCase;
  6170. }
  6171. else
  6172. {
  6173. RowOrder++;
  6174. dr = dt.NewRow();
  6175. dr["Country"] = "(" + num[RowOrder] + ")" + ad.Country;
  6176. dr["TalkCase"] = "1、" + ad.TalkCase;
  6177. dt.Rows.Add(dr);
  6178. TalkcaseOrder = 0;
  6179. }
  6180. }
  6181. else
  6182. {
  6183. dr["Country"] = "(" + num[i] + ")" + ad.Country;
  6184. dr["TalkCase"] = "1、" + ad.TalkCase;
  6185. dt.Rows.Add(dr);
  6186. }
  6187. id = ad.Id;
  6188. }
  6189. //模板路径
  6190. var tempPath = AppSettingsHelper.Get("WordBasePath") + "Template/省外办出访请示模板.doc";
  6191. //载入模板
  6192. Aspose.Words.Document doc = new Aspose.Words.Document(tempPath);
  6193. #region 填充word内容
  6194. #region 填充请示内容
  6195. //利用DocumentBuilder处理表格或是书签
  6196. Aspose.Words.DocumentBuilder builder = new DocumentBuilder(doc);
  6197. string bookmark = "";
  6198. try
  6199. {
  6200. for (int i = 0; i < dt.Rows.Count; i++)
  6201. {
  6202. bookmark = bookmark
  6203. + dt.Rows[i]["Country"].ToString() + "\r\n"
  6204. + dt.Rows[i]["TalkCase"].ToString() + "\r\n";
  6205. }
  6206. }
  6207. catch
  6208. {
  6209. }
  6210. #endregion
  6211. #region 填充word模板书签内容
  6212. //团组信息
  6213. var di = await _sqlSugar.Queryable<Grp_DelegationInfo>().Where(x => x.IsDel == 0 && x.Id == dto.DiId).FirstAsync();
  6214. //接团客户名单实例
  6215. string client_sql = string.Format(@"
  6216. SELECT
  6217. tcl.Id,
  6218. tcl.DiId,
  6219. tcl.IsAccompany,
  6220. temp.*,
  6221. u.CnName AS Operator,
  6222. tcl.CreateTime AS OperatingTime
  6223. FROM
  6224. Grp_TourClientList tcl
  6225. LEFT JOIN (
  6226. SELECT
  6227. dc.Id AS DcId,
  6228. dc.LastName,
  6229. dc.FirstName,
  6230. ccom.CompanyFullName,
  6231. dc.Job,
  6232. cc.CertNo AS IDCardNo,
  6233. dc.Sex,
  6234. dc.BirthDay
  6235. FROM
  6236. Crm_DeleClient dc
  6237. LEFT JOIN Crm_CustomerCompany ccom ON dc.CrmCompanyId = ccom.Id
  6238. AND ccom.IsDel = 0
  6239. LEFT JOIN Crm_CustomerCert cc ON dc.Id = cc.DcId
  6240. AND cc.SdId = 773
  6241. AND cc.IsDel = 0
  6242. WHERE
  6243. dc.IsDel = 0
  6244. ) temp ON temp.DcId = tcl.ClientId
  6245. LEFT JOIN Sys_Users u ON tcl.CreateUserId = u.Id
  6246. WHERE
  6247. tcl.IsDel = 0
  6248. AND tcl.DiId = {0}", dto.DiId);
  6249. var clientList = await _sqlSugar.SqlQueryable<TourClientListDetailsView>(client_sql).ToListAsync();
  6250. //机票代码实例
  6251. DataTable airTable = GeneralMethod.GetTableByBlackCode(dto.DiId);
  6252. //三字码
  6253. var threeCodes = await _sqlSugar.Queryable<Res_ThreeCode>().Where(x => x.IsDel == 0).ToListAsync();
  6254. //团名
  6255. if (doc.Range.Bookmarks["TeamName"] != null)
  6256. {
  6257. Bookmark mark = doc.Range.Bookmarks["TeamName"];
  6258. mark.Text = di.TeamName;
  6259. }
  6260. //团长名
  6261. if (doc.Range.Bookmarks["ClientName2"] != null)
  6262. {
  6263. Bookmark mark = doc.Range.Bookmarks["ClientName2"];
  6264. if (clientList == null || clientList.Count == 0)
  6265. {
  6266. mark.Text = "【本团尚未录入客户名单,请联系主管!】";
  6267. }
  6268. else
  6269. {
  6270. mark.Text = clientList[0].CompanyFullName + clientList[0].Job + clientList[0].LastName + clientList[0].FirstName;
  6271. }
  6272. }
  6273. if (doc.Range.Bookmarks["ClientName"] != null)
  6274. {
  6275. Bookmark mark = doc.Range.Bookmarks["ClientName"];
  6276. if (clientList == null || clientList.Count == 0)
  6277. {
  6278. mark.Text = "【本团尚未录入客户名单,请联系主管!】";
  6279. }
  6280. else
  6281. {
  6282. mark.Text = clientList[0].CompanyFullName + clientList[0].Job + clientList[0].LastName + clientList[0].FirstName;
  6283. }
  6284. }
  6285. //跟团人数
  6286. if (doc.Range.Bookmarks["ClientNumber2"] != null)
  6287. {
  6288. Bookmark mark = doc.Range.Bookmarks["ClientNumber2"];
  6289. mark.Text = di.VisitPNumber.ToString();
  6290. }
  6291. if (doc.Range.Bookmarks["ClientNumber"] != null)
  6292. {
  6293. Bookmark mark = doc.Range.Bookmarks["ClientNumber"];
  6294. mark.Text = di.VisitPNumber.ToString();
  6295. }
  6296. //地区
  6297. if (doc.Range.Bookmarks["Area"] != null)
  6298. {
  6299. Bookmark mark = doc.Range.Bookmarks["Area"];
  6300. mark.Text = Area.TrimEnd('、');
  6301. }
  6302. if (doc.Range.Bookmarks["Area2"] != null)
  6303. {
  6304. Bookmark mark = doc.Range.Bookmarks["Area2"];
  6305. mark.Text = Area.TrimEnd('、');
  6306. }
  6307. //出访目的
  6308. if (doc.Range.Bookmarks["Destination2"] != null)
  6309. {
  6310. Bookmark mark = doc.Range.Bookmarks["Destination2"];
  6311. mark.Text = di.VisitPurpose;
  6312. }
  6313. if (doc.Range.Bookmarks["Destination"] != null)
  6314. {
  6315. Bookmark mark = doc.Range.Bookmarks["Destination"];
  6316. mark.Text = di.VisitPurpose;
  6317. }
  6318. //邀请方
  6319. if (doc.Range.Bookmarks["Official"] != null)
  6320. {
  6321. Bookmark mark = doc.Range.Bookmarks["Official"];
  6322. mark.Text = Official.TrimEnd('、');
  6323. }
  6324. //出访日期
  6325. if (doc.Range.Bookmarks["VisitDate"] != null)
  6326. {
  6327. Bookmark mark = doc.Range.Bookmarks["VisitDate"];
  6328. mark.Text = di.VisitStartDate.Year + "年"
  6329. + di.VisitStartDate.Month + "月"
  6330. + di.VisitStartDate.Day + "日"
  6331. + "至"
  6332. + di.VisitEndDate.Month + "月"
  6333. + di.VisitEndDate.Day + "日";
  6334. }
  6335. //请示范例
  6336. if (doc.Range.Bookmarks["Temp"] != null)
  6337. {
  6338. Bookmark mark = doc.Range.Bookmarks["Temp"];
  6339. mark.Text = bookmark;
  6340. }
  6341. //出访起止日期
  6342. if (doc.Range.Bookmarks["Duration"] != null)
  6343. {
  6344. Bookmark mark = doc.Range.Bookmarks["Duration"];
  6345. mark.Text = di.VisitStartDate.Year + "年"
  6346. + di.VisitStartDate.Month + "月"
  6347. + di.VisitStartDate.Day + "日"
  6348. + "至"
  6349. + di.VisitEndDate.Month + "月"
  6350. + di.VisitEndDate.Day + "日";
  6351. }
  6352. //出访天数
  6353. if (doc.Range.Bookmarks["Durationdays"] != null)
  6354. {
  6355. Bookmark mark = doc.Range.Bookmarks["Durationdays"];
  6356. mark.Text = di.VisitDays.ToString();
  6357. }
  6358. if (doc.Range.Bookmarks["Durationdays2"] != null)
  6359. {
  6360. Bookmark mark = doc.Range.Bookmarks["Durationdays2"];
  6361. mark.Text = di.VisitDays.ToString();
  6362. }
  6363. #region 根据黑屏代码获取国家呆的天数;获取路线
  6364. string result_CountryAndDay = "", result_Air = "";
  6365. if (airTable.Rows[airTable.Rows.Count - 1]["Day"].ToString() == "")
  6366. {
  6367. result_CountryAndDay = airTable.Rows[airTable.Rows.Count - 1]["Fliagtcode"].ToString();
  6368. result_Air = result_CountryAndDay;
  6369. }
  6370. else
  6371. {
  6372. string place_start, place_end;
  6373. DateTime time_start, time_end;
  6374. Res_ThreeCode three = null;
  6375. for (int i = 0; i < airTable.Rows.Count; i++)
  6376. {
  6377. //三字码实例
  6378. three = threeCodes.Find(x => x.Three == airTable.Rows[i]["Three"].ToString().Substring(0, 3));
  6379. if (three == null)
  6380. {
  6381. place_start = "【未收录三字码" + airTable.Rows[i]["Three"].ToString().Substring(0, 3) + ",请联系机票同事】";
  6382. }
  6383. else
  6384. {
  6385. if (string.IsNullOrEmpty(three.Country))
  6386. {
  6387. place_start = "【本三字码" + airTable.Rows[i]["Three"].ToString().Substring(0, 3) + "未录入国家信息,请联系机票同事】";
  6388. }
  6389. else
  6390. {
  6391. place_start = three.City;
  6392. }
  6393. }
  6394. three = threeCodes.Find(x => x.Three == airTable.Rows[i]["Three"].ToString().Substring(3, 3));
  6395. if (three == null)
  6396. {
  6397. place_end = "【未收录" + airTable.Rows[i]["Three"].ToString().Substring(0, 3) + ",请联系机票同事】";
  6398. }
  6399. else
  6400. {
  6401. if (string.IsNullOrEmpty(three.Country))
  6402. {
  6403. place_end = "【本三字码" + airTable.Rows[i]["Three"].ToString().Substring(0, 3) + "未录入国家信息,请联系机票同事】";
  6404. }
  6405. else
  6406. {
  6407. place_end = three.City;
  6408. }
  6409. }
  6410. result_Air += place_start + "一" + place_end + "一";
  6411. //获取国家和呆的天数
  6412. if (i > 0 && i < airTable.Rows.Count - 1)
  6413. {
  6414. time_start = Convert.ToDateTime(airTable.Rows[i - 1]["Day"]);
  6415. time_end = Convert.ToDateTime(airTable.Rows[i]["Day"]);
  6416. TimeSpan daycount = time_end.Subtract(time_start);
  6417. result_CountryAndDay += place_start + daycount.Days + "天" + ",";
  6418. }
  6419. }
  6420. }
  6421. #endregion
  6422. //每个国家呆的天数
  6423. if (doc.Range.Bookmarks["CountryAndDay"] != null)
  6424. {
  6425. Bookmark mark = doc.Range.Bookmarks["CountryAndDay"];
  6426. mark.Text = result_CountryAndDay.TrimEnd(',');
  6427. }
  6428. #region 出访路线城市字符串去重
  6429. result_Air = result_Air.TrimEnd('一');
  6430. string[] airTemp = result_Air.Split('一');
  6431. airTemp = airTemp.Distinct().ToArray();
  6432. result_Air = "";
  6433. foreach (string str in airTemp)
  6434. {
  6435. result_Air += str + "一";
  6436. }
  6437. result_Air = result_Air + airTemp[0];
  6438. #endregion
  6439. //出访路线
  6440. if (doc.Range.Bookmarks["Air"] != null)
  6441. {
  6442. Bookmark mark = doc.Range.Bookmarks["Air"];
  6443. mark.Text = result_Air;
  6444. }
  6445. #endregion
  6446. #region 客户名单
  6447. NodeCollection allTables = doc.GetChildNodes(NodeType.Table, true);
  6448. Aspose.Words.Tables.Table tableClient = allTables[0] as Aspose.Words.Tables.Table;
  6449. if (clientList == null || clientList.Count == 0)
  6450. {
  6451. builder.MoveToCell(0, 1, 0, 0);
  6452. builder.Write("【本团尚未录入客户名单,请联系主管!】");
  6453. }
  6454. else
  6455. {
  6456. for (int i = 0; i < clientList.Count; i++)
  6457. {
  6458. builder.MoveToCell(0, i + 1, 0, 0);
  6459. builder.Write((i + 1).ToString());
  6460. builder.MoveToCell(0, i + 1, 1, 0);
  6461. builder.Write(clientList[i].LastName.ToString() + clientList[i].FirstName.ToString());
  6462. builder.MoveToCell(0, i + 1, 2, 0);
  6463. builder.Write(clientList[i].Sex.ToString());
  6464. builder.MoveToCell(0, i + 1, 3, 0);
  6465. builder.Write(clientList[i].BirthDay);
  6466. builder.MoveToCell(0, i + 1, 4, 0);
  6467. builder.Write(clientList[i].CompanyFullName + clientList[i].Job.ToString());
  6468. }
  6469. }
  6470. while (tableClient.Rows.Count > clientList.Count + 1)
  6471. {
  6472. tableClient.Rows.RemoveAt(clientList.Count + 1);
  6473. }
  6474. #endregion
  6475. #region 商邀行程
  6476. string content = "", temp1 = "", temp2 = "";
  6477. //获取数据,放到datatable
  6478. var listapt = await _sqlSugar.Queryable<Grp_ApprovalTravel>()
  6479. .LeftJoin<Grp_ApprovalTravelDetails>((at, atd) => at.Id == atd.ParentId)
  6480. .Where((at, atd) => at.IsDel == 0 && at.Diid == dto.DiId)
  6481. .Select((at, atd) => new
  6482. {
  6483. at.Id,
  6484. at.Date,
  6485. atd.Time,
  6486. atd.Details
  6487. })
  6488. .ToListAsync();
  6489. if (listapt == null || listapt.Count == 0)
  6490. {
  6491. content = "尚未生成商邀行程";
  6492. }
  6493. else
  6494. {
  6495. for (int i = 0; i < listapt.Count; i++)
  6496. {
  6497. content = content + " " + listapt[i].Date + "\r\n";
  6498. temp1 = listapt[i].Time;
  6499. temp2 = listapt[i].Details;
  6500. if (!string.IsNullOrEmpty(temp1))
  6501. {
  6502. string[] str = temp1.Replace("\r\n", " ").Split(new char[] { ' ' }, StringSplitOptions.RemoveEmptyEntries);
  6503. string[] str2 = temp2.Replace("\r\n", " ").Split(new char[] { ' ' }, StringSplitOptions.RemoveEmptyEntries);
  6504. for (int j = 0; j < str.Length; j++)
  6505. {
  6506. content = content + " " + str[j] + str2[j] + "\r\n";
  6507. }
  6508. }
  6509. }
  6510. }
  6511. if (doc.Range.Bookmarks["Content"] != null)
  6512. {
  6513. Bookmark mark = doc.Range.Bookmarks["Content"];
  6514. mark.Text = content;
  6515. }
  6516. while (tableClient.Rows.Count > 1 + clientList.Count)
  6517. {
  6518. tableClient.Rows.RemoveAt(1 + clientList.Count);
  6519. }
  6520. #endregion
  6521. #endregion
  6522. string strFileName = $"/AskData/省外办出访请示{Guid.NewGuid().ToString()}.doc";
  6523. doc.Save(AppSettingsHelper.Get("WordBasePath") + strFileName); //"C:\\Server\\File\\OA2023\\Office\\Word" + strFileName
  6524. var url = AppSettingsHelper.Get("WordBaseUrl") + AppSettingsHelper.Get("WordFtpPath") + strFileName; //"C:\\Server\\File\\OA2023\\Office\\Word" + strFileName
  6525. return Ok(JsonView(true, "操作成功!", new { url = url }));
  6526. }
  6527. /// <summary>
  6528. /// 请示资料信息
  6529. /// 市外办下载
  6530. /// </summary>
  6531. /// <param name="dto"></param>
  6532. /// <returns></returns>
  6533. [HttpPost]
  6534. [ProducesResponseType(typeof(JsonView), StatusCodes.Status200OK)]
  6535. public async Task<IActionResult> AskDataCityFAOPFile(AskDataProvinceFileDto dto)
  6536. {
  6537. if (dto.DataIdItem.Length < 1) return Ok(JsonView(false, $"请传入有效的数据Id集合!"));
  6538. if (dto.DiId < 1) return Ok(JsonView(false, $"请传入有效的数据DiId参数!"));
  6539. //大写数字序号
  6540. string[] num = { "一", "二", "三", "四", "五", "六", "七", "八", "九", "十" ,
  6541. "十一","十二","十三","十四","十五","十六","十七","十八","十九","二十",
  6542. "二十一","二十二","二十三","二十四","二十五","二十六","二十七","二十八","二十九","三十",
  6543. "三十一","三十二","三十三","三十四","三十五","三十六","三十七","三十八","三十九","四十"};
  6544. //数据源
  6545. var idItem = dto.DataIdItem.OrderBy(x => x);
  6546. var askDatas = await _sqlSugar.Queryable<Res_AskData>().Where(x => x.IsDel == 0 && idItem.Contains(x.Id)).ToListAsync();
  6547. //书签名
  6548. string Area = "",
  6549. Official = "";
  6550. //表格数据源
  6551. DataTable dt = new DataTable();
  6552. dt.Columns.Add("Country", typeof(string));
  6553. dt.Columns.Add("TalkCase", typeof(string));
  6554. int id = 0;
  6555. int TalkcaseOrder = 0;
  6556. int RowOrder = 0;
  6557. //填充数据
  6558. for (int i = 0; i < askDatas.Count; i++)
  6559. {
  6560. var ad = askDatas[i];
  6561. Area = Area + ad.Country + ad.Area + "、";
  6562. Official = Official + ad.UnitName + "、";
  6563. //填充表格数据源
  6564. DataRow dr = dt.NewRow();
  6565. if (i > 0)
  6566. {
  6567. var adtemp = askDatas[i - 1];
  6568. if (ad.Country == adtemp.Country)
  6569. {
  6570. TalkcaseOrder++;
  6571. dt.Rows[RowOrder]["TalkCase"] = dt.Rows[RowOrder]["TalkCase"] + "\r\n" + (1 + TalkcaseOrder).ToString() + "、" + ad.TalkCase;
  6572. }
  6573. else
  6574. {
  6575. RowOrder++;
  6576. dr = dt.NewRow();
  6577. dr["Country"] = "(" + num[RowOrder] + ")" + ad.Country;
  6578. dr["TalkCase"] = "1、" + ad.TalkCase;
  6579. dt.Rows.Add(dr);
  6580. TalkcaseOrder = 0;
  6581. }
  6582. }
  6583. else
  6584. {
  6585. dr["Country"] = "(" + num[i] + ")" + ad.Country;
  6586. dr["TalkCase"] = "1、" + ad.TalkCase;
  6587. dt.Rows.Add(dr);
  6588. }
  6589. id = ad.Id;
  6590. }
  6591. //模板路径
  6592. var tempPath = AppSettingsHelper.Get("WordBasePath") + "Template/市外办出访请示.doc";
  6593. //载入模板
  6594. Aspose.Words.Document doc = new Aspose.Words.Document(tempPath);
  6595. #region 填充word内容
  6596. #region 填充请示内容
  6597. //利用DocumentBuilder处理表格或是书签
  6598. Aspose.Words.DocumentBuilder builder = new DocumentBuilder(doc);
  6599. string bookmark = "";
  6600. try
  6601. {
  6602. for (int i = 0; i < dt.Rows.Count; i++)
  6603. {
  6604. bookmark = bookmark
  6605. + dt.Rows[i]["Country"].ToString() + "\r\n"
  6606. + dt.Rows[i]["TalkCase"].ToString() + "\r\n";
  6607. }
  6608. }
  6609. catch
  6610. {
  6611. }
  6612. #endregion
  6613. #region 填充word模板书签内容
  6614. //团组信息
  6615. var di = await _sqlSugar.Queryable<Grp_DelegationInfo>().Where(x => x.IsDel == 0 && x.Id == dto.DiId).FirstAsync();
  6616. //接团客户名单实例
  6617. string client_sql = string.Format(@"
  6618. SELECT
  6619. tcl.Id,
  6620. tcl.DiId,
  6621. tcl.IsAccompany,
  6622. temp.*,
  6623. u.CnName AS Operator,
  6624. tcl.CreateTime AS OperatingTime
  6625. FROM
  6626. Grp_TourClientList tcl
  6627. LEFT JOIN (
  6628. SELECT
  6629. dc.Id AS DcId,
  6630. dc.LastName,
  6631. dc.FirstName,
  6632. ccom.CompanyFullName,
  6633. dc.Job,
  6634. cc.CertNo AS IDCardNo,
  6635. dc.Sex,
  6636. dc.BirthDay
  6637. FROM
  6638. Crm_DeleClient dc
  6639. LEFT JOIN Crm_CustomerCompany ccom ON dc.CrmCompanyId = ccom.Id
  6640. AND ccom.IsDel = 0
  6641. LEFT JOIN Crm_CustomerCert cc ON dc.Id = cc.DcId
  6642. AND cc.SdId = 773
  6643. AND cc.IsDel = 0
  6644. WHERE
  6645. dc.IsDel = 0
  6646. ) temp ON temp.DcId = tcl.ClientId
  6647. LEFT JOIN Sys_Users u ON tcl.CreateUserId = u.Id
  6648. WHERE
  6649. tcl.IsDel = 0
  6650. AND tcl.DiId = {0}", dto.DiId);
  6651. var clientList = await _sqlSugar.SqlQueryable<TourClientListDetailsView>(client_sql).ToListAsync();
  6652. //机票代码实例
  6653. DataTable airTable = GeneralMethod.GetTableByBlackCode(dto.DiId);
  6654. //三字码
  6655. var threeCodes = await _sqlSugar.Queryable<Res_ThreeCode>().Where(x => x.IsDel == 0).ToListAsync();
  6656. //团名
  6657. if (doc.Range.Bookmarks["TeamName"] != null)
  6658. {
  6659. Bookmark mark = doc.Range.Bookmarks["TeamName"];
  6660. mark.Text = di.TeamName;
  6661. }
  6662. //团长名
  6663. if (doc.Range.Bookmarks["ClientName2"] != null)
  6664. {
  6665. Bookmark mark = doc.Range.Bookmarks["ClientName2"];
  6666. if (clientList == null || clientList.Count == 0)
  6667. {
  6668. mark.Text = "【本团尚未录入客户名单,请联系主管!】";
  6669. }
  6670. else
  6671. {
  6672. mark.Text = clientList[0].CompanyFullName + clientList[0].Job + clientList[0].LastName + clientList[0].FirstName;
  6673. }
  6674. }
  6675. if (doc.Range.Bookmarks["ClientName"] != null)
  6676. {
  6677. Bookmark mark = doc.Range.Bookmarks["ClientName"];
  6678. if (clientList == null || clientList.Count == 0)
  6679. {
  6680. mark.Text = "【本团尚未录入客户名单,请联系主管!】";
  6681. }
  6682. else
  6683. {
  6684. mark.Text = clientList[0].CompanyFullName + clientList[0].Job + clientList[0].LastName + clientList[0].FirstName;
  6685. }
  6686. }
  6687. //跟团人数
  6688. if (doc.Range.Bookmarks["ClientNumber2"] != null)
  6689. {
  6690. Bookmark mark = doc.Range.Bookmarks["ClientNumber2"];
  6691. mark.Text = di.VisitPNumber.ToString();
  6692. }
  6693. if (doc.Range.Bookmarks["ClientNumber"] != null)
  6694. {
  6695. Bookmark mark = doc.Range.Bookmarks["ClientNumber"];
  6696. mark.Text = di.VisitPNumber.ToString();
  6697. }
  6698. //地区
  6699. if (doc.Range.Bookmarks["Area"] != null)
  6700. {
  6701. Bookmark mark = doc.Range.Bookmarks["Area"];
  6702. mark.Text = Area.TrimEnd('、');
  6703. }
  6704. if (doc.Range.Bookmarks["Area2"] != null)
  6705. {
  6706. Bookmark mark = doc.Range.Bookmarks["Area2"];
  6707. mark.Text = Area.TrimEnd('、');
  6708. }
  6709. //出访目的
  6710. if (doc.Range.Bookmarks["Destination2"] != null)
  6711. {
  6712. Bookmark mark = doc.Range.Bookmarks["Destination2"];
  6713. mark.Text = di.VisitPurpose;
  6714. }
  6715. if (doc.Range.Bookmarks["Destination"] != null)
  6716. {
  6717. Bookmark mark = doc.Range.Bookmarks["Destination"];
  6718. mark.Text = di.VisitPurpose;
  6719. }
  6720. //邀请方
  6721. if (doc.Range.Bookmarks["Official"] != null)
  6722. {
  6723. Bookmark mark = doc.Range.Bookmarks["Official"];
  6724. mark.Text = Official.TrimEnd('、');
  6725. }
  6726. //出访日期
  6727. if (doc.Range.Bookmarks["VisitDate"] != null)
  6728. {
  6729. Bookmark mark = doc.Range.Bookmarks["VisitDate"];
  6730. mark.Text = di.VisitStartDate.Year + "年"
  6731. + di.VisitStartDate.Month + "月"
  6732. + di.VisitStartDate.Day + "日"
  6733. + "至"
  6734. + di.VisitEndDate.Month + "月"
  6735. + di.VisitEndDate.Day + "日";
  6736. }
  6737. //请示范例
  6738. if (doc.Range.Bookmarks["Temp"] != null)
  6739. {
  6740. Bookmark mark = doc.Range.Bookmarks["Temp"];
  6741. mark.Text = bookmark;
  6742. }
  6743. //出访起止日期
  6744. if (doc.Range.Bookmarks["Duration"] != null)
  6745. {
  6746. Bookmark mark = doc.Range.Bookmarks["Duration"];
  6747. mark.Text = di.VisitStartDate.Year + "年"
  6748. + di.VisitStartDate.Month + "月"
  6749. + di.VisitStartDate.Day + "日"
  6750. + "至"
  6751. + di.VisitEndDate.Month + "月"
  6752. + di.VisitEndDate.Day + "日";
  6753. }
  6754. //出访天数
  6755. if (doc.Range.Bookmarks["Durationdays"] != null)
  6756. {
  6757. Bookmark mark = doc.Range.Bookmarks["Durationdays"];
  6758. mark.Text = di.VisitDays.ToString();
  6759. }
  6760. if (doc.Range.Bookmarks["Durationdays2"] != null)
  6761. {
  6762. Bookmark mark = doc.Range.Bookmarks["Durationdays2"];
  6763. mark.Text = di.VisitDays.ToString();
  6764. }
  6765. #region 根据黑屏代码获取国家呆的天数;获取路线
  6766. string result_CountryAndDay = "", result_Air = "";
  6767. if (airTable.Rows[airTable.Rows.Count - 1]["Day"].ToString() == "")
  6768. {
  6769. result_CountryAndDay = airTable.Rows[airTable.Rows.Count - 1]["Fliagtcode"].ToString();
  6770. result_Air = result_CountryAndDay;
  6771. }
  6772. else
  6773. {
  6774. string place_start, place_end;
  6775. DateTime time_start, time_end;
  6776. Res_ThreeCode three = null;
  6777. for (int i = 0; i < airTable.Rows.Count; i++)
  6778. {
  6779. //三字码实例
  6780. three = threeCodes.Find(x => x.Three == airTable.Rows[i]["Three"].ToString().Substring(0, 3));
  6781. if (three == null)
  6782. {
  6783. place_start = "【未收录三字码" + airTable.Rows[i]["Three"].ToString().Substring(0, 3) + ",请联系机票同事】";
  6784. }
  6785. else
  6786. {
  6787. if (string.IsNullOrEmpty(three.Country))
  6788. {
  6789. place_start = "【本三字码" + airTable.Rows[i]["Three"].ToString().Substring(0, 3) + "未录入国家信息,请联系机票同事】";
  6790. }
  6791. else
  6792. {
  6793. place_start = three.City;
  6794. }
  6795. }
  6796. three = threeCodes.Find(x => x.Three == airTable.Rows[i]["Three"].ToString().Substring(3, 3));
  6797. if (three == null)
  6798. {
  6799. place_end = "【未收录" + airTable.Rows[i]["Three"].ToString().Substring(0, 3) + ",请联系机票同事】";
  6800. }
  6801. else
  6802. {
  6803. if (string.IsNullOrEmpty(three.Country))
  6804. {
  6805. place_end = "【本三字码" + airTable.Rows[i]["Three"].ToString().Substring(0, 3) + "未录入国家信息,请联系机票同事】";
  6806. }
  6807. else
  6808. {
  6809. place_end = three.City;
  6810. }
  6811. }
  6812. result_Air += place_start + "一" + place_end + "一";
  6813. //获取国家和呆的天数
  6814. if (i > 0 && i < airTable.Rows.Count - 1)
  6815. {
  6816. time_start = Convert.ToDateTime(airTable.Rows[i - 1]["Day"]);
  6817. time_end = Convert.ToDateTime(airTable.Rows[i]["Day"]);
  6818. TimeSpan daycount = time_end.Subtract(time_start);
  6819. result_CountryAndDay += place_start + daycount.Days + "天" + ",";
  6820. }
  6821. }
  6822. }
  6823. #endregion
  6824. //每个国家呆的天数
  6825. if (doc.Range.Bookmarks["CountryAndDay"] != null)
  6826. {
  6827. Bookmark mark = doc.Range.Bookmarks["CountryAndDay"];
  6828. mark.Text = result_CountryAndDay.TrimEnd(',');
  6829. }
  6830. #region 出访路线城市字符串去重
  6831. result_Air = result_Air.TrimEnd('一');
  6832. string[] airTemp = result_Air.Split('一');
  6833. airTemp = airTemp.Distinct().ToArray();
  6834. result_Air = "";
  6835. foreach (string str in airTemp)
  6836. {
  6837. result_Air += str + "一";
  6838. }
  6839. result_Air = result_Air + airTemp[0];
  6840. #endregion
  6841. //出访路线
  6842. if (doc.Range.Bookmarks["Air"] != null)
  6843. {
  6844. Bookmark mark = doc.Range.Bookmarks["Air"];
  6845. mark.Text = result_Air;
  6846. }
  6847. #endregion
  6848. #region 客户名单
  6849. NodeCollection allTables = doc.GetChildNodes(NodeType.Table, true);
  6850. Aspose.Words.Tables.Table tableClient = allTables[0] as Aspose.Words.Tables.Table;
  6851. if (clientList == null || clientList.Count == 0)
  6852. {
  6853. builder.MoveToCell(0, 1, 0, 0);
  6854. builder.Write("【本团尚未录入客户名单,请联系主管!】");
  6855. }
  6856. else
  6857. {
  6858. for (int i = 0; i < clientList.Count; i++)
  6859. {
  6860. builder.MoveToCell(0, i + 1, 0, 0);
  6861. builder.Write((i + 1).ToString());
  6862. builder.MoveToCell(0, i + 1, 1, 0);
  6863. builder.Write(clientList[i].LastName.ToString() + clientList[i].FirstName.ToString());
  6864. builder.MoveToCell(0, i + 1, 2, 0);
  6865. builder.Write(clientList[i].Sex.ToString());
  6866. builder.MoveToCell(0, i + 1, 3, 0);
  6867. builder.Write(clientList[i].BirthDay);
  6868. builder.MoveToCell(0, i + 1, 4, 0);
  6869. builder.Write(clientList[i].CompanyFullName + clientList[i].Job.ToString());
  6870. }
  6871. }
  6872. while (tableClient.Rows.Count > clientList.Count + 1)
  6873. {
  6874. tableClient.Rows.RemoveAt(clientList.Count + 1);
  6875. }
  6876. #endregion
  6877. #region 商邀行程
  6878. string content = "", temp1 = "", temp2 = "";
  6879. //获取数据,放到datatable
  6880. var listapt = await _sqlSugar.Queryable<Grp_ApprovalTravel>()
  6881. .LeftJoin<Grp_ApprovalTravelDetails>((at, atd) => at.Id == atd.ParentId)
  6882. .Where((at, atd) => at.IsDel == 0 && at.Diid == dto.DiId)
  6883. .Select((at, atd) => new
  6884. {
  6885. at.Id,
  6886. at.Date,
  6887. atd.Time,
  6888. atd.Details
  6889. })
  6890. .ToListAsync();
  6891. if (listapt == null || listapt.Count == 0)
  6892. {
  6893. content = "尚未生成商邀行程";
  6894. }
  6895. else
  6896. {
  6897. for (int i = 0; i < listapt.Count; i++)
  6898. {
  6899. content = content + " " + listapt[i].Date + "\r\n";
  6900. temp1 = listapt[i].Time;
  6901. temp2 = listapt[i].Details;
  6902. if (!string.IsNullOrEmpty(temp1))
  6903. {
  6904. string[] str = temp1.Replace("\r\n", " ").Split(new char[] { ' ' }, StringSplitOptions.RemoveEmptyEntries);
  6905. string[] str2 = temp2.Replace("\r\n", " ").Split(new char[] { ' ' }, StringSplitOptions.RemoveEmptyEntries);
  6906. for (int j = 0; j < str.Length; j++)
  6907. {
  6908. content = content + " " + str[j] + str2[j] + "\r\n";
  6909. }
  6910. }
  6911. }
  6912. }
  6913. if (doc.Range.Bookmarks["Content"] != null)
  6914. {
  6915. Bookmark mark = doc.Range.Bookmarks["Content"];
  6916. mark.Text = content;
  6917. }
  6918. while (tableClient.Rows.Count > 1 + clientList.Count)
  6919. {
  6920. tableClient.Rows.RemoveAt(1 + clientList.Count);
  6921. }
  6922. #endregion
  6923. #endregion
  6924. string strFileName = $"/AskData/市外办出访请示{Guid.NewGuid().ToString()}.doc";
  6925. doc.Save(AppSettingsHelper.Get("WordBasePath") + strFileName); //"C:\\Server\\File\\OA2023\\Office\\Word" + strFileName
  6926. var url = AppSettingsHelper.Get("WordBaseUrl") + AppSettingsHelper.Get("WordFtpPath") + strFileName; //"C:\\Server\\File\\OA2023\\Office\\Word" + strFileName
  6927. return Ok(JsonView(true, "操作成功!", new { url = url }));
  6928. }
  6929. #endregion
  6930. #region 机票黑屏代码
  6931. /// <summary>
  6932. /// 根据团组Id查询黑屏代码列表
  6933. /// </summary>
  6934. /// <param name="dto"></param>
  6935. /// <returns></returns>
  6936. [HttpPost]
  6937. [ProducesResponseType(typeof(JsonView), StatusCodes.Status200OK)]
  6938. public async Task<IActionResult> QueryTicketBlackCodeByDiId(QueryTicketBlackCodeByDiIdDto dto)
  6939. {
  6940. Result groupData = await _ticketBlackCodeRep.QueryTicketBlackCodeByDiId(dto);
  6941. if (groupData.Code != 0)
  6942. {
  6943. return Ok(JsonView(false, groupData.Msg));
  6944. }
  6945. return Ok(JsonView(true, groupData.Msg, groupData.Data));
  6946. }
  6947. /// <summary>
  6948. /// 根据黑屏代码数据Id查询信息
  6949. /// </summary>
  6950. /// <param name="dto"></param>
  6951. /// <returns></returns>
  6952. [HttpPost]
  6953. [ProducesResponseType(typeof(JsonView), StatusCodes.Status200OK)]
  6954. public async Task<IActionResult> QueryTicketBlackCodeById(QueryTicketBlackCodeByIdDto dto)
  6955. {
  6956. Result groupData = await _ticketBlackCodeRep.QueryTicketBlackCodeById(dto);
  6957. if (groupData.Code != 0)
  6958. {
  6959. return Ok(JsonView(false, groupData.Msg));
  6960. }
  6961. return Ok(JsonView(true, groupData.Msg, groupData.Data));
  6962. }
  6963. /// <summary>
  6964. /// 黑屏代码操作(Status:1.新增,2.修改)
  6965. /// </summary>
  6966. /// <param name="dto"></param>
  6967. /// <returns></returns>
  6968. [HttpPost]
  6969. [ProducesResponseType(typeof(JsonView), StatusCodes.Status200OK)]
  6970. public async Task<IActionResult> OpTicketBlackCode(OpTicketBlackCodeDto dto)
  6971. {
  6972. Result groupData = await _ticketBlackCodeRep.OpTicketBlackCode(dto);
  6973. if (groupData.Code != 0)
  6974. {
  6975. return Ok(JsonView(false, groupData.Msg));
  6976. }
  6977. try
  6978. {
  6979. var tongzhi = _ticketBlackCodeRep.DescBlackToVisa(dto.DiId);
  6980. if (tongzhi.Code == 0)
  6981. {
  6982. var info = tongzhi.Data.GetType().GetProperty("info").GetValue(tongzhi.Data) as List<string>;
  6983. var data = tongzhi.Data.GetType().GetProperty("data").GetValue(tongzhi.Data) as List<CountryDataTime>;
  6984. if (data != null)
  6985. await AppNoticeLibrary.SendUserMsg_blackAirInfo_ToVisaUser(dto.DiId, data);
  6986. }
  6987. //行程代码变更通知
  6988. await AppNoticeLibrary.SendUserMsg_GroupShare_ToDP(dto.DiId, dto.CreateUserId);
  6989. }
  6990. catch (Exception)
  6991. {
  6992. throw;
  6993. }
  6994. return Ok(JsonView(true, groupData.Msg, groupData.Data));
  6995. }
  6996. /// <summary>
  6997. /// 删除黑屏代码
  6998. /// </summary>
  6999. /// <param name="dto"></param>
  7000. /// <returns></returns>
  7001. [HttpPost]
  7002. [ProducesResponseType(typeof(JsonView), StatusCodes.Status200OK)]
  7003. public async Task<IActionResult> DelTicketBlackCode(DelBaseDto dto)
  7004. {
  7005. var res = await _ticketBlackCodeRep.SoftDeleteByIdAsync<Air_TicketBlackCode>(dto.Id.ToString(), dto.DeleteUserId);
  7006. if (!res)
  7007. {
  7008. return Ok(JsonView(false, "删除失败"));
  7009. }
  7010. return Ok(JsonView(true, "删除成功!"));
  7011. }
  7012. #endregion
  7013. #region 翻译人员
  7014. /// <summary>
  7015. /// 翻译人员库
  7016. /// Init
  7017. /// </summary>
  7018. /// <returns></returns>
  7019. [HttpGet]
  7020. [ProducesResponseType(typeof(JsonView), StatusCodes.Status200OK)]
  7021. public async Task<IActionResult> TranslatorLibraryInit()
  7022. {
  7023. var currencyData = await _sqlSugar.Queryable<Sys_SetData>()
  7024. .Where(x => x.IsDel == 0 && x.STid == 66)
  7025. .Select(x => new
  7026. {
  7027. x.Id,
  7028. x.Name,
  7029. })
  7030. .ToListAsync();
  7031. var officialDutyData = await _sqlSugar.Queryable<Res_OfficialActivities>()
  7032. .LeftJoin<Grp_DelegationInfo>((oa, di) => oa.DiId == di.Id)
  7033. .Where((oa, di) => oa.IsDel == 0)
  7034. .Select((oa, di) => new
  7035. {
  7036. oa.Id,
  7037. oa.Client,
  7038. oa.DiId,
  7039. di.TeamName
  7040. })
  7041. .ToListAsync();
  7042. var officialDutyData1 = officialDutyData.GroupBy(x => x.DiId)
  7043. .Select(g => new
  7044. {
  7045. id = g.Key,
  7046. label = g.FirstOrDefault()?.TeamName ?? "-",
  7047. children = g.Select(g1 => new { id = g1.Id, label = g1.Client }).ToList()
  7048. })
  7049. .ToList();
  7050. var view = new
  7051. {
  7052. currencyData,
  7053. officialDutyData = officialDutyData1
  7054. };
  7055. return Ok(JsonView(view));
  7056. }
  7057. /// <summary>
  7058. /// 翻译人员库
  7059. /// 详情
  7060. /// </summary>
  7061. /// <param name="id"></param>
  7062. /// <returns></returns>
  7063. [HttpGet("id")]
  7064. [ProducesResponseType(typeof(JsonView), StatusCodes.Status200OK)]
  7065. public async Task<IActionResult> TranslatorLibraryInfo(int id)
  7066. {
  7067. if (id < 1) return Ok(JsonView(false, "请传入有效的Id!"));
  7068. var info = await _translatorRep.Query(x => x.Id == id).FirstAsync();
  7069. EncryptionProcessor.DecryptProperties(info);
  7070. var view = _mapper.Map<TranslatorLibraryInfoView>(info);
  7071. //var data1 = await _sqlSugar.Queryable<Grp_OfficialDutyLinkTranslator>().Where(x => x.TranslatorId == id).Select(x => x.Id).ToArrayAsync();
  7072. view.OfficialDutyIdItem = await _sqlSugar
  7073. .Queryable<Grp_OfficialDutyLinkTranslator>()
  7074. .Where(x => x.IsDel == 0 && x.TranslatorId == id)
  7075. .Select(x => x.OfficialDutyId)
  7076. .ToArrayAsync();
  7077. return Ok(JsonView(view));
  7078. }
  7079. /// <summary>
  7080. /// 翻译人员库
  7081. /// Item
  7082. /// </summary>
  7083. /// <param name="dto"></param>
  7084. /// <returns></returns>
  7085. [HttpPost]
  7086. [ProducesResponseType(typeof(JsonView), StatusCodes.Status200OK)]
  7087. public async Task<IActionResult> TranslatorLibraryItem(TranslatorLibraryItemDto dto)
  7088. {
  7089. if (dto.PortType < 1 || dto.PortType > 3) return Ok(JsonView(false, MsgTips.Port));
  7090. var name = AesEncryptionHelper.Encrypt(dto.Name);
  7091. RefAsync<int> total = 0;
  7092. var view = await _sqlSugar.Queryable<Res_TranslatorLibrary>()
  7093. .Where(x => x.IsDel == 0)
  7094. .WhereIF(!string.IsNullOrEmpty(name), x => x.Name.Contains(name))
  7095. .Select(x => new TranslatorLibraryItemView()
  7096. {
  7097. Id = x.Id,
  7098. Area = x.Area,
  7099. Name = x.Name,
  7100. Sex = x.Sex == 0 ? "未设置" : x.Sex == 1 ? "男" : "女",
  7101. Tel = x.Tel,
  7102. Email = x.Email,
  7103. WechatNo = x.WechatNo,
  7104. Language = x.Language,
  7105. Price = x.Price,
  7106. Currency = SqlFunc.Subqueryable<Sys_SetData>().Where(x1 => x1.Id == x.Currency).Select(x1 => x1.Name) ?? "-",
  7107. CreateUserName = SqlFunc.Subqueryable<Sys_Users>().Where(x1 => x1.Id == x.CreateUserId).Select(x1 => x1.CnName) ?? "-",
  7108. LastUpddateTime = x.LastUpdateTime,
  7109. CreateTime = x.CreateTime,
  7110. Remark = x.Remark
  7111. })
  7112. .OrderByDescending(x => x.CreateTime)
  7113. .ToPageListAsync(dto.PageIndex, dto.PageSize, total);
  7114. foreach (var item in view) EncryptionProcessor.DecryptProperties(item);
  7115. return Ok(JsonView(view, total));
  7116. }
  7117. /// <summary>
  7118. /// 翻译人员库
  7119. /// OP
  7120. /// </summary>
  7121. /// <param name="dto"></param>
  7122. /// <returns></returns>
  7123. [HttpPost]
  7124. [ProducesResponseType(typeof(JsonView), StatusCodes.Status200OK)]
  7125. public async Task<IActionResult> TranslatorLibraryOp([FromForm] TranslatorLibraryOpDto dto)
  7126. {
  7127. if (dto.PortType < 1 || dto.PortType > 3) return Ok(JsonView(false, MsgTips.Port));
  7128. if (dto.Status < 1 || dto.Status > 2) return Ok(JsonView(false, MsgTips.Status));
  7129. var currUserInfo = JwtHelper.SerializeJwt(HttpContext.Request.Headers.Authorization);
  7130. if (currUserInfo == null) return Ok(JsonView(false, "请传入token!"));
  7131. var fileNames = new List<string>();
  7132. //D:\FTP\File\OA2023\Office\GrpFile\TranslatorLibrary
  7133. var fileBasePath = $"{AppSettingsHelper.Get("GrpFileBasePath")}TranslatorLibrary";
  7134. #region 保存文件
  7135. if (dto.Files != null)
  7136. {
  7137. if (!System.IO.Directory.Exists(fileBasePath))
  7138. {
  7139. System.IO.Directory.CreateDirectory(fileBasePath);
  7140. }
  7141. foreach (var item in dto.Files)
  7142. {
  7143. if (item.Length < 1) continue;
  7144. var file = item;
  7145. // 将文件保存到指定位置
  7146. var filePath = Path.Combine(fileBasePath, file.FileName);
  7147. using (var stream = new FileStream(filePath, FileMode.Create))
  7148. {
  7149. await file.CopyToAsync(stream);
  7150. }
  7151. //验证文件是否上传成功
  7152. if (!System.IO.File.Exists(filePath))
  7153. {
  7154. foreach (var filePathStr in fileNames)
  7155. {
  7156. System.IO.File.Delete(Path.Combine(fileBasePath, filePathStr));
  7157. }
  7158. return Ok(JsonView(false, "文件上传失败!"));
  7159. }
  7160. fileNames.Add(file.FileName);
  7161. }
  7162. }
  7163. #endregion
  7164. #region 参数处理
  7165. EncryptionProcessor.EncryptProperties(dto);
  7166. var translatorInfo = _mapper.Map<Res_TranslatorLibrary>(dto);
  7167. translatorInfo.LastUpdateTime = translatorInfo.CreateTime;
  7168. translatorInfo.CreateUserId = currUserInfo.UserId;
  7169. #endregion
  7170. _sqlSugar.BeginTran();
  7171. if (dto.Status == 1)
  7172. {
  7173. //验证重复
  7174. var info = await _translatorRep.Query(x => x.Area.Equals(dto.Area) && x.Name.Equals(dto.Name)).FirstAsync();
  7175. if (info != null) return Ok(JsonView(false, "该条数据已存在!"));
  7176. if (fileNames.Count > 0)
  7177. {
  7178. translatorInfo.Files = AesEncryptionHelper.Encrypt(JsonConvert.SerializeObject(fileNames));
  7179. }
  7180. var id = await _translatorRep.AddAsync(translatorInfo);
  7181. if (id > 0)
  7182. {
  7183. #region 新增(公务信息关联翻译人员) 关联信息
  7184. if (dto.officialDutyIdItem != null && dto.officialDutyIdItem.Length > 0)
  7185. {
  7186. var officialDutyLinkTranslators = new List<Grp_OfficialDutyLinkTranslator>();
  7187. foreach (var officialDutyId in dto.officialDutyIdItem)
  7188. {
  7189. officialDutyLinkTranslators.Add(new Grp_OfficialDutyLinkTranslator()
  7190. {
  7191. TranslatorId = id,
  7192. OfficialDutyId = officialDutyId,
  7193. CreateUserId = currUserInfo.UserId,
  7194. Remark = $"翻译人员库-->添加"
  7195. });
  7196. }
  7197. if (officialDutyLinkTranslators.Count > 0)
  7198. {
  7199. await _sqlSugar.Insertable(officialDutyLinkTranslators).ExecuteCommandAsync();
  7200. }
  7201. }
  7202. #endregion
  7203. _sqlSugar.CommitTran();
  7204. return Ok(JsonView(true));
  7205. }
  7206. }
  7207. else if (dto.Status == 2)
  7208. {
  7209. var info = await _translatorRep.Query(x => x.Id == dto.Id).FirstAsync();
  7210. if (info != null)
  7211. {
  7212. if (!string.IsNullOrEmpty(info.Files))
  7213. {
  7214. try
  7215. {
  7216. var selectFiles = JsonConvert.DeserializeObject<List<string>>(info.Files);
  7217. fileNames.AddRange(selectFiles);
  7218. }
  7219. catch (Exception)
  7220. {
  7221. }
  7222. }
  7223. }
  7224. if (fileNames.Count > 0)
  7225. {
  7226. translatorInfo.Files = AesEncryptionHelper.Encrypt(JsonConvert.SerializeObject(fileNames));
  7227. }
  7228. translatorInfo.LastUpdateTime = DateTime.Now;
  7229. var upd = await _translatorRep
  7230. .UpdateAsync(x => x.Id == dto.Id, x => new Res_TranslatorLibrary()
  7231. {
  7232. Area = translatorInfo.Area,
  7233. Name = translatorInfo.Name,
  7234. Sex = translatorInfo.Sex,
  7235. Photo = translatorInfo.Photo,
  7236. Tel = translatorInfo.Tel,
  7237. Email = translatorInfo.Email,
  7238. WechatNo = translatorInfo.WechatNo,
  7239. OtherSocialAccounts = translatorInfo.OtherSocialAccounts,
  7240. Language = translatorInfo.Language,
  7241. Price = translatorInfo.Price,
  7242. Currency = translatorInfo.Currency,
  7243. Remark = translatorInfo.Remark,
  7244. Files = translatorInfo.Files,
  7245. LastUpdateTime = translatorInfo.LastUpdateTime,
  7246. LastUpdateUserId = translatorInfo.LastUpdateUserId,
  7247. });
  7248. if (upd)
  7249. {
  7250. #region 更新(公务信息关联翻译人员) 关联信息
  7251. var officialDutyLinkTranslators_select = await _sqlSugar.Queryable<Grp_OfficialDutyLinkTranslator>()
  7252. .Where(x => x.IsDel == 0 && x.TranslatorId == dto.Id)
  7253. .ToListAsync();
  7254. //删除
  7255. var officialDutyLinkTranslatorIds = officialDutyLinkTranslators_select.Select(x => x.Id).ToList();
  7256. if (officialDutyLinkTranslatorIds.Count > 0)
  7257. {
  7258. await _sqlSugar.Updateable<Grp_OfficialDutyLinkTranslator>()
  7259. .SetColumnsIF(officialDutyLinkTranslatorIds.Count > 0, x => new Grp_OfficialDutyLinkTranslator()
  7260. {
  7261. DeleteUserId = currUserInfo.UserId,
  7262. DeleteTime = DateTime.Now.ToString("yyyy-MM-dd HH:mm:ss"),
  7263. IsDel = 1,
  7264. Remark = $"翻译人员库-->删除"
  7265. })
  7266. .Where(x => officialDutyLinkTranslatorIds.Contains(x.Id))
  7267. .ExecuteCommandAsync();
  7268. }
  7269. if (dto.officialDutyIdItem != null && dto.officialDutyIdItem.Length > 0)
  7270. {
  7271. //添加
  7272. var officialDutyLinkTranslators = new List<Grp_OfficialDutyLinkTranslator>();
  7273. foreach (var officialDutyId in dto.officialDutyIdItem)
  7274. {
  7275. officialDutyLinkTranslators.Add(new Grp_OfficialDutyLinkTranslator()
  7276. {
  7277. TranslatorId = dto.Id,
  7278. OfficialDutyId = officialDutyId,
  7279. CreateUserId = currUserInfo.UserId,
  7280. Remark = $"翻译人员库-->更新"
  7281. });
  7282. }
  7283. if (officialDutyLinkTranslators.Count > 0)
  7284. {
  7285. await _sqlSugar.Insertable(officialDutyLinkTranslators).ExecuteCommandAsync();
  7286. }
  7287. }
  7288. #endregion
  7289. _sqlSugar.CommitTran();
  7290. return Ok(JsonView(true));
  7291. }
  7292. }
  7293. _sqlSugar.RollbackTran();
  7294. return Ok(JsonView(false));
  7295. }
  7296. /// <summary>
  7297. /// 翻译人员库
  7298. /// Del
  7299. /// </summary>
  7300. /// <param name="id"></param>
  7301. /// <returns></returns>
  7302. [HttpDelete("id")]
  7303. [ProducesResponseType(typeof(JsonView), StatusCodes.Status200OK)]
  7304. public async Task<IActionResult> TranslatorLibraryDel(int id)
  7305. {
  7306. if (id < 1) return Ok(JsonView(false, MsgTips.Id));
  7307. var currUserInfo = JwtHelper.SerializeJwt(HttpContext.Request.Headers.Authorization);
  7308. if (currUserInfo == null) return Ok(JsonView(false, "请传入token!"));
  7309. _sqlSugar.BeginTran();
  7310. var upd = await _translatorRep.SoftDeleteByIdAsync<Res_TranslatorLibrary>(id.ToString(), currUserInfo.UserId);
  7311. if (!upd)
  7312. {
  7313. _sqlSugar.RollbackTran();
  7314. return Ok(JsonView(false));
  7315. }
  7316. #region 删除公务出访
  7317. await _sqlSugar.Updateable<Grp_OfficialDutyLinkTranslator>()
  7318. .SetColumns(x => new Grp_OfficialDutyLinkTranslator()
  7319. {
  7320. DeleteUserId = currUserInfo.UserId,
  7321. DeleteTime = DateTime.Now.ToString("yyyy-MM-dd HH:mm:ss"),
  7322. IsDel = 1,
  7323. Remark = $"翻译人员库-->删除"
  7324. })
  7325. .Where(x => x.TranslatorId == id)
  7326. .ExecuteCommandAsync();
  7327. #endregion
  7328. _sqlSugar.CommitTran();
  7329. return Ok(JsonView(true));
  7330. }
  7331. #endregion
  7332. #region 策划部供应商资料
  7333. /// <summary>
  7334. /// 策划部供应商资料
  7335. /// Init
  7336. /// </summary>
  7337. /// <returns></returns>
  7338. [HttpGet]
  7339. [ProducesResponseType(typeof(JsonView), StatusCodes.Status200OK)]
  7340. public async Task<IActionResult> MediaSupplierInit()
  7341. {
  7342. return Ok(await _mediaSupplierRep.Init());
  7343. }
  7344. /// <summary>
  7345. /// 策划部供应商资料
  7346. /// Info
  7347. /// </summary>
  7348. /// <param name="dto"></param>
  7349. /// <returns></returns>
  7350. [HttpPost]
  7351. [ProducesResponseType(typeof(JsonView), StatusCodes.Status200OK)]
  7352. public async Task<IActionResult> MediaSupplierInfo(MediaSupplierInfoDto dto)
  7353. {
  7354. if (!_portIds.Contains(dto.PortType)) return Ok(JsonView(false, MsgTips.Port));
  7355. if (dto.Id < 1) return Ok(JsonView(false, MsgTips.Port));
  7356. return Ok(await _mediaSupplierRep.Info(dto));
  7357. }
  7358. /// <summary>
  7359. /// 策划部供应商资料
  7360. /// 分页查询
  7361. /// </summary>
  7362. /// <param name="dto"></param>
  7363. /// <returns></returns>
  7364. [HttpPost]
  7365. [ProducesResponseType(typeof(JsonView), StatusCodes.Status200OK)]
  7366. public async Task<IActionResult> MediaSupplierPageItem(MediaSupplierPageItemDto dto)
  7367. {
  7368. if (!_portIds.Contains(dto.PortType)) return Ok(JsonView(false, MsgTips.Port));
  7369. return Ok(await _mediaSupplierRep.PageItem(dto));
  7370. }
  7371. /// <summary>
  7372. /// 策划部供应商资料
  7373. /// 操作(添加 Or 编辑)
  7374. /// </summary>
  7375. /// <param name="dto"></param>
  7376. /// <returns></returns>
  7377. [HttpPost]
  7378. [ProducesResponseType(typeof(JsonView), StatusCodes.Status200OK)]
  7379. public async Task<IActionResult> MediaSupplierOp(MediaSupplierAddOrEditDto dto)
  7380. {
  7381. var userId = dto.CurrUserId;
  7382. if (!_portIds.Contains(dto.PortType)) return Ok(JsonView(false, MsgTips.Port));
  7383. if (userId < 1) return Ok(JsonView(false, MsgTips.UserId));
  7384. return Ok(await _mediaSupplierRep.AddOrEdit(dto));
  7385. }
  7386. /// <summary>
  7387. /// 策划部供应商资料
  7388. /// 删除
  7389. /// </summary>
  7390. /// <param name="dto"></param>
  7391. /// <returns></returns>
  7392. [HttpPost]
  7393. [ProducesResponseType(typeof(JsonView), StatusCodes.Status200OK)]
  7394. public async Task<IActionResult> MediaSupplierSoftDel(MediaSupplierSoftDelDto dto)
  7395. {
  7396. int userId = dto.CurrUserId, id = dto.Id;
  7397. if (!_portIds.Contains(dto.PortType)) return Ok(JsonView(false, MsgTips.Port));
  7398. if (userId < 1) return Ok(JsonView(false, MsgTips.UserId));
  7399. if (id < 1) return Ok(JsonView(false, MsgTips.Id));
  7400. return Ok(await _mediaSupplierRep.SoftDel(dto));
  7401. }
  7402. #endregion
  7403. #region 保险国家基础费用
  7404. /// <summary>
  7405. /// 保险国家基础费用
  7406. /// Info
  7407. /// </summary>
  7408. /// <param name="dto"></param>
  7409. /// <returns></returns>
  7410. [HttpPost]
  7411. [ProducesResponseType(typeof(JsonView), StatusCodes.Status200OK)]
  7412. public async Task<IActionResult> InsuranceCostInfo(InsuranceCostInfoDto dto)
  7413. {
  7414. if (!_portIds.Contains(dto.PortType)) return Ok(JsonView(false, MsgTips.Port));
  7415. if (dto.Id < 1) return Ok(JsonView(false, MsgTips.Port));
  7416. var id = dto.Id;
  7417. var info = await _sqlSugar.Queryable<Res_BasicInsuranceCost>()
  7418. .Where(x => x.Id == id && x.IsDel == 0)
  7419. .Select(x => new
  7420. {
  7421. x.Id,
  7422. x.IsSchengen,
  7423. x.CountryName,
  7424. x.Cost,
  7425. x.Remark,
  7426. CreateUserName = SqlFunc.Subqueryable<Sys_Users>().Where(s => s.Id == x.CreateUserId).Select(s => s.CnName),
  7427. x.CreateTime
  7428. })
  7429. .FirstAsync();
  7430. return Ok(JsonView(info));
  7431. }
  7432. /// <summary>
  7433. /// 保险国家基础费用
  7434. /// 分页查询
  7435. /// </summary>
  7436. /// <param name="dto"></param>
  7437. /// <returns></returns>
  7438. [HttpPost]
  7439. [ProducesResponseType(typeof(JsonView), StatusCodes.Status200OK)]
  7440. public async Task<IActionResult> InsuranceCostPageItem(InsuranceCostPageItemDto dto)
  7441. {
  7442. if (!_portIds.Contains(dto.PortType)) return Ok(JsonView(false, MsgTips.Port));
  7443. RefAsync<int> total = 0;
  7444. var data = await _sqlSugar.Queryable<Res_BasicInsuranceCost>()
  7445. .LeftJoin<Sys_Users>((bic, u) => bic.CreateUserId == u.Id)
  7446. .Where((bic, u) => bic.IsDel == 0)
  7447. .WhereIF(!string.IsNullOrEmpty(dto.Search), (bic, u) => bic.CountryName.Contains(dto.Search))
  7448. .OrderByDescending((bic, u) => bic.Id)
  7449. .Select((bic, u) => new
  7450. {
  7451. bic.Id,
  7452. bic.IsSchengen,
  7453. bic.CountryName,
  7454. bic.Cost,
  7455. bic.Remark,
  7456. CreateUserName = u.CnName,
  7457. bic.CreateTime
  7458. })
  7459. .ToPageListAsync(dto.PageIndex, dto.PageSize, total);
  7460. return Ok(JsonView(data, total));
  7461. }
  7462. /// <summary>
  7463. /// 保险国家基础费用
  7464. /// 操作(添加 Or 编辑)
  7465. /// </summary>
  7466. /// <param name="dto"></param>
  7467. /// <returns></returns>
  7468. [HttpPost]
  7469. [ProducesResponseType(typeof(JsonView), StatusCodes.Status200OK)]
  7470. public async Task<IActionResult> InsuranceCostOp(InsuranceCostOpDto dto)
  7471. {
  7472. var result = new JsonView() { Code = 400, Msg = "操作失败" };
  7473. var userId = dto.CurrUserId;
  7474. if (!_portIds.Contains(dto.PortType)) return Ok(JsonView(false, MsgTips.Port));
  7475. if (userId < 1) return Ok(JsonView(false, MsgTips.UserId));
  7476. var countryName = dto.CountryName.Trim();
  7477. if (string.IsNullOrEmpty(countryName)) return Ok(JsonView(false, $"国家名称不能为空!"));
  7478. var info = _mapper.Map<Res_BasicInsuranceCost>(dto);
  7479. info.CreateUserId = dto.CurrUserId;
  7480. info.CountryName = countryName;
  7481. if (dto.Id < 1) //添加
  7482. {
  7483. var addInfo = await _insuranceCostRep.Query(x => x.CountryName.Equals(countryName)).FirstAsync();
  7484. if (addInfo != null) return Ok(JsonView(false, $"该国家信息已存在,不可添加!"));
  7485. var add = await _insuranceCostRep.AddAsync(info);
  7486. if (add < 1) return Ok(JsonView(false, "添加失败!"));
  7487. }
  7488. else //修改
  7489. {
  7490. var upd = await _insuranceCostRep.UpdateAsync(x => x.Id == dto.Id, x => new Res_BasicInsuranceCost
  7491. {
  7492. IsSchengen = dto.IsSchengen,
  7493. CountryName = dto.CountryName,
  7494. Cost = dto.Cost,
  7495. Remark = dto.Remark
  7496. });
  7497. if (!upd) return Ok(JsonView(false, "修改失败!"));
  7498. }
  7499. return Ok(JsonView(true));
  7500. }
  7501. /// <summary>
  7502. /// 保险国家基础费用
  7503. /// 删除
  7504. /// </summary>
  7505. /// <param name="dto"></param>
  7506. /// <returns></returns>
  7507. [HttpPost]
  7508. [ProducesResponseType(typeof(JsonView), StatusCodes.Status200OK)]
  7509. public async Task<IActionResult> InsuranceCostSoftDel(InsuranceCostSoftDelDto dto)
  7510. {
  7511. int userId = dto.CurrUserId, id = dto.Id;
  7512. if (!_portIds.Contains(dto.PortType)) return Ok(JsonView(false, MsgTips.Port));
  7513. if (userId < 1) return Ok(JsonView(false, MsgTips.UserId));
  7514. if (id < 1) return Ok(JsonView(false, MsgTips.Id));
  7515. var del = await _insuranceCostRep.SoftDeleteAsync(x => x.Id == id, userId);
  7516. if (!del) return Ok(JsonView(false));
  7517. return Ok(JsonView(true));
  7518. }
  7519. #endregion
  7520. #region 世运会成本预算明细
  7521. /// <summary>
  7522. /// 世运会成本预算明细
  7523. /// Info
  7524. /// </summary>
  7525. /// <param name="id"></param>
  7526. /// <returns></returns>
  7527. [HttpGet("{id}")]
  7528. [ProducesResponseType(typeof(JsonView), StatusCodes.Status200OK)]
  7529. public async Task<IActionResult> GamesBudgetMasterInfo([Required(ErrorMessage = "数据编号不能为空!")] int id)
  7530. {
  7531. return Ok(await _gamesBudgetMasterRep.InfoAsync(id));
  7532. }
  7533. /// <summary>
  7534. /// 世运会成本预算明细
  7535. /// List
  7536. /// </summary>
  7537. /// <param name="dto"></param>
  7538. /// <returns></returns>
  7539. [HttpPost]
  7540. [ProducesResponseType(typeof(JsonView), StatusCodes.Status200OK)]
  7541. public async Task<IActionResult> GamesBudgetMasterList(GamesBudgetMasterListDto dto)
  7542. {
  7543. return Ok(await _gamesBudgetMasterRep.ListAsync(dto));
  7544. }
  7545. /// <summary>
  7546. /// 世运会成本预算明细
  7547. /// OP(Add/Edit)
  7548. /// </summary>
  7549. /// <param name="dto"></param>
  7550. /// <returns></returns>
  7551. [HttpPost]
  7552. [ProducesResponseType(typeof(JsonView), StatusCodes.Status200OK)]
  7553. public async Task<IActionResult> GamesBudgetMasterOP(GamesBudgetMasterOPDto dto)
  7554. {
  7555. if (dto.Id > 0)
  7556. {
  7557. //修改
  7558. var editDto = _mapper.Map<GamesBudgetMasterEditDto>(dto);
  7559. return Ok(await _gamesBudgetMasterRep.EditAsync(editDto));
  7560. }
  7561. else if (dto.Id < 1)
  7562. {
  7563. //添加
  7564. var addDto = _mapper.Map<GamesBudgetMasterAddDto>(dto);
  7565. return Ok(await _gamesBudgetMasterRep.AddAsync(addDto));
  7566. }
  7567. return Ok(JsonView(false));
  7568. }
  7569. /// <summary>
  7570. /// 世运会成本预算明细
  7571. /// 删除
  7572. /// </summary>
  7573. /// <param name="dto"></param>
  7574. /// <returns></returns>
  7575. [HttpPost]
  7576. [ProducesResponseType(typeof(JsonView), StatusCodes.Status200OK)]
  7577. public async Task<IActionResult> GamesBudgetMasterDel(GamesBudgetMasterDelDto dto)
  7578. {
  7579. return Ok(await _gamesBudgetMasterRep.DelAsync(dto));
  7580. }
  7581. #endregion
  7582. #region 境外用车
  7583. /// <summary>
  7584. /// 境外用车
  7585. /// Init
  7586. /// </summary>
  7587. /// <returns></returns>
  7588. [HttpGet()]
  7589. [ProducesResponseType(typeof(JsonView), StatusCodes.Status200OK)]
  7590. public async Task<IActionResult> OverseaVehicleInitAsync()
  7591. {
  7592. return Ok(await _overseaVehicleRep.InitAsync());
  7593. }
  7594. /// <summary>
  7595. /// 境外用车
  7596. /// Info
  7597. /// </summary>
  7598. /// <param name="id"></param>
  7599. /// <returns></returns>
  7600. [HttpGet("{id}")]
  7601. [ProducesResponseType(typeof(JsonView), StatusCodes.Status200OK)]
  7602. public async Task<IActionResult> OverseaVehicleInfoAsync([Required(ErrorMessage = "数据编号不能为空!")] int id)
  7603. {
  7604. return Ok(await _overseaVehicleRep.InfoAsync(id));
  7605. }
  7606. /// <summary>
  7607. /// 境外用车
  7608. /// List
  7609. /// </summary>
  7610. /// <param name="dto"></param>
  7611. /// <returns></returns>
  7612. [HttpPost]
  7613. [ProducesResponseType(typeof(JsonView), StatusCodes.Status200OK)]
  7614. public async Task<IActionResult> OverseaVehicleListAsync(OverseaVehicleListDto dto)
  7615. {
  7616. return Ok(await _overseaVehicleRep.ListAsync(dto));
  7617. }
  7618. /// <summary>
  7619. /// 境外用车
  7620. /// OP(Add/Edit)
  7621. /// </summary>
  7622. /// <param name="dto"></param>
  7623. /// <returns></returns>
  7624. [HttpPost]
  7625. [ProducesResponseType(typeof(JsonView), StatusCodes.Status200OK)]
  7626. public async Task<IActionResult> OverseaVehicleAddOrEditAsync(OverseaVehicleAddOrEditDto dto)
  7627. {
  7628. if (dto.Id > 0)
  7629. {
  7630. //修改
  7631. return Ok(await _overseaVehicleRep.EditAsync(dto));
  7632. }
  7633. else if (dto.Id < 1)
  7634. {
  7635. //添加
  7636. return Ok(await _overseaVehicleRep.AddAsync(dto));
  7637. }
  7638. return Ok(JsonView(false));
  7639. }
  7640. /// <summary>
  7641. /// 境外用车
  7642. /// 删除
  7643. /// </summary>
  7644. /// <param name="dto"></param>
  7645. /// <returns></returns>
  7646. [HttpPost]
  7647. [ProducesResponseType(typeof(JsonView), StatusCodes.Status200OK)]
  7648. public async Task<IActionResult> OverseaVehicleDelAsync(OverseaVehicleDelDto dto)
  7649. {
  7650. return Ok(await _overseaVehicleRep.DelAsync(dto));
  7651. }
  7652. #endregion
  7653. #region 物料成本
  7654. /// <summary>
  7655. /// 物料成本列表查询
  7656. /// </summary>
  7657. /// <param name="dto"></param>
  7658. /// <returns></returns>
  7659. [HttpPost]
  7660. [ProducesResponseType(typeof(JsonView), StatusCodes.Status200OK)]
  7661. public async Task<IActionResult> MaterialCostList(QueryMaterialCostDto dto)
  7662. {
  7663. var data = await _materialCostRep.GetListAsync(dto);
  7664. return Ok(JsonView(true, "查询成功", data));
  7665. }
  7666. /// <summary>
  7667. /// 物料成本详情
  7668. /// </summary>
  7669. /// <param name="id"></param>
  7670. /// <returns></returns>
  7671. [HttpGet]
  7672. [ProducesResponseType(typeof(JsonView), StatusCodes.Status200OK)]
  7673. public async Task<IActionResult> MaterialCostInfo(int id)
  7674. {
  7675. var data = await _materialCostRep.GetDetailAsync(id);
  7676. return Ok(JsonView(true, "查询成功", data));
  7677. }
  7678. /// <summary>
  7679. /// 物料成本添加/修改
  7680. /// </summary>
  7681. /// <param name="dto"></param>
  7682. /// <returns></returns>
  7683. [HttpPost]
  7684. [ProducesResponseType(typeof(JsonView), StatusCodes.Status200OK)]
  7685. public async Task<IActionResult> MaterialCostAddOrEdit(UpMaterialCostDto dto)
  7686. {
  7687. if (dto.Id > 0)
  7688. {
  7689. // 修改
  7690. var entity = _mapper.Map<Res_MaterialCost>(dto);
  7691. var result = await _materialCostRep.UpdateAsync(s => s.Id == dto.Id, it => new Res_MaterialCost()
  7692. {
  7693. TypeId = entity.TypeId,
  7694. Name = entity.Name,
  7695. Price = entity.Price,
  7696. Number = entity.Number,
  7697. Unit = entity.Unit,
  7698. SetDataId = entity.SetDataId,
  7699. CityId = entity.CityId,
  7700. Remark = entity.Remark
  7701. });
  7702. return Ok(JsonView(result, result ? "修改成功" : "修改失败"));
  7703. }
  7704. else
  7705. {
  7706. // 添加
  7707. var entity = _mapper.Map<Res_MaterialCost>(dto);
  7708. entity.CreateTime = DateTime.Now;
  7709. var result = await _materialCostRep.AddAsync(entity);
  7710. return Ok(JsonView(result > 0, result > 0 ? "添加成功" : "添加失败"));
  7711. }
  7712. }
  7713. /// <summary>
  7714. /// 物料成本删除
  7715. /// </summary>
  7716. /// <param name="dto"></param>
  7717. /// <returns></returns>
  7718. [HttpPost]
  7719. [ProducesResponseType(typeof(JsonView), StatusCodes.Status200OK)]
  7720. public async Task<IActionResult> MaterialCostDel(DelBaseDto dto)
  7721. {
  7722. var result = await _materialCostRep.SoftDeleteAsync(dto.Id.ToString());
  7723. return Ok(JsonView(result, result ? "删除成功" : "删除失败"));
  7724. }
  7725. #endregion
  7726. #region OP地接资料 - AI
  7727. /// <summary>
  7728. /// OP地接资料AI 基础数据源
  7729. /// </summary>
  7730. /// <returns></returns>
  7731. [HttpGet]
  7732. [ProducesResponseType(typeof(JsonView), StatusCodes.Status200OK)]
  7733. public async Task<IActionResult> OpLocalServiceInit()
  7734. {
  7735. var itemNames = await OpLocalServiceAINames();
  7736. var countries = await InvitationAICountryName();
  7737. var orgScales = OrgScale.BuildInitialData().Select(x => x.Name).ToList();
  7738. var orgLevels = new List<string>() {
  7739. "全部",
  7740. "总部",
  7741. "分公司"
  7742. };
  7743. return Ok(JsonView(true, $"查询成功!", new
  7744. {
  7745. itemNames,
  7746. countries,
  7747. orgScales,
  7748. orgLevels
  7749. }));
  7750. }
  7751. /// <summary>
  7752. /// OP地接资料AI 资料列表
  7753. /// </summary>
  7754. /// <returns></returns>
  7755. [HttpGet("{name}")]
  7756. [ProducesResponseType(typeof(JsonView), StatusCodes.Status200OK)]
  7757. public async Task<IActionResult> OpLocalServiceAIItemByName(string name)
  7758. {
  7759. if (string.IsNullOrWhiteSpace(name)) return Ok(JsonView(false, "名称无效"));
  7760. // 1. 获取主表记录
  7761. var info = await _sqlSugar.Queryable<Res_OpLocalAI>()
  7762. .FirstAsync(x => x.IsDel == 0 && x.LocalName == name);
  7763. var groupInfo = await _sqlSugar.Queryable<Grp_DelegationInfo>().FirstAsync(x => x.IsDel == 0 && x.TeamName.Equals(name));
  7764. string baseUrl = AppSettingsHelper.Get("OfficeBaseUrl")?.TrimEnd('/');
  7765. // 2. 分支处理:若无主表记录,尝试从团组表同步基本信息
  7766. if (info == null)
  7767. {
  7768. return Ok(JsonView(true, "暂无数据", new
  7769. {
  7770. Id = 0,
  7771. GroupId = groupInfo?.Id ?? 0,
  7772. LocalName = name,
  7773. AiCrawledDetails = new List<LocalAgencyInfo>(),
  7774. Entry = new
  7775. {
  7776. Objective = groupInfo?.VisitPurpose ?? "商务会谈",
  7777. OriginUnit = groupInfo?.ClientUnit ?? "",
  7778. TargetCountry = _delegationInfoRep.GroupSplitCountry(groupInfo?.VisitCountry ?? "")
  7779. }
  7780. }));
  7781. }
  7782. // 3. 数据填充(使用 ??= 语法确保 Null 安全)
  7783. info.EntryInfo ??= new LocalEntryInfo();
  7784. if (string.IsNullOrEmpty(info.EntryInfo.Objective))
  7785. info.EntryInfo.Objective = groupInfo?.VisitPurpose ?? "商务会谈";
  7786. if (string.IsNullOrEmpty(info.EntryInfo.OriginUnit))
  7787. info.EntryInfo.OriginUnit = groupInfo?.ClientUnit ?? "";
  7788. if (info.EntryInfo.TargetCountry?.Any() != true)
  7789. info.EntryInfo.TargetCountry = _delegationInfoRep.GroupSplitCountry(groupInfo?.VisitCountry ?? "");
  7790. // 4. 排序:ThenByDescending 确保多级排序生效
  7791. info.AiCrawledDetails = info.AiCrawledDetails
  7792. .OrderByDescending(x => x.IsChecked)
  7793. .ThenByDescending(x => x.OperatedAt)
  7794. .ToList();
  7795. var details = info?.AiCrawledDetails ?? new List<LocalAgencyInfo>();
  7796. if (details.Any())
  7797. {
  7798. foreach (var detail in details)
  7799. {
  7800. // 附件路径处理
  7801. if (detail.EmailInfo?.AttachmentPaths?.Any() == true)
  7802. {
  7803. detail.EmailInfo.AttachmentPaths = detail.EmailInfo.AttachmentPaths
  7804. .Select(p => p.StartsWith("http") ? p : $"{baseUrl}/{p.TrimStart('/')}")
  7805. .ToList();
  7806. }
  7807. }
  7808. }
  7809. return Ok(JsonView(true, "查询成功!", new
  7810. {
  7811. info.Id,
  7812. info.GroupId,
  7813. info.LocalName,
  7814. AiCrawledDetails = details,
  7815. Entry = info.EntryInfo
  7816. }));
  7817. }
  7818. /// <summary>
  7819. /// OP地接资料AI 设置词条
  7820. /// </summary>
  7821. /// <returns></returns>
  7822. [HttpPost]
  7823. [ProducesResponseType(typeof(JsonView), StatusCodes.Status200OK)]
  7824. public async Task<IActionResult> OpLocalServiceAISetPrompt(OpLocalServiceAISetPromptDto dto)
  7825. {
  7826. // 基础校验
  7827. if (string.IsNullOrWhiteSpace(dto.LocalName) || dto.TargetCountry == null || dto.TargetCountry.Count == 0)
  7828. return Ok(JsonView(false, "请传入有效的地接名称、国家、规模类型!"));
  7829. var localName = dto.LocalName;
  7830. var groupInfo = await _sqlSugar.Queryable<Grp_DelegationInfo>()
  7831. .Where(x => x.IsDel == 0 && x.TeamName.Equals(localName))
  7832. .Select(x => new { x.Id, x.TeamName, x.VisitPurpose })
  7833. .FirstAsync();
  7834. int groupId = groupInfo?.Id ?? 0;
  7835. #region 数据库操作
  7836. // 数据库信息获取方式
  7837. var dataInfo = await _sqlSugar.Queryable<Res_OpLocalAI>().Where(x => x.IsDel == 0 && x.LocalName.Equals(localName)).FirstAsync();
  7838. #region 词条信息
  7839. string operatorName = await _sqlSugar.Queryable<Sys_Users>().Where(x => x.IsDel == 0 && x.Id == dto.CurrUserId).Select(x => x.CnName).FirstAsync() ?? "-";
  7840. var entryInfo = new LocalEntryInfo()
  7841. {
  7842. OriginUnit = dto.OriginUnit,
  7843. TargetCountry = dto.TargetCountry,
  7844. ScaleTypes = dto.ScaleTypes,
  7845. IsBackground = dto.IsBackground,
  7846. OrgLevel = dto.OrgLevel,
  7847. OtherConstraints = dto.OtherConstraints,
  7848. Operator = operatorName,
  7849. OperatedAt = DateTime.Now
  7850. };
  7851. #endregion
  7852. // 3.如果以上两种方式都没有查询到数据,则说明是新数据,需要添加到数据库
  7853. if (dataInfo == null)
  7854. {
  7855. // 3.1 新数据,需要添加到数据库
  7856. dataInfo = new Res_OpLocalAI()
  7857. {
  7858. LocalName = localName,
  7859. GroupId = groupId,
  7860. EntryInfo = entryInfo,
  7861. CreateUserId = dto.CurrUserId
  7862. };
  7863. var insert = await _sqlSugar.Insertable(dataInfo).ExecuteReturnIdentityAsync();
  7864. if (insert < 1)
  7865. {
  7866. return Ok(JsonView(false, $"词条信息新增失败!"));
  7867. }
  7868. dataInfo.Id = insert;
  7869. }
  7870. else
  7871. {
  7872. // 3.2 数据存在 则更新数据(覆盖原有的AI爬取详情,保留原有的其他字段不变)
  7873. dataInfo.EntryInfo = entryInfo;
  7874. var update = await _sqlSugar.Updateable(dataInfo).ExecuteCommandAsync();
  7875. if (update < 1)
  7876. {
  7877. return Ok(JsonView(false, $"词条信息更新失败!"));
  7878. }
  7879. }
  7880. #endregion
  7881. return Ok(JsonView(true, $"设置成功!", new
  7882. {
  7883. dataInfo.Id,
  7884. dataInfo.LocalName,
  7885. dataInfo.GroupId,
  7886. Entry = dataInfo.EntryInfo,
  7887. }));
  7888. }
  7889. /// <summary>
  7890. /// OP地接资料AI 设置复选框选中 批量
  7891. /// </summary>
  7892. /// <param name="dto"></param>
  7893. /// <returns></returns>
  7894. [HttpPost]
  7895. public async Task<IActionResult> OpLocalServiceAISetChecked([FromBody] OpLocalServiceAISetCheckedDto dto)
  7896. {
  7897. // 1. 基础参数校验 (注意:这里移除了对 Guids.Any() 的强校验,允许空集合)
  7898. if (dto.Id < 1 || dto.CurrUserId < 1 || dto.Guids == null)
  7899. return Ok(JsonView(false, "请求参数不完整"));
  7900. // 2. 获取主记录
  7901. var invAiInfo = await _sqlSugar.Queryable<Res_OpLocalAI>()
  7902. .FirstAsync(x => x.IsDel == 0 && x.Id == dto.Id);
  7903. if (invAiInfo?.AiCrawledDetails == null || !invAiInfo.AiCrawledDetails.Any())
  7904. return Ok(JsonView(false, "数据不存在或集合为空"));
  7905. // 3. 准备更新所需数据
  7906. var guidSet = dto.Guids.ToHashSet();
  7907. var now = DateTime.Now;
  7908. // 获取操作人姓名
  7909. string operatorName = await _sqlSugar.Queryable<Sys_Users>()
  7910. .Where(x => x.Id == dto.CurrUserId)
  7911. .Select(x => x.CnName)
  7912. .FirstAsync() ?? "-";
  7913. // 4. 单次遍历完成 重置 + 更新
  7914. // 如果 guidSet 为空,循环会将所有项的 IsChecked 置为 false
  7915. foreach (var item in invAiInfo.AiCrawledDetails)
  7916. {
  7917. if (guidSet.Contains(item.Guid))
  7918. {
  7919. item.IsChecked = true;
  7920. }
  7921. else
  7922. {
  7923. item.IsChecked = false;
  7924. }
  7925. item.OperatedAt = now;
  7926. item.Operator = operatorName;
  7927. }
  7928. // 5. 排序逻辑
  7929. // 即使是全量取消选中,也可以按最后操作时间排序,让最近变动的项在前
  7930. invAiInfo.AiCrawledDetails = invAiInfo.AiCrawledDetails
  7931. .OrderByDescending(x => x.OperatedAt)
  7932. .ToList();
  7933. // 6. 提交数据库
  7934. // 注意:SqlSugar 的 UpdateColumns 会序列化整个对象列表并覆盖数据库字段
  7935. await _sqlSugar.Updateable(invAiInfo)
  7936. .UpdateColumns(x => x.AiCrawledDetails)
  7937. .ExecuteCommandAsync();
  7938. return Ok(JsonView(true, guidSet.Any() ? "设置成功" : "已全部取消选中"));
  7939. }
  7940. /// <summary>
  7941. /// OP地接资料AI 保存
  7942. /// </summary>
  7943. /// <returns></returns>
  7944. [HttpPost]
  7945. [ProducesResponseType(typeof(JsonView), StatusCodes.Status200OK)]
  7946. public async Task<IActionResult> OpLocalServiceAISave(OpLocalServiceAISaveDto dto)
  7947. {
  7948. if (dto.Id < 1) return Ok(JsonView(false, "请先设置AI关键字"));
  7949. if (dto.CurrUserId < 1) return Ok(JsonView(false, "请传入用户Id"));
  7950. if (dto.AiCrawledDetail == null) return Ok(JsonView(false, "请选择保存的邀请方详情"));
  7951. var localAiInfo = await _sqlSugar.Queryable<Res_OpLocalAI>()
  7952. .Where(x => x.Id == dto.Id)
  7953. .FirstAsync();
  7954. if (localAiInfo == null) return Ok(JsonView(false, "数据信息为空"));
  7955. var dataList = localAiInfo?.AiCrawledDetails;
  7956. if (dataList == null) return Ok(JsonView(false, "邀请方数据信息为空"));
  7957. var datas = dataList.Where(x => x.Guid != dto.AiCrawledDetail.Guid).ToList();
  7958. var editInfo = dto.AiCrawledDetail;
  7959. // 如果 Guid 为空,说明是新增数据,需要生成新的 Guid
  7960. if (string.IsNullOrEmpty(editInfo.Guid))
  7961. {
  7962. editInfo.Guid = Guid.NewGuid().ToString("N");
  7963. editInfo.Source = 2; // 标记为手动新增数据
  7964. editInfo.OperatedAt = DateTime.Now;
  7965. }
  7966. var opUserName = await _sqlSugar.Queryable<Sys_Users>().Where(x => x.IsDel == 0 && x.Id == dto.CurrUserId).Select(x => x.CnName).FirstAsync() ?? "-";
  7967. editInfo.Operator = opUserName;
  7968. editInfo.OperatedAt = DateTime.Now;
  7969. datas.Add(editInfo);
  7970. localAiInfo.AiCrawledDetails = datas.OrderByDescending(x => x.OperatedAt).ToList();
  7971. var editUpd = await _sqlSugar.Updateable(localAiInfo).UpdateColumns(x => x.AiCrawledDetails).ExecuteCommandAsync();
  7972. if (editUpd < 1)
  7973. {
  7974. return Ok(JsonView(true, "保存失败"));
  7975. }
  7976. return Ok(JsonView(true, "保存成功"));
  7977. }
  7978. /// <summary>
  7979. /// OP地接资料AI 单条删除
  7980. /// </summary>
  7981. /// <returns></returns>
  7982. [HttpPost]
  7983. [ProducesResponseType(typeof(JsonView), StatusCodes.Status200OK)]
  7984. public async Task<IActionResult> OpLocalServiceAISingleDel(OpLocalServiceAISingleDelDto dto)
  7985. {
  7986. int id = dto.Id;
  7987. string guid = dto.Guid;
  7988. // 基础校验
  7989. if (id < 1 || string.IsNullOrWhiteSpace(guid))
  7990. return Ok(JsonView(false, "请传入有效的Id和Guid!"));
  7991. var dataInfo = await _sqlSugar.Queryable<Res_OpLocalAI>().Where(x => x.IsDel == 0 && x.Id == id).FirstAsync();
  7992. if (dataInfo == null)
  7993. return Ok(JsonView(false, "当前数据信息不存在!"));
  7994. var newDataInfos = dataInfo.AiCrawledDetails.Where(x => x.Guid != guid).ToList();
  7995. dataInfo.AiCrawledDetails = newDataInfos.OrderByDescending(x => x.OperatedAt).ToList();
  7996. var update = await _sqlSugar.Updateable(dataInfo).ExecuteCommandAsync();
  7997. if (update < 1)
  7998. {
  7999. return Ok(JsonView(false, $"删除失败!"));
  8000. }
  8001. return Ok(JsonView(true, $"删除成功!", new
  8002. {
  8003. dataInfo.Id,
  8004. dataInfo.LocalName,
  8005. dataInfo.GroupId,
  8006. dataInfo.AiCrawledDetails
  8007. }));
  8008. }
  8009. ///// <summary>
  8010. ///// OP地接资料AI AI查询出的数据添加至商邀资料库
  8011. ///// </summary>
  8012. ///// <returns></returns>
  8013. //[HttpPost]
  8014. //[ProducesResponseType(typeof(JsonView), StatusCodes.Status200OK)]
  8015. //public async Task<IActionResult> OpLocalServiceAIInsertResource([FromBody] OpLocalServiceAIInsertResourceDto dto)
  8016. //{
  8017. // // 基础校验
  8018. // if (dto.Id < 1 || dto.CurrUserId < 1 || dto.Guids == null || !dto.Guids.Any())
  8019. // return Ok(JsonView(false, "参数不完整,请选择有效的单位数据"));
  8020. // // 获取主记录
  8021. // var dataInfo = await _sqlSugar.Queryable<Res_InvitationAI>()
  8022. // .FirstAsync(x => x.IsDel == 0 && x.Id == dto.Id);
  8023. // if (dataInfo?.AiCrawledDetails == null)
  8024. // return Ok(JsonView(false, "当前数据信息不存在!"));
  8025. // // 筛选出待转正的 AI 数据 (使用 HashSet 优化匹配)
  8026. // var guidSet = dto.Guids.ToHashSet();
  8027. // var targetAiDetails = dataInfo.AiCrawledDetails
  8028. // .Where(x => x.Source != 0 && guidSet.Contains(x.Guid))
  8029. // .ToList();
  8030. // if (!targetAiDetails.Any())
  8031. // return Ok(JsonView(false, "未找到符合条件的 AI 获取信息,无法重复添加或来源错误!"));
  8032. // // 准备入库数据(批量构建)
  8033. // var now = DateTime.Now;
  8034. // var opUserName = await _sqlSugar.Queryable<Sys_Users>().Where(x => x.IsDel == 0 && x.Id == dto.CurrUserId).Select(x => x.CnName).FirstAsync() ?? "-";
  8035. // var rawLocalDatas = targetAiDetails.Select(item => {
  8036. // // 修改内存中的状态为本地数据
  8037. // item.Source = 0;
  8038. // item.OperatedAt = now;
  8039. // item.Operator = opUserName;
  8040. // return new Res_InvitationOfficialActivityData()
  8041. // {
  8042. // // 批量加密操作
  8043. // Country = AesEncryptionHelper.Encrypt(item.Region),
  8044. // UnitName = AesEncryptionHelper.Encrypt(item.NameCn),
  8045. // Address = AesEncryptionHelper.Encrypt(item.Address),
  8046. // Field = AesEncryptionHelper.Encrypt(item.Scope),
  8047. // Contact = AesEncryptionHelper.Encrypt(item.Contact),
  8048. // Tel = AesEncryptionHelper.Encrypt(item.Phone),
  8049. // Email = AesEncryptionHelper.Encrypt(item.Email),
  8050. // UnitWeb = AesEncryptionHelper.Encrypt(item.SiteUrl),
  8051. // LastUpdateUserId = dto.CurrUserId,
  8052. // LastUpdateTime = now,
  8053. // CreateUserId = dto.CurrUserId,
  8054. // CreateTime = now
  8055. // };
  8056. // }).ToList();
  8057. // // 开启事务执行双表更新
  8058. // var result = await _sqlSugar.UseTranAsync(async () =>
  8059. // {
  8060. // // 插入本地资料库
  8061. // await _sqlSugar.Insertable(rawLocalDatas).ExecuteCommandAsync();
  8062. // // 更新 AI 主表的状态(全量覆盖 JSON 列)
  8063. // await _sqlSugar.Updateable(dataInfo)
  8064. // .UpdateColumns(x => x.AiCrawledDetails)
  8065. // .ExecuteCommandAsync();
  8066. // });
  8067. // return Ok(result.IsSuccess
  8068. // ? JsonView(true, "成功添加至资料库并更新来源状态")
  8069. // : JsonView(false, $"操作失败:{result.ErrorMessage}"));
  8070. //}
  8071. /// <summary>
  8072. /// OP地接资料AI TokenHub AI查询资料(SSE流式推送)
  8073. /// </summary>
  8074. [HttpPost]
  8075. public async Task OpLocalServiceAISearchStream([FromBody] OpLocalServiceAISearchDto dto)
  8076. {
  8077. HttpContext.InitializeSse();
  8078. try
  8079. {
  8080. await HttpContext.SendSseStepAsync(5, "正在加载配置...");
  8081. #region 1. 异步并行化验证 (Performance Boost)
  8082. var localAiInfo = await _sqlSugar.Queryable<Res_OpLocalAI>().Where(x => x.IsDel == 0 && x.Id == dto.Id).FirstAsync();
  8083. var operatorName = await _sqlSugar.Queryable<Sys_Users>()
  8084. .Where(x => x.IsDel == 0 && x.Id == dto.CurrUserId)
  8085. .Select(x => x.CnName).FirstAsync();
  8086. if (localAiInfo?.EntryInfo == null)
  8087. {
  8088. await HttpContext.SendSseStepAsync(-1, "未找到有效的关键字配置。");
  8089. return;
  8090. }
  8091. var entryInfo = localAiInfo.EntryInfo;
  8092. var targetCountrySet = new HashSet<string>(entryInfo.TargetCountry);
  8093. #endregion
  8094. #region 2. 内存计算优化
  8095. //await HttpContext.SendSseStepAsync(20, "正在解析本地典籍 (并行解密)...");
  8096. //// 仅查询必要字段,减少 DataReader 压力
  8097. //var rawLocalDatas = await _sqlSugar.Queryable<Res_InvitationOfficialActivityData>()
  8098. // .Where(x => x.IsDel == 0 && !string.IsNullOrEmpty(x.UnitName))
  8099. // .Select(x => new { x.Country, x.UnitName, x.Address, x.Field, x.Contact, x.Tel, x.Email })
  8100. // .ToListAsync();
  8101. //// PLINQ 核心炼金:利用多核并行解密
  8102. //var allDecrypted = rawLocalDatas.AsParallel().Select(item => new InvitationAIInfo
  8103. //{
  8104. // Guid = Guid.NewGuid().ToString("N"),
  8105. // Source = 0, // 标识来源:本地
  8106. // Region = AesEncryptionHelper.Decrypt(item.Country),
  8107. // NameCn = AesEncryptionHelper.Decrypt(item.UnitName),
  8108. // Address = AesEncryptionHelper.Decrypt(item.Address),
  8109. // Scope = AesEncryptionHelper.Decrypt(item.Field),
  8110. // Contact = AesEncryptionHelper.Decrypt(item.Contact),
  8111. // Phone = AesEncryptionHelper.Decrypt(item.Tel),
  8112. // Email = AesEncryptionHelper.Decrypt(item.Email),
  8113. // OperatedAt = DateTime.Now,
  8114. // Operator = operatorName,
  8115. //}).ToList();
  8116. //// 筛选符合国家的本地数据
  8117. //var matchedCountries = allDecrypted.Where(x => targetCountrySet.Contains(x.Region)).ToList();
  8118. var matchedCountries = new List<LocalAgencyInfo>();
  8119. #endregion
  8120. #region 3. 混元 AI 协同炼金
  8121. await HttpContext.SendSseStepAsync(30, $"正在计算单位资料配额...");
  8122. // --- 差额补全 (Gap Filling) ---
  8123. var localDatas = new List<LocalAgencyInfo>();
  8124. var aiTasks = new List<CountryAIPormptInfo>();
  8125. if (targetCountrySet.Any())
  8126. {
  8127. foreach (var item in targetCountrySet)
  8128. {
  8129. aiTasks.Add(new CountryAIPormptInfo() {
  8130. Count = entryInfo.NeedCount,
  8131. Country = item
  8132. });
  8133. }
  8134. }
  8135. var hunyuanAIInvDatas = new List<LocalAgencyInfo>();
  8136. if (aiTasks.Any())
  8137. {
  8138. await HttpContext.SendSseStepAsync(60, $"AI 正在跨境检索 {string.Join(", ", aiTasks.Select(x => $"{x.Country}({x.Count}条)"))} 单位资料...");
  8139. string searchQuestion = OpLocalBuildHunyuanPrompt(aiTasks, entryInfo);
  8140. _logger.LogInformation(@"地接名称:{InvName}; TokenHub AI查询提示词:{searchQuestion}", localAiInfo.LocalName, searchQuestion);
  8141. string searchRaw = string.Empty;
  8142. var reply = await _tokenHubService.ChatAsync(new ChatRequest()
  8143. {
  8144. Messages = new List<OASystem.API.OAMethodLib.TokenHubAI.Models.ChatMessage>()
  8145. {
  8146. new() { Content = searchQuestion }
  8147. }
  8148. });
  8149. if (reply.IsSuccess) searchRaw = reply?.Content ?? string.Empty;
  8150. else
  8151. {
  8152. await HttpContext.SendSseStepAsync(-1, $"TokenHub AI 查询资料失败: {reply?.ErrorMessage}");
  8153. }
  8154. _logger.LogInformation(@"respose json data:{jsondata};", searchRaw);
  8155. var aiParsed = CleanAndParseJson<List<LocalAgencyInfo>>(searchRaw);
  8156. if (aiParsed != null)
  8157. {
  8158. hunyuanAIInvDatas = aiParsed.Select(x => {
  8159. x.Guid = Guid.NewGuid().ToString("N");
  8160. x.Source = 1;
  8161. x.Operator = operatorName;
  8162. x.OperatedAt = DateTime.Now;
  8163. return x;
  8164. }).ToList();
  8165. }
  8166. }
  8167. #endregion
  8168. await HttpContext.SendSseStepAsync(90, "正在同步成果至持久化仓库...");
  8169. #region 4. 数据融合与更新
  8170. var finalResult = localDatas.Concat(hunyuanAIInvDatas).ToList();
  8171. // 仅更新指定列,性能更优
  8172. localAiInfo.AiCrawledDetails = finalResult.OrderByDescending(x => x.OperatedAt).ToList();
  8173. bool isOk = await _sqlSugar.Updateable(localAiInfo)
  8174. .UpdateColumns(x => x.AiCrawledDetails)
  8175. .ExecuteCommandHasChangeAsync();
  8176. if (!isOk) await HttpContext.SendSseStepAsync(-1, $"数据持久化失败。");
  8177. #endregion
  8178. // 5. 终焉推送
  8179. await HttpContext.SendSseStepAsync(100, "操作成功!资料已全部就绪。", new
  8180. {
  8181. localAiInfo.Id,
  8182. AiCrawledDetails = finalResult.OrderByDescending(x => x.Source).ThenBy(x => x.Region).ToList()
  8183. });
  8184. }
  8185. catch (TokenHubException ex)
  8186. {
  8187. await HttpContext.SendSseStepAsync(-1, $"TokenHub AI 调用失败: {ex.Message}");
  8188. }
  8189. catch (Exception ex)
  8190. {
  8191. await HttpContext.SendSseStepAsync(-1, $"SSE 错误:{ex.InnerException.Message}({ex.Message})");
  8192. }
  8193. finally
  8194. {
  8195. await HttpContext.FinalizeSseAsync();
  8196. }
  8197. }
  8198. /// <summary>
  8199. /// OP地接资料AI TokenHub AI续写(SSE流式推送)
  8200. /// </summary>
  8201. /// <param name="dto"></param>
  8202. /// <returns></returns>
  8203. [HttpPost]
  8204. public async Task OpLocalServiceAICompleteTextStream([FromBody] OpLocalServiceAICompleteTextStreamDto dto)
  8205. {
  8206. HttpContext.InitializeSse();
  8207. try
  8208. {
  8209. // 初始化检查 (Progress: 5%)
  8210. var invAiInfo = await _sqlSugar.Queryable<Res_OpLocalAI>().Where(x => x.Id == dto.Id).FirstAsync();
  8211. if (invAiInfo?.EntryInfo == null)
  8212. {
  8213. await HttpContext.SendSseStepAsync(-1, "请先设置关键字信息!");
  8214. return;
  8215. }
  8216. await HttpContext.SendSseStepAsync(10, "任务初始化完成,正在计算补齐缺口...");
  8217. // 任务拆解逻辑
  8218. var entryInfo = invAiInfo.EntryInfo;
  8219. var aiTasks = new List<CountryAIPormptInfo>();
  8220. int targetPerCountry = entryInfo.NeedCount;
  8221. foreach (var countryName in entryInfo.TargetCountry)
  8222. {
  8223. var countryDataCount = invAiInfo.AiCrawledDetails.Count(x => x.Region == countryName);
  8224. int aiNeedCount = targetPerCountry - countryDataCount;
  8225. if (aiNeedCount > 0) aiTasks.Add(new() { Country = countryName, Count = aiNeedCount });
  8226. }
  8227. if (!aiTasks.Any())
  8228. {
  8229. await HttpContext.SendSseStepAsync(100, "数据已满额,无需续写", invAiInfo.AiCrawledDetails);
  8230. return;
  8231. }
  8232. // 准备 AI Prompt (Progress: 20%)
  8233. var existingNames = new HashSet<string>(invAiInfo.AiCrawledDetails.Where(x => x.Source == 1).Select(x => x.NameCn));
  8234. string promptOther = $"请基于以下已存在的名称列表进行推荐,避免重复:{string.Join(", ", existingNames)}。{entryInfo.OtherConstraints}";
  8235. entryInfo.OtherConstraints = promptOther; // 将去重提示注入 entryInfo,确保 Prompt 构建时包含该信息
  8236. // 构建 Question
  8237. string question = OpLocalBuildHunyuanPrompt(aiTasks, entryInfo);
  8238. await HttpContext.SendSseStepAsync(30, "AI 正在深度检索跨境OP地接数据,请稍候...");
  8239. // 调用 AI (Progress: 30% - 80%)
  8240. string aiRawResponse = string.Empty;
  8241. var request = new ChatRequest()
  8242. {
  8243. Messages = new List<OASystem.API.OAMethodLib.TokenHubAI.Models.ChatMessage>()
  8244. {
  8245. new() { Content = question }
  8246. }
  8247. };
  8248. var reply = await _tokenHubService.ChatAsync(request);
  8249. if (reply.IsSuccess) aiRawResponse = reply?.Content ?? string.Empty;
  8250. else
  8251. {
  8252. await HttpContext.SendSseStepAsync(-1, $"TokenHub AI 续写资料失败: {reply?.ErrorMessage}");
  8253. }
  8254. await HttpContext.SendSseStepAsync(85, "数据已捕获,正在进行格式校验与去重...");
  8255. // 5. 解析与清洗数据
  8256. var hunyuanAIInvDatas = new List<LocalAgencyInfo>();
  8257. if (!string.IsNullOrWhiteSpace(aiRawResponse))
  8258. {
  8259. string cleanJson = aiRawResponse.Trim();
  8260. if (cleanJson.Contains("```json"))
  8261. {
  8262. cleanJson = Regex.Match(cleanJson, @"```json([\s\S]*?)```").Groups[1].Value.Trim();
  8263. }
  8264. else if (cleanJson.Contains("```"))
  8265. {
  8266. cleanJson = Regex.Match(cleanJson, @"```([\s\S]*?)```").Groups[1].Value.Trim();
  8267. }
  8268. hunyuanAIInvDatas = JsonConvert.DeserializeObject<List<LocalAgencyInfo>>(cleanJson) ?? new List<LocalAgencyInfo>();
  8269. }
  8270. string operatorName = await _sqlSugar.Queryable<Sys_Users>().Where(x => x.Id == dto.CurrUserId).Select(x => x.CnName).FirstAsync() ?? "-";
  8271. foreach (var x in hunyuanAIInvDatas)
  8272. {
  8273. x.Guid = Guid.NewGuid().ToString("N");
  8274. x.Source = 1;
  8275. x.Operator = operatorName;
  8276. x.OperatedAt = DateTime.Now;
  8277. }
  8278. // 6. 数据库操作 (Progress: 95%)
  8279. invAiInfo.AiCrawledDetails.AddRange(hunyuanAIInvDatas);
  8280. invAiInfo.AiCrawledDetails = invAiInfo.AiCrawledDetails.OrderByDescending(x => x.OperatedAt).ToList();
  8281. var update = await _sqlSugar.Updateable(invAiInfo).UpdateColumns(x => x.AiCrawledDetails).ExecuteCommandAsync();
  8282. if (update > 0)
  8283. {
  8284. await HttpContext.SendSseStepAsync(100, $"AI 续写成功!新增 {hunyuanAIInvDatas.Count} 条数据", invAiInfo.AiCrawledDetails);
  8285. }
  8286. else
  8287. {
  8288. await HttpContext.SendSseStepAsync(-1, "数据库更新失败");
  8289. }
  8290. }
  8291. catch (TokenHubException ex)
  8292. {
  8293. await HttpContext.SendSseStepAsync(-1, $"TokenHub AI 调用失败: {ex.Message}");
  8294. }
  8295. catch (Exception ex)
  8296. {
  8297. _logger.LogError(ex, "SSE 续写异常");
  8298. await HttpContext.SendSseStepAsync(-1, $"炼金炸炉:{ex.Message}");
  8299. }
  8300. finally
  8301. {
  8302. await HttpContext.FinalizeSseAsync();
  8303. }
  8304. }
  8305. /// <summary>
  8306. /// OP地接资料AI 生成报价邮件(SSE流式推送)
  8307. /// </summary>
  8308. /// <returns></returns>
  8309. [HttpPost]
  8310. public async Task OpLocalServiceAIGenerateEmailStream([FromBody] OpLocalServiceAIGenerateEmailStreamDto dto)
  8311. {
  8312. HttpContext.InitializeSse();
  8313. try
  8314. {
  8315. // 1. 基础校验与资源加载 (Progress: 5%)
  8316. if (dto.Id < 1 || dto.Guids == null || !dto.Guids.Any())
  8317. {
  8318. await HttpContext.SendSseStepAsync(-1, "请求参数不完整,请选择单位");
  8319. return;
  8320. }
  8321. var localAiInfo = await _sqlSugar.Queryable<Res_OpLocalAI>().Where(x => x.Id == dto.Id).FirstAsync();
  8322. if (localAiInfo?.AiCrawledDetails == null)
  8323. {
  8324. await HttpContext.SendSseStepAsync(-1, "基础信息不存在");
  8325. return;
  8326. }
  8327. await HttpContext.SendSseStepAsync(10, "AI 正在分析考察团组背景与访问意图...");
  8328. // 2. 准备 AI 上下文数据 (Progress: 15%)
  8329. var clientInfoSources = localAiInfo.AiCrawledDetails.Where(x => dto.Guids.Contains(x.Guid)).ToList();
  8330. var groupInfo = await _sqlSugar.Queryable<Grp_DelegationInfo>()
  8331. .Where(x => x.IsDel == 0 && x.Id == localAiInfo.GroupId)
  8332. .Select(x => new { x.TeamName, x.VisitPNumber, x.VisitDate, x.VisitStartDate, x.VisitEndDate, x.VisitCountry })
  8333. .FirstAsync();
  8334. string dateRange = $"{groupInfo.VisitStartDate:yyyy-MM-dd} ~ {groupInfo.VisitEndDate:yyyy-MM-dd}";
  8335. string operatorName = await _sqlSugar.Queryable<Sys_Users>()
  8336. .Where(x => x.Id == dto.CurrUserId).Select(x => x.CnName).FirstAsync() ?? "-";
  8337. // 3. 构建 Prompt 并调用 AI (Progress: 25%)
  8338. await HttpContext.SendSseStepAsync(30, $"AI 正在为 {clientInfoSources.Count} 家单位撰写定制化正式邮件...");
  8339. // 构建生成邮件的 Prompt
  8340. string pormpt = BuildBatchCreateEmailPrompt(
  8341. new AICreateEmailTask() {
  8342. PersonCount = groupInfo.VisitPNumber,
  8343. DateRange = dateRange,
  8344. Agencies = clientInfoSources,
  8345. EntryInfo = localAiInfo.EntryInfo
  8346. }
  8347. );
  8348. // 调用 AI (此处为阻塞式等待 AI 结果,若混元支持 Stream 可进一步拆解)
  8349. string aiResponse = string.Empty;
  8350. var reply = await _tokenHubService.ChatAsync(new ChatRequest()
  8351. {
  8352. Messages = new List<OASystem.API.OAMethodLib.TokenHubAI.Models.ChatMessage>()
  8353. {
  8354. new() { Content = pormpt }
  8355. }
  8356. });
  8357. if (reply.IsSuccess) aiResponse = reply?.Content ?? string.Empty;
  8358. else
  8359. {
  8360. await HttpContext.SendSseStepAsync(-1, $"TokenHub AI 报价邮件编写失败: {reply?.ErrorMessage}");
  8361. }
  8362. await HttpContext.SendSseStepAsync(80, "邮件初稿已生成,正在进行 HTML 格式校验与转义处理...");
  8363. // 4. 解析结果 (Progress: 85%)
  8364. var hunyuanAIEmailDatas = new List<AICreateEmailInfo>();
  8365. if (!string.IsNullOrWhiteSpace(aiResponse))
  8366. {
  8367. // 预处理:过滤 AI 可能返回的 Markdown 标记
  8368. string cleanJson = aiResponse.Trim();
  8369. if (cleanJson.StartsWith("```json"))
  8370. cleanJson = cleanJson.Substring(7, cleanJson.Length - 10).Trim();
  8371. else if (cleanJson.StartsWith("```"))
  8372. cleanJson = cleanJson.Substring(3, cleanJson.Length - 6).Trim();
  8373. try
  8374. {
  8375. // 解析并注入 Source 标识
  8376. hunyuanAIEmailDatas = JsonConvert.DeserializeObject<List<AICreateEmailInfo>>(cleanJson);
  8377. }
  8378. catch (JsonException ex)
  8379. {
  8380. // 记录日志并考虑 fallback 策略
  8381. _logger.LogError(ex, "Hunyuan AI 响应解析失败。原始数据:{Response}", aiResponse);
  8382. }
  8383. }
  8384. if (hunyuanAIEmailDatas == null || !hunyuanAIEmailDatas.Any())
  8385. {
  8386. await HttpContext.SendSseStepAsync(-1, "AI 格式解析失败,请尝试重新生成");
  8387. return;
  8388. }
  8389. // 5. 更新数据模型 (Progress: 90%)
  8390. foreach (var client in clientInfoSources)
  8391. {
  8392. var aiEmail = hunyuanAIEmailDatas.FirstOrDefault(x => x.Guid == client.Guid);
  8393. if (aiEmail != null)
  8394. {
  8395. client.EmailInfo.Status = 2; // 已生成
  8396. client.EmailInfo.EmailTitle = aiEmail.Subject;
  8397. client.EmailInfo.EmailContent = aiEmail.Content;
  8398. client.EmailInfo.Operator = operatorName;
  8399. client.EmailInfo.OperatedAt = DateTime.Now;
  8400. }
  8401. }
  8402. localAiInfo.AiCrawledDetails = clientInfoSources.OrderByDescending(x => x.IsChecked).ThenByDescending(x => x.OperatedAt).ToList();
  8403. // 6. 数据库持久化 (Progress: 95%)
  8404. // 采用增量更新策略,避免直接 Where 过滤掉其他未选中的数据
  8405. var update = await _sqlSugar.Updateable(localAiInfo).UpdateColumns(x => x.AiCrawledDetails).ExecuteCommandAsync();
  8406. if (update > 0)
  8407. {
  8408. await HttpContext.SendSseStepAsync(100, "邮件全部生成完毕并已存入团组资料库", localAiInfo.AiCrawledDetails);
  8409. }
  8410. else
  8411. {
  8412. await HttpContext.SendSseStepAsync(-1, "数据库写入失败,请联系管理员");
  8413. }
  8414. }
  8415. catch (TokenHubException ex)
  8416. {
  8417. await HttpContext.SendSseStepAsync(-1, $"TokenHub AI 调用失败: {ex.Message}");
  8418. }
  8419. catch (Exception ex)
  8420. {
  8421. _logger.LogError(ex, "邮件生成异常");
  8422. await HttpContext.SendSseStepAsync(-1, $"生成失败:{ex.Message}");
  8423. }
  8424. finally
  8425. {
  8426. await HttpContext.FinalizeSseAsync();
  8427. }
  8428. }
  8429. /// <summary>
  8430. /// OP地接资料AI 邮件保存
  8431. /// </summary>
  8432. /// <returns></returns>
  8433. [HttpPost]
  8434. [ProducesResponseType(typeof(JsonView), StatusCodes.Status200OK)]
  8435. public async Task<IActionResult> OpLocalServiceAIEmailSave(OpLocalServiceAIEmailSaveDto dto)
  8436. {
  8437. if (dto.Id < 1) return Ok(JsonView(false, "请选择保存的团组"));
  8438. if (dto.CurrUserId < 1) return Ok(JsonView(false, "请传入用户Id"));
  8439. if (string.IsNullOrEmpty(dto.Guid)) return Ok(JsonView(false, "请传入Guid"));
  8440. if (string.IsNullOrEmpty(dto.EmailTitle)) return Ok(JsonView(false, "请传入邮件标题"));
  8441. if (string.IsNullOrEmpty(dto.EmailContent)) return Ok(JsonView(false, "请传入邮件内容"));
  8442. var opLocalAiInfo = await _sqlSugar.Queryable<Res_OpLocalAI>()
  8443. .Where(x => x.Id == dto.Id)
  8444. .FirstAsync();
  8445. if (opLocalAiInfo == null) return Ok(JsonView(false, "地接资料方信息为空"));
  8446. var dataList = opLocalAiInfo?.AiCrawledDetails;
  8447. if (dataList == null) return Ok(JsonView(false, "地接资料方信息为空"));
  8448. var editInfo = dataList.Where(x => x.Guid == dto.Guid).FirstOrDefault();
  8449. if (editInfo == null) return Ok(JsonView(false, "地接资料方信息为空"));
  8450. var opUserName = await _sqlSugar.Queryable<Sys_Users>().Where(x => x.IsDel == 0 && x.Id == dto.CurrUserId).Select(x => x.CnName).FirstAsync() ?? "-";
  8451. editInfo.EmailInfo.EmailTitle = dto.EmailTitle;
  8452. editInfo.EmailInfo.EmailContent = dto.EmailContent;
  8453. editInfo.EmailInfo.Operator = opUserName;
  8454. editInfo.EmailInfo.OperatedAt = DateTime.Now;
  8455. var datas = dataList.Where(x => x.Guid != dto.Guid).ToList();
  8456. datas.Add(editInfo);
  8457. // 排序
  8458. opLocalAiInfo.AiCrawledDetails = datas.OrderByDescending(x => x.OperatedAt).ToList();
  8459. var editUpd = await _sqlSugar.Updateable(opLocalAiInfo).UpdateColumns(x => x.AiCrawledDetails).ExecuteCommandAsync();
  8460. if (editUpd < 1)
  8461. {
  8462. return Ok(JsonView(true, "邮件保存失败"));
  8463. }
  8464. return Ok(JsonView(true, "邮件保存成功"));
  8465. }
  8466. /// <summary>
  8467. /// OP地接资料AI 邮箱附件上传
  8468. /// </summary>
  8469. [HttpPost]
  8470. [ProducesResponseType(typeof(JsonView), StatusCodes.Status200OK)]
  8471. public async Task<IActionResult> OpLocalServiceAIFileSave([FromForm] OpLocalServiceAIFileSaveDto dto)
  8472. {
  8473. // 1. 炼金前置:严格参数校验
  8474. if (dto.Id < 1) return Ok(JsonView(false, "请选择保存的团组"));
  8475. if (dto.CurrUserId < 1) return Ok(JsonView(false, "请传入用户Id"));
  8476. if (string.IsNullOrEmpty(dto.Guid)) return Ok(JsonView(false, "请传入Guid"));
  8477. if (dto.Attachments == null || !dto.Attachments.Any()) return Ok(JsonView(false, "请传入附件"));
  8478. // 2. 数据获取:利用 SqlSugar 异步查询
  8479. var opAiInfo = await _sqlSugar.Queryable<Res_OpLocalAI>()
  8480. .InSingleAsync(dto.Id);
  8481. if (opAiInfo?.AiCrawledDetails == null) return Ok(JsonView(false, "地接方信息不存在"));
  8482. // 3. 业务定位:定位到具体需要编辑的 Guid 记录
  8483. var editInfo = opAiInfo.AiCrawledDetails.FirstOrDefault(x => x.Guid == dto.Guid);
  8484. if (editInfo == null) return Ok(JsonView(false, "未找到匹配的 Guid 记录"));
  8485. // 4. 追踪信息更新
  8486. var opUserName = await _sqlSugar.Queryable<Sys_Users>()
  8487. .Where(x => x.Id == dto.CurrUserId && x.IsDel == 0)
  8488. .Select(x => x.CnName)
  8489. .FirstAsync() ?? "-";
  8490. // 更新操作人与时间 (利用引用类型特性)
  8491. editInfo.Operator = opUserName;
  8492. editInfo.OperatedAt = DateTime.Now;
  8493. // 初始化 EmailInfo 确保不为 null
  8494. editInfo.EmailInfo ??= new LocalEmailInfo();
  8495. editInfo.EmailInfo.Operator = opUserName;
  8496. editInfo.EmailInfo.OperatedAt = DateTime.Now;
  8497. editInfo.EmailInfo.AttachmentPaths ??= new List<string>();
  8498. // 5. 构建绝对路径与相对路径
  8499. string dirName = $"{editInfo.NameEn?.Trim() ?? "Default"}_{dto.Guid}";
  8500. string baseDir = AppSettingsHelper.Get("OpLocalServiceAIAssistBasePath");
  8501. string ftpBase = AppSettingsHelper.Get("OpLocalServiceAIAssistFtpPath");
  8502. string absolutePath = Path.Combine(baseDir, dirName);
  8503. string relativePathPrefix = $"{ftpBase}{dirName}/";
  8504. // 核心修复:确保父级目录递归创建,防止 DirectoryNotFoundException
  8505. if (!Directory.Exists(absolutePath))
  8506. {
  8507. Directory.CreateDirectory(absolutePath);
  8508. }
  8509. var newSavedRelativePaths = new List<string>();
  8510. // 6. 持续文件存储与实时验证
  8511. foreach (var file in dto.Attachments)
  8512. {
  8513. // 文件名安全过滤
  8514. string safeName = string.Join("_", file.FileName.Split(Path.GetInvalidFileNameChars()));
  8515. string fullPath = Path.Combine(absolutePath, safeName);
  8516. // 重名冲突处理:文件名(n).ext
  8517. if (System.IO.File.Exists(fullPath))
  8518. {
  8519. string fileNameOnly = Path.GetFileNameWithoutExtension(safeName);
  8520. string extension = Path.GetExtension(safeName);
  8521. int count = 1;
  8522. while (System.IO.File.Exists(fullPath))
  8523. {
  8524. safeName = $"{fileNameOnly}({count++}){extension}";
  8525. fullPath = Path.Combine(absolutePath, safeName);
  8526. }
  8527. }
  8528. try
  8529. {
  8530. // 写入流:使用作用域隔离,确保退出 using 时立即释放句柄
  8531. using (var stream = new FileStream(fullPath, FileMode.Create, FileAccess.Write, FileShare.None))
  8532. {
  8533. await file.CopyToAsync(stream);
  8534. await stream.FlushAsync(); // 强制落盘
  8535. }
  8536. // 实时验证:物理验证 + 逻辑校验
  8537. var fileInfo = new FileInfo(fullPath);
  8538. if (fileInfo.Exists && fileInfo.Length == file.Length)
  8539. {
  8540. newSavedRelativePaths.Add($"{relativePathPrefix}{safeName}");
  8541. }
  8542. }
  8543. catch (Exception ex)
  8544. {
  8545. // 发生 IO 异常时清理残余文件并抛出,触发外层处理
  8546. if (System.IO.File.Exists(fullPath)) System.IO.File.Delete(fullPath);
  8547. // 此处记录日志:
  8548. _logger.LogError(ex, "File Save Error");
  8549. continue; // 跳过失败的文件,继续下一个
  8550. }
  8551. }
  8552. if (!newSavedRelativePaths.Any()) return Ok(JsonView(false, "所有文件上传均失败"));
  8553. // 7. 数据合并与持久化:
  8554. // 将新路径与旧路径合并,并去重
  8555. var updatedPaths = editInfo.EmailInfo.AttachmentPaths;
  8556. updatedPaths.AddRange(newSavedRelativePaths);
  8557. editInfo.EmailInfo.AttachmentPaths = updatedPaths.Distinct().ToList();
  8558. // 重新排序整体列表
  8559. opAiInfo.AiCrawledDetails = opAiInfo.AiCrawledDetails
  8560. .OrderByDescending(x => x.OperatedAt)
  8561. .ToList();
  8562. // SqlSugar 高效部分列更新
  8563. var isSuccess = await _sqlSugar.Updateable(opAiInfo)
  8564. .UpdateColumns(x => x.AiCrawledDetails)
  8565. .ExecuteCommandAsync() > 0;
  8566. if (!isSuccess) return Ok(JsonView(false, "数据库更新失败"));
  8567. // 8. 返回前端数据 (支持 lowerCamelCase)
  8568. var officeBaseUrl = AppSettingsHelper.Get("OfficeBaseUrl");
  8569. var finalResultUrls = editInfo.EmailInfo.AttachmentPaths
  8570. .Select(path => $"{officeBaseUrl}{path}")
  8571. .ToList();
  8572. return Ok(JsonView(true, "邮件附件保存成功", finalResultUrls));
  8573. }
  8574. /// <summary>
  8575. /// OP地接资料AI 邮箱附件删除
  8576. /// </summary>
  8577. [HttpPost]
  8578. [ProducesResponseType(typeof(JsonView), StatusCodes.Status200OK)]
  8579. public async Task<IActionResult> OpLocalServiceAIFileDel([FromBody] OpLocalServiceAIFileDelDto dto)
  8580. {
  8581. // 1. 基础校验
  8582. if (dto.Id < 1) return Ok(JsonView(false, "请选择团组"));
  8583. if (string.IsNullOrEmpty(dto.Guid)) return Ok(JsonView(false, "请传入Guid"));
  8584. if (dto.AttachmentNames == null || !dto.AttachmentNames.Any()) return Ok(JsonView(false, "请传入待删除附件名称"));
  8585. // 2. 数据获取
  8586. var invAiInfo = await _sqlSugar.Queryable<Res_OpLocalAI>().InSingleAsync(dto.Id);
  8587. if (invAiInfo?.AiCrawledDetails == null) return Ok(JsonView(false, "业务数据不存在"));
  8588. var editInfo = invAiInfo.AiCrawledDetails.FirstOrDefault(x => x.Guid == dto.Guid);
  8589. if (editInfo?.EmailInfo?.AttachmentPaths == null) return Ok(JsonView(false, "未找到对应的附件记录"));
  8590. // 3. 操作人追踪
  8591. var opUserName = await _sqlSugar.Queryable<Sys_Users>()
  8592. .Where(x => x.Id == dto.CurrUserId && x.IsDel == 0)
  8593. .Select(x => x.CnName).FirstAsync() ?? "System";
  8594. // 更新追踪状态
  8595. editInfo.Operator = editInfo.EmailInfo.Operator = opUserName;
  8596. editInfo.OperatedAt = editInfo.EmailInfo.OperatedAt = DateTime.Now;
  8597. // 4. 路径
  8598. string dirName = $"{editInfo.NameEn?.Trim() ?? "Default"}_{dto.Guid}";
  8599. string absoluteDir = Path.Combine(AppSettingsHelper.Get("OpLocalServiceAIAssistBasePath"), dirName);
  8600. // 获取当前数据库中的相对路径列表
  8601. var currentPaths = editInfo.EmailInfo.AttachmentPaths;
  8602. bool isChanged = false;
  8603. // 5. 核心删除逻辑
  8604. foreach (var fileName in dto.AttachmentNames)
  8605. {
  8606. // 查找数据库中是否存在包含该文件名的路径 (忽略大小写比较)
  8607. var targetRelativePath = currentPaths.FirstOrDefault(p => p.EndsWith("/" + fileName) || p.Equals(fileName));
  8608. if (targetRelativePath != null)
  8609. {
  8610. // A. 从数据库记录中移除
  8611. currentPaths.Remove(targetRelativePath);
  8612. isChanged = true;
  8613. // B. 物理文件删除逻辑
  8614. // 注意:fullPath 必须是 [基础路径] + [目录名] + [纯文件名]
  8615. string fullPath = Path.Combine(absoluteDir, fileName);
  8616. try
  8617. {
  8618. if (System.IO.File.Exists(fullPath))
  8619. {
  8620. System.IO.File.Delete(fullPath);
  8621. }
  8622. }
  8623. catch (Exception ex)
  8624. {
  8625. // 记录 IO 异常但不中断流程
  8626. _logger.LogWarning($"物理文件删除失败: {fullPath}, {ex.Message}");
  8627. }
  8628. }
  8629. }
  8630. if (!isChanged) return Ok(JsonView(false, "未找到匹配的可删除附件"));
  8631. // 6. 数据同步与持久化
  8632. editInfo.EmailInfo.AttachmentPaths = currentPaths;
  8633. // 排序 (利用引用类型,无需手动 Add/Remove)
  8634. invAiInfo.AiCrawledDetails = invAiInfo.AiCrawledDetails.OrderByDescending(x => x.OperatedAt).ToList();
  8635. var isUpd = await _sqlSugar.Updateable(invAiInfo)
  8636. .UpdateColumns(x => x.AiCrawledDetails)
  8637. .ExecuteCommandAsync() > 0;
  8638. if (!isUpd) return Ok(JsonView(false, "数据库记录更新失败"));
  8639. // 7. 返回剩余附件的完整访问地址
  8640. var officeBaseUrl = AppSettingsHelper.Get("OfficeBaseUrl");
  8641. var remainingUrls = editInfo.EmailInfo.AttachmentPaths
  8642. .Select(path => $"{officeBaseUrl}{path}")
  8643. .ToList();
  8644. return Ok(JsonView(true, "附件删除成功", remainingUrls));
  8645. }
  8646. /// <summary>
  8647. /// OP地接资料AI 发送邮件(SSE 流式推送)
  8648. /// </summary>
  8649. [HttpPost]
  8650. public async Task OpLocalServiceAISeedEmailStream([FromBody] OpLocalServiceAISeedEmailStreamDto dto)
  8651. {
  8652. // 1. 初始化 SSE
  8653. HttpContext.InitializeSse();
  8654. try
  8655. {
  8656. await HttpContext.SendSseStepAsync(5, "正在准备发送邮件队列...");
  8657. #region 1. 参数与权限前置校验
  8658. if (dto.Id < 1 || dto.CurrUserId < 1 || dto.Guids == null || !dto.Guids.Any())
  8659. {
  8660. await HttpContext.SendSseStepAsync(-1, "参数验证失败,请检查选择的单位及用户状态。");
  8661. return;
  8662. }
  8663. // hotmail 配置信息验证
  8664. var hotmailConfig = await _hotmailService.GetUserMailConfig(dto.CurrUserId);
  8665. (bool verify, string msg) = _hotmailService.ConfigVerify(hotmailConfig);
  8666. if (!verify)
  8667. {
  8668. await HttpContext.SendSseStepAsync(-1, msg);
  8669. return;
  8670. }
  8671. // 获取地接信息和用户信息
  8672. var opAiInfo = await _sqlSugar.Queryable<Res_OpLocalAI>().FirstAsync(x => x.IsDel == 0 && x.Id == dto.Id);
  8673. var userInfo = await _sqlSugar.Queryable<Sys_Users>()
  8674. .Where(x => x.IsDel == 0 && x.Id == dto.CurrUserId)
  8675. .Select(x => new { x.Email, x.CnName })
  8676. .FirstAsync();
  8677. if (opAiInfo?.AiCrawledDetails == null)
  8678. {
  8679. await HttpContext.SendSseStepAsync(-1, "未找到有效的地接方数据。");
  8680. return;
  8681. }
  8682. // 提取待发送的目标集合
  8683. var guidSet = new HashSet<string>(dto.Guids);
  8684. var seedInvInfos = opAiInfo.AiCrawledDetails.Where(x => guidSet.Contains(x.Guid)).ToList();
  8685. if (!seedInvInfos.Any())
  8686. {
  8687. await HttpContext.SendSseStepAsync(-1, "所选单位信息在原始库中不存在。");
  8688. return;
  8689. }
  8690. #endregion
  8691. await HttpContext.SendSseStepAsync(15, $"准备就绪,共计 {seedInvInfos.Count} 封邮件待发送...");
  8692. #region 2. 批量发送逻辑 (流式反馈)
  8693. int total = seedInvInfos.Count;
  8694. int current = 0;
  8695. var successCount = 0;
  8696. var failCount = 0;
  8697. foreach (var item in seedInvInfos)
  8698. {
  8699. current++;
  8700. // 计算进度:从 20% 到 90%
  8701. int progress = 20 + (int)((double)current / total * 70);
  8702. if (string.IsNullOrEmpty(item.EmailInfo?.EmailTitle) || string.IsNullOrEmpty(item.EmailInfo?.EmailContent))
  8703. {
  8704. await HttpContext.SendSseStepAsync(progress, $"跳过:{item.NameCn} (邮件标题或内容缺失)");
  8705. failCount++;
  8706. continue;
  8707. }
  8708. try
  8709. {
  8710. var req = new MailDto()
  8711. {
  8712. Subject = item.EmailInfo.EmailTitle,
  8713. Content = item.EmailInfo.EmailContent,
  8714. To = item.Email,
  8715. AttachmentPaths = item.EmailInfo.AttachmentPaths
  8716. };
  8717. var res = await _hotmailService.SendMailAsync(hotmailConfig.UserName, req);
  8718. if (res.IsSuccess)
  8719. {
  8720. successCount++;
  8721. // 更新本地状态
  8722. item.EmailInfo.Status = 4; // 发送成功状态
  8723. item.EmailInfo.Operator = userInfo.CnName;
  8724. item.EmailInfo.OperatedAt = DateTime.Now;
  8725. await HttpContext.SendSseStepAsync(progress, $"成功:已向 {item.NameCn}({item.Email}) 发送邮件");
  8726. }
  8727. else
  8728. {
  8729. failCount++;
  8730. await HttpContext.SendSseStepAsync(progress, $"失败:{item.NameCn} 发送失败 ({res.Message})");
  8731. }
  8732. }
  8733. catch (Exception ex)
  8734. {
  8735. failCount++;
  8736. _logger.LogError(ex, $"企微邮件推送异常:{item.NameCn}");
  8737. await HttpContext.SendSseStepAsync(progress, $"异常:{item.NameCn} 连接超时");
  8738. }
  8739. }
  8740. #endregion
  8741. await HttpContext.SendSseStepAsync(95, "正在归档发送记录...");
  8742. #region 3. 数据同步与收尾
  8743. // 更新内存中的全量数据:排除旧的,加入已更新状态的
  8744. opAiInfo.AiCrawledDetails = opAiInfo.AiCrawledDetails
  8745. .Where(x => !guidSet.Contains(x.Guid))
  8746. .Concat(seedInvInfos)
  8747. .OrderByDescending(x => x.EmailInfo.OperatedAt)
  8748. .ToList();
  8749. // 精准更新 JSON 列
  8750. var updateOk = await _sqlSugar.Updateable(opAiInfo)
  8751. .UpdateColumns(x => x.AiCrawledDetails)
  8752. .ExecuteCommandHasChangeAsync();
  8753. if (!updateOk)
  8754. {
  8755. await HttpContext.SendSseStepAsync(-1, "记录保存失败,请检查数据库连接。");
  8756. return;
  8757. }
  8758. await HttpContext.SendSseStepAsync(100, $"任务完成!成功: {successCount}, 失败: {failCount}", new
  8759. {
  8760. Id = opAiInfo.Id,
  8761. SuccessCount = successCount,
  8762. FailCount = failCount
  8763. });
  8764. #endregion
  8765. }
  8766. catch (Exception ex)
  8767. {
  8768. _logger.LogError(ex, "邮件发送流发生崩溃");
  8769. await HttpContext.SendSseStepAsync(-1, $"系统错误:{ex.Message}");
  8770. }
  8771. finally
  8772. {
  8773. await HttpContext.FinalizeSseAsync();
  8774. }
  8775. }
  8776. #endregion
  8777. #region Snovio 第三方检索
  8778. /// <summary>
  8779. /// 创建 Snov.io 公司数据库搜索任务,并等待返回实际结果。
  8780. /// </summary>
  8781. /// <param name="request">公司名称、行业、地区等搜索条件。</param>
  8782. /// <param name="timeoutSeconds">最大等待秒数,不传时由 Snovio 服务使用默认值。</param>
  8783. /// <param name="pollIntervalSeconds">结果轮询间隔秒数,不传时由 Snovio 服务使用默认值。</param>
  8784. /// <param name="cancellationToken">取消令牌。</param>
  8785. /// <returns>统一响应体,Data 为公司搜索结果。</returns>
  8786. [HttpPost]
  8787. [ProducesResponseType(typeof(JsonView), StatusCodes.Status200OK)]
  8788. public async Task<IActionResult> SnovioSearchCompanies(
  8789. [FromBody] SnovioCompanySearchRequest request,
  8790. [FromQuery] int? timeoutSeconds = null,
  8791. [FromQuery] int? pollIntervalSeconds = null,
  8792. CancellationToken cancellationToken = default)
  8793. {
  8794. return await ExecuteSnovioAsync(
  8795. () => _snovioService.SearchCompaniesAsync(
  8796. request,
  8797. ToSnovioDuration(timeoutSeconds, nameof(timeoutSeconds)),
  8798. ToSnovioDuration(pollIntervalSeconds, nameof(pollIntervalSeconds)),
  8799. cancellationToken),
  8800. "Snovio 公司搜索");
  8801. }
  8802. /// <summary>
  8803. /// 创建 Snov.io 公司数据库搜索任务,仅返回 task_hash,不等待最终结果。
  8804. /// </summary>
  8805. /// <param name="request">公司名称、行业、地区等搜索条件。</param>
  8806. /// <param name="cancellationToken">取消令牌。</param>
  8807. /// <returns>统一响应体,Data 为包含 task_hash 和结果地址的任务响应。</returns>
  8808. [HttpPost]
  8809. [ProducesResponseType(typeof(JsonView), StatusCodes.Status200OK)]
  8810. public async Task<IActionResult> SnovioStartCompanySearch(
  8811. [FromBody] SnovioCompanySearchRequest request,
  8812. CancellationToken cancellationToken = default)
  8813. {
  8814. return await ExecuteSnovioAsync(
  8815. () => _snovioService.StartCompanySearchAsync(request, cancellationToken),
  8816. "Snovio 公司搜索任务创建");
  8817. }
  8818. /// <summary>
  8819. /// 获取 Snov.io 公司数据库搜索任务当前结果。
  8820. /// </summary>
  8821. /// <param name="taskHash">创建任务时返回的 task_hash。</param>
  8822. /// <param name="cancellationToken">取消令牌。</param>
  8823. /// <returns>统一响应体,Data 为公司搜索结果;任务处理中时返回 in_progress 状态。</returns>
  8824. [HttpGet]
  8825. [ProducesResponseType(typeof(JsonView), StatusCodes.Status200OK)]
  8826. public async Task<IActionResult> SnovioGetCompanySearchResult(
  8827. [FromQuery] string taskHash,
  8828. CancellationToken cancellationToken = default)
  8829. {
  8830. return await ExecuteSnovioAsync(
  8831. () => _snovioService.GetCompanySearchResultAsync(taskHash, cancellationToken),
  8832. "Snovio 公司搜索结果获取");
  8833. }
  8834. /// <summary>
  8835. /// 创建 Snov.io 域名潜在客户搜索任务,并等待返回实际结果。
  8836. /// </summary>
  8837. /// <param name="request">域名、职位和分页等搜索条件。</param>
  8838. /// <param name="timeoutSeconds">最大等待秒数,不传时由 Snovio 服务使用默认值。</param>
  8839. /// <param name="pollIntervalSeconds">结果轮询间隔秒数,不传时由 Snovio 服务使用默认值。</param>
  8840. /// <param name="cancellationToken">取消令牌。</param>
  8841. /// <returns>统一响应体,Data 为域名潜在客户搜索结果。</returns>
  8842. [HttpPost]
  8843. [ProducesResponseType(typeof(JsonView), StatusCodes.Status200OK)]
  8844. public async Task<IActionResult> SnovioSearchDomainProspects(
  8845. [FromBody] SnovioDomainProspectsSearchRequest request,
  8846. [FromQuery] int? timeoutSeconds = null,
  8847. [FromQuery] int? pollIntervalSeconds = null,
  8848. CancellationToken cancellationToken = default)
  8849. {
  8850. return await ExecuteSnovioAsync(
  8851. () => _snovioService.SearchDomainProspectsAsync(
  8852. request,
  8853. ToSnovioDuration(timeoutSeconds, nameof(timeoutSeconds)),
  8854. ToSnovioDuration(pollIntervalSeconds, nameof(pollIntervalSeconds)),
  8855. cancellationToken),
  8856. "Snovio 域名潜在客户搜索");
  8857. }
  8858. /// <summary>
  8859. /// 创建 Snov.io 域名潜在客户搜索任务,仅返回 task_hash,不等待最终结果。
  8860. /// </summary>
  8861. /// <param name="request">域名、职位和分页等搜索条件。</param>
  8862. /// <param name="cancellationToken">取消令牌。</param>
  8863. /// <returns>统一响应体,Data 为包含 task_hash 和结果地址的任务响应。</returns>
  8864. [HttpPost]
  8865. [ProducesResponseType(typeof(JsonView), StatusCodes.Status200OK)]
  8866. public async Task<IActionResult> SnovioStartDomainProspectsSearch(
  8867. [FromBody] SnovioDomainProspectsSearchRequest request,
  8868. CancellationToken cancellationToken = default)
  8869. {
  8870. return await ExecuteSnovioAsync(
  8871. () => _snovioService.StartDomainProspectsSearchAsync(request, cancellationToken),
  8872. "Snovio 域名潜在客户搜索任务创建");
  8873. }
  8874. /// <summary>
  8875. /// 获取 Snov.io 域名潜在客户搜索任务当前结果。
  8876. /// </summary>
  8877. /// <param name="taskHash">创建任务时返回的 task_hash。</param>
  8878. /// <param name="cancellationToken">取消令牌。</param>
  8879. /// <returns>统一响应体,Data 为潜在客户列表;任务处理中时返回 in_progress 状态。</returns>
  8880. [HttpGet]
  8881. [ProducesResponseType(typeof(JsonView), StatusCodes.Status200OK)]
  8882. public async Task<IActionResult> SnovioGetDomainProspectsSearchResult(
  8883. [FromQuery] string taskHash,
  8884. CancellationToken cancellationToken = default)
  8885. {
  8886. return await ExecuteSnovioAsync(
  8887. () => _snovioService.GetDomainProspectsSearchResultAsync(taskHash, cancellationToken),
  8888. "Snovio 域名潜在客户搜索结果获取");
  8889. }
  8890. /// <summary>
  8891. /// 创建指定潜客的 Snov.io 邮箱检索任务,并等待返回实际邮箱结果。
  8892. /// </summary>
  8893. /// <param name="prospectHash">域名潜在客户搜索结果中返回的 prospect_hash。</param>
  8894. /// <param name="request">可选的邮箱检索请求体,目前支持 webhook_url。</param>
  8895. /// <param name="timeoutSeconds">最大等待秒数,不传时由 Snovio 服务使用默认值。</param>
  8896. /// <param name="pollIntervalSeconds">结果轮询间隔秒数,不传时由 Snovio 服务使用默认值。</param>
  8897. /// <param name="cancellationToken">取消令牌。</param>
  8898. /// <returns>统一响应体,Data 为邮箱列表及其 SMTP 状态。</returns>
  8899. [HttpPost]
  8900. [ProducesResponseType(typeof(JsonView), StatusCodes.Status200OK)]
  8901. public async Task<IActionResult> SnovioSearchProspectEmail(
  8902. [FromQuery] string prospectHash,
  8903. [FromBody(EmptyBodyBehavior = Microsoft.AspNetCore.Mvc.ModelBinding.EmptyBodyBehavior.Allow)] SnovioProspectEmailSearchRequest? request,
  8904. [FromQuery] int? timeoutSeconds = null,
  8905. [FromQuery] int? pollIntervalSeconds = null,
  8906. CancellationToken cancellationToken = default)
  8907. {
  8908. return await ExecuteSnovioAsync(
  8909. () => _snovioService.SearchProspectEmailAsync(
  8910. prospectHash,
  8911. request,
  8912. ToSnovioDuration(timeoutSeconds, nameof(timeoutSeconds)),
  8913. ToSnovioDuration(pollIntervalSeconds, nameof(pollIntervalSeconds)),
  8914. cancellationToken),
  8915. "Snovio 潜客邮箱检索");
  8916. }
  8917. /// <summary>
  8918. /// 创建指定潜客的 Snov.io 邮箱检索任务,仅返回 task_hash,不等待最终结果。
  8919. /// </summary>
  8920. /// <param name="prospectHash">域名潜在客户搜索结果中返回的 prospect_hash。</param>
  8921. /// <param name="request">可选的邮箱检索请求体,目前支持 webhook_url。</param>
  8922. /// <param name="cancellationToken">取消令牌。</param>
  8923. /// <returns>统一响应体,Data 为包含 task_hash 和结果地址的任务响应。</returns>
  8924. [HttpPost]
  8925. [ProducesResponseType(typeof(JsonView), StatusCodes.Status200OK)]
  8926. public async Task<IActionResult> SnovioStartProspectEmailSearch(
  8927. [FromQuery] string prospectHash,
  8928. [FromBody(EmptyBodyBehavior = Microsoft.AspNetCore.Mvc.ModelBinding.EmptyBodyBehavior.Allow)] SnovioProspectEmailSearchRequest? request,
  8929. CancellationToken cancellationToken = default)
  8930. {
  8931. return await ExecuteSnovioAsync(
  8932. () => _snovioService.StartProspectEmailSearchAsync(
  8933. prospectHash,
  8934. request,
  8935. cancellationToken),
  8936. "Snovio 潜客邮箱检索任务创建");
  8937. }
  8938. /// <summary>
  8939. /// 获取 Snov.io 潜客邮箱检索任务当前结果。
  8940. /// </summary>
  8941. /// <param name="taskHash">邮箱检索任务创建时返回的 task_hash。</param>
  8942. /// <param name="cancellationToken">取消令牌。</param>
  8943. /// <returns>统一响应体,Data 为邮箱及 SMTP 状态;任务处理中时返回 in_progress 状态。</returns>
  8944. [HttpGet]
  8945. [ProducesResponseType(typeof(JsonView), StatusCodes.Status200OK)]
  8946. public async Task<IActionResult> SnovioGetProspectEmailSearchResult(
  8947. [FromQuery] string taskHash,
  8948. CancellationToken cancellationToken = default)
  8949. {
  8950. return await ExecuteSnovioAsync(
  8951. () => _snovioService.GetProspectEmailSearchResultAsync(taskHash, cancellationToken),
  8952. "Snovio 潜客邮箱检索结果获取");
  8953. }
  8954. /// <summary>
  8955. /// 将秒数转换为 Snovio 服务使用的时间间隔。
  8956. /// </summary>
  8957. private static TimeSpan? ToSnovioDuration(int? seconds, string parameterName)
  8958. {
  8959. if (!seconds.HasValue)
  8960. return null;
  8961. if (seconds.Value <= 0)
  8962. throw new ArgumentOutOfRangeException(parameterName, "时间参数必须大于 0 秒。");
  8963. return TimeSpan.FromSeconds(seconds.Value);
  8964. }
  8965. /// <summary>
  8966. /// 统一处理 Snovio 服务调用结果和异常。
  8967. /// </summary>
  8968. private async Task<IActionResult> ExecuteSnovioAsync<T>(
  8969. Func<Task<T>> operation,
  8970. string operationName)
  8971. {
  8972. try
  8973. {
  8974. var result = await operation();
  8975. return Ok(JsonView(true, $"{operationName}成功", result));
  8976. }
  8977. catch (OperationCanceledException)
  8978. {
  8979. throw;
  8980. }
  8981. catch (ArgumentException ex)
  8982. {
  8983. return Ok(JsonView(false, ex.Message));
  8984. }
  8985. catch (TimeoutException ex)
  8986. {
  8987. _logger.LogWarning(ex, "{OperationName}超时", operationName);
  8988. return Ok(JsonView(false, ex.Message));
  8989. }
  8990. catch (HttpRequestException ex)
  8991. {
  8992. _logger.LogError(ex, "{OperationName}请求失败", operationName);
  8993. return Ok(JsonView(false, $"{operationName}请求失败:{ex.Message}"));
  8994. }
  8995. catch (InvalidOperationException ex)
  8996. {
  8997. _logger.LogError(ex, "{OperationName}返回结果无效或任务失败", operationName);
  8998. return Ok(JsonView(false, ex.Message));
  8999. }
  9000. catch (Exception ex)
  9001. {
  9002. _logger.LogError(ex, "{OperationName}发生未知异常", operationName);
  9003. return Ok(JsonView(false, $"{operationName}失败,请稍后重试。"));
  9004. }
  9005. }
  9006. #endregion
  9007. }
  9008. }