app.py 330 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976977978979980981982983984985986987988989990991992993994995996997998999100010011002100310041005100610071008100910101011101210131014101510161017101810191020102110221023102410251026102710281029103010311032103310341035103610371038103910401041104210431044104510461047104810491050105110521053105410551056105710581059106010611062106310641065106610671068106910701071107210731074107510761077107810791080108110821083108410851086108710881089109010911092109310941095109610971098109911001101110211031104110511061107110811091110111111121113111411151116111711181119112011211122112311241125112611271128112911301131113211331134113511361137113811391140114111421143114411451146114711481149115011511152115311541155115611571158115911601161116211631164116511661167116811691170117111721173117411751176117711781179118011811182118311841185118611871188118911901191119211931194119511961197119811991200120112021203120412051206120712081209121012111212121312141215121612171218121912201221122212231224122512261227122812291230123112321233123412351236123712381239124012411242124312441245124612471248124912501251125212531254125512561257125812591260126112621263126412651266126712681269127012711272127312741275127612771278127912801281128212831284128512861287128812891290129112921293129412951296129712981299130013011302130313041305130613071308130913101311131213131314131513161317131813191320132113221323132413251326132713281329133013311332133313341335133613371338133913401341134213431344134513461347134813491350135113521353135413551356135713581359136013611362136313641365136613671368136913701371137213731374137513761377137813791380138113821383138413851386138713881389139013911392139313941395139613971398139914001401140214031404140514061407140814091410141114121413141414151416141714181419142014211422142314241425142614271428142914301431143214331434143514361437143814391440144114421443144414451446144714481449145014511452145314541455145614571458145914601461146214631464146514661467146814691470147114721473147414751476147714781479148014811482148314841485148614871488148914901491149214931494149514961497149814991500150115021503150415051506150715081509151015111512151315141515151615171518151915201521152215231524152515261527152815291530153115321533153415351536153715381539154015411542154315441545154615471548154915501551155215531554155515561557155815591560156115621563156415651566156715681569157015711572157315741575157615771578157915801581158215831584158515861587158815891590159115921593159415951596159715981599160016011602160316041605160616071608160916101611161216131614161516161617161816191620162116221623162416251626162716281629163016311632163316341635163616371638163916401641164216431644164516461647164816491650165116521653165416551656165716581659166016611662166316641665166616671668166916701671167216731674167516761677167816791680168116821683168416851686168716881689169016911692169316941695169616971698169917001701170217031704170517061707170817091710171117121713171417151716171717181719172017211722172317241725172617271728172917301731173217331734173517361737173817391740174117421743174417451746174717481749175017511752175317541755175617571758175917601761176217631764176517661767176817691770177117721773177417751776177717781779178017811782178317841785178617871788178917901791179217931794179517961797179817991800180118021803180418051806180718081809181018111812181318141815181618171818181918201821182218231824182518261827182818291830183118321833183418351836183718381839184018411842184318441845184618471848184918501851185218531854185518561857185818591860186118621863186418651866186718681869187018711872187318741875187618771878187918801881188218831884188518861887188818891890189118921893189418951896189718981899190019011902190319041905190619071908190919101911191219131914191519161917191819191920192119221923192419251926192719281929193019311932193319341935193619371938193919401941194219431944194519461947194819491950195119521953195419551956195719581959196019611962196319641965196619671968196919701971197219731974197519761977197819791980198119821983198419851986198719881989199019911992199319941995199619971998199920002001200220032004200520062007200820092010201120122013201420152016201720182019202020212022202320242025202620272028202920302031203220332034203520362037203820392040204120422043204420452046204720482049205020512052205320542055205620572058205920602061206220632064206520662067206820692070207120722073207420752076207720782079208020812082208320842085208620872088208920902091209220932094209520962097209820992100210121022103210421052106210721082109211021112112211321142115211621172118211921202121212221232124212521262127212821292130213121322133213421352136213721382139214021412142214321442145214621472148214921502151215221532154215521562157215821592160216121622163216421652166216721682169217021712172217321742175217621772178217921802181218221832184218521862187218821892190219121922193219421952196219721982199220022012202220322042205220622072208220922102211221222132214221522162217221822192220222122222223222422252226222722282229223022312232223322342235223622372238223922402241224222432244224522462247224822492250225122522253225422552256225722582259226022612262226322642265226622672268226922702271227222732274227522762277227822792280228122822283228422852286228722882289229022912292229322942295229622972298229923002301230223032304230523062307230823092310231123122313231423152316231723182319232023212322232323242325232623272328232923302331233223332334233523362337233823392340234123422343234423452346234723482349235023512352235323542355235623572358235923602361236223632364236523662367236823692370237123722373237423752376237723782379238023812382238323842385238623872388238923902391239223932394239523962397239823992400240124022403240424052406240724082409241024112412241324142415241624172418241924202421242224232424242524262427242824292430243124322433243424352436243724382439244024412442244324442445244624472448244924502451245224532454245524562457245824592460246124622463246424652466246724682469247024712472247324742475247624772478247924802481248224832484248524862487248824892490249124922493249424952496249724982499250025012502250325042505250625072508250925102511251225132514251525162517251825192520252125222523252425252526252725282529253025312532253325342535253625372538253925402541254225432544254525462547254825492550255125522553255425552556255725582559256025612562256325642565256625672568256925702571257225732574257525762577257825792580258125822583258425852586258725882589259025912592259325942595259625972598259926002601260226032604260526062607260826092610261126122613261426152616261726182619262026212622262326242625262626272628262926302631263226332634263526362637263826392640264126422643264426452646264726482649265026512652265326542655265626572658265926602661266226632664266526662667266826692670267126722673267426752676267726782679268026812682268326842685268626872688268926902691269226932694269526962697269826992700270127022703270427052706270727082709271027112712271327142715271627172718271927202721272227232724272527262727272827292730273127322733273427352736273727382739274027412742274327442745274627472748274927502751275227532754275527562757275827592760276127622763276427652766276727682769277027712772277327742775277627772778277927802781278227832784278527862787278827892790279127922793279427952796279727982799280028012802280328042805280628072808280928102811281228132814281528162817281828192820282128222823282428252826282728282829283028312832283328342835283628372838283928402841284228432844284528462847284828492850285128522853285428552856285728582859286028612862286328642865286628672868286928702871287228732874287528762877287828792880288128822883288428852886288728882889289028912892289328942895289628972898289929002901290229032904290529062907290829092910291129122913291429152916291729182919292029212922292329242925292629272928292929302931293229332934293529362937293829392940294129422943294429452946294729482949295029512952295329542955295629572958295929602961296229632964296529662967296829692970297129722973297429752976297729782979298029812982298329842985298629872988298929902991299229932994299529962997299829993000300130023003300430053006300730083009301030113012301330143015301630173018301930203021302230233024302530263027302830293030303130323033303430353036303730383039304030413042304330443045304630473048304930503051305230533054305530563057305830593060306130623063306430653066306730683069307030713072307330743075307630773078307930803081308230833084308530863087308830893090309130923093309430953096309730983099310031013102310331043105310631073108310931103111311231133114311531163117311831193120312131223123312431253126312731283129313031313132313331343135313631373138313931403141314231433144314531463147314831493150315131523153315431553156315731583159316031613162316331643165316631673168316931703171317231733174317531763177317831793180318131823183318431853186318731883189319031913192319331943195319631973198319932003201320232033204320532063207320832093210321132123213321432153216321732183219322032213222322332243225322632273228322932303231323232333234323532363237323832393240324132423243324432453246324732483249325032513252325332543255325632573258325932603261326232633264326532663267326832693270327132723273327432753276327732783279328032813282328332843285328632873288328932903291329232933294329532963297329832993300330133023303330433053306330733083309331033113312331333143315331633173318331933203321332233233324332533263327332833293330333133323333333433353336333733383339334033413342334333443345334633473348334933503351335233533354335533563357335833593360336133623363336433653366336733683369337033713372337333743375337633773378337933803381338233833384338533863387338833893390339133923393339433953396339733983399340034013402340334043405340634073408340934103411341234133414341534163417341834193420342134223423342434253426342734283429343034313432343334343435343634373438343934403441344234433444344534463447344834493450345134523453345434553456345734583459346034613462346334643465346634673468346934703471347234733474347534763477347834793480348134823483348434853486348734883489349034913492349334943495349634973498349935003501350235033504350535063507350835093510351135123513351435153516351735183519352035213522352335243525352635273528352935303531353235333534353535363537353835393540354135423543354435453546354735483549355035513552355335543555355635573558355935603561356235633564356535663567356835693570357135723573357435753576357735783579358035813582358335843585358635873588358935903591359235933594359535963597359835993600360136023603360436053606360736083609361036113612361336143615361636173618361936203621362236233624362536263627362836293630363136323633363436353636363736383639364036413642364336443645364636473648364936503651365236533654365536563657365836593660366136623663366436653666366736683669367036713672367336743675367636773678367936803681368236833684368536863687368836893690369136923693369436953696369736983699370037013702370337043705370637073708370937103711371237133714371537163717371837193720372137223723372437253726372737283729373037313732373337343735373637373738373937403741374237433744374537463747374837493750375137523753375437553756375737583759376037613762376337643765376637673768376937703771377237733774377537763777377837793780378137823783378437853786378737883789379037913792379337943795379637973798379938003801380238033804380538063807380838093810381138123813381438153816381738183819382038213822382338243825382638273828382938303831383238333834383538363837383838393840384138423843384438453846384738483849385038513852385338543855385638573858385938603861386238633864386538663867386838693870387138723873387438753876387738783879388038813882388338843885388638873888388938903891389238933894389538963897389838993900390139023903390439053906390739083909391039113912391339143915391639173918391939203921392239233924392539263927392839293930393139323933393439353936393739383939394039413942394339443945394639473948394939503951395239533954395539563957395839593960396139623963396439653966396739683969397039713972397339743975397639773978397939803981398239833984398539863987398839893990399139923993399439953996399739983999400040014002400340044005400640074008400940104011401240134014401540164017401840194020402140224023402440254026402740284029403040314032403340344035403640374038403940404041404240434044404540464047404840494050405140524053405440554056405740584059406040614062406340644065406640674068406940704071407240734074407540764077407840794080408140824083408440854086408740884089409040914092409340944095409640974098409941004101410241034104410541064107410841094110411141124113411441154116411741184119412041214122412341244125412641274128412941304131413241334134413541364137413841394140414141424143414441454146414741484149415041514152415341544155415641574158415941604161416241634164416541664167416841694170417141724173417441754176417741784179418041814182418341844185418641874188418941904191419241934194419541964197419841994200420142024203420442054206420742084209421042114212421342144215421642174218421942204221422242234224422542264227422842294230423142324233423442354236423742384239424042414242424342444245424642474248424942504251425242534254425542564257425842594260426142624263426442654266426742684269427042714272427342744275427642774278427942804281428242834284428542864287428842894290429142924293429442954296429742984299430043014302430343044305430643074308430943104311431243134314431543164317431843194320432143224323432443254326432743284329433043314332433343344335433643374338433943404341434243434344434543464347434843494350435143524353435443554356435743584359436043614362436343644365436643674368436943704371437243734374437543764377437843794380438143824383438443854386438743884389439043914392439343944395439643974398439944004401440244034404440544064407440844094410441144124413441444154416441744184419442044214422442344244425442644274428442944304431443244334434443544364437443844394440444144424443444444454446444744484449445044514452445344544455445644574458445944604461446244634464446544664467446844694470447144724473447444754476447744784479448044814482448344844485448644874488448944904491449244934494449544964497449844994500450145024503450445054506450745084509451045114512451345144515451645174518451945204521452245234524452545264527452845294530453145324533453445354536453745384539454045414542454345444545454645474548454945504551455245534554455545564557455845594560456145624563456445654566456745684569457045714572457345744575457645774578457945804581458245834584458545864587458845894590459145924593459445954596459745984599460046014602460346044605460646074608460946104611461246134614461546164617461846194620462146224623462446254626462746284629463046314632463346344635463646374638463946404641464246434644464546464647464846494650465146524653465446554656465746584659466046614662466346644665466646674668466946704671467246734674467546764677467846794680468146824683468446854686468746884689469046914692469346944695469646974698469947004701470247034704470547064707470847094710471147124713471447154716471747184719472047214722472347244725472647274728472947304731473247334734473547364737473847394740474147424743474447454746474747484749475047514752475347544755475647574758475947604761476247634764476547664767476847694770477147724773477447754776477747784779478047814782478347844785478647874788478947904791479247934794479547964797479847994800480148024803480448054806480748084809481048114812481348144815481648174818481948204821482248234824482548264827482848294830483148324833483448354836483748384839484048414842484348444845484648474848484948504851485248534854485548564857485848594860486148624863486448654866486748684869487048714872487348744875487648774878487948804881488248834884488548864887488848894890489148924893489448954896489748984899490049014902490349044905490649074908490949104911491249134914491549164917491849194920492149224923492449254926492749284929493049314932493349344935493649374938493949404941494249434944494549464947494849494950495149524953495449554956495749584959496049614962496349644965496649674968496949704971497249734974497549764977497849794980498149824983498449854986498749884989499049914992499349944995499649974998499950005001500250035004500550065007500850095010501150125013501450155016501750185019502050215022502350245025502650275028502950305031503250335034503550365037503850395040504150425043504450455046504750485049505050515052505350545055505650575058505950605061506250635064506550665067506850695070507150725073507450755076507750785079508050815082508350845085508650875088508950905091509250935094509550965097509850995100510151025103510451055106510751085109511051115112511351145115511651175118511951205121512251235124512551265127512851295130513151325133513451355136513751385139514051415142514351445145514651475148514951505151515251535154515551565157515851595160516151625163516451655166516751685169517051715172517351745175517651775178517951805181518251835184518551865187518851895190519151925193519451955196519751985199520052015202520352045205520652075208520952105211521252135214521552165217521852195220522152225223522452255226522752285229523052315232523352345235523652375238523952405241524252435244524552465247524852495250525152525253525452555256525752585259526052615262526352645265526652675268526952705271527252735274527552765277527852795280528152825283528452855286528752885289529052915292529352945295529652975298529953005301530253035304530553065307530853095310531153125313531453155316531753185319532053215322532353245325532653275328532953305331533253335334533553365337533853395340534153425343534453455346534753485349535053515352535353545355535653575358535953605361536253635364536553665367536853695370537153725373537453755376537753785379538053815382538353845385538653875388538953905391539253935394539553965397539853995400540154025403540454055406540754085409541054115412541354145415541654175418541954205421542254235424542554265427542854295430543154325433543454355436543754385439544054415442544354445445544654475448544954505451545254535454545554565457545854595460546154625463546454655466546754685469547054715472547354745475547654775478547954805481548254835484548554865487548854895490549154925493549454955496549754985499550055015502550355045505550655075508550955105511551255135514551555165517551855195520552155225523552455255526552755285529553055315532553355345535553655375538553955405541554255435544554555465547554855495550555155525553555455555556555755585559556055615562556355645565556655675568556955705571557255735574557555765577557855795580558155825583558455855586558755885589559055915592559355945595559655975598559956005601560256035604560556065607560856095610561156125613561456155616561756185619562056215622562356245625562656275628562956305631563256335634563556365637563856395640564156425643564456455646564756485649565056515652565356545655565656575658565956605661566256635664566556665667566856695670567156725673567456755676567756785679568056815682568356845685568656875688568956905691569256935694569556965697569856995700570157025703570457055706570757085709571057115712571357145715571657175718571957205721572257235724572557265727572857295730573157325733573457355736573757385739574057415742574357445745574657475748574957505751575257535754575557565757575857595760576157625763576457655766576757685769577057715772577357745775577657775778577957805781578257835784578557865787578857895790579157925793579457955796579757985799580058015802580358045805580658075808580958105811581258135814581558165817581858195820582158225823582458255826582758285829583058315832583358345835583658375838583958405841584258435844584558465847584858495850585158525853585458555856585758585859586058615862586358645865586658675868586958705871587258735874587558765877587858795880588158825883588458855886588758885889589058915892589358945895589658975898589959005901590259035904590559065907590859095910591159125913591459155916591759185919592059215922592359245925592659275928592959305931593259335934593559365937593859395940594159425943594459455946594759485949595059515952595359545955595659575958595959605961596259635964596559665967596859695970597159725973597459755976597759785979598059815982598359845985598659875988598959905991599259935994599559965997599859996000600160026003600460056006600760086009601060116012601360146015601660176018601960206021602260236024602560266027602860296030603160326033603460356036603760386039604060416042604360446045604660476048604960506051605260536054605560566057605860596060606160626063606460656066606760686069607060716072607360746075607660776078607960806081608260836084608560866087608860896090609160926093609460956096609760986099610061016102610361046105610661076108610961106111611261136114611561166117611861196120612161226123612461256126612761286129613061316132613361346135613661376138613961406141614261436144614561466147614861496150615161526153615461556156615761586159616061616162616361646165616661676168616961706171617261736174617561766177617861796180618161826183618461856186618761886189619061916192619361946195619661976198619962006201620262036204620562066207620862096210621162126213621462156216621762186219622062216222622362246225622662276228622962306231623262336234623562366237623862396240624162426243624462456246624762486249625062516252625362546255625662576258625962606261626262636264626562666267626862696270627162726273627462756276627762786279628062816282628362846285628662876288628962906291629262936294629562966297629862996300630163026303630463056306630763086309631063116312631363146315631663176318631963206321632263236324632563266327632863296330633163326333633463356336633763386339634063416342634363446345634663476348634963506351635263536354635563566357635863596360636163626363636463656366636763686369637063716372637363746375637663776378637963806381638263836384638563866387638863896390639163926393639463956396639763986399640064016402640364046405640664076408640964106411641264136414641564166417641864196420642164226423642464256426642764286429643064316432643364346435643664376438643964406441644264436444644564466447644864496450645164526453645464556456645764586459646064616462646364646465646664676468646964706471647264736474647564766477647864796480648164826483648464856486648764886489649064916492649364946495649664976498649965006501650265036504650565066507650865096510651165126513651465156516651765186519652065216522652365246525652665276528652965306531653265336534653565366537653865396540654165426543654465456546654765486549655065516552655365546555655665576558655965606561656265636564656565666567656865696570657165726573657465756576657765786579658065816582658365846585658665876588658965906591659265936594659565966597659865996600660166026603660466056606660766086609661066116612661366146615661666176618661966206621662266236624662566266627662866296630663166326633663466356636663766386639664066416642664366446645664666476648664966506651665266536654665566566657665866596660666166626663666466656666666766686669667066716672667366746675667666776678667966806681668266836684668566866687668866896690669166926693669466956696669766986699670067016702670367046705670667076708670967106711671267136714671567166717671867196720672167226723672467256726672767286729673067316732673367346735673667376738673967406741674267436744674567466747674867496750675167526753675467556756675767586759676067616762676367646765676667676768676967706771677267736774677567766777677867796780678167826783678467856786678767886789679067916792679367946795679667976798679968006801680268036804680568066807680868096810681168126813681468156816681768186819682068216822682368246825682668276828682968306831683268336834683568366837683868396840684168426843684468456846684768486849685068516852685368546855685668576858685968606861686268636864686568666867686868696870687168726873687468756876687768786879688068816882688368846885688668876888688968906891689268936894689568966897689868996900690169026903690469056906690769086909691069116912691369146915691669176918691969206921692269236924692569266927692869296930693169326933693469356936693769386939694069416942694369446945694669476948694969506951695269536954695569566957695869596960696169626963696469656966696769686969697069716972697369746975697669776978697969806981698269836984698569866987698869896990699169926993699469956996699769986999700070017002700370047005700670077008700970107011701270137014701570167017701870197020702170227023702470257026702770287029703070317032703370347035703670377038703970407041704270437044704570467047704870497050705170527053705470557056705770587059706070617062706370647065706670677068706970707071707270737074707570767077707870797080708170827083708470857086708770887089709070917092709370947095709670977098709971007101710271037104710571067107710871097110711171127113711471157116711771187119712071217122712371247125712671277128712971307131713271337134713571367137713871397140714171427143714471457146714771487149715071517152715371547155715671577158715971607161716271637164716571667167716871697170717171727173717471757176717771787179718071817182718371847185718671877188718971907191719271937194719571967197719871997200720172027203720472057206720772087209721072117212721372147215721672177218721972207221722272237224722572267227722872297230723172327233723472357236723772387239724072417242724372447245724672477248724972507251725272537254725572567257725872597260726172627263726472657266726772687269727072717272727372747275727672777278727972807281728272837284728572867287728872897290729172927293729472957296729772987299730073017302730373047305730673077308730973107311731273137314731573167317731873197320732173227323732473257326732773287329733073317332733373347335733673377338733973407341734273437344734573467347734873497350735173527353735473557356735773587359736073617362736373647365736673677368736973707371737273737374737573767377737873797380738173827383738473857386738773887389739073917392739373947395739673977398739974007401740274037404740574067407740874097410741174127413741474157416741774187419742074217422742374247425742674277428742974307431743274337434743574367437743874397440744174427443744474457446744774487449745074517452745374547455
  1. import os
  2. import pymysql
  3. import requests
  4. import json
  5. import re
  6. import threading
  7. import urllib3
  8. import fitz # PyMuPDF
  9. import base64
  10. import hashlib
  11. from io import BytesIO
  12. from PIL import Image, ImageDraw, ImageFont
  13. from flask import Flask, render_template, request, redirect, url_for, session, flash, jsonify, Response, stream_with_context, send_file
  14. from werkzeug.utils import secure_filename
  15. from oss_utils import upload_to_oss
  16. from ocr_utils import extract_page_number
  17. import time
  18. from datetime import datetime
  19. # Suppress InsecureRequestWarning
  20. urllib3.disable_warnings(urllib3.exceptions.InsecureRequestWarning)
  21. # 静态资源目录使用绝对路径,避免服务器工作目录不同导致找不到文件
  22. _BASE_DIR = os.path.dirname(os.path.abspath(__file__))
  23. _STATIC_DIR = os.path.join(_BASE_DIR, 'static')
  24. _HOME_IMAGES_DIR = os.path.join(_STATIC_DIR, 'images', 'home')
  25. _HOME_FONTS_DIR = os.path.join(_STATIC_DIR, 'fonts', 'home')
  26. app = Flask(__name__, static_folder=_STATIC_DIR, static_url_path='/manager/static')
  27. app.secret_key = 'genealogy_secret_key'
  28. app.config['UPLOAD_FOLDER'] = 'uploads'
  29. os.makedirs(app.config['UPLOAD_FOLDER'], exist_ok=True)
  30. os.makedirs(_HOME_IMAGES_DIR, exist_ok=True)
  31. os.makedirs(_HOME_FONTS_DIR, exist_ok=True)
  32. @app.context_processor
  33. def inject_home_assets():
  34. """首页静态资源 URL 助手,统一走 /manager/static/ 前缀。"""
  35. prefix = app.static_url_path.rstrip('/')
  36. def home_img(filename):
  37. return f"{prefix}/images/home/{filename}"
  38. def home_font(filename):
  39. return f"{prefix}/fonts/home/{filename}"
  40. return dict(home_img=home_img, home_font=home_font, static_prefix=prefix)
  41. # 数据库配置
  42. DB_CONFIG = {
  43. "host": "rm-f8ze60yirdj8786u2wo.mysql.rds.aliyuncs.com",
  44. "port": 3306,
  45. "user": "root",
  46. "password": "csqz@20255",
  47. "db": "csqz-client",
  48. "charset": "utf8mb4",
  49. "cursorclass": pymysql.cursors.DictCursor
  50. }
  51. # 微信小程序配置
  52. WECHAT_APP_ID = "wx98f5cf1c60f793b8"
  53. WECHAT_APP_SECRET = "3d34d5be301f893fe86349122deada65"
  54. # Access Token 缓存
  55. access_token = None
  56. access_token_expire_time = 0
  57. access_token_lock = threading.Lock()
  58. # 图片扩展名列表
  59. IMAGE_EXTENSIONS = {'.jpg', '.jpeg', '.png', '.gif', '.bmp', '.webp', '.tiff'}
  60. # ── 水印配置 ──────────────────────────────────────────────
  61. WM_CACHE_DIR = '/tmp/wm_cache'
  62. WM_CACHE_TTL = 7 * 24 * 3600 # 7天(秒)
  63. WM_TEXT_LINES = [
  64. '族谱资料 未经纸质原件持有人授权',
  65. '禁止任何形式复制、传播',
  66. ]
  67. WM_ALPHA = 180 # 0-255,70% 不透明
  68. WM_ANGLE = 30 # 斜放角度(度)
  69. WM_COLOR = (150, 0, 0)
  70. WM_FONT_SIZE_RATIO = 52 # font_size = min(W,H) // ratio
  71. WM_FONT_PATHS = [ # 按优先级尝试(项目内字体优先,跨平台通用)
  72. os.path.join(os.path.dirname(__file__), 'static', 'fonts', 'wqy-microhei.ttc'),
  73. '/System/Library/Fonts/STHeiti Medium.ttc', # macOS
  74. '/System/Library/Fonts/STHeiti Light.ttc', # macOS
  75. '/System/Library/Fonts/PingFang.ttc', # macOS
  76. '/usr/share/fonts/truetype/wqy/wqy-microhei.ttc', # Linux (apt)
  77. '/usr/share/fonts/truetype/wqy/wqy-zenhei.ttc', # Linux (apt)
  78. '/usr/share/fonts/wqy-microhei/wqy-microhei.ttc', # Linux (yum)
  79. ]
  80. os.makedirs(WM_CACHE_DIR, exist_ok=True)
  81. _wm_font_cache = {}
  82. def _get_wm_font(size):
  83. if size in _wm_font_cache:
  84. return _wm_font_cache[size]
  85. font = None
  86. for fp in WM_FONT_PATHS:
  87. if os.path.exists(fp):
  88. try:
  89. font = ImageFont.truetype(fp, size)
  90. break
  91. except Exception:
  92. continue
  93. if font is None:
  94. font = ImageFont.load_default()
  95. _wm_font_cache[size] = font
  96. return font
  97. def _apply_pillow_watermark(img_bytes):
  98. """对图片二进制数据叠加平铺水印,返回 JPEG 字节。"""
  99. orig = Image.open(BytesIO(img_bytes)).convert('RGBA')
  100. W, H = orig.size
  101. font_size = max(14, min(W, H) // WM_FONT_SIZE_RATIO)
  102. font = _get_wm_font(font_size)
  103. color_with_alpha = WM_COLOR + (WM_ALPHA,)
  104. # 测量文字尺寸
  105. dummy = Image.new('RGBA', (1, 1))
  106. dd = ImageDraw.Draw(dummy)
  107. bboxes = [dd.textbbox((0, 0), t, font=font) for t in WM_TEXT_LINES]
  108. tw = max(b[2] - b[0] for b in bboxes) + 40
  109. line_h = max(b[3] - b[1] for b in bboxes)
  110. th = line_h * len(WM_TEXT_LINES) + 30
  111. # 绘制单块水印贴片
  112. tile = Image.new('RGBA', (tw, th), (0, 0, 0, 0))
  113. td = ImageDraw.Draw(tile)
  114. for i, line in enumerate(WM_TEXT_LINES):
  115. td.text((20, 10 + i * (line_h + 8)), line, font=font, fill=color_with_alpha)
  116. rotated = tile.rotate(WM_ANGLE, expand=True)
  117. rw, rh = rotated.size
  118. # 交错平铺到与原图等大的水印层
  119. wm_layer = Image.new('RGBA', (W, H), (0, 0, 0, 0))
  120. step_x = rw + max(10, W // 18)
  121. step_y = rh + max(8, H // 22)
  122. row = 0
  123. for py in range(-rh, H + rh, step_y):
  124. ox = (row % 2) * (step_x // 2)
  125. for px in range(-rw + ox, W + rw, step_x):
  126. wm_layer.paste(rotated, (px, py), rotated)
  127. row += 1
  128. result = Image.alpha_composite(orig, wm_layer).convert('RGB')
  129. buf = BytesIO()
  130. result.save(buf, format='JPEG', quality=88)
  131. return buf.getvalue()
  132. def _wm_cache_path(oss_url):
  133. key = hashlib.sha256(oss_url.encode()).hexdigest()
  134. return os.path.join(WM_CACHE_DIR, key + '.jpg')
  135. def _add_dynamic_watermark(img_bytes, username, dt_str):
  136. """
  137. 在静态平铺水印基础上,叠加 1-2 个动态水印(当前用户名 + 查看时间)。
  138. 位置:左上角、右下角,横排,不旋转,半透明深红色。
  139. """
  140. img = Image.open(BytesIO(img_bytes)).convert('RGBA')
  141. W, H = img.size
  142. dyn_font_size = max(12, min(W, H) // 60)
  143. dyn_font = _get_wm_font(dyn_font_size)
  144. dyn_color = (150, 0, 0, 160) # 深红,62% 不透明
  145. dyn_text = f'{username} {dt_str}'
  146. overlay = Image.new('RGBA', (W, H), (0, 0, 0, 0))
  147. od = ImageDraw.Draw(overlay)
  148. # 测量文字宽高
  149. bbox = od.textbbox((0, 0), dyn_text, font=dyn_font)
  150. tw = bbox[2] - bbox[0]
  151. th = bbox[3] - bbox[1]
  152. pad = max(8, dyn_font_size // 2)
  153. # 位置1:左上角
  154. od.text((pad, pad), dyn_text, font=dyn_font, fill=dyn_color)
  155. # 位置2:右下角
  156. od.text((W - tw - pad, H - th - pad), dyn_text, font=dyn_font, fill=dyn_color)
  157. result = Image.alpha_composite(img, overlay).convert('RGB')
  158. buf = BytesIO()
  159. result.save(buf, format='JPEG', quality=88)
  160. return buf.getvalue()
  161. def add_oss_watermark(url, username=None):
  162. """
  163. 将图片 URL 替换为本地水印代理 URL(展示层使用,数据库原链接不受影响)。
  164. 非图片格式直接返回原 URL。
  165. """
  166. if not url:
  167. return url
  168. lower_url = url.lower().split('?')[0]
  169. is_image = any(lower_url.endswith(ext) for ext in IMAGE_EXTENSIONS)
  170. if not is_image:
  171. return url
  172. encoded = base64.urlsafe_b64encode(url.encode()).decode().rstrip('=')
  173. return f'/manager/image/wm?u={encoded}'
  174. def get_wechat_access_token():
  175. """获取微信小程序access_token,带缓存和线程安全"""
  176. global access_token, access_token_expire_time
  177. with access_token_lock:
  178. # 检查缓存是否有效(提前1小时刷新)
  179. now = time.time()
  180. if access_token and access_token_expire_time > now + 3600:
  181. return access_token
  182. # 需要获取新的access_token
  183. url = "https://api.weixin.qq.com/cgi-bin/token"
  184. params = {
  185. "grant_type": "client_credential",
  186. "appid": WECHAT_APP_ID,
  187. "secret": WECHAT_APP_SECRET
  188. }
  189. try:
  190. response = requests.get(url, params=params, timeout=30)
  191. data = response.json()
  192. if 'access_token' in data:
  193. access_token = data['access_token']
  194. expires_in = data.get('expires_in', 7200)
  195. access_token_expire_time = now + expires_in
  196. print(f"[WeChat API] Access token obtained, expires in {expires_in} seconds")
  197. return access_token
  198. else:
  199. print(f"[WeChat API] Failed to get access_token: {data}")
  200. return None
  201. except Exception as e:
  202. print(f"[WeChat API] Error getting access_token: {e}")
  203. return None
  204. def decrypt_wechat_phone(encrypted_data, iv, session_key):
  205. """解密微信手机号(需要使用官方解密库)"""
  206. try:
  207. from cryptography.hazmat.primitives.ciphers import Cipher, algorithms, modes
  208. from cryptography.hazmat.backends import default_backend
  209. import base64
  210. # AES解密
  211. session_key = base64.b64decode(session_key)
  212. encrypted_data = base64.b64decode(encrypted_data)
  213. iv = base64.b64decode(iv)
  214. cipher = Cipher(algorithms.AES(session_key), modes.CBC(iv), backend=default_backend())
  215. decryptor = cipher.decryptor()
  216. decrypted = decryptor.update(encrypted_data) + decryptor.finalize()
  217. # PKCS7 padding去除
  218. padding = ord(decrypted[-1:])
  219. decrypted = decrypted[:-padding]
  220. return json.loads(decrypted.decode('utf-8'))
  221. except Exception as e:
  222. print(f"[WeChat Decrypt] Error decrypting phone: {e}")
  223. return None
  224. from PIL import Image
  225. def compress_image_if_needed(file_path, max_dim=2000):
  226. """Compress, resize and normalize image to JPEG for AI processing."""
  227. try:
  228. # We always want to normalize to JPEG so AI doesn't complain about format
  229. with Image.open(file_path) as img:
  230. # Convert RGBA/P or any other mode to RGB for JPEG saving
  231. if img.mode != 'RGB':
  232. img = img.convert('RGB')
  233. width, height = img.size
  234. if max(width, height) > max_dim:
  235. ratio = max_dim / max(width, height)
  236. new_size = (int(width * ratio), int(height * ratio))
  237. img = img.resize(new_size, Image.Resampling.LANCZOS)
  238. # Always save as JPEG to normalize the format
  239. new_path = os.path.splitext(file_path)[0] + '_normalized.jpg'
  240. img.save(new_path, 'JPEG', quality=85)
  241. return new_path
  242. except Exception as e:
  243. print(f"Warning: Image compression/normalization failed for {file_path}: {e}")
  244. return file_path
  245. REFERENCE_IMAGE_EXTENSIONS = {'.jpg', '.jpeg', '.png', '.gif', '.webp', '.bmp'}
  246. def save_reference_image_to_oss(file, member_id=None):
  247. """Upload a reference document image to OSS. Returns (oss_url, file_name)."""
  248. import uuid
  249. if not file or not file.filename:
  250. raise ValueError('未选择文件')
  251. ext = os.path.splitext(file.filename)[1].lower()
  252. if ext not in REFERENCE_IMAGE_EXTENSIONS:
  253. raise ValueError('仅支持 JPG、PNG、GIF、WEBP 格式的图片')
  254. timestamp = int(time.time())
  255. if member_id:
  256. custom_filename = f"参考件_{member_id}_{timestamp}{ext}"
  257. else:
  258. custom_filename = f"参考件_temp_{uuid.uuid4().hex[:8]}_{timestamp}{ext}"
  259. filename = secure_filename(custom_filename)
  260. if not filename or not os.path.splitext(filename)[1]:
  261. filename = f"reference_{uuid.uuid4().hex[:8]}{ext}"
  262. file_path = os.path.join(app.config['UPLOAD_FOLDER'], filename)
  263. file.save(file_path)
  264. try:
  265. upload_path = compress_image_if_needed(file_path)
  266. oss_url = upload_to_oss(upload_path, custom_filename=filename)
  267. if not oss_url:
  268. raise ValueError('上传到 OSS 失败')
  269. return oss_url, filename
  270. finally:
  271. for path in {file_path, os.path.splitext(file_path)[0] + '_normalized.jpg'}:
  272. if path and os.path.exists(path):
  273. try:
  274. os.remove(path)
  275. except OSError:
  276. pass
  277. def apply_reference_from_form(data, form, session, is_update=False):
  278. """Apply reference document fields from form submission."""
  279. delete_reference = form.get('delete_reference') == '1'
  280. reference_oss_url = (form.get('reference_oss_url') or '').strip()
  281. reference_file_name = (form.get('reference_file_name') or '').strip()
  282. if delete_reference:
  283. data['reference_oss_url'] = None
  284. data['reference_file_name'] = None
  285. data['reference_upload_time'] = None
  286. data['reference_upload_uid'] = None
  287. elif reference_oss_url:
  288. data['reference_oss_url'] = reference_oss_url
  289. data['reference_file_name'] = reference_file_name or None
  290. data['reference_upload_time'] = datetime.now()
  291. data['reference_upload_uid'] = session['user_id']
  292. elif not is_update:
  293. data['reference_oss_url'] = None
  294. data['reference_file_name'] = None
  295. data['reference_upload_time'] = None
  296. data['reference_upload_uid'] = None
  297. return data
  298. INVALID_SOURCE_RECORD_ID = 1 # 历史占位值,表示未关联扫描件
  299. def normalize_source_record_id(source_record_id):
  300. """source_record_id=1 视为未关联扫描件。"""
  301. if source_record_id is None or source_record_id == '':
  302. return None
  303. try:
  304. val = int(source_record_id)
  305. except (TypeError, ValueError):
  306. return source_record_id
  307. return None if val == INVALID_SOURCE_RECORD_ID else val
  308. def clear_invalid_member_scan_fields(member):
  309. """清除因 source_record_id=1 误关联的扫描件展示字段。"""
  310. if not member:
  311. return member
  312. if normalize_source_record_id(member.get('source_record_id')) is None:
  313. member['source_record_id'] = None
  314. for field in ('source_image_url', 'source_page', 'genealogy_version',
  315. 'genealogy_source', 'upload_person'):
  316. member[field] = None
  317. return member
  318. # 尝试使用数据库连接池,如果不可用则使用普通连接
  319. try:
  320. try:
  321. from dbutils.pooled_db import PooledDB # dbutils >= 2.0(新包名)
  322. except ImportError:
  323. from DBUtils.PooledDB import PooledDB # DBUtils <= 1.x(旧包名)
  324. # 创建连接池
  325. pool = PooledDB(
  326. creator=pymysql,
  327. maxconnections=10, # 连接池最大连接数
  328. mincached=2, # 初始化时创建的空闲连接数
  329. maxcached=5, # 最大空闲连接数
  330. maxshared=3, # 最大共享连接数
  331. blocking=True, # 连接池满时是否阻塞等待
  332. maxusage=1000, # 一个连接最多被重复使用的次数,防止连接长时间使用失效
  333. setsession=[], # 开始会话前执行的命令列表
  334. ping=1, # 每次获取连接时都检查连接是否可用
  335. **DB_CONFIG
  336. )
  337. def get_db_connection():
  338. conn = pool.connection()
  339. print(f"[Database] Got connection from pool: {id(conn)}")
  340. return conn
  341. print("[Database] Database connection pool initialized successfully")
  342. except ImportError:
  343. # 如果DBUtils不可用,使用普通连接
  344. def get_db_connection():
  345. conn = pymysql.connect(**DB_CONFIG)
  346. print(f"[Database] Created new connection: {id(conn)}")
  347. return conn
  348. print("[Database] DBUtils not available, using regular database connections")
  349. def get_mp_user_from_token(token):
  350. """通过 token 获取小程序用户信息,返回 mp_users 行或 None"""
  351. if not token:
  352. return None
  353. conn = get_db_connection()
  354. try:
  355. with conn.cursor() as cursor:
  356. cursor.execute("SELECT id, openid, phone FROM mp_users WHERE token = %s", (token,))
  357. return cursor.fetchone()
  358. except Exception:
  359. return None
  360. finally:
  361. conn.close()
  362. def verify_connection(conn):
  363. """Verify database connection is still alive"""
  364. try:
  365. cursor = conn.cursor()
  366. cursor.execute("SELECT 1")
  367. cursor.fetchone()
  368. cursor.close()
  369. return True
  370. except Exception as e:
  371. print(f"[Database] Connection verification failed: {e}")
  372. return False
  373. def safe_commit(conn):
  374. """Safely commit transaction with error handling"""
  375. try:
  376. conn.commit()
  377. print(f"[Database] Transaction committed successfully")
  378. return True
  379. except Exception as e:
  380. print(f"[Database] Commit failed: {e}")
  381. try:
  382. conn.rollback()
  383. print(f"[Database] Rollback completed")
  384. except Exception as rollback_err:
  385. print(f"[Database] Rollback also failed: {rollback_err}")
  386. return False
  387. def format_timestamp(ts):
  388. if not ts: return '未知'
  389. try:
  390. # 兼容秒和毫秒
  391. if ts > 10000000000: # 超过2286年的秒数,通常认为是毫秒
  392. ts = ts / 1000
  393. return time.strftime('%Y-%m-%d', time.localtime(ts))
  394. except:
  395. return '未知'
  396. def manual_simplify(text):
  397. """
  398. Simple fallback for common Traditional to Simplified conversion
  399. if AI fails to convert specific characters.
  400. """
  401. if not text: return text
  402. mapping = {
  403. '學': '学', '國': '国', '萬': '万', '寶': '宝', '興': '兴',
  404. '華': '华', '會': '会', '葉': '叶', '藝': '艺', '號': '号',
  405. '處': '处', '見': '见', '視': '视', '言': '言', '語': '语',
  406. '貝': '贝', '車': '车', '長': '长', '門': '门', '韋': '韦',
  407. '頁': '页', '風': '风', '飛': '飞', '食': '食', '馬': '马',
  408. '魚': '鱼', '鳥': '鸟', '麥': '麦', '黃': '黄', '齊': '齐',
  409. '齒': '齿', '龍': '龙', '龜': '龟', '壽': '寿', '榮': '荣',
  410. '愛': '爱', '慶': '庆', '衛': '卫', '賢': '贤', '義': '义',
  411. '禮': '礼', '樂': '乐', '靈': '灵', '滅': '灭', '氣': '气',
  412. '智': '智', '信': '信', '仁': '仁', '勇': '勇', '嚴': '严',
  413. '銳': '锐', '優': '优', '楊': '杨', '吳': '吴', '銀': '银'
  414. }
  415. result = ""
  416. for char in text:
  417. result += mapping.get(char, char)
  418. return result
  419. def convert_to_simplified(text):
  420. """繁体转简体,优先使用 zhconv 库,失败则降级到 manual_simplify"""
  421. if not text:
  422. return text
  423. try:
  424. import zhconv
  425. return zhconv.convert(text, 'zh-hans')
  426. except Exception:
  427. return manual_simplify(text)
  428. def _build_reverse_simplify_map():
  429. """
  430. Build a reverse map from simplified char -> list of traditional chars
  431. based on the fallback manual_simplify mapping.
  432. """
  433. mapping = {
  434. '學': '学', '國': '国', '萬': '万', '寶': '宝', '興': '兴',
  435. '華': '华', '會': '会', '葉': '叶', '藝': '艺', '號': '号',
  436. '處': '处', '見': '见', '視': '视', '言': '言', '語': '语',
  437. '貝': '贝', '車': '车', '長': '长', '門': '门', '韋': '韦',
  438. '頁': '页', '風': '风', '飛': '飞', '食': '食', '馬': '马',
  439. '魚': '鱼', '鳥': '鸟', '麥': '麦', '黃': '黄', '齊': '齐',
  440. '齒': '齿', '龍': '龙', '龜': '龟', '壽': '寿', '榮': '荣',
  441. '愛': '爱', '慶': '庆', '衛': '卫', '賢': '贤', '義': '义',
  442. '禮': '礼', '樂': '乐', '靈': '灵', '滅': '灭', '氣': '气',
  443. '智': '智', '信': '信', '仁': '仁', '勇': '勇', '嚴': '严',
  444. '銳': '锐', '優': '优', '楊': '杨', '吳': '吴', '銀': '银'
  445. }
  446. rev = {}
  447. for trad, simp in mapping.items():
  448. rev.setdefault(simp, [])
  449. if trad not in rev[simp]:
  450. rev[simp].append(trad)
  451. return rev
  452. _REVERSE_SIMPLIFY_MAP = _build_reverse_simplify_map()
  453. def expand_name_search_variants(keyword, max_variants=60):
  454. """
  455. Expand keyword into a small set of variants so Simplified/Traditional
  456. searches can match both `name` and `simplified_name`.
  457. - Always includes original keyword
  458. - Includes fallback-trad->simp conversion
  459. - Includes best-effort simp->trad expansions based on reverse map
  460. """
  461. if not keyword:
  462. return []
  463. kw = str(keyword).strip()
  464. if not kw:
  465. return []
  466. variants = set([kw])
  467. variants.add(manual_simplify(kw))
  468. # Build possible traditional variants when the input is simplified.
  469. # For each char, if we have traditional candidates, branch; otherwise keep itself.
  470. choices = []
  471. for ch in kw:
  472. cand = _REVERSE_SIMPLIFY_MAP.get(ch)
  473. if cand:
  474. # include itself too (covers already-traditional or neutral chars)
  475. choices.append([ch] + cand)
  476. else:
  477. choices.append([ch])
  478. # Cartesian product with early stop.
  479. results = ['']
  480. for opts in choices:
  481. new_results = []
  482. for prefix in results:
  483. for opt in opts:
  484. new_results.append(prefix + opt)
  485. if len(new_results) >= max_variants:
  486. break
  487. if len(new_results) >= max_variants:
  488. break
  489. results = new_results
  490. if len(results) >= max_variants:
  491. break
  492. for r in results:
  493. if r:
  494. variants.add(r)
  495. variants.add(manual_simplify(r))
  496. # Keep deterministic order for stable SQL params
  497. ordered = []
  498. for v in variants:
  499. v2 = (v or '').strip()
  500. if v2 and v2 not in ordered:
  501. ordered.append(v2)
  502. if len(ordered) >= max_variants:
  503. break
  504. return ordered
  505. def clean_name(name):
  506. """
  507. Clean name according to Liu family genealogy rules:
  508. 1. If name is '学公' or '留学公', keep 'Gong' (exception).
  509. 2. Otherwise, if name ends with '公', remove '公'.
  510. 3. If name does not start with '留', prepend '留'.
  511. """
  512. if not name: return name
  513. name = name.strip()
  514. # Pre-process: Ensure Simplified Chinese for specific chars
  515. name = manual_simplify(name)
  516. # 1. Check exceptions (names that SHOULD keep 'Gong')
  517. exceptions = ['学公', '留学公']
  518. if name in exceptions:
  519. if not name.startswith('留'):
  520. name = '留' + name
  521. return name
  522. # 2. General Rule: Remove 'Gong' suffix
  523. if name.endswith('公'):
  524. name = name[:-1]
  525. # 3. Ensure 'Liu' surname
  526. if not name.startswith('留'):
  527. name = '留' + name
  528. return name
  529. def is_female_value(sex_value):
  530. """Return True when sex value represents female."""
  531. if sex_value is None:
  532. return False
  533. s = str(sex_value).strip().lower()
  534. return s in ('女', '2', 'female', 'f')
  535. def normalize_lookup_name(name):
  536. """Normalize names for loose matching in AI parsed content."""
  537. if not name:
  538. return ''
  539. return manual_simplify(str(name)).strip()
  540. def should_skip_liu_prefix_for_person(person, spouse_name_set):
  541. """
  542. Female spouse records should not auto-prepend '留' in simplified_name.
  543. We treat a person as female spouse if:
  544. 1) sex is female, and
  545. 2) has spouse_name field OR appears in another person's spouse_name list.
  546. """
  547. if not isinstance(person, dict):
  548. return False
  549. if not is_female_value(person.get('sex')):
  550. return False
  551. own_names = set()
  552. own_names.add(normalize_lookup_name(person.get('name')))
  553. own_names.add(normalize_lookup_name(person.get('original_name')))
  554. own_names.discard('')
  555. has_spouse_name = bool(normalize_lookup_name(person.get('spouse_name')))
  556. referenced_by_other = any(n in spouse_name_set for n in own_names)
  557. return has_spouse_name or referenced_by_other
  558. def get_normalized_base64_image(image_url):
  559. """Download image, normalize to JPEG, and return base64 data URI for AI payload."""
  560. import io
  561. import base64
  562. import requests
  563. from PIL import Image
  564. try:
  565. response = requests.get(image_url, timeout=30)
  566. response.raise_for_status()
  567. with Image.open(io.BytesIO(response.content)) as img:
  568. # Convert to RGB to ensure JPEG compatibility
  569. if img.mode != 'RGB':
  570. img = img.convert('RGB')
  571. # Resize if too large
  572. max_dim = 2000
  573. if max(img.width, img.height) > max_dim:
  574. ratio = max_dim / max(img.width, img.height)
  575. new_size = (int(img.width * ratio), int(img.height * ratio))
  576. img = img.resize(new_size, Image.Resampling.LANCZOS)
  577. # Save as JPEG in memory
  578. buffer = io.BytesIO()
  579. img.save(buffer, format='JPEG', quality=85)
  580. b64_str = base64.b64encode(buffer.getvalue()).decode('utf-8')
  581. return f"data:image/jpeg;base64,{b64_str}"
  582. except Exception as e:
  583. print(f"Error normalizing image from {image_url}: {e}")
  584. return image_url # Fallback to original URL if processing fails
  585. def process_ai_task(record_id, image_url):
  586. """Background task to process image with AI and store result."""
  587. print(f"[AI Task] Starting task for record {record_id}...")
  588. conn = get_db_connection()
  589. try:
  590. with conn.cursor() as cursor:
  591. cursor.execute("UPDATE genealogy_records SET ai_status = 1 WHERE id = %s", (record_id,))
  592. conn.commit()
  593. print(f"[AI Task] Status updated to 'Processing' for record {record_id}")
  594. api_key = "a1800657-9212-4afe-9b7c-b49f015c54d3"
  595. api_url = "https://ark.cn-beijing.volces.com/api/v3/responses"
  596. prompt = """
  597. 请分析这张家谱图片,提取其中关于人物的信息。
  598. 请务必将繁体字转换为简体字(original_name 字段除外)。
  599. 特别注意:'name' 字段必须是纯简体中文,不能包含繁体字(例如:'學'应转换为'学','劉'应转换为'刘','萬'应转换为'万')。
  600. 请提取以下字段(如果存在):
  601. - original_name: 原始姓名(严格保持图片上的繁体字,不做任何修改或转换)
  602. - name: 简体姓名(必须转换为简体中文,去除不需要的敬称)
  603. - sex: 性别(男/女)
  604. - birthday: 出生日期(尝试转换为YYYY-MM-DD格式,如果无法确定年份可只填月日)
  605. - death_date: 逝世日期(如文本中出现“殁”、“葬”、“卒”等字眼及其对应的时间,请提取)
  606. - father_name: 父亲姓名
  607. - spouse_name: 配偶姓名
  608. - generation: 第几世/代数
  609. - name_word: 字辈(例如名字为“学勤公”,“学”为字辈;提取名字中的字辈信息)
  610. - education: 学历/功名
  611. - title: 官职/称号
  612. 请严格以JSON列表格式返回,不要包含Markdown代码块标记(如 ```json ... ```),直接返回JSON数组。
  613. 如果包含多个人物,请都提取出来。
  614. Do not output any reasoning or explanation, just the JSON.
  615. """
  616. ai_payload_url = get_normalized_base64_image(image_url)
  617. payload = {
  618. "model": "doubao-seed-1-8-251228",
  619. "stream": True, # Streaming for robust handling
  620. "input": [
  621. {
  622. "role": "user",
  623. "content": [
  624. {"type": "input_image", "image_url": ai_payload_url},
  625. {"type": "input_text", "text": prompt}
  626. ]
  627. }
  628. ]
  629. }
  630. headers = {
  631. "Authorization": f"Bearer {api_key}",
  632. "Content-Type": "application/json"
  633. }
  634. max_retries = 3
  635. last_exception = None
  636. for attempt in range(max_retries):
  637. try:
  638. print(f"[AI Task] Attempt {attempt+1}/{max_retries} connecting to API for record {record_id}...")
  639. response = requests.post(
  640. api_url,
  641. json=payload,
  642. headers=headers,
  643. timeout=1200,
  644. stream=True,
  645. verify=False,
  646. proxies={"http": None, "https": None}
  647. )
  648. if response.status_code == 200:
  649. print(f"[AI Task] Connection established for record {record_id}, receiving stream...")
  650. full_content = ""
  651. for line in response.iter_lines():
  652. if not line: continue
  653. line_str = line.decode('utf-8')
  654. # Debug: Print full line to understand event flow
  655. print(f"[AI Task Debug] Raw Line: {line_str[:500]}") # Truncate very long lines
  656. if line_str.startswith('data: '):
  657. json_str = line_str[6:]
  658. if json_str.strip() == '[DONE]':
  659. print("[AI Task Debug] Received [DONE]")
  660. break
  661. try:
  662. chunk = json.loads(json_str)
  663. chunk_type = chunk.get('type')
  664. # Standard OpenAI format (choices)
  665. if 'choices' in chunk and len(chunk['choices']) > 0:
  666. delta = chunk['choices'][0].get('delta', {})
  667. if 'content' in delta:
  668. full_content += delta['content']
  669. # Doubao/Volcengine specific formats (delta)
  670. elif chunk_type == 'response.text.delta':
  671. full_content += chunk.get('delta', '')
  672. # Check response.completed if empty
  673. elif chunk_type == 'response.completed' and not full_content:
  674. output = chunk.get('response', {}).get('output', [])
  675. for item in output:
  676. # Also extract from reasoning if it contains JSON-like text
  677. if item.get('type') == 'reasoning':
  678. summary = item.get('summary', [])
  679. for sum_item in summary:
  680. if sum_item.get('type') == 'summary_text':
  681. full_content += sum_item.get('text', '')
  682. elif item.get('type') == 'message':
  683. content = item.get('content')
  684. if isinstance(content, str):
  685. full_content += content
  686. elif isinstance(content, list):
  687. for part in content:
  688. if isinstance(part, dict) and part.get('type') == 'text':
  689. full_content += part.get('text', '')
  690. # Fallback: output_item.added
  691. elif chunk_type == 'response.output_item.added':
  692. item = chunk.get('item', {})
  693. if item.get('role') == 'assistant':
  694. content_field = item.get('content', [])
  695. if isinstance(content_field, str):
  696. full_content += content_field
  697. elif isinstance(content_field, list):
  698. for part in content_field:
  699. if isinstance(part, dict) and part.get('type') == 'text':
  700. full_content += part.get('text', '')
  701. except Exception as e:
  702. print(f"[AI Task] Chunk parse error: {e}")
  703. else:
  704. # Fallback for non-SSE
  705. try:
  706. chunk = json.loads(line_str)
  707. if 'choices' in chunk and len(chunk['choices']) > 0:
  708. content = chunk['choices'][0]['message']['content']
  709. full_content += content
  710. except:
  711. pass
  712. print(f"[AI Task] Stream finished. Content length: {len(full_content)}")
  713. if len(full_content) == 0:
  714. print(f"[AI Task] WARNING: No content received from AI stream.")
  715. # Continue to JSON parse to fail gracefully
  716. # Clean JSON
  717. try:
  718. # 1. Try finding [...] array
  719. start = full_content.find('[')
  720. end = full_content.rfind(']')
  721. # 2. If not found, try finding {...} object and wrap it
  722. is_single_object = False
  723. if start == -1 or end == -1 or end <= start:
  724. start = full_content.find('{')
  725. end = full_content.rfind('}')
  726. is_single_object = True
  727. if start != -1 and end != -1 and end > start:
  728. content_clean = full_content[start:end+1]
  729. else:
  730. # Fallback to regex or raw
  731. content_clean = re.sub(r'^```json\s*', '', full_content)
  732. content_clean = re.sub(r'```$', '', content_clean)
  733. parsed = json.loads(content_clean)
  734. # Normalize single object to list
  735. if is_single_object and isinstance(parsed, dict):
  736. parsed = [parsed]
  737. content_clean = json.dumps(parsed, ensure_ascii=False)
  738. elif isinstance(parsed, dict) and not isinstance(parsed, list):
  739. # Just in case json.loads parsed a dict even if we looked for []
  740. parsed = [parsed]
  741. content_clean = json.dumps(parsed, ensure_ascii=False)
  742. # Build spouse name lookup for "female spouse" detection
  743. spouse_name_set = set()
  744. if isinstance(parsed, list):
  745. for person in parsed:
  746. n = normalize_lookup_name(person.get('spouse_name'))
  747. if n:
  748. spouse_name_set.add(n)
  749. # Clean names in parsed content
  750. if isinstance(parsed, list):
  751. for person in parsed:
  752. # Process Name: 'name' is Simplified from AI, 'original_name' is Traditional/Raw from AI
  753. simplified_name = person.get('name', '') or person.get('original_name', '')
  754. original_name = person.get('original_name', '')
  755. # Female spouse: only simplify Chinese, do NOT prepend '留'
  756. if should_skip_liu_prefix_for_person(person, spouse_name_set):
  757. cleaned_simplified = manual_simplify(simplified_name)
  758. else:
  759. # Same-clan default: prepend '留' and handle trailing '公'
  760. cleaned_simplified = clean_name(simplified_name)
  761. person['simplified_name'] = cleaned_simplified
  762. # Store raw name in 'name' field (as requested)
  763. if original_name:
  764. person['name'] = original_name
  765. else:
  766. # Fallback: if no original_name returned, use the uncleaned name as 'name'
  767. # or keep existing logic. But user wants raw in 'name'.
  768. # If AI didn't return original_name, 'name' is likely simplified.
  769. pass # Keep 'name' as is (which is Simplified) if original_name missing
  770. # Father name:同族,需要按“留”姓规则清洗
  771. if 'father_name' in person and person['father_name']:
  772. person['father_name'] = clean_name(person['father_name'])
  773. # Spouse name:只做繁转简,不拼接“留”姓,也不去“公”
  774. if 'spouse_name' in person and person['spouse_name']:
  775. person['spouse_name'] = manual_simplify(person['spouse_name'])
  776. # Re-serialize
  777. content_clean = json.dumps(parsed, ensure_ascii=False)
  778. with conn.cursor() as cursor:
  779. cursor.execute("UPDATE genealogy_records SET ai_status = 2, ai_content = %s WHERE id = %s", (content_clean, record_id))
  780. conn.commit()
  781. print(f"[AI Task] SUCCESS: Record {record_id} processed and saved.")
  782. return # Success
  783. except json.JSONDecodeError as err:
  784. raise Exception(f"JSON Parse Error: {str(err)}. Raw: {full_content}")
  785. else:
  786. raise Exception(f"API Error {response.status_code}: {response.text}")
  787. except Exception as e:
  788. print(f"[AI Task] Attempt {attempt+1} failed for record {record_id}: {e}")
  789. last_exception = e
  790. if attempt < max_retries - 1:
  791. wait_time = 2 * (attempt + 1)
  792. print(f"[AI Task] Waiting {wait_time}s before retry...")
  793. time.sleep(wait_time)
  794. raise last_exception or Exception("Unknown error")
  795. except Exception as e:
  796. print(f"[AI Task] FINAL FAILURE for record {record_id}: {e}")
  797. try:
  798. with conn.cursor() as cursor:
  799. cursor.execute("UPDATE genealogy_records SET ai_status = 3, ai_content = %s WHERE id = %s", (f"Max Retries Exceeded. Error: {str(e)}", record_id))
  800. conn.commit()
  801. except:
  802. pass
  803. finally:
  804. conn.close()
  805. print(f"[AI Task] Task finished for record {record_id}")
  806. def ensure_pdf_table():
  807. conn = get_db_connection()
  808. try:
  809. with conn.cursor() as cursor:
  810. cursor.execute("""
  811. CREATE TABLE IF NOT EXISTS genealogy_pdfs (
  812. id INT AUTO_INCREMENT PRIMARY KEY,
  813. file_name VARCHAR(255) NOT NULL,
  814. oss_url TEXT NOT NULL,
  815. description VARCHAR(500) DEFAULT '',
  816. upload_time TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
  817. uploader VARCHAR(100) DEFAULT '',
  818. version_name VARCHAR(255) DEFAULT '',
  819. version_source VARCHAR(255) DEFAULT '',
  820. file_provider VARCHAR(100) DEFAULT '',
  821. parse_status INT DEFAULT 0
  822. ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4
  823. """)
  824. # 检查是否存在parse_status字段,如果不存在则添加
  825. cursor.execute("SHOW COLUMNS FROM genealogy_pdfs LIKE 'parse_status'")
  826. if not cursor.fetchone():
  827. cursor.execute("ALTER TABLE genealogy_pdfs ADD COLUMN parse_status INT DEFAULT 0")
  828. # 检查是否存在version_name字段,如果不存在则添加
  829. cursor.execute("SHOW COLUMNS FROM genealogy_pdfs LIKE 'version_name'")
  830. if not cursor.fetchone():
  831. cursor.execute("ALTER TABLE genealogy_pdfs ADD COLUMN version_name VARCHAR(255) DEFAULT ''")
  832. # 检查是否存在version_source字段,如果不存在则添加
  833. cursor.execute("SHOW COLUMNS FROM genealogy_pdfs LIKE 'version_source'")
  834. if not cursor.fetchone():
  835. cursor.execute("ALTER TABLE genealogy_pdfs ADD COLUMN version_source VARCHAR(255) DEFAULT ''")
  836. # 检查是否存在file_provider字段,如果不存在则添加
  837. cursor.execute("SHOW COLUMNS FROM genealogy_pdfs LIKE 'file_provider'")
  838. if not cursor.fetchone():
  839. cursor.execute("ALTER TABLE genealogy_pdfs ADD COLUMN file_provider VARCHAR(100) DEFAULT ''")
  840. conn.commit()
  841. finally:
  842. conn.close()
  843. @app.route('/manager/image/wm')
  844. def image_watermark_proxy():
  845. """
  846. 图片水印代理:下载 OSS 原图,叠加 Pillow 平铺水印后返回给浏览器。
  847. 原数据库中的 oss_url 字段保持不变,水印仅在展示层生效。
  848. query param: u = base64url(oss_url)
  849. """
  850. if 'user_id' not in session:
  851. return '', 401
  852. u_param = request.args.get('u', '')
  853. if not u_param:
  854. return '', 400
  855. # 还原原始 OSS URL
  856. try:
  857. padding = 4 - len(u_param) % 4
  858. oss_url = base64.urlsafe_b64decode(u_param + '=' * (padding % 4)).decode()
  859. except Exception:
  860. return '', 400
  861. # 安全校验:只允许代理已知域名的图片
  862. allowed_hosts = ('file.chunsunqiuzhu.com', 'chunsunqiuzhu.com')
  863. from urllib.parse import urlparse
  864. parsed = urlparse(oss_url)
  865. if not any(parsed.netloc.endswith(h) for h in allowed_hosts):
  866. return '', 403
  867. cache_path = _wm_cache_path(oss_url)
  868. # 阶段1:获取静态平铺水印(优先从磁盘缓存读取)
  869. static_bytes = None
  870. if os.path.exists(cache_path):
  871. age = time.time() - os.path.getmtime(cache_path)
  872. if age < WM_CACHE_TTL:
  873. try:
  874. with open(cache_path, 'rb') as f:
  875. static_bytes = f.read()
  876. except Exception:
  877. static_bytes = None
  878. if static_bytes is None:
  879. # 下载原图
  880. try:
  881. resp = requests.get(oss_url, timeout=30)
  882. resp.raise_for_status()
  883. except Exception as e:
  884. print(f'[WM Proxy] Failed to fetch {oss_url}: {e}')
  885. return '', 502
  886. # 叠加静态平铺水印
  887. try:
  888. static_bytes = _apply_pillow_watermark(resp.content)
  889. except Exception as e:
  890. print(f'[WM Proxy] Static watermark error for {oss_url}: {e}')
  891. static_bytes = resp.content # 降级:使用原图
  892. # 写磁盘缓存
  893. try:
  894. with open(cache_path, 'wb') as f:
  895. f.write(static_bytes)
  896. except Exception as e:
  897. print(f'[WM Proxy] Cache write error: {e}')
  898. # 阶段2:在静态水印上实时叠加动态水印(用户名 + 当前时间,不缓存)
  899. username = session.get('username', 'genealogy')
  900. dt_str = datetime.now().strftime('%Y-%m-%d %H:%M')
  901. try:
  902. final_bytes = _add_dynamic_watermark(static_bytes, username, dt_str)
  903. except Exception as e:
  904. print(f'[WM Proxy] Dynamic watermark error: {e}')
  905. final_bytes = static_bytes
  906. return Response(final_bytes, mimetype='image/jpeg',
  907. headers={'Cache-Control': 'no-store'})
  908. @app.route('/manager/pdf_management')
  909. def pdf_management():
  910. if 'user_id' not in session:
  911. return redirect(url_for('login'))
  912. username = session.get('username', 'unknown')
  913. is_super_admin = session.get('is_super_admin', 'NOT_SET')
  914. print(f"[PDF Management Access] User: {username}, is_super_admin: {is_super_admin}")
  915. # Verify is_super_admin against database - always check latest status
  916. conn = get_db_connection()
  917. try:
  918. with conn.cursor() as cursor:
  919. cursor.execute("SELECT is_super_admin FROM users WHERE id = %s", (session['user_id'],))
  920. db_result = cursor.fetchone()
  921. db_is_super = db_result['is_super_admin'] if db_result else 0
  922. print(f"[PDF Management Access] DB is_super_admin: {db_is_super}")
  923. if not db_is_super:
  924. print(f"[PDF Management Access] Denied for {username} (DB check)")
  925. flash('无权限访问此页面')
  926. return redirect(url_for('home'))
  927. finally:
  928. conn.close()
  929. print(f"[PDF Management Access] Allowed for {username}")
  930. ensure_pdf_table()
  931. view_id = request.args.get('view', type=int)
  932. preview = request.args.get('preview', type=bool, default=False)
  933. selected_pdf = None
  934. conn = get_db_connection()
  935. try:
  936. with conn.cursor() as cursor:
  937. cursor.execute("SELECT * FROM genealogy_pdfs ORDER BY upload_time DESC")
  938. pdfs = cursor.fetchall()
  939. if view_id and preview:
  940. cursor.execute("SELECT * FROM genealogy_pdfs WHERE id = %s", (view_id,))
  941. selected_pdf = cursor.fetchone()
  942. finally:
  943. conn.close()
  944. return render_template('pdf_management.html', pdfs=pdfs, selected_pdf=selected_pdf)
  945. @app.route('/manager/parse_pdf/<int:pdf_id>', methods=['POST'])
  946. def parse_pdf(pdf_id):
  947. if 'user_id' not in session:
  948. return jsonify({"success": False, "message": "Unauthorized"}), 401
  949. # 标记PDF为解析中
  950. conn = get_db_connection()
  951. try:
  952. with conn.cursor() as cursor:
  953. cursor.execute("UPDATE genealogy_pdfs SET parse_status = 1 WHERE id = %s", (pdf_id,))
  954. conn.commit()
  955. finally:
  956. conn.close()
  957. # 异步执行PDF解析
  958. def parse_pdf_async():
  959. try:
  960. # 获取PDF信息
  961. conn = get_db_connection()
  962. pdf_info = None
  963. try:
  964. with conn.cursor() as cursor:
  965. cursor.execute("SELECT * FROM genealogy_pdfs WHERE id = %s", (pdf_id,))
  966. pdf_info = cursor.fetchone()
  967. finally:
  968. conn.close()
  969. if not pdf_info:
  970. return
  971. # 下载PDF并拆分
  972. pdf_url = pdf_info['oss_url']
  973. response = requests.get(pdf_url)
  974. response.raise_for_status()
  975. # 保存临时PDF文件
  976. temp_pdf_path = f"/tmp/{pdf_info['file_name']}"
  977. with open(temp_pdf_path, 'wb') as f:
  978. f.write(response.content)
  979. # 使用PyMuPDF拆分PDF
  980. doc = fitz.open(temp_pdf_path)
  981. page_count = doc.page_count
  982. # 每个PDF的页码从1开始计算
  983. max_page = 0
  984. # 逐页处理
  985. for i in range(page_count):
  986. page = doc[i]
  987. pix = page.get_pixmap()
  988. image_path = f"/tmp/{pdf_info['file_name']}_page_{i+1}.png"
  989. pix.save(image_path)
  990. # 上传图片到OSS
  991. image_oss_url = upload_to_oss(image_path, f"{pdf_info['file_name']}_page_{i+1}.png")
  992. # 检查上传是否成功
  993. if not image_oss_url:
  994. raise Exception(f"Failed to upload image to OSS: {image_path}")
  995. # 保存到genealogy_records表
  996. conn = get_db_connection()
  997. try:
  998. with conn.cursor() as cursor:
  999. cursor.execute("""
  1000. INSERT INTO genealogy_records
  1001. (file_name, oss_url, file_type, page_number, genealogy_version, genealogy_source, upload_person, upload_time)
  1002. VALUES (%s, %s, %s, %s, %s, %s, %s, CURRENT_TIMESTAMP)
  1003. """, (
  1004. f"{pdf_info['file_name']}_page_{i+1}.png",
  1005. image_oss_url,
  1006. '图片',
  1007. max_page + i + 1,
  1008. pdf_info['version_name'],
  1009. pdf_info['version_source'],
  1010. pdf_info['file_provider']
  1011. ))
  1012. conn.commit()
  1013. finally:
  1014. conn.close()
  1015. # 删除临时图片文件
  1016. if os.path.exists(image_path):
  1017. os.remove(image_path)
  1018. # 删除临时PDF文件
  1019. if os.path.exists(temp_pdf_path):
  1020. os.remove(temp_pdf_path)
  1021. # 更新PDF解析状态为成功
  1022. conn = get_db_connection()
  1023. try:
  1024. with conn.cursor() as cursor:
  1025. cursor.execute("UPDATE genealogy_pdfs SET parse_status = 2 WHERE id = %s", (pdf_id,))
  1026. conn.commit()
  1027. finally:
  1028. conn.close()
  1029. except Exception as e:
  1030. # 更新PDF解析状态为失败
  1031. conn = get_db_connection()
  1032. try:
  1033. with conn.cursor() as cursor:
  1034. cursor.execute("UPDATE genealogy_pdfs SET parse_status = 3 WHERE id = %s", (pdf_id,))
  1035. conn.commit()
  1036. finally:
  1037. conn.close()
  1038. print(f"PDF解析失败: {e}")
  1039. # 启动异步任务
  1040. thread = threading.Thread(target=parse_pdf_async)
  1041. thread.daemon = True
  1042. thread.start()
  1043. return jsonify({"success": True, "message": "PDF解析已开始,将在后台执行"})
  1044. @app.route('/manager/batch_ai_parse', methods=['GET'])
  1045. def batch_ai_parse():
  1046. """Batch AI parse for unprocessed records."""
  1047. if 'user_id' not in session:
  1048. return jsonify({"success": False, "message": "Unauthorized"}), 401
  1049. # Start background thread
  1050. thread = threading.Thread(target=batch_ai_parse_async)
  1051. thread.daemon = True
  1052. thread.start()
  1053. return jsonify({"success": True, "message": "批量AI解析已开始,请稍候查看结果"})
  1054. def batch_ai_parse_async():
  1055. """Background task to batch AI parse unprocessed records."""
  1056. timestamp = datetime.now().strftime('%Y-%m-%d %H:%M:%S')
  1057. print(f"[{timestamp}] [Batch AI Parse] Starting batch AI parse task...")
  1058. # Get unprocessed records (ai_status = 0)
  1059. conn = None
  1060. unprocessed_records = []
  1061. try:
  1062. conn = get_db_connection()
  1063. with conn.cursor() as cursor:
  1064. cursor.execute("SELECT id, oss_url FROM genealogy_records WHERE ai_status = 0 order by page_number")
  1065. unprocessed_records = cursor.fetchall()
  1066. conn.close()
  1067. conn = None
  1068. total_records = len(unprocessed_records)
  1069. timestamp = datetime.now().strftime('%Y-%m-%d %H:%M:%S')
  1070. print(f"[{timestamp}] [Batch AI Parse] Found {total_records} unprocessed records")
  1071. if total_records == 0:
  1072. timestamp = datetime.now().strftime('%Y-%m-%d %H:%M:%S')
  1073. print(f"[{timestamp}] [Batch AI Parse] No unprocessed records found")
  1074. return
  1075. # Control concurrency to 5
  1076. max_concurrency = 5
  1077. semaphore = threading.Semaphore(max_concurrency)
  1078. threads = []
  1079. def process_record(record):
  1080. """Process a single record with semaphore."""
  1081. with semaphore:
  1082. try:
  1083. record_id = record['id']
  1084. image_url = record['oss_url']
  1085. timestamp = datetime.now().strftime('%Y-%m-%d %H:%M:%S')
  1086. print(f"[{timestamp}] [Batch AI Parse] Processing record {record_id}")
  1087. process_ai_task(record_id, image_url)
  1088. except Exception as e:
  1089. timestamp = datetime.now().strftime('%Y-%m-%d %H:%M:%S')
  1090. print(f"[{timestamp}] [Batch AI Parse] Error processing record {record['id']}: {e}")
  1091. # If failed, we'll handle it in the next batch
  1092. # Start threads for each record
  1093. for record in unprocessed_records:
  1094. thread = threading.Thread(target=process_record, args=(record,))
  1095. thread.daemon = True
  1096. thread.start()
  1097. threads.append(thread)
  1098. # Wait for all threads to complete
  1099. for thread in threads:
  1100. thread.join()
  1101. timestamp = datetime.now().strftime('%Y-%m-%d %H:%M:%S')
  1102. print(f"[{timestamp}] [Batch AI Parse] Batch processing completed. Processed {total_records} records")
  1103. # Check for failed records and restart them
  1104. check_failed_records()
  1105. except Exception as e:
  1106. timestamp = datetime.now().strftime('%Y-%m-%d %H:%M:%S')
  1107. print(f"[{timestamp}] [Batch AI Parse] Error: {e}")
  1108. finally:
  1109. if conn:
  1110. try:
  1111. conn.close()
  1112. except:
  1113. pass
  1114. def check_failed_records():
  1115. """Check for failed records and restart them."""
  1116. timestamp = datetime.now().strftime('%Y-%m-%d %H:%M:%S')
  1117. print(f"[{timestamp}] [Batch AI Parse] Checking for failed records...")
  1118. conn = None
  1119. failed_records = []
  1120. try:
  1121. conn = get_db_connection()
  1122. with conn.cursor() as cursor:
  1123. cursor.execute("SELECT id, oss_url FROM genealogy_records WHERE ai_status = 3")
  1124. failed_records = cursor.fetchall()
  1125. conn.close()
  1126. conn = None
  1127. total_failed = len(failed_records)
  1128. timestamp = datetime.now().strftime('%Y-%m-%d %H:%M:%S')
  1129. print(f"[{timestamp}] [Batch AI Parse] Found {total_failed} failed records")
  1130. if total_failed == 0:
  1131. timestamp = datetime.now().strftime('%Y-%m-%d %H:%M:%S')
  1132. print(f"[{timestamp}] [Batch AI Parse] No failed records found")
  1133. return
  1134. # Control concurrency to 5 for failed records
  1135. max_concurrency = 5
  1136. semaphore = threading.Semaphore(max_concurrency)
  1137. threads = []
  1138. def process_failed_record(record):
  1139. """Process a failed record with semaphore."""
  1140. with semaphore:
  1141. retry_conn = None
  1142. try:
  1143. record_id = record['id']
  1144. image_url = record['oss_url']
  1145. timestamp = datetime.now().strftime('%Y-%m-%d %H:%M:%S')
  1146. print(f"[{timestamp}] [Batch AI Parse] Retrying failed record {record_id}")
  1147. # Reset status to processing
  1148. retry_conn = get_db_connection()
  1149. with retry_conn.cursor() as cursor:
  1150. cursor.execute("UPDATE genealogy_records SET ai_status = 1 WHERE id = %s", (record_id,))
  1151. retry_conn.commit()
  1152. retry_conn.close()
  1153. retry_conn = None
  1154. process_ai_task(record_id, image_url)
  1155. except Exception as e:
  1156. timestamp = datetime.now().strftime('%Y-%m-%d %H:%M:%S')
  1157. print(f"[{timestamp}] [Batch AI Parse] Error retrying record {record['id']}: {e}")
  1158. finally:
  1159. if retry_conn:
  1160. try:
  1161. retry_conn.close()
  1162. except:
  1163. pass
  1164. # Start threads for each failed record
  1165. for record in failed_records:
  1166. thread = threading.Thread(target=process_failed_record, args=(record,))
  1167. thread.daemon = True
  1168. thread.start()
  1169. threads.append(thread)
  1170. # Wait for all threads to complete
  1171. for thread in threads:
  1172. thread.join()
  1173. timestamp = datetime.now().strftime('%Y-%m-%d %H:%M:%S')
  1174. print(f"[{timestamp}] [Batch AI Parse] Retry processing completed. Retried {total_failed} failed records")
  1175. except Exception as e:
  1176. timestamp = datetime.now().strftime('%Y-%m-%d %H:%M:%S')
  1177. print(f"[{timestamp}] [Batch AI Parse] Error checking failed records: {e}")
  1178. finally:
  1179. if conn:
  1180. try:
  1181. conn.close()
  1182. except:
  1183. pass
  1184. @app.route('/manager/delete_pdf/<int:pdf_id>', methods=['POST'])
  1185. def delete_pdf(pdf_id):
  1186. if 'user_id' not in session:
  1187. return jsonify({"success": False, "message": "Unauthorized"}), 401
  1188. conn = get_db_connection()
  1189. try:
  1190. with conn.cursor() as cursor:
  1191. cursor.execute("DELETE FROM genealogy_pdfs WHERE id = %s", (pdf_id,))
  1192. conn.commit()
  1193. flash('PDF文件记录已删除')
  1194. except Exception as e:
  1195. flash(f'删除失败: {e}')
  1196. finally:
  1197. conn.close()
  1198. return redirect(url_for('pdf_management'))
  1199. @app.route('/manager/')
  1200. def index():
  1201. if 'user_id' not in session:
  1202. return redirect(url_for('login'))
  1203. # 获取当前登录用户名
  1204. username = session.get('username', 'genealogy')
  1205. page = request.args.get('page', 1, type=int)
  1206. version = request.args.get('version', '').strip()
  1207. print(f"Received version parameter: '{version}'")
  1208. source = request.args.get('source', '').strip()
  1209. person = request.args.get('person', '').strip()
  1210. file_type = request.args.get('file_type', '').strip()
  1211. per_page = 10
  1212. offset = (page - 1) * per_page
  1213. conn = get_db_connection()
  1214. try:
  1215. with conn.cursor() as cursor:
  1216. query_conditions = []
  1217. params = []
  1218. if version:
  1219. query_conditions.append("genealogy_version LIKE %s")
  1220. params.append(f"%{version}%")
  1221. if source:
  1222. query_conditions.append("genealogy_source LIKE %s")
  1223. params.append(f"%{source}%")
  1224. if person:
  1225. query_conditions.append("upload_person LIKE %s")
  1226. params.append(f"%{person}%")
  1227. if file_type:
  1228. query_conditions.append("file_type = %s")
  1229. params.append(file_type)
  1230. where_clause = ""
  1231. if query_conditions:
  1232. where_clause = "WHERE " + " AND ".join(query_conditions)
  1233. count_sql = f"SELECT COUNT(*) as count FROM genealogy_records {where_clause}"
  1234. cursor.execute(count_sql, params)
  1235. total = cursor.fetchone()['count']
  1236. sql = f"SELECT * FROM genealogy_records {where_clause} ORDER BY page_number ASC LIMIT %s OFFSET %s"
  1237. cursor.execute(sql, params + [per_page, offset])
  1238. records = cursor.fetchall()
  1239. # 为图片URL添加水印
  1240. for record in records:
  1241. if record.get('oss_url'):
  1242. record['oss_url'] = add_oss_watermark(record['oss_url'], username)
  1243. total_pages = (total + per_page - 1) // per_page
  1244. finally:
  1245. conn.close()
  1246. return render_template('index.html', records=records, page=page, total_pages=total_pages, version=version, source=source, person=person, file_type=file_type, total=total)
  1247. @app.route('/manager/members')
  1248. def members():
  1249. if 'user_id' not in session:
  1250. return redirect(url_for('login'))
  1251. search_name = request.args.get('name', '').strip()
  1252. search_type = request.args.get('search_type', 'name').strip() # name | name_word
  1253. page = request.args.get('page', 1, type=int)
  1254. per_page = 10
  1255. offset = (page - 1) * per_page
  1256. print(f"[Members List] Fetching members page: {page}, search: '{search_name}', type: '{search_type}', per_page: {per_page}")
  1257. conn = get_db_connection()
  1258. try:
  1259. with conn.cursor() as cursor:
  1260. order_clause = "ORDER BY COALESCE(fmi.modified_time, fmi.create_time) DESC"
  1261. father_join = """
  1262. LEFT JOIN family_relation_info fri
  1263. ON fmi.id = fri.child_mid AND fri.relation_type = 1 AND COALESCE(fri.sub_relation_type, 0) != 3
  1264. LEFT JOIN family_member_info father ON fri.parent_mid = father.id
  1265. """
  1266. father_cols = ", father.id as father_id, father.name as father_name, father.simplified_name as father_simplified_name, fri.child_order as child_order_to_father"
  1267. if search_name and search_type == 'name_word':
  1268. # 字辈精确搜索
  1269. where_clause = "fmi.name_word = %s"
  1270. count_params = [search_name]
  1271. data_params = [search_name]
  1272. elif search_name:
  1273. # 姓名模糊搜索(支持繁简变体)
  1274. variants = expand_name_search_variants(search_name)
  1275. where_parts = []
  1276. count_params = []
  1277. for v in variants:
  1278. where_parts.append("(fmi.name LIKE %s OR fmi.simplified_name LIKE %s)")
  1279. like = f"%{v}%"
  1280. count_params.extend([like, like])
  1281. where_clause = " OR ".join(where_parts) if where_parts else "(fmi.name LIKE %s OR fmi.simplified_name LIKE %s)"
  1282. if not where_parts:
  1283. like = f"%{search_name}%"
  1284. count_params = [like, like]
  1285. data_params = count_params[:]
  1286. else:
  1287. where_clause = None
  1288. count_params = []
  1289. data_params = []
  1290. # 1. 计算总数
  1291. if where_clause:
  1292. count_sql = f"SELECT COUNT(*) as count FROM family_member_info fmi WHERE {where_clause}"
  1293. cursor.execute(count_sql, tuple(count_params))
  1294. else:
  1295. cursor.execute("SELECT COUNT(*) as count FROM family_member_info")
  1296. result = cursor.fetchone()
  1297. total = result['count'] if result else 0
  1298. total_pages = (total + per_page - 1) // per_page
  1299. # 2. 查询分页数据
  1300. select_cols = f"fmi.id, fmi.name, fmi.simplified_name, fmi.sex, fmi.name_word, fmi.name_word_generation, fmi.birthday, fmi.occupation, fmi.family_rank, fmi.branch_family_hall, fmi.residential_address, fmi.is_pass_away, fmi.create_time, fmi.modified_time{father_cols}"
  1301. if where_clause:
  1302. sql = f"SELECT {select_cols} FROM family_member_info fmi {father_join} WHERE {where_clause} {order_clause} LIMIT %s OFFSET %s"
  1303. cursor.execute(sql, tuple(data_params + [per_page, offset]))
  1304. else:
  1305. sql = f"SELECT {select_cols} FROM family_member_info fmi {father_join} {order_clause} LIMIT %s OFFSET %s"
  1306. cursor.execute(sql, (per_page, offset))
  1307. members = cursor.fetchall()
  1308. print(f"[Members List] Fetched {len(members)} members")
  1309. # 格式化日期
  1310. for m in members:
  1311. m['birthday_str'] = format_timestamp(m.get('birthday'))
  1312. if m.get('create_time'):
  1313. m['create_time_str'] = m['create_time'].strftime('%Y-%m-%d')
  1314. if m.get('modified_time'):
  1315. m['modified_time_str'] = m['modified_time'].strftime('%Y-%m-%d %H:%M')
  1316. # 批量查询兄弟信息(只含有 father_id 的成员)
  1317. father_ids = list({m['father_id'] for m in members if m.get('father_id')})
  1318. member_ids_set = {m['id'] for m in members}
  1319. siblings_map = {} # father_id -> list of {id, name, simplified_name}
  1320. if father_ids:
  1321. fmt = ','.join(['%s'] * len(father_ids))
  1322. cursor.execute(f"""
  1323. SELECT fr.parent_mid AS father_id,
  1324. m.id AS sibling_id,
  1325. m.name AS sibling_name,
  1326. m.simplified_name AS sibling_simplified_name
  1327. FROM family_relation_info fr
  1328. JOIN family_member_info m ON fr.child_mid = m.id
  1329. WHERE fr.parent_mid IN ({fmt})
  1330. AND fr.relation_type = 1
  1331. AND COALESCE(fr.sub_relation_type, 0) != 2
  1332. ORDER BY COALESCE(fr.child_order, 9999), m.id
  1333. """, father_ids)
  1334. for row in cursor.fetchall():
  1335. fid = row['father_id']
  1336. if fid not in siblings_map:
  1337. siblings_map[fid] = []
  1338. siblings_map[fid].append({
  1339. 'id': row['sibling_id'],
  1340. 'name': row['sibling_name'],
  1341. 'simplified_name': row['sibling_simplified_name'],
  1342. })
  1343. # 将兄弟列表(排除自身)附加到每个成员
  1344. for m in members:
  1345. fid = m.get('father_id')
  1346. mid = m['id']
  1347. if fid and fid in siblings_map:
  1348. m['siblings'] = [s for s in siblings_map[fid] if s['id'] != mid]
  1349. else:
  1350. m['siblings'] = []
  1351. finally:
  1352. print(f"[Members List] Closing database connection")
  1353. conn.close()
  1354. return render_template('members.html', members=members, search_name=search_name, search_type=search_type, page=page, total_pages=total_pages, total=total)
  1355. @app.route('/manager/batch_genealogy')
  1356. def batch_genealogy():
  1357. if 'user_id' not in session:
  1358. return redirect(url_for('login'))
  1359. return render_template('batch_genealogy.html')
  1360. @app.route('/manager/suspected_errors')
  1361. def suspected_errors():
  1362. if 'user_id' not in session:
  1363. return redirect(url_for('login'))
  1364. search_name = request.args.get('name', '').strip()
  1365. page = request.args.get('page', 1, type=int)
  1366. per_page = 20
  1367. offset = (page - 1) * per_page
  1368. conn = get_db_connection()
  1369. try:
  1370. with conn.cursor() as cursor:
  1371. # Base query with condition for non-empty suspected_error (using TRIM to remove whitespace)
  1372. base_query = "SELECT id, name, simplified_name, sex, name_word_generation, birthday, suspected_error FROM family_member_info WHERE suspected_error IS NOT NULL AND TRIM(suspected_error) != ''"
  1373. count_query = "SELECT COUNT(*) as count FROM family_member_info WHERE suspected_error IS NOT NULL AND TRIM(suspected_error) != ''"
  1374. # Add search condition if provided
  1375. params = []
  1376. if search_name:
  1377. # Support both traditional and simplified name search
  1378. base_query += " AND (name LIKE %s OR simplified_name LIKE %s)"
  1379. count_query += " AND (name LIKE %s OR simplified_name LIKE %s)"
  1380. search_param = f"%{search_name}%"
  1381. params.extend([search_param, search_param])
  1382. # Get total count
  1383. cursor.execute(count_query, params)
  1384. result = cursor.fetchone()
  1385. total = result['count'] if result else 0
  1386. total_pages = (total + per_page - 1) // per_page
  1387. # Get members with pagination
  1388. base_query += " ORDER BY name LIMIT %s OFFSET %s"
  1389. params.extend([per_page, offset])
  1390. cursor.execute(base_query, params)
  1391. members = cursor.fetchall()
  1392. # Format birthday for display
  1393. for member in members:
  1394. if member['birthday']:
  1395. member['birthday_str'] = format_timestamp(member['birthday'])
  1396. else:
  1397. member['birthday_str'] = '未知'
  1398. finally:
  1399. conn.close()
  1400. return render_template('suspected_errors.html', members=members, search_name=search_name, page=page, total_pages=total_pages, total=total)
  1401. @app.route('/manager/tree')
  1402. def tree():
  1403. if 'user_id' not in session:
  1404. return redirect(url_for('login'))
  1405. return render_template('tree.html')
  1406. @app.route('/manager/lineage_query')
  1407. def lineage_query():
  1408. if 'user_id' not in session:
  1409. return redirect(url_for('login'))
  1410. return render_template('lineage_query.html')
  1411. @app.route('/manager/tree_classic')
  1412. def tree_classic():
  1413. if 'user_id' not in session:
  1414. return redirect(url_for('login'))
  1415. return render_template('tree_classic.html')
  1416. @app.route('/manager/tree_gen')
  1417. def tree_gen():
  1418. if 'user_id' not in session:
  1419. return redirect(url_for('login'))
  1420. return render_template('tree_gen.html')
  1421. @app.route('/manager/api/tree_data')
  1422. def tree_data():
  1423. if 'user_id' not in session:
  1424. return jsonify({"error": "Unauthorized"}), 401
  1425. conn = get_db_connection()
  1426. try:
  1427. with conn.cursor() as cursor:
  1428. # 获取所有成员
  1429. cursor.execute("SELECT id, name, simplified_name, sex, family_rank, name_word_generation FROM family_member_info")
  1430. members = cursor.fetchall()
  1431. # 获取所有关系 (1:父子 2:母子 10:夫妻 11:兄弟 12:姐妹),包括子类型
  1432. cursor.execute("SELECT parent_mid, child_mid, relation_type, sub_relation_type FROM family_relation_info")
  1433. relations = cursor.fetchall()
  1434. return jsonify({"members": members, "relations": relations})
  1435. finally:
  1436. conn.close()
  1437. @app.route('/manager/api/search_member', methods=['POST'])
  1438. def search_member():
  1439. if 'user_id' not in session:
  1440. return jsonify({"success": False, "message": "Unauthorized"}), 401
  1441. data = request.get_json()
  1442. keyword = data.get('keyword', '').strip()
  1443. search_type = data.get('search_type', 'name') # name | former_name | childhood_name | name_word
  1444. if not keyword:
  1445. return jsonify({"success": False, "message": "请输入搜索关键词"})
  1446. conn = get_db_connection()
  1447. try:
  1448. with conn.cursor() as cursor:
  1449. like = f'%{keyword}%'
  1450. # 根据搜索类型构建 WHERE 子句
  1451. if search_type == 'former_name':
  1452. where_clause = "fmi.former_name LIKE %s"
  1453. where_params = (like,)
  1454. order_params = (keyword, keyword, f'{keyword}%', f'{keyword}%')
  1455. elif search_type == 'childhood_name':
  1456. where_clause = "fmi.childhood_name LIKE %s"
  1457. where_params = (like,)
  1458. order_params = (keyword, keyword, f'{keyword}%', f'{keyword}%')
  1459. elif search_type == 'name_word':
  1460. where_clause = "fmi.name_word LIKE %s"
  1461. where_params = (like,)
  1462. order_params = (keyword, keyword, f'{keyword}%', f'{keyword}%')
  1463. else: # name(默认:姓名搜索)
  1464. where_clause = "fmi.name LIKE %s OR fmi.simplified_name LIKE %s OR fmi.former_name LIKE %s"
  1465. where_params = (like, like, like)
  1466. order_params = (keyword, keyword, f'{keyword}%', f'{keyword}%')
  1467. cursor.execute(f"""
  1468. SELECT
  1469. fmi.id,
  1470. fmi.name,
  1471. fmi.simplified_name,
  1472. fmi.name_word_generation,
  1473. fmi.former_name,
  1474. fmi.childhood_name,
  1475. fmi.name_word,
  1476. -- 父亲:优先非入继父(sub_type != 3),取第一条
  1477. (SELECT p.name FROM family_relation_info r
  1478. JOIN family_member_info p ON r.parent_mid = p.id
  1479. WHERE r.child_mid = fmi.id AND r.relation_type = 1
  1480. ORDER BY CASE WHEN COALESCE(r.sub_relation_type,0)=3 THEN 1 ELSE 0 END, r.id
  1481. LIMIT 1) AS father_name,
  1482. (SELECT p.simplified_name FROM family_relation_info r
  1483. JOIN family_member_info p ON r.parent_mid = p.id
  1484. WHERE r.child_mid = fmi.id AND r.relation_type = 1
  1485. ORDER BY CASE WHEN COALESCE(r.sub_relation_type,0)=3 THEN 1 ELSE 0 END, r.id
  1486. LIMIT 1) AS father_simplified_name,
  1487. -- 出继标注
  1488. (SELECT CONCAT(
  1489. '出继自',
  1490. COALESCE(pbio.simplified_name, pbio.name, '?'),
  1491. ',入继至',
  1492. COALESCE(padopt.simplified_name, padopt.name, '?'))
  1493. FROM family_relation_info rbio
  1494. JOIN family_member_info pbio ON rbio.parent_mid = pbio.id
  1495. JOIN family_relation_info radopt ON radopt.child_mid = fmi.id AND radopt.sub_relation_type = 3
  1496. JOIN family_member_info padopt ON radopt.parent_mid = padopt.id
  1497. WHERE rbio.child_mid = fmi.id AND rbio.sub_relation_type = 2
  1498. LIMIT 1) AS adoption_note
  1499. FROM family_member_info fmi
  1500. WHERE {where_clause}
  1501. GROUP BY fmi.id
  1502. ORDER BY
  1503. CASE WHEN fmi.name = %s THEN 1
  1504. WHEN fmi.simplified_name = %s THEN 2
  1505. WHEN fmi.name LIKE %s THEN 3
  1506. WHEN fmi.simplified_name LIKE %s THEN 4
  1507. ELSE 5 END
  1508. """, (*where_params, *order_params))
  1509. members = cursor.fetchall()
  1510. if not members:
  1511. return jsonify({"success": False, "message": "未找到匹配的成员"})
  1512. # 批量查询每位命中人员的子女(按 child_order 排序)
  1513. member_ids = [m['id'] for m in members]
  1514. fmt = ','.join(['%s'] * len(member_ids))
  1515. cursor.execute(f"""
  1516. SELECT r.parent_mid,
  1517. c.id AS child_id,
  1518. COALESCE(c.simplified_name, c.name) AS child_display_name
  1519. FROM family_relation_info r
  1520. JOIN family_member_info c ON r.child_mid = c.id
  1521. WHERE r.parent_mid IN ({fmt}) AND r.relation_type = 1
  1522. ORDER BY COALESCE(r.child_order, 9999), c.id
  1523. """, member_ids)
  1524. children_map = {}
  1525. for row in cursor.fetchall():
  1526. pid = row['parent_mid']
  1527. if pid not in children_map:
  1528. children_map[pid] = []
  1529. children_map[pid].append({'id': row['child_id'], 'name': row['child_display_name']})
  1530. members_out = []
  1531. for m in members:
  1532. d = dict(m)
  1533. d['children'] = children_map.get(m['id'], [])
  1534. members_out.append(d)
  1535. return jsonify({"success": True, "members": members_out})
  1536. finally:
  1537. conn.close()
  1538. @app.route('/manager/api/get_lineage/<int:member_id>')
  1539. def get_lineage(member_id):
  1540. if 'user_id' not in session:
  1541. return jsonify({"success": False, "message": "Unauthorized"}), 401
  1542. # 追溯模式:incense(香火传承,入继→养父为上辈) | blood(血脉追溯,亲生父为上辈)
  1543. mode = request.args.get('mode', 'incense')
  1544. import time
  1545. start_time = time.time()
  1546. print(f"[Lineage Query] Starting query for member_id: {member_id} mode={mode} at {time.strftime('%Y-%m-%d %H:%M:%S')}")
  1547. conn = get_db_connection()
  1548. try:
  1549. with conn.cursor() as cursor:
  1550. # Step 1: Get center person
  1551. step_start = time.time()
  1552. cursor.execute("SELECT id, name, simplified_name, name_word, name_word_generation, former_name, childhood_name FROM family_member_info WHERE id = %s", (member_id,))
  1553. center = cursor.fetchone()
  1554. print(f"[Lineage Query] Step 1 - Get center: {time.time() - step_start:.3f}s")
  1555. if not center:
  1556. return jsonify({"success": False, "message": "成员不存在"})
  1557. # Step 2: Get ancestors with their siblings (generations)
  1558. step_start = time.time()
  1559. generations = [] # Array of generations, each with main ancestor and siblings
  1560. current_id = member_id
  1561. max_depth = 100 # 支持最多 100 代祖先(实际家谱一般不超过 80 代)
  1562. ancestor_ids = [] # Track ancestor IDs for exclusion when expanding
  1563. displayed_ids = set() # Track IDs that are already displayed
  1564. displayed_ids.add(member_id) # Center person is displayed
  1565. visited_ancestor_ids = set([member_id]) # 循环检测:避免脏数据死循环
  1566. for depth in range(max_depth):
  1567. # 获取所有父母关系(支持出继/入继)
  1568. cursor.execute("""
  1569. SELECT p.id, p.name, p.simplified_name, p.name_word, p.name_word_generation, p.former_name, p.childhood_name,
  1570. EXISTS(SELECT 1 FROM family_relation_info WHERE parent_mid = p.id AND relation_type IN (1, 2)) as has_children,
  1571. r.sub_relation_type
  1572. FROM family_relation_info r
  1573. JOIN family_member_info p ON r.parent_mid = p.id
  1574. WHERE r.child_mid = %s AND r.relation_type IN (1, 2)
  1575. """, (current_id,))
  1576. parents = cursor.fetchall()
  1577. if not parents:
  1578. break
  1579. # 分拣各类父母关系
  1580. normal_parent = None
  1581. adoptive_parent = None # sub_type=3:入继养父
  1582. bio_parent = None # sub_type=2:出继亲生父
  1583. for p in parents:
  1584. if p['sub_relation_type'] == 3:
  1585. adoptive_parent = p
  1586. elif p['sub_relation_type'] == 2:
  1587. bio_parent = p
  1588. else:
  1589. normal_parent = p
  1590. if mode == 'blood':
  1591. # 血脉追溯:亲生父优先(出继亦沿亲生路径)
  1592. parent = normal_parent or bio_parent or adoptive_parent
  1593. # 血脉模式下若存在养父,仍标注出继/入继关系
  1594. if adoptive_parent is not None:
  1595. _bio_src = bio_parent or normal_parent
  1596. bio_name = (_bio_src.get('simplified_name') or _bio_src.get('name')) if _bio_src else None
  1597. adop_name = adoptive_parent.get('simplified_name') or adoptive_parent.get('name')
  1598. if bio_name and adop_name:
  1599. adopt_label = f"出继自{bio_name},入继至{adop_name}"
  1600. elif adop_name:
  1601. adopt_label = f"入继至{adop_name}"
  1602. else:
  1603. adopt_label = "出继"
  1604. if depth == 0:
  1605. center['adoption_label'] = adopt_label
  1606. elif generations:
  1607. generations[-1]['ancestor']['adoption_label'] = adopt_label
  1608. else:
  1609. # 香火传承(默认):入继养父优先
  1610. parent = adoptive_parent or normal_parent or bio_parent
  1611. # 若走入继路径,在当事人卡片标注"出继自xx,入继至yy"
  1612. if parent is adoptive_parent and adoptive_parent is not None:
  1613. # bio 父:sub_type=2 优先,否则取 normal (sub_type=0)
  1614. _bio_src = bio_parent or normal_parent
  1615. bio_name = (_bio_src.get('simplified_name') or _bio_src.get('name')) if _bio_src else None
  1616. adop_name = adoptive_parent.get('simplified_name') or adoptive_parent.get('name')
  1617. if bio_name and adop_name:
  1618. adopt_label = f"出继自{bio_name},入继至{adop_name}"
  1619. elif bio_name:
  1620. adopt_label = f"出继自{bio_name}"
  1621. else:
  1622. adopt_label = "出继"
  1623. if depth == 0:
  1624. center['adoption_label'] = adopt_label
  1625. elif generations:
  1626. generations[-1]['ancestor']['adoption_label'] = adopt_label
  1627. # 祖先卡片不携带子辈关系类型(避免把子的出继/入继标在父身上)
  1628. parent['sub_relation_type'] = None
  1629. # 循环检测:如果该祖先已在链中出现过,终止(数据异常保护)
  1630. if parent['id'] in visited_ancestor_ids:
  1631. break
  1632. visited_ancestor_ids.add(parent['id'])
  1633. ancestor_ids.append(parent['id'])
  1634. displayed_ids.add(parent['id'])
  1635. # Get siblings of this ancestor (father's brothers)
  1636. # First get grandparent (parent's father)
  1637. # 香火传承:优先选养父(sub_type=3);血脉传承:优先选生父(sub_type=0/2)
  1638. if mode == 'incense':
  1639. gp_order = "CASE WHEN COALESCE(r.sub_relation_type, 0) = 3 THEN 0 ELSE 1 END"
  1640. else:
  1641. gp_order = "CASE WHEN COALESCE(r.sub_relation_type, 0) = 3 THEN 1 ELSE 0 END"
  1642. cursor.execute(f"""
  1643. SELECT gp.id, gp.name, gp.simplified_name
  1644. FROM family_relation_info r
  1645. JOIN family_member_info gp ON r.parent_mid = gp.id
  1646. WHERE r.child_mid = %s AND r.relation_type IN (1, 2)
  1647. ORDER BY {gp_order}, r.id
  1648. LIMIT 1
  1649. """, (parent['id'],))
  1650. grandparent = cursor.fetchone()
  1651. parent_siblings = []
  1652. if grandparent:
  1653. # 获取祖先自身的 child_order(在祖父下的排行)
  1654. cursor.execute("""
  1655. SELECT COALESCE(child_order, NULL) AS child_order
  1656. FROM family_relation_info
  1657. WHERE parent_mid = %s AND child_mid = %s AND relation_type IN (1, 2)
  1658. LIMIT 1
  1659. """, (grandparent['id'], parent['id']))
  1660. co_row = cursor.fetchone()
  1661. parent['child_order'] = co_row['child_order'] if co_row else None
  1662. # 获取祖先的兄弟(按模式过滤 + 出继/入继标注)
  1663. if mode == 'blood':
  1664. anc_sib_filter = "AND COALESCE(r.sub_relation_type, 0) != 3"
  1665. else:
  1666. anc_sib_filter = """AND (
  1667. COALESCE(r.sub_relation_type, 0) != 2
  1668. OR NOT EXISTS (
  1669. SELECT 1 FROM family_relation_info r2
  1670. WHERE r2.child_mid = c.id AND r2.sub_relation_type = 3
  1671. )
  1672. )"""
  1673. cursor.execute(f"""
  1674. SELECT c.id, c.name, c.simplified_name, c.name_word, c.name_word_generation, c.former_name, c.childhood_name,
  1675. EXISTS(SELECT 1 FROM family_relation_info WHERE parent_mid = c.id AND relation_type IN (1, 2)) as has_children,
  1676. COALESCE(r.child_order, NULL) AS child_order,
  1677. r.sub_relation_type
  1678. FROM family_relation_info r
  1679. JOIN family_member_info c ON r.child_mid = c.id
  1680. WHERE r.parent_mid = %s AND r.relation_type IN (1, 2) AND c.id != %s
  1681. {anc_sib_filter}
  1682. ORDER BY COALESCE(r.child_order, 99999), c.id
  1683. LIMIT 30
  1684. """, (grandparent['id'], parent['id']))
  1685. parent_siblings = cursor.fetchall()
  1686. # 生成兄弟节点标注(入继/出继双向)
  1687. # 兄弟是祖父的子女,所以"入继至/出继自"用祖父名,不是当前祖先名
  1688. gp_display = grandparent.get('simplified_name') or grandparent.get('name') or ''
  1689. for sib in parent_siblings:
  1690. if sib.get('sub_relation_type') == 3:
  1691. # 入继兄弟:出继自bio,入继至祖父(祖父是收养方)
  1692. cursor.execute("""
  1693. SELECT p.simplified_name, p.name
  1694. FROM family_relation_info r
  1695. JOIN family_member_info p ON r.parent_mid = p.id
  1696. WHERE r.child_mid = %s AND r.sub_relation_type != 3
  1697. ORDER BY r.sub_relation_type DESC LIMIT 1
  1698. """, (sib['id'],))
  1699. sbp = cursor.fetchone()
  1700. bio_name = (sbp['simplified_name'] or sbp['name']) if sbp else None
  1701. if bio_name and gp_display:
  1702. sib['adoption_label'] = f"出继自{bio_name},入继至{gp_display}"
  1703. elif bio_name:
  1704. sib['adoption_label'] = f"出继自{bio_name}"
  1705. else:
  1706. sib['adoption_label'] = "出继"
  1707. elif mode == 'blood' and sib.get('sub_relation_type') == 2:
  1708. # 血脉模式出继兄弟:出继自祖父(祖父是出继方),入继至养父
  1709. cursor.execute("""
  1710. SELECT p.simplified_name, p.name
  1711. FROM family_relation_info r
  1712. JOIN family_member_info p ON r.parent_mid = p.id
  1713. WHERE r.child_mid = %s AND r.sub_relation_type = 3 LIMIT 1
  1714. """, (sib['id'],))
  1715. adop = cursor.fetchone()
  1716. adop_name = (adop['simplified_name'] or adop['name']) if adop else None
  1717. if gp_display and adop_name:
  1718. sib['adoption_label'] = f"出继自{gp_display},入继至{adop_name}"
  1719. elif adop_name:
  1720. sib['adoption_label'] = f"入继至{adop_name}"
  1721. else:
  1722. sib['adoption_label'] = "出继"
  1723. # Mark sibling IDs as displayed
  1724. for sibling in parent_siblings:
  1725. displayed_ids.add(sibling['id'])
  1726. # Check if parent has any children NOT already displayed
  1727. # Only show expand button if there are undisplayed children
  1728. cursor.execute("""
  1729. SELECT COUNT(*) as count
  1730. FROM family_relation_info r
  1731. JOIN family_member_info c ON r.child_mid = c.id
  1732. WHERE r.parent_mid = %s AND r.relation_type IN (1, 2)
  1733. """, (parent['id'],))
  1734. total_children = cursor.fetchone()['count']
  1735. # Check if current child is displayed (current_id is the child of parent)
  1736. child_displayed = current_id in displayed_ids
  1737. # Show expand if there are children not displayed
  1738. show_expand = total_children > (1 if child_displayed else 0)
  1739. parent['show_expand'] = show_expand
  1740. generations.append({
  1741. 'ancestor': parent,
  1742. 'siblings': parent_siblings,
  1743. 'depth': depth
  1744. })
  1745. current_id = parent['id']
  1746. print(f"[Lineage Query] Step 2 - Get generations ({len(generations)}): {time.time() - step_start:.3f}s")
  1747. # Step 3: Get immediate children only (limited count)
  1748. step_start = time.time()
  1749. # 获取子女:根据模式选择不同过滤策略
  1750. # 香火传承:包含入继子女、普通子女,排除已被人收继的出继子女
  1751. # 血脉追溯:包含亲生子女(含出继走的),排除从别处入继的子女
  1752. if mode == 'blood':
  1753. children_filter = "AND COALESCE(r.sub_relation_type, 0) != 3"
  1754. else:
  1755. children_filter = """AND (
  1756. COALESCE(r.sub_relation_type, 0) != 2
  1757. OR NOT EXISTS (
  1758. SELECT 1 FROM family_relation_info r2
  1759. WHERE r2.child_mid = c.id AND r2.sub_relation_type = 3
  1760. )
  1761. )"""
  1762. cursor.execute(f"""
  1763. SELECT c.id, c.name, c.simplified_name, c.name_word, c.name_word_generation, c.former_name, c.childhood_name,
  1764. EXISTS(SELECT 1 FROM family_relation_info WHERE parent_mid = c.id AND relation_type IN (1, 2)) as has_children,
  1765. r.sub_relation_type,
  1766. r.child_order
  1767. FROM family_relation_info r
  1768. JOIN family_member_info c ON r.child_mid = c.id
  1769. WHERE r.parent_mid = %s AND r.relation_type IN (1, 2)
  1770. {children_filter}
  1771. ORDER BY COALESCE(r.child_order, 99999), c.id
  1772. LIMIT 30
  1773. """, (member_id,))
  1774. children = cursor.fetchall()
  1775. # 香火模式:对入继子女标注"出继自xx,入继至yy";血脉模式:对出继子女同样双标
  1776. center_display = center.get('simplified_name') or center.get('name') or ''
  1777. for child in children:
  1778. if mode == 'incense' and child['sub_relation_type'] == 3:
  1779. # 亲生父:可能是 sub_type=2(出继标记)或 sub_type=0(普通关系)
  1780. cursor.execute("""
  1781. SELECT p.simplified_name, p.name
  1782. FROM family_relation_info r
  1783. JOIN family_member_info p ON r.parent_mid = p.id
  1784. WHERE r.child_mid = %s AND r.sub_relation_type != 3
  1785. ORDER BY r.sub_relation_type DESC
  1786. LIMIT 1
  1787. """, (child['id'],))
  1788. bio_p = cursor.fetchone()
  1789. bio_name = (bio_p['simplified_name'] or bio_p['name']) if bio_p else None
  1790. if bio_name and center_display:
  1791. child['adoption_label'] = f"出继自{bio_name},入继至{center_display}"
  1792. elif bio_name:
  1793. child['adoption_label'] = f"出继自{bio_name}"
  1794. else:
  1795. child['adoption_label'] = "出继"
  1796. elif mode == 'blood' and child['sub_relation_type'] in (0, 2):
  1797. # 血脉模式:找养父(sub_type=3)标注去向
  1798. cursor.execute("""
  1799. SELECT p.simplified_name, p.name
  1800. FROM family_relation_info r
  1801. JOIN family_member_info p ON r.parent_mid = p.id
  1802. WHERE r.child_mid = %s AND r.sub_relation_type = 3 LIMIT 1
  1803. """, (child['id'],))
  1804. adop_p = cursor.fetchone()
  1805. adop_name = (adop_p['simplified_name'] or adop_p['name']) if adop_p else None
  1806. if adop_name:
  1807. if center_display:
  1808. child['adoption_label'] = f"出继自{center_display},入继至{adop_name}"
  1809. else:
  1810. child['adoption_label'] = f"入继至{adop_name}"
  1811. child['sub_relation_type'] = 2 # 保持供前端显示 adopted-out 样式
  1812. # Initialize children array
  1813. for child in children:
  1814. child['children'] = []
  1815. print(f"[Lineage Query] Step 3 - Get children ({len(children)}): {time.time() - step_start:.3f}s")
  1816. # Step 4: Get siblings of center person + center's own child_order
  1817. step_start = time.time()
  1818. siblings = []
  1819. center_child_order = None
  1820. if generations:
  1821. parent_id = generations[0]['ancestor']['id'] # Father
  1822. # 中心人物自身的排行
  1823. cursor.execute("""
  1824. SELECT COALESCE(child_order, NULL) AS child_order
  1825. FROM family_relation_info
  1826. WHERE parent_mid = %s AND child_mid = %s AND relation_type IN (1, 2)
  1827. LIMIT 1
  1828. """, (parent_id, member_id))
  1829. co_row = cursor.fetchone()
  1830. center_child_order = co_row['child_order'] if co_row else None
  1831. # 香火模式:排除已出继的兄弟(已被别家收入的);血脉模式:排除从外面入继进来的
  1832. if mode == 'blood':
  1833. sib_filter = "AND COALESCE(r.sub_relation_type, 0) != 3"
  1834. else:
  1835. sib_filter = """AND (
  1836. COALESCE(r.sub_relation_type, 0) != 2
  1837. OR NOT EXISTS (
  1838. SELECT 1 FROM family_relation_info r2
  1839. WHERE r2.child_mid = c.id AND r2.sub_relation_type = 3
  1840. )
  1841. )"""
  1842. cursor.execute(f"""
  1843. SELECT c.id, c.name, c.simplified_name, c.name_word, c.name_word_generation, c.former_name, c.childhood_name,
  1844. EXISTS(SELECT 1 FROM family_relation_info WHERE parent_mid = c.id AND relation_type IN (1, 2)) as has_children,
  1845. r.sub_relation_type,
  1846. COALESCE(r.child_order, NULL) AS child_order
  1847. FROM family_relation_info r
  1848. JOIN family_member_info c ON r.child_mid = c.id
  1849. WHERE r.parent_mid = %s AND r.relation_type IN (1, 2) AND c.id != %s
  1850. {sib_filter}
  1851. ORDER BY COALESCE(r.child_order, 99999), c.id
  1852. LIMIT 30
  1853. """, (parent_id, member_id))
  1854. siblings = cursor.fetchall()
  1855. # 为入继兄弟补充"出继自xx,入继至yy"标注
  1856. parent_display = (generations[0]['ancestor'].get('simplified_name') or
  1857. generations[0]['ancestor'].get('name') or '') if generations else ''
  1858. for sib in siblings:
  1859. if sib.get('sub_relation_type') == 3:
  1860. cursor.execute("""
  1861. SELECT p.simplified_name, p.name
  1862. FROM family_relation_info r
  1863. JOIN family_member_info p ON r.parent_mid = p.id
  1864. WHERE r.child_mid = %s AND r.sub_relation_type != 3
  1865. ORDER BY r.sub_relation_type DESC
  1866. LIMIT 1
  1867. """, (sib['id'],))
  1868. sbp = cursor.fetchone()
  1869. bio_name = (sbp['simplified_name'] or sbp['name']) if sbp else None
  1870. if bio_name and parent_display:
  1871. sib['adoption_label'] = f"出继自{bio_name},入继至{parent_display}"
  1872. elif bio_name:
  1873. sib['adoption_label'] = f"出继自{bio_name}"
  1874. else:
  1875. sib['adoption_label'] = "出继"
  1876. elif mode == 'blood' and sib.get('sub_relation_type') == 2:
  1877. # 血脉模式:出继兄弟 → 出继自当前父亲,入继至养父
  1878. cursor.execute("""
  1879. SELECT p.simplified_name, p.name
  1880. FROM family_relation_info r
  1881. JOIN family_member_info p ON r.parent_mid = p.id
  1882. WHERE r.child_mid = %s AND r.sub_relation_type = 3 LIMIT 1
  1883. """, (sib['id'],))
  1884. adop = cursor.fetchone()
  1885. adop_name = (adop['simplified_name'] or adop['name']) if adop else None
  1886. if parent_display and adop_name:
  1887. sib['adoption_label'] = f"出继自{parent_display},入继至{adop_name}"
  1888. elif adop_name:
  1889. sib['adoption_label'] = f"入继至{adop_name}"
  1890. else:
  1891. sib['adoption_label'] = "出继"
  1892. print(f"[Lineage Query] Step 4 - Get siblings ({len(siblings)}): {time.time() - step_start:.3f}s")
  1893. total_time = time.time() - start_time
  1894. print(f"[Lineage Query] Total time: {total_time:.3f}s")
  1895. # 判断是否还有更高的祖先(顶端祖先是否仍有父亲)
  1896. has_more_ancestors = False
  1897. topmost_ancestor_id = None
  1898. if generations:
  1899. topmost_ancestor_id = generations[-1]['ancestor']['id']
  1900. cursor.execute("""
  1901. SELECT COUNT(*) as cnt FROM family_relation_info
  1902. WHERE child_mid = %s AND relation_type IN (1,2)
  1903. """, (topmost_ancestor_id,))
  1904. has_more_ancestors = cursor.fetchone()['cnt'] > 0
  1905. return jsonify({
  1906. "success": True,
  1907. "data": {
  1908. "center": {**center, "child_order": center_child_order},
  1909. "generations": generations,
  1910. "ancestor_ids": ancestor_ids,
  1911. "siblings": siblings,
  1912. "children": children,
  1913. "has_more_ancestors": has_more_ancestors,
  1914. "topmost_ancestor_id": topmost_ancestor_id
  1915. }
  1916. })
  1917. except Exception as e:
  1918. print(f"[Lineage Query] Error: {e}")
  1919. return jsonify({"success": False, "message": str(e)})
  1920. finally:
  1921. conn.close()
  1922. @app.route('/manager/api/get_ancestors_above/<int:ancestor_id>')
  1923. def get_ancestors_above(ancestor_id):
  1924. """从指定祖先节点继续向上追溯,用于世系查询"继续向上"按钮"""
  1925. if 'user_id' not in session:
  1926. return jsonify({"success": False, "message": "Unauthorized"}), 401
  1927. mode = request.args.get('mode', 'incense')
  1928. conn = get_db_connection()
  1929. try:
  1930. with conn.cursor() as cursor:
  1931. generations = []
  1932. current_id = ancestor_id
  1933. max_depth = 100
  1934. visited_ids = set([ancestor_id])
  1935. # 计算 anchor 节点(ancestor_id)自身的 adoption_label(已在上层渲染,此处只补充标签)
  1936. anchor_adoption_label = None
  1937. cursor.execute("""
  1938. SELECT p.id, p.name, p.simplified_name, r.sub_relation_type
  1939. FROM family_relation_info r
  1940. JOIN family_member_info p ON r.parent_mid = p.id
  1941. WHERE r.child_mid = %s AND r.relation_type IN (1, 2)
  1942. """, (ancestor_id,))
  1943. anchor_parents = cursor.fetchall()
  1944. anchor_bio = None
  1945. anchor_adop = None
  1946. anchor_normal = None
  1947. for ap in anchor_parents:
  1948. if ap['sub_relation_type'] == 3:
  1949. anchor_adop = ap
  1950. elif ap['sub_relation_type'] == 2:
  1951. anchor_bio = ap
  1952. else:
  1953. anchor_normal = ap
  1954. if anchor_adop:
  1955. _bio_src = anchor_bio or anchor_normal
  1956. bio_name = (_bio_src.get('simplified_name') or _bio_src.get('name')) if _bio_src else None
  1957. adop_name = anchor_adop.get('simplified_name') or anchor_adop.get('name')
  1958. if bio_name and adop_name:
  1959. anchor_adoption_label = f"出继自{bio_name},入继至{adop_name}"
  1960. elif bio_name:
  1961. anchor_adoption_label = f"出继自{bio_name}"
  1962. else:
  1963. anchor_adoption_label = "出继"
  1964. for depth in range(max_depth):
  1965. cursor.execute("""
  1966. SELECT p.id, p.name, p.simplified_name, p.name_word, p.name_word_generation, p.former_name, p.childhood_name,
  1967. EXISTS(SELECT 1 FROM family_relation_info
  1968. WHERE parent_mid = p.id AND relation_type IN (1,2)) as has_children,
  1969. r.sub_relation_type
  1970. FROM family_relation_info r
  1971. JOIN family_member_info p ON r.parent_mid = p.id
  1972. WHERE r.child_mid = %s AND r.relation_type IN (1, 2)
  1973. """, (current_id,))
  1974. parents = cursor.fetchall()
  1975. if not parents:
  1976. break
  1977. # 分拣各类父母关系
  1978. normal_parent = None
  1979. adoptive_parent = None
  1980. bio_parent = None
  1981. for p in parents:
  1982. if p['sub_relation_type'] == 3:
  1983. adoptive_parent = p
  1984. elif p['sub_relation_type'] == 2:
  1985. bio_parent = p
  1986. else:
  1987. normal_parent = p
  1988. if mode == 'blood':
  1989. parent = normal_parent or bio_parent or adoptive_parent
  1990. # 血脉模式下若存在养父,仍标注出继/入继关系
  1991. if adoptive_parent is not None:
  1992. _bio_src = bio_parent or normal_parent
  1993. bio_name = (_bio_src.get('simplified_name') or _bio_src.get('name')) if _bio_src else None
  1994. adop_name = adoptive_parent.get('simplified_name') or adoptive_parent.get('name')
  1995. if bio_name and adop_name:
  1996. adopt_label = f"出继自{bio_name},入继至{adop_name}"
  1997. elif adop_name:
  1998. adopt_label = f"入继至{adop_name}"
  1999. else:
  2000. adopt_label = "出继"
  2001. if depth == 0:
  2002. anchor_adoption_label = adopt_label
  2003. elif generations:
  2004. generations[-1]['ancestor']['adoption_label'] = adopt_label
  2005. else:
  2006. parent = adoptive_parent or normal_parent or bio_parent
  2007. # 若走入继路径,在 current_id 对应的人物上标注"出继自xx,入继至yy"
  2008. if parent is adoptive_parent and adoptive_parent is not None:
  2009. _bio_src = bio_parent or normal_parent
  2010. bio_name = (_bio_src.get('simplified_name') or _bio_src.get('name')) if _bio_src else None
  2011. adop_name = adoptive_parent.get('simplified_name') or adoptive_parent.get('name')
  2012. if bio_name and adop_name:
  2013. adopt_label = f"出继自{bio_name},入继至{adop_name}"
  2014. elif bio_name:
  2015. adopt_label = f"出继自{bio_name}"
  2016. else:
  2017. adopt_label = "出继"
  2018. if depth == 0:
  2019. anchor_adoption_label = adopt_label
  2020. elif generations:
  2021. generations[-1]['ancestor']['adoption_label'] = adopt_label
  2022. # 祖先卡片不携带子辈关系类型
  2023. parent['sub_relation_type'] = None
  2024. if parent['id'] in visited_ids:
  2025. break
  2026. visited_ids.add(parent['id'])
  2027. # 查祖父,用于获取该祖先的兄弟
  2028. # 香火传承:优先养父(sub_type=3);血脉传承:优先生父
  2029. if mode == 'incense':
  2030. gp_order_ab = "CASE WHEN COALESCE(r.sub_relation_type, 0) = 3 THEN 0 ELSE 1 END"
  2031. else:
  2032. gp_order_ab = "CASE WHEN COALESCE(r.sub_relation_type, 0) = 3 THEN 1 ELSE 0 END"
  2033. cursor.execute(f"""
  2034. SELECT gp.id, gp.name, gp.simplified_name FROM family_relation_info r
  2035. JOIN family_member_info gp ON r.parent_mid = gp.id
  2036. WHERE r.child_mid = %s AND r.relation_type IN (1, 2)
  2037. ORDER BY {gp_order_ab}, r.id
  2038. LIMIT 1
  2039. """, (parent['id'],))
  2040. grandparent = cursor.fetchone()
  2041. parent_siblings = []
  2042. if grandparent:
  2043. cursor.execute("""
  2044. SELECT COALESCE(child_order, 1) AS child_order
  2045. FROM family_relation_info
  2046. WHERE parent_mid = %s AND child_mid = %s AND relation_type IN (1,2) LIMIT 1
  2047. """, (grandparent['id'], parent['id']))
  2048. co_row = cursor.fetchone()
  2049. parent['child_order'] = co_row['child_order'] if co_row else 1
  2050. if mode == 'blood':
  2051. ab_sib_filter = "AND COALESCE(r.sub_relation_type, 0) != 3"
  2052. else:
  2053. ab_sib_filter = """AND (
  2054. COALESCE(r.sub_relation_type, 0) != 2
  2055. OR NOT EXISTS (
  2056. SELECT 1 FROM family_relation_info r2
  2057. WHERE r2.child_mid = c.id AND r2.sub_relation_type = 3
  2058. )
  2059. )"""
  2060. cursor.execute(f"""
  2061. SELECT c.id, c.name, c.simplified_name, c.name_word, c.name_word_generation, c.former_name, c.childhood_name,
  2062. EXISTS(SELECT 1 FROM family_relation_info
  2063. WHERE parent_mid = c.id AND relation_type IN (1,2)) as has_children,
  2064. COALESCE(r.child_order, 1) AS child_order
  2065. FROM family_relation_info r
  2066. JOIN family_member_info c ON r.child_mid = c.id
  2067. WHERE r.parent_mid = %s AND r.relation_type IN (1,2) AND c.id != %s
  2068. {ab_sib_filter}
  2069. ORDER BY COALESCE(r.child_order, 1), c.id
  2070. LIMIT 10
  2071. """, (grandparent['id'], parent['id']))
  2072. parent_siblings = cursor.fetchall()
  2073. # 兄弟是祖父的子女,"入继至/出继自"用祖父名
  2074. ab_gp_display = grandparent.get('simplified_name') or grandparent.get('name') or ''
  2075. for s in parent_siblings:
  2076. s['has_children'] = bool(s['has_children'])
  2077. if s.get('sub_relation_type') == 3:
  2078. cursor.execute("""
  2079. SELECT p.simplified_name, p.name
  2080. FROM family_relation_info r
  2081. JOIN family_member_info p ON r.parent_mid = p.id
  2082. WHERE r.child_mid = %s AND r.sub_relation_type != 3
  2083. ORDER BY r.sub_relation_type DESC LIMIT 1
  2084. """, (s['id'],))
  2085. sbp = cursor.fetchone()
  2086. bio_name = (sbp['simplified_name'] or sbp['name']) if sbp else None
  2087. if bio_name and ab_gp_display:
  2088. s['adoption_label'] = f"出继自{bio_name},入继至{ab_gp_display}"
  2089. elif bio_name:
  2090. s['adoption_label'] = f"出继自{bio_name}"
  2091. else:
  2092. s['adoption_label'] = "出继"
  2093. elif mode == 'blood' and s.get('sub_relation_type') == 2:
  2094. cursor.execute("""
  2095. SELECT p.simplified_name, p.name
  2096. FROM family_relation_info r
  2097. JOIN family_member_info p ON r.parent_mid = p.id
  2098. WHERE r.child_mid = %s AND r.sub_relation_type = 3 LIMIT 1
  2099. """, (s['id'],))
  2100. adop = cursor.fetchone()
  2101. adop_name = (adop['simplified_name'] or adop['name']) if adop else None
  2102. if ab_gp_display and adop_name:
  2103. s['adoption_label'] = f"出继自{ab_gp_display},入继至{adop_name}"
  2104. elif adop_name:
  2105. s['adoption_label'] = f"入继至{adop_name}"
  2106. else:
  2107. s['adoption_label'] = "出继"
  2108. else:
  2109. parent['child_order'] = None
  2110. parent['has_children'] = bool(parent['has_children'])
  2111. generations.append({
  2112. 'ancestor': parent,
  2113. 'siblings': list(parent_siblings),
  2114. 'depth': depth
  2115. })
  2116. current_id = parent['id']
  2117. # 是否还有更高的祖先
  2118. has_more_ancestors = False
  2119. topmost_ancestor_id = None
  2120. if generations:
  2121. topmost_ancestor_id = generations[-1]['ancestor']['id']
  2122. cursor.execute("""
  2123. SELECT COUNT(*) as cnt FROM family_relation_info
  2124. WHERE child_mid = %s AND relation_type IN (1,2)
  2125. """, (topmost_ancestor_id,))
  2126. has_more_ancestors = cursor.fetchone()['cnt'] > 0
  2127. return jsonify({
  2128. "success": True,
  2129. "data": {
  2130. "generations": generations,
  2131. "has_more_ancestors": has_more_ancestors,
  2132. "topmost_ancestor_id": topmost_ancestor_id,
  2133. "anchor_adoption_label": anchor_adoption_label
  2134. }
  2135. })
  2136. except Exception as e:
  2137. return jsonify({"success": False, "message": str(e)})
  2138. finally:
  2139. conn.close()
  2140. @app.route('/manager/api/get_descendants/<int:parent_id>')
  2141. def get_descendants(parent_id):
  2142. if 'user_id' not in session:
  2143. return jsonify({"success": False, "message": "Unauthorized"}), 401
  2144. excluded_ids = request.args.get('exclude', '')
  2145. excluded_list = [int(i.strip()) for i in excluded_ids.split(',') if i.strip().isdigit()]
  2146. mode = request.args.get('mode', 'incense')
  2147. # 按模式决定子女过滤策略(与 get_lineage 保持一致)
  2148. if mode == 'blood':
  2149. mode_filter = "AND COALESCE(r.sub_relation_type, 0) != 3"
  2150. else: # incense
  2151. mode_filter = """AND (
  2152. COALESCE(r.sub_relation_type, 0) != 2
  2153. OR NOT EXISTS (
  2154. SELECT 1 FROM family_relation_info r2
  2155. WHERE r2.child_mid = c.id AND r2.sub_relation_type = 3
  2156. )
  2157. )"""
  2158. print(f"[get_descendants] Parent ID: {parent_id}, mode={mode}, Excluded IDs: {excluded_list}")
  2159. conn = get_db_connection()
  2160. try:
  2161. with conn.cursor() as cursor:
  2162. # 先获取父节点名字(用于"入继至xx"标注)
  2163. cursor.execute(
  2164. "SELECT simplified_name, name FROM family_member_info WHERE id = %s",
  2165. (parent_id,)
  2166. )
  2167. parent_row = cursor.fetchone()
  2168. parent_display = (parent_row.get('simplified_name') or parent_row.get('name') or '') if parent_row else ''
  2169. if excluded_list:
  2170. placeholders = ', '.join(['%s'] * len(excluded_list))
  2171. cursor.execute(f"""
  2172. SELECT c.id, c.name, c.simplified_name, c.name_word, c.name_word_generation,
  2173. c.former_name, c.childhood_name,
  2174. r.sub_relation_type,
  2175. COALESCE(r.child_order, 99999) AS child_order,
  2176. EXISTS(SELECT 1 FROM family_relation_info
  2177. WHERE parent_mid = c.id AND relation_type IN (1, 2)) as has_children
  2178. FROM family_relation_info r
  2179. JOIN family_member_info c ON r.child_mid = c.id
  2180. WHERE r.parent_mid = %s AND r.relation_type IN (1, 2)
  2181. AND c.id NOT IN ({placeholders})
  2182. {mode_filter}
  2183. ORDER BY COALESCE(r.child_order, 99999), c.id
  2184. LIMIT 20
  2185. """, (parent_id,) + tuple(excluded_list))
  2186. else:
  2187. cursor.execute(f"""
  2188. SELECT c.id, c.name, c.simplified_name, c.name_word, c.name_word_generation,
  2189. c.former_name, c.childhood_name,
  2190. r.sub_relation_type,
  2191. COALESCE(r.child_order, 99999) AS child_order,
  2192. EXISTS(SELECT 1 FROM family_relation_info
  2193. WHERE parent_mid = c.id AND relation_type IN (1, 2)) as has_children
  2194. FROM family_relation_info r
  2195. JOIN family_member_info c ON r.child_mid = c.id
  2196. WHERE r.parent_mid = %s AND r.relation_type IN (1, 2)
  2197. {mode_filter}
  2198. ORDER BY COALESCE(r.child_order, 99999), c.id
  2199. LIMIT 20
  2200. """, (parent_id,))
  2201. children = cursor.fetchall()
  2202. # 为入继/出继子女生成 adoption_label
  2203. for child in children:
  2204. child['children'] = []
  2205. if child.get('sub_relation_type') == 3:
  2206. # 入继子女:出继自bio父,入继至当前节点
  2207. cursor.execute("""
  2208. SELECT p.simplified_name, p.name
  2209. FROM family_relation_info r
  2210. JOIN family_member_info p ON r.parent_mid = p.id
  2211. WHERE r.child_mid = %s AND r.sub_relation_type != 3
  2212. ORDER BY r.sub_relation_type DESC
  2213. LIMIT 1
  2214. """, (child['id'],))
  2215. bio_p = cursor.fetchone()
  2216. bio_name = (bio_p['simplified_name'] or bio_p['name']) if bio_p else None
  2217. if bio_name and parent_display:
  2218. child['adoption_label'] = f"出继自{bio_name},入继至{parent_display}"
  2219. elif bio_name:
  2220. child['adoption_label'] = f"出继自{bio_name}"
  2221. else:
  2222. child['adoption_label'] = "出继"
  2223. elif mode == 'blood' and child.get('sub_relation_type') == 2:
  2224. # 血脉模式出继子女:出继自当前节点,入继至养父
  2225. cursor.execute("""
  2226. SELECT p.simplified_name, p.name
  2227. FROM family_relation_info r
  2228. JOIN family_member_info p ON r.parent_mid = p.id
  2229. WHERE r.child_mid = %s AND r.sub_relation_type = 3 LIMIT 1
  2230. """, (child['id'],))
  2231. adop_p = cursor.fetchone()
  2232. adop_name = (adop_p['simplified_name'] or adop_p['name']) if adop_p else None
  2233. if parent_display and adop_name:
  2234. child['adoption_label'] = f"出继自{parent_display},入继至{adop_name}"
  2235. elif adop_name:
  2236. child['adoption_label'] = f"入继至{adop_name}"
  2237. else:
  2238. child['adoption_label'] = "出继"
  2239. return jsonify({"success": True, "children": children})
  2240. finally:
  2241. conn.close()
  2242. @app.route('/manager/api/save_relation', methods=['POST'])
  2243. def save_relation():
  2244. if 'user_id' not in session:
  2245. return jsonify({"success": False, "message": "Unauthorized"}), 401
  2246. data = request.json
  2247. source_mid = data.get('source_mid') # The member being dragged
  2248. target_mid = data.get('target_mid') # The member being dropped onto
  2249. rel_type = int(data.get('relation_type'))
  2250. sub_rel_type = int(data.get('sub_relation_type', 0))
  2251. if not source_mid or not target_mid or not rel_type:
  2252. return jsonify({"success": False, "message": "参数不完整"}), 400
  2253. conn = get_db_connection()
  2254. try:
  2255. with conn.cursor() as cursor:
  2256. # 简单处理:如果是父子/母子关系
  2257. # target_mid 是父辈,source_mid 是子辈
  2258. parent_mid = target_mid
  2259. child_mid = source_mid
  2260. gen_diff = 1
  2261. if rel_type == 10: # 夫妻
  2262. # 夫妻关系中,我们通常把关联人设为 parent_mid
  2263. parent_mid = target_mid
  2264. child_mid = source_mid
  2265. gen_diff = 0
  2266. elif rel_type in [11, 12]: # 兄弟姐妹
  2267. # 这里逻辑上比较复杂,通常兄弟姐妹有共同父母。
  2268. # 简化处理:暂时存为同级关系 (gen_diff=0)
  2269. parent_mid = target_mid
  2270. child_mid = source_mid
  2271. gen_diff = 0
  2272. # 删除旧关系
  2273. cursor.execute("DELETE FROM family_relation_info WHERE source_mid = %s", (source_mid,))
  2274. # 插入新关系
  2275. sql = """
  2276. INSERT INTO family_relation_info
  2277. (parent_mid, child_mid, relation_type, sub_relation_type, source_mid, generation_diff)
  2278. VALUES (%s, %s, %s, %s, %s, %s)
  2279. """
  2280. cursor.execute(sql, (parent_mid, child_mid, rel_type, sub_rel_type, source_mid, gen_diff))
  2281. conn.commit()
  2282. return jsonify({"success": True, "message": "关系已保存"})
  2283. except Exception as e:
  2284. return jsonify({"success": False, "message": str(e)}), 500
  2285. finally:
  2286. conn.close()
  2287. @app.route('/manager/api/members')
  2288. def get_members():
  2289. if 'user_id' not in session:
  2290. return jsonify({"success": False, "message": "Unauthorized"}), 401
  2291. page = int(request.args.get('page', 1))
  2292. search = request.args.get('search', '')
  2293. search_type = request.args.get('search_type', 'name') # name | name_word
  2294. per_page = 10
  2295. offset = (page - 1) * per_page
  2296. conn = get_db_connection()
  2297. try:
  2298. with conn.cursor() as cursor:
  2299. if search and search_type == 'name_word':
  2300. where_clause = "fmi.name_word = %s"
  2301. count_params = (search,)
  2302. data_params = (search, per_page, offset)
  2303. elif search:
  2304. where_clause = "fmi.name LIKE %s OR fmi.simplified_name LIKE %s"
  2305. count_params = (f'%{search}%', f'%{search}%')
  2306. data_params = (f'%{search}%', f'%{search}%', per_page, offset)
  2307. else:
  2308. where_clause = None
  2309. count_params = ()
  2310. data_params = (per_page, offset)
  2311. if where_clause:
  2312. cursor.execute(f"SELECT COUNT(*) as total FROM family_member_info fmi WHERE {where_clause}", count_params)
  2313. else:
  2314. cursor.execute("SELECT COUNT(*) as total FROM family_member_info")
  2315. total_result = cursor.fetchone()
  2316. total = total_result['total'] if total_result else 0
  2317. base_sql = """
  2318. SELECT
  2319. fmi.id, fmi.name, fmi.simplified_name, fmi.sex,
  2320. fmi.name_word, fmi.name_word_generation,
  2321. father.name as father_name, father.simplified_name as father_simplified_name,
  2322. father.name_word_generation as father_generation
  2323. FROM family_member_info fmi
  2324. LEFT JOIN family_relation_info fri ON fmi.id = fri.child_mid AND fri.relation_type IN (1, 2)
  2325. LEFT JOIN family_member_info father ON fri.parent_mid = father.id
  2326. """
  2327. if where_clause:
  2328. cursor.execute(base_sql + f" WHERE {where_clause} LIMIT %s OFFSET %s", data_params)
  2329. else:
  2330. cursor.execute(base_sql + " LIMIT %s OFFSET %s", data_params)
  2331. members = cursor.fetchall()
  2332. members_list = []
  2333. for member in members:
  2334. members_list.append({
  2335. 'id': member['id'],
  2336. 'name': member['name'],
  2337. 'simplified_name': member['simplified_name'],
  2338. 'sex': member['sex'],
  2339. 'name_word': member.get('name_word'),
  2340. 'name_word_generation': member.get('name_word_generation'),
  2341. 'father_name': member.get('father_name'),
  2342. 'father_simplified_name': member.get('father_simplified_name'),
  2343. 'father_generation': member.get('father_generation')
  2344. })
  2345. return jsonify({"success": True, "members": members_list, "total": total})
  2346. except Exception as e:
  2347. return jsonify({"success": False, "message": f"获取成员失败: {e}"}), 500
  2348. finally:
  2349. conn.close()
  2350. def call_doubao_api(prompt, image_url=None):
  2351. """调用豆包API处理文本"""
  2352. api_key = "a1800657-9212-4afe-9b7c-b49f015c54d3"
  2353. api_url = "https://ark.cn-beijing.volces.com/api/v3/responses"
  2354. payload = {
  2355. "model": "doubao-seed-1-8-251228",
  2356. "stream": False,
  2357. "input": [
  2358. {
  2359. "role": "user",
  2360. "content": [
  2361. {"type": "input_text", "text": prompt}
  2362. ]
  2363. }
  2364. ]
  2365. }
  2366. headers = {
  2367. "Authorization": f"Bearer {api_key}",
  2368. "Content-Type": "application/json"
  2369. }
  2370. try:
  2371. response = requests.post(
  2372. api_url,
  2373. json=payload,
  2374. headers=headers,
  2375. timeout=120,
  2376. verify=False,
  2377. proxies={"http": None, "https": None}
  2378. )
  2379. if response.status_code == 200:
  2380. result = response.json()
  2381. print(f"[AI API] Raw response: {result}")
  2382. # 解析响应 - 尝试多种格式
  2383. if 'output' in result:
  2384. for item in result['output']:
  2385. if item.get('type') == 'message':
  2386. content = item.get('content')
  2387. if isinstance(content, str):
  2388. return content
  2389. elif isinstance(content, list):
  2390. for part in content:
  2391. if isinstance(part, dict) and part.get('type') == 'text':
  2392. return part.get('text', '')
  2393. elif isinstance(content, dict) and 'text' in content:
  2394. return content.get('text', '')
  2395. # 尝试其他响应格式
  2396. if 'choices' in result and len(result['choices']) > 0:
  2397. message = result['choices'][0].get('message', {})
  2398. return message.get('content', '')
  2399. # 尝试直接获取文本内容
  2400. if 'text' in result:
  2401. return result['text']
  2402. # 尝试获取响应中的message
  2403. if 'message' in result:
  2404. msg = result['message']
  2405. if isinstance(msg, str):
  2406. return msg
  2407. elif isinstance(msg, dict) and 'content' in msg:
  2408. return msg['content']
  2409. # 返回字符串形式
  2410. return str(result)
  2411. else:
  2412. print(f"[AI API] Error: {response.status_code} - {response.text}")
  2413. return None
  2414. except Exception as e:
  2415. print(f"[AI API] Exception: {e}")
  2416. return None
  2417. def parse_ai_response(ai_response):
  2418. """解析AI响应,提取族谱原文"""
  2419. if not ai_response:
  2420. return None, None
  2421. # 尝试从响应中提取JSON
  2422. try:
  2423. # 移除可能的markdown代码块标记
  2424. text = ai_response.strip()
  2425. if text.startswith('```json'):
  2426. text = text[7:]
  2427. if text.endswith('```'):
  2428. text = text[:-3]
  2429. text = text.strip()
  2430. # 尝试解析JSON
  2431. result = json.loads(text)
  2432. traditional = result.get('genealogy_traditional', '')
  2433. simplified = result.get('genealogy_simplified', '')
  2434. if traditional or simplified:
  2435. return traditional, simplified
  2436. except json.JSONDecodeError:
  2437. print(f"[AI Parse] JSON decode error: {ai_response[:200]}")
  2438. # 如果JSON解析失败,尝试直接提取文本
  2439. # 尝试匹配模式
  2440. import re
  2441. traditional_match = re.search(r'genealogy_traditional["\']?\s*[,:]\s*["\']([^"\']+)["\']', ai_response)
  2442. simplified_match = re.search(r'genealogy_simplified["\']?\s*[,:]\s*["\']([^"\']+)["\']', ai_response)
  2443. traditional = traditional_match.group(1) if traditional_match else ''
  2444. simplified = simplified_match.group(1) if simplified_match else ''
  2445. return traditional, simplified
  2446. @app.route('/manager/api/members/empty_genealogy', methods=['GET'])
  2447. def get_members_empty_genealogy():
  2448. """获取族谱原文为空的成员列表"""
  2449. if 'user_id' not in session:
  2450. return jsonify({"success": False, "message": "Unauthorized"}), 401
  2451. page = int(request.args.get('page', 1))
  2452. per_page = int(request.args.get('per_page', 20))
  2453. offset = (page - 1) * per_page
  2454. conn = get_db_connection()
  2455. try:
  2456. with conn.cursor() as cursor:
  2457. # Count total
  2458. cursor.execute("""
  2459. SELECT COUNT(*) as total
  2460. FROM family_member_info
  2461. WHERE (genealogy_original_traditional IS NULL OR genealogy_original_traditional = '' OR genealogy_original_traditional = 'None')
  2462. AND (genealogy_original_simplified IS NULL OR genealogy_original_simplified = '' OR genealogy_original_simplified = 'None')
  2463. """)
  2464. total_result = cursor.fetchone()
  2465. total = total_result['total'] if total_result else 0
  2466. # Get members
  2467. cursor.execute("""
  2468. SELECT id, name, simplified_name, name_word_generation, sex, occupation, notes, birth_place
  2469. FROM family_member_info
  2470. WHERE (genealogy_original_traditional IS NULL OR genealogy_original_traditional = '' OR genealogy_original_traditional = 'None')
  2471. AND (genealogy_original_simplified IS NULL OR genealogy_original_simplified = '' OR genealogy_original_simplified = 'None')
  2472. LIMIT %s OFFSET %s
  2473. """, (per_page, offset))
  2474. members = cursor.fetchall()
  2475. # 关联查询父亲信息
  2476. member_list = []
  2477. for member in members:
  2478. cursor.execute("""
  2479. SELECT p.name, p.simplified_name, p.name_word_generation
  2480. FROM family_relation_info r
  2481. JOIN family_member_info p ON r.parent_mid = p.id
  2482. WHERE r.child_mid = %s AND r.relation_type = 1
  2483. LIMIT 1
  2484. """, (member['id'],))
  2485. father = cursor.fetchone()
  2486. cursor.execute("""
  2487. SELECT p.name, p.simplified_name
  2488. FROM family_relation_info r
  2489. JOIN family_member_info p ON r.parent_mid = p.id
  2490. WHERE r.child_mid = %s AND r.relation_type = 2
  2491. LIMIT 1
  2492. """, (member['id'],))
  2493. mother = cursor.fetchone()
  2494. member_list.append({
  2495. 'id': member['id'],
  2496. 'name': member['name'],
  2497. 'simplified_name': member['simplified_name'],
  2498. 'name_word_generation': member['name_word_generation'],
  2499. 'sex': member['sex'],
  2500. 'occupation': member['occupation'],
  2501. 'notes': member['notes'],
  2502. 'birth_place': member['birth_place'],
  2503. 'father_name': father['name'] if father else None,
  2504. 'father_simplified_name': father['simplified_name'] if father else None,
  2505. 'father_generation': father['name_word_generation'] if father else None,
  2506. 'mother_name': mother['name'] if mother else None,
  2507. 'mother_simplified_name': mother['simplified_name'] if mother else None
  2508. })
  2509. return jsonify({"success": True, "members": member_list, "total": total})
  2510. except Exception as e:
  2511. return jsonify({"success": False, "message": f"获取成员失败: {e}"}), 500
  2512. finally:
  2513. conn.close()
  2514. @app.route('/manager/api/members/batch_process_genealogy', methods=['POST'])
  2515. def batch_process_genealogy():
  2516. """批量处理成员族谱原文"""
  2517. if 'user_id' not in session:
  2518. return jsonify({"success": False, "message": "Unauthorized"}), 401
  2519. data = request.get_json()
  2520. member_ids = data.get('member_ids', [])
  2521. if not member_ids or len(member_ids) > 10:
  2522. return jsonify({"success": False, "message": "请选择1-10个成员进行处理"}), 400
  2523. conn = get_db_connection()
  2524. results = []
  2525. try:
  2526. for member_id in member_ids:
  2527. with conn.cursor() as cursor:
  2528. cursor.execute("""
  2529. SELECT id, name, simplified_name, name_word_generation,
  2530. birth_place, occupation, notes, sex
  2531. FROM family_member_info WHERE id = %s
  2532. """, (member_id,))
  2533. member = cursor.fetchone()
  2534. # 获取父亲信息
  2535. cursor.execute("""
  2536. SELECT p.name, p.simplified_name
  2537. FROM family_relation_info r
  2538. JOIN family_member_info p ON r.parent_mid = p.id
  2539. WHERE r.child_mid = %s AND r.relation_type = 1
  2540. LIMIT 1
  2541. """, (member_id,))
  2542. father = cursor.fetchone()
  2543. # 获取母亲信息
  2544. cursor.execute("""
  2545. SELECT p.name, p.simplified_name
  2546. FROM family_relation_info r
  2547. JOIN family_member_info p ON r.parent_mid = p.id
  2548. WHERE r.child_mid = %s AND r.relation_type = 2
  2549. LIMIT 1
  2550. """, (member_id,))
  2551. mother = cursor.fetchone()
  2552. member['father_name'] = father['name'] if father else None
  2553. member['father_simplified_name'] = father['simplified_name'] if father else None
  2554. member['mother_name'] = mother['name'] if mother else None
  2555. member['mother_simplified_name'] = mother['simplified_name'] if mother else None
  2556. if not member:
  2557. results.append({"member_id": member_id, "success": False, "message": "成员不存在"})
  2558. continue
  2559. # 构建AI提示词
  2560. member_info = f"""
  2561. 姓名(繁体):{member['name']}
  2562. 姓名(简体):{member['simplified_name'] or '未知'}
  2563. 世系世代:{member['name_word_generation'] or '未知'}
  2564. 父亲姓名:{member['father_name'] or '未知'}
  2565. 母亲姓名:{member['mother_name'] or '未知'}
  2566. 出生地:{member['birth_place'] or '未知'}
  2567. 职业:{member['occupation'] or '未知'}
  2568. 备注:{member['notes'] or '无'}
  2569. """
  2570. prompt = f"""
  2571. 请根据以下人员信息,模拟生成该人员的族谱原文:
  2572. {member_info}
  2573. 请输出两个字段:
  2574. 1. genealogy_traditional: 族谱原文(繁体中文,模仿传统族谱格式)
  2575. 2. genealogy_simplified: 族谱原文(简体中文,将繁体转换为简体)
  2576. 请严格按照JSON格式输出,不要包含任何额外解释:
  2577. {{
  2578. "genealogy_traditional": "繁体族谱原文内容",
  2579. "genealogy_simplified": "简体族谱原文内容"
  2580. }}
  2581. """
  2582. ai_response = call_doubao_api(prompt)
  2583. print(f"[AI Response] Member {member_id}: {ai_response}")
  2584. if ai_response:
  2585. # 使用新的解析函数
  2586. traditional, simplified = parse_ai_response(ai_response)
  2587. if traditional or simplified:
  2588. with conn.cursor() as cursor:
  2589. cursor.execute("""
  2590. UPDATE family_member_info
  2591. SET genealogy_original_traditional = %s,
  2592. genealogy_original_simplified = %s
  2593. WHERE id = %s
  2594. """, (traditional, simplified, member_id))
  2595. conn.commit()
  2596. results.append({
  2597. "member_id": member_id,
  2598. "name": member['name'],
  2599. "success": True,
  2600. "traditional": traditional[:100] + "..." if len(traditional) > 100 else traditional,
  2601. "simplified": simplified[:100] + "..." if len(simplified) > 100 else simplified
  2602. })
  2603. else:
  2604. results.append({
  2605. "member_id": member_id,
  2606. "name": member['name'],
  2607. "success": False,
  2608. "message": "AI未返回有效数据"
  2609. })
  2610. else:
  2611. results.append({
  2612. "member_id": member_id,
  2613. "name": member['name'],
  2614. "success": False,
  2615. "message": "AI调用失败"
  2616. })
  2617. return jsonify({"success": True, "results": results})
  2618. except Exception as e:
  2619. print(f"[Batch Process] Exception: {e}")
  2620. return jsonify({"success": False, "message": f"批量处理失败: {e}"}), 500
  2621. finally:
  2622. conn.close()
  2623. @app.route('/manager/api/member/<int:member_id>')
  2624. def get_member(member_id):
  2625. if 'user_id' not in session:
  2626. return jsonify({"success": False, "message": "Unauthorized"}), 401
  2627. conn = get_db_connection()
  2628. try:
  2629. with conn.cursor() as cursor:
  2630. cursor.execute("SELECT id, name, name_word_generation, source_record_id FROM family_member_info WHERE id = %s", (member_id,))
  2631. member = cursor.fetchone()
  2632. if not member:
  2633. return jsonify({"success": False, "message": "成员不存在"}), 404
  2634. return jsonify({"member": member})
  2635. except Exception as e:
  2636. return jsonify({"success": False, "message": f"获取成员失败: {e}"}), 500
  2637. finally:
  2638. conn.close()
  2639. @app.route('/manager/api/check_relations', methods=['POST'])
  2640. def check_relations():
  2641. if 'user_id' not in session:
  2642. return jsonify({"success": False, "message": "Unauthorized"}), 401
  2643. data = request.json
  2644. people = data.get('people', [])
  2645. if not people:
  2646. return jsonify({"success": False, "matches": {}})
  2647. conn = get_db_connection()
  2648. matches = {}
  2649. try:
  2650. with conn.cursor() as cursor:
  2651. # Collect all father names and spouse names to query
  2652. names_to_check = set()
  2653. for p in people:
  2654. if p.get('father_name'): names_to_check.add(p['father_name'])
  2655. if p.get('spouse_name'): names_to_check.add(p['spouse_name'])
  2656. if not names_to_check:
  2657. return jsonify({"success": True, "matches": {}})
  2658. # Query DB
  2659. format_strings = ','.join(['%s'] * len(names_to_check))
  2660. if names_to_check:
  2661. sql = "SELECT id, name, simplified_name, sex, birthday FROM family_member_info WHERE name IN (%s) OR simplified_name IN (%s)" % (format_strings, format_strings)
  2662. cursor.execute(sql, tuple(names_to_check) * 2)
  2663. results = cursor.fetchall()
  2664. else:
  2665. results = []
  2666. # Organize by name
  2667. db_map = {} # name -> [list of members]
  2668. for r in results:
  2669. # Add under 'name' (Traditional/Old Simplified)
  2670. if r['name'] not in db_map: db_map[r['name']] = []
  2671. db_map[r['name']].append(r)
  2672. # Add under 'simplified_name' if exists
  2673. if r.get('simplified_name'):
  2674. sname = r['simplified_name']
  2675. if sname not in db_map: db_map[sname] = []
  2676. # Avoid duplicates if simplified_name is same as name?
  2677. # The list might contain same object reference, which is fine.
  2678. if sname != r['name']:
  2679. db_map[sname].append(r)
  2680. # Build matches for each input person
  2681. for index, p in enumerate(people):
  2682. p_match = {}
  2683. # Check Father
  2684. fname = p.get('father_name')
  2685. if fname and fname in db_map:
  2686. candidates = db_map[fname]
  2687. # Filter: Father should be Male usually, and older than child (if birthday available)
  2688. valid_fathers = [c for c in candidates if c['sex'] == 1]
  2689. if valid_fathers:
  2690. p_match['father'] = valid_fathers # Return all candidates
  2691. # Check Spouse
  2692. sname = p.get('spouse_name')
  2693. if sname and sname in db_map:
  2694. candidates = db_map[sname]
  2695. # Filter: Spouse usually opposite sex
  2696. target_sex = 1 if p.get('sex') == '女' else 2
  2697. valid_spouses = [c for c in candidates if c['sex'] == target_sex]
  2698. if valid_spouses:
  2699. p_match['spouse'] = valid_spouses
  2700. if p_match:
  2701. matches[index] = p_match
  2702. return jsonify({"success": True, "matches": matches})
  2703. finally:
  2704. conn.close()
  2705. @app.route('/manager/api/upload_reference', methods=['POST'])
  2706. def api_upload_reference():
  2707. """新增成员时上传参考件(无需 member_id)"""
  2708. if 'user_id' not in session:
  2709. return jsonify({"success": False, "message": "未登录"}), 401
  2710. file = request.files.get('file')
  2711. try:
  2712. oss_url, file_name = save_reference_image_to_oss(file)
  2713. username = session.get('username', 'genealogy')
  2714. return jsonify({
  2715. "success": True,
  2716. "oss_url": add_oss_watermark(oss_url, username),
  2717. "oss_url_raw": oss_url,
  2718. "file_name": file_name,
  2719. })
  2720. except ValueError as e:
  2721. return jsonify({"success": False, "message": str(e)}), 400
  2722. except Exception as e:
  2723. print(f"[Upload Reference] Error: {e}")
  2724. return jsonify({"success": False, "message": str(e)}), 500
  2725. @app.route('/manager/api/member/<int:member_id>/reference', methods=['POST', 'DELETE'])
  2726. def api_member_reference(member_id):
  2727. """编辑成员时上传或删除参考件"""
  2728. if 'user_id' not in session:
  2729. return jsonify({"success": False, "message": "未登录"}), 401
  2730. username = session.get('username', 'genealogy')
  2731. conn = get_db_connection()
  2732. try:
  2733. with conn.cursor() as cursor:
  2734. cursor.execute("SELECT id FROM family_member_info WHERE id = %s", (member_id,))
  2735. if not cursor.fetchone():
  2736. return jsonify({"success": False, "message": "成员不存在"}), 404
  2737. if request.method == 'DELETE':
  2738. cursor.execute("""
  2739. UPDATE family_member_info
  2740. SET reference_oss_url = NULL, reference_file_name = NULL,
  2741. reference_upload_time = NULL, reference_upload_uid = NULL
  2742. WHERE id = %s
  2743. """, (member_id,))
  2744. conn.commit()
  2745. return jsonify({"success": True, "message": "参考件已删除"})
  2746. file = request.files.get('file')
  2747. oss_url, file_name = save_reference_image_to_oss(file, member_id=member_id)
  2748. cursor.execute("""
  2749. UPDATE family_member_info
  2750. SET reference_oss_url = %s, reference_file_name = %s,
  2751. reference_upload_time = %s, reference_upload_uid = %s
  2752. WHERE id = %s
  2753. """, (oss_url, file_name, datetime.now(), session['user_id'], member_id))
  2754. conn.commit()
  2755. return jsonify({
  2756. "success": True,
  2757. "message": "参考件上传成功",
  2758. "oss_url": add_oss_watermark(oss_url, username),
  2759. "oss_url_raw": oss_url,
  2760. "file_name": file_name,
  2761. })
  2762. except ValueError as e:
  2763. return jsonify({"success": False, "message": str(e)}), 400
  2764. except Exception as e:
  2765. conn.rollback()
  2766. print(f"[Member Reference] Error: {e}")
  2767. return jsonify({"success": False, "message": str(e)}), 500
  2768. finally:
  2769. conn.close()
  2770. @app.route('/manager/add_member', methods=['GET', 'POST'])
  2771. def add_member():
  2772. if 'user_id' not in session:
  2773. return redirect(url_for('login'))
  2774. # 获取当前登录用户名
  2775. username = session.get('username', 'genealogy')
  2776. conn = get_db_connection()
  2777. try:
  2778. # Check for source_record_id (from GET or POST)
  2779. source_record_id = normalize_source_record_id(
  2780. request.args.get('record_id') or request.form.get('source_record_id')
  2781. )
  2782. prefilled_content = None
  2783. source_oss_url = None
  2784. if source_record_id:
  2785. with conn.cursor() as cursor:
  2786. cursor.execute("SELECT oss_url, ai_content, ai_status FROM genealogy_records WHERE id = %s", (source_record_id,))
  2787. rec = cursor.fetchone()
  2788. if rec:
  2789. source_oss_url = rec['oss_url']
  2790. # Check ai_status (2 = success)
  2791. if rec['ai_status'] == 2 and rec['ai_content']:
  2792. prefilled_content = rec['ai_content']
  2793. if request.method == 'POST':
  2794. # 处理生日转换为 Unix 时间戳
  2795. birthday_str = request.form.get('birthday')
  2796. birthday_ts = 0
  2797. if birthday_str:
  2798. try:
  2799. birthday_ts = int(datetime.strptime(birthday_str, '%Y-%m-%d').timestamp())
  2800. except ValueError:
  2801. birthday_ts = 0
  2802. # 关系数据 - 支持多条关系
  2803. relations = []
  2804. # Parse relations from form data
  2805. i = 0
  2806. while i < 50:
  2807. parent_mid = request.form.get(f'relations[{i}][parent_mid]')
  2808. rel_type = request.form.get(f'relations[{i}][relation_type]')
  2809. if parent_mid is None and rel_type is None:
  2810. break
  2811. i += 1
  2812. if not parent_mid or not rel_type:
  2813. continue
  2814. sub_rel_type = request.form.get(f'relations[{i-1}][sub_relation_type]', '0')
  2815. child_order_raw = request.form.get(f'relations[{i-1}][child_order]', '')
  2816. child_order = int(child_order_raw) if child_order_raw.strip().isdigit() else None
  2817. relations.append({
  2818. 'parent_mid': int(parent_mid),
  2819. 'relation_type': int(rel_type),
  2820. 'sub_relation_type': int(sub_rel_type),
  2821. 'child_order': child_order
  2822. })
  2823. # For backward compatibility, check old-style single relation
  2824. if not relations:
  2825. related_mid = request.form.get('related_mid')
  2826. relation_type = request.form.get('relation_type')
  2827. if related_mid and relation_type:
  2828. child_order_raw = request.form.get('child_order', '')
  2829. child_order = int(child_order_raw) if child_order_raw.strip().isdigit() else None
  2830. relations.append({
  2831. 'parent_mid': int(related_mid),
  2832. 'relation_type': int(relation_type),
  2833. 'sub_relation_type': int(request.form.get('sub_relation_type', '0')),
  2834. 'child_order': child_order
  2835. })
  2836. # 年龄校验逻辑
  2837. for rel in relations:
  2838. if rel['relation_type'] in [1, 2]: # 1:父子 2:母子
  2839. with conn.cursor() as cursor:
  2840. cursor.execute("SELECT name, birthday FROM family_member_info WHERE id = %s", (rel['parent_mid'],))
  2841. parent = cursor.fetchone()
  2842. if parent and parent['birthday'] > 0 and birthday_ts > 0:
  2843. if birthday_ts < parent['birthday']:
  2844. error_msg = f"数据冲突:成员年龄不能比其父亲/母亲({parent['name']})大,请检查并修正出生日期。"
  2845. flash(error_msg)
  2846. # Re-fetch data for rendering
  2847. cursor.execute("SELECT id, name FROM family_member_info ORDER BY name")
  2848. all_members = cursor.fetchall()
  2849. cursor.execute("SELECT * FROM genealogy_records ORDER BY page_number ASC")
  2850. images = cursor.fetchall()
  2851. # 为图片URL添加水印
  2852. for img in images:
  2853. if img.get('oss_url'):
  2854. img['oss_url'] = add_oss_watermark(img['oss_url'], username)
  2855. if request.headers.get('X-Requested-With') == 'XMLHttpRequest' or request.is_json:
  2856. return jsonify({
  2857. "success": False,
  2858. "message": error_msg
  2859. }), 400
  2860. selected_member_name = ''
  2861. return render_template('add_member.html', all_members=all_members, images=images,
  2862. prefilled_content=prefilled_content, source_oss_url=source_oss_url, source_record_id=source_record_id, selected_member_name=selected_member_name)
  2863. break
  2864. # 获取表单数据
  2865. data = {
  2866. 'name': request.form['name'],
  2867. 'simplified_name': request.form.get('simplified_name'),
  2868. 'genealogy_original_traditional': request.form.get('genealogy_original_traditional'),
  2869. 'genealogy_original_simplified': request.form.get('genealogy_original_simplified'),
  2870. 'former_name': request.form.get('former_name'),
  2871. 'childhood_name': request.form.get('childhood_name'),
  2872. 'name_word': request.form.get('name_word'),
  2873. 'name_word_generation': ';'.join([g.strip() for g in request.form.getlist('lineage_generations[]') if g.strip()]),
  2874. 'name_title': request.form.get('name_title'),
  2875. 'sex': request.form['sex'],
  2876. 'birthday': birthday_ts,
  2877. 'is_pass_away': request.form.get('is_pass_away', 0),
  2878. 'marital_status': request.form.get('marital_status', 0),
  2879. 'birth_place': request.form.get('birth_place'),
  2880. 'branch_family_hall': request.form.get('branch_family_hall'),
  2881. 'cluster_place': request.form.get('cluster_place'),
  2882. 'nation': request.form.get('nation'),
  2883. 'residential_address': request.form.get('residential_address'),
  2884. 'phone': request.form.get('phone'),
  2885. 'mail': request.form.get('mail'),
  2886. 'wechat_account': request.form.get('wechat_account'),
  2887. 'id_number': request.form.get('id_number'),
  2888. 'occupation': request.form.get('occupation'),
  2889. 'educational': request.form.get('educational'),
  2890. 'blood_type': request.form.get('blood_type'),
  2891. 'religion': request.form.get('religion'),
  2892. 'hobbies': request.form.get('hobbies'),
  2893. 'personal_achievements': request.form.get('personal_achievements'),
  2894. 'family_rank': request.form.get('family_rank'),
  2895. 'tags': request.form.get('tags'),
  2896. 'notes': request.form.get('notes'),
  2897. 'suspected_error': request.form.get('suspected_error').strip() if request.form.get('suspected_error') else '',
  2898. 'source_record_id': normalize_source_record_id(request.form.get('source_record_id') or None),
  2899. 'create_uid': session['user_id'] # 记录当前操作人
  2900. }
  2901. apply_reference_from_form(data, request.form, session, is_update=False)
  2902. # ... (rest of logic) ...
  2903. with conn.cursor() as cursor:
  2904. print(f"[Add Member] Inserting member data: {data}")
  2905. fields = ", ".join(data.keys())
  2906. placeholders = ", ".join(["%s"] * len(data))
  2907. sql = f"INSERT INTO family_member_info ({fields}) VALUES ({placeholders})"
  2908. print(f"[Add Member] Executing SQL: {sql}")
  2909. print(f"[Add Member] SQL parameters: {list(data.values())}")
  2910. cursor.execute(sql, list(data.values()))
  2911. member_id = cursor.lastrowid
  2912. print(f"[Add Member] Inserted member with ID: {member_id}")
  2913. # 录入关系(支持多条)
  2914. sql_relation = """
  2915. INSERT INTO family_relation_info
  2916. (parent_mid, child_mid, relation_type, sub_relation_type, source_mid, generation_diff, child_order)
  2917. VALUES (%s, %s, %s, %s, %s, %s, %s)
  2918. """
  2919. for rel in relations:
  2920. rel_type = rel['relation_type']
  2921. parent_mid = rel['parent_mid']
  2922. sub_relation_type = rel['sub_relation_type']
  2923. child_order = rel.get('child_order') if rel_type in [1, 2] else None
  2924. gen_diff = 1 if rel_type in [1, 2] else 0
  2925. print(f"[Add Member] Inserting relation: parent_mid={parent_mid}, child_mid={member_id}, relation_type={rel_type}, sub_relation_type={sub_relation_type}, child_order={child_order}")
  2926. cursor.execute(sql_relation, (parent_mid, member_id, rel_type, sub_relation_type, member_id, gen_diff, child_order))
  2927. # Update AI Record Status if applicable
  2928. source_record_id = data.get('source_record_id')
  2929. source_index = request.form.get('source_index')
  2930. if source_record_id and source_index and source_index.isdigit():
  2931. try:
  2932. idx = int(source_index)
  2933. print(f"[Add Member] Updating AI record status: record_id={source_record_id}, index={idx}")
  2934. cursor.execute("SELECT ai_content FROM genealogy_records WHERE id = %s FOR UPDATE", (source_record_id,))
  2935. rec = cursor.fetchone()
  2936. if rec and rec['ai_content']:
  2937. import json
  2938. content = json.loads(rec['ai_content'])
  2939. # Ensure content is a list (it might be a dict if single object, though we try to normalize)
  2940. if isinstance(content, dict):
  2941. content = [content]
  2942. if isinstance(content, list):
  2943. updated = False
  2944. if 0 <= idx < len(content):
  2945. # Always update the status regardless of current value
  2946. content[idx]['is_imported'] = True
  2947. content[idx]['imported_member_id'] = member_id
  2948. updated = True
  2949. if updated:
  2950. new_content = json.dumps(content, ensure_ascii=False)
  2951. cursor.execute("UPDATE genealogy_records SET ai_content = %s WHERE id = %s", (new_content, source_record_id))
  2952. print(f"[Add Member] Updated AI record status")
  2953. except Exception as e:
  2954. print(f"[Add Member] Error updating AI content status: {e}")
  2955. print(f"[Add Member] Committing transaction")
  2956. if safe_commit(conn):
  2957. print(f"[Add Member] Transaction committed successfully")
  2958. # 代数连续性校验
  2959. gen_warns = _check_member_generation(conn, member_id)
  2960. if request.headers.get('X-Requested-With') == 'XMLHttpRequest' or request.is_json:
  2961. return jsonify({"success": True, "message": "成员录入成功", "member_id": member_id,
  2962. "generation_warnings": gen_warns})
  2963. flash('成员录入成功')
  2964. for w in gen_warns:
  2965. flash(w, 'warning')
  2966. return redirect(url_for('members'))
  2967. else:
  2968. print(f"[Add Member] Transaction commit failed!")
  2969. if request.headers.get('X-Requested-With') == 'XMLHttpRequest' or request.is_json:
  2970. return jsonify({"success": False, "message": "成员录入失败,事务提交失败"}), 500
  2971. flash('成员录入失败,事务提交失败')
  2972. return redirect(url_for('add_member'))
  2973. with conn.cursor() as cursor:
  2974. cursor.execute("SELECT id, name FROM family_member_info ORDER BY name")
  2975. all_members = cursor.fetchall()
  2976. cursor.execute("SELECT * FROM genealogy_records ORDER BY page_number ASC")
  2977. images = cursor.fetchall()
  2978. # 为图片URL添加水印
  2979. for img in images:
  2980. if img.get('oss_url'):
  2981. img['oss_url'] = add_oss_watermark(img['oss_url'], username)
  2982. except Exception as e:
  2983. flash(f'发生错误: {e}')
  2984. all_members = []
  2985. images = []
  2986. finally:
  2987. conn.close()
  2988. selected_member_name = ''
  2989. return render_template('add_member.html', all_members=all_members, images=images,
  2990. prefilled_content=prefilled_content, source_oss_url=source_oss_url, source_record_id=source_record_id, selected_member_name=selected_member_name)
  2991. @app.route('/manager/edit_member/<int:member_id>', methods=['GET', 'POST'])
  2992. def edit_member(member_id):
  2993. if 'user_id' not in session:
  2994. return redirect(url_for('login'))
  2995. # 获取当前登录用户名
  2996. username = session.get('username', 'genealogy')
  2997. conn = get_db_connection()
  2998. try:
  2999. if request.method == 'POST':
  3000. birthday_str = request.form.get('birthday')
  3001. birthday_ts = 0
  3002. if birthday_str:
  3003. try:
  3004. birthday_ts = int(datetime.strptime(birthday_str, '%Y-%m-%d').timestamp())
  3005. except ValueError:
  3006. birthday_ts = 0
  3007. # 关系数据 - 支持多条关系
  3008. relations = []
  3009. i = 0
  3010. while i < 50: # 最多 50 条,防止无限循环
  3011. parent_mid = request.form.get(f'relations[{i}][parent_mid]')
  3012. rel_type = request.form.get(f'relations[{i}][relation_type]')
  3013. # 该索引在表单中完全不存在时(非空值,而是键不存在)说明已超出行数,退出
  3014. if parent_mid is None and rel_type is None:
  3015. break
  3016. i += 1
  3017. # parent_mid 或 relation_type 为空(用户清除了该行)则跳过,不入库
  3018. if not parent_mid or not rel_type:
  3019. continue
  3020. sub_rel_type = request.form.get(f'relations[{i-1}][sub_relation_type]', '0')
  3021. child_order_raw = request.form.get(f'relations[{i-1}][child_order]', '')
  3022. child_order = int(child_order_raw) if child_order_raw.strip().isdigit() else None
  3023. relations.append({
  3024. 'parent_mid': int(parent_mid),
  3025. 'relation_type': int(rel_type),
  3026. 'sub_relation_type': int(sub_rel_type),
  3027. 'child_order': child_order,
  3028. })
  3029. # For backward compatibility
  3030. if not relations:
  3031. related_mid = request.form.get('related_mid')
  3032. relation_type = request.form.get('relation_type')
  3033. if related_mid and relation_type:
  3034. child_order_raw = request.form.get('child_order', '')
  3035. relations.append({
  3036. 'parent_mid': int(related_mid),
  3037. 'relation_type': int(relation_type),
  3038. 'sub_relation_type': int(request.form.get('sub_relation_type', '0')),
  3039. 'child_order': int(child_order_raw) if child_order_raw.strip().isdigit() else None,
  3040. })
  3041. # 年龄校验逻辑
  3042. for rel in relations:
  3043. if rel['relation_type'] in [1, 2]:
  3044. with conn.cursor() as cursor:
  3045. cursor.execute("SELECT name, birthday FROM family_member_info WHERE id = %s", (rel['parent_mid'],))
  3046. parent = cursor.fetchone()
  3047. if parent and parent['birthday'] > 0 and birthday_ts > 0:
  3048. if birthday_ts < parent['birthday']:
  3049. flash(f"数据冲突:成员年龄不能比其父亲/母亲({parent['name']})大,请检查并修正出生日期。")
  3050. # 重新加载编辑页所需数据
  3051. cursor.execute("SELECT * FROM family_member_info WHERE id = %s", (member_id,))
  3052. member = cursor.fetchone()
  3053. member['birthday_date'] = birthday_str # 保持用户输入
  3054. cursor.execute("SELECT id, name FROM family_member_info WHERE id != %s ORDER BY name", (member_id,))
  3055. all_members = cursor.fetchall()
  3056. cursor.execute("SELECT * FROM genealogy_records ORDER BY page_number ASC")
  3057. images = cursor.fetchall()
  3058. # 为图片URL添加水印
  3059. for img in images:
  3060. if img.get('oss_url'):
  3061. img['oss_url'] = add_oss_watermark(img['oss_url'], username)
  3062. if request.headers.get('X-Requested-With') == 'XMLHttpRequest' or request.is_json:
  3063. return jsonify({
  3064. "success": False,
  3065. "message": f"数据冲突:成员年龄不能比其父亲/母亲({parent['name']})大,请检查并修正出生日期。"
  3066. }), 400
  3067. selected_member_name = ''
  3068. if member:
  3069. clear_invalid_member_scan_fields(member)
  3070. return render_template('add_member.html', member=member, images=images, all_members=all_members, selected_member_name=selected_member_name, source_record_id=normalize_source_record_id(member.get('source_record_id') if member else None))
  3071. break
  3072. data = {
  3073. 'name': request.form['name'],
  3074. 'simplified_name': request.form.get('simplified_name'),
  3075. 'genealogy_original_traditional': request.form.get('genealogy_original_traditional'),
  3076. 'genealogy_original_simplified': request.form.get('genealogy_original_simplified'),
  3077. 'former_name': request.form.get('former_name'),
  3078. 'childhood_name': request.form.get('childhood_name'),
  3079. 'name_word': request.form.get('name_word'),
  3080. 'name_word_generation': ';'.join([g.strip() for g in request.form.getlist('lineage_generations[]') if g.strip()]),
  3081. 'name_title': request.form.get('name_title'),
  3082. 'sex': request.form['sex'],
  3083. 'birthday': birthday_ts,
  3084. 'is_pass_away': request.form.get('is_pass_away', 0),
  3085. 'marital_status': request.form.get('marital_status', 0),
  3086. 'birth_place': request.form.get('birth_place'),
  3087. 'branch_family_hall': request.form.get('branch_family_hall'),
  3088. 'cluster_place': request.form.get('cluster_place'),
  3089. 'nation': request.form.get('nation'),
  3090. 'residential_address': request.form.get('residential_address'),
  3091. 'phone': request.form.get('phone'),
  3092. 'mail': request.form.get('mail'),
  3093. 'wechat_account': request.form.get('wechat_account'),
  3094. 'id_number': request.form.get('id_number'),
  3095. 'occupation': request.form.get('occupation'),
  3096. 'educational': request.form.get('educational'),
  3097. 'blood_type': request.form.get('blood_type'),
  3098. 'religion': request.form.get('religion'),
  3099. 'hobbies': request.form.get('hobbies'),
  3100. 'personal_achievements': request.form.get('personal_achievements'),
  3101. 'family_rank': request.form.get('family_rank'),
  3102. 'tags': request.form.get('tags'),
  3103. 'notes': request.form.get('notes'),
  3104. 'suspected_error': request.form.get('suspected_error').strip() if request.form.get('suspected_error') else '',
  3105. 'source_record_id': normalize_source_record_id(request.form.get('source_record_id') or None),
  3106. 'create_uid': session['user_id'] # 记录当前操作人
  3107. }
  3108. apply_reference_from_form(data, request.form, session, is_update=True)
  3109. with conn.cursor() as cursor:
  3110. print(f"[Edit Member] Updating member data: {data}")
  3111. update_parts = [f"{k} = %s" for k in data.keys()]
  3112. sql = f"UPDATE family_member_info SET {', '.join(update_parts)} WHERE id = %s"
  3113. print(f"[Edit Member] Executing SQL: {sql}")
  3114. print(f"[Edit Member] SQL parameters: {list(data.values()) + [member_id]}")
  3115. cursor.execute(sql, list(data.values()) + [member_id])
  3116. print(f"[Edit Member] Updated member with ID: {member_id}")
  3117. # 更新关系(支持多条)
  3118. print(f"[Edit Member] Deleting existing relations for member ID: {member_id}")
  3119. cursor.execute("DELETE FROM family_relation_info WHERE source_mid = %s", (member_id,))
  3120. sql_relation = """
  3121. INSERT INTO family_relation_info
  3122. (parent_mid, child_mid, relation_type, sub_relation_type, source_mid, generation_diff, child_order)
  3123. VALUES (%s, %s, %s, %s, %s, %s, %s)
  3124. """
  3125. for rel in relations:
  3126. rel_type = rel['relation_type']
  3127. parent_mid = rel['parent_mid']
  3128. sub_relation_type = rel['sub_relation_type']
  3129. child_order = rel.get('child_order') if rel_type in [1, 2] else None
  3130. gen_diff = 1 if rel_type in [1, 2] else 0
  3131. print(f"[Edit Member] Inserting relation: parent_mid={parent_mid}, child_mid={member_id}, relation_type={rel_type}, sub_relation_type={sub_relation_type}, child_order={child_order}")
  3132. cursor.execute(sql_relation, (parent_mid, member_id, rel_type, sub_relation_type, member_id, gen_diff, child_order))
  3133. # Update AI Record Status if applicable
  3134. source_record_id = data.get('source_record_id')
  3135. source_index = request.form.get('source_index')
  3136. if source_record_id and source_index and source_index.isdigit():
  3137. try:
  3138. idx = int(source_index)
  3139. print(f"[Edit Member] Updating AI record status: record_id={source_record_id}, index={idx}")
  3140. cursor.execute("SELECT ai_content FROM genealogy_records WHERE id = %s FOR UPDATE", (source_record_id,))
  3141. rec = cursor.fetchone()
  3142. if rec and rec['ai_content']:
  3143. import json
  3144. content = json.loads(rec['ai_content'])
  3145. if isinstance(content, dict):
  3146. content = [content]
  3147. if isinstance(content, list):
  3148. updated = False
  3149. if 0 <= idx < len(content):
  3150. # Always update the status regardless of current value
  3151. content[idx]['is_imported'] = True
  3152. content[idx]['imported_member_id'] = member_id
  3153. updated = True
  3154. if updated:
  3155. new_content = json.dumps(content, ensure_ascii=False)
  3156. cursor.execute("UPDATE genealogy_records SET ai_content = %s WHERE id = %s", (new_content, source_record_id))
  3157. print(f"[Edit Member] Updated AI record status")
  3158. except Exception as e:
  3159. print(f"[Edit Member] Error updating AI content status: {e}")
  3160. print(f"[Edit Member] Committing transaction")
  3161. conn.commit()
  3162. print(f"[Edit Member] Transaction committed successfully")
  3163. # 代数连续性校验
  3164. gen_warns = _check_member_generation(conn, member_id)
  3165. if request.headers.get('X-Requested-With') == 'XMLHttpRequest' or request.is_json:
  3166. return jsonify({"success": True, "message": "成员信息更新成功",
  3167. "generation_warnings": gen_warns})
  3168. flash('成员信息更新成功')
  3169. for w in gen_warns:
  3170. flash(w, 'warning')
  3171. return redirect(url_for('members'))
  3172. with conn.cursor() as cursor:
  3173. cursor.execute("SELECT * FROM family_member_info WHERE id = %s", (member_id,))
  3174. member = cursor.fetchone()
  3175. if not member:
  3176. flash('成员不存在')
  3177. return redirect(url_for('members'))
  3178. # 格式化日期供显示
  3179. if member.get('birthday'):
  3180. member['birthday_date'] = format_timestamp(member['birthday'])
  3181. # 获取现有关系(支持多条)
  3182. cursor.execute("SELECT * FROM family_relation_info WHERE source_mid = %s ORDER BY id", (member_id,))
  3183. relations = cursor.fetchall()
  3184. cursor.execute("SELECT id, name FROM family_member_info WHERE id != %s ORDER BY name", (member_id,))
  3185. all_members = cursor.fetchall()
  3186. cursor.execute("SELECT * FROM genealogy_records ORDER BY page_number ASC")
  3187. images = cursor.fetchall()
  3188. # 为图片URL添加水印
  3189. for img in images:
  3190. if img.get('oss_url'):
  3191. img['oss_url'] = add_oss_watermark(img['oss_url'], username)
  3192. if member.get('reference_oss_url'):
  3193. member['reference_image_url'] = add_oss_watermark(member['reference_oss_url'], username)
  3194. finally:
  3195. conn.close()
  3196. # Calculate selected_member_names based on relations
  3197. selected_member_names = []
  3198. if relations:
  3199. for rel in relations:
  3200. if rel.get('parent_mid'):
  3201. for m in all_members:
  3202. if m['id'] == rel['parent_mid']:
  3203. selected_member_names.append(m['name'])
  3204. break
  3205. else:
  3206. selected_member_names.append('')
  3207. else:
  3208. selected_member_names.append('')
  3209. # Get source_record_id from member data
  3210. if member:
  3211. clear_invalid_member_scan_fields(member)
  3212. source_record_id = normalize_source_record_id(member.get('source_record_id') if member else None)
  3213. return render_template('add_member.html', member=member, images=images, all_members=all_members, relations=relations, selected_member_names=selected_member_names, source_record_id=source_record_id)
  3214. @app.route('/manager/member_detail/<int:member_id>')
  3215. def member_detail(member_id):
  3216. if 'user_id' not in session:
  3217. return redirect(url_for('login'))
  3218. # 获取当前登录用户名
  3219. username = session.get('username', 'genealogy')
  3220. conn = get_db_connection()
  3221. try:
  3222. with conn.cursor() as cursor:
  3223. # Join with genealogy_records to get source image info
  3224. sql = """
  3225. SELECT m.*, r.oss_url as source_image_url, r.page_number as source_page,
  3226. r.genealogy_version, r.genealogy_source, r.upload_person
  3227. FROM family_member_info m
  3228. LEFT JOIN genealogy_records r ON m.source_record_id = r.id AND m.source_record_id != %s
  3229. WHERE m.id = %s
  3230. """
  3231. cursor.execute(sql, (INVALID_SOURCE_RECORD_ID, member_id))
  3232. member = cursor.fetchone()
  3233. if not member:
  3234. flash('成员不存在')
  3235. return redirect(url_for('members'))
  3236. clear_invalid_member_scan_fields(member)
  3237. # 为图片URL添加水印
  3238. if member.get('source_image_url'):
  3239. member['source_image_url'] = add_oss_watermark(member['source_image_url'], username)
  3240. if member.get('reference_oss_url'):
  3241. member['reference_image_url'] = add_oss_watermark(member['reference_oss_url'], username)
  3242. member['birthday_str'] = format_timestamp(member.get('birthday'))
  3243. # 获取关系(包含子类型和第几子)
  3244. cursor.execute("""
  3245. SELECT m.id, m.name, m.simplified_name, r.relation_type, r.sub_relation_type, r.child_order
  3246. FROM family_relation_info r
  3247. JOIN family_member_info m ON r.parent_mid = m.id
  3248. WHERE r.child_mid = %s
  3249. """, (member_id,))
  3250. parents = cursor.fetchall()
  3251. cursor.execute("""
  3252. SELECT m.id, m.name, m.simplified_name, r.relation_type, r.sub_relation_type, r.child_order
  3253. FROM family_relation_info r
  3254. JOIN family_member_info m ON r.child_mid = m.id
  3255. WHERE r.parent_mid = %s
  3256. ORDER BY COALESCE(r.child_order, 99999), m.id
  3257. """, (member_id,))
  3258. children = cursor.fetchall()
  3259. # 计算入继说明:若该成员有 sub_relation_type=3(养父母)记录,
  3260. # 则从 sub_relation_type=2(生父母)记录中取排行,生成"由xxx公第N子入继"
  3261. _order_labels = {1:'长', 2:'次', 3:'三', 4:'四', 5:'五',
  3262. 6:'六', 7:'七', 8:'八', 9:'九', 10:'十'}
  3263. adopt_info = None
  3264. is_adopted_in = any(p['sub_relation_type'] == 3 for p in parents)
  3265. if is_adopted_in:
  3266. bio = next((p for p in parents if p['sub_relation_type'] == 2), None)
  3267. if bio:
  3268. bio_name = bio['simplified_name'] or bio['name']
  3269. order = bio['child_order']
  3270. order_str = _order_labels.get(order, f'第{order}') if order else '某'
  3271. adopt_info = f"由{bio_name}公{order_str}子入继"
  3272. finally:
  3273. conn.close()
  3274. return render_template('member_detail.html', member=member, parents=parents,
  3275. children=children, adopt_info=adopt_info)
  3276. @app.route('/manager/delete_member/<int:member_id>', methods=['POST'])
  3277. def delete_member(member_id):
  3278. if 'user_id' not in session:
  3279. return jsonify({"success": False, "message": "Unauthorized"}), 401
  3280. conn = get_db_connection()
  3281. try:
  3282. with conn.cursor() as cursor:
  3283. # 1. 删除关系表中关联该成员的所有记录
  3284. cursor.execute("DELETE FROM family_relation_info WHERE parent_mid = %s OR child_mid = %s OR source_mid = %s",
  3285. (member_id, member_id, member_id))
  3286. # 2. 删除成员本身
  3287. cursor.execute("DELETE FROM family_member_info WHERE id = %s", (member_id,))
  3288. conn.commit()
  3289. flash('成员及其关系已成功删除')
  3290. return redirect(url_for('members'))
  3291. except Exception as e:
  3292. conn.rollback()
  3293. flash(f'删除失败: {e}')
  3294. return redirect(url_for('members'))
  3295. finally:
  3296. conn.close()
  3297. @app.route('/manager/home')
  3298. def home():
  3299. """Home page - Dashboard for the genealogy management system"""
  3300. if 'user_id' not in session:
  3301. return redirect(url_for('login'))
  3302. # Force re-login if is_super_admin not set in session (fresh login required)
  3303. if 'is_super_admin' not in session:
  3304. session.clear()
  3305. flash('请重新登录以获取最新权限')
  3306. return redirect(url_for('login'))
  3307. conn = get_db_connection()
  3308. try:
  3309. with conn.cursor() as cursor:
  3310. # Get member count
  3311. cursor.execute("SELECT COUNT(*) as count FROM family_member_info")
  3312. member_count = cursor.fetchone()['count']
  3313. # Get record count
  3314. cursor.execute("SELECT COUNT(*) as count FROM genealogy_records")
  3315. record_count = cursor.fetchone()['count']
  3316. # Get PDF count
  3317. cursor.execute("SELECT COUNT(*) as count FROM genealogy_pdfs")
  3318. pdf_count = cursor.fetchone()['count']
  3319. # Get suspected error count
  3320. cursor.execute("SELECT COUNT(*) as count FROM family_member_info WHERE suspected_error IS NOT NULL AND TRIM(suspected_error) != ''")
  3321. error_count = cursor.fetchone()['count']
  3322. finally:
  3323. conn.close()
  3324. return render_template('home.html',
  3325. member_count=member_count,
  3326. record_count=record_count,
  3327. pdf_count=pdf_count,
  3328. error_count=error_count)
  3329. @app.route('/manager/login', methods=['GET', 'POST'])
  3330. def login():
  3331. if request.method == 'POST':
  3332. username = request.form['username']
  3333. password = request.form['password']
  3334. try:
  3335. conn = get_db_connection()
  3336. try:
  3337. with conn.cursor() as cursor:
  3338. cursor.execute("SELECT * FROM users WHERE username=%s AND password=%s", (username, password))
  3339. user = cursor.fetchone()
  3340. if user:
  3341. session['user_id'] = user['id']
  3342. session['username'] = user['username']
  3343. session['is_super_admin'] = user.get('is_super_admin', 0) == 1
  3344. return redirect(url_for('home'))
  3345. else:
  3346. flash('用户名或密码错误')
  3347. finally:
  3348. conn.close()
  3349. except Exception as e:
  3350. flash(f'数据库连接错误: {str(e)}')
  3351. print(f'Login error: {str(e)}')
  3352. return render_template('login.html')
  3353. @app.route('/manager/logout')
  3354. def logout():
  3355. session.clear()
  3356. return redirect(url_for('login'))
  3357. @app.route('/manager/api/check_name')
  3358. def check_name():
  3359. if 'user_id' not in session:
  3360. return jsonify({"success": False, "message": "Unauthorized"}), 401
  3361. name = request.args.get('name', '').strip()
  3362. if not name:
  3363. return jsonify({"success": True, "exists": False})
  3364. conn = get_db_connection()
  3365. try:
  3366. with conn.cursor() as cursor:
  3367. # Check for name or simplified_name match
  3368. cursor.execute("SELECT id, name, simplified_name, sex, birthday, is_pass_away FROM family_member_info WHERE name = %s OR simplified_name = %s", (name, name))
  3369. matches = cursor.fetchall()
  3370. if matches:
  3371. # Format birthday for display
  3372. for m in matches:
  3373. if m.get('birthday'):
  3374. m['birthday_str'] = format_timestamp(m['birthday'])
  3375. else:
  3376. m['birthday_str'] = '未知'
  3377. return jsonify({"success": True, "exists": True, "matches": matches})
  3378. else:
  3379. return jsonify({"success": True, "exists": False})
  3380. except Exception as e:
  3381. return jsonify({"success": False, "error": str(e)}), 500
  3382. finally:
  3383. conn.close()
  3384. import requests
  3385. import json
  3386. import re
  3387. @app.route('/manager/api/recognize_image', methods=['POST'])
  3388. def recognize_image():
  3389. if 'user_id' not in session:
  3390. return jsonify({"success": False, "message": "Unauthorized"}), 401
  3391. data = request.json
  3392. image_url = data.get('image_url')
  3393. if not image_url:
  3394. return jsonify({"success": False, "message": "No image URL provided"}), 400
  3395. api_key = "a1800657-9212-4afe-9b7c-b49f015c54d3"
  3396. api_url = "https://ark.cn-beijing.volces.com/api/v3/responses"
  3397. prompt = """
  3398. 请分析这张家谱图片,提取其中关于人物的信息。
  3399. 请务必将繁体字转换为简体字(original_name 字段除外)。
  3400. 特别注意:'name' 字段必须是纯简体中文,不能包含繁体字(例如:'學'应转换为'学','劉'应转换为'刘','萬'应转换为'万')。
  3401. 请提取以下字段(如果存在):
  3402. - original_name: 原始姓名(严格保持图片上的繁体字,不做任何修改或转换)
  3403. - name: 简体姓名(必须转换为简体中文,去除不需要的敬称)
  3404. - sex: 性别(男/女)
  3405. - birthday: 出生日期(尝试转换为YYYY-MM-DD格式,如果无法确定年份可只填月日)
  3406. - death_date: 逝世日期(如文本中出现“殁”、“葬”、“卒”等字眼及其对应的时间,请提取)
  3407. - father_name: 父亲姓名
  3408. - spouse_name: 配偶姓名
  3409. - generation: 第几世/代数
  3410. - name_word: 字辈(例如名字为“学勤公”,“学”为字辈;提取名字中的字辈信息)
  3411. - education: 学历/功名
  3412. - title: 官职/称号
  3413. 请严格以JSON列表格式返回,不要包含Markdown代码块标记(如 ```json ... ```),直接返回JSON数组。
  3414. 如果包含多个人物,请都提取出来。
  3415. """
  3416. ai_payload_url = get_normalized_base64_image(image_url)
  3417. payload = {
  3418. "model": "doubao-seed-1-8-251228",
  3419. "stream": True,
  3420. "input": [
  3421. {
  3422. "role": "user",
  3423. "content": [
  3424. {
  3425. "type": "input_image",
  3426. "image_url": ai_payload_url
  3427. },
  3428. {
  3429. "type": "input_text",
  3430. "text": prompt
  3431. }
  3432. ]
  3433. }
  3434. ]
  3435. }
  3436. headers = {
  3437. "Authorization": f"Bearer {api_key}",
  3438. "Content-Type": "application/json"
  3439. }
  3440. def generate():
  3441. yield "正在连接 AI 服务...\n"
  3442. try:
  3443. # 使用 stream=True, timeout=120
  3444. # 增加 verify=False 以防 SSL 问题(开发环境)
  3445. # 增加 proxies=None 以防本地代理干扰
  3446. with requests.post(
  3447. api_url,
  3448. json=payload,
  3449. headers=headers,
  3450. stream=True,
  3451. timeout=1200,
  3452. verify=False,
  3453. proxies={"http": None, "https": None}
  3454. ) as r:
  3455. if r.status_code != 200:
  3456. yield f"Error: API returned status code {r.status_code}. Response: {r.text}"
  3457. return
  3458. yield "连接成功,正在等待 AI 响应...\n"
  3459. full_reasoning = ""
  3460. json_started = False
  3461. for line in r.iter_lines():
  3462. if line:
  3463. line_str = line.decode('utf-8')
  3464. if line_str.startswith('data: '):
  3465. json_str = line_str[6:]
  3466. if json_str.strip() == '[DONE]':
  3467. break
  3468. try:
  3469. chunk = json.loads(json_str)
  3470. # 处理 standard OpenAI choices format (content)
  3471. if 'choices' in chunk and len(chunk['choices']) > 0:
  3472. delta = chunk['choices'][0].get('delta', {})
  3473. if 'content' in delta:
  3474. if not json_started:
  3475. yield "|||JSON_START|||"
  3476. json_started = True
  3477. yield delta['content']
  3478. # 处理 standard OpenAI choices format (reasoning_content) if any
  3479. if 'reasoning_content' in delta:
  3480. yield f"\n[推理]: {delta['reasoning_content']}"
  3481. # 处理 Doubao/Volcano specific formats
  3482. # Type: response.reasoning_summary_text.delta
  3483. if chunk.get('type') == 'response.reasoning_summary_text.delta':
  3484. if 'delta' in chunk:
  3485. yield chunk['delta']
  3486. # Type: response.text.delta
  3487. if chunk.get('type') == 'response.text.delta':
  3488. if 'delta' in chunk:
  3489. if not json_started:
  3490. yield "|||JSON_START|||"
  3491. json_started = True
  3492. yield chunk['delta']
  3493. # Type: response.output_item.added (May contain initial content or status)
  3494. # Type: response.reasoning_summary_part.added
  3495. except Exception as e:
  3496. print(f"Chunk parse error: {e}")
  3497. else:
  3498. # 尝试直接解析非 data: 开头的行
  3499. try:
  3500. chunk = json.loads(line_str)
  3501. if 'choices' in chunk and len(chunk['choices']) > 0:
  3502. content = chunk['choices'][0]['message']['content']
  3503. yield content
  3504. except:
  3505. pass
  3506. except Exception as e:
  3507. yield f"\n[Error: {str(e)}]"
  3508. return Response(stream_with_context(generate()), mimetype='text/plain')
  3509. @app.route('/manager/api/start_analysis/<int:record_id>', methods=['POST'])
  3510. def start_analysis(record_id):
  3511. if 'user_id' not in session:
  3512. return jsonify({"success": False, "message": "Unauthorized"}), 401
  3513. conn = get_db_connection()
  3514. try:
  3515. with conn.cursor() as cursor:
  3516. # Check if record exists
  3517. cursor.execute("SELECT oss_url, ai_status FROM genealogy_records WHERE id = %s", (record_id,))
  3518. record = cursor.fetchone()
  3519. if not record:
  3520. return jsonify({"success": False, "message": "Record not found"}), 404
  3521. # Update status to processing (1)
  3522. cursor.execute("UPDATE genealogy_records SET ai_status = 1 WHERE id = %s", (record_id,))
  3523. conn.commit()
  3524. # Start background task
  3525. threading.Thread(target=process_ai_task, args=(record_id, record['oss_url'])).start()
  3526. return jsonify({"success": True, "message": "Analysis started"})
  3527. except Exception as e:
  3528. return jsonify({"success": False, "message": str(e)}), 500
  3529. finally:
  3530. conn.close()
  3531. def process_files_background(upload_folder, saved_files, manual_page, suggested_page, genealogy_version, genealogy_source, upload_person):
  3532. current_suggested_page = int(manual_page) if manual_page and str(manual_page).isdigit() else suggested_page
  3533. ensure_pdf_table()
  3534. for item in saved_files:
  3535. if len(item) >= 4:
  3536. filename, file_path, file_page, original_filename = item[0], item[1], item[2], item[3]
  3537. elif len(item) == 3:
  3538. filename, file_path, file_page = item
  3539. original_filename = filename
  3540. else:
  3541. filename, file_path = item[0], item[1]
  3542. file_page = None
  3543. original_filename = filename
  3544. try:
  3545. if filename.lower().endswith('.pdf'):
  3546. import uuid
  3547. display_pdf_name = (original_filename or filename).strip() or filename
  3548. oss_pdf_name = secure_filename(display_pdf_name)
  3549. if not oss_pdf_name or not oss_pdf_name.lower().endswith('.pdf'):
  3550. oss_pdf_name = f"genealogy_pdf_{uuid.uuid4().hex[:8]}.pdf"
  3551. pdf_oss_url = upload_to_oss(file_path, custom_filename=oss_pdf_name)
  3552. if pdf_oss_url:
  3553. desc_parts = []
  3554. if genealogy_version:
  3555. desc_parts.append(genealogy_version)
  3556. if genealogy_source:
  3557. desc_parts.append(genealogy_source)
  3558. pdf_description = ' · '.join(desc_parts) if desc_parts else ''
  3559. conn_pdf = get_db_connection()
  3560. try:
  3561. with conn_pdf.cursor() as cursor:
  3562. cursor.execute(
  3563. "INSERT INTO genealogy_pdfs (file_name, oss_url, description, uploader) VALUES (%s, %s, %s, %s)",
  3564. (display_pdf_name, pdf_oss_url, pdf_description, upload_person or '')
  3565. )
  3566. conn_pdf.commit()
  3567. except Exception as pdf_meta_e:
  3568. print(f"Error inserting genealogy_pdfs for {display_pdf_name}: {pdf_meta_e}")
  3569. finally:
  3570. conn_pdf.close()
  3571. else:
  3572. print(f"Warning: full PDF upload to OSS failed for {filename}, scan pages will still be processed.")
  3573. doc = fitz.open(file_path)
  3574. for page_index in range(len(doc)):
  3575. img_path = None
  3576. try:
  3577. page = doc.load_page(page_index)
  3578. max_dim = max(page.rect.width, page.rect.height)
  3579. zoom = 2000 / max_dim if max_dim > 0 else 2.0
  3580. if zoom > 2.5: zoom = 2.5
  3581. mat = fitz.Matrix(zoom, zoom)
  3582. # Use get_pixmap with matrix directly
  3583. pix = page.get_pixmap(matrix=mat)
  3584. final_page = current_suggested_page
  3585. if genealogy_version and genealogy_source:
  3586. if final_page is not None and str(final_page).strip() != '':
  3587. img_filename = f"{genealogy_version}_{genealogy_source}_{final_page}.jpg"
  3588. else:
  3589. img_filename = f"{genealogy_version}_{genealogy_source}.jpg"
  3590. else:
  3591. img_filename = f"{os.path.splitext(filename)[0]}_page_{page_index+1}.jpg"
  3592. img_path = os.path.join(upload_folder, img_filename)
  3593. # Save the pixmap to the image path
  3594. pix.save(img_path)
  3595. oss_url = upload_to_oss(img_path, custom_filename=img_filename)
  3596. if oss_url:
  3597. conn = get_db_connection()
  3598. try:
  3599. with conn.cursor() as cursor:
  3600. sql = """INSERT INTO genealogy_records
  3601. (file_name, oss_url, page_number, ai_status, genealogy_version, genealogy_source, upload_person, file_type)
  3602. VALUES (%s, %s, %s, 1, %s, %s, %s, %s)"""
  3603. cursor.execute(sql, (img_filename, oss_url, final_page, genealogy_version, genealogy_source, upload_person, 'PDF'))
  3604. record_id = cursor.lastrowid
  3605. conn.commit()
  3606. threading.Thread(target=process_ai_task, args=(record_id, oss_url)).start()
  3607. current_suggested_page += 1
  3608. finally:
  3609. conn.close()
  3610. except Exception as page_e:
  3611. print(f"Error processing page {page_index} of {filename}: {page_e}")
  3612. finally:
  3613. if img_path and os.path.exists(img_path):
  3614. try:
  3615. os.remove(img_path)
  3616. except:
  3617. pass
  3618. doc.close()
  3619. else:
  3620. img_path = compress_image_if_needed(file_path)
  3621. # Use explicitly set page number if provided, otherwise extract from filename or auto-increment
  3622. if file_page and str(file_page).isdigit():
  3623. final_page = int(file_page)
  3624. current_suggested_page = final_page + 1
  3625. page_num = final_page
  3626. else:
  3627. page_num = extract_page_number(img_path)
  3628. final_page = page_num if page_num else current_suggested_page
  3629. ext = os.path.splitext(img_path)[1]
  3630. if genealogy_version and genealogy_source:
  3631. if final_page is not None and str(final_page).strip() != '':
  3632. img_filename = f"{genealogy_version}_{genealogy_source}_{final_page}{ext}"
  3633. else:
  3634. img_filename = f"{genealogy_version}_{genealogy_source}{ext}"
  3635. else:
  3636. img_filename = os.path.basename(img_path)
  3637. oss_url = upload_to_oss(img_path, custom_filename=img_filename)
  3638. if oss_url:
  3639. conn = get_db_connection()
  3640. try:
  3641. with conn.cursor() as cursor:
  3642. sql = """INSERT INTO genealogy_records
  3643. (file_name, oss_url, page_number, ai_status, genealogy_version, genealogy_source, upload_person, file_type)
  3644. VALUES (%s, %s, %s, 1, %s, %s, %s, %s)"""
  3645. cursor.execute(sql, (img_filename, oss_url, final_page, genealogy_version, genealogy_source, upload_person, '图片'))
  3646. record_id = cursor.lastrowid
  3647. conn.commit()
  3648. threading.Thread(target=process_ai_task, args=(record_id, oss_url)).start()
  3649. if page_num:
  3650. current_suggested_page = page_num + 1
  3651. else:
  3652. current_suggested_page += 1
  3653. finally:
  3654. conn.close()
  3655. if img_path and img_path != file_path and os.path.exists(img_path):
  3656. try:
  3657. os.remove(img_path)
  3658. except:
  3659. pass
  3660. except Exception as e:
  3661. print(f"Error processing file {filename}: {e}")
  3662. finally:
  3663. if os.path.exists(file_path):
  3664. try:
  3665. os.remove(file_path)
  3666. except:
  3667. pass
  3668. @app.route('/manager/upload', methods=['GET', 'POST'])
  3669. def upload():
  3670. if 'user_id' not in session:
  3671. return redirect(url_for('login'))
  3672. # 获取建议页码 (当前最大页码 + 1)
  3673. conn = get_db_connection()
  3674. suggested_page = 1
  3675. try:
  3676. with conn.cursor() as cursor:
  3677. cursor.execute("SELECT MAX(page_number) as max_p FROM genealogy_records")
  3678. result = cursor.fetchone()
  3679. if result and result['max_p']:
  3680. suggested_page = result['max_p'] + 1
  3681. finally:
  3682. conn.close()
  3683. if request.method == 'POST':
  3684. if 'file' not in request.files:
  3685. flash('未选择文件')
  3686. return redirect(request.url)
  3687. files = request.files.getlist('file')
  3688. if not files or files[0].filename == '':
  3689. flash('未选择文件')
  3690. return redirect(request.url)
  3691. manual_page = request.form.get('manual_page')
  3692. genealogy_version = request.form.get('genealogy_version', '')
  3693. genealogy_source = request.form.get('genealogy_source', '')
  3694. upload_person = request.form.get('upload_person', '')
  3695. if not upload_person:
  3696. upload_person = session.get('username', '')
  3697. import uuid
  3698. saved_files = []
  3699. for i, file in enumerate(files):
  3700. if not file or not file.filename:
  3701. continue
  3702. original_filename = file.filename
  3703. ext = os.path.splitext(original_filename)[1].lower()
  3704. base_name = secure_filename(original_filename)
  3705. # If secure_filename removes all characters (e.g., pure Chinese name) or just leaves 'pdf'
  3706. if not base_name or base_name == ext.strip('.'):
  3707. filename = f"upload_{uuid.uuid4().hex[:8]}{ext}"
  3708. else:
  3709. # Ensure the extension is preserved
  3710. if not base_name.lower().endswith(ext):
  3711. filename = f"{base_name}{ext}"
  3712. else:
  3713. filename = base_name
  3714. file_path = os.path.join(app.config['UPLOAD_FOLDER'], filename)
  3715. file.save(file_path)
  3716. # Fetch individual page number if it exists
  3717. file_page = request.form.get(f'page_number_{i}')
  3718. saved_files.append((filename, file_path, file_page, original_filename))
  3719. if saved_files:
  3720. threading.Thread(
  3721. target=process_files_background,
  3722. args=(app.config['UPLOAD_FOLDER'], saved_files, manual_page, suggested_page, genealogy_version, genealogy_source, upload_person)
  3723. ).start()
  3724. flash('上传完成,AI解析中,稍后查看')
  3725. time.sleep(1.5)
  3726. return redirect(url_for('index'))
  3727. return render_template('upload.html', suggested_page=suggested_page)
  3728. @app.route('/manager/save_upload', methods=['POST'])
  3729. def save_upload():
  3730. if 'user_id' not in session: return redirect(url_for('login'))
  3731. filename = request.form.get('filename')
  3732. oss_url = request.form.get('oss_url')
  3733. page_number = request.form.get('page_number')
  3734. genealogy_version = request.form.get('genealogy_version', '')
  3735. genealogy_source = request.form.get('genealogy_source', '')
  3736. upload_person = request.form.get('upload_person', session.get('username', ''))
  3737. file_type = request.form.get('file_type', '图片')
  3738. if not oss_url or not page_number:
  3739. flash('页码不能为空')
  3740. return redirect(url_for('upload'))
  3741. conn = get_db_connection()
  3742. try:
  3743. with conn.cursor() as cursor:
  3744. sql = """INSERT INTO genealogy_records
  3745. (file_name, oss_url, page_number, ai_status, genealogy_version, genealogy_source, upload_person, file_type)
  3746. VALUES (%s, %s, %s, 1, %s, %s, %s, %s)"""
  3747. cursor.execute(sql, (filename, oss_url, page_number, genealogy_version, genealogy_source, upload_person, file_type))
  3748. record_id = cursor.lastrowid
  3749. conn.commit()
  3750. # Start AI Task
  3751. threading.Thread(target=process_ai_task, args=(record_id, oss_url)).start()
  3752. flash('上传完成,AI解析中,稍后查看')
  3753. except Exception as e:
  3754. flash(f'保存失败: {e}')
  3755. finally:
  3756. conn.close()
  3757. return redirect(url_for('index'))
  3758. @app.route('/manager/delete_upload/<int:record_id>', methods=['POST'])
  3759. def delete_upload(record_id):
  3760. if 'user_id' not in session:
  3761. return jsonify({"success": False, "message": "Unauthorized"}), 401
  3762. conn = get_db_connection()
  3763. try:
  3764. with conn.cursor() as cursor:
  3765. # 删除记录
  3766. cursor.execute("DELETE FROM genealogy_records WHERE id = %s", (record_id,))
  3767. conn.commit()
  3768. flash('文件记录已成功删除')
  3769. return redirect(url_for('index'))
  3770. except Exception as e:
  3771. conn.rollback()
  3772. flash(f'删除失败: {e}')
  3773. return redirect(url_for('index'))
  3774. finally:
  3775. conn.close()
  3776. @app.route('/manager/upload_pdf', methods=['GET', 'POST'])
  3777. def upload_pdf():
  3778. if 'user_id' not in session:
  3779. return redirect(url_for('login'))
  3780. if request.method == 'GET':
  3781. return render_template('upload_pdf.html')
  3782. # POST请求处理
  3783. if 'file' not in request.files:
  3784. flash('请选择要上传的PDF文件')
  3785. return redirect(request.url)
  3786. file = request.files['file']
  3787. if file.filename == '':
  3788. flash('请选择要上传的PDF文件')
  3789. return redirect(request.url)
  3790. # 检查文件类型
  3791. if not file.filename.lower().endswith('.pdf'):
  3792. flash('只支持PDF文件上传')
  3793. return redirect(request.url)
  3794. # 获取表单数据
  3795. version_name = request.form.get('version_name', '').strip()
  3796. version_source = request.form.get('version_source', '').strip()
  3797. file_provider = request.form.get('file_provider', '').strip()
  3798. # 验证必填字段
  3799. if not version_name:
  3800. flash('版本名称为必填项')
  3801. return redirect(request.url)
  3802. if not version_source:
  3803. flash('版本来源为必填项')
  3804. return redirect(request.url)
  3805. # 如果未提供文件提供人,使用当前登录用户
  3806. if not file_provider:
  3807. file_provider = session.get('user_id', '未知')
  3808. import uuid
  3809. original_filename = file.filename
  3810. ext = os.path.splitext(original_filename)[1].lower()
  3811. base_name = secure_filename(original_filename)
  3812. if not base_name or base_name == ext.strip('.'):
  3813. filename = f"genealogy_pdf_{uuid.uuid4().hex[:8]}{ext}"
  3814. else:
  3815. if not base_name.lower().endswith(ext):
  3816. filename = f"{base_name}{ext}"
  3817. else:
  3818. filename = base_name
  3819. file_path = os.path.join(app.config['UPLOAD_FOLDER'], filename)
  3820. file.save(file_path)
  3821. try:
  3822. # Upload to OSS
  3823. oss_url = upload_to_oss(file_path, custom_filename=filename)
  3824. if not oss_url:
  3825. flash('文件上传失败')
  3826. return redirect(request.url)
  3827. # Save to database
  3828. conn = get_db_connection()
  3829. try:
  3830. with conn.cursor() as cursor:
  3831. cursor.execute(
  3832. "INSERT INTO genealogy_pdfs (file_name, oss_url, version_name, version_source, file_provider, upload_time) VALUES (%s, %s, %s, %s, %s, CURRENT_TIMESTAMP)",
  3833. (original_filename, oss_url, version_name, version_source, file_provider)
  3834. )
  3835. conn.commit()
  3836. flash('PDF文件上传成功')
  3837. return redirect(url_for('pdf_management'))
  3838. except Exception as e:
  3839. flash(f'保存失败: {e}')
  3840. return redirect(request.url)
  3841. finally:
  3842. conn.close()
  3843. finally:
  3844. if os.path.exists(file_path):
  3845. try:
  3846. os.remove(file_path)
  3847. except:
  3848. pass
  3849. def process_pdf_pages(file_path, pdf_oss_url, uploader):
  3850. """Process PDF pages and add them to genealogy records"""
  3851. try:
  3852. import fitz
  3853. doc = fitz.open(file_path)
  3854. # Get current max page number
  3855. conn = get_db_connection()
  3856. suggested_page = 1
  3857. try:
  3858. with conn.cursor() as cursor:
  3859. cursor.execute("SELECT MAX(page_number) as max_p FROM genealogy_records")
  3860. result = cursor.fetchone()
  3861. if result and result['max_p']:
  3862. suggested_page = result['max_p'] + 1
  3863. finally:
  3864. conn.close()
  3865. for page_index in range(len(doc)):
  3866. try:
  3867. page = doc[page_index]
  3868. pix = page.get_pixmap(dpi=150)
  3869. # Save as image
  3870. img_filename = f"{os.path.splitext(os.path.basename(file_path))[0]}_page_{page_index+1}.jpg"
  3871. img_path = os.path.join(app.config['UPLOAD_FOLDER'], img_filename)
  3872. pix.save(img_path)
  3873. # Upload to OSS
  3874. img_oss_url = upload_to_oss(img_path, custom_filename=img_filename)
  3875. if img_oss_url:
  3876. # Save to genealogy_records
  3877. conn = get_db_connection()
  3878. try:
  3879. with conn.cursor() as cursor:
  3880. cursor.execute(
  3881. "INSERT INTO genealogy_records (file_name, oss_url, page_number, ai_status, upload_person, file_type) VALUES (%s, %s, %s, 1, %s, %s)",
  3882. (img_filename, img_oss_url, suggested_page + page_index, uploader, '图片')
  3883. )
  3884. record_id = cursor.lastrowid
  3885. conn.commit()
  3886. # Start AI processing
  3887. threading.Thread(target=process_ai_task, args=(record_id, img_oss_url)).start()
  3888. finally:
  3889. conn.close()
  3890. except Exception as e:
  3891. print(f"Error processing page {page_index+1}: {e}")
  3892. finally:
  3893. if 'img_path' in locals() and os.path.exists(img_path):
  3894. try:
  3895. os.remove(img_path)
  3896. except:
  3897. pass
  3898. except Exception as e:
  3899. print(f"Error processing PDF: {e}")
  3900. # --- Settlement Routes ---
  3901. @app.route('/manager/settlements')
  3902. def settlements():
  3903. if 'user_id' not in session:
  3904. return redirect(url_for('login'))
  3905. return render_template('settlements.html')
  3906. @app.route('/manager/api/settlements', methods=['GET'])
  3907. def get_settlements():
  3908. if 'user_id' not in session:
  3909. return jsonify({"success": False, "message": "Unauthorized"}), 401
  3910. conn = get_db_connection()
  3911. try:
  3912. with conn.cursor() as cursor:
  3913. cursor.execute("""
  3914. SELECT s.*, m.name as representative_name, m.simplified_name as representative_simplified_name
  3915. FROM family_settlements s
  3916. LEFT JOIN family_member_info m ON s.representative_id = m.id
  3917. ORDER BY s.created_at DESC
  3918. """)
  3919. settlements = cursor.fetchall()
  3920. # Convert Decimal to float/int for JSON serialization
  3921. result = []
  3922. for s in settlements:
  3923. item = dict(s)
  3924. if item.get('latitude'):
  3925. item['latitude'] = float(item['latitude'])
  3926. if item.get('longitude'):
  3927. item['longitude'] = float(item['longitude'])
  3928. if item.get('population'):
  3929. item['population'] = int(item['population'])
  3930. result.append(item)
  3931. return jsonify({"success": True, "settlements": result})
  3932. finally:
  3933. conn.close()
  3934. @app.route('/manager/api/settlements/<int:id>', methods=['GET'])
  3935. def get_settlement(id):
  3936. if 'user_id' not in session:
  3937. return jsonify({"success": False, "message": "Unauthorized"}), 401
  3938. conn = get_db_connection()
  3939. try:
  3940. with conn.cursor() as cursor:
  3941. cursor.execute("""
  3942. SELECT s.*, m.name as representative_name, m.simplified_name as representative_simplified_name
  3943. FROM family_settlements s
  3944. LEFT JOIN family_member_info m ON s.representative_id = m.id
  3945. WHERE s.id = %s
  3946. """, (id,))
  3947. settlement = cursor.fetchone()
  3948. if settlement:
  3949. # Convert Decimal to float/int for JSON serialization
  3950. item = dict(settlement)
  3951. if item.get('latitude'):
  3952. item['latitude'] = float(item['latitude'])
  3953. if item.get('longitude'):
  3954. item['longitude'] = float(item['longitude'])
  3955. if item.get('population'):
  3956. item['population'] = int(item['population'])
  3957. return jsonify({"success": True, "settlement": item})
  3958. else:
  3959. return jsonify({"success": False, "message": "聚落不存在"})
  3960. finally:
  3961. conn.close()
  3962. @app.route('/manager/api/settlements', methods=['POST'])
  3963. def add_settlement():
  3964. if 'user_id' not in session:
  3965. return jsonify({"success": False, "message": "Unauthorized"}), 401
  3966. if not session.get('is_super_admin'):
  3967. return jsonify({"success": False, "message": "权限不足"}), 403
  3968. data = request.get_json()
  3969. conn = get_db_connection()
  3970. try:
  3971. with conn.cursor() as cursor:
  3972. cursor.execute("""
  3973. INSERT INTO family_settlements
  3974. (name, region, latitude, longitude, population, representative_id, description, surname_type, new_surname, enthusiastic_members)
  3975. VALUES (%s, %s, %s, %s, %s, %s, %s, %s, %s, %s)
  3976. """, (
  3977. data.get('name'),
  3978. data.get('region'),
  3979. data.get('latitude') or None,
  3980. data.get('longitude') or None,
  3981. data.get('population') or 0,
  3982. data.get('representative_id') or None,
  3983. data.get('description'),
  3984. data.get('surname_type') or 0,
  3985. data.get('new_surname') or None,
  3986. data.get('enthusiastic_members') or None
  3987. ))
  3988. conn.commit()
  3989. return jsonify({"success": True, "message": "添加成功"})
  3990. finally:
  3991. conn.close()
  3992. @app.route('/manager/api/settlements/<int:id>', methods=['PUT'])
  3993. def update_settlement(id):
  3994. if 'user_id' not in session:
  3995. return jsonify({"success": False, "message": "Unauthorized"}), 401
  3996. if not session.get('is_super_admin'):
  3997. return jsonify({"success": False, "message": "权限不足"}), 403
  3998. data = request.get_json()
  3999. conn = get_db_connection()
  4000. try:
  4001. with conn.cursor() as cursor:
  4002. cursor.execute("""
  4003. UPDATE family_settlements
  4004. SET name=%s, region=%s, latitude=%s, longitude=%s,
  4005. population=%s, representative_id=%s, description=%s,
  4006. surname_type=%s, new_surname=%s, enthusiastic_members=%s
  4007. WHERE id=%s
  4008. """, (
  4009. data.get('name'),
  4010. data.get('region'),
  4011. data.get('latitude') or None,
  4012. data.get('longitude') or None,
  4013. data.get('population') or 0,
  4014. data.get('representative_id') or None,
  4015. data.get('description'),
  4016. data.get('surname_type') or 0,
  4017. data.get('new_surname') or None,
  4018. data.get('enthusiastic_members') or None,
  4019. id
  4020. ))
  4021. conn.commit()
  4022. return jsonify({"success": True, "message": "更新成功"})
  4023. finally:
  4024. conn.close()
  4025. @app.route('/manager/api/settlements/<int:id>', methods=['DELETE'])
  4026. def delete_settlement(id):
  4027. if 'user_id' not in session:
  4028. return jsonify({"success": False, "message": "Unauthorized"}), 401
  4029. if not session.get('is_super_admin'):
  4030. return jsonify({"success": False, "message": "权限不足"}), 403
  4031. conn = get_db_connection()
  4032. try:
  4033. with conn.cursor() as cursor:
  4034. cursor.execute("DELETE FROM family_settlements WHERE id=%s", (id,))
  4035. conn.commit()
  4036. return jsonify({"success": True, "message": "删除成功"})
  4037. finally:
  4038. conn.close()
  4039. # 异步批量处理族谱原文功能
  4040. import uuid
  4041. def init_batch_task_table():
  4042. """初始化批量任务表(如果不存在)"""
  4043. conn = get_db_connection()
  4044. try:
  4045. with conn.cursor() as cursor:
  4046. cursor.execute("""
  4047. CREATE TABLE IF NOT EXISTS batch_genealogy_task (
  4048. id INT AUTO_INCREMENT PRIMARY KEY,
  4049. task_id VARCHAR(64) UNIQUE NOT NULL,
  4050. user_id INT NOT NULL,
  4051. status VARCHAR(20) DEFAULT 'pending',
  4052. total_count INT DEFAULT 0,
  4053. completed_count INT DEFAULT 0,
  4054. failed_count INT DEFAULT 0,
  4055. last_processed_id INT DEFAULT 0,
  4056. created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
  4057. updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
  4058. results TEXT
  4059. );
  4060. """)
  4061. # 检查是否存在last_processed_id字段,如果不存在则添加
  4062. cursor.execute("SHOW COLUMNS FROM batch_genealogy_task LIKE 'last_processed_id'")
  4063. if not cursor.fetchone():
  4064. cursor.execute("ALTER TABLE batch_genealogy_task ADD COLUMN last_processed_id INT DEFAULT 0")
  4065. conn.commit()
  4066. print("[Database] batch_genealogy_task table initialized")
  4067. except Exception as e:
  4068. print(f"[Database] Error creating batch_genealogy_task table: {e}")
  4069. finally:
  4070. conn.close()
  4071. # 初始化表
  4072. init_batch_task_table()
  4073. def migrate_child_order_column():
  4074. """为 family_relation_info 表添加 child_order 字段(如不存在)"""
  4075. conn = get_db_connection()
  4076. try:
  4077. with conn.cursor() as cursor:
  4078. cursor.execute("SHOW COLUMNS FROM family_relation_info LIKE 'child_order'")
  4079. if not cursor.fetchone():
  4080. cursor.execute(
  4081. "ALTER TABLE family_relation_info ADD COLUMN child_order INT DEFAULT NULL COMMENT '第几子,用于兄弟排序'"
  4082. )
  4083. conn.commit()
  4084. print("[DB Migrate] Added child_order column to family_relation_info")
  4085. else:
  4086. print("[DB Migrate] child_order column already exists")
  4087. except Exception as e:
  4088. print(f"[DB Migrate] Error adding child_order: {e}")
  4089. finally:
  4090. conn.close()
  4091. migrate_child_order_column()
  4092. def migrate_enthusiastic_members_column():
  4093. """为 family_settlements 表添加 enthusiastic_members 字段(如不存在)"""
  4094. conn = get_db_connection()
  4095. try:
  4096. with conn.cursor() as cursor:
  4097. cursor.execute("SHOW COLUMNS FROM family_settlements LIKE 'enthusiastic_members'")
  4098. if not cursor.fetchone():
  4099. cursor.execute(
  4100. "ALTER TABLE family_settlements ADD COLUMN enthusiastic_members TEXT DEFAULT NULL COMMENT '热心宗亲,多人以逗号分隔'"
  4101. )
  4102. conn.commit()
  4103. print("[DB Migrate] Added enthusiastic_members column to family_settlements")
  4104. else:
  4105. print("[DB Migrate] enthusiastic_members column already exists")
  4106. except Exception as e:
  4107. print(f"[DB Migrate] Error adding enthusiastic_members: {e}")
  4108. finally:
  4109. conn.close()
  4110. migrate_enthusiastic_members_column()
  4111. def migrate_reference_document_columns():
  4112. """为 family_member_info 表添加参考件字段(如不存在)"""
  4113. columns = [
  4114. ("reference_oss_url", "TEXT NULL COMMENT '参考件OSS地址'"),
  4115. ("reference_file_name", "VARCHAR(255) NULL COMMENT '参考件文件名'"),
  4116. ("reference_upload_time", "TIMESTAMP NULL COMMENT '参考件上传时间'"),
  4117. ("reference_upload_uid", "INT NULL COMMENT '参考件上传人ID'"),
  4118. ]
  4119. conn = get_db_connection()
  4120. try:
  4121. with conn.cursor() as cursor:
  4122. for col_name, col_def in columns:
  4123. cursor.execute(f"SHOW COLUMNS FROM family_member_info LIKE '{col_name}'")
  4124. if not cursor.fetchone():
  4125. cursor.execute(f"ALTER TABLE family_member_info ADD COLUMN {col_name} {col_def}")
  4126. print(f"[DB Migrate] Added {col_name} column to family_member_info")
  4127. else:
  4128. print(f"[DB Migrate] {col_name} column already exists")
  4129. conn.commit()
  4130. except Exception as e:
  4131. print(f"[DB Migrate] Error adding reference document columns: {e}")
  4132. finally:
  4133. conn.close()
  4134. migrate_reference_document_columns()
  4135. def async_process_genealogy_task(task_id, member_ids, user_id):
  4136. """异步处理族谱原文任务"""
  4137. results = []
  4138. conn = get_db_connection()
  4139. try:
  4140. # 更新任务状态为处理中
  4141. with conn.cursor() as cursor:
  4142. cursor.execute("""
  4143. UPDATE batch_genealogy_task
  4144. SET status = 'processing', total_count = %s
  4145. WHERE task_id = %s
  4146. """, (len(member_ids), task_id))
  4147. conn.commit()
  4148. completed_count = 0
  4149. failed_count = 0
  4150. for member_id in member_ids:
  4151. try:
  4152. with conn.cursor() as cursor:
  4153. cursor.execute("""
  4154. SELECT id, name, simplified_name, name_word_generation,
  4155. birth_place, occupation, notes, sex
  4156. FROM family_member_info WHERE id = %s
  4157. """, (member_id,))
  4158. member = cursor.fetchone()
  4159. # 获取父亲信息
  4160. cursor.execute("""
  4161. SELECT p.name, p.simplified_name
  4162. FROM family_relation_info r
  4163. JOIN family_member_info p ON r.parent_mid = p.id
  4164. WHERE r.child_mid = %s AND r.relation_type = 1
  4165. LIMIT 1
  4166. """, (member_id,))
  4167. father = cursor.fetchone()
  4168. # 获取母亲信息
  4169. cursor.execute("""
  4170. SELECT p.name, p.simplified_name
  4171. FROM family_relation_info r
  4172. JOIN family_member_info p ON r.parent_mid = p.id
  4173. WHERE r.child_mid = %s AND r.relation_type = 2
  4174. LIMIT 1
  4175. """, (member_id,))
  4176. mother = cursor.fetchone()
  4177. member['father_name'] = father['name'] if father else None
  4178. member['father_simplified_name'] = father['simplified_name'] if father else None
  4179. member['mother_name'] = mother['name'] if mother else None
  4180. member['mother_simplified_name'] = mother['simplified_name'] if mother else None
  4181. except Exception as e:
  4182. print(f"[Async Process] Error getting member {member_id}: {e}")
  4183. results.append({
  4184. "member_id": member_id,
  4185. "name": "未知",
  4186. "success": False,
  4187. "message": f"获取成员信息失败: {e}"
  4188. })
  4189. failed_count += 1
  4190. continue
  4191. if not member:
  4192. results.append({
  4193. "member_id": member_id,
  4194. "name": "未知",
  4195. "success": False,
  4196. "message": "成员不存在"
  4197. })
  4198. failed_count += 1
  4199. continue
  4200. # 构建AI提示词
  4201. member_info = f"""
  4202. 姓名(繁体):{member['name']}
  4203. 姓名(简体):{member['simplified_name'] or '未知'}
  4204. 世系世代:{member['name_word_generation'] or '未知'}
  4205. 父亲姓名:{member['father_name'] or '未知'}
  4206. 母亲姓名:{member['mother_name'] or '未知'}
  4207. 出生地:{member['birth_place'] or '未知'}
  4208. 职业:{member['occupation'] or '未知'}
  4209. 备注:{member['notes'] or '无'}
  4210. """
  4211. prompt = f"""
  4212. 请根据以下人员信息,模拟生成该人员的族谱原文:
  4213. {member_info}
  4214. 请输出两个字段:
  4215. 1. genealogy_traditional: 族谱原文(繁体中文,模仿传统族谱格式)
  4216. 2. genealogy_simplified: 族谱原文(简体中文,将繁体转换为简体)
  4217. 请严格按照JSON格式输出,不要包含任何额外解释:
  4218. {{
  4219. "genealogy_traditional": "繁体族谱原文内容",
  4220. "genealogy_simplified": "简体族谱原文内容"
  4221. }}
  4222. """
  4223. ai_response = call_doubao_api(prompt)
  4224. if ai_response:
  4225. traditional, simplified = parse_ai_response(ai_response)
  4226. if traditional or simplified:
  4227. try:
  4228. with conn.cursor() as cursor:
  4229. cursor.execute("""
  4230. UPDATE family_member_info
  4231. SET genealogy_original_traditional = %s,
  4232. genealogy_original_simplified = %s
  4233. WHERE id = %s
  4234. """, (traditional, simplified, member_id))
  4235. conn.commit()
  4236. results.append({
  4237. "member_id": member_id,
  4238. "name": member['name'],
  4239. "success": True,
  4240. "traditional": traditional[:100] + "..." if len(traditional) > 100 else traditional,
  4241. "simplified": simplified[:100] + "..." if len(simplified) > 100 else simplified
  4242. })
  4243. completed_count += 1
  4244. except Exception as e:
  4245. print(f"[Async Process] Error updating member {member_id}: {e}")
  4246. results.append({
  4247. "member_id": member_id,
  4248. "name": member['name'],
  4249. "success": False,
  4250. "message": f"保存失败: {e}"
  4251. })
  4252. failed_count += 1
  4253. else:
  4254. results.append({
  4255. "member_id": member_id,
  4256. "name": member['name'],
  4257. "success": False,
  4258. "message": "AI未返回有效数据"
  4259. })
  4260. failed_count += 1
  4261. else:
  4262. results.append({
  4263. "member_id": member_id,
  4264. "name": member['name'],
  4265. "success": False,
  4266. "message": "AI调用失败"
  4267. })
  4268. failed_count += 1
  4269. # 更新任务状态
  4270. status = 'completed' if failed_count == 0 else 'completed_with_errors'
  4271. with conn.cursor() as cursor:
  4272. cursor.execute("""
  4273. UPDATE batch_genealogy_task
  4274. SET status = %s, completed_count = %s, failed_count = %s, results = %s
  4275. WHERE task_id = %s
  4276. """, (status, completed_count, failed_count, json.dumps(results, ensure_ascii=False), task_id))
  4277. conn.commit()
  4278. print(f"[Async Process] Task {task_id} completed: {completed_count} success, {failed_count} failed")
  4279. except Exception as e:
  4280. print(f"[Async Process] Error in task {task_id}: {e}")
  4281. with conn.cursor() as cursor:
  4282. cursor.execute("""
  4283. UPDATE batch_genealogy_task
  4284. SET status = 'failed', results = %s
  4285. WHERE task_id = %s
  4286. """, (json.dumps({"error": str(e)}, ensure_ascii=False), task_id))
  4287. conn.commit()
  4288. finally:
  4289. conn.close()
  4290. @app.route('/manager/api/members/batch_process_genealogy_async', methods=['POST'])
  4291. def batch_process_genealogy_async():
  4292. """异步批量处理族谱原文"""
  4293. if 'user_id' not in session:
  4294. return jsonify({"success": False, "message": "Unauthorized"}), 401
  4295. data = request.get_json()
  4296. member_ids = data.get('member_ids', [])
  4297. if not member_ids:
  4298. return jsonify({"success": False, "message": "请选择成员进行处理"}), 400
  4299. # 生成任务ID
  4300. task_id = str(uuid.uuid4())
  4301. # 保存任务到数据库
  4302. conn = get_db_connection()
  4303. try:
  4304. with conn.cursor() as cursor:
  4305. cursor.execute("""
  4306. INSERT INTO batch_genealogy_task (task_id, user_id, status, total_count)
  4307. VALUES (%s, %s, 'pending', %s)
  4308. """, (task_id, session['user_id'], len(member_ids)))
  4309. conn.commit()
  4310. finally:
  4311. conn.close()
  4312. # 启动异步线程处理
  4313. threading.Thread(target=async_process_genealogy_task, args=(task_id, member_ids, session['user_id'])).start()
  4314. return jsonify({
  4315. "success": True,
  4316. "task_id": task_id,
  4317. "message": "任务已创建,正在后台处理中"
  4318. })
  4319. @app.route('/manager/api/members/batch_task_status/<task_id>', methods=['GET'])
  4320. def get_batch_task_status(task_id):
  4321. """获取批量任务状态"""
  4322. if 'user_id' not in session:
  4323. return jsonify({"success": False, "message": "Unauthorized"}), 401
  4324. conn = get_db_connection()
  4325. try:
  4326. with conn.cursor() as cursor:
  4327. cursor.execute("""
  4328. SELECT task_id, status, total_count, completed_count, failed_count,
  4329. created_at, updated_at, results
  4330. FROM batch_genealogy_task
  4331. WHERE task_id = %s AND user_id = %s
  4332. """, (task_id, session['user_id']))
  4333. task = cursor.fetchone()
  4334. if task:
  4335. result = {
  4336. "task_id": task['task_id'],
  4337. "status": task['status'],
  4338. "total_count": task['total_count'],
  4339. "completed_count": task['completed_count'],
  4340. "failed_count": task['failed_count'],
  4341. "created_at": task['created_at'].isoformat() if task['created_at'] else None,
  4342. "updated_at": task['updated_at'].isoformat() if task['updated_at'] else None
  4343. }
  4344. if task['results']:
  4345. try:
  4346. result['results'] = json.loads(task['results'])
  4347. except:
  4348. result['results'] = task['results']
  4349. return jsonify({"success": True, "task": result})
  4350. else:
  4351. return jsonify({"success": False, "message": "任务不存在或无权访问"}), 404
  4352. finally:
  4353. conn.close()
  4354. @app.route('/manager/api/members/batch_tasks', methods=['GET'])
  4355. def get_batch_tasks():
  4356. """获取用户的批量任务列表"""
  4357. if 'user_id' not in session:
  4358. return jsonify({"success": False, "message": "Unauthorized"}), 401
  4359. conn = get_db_connection()
  4360. try:
  4361. with conn.cursor() as cursor:
  4362. cursor.execute("""
  4363. SELECT task_id, status, total_count, completed_count, failed_count,
  4364. last_processed_id, created_at, updated_at
  4365. FROM batch_genealogy_task
  4366. WHERE user_id = %s
  4367. ORDER BY created_at DESC
  4368. LIMIT 20
  4369. """, (session['user_id'],))
  4370. tasks = cursor.fetchall()
  4371. result = []
  4372. for task in tasks:
  4373. result.append({
  4374. "task_id": task['task_id'],
  4375. "status": task['status'],
  4376. "total_count": task['total_count'],
  4377. "completed_count": task['completed_count'],
  4378. "failed_count": task['failed_count'],
  4379. "last_processed_id": task['last_processed_id'],
  4380. "created_at": task['created_at'].isoformat() if task['created_at'] else None,
  4381. "updated_at": task['updated_at'].isoformat() if task['updated_at'] else None
  4382. })
  4383. return jsonify({"success": True, "tasks": result})
  4384. finally:
  4385. conn.close()
  4386. def call_doubao_image_api(image_url, prompt):
  4387. """调用豆包API处理图片,提取文本内容"""
  4388. api_key = "a1800657-9212-4afe-9b7c-b49f015c54d3"
  4389. api_url = "https://ark.cn-beijing.volces.com/api/v3/responses"
  4390. ai_payload_url = get_normalized_base64_image(image_url)
  4391. payload = {
  4392. "model": "doubao-seed-1-8-251228",
  4393. "stream": False,
  4394. "input": [
  4395. {
  4396. "role": "user",
  4397. "content": [
  4398. {"type": "input_image", "image_url": ai_payload_url},
  4399. {"type": "input_text", "text": prompt}
  4400. ]
  4401. }
  4402. ]
  4403. }
  4404. headers = {
  4405. "Authorization": f"Bearer {api_key}",
  4406. "Content-Type": "application/json"
  4407. }
  4408. try:
  4409. response = requests.post(
  4410. api_url,
  4411. json=payload,
  4412. headers=headers,
  4413. timeout=120,
  4414. verify=False,
  4415. proxies={"http": None, "https": None}
  4416. )
  4417. if response.status_code == 200:
  4418. return response.json()
  4419. else:
  4420. print(f"[Image AI API] Error: {response.status_code} - {response.text}")
  4421. return None
  4422. except Exception as e:
  4423. print(f"[Image AI API] Exception: {e}")
  4424. return None
  4425. def extract_pure_text(response):
  4426. """从API响应中提取纯文本内容,优先返回 message 类型的最终答案"""
  4427. if not response:
  4428. return ''
  4429. # 优先从 output 列表中提取 message 类型(最终答案)
  4430. if 'output' in response:
  4431. # 第一遍:只找 message 类型
  4432. for item in response['output']:
  4433. if item.get('type') == 'message':
  4434. content = item.get('content')
  4435. if isinstance(content, str):
  4436. return content
  4437. elif isinstance(content, list):
  4438. text_parts = []
  4439. for part in content:
  4440. if isinstance(part, dict) and part.get('type') == 'text':
  4441. text_parts.append(part.get('text', ''))
  4442. elif isinstance(part, str):
  4443. text_parts.append(part)
  4444. result = ''.join(text_parts)
  4445. if result:
  4446. return result
  4447. # 第二遍:没有 message 时才使用 reasoning 内容作为兜底
  4448. for item in response['output']:
  4449. if item.get('type') == 'reasoning':
  4450. content = item.get('content')
  4451. all_text = ''
  4452. summary = item.get('summary', [])
  4453. for part in summary:
  4454. if isinstance(part, dict):
  4455. if part.get('type') in ('summary_text', 'text'):
  4456. all_text += part.get('text', '')
  4457. elif isinstance(part, str):
  4458. all_text += part
  4459. if isinstance(content, str):
  4460. all_text += content
  4461. elif isinstance(content, list):
  4462. for part in content:
  4463. if isinstance(part, dict) and part.get('type') == 'text':
  4464. all_text += part.get('text', '')
  4465. elif isinstance(part, str):
  4466. all_text += part
  4467. if all_text:
  4468. return all_text
  4469. # 第三遍:content 直接是字符串的情况
  4470. for item in response['output']:
  4471. content = item.get('content')
  4472. if isinstance(content, str) and content:
  4473. return content
  4474. # 尝试从 choices 中提取(兼容 OpenAI 格式)
  4475. if 'choices' in response and len(response['choices']) > 0:
  4476. message = response['choices'][0].get('message', {})
  4477. return message.get('content', '')
  4478. return str(response)
  4479. def build_genealogy_prompt(member_name):
  4480. """
  4481. 构建用于竖排繁体家谱图片 OCR 提取的 Prompt。
  4482. 家谱图片为竖排版式(从上到下、从右到左),每位人物记录通常包含:
  4483. 辈字+名讳、字号、行次、父子关系、配偶(配某氏)、生卒年、葬地、子嗣等。
  4484. """
  4485. return f"""这是一张竖排繁体中文家谱图片。图片文字采用竖排格式,从上到下、从右到左逐列阅读。
  4486. 每位人物的记录通常包含以下内容(不一定全有):
  4487. - 辈字加名讳(如:公諱光元)
  4488. - 字号(如:字維亮)
  4489. - 行次(如:行仁一)
  4490. - 与父亲的关系(如:某某公長子、次子、三子)
  4491. - 配偶(如:配李氏、娶王氏)
  4492. - 生卒年月(如:生於某年某月、卒於某年某月)
  4493. - 葬地(如:葬祖山某向、塟於某地)
  4494. - 子嗣(如:子二:長某某、次某某)
  4495. 任务:找到人物「{member_name}」在图片中的完整记录,将其繁体原文逐字准确复制输出。
  4496. 要求:
  4497. 1. 只输出「{member_name}」这一个人物的记录,不包含其他人的内容
  4498. 2. 保持繁体字原貌,不要转换为简体
  4499. 3. 保留原文中的标点符号
  4500. 4. 不要添加任何解释、标注、序号或额外说明
  4501. 5. 直接输出原文内容"""
  4502. def _extract_from_thinking_output(text):
  4503. """
  4504. 从推理模型的思维链输出中提取最终答案。
  4505. 推理模型(如 doubao-seed 系列)会在 message 内容里写出完整思考过程:
  4506. 反复写候选答案、说"不对"再修正,最后以"现在确认/所以输出这个内容"等结论收尾。
  4507. 本函数的策略:
  4508. 1. 找最后一个"答案引导词 + 冒号"之后的文本(如"准确的原文是:"、"准确复制:")
  4509. 2. 若无引导词,则取"现在确认"/"所以输出这个内容"之前的最后一段文本
  4510. 3. 以上均失败则原文返回
  4511. """
  4512. # 思维链特征词
  4513. THINKING_SIGNALS = ['不对,', '现在确认', '准确复制', '准确的原文是', '正确的输出是', '所以输出这个内容']
  4514. if not any(sig in text for sig in THINKING_SIGNALS):
  4515. return text # 非思维链输出,原样返回
  4516. print(f"[CleanText] Detected thinking-model output, extracting final answer")
  4517. # ---- 策略1:找最后一个答案引导词 ----
  4518. ANSWER_INTRO_PATTERNS = [
  4519. r'准确的原文是[::]\s*',
  4520. r'正确的输出是[::]\s*',
  4521. r'现在准确复制[::]\s*',
  4522. r'准确复制[::]\s*',
  4523. r'应该是[::]\s*',
  4524. r'因此输出[::]\s*',
  4525. r'所以正确.*?是[::]\s*',
  4526. r'原文是[::]\s*',
  4527. r'输出[::]\s*',
  4528. ]
  4529. last_end = -1
  4530. for pattern in ANSWER_INTRO_PATTERNS:
  4531. for m in re.finditer(pattern, text):
  4532. if m.end() > last_end:
  4533. last_end = m.end()
  4534. if last_end >= 0:
  4535. remaining = text[last_end:]
  4536. # 取到第一个"结束标志"前
  4537. END_MARKERS = ['不对', '现在确认', '但是', '然而', '\n\n']
  4538. end_pos = len(remaining)
  4539. for marker in END_MARKERS:
  4540. idx = remaining.find(marker)
  4541. if 0 < idx < end_pos:
  4542. end_pos = idx
  4543. candidate = remaining[:end_pos].strip()
  4544. if len(candidate) >= 5:
  4545. print(f"[CleanText] Extracted via answer-intro pattern: '{candidate[:80]}'")
  4546. return candidate
  4547. # ---- 策略2:取"现在确认"之前的最后一段 ----
  4548. for end_phrase in ['现在确认', '所以输出这个内容', '这就是.*?的完整记录']:
  4549. m = re.search(end_phrase, text)
  4550. if m:
  4551. before = text[:m.start()].rstrip()
  4552. # 找最后一个换行符,取之后的内容
  4553. last_nl = before.rfind('\n')
  4554. candidate = (before[last_nl + 1:] if last_nl >= 0 else before[-400:]).strip()
  4555. if len(candidate) >= 5:
  4556. print(f"[CleanText] Extracted before confirmation phrase: '{candidate[:80]}'")
  4557. return candidate
  4558. return text # 均失败则原样返回
  4559. def _apply_char_whitelist(text):
  4560. """只保留汉字(含扩展A区)和常见中文标点"""
  4561. return re.sub(
  4562. r'[^\u4e00-\u9fff\u3400-\u4dbf\u3000-\u303f\uff00-\uffef,。;:、()【】「」『』〔〕·~—…《》]',
  4563. '', text
  4564. ).strip()
  4565. def clean_genealogy_text(text):
  4566. """
  4567. 清理从 AI 响应中提取的族谱文本。
  4568. - 处理 Markdown/JSON 格式噪声
  4569. - 自动识别思维链推理模型输出,提取最终答案段落
  4570. - 保留中文字符和中文标点,去除英文说明行
  4571. """
  4572. if not text:
  4573. return ''
  4574. text = text.strip()
  4575. # 去除代码块标记
  4576. text = re.sub(r'^```[a-z]*\n?', '', text)
  4577. text = re.sub(r'\n?```$', '', text)
  4578. text = text.strip()
  4579. # 尝试解析 JSON,从已知字段提取
  4580. try:
  4581. result = json.loads(text)
  4582. if isinstance(result, dict):
  4583. for key in ['text', 'content', 'result', 'traditional', 'genealogy_traditional']:
  4584. if key in result:
  4585. text = str(result[key])
  4586. break
  4587. except (json.JSONDecodeError, ValueError):
  4588. pass
  4589. # 针对思维链推理模型输出,提取最终答案(必须在行过滤之前,因为推理文本中含有必要的换行结构)
  4590. text = _extract_from_thinking_output(text)
  4591. # 按行过滤:去除纯英文/数字行、空行及明显解释性前缀行
  4592. lines = text.splitlines()
  4593. kept_lines = []
  4594. for line in lines:
  4595. line = line.strip()
  4596. if not line:
  4597. continue
  4598. non_ascii = sum(1 for c in line if ord(c) > 127)
  4599. if non_ascii == 0:
  4600. continue
  4601. if re.match(r'^(注[::]|说明[::]|Note[::]|备注[::])', line):
  4602. continue
  4603. kept_lines.append(line)
  4604. text = ''.join(kept_lines)
  4605. # 字符白名单:只保留汉字和中文标点
  4606. text = _apply_char_whitelist(text)
  4607. return text
  4608. def async_process_all_empty_genealogy(task_id, user_id):
  4609. """
  4610. 异步批量处理族谱原文为空的成员,支持断点续跑。
  4611. 连接管理原则:DB 连接仅在快速读写期间持有,AI 调用(最长120s)期间
  4612. 不占用任何 DB 连接,避免影响其他用户的正常操作。
  4613. """
  4614. import time
  4615. # ── 1. 读取断点位置,立即释放连接 ──────────────────────────────────────
  4616. conn = get_db_connection()
  4617. try:
  4618. with conn.cursor() as cursor:
  4619. cursor.execute(
  4620. "SELECT last_processed_id FROM batch_genealogy_task WHERE task_id = %s",
  4621. (task_id,)
  4622. )
  4623. task = cursor.fetchone()
  4624. last_processed_id = task['last_processed_id'] if task else 0
  4625. finally:
  4626. conn.close()
  4627. completed_count = 0
  4628. failed_count = 0
  4629. results = []
  4630. while True:
  4631. # ── 2. 取下一条待处理成员(短暂占用连接后立即释放)────────────────
  4632. conn = get_db_connection()
  4633. try:
  4634. with conn.cursor() as cursor:
  4635. cursor.execute("""
  4636. SELECT m.id, m.name, m.name_word_generation, m.source_record_id,
  4637. r.oss_url AS image_url, r.ai_content AS record_ai_content
  4638. FROM family_member_info m
  4639. LEFT JOIN genealogy_records r ON m.source_record_id = r.id
  4640. WHERE (m.genealogy_original_traditional IS NULL
  4641. OR m.genealogy_original_traditional = ''
  4642. OR m.genealogy_original_traditional = 'None')
  4643. AND (m.genealogy_original_simplified IS NULL
  4644. OR m.genealogy_original_simplified = ''
  4645. OR m.genealogy_original_simplified = 'None')
  4646. AND m.id > %s
  4647. ORDER BY m.id ASC
  4648. LIMIT 1
  4649. """, (last_processed_id,))
  4650. member = cursor.fetchone()
  4651. finally:
  4652. conn.close()
  4653. if not member:
  4654. break
  4655. member_id = member['id']
  4656. member_name = member['name']
  4657. image_url = member['image_url']
  4658. record_ai_content = member['record_ai_content']
  4659. print(f"[Batch Process] Processing member {member_id}: {member_name}")
  4660. traditional = ""
  4661. simplified = ""
  4662. extract_source = "basic_info"
  4663. try:
  4664. # ── 3. AI 提取(此阶段不持有任何 DB 连接)────────────────────
  4665. if image_url:
  4666. print(f"[Batch Process] Extracting from image: {image_url}")
  4667. prompt = build_genealogy_prompt(member_name)
  4668. ai_response = call_doubao_image_api(image_url, prompt)
  4669. print(f"[Batch Process] AI response for {member_id}: {str(ai_response)[:300]}")
  4670. if ai_response:
  4671. raw_text = extract_pure_text(ai_response)
  4672. traditional = clean_genealogy_text(raw_text)
  4673. print(f"[Batch Process] Cleaned traditional: {traditional[:100]}")
  4674. name_chars = [c for c in member_name if '\u4e00' <= c <= '\u9fff']
  4675. name_found = any(c in traditional for c in name_chars)
  4676. if traditional and len(traditional) >= 5 and name_found:
  4677. simplified = convert_to_simplified(traditional)
  4678. extract_source = "image"
  4679. print(f"[Batch Process] Image extract OK - trad: {traditional[:80]}")
  4680. else:
  4681. traditional = ""
  4682. simplified = ""
  4683. print(f"[Batch Process] Image extract invalid "
  4684. f"(name_found={name_found}, len={len(traditional)}), resetting")
  4685. # ── 4. 回退:从 record AI content 拼装(内存操作,无需 DB)──
  4686. if not (traditional and simplified) and record_ai_content:
  4687. print(f"[Batch Process] Fallback: trying record AI content")
  4688. try:
  4689. ai_content = json.loads(record_ai_content)
  4690. if isinstance(ai_content, list):
  4691. current_person = None
  4692. for person in ai_content:
  4693. person_name = person.get('original_name', person.get('name', '')).strip()
  4694. if person_name and (
  4695. member_name in person_name or person_name in member_name
  4696. ):
  4697. current_person = person
  4698. break
  4699. if current_person:
  4700. name = current_person.get('original_name',
  4701. current_person.get('name', member_name))
  4702. father_name = current_person.get('father_name', '')
  4703. spouse_name = current_person.get('spouse_name', '')
  4704. generation = current_person.get('generation',
  4705. member['name_word_generation'])
  4706. traditional = f"{name},{father_name}之子" if father_name else name
  4707. if spouse_name:
  4708. traditional += f",配{spouse_name}"
  4709. if generation:
  4710. traditional = f"第{generation}世 " + traditional
  4711. simplified = convert_to_simplified(traditional)
  4712. extract_source = "ai_content"
  4713. print(f"[Batch Process] AI content fallback: {traditional[:80]}")
  4714. else:
  4715. print(f"[Batch Process] No matching person for '{member_name}' in AI content")
  4716. except Exception as e:
  4717. print(f"[Batch Process] Failed to parse record AI content: {e}")
  4718. # ── 5. 最终回退:从关系表查父亲和配偶,短暂占用连接后立即释放 ──
  4719. if not (traditional and simplified):
  4720. print(f"[Batch Process] Fallback: basic info from DB")
  4721. conn = get_db_connection()
  4722. try:
  4723. with conn.cursor() as cursor:
  4724. cursor.execute("""
  4725. SELECT p.name FROM family_relation_info r
  4726. JOIN family_member_info p ON r.parent_mid = p.id
  4727. WHERE r.child_mid = %s AND r.relation_type = 1 LIMIT 1
  4728. """, (member_id,))
  4729. father = cursor.fetchone()
  4730. cursor.execute("""
  4731. SELECT p.name FROM family_relation_info r
  4732. JOIN family_member_info p ON r.parent_mid = p.id
  4733. WHERE r.child_mid = %s AND r.relation_type = 2 LIMIT 1
  4734. """, (member_id,))
  4735. spouse = cursor.fetchone()
  4736. finally:
  4737. conn.close()
  4738. father_name = father['name'] if father else ''
  4739. spouse_name = spouse['name'] if spouse else ''
  4740. generation = member['name_word_generation']
  4741. traditional = f"{member_name},{father_name}之子" if father_name else member_name
  4742. if spouse_name:
  4743. traditional += f",配{spouse_name}"
  4744. if generation:
  4745. traditional = f"第{generation}世 " + traditional
  4746. simplified = convert_to_simplified(traditional)
  4747. extract_source = "basic_info"
  4748. print(f"[Batch Process] Basic info fallback: {traditional[:80]}")
  4749. except Exception as extract_err:
  4750. print(f"[Batch Process] Extraction error for member {member_id}: {extract_err}")
  4751. traditional = ""
  4752. simplified = ""
  4753. # ── 6. 保存结果(短暂占用连接后立即释放)────────────────────────
  4754. last_processed_id = member_id
  4755. conn = get_db_connection()
  4756. try:
  4757. if traditional and simplified:
  4758. with conn.cursor() as cursor:
  4759. cursor.execute("""
  4760. UPDATE family_member_info
  4761. SET genealogy_original_traditional = %s,
  4762. genealogy_original_simplified = %s
  4763. WHERE id = %s
  4764. """, (traditional, simplified, member_id))
  4765. completed_count += 1
  4766. results.append({
  4767. "member_id": member_id,
  4768. "name": member_name,
  4769. "success": True,
  4770. "source": extract_source,
  4771. "traditional_length": len(traditional),
  4772. "simplified_length": len(simplified),
  4773. })
  4774. print(f"[Batch Process] Saved member {member_id} (source={extract_source})")
  4775. else:
  4776. failed_count += 1
  4777. results.append({
  4778. "member_id": member_id,
  4779. "name": member_name,
  4780. "success": False,
  4781. "message": "无法提取或生成族谱原文",
  4782. })
  4783. print(f"[Batch Process] Skipped member {member_id}: no valid text extracted")
  4784. with conn.cursor() as cursor:
  4785. cursor.execute("""
  4786. UPDATE batch_genealogy_task
  4787. SET completed_count = %s,
  4788. failed_count = %s,
  4789. last_processed_id = %s,
  4790. status = 'processing'
  4791. WHERE task_id = %s
  4792. """, (completed_count, failed_count, last_processed_id, task_id))
  4793. conn.commit()
  4794. except Exception as db_err:
  4795. print(f"[Batch Process] DB save error for member {member_id}: {db_err}")
  4796. failed_count += 1
  4797. finally:
  4798. conn.close()
  4799. # 每条处理完后短暂暂停,降低对 AI API 和服务器资源的压力
  4800. time.sleep(0.5)
  4801. # ── 7. 任务完成,写入最终状态 ─────────────────────────────────────────
  4802. conn = get_db_connection()
  4803. try:
  4804. status = 'completed' if failed_count == 0 else 'completed_with_errors'
  4805. with conn.cursor() as cursor:
  4806. cursor.execute("""
  4807. UPDATE batch_genealogy_task
  4808. SET status = %s,
  4809. completed_count = %s,
  4810. failed_count = %s,
  4811. results = %s
  4812. WHERE task_id = %s
  4813. """, (status, completed_count, failed_count,
  4814. json.dumps(results, ensure_ascii=False), task_id))
  4815. conn.commit()
  4816. print(f"[Batch Process] Task {task_id} done: "
  4817. f"{completed_count} success, {failed_count} failed")
  4818. except Exception as e:
  4819. print(f"[Batch Process] Error writing final status for {task_id}: {e}")
  4820. finally:
  4821. conn.close()
  4822. @app.route('/manager/api/members/extract_genealogy/<int:member_id>', methods=['GET'])
  4823. def extract_single_genealogy(member_id):
  4824. """单人员提取族谱原文,核心逻辑与批量处理一致,提取后写入数据库"""
  4825. if 'user_id' not in session:
  4826. return jsonify({"success": False, "message": "Unauthorized"}), 401
  4827. conn = get_db_connection()
  4828. try:
  4829. # 查询成员信息
  4830. with conn.cursor() as cursor:
  4831. cursor.execute("""
  4832. SELECT
  4833. m.id, m.name, m.name_word_generation,
  4834. m.source_record_id, r.oss_url as image_url,
  4835. r.ai_content AS record_ai_content
  4836. FROM family_member_info m
  4837. LEFT JOIN genealogy_records r ON m.source_record_id = r.id
  4838. WHERE m.id = %s
  4839. """, (member_id,))
  4840. row = cursor.fetchone()
  4841. if not row:
  4842. return jsonify({"success": False, "message": "未找到成员"}), 404
  4843. # 处理字典或元组格式的返回
  4844. if isinstance(row, dict):
  4845. member = row
  4846. else:
  4847. member = {
  4848. 'id': row[0],
  4849. 'name': row[1],
  4850. 'name_word_generation': row[2],
  4851. 'source_record_id': row[3],
  4852. 'image_url': row[4],
  4853. 'record_ai_content': row[5]
  4854. }
  4855. # 调试:打印查询结果
  4856. print(f"[Single Extract] Query result - id: {member['id']}, name: '{member['name']}', name_word_generation: '{member['name_word_generation']}', source_record_id: {member['source_record_id']}, image_url: '{member['image_url']}', record_ai_content: '{member['record_ai_content'][:50] if member['record_ai_content'] else None}'")
  4857. traditional = ""
  4858. simplified = ""
  4859. source = "basic_info"
  4860. image_url = member['image_url']
  4861. record_ai_content = member['record_ai_content']
  4862. print(f"[Single Extract] Processing member {member_id}: {member['name']}")
  4863. # 优先从关联图片中提取族谱原文
  4864. if image_url:
  4865. print(f"[Single Extract] Extracting from image: {image_url}")
  4866. member_name = member['name']
  4867. prompt = build_genealogy_prompt(member_name)
  4868. ai_response = call_doubao_image_api(image_url, prompt)
  4869. print(f"[Single Extract] AI response: {str(ai_response)[:500]}")
  4870. if ai_response:
  4871. raw_text = extract_pure_text(ai_response)
  4872. print(f"[Single Extract] Raw text from response: '{raw_text[:300]}'")
  4873. traditional = clean_genealogy_text(raw_text)
  4874. print(f"[Single Extract] Cleaned traditional: '{traditional[:200]}', length: {len(traditional)}")
  4875. # 验证提取结果是否包含该人物的姓名(至少包含名字中的一个字)
  4876. name_chars = [c for c in member_name if '\u4e00' <= c <= '\u9fff']
  4877. name_found = any(c in traditional for c in name_chars)
  4878. if traditional and len(traditional) >= 5 and name_found:
  4879. simplified = convert_to_simplified(traditional)
  4880. source = "image"
  4881. print(f"[Single Extract] Extracted from image - traditional: {traditional[:100]}, simplified: {simplified[:100]}")
  4882. else:
  4883. traditional = ""
  4884. simplified = ""
  4885. if not name_found:
  4886. print(f"[Single Extract] Extracted text does not contain name '{member_name}', resetting")
  4887. else:
  4888. print(f"[Single Extract] Image extraction too short ({len(traditional)} chars), resetting")
  4889. else:
  4890. print(f"[Single Extract] AI response is None or empty")
  4891. else:
  4892. print(f"[Single Extract] No image URL found for member {member_id}")
  4893. # 如果从图片提取失败或没有图片,尝试从已有的AI解析内容中提取
  4894. if not (traditional and simplified) and record_ai_content:
  4895. print(f"[Single Extract] Trying to extract from record AI content")
  4896. try:
  4897. ai_content = json.loads(record_ai_content)
  4898. if isinstance(ai_content, list) and len(ai_content) > 0:
  4899. current_person = None
  4900. member_name = member['name']
  4901. for person in ai_content:
  4902. person_name = person.get('original_name', person.get('name', '')).strip()
  4903. if person_name and (member_name in person_name or person_name in member_name):
  4904. current_person = person
  4905. break
  4906. if current_person:
  4907. name = current_person.get('original_name', current_person.get('name', member['name']))
  4908. father_name = current_person.get('father_name', '')
  4909. spouse_name = current_person.get('spouse_name', '')
  4910. generation = current_person.get('generation', member['name_word_generation'])
  4911. traditional = f"{name},{father_name}之子"
  4912. if spouse_name:
  4913. traditional += f",配{spouse_name}"
  4914. if generation:
  4915. traditional = f"第{generation}世 " + traditional
  4916. simplified = convert_to_simplified(traditional)
  4917. source = "ai_content"
  4918. print(f"[Single Extract] Generated from AI content: {traditional}")
  4919. except Exception as e:
  4920. print(f"[Single Extract] Failed to parse record AI content: {e}")
  4921. # 如果还是没有内容,使用基本信息生成(标注来源为 basic_info)
  4922. if not (traditional and simplified):
  4923. print(f"[Single Extract] Generating from basic info")
  4924. with conn.cursor() as cursor:
  4925. cursor.execute("""
  4926. SELECT p.name, p.simplified_name
  4927. FROM family_relation_info r
  4928. JOIN family_member_info p ON r.parent_mid = p.id
  4929. WHERE r.child_mid = %s AND r.relation_type = 1
  4930. LIMIT 1
  4931. """, (member_id,))
  4932. father_row = cursor.fetchone()
  4933. father_name = father_row[0] if father_row else ''
  4934. cursor.execute("""
  4935. SELECT p.name, p.simplified_name
  4936. FROM family_relation_info r
  4937. JOIN family_member_info p ON r.parent_mid = p.id
  4938. WHERE r.child_mid = %s AND r.relation_type = 2
  4939. LIMIT 1
  4940. """, (member_id,))
  4941. spouse_row = cursor.fetchone()
  4942. spouse_name = spouse_row[0] if spouse_row else ''
  4943. generation = member['name_word_generation']
  4944. name = member['name']
  4945. traditional = f"{name},{father_name}之子" if father_name else name
  4946. if spouse_name:
  4947. traditional += f",配{spouse_name}"
  4948. if generation:
  4949. traditional = f"第{generation}世 " + traditional
  4950. simplified = convert_to_simplified(traditional)
  4951. source = "basic_info"
  4952. print(f"[Single Extract] Generated from basic info: {traditional}")
  4953. # 调试:打印最终结果
  4954. print(f"[Single Extract] Final result - traditional: '{traditional}', simplified: '{simplified}'")
  4955. # 写入数据库
  4956. if traditional and simplified:
  4957. with conn.cursor() as cursor:
  4958. cursor.execute("""
  4959. UPDATE family_member_info
  4960. SET genealogy_original_traditional = %s,
  4961. genealogy_original_simplified = %s
  4962. WHERE id = %s
  4963. """, (traditional, simplified, member_id))
  4964. conn.commit()
  4965. print(f"[Single Extract] Successfully saved to database")
  4966. return jsonify({
  4967. "success": True,
  4968. "member_id": member_id,
  4969. "name": member['name'],
  4970. "genealogy_traditional": traditional,
  4971. "genealogy_simplified": simplified,
  4972. "source": source
  4973. })
  4974. else:
  4975. return jsonify({
  4976. "success": False,
  4977. "member_id": member_id,
  4978. "message": "无法提取或生成族谱原文"
  4979. })
  4980. except Exception as e:
  4981. import traceback
  4982. print(f"[Single Extract] Error: {e}")
  4983. print(f"[Single Extract] Traceback: {traceback.format_exc()}")
  4984. return jsonify({
  4985. "success": False,
  4986. "member_id": member_id,
  4987. "message": str(e),
  4988. "error_type": type(e).__name__
  4989. })
  4990. finally:
  4991. conn.close()
  4992. @app.route('/manager/api/members/batch_resume_task', methods=['GET'])
  4993. def batch_resume_task():
  4994. """
  4995. 恢复因服务重启而中断的批量任务(GET,方便浏览器直接访问)。
  4996. 可选参数:?task_id=xxx 不传则自动找最近一条中断任务。
  4997. """
  4998. if 'user_id' not in session:
  4999. return jsonify({"success": False, "message": "Unauthorized"}), 401
  5000. task_id = request.args.get('task_id')
  5001. conn = get_db_connection()
  5002. try:
  5003. with conn.cursor() as cursor:
  5004. if task_id:
  5005. cursor.execute("""
  5006. SELECT task_id, status, last_processed_id, total_count, completed_count, failed_count
  5007. FROM batch_genealogy_task
  5008. WHERE task_id = %s AND user_id = %s
  5009. """, (task_id, session['user_id']))
  5010. else:
  5011. # 找最近一条中断的任务
  5012. cursor.execute("""
  5013. SELECT task_id, status, last_processed_id, total_count, completed_count, failed_count
  5014. FROM batch_genealogy_task
  5015. WHERE user_id = %s AND status IN ('pending', 'processing', 'interrupted')
  5016. ORDER BY created_at DESC
  5017. LIMIT 1
  5018. """, (session['user_id'],))
  5019. task = cursor.fetchone()
  5020. if not task:
  5021. return jsonify({"success": False, "message": "未找到可恢复的任务"}), 404
  5022. task_id = task['task_id']
  5023. # 重新标记为 processing,准备恢复线程
  5024. with conn.cursor() as cursor:
  5025. cursor.execute("""
  5026. UPDATE batch_genealogy_task
  5027. SET status = 'processing'
  5028. WHERE task_id = %s
  5029. """, (task_id,))
  5030. conn.commit()
  5031. threading.Thread(
  5032. target=async_process_all_empty_genealogy,
  5033. args=(task_id, session['user_id']),
  5034. daemon=True
  5035. ).start()
  5036. return jsonify({
  5037. "success": True,
  5038. "task_id": task_id,
  5039. "message": f"任务已从断点恢复(已完成 {task['completed_count']},从 last_processed_id={task['last_processed_id']} 继续)",
  5040. "last_processed_id": task['last_processed_id'],
  5041. "completed_count": task['completed_count'],
  5042. "total_count": task['total_count'],
  5043. })
  5044. finally:
  5045. conn.close()
  5046. @app.route('/manager/api/members/batch_process_all_empty', methods=['GET'])
  5047. def batch_process_all_empty():
  5048. """简便批量处理接口:自动处理所有族谱原文为空的成员,支持断点续跑"""
  5049. if 'user_id' not in session:
  5050. return jsonify({"success": False, "message": "Unauthorized"}), 401
  5051. conn = get_db_connection()
  5052. try:
  5053. with conn.cursor() as cursor:
  5054. cursor.execute("""
  5055. SELECT COUNT(*) as count
  5056. FROM family_member_info
  5057. WHERE (genealogy_original_traditional IS NULL OR genealogy_original_traditional = '' OR genealogy_original_traditional = 'None')
  5058. AND (genealogy_original_simplified IS NULL OR genealogy_original_simplified = '' OR genealogy_original_simplified = 'None')
  5059. """)
  5060. result = cursor.fetchone()
  5061. total_empty = result['count'] if result else 0
  5062. cursor.execute("""
  5063. SELECT task_id, status, last_processed_id, total_count, completed_count, failed_count
  5064. FROM batch_genealogy_task
  5065. WHERE user_id = %s AND status IN ('pending', 'processing')
  5066. ORDER BY created_at DESC
  5067. LIMIT 1
  5068. """, (session['user_id'],))
  5069. running_task = cursor.fetchone()
  5070. if running_task:
  5071. return jsonify({
  5072. "success": False,
  5073. "message": "存在正在进行的任务,若服务已重启可调用 POST /manager/api/members/batch_resume_task 恢复",
  5074. "task_id": running_task['task_id'],
  5075. "status": running_task['status'],
  5076. "last_processed_id": running_task['last_processed_id'],
  5077. "completed_count": running_task['completed_count'],
  5078. "total_count": running_task['total_count'],
  5079. "resume_tip": "POST /manager/api/members/batch_resume_task body: {\"task_id\": \"" + running_task['task_id'] + "\"}"
  5080. })
  5081. task_id = str(uuid.uuid4())
  5082. with conn.cursor() as cursor:
  5083. cursor.execute("""
  5084. INSERT INTO batch_genealogy_task (task_id, user_id, status, total_count, last_processed_id)
  5085. VALUES (%s, %s, 'processing', %s, 0)
  5086. """, (task_id, session['user_id'], total_empty))
  5087. conn.commit()
  5088. threading.Thread(
  5089. target=async_process_all_empty_genealogy,
  5090. args=(task_id, session['user_id']),
  5091. daemon=True
  5092. ).start()
  5093. return jsonify({
  5094. "success": True,
  5095. "task_id": task_id,
  5096. "message": f"任务已创建,将处理 {total_empty} 个族谱原文为空的成员",
  5097. "total_count": total_empty
  5098. })
  5099. finally:
  5100. conn.close()
  5101. # ==================== 微信小程序 API 接口 ====================
  5102. @app.route('/manager/api/wechat/login', methods=['POST'])
  5103. def api_wechat_login():
  5104. """微信小程序登录接口(正式流程)"""
  5105. import time
  5106. start_time = time.time()
  5107. try:
  5108. data = request.get_json()
  5109. if not data:
  5110. print(f"[API Wechat Login] Error: No request data")
  5111. return jsonify({"success": False, "message": "请求数据为空"}), 400
  5112. code = data.get('code', '')
  5113. encrypted_data = data.get('encryptedData', '')
  5114. iv = data.get('iv', '')
  5115. phone_code = data.get('phoneCode', '')
  5116. if not code:
  5117. print(f"[API Wechat Login] Error: Missing code parameter")
  5118. return jsonify({"success": False, "message": "缺少code参数"}), 400
  5119. print(f"[API Wechat Login] Received login request, code: {code[:10]}..., phoneCode: {phone_code[:10]}...")
  5120. # 1. 使用code获取session_key和openid
  5121. session_url = "https://api.weixin.qq.com/sns/jscode2session"
  5122. session_params = {
  5123. "appid": WECHAT_APP_ID,
  5124. "secret": WECHAT_APP_SECRET,
  5125. "js_code": code,
  5126. "grant_type": "authorization_code"
  5127. }
  5128. try:
  5129. session_response = requests.get(session_url, params=session_params, timeout=15)
  5130. session_response.raise_for_status()
  5131. except requests.exceptions.RequestException as e:
  5132. print(f"[WeChat Login] Session request failed: {e}")
  5133. return jsonify({"success": False, "message": f"网络请求失败: {str(e)}"}), 500
  5134. session_data = session_response.json()
  5135. print(f"[WeChat Login] Session response: {session_data}")
  5136. if 'errcode' in session_data and session_data['errcode'] != 0:
  5137. print(f"[WeChat Login] Session error: {session_data}")
  5138. return jsonify({"success": False, "message": session_data.get('errmsg', '登录失败')}), 400
  5139. openid = session_data.get('openid')
  5140. session_key = session_data.get('session_key')
  5141. if not openid:
  5142. print(f"[WeChat Login] Error: openid is empty")
  5143. return jsonify({"success": False, "message": "获取openid失败"}), 400
  5144. # 2. 获取手机号(支持两种方式)
  5145. phone = None
  5146. # 方式一:使用phoneCode调用官方接口(推荐)
  5147. if phone_code:
  5148. print(f"[WeChat Phone] Trying to get phone via phoneCode")
  5149. try:
  5150. access_token = get_wechat_access_token()
  5151. if access_token:
  5152. phone_url = f"https://api.weixin.qq.com/wxa/business/getuserphonenumber?access_token={access_token}"
  5153. phone_response = requests.post(phone_url, json={"code": phone_code}, timeout=15)
  5154. phone_response.raise_for_status()
  5155. phone_result = phone_response.json()
  5156. print(f"[WeChat Phone] Phone API response: {phone_result}")
  5157. if phone_result.get('errcode') == 0 and phone_result.get('phone_info'):
  5158. phone = phone_result['phone_info'].get('phoneNumber')
  5159. print(f"[WeChat Phone] Phone obtained via phoneCode: {phone}")
  5160. else:
  5161. print(f"[WeChat Phone] Failed to get phone via phoneCode: {phone_result}")
  5162. else:
  5163. print(f"[WeChat Phone] Failed to get access_token")
  5164. except requests.exceptions.RequestException as e:
  5165. print(f"[WeChat Phone] Phone request failed: {e}")
  5166. # 方式二:使用encryptedData解密(兼容旧方式)
  5167. if not phone and encrypted_data and iv and session_key:
  5168. print(f"[WeChat Phone] Trying to decrypt phone via encryptedData")
  5169. phone_data = decrypt_wechat_phone(encrypted_data, iv, session_key)
  5170. if phone_data and 'phoneNumber' in phone_data:
  5171. phone = phone_data['phoneNumber']
  5172. print(f"[WeChat Phone] Phone obtained via decryption: {phone}")
  5173. # 3. 创建或获取小程序用户(使用mp_users表)
  5174. conn = get_db_connection()
  5175. try:
  5176. with conn.cursor() as cursor:
  5177. cursor.execute("SELECT id, phone FROM mp_users WHERE openid = %s", (openid,))
  5178. mp_user = cursor.fetchone()
  5179. if mp_user:
  5180. update_fields = []
  5181. update_params = []
  5182. if phone and phone != mp_user.get('phone'):
  5183. update_fields.append("phone = %s")
  5184. update_params.append(phone)
  5185. update_fields.append("last_login_at = CURRENT_TIMESTAMP")
  5186. update_params.append(openid)
  5187. if update_fields:
  5188. sql = f"UPDATE mp_users SET {', '.join(update_fields)} WHERE openid = %s"
  5189. cursor.execute(sql, update_params)
  5190. conn.commit()
  5191. user_id = mp_user['id']
  5192. else:
  5193. cursor.execute("""
  5194. INSERT INTO mp_users (openid, phone, created_at, updated_at, last_login_at)
  5195. VALUES (%s, %s, CURRENT_TIMESTAMP, CURRENT_TIMESTAMP, CURRENT_TIMESTAMP)
  5196. """, (openid, phone))
  5197. conn.commit()
  5198. user_id = cursor.lastrowid
  5199. print(f"[WeChat Login] Created new user: {user_id}, openid: {openid[:10]}...")
  5200. finally:
  5201. conn.close()
  5202. import uuid
  5203. token = str(uuid.uuid4())
  5204. # 持久化 token,用于后续接口识别用户身份
  5205. conn2 = get_db_connection()
  5206. try:
  5207. with conn2.cursor() as cursor2:
  5208. cursor2.execute("UPDATE mp_users SET token = %s WHERE id = %s", (token, user_id))
  5209. conn2.commit()
  5210. finally:
  5211. conn2.close()
  5212. elapsed = time.time() - start_time
  5213. print(f"[API Wechat Login] Success, elapsed: {elapsed:.2f}s, user_id: {user_id}, phone: {phone}")
  5214. return jsonify({
  5215. "success": True,
  5216. "token": token,
  5217. "user": {
  5218. "id": user_id,
  5219. "openid": openid,
  5220. "phone": phone,
  5221. "login_type": "wechat_mp"
  5222. }
  5223. })
  5224. except Exception as e:
  5225. elapsed = time.time() - start_time
  5226. print(f"[API Wechat Login] Error: {e}, elapsed: {elapsed:.2f}s")
  5227. return jsonify({"success": False, "message": str(e)}), 500
  5228. @app.route('/manager/api/members/search', methods=['GET'])
  5229. def api_search_members():
  5230. """搜索成员(小程序用)"""
  5231. keyword = request.args.get('keyword', '')
  5232. token = request.headers.get('Authorization', '').replace('Bearer ', '')
  5233. if not token:
  5234. return jsonify({"success": False, "message": "未登录"}), 401
  5235. conn = get_db_connection()
  5236. try:
  5237. with conn.cursor() as cursor:
  5238. base_sql = """
  5239. SELECT
  5240. m.id, m.name, m.simplified_name, m.name_word_generation,
  5241. m.sex, m.birthday, m.family_rank, m.is_pass_away, m.marital_status,
  5242. p.name AS father_name,
  5243. p.simplified_name AS father_simplified_name,
  5244. p.name_word_generation AS father_generation,
  5245. r.relation_type AS father_relation_type
  5246. FROM family_member_info m
  5247. LEFT JOIN family_relation_info r
  5248. ON r.child_mid = m.id AND r.relation_type IN (1, 2)
  5249. LEFT JOIN family_member_info p ON p.id = r.parent_mid
  5250. {where}
  5251. ORDER BY m.name_word_generation ASC, m.id ASC
  5252. LIMIT 30
  5253. """
  5254. if keyword:
  5255. cursor.execute(
  5256. base_sql.format(where="WHERE m.name LIKE %s OR m.simplified_name LIKE %s"),
  5257. (f"%{keyword}%", f"%{keyword}%")
  5258. )
  5259. else:
  5260. cursor.execute(base_sql.format(where=""))
  5261. members = cursor.fetchall()
  5262. for m in members:
  5263. m['birthday_date'] = format_timestamp(m.get('birthday'))
  5264. return jsonify({"success": True, "data": members})
  5265. finally:
  5266. conn.close()
  5267. @app.route('/manager/api/members/check_duplicate', methods=['GET'])
  5268. def api_check_duplicate():
  5269. """检查同名成员"""
  5270. name = request.args.get('name', '')
  5271. conn = get_db_connection()
  5272. try:
  5273. with conn.cursor() as cursor:
  5274. cursor.execute("""
  5275. SELECT id, name, simplified_name, name_word_generation
  5276. FROM family_member_info
  5277. WHERE name = %s OR simplified_name = %s
  5278. LIMIT 10
  5279. """, (name, name))
  5280. members = cursor.fetchall()
  5281. return jsonify({
  5282. "success": True,
  5283. "data": members
  5284. })
  5285. finally:
  5286. conn.close()
  5287. @app.route('/manager/api/members/<int:member_id>', methods=['GET'])
  5288. def api_get_member(member_id):
  5289. """获取单个成员信息(含关系)"""
  5290. token = request.headers.get('Authorization', '').replace('Bearer ', '')
  5291. if not token:
  5292. return jsonify({"success": False, "message": "未登录"}), 401
  5293. conn = get_db_connection()
  5294. try:
  5295. with conn.cursor() as cursor:
  5296. cursor.execute("SELECT * FROM family_member_info WHERE id = %s", (member_id,))
  5297. member = cursor.fetchone()
  5298. if not member:
  5299. return jsonify({"success": False, "message": "成员不存在"}), 404
  5300. member['birthday_date'] = format_timestamp(member.get('birthday'))
  5301. if member.get('create_time'):
  5302. member['create_time'] = member['create_time'].strftime('%Y-%m-%d %H:%M')
  5303. if member.get('modified_time'):
  5304. member['modified_time'] = member['modified_time'].strftime('%Y-%m-%d %H:%M')
  5305. # 父母
  5306. cursor.execute("""
  5307. SELECT m.id, m.name, m.simplified_name, m.name_word_generation,
  5308. r.relation_type, r.sub_relation_type, r.child_order
  5309. FROM family_relation_info r
  5310. JOIN family_member_info m ON m.id = r.parent_mid
  5311. WHERE r.child_mid = %s
  5312. ORDER BY r.relation_type ASC
  5313. """, (member_id,))
  5314. parents = cursor.fetchall()
  5315. # 子女
  5316. cursor.execute("""
  5317. SELECT m.id, m.name, m.simplified_name, m.name_word_generation,
  5318. r.relation_type, r.sub_relation_type, r.child_order
  5319. FROM family_relation_info r
  5320. JOIN family_member_info m ON m.id = r.child_mid
  5321. WHERE r.parent_mid = %s
  5322. ORDER BY COALESCE(r.child_order, 9999), m.id ASC
  5323. """, (member_id,))
  5324. children = cursor.fetchall()
  5325. # relation_type: 1=父, 2=母
  5326. relation_labels = {1: '父', 2: '母', 3: '祖父', 4: '祖母'}
  5327. for p in parents:
  5328. p['relation_label'] = relation_labels.get(p.get('relation_type'), '亲属')
  5329. # 计算入继说明
  5330. _order_labels = {1:'长', 2:'次', 3:'三', 4:'四', 5:'五',
  5331. 6:'六', 7:'七', 8:'八', 9:'九', 10:'十'}
  5332. adopt_info = None
  5333. is_adopted_in = any(p.get('sub_relation_type') == 3 for p in parents)
  5334. if is_adopted_in:
  5335. bio = next((p for p in parents if p.get('sub_relation_type') == 2), None)
  5336. if bio:
  5337. bio_name = bio.get('simplified_name') or bio.get('name', '')
  5338. order = bio.get('child_order')
  5339. order_str = _order_labels.get(order, f'第{order}') if order else '某'
  5340. adopt_info = f"由{bio_name}公{order_str}子入继"
  5341. return jsonify({
  5342. "success": True,
  5343. "data": {**member, "parents": parents, "children": children,
  5344. "adopt_info": adopt_info}
  5345. })
  5346. finally:
  5347. conn.close()
  5348. @app.route('/manager/api/members/add', methods=['POST'])
  5349. def api_add_member():
  5350. """添加成员(小程序用)"""
  5351. token = request.headers.get('Authorization', '').replace('Bearer ', '')
  5352. if not token:
  5353. return jsonify({"success": False, "message": "未登录"}), 401
  5354. mp_user = get_mp_user_from_token(token)
  5355. mp_user_id = mp_user['id'] if mp_user else None
  5356. try:
  5357. data = request.get_json()
  5358. name = data.get('name', '')
  5359. simplified_name = data.get('simplified_name', '')
  5360. sex = data.get('sex', 1)
  5361. birthday_str = data.get('birthday', '')
  5362. family_rank = data.get('family_rank', '')
  5363. name_word_generation = data.get('name_word_generation', '')
  5364. is_pass_away = data.get('is_pass_away', 0)
  5365. marital_status = data.get('marital_status', 0)
  5366. former_name = data.get('former_name', '')
  5367. phone = data.get('phone', '')
  5368. notes = data.get('notes', '')
  5369. relations = data.get('relations', [])
  5370. if not name:
  5371. return jsonify({"success": False, "message": "姓名不能为空"}), 400
  5372. # 将日期字符串 "YYYY-MM-DD" 转为 Unix 时间戳(与后台一致)
  5373. birthday_ts = 0
  5374. if birthday_str:
  5375. try:
  5376. birthday_ts = int(datetime.strptime(birthday_str, '%Y-%m-%d').timestamp())
  5377. except Exception:
  5378. birthday_ts = 0
  5379. conn = get_db_connection()
  5380. try:
  5381. with conn.cursor() as cursor:
  5382. cursor.execute("""
  5383. INSERT INTO family_member_info
  5384. (name, simplified_name, sex, birthday, family_rank,
  5385. name_word_generation, is_pass_away, marital_status, former_name, phone, notes,
  5386. data_source, create_uid, create_time, modified_time)
  5387. VALUES (%s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, 'miniprogram', %s, CURRENT_TIMESTAMP, CURRENT_TIMESTAMP)
  5388. """, (name, simplified_name, sex, birthday_ts, family_rank,
  5389. name_word_generation, is_pass_away, marital_status, former_name, phone, notes,
  5390. mp_user_id))
  5391. conn.commit()
  5392. new_member_id = cursor.lastrowid
  5393. # 添加关系
  5394. for rel in relations:
  5395. parent_mid = rel.get('parent_mid')
  5396. relation_type = rel.get('relation_type', 1)
  5397. sub_relation_type = rel.get('sub_relation_type', 0)
  5398. if parent_mid:
  5399. cursor.execute("""
  5400. INSERT INTO family_relation_info
  5401. (parent_mid, child_mid, relation_type, sub_relation_type)
  5402. VALUES (%s, %s, %s, %s)
  5403. """, (parent_mid, new_member_id, relation_type, sub_relation_type))
  5404. conn.commit()
  5405. return jsonify({
  5406. "success": True,
  5407. "message": "添加成功",
  5408. "memberId": new_member_id
  5409. })
  5410. finally:
  5411. conn.close()
  5412. except Exception as e:
  5413. print(f"[API Add Member] Error: {e}")
  5414. return jsonify({"success": False, "message": str(e)}), 500
  5415. @app.route('/manager/api/members/my', methods=['GET'])
  5416. def api_my_members():
  5417. """获取当前小程序用户录入的所有成员"""
  5418. token = request.headers.get('Authorization', '').replace('Bearer ', '')
  5419. mp_user = get_mp_user_from_token(token)
  5420. if not mp_user:
  5421. return jsonify({"success": False, "message": "未登录或登录已过期"}), 401
  5422. conn = get_db_connection()
  5423. try:
  5424. with conn.cursor() as cursor:
  5425. cursor.execute("""
  5426. SELECT id, name, simplified_name, sex, birthday, name_word_generation,
  5427. family_rank, is_pass_away, marital_status, create_time
  5428. FROM family_member_info
  5429. WHERE create_uid = %s AND data_source = 'miniprogram'
  5430. ORDER BY create_time DESC
  5431. """, (mp_user['id'],))
  5432. members = cursor.fetchall()
  5433. for m in members:
  5434. m['birthday_date'] = format_timestamp(m.get('birthday'))
  5435. if m.get('create_time'):
  5436. m['create_time'] = m['create_time'].strftime('%Y-%m-%d %H:%M')
  5437. return jsonify({"success": True, "data": members})
  5438. except Exception as e:
  5439. print(f"[API My Members] Error: {e}")
  5440. return jsonify({"success": False, "message": str(e)}), 500
  5441. finally:
  5442. conn.close()
  5443. @app.route('/manager/api/member/<int:member_id>', methods=['PUT'])
  5444. def api_update_member(member_id):
  5445. """更新成员信息(小程序用,只能修改自己录入的)"""
  5446. token = request.headers.get('Authorization', '').replace('Bearer ', '')
  5447. mp_user = get_mp_user_from_token(token)
  5448. if not mp_user:
  5449. return jsonify({"success": False, "message": "未登录或登录已过期"}), 401
  5450. conn = get_db_connection()
  5451. try:
  5452. with conn.cursor() as cursor:
  5453. cursor.execute(
  5454. "SELECT id, create_uid, data_source FROM family_member_info WHERE id = %s",
  5455. (member_id,)
  5456. )
  5457. member = cursor.fetchone()
  5458. if not member:
  5459. return jsonify({"success": False, "message": "成员不存在"}), 404
  5460. if member['data_source'] != 'miniprogram' or member['create_uid'] != mp_user['id']:
  5461. return jsonify({"success": False, "message": "无权限修改此成员"}), 403
  5462. data = request.get_json() or {}
  5463. name = data.get('name', '').strip()
  5464. if not name:
  5465. return jsonify({"success": False, "message": "姓名不能为空"}), 400
  5466. birthday_str = data.get('birthday', '')
  5467. birthday_ts = 0
  5468. if birthday_str:
  5469. try:
  5470. from datetime import datetime as _dt
  5471. birthday_ts = int(_dt.strptime(birthday_str, '%Y-%m-%d').timestamp())
  5472. except Exception:
  5473. birthday_ts = 0
  5474. cursor.execute("""
  5475. UPDATE family_member_info
  5476. SET name=%s, simplified_name=%s, sex=%s, birthday=%s,
  5477. family_rank=%s, name_word_generation=%s, is_pass_away=%s,
  5478. marital_status=%s, phone=%s, notes=%s, modified_time=CURRENT_TIMESTAMP
  5479. WHERE id=%s
  5480. """, (
  5481. name,
  5482. data.get('simplified_name', ''),
  5483. int(data.get('sex', 1)),
  5484. birthday_ts,
  5485. data.get('family_rank') or None,
  5486. data.get('name_word_generation', ''),
  5487. int(data.get('is_pass_away', 0)),
  5488. int(data.get('marital_status', 0)),
  5489. data.get('phone', ''),
  5490. data.get('notes', ''),
  5491. member_id
  5492. ))
  5493. conn.commit()
  5494. return jsonify({"success": True, "message": "修改成功"})
  5495. except Exception as e:
  5496. conn.rollback()
  5497. print(f"[API Update Member] Error: {e}")
  5498. return jsonify({"success": False, "message": str(e)}), 500
  5499. finally:
  5500. conn.close()
  5501. @app.route('/manager/api/members/<int:member_id>', methods=['DELETE'])
  5502. def api_delete_member(member_id):
  5503. """删除成员(小程序用,只能删除自己录入的)"""
  5504. token = request.headers.get('Authorization', '').replace('Bearer ', '')
  5505. mp_user = get_mp_user_from_token(token)
  5506. if not mp_user:
  5507. return jsonify({"success": False, "message": "未登录或登录已过期"}), 401
  5508. conn = get_db_connection()
  5509. try:
  5510. with conn.cursor() as cursor:
  5511. cursor.execute("""
  5512. SELECT id, create_uid, data_source FROM family_member_info WHERE id = %s
  5513. """, (member_id,))
  5514. member = cursor.fetchone()
  5515. if not member:
  5516. return jsonify({"success": False, "message": "成员不存在"}), 404
  5517. if member['data_source'] != 'miniprogram' or member['create_uid'] != mp_user['id']:
  5518. return jsonify({"success": False, "message": "无权限删除此成员"}), 403
  5519. cursor.execute("DELETE FROM family_relation_info WHERE parent_mid = %s OR child_mid = %s",
  5520. (member_id, member_id))
  5521. cursor.execute("DELETE FROM family_member_info WHERE id = %s", (member_id,))
  5522. conn.commit()
  5523. return jsonify({"success": True, "message": "删除成功"})
  5524. except Exception as e:
  5525. conn.rollback()
  5526. print(f"[API Delete Member] Error: {e}")
  5527. return jsonify({"success": False, "message": str(e)}), 500
  5528. finally:
  5529. conn.close()
  5530. @app.route('/manager/api/lineage/<int:member_id>', methods=['GET'])
  5531. def api_get_lineage(member_id):
  5532. """获取世系信息(小程序用)- 完整多代版"""
  5533. token = request.headers.get('Authorization', '').replace('Bearer ', '')
  5534. if not token:
  5535. return jsonify({"success": False, "message": "未登录"}), 401
  5536. mode = request.args.get('mode', 'incense')
  5537. conn = get_db_connection()
  5538. try:
  5539. with conn.cursor() as cursor:
  5540. # Step 1: 获取查询人物
  5541. cursor.execute("""
  5542. SELECT id, name, simplified_name, name_word, name_word_generation,
  5543. former_name, childhood_name
  5544. FROM family_member_info WHERE id = %s
  5545. """, (member_id,))
  5546. center = cursor.fetchone()
  5547. if not center:
  5548. return jsonify({"success": False, "message": "成员不存在"}), 404
  5549. # Step 2: 向上追溯祖先链(最多100代),每代带同辈兄弟
  5550. generations = []
  5551. current_id = member_id
  5552. max_depth = 100
  5553. visited_ancestor_ids = set([member_id]) # 循环检测
  5554. for depth in range(max_depth):
  5555. cursor.execute("""
  5556. SELECT p.id, p.name, p.simplified_name, p.name_word, p.name_word_generation, p.former_name, p.childhood_name,
  5557. EXISTS(SELECT 1 FROM family_relation_info
  5558. WHERE parent_mid = p.id AND relation_type IN (1,2)) as has_children,
  5559. r.sub_relation_type
  5560. FROM family_relation_info r
  5561. JOIN family_member_info p ON r.parent_mid = p.id
  5562. WHERE r.child_mid = %s AND r.relation_type IN (1, 2)
  5563. """, (current_id,))
  5564. parents = cursor.fetchall()
  5565. if not parents:
  5566. break
  5567. # 分拣各类父母关系
  5568. normal_parent = None
  5569. adoptive_parent = None
  5570. bio_parent = None
  5571. for p in parents:
  5572. if p['sub_relation_type'] == 3:
  5573. adoptive_parent = p
  5574. elif p['sub_relation_type'] == 2:
  5575. bio_parent = p
  5576. else:
  5577. normal_parent = p
  5578. if mode == 'blood':
  5579. parent = normal_parent or bio_parent or adoptive_parent
  5580. # 血脉模式下若存在养父,仍标注出继/入继关系
  5581. if adoptive_parent is not None:
  5582. _bio_src = bio_parent or normal_parent
  5583. bio_name = (_bio_src.get('simplified_name') or _bio_src.get('name')) if _bio_src else None
  5584. adop_name = adoptive_parent.get('simplified_name') or adoptive_parent.get('name')
  5585. if bio_name and adop_name:
  5586. adopt_label = f"出继自{bio_name},入继至{adop_name}"
  5587. elif adop_name:
  5588. adopt_label = f"入继至{adop_name}"
  5589. else:
  5590. adopt_label = "出继"
  5591. if depth == 0:
  5592. center['adoption_label'] = adopt_label
  5593. elif generations:
  5594. generations[-1]['ancestor']['adoption_label'] = adopt_label
  5595. else:
  5596. parent = adoptive_parent or normal_parent or bio_parent
  5597. if parent is adoptive_parent and adoptive_parent is not None:
  5598. _bio_src = bio_parent or normal_parent
  5599. bio_name = (_bio_src.get('simplified_name') or _bio_src.get('name')) if _bio_src else None
  5600. adop_name = adoptive_parent.get('simplified_name') or adoptive_parent.get('name')
  5601. if bio_name and adop_name:
  5602. adopt_label = f"出继自{bio_name},入继至{adop_name}"
  5603. elif bio_name:
  5604. adopt_label = f"出继自{bio_name}"
  5605. else:
  5606. adopt_label = "出继"
  5607. if depth == 0:
  5608. center['adoption_label'] = adopt_label
  5609. elif generations:
  5610. generations[-1]['ancestor']['adoption_label'] = adopt_label
  5611. # 祖先卡片不携带子辈关系类型
  5612. parent['sub_relation_type'] = None
  5613. # 循环检测
  5614. if parent['id'] in visited_ancestor_ids:
  5615. break
  5616. visited_ancestor_ids.add(parent['id'])
  5617. # 查祖父以获取该祖先的兄弟
  5618. # 香火传承:优先养父(sub_type=3);血脉传承:优先生父
  5619. if mode == 'incense':
  5620. gp_order_wx = "CASE WHEN COALESCE(r.sub_relation_type, 0) = 3 THEN 0 ELSE 1 END"
  5621. else:
  5622. gp_order_wx = "CASE WHEN COALESCE(r.sub_relation_type, 0) = 3 THEN 1 ELSE 0 END"
  5623. cursor.execute(f"""
  5624. SELECT gp.id, gp.name, gp.simplified_name FROM family_relation_info r
  5625. JOIN family_member_info gp ON r.parent_mid = gp.id
  5626. WHERE r.child_mid = %s AND r.relation_type IN (1, 2)
  5627. ORDER BY {gp_order_wx}, r.id
  5628. LIMIT 1
  5629. """, (parent['id'],))
  5630. grandparent = cursor.fetchone()
  5631. parent_siblings = []
  5632. if grandparent:
  5633. # 获取祖先自身的 child_order
  5634. cursor.execute("""
  5635. SELECT COALESCE(child_order, 1) AS child_order
  5636. FROM family_relation_info
  5637. WHERE parent_mid = %s AND child_mid = %s AND relation_type IN (1,2)
  5638. LIMIT 1
  5639. """, (grandparent['id'], parent['id']))
  5640. co_row = cursor.fetchone()
  5641. parent['child_order'] = co_row['child_order'] if co_row else 1
  5642. if mode == 'blood':
  5643. _sib_filter = "AND COALESCE(r.sub_relation_type, 0) != 3"
  5644. else:
  5645. _sib_filter = """AND (
  5646. COALESCE(r.sub_relation_type, 0) != 2
  5647. OR NOT EXISTS (
  5648. SELECT 1 FROM family_relation_info r2
  5649. WHERE r2.child_mid = c.id AND r2.sub_relation_type = 3
  5650. )
  5651. )"""
  5652. cursor.execute(f"""
  5653. SELECT c.id, c.name, c.simplified_name, c.name_word, c.name_word_generation, c.former_name, c.childhood_name,
  5654. EXISTS(SELECT 1 FROM family_relation_info
  5655. WHERE parent_mid = c.id AND relation_type IN (1,2)) as has_children,
  5656. COALESCE(r.child_order, 1) AS child_order
  5657. FROM family_relation_info r
  5658. JOIN family_member_info c ON r.child_mid = c.id
  5659. WHERE r.parent_mid = %s AND r.relation_type IN (1,2) AND c.id != %s
  5660. {{_sib_filter}}
  5661. ORDER BY COALESCE(r.child_order, 1), c.id
  5662. LIMIT 10
  5663. """, (grandparent['id'], parent['id']))
  5664. parent_siblings = cursor.fetchall()
  5665. # 兄弟是祖父的子女,"入继至/出继自"用祖父名
  5666. wx_gp_display = grandparent.get('simplified_name') or grandparent.get('name') or ''
  5667. for s in parent_siblings:
  5668. s['has_children'] = bool(s['has_children'])
  5669. if s.get('sub_relation_type') == 3:
  5670. cursor.execute("""
  5671. SELECT p.simplified_name, p.name
  5672. FROM family_relation_info r
  5673. JOIN family_member_info p ON r.parent_mid = p.id
  5674. WHERE r.child_mid = %s AND r.sub_relation_type != 3
  5675. ORDER BY r.sub_relation_type DESC LIMIT 1
  5676. """, (s['id'],))
  5677. sbp = cursor.fetchone()
  5678. bio_name = (sbp['simplified_name'] or sbp['name']) if sbp else None
  5679. if bio_name and wx_gp_display:
  5680. s['adoption_label'] = f"出继自{bio_name},入继至{wx_gp_display}"
  5681. elif bio_name:
  5682. s['adoption_label'] = f"出继自{bio_name}"
  5683. else:
  5684. s['adoption_label'] = "出继"
  5685. elif mode == 'blood' and s.get('sub_relation_type') == 2:
  5686. cursor.execute("""
  5687. SELECT p.simplified_name, p.name
  5688. FROM family_relation_info r
  5689. JOIN family_member_info p ON r.parent_mid = p.id
  5690. WHERE r.child_mid = %s AND r.sub_relation_type = 3 LIMIT 1
  5691. """, (s['id'],))
  5692. adop = cursor.fetchone()
  5693. adop_name = (adop['simplified_name'] or adop['name']) if adop else None
  5694. if wx_gp_display and adop_name:
  5695. s['adoption_label'] = f"出继自{wx_gp_display},入继至{adop_name}"
  5696. elif adop_name:
  5697. s['adoption_label'] = f"入继至{adop_name}"
  5698. else:
  5699. s['adoption_label'] = "出继"
  5700. else:
  5701. parent['child_order'] = None
  5702. parent['has_children'] = bool(parent['has_children'])
  5703. generations.append({
  5704. 'ancestor': parent,
  5705. 'siblings': list(parent_siblings),
  5706. 'depth': depth
  5707. })
  5708. current_id = parent['id']
  5709. # Step 3: 获取子女(排除出继、保留入继,带排行)
  5710. cursor.execute("""
  5711. SELECT c.id, c.name, c.simplified_name, c.name_word, c.name_word_generation, c.former_name, c.childhood_name,
  5712. EXISTS(SELECT 1 FROM family_relation_info
  5713. WHERE parent_mid = c.id AND relation_type IN (1,2)) as has_children,
  5714. COALESCE(r.child_order, 1) AS child_order, r.sub_relation_type
  5715. FROM family_relation_info r
  5716. JOIN family_member_info c ON r.child_mid = c.id
  5717. WHERE r.parent_mid = %s AND r.relation_type IN (1, 2)
  5718. AND (
  5719. COALESCE(r.sub_relation_type, 0) != 2
  5720. OR NOT EXISTS (
  5721. SELECT 1 FROM family_relation_info r2
  5722. WHERE r2.child_mid = c.id AND r2.sub_relation_type = 3
  5723. )
  5724. )
  5725. ORDER BY COALESCE(r.child_order, 1), c.id
  5726. LIMIT 20
  5727. """, (member_id,))
  5728. children = cursor.fetchall()
  5729. center_display_wx = center.get('simplified_name') or center.get('name') or ''
  5730. for c in children:
  5731. c['has_children'] = bool(c['has_children'])
  5732. # 入继子女:附加生父信息,生成"出继自xx,入继至yy"标注
  5733. if c['sub_relation_type'] == 3:
  5734. cursor.execute("""
  5735. SELECT p.name, p.simplified_name
  5736. FROM family_relation_info r
  5737. JOIN family_member_info p ON r.parent_mid = p.id
  5738. WHERE r.child_mid = %s AND r.sub_relation_type != 3
  5739. ORDER BY r.sub_relation_type DESC
  5740. LIMIT 1
  5741. """, (c['id'],))
  5742. bp = cursor.fetchone()
  5743. bio_name = (bp['simplified_name'] or bp['name']) if bp else None
  5744. if bio_name and center_display_wx:
  5745. c['adoption_label'] = f"出继自{bio_name},入继至{center_display_wx}"
  5746. elif bio_name:
  5747. c['adoption_label'] = f"出继自{bio_name}"
  5748. else:
  5749. c['adoption_label'] = "出继"
  5750. # Step 4: 获取查询人物的同辈兄弟(含center自己的child_order)
  5751. siblings = []
  5752. center_child_order = None
  5753. if generations:
  5754. parent_id = generations[0]['ancestor']['id']
  5755. # 先获取 center 自身的 child_order
  5756. cursor.execute("""
  5757. SELECT child_order FROM family_relation_info
  5758. WHERE parent_mid = %s AND child_mid = %s AND relation_type IN (1,2)
  5759. LIMIT 1
  5760. """, (parent_id, member_id))
  5761. co_row = cursor.fetchone()
  5762. center_child_order = (co_row['child_order'] if co_row and co_row['child_order'] else 1)
  5763. cursor.execute("""
  5764. SELECT c.id, c.name, c.simplified_name, c.name_word, c.name_word_generation, c.former_name, c.childhood_name,
  5765. EXISTS(SELECT 1 FROM family_relation_info
  5766. WHERE parent_mid = c.id AND relation_type IN (1,2)) as has_children,
  5767. r.sub_relation_type, COALESCE(r.child_order, 1) AS child_order
  5768. FROM family_relation_info r
  5769. JOIN family_member_info c ON r.child_mid = c.id
  5770. WHERE r.parent_mid = %s AND r.relation_type IN (1,2) AND c.id != %s
  5771. ORDER BY COALESCE(r.child_order, 1), c.id
  5772. LIMIT 10
  5773. """, (parent_id, member_id))
  5774. siblings = cursor.fetchall()
  5775. for s in siblings:
  5776. s['has_children'] = bool(s['has_children'])
  5777. # 判断是否还有更高的祖先
  5778. has_more_ancestors = False
  5779. topmost_ancestor_id = None
  5780. if generations:
  5781. topmost_ancestor_id = generations[-1]['ancestor']['id']
  5782. cursor.execute("""
  5783. SELECT COUNT(*) as cnt FROM family_relation_info
  5784. WHERE child_mid = %s AND relation_type IN (1,2)
  5785. """, (topmost_ancestor_id,))
  5786. has_more_ancestors = cursor.fetchone()['cnt'] > 0
  5787. return jsonify({
  5788. "success": True,
  5789. "data": {
  5790. "center": {**center, "child_order": center_child_order or 1},
  5791. "generations": generations,
  5792. "siblings": list(siblings),
  5793. "children": list(children),
  5794. "has_more_ancestors": has_more_ancestors,
  5795. "topmost_ancestor_id": topmost_ancestor_id
  5796. }
  5797. })
  5798. except Exception as e:
  5799. print(f"[API Lineage] Error: {e}")
  5800. return jsonify({"success": False, "message": str(e)}), 500
  5801. finally:
  5802. conn.close()
  5803. @app.route('/manager/api/lineage/<int:ancestor_id>/ancestors_above', methods=['GET'])
  5804. def api_get_ancestors_above(ancestor_id):
  5805. """小程序世系查询:从指定祖先节点继续向上追溯(分批加载更多祖先)"""
  5806. token = request.headers.get('Authorization', '').replace('Bearer ', '')
  5807. if not token:
  5808. return jsonify({"success": False, "message": "未登录"}), 401
  5809. conn = get_db_connection()
  5810. try:
  5811. with conn.cursor() as cursor:
  5812. generations = []
  5813. current_id = ancestor_id
  5814. max_depth = 100
  5815. visited_ids = set([ancestor_id])
  5816. # 计算 anchor 节点(ancestor_id)自身的 adoption_label
  5817. anchor_adoption_label_wx = None
  5818. cursor.execute("""
  5819. SELECT p.id, p.name, p.simplified_name, r.sub_relation_type
  5820. FROM family_relation_info r
  5821. JOIN family_member_info p ON r.parent_mid = p.id
  5822. WHERE r.child_mid = %s AND r.relation_type IN (1, 2)
  5823. """, (ancestor_id,))
  5824. anchor_parents_wx = cursor.fetchall()
  5825. anchor_bio_wx = None
  5826. anchor_adop_wx = None
  5827. anchor_normal_wx = None
  5828. for ap in anchor_parents_wx:
  5829. if ap['sub_relation_type'] == 3:
  5830. anchor_adop_wx = ap
  5831. elif ap['sub_relation_type'] == 2:
  5832. anchor_bio_wx = ap
  5833. else:
  5834. anchor_normal_wx = ap
  5835. if anchor_adop_wx:
  5836. _bio_src_wx = anchor_bio_wx or anchor_normal_wx
  5837. bio_name_wx = (_bio_src_wx.get('simplified_name') or _bio_src_wx.get('name')) if _bio_src_wx else None
  5838. adop_name_wx = anchor_adop_wx.get('simplified_name') or anchor_adop_wx.get('name')
  5839. if bio_name_wx and adop_name_wx:
  5840. anchor_adoption_label_wx = f"出继自{bio_name_wx},入继至{adop_name_wx}"
  5841. elif bio_name_wx:
  5842. anchor_adoption_label_wx = f"出继自{bio_name_wx}"
  5843. else:
  5844. anchor_adoption_label_wx = "出继"
  5845. for depth in range(max_depth):
  5846. cursor.execute("""
  5847. SELECT p.id, p.name, p.simplified_name, p.name_word, p.name_word_generation, p.former_name, p.childhood_name,
  5848. EXISTS(SELECT 1 FROM family_relation_info
  5849. WHERE parent_mid = p.id AND relation_type IN (1,2)) as has_children,
  5850. r.sub_relation_type
  5851. FROM family_relation_info r
  5852. JOIN family_member_info p ON r.parent_mid = p.id
  5853. WHERE r.child_mid = %s AND r.relation_type IN (1, 2)
  5854. """, (current_id,))
  5855. parents = cursor.fetchall()
  5856. if not parents:
  5857. break
  5858. # 父母优先级:入继养父母(3) > 普通亲生(0) > 出继亲生(2)
  5859. normal_parent = None
  5860. adoptive_parent = None
  5861. bio_parent = None
  5862. for p in parents:
  5863. if p['sub_relation_type'] == 3:
  5864. adoptive_parent = p
  5865. elif p['sub_relation_type'] == 2:
  5866. bio_parent = p
  5867. else:
  5868. normal_parent = p
  5869. if mode == 'blood':
  5870. parent = normal_parent or bio_parent or adoptive_parent
  5871. if adoptive_parent is not None:
  5872. _bio_src = bio_parent or normal_parent
  5873. bio_name = (_bio_src.get('simplified_name') or _bio_src.get('name')) if _bio_src else None
  5874. adop_name = adoptive_parent.get('simplified_name') or adoptive_parent.get('name')
  5875. if bio_name and adop_name:
  5876. adopt_label = f"出继自{bio_name},入继至{adop_name}"
  5877. elif adop_name:
  5878. adopt_label = f"入继至{adop_name}"
  5879. else:
  5880. adopt_label = "出继"
  5881. if depth == 0:
  5882. anchor_adoption_label_wx = adopt_label
  5883. elif generations:
  5884. generations[-1]['ancestor']['adoption_label'] = adopt_label
  5885. else:
  5886. parent = adoptive_parent or normal_parent or bio_parent
  5887. # 若走入继路径,在 current_id 对应的人物上标注"出继自xx,入继至yy"
  5888. if parent is adoptive_parent and adoptive_parent is not None:
  5889. _bio_src = bio_parent or normal_parent
  5890. bio_name = (_bio_src.get('simplified_name') or _bio_src.get('name')) if _bio_src else None
  5891. adop_name = adoptive_parent.get('simplified_name') or adoptive_parent.get('name')
  5892. if bio_name and adop_name:
  5893. adopt_label = f"出继自{bio_name},入继至{adop_name}"
  5894. elif bio_name:
  5895. adopt_label = f"出继自{bio_name}"
  5896. else:
  5897. adopt_label = "出继"
  5898. if depth == 0:
  5899. anchor_adoption_label_wx = adopt_label
  5900. elif generations:
  5901. generations[-1]['ancestor']['adoption_label'] = adopt_label
  5902. # 祖先卡片不携带子辈关系类型
  5903. parent['sub_relation_type'] = None
  5904. if parent['id'] in visited_ids:
  5905. break
  5906. visited_ids.add(parent['id'])
  5907. # 香火传承:优先养父(sub_type=3);血脉传承:优先生父
  5908. if mode == 'incense':
  5909. gp_order_wxab = "CASE WHEN COALESCE(r.sub_relation_type, 0) = 3 THEN 0 ELSE 1 END"
  5910. else:
  5911. gp_order_wxab = "CASE WHEN COALESCE(r.sub_relation_type, 0) = 3 THEN 1 ELSE 0 END"
  5912. cursor.execute(f"""
  5913. SELECT gp.id, gp.name, gp.simplified_name FROM family_relation_info r
  5914. JOIN family_member_info gp ON r.parent_mid = gp.id
  5915. WHERE r.child_mid = %s AND r.relation_type IN (1, 2)
  5916. ORDER BY {gp_order_wxab}, r.id
  5917. LIMIT 1
  5918. """, (parent['id'],))
  5919. grandparent = cursor.fetchone()
  5920. parent_siblings = []
  5921. if grandparent:
  5922. cursor.execute("""
  5923. SELECT COALESCE(child_order, 1) AS child_order
  5924. FROM family_relation_info
  5925. WHERE parent_mid = %s AND child_mid = %s AND relation_type IN (1,2) LIMIT 1
  5926. """, (grandparent['id'], parent['id']))
  5927. co_row = cursor.fetchone()
  5928. parent['child_order'] = co_row['child_order'] if co_row else 1
  5929. if mode == 'blood':
  5930. _sib_filter = "AND COALESCE(r.sub_relation_type, 0) != 3"
  5931. else:
  5932. _sib_filter = """AND (
  5933. COALESCE(r.sub_relation_type, 0) != 2
  5934. OR NOT EXISTS (
  5935. SELECT 1 FROM family_relation_info r2
  5936. WHERE r2.child_mid = c.id AND r2.sub_relation_type = 3
  5937. )
  5938. )"""
  5939. cursor.execute(f"""
  5940. SELECT c.id, c.name, c.simplified_name, c.name_word, c.name_word_generation, c.former_name, c.childhood_name,
  5941. EXISTS(SELECT 1 FROM family_relation_info
  5942. WHERE parent_mid = c.id AND relation_type IN (1,2)) as has_children,
  5943. COALESCE(r.child_order, 1) AS child_order
  5944. FROM family_relation_info r
  5945. JOIN family_member_info c ON r.child_mid = c.id
  5946. WHERE r.parent_mid = %s AND r.relation_type IN (1,2) AND c.id != %s
  5947. {{_sib_filter}}
  5948. ORDER BY COALESCE(r.child_order, 1), c.id
  5949. LIMIT 10
  5950. """, (grandparent['id'], parent['id']))
  5951. parent_siblings = cursor.fetchall()
  5952. # 兄弟是祖父的子女,"入继至/出继自"用祖父名
  5953. wxab_gp_display = grandparent.get('simplified_name') or grandparent.get('name') or ''
  5954. for s in parent_siblings:
  5955. s['has_children'] = bool(s['has_children'])
  5956. if s.get('sub_relation_type') == 3:
  5957. cursor.execute("""
  5958. SELECT p.simplified_name, p.name
  5959. FROM family_relation_info r
  5960. JOIN family_member_info p ON r.parent_mid = p.id
  5961. WHERE r.child_mid = %s AND r.sub_relation_type != 3
  5962. ORDER BY r.sub_relation_type DESC LIMIT 1
  5963. """, (s['id'],))
  5964. sbp = cursor.fetchone()
  5965. bio_name = (sbp['simplified_name'] or sbp['name']) if sbp else None
  5966. if bio_name and wxab_gp_display:
  5967. s['adoption_label'] = f"出继自{bio_name},入继至{wxab_gp_display}"
  5968. elif bio_name:
  5969. s['adoption_label'] = f"出继自{bio_name}"
  5970. else:
  5971. s['adoption_label'] = "出继"
  5972. elif mode == 'blood' and s.get('sub_relation_type') == 2:
  5973. cursor.execute("""
  5974. SELECT p.simplified_name, p.name
  5975. FROM family_relation_info r
  5976. JOIN family_member_info p ON r.parent_mid = p.id
  5977. WHERE r.child_mid = %s AND r.sub_relation_type = 3 LIMIT 1
  5978. """, (s['id'],))
  5979. adop = cursor.fetchone()
  5980. adop_name = (adop['simplified_name'] or adop['name']) if adop else None
  5981. if wxab_gp_display and adop_name:
  5982. s['adoption_label'] = f"出继自{wxab_gp_display},入继至{adop_name}"
  5983. elif adop_name:
  5984. s['adoption_label'] = f"入继至{adop_name}"
  5985. else:
  5986. s['adoption_label'] = "出继"
  5987. else:
  5988. parent['child_order'] = None
  5989. parent['has_children'] = bool(parent['has_children'])
  5990. generations.append({
  5991. 'ancestor': parent,
  5992. 'siblings': list(parent_siblings),
  5993. 'depth': depth
  5994. })
  5995. current_id = parent['id']
  5996. has_more_ancestors = False
  5997. topmost_ancestor_id = None
  5998. if generations:
  5999. topmost_ancestor_id = generations[-1]['ancestor']['id']
  6000. cursor.execute("""
  6001. SELECT COUNT(*) as cnt FROM family_relation_info
  6002. WHERE child_mid = %s AND relation_type IN (1,2)
  6003. """, (topmost_ancestor_id,))
  6004. has_more_ancestors = cursor.fetchone()['cnt'] > 0
  6005. return jsonify({
  6006. "success": True,
  6007. "data": {
  6008. "generations": generations,
  6009. "has_more_ancestors": has_more_ancestors,
  6010. "topmost_ancestor_id": topmost_ancestor_id,
  6011. "anchor_adoption_label": anchor_adoption_label_wx
  6012. }
  6013. })
  6014. except Exception as e:
  6015. print(f"[API Ancestors Above] Error: {e}")
  6016. return jsonify({"success": False, "message": str(e)}), 500
  6017. finally:
  6018. conn.close()
  6019. @app.route('/manager/api/mp/wx/auth/login', methods=['POST'])
  6020. def mp_wx_login():
  6021. """微信小程序登录接口"""
  6022. try:
  6023. data = request.get_json()
  6024. code = data.get('code', '')
  6025. userInfo = data.get('userInfo', {})
  6026. if not code:
  6027. return jsonify({"success": False, "message": "缺少code参数"}), 400
  6028. openid = f"mock_openid_{code[:8]}"
  6029. conn = get_db_connection()
  6030. try:
  6031. with conn.cursor() as cursor:
  6032. cursor.execute("SELECT id, openid, member_id, is_bound FROM family_member_bind WHERE openid = %s", (openid,))
  6033. bind_info = cursor.fetchone()
  6034. if not bind_info:
  6035. cursor.execute("INSERT INTO family_member_bind (openid, created_at) VALUES (%s, CURRENT_TIMESTAMP)", (openid,))
  6036. conn.commit()
  6037. bind_info = {
  6038. 'id': cursor.lastrowid,
  6039. 'openid': openid,
  6040. 'member_id': None,
  6041. 'is_bound': 0
  6042. }
  6043. finally:
  6044. conn.close()
  6045. return jsonify({
  6046. "success": True,
  6047. "data": {
  6048. "openid": openid,
  6049. "token": f"mock_token_{openid}",
  6050. "isBound": bool(bind_info['is_bound']),
  6051. "memberId": bind_info['member_id']
  6052. }
  6053. })
  6054. except Exception as e:
  6055. print(f"[MP Login] Error: {e}")
  6056. return jsonify({"success": False, "message": str(e)}), 500
  6057. @app.route('/manager/api/mp/wx/config/getConfig', methods=['GET'])
  6058. def mp_wx_get_config():
  6059. """获取配置信息"""
  6060. config_key = request.args.get('configKey', '')
  6061. config_data = {
  6062. "CAROUSEL": {
  6063. "success": True,
  6064. "images": [
  6065. {
  6066. "image": "",
  6067. "title": "留家族旅",
  6068. "subtitle": "传承家族文化"
  6069. }
  6070. ]
  6071. },
  6072. "HONOR": {
  6073. "success": True,
  6074. "data": {
  6075. "name": "留越",
  6076. "role": "族谱发起人",
  6077. "desc": "2025年发起族谱建设,统筹信息收集"
  6078. }
  6079. }
  6080. }
  6081. result = config_data.get(config_key, {"success": False, "message": "配置不存在"})
  6082. return jsonify(result)
  6083. @app.route('/manager/api/mp/wx/family/member/selfCard', methods=['GET'])
  6084. def mp_wx_self_card():
  6085. """获取用户自己的卡片信息"""
  6086. openid = request.headers.get('X-MP-Openid', '')
  6087. if not openid:
  6088. return jsonify({"success": False, "message": "未登录"}), 401
  6089. conn = get_db_connection()
  6090. try:
  6091. with conn.cursor() as cursor:
  6092. cursor.execute("SELECT member_id FROM family_member_bind WHERE openid = %s", (openid,))
  6093. bind_info = cursor.fetchone()
  6094. if not bind_info or not bind_info['member_id']:
  6095. return jsonify({"success": False, "message": "未绑定成员"})
  6096. cursor.execute("SELECT id, name, simplified_name, sex, birthday, occupation, family_rank, branch_family_hall, residential_address FROM family_member_info WHERE id = %s", (bind_info['member_id'],))
  6097. member = cursor.fetchone()
  6098. if not member:
  6099. return jsonify({"success": False, "message": "成员不存在"})
  6100. return jsonify({
  6101. "success": True,
  6102. "data": member
  6103. })
  6104. finally:
  6105. conn.close()
  6106. @app.route('/manager/api/mp/wx/family/member/search', methods=['GET'])
  6107. def mp_wx_search_members():
  6108. """搜索家族成员"""
  6109. keyword = request.args.get('keyword', '')
  6110. conn = get_db_connection()
  6111. try:
  6112. with conn.cursor() as cursor:
  6113. if keyword:
  6114. cursor.execute("""
  6115. SELECT id, name, simplified_name, sex, birthday, family_rank
  6116. FROM family_member_info
  6117. WHERE name LIKE %s OR simplified_name LIKE %s
  6118. ORDER BY name_word_generation ASC, id ASC
  6119. LIMIT 20
  6120. """, (f"%{keyword}%", f"%{keyword}%"))
  6121. else:
  6122. cursor.execute("""
  6123. SELECT id, name, simplified_name, sex, birthday, family_rank
  6124. FROM family_member_info
  6125. ORDER BY name_word_generation ASC, id ASC
  6126. LIMIT 20
  6127. """)
  6128. members = cursor.fetchall()
  6129. return jsonify({
  6130. "success": True,
  6131. "data": members
  6132. })
  6133. finally:
  6134. conn.close()
  6135. @app.route('/manager/api/mp/wx/family/member/bind', methods=['POST'])
  6136. def mp_wx_bind_member():
  6137. """绑定用户到家族成员"""
  6138. try:
  6139. data = request.get_json()
  6140. openid = data.get('openid', '')
  6141. member_id = data.get('memberId', '')
  6142. if not openid or not member_id:
  6143. return jsonify({"success": False, "message": "参数错误"}), 400
  6144. conn = get_db_connection()
  6145. try:
  6146. with conn.cursor() as cursor:
  6147. cursor.execute("SELECT id FROM family_member_info WHERE id = %s", (member_id,))
  6148. member = cursor.fetchone()
  6149. if not member:
  6150. return jsonify({"success": False, "message": "成员不存在"})
  6151. cursor.execute("UPDATE family_member_bind SET member_id = %s, is_bound = 1, updated_at = CURRENT_TIMESTAMP WHERE openid = %s", (member_id, openid))
  6152. conn.commit()
  6153. return jsonify({"success": True, "message": "绑定成功"})
  6154. finally:
  6155. conn.close()
  6156. except Exception as e:
  6157. print(f"[MP Bind] Error: {e}")
  6158. return jsonify({"success": False, "message": str(e)}), 500
  6159. @app.route('/manager/api/mp/wx/family/lineage', methods=['GET'])
  6160. def mp_wx_get_lineage():
  6161. """获取世系信息"""
  6162. openid = request.headers.get('X-MP-Openid', '')
  6163. member_id = request.args.get('memberId', '')
  6164. if not openid:
  6165. return jsonify({"success": False, "message": "未登录"}), 401
  6166. conn = get_db_connection()
  6167. try:
  6168. if not member_id:
  6169. with conn.cursor() as cursor:
  6170. cursor.execute("SELECT member_id FROM family_member_bind WHERE openid = %s", (openid,))
  6171. bind_info = cursor.fetchone()
  6172. if bind_info and bind_info['member_id']:
  6173. member_id = bind_info['member_id']
  6174. else:
  6175. return jsonify({"success": False, "message": "未绑定成员"})
  6176. with conn.cursor() as cursor:
  6177. cursor.execute("""
  6178. SELECT id, name, simplified_name, sex, name_word_generation, birthday, occupation, family_rank, branch_family_hall, residential_address
  6179. FROM family_member_info
  6180. WHERE id = %s
  6181. """, (member_id,))
  6182. member = cursor.fetchone()
  6183. if not member:
  6184. return jsonify({"success": False, "message": "成员不存在"})
  6185. current_member = {
  6186. "id": member['id'],
  6187. "name": member['name'],
  6188. "simplified_name": member['simplified_name'],
  6189. "sex": member['sex'],
  6190. "name_word_generation": member['name_word_generation'],
  6191. "birthday": member['birthday'],
  6192. "occupation": member['occupation'],
  6193. "family_rank": member['family_rank'],
  6194. "branch_family_hall": member['branch_family_hall'],
  6195. "residential_address": member['residential_address']
  6196. }
  6197. cursor.execute("""
  6198. SELECT p.id, p.name, p.simplified_name, p.sex, p.name_word_generation, p.birthday
  6199. FROM family_relation_info r
  6200. JOIN family_member_info p ON r.parent_mid = p.id
  6201. WHERE r.child_mid = %s AND r.relation_type = 1
  6202. """, (member_id,))
  6203. father = cursor.fetchone()
  6204. cursor.execute("""
  6205. SELECT p.id, p.name, p.simplified_name, p.sex, p.name_word_generation, p.birthday
  6206. FROM family_relation_info r
  6207. JOIN family_member_info p ON r.parent_mid = p.id
  6208. WHERE r.child_mid = %s AND r.relation_type = 2
  6209. """, (member_id,))
  6210. mother = cursor.fetchone()
  6211. ancestors = []
  6212. if father or mother:
  6213. ancestors.append({
  6214. "father": father,
  6215. "mother": mother
  6216. })
  6217. cursor.execute("""
  6218. SELECT c.id, c.name, c.simplified_name, c.sex, c.name_word_generation, c.birthday, r.child_order
  6219. FROM family_relation_info r
  6220. JOIN family_member_info c ON r.child_mid = c.id
  6221. WHERE r.parent_mid = %s AND r.relation_type IN (1, 2)
  6222. ORDER BY COALESCE(r.child_order, 999) ASC
  6223. """, (member_id,))
  6224. children = cursor.fetchall()
  6225. cursor.execute("""
  6226. SELECT DISTINCT s.id, s.name, s.simplified_name, s.sex
  6227. FROM family_relation_info r1
  6228. JOIN family_relation_info r2 ON r1.parent_mid = r2.parent_mid
  6229. JOIN family_member_info s ON r2.child_mid = s.id
  6230. WHERE r1.child_mid = %s AND r2.child_mid != %s
  6231. ORDER BY COALESCE(r2.child_order, 999) ASC
  6232. """, (member_id, member_id))
  6233. siblings = cursor.fetchall()
  6234. return jsonify({
  6235. "success": True,
  6236. "data": {
  6237. "member": current_member,
  6238. "ancestors": ancestors,
  6239. "children": children,
  6240. "siblings": siblings
  6241. }
  6242. })
  6243. finally:
  6244. conn.close()
  6245. @app.route('/manager/api/mp/wx/family/member/add', methods=['POST'])
  6246. def mp_wx_add_member():
  6247. """添加家族成员"""
  6248. try:
  6249. data = request.get_json()
  6250. openid = data.get('openid', '')
  6251. member_data = data.get('memberData', {})
  6252. relation_data = data.get('relationData', {})
  6253. if not openid:
  6254. return jsonify({"success": False, "message": "未登录"}), 401
  6255. conn = get_db_connection()
  6256. try:
  6257. with conn.cursor() as cursor:
  6258. member_info = {
  6259. 'name': member_data.get('name', ''),
  6260. 'simplified_name': member_data.get('simplified_name', '') or member_data.get('name', ''),
  6261. 'sex': member_data.get('sex', 1),
  6262. 'birthday': member_data.get('birthday', ''),
  6263. 'occupation': member_data.get('occupation', ''),
  6264. 'family_rank': member_data.get('family_rank', ''),
  6265. 'branch_family_hall': member_data.get('branch_family_hall', ''),
  6266. 'residential_address': member_data.get('residential_address', ''),
  6267. 'genealogy_text': member_data.get('genealogy_text', ''),
  6268. 'create_time': datetime.now(),
  6269. 'modified_time': datetime.now()
  6270. }
  6271. fields = ", ".join(member_info.keys())
  6272. placeholders = ", ".join(["%s"] * len(member_info))
  6273. sql = f"INSERT INTO family_member_info ({fields}) VALUES ({placeholders})"
  6274. cursor.execute(sql, list(member_info.values()))
  6275. new_member_id = cursor.lastrowid
  6276. relation_type = relation_data.get('relationType', '')
  6277. if relation_type:
  6278. parent_id = relation_data.get('parentId')
  6279. child_order = relation_data.get('childOrder', 1)
  6280. if parent_id and relation_type in ['father', 'mother', 'child']:
  6281. rel_type = 1 if relation_type == 'father' else 2 if relation_type == 'mother' else 1
  6282. cursor.execute("""
  6283. INSERT INTO family_relation_info
  6284. (parent_mid, child_mid, relation_type, child_order, source_mid, generation_diff)
  6285. VALUES (%s, %s, %s, %s, %s, 1)
  6286. """, (parent_id, new_member_id, rel_type, child_order, new_member_id))
  6287. conn.commit()
  6288. return jsonify({
  6289. "success": True,
  6290. "message": "添加成功",
  6291. "memberId": new_member_id
  6292. })
  6293. except Exception as e:
  6294. conn.rollback()
  6295. raise e
  6296. finally:
  6297. conn.close()
  6298. except Exception as e:
  6299. print(f"[MP Add Member] Error: {e}")
  6300. return jsonify({"success": False, "message": str(e)}), 500
  6301. @app.route('/manager/api/mp/wx/family/member/update', methods=['POST'])
  6302. def mp_wx_update_member():
  6303. """更新家族成员信息"""
  6304. try:
  6305. data = request.get_json()
  6306. openid = data.get('openid', '')
  6307. member_id = data.get('memberId', '')
  6308. update_data = data.get('updateData', {})
  6309. if not openid or not member_id:
  6310. return jsonify({"success": False, "message": "参数错误"}), 400
  6311. conn = get_db_connection()
  6312. try:
  6313. update_parts = []
  6314. params = []
  6315. if 'name' in update_data:
  6316. update_parts.append("name = %s")
  6317. params.append(update_data['name'])
  6318. if 'simplified_name' in update_data:
  6319. update_parts.append("simplified_name = %s")
  6320. params.append(update_data['simplified_name'])
  6321. if 'sex' in update_data:
  6322. update_parts.append("sex = %s")
  6323. params.append(update_data['sex'])
  6324. if 'birthday' in update_data:
  6325. update_parts.append("birthday = %s")
  6326. params.append(update_data['birthday'])
  6327. if 'occupation' in update_data:
  6328. update_parts.append("occupation = %s")
  6329. params.append(update_data['occupation'])
  6330. if 'family_rank' in update_data:
  6331. update_parts.append("family_rank = %s")
  6332. params.append(update_data['family_rank'])
  6333. if 'branch_family_hall' in update_data:
  6334. update_parts.append("branch_family_hall = %s")
  6335. params.append(update_data['branch_family_hall'])
  6336. if 'residential_address' in update_data:
  6337. update_parts.append("residential_address = %s")
  6338. params.append(update_data['residential_address'])
  6339. update_parts.append("modified_time = CURRENT_TIMESTAMP")
  6340. params.append(member_id)
  6341. sql = f"UPDATE family_member_info SET {', '.join(update_parts)} WHERE id = %s"
  6342. with conn.cursor() as cursor:
  6343. cursor.execute(sql, params)
  6344. conn.commit()
  6345. return jsonify({"success": True, "message": "更新成功"})
  6346. except Exception as e:
  6347. conn.rollback()
  6348. raise e
  6349. finally:
  6350. conn.close()
  6351. except Exception as e:
  6352. print(f"[MP Update Member] Error: {e}")
  6353. return jsonify({"success": False, "message": str(e)}), 500
  6354. # ==================== End 微信小程序 API 接口 ====================
  6355. # ==================== 代数连续性核查 ====================
  6356. def _check_member_generation(conn, member_id):
  6357. """
  6358. 保存成员后调用:检查该成员与其亲生父亲的代数是否连续。
  6359. 返回 list of warning_str(空列表表示无问题)。
  6360. """
  6361. try:
  6362. with conn.cursor() as cursor:
  6363. cursor.execute(
  6364. "SELECT name, simplified_name, name_word_generation FROM family_member_info WHERE id = %s",
  6365. (member_id,)
  6366. )
  6367. member = cursor.fetchone()
  6368. if not member:
  6369. return []
  6370. cursor.execute("""
  6371. SELECT p.id, p.name, p.simplified_name, p.name_word_generation
  6372. FROM family_relation_info r
  6373. JOIN family_member_info p ON r.parent_mid = p.id
  6374. WHERE r.child_mid = %s AND r.relation_type = 1
  6375. AND COALESCE(r.sub_relation_type, 0) != 3
  6376. LIMIT 5
  6377. """, (member_id,))
  6378. parents = cursor.fetchall()
  6379. if not parents:
  6380. return []
  6381. member_name = member.get('simplified_name') or member.get('name') or f'ID:{member_id}'
  6382. child_gens = _parse_generation_labels(member.get('name_word_generation') or '')
  6383. warnings = []
  6384. for parent in parents:
  6385. parent_gens = _parse_generation_labels(parent.get('name_word_generation') or '')
  6386. if not parent_gens or not child_gens:
  6387. continue
  6388. parent_map = dict(parent_gens)
  6389. child_map = dict(child_gens)
  6390. common = set(parent_map.keys()) & set(child_map.keys())
  6391. for prefix in common:
  6392. p_gen = parent_map[prefix]
  6393. c_gen = child_map[prefix]
  6394. if c_gen != p_gen + 1:
  6395. parent_name = parent.get('simplified_name') or parent.get('name') or '?'
  6396. expected = p_gen + 1
  6397. diff_desc = (
  6398. f"与父亲相同(均为第{c_gen}代)" if c_gen == p_gen
  6399. else f"跳跃了{c_gen - p_gen - 1}代(应为第{expected}代,实为第{c_gen}代)" if c_gen > p_gen + 1
  6400. else f"倒退(父第{p_gen}代,子第{c_gen}代)"
  6401. )
  6402. warnings.append(
  6403. f"⚠️ 代数不连续 [{prefix}]:{member_name} 第{c_gen}代,"
  6404. f"父亲 {parent_name} 第{p_gen}代,{diff_desc}"
  6405. )
  6406. return warnings
  6407. except Exception as e:
  6408. print(f"[GenerationCheck] Warning: {e}")
  6409. return []
  6410. def _cn_to_int(s):
  6411. """将中文数字(如"三十二")转为整数,支持 1-999。"""
  6412. if not s:
  6413. return None
  6414. digits = {'零':0,'一':1,'二':2,'三':3,'四':4,'五':5,'六':6,'七':7,'八':8,'九':9}
  6415. units = {'十':10,'百':100,'千':1000}
  6416. result, temp = 0, 0
  6417. for ch in s:
  6418. if ch in digits:
  6419. temp = digits[ch]
  6420. elif ch in units:
  6421. if temp == 0 and ch == '十': # 十一 → 11,十 → 10
  6422. temp = 1
  6423. result += temp * units[ch]
  6424. temp = 0
  6425. result += temp
  6426. return result or None
  6427. def _parse_generation_labels(gen_str):
  6428. """
  6429. 解析 name_word_generation 字段(可含多个分号分隔的代数标签),
  6430. 返回 list of (branch_prefix, gen_int)。
  6431. 例:"衢州第三十代;某支第五代" → [("衢州", 30), ("某支", 5)]
  6432. """
  6433. import re
  6434. if not gen_str:
  6435. return []
  6436. results = []
  6437. for part in gen_str.split(';'):
  6438. part = part.strip()
  6439. m = re.match(r'^(.*?)第([零一二三四五六七八九十百千]+)代$', part)
  6440. if m:
  6441. prefix = m.group(1)
  6442. num = _cn_to_int(m.group(2))
  6443. if num is not None:
  6444. results.append((prefix, num))
  6445. return results
  6446. @app.route('/manager/generation_check')
  6447. def generation_check_page():
  6448. if 'user_id' not in session:
  6449. return redirect(url_for('login'))
  6450. return render_template('generation_check.html')
  6451. @app.route('/manager/api/generation_check')
  6452. def generation_check_api():
  6453. """
  6454. 核查所有父子关系中代数是否连续(子=父+1)。
  6455. 仅检查亲生父子关系(relation_type=1,排除入继 sub_type=3)。
  6456. 返回所有不连续的情况,包含人员信息方便排查。
  6457. """
  6458. if 'user_id' not in session:
  6459. return jsonify({"success": False, "message": "Unauthorized"}), 401
  6460. conn = get_db_connection()
  6461. try:
  6462. with conn.cursor() as cursor:
  6463. # 获取所有成员的代数字段
  6464. cursor.execute("""
  6465. SELECT id, name, simplified_name, name_word_generation
  6466. FROM family_member_info
  6467. """)
  6468. members = {row['id']: row for row in cursor.fetchall()}
  6469. # 获取所有亲生父子关系(排除入继)
  6470. cursor.execute("""
  6471. SELECT parent_mid, child_mid
  6472. FROM family_relation_info
  6473. WHERE relation_type = 1
  6474. AND COALESCE(sub_relation_type, 0) != 3
  6475. """)
  6476. relations = cursor.fetchall()
  6477. errors = []
  6478. warnings = [] # 父或子缺少可解析代数
  6479. for rel in relations:
  6480. pid = rel['parent_mid']
  6481. cid = rel['child_mid']
  6482. parent = members.get(pid)
  6483. child = members.get(cid)
  6484. if not parent or not child:
  6485. continue
  6486. parent_gens = _parse_generation_labels(parent.get('name_word_generation') or '')
  6487. child_gens = _parse_generation_labels(child.get('name_word_generation') or '')
  6488. if not parent_gens or not child_gens:
  6489. # 至少一方没有可解析的代数,记为警告
  6490. if parent.get('name_word_generation') or child.get('name_word_generation'):
  6491. warnings.append({
  6492. 'type': 'missing_gen',
  6493. 'parent_id': pid,
  6494. 'parent_name': parent.get('simplified_name') or parent.get('name'),
  6495. 'parent_gen_raw': parent.get('name_word_generation') or '',
  6496. 'child_id': cid,
  6497. 'child_name': child.get('simplified_name') or child.get('name'),
  6498. 'child_gen_raw': child.get('name_word_generation') or '',
  6499. })
  6500. continue
  6501. # 对每个共有的支系前缀检查是否 child_gen == parent_gen + 1
  6502. parent_map = dict(parent_gens)
  6503. child_map = dict(child_gens)
  6504. common_prefixes = set(parent_map.keys()) & set(child_map.keys())
  6505. if not common_prefixes:
  6506. # 前缀不同,可能是跨支系,不强制报错
  6507. continue
  6508. for prefix in sorted(common_prefixes):
  6509. p_gen = parent_map[prefix]
  6510. c_gen = child_map[prefix]
  6511. if c_gen != p_gen + 1:
  6512. errors.append({
  6513. 'type': 'discontinuous',
  6514. 'prefix': prefix,
  6515. 'parent_id': pid,
  6516. 'parent_name': parent.get('simplified_name') or parent.get('name'),
  6517. 'parent_gen': p_gen,
  6518. 'parent_gen_raw': parent.get('name_word_generation') or '',
  6519. 'child_id': cid,
  6520. 'child_name': child.get('simplified_name') or child.get('name'),
  6521. 'child_gen': c_gen,
  6522. 'child_gen_raw': child.get('name_word_generation') or '',
  6523. 'expected_child_gen': p_gen + 1,
  6524. 'diff': c_gen - p_gen,
  6525. })
  6526. # 按支系前缀和父代数排序
  6527. errors.sort(key=lambda x: (x['prefix'], x['parent_gen']))
  6528. warnings.sort(key=lambda x: (x['parent_id']))
  6529. return jsonify({
  6530. "success": True,
  6531. "error_count": len(errors),
  6532. "warning_count": len(warnings),
  6533. "errors": errors,
  6534. "warnings": warnings,
  6535. })
  6536. except Exception as e:
  6537. import traceback
  6538. return jsonify({"success": False, "message": str(e), "trace": traceback.format_exc()}), 500
  6539. finally:
  6540. conn.close()
  6541. # ==================== End 代数连续性核查 ====================
  6542. if __name__ == '__main__':
  6543. app.run(debug=False, host='0.0.0.0', port=5001)