| 1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889909192939495969798991001011021031041051061071081091101111121131141151161171181191201211221231241251261271281291301311321331341351361371381391401411421431441451461471481491501511521531541551561571581591601611621631641651661671681691701711721731741751761771781791801811821831841851861871881891901911921931941951961971981992002012022032042052062072082092102112122132142152162172182192202212222232242252262272282292302312322332342352362372382392402412422432442452462472482492502512522532542552562572582592602612622632642652662672682692702712722732742752762772782792802812822832842852862872882892902912922932942952962972982993003013023033043053063073083093103113123133143153163173183193203213223233243253263273283293303313323333343353363373383393403413423433443453463473483493503513523533543553563573583593603613623633643653663673683693703713723733743753763773783793803813823833843853863873883893903913923933943953963973983994004014024034044054064074084094104114124134144154164174184194204214224234244254264274284294304314324334344354364374384394404414424434444454464474484494504514524534544554564574584594604614624634644654664674684694704714724734744754764774784794804814824834844854864874884894904914924934944954964974984995005015025035045055065075085095105115125135145155165175185195205215225235245255265275285295305315325335345355365375385395405415425435445455465475485495505515525535545555565575585595605615625635645655665675685695705715725735745755765775785795805815825835845855865875885895905915925935945955965975985996006016026036046056066076086096106116126136146156166176186196206216226236246256266276286296306316326336346356366376386396406416426436446456466476486496506516526536546556566576586596606616626636646656666676686696706716726736746756766776786796806816826836846856866876886896906916926936946956966976986997007017027037047057067077087097107117127137147157167177187197207217227237247257267277287297307317327337347357367377387397407417427437447457467477487497507517527537547557567577587597607617627637647657667677687697707717727737747757767777787797807817827837847857867877887897907917927937947957967977987998008018028038048058068078088098108118128138148158168178188198208218228238248258268278288298308318328338348358368378388398408418428438448458468478488498508518528538548558568578588598608618628638648658668678688698708718728738748758768778788798808818828838848858868878888898908918928938948958968978988999009019029039049059069079089099109119129139149159169179189199209219229239249259269279289299309319329339349359369379389399409419429439449459469479489499509519529539549559569579589599609619629639649659669679689699709719729739749759769779789799809819829839849859869879889899909919929939949959969979989991000100110021003100410051006100710081009101010111012101310141015101610171018101910201021102210231024102510261027102810291030103110321033103410351036103710381039104010411042104310441045104610471048104910501051105210531054105510561057105810591060106110621063106410651066106710681069107010711072107310741075107610771078107910801081108210831084108510861087108810891090109110921093109410951096109710981099110011011102110311041105110611071108110911101111111211131114111511161117111811191120112111221123112411251126112711281129113011311132113311341135113611371138113911401141114211431144114511461147114811491150115111521153115411551156115711581159116011611162116311641165116611671168116911701171117211731174117511761177117811791180118111821183118411851186118711881189119011911192119311941195119611971198119912001201120212031204120512061207120812091210121112121213121412151216121712181219122012211222122312241225122612271228122912301231123212331234123512361237123812391240124112421243124412451246124712481249125012511252125312541255125612571258125912601261126212631264126512661267126812691270127112721273127412751276127712781279128012811282128312841285128612871288128912901291129212931294129512961297129812991300130113021303130413051306130713081309131013111312131313141315131613171318131913201321132213231324132513261327132813291330133113321333133413351336133713381339134013411342134313441345134613471348134913501351135213531354135513561357135813591360136113621363136413651366136713681369137013711372137313741375137613771378137913801381138213831384138513861387138813891390139113921393139413951396139713981399140014011402140314041405140614071408140914101411141214131414141514161417141814191420142114221423142414251426142714281429143014311432143314341435143614371438143914401441144214431444144514461447144814491450145114521453145414551456145714581459146014611462146314641465146614671468146914701471147214731474147514761477147814791480148114821483148414851486148714881489149014911492149314941495149614971498149915001501150215031504150515061507150815091510151115121513151415151516151715181519152015211522152315241525152615271528152915301531153215331534153515361537153815391540154115421543154415451546154715481549155015511552155315541555155615571558155915601561156215631564156515661567156815691570157115721573157415751576157715781579158015811582158315841585158615871588158915901591159215931594159515961597159815991600160116021603160416051606160716081609161016111612161316141615161616171618161916201621162216231624162516261627162816291630163116321633163416351636163716381639164016411642164316441645164616471648164916501651165216531654165516561657165816591660166116621663166416651666166716681669167016711672167316741675167616771678167916801681168216831684168516861687168816891690169116921693169416951696169716981699170017011702170317041705170617071708170917101711171217131714171517161717171817191720172117221723172417251726172717281729173017311732173317341735173617371738173917401741174217431744174517461747174817491750175117521753175417551756175717581759176017611762176317641765176617671768176917701771177217731774177517761777177817791780178117821783178417851786178717881789179017911792179317941795179617971798179918001801180218031804180518061807180818091810181118121813181418151816181718181819182018211822182318241825182618271828182918301831183218331834183518361837183818391840184118421843184418451846184718481849185018511852185318541855185618571858185918601861186218631864186518661867186818691870187118721873187418751876187718781879188018811882188318841885188618871888188918901891189218931894189518961897189818991900190119021903190419051906190719081909191019111912191319141915191619171918191919201921192219231924192519261927192819291930193119321933193419351936193719381939194019411942194319441945194619471948194919501951195219531954195519561957195819591960196119621963196419651966196719681969197019711972197319741975197619771978197919801981198219831984198519861987198819891990199119921993199419951996199719981999200020012002200320042005200620072008200920102011201220132014201520162017201820192020202120222023202420252026202720282029203020312032203320342035203620372038203920402041204220432044204520462047204820492050205120522053205420552056205720582059206020612062206320642065206620672068206920702071207220732074207520762077207820792080208120822083208420852086208720882089209020912092209320942095209620972098209921002101210221032104210521062107210821092110211121122113211421152116211721182119212021212122212321242125212621272128212921302131213221332134213521362137213821392140214121422143214421452146214721482149215021512152215321542155215621572158215921602161216221632164216521662167216821692170217121722173217421752176217721782179218021812182218321842185218621872188218921902191219221932194219521962197219821992200220122022203220422052206220722082209221022112212221322142215221622172218221922202221222222232224222522262227222822292230223122322233223422352236223722382239224022412242224322442245224622472248224922502251225222532254225522562257225822592260226122622263226422652266226722682269227022712272227322742275227622772278227922802281228222832284228522862287228822892290229122922293229422952296229722982299230023012302230323042305230623072308230923102311231223132314231523162317231823192320232123222323232423252326232723282329233023312332233323342335233623372338233923402341234223432344234523462347234823492350235123522353235423552356235723582359236023612362236323642365236623672368236923702371237223732374237523762377237823792380238123822383238423852386238723882389239023912392239323942395239623972398239924002401240224032404240524062407240824092410241124122413241424152416241724182419242024212422242324242425242624272428242924302431243224332434243524362437243824392440244124422443244424452446244724482449245024512452245324542455245624572458245924602461246224632464246524662467246824692470247124722473247424752476247724782479248024812482248324842485248624872488248924902491249224932494249524962497249824992500250125022503250425052506250725082509251025112512251325142515251625172518251925202521252225232524252525262527252825292530253125322533253425352536253725382539254025412542254325442545254625472548254925502551255225532554255525562557255825592560256125622563256425652566256725682569257025712572257325742575257625772578257925802581258225832584258525862587258825892590259125922593259425952596259725982599260026012602260326042605260626072608260926102611261226132614261526162617261826192620262126222623262426252626262726282629263026312632263326342635263626372638263926402641264226432644264526462647264826492650265126522653265426552656265726582659266026612662266326642665266626672668266926702671267226732674267526762677267826792680268126822683268426852686268726882689269026912692269326942695269626972698269927002701270227032704270527062707270827092710271127122713271427152716271727182719272027212722272327242725272627272728272927302731273227332734273527362737273827392740274127422743274427452746274727482749275027512752275327542755275627572758275927602761276227632764276527662767276827692770277127722773277427752776277727782779278027812782278327842785278627872788278927902791279227932794279527962797279827992800280128022803280428052806280728082809281028112812281328142815281628172818281928202821282228232824282528262827282828292830283128322833283428352836283728382839284028412842284328442845284628472848284928502851285228532854285528562857285828592860286128622863286428652866286728682869287028712872287328742875287628772878287928802881288228832884288528862887288828892890289128922893289428952896289728982899290029012902290329042905290629072908290929102911291229132914291529162917291829192920292129222923292429252926292729282929293029312932293329342935293629372938293929402941294229432944294529462947294829492950295129522953295429552956295729582959296029612962296329642965296629672968296929702971297229732974297529762977297829792980298129822983298429852986298729882989299029912992299329942995299629972998299930003001300230033004300530063007300830093010301130123013301430153016301730183019302030213022302330243025302630273028302930303031303230333034303530363037303830393040304130423043304430453046304730483049305030513052305330543055305630573058305930603061306230633064306530663067306830693070307130723073307430753076307730783079308030813082308330843085308630873088308930903091309230933094309530963097309830993100310131023103310431053106310731083109311031113112311331143115311631173118311931203121312231233124312531263127312831293130313131323133313431353136313731383139314031413142314331443145314631473148314931503151315231533154315531563157315831593160316131623163316431653166316731683169317031713172317331743175317631773178317931803181318231833184318531863187318831893190319131923193319431953196319731983199320032013202320332043205320632073208320932103211321232133214321532163217321832193220322132223223322432253226322732283229323032313232323332343235323632373238323932403241324232433244324532463247324832493250325132523253325432553256325732583259326032613262326332643265326632673268326932703271327232733274327532763277327832793280328132823283328432853286328732883289329032913292329332943295329632973298329933003301330233033304330533063307330833093310331133123313331433153316331733183319332033213322332333243325332633273328332933303331333233333334333533363337333833393340334133423343334433453346334733483349335033513352335333543355335633573358335933603361336233633364336533663367336833693370337133723373337433753376337733783379338033813382338333843385338633873388338933903391339233933394339533963397339833993400340134023403340434053406340734083409341034113412341334143415341634173418341934203421342234233424342534263427342834293430343134323433343434353436343734383439344034413442344334443445344634473448344934503451345234533454345534563457345834593460346134623463346434653466346734683469347034713472347334743475347634773478347934803481348234833484348534863487348834893490349134923493349434953496349734983499350035013502350335043505350635073508350935103511351235133514351535163517351835193520352135223523352435253526352735283529353035313532353335343535353635373538353935403541354235433544354535463547354835493550355135523553355435553556355735583559356035613562356335643565356635673568356935703571357235733574357535763577357835793580358135823583358435853586358735883589359035913592359335943595359635973598359936003601360236033604360536063607360836093610361136123613361436153616361736183619362036213622362336243625362636273628362936303631363236333634363536363637363836393640364136423643364436453646364736483649365036513652365336543655365636573658365936603661366236633664366536663667366836693670367136723673367436753676367736783679368036813682368336843685368636873688368936903691369236933694369536963697369836993700370137023703370437053706370737083709371037113712371337143715371637173718371937203721372237233724372537263727372837293730373137323733373437353736373737383739374037413742374337443745374637473748374937503751375237533754375537563757375837593760376137623763376437653766376737683769377037713772377337743775377637773778377937803781378237833784378537863787378837893790379137923793379437953796379737983799380038013802380338043805380638073808380938103811381238133814381538163817381838193820382138223823382438253826382738283829383038313832383338343835383638373838383938403841384238433844384538463847384838493850385138523853385438553856385738583859386038613862386338643865386638673868386938703871387238733874387538763877387838793880388138823883388438853886388738883889389038913892389338943895389638973898389939003901390239033904390539063907390839093910391139123913391439153916391739183919392039213922392339243925392639273928392939303931393239333934393539363937393839393940394139423943394439453946394739483949395039513952395339543955395639573958395939603961396239633964396539663967396839693970397139723973397439753976397739783979398039813982398339843985398639873988398939903991399239933994399539963997399839994000400140024003400440054006400740084009401040114012401340144015401640174018401940204021402240234024402540264027402840294030403140324033403440354036403740384039404040414042404340444045404640474048404940504051405240534054405540564057405840594060406140624063406440654066406740684069407040714072407340744075407640774078407940804081408240834084408540864087408840894090409140924093409440954096409740984099410041014102410341044105410641074108410941104111411241134114411541164117411841194120412141224123412441254126412741284129413041314132413341344135413641374138413941404141414241434144414541464147414841494150415141524153415441554156415741584159416041614162416341644165416641674168416941704171417241734174417541764177417841794180418141824183418441854186418741884189419041914192419341944195419641974198419942004201420242034204420542064207420842094210421142124213421442154216421742184219422042214222422342244225422642274228422942304231423242334234423542364237423842394240424142424243424442454246424742484249425042514252425342544255425642574258425942604261426242634264426542664267426842694270427142724273427442754276427742784279428042814282428342844285428642874288428942904291429242934294429542964297429842994300430143024303430443054306430743084309431043114312431343144315431643174318431943204321432243234324432543264327432843294330433143324333433443354336433743384339434043414342434343444345434643474348434943504351435243534354435543564357435843594360436143624363436443654366436743684369437043714372437343744375437643774378437943804381438243834384438543864387438843894390439143924393439443954396439743984399440044014402440344044405440644074408440944104411441244134414441544164417441844194420442144224423442444254426442744284429443044314432443344344435443644374438443944404441444244434444444544464447444844494450445144524453445444554456445744584459446044614462446344644465446644674468446944704471447244734474447544764477447844794480448144824483448444854486448744884489449044914492449344944495449644974498449945004501450245034504450545064507450845094510451145124513451445154516451745184519452045214522452345244525452645274528452945304531453245334534453545364537453845394540454145424543454445454546454745484549455045514552455345544555455645574558455945604561456245634564456545664567456845694570457145724573457445754576457745784579458045814582458345844585458645874588458945904591459245934594459545964597459845994600460146024603460446054606460746084609461046114612461346144615461646174618461946204621462246234624462546264627462846294630463146324633463446354636463746384639464046414642464346444645464646474648464946504651465246534654465546564657465846594660466146624663466446654666466746684669467046714672467346744675467646774678467946804681468246834684468546864687468846894690469146924693469446954696469746984699470047014702470347044705470647074708470947104711471247134714471547164717471847194720472147224723472447254726472747284729473047314732473347344735473647374738473947404741474247434744474547464747474847494750475147524753475447554756475747584759476047614762476347644765476647674768476947704771477247734774477547764777477847794780478147824783478447854786478747884789479047914792479347944795479647974798479948004801480248034804480548064807480848094810481148124813481448154816481748184819482048214822482348244825482648274828482948304831483248334834483548364837483848394840484148424843484448454846484748484849485048514852485348544855485648574858485948604861486248634864486548664867486848694870487148724873487448754876487748784879488048814882488348844885488648874888488948904891489248934894489548964897489848994900490149024903490449054906490749084909491049114912491349144915491649174918491949204921492249234924492549264927492849294930493149324933493449354936493749384939494049414942494349444945494649474948494949504951495249534954495549564957495849594960496149624963496449654966496749684969497049714972497349744975497649774978497949804981498249834984498549864987498849894990499149924993499449954996499749984999500050015002500350045005500650075008500950105011501250135014501550165017501850195020502150225023502450255026502750285029503050315032503350345035503650375038503950405041504250435044504550465047504850495050505150525053505450555056505750585059506050615062506350645065506650675068506950705071507250735074507550765077507850795080508150825083508450855086508750885089509050915092509350945095509650975098509951005101510251035104510551065107510851095110511151125113511451155116511751185119512051215122512351245125512651275128512951305131513251335134513551365137513851395140514151425143514451455146514751485149515051515152515351545155515651575158515951605161516251635164516551665167516851695170517151725173517451755176517751785179518051815182518351845185518651875188518951905191519251935194519551965197519851995200520152025203520452055206520752085209521052115212521352145215521652175218521952205221522252235224522552265227522852295230523152325233523452355236523752385239524052415242524352445245524652475248524952505251525252535254525552565257525852595260526152625263526452655266526752685269527052715272527352745275527652775278527952805281528252835284528552865287528852895290529152925293529452955296529752985299530053015302530353045305530653075308530953105311531253135314531553165317531853195320532153225323532453255326532753285329533053315332533353345335533653375338533953405341534253435344534553465347534853495350535153525353535453555356535753585359536053615362536353645365536653675368536953705371537253735374537553765377537853795380538153825383538453855386538753885389539053915392539353945395539653975398539954005401540254035404540554065407540854095410541154125413541454155416541754185419542054215422542354245425542654275428542954305431543254335434543554365437543854395440544154425443544454455446544754485449545054515452545354545455545654575458545954605461546254635464546554665467546854695470547154725473547454755476547754785479548054815482548354845485548654875488548954905491549254935494549554965497549854995500550155025503550455055506550755085509551055115512551355145515551655175518551955205521552255235524552555265527552855295530553155325533553455355536553755385539554055415542554355445545554655475548554955505551555255535554555555565557555855595560556155625563556455655566556755685569557055715572557355745575557655775578557955805581558255835584558555865587558855895590559155925593559455955596559755985599560056015602560356045605560656075608560956105611561256135614561556165617561856195620562156225623562456255626562756285629563056315632563356345635563656375638563956405641564256435644564556465647564856495650565156525653565456555656565756585659566056615662566356645665566656675668566956705671567256735674567556765677567856795680568156825683568456855686568756885689569056915692569356945695569656975698569957005701570257035704570557065707570857095710571157125713571457155716571757185719572057215722572357245725572657275728572957305731573257335734573557365737573857395740574157425743574457455746574757485749575057515752575357545755575657575758575957605761576257635764576557665767576857695770577157725773577457755776577757785779578057815782578357845785578657875788578957905791579257935794579557965797579857995800580158025803580458055806580758085809581058115812581358145815581658175818581958205821582258235824582558265827582858295830583158325833583458355836583758385839584058415842584358445845584658475848584958505851585258535854585558565857585858595860586158625863586458655866586758685869587058715872587358745875587658775878587958805881588258835884588558865887588858895890589158925893589458955896589758985899590059015902590359045905590659075908590959105911591259135914591559165917591859195920592159225923592459255926592759285929593059315932593359345935593659375938593959405941594259435944594559465947594859495950595159525953595459555956595759585959596059615962596359645965596659675968596959705971597259735974597559765977597859795980598159825983598459855986598759885989599059915992599359945995599659975998599960006001600260036004600560066007600860096010601160126013601460156016601760186019602060216022602360246025602660276028602960306031603260336034603560366037603860396040604160426043604460456046604760486049605060516052605360546055605660576058605960606061606260636064606560666067606860696070607160726073607460756076607760786079608060816082608360846085608660876088608960906091609260936094609560966097609860996100610161026103610461056106610761086109611061116112611361146115611661176118611961206121612261236124612561266127612861296130613161326133613461356136613761386139614061416142614361446145614661476148614961506151615261536154615561566157615861596160616161626163616461656166616761686169617061716172617361746175617661776178617961806181618261836184618561866187618861896190619161926193619461956196619761986199620062016202620362046205620662076208620962106211621262136214621562166217621862196220622162226223622462256226622762286229623062316232623362346235623662376238623962406241624262436244624562466247624862496250625162526253625462556256625762586259626062616262626362646265626662676268626962706271627262736274627562766277627862796280628162826283628462856286628762886289629062916292629362946295629662976298629963006301630263036304630563066307630863096310631163126313631463156316631763186319632063216322632363246325632663276328632963306331633263336334633563366337633863396340634163426343634463456346634763486349635063516352635363546355635663576358635963606361636263636364636563666367636863696370637163726373637463756376637763786379638063816382638363846385638663876388638963906391639263936394639563966397639863996400640164026403640464056406640764086409641064116412641364146415641664176418641964206421642264236424642564266427642864296430643164326433643464356436643764386439644064416442644364446445644664476448644964506451645264536454645564566457645864596460646164626463646464656466646764686469647064716472647364746475647664776478647964806481648264836484648564866487648864896490649164926493649464956496649764986499650065016502650365046505650665076508650965106511651265136514651565166517651865196520652165226523652465256526652765286529653065316532653365346535653665376538653965406541654265436544654565466547654865496550655165526553655465556556655765586559656065616562656365646565656665676568656965706571657265736574657565766577657865796580658165826583658465856586658765886589659065916592659365946595659665976598659966006601660266036604660566066607660866096610661166126613661466156616661766186619662066216622662366246625662666276628662966306631663266336634663566366637663866396640664166426643664466456646664766486649665066516652665366546655665666576658665966606661666266636664666566666667666866696670667166726673667466756676667766786679668066816682668366846685668666876688668966906691669266936694669566966697669866996700670167026703670467056706670767086709671067116712671367146715671667176718671967206721672267236724672567266727672867296730673167326733673467356736673767386739674067416742674367446745674667476748674967506751675267536754675567566757675867596760676167626763676467656766676767686769677067716772677367746775677667776778677967806781678267836784678567866787678867896790679167926793679467956796679767986799680068016802680368046805680668076808680968106811681268136814681568166817681868196820682168226823682468256826682768286829683068316832683368346835683668376838683968406841684268436844684568466847684868496850685168526853685468556856685768586859686068616862686368646865686668676868686968706871687268736874687568766877687868796880688168826883688468856886688768886889689068916892689368946895689668976898689969006901690269036904690569066907690869096910691169126913691469156916691769186919692069216922692369246925692669276928692969306931693269336934693569366937693869396940694169426943694469456946694769486949695069516952695369546955695669576958695969606961696269636964696569666967696869696970697169726973697469756976697769786979698069816982698369846985698669876988698969906991699269936994699569966997699869997000700170027003700470057006700770087009701070117012701370147015701670177018701970207021702270237024702570267027702870297030703170327033703470357036703770387039704070417042704370447045704670477048704970507051705270537054705570567057705870597060706170627063706470657066706770687069707070717072707370747075707670777078707970807081708270837084708570867087708870897090709170927093709470957096709770987099710071017102710371047105710671077108710971107111711271137114711571167117711871197120712171227123712471257126712771287129713071317132713371347135713671377138713971407141714271437144714571467147714871497150715171527153715471557156715771587159716071617162716371647165716671677168716971707171717271737174717571767177717871797180718171827183718471857186718771887189719071917192719371947195719671977198719972007201720272037204720572067207720872097210721172127213721472157216721772187219722072217222722372247225722672277228722972307231723272337234723572367237723872397240724172427243724472457246724772487249725072517252725372547255725672577258725972607261726272637264726572667267726872697270727172727273727472757276727772787279728072817282728372847285728672877288728972907291729272937294729572967297729872997300730173027303730473057306730773087309731073117312731373147315731673177318731973207321732273237324732573267327732873297330733173327333733473357336733773387339734073417342734373447345734673477348734973507351735273537354735573567357735873597360736173627363736473657366736773687369737073717372737373747375737673777378737973807381738273837384738573867387738873897390739173927393739473957396739773987399740074017402740374047405740674077408740974107411741274137414741574167417741874197420742174227423742474257426742774287429743074317432743374347435743674377438743974407441744274437444744574467447744874497450745174527453745474557456745774587459746074617462746374647465746674677468746974707471747274737474747574767477747874797480748174827483748474857486748774887489749074917492749374947495749674977498749975007501750275037504750575067507750875097510751175127513751475157516751775187519752075217522752375247525752675277528752975307531753275337534753575367537753875397540754175427543754475457546754775487549755075517552755375547555755675577558755975607561756275637564756575667567756875697570757175727573757475757576757775787579758075817582758375847585758675877588758975907591759275937594759575967597759875997600760176027603760476057606760776087609761076117612761376147615761676177618761976207621762276237624762576267627762876297630763176327633763476357636763776387639764076417642764376447645764676477648764976507651765276537654765576567657765876597660766176627663766476657666766776687669767076717672767376747675767676777678767976807681768276837684768576867687768876897690769176927693769476957696769776987699770077017702770377047705770677077708770977107711771277137714771577167717771877197720772177227723772477257726772777287729773077317732773377347735773677377738773977407741774277437744774577467747774877497750775177527753775477557756775777587759776077617762776377647765776677677768776977707771777277737774777577767777777877797780778177827783778477857786778777887789779077917792779377947795779677977798779978007801780278037804780578067807780878097810781178127813781478157816781778187819782078217822782378247825782678277828782978307831783278337834783578367837783878397840784178427843784478457846784778487849785078517852785378547855785678577858785978607861786278637864786578667867786878697870787178727873787478757876787778787879788078817882788378847885788678877888788978907891789278937894789578967897789878997900790179027903790479057906790779087909791079117912791379147915791679177918791979207921792279237924792579267927792879297930793179327933793479357936793779387939794079417942794379447945794679477948794979507951795279537954795579567957795879597960796179627963796479657966796779687969797079717972797379747975797679777978797979807981798279837984798579867987798879897990799179927993799479957996799779987999800080018002800380048005800680078008800980108011801280138014801580168017801880198020802180228023802480258026802780288029803080318032803380348035803680378038803980408041804280438044804580468047804880498050805180528053805480558056805780588059806080618062806380648065806680678068806980708071807280738074807580768077807880798080808180828083808480858086808780888089809080918092809380948095809680978098809981008101810281038104810581068107810881098110811181128113811481158116811781188119812081218122812381248125812681278128812981308131813281338134813581368137813881398140814181428143814481458146814781488149815081518152815381548155815681578158815981608161816281638164816581668167816881698170817181728173817481758176817781788179818081818182818381848185818681878188818981908191819281938194819581968197819881998200820182028203820482058206820782088209821082118212821382148215821682178218821982208221822282238224822582268227822882298230823182328233823482358236823782388239824082418242824382448245824682478248824982508251825282538254825582568257825882598260826182628263826482658266826782688269827082718272827382748275827682778278827982808281828282838284828582868287828882898290829182928293829482958296829782988299830083018302830383048305830683078308830983108311831283138314831583168317831883198320832183228323832483258326832783288329833083318332833383348335833683378338833983408341834283438344834583468347834883498350835183528353835483558356835783588359836083618362836383648365836683678368836983708371837283738374837583768377837883798380838183828383838483858386838783888389839083918392839383948395839683978398839984008401840284038404840584068407840884098410841184128413841484158416841784188419842084218422842384248425842684278428842984308431843284338434843584368437843884398440844184428443844484458446844784488449845084518452845384548455845684578458845984608461846284638464846584668467846884698470847184728473847484758476847784788479848084818482848384848485848684878488848984908491849284938494849584968497849884998500850185028503850485058506850785088509851085118512851385148515851685178518851985208521852285238524852585268527852885298530853185328533853485358536853785388539854085418542854385448545854685478548854985508551855285538554855585568557855885598560856185628563856485658566856785688569857085718572857385748575857685778578857985808581858285838584858585868587858885898590859185928593859485958596859785988599860086018602860386048605860686078608860986108611861286138614861586168617861886198620862186228623862486258626862786288629863086318632863386348635863686378638863986408641864286438644864586468647864886498650865186528653865486558656865786588659866086618662866386648665866686678668866986708671867286738674867586768677867886798680868186828683868486858686868786888689869086918692869386948695869686978698869987008701870287038704870587068707870887098710871187128713871487158716871787188719872087218722872387248725872687278728872987308731873287338734873587368737873887398740874187428743874487458746874787488749875087518752875387548755875687578758875987608761876287638764876587668767876887698770877187728773877487758776877787788779878087818782878387848785878687878788878987908791879287938794879587968797879887998800880188028803880488058806880788088809881088118812881388148815881688178818881988208821882288238824882588268827882888298830883188328833883488358836883788388839884088418842884388448845884688478848884988508851885288538854885588568857885888598860886188628863886488658866886788688869887088718872887388748875887688778878887988808881888288838884888588868887888888898890889188928893889488958896889788988899890089018902890389048905890689078908890989108911891289138914891589168917891889198920892189228923892489258926892789288929893089318932893389348935893689378938893989408941894289438944894589468947894889498950895189528953895489558956895789588959896089618962896389648965896689678968896989708971897289738974897589768977897889798980898189828983898489858986898789888989899089918992899389948995899689978998899990009001900290039004900590069007900890099010901190129013901490159016901790189019902090219022902390249025902690279028902990309031903290339034903590369037903890399040904190429043904490459046904790489049905090519052905390549055905690579058905990609061906290639064906590669067906890699070907190729073907490759076907790789079908090819082908390849085908690879088908990909091909290939094909590969097909890999100910191029103910491059106910791089109911091119112911391149115911691179118911991209121912291239124912591269127912891299130913191329133913491359136913791389139914091419142914391449145914691479148914991509151915291539154915591569157915891599160916191629163916491659166916791689169917091719172917391749175917691779178917991809181918291839184918591869187918891899190919191929193919491959196919791989199920092019202920392049205920692079208920992109211921292139214921592169217921892199220922192229223922492259226922792289229923092319232923392349235923692379238923992409241924292439244924592469247924892499250925192529253925492559256925792589259926092619262926392649265926692679268926992709271927292739274927592769277927892799280928192829283928492859286928792889289929092919292929392949295929692979298929993009301930293039304930593069307930893099310931193129313931493159316931793189319932093219322932393249325932693279328932993309331933293339334933593369337933893399340934193429343934493459346934793489349935093519352935393549355935693579358935993609361936293639364936593669367936893699370937193729373937493759376937793789379938093819382938393849385938693879388938993909391939293939394939593969397939893999400940194029403940494059406940794089409941094119412941394149415941694179418941994209421942294239424942594269427942894299430943194329433943494359436943794389439944094419442944394449445944694479448944994509451945294539454945594569457945894599460946194629463946494659466946794689469947094719472947394749475947694779478947994809481948294839484948594869487948894899490949194929493949494959496949794989499950095019502950395049505950695079508950995109511951295139514951595169517951895199520952195229523952495259526952795289529953095319532953395349535953695379538953995409541954295439544954595469547954895499550955195529553955495559556955795589559956095619562956395649565956695679568956995709571957295739574957595769577957895799580958195829583958495859586958795889589959095919592959395949595959695979598959996009601960296039604960596069607960896099610961196129613961496159616961796189619962096219622962396249625962696279628962996309631963296339634963596369637963896399640964196429643964496459646964796489649965096519652965396549655965696579658965996609661966296639664966596669667966896699670967196729673967496759676967796789679968096819682968396849685968696879688968996909691969296939694969596969697969896999700970197029703970497059706970797089709971097119712971397149715971697179718971997209721972297239724972597269727972897299730973197329733973497359736973797389739974097419742974397449745974697479748974997509751975297539754975597569757975897599760976197629763976497659766976797689769977097719772977397749775977697779778977997809781978297839784978597869787978897899790979197929793979497959796979797989799980098019802980398049805980698079808980998109811981298139814981598169817981898199820982198229823982498259826982798289829983098319832983398349835983698379838983998409841984298439844984598469847984898499850985198529853985498559856985798589859986098619862986398649865986698679868986998709871987298739874987598769877987898799880988198829883988498859886988798889889989098919892989398949895989698979898989999009901990299039904990599069907990899099910991199129913991499159916991799189919992099219922992399249925992699279928992999309931993299339934993599369937993899399940994199429943994499459946994799489949995099519952995399549955995699579958995999609961996299639964996599669967996899699970997199729973997499759976997799789979998099819982998399849985998699879988998999909991999299939994999599969997999899991000010001100021000310004100051000610007100081000910010100111001210013100141001510016100171001810019100201002110022100231002410025100261002710028100291003010031100321003310034100351003610037100381003910040100411004210043100441004510046100471004810049100501005110052100531005410055100561005710058100591006010061100621006310064100651006610067100681006910070100711007210073100741007510076100771007810079100801008110082100831008410085100861008710088100891009010091100921009310094100951009610097100981009910100101011010210103101041010510106101071010810109101101011110112101131011410115101161011710118101191012010121101221012310124101251012610127101281012910130101311013210133101341013510136101371013810139101401014110142101431014410145101461014710148101491015010151101521015310154101551015610157101581015910160101611016210163101641016510166101671016810169101701017110172101731017410175101761017710178101791018010181101821018310184101851018610187101881018910190101911019210193101941019510196101971019810199102001020110202102031020410205102061020710208102091021010211102121021310214102151021610217102181021910220102211022210223102241022510226102271022810229102301023110232102331023410235102361023710238102391024010241102421024310244102451024610247102481024910250102511025210253102541025510256102571025810259102601026110262102631026410265102661026710268102691027010271102721027310274102751027610277102781027910280102811028210283102841028510286102871028810289102901029110292102931029410295102961029710298102991030010301103021030310304103051030610307103081030910310103111031210313103141031510316103171031810319103201032110322103231032410325103261032710328103291033010331103321033310334103351033610337103381033910340103411034210343103441034510346103471034810349103501035110352103531035410355103561035710358103591036010361103621036310364103651036610367103681036910370103711037210373103741037510376103771037810379103801038110382103831038410385103861038710388103891039010391103921039310394103951039610397103981039910400104011040210403104041040510406104071040810409104101041110412104131041410415104161041710418104191042010421104221042310424104251042610427104281042910430104311043210433104341043510436104371043810439104401044110442104431044410445104461044710448104491045010451104521045310454104551045610457104581045910460104611046210463104641046510466104671046810469104701047110472104731047410475104761047710478104791048010481104821048310484104851048610487104881048910490104911049210493104941049510496104971049810499105001050110502105031050410505105061050710508105091051010511105121051310514105151051610517105181051910520105211052210523105241052510526105271052810529105301053110532105331053410535105361053710538105391054010541105421054310544105451054610547105481054910550105511055210553105541055510556105571055810559105601056110562105631056410565105661056710568105691057010571105721057310574105751057610577105781057910580105811058210583105841058510586105871058810589105901059110592105931059410595105961059710598105991060010601106021060310604106051060610607106081060910610106111061210613106141061510616106171061810619106201062110622106231062410625106261062710628106291063010631106321063310634106351063610637106381063910640106411064210643106441064510646106471064810649106501065110652106531065410655106561065710658106591066010661106621066310664106651066610667106681066910670106711067210673106741067510676106771067810679106801068110682106831068410685106861068710688106891069010691106921069310694106951069610697106981069910700107011070210703107041070510706107071070810709107101071110712107131071410715107161071710718107191072010721107221072310724107251072610727107281072910730107311073210733107341073510736107371073810739107401074110742107431074410745107461074710748107491075010751107521075310754107551075610757107581075910760107611076210763107641076510766107671076810769107701077110772107731077410775107761077710778107791078010781107821078310784107851078610787107881078910790107911079210793107941079510796107971079810799108001080110802108031080410805108061080710808108091081010811108121081310814108151081610817108181081910820108211082210823108241082510826108271082810829108301083110832108331083410835108361083710838108391084010841108421084310844108451084610847108481084910850108511085210853108541085510856108571085810859108601086110862108631086410865108661086710868108691087010871108721087310874108751087610877108781087910880108811088210883108841088510886108871088810889108901089110892108931089410895108961089710898108991090010901109021090310904109051090610907109081090910910109111091210913109141091510916109171091810919109201092110922109231092410925109261092710928109291093010931109321093310934109351093610937109381093910940109411094210943109441094510946109471094810949109501095110952109531095410955109561095710958109591096010961109621096310964109651096610967109681096910970109711097210973109741097510976109771097810979109801098110982109831098410985109861098710988109891099010991109921099310994109951099610997109981099911000110011100211003110041100511006110071100811009110101101111012110131101411015110161101711018110191102011021110221102311024110251102611027110281102911030110311103211033110341103511036110371103811039110401104111042110431104411045110461104711048110491105011051110521105311054110551105611057110581105911060110611106211063110641106511066110671106811069110701107111072110731107411075110761107711078110791108011081110821108311084110851108611087110881108911090110911109211093110941109511096110971109811099111001110111102111031110411105111061110711108111091111011111111121111311114111151111611117111181111911120111211112211123111241112511126111271112811129111301113111132111331113411135111361113711138111391114011141111421114311144111451114611147111481114911150111511115211153111541115511156111571115811159111601116111162111631116411165111661116711168111691117011171111721117311174111751117611177111781117911180111811118211183111841118511186111871118811189111901119111192111931119411195111961119711198111991120011201112021120311204112051120611207112081120911210112111121211213112141121511216112171121811219112201122111222112231122411225112261122711228112291123011231112321123311234112351123611237112381123911240112411124211243112441124511246112471124811249112501125111252112531125411255112561125711258112591126011261112621126311264112651126611267112681126911270112711127211273112741127511276112771127811279112801128111282112831128411285112861128711288112891129011291112921129311294112951129611297112981129911300113011130211303113041130511306113071130811309113101131111312113131131411315113161131711318113191132011321113221132311324113251132611327113281132911330113311133211333113341133511336113371133811339113401134111342113431134411345113461134711348113491135011351113521135311354113551135611357113581135911360113611136211363113641136511366113671136811369113701137111372113731137411375113761137711378113791138011381113821138311384113851138611387113881138911390113911139211393113941139511396113971139811399114001140111402114031140411405114061140711408114091141011411114121141311414114151141611417114181141911420114211142211423114241142511426114271142811429114301143111432114331143411435114361143711438114391144011441114421144311444114451144611447114481144911450114511145211453114541145511456114571145811459114601146111462114631146411465114661146711468114691147011471114721147311474114751147611477114781147911480114811148211483114841148511486114871148811489114901149111492114931149411495114961149711498114991150011501115021150311504115051150611507115081150911510115111151211513115141151511516115171151811519115201152111522115231152411525115261152711528115291153011531115321153311534115351153611537115381153911540115411154211543115441154511546115471154811549115501155111552115531155411555115561155711558115591156011561115621156311564115651156611567115681156911570115711157211573115741157511576115771157811579115801158111582115831158411585115861158711588115891159011591115921159311594115951159611597115981159911600116011160211603116041160511606116071160811609116101161111612116131161411615116161161711618116191162011621116221162311624116251162611627116281162911630116311163211633116341163511636116371163811639116401164111642116431164411645116461164711648116491165011651116521165311654116551165611657116581165911660116611166211663116641166511666116671166811669116701167111672116731167411675116761167711678116791168011681116821168311684116851168611687116881168911690116911169211693116941169511696116971169811699117001170111702117031170411705117061170711708117091171011711117121171311714117151171611717117181171911720117211172211723117241172511726117271172811729117301173111732117331173411735117361173711738117391174011741117421174311744117451174611747117481174911750117511175211753117541175511756117571175811759117601176111762117631176411765117661176711768117691177011771117721177311774117751177611777117781177911780117811178211783117841178511786117871178811789117901179111792117931179411795117961179711798117991180011801118021180311804118051180611807118081180911810118111181211813118141181511816118171181811819118201182111822118231182411825118261182711828118291183011831118321183311834118351183611837118381183911840118411184211843118441184511846118471184811849118501185111852118531185411855118561185711858118591186011861118621186311864118651186611867118681186911870118711187211873118741187511876118771187811879118801188111882118831188411885118861188711888118891189011891118921189311894118951189611897118981189911900119011190211903119041190511906119071190811909119101191111912119131191411915119161191711918119191192011921119221192311924119251192611927119281192911930119311193211933119341193511936119371193811939119401194111942119431194411945119461194711948119491195011951119521195311954119551195611957119581195911960119611196211963119641196511966119671196811969119701197111972119731197411975119761197711978119791198011981119821198311984119851198611987119881198911990119911199211993119941199511996119971199811999120001200112002120031200412005120061200712008120091201012011120121201312014120151201612017120181201912020120211202212023120241202512026120271202812029120301203112032120331203412035120361203712038120391204012041120421204312044120451204612047120481204912050120511205212053120541205512056120571205812059120601206112062120631206412065120661206712068120691207012071120721207312074120751207612077120781207912080120811208212083120841208512086120871208812089120901209112092120931209412095120961209712098120991210012101121021210312104121051210612107121081210912110121111211212113121141211512116121171211812119121201212112122121231212412125121261212712128121291213012131121321213312134121351213612137121381213912140121411214212143121441214512146121471214812149121501215112152121531215412155121561215712158121591216012161121621216312164121651216612167121681216912170121711217212173121741217512176121771217812179121801218112182121831218412185121861218712188121891219012191121921219312194121951219612197121981219912200122011220212203122041220512206122071220812209122101221112212122131221412215122161221712218122191222012221122221222312224122251222612227122281222912230122311223212233122341223512236122371223812239122401224112242122431224412245122461224712248122491225012251122521225312254122551225612257122581225912260122611226212263122641226512266122671226812269122701227112272122731227412275122761227712278122791228012281122821228312284122851228612287122881228912290122911229212293122941229512296122971229812299123001230112302123031230412305123061230712308123091231012311123121231312314123151231612317123181231912320123211232212323123241232512326123271232812329123301233112332123331233412335123361233712338123391234012341123421234312344123451234612347123481234912350123511235212353123541235512356123571235812359123601236112362123631236412365123661236712368123691237012371123721237312374123751237612377123781237912380123811238212383123841238512386123871238812389123901239112392123931239412395123961239712398123991240012401124021240312404124051240612407124081240912410124111241212413124141241512416124171241812419124201242112422124231242412425124261242712428124291243012431124321243312434124351243612437124381243912440124411244212443124441244512446124471244812449124501245112452124531245412455124561245712458124591246012461124621246312464124651246612467124681246912470124711247212473124741247512476124771247812479124801248112482124831248412485124861248712488124891249012491124921249312494124951249612497124981249912500125011250212503125041250512506125071250812509125101251112512125131251412515125161251712518125191252012521125221252312524125251252612527125281252912530125311253212533125341253512536125371253812539125401254112542125431254412545125461254712548125491255012551125521255312554125551255612557125581255912560125611256212563125641256512566125671256812569125701257112572125731257412575125761257712578125791258012581125821258312584125851258612587125881258912590125911259212593125941259512596125971259812599126001260112602126031260412605126061260712608126091261012611126121261312614126151261612617126181261912620126211262212623126241262512626126271262812629126301263112632126331263412635126361263712638126391264012641126421264312644126451264612647126481264912650126511265212653126541265512656126571265812659126601266112662126631266412665126661266712668126691267012671126721267312674126751267612677126781267912680126811268212683126841268512686126871268812689126901269112692126931269412695126961269712698126991270012701127021270312704127051270612707127081270912710127111271212713127141271512716127171271812719127201272112722127231272412725127261272712728127291273012731127321273312734127351273612737127381273912740127411274212743127441274512746127471274812749127501275112752127531275412755127561275712758127591276012761127621276312764127651276612767127681276912770127711277212773127741277512776127771277812779127801278112782127831278412785127861278712788127891279012791127921279312794127951279612797127981279912800128011280212803128041280512806128071280812809128101281112812128131281412815128161281712818128191282012821128221282312824128251282612827128281282912830128311283212833128341283512836128371283812839128401284112842128431284412845128461284712848128491285012851128521285312854128551285612857128581285912860128611286212863128641286512866128671286812869128701287112872128731287412875128761287712878128791288012881128821288312884128851288612887128881288912890128911289212893128941289512896128971289812899129001290112902129031290412905129061290712908129091291012911129121291312914129151291612917129181291912920129211292212923129241292512926129271292812929129301293112932129331293412935129361293712938129391294012941129421294312944129451294612947129481294912950129511295212953129541295512956129571295812959129601296112962129631296412965129661296712968129691297012971129721297312974129751297612977129781297912980129811298212983129841298512986129871298812989129901299112992129931299412995129961299712998129991300013001130021300313004130051300613007130081300913010130111301213013130141301513016130171301813019130201302113022130231302413025130261302713028130291303013031130321303313034130351303613037130381303913040130411304213043130441304513046130471304813049130501305113052130531305413055130561305713058130591306013061130621306313064130651306613067130681306913070130711307213073130741307513076130771307813079130801308113082130831308413085130861308713088130891309013091130921309313094130951309613097130981309913100131011310213103131041310513106131071310813109131101311113112131131311413115131161311713118131191312013121131221312313124131251312613127131281312913130131311313213133131341313513136131371313813139131401314113142131431314413145131461314713148131491315013151131521315313154131551315613157131581315913160131611316213163131641316513166131671316813169131701317113172131731317413175131761317713178131791318013181131821318313184131851318613187131881318913190131911319213193131941319513196131971319813199132001320113202132031320413205132061320713208132091321013211132121321313214132151321613217132181321913220132211322213223132241322513226132271322813229132301323113232132331323413235132361323713238132391324013241132421324313244132451324613247132481324913250132511325213253132541325513256132571325813259132601326113262132631326413265132661326713268132691327013271132721327313274132751327613277132781327913280132811328213283132841328513286132871328813289132901329113292132931329413295132961329713298132991330013301133021330313304133051330613307133081330913310133111331213313133141331513316133171331813319133201332113322133231332413325133261332713328133291333013331133321333313334133351333613337133381333913340133411334213343133441334513346133471334813349133501335113352133531335413355133561335713358133591336013361133621336313364133651336613367133681336913370133711337213373133741337513376133771337813379133801338113382133831338413385133861338713388133891339013391133921339313394133951339613397133981339913400134011340213403134041340513406134071340813409134101341113412134131341413415134161341713418134191342013421134221342313424134251342613427134281342913430134311343213433134341343513436134371343813439134401344113442134431344413445134461344713448134491345013451134521345313454134551345613457134581345913460134611346213463134641346513466134671346813469134701347113472134731347413475134761347713478134791348013481134821348313484134851348613487134881348913490134911349213493134941349513496134971349813499135001350113502135031350413505135061350713508135091351013511135121351313514135151351613517135181351913520135211352213523135241352513526135271352813529135301353113532135331353413535135361353713538135391354013541135421354313544135451354613547135481354913550135511355213553135541355513556135571355813559135601356113562135631356413565135661356713568135691357013571135721357313574135751357613577135781357913580135811358213583135841358513586135871358813589135901359113592135931359413595135961359713598135991360013601136021360313604136051360613607136081360913610136111361213613136141361513616136171361813619136201362113622136231362413625136261362713628136291363013631136321363313634136351363613637136381363913640136411364213643136441364513646136471364813649136501365113652136531365413655136561365713658136591366013661136621366313664136651366613667136681366913670136711367213673136741367513676136771367813679136801368113682136831368413685136861368713688136891369013691136921369313694136951369613697136981369913700137011370213703137041370513706137071370813709137101371113712137131371413715137161371713718137191372013721137221372313724137251372613727137281372913730137311373213733137341373513736137371373813739137401374113742137431374413745137461374713748137491375013751137521375313754137551375613757137581375913760137611376213763137641376513766137671376813769137701377113772137731377413775137761377713778137791378013781137821378313784137851378613787137881378913790137911379213793137941379513796137971379813799138001380113802138031380413805138061380713808138091381013811138121381313814138151381613817138181381913820138211382213823138241382513826138271382813829138301383113832138331383413835138361383713838138391384013841138421384313844138451384613847138481384913850138511385213853138541385513856138571385813859138601386113862138631386413865138661386713868138691387013871138721387313874138751387613877138781387913880138811388213883138841388513886138871388813889138901389113892138931389413895138961389713898138991390013901139021390313904139051390613907139081390913910139111391213913139141391513916139171391813919139201392113922139231392413925139261392713928139291393013931139321393313934139351393613937139381393913940139411394213943139441394513946139471394813949139501395113952139531395413955139561395713958139591396013961139621396313964139651396613967139681396913970139711397213973139741397513976139771397813979139801398113982139831398413985139861398713988139891399013991139921399313994139951399613997139981399914000140011400214003140041400514006140071400814009140101401114012140131401414015140161401714018140191402014021140221402314024140251402614027140281402914030140311403214033140341403514036140371403814039140401404114042140431404414045140461404714048140491405014051140521405314054140551405614057140581405914060140611406214063140641406514066140671406814069140701407114072140731407414075140761407714078140791408014081140821408314084140851408614087140881408914090140911409214093140941409514096140971409814099141001410114102141031410414105141061410714108141091411014111141121411314114141151411614117141181411914120141211412214123141241412514126141271412814129141301413114132141331413414135141361413714138141391414014141141421414314144141451414614147141481414914150141511415214153141541415514156141571415814159141601416114162141631416414165141661416714168141691417014171141721417314174141751417614177141781417914180141811418214183141841418514186141871418814189141901419114192141931419414195141961419714198141991420014201142021420314204142051420614207142081420914210142111421214213142141421514216142171421814219142201422114222142231422414225142261422714228142291423014231142321423314234142351423614237142381423914240142411424214243142441424514246142471424814249142501425114252142531425414255142561425714258142591426014261142621426314264142651426614267142681426914270142711427214273142741427514276142771427814279142801428114282142831428414285142861428714288142891429014291142921429314294142951429614297142981429914300143011430214303143041430514306143071430814309143101431114312143131431414315143161431714318143191432014321143221432314324143251432614327143281432914330143311433214333143341433514336143371433814339143401434114342143431434414345143461434714348143491435014351143521435314354143551435614357143581435914360143611436214363143641436514366143671436814369143701437114372143731437414375143761437714378143791438014381143821438314384143851438614387143881438914390143911439214393143941439514396143971439814399144001440114402144031440414405144061440714408144091441014411144121441314414144151441614417144181441914420144211442214423144241442514426144271442814429144301443114432144331443414435144361443714438144391444014441144421444314444144451444614447144481444914450144511445214453144541445514456144571445814459144601446114462144631446414465144661446714468144691447014471144721447314474144751447614477144781447914480144811448214483144841448514486144871448814489144901449114492144931449414495144961449714498144991450014501145021450314504145051450614507145081450914510145111451214513145141451514516145171451814519145201452114522145231452414525145261452714528145291453014531145321453314534145351453614537145381453914540145411454214543145441454514546145471454814549145501455114552145531455414555145561455714558145591456014561145621456314564145651456614567145681456914570145711457214573145741457514576145771457814579145801458114582145831458414585145861458714588145891459014591145921459314594145951459614597145981459914600146011460214603146041460514606146071460814609146101461114612146131461414615146161461714618146191462014621146221462314624146251462614627146281462914630146311463214633146341463514636146371463814639146401464114642146431464414645146461464714648146491465014651146521465314654146551465614657146581465914660146611466214663146641466514666146671466814669146701467114672146731467414675146761467714678146791468014681146821468314684146851468614687146881468914690146911469214693146941469514696146971469814699147001470114702147031470414705147061470714708147091471014711147121471314714147151471614717147181471914720147211472214723147241472514726147271472814729147301473114732147331473414735147361473714738147391474014741147421474314744147451474614747147481474914750147511475214753147541475514756147571475814759147601476114762147631476414765147661476714768147691477014771147721477314774147751477614777147781477914780147811478214783147841478514786147871478814789147901479114792147931479414795147961479714798147991480014801148021480314804148051480614807148081480914810148111481214813148141481514816148171481814819148201482114822148231482414825148261482714828148291483014831148321483314834148351483614837148381483914840148411484214843148441484514846148471484814849148501485114852148531485414855148561485714858148591486014861148621486314864148651486614867148681486914870148711487214873148741487514876148771487814879148801488114882148831488414885148861488714888148891489014891148921489314894148951489614897148981489914900149011490214903149041490514906149071490814909149101491114912149131491414915149161491714918149191492014921149221492314924149251492614927149281492914930149311493214933149341493514936149371493814939149401494114942149431494414945149461494714948149491495014951149521495314954149551495614957149581495914960149611496214963149641496514966149671496814969149701497114972149731497414975149761497714978149791498014981149821498314984149851498614987149881498914990149911499214993149941499514996149971499814999150001500115002150031500415005150061500715008150091501015011150121501315014150151501615017150181501915020150211502215023150241502515026150271502815029150301503115032150331503415035150361503715038150391504015041150421504315044150451504615047150481504915050 |
- package com.etotem.cfc.config;
- import com.etotem.cfc.mapper.*;
- import com.etotem.cfc.service.CfLegacyLedgerMigrationService;
- import com.fasterxml.jackson.databind.JsonNode;
- import com.fasterxml.jackson.databind.ObjectMapper;
- import javax.annotation.Resource;
- import org.slf4j.Logger;
- import org.slf4j.LoggerFactory;
- import lombok.var;
- import org.springframework.beans.factory.annotation.Value;
- import org.springframework.boot.CommandLineRunner;
- import org.springframework.core.io.ClassPathResource;
- import org.springframework.core.io.ResourceLoader;
- import org.springframework.jdbc.core.JdbcTemplate;
- import org.springframework.stereotype.Component;
- import org.springframework.util.StreamUtils;
- import java.nio.charset.StandardCharsets;
- import java.sql.Connection;
- import java.sql.PreparedStatement;
- import java.sql.SQLException;
- import java.util.ArrayList;
- import java.util.HashMap;
- import java.util.List;
- import java.util.Map;
- import java.util.regex.Pattern;
- @Component
- public class DatabaseInitializer implements CommandLineRunner {
- private static final Logger log = LoggerFactory.getLogger(DatabaseInitializer.class);
- /** 迁移是否已全部完成(供定时任务判断,避免启动期时序竞态) */
- private static volatile boolean migrationsComplete = false;
- public static boolean isMigrationsComplete() {
- return migrationsComplete;
- }
- @Resource
- private JdbcTemplate jdbcTemplate;
- @Resource
- private ResourceLoader resourceLoader;
- @Resource
- private CfLegacyLedgerMigrationService cfLegacyLedgerMigrationService;
- @Value("${app.init-test-data:false}")
- private boolean initTestData;
- @Override
- public void run(String... args) throws Exception {
- log.info("开始初始化数据库...");
- // 1. 检查并创建数据库
- initializeDatabase();
- // 2. 检查并创建表
- initializeTables();
- // 3. 执行数据库迁移(添加新列)
- runMigrations();
- // 4. 初始化食品相关表(DDL,始终执行)
- initFoodTables();
- // 5. 初始化测试/种子数据(仅测试/开发环境)
- if (initTestData) {
- log.info("init-test-data=true,开始初始化测试/种子数据...");
- initializeDefaultData();
- log.info("测试/种子数据初始化完成!");
- } else {
- log.info("init-test-data=false,跳过测试/种子数据初始化(生产模式)");
- }
- migrationsComplete = true;
- log.info("数据库初始化完成!");
- }
- private void initializeDatabase() {
- try {
- // 尝试连接数据库,如果不存在则创建
- jdbcTemplate.execute("CREATE DATABASE IF NOT EXISTS zxyj DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci");
- log.info("数据库检查完成");
- } catch (Exception e) {
- log.warn("创建数据库失败: {}", e.getMessage());
- }
- }
- private void initializeTables() {
- try {
- org.springframework.core.io.Resource resource = resourceLoader.getResource("classpath:schema.sql");
- String content;
- try (java.io.InputStream is = resource.getInputStream()) {
- content = new String(StreamUtils.copyToByteArray(is), StandardCharsets.UTF_8);
- }
- // Remove SQL single-line comments
- content = content.replaceAll("(?m)^\\s*--.*$", "");
- // 提取所有 CREATE TABLE IF NOT EXISTS 语句。
- // 不能用非贪婪 ".*?;" 匹配:COMMENT 字符串字面量中可能含分号(如
- // task_reminder_config.offset_minutes 的注释 "截止前N分钟; after_timeout..."),
- // 会被提前截断导致语句不完整而建表失败。此处按字符扫描,跳过引号内部的分号。
- List<String> createTableSQLs = new ArrayList<>();
- Pattern createTableHead = Pattern.compile(
- "^\\s*CREATE\\s+TABLE\\s+IF\\s+NOT\\s+EXISTS\\b", Pattern.CASE_INSENSITIVE);
- StringBuilder stmt = new StringBuilder();
- boolean inSingleQuote = false;
- boolean inBacktick = false;
- for (int i = 0; i < content.length(); i++) {
- char c = content.charAt(i);
- if (c == '\'' && !inBacktick) {
- if (inSingleQuote && i + 1 < content.length() && content.charAt(i + 1) == '\'') {
- stmt.append("''");
- i++;
- continue;
- }
- inSingleQuote = !inSingleQuote;
- stmt.append(c);
- } else if (c == '`' && !inSingleQuote) {
- inBacktick = !inBacktick;
- stmt.append(c);
- } else if (c == ';' && !inSingleQuote && !inBacktick) {
- String sql = stmt.toString().trim();
- if (!sql.isEmpty() && createTableHead.matcher(sql).find()) {
- createTableSQLs.add(sql + ";");
- }
- stmt.setLength(0);
- } else {
- stmt.append(c);
- }
- }
- log.info("从 schema.sql 加载了 {} 个建表语句", createTableSQLs.size());
- for (String sql : createTableSQLs) {
- try {
- jdbcTemplate.execute(sql);
- } catch (Exception e) {
- log.warn("创建表失败: {}", e.getMessage());
- }
- }
- log.info("数据表检查完成");
- } catch (Exception e) {
- log.error("读取 schema.sql 失败", e);
- }
- }
- private void runMigrations() {
- // 迁移1: 任务表添加执行者字段
- try {
- jdbcTemplate.execute("ALTER TABLE tasks ADD COLUMN executor_type ENUM('child', 'parent') DEFAULT 'child' COMMENT '执行者类型'");
- log.info("已添加executor_type列到tasks表");
- } catch (Exception e) {
- // 列已存在,忽略错误
- }
- try {
- jdbcTemplate.execute("ALTER TABLE tasks ADD COLUMN executor_id BIGINT COMMENT '执行者ID'");
- log.info("已添加executor_id列到tasks表");
- } catch (Exception e) {
- // 列已存在,忽略错误
- }
- try {
- jdbcTemplate.execute("ALTER TABLE tasks ADD INDEX idx_executor (executor_type, executor_id)");
- log.info("已添加idx_executor索引到tasks表");
- } catch (Exception e) {
- // 索引已存在,忽略错误
- }
- // 迁移2: 用户表添加角色和积分字段
- try {
- jdbcTemplate.execute("ALTER TABLE users ADD COLUMN roles VARCHAR(100) COMMENT '角色列表'");
- log.info("已添加roles列到users表");
- } catch (Exception e) {
- // 列已存在,忽略错误
- }
- try {
- jdbcTemplate.execute("ALTER TABLE users ADD COLUMN total_points INT DEFAULT 0 COMMENT '家长总积分'");
- log.info("已添加total_points列到users表");
- } catch (Exception e) {
- // 列已存在,忽略错误
- }
- try {
- jdbcTemplate.execute("ALTER TABLE users ADD INDEX idx_roles (roles(20))");
- log.info("已添加idx_roles索引到users表");
- } catch (Exception e) {
- // 索引已存在,忽略错误
- }
- // 迁移3: users表添加新字段
- try {
- jdbcTemplate.execute("ALTER TABLE users ADD COLUMN id_card VARCHAR(18) COMMENT '身份证号'");
- log.info("已添加id_card列到users表");
- } catch (Exception e) {
- // 列已存在,忽略错误
- }
- try {
- jdbcTemplate.execute("ALTER TABLE users ADD COLUMN real_name VARCHAR(50) COMMENT '真实姓名'");
- log.info("已添加real_name列到users表");
- } catch (Exception e) {
- // 列已存在,忽略错误
- }
- try {
- jdbcTemplate.execute("ALTER TABLE users ADD COLUMN phone VARCHAR(11) COMMENT '手机号'");
- log.info("已添加phone列到users表");
- } catch (Exception e) {
- // 列已存在,忽略错误
- }
- try {
- jdbcTemplate.execute("ALTER TABLE users ADD COLUMN birthday DATE COMMENT '生日'");
- log.info("已添加birthday列到users表");
- } catch (Exception e) {
- // 列已存在,忽略错误
- }
- try {
- jdbcTemplate.execute("ALTER TABLE users ADD COLUMN gender ENUM('male', 'female') COMMENT '性别'");
- log.info("已添加gender列到users表");
- } catch (Exception e) {
- // 列已存在,忽略错误
- }
- try {
- jdbcTemplate.execute("ALTER TABLE users ADD INDEX idx_phone (phone)");
- log.info("已添加idx_phone索引到users表");
- } catch (Exception e) {
- }
- try {
- jdbcTemplate.execute("ALTER TABLE users MODIFY COLUMN role ENUM('parent', 'child', 'teacher') NOT NULL");
- log.info("已更新users.role字段支持teacher角色");
- } catch (Exception e) {
- // 修改失败,忽略错误
- }
- // 迁移5.1: 修改users.role为支持admin
- try {
- jdbcTemplate.execute("ALTER TABLE users MODIFY COLUMN role ENUM('parent', 'child', 'teacher', 'admin') NOT NULL");
- log.info("已更新users.role字段支持admin角色");
- } catch (Exception e) {
- // 修改失败,忽略错误
- }
- // 迁移5.2: 确保管理员角色为admin(修复旧数据遗留问题)
- try {
- int updated = jdbcTemplate.update("UPDATE admins SET role = 'admin' WHERE username = 'admin'");
- if (updated > 0) {
- log.info("已修正管理员角色为admin");
- }
- } catch (Exception e) {
- // 更新失败,忽略错误
- }
- // 迁移6: 创建streets表(街道地址维度表)
- try {
- jdbcTemplate.execute("CREATE TABLE IF NOT EXISTS streets (" +
- "id BIGINT AUTO_INCREMENT PRIMARY KEY, " +
- "province VARCHAR(50) COMMENT '省份', " +
- "province_code VARCHAR(20) COMMENT '省份编码', " +
- "city VARCHAR(50) COMMENT '城市', " +
- "city_code VARCHAR(20) COMMENT '城市编码', " +
- "district VARCHAR(50) COMMENT '区县', " +
- "district_code VARCHAR(20) COMMENT '区县编码', " +
- "street VARCHAR(100) COMMENT '街道', " +
- "street_code VARCHAR(20) COMMENT '街道编码', " +
- "full_name VARCHAR(200) COMMENT '完整地址', " +
- "level INT COMMENT '层级:1省2市3区4街道', " +
- "parent_id BIGINT COMMENT '父级ID', " +
- "sort_order INT DEFAULT 0 COMMENT '排序', " +
- "created_at DATETIME DEFAULT CURRENT_TIMESTAMP, " +
- "updated_at DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, " +
- "INDEX idx_level (level), " +
- "INDEX idx_parent_id (parent_id), " +
- "INDEX idx_street_code (street_code), " +
- "INDEX idx_full_name (full_name(50))" +
- ") ENGINE=InnoDB DEFAULT CHARSET=utf8mb4");
- log.info("已创建streets表");
- } catch (Exception e) {
- // 表已存在,忽略错误
- }
- // 迁移7: families表添加地址字段
- try {
- jdbcTemplate.execute("ALTER TABLE families ADD COLUMN street_id BIGINT COMMENT '街道ID'");
- log.info("已添加street_id列到families表");
- } catch (Exception e) {
- // 列已存在,忽略错误
- }
- try {
- jdbcTemplate.execute("ALTER TABLE families ADD COLUMN province VARCHAR(50) COMMENT '省份'");
- jdbcTemplate.execute("ALTER TABLE families ADD COLUMN city VARCHAR(50) COMMENT '城市'");
- jdbcTemplate.execute("ALTER TABLE families ADD COLUMN district VARCHAR(50) COMMENT '区县'");
- jdbcTemplate.execute("ALTER TABLE families ADD COLUMN street VARCHAR(100) COMMENT '街道'");
- log.info("已添加地址字段到families表");
- } catch (Exception e) {
- // 列已存在,忽略错误
- }
- try {
- jdbcTemplate.execute("ALTER TABLE families ADD INDEX idx_street_id (street_id)");
- log.info("已添加idx_street_id索引到families表");
- } catch (Exception e) {
- // 索引已存在,忽略错误
- }
- // 迁移7b: families表添加teacher_id和creator_id字段
- try {
- jdbcTemplate.execute("ALTER TABLE families ADD COLUMN teacher_id BIGINT COMMENT '绑定的成长规划师ID'");
- log.info("已添加teacher_id列到families表");
- } catch (Exception e) {
- // 列已存在,忽略错误
- }
- try {
- jdbcTemplate.execute("ALTER TABLE families ADD COLUMN creator_id BIGINT COMMENT '家庭创建者ID'");
- log.info("已添加creator_id列到families表");
- } catch (Exception e) {
- // 列已存在,忽略错误
- }
- // 迁移8: teachers表添加地址字段
- try {
- jdbcTemplate.execute("ALTER TABLE teachers ADD COLUMN street_id BIGINT COMMENT '街道ID'");
- log.info("已添加street_id列到teachers表");
- } catch (Exception e) {
- // 列已存在,忽略错误
- }
- try {
- jdbcTemplate.execute("ALTER TABLE teachers ADD COLUMN province VARCHAR(50) COMMENT '省份'");
- jdbcTemplate.execute("ALTER TABLE teachers ADD COLUMN city VARCHAR(50) COMMENT '城市'");
- jdbcTemplate.execute("ALTER TABLE teachers ADD COLUMN district VARCHAR(50) COMMENT '区县'");
- jdbcTemplate.execute("ALTER TABLE teachers ADD COLUMN street VARCHAR(100) COMMENT '街道'");
- log.info("已添加地址字段到teachers表");
- } catch (Exception e) {
- // 列已存在,忽略错误
- }
- try {
- jdbcTemplate.execute("ALTER TABLE teachers ADD INDEX idx_street_id (street_id)");
- log.info("已添加idx_street_id索引到teachers表");
- } catch (Exception e) {
- // 索引已存在,忽略错误
- }
- // 迁移9: users表添加成长规划师相关字段
- try {
- jdbcTemplate.execute("ALTER TABLE users ADD COLUMN teacher_no VARCHAR(50) COMMENT '成长规划师证书号'");
- log.info("已添加teacher_no列到users表");
- } catch (Exception e) {
- // 列已存在,忽略错误
- }
- try {
- jdbcTemplate.execute("ALTER TABLE users ADD COLUMN teacher_photo VARCHAR(500) COMMENT '成长规划师照片'");
- log.info("已添加teacher_photo列到users表");
- } catch (Exception e) {
- // 列已存在,忽略错误
- }
- try {
- jdbcTemplate.execute("ALTER TABLE users ADD COLUMN certificate_image VARCHAR(500) COMMENT '成长规划师证书照片'");
- log.info("已添加certificate_image列到users表");
- } catch (Exception e) {
- // 列已存在,忽略错误
- }
- try {
- jdbcTemplate.execute("ALTER TABLE users ADD COLUMN teacher_status VARCHAR(20) DEFAULT NULL COMMENT '成长规划师审核状态'");
- log.info("已添加teacher_status列到users表");
- } catch (Exception e) {
- // 列已存在,忽略错误
- }
- try {
- jdbcTemplate.execute("ALTER TABLE users ADD INDEX idx_teacher_status (teacher_status)");
- log.info("已添加idx_teacher_status索引到users表");
- } catch (Exception e) {
- // 索引已存在,忽略错误
- }
- // 迁移10: users表添加teacherFamilyIds字段(成长规划师关联的家庭ID列表)
- try {
- jdbcTemplate.execute("ALTER TABLE users ADD COLUMN teacher_family_ids VARCHAR(500) COMMENT '成长规划师关联的家庭ID列表(逗号分隔)'");
- log.info("已添加teacher_family_ids列到users表");
- } catch (Exception e) {
- // 列已存在,忽略错误
- }
- // 迁移11: families表添加teacherId字段(绑定的成长规划师ID)
- try {
- jdbcTemplate.execute("ALTER TABLE families ADD COLUMN teacher_id BIGINT COMMENT '绑定的成长规划师ID'");
- log.info("已添加teacher_id列到families表");
- } catch (Exception e) {
- // 列已存在,忽略错误
- }
- try {
- jdbcTemplate.execute("ALTER TABLE families ADD INDEX idx_teacher_id (teacher_id)");
- log.info("已添加idx_teacher_id索引到families表");
- } catch (Exception e) {
- // 索引已存在,忽略错误
- }
- // 迁移12: 创建 teacher_messages 表(家长咨询成长规划师)
- try {
- jdbcTemplate.execute("CREATE TABLE IF NOT EXISTS teacher_messages (" +
- "id BIGINT AUTO_INCREMENT PRIMARY KEY, " +
- "family_id BIGINT NOT NULL COMMENT '家庭ID', " +
- "parent_id BIGINT NOT NULL COMMENT '家长用户ID', " +
- "teacher_id BIGINT NOT NULL COMMENT '成长规划师用户ID', " +
- "content TEXT NOT NULL COMMENT '消息内容', " +
- "reply_content TEXT COMMENT '回复内容', " +
- "status VARCHAR(20) DEFAULT 'pending' COMMENT '状态:pending(待回复)/replied(已回复)', " +
- "created_at DATETIME DEFAULT CURRENT_TIMESTAMP, " +
- "replied_at DATETIME COMMENT '回复时间', " +
- "INDEX idx_family_id (family_id), " +
- "INDEX idx_teacher_id (teacher_id), " +
- "INDEX idx_parent_id (parent_id), " +
- "INDEX idx_status (status)" +
- ") ENGINE=InnoDB DEFAULT CHARSET=utf8mb4");
- log.info("已创建teacher_messages表");
- } catch (Exception e) {
- // 表已存在,忽略错误
- }
- // 迁移13: admin_task_templates表添加teacher_id字段
- try {
- jdbcTemplate.execute("ALTER TABLE admin_task_templates ADD COLUMN teacher_id BIGINT COMMENT '成长规划师ID'");
- log.info("已添加teacher_id列到admin_task_templates表");
- } catch (Exception e) {
- // 列已存在,忽略错误
- }
- // 迁移13b: admin_task_templates表添加新字段
- try {
- jdbcTemplate.execute("ALTER TABLE admin_task_templates ADD COLUMN complete_types VARCHAR(100) COMMENT '完成方式JSON'");
- log.info("已添加complete_types列到admin_task_templates表");
- } catch (Exception e) {
- // 列已存在,忽略错误
- }
- try {
- jdbcTemplate.execute("ALTER TABLE admin_task_templates ADD COLUMN duration INT COMMENT '预计完成时长(分钟)'");
- log.info("已添加duration列到admin_task_templates表");
- } catch (Exception e) {
- // 列已存在,忽略错误
- }
- try {
- jdbcTemplate.execute("ALTER TABLE admin_task_templates ADD COLUMN review_type VARCHAR(20) COMMENT '审核人类型: creator/parent/teacher/ai'");
- log.info("已添加review_type列到admin_task_templates表");
- } catch (Exception e) {
- // 列已存在,忽略错误
- }
- try {
- jdbcTemplate.execute("ALTER TABLE admin_task_templates ADD COLUMN task_type VARCHAR(20) COMMENT '任务性质: onetime/recurring'");
- log.info("已添加task_type列到admin_task_templates表");
- } catch (Exception e) {
- // 列已存在,忽略错误
- }
- try {
- jdbcTemplate.execute("ALTER TABLE admin_task_templates ADD COLUMN frequency VARCHAR(20) COMMENT '频率: daily/weekly/monthly'");
- log.info("已添加frequency列到admin_task_templates表");
- } catch (Exception e) {
- // 列已存在,忽略错误
- }
- try {
- jdbcTemplate.execute("ALTER TABLE admin_task_templates ADD COLUMN max_frequency INT COMMENT '周期内最多完成次数'");
- log.info("已添加max_frequency列到admin_task_templates表");
- } catch (Exception e) {
- // 列已存在,忽略错误
- }
- // 迁移14: rewards表child_id字段允许NULL(模板奖励不需要child_id)
- try {
- jdbcTemplate.execute("ALTER TABLE rewards MODIFY COLUMN child_id BIGINT NULL COMMENT '孩子ID,模板奖励为NULL'");
- log.info("已修改rewards.child_id字段允许NULL");
- } catch (Exception e) {
- // 修改失败,忽略错误
- }
- // 迁移15: admins表添加缺失列
- try {
- jdbcTemplate.execute("ALTER TABLE admins ADD COLUMN nickname VARCHAR(50) DEFAULT NULL AFTER password");
- log.info("已添加nickname列到admins表");
- } catch (Exception e) {
- // 列已存在,忽略错误
- }
- try {
- jdbcTemplate.execute("ALTER TABLE admins ADD COLUMN email VARCHAR(100) DEFAULT NULL AFTER phone");
- log.info("已添加email列到admins表");
- } catch (Exception e) {
- // 列已存在,忽略错误
- }
- try {
- jdbcTemplate.execute("ALTER TABLE admins MODIFY COLUMN status VARCHAR(20) DEFAULT 'active'");
- log.info("已修改admins.status字段类型");
- } catch (Exception e) {
- // 修改失败,忽略错误
- }
- // 迁移16: 创建用户操作日志表
- try {
- jdbcTemplate.execute("CREATE TABLE IF NOT EXISTS user_operation_logs (" +
- "id BIGINT AUTO_INCREMENT PRIMARY KEY, " +
- "user_id BIGINT COMMENT \'用户ID\', " +
- "nickname VARCHAR(50) COMMENT \'用户昵称\', " +
- "operation_type VARCHAR(20) NOT NULL COMMENT \'操作类型: login/logout/create/update/delete/query等\', " +
- "description VARCHAR(500) COMMENT \'操作描述\', " +
- "request_path VARCHAR(200) COMMENT \'请求路径\', " +
- "request_method VARCHAR(10) COMMENT \'请求方法\', " +
- "request_params TEXT COMMENT \'请求参数(脱敏)\', " +
- "response_status INT COMMENT \'响应状态码\', " +
- "client_ip VARCHAR(50) COMMENT \'客户端IP\', " +
- "user_agent VARCHAR(500) COMMENT \'User-Agent\', " +
- "created_at DATETIME DEFAULT CURRENT_TIMESTAMP, " +
- "INDEX idx_user_id (user_id), " +
- "INDEX idx_operation_type (operation_type), " +
- "INDEX idx_created_at (created_at)" +
- ") ENGINE=InnoDB DEFAULT CHARSET=utf8mb4");
- log.info("已创建user_operation_logs表");
- } catch (Exception e) {
- // 表已存在,忽略错误
- }
- // 迁移16: tasks表添加bonus_rule字段(POINT-002: 超额积分规则)
- try {
- jdbcTemplate.execute("ALTER TABLE tasks ADD COLUMN bonus_rule VARCHAR(500) COMMENT \'超额积分规则JSON\'");
- log.info("已添加bonus_rule列到tasks表");
- } catch (Exception e) {
- // 列已存在,忽略错误
- }
- // 迁移17: tasks表添加points_expire_days字段(POINT-009: 积分有效期)
- try {
- jdbcTemplate.execute("ALTER TABLE tasks ADD COLUMN points_expire_days INT DEFAULT 0 COMMENT \'积分有效期天数,0表示永久有效\'");
- log.info("已添加points_expire_days列到tasks表");
- } catch (Exception e) {
- // 列已存在,忽略错误
- }
- // 迁移18: points_log表添加expire_at字段(POINT-009: 积分有效期)
- try {
- jdbcTemplate.execute("ALTER TABLE points_log ADD COLUMN expire_at DATE COMMENT \'积分过期日期\'");
- log.info("已添加expire_at列到points_log表");
- } catch (Exception e) {
- // 列已存在,忽略错误
- }
- // 迁移19: points_log表添加expired字段(POINT-009: 过期状态标记)
- try {
- jdbcTemplate.execute("ALTER TABLE points_log ADD COLUMN expired TINYINT DEFAULT 0 COMMENT \'是否已过期: 0否1是\'");
- log.info("已添加expired列到points_log表");
- } catch (Exception e) {
- // 列已存在,忽略错误
- }
- // 迁移20: tasks表添加template_id字段(TASK-004: 重复任务模板追踪)
- try {
- jdbcTemplate.execute("ALTER TABLE tasks ADD COLUMN template_id BIGINT COMMENT \'模板任务ID\'");
- log.info("已添加template_id列到tasks表");
- } catch (Exception e) {
- // 列已存在,忽略错误
- }
- // 迁移21: tasks表添加is_template字段(TASK-004: 标记模板任务)
- try {
- jdbcTemplate.execute("ALTER TABLE tasks ADD COLUMN is_template TINYINT DEFAULT 0 COMMENT '是否为模板任务'");
- log.info("已添加is_template列到tasks表");
- } catch (Exception e) {
- // 列已存在,忽略错误
- }
- // 迁移22: 修复tasks表child_id和deadline允许NULL(支持多孩子和家长任务)
- try {
- jdbcTemplate.execute("ALTER TABLE tasks MODIFY COLUMN child_id BIGINT NULL COMMENT '执行者ID'");
- log.info("已修改tasks.child_id允许NULL");
- } catch (Exception e) {
- // 修改失败,忽略错误
- }
- try {
- jdbcTemplate.execute("ALTER TABLE tasks MODIFY COLUMN deadline DATETIME NULL COMMENT '截止时间'");
- log.info("已修改tasks.deadline允许NULL");
- } catch (Exception e) {
- // 修改失败,忽略错误
- }
- // 迁移23: tasks表添加新字段
- try {
- jdbcTemplate.execute("ALTER TABLE tasks ADD COLUMN task_type VARCHAR(20) COMMENT '任务性质: onetime/recurring'");
- log.info("已添加task_type列到tasks表");
- } catch (Exception e) {
- // 列已存在,忽略错误
- }
- // 迁移24: tasks表添加parent_task_id字段(购买任务→每日使用子任务链;子任务在父任务完成前不显示)
- try {
- jdbcTemplate.execute("ALTER TABLE tasks ADD COLUMN parent_task_id BIGINT COMMENT '父任务ID(购买任务→每日使用子任务)'");
- log.info("已添加parent_task_id列到tasks表");
- } catch (Exception e) {
- // 列已存在,忽略错误
- }
- try {
- jdbcTemplate.execute("ALTER TABLE tasks ADD COLUMN frequency VARCHAR(20) COMMENT '频率: daily/weekly/monthly'");
- log.info("已添加frequency列到tasks表");
- } catch (Exception e) {
- // 列已存在,忽略错误
- }
- try {
- jdbcTemplate.execute("ALTER TABLE tasks ADD COLUMN max_frequency INT COMMENT '周期内最多完成次数'");
- log.info("已添加max_frequency列到tasks表");
- } catch (Exception e) {
- // 列已存在,忽略错误
- }
- try {
- jdbcTemplate.execute("ALTER TABLE tasks ADD COLUMN complete_types VARCHAR(100) COMMENT '完成方式JSON'");
- log.info("已添加complete_types列到tasks表");
- } catch (Exception e) {
- // 列已存在,忽略错误
- }
- try {
- jdbcTemplate.execute("ALTER TABLE tasks ADD COLUMN review_type VARCHAR(20) COMMENT '审核人类型: creator/parent/teacher/ai'");
- log.info("已添加review_type列到tasks表");
- } catch (Exception e) {
- // 列已存在,忽略错误
- }
- try {
- jdbcTemplate.execute("ALTER TABLE tasks ADD COLUMN earliest_start DATETIME COMMENT '最早开始时间'");
- log.info("已添加earliest_start列到tasks表");
- } catch (Exception e) {
- // 列已存在,忽略错误
- }
- try {
- jdbcTemplate.execute("ALTER TABLE tasks ADD COLUMN latest_end DATETIME COMMENT '最晚结束时间'");
- log.info("已添加latest_end列到tasks表");
- } catch (Exception e) {
- // 列已存在,忽略错误
- }
- // 迁移24: tasks表添加minigame_code字段(小游戏任务)
- try {
- jdbcTemplate.execute("ALTER TABLE tasks ADD COLUMN minigame_code VARCHAR(50) DEFAULT NULL COMMENT '小游戏代码: schulte/1a2b/sudoku'");
- log.info("已添加minigame_code列到tasks表");
- } catch (Exception e) {
- // 列已存在,忽略错误
- }
- // 迁移25: tasks表添加duration字段(TASK-011: 任务预计时长)
- try {
- jdbcTemplate.execute("ALTER TABLE tasks ADD COLUMN duration INT DEFAULT 0 COMMENT '预计时长(分钟)'");
- log.info("已添加duration列到tasks表");
- } catch (Exception e) {
- // 列已存在,忽略错误
- }
- try {
- jdbcTemplate.execute("ALTER TABLE tasks ADD INDEX idx_minigame (minigame_code) WHERE minigame_code IS NOT NULL");
- log.info("已添加idx_minigame索引到tasks表");
- } catch (Exception e) {
- // 索引已存在,忽略错误
- }
- try {
- jdbcTemplate.execute("CREATE TABLE IF NOT EXISTS trial_memberships (" +
- "id BIGINT AUTO_INCREMENT PRIMARY KEY, " +
- "user_id BIGINT NOT NULL COMMENT '用户ID', " +
- "family_id BIGINT NOT NULL COMMENT '家庭ID', " +
- "start_date DATETIME NOT NULL COMMENT '试用开始时间', " +
- "end_date DATETIME NOT NULL COMMENT '试用结束时间', " +
- "status VARCHAR(20) DEFAULT 'PENDING' COMMENT '状态:PENDING/ACTIVE/EXPIRED/CONVERTED', " +
- "trial_days INT DEFAULT 7 COMMENT '试用天数', " +
- "created_at DATETIME DEFAULT CURRENT_TIMESTAMP, " +
- "updated_at DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, " +
- "INDEX idx_user_id (user_id), " +
- "INDEX idx_family_id (family_id), " +
- "INDEX idx_status (status), " +
- "INDEX idx_end_date (end_date)" +
- ") ENGINE=InnoDB DEFAULT CHARSET=utf8mb4");
- log.info("已创建trial_memberships表");
- } catch (Exception e) {
- log.warn("创建trial_memberships表可能已存在: {}", e.getMessage());
- }
- // assessment_appointments 新字段
- try {
- jdbcTemplate.execute("ALTER TABLE assessment_appointments ADD COLUMN user_id BIGINT COMMENT '用户ID'");
- } catch (Exception e) {
- }
- try {
- jdbcTemplate.execute("ALTER TABLE assessment_appointments ADD COLUMN guide_id BIGINT COMMENT '成长规划师ID'");
- } catch (Exception e) {
- }
- try {
- jdbcTemplate.execute("ALTER TABLE assessment_appointments ADD COLUMN package_id BIGINT COMMENT '套餐ID'");
- } catch (Exception e) {
- }
- try {
- jdbcTemplate.execute("ALTER TABLE assessment_appointments ADD COLUMN remark VARCHAR(500) COMMENT '备注'");
- } catch (Exception e) {
- }
- // assessment_orders 表
- try {
- jdbcTemplate.execute("CREATE TABLE IF NOT EXISTS assessment_orders (" +
- "id BIGINT AUTO_INCREMENT PRIMARY KEY, " +
- "order_no VARCHAR(64) NOT NULL UNIQUE, " +
- "family_id BIGINT NOT NULL, " +
- "user_id BIGINT NOT NULL, " +
- "child_id BIGINT, " +
- "guide_id BIGINT NOT NULL, " +
- "package_id BIGINT NOT NULL, " +
- "guide_name VARCHAR(50), " +
- "package_name VARCHAR(100), " +
- "total_price BIGINT DEFAULT 0, " +
- "discount_amount BIGINT DEFAULT 0, " +
- "actual_price BIGINT DEFAULT 0, " +
- "status VARCHAR(20) DEFAULT 'pending', " +
- "pay_time VARCHAR(50), " +
- "pay_type VARCHAR(20), " +
- "transaction_id VARCHAR(64), " +
- "created_at DATETIME DEFAULT CURRENT_TIMESTAMP, " +
- "updated_at DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, " +
- "INDEX idx_family (family_id), " +
- "INDEX idx_guide (guide_id), " +
- "INDEX idx_status (status)) " +
- "ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='评估订单表'");
- } catch (Exception e) {
- }
- // 迁移26: guide_applications表添加address和bank_card字段
- try {
- jdbcTemplate.execute("ALTER TABLE guide_applications ADD COLUMN address VARCHAR(500) COMMENT '地址'");
- log.info("已添加address列到guide_applications表");
- } catch (Exception e) {
- // 列已存在,忽略错误
- }
- try {
- jdbcTemplate.execute("ALTER TABLE guide_applications ADD COLUMN bank_card VARCHAR(50) COMMENT '银行卡号'");
- log.info("已添加bank_card列到guide_applications表");
- } catch (Exception e) {
- // 列已存在,忽略错误
- }
- try {
- jdbcTemplate.execute("ALTER TABLE guide_applications ADD COLUMN bank_name VARCHAR(100) COMMENT '开户行'");
- log.info("已添加bank_name列到guide_applications表");
- } catch (Exception e) {
- // 列已存在,忽略错误
- }
- // 迁移27: users表添加address字段(成长规划师地址)
- try {
- jdbcTemplate.execute("ALTER TABLE users ADD COLUMN address VARCHAR(500) COMMENT '地址'");
- log.info("已添加address列到users表");
- } catch (Exception e) {
- // 列已存在,忽略错误
- }
- // 迁移28: 创建guide_application_records表(申请过程记录)
- try {
- jdbcTemplate.execute("CREATE TABLE IF NOT EXISTS guide_application_records (" +
- "id BIGINT AUTO_INCREMENT PRIMARY KEY, " +
- "application_id BIGINT NOT NULL COMMENT '关联申请ID', " +
- "user_id BIGINT NOT NULL COMMENT '操作人用户ID', " +
- "action VARCHAR(50) NOT NULL COMMENT '操作: submit/approve/reject/withdraw/resubmit', " +
- "from_status VARCHAR(20) COMMENT '操作前状态', " +
- "to_status VARCHAR(20) NOT NULL COMMENT '操作后状态', " +
- "remark VARCHAR(500) COMMENT '备注/原因', " +
- "created_at DATETIME DEFAULT CURRENT_TIMESTAMP, " +
- "INDEX idx_application_id (application_id), " +
- "INDEX idx_user_id (user_id), " +
- "INDEX idx_action (action)" +
- ") ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='成长规划师申请过程记录表'");
- log.info("已创建guide_application_records表");
- } catch (Exception e) {
- }
- try {
- Integer exists = jdbcTemplate.queryForObject(
- "SELECT COUNT(*) FROM information_schema.COLUMNS WHERE TABLE_SCHEMA = DATABASE() AND TABLE_NAME = 'points_log' AND COLUMN_NAME = 'category'",
- Integer.class);
- if (exists == null || exists == 0) {
- jdbcTemplate.execute("ALTER TABLE points_log ADD COLUMN category VARCHAR(20) DEFAULT 'task' COMMENT '积分分类: task任务积分, system系统积分'");
- log.info("已添加category列到points_log表");
- } else {
- log.info("category列已存在,跳过");
- }
- } catch (Exception e) {
- log.warn("检查/添加category列失败: {}", e.getMessage());
- }
- // 迁移22: 创建invite_card表
- try {
- jdbcTemplate.execute("CREATE TABLE IF NOT EXISTS invite_card (" +
- "id BIGINT AUTO_INCREMENT PRIMARY KEY, " +
- "type VARCHAR(20) NOT NULL COMMENT 'child/family/guide', " +
- "code VARCHAR(64) NOT NULL UNIQUE COMMENT '邀请码', " +
- "ref_id BIGINT NOT NULL COMMENT '关联ID(childId/familyId)', " +
- "creator_id BIGINT NOT NULL COMMENT '创建人', " +
- "expires_at DATETIME NOT NULL, " +
- "used TINYINT DEFAULT 0, " +
- "created_at DATETIME DEFAULT CURRENT_TIMESTAMP, " +
- "updated_at DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, " +
- "INDEX idx_code (code), " +
- "INDEX idx_ref_type (ref_id, type)" +
- ") ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='邀请卡片'");
- log.info("已创建invite_card表");
- } catch (Exception e) {
- // 表已存在,忽略错误
- }
- // 迁移23: 创建wishes表
- try {
- jdbcTemplate.execute("CREATE TABLE IF NOT EXISTS wishes (" +
- "id BIGINT AUTO_INCREMENT PRIMARY KEY, " +
- "family_id BIGINT COMMENT '家庭ID', " +
- "child_id BIGINT COMMENT '目标孩子ID', " +
- "created_by BIGINT COMMENT '创建者用户ID', " +
- "creator_type VARCHAR(20) COMMENT '创建者类型: child/parent', " +
- "title VARCHAR(200) NOT NULL COMMENT '心愿标题', " +
- "description VARCHAR(500) COMMENT '心愿描述', " +
- "category VARCHAR(32) COMMENT '心愿分类', " +
- "points_required INT COMMENT '所需积分', " +
- "points_set_by BIGINT COMMENT '定价者ID', " +
- "points_set_at DATETIME COMMENT '定价时间', " +
- "status VARCHAR(32) DEFAULT 'active' COMMENT '状态: draft/active/pending_exchange/approved/exchanging/fulfilled/cancelled/rejected', " +
- "exchange_requested_at DATETIME COMMENT '申请兑换时间', " +
- "exchange_approved_by BIGINT COMMENT '批准者ID', " +
- "exchange_approved_at DATETIME COMMENT '批准时间', " +
- "points_deducted_at DATETIME COMMENT '扣积分时间', " +
- "fulfilled_by BIGINT COMMENT '兑现者ID', " +
- "fulfilled_at DATETIME COMMENT '兑现时间', " +
- "child_confirmed_at DATETIME COMMENT '孩子确认收货时间', " +
- "rejected_by BIGINT COMMENT '拒绝者ID', " +
- "rejected_at DATETIME COMMENT '拒绝时间', " +
- "reject_reason VARCHAR(500) COMMENT '拒绝原因', " +
- "reject_count INT DEFAULT 0 COMMENT '被拒绝次数', " +
- "cancelled_by BIGINT COMMENT '取消者ID', " +
- "cancelled_at DATETIME COMMENT '取消时间', " +
- "cancel_reason VARCHAR(500) COMMENT '取消原因', " +
- "created_at DATETIME DEFAULT CURRENT_TIMESTAMP, " +
- "updated_at DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, " +
- "INDEX idx_family_id (family_id), " +
- "INDEX idx_child_id (child_id), " +
- "INDEX idx_status (status)" +
- ") ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='心愿单表'");
- log.info("已创建wishes表");
- } catch (Exception e) {
- log.warn("创建wishes表可能已存在: {}", e.getMessage());
- }
- // 迁移24: users表添加个人信息补充字段
- try {
- Integer exists = jdbcTemplate.queryForObject(
- "SELECT COUNT(*) FROM information_schema.COLUMNS WHERE TABLE_SCHEMA = DATABASE() AND TABLE_NAME = 'users' AND COLUMN_NAME = 'ethnicity'",
- Integer.class);
- if (exists == null || exists == 0) {
- jdbcTemplate.execute("ALTER TABLE users " +
- "ADD COLUMN ethnicity VARCHAR(32) DEFAULT NULL COMMENT '民族', " +
- "ADD COLUMN blood_type VARCHAR(16) DEFAULT NULL COMMENT '血型', " +
- "ADD COLUMN highest_education VARCHAR(32) DEFAULT NULL COMMENT '最高学历', " +
- "ADD COLUMN marital_status VARCHAR(16) DEFAULT NULL COMMENT '婚姻状态', " +
- "ADD COLUMN family_role VARCHAR(16) DEFAULT NULL COMMENT '家长身份: 爸爸/妈妈/爷爷/奶奶/姥爷/姥姥/其他'");
- log.info("已添加个人信息补充字段到users表");
- } else {
- log.info("个人信息补充字段已存在,跳过");
- }
- } catch (Exception e) {
- log.warn("检查/添加个人信息补充字段失败: {}", e.getMessage());
- }
- // Phase 0: 浠艾福俱乐部 — 服务商字段
- try {
- Integer exists = jdbcTemplate.queryForObject(
- "SELECT COUNT(*) FROM information_schema.COLUMNS WHERE TABLE_SCHEMA = DATABASE() AND TABLE_NAME = 'users' AND COLUMN_NAME = 'vendor_type'",
- Integer.class);
- if (exists == null || exists == 0) {
- jdbcTemplate.execute("ALTER TABLE users ADD COLUMN vendor_type VARCHAR(32) DEFAULT NULL COMMENT '服务商类型: planner/activity_provider/product_supplier'");
- jdbcTemplate.execute("ALTER TABLE users ADD COLUMN vendor_status VARCHAR(16) DEFAULT NULL COMMENT '服务商审核状态: pending/approved/rejected'");
- jdbcTemplate.execute("ALTER TABLE users ADD COLUMN vendor_reject_reason VARCHAR(500) DEFAULT NULL COMMENT '审核拒绝原因'");
- jdbcTemplate.execute("ALTER TABLE users ADD COLUMN is_family_admin TINYINT DEFAULT 0 COMMENT '是否家庭管理员'");
- jdbcTemplate.execute("ALTER TABLE users ADD COLUMN vendor_info VARCHAR(2000) DEFAULT NULL COMMENT '服务商资质信息(JSON)'");
- log.info("已添加浠艾福俱乐部服务商字段到users表");
- } else {
- log.info("浠艾福俱乐部服务商字段已存在,跳过");
- }
- } catch (Exception e) {
- log.warn("检查/添加服务商字段失败: {}", e.getMessage());
- }
- // Phase 0: 迁移已有规划师到服务商体系
- try {
- int updated = jdbcTemplate.update(
- "UPDATE users SET vendor_type = 'planner', vendor_status = 'approved' WHERE teacher_status = 'approved' AND vendor_type IS NULL"
- );
- if (updated > 0) {
- log.info("已迁移 {} 名规划师到服务商体系", updated);
- }
- } catch (Exception e) {
- log.warn("迁移规划师到服务商体系失败: {}", e.getMessage());
- }
- // ==================== Phase 1: 浠艾福俱乐部商品系统 ====== // 商品表
- try {
- jdbcTemplate.execute(
- "CREATE TABLE IF NOT EXISTS products (" +
- " id BIGINT AUTO_INCREMENT PRIMARY KEY," +
- " name VARCHAR(200) NOT NULL," +
- " description VARCHAR(1000)," +
- " intro TEXT," +
- " cover_image VARCHAR(500)," +
- " images TEXT," +
- " price INT NOT NULL DEFAULT 0 COMMENT '价格(分)'," +
- " stock INT NOT NULL DEFAULT 0," +
- " sales_count INT NOT NULL DEFAULT 0," +
- " product_type VARCHAR(32) NOT NULL COMMENT 'activity/course/physical/digital/service'," +
- " vendor_id BIGINT," +
- " vendor_name VARCHAR(100)," +
- " status VARCHAR(32) NOT NULL DEFAULT 'draft' COMMENT 'draft/pending/approved/rejected/on_shelf/off_shelf'," +
- " reject_reason VARCHAR(500)," +
- " member_price INT COMMENT '会员价(分)'," +
- " profit_rate INT DEFAULT 0 COMMENT '平台利润率(千分比)'," +
- " member_eligible VARCHAR(32) DEFAULT 'none' COMMENT 'all/budget/none'," +
- " domain VARCHAR(32) COMMENT 'action/mind/body/wisdom/wealth'," +
- " external_source VARCHAR(64)," +
- " external_id VARCHAR(128)," +
- " external_data TEXT," +
- " created_at DATETIME DEFAULT CURRENT_TIMESTAMP," +
- " updated_at DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP" +
- ")"
- );
- } catch (Exception e) {
- log.warn("创建products表失败: {}", e.getMessage());
- }
- // 商品SKU表
- try {
- jdbcTemplate.execute(
- "CREATE TABLE IF NOT EXISTS product_skus (" +
- " id INT NOT NULL AUTO_INCREMENT," +
- " product_id INT NOT NULL COMMENT '商品ID'," +
- " sku_code VARCHAR(64) NOT NULL COMMENT 'SKU编码'," +
- " specs JSON NOT NULL COMMENT '规格JSON'," +
- " price DECIMAL(10,2) NOT NULL COMMENT 'SKU价格'," +
- " stock INT NOT NULL DEFAULT 0 COMMENT '库存'," +
- " original_price DECIMAL(10,2) COMMENT '原价/划线价'," +
- " image VARCHAR(512) COMMENT 'SKU图片'," +
- " enabled TINYINT(1) NOT NULL DEFAULT 1," +
- " create_time DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP," +
- " update_time DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP," +
- " PRIMARY KEY (id)," +
- " KEY idx_product_id (product_id)" +
- ") ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='商品SKU'"
- );
- log.info("已创建product_skus表");
- } catch (Exception e) {
- log.warn("创建product_skus表失败: {}", e.getMessage());
- }
- // 订单表
- try {
- jdbcTemplate.execute(
- "CREATE TABLE IF NOT EXISTS product_orders (" +
- " id BIGINT AUTO_INCREMENT PRIMARY KEY," +
- " order_no VARCHAR(64) NOT NULL UNIQUE," +
- " product_id BIGINT NOT NULL," +
- " product_name VARCHAR(200)," +
- " product_type VARCHAR(32)," +
- " buyer_id BIGINT NOT NULL," +
- " family_id BIGINT," +
- " quantity INT NOT NULL DEFAULT 1," +
- " unit_price INT NOT NULL COMMENT '单价(分)'," +
- " total_amount INT NOT NULL COMMENT '总金额(分)'," +
- " status VARCHAR(32) NOT NULL DEFAULT 'pending' COMMENT 'pending/paid/completed/cancelled/refunded'," +
- " payment_method VARCHAR(32) COMMENT 'wechat/points/membership'," +
- " remark VARCHAR(500)," +
- " paid_at DATETIME," +
- " created_at DATETIME DEFAULT CURRENT_TIMESTAMP," +
- " updated_at DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP" +
- ")"
- );
- } catch (Exception e) {
- log.warn("创建product_orders表失败: {}", e.getMessage());
- }
- // 商品浏览/停留时长埋点表(支撑运营看板「商品停留时长 Top」)
- try {
- jdbcTemplate.execute(
- "CREATE TABLE IF NOT EXISTS product_view_records (" +
- " id BIGINT AUTO_INCREMENT PRIMARY KEY," +
- " user_id BIGINT," +
- " product_id BIGINT NOT NULL," +
- " product_name VARCHAR(200)," +
- " product_type VARCHAR(32)," +
- " duration_seconds INT NOT NULL DEFAULT 0 COMMENT '本次浏览停留秒数'," +
- " source VARCHAR(32) COMMENT 'miniprogram/web'," +
- " view_at DATETIME," +
- " created_at DATETIME DEFAULT CURRENT_TIMESTAMP" +
- ")"
- );
- } catch (Exception e) {
- log.warn("创建product_view_records表失败: {}", e.getMessage());
- }
- // 系统配置表
- try {
- jdbcTemplate.execute(
- "CREATE TABLE IF NOT EXISTS sys_config (" +
- " id BIGINT AUTO_INCREMENT PRIMARY KEY," +
- " config_key VARCHAR(64) NOT NULL UNIQUE," +
- " config_value TEXT," +
- " description VARCHAR(500)," +
- " updated_at DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP" +
- ")"
- );
- } catch (Exception e) {
- log.warn("创建sys_config表失败: {}", e.getMessage());
- }
- // Migration: product_orders add logistics fields
- try {
- jdbcTemplate.execute("ALTER TABLE product_orders ADD COLUMN logistics_no VARCHAR(64) COMMENT '物流单号'");
- log.info("已添加logistics_no列到product_orders表");
- } catch (Exception e) {
- }
- try {
- jdbcTemplate.execute("ALTER TABLE product_orders ADD COLUMN logistics_company VARCHAR(100) COMMENT '物流公司'");
- log.info("已添加logistics_company列到product_orders表");
- } catch (Exception e) {
- }
- // SysConfig 种子数据
- try {
- insertSysConfigSeed("points_exchange_rate", "1", "积分兑换汇率(1积分=1分,即0.01元)");
- insertSysConfigSeed("points_deduct_cap", "0.5", "积分抵扣上限(50%)");
- insertSysConfigSeed("member_fee_standard", "1314", "标准会员年费(元)");
- insertSysConfigSeed("member_fee_premium", "1316800", "高级家庭会员年费(分)");
- insertSysConfigSeed("member_eligible_budget", "1314", "等值商品选择预算上限(元)");
- insertSysConfigSeed("renewal_discount_rate", "0.85", "续费折扣率");
- insertSysConfigSeed("member_discount_standard", "0.9", "标准会员折扣率");
- insertSysConfigSeed("member_discount_premium", "8000", "高级家庭会员商品折扣(bps)");
- insertSysConfigSeed("member_fee_family", "131400", "家庭会员年费(分)");
- insertSysConfigSeed("member_fee_family_yearly", "131400", "家庭会员年费(分)");
- insertSysConfigSeed("member_fee_family_monthly", "13140", "家庭会员月费(分)");
- insertSysConfigSeed("member_fee_family_quarterly", "32850", "家庭会员季费(分)");
- insertSysConfigSeed("member_upgrade_threshold", "131400", "自动升级家庭会员累计消费门槛(分)");
- insertSysConfigSeed("member_discount_family", "9000", "家庭会员商品折扣(bps)");
- insertSysConfigSeed("member_fee_premium_monthly", "131400", "高级家庭会员月费(分)");
- insertSysConfigSeed("member_fee_family_original_yearly", "199900", "家庭会员年费原价(分)");
- insertSysConfigSeed("member_fee_family_original", "199900", "家庭会员原价兜底(分)");
- insertSysConfigSeed("member_fee_premium_original", "1599900", "高级会员年费原价(分)");
- insertSysConfigSeed("member_fee_premium_original_monthly", "159900", "高级会员月费原价(分)");
- insertSysConfigSeed("commission_member_l1_rate", "2000", "推荐家庭会员L1佣金率(bps)");
- insertSysConfigSeed("commission_member_l2_rate", "500", "推荐家庭会员L2佣金率(bps)");
- insertSysConfigSeed("commission_product_profit_share", "1000", "下线商品购买利润分成(bps)");
- insertSysConfigSeed("commission_service_rate", "1000", "下线服务购买佣金率(bps)");
- insertSysConfigSeed("commission_event_fixed", "2000", "推荐新用户报名活动固定佣金(分)");
- insertSysConfigSeed("withdraw_min_amount", "10000", "最低提现金额(分)");
- insertSysConfigSeed("withdraw_tax_rate", "2000", "佣金税率(bps)");
- insertSysConfigSeed("withdraw_fee_free_threshold", "80000", "免税额度(分)");
- insertSysConfigSeed("article_energy_rate_normal", "1", "普通文章能量汇率(每100字)");
- insertSysConfigSeed("article_energy_rate_premium", "2", "优质文章能量汇率(每100字)");
- insertSysConfigSeed("article_energy_rate_original", "3", "原创文章能量汇率(每100字)");
- insertSysConfigSeed("article_reading_min_seconds", "60", "文章阅读达标最少秒数");
- insertSysConfigSeed("activity_checkin_default_points", "5", "活动签到默认积分");
- insertSysConfigSeed("health_checkin_points", "5", "健康打卡积分");
- insertSysConfigSeed("finance_checkin_points", "5", "理财打卡积分");
- insertSysConfigSeed("trial_enabled", "true", "是否启用会员试用");
- insertSysConfigSeed("trial_days", "7", "试用天数");
- insertSysConfigSeed("trial_features", "[\"free_activities\",\"free_courses\",\"teacher_consultation\"]", "试用期间可用功能");
- insertSysConfigSeed("member_pay_virtual_enabled", "1", "会员支付是否使用虚拟支付通道(1=虚拟,0=普通微信)");
- insertSysConfigSeed("gift_membership_threshold", "198000", "购买送会员商品累计达到该金额(分)赠送会员");
- insertSysConfigSeed("gift_membership_level", "FAMILY", "达到送会员额度后赠送的会员等级");
- insertSysConfigSeed("gift_membership_original_threshold", "1000000", "送会员商品原价达标阈值(分),达到此金额后送会员商品部分实付一口价");
- insertSysConfigSeed("gift_membership_pay_price", "198000", "送会员商品达标后一口实付价(分)");
- insertSysConfigSeed("health_dimension_weights", "{\"growth\":0.20,\"sleep\":0.15,\"vision\":0.10,\"immunity\":0.15,\"nutrition\":0.15,\"gut\":0.10,\"exercise\":0.15}", "7维健康→身能量权重配置");
- insertSysConfigSeed("health_dimension_sync_factor", "0.5", "7维健康同步至身能量系数");
- log.info("SysConfig 种子数据已加载");
- } catch (Exception e) {
- log.warn("SysConfig 种子数据初始化失败: {}", e.getMessage());
- }
- // Product 种子数据(5 个示例商品,覆盖全部 5 种 productType)
- try {
- insertProductSeed("亲子关系工作坊", "线下半日工作坊,重建家庭对话模式", "activity", 38800, "action", "all");
- insertProductSeed("情绪管理21天训练营", "21天视频课程,识别并管理情绪", "course", 19900, "mind", "all");
- insertProductSeed("肠道菌群检测套装", "家用检测 + 报告解读", "physical", 59900, "body", "budget");
- insertProductSeed("儿童多模态认知测评", "在线测评 + 个性化建议", "digital", 9900, "wisdom", "none");
- insertProductSeed("成长规划师1对1咨询", "60分钟专业规划", "service", 50000, "wealth", "budget");
- log.info("Product 种子数据已加载");
- } catch (Exception e) {
- log.warn("Product 种子数据初始化失败: {}", e.getMessage());
- }
- // 活动种子数据(3 个示例活动,覆盖身/心/智三个维度)
- try {
- insertActivitySeed("亲子户外徒步探险", "周末亲子徒步活动,在自然中锻炼体魄、增进亲子关系,全程约5公里。",
- "body", "offline", "published",
- "2026-08-15 09:00:00", "2026-08-15 16:00:00", "北京奥林匹克森林公园", 50, 0, 0,
- "{\"body\":80,\"mind\":0,\"wisdom\":0,\"action\":20,\"wealth\":0}");
- insertActivitySeed("家庭正念静心工作坊", "专业导师带领的正念冥想体验,学习情绪调节和家庭沟通技巧。",
- "mind", "offline", "published",
- "2026-08-22 14:00:00", "2026-08-22 17:00:00", "北京市朝阳区心理成长中心", 30, 9900, 7900,
- "{\"body\":0,\"mind\":80,\"wisdom\":10,\"action\":10,\"wealth\":0}");
- insertActivitySeed("少儿科学思维探索营", "通过动手实验激发孩子对科学的兴趣,培养逻辑思维与创造力。",
- "wisdom", "online", "published",
- "2026-09-05 10:00:00", "2026-09-05 12:00:00", "线上直播", 200, 19900, 15900,
- "{\"body\":0,\"mind\":0,\"wisdom\":70,\"action\":30,\"wealth\":0}");
- log.info("Activity 种子数据已加载");
- } catch (Exception e) {
- log.warn("Activity 种子数据初始化失败: {}", e.getMessage());
- }
- // 方案规则种子数据(主链路 P0-3:模板包 + 方案生成规则)
- try {
- Long pkgFocus = ensureTemplatePackage("专注力提升方案模板",
- "针对注意力维度低分(0-60)的14天专注力提升训练", 14, "专注力");
- Long pkgKeep = ensureTemplatePackage("注意力巩固方案模板",
- "针对注意力维度正常(61-100)的14天巩固训练", 14, "注意力");
- ensureTemplateItems(pkgFocus, new String[][]{
- {"舒尔特方格训练", "每日5分钟舒尔特方格,提升视觉专注", "5", "daily"},
- {"静坐冥想", "每日10分钟静坐冥想,训练抗干扰能力", "3", "daily"},
- {"番茄钟作业法", "使用番茄钟完成当日作业,25分钟一节", "5", "daily"}
- });
- ensureTemplateItems(pkgKeep, new String[][]{
- {"专注力保持打卡", "每日记录专注时长,保持良好习惯", "3", "daily"},
- {"优质睡眠打卡", "每日21:30前入睡,保障注意力恢复", "3", "daily"},
- {"亲子共读", "每周3次亲子共读,培养持续专注", "5", "weekly"}
- });
- ensurePlanRule("attention", 0, 60, pkgFocus, 1);
- ensurePlanRule("attention", 61, 100, pkgKeep, 2);
- log.info("方案规则种子数据已加载: 模板包 focus={}, keep={}", pkgFocus, pkgKeep);
- } catch (Exception e) {
- log.warn("方案规则种子数据初始化失败: {}", e.getMessage());
- }
- // 报告解析中台:注册菌群检测报告类型(主链路 P0-4)
- try {
- Integer count = jdbcTemplate.queryForObject(
- "SELECT COUNT(*) FROM report_type_registry WHERE type_id = ?", Integer.class, "gut_flora");
- if (count == null || count == 0) {
- jdbcTemplate.update(
- "INSERT INTO report_type_registry (type_id, family, display_name, description, parser_path, " +
- "min_confidence, is_active, source, review_status, version, created_at, updated_at) " +
- "VALUES ('gut_flora', 'gut_flora', '菌群检测', '募极生物/北京肠道菌群检测报告', " +
- "'gut_flora/extract_gut_flora.py', 30, 1, 'prebuilt', 'approved', '1.0', NOW(), NOW())");
- log.info("已注册报告类型 gut_flora");
- } else {
- log.info("报告类型 gut_flora 已存在,跳过");
- }
- } catch (Exception e) {
- log.warn("报告类型注册失败: {}", e.getMessage());
- }
- // 文章种子数据(3 个示例文章,覆盖学习力/情绪管理/亲子教育分类)
- try {
- insertArticleSeed("如何培养孩子的自主学习力",
- "从内在动机出发,帮助孩子建立自主学习习惯的实用方法指南。",
- "article", 4L, "wisdom", "published", "曹老师",
- "normal", 5, 1800,
- "{\"body\":0,\"mind\":0,\"wisdom\":60,\"action\":40,\"wealth\":0}");
- insertArticleSeed("儿童情绪管理的五个关键步骤",
- "当孩子情绪爆发时,家长应该如何应对?本文提供一套可操作的情绪管理框架。",
- "article", 2L, "mind", "published", "李老师",
- "premium", 4, 2200,
- "{\"body\":0,\"mind\":80,\"wisdom\":10,\"action\":10,\"wealth\":0}");
- insertArticleSeed("亲子运动的正确打开方式",
- "不同年龄段孩子的运动建议,让全家人在快乐中收获健康。",
- "article", 3L, "action", "published", "王老师",
- "normal", 3, 1500,
- "{\"body\":40,\"mind\":0,\"wisdom\":0,\"action\":60,\"wealth\":0}");
- log.info("Article 种子数据已加载");
- } catch (Exception e) {
- log.warn("Article 种子数据初始化失败: {}", e.getMessage());
- }
- // 补充商品种子数据(2 个,丰富商品库)
- try {
- insertProductSeed("少儿专注力训练教具盒", "含舒尔特方格、记忆卡片等8种专注力训练工具,适合5-12岁儿童。", "physical", 16800, "wisdom", "all");
- insertProductSeed("家庭年度VIP成长会员", "一年内畅享全部线上课程、活动折扣及规划师咨询服务。", "digital", 298000, "action", "all");
- log.info("补充 Product 种子数据已加载");
- } catch (Exception e) {
- log.warn("补充 Product 种子数据初始化失败: {}", e.getMessage());
- }
- // ==================== Phase 2: 内容区块可见性配置 ====== // 内容区块配置表
- try {
- jdbcTemplate.execute(
- "CREATE TABLE IF NOT EXISTS content_sections (" +
- " id BIGINT AUTO_INCREMENT PRIMARY KEY," +
- " page_key VARCHAR(50) NOT NULL," +
- " section_key VARCHAR(50) NOT NULL," +
- " title VARCHAR(100)," +
- " allowed_roles VARCHAR(255) DEFAULT '[\"anonymous\"]'," +
- " sort_order INT DEFAULT 0," +
- " status VARCHAR(20) DEFAULT 'active'," +
- " created_at DATETIME DEFAULT CURRENT_TIMESTAMP," +
- " updated_at DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP" +
- ")"
- );
- log.info("content_sections 表已创建");
- } catch (Exception e) {
- log.warn("创建 content_sections 表失败: {}", e.getMessage());
- }
- // ContentSection 种子数据
- try {
- insertContentSectionSeed("mind", "func_entries", "功能入口", "[\"anonymous\"]", 1);
- insertContentSectionSeed("mind", "daily_tip", "每日心理", "[\"anonymous\"]", 2);
- insertContentSectionSeed("mind", "recommended_reading", "推荐阅读", "[\"anonymous\"]", 3);
- insertContentSectionSeed("mind", "premium_content", "会员专属内容", "[\"parent\",\"teacher\"]", 4);
- insertContentSectionSeed("mind", "mind_tasks", "心智任务", "[\"parent\",\"child\"]", 5);
- insertContentSectionSeed("mind", "related_activities", "相关活动", "[\"parent\",\"child\"]", 6);
- insertContentSectionSeed("mind", "purchased_courses", "已购课程", "[\"parent\",\"child\"]", 7);
- insertContentSectionSeed("body", "func_entries", "功能入口", "[\"anonymous\"]", 1);
- insertContentSectionSeed("body", "daily_stats", "今日数据", "[\"anonymous\"]", 2);
- insertContentSectionSeed("body", "health_tips", "健康小贴士", "[\"anonymous\"]", 3);
- insertContentSectionSeed("body", "teacher_tips", "规划师推荐", "[\"teacher\"]", 4);
- log.info("ContentSection 种子数据已加载");
- } catch (Exception e) {
- log.warn("ContentSection 种子数据初始化失败: {}", e.getMessage());
- }
- // ==================== 文章内容系统种子数据 ====== // 文章分类种子数据
- try {
- Integer catCount = jdbcTemplate.queryForObject(
- "SELECT COUNT(*) FROM article_categories WHERE id IN (1,2,3,4,5)", Integer.class);
- if (catCount == null || catCount == 0) {
- jdbcTemplate.execute("INSERT IGNORE INTO article_categories (id, name, icon, color, sort_order, status) VALUES " +
- "(1, '心理健康', '🧠', '#5B9BD5', 1, 1), " +
- "(2, '情绪管理', '💖', '#FF6B35', 2, 1), " +
- "(3, '亲子教育', '👨👩👧👦', '#4CAF50', 3, 1), " +
- "(4, '学习力', '📚', '#FFD700', 4, 1), " +
- "(5, '专注力', '🎯', '#8D6E63', 5, 1)");
- log.info("文章分类种子数据已加载");
- }
- } catch (Exception e) {
- log.warn("文章分类种子数据初始化失败: {}", e.getMessage());
- }
- // ==================== 佣金系统 ====== // 迁移: users表添加推荐人字段
- try {
- Integer exists = jdbcTemplate.queryForObject(
- "SELECT COUNT(*) FROM information_schema.COLUMNS WHERE TABLE_SCHEMA = DATABASE() AND TABLE_NAME = 'users' AND COLUMN_NAME = 'referrer_id'",
- Integer.class);
- if (exists == null || exists == 0) {
- jdbcTemplate.execute("ALTER TABLE users ADD COLUMN referrer_id BIGINT DEFAULT NULL COMMENT '推荐人ID'");
- jdbcTemplate.execute("ALTER TABLE users ADD COLUMN referral_code VARCHAR(32) DEFAULT NULL COMMENT '个人邀请码'");
- jdbcTemplate.execute("ALTER TABLE users ADD INDEX idx_referral_code (referral_code)");
- log.info("已添加佣金字段到users表");
- } else {
- log.info("佣金字段已存在,跳过");
- }
- } catch (Exception e) {
- log.warn("检查/添加佣金字段失败: {}", e.getMessage());
- }
- // 迁移: products表添加利润率字段
- try {
- Integer exists = jdbcTemplate.queryForObject(
- "SELECT COUNT(*) FROM information_schema.COLUMNS WHERE TABLE_SCHEMA = DATABASE() AND TABLE_NAME = 'products' AND COLUMN_NAME = 'profit_rate'",
- Integer.class);
- if (exists == null || exists == 0) {
- jdbcTemplate.execute("ALTER TABLE products ADD COLUMN profit_rate INT DEFAULT 0 COMMENT '平台利润率(bps,100=1%)'");
- log.info("已添加profit_rate列到products表");
- } else {
- log.info("profit_rate列已存在,跳过");
- }
- } catch (Exception e) {
- log.warn("检查/添加profit_rate列失败: {}", e.getMessage());
- }
- // 新表: commission_records (佣金记录)
- try {
- jdbcTemplate.execute("CREATE TABLE IF NOT EXISTS commission_records (" +
- "id BIGINT AUTO_INCREMENT PRIMARY KEY, " +
- "order_id BIGINT NOT NULL COMMENT '订单ID', " +
- "order_type VARCHAR(32) NOT NULL COMMENT '订单类型: membership/product/assessment/package', " +
- "referrer_id BIGINT NOT NULL COMMENT '推荐人ID', " +
- "buyer_id BIGINT NOT NULL COMMENT '购买人ID', " +
- "commission_type VARCHAR(16) NOT NULL COMMENT '佣金类型: member/profit', " +
- "order_amount INT NOT NULL COMMENT '订单金额(分)', " +
- "profit_rate INT DEFAULT 0 COMMENT '利润率(bps,100=1%)', " +
- "commission_amount INT NOT NULL COMMENT '佣金金额(分)', " +
- "status VARCHAR(16) NOT NULL DEFAULT 'settled' COMMENT '状态: pending/settled', " +
- "remark VARCHAR(255) DEFAULT NULL, " +
- "created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, " +
- "INDEX idx_referrer (referrer_id), " +
- "INDEX idx_buyer (buyer_id), " +
- "INDEX idx_order (order_id, order_type)" +
- ") ENGINE=InnoDB DEFAULT CHARSET=utf8mb4");
- log.info("已创建commission_records表");
- } catch (Exception e) {
- log.warn("创建commission_records表失败: {}", e.getMessage());
- }
- // 新表: withdrawal_requests (提现申请)
- try {
- jdbcTemplate.execute("CREATE TABLE IF NOT EXISTS withdrawal_requests (" +
- "id BIGINT AUTO_INCREMENT PRIMARY KEY, " +
- "user_id BIGINT NOT NULL COMMENT '用户ID', " +
- "amount INT NOT NULL COMMENT '提现金额(分)', " +
- "account_info TEXT COMMENT '账户信息(JSON)', " +
- "status VARCHAR(16) NOT NULL DEFAULT 'pending' COMMENT '状态: pending/approved/rejected', " +
- "audit_by BIGINT DEFAULT NULL COMMENT '审核人ID', " +
- "audit_at DATETIME DEFAULT NULL COMMENT '审核时间', " +
- "remark VARCHAR(255) DEFAULT NULL COMMENT '审核备注', " +
- "created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, " +
- "INDEX idx_user (user_id), " +
- "INDEX idx_status (status)" +
- ") ENGINE=InnoDB DEFAULT CHARSET=utf8mb4");
- log.info("已创建withdrawal_requests表");
- } catch (Exception e) {
- log.warn("创建withdrawal_requests表失败: {}", e.getMessage());
- }
- // ==================== 会员体系 & 佣金扩展 ====== // 迁移: users表添加会员字段
- try {
- Integer exists = jdbcTemplate.queryForObject(
- "SELECT COUNT(*) FROM information_schema.COLUMNS WHERE TABLE_SCHEMA = DATABASE() AND TABLE_NAME = 'users' AND COLUMN_NAME = 'member_level'",
- Integer.class);
- if (exists == null || exists == 0) {
- jdbcTemplate.execute("ALTER TABLE users ADD COLUMN member_level VARCHAR(20) DEFAULT 'FREE' COMMENT '会员等级: FREE/FAMILY/PROVIDER'");
- jdbcTemplate.execute("ALTER TABLE users ADD COLUMN member_expire_time DATETIME DEFAULT NULL COMMENT '家庭会员到期时间'");
- jdbcTemplate.execute("ALTER TABLE users ADD COLUMN total_consumption BIGINT DEFAULT 0 COMMENT '累计消费金额(分)'");
- jdbcTemplate.execute("ALTER TABLE users ADD COLUMN upgrade_from_consumption TINYINT DEFAULT 0 COMMENT '是否通过累计消费自动升级'");
- jdbcTemplate.execute("ALTER TABLE users ADD COLUMN direct_count INT DEFAULT 0 COMMENT '直接推荐人数'");
- jdbcTemplate.execute("ALTER TABLE users ADD COLUMN indirect_count INT DEFAULT 0 COMMENT '间接推荐人数'");
- jdbcTemplate.execute("ALTER TABLE users ADD COLUMN total_commission_earned BIGINT DEFAULT 0 COMMENT '累计获得佣金(分)'");
- log.info("已添加会员字段到users表");
- } else {
- log.info("会员字段已存在,跳过");
- }
- } catch (Exception e) {
- log.warn("检查/添加会员字段失败: {}", e.getMessage());
- }
- // 迁移: commission_records表添加扩展字段
- try {
- Integer exists = jdbcTemplate.queryForObject(
- "SELECT COUNT(*) FROM information_schema.COLUMNS WHERE TABLE_SCHEMA = DATABASE() AND TABLE_NAME = 'commission_records' AND COLUMN_NAME = 'level'",
- Integer.class);
- if (exists == null || exists == 0) {
- jdbcTemplate.execute("ALTER TABLE commission_records ADD COLUMN level INT DEFAULT 1 COMMENT '佣金层级: 1=直接推荐, 2=间接推荐'");
- jdbcTemplate.execute("ALTER TABLE commission_records ADD COLUMN settled_at DATETIME DEFAULT NULL COMMENT '结算时间'");
- jdbcTemplate.execute("ALTER TABLE commission_records ADD COLUMN withdrawn_at DATETIME DEFAULT NULL COMMENT '提现时间'");
- log.info("已添加扩展字段到commission_records表");
- } else {
- log.info("commission_records扩展字段已存在,跳过");
- }
- } catch (Exception e) {
- log.warn("检查/添加commission_records扩展字段失败: {}", e.getMessage());
- }
- // 新表: member_upgrade_records (会员升级记录)
- try {
- jdbcTemplate.execute("CREATE TABLE IF NOT EXISTS member_upgrade_records (" +
- "id BIGINT AUTO_INCREMENT PRIMARY KEY, " +
- "user_id BIGINT NOT NULL COMMENT '用户ID', " +
- "from_level VARCHAR(20) NOT NULL COMMENT '升级前等级', " +
- "to_level VARCHAR(20) NOT NULL COMMENT '升级后等级', " +
- "upgrade_type VARCHAR(20) NOT NULL COMMENT '升级类型: pay/consumption/admin', " +
- "order_id BIGINT DEFAULT NULL COMMENT '关联支付订单ID', " +
- "consumption_amount BIGINT DEFAULT NULL COMMENT '触发升级的累计消费金额', " +
- "expire_time DATETIME DEFAULT NULL COMMENT '家庭会员到期时间', " +
- "created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, " +
- "INDEX idx_user (user_id), " +
- "INDEX idx_order (order_id)" +
- ") ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='会员升级记录'");
- log.info("已创建member_upgrade_records表");
- } catch (Exception e) {
- log.warn("创建member_upgrade_records表失败: {}", e.getMessage());
- }
- // ==================== 文章内容系统 ====== // 文章分类表
- try {
- jdbcTemplate.execute("CREATE TABLE IF NOT EXISTS article_categories (" +
- "id BIGINT AUTO_INCREMENT PRIMARY KEY, " +
- "name VARCHAR(50) NOT NULL COMMENT '分类名', " +
- "icon VARCHAR(20) DEFAULT '' COMMENT '图标emoji', " +
- "color VARCHAR(20) DEFAULT '#5B9BD5' COMMENT '标识色', " +
- "sort_order INT DEFAULT 0 COMMENT '排序', " +
- "status TINYINT DEFAULT 1 COMMENT '1启用/0禁用'" +
- ") ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='文章分类'");
- log.info("已创建article_categories表");
- } catch (Exception e) {
- log.warn("创建article_categories表失败: {}", e.getMessage());
- }
- // 文章表
- try {
- jdbcTemplate.execute("CREATE TABLE IF NOT EXISTS articles (" +
- "id BIGINT AUTO_INCREMENT PRIMARY KEY, " +
- "category_id BIGINT DEFAULT 0 COMMENT '所属分类ID', " +
- "title VARCHAR(200) NOT NULL COMMENT '标题', " +
- "summary VARCHAR(500) DEFAULT '' COMMENT '摘要', " +
- "cover_image VARCHAR(500) DEFAULT '' COMMENT '封面图URL', " +
- "content LONGTEXT COMMENT '富文本内容', " +
- "tags VARCHAR(200) DEFAULT '' COMMENT '标签JSON数组', " +
- "author VARCHAR(100) DEFAULT '浠艾福' COMMENT '作者', " +
- "read_time INT DEFAULT 0 COMMENT '预计阅读分钟数', " +
- "related_dimensions VARCHAR(100) DEFAULT '' COMMENT '关联五维JSON数组', " +
- "visibility VARCHAR(20) DEFAULT 'public' COMMENT '浏览权限:public/login/private', " +
- "visible_to TEXT COMMENT '私密指定人群JSON', " +
- "status VARCHAR(20) DEFAULT 'draft' COMMENT 'draft/published/archived', " +
- "is_featured TINYINT DEFAULT 0 COMMENT '1精选/0普通', " +
- "published_at DATETIME DEFAULT NULL COMMENT '发布时间', " +
- "view_count INT DEFAULT 0 COMMENT '浏览次数', " +
- "created_by BIGINT DEFAULT 0 COMMENT '发布人adminID', " +
- "created_at DATETIME DEFAULT CURRENT_TIMESTAMP, " +
- "updated_at DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP" +
- ") ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='文章'");
- log.info("已创建articles表");
- } catch (Exception e) {
- log.warn("创建articles表失败: {}", e.getMessage());
- }
- // 迁移: articles表补充新字段(兼容旧表)
- try {
- Integer colExists = jdbcTemplate.queryForObject(
- "SELECT COUNT(*) FROM information_schema.COLUMNS WHERE TABLE_SCHEMA = DATABASE() AND TABLE_NAME = 'articles' AND COLUMN_NAME = 'visibility'",
- Integer.class);
- if (colExists == null || colExists == 0) {
- jdbcTemplate.execute("ALTER TABLE articles ADD COLUMN visibility VARCHAR(20) DEFAULT 'public' COMMENT '浏览权限:public/login/private' AFTER related_dimensions");
- jdbcTemplate.execute("ALTER TABLE articles ADD COLUMN visible_to TEXT COMMENT '私密指定人群JSON' AFTER visibility");
- jdbcTemplate.execute("ALTER TABLE articles ADD COLUMN tags VARCHAR(200) DEFAULT '' COMMENT '标签JSON数组' AFTER content");
- jdbcTemplate.execute("ALTER TABLE articles ADD COLUMN related_dimensions VARCHAR(100) DEFAULT '' COMMENT '关联五维JSON数组' AFTER read_time");
- jdbcTemplate.execute("ALTER TABLE articles ADD COLUMN is_featured TINYINT DEFAULT 0 COMMENT '1精选/0普通' AFTER status");
- jdbcTemplate.execute("ALTER TABLE articles ADD COLUMN published_at DATETIME DEFAULT NULL COMMENT '发布时间' AFTER is_featured");
- jdbcTemplate.execute("ALTER TABLE articles ADD COLUMN view_count INT DEFAULT 0 COMMENT '浏览次数' AFTER published_at");
- jdbcTemplate.execute("ALTER TABLE articles ADD COLUMN created_by BIGINT DEFAULT 0 COMMENT '发布人adminID' AFTER view_count");
- log.info("已迁移articles表补充字段");
- } else {
- log.info("articles表字段已完整,跳过迁移");
- }
- } catch (Exception e) {
- log.warn("articles表字段迁移失败: {}", e.getMessage());
- }
- // 迁移: articles表补充知识中心新字段(content_type/difficulty_level/dimension_ids)
- try {
- Integer colExists = jdbcTemplate.queryForObject(
- "SELECT COUNT(*) FROM information_schema.COLUMNS WHERE TABLE_SCHEMA = DATABASE() AND TABLE_NAME = 'articles' AND COLUMN_NAME = 'content_type'",
- Integer.class);
- if (colExists == null || colExists == 0) {
- jdbcTemplate.execute("ALTER TABLE articles ADD COLUMN content_type VARCHAR(20) DEFAULT 'article' COMMENT '内容类型: article/knowledge/course/tip' AFTER article_type");
- jdbcTemplate.execute("ALTER TABLE articles ADD COLUMN difficulty_level TINYINT DEFAULT 0 COMMENT '难度等级 1-5' AFTER content_type");
- jdbcTemplate.execute("ALTER TABLE articles ADD COLUMN dimension_ids TEXT COMMENT '关联维度ID列表JSON' AFTER related_dimensions");
- log.info("已迁移articles表知识中心新字段");
- } else {
- log.info("articles表知识中心字段已完整,跳过迁移");
- }
- } catch (Exception e) {
- log.warn("articles表知识中心字段迁移失败: {}", e.getMessage());
- }
- // 文章标签关联表
- try {
- jdbcTemplate.execute("CREATE TABLE IF NOT EXISTS article_tags (" +
- "id BIGINT AUTO_INCREMENT PRIMARY KEY, " +
- "article_id BIGINT NOT NULL COMMENT '文章ID', " +
- "tag_id BIGINT NOT NULL COMMENT '标签ID(关联dan_knowledge_tag.id)', " +
- "created_at DATETIME DEFAULT CURRENT_TIMESTAMP, " +
- "UNIQUE KEY uk_article_tag (article_id, tag_id)" +
- ") ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='文章-标签关联表'");
- log.info("已创建article_tags表");
- } catch (Exception e) {
- log.warn("创建article_tags表失败: {}", e.getMessage());
- }
- // ==================== 五维能量系统(账本模式) ====== // 维度定义表
- try {
- jdbcTemplate.execute(
- "CREATE TABLE IF NOT EXISTS energy_dimension (" +
- "id BIGINT AUTO_INCREMENT PRIMARY KEY, " +
- "code VARCHAR(16) NOT NULL UNIQUE COMMENT '维度代码: body/mind/wisdom/action/wealth', " +
- "name VARCHAR(20) NOT NULL COMMENT '维度名称: 身/心/智/行/富', " +
- "icon VARCHAR(16) COMMENT '图标emoji', " +
- "element VARCHAR(10) COMMENT '五行元素: 土/火/金/木/水', " +
- "sort_order INT DEFAULT 0 COMMENT '显示顺序', " +
- "status TINYINT DEFAULT 1 COMMENT '1启用/0禁用', " +
- "created_at DATETIME DEFAULT CURRENT_TIMESTAMP" +
- ") ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='能量维度定义表'"
- );
- log.info("已创建energy_dimension表");
- } catch (Exception e) {
- log.warn("创建energy_dimension表失败: {}", e.getMessage());
- }
- // 服务-维度比例配置表
- try {
- jdbcTemplate.execute(
- "CREATE TABLE IF NOT EXISTS energy_source_config (" +
- "id BIGINT AUTO_INCREMENT PRIMARY KEY, " +
- "source_type VARCHAR(32) NOT NULL COMMENT '来源类型: task/activity/course/product/consultation', " +
- "source_id BIGINT NOT NULL COMMENT '来源业务表ID', " +
- "source_name VARCHAR(200) COMMENT '冗余名称', " +
- "dimension_id BIGINT NOT NULL COMMENT '维度ID', " +
- "ratio INT NOT NULL COMMENT '占比(bps,10000=100%)', " +
- "created_at DATETIME DEFAULT CURRENT_TIMESTAMP, " +
- "INDEX idx_source (source_type, source_id)" +
- ") ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='服务-维度比例配置表'"
- );
- log.info("已创建energy_source_config表");
- } catch (Exception e) {
- log.warn("创建energy_source_config表失败: {}", e.getMessage());
- }
- // 购物车
- try {
- jdbcTemplate.execute("CREATE TABLE IF NOT EXISTS cart_items (" +
- "id BIGINT AUTO_INCREMENT PRIMARY KEY," +
- "user_id BIGINT NOT NULL COMMENT '用户ID'," +
- "product_id BIGINT NOT NULL COMMENT '商品ID'," +
- "quantity INT DEFAULT 1 COMMENT '数量'," +
- "selected TINYINT(1) DEFAULT 1 COMMENT '是否选中'," +
- "created_at DATETIME DEFAULT CURRENT_TIMESTAMP," +
- "updated_at DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP," +
- "INDEX idx_user (user_id)," +
- "INDEX idx_product (product_id)," +
- "UNIQUE KEY uk_user_product (user_id, product_id)" +
- ") ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='购物车'");
- log.info("已创建cart_items表");
- } catch (Exception e) {
- log.warn("创建cart_items表失败: {}", e.getMessage());
- }
- try {
- jdbcTemplate.execute("CREATE TABLE IF NOT EXISTS shop_categories (" +
- "id BIGINT AUTO_INCREMENT PRIMARY KEY," +
- "name VARCHAR(64) NOT NULL COMMENT '分类名称'," +
- "parent_id BIGINT DEFAULT 0 COMMENT '父分类ID(0=根节点)'," +
- "level INT DEFAULT 0 COMMENT '层级(0/1/2/3)'," +
- "sort INT DEFAULT 0 COMMENT '排序'," +
- "image VARCHAR(512) COMMENT '分类图标'," +
- "enabled TINYINT(1) DEFAULT 1 COMMENT '启用'," +
- "dimension_codes VARCHAR(128) COMMENT '维度编码(逗号分隔: body,mind,heart)'," +
- "product_types VARCHAR(128) COMMENT '商品类型(逗号分隔: PHYSICAL,COURSE,ASSESSMENT)'," +
- "min_visible_level VARCHAR(50) COMMENT '最低可见会员级别编码(NULL=全部可见)'," +
- "created_at DATETIME DEFAULT CURRENT_TIMESTAMP," +
- "updated_at DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP," +
- "INDEX idx_parent (parent_id)," +
- "INDEX idx_sort (sort)" +
- ") ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='商品类目'");
- log.info("已创建shop_categories表");
- } catch (Exception e) {
- log.warn("创建shop_categories表失败: {}", e.getMessage());
- }
- try {
- jdbcTemplate.execute("CREATE TABLE IF NOT EXISTS distribution_systems (" +
- "id BIGINT AUTO_INCREMENT PRIMARY KEY," +
- "name VARCHAR(64) NOT NULL COMMENT '体系名称'," +
- "description VARCHAR(255) COMMENT '描述'," +
- "owner_id BIGINT COMMENT '创建者ID'," +
- "profit_rate INT DEFAULT 0 COMMENT '平台利润率(千分比)'," +
- "enabled TINYINT(1) DEFAULT 1 COMMENT '启用'," +
- "created_at DATETIME DEFAULT CURRENT_TIMESTAMP," +
- "updated_at DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP" +
- ") ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='销售体系'");
- log.info("已创建distribution_systems表");
- } catch (Exception e) {
- log.warn("创建distribution_systems表失败: {}", e.getMessage());
- }
- try {
- jdbcTemplate.execute("CREATE TABLE IF NOT EXISTS distribution_relations (" +
- "id BIGINT AUTO_INCREMENT PRIMARY KEY," +
- "system_id BIGINT NOT NULL COMMENT '体系ID'," +
- "user_id BIGINT NOT NULL COMMENT '用户ID'," +
- "parent_id BIGINT COMMENT '上级用户ID(null=根)'," +
- "depth INT DEFAULT 0 COMMENT '层级深度'," +
- "created_at DATETIME DEFAULT CURRENT_TIMESTAMP," +
- "INDEX idx_system (system_id)," +
- "INDEX idx_user (user_id)," +
- "INDEX idx_parent (parent_id)" +
- ") ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='销售关系'");
- log.info("已创建distribution_relations表");
- } catch (Exception e) {
- log.warn("创建distribution_relations表失败: {}", e.getMessage());
- }
- try {
- jdbcTemplate.execute("CREATE TABLE IF NOT EXISTS after_sales_requests (" +
- "id BIGINT AUTO_INCREMENT PRIMARY KEY," +
- "order_id BIGINT NOT NULL COMMENT '订单ID'," +
- "order_no VARCHAR(32) NOT NULL COMMENT '订单号'," +
- "user_id BIGINT NOT NULL COMMENT '用户ID'," +
- "type VARCHAR(32) NOT NULL COMMENT '类型: REFUND/RETURN_REFUND'," +
- "reason VARCHAR(255) COMMENT '原因'," +
- "description TEXT COMMENT '描述'," +
- "images TEXT COMMENT '凭证图片(JSON数组)'," +
- "refund_amount INT DEFAULT 0 COMMENT '退款金额(分)'," +
- "status VARCHAR(20) DEFAULT 'pending' COMMENT '状态: pending/approved/rejected/completed'," +
- "reject_reason VARCHAR(255) COMMENT '驳回原因'," +
- "handled_by BIGINT COMMENT '处理人ID'," +
- "handled_at DATETIME COMMENT '处理时间'," +
- "created_at DATETIME DEFAULT CURRENT_TIMESTAMP," +
- "updated_at DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP," +
- "INDEX idx_order (order_id)," +
- "INDEX idx_user (user_id)," +
- "INDEX idx_status (status)" +
- ") ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='售后申请'");
- log.info("已创建after_sales_requests表");
- } catch (Exception e) {
- log.warn("创建after_sales_requests表失败: {}", e.getMessage());
- }
- try {
- jdbcTemplate.execute("CREATE TABLE IF NOT EXISTS shop_discount_rules (" +
- "id BIGINT AUTO_INCREMENT PRIMARY KEY," +
- "name VARCHAR(64) NOT NULL COMMENT '规则名称'," +
- "type VARCHAR(32) NOT NULL COMMENT '类型: PERCENT/FIXED/THRESHOLD'," +
- "value INT NOT NULL COMMENT '折扣值(PERCENT=百分比数字, FIXED/THRESHOLD=金额分)'," +
- "threshold_amount INT COMMENT '满减门槛(分,仅THRESHOLD)'," +
- "category_id BIGINT COMMENT '适用类目ID(null=全部)'," +
- "distribution_system_id BIGINT COMMENT '适用体系ID(null=全部)'," +
- "product_ids TEXT COMMENT '适用商品ID(逗号分隔,null=全部)'," +
- "start_time DATETIME COMMENT '开始时间'," +
- "end_time DATETIME COMMENT '结束时间'," +
- "max_uses INT DEFAULT 0 COMMENT '最大使用次数(0=不限)'," +
- "used_count INT DEFAULT 0 COMMENT '已使用次数'," +
- "enabled TINYINT(1) DEFAULT 1 COMMENT '启用'," +
- "created_at DATETIME DEFAULT CURRENT_TIMESTAMP," +
- "updated_at DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP," +
- "INDEX idx_category (category_id)," +
- "INDEX idx_enabled (enabled)" +
- ") ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='折扣规则'");
- log.info("已创建shop_discount_rules表");
- } catch (Exception e) {
- log.warn("创建shop_discount_rules表失败: {}", e.getMessage());
- }
- // 电商扩展字段 - products表
- try {
- jdbcTemplate.execute("ALTER TABLE products ADD COLUMN category_id BIGINT COMMENT '商品类目ID'");
- log.info("已添加category_id列到products表");
- } catch (Exception e) {
- // 列已存在,忽略错误
- }
- try {
- jdbcTemplate.execute("ALTER TABLE products ADD COLUMN distribution_system_id BIGINT COMMENT '销售体系ID'");
- log.info("已添加distribution_system_id列到products表");
- } catch (Exception e) {
- // 列已存在,忽略错误
- }
- // 电商扩展字段 - product_orders表
- try {
- jdbcTemplate.execute("ALTER TABLE product_orders ADD COLUMN discount_amount INT DEFAULT 0 COMMENT '优惠金额(分)'");
- log.info("已添加discount_amount列到product_orders表");
- } catch (Exception e) {
- // 列已存在,忽略错误
- }
- try {
- jdbcTemplate.execute("ALTER TABLE product_orders ADD COLUMN coupon_id BIGINT COMMENT '优惠券ID'");
- log.info("已添加coupon_id列到product_orders表");
- } catch (Exception e) {
- // 列已存在,忽略错误
- }
- try {
- jdbcTemplate.execute("ALTER TABLE product_orders ADD COLUMN cancel_at DATETIME COMMENT '取消时间'");
- log.info("已添加cancel_at列到product_orders表");
- } catch (Exception e) {
- // 列已存在,忽略错误
- }
- try {
- jdbcTemplate.execute("ALTER TABLE product_orders ADD COLUMN transaction_id VARCHAR(64) COMMENT '微信支付交易号'");
- log.info("已添加transaction_id列到product_orders表");
- } catch (Exception e) {
- // 列已存在,忽略错误
- }
- try {
- jdbcTemplate.execute("ALTER TABLE product_orders ADD COLUMN cover_image VARCHAR(512) COMMENT '商品封面图快照'");
- log.info("已添加cover_image列到product_orders表");
- } catch (Exception e) {
- // 列已存在,忽略错误
- }
- try {
- jdbcTemplate.execute("ALTER TABLE product_orders ADD COLUMN address_snapshot TEXT COMMENT '收货地址快照(JSON)'");
- log.info("已添加address_snapshot列到product_orders表");
- } catch (Exception e) {
- // 列已存在,忽略错误
- }
- // 能量流水表
- try {
- jdbcTemplate.execute(
- "CREATE TABLE IF NOT EXISTS energy_log (" +
- "id BIGINT AUTO_INCREMENT PRIMARY KEY, " +
- "child_id BIGINT NOT NULL COMMENT '孩子ID', " +
- "dimension_id BIGINT NOT NULL COMMENT '维度ID', " +
- "amount INT NOT NULL COMMENT '变动量(正=获得,负=消耗)', " +
- "balance_after INT NOT NULL COMMENT '变动后余额', " +
- "source_type VARCHAR(32) COMMENT '来源类型: task/product/medical_report/daily_record', " +
- "source_id BIGINT COMMENT '来源ID', " +
- "ref_id BIGINT COMMENT '关联旧流水ID(覆盖时标记)', " +
- "expires_at DATETIME COMMENT '过期时间(空=永不过期)', " +
- "description VARCHAR(500) COMMENT '描述', " +
- "created_at DATETIME DEFAULT CURRENT_TIMESTAMP, " +
- "INDEX idx_child_dim (child_id, dimension_id), " +
- "INDEX idx_created (created_at), " +
- "INDEX idx_source (source_type, source_id)" +
- ") ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='能量流水表'"
- );
- log.info("已创建energy_log表");
- } catch (Exception e) {
- log.warn("创建energy_log表失败: {}", e.getMessage());
- }
- // 维度余额表
- try {
- jdbcTemplate.execute(
- "CREATE TABLE IF NOT EXISTS energy_balance (" +
- "id BIGINT AUTO_INCREMENT PRIMARY KEY, " +
- "child_id BIGINT NOT NULL COMMENT '孩子ID', " +
- "dimension_id BIGINT NOT NULL COMMENT '维度ID', " +
- "balance INT DEFAULT 0 COMMENT '当前能量值(可为负)', " +
- "total_earned INT DEFAULT 0 COMMENT '累计获得', " +
- "total_spent INT DEFAULT 0 COMMENT '累计消耗', " +
- "updated_at DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, " +
- "UNIQUE KEY uk_child_dim (child_id, dimension_id)" +
- ") ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='维度余额表'"
- );
- log.info("已创建energy_balance表");
- } catch (Exception e) {
- log.warn("创建energy_balance表失败: {}", e.getMessage());
- }
- // 维度种子数据(按相生链: 智→富→行→心→身)
- try {
- Integer dimCount = jdbcTemplate.queryForObject(
- "SELECT COUNT(*) FROM energy_dimension", Integer.class);
- if (dimCount == null || dimCount == 0) {
- jdbcTemplate.execute("INSERT INTO energy_dimension (code, name, icon, element, sort_order, status) VALUES " +
- "('mind', '心', '🔥', '火', 4, 1), " +
- "('body', '身', '🌏', '土', 5, 1), " +
- "('wisdom', '智', '⚔️', '金', 1, 1), " +
- "('action', '行', '🌿', '木', 3, 1), " +
- "('wealth', '富', '💧', '水', 2, 1)");
- log.info("能量维度种子数据已初始化");
- }
- } catch (Exception e) {
- log.warn("初始化能量维度种子数据失败: {}", e.getMessage());
- }
- // 为已有商品插入默认比例配置(与Product.domain一致)
- try {
- Integer productConfigCount = jdbcTemplate.queryForObject(
- "SELECT COUNT(*) FROM energy_source_config WHERE source_type = 'product'", Integer.class);
- if (productConfigCount == null || productConfigCount == 0) {
- String[][] domainMapping = {
- {"action", "action"}, {"mind", "mind"}, {"body", "body"},
- {"wisdom", "wisdom"}, {"wealth", "wealth"}
- };
- for (String[] mapping : domainMapping) {
- jdbcTemplate.execute(
- "INSERT INTO energy_source_config (source_type, source_id, source_name, dimension_id, ratio) " +
- "SELECT 'product', p.id, p.name, d.id, 10000 " +
- "FROM products p " +
- "JOIN energy_dimension d ON d.code = '" + mapping[1] + "' " +
- "WHERE p.domain = '" + mapping[0] + "' " +
- "AND NOT EXISTS (" +
- " SELECT 1 FROM energy_source_config esc " +
- " WHERE esc.source_type = 'product' AND esc.source_id = p.id" +
- ")"
- );
- }
- log.info("已有商品能量比例配置已初始化");
- }
- } catch (Exception e) {
- log.warn("初始化商品能量比例配置失败: {}", e.getMessage());
- }
- // ==================== 精准营养 P0: health_report_survey =============
- try {
- jdbcTemplate.execute("CREATE TABLE IF NOT EXISTS health_report_survey (" +
- "id BIGINT AUTO_INCREMENT PRIMARY KEY, " +
- "report_id BIGINT NOT NULL COMMENT '关联报告ID', " +
- "user_id BIGINT NOT NULL COMMENT '用户ID', " +
- "child_id BIGINT NOT NULL COMMENT '孩子ID', " +
- "survey_data JSON COMMENT '调研问卷结果JSON', " +
- "status VARCHAR(20) DEFAULT 'pending' COMMENT '状态: pending/completed', " +
- "created_at DATETIME DEFAULT CURRENT_TIMESTAMP, " +
- "updated_at DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, " +
- "UNIQUE KEY uk_report (report_id)" +
- ") ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='报告调研问卷'");
- log.info("已创建 health_report_survey 表");
- } catch (Exception e) {
- log.warn("创建 health_report_survey 表失败: {}", e.getMessage());
- }
- // ==================== 精准营养 P0: health_gut_flora + health_disease_risk =============
-
- try {
- jdbcTemplate.execute("CREATE TABLE IF NOT EXISTS health_gut_flora (" +
- "id BIGINT AUTO_INCREMENT PRIMARY KEY, " +
- "report_id BIGINT NOT NULL COMMENT '关联报告ID', " +
- "bacteria_name VARCHAR(100) NULL COMMENT '菌种名称', " +
- "bacteria_value VARCHAR(50) COMMENT '菌种丰度值', " +
- "normal_range VARCHAR(100) COMMENT '正常范围', " +
- "status VARCHAR(20) COMMENT '状态: 正常/偏低/偏高', " +
- "category VARCHAR(50) COMMENT '分类: 有益菌/有害菌/中性菌', " +
- "sort_order INT DEFAULT 0 COMMENT '排序', " +
- "created_at DATETIME DEFAULT CURRENT_TIMESTAMP, " +
- "INDEX idx_report (report_id)" +
- ") ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='菌群检测专项数据'");
- log.info("已创建 health_gut_flora 表");
- } catch (Exception e) {
- log.warn("创建 health_gut_flora 表失败: {}", e.getMessage());
- }
- try {
- jdbcTemplate.execute("CREATE TABLE IF NOT EXISTS health_disease_risk (" +
- "id BIGINT AUTO_INCREMENT PRIMARY KEY, " +
- "report_id BIGINT NOT NULL COMMENT '关联报告ID', " +
- "disease_name VARCHAR(100) NOT NULL COMMENT '疾病名称', " +
- "risk_value VARCHAR(50) COMMENT '风险值(0-1)', " +
- "risk_level VARCHAR(20) COMMENT '风险等级: 高风险/需注意/低风险', " +
- "created_at DATETIME DEFAULT CURRENT_TIMESTAMP, " +
- "INDEX idx_report (report_id)" +
- ") ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='疾病风险评估数据'");
- log.info("已创建 health_disease_risk 表");
- } catch (Exception e) {
- log.warn("创建 health_disease_risk 表失败: {}", e.getMessage());
- }
- // ==================== 迁移: health_reports 表添加新字段 =============
- try {
- jdbcTemplate.execute("ALTER TABLE health_reports ADD COLUMN balance_score INT DEFAULT NULL COMMENT '菌群平衡分'");
- log.info("已添加balance_score列到health_reports表");
- } catch (Exception e) {
- // 列已存在,忽略错误
- }
- try {
- jdbcTemplate.execute("ALTER TABLE health_reports ADD COLUMN diversity_score INT DEFAULT NULL COMMENT '菌群多样性分'");
- log.info("已添加diversity_score列到health_reports表");
- } catch (Exception e) {
- // 列已存在,忽略错误
- }
- try {
- jdbcTemplate.execute("ALTER TABLE health_reports ADD COLUMN beneficial_score INT DEFAULT NULL COMMENT '有益菌评分'");
- log.info("已添加beneficial_score列到health_reports表");
- } catch (Exception e) {
- // 列已存在,忽略错误
- }
- try {
- jdbcTemplate.execute("ALTER TABLE health_reports ADD COLUMN harmful_score INT DEFAULT NULL COMMENT '有害菌评分'");
- log.info("已添加harmful_score列到health_reports表");
- } catch (Exception e) {
- // 列已存在,忽略错误
- }
- try {
- jdbcTemplate.execute("ALTER TABLE health_reports ADD COLUMN core_genus_score INT DEFAULT NULL COMMENT '核心菌属评分'");
- log.info("已添加core_genus_score列到health_reports表");
- } catch (Exception e) {
- // 列已存在,忽略错误
- }
- // 迁移: health_reports 表添加报告编号和姓名字段
- try {
- jdbcTemplate.execute("ALTER TABLE health_reports ADD COLUMN report_number VARCHAR(50) DEFAULT NULL COMMENT '报告编号'");
- log.info("已添加report_number列到health_reports表");
- } catch (Exception e) {
- // 列已存在,忽略错误
- }
- try {
- jdbcTemplate.execute("ALTER TABLE health_reports ADD COLUMN person_name VARCHAR(50) DEFAULT NULL COMMENT '被检测人姓名'");
- log.info("已添加person_name列到health_reports表");
- } catch (Exception e) {
- // 列已存在,忽略错误
- }
- // ==================== 迁移: health_gut_flora 表添加新字段 =============
- try {
- jdbcTemplate.execute("ALTER TABLE health_gut_flora ADD COLUMN population_level VARCHAR(50) DEFAULT NULL COMMENT '处于人群%水平'");
- log.info("已添加population_level列到health_gut_flora表");
- } catch (Exception e) {
- // 列已存在,忽略错误
- }
- try {
- jdbcTemplate.execute("ALTER TABLE health_gut_flora ADD COLUMN detection_rate VARCHAR(50) DEFAULT NULL COMMENT '%正常人有检出'");
- log.info("已添加detection_rate列到health_gut_flora表");
- } catch (Exception e) {
- // 列已存在,忽略错误
- }
- try {
- jdbcTemplate.execute("ALTER TABLE health_gut_flora ADD COLUMN description TEXT DEFAULT NULL COMMENT '菌种说明'");
- log.info("已添加description列到health_gut_flora表");
- } catch (Exception e) {
- // 列已存在,忽略错误
- }
- try {
- jdbcTemplate.execute("ALTER TABLE health_gut_flora ADD COLUMN `level` VARCHAR(20) DEFAULT 'GENUS' COMMENT '级别: GENUS/SPECIES'");
- log.info("已添加level列到health_gut_flora表");
- } catch (Exception e) {
- // 列已存在,忽略错误
- }
- // ==================== 迁移: guides 表统一 ====== // 迁移: guides 添加 type 字段(区分服务商类型)
- try {
- jdbcTemplate.execute("ALTER TABLE guides ADD COLUMN type VARCHAR(32) DEFAULT 'teacher' COMMENT '服务商类型: teacher/planner/activity_provider/product_supplier'");
- log.info("已添加type列到guides表");
- } catch (Exception e) {
- // 列已存在,忽略错误
- }
- // 迁移: guides 添加 teachers 合并字段
- try {
- jdbcTemplate.execute("ALTER TABLE guides ADD COLUMN teacher_no VARCHAR(20) COMMENT '服务商编号'");
- log.info("已添加teacher_no列到guides表");
- } catch (Exception e) {
- }
- try {
- jdbcTemplate.execute("ALTER TABLE guides ADD COLUMN title VARCHAR(50) COMMENT '职称/称号'");
- } catch (Exception e) {
- }
- try {
- jdbcTemplate.execute("ALTER TABLE guides ADD COLUMN specialty VARCHAR(200) COMMENT '专长领域'");
- } catch (Exception e) {
- }
- try {
- jdbcTemplate.execute("ALTER TABLE guides ADD COLUMN bio TEXT COMMENT '简介'");
- } catch (Exception e) {
- }
- try {
- jdbcTemplate.execute("ALTER TABLE guides ADD COLUMN experience_years TINYINT DEFAULT 0 COMMENT '从业年限'");
- } catch (Exception e) {
- }
- try {
- jdbcTemplate.execute("ALTER TABLE guides ADD COLUMN rating INT DEFAULT 500 COMMENT '评分(百分位,500=5.00)'");
- } catch (Exception e) {
- }
- try {
- jdbcTemplate.execute("ALTER TABLE guides ADD COLUMN total_appointments INT DEFAULT 0 COMMENT '总服务次数'");
- } catch (Exception e) {
- }
- try {
- jdbcTemplate.execute("ALTER TABLE guides ADD COLUMN street_id BIGINT COMMENT '街道ID'");
- } catch (Exception e) {
- }
- try {
- jdbcTemplate.execute("ALTER TABLE guides ADD COLUMN province VARCHAR(50) COMMENT '省份'");
- jdbcTemplate.execute("ALTER TABLE guides ADD COLUMN city VARCHAR(50) COMMENT '城市'");
- jdbcTemplate.execute("ALTER TABLE guides ADD COLUMN district VARCHAR(50) COMMENT '区县'");
- jdbcTemplate.execute("ALTER TABLE guides ADD COLUMN street VARCHAR(100) COMMENT '街道'");
- log.info("已添加地址字段到guides表");
- } catch (Exception e) {
- }
- try {
- jdbcTemplate.execute("ALTER TABLE guides ADD COLUMN verified_at DATETIME COMMENT '审核通过时间'");
- } catch (Exception e) {
- }
- // 迁移: 将 teachers 表数据合并到 guides(type='teacher')
- try {
- int migrated = jdbcTemplate.update(
- "INSERT IGNORE INTO guides (user_id, type, teacher_no, title, specialty, bio, experience_years, rating, total_appointments, " +
- "street_id, province, city, district, street, verified_at, status, level, created_at, updated_at) " +
- "SELECT t.user_id, 'teacher', t.teacher_no, t.title, t.specialty, t.bio, t.experience_years, t.rating, t.total_appointments, " +
- "t.street_id, t.province, t.city, t.district, t.street, t.verified_at, " +
- "CASE WHEN t.status = 'active' THEN 'active' ELSE 'inactive' END, " +
- "CASE WHEN t.verification_status = 'approved' THEN 'senior' WHEN t.verification_status = 'pending' THEN 'junior' ELSE 'junior' END, " +
- "t.created_at, t.updated_at " +
- "FROM teachers t " +
- "LEFT JOIN guides g ON g.user_id = t.user_id " +
- "WHERE g.id IS NULL"
- );
- if (migrated > 0) {
- log.info("已从 teachers 表迁移 {} 条数据到 guides(type=teacher)", migrated);
- }
- } catch (Exception e) {
- log.warn("迁移 teachers → guides 数据失败: {}", e.getMessage());
- }
- // 迁移: dan_assessment_results 表添加认知维度字段(6维认知体系扩展)
- try {
- Integer colExists = jdbcTemplate.queryForObject(
- "SELECT COUNT(*) FROM information_schema.COLUMNS WHERE TABLE_SCHEMA = DATABASE() AND TABLE_NAME = 'dan_assessment_results' AND COLUMN_NAME = 'perception_score'",
- Integer.class);
- if (colExists == null || colExists == 0) {
- jdbcTemplate.execute("ALTER TABLE dan_assessment_results " +
- "ADD COLUMN perception_score INT COMMENT '感知得分', " +
- "ADD COLUMN spatial_score INT COMMENT '空间思维得分', " +
- "ADD COLUMN processing_speed_score INT COMMENT '加工速度得分'");
- log.info("已为 dan_assessment_results 表添加认知维度字段");
- } else {
- log.info("dan_assessment_results 认知维度字段已存在,跳过");
- }
- } catch (Exception e) {
- log.warn("检查/添加 dan_assessment_results 认知维度字段失败: {}", e.getMessage());
- }
- // 迁移: dan_assessment_results 表添加 EMI 维度字段(v1.2 心智模型重构)
- try {
- Integer colExists = jdbcTemplate.queryForObject(
- "SELECT COUNT(*) FROM information_schema.COLUMNS WHERE TABLE_SCHEMA = DATABASE() AND TABLE_NAME = 'dan_assessment_results' AND COLUMN_NAME = 'emotion_management_score'",
- Integer.class);
- if (colExists == null || colExists == 0) {
- jdbcTemplate.execute("ALTER TABLE dan_assessment_results " +
- "ADD COLUMN emotion_management_score INT COMMENT '情绪管理得分(EMI)', " +
- "ADD COLUMN empathy_score INT COMMENT '同理心得分(EMI)', " +
- "ADD COLUMN social_adaptability_score INT COMMENT '社会适应得分(EMI)', " +
- "ADD COLUMN self_motivation_score INT COMMENT '自我激励得分(EMI)'");
- log.info("已为 dan_assessment_results 表添加 EMI 维度字段");
- } else {
- log.info("dan_assessment_results EMI 维度字段已存在,跳过");
- }
- } catch (Exception e) {
- log.warn("检查/添加 dan_assessment_results EMI 维度字段失败: {}", e.getMessage());
- }
- // 迁移: dan_assessment_results 表添加 EMI 心理子维度字段
- try {
- Integer colExists = jdbcTemplate.queryForObject(
- "SELECT COUNT(*) FROM information_schema.COLUMNS WHERE TABLE_SCHEMA = DATABASE() AND TABLE_NAME = 'dan_assessment_results' AND COLUMN_NAME = 'emotion_score'",
- Integer.class);
- if (colExists == null || colExists == 0) {
- jdbcTemplate.execute("ALTER TABLE dan_assessment_results " +
- "ADD COLUMN emotion_score INT COMMENT '情绪商数(0-100)', " +
- "ADD COLUMN resilience_score INT COMMENT '心理韧性(0-100)', " +
- "ADD COLUMN stress_coping_score INT COMMENT '压力应对(0-100)', " +
- "ADD COLUMN self_awareness_score INT COMMENT '自我认知(0-100)'");
- log.info("已为 dan_assessment_results 表添加 EMI 心理子维度字段");
- } else {
- log.info("dan_assessment_results EMI 心理子维度字段已存在,跳过");
- }
- } catch (Exception e) {
- log.warn("检查/添加 dan_assessment_results EMI 心理子维度字段失败: {}", e.getMessage());
- }
- // 迁移: dan_assessment_results 表添加大五人格维度字段
- try {
- Integer colExists = jdbcTemplate.queryForObject(
- "SELECT COUNT(*) FROM information_schema.COLUMNS WHERE TABLE_SCHEMA = DATABASE() AND TABLE_NAME = 'dan_assessment_results' AND COLUMN_NAME = 'openness_score'",
- Integer.class);
- if (colExists == null || colExists == 0) {
- jdbcTemplate.execute("ALTER TABLE dan_assessment_results " +
- "ADD COLUMN openness_score INT COMMENT '开放性(0-100)', " +
- "ADD COLUMN conscientiousness_score INT COMMENT '尽责性(0-100)', " +
- "ADD COLUMN extraversion_score INT COMMENT '外向性(0-100)', " +
- "ADD COLUMN agreeableness_score INT COMMENT '宜人性(0-100)', " +
- "ADD COLUMN neuroticism_score INT COMMENT '神经质(0-100)', " +
- "ADD COLUMN big_five_overall_score INT COMMENT '大五人格综合分'");
- log.info("已为 dan_assessment_results 表添加大五人格维度字段");
- } else {
- log.info("dan_assessment_results 大五人格维度字段已存在,跳过");
- }
- } catch (Exception e) {
- log.warn("检查/添加 dan_assessment_results 大五人格维度字段失败: {}", e.getMessage());
- }
- // 迁移: users 表添加出生时辰字段
- try {
- jdbcTemplate.execute("ALTER TABLE users ADD COLUMN birth_hour VARCHAR(10) COMMENT '出生时辰: 子丑寅卯辰巳午未申酉戌亥'");
- log.info("已添加birth_hour列到users表");
- } catch (Exception e) {
- // 列已存在,忽略错误
- }
- // 迁移: 重建 family_member_attributes 表结构(v1.2 心智模型)
- try {
- Integer colExists = jdbcTemplate.queryForObject(
- "SELECT COUNT(*) FROM information_schema.COLUMNS WHERE TABLE_SCHEMA = DATABASE() AND TABLE_NAME = 'family_member_attributes' AND COLUMN_NAME = 'member_id'",
- Integer.class);
- if (colExists == null || colExists == 0) {
- // 旧表存在但结构不同,先删除旧表重建
- jdbcTemplate.execute("DROP TABLE IF EXISTS family_member_attributes_old");
- jdbcTemplate.execute("RENAME TABLE family_member_attributes TO family_member_attributes_old");
- jdbcTemplate.execute("CREATE TABLE IF NOT EXISTS family_member_attributes (" +
- "id BIGINT AUTO_INCREMENT PRIMARY KEY, " +
- "member_id BIGINT NOT NULL COMMENT '家庭成员ID(children表或users表)', " +
- "member_type VARCHAR(10) NOT NULL COMMENT '成员类型: child/parent', " +
- "birth_datetime DATETIME COMMENT '出生日期(精确到分钟,用于八字排盘)', " +
- "eight_characters JSON COMMENT '八字四柱JSON', " +
- "wuxing_elements JSON COMMENT '五行星盘元素JSON', " +
- "zodiac VARCHAR(16) COMMENT '生肖: rat/ox/tiger/rabbit/dragon/snake/horse/sheep/monkey/rooster/dog/pig', " +
- "blood_type VARCHAR(5) COMMENT '血型: A/B/AB/O', " +
- "mind_base_score INT DEFAULT 0 COMMENT '心维度基础分(百分位,500=5.00)', " +
- "wisdom_base_score INT DEFAULT 0 COMMENT '智维度基础分(百分位,500=5.00)', " +
- "behavior_modifier JSON COMMENT '行为修正因子JSON', " +
- "created_at DATETIME DEFAULT CURRENT_TIMESTAMP, " +
- "updated_at DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, " +
- "UNIQUE KEY uk_member (member_id, member_type)" +
- ") ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='家庭成员先天属性表'");
- log.info("已重建 family_member_attributes 表(v1.2 心智模型)");
- } else {
- log.info("family_member_attributes 表已为 v1.2 结构,跳过");
- }
- } catch (Exception e) {
- log.warn("重建 family_member_attributes 表失败: {}", e.getMessage());
- }
- try {
- Integer colExists = jdbcTemplate.queryForObject(
- "SELECT COUNT(*) FROM information_schema.COLUMNS WHERE TABLE_SCHEMA = DATABASE() AND TABLE_NAME = 'users' AND COLUMN_NAME = 'show_to_family'",
- Integer.class);
- if (colExists == null || colExists == 0) {
- jdbcTemplate.execute("ALTER TABLE users ADD COLUMN show_to_family TINYINT DEFAULT 1 COMMENT '是否对家庭成员可见: 1=是, 0=否'");
- log.info("已为 users 表添加 show_to_family 列");
- } else {
- log.info("users 表 show_to_family 列已存在,跳过");
- }
- } catch (Exception e) {
- log.warn("检查/添加 users 表 show_to_family 列失败: {}", e.getMessage());
- }
- // 迁移: tasks 表添加 family_member_id 列(FamilyMember 迁移)
- try {
- Integer colExists = jdbcTemplate.queryForObject(
- "SELECT COUNT(*) FROM information_schema.COLUMNS WHERE TABLE_SCHEMA = DATABASE() AND TABLE_NAME = 'tasks' AND COLUMN_NAME = 'family_member_id'",
- Integer.class);
- if (colExists == null || colExists == 0) {
- jdbcTemplate.execute("ALTER TABLE tasks ADD COLUMN family_member_id BIGINT COMMENT '家庭成员ID(关联 family_members.id)'");
- log.info("已为 tasks 表添加 family_member_id 列");
- } else {
- log.info("tasks 表 family_member_id 列已存在,跳过");
- }
- } catch (Exception e) {
- log.warn("检查/添加 tasks 表 family_member_id 列失败: {}", e.getMessage());
- }
- // 迁移: family_members 表添加 role_override 列(FamilyMember 迁移)
- try {
- Integer colExists = jdbcTemplate.queryForObject(
- "SELECT COUNT(*) FROM information_schema.COLUMNS WHERE TABLE_SCHEMA = DATABASE() AND TABLE_NAME = 'family_members' AND COLUMN_NAME = 'role_override'",
- Integer.class);
- if (colExists == null || colExists == 0) {
- jdbcTemplate.execute("ALTER TABLE family_members ADD COLUMN role_override VARCHAR(20) DEFAULT 'auto' COMMENT '角色覆盖: auto/parent/child/elderly,空=auto'");
- log.info("已为 family_members 表添加 role_override 列");
- } else {
- log.info("family_members 表 role_override 列已存在,跳过");
- }
- } catch (Exception e) {
- log.warn("检查/添加 family_members 表 role_override 列失败: {}", e.getMessage());
- }
- // 迁移: family_members 表添加 id_card 列(孩子身份证号)
- try {
- Integer colExists = jdbcTemplate.queryForObject(
- "SELECT COUNT(*) FROM information_schema.COLUMNS WHERE TABLE_SCHEMA = DATABASE() AND TABLE_NAME = 'family_members' AND COLUMN_NAME = 'id_card'",
- Integer.class);
- if (colExists == null || colExists == 0) {
- jdbcTemplate.execute("ALTER TABLE family_members ADD COLUMN id_card VARCHAR(18) DEFAULT NULL COMMENT '身份证号(孩子)'");
- log.info("已为 family_members 表添加 id_card 列");
- } else {
- log.info("family_members 表 id_card 列已存在,跳过");
- }
- } catch (Exception e) {
- log.warn("检查/添加 family_members 表 id_card 列失败: {}", e.getMessage());
- }
- // 迁移: family_members 表添加孩子专属字段(原children表合并)
- String[][] childColumns = {
- {"penalty_enabled", "TINYINT DEFAULT 1 COMMENT '是否扣减积分(0否1是)'"},
- {"theme", "VARCHAR(32) DEFAULT 'default' COMMENT '主题'"},
- {"system_points", "INT DEFAULT 0 COMMENT '系统积分(干预/平台奖励,仅只读历史;CF值见cf_balance)'"},
- {"streak_days", "INT DEFAULT 0 COMMENT '连续打卡天数'"},
- {"last_task_date", "DATE COMMENT '上次任务日期'"},
- {"focus_max_daily", "TINYINT DEFAULT 3 COMMENT '每日专注上限'"},
- {"focus_remaining", "TINYINT DEFAULT 3 COMMENT '今日剩余专注次数'"},
- {"focus_reset_date", "DATE COMMENT '专注次数重置日期'"}
- };
- for (String[] col : childColumns) {
- try {
- Integer colExists = jdbcTemplate.queryForObject(
- "SELECT COUNT(*) FROM information_schema.COLUMNS WHERE TABLE_SCHEMA = DATABASE() AND TABLE_NAME = 'family_members' AND COLUMN_NAME = ?",
- Integer.class, col[0]);
- if (colExists == null || colExists == 0) {
- jdbcTemplate.execute("ALTER TABLE family_members ADD COLUMN " + col[0] + " " + col[1]);
- log.info("已为 family_members 表添加 {} 列", col[0]);
- }
- } catch (Exception e) {
- log.warn("检查/添加 family_members 表 {} 列失败: {}", col[0], e.getMessage());
- }
- }
- // 家庭成员表添加 role_override 列(替换旧 capability_role)
- ensureColumn("family_members", "role_override", "VARCHAR(20) DEFAULT 'auto' COMMENT '角色覆盖: auto/parent/child/elderly'");
- try {
- jdbcTemplate.execute("ALTER TABLE family_members DROP COLUMN capability_role");
- log.info("已删除 family_members.capability_role 旧列,已由 role_override 替代");
- } catch (Exception e) {
- // 列不存在或已删除,忽略错误
- }
- // activities 表添加报名/签到相关字段
- ensureColumn("activities", "require_registration", "TINYINT DEFAULT 1 COMMENT '是否需要报名: 1=需要, 0=直接参与'");
- ensureColumn("activities", "require_checkin_review", "TINYINT DEFAULT 1 COMMENT '打卡是否需要审核: 1=需要, 0=直接通过'");
- ensureColumn("activities", "energy_penalty_unregistered", "INT DEFAULT 5 COMMENT '未报名打卡的行能量扣减值'");
- // 2026-10-22 整改:报名审核开关 + 名额占用计数
- ensureColumn("activities", "require_review", "TINYINT DEFAULT 0 COMMENT '报名是否需要审核: 1=需要, 0=免审即报名通过'");
- ensureColumn("activities", "approved_count", "INT DEFAULT 0 COMMENT '已通过报名人数(名额占用统计)'");
- ensureColumn("activities", "signed_count", "INT DEFAULT 0 COMMENT '签到人数(checkin 时 +1,区别于 approved_count 的报名口径)'");
- // 修正 require_checkin_review 默认值并归一化存量(该字段此前未被读取,无业务依赖,避免存量活动突然进入待审核)
- // 同时按已通过报名回填 approved_count,使名额统计与存量数据一致
- try {
- jdbcTemplate.execute("ALTER TABLE activities MODIFY COLUMN require_checkin_review TINYINT DEFAULT 0 COMMENT '打卡是否需要审核: 1=需要, 0=免审直接通过'");
- jdbcTemplate.execute("UPDATE activities SET require_checkin_review = 0 WHERE require_checkin_review = 1");
- jdbcTemplate.execute("UPDATE activities a SET a.approved_count = (SELECT cnt FROM (SELECT COUNT(*) AS cnt FROM activity_registrations r WHERE r.activity_id = a.id AND r.status = 'approved') t)");
- log.info("已修正 require_checkin_review 默认值并回填 approved_count");
- } catch (Exception e) {
- log.warn("修正 activities 报名/签到审核字段失败: {}", e.getMessage());
- }
- // family_members 表添加关系评分字段
- ensureColumn("family_members", "trust_score", "DECIMAL(5,2) DEFAULT 0 COMMENT '信任度 0-100'");
- ensureColumn("family_members", "intimacy_score", "DECIMAL(5,2) DEFAULT 0 COMMENT '亲密度 0-100'");
- ensureColumn("family_members", "communication_score", "DECIMAL(5,2) DEFAULT 0 COMMENT '沟通质量 0-100'");
- ensureColumn("family_members", "last_interaction_at", "DATETIME COMMENT '最近互动时间'");
- // 创建购物车表
- try {
- jdbcTemplate.execute("CREATE TABLE IF NOT EXISTS cart (" +
- "id BIGINT AUTO_INCREMENT PRIMARY KEY, " +
- "user_id BIGINT NOT NULL COMMENT '用户ID', " +
- "product_id BIGINT NOT NULL COMMENT '商品ID', " +
- "quantity INT NOT NULL DEFAULT 1 COMMENT '数量', " +
- "created_at DATETIME DEFAULT CURRENT_TIMESTAMP, " +
- "updated_at DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, " +
- "INDEX idx_user_product (user_id, product_id)" +
- ") ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='购物车'");
- log.info("已创建 cart 表");
- } catch (Exception e) {
- log.warn("创建 cart 表失败: {}", e.getMessage());
- }
- // ==================== 迁移: activities表添加visible_scope列 =============
- try {
- Integer colExists = jdbcTemplate.queryForObject(
- "SELECT COUNT(*) FROM information_schema.COLUMNS WHERE TABLE_SCHEMA = DATABASE() AND TABLE_NAME = 'activities' AND COLUMN_NAME = 'visible_scope'",
- Integer.class);
- if (colExists == null || colExists == 0) {
- jdbcTemplate.execute("ALTER TABLE activities ADD COLUMN visible_scope VARCHAR(20) DEFAULT NULL COMMENT '可见范围: family(家庭)/role(角色)/user(用户)' AFTER visibility");
- log.info("已添加visible_scope列到activities表");
- } else {
- log.info("activities表visible_scope列已存在,跳过");
- }
- } catch (Exception e) {
- log.warn("检查/添加activities表visible_scope列失败: {}", e.getMessage());
- }
- // ==================== 迁移: articles表添加article_type列 =============
- try {
- Integer colExists = jdbcTemplate.queryForObject(
- "SELECT COUNT(*) FROM information_schema.COLUMNS WHERE TABLE_SCHEMA = DATABASE() AND TABLE_NAME = 'articles' AND COLUMN_NAME = 'article_type'",
- Integer.class);
- if (colExists == null || colExists == 0) {
- jdbcTemplate.execute("ALTER TABLE articles ADD COLUMN article_type VARCHAR(20) DEFAULT 'normal' COMMENT '文章类型: normal(普通)/premium(优质)/original(原创)' AFTER read_time");
- log.info("已添加article_type列到articles表");
- } else {
- log.info("articles表article_type列已存在,跳过");
- }
- } catch (Exception e) {
- log.warn("检查/添加articles表article_type列失败: {}", e.getMessage());
- }
- // ==================== 迁移: articles表添加word_count列 =============
- try {
- Integer colExists = jdbcTemplate.queryForObject(
- "SELECT COUNT(*) FROM information_schema.COLUMNS WHERE TABLE_SCHEMA = DATABASE() AND TABLE_NAME = 'articles' AND COLUMN_NAME = 'word_count'",
- Integer.class);
- if (colExists == null || colExists == 0) {
- jdbcTemplate.execute("ALTER TABLE articles ADD COLUMN word_count INT DEFAULT 0 COMMENT '文章字数' AFTER article_type");
- log.info("已添加word_count列到articles表");
- } else {
- log.info("articles表word_count列已存在,跳过");
- }
- } catch (Exception e) {
- log.warn("检查/添加articles表word_count列失败: {}", e.getMessage());
- }
- // ==================== 迁移: article_reading_records表添加task_id列 =============
- try {
- Integer colExists = jdbcTemplate.queryForObject(
- "SELECT COUNT(*) FROM information_schema.COLUMNS WHERE TABLE_SCHEMA = DATABASE() AND TABLE_NAME = 'article_reading_records' AND COLUMN_NAME = 'task_id'",
- Integer.class);
- if (colExists == null || colExists == 0) {
- jdbcTemplate.execute("ALTER TABLE article_reading_records ADD COLUMN task_id BIGINT DEFAULT NULL COMMENT '关联的任务ID' AFTER duration_seconds");
- log.info("已添加task_id列到article_reading_records表");
- } else {
- log.info("article_reading_records表task_id列已存在,跳过");
- }
- } catch (Exception e) {
- log.warn("检查/添加article_reading_records表task_id列失败: {}", e.getMessage());
- }
- // ==================== 迁移: tasks表添加source_type列 =============
- try {
- Integer colExists = jdbcTemplate.queryForObject(
- "SELECT COUNT(*) FROM information_schema.COLUMNS WHERE TABLE_SCHEMA = DATABASE() AND TABLE_NAME = 'tasks' AND COLUMN_NAME = 'source_type'",
- Integer.class);
- if (colExists == null || colExists == 0) {
- jdbcTemplate.execute("ALTER TABLE tasks ADD COLUMN source_type VARCHAR(20) DEFAULT NULL COMMENT '来源类型: article/activity/manual' AFTER minigame_code");
- log.info("已添加source_type列到tasks表");
- } else {
- log.info("tasks表source_type列已存在,跳过");
- }
- } catch (Exception e) {
- log.warn("检查/添加tasks表source_type列失败: {}", e.getMessage());
- }
- // ==================== 迁移: tasks表添加source_id列 =============
- try {
- Integer colExists = jdbcTemplate.queryForObject(
- "SELECT COUNT(*) FROM information_schema.COLUMNS WHERE TABLE_SCHEMA = DATABASE() AND TABLE_NAME = 'tasks' AND COLUMN_NAME = 'source_id'",
- Integer.class);
- if (colExists == null || colExists == 0) {
- jdbcTemplate.execute("ALTER TABLE tasks ADD COLUMN source_id BIGINT DEFAULT NULL COMMENT '来源ID' AFTER source_type");
- log.info("已添加source_id列到tasks表");
- } else {
- log.info("tasks表source_id列已存在,跳过");
- }
- } catch (Exception e) {
- log.warn("检查/添加tasks表source_id列失败: {}", e.getMessage());
- }
- // ==================== 迁移: activities表添加checkin_points列 =============
- try {
- Integer colExists = jdbcTemplate.queryForObject(
- "SELECT COUNT(*) FROM information_schema.COLUMNS WHERE TABLE_SCHEMA = DATABASE() AND TABLE_NAME = 'activities' AND COLUMN_NAME = 'checkin_points'",
- Integer.class);
- if (colExists == null || colExists == 0) {
- jdbcTemplate.execute("ALTER TABLE activities ADD COLUMN checkin_points INT DEFAULT 0 COMMENT '签到积分(0=使用系统默认)' AFTER price");
- log.info("已添加checkin_points列到activities表");
- } else {
- log.info("activities表checkin_points列已存在,跳过");
- }
- } catch (Exception e) {
- log.warn("检查/添加activities表checkin_points列失败: {}", e.getMessage());
- }
- // 迁移: 创建family_invitations表(家庭邀请令牌)
- try {
- jdbcTemplate.execute("CREATE TABLE IF NOT EXISTS family_invitations (" +
- "id BIGINT AUTO_INCREMENT PRIMARY KEY, " +
- "family_id BIGINT NOT NULL COMMENT '家庭ID', " +
- "token VARCHAR(64) NOT NULL UNIQUE COMMENT '邀请令牌(UUID)', " +
- "created_by BIGINT NOT NULL COMMENT '创建人用户ID', " +
- "status VARCHAR(16) DEFAULT 'active' COMMENT '状态: active/used/expired/revoked', " +
- "expires_at DATETIME COMMENT '过期时间', " +
- "created_at DATETIME DEFAULT CURRENT_TIMESTAMP, " +
- "updated_at DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, " +
- "INDEX idx_family_id (family_id), " +
- "INDEX idx_token (token), " +
- "INDEX idx_status (status)" +
- ") ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='家庭邀请令牌'");
- log.info("已创建family_invitations表");
- } catch (Exception e) {
- log.warn("创建family_invitations表失败: {}", e.getMessage());
- }
- // ==================== P0 营养师: nutrition_indicator_mapping =============
- try {
- jdbcTemplate.execute("CREATE TABLE IF NOT EXISTS nutrition_indicator_mapping (" +
- "id BIGINT AUTO_INCREMENT PRIMARY KEY," +
- "indicator_name_pattern VARCHAR(200) NOT NULL COMMENT '指标名匹配模式(SQL LIKE语法,%为通配符)'," +
- "nutrient VARCHAR(50) NOT NULL COMMENT '关联营养素名'," +
- "severity_weight DECIMAL(3,2) DEFAULT 1.00 COMMENT '权重(0-1)'," +
- "deficiency_symptoms TEXT COMMENT '缺乏症状描述'," +
- "excess_symptoms TEXT COMMENT '过量症状描述'," +
- "food_suggestions TEXT COMMENT '推荐食物(逗号分隔)'," +
- "sort_order INT DEFAULT 0 COMMENT '排序(大值优先匹配)'," +
- "created_at DATETIME DEFAULT CURRENT_TIMESTAMP," +
- "updated_at DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP," +
- "INDEX idx_nutrient (nutrient)" +
- ") ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='指标-营养素映射表'");
- log.info("已创建 nutrition_indicator_mapping 表");
- } catch (Exception e) {
- log.warn("创建 nutrition_indicator_mapping 表失败: {}", e.getMessage());
- }
- // P0 营养师: nutrition_deficiency_record
- try {
- jdbcTemplate.execute("CREATE TABLE IF NOT EXISTS nutrition_deficiency_record (" +
- "id BIGINT AUTO_INCREMENT PRIMARY KEY," +
- "child_id BIGINT NOT NULL COMMENT '孩子ID'," +
- "report_id BIGINT NOT NULL COMMENT '关联报告ID'," +
- "nutrient VARCHAR(50) NOT NULL COMMENT '营养素名'," +
- "deficiency_type VARCHAR(20) NOT NULL COMMENT 'deficiency/excess/borderline/unclassified'," +
- "severity DECIMAL(5,2) DEFAULT 0 COMMENT '严重程度(0-1)'," +
- "indicator_name VARCHAR(100) COMMENT '关联指标名'," +
- "indicator_value VARCHAR(50) COMMENT '指标检测值'," +
- "ref_range VARCHAR(100) COMMENT '参考范围'," +
- "symptoms TEXT COMMENT '症状描述'," +
- "food_suggestions TEXT COMMENT '推荐食物'," +
- "created_at DATETIME DEFAULT CURRENT_TIMESTAMP," +
- "INDEX idx_child (child_id)," +
- "INDEX idx_report (report_id)," +
- "INDEX idx_nutrient (nutrient)" +
- ") ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='营养缺乏分析记录'");
- log.info("已创建 nutrition_deficiency_record 表");
- } catch (Exception e) {
- log.warn("创建 nutrition_deficiency_record 表失败: {}", e.getMessage());
- }
- // P0 营养师: 指标-营养素映射 种子数据
- try {
- Integer count = jdbcTemplate.queryForObject(
- "SELECT COUNT(*) FROM nutrition_indicator_mapping", Integer.class);
- if (count == null || count == 0) {
- String[][] seedData = {
- {"铁蛋白", "铁", "1.00", "乏力、面色苍白、注意力不集中", "铁过量可能损伤肝脏", "动物肝脏,红肉,菠菜,黑木耳", "10"},
- {"25-羟基维生素D", "维生素D", "1.00", "影响钙吸收、骨骼发育", "维生素D过量可致高钙血症", "三文鱼,蛋黄,蘑菇,强化牛奶", "20"},
- {"维生素D", "维生素D", "1.00", "影响钙吸收、骨骼发育、免疫力下降", "维生素D过量可致高钙血症", "三文鱼,蛋黄,蘑菇,强化牛奶", "15"},
- {"锌", "锌", "0.80", "食欲减退、味觉异常、免疫力降低", "锌过量影响铜吸收", "牡蛎,牛肉,南瓜子,扁豆", "10"},
- {"钙", "钙", "0.80", "骨骼发育迟缓、牙齿问题", "高钙血症、肾结石", "牛奶,豆腐,虾皮,芝麻酱", "10"},
- {"维生素B12", "维生素B12", "1.00", "贫血、神经发育迟缓", "一般不易过量", "牛肉,鸡蛋,牛奶,鱼肉", "10"},
- {"维生素C", "维生素C", "0.60", "免疫力下降、牙龈出血", "腹泻、肾结石风险", "猕猴桃,橙子,草莓,青椒", "10"},
- {"白蛋白", "蛋白质", "0.90", "发育迟缓、肌肉萎缩", "肾脏负担", "鸡蛋,鱼肉,牛奶,豆制品", "10"},
- {"总蛋白", "蛋白质", "0.90", "营养不良、免疫力下降", "血液粘稠", "鸡蛋,鱼肉,牛奶,豆制品", "9"},
- {"膳食纤维", "膳食纤维", "0.50", "便秘、肠道菌群失衡", "腹胀、排便过多", "全谷物,蔬菜,豆类,水果", "10"},
- {"叶酸", "叶酸", "0.70", "贫血、发育迟缓", "一般不易过量", "菠菜,动物肝脏,豆类,柑橘", "10"},
- {"维生素A", "维生素A", "0.70", "视力问题、皮肤干燥、免疫力下降", "头痛、肝脏损伤", "胡萝卜,动物肝脏,南瓜,菠菜", "10"},
- {"硒", "硒", "0.50", "免疫力下降、甲状腺功能异常", "脱发、指甲脆裂", "巴西坚果,海鲜,蘑菇", "10"},
- {"镁", "镁", "0.40", "肌肉痉挛、睡眠问题", "腹泻、低血压", "坚果,全谷物,深绿蔬菜", "10"},
- {"益生菌%", "益生菌", "0.60", "肠道菌群失衡、消化问题", "一般不易过量", "酸奶,泡菜,味噌,康普茶", "10"}
- };
- for (String[] row : seedData) {
- jdbcTemplate.update("INSERT IGNORE INTO nutrition_indicator_mapping " +
- "(indicator_name_pattern, nutrient, severity_weight, deficiency_symptoms, excess_symptoms, food_suggestions, sort_order) " +
- "VALUES (?, ?, ?, ?, ?, ?, ?)",
- row[0], row[1], new java.math.BigDecimal(row[2]), row[3], row[4], row[5], Integer.parseInt(row[6]));
- }
- log.info("nutrition_indicator_mapping 种子数据已加载 (15条)");
- } else {
- log.info("nutrition_indicator_mapping 种子数据已存在 ({}条), 跳过", count);
- }
- } catch (Exception e) {
- log.warn("nutrition_indicator_mapping 种子数据初始化失败: {}", e.getMessage());
- }
- // ==================== 心版块 Phase 1: emotion_checkin 表 =============
- try {
- jdbcTemplate.execute("CREATE TABLE IF NOT EXISTS emotion_checkin (" +
- "id BIGINT AUTO_INCREMENT PRIMARY KEY, " +
- "child_id BIGINT NOT NULL COMMENT '孩子ID', " +
- "user_id BIGINT NOT NULL COMMENT '打卡人用户ID', " +
- "emotion_type INT NOT NULL COMMENT '情绪类型: 1快乐/2平和平静/3平静/4有点不舒服/5难受/6烦躁/7生气/8焦虑/9恐惧', " +
- "emotion_name VARCHAR(20) COMMENT '情绪名称: happy/calm/peaceful/uncomfortable/sad/irritable/angry/anxious/fearful', " +
- "energy_level INT NOT NULL DEFAULT 3 COMMENT '能量状态: 1很低/2低/3中等/4高/5很高', " +
- "note TEXT COMMENT '打卡备注', " +
- "photo_url VARCHAR(500) COMMENT '照片URL', " +
- "favorite_thing VARCHAR(500) COMMENT '今日最喜欢的一件事', " +
- "points_awarded INT DEFAULT 0 COMMENT '奖励积分', " +
- "parent_visible TINYINT DEFAULT 1 COMMENT '家长可见: 1可见/0不可见', " +
- "checkin_date DATE NOT NULL COMMENT '打卡日期', " +
- "created_at DATETIME DEFAULT CURRENT_TIMESTAMP, " +
- "updated_at DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, " +
- "INDEX idx_child_id (child_id), " +
- "INDEX idx_user_id (user_id), " +
- "INDEX idx_checkin_date (checkin_date), " +
- "INDEX idx_child_date (child_id, checkin_date)" +
- ") ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='情绪打卡表'");
- log.info("已创建 emotion_checkin 情绪打卡表");
- } catch (Exception e) {
- log.warn("创建 emotion_checkin 表失败: {}", e.getMessage());
- }
- // ==================== 1a-2: DAN Schema 修复 ============= // v1.2 EMI 心理子维度
- String[] danEmiFields = {"emotion_score", "resilience_score", "stress_coping_score", "self_awareness_score"};
- for (String field : danEmiFields) {
- try {
- Integer exists = jdbcTemplate.queryForObject(
- "SELECT COUNT(*) FROM information_schema.COLUMNS WHERE TABLE_SCHEMA = DATABASE() AND TABLE_NAME = 'dan_assessment_results' AND COLUMN_NAME = ?",
- Integer.class, field);
- if (exists == null || exists == 0) {
- jdbcTemplate.execute("ALTER TABLE dan_assessment_results ADD COLUMN " + field + " INT DEFAULT NULL COMMENT 'EMI子维度 0-100'");
- log.info("已添加 {} 列到 dan_assessment_results 表", field);
- }
- } catch (Exception e) {
- log.warn("检查/添加 {} 列失败: {}", field, e.getMessage());
- }
- }
- // 大五人格维度
- String[] danBigFiveFields = {"openness_score", "conscientiousness_score", "extraversion_score", "agreeableness_score", "neuroticism_score", "big_five_overall_score"};
- for (String field : danBigFiveFields) {
- try {
- Integer exists = jdbcTemplate.queryForObject(
- "SELECT COUNT(*) FROM information_schema.COLUMNS WHERE TABLE_SCHEMA = DATABASE() AND TABLE_NAME = 'dan_assessment_results' AND COLUMN_NAME = ?",
- Integer.class, field);
- if (exists == null || exists == 0) {
- jdbcTemplate.execute("ALTER TABLE dan_assessment_results ADD COLUMN " + field + " INT DEFAULT NULL COMMENT '大五人格 0-100'");
- log.info("已添加 {} 列到 dan_assessment_results 表", field);
- }
- } catch (Exception e) {
- log.warn("检查/添加 {} 列失败: {}", field, e.getMessage());
- }
- }
- // EMI 维度(情绪商数评估字段)
- String[] danEmiDimFields = {"emotion_management_score", "empathy_score", "social_adaptability_score", "self_motivation_score"};
- for (String field : danEmiDimFields) {
- try {
- Integer exists = jdbcTemplate.queryForObject(
- "SELECT COUNT(*) FROM information_schema.COLUMNS WHERE TABLE_SCHEMA = DATABASE() AND TABLE_NAME = 'dan_assessment_results' AND COLUMN_NAME = ?",
- Integer.class, field);
- if (exists == null || exists == 0) {
- jdbcTemplate.execute("ALTER TABLE dan_assessment_results ADD COLUMN " + field + " INT DEFAULT NULL COMMENT 'EMI维度 0-100'");
- log.info("已添加 {} 列到 dan_assessment_results 表", field);
- }
- } catch (Exception e) {
- log.warn("检查/添加 {} 列失败: {}", field, e.getMessage());
- }
- }
- // ==================== 1b-1: bazi_reading_template 五行命理解读表 =============
- try {
- jdbcTemplate.execute("CREATE TABLE IF NOT EXISTS bazi_reading_template (" +
- "id BIGINT AUTO_INCREMENT PRIMARY KEY, " +
- "element VARCHAR(20) NOT NULL COMMENT '五行: wood/fire/earth/metal/water', " +
- "`condition` VARCHAR(20) NOT NULL COMMENT '状态: strong/weak/balanced', " +
- "personal_trait TEXT COMMENT '性格特质描述', " +
- "mind_growth_advice TEXT COMMENT '心理成长建议', " +
- "daily_tip_template TEXT COMMENT '每日宜忌模板', " +
- "created_at DATETIME DEFAULT CURRENT_TIMESTAMP, " +
- "UNIQUE KEY uk_element_condition (element, `condition`)" +
- ") ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='五行命理解读模板'");
- log.info("已创建 bazi_reading_template 表");
- } catch (Exception e) {
- log.warn("创建 bazi_reading_template 表失败: {}", e.getMessage());
- }
- // ==================== 1b-1: numsoul_config 生命灵数配置表 =============
- try {
- jdbcTemplate.execute("CREATE TABLE IF NOT EXISTS numsoul_config (" +
- "number INT PRIMARY KEY COMMENT '灵数1-9', " +
- "title VARCHAR(50) NOT NULL COMMENT '称号', " +
- "keywords VARCHAR(200) COMMENT '性格关键词', " +
- "mind_growth_advice TEXT COMMENT '心理成长建议', " +
- "recommended_article_tags VARCHAR(200) COMMENT '关联文章标签', " +
- "color_hex VARCHAR(7) COMMENT '代表色' " +
- ") ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='生命灵数配置'");
- log.info("已创建 numsoul_config 表");
- } catch (Exception e) {
- log.warn("创建 numsoul_config 表失败: {}", e.getMessage());
- }
- // ==================== 1b-1: bazi_reading_template 种子数据 =============
- try {
- Integer count = jdbcTemplate.queryForObject(
- "SELECT COUNT(*) FROM bazi_reading_template", Integer.class);
- if (count == null || count == 0) {
- jdbcTemplate.batchUpdate(
- "INSERT INTO bazi_reading_template (element, `condition`, personal_trait, mind_growth_advice, daily_tip_template) VALUES (?, ?, ?, ?, ?)",
- java.util.Arrays.asList(new Object[][]{
- {"wood", "strong", "性格坚韧、有进取心,具有领导力和开拓精神", "学会适当放松,避免过刚易折,培养弹性思维", "宜进行伸展运动,忌高强度冲突"},
- {"wood", "weak", "温和有礼、善于合作,但决策时容易犹豫", "增强自信心训练,培养果断决策能力", "宜与自然相处,忌过度迁就他人"},
- {"wood", "balanced", "性格中庸有度,既有韧性又有灵活性", "保持现状,适当拓展社交圈", "宜户外活动,忌久坐不动"},
- {"fire", "strong", "热情开朗、充满活力,富有感染力和创造力", "注意情绪管理,避免急躁冲动", "宜冷水刺激冷静,忌暴怒争执"},
- {"fire", "weak", "内敛含蓄、心思细腻,对艺术有敏锐感知", "适当表达情感,培养社交自信", "宜温暖陪伴,忌独处过久"},
- {"fire", "balanced", "性格温润有度,既有热情又懂克制", "保持兴趣爱好,适度拓展表达", "宜参与社交活动,忌情绪压抑"},
- {"earth", "strong", "稳重可靠、有责任心,是团队中的中流砥柱", "避免过度承担,学会说'不'", "宜适量运动,忌思虑过重"},
- {"earth", "weak", "随和友善、适应性强,容易与他人相处", "增强主见性,培养独立思考能力", "宜接触新事物,忌固守不变"},
- {"earth", "balanced", "性情敦厚、处事稳妥,人际关系和谐", "保持学习,适当挑战自我", "宜亲近自然,忌暴饮暴食"},
- {"metal", "strong", "意志坚定、追求完美,具有卓越的执行力", "学会灵活变通,避免过于严苛", "宜艺术鉴赏,忌过度批评"},
- {"metal", "weak", "温和寡言、善于倾听,是值得信赖的朋友", "增强表达勇气,培养自信心", "宜团体活动,忌过度自我封闭"},
- {"metal", "balanced", "为人正直、做事有度,不乏柔韧与包容", "保持开放心态,适度多元发展", "宜阅读学习,忌钻牛角尖"},
- {"water", "strong", "智慧深沉、善于谋略,具有出色洞察力", "避免过度缜密,适当放松心情", "宜静坐冥想,忌思虑过深"},
- {"water", "weak", "敏感细腻、有同理心,富有创作天赋", "增强抵抗力,培养情绪韧性", "宜温暖环境,忌受寒受凉"},
- {"water", "balanced", "性格圆融、随机应变,处事灵活得宜", "保持好奇心,适度扩展影响力", "宜游泳泡澡,忌情绪波动"},
- })
- );
- log.info("bazi_reading_template 种子数据已加载 (15条)");
- }
- } catch (Exception e) {
- log.warn("bazi_reading_template 种子数据初始化失败: {}", e.getMessage());
- }
- // ==================== 1b-1: numsoul_config 种子数据 =============
- try {
- Integer count = jdbcTemplate.queryForObject(
- "SELECT COUNT(*) FROM numsoul_config", Integer.class);
- if (count == null || count == 0) {
- jdbcTemplate.batchUpdate(
- "INSERT INTO numsoul_config (number, title, keywords, mind_growth_advice, recommended_article_tags, color_hex) VALUES (?, ?, ?, ?, ?, ?)",
- java.util.Arrays.asList(new Object[][]{
- {1, "开创者", "独立、领导、创新、自信", "培养团队协作意识,避免独断专行", "领导力,自信培养", "#FF4444"},
- {2, "和平者", "温和、合作、敏感、耐心", "增强自我主张,避免过度迎合他人", "人际交往,情绪表达", "#FF8C00"},
- {3, "表达者", "创意、社交、乐观、表达", "保持专注,避免浅尝辄止", "创造力,社交能力", "#FFD700"},
- {4, "建设者", "务实、稳定、有序、勤奋", "学会变通,避免过于固执", "专注力,学习习惯", "#4CAF50"},
- {5, "自由者", "冒险、变化、好奇、灵活", "培养持久力,避免缺乏恒心", "探索精神,适应能力", "#2196F3"},
- {6, "关怀者", "责任、关爱、奉献、平衡", "学会自我关爱,避免过度付出", "家庭关系,责任心", "#9C27B0"},
- {7, "探索者", "智慧、分析、内省、完美", "与人分享见解,避免过度封闭", "思维能力,学习力", "#673AB7"},
- {8, "成就者", "雄心、管理、决策、权威", "关注内在成长,避免过于追求外在成就", "目标管理,理财思维", "#3F51B5"},
- {9, "博爱者", "包容、智慧、善良、豁达", "设定个人边界,避免过度牺牲", "人际关系,情绪管理", "#E91E63"},
- })
- );
- log.info("numsoul_config 种子数据已加载 (9条)");
- }
- } catch (Exception e) {
- log.warn("numsoul_config 种子数据初始化失败: {}", e.getMessage());
- }
- try {
- jdbcTemplate.execute("CREATE TABLE IF NOT EXISTS user_address (" +
- "id BIGINT AUTO_INCREMENT PRIMARY KEY, " +
- "user_id BIGINT NOT NULL COMMENT '用户ID', " +
- "receiver_name VARCHAR(50) NOT NULL COMMENT '收货人姓名', " +
- "phone VARCHAR(20) NOT NULL COMMENT '收货人电话', " +
- "province VARCHAR(50) COMMENT '省份', " +
- "city VARCHAR(50) COMMENT '城市', " +
- "district VARCHAR(50) COMMENT '区县', " +
- "street VARCHAR(200) COMMENT '详细地址', " +
- "is_default INT DEFAULT 0 COMMENT '是否默认 1=是 0=否', " +
- "created_at DATETIME DEFAULT CURRENT_TIMESTAMP, " +
- "updated_at DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, " +
- "INDEX idx_user_id (user_id)" +
- ") ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='收货地址表'");
- log.info("已创建 user_address 表");
- } catch (Exception e) {
- log.warn("创建 user_address 表失败: {}", e.getMessage());
- }
- // ==================== 会员价格体系 ====== // 迁移: activities表添加member_price列
- try {
- Integer exists = jdbcTemplate.queryForObject(
- "SELECT COUNT(*) FROM information_schema.COLUMNS WHERE TABLE_SCHEMA = DATABASE() AND TABLE_NAME = 'activities' AND COLUMN_NAME = 'member_price'",
- Integer.class);
- if (exists == null || exists == 0) {
- jdbcTemplate.execute("ALTER TABLE activities ADD COLUMN member_price INT COMMENT '会员价(分)' AFTER price");
- log.info("已添加member_price列到activities表");
- } else {
- log.info("member_price列已存在,跳过");
- }
- } catch (Exception e) {
- log.warn("检查/添加activities.member_price失败: {}", e.getMessage());
- }
- // 迁移: 创建 member_discount_configs 表(分类级别折扣配置)
- try {
- jdbcTemplate.execute("CREATE TABLE IF NOT EXISTS member_discount_configs (" +
- "id BIGINT AUTO_INCREMENT PRIMARY KEY, " +
- "level_code VARCHAR(20) NOT NULL COMMENT '会员等级: FAMILY/PROVIDER', " +
- "target_type VARCHAR(20) NOT NULL COMMENT '目标类型: product/activity', " +
- "target_id BIGINT DEFAULT NULL COMMENT '目标ID(NULL表示分类级别)', " +
- "category_id BIGINT DEFAULT NULL COMMENT '类目ID(分类级别折扣)', " +
- "discount_percent INT NOT NULL COMMENT '折扣百分比(如90表示90%)', " +
- "enabled TINYINT DEFAULT 1 COMMENT '是否启用', " +
- "created_at DATETIME DEFAULT CURRENT_TIMESTAMP, " +
- "updated_at DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, " +
- "INDEX idx_level (level_code), " +
- "INDEX idx_target (target_type, target_id), " +
- "INDEX idx_category (target_type, category_id)" +
- ") ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='会员折扣配置'");
- log.info("已创建member_discount_configs表");
- } catch (Exception e) {
- log.warn("创建member_discount_configs表失败: {}", e.getMessage());
- }
- // ==================== 食材推荐指数 Phase: food_recommend_idx / food_recommend_idx_log =============
- try {
- jdbcTemplate.execute("CREATE TABLE IF NOT EXISTS `food_recommend_idx` (" +
- "`id` BIGINT NOT NULL AUTO_INCREMENT, " +
- "`user_id` BIGINT NOT NULL COMMENT '用户ID', " +
- "`food_id` BIGINT NOT NULL COMMENT '食材ID', " +
- "`food_name` VARCHAR(100) COMMENT '食材名称', " +
- "`index_score` INT NOT NULL DEFAULT 0 COMMENT '推荐指数(0-100, 来自菌群检测报告)', " +
- "`health_report_id` BIGINT COMMENT '关联健康报告ID', " +
- "`calculated_at` DATETIME COMMENT '计算时间', " +
- "`created_at` DATETIME DEFAULT CURRENT_TIMESTAMP, " +
- "PRIMARY KEY (`id`), " +
- "INDEX `idx_fri_user` (`user_id`), " +
- "INDEX `idx_fri_food` (`food_id`), " +
- "UNIQUE KEY `uk_fri_user_food` (`user_id`, `food_id`) " +
- ") ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='食材推荐指数当前值'");
- log.info("已创建/确认 food_recommend_idx 表");
- } catch (Exception e) {
- log.warn("创建 food_recommend_idx 表失败: {}", e.getMessage());
- }
- try {
- jdbcTemplate.execute("CREATE TABLE IF NOT EXISTS `food_recommend_idx_log` (" +
- "`id` BIGINT NOT NULL AUTO_INCREMENT, " +
- "`user_id` BIGINT NOT NULL COMMENT '用户ID', " +
- "`food_id` BIGINT NOT NULL COMMENT '食材ID', " +
- "`food_name` VARCHAR(100) COMMENT '食材名称', " +
- "`previous_index` INT COMMENT '变更前推荐指数', " +
- "`new_index` INT COMMENT '变更后推荐指数', " +
- "`health_report_id` BIGINT COMMENT '触发的健康报告ID', " +
- "`change_reason` VARCHAR(200) COMMENT '变更原因', " +
- "`changed_at` DATETIME DEFAULT CURRENT_TIMESTAMP, " +
- "PRIMARY KEY (`id`), " +
- "INDEX `idx_fril_user` (`user_id`), " +
- "INDEX `idx_fril_food` (`food_id`) " +
- ") ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='食材推荐指数变更历史日志'");
- log.info("已创建/确认 food_recommend_idx_log 表");
- } catch (Exception e) {
- log.warn("创建 food_recommend_idx_log 表失败: {}", e.getMessage());
- }
- // 迁移25: activities表添加审核相关字段(活动工作流:待审核/驳回/发布)
- try {
- jdbcTemplate.execute("ALTER TABLE activities ADD COLUMN audit_status VARCHAR(20) DEFAULT NULL COMMENT '审核状态:approved/pending/rejected'");
- log.info("已添加audit_status列到activities表");
- } catch (Exception e) {
- // 列已存在,忽略错误
- }
- try {
- jdbcTemplate.execute("ALTER TABLE activities ADD COLUMN audit_reason VARCHAR(500) DEFAULT NULL COMMENT '驳回原因'");
- log.info("已添加audit_reason列到activities表");
- } catch (Exception e) {
- // 列已存在,忽略错误
- }
- try {
- jdbcTemplate.execute("ALTER TABLE activities ADD COLUMN auditor_id BIGINT DEFAULT NULL COMMENT '审核人ID'");
- log.info("已添加auditor_id列到activities表");
- } catch (Exception e) {
- // 列已存在,忽略错误
- }
- try {
- jdbcTemplate.execute("ALTER TABLE activities ADD COLUMN audited_at DATETIME DEFAULT NULL COMMENT '审核时间'");
- log.info("已添加audited_at列到activities表");
- } catch (Exception e) {
- }
- // 家庭挑战:目标模式与目标值(aggregate=集体总合目标 / all_members=全员个人目标)
- ensureColumn("family_challenge", "target_mode", "VARCHAR(20) DEFAULT 'aggregate' COMMENT '目标模式: aggregate=集体总合/all_members=全员个人目标'");
- ensureColumn("family_challenge", "target_value", "INT DEFAULT 0 COMMENT '目标值(aggregate=家庭合计值, all_members=每人目标值)'");
- // 迁移: family_challenge 表添加回复时效字段(待同意成员超时未回复自动拒绝)
- ensureColumn("family_challenge", "respond_deadline", "DATETIME NULL COMMENT '回复时效截止时间(超过后未回复的 pending 成员自动置为 rejected)'");
- // 迁移248: 创建 health_report_edit_logs 表(健康报告修改留痕,原迁移222误置于initializeDefaultData中仅测试环境执行)
- try {
- jdbcTemplate.execute("CREATE TABLE IF NOT EXISTS health_report_edit_logs (" +
- "id BIGINT AUTO_INCREMENT PRIMARY KEY, " +
- "report_id BIGINT NOT NULL, " +
- "user_id BIGINT, " +
- "report_type VARCHAR(32), " +
- "action VARCHAR(20) COMMENT 'create/edit', " +
- "before_payload LONGTEXT, " +
- "after_payload LONGTEXT, " +
- "created_at DATETIME DEFAULT CURRENT_TIMESTAMP, " +
- "INDEX idx_report_id (report_id)" +
- ") ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='健康报告修改留痕'");
- log.info("已创建health_report_edit_logs表");
- } catch (Exception e) {
- // 表已存在,忽略错误
- }
- // 迁移249: file_record 表添加 file_type 和 description 列(原迁移221/228误置于seedFullRegionData中,区域数据非空时永不执行)
- // 迁移250: health_plans 添加 plan_json 和 status 列(支持结构化方案编辑)
- try {
- ensureColumn("health_plans", "plan_json", "JSON COMMENT '结构化方案内容(总览+营养+饮食+运动)'");
- } catch (Exception e) {
- log.warn("health_plans.plan_json 添加失败(可能已存在): " + e.getMessage());
- }
- try {
- ensureColumn("health_plans", "status", "VARCHAR(20) DEFAULT 'draft' COMMENT '方案状态: draft(草稿)/pending_review(待审核)/published(已发布)/rejected(已驳回)'");
- } catch (Exception e) {
- log.warn("health_plans.status 添加失败(可能已存在): " + e.getMessage());
- }
- // 迁移250: health_plans 新增审核字段(规划师/营养师方案维护流程)
- try {
- ensureColumn("health_plans", "reviewed_by", "BIGINT DEFAULT NULL COMMENT '审核人ID(规划师/营养师)'");
- ensureColumn("health_plans", "reviewed_at", "DATETIME DEFAULT NULL COMMENT '审核时间'");
- ensureColumn("health_plans", "review_comment", "TEXT DEFAULT NULL COMMENT '审核意见'");
- } catch (Exception e) {
- log.warn("health_plans 审核字段添加失败(可能已存在): " + e.getMessage());
- }
- // 迁移251: health_plans 新增 teacher_id 列(用户指定规划师)
- try {
- ensureColumn("health_plans", "teacher_id", "BIGINT DEFAULT NULL COMMENT '指定规划师ID(用户选择)'");
- } catch (Exception ex) {
- log.warn("health_plans.teacher_id 添加失败(可能已存在): " + ex.getMessage());
- }
- // 迁移251b: health_plans 新增 updated_at 列(实体HealthPlan含该字段,缺失导致selectById报Unknown column)
- try {
- ensureColumn("health_plans", "updated_at", "DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间'");
- } catch (Exception ex) {
- log.warn("health_plans.updated_at 添加失败(可能已存在): " + ex.getMessage());
- }
- // 迁移251c: finance_checkins.child_id 允许NULL(家长本人打卡不选孩子,Service已支持memberId为空;原NOT NULL无默认值导致INSERT报错)
- try {
- jdbcTemplate.execute("ALTER TABLE finance_checkins MODIFY COLUMN child_id BIGINT NULL COMMENT '孩子ID,家长本人打卡时为NULL'");
- log.info("已修改finance_checkins.child_id字段允许NULL");
- } catch (Exception e) {
- log.warn("finance_checkins.child_id 修改失败: " + e.getMessage());
- }
- // 迁移251c: finance_checkins 补齐 type 列(initGuideModuleTables建表版本缺失,实体FinanceCheckin含type字段且收支统计依赖)
- try {
- ensureColumn("finance_checkins", "type", "VARCHAR(10) DEFAULT 'expense' COMMENT '类型: income(收入)/expense(支出)'");
- } catch (Exception e) {
- log.warn("finance_checkins.type 添加失败(可能已存在): " + e.getMessage());
- }
- // 迁移258: 创建energy_daily_limit表(每日能量上限配置持久化)
- try {
- jdbcTemplate.execute("CREATE TABLE IF NOT EXISTS energy_daily_limit (" +
- "id BIGINT AUTO_INCREMENT PRIMARY KEY, " +
- "dimension_code VARCHAR(20) NOT NULL COMMENT '维度代码: body/mind/wisdom/action/wealth', " +
- "daily_limit INT NOT NULL DEFAULT 50 COMMENT '每日能量上限', " +
- "created_at DATETIME DEFAULT CURRENT_TIMESTAMP, " +
- "updated_at DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, " +
- "UNIQUE KEY uk_dimension_code (dimension_code)" +
- ") ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='每日能量上限配置'");
- log.info("已创建energy_daily_limit表");
- // 插入默认数据(幂等)
- jdbcTemplate.execute("INSERT IGNORE INTO energy_daily_limit (dimension_code, daily_limit) VALUES " +
- "('body', 50), ('mind', 50), ('wisdom', 80), ('action', 60), ('wealth', 50)");
- log.info("已插入energy_daily_limit默认数据");
- } catch (Exception ex) {
- log.warn("energy_daily_limit 表创建失败(可能已存在): " + ex.getMessage());
- }
- // 迁移251d: diet_preferences 补齐 must_eat 列(实体DietPreferences含mustEat字段,建表时两处均遗漏,查询报Unknown column)
- try {
- ensureColumn("diet_preferences", "must_eat", "JSON COMMENT '必吃食物列表'");
- } catch (Exception e) {
- log.warn("diet_preferences.must_eat 添加失败(可能已存在): " + e.getMessage());
- }
- ensureColumn("file_record", "file_type", "VARCHAR(50) DEFAULT NULL COMMENT '文件类型: image/pdf/video/audio/other'");
- ensureColumn("file_record", "description", "TEXT COMMENT '文件描述'");
- log.info("数据库迁移完成");
- // 迁移252: 体素/心理/认知指标定义种子数据
- try {
- Object[][] rows = {
- {"body", "nutrition", "body_fat", "体脂率(%)", "18-28", "1"},
- {"body", "nutrition", "bmi", "BMI指数", "18.5-24", "2"},
- {"body", "nutrition", "protein", "蛋白质摄入(g/日)", "50-80", "3"},
- {"body", "nutrition", "calcium", "钙摄入(mg/日)", "800-1000", "4"},
- {"body", "nutrition", "vitamin_d", "维生素D(ng/ml)", "30-100", "5"},
- {"body", "chronic", "blood_pressure_systolic", "收缩压(mmHg)", "90-120", "1"},
- {"body", "chronic", "blood_pressure_diastolic", "舒张压(mmHg)", "60-80", "2"},
- {"body", "chronic", "blood_sugar_fasting", "空腹血糖(mmol/L)", "3.9-6.1", "3"},
- {"body", "chronic", "cholesterol_total", "总胆固醇(mmol/L)", "3.1-5.2", "4"},
- {"body", "chronic", "triglyceride", "甘油三酯(mmol/L)", "0.56-1.7", "5"},
- {"body", "exercise", "step_count", "日均步数", "6000-10000", "1"},
- {"body", "exercise", "sleep_duration", "睡眠时长(小时)", "7-9", "2"},
- {"mind", "emotion", "emotion_stability", "情绪稳定性", "0-100", "1"},
- {"mind", "emotion", "anxiety_level", "焦虑水平", "0-100", "2"},
- {"mind", "emotion", "sleep_quality", "睡眠质量", "0-100", "3"},
- {"mind", "personality", "openness", "开放性", "0-100", "1"},
- {"mind", "personality", "conscientiousness", "尽责性", "0-100", "2"},
- {"mind", "personality", "extraversion", "外向性", "0-100", "3"},
- {"mind", "personality", "agreeableness", "宜人性", "0-100", "4"},
- {"mind", "personality", "neuroticism", "神经质(反向)", "0-100", "5"},
- {"wisdom", "cognitive", "perception", "感知能力", "0-100", "1"},
- {"wisdom", "cognitive", "focus", "专注力", "0-100", "2"},
- {"wisdom", "cognitive", "memory", "记忆力", "0-100", "3"},
- {"wisdom", "cognitive", "logic", "逻辑思维", "0-100", "4"},
- {"wisdom", "cognitive", "spatial", "空间思维", "0-100", "5"},
- {"wisdom", "cognitive", "processing_speed", "加工速度", "0-100", "6"},
- {"wisdom", "cognitive", "growth_mindset", "成长型思维", "0-100", "7"}
- };
- for (Object[] r : rows) {
- jdbcTemplate.update(
- "INSERT IGNORE INTO indicator_definitions (domain, category, code, name, data_type, ref_range, sort_order, status) VALUES (?,?,?,?,?,?,?,1)",
- r[0], r[1], r[2], r[3], "number", r[4], Integer.parseInt((String) r[5])
- );
- }
- log.info("迁移252: indicator_definitions 种子数据已加载 ({}条)", rows.length);
- } catch (Exception e) {
- log.warn("迁移252失败: {}", e.getMessage());
- }
- log.info("数据库迁移完成");
- // ==================== 健康维度 Phase 1: health_dimension_score / health_data_source_record / health_norm_reference ============= migrateHealthDimensionsTable();
- seedHealthNorms();
- seedMindActionWealthNorms();
- runLateMigrations();
- migration102();
- // 迁移226-247: 原seedFullRegionData内的DDL迁移,确保不因省市区数据已存在而跳过
- runFullRegionDataMigrations();
- // seedFullRegionData(); 已废弃,区域数据由独立迁移处理
- // 迁移269: sleep_record 修改 sleep_time/wake_time 为 TIME(前端按 HH:mm 打卡,java.sql.Time 三段格式)
- try {
- jdbcTemplate.execute("ALTER TABLE sleep_record MODIFY COLUMN sleep_time TIME, MODIFY COLUMN wake_time TIME");
- log.info("已修改 sleep_record 时间为 TIME 类型");
- } catch (Exception e) {
- log.warn("sleep_record 时间列修改失败(可能已为 TIME): {}", e.getMessage());
- }
- // ==================== 迁移270: 先天画像来源权重配置表 =============
- try {
- jdbcTemplate.execute("CREATE TABLE IF NOT EXISTS innate_portrait_config (" +
- "id BIGINT AUTO_INCREMENT PRIMARY KEY," +
- "source_type VARCHAR(20) NOT NULL," +
- "source_name VARCHAR(50) NOT NULL," +
- "weight INT DEFAULT 25," +
- "enabled TINYINT(1) DEFAULT 1," +
- "ai_enabled TINYINT(1) DEFAULT 1," +
- "sort_order INT DEFAULT 0," +
- "created_at DATETIME DEFAULT CURRENT_TIMESTAMP," +
- "updated_at DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP," +
- "UNIQUE KEY uk_source (source_type)" +
- ") ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='先天画像来源权重配置'");
- Integer cnt = jdbcTemplate.queryForObject("SELECT COUNT(*) FROM innate_portrait_config", Integer.class);
- if (cnt == null || cnt == 0) {
- jdbcTemplate.batchUpdate(
- "INSERT INTO innate_portrait_config (source_type, source_name, weight, sort_order) VALUES (?, ?, ?, ?)",
- java.util.Arrays.asList(new Object[][]{
- {"zodiac", "星座", 2500, 1},
- {"bazi", "八字", 2500, 2},
- {"blood", "血型", 2500, 3},
- {"numsoul", "数字能量", 2500, 4},
- })
- );
- log.info("迁移270: innate_portrait_config 种子数据已加载 (4条)");
- }
- log.info("迁移270: innate_portrait_config 表创建完成");
- } catch (Exception e) {
- log.warn("迁移270失败: {}", e.getMessage());
- }
- // ==================== 迁移271: 先天画像报告缓存表 =============
- try {
- jdbcTemplate.execute("CREATE TABLE IF NOT EXISTS innate_portrait_report (" +
- "id BIGINT AUTO_INCREMENT PRIMARY KEY," +
- "member_id BIGINT NOT NULL," +
- "member_type VARCHAR(10) NOT NULL," +
- "family_id BIGINT NOT NULL," +
- "portrait_json TEXT," +
- "ai_reading TEXT," +
- "generated_at DATETIME," +
- "created_at DATETIME DEFAULT CURRENT_TIMESTAMP," +
- "UNIQUE KEY uk_member (member_id, member_type)" +
- ") ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='先天画像报告缓存'");
- log.info("迁移271: innate_portrait_report 表创建完成");
- } catch (Exception e) {
- log.warn("迁移271失败: {}", e.getMessage());
- }
- // ==================== 迁移272: 数字能量详细配置表 =============
- try {
- jdbcTemplate.execute("CREATE TABLE IF NOT EXISTS numsoul_detail_config (" +
- "id BIGINT AUTO_INCREMENT PRIMARY KEY," +
- "number_type VARCHAR(20) NOT NULL," +
- "number_value INT NOT NULL," +
- "title VARCHAR(50)," +
- "keywords VARCHAR(200)," +
- "mind_base INT DEFAULT 0," +
- "mind_advice TEXT," +
- "color_hex VARCHAR(10)," +
- "created_at DATETIME DEFAULT CURRENT_TIMESTAMP," +
- "UNIQUE KEY uk_type_num (number_type, number_value)" +
- ") ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='数字能量详细配置'");
- Integer cnt2 = jdbcTemplate.queryForObject("SELECT COUNT(*) FROM numsoul_detail_config", Integer.class);
- if (cnt2 == null || cnt2 == 0) {
- jdbcTemplate.batchUpdate(
- "INSERT INTO numsoul_detail_config (number_type, number_value, title, keywords, mind_base, mind_advice, color_hex) VALUES (?, ?, ?, ?, ?, ?, ?)",
- java.util.Arrays.asList(new Object[][]{
- {"life_path", 1, "开创者", "独立、领导、创新", 68, "培养团队协作意识,避免独断专行", "#FF4444"},
- {"life_path", 2, "和平者", "温和、合作、敏感", 72, "增强自我主张,避免过度迎合", "#FF8C00"},
- {"life_path", 3, "表达者", "创意、社交、乐观", 78, "保持专注,避免浅尝辄止", "#FFD700"},
- {"life_path", 4, "建设者", "务实、稳定、有序", 70, "学会变通,避免过于固执", "#4CAF50"},
- {"life_path", 5, "自由者", "冒险、变化、好奇", 75, "培养持久力,避免缺乏恒心", "#2196F3"},
- {"life_path", 6, "关怀者", "责任、关爱、奉献", 80, "学会自我关爱,避免过度付出", "#9C27B0"},
- {"life_path", 7, "探索者", "智慧、分析、内省", 76, "与人分享见解,避免过度封闭", "#673AB7"},
- {"life_path", 8, "成就者", "雄心、管理、决策", 72, "关注内在成长,避免过度追求外在", "#3F51B5"},
- {"life_path", 9, "博爱者", "包容、智慧、善良", 82, "设定个人边界,避免过度牺牲", "#E91E63"},
- {"life_path", 11, "启明者", "直觉、灵感、理想", 85, "落地执行,避免空想", "#7C4DFF"},
- {"life_path", 22, "建造师", "远见、实践、宏图", 88, "分步落地大目标", "#536DFE"},
- {"life_path", 33, "引路人", "大爱、奉献、疗愈", 90, "先照顾好自己再照亮他人", "#FF4081"},
- })
- );
- log.info("迁移272: numsoul_detail_config 生命灵数种子已加载");
- }
- log.info("迁移272: numsoul_detail_config 表创建完成");
- } catch (Exception e) {
- log.warn("迁移272失败: {}", e.getMessage());
- }
- // 迁移273: dan_report_uploads.file_id 加宽(OSS 完整 URL 超过原 VARCHAR(100) 导致截断报错)
- try {
- jdbcTemplate.execute("ALTER TABLE dan_report_uploads MODIFY COLUMN file_id VARCHAR(500) COMMENT '媒资文件ID或相对路径'");
- log.info("迁移273: 已加宽 dan_report_uploads.file_id 为 VARCHAR(500)");
- } catch (Exception e) {
- log.warn("迁移273失败: {}", e.getMessage());
- }
- // 迁移274: dan_assessment_results.report_file_id 加宽(确认入库写入 OSS URL 超长截断)
- try {
- jdbcTemplate.execute("ALTER TABLE dan_assessment_results MODIFY COLUMN report_file_id VARCHAR(500) COMMENT '上传文件媒资ID或相对路径'");
- log.info("迁移274: 已加宽 dan_assessment_results.report_file_id 为 VARCHAR(500)");
- } catch (Exception e) {
- log.warn("迁移274失败: {}", e.getMessage());
- }
- // 迁移275: dan_assessment_results.dan_level 兼容旧库(NOT NULL 无默认导致家长上传确认失败)
- ensureColumn("dan_assessment_results", "dan_level",
- "VARCHAR(10) NULL DEFAULT NULL COMMENT 'DAN等级A/B/C/D'");
- try {
- jdbcTemplate.execute("ALTER TABLE dan_assessment_results MODIFY COLUMN dan_level VARCHAR(10) NULL DEFAULT NULL COMMENT 'DAN等级A/B/C/D'");
- log.info("迁移275: 已将 dan_assessment_results.dan_level 改为可空并带默认");
- } catch (Exception e) {
- log.warn("迁移275失败: {}", e.getMessage());
- }
- runSystemMigrations();
- registerCollectExpectedIndicators();
- // 减脂专题表创建(远程 fatloss 功能)
- createFatWeightRecordTable();
- createFatDietRecordTable();
- createFatExerciseRecordTable();
- createFatGoalTable();
- addProductVendorVisibleColumn();
- // 迁移367: products 表添加健康约束字段(基础疾病禁忌/过敏原/药物相互作用标记)
- addProductHealthConstraintColumns();
- // 迁移368: 创建 drug_interactions 表(药物-食物禁忌对照表)
- createDrugInteractionsTable();
- // 迁移369: 创建 disease_intake_limits 表(疾病-饮食摄入阈值)
- createDiseaseIntakeLimitsTable();
- // 迁移370: 初始化 drug_interactions 种子数据
- seedDrugInteractionsData();
- // 迁移371: 初始化 disease_intake_limits 种子数据
- seedDiseaseIntakeLimitsData();
- // 指标统一层迁移(356~362)
- migrateIndicatorDefinitionsColumns();
- migrateIndicatorValuesColumns();
- addIndicatorValuesKeys();
- createIndicatorMappingRulesTable();
- createIndicatorCorrelationsTable();
- ensureIndicatorCollectionDate();
- seedDimensionDefinitions();
- // 商品组合推荐与采购任务迁移(363)
- createProcurementTaskTable();
- // 迁移364: health_report_drafts 添加解析进度/重试字段(known-type 解析失败退避重试,不再静默回退 opencode)
- addDraftParseProgressColumns();
- // 迁移365: diet_preferences 添加无特殊偏好标记(问卷第3步「无特殊偏好」一键跳过落库)
- ensureColumn("diet_preferences", "no_preference", "TINYINT(1) DEFAULT 0 COMMENT '无特殊偏好标记(1=用户明确选择无特殊偏好)'");
- // 迁移366: 9.9 元/月健康会员复用既有 BASIC「体验会员」等级(迁移360/schema.sql 已创建),
- // 移除早期误建的重复等级 LITE(与 BASIC 同为 9.9 元/月),并迁移可能已落库的 LITE 记录。
- // 三段独立 try,确保任一段异常都不会阻断后续的等级删除。
- try {
- jdbcTemplate.update("UPDATE family_memberships SET level_code = 'BASIC' WHERE level_code = 'LITE'");
- } catch (Exception e) {
- log.warn("迁移366: 迁移 family_memberships 中的 LITE 记录失败: {}", e.getMessage());
- }
- try {
- jdbcTemplate.update("UPDATE users SET member_level = 'BASIC' WHERE member_level = 'LITE'");
- } catch (Exception e) {
- log.warn("迁移366: 迁移 users.member_level 中的 LITE 记录失败: {}", e.getMessage());
- }
- try {
- int removed = jdbcTemplate.update("DELETE FROM membership_levels WHERE level_code = 'LITE'");
- log.info("迁移366: 9.9 元/月健康会员统一使用 BASIC 体验会员,已移除重复的 LITE 等级({} 行)", removed);
- } catch (Exception e) {
- log.warn("迁移366: 清理 LITE 等级失败或不存在: {}", e.getMessage());
- }
- // 迁移372: nutrition_product 添加 efficacy_tags 列(成分功效标签)
- addNutritionProductEfficacyTags();
- // 迁移373: products 添加 tags 列(文章-商品标签关联推荐)
- addProductTagsColumn();
- // 迁移374: 创建 user_content_weights 表(用户内容偏好权重表)
- addUserContentWeightsTable();
- // 迁移375: 修复 article_quiz_records 表结构(旧表缺 member_id/total_questions/cf_earned/energy_earned,questions/answers 需为 TEXT)
- fixArticleQuizRecordsTable();
- // 迁移376: health_status 添加 lifestyle JSON 列(心情/睡眠/排便/抗生素/过敏表现)
- ensureColumn("health_status", "lifestyle", "JSON DEFAULT NULL COMMENT '生活方式与肠道健康 {mood,sleepDuration,bedtime,bowelFrequency,bowelForm,antibioticHistory,allergySymptoms}'");
- // 迁移377: diet_preferences 添加 diet_structure JSON 列(蔬果量/乳制品/含糖饮料频率)
- ensureColumn("diet_preferences", "diet_structure", "JSON DEFAULT NULL COMMENT '饮食结构现状 {vegetableFruit,dairy,sugaryDrinks}'");
- // 迁移378: 创建 member_indicator_current 表(成员指标当前值 + 相对上次变化 delta)
- addMemberIndicatorCurrentTable();
- // 迁移379: 创建 report_member_permissions 表(管理员授权某用户查看某成员的全部报告)
- addReportMemberPermissionsTable();
- // 迁移380: 修正 FAMILY 会员虚拟道具映射 member1314 → member1980(会员等级表 FAMILY 年费 ¥1980,原道具价 ¥1314 与下单金额不一致,会被微信"下单金额=道具价"校验拒付)
- fixFamilyMembershipVirtualGoodsMapping();
- // 迁移381: 创建 activity_poster_templates 表 + 初始化 3 套默认海报模板(Web 管理端定义 → 小程序端海报模板选择)
- createActivityPosterTemplatesTable();
- // 迁移385: 活动报名添加驳回原因列(审核中心详情页驳回时写入)
- addActivityRegistrationRejectReasonColumn();
- // 迁移386: 清理规划师占位假数据(teacher_status='pending' 且 role<>'teacher'),默认值改 NULL
- cleanPlannerPlaceholderData();
- // 迁移387: 下线独立“服务角色申请审核”菜单(申请按角色分流合并进审核中心)
- hideServiceRoleApplicationMenu();
- // 迁移388: activities表添加intro列(活动简介,纯文本短摘要,用于活动列表卡片展示)
- ensureColumn("activities", "intro", "VARCHAR(200) DEFAULT NULL COMMENT '活动简介(列表卡片展示)'");
- // 迁移389: activities表添加tags列(活动标签,逗号分隔,根据活动内容自动生成)
- ensureColumn("activities", "tags", "VARCHAR(255) DEFAULT NULL COMMENT '活动标签,逗号分隔'");
- // 迁移382: health_status 添加 signature 列(用户手签名,base64 PNG data URL,用于问卷回答确认)
- ensureColumn("health_status", "signature", "MEDIUMTEXT DEFAULT NULL COMMENT '用户手签名(base64 PNG data URL),确认问卷回答准确性'");
- // 迁移383: health_status 添加 signed_at 列(手签名时间;问卷内容变更后置空,需重新签署)
- ensureColumn("health_status", "signed_at", "DATETIME DEFAULT NULL COMMENT '手签名时间(问卷变更后置空,需重新签署)'");
- // 迁移384: water_record 添加 task_id 列(喝水打卡任务回写,与 health_meal_record.task_id 同款)
- ensureColumn("water_record", "task_id", "BIGINT NULL COMMENT '关联任务ID(喝水打卡任务回写)'");
- // 迁移385: family_platform_exchange_record 添加 family_coupon_id 列(把 CF 兑换记录与发出的券实例绑定)
- // 目的:订单取消/退款时,对用户主动选择使用的券实例能精确判断「是否为 CF 兑换而来」并退还 CF 值;
- // 平台赠送券(JOIN/POPULATION/MANUAL)无对应兑换记录,退款时不退 CF。
- ensureColumn("family_platform_exchange_record", "family_coupon_id",
- "BIGINT DEFAULT NULL COMMENT '兑换发放的优惠券实例ID(family_coupon.id), 退款时据此退还CF值'");
- // 迁移388: activities 添加 poster_template_id 列(小程序端创建活动时选择海报模板并保存,详情页生成海报时默认使用)
- ensureColumn("activities", "poster_template_id",
- "BIGINT NULL COMMENT '创建时选择的海报模板ID(activity_poster_templates.id, 小程序端海报模板选择)'");
- // 迁移389: 创建 special_price_deals 表(限时特价活动/扫码闪购,见 specs/2026-10-20-limited-time-special-price-design.md)
- try {
- jdbcTemplate.execute("CREATE TABLE IF NOT EXISTS special_price_deals (" +
- "id BIGINT AUTO_INCREMENT PRIMARY KEY," +
- "product_id BIGINT NOT NULL," +
- "name VARCHAR(100) NOT NULL," +
- "special_price INT NOT NULL COMMENT '特价(分)'," +
- "start_time DATETIME," +
- "end_time DATETIME," +
- "total_quantity INT NOT NULL DEFAULT 0," +
- "sold_quantity INT NOT NULL DEFAULT 0," +
- "per_user_limit INT NOT NULL DEFAULT 1," +
- "allow_coupon TINYINT NOT NULL DEFAULT 0," +
- "status VARCHAR(20) NOT NULL DEFAULT 'draft'," +
- "created_at DATETIME," +
- "updated_at DATETIME," +
- "INDEX idx_product (product_id)," +
- "INDEX idx_status (status, start_time, end_time)" +
- ") ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='限时特价活动'");
- log.info("迁移389: 已创建 special_price_deals 表");
- } catch (Exception e) {
- log.warn("迁移389: 创建 special_price_deals 表失败(可能已存在): {}", e.getMessage());
- }
- // 迁移390: product_orders 添加 special_deal_id 列(限时特价订单关联)
- ensureColumn("product_orders", "special_deal_id", "BIGINT NULL COMMENT '限时特价活动ID(非空=特价单)'");
- // 迁移391: activity_registrations 添加取消时间/原因列(P0-2 取消报名支持 approved + 退款链路,取消时记录)
- ensureColumn("activity_registrations", "cancelled_at", "DATETIME DEFAULT NULL COMMENT '取消报名时间'");
- ensureColumn("activity_registrations", "cancel_reason", "VARCHAR(200) DEFAULT NULL COMMENT '取消原因'");
- // 迁移392: activity_feedback 添加 updated_at 列 + activity_id+child_id 唯一索引(P1-3 评价去重:同一孩子只能评一次,重复提交改为更新)
- try {
- ensureColumn("activity_feedback", "updated_at", "DATETIME DEFAULT NULL COMMENT '更新时间(重复提交评价时更新)'");
- jdbcTemplate.execute("ALTER TABLE activity_feedback ADD UNIQUE KEY uk_activity_child (activity_id, child_id)");
- log.info("迁移392: 已为 activity_feedback 添加 updated_at 列和 uk_activity_child 唯一索引");
- } catch (Exception e) {
- log.warn("迁移392: 添加 activity_feedback.updated_at / uk_activity_child 失败(可能已存在): {}", e.getMessage());
- }
- // 迁移393: 创建 organizations 表(机构/企业组织主表)
- try {
- jdbcTemplate.execute("CREATE TABLE IF NOT EXISTS `organizations` (" +
- "`id` BIGINT AUTO_INCREMENT PRIMARY KEY, " +
- "`name` VARCHAR(128) NOT NULL COMMENT '组织名称', " +
- "`type` VARCHAR(32) COMMENT 'enterprise/institution/other', " +
- "`code` VARCHAR(64) UNIQUE COMMENT '组织编号(唯一标识)', " +
- "`admin_user_id` BIGINT COMMENT '组织管理员 userId(关联 users.id)', " +
- "`contact_name` VARCHAR(64) COMMENT '联系人姓名', " +
- "`contact_phone` VARCHAR(32) COMMENT '联系人电话', " +
- "`description` TEXT COMMENT '组织描述', " +
- "`status` VARCHAR(16) NOT NULL DEFAULT 'active' COMMENT 'active/disabled', " +
- "`created_at` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, " +
- "`updated_at` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, " +
- "INDEX `idx_admin_user_id` (`admin_user_id`), " +
- "INDEX `idx_status` (`status`) " +
- ") ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='机构/企业组织主表'");
- log.info("迁移393: 已创建 organizations 表");
- } catch (Exception e) {
- log.warn("迁移393: 创建 organizations 表失败(可能已存在): {}", e.getMessage());
- }
- // 迁移393b: 创建 org_members 表(组织成员关联表)
- try {
- jdbcTemplate.execute("CREATE TABLE IF NOT EXISTS `org_members` (" +
- "`id` BIGINT AUTO_INCREMENT PRIMARY KEY, " +
- "`org_id` BIGINT NOT NULL COMMENT '组织ID', " +
- "`user_id` BIGINT NOT NULL COMMENT '用户ID(users.id)', " +
- "`member_type` VARCHAR(16) NOT NULL COMMENT 'planner/user', " +
- "`role_in_org` VARCHAR(32) COMMENT '组织内角色(teacher/student/member等)', " +
- "`status` VARCHAR(16) NOT NULL DEFAULT 'active' COMMENT 'active/inactive', " +
- "`joined_at` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, " +
- "UNIQUE KEY `uk_org_user` (`org_id`, `user_id`), " +
- "INDEX `idx_org_id` (`org_id`), " +
- "INDEX `idx_user_id` (`user_id`), " +
- "INDEX `idx_status` (`status`) " +
- ") ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='组织成员关联表'");
- log.info("迁移393b: 已创建 org_members 表");
- } catch (Exception e) {
- log.warn("迁移393b: 创建 org_members 表失败(可能已存在): {}", e.getMessage());
- }
- // 迁移394: 创建 planner_cert_categories 表 + 初始化首批学习种类(planner/nutritionist/assessor)
- // 见 docs/superpowers/specs/2026-10-22-planner-learning-assessment-gate-design.md
- createPlannerCertCategoriesTable();
- // 迁移395: 创建 planner_courses 表(线上课程目录,映射小鹅通课程)
- createPlannerCoursesTable();
- // 迁移396: 创建 planner_course_progress 表(每用户每课程进度,小鹅通回传/手动同步写入)
- createPlannerCourseProgressTable();
- // 迁移397: 创建 planner_offline_course_map 表(线下培训活动↔认证类别桥接,复用 activities 模块)
- createPlannerOfflineCourseMapTable();
- // 迁移398: 创建 activity_checkins 表(活动签到记录,供培训课时桥接,修复签到审核形同虚设旧患)
- createActivityCheckinsTable();
- // 迁移399: 创建 planner_certifications 表(规划师每类别培训认证主表,认证闸门依据)
- createPlannerCertificationsTable();
- // 迁移400: 创建 planner_exam_papers / planner_exam_questions / planner_exam_attempts 表(考核自建)
- createPlannerExamTables();
- // 迁移401: 存量规划师补登认证(D6:active guide 按已持有基础类别 planner certified 补登,避免误封存量)
- backfillExistingPlannerCertifications();
- // 迁移402: delivery_method COMMENT 增加 6=同城配送(履约多渠道,规格 2026-10-22-fulfillment-multi-mode-design)
- addDeliveryMethod6Comments();
- // 迁移403: withdrawal_requests 增加 reject_reason 列(审核中心统一详情:提现驳回原因与活动报名/服务角色申请口径一致)
- addWithdrawalRejectReasonColumn();
- // 迁移404: articles.audio_url 由 varchar(500) 改为 MEDIUMTEXT(多段音频JSON数组可超500字符)
- alterArticleAudioUrlToMediumtext();
- // 迁移406: activity_poster_templates 增加 layout_config 列(海报模板元素位置/样式可配置,需求:活动详情海报预览重设计)
- addActivityPosterTemplateLayoutConfigColumn();
- // 迁移407: 创建 cf_balance / cf_balance_log(个人 CF 值账本,成员维度;F-1)
- createCfBalanceTables();
- // 迁移408: 创建 cf_earn_rule / cf_earn_rule_history + 24 行规则种子(F02–F24;F-1)
- createCfEarnRuleTables();
- seedCfEarnRules();
- // 迁移409: 创建 cf_direct_sales_tier / cf_indirect_sales_tier(两级销售奖档位;F-1)
- createCfSalesTierTables();
- // 迁移410: 创建 cf_earn_daily_limit + 会员等级日限种子(N11;F-1)
- createCfEarnDailyLimitTable();
- seedCfEarnDailyLimits();
- // 迁移411: coupon / user_coupon 补 family_id(优惠券家庭化 N3;F-1)
- addCouponFamilyIdColumns();
- // 迁移412: 创建 service_config + 散落参数迁入 + CF 通用运营参数种子(W0.1)
- // 顺序:先迁旧位置实际值,再补种子默认值(避免默认值覆盖 sys_config 里的真实配置)
- createServiceConfigTable();
- migrateScatteredCfConfigs();
- seedCfServiceConfig();
- // 迁移413: service_config 收编 product_config(W0.2 双配置中心合并)
- // 幂等:product_key 列/索引已存在则跳过;product_config 表不存在时数据并入为空跑
- addServiceConfigProductKeyColumn();
- migrateProductConfigToServiceConfig();
- // 迁移414: 创建 audit_log 表(管理端写操作统一审计,W0.3;表只增不删改)
- createAuditLogTable();
- // 迁移415: 创建 family_food_overview 表(家庭×食材 持久化最小值总览;T28)
- // 家庭总览取"家庭成员该项食材最新推荐指数的最小值"并持久化,由 recalculateForUser 触发重算
- createFamilyFoodOverviewTable();
- // 迁移416: cf_balance_log 补 operator_user_id + 幂等键升级 (member_id,type,ref_type,ref_id)
- // + CF 提现开关/汇率种子(F-2)
- migrateCfBalanceLogForLedger();
- // 迁移417: 三套旧 CF 账本一次性迁入 cf_balance(F-6 存量迁移)
- // 顺序:必须在迁移416(uk_ref_type 唯一键升级)之后执行,幂等守卫与新唯一键保持一致。
- // 幂等 ref_type 分别为 migration_family_pool / migration_system_points /
- // migration_user_wallet,重复启动不会重复入账。
- try {
- int migrated = cfLegacyLedgerMigrationService.migrate();
- log.info("迁移417: 旧账本存量迁移完成,本次新增 {} 笔", migrated);
- } catch (Exception e) {
- // 事务已整体回滚;保留启动可用性,下次启动可安全重试。
- log.warn("迁移417: 旧账本存量迁移失败,本批已回滚,下次启动重试: {}", e.getMessage());
- }
- // 迁移418: 两级销售奖档位种子 + F03a/F03b 幂等 ref_type 对齐(F-4 分佣收口)
- seedCfSalesTiers();
- alignSalesRuleRefTypes();
- // 迁移419: membership_levels 补 max_daily_analyze(会员等级每日报告解析次数上限)
- // 背景:ReportAnalyzeQuotaService(报告解析配额)已入库,但 MembershipLevel 实体字段与
- // membership_levels 表列在历史提交中丢失,导致编译失败 + 运行时 Unknown column。
- // 此处补列(幂等);0/NULL 表示不限次。
- ensureColumn("membership_levels", "max_daily_analyze",
- "INT DEFAULT NULL COMMENT '每日报告解析次数上限(0/NULL=不限)'");
- // 迁移420: 创建 ppoint_consumption_log(滚动一年销售奖档位聚合数据源;F-4)
- // 注:迁移419 已被 membership_levels.max_daily_analyze 占用,本表顺延为 420。
- createPpointConsumptionLogTable();
- // 迁移421: activities 表新增适用人群字段 audience_scope / audience_type(A2 活动人群与费用模型重构)
- // 说明:沿用 visible_scope 迁移的 information_schema.COLUMNS 存在性检查范式,
- // 以便用 AFTER visible_to 精确定位列顺序(ensureColumn 不支持列定位)。
- addActivityAudienceColumns();
- // 迁移422: 指标多期展示(DS-2026-10-25-01)
- // 顺序红线:先确保 health_indicators.collection_date 物理列存在,才能放开实体映射;
- // 顺序颠倒会导致缺列环境所有 health_indicators 查询报 Unknown column。
- ensureHealthIndicatorsCollectionDateColumn();
- migrateIndicatorValuesTimelineColumns();
- // 迁移423: 体脂秤测量记录表(sourceId 由 memberId 改为 recordId 的前置表)
- createBodyFatScaleRecordsTable();
- // 迁移424: health_plans 加 plan_type(区分健康方案/家庭归因干预方案;G5)
- migrateHealthPlansPlanType();
- // 迁移425: 家庭归因常模值表(G10,菌群 710 常模落点)
- createAttributionNormValuesTable();
- // 迁移425-导入: 菌群常模可配置 JSON 导入(空壳文件导入 0 行,不报错)
- importAttributionNormGutFlora();
- // 迁移425-种子: 家庭归因确认问卷模板种子(第八章 16 条,幂等)
- seedFamilyAttributionSurveyTemplate();
- // 迁移426: 家庭归因报告表 attribution_reports(B1 核心闭环③,report_json+intermediate_state+redline_hits+status)
- createAttributionReportsTable();
- // 迁移427: 创建 content_dimension_threshold 表(A-后端 内容维度阈值显隐配置:商品/文章/活动/游戏 五维+子项+自定义指标)
- createContentDimensionThresholdTable();
- // 迁移428: 会员优惠券转赠表 coupon_gift_transfer + family_coupon.is_gift 列
- // (用户可把自己的会员券转赠给推荐下级,用于购买会员,可撤回)
- createCouponGiftTransferTable();
- // 迁移429: 用户关注指标收藏表 indicator_favorite(T04 方案A:后端持久化,跨端同步)
- createIndicatorFavoriteTable();
- // 迁移430: 商品成本价改为整型(分) —— P点口径:P点 = 实际成交额 - 成本价(分)
- migrateProductsPurchasePriceToCents();
- // 迁移431: 销售奖档位门槛 ×100(元→分)+ 档位名改为 新秀/小成/中富/大成/至富
- migrateSalesAwardTierThresholds();
- // 迁移429(家庭): 存量家庭名回填(家庭主账号已填真实姓名但家庭名仍为旧规则 → 按新规则回填「XX家庭」;DML,无 schema 变更,幂等)
- migrateFamilyNameBackfill();
- // 迁移429(家庭): 存量家庭名回填(家庭主账号已填真实姓名但家庭名仍为旧规则 → 按新规则回填「XX家庭」;DML,无 schema 变更,幂等)
- migrateFamilyNameBackfill();
- // 迁移432: report_type_registry 添加 linked_product_id 列(报告类型→商城商品链接配置,规格 2026-10-30-report-type-product-link-design)
- ensureColumn("report_type_registry", "linked_product_id",
- "BIGINT DEFAULT NULL COMMENT '关联商城商品 products.id,NULL=不可链接'");
- // 迁移433: 家庭指标聚合方式配置表(五维能量汇总/珍珠图/章鱼图汇集配置)
- createFamilyMetricAggConfigTable();
- // 迁移434: SKU商品组合明细表 + 订单记录下单SKU(组合SKU识别)
- migrateSkuCombo();
- // 迁移436: products 表添加营养数据字段(热量/蛋白质/脂肪/碳水/膳食纤维/营养标签)
- migrateProductNutritionFields();
- // 迁移437: T35 活动管理团队 —— activities.team_id + activity_team/activity_team_member/service_assignment 表
- migrateActivityTeam();
- // 迁移440: 套餐内容明细表(套餐=独立商品,内容=子商品SKU×数量,挂套餐SKU级)
- migrateProductComboItems();
- }
- /**
- * 迁移430: 商品成本价改为整型(分)(P点口径调整)。
- *
- * <p>原 products.purchase_price DECIMAL(10,2),值与 price 同量级(分);改为 INT 存分。
- * 仅改类型不做数值换算(幂等:已是 INT 则跳过)。</p>
- */
- private void migrateProductsPurchasePriceToCents() {
- try {
- String colType = jdbcTemplate.queryForObject(
- "SELECT DATA_TYPE FROM information_schema.COLUMNS WHERE TABLE_SCHEMA=DATABASE() "
- + "AND TABLE_NAME='products' AND COLUMN_NAME='purchase_price'", String.class);
- if ("decimal".equalsIgnoreCase(colType)) {
- // 值本身已是分(与 price 同量级、低于售价),只改类型不做数值换算
- jdbcTemplate.execute("ALTER TABLE products MODIFY COLUMN purchase_price INT NOT NULL DEFAULT 0 COMMENT '成本价(分)'");
- log.info("迁移430完成:products.purchase_price decimal→int(分)(值已是分,仅改类型)");
- } else {
- log.info("迁移430跳过:products.purchase_price 已是 {} 类型", colType);
- }
- } catch (Exception e) {
- log.warn("迁移430失败(幂等容忍): {}", e.getMessage());
- }
- }
- /**
- * 迁移431: 销售奖档位门槛由元改为分(×100),并按 id 重命名为新档位名(费率不变)。
- *
- * <p>档位:新秀(1w)/小成(5w)/中富(20w)/大成(50w)/至富(100w),单位分。</p>
- */
- private void migrateSalesAwardTierThresholds() {
- try {
- // 唯一键 uk_min 冲突规避:先设临时值(唯一且不撞最终值),再设最终值。
- // 单条 CASE UPDATE 逐行执行时,若直接改 100→10000 会与尚未更新的 10000(原钻石)撞 uk_min。
- jdbcTemplate.execute(
- "UPDATE cf_direct_sales_tier SET min_annual_ppoints = 9000000 + id, tier_name = CONCAT('TMP', id) "
- + "WHERE id BETWEEN 1 AND 5 AND tier_name IN ('青铜','白银','黄金','铂金','钻石')");
- jdbcTemplate.execute(
- "UPDATE cf_direct_sales_tier SET min_annual_ppoints = CASE id "
- + "WHEN 1 THEN 10000 WHEN 2 THEN 50000 WHEN 3 THEN 200000 WHEN 4 THEN 500000 WHEN 5 THEN 1000000 END, "
- + "tier_name = CASE id WHEN 1 THEN '新秀' WHEN 2 THEN '小成' WHEN 3 THEN '中富' "
- + "WHEN 4 THEN '大成' WHEN 5 THEN '至富' END "
- + "WHERE id BETWEEN 1 AND 5 AND min_annual_ppoints BETWEEN 9000001 AND 9000005");
- jdbcTemplate.execute(
- "UPDATE cf_indirect_sales_tier SET min_self_ppoints = 9000000 + id, tier_name = CONCAT('TMP', id) "
- + "WHERE id BETWEEN 1 AND 5 AND tier_name IN ('青铜','白银','黄金','铂金','钻石')");
- jdbcTemplate.execute(
- "UPDATE cf_indirect_sales_tier SET min_self_ppoints = CASE id "
- + "WHEN 1 THEN 10000 WHEN 2 THEN 50000 WHEN 3 THEN 200000 WHEN 4 THEN 500000 WHEN 5 THEN 1000000 END, "
- + "tier_name = CASE id WHEN 1 THEN '新秀' WHEN 2 THEN '小成' WHEN 3 THEN '中富' "
- + "WHEN 4 THEN '大成' WHEN 5 THEN '至富' END "
- + "WHERE id BETWEEN 1 AND 5 AND min_self_ppoints BETWEEN 9000001 AND 9000005");
- log.info("迁移431完成:销售奖档位门槛×100(分),档位名新秀/小成/中富/大成/至富");
- } catch (Exception e) {
- log.warn("迁移431失败(幂等容忍): {}", e.getMessage());
- }
- }
- /** 迁移:家庭主账号已填真实姓名但家庭名仍为旧规则 → 回填为「XX家庭」(幂等,重复执行无副作用) */
- private void migrateFamilyNameBackfill() {
- String sql = "UPDATE families f JOIN users u ON u.id = f.creator_id " +
- "SET f.name = CONCAT(TRIM(u.real_name), '家庭'), f.updated_at = NOW() " +
- "WHERE u.real_name IS NOT NULL AND TRIM(u.real_name) <> '' " +
- "AND f.name <> CONCAT(TRIM(u.real_name), '家庭')";
- try {
- int n = jdbcTemplate.update(sql);
- log.info("迁移[家庭名回填XX家庭] 影响行数={}", n);
- } catch (Exception e) {
- log.warn("迁移[家庭名回填XX家庭] 执行异常: {}", e.getMessage());
- }
- }
- /**
- * 迁移421: activities 表新增 audience_scope / audience_type 两列(A2)
- * - audience_scope: all(默认,全部用户可参加) / member_only(仅会员可参加,后端硬门禁)
- * - audience_type : adult(默认,仅成人) / include_child / include_elder / include_child_elder
- * 长者档不新增独立价格列(决策D1),长者复用成人价,audience_type 仅作展示与筛选标签。
- * 幂等:information_schema 检查列是否存在,存在则跳过。
- */
- private void addActivityAudienceColumns() {
- // 迁移421-1: activities表添加audience_scope列
- try {
- Integer colExists = jdbcTemplate.queryForObject(
- "SELECT COUNT(*) FROM information_schema.COLUMNS WHERE TABLE_SCHEMA = DATABASE() AND TABLE_NAME = 'activities' AND COLUMN_NAME = 'audience_scope'",
- Integer.class);
- if (colExists == null || colExists == 0) {
- jdbcTemplate.execute("ALTER TABLE activities ADD COLUMN audience_scope VARCHAR(20) DEFAULT 'all' COMMENT '参与人群范围: all(全部用户)/member_only(仅会员可参加)' AFTER visible_to");
- log.info("已添加audience_scope列到activities表");
- } else {
- log.info("activities表audience_scope列已存在,跳过");
- }
- } catch (Exception e) {
- log.warn("检查/添加activities表audience_scope列失败: {}", e.getMessage());
- }
- // 迁移421-2: activities表添加audience_type列
- try {
- Integer colExists = jdbcTemplate.queryForObject(
- "SELECT COUNT(*) FROM information_schema.COLUMNS WHERE TABLE_SCHEMA = DATABASE() AND TABLE_NAME = 'activities' AND COLUMN_NAME = 'audience_type'",
- Integer.class);
- if (colExists == null || colExists == 0) {
- jdbcTemplate.execute("ALTER TABLE activities ADD COLUMN audience_type VARCHAR(20) DEFAULT 'adult' COMMENT '参与人群构成: adult(仅成人)/include_child/include_elder/include_child_elder' AFTER audience_scope");
- log.info("已添加audience_type列到activities表");
- } else {
- log.info("activities表audience_type列已存在,跳过");
- }
- } catch (Exception e) {
- log.warn("检查/添加activities表audience_type列失败: {}", e.getMessage());
- }
- }
- /**
- * 迁移436:products 表添加营养数据字段。
- * <p>商品营养数据用于管理端录入、小程序详情页展示与推荐按营养标签筛选。
- * 全部字段允许为 NULL(NULL = 未配置),迁移幂等可重复执行。
- * <p>编号说明:原分支为迁移417,合入主线时 417 已被「旧 CF 账本存量迁移」占用,顺延至主线最新 435 (product_bundle_items放开可空) + 1 = 436。
- */
- private void migrateProductNutritionFields() {
- if (!tableExists("products")) {
- log.warn("迁移436: products 表不存在,跳过营养数据字段迁移");
- return;
- }
- try {
- ensureColumn("products", "calories_kcal", "DECIMAL(10,2) DEFAULT NULL COMMENT '热量(kcal/100g),NULL=未配置'");
- ensureColumn("products", "protein_g", "DECIMAL(10,2) DEFAULT NULL COMMENT '蛋白质(g/100g),NULL=未配置'");
- ensureColumn("products", "fat_g", "DECIMAL(10,2) DEFAULT NULL COMMENT '脂肪(g/100g),NULL=未配置'");
- ensureColumn("products", "carb_g", "DECIMAL(10,2) DEFAULT NULL COMMENT '碳水化合物(g/100g),NULL=未配置'");
- ensureColumn("products", "fiber_g", "DECIMAL(10,2) DEFAULT NULL COMMENT '膳食纤维(g/100g),NULL=未配置'");
- ensureColumn("products", "nutrition_tags", "VARCHAR(255) DEFAULT NULL COMMENT '营养标签,逗号分隔(如\"低糖,高蛋白\"),NULL=无标签'");
- log.info("迁移436: products 表营养数据字段已就绪");
- } catch (Exception e) {
- log.warn("迁移436: products 表营养数据字段迁移失败: {}", e.getMessage());
- }
- }
- /**
- * 迁移437:T35 活动管理团队。
- *
- * <p>activities.team_id 关联 activity_team;activity_team/activity_team_member 承载团队与成员;
- * service_assignment 为通用服务指派表(service_type=ACTIVITY_SUPPORT),供活动运营服务指派使用,
- * 与 T34 服务指派机制同一张表,后续服务类型只需新增 service_type 取值。
- * 全部 DDL 幂等(ensureColumn / CREATE TABLE IF NOT EXISTS)。</p>
- */
- private void migrateActivityTeam() {
- try {
- ensureColumn("activities", "team_id",
- "BIGINT DEFAULT NULL COMMENT '活动管理团队ID(activity_team.id), NULL=无团队; 团队成员可协作运营(迁移437/T35)'");
- log.info("迁移437: activities.team_id 列已就绪");
- } catch (Exception e) {
- log.warn("迁移437: activities.team_id 列检查失败: {}", e.getMessage());
- }
- try {
- jdbcTemplate.execute("CREATE TABLE IF NOT EXISTS activity_team (" +
- "id BIGINT AUTO_INCREMENT PRIMARY KEY, " +
- "name VARCHAR(100) NOT NULL COMMENT '团队名称', " +
- "description VARCHAR(500) DEFAULT NULL COMMENT '团队简介', " +
- "owner_id BIGINT NOT NULL COMMENT '团长userId', " +
- "status VARCHAR(20) NOT NULL DEFAULT 'active' COMMENT 'active/disabled', " +
- "created_at DATETIME DEFAULT CURRENT_TIMESTAMP, " +
- "updated_at DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, " +
- "INDEX idx_owner (owner_id)" +
- ") ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='活动管理团队'");
- log.info("迁移437: 已创建 activity_team 表");
- } catch (Exception e) {
- log.warn("迁移437: 创建 activity_team 表失败(可能已存在): {}", e.getMessage());
- }
- try {
- jdbcTemplate.execute("CREATE TABLE IF NOT EXISTS activity_team_member (" +
- "id BIGINT AUTO_INCREMENT PRIMARY KEY, " +
- "team_id BIGINT NOT NULL COMMENT '团队ID(activity_team.id)', " +
- "user_id BIGINT NOT NULL COMMENT '成员userId', " +
- "role VARCHAR(32) NOT NULL DEFAULT 'operator' COMMENT 'owner(团长)/manager(运营主管)/operator(运营成员)', " +
- "status VARCHAR(20) NOT NULL DEFAULT 'active' COMMENT 'active(在团)/removed(已移出)', " +
- "joined_at DATETIME DEFAULT CURRENT_TIMESTAMP, " +
- "UNIQUE KEY uk_team_user (team_id, user_id), " +
- "INDEX idx_user (user_id)" +
- ") ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='活动团队成员'");
- log.info("迁移437: 已创建 activity_team_member 表");
- } catch (Exception e) {
- log.warn("迁移437: 创建 activity_team_member 表失败(可能已存在): {}", e.getMessage());
- }
- try {
- jdbcTemplate.execute("CREATE TABLE IF NOT EXISTS service_assignment (" +
- "id BIGINT AUTO_INCREMENT PRIMARY KEY, " +
- "service_type VARCHAR(32) NOT NULL COMMENT '服务类型: ACTIVITY_SUPPORT(活动运营)', " +
- "target_type VARCHAR(32) NOT NULL COMMENT '目标类型: ACTIVITY(具体活动)/ACTIVITY_TEAM(团队整体)', " +
- "target_id BIGINT NOT NULL COMMENT '目标ID', " +
- "team_id BIGINT DEFAULT NULL COMMENT '关联团队ID(activity_team.id)', " +
- "assignee_id BIGINT NOT NULL COMMENT '被指派人userId', " +
- "role VARCHAR(32) NOT NULL DEFAULT 'support' COMMENT 'lead/support/checkin/content', " +
- "status VARCHAR(20) NOT NULL DEFAULT 'active' COMMENT 'active/revoked', " +
- "remark VARCHAR(255) DEFAULT NULL COMMENT '备注', " +
- "assigned_by BIGINT DEFAULT NULL COMMENT '指派操作人userId', " +
- "created_at DATETIME DEFAULT CURRENT_TIMESTAMP, " +
- "updated_at DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, " +
- "UNIQUE KEY uk_service_assign (service_type, target_type, target_id, assignee_id, role), " +
- "INDEX idx_team (team_id), " +
- "INDEX idx_assignee (assignee_id)" +
- ") ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='服务指派(活动运营ACTIVITY_SUPPORT, T34机制复用)'");
- log.info("迁移437: 已创建 service_assignment 表");
- } catch (Exception e) {
- log.warn("迁移437: 创建 service_assignment 表失败(可能已存在): {}", e.getMessage());
- }
- }
- /**
- * 迁移440:套餐内容明细表 product_combo_items。
- * <p>套餐=独立商品(product_type='bundle'),内容=套餐包含哪些子商品(精确到子商品SKU)×数量;
- * 内容挂套餐SKU级(combo_sku_id)。下单/支付只动套餐SKU自身库存,不扣子商品库存。
- * <p>幂等:CREATE TABLE IF NOT EXISTS + 异常兜底。
- */
- private void migrateProductComboItems() {
- try {
- jdbcTemplate.execute("CREATE TABLE IF NOT EXISTS `product_combo_items` ("
- + "`id` BIGINT NOT NULL AUTO_INCREMENT, "
- + "`combo_sku_id` BIGINT NOT NULL COMMENT '套餐SKU ID(product_skus.id),内容挂SKU级', "
- + "`child_product_id` BIGINT NOT NULL COMMENT '子商品ID', "
- + "`child_sku_id` BIGINT NOT NULL COMMENT '子商品SKU ID(精确到规格,必填)', "
- + "`quantity` INT NOT NULL DEFAULT 1 COMMENT '该子商品数量', "
- + "`created_at` DATETIME DEFAULT CURRENT_TIMESTAMP, "
- + "PRIMARY KEY (`id`), "
- + "UNIQUE KEY `uk_combo_sku_child` (`combo_sku_id`, `child_sku_id`) "
- + ") ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='套餐内容明细(套餐商品包含的子商品SKU×数量)'");
- log.info("迁移440: 已创建 product_combo_items 表");
- } catch (Exception e) {
- log.warn("迁移440: 创建 product_combo_items 表失败(可能已存在): {}", e.getMessage());
- }
- }
- private void migrateHealthDimensionsTable() {
- try {
- if (tableExists("health_dimension_score")) {
- jdbcTemplate.execute("RENAME TABLE `health_dimension_score` TO `health_dimension_score_old`");
- }
- } catch (Exception e) {
- log.warn("health_dimension_score rename skipped: {}", e.getMessage());
- }
- try {
- jdbcTemplate.execute("CREATE TABLE IF NOT EXISTS `health_dimension_scores` (\n" +
- " `id` bigint NOT NULL AUTO_INCREMENT,\n" +
- " `member_id` bigint NOT NULL COMMENT '家庭成员ID',\n" +
- " `dimension` varchar(20) NOT NULL COMMENT '维度: growth/sleep/vision/immunity/nutrition/gut/exercise',\n" +
- " `score` int NOT NULL COMMENT '0-100分',\n" +
- " `percentile` int DEFAULT NULL COMMENT '人群百分位 0-100',\n" +
- " `data_source` varchar(20) NOT NULL COMMENT '数据源: REPORT/PHOTO/VOICE/MANUAL',\n" +
- " `tier` tinyint NOT NULL DEFAULT 3 COMMENT '数据层级: 1自动/2语音/3手动',\n" +
- " `raw_data` json DEFAULT NULL COMMENT '原始数据快照',\n" +
- " `assess_date` date NOT NULL COMMENT '评估日期',\n" +
- " `expire_date` date DEFAULT NULL COMMENT '数据过期日',\n" +
- " `created_at` datetime DEFAULT CURRENT_TIMESTAMP,\n" +
- " `updated_at` datetime DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,\n" +
- " PRIMARY KEY (`id`),\n" +
- " UNIQUE KEY `uk_member_dim_date` (`member_id`, `dimension`, `assess_date`)\n" +
- ") ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='健康维度评分表'");
- } catch (Exception e) {
- log.warn("health_dimension_scores 表创建失败: {}", e.getMessage());
- }
- try {
- if (tableExists("health_data_source_record")) {
- jdbcTemplate.execute("RENAME TABLE `health_data_source_record` TO `health_data_source_record_old`");
- }
- } catch (Exception e) {
- log.warn("health_data_source_record rename skipped: {}", e.getMessage());
- }
- try {
- jdbcTemplate.execute("CREATE TABLE IF NOT EXISTS `health_data_sources` (\n" +
- " `id` bigint NOT NULL AUTO_INCREMENT,\n" +
- " `member_id` bigint NOT NULL,\n" +
- " `source_type` varchar(20) NOT NULL COMMENT 'PDF/PHOTO/VOICE/MANUAL/WEIXIN_STEP',\n" +
- " `dimension_codes` json NOT NULL COMMENT '影响的维度列表',\n" +
- " `parsed_result` json DEFAULT NULL COMMENT '解析结果全量',\n" +
- " `file_url` varchar(500) DEFAULT NULL COMMENT '原始文件URL',\n" +
- " `confidence` decimal(3,2) DEFAULT NULL COMMENT 'AI解析置信度',\n" +
- " `created_at` datetime DEFAULT CURRENT_TIMESTAMP,\n" +
- " PRIMARY KEY (`id`),\n" +
- " KEY `idx_member_source` (`member_id`, `source_type`)\n" +
- ") ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='健康数据源记录表'");
- } catch (Exception e) {
- log.warn("health_data_sources 表创建失败: {}", e.getMessage());
- }
- try {
- if (tableExists("health_norm_reference")) {
- jdbcTemplate.execute("RENAME TABLE `health_norm_reference` TO `health_norm_reference_old`");
- }
- } catch (Exception e) {
- log.warn("health_norm_reference rename skipped: {}", e.getMessage());
- }
- try {
- jdbcTemplate.execute("CREATE TABLE IF NOT EXISTS `health_norm_reference` (\n" +
- " `id` bigint NOT NULL AUTO_INCREMENT,\n" +
- " `dimension` varchar(20) NOT NULL COMMENT '维度',\n" +
- " `gender` varchar(10) NOT NULL COMMENT 'male/female/all',\n" +
- " `age_min` int NOT NULL COMMENT '年龄下限(月)',\n" +
- " `age_max` int NOT NULL COMMENT '年龄上限(月)',\n" +
- " `percentile_5` int DEFAULT NULL,\n" +
- " `percentile_15` int DEFAULT NULL,\n" +
- " `percentile_25` int DEFAULT NULL,\n" +
- " `percentile_50` int DEFAULT NULL COMMENT '中位值',\n" +
- " `percentile_75` int DEFAULT NULL,\n" +
- " `percentile_85` int DEFAULT NULL,\n" +
- " `percentile_95` int DEFAULT NULL,\n" +
- " `source` varchar(50) DEFAULT NULL COMMENT '常模来源',\n" +
- " `created_at` datetime DEFAULT CURRENT_TIMESTAMP,\n" +
- " PRIMARY KEY (`id`),\n" +
- " UNIQUE KEY `uk_dim_gender_age` (`dimension`, `gender`, `age_min`, `age_max`)\n" +
- ") ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='健康常模对照表'");
- } catch (Exception e) {
- log.warn("health_norm_reference 表重建失败: {}", e.getMessage());
- }
- try {
- jdbcTemplate.execute("INSERT IGNORE INTO health_dimension_scores (member_id, dimension, score, data_source, tier, assess_date, created_at)\n" +
- " SELECT member_id,\n" +
- " CASE dimension\n" +
- " WHEN 'body' THEN 'growth'\n" +
- " WHEN 'fitness' THEN 'exercise'\n" +
- " WHEN 'cardio' THEN 'exercise'\n" +
- " WHEN 'blood' THEN 'immunity'\n" +
- " WHEN 'sleep' THEN 'sleep'\n" +
- " WHEN 'psyche' THEN 'growth'\n" +
- " WHEN 'diet' THEN 'nutrition'\n" +
- " ELSE dimension\n" +
- " END,\n" +
- " score,\n" +
- " UPPER(data_source),\n" +
- " 3,\n" +
- " record_date,\n" +
- " NOW()\n" +
- " FROM health_dimension_score_old WHERE is_latest = 1");
- } catch (Exception e) {
- log.warn("旧维度评分数据迁移跳过: {}", e.getMessage());
- }
- }
- private void seedHealthNorms() {
- try {
- int count = jdbcTemplate.queryForObject("SELECT COUNT(*) FROM health_norm_reference", Integer.class);
- if (count == 0) {
- jdbcTemplate.execute("INSERT INTO health_norm_reference (dimension, gender, age_min, age_max, percentile_5, percentile_15, percentile_25, percentile_50, percentile_75, percentile_85, percentile_95, source) VALUES\n" +
- "('growth', 'male', 72, 84, 25, 35, 45, 55, 70, 80, 90, 'BMI-for-age→百分制'),\n" +
- "('growth', 'female', 72, 84, 25, 35, 45, 55, 70, 80, 90, 'BMI-for-age→百分制'),\n" +
- "('growth', 'male', 84, 96, 25, 35, 45, 55, 70, 80, 90, 'BMI-for-age→百分制'),\n" +
- "('growth', 'female', 84, 96, 25, 35, 45, 55, 70, 80, 90, 'BMI-for-age→百分制'),\n" +
- "('growth', 'male', 96, 120, 25, 35, 45, 55, 70, 80, 90, 'BMI-for-age→百分制'),\n" +
- "('growth', 'female', 96, 120, 25, 35, 45, 55, 70, 80, 90, 'BMI-for-age→百分制'),\n" +
- "('sleep', 'all', 0, 12, 40, 50, 60, 70, 80, 88, 95, 'NSF婴幼儿睡眠→百分制'),\n" +
- "('sleep', 'all', 12, 36, 40, 50, 60, 70, 80, 88, 95, 'NSF幼儿睡眠→百分制'),\n" +
- "('sleep', 'all', 36, 72, 40, 50, 60, 70, 80, 88, 95, 'NSF学龄前睡眠→百分制'),\n" +
- "('sleep', 'all', 72, 156, 40, 50, 60, 70, 80, 88, 95, 'NSF学龄儿童睡眠→百分制'),\n" +
- "('vision', 'male', 72, 144, 45, 55, 65, 75, 85, 90, 95, '视力发育标准→百分制'),\n" +
- "('vision', 'female', 72, 144, 45, 55, 65, 75, 85, 90, 95, '视力发育标准→百分制'),\n" +
- "('immunity', 'all', 0, 72, 15, 30, 45, 65, 80, 90, 95, '年患病次数→百分制'),\n" +
- "('immunity', 'all', 72, 192, 15, 35, 50, 70, 85, 92, 98, '年患病次数→百分制'),\n" +
- "('nutrition', 'all', 36, 192, 20, 30, 40, 50, 60, 70, 80, '膳食多样性→百分制'),\n" +
- "('gut', 'all', 0, 192, 40, 50, 60, 70, 80, 85, 95, '菌群人群分布(平台自建)'),\n" +
- "('exercise', 'male', 72, 144, 30, 40, 50, 60, 70, 80, 90, '国家学生体质健康标准'),\n" +
- "('exercise', 'female', 72, 144, 25, 35, 45, 55, 65, 75, 85, '国家学生体质健康标准')");
- log.info("health_norm_reference 常模种子数据已加载 (18条, 统一0-100百分制)");
- } else {
- log.info("health_norm_reference 常模数据已存在 ({}条), 跳过", count);
- }
- } catch (Exception e) {
- log.warn("health_norm_reference 常模种子数据初始化失败: {}", e.getMessage());
- }
- // 迁移: articles表添加审核和作者类型字段
- try {
- Integer exists = jdbcTemplate.queryForObject(
- "SELECT COUNT(*) FROM information_schema.COLUMNS WHERE TABLE_SCHEMA = DATABASE() AND TABLE_NAME = 'articles' AND COLUMN_NAME = 'audit_status'",
- Integer.class);
- if (exists == null || exists == 0) {
- jdbcTemplate.execute("ALTER TABLE articles " +
- "ADD COLUMN author_type VARCHAR(20) DEFAULT 'admin' COMMENT '作者类型:admin/parent/child' AFTER created_by, " +
- "ADD COLUMN audit_status VARCHAR(20) DEFAULT 'approved' COMMENT '审核状态:approved/pending/rejected' AFTER status, " +
- "ADD COLUMN audit_reason TEXT COMMENT '驳回原因' AFTER audit_status, " +
- "ADD COLUMN auditor_id BIGINT DEFAULT 0 COMMENT '审核人 ID' AFTER audit_reason, " +
- "ADD COLUMN audited_at DATETIME COMMENT '审核时间' AFTER auditor_id");
- log.info("已添加审核和作者类型字段到articles表");
- } else {
- log.info("articles表审核字段已存在,跳过");
- }
- } catch (Exception e) {
- log.warn("检查/添加articles表审核字段失败: {}", e.getMessage());
- }
- // 迁移20240709-01: content字段扩为TEXT - 用户输入内容可能超VARCHAR上限
- // activities.description
- modifyColumnToText("activities", "description", "活动描述");
- // tasks.description
- modifyColumnToText("tasks", "description", "任务描述");
- // wishes.description, reject_reason, cancel_reason
- modifyColumnToText("wishes", "description", "心愿描述");
- modifyColumnToText("wishes", "reject_reason", "拒绝原因");
- modifyColumnToText("wishes", "cancel_reason", "取消原因");
- // reward_wishlists.description
- modifyColumnToText("reward_wishlists", "description", "心愿描述");
- // products.description, reject_reason
- modifyColumnToText("products", "description", "商品描述");
- modifyColumnToText("products", "reject_reason", "审核拒绝原因");
- // product_orders.remark
- modifyColumnToText("product_orders", "remark", "备注");
- // articles.summary
- modifyColumnToText("articles", "summary", "摘要");
- // activities.audit_reason
- modifyColumnToText("activities", "audit_reason", "驳回原因");
- // guide_applications.reject_reason
- modifyColumnToText("guide_applications", "reject_reason", "拒绝原因");
- // guide_application_records.remark
- modifyColumnToText("guide_application_records", "remark", "备注/原因");
- // user_operation_log.description
- modifyColumnToText("user_operation_log", "description", "操作描述");
- // follow_up.description, teacher_review
- modifyColumnToText("follow_up", "description", "描述");
- modifyColumnToText("follow_up", "teacher_review", "规划师点评");
- // energy_log.description
- modifyColumnToText("energy_log", "description", "描述");
- // sys_config.description
- modifyColumnToText("sys_config", "description", "配置说明");
- // points_log.description
- modifyColumnToText("points_log", "description", "描述");
- // insurance_policies.remark
- modifyColumnToText("insurance_policies", "remark", "备注");
- // health_reports.remark
- modifyColumnToText("health_reports", "remark", "备注");
- // guide_service_logs.notes
- modifyColumnToText("guide_service_logs", "notes", "服务记录");
- // guide_packages.description
- modifyColumnToText("guide_packages", "description", "套餐说明");
- // guide_package_templates.description
- modifyColumnToText("guide_package_templates", "description", "说明");
- // service_types.description
- modifyColumnToText("service_types", "description", "描述");
- // commission_records.remark
- modifyColumnToText("commission_records", "remark", "备注");
- // zodiac_pairing.description, advice
- modifyColumnToText("zodiac_pairing", "description", "配对解读");
- modifyColumnToText("zodiac_pairing", "advice", "相处建议");
- // blood_type_pairing.description, advice
- modifyColumnToText("blood_type_pairing", "description", "配对描述");
- modifyColumnToText("blood_type_pairing", "advice", "相处建议");
- // life_number_relationship.description, advice
- modifyColumnToText("life_number_relationship", "description", "关系解读");
- modifyColumnToText("life_number_relationship", "advice", "相处建议");
- // daily_health_checkin.remark
- modifyColumnToText("daily_health_checkin", "remark", "备注");
- // health_sleep_record.remark
- modifyColumnToText("health_sleep_record", "remark", "备注");
- // health_exercise_record.remark
- modifyColumnToText("health_exercise_record", "remark", "备注");
- // finance_checkins.note
- modifyColumnToText("finance_checkins", "note", "备注");
- // guide_packages.description (old duplicate)
- // assessment_appointments.remark
- modifyColumnToText("assessment_appointments", "remark", "备注");
- // withdrawal_requests.remark
- modifyColumnToText("withdrawal_requests", "remark", "备注");
- // supply_settlements.remark
- modifyColumnToText("supply_settlements", "remark", "备注");
- // content_sections.description
- ensureColumn("content_sections", "description", "TEXT COMMENT '描述说明'");
- // education_systems.description
- modifyColumnToText("education_systems", "description", "描述");
- // dimension_config.description
- modifyColumnToText("dimension_config", "description", "描述");
- // energy_rules.description
- modifyColumnToText("energy_rules", "description", "规则描述");
- // team_goals.description
- modifyColumnToText("team_goals", "description", "团队描述");
- // product_dimension_config.remark
- modifyColumnToText("product_dimension_config", "remark", "备注");
- // indicator_values.remark
- modifyColumnToText("indicator_values", "remark", "备注");
- // tongue_records.overall_assessment
- modifyColumnToText("tongue_records", "overall_assessment", "总体评估");
- // system_config.description, config_value
- modifyColumnToText("system_config", "description", "说明");
- modifyColumnToText("system_config", "config_value", "配置值");
- // supply_system.description
- modifyColumnToText("supply_system", "description", "描述");
- // supply_settlement.remark
- modifyColumnToText("supply_settlement", "remark", "备注");
- // growth_task.description
- modifyColumnToText("growth_task", "description", "描述");
- // distribution_systems.description
- modifyColumnToText("distribution_systems", "description", "团队描述");
- // mini_games.description
- modifyColumnToText("mini_games", "description", "游戏描述");
- // blood_type_config.description
- modifyColumnToText("blood_type_config", "description", "性格描述");
- // 迁移20240709-02: 北京菌群报告采集增强(P0 疾病关联菌 / P1 菌群状态)
- // health_disease_risk.related_bacteria: 疾病关联菌(异常菌)明细 JSON
- ensureColumn("health_disease_risk", "related_bacteria", "TEXT NULL COMMENT '关联菌明细JSON:[{bacteria,correlation,value,status}]'");
- // health_reports.gut_status_level: 肠道菌群状态(0度~4度)
- ensureColumn("health_reports", "gut_status_level", "VARCHAR(20) NULL COMMENT '肠道菌群状态(0度~4度)'");
- // 迁移248: 任务系统升级(行为调度中心)——tasks 新增字段
- ensureColumn("tasks", "prerequisite_task_id", "BIGINT NULL COMMENT '前置任务ID:完成后才解锁本任务'");
- ensureColumn("tasks", "action_type", "VARCHAR(32) NULL COMMENT '联动动作: minigame/focus/survey/article/activity/cart/order'");
- ensureColumn("tasks", "action_config", "JSON NULL COMMENT '联动参数: {\"gameCode\":\"schulte\",\"productId\":123,\"minSeconds\":30,\"completeMilestone\":\"paid\"/\"received\",\"surveyId\":99}'");
- ensureColumn("tasks", "require_input", "TINYINT DEFAULT 0 COMMENT '1=需填写内容才能完成'");
- ensureColumn("tasks", "start_required", "TINYINT DEFAULT 0 COMMENT '1=需先开始再完成(两阶段)'");
- ensureColumn("tasks", "start_count", "INT DEFAULT 0 COMMENT '已开始次数(累计)'");
- ensureColumn("tasks", "min_duration_seconds", "INT DEFAULT 0 COMMENT '最短持续秒数(专注/游戏类,未达标不计完成)'");
- // 迁移248: 任务执行进度表(两阶段/里程碑/填写内容记录)
- try {
- jdbcTemplate.execute("CREATE TABLE IF NOT EXISTS task_executions (" +
- "id BIGINT AUTO_INCREMENT PRIMARY KEY, " +
- "task_id BIGINT NOT NULL COMMENT '任务实例ID', " +
- "member_id BIGINT NOT NULL COMMENT '执行成员ID(family_members.id)', " +
- "status VARCHAR(20) NOT NULL DEFAULT 'started' COMMENT 'started/finished/failed', " +
- "started_at DATETIME NOT NULL COMMENT '开始时间', " +
- "finished_at DATETIME COMMENT '完成时间', " +
- "milestone VARCHAR(32) COMMENT '购物任务里程碑: cart_added/paid/received(当前进度)', " +
- "content TEXT COMMENT '填写内容(require_input 任务)', " +
- "content_type VARCHAR(20) COMMENT 'content 类型: text/image/audio/video', " +
- "extra JSON COMMENT '扩展数据: {gameScore, focusMinutes, orderId, articleId, surveyId}', " +
- "created_at DATETIME DEFAULT CURRENT_TIMESTAMP, " +
- "INDEX idx_task_member (task_id, member_id), " +
- "INDEX idx_status (status)" +
- ") ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='任务执行进度表'");
- log.info("已创建task_executions表");
- } catch (Exception e) {
- log.warn("创建task_executions表失败: {}", e.getMessage());
- }
- // 迁移248: focus_sessions 添加 task_id 字段(专注任务挂钩)
- ensureColumn("focus_sessions", "task_id", "BIGINT NULL COMMENT '关联的任务ID(专注任务挂钩)'");
- }
- private void seedMindActionWealthNorms() {
- try {
- Integer count = jdbcTemplate.queryForObject(
- "SELECT COUNT(*) FROM health_norm_reference WHERE dimension LIKE 'bigFive%' OR dimension LIKE 'emotion%' OR dimension LIKE 'drive%' OR dimension LIKE 'selfConcept%' OR dimension LIKE 'action%' OR dimension LIKE 'wealth%'",
- Integer.class);
- if (count != null && count > 0) {
- log.info("mind/action/wealth 常模数据已存在 ({}条), 跳过", count);
- return;
- }
- java.util.List<String> sqls = new java.util.ArrayList<>();
- sqls.add("INSERT IGNORE INTO health_norm_reference (dimension, gender, age_min, age_max, percentile_5, percentile_15, percentile_25, percentile_50, percentile_75, percentile_85, percentile_95, source) VALUES \n" +
- "('bigFiveOpenness','all',0,84,20,27,35,45,55,63,70,'心理常模'),\n" +
- "('bigFiveOpenness','all',84,144,27,34,42,52,62,70,77,'心理常模'),\n" +
- "('bigFiveOpenness','all',144,216,30,37,45,55,65,73,80,'心理常模'),\n" +
- "('bigFiveOpenness','all',216,1200,25,32,40,50,60,68,75,'心理常模')");
- sqls.add("INSERT IGNORE INTO health_norm_reference (dimension, gender, age_min, age_max, percentile_5, percentile_15, percentile_25, percentile_50, percentile_75, percentile_85, percentile_95, source) VALUES \n" +
- "('bigFiveConscientiousness','all',0,84,15,22,30,40,50,58,65,'心理常模'),\n" +
- "('bigFiveConscientiousness','all',84,144,23,30,38,48,58,66,73,'心理常模'),\n" +
- "('bigFiveConscientiousness','all',144,216,30,37,45,55,65,73,80,'心理常模'),\n" +
- "('bigFiveConscientiousness','all',216,1200,33,40,48,58,68,76,83,'心理常模')");
- sqls.add("INSERT IGNORE INTO health_norm_reference (dimension, gender, age_min, age_max, percentile_5, percentile_15, percentile_25, percentile_50, percentile_75, percentile_85, percentile_95, source) VALUES \n" +
- "('bigFiveExtraversion','all',0,84,30,37,45,55,65,73,80,'心理常模'),\n" +
- "('bigFiveExtraversion','all',84,144,27,34,42,52,62,70,77,'心理常模'),\n" +
- "('bigFiveExtraversion','all',144,216,25,32,40,50,60,68,75,'心理常模'),\n" +
- "('bigFiveExtraversion','all',216,1200,25,32,40,50,60,68,75,'心理常模')");
- sqls.add("INSERT IGNORE INTO health_norm_reference (dimension, gender, age_min, age_max, percentile_5, percentile_15, percentile_25, percentile_50, percentile_75, percentile_85, percentile_95, source) VALUES \n" +
- "('bigFiveAgreeableness','all',0,84,33,40,48,58,68,76,83,'心理常模'),\n" +
- "('bigFiveAgreeableness','all',84,144,30,37,45,55,65,73,80,'心理常模'),\n" +
- "('bigFiveAgreeableness','all',144,216,28,35,43,53,63,71,78,'心理常模'),\n" +
- "('bigFiveAgreeableness','all',216,1200,30,37,45,55,65,73,80,'心理常模')");
- sqls.add("INSERT IGNORE INTO health_norm_reference (dimension, gender, age_min, age_max, percentile_5, percentile_15, percentile_25, percentile_50, percentile_75, percentile_85, percentile_95, source) VALUES \n" +
- "('bigFiveNeuroticism','all',0,84,30,37,45,55,65,73,80,'心理常模'),\n" +
- "('bigFiveNeuroticism','all',84,144,23,30,38,48,58,66,73,'心理常模'),\n" +
- "('bigFiveNeuroticism','all',144,216,20,27,35,45,55,63,70,'心理常模'),\n" +
- "('bigFiveNeuroticism','all',216,1200,30,37,45,55,65,73,80,'心理常模')");
- sqls.add("INSERT IGNORE INTO health_norm_reference (dimension, gender, age_min, age_max, percentile_5, percentile_15, percentile_25, percentile_50, percentile_75, percentile_85, percentile_95, source) VALUES \n" +
- "('emotionStability','all',0,84,17,24,32,42,52,60,67,'心理常模'),\n" +
- "('emotionStability','all',84,144,27,34,42,52,62,70,77,'心理常模'),\n" +
- "('emotionStability','all',144,216,33,40,48,58,68,76,83,'心理常模'),\n" +
- "('emotionStability','all',216,1200,37,44,52,62,72,80,87,'心理常模')");
- sqls.add("INSERT IGNORE INTO health_norm_reference (dimension, gender, age_min, age_max, percentile_5, percentile_15, percentile_25, percentile_50, percentile_75, percentile_85, percentile_95, source) VALUES \n" +
- "('driveAutonomy','all',0,84,10,17,25,35,45,53,60,'心理常模'),\n" +
- "('driveAutonomy','all',84,144,25,32,40,50,60,68,75,'心理常模'),\n" +
- "('driveAutonomy','all',144,216,37,44,52,62,72,80,87,'心理常模'),\n" +
- "('driveAutonomy','all',216,1200,43,50,58,68,78,86,93,'心理常模')");
- sqls.add("INSERT IGNORE INTO health_norm_reference (dimension, gender, age_min, age_max, percentile_5, percentile_15, percentile_25, percentile_50, percentile_75, percentile_85, percentile_95, source) VALUES \n" +
- "('driveCompetence','all',0,84,15,22,30,40,50,58,65,'心理常模'),\n" +
- "('driveCompetence','all',84,144,27,34,42,52,62,70,77,'心理常模'),\n" +
- "('driveCompetence','all',144,216,35,42,50,60,70,78,85,'心理常模'),\n" +
- "('driveCompetence','all',216,1200,37,44,52,62,72,80,87,'心理常模')");
- sqls.add("INSERT IGNORE INTO health_norm_reference (dimension, gender, age_min, age_max, percentile_5, percentile_15, percentile_25, percentile_50, percentile_75, percentile_85, percentile_95, source) VALUES \n" +
- "('driveRelatedness','all',0,84,30,37,45,55,65,73,80,'心理常模'),\n" +
- "('driveRelatedness','all',84,144,27,34,42,52,62,70,77,'心理常模'),\n" +
- "('driveRelatedness','all',144,216,25,32,40,50,60,68,75,'心理常模'),\n" +
- "('driveRelatedness','all',216,1200,30,37,45,55,65,73,80,'心理常模')");
- sqls.add("INSERT IGNORE INTO health_norm_reference (dimension, gender, age_min, age_max, percentile_5, percentile_15, percentile_25, percentile_50, percentile_75, percentile_85, percentile_95, source) VALUES \n" +
- "('selfConceptOverall','all',0,84,20,27,35,45,55,63,70,'心理常模'),\n" +
- "('selfConceptOverall','all',84,144,30,37,45,55,65,73,80,'心理常模'),\n" +
- "('selfConceptOverall','all',144,216,33,40,48,58,68,76,83,'心理常模'),\n" +
- "('selfConceptOverall','all',216,1200,33,40,48,58,68,76,83,'心理常模')");
- sqls.add("INSERT IGNORE INTO health_norm_reference (dimension, gender, age_min, age_max, percentile_5, percentile_15, percentile_25, percentile_50, percentile_75, percentile_85, percentile_95, source) VALUES \n" +
- "('actionChildParentChild','all',0,84,35,42,50,60,70,78,85,'关系质量常模'),\n" +
- "('actionChildParentChild','all',84,144,33,40,48,58,68,76,83,'关系质量常模'),\n" +
- "('actionChildParentChild','all',144,216,30,37,45,55,65,73,80,'关系质量常模')");
- sqls.add("INSERT IGNORE INTO health_norm_reference (dimension, gender, age_min, age_max, percentile_5, percentile_15, percentile_25, percentile_50, percentile_75, percentile_85, percentile_95, source) VALUES \n" +
- "('actionSibling','all',0,84,20,27,35,45,55,63,70,'关系质量常模'),\n" +
- "('actionSibling','all',84,144,25,32,40,50,60,68,75,'关系质量常模'),\n" +
- "('actionSibling','all',144,216,30,37,45,55,65,73,80,'关系质量常模')");
- sqls.add("INSERT IGNORE INTO health_norm_reference (dimension, gender, age_min, age_max, percentile_5, percentile_15, percentile_25, percentile_50, percentile_75, percentile_85, percentile_95, source) VALUES \n" +
- "('actionRelative','all',0,84,25,32,40,50,60,68,75,'关系质量常模'),\n" +
- "('actionRelative','all',84,144,30,37,45,55,65,73,80,'关系质量常模'),\n" +
- "('actionRelative','all',144,216,33,40,48,58,68,76,83,'关系质量常模')");
- sqls.add("INSERT IGNORE INTO health_norm_reference (dimension, gender, age_min, age_max, percentile_5, percentile_15, percentile_25, percentile_50, percentile_75, percentile_85, percentile_95, source) VALUES \n" +
- "('actionTeacherStudent','all',0,84,25,32,40,50,60,68,75,'关系质量常模'),\n" +
- "('actionTeacherStudent','all',84,144,35,42,50,60,70,78,85,'关系质量常模'),\n" +
- "('actionTeacherStudent','all',144,216,37,44,52,62,72,80,87,'关系质量常模')");
- sqls.add("INSERT IGNORE INTO health_norm_reference (dimension, gender, age_min, age_max, percentile_5, percentile_15, percentile_25, percentile_50, percentile_75, percentile_85, percentile_95, source) VALUES \n" +
- "('actionChildSociality','all',0,84,23,30,38,48,58,66,73,'关系质量常模'),\n" +
- "('actionChildSociality','all',84,144,30,37,45,55,65,73,80,'关系质量常模'),\n" +
- "('actionChildSociality','all',144,216,35,42,50,60,70,78,85,'关系质量常模')");
- sqls.add("INSERT IGNORE INTO health_norm_reference (dimension, gender, age_min, age_max, percentile_5, percentile_15, percentile_25, percentile_50, percentile_75, percentile_85, percentile_95, source) VALUES \n" +
- "('actionChildInterpersonal','all',0,84,20,27,35,45,55,63,70,'关系质量常模'),\n" +
- "('actionChildInterpersonal','all',84,144,27,34,42,52,62,70,77,'关系质量常模'),\n" +
- "('actionChildInterpersonal','all',144,216,33,40,48,58,68,76,83,'关系质量常模')");
- sqls.add("INSERT IGNORE INTO health_norm_reference (dimension, gender, age_min, age_max, percentile_5, percentile_15, percentile_25, percentile_50, percentile_75, percentile_85, percentile_95, source) VALUES \n" +
- "('actionParentChild','all',216,1200,33,40,48,58,68,76,83,'关系质量常模')");
- sqls.add("INSERT IGNORE INTO health_norm_reference (dimension, gender, age_min, age_max, percentile_5, percentile_15, percentile_25, percentile_50, percentile_75, percentile_85, percentile_95, source) VALUES \n" +
- "('actionMarital','all',216,1200,30,37,45,55,65,73,80,'关系质量常模')");
- sqls.add("INSERT IGNORE INTO health_norm_reference (dimension, gender, age_min, age_max, percentile_5, percentile_15, percentile_25, percentile_50, percentile_75, percentile_85, percentile_95, source) VALUES \n" +
- "('actionParental','all',216,1200,27,34,42,52,62,70,77,'关系质量常模')");
- sqls.add("INSERT IGNORE INTO health_norm_reference (dimension, gender, age_min, age_max, percentile_5, percentile_15, percentile_25, percentile_50, percentile_75, percentile_85, percentile_95, source) VALUES \n" +
- "('actionLeadership','all',216,1200,30,37,45,55,65,73,80,'关系质量常模')");
- sqls.add("INSERT IGNORE INTO health_norm_reference (dimension, gender, age_min, age_max, percentile_5, percentile_15, percentile_25, percentile_50, percentile_75, percentile_85, percentile_95, source) VALUES \n" +
- "('actionSociality','all',216,1200,35,42,50,60,70,78,85,'关系质量常模')");
- sqls.add("INSERT IGNORE INTO health_norm_reference (dimension, gender, age_min, age_max, percentile_5, percentile_15, percentile_25, percentile_50, percentile_75, percentile_85, percentile_95, source) VALUES \n" +
- "('actionInterpersonal','all',216,1200,33,40,48,58,68,76,83,'关系质量常模')");
- sqls.add("INSERT IGNORE INTO health_norm_reference (dimension, gender, age_min, age_max, percentile_5, percentile_15, percentile_25, percentile_50, percentile_75, percentile_85, percentile_95, source) VALUES \n" +
- "('wealthSkill','all',0,84,10,17,25,35,45,53,60,'财富素养常模'),\n" +
- "('wealthSkill','all',84,144,20,27,35,45,55,63,70,'财富素养常模'),\n" +
- "('wealthSkill','all',144,216,30,37,45,55,65,73,80,'财富素养常模'),\n" +
- "('wealthSkill','all',216,1200,35,42,50,60,70,78,85,'财富素养常模')");
- sqls.add("INSERT IGNORE INTO health_norm_reference (dimension, gender, age_min, age_max, percentile_5, percentile_15, percentile_25, percentile_50, percentile_75, percentile_85, percentile_95, source) VALUES \n" +
- "('wealthAltruism','all',0,84,5,12,20,30,40,48,55,'财富素养常模'),\n" +
- "('wealthAltruism','all',84,144,15,22,30,40,50,58,65,'财富素养常模'),\n" +
- "('wealthAltruism','all',144,216,25,32,40,50,60,68,75,'财富素养常模'),\n" +
- "('wealthAltruism','all',216,1200,30,37,45,55,65,73,80,'财富素养常模')");
- sqls.add("INSERT IGNORE INTO health_norm_reference (dimension, gender, age_min, age_max, percentile_5, percentile_15, percentile_25, percentile_50, percentile_75, percentile_85, percentile_95, source) VALUES \n" +
- "('wealthIncome','all',0,84,0,7,15,25,35,43,50,'财富素养常模'),\n" +
- "('wealthIncome','all',84,144,10,17,25,35,45,53,60,'财富素养常模'),\n" +
- "('wealthIncome','all',144,216,20,27,35,45,55,63,70,'财富素养常模'),\n" +
- "('wealthIncome','all',216,1200,30,37,45,55,65,73,80,'财富素养常模')");
- sqls.add("INSERT IGNORE INTO health_norm_reference (dimension, gender, age_min, age_max, percentile_5, percentile_15, percentile_25, percentile_50, percentile_75, percentile_85, percentile_95, source) VALUES \n" +
- "('wealthFx','all',0,84,0,2,10,20,30,38,45,'财富素养常模'),\n" +
- "('wealthFx','all',84,144,5,12,20,30,40,48,55,'财富素养常模'),\n" +
- "('wealthFx','all',144,216,15,22,30,40,50,58,65,'财富素养常模'),\n" +
- "('wealthFx','all',216,1200,25,32,40,50,60,68,75,'财富素养常模')");
- for (String sql : sqls) {
- jdbcTemplate.execute(sql);
- }
- log.info("mind/action/wealth 常模种子数据已加载 ({}条)", sqls.size());
- } catch (Exception e) {
- log.warn("mind/action/wealth 常模种子数据初始化失败: {}", e.getMessage());
- }
- }
- private void modifyColumnToText(String table, String column, String comment) {
- if (!tableExists(table)) {
- return;
- }
- try {
- jdbcTemplate.execute("ALTER TABLE " + table + " MODIFY COLUMN " + column + " TEXT COMMENT '" + comment + "'");
- log.info("已修改{}.{}为TEXT类型", table, column);
- } catch (Exception e) {
- log.warn("修改{}.{}为TEXT失败: {}", table, column, e.getMessage());
- }
- }
- private void initializeDefaultData() {
- // 初始化预设奖励模板
- try {
- // 检查是否已有预设奖励
- Integer count = jdbcTemplate.queryForObject(
- "SELECT COUNT(*) FROM rewards WHERE is_template = 1", Integer.class);
- if (count == null || count == 0) {
- // 插入预设奖励模板(child_id为NULL表示模板)
- jdbcTemplate.execute("INSERT INTO rewards (family_id, child_id, title, points_required, category, is_template) VALUES " +
- "(0, NULL, \'看动画片30分钟\', 30, \'精神类\', 1), " +
- "(0, NULL, \'玩平板游戏30分钟\', 40, \'特权类\', 1), " +
- "(0, NULL, \'去游乐场\', 100, \'物质类\', 1), " +
- "(0, NULL, \'买玩具\', 150, \'物质类\', 1), " +
- "(0, NULL, \'吃冰淇淋\', 20, \'物质类\', 1), " +
- "(0, NULL, \'选择晚餐\', 25, \'特权类\', 1)");
- log.info("预设奖励模板已初始化");
- }
- } catch (Exception e) {
- log.warn("初始化预设奖励失败: {}", e.getMessage());
- }
- // 初始化奖励分类
- try {
- Integer count = jdbcTemplate.queryForObject(
- "SELECT COUNT(*) FROM reward_categories WHERE family_id = 0", Integer.class);
- if (count == null || count == 0) {
- jdbcTemplate.execute("INSERT INTO reward_categories (family_id, name, icon, sort_order) VALUES " +
- "(0, '物质类', 'gift', 1), " +
- "(0, '精神类', 'star', 2), " +
- "(0, '特权类', 'crown', 3)");
- log.info("奖励分类已初始化");
- }
- } catch (Exception e) {
- log.warn("初始化奖励分类失败: {}", e.getMessage());
- }
- // 初始化打卡里程碑
- try {
- Integer count = jdbcTemplate.queryForObject(
- "SELECT COUNT(*) FROM streak_milestones", Integer.class);
- if (count == null || count == 0) {
- jdbcTemplate.execute("INSERT INTO streak_milestones (days, reward_points) VALUES " +
- "(3, 5), (7, 10), (30, 30), (100, 100)");
- log.info("打卡里程碑已初始化");
- }
- } catch (Exception e) {
- log.warn("初始化打卡里程碑失败: {}", e.getMessage());
- }
- // 初始化勋章定义
- try {
- Integer count = jdbcTemplate.queryForObject(
- "SELECT COUNT(*) FROM badge_definitions", Integer.class);
- if (count == null || count == 0) {
- jdbcTemplate.execute("INSERT INTO badge_definitions (badge_type, name, description, icon, condition_type, condition_value, reward_points) VALUES " +
- "('first_task', '初次完成任务', '首次完成任意任务', 'star', 'task_count', 1, 0), " +
- "('streak_3', '连续3天', '连续3天按时完成任务', 'fire', 'streak_days', 3, 5), " +
- "('streak_7', '连续7天', '连续7天按时完成任务', 'fire', 'streak_days', 7, 10), " +
- "('points_50', '积分达人', '一周获得50积分', 'trophy', 'weekly_points', 50, 0), " +
- "('task_100', '任务达人', '完成100个任务', 'medal', 'task_count', 100, 20)");
- log.info("勋章定义已初始化");
- }
- } catch (Exception e) {
- log.warn("初始化勋章定义失败: {}", e.getMessage());
- }
- // 初始化小游戏数据
- try {
- Integer count = jdbcTemplate.queryForObject(
- "SELECT COUNT(*) FROM mini_games", Integer.class);
- if (count == null || count == 0) {
- jdbcTemplate.execute("INSERT INTO mini_games (game_code, game_name, description, icon, default_duration, default_points, game_url, sort_order, status) VALUES " +
- "('schulte', '舒尔特方格', '训练专注力和眼动能力,找到1-25按顺序点击', '🎯', 5, 5, '/pages/games/schulte', 1, 1), " +
- "('1a2b', '猜数字', '猜对数字和位置获得积分,锻炼逻辑思维', '🔢', 10, 5, '/pages/games/1a2b', 2, 1), " +
- "('sudoku', '数独', '填充数字使每行每列每个宫格都包含1-9', '🔳', 15, 8, '/pages/games/sudoku', 3, 1)");
- log.info("小游戏数据已初始化");
- }
- } catch (Exception e) {
- log.warn("初始化小游戏数据失败: {}", e.getMessage());
- }
- // 初始化默认管理员
- try {
- // 先检查并添加role列(如果不存在)
- try {
- jdbcTemplate.execute("ALTER TABLE admins ADD COLUMN role VARCHAR(20) DEFAULT \'admin\' AFTER email");
- log.info("已添加role列到admins表");
- } catch (Exception e) {
- // 列已存在,忽略错误
- }
- Integer count = jdbcTemplate.queryForObject(
- "SELECT COUNT(*) FROM admins", Integer.class);
- if (count == null || count == 0) {
- // 默认管理员: 手机号 13800138000, 密码 admin123
- String encryptedPassword = org.springframework.util.DigestUtils.md5DigestAsHex("xzyj_admin_salt_admin123".getBytes());
- jdbcTemplate.execute("INSERT INTO admins (username, password, real_name, phone, email, role, status) VALUES " +
- "(\'admin\', \'" + encryptedPassword + "\', \'系统管理员\', \'13800138000\', \'admin@xzyj.com\', \'admin\', 1)");
- log.info("默认管理员已初始化: 手机号 13800138000, 密码 admin123");
- }
- } catch (Exception e) {
- log.warn("初始化默认管理员失败: {}", e.getMessage());
- }
- // 初始化成长规划师WEB管理账号
- try {
- Integer teacherCount = jdbcTemplate.queryForObject(
- "SELECT COUNT(*) FROM admins WHERE username = 'teacher'", Integer.class);
- if (teacherCount == null || teacherCount == 0) {
- String teacherPassword = org.springframework.util.DigestUtils.md5DigestAsHex("xzyj_admin_salt_teacher123".getBytes());
- jdbcTemplate.execute("INSERT INTO admins (username, password, real_name, phone, role, status) VALUES " +
- "('teacher', '" + teacherPassword + "', '成长规划师', '13800138003', 'teacher', 1)");
- log.info("成长规划师管理账号已初始化: 用户名 teacher, 密码 teacher123");
- }
- } catch (Exception e) {
- log.warn("初始化成长规划师管理账号失败: {}", e.getMessage());
- }
- // 初始化任务模板示例数据
- try {
- Integer count = jdbcTemplate.queryForObject(
- "SELECT COUNT(*) FROM admin_task_templates", Integer.class);
- if (count == null || count == 0) {
- jdbcTemplate.execute("INSERT INTO admin_task_templates (title, description, points, category, need_review, recommended_age, difficulty, is_active) VALUES " +
- "('完成作业', '按时完成学校布置的家庭作业', 10, '学习', 1, 6, 'medium', 1), " +
- "('阅读30分钟', '自主阅读课外书籍30分钟', 8, '阅读', 0, 5, 'easy', 1), " +
- "('整理房间', '收拾整理自己的房间和书桌', 6, '家务', 0, 6, 'easy', 1), " +
- "('洗碗', '饭后帮忙清洗餐具', 5, '家务', 1, 8, 'medium', 1), " +
- "('户外运动1小时', '进行跑步、球类等户外运动', 12, '运动', 0, 7, 'hard', 1), " +
- "('练习书法', '练习毛笔或硬笔书法20分钟', 7, '学习', 0, 7, 'medium', 1), " +
- "('早睡早起', '晚上9点前睡觉,早上7点前起床', 5, '生活习惯', 0, 5, 'easy', 1), " +
- "('背诵古诗', '背诵一首古诗词', 8, '学习', 1, 6, 'medium', 1)");
- log.info("任务模板示例数据已初始化");
- }
- } catch (Exception e) {
- log.warn("初始化任务模板失败: {}", e.getMessage());
- }
- // 初始化街道数据示例(广东省广州市)
- try {
- Integer count = jdbcTemplate.queryForObject(
- "SELECT COUNT(*) FROM streets", Integer.class);
- if (count == null || count == 0) {
- // 省
- jdbcTemplate.execute("INSERT INTO streets (province, province_code, full_name, level, parent_id, sort_order) VALUES " +
- "('广东省', '44', '广东省', 1, NULL, 1)");
- // 市
- jdbcTemplate.execute("INSERT INTO streets (province, province_code, city, city_code, full_name, level, parent_id, sort_order) VALUES " +
- "('广东省', '44', '广州市', '4401', '广东省广州市', 2, 1, 1), " +
- "('广东省', '44', '深圳市', '4403', '广东省深圳市', 2, 1, 2), " +
- "('广东省', '44', '珠海市', '4404', '广东省珠海市', 2, 1, 3)");
- // 区
- jdbcTemplate.execute("INSERT INTO streets (province, province_code, city, city_code, district, district_code, full_name, level, parent_id, sort_order) VALUES " +
- "('广东省', '44', '广州市', '4401', '天河区', '440106', '广东省广州市天河区', 3, 2, 1), " +
- "('广东省', '44', '广州市', '4401', '越秀区', '440104', '广东省广州市越秀区', 3, 2, 2), " +
- "('广东省', '44', '广州市', '4401', '番禺区', '440113', '广东省广州市番禺区', 3, 2, 3), " +
- "('广东省', '44', '深圳市', '4403', '南山区', '440305', '广东省深圳市南山区', 3, 3, 1), " +
- "('广东省', '44', '深圳市', '4403', '福田区', '440304', '广东省深圳市福田区', 3, 3, 2)");
- // 街道
- jdbcTemplate.execute("INSERT INTO streets (province, province_code, city, city_code, district, district_code, street, street_code, full_name, level, parent_id, sort_order) VALUES " +
- "('广东省', '44', '广州市', '4401', '天河区', '440106', '天河南街道', '440106001', '广东省广州市天河区天河南街道', 4, 4, 1), " +
- "('广东省', '44', '广州市', '4401', '天河区', '440106', '沙河街道', '440106002', '广东省广州市天河区沙河街道', 4, 4, 2), " +
- "('广东省', '44', '广州市', '4401', '越秀区', '440104', '东山街道', '440104001', '广东省广州市越秀区东山街道', 4, 5, 1), " +
- "('广东省', '44', '广州市', '4401', '番禺区', '440113', '市桥街道', '440113001', '广东省广州市番禺区市桥街道', 4, 6, 1), " +
- "('广东省', '44', '深圳市', '4403', '南山区', '440305', '南山街道', '440305001', '广东省深圳市南山区南山街道', 4, 7, 1), " +
- "('广东省', '44', '深圳市', '4403', '福田区', '440304', '福田街道', '440304001', '广东省深圳市福田区福田街道', 4, 8, 1)");
- log.info("街道示例数据已初始化");
- }
- } catch (Exception e) {
- log.warn("初始化街道数据失败: {}", e.getMessage());
- }
- try {
- Integer count = jdbcTemplate.queryForObject(
- "SELECT COUNT(*) FROM admins", Integer.class);
- if (count == null || count == 0) {
- // 默认管理员: 手机号 13800138000, 密码 admin123
- String encryptedPassword = org.springframework.util.DigestUtils.md5DigestAsHex("xzyj_admin_salt_admin123".getBytes());
- jdbcTemplate.execute("INSERT INTO admins (username, password, real_name, phone, email, role, status) VALUES " +
- "('admin', '" + encryptedPassword + "', '系统管理员', '13800138000', 'admin@xzyj.com', 'admin', 1)");
- log.info("默认管理员已初始化: 手机号 13800138000, 密码 admin123");
- }
- } catch (Exception e) {
- log.warn("初始化任务模板失败: {}", e.getMessage());
- }
- // 初始化 users 表测试用户
- try {
- // 创建默认家庭(如果不存在)
- Integer familyCount = jdbcTemplate.queryForObject(
- "SELECT COUNT(*) FROM families WHERE id = 1", Integer.class);
- if (familyCount == null || familyCount == 0) {
- jdbcTemplate.execute("INSERT INTO families (id, name, invite_code) VALUES (1, '默认家庭', '888888')");
- log.info("默认家庭已创建");
- }
- // 密码加密: MD5("xzyj_salt_" + password)
- String adminPassword = org.springframework.util.DigestUtils.md5DigestAsHex("xzyj_salt_admin123".getBytes());
- String teacherPassword = org.springframework.util.DigestUtils.md5DigestAsHex("xzyj_salt_teacher123".getBytes());
- // 创建管理员用户(管理员角色,密码 admin123)
- Integer adminUserCount = jdbcTemplate.queryForObject(
- "SELECT COUNT(*) FROM users WHERE phone = '13800138000'", Integer.class);
- if (adminUserCount == null || adminUserCount == 0) {
- jdbcTemplate.execute("INSERT INTO users (openid, family_id, role, phone, nickname, real_name, password) VALUES " +
- "('seed_admin_13800138000', 1, 'admin', '13800138000', '系统管理员', '系统管理员', '" + adminPassword + "')");
- log.info("测试管理员用户已创建: 13800138000 (admin角色, 密码 admin123)");
- }
- // 创建测试教师用户
- Integer teacherUserCount = jdbcTemplate.queryForObject(
- "SELECT COUNT(*) FROM users WHERE phone = '13800138001'", Integer.class);
- if (teacherUserCount == null || teacherUserCount == 0) {
- jdbcTemplate.execute("INSERT INTO users (openid, family_id, role, phone, nickname, real_name, password) VALUES " +
- "('seed_teacher_13800138001', 1, 'teacher', '13800138001', '测试教师', '测试教师', '" + teacherPassword + "')");
- log.info("测试教师用户已创建: 13800138001 (教师角色, 密码 teacher123)");
- }
- } catch (Exception e) {
- log.warn("初始化测试用户失败: {}", e.getMessage());
- }
- // 初始化星座配置种子数据
- try {
- Integer count = jdbcTemplate.queryForObject(
- "SELECT COUNT(*) FROM zodiac_config", Integer.class);
- if (count == null || count == 0) {
- jdbcTemplate.execute("INSERT INTO zodiac_config (zodiac_code, zodiac_name, element, start_month, start_day, end_month, end_day, mind_base, wisdom_base, body_modifier, action_modifier, wealth_modifier) VALUES " +
- "('aries', '白羊座', '火', 3, 21, 4, 19, 65, 70, 1.10, 1.05, 0.95), " +
- "('taurus', '金牛座', '土', 4, 20, 5, 20, 60, 65, 1.05, 1.10, 1.15), " +
- "('gemini', '双子座', '金', 5, 21, 6, 21, 70, 80, 0.95, 1.15, 0.90), " +
- "('cancer', '巨蟹座', '水', 6, 22, 7, 22, 75, 60, 1.00, 0.90, 1.05), " +
- "('leo', '狮子座', '火', 7, 23, 8, 22, 65, 75, 1.15, 1.00, 1.10), " +
- "('virgo', '处女座', '土', 8, 23, 9, 22, 55, 85, 0.95, 1.05, 1.00), " +
- "('libra', '天秤座', '金', 9, 23, 10, 23, 70, 75, 0.90, 0.95, 1.10), " +
- "('scorpio', '天蝎座', '水', 10, 24, 11, 22, 80, 70, 1.10, 1.00, 1.15), " +
- "('sagittarius', '射手座', '火', 11, 23, 12, 21, 60, 65, 1.15, 1.15, 0.95), " +
- "('capricorn', '摩羯座', '土', 12, 22, 1, 19, 55, 75, 1.05, 1.10, 1.20), " +
- "('aquarius', '水瓶座', '金', 1, 20, 2, 18, 75, 85, 0.95, 0.90, 1.00), " +
- "('pisces', '双鱼座', '水', 2, 19, 3, 20, 80, 60, 0.90, 0.95, 0.95)");
- log.info("星座配置种子数据已初始化");
- }
- } catch (Exception e) {
- log.warn("初始化星座配置失败: {}", e.getMessage());
- }
- // 初始化八字配置种子数据(十天干 + 十二地支)
- try {
- Integer count = jdbcTemplate.queryForObject(
- "SELECT COUNT(*) FROM bazi_config", Integer.class);
- if (count == null || count == 0) {
- jdbcTemplate.execute("INSERT INTO bazi_config (stem_or_branch, type, element, yin_yang, mind_base, wisdom_base, body_modifier, action_modifier, wealth_modifier) VALUES " +
- "('甲', 'stem', '木', 'yang', 60, 75, 1.05, 1.10, 1.00), " +
- "('乙', 'stem', '木', 'yin', 65, 70, 0.95, 1.05, 1.00), " +
- "('丙', 'stem', '火', 'yang', 75, 65, 1.10, 1.00, 0.95), " +
- "('丁', 'stem', '火', 'yin', 70, 60, 1.05, 0.95, 1.00), " +
- "('戊', 'stem', '土', 'yang', 60, 65, 1.15, 1.05, 1.10), " +
- "('己', 'stem', '土', 'yin', 55, 60, 1.10, 1.00, 1.15), " +
- "('庚', 'stem', '金', 'yang', 65, 80, 1.05, 1.10, 1.05), " +
- "('辛', 'stem', '金', 'yin', 60, 75, 0.95, 1.00, 1.10), " +
- "('壬', 'stem', '水', 'yang', 70, 70, 1.00, 0.95, 1.15), " +
- "('癸', 'stem', '水', 'yin', 75, 65, 0.95, 0.90, 1.10), " +
- "('子', 'branch', '水', 'yang', 70, 65, 1.00, 0.95, 1.05), " +
- "('丑', 'branch', '土', 'yin', 55, 60, 1.10, 1.05, 1.10), " +
- "('寅', 'branch', '木', 'yang', 65, 75, 1.15, 1.10, 1.00), " +
- "('卯', 'branch', '木', 'yin', 60, 70, 1.05, 1.05, 0.95), " +
- "('辰', 'branch', '土', 'yang', 60, 65, 1.10, 1.00, 1.10), " +
- "('巳', 'branch', '火', 'yin', 75, 70, 1.05, 0.95, 1.00), " +
- "('午', 'branch', '火', 'yang', 80, 65, 1.15, 1.00, 0.95), " +
- "('未', 'branch', '土', 'yin', 55, 60, 1.05, 1.00, 1.05), " +
- "('申', 'branch', '金', 'yang', 65, 80, 1.00, 1.10, 1.05), " +
- "('酉', 'branch', '金', 'yin', 60, 75, 0.95, 1.05, 1.10), " +
- "('戌', 'branch', '土', 'yang', 60, 65, 1.10, 1.00, 1.15), " +
- "('亥', 'branch', '水', 'yin', 70, 60, 1.00, 0.95, 1.05)");
- log.info("八字配置种子数据已初始化");
- }
- } catch (Exception e) {
- log.warn("初始化八字配置失败: {}", e.getMessage());
- }
- // 初始化血型配置种子数据
- try {
- Integer count = jdbcTemplate.queryForObject(
- "SELECT COUNT(*) FROM blood_type_config", Integer.class);
- if (count == null || count == 0) {
- jdbcTemplate.execute("INSERT INTO blood_type_config (blood_type, description, mind_base, wisdom_base, body_modifier, action_modifier, wealth_modifier) VALUES " +
- "('A', '认真负责、严谨细致,但易焦虑', 70, 75, 0.95, 1.00, 1.05), " +
- "('B', '乐观开朗、好奇心强,但缺乏耐心', 65, 70, 1.05, 1.10, 0.95), " +
- "('AB', '理性冷静、适应力强,但优柔寡断', 75, 80, 1.00, 0.95, 1.10), " +
- "('O', '自信果断、行动力强,但固执己见', 60, 65, 1.15, 1.15, 1.15)");
- log.info("血型配置种子数据已初始化");
- }
- } catch (Exception e) {
- log.warn("初始化血型配置失败: {}", e.getMessage());
- }
- // 初始化优惠券种子数据
- try {
- Integer count = jdbcTemplate.queryForObject(
- "SELECT COUNT(*) FROM coupon", Integer.class);
- if (count == null || count == 0) {
- jdbcTemplate.update(
- "INSERT INTO coupon (name, type, value, min_spend, applicable_to, valid_from, valid_until, total_count, used_count, grant_type, grant_level_code) " +
- "VALUES (?, ?, ?, ?, ?, NOW(), DATE_ADD(NOW(), INTERVAL 1 YEAR), -1, 0, 'REGISTER', NULL)",
- "新用户专享10元券", "FIXED", 1000, 0, "ALL");
- jdbcTemplate.update(
- "INSERT INTO coupon (name, type, value, min_spend, applicable_to, valid_from, valid_until, total_count, used_count, grant_type, grant_level_code) " +
- "VALUES (?, ?, ?, ?, ?, NOW(), DATE_ADD(NOW(), INTERVAL 1 YEAR), -1, 0, NULL, NULL)",
- "会员升级50元券", "FIXED", 5000, 9900, "MEMBERSHIP");
- // JOIN 赠券模板:开通家庭会员赠 20 元券
- jdbcTemplate.update(
- "INSERT INTO coupon (name, type, value, min_spend, applicable_to, valid_from, valid_until, total_count, used_count, grant_type, grant_level_code, grant_quantity) " +
- "VALUES (?, ?, ?, ?, ?, NOW(), DATE_ADD(NOW(), INTERVAL 1 YEAR), -1, 0, 'JOIN', 'FAMILY', 1)");
- // JOIN 赠券模板:开通 Premium 会员赠 50 元券
- jdbcTemplate.update(
- "INSERT INTO coupon (name, type, value, min_spend, applicable_to, valid_from, valid_until, total_count, used_count, grant_type, grant_level_code, grant_quantity) " +
- "VALUES (?, ?, ?, ?, ?, NOW(), DATE_ADD(NOW(), INTERVAL 1 YEAR), -1, 0, 'JOIN', 'PREMIUM', 1)");
- log.info("优惠券种子数据已初始化(含 JOIN 赠券模板)");
- }
- // COUPON-REG:幂等修正存量库——将「新用户专享10元券」模板标记为 REGISTER 自动发放
- // (存量库种子创建时 grant_type 为 NULL,改后无需 DDL,仅数据修正)
- int fixed = jdbcTemplate.update(
- "UPDATE coupon SET grant_type = 'REGISTER' " +
- "WHERE name = '新用户专享10元券' AND (grant_type IS NULL OR grant_type = '')");
- if (fixed > 0) {
- log.info("已标记 {} 张存量「新用户专享10元券」模板为 REGISTER 自动发放", fixed);
- }
- } catch (Exception e) {
- log.warn("初始化优惠券数据失败: {}", e.getMessage());
- }
- // 初始化成长规划师模块相关表
- initGuideModuleTables(jdbcTemplate);
- }
- private void initGuideModuleTables(JdbcTemplate jdbcTemplate) {
- // 服务类型表
- try {
- jdbcTemplate.execute("CREATE TABLE IF NOT EXISTS service_types (" +
- "id BIGINT AUTO_INCREMENT PRIMARY KEY, " +
- "name VARCHAR(100) NOT NULL COMMENT '类型名称', " +
- "icon VARCHAR(50) COMMENT '图标', " +
- "description VARCHAR(500) COMMENT '描述', " +
- "sort_order INT DEFAULT 0 COMMENT '排序', " +
- "status VARCHAR(20) DEFAULT 'ACTIVE' COMMENT '状态:ACTIVE/DISABLED', " +
- "created_at DATETIME DEFAULT CURRENT_TIMESTAMP, " +
- "updated_at DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP" +
- ")");
- } catch (Exception e) {
- log.warn("创建service_types表失败: {}", e.getMessage());
- }
- // 服务内容表
- try {
- jdbcTemplate.execute("CREATE TABLE IF NOT EXISTS service_contents (" +
- "id BIGINT AUTO_INCREMENT PRIMARY KEY, " +
- "type_id BIGINT NOT NULL COMMENT '关联服务类型', " +
- "title VARCHAR(200) NOT NULL COMMENT '内容标题', " +
- "content TEXT COMMENT '富文本内容', " +
- "duration INT COMMENT '建议时长(分钟)', " +
- "age_range VARCHAR(50) COMMENT '适用年龄', " +
- "price INT COMMENT '参考价格(分)', " +
- "status VARCHAR(20) DEFAULT 'DRAFT' COMMENT '状态:DRAFT/ACTIVE', " +
- "created_at DATETIME DEFAULT CURRENT_TIMESTAMP, " +
- "updated_at DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP" +
- ")");
- } catch (Exception e) {
- log.warn("创建service_contents表失败: {}", e.getMessage());
- }
- // 套餐模板表
- try {
- jdbcTemplate.execute("CREATE TABLE IF NOT EXISTS guide_package_templates (" +
- "id BIGINT AUTO_INCREMENT PRIMARY KEY, " +
- "name VARCHAR(100) NOT NULL COMMENT '模板名称', " +
- "validity_months INT COMMENT '有效期(月)', " +
- "reference_price INT COMMENT '参考价格(分)', " +
- "min_price INT COMMENT '最低售价(分)', " +
- "description VARCHAR(500) COMMENT '说明', " +
- "status VARCHAR(20) DEFAULT 'ACTIVE' COMMENT '状态:ACTIVE/DISABLED', " +
- "created_at DATETIME DEFAULT CURRENT_TIMESTAMP, " +
- "updated_at DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP" +
- ")");
- } catch (Exception e) {
- log.warn("创建guide_package_templates表失败: {}", e.getMessage());
- }
- // 套餐模板服务项表
- try {
- jdbcTemplate.execute("CREATE TABLE IF NOT EXISTS package_template_services (" +
- "id BIGINT AUTO_INCREMENT PRIMARY KEY, " +
- "template_id BIGINT NOT NULL COMMENT '关联模板', " +
- "content_id BIGINT NOT NULL COMMENT '关联服务内容', " +
- "quantity INT DEFAULT 1 COMMENT '数量', " +
- "unit VARCHAR(20) COMMENT '单位' " +
- ")");
- } catch (Exception e) {
- log.warn("创建package_template_services表失败: {}", e.getMessage());
- }
- // 成长规划师套餐表
- try {
- jdbcTemplate.execute("CREATE TABLE IF NOT EXISTS guide_packages (" +
- "id BIGINT AUTO_INCREMENT PRIMARY KEY, " +
- "teacher_id BIGINT NOT NULL COMMENT '成长规划师ID', " +
- "template_id BIGINT COMMENT '关联模板', " +
- "name VARCHAR(100) NOT NULL COMMENT '套餐名称', " +
- "price INT NOT NULL COMMENT '销售价格(分)', " +
- "validity_months INT COMMENT '有效期(月)', " +
- "description VARCHAR(500) COMMENT '套餐说明', " +
- "status VARCHAR(20) DEFAULT 'DRAFT' COMMENT '状态:DRAFT/PUBLISHED/PAUSED', " +
- "sales_count INT DEFAULT 0 COMMENT '销量', " +
- "share_code VARCHAR(50) COMMENT '分享码', " +
- "created_at DATETIME DEFAULT CURRENT_TIMESTAMP, " +
- "updated_at DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP" +
- ")");
- } catch (Exception e) {
- log.warn("创建guide_packages表失败: {}", e.getMessage());
- }
- // 迁移:guide_packages 表添加缺失列
- try {
- jdbcTemplate.execute("ALTER TABLE guide_packages ADD COLUMN guide_id BIGINT COMMENT '成长规划师ID' AFTER id");
- } catch (Exception e) {
- }
- try {
- jdbcTemplate.execute("ALTER TABLE guide_packages ADD COLUMN package_id BIGINT COMMENT '关联模板' AFTER guide_id");
- } catch (Exception e) {
- }
- try {
- jdbcTemplate.execute("ALTER TABLE guide_packages ADD COLUMN commission_rate INT COMMENT '佣金比例(bps,100=1%)' AFTER package_id");
- } catch (Exception e) {
- }
- try {
- jdbcTemplate.execute("ALTER TABLE guide_packages MODIFY COLUMN teacher_id BIGINT NULL");
- } catch (Exception e) {
- }
- try {
- jdbcTemplate.execute("ALTER TABLE guide_packages MODIFY COLUMN template_id BIGINT NULL");
- } catch (Exception e) {
- }
- // 迁移:guide_packages 表添加 level 列(GuidePackage 实体需要)
- try {
- jdbcTemplate.execute("ALTER TABLE guide_packages ADD COLUMN level VARCHAR(20) COMMENT '套餐等级' AFTER name");
- } catch (Exception e) {
- }
- // 迁移:guide_packages 表添加 validity_days 列
- try {
- jdbcTemplate.execute("ALTER TABLE guide_packages ADD COLUMN validity_days INT DEFAULT 0 COMMENT '有效期天数' AFTER validity_months");
- } catch (Exception e) {
- }
- // 迁移:guide_packages 表添加 sort_order 列
- try {
- jdbcTemplate.execute("ALTER TABLE guide_packages ADD COLUMN sort_order INT DEFAULT 0 COMMENT '排序' AFTER status");
- } catch (Exception e) {
- }
- // 成长规划师套餐服务项表
- try {
- jdbcTemplate.execute("CREATE TABLE IF NOT EXISTS guide_package_services (" +
- "id BIGINT AUTO_INCREMENT PRIMARY KEY, " +
- "package_id BIGINT NOT NULL COMMENT '关联套餐', " +
- "content_id BIGINT NOT NULL COMMENT '关联服务内容', " +
- "quantity INT DEFAULT 1 COMMENT '数量', " +
- "unit VARCHAR(20) COMMENT '单位' " +
- ")");
- } catch (Exception e) {
- log.warn("创建guide_package_services表失败: {}", e.getMessage());
- }
- // 订单表
- try {
- jdbcTemplate.execute("CREATE TABLE IF NOT EXISTS guide_orders (" +
- "id BIGINT AUTO_INCREMENT PRIMARY KEY, " +
- "order_no VARCHAR(50) NOT NULL COMMENT '订单号', " +
- "teacher_id BIGINT NOT NULL COMMENT '成长规划师ID', " +
- "family_id BIGINT NOT NULL COMMENT '家庭ID', " +
- "package_id BIGINT NOT NULL COMMENT '套餐ID', " +
- "amount INT NOT NULL COMMENT '金额(分)', " +
- "status VARCHAR(20) DEFAULT 'PENDING' COMMENT '状态:PENDING/PAID/COMPLETED/REFUNDED', " +
- "pay_time DATETIME COMMENT '支付时间', " +
- "created_at DATETIME DEFAULT CURRENT_TIMESTAMP, " +
- "updated_at DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP" +
- ")");
- } catch (Exception e) {
- log.warn("创建guide_orders表失败: {}", e.getMessage());
- }
- // 佣金明细表
- try {
- jdbcTemplate.execute("CREATE TABLE IF NOT EXISTS guide_commissions (" +
- "id BIGINT AUTO_INCREMENT PRIMARY KEY, " +
- "teacher_id BIGINT NOT NULL COMMENT '成长规划师ID', " +
- "order_id BIGINT NOT NULL COMMENT '订单ID', " +
- "amount INT NOT NULL COMMENT '佣金金额(分)', " +
- "rate INT COMMENT '佣金比例(bps,100=1%)', " +
- "status VARCHAR(20) DEFAULT 'PENDING' COMMENT '状态:PENDING/COMPLETED', " +
- "created_at DATETIME DEFAULT CURRENT_TIMESTAMP" +
- ")");
- } catch (Exception e) {
- log.warn("创建guide_commissions表失败: {}", e.getMessage());
- }
- // 服务记录表
- try {
- jdbcTemplate.execute("CREATE TABLE IF NOT EXISTS guide_service_logs (" +
- "id BIGINT AUTO_INCREMENT PRIMARY KEY, " +
- "teacher_id BIGINT NOT NULL COMMENT '成长规划师ID', " +
- "family_id BIGINT NOT NULL COMMENT '家庭ID', " +
- "order_id BIGINT COMMENT '订单ID', " +
- "service_content_id BIGINT NOT NULL COMMENT '服务内容ID', " +
- "notes VARCHAR(500) COMMENT '服务记录', " +
- "duration INT COMMENT '服务时长(分钟)', " +
- "created_at DATETIME DEFAULT CURRENT_TIMESTAMP" +
- ")");
- } catch (Exception e) {
- log.warn("创建guide_service_logs表失败: {}", e.getMessage());
- }
- // 成长规划师邀请码表
- try {
- jdbcTemplate.execute("CREATE TABLE IF NOT EXISTS guide_invite_codes (" +
- "id BIGINT AUTO_INCREMENT PRIMARY KEY, " +
- "guide_id BIGINT NOT NULL COMMENT '成长规划师用户ID', " +
- "code VARCHAR(50) NOT NULL UNIQUE COMMENT '邀请码', " +
- "status VARCHAR(20) DEFAULT 'active' COMMENT '状态', " +
- "used_by_user_id BIGINT COMMENT '使用该邀请码的用户ID', " +
- "used_at DATETIME COMMENT '使用时间', " +
- "expire_at DATETIME COMMENT '过期时间', " +
- "use_count INT DEFAULT 0 COMMENT '使用次数', " +
- "max_use_count INT COMMENT '最大使用次数', " +
- "created_at DATETIME DEFAULT CURRENT_TIMESTAMP, " +
- "updated_at DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP" +
- ")");
- } catch (Exception e) {
- log.warn("创建guide_invite_codes表失败: {}", e.getMessage());
- }
- // 迁移29: assessment_orders表添加appointment_id字段
- try {
- jdbcTemplate.execute("ALTER TABLE assessment_orders ADD COLUMN appointment_id BIGINT COMMENT '关联的预约ID'");
- log.info("已添加appointment_id列到assessment_orders表");
- } catch (Exception e) {
- // 列已存在,忽略错误
- }
- // 财富维度: 财商打卡记录表
- try {
- jdbcTemplate.execute("CREATE TABLE IF NOT EXISTS finance_checkins (" +
- "id BIGINT AUTO_INCREMENT PRIMARY KEY, " +
- "user_id BIGINT NOT NULL COMMENT '用户ID', " +
- "child_id BIGINT NULL COMMENT '孩子ID,家长本人打卡时为NULL', " +
- "checkin_type VARCHAR(20) NOT NULL COMMENT '打卡类型: daily_plan/saving/expense/review', " +
- "type VARCHAR(10) DEFAULT 'expense' COMMENT '类型: income(收入)/expense(支出)', " +
- "checkin_date DATE NOT NULL COMMENT '打卡日期', " +
- "amount INT COMMENT '金额(分,收支记录)', " +
- "category VARCHAR(50) COMMENT '收支分类', " +
- "note VARCHAR(200) COMMENT '备注', " +
- "mood VARCHAR(20) COMMENT '心情', " +
- "earned_energy INT DEFAULT 0 COMMENT '获得的能量值', " +
- "created_at DATETIME DEFAULT CURRENT_TIMESTAMP, " +
- "INDEX idx_user_date (user_id, checkin_date), " +
- "INDEX idx_child (child_id)" +
- ") ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='财商打卡记录表'");
- log.info("已创建finance_checkins表");
- } catch (Exception e) {
- log.warn("创建finance_checkins表失败: {}", e.getMessage());
- }
- // 财富维度: 保单管理表
- try {
- jdbcTemplate.execute("CREATE TABLE IF NOT EXISTS insurance_policies (" +
- "id BIGINT AUTO_INCREMENT PRIMARY KEY, " +
- "user_id BIGINT NOT NULL COMMENT '用户ID', " +
- "child_id BIGINT COMMENT '关联孩子ID', " +
- "policy_name VARCHAR(100) NOT NULL COMMENT '保单名称', " +
- "insurance_company VARCHAR(100) COMMENT '保险公司', " +
- "policy_type VARCHAR(50) COMMENT '保单类型: medical/life/accident/education/endowment', " +
- "insured_person VARCHAR(50) NOT NULL COMMENT '被保人姓名', " +
- "premium INT COMMENT '保费(分)', " +
- "sum_insured INT COMMENT '保额(分)', " +
- "start_date DATE COMMENT '生效日期', " +
- "end_date DATE COMMENT '到期日期', " +
- "renewal_type VARCHAR(20) DEFAULT 'annual' COMMENT '续费类型: annual/monthly/single', " +
- "status VARCHAR(20) DEFAULT 'active' COMMENT '状态: active/expired/cancelled', " +
- "policy_no VARCHAR(100) COMMENT '保单号', " +
- "remark VARCHAR(500) COMMENT '备注', " +
- "created_at DATETIME DEFAULT CURRENT_TIMESTAMP, " +
- "updated_at DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, " +
- "INDEX idx_user (user_id), " +
- "INDEX idx_child (child_id), " +
- "INDEX idx_status (status)" +
- ") ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='保单管理表'");
- log.info("已创建insurance_policies表");
- } catch (Exception e) {
- log.warn("创建insurance_policies表失败: {}", e.getMessage());
- }
- // ==================== 健康检测报告模块 ============= // 健康报告表(体检报告/菌群检测报告)
- try {
- jdbcTemplate.execute("CREATE TABLE IF NOT EXISTS health_reports (" +
- "id BIGINT AUTO_INCREMENT PRIMARY KEY, " +
- "user_id BIGINT NOT NULL COMMENT '用户ID', " +
- "family_id BIGINT COMMENT '家庭ID', " +
- "report_type VARCHAR(32) NOT NULL COMMENT '报告类型: physical_exam(体检)/gut_flora(菌群检测)', " +
- "report_date DATE COMMENT '报告日期', " +
- "overall_score INT COMMENT '健康总分(0-100)', " +
- "gut_health_score INT COMMENT '菌群健康分', " +
- "chronic_disease_score INT COMMENT '慢病控制分', " +
- "nutrition_score INT COMMENT '营养均衡分', " +
- "gut_age VARCHAR(32) COMMENT '肠道预测年龄', " +
- "gut_type VARCHAR(50) COMMENT '肠型', " +
- "file_url VARCHAR(500) COMMENT '报告文件URL', " +
- "status VARCHAR(20) DEFAULT 'active' COMMENT '状态: active/archived', " +
- "remark VARCHAR(500) COMMENT '备注', " +
- "created_at DATETIME DEFAULT CURRENT_TIMESTAMP, " +
- "updated_at DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, " +
- "INDEX idx_user_id (user_id), " +
- "INDEX idx_family_id (family_id), " +
- "INDEX idx_report_type (report_type)" +
- ") ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='健康检测报告表'");
- log.info("已创建health_reports表");
- } catch (Exception e) {
- log.warn("创建health_reports表可能已存在: {}", e.getMessage());
- }
- // 健康指标明细表
- try {
- jdbcTemplate.execute("CREATE TABLE IF NOT EXISTS health_indicators (" +
- "id BIGINT AUTO_INCREMENT PRIMARY KEY, " +
- "report_id BIGINT NOT NULL COMMENT '关联报告ID', " +
- "category VARCHAR(50) COMMENT '指标分类: 营养状况/疾病风险/肠道屏障/神经递质/...', " +
- "indicator_name VARCHAR(100) NOT NULL COMMENT '指标名称', " +
- "indicator_value VARCHAR(100) COMMENT '检测值', " +
- "unit VARCHAR(50) COMMENT '单位', " +
- "ref_range VARCHAR(200) COMMENT '参考范围', " +
- "status VARCHAR(50) COMMENT '评估状态: 正常/偏低/偏高/过多/注意/低风险/未检出', " +
- "sort_order INT DEFAULT 0 COMMENT '显示排序', " +
- "created_at DATETIME DEFAULT CURRENT_TIMESTAMP, " +
- "INDEX idx_report_id (report_id), " +
- "INDEX idx_category (category)" +
- ") ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='健康指标明细表'");
- log.info("已创建health_indicators表");
- } catch (Exception e) {
- log.warn("创建health_indicators表可能已存在: {}", e.getMessage());
- }
- // ---------- 健康报告表 新增菌群摘要字段 ----------
- try {
- jdbcTemplate.execute("ALTER TABLE health_reports " +
- "ADD COLUMN gut_balance_score INT COMMENT '菌群平衡分(0-100)', " +
- "ADD COLUMN gut_diversity_score INT COMMENT '菌群多样性分(0-100)', " +
- "ADD COLUMN beneficial_bacteria_score INT COMMENT '有益菌分(0-100)', " +
- "ADD COLUMN harmful_bacteria_score INT COMMENT '有害菌分(0-100)', " +
- "ADD COLUMN core_species_score INT COMMENT '核心菌属分(0-100)', " +
- "ADD COLUMN health_rating VARCHAR(100) COMMENT '总分评级,如亚健康/优秀等'");
- log.info("已为health_reports表添加6个菌群摘要字段");
- } catch (Exception e) {
- log.warn("为health_reports表添加字段可能已存在: {}", e.getMessage());
- }
- ensureColumn("health_reports", "subject_id", "BIGINT COMMENT '报告主体用户ID(关联users表,为null表示未绑定)'");
- log.info("成长规划师模块、财富维度及健康检测报告数据库表初始化完成");
- // 健康打卡表
- try {
- jdbcTemplate.execute("CREATE TABLE IF NOT EXISTS health_checkins (" +
- "id BIGINT AUTO_INCREMENT PRIMARY KEY, " +
- "user_id BIGINT COMMENT '用户ID(家长)', " +
- "child_id BIGINT COMMENT '孩子ID', " +
- "checkin_date DATE COMMENT '打卡日期', " +
- "behaviors TEXT COMMENT '打卡行为JSON数组', " +
- "note VARCHAR(500) COMMENT '备注', " +
- "mood VARCHAR(20) COMMENT '心情: happy/normal/sad/angry/sick', " +
- "earned_energy INT DEFAULT 5 COMMENT '获得能量值', " +
- "created_at DATETIME DEFAULT CURRENT_TIMESTAMP, " +
- "INDEX idx_child_id (child_id), " +
- "INDEX idx_checkin_date (checkin_date)" +
- ") ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='健康打卡表'");
- log.info("已创建health_checkins表");
- } catch (Exception e) {
- log.warn("创建health_checkins表可能已存在: {}", e.getMessage());
- }
- // 迁移31: 创建联系人表
- // 创建文章阅读记录表
- try {
- jdbcTemplate.execute("CREATE TABLE IF NOT EXISTS article_reading_records (" +
- "id BIGINT AUTO_INCREMENT PRIMARY KEY, " +
- "user_id BIGINT COMMENT '用户ID', " +
- "child_id BIGINT NOT NULL COMMENT '孩子ID', " +
- "content VARCHAR(500) COMMENT '阅读内容摘要', " +
- "duration_seconds INT DEFAULT 0 COMMENT '阅读时长(秒)', " +
- "read_at DATETIME COMMENT '阅读时间', " +
- "created_at DATETIME, " +
- "INDEX idx_child_id (child_id), " +
- "INDEX idx_read_at (read_at)" +
- ") ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='文章阅读记录表'");
- log.info("已创建article_reading_records表");
- } catch (Exception e) {
- log.warn("创建article_reading_records表可能已存在: {}", e.getMessage());
- }
- // 迁移32: products表添加配送/类目/销售字段(商品编辑表单对齐)
- ensureColumn("products", "delivery_method", "TINYINT DEFAULT 1 COMMENT '配送方式: 1=快递 2=自提 3=两者皆可'");
- ensureColumn("products", "category_id", "BIGINT COMMENT '商品类目ID'");
- ensureColumn("products", "distribution_system_id", "BIGINT COMMENT '销售体系ID'");
- // 迁移33: 创建供应商销售体系表 + product_orders扩展字段
- // 迁移222: health_reports 添加 shannon_index, payload_json 列并创建 health_report_edit_logs 表
- try {
- jdbcTemplate.execute("ALTER TABLE health_reports ADD COLUMN shannon_index VARCHAR(32) DEFAULT NULL COMMENT '香农指数'");
- log.info("已添加shannon_index列到health_reports表");
- } catch (Exception e) {
- // 列已存在,忽略错误
- }
- try {
- jdbcTemplate.execute("ALTER TABLE health_reports ADD COLUMN payload_json LONGTEXT COMMENT '报告完整载荷快照'");
- log.info("已添加payload_json列到health_reports表");
- } catch (Exception e) {
- // 列已存在,忽略错误
- }
- try {
- jdbcTemplate.execute("CREATE TABLE IF NOT EXISTS health_report_edit_logs (" +
- "id BIGINT AUTO_INCREMENT PRIMARY KEY, " +
- "report_id BIGINT NOT NULL, " +
- "user_id BIGINT, " +
- "report_type VARCHAR(32), " +
- "action VARCHAR(20) COMMENT 'create/edit', " +
- "before_payload LONGTEXT, " +
- "after_payload LONGTEXT, " +
- "created_at DATETIME DEFAULT CURRENT_TIMESTAMP" +
- ") ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='健康报告修改留痕'");
- log.info("已创建health_report_edit_logs表");
- } catch (Exception e) {
- // 表已存在,忽略错误
- }
- ensureColumn("product_orders", "supply_system_id", "BIGINT COMMENT '所属供应商体系ID'");
- ensureColumn("product_orders", "supplier_id", "BIGINT COMMENT '收款供应商用户ID'");
- ensureColumn("product_orders", "supply_commission", "INT COMMENT '销售佣金(分)'");
- try {
- jdbcTemplate.execute("ALTER TABLE product_orders ADD KEY idx_supply_system_id (supply_system_id)");
- log.info("已添加idx_supply_system_id索引到product_orders表");
- } catch (Exception e) {
- log.warn("添加idx_supply_system_id索引可能已存在: {}", e.getMessage());
- }
- try {
- jdbcTemplate.execute("CREATE TABLE IF NOT EXISTS supply_system (" +
- "id BIGINT AUTO_INCREMENT PRIMARY KEY, " +
- "name VARCHAR(100) NOT NULL COMMENT '体系名称', " +
- "admin_id BIGINT NOT NULL COMMENT '管理员用户ID', " +
- "settlement_period_days INT NOT NULL DEFAULT 30 COMMENT '账期(天)', " +
- "platform_profit_rate DECIMAL(5,2) NOT NULL DEFAULT 0 COMMENT '平台留利比例(%)', " +
- "description VARCHAR(500) COMMENT '描述', " +
- "status VARCHAR(20) NOT NULL DEFAULT 'active' COMMENT '状态: active/disabled', " +
- "created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, " +
- "updated_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, " +
- "KEY idx_admin_id (admin_id)" +
- ") ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='供应商体系'");
- log.info("已创建supply_system表");
- } catch (Exception e) {
- log.warn("创建supply_system表可能已存在: {}", e.getMessage());
- }
- try {
- jdbcTemplate.execute("CREATE TABLE IF NOT EXISTS supply_system_member (" +
- "id BIGINT AUTO_INCREMENT PRIMARY KEY, " +
- "system_id BIGINT NOT NULL COMMENT '体系ID', " +
- "user_id BIGINT NOT NULL COMMENT '用户ID', " +
- "role VARCHAR(20) NOT NULL DEFAULT 'member' COMMENT '角色: admin/member', " +
- "status VARCHAR(20) NOT NULL DEFAULT 'active' COMMENT '状态: active/inactive', " +
- "joined_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, " +
- "UNIQUE KEY uk_system_user (system_id, user_id), " +
- "KEY idx_user_id (user_id)" +
- ") ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='体系成员'");
- log.info("已创建supply_system_member表");
- } catch (Exception e) {
- log.warn("创建supply_system_member表可能已存在: {}", e.getMessage());
- }
- try {
- jdbcTemplate.execute("CREATE TABLE IF NOT EXISTS supply_settlement (" +
- "id BIGINT AUTO_INCREMENT PRIMARY KEY, " +
- "system_id BIGINT NOT NULL COMMENT '体系ID', " +
- "period_start DATE NOT NULL COMMENT '结算周期开始', " +
- "period_end DATE NOT NULL COMMENT '结算周期结束', " +
- "total_sales INT NOT NULL DEFAULT 0 COMMENT '销售总额(分)', " +
- "platform_profit INT NOT NULL DEFAULT 0 COMMENT '平台留利金额(分)', " +
- "supplier_payout INT NOT NULL DEFAULT 0 COMMENT '供应商实付(分)', " +
- "status VARCHAR(20) NOT NULL DEFAULT 'pending' COMMENT '状态: pending/settled', " +
- "settled_at DATETIME COMMENT '结算时间', " +
- "remark VARCHAR(500) COMMENT '备注', " +
- "created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, " +
- "KEY idx_system_id (system_id), " +
- "KEY idx_period (period_start, period_end)" +
- ") ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='结算记录'");
- log.info("已创建supply_settlement表");
- } catch (Exception e) {
- log.warn("创建supply_settlement表可能已存在: {}", e.getMessage());
- }
- try {
- jdbcTemplate.execute("CREATE TABLE IF NOT EXISTS supply_settlement_detail (" +
- "id BIGINT AUTO_INCREMENT PRIMARY KEY, " +
- "settlement_id BIGINT NOT NULL COMMENT '结算ID', " +
- "order_id BIGINT NOT NULL COMMENT '订单ID', " +
- "order_no VARCHAR(64) NOT NULL COMMENT '订单号', " +
- "product_name VARCHAR(200) NOT NULL COMMENT '商品名称', " +
- "amount INT NOT NULL COMMENT '订单金额(分)', " +
- "platform_fee INT NOT NULL COMMENT '平台留利(分)', " +
- "supplier_id BIGINT COMMENT '收款供应商用户ID', " +
- "supplier_name VARCHAR(100) COMMENT '收款供应商名称', " +
- "payout INT NOT NULL COMMENT '供应商实付(分)', " +
- "created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, " +
- "KEY idx_settlement_id (settlement_id), " +
- "KEY idx_order_id (order_id)" +
- ") ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='结算明细'");
- log.info("已创建supply_settlement_detail表");
- } catch (Exception e) {
- log.warn("创建supply_settlement_detail表可能已存在: {}", e.getMessage());
- }
- // 迁移: growth_records表添加来源类型/去重/补充字段
- try {
- jdbcTemplate.execute("ALTER TABLE growth_records ADD COLUMN source_type VARCHAR(20) DEFAULT 'manual' COMMENT '来源类型: manual/platform_purchase/self_upload/supplement'");
- log.info("已添加source_type列到growth_records表");
- } catch (Exception e) {
- }
- try {
- jdbcTemplate.execute("ALTER TABLE growth_records ADD COLUMN source_file_url VARCHAR(500) COMMENT '原始报告文件URL'");
- log.info("已添加source_file_url列到growth_records表");
- } catch (Exception e) {
- }
- try {
- jdbcTemplate.execute("ALTER TABLE growth_records ADD COLUMN order_id BIGINT COMMENT '关联订单ID'");
- log.info("已添加order_id列到growth_records表");
- } catch (Exception e) {
- }
- try {
- jdbcTemplate.execute("ALTER TABLE growth_records ADD COLUMN report_date DATETIME COMMENT '测评/报告日期'");
- log.info("已添加report_date列到growth_records表");
- } catch (Exception e) {
- }
- try {
- jdbcTemplate.execute("ALTER TABLE growth_records ADD COLUMN parent_record_id BIGINT COMMENT '关联的原始档案ID(补充档案专用)'");
- log.info("已添加parent_record_id列到growth_records表");
- } catch (Exception e) {
- }
- try {
- jdbcTemplate.execute("ALTER TABLE growth_records ADD COLUMN is_latest TINYINT(1) DEFAULT 1 COMMENT '是否同组最新记录'");
- log.info("已添加is_latest列到growth_records表");
- } catch (Exception e) {
- }
- try {
- jdbcTemplate.execute("ALTER TABLE growth_records ADD COLUMN dedup_hash VARCHAR(64) COMMENT '去重哈希值(SHA256)'");
- log.info("已添加dedup_hash列到growth_records表");
- } catch (Exception e) {
- }
- try {
- jdbcTemplate.execute("ALTER TABLE growth_records ADD COLUMN parsed_data TEXT COMMENT '上传报告的解析结果(JSON)'");
- log.info("已添加parsed_data列到growth_records表");
- } catch (Exception e) {
- }
- try {
- jdbcTemplate.execute("CREATE UNIQUE INDEX IF NOT EXISTS uk_growth_dedup_hash ON growth_records(dedup_hash)");
- log.info("已添加uk_growth_dedup_hash唯一索引到growth_records表");
- } catch (Exception e) {
- try {
- jdbcTemplate.execute("ALTER TABLE growth_records ADD UNIQUE INDEX uk_growth_dedup_hash (dedup_hash)");
- log.info("已通过ALTER TABLE添加uk_growth_dedup_hash唯一索引");
- } catch (Exception e2) {
- log.warn("添加uk_growth_dedup_hash索引可能已存在: {}", e2.getMessage());
- }
- }
- // 初始化成长任务种子数据
- try {
- Integer count = jdbcTemplate.queryForObject(
- "SELECT COUNT(*) FROM growth_task", Integer.class);
- if (count == null || count == 0) {
- jdbcTemplate.execute("INSERT INTO growth_task (type, title, description, reward_points, reward_energy, target_value, task_key, enabled, sort_order, created_at) VALUES " +
- "('DAILY', '每日签到', '完成健康打卡签到', 20, 3, 1, 'DAILY_CHECKIN', 1, 1, NOW()), " +
- "('DAILY', '完成任务', '完成一个任务', 30, 5, 1, 'DAILY_TASK', 1, 2, NOW()), " +
- "('DAILY', '每日分享', '分享小程序给好友', 10, 2, 1, 'DAILY_SHARE', 1, 3, NOW()), " +
- "('DAILY', 'AI对话', '与AI助手对话一次', 15, 3, 1, 'DAILY_AI', 1, 4, NOW())");
- log.info("成长任务种子数据已初始化");
- }
- } catch (Exception e) {
- log.warn("初始化成长任务种子数据失败: {}", e.getMessage());
- }
- // 迁移34: dan_assessment_results表添加家庭成员/EMI心理/大五人格/孩子快照字段
- ensureColumn("dan_assessment_results", "family_member_id", "BIGINT COMMENT '家庭成员ID'");
- ensureColumn("dan_assessment_results", "family_member_name", "VARCHAR(50) COMMENT '成员姓名快照'");
- ensureColumn("dan_assessment_results", "family_member_birthday", "DATE COMMENT '成员出生日期快照'");
- ensureColumn("dan_assessment_results", "family_member_gender", "VARCHAR(10) COMMENT '成员性别快照'");
- ensureColumn("dan_assessment_results", "family_member_age", "INT COMMENT '成员年龄快照'");
- ensureColumn("dan_assessment_results", "emotion_score", "INT COMMENT '情绪商数(EMI)'");
- ensureColumn("dan_assessment_results", "resilience_score", "INT COMMENT '心理韧性(EMI)'");
- ensureColumn("dan_assessment_results", "stress_coping_score", "INT COMMENT '压力应对(EMI)'");
- ensureColumn("dan_assessment_results", "self_awareness_score", "INT COMMENT '自我认知(EMI)'");
- ensureColumn("dan_assessment_results", "openness_score", "INT COMMENT '开放性(大五人格)'");
- ensureColumn("dan_assessment_results", "conscientiousness_score", "INT COMMENT '尽责性(大五人格)'");
- ensureColumn("dan_assessment_results", "extraversion_score", "INT COMMENT '外向性(大五人格)'");
- ensureColumn("dan_assessment_results", "agreeableness_score", "INT COMMENT '宜人性(大五人格)'");
- ensureColumn("dan_assessment_results", "neuroticism_score", "INT COMMENT '神经质(大五人格)'");
- ensureColumn("dan_assessment_results", "big_five_overall_score", "INT COMMENT '大五人格综合分'");
- ensureColumn("dan_assessment_results", "child_name", "VARCHAR(50) COMMENT '孩子姓名快照'");
- ensureColumn("dan_assessment_results", "child_birthday", "DATE COMMENT '出生日期快照'");
- ensureColumn("dan_assessment_results", "child_gender", "VARCHAR(10) COMMENT '性别快照'");
- ensureColumn("dan_assessment_results", "child_age", "INT COMMENT '年龄快照'");
- ensureColumn("dan_assessment_results", "school", "VARCHAR(100) COMMENT '学校快照'");
- ensureColumn("dan_assessment_results", "grade", "VARCHAR(20) COMMENT '年级快照'");
- ensureColumn("dan_assessment_results", "grade_class", "VARCHAR(20) COMMENT '班级快照'");
- ensureColumn("dan_assessment_results", "height", "INT COMMENT '身高(CM)快照'");
- ensureColumn("dan_assessment_results", "weight", "INT COMMENT '体重(KG)快照'");
- ensureColumn("dan_assessment_results", "family_status", "VARCHAR(50) COMMENT '家庭状况快照'");
- ensureColumn("dan_assessment_results", "parent_degree", "VARCHAR(50) COMMENT '父母最高学历快照'");
- ensureColumn("dan_assessment_results", "nation", "VARCHAR(20) COMMENT '民族快照'");
- ensureColumn("dan_assessment_results", "province", "VARCHAR(50) COMMENT '省快照'");
- ensureColumn("dan_assessment_results", "city", "VARCHAR(50) COMMENT '市快照'");
- ensureColumn("dan_assessment_results", "district", "VARCHAR(50) COMMENT '区快照'");
- ensureColumn("dan_assessment_results", "address", "VARCHAR(200) COMMENT '详细地址快照'");
- ensureColumn("dan_assessment_results", "single_child", "VARCHAR(10) COMMENT '是否独生子女快照'");
- ensureColumn("dan_assessment_results", "school_score", "VARCHAR(20) COMMENT '在校成绩快照'");
- ensureColumn("dan_assessment_results", "urgent_name", "VARCHAR(50) COMMENT '紧急联系人姓名'");
- ensureColumn("dan_assessment_results", "urgent_phone", "VARCHAR(20) COMMENT '紧急联系人电话'");
- ensureColumn("dan_assessment_results", "pics", "TEXT COMMENT '图片IDs'");
- ensureColumn("dan_assessment_results", "videos", "TEXT COMMENT '视频IDs'");
- ensureColumn("dan_assessment_results", "audio", "VARCHAR(100) COMMENT '音频ID'");
- ensureColumn("dan_assessment_results", "audio_url", "VARCHAR(255) COMMENT '音频URL'");
- ensureColumn("dan_assessment_results", "process_desc", "TEXT COMMENT '过程描述'");
- ensureColumn("dan_assessment_results", "result_desc", "TEXT COMMENT '结果简评'");
- // 迁移35: points_log表添加family_member_id列(EnergyService.calcChildWealth家庭能量计算需要)
- ensureColumn("points_log", "family_member_id", "BIGINT COMMENT '家庭成员ID'");
- // 迁移36: family_members表添加generation + is_spouse列(辈分重构)
- ensureColumn("family_members", "generation", "INT COMMENT '辈分值(0=家庭创建者,+n=向上n代,-n=向下n代)'");
- ensureColumn("family_members", "is_spouse", "TINYINT(1) DEFAULT 0 COMMENT '是否配偶(同辈)'");
- // 迁移37: relationship_type列已删除,旧数据已迁移完成
- // 迁移38: product_orders表添加points_used/points_cost/money_amount列(混合支付支持)
- ensureColumn("product_orders", "points_used", "INT DEFAULT 0 COMMENT '使用的积分数量'");
- ensureColumn("product_orders", "points_cost", "INT DEFAULT 0 COMMENT '积分抵扣金额(分)'");
- ensureColumn("product_orders", "money_amount", "INT DEFAULT 0 COMMENT '现金支付金额(分)'");
- // 迁移39: 创建供应商体系相关表
- try {
- jdbcTemplate.execute("CREATE TABLE IF NOT EXISTS supply_system (" +
- "id BIGINT AUTO_INCREMENT PRIMARY KEY, " +
- "name VARCHAR(100) NOT NULL COMMENT '体系名称', " +
- "admin_id BIGINT COMMENT '管理员用户ID', " +
- "settlement_period_days INT DEFAULT 30 COMMENT '账期天数', " +
- "platform_profit_rate DECIMAL(10,2) DEFAULT 0.00 COMMENT '平台留利比例(%)', " +
- "description VARCHAR(500) COMMENT '描述', " +
- "status VARCHAR(20) DEFAULT 'active' COMMENT '状态 active/disabled', " +
- "created_at DATETIME DEFAULT CURRENT_TIMESTAMP, " +
- "updated_at DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, " +
- "INDEX idx_admin (admin_id), " +
- "INDEX idx_status (status)" +
- ") ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='供应商体系'");
- log.info("已创建supply_system表");
- } catch (Exception e) {
- log.warn("创建supply_system表失败: {}", e.getMessage());
- }
- try {
- jdbcTemplate.execute("CREATE TABLE IF NOT EXISTS supply_system_member (" +
- "id BIGINT AUTO_INCREMENT PRIMARY KEY, " +
- "system_id BIGINT NOT NULL COMMENT '体系ID', " +
- "user_id BIGINT NOT NULL COMMENT '用户ID', " +
- "role VARCHAR(20) DEFAULT 'member' COMMENT '角色 admin/member', " +
- "status VARCHAR(20) DEFAULT 'active' COMMENT '状态 active/disabled', " +
- "joined_at DATETIME DEFAULT CURRENT_TIMESTAMP, " +
- "INDEX idx_system (system_id), " +
- "INDEX idx_user (user_id), " +
- "UNIQUE INDEX idx_system_user (system_id, user_id)" +
- ") ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='供应商体系成员'");
- log.info("已创建supply_system_member表");
- } catch (Exception e) {
- log.warn("创建supply_system_member表失败: {}", e.getMessage());
- }
- try {
- jdbcTemplate.execute("CREATE TABLE IF NOT EXISTS supply_settlement (" +
- "id BIGINT AUTO_INCREMENT PRIMARY KEY, " +
- "system_id BIGINT NOT NULL COMMENT '体系ID', " +
- "period_start DATE COMMENT '结算周期开始', " +
- "period_end DATE COMMENT '结算周期结束', " +
- "total_sales INT DEFAULT 0 COMMENT '销售总额(分)', " +
- "platform_profit INT DEFAULT 0 COMMENT '平台留利(分)', " +
- "supplier_payout INT DEFAULT 0 COMMENT '供应商实付(分)', " +
- "status VARCHAR(20) DEFAULT 'pending' COMMENT '状态 pending/settled', " +
- "settled_at DATETIME COMMENT '结算时间', " +
- "remark VARCHAR(500) COMMENT '备注', " +
- "created_at DATETIME DEFAULT CURRENT_TIMESTAMP, " +
- "INDEX idx_system (system_id), " +
- "INDEX idx_status (status), " +
- "INDEX idx_period (period_start, period_end)" +
- ") ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='供应商结算单'");
- log.info("已创建supply_settlement表");
- } catch (Exception e) {
- log.warn("创建supply_settlement表失败: {}", e.getMessage());
- }
- try {
- jdbcTemplate.execute("CREATE TABLE IF NOT EXISTS supply_settlement_detail (" +
- "id BIGINT AUTO_INCREMENT PRIMARY KEY, " +
- "settlement_id BIGINT NOT NULL COMMENT '结算单ID', " +
- "order_id BIGINT COMMENT '订单ID', " +
- "order_no VARCHAR(64) COMMENT '订单号', " +
- "product_name VARCHAR(200) COMMENT '商品名称', " +
- "amount INT DEFAULT 0 COMMENT '订单金额(分)', " +
- "platform_fee INT DEFAULT 0 COMMENT '平台留利(分)', " +
- "supplier_id BIGINT COMMENT '收款供应商ID', " +
- "supplier_name VARCHAR(100) COMMENT '收款供应商名称', " +
- "payout INT DEFAULT 0 COMMENT '实付(分)', " +
- "created_at DATETIME DEFAULT CURRENT_TIMESTAMP, " +
- "INDEX idx_settlement (settlement_id), " +
- "INDEX idx_order (order_id)" +
- ") ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='供应商结算明细'");
- log.info("已创建supply_settlement_detail表");
- } catch (Exception e) {
- log.warn("创建supply_settlement_detail表失败: {}", e.getMessage());
- }
- // 迁移40: 添加supplier_admin角色到users.role(列已为VARCHAR,直接可用)
- // 当前role列为VARCHAR(20),无需ALTER ENUM
- // 迁移40: users表添加mascot字段(AI助手形象: xibao浠宝/fubao福宝)
- ensureColumn("users", "mascot", "VARCHAR(20) DEFAULT NULL COMMENT 'AI助手形象: xibao(浠宝)/fubao(福宝)'");
- // 迁移41: 创建family_relationships表(家庭成员双向关系,替代relationship_type/role_override)
- try {
- jdbcTemplate.execute("CREATE TABLE IF NOT EXISTS family_relationships (" +
- "id BIGINT AUTO_INCREMENT PRIMARY KEY, " +
- "family_id BIGINT NOT NULL COMMENT '所属家庭ID', " +
- "from_member_id BIGINT NOT NULL COMMENT '关系起点成员ID', " +
- "to_member_id BIGINT NOT NULL COMMENT '关系终点成员ID', " +
- "generation_value INT NOT NULL COMMENT '辈分值: -1=子女, 0=同辈, 1=父母, 2=祖父母, 3=曾祖父母', " +
- "relationship_key VARCHAR(50) COMMENT '关系标签key(冗余用于显示)', " +
- "created_at DATETIME DEFAULT CURRENT_TIMESTAMP, " +
- "updated_at DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, " +
- "UNIQUE KEY uk_from_to (from_member_id, to_member_id), " +
- "INDEX idx_family_id (family_id), " +
- "INDEX idx_from (from_member_id), " +
- "INDEX idx_to (to_member_id)" +
- ") ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='家庭成员双向关系表'");
- log.info("已创建family_relationships表");
- } catch (Exception e) {
- log.warn("创建family_relationships表可能已存在: {}", e.getMessage());
- }
- // 迁移42: 创建butler_sessions表(AI管家会话管理)
- try {
- jdbcTemplate.execute("CREATE TABLE IF NOT EXISTS butler_sessions (" +
- "id BIGINT AUTO_INCREMENT PRIMARY KEY, " +
- "user_id BIGINT NOT NULL COMMENT '用户ID', " +
- "title VARCHAR(200) DEFAULT '新会话' COMMENT '会话标题', " +
- "conversation_id VARCHAR(100) COMMENT 'Dify对话ID', " +
- "context TEXT COMMENT '上下文信息(JSON)', " +
- "status VARCHAR(20) DEFAULT 'active' COMMENT '状态: active(进行中)/archived(已归档)', " +
- "created_at DATETIME DEFAULT CURRENT_TIMESTAMP, " +
- "updated_at DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, " +
- "INDEX idx_user_id (user_id), " +
- "INDEX idx_status (status), " +
- "INDEX idx_user_status (user_id, status)" +
- ") ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='AI管家会话表'");
- log.info("已创建butler_sessions表");
- } catch (Exception e) {
- log.warn("创建butler_sessions表可能已存在: {}", e.getMessage());
- }
- // 迁移 79: 创建 tianpan_daily_fortune 表(家庭每日运势)
- try {
- jdbcTemplate.execute("CREATE TABLE IF NOT EXISTS tianpan_daily_fortune (" +
- "id BIGINT AUTO_INCREMENT PRIMARY KEY, " +
- "family_id BIGINT NOT NULL COMMENT '家庭ID', " +
- "date VARCHAR(20) NOT NULL COMMENT '日期 YYYY-MM-DD', " +
- "lunar_date VARCHAR(50) COMMENT '农历日期', " +
- "day_ganzhi VARCHAR(10) COMMENT '日柱', " +
- "five_elements_main VARCHAR(20) COMMENT '当日五行主气', " +
- "five_elements_detail VARCHAR(100) COMMENT '五行详情', " +
- "auspicious TEXT COMMENT '宜做的事(JSON数组)', " +
- "inauspicious TEXT COMMENT '忌做的事(JSON数组)', " +
- "family_advice TEXT COMMENT '家庭建议', " +
- "focus_member_name VARCHAR(50) COMMENT '关注成员', " +
- "focus_member_tip TEXT COMMENT '成员提示', " +
- "wuxing_tip TEXT COMMENT '五行提示', " +
- "created_at DATETIME DEFAULT CURRENT_TIMESTAMP, " +
- "updated_at DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, " +
- "UNIQUE KEY uk_family_date (family_id, date), " +
- "INDEX idx_family_id (family_id), " +
- "INDEX idx_date (date)" +
- ") ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='家庭每日运势表'");
- log.info("已创建 tianpan_daily_fortune 表");
- } catch (Exception e) {
- log.warn("创建 tianpan_daily_fortune 表可能已存在: {}", e.getMessage());
- }
- // 迁移 80: 创建 tianpan_member_annual_energy 表(成员年度能量)
- try {
- jdbcTemplate.execute("CREATE TABLE IF NOT EXISTS tianpan_member_annual_energy (" +
- "id BIGINT AUTO_INCREMENT PRIMARY KEY, " +
- "member_id BIGINT NOT NULL COMMENT '成员ID', " +
- "year INT NOT NULL COMMENT '年份', " +
- "dimensions_body INT DEFAULT 0 COMMENT '身能量', " +
- "dimensions_mind INT DEFAULT 0 COMMENT '心能量', " +
- "dimensions_wisdom INT DEFAULT 0 COMMENT '智能量', " +
- "dimensions_action INT DEFAULT 0 COMMENT '行能量', " +
- "dimensions_wealth INT DEFAULT 0 COMMENT '富能量', " +
- "jupiter_phase VARCHAR(50) COMMENT '木星相位', " +
- "saturn_phase VARCHAR(50) COMMENT '土星相位', " +
- "highlights TEXT COMMENT '年度亮点(JSON数组)', " +
- "created_at DATETIME DEFAULT CURRENT_TIMESTAMP, " +
- "updated_at DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, " +
- "UNIQUE KEY uk_member_year (member_id, year), " +
- "INDEX idx_member_id (member_id), " +
- "INDEX idx_year (year)" +
- ") ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='成员年度能量表'");
- log.info("已创建 tianpan_member_annual_energy 表");
- } catch (Exception e) {
- log.warn("创建 tianpan_member_annual_energy 表可能已存在: {}", e.getMessage());
- }
- }
- private void insertSysConfigSeed(String key, String value, String desc) {
- Integer count = jdbcTemplate.queryForObject(
- "SELECT COUNT(*) FROM sys_config WHERE config_key = ?", Integer.class, key);
- if (count == null || count == 0) {
- jdbcTemplate.update(
- "INSERT INTO sys_config (config_key, config_value, description, updated_at) VALUES (?, ?, ?, NOW())",
- key, value, desc);
- }
- }
- private void initFoodTables() {
- log.info("开始初始化食品相关表...");
- // 4张新表 (CREATE TABLE IF NOT EXISTS)
- String[] ddl = {
- "CREATE TABLE IF NOT EXISTS foods ("
- + "id BIGINT AUTO_INCREMENT PRIMARY KEY, "
- + "name VARCHAR(100) NOT NULL COMMENT '食材名称', "
- + "category VARCHAR(50) COMMENT '分类: 蔬菜/水果/肉禽/水产/蛋奶/豆类/谷物/调味/其他', "
- + "unit VARCHAR(20) DEFAULT 'g' COMMENT '计量单位', "
- + "calories DECIMAL(8,2) COMMENT '热量(kcal/100g)', "
- + "protein DECIMAL(8,2) COMMENT '蛋白质(g/100g)', "
- + "fat DECIMAL(8,2) COMMENT '脂肪(g/100g)', "
- + "carbs DECIMAL(8,2) COMMENT '碳水(g/100g)', "
- + "fiber DECIMAL(8,2) COMMENT '膳食纤维(g/100g)', "
- + "energy_kj DECIMAL(8,2) COMMENT '能量(kJ/100g)', "
- + "starch DECIMAL(8,2) COMMENT '淀粉(g/100g)', "
- + "cholesterol DECIMAL(8,2) COMMENT '胆固醇(mg/100g)', "
- + "price_level TINYINT COMMENT '价格等级 1-5', "
- + "nutrition_tags VARCHAR(500) COMMENT '营养标签JSON', "
- + "suitable_for VARCHAR(200) COMMENT '适宜人群JSON', "
- + "allergens VARCHAR(200) COMMENT '过敏原JSON', "
- + "external_source VARCHAR(100) COMMENT '数据来源', "
- + "external_id VARCHAR(100) COMMENT '外部ID', "
- + "cover_image VARCHAR(500) COMMENT '食材图片', "
- + "report_id BIGINT COMMENT '关联报告ID(从PDF解析的食物推荐)', "
- + "score INT COMMENT '食物评分', "
- + "diet_type VARCHAR(50) COMMENT '饮食类型', "
- + "status VARCHAR(20) DEFAULT 'active', "
- + "sort_order INT DEFAULT 0, "
- + "created_at DATETIME DEFAULT CURRENT_TIMESTAMP, "
- + "updated_at DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, "
- + "INDEX idx_category (category), "
- + "INDEX idx_price (price_level)"
- + ");",
- "CREATE TABLE IF NOT EXISTS recipes ("
- + "id BIGINT AUTO_INCREMENT PRIMARY KEY, "
- + "name VARCHAR(200) NOT NULL COMMENT '食谱名称', "
- + "meal_type VARCHAR(20) COMMENT '早餐/午餐/晚餐/加餐', "
- + "description TEXT COMMENT '简介', "
- + "ingredients TEXT NOT NULL COMMENT '食材清单JSON', "
- + "steps TEXT COMMENT '烹饪步骤JSON', "
- + "cook_time INT COMMENT '烹饪时间(分钟)', "
- + "difficulty VARCHAR(20) COMMENT '难度: 简单/中等/困难', "
- + "total_calories DECIMAL(8,2) COMMENT '总热量', "
- + "total_protein DECIMAL(8,2) COMMENT '总蛋白质', "
- + "total_fat DECIMAL(8,2) COMMENT '总脂肪', "
- + "total_carbs DECIMAL(8,2) COMMENT '总碳水', "
- + "total_fiber DECIMAL(8,2) COMMENT '总膳食纤维', "
- + "suitable_for VARCHAR(200) COMMENT '适宜场景', "
- + "nutrition_tags VARCHAR(500) COMMENT '营养标签', "
- + "external_source VARCHAR(100), "
- + "external_id VARCHAR(100), "
- + "cover_image VARCHAR(500), "
- + "status VARCHAR(20) DEFAULT 'active', "
- + "created_at DATETIME DEFAULT CURRENT_TIMESTAMP, "
- + "updated_at DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, "
- + "INDEX idx_meal_type (meal_type), "
- + "INDEX idx_cook_time (cook_time)"
- + ");",
- "CREATE TABLE IF NOT EXISTS seasonal_foods ("
- + "id BIGINT AUTO_INCREMENT PRIMARY KEY, "
- + "food_id BIGINT NOT NULL COMMENT '关联foods.id', "
- + "month TINYINT NOT NULL COMMENT '月份 1-12', "
- + "region VARCHAR(50) DEFAULT '全国' COMMENT '产地', "
- + "peak_flag TINYINT DEFAULT 0 COMMENT '盛产标志', "
- + "sort_order INT DEFAULT 0, "
- + "created_at DATETIME DEFAULT CURRENT_TIMESTAMP, "
- + "INDEX idx_food_month (food_id, month), "
- + "INDEX idx_month (month)"
- + ");",
- "CREATE TABLE IF NOT EXISTS meal_logs ("
- + "id BIGINT AUTO_INCREMENT PRIMARY KEY, "
- + "user_id BIGINT NOT NULL COMMENT '用户ID', "
- + "child_id BIGINT COMMENT '孩子ID', "
- + "meal_type VARCHAR(20) NOT NULL COMMENT '早餐/午餐/晚餐/加餐', "
- + "meal_date DATE NOT NULL COMMENT '用餐日期', "
- + "meal_time DATETIME COMMENT '具体时间', "
- + "foods TEXT NOT NULL COMMENT '食物清单JSON', "
- + "total_calories DECIMAL(8,2) COMMENT '总热量', "
- + "total_protein DECIMAL(8,2) COMMENT '总蛋白质', "
- + "total_fat DECIMAL(8,2) COMMENT '总脂肪', "
- + "total_carbs DECIMAL(8,2) COMMENT '总碳水', "
- + "total_fiber DECIMAL(8,2) COMMENT '总膳食纤维', "
- + "note TEXT COMMENT '备注', "
- + "images VARCHAR(1000) COMMENT '图片JSON', "
- + "created_at DATETIME DEFAULT CURRENT_TIMESTAMP, "
- + "INDEX idx_user_date (user_id, meal_date), "
- + "INDEX idx_child_date (child_id, meal_date)"
- + ");",
- "CREATE TABLE IF NOT EXISTS report_food_suitability ("
- + "id BIGINT AUTO_INCREMENT PRIMARY KEY, "
- + "report_id BIGINT NOT NULL COMMENT '健康报告ID', "
- + "food_id BIGINT COMMENT '食材ID', "
- + "food_name VARCHAR(100) NOT NULL COMMENT '食材名称', "
- + "category VARCHAR(50) COMMENT '分类', "
- + "score INT NOT NULL COMMENT '适宜度评分', "
- + "created_at DATETIME DEFAULT CURRENT_TIMESTAMP, "
- + "INDEX idx_report (report_id), "
- + "INDEX idx_food (food_id)"
- + ");",
- "CREATE TABLE IF NOT EXISTS user_food_preference ("
- + "id BIGINT AUTO_INCREMENT PRIMARY KEY, "
- + "user_id BIGINT NOT NULL COMMENT '用户ID', "
- + "food_id BIGINT COMMENT '食材ID', "
- + "food_name VARCHAR(100) NOT NULL COMMENT '食材名称', "
- + "score INT NOT NULL DEFAULT 0 COMMENT '评分', "
- + "reason VARCHAR(500) COMMENT '原因', "
- + "created_at DATETIME DEFAULT CURRENT_TIMESTAMP, "
- + "updated_at DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, "
- + "INDEX idx_user (user_id)"
- + ");",
- "CREATE TABLE IF NOT EXISTS food_nutrients ("
- + "id BIGINT AUTO_INCREMENT PRIMARY KEY, "
- + "food_id BIGINT NOT NULL COMMENT '食材ID', "
- + "nutrient_code VARCHAR(20) NOT NULL COMMENT '营养素编码', "
- + "amount_per_100g DECIMAL(10,4) NOT NULL COMMENT '每100g含量', "
- + "unit VARCHAR(20) NOT NULL COMMENT '单位', "
- + "source VARCHAR(50) DEFAULT 'importer' COMMENT '数据来源', "
- + "created_at DATETIME DEFAULT CURRENT_TIMESTAMP, "
- + "updated_at DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, "
- + "UNIQUE KEY uk_food_nutrient (food_id, nutrient_code), "
- + "INDEX idx_nutrient (nutrient_code)"
- + ");",
- "CREATE TABLE IF NOT EXISTS rdi_standards ("
- + "id BIGINT AUTO_INCREMENT PRIMARY KEY, "
- + "nutrient_code VARCHAR(20) NOT NULL COMMENT '营养素编码', "
- + "age_group VARCHAR(10) NOT NULL COMMENT '年龄段', "
- + "gender VARCHAR(6) DEFAULT 'all' COMMENT '性别', "
- + "rdi_type VARCHAR(10) NOT NULL COMMENT '参考类型', "
- + "amount DECIMAL(10,4) NOT NULL COMMENT '推荐量', "
- + "unit VARCHAR(20) NOT NULL COMMENT '单位', "
- + "source VARCHAR(100) COMMENT '数据来源', "
- + "created_at DATETIME DEFAULT CURRENT_TIMESTAMP, "
- + "UNIQUE KEY uk_rdi (nutrient_code, age_group, gender, rdi_type), "
- + "INDEX idx_age (age_group)"
- + ");",
- "CREATE TABLE IF NOT EXISTS shopping_lists ("
- + "id BIGINT AUTO_INCREMENT PRIMARY KEY, "
- + "user_id BIGINT NOT NULL COMMENT '用户ID', "
- + "child_id BIGINT COMMENT '孩子ID', "
- + "report_id BIGINT COMMENT '健康报告ID', "
- + "title VARCHAR(200) COMMENT '标题', "
- + "source VARCHAR(20) NOT NULL COMMENT '来源', "
- + "status VARCHAR(20) DEFAULT 'active' COMMENT '状态', "
- + "total_items INT DEFAULT 0 COMMENT '总项数', "
- + "purchased_items INT DEFAULT 0 COMMENT '已购项数', "
- + "created_at DATETIME DEFAULT CURRENT_TIMESTAMP, "
- + "updated_at DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, "
- + "INDEX idx_user (user_id), "
- + "INDEX idx_report (report_id), "
- + "INDEX idx_status (status)"
- + ");",
- "CREATE TABLE IF NOT EXISTS shopping_list_items ("
- + "id BIGINT AUTO_INCREMENT PRIMARY KEY, "
- + "list_id BIGINT NOT NULL COMMENT '购物清单ID', "
- + "food_id BIGINT COMMENT '食材ID', "
- + "food_name VARCHAR(100) NOT NULL COMMENT '食材名称', "
- + "category VARCHAR(50) COMMENT '分类', "
- + "quantity DECIMAL(8,2) DEFAULT 1 COMMENT '数量', "
- + "unit VARCHAR(20) DEFAULT '份' COMMENT '单位', "
- + "priority VARCHAR(10) DEFAULT 'medium' COMMENT '优先级', "
- + "reason VARCHAR(200) COMMENT '推荐原因', "
- + "score INT COMMENT '评分', "
- + "source VARCHAR(20) COMMENT '来源', "
- + "purchased TINYINT DEFAULT 0 COMMENT '是否已购', "
- + "purchased_at DATETIME COMMENT '购买时间', "
- + "sort_order INT DEFAULT 0 COMMENT '排序', "
- + "created_at DATETIME DEFAULT CURRENT_TIMESTAMP, "
- + "INDEX idx_list (list_id), "
- + "INDEX idx_purchased (purchased)"
- + ");",
- "CREATE TABLE IF NOT EXISTS follow_up ("
- + "id BIGINT AUTO_INCREMENT PRIMARY KEY, "
- + "family_id BIGINT NOT NULL COMMENT '家庭ID', "
- + "child_id BIGINT COMMENT '孩子ID', "
- + "teacher_id BIGINT COMMENT '规划师ID', "
- + "deficiency_id BIGINT COMMENT '缺乏项ID', "
- + "message_id BIGINT COMMENT '消息ID', "
- + "status VARCHAR(20) DEFAULT 'pending' COMMENT '状态', "
- + "priority VARCHAR(10) DEFAULT 'medium' COMMENT '优先级', "
- + "description VARCHAR(500) COMMENT '描述', "
- + "parent_feedback TEXT COMMENT '家长反馈', "
- + "teacher_review VARCHAR(500) COMMENT '规划师点评', "
- + "due_date DATETIME COMMENT '截止日期', "
- + "completed_at DATETIME COMMENT '完成时间', "
- + "created_at DATETIME DEFAULT CURRENT_TIMESTAMP, "
- + "updated_at DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, "
- + "INDEX idx_family (family_id), "
- + "INDEX idx_teacher (teacher_id), "
- + "INDEX idx_status (status), "
- + "INDEX idx_due (due_date)"
- + ");"
- };
- for (String sql : ddl) {
- try {
- jdbcTemplate.execute(sql);
- } catch (Exception e) {
- log.warn("食品表DDL执行失败(可能已存在): {}", e.getMessage());
- }
- }
- // 扩展 user_nutrition_profile(如果表不存在则跳过)
- ensureColumn("user_nutrition_profile", "budget_monthly", "INT COMMENT '月食品预算(元)'");
- ensureColumn("user_nutrition_profile", "family_taste_preferences", "TEXT COMMENT '家庭成员口味偏好JSON'");
- ensureColumn("user_nutrition_profile", "cuisine_style", "VARCHAR(100) COMMENT '偏好菜系'");
- ensureColumn("user_nutrition_profile", "meal_count", "INT DEFAULT 3 COMMENT '每日餐数'");
- ensureColumn("user_nutrition_profile", "cooking_ability", "VARCHAR(20) DEFAULT 'simple' COMMENT '烹饪能力: simple/medium/advanced'");
- log.info("食品相关表初始化完成");
- }
- private boolean tableExists(String tableName) {
- try {
- Integer count = jdbcTemplate.queryForObject(
- "SELECT COUNT(*) FROM information_schema.TABLES WHERE TABLE_SCHEMA = DATABASE() AND TABLE_NAME = ?",
- Integer.class, tableName);
- return count != null && count > 0;
- } catch (Exception e) {
- return false;
- }
- }
- private boolean columnExists(String table, String column) {
- try {
- Integer count = jdbcTemplate.queryForObject(
- "SELECT COUNT(*) FROM information_schema.COLUMNS WHERE TABLE_SCHEMA = DATABASE() AND TABLE_NAME = ? AND COLUMN_NAME = ?",
- Integer.class, table, column);
- return count != null && count > 0;
- } catch (Exception e) {
- return false;
- }
- }
- private void ensureColumn(String table, String column, String definition) {
- try {
- jdbcTemplate.execute("ALTER TABLE " + table + " ADD COLUMN " + column + " " + definition);
- log.info("已添加列 {}.{}", table, column);
- } catch (Exception e) {
- String msg = e.getMessage();
- if (msg != null && (msg.contains("Duplicate") || msg.contains("already exists"))) {
- log.info("列 {}.{} 已存在,跳过", table, column);
- } else {
- log.error("添加列 {}.{} 失败:{}", table, column, msg);
- throw e;
- }
- }
- }
- /** 幂等创建任务模板包,返回 package id */
- private Long ensureTemplatePackage(String name, String desc, int cycleDays, String category) {
- Integer cnt = jdbcTemplate.queryForObject(
- "SELECT COUNT(*) FROM task_template_packages WHERE name = ?", Integer.class, name);
- if (cnt != null && cnt > 0) {
- return jdbcTemplate.queryForObject(
- "SELECT id FROM task_template_packages WHERE name = ? LIMIT 1", Long.class, name);
- }
- jdbcTemplate.update(
- "INSERT INTO task_template_packages (creator_id, name, description, cycle_days, category, is_public, status, created_at, updated_at) " +
- "VALUES (1, ?, ?, ?, ?, 1, 'published', NOW(), NOW())",
- name, desc, cycleDays, category);
- return jdbcTemplate.queryForObject("SELECT LAST_INSERT_ID()", Long.class);
- }
- /** 幂等写入模板包任务项 */
- private void ensureTemplateItems(Long packageId, String[][] items) {
- Integer cnt = jdbcTemplate.queryForObject(
- "SELECT COUNT(*) FROM task_template_items WHERE package_id = ?", Integer.class, packageId);
- if (cnt != null && cnt > 0) return;
- for (String[] it : items) {
- jdbcTemplate.update(
- "INSERT INTO task_template_items (package_id, task_name, description, points, frequency, sort_order, created_at, updated_at) " +
- "VALUES (?, ?, ?, ?, ?, 0, NOW(), NOW())",
- packageId, it[0], it[1], Integer.valueOf(it[2]), it[3]);
- }
- }
- /** 幂等写入方案生成规则 */
- private void ensurePlanRule(String dimension, int scoreMin, int scoreMax, Long templateId, int priority) {
- Integer cnt = jdbcTemplate.queryForObject(
- "SELECT COUNT(*) FROM assessment_plan_rules WHERE dimension = ? AND score_min = ? AND score_max = ?",
- Integer.class, dimension, scoreMin, scoreMax);
- if (cnt != null && cnt > 0) return;
- jdbcTemplate.update(
- "INSERT INTO assessment_plan_rules (dimension, score_min, score_max, template_id, priority, is_active, created_at, updated_at) " +
- "VALUES (?, ?, ?, ?, ?, 1, NOW(), NOW())",
- dimension, scoreMin, scoreMax, templateId, priority);
- }
- private void insertProductSeed(String name, String desc, String type,
- Integer price,
- String domain, String memberEligible) {
- Integer count = jdbcTemplate.queryForObject(
- "SELECT COUNT(*) FROM products WHERE name = ? AND product_type = ?", Integer.class, name, type);
- if (count == null || count == 0) {
- jdbcTemplate.update(
- "INSERT INTO products (name, description, price, product_type, vendor_id, vendor_name, status, domain, member_eligible, created_at, updated_at) " +
- "VALUES (?, ?, ?, ?, NULL, '平台官方', 'on_shelf', ?, ?, NOW(), NOW())",
- name, desc, price, type, domain, memberEligible);
- }
- }
- private void insertActivitySeed(String title, String desc, String dimensionCode,
- String activityType, String status,
- String startTime, String endTime, String location,
- int maxParticipants, int price, int memberPrice,
- String dimensionWeights) {
- Integer count = jdbcTemplate.queryForObject(
- "SELECT COUNT(*) FROM activities WHERE title = ?", Integer.class, title);
- if (count == null || count == 0) {
- jdbcTemplate.update(
- "INSERT INTO activities (title, description, dimension_code, dimension_weights, activity_type, status, " +
- "start_time, end_time, location, max_participants, current_participants, price, member_price, " +
- "require_registration, checkin_points, created_at, updated_at) " +
- "VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, 0, ?, ?, 1, 10, NOW(), NOW())",
- title, desc, dimensionCode, dimensionWeights, activityType, status,
- startTime, endTime, location, maxParticipants, price, memberPrice);
- }
- }
- private void insertArticleSeed(String title, String summary, String contentType,
- Long categoryId, String dimensionCode, String status,
- String author, String articleType, int readTime, int wordCount,
- String dimensionWeights) {
- Integer count = jdbcTemplate.queryForObject(
- "SELECT COUNT(*) FROM articles WHERE title = ?", Integer.class, title);
- if (count == null || count == 0) {
- jdbcTemplate.update(
- "INSERT INTO articles (title, summary, content, content_type, category_id, " +
- "related_dimensions, dimension_weights, status, author, article_type, " +
- "read_time, word_count, is_featured, view_count, author_type, audit_status, " +
- "visibility, created_at, updated_at) " +
- "VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, 1, 0, 'admin', 'approved', 'public', NOW(), NOW())",
- title, summary, "(此处为示例文章正文内容,实际数据应在运营后台编辑发布。)",
- contentType, categoryId, "[\"" + dimensionCode + "\"]", dimensionWeights,
- status, author, articleType, readTime, wordCount);
- }
- }
- private void insertContentSectionSeed(String pageKey, String sectionKey, String title,
- String allowedRoles, int sortOrder) {
- Integer count = jdbcTemplate.queryForObject(
- "SELECT COUNT(*) FROM content_sections WHERE page_key = ? AND section_key = ?",
- Integer.class, pageKey, sectionKey);
- if (count == null || count == 0) {
- jdbcTemplate.update(
- "INSERT INTO content_sections (page_key, section_key, title, allowed_roles, sort_order, status, created_at, updated_at) " +
- "VALUES (?, ?, ?, ?, ?, 'active', NOW(), NOW())",
- pageKey, sectionKey, title, allowedRoles, sortOrder);
- }
- }
- private void runLateMigrations() {
- // 迁移51: 创建 member_subscription 表(家庭订阅记录)
- try {
- jdbcTemplate.execute("CREATE TABLE IF NOT EXISTS member_subscription (" +
- "id BIGINT AUTO_INCREMENT PRIMARY KEY, " +
- "family_id BIGINT NOT NULL COMMENT '家庭ID', " +
- "`level` VARCHAR(10) NOT NULL DEFAULT 'L1' COMMENT 'L1=一生一世, L2=久久一生', " +
- "status VARCHAR(20) NOT NULL DEFAULT 'active' COMMENT 'active/expired/cancelled', " +
- "start_time DATETIME DEFAULT NULL COMMENT '开始时间', " +
- "expire_time DATETIME DEFAULT NULL COMMENT '到期时间', " +
- "payment_id BIGINT DEFAULT NULL COMMENT '关联订单ID', " +
- "auto_renew TINYINT(1) DEFAULT 0 COMMENT '自动续费(L2)', " +
- "created_at DATETIME DEFAULT CURRENT_TIMESTAMP, " +
- "updated_at DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, " +
- "INDEX idx_family_id (family_id), " +
- "INDEX idx_status (status), " +
- "INDEX idx_expire_time (expire_time)" +
- ") ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='家庭订阅记录'");
- log.info("已创建member_subscription表");
- } catch (Exception e) {
- log.warn("创建member_subscription表失败: {}", e.getMessage());
- }
- // 迁移52: 创建 member_subscription_order 表(订阅订单)
- try {
- jdbcTemplate.execute("CREATE TABLE IF NOT EXISTS member_subscription_order (" +
- "id BIGINT AUTO_INCREMENT PRIMARY KEY, " +
- "family_id BIGINT NOT NULL COMMENT '家庭ID', " +
- "`level` VARCHAR(10) NOT NULL COMMENT 'L1/L2', " +
- "amount INT NOT NULL COMMENT '金额(分)', " +
- "status VARCHAR(20) NOT NULL DEFAULT 'pending' COMMENT 'pending/paid/cancelled/refunded', " +
- "payment_type VARCHAR(20) DEFAULT NULL COMMENT '支付方式 wechat/alipay', " +
- "transaction_id VARCHAR(64) DEFAULT NULL COMMENT '微信支付订单号', " +
- "order_no VARCHAR(64) NOT NULL COMMENT '订单号', " +
- "created_at DATETIME DEFAULT CURRENT_TIMESTAMP, " +
- "paid_at DATETIME DEFAULT NULL, " +
- "UNIQUE KEY uk_order_no (order_no), " +
- "INDEX idx_family_id (family_id), " +
- "INDEX idx_status (status)" +
- ") ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='订阅订单'");
- log.info("已创建member_subscription_order表");
- } catch (Exception e) {
- log.warn("创建member_subscription_order表失败: {}", e.getMessage());
- }
- // 迁移53: 创建 subscription_benefit_log 表(权益使用日志)
- try {
- jdbcTemplate.execute("CREATE TABLE IF NOT EXISTS subscription_benefit_log (" +
- "id BIGINT AUTO_INCREMENT PRIMARY KEY, " +
- "subscription_id BIGINT NOT NULL COMMENT '订阅ID', " +
- "benefit_code VARCHAR(50) NOT NULL COMMENT '权益代码', " +
- "used_at DATETIME DEFAULT CURRENT_TIMESTAMP, " +
- "detail TEXT COMMENT '使用详情(JSON)', " +
- "INDEX idx_subscription_id (subscription_id)" +
- ") ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='权益使用日志'");
- log.info("已创建subscription_benefit_log表");
- } catch (Exception e) {
- log.warn("创建subscription_benefit_log表失败: {}", e.getMessage());
- }
- // 迁移54: 创建 promotion_tier 表(推广等级 R0-R4)
- try {
- jdbcTemplate.execute("CREATE TABLE IF NOT EXISTS promotion_tier (" +
- "id BIGINT AUTO_INCREMENT PRIMARY KEY, " +
- "user_id BIGINT NOT NULL COMMENT '用户ID', " +
- "tier VARCHAR(20) NOT NULL DEFAULT 'R0' COMMENT 'R0利他者/R1传福人/R2聚能师/R3启慧者/R4传承者', " +
- "team_size_1st INT DEFAULT 0 COMMENT '一层团队人数', " +
- "team_size_2nd INT DEFAULT 0 COMMENT '二层团队人数', " +
- "team_size_3rd INT DEFAULT 0 COMMENT '三层团队人数', " +
- "total_team_size INT DEFAULT 0 COMMENT '三层累计总人数', " +
- "total_referral_earnings INT DEFAULT 0 COMMENT '累计推荐佣金(分)', " +
- "total_share_earnings INT DEFAULT 0 COMMENT '累计消费返利(分)', " +
- "promoted_at DATETIME DEFAULT NULL COMMENT '最近晋级时间', " +
- "last_change_at DATETIME DEFAULT NULL COMMENT '最后变更时间', " +
- "created_at DATETIME DEFAULT CURRENT_TIMESTAMP, " +
- "updated_at DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, " +
- "UNIQUE KEY uk_user_id (user_id), " +
- "INDEX idx_tier (tier)" +
- ") ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='推广等级'");
- log.info("已创建promotion_tier表");
- } catch (Exception e) {
- log.warn("创建promotion_tier表失败: {}", e.getMessage());
- }
- // 迁移55: 创建 promotion_tier_change_log 表(推广等级变更记录)
- try {
- jdbcTemplate.execute("CREATE TABLE IF NOT EXISTS promotion_tier_change_log (" +
- "id BIGINT AUTO_INCREMENT PRIMARY KEY, " +
- "user_id BIGINT NOT NULL COMMENT '用户ID', " +
- "old_tier VARCHAR(20) DEFAULT NULL COMMENT '变更前等级', " +
- "new_tier VARCHAR(20) NOT NULL COMMENT '变更后等级', " +
- "reason VARCHAR(50) DEFAULT NULL COMMENT 'qualify升级/demote降级/admin管理员调整', " +
- "changed_at DATETIME DEFAULT CURRENT_TIMESTAMP, " +
- "INDEX idx_user_id (user_id)" +
- ") ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='推广等级变更记录'");
- log.info("已创建promotion_tier_change_log表");
- } catch (Exception e) {
- log.warn("创建promotion_tier_change_log表失败: {}", e.getMessage());
- }
- // 迁移56: 创建 referral_tree 表(推荐关系树三层)
- try {
- jdbcTemplate.execute("CREATE TABLE IF NOT EXISTS referral_tree (" +
- "id BIGINT AUTO_INCREMENT PRIMARY KEY, " +
- "parent_id BIGINT NOT NULL COMMENT '推荐人ID', " +
- "child_id BIGINT NOT NULL COMMENT '被推荐人ID', " +
- "level INT NOT NULL COMMENT '层级深度(1/2/3)', " +
- "path VARCHAR(255) DEFAULT NULL COMMENT 'materialized path如/1/2/3/', " +
- "created_at DATETIME DEFAULT CURRENT_TIMESTAMP, " +
- "UNIQUE KEY uk_child_id (child_id), " +
- "INDEX idx_parent_id (parent_id), " +
- "INDEX idx_level (level)" +
- ") ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='推荐关系树'");
- log.info("已创建referral_tree表");
- } catch (Exception e) {
- log.warn("创建referral_tree表失败: {}", e.getMessage());
- }
- // 迁移57: 创建 product_ppoint 表(产品P点配置)
- try {
- jdbcTemplate.execute("CREATE TABLE IF NOT EXISTS product_ppoint (" +
- "id BIGINT AUTO_INCREMENT PRIMARY KEY, " +
- "product_id BIGINT NOT NULL COMMENT '产品ID', " +
- "ppoint INT NOT NULL DEFAULT 0 COMMENT 'P点值(平台利润分)', " +
- "start_date DATE DEFAULT NULL COMMENT '生效日期', " +
- "end_date DATE DEFAULT NULL COMMENT '失效日期', " +
- "created_by BIGINT DEFAULT NULL COMMENT '创建人', " +
- "created_at DATETIME DEFAULT CURRENT_TIMESTAMP, " +
- "INDEX idx_product_id (product_id), " +
- "INDEX idx_dates (start_date, end_date)" +
- ") ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='产品P点配置'");
- log.info("已创建product_ppoint表");
- } catch (Exception e) {
- log.warn("创建product_ppoint表失败: {}", e.getMessage());
- }
- // 迁移58: 创建 assessment_products 表(测评商品扩展信息)
- try {
- jdbcTemplate.execute("CREATE TABLE IF NOT EXISTS assessment_products (" +
- "id BIGINT AUTO_INCREMENT PRIMARY KEY, " +
- "product_id BIGINT NOT NULL COMMENT '关联products表', " +
- "assessment_type VARCHAR(20) NOT NULL COMMENT 'single=单包, bundle=套餐', " +
- "total_sessions INT NOT NULL DEFAULT 1 COMMENT '包含测评次数', " +
- "validity_days INT NOT NULL DEFAULT 365 COMMENT '有效期(天)', " +
- "guide_scope VARCHAR(50) DEFAULT 'all' COMMENT '规划师范围: all/assigned/auto', " +
- "created_at DATETIME DEFAULT CURRENT_TIMESTAMP, " +
- "updated_at DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, " +
- "INDEX idx_product_id (product_id)" +
- ") ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='测评商品扩展信息'");
- log.info("已创建assessment_products表");
- } catch (Exception e) {
- log.warn("创建assessment_products表失败: {}", e.getMessage());
- }
- // 迁移59: 创建 assessment_quotas 表(测评额度)
- try {
- jdbcTemplate.execute("CREATE TABLE IF NOT EXISTS assessment_quotas (" +
- "id BIGINT AUTO_INCREMENT PRIMARY KEY, " +
- "quota_no VARCHAR(32) NOT NULL COMMENT '额度编号', " +
- "family_id BIGINT NOT NULL COMMENT '家庭ID', " +
- "child_id BIGINT COMMENT '指定孩子ID(NULL=不限)', " +
- "product_order_id BIGINT NOT NULL COMMENT '来源订单ID', " +
- "product_id BIGINT NOT NULL COMMENT '商品ID', " +
- "product_name VARCHAR(100) NOT NULL COMMENT '商品名快照', " +
- "total_sessions INT NOT NULL DEFAULT 1 COMMENT '总次数', " +
- "remaining_sessions INT NOT NULL DEFAULT 1 COMMENT '剩余次数', " +
- "validity_start DATETIME NOT NULL COMMENT '有效期开始', " +
- "validity_end DATETIME NOT NULL COMMENT '有效期截止', " +
- "status VARCHAR(20) NOT NULL DEFAULT 'active' COMMENT 'active/expired/used_all/cancelled', " +
- "created_at DATETIME DEFAULT CURRENT_TIMESTAMP, " +
- "updated_at DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, " +
- "INDEX idx_family (family_id), " +
- "INDEX idx_order (product_order_id), " +
- "UNIQUE KEY uk_quota_no (quota_no)" +
- ") ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='测评额度表'");
- log.info("已创建assessment_quotas表");
- } catch (Exception e) {
- log.warn("创建assessment_quotas表失败: {}", e.getMessage());
- }
- // 迁移60: 创建 assessment_executions 表(测评执行记录)
- try {
- jdbcTemplate.execute("CREATE TABLE IF NOT EXISTS assessment_executions (" +
- "id BIGINT AUTO_INCREMENT PRIMARY KEY, " +
- "quota_id BIGINT NOT NULL COMMENT '关联额度ID', " +
- "child_id BIGINT NOT NULL COMMENT '被测评孩子', " +
- "guide_id BIGINT COMMENT '执行规划师ID', " +
- "assessment_type VARCHAR(30) COMMENT '本次具体测评类型', " +
- "appointment_id BIGINT COMMENT '关联预约ID', " +
- "result_id BIGINT COMMENT '关联测评结果ID', " +
- "status VARCHAR(20) NOT NULL DEFAULT 'pending' COMMENT 'pending/in_progress/completed/cancelled', " +
- "scheduled_date DATETIME COMMENT '预约执行时间', " +
- "completed_at DATETIME COMMENT '实际完成时间', " +
- "notes VARCHAR(500) COMMENT '备注', " +
- "created_at DATETIME DEFAULT CURRENT_TIMESTAMP, " +
- "updated_at DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, " +
- "INDEX idx_quota (quota_id), " +
- "INDEX idx_child (child_id), " +
- "INDEX idx_appointment (appointment_id)" +
- ") ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='测评执行记录表'");
- log.info("已创建assessment_executions表");
- } catch (Exception e) {
- log.warn("创建assessment_executions表失败: {}", e.getMessage());
- }
- // 迁移61: dan_assessment_results表添加source字段(报告来源)
- ensureColumn("dan_assessment_results", "source",
- "VARCHAR(20) DEFAULT 'planner_entry' COMMENT '报告来源: planner_entry/parent_upload/auto_fetch'");
- // 迁移62: 创建 growth_guidances 表(结构化指导意见)
- try {
- jdbcTemplate.execute("CREATE TABLE IF NOT EXISTS growth_guidances (" +
- "id BIGINT AUTO_INCREMENT PRIMARY KEY, " +
- "result_id BIGINT NOT NULL COMMENT '关联测评结果ID', " +
- "dimension VARCHAR(30) NOT NULL COMMENT '维度: attention/focus/memory/logic/emotion/general', " +
- "score INT COMMENT '该维度得分(0-100)', " +
- "suggestion TEXT NOT NULL COMMENT '建议内容', " +
- "priority INT DEFAULT 0 COMMENT '优先级(0=普通, 1=重要, 2=紧急)', " +
- "sort_order INT DEFAULT 0 COMMENT '排序', " +
- "created_at DATETIME DEFAULT CURRENT_TIMESTAMP, " +
- "updated_at DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, " +
- "INDEX idx_result (result_id)" +
- ") ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='测评指导意见(结构化)'");
- log.info("已创建growth_guidances表");
- } catch (Exception e) {
- log.warn("创建growth_guidances表失败: {}", e.getMessage());
- }
- // 迁移63: 创建 assessment_plan_rules 表(方案生成规则)
- try {
- jdbcTemplate.execute("CREATE TABLE IF NOT EXISTS assessment_plan_rules (" +
- "id BIGINT AUTO_INCREMENT PRIMARY KEY, " +
- "dimension VARCHAR(30) NOT NULL COMMENT '测评维度: attention/focus/memory/logic/emotion', " +
- "score_min INT NOT NULL COMMENT '分数下限(含)', " +
- "score_max INT NOT NULL COMMENT '分数上限(含)', " +
- "template_id BIGINT NOT NULL COMMENT '关联TaskTemplatePackage ID', " +
- "priority INT DEFAULT 0 COMMENT '优先级', " +
- "is_active TINYINT(1) DEFAULT 1 COMMENT '是否启用', " +
- "created_at DATETIME DEFAULT CURRENT_TIMESTAMP, " +
- "updated_at DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, " +
- "INDEX idx_dimension (dimension), " +
- "INDEX idx_score (score_min, score_max)" +
- ") ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='方案生成规则'");
- log.info("已创建assessment_plan_rules表");
- } catch (Exception e) {
- log.warn("创建assessment_plan_rules表失败: {}", e.getMessage());
- }
- // 迁移64: task_plan_instances表添加source字段(方案来源)
- ensureColumn("task_plan_instances", "source",
- "VARCHAR(20) DEFAULT 'manual' COMMENT '方案来源: manual/assessment/template'");
- // 迁移65: task_plan_instances表添加source_result_id和remark字段
- ensureColumn("task_plan_instances", "source_result_id",
- "BIGINT COMMENT '关联测评结果ID(来源为assessment时)'");
- ensureColumn("task_plan_instances", "remark",
- "TEXT COMMENT '方案备注(含指导意见摘要)'");
- // 迁移66: task_plan_instances表添加审核确认字段(阶段五)
- ensureColumn("task_plan_instances", "reviewed_by",
- "BIGINT COMMENT '审核人(规划师ID)'");
- ensureColumn("task_plan_instances", "reviewed_at",
- "DATETIME COMMENT '审核时间'");
- ensureColumn("task_plan_instances", "review_comment",
- "VARCHAR(500) COMMENT '审核意见'");
- ensureColumn("task_plan_instances", "confirmed_by",
- "BIGINT COMMENT '确认人(家长ID)'");
- ensureColumn("task_plan_instances", "confirmed_at",
- "DATETIME COMMENT '确认时间'");
- ensureColumn("task_plan_instances", "activated_at",
- "DATETIME COMMENT '激活时间'");
- // 迁移67: family_access_logs 表(G7 familyId 越权访问日志,可选,用于安全审计)
- try {
- jdbcTemplate.execute("CREATE TABLE IF NOT EXISTS family_access_logs (" +
- "id BIGINT AUTO_INCREMENT PRIMARY KEY, " +
- "user_id BIGINT NOT NULL COMMENT '访问方用户ID', " +
- "family_id BIGINT NOT NULL COMMENT '被访问的家庭ID', " +
- "request_uri VARCHAR(500) NOT NULL COMMENT '请求路径', " +
- "request_method VARCHAR(10) NOT NULL, " +
- "access_type VARCHAR(20) NOT NULL COMMENT 'check_type: query/path/body', " +
- "result VARCHAR(10) NOT NULL COMMENT 'allowed/denied/log_only', " +
- "client_ip VARCHAR(50), " +
- "user_agent VARCHAR(500), " +
- "created_at DATETIME DEFAULT CURRENT_TIMESTAMP, " +
- "INDEX idx_user_time (user_id, created_at), " +
- "INDEX idx_family_time (family_id, created_at)" +
- ") ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='家庭访问安全日志'");
- log.info("已创建family_access_logs表");
- } catch (Exception e) {
- log.warn("创建family_access_logs表失败: {}", e.getMessage());
- }
- // 迁移68: 创建 five_dimension_scores 表(G1 五维能量分数持久化,记录每次计算的维度分数快照)
- try {
- jdbcTemplate.execute("CREATE TABLE IF NOT EXISTS five_dimension_scores (" +
- "id BIGINT AUTO_INCREMENT PRIMARY KEY, " +
- "member_id BIGINT NOT NULL COMMENT '成员ID(user或family_member)', " +
- "member_type VARCHAR(20) NOT NULL COMMENT '成员类型: user/member', " +
- "family_id BIGINT NOT NULL COMMENT '家庭ID', " +
- "dimension_code VARCHAR(20) NOT NULL COMMENT '维度编码: body/mind/wisdom/action/wealth', " +
- "score INT NOT NULL COMMENT '分数(0-100)', " +
- "assessed_at DATETIME NOT NULL COMMENT '评估时间', " +
- "source_type VARCHAR(50) COMMENT '来源类型: periodic/task/assessment/manual', " +
- "source_id BIGINT COMMENT '来源记录ID', " +
- "detail_json TEXT COMMENT '详细数据JSON', " +
- "created_at DATETIME DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间', " +
- "INDEX idx_member_dim (member_id, member_type, dimension_code), " +
- "INDEX idx_family_time (family_id, assessed_at), " +
- "INDEX idx_source (source_type, source_id)" +
- ") ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='五维能量分数历史记录'");
- log.info("已创建five_dimension_scores表");
- // 迁移69: 创建 promotion_tier_config 表(推广等级R0-R4阈值配置)
- try {
- jdbcTemplate.execute("CREATE TABLE IF NOT EXISTS promotion_tier_config (" +
- "id BIGINT AUTO_INCREMENT PRIMARY KEY, " +
- "tier_code VARCHAR(10) NOT NULL UNIQUE COMMENT '等级编码: R0/R1/R2/R3/R4', " +
- "tier_name VARCHAR(50) NOT NULL COMMENT '等级名称', " +
- "min_team_size_1st INT NOT NULL DEFAULT 0 COMMENT '一级团队人数下限', " +
- "min_team_size_2nd INT NOT NULL DEFAULT 0 COMMENT '二级团队人数下限', " +
- "min_total_referral_earnings INT NOT NULL DEFAULT 0 COMMENT '累计推荐佣金下限(分)', " +
- "min_total_share_earnings INT NOT NULL DEFAULT 0 COMMENT '累计消费返利下限(分)', " +
- "commission_rate_l1 INT NOT NULL DEFAULT 0 COMMENT 'L1佣金比例(%)', " +
- "commission_rate_l2 INT NOT NULL DEFAULT 0 COMMENT 'L2佣金比例(%)', " +
- "sort_order INT NOT NULL DEFAULT 0 COMMENT '排序', " +
- "created_at DATETIME DEFAULT CURRENT_TIMESTAMP, " +
- "updated_at DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP" +
- ") ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='推广等级阈值配置'");
- log.info("已创建promotion_tier_config表");
- } catch (Exception e) {
- log.warn("创建promotion_tier_config表失败: {}", e.getMessage());
- }
- try {
- Integer configCount = jdbcTemplate.queryForObject(
- "SELECT COUNT(*) FROM promotion_tier_config", Integer.class);
- if (configCount == null || configCount == 0) {
- jdbcTemplate.execute("INSERT INTO promotion_tier_config (tier_code, tier_name, min_team_size_1st, min_team_size_2nd, min_total_referral_earnings, min_total_share_earnings, commission_rate_l1, commission_rate_l2, sort_order) VALUES " +
- "('R0', '初始用户', 0, 0, 0, 0, 0, 0, 0), " +
- "('R1', '推广达人', 3, 0, 0, 0, 20, 5, 1), " +
- "('R2', '高级推广', 10, 5, 50000, 20000, 25, 8, 2), " +
- "('R3', '资深推广', 30, 20, 200000, 80000, 30, 10, 3), " +
- "('R4', '首席推广', 100, 50, 500000, 200000, 35, 15, 4)");
- log.info("R0-R4推广等级阈值种子数据已初始化");
- }
- } catch (Exception e) {
- log.warn("初始化推广等级阈值种子数据失败: {}", e.getMessage());
- }
- } catch (Exception e) {
- log.warn("创建five_dimension_scores表失败: {}", e.getMessage());
- }
- // 迁移70: package_orders表添加user_coupon_id列
- try {
- jdbcTemplate.execute("ALTER TABLE package_orders ADD COLUMN user_coupon_id BIGINT COMMENT '使用的用户优惠券ID'");
- log.info("已添加user_coupon_id列到package_orders表");
- } catch (Exception e) {
- log.warn("添加user_coupon_id列失败(可能已存在): {}", e.getMessage());
- }
- // 迁移71: family_invitations表添加max_use_count和used_count列(G9 邀请次数控制)
- ensureColumn("family_invitations", "max_use_count", "INT DEFAULT 1 COMMENT '最大使用次数'");
- ensureColumn("family_invitations", "used_count", "INT DEFAULT 0 COMMENT '已使用次数'");
- // 迁移72: 创建teacher_family_binding_requests表(G10 规划师绑定家庭审批)
- try {
- jdbcTemplate.execute("CREATE TABLE IF NOT EXISTS teacher_family_binding_requests (" +
- "id BIGINT AUTO_INCREMENT PRIMARY KEY, " +
- "teacher_id BIGINT NOT NULL COMMENT '规划师用户ID', " +
- "family_id BIGINT NOT NULL COMMENT '家庭ID', " +
- "invitation_token VARCHAR(128) COMMENT '关联邀请码/令牌', " +
- "status VARCHAR(20) NOT NULL DEFAULT 'pending' COMMENT 'pending/approved/rejected/cancelled', " +
- "requested_at DATETIME COMMENT '申请时间', " +
- "decided_by BIGINT COMMENT '审批人用户ID', " +
- "decided_at DATETIME COMMENT '审批时间', " +
- "reject_reason VARCHAR(200) COMMENT '拒绝原因', " +
- "message VARCHAR(500) COMMENT '申请留言', " +
- "created_at DATETIME DEFAULT CURRENT_TIMESTAMP, " +
- "updated_at DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, " +
- "UNIQUE KEY uk_teacher_family (teacher_id, family_id), " +
- "INDEX idx_family_status (family_id, status) " +
- ") ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='规划师绑定家庭申请表'");
- log.info("已创建teacher_family_binding_requests表");
- } catch (Exception e) {
- log.warn("创建teacher_family_binding_requests表失败: {}", e.getMessage());
- }
- // 迁移73: 创建chat_conversations表(G14 Dify会话本地镜像)
- try {
- jdbcTemplate.execute("CREATE TABLE IF NOT EXISTS chat_conversations (" +
- "id BIGINT AUTO_INCREMENT PRIMARY KEY, " +
- "user_id BIGINT NOT NULL, " +
- "dify_conversation_id VARCHAR(64), " +
- "assistant_type VARCHAR(32) COMMENT 'family/nutrition/tongue', " +
- "title VARCHAR(200), " +
- "last_message_at DATETIME, " +
- "created_at DATETIME DEFAULT CURRENT_TIMESTAMP, " +
- "UNIQUE KEY uk_dify_conv (dify_conversation_id), " +
- "INDEX idx_user (user_id) " +
- ") ENGINE=InnoDB DEFAULT CHARSET=utf8mb4");
- log.info("已创建chat_conversations表");
- } catch (Exception e) {
- log.warn("创建chat_conversations表失败: {}", e.getMessage());
- }
- // 迁移74: 创建chat_messages表(G14 Dify会话本地镜像)
- try {
- jdbcTemplate.execute("CREATE TABLE IF NOT EXISTS chat_messages (" +
- "id BIGINT AUTO_INCREMENT PRIMARY KEY, " +
- "conversation_id BIGINT NOT NULL, " +
- "dify_message_id VARCHAR(64), " +
- "role VARCHAR(20) NOT NULL COMMENT 'user/assistant', " +
- "content TEXT NOT NULL, " +
- "inputs_json TEXT, " +
- "metadata TEXT, " +
- "created_at DATETIME DEFAULT CURRENT_TIMESTAMP, " +
- "INDEX idx_conv (conversation_id) " +
- ") ENGINE=InnoDB DEFAULT CHARSET=utf8mb4");
- log.info("已创建chat_messages表");
- } catch (Exception e) {
- log.warn("创建chat_messages表失败: {}", e.getMessage());
- }
- // 迁移75: 创建schema_versions表(G23 迁移版本追踪)
- try {
- jdbcTemplate.execute("CREATE TABLE IF NOT EXISTS schema_versions (" +
- "id BIGINT AUTO_INCREMENT PRIMARY KEY, " +
- "version_code VARCHAR(64) NOT NULL UNIQUE, " +
- "description VARCHAR(500), " +
- "checksum VARCHAR(64), " +
- "applied_at DATETIME DEFAULT CURRENT_TIMESTAMP, " +
- "applied_by VARCHAR(100) " +
- ") ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='迁移版本追踪表'");
- log.info("已创建schema_versions表");
- } catch (Exception e) {
- log.warn("创建schema_versions表失败: {}", e.getMessage());
- }
- // 迁移76: DanAssessmentResult添加structured_analysis字段(G3)
- ensureColumn("dan_assessment_results", "structured_analysis", "TEXT COMMENT '结构化分析JSON'");
- // 迁移77: 创建 pending_refund 表(待退款记录)
- try {
- jdbcTemplate.execute("CREATE TABLE IF NOT EXISTS pending_refund (" +
- "id BIGINT AUTO_INCREMENT PRIMARY KEY, " +
- "user_id BIGINT NOT NULL COMMENT '用户ID', " +
- "withdrawal_request_id BIGINT COMMENT '提现申请ID', " +
- "amount INT NOT NULL COMMENT '退款金额(分)', " +
- "attempt_count INT DEFAULT 0 COMMENT '当前尝试次数', " +
- "max_attempts INT DEFAULT 10 COMMENT '最大重试次数', " +
- "next_retry_time DATETIME COMMENT '下次重试时间', " +
- "status VARCHAR(20) DEFAULT 'pending' COMMENT '状态: pending/processing/success/failed', " +
- "failure_reason VARCHAR(500) COMMENT '失败原因', " +
- "refunded_amount INT COMMENT '实际退款金额(分)', " +
- "refunded_at DATETIME COMMENT '退款时间', " +
- "remark VARCHAR(500) COMMENT '备注', " +
- "created_at DATETIME DEFAULT CURRENT_TIMESTAMP, " +
- "updated_at DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, " +
- "INDEX idx_user_id (user_id), " +
- "INDEX idx_status (status), " +
- "INDEX idx_next_retry (next_retry_time), " +
- "INDEX idx_withdrawal_request (withdrawal_request_id)" +
- ") ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='待退款记录表'");
- log.info("已创建pending_refund表");
- } catch (Exception e) {
- log.warn("创建pending_refund表失败: {}", e.getMessage());
- }
- // 迁移78: product_orders添加consignee_id字段(OrderTimeoutCancelTask查询需要)
- ensureColumn("product_orders", "consignee_id", "BIGINT COMMENT '收货人ID'");
- // 迁移79: pending_refund添加order_type/order_no字段(商品退款走待退款队列)
- ensureColumn("pending_refund", "order_type", "VARCHAR(20) DEFAULT 'withdrawal' COMMENT '退款类型: withdrawal/product'");
- ensureColumn("pending_refund", "order_no", "VARCHAR(64) COMMENT '商品订单号'");
- try {
- jdbcTemplate.execute("ALTER TABLE pending_refund ADD INDEX idx_order_type (order_type)");
- } catch (Exception e) {
- }
- try {
- jdbcTemplate.execute("ALTER TABLE pending_refund ADD INDEX idx_order_no (order_no)");
- } catch (Exception e) {
- }
- // 迁移80: dan_assessment_results添加dimension字段(区分mind/wisdom报告上传)
- ensureColumn("dan_assessment_results", "dimension",
- "VARCHAR(10) DEFAULT 'mind' COMMENT '报告维度: mind(心-A2)/wisdom(智-B4)'");
- ensureColumn("dan_assessment_results", "report_file_id", "VARCHAR(100) COMMENT '上传文件媒资ID'");
- ensureColumn("dan_assessment_results", "draft_status",
- "VARCHAR(10) DEFAULT 'pending' COMMENT '草稿状态: pending(待确认)/confirmed(已确认)'");
- // 迁移81: activities添加dimension_weights JSON列(五维权重百分比)
- ensureColumn("activities", "dimension_weights",
- "JSON DEFAULT NULL COMMENT '{\"body\":0,\"mind\":0,\"wisdom\":0,\"action\":0,\"wealth\":0}'");
- ensureColumn("products", "dimension_weights",
- "JSON DEFAULT NULL COMMENT '{\"body\":0,\"mind\":0,\"wisdom\":0,\"action\":0,\"wealth\":0}'");
- ensureColumn("articles", "dimension_weights",
- "JSON DEFAULT NULL COMMENT '{\"body\":0,\"mind\":0,\"wisdom\":0,\"action\":0,\"wealth\":0}'");
- // 迁移82: 存量数据回填 — 根据dimension_code/domain/relatedDimensions初始化dimension_weights
- try {
- // 回填activities: 已有dimension_code的记录,设对应维度为100
- int actCount = jdbcTemplate.queryForObject(
- "SELECT COUNT(*) FROM activities WHERE dimension_code IS NOT NULL AND dimension_code != '' AND dimension_weights IS NULL",
- Integer.class);
- if (actCount > 0) {
- jdbcTemplate.update(
- "UPDATE activities SET dimension_weights = " +
- "JSON_OBJECT(" +
- " 'body', IF(dimension_code='body',100,0)," +
- " 'mind', IF(dimension_code='mind',100,0)," +
- " 'wisdom', IF(dimension_code='wisdom',100,0)," +
- " 'action', IF(dimension_code='action',100,0)," +
- " 'wealth', IF(dimension_code='wealth',100,0)" +
- ") WHERE dimension_code IS NOT NULL AND dimension_code != '' AND dimension_weights IS NULL");
- log.info("已回填{}条activities的dimension_weights", actCount);
- }
- } catch (Exception e) {
- log.warn("回填activities.dimension_weights失败: {}", e.getMessage());
- }
- try {
- // 回填products: 已有domain的记录,设对应维度为100
- int prodCount = jdbcTemplate.queryForObject(
- "SELECT COUNT(*) FROM products WHERE domain IS NOT NULL AND domain != '' AND dimension_weights IS NULL",
- Integer.class);
- if (prodCount > 0) {
- jdbcTemplate.update(
- "UPDATE products SET dimension_weights = " +
- "JSON_OBJECT(" +
- " 'body', IF(domain='body',100,0)," +
- " 'mind', IF(domain='mind',100,0)," +
- " 'wisdom', IF(domain='wisdom',100,0)," +
- " 'action', IF(domain='action',100,0)," +
- " 'wealth', IF(domain='wealth',100,0)" +
- ") WHERE domain IS NOT NULL AND domain != '' AND dimension_weights IS NULL");
- log.info("已回填{}条products的dimension_weights", prodCount);
- }
- } catch (Exception e) {
- log.warn("回填products.dimension_weights失败: {}", e.getMessage());
- }
- try {
- // 回填articles: 已有relatedDimensions的记录,尝试解析维度名并设权重
- int artCount = jdbcTemplate.queryForObject(
- "SELECT COUNT(*) FROM articles WHERE related_dimensions IS NOT NULL AND related_dimensions != '' AND dimension_weights IS NULL",
- Integer.class);
- if (artCount > 0) {
- // 用MySQL函数解析relatedDimensions中的维度关键词
- jdbcTemplate.update("UPDATE articles SET dimension_weights = JSON_OBJECT(" +
- "'body', IF(INSTR(COALESCE(related_dimensions,''),'body')>0 OR INSTR(COALESCE(related_dimensions,''),'身')>0, 100, 0)," +
- "'mind', IF(INSTR(COALESCE(related_dimensions,''),'mind')>0 OR INSTR(COALESCE(related_dimensions,''),'心')>0, 100, 0)," +
- "'wisdom', IF(INSTR(COALESCE(related_dimensions,''),'wisdom')>0 OR INSTR(COALESCE(related_dimensions,''),'智')>0, 100, 0)," +
- "'action', IF(INSTR(COALESCE(related_dimensions,''),'action')>0 OR INSTR(COALESCE(related_dimensions,''),'行')>0, 100, 0)," +
- "'wealth', IF(INSTR(COALESCE(related_dimensions,''),'wealth')>0 OR INSTR(COALESCE(related_dimensions,''),'富')>0, 100, 0)" +
- ") WHERE related_dimensions IS NOT NULL AND related_dimensions != '' AND dimension_weights IS NULL");
- log.info("已回填{}条articles的dimension_weights", artCount);
- }
- } catch (Exception e) {
- log.warn("回填articles.dimension_weights失败: {}", e.getMessage());
- }
- // 迁移83: 创建dimension_weights表(管理端保存维度权重使用)
- try {
- jdbcTemplate.execute("CREATE TABLE IF NOT EXISTS dimension_weights (" +
- "id BIGINT AUTO_INCREMENT PRIMARY KEY," +
- "target_type VARCHAR(20) NOT NULL COMMENT '目标类型: activity/product/article'," +
- "target_id BIGINT NOT NULL COMMENT '目标ID'," +
- "dimension VARCHAR(20) NOT NULL COMMENT '维度: body/mind/wisdom/action/wealth'," +
- "weight INT DEFAULT 0 COMMENT '权重百分比'," +
- "enabled TINYINT(1) DEFAULT 0 COMMENT '是否启用'," +
- "created_at DATETIME DEFAULT CURRENT_TIMESTAMP," +
- "updated_at DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP," +
- "INDEX idx_target (target_type, target_id)" +
- ") ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='维度权重表'");
- log.info("已创建dimension_weights表");
- } catch (Exception e) {
- log.warn("创建dimension_weights表失败: {}", e.getMessage());
- }
- // 迁移84: cart表添加skuId和specOptionIds字段(支持SKU选择)
- ensureColumn("cart", "sku_id", "BIGINT COMMENT '选中的SKU ID'");
- ensureColumn("cart", "spec_option_ids", "VARCHAR(500) COMMENT '选中的规格选项IDs JSON'");
- // 迁移86: product_orders表添加purchase_info字段(购买信息快照JSON,OrderTimeoutCancelTask查询需要)
- ensureColumn("product_orders", "purchase_info", "TEXT COMMENT '购买信息JSON: {id_card, hand_signature, ...}'");
- // 迁移87: articles表添加content_type/difficulty_level/dimension_ids/dimension_weights字段
- ensureColumn("articles", "content_type", "VARCHAR(20) DEFAULT 'text' COMMENT '内容格式:text/html/markdown'");
- ensureColumn("articles", "difficulty_level", "INT DEFAULT 0 COMMENT '难度等级:1-5'");
- ensureColumn("articles", "dimension_ids", "VARCHAR(100) DEFAULT '' COMMENT '关联维度IDs JSON'");
- ensureColumn("articles", "dimension_weights", "TEXT COMMENT '五维能量权重JSON'");
- // 迁移88: activities表添加dimension_weights列(权重感知活动查询)
- ensureColumn("activities", "dimension_weights", "TEXT COMMENT '五维能量权重JSON'");
- // 迁移89: products表添加dimension_weights列(权重感知商品查询)
- ensureColumn("products", "dimension_weights", "TEXT COMMENT '五维能量权重JSON'");
- // 迁移85: 创建社交圈子表(珍珠图/圈子系统)
- try {
- jdbcTemplate.execute("CREATE TABLE IF NOT EXISTS social_circle (" +
- "id BIGINT AUTO_INCREMENT PRIMARY KEY, " +
- "name VARCHAR(100) NOT NULL COMMENT '圈子名称', " +
- "type VARCHAR(50) NOT NULL COMMENT '类型: topic/activity/hobby/ability/product/health/provider', " +
- "match_source VARCHAR(50) NOT NULL COMMENT '匹配源', " +
- "source_id BIGINT COMMENT '匹配源ID', " +
- "member_count INT DEFAULT 0 COMMENT '成员数', " +
- "created_at DATETIME DEFAULT CURRENT_TIMESTAMP" +
- ") ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='社交圈/珍珠'");
- log.info("已创建social_circle表");
- } catch (Exception e) { /* 表已存在忽略 */ }
- try {
- jdbcTemplate.execute("CREATE TABLE IF NOT EXISTS social_circle_member (" +
- "id BIGINT AUTO_INCREMENT PRIMARY KEY, " +
- "circle_id BIGINT NOT NULL, " +
- "member_id BIGINT NOT NULL COMMENT '用户ID', " +
- "member_type VARCHAR(20) NOT NULL COMMENT 'parent/child', " +
- "joined_at DATETIME DEFAULT CURRENT_TIMESTAMP, " +
- "UNIQUE KEY uk_circle_member (circle_id, member_id)" +
- ") ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='圈子成员'");
- log.info("已创建social_circle_member表");
- } catch (Exception e) { /* 表已存在忽略 */ }
- // 迁移96: 创建活动订单评价表
- try {
- jdbcTemplate.execute("CREATE TABLE IF NOT EXISTS review_orders (" +
- "id BIGINT AUTO_INCREMENT PRIMARY KEY, " +
- "order_id BIGINT NOT NULL COMMENT '订单ID', " +
- "reviewer_id BIGINT NOT NULL COMMENT '评价人ID', " +
- "rating INT COMMENT '总体评分', " +
- "review_text TEXT COMMENT '评价内容', " +
- "review_at DATETIME COMMENT '评价时间', " +
- "organization_rating INT COMMENT '活动组织评分(0-5)', " +
- "content_rating INT COMMENT '活动内容评分(0-5)', " +
- "experience_rating INT COMMENT '活动体验评分(0-5)', " +
- "created_at DATETIME DEFAULT CURRENT_TIMESTAMP, " +
- "updated_at DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, " +
- "UNIQUE KEY uk_order_id (order_id), " +
- "INDEX idx_reviewer_id (reviewer_id)" +
- ") ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='活动订单评价表'");
- log.info("已创建review_orders表");
- } catch (Exception e) {
- log.error("迁移96 失败: " + e.getMessage());
- }
- // 迁移97: growth_task表扩展 — 支持AI_GENERATED类型 + 五维维度/来源会话字段
- try {
- jdbcTemplate.execute("ALTER TABLE growth_task MODIFY COLUMN type "
- + "VARCHAR(16) NOT NULL COMMENT 'DAILY/NEWBIE/AI_GENERATED'");
- log.info("已扩展growth_task.type为支持AI_GENERATED");
- } catch (Exception e) {
- log.warn("扩展growth_task.type失败(可能已经扩展): " + e.getMessage());
- }
- ensureColumn("growth_task", "dimension", "VARCHAR(20) NULL COMMENT '五维维度(body/mind/wisdom/action/wealth)'");
- ensureColumn("growth_task", "source_conversation_id", "VARCHAR(100) NULL COMMENT '来源AI会话ID'");
- log.info("迁移97: growth_task 表扩展完成");
- // 迁移98: health_gut_flora 表扩展 level 字段长度(支持 CLASS/ORDER/FAMILY)
- try {
- jdbcTemplate.execute("ALTER TABLE health_gut_flora MODIFY COLUMN level VARCHAR(20) COMMENT '级别: CLASS/ORDER/FAMILY/GENUS/SPECIES'");
- log.info("已扩展health_gut_flora.level字段长度");
- } catch (Exception e) {
- log.warn("扩展health_gut_flora.level失败: {}", e.getMessage());
- }
- log.info("迁移 98: health_gut_flora 表扩展完成");
-
- // 迁移 100: health_gut_flora 表扩展 population_level 和 detection_rate 字段长度(支持更长百分比)
- try {
- jdbcTemplate.execute("ALTER TABLE health_gut_flora MODIFY COLUMN population_level VARCHAR(100) DEFAULT NULL COMMENT '处于人群%水平'");
- log.info("已扩展 health_gut_flora.population_level 字段长度至 VARCHAR(100)");
- } catch (Exception e) {
- log.warn("扩展 health_gut_flora.population_level 失败:{}", e.getMessage());
- }
-
- try {
- jdbcTemplate.execute("ALTER TABLE health_gut_flora MODIFY COLUMN detection_rate VARCHAR(100) DEFAULT NULL COMMENT '%正常人有检出'");
- log.info("已扩展 health_gut_flora.detection_rate 字段长度至 VARCHAR(100)");
- } catch (Exception e) {
- log.warn("扩展 health_gut_flora.detection_rate 失败:{}", e.getMessage());
- }
- log.info("迁移 100: health_gut_flora 表扩展完成");
- // 迁移99: users.role 从 ENUM 改为 VARCHAR(20)(支持动态角色值)
- try {
- jdbcTemplate.execute("ALTER TABLE users MODIFY COLUMN role VARCHAR(20) NOT NULL COMMENT '角色: parent/child/teacher'");
- log.info("已修改users.role为VARCHAR(20)");
- } catch (Exception e) {
- log.warn("修改users.role失败(可能已修改): " + e.getMessage());
- }
- // 迁移100: assessment_appointments表添加assessor_id(测评师角色分离)
- ensureColumn("assessment_appointments", "assessor_id",
- "BIGINT COMMENT '测评师ID(供应商体系), 替代guideId语义'");
- // 迁移100: dan_assessment_executions表添加appointment_id
- ensureColumn("dan_assessment_executions", "appointment_id",
- "BIGINT COMMENT '关联assessment_appointments.id'");
- log.info("迁移100: 已添加assessor_id和appointment_id字段");
- // 迁移101: 创建 micro_actions 表(12个预定义1秒微行动)
- try {
- jdbcTemplate.execute("CREATE TABLE IF NOT EXISTS micro_actions (" +
- "id BIGINT AUTO_INCREMENT PRIMARY KEY, " +
- "code VARCHAR(32) NOT NULL COMMENT '行动编码: DRINK_WATER, DEEP_BREATH, ...', " +
- "name VARCHAR(50) NOT NULL COMMENT '行动名称', " +
- "icon VARCHAR(10) COMMENT '图标emoji', " +
- "description VARCHAR(200) COMMENT '行动描述', " +
- "sort_order INT DEFAULT 0 COMMENT '排序号', " +
- "status TINYINT DEFAULT 1 COMMENT '状态: 1启用 0禁用', " +
- "created_at DATETIME DEFAULT CURRENT_TIMESTAMP, " +
- "UNIQUE KEY uk_code (code), " +
- "INDEX idx_status (status)" +
- ") ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='微行动定义(12个1秒健康行动)'");
- log.info("已创建micro_actions表");
- // 插入12个预定义微行动
- jdbcTemplate.execute("INSERT IGNORE INTO micro_actions (code, name, icon, description, sort_order) VALUES " +
- "('DRINK_WATER', '喝一杯水', '☕️', '喝一杯温开水,补充水分', 1), " +
- "('DEEP_BREATH', '深呼吸3次', '🫁', '深吸慢呼3次,放松身心', 2), " +
- "('STRETCH', '站起来伸懒腰', '🧘', '站起来伸个懒腰,活动筋骨', 3), " +
- "('LOOK_FAR', '看远处10秒', '🌿', '看远处绿色植物10秒,缓解眼疲劳', 4), " +
- "('SMILE', '微笑一下', '😊', '对自己微笑,心情会变好', 5), " +
- "('NECK_ROLL', '转转头颈5圈', '🔄', '缓慢转动头颈5圈,放松颈椎', 6), " +
- "('FIST_RELAX', '握拳放松3次', '✊', '用力握拳3秒后放松,重复3次', 7), " +
- "('SIP_WATER', '喝口水润喉', '💧', '小口喝水润润喉咙', 8), " +
- "('BLINK', '眨眨眼5次', '👁️', '快速眨眼5次,湿润眼球', 9), " +
- "('ACUPRESSURE', '按揉合谷穴3下', '🖐️', '按揉虎口合谷穴3下,提神醒脑', 10), " +
- "('TIPTOE', '踮脚尖5次', '⬆️', '踮起脚尖再放下,重复5次', 11), " +
- "('SHRUG', '耸肩放松3次', '↕️', '耸肩至耳朵再放松,重复3次', 12)");
- log.info("已初始化12个微行动");
- } catch (Exception e) {
- log.warn("创建micro_actions表或初始化数据失败: {}", e.getMessage());
- }
- // 迁移102: 创建 micro_action_records 表(微行动完成记录)
- try {
- jdbcTemplate.execute("CREATE TABLE IF NOT EXISTS micro_action_records (" +
- "id BIGINT AUTO_INCREMENT PRIMARY KEY, " +
- "user_id BIGINT NOT NULL COMMENT '用户ID', " +
- "child_id BIGINT NOT NULL COMMENT '家庭成员ID', " +
- "action_id BIGINT NOT NULL COMMENT '行动ID', " +
- "completed_date DATETIME NOT NULL COMMENT '完成日期', " +
- "earned_energy INT DEFAULT 1 COMMENT '获得能量值', " +
- "note VARCHAR(200) COMMENT '备注', " +
- "created_at DATETIME DEFAULT CURRENT_TIMESTAMP, " +
- "INDEX idx_child_date (child_id, completed_date), " +
- "INDEX idx_user_id (user_id)" +
- ") ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='微行动完成记录'");
- log.info("已创建micro_action_records表");
- } catch (Exception e) {
- log.warn("创建micro_action_records表失败: {}", e.getMessage());
- }
- // 迁移103: 创建 daily_feedback 表(AI/预设每日健康反馈)
- try {
- jdbcTemplate.execute("CREATE TABLE IF NOT EXISTS daily_feedback (" +
- "id BIGINT AUTO_INCREMENT PRIMARY KEY, " +
- "user_id BIGINT NOT NULL COMMENT '用户ID', " +
- "child_id BIGINT NOT NULL COMMENT '家庭成员ID', " +
- "feedback_date DATETIME NOT NULL COMMENT '反馈日期', " +
- "feedback_type VARCHAR(20) DEFAULT 'daily' COMMENT '反馈类型: daily/weekly/milestone', " +
- "content VARCHAR(500) NOT NULL COMMENT 'AI生成的反馈文本', " +
- "milestone_event VARCHAR(100) COMMENT '关联的里程碑事件', " +
- "is_read TINYINT DEFAULT 0 COMMENT '是否已读: 0未读 1已读', " +
- "source VARCHAR(20) DEFAULT 'preset' COMMENT '来源: ai_generated / preset', " +
- "created_at DATETIME DEFAULT CURRENT_TIMESTAMP, " +
- "INDEX idx_child_date (child_id, feedback_date), " +
- "INDEX idx_feedback_type (feedback_type)" +
- ") ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='每日健康反馈'");
- log.info("已创建daily_feedback表");
- } catch (Exception e) {
- log.warn("创建daily_feedback表失败: {}", e.getMessage());
- }
- // 迁移104: 创建 health_alerts 表(健康行为智能预警)
- try {
- jdbcTemplate.execute("CREATE TABLE IF NOT EXISTS health_alerts (" +
- "id BIGINT AUTO_INCREMENT PRIMARY KEY, " +
- "user_id BIGINT COMMENT '用户ID', " +
- "child_id BIGINT NOT NULL COMMENT '家庭成员ID', " +
- "rule_code VARCHAR(10) NOT NULL COMMENT '规则编码: R001-R006', " +
- "alert_text VARCHAR(200) NOT NULL COMMENT '预警文案', " +
- "action_suggestion VARCHAR(200) COMMENT '行动建议', " +
- "severity VARCHAR(10) DEFAULT 'medium' COMMENT '严重程度: high/medium/low', " +
- "dismissed TINYINT DEFAULT 0 COMMENT '是否已关闭: 0未关闭 1已关闭', " +
- "dismissed_at DATETIME COMMENT '关闭时间', " +
- "alert_date DATETIME NOT NULL COMMENT '预警日期', " +
- "created_at DATETIME DEFAULT CURRENT_TIMESTAMP, " +
- "INDEX idx_child_date (child_id, alert_date), " +
- "INDEX idx_rule (rule_code)" +
- ") ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='健康预警记录'");
- log.info("已创建health_alerts表");
- } catch (Exception e) {
- log.warn("创建health_alerts表失败: {}", e.getMessage());
- }
- // 迁移105: 创建 health_level 表(身份重塑等级定义)
- try {
- jdbcTemplate.execute("CREATE TABLE IF NOT EXISTS health_level (" +
- "id BIGINT AUTO_INCREMENT PRIMARY KEY, " +
- "level_num INT NOT NULL COMMENT '等级编号 1-5', " +
- "title VARCHAR(50) NOT NULL COMMENT '称号', " +
- "badge_icon VARCHAR(20) COMMENT '徽章图标', " +
- "min_score INT DEFAULT 0 COMMENT '所需最低健康积分', " +
- "max_score INT DEFAULT 99999 COMMENT '最高积分上限'" +
- ") ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='健康等级定义'");
- log.info("已创建health_level表");
- } catch (Exception e) {
- log.warn("创建health_level表失败: {}", e.getMessage());
- }
- // 迁移106: 创建 health_timeline_event 表(健康故事时间线)
- try {
- jdbcTemplate.execute("CREATE TABLE IF NOT EXISTS health_timeline_event (" +
- "id BIGINT AUTO_INCREMENT PRIMARY KEY, " +
- "user_id BIGINT COMMENT '用户ID', " +
- "child_id BIGINT NOT NULL COMMENT '家庭成员ID', " +
- "event_type VARCHAR(30) NOT NULL COMMENT '事件类型: streak_7/level_2/first_report', " +
- "event_text VARCHAR(200) NOT NULL COMMENT '事件描述', " +
- "ref_id VARCHAR(50) COMMENT '关联ID', " +
- "event_date DATETIME NOT NULL COMMENT '事件日期', " +
- "created_at DATETIME DEFAULT CURRENT_TIMESTAMP, " +
- "INDEX idx_child_date (child_id, event_date), " +
- "INDEX idx_event_type (event_type)" +
- ") ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='健康时间线事件'");
- log.info("已创建health_timeline_event表");
- } catch (Exception e) {
- log.warn("创建health_timeline_event表失败: {}", e.getMessage());
- }
- // 迁移107: 创建 family_challenge 表(P2-2家庭挑战)
- try {
- jdbcTemplate.execute("CREATE TABLE IF NOT EXISTS family_challenge (" +
- "id BIGINT AUTO_INCREMENT PRIMARY KEY, " +
- "family_id BIGINT NOT NULL COMMENT '家庭ID', " +
- "challenge_type VARCHAR(50) NOT NULL COMMENT 'full_checkin/sports/health_week', " +
- "title VARCHAR(200) NOT NULL COMMENT '挑战标题', " +
- "description TEXT COMMENT '挑战描述', " +
- "duration_days INT DEFAULT 7 COMMENT '持续天数', " +
- "reward_points INT DEFAULT 0 COMMENT '奖励积分', " +
- "reward_badge VARCHAR(100) COMMENT '奖励徽章', " +
- "start_date DATE COMMENT '开始日期', " +
- "end_date DATE COMMENT '结束日期', " +
- "status VARCHAR(20) DEFAULT 'active' COMMENT 'active/completed/cancelled', " +
- "created_at DATETIME DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间', " +
- "INDEX idx_family_id (family_id)" +
- ") ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='家庭挑战'");
- log.info("已创建family_challenge表");
- } catch (Exception e) {
- log.warn("创建family_challenge表失败: {}", e.getMessage());
- }
- // 迁移108: 创建 family_challenge_progress 表(P2-2挑战进度)
- try {
- jdbcTemplate.execute("CREATE TABLE IF NOT EXISTS family_challenge_progress (" +
- "id BIGINT AUTO_INCREMENT PRIMARY KEY, " +
- "challenge_id BIGINT NOT NULL COMMENT '挑战ID', " +
- "child_id BIGINT NOT NULL COMMENT '家庭成员ID', " +
- "progress_value INT DEFAULT 0 COMMENT '进度值', " +
- "completed TINYINT(1) DEFAULT 0 COMMENT '是否完成', " +
- "created_at DATETIME DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间', " +
- "updated_at DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间', " +
- "INDEX idx_challenge_id (challenge_id)" +
- ") ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='家庭挑战进度'");
- log.info("已创建family_challenge_progress表");
- } catch (Exception e) {
- log.warn("创建family_challenge_progress表失败: {}", e.getMessage());
- }
- // 迁移109: 创建 health_interaction 表(P2-2点赞记录)
- try {
- jdbcTemplate.execute("CREATE TABLE IF NOT EXISTS health_interaction (" +
- "id BIGINT AUTO_INCREMENT PRIMARY KEY, " +
- "family_id BIGINT NOT NULL COMMENT '家庭ID', " +
- "from_user_id BIGINT NOT NULL COMMENT '点赞用户ID', " +
- "to_user_id BIGINT NOT NULL COMMENT '被赞用户ID', " +
- "action_type VARCHAR(50) NOT NULL COMMENT '行为类型', " +
- "action_id BIGINT COMMENT '关联行为记录ID', " +
- "created_at DATETIME DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间', " +
- "INDEX idx_family_action (family_id, action_type, action_id)" +
- ") ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='健康互动记录(点赞)'");
- log.info("已创建health_interaction表");
- } catch (Exception e) {
- log.warn("创建health_interaction表失败: {}", e.getMessage());
- }
- // 迁移110: 创建 health_circle 表(P3 健康圈)
- try {
- jdbcTemplate.execute("CREATE TABLE IF NOT EXISTS health_circle (" +
- "id BIGINT AUTO_INCREMENT PRIMARY KEY, " +
- "name VARCHAR(100) NOT NULL COMMENT '圈名称', " +
- "description VARCHAR(500) COMMENT '圈描述', " +
- "creator_family_id BIGINT COMMENT '创建家庭ID', " +
- "invite_code VARCHAR(32) COMMENT '邀请码', " +
- "member_limit INT DEFAULT 50 COMMENT '成员上限', " +
- "status VARCHAR(20) DEFAULT 'active' COMMENT '状态 active/closed', " +
- "created_at DATETIME DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间', " +
- "INDEX idx_invite_code (invite_code), " +
- "INDEX idx_creator (creator_family_id)" +
- ") ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='健康圈'");
- log.info("已创建health_circle表");
- } catch (Exception e) {
- log.warn("创建health_circle表失败: {}", e.getMessage());
- }
- // 迁移111: 创建 health_circle_member 表(P3 健康圈成员)
- try {
- jdbcTemplate.execute("CREATE TABLE IF NOT EXISTS health_circle_member (" +
- "id BIGINT AUTO_INCREMENT PRIMARY KEY, " +
- "circle_id BIGINT NOT NULL COMMENT '圈ID', " +
- "family_id BIGINT NOT NULL COMMENT '家庭ID', " +
- "role VARCHAR(20) DEFAULT 'member' COMMENT '角色 member/admin', " +
- "joined_at DATETIME COMMENT '加入时间', " +
- "left_at DATETIME COMMENT '退出时间', " +
- "status VARCHAR(20) DEFAULT 'active' COMMENT '状态 active/inactive', " +
- "created_at DATETIME DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间', " +
- "INDEX idx_circle_id (circle_id), " +
- "INDEX idx_family_id (family_id)" +
- ") ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='健康圈成员'");
- log.info("已创建health_circle_member表");
- } catch (Exception e) {
- log.warn("创建health_circle_member表失败: {}", e.getMessage());
- }
- // 迁移112: 创建 circle_challenge 表(P3 圈挑战)
- try {
- jdbcTemplate.execute("CREATE TABLE IF NOT EXISTS circle_challenge (" +
- "id BIGINT AUTO_INCREMENT PRIMARY KEY, " +
- "circle_id BIGINT NOT NULL COMMENT '圈ID', " +
- "challenge_type VARCHAR(50) NOT NULL COMMENT '挑战类型 checkin/step/meditation', " +
- "title VARCHAR(200) NOT NULL COMMENT '挑战标题', " +
- "description VARCHAR(500) COMMENT '挑战描述', " +
- "duration_days INT DEFAULT 7 COMMENT '持续天数', " +
- "period VARCHAR(20) DEFAULT 'weekly' COMMENT '周期 weekly/monthly', " +
- "start_date DATETIME COMMENT '开始日期', " +
- "end_date DATETIME COMMENT '结束日期', " +
- "status VARCHAR(20) DEFAULT 'active' COMMENT '状态 active/completed/cancelled', " +
- "created_at DATETIME DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间', " +
- "INDEX idx_circle_id (circle_id), " +
- "INDEX idx_status (status)" +
- ") ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='圈挑战'");
- log.info("已创建circle_challenge表");
- } catch (Exception e) {
- log.warn("创建circle_challenge表失败: {}", e.getMessage());
- }
- // 迁移113: 创建 family_commission 表(P2 推广佣金)
- try {
- jdbcTemplate.execute("CREATE TABLE IF NOT EXISTS family_commission (" +
- "id BIGINT AUTO_INCREMENT PRIMARY KEY, " +
- "promoter_family_id BIGINT NOT NULL COMMENT '推广方家庭ID', " +
- "buyer_family_id BIGINT NOT NULL COMMENT '购买方家庭ID', " +
- "order_id BIGINT COMMENT '关联订单ID', " +
- "amount DECIMAL(10,2) DEFAULT 0.00 COMMENT '订单金额', " +
- "commission_rate DECIMAL(5,2) DEFAULT 10.00 COMMENT '佣金比例%', " +
- "commission_amount DECIMAL(10,2) DEFAULT 0.00 COMMENT '佣金金额', " +
- "status VARCHAR(20) DEFAULT 'pending' COMMENT 'pending/paid/cancelled', " +
- "created_at DATETIME DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间', " +
- "paid_at DATETIME COMMENT '支付时间', " +
- "INDEX idx_promoter (promoter_family_id), " +
- "INDEX idx_buyer (buyer_family_id), " +
- "INDEX idx_order (order_id)" +
- ") ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='家庭推广佣金记录'");
- log.info("已创建family_commission表");
- } catch (Exception e) {
- log.warn("创建family_commission表失败: {}", e.getMessage());
- }
- // 迁移114: 创建 promotion_binding 表(P2 推广绑定)
- try {
- jdbcTemplate.execute("CREATE TABLE IF NOT EXISTS promotion_binding (" +
- "id BIGINT AUTO_INCREMENT PRIMARY KEY, " +
- "promoter_family_id BIGINT NOT NULL COMMENT '推广方家庭ID', " +
- "invitee_family_id BIGINT NOT NULL COMMENT '被邀请方家庭ID', " +
- "invite_code VARCHAR(32) COMMENT '使用的邀请码', " +
- "binding_type VARCHAR(20) DEFAULT 'direct' COMMENT 'direct/indirect', " +
- "status VARCHAR(20) DEFAULT 'active' COMMENT 'active/inactive', " +
- "created_at DATETIME DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间', " +
- "UNIQUE KEY uk_promoter_invitee (promoter_family_id, invitee_family_id), " +
- "INDEX idx_promoter (promoter_family_id), " +
- "INDEX idx_invitee (invitee_family_id)" +
- ") ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='推广绑定关系'");
- log.info("已创建promotion_binding表");
- } catch (Exception e) {
- log.warn("创建promotion_binding表失败: {}", e.getMessage());
- }
- // 迁移115: 创建 activity_feedback 表(活动评价功能)
- try {
- jdbcTemplate.execute("CREATE TABLE IF NOT EXISTS activity_feedback (" +
- "id BIGINT AUTO_INCREMENT PRIMARY KEY COMMENT '主键ID', " +
- "activity_id BIGINT NOT NULL COMMENT '活动ID', " +
- "user_id BIGINT NOT NULL COMMENT '用户ID', " +
- "child_id BIGINT COMMENT '孩子ID', " +
- "ratings TEXT COMMENT '评分JSON: {\"组织形式\":5,\"会场气氛\":4}', " +
- "comment TEXT COMMENT '文字评价', " +
- "status TINYINT DEFAULT 0 COMMENT '状态: 0=待审核 1=已发布', " +
- "created_at DATETIME DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间', " +
- "INDEX idx_activity_id (activity_id), " +
- "INDEX idx_user_id (user_id), " +
- "INDEX idx_child_id (child_id)" +
- ") ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='活动评价表'");
- log.info("已创建activity_feedback表");
- } catch (Exception e) {
- // 表已存在,忽略
- }
- // 迁移115b: activity表添加feedback_dimensions字段
- ensureColumn("activities", "feedback_dimensions", "TEXT COMMENT '评价维度列表JSON: [\"组织形式\",\"会场气氛\",\"主持人\",\"专家\"]'");
- // 迁移116: growth_records表添加family_member_id和family_member_age列(修复/record/list 500错误)
- ensureColumn("growth_records", "family_member_id", "BIGINT COMMENT '关联家庭成员ID'");
- ensureColumn("growth_records", "family_member_age", "INT COMMENT '成员年龄快照'");
- // 迁移117: 创建 emotion_checkin 表情绪打卡表(Fix 心理与情绪模块 500 错误)
- try {
- jdbcTemplate.execute("CREATE TABLE IF NOT EXISTS emotion_checkin (" +
- "id BIGINT AUTO_INCREMENT PRIMARY KEY, " +
- "child_id BIGINT NOT NULL COMMENT '家庭成员ID', " +
- "mood_weather VARCHAR(50) COMMENT '心情晴雨表: sunny/cloudy/rainy/stormy', " +
- "emotion_tags VARCHAR(200) COMMENT '情绪标签(逗号分隔)', " +
- "photo_url VARCHAR(500) COMMENT '配图URL', " +
- "note TEXT COMMENT '文字备注', " +
- "energy_awarded INT DEFAULT 0 COMMENT '奖励能量值', " +
- "checkin_date DATE COMMENT '打卡日期', " +
- "mood_score INT COMMENT '心情分数(1-10)', " +
- "emotion_type VARCHAR(50) COMMENT '情绪类型', " +
- "stress_level INT COMMENT '压力等级(1-10)', " +
- "arousal_level INT COMMENT '激活水平(1-10)', " +
- "energy_level INT COMMENT '精力水平(1-10)', " +
- "sleep_hours DECIMAL(4,1) COMMENT '睡眠时长', " +
- "sleep_quality INT COMMENT '睡眠质量(1-10)', " +
- "created_at DATETIME DEFAULT CURRENT_TIMESTAMP, " +
- "updated_at DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, " +
- "INDEX idx_child_date (child_id, checkin_date)" +
- ") ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='情绪打卡记录'");
- log.info("已创建emotion_checkin表");
- } catch (Exception e) {
- log.warn("创建emotion_checkin表可能已存在: {}", e.getMessage());
- }
- // 迁移118: 创建 emotion_alert 表情绪告警表
- try {
- jdbcTemplate.execute("CREATE TABLE IF NOT EXISTS emotion_alert (" +
- "id BIGINT AUTO_INCREMENT PRIMARY KEY, " +
- "child_id BIGINT NOT NULL COMMENT '家庭成员ID', " +
- "alert_type VARCHAR(50) NOT NULL COMMENT '告警类型: consecutive_low_mood/crisis_keyword/sharp_decline', " +
- "severity VARCHAR(20) NOT NULL DEFAULT 'low' COMMENT '严重级别: low/medium/high', " +
- "title VARCHAR(200) COMMENT '告警标题', " +
- "message TEXT COMMENT '告警具体描述', " +
- "related_checkin_id BIGINT COMMENT '关联情绪打卡ID', " +
- "resolved TINYINT(1) DEFAULT 0 COMMENT '是否已处理', " +
- "created_at DATETIME DEFAULT CURRENT_TIMESTAMP, " +
- "resolved_at DATETIME COMMENT '处理时间', " +
- "INDEX idx_child (child_id), " +
- "INDEX idx_alert_type (alert_type)" +
- ") ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='情绪告警'");
- log.info("已创建emotion_alert表");
- } catch (Exception e) {
- log.warn("创建emotion_alert表可能已存在: {}", e.getMessage());
- }
- // 迁移119: 创建 membership_levels 表(会员等级配置)
- try {
- jdbcTemplate.execute("CREATE TABLE IF NOT EXISTS membership_levels (" +
- "id BIGINT AUTO_INCREMENT PRIMARY KEY, " +
- "level_code VARCHAR(50) NOT NULL COMMENT '等级编码: FREE/PROFESSIONAL/ENTERPRISE', " +
- "level_name VARCHAR(100) NOT NULL COMMENT '等级名称', " +
- "level_desc TEXT COMMENT '等级描述', " +
- "price_monthly INT COMMENT '月费(分)', " +
- "price_quarterly INT COMMENT '季费(分)', " +
- "price_yearly INT COMMENT '年费(分)', " +
- "features TEXT COMMENT '功能权限列表(JSON)', " +
- "max_children INT DEFAULT 0 COMMENT '可添加孩子数量', " +
- "max_tasks_per_day INT DEFAULT 0 COMMENT '每日任务上限', " +
- "ai_review_enabled TINYINT(1) DEFAULT 0 COMMENT 'AI审核功能', " +
- "priority_support TINYINT(1) DEFAULT 0 COMMENT '优先客服', " +
- "teacher_consultation TINYINT(1) DEFAULT 0 COMMENT '老师咨询', " +
- "level_order INT DEFAULT 1 COMMENT '级别数字(越小级别越低)', " +
- "created_at DATETIME DEFAULT CURRENT_TIMESTAMP, " +
- "updated_at DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, " +
- "UNIQUE KEY uk_level_code (level_code)" +
- ") ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='会员等级配置'");
- log.info("已创建membership_levels表");
- } catch (Exception e) {
- log.warn("创建membership_levels表可能已存在: {}", e.getMessage());
- }
- // 迁移120: 插入 membership_levels 种子数据
- try {
- jdbcTemplate.execute("INSERT IGNORE INTO membership_levels (level_code, level_name, level_desc, price_monthly, price_quarterly, price_yearly, max_children, max_tasks_per_day, ai_review_enabled, priority_support, teacher_consultation) VALUES " +
- "('FREE', '免费会员', '基础免费功能', 0, 0, 0, 2, 5, 0, 0, 0), " +
- "('FAMILY', '家庭会员', '家庭会员全部功能', 13140, 32850, 131400, 6, 20, 1, 1, 1), " +
- "('PROVIDER', '标准会员', '标准会员服务', 13140, 32850, 131400, 10, 50, 1, 1, 1)");
- log.info("已插入membership_levels种子数据");
- } catch (Exception e) {
- log.warn("插入membership_levels种子数据可能已存在: {}", e.getMessage());
- }
- // 迁移121: 创建 family_memberships 表(家庭会员订阅记录)
- try {
- jdbcTemplate.execute("CREATE TABLE IF NOT EXISTS family_memberships (" +
- "id BIGINT AUTO_INCREMENT PRIMARY KEY, " +
- "family_id BIGINT NOT NULL COMMENT '家庭ID', " +
- "level_code VARCHAR(50) NOT NULL DEFAULT 'FREE' COMMENT '会员等级编码', " +
- "start_date DATETIME COMMENT '开始时间', " +
- "end_date DATETIME COMMENT '结束时间', " +
- "payment_method VARCHAR(50) COMMENT '支付方式: wechat/alipay/card', " +
- "payment_status VARCHAR(20) DEFAULT 'pending' COMMENT '支付状态: pending/paid/expired/refunded', " +
- "order_no VARCHAR(100) COMMENT '订单号', " +
- "transaction_id VARCHAR(200) COMMENT '交易流水号', " +
- "amount INT COMMENT '支付金额(分)', " +
- "auto_renew TINYINT(1) DEFAULT 0 COMMENT '自动续费', " +
- "created_at DATETIME DEFAULT CURRENT_TIMESTAMP, " +
- "updated_at DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, " +
- "INDEX idx_family_id (family_id), " +
- "INDEX idx_end_date (end_date)" +
- ") ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='家庭会员订阅'");
- log.info("已创建family_memberships表");
- } catch (Exception e) {
- log.warn("创建family_memberships表可能已存在: {}", e.getMessage());
- }
- // 迁移121: 创建 share_events 表(分享事件记录)
- try {
- jdbcTemplate.execute("CREATE TABLE IF NOT EXISTS share_events (" +
- "id BIGINT AUTO_INCREMENT PRIMARY KEY, " +
- "user_id BIGINT NOT NULL COMMENT '分享用户ID', " +
- "target_type VARCHAR(50) NOT NULL COMMENT '分享目标类型: article/growth/product', " +
- "target_id BIGINT NOT NULL COMMENT '分享目标ID', " +
- "share_channel VARCHAR(50) COMMENT '分享渠道: wechat/timeline/qq/url', " +
- "share_title VARCHAR(200) COMMENT '分享标题', " +
- "share_url VARCHAR(500) COMMENT '分享URL', " +
- "click_count INT DEFAULT 0 COMMENT '点击次数', " +
- "read_count INT DEFAULT 0 COMMENT '阅读次数', " +
- "reward_points INT DEFAULT 0 COMMENT '奖励积分', " +
- "status VARCHAR(20) DEFAULT 'active' COMMENT '状态: active/expired', " +
- "expires_at DATETIME COMMENT '过期时间', " +
- "created_at DATETIME DEFAULT CURRENT_TIMESTAMP, " +
- "updated_at DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, " +
- "INDEX idx_user (user_id), " +
- "INDEX idx_target (target_type, target_id)" +
- ") ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='分享事件'");
- log.info("已创建share_events表");
- } catch (Exception e) {
- log.warn("创建share_events表可能已存在: {}", e.getMessage());
- }
- // 迁移122: 创建 share_reads 表(分享阅读记录)
- try {
- jdbcTemplate.execute("CREATE TABLE IF NOT EXISTS share_reads (" +
- "id BIGINT AUTO_INCREMENT PRIMARY KEY, " +
- "share_event_id BIGINT NOT NULL COMMENT '分享事件ID', " +
- "reader_id BIGINT COMMENT '阅读者用户ID', " +
- "reader_ip VARCHAR(50) COMMENT '阅读者IP', " +
- "read_duration INT COMMENT '阅读时长(秒)', " +
- "read_at DATETIME DEFAULT CURRENT_TIMESTAMP COMMENT '阅读时间', " +
- "INDEX idx_share_event (share_event_id), " +
- "INDEX idx_reader (reader_id)" +
- ") ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='分享阅读记录'");
- log.info("已创建share_reads表");
- } catch (Exception e) {
- log.warn("创建share_reads表可能已存在: {}", e.getMessage());
- }
- // 迁移123: 创建 food_recommend_idx 表(饮食推荐指数)
- try {
- jdbcTemplate.execute("CREATE TABLE IF NOT EXISTS food_recommend_idx (" +
- "id BIGINT AUTO_INCREMENT PRIMARY KEY, " +
- "user_id BIGINT NOT NULL COMMENT '用户ID', " +
- "food_id BIGINT NOT NULL COMMENT '食材ID', " +
- "food_name VARCHAR(100) COMMENT '食材名称', " +
- "index_score INT COMMENT '推荐指数(0-100)', " +
- "health_report_id BIGINT COMMENT '关联健康报告ID', " +
- "calculated_at DATETIME COMMENT '计算时间', " +
- "created_at DATETIME DEFAULT CURRENT_TIMESTAMP, " +
- "INDEX idx_user (user_id), " +
- "INDEX idx_food (food_id), " +
- "INDEX idx_report (health_report_id)" +
- ") ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='饮食推荐指数'");
- log.info("已创建food_recommend_idx表");
- } catch (Exception e) {
- log.warn("创建food_recommend_idx表可能已存在: {}", e.getMessage());
- }
- // 迁移124: 创建 points_exchange_products 表(积分兑换商品)
- try {
- jdbcTemplate.execute("CREATE TABLE IF NOT EXISTS points_exchange_products (" +
- "id BIGINT AUTO_INCREMENT PRIMARY KEY, " +
- "name VARCHAR(200) NOT NULL COMMENT '商品名称', " +
- "description TEXT COMMENT '商品描述', " +
- "cover_image VARCHAR(500) COMMENT '封面图片', " +
- "points_price INT NOT NULL COMMENT '积分价格', " +
- "stock INT DEFAULT 0 COMMENT '库存', " +
- "product_type VARCHAR(50) COMMENT '商品类型', " +
- "category VARCHAR(50) COMMENT '分类', " +
- "expiry_days INT COMMENT '有效期(天)', " +
- "sort_order INT DEFAULT 0 COMMENT '排序', " +
- "status VARCHAR(20) DEFAULT 'active' COMMENT '状态: active/disabled', " +
- "created_at DATETIME DEFAULT CURRENT_TIMESTAMP, " +
- "updated_at DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, " +
- "INDEX idx_status (status), " +
- "INDEX idx_category (category)" +
- ") ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='积分兑换商品'");
- log.info("已创建points_exchange_products表");
- } catch (Exception e) {
- log.warn("创建points_exchange_products表可能已存在: {}", e.getMessage());
- }
- // 迁移125: 创建 points_exchange_records 表(积分兑换记录)
- try {
- jdbcTemplate.execute("CREATE TABLE IF NOT EXISTS points_exchange_records (" +
- "id BIGINT AUTO_INCREMENT PRIMARY KEY, " +
- "user_id BIGINT COMMENT '用户ID', " +
- "child_id BIGINT COMMENT '孩子ID', " +
- "family_member_id BIGINT COMMENT '家庭成员ID', " +
- "product_id BIGINT NOT NULL COMMENT '商品ID', " +
- "product_name VARCHAR(200) COMMENT '商品名称快照', " +
- "points_cost INT NOT NULL COMMENT '消耗积分', " +
- "quantity INT DEFAULT 1 COMMENT '数量', " +
- "status VARCHAR(20) DEFAULT 'pending' COMMENT '状态: pending/completed/cancelled', " +
- "redeem_code VARCHAR(100) COMMENT '兑换码', " +
- "expired_at DATETIME COMMENT '过期时间', " +
- "created_at DATETIME DEFAULT CURRENT_TIMESTAMP, " +
- "updated_at DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, " +
- "INDEX idx_user (user_id), " +
- "INDEX idx_product (product_id), " +
- "INDEX idx_status (status)" +
- ") ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='积分兑换记录'");
- log.info("已创建points_exchange_records表");
- } catch (Exception e) {
- log.warn("创建points_exchange_records表可能已存在: {}", e.getMessage());
- }
- // 迁移126: emotion_checkin表修复列类型与添加新列
- ensureColumn("emotion_checkin", "mood_weather", "VARCHAR(20) DEFAULT NULL COMMENT '心情天气: sunny/cloudy/rainy/stormy'");
- ensureColumn("emotion_checkin", "emotion_tags", "VARCHAR(200) DEFAULT NULL COMMENT '情绪标签,逗号分隔'");
- ensureColumn("emotion_checkin", "energy_awarded", "INT DEFAULT 0 COMMENT '发放能量值'");
- ensureColumn("emotion_checkin", "mood_score", "INT DEFAULT NULL COMMENT '心情分数(0-100)'");
- ensureColumn("emotion_checkin", "stress_level", "INT DEFAULT NULL COMMENT '压力等级(1-5)'");
- ensureColumn("emotion_checkin", "arousal_level", "INT DEFAULT NULL COMMENT '唤醒等级(1-5)'");
- ensureColumn("emotion_checkin", "sleep_hours", "DECIMAL(4,1) DEFAULT NULL COMMENT '睡眠时长(小时)'");
- ensureColumn("emotion_checkin", "sleep_quality", "VARCHAR(20) DEFAULT NULL COMMENT '睡眠质量: good/fair/poor'");
- try {
- jdbcTemplate.execute("ALTER TABLE emotion_checkin MODIFY COLUMN user_id BIGINT NULL COMMENT '用户ID(旧字段,可为空)'");
- log.info("已修改emotion_checkin.user_id为可空");
- } catch (Exception e) {
- log.warn("修改emotion_checkin.user_id可能已处理: {}", e.getMessage());
- }
- try {
- jdbcTemplate.execute("ALTER TABLE emotion_checkin MODIFY COLUMN emotion_type VARCHAR(50) DEFAULT 'custom' COMMENT '情绪类型'");
- log.info("已修改emotion_checkin.emotion_type为VARCHAR");
- } catch (Exception e) {
- log.warn("修改emotion_checkin.emotion_type可能已处理: {}", e.getMessage());
- }
- ensureColumn("family_fortune", "fortune_level_text", "VARCHAR(50) DEFAULT NULL COMMENT '运势等级文字'");
- ensureColumn("family_fortune", "fortune_description", "VARCHAR(500) DEFAULT NULL COMMENT '运势描述'");
- ensureColumn("family_fortune", "mood_emoji", "VARCHAR(20) DEFAULT NULL COMMENT '心情表情'");
- ensureColumn("family_fortune", "mood_text", "VARCHAR(100) DEFAULT NULL COMMENT '心情描述文字'");
- ensureColumn("family_fortune", "weekly_tip", "VARCHAR(500) DEFAULT NULL COMMENT '本周建议'");
- // 迁移129: membership_levels表添加price_quarterly列
- ensureColumn("membership_levels", "price_quarterly", "DECIMAL(10,2) DEFAULT NULL COMMENT '季度价格'");
- // 迁移130: share_events表添加缺失列
- ensureColumn("share_events", "target_type", "VARCHAR(20) DEFAULT NULL COMMENT '分享目标类型'");
- ensureColumn("share_events", "share_channel", "VARCHAR(20) DEFAULT NULL COMMENT '分享渠道'");
- ensureColumn("share_events", "share_title", "VARCHAR(200) DEFAULT NULL COMMENT '分享标题'");
- ensureColumn("share_events", "share_url", "VARCHAR(500) DEFAULT NULL COMMENT '分享URL'");
- ensureColumn("share_events", "read_count", "INT DEFAULT 0 COMMENT '阅读次数'");
- ensureColumn("share_events", "reward_points", "INT DEFAULT 0 COMMENT '奖励积分'");
- ensureColumn("share_events", "status", "VARCHAR(20) DEFAULT 'active' COMMENT '状态'");
- ensureColumn("share_events", "expires_at", "DATETIME DEFAULT NULL COMMENT '过期时间'");
- ensureColumn("share_events", "updated_at", "DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间'");
- // 迁移131: share_reads表添加缺失列
- ensureColumn("share_reads", "read_duration", "INT DEFAULT NULL COMMENT '阅读时长(秒)'");
- ensureColumn("share_reads", "read_at", "DATETIME DEFAULT CURRENT_TIMESTAMP COMMENT '阅读时间'");
- // 迁移132: points_exchange_records表确保family_member_id列
- ensureColumn("points_exchange_records", "family_member_id", "BIGINT COMMENT '家庭成员ID'");
- // 迁移133: products表列对齐 — rename min_quantity→purchase_count_threshold, add missing columns
- if (columnExists("products", "min_quantity")) {
- try {
- jdbcTemplate.execute("ALTER TABLE products CHANGE COLUMN min_quantity purchase_count_threshold INT DEFAULT 0 COMMENT '最小购买数量限制'");
- log.info("已重命名products.min_quantity→purchase_count_threshold");
- } catch (Exception e) {
- log.warn("重命名products列失败(可能已处理): {}", e.getMessage());
- }
- }
- // 迁移134: products表添加sort_order列(商品按排序字段排序展示)
- ensureColumn("products", "sort_order", "INT DEFAULT 0 COMMENT '排序(值越小越靠前)'");
- ensureColumn("products", "recommendation_tags", "VARCHAR(500) DEFAULT NULL COMMENT '推荐标签'");
- ensureColumn("products", "is_recommended", "TINYINT(1) DEFAULT 0 COMMENT '是否推荐商品 0否 1是'");
- ensureColumn("products", "repurchase_interval_days", "INT DEFAULT 30 COMMENT '复购间隔天数'");
- // 迁移134: 创建 category_ppoint 表(品类P点比例配置)
- try {
- jdbcTemplate.execute("CREATE TABLE IF NOT EXISTS category_ppoint (" +
- "id BIGINT AUTO_INCREMENT PRIMARY KEY, " +
- "category_id BIGINT NOT NULL COMMENT '商品类目ID', " +
- "ratio INT DEFAULT 0 COMMENT 'P点比例(千分比)', " +
- "start_date DATETIME COMMENT '生效日期', " +
- "end_date DATETIME COMMENT '失效日期(null=永不过期)', " +
- "created_by BIGINT COMMENT '创建人', " +
- "created_at DATETIME DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间', " +
- "INDEX idx_category_id (category_id)" +
- ") ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='品类P点比例配置'");
- log.info("已创建category_ppoint表");
- } catch (Exception e) {
- log.warn("创建category_ppoint表失败: {}", e.getMessage());
- }
- // 迁移135: 创建 commission_dist_log 表(P点分润记录)
- try {
- jdbcTemplate.execute("CREATE TABLE IF NOT EXISTS commission_dist_log (" +
- "id BIGINT AUTO_INCREMENT PRIMARY KEY, " +
- "buyer_id BIGINT NOT NULL COMMENT '买家用户ID', " +
- "order_id BIGINT COMMENT '订单ID', " +
- "product_id BIGINT COMMENT '商品ID', " +
- "source_ppoint INT DEFAULT 0 COMMENT '本次产生的原始P点数量', " +
- "level1_user_id BIGINT COMMENT '一级推荐人用户ID', " +
- "level1_ppoint INT DEFAULT 0 COMMENT '一级推荐人获得P点', " +
- "level1_wealth_percent INT DEFAULT 0 COMMENT '一级推荐人当时财富等级(profitSharePercent)', " +
- "level2_user_id BIGINT COMMENT '二级推荐人用户ID', " +
- "level2_ppoint INT DEFAULT 0 COMMENT '二级推荐人获得P点', " +
- "level2_wealth_percent INT DEFAULT 0 COMMENT '二级推荐人当时财富等级(profitSharePercent)', " +
- "status VARCHAR(20) DEFAULT 'pending' COMMENT '状态: pending/completed/failed', " +
- "remark VARCHAR(500) COMMENT '备注', " +
- "created_at DATETIME DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间', " +
- "updated_at DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间', " +
- "INDEX idx_buyer (buyer_id), " +
- "INDEX idx_order (order_id), " +
- "INDEX idx_level1 (level1_user_id), " +
- "INDEX idx_level2 (level2_user_id)" +
- ") ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='P点分润记录'");
- log.info("已创建commission_dist_log表");
- } catch (Exception e) {
- log.warn("创建commission_dist_log表失败: {}", e.getMessage());
- }
- // 迁移136: payment_orders表添加period列(会员订阅周期)
- ensureColumn("payment_orders", "period", "VARCHAR(20) DEFAULT 'yearly' COMMENT '订阅周期: monthly/quarterly/yearly'");
- // 迁移137: 扩大payment_orders.payment_type列长度(原列可能为VARCHAR(5),存不下"yearly")
- try {
- jdbcTemplate.execute("ALTER TABLE payment_orders MODIFY COLUMN payment_type VARCHAR(20) DEFAULT 'yearly' COMMENT '支付类型: yearly/monthly/trial'");
- log.info("已修改payment_orders.payment_type列长度为VARCHAR(20)");
- } catch (Exception e) {
- log.warn("修改payment_orders.payment_type列长度失败: {}", e.getMessage());
- }
- // 迁移138: tasks表添加dimension_code和dimension_weights列(原migration102内从未被调用)
- ensureColumn("tasks", "dimension_code", "VARCHAR(20) COMMENT '五维维度: body/mind/wisdom/action/wealth'");
- ensureColumn("tasks", "dimension_weights", "JSON COMMENT '五维权重: {\"body\":0,\"mind\":0,\"wisdom\":0,\"action\":0,\"wealth\":0}'");
- // 迁移139: products表添加family_scope列(推荐系统-家庭消费去重,原migration102内从未被调用)
- ensureColumn("products", "family_scope", "TINYINT DEFAULT 1 COMMENT '是否面向家庭: 1=家庭成员消费过即排除, 0=仅按个人消费排除'");
- // 迁移140: activities表添加family_scope列(推荐系统-家庭参与去重,原migration102内从未被调用)
- ensureColumn("activities", "family_scope", "TINYINT DEFAULT 1 COMMENT '是否面向家庭: 1=家庭成员参与过即排除, 0=仅按个人参与排除'");
- // 迁移141: 修正五维权重维度code体系(后台DimensionWeightPicker曾用mind=智/heart=心,标准体系为wisdom=智/mind=心)
- // 旧体系写入的数据导致小程序端按wisdom/mind查询JSON_EXTRACT匹配不到,新活动显示不出来
- try {
- // 1. dimension_weights 表:仅处理含 heart 的目标(旧体系写入的),mind(智)→wisdom,heart(心)→mind
- int dwHeartCount = jdbcTemplate.queryForObject(
- "SELECT COUNT(*) FROM dimension_weights WHERE dimension = 'heart'", Integer.class);
- if (dwHeartCount > 0) {
- jdbcTemplate.update("UPDATE dimension_weights dw " +
- "JOIN (SELECT DISTINCT target_type, target_id FROM dimension_weights WHERE dimension = 'heart') h " +
- "ON dw.target_type = h.target_type AND dw.target_id = h.target_id " +
- "SET dw.dimension = 'wisdom' WHERE dw.dimension = 'mind'");
- jdbcTemplate.update("UPDATE dimension_weights SET dimension = 'mind' WHERE dimension = 'heart'");
- log.info("已修正dimension_weights表维度code: {}条heart记录", dwHeartCount);
- }
- } catch (Exception e) {
- log.warn("修正dimension_weights表维度code失败: {}", e.getMessage());
- }
- try {
- // 2. 主表 dimension_weights JSON 列:含 heart 键的记录,mind键→wisdom键,heart键→mind键
- String[] jsonTables = {"activities", "products", "articles"};
- for (String table : jsonTables) {
- int jsonCount = jdbcTemplate.queryForObject(
- "SELECT COUNT(*) FROM " + table + " WHERE dimension_weights IS NOT NULL " +
- "AND JSON_CONTAINS_PATH(dimension_weights, 'one', '$.heart')", Integer.class);
- if (jsonCount > 0) {
- jdbcTemplate.update("UPDATE " + table + " SET dimension_weights = JSON_SET(" +
- "JSON_REMOVE(JSON_REMOVE(dimension_weights, '$.mind'), '$.heart'), " +
- "'$.wisdom', JSON_EXTRACT(dimension_weights, '$.mind'), " +
- "'$.mind', JSON_EXTRACT(dimension_weights, '$.heart')) " +
- "WHERE dimension_weights IS NOT NULL " +
- "AND JSON_CONTAINS_PATH(dimension_weights, 'one', '$.heart')");
- log.info("已修正{}表dimension_weights JSON: {}条", table, jsonCount);
- }
- }
- } catch (Exception e) {
- log.warn("修正主表dimension_weights JSON失败: {}", e.getMessage());
- }
- // 迁移142: activities表添加报名截止时间与会员/非会员双收费字段(活动升级:报名时间窗口+孩子/家长双收费+会员区分,需求#3#6)
- ensureColumn("activities", "registration_deadline", "DATETIME COMMENT '报名截止时间(NULL=不限制, 默认=活动开始时间)'");
- ensureColumn("activities", "child_fee", "INT DEFAULT 0 COMMENT '孩子·非会员价(分)'");
- ensureColumn("activities", "child_member_fee", "INT DEFAULT 0 COMMENT '孩子·会员价(分), 0=免费'");
- ensureColumn("activities", "parent_fee", "INT DEFAULT 0 COMMENT '家长·非会员价(分)'");
- ensureColumn("activities", "parent_member_fee", "INT DEFAULT 0 COMMENT '家长·会员价(分), 0=免费'");
- ensureColumn("activities", "creator_id", "BIGINT COMMENT '创建人userId(普通用户发布者, admin创建可为空)'");
- // 迁移143: 创建活动报名订单表(活动付费报名:先支付后报名,需求#4)
- try {
- jdbcTemplate.execute("CREATE TABLE IF NOT EXISTS activity_orders (" +
- "id BIGINT AUTO_INCREMENT PRIMARY KEY, " +
- "order_no VARCHAR(64) NOT NULL UNIQUE COMMENT '订单号(ACT+时间戳+随机)', " +
- "activity_id BIGINT NOT NULL COMMENT '活动ID', " +
- "user_id BIGINT NOT NULL COMMENT '下单人userId', " +
- "total_amount INT NOT NULL COMMENT '实付金额(分)', " +
- "registrants_json TEXT COMMENT '报名人快照JSON: [{name, phone, childId, type: child/parent, fee}]', " +
- "status VARCHAR(20) DEFAULT 'pending' COMMENT '状态: pending/paid/cancelled', " +
- "transaction_id VARCHAR(64) COMMENT '微信支付单号', " +
- "paid_at DATETIME COMMENT '支付时间', " +
- "created_at DATETIME DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间', " +
- "INDEX idx_activity (activity_id), " +
- "INDEX idx_user (user_id), " +
- "INDEX idx_status (status)" +
- ") ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='活动报名订单表'");
- log.info("已创建activity_orders表");
- } catch (Exception e) {
- log.warn("创建activity_orders表失败: {}", e.getMessage());
- }
- // 迁移144: 创建活动总结资料表(总结中阶段发布人上传资料,需求#9)
- try {
- jdbcTemplate.execute("CREATE TABLE IF NOT EXISTS activity_materials (" +
- "id BIGINT AUTO_INCREMENT PRIMARY KEY, " +
- "activity_id BIGINT NOT NULL COMMENT '活动ID', " +
- "title VARCHAR(200) COMMENT '标题', " +
- "file_url VARCHAR(500) NOT NULL COMMENT '文件地址(图片/PDF/外链URL)', " +
- "file_type VARCHAR(20) DEFAULT 'image' COMMENT '类型: image/pdf/link', " +
- "created_at DATETIME DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间', " +
- "INDEX idx_activity (activity_id)" +
- ") ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='活动总结资料表'");
- log.info("已创建activity_materials表");
- } catch (Exception e) {
- log.warn("创建activity_materials表失败: {}", e.getMessage());
- }
- }
- private void runMigration82() {
- try {
- jdbcTemplate.execute("CREATE TABLE IF NOT EXISTS ai_conversation_summary (" +
- "id BIGINT AUTO_INCREMENT PRIMARY KEY, " +
- "user_id BIGINT NOT NULL COMMENT '用户ID', " +
- "conversation_id VARCHAR(100) NOT NULL COMMENT 'Dify会话ID', " +
- "summary_text TEXT COMMENT '对话摘要文本', " +
- "summary_tokens INT DEFAULT 0 COMMENT '摘要token数', " +
- "message_count INT DEFAULT 0 COMMENT '本次对话消息数', " +
- "created_at DATETIME DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间', " +
- "INDEX idx_conversation (conversation_id), " +
- "INDEX idx_user (user_id)" +
- ") ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='AI对话摘要'");
- log.info("已创建ai_conversation_summary表");
- } catch (Exception e) {
- log.warn("创建ai_conversation_summary表失败: {}", e.getMessage());
- }
- try {
- jdbcTemplate.execute("CREATE TABLE IF NOT EXISTS ai_user_facts (" +
- "id BIGINT AUTO_INCREMENT PRIMARY KEY, " +
- "user_id BIGINT NOT NULL COMMENT '用户ID', " +
- "fact_key VARCHAR(100) NOT NULL COMMENT '事实键', " +
- "fact_value VARCHAR(500) COMMENT '事实值', " +
- "source_conversation VARCHAR(100) COMMENT '来源会话ID', " +
- "confidence DECIMAL(3,2) DEFAULT 1.00 COMMENT '置信度0-1', " +
- "created_at DATETIME DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间', " +
- "updated_at DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间', " +
- "INDEX idx_user_key (user_id, fact_key), " +
- "INDEX idx_conversation (source_conversation)" +
- ") ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='AI用户关键事实'");
- log.info("已创建ai_user_facts表");
- } catch (Exception e) {
- log.warn("创建ai_user_facts表失败: {}", e.getMessage());
- }
- }
- private void runMigration86() {
- try {
- jdbcTemplate.execute("CREATE TABLE IF NOT EXISTS supply_hierarchy (" +
- "id BIGINT AUTO_INCREMENT PRIMARY KEY, " +
- "system_id BIGINT NOT NULL COMMENT '供应商体系ID', " +
- "parent_id BIGINT NOT NULL COMMENT '上级供应商ID', " +
- "child_id BIGINT NOT NULL COMMENT '下级供应商ID', " +
- "level INT DEFAULT 1 COMMENT '层级深度', " +
- "status VARCHAR(20) DEFAULT 'active', " +
- "created_at DATETIME DEFAULT CURRENT_TIMESTAMP, " +
- "updated_at DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, " +
- "UNIQUE KEY uk_system_parent_child (system_id, parent_id, child_id), " +
- "KEY idx_system_child (system_id, child_id), " +
- "KEY idx_parent (parent_id)" +
- ") ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='供应商上下级关系表'");
- log.info("已创建supply_hierarchy表");
- } catch (Exception e) {
- log.warn("创建supply_hierarchy表失败: {}", e.getMessage());
- }
- try {
- jdbcTemplate.execute("CREATE TABLE IF NOT EXISTS supply_hierarchy_change_request (" +
- "id BIGINT AUTO_INCREMENT PRIMARY KEY, " +
- "system_id BIGINT NOT NULL, " +
- "applicant_id BIGINT NOT NULL COMMENT '申请人ID', " +
- "target_parent_id BIGINT NOT NULL COMMENT '目标上级ID', " +
- "status VARCHAR(20) DEFAULT 'pending', " +
- "reason VARCHAR(500), " +
- "reject_reason VARCHAR(500), " +
- "created_at DATETIME DEFAULT CURRENT_TIMESTAMP, " +
- "reviewed_at DATETIME, " +
- "reviewed_by BIGINT, " +
- "KEY idx_system_applicant (system_id, applicant_id), " +
- "KEY idx_status (status)" +
- ") ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='供应商更换上级申请表'");
- log.info("已创建supply_hierarchy_change_request表");
- } catch (Exception e) {
- log.warn("创建supply_hierarchy_change_request表失败: {}", e.getMessage());
- }
- try {
- jdbcTemplate.execute("CREATE TABLE IF NOT EXISTS butler_profiles (" +
- "id BIGINT AUTO_INCREMENT PRIMARY KEY, " +
- "user_id BIGINT NOT NULL, " +
- "tier VARCHAR(10), " +
- "status VARCHAR(20) DEFAULT 'pending', " +
- "member_count INT DEFAULT 0, " +
- "max_members INT DEFAULT 50, " +
- "annual_fee_l1 BIGINT, " +
- "annual_fee_l2 BIGINT, " +
- "description TEXT, " +
- "certificates TEXT, " +
- "reject_reason VARCHAR(500), " +
- "commission_rate_l1 INT DEFAULT 1000, " +
- "commission_rate_l2 INT DEFAULT 5000, " +
- "created_at DATETIME DEFAULT CURRENT_TIMESTAMP, " +
- "updated_at DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, " +
- "approved_at DATETIME, " +
- "UNIQUE KEY uk_user_id (user_id), " +
- "INDEX idx_status (status), " +
- "INDEX idx_tier (tier)" +
- ") ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='管家服务提供商档案'");
- log.info("已创建butler_profiles表");
- } catch (Exception e) {
- log.warn("创建butler_profiles表失败: {}", e.getMessage());
- }
- try {
- jdbcTemplate.execute("CREATE TABLE IF NOT EXISTS butler_service_records (" +
- "id BIGINT AUTO_INCREMENT PRIMARY KEY, " +
- "butler_id BIGINT NOT NULL, " +
- "member_user_id BIGINT NOT NULL, " +
- "subscription_id BIGINT, " +
- "service_type VARCHAR(20) NOT NULL, " +
- "amount_p BIGINT NOT NULL, " +
- "platform_fee_p BIGINT DEFAULT 0, " +
- "butler_amount_p BIGINT DEFAULT 0, " +
- "member_tier VARCHAR(10), " +
- "`year_month` VARCHAR(7), " +
- "remark VARCHAR(500), " +
- "created_at DATETIME DEFAULT CURRENT_TIMESTAMP, " +
- "INDEX idx_butler_id (butler_id), " +
- "INDEX idx_member_user_id (member_user_id), " +
- "INDEX idx_year_month (`year_month`)" +
- ") ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='管家服务记录'");
- log.info("已创建butler_service_records表");
- } catch (Exception e) {
- log.warn("创建butler_service_records表失败: {}", e.getMessage());
- }
- try {
- jdbcTemplate.execute("CREATE TABLE IF NOT EXISTS butler_commission_settlements (" +
- "id BIGINT AUTO_INCREMENT PRIMARY KEY, " +
- "butler_id BIGINT NOT NULL, " +
- "`year_month` VARCHAR(7) NOT NULL, " +
- "member_count INT DEFAULT 0, " +
- "annual_base_amount_p BIGINT DEFAULT 0, " +
- "annual_butler_amount_p BIGINT DEFAULT 0, " +
- "annual_platform_amount_p BIGINT DEFAULT 0, " +
- "per_session_total_p BIGINT DEFAULT 0, " +
- "per_session_platform_total_p BIGINT DEFAULT 0, " +
- "per_session_butler_total_p BIGINT DEFAULT 0, " +
- "total_butler_amount_p BIGINT DEFAULT 0, " +
- "status VARCHAR(20) DEFAULT 'pending', " +
- "settled_at DATETIME, " +
- "settled_by BIGINT, " +
- "created_at DATETIME DEFAULT CURRENT_TIMESTAMP, " +
- "UNIQUE KEY uk_butler_month (butler_id, `year_month`), " +
- "INDEX idx_status (status), " +
- "INDEX idx_year_month (`year_month`)" +
- ") ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='管家月度佣金结算单'");
- log.info("已创建butler_commission_settlements表");
- } catch (Exception e) {
- log.warn("创建butler_commission_settlements表失败: {}", e.getMessage());
- }
- try {
- jdbcTemplate.execute("ALTER TABLE users ADD COLUMN butler_status VARCHAR(20)");
- jdbcTemplate.execute("ALTER TABLE users ADD COLUMN butler_info TEXT");
- jdbcTemplate.execute("ALTER TABLE users ADD COLUMN butler_approved_at DATETIME");
- log.info("已添加 butler 相关列到 users 表");
- } catch (Exception e) {
- log.warn("Migration skipped (already exists): {}", e.getMessage());
- }
- // 迁移 86: 创建 butler_assignments 表(会员 - 管家分配记录)
- try {
- jdbcTemplate.execute("CREATE TABLE IF NOT EXISTS butler_assignments (" +
- "id BIGINT AUTO_INCREMENT PRIMARY KEY, " +
- "family_id BIGINT NOT NULL COMMENT '家庭 ID', " +
- "butler_user_id BIGINT NOT NULL COMMENT '管家用户 ID', " +
- "subscription_id BIGINT COMMENT '关联订阅 ID', " +
- "level VARCHAR(10) NOT NULL COMMENT '订阅级别 L1/L2', " +
- "assigned_at DATETIME DEFAULT CURRENT_TIMESTAMP, " +
- "UNIQUE KEY uk_family_assignment (family_id), " +
- "INDEX idx_butler_user_id (butler_user_id), " +
- "INDEX idx_subscription_id (subscription_id)" +
- ") ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='会员 - 管家分配记录'");
- log.info("已创建 butler_assignments 表");
- } catch (Exception e) {
- log.warn("创建 butler_assignments 表失败:{}", e.getMessage());
- }
- }
- private void runMigration88() {
- // 迁移 88: 创建家庭公共账户体系(family_earnings / family_earnings_member / family_earnings_record)
- // 表 1: family_earnings(家庭总账)
- try {
- jdbcTemplate.execute("CREATE TABLE IF NOT EXISTS family_earnings (" +
- "id BIGINT AUTO_INCREMENT PRIMARY KEY, " +
- "family_id BIGINT NOT NULL UNIQUE COMMENT '家庭 ID', " +
- "total_earned BIGINT NOT NULL DEFAULT 0 COMMENT '累计推荐收入(分)', " +
- "available_amount BIGINT NOT NULL DEFAULT 0 COMMENT '当前可支配余额(分)', " +
- "withdrawn_amount BIGINT NOT NULL DEFAULT 0 COMMENT '已提现金额(分)', " +
- "distributed_amount BIGINT NOT NULL DEFAULT 0 COMMENT '已颁发给成员的金额(分)', " +
- "updated_at DATETIME, " +
- "created_at DATETIME DEFAULT CURRENT_TIMESTAMP, " +
- "INDEX idx_family_id (family_id)" +
- ") ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='家庭公共账户'");
- log.info("已创建 family_earnings 表");
- } catch (Exception e) {
- log.warn("创建 family_earnings 表失败:{}", e.getMessage());
- }
- // 表 2: family_earnings_member(成员已分配余额)
- try {
- jdbcTemplate.execute("CREATE TABLE IF NOT EXISTS family_earnings_member (" +
- "id BIGINT AUTO_INCREMENT PRIMARY KEY, " +
- "family_id BIGINT NOT NULL, " +
- "user_id BIGINT NOT NULL, " +
- "allocated_amount BIGINT NOT NULL DEFAULT 0 COMMENT '管理员累计分配(分)', " +
- "consumed_amount BIGINT NOT NULL DEFAULT 0 COMMENT '已消费金额(分)', " +
- "available_amount BIGINT NOT NULL DEFAULT 0 COMMENT '可消费余额=allocated-consumed(分)', " +
- "can_use_family_balance TINYINT(1) NOT NULL DEFAULT 0 COMMENT '是否允许使用家庭公共池', " +
- "updated_at DATETIME, " +
- "created_at DATETIME DEFAULT CURRENT_TIMESTAMP, " +
- "UNIQUE KEY idx_family_user (family_id, user_id)" +
- ") ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='家庭成员已分配余额'");
- log.info("已创建 family_earnings_member 表");
- } catch (Exception e) {
- log.warn("创建 family_earnings_member 表失败:{}", e.getMessage());
- }
- // 表 3: family_earnings_record(收支流水)
- try {
- jdbcTemplate.execute("CREATE TABLE IF NOT EXISTS family_earnings_record (" +
- "id BIGINT AUTO_INCREMENT PRIMARY KEY, " +
- "family_id BIGINT NOT NULL, " +
- "type VARCHAR(20) NOT NULL COMMENT 'earn 收入/withdraw 提现/distribute 颁发/consume 消费', " +
- "amount BIGINT NOT NULL COMMENT '金额(分)', " +
- "from_user_id BIGINT COMMENT '操作人', " +
- "to_user_id BIGINT COMMENT '受益人(颁发/消费时填写)', " +
- "note VARCHAR(255) COMMENT '备注', " +
- "created_at DATETIME DEFAULT CURRENT_TIMESTAMP, " +
- "INDEX idx_family_id (family_id), " +
- "INDEX idx_type (type)" +
- ") ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='家庭收益收支流水'");
- log.info("已创建 family_earnings_record 表");
- } catch (Exception e) {
- log.warn("创建 family_earnings_record 表失败:{}", e.getMessage());
- }
- // commission_records 加 family_id 列
- ensureColumn("commission_records", "family_id", "BIGINT COMMENT '归属家庭 ID'");
- // 迁移90: contacts表添加家庭成员关联字段(联系人→家庭成员升级路径)
- ensureColumn("contacts", "family_member_id", "BIGINT DEFAULT NULL COMMENT '关联的家庭成员ID'");
- ensureColumn("contacts", "invited_status", "VARCHAR(16) DEFAULT 'none' COMMENT '邀请加入家庭状态: none/invited/accepted/declined'");
- // 迁移91: 创建family_member_logs表(家庭成员变更日志)
- try {
- jdbcTemplate.execute("CREATE TABLE IF NOT EXISTS family_member_logs (" +
- "id BIGINT AUTO_INCREMENT PRIMARY KEY, " +
- "family_id BIGINT NOT NULL COMMENT '家庭ID', " +
- "member_id BIGINT NOT NULL COMMENT '成员ID', " +
- "action VARCHAR(32) NOT NULL COMMENT '操作: join/leave/update/kick', " +
- "old_value VARCHAR(500) COMMENT '旧值', " +
- "new_value VARCHAR(500) COMMENT '新值', " +
- "operator_id BIGINT COMMENT '操作人用户ID', " +
- "created_at DATETIME DEFAULT CURRENT_TIMESTAMP, " +
- "INDEX idx_family_id (family_id), " +
- "INDEX idx_member_id (member_id)" +
- ") ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='家庭成员变更日志'");
- log.info("已创建family_member_logs表");
- } catch (Exception e) {
- // 表已存在,忽略错误
- }
- // 迁移92: 创建health_meal_record表(饮食打卡记录)
- try {
- jdbcTemplate.execute("CREATE TABLE IF NOT EXISTS health_meal_record (" +
- "id BIGINT AUTO_INCREMENT PRIMARY KEY, " +
- "member_id BIGINT NOT NULL COMMENT '家庭成员ID', " +
- "checkin_id BIGINT COMMENT '关联打卡ID', " +
- "meal_type VARCHAR(32) COMMENT '餐次: breakfast/lunch/dinner/snack', " +
- "food_items TEXT COMMENT '食物清单JSON', " +
- "total_calories INT COMMENT '总卡路里', " +
- "protein_g INT COMMENT '蛋白质克数', " +
- "fat_g INT COMMENT '脂肪克数', " +
- "carbs_g INT COMMENT '碳水克数', " +
- "fiber_g INT COMMENT '膳食纤维克数', " +
- "meal_time DATETIME COMMENT '用餐时间', " +
- "remark VARCHAR(500) COMMENT '备注', " +
- "created_at DATETIME DEFAULT CURRENT_TIMESTAMP, " +
- "INDEX idx_member_id (member_id)" +
- ") ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='饮食打卡记录'");
- log.info("已创建health_meal_record表");
- } catch (Exception e) {
- // 表已存在,忽略错误
- }
- // 迁移93: 创建health_meditation_record表(冥想打卡记录)
- try {
- jdbcTemplate.execute("CREATE TABLE IF NOT EXISTS health_meditation_record (" +
- "id BIGINT AUTO_INCREMENT PRIMARY KEY, " +
- "member_id BIGINT NOT NULL COMMENT '家庭成员ID', " +
- "checkin_id BIGINT COMMENT '关联打卡ID', " +
- "duration_minutes INT COMMENT '时长(分钟)', " +
- "type VARCHAR(32) COMMENT '类型: mindfulness/guided/breathing/body_scan', " +
- "mood VARCHAR(32) COMMENT '冥想后心情', " +
- "remark VARCHAR(500) COMMENT '备注', " +
- "created_at DATETIME DEFAULT CURRENT_TIMESTAMP, " +
- "INDEX idx_member_id (member_id)" +
- ") ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='冥想打卡记录'");
- log.info("已创建health_meditation_record表");
- } catch (Exception e) {
- // 表已存在,忽略错误
- }
- // 迁移94: 创建ecom_supplier表(电商供应商管理)
- try {
- jdbcTemplate.execute("CREATE TABLE IF NOT EXISTS ecom_supplier (" +
- "id BIGINT AUTO_INCREMENT PRIMARY KEY, " +
- "name VARCHAR(100) NOT NULL COMMENT '供应商名称', " +
- "contact_name VARCHAR(50) COMMENT '联系人', " +
- "contact_phone VARCHAR(20) COMMENT '联系电话', " +
- "status TINYINT DEFAULT 1 COMMENT '启用状态 1启用 0禁用', " +
- "remark VARCHAR(500) COMMENT '备注', " +
- "created_at DATETIME DEFAULT CURRENT_TIMESTAMP, " +
- "updated_at DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP" +
- ") ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='电商供应商'");
- log.info("已创建ecom_supplier表");
- } catch (Exception e) {
- // 表已存在,忽略错误
- }
- // 迁移95: 创建rebate_commission_config表(佣金返利配置)
- try {
- jdbcTemplate.execute("CREATE TABLE IF NOT EXISTS rebate_commission_config (" +
- "id BIGINT AUTO_INCREMENT PRIMARY KEY, " +
- "name VARCHAR(100) NOT NULL COMMENT '配置名称', " +
- "rate INT COMMENT '佣金比例(万分比)', " +
- "rate_type TINYINT DEFAULT 1 COMMENT '比例类型 1百分比 2固定金额', " +
- "min_amount INT COMMENT '最低金额(分)', " +
- "max_amount INT COMMENT '最高金额(分)', " +
- "remark VARCHAR(500) COMMENT '备注', " +
- "created_at DATETIME DEFAULT CURRENT_TIMESTAMP, " +
- "updated_at DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP" +
- ") ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='佣金返利配置'");
- log.info("已创建rebate_commission_config表");
- } catch (Exception e) {
- // 表已存在,忽略错误
- }
- }
- private void runMigration89() {
- // 迁移 89: supply_system表添加models字段(AI模型供应商配置)
- ensureColumn("supply_system", "models", "TEXT COMMENT 'AI模型列表JSON,如[\"agnes-image-2.0-flash\",\"agnes-2.5-flash\"]'");
- log.info("已添加models列到supply_system表");
- }
- private void runMigration90() {
- // 迁移 90: supply_system表添加api_endpoint和api_key字段
- ensureColumn("supply_system", "api_endpoint", "VARCHAR(512) COMMENT 'API端点'");
- ensureColumn("supply_system", "api_key", "VARCHAR(256) COMMENT 'API密钥'");
- log.info("已添加api_endpoint和api_key列到supply_system表");
- }
- private void runMigration91() {
- // 迁移 91: users表添加营养师审核字段
- ensureColumn("users", "nutritionist_status", "VARCHAR(20) DEFAULT NULL COMMENT '营养师审核状态: pending/approved/rejected'");
- ensureColumn("users", "nutritionist_reject_reason", "VARCHAR(500) DEFAULT NULL COMMENT '营养师审核拒绝原因'");
- log.info("已添加nutritionist_status和nutritionist_reject_reason列到users表");
- }
- private void runMigration92() {
- // 迁移 92: 创建dan_assessment_executions表(DAN测评服务执行记录)
- try {
- jdbcTemplate.execute("CREATE TABLE IF NOT EXISTS `dan_assessment_executions` (" +
- "`id` BIGINT AUTO_INCREMENT PRIMARY KEY, " +
- "`assessment_execution_id` BIGINT NOT NULL COMMENT '执行记录ID', " +
- "`assessment_order_id` BIGINT NOT NULL COMMENT '订单ID', " +
- "`child_id` BIGINT NOT NULL COMMENT '孩子ID', " +
- "`family_id` BIGINT NOT NULL COMMENT '家庭ID', " +
- "`quota_id` BIGINT COMMENT '额度ID', " +
- "`assessor_id` BIGINT NOT NULL COMMENT '测评师userId', " +
- "`assessor_status` ENUM('pending','completed','rejected') DEFAULT 'pending' COMMENT '测评师状态', " +
- "`assessor_completed_at` DATETIME COMMENT '测评师完成时间', " +
- "`assessor_notes` VARCHAR(200) COMMENT '测评摘要/服务备注', " +
- "`planner_id` BIGINT COMMENT '规划师userId', " +
- "`planner_status` ENUM('pending','completed','rejected') DEFAULT 'pending' COMMENT '规划师状态', " +
- "`planner_completed_at` DATETIME COMMENT '规划师完成时间', " +
- "`planner_notes` VARCHAR(200) COMMENT '规划服务备注', " +
- "`user_confirmed` TINYINT(1) DEFAULT 0 COMMENT '用户确认:0未确认,1已确认', " +
- "`user_confirmed_at` DATETIME COMMENT '用户确认时间', " +
- "`created_at` DATETIME DEFAULT CURRENT_TIMESTAMP, " +
- "`updated_at` DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, " +
- "KEY `idx_assessment_execution_id` (`assessment_execution_id`), " +
- "KEY `idx_assessment_order_id` (`assessment_order_id`), " +
- "KEY `idx_assessor_id` (`assessor_id`), " +
- "KEY `idx_planner_id` (`planner_id`), " +
- "KEY `idx_child_id` (`child_id`), " +
- "KEY `idx_family_id` (`family_id`)" +
- ") ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='DAN测评服务执行记录'");
- log.info("已创建dan_assessment_executions表");
- } catch (Exception e) {
- // 表已存在,忽略错误
- }
- // 迁移 92: 创建service_ratings表(服务评分记录)
- try {
- jdbcTemplate.execute("CREATE TABLE IF NOT EXISTS `service_ratings` (" +
- "`id` BIGINT AUTO_INCREMENT PRIMARY KEY, " +
- "`execution_id` BIGINT NOT NULL COMMENT '执行记录ID', " +
- "`assessment_order_id` BIGINT NOT NULL COMMENT '订单ID', " +
- "`user_id` BIGINT NOT NULL COMMENT '评分用户ID', " +
- "`assessor_id` BIGINT NOT NULL COMMENT '测评师userId', " +
- "`assessor_rating` TINYINT DEFAULT 0 COMMENT '测评师评分 1-5', " +
- "`assessor_comment` VARCHAR(500) COMMENT '评价内容', " +
- "`planner_id` BIGINT COMMENT '规划师userId', " +
- "`planner_rating` TINYINT COMMENT '规划师评分 1-5', " +
- "`planner_comment` VARCHAR(500) COMMENT '评价内容', " +
- "`created_at` DATETIME DEFAULT CURRENT_TIMESTAMP, " +
- "UNIQUE KEY `uniq_execution_user` (`execution_id`, `user_id`)," +
- "KEY `idx_assessor_id` (`assessor_id`)," +
- "KEY `idx_planner_id` (`planner_id`)" +
- ") ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='服务评分记录'");
- log.info("已创建service_ratings表");
- } catch (Exception e) {
- // 表已存在,忽略错误
- }
- // 迁移 92: 创建service_settlements表(服务结算记录)
- try {
- jdbcTemplate.execute("CREATE TABLE IF NOT EXISTS `service_settlements` (" +
- "`id` BIGINT AUTO_INCREMENT PRIMARY KEY, " +
- "`execution_id` BIGINT NOT NULL COMMENT '执行记录ID', " +
- "`assessment_order_id` BIGINT NOT NULL COMMENT '订单ID', " +
- "`provider_id` BIGINT NOT NULL COMMENT '服务提供方userId', " +
- "`provider_type` ENUM('assessor','planner') NOT NULL COMMENT '服务方类型', " +
- "`service_fee` BIGINT DEFAULT 0 COMMENT '服务费用(分)', " +
- "`commission_rate` INT COMMENT '佣金比例(bps)', " +
- "`commission_amount` BIGINT DEFAULT 0 COMMENT '结算金额(分)', " +
- "`status` ENUM('pending','settled','failed') DEFAULT 'pending' COMMENT '结算状态', " +
- "`settled_at` DATETIME COMMENT '结算时间', " +
- "`transaction_id` VARCHAR(100) COMMENT '转账交易ID', " +
- "`created_at` DATETIME DEFAULT CURRENT_TIMESTAMP, " +
- "`updated_at` DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, " +
- "KEY `idx_execution_id` (`execution_id`), " +
- "KEY `idx_provider_id` (`provider_id`), " +
- "KEY `idx_status` (`status`)" +
- ") ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='服务结算记录'");
- log.info("已创建service_settlements表");
- } catch (Exception e) {
- // 表已存在,忽略错误
- }
- // 迁移 96: product_orders 添加活动provision字段 + activity_provisions 表(活动商品统一管理)
- try {
- jdbcTemplate.execute("ALTER TABLE product_orders ADD COLUMN activity_id BIGINT DEFAULT NULL COMMENT '关联活动ID(product_type=activity时有效)'");
- log.info("已添加activity_id列到product_orders表");
- } catch (Exception e) {
- // 列已存在,忽略错误
- }
- try {
- jdbcTemplate.execute("ALTER TABLE product_orders ADD COLUMN provision_status VARCHAR(32) DEFAULT NULL COMMENT '活动provision状态: unprovisioned(待审核)/provisioned(已发货)/signed_in(已签到)/completed(已完成)/cancelled(审核拒绝)'");
- log.info("已添加provision_status列到product_orders表");
- } catch (Exception e) {
- // 列已存在,忽略错误
- }
- try {
- jdbcTemplate.execute("ALTER TABLE product_orders ADD COLUMN auditor_id BIGINT DEFAULT NULL COMMENT '审核人ID'");
- log.info("已添加auditor_id列到product_orders表");
- } catch (Exception e) {
- // 列已存在,忽略错误
- }
- try {
- jdbcTemplate.execute("ALTER TABLE product_orders ADD COLUMN audited_at DATETIME DEFAULT NULL COMMENT '审核时间'");
- log.info("已添加audited_at列到product_orders表");
- } catch (Exception e) {
- // 列已存在,忽略错误
- }
- try {
- jdbcTemplate.execute("ALTER TABLE product_orders ADD COLUMN audit_reason TEXT DEFAULT NULL COMMENT '审核拒绝原因'");
- log.info("已添加audit_reason列到product_orders表");
- } catch (Exception e) {
- // 列已存在,忽略错误
- }
- try {
- jdbcTemplate.execute("CREATE TABLE IF NOT EXISTS activity_provisions (" +
- "id BIGINT AUTO_INCREMENT PRIMARY KEY, " +
- "order_id BIGINT NOT NULL COMMENT '商品订单ID', " +
- "order_no VARCHAR(64) NOT NULL COMMENT '订单编号', " +
- "product_id BIGINT NOT NULL COMMENT '商品ID', " +
- "activity_id BIGINT NOT NULL COMMENT '活动ID', " +
- "buyer_id BIGINT NOT NULL COMMENT '购买者用户ID', " +
- "family_id BIGINT COMMENT '家庭ID', " +
- "child_id BIGINT COMMENT '报名孩子ID(签到时填写)', " +
- "status VARCHAR(32) NOT NULL DEFAULT 'unprovisioned' COMMENT 'unprovisioned/provisioned/signed_in/completed/cancelled', " +
- "qr_code VARCHAR(512) COMMENT '签到二维码', " +
- "checked_in_at DATETIME COMMENT '签到时间', " +
- "completed_at DATETIME COMMENT '完成时间', " +
- "refund_amount INT COMMENT '退款金额(分)', " +
- "refund_at DATETIME COMMENT '退款时间', " +
- "created_at DATETIME DEFAULT CURRENT_TIMESTAMP, " +
- "updated_at DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, " +
- "INDEX idx_order (order_id), " +
- "INDEX idx_activity (activity_id), " +
- "INDEX idx_buyer (buyer_id), " +
- "INDEX idx_status (status)" +
- ") ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='活动provision记录表'");
- log.info("已创建activity_provisions表");
- } catch (Exception e) {
- // 表已存在,忽略错误
- }
- // 迁移 97: DAN测评服务佣金配置
- seedDanCommissionConfig();
- // 迁移 98: 创建 health_knowledge_base 表(菌群/营养知识库)
- try {
- jdbcTemplate.execute("CREATE TABLE IF NOT EXISTS `health_knowledge_base` (" +
- "`id` BIGINT AUTO_INCREMENT PRIMARY KEY, " +
- "`item_type` VARCHAR(50) NOT NULL COMMENT '类型: bacteria/nutrient/indicator/vitamin/amino_acid/disease', " +
- "`item_name` VARCHAR(200) NOT NULL COMMENT '项目名称(精确匹配)', " +
- "`category` VARCHAR(100) COMMENT '分类(如 有益菌/有害菌)', " +
- "`normal_range` VARCHAR(200) COMMENT '正常范围参考', " +
- "`description` TEXT COMMENT '详细说明', " +
- "`suggestion` TEXT COMMENT '相关建议', " +
- "`source` VARCHAR(100) COMMENT '数据来源', " +
- "`created_at` DATETIME DEFAULT CURRENT_TIMESTAMP, " +
- "`updated_at` DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, " +
- "UNIQUE KEY `uk_item_type_name` (`item_type`, `item_name`), " +
- "KEY `idx_item_type` (`item_type`)" +
- ") ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='健康知识库 - 菌属/营养素/指标说明'");
- log.info("已创建health_knowledge_base表");
- } catch (Exception e) {
- // 表已存在,忽略错误
- }
- }
- private void seedDanCommissionConfig() {
- try {
- jdbcTemplate.execute("INSERT IGNORE INTO sys_config (config_key, config_value, description, created_at) VALUES " +
- "('dan_commission_rate', '10', 'DAN测评服务通用佣金比例(%)', NOW()), " +
- "('dan_commission_rate_assessor', '10', 'DAN测评评估师佣金比例(%)', NOW()), " +
- "('dan_commission_rate_planner', '10', 'DAN测评规划师佣金比例(%)', NOW())");
- log.info("已初始化DAN测评佣金配置");
- } catch (Exception e) {
- log.warn("DAN佣金配置种子数据可能已存在: " + e.getMessage());
- }
- // 迁移 98: users 表添加软删除标记(@TableLogic)
- try {
- jdbcTemplate.execute("ALTER TABLE users ADD COLUMN deleted TINYINT DEFAULT 0 COMMENT '软删除标记: 0=未删除, 1=已删除'");
- log.info("已添加deleted列到users表");
- } catch (Exception e) {
- // 列已存在,忽略错误
- }
- // 迁移 103: products表添加min_quantity列(商品最小购买数限制)
- ensureColumn("products", "min_quantity", "INT DEFAULT 1 COMMENT '最小购买数量限制'");
- // 迁移 104: tianpan_daily_fortune表添加缺失字段(FamilyFortune实体对齐)
- ensureColumn("tianpan_daily_fortune", "lucky_direction", "VARCHAR(20) COMMENT '幸运方向'");
- ensureColumn("tianpan_daily_fortune", "fortune_level", "VARCHAR(20) COMMENT '运势等级'");
- ensureColumn("tianpan_daily_fortune", "fortune_level_text", "VARCHAR(100) COMMENT '运势等级描述'");
- ensureColumn("tianpan_daily_fortune", "fortune_description", "TEXT COMMENT '运势描述'");
- ensureColumn("tianpan_daily_fortune", "mood_score", "INT DEFAULT 50 COMMENT '心情分数'");
- ensureColumn("tianpan_daily_fortune", "mood_emoji", "VARCHAR(10) COMMENT '心情emoji'");
- ensureColumn("tianpan_daily_fortune", "mood_text", "VARCHAR(50) COMMENT '心情文本'");
- ensureColumn("tianpan_daily_fortune", "weekly_tip", "TEXT COMMENT '每周提示'");
- // 验证迁移104: 确认tianpan_daily_fortune关键列存在
- try {
- jdbcTemplate.queryForObject(
- "SELECT COUNT(*) FROM information_schema.COLUMNS WHERE TABLE_SCHEMA=DATABASE() AND TABLE_NAME='tianpan_daily_fortune' AND COLUMN_NAME='fortune_level_text'",
- Integer.class);
- log.info("验证通过: tianpan_daily_fortune.fortune_level_text 列存在");
- } catch (Exception e) {
- log.error("验证失败: tianpan_daily_fortune.fortune_level_text 列仍然不存在,迁移可能失败: {}", e.getMessage());
- }
- // 验证迁移103: 确认products.min_quantity列存在
- try {
- jdbcTemplate.queryForObject(
- "SELECT COUNT(*) FROM information_schema.COLUMNS WHERE TABLE_SCHEMA=DATABASE() AND TABLE_NAME='products' AND COLUMN_NAME='min_quantity'",
- Integer.class);
- log.info("验证通过: products.min_quantity 列存在");
- } catch (Exception e) {
- log.error("验证失败: products.min_quantity 列仍然不存在,迁移可能失败: {}", e.getMessage());
- }
- // 迁移 105: users表添加article_admin_status和activity_provider_status字段
- ensureColumn("users", "article_admin_status", "VARCHAR(20) DEFAULT 'pending' COMMENT '文章管理员审核状态'");
- ensureColumn("users", "activity_provider_status", "VARCHAR(20) DEFAULT 'pending' COMMENT '活动供应商审核状态'");
- }
- private void runMigration93() {
- // 迁移 93: 创建 dan_report_uploads 表(DAN PDF报告独立化)
- try {
- jdbcTemplate.execute("CREATE TABLE IF NOT EXISTS dan_report_uploads (" +
- "id BIGINT AUTO_INCREMENT PRIMARY KEY, " +
- "family_id BIGINT NOT NULL COMMENT '家庭ID', " +
- "family_member_id BIGINT NOT NULL COMMENT '家庭成员ID', " +
- "uploader_user_id BIGINT NOT NULL COMMENT '上传者用户ID', " +
- "dimension VARCHAR(16) NOT NULL COMMENT '维度: mind/wisdom', " +
- "report_type VARCHAR(16) COMMENT 'A2/B4', " +
- "file_url VARCHAR(500) NOT NULL COMMENT 'PDF文件URL', " +
- "file_id VARCHAR(500) COMMENT '媒资文件ID或相对路径', " +
- "file_name VARCHAR(200) COMMENT '原始文件名', " +
- "file_size BIGINT COMMENT '文件大小(字节)', " +
- "uploaded_at DATETIME DEFAULT CURRENT_TIMESTAMP, " +
- "parsed_items JSON COMMENT '解析的指标项', " +
- "parsed_extra JSON COMMENT '解析的额外信息', " +
- "parsed_summary TEXT COMMENT '解析的总结文本', " +
- "parsed_suggestions TEXT COMMENT '解析的建议文本', " +
- "parser_version VARCHAR(32) COMMENT '解析器版本', " +
- "parse_status VARCHAR(16) DEFAULT 'pending' COMMENT 'pending/success/failed', " +
- "parse_error VARCHAR(500) COMMENT '解析失败原因', " +
- "parsed_at DATETIME COMMENT '解析完成时间', " +
- "draft_status VARCHAR(16) DEFAULT 'pending' COMMENT 'pending/confirmed/discarded', " +
- "confirmed_at DATETIME COMMENT '确认时间', " +
- "confirmed_by BIGINT COMMENT '确认操作人用户ID', " +
- "edited_items JSON COMMENT '编辑后的指标项', " +
- "edited_extra JSON COMMENT '编辑后的额外信息', " +
- "edited_summary TEXT COMMENT '编辑后的总结', " +
- "edited_suggestions TEXT COMMENT '编辑后的建议', " +
- "edit_count INT DEFAULT 0 COMMENT '编辑次数', " +
- "linked_result_id BIGINT COMMENT '关联 dan_assessment_results.id', " +
- "created_at DATETIME DEFAULT CURRENT_TIMESTAMP, " +
- "updated_at DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, " +
- "deleted_at DATETIME COMMENT '软删除时间', " +
- "INDEX idx_family_id (family_id), " +
- "INDEX idx_member_id (family_member_id), " +
- "INDEX idx_draft_status (draft_status), " +
- "INDEX idx_dimension (dimension), " +
- "INDEX idx_parse_status (parse_status), " +
- "INDEX idx_linked_result (linked_result_id)" +
- ") ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='DAN测评PDF报告独立记录'");
- log.info("已创建dan_report_uploads表");
- } catch (Exception e) {
- // 表已存在,忽略
- }
- }
- private void runMigration94() {
- // 迁移 94: dan_assessment_results表添加独立化扩展字段(关联新表)
- ensureColumn("dan_assessment_results", "source_report_id", "BIGINT DEFAULT NULL COMMENT '关联 dan_report_uploads.id'");
- ensureColumn("dan_assessment_results", "analysis_status", "VARCHAR(16) DEFAULT 'pending' COMMENT '分析状态: pending/confirmed'");
- log.info("已添加独立化扩展列到dan_assessment_results表");
- }
- private void runMigration95() {
- // 迁移 95: 历史 dan_assessment_results 数据 ETL 回填到 dan_report_uploads
- // 条件: source='parent_upload' AND source_report_id IS NULL(真正的历史遗留数据)
- // 注意: 新流程的记录通过 confirmUpload 已正确设置 source_report_id,无需处理
- try {
- // Step 1: 插入 dan_report_uploads 记录(来自 dan_assessment_results 的历史数据)
- jdbcTemplate.execute("INSERT IGNORE INTO dan_report_uploads " +
- "(family_id, family_member_id, uploader_user_id, dimension, report_type, " +
- "file_url, file_id, draft_status, confirmed_at, confirmed_by, " +
- "edited_items, edited_summary, edited_suggestions, linked_result_id, " +
- "parsed_status, parse_status, created_at, updated_at) " +
- "SELECT " +
- " fm.family_id, " +
- " r.family_member_id, " +
- " r.teacher_id, " +
- " r.dimension, " +
- " r.source, " +
- " COALESCE(r.report_file_id, ''), " +
- " r.report_file_id, " +
- " 'confirmed', " +
- " r.created_at, " +
- " r.teacher_id, " +
- " r.structured_analysis, " +
- " r.analysis_report, " +
- " r.growth_suggestions, " +
- " r.id, " +
- " 'success', " +
- " 'confirmed', " +
- " r.created_at, " +
- " r.updated_at " +
- "FROM dan_assessment_results r " +
- "LEFT JOIN family_members fm ON fm.id = r.family_member_id " +
- "WHERE r.source = 'parent_upload' " +
- " AND r.source_report_id IS NULL " +
- " AND r.draft_status = 'confirmed' " +
- " AND NOT EXISTS (SELECT 1 FROM dan_report_uploads u WHERE u.linked_result_id = r.id)"
- );
- log.info("迁移95 Step1: 已将历史 dan_assessment_results 回填到 dan_report_uploads");
- } catch (Exception e) {
- log.error("迁移95 Step1 失败: " + e.getMessage());
- }
- // Step 2: 回填 dan_assessment_results.source_report_id(关联新建的 dan_report_uploads 记录)
- try {
- jdbcTemplate.execute("UPDATE dan_assessment_results r " +
- "INNER JOIN dan_report_uploads u ON u.linked_result_id = r.id " +
- "SET r.source_report_id = u.id " +
- "WHERE r.source = 'parent_upload' " +
- " AND r.source_report_id IS NULL " +
- " AND r.draft_status = 'confirmed'"
- );
- log.info("迁移95 Step2: 已回填 dan_assessment_results.source_report_id");
- } catch (Exception e) {
- log.error("迁移95 Step2 失败: " + e.getMessage());
- }
- }
- private void runMigration97() {
- // 迁移93: users表添加roles字段
- ensureColumn("users", "roles", "VARCHAR(100) COMMENT '角色列表'");
- }
- private void runMigration96() {
- // 迁移96: 修复assessment_appointments.status列类型为TINYINT(原库可能为VARCHAR/ENUM导致Data truncated)
- try {
- jdbcTemplate.execute("ALTER TABLE assessment_appointments MODIFY COLUMN status TINYINT DEFAULT 0 COMMENT '0=待确认,1=已确认,2=已完成,3=已取消,4=未到'");
- log.info("已修复assessment_appointments.status列类型为TINYINT");
- } catch (Exception e) {
- log.warn("Migration 96 skipped: {}", e.getMessage());
- }
- }
- private void runMigration98() {
- // 迁移98: family_members表新增辈分字段(TASK-P0-001 辈分重构)
- try {
- jdbcTemplate.execute("ALTER TABLE family_members ADD COLUMN generation INT COMMENT '辈分值(0=家庭创建者,+n=向上,-n=向下)'");
- log.info("已添加generation列到family_members表");
- } catch (Exception e) {
- // 列已存在,忽略错误
- }
- try {
- jdbcTemplate.execute("ALTER TABLE family_members ADD COLUMN is_spouse TINYINT(1) DEFAULT 0 COMMENT '是否配偶(同辈中)'");
- log.info("已添加is_spouse列到family_members表");
- } catch (Exception e) {
- // 列已存在,忽略错误
- }
- // 迁移: products表添加min_quantity列(商品最小购买数限制)
- ensureColumn("products", "min_quantity", "INT DEFAULT 1 COMMENT '最小购买数量限制'");
- // 迁移: 素材库历史数据修复(file_type 回填 + 按分类补标签,幂等)
- backfillAssetLibrary();
- }
- /**
- * 素材库历史数据修复(幂等):
- * <ol>
- * <li>回填 file_record.file_type(历史记录可能为 NULL,导致素材库类型筛选失效)</li>
- * <li>按分类名给记录补自动标签(规则与上传时一致;文件类型不参与标签)</li>
- * </ol>
- */
- private void backfillAssetLibrary() {
- try {
- int n = jdbcTemplate.update(
- "UPDATE file_record SET file_type = CASE " +
- "WHEN content_type LIKE 'image/%' THEN 'image' " +
- "WHEN content_type LIKE 'video/%' THEN 'video' " +
- "WHEN content_type LIKE 'audio/%' THEN 'audio' " +
- "WHEN content_type LIKE 'text/%' THEN 'document' " +
- "WHEN content_type IN ('application/pdf','application/msword'," +
- "'application/vnd.openxmlformats-officedocument.wordprocessingml.document'," +
- "'application/vnd.ms-excel'," +
- "'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet'," +
- "'application/vnd.ms-powerpoint'," +
- "'application/vnd.openxmlformats-officedocument.presentationml.presentation') THEN 'document' " +
- "WHEN SUBSTRING_INDEX(LOWER(original_filename), '.', -1) " +
- "IN ('jpg','jpeg','png','gif','webp','bmp','svg','ico','heic') THEN 'image' " +
- "WHEN SUBSTRING_INDEX(LOWER(original_filename), '.', -1) " +
- "IN ('mp4','webm','ogg','mov','m4v','avi','mkv') THEN 'video' " +
- "WHEN SUBSTRING_INDEX(LOWER(original_filename), '.', -1) " +
- "IN ('mp3','wav','m4a','aac','flac','amr') THEN 'audio' " +
- "WHEN SUBSTRING_INDEX(LOWER(original_filename), '.', -1) " +
- "IN ('pdf','doc','docx','xls','xlsx','ppt','pptx','txt','md','csv','rtf','odt','ods') THEN 'document' " +
- "ELSE 'other' END " +
- "WHERE file_type IS NULL OR file_type = '' " +
- "OR file_type NOT IN ('image','video','audio','document','other')");
- if (n > 0) {
- log.info("素材库回填 file_type: {} 条", n);
- }
- } catch (Exception e) {
- log.warn("素材库回填 file_type 失败: {}", e.getMessage());
- }
- try {
- List<Map<String, Object>> cats = jdbcTemplate.queryForList(
- "SELECT category FROM file_record WHERE category IS NOT NULL AND category <> '' GROUP BY category");
- int tagged = 0;
- for (Map<String, Object> row : cats) {
- String category = String.valueOf(row.get("category"));
- String tagName = com.etotem.cfc.util.AssetCategoryTag.label(category);
- String color = com.etotem.cfc.util.AssetCategoryTag.color(category);
- List<Map<String, Object>> found = jdbcTemplate.queryForList(
- "SELECT id FROM file_tag WHERE name = ? LIMIT 1", tagName);
- Long tagId;
- if (found.isEmpty()) {
- jdbcTemplate.update(
- "INSERT INTO file_tag (name, color, sort_order, created_at) VALUES (?, ?, 0, NOW())",
- tagName, color);
- tagId = jdbcTemplate.queryForObject(
- "SELECT id FROM file_tag WHERE name = ? LIMIT 1", Long.class, tagName);
- } else {
- tagId = ((Number) found.get(0).get("id")).longValue();
- }
- tagged += jdbcTemplate.update(
- "INSERT IGNORE INTO file_tag_rel (file_id, tag_id) " +
- "SELECT id, ? FROM file_record WHERE category = ?",
- tagId, category);
- }
- if (tagged > 0) {
- log.info("素材库按分类补标签: {} 条", tagged);
- }
- } catch (Exception e) {
- log.warn("素材库按分类补标签失败: {}", e.getMessage());
- }
- }
- private void runMigration99() {
- // 迁移99: 旧关系类型数据回填到辈分字段(TASK-P0-001 辈分重构)
- try {
- jdbcTemplate.execute("UPDATE family_members SET generation = 0, is_spouse = 1 WHERE relationship_type = 'spouse'");
- jdbcTemplate.execute("UPDATE family_members SET generation = 1 WHERE relationship_type = 'parent'");
- jdbcTemplate.execute("UPDATE family_members SET generation = -1 WHERE relationship_type = 'child'");
- jdbcTemplate.execute("UPDATE family_members SET generation = 0 WHERE relationship_type = 'sibling'");
- jdbcTemplate.execute("UPDATE family_members SET generation = 0 WHERE relationship_type IS NULL");
- log.info("迁移99: 已回填generation/is_spouse旧数据");
- } catch (Exception e) {
- log.error("迁移99 失败: " + e.getMessage());
- }
- ensureColumn("products", "min_quantity", "INT DEFAULT 1 COMMENT '最小购买数量限制'");
- ensureColumn("growth_records", "family_member_id", "BIGINT COMMENT '家庭成员ID'");
- ensureColumn("growth_records", "family_member_age", "INT COMMENT '家庭成员年龄(月)'");
- ensureColumn("growth_records", "school", "VARCHAR(100) COMMENT '学校名称'");
- ensureColumn("growth_records", "grade", "INT COMMENT '年级(数字)'");
- ensureColumn("growth_records", "academic_performance", "VARCHAR(200) COMMENT '成绩/学业表现'");
- ensureColumn("growth_records", "school_province", "VARCHAR(50) COMMENT '学校所在省'");
- ensureColumn("growth_records", "school_city", "VARCHAR(50) COMMENT '学校所在市'");
- ensureColumn("growth_records", "school_district", "VARCHAR(50) COMMENT '学校所在区'");
- ensureColumn("growth_records", "school_street", "VARCHAR(100) COMMENT '学校所在街道'");
- }
- private void runMigration100() {
- // 迁移100: 删除废弃列和关系类型配置表(TASK-P0-001 辈分重构)
- try {
- jdbcTemplate.execute("ALTER TABLE family_members DROP COLUMN relationship_type");
- log.info("已删除family_members.relationship_type列");
- } catch (Exception e) {
- // 列已不存在,忽略
- }
- try {
- jdbcTemplate.execute("ALTER TABLE family_members DROP COLUMN role_override");
- log.info("已删除family_members.role_override列");
- } catch (Exception e) {
- // 列已不存在,忽略
- }
- }
- // 迁移 101: product_skus 表添加 min_quantity 列(商品规格最小购买数限制)
- private void runMigration101() {
- ensureColumn("product_skus", "min_quantity", "INT DEFAULT 1 COMMENT '最小购买数量限制'");
- }
-
- // 迁移 102: health_gut_flora 表扩大 population_level 和 detection_rate 字段长度
- private void runMigration102() {
- // 将 VARCHAR(50) 扩大到 TEXT(处理 AI 解析出的长文本)
- ensureColumn("health_gut_flora", "population_level",
- "TEXT DEFAULT NULL COMMENT '处于人群%水平'");
- ensureColumn("health_gut_flora", "detection_rate",
- "TEXT DEFAULT NULL COMMENT '%正常人有检出'");
- }
- private void migration102() {
- // 迁移 102: health_gut_flora 表扩大 population_level 和 detection_rate 字段长度(TEXT)
- ensureColumn("health_gut_flora", "population_level",
- "TEXT DEFAULT NULL COMMENT '处于人群%水平'");
- ensureColumn("health_gut_flora", "detection_rate",
- "TEXT DEFAULT NULL COMMENT '%正常人有检出'");
-
- // 迁移 102: 创建 health_dimension_scores 表(7 维健康模型)
- try {
- jdbcTemplate.execute("CREATE TABLE IF NOT EXISTS health_dimension_scores (" +
- "id BIGINT AUTO_INCREMENT PRIMARY KEY, " +
- "member_id BIGINT NOT NULL COMMENT '家庭成员ID', " +
- "dimension VARCHAR(20) NOT NULL COMMENT '维度编码', " +
- "score INT NOT NULL COMMENT '评分(0-100)', " +
- "percentile INT COMMENT '百分位', " +
- "data_source VARCHAR(20) COMMENT '数据来源', " +
- "tier INT DEFAULT 3 COMMENT '数据可信度层级', " +
- "raw_data TEXT COMMENT '原始数据JSON', " +
- "assess_date DATE COMMENT '评估日期', " +
- "expire_date DATE COMMENT '过期日期', " +
- "created_at DATETIME DEFAULT CURRENT_TIMESTAMP, " +
- "updated_at DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, " +
- "INDEX idx_member_dim (member_id, dimension), " +
- "INDEX idx_assess_date (assess_date)" +
- ") ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='健康维度评分'");
- log.info("已创建health_dimension_scores表");
- } catch (Exception e) {
- log.warn("创建health_dimension_scores表可能已存在: {}", e.getMessage());
- }
- // 迁移102b: 创建 health_data_sources 表(多媒体数据源记录)
- try {
- jdbcTemplate.execute("CREATE TABLE IF NOT EXISTS health_data_sources (" +
- "id BIGINT AUTO_INCREMENT PRIMARY KEY, " +
- "member_id BIGINT NOT NULL COMMENT '家庭成员ID', " +
- "source_type VARCHAR(20) NOT NULL COMMENT '数据源类型: VOICE/PHOTO/REPORT/MANUAL', " +
- "dimension_codes VARCHAR(100) COMMENT '关联维度编码(逗号分隔)', " +
- "parsed_result TEXT COMMENT '解析结果JSON', " +
- "file_url VARCHAR(500) COMMENT '原始文件URL', " +
- "confidence DECIMAL(5,4) COMMENT 'AI解析置信度(0-1)', " +
- "created_at DATETIME DEFAULT CURRENT_TIMESTAMP, " +
- "INDEX idx_member_source (member_id, source_type), " +
- "INDEX idx_member_id (member_id)" +
- ") ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='健康数据源记录'");
- log.info("已创建health_data_sources表");
- } catch (Exception e) {
- log.warn("创建health_data_sources表可能已存在: {}", e.getMessage());
- }
- // 迁移103: health_indicators表添加collection_date列(指标采集日期)
- ensureColumn("health_indicators", "collection_date", "DATETIME COMMENT '指标采集日期(报告检测日期)'");
- // 迁移104: promotion_tier表确保team_size_1st/2nd/3rd列存在(MyBatis-Plus camelCase映射修正)
- ensureColumn("promotion_tier", "team_size_1st", "INT DEFAULT 0 COMMENT '一层团队人数'");
- ensureColumn("promotion_tier", "team_size_2nd", "INT DEFAULT 0 COMMENT '二层团队人数'");
- ensureColumn("promotion_tier", "team_size_3rd", "INT DEFAULT 0 COMMENT '三层团队人数'");
- // 迁移105: article_reading_records表添加article_id和updated_at列
- try {
- jdbcTemplate.execute("ALTER TABLE article_reading_records ADD COLUMN article_id BIGINT DEFAULT NULL COMMENT '文章ID' AFTER id");
- log.info("已添加article_id列到article_reading_records表");
- } catch (Exception e) {
- // 列已存在,忽略
- }
- try {
- jdbcTemplate.execute("ALTER TABLE article_reading_records ADD COLUMN updated_at DATETIME DEFAULT NULL COMMENT '更新时间' AFTER read_at");
- log.info("已添加updated_at列到article_reading_records表");
- } catch (Exception e) {
- // 列已存在,忽略
- }
- try {
- jdbcTemplate.execute("ALTER TABLE article_reading_records ADD INDEX idx_article_child_date (article_id, child_id, read_at)");
- log.info("已添加idx_article_child_date索引");
- } catch (Exception e) {
- // 索引已存在,忽略
- }
- // 迁移106: 创建 report_summary 统一报告汇总表
- try {
- jdbcTemplate.execute("CREATE TABLE IF NOT EXISTS report_summary (" +
- "id BIGINT AUTO_INCREMENT PRIMARY KEY COMMENT '主键ID', " +
- "report_type VARCHAR(32) NOT NULL COMMENT '报告类型', " +
- "source_table VARCHAR(32) NOT NULL COMMENT '来源表', " +
- "source_id BIGINT NOT NULL COMMENT '来源表记录ID', " +
- "user_id BIGINT NOT NULL COMMENT '创建者用户ID', " +
- "family_id BIGINT COMMENT '家庭ID', " +
- "subject_id BIGINT COMMENT '报告主体ID', " +
- "subject_name VARCHAR(50) COMMENT '报告主体名称', " +
- "title VARCHAR(200) COMMENT '报告标题', " +
- "summary TEXT COMMENT '摘要', " +
- "file_url VARCHAR(500) COMMENT '文件URL', " +
- "file_size BIGINT COMMENT '文件大小', " +
- "file_name VARCHAR(200) COMMENT '文件名', " +
- "has_file TINYINT DEFAULT 0 COMMENT '是否有文件', " +
- "overall_score INT COMMENT '综合评分', " +
- "report_date DATE COMMENT '报告日期', " +
- "status VARCHAR(20) COMMENT '状态', " +
- "detail_route VARCHAR(100) COMMENT '详情页路由', " +
- "created_at DATETIME DEFAULT CURRENT_TIMESTAMP, " +
- "updated_at DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, " +
- "INDEX idx_user_id (user_id), " +
- "INDEX idx_family_id (family_id), " +
- "INDEX idx_subject_id (subject_id), " +
- "INDEX idx_report_type (report_type), " +
- "INDEX idx_report_date (report_date), " +
- "INDEX idx_status (status), " +
- "UNIQUE KEY uk_source (source_table, source_id)" +
- ") ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='统一报告汇总表'");
- log.info("已创建 report_summary 表");
- } catch (Exception e) {
- // 表已存在,忽略
- }
- // 迁移107: tasks表添加dimension_code和dimension_weights列(五维维度支持)
- try {
- ensureColumn("tasks", "dimension_code", "VARCHAR(20) COMMENT '五维维度: body/mind/wisdom/action/wealth'");
- ensureColumn("tasks", "dimension_weights", "JSON COMMENT '五维权重: {\"body\":0,\"mind\":0,\"wisdom\":0,\"action\":0,\"wealth\":0}'");
- log.info("已完成tasks表维度字段添加");
- } catch (Exception e) {
- log.warn("添加tasks表维度字段失败: {}", e.getMessage());
- }
- // 迁移108: 创建 energy_behavior_config 表(能量行为配置)
- try {
- jdbcTemplate.execute("CREATE TABLE IF NOT EXISTS energy_behavior_config (" +
- "id BIGINT AUTO_INCREMENT PRIMARY KEY, " +
- "behavior_code VARCHAR(32) NOT NULL COMMENT '行为编码', " +
- "behavior_name VARCHAR(50) NOT NULL COMMENT '行为名称', " +
- "description VARCHAR(200) COMMENT '行为描述', " +
- "default_amount INT DEFAULT 5 COMMENT '默认能量值', " +
- "amount_source VARCHAR(32) DEFAULT 'fixed' COMMENT '能量值来源', " +
- "dimension_assign JSON COMMENT '维度分配JSON', " +
- "daily_limit INT DEFAULT 0 COMMENT '每日上限', " +
- "cooldown_seconds INT DEFAULT 0 COMMENT '冷却时间(秒)', " +
- "expire_days INT DEFAULT 0 COMMENT '过期天数', " +
- "enabled TINYINT DEFAULT 1 COMMENT '是否启用', " +
- "created_at DATETIME DEFAULT CURRENT_TIMESTAMP, " +
- "updated_at DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, " +
- "UNIQUE KEY uk_behavior_code (behavior_code)" +
- ") ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='能量行为配置表'");
- log.info("已创建 energy_behavior_config 表");
- // 插入预置数据(仅当表为空时插入)
- Integer count = jdbcTemplate.queryForObject("SELECT COUNT(*) FROM energy_behavior_config", Integer.class);
- if (count == null || count == 0) {
- String[][] seedData = {
- {"task_complete", "完成任务", "完成任务获得的能量", "5", "points", "[{\"dim\":\"action\",\"ratio\":100}]", "200", "0", "0"},
- {"game_finish", "完成游戏", "完成小游戏获得的能量", "5", "points", "[{\"dim\":\"mind\",\"ratio\":30},{\"dim\":\"wisdom\",\"ratio\":50},{\"dim\":\"action\",\"ratio\":20}]", "200", "0", "0"},
- {"activity_checkin", "活动签到", "活动签到获得的能量", "5", "fixed", "[{\"dim\":\"body\",\"ratio\":30},{\"dim\":\"action\",\"ratio\":70}]", "100", "0", "0"},
- {"article_read", "阅读文章", "阅读文章获得的能量", "5", "points", "[{\"dim\":\"mind\",\"ratio\":30},{\"dim\":\"wisdom\",\"ratio\":50},{\"dim\":\"action\",\"ratio\":20}]", "100", "0", "0"},
- {"product_purchase", "商品购买", "购买商品获得的能量", "10", "order_amount", "[{\"dim\":\"wealth\",\"ratio\":100}]", "500", "0", "30"},
- {"health_checkin", "健康打卡", "健康打卡获得的能量", "5", "fixed", "[{\"dim\":\"body\",\"ratio\":100}]", "50", "0", "30"},
- {"finance_checkin", "理财打卡", "理财打卡获得的能量", "5", "fixed", "[{\"dim\":\"wealth\",\"ratio\":100}]", "50", "0", "0"},
- {"emotion_checkin", "情绪打卡", "情绪打卡获得的能量", "5", "fixed", "[{\"dim\":\"mind\",\"ratio\":100}]", "40", "0", "0"},
- {"appointment_submit", "提交测评预约", "提交测评预约获得的能量", "10", "fixed", "[{\"dim\":\"action\",\"ratio\":100}]", "50", "0", "0"},
- {"appointment_complete", "完成测评", "完成测评获得的能量", "30", "fixed", "[{\"dim\":\"mind\",\"ratio\":50},{\"dim\":\"wisdom\",\"ratio\":50}]", "100", "0", "0"},
- {"streak_daily", "连续打卡每日", "每日连续打卡获得的能量", "5", "fixed", "[{\"dim\":\"body\",\"ratio\":30},{\"dim\":\"mind\",\"ratio\":20},{\"dim\":\"action\",\"ratio\":50}]", "50", "86400", "0"},
- {"streak_milestone", "打卡里程碑", "达到打卡里程碑获得的能量", "20", "config", "[{\"dim\":\"body\",\"ratio\":30},{\"dim\":\"mind\",\"ratio\":20},{\"dim\":\"action\",\"ratio\":50}]", "100", "0", "0"},
- {"growth_task", "成长任务", "完成每日成长任务获得的能量", "10", "config", "[{\"dim\":\"body\",\"ratio\":20},{\"dim\":\"mind\",\"ratio\":20},{\"dim\":\"wisdom\",\"ratio\":20},{\"dim\":\"action\",\"ratio\":20},{\"dim\":\"wealth\",\"ratio\":20}]", "100", "0", "1"},
- {"micro_action", "微行动", "完成微行动获得的能量", "1", "fixed", "[{\"dim\":\"action\",\"ratio\":100}]", "20", "60", "30"},
- {"onboarding", "新手引导", "完成新手引导任务获得的能量", "10", "config", "[{\"dim\":\"body\",\"ratio\":20},{\"dim\":\"mind\",\"ratio\":20},{\"dim\":\"wisdom\",\"ratio\":20},{\"dim\":\"action\",\"ratio\":20},{\"dim\":\"wealth\",\"ratio\":20}]", "50", "0", "30"},
- {"invite_milestone", "邀请里程碑", "达到邀请人数里程碑获得的能量", "30", "config", "[{\"dim\":\"action\",\"ratio\":40},{\"dim\":\"wealth\",\"ratio\":60}]", "200", "0", "30"},
- {"dimension_sync", "维度同步", "健康维度数据上传同步获得的能量", "10", "config", "[{\"dim\":\"body\",\"ratio\":100}]", "100", "0", "30"},
- {"invite_friend", "邀请好友加入", "邀请好友注册加入获得的能量", "20", "fixed", "[{\"dim\":\"action\",\"ratio\":50},{\"dim\":\"wealth\",\"ratio\":50}]", "200", "0", "0"},
- {"invite_family", "邀请家庭成员", "邀请家庭成员加入家庭获得的能量", "10", "fixed", "[{\"dim\":\"mind\",\"ratio\":30},{\"dim\":\"action\",\"ratio\":70}]", "100", "0", "0"},
- {"report_upload", "上传体检报告", "上传体检报告获得的能量", "30", "fixed", "[{\"dim\":\"body\",\"ratio\":100}]", "100", "0", "30"},
- {"plan_complete", "完成成长计划", "完成成长计划复盘获得的能量", "20", "fixed", "[{\"dim\":\"body\",\"ratio\":20},{\"dim\":\"mind\",\"ratio\":20},{\"dim\":\"wisdom\",\"ratio\":20},{\"dim\":\"action\",\"ratio\":20},{\"dim\":\"wealth\",\"ratio\":20}]", "100", "0", "0"},
- {"wisdom_report", "上传智测评报告", "上传智慧测评报告获得的能量", "30", "fixed", "[{\"dim\":\"wisdom\",\"ratio\":100}]", "100", "0", "30"},
- };
- String sql = "INSERT INTO energy_behavior_config (behavior_code, behavior_name, description, default_amount, amount_source, dimension_assign, daily_limit, cooldown_seconds, expire_days, enabled) VALUES (?,?,?,?,?,?,?,?,?,1)";
- for (String[] row : seedData) {
- try {
- jdbcTemplate.update(sql, row[0], row[1], row[2], Integer.parseInt(row[3]), row[4], row[5], Integer.parseInt(row[6]), Integer.parseInt(row[7]), Integer.parseInt(row[8]));
- } catch (Exception e) {
- log.warn("插入能量行为配置失败: behavior_code={}, error={}", row[0], e.getMessage());
- }
- }
- log.info("已插入{}条能量行为配置", seedData.length);
- }
- } catch (Exception e) {
- log.warn("创建 energy_behavior_config 表失败: {}", e.getMessage());
- }
- // 迁移109: finance_checkins表添加type列(记账类型: income/expense)
- try {
- ensureColumn("finance_checkins", "type", "VARCHAR(10) DEFAULT 'expense' COMMENT '类型: income(收入)/expense(支出)'");
- log.info("已完成finance_checkins.type字段添加");
- } catch (Exception e) {
- log.warn("添加finance_checkins.type字段失败: {}", e.getMessage());
- }
- // 迁移110: 创建 article_comments 表(文章评论)
- try {
- jdbcTemplate.execute("CREATE TABLE IF NOT EXISTS article_comments (" +
- "id BIGINT AUTO_INCREMENT PRIMARY KEY, " +
- "article_id BIGINT NOT NULL COMMENT '文章ID', " +
- "parent_id BIGINT COMMENT '父评论ID', " +
- "reply_to_id BIGINT COMMENT '引用的评论ID', " +
- "user_id BIGINT NOT NULL COMMENT '评论用户ID', " +
- "content TEXT NOT NULL COMMENT '评论内容', " +
- "status VARCHAR(20) DEFAULT 'pending' COMMENT 'pending/approved/rejected', " +
- "audit_by BIGINT COMMENT '审核人ID', " +
- "audit_at DATETIME COMMENT '审核时间', " +
- "earned_energy INT DEFAULT 0 COMMENT '获得的能量', " +
- "created_at DATETIME DEFAULT CURRENT_TIMESTAMP, " +
- "INDEX idx_article (article_id), " +
- "INDEX idx_article_status (article_id, status), " +
- "INDEX idx_parent (parent_id)" +
- ") ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='文章评论'");
- log.info("已创建 article_comments 表");
- } catch (Exception e) {
- log.warn("创建 article_comments 表失败: {}", e.getMessage());
- }
- // 迁移111: 创建 service_role_applications 表(服务角色统一申请)
- try {
- jdbcTemplate.execute("CREATE TABLE IF NOT EXISTS service_role_applications (" +
- "id int(11) NOT NULL AUTO_INCREMENT, " +
- "user_id int(11) NOT NULL COMMENT '申请人ID', " +
- "roles varchar(255) NOT NULL COMMENT '申请角色,JSON数组', " +
- "id_card varchar(18) DEFAULT '' COMMENT '身份证号', " +
- "id_card_images text COMMENT '身份证照片,JSON数组', " +
- "certificate_images text COMMENT '资质证书照片,JSON数组', " +
- "province varchar(50) DEFAULT '' COMMENT '省', " +
- "city varchar(50) DEFAULT '' COMMENT '市', " +
- "district varchar(50) DEFAULT '' COMMENT '区', " +
- "street varchar(50) DEFAULT '' COMMENT '街道', " +
- "address_detail varchar(200) DEFAULT '' COMMENT '详细地址', " +
- "status tinyint(4) NOT NULL DEFAULT 0 COMMENT '0-pending 1-approved 2-rejected', " +
- "reject_reason varchar(500) DEFAULT '' COMMENT '驳回原因', " +
- "reviewed_by int(11) DEFAULT NULL COMMENT '审核人ID', " +
- "reviewed_at datetime DEFAULT NULL COMMENT '审核时间', " +
- "created_at datetime NOT NULL DEFAULT CURRENT_TIMESTAMP, " +
- "updated_at datetime NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, " +
- "PRIMARY KEY (id), " +
- "KEY idx_user_id (user_id), " +
- "KEY idx_status (status)" +
- ") ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='服务角色申请表'");
- log.info("已创建service_role_applications表");
- } catch (Exception e) {
- // 表已存在,忽略错误
- }
- // 迁移112: users表添加article_admin_status字段(文章管理员状态)
- log.info("迁移112: 开始添加article_admin_status列到users表");
- try {
- jdbcTemplate.execute("ALTER TABLE users ADD COLUMN article_admin_status VARCHAR(20) DEFAULT NULL COMMENT '文章管理员状态: pending/approved/rejected'");
- log.info("迁移112: 已添加article_admin_status列到users表");
- } catch (Exception e) {
- // 列已存在,幂等忽略(用warn而非error避免日志噪声)
- log.warn("迁移112: 添加article_admin_status列失败(可能已存在): {}", e.getMessage());
- }
- // 迁移113: users表添加activity_provider_status字段(活动提供商状态)
- try {
- jdbcTemplate.execute("ALTER TABLE users ADD COLUMN activity_provider_status VARCHAR(20) DEFAULT NULL COMMENT '活动提供商状态: pending/approved/rejected'");
- log.info("已添加activity_provider_status列到users表");
- } catch (Exception e) {
- // 列已存在,忽略错误
- }
- // 迁移114: 创建 periodic_services 表(周期性服务续费配置)
- try {
- jdbcTemplate.execute("CREATE TABLE IF NOT EXISTS periodic_services (" +
- "id BIGINT AUTO_INCREMENT PRIMARY KEY, " +
- "product_id BIGINT NOT NULL COMMENT '关联商品ID', " +
- "service_name VARCHAR(100) COMMENT '服务名称', " +
- "cycle_days INT NOT NULL COMMENT '周期天数(如365=年度服务)', " +
- "remind_before_days INT DEFAULT 30 COMMENT '提前提醒天数', " +
- "auto_renew TINYINT DEFAULT 0 COMMENT '是否支持自动续费: 0=否 1=是', " +
- "enabled TINYINT DEFAULT 1 COMMENT '是否启用: 0=禁用 1=启用', " +
- "created_at DATETIME DEFAULT CURRENT_TIMESTAMP, " +
- "updated_at DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, " +
- "INDEX idx_product (product_id)" +
- ") ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='周期性服务续费配置'");
- log.info("已创建 periodic_services 表");
- } catch (Exception e) {
- log.warn("创建 periodic_services 表失败: {}", e.getMessage());
- }
- // 迁移115: 创建 recommendation_logs 表(推荐行为记录)
- try {
- jdbcTemplate.execute("CREATE TABLE IF NOT EXISTS recommendation_logs (" +
- "id BIGINT AUTO_INCREMENT PRIMARY KEY, " +
- "user_id BIGINT COMMENT '用户ID', " +
- "family_id BIGINT COMMENT '家庭ID', " +
- "child_id BIGINT COMMENT '孩子ID', " +
- "content_type VARCHAR(20) NOT NULL COMMENT '内容类型: article/task/product/activity', " +
- "content_id BIGINT NOT NULL COMMENT '内容ID', " +
- "recommend_score INT COMMENT '推荐分值', " +
- "action VARCHAR(20) COMMENT '行为: show/click/consume', " +
- "created_at DATETIME DEFAULT CURRENT_TIMESTAMP, " +
- "INDEX idx_user_content (user_id, content_type, content_id)" +
- ") ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='推荐行为记录'");
- log.info("已创建 recommendation_logs 表");
- } catch (Exception e) {
- log.warn("创建 recommendation_logs 表失败: {}", e.getMessage());
- }
- // 迁移116: products表添加family_scope列(推荐系统-家庭消费去重)
- try {
- ensureColumn("products", "family_scope", "TINYINT DEFAULT 1 COMMENT '是否面向家庭: 1=家庭成员消费过即排除, 0=仅按个人消费排除'");
- log.info("已完成products.family_scope字段添加");
- } catch (Exception e) {
- log.warn("添加products.family_scope字段失败: {}", e.getMessage());
- }
- // 迁移117: activities表添加family_scope列(推荐系统-家庭参与去重)
- try {
- ensureColumn("activities", "family_scope", "TINYINT DEFAULT 1 COMMENT '是否面向家庭: 1=家庭成员参与过即排除, 0=仅按个人参与排除'");
- log.info("已完成activities.family_scope字段添加");
- } catch (Exception e) {
- log.warn("添加activities.family_scope字段失败: {}", e.getMessage());
- }
- // 迁移118: activity_registrations表child_id改为可空 + 添加registrant_name/phone(报名表单-多人报名)
- try {
- ensureColumn("activity_registrations", "registrant_name", "VARCHAR(100) COMMENT '报名人姓名'");
- log.info("已完成activity_registrations.registrant_name字段添加");
- } catch (Exception e) {
- log.warn("添加activity_registrations.registrant_name字段失败: {}", e.getMessage());
- }
- try {
- ensureColumn("activity_registrations", "registrant_phone", "VARCHAR(20) COMMENT '报名人联系方式'");
- log.info("已完成activity_registrations.registrant_phone字段添加");
- } catch (Exception e) {
- log.warn("添加activity_registrations.registrant_phone字段失败: {}", e.getMessage());
- }
- try {
- jdbcTemplate.execute("ALTER TABLE activity_registrations MODIFY COLUMN child_id BIGINT COMMENT '孩子ID(手动输入报名人为空)'");
- log.info("已完成activity_registrations.child_id改为可空");
- } catch (Exception e) {
- log.warn("修改activity_registrations.child_id失败: {}", e.getMessage());
- }
- // 迁移119: package_orders表添加联系人字段(产品页下单)
- try {
- ensureColumn("package_orders", "contact_name", "VARCHAR(50) COMMENT '联系人姓名'");
- ensureColumn("package_orders", "contact_phone", "VARCHAR(20) COMMENT '联系人手机号'");
- ensureColumn("package_orders", "contact_address", "VARCHAR(500) COMMENT '收件地址'");
- log.info("已完成package_orders表联系人字段添加");
- } catch (Exception e) {
- log.warn("添加package_orders联系人字段失败: {}", e.getMessage());
- }
- // 迁移120: activities表添加registration_deadline列(活动中心-报名截止时间展示)
- try {
- ensureColumn("activities", "registration_deadline", "DATETIME COMMENT '报名截止时间'");
- log.info("已完成activities.registration_deadline字段添加");
- } catch (Exception e) {
- log.warn("添加activities.registration_deadline字段失败: {}", e.getMessage());
- }
- // 迁移143: 创建小程序虚拟支付道具映射表
- try {
- jdbcTemplate.execute("CREATE TABLE IF NOT EXISTS virtual_goods_config (" +
- "id BIGINT AUTO_INCREMENT PRIMARY KEY, " +
- "goods_type VARCHAR(32) NOT NULL COMMENT 'ASSESSMENT_PACKAGE / MEMBERSHIP / SUBSCRIPTION', " +
- "biz_key VARCHAR(64) NOT NULL COMMENT '业务SKU标识: 测评套餐ID / 会员等级code / 订阅档位code', " +
- "product_id VARCHAR(64) NOT NULL COMMENT '微信商户后台道具ID', " +
- "goods_name VARCHAR(128) NOT NULL COMMENT '道具名称', " +
- "status TINYINT NOT NULL DEFAULT 1 COMMENT '1=启用 0=停用', " +
- "create_time DATETIME DEFAULT CURRENT_TIMESTAMP, " +
- "update_time DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, " +
- "UNIQUE KEY uk_goods_type_biz (goods_type, biz_key)" +
- ") ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='小程序虚拟支付道具映射'");
- log.info("已创建virtual_goods_config表");
- } catch (Exception e) {
- log.warn("创建virtual_goods_config表失败: {}", e.getMessage());
- }
- // 迁移144: users表添加session_key列(小程序虚拟支付signature签名会话密钥)
- try {
- ensureColumn("users", "session_key", "VARCHAR(64) COMMENT '微信登录会话密钥(虚拟支付签名用)'");
- log.info("已完成users.session_key字段添加");
- } catch (Exception e) {
- log.warn("添加users.session_key字段失败: {}", e.getMessage());
- }
- // 迁移145: 创建家庭天盘周报导出记录表(ReportSummaryService 全量同步依赖,此前从未建表)
- try {
- jdbcTemplate.execute("CREATE TABLE IF NOT EXISTS family_fortune_report (" +
- "id BIGINT AUTO_INCREMENT PRIMARY KEY, " +
- "family_id INT NOT NULL COMMENT '家庭ID', " +
- "element VARCHAR(32) COMMENT '主导五行元素', " +
- "lucky_direction VARCHAR(64) COMMENT '本周吉位', " +
- "pdf_path VARCHAR(255) COMMENT 'PDF周报文件路径', " +
- "file_size BIGINT COMMENT '文件大小(字节)', " +
- "created_at DATETIME DEFAULT CURRENT_TIMESTAMP, " +
- "INDEX idx_family_id (family_id)" +
- ") ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='家庭天盘周报导出记录'");
- log.info("已创建family_fortune_report表");
- } catch (Exception e) {
- log.warn("创建family_fortune_report表失败: {}", e.getMessage());
- }
- // 迁移146: 创建五维家庭自检记录表(P0-1 五维自检问卷,书稿终章15题)
- try {
- jdbcTemplate.execute("CREATE TABLE IF NOT EXISTS five_dimension_self_checks (" +
- "id BIGINT AUTO_INCREMENT PRIMARY KEY, " +
- "user_id BIGINT NOT NULL COMMENT '答卷人用户ID', " +
- "family_member_id BIGINT COMMENT '关联家庭成员ID(可选)', " +
- "answers_json TEXT COMMENT '用户答案JSON', " +
- "scores_json VARCHAR(512) COMMENT '各维度得分JSON', " +
- "total_score INT COMMENT '五维总分(满分45)', " +
- "advice_json TEXT COMMENT '五行相生寻源建议JSON', " +
- "created_at DATETIME DEFAULT CURRENT_TIMESTAMP, " +
- "INDEX idx_user_id (user_id)" +
- ") ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='五维家庭自检记录'");
- log.info("已创建five_dimension_self_checks表");
- } catch (Exception e) {
- log.warn("创建five_dimension_self_checks表失败: {}", e.getMessage());
- }
- // 迁移147: virtual_goods_config 插入会员年卡道具映射(虚拟支付上线准备)
- try {
- jdbcTemplate.execute("INSERT IGNORE INTO virtual_goods_config (goods_type, biz_key, product_id, goods_name, status) " +
- "VALUES ('MEMBERSHIP', 'member1314', 'TBD', '会员年卡', 1)");
- log.info("已插入会员年卡道具映射: member1314");
- } catch (Exception e) {
- log.warn("插入会员年卡道具映射失败: {}", e.getMessage());
- }
- // 迁移148: 修复 membership_levels 表重复行 + 添加 UNIQUE KEY uk_level_code
- // 原因:早期建表缺少 UNIQUE KEY uk_level_code,加上 INSERT IGNORE 在无唯一键时反复插入,
- // 导致生产环境 FREE/FAMILY/PROVIDER 三个等级各 69 行重复,
- // MembershipService.createOrder() 调用 selectOne(level_code) 触发 TooManyResultsException。
- try {
- // 1) 删除重复行:保留每个 level_code 最早插入的(id 最小),其余删除
- int deleted = jdbcTemplate.update(
- "DELETE m1 FROM membership_levels m1 " +
- "INNER JOIN membership_levels m2 " +
- "ON m1.level_code = m2.level_code AND m1.id > m2.id");
- log.info("已清理 membership_levels 重复行: 删除{}条", deleted);
- } catch (Exception e) {
- log.warn("清理 membership_levels 重复行失败: {}", e.getMessage());
- }
- try {
- // 2) 添加 UNIQUE KEY(幂等:先查再 ADD)
- Integer idxCount = jdbcTemplate.queryForObject(
- "SELECT COUNT(*) FROM information_schema.statistics " +
- "WHERE table_schema = DATABASE() AND table_name = 'membership_levels' AND index_name = 'uk_level_code'",
- Integer.class);
- if (idxCount == null || idxCount == 0) {
- jdbcTemplate.execute("ALTER TABLE membership_levels ADD UNIQUE KEY uk_level_code (level_code)");
- log.info("已添加 uk_level_code 唯一索引到 membership_levels 表");
- } else {
- log.info("uk_level_code 唯一索引已存在,跳过添加");
- }
- } catch (Exception e) {
- log.warn("添加 uk_level_code 唯一索引失败: {}", e.getMessage());
- }
- // 迁移149: payment_orders表添加user_coupon_id列(VirtualPayTimeoutTask selectList 触发 Unknown column)
- // 原因:旧库建表时缺少此列,entity PaymentOrder 已声明 userCouponId,导致 MyBatis-Plus 全列查询失败。
- // 本地与远端新库已有,本迁移对它们幂等。
- try {
- ensureColumn("payment_orders", "user_coupon_id", "BIGINT COMMENT '使用的用户优惠券ID'");
- log.info("已确保payment_orders.user_coupon_id列存在");
- } catch (Exception e) {
- log.warn("添加payment_orders.user_coupon_id列失败: {}", e.getMessage());
- }
- // 迁移150: activities表添加member_price列(会员价支持)
- try {
- ensureColumn("activities", "member_price", "INT COMMENT '会员价(分)' AFTER price");
- log.info("已确保activities.member_price列存在");
- } catch (Exception e) {
- log.warn("添加activities.member_price列失败: {}", e.getMessage());
- }
- // 迁移151: growth_records表补齐学校/年级字段(GrowthRecord 实体全列查询 Unknown column 'school')
- // 原因:实体已声明 school/grade/academicPerformance/schoolProvince/schoolCity/schoolDistrict/schoolStreet,
- // 且 @TableField 为 camelCase 列名,但旧库建表时缺失这些列;
- // runMigration99() 为死代码从未执行,且其 snake_case 列名与实体 camelCase 不匹配。
- ensureColumn("growth_records", "school", "VARCHAR(100) COMMENT '学校名称'");
- ensureColumn("growth_records", "grade", "INT COMMENT '年级(数字)'");
- ensureColumn("growth_records", "academicPerformance", "VARCHAR(200) COMMENT '成绩/学业表现'");
- ensureColumn("growth_records", "schoolProvince", "VARCHAR(50) COMMENT '学校所在省'");
- ensureColumn("growth_records", "schoolCity", "VARCHAR(50) COMMENT '学校所在市'");
- ensureColumn("growth_records", "schoolDistrict", "VARCHAR(50) COMMENT '学校所在区'");
- ensureColumn("growth_records", "schoolStreet", "VARCHAR(100) COMMENT '学校所在街道'");
- log.info("已确保growth_records学校/年级相关列存在");
- // 迁移152: 创建 product_gift_rules 表(商品赠送会员规则)
- try {
- jdbcTemplate.execute("CREATE TABLE IF NOT EXISTS product_gift_rules (" +
- "id BIGINT AUTO_INCREMENT PRIMARY KEY, " +
- "product_id BIGINT NOT NULL COMMENT '关联products表', " +
- "level_code VARCHAR(50) NOT NULL COMMENT '赠送会员等级编码(FAMILY/PREMIUM等)', " +
- "status TINYINT(1) DEFAULT 1 COMMENT '1=启用 0=停用', " +
- "created_at DATETIME DEFAULT CURRENT_TIMESTAMP, " +
- "updated_at DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, " +
- "UNIQUE KEY uk_product_level (product_id, level_code), " +
- "INDEX idx_product_id (product_id)" +
- ") ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='商品赠送会员规则'");
- log.info("创建product_gift_rules表成功");
- } catch (Exception e) {
- log.warn("创建product_gift_rules表失败: {}", e.getMessage());
- }
- // 迁移153: 创建 growth_archive_products 表(成长档案关联商品配置)
- try {
- jdbcTemplate.execute("CREATE TABLE IF NOT EXISTS growth_archive_products (" +
- "id BIGINT AUTO_INCREMENT PRIMARY KEY, " +
- "product_id BIGINT NOT NULL COMMENT '关联products表', " +
- "status TINYINT(1) DEFAULT 1 COMMENT '1=启用 0=停用', " +
- "sort_order INT DEFAULT 0 COMMENT '展示排序(升序)', " +
- "created_at DATETIME DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间', " +
- "updated_at DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间', " +
- "UNIQUE KEY uk_product_id (product_id), " +
- "KEY idx_status (status)" +
- ") ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='成长档案关联商品配置'");
- log.info("已创建growth_archive_products表");
- } catch (Exception e) {
- log.warn("创建growth_archive_products表失败: {}", e.getMessage());
- }
- // 迁移154: membership_levels 表插入 PREMIUM 等级种子数据(前端升级页可选"高级家庭会员",createOrder 查表为 null 抛"会员等级不存在")
- try {
- jdbcTemplate.execute("INSERT IGNORE INTO membership_levels (level_code, level_name, level_desc, price_monthly, price_quarterly, price_yearly, max_children, max_tasks_per_day, ai_review_enabled, priority_support, teacher_consultation) VALUES " +
- "('PREMIUM', '高级家庭会员', '高级家庭会员专属功能', 131400, NULL, 1316800, 10, 50, 1, 1, 1)");
- log.info("已插入PREMIUM等级种子数据");
- } catch (Exception e) {
- log.warn("插入PREMIUM等级种子数据失败: {}", e.getMessage());
- }
- // 迁移155: 创建 interaction_logs 表(关系互动记录)
- try {
- jdbcTemplate.execute("CREATE TABLE IF NOT EXISTS interaction_logs (" +
- "id BIGINT AUTO_INCREMENT PRIMARY KEY, " +
- "family_id BIGINT NOT NULL COMMENT '所属家庭ID', " +
- "from_member_id BIGINT NOT NULL COMMENT '互动发起方成员ID', " +
- "to_member_id BIGINT NOT NULL COMMENT '互动接收方成员ID', " +
- "interaction_type VARCHAR(20) NOT NULL COMMENT '互动类型: dining/outgoing/call/video/chat/play/gift', " +
- "description VARCHAR(500) COMMENT '互动描述', " +
- "happened_at DATETIME DEFAULT CURRENT_TIMESTAMP COMMENT '互动发生时间', " +
- "created_at DATETIME DEFAULT CURRENT_TIMESTAMP, " +
- "INDEX idx_ilog_family (family_id), " +
- "INDEX idx_ilog_to_member (to_member_id) " +
- ") ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='家庭成员互动记录'");
- log.info("已创建interaction_logs表");
- } catch (Exception e) {
- // 表已存在,忽略
- }
- // 迁移156: 创建 relationship_questionnaire_snapshots 表(关系问卷快照)
- try {
- jdbcTemplate.execute("CREATE TABLE IF NOT EXISTS relationship_questionnaire_snapshots (" +
- "id BIGINT AUTO_INCREMENT PRIMARY KEY, " +
- "family_member_id BIGINT NOT NULL COMMENT '家庭成员ID', " +
- "respondent_id BIGINT NOT NULL COMMENT '答卷人用户ID', " +
- "member_name VARCHAR(50) COMMENT '成员名称', " +
- "relationship_type VARCHAR(20) COMMENT '关系类型: parent/child', " +
- "ai_generated_json TEXT COMMENT 'AI生成的题目JSON', " +
- "version VARCHAR(20) COMMENT '版本号', " +
- "created_at DATETIME DEFAULT CURRENT_TIMESTAMP, " +
- "INDEX idx_q_snap_member (family_member_id) " +
- ") ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='关系问卷快照'");
- log.info("已创建relationship_questionnaire_snapshots表");
- } catch (Exception e) {
- // 表已存在,忽略
- }
- // 迁移158: users.family_id 允许 NULL(新用户微信登录时无家庭)
- try {
- jdbcTemplate.execute("ALTER TABLE users MODIFY COLUMN family_id BIGINT COMMENT '家庭ID'");
- log.info("已修改users.family_id允许NULL");
- } catch (Exception e) {
- // 忽略错误
- }
- // 迁移157: 创建 relationship_questionnaire_responses 表(关系问卷回答)
- try {
- jdbcTemplate.execute("CREATE TABLE IF NOT EXISTS relationship_questionnaire_responses (" +
- "id BIGINT AUTO_INCREMENT PRIMARY KEY, " +
- "snapshot_id BIGINT NOT NULL COMMENT '问卷快照ID', " +
- "respondent_id BIGINT NOT NULL COMMENT '答卷人用户ID', " +
- "answers_json TEXT COMMENT '用户回答JSON', " +
- "trust_score DECIMAL(5,2) DEFAULT 0 COMMENT '信任度得分', " +
- "intimacy_score DECIMAL(5,2) DEFAULT 0 COMMENT '亲密度得分', " +
- "communication_score DECIMAL(5,2) DEFAULT 0 COMMENT '沟通质量得分', " +
- "interaction_bonus DECIMAL(5,2) DEFAULT 0 COMMENT '互动记录加分', " +
- "total_score DECIMAL(5,2) DEFAULT 0 COMMENT '综合得分', " +
- "calculated_at DATETIME DEFAULT CURRENT_TIMESTAMP, " +
- "INDEX idx_q_resp_snap (snapshot_id) " +
- ") ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='关系问卷回答记录'");
- log.info("已创建relationship_questionnaire_responses表");
- } catch (Exception e) {
- // 表已存在,忽略
- }
- // 迁移158: family_challenge 添加 creator_id 列(用户自定义挑战)
- ensureColumn("family_challenge", "creator_id", "BIGINT COMMENT '创建者ID(NULL=系统创建)'");
- // 迁移159: guide_orders表order_no加唯一约束(防重复下单,整改R1)
- try {
- // 先清理历史重复数据:每组order_no保留id最小的一行
- jdbcTemplate.execute("DELETE g1 FROM guide_orders g1 INNER JOIN guide_orders g2 " +
- "ON g1.order_no = g2.order_no AND g1.id > g2.id");
- log.info("已清理guide_orders重复order_no");
- } catch (Exception e) {
- // 忽略:无重复数据时删除0行,不报错
- }
- try {
- jdbcTemplate.execute("ALTER TABLE guide_orders ADD UNIQUE KEY uk_order_no (order_no)");
- log.info("已为guide_orders.order_no添加唯一约束");
- } catch (Exception e) {
- // 约束已存在,忽略
- }
- // 迁移160: payment_orders表order_no升级为唯一约束(防支付回调重复,整改R2)
- try {
- jdbcTemplate.execute("DELETE p1 FROM payment_orders p1 INNER JOIN payment_orders p2 " +
- "ON p1.order_no = p2.order_no AND p1.id > p2.id");
- log.info("已清理payment_orders重复order_no");
- } catch (Exception e) {
- // 忽略
- }
- try {
- // 先删除原普通索引,再加唯一索引(同名列不可并存)
- jdbcTemplate.execute("ALTER TABLE payment_orders DROP INDEX idx_order_no");
- jdbcTemplate.execute("ALTER TABLE payment_orders ADD UNIQUE KEY uk_order_no (order_no)");
- log.info("已为payment_orders.order_no添加唯一约束");
- } catch (Exception e) {
- // 索引已存在或已处理,忽略
- }
- // 迁移161: 创建 daily_checkin 表(每日健康综合打卡,含饮食/运动/睡眠/饮水/心情评分)
- try {
- jdbcTemplate.execute("CREATE TABLE IF NOT EXISTS daily_checkin (" +
- "id BIGINT AUTO_INCREMENT PRIMARY KEY, " +
- "member_id BIGINT NOT NULL COMMENT '家庭成员ID', " +
- "checkin_date DATE COMMENT '打卡日期', " +
- "diet_score INT COMMENT '饮食评分', " +
- "exercise_score INT COMMENT '运动评分', " +
- "sleep_score INT COMMENT '睡眠评分', " +
- "water_intake INT COMMENT '饮水量(ml)', " +
- "mood_score INT COMMENT '心情评分', " +
- "weight_kg DECIMAL(5,2) COMMENT '体重(kg)', " +
- "height_cm DECIMAL(5,2) COMMENT '身高(cm)', " +
- "remark TEXT COMMENT '备注', " +
- "created_at DATETIME DEFAULT CURRENT_TIMESTAMP, " +
- "updated_at DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, " +
- "INDEX idx_member_id (member_id)" +
- ") ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='每日健康打卡表'");
- log.info("已创建daily_checkin表");
- } catch (Exception ex) {
- log.warn("创建daily_checkin表失败: {}", ex.getMessage());
- }
- // 迁移162: 创建 exercise_record 表(运动打卡记录)
- try {
- jdbcTemplate.execute("CREATE TABLE IF NOT EXISTS exercise_record (" +
- "id BIGINT AUTO_INCREMENT PRIMARY KEY, " +
- "member_id BIGINT NOT NULL COMMENT '家庭成员ID', " +
- "checkin_id BIGINT COMMENT '关联打卡ID', " +
- "exercise_type VARCHAR(50) COMMENT '运动类型', " +
- "duration_minutes INT COMMENT '时长(分钟)', " +
- "intensity VARCHAR(20) COMMENT '强度: low/medium/high', " +
- "calories_burned INT COMMENT '消耗卡路里', " +
- "heart_rate_avg INT COMMENT '平均心率', " +
- "heart_rate_max INT COMMENT '最大心率', " +
- "step_count INT COMMENT '微信运动步数', " +
- "start_time DATETIME COMMENT '开始时间', " +
- "end_time DATETIME COMMENT '结束时间', " +
- "remark TEXT COMMENT '备注', " +
- "created_at DATETIME DEFAULT CURRENT_TIMESTAMP, " +
- "INDEX idx_member_id (member_id)" +
- ") ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='运动打卡记录'");
- log.info("已创建exercise_record表");
- } catch (Exception ex) {
- log.warn("创建exercise_record表失败: {}", ex.getMessage());
- }
- // 迁移163: family_challenge 添加 creator_id 列(用户自定义挑战,原迁移158在runMigration100死代码中从未执行)
- ensureColumn("family_challenge", "creator_id", "BIGINT COMMENT '创建者ID(NULL=系统创建)'");
- // 迁移164: coupon表扩展会员券字段 + 创建coupon_grant_log表(会员体系重构: 统一价+优惠券发放/积分兑换)
- try {
- ensureColumn("coupon", "discount_rate", "INT COMMENT '折扣券折扣率(千分比,9000=9折)'");
- ensureColumn("coupon", "product_id", "BIGINT COMMENT '绑定商品ID,NULL=全场通用'");
- ensureColumn("coupon", "points_price", "INT DEFAULT 0 COMMENT '积分兑换价,0=不可积分兑换'");
- ensureColumn("coupon", "grant_type", "VARCHAR(16) COMMENT '发放规则:JOIN/PERIODIC/POPULATION,NULL=不自动发放'");
- ensureColumn("coupon", "grant_level_code", "VARCHAR(32) COMMENT '发放规则对应会员等级'");
- ensureColumn("coupon", "grant_period", "VARCHAR(16) COMMENT 'PERIODIC周期:MONTHLY/QUARTERLY'");
- ensureColumn("coupon", "grant_quantity", "INT DEFAULT 1 COMMENT '每次发放数量'");
- log.info("已为coupon表添加会员券字段");
- } catch (Exception e) {
- log.warn("添加coupon表会员券字段失败: {}", e.getMessage());
- }
- try {
- jdbcTemplate.execute("CREATE TABLE IF NOT EXISTS coupon_grant_log (" +
- "id BIGINT AUTO_INCREMENT PRIMARY KEY, " +
- "user_id BIGINT NOT NULL COMMENT '收券用户ID', " +
- "coupon_id BIGINT NOT NULL COMMENT '券模板ID', " +
- "grant_type VARCHAR(16) NOT NULL COMMENT 'JOIN/PERIODIC/POPULATION/EXCHANGE', " +
- "period VARCHAR(16) COMMENT '周期标识(YYYY-MM或YYYY-Qn),PERIODIC防重用', " +
- "quantity INT DEFAULT 1 COMMENT '发放数量', " +
- "source VARCHAR(64) COMMENT '触发来源(订单号/成员ID等)', " +
- "created_at DATETIME DEFAULT CURRENT_TIMESTAMP, " +
- "UNIQUE KEY uk_grant (user_id, coupon_id, grant_type, period), " +
- "INDEX idx_coupon (coupon_id), " +
- "INDEX idx_user (user_id)" +
- ") ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='优惠券发放流水表'");
- log.info("已创建coupon_grant_log表");
- } catch (Exception e) {
- log.warn("创建coupon_grant_log表失败: {}", e.getMessage());
- }
- // 迁移165: virtual_goods_config 添加 goods_price 列(会员道具价格,用于拆单判断)
- ensureColumn("virtual_goods_config", "goods_price", "INT COMMENT '道具价格(分), 用于拆单判断'");
- // 迁移166: payment_orders 添加 group_no/period_no 列(会员拆单支付,配置驱动拆单)
- ensureColumn("payment_orders", "group_no", "VARCHAR(64) COMMENT '拆单组号(同一拆单组共享)'");
- ensureColumn("payment_orders", "period_no", "INT DEFAULT 1 COMMENT '期数序号(1开始)'");
- // 迁移167: 回填 families.creator_id(早期版本从未写入,导致会员开通/虚拟支付兜底失败)
- // 优先 is_family_admin=true 的用户
- try {
- jdbcTemplate.execute("UPDATE families f JOIN users u ON f.id = u.family_id AND u.is_family_admin = 1 " +
- "SET f.creator_id = u.id WHERE f.creator_id IS NULL");
- log.info("已回填families.creator_id(优先家庭管理员)");
- } catch (Exception e) {
- log.warn("回填families.creator_id(管理员)失败: {}", e.getMessage());
- }
- // 剩余家庭:任一 openid 非空用户(子查询确定性取一条)
- try {
- jdbcTemplate.execute("UPDATE families f SET f.creator_id = (" +
- "SELECT u.id FROM users u WHERE u.family_id = f.id " +
- "AND u.openid IS NOT NULL AND u.openid <> '' " +
- "ORDER BY (u.is_family_admin = 1) DESC, u.id ASC LIMIT 1" +
- ") WHERE f.creator_id IS NULL");
- log.info("已回填families.creator_id(任一成员)");
- } catch (Exception e) {
- log.warn("回填families.creator_id(任一成员)失败: {}", e.getMessage());
- }
- // 迁移168: 修复文章表 null status 为 'draft'(旧版新建文章未设默认值导致无法编辑)
- try {
- jdbcTemplate.execute("UPDATE articles SET status = 'draft' WHERE status IS NULL");
- log.info("已修复articles表中status为null的记录为draft");
- } catch (Exception e) {
- log.warn("修复articles表null status失败: {}", e.getMessage());
- }
- // 迁移169: 清理已废弃的 relationship_types 表(schema.sql 已移除,迁移代码中 ALTER 语句已清理)
- try {
- jdbcTemplate.execute("DROP TABLE IF EXISTS relationship_types");
- log.info("已清理废弃的relationship_types表");
- } catch (Exception e) {
- log.warn("清理relationship_types表失败: {}", e.getMessage());
- }
- // 迁移170: 创建 family_join_requests 表(家庭加入请求待审批)
- try {
- jdbcTemplate.execute("CREATE TABLE IF NOT EXISTS family_join_requests (" +
- "id BIGINT AUTO_INCREMENT PRIMARY KEY, " +
- "requester_user_id BIGINT NOT NULL COMMENT '申请加入的用户ID', " +
- "target_family_id BIGINT NOT NULL COMMENT '目标家庭ID', " +
- "join_method VARCHAR(20) NOT NULL COMMENT '加入方式: by_code/by_token', " +
- "invite_ref VARCHAR(50) COMMENT '邀请码或令牌', " +
- "status VARCHAR(20) DEFAULT 'pending' COMMENT '状态: pending/approved/rejected/cancelled', " +
- "approver_user_id BIGINT COMMENT '审批人ID', " +
- "comment TEXT COMMENT '审批备注', " +
- "created_at DATETIME DEFAULT CURRENT_TIMESTAMP, " +
- "updated_at DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, " +
- "INDEX idx_requester (requester_user_id), " +
- "INDEX idx_target_family (target_family_id), " +
- "INDEX idx_status (status) " +
- ") ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='家庭加入请求(待审批)'");
- log.info("已创建family_join_requests表");
- } catch (Exception e) {
- // 表已存在,忽略
- }
- // 迁移171: 创建 knowledge_base_section 表(统一知识库按模块存储JSON,DB为查询源,classpath种子兜底)
- try {
- jdbcTemplate.execute("CREATE TABLE IF NOT EXISTS knowledge_base_section (" +
- "id BIGINT AUTO_INCREMENT PRIMARY KEY, " +
- "module_key VARCHAR(100) NOT NULL COMMENT '模块键(如 食物营养库/菌属功能库/报告指标分级标准)', " +
- "module_name VARCHAR(200) COMMENT '模块中文名', " +
- "content LONGTEXT COMMENT '模块内容(JSON序列化)', " +
- "version VARCHAR(50) COMMENT '数据版本(来自元数据.版本)', " +
- "created_at DATETIME DEFAULT CURRENT_TIMESTAMP, " +
- "updated_at DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, " +
- "UNIQUE KEY uk_module_key (module_key) " +
- ") ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='统一知识库 - 按模块存储JSON'");
- log.info("已创建knowledge_base_section表");
- } catch (Exception e) {
- // 表已存在,忽略
- }
- // 迁移172: products 添加 min_stock_alert 列(库存预警阈值,配合库存管理模块)
- // 迁移173: 商品配送与履约扩展(自取/线上服务),需求: 配送方式1=快递 2=自提 3=两者皆可 4=线上-可选人 5=线上-单人
- // 173a: product_orders 新增履约字段
- ensureColumn("product_orders", "pickup_location_id", "BIGINT COMMENT '自取地点ID'");
- ensureColumn("product_orders", "pickup_location_snapshot", "TEXT COMMENT '自取地点快照(JSON)'");
- ensureColumn("product_orders", "time_slot_id", "BIGINT COMMENT '所选时间段ID'");
- ensureColumn("product_orders", "time_slot_snapshot", "TEXT COMMENT '时间段快照(JSON)'");
- ensureColumn("product_orders", "service_person_id", "BIGINT COMMENT '所选服务者ID(deliveryMethod=4)'");
- ensureColumn("product_orders", "service_person_snapshot", "TEXT COMMENT '服务者快照(JSON)'");
- ensureColumn("product_orders", "fulfill_status", "VARCHAR(32) DEFAULT 'pending' COMMENT '履约状态: pending/confirmed_by_vendor/received_by_user/completed'");
- ensureColumn("product_orders", "fulfill_confirmed_at", "DATETIME COMMENT '供应商确认时间'");
- ensureColumn("product_orders", "received_at", "DATETIME COMMENT '用户确认收货时间'");
- // 173b: 自取地点表
- try {
- jdbcTemplate.execute("CREATE TABLE IF NOT EXISTS product_pickup_locations (" +
- "id BIGINT AUTO_INCREMENT PRIMARY KEY COMMENT '主键', " +
- "product_id BIGINT NOT NULL COMMENT '所属商品ID', " +
- "name VARCHAR(100) NOT NULL COMMENT '地点名称', " +
- "address VARCHAR(500) NOT NULL COMMENT '详细地址', " +
- "longitude DECIMAL(10,7) COMMENT '经度', " +
- "latitude DECIMAL(10,7) COMMENT '纬度', " +
- "contact_name VARCHAR(50) COMMENT '联系人', " +
- "contact_phone VARCHAR(20) COMMENT '联系电话', " +
- "sort_order INT DEFAULT 0 COMMENT '排序', " +
- "enabled TINYINT DEFAULT 1 COMMENT '启用: 0/1', " +
- "created_at DATETIME DEFAULT CURRENT_TIMESTAMP, " +
- "updated_at DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, " +
- "INDEX idx_product_id (product_id)" +
- ") ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='商品自取地点'");
- log.info("已创建product_pickup_locations表");
- } catch (Exception ex) {
- log.warn("创建product_pickup_locations表失败: {}", ex.getMessage());
- }
- // 173c: 自取时间段模板表
- try {
- jdbcTemplate.execute("CREATE TABLE IF NOT EXISTS product_pickup_time_slots (" +
- "id BIGINT AUTO_INCREMENT PRIMARY KEY COMMENT '主键', " +
- "location_id BIGINT NOT NULL COMMENT '所属地点ID', " +
- "start_time TIME NOT NULL COMMENT '开始时间 HH:MM', " +
- "end_time TIME NOT NULL COMMENT '结束时间 HH:MM', " +
- "weekday TINYINT COMMENT '周几(1-7), null=按具体日期', " +
- "specific_date DATE COMMENT '具体日期, null=按周循环', " +
- "max_capacity INT DEFAULT 1 COMMENT '该时段最大接待人数', " +
- "booked_count INT DEFAULT 0 COMMENT '已预订数', " +
- "enabled TINYINT DEFAULT 1, " +
- "created_at DATETIME DEFAULT CURRENT_TIMESTAMP, " +
- "updated_at DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, " +
- "INDEX idx_location_id (location_id), " +
- "INDEX idx_specific_date (specific_date)" +
- ") ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='自取时间段模板'");
- log.info("已创建product_pickup_time_slots表");
- } catch (Exception ex) {
- log.warn("创建product_pickup_time_slots表失败: {}", ex.getMessage());
- }
- // 173d: 线上服务者表
- try {
- jdbcTemplate.execute("CREATE TABLE IF NOT EXISTS product_service_persons (" +
- "id BIGINT AUTO_INCREMENT PRIMARY KEY COMMENT '主键', " +
- "product_id BIGINT NOT NULL COMMENT '所属商品ID', " +
- "user_id BIGINT NOT NULL COMMENT '服务者用户ID(关联users表)', " +
- "name VARCHAR(50) NOT NULL COMMENT '展示名称', " +
- "title VARCHAR(100) COMMENT '头衔(如高级营养师)', " +
- "intro TEXT COMMENT '个人介绍', " +
- "avatar VARCHAR(500) COMMENT '头像URL', " +
- "price INT NOT NULL COMMENT '该人专属价格(分)', " +
- "original_price INT COMMENT '原价(分)', " +
- "sort_order INT DEFAULT 0, " +
- "enabled TINYINT DEFAULT 1, " +
- "created_at DATETIME DEFAULT CURRENT_TIMESTAMP, " +
- "updated_at DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, " +
- "INDEX idx_product_id (product_id), " +
- "INDEX idx_user_id (user_id)" +
- ") ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='商品线上服务者'");
- log.info("已创建product_service_persons表");
- } catch (Exception ex) {
- log.warn("创建product_service_persons表失败: {}", ex.getMessage());
- }
- // 173e: 服务者可预约时间段表
- try {
- jdbcTemplate.execute("CREATE TABLE IF NOT EXISTS product_service_person_slots (" +
- "id BIGINT AUTO_INCREMENT PRIMARY KEY COMMENT '主键', " +
- "person_id BIGINT NOT NULL COMMENT '所属服务者ID', " +
- "start_time DATETIME NOT NULL COMMENT '开始时间', " +
- "end_time DATETIME NOT NULL COMMENT '结束时间', " +
- "max_capacity INT DEFAULT 1 COMMENT '容量', " +
- "booked_count INT DEFAULT 0 COMMENT '已预订数', " +
- "enabled TINYINT DEFAULT 1, " +
- "created_at DATETIME DEFAULT CURRENT_TIMESTAMP, " +
- "updated_at DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, " +
- "INDEX idx_person_id (person_id), " +
- "INDEX idx_start_time (start_time)" +
- ") ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='服务者可预约时间段'");
- log.info("已创建product_service_person_slots表");
- } catch (Exception ex) {
- log.warn("创建product_service_person_slots表失败: {}", ex.getMessage());
- }
- // 173f: 单人线上服务时间段表
- try {
- jdbcTemplate.execute("CREATE TABLE IF NOT EXISTS product_online_slots (" +
- "id BIGINT AUTO_INCREMENT PRIMARY KEY COMMENT '主键', " +
- "product_id BIGINT NOT NULL COMMENT '所属商品ID', " +
- "start_time DATETIME NOT NULL COMMENT '开始时间', " +
- "end_time DATETIME NOT NULL COMMENT '结束时间', " +
- "max_capacity INT DEFAULT 1 COMMENT '容量', " +
- "booked_count INT DEFAULT 0 COMMENT '已预订数', " +
- "enabled TINYINT DEFAULT 1, " +
- "created_at DATETIME DEFAULT CURRENT_TIMESTAMP, " +
- "updated_at DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, " +
- "INDEX idx_product_id (product_id), " +
- "INDEX idx_start_time (start_time)" +
- ") ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='单人线上服务时间段'");
- log.info("已创建product_online_slots表");
- } catch (Exception ex) {
- log.warn("创建product_online_slots表失败: {}", ex.getMessage());
- }
- // 迁移166: 创建 membership_benefits 表(会员权益配置)
- try {
- jdbcTemplate.execute("CREATE TABLE IF NOT EXISTS membership_benefits (" +
- "id BIGINT AUTO_INCREMENT PRIMARY KEY, " +
- "level_code VARCHAR(50) NOT NULL COMMENT '会员等级: FREE/PROFESSIONAL/ENTERPRISE', " +
- "benefit_code VARCHAR(50) NOT NULL COMMENT '权益编码: AI_CHAT,FAMILY_MEMBERS等', " +
- "benefit_name VARCHAR(100) NOT NULL COMMENT '权益名称', " +
- "benefit_desc VARCHAR(500) COMMENT '权益描述', " +
- "icon VARCHAR(50) COMMENT '图标emoji', " +
- "bg_color VARCHAR(20) COMMENT '背景色', " +
- "sort_order INT DEFAULT 0 COMMENT '排序顺序', " +
- "created_at DATETIME DEFAULT CURRENT_TIMESTAMP, " +
- "updated_at DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, " +
- "UNIQUE KEY uk_level_benefit (level_code, benefit_code), " +
- "INDEX idx_level (level_code)" +
- ") ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='会员权益配置'");
- log.info("已创建membership_benefits表");
- } catch (Exception ex) {
- log.warn("创建membership_benefits表失败: {}", ex.getMessage());
- }
- // 迁移167: membership_benefits 种子数据
- try {
- jdbcTemplate.execute("INSERT IGNORE INTO membership_benefits (level_code, benefit_code, benefit_name, benefit_desc, icon, bg_color, sort_order) VALUES " +
- "('FREE', 'NO_BENEFIT', '暂无会员权益', '开通会员解锁更多权益', '🔒', '#F3F4F6', 0), " +
- "('FREE', 'UPGRADE_HINT', '升级会员', '开通会员享专属权益', '👑', '#FEF3C7', 99), " +
- "('FAMILY', 'AI_CHAT', 'AI 健康顾问', '7×24 小时不限次数问答', '🤖', '#DBEAFE', 1), " +
- "('FAMILY', 'FAMILY_MEMBERS', '家庭档案管理', '管理最多 10 位家庭成员', '👨👩👧👦', '#D1FAE5', 2), " +
- "('FAMILY', 'HEALTH_PLAN', '月度健康计划', '个性化健康改善方案', '📋', '#FEF3C7', 3), " +
- "('FAMILY', 'REPORT_READING', '报告智能解读', '体检/菌群/测评报告 AI 解读', '📊', '#FCE7F3', 4), " +
- "('FAMILY', 'MANAGER_SERVICE', '专属健康教练服务', '认证健康教练月度随访', '👨🏫', '#FFEDD5', 5), " +
- "('FAMILY', 'PRODUCT_DISCOUNT', '商品会员折扣', '全场商品 8-9 折优惠', '🛒', '#E0E7FF', 6), " +
- "('FAMILY', 'COUPON_ACCESS', '优惠券使用权限', '可使用平台优惠券享更多优惠', '🎫', '#FEE2E2', 7), " +
- "('PREMIUM', 'AI_CHAT', 'AI 健康顾问', '7×24 小时不限次数问答', '🤖', '#DBEAFE', 1), " +
- "('PREMIUM', 'FAMILY_MEMBERS', '家庭档案管理', '管理最多 20 位家庭成员', '👨👩👧👦', '#D1FAE5', 2), " +
- "('PREMIUM', 'HEALTH_PLAN', '月度健康计划', '个性化健康改善方案 + 定制营养建议', '📋', '#FEF3C7', 3), " +
- "('PREMIUM', 'REPORT_READING', '报告智能解读', '体检/菌群/测评报告 AI 深度解读', '📊', '#FCE7F3', 4), " +
- "('PREMIUM', 'MANAGER_SERVICE', '专属健康教练服务', '每周 1 次电话随访 + 在线答疑', '👨🏫', '#FFEDD5', 5), " +
- "('PREMIUM', 'PRODUCT_DISCOUNT', '商品会员折扣', '全场商品 6-8 折优惠', '🛒', '#E0E7FF', 6), " +
- "('PREMIUM', 'COUPON_ACCESS', '优惠券使用权限', '可使用平台优惠券 + 会员专属券', '🎫', '#FEE2E2', 7), " +
- "('PREMIUM', 'PRIORITY_SUPPORT', '优先客服', '专属客服通道,快速响应', '🎧', '#EDE9FE', 8), " +
- "('PREMIUM', 'TEACHER_CONSULT', '规划师一对一咨询', '每月 1 次成长规划师专业咨询', '👩🏫', '#FBCFE8', 9)");
- log.info("已插入membership_benefits种子数据");
- } catch (Exception ex) {
- log.warn("插入membership_benefits种子数据失败: {}", ex.getMessage());
- }
- // 迁移168: 创建用户同意记录表(法律合规)
- try {
- jdbcTemplate.execute("CREATE TABLE IF NOT EXISTS user_consent_records (" +
- "id BIGINT AUTO_INCREMENT PRIMARY KEY, " +
- "user_id BIGINT NOT NULL COMMENT '用户ID', " +
- "consent_type VARCHAR(50) NOT NULL COMMENT '同意类型: privacy/agreement/children_data/ai_processing', " +
- "version VARCHAR(20) NOT NULL COMMENT '协议版本号', " +
- "status VARCHAR(20) NOT NULL DEFAULT 'agreed' COMMENT 'agreed/withdrawn/archived', " +
- "ip_address VARCHAR(45) COMMENT '用户IP地址', " +
- "created_at DATETIME NOT NULL, " +
- "updated_at DATETIME, " +
- "INDEX idx_user_consent (user_id, consent_type), " +
- "INDEX idx_consent_type (consent_type)" +
- ") ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='用户同意记录'");
- log.info("已创建user_consent_records表");
- } catch (Exception e) {
- log.warn("创建user_consent_records表失败(可能已存在): " + e.getMessage());
- }
- // 迁移169: 创建用户数据授权表(法律合规)
- try {
- jdbcTemplate.execute("CREATE TABLE IF NOT EXISTS user_data_permissions (" +
- "id BIGINT AUTO_INCREMENT PRIMARY KEY, " +
- "user_id BIGINT NOT NULL COMMENT '用户ID', " +
- "allow_ai_analysis TINYINT(1) DEFAULT 0 COMMENT '允许AI分析对话数据', " +
- "allow_personalized_recommend TINYINT(1) DEFAULT 1 COMMENT '允许个性化推荐', " +
- "allow_share_teacher TINYINT(1) DEFAULT 1 COMMENT '允许向成长规划师共享数据', " +
- "allow_third_party_share TINYINT(1) DEFAULT 0 COMMENT '允许与第三方共享数据', " +
- "created_at DATETIME NOT NULL, " +
- "updated_at DATETIME, " +
- "INDEX idx_user_perm (user_id)" +
- ") ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='用户数据使用授权'");
- log.info("已创建user_data_permissions表");
- } catch (Exception e) {
- log.warn("创建user_data_permissions表失败(可能已存在): " + e.getMessage());
- }
- // 迁移174: family_members 添加饮食相关字段(饮食模块需求)
- try {
- jdbcTemplate.execute("ALTER TABLE family_members ADD COLUMN is_living_together TINYINT(1) DEFAULT 1 COMMENT '是否同住(0=不同住,不出现在食谱推荐中)'");
- log.info("已添加is_living_together列到family_members表");
- } catch (Exception e) {
- log.warn("family_members 添加 is_living_together 列失败(可能已存在): " + e.getMessage());
- }
- try {
- jdbcTemplate.execute("ALTER TABLE family_members ADD COLUMN meal_with_breakfast_weekday TINYINT(1) DEFAULT 0 COMMENT '工作日早餐同餐'");
- log.info("已添加meal_with_breakfast_weekday列到family_members表");
- } catch (Exception e) {
- log.warn("family_members 添加 meal_with_breakfast_weekday 列失败(可能已存在): " + e.getMessage());
- }
- try {
- jdbcTemplate.execute("ALTER TABLE family_members ADD COLUMN meal_with_lunch_weekday TINYINT(1) DEFAULT 0 COMMENT '工作日午餐同餐'");
- log.info("已添加meal_with_lunch_weekday列到family_members表");
- } catch (Exception e) {
- log.warn("family_members 添加 meal_with_lunch_weekday 列失败(可能已存在): " + e.getMessage());
- }
- try {
- jdbcTemplate.execute("ALTER TABLE family_members ADD COLUMN meal_with_dinner_weekday TINYINT(1) DEFAULT 0 COMMENT '工作日晚餐同餐'");
- log.info("已添加meal_with_dinner_weekday列到family_members表");
- } catch (Exception e) {
- log.warn("family_members 添加 meal_with_dinner_weekday 列失败(可能已存在): " + e.getMessage());
- }
- try {
- jdbcTemplate.execute("ALTER TABLE family_members ADD COLUMN meal_with_breakfast_weekend TINYINT(1) DEFAULT 0 COMMENT '周末早餐同餐'");
- log.info("已添加meal_with_breakfast_weekend列到family_members表");
- } catch (Exception e) {
- log.warn("family_members 添加 meal_with_breakfast_weekend 列失败(可能已存在): " + e.getMessage());
- }
- try {
- jdbcTemplate.execute("ALTER TABLE family_members ADD COLUMN meal_with_lunch_weekend TINYINT(1) DEFAULT 0 COMMENT '周末午餐同餐'");
- log.info("已添加meal_with_lunch_weekend列到family_members表");
- } catch (Exception e) {
- log.warn("family_members 添加 meal_with_lunch_weekend 列失败(可能已存在): " + e.getMessage());
- }
- try {
- jdbcTemplate.execute("ALTER TABLE family_members ADD COLUMN meal_with_dinner_weekend TINYINT(1) DEFAULT 0 COMMENT '周末晚餐同餐'");
- log.info("已添加meal_with_dinner_weekend列到family_members表");
- } catch (Exception e) {
- log.warn("family_members 添加 meal_with_dinner_weekend 列失败(可能已存在): " + e.getMessage());
- }
- // 迁移175: 创建 diet_preferences 表(饮食偏好调研表)
- try {
- jdbcTemplate.execute("CREATE TABLE IF NOT EXISTS diet_preferences (" +
- "id BIGINT AUTO_INCREMENT PRIMARY KEY, " +
- "family_member_id BIGINT NOT NULL COMMENT '绑定家庭成员', " +
- "allergies JSON COMMENT '过敏原列表', " +
- "absolute_avoid JSON COMMENT '绝对忌口列表', " +
- "must_eat JSON COMMENT '必吃食物列表', " +
- "religious_diet VARCHAR(50) DEFAULT 'none' COMMENT '宗教饮食', " +
- "spice_level TINYINT DEFAULT NULL COMMENT '辣度 0-5', " +
- "flavor_pref JSON COMMENT '口味偏好', " +
- "cuisine_pref JSON COMMENT '菜系偏好', " +
- "cooking_methods JSON COMMENT '烹饪方式偏好', " +
- "health_goals JSON COMMENT '健康目标', " +
- "goal_source VARCHAR(20) DEFAULT NULL COMMENT 'manual/system_recommend', " +
- "goal_confirmed TINYINT(1) DEFAULT 0 COMMENT '用户已确认', " +
- "filled_at TIMESTAMP NULL COMMENT '首次填写时间', " +
- "updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, " +
- "UNIQUE KEY uk_member (family_member_id), " +
- "INDEX idx_updated (updated_at)" +
- ") ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='饮食偏好调研表'");
- log.info("已创建diet_preferences表");
- } catch (Exception e) {
- log.warn("创建 diet_preferences 表失败(可能已存在): " + e.getMessage());
- }
- // 迁移176: 创建 meal_configs 表(共餐配置表)
- try {
- jdbcTemplate.execute("CREATE TABLE IF NOT EXISTS meal_configs (" +
- "id BIGINT AUTO_INCREMENT PRIMARY KEY, " +
- "family_id BIGINT NOT NULL COMMENT '家庭ID', " +
- "config_date_type VARCHAR(20) NOT NULL COMMENT '工作日/周末/节假日', " +
- "meal_type VARCHAR(20) NOT NULL COMMENT 'breakfast/lunch/dinner', " +
- "participant_member_ids JSON NOT NULL COMMENT '参与成员ID列表', " +
- "notes VARCHAR(255) DEFAULT NULL COMMENT '备注', " +
- "created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, " +
- "updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, " +
- "UNIQUE KEY uk_config (family_id, config_date_type, meal_type), " +
- "INDEX idx_family (family_id)" +
- ") ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='共餐配置表'");
- log.info("已创建meal_configs表");
- } catch (Exception e) {
- log.warn("创建 meal_configs 表失败(可能已存在): " + e.getMessage());
- }
- // 迁移177: 创建 diet_recommendations 表(食谱推荐方案表)
- try {
- jdbcTemplate.execute("CREATE TABLE IF NOT EXISTS diet_recommendations (" +
- "id BIGINT AUTO_INCREMENT PRIMARY KEY, " +
- "family_id BIGINT NOT NULL COMMENT '家庭ID', " +
- "recommendation_date DATE NOT NULL COMMENT '推荐日期', " +
- "meal_type VARCHAR(20) NOT NULL COMMENT 'breakfast/lunch/dinner', " +
- "participant_member_ids JSON COMMENT '参与成员ID列表', " +
- "menu_json MEDIUMTEXT NOT NULL COMMENT '菜品列表JSON', " +
- "nutrition_summary JSON COMMENT '营养汇总', " +
- "status VARCHAR(20) DEFAULT 'pending' COMMENT 'pending/accepted/adjusted/completed/skipped', " +
- "version INT DEFAULT 1 COMMENT '版本号', " +
- "created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, " +
- "updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, " +
- "INDEX idx_family_date (family_id, recommendation_date), " +
- "INDEX idx_meal (meal_type, recommendation_date)" +
- ") ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='食谱推荐方案表'");
- log.info("已创建diet_recommendations表");
- } catch (Exception e) {
- log.warn("创建 diet_recommendations 表失败(可能已存在): " + e.getMessage());
- }
- // 迁移178: 创建 diet_records 表(饮食记录表)
- try {
- jdbcTemplate.execute("CREATE TABLE IF NOT EXISTS diet_records (" +
- "id BIGINT AUTO_INCREMENT PRIMARY KEY, " +
- "family_id BIGINT NOT NULL COMMENT '家庭ID', " +
- "member_id BIGINT NOT NULL COMMENT '吃的成员ID', " +
- "meal_type VARCHAR(20) NOT NULL COMMENT 'breakfast/lunch/dinner/snack', " +
- "record_date DATE NOT NULL COMMENT '记录日期', " +
- "record_method VARCHAR(20) DEFAULT NULL COMMENT 'photo/manual/from_recommendation', " +
- "image_url VARCHAR(500) DEFAULT NULL COMMENT '原图OSS路径', " +
- "ai_recognized_foods JSON DEFAULT NULL COMMENT 'AI识别结果', " +
- "user_confirmed_foods JSON NOT NULL COMMENT '用户最终确认食材', " +
- "notes VARCHAR(500) DEFAULT NULL COMMENT '备注', " +
- "created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, " +
- "INDEX idx_member_date (member_id, record_date), " +
- "INDEX idx_family_date (family_id, record_date)" +
- ") ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='饮食记录表'");
- log.info("已创建diet_records表");
- } catch (Exception e) {
- log.warn("创建 diet_records 表失败(可能已存在): " + e.getMessage());
- }
- // 迁移179: 创建 diet_record_items 表(饮食记录食材明细表)
- try {
- jdbcTemplate.execute("CREATE TABLE IF NOT EXISTS diet_record_items (" +
- "id BIGINT AUTO_INCREMENT PRIMARY KEY, " +
- "record_id BIGINT NOT NULL COMMENT 'diet_records.id', " +
- "food_name VARCHAR(100) NOT NULL COMMENT '食材名称', " +
- "food_id BIGINT DEFAULT NULL COMMENT '关联foods表ID', " +
- "confidence DECIMAL(3,2) DEFAULT NULL COMMENT 'AI置信度0-1', " +
- "source VARCHAR(20) DEFAULT NULL COMMENT 'ai_recognized/manual/from_recommendation', " +
- "INDEX idx_record (record_id)" +
- ") ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='饮食记录食材明细表'");
- log.info("已创建diet_record_items表");
- } catch (Exception e) {
- log.warn("创建 diet_record_items 表失败(可能已存在): " + e.getMessage());
- }
- // 迁移180: health_reports 添加代上传字段(规划师/营养师代用户提交报告,修复 Unknown column 'proxy_uploader_id' 错误)
- ensureColumn("health_reports", "proxy_uploader_id", "BIGINT COMMENT '代上传人ID(规划师/营养师)'");
- ensureColumn("health_reports", "proxy_upload_note", "TEXT COMMENT '代上传备注'");
- ensureColumn("health_reports", "ai_analysis", "TEXT COMMENT 'AI解读结果JSON'");
- // 迁移180b: health_reports 添加报告来源格式(beijing/guhe,两种菌群报告数据结构不兼容,需区分)
- ensureColumn("health_reports", "report_format", "VARCHAR(32) COMMENT '报告来源格式: beijing(北京菌群)/guhe(谷禾)'");
- // 迁移181: health_report_drafts 添加备注字段(代上传说明文字)
- ensureColumn("health_report_drafts", "remark", "VARCHAR(500) DEFAULT NULL COMMENT '备注(如代上传时的说明文字)'");
- // 迁移182: 创建 family_challenge_participant 表(挑战参与成员,任意成员发起/可选参与)
- try {
- jdbcTemplate.execute("CREATE TABLE IF NOT EXISTS family_challenge_participant (" +
- "id BIGINT AUTO_INCREMENT PRIMARY KEY, " +
- "challenge_id BIGINT NOT NULL COMMENT '挑战ID', " +
- "member_id BIGINT NOT NULL COMMENT '家庭成员ID', " +
- "status VARCHAR(20) NOT NULL COMMENT 'pending/agreed/rejected', " +
- "agreed_at DATETIME NULL COMMENT '同意/拒绝时间', " +
- "created_at DATETIME DEFAULT CURRENT_TIMESTAMP, " +
- "updated_at DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, " +
- "UNIQUE KEY uk_challenge_member (challenge_id, member_id), " +
- "INDEX idx_member (member_id)" +
- ") ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='家庭挑战参与成员'");
- log.info("已创建family_challenge_participant表");
- } catch (Exception e) {
- log.warn("创建 family_challenge_participant 表失败(可能已存在): " + e.getMessage());
- }
- // 迁移183: 创建 family_relationship_scores 表(方向性关系分数,A对B ≠ B对A)
- try {
- jdbcTemplate.execute("CREATE TABLE IF NOT EXISTS family_relationship_scores (" +
- "id BIGINT AUTO_INCREMENT PRIMARY KEY, " +
- "family_id BIGINT NOT NULL COMMENT '家庭ID', " +
- "from_member_id BIGINT NOT NULL COMMENT '分数持有方(A对B的行)', " +
- "to_member_id BIGINT NOT NULL COMMENT '分数对象方', " +
- "trust_score DECIMAL(5,2) DEFAULT 0 COMMENT 'A对B的信任', " +
- "intimacy_score DECIMAL(5,2) DEFAULT 0 COMMENT 'A对B的亲近', " +
- "communication_score DECIMAL(5,2) DEFAULT 0 COMMENT 'A对B的沟通', " +
- "created_at DATETIME DEFAULT CURRENT_TIMESTAMP, " +
- "updated_at DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, " +
- "UNIQUE KEY uk_from_to (from_member_id, to_member_id), " +
- "INDEX idx_family (family_id)" +
- ") ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='方向性关系分数'");
- log.info("已创建family_relationship_scores表");
- } catch (Exception e) {
- log.warn("创建 family_relationship_scores 表失败(可能已存在): " + e.getMessage());
- }
- // 迁移184: 创建 family_challenge_score_config 表(挑战评分配置,后台可配置)+ 默认数据
- try {
- jdbcTemplate.execute("CREATE TABLE IF NOT EXISTS family_challenge_score_config (" +
- "id BIGINT AUTO_INCREMENT PRIMARY KEY, " +
- "action VARCHAR(20) NOT NULL COMMENT 'initiate/agree/reject', " +
- "score_type VARCHAR(20) NOT NULL COMMENT 'energy/trust/intimacy/communication', " +
- "value INT NOT NULL DEFAULT 0 COMMENT '增减值(可正可负,默认±1)', " +
- "enabled TINYINT DEFAULT 1, " +
- "updated_at DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, " +
- "UNIQUE KEY uk_action_type (action, score_type)" +
- ") ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='家庭挑战评分配置'");
- jdbcTemplate.execute("INSERT IGNORE INTO family_challenge_score_config (action, score_type, value, enabled) VALUES " +
- "('initiate','energy',1,1),('initiate','trust',0,1),('initiate','intimacy',1,1),('initiate','communication',1,1)," +
- "('agree','energy',1,1),('agree','trust',1,1),('agree','intimacy',1,1),('agree','communication',1,1)," +
- "('reject','energy',-1,1),('reject','trust',-1,1),('reject','intimacy',-1,1),('reject','communication',1,1)");
- log.info("已创建family_challenge_score_config表并写入默认配置");
- } catch (Exception e) {
- log.warn("创建 family_challenge_score_config 表失败(可能已存在): " + e.getMessage());
- }
- // 迁移185: 存量挑战补全 participant 行(全员 agreed,兼容旧数据,不破坏)
- try {
- jdbcTemplate.execute("INSERT IGNORE INTO family_challenge_participant (challenge_id, member_id, status, agreed_at) " +
- "SELECT fc.id, fm.id, 'agreed', NOW() FROM family_challenge fc " +
- "JOIN family_members fm ON fm.family_id = fc.family_id " +
- "WHERE NOT EXISTS (SELECT 1 FROM family_challenge_participant fcp WHERE fcp.challenge_id = fc.id)");
- log.info("已为存量挑战补全participant行");
- } catch (Exception e) {
- log.warn("存量挑战补全participant失败(可能已处理): " + e.getMessage());
- }
- // ===== 报告指纹检测与解析器调度系统 =====
- // 迁移186: 创建 report_type_registry 表(报告类型注册表)
- try {
- jdbcTemplate.execute("CREATE TABLE IF NOT EXISTS report_type_registry (" +
- "id BIGINT AUTO_INCREMENT PRIMARY KEY, " +
- "type_id VARCHAR(64) NOT NULL COMMENT '类型标识,如 dan_a1/gut_beijing', " +
- "family VARCHAR(32) DEFAULT NULL COMMENT '报告家族: dan/gut_flora/其他', " +
- "display_name VARCHAR(100) NOT NULL COMMENT '显示名称', " +
- "description VARCHAR(500) DEFAULT NULL COMMENT '描述', " +
- "parser_path VARCHAR(255) DEFAULT NULL COMMENT 'Python解析器脚本路径', " +
- "min_confidence INT DEFAULT 10 COMMENT '识别阈值', " +
- "is_active TINYINT(1) DEFAULT 1 COMMENT '启用状态', " +
- "source VARCHAR(20) DEFAULT 'prebuilt' COMMENT '来源: prebuilt/imported/auto_generated', " +
- "review_status VARCHAR(20) DEFAULT 'approved' COMMENT '审核状态: approved/pending/rejected', " +
- "version VARCHAR(20) DEFAULT '1.0' COMMENT '版本', " +
- "created_by BIGINT DEFAULT NULL COMMENT '创建人ID', " +
- "created_at DATETIME DEFAULT CURRENT_TIMESTAMP, " +
- "updated_at DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, " +
- "UNIQUE KEY uk_type_id (type_id)" +
- ") ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='报告类型注册表'");
- log.info("已创建report_type_registry表");
- } catch (Exception e) {
- log.warn("创建 report_type_registry 表失败(可能已存在): " + e.getMessage());
- }
- // 迁移187: 创建 report_fingerprint 表(指纹规则)
- try {
- jdbcTemplate.execute("CREATE TABLE IF NOT EXISTS report_fingerprint (" +
- "id BIGINT AUTO_INCREMENT PRIMARY KEY, " +
- "type_id VARCHAR(64) NOT NULL COMMENT '关联report_type_registry.type_id', " +
- "pattern VARCHAR(200) NOT NULL COMMENT '文本特征', " +
- "weight INT DEFAULT 10 COMMENT '权重(正=加分,负=排除扣分)', " +
- "is_exclusion TINYINT(1) DEFAULT 0 COMMENT '是否为排除规则', " +
- "sort_order INT DEFAULT 0 COMMENT '排序', " +
- "created_at DATETIME DEFAULT CURRENT_TIMESTAMP, " +
- "INDEX idx_fp_type (type_id)" +
- ") ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='报告指纹规则表'");
- log.info("已创建report_fingerprint表");
- } catch (Exception e) {
- log.warn("创建 report_fingerprint 表失败(可能已存在): " + e.getMessage());
- }
- // 迁移188: 创建 report_unknown_cluster 表(未知报告聚类,自学习用)
- try {
- jdbcTemplate.execute("CREATE TABLE IF NOT EXISTS report_unknown_cluster (" +
- "id BIGINT AUTO_INCREMENT PRIMARY KEY, " +
- "cluster_key VARCHAR(64) NOT NULL COMMENT '聚类哈希键(同类报告分组)', " +
- "common_signatures TEXT COMMENT '公共文本特征JSON(用于生成指纹)', " +
- "llm_template TEXT COMMENT 'LLM解析结果模板JSON', " +
- "report_count INT DEFAULT 0 COMMENT '累计报告数', " +
- "status VARCHAR(20) DEFAULT 'collecting' COMMENT '状态: collecting/ready/generated/imported', " +
- "generated_type_id VARCHAR(64) DEFAULT NULL COMMENT '生成的类型ID', " +
- "created_at DATETIME DEFAULT CURRENT_TIMESTAMP, " +
- "updated_at DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, " +
- "UNIQUE KEY uk_cluster_key (cluster_key)" +
- ") ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='未知报告聚类表'");
- log.info("已创建report_unknown_cluster表");
- } catch (Exception e) {
- log.warn("创建 report_unknown_cluster 表失败(可能已存在): " + e.getMessage());
- }
- // 迁移189: 创建 report_unknown_upload 表(未知报告明细)
- try {
- jdbcTemplate.execute("CREATE TABLE IF NOT EXISTS report_unknown_upload (" +
- "id BIGINT AUTO_INCREMENT PRIMARY KEY, " +
- "cluster_id BIGINT DEFAULT NULL COMMENT '关联report_unknown_cluster.id', " +
- "file_url VARCHAR(500) DEFAULT NULL COMMENT '文件路径', " +
- "file_hash VARCHAR(64) DEFAULT NULL COMMENT '文件哈希', " +
- "llm_result TEXT COMMENT 'LLM解析结果JSON', " +
- "uploaded_at DATETIME DEFAULT CURRENT_TIMESTAMP, " +
- "INDEX idx_unknown_cluster (cluster_id)" +
- ") ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='未知报告上传明细表'");
- log.info("已创建report_unknown_upload表");
- } catch (Exception e) {
- log.warn("创建 report_unknown_upload 表失败(可能已存在): " + e.getMessage());
- }
- // 迁移190: 创建 report_parser_import 表(外部导入记录)
- try {
- jdbcTemplate.execute("CREATE TABLE IF NOT EXISTS report_parser_import (" +
- "id BIGINT AUTO_INCREMENT PRIMARY KEY, " +
- "package_name VARCHAR(100) NOT NULL COMMENT '导入包名称', " +
- "package_file_url VARCHAR(500) DEFAULT NULL COMMENT '导入包文件路径', " +
- "fingerprint_json TEXT COMMENT '指纹规则JSON', " +
- "parser_script TEXT COMMENT '解析器脚本内容', " +
- "status VARCHAR(20) DEFAULT 'pending' COMMENT '状态: pending/approved/rejected', " +
- "imported_by BIGINT DEFAULT NULL COMMENT '导入人ID', " +
- "reviewed_by BIGINT DEFAULT NULL COMMENT '审核人ID', " +
- "review_note VARCHAR(500) DEFAULT NULL COMMENT '审核意见', " +
- "created_at DATETIME DEFAULT CURRENT_TIMESTAMP, " +
- "updated_at DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP" +
- ") ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='报告解析器导入记录表'");
- log.info("已创建report_parser_import表");
- } catch (Exception e) {
- log.warn("创建 report_parser_import 表失败(可能已存在): " + e.getMessage());
- }
- // 迁移191: foods 添加 report_id/score 列(Food 实体已含字段,生产库缺列导致 Unknown column 'report_id' 错误)
- ensureColumn("foods", "report_id", "BIGINT DEFAULT NULL COMMENT '关联报告ID(从PDF解析的食物推荐)'");
- ensureColumn("foods", "score", "INT DEFAULT NULL COMMENT '食物评分'");
- // 迁移263: foods 补齐 energy_kj/starch/cholesterol/diet_type 列(前三列原迁移在文件重组时丢失、生产靠历史执行残留;
- // dietType 为实体孤立字段从未建列,MyBatis-Plus 全字段 SELECT 报 Unknown column)
- ensureColumn("foods", "energy_kj", "DECIMAL(8,2) DEFAULT NULL COMMENT '能量(kJ/100g)'");
- ensureColumn("foods", "starch", "DECIMAL(8,2) DEFAULT NULL COMMENT '淀粉(g/100g)'");
- ensureColumn("foods", "cholesterol", "DECIMAL(8,2) DEFAULT NULL COMMENT '胆固醇(mg/100g)'");
- ensureColumn("foods", "diet_type", "VARCHAR(50) DEFAULT NULL COMMENT '饮食类型'");
- // 迁移192: verification_codes.type 添加默认值(旧版插入未携带 type 字段,DB 列 NOT NULL 无默认值导致报错)
- try {
- jdbcTemplate.execute("ALTER TABLE verification_codes MODIFY COLUMN `type` VARCHAR(20) NOT NULL DEFAULT 'login' COMMENT '验证码类型: login/register/reset'");
- log.info("已为verification_codes.type添加默认值");
- } catch (Exception e) {
- log.warn("verification_codes.type 默认值修改失败(可能已修改): " + e.getMessage());
- }
- // 迁移193: family_relationships表添加关系质量评分三列(关系条展示增强)
- ensureColumn("family_relationships", "trust_score", "DECIMAL(5,2) DEFAULT 0 COMMENT '信任度(答卷人视角)'");
- ensureColumn("family_relationships", "intimacy_score", "DECIMAL(5,2) DEFAULT 0 COMMENT '亲密度(答卷人视角)'");
- ensureColumn("family_relationships", "communication_score", "DECIMAL(5,2) DEFAULT 0 COMMENT '沟通质量(答卷人视角)'");
- // 迁移194: family_join_requests 添加 family_role 列(保存申请人角色)
- ensureColumn("family_join_requests", "family_role", "VARCHAR(20) DEFAULT NULL COMMENT '申请人在家庭中的角色:爸爸/妈妈/爷爷/奶奶等'");
- // 迁移195: tasks 表新增 accept_deadline 列 + 扩展 status 枚举(支持 assigned/in_progress/rejected)
- ensureColumn("tasks", "accept_deadline", "DATETIME DEFAULT NULL COMMENT '接受截止时间(分配给其他家庭成员的任务)'");
- try {
- jdbcTemplate.execute("ALTER TABLE tasks MODIFY COLUMN `status` ENUM('pending','assigned','in_progress','completed','rejected','overdue','cancelled') DEFAULT 'pending' COMMENT '状态: pending=待审核 assigned=待接受 in_progress=进行中 completed=已完成 rejected=已拒绝 overdue=已过期 cancelled=已取消'");
- log.info("已更新tasks.status枚举值");
- } catch (Exception e) {
- log.warn("tasks.status枚举修改失败(可能已修改): " + e.getMessage());
- }
- // 迁移196: 创建 user_platform_balance 表(CF值余额)
- try {
- jdbcTemplate.execute("CREATE TABLE IF NOT EXISTS user_platform_balance (" +
- "id BIGINT AUTO_INCREMENT PRIMARY KEY, " +
- "user_id BIGINT NOT NULL COMMENT '用户ID', " +
- "family_id BIGINT COMMENT '归属家庭ID', " +
- "total_earned INT DEFAULT 0 COMMENT '累计获得', " +
- "available INT DEFAULT 0 COMMENT '可用余额', " +
- "frozen INT DEFAULT 0 COMMENT '冻结(提现审核中)', " +
- "withdrawn INT DEFAULT 0 COMMENT '已提现', " +
- "exchanged INT DEFAULT 0 COMMENT '已兑换', " +
- "updated_at DATETIME, " +
- "created_at DATETIME, " +
- "UNIQUE KEY uk_user (user_id)" +
- ") ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='用户CF值余额'");
- log.info("已创建user_platform_balance表");
- } catch (Exception e) {
- log.warn("创建user_platform_balance表失败: " + e.getMessage());
- }
- // 迁移197: 创建 platform_balance_log 表(CF值流水)
- try {
- jdbcTemplate.execute("CREATE TABLE IF NOT EXISTS platform_balance_log (" +
- "id BIGINT AUTO_INCREMENT PRIMARY KEY, " +
- "user_id BIGINT NOT NULL COMMENT '用户ID', " +
- "family_id BIGINT COMMENT '归属家庭ID', " +
- "type VARCHAR(20) NOT NULL COMMENT 'earn/spend/freeze/unfreeze/withdraw', " +
- "amount INT NOT NULL COMMENT '变动数量', " +
- "balance_after INT NOT NULL COMMENT '变动后可用余额', " +
- "ref_type VARCHAR(30) COMMENT 'product_order/activity/withdrawal/migration', " +
- "ref_id BIGINT COMMENT '关联业务ID', " +
- "remark VARCHAR(255) COMMENT '备注', " +
- "created_at DATETIME, " +
- "INDEX idx_user_time (user_id, created_at)" +
- ") ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='CF值流水'");
- log.info("已创建platform_balance_log表");
- } catch (Exception e) {
- log.warn("创建platform_balance_log表失败: " + e.getMessage());
- }
- // 迁移198: 存量佣金迁移为CF值(settled→available, pending→frozen)
- try {
- jdbcTemplate.execute("INSERT INTO user_platform_balance (user_id, total_earned, available, created_at, updated_at) " +
- "SELECT referrer_id, SUM(commission_amount), SUM(commission_amount), NOW(), NOW() " +
- "FROM commission_records WHERE status='settled' GROUP BY referrer_id " +
- "ON DUPLICATE KEY UPDATE available=available+VALUES(available), " +
- "total_earned=total_earned+VALUES(total_earned), updated_at=NOW()");
- log.info("已迁移settled佣金为可用CF值");
- } catch (Exception e) {
- log.warn("迁移settled佣金失败: " + e.getMessage());
- }
- // 迁移199: pending 佣金 → 冻结CF
- try {
- jdbcTemplate.execute("INSERT INTO user_platform_balance (user_id, total_earned, frozen, created_at, updated_at) " +
- "SELECT referrer_id, SUM(commission_amount), SUM(commission_amount), NOW(), NOW() " +
- "FROM commission_records WHERE status='pending' GROUP BY referrer_id " +
- "ON DUPLICATE KEY UPDATE frozen=frozen+VALUES(frozen), " +
- "total_earned=total_earned+VALUES(total_earned), updated_at=NOW()");
- log.info("已迁移pending佣金为冻结CF值");
- } catch (Exception e) {
- log.warn("迁移pending佣金失败: " + e.getMessage());
- }
- // 迁移200: activities表添加platform_points字段
- ensureColumn("activities", "platform_points", "INT DEFAULT 0 COMMENT '活动奖励CF值(0=不发)'");
- // 迁移201: 新增CF值配置项
- insertSysConfigSeed("product_platform_points_share", "1000", "商品CF值利润分成比例(bps)");
- insertSysConfigSeed("platform_withdraw_rate", "100", "CF值提现汇率(CF数/元)");
- // 迁移202: withdrawal_requests表添加 type 字段(提现来源)
- ensureColumn("withdrawal_requests", "type", "VARCHAR(20) DEFAULT 'platform_points' COMMENT '提现来源: platform_points(CF值) / commission(历史佣金)'");
- // 迁移203: membership_levels 添加原价字段(LIFETIME 需求:价格来源统一迁移至表内)
- ensureColumn("membership_levels", "original_price_monthly", "INT NULL COMMENT '原价-月费(分)'");
- ensureColumn("membership_levels", "original_price_quarterly", "INT NULL COMMENT '原价-季费(分)'");
- ensureColumn("membership_levels", "original_price_yearly", "INT NULL COMMENT '原价-年费(分)'");
- // 迁移203.1: 幂等数据同步(sys_config 原价/现值 → membership_levels,仅当表内为 0/NULL 时)
- try {
- // FAMILY 原价年费(sys_config member_fee_family_original_yearly / member_fee_family_original,默认 199900)
- jdbcTemplate.update("UPDATE membership_levels SET original_price_yearly = 199900 " +
- "WHERE level_code = 'FAMILY' AND (original_price_yearly IS NULL OR original_price_yearly = 0)");
- // PREMIUM 原价年费/月费(sys_config member_fee_premium_original / member_fee_premium_original_monthly)
- jdbcTemplate.update("UPDATE membership_levels SET original_price_yearly = 1599900, original_price_monthly = 159900 " +
- "WHERE level_code = 'PREMIUM' AND (original_price_yearly IS NULL OR original_price_yearly = 0)");
- // FAMILY 现价补齐(现值 13140/32850/131400)
- jdbcTemplate.update("UPDATE membership_levels SET price_monthly = 13140, price_quarterly = 32850, price_yearly = 131400 " +
- "WHERE level_code = 'FAMILY' AND (price_yearly IS NULL OR price_yearly = 0)");
- // PREMIUM 现价补齐(现值 131400/1316800;季费保持 NULL=不支持)
- jdbcTemplate.update("UPDATE membership_levels SET price_monthly = 131400, price_yearly = 1316800 " +
- "WHERE level_code = 'PREMIUM' AND (price_yearly IS NULL OR price_yearly = 0)");
- log.info("已同步会员价格/原价至membership_levels");
- } catch (Exception e) {
- log.warn("同步membership_levels价格失败: {}", e.getMessage());
- }
- // 迁移204: 插入 LIFETIME 终身会员等级行(不可购买,仅管理员授予,价格全 0)
- try {
- jdbcTemplate.execute("INSERT IGNORE INTO membership_levels " +
- "(level_code, level_name, level_desc, price_monthly, price_quarterly, price_yearly, " +
- "original_price_monthly, original_price_quarterly, original_price_yearly, " +
- "features, max_children, max_tasks_per_day, ai_review_enabled, priority_support, teacher_consultation) VALUES " +
- "('LIFETIME', '终身会员', '终身家庭会员权益,永久有效无需续费', 0, 0, 0, 0, 0, 0, " +
- "'[\"free_activities\",\"free_courses\",\"full_price_purchase\",\"referral_commission\"," +
- "\"discount_purchase\",\"member_activities\",\"create_family\",\"invite_family\",\"purchase_commission\"]', " +
- "6, 20, 1, 1, 1)");
- log.info("已插入LIFETIME等级种子数据");
- } catch (Exception e) {
- log.warn("插入LIFETIME等级种子数据失败: {}", e.getMessage());
- }
- // 迁移205: 创建平台公告表
- try {
- jdbcTemplate.execute(
- "CREATE TABLE IF NOT EXISTS notices (" +
- "id BIGINT AUTO_INCREMENT PRIMARY KEY, " +
- "title VARCHAR(200) NOT NULL COMMENT '公告标题', " +
- "content TEXT NOT NULL COMMENT '公告内容', " +
- "type VARCHAR(20) DEFAULT 'platform' COMMENT '类型: platform=平台公告 activity=活动 promotion=优惠', " +
- "status VARCHAR(20) DEFAULT 'active' COMMENT '状态: draft=草稿 active=已发布 deleted=已删除', " +
- "admin_id BIGINT DEFAULT NULL COMMENT '发布管理员ID', " +
- "created_at DATETIME DEFAULT CURRENT_TIMESTAMP, " +
- "updated_at DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP" +
- ") ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='平台公告'"
- );
- log.info("已创建notices表");
- } catch (Exception e) {
- log.warn("创建notices表失败: " + e.getMessage());
- }
- // 迁移206: member_upgrade_records 添加审批字段(LIFETIME 授予审批需求,修复升级记录查询 Unknown column 500)
- ensureColumn("member_upgrade_records", "approve_remark", "VARCHAR(500) DEFAULT NULL COMMENT '审批备注'");
- ensureColumn("member_upgrade_records", "approve_status", "VARCHAR(20) DEFAULT 'approved' COMMENT '审批状态: approved/rejected'");
- ensureColumn("member_upgrade_records", "approve_user_id", "BIGINT DEFAULT NULL COMMENT '审批人ID'");
- ensureColumn("member_upgrade_records", "approve_time", "DATETIME DEFAULT NULL COMMENT '审批时间'");
- // 迁移207: 创建 health_plans 表(健康方案汇总记录)
- try {
- jdbcTemplate.execute(
- "CREATE TABLE IF NOT EXISTS health_plans (" +
- "id BIGINT AUTO_INCREMENT PRIMARY KEY, " +
- "family_id BIGINT NOT NULL COMMENT '家庭ID', " +
- "member_ids VARCHAR(200) NOT NULL COMMENT '目标成员ID列表(逗号分隔)', " +
- "dimensions VARCHAR(100) NOT NULL COMMENT '维度列表(逗号分隔)', " +
- "goal TEXT NOT NULL COMMENT '用户输入的目标', " +
- "plan_content TEXT COMMENT 'LangGraph生成的方案内容', " +
- "member_name VARCHAR(50) COMMENT '成员名称摘要', " +
- "created_at DATETIME DEFAULT CURRENT_TIMESTAMP, " +
- "INDEX idx_family (family_id), " +
- "INDEX idx_created (created_at)" +
- ") ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='健康方案汇总记录'"
- );
- log.info("已创建health_plans表");
- } catch (Exception e) {
- log.warn("创建health_plans表失败(可能已存在): " + e.getMessage());
- }
- // 迁移208: health_gut_flora.detection_rate 列扩容(VARCHAR(50)→VARCHAR(255),源数据超长导致 MysqlDataTruncation)
- try {
- jdbcTemplate.execute("ALTER TABLE health_gut_flora MODIFY COLUMN detection_rate VARCHAR(255) DEFAULT NULL COMMENT '检出率'");
- log.info("health_gut_flora.detection_rate 已扩容为 VARCHAR(255)");
- } catch (Exception e) {
- log.warn("health_gut_flora.detection_rate 扩容失败(可能已修改): " + e.getMessage());
- }
- // 迁移225: articles 添加 growth_category 列(成长分类筛选,Article 实体已含字段,生产库缺列导致 Unknown column 'growth_category')
- ensureColumn("articles", "growth_category", "VARCHAR(50) DEFAULT NULL COMMENT '成长分类'");
- // 迁移209: 将 ecom_supplier 合并至 supply_system(商品供应商管理 → 供应商体系管理统一)
- // 1. 给 supply_system 加联系人字段
- ensureColumn("supply_system", "contact_name", "VARCHAR(50) DEFAULT NULL COMMENT '联系人'");
- ensureColumn("supply_system", "contact_phone", "VARCHAR(20) DEFAULT NULL COMMENT '联系电话'");
- ensureColumn("supply_system", "remark", "VARCHAR(500) DEFAULT NULL COMMENT '备注'");
- log.info("迁移209: supply_system 已添加 contact_name/contact_phone/remark 列");
- // 2. 将 ecom_supplier 存量数据迁移到 supply_system(以平台管理员 id=1 为默认 admin)
- try {
- jdbcTemplate.update(
- "INSERT INTO supply_system (name, admin_id, settlement_period_days, platform_profit_rate, status, " +
- "contact_name, contact_phone, remark, created_at, updated_at) " +
- "SELECT name, 1, 30, 0, " +
- "CASE WHEN status = 1 THEN 'active' ELSE 'disabled' END, " +
- "contact_name, contact_phone, remark, created_at, COALESCE(updated_at, created_at) " +
- "FROM ecom_supplier WHERE NOT EXISTS (" +
- " SELECT 1 FROM supply_system ss WHERE ss.name = ecom_supplier.name LIMIT 1)"
- );
- log.info("迁移209: ecom_supplier 存量数据已迁移至 supply_system");
- } catch (Exception e) {
- log.warn("迁移209: ecom_supplier 迁移失败(可能已存在): {}", e.getMessage());
- }
- }
- private void runFullRegionDataMigrations() {
- // 迁移226: 创建 family_platform_balance 表 + family_platform_balance_log 表 + products.points_multiplier 列
- try {
- jdbcTemplate.execute(
- "CREATE TABLE IF NOT EXISTS family_platform_balance (" +
- " id BIGINT AUTO_INCREMENT PRIMARY KEY, " +
- " family_id BIGINT NOT NULL COMMENT '家庭ID', " +
- " total_earned INT DEFAULT 0 COMMENT '累计获得', " +
- " available INT DEFAULT 0 COMMENT '可用余额', " +
- " frozen INT DEFAULT 0 COMMENT '冻结(提现审核中)', " +
- " withdrawn INT DEFAULT 0 COMMENT '已提现', " +
- " exchanged INT DEFAULT 0 COMMENT '已兑换', " +
- " created_at DATETIME DEFAULT CURRENT_TIMESTAMP, " +
- " updated_at DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, " +
- " UNIQUE KEY uk_family (family_id)" +
- ") ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='家庭平台积分池余额'"
- );
- log.info("已创建family_platform_balance表");
- } catch (Exception e) {
- log.warn("创建family_platform_balance表失败: " + e.getMessage());
- }
- try {
- jdbcTemplate.execute(
- "CREATE TABLE IF NOT EXISTS family_platform_balance_log (" +
- " id BIGINT AUTO_INCREMENT PRIMARY KEY, " +
- " family_id BIGINT NOT NULL COMMENT '家庭ID', " +
- " type VARCHAR(20) NOT NULL COMMENT 'earn/spend/freeze/unfreeze/withdraw/adjust/migration', " +
- " amount INT NOT NULL COMMENT '变动数量', " +
- " balance_after INT NOT NULL COMMENT '变动后可用余额', " +
- " ref_type VARCHAR(50) COMMENT 'product_order/referral_dist/activity/withdrawal/adjust/migration/refund', " +
- " ref_id BIGINT COMMENT '关联业务ID', " +
- " remark VARCHAR(255) COMMENT '备注', " +
- " created_at DATETIME DEFAULT CURRENT_TIMESTAMP, " +
- " INDEX idx_family (family_id), " +
- " INDEX idx_ref (ref_type, ref_id)" +
- ") ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='家庭平台积分流水'"
- );
- log.info("已创建family_platform_balance_log表");
- } catch (Exception e) {
- log.warn("创建family_platform_balance_log表失败: " + e.getMessage());
- }
- ensureColumn("products", "points_multiplier", "DECIMAL(10,2) NOT NULL DEFAULT 1.00 COMMENT '平台积分倍数,1元=1积分×倍数'");
- log.info("已添加products.points_multiplier列");
- // 迁移227: 创建 family_platform_exchange_record 表(优惠券CF兑换记录;券挂家庭,CF扣兑换人个人账本)
- try {
- jdbcTemplate.execute(
- "CREATE TABLE IF NOT EXISTS family_platform_exchange_record (" +
- " id BIGINT AUTO_INCREMENT PRIMARY KEY, " +
- " user_id BIGINT NOT NULL COMMENT '操作用户ID', " +
- " family_id BIGINT NOT NULL COMMENT '家庭ID', " +
- " coupon_id BIGINT NOT NULL COMMENT '关联优惠券ID', " +
- " coupon_name VARCHAR(200) COMMENT '优惠券名称', " +
- " cf_cost INT NOT NULL COMMENT '消耗CF值数量', " +
- " status VARCHAR(20) NOT NULL DEFAULT 'completed' COMMENT 'completed/failed', " +
- " redeem_code VARCHAR(32) COMMENT '兑换码', " +
- " remark VARCHAR(255) COMMENT '备注', " +
- " created_at DATETIME DEFAULT CURRENT_TIMESTAMP, " +
- " updated_at DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, " +
- " INDEX idx_family_time (family_id, created_at), " +
- " INDEX idx_user (user_id)" +
- ") ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='优惠券CF兑换记录(券挂家庭,CF扣兑换人个人账本cf_balance)'"
- );
- log.info("已创建family_platform_exchange_record表");
- } catch (Exception e) {
- log.warn("创建family_platform_exchange_record表失败: " + e.getMessage());
- }
- // 迁移228: nutrition_product 添加 ingredients 列 + suitable_for 升级为 TEXT(存放详情页图像提取的成份/配料表与适用说明)
- ensureColumn("nutrition_product", "ingredients", "TEXT DEFAULT NULL COMMENT '成份/配料表(从详情页图片提取)'");
- try {
- jdbcTemplate.execute("ALTER TABLE nutrition_product MODIFY COLUMN suitable_for TEXT DEFAULT NULL COMMENT '适用人群/使用方法'");
- log.info("已将nutrition_product.suitable_for升级为TEXT类型");
- } catch (Exception e) {
- // 修改失败,忽略错误(幂等)
- }
- // 迁移222: 创建 file_tag 表(文件标签字典)
- try {
- jdbcTemplate.execute(
- "CREATE TABLE IF NOT EXISTS file_tag (" +
- "id BIGINT AUTO_INCREMENT PRIMARY KEY, " +
- "name VARCHAR(50) NOT NULL COMMENT '标签名称', " +
- "color VARCHAR(7) DEFAULT '#409EFF' COMMENT '标签颜色', " +
- "sort_order INT DEFAULT 0 COMMENT '排序号', " +
- "created_at DATETIME DEFAULT CURRENT_TIMESTAMP, " +
- "UNIQUE KEY uk_name (name)" +
- ") ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='文件标签字典'"
- );
- log.info("已创建file_tag表");
- } catch (Exception e) {
- log.warn("创建file_tag表失败: " + e.getMessage());
- }
- String[][] presetTags = {
- {"测评报告", "#409EFF"}, {"健康报告", "#67C23A"}, {"文章配图", "#E6A23C"},
- {"成长档案", "#909399"}, {"身", "#FF8C42"}, {"智", "#6366F1"},
- {"富", "#F59E0B"}, {"行", "#10B981"}, {"心", "#FF6B9D"}, {"重要", "#F56C6C"}
- };
- for (int i = 0; i < presetTags.length; i++) {
- try {
- jdbcTemplate.update("INSERT IGNORE INTO file_tag (name, color, sort_order) VALUES (?, ?, ?)",
- presetTags[i][0], presetTags[i][1], i);
- } catch (Exception e) { /* 忽略重复 */ }
- }
- log.info("已预置文件标签");
- // 迁移223: 创建 file_tag_rel 表
- try {
- jdbcTemplate.execute(
- "CREATE TABLE IF NOT EXISTS file_tag_rel (" +
- "id BIGINT AUTO_INCREMENT PRIMARY KEY, " +
- "file_id BIGINT NOT NULL COMMENT 'file_record.id', " +
- "tag_id BIGINT NOT NULL COMMENT 'file_tag.id', " +
- "UNIQUE KEY uk_file_tag (file_id, tag_id), " +
- "INDEX idx_tag (tag_id)" +
- ") ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='文件-标签关联'"
- );
- log.info("已创建file_tag_rel表");
- } catch (Exception e) {
- log.warn("创建file_tag_rel表失败: " + e.getMessage());
- }
- // 迁移229: 新增GROWTH_COACH权益(7X24小时成长教练)到FAMILY/PREMIUM,及mascot_ai功能开关
- try {
- jdbcTemplate.execute("INSERT IGNORE INTO membership_benefits (level_code, benefit_code, benefit_name, benefit_desc, icon, bg_color, sort_order) VALUES " +
- "('FAMILY', 'GROWTH_COACH', '7X24小时成长教练', 'AI智能教练随时解答家庭教育问题', '🎯', '#FEF3C7', 10), " +
- "('PREMIUM', 'GROWTH_COACH', '7X24小时成长教练', 'AI智能教练随时解答家庭教育问题 + 优先响应', '🎯', '#FEF3C7', 10)");
- log.info("已插入GROWTH_COACH权益种子数据");
- } catch (Exception ex) {
- log.warn("插入GROWTH_COACH权益种子数据失败: {}", ex.getMessage());
- }
- // 迁移232: recommendation_logs 追加 recommend_type 字段
- ensureColumn("recommendation_logs", "recommend_type", "VARCHAR(20) DEFAULT 'tag' COMMENT '推荐方式: profile/dimension/tag'");
- log.info("已添加recommendation_logs.recommend_type列");
- // 迁移233: 将 Article/Activity 现有 dimension_weights JSON 同步到 dimension_weights 表
- // (修正:源表名 article→articles、activity→activities;activity与article同为五维JSON对象按维度键展开;
- // dimension_weights表无唯一键,INSERT IGNORE不去重,用sys_config一次性标记+自连接去重兜底)
- try {
- Integer synced = jdbcTemplate.queryForObject(
- "SELECT COUNT(*) FROM sys_config WHERE config_key = 'dimension_weights_legacy_sync_done'",
- Integer.class);
- if (synced == null || synced == 0) {
- jdbcTemplate.execute(
- "INSERT IGNORE INTO dimension_weights (target_type, target_id, dimension, weight, enabled) " +
- "SELECT 'article', a.id, 'body', COALESCE(CAST(JSON_EXTRACT(a.dimension_weights, '$.body') AS SIGNED), 0), 1 " +
- "FROM articles a WHERE a.dimension_weights IS NOT NULL AND a.dimension_weights != '' AND JSON_EXTRACT(a.dimension_weights, '$.body') IS NOT NULL " +
- "UNION ALL " +
- "SELECT 'article', a.id, 'mind', COALESCE(CAST(JSON_EXTRACT(a.dimension_weights, '$.mind') AS SIGNED), 0), 1 " +
- "FROM articles a WHERE a.dimension_weights IS NOT NULL AND a.dimension_weights != '' AND JSON_EXTRACT(a.dimension_weights, '$.mind') IS NOT NULL " +
- "UNION ALL " +
- "SELECT 'article', a.id, 'wisdom', COALESCE(CAST(JSON_EXTRACT(a.dimension_weights, '$.wisdom') AS SIGNED), 0), 1 " +
- "FROM articles a WHERE a.dimension_weights IS NOT NULL AND a.dimension_weights != '' AND JSON_EXTRACT(a.dimension_weights, '$.wisdom') IS NOT NULL " +
- "UNION ALL " +
- "SELECT 'article', a.id, 'action', COALESCE(CAST(JSON_EXTRACT(a.dimension_weights, '$.action') AS SIGNED), 0), 1 " +
- "FROM articles a WHERE a.dimension_weights IS NOT NULL AND a.dimension_weights != '' AND JSON_EXTRACT(a.dimension_weights, '$.action') IS NOT NULL " +
- "UNION ALL " +
- "SELECT 'article', a.id, 'wealth', COALESCE(CAST(JSON_EXTRACT(a.dimension_weights, '$.wealth') AS SIGNED), 0), 1 " +
- "FROM articles a WHERE a.dimension_weights IS NOT NULL AND a.dimension_weights != '' AND JSON_EXTRACT(a.dimension_weights, '$.wealth') IS NOT NULL"
- );
- jdbcTemplate.execute(
- "INSERT IGNORE INTO dimension_weights (target_type, target_id, dimension, weight, enabled) " +
- "SELECT 'activity', act.id, 'body', COALESCE(CAST(JSON_EXTRACT(act.dimension_weights, '$.body') AS SIGNED), 0), 1 " +
- "FROM activities act WHERE act.dimension_weights IS NOT NULL AND act.dimension_weights != '' AND JSON_EXTRACT(act.dimension_weights, '$.body') IS NOT NULL " +
- "UNION ALL " +
- "SELECT 'activity', act.id, 'mind', COALESCE(CAST(JSON_EXTRACT(act.dimension_weights, '$.mind') AS SIGNED), 0), 1 " +
- "FROM activities act WHERE act.dimension_weights IS NOT NULL AND act.dimension_weights != '' AND JSON_EXTRACT(act.dimension_weights, '$.mind') IS NOT NULL " +
- "UNION ALL " +
- "SELECT 'activity', act.id, 'wisdom', COALESCE(CAST(JSON_EXTRACT(act.dimension_weights, '$.wisdom') AS SIGNED), 0), 1 " +
- "FROM activities act WHERE act.dimension_weights IS NOT NULL AND act.dimension_weights != '' AND JSON_EXTRACT(act.dimension_weights, '$.wisdom') IS NOT NULL " +
- "UNION ALL " +
- "SELECT 'activity', act.id, 'action', COALESCE(CAST(JSON_EXTRACT(act.dimension_weights, '$.action') AS SIGNED), 0), 1 " +
- "FROM activities act WHERE act.dimension_weights IS NOT NULL AND act.dimension_weights != '' AND JSON_EXTRACT(act.dimension_weights, '$.action') IS NOT NULL " +
- "UNION ALL " +
- "SELECT 'activity', act.id, 'wealth', COALESCE(CAST(JSON_EXTRACT(act.dimension_weights, '$.wealth') AS SIGNED), 0), 1 " +
- "FROM activities act WHERE act.dimension_weights IS NOT NULL AND act.dimension_weights != '' AND JSON_EXTRACT(act.dimension_weights, '$.wealth') IS NOT NULL"
- );
- jdbcTemplate.execute(
- "DELETE dw1 FROM dimension_weights dw1 INNER JOIN dimension_weights dw2 ON " +
- "dw1.target_type = dw2.target_type AND dw1.target_id = dw2.target_id AND dw1.dimension = dw2.dimension " +
- "AND dw1.id > dw2.id"
- );
- jdbcTemplate.update("INSERT INTO sys_config (config_key, config_value, description) VALUES ('dimension_weights_legacy_sync_done', '1', 'Article/Activity五维权重已同步到dimension_weights表')");
- log.info("已完成Article/Activity维度权重到dimension_weights表的一次性迁移");
- } else {
- log.info("迁移233: 维度权重已同步过,跳过");
- }
- } catch (Exception ex) {
- log.warn("Article/Activity维度权重同步失败: {}", ex.getMessage());
- }
- // 迁移234: 创建 problem_domains 表(问题域定义,改版v1:需求驱动·逐步解锁)
- try {
- jdbcTemplate.execute(
- "CREATE TABLE IF NOT EXISTS problem_domains (" +
- "id BIGINT AUTO_INCREMENT PRIMARY KEY, " +
- "code VARCHAR(50) NOT NULL COMMENT '唯一编码: weight_loss/sleep/constipation', " +
- "name VARCHAR(50) NOT NULL COMMENT '名称: 减肥/睡眠/便秘', " +
- "icon VARCHAR(255) DEFAULT '' COMMENT '图标', " +
- "description VARCHAR(500) DEFAULT '' COMMENT '一句话描述', " +
- "flow_intro_json TEXT COMMENT '服务流程介绍(7步) JSON', " +
- "survey_template_id BIGINT DEFAULT NULL COMMENT '专项调研问卷模板ID(SurveyTemplate.id)', " +
- "content_tags VARCHAR(500) DEFAULT '' COMMENT '内容标签,逗号分隔(过滤文章/知识库)', " +
- "recommended_product_ids VARCHAR(500) DEFAULT '' COMMENT '推荐产品ID,逗号分隔(Product.id)', " +
- "task_template_package_ids VARCHAR(500) DEFAULT '' COMMENT '套餐绑定的任务模板包ID(TaskTemplatePackage.id)', " +
- "reward_cf_value INT DEFAULT 0 COMMENT '全部完成奖励CF值', " +
- "status TINYINT DEFAULT 1 COMMENT '1启用 0停用', " +
- "sort_order INT DEFAULT 0, " +
- "created_at DATETIME DEFAULT CURRENT_TIMESTAMP, " +
- "updated_at DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, " +
- "UNIQUE KEY uk_code (code)" +
- ") ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='问题域定义(改版v1)'"
- );
- log.info("已创建problem_domains表");
- } catch (Exception e) {
- log.warn("创建problem_domains表失败: " + e.getMessage());
- }
- // 迁移235: 创建 user_intents 表(用户意图与服务旅程)
- try {
- jdbcTemplate.execute(
- "CREATE TABLE IF NOT EXISTS user_intents (" +
- "id BIGINT AUTO_INCREMENT PRIMARY KEY, " +
- "user_id BIGINT NOT NULL COMMENT '用户ID', " +
- "intent_type VARCHAR(20) DEFAULT '' COMMENT 'A/B/C 用户类型', " +
- "problem_domain_code VARCHAR(50) DEFAULT '' COMMENT '问题域编码(仅C类)', " +
- "journey_stage INT DEFAULT 0 COMMENT '旅程阶段 0-7', " +
- "stage_updated_at DATETIME DEFAULT NULL COMMENT '最近阶段变化时间', " +
- "survey_template_id BIGINT DEFAULT NULL COMMENT '最近作答问卷模板ID', " +
- "survey_submitted_at DATETIME DEFAULT NULL COMMENT '最近问卷提交时间', " +
- "survey_result_json TEXT COMMENT '问卷答案→推荐结果快照', " +
- "created_at DATETIME DEFAULT CURRENT_TIMESTAMP, " +
- "updated_at DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, " +
- "UNIQUE KEY uk_user (user_id)" +
- ") ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='用户意图与服务旅程(改版v1)'"
- );
- log.info("已创建user_intents表");
- } catch (Exception e) {
- log.warn("创建user_intents表失败: " + e.getMessage());
- }
- // 迁移236: 创建 user_notifications 表(用户级站内主动通知,与notices平台公告并存)
- try {
- jdbcTemplate.execute(
- "CREATE TABLE IF NOT EXISTS user_notifications (" +
- "id BIGINT AUTO_INCREMENT PRIMARY KEY, " +
- "user_id BIGINT NOT NULL COMMENT '接收用户ID', " +
- "type VARCHAR(30) DEFAULT 'report_ready' COMMENT 'report_ready/reward_granted/logistics/system', " +
- "title VARCHAR(200) NOT NULL, " +
- "content VARCHAR(1000) DEFAULT '', " +
- "ref_type VARCHAR(30) DEFAULT '' COMMENT '关联对象类型: report/package/task', " +
- "ref_id BIGINT DEFAULT NULL COMMENT '关联对象ID', " +
- "is_read TINYINT DEFAULT 0, " +
- "created_at DATETIME DEFAULT CURRENT_TIMESTAMP, " +
- "INDEX idx_user_read (user_id, is_read)" +
- ") ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='站内通知(改版v1)'"
- );
- log.info("已创建user_notifications表");
- } catch (Exception e) {
- log.warn("创建user_notifications表失败: " + e.getMessage());
- }
- // 迁移237: tasks 表添加 member_only 字段(1=会员任务,仅方案/套餐生成)
- ensureColumn("tasks", "member_only", "TINYINT DEFAULT 0 COMMENT '1=会员任务(方案/套餐生成),执行需会员; 0=普通任务可免费执行'");
- // 迁移238: package_orders 表添加物流状态字段(试剂盒寄送/样本寄回)
- ensureColumn("package_orders", "logistics_status", "VARCHAR(20) DEFAULT 'none' COMMENT 'none/kit_shipped/kit_received/sample_shipped/sample_received'");
- ensureColumn("package_orders", "tracking_no", "VARCHAR(100) DEFAULT '' COMMENT '快递单号'");
- log.info("已添加package_orders物流字段");
- // 迁移239: 创建 health_status 表(当前状态调研-健康现状档案,SS-2026-08-13)
- try {
- jdbcTemplate.execute(
- "CREATE TABLE IF NOT EXISTS health_status (" +
- "id BIGINT AUTO_INCREMENT PRIMARY KEY, " +
- "user_id BIGINT NOT NULL COMMENT '所属账号(一个账号一份)', " +
- "height_cm DECIMAL(5,1) DEFAULT NULL COMMENT '身高(cm)', " +
- "weight_kg DECIMAL(5,1) DEFAULT NULL COMMENT '体重(kg)', " +
- "blood_pressure VARCHAR(20) DEFAULT NULL COMMENT '血压,如 120/80', " +
- "blood_glucose DECIMAL(4,1) DEFAULT NULL COMMENT '空腹血糖(mmol/L)', " +
- "blood_lipids JSON DEFAULT NULL COMMENT '血脂 [{name,value,unit,note}]', " +
- "disease_history JSON DEFAULT NULL COMMENT '疾病史 [{name,note}]', " +
- "medications JSON DEFAULT NULL COMMENT '在服药物/治疗 [{name,note}]', " +
- "notes VARCHAR(500) DEFAULT NULL COMMENT '其他补充说明', " +
- "filled_at DATETIME DEFAULT NULL COMMENT '首次填写时间', " +
- "updated_at DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间', " +
- "UNIQUE KEY uk_user (user_id)" +
- ") ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='当前状态调研-健康现状档案'"
- );
- log.info("已创建health_status表");
- } catch (Exception e) {
- log.warn("创建health_status表失败: " + e.getMessage());
- }
- // 迁移240: 创建 system_prompts 表(系统提示词统一管理,cfc-web后台可维护)
- try {
- jdbcTemplate.execute(
- "CREATE TABLE IF NOT EXISTS system_prompts (" +
- "id BIGINT AUTO_INCREMENT PRIMARY KEY, " +
- "prompt_key VARCHAR(64) NOT NULL COMMENT '提示词键(如 health_coach/analysis/chat)', " +
- "title VARCHAR(100) NOT NULL COMMENT '提示词标题', " +
- "content TEXT NOT NULL COMMENT '提示词内容', " +
- "default_content TEXT COMMENT '出厂默认内容(重置时恢复)', " +
- "version INT NOT NULL DEFAULT 1 COMMENT '版本号', " +
- "status TINYINT NOT NULL DEFAULT 1 COMMENT '1=启用 0=禁用', " +
- "remark VARCHAR(255) COMMENT '备注', " +
- "updated_by VARCHAR(64) COMMENT '更新人', " +
- "created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, " +
- "updated_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, " +
- "UNIQUE KEY uk_prompt_key (prompt_key)" +
- ") ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='系统提示词管理'"
- );
- log.info("已创建system_prompts表");
- } catch (Exception e) {
- log.warn("创建system_prompts表失败: " + e.getMessage());
- }
- // 迁移240b: 插入默认系统提示词(仅当表为空时)
- try {
- jdbcTemplate.execute(
- "INSERT IGNORE INTO system_prompts (prompt_key, title, content, default_content, version, remark) VALUES " +
- "('health_butler', '家庭健康管家', " +
- "'你是一个家庭AI健康管家, 为用户提供专业、温暖的个性化健康建议。\\n你的能力:\\n1. 基于健康知识库(菌属功能、指标解读、营养素、食物)提供科学建议\\n2. 解读健康报告数据, 结合知识库给出改善方案\\n3. 根据菌群检测结果推荐精准营养方案\\n4. 提供可执行的健康微行动\\n回答原则:\\n1. 用中文, 像家庭医生一样亲切但专业\\n2. 引用知识库中的菌属/指标/食物信息作为证据\\n3. 优先使用知识库数据, 其次使用通用健康知识\\n4. 需要产品推荐时使用搜索工具\\n5. 生成 [TASK: {\"title\": \"任务名\", \"dimension\": \"身\", \"points\": 10, \"frequency\": \"每天\"}] 标记创建健康行动任务\\n6. 超出知识库范围的慢性病/急症问题, 建议咨询专业医生\\n7. 不要编造医学建议, 不夸大效果', " +
- "'你是一个家庭AI健康管家, 为用户提供专业、温暖的个性化健康建议。\\n你的能力:\\n1. 基于健康知识库(菌属功能、指标解读、营养素、食物)提供科学建议\\n2. 解读健康报告数据, 结合知识库给出改善方案\\n3. 根据菌群检测结果推荐精准营养方案\\n4. 提供可执行的健康微行动\\n回答原则:\\n1. 用中文, 像家庭医生一样亲切但专业\\n2. 引用知识库中的菌属/指标/食物信息作为证据\\n3. 优先使用知识库数据, 其次使用通用健康知识\\n4. 需要产品推荐时使用搜索工具\\n5. 生成 [TASK: {\"title\": \"任务名\", \"dimension\": \"身\", \"points\": 10, \"frequency\": \"每天\"}] 标记创建健康行动任务\\n6. 超出知识库范围的慢性病/急症问题, 建议咨询专业医生\\n7. 不要编造医学建议, 不夸大效果', " +
- "1, 'AI健康管家对话'), " +
- "('health_coach', '儿童健康教练', " +
- "'你是一个儿童健康教练,喜欢用提问的方式引导家长发现问题和解决方案。\\n核心原则:\\n1. 先问后答:不要直接给答案,用问题引导家长思考\\n2. 用开放式问题了解情况,再给针对性建议\\n3. 语气温暖,像一个耐心的教练在陪伴家长成长\\n你的职责:\\n1. 儿童体质调理(如挑食、瘦弱、易疲劳、反复生病)—— 用提问了解情况\\n2. 科学喂养和营养建议 —— 先问饮食现状再给建议\\n3. 日常保健(睡眠、运动、季节防护)—— 引导家长发现生活中的问题\\n4. 体质辨识基础咨询 —— 通过提问帮助家长理解孩子体质特点\\n5. 健康问题判断 —— 先收集症状信息再建议是否就医\\n回答结构: 提问-分析-建议-跟进', " +
- "'你是一个儿童健康教练,喜欢用提问的方式引导家长发现问题和解决方案。\\n核心原则:\\n1. 先问后答:不要直接给答案,用问题引导家长思考\\n2. 用开放式问题了解情况,再给针对性建议\\n3. 语气温暖,像一个耐心的教练在陪伴家长成长\\n你的职责:\\n1. 儿童体质调理(如挑食、瘦弱、易疲劳、反复生病)—— 用提问了解情况\\n2. 科学喂养和营养建议 —— 先问饮食现状再给建议\\n3. 日常保健(睡眠、运动、季节防护)—— 引导家长发现生活中的问题\\n4. 体质辨识基础咨询 —— 通过提问帮助家长理解孩子体质特点\\n5. 健康问题判断 —— 先收集症状信息再建议是否就医\\n回答结构: 提问-分析-建议-跟进', " +
- "1, '儿童健康咨询对话'), " +
- "('chat_assistant', '儿童成长助手', " +
- "'你是一个儿童成长家庭助手, 回答关于孩子成长、健康、教育的各种问题。', " +
- "'你是一个儿童成长家庭助手, 回答关于孩子成长、健康、教育的各种问题。', " +
- "1, '通用对话助手')"
- );
- log.info("已插入默认系统提示词");
- } catch (Exception e) {
- log.warn("插入默认系统提示词失败: " + e.getMessage());
- }
- // 迁移242: 创建 sys_menu 表(管理后台侧边栏菜单,数据库驱动 + 角色授权)
- try {
- jdbcTemplate.execute(
- "CREATE TABLE IF NOT EXISTS sys_menu (" +
- " id BIGINT AUTO_INCREMENT PRIMARY KEY, " +
- " parent_id BIGINT NOT NULL DEFAULT 0 COMMENT '父菜单ID,0=顶级', " +
- " title VARCHAR(50) COMMENT '目录标题(有子菜单时用,对应 el-submenu)', " +
- " label VARCHAR(50) COMMENT '叶子菜单显示名(对应 el-menu-item)', " +
- " path VARCHAR(200) COMMENT '路由路径(叶子菜单必填,如 /families)', " +
- " icon VARCHAR(100) COMMENT 'Element UI 图标类名,如 el-icon-s-custom', " +
- " perm VARCHAR(100) COMMENT '权限标识,如 family:list / task:* / system:config', " +
- " sort INT DEFAULT 0 COMMENT '排序号,小的在前', " +
- " visible TINYINT DEFAULT 1 COMMENT '是否显示 1=显示 0=隐藏', " +
- " created_at DATETIME DEFAULT CURRENT_TIMESTAMP, " +
- " updated_at DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, " +
- " INDEX idx_parent (parent_id)" +
- ") ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='后台侧边栏菜单表'"
- );
- log.info("已创建sys_menu表");
- } catch (Exception e) {
- log.warn("创建sys_menu表失败: " + e.getMessage());
- }
- // 迁移243: 创建 sys_role_menu 表(角色-菜单授权)
- try {
- jdbcTemplate.execute(
- "CREATE TABLE IF NOT EXISTS sys_role_menu (" +
- " id BIGINT AUTO_INCREMENT PRIMARY KEY, " +
- " role VARCHAR(50) NOT NULL COMMENT '角色标识: admin/teacher/nutritionist/article_manager/activity_manager/supplier_admin', " +
- " menu_id BIGINT NOT NULL COMMENT 'sys_menu.id', " +
- " created_at DATETIME DEFAULT CURRENT_TIMESTAMP, " +
- " UNIQUE KEY uk_role_menu (role, menu_id)" +
- ") ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='角色-菜单授权表'"
- );
- log.info("已创建sys_role_menu表");
- } catch (Exception e) {
- log.warn("创建sys_role_menu表失败: " + e.getMessage());
- }
- // 迁移244: sys_menu 菜单种子数据(对应 Layout.vue 硬编码 menuItems,表为空才插入,幂等)
- try {
- Integer menuCount = jdbcTemplate.queryForObject("SELECT COUNT(*) FROM sys_menu", Integer.class);
- if (menuCount == null || menuCount == 0) {
- seedSysMenus();
- log.info("sys_menu 种子数据已插入");
- } else {
- log.info("sys_menu 已有 {} 条数据,跳过种子插入", menuCount);
- }
- } catch (Exception e) {
- log.warn("初始化sys_menu种子数据失败: " + e.getMessage());
- }
- // 迁移245: 创建 notification_templates 表(通知模板管理 W3)
- try {
- jdbcTemplate.execute("CREATE TABLE IF NOT EXISTS notification_templates (" +
- "id BIGINT AUTO_INCREMENT PRIMARY KEY, " +
- "type VARCHAR(30) NOT NULL COMMENT '通知类型: report_ready/reward_granted/system', " +
- "title VARCHAR(200) NOT NULL COMMENT '默认标题(支持占位符)', " +
- "content VARCHAR(1000) NOT NULL COMMENT '默认内容模板(支持占位符)', " +
- "channels VARCHAR(100) DEFAULT 'in_app' COMMENT '推送通道: in_app,sms,wechat,phone 逗号分隔', " +
- "status TINYINT DEFAULT 1 COMMENT '1启用 0停用', " +
- "created_at DATETIME DEFAULT CURRENT_TIMESTAMP, " +
- "updated_at DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, " +
- "UNIQUE KEY uk_type (type)" +
- ") ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='通知模板(改版v1 W3)'");
- log.info("已创建notification_templates表");
- } catch (Exception e) {
- log.warn("创建notification_templates表可能已存在: {}", e.getMessage());
- }
- // 迁移245b: notification_templates 种子数据(表为空才插入,幂等)
- try {
- Integer tplCount = jdbcTemplate.queryForObject("SELECT COUNT(*) FROM notification_templates", Integer.class);
- if (tplCount == null || tplCount == 0) {
- jdbcTemplate.execute("INSERT INTO notification_templates (type, title, content, channels) VALUES " +
- "('report_ready', '健康报告已完成分析', '您的健康报告已完成解析,可前往查看结果与成长方案。', 'in_app,sms'), " +
- "('reward_granted', '问题域完成奖励已到账', '恭喜完成「{domainName}」全部任务,奖励 {reward} CF 已发放至家庭账户。', 'in_app,sms,wechat'), " +
- "('system', '系统通知', '您有一条新的系统通知,请前往查看。', 'in_app'), " +
- "('energy_change', '能量变动', '{description}获得{amount}能量', 'in_app'), " +
- "('points_change', '积分变动', '{description}变动{amount}积分', 'in_app'), " +
- "('cf_change', 'CF值变动', '{remark}获得{amount} CF值', 'in_app'), " +
- "('procurement_completed', '采购任务已完成', '您的【{comboTier}】套餐采购任务已完成,共{productCount}件商品,下单时间:{createTime},产品即将送达,请及时查收。', 'in_app,wechat')");
- log.info("notification_templates 种子数据已插入");
- } else {
- log.info("notification_templates 已有 {} 条数据,跳过种子插入", tplCount);
- }
- } catch (Exception e) {
- log.warn("初始化notification_templates种子数据失败: " + e.getMessage());
- }
- // 迁移246: product_skus 存量价格 ×100(元→分),sys_config 幂等标记
- try {
- Integer migrated = jdbcTemplate.queryForObject(
- "SELECT COUNT(*) FROM sys_config WHERE config_key = 'sku_price_to_cents_done'",
- Integer.class);
- if (migrated == null || migrated == 0) {
- jdbcTemplate.update("UPDATE product_skus SET price = IFNULL(price, 0) * 100, original_price = IFNULL(original_price, 0) * 100");
- jdbcTemplate.update("INSERT INTO sys_config (config_key, config_value, description) VALUES ('sku_price_to_cents_done', '1', 'SKU价格已迁移为分单位')");
- log.info("迁移246: product_skus 价格已×100(元→分)");
- } else {
- log.info("迁移246: product_skus 价格已迁移过,跳过");
- }
- } catch (Exception e) {
- log.warn("迁移246失败: {}", e.getMessage());
- }
- // 迁移247: product_skus 添加 linked_product_id 和 label 列(SKU关联商品选择器)
- ensureColumn("product_skus", "linked_product_id", "BIGINT COMMENT '关联商品ID,null=Legacy SKU'");
- ensureColumn("product_skus", "label", "VARCHAR(100) COMMENT '自定义显示标签,null=使用linked_product.name'");
- // 迁移248: families 添加 butler_id(B-5 管家行级隔离)
- ensureColumn("families", "butler_id", "BIGINT COMMENT '绑定的管家userId'");
- try {
- jdbcTemplate.execute("CREATE INDEX idx_families_butler_id ON families(butler_id)");
- log.info("已创建families.butler_id索引");
- } catch (Exception e) {
- String msg = e.getMessage();
- if (msg == null || !(msg.contains("Duplicate") || msg.contains("exists"))) {
- log.warn("创建families.butler_id索引失败: {}", msg);
- }
- }
- // 迁移250: 清理测试脏数据(B-7:TEST-Energy-Grant 测试任务、乱码昵称)
- try {
- int deletedTasks = jdbcTemplate.update("DELETE FROM tasks WHERE title LIKE 'TEST-Energy-Grant%'");
- int fixedNicknames = jdbcTemplate.update("UPDATE users SET nickname = '测试家庭' WHERE nickname LIKE '娴嬭瘯%' AND nickname <> '测试家庭'");
- if (deletedTasks > 0 || fixedNicknames > 0) {
- log.info("迁移250: 已清理测试任务 {} 条,修复乱码昵称 {} 个", deletedTasks, fixedNicknames);
- }
- } catch (Exception e) {
- log.warn("迁移250失败: {}", e.getMessage());
- }
- // 迁移252: 创建用户画像表
- try {
- jdbcTemplate.execute("CREATE TABLE IF NOT EXISTS profile_snapshot (" +
- "id BIGINT AUTO_INCREMENT PRIMARY KEY, " +
- "member_id BIGINT NOT NULL UNIQUE, " +
- "dimension_scores JSON DEFAULT NULL, " +
- "body_metrics JSON DEFAULT NULL, " +
- "mind_metrics JSON DEFAULT NULL, " +
- "wisdom_metrics JSON DEFAULT NULL, " +
- "action_metrics JSON DEFAULT NULL, " +
- "wealth_metrics JSON DEFAULT NULL, " +
- "problem_domains VARCHAR(500) DEFAULT NULL, " +
- "computed_at DATETIME DEFAULT NULL, " +
- "updated_at DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, " +
- "INDEX idx_member_id (member_id)" +
- ") ENGINE=InnoDB DEFAULT CHARSET=utf8mb4");
- log.info("已创建profile_snapshot表");
- } catch (Exception e) {
- log.warn("创建profile_snapshot表可能已存在: {}", e.getMessage());
- }
- try {
- jdbcTemplate.execute("CREATE TABLE IF NOT EXISTS profile_history (" +
- "id BIGINT AUTO_INCREMENT PRIMARY KEY, " +
- "member_id BIGINT NOT NULL, " +
- "snapshot_date DATE NOT NULL, " +
- "all_metrics JSON NOT NULL, " +
- "created_at DATETIME DEFAULT CURRENT_TIMESTAMP, " +
- "INDEX idx_member_date (member_id, snapshot_date)" +
- ") ENGINE=InnoDB DEFAULT CHARSET=utf8mb4");
- log.info("已创建profile_history表");
- } catch (Exception e) {
- log.warn("创建profile_history表可能已存在: {}", e.getMessage());
- }
- // 迁移254: 创建 report_template 表(报告展示模板配置)
- try {
- jdbcTemplate.execute("CREATE TABLE IF NOT EXISTS report_template (" +
- "id BIGINT AUTO_INCREMENT PRIMARY KEY, " +
- "name VARCHAR(100) NOT NULL COMMENT '模板名称', " +
- "report_type VARCHAR(50) NOT NULL COMMENT '报告类型: gut_flora/dan/physical_exam/tongue', " +
- "config JSON NOT NULL COMMENT '模板配置JSON: fixedBlocks+freeDisplay', " +
- "is_active TINYINT DEFAULT 1 COMMENT '0=停用 1=启用', " +
- "created_at DATETIME DEFAULT CURRENT_TIMESTAMP, " +
- "updated_at DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, " +
- "UNIQUE KEY uk_report_type (report_type)" +
- ") ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='报告展示模板'");
- log.info("已创建report_template表");
- } catch (Exception e) {
- log.warn("report_template表可能已存在: {}", e.getMessage());
- }
- // 迁移255: report_unknown_upload 添加 annotation 列(管理员标注训练数据)
- ensureColumn("report_unknown_upload", "annotation", "TEXT COMMENT '管理员标注: 手动指定类型+确认提取结果JSON'");
- // 迁移256: social_circle 添加圈子管理字段
- ensureColumn("social_circle", "description", "VARCHAR(500) NULL COMMENT '圈子描述'");
- ensureColumn("social_circle", "cover_image", "VARCHAR(500) NULL COMMENT '封面图URL'");
- ensureColumn("social_circle", "join_condition", "TINYINT(1) DEFAULT 0 COMMENT '加入条件: 0=公开/1=审核/2=邀请'");
- ensureColumn("social_circle", "review_required", "TINYINT(1) DEFAULT 0 COMMENT '是否需要审核: 0=否/1=是'");
- ensureColumn("social_circle", "is_public", "TINYINT(1) DEFAULT 1 COMMENT '是否公开可见: 0=否/1=是'");
- // 迁移257: social_circle_member 添加审核状态和角色字段
- ensureColumn("social_circle_member", "status", "VARCHAR(20) DEFAULT 'approved' COMMENT '申请状态: pending/approved/rejected'");
- ensureColumn("social_circle_member", "role", "VARCHAR(20) DEFAULT 'member' COMMENT '角色: member/admin'");
- // 迁移258: 创建圈子动态帖子表
- try {
- jdbcTemplate.execute("CREATE TABLE IF NOT EXISTS social_circle_post (" +
- "id BIGINT AUTO_INCREMENT PRIMARY KEY, " +
- "circle_id BIGINT NOT NULL COMMENT '圈子ID', " +
- "member_id BIGINT NOT NULL COMMENT '发布者成员ID', " +
- "member_type VARCHAR(20) NOT NULL COMMENT 'parent/child', " +
- "type VARCHAR(50) NOT NULL COMMENT '帖子类型: checkin/diet/exercise/sleep/water/mood/text', " +
- "content TEXT COMMENT '文字内容', " +
- "checkin_id BIGINT COMMENT '关联打卡记录ID', " +
- "image_urls TEXT COMMENT '图片URL列表JSON', " +
- "like_count INT DEFAULT 0 COMMENT '点赞数', " +
- "comment_count INT DEFAULT 0 COMMENT '评论数', " +
- "created_at DATETIME DEFAULT CURRENT_TIMESTAMP, " +
- "deleted TINYINT(1) DEFAULT 0, " +
- "INDEX idx_circle_time (circle_id, created_at), " +
- "INDEX idx_member_time (member_id, created_at)" +
- ") ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='圈子动态帖子'");
- log.info("已创建social_circle_post表");
- } catch (Exception e) {
- log.warn("social_circle_post表可能已存在");
- }
- // 迁移259: 创建圈子点赞表
- try {
- jdbcTemplate.execute("CREATE TABLE IF NOT EXISTS social_circle_like (" +
- "id BIGINT AUTO_INCREMENT PRIMARY KEY, " +
- "circle_id BIGINT NOT NULL, " +
- "post_id BIGINT NOT NULL COMMENT '被点赞的帖子ID', " +
- "member_id BIGINT NOT NULL COMMENT '点赞成员ID', " +
- "member_type VARCHAR(20) NOT NULL, " +
- "created_at DATETIME DEFAULT CURRENT_TIMESTAMP, " +
- "UNIQUE KEY uk_like (circle_id, post_id, member_id)" +
- ") ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='圈子点赞记录'");
- log.info("已创建social_circle_like表");
- } catch (Exception e) {
- log.warn("social_circle_like表可能已存在");
- }
- // 迁移260: 创建圈子评论表
- try {
- jdbcTemplate.execute("CREATE TABLE IF NOT EXISTS social_circle_comment (" +
- "id BIGINT AUTO_INCREMENT PRIMARY KEY, " +
- "circle_id BIGINT NOT NULL, " +
- "post_id BIGINT NOT NULL COMMENT '所属帖子ID', " +
- "parent_id BIGINT DEFAULT NULL COMMENT '回复的评论ID(NULL=一级评论)', " +
- "member_id BIGINT NOT NULL COMMENT '评论者成员ID', " +
- "member_type VARCHAR(20) NOT NULL, " +
- "content TEXT NOT NULL COMMENT '评论内容', " +
- "created_at DATETIME DEFAULT CURRENT_TIMESTAMP, " +
- "INDEX idx_post_time (post_id, created_at)" +
- ") ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='圈子评论记录'");
- log.info("已创建social_circle_comment表");
- } catch (Exception e) {
- log.warn("social_circle_comment表可能已存在");
- }
- // 迁移261: butler_profiles 添加停接新单开关(管家自助选择功能)
- ensureColumn("butler_profiles", "accepting",
- "TINYINT(1) NOT NULL DEFAULT 1 COMMENT '是否接受新家庭绑定: 0=停接/1=接单'");
- // 迁移262: family_join_requests 添加 inviter_user_id 列(审批人=邀请人,谁分享的卡谁确认)
- try {
- jdbcTemplate.execute("ALTER TABLE family_join_requests ADD COLUMN inviter_user_id BIGINT NULL COMMENT '邀请人用户ID(分享邀请令牌的人)' AFTER invite_ref");
- jdbcTemplate.execute("ALTER TABLE family_join_requests ADD INDEX idx_inviter (inviter_user_id)");
- log.info("已添加inviter_user_id列到family_join_requests表");
- } catch (Exception e) {
- // 列/索引已存在,忽略错误
- log.warn("family_join_requests.inviter_user_id可能已存在: {}", e.getMessage());
- }
- // 迁移263: users 表添加 portrait_prompt 列(用户画像 prompt 拼装需求)
- try {
- jdbcTemplate.execute("ALTER TABLE users ADD COLUMN portrait_prompt TEXT COMMENT '用户自定义画像 prompt' AFTER mascot");
- log.info("已添加 portrait_prompt 列到 users 表");
- } catch (Exception e) {
- log.warn("users.portrait_prompt可能已存在: {}", e.getMessage());
- }
- // 迁移264: 方案分解任务补全 family_member_id,修正 deadline 落在次日 00:00 的边界问题
- try {
- jdbcTemplate.update(
- "UPDATE tasks SET family_member_id = child_id " +
- "WHERE source_type = 'plan' AND family_member_id IS NULL AND child_id IS NOT NULL");
- jdbcTemplate.update(
- "UPDATE tasks SET deadline = TIMESTAMP(DATE(deadline)) - INTERVAL 1 SECOND " +
- "WHERE source_type = 'plan' AND TIME(deadline) = '00:00:00'");
- log.info("已修复方案分解任务的 family_member_id 与 deadline 边界");
- } catch (Exception e) {
- log.warn("修复方案分解任务字段失败: {}", e.getMessage());
- }
- // 迁移265: 方案分解任务开启审核,打卡后需规划师/管理员审核通过才发放积分
- try {
- jdbcTemplate.update(
- "UPDATE tasks SET need_review = 1 " +
- "WHERE source_type = 'plan' AND (need_review IS NULL OR need_review = 0) AND status IN ('pending', 'pending_review')");
- log.info("已开启方案分解任务的审核模式");
- } catch (Exception e) {
- log.warn("开启方案分解任务审核失败: {}", e.getMessage());
- }
- // 迁移266: tasks.status 扩展 pending_review/locked(打卡提交待审核、前置任务锁定)
- try {
- jdbcTemplate.execute(
- "ALTER TABLE tasks MODIFY COLUMN `status` " +
- "ENUM('pending','assigned','in_progress','completed','rejected','overdue','cancelled','locked','pending_review') " +
- "DEFAULT 'pending' COMMENT '状态: pending=待完成 assigned=待接受 in_progress=进行中 completed=已完成 rejected=已拒绝 overdue=已过期 cancelled=已取消 locked=前置锁定 pending_review=待审核'");
- log.info("已扩展 tasks.status 枚举,支持 pending_review/locked");
- } catch (Exception e) {
- log.warn("扩展 tasks.status 枚举失败(可能已修改): {}", e.getMessage());
- }
- // 迁移268: coupon 表添加 status 列(优惠券停用/启用)
- migrateCouponStatus();
- // 迁移276: five_dimension_self_checks 表添加 question_ids_json 列(自检轮换用)
- ensureColumn("five_dimension_self_checks", "question_ids_json", "VARCHAR(255) COMMENT '本次自检使用的题号JSON: [1,5,9,...]'");
- // 迁移277: 创建 self_check_ignores 表(记录用户忽略自检提醒的动作,作为 15 天周期重置依据)
- try {
- jdbcTemplate.execute("CREATE TABLE IF NOT EXISTS self_check_ignores (" +
- "id BIGINT AUTO_INCREMENT PRIMARY KEY, " +
- "user_id BIGINT NOT NULL COMMENT '用户ID', " +
- "check_id BIGINT COMMENT '忽略的自检记录ID(可空)', " +
- "created_at DATETIME DEFAULT CURRENT_TIMESTAMP COMMENT '忽略时间', " +
- "INDEX idx_user_id (user_id)" +
- ") ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='五维自检忽略提醒记录'");
- log.info("已创建 self_check_ignores 表");
- } catch (Exception e) {
- log.warn("创建 self_check_ignores 表失败: {}", e.getMessage());
- }
- // 迁移275: users 表添加 family_inviter_id(家庭邀请追踪)
- ensureColumn("users", "family_inviter_id", "BIGINT DEFAULT NULL COMMENT '家庭邀请人ID(首次注册时绑定,用于追踪邀请来源)'");
- }
-
- /**
- * 迁移268: coupon 表添加 status 列(优惠券停用/启用,幂等)
- * 存量数据 status 为 NULL,查询层按 ACTIVE 处理,不影响现有券。
- */
- private void migrateCouponStatus() {
- try {
- jdbcTemplate.execute("ALTER TABLE coupon ADD COLUMN status VARCHAR(16) DEFAULT 'ACTIVE' COMMENT '状态:ACTIVE=启用/DISABLED=停用'");
- log.info("迁移268: 已为 coupon 表添加 status 列");
- } catch (Exception e) {
- // 列已存在,忽略错误
- }
- }
- /**
- * 角色-菜单授权(幂等,唯一键 uk_role_menu 冲突忽略)
- */
- private void grantMenuToRole(String role, Long menuId) {
- if (role == null || menuId == null) {
- return;
- }
- try {
- jdbcTemplate.update("INSERT IGNORE INTO sys_role_menu (role, menu_id) VALUES (?, ?)", role, menuId);
- } catch (Exception ignored) {
- }
- }
- /**
- * 查询单个菜单 id,不存在返回 null
- */
- private Long queryMenuId(String sql) {
- try {
- List<Long> ids = jdbcTemplate.queryForList(sql, Long.class);
- return ids.isEmpty() ? null : ids.get(0);
- } catch (Exception e) {
- return null;
- }
- }
- /**
- * 迁移244辅助方法: 插入 sys_menu 种子数据
- * 结构与 cfc-web/src/views/Layout.vue 的 menuItems 硬编码一致
- * 数组格式: {parentKey(空=顶级), key, title(目录), label(叶子), path, icon, perm, sort}
- */
- private void seedSysMenus() {
- Map<String, Long> keyToId = new HashMap<>();
- String[][] menus = {
- // 顶级叶子
- {"", "home", "", "首页", "/dashboard", "el-icon-s-home", "dashboard", "0"},
- // 家庭运营(family:*)
- {"", "operation", "家庭运营", "", "", "el-icon-s-custom", "family:*", "1"},
- {"operation", "families", "", "家庭列表", "/families", "el-icon-s-custom", "family:list,family:read,family:bound", "0"},
- {"operation", "children", "", "孩子管理", "/children", "el-icon-user-solid", "family:children,family:read,family:bound", "1"},
- {"operation", "points", "", "积分管理", "/points", "el-icon-s-finance", "family:points", "2"},
- {"operation", "pointsLog", "", "积分记录", "/points-log", "el-icon-document", "family:points,family:bound", "3"},
- {"operation", "wishes", "", "心愿管理", "/wishes", "el-icon-star-off", "family:wishes", "4"},
- {"operation", "rewards", "", "奖励管理", "/rewards", "el-icon-s-goods", "reward:list", "5"},
- // 方案中心(plan:*,插在家庭运营与任务中心之间)
- {"", "planCenter", "方案中心", "", "", "el-icon-notebook-2", "plan:*", "2"},
- {"planCenter", "healthPlanManage", "", "健康方案管理", "/health-plan-manage", "el-icon-first-aid-kit", "plan:health", "0"},
- {"planCenter", "assessmentPlanManage", "", "测评方案管理", "/assessment-plan-manage", "el-icon-edit", "plan:assessment", "1"},
- // 任务中心(task:*)
- {"", "taskCenter", "任务中心", "", "", "el-icon-s-order", "task:*", "3"},
- {"taskCenter", "tasks", "", "任务列表", "/tasks", "el-icon-s-order", "task:list", "0"},
- {"taskCenter", "taskTemplates", "", "任务模板", "/task-templates", "el-icon-document", "task:templates", "1"},
- {"taskCenter", "growthTask", "", "成长任务管理", "/growth-task", "el-icon-s-management", "task:growth", "2"},
- // 审核中心(audit,归集各审核页:服务角色申请/活动审核/退款/提现)
- {"", "reviewCenterGroup", "审核中心", "", "", "el-icon-s-check", "audit", "4"},
- {"reviewCenterGroup", "reviewCenter", "", "审核中心总览", "/review-center", "el-icon-s-check", "audit", "0"},
- {"reviewCenterGroup", "activityReview", "", "活动审核", "/activity-review", "el-icon-document-checked", "audit:activity", "2"},
- {"reviewCenterGroup", "pendingRefund", "", "待退款管理", "/pending-refund", "el-icon-warning", "audit:refund", "3"},
- {"reviewCenterGroup", "earningWithdraw", "", "收益提现审核", "/family-earnings-withdraw", "el-icon-document-checked", "audit:withdraw", "4"},
- // 商城营销(commerce:*)
- {"", "commerce", "商城营销", "", "", "el-icon-s-goods", "commerce:*", "5"},
- {"commerce", "productManage", "", "商品管理", "/product-manage", "el-icon-s-goods", "commerce:products,commerce:self", "0"},
- {"commerce", "inventory", "", "库存管理", "/inventory", "el-icon-document", "inventory:self", "1"},
- {"commerce", "orderManage", "", "订单管理", "/order-manage", "el-icon-s-order", "commerce:orders,order:self", "2"},
- {"commerce", "ecomSupplier", "", "供应商管理", "/supply-system", "el-icon-s-shop", "system:supply", "3"},
- {"commerce", "productProfitRate", "", "产品利润率", "/product-profit-rate", "el-icon-data-line", "finance:profit-rate", "4"},
- {"commerce", "coupon", "", "优惠券管理", "/coupon", "el-icon-ticket", "commerce:coupon", "5"},
- {"commerce", "couponGrantLog", "", "发券记录", "/coupon-grant-log", "el-icon-document", "commerce:coupon", "6"},
- {"commerce", "discountRules", "", "限时折扣管理", "/discount-rules", "el-icon-timer", "commerce:discount", "7"},
- {"commerce", "promotion", "", "推广管理", "/promotion", "el-icon-s-marketing", "commerce:promotion", "7"},
- {"commerce", "familyEarnings", "", "家庭收益", "/family-earnings", "el-icon-s-money", "finance:family-earnings", "8"},
- // 知识中心(articles:*)
- {"", "knowledge", "知识中心", "", "", "el-icon-document", "articles:*", "6"},
- {"knowledge", "articleCategories", "", "知识分类", "/article-categories", "el-icon-folder", "articles:categories", "0"},
- {"knowledge", "articleManage", "", "知识管理", "/article-manage", "el-icon-document", "articles:manage", "1"},
- {"knowledge", "commentReview", "", "评论审核", "/comment-review", "el-icon-chat-dot-round", "articles:manage", "2"},
- {"knowledge", "knowledgeTags", "", "知识标签", "/knowledge-tags", "el-icon-price-tag", "articles:categories", "3"},
- {"knowledge", "knowledgeBase", "", "知识库", "/knowledge-base", "el-icon-reading", "knowledge:base", "4"},
- {"knowledge", "healthKnowledge", "", "健康知识库", "/health-knowledge", "el-icon-first-aid-kit", "knowledge:base", "5"},
- // 活动管理(activity:*,活动审核已移入审核中心)
- {"", "activity", "活动管理", "", "", "el-icon-date", "activity:*", "7"},
- {"activity", "activities", "", "活动列表", "/activities", "el-icon-date", "activity:list,activity:self", "0"},
- {"activity", "activityRegReview", "", "活动报名审核", "/activity-registration-review", "el-icon-document-checked", "activity:registration", "1"},
- // 健康饮食(health:*,能量规则/行为/周期性服务已移入系统配置·健康配置,LangGraph已移入报告解析系统;报告管理/报告审核/应季食材按方案挂载)
- {"", "health", "健康饮食", "", "", "el-icon-first-aid-kit", "health:*", "8"},
- {"health", "reportManagement", "", "报告管理", "/report-management", "el-icon-document", "health:reports", "0"},
- {"health", "proxyReportUpload", "", "代传报告", "/report-proxy-upload", "el-icon-upload", "health:reports", "1"},
- {"health", "healthIndicators", "", "健康指标", "/health-indicators", "el-icon-data-line", "health:indicators", "2"},
- {"health", "healthCheckins", "", "健康打卡", "/health-checkins", "el-icon-s-order", "health:checkins", "3"},
- {"health", "emotionAlert", "", "情绪告警", "/emotion-alert", "el-icon-warning", "health:checkins", "4"},
- {"health", "nutritionMappings", "", "营养素映射", "/nutrition-mappings", "el-icon-connection", "health:mappings", "5"},
- {"health", "foods", "", "食材管理", "/foods", "el-icon-apple", "diet:foods", "6"},
- {"health", "recipes", "", "食谱管理", "/recipes", "el-icon-dish", "diet:recipes", "7"},
- {"health", "seasonalFoods", "", "应季食材", "/seasonal-foods", "el-icon-apple", "diet:foods", "8"},
- {"health", "nutritionProducts", "", "营养产品", "/nutrition-products", "el-icon-first-aid-kit", "health:products", "9"},
- {"health", "energySandbox", "", "五维能量", "/energy-sandbox", "el-icon-data-line", "health:energy", "10"},
- // 报告解析系统(config:report-parser,LangGraph 管理归入)
- {"", "reportParser", "报告解析系统", "", "", "el-icon-cpu", "config:report-parser", "9"},
- {"reportParser", "reportTypes", "", "报告类型管理", "/report-types", "el-icon-collection", "config:report-parser", "0"},
- {"reportParser", "reportParserImport", "", "解析器导入", "/report-parser-import", "el-icon-upload2", "config:report-parser", "1"},
- {"reportParser", "reportUnknownClusters", "", "未知报告审核", "/report-unknown-clusters", "el-icon-question", "config:report-parser", "2"},
- {"reportParser", "reportAutoLearn", "", "报告自学习", "/report-auto-learn", "el-icon-magic-stick", "config:report-parser", "3"},
- {"reportParser", "langgraphAdmin", "", "LangGraph 管理", "/langgraph-admin", "el-icon-cpu", "config:report-parser", "4"},
- // 家庭服务(service:*)
- {"", "familyService", "家庭服务", "", "", "el-icon-s-custom", "service:*", "10"},
- {"familyService", "myFamilies", "", "我的家庭", "/my-families", "el-icon-s-custom", "service:family", "0"},
- {"familyService", "teacherTeam", "", "我的团队", "/teacher-team", "el-icon-s-custom", "service:team", "1"},
- // 业务管理(biz:*)
- {"", "biz", "业务管理", "", "", "el-icon-s-marketing", "biz:*", "11"},
- {"biz", "teacherPackages", "", "任务模板管理", "/teacher-packages", "el-icon-s-goods", "biz:packages", "0"},
- {"biz", "teacherOrders", "", "订单佣金", "/teacher-orders", "el-icon-s-order", "biz:orders", "1"},
- // 测评咨询(测评管理/测评订单/待分配规划师已从系统配置移入)
- {"", "assessment", "测评咨询", "", "", "el-icon-edit", "assessment:*", "12"},
- {"assessment", "assessmentAdmin", "", "测评管理", "/assessment-admin", "el-icon-edit", "assessment:manage", "0"},
- {"assessment", "assessmentOrders", "", "测评订单", "/assessment-orders", "el-icon-s-order", "assessment:orders", "1"},
- {"assessment", "assessmentAssign", "", "待分配规划师", "/assessment-assign", "el-icon-user", "assessment:assign", "2"},
- {"assessment", "teacherAssessment", "", "DAN测评", "/teacher-assessment", "el-icon-edit", "assessment:dan", "3"},
- {"assessment", "teacherConsult", "", "家长咨询", "/teacher-consult", "el-icon-chat-dot-round", "assessment:consult", "4"},
- {"assessment", "growthRecords", "", "成长记录", "/growth-records", "el-icon-document", "growth:records", "5"},
- {"assessment", "growthPlans", "", "成长计划", "/growth-plans", "el-icon-document", "growth:plans", "6"},
- // 系统配置(system:*,权限拆分 system:base / config:member / config:dimension / config:health)
- {"", "system", "系统配置", "", "", "el-icon-s-tools", "system:*", "13"},
- {"system", "baseManage", "基础管理", "", "", "el-icon-s-tools", "system:base", "0"},
- {"baseManage", "sysConfig", "", "系统配置", "/sys-config", "el-icon-s-tools", "system:base", "0"},
- {"baseManage", "users", "", "用户管理", "/users", "el-icon-user", "system:users", "1"},
- {"baseManage", "operationLogs", "", "操作日志", "/operation-logs", "el-icon-s-order", "system:logs", "2"},
- {"baseManage", "membershipCenter", "", "会员中心", "/membership-center", "el-icon-s-custom", "config:member", "3"},
- {"baseManage", "badgeManage", "", "勋章管理", "/badge-manage", "el-icon-medal", "config:member", "4"},
- {"baseManage", "virtualGoodsConfig", "", "虚拟支付道具", "/virtual-goods-config", "el-icon-goods", "config:member", "5"},
- {"baseManage", "gates", "", "关卡配置", "/gates", "el-icon-key", "system:base", "6"},
- {"baseManage", "menuManage", "", "菜单管理", "/menu-manage", "el-icon-menu", "system:menu", "7"},
- {"system", "configCenter", "配置中心", "", "", "el-icon-setting", "config:dimension", "1"},
- {"configCenter", "dimensionConfig", "", "维度配置", "/dimension-config", "el-icon-data-line", "config:dimension", "0"},
- {"system", "supplyHierarchy", "供应商体系", "", "", "el-icon-s-management", "system:supply", "2"},
- {"supplyHierarchy", "supplySystem", "", "供应商体系", "/supply-system", "el-icon-s-management", "system:supply", "0"},
- {"system", "supplierManage", "供应商管理", "", "", "el-icon-s-shop", "supply:manage", "3"},
- {"supplierManage", "supplyManage", "", "供应商管理", "/supply-manage", "el-icon-s-shop", "supply:manage", "0"},
- {"supplierManage", "supplierProducts", "", "商品管理", "/supplier-products", "el-icon-s-goods", "supply:manage", "1"},
- {"system", "healthConfig", "健康配置", "", "", "el-icon-first-aid-kit", "config:health", "4"},
- {"healthConfig", "healthNormConfig", "", "健康常模", "/health-norm-config", "el-icon-data-line", "config:health", "0"},
- {"healthConfig", "healthDataSource", "", "健康数据源", "/health-data-source", "el-icon-data-line", "config:health", "1"},
- {"healthConfig", "healthEnergyConfig", "", "五维能量配置", "/health-energy-config", "el-icon-data-line", "config:health", "2"},
- {"healthConfig", "energyRule", "", "能量规则管理", "/energy-rule", "el-icon-setting", "config:health", "3"},
- {"healthConfig", "energyBehaviorConfig", "", "能量行为配置", "/energy-behavior-config", "el-icon-edit-outline", "config:health", "4"},
- {"healthConfig", "periodicServiceConfig", "", "周期性服务配置", "/periodic-service-config", "el-icon-refresh", "config:health", "5"},
- {"system", "lifeDimension", "人生维度", "", "", "el-icon-s-custom", "config:dimension", "5"},
- {"lifeDimension", "zodiacConfigs", "", "星座配置", "/zodiac-configs", "el-icon-s-management", "config:dimension", "0"},
- {"lifeDimension", "baziConfigs", "", "八字配置", "/bazi-configs", "el-icon-s-management", "config:dimension", "1"},
- {"lifeDimension", "bloodTypeConfigs", "", "血型配置", "/blood-type-configs", "el-icon-s-management", "config:dimension", "2"},
- {"system", "virtualTeam", "虚拟团队", "", "", "el-icon-s-custom", "system:base", "6"},
- {"virtualTeam", "virtualTeams", "", "虚拟团队", "/virtual-teams", "el-icon-s-custom", "system:base", "0"},
- };
- for (String[] m : menus) {
- Long parentId = 0L;
- if (m[0] != null && !m[0].isEmpty()) {
- parentId = keyToId.get(m[0]);
- if (parentId == null) {
- parentId = 0L;
- }
- }
- String title = m[2] != null && !m[2].isEmpty() ? m[2] : null;
- String label = m[3] != null && !m[3].isEmpty() ? m[3] : null;
- String path = m[4] != null && !m[4].isEmpty() ? m[4] : null;
- String icon = m[5] != null && !m[5].isEmpty() ? m[5] : null;
- String perm = m[6] != null && !m[6].isEmpty() ? m[6] : null;
- int sort = Integer.parseInt(m[7]);
- jdbcTemplate.update(
- "INSERT INTO sys_menu (parent_id, title, label, path, icon, perm, sort, visible) VALUES (?, ?, ?, ?, ?, ?, ?, 1)",
- parentId, title, label, path, icon, perm, sort
- );
- Long id = jdbcTemplate.queryForObject("SELECT LAST_INSERT_ID()", Long.class);
- keyToId.put(m[1], id);
- }
- // ===== 迁移:初始化方案规则数据 (P1) =====
- try {
- // 使用 jdbcTemplate 的 DataSource
- var ds = jdbcTemplate.getDataSource();
- if (ds instanceof javax.sql.DataSource) {
- try (Connection conn = ((javax.sql.DataSource) ds).getConnection()) {
- PreparedStatement ps = null;
- try {
- // 插入注意力提升方案规则 1:0-60 分 → 模板 ID=38
- ps = conn.prepareStatement("INSERT INTO assessment_plan_rules (id, dimension, score_min, score_max, template_id, priority, is_active, created_at, updated_at) VALUES (NULL, ?, ?, ?, ?, ?, ?, NOW(), NOW()) ON DUPLICATE KEY UPDATE updated_at=NOW()");
- ps.setString(1, "attention");
- ps.setInt(2, 0);
- ps.setInt(3, 60);
- ps.setLong(4, 38L); // 使用现有的模板 ID
- ps.setInt(5, 1);
- ps.setBoolean(6, true);
- ps.executeUpdate();
- log.info("已插入注意力方案规则 1(0-60 分)");
- ps.close();
- // 插入注意力提升方案规则 2:61-100 分 → 模板 ID=39
- ps = conn.prepareStatement("INSERT INTO assessment_plan_rules (id, dimension, score_min, score_max, template_id, priority, is_active, created_at, updated_at) VALUES (NULL, ?, ?, ?, ?, ?, ?, NOW(), NOW()) ON DUPLICATE KEY UPDATE updated_at=NOW()");
- ps.setString(1, "attention");
- ps.setInt(2, 61);
- ps.setInt(3, 100);
- ps.setLong(4, 39L);
- ps.setInt(5, 2);
- ps.setBoolean(6, true);
- ps.executeUpdate();
- log.info("已插入注意力方案规则 2(61-100 分)");
- ps.close();
- } finally {
- if (ps != null) ps.close();
- }
- }
- }
- } catch (SQLException e) {
- log.warn("初始化方案规则失败:{}", e.getMessage());
- }
- // ===== 迁移:初始化报告类型注册 (R1) =====
- try {
- // 使用 jdbcTemplate 的 DataSource
- var ds = jdbcTemplate.getDataSource();
- if (ds instanceof javax.sql.DataSource) {
- try (Connection conn = ((javax.sql.DataSource) ds).getConnection()) {
- PreparedStatement ps = null;
- try {
- // 注册肠道菌群检测报告类型
- ps = conn.prepareStatement("INSERT INTO report_type_registry (id, type_id, display_name, parser_path, min_confidence, is_active, source, review_status, version, created_at, updated_at) VALUES (NULL, ?, ?, ?, ?, ?, ?, ?, NOW(), NOW()) ON DUPLICATE KEY UPDATE updated_at=NOW()");
- ps.setString(1, "gut_flora");
- ps.setString(2, "肠道菌群检测");
- ps.setString(3, "dan_reports/gut_flora.py");
- ps.setInt(4, 10);
- ps.setBoolean(5, true);
- ps.setString(6, "prebuilt");
- ps.setString(7, "approved");
- ps.setString(8, "1.0");
- ps.executeUpdate();
- log.info("已注册报告类型:gut_flora");
- ps.close();
- } finally {
- if (ps != null) ps.close();
- }
- }
- }
- } catch (SQLException e) {
- log.warn("注册报告类型 gut_flora 失败:{}", e.getMessage());
- }
- // 迁移278-313: 已移至独立方法 runSystemMigrations()(原误置于本方法仅空表触发)
- }
- // 迁移278-313: 系统级 DDL/数据迁移(原误置于 seedSysMenus 内,空表才执行,生产永远跳过)
- // 抽出为独立方法,由 runMigrations() 无条件调用,确保生产库每次启动补齐。
- private void runSystemMigrations() {
- // 迁移278: 创建 family_coupon 表 + family_coupon_grant_log 表
- // (优惠券全链路改为家庭维度:2026-08-31)
- try {
- jdbcTemplate.execute("CREATE TABLE IF NOT EXISTS family_coupon (" +
- "id BIGINT AUTO_INCREMENT PRIMARY KEY, " +
- "family_id BIGINT NOT NULL COMMENT '所属家庭ID', " +
- "coupon_id BIGINT NOT NULL COMMENT '券模板ID', " +
- "status VARCHAR(16) DEFAULT 'AVAILABLE' COMMENT 'AVAILABLE/USED', " +
- "received_at DATETIME DEFAULT CURRENT_TIMESTAMP, " +
- "used_at DATETIME, " +
- "order_id BIGINT COMMENT '核销时写入的订单号', " +
- "INDEX idx_family_coupon (family_id, coupon_id), " +
- "INDEX idx_status (status), " +
- "INDEX idx_order_id (order_id)" +
- ") ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='家庭优惠券表'");
- log.info("已创建 family_coupon 表");
- } catch (Exception e) {
- log.warn("family_coupon 表已存在,跳过创建: {}", e.getMessage());
- }
- try {
- jdbcTemplate.execute("CREATE TABLE IF NOT EXISTS family_coupon_grant_log (" +
- "id BIGINT AUTO_INCREMENT PRIMARY KEY, " +
- "family_id BIGINT NOT NULL COMMENT '受赠家庭ID', " +
- "coupon_id BIGINT NOT NULL COMMENT '券模板ID', " +
- "grant_type VARCHAR(16) NOT NULL COMMENT 'JOIN/PERIODIC/POPULATION/EXCHANGE/CF_EXCHANGE', " +
- "period VARCHAR(16) COMMENT '周期标识(YYYY-MM或YYYY-Qn),PERIODIC防重用', " +
- "quantity INT DEFAULT 1 COMMENT '发放数量', " +
- "source VARCHAR(64) COMMENT '触发来源(订单号/成员ID等)', " +
- "created_at DATETIME DEFAULT CURRENT_TIMESTAMP, " +
- "UNIQUE KEY uk_family_grant (family_id, coupon_id, grant_type, period), " +
- "INDEX idx_coupon (coupon_id), " +
- "INDEX idx_family (family_id)" +
- ") ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='家庭优惠券发放流水表'");
- log.info("已创建 family_coupon_grant_log 表");
- } catch (Exception e) {
- log.warn("family_coupon_grant_log 表已存在,跳过创建: {}", e.getMessage());
- }
- // 迁移279: 创建 cf_rate_tier 表(CF值返佣阶梯配置,替代 promotion_tier_config)
- try {
- jdbcTemplate.execute("CREATE TABLE IF NOT EXISTS cf_rate_tier (" +
- "id BIGINT AUTO_INCREMENT PRIMARY KEY, " +
- "tier_name VARCHAR(50) NOT NULL COMMENT '档位名称', " +
- "min_team_size INT NOT NULL DEFAULT 0 COMMENT '团队规模下限(含)', " +
- "rate_percent INT NOT NULL DEFAULT 0 COMMENT '返佣比例(%)', " +
- "sort_order INT NOT NULL DEFAULT 0 COMMENT '排序,越大越高', " +
- "enabled TINYINT DEFAULT 1 COMMENT '1启用/0停用', " +
- "created_at DATETIME DEFAULT CURRENT_TIMESTAMP, " +
- "updated_at DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, " +
- "INDEX idx_min_size (min_team_size)" +
- ") ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='CF值返佣阶梯配置'");
- log.info("已创建cf_rate_tier表");
- Integer cnt = jdbcTemplate.queryForObject("SELECT COUNT(*) FROM cf_rate_tier", Integer.class);
- if (cnt == null || cnt == 0) {
- jdbcTemplate.execute("INSERT INTO cf_rate_tier (tier_name, min_team_size, rate_percent, sort_order) VALUES " +
- "('铜牌', 0, 5, 1), ('银牌', 3, 10, 2), ('金牌', 10, 15, 3), ('铂金', 30, 20, 4), ('钻石', 100, 25, 5)");
- log.info("cf_rate_tier 种子数据已初始化");
- }
- } catch (Exception e) {
- log.warn("创建cf_rate_tier表失败: {}", e.getMessage());
- }
- // 迁移280: 创建 cf_transfer_record 表(CF值流转记录)
- try {
- jdbcTemplate.execute("CREATE TABLE IF NOT EXISTS cf_transfer_record (" +
- "id BIGINT AUTO_INCREMENT PRIMARY KEY, " +
- "from_user_id BIGINT COMMENT '转出用户ID(null=平台)', " +
- "to_user_id BIGINT COMMENT '转入用户ID', " +
- "family_id BIGINT COMMENT '所属家庭ID', " +
- "amount INT NOT NULL COMMENT 'CF值数量', " +
- "type VARCHAR(16) NOT NULL COMMENT 'transfer/allocate/refund', " +
- "ref_type VARCHAR(50) COMMENT '关联业务类型', " +
- "ref_id BIGINT COMMENT '关联业务ID', " +
- "remark VARCHAR(255), " +
- "created_at DATETIME DEFAULT CURRENT_TIMESTAMP, " +
- "INDEX idx_from (from_user_id), " +
- "INDEX idx_to (to_user_id), " +
- "INDEX idx_family (family_id)" +
- ") ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='CF值流转记录'");
- log.info("已创建cf_transfer_record表");
- } catch (Exception e) {
- log.warn("创建cf_transfer_record表失败: {}", e.getMessage());
- }
- // 迁移281: promotion_tier 精简列(删三层,加 rate_percent)
- try {
- jdbcTemplate.execute("ALTER TABLE promotion_tier DROP COLUMN team_size_1st, DROP COLUMN team_size_2nd, DROP COLUMN team_size_3rd");
- log.info("promotion_tier 已删除三层团队列");
- } catch (Exception e) {
- log.warn("promotion_tier 三层列删除失败(可能不存在): {}", e.getMessage());
- }
- try {
- jdbcTemplate.execute("ALTER TABLE promotion_tier ADD COLUMN rate_percent INT DEFAULT 0 COMMENT '当前返佣比例(%)'");
- log.info("promotion_tier 已添加 rate_percent");
- } catch (Exception e) {
- log.warn("promotion_tier.rate_percent 已存在: {}", e.getMessage());
- }
- // 迁移282: coupon 表添加 family_id
- try {
- jdbcTemplate.execute("ALTER TABLE coupon ADD COLUMN family_id BIGINT DEFAULT NULL COMMENT '绑定家庭ID(NULL=不绑定)'");
- log.info("coupon 已添加 family_id");
- } catch (Exception e) {
- log.warn("coupon.family_id 已存在: {}", e.getMessage());
- }
- // 迁移283: referral_tree 唯一键改为 (parent_id, child_id) 联合唯一(支持每祖先一行物化)
- try {
- jdbcTemplate.execute("ALTER TABLE referral_tree DROP INDEX uk_child_id");
- log.info("referral_tree 已删除 uk_child_id 唯一键");
- } catch (Exception e) {
- log.warn("referral_tree.uk_child_id 删除失败(可能不存在): {}", e.getMessage());
- }
- try {
- jdbcTemplate.execute("ALTER TABLE referral_tree ADD UNIQUE KEY uk_parent_child (parent_id, child_id)");
- log.info("referral_tree 已添加 uk_parent_child 联合唯一键");
- } catch (Exception e) {
- log.warn("referral_tree.uk_parent_child 已存在: {}", e.getMessage());
- }
- // 迁移284: cf_transfer_record 加 (ref_type, ref_id) 唯一索引(幂等兜底,防止并发 earn 重复发放)
- try {
- jdbcTemplate.execute("ALTER TABLE cf_transfer_record ADD UNIQUE KEY uk_ref_type_id (ref_type, ref_id)");
- log.info("cf_transfer_record 已添加 uk_ref_type_id 唯一索引");
- } catch (Exception e) {
- log.warn("cf_transfer_record.uk_ref_type_id 已存在: {}", e.getMessage());
- }
- // 迁移285: 创建 product_gift_configs 表(商品赠品配置,商品×等级)
- try {
- jdbcTemplate.execute("CREATE TABLE IF NOT EXISTS product_gift_configs (" +
- "id BIGINT AUTO_INCREMENT PRIMARY KEY, " +
- "product_id BIGINT NOT NULL COMMENT '关联products表', " +
- "level_code VARCHAR(50) NOT NULL COMMENT '会员等级编码(FAMILY/PREMIUM等)', " +
- "status TINYINT(1) DEFAULT 1 COMMENT '1=启用 0=停用', " +
- "created_at DATETIME DEFAULT CURRENT_TIMESTAMP, " +
- "updated_at DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, " +
- "UNIQUE KEY uk_product_level (product_id, level_code), " +
- "INDEX idx_product_id (product_id), " +
- "INDEX idx_level_code (level_code)" +
- ") ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='商品赠品配置(商品×等级)'");
- log.info("已创建product_gift_configs表");
- } catch (Exception e) {
- // 表已存在,忽略
- }
- // 迁移286: 创建 product_gift_items 表(商品赠品项)
- try {
- jdbcTemplate.execute("CREATE TABLE IF NOT EXISTS product_gift_items (" +
- "id BIGINT AUTO_INCREMENT PRIMARY KEY, " +
- "config_id BIGINT NOT NULL COMMENT '关联product_gift_configs.id', " +
- "gift_type VARCHAR(20) NOT NULL COMMENT 'product=赠品商品 cf=CF值', " +
- "gift_product_id BIGINT COMMENT '赠品商品ID(gift_type=product时必填)', " +
- "gift_product_name VARCHAR(200) COMMENT '赠品商品名快照', " +
- "gift_product_image VARCHAR(500) COMMENT '赠品商品图快照', " +
- "cf_value INT DEFAULT 0 COMMENT 'CF值数量(gift_type=cf时必填,单位分)', " +
- "sort_order INT DEFAULT 0 COMMENT '排序(值越小越靠前)', " +
- "created_at DATETIME DEFAULT CURRENT_TIMESTAMP, " +
- "INDEX idx_config_id (config_id)" +
- ") ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='商品赠品项'");
- log.info("已创建product_gift_items表");
- } catch (Exception e) {
- // 表已存在,忽略
- }
- // 迁移287: 创建 product_order_gifts 表(订单赠品记录)
- try {
- jdbcTemplate.execute("CREATE TABLE IF NOT EXISTS product_order_gifts (" +
- "id BIGINT AUTO_INCREMENT PRIMARY KEY, " +
- "order_id BIGINT NOT NULL COMMENT '关联product_orders.id', " +
- "order_no VARCHAR(64) NOT NULL COMMENT '订单号快照', " +
- "gift_type VARCHAR(20) NOT NULL COMMENT 'product=赠品商品 cf=CF值', " +
- "gift_product_id BIGINT COMMENT '赠品商品ID', " +
- "gift_product_name VARCHAR(200) COMMENT '赠品商品名快照', " +
- "gift_product_image VARCHAR(500) COMMENT '赠品商品图快照', " +
- "cf_value INT DEFAULT 0 COMMENT 'CF值数量(gift_type=cf时)', " +
- "quantity INT DEFAULT 1 COMMENT '数量(默认1)', " +
- "fulfilled TINYINT(1) DEFAULT 0 COMMENT '0=已选未发放 1=已发放', " +
- "created_at DATETIME DEFAULT CURRENT_TIMESTAMP, " +
- "INDEX idx_order_id (order_id), " +
- "INDEX idx_order_no (order_no)" +
- ") ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='订单赠品记录'");
- log.info("已创建product_order_gifts表");
- } catch (Exception e) {
- // 表已存在,忽略
- }
- // 迁移288: health_report_drafts 加异步采集状态列(报告采集流程异步化)
- // 用途:上传后进入后台异步采集(指纹判定→专用脚本/opencode),前端轮询 parse_status
- ensureColumn("health_report_drafts", "parse_status",
- "VARCHAR(20) DEFAULT NULL COMMENT '异步解析状态: collecting/completed/failed(NULL=未触发异步采集)'");
- ensureColumn("health_report_drafts", "parse_method",
- "VARCHAR(32) DEFAULT NULL COMMENT '解析方式: fingerprint_script/opencode/langgraph'");
- ensureColumn("health_report_drafts", "parse_error",
- "VARCHAR(2000) DEFAULT NULL COMMENT '异步解析失败原因'");
- log.info("已添加 health_report_drafts 异步采集状态列 (parse_status/parse_method/parse_error)");
- // 迁移289: health_reports 补 report_format 列(HealthReport 实体已含字段,但从未迁移至 DB,全字段 SELECT 报 Unknown column)
- ensureColumn("health_reports", "report_format",
- "VARCHAR(32) DEFAULT NULL COMMENT '报告格式: pdf/image'");
- // 迁移290: 补齐实体已含但 DB 从未迁移的真实缺列(全字段 SELECT 防 Unknown column)
- // 逐表核实(对照 schema.sql + 历史 ADD COLUMN/ensureColumn),仅含确认不存在的列,共 6 表 21 列
- // 风险预留:promotion_tier_config 实体与 DB 为两套不同晋级模型(已废弃,由 cf_rate_tier 迁移279 替代),
- // 按用户决定保持现状不迁移,避免与旧种子数据冲突;后续清理旧实体依赖(CommissionService等)时再处理
- ensureColumn("activities", "visibility", "VARCHAR(20) DEFAULT 'public' COMMENT '浏览权限:public/login/private'");
- ensureColumn("activities", "visible_to", "TEXT DEFAULT NULL COMMENT '私密指定人群JSON'");
- ensureColumn("dan_knowledge_base", "file_url", "VARCHAR(500) DEFAULT NULL COMMENT 'fileUrl'");
- ensureColumn("dan_knowledge_base", "file_name", "VARCHAR(255) DEFAULT NULL COMMENT 'fileName'");
- ensureColumn("dan_knowledge_base", "source_url", "VARCHAR(500) DEFAULT NULL COMMENT 'sourceUrl'");
- ensureColumn("dan_knowledge_base", "source_type", "VARCHAR(20) DEFAULT NULL COMMENT 'sourceType'");
- ensureColumn("distribution_relations", "invite_code", "VARCHAR(64) DEFAULT NULL COMMENT 'inviteCode'");
- ensureColumn("family_members", "birth_hour", "VARCHAR(10) DEFAULT NULL COMMENT '出生时辰'");
- ensureColumn("family_members", "birth_weight", "INT DEFAULT NULL COMMENT '出生体重(g)'");
- ensureColumn("family_members", "birth_place", "VARCHAR(255) DEFAULT NULL COMMENT '出生地'");
- ensureColumn("family_members", "is_cesarean", "TINYINT(1) DEFAULT 0 COMMENT '是否剖腹产: 0否 1是'");
- ensureColumn("health_indicators", "symptoms", "TEXT DEFAULT NULL COMMENT '症状描述'");
- ensureColumn("product_orders", "tracking_number", "VARCHAR(100) DEFAULT NULL COMMENT '快递单号'");
- ensureColumn("product_orders", "express_company", "VARCHAR(100) DEFAULT NULL COMMENT '快递公司'");
- ensureColumn("product_orders", "delivery_method", "INT DEFAULT NULL COMMENT '配送方式: 1=快递 2=自提 3=两者皆可 4=线上-可选人 5=线上-单人'");
- ensureColumn("product_orders", "pickup_code", "VARCHAR(64) DEFAULT NULL COMMENT '自提码'");
- ensureColumn("product_orders", "confirm_time", "DATETIME DEFAULT NULL COMMENT '确认收货时间'");
- ensureColumn("product_orders", "refund_status", "INT DEFAULT NULL COMMENT '退款状态'");
- ensureColumn("product_orders", "refund_reason", "VARCHAR(255) DEFAULT NULL COMMENT '退款原因'");
- ensureColumn("product_orders", "refund_amount", "INT DEFAULT NULL COMMENT '退款金额(分)'");
- ensureColumn("product_orders", "refund_time", "DATETIME DEFAULT NULL COMMENT '退款时间'");
- log.info("已补齐真实缺列 (迁移290, 6表21列)");
- // 迁移291: food_nutrients 列名对齐(amount_per_100g -> amount_per100g)
- // FoodNutrient 实体字段 amountPer100g 经 MyBatis-Plus camel->snake 映射为 amount_per100g,
- // 但建表 SQL 误用 amount_per_100g,导致实体全字段 SELECT 报 Unknown column 'amount_per100g'。
- // 当前 FoodNutrientMapper 无 Service 调用,属潜在隐患;此处统一列名为 amount_per100g。
- try {
- Integer cnt = jdbcTemplate.queryForObject(
- "SELECT COUNT(*) FROM information_schema.COLUMNS WHERE TABLE_SCHEMA = DATABASE() " +
- "AND TABLE_NAME = 'food_nutrients' AND COLUMN_NAME = 'amount_per_100g'", Integer.class);
- if (cnt != null && cnt > 0) {
- jdbcTemplate.execute("ALTER TABLE food_nutrients CHANGE COLUMN amount_per_100g amount_per100g DECIMAL(10,4) NOT NULL COMMENT '每100g含量'");
- log.info("已重命名 food_nutrients.amount_per_100g -> amount_per100g");
- }
- } catch (Exception e) {
- // 表不存在或列不存在,忽略
- }
- // 迁移292: health_status 补 member_id 并改为成员级归档(Unknown column 'member_id')
- // 迁移239 建表只有 user_id,schema.sql 与实体已含 member_id(成员级档案),生产缺列导致 selectOne 报错。
- // 原 UNIQUE KEY uk_user(user_id) 限制每账号仅一行,与成员级多行归档冲突,改为复合索引 idx_user_member。
- ensureColumn("health_status", "member_id", "BIGINT DEFAULT NULL COMMENT '家庭成员ID(NULL=账号级,非空=成员级归档)'");
- try {
- jdbcTemplate.execute("ALTER TABLE health_status DROP INDEX uk_user");
- log.info("已移除 health_status.uk_user 唯一键(允许成员级多行归档)");
- } catch (Exception e) {
- // 索引不存在,忽略
- }
- try {
- jdbcTemplate.execute("ALTER TABLE health_status ADD INDEX idx_user_member (user_id, member_id)");
- log.info("已添加 health_status.idx_user_member 复合索引");
- } catch (Exception e) {
- // 索引已存在,忽略
- }
- // 迁移293: family_members 添加 is_admin 列(后台家庭成员仅区分管理员/成员,不再区分家长/孩子)
- ensureColumn("family_members", "is_admin", "TINYINT(1) DEFAULT 0 COMMENT '是否家庭管理员: 1=管理员, 0=普通成员'");
- // 回填:将每个家庭的创建者(families.creator_id 匹配 family_members.user_id)设为管理员
- try {
- jdbcTemplate.execute("UPDATE family_members fm JOIN families f ON fm.family_id = f.id " +
- "SET fm.is_admin = 1 WHERE fm.user_id = f.creator_id AND (fm.is_admin IS NULL OR fm.is_admin = 0)");
- log.info("迁移293: 已回填家庭创建者为管理员");
- } catch (Exception e) {
- log.warn("迁移293: 回填家庭创建者为管理员失败: {}", e.getMessage());
- }
- // 迁移294: articles 表加 sort_order 列(精选文章推荐排序,越小越靠前)
- ensureColumn("articles", "sort_order", "INT NOT NULL DEFAULT 0 COMMENT '推荐排序值(越小越靠前)'");
- log.info("已添加 articles.sort_order 列(迁移294)");
- // 迁移294b: articles 表添加 audio_url/audio_text 列(文章AI朗读音频)
- ensureColumn("articles", "audio_url", "VARCHAR(500) DEFAULT NULL COMMENT 'AI朗读音频URL'");
- ensureColumn("articles", "audio_text", "TEXT COMMENT '朗读文本(纯文本,缺省从content剥离)'");
- log.info("已添加 articles.audio_url/audio_text 列(迁移294b)");
- // 迁移295: task_plan_instances 补齐实体已含但 DB 缺失的审核/确认/激活列(review/activate 写字段报 Unknown column)
- ensureColumn("task_plan_instances", "source", "VARCHAR(20) DEFAULT NULL COMMENT '来源: manual/assessment/template'");
- ensureColumn("task_plan_instances", "source_result_id", "BIGINT DEFAULT NULL COMMENT '关联测评结果ID(source=assessment时)'");
- ensureColumn("task_plan_instances", "remark", "TEXT COMMENT '方案备注(含指导意见摘要)'");
- ensureColumn("task_plan_instances", "reviewed_by", "BIGINT DEFAULT NULL COMMENT '审核人(规划师ID)'");
- ensureColumn("task_plan_instances", "reviewed_at", "DATETIME DEFAULT NULL COMMENT '审核时间'");
- ensureColumn("task_plan_instances", "review_comment", "TEXT COMMENT '审核意见'");
- ensureColumn("task_plan_instances", "confirmed_by", "BIGINT DEFAULT NULL COMMENT '确认人(家长ID)'");
- ensureColumn("task_plan_instances", "confirmed_at", "DATETIME DEFAULT NULL COMMENT '确认时间'");
- ensureColumn("task_plan_instances", "activated_at", "DATETIME DEFAULT NULL COMMENT '激活时间'");
- log.info("迁移295: 已补齐 task_plan_instances 审核/确认/激活列");
- // 迁移296: 健康方案任务 source_type 'plan' -> 'health_plan'(消除与 task_plan_instances 的 ID 域冲突)
- try {
- // 冲突检测:source_id 同时存在于 health_plans 和 task_plan_instances 的任务(需人工确认,不阻断迁移)
- Integer conflict = jdbcTemplate.queryForObject(
- "SELECT COUNT(*) FROM tasks t WHERE t.source_type = 'plan' " +
- "AND EXISTS (SELECT 1 FROM health_plans h WHERE h.id = t.source_id) " +
- "AND EXISTS (SELECT 1 FROM task_plan_instances i WHERE i.id = t.source_id)",
- Integer.class);
- if (conflict != null && conflict > 0) {
- log.warn("迁移296: 检测到{}条冲突任务(source_id同时存在于health_plans和task_plan_instances),需人工确认;无冲突部分仍继续迁移", conflict);
- }
- // 始终执行 UPDATE:仅迁移"存在于 health_plans 且不存在于 task_plan_instances"的任务(冲突部分不迁移,避免误改套餐任务)
- int updated = jdbcTemplate.update(
- "UPDATE tasks SET source_type = 'health_plan' " +
- "WHERE source_type = 'plan' " +
- "AND EXISTS (SELECT 1 FROM health_plans h WHERE h.id = tasks.source_id) " +
- "AND NOT EXISTS (SELECT 1 FROM task_plan_instances i WHERE i.id = tasks.source_id)");
- log.info("迁移296: 已修正{}条健康方案任务 source_type -> health_plan", updated);
- } catch (Exception e) {
- log.warn("迁移296执行失败(health_plans 表可能不存在): {}", e.getMessage());
- }
- // 迁移297: 认知训练方案任务补设 source_type='training_plan'(training_plan_tasks 关联溯源)
- try {
- Integer updated = jdbcTemplate.update(
- "UPDATE tasks t SET t.source_type = 'training_plan' " +
- "WHERE (t.source_type IS NULL OR t.source_type = '') " +
- "AND EXISTS (SELECT 1 FROM training_plan_tasks pt WHERE pt.task_id = t.id)");
- log.info("迁移297: 已补设{}条认知训练方案任务 source_type -> training_plan", updated);
- } catch (Exception e) {
- log.warn("迁移297执行失败(training_plan_tasks 表可能不存在): {}", e.getMessage());
- }
- // 迁移298: 存量 role='child' 账号强制迁移为成员模型
- // 1) 有 openid 的 child → role 改 parent(可登录成员,登录后进成员端界面,不能被切换)
- // 2) 无 openid 的 child → 保持不可登录,确保有 family_members 记录(仅可被切换)
- try {
- Integer updated = jdbcTemplate.update(
- "UPDATE users SET role='parent' WHERE role='child' AND openid IS NOT NULL AND openid <> ''");
- log.info("迁移298: 已迁移{}条有 openid 的 child 账号 role → parent", updated);
- } catch (Exception e) {
- log.warn("迁移298第一步失败", e);
- }
- try {
- jdbcTemplate.execute(
- "INSERT INTO family_members (family_id, user_id, nickname, gender, birthday, generation, show_to_family, total_points, created_at, updated_at) " +
- "SELECT u.family_id, u.id, u.nickname, u.gender, u.birthday, -1, 1, 0, NOW(), NOW() " +
- "FROM users u " +
- "WHERE u.role='child' AND (u.openid IS NULL OR u.openid='') " +
- "AND u.family_id IS NOT NULL " +
- "AND NOT EXISTS (SELECT 1 FROM family_members fm WHERE fm.user_id = u.id)");
- log.info("迁移298: 已为无 openid 的 child 账号补齐 family_members 记录");
- } catch (Exception e) {
- log.warn("迁移298第二步失败", e);
- }
- // 迁移322: 废弃 children 表 —— 数据迁移到 family_members 后删除表(幂等,表不存在时跳过)
- try {
- Integer childrenTableCnt = jdbcTemplate.queryForObject(
- "SELECT COUNT(*) FROM information_schema.TABLES WHERE TABLE_SCHEMA = DATABASE() AND TABLE_NAME = 'children'",
- Integer.class);
- if (childrenTableCnt == null || childrenTableCnt == 0) {
- log.info("迁移322: children 表不存在,跳过(已废弃)");
- } else {
- // 1. 匹配更新:family_members 已存在该成员记录时补全字段
- jdbcTemplate.execute(
- "UPDATE family_members fm INNER JOIN children c " +
- "ON fm.family_id = c.family_id AND fm.nickname = c.nickname AND fm.user_id = c.user_id " +
- "SET fm.id_card = c.id_card, fm.penalty_enabled = c.penalty_enabled, fm.theme = c.theme, " +
- "fm.system_points = c.system_points, fm.streak_days = c.streak_days, fm.last_task_date = c.last_task_date, " +
- "fm.focus_max_daily = c.focus_max_daily, fm.focus_remaining = c.focus_remaining, " +
- "fm.focus_reset_date = c.focus_reset_date, fm.total_points = c.total_points, " +
- "fm.show_to_family = c.show_to_family, fm.birthday = c.birthday, fm.gender = c.gender, " +
- "fm.phone = c.phone, fm.role_override = 'child'");
- log.info("迁移322: 已更新 family_members 中已存在的 children 匹配记录字段");
- // 2. 不匹配则插入新记录
- jdbcTemplate.execute(
- "INSERT INTO family_members (family_id, user_id, nickname, birthday, gender, phone, " +
- "id_card, penalty_enabled, theme, system_points, streak_days, last_task_date, " +
- "focus_max_daily, focus_remaining, focus_reset_date, show_to_family, total_points, " +
- "role_override, created_at, updated_at) " +
- "SELECT family_id, user_id, nickname, birthday, gender, phone, " +
- "id_card, penalty_enabled, theme, system_points, streak_days, last_task_date, " +
- "focus_max_daily, focus_remaining, focus_reset_date, show_to_family, total_points, " +
- "'child', created_at, updated_at " +
- "FROM children " +
- "WHERE NOT EXISTS (SELECT 1 FROM family_members fm2 WHERE fm2.family_id = children.family_id " +
- "AND fm2.nickname = children.nickname AND fm2.user_id = children.user_id)");
- log.info("迁移322: 已从 children 插入不匹配记录到 family_members");
- // 3. 删除 children 表
- jdbcTemplate.execute("DROP TABLE IF EXISTS children");
- log.info("迁移322: 已删除 children 表(数据已迁移到 family_members)");
- }
- } catch (Exception e) {
- log.warn("迁移322执行失败(children 表可能不存在或迁移范围内字段缺失): {}", e.getMessage());
- }
- ensureColumn("promotion_tier", "rate_percent", "INT DEFAULT 0 COMMENT '当前返佣比例(%)'");
- // 迁移300: family_members 添加 status 列(成员状态: active/recycled,非微信用户被移出进回收箱)
- ensureColumn("family_members", "status", "VARCHAR(16) DEFAULT 'active' COMMENT '成员状态: active=正常, recycled=回收箱'");
- // ===== 生产库对账候选风险记录(已核对生产库,2026-09,按用户决定不迁移/暂不动,仅记录)=====
- // 注:以下为 entity 期望但生产库未对齐.项目,用户已决定保持现状或记为候选,勿重复迁移。
-
- // exportUserData 全字段 SELECT 会报 Unknown column 'phone')。schema.sql 已将 children 标为
- // 已废除/并入 family_members,用户决定不迁移。
- // 2. user_trigger_rules:生产表仅剩 user_id(PK,auto_increment)/action_config/last_triggered_at,
- // 实体期望 id/trigger_type/trigger_action/status/created_at 共 8 列,表 0 行且不在 schema.sql。
- // UserSegmentationService.getTriggerRules 会查 status,命中时会报 Unknown column 'status'。候选。
- // 3. promotion_tier.rate_percent:生产用旧模型(tier/team_size_1st/2nd/3rd/total_*),实体期望新模型,
-
- // 4. promotion_tier_config:实体已废弃,改用 cf_rate_tier(迁移279),保持现状。
- // 5. family_members.member_type:实体为 VO 计算值已加 @TableField(exist=false)(不落库),schema.sql
- // 无此列;2026-09-06 生产已手动 ALTER 补列(JwtInterceptor/SurveyServiceImpl 查询报 Unknown column
- // 'member_type' 修复,生产旧代码兼容用),部署含 exist=false 的新代码后该列不再被引用,可留可删。
- // 迁移302: tasks表新增is_daily_progress/target_value字段(统一任务系统;原迁移298用 AFTER source_id,列缺失时会被静默吞掉)
- ensureColumn("tasks", "is_daily_progress", "TINYINT DEFAULT 0 COMMENT '1=每日进度型任务(无deadline,每日重置)'");
- ensureColumn("tasks", "target_value", "INT DEFAULT NULL COMMENT '进度目标值(is_daily_progress=1时有效)'");
- try {
- jdbcTemplate.execute("ALTER TABLE tasks MODIFY COLUMN source_type VARCHAR(32) DEFAULT NULL COMMENT '来源类型: self/assigned/plan/health_plan/training_plan/onboarding/ai/template/article/activity/purchase'");
- log.info("已更新tasks.source_type注释");
- } catch (Exception e) {
- // 修改失败,忽略
- }
- // 迁移296: admin_task_templates表新增onboarding_sequence字段(统一任务系统:新人引导复用任务模板表)
- try {
- jdbcTemplate.execute("ALTER TABLE admin_task_templates ADD COLUMN onboarding_sequence INT DEFAULT NULL COMMENT '新人引导顺序(source_type=onboarding的模板用)' AFTER teacher_id");
- log.info("已添加onboarding_sequence列到admin_task_templates表");
- } catch (Exception e) {
- // 列已存在,忽略
- }
- // 迁移297: 统一任务系统数据迁移——growth_task DAILY定义迁入admin_task_templates(仅执行一次,幂等)
- try {
- Integer migrated = jdbcTemplate.queryForObject(
- "SELECT COUNT(*) FROM sys_config WHERE config_key = 'TASK_UNIFY_GT_MIGRATED'", Integer.class);
- if (migrated == null || migrated == 0) {
- // DAILY 类型 → admin_task_templates(title 拼入 [task_key] 前缀,供 updateProgress 匹配)
- jdbcTemplate.execute("INSERT INTO admin_task_templates (title, description, points, category, need_review, is_active, task_type, frequency, created_at, updated_at) " +
- "SELECT CONCAT('[', task_key, ']', title), description, reward_points, '成长', 0, enabled, 'recurring', 'daily', created_at, created_at " +
- "FROM growth_task WHERE type = 'DAILY' AND enabled = 1");
- jdbcTemplate.update("INSERT INTO sys_config (config_key, config_value, description) VALUES ('TASK_UNIFY_GT_MIGRATED', '1', '统一任务系统: growth_task DAILY定义已迁入admin_task_templates')");
- log.info("迁移297: growth_task DAILY定义已迁入admin_task_templates");
- }
- } catch (Exception e) {
- log.warn("迁移297: growth_task迁移失败或已执行: {}", e.getMessage());
- }
- // 说明:growth_task_log 历史进度、onboarding_task 存量数据仅保留在旧表(只读备份),不回填 tasks(历史数据无法1:1映射到新模型)
- // 迁移299: 插入新人引导任务模板种子数据(统一任务系统:onboarding 复用 admin_task_templates)
- // title = 原 taskType(REGISTER/COMPLETE_PROFILE/...),onboarding_sequence 定义顺序链
- try {
- Integer onboardCount = jdbcTemplate.queryForObject(
- "SELECT COUNT(*) FROM admin_task_templates WHERE onboarding_sequence IS NOT NULL", Integer.class);
- if (onboardCount == null || onboardCount == 0) {
- jdbcTemplate.execute("INSERT INTO admin_task_templates (title, description, points, category, need_review, is_active, task_type, frequency, onboarding_sequence, created_at, updated_at) VALUES " +
- "('REGISTER', '完成注册', 100, '成长', 0, 1, 'onetime', 'none', 1, NOW(), NOW()), " +
- "('COMPLETE_PROFILE', '完善个人资料', 50, '成长', 0, 1, 'onetime', 'none', 2, NOW(), NOW()), " +
- "('ADD_CHILD', '添加家庭成员', 100, '成长', 0, 1, 'onetime', 'none', 3, NOW(), NOW()), " +
- "('FIRST_CHECKIN', '完成首次健康打卡', 30, '成长', 0, 1, 'onetime', 'none', 4, NOW(), NOW()), " +
- "('FIRST_TASK', '完成首次任务', 50, '成长', 0, 1, 'onetime', 'none', 5, NOW(), NOW()), " +
- "('BIND_REFERRAL', '绑定推荐人', 50, '成长', 0, 1, 'onetime', 'none', 6, NOW(), NOW()), " +
- "('COMPLETE_SELF_CHECK', '完成一次五维自检', 60, '成长', 0, 1, 'onetime', 'none', 7, NOW(), NOW())");
- log.info("迁移299: 已插入7条新人引导任务模板种子数据");
- }
- } catch (Exception e) {
- log.warn("迁移299: 插入新人引导任务模板失败或已存在: {}", e.getMessage());
- }
- // 迁移301: platform_balance_log 加 (user_id, ref_type, ref_id) 唯一索引(earn 幂等 DB 兜底,防止并发重复发放)
- try {
- jdbcTemplate.execute("DELETE l1 FROM platform_balance_log l1 " +
- "INNER JOIN platform_balance_log l2 ON l1.user_id = l2.user_id " +
- "AND l1.ref_type = l2.ref_type AND l1.ref_id = l2.ref_id AND l1.id > l2.id " +
- "WHERE l1.ref_id IS NOT NULL AND l1.ref_type IS NOT NULL");
- log.info("迁移301: platform_balance_log 历史重复流水已清理");
- jdbcTemplate.execute("ALTER TABLE platform_balance_log ADD UNIQUE KEY uk_user_ref (user_id, ref_type, ref_id)");
- log.info("迁移301: platform_balance_log 已添加 uk_user_ref 唯一索引");
- } catch (Exception e) {
- log.warn("迁移301: platform_balance_log 唯一索引处理失败: {}", e.getMessage());
- }
- // 迁移302: 创建 contact_help_logs 表(章鱼图帮助记录)
- try {
- jdbcTemplate.execute("CREATE TABLE IF NOT EXISTS contact_help_logs (" +
- "id BIGINT AUTO_INCREMENT PRIMARY KEY, " +
- "user_id BIGINT NOT NULL COMMENT '被帮助者(当前用户)', " +
- "contact_id BIGINT NOT NULL COMMENT '帮助者(联系人)', " +
- "help_type VARCHAR(32) COMMENT '帮助类型: MONEY/ITEM/EMOTION/RESOURCE/SKILL/TIME', " +
- "amount DECIMAL(10,2) DEFAULT 0 COMMENT '帮助金额(非金钱类为0)', " +
- "description VARCHAR(255) COMMENT '帮助内容简述', " +
- "happened_at DATETIME COMMENT '帮助发生时间', " +
- "created_at DATETIME DEFAULT CURRENT_TIMESTAMP, " +
- "INDEX idx_chl_user (user_id), " +
- "INDEX idx_chl_contact (contact_id) " +
- ") ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='联系人帮助记录(章鱼图数据源)'");
- log.info("已创建contact_help_logs表");
- } catch (Exception e) {
- log.warn("创建contact_help_logs表失败: {}", e.getMessage());
- }
- // 迁移303: 创建 resource_items 表(珍珠图资源登记:信息/场所/技能类资源)
- try {
- jdbcTemplate.execute("CREATE TABLE IF NOT EXISTS resource_items (" +
- "id BIGINT AUTO_INCREMENT PRIMARY KEY, " +
- "user_id BIGINT NOT NULL COMMENT '资源拥有者(当前用户)', " +
- "type VARCHAR(16) NOT NULL COMMENT '资源类型: INFO(信息)/PLACE(场所)/SKILL(技能)', " +
- "name VARCHAR(100) NOT NULL COMMENT '资源名称', " +
- "description VARCHAR(255) COMMENT '资源描述(可调用方式/说明)', " +
- "contact_id BIGINT DEFAULT NULL COMMENT '关联联系人ID(可选)', " +
- "created_at DATETIME DEFAULT CURRENT_TIMESTAMP, " +
- "INDEX idx_ri_user (user_id) " +
- ") ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='珍珠图资源登记表(信息/场所/技能类资源)'");
- log.info("已创建resource_items表");
- } catch (Exception e) {
- log.warn("创建resource_items表失败: {}", e.getMessage());
- }
- // 迁移304: health_gut_flora.bacteria_name 改为可空(LLM解析偶发缺菌种名,NOT NULL 导致采集入库失败)
- try {
- jdbcTemplate.execute("ALTER TABLE health_gut_flora MODIFY COLUMN bacteria_name VARCHAR(100) NULL COMMENT '菌种名称'");
- log.info("迁移304: health_gut_flora.bacteria_name 已改为可空");
- } catch (Exception e) {
- log.warn("迁移304: health_gut_flora.bacteria_name 修改失败: {}", e.getMessage());
- }
- // 迁移305: health_indicators.status 放宽到 VARCHAR(100)(opencode 回退解析产出自由文本 status,
- // 超长写入报 Data too long for column 'status',draft 入库失败)
- try {
- jdbcTemplate.execute("ALTER TABLE health_indicators MODIFY COLUMN status VARCHAR(100) NULL COMMENT '评估状态: 正常/偏低/偏高/过多/注意/低风险/未检出'");
- log.info("迁移305: health_indicators.status 已放宽到 VARCHAR(100)");
- } catch (Exception e) {
- log.warn("迁移305: health_indicators.status 修改失败: {}", e.getMessage());
- }
- // 迁移306: products表添加visible_roles列(商品类型权限过滤,NULL=全部角色可见)
- try {
- jdbcTemplate.execute("ALTER TABLE products ADD COLUMN visible_roles JSON DEFAULT NULL COMMENT '可见角色: [\"parent\",\"child\",\"teacher\",\"admin\"],NULL=全部可见'");
- log.info("迁移306: products.visible_roles 列已添加");
- } catch (Exception e) {
- // ignore
- }
- // 迁移307: resource_items 表添加社会连接分类字段(珍珠图重构:25 类连接类型 + 价值分级)
- try {
- jdbcTemplate.execute("ALTER TABLE resource_items ADD COLUMN connection_type VARCHAR(50) NOT NULL DEFAULT '' COMMENT '连接类型枚举名(ConnectionType)'");
- } catch (Exception e) {
- log.warn("迁移307: resource_items.connection_type 已存在或添加失败: {}", e.getMessage());
- }
- try {
- jdbcTemplate.execute("ALTER TABLE resource_items ADD COLUMN priority TINYINT DEFAULT 2 COMMENT '优先级: 1=高 2=中 3=低(手动覆盖)'");
- } catch (Exception e) {
- log.warn("迁移307: resource_items.priority 已存在或添加失败: {}", e.getMessage());
- }
- try {
- jdbcTemplate.execute("ALTER TABLE resource_items ADD COLUMN value_score INT DEFAULT 0 COMMENT '价值分 0-100(自动计算,可手动覆盖)'");
- } catch (Exception e) {
- log.warn("迁移307: resource_items.value_score 已存在或添加失败: {}", e.getMessage());
- }
- try {
- jdbcTemplate.execute("ALTER TABLE resource_items ADD COLUMN last_interaction_at DATETIME DEFAULT NULL COMMENT '最近互动时间'");
- } catch (Exception e) {
- log.warn("迁移307: resource_items.last_interaction_at 已存在或添加失败: {}", e.getMessage());
- }
- try {
- jdbcTemplate.execute("ALTER TABLE resource_items ADD COLUMN interaction_count INT DEFAULT 0 COMMENT '互动总次数'");
- } catch (Exception e) {
- log.warn("迁移307: resource_items.interaction_count 已存在或添加失败: {}", e.getMessage());
- }
- try {
- jdbcTemplate.execute("ALTER TABLE resource_items ADD INDEX idx_ri_connection_type (connection_type)");
- } catch (Exception e) {
- log.warn("迁移307: resource_items.connection_type 索引已存在或添加失败: {}", e.getMessage());
- }
- log.info("迁移307: resource_items 社会连接字段添加完成");
- // 迁移308: 创建珍珠图互动日志表
- try {
- jdbcTemplate.execute("CREATE TABLE IF NOT EXISTS pearl_interaction_log (" +
- "id BIGINT AUTO_INCREMENT PRIMARY KEY, " +
- "user_id BIGINT NOT NULL COMMENT '操作人', " +
- "resource_item_id BIGINT NOT NULL COMMENT '关联资源', " +
- "interaction_type VARCHAR(20) NOT NULL COMMENT 'PHONE/WECHAT/MEETING/OTHER', " +
- "content VARCHAR(500) DEFAULT NULL COMMENT '互动内容简记', " +
- "happened_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '互动发生时间', " +
- "created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, " +
- "INDEX idx_pil_user (user_id), " +
- "INDEX idx_pil_resource (resource_item_id), " +
- "INDEX idx_pil_happened (happened_at) " +
- ") ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='珍珠图互动日志'");
- log.info("迁移308: 已创建 pearl_interaction_log 表");
- } catch (Exception e) {
- log.warn("迁移308: 创建 pearl_interaction_log 表失败: {}", e.getMessage());
- }
- // 迁移309: 创建珍珠图互动提醒表
- try {
- jdbcTemplate.execute("CREATE TABLE IF NOT EXISTS pearl_interaction_reminder (" +
- "id BIGINT AUTO_INCREMENT PRIMARY KEY, " +
- "user_id BIGINT NOT NULL, " +
- "resource_item_id BIGINT NOT NULL, " +
- "connection_type VARCHAR(50) NOT NULL, " +
- "suggest_interval_month INT NOT NULL COMMENT '建议互动周期(月)', " +
- "last_interaction_at DATETIME DEFAULT NULL, " +
- "days_since_last INT NOT NULL COMMENT '距上次互动天数', " +
- "status VARCHAR(20) DEFAULT 'PENDING' COMMENT 'PENDING/DONE/DISMISSED', " +
- "created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, " +
- "updated_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, " +
- "INDEX idx_pir_user_status (user_id, status), " +
- "INDEX idx_pir_resource (resource_item_id) " +
- ") ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='珍珠图互动提醒'");
- log.info("迁移309: 已创建 pearl_interaction_reminder 表");
- } catch (Exception e) {
- log.warn("迁移309: 创建 pearl_interaction_reminder 表失败: {}", e.getMessage());
- }
- try {
- jdbcTemplate.execute("CREATE TABLE IF NOT EXISTS octopus_effect_record (" +
- "id BIGINT AUTO_INCREMENT PRIMARY KEY, " +
- "user_id BIGINT NOT NULL COMMENT '当前用户(租户键)', " +
- "member_order_id BIGINT COMMENT '关联 MemberSubscriptionOrder.id(会员订单)', " +
- "effect_type TINYINT UNSIGNED NOT NULL COMMENT '成效类型 bitmask:1=金额大,2=频次高,4=新成交(支持多选)', " +
- "effect_amount DECIMAL(10,2) DEFAULT 0 COMMENT '成效金额(元)', " +
- "effect_desc VARCHAR(500) COMMENT '成效描述', " +
- "status TINYINT DEFAULT 1 COMMENT '1=有效,0=无效', " +
- "created_at DATETIME DEFAULT CURRENT_TIMESTAMP, " +
- "INDEX idx_oer_user (user_id), " +
- "INDEX idx_oer_order (member_order_id) " +
- ") ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='关键人拓展-成效记录'");
- log.info("已创建octopus_effect_record表");
- } catch (Exception e) {
- log.warn("创建octopus_effect_record表失败: {}", e.getMessage());
- }
- try {
- jdbcTemplate.execute("CREATE TABLE IF NOT EXISTS octopus_key_person (" +
- "id BIGINT AUTO_INCREMENT PRIMARY KEY, " +
- "user_id BIGINT NOT NULL COMMENT '当前用户(租户键)', " +
- "name VARCHAR(64) NOT NULL COMMENT '关键人姓名', " +
- "organization VARCHAR(128) COMMENT '单位/公司', " +
- "department VARCHAR(64) COMMENT '部门', " +
- "title VARCHAR(64) COMMENT '职务/职位', " +
- "first_meet_scene VARCHAR(128) COMMENT '初识场合', " +
- "know_way VARCHAR(255) COMMENT '如何认识(认识路径)', " +
- "contact_info VARCHAR(255) COMMENT '联系方式(可选)', " +
- "created_at DATETIME DEFAULT CURRENT_TIMESTAMP, " +
- "INDEX idx_okp_user (user_id), " +
- "INDEX idx_okp_org (organization) " +
- ") ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='关键人画像'");
- log.info("已创建octopus_key_person表");
- } catch (Exception e) {
- log.warn("创建octopus_key_person表失败: {}", e.getMessage());
- }
- try {
- jdbcTemplate.execute("CREATE TABLE IF NOT EXISTS octopus_effect_key_person (" +
- "id BIGINT AUTO_INCREMENT PRIMARY KEY, " +
- "effect_id BIGINT NOT NULL COMMENT '关联 octopus_effect_record.id', " +
- "key_person_id BIGINT NOT NULL COMMENT '关联 octopus_key_person.id', " +
- "role TINYINT NOT NULL COMMENT '1=引荐,2=决策,3=其他', " +
- "created_at DATETIME DEFAULT CURRENT_TIMESTAMP, " +
- "INDEX idx_oekp_effect (effect_id), " +
- "INDEX idx_oekp_person (key_person_id), " +
- "UNIQUE KEY uk_oekp (effect_id,key_person_id) " +
- ") ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='成效-关键人关联'");
- log.info("已创建octopus_effect_key_person表");
- } catch (Exception e) {
- log.warn("创建octopus_effect_key_person表失败: {}", e.getMessage());
- }
- // 迁移310: health_meal_record 表添加 record_date(补打日期)与 is_backfill(是否补打)字段(饮食记录重设计)
- try {
- jdbcTemplate.execute("ALTER TABLE health_meal_record ADD COLUMN record_date DATE NULL COMMENT '用餐日期(补打时为实际用餐日,默认等于创建日)'");
- log.info("迁移310: health_meal_record.record_date 列已添加");
- } catch (Exception e) {
- log.warn("迁移310: health_meal_record.record_date 已存在或添加失败: {}", e.getMessage());
- }
- try {
- jdbcTemplate.execute("ALTER TABLE health_meal_record ADD COLUMN is_backfill TINYINT DEFAULT 0 COMMENT '是否补打: 1=补打(无能量值), 0=当日打卡'");
- log.info("迁移310: health_meal_record.is_backfill 列已添加");
- } catch (Exception e) {
- log.warn("迁移310: health_meal_record.is_backfill 已存在或添加失败: {}", e.getMessage());
- }
- try {
- jdbcTemplate.execute("ALTER TABLE health_meal_record ADD INDEX idx_meal_record_date (member_id, record_date)");
- log.info("迁移310: health_meal_record (member_id, record_date) 索引已添加");
- } catch (Exception e) {
- log.warn("迁移310: health_meal_record 索引已存在或添加失败: {}", e.getMessage());
- }
- // 迁移310: health_reports / health_report_drafts 补 file_hash 列
- // 实体与 schema.sql 已含该列,但建表迁移从未 ADD;upload-only 按 MD5 去重时 SELECT 报 Unknown column 'file_hash'
- ensureColumn("health_reports", "file_hash",
- "VARCHAR(64) DEFAULT NULL COMMENT '文件MD5哈希,用于重复上传检测'");
- ensureColumn("health_report_drafts", "file_hash",
- "VARCHAR(64) DEFAULT NULL COMMENT '文件MD5哈希,用于重复上传检测'");
- log.info("迁移310: 已补齐 health_reports/health_report_drafts.file_hash");
- // 迁移311: report_type_registry 补 script_status 列
- // 实体/schema 已含;异步采集指纹识别 SELECT 全字段时报 Unknown column 'script_status'
- ensureColumn("report_type_registry", "script_status",
- "VARCHAR(20) DEFAULT NULL COMMENT '采集脚本生成状态: generating/ready/failed'");
- log.info("迁移311: 已补齐 report_type_registry.script_status");
- // 迁移312: 为 gut_flora 补默认指纹规则(空表会导致指纹引擎永远 unknown)
- seedGutFloraFingerprints();
- // 迁移316-329: 原误置于 seedGutFloraFingerprints 早返回(return)之后,gut_flora 指纹存在时永不执行;
- // 现移至 runSystemMigrations 无条件执行(recommendation_rules / task_orchestration / onboarding_guide_config / 打卡表照片语音字段)
- // 迁移316: 创建 recommendation_rules 表(统一推荐规则配置)
- try {
- jdbcTemplate.execute("CREATE TABLE IF NOT EXISTS recommendation_rules (" +
- "id BIGINT AUTO_INCREMENT PRIMARY KEY, " +
- "rule_type TINYINT NOT NULL COMMENT '1-5', " +
- "rule_name VARCHAR(100) COMMENT '运营名称', " +
- "trigger_product_id BIGINT COMMENT '触发商品ID', " +
- "trigger_product_ids VARCHAR(1000) COMMENT '触发商品ID JSON 数组', " +
- "target_product_id BIGINT COMMENT '目标商品ID', " +
- "priority INT DEFAULT 10 COMMENT '优先级,越大越优', " +
- "enabled TINYINT DEFAULT 1 COMMENT '启用 1/0', " +
- "time_window_days INT COMMENT '回溯窗口(天)', " +
- "delay_days INT COMMENT '复购延迟天数', " +
- "scope VARCHAR(20) COMMENT 'family/individual', " +
- "scene_codes VARCHAR(500) COMMENT '生效场景 JSON 数组', " +
- "created_at DATETIME DEFAULT CURRENT_TIMESTAMP, " +
- "updated_at DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP" +
- ") ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='商品推荐规则配置'");
- log.info("迁移316: 已创建recommendation_rules表");
- } catch (Exception e) {
- // 表已存在,忽略错误
- }
- // 迁移317: recommendation_rules 表索引
- try {
- jdbcTemplate.execute("CREATE INDEX idx_rules_enabled ON recommendation_rules(enabled)");
- log.info("迁移317: 已创建idx_rules_enabled索引");
- } catch (Exception e) {
- // 索引已存在,忽略错误
- }
- try {
- jdbcTemplate.execute("CREATE INDEX idx_rules_scene ON recommendation_rules(scene_codes)");
- log.info("迁移317: 已创建idx_rules_scene索引");
- } catch (Exception e) {
- // 索引已存在,忽略错误
- }
- try {
- jdbcTemplate.execute("CREATE INDEX idx_rules_priority ON recommendation_rules(priority)");
- log.info("迁移317: 已创建idx_rules_priority索引");
- } catch (Exception e) {
- // 索引已存在,忽略错误
- }
- try {
- jdbcTemplate.execute("CREATE TABLE IF NOT EXISTS `task_orchestration_flows` (" +
- "`id` bigint NOT NULL AUTO_INCREMENT," +
- "`name` varchar(100) NOT NULL," +
- "`description` text," +
- "`creator_id` bigint NOT NULL," +
- "`family_id` bigint DEFAULT NULL," +
- "`version` int NOT NULL DEFAULT 1," +
- "`status` varchar(20) NOT NULL DEFAULT 'draft'," +
- "`schedule_cron` varchar(64) DEFAULT NULL," +
- "`config_json` json DEFAULT NULL," +
- "`created_at` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP," +
- "`updated_at` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP," +
- "PRIMARY KEY (`id`)," +
- "KEY `idx_family_id` (`family_id`)," +
- "KEY `idx_status` (`status`)" +
- ") ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='任务编排流定义'");
- log.info("迁移329: 已创建task_orchestration_flows表");
- } catch (Exception e) {
- log.warn("迁移329: task_orchestration_flows 已存在或创建失败: {}", e.getMessage());
- }
- try {
- jdbcTemplate.execute("CREATE TABLE IF NOT EXISTS `task_orchestration_edges` (" +
- "`id` bigint NOT NULL AUTO_INCREMENT," +
- "`flow_id` bigint NOT NULL," +
- "`from_node_id` varchar(64) NOT NULL," +
- "`to_node_id` varchar(64) NOT NULL," +
- "`edge_type` varchar(20) NOT NULL," +
- "`operator` varchar(10) NOT NULL DEFAULT 'AND'," +
- "`sort_order` int NOT NULL DEFAULT 0," +
- "`created_at` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP," +
- "PRIMARY KEY (`id`)," +
- "KEY `idx_flow_id` (`flow_id`)," +
- "UNIQUE KEY `uk_flow_edge` (`flow_id`, `from_node_id`, `to_node_id`)" +
- ") ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='任务编排流边定义'");
- log.info("迁移329: 已创建task_orchestration_edges表");
- } catch (Exception e) {
- log.warn("迁移329: task_orchestration_edges 已存在或创建失败: {}", e.getMessage());
- }
- try {
- jdbcTemplate.execute("CREATE TABLE IF NOT EXISTS `task_orchestration_executions` (" +
- "`id` bigint NOT NULL AUTO_INCREMENT," +
- "`flow_id` bigint NOT NULL," +
- "`flow_version` int NOT NULL DEFAULT 1," +
- "`family_id` bigint NOT NULL," +
- "`family_member_id` bigint NOT NULL," +
- "`trigger_source` varchar(20) NOT NULL DEFAULT 'manual'," +
- "`status` varchar(20) NOT NULL DEFAULT 'running'," +
- "`started_at` datetime NOT NULL," +
- "`finished_at` datetime DEFAULT NULL," +
- "`error_reason` varchar(255) DEFAULT NULL," +
- "`created_at` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP," +
- "`updated_at` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP," +
- "PRIMARY KEY (`id`)," +
- "KEY `idx_execution_flow` (`flow_id`)," +
- "KEY `idx_execution_status` (`status`)" +
- ") ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='任务编排流执行记录'");
- log.info("迁移329: 已创建task_orchestration_executions表");
- } catch (Exception e) {
- log.warn("迁移329: task_orchestration_executions 已存在或创建失败: {}", e.getMessage());
- }
- try {
- jdbcTemplate.execute("CREATE TABLE IF NOT EXISTS `task_orchestration_node_instances` (" +
- "`id` bigint NOT NULL AUTO_INCREMENT," +
- "`execution_id` bigint NOT NULL," +
- "`node_id` varchar(64) NOT NULL," +
- "`task_id` bigint DEFAULT NULL," +
- "`status` varchar(20) NOT NULL DEFAULT 'pending'," +
- "`generation` int NOT NULL DEFAULT 0," +
- "`condition_met_at` datetime DEFAULT NULL COMMENT '条件终止:外部API通知条件达成时间'," +
- "`loop_termination_reason` varchar(50) DEFAULT NULL," +
- "`started_at` datetime DEFAULT NULL," +
- "`completed_at` datetime DEFAULT NULL," +
- "`error_reason` varchar(255) DEFAULT NULL," +
- "`created_at` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP," +
- "`updated_at` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP," +
- "PRIMARY KEY (`id`)," +
- "UNIQUE KEY `uk_node_instance` (`execution_id`, `node_id`, `generation`)," +
- "KEY `idx_execution_id` (`execution_id`)," +
- "KEY `idx_task_id` (`task_id`)" +
- ") ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='任务编排流节点实例'");
- log.info("迁移329: 已创建task_orchestration_node_instances表");
- } catch (Exception e) {
- log.warn("迁移329: task_orchestration_node_instances 已存在或创建失败: {}", e.getMessage());
- }
- // 迁移318: sys_config 预置首页引导浮层默认配置(onboarding_guide_config key)
- try {
- String defaultConfig = "{"
- + "\"enabled\":true,"
- + "\"version\":\"1.0\","
- + "\"positions\":[\"home\",\"child\"],"
- + "\"parent\":["
- + "{\"icon\":\"🔍\",\"title\":\"你真的了解孩子的状态吗?\",\"desc\":\"情绪低落没察觉、习惯变化没注意、能力短板没发现——很多问题等爆发了才看见。浠艾福帮你在问题发生前,看清孩子真实的身心状态。\",\"isIntro\":true},"
- + "{\"icon\":\"🌏\",\"title\":\"五维沙盘,看见全家\",\"desc\":\"身·心·智·行·富,一张全景图看清每个维度。孩子身体好不好、情绪怎么样、能力到哪了——不用猜,看得见.\"},"
- + "{\"icon\":\"📋\",\"title\":\"小任务,大改变\",\"desc\":\"布置每日小任务,完成就能攒星星。每天一点点积累,好习惯自然养成.\"},"
- + "{\"icon\":\"🎁\",\"title\":\"心愿墙,看得见努力\",\"desc\":\"孩子攒够星星就能兑换心愿。努力有回报,目标感和坚持力一起培养.\"}"
- + "],"
- + "\"child\":["
- + "{\"icon\":\"🔍\",\"title\":\"你的每一点成长,都被看见\",\"desc\":\"今天完成了几个任务?比昨天进步了多少?浠艾福帮你记录每一点努力,让成长看得见。\",\"isIntro\":true},"
- + "{\"icon\":\"🌟\",\"title\":\"赚星星\",\"desc\":\"完成爸爸妈妈布置的任务,就能获得星星积分,越努力越多!\"},"
- + "{\"icon\":\"🎮\",\"title\":\"玩中学\",\"desc\":\"完成任务后可以玩小游戏、兑换心愿,学习和快乐两不误!\"},"
- + "{\"icon\":\"🏆\",\"title\":\"升级挑战\",\"desc\":\"从星星宝宝到宇宙小英雄,一步步升级,看看你能走多远!\"}"
- + "]"
- + "}";
- jdbcTemplate.update(
- "INSERT IGNORE INTO sys_config (config_key, config_value, description) VALUES ('onboarding_guide_config', ?, '首页引导浮层配置')",
- defaultConfig
- );
- log.info("迁移318: 已预置onboarding_guide_config默认配置");
- } catch (Exception e) {
- log.warn("预置onboarding_guide_config可能已存在: {}", e.getMessage());
- }
- // 迁移328: 饮食/运动打卡表补充照片与语音字段(前端已上传 media URL,补上持久化列)
- try {
- ensureColumn("health_meal_record", "photo_urls", "TEXT COMMENT '照片URL列表(逗号分隔)'");
- ensureColumn("health_meal_record", "voice_url", "VARCHAR(500) COMMENT '语音URL'");
- ensureColumn("health_meal_record", "voice_text", "TEXT COMMENT '语音转写文字'");
- ensureColumn("exercise_record", "photo_urls", "TEXT COMMENT '照片URL列表(逗号分隔)'");
- ensureColumn("exercise_record", "voice_url", "VARCHAR(500) COMMENT '语音URL'");
- ensureColumn("exercise_record", "voice_text", "TEXT COMMENT '语音转写文字'");
- log.info("迁移328: 已为health_meal_record/exercise_record补充照片语音字段");
- } catch (Exception e) {
- log.warn("迁移328: 补充照片语音字段失败: {}", e.getMessage());
- }
- // 迁移333-336: 引导弹窗配置表(guide_popup 系列,原远程版本误置于 seedGutFloraFingerprints 早返回之后,
- // 现并入 runSystemMigrations 无条件执行;编号 333-336 为本地迁移329(task_orchestration)以后的续号)
- // 迁移333: 创建 guide_popup 表(弹窗引导页配置化管理)
- try {
- jdbcTemplate.execute("CREATE TABLE IF NOT EXISTS guide_popup (" +
- "id BIGINT AUTO_INCREMENT PRIMARY KEY COMMENT '主键ID', " +
- "slug VARCHAR(64) NOT NULL UNIQUE COMMENT '唯一标识', " +
- "title VARCHAR(128) NOT NULL DEFAULT '' COMMENT '弹窗标题', " +
- "description TEXT COMMENT '主描述文案', " +
- "icon VARCHAR(256) NOT NULL DEFAULT '' COMMENT '图标:emoji或图片URL', " +
- "icon_type TINYINT NOT NULL DEFAULT 0 COMMENT '图标类型:0=emoji 1=图片', " +
- "theme_color VARCHAR(20) NOT NULL DEFAULT '#F97316' COMMENT '主题色', " +
- "btn_text VARCHAR(64) NOT NULL DEFAULT '知道了' COMMENT '按钮文案', " +
- "btn_action VARCHAR(256) NOT NULL DEFAULT '' COMMENT '点击跳转路径,空=关闭', " +
- "status TINYINT NOT NULL DEFAULT 1 COMMENT '状态:0=禁用 1=启用', " +
- "sort_order INT NOT NULL DEFAULT 0 COMMENT '排序优先级', " +
- "created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间', " +
- "updated_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间', " +
- "INDEX idx_status_sort (status, sort_order)" +
- ") ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='引导弹窗配置表'");
- log.info("迁移333: 已创建 guide_popup 表");
- } catch (Exception e) {
- log.warn("guide_popup 表可能已存在: {}", e.getMessage());
- }
- // 迁移334: 创建 guide_popup_step 表(引导弹窗步骤)
- try {
- jdbcTemplate.execute("CREATE TABLE IF NOT EXISTS guide_popup_step (" +
- "id BIGINT AUTO_INCREMENT PRIMARY KEY COMMENT '主键ID', " +
- "popup_id BIGINT NOT NULL COMMENT '关联 guide_popup.id', " +
- "step_order INT NOT NULL DEFAULT 0 COMMENT '步骤顺序', " +
- "title VARCHAR(128) NOT NULL DEFAULT '' COMMENT '步骤标题', " +
- "`desc` TEXT COMMENT '步骤描述', " +
- "icon VARCHAR(256) NOT NULL DEFAULT '' COMMENT '步骤图标', " +
- "icon_type TINYINT NOT NULL DEFAULT 0 COMMENT '图标类型:0=emoji 1=图片', " +
- "is_intro TINYINT NOT NULL DEFAULT 0 COMMENT '是否为开场身份定义页', " +
- "btn_text VARCHAR(64) NOT NULL DEFAULT '下一步' COMMENT '本步骤按钮文案', " +
- "btn_action VARCHAR(256) NOT NULL DEFAULT '' COMMENT '最后一步跳转链接', " +
- "created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间', " +
- "INDEX idx_popup_order (popup_id, step_order)" +
- ") ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='引导弹窗步骤表'");
- log.info("迁移334: 已创建 guide_popup_step 表");
- } catch (Exception e) {
- log.warn("guide_popup_step 表可能已存在: {}", e.getMessage());
- }
- // 迁移335: 创建 guide_popup_trigger 表(引导弹窗触发条件)
- try {
- jdbcTemplate.execute("CREATE TABLE IF NOT EXISTS guide_popup_trigger (" +
- "id BIGINT AUTO_INCREMENT PRIMARY KEY COMMENT '主键ID', " +
- "popup_id BIGINT NOT NULL COMMENT '关联 guide_popup.id', " +
- "trigger_type VARCHAR(32) NOT NULL COMMENT '触发类型:page_enter/task_complete/assessment_done/first_login/daily_active/role_change', " +
- "target_page VARCHAR(128) DEFAULT '' COMMENT '目标页面路径', " +
- "target_action VARCHAR(128) DEFAULT '' COMMENT '触发行为标识', " +
- "min_role VARCHAR(32) DEFAULT 'parent' COMMENT '最低角色要求', " +
- "extra_config JSON DEFAULT NULL COMMENT '扩展条件', " +
- "created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间', " +
- "INDEX idx_popup_trigger (popup_id)" +
- ") ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='引导弹窗触发条件表'");
- log.info("迁移335: 已创建 guide_popup_trigger 表");
- } catch (Exception e) {
- log.warn("guide_popup_trigger 表可能已存在: {}", e.getMessage());
- }
- // 迁移336: 创建 guide_popup_log 表(引导弹窗用户消费记录)
- try {
- jdbcTemplate.execute("CREATE TABLE IF NOT EXISTS guide_popup_log (" +
- "id BIGINT AUTO_INCREMENT PRIMARY KEY COMMENT '主键ID', " +
- "popup_id BIGINT NOT NULL COMMENT '关联 guide_popup.id', " +
- "user_id BIGINT NOT NULL COMMENT '用户ID users.id', " +
- "member_id BIGINT DEFAULT NULL COMMENT '家庭成员ID family_members.id', " +
- "role VARCHAR(32) NOT NULL COMMENT '弹出时所在角色', " +
- "consumed_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '消费时间', " +
- "button_clicked TINYINT NOT NULL DEFAULT 0 COMMENT '是否点击按钮', " +
- "INDEX idx_user_popup (user_id, popup_id), " +
- "INDEX idx_consume_time (consumed_at)" +
- ") ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='引导弹窗用户消费记录表'");
- log.info("迁移336: 已创建 guide_popup_log 表");
- } catch (Exception e) {
- log.warn("guide_popup_log 表可能已存在: {}", e.getMessage());
- }
- // 迁移323: 注册素质测评A2报告类型(dan家族)+ 默认指纹规则
- // 统一上传后指纹识别为 dan_a2 → ReportCollectService 按 report_type_registry.family=dan 分流走 DAN 采集管道
- try {
- Integer danA2Count = jdbcTemplate.queryForObject(
- "SELECT COUNT(*) FROM report_type_registry WHERE type_id = ?", Integer.class, "dan_a2");
- if (danA2Count == null || danA2Count == 0) {
- jdbcTemplate.update(
- "INSERT INTO report_type_registry (type_id, family, display_name, description, parser_path, " +
- "min_confidence, is_active, source, review_status, version, created_at, updated_at) " +
- "VALUES ('dan_a2', 'dan', '素质测评A2', '核心素养评估报告:大五人格/认知能力/社会关系等', " +
- "NULL, 30, 1, 'prebuilt', 'approved', '1.0', NOW(), NOW())");
- log.info("迁移323: 已注册报告类型 dan_a2(dan家族)");
- } else {
- log.info("迁移323: 报告类型 dan_a2 已存在,跳过");
- }
- } catch (Exception e) {
- log.warn("迁移323: 注册 dan_a2 类型失败: {}", e.getMessage());
- }
- seedDanA2Fingerprints();
- seedDiscountRulesMenu();
- createUserShortcutConfigTable();
- createUserAppUsageTable();
- seedGuidePopupData();
- addGiftAmountColumn();
- createGiftMembershipProgressTable();
- addFlowTaskColumnsToTasksTable();
- addPolicyTermsColumn();
- createHomeBannersTable();
- ensureHealthReportDraftsMemberIdColumn();
- ensureHealthPlansTotalDaysColumn();
- seedTaskReminderTemplate();
- backfillHealthPlanTaskPrerequisite();
- backfillTemplateInstanceFamilyMemberId();
- createReportTransferRequestsTable();
- addProductMinDiscountLevel();
- restoreBasicMembershipLevel();
- backfillMembershipLevelOrder();
- fixMembershipLevelOrder();
- removeReportUploadQuota();
- applyBasicMembership20261023Spec();
- createProductDimensionMappingTable();
- createProductRecommendationLogTable();
- createRepurchaseReminderTables();
- createRecommendationProductAssociationsTable();
- addAbTestColumnsToRecommendationLog();
- createTaskReminderConfigTable();
- addMissingColumnsToRecommendationLog();
- createDietFamilySelectedIngredientsTable();
- createSsoTicketTable();
- // 迁移327: 创建 report_feedback 表(报告反馈+AI解答)
- try {
- jdbcTemplate.execute("CREATE TABLE IF NOT EXISTS report_feedback (" +
- "id BIGINT AUTO_INCREMENT PRIMARY KEY, " +
- "user_id BIGINT NOT NULL COMMENT '提交人 users.id', " +
- "member_id BIGINT DEFAULT NULL COMMENT '关联家庭成员 family_members.id', " +
- "report_id BIGINT DEFAULT NULL COMMENT '健康报告ID', " +
- "draft_id BIGINT DEFAULT NULL COMMENT '草稿ID', " +
- "feedback_type VARCHAR(20) DEFAULT 'question' COMMENT 'question=疑问 / suggestion=建议', " +
- "content VARCHAR(500) NOT NULL COMMENT '反馈内容', " +
- "ai_answer TEXT COMMENT 'AI 解答', " +
- "ai_status VARCHAR(20) DEFAULT 'pending' COMMENT 'pending/answered/failed', " +
- "conversation_id VARCHAR(64) DEFAULT NULL COMMENT 'AI 会话ID(付费多轮)', " +
- "created_at DATETIME NOT NULL COMMENT '创建时间', " +
- "INDEX idx_rf_user (user_id, created_at), " +
- "INDEX idx_rf_report (report_id) " +
- ") ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='报告反馈与AI解答'");
- log.info("迁移327: 已创建 report_feedback 表");
- } catch (Exception e) {
- // 表已存在,忽略错误
- }
- // 迁移313: 修复 meal_configs 历史双编码 participant_member_ids(共餐配置前端曾 JSON.stringify 二次编码,存成了 "\"[1,2]\"",
- // 导致再次进入页面时无法识别选中成员)。仅处理以双引号包裹的值,不触碰正常 "[]..." 格式
- try {
- jdbcTemplate.execute("UPDATE meal_configs SET participant_member_ids = JSON_UNQUOTE(participant_member_ids) " +
- "WHERE participant_member_ids LIKE '\"%\"' ");
- log.info("迁移313: 已修复 meal_configs 双编码 participant_member_ids");
- } catch (Exception e) {
- log.warn("迁移313: 修复 meal_configs participant_member_ids 失败: {}", e.getMessage());
- }
- // 迁移314: meal_configs 增加 config_date 字段(当天临时共餐配置)+ 修改 UNIQUE KEY
- // 支持「今天」模式:config_date 有值=当天临时配置(优先级最高),config_date=NULL+config_date_type=工作日/周末默认配置
- try {
- jdbcTemplate.execute("ALTER TABLE meal_configs ADD COLUMN config_date DATE DEFAULT NULL COMMENT '当天临时配置日期(NULL=默认工作日/周末配置)'");
- log.info("迁移314: meal_configs 已添加 config_date 列");
- } catch (Exception e) {
- log.warn("迁移314: meal_configs 添加 config_date 列失败(可能已存在): {}", e.getMessage());
- }
- try {
- jdbcTemplate.execute("ALTER TABLE meal_configs ADD COLUMN config_date_eff DATE GENERATED ALWAYS AS (COALESCE(config_date, '9999-12-31')) STORED COMMENT '生效日期(NULL映射为9999-12-31,用于唯一约束)'");
- log.info("迁移314: meal_configs 已添加生成列 config_date_eff");
- } catch (Exception e) {
- log.warn("迁移314: meal_configs 添加 config_date_eff 生成列失败(可能已存在): {}", e.getMessage());
- }
- try {
- jdbcTemplate.execute("ALTER TABLE meal_configs DROP INDEX uk_config");
- log.info("迁移314: meal_configs 已删除旧 UNIQUE KEY uk_config");
- } catch (Exception e) {
- log.warn("迁移314: 删除旧 UNIQUE KEY 失败(可能不存在): {}", e.getMessage());
- }
- try {
- jdbcTemplate.execute("ALTER TABLE meal_configs ADD UNIQUE KEY uk_config (family_id, config_date_eff, meal_type)");
- log.info("迁移314: meal_configs 已添加新 UNIQUE KEY uk_config(支持当天临时配置)");
- } catch (Exception e) {
- log.warn("迁移314: 添加新 UNIQUE KEY 失败: {}", e.getMessage());
- }
- try {
- jdbcTemplate.execute("ALTER TABLE meal_configs ADD INDEX idx_config_date (config_date)");
- log.info("迁移314: meal_configs 已添加索引 idx_config_date");
- } catch (Exception e) {
- log.warn("迁移314: meal_configs 添加索引 idx_config_date 失败(可能已存在): {}", e.getMessage());
- }
- try {
- jdbcTemplate.execute("CREATE TABLE IF NOT EXISTS doa_stage (" +
- "id BIGINT AUTO_INCREMENT PRIMARY KEY, " +
- "family_id BIGINT NOT NULL COMMENT '家庭ID', " +
- "member_id BIGINT NOT NULL COMMENT '目标归属成员ID(family_members.id)', " +
- "title VARCHAR(200) COMMENT '阶段主题(可选)', " +
- "start_date DATE NOT NULL COMMENT '阶段开始日期', " +
- "end_date DATE NOT NULL COMMENT '阶段结束日期', " +
- "duration_weeks TINYINT NOT NULL COMMENT '周期周数:2/4/8/12', " +
- "status VARCHAR(20) DEFAULT 'active' COMMENT 'active/completed/cancelled', " +
- "created_by BIGINT NOT NULL COMMENT '创建人userId(代管时为代管人)', " +
- "created_at DATETIME DEFAULT CURRENT_TIMESTAMP, " +
- "updated_at DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, " +
- "INDEX idx_ds_family_member (family_id, member_id), " +
- "INDEX idx_ds_status (status) " +
- ") ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='DOA 阶段'");
- log.info("迁移318: 已创建 doa_stage 表");
- } catch (Exception e) {
- log.warn("迁移318: 创建 doa_stage 表失败: {}", e.getMessage());
- }
- try {
- jdbcTemplate.execute("CREATE TABLE IF NOT EXISTS doa_goal (" +
- "id BIGINT AUTO_INCREMENT PRIMARY KEY, " +
- "stage_id BIGINT NOT NULL COMMENT '所属阶段ID', " +
- "domain VARCHAR(20) NOT NULL COMMENT '领域:health/wealth/family/social/help', " +
- "content VARCHAR(500) NOT NULL COMMENT '量化目标内容', " +
- "status VARCHAR(20) DEFAULT 'active' COMMENT 'active/achieved/abandoned', " +
- "sort_order TINYINT DEFAULT 0 COMMENT '排序', " +
- "created_at DATETIME DEFAULT CURRENT_TIMESTAMP, " +
- "updated_at DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, " +
- "INDEX idx_dg_stage (stage_id) " +
- ") ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='DOA 阶段目标'");
- log.info("迁移318: 已创建 doa_goal 表");
- } catch (Exception e) {
- log.warn("迁移318: 创建 doa_goal 表失败: {}", e.getMessage());
- }
- try {
- jdbcTemplate.execute("CREATE TABLE IF NOT EXISTS doa_week (" +
- "id BIGINT AUTO_INCREMENT PRIMARY KEY, " +
- "stage_id BIGINT NOT NULL COMMENT '所属阶段ID', " +
- "week_no TINYINT NOT NULL COMMENT '第几周(1起)', " +
- "week_start DATE NOT NULL COMMENT '本周开始(周一)', " +
- "week_end DATE NOT NULL COMMENT '本周结束(周日)', " +
- "plan TEXT COMMENT '周行动宣告计划(A表)', " +
- "achieved TEXT COMMENT '复盘:本周达成了什么', " +
- "experience TEXT COMMENT '复盘:有什么体验', " +
- "worked TEXT COMMENT '复盘:行得通的', " +
- "improve TEXT COMMENT '复盘:要提升/行不通的', " +
- "next_challenge TEXT COMMENT '复盘:下周挑战自己什么', " +
- "status VARCHAR(20) DEFAULT 'pending' COMMENT 'pending/planned/reviewed', " +
- "created_at DATETIME DEFAULT CURRENT_TIMESTAMP, " +
- "updated_at DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, " +
- "UNIQUE KEY uk_dw_stage_week (stage_id, week_no), " +
- "INDEX idx_dw_stage (stage_id) " +
- ") ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='DOA 周记录'");
- log.info("迁移318: 已创建 doa_week 表");
- } catch (Exception e) {
- log.warn("迁移318: 创建 doa_week 表失败: {}", e.getMessage());
- }
- // 迁移318: 基础管理分组挂载「系统提示词」菜单(cfc-web菜单树同步)
- migrateSystemPromptMenu();
- // 迁移319: system_prompts 插入 health_plan 提示词种子(仅当不存在时)
- try {
- Integer count = jdbcTemplate.queryForObject(
- "SELECT COUNT(*) FROM system_prompts WHERE prompt_key = 'health_plan'", Integer.class);
- if (count == null || count == 0) {
- jdbcTemplate.update(
- "INSERT INTO system_prompts (prompt_key, title, content, default_content, version, status, remark) VALUES (?, ?, ?, ?, 1, 1, ?)",
- "health_plan", "健康方案生成",
- "你是一个专业的家庭健康方案规划师。根据用户的健康数据和目标,生成结构化的改善方案。\n\n" +
- "## 输出格式(必须严格遵守 JSON)\n\n" +
- "```\n" +
- "{\n" +
- " \"overview\": \"总体概述(100字以内,说明方案目标和核心策略)\",\n" +
- " \"sections\": [\n" +
- " {\n" +
- " \"key\": \"nutrition\",\n" +
- " \"title\": \"营养补充建议\",\n" +
- " \"content\": \"Markdown 格式的详细内容,包含具体产品推荐、用量、服用时间\",\n" +
- " \"items\": [{\"name\": \"产品名\", \"dosage\": \"用量\", \"timing\": \"服用时间\", \"reason\": \"推荐理由\"}]\n" +
- " },\n" +
- " {\n" +
- " \"key\": \"diet\",\n" +
- " \"title\": \"饮食建议\",\n" +
- " \"content\": \"Markdown 格式的餐饮建议,包含早餐/午餐/晚餐建议\",\n" +
- " \"items\": [{\"meal\": \"餐型\", \"food\": \"食物建议\", \"notes\": \"注意事项\"}]\n" +
- " },\n" +
- " {\n" +
- " \"key\": \"exercise\",\n" +
- " \"title\": \"运动计划\",\n" +
- " \"content\": \"Markdown 格式的运动建议,包含频率、时长、类型\",\n" +
- " \"items\": [{\"type\": \"运动类型\", \"duration\": \"时长\", \"frequency\": \"频率\", \"notes\": \"注意事项\"}]\n" +
- " }\n" +
- " ]\n" +
- "}\n" +
- "```\n\n" +
- "## 原则\n" +
- "1. 基于实际数据给出建议,不编造\n" +
- "2. 引用知识库时标注来源\n" +
- "3. 建议要具体可执行,避免空泛\n" +
- "4. 营养补充部分要具体到品牌/产品类型和用量\n" +
- "5. 严重健康问题建议咨询医生",
- "你是一个专业的家庭健康方案规划师。根据用户的健康数据和目标,生成结构化的改善方案。",
- "健康方案生成 prompt(营养+饮食+运动三段结构化输出)");
- log.info("迁移319: 已插入 health_plan 提示词种子数据");
- } else {
- log.info("迁移319: health_plan 提示词已存在,跳过");
- }
- } catch (Exception e) {
- log.warn("迁移319: 插入 health_plan 提示词种子数据失败: {}", e.getMessage());
- }
- try {
- jdbcTemplate.execute("ALTER TABLE exercise_record ADD COLUMN step_count INT COMMENT '微信运动步数'");
- log.info("迁移320: exercise_record.step_count 列已添加");
- } catch (Exception e) {
- log.warn("迁移320: exercise_record.step_count 列已存在或添加失败: {}", e.getMessage());
- }
- // 迁移321: 商品/分类可见性由「角色」改为「按会员级别」控制
- // - membership_levels.level_order: 级别数字(数字越大级别越高): FREE=1 BASIC=2 FAMILY=3 PREMIUM=4 LIFETIME=5; PROVIDER=1(非会员)
- // - products.min_visible_level: 最低可见会员级别编码(NULL=所有用户含未登录可见)
- // - shop_categories.min_visible_level: 分类最低可见会员级别编码(NULL=所有用户含未登录可见)
- ensureColumn("membership_levels", "level_order",
- "INT DEFAULT 1 COMMENT '级别数字(越大级别越高): FREE=1 BASIC=2 FAMILY=3 PREMIUM=4 LIFETIME=5; PROVIDER=1(非会员)'");
- try {
- jdbcTemplate.update("UPDATE membership_levels SET level_order = CASE level_code " +
- "WHEN 'FREE' THEN 1 WHEN 'BASIC' THEN 2 WHEN 'FAMILY' THEN 3 " +
- "WHEN 'PREMIUM' THEN 4 WHEN 'LIFETIME' THEN 5 WHEN 'PROVIDER' THEN 1 ELSE level_order END");
- log.info("迁移321: 已回填 membership_levels.level_order");
- } catch (Exception e) {
- log.warn("迁移321: 回填 membership_levels.level_order 失败: {}", e.getMessage());
- }
- ensureColumn("products", "min_visible_level",
- "VARCHAR(50) DEFAULT NULL COMMENT '最低可见会员级别编码(FREE/FAMILY/PROVIDER/PREMIUM/LIFETIME, NULL=全部可见)'");
- ensureColumn("shop_categories", "min_visible_level",
- "VARCHAR(50) DEFAULT NULL COMMENT '最低可见会员级别编码(FREE/FAMILY/PROVIDER/PREMIUM/LIFETIME, NULL=全部可见)'");
- log.info("迁移321: 已完成商品/分类按会员级别可见性改造");
- // 迁移299: 创建 article_quiz_records 表(文章阅读答题记录)
- try {
- jdbcTemplate.execute("CREATE TABLE IF NOT EXISTS article_quiz_records (" +
- "id BIGINT AUTO_INCREMENT PRIMARY KEY, " +
- "article_id BIGINT DEFAULT NULL, " +
- "user_id BIGINT DEFAULT NULL, " +
- "member_id BIGINT DEFAULT NULL, " +
- "questions TEXT, " +
- "answers TEXT, " +
- "score INT DEFAULT 0, " +
- "total_questions INT DEFAULT 0, " +
- "cf_earned INT DEFAULT 0, " +
- "energy_earned INT DEFAULT 0, " +
- "created_at DATETIME, " +
- "updated_at DATETIME, " +
- "INDEX idx_member (member_id), " +
- "INDEX idx_article (article_id)" +
- ") ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='文章答题记录表'");
- log.info("已创建article_quiz_records表");
- } catch (Exception e) {
- log.warn("创建article_quiz_records表可能已存在: {}", e.getMessage());
- }
- // 迁移300: profile_snapshot 增加 quiz_summary 列(答题认知雷达汇总)
- ensureColumn("profile_snapshot", "quiz_summary", "JSON DEFAULT NULL COMMENT '答题认知雷达汇总'");
- // 迁移323: dan_assessment_results 添加 language_score 列(游戏化认知测评:语言表达维度)
- ensureColumn("dan_assessment_results", "language_score", "INT COMMENT '语言表达得分'");
- log.info("迁移323: 已确保 language_score 列存在于 dan_assessment_results");
- // 迁移324-325: octopus_effect_record 添加 happened_at 列 + article_reading_records 回填与补列
- try {
- jdbcTemplate.execute("ALTER TABLE octopus_effect_record ADD COLUMN happened_at DATETIME COMMENT '成效发生时间'");
- log.info("迁移324: octopus_effect_record.happened_at 列已添加");
- } catch (Exception e) {
- log.warn("迁移324: octopus_effect_record.happened_at 列已存在或添加失败: {}", e.getMessage());
- }
- try {
- int updated = jdbcTemplate.update(
- "UPDATE article_reading_records r " +
- "LEFT JOIN tasks t ON r.task_id = t.id AND t.source_type = 'article' " +
- "SET r.article_id = t.source_id " +
- "WHERE r.article_id IS NULL AND t.id IS NOT NULL AND t.source_id IS NOT NULL"
- );
- log.info("迁移325: 已回填 article_reading_records.article_id {} 条记录", updated);
- } catch (Exception e) {
- log.warn("迁移325: 回填 article_reading_records.article_id 失败: {}", e.getMessage());
- }
- ensureColumn("article_reading_records", "reading_completed", "TINYINT DEFAULT 0 COMMENT '是否已阅读达标(达到最低时长阈值)'");
- // 迁移342: octopus_key_person 添加 first_meet_time / referrer_id / system_user_id 列(感恩记录新增字段)
- ensureColumn("octopus_key_person", "first_meet_time", "DATETIME COMMENT '相识具体时间'");
- log.info("迁移342: octopus_key_person.first_meet_time 列已添加");
- ensureColumn("octopus_key_person", "referrer_id", "BIGINT DEFAULT NULL COMMENT '介绍人关键人ID(指向同用户另一条关键人)'");
- log.info("迁移342: octopus_key_person.referrer_id 列已添加");
- ensureColumn("octopus_key_person", "system_user_id", "BIGINT DEFAULT NULL COMMENT '匹配到的系统用户ID(如有)'");
- log.info("迁移342: octopus_key_person.system_user_id 列已添加");
- try {
- Integer reportCount = jdbcTemplate.queryForObject(
- "SELECT COUNT(*) FROM admin_task_templates WHERE title = 'REPORT_UPLOAD' AND onboarding_sequence IS NOT NULL", Integer.class);
- if (reportCount == null || reportCount == 0) {
- jdbcTemplate.execute("INSERT INTO admin_task_templates (title, description, points, category, need_review, is_active, task_type, frequency, onboarding_sequence, created_at, updated_at) VALUES " +
- "('REPORT_UPLOAD', '完成首次报告上传', 300, '成长', 0, 1, 'onetime', 'none', 8, NOW(), NOW())");
- log.info("迁移343: 已插入完成首次报告上传新人引导任务模板");
- }
- } catch (Exception e) {
- log.warn("迁移343: 插入完成首次报告上传任务模板失败或已存在: {}", e.getMessage());
- }
- }
- /**
- * 迁移318: 系统配置·基础管理 挂载 系统提示词 菜单(幂等)
- * 仅当 sys_menu 表已有数据(即角色授权已存在)时补授权,避免破坏兼容逻辑
- */
- private void migrateSystemPromptMenu() {
- try {
- Long baseManageId = queryMenuId("SELECT id FROM sys_menu WHERE title='基础管理'");
- if (baseManageId == null) {
- log.warn("迁移318: 未找到基础管理分组,跳过");
- return;
- }
- Long promptId = insertSystemPromptMenu(baseManageId);
- if (promptId != null) {
- Integer grantCount = jdbcTemplate.queryForObject("SELECT COUNT(*) FROM sys_role_menu", Integer.class);
- if (grantCount != null && grantCount > 0) {
- grantMenuToRole("admin", promptId);
- log.info("迁移318: 已授权 admin 角色");
- }
- log.info("迁移318: 系统提示词菜单挂载完成 id={}", promptId);
- }
- } catch (Exception e) {
- log.warn("迁移318失败: {}", e.getMessage());
- }
- }
- /**
- * 按 path 幂等插入系统提示词叶子菜单,返回菜单 id(已存在返回原 id)
- */
- private Long insertSystemPromptMenu(Long parentId) {
- Long existed = queryMenuId("SELECT id FROM sys_menu WHERE path='/system-prompt'");
- if (existed != null) {
- return existed;
- }
- try {
- jdbcTemplate.update(
- "INSERT INTO sys_menu (parent_id, title, label, path, icon, perm, sort, visible) VALUES (?, NULL, '系统提示词', '/system-prompt', 'el-icon-chat-line-square', 'system:base', 8, 1)",
- parentId);
- Long id = jdbcTemplate.queryForObject("SELECT LAST_INSERT_ID()", Long.class);
- log.info("迁移318: 已挂载系统提示词菜单");
- return id;
- } catch (Exception e) {
- log.warn("迁移318: 挂载菜单失败: {}", e.getMessage());
- return null;
- }
- }
- private void seedGutFloraFingerprints() {
- try {
- Integer count = jdbcTemplate.queryForObject(
- "SELECT COUNT(*) FROM report_fingerprint WHERE type_id = ?", Integer.class, "gut_flora");
- if (count != null && count > 0) {
- log.info("迁移312: gut_flora 指纹规则已存在,跳过");
- return;
- }
- String[][] rows = {
- {"肠道菌群检测", "20", "0", "1"},
- {"核心菌属", "15", "0", "2"},
- {"高通量测序", "15", "0", "3"},
- {"菌群健康", "10", "0", "4"},
- {"益生菌", "8", "0", "5"},
- {"Shannon", "8", "0", "6"},
- {"肠道菌群", "10", "0", "7"},
- };
- for (String[] row : rows) {
- jdbcTemplate.update(
- "INSERT INTO report_fingerprint (type_id, pattern, weight, is_exclusion, sort_order, created_at) "
- + "VALUES ('gut_flora', ?, ?, ?, ?, NOW())",
- row[0], Integer.valueOf(row[1]), Integer.valueOf(row[2]), Integer.valueOf(row[3]));
- }
- log.info("迁移312: 已写入 gut_flora 默认指纹规则 {} 条", rows.length);
- } catch (Exception e) {
- log.warn("迁移312: 写入 gut_flora 指纹规则失败: {}", e.getMessage());
- }
- }
- /**
- * 迁移323: 为 dan_a2(素质测评A2)补默认指纹规则。
- * 空表会导致指纹引擎永远 unknown;A2 报告特征词取自 DanReportParseService.parseA2Report 的章节识别词。
- * dan_a2 注册表 min_confidence=30,两高权重词同时命中即判定成功,避免单词误判。
- */
- private void seedDanA2Fingerprints() {
- try {
- Integer count = jdbcTemplate.queryForObject(
- "SELECT COUNT(*) FROM report_fingerprint WHERE type_id = ?", Integer.class, "dan_a2");
- if (count != null && count > 0) {
- log.info("迁移323: dan_a2 指纹规则已存在,跳过");
- return;
- }
- String[][] rows = {
- {"大五人格", "20", "0", "1"},
- {"人格特质", "15", "0", "2"},
- {"核心素养", "15", "0", "3"},
- {"认知能力", "10", "0", "4"},
- {"感知觉", "10", "0", "5"},
- {"评估报告", "8", "0", "6"},
- {"综合评估", "8", "0", "7"},
- };
- for (String[] row : rows) {
- jdbcTemplate.update(
- "INSERT INTO report_fingerprint (type_id, pattern, weight, is_exclusion, sort_order, created_at) "
- + "VALUES ('dan_a2', ?, ?, ?, ?, NOW())",
- row[0], Integer.valueOf(row[1]), Integer.valueOf(row[2]), Integer.valueOf(row[3]));
- }
- log.info("迁移323: 已写入 dan_a2 默认指纹规则 {} 条", rows.length);
- } catch (Exception e) {
- log.warn("迁移323: 写入 dan_a2 指纹规则失败: {}", e.getMessage());
- }
- }
- // 迁移326: 商城营销菜单新增「限时折扣管理」(幂等)
- private void seedDiscountRulesMenu() {
- try {
- Integer count = jdbcTemplate.queryForObject(
- "SELECT COUNT(*) FROM sys_menu WHERE path = '/discount-rules'", Integer.class);
- if (count != null && count > 0) {
- log.info("迁移326: 限时折扣管理菜单已存在,跳过");
- return;
- }
- jdbcTemplate.update(
- "INSERT INTO sys_menu (parent_id, title, label, path, icon, perm, sort, visible, created_at) "
- + "VALUES (15, NULL, '限时折扣管理', '/discount-rules', 'el-icon-timer', 'commerce:discount', 7, 1, NOW())");
- log.info("迁移326: 已添加限时折扣管理菜单");
- } catch (Exception e) {
- log.warn("迁移326: 添加限时折扣管理菜单失败: {}", e.getMessage());
- }
- }
- // 迁移327: 创建 user_shortcut_config 表(首页快捷方式自定义配置,每应用一条记录,locked=置顶)
- private void createUserShortcutConfigTable() {
- try {
- jdbcTemplate.execute("CREATE TABLE IF NOT EXISTS user_shortcut_config (" +
- "id BIGINT AUTO_INCREMENT PRIMARY KEY, " +
- "user_id BIGINT NOT NULL COMMENT '用户ID', " +
- "app_key VARCHAR(64) NOT NULL COMMENT '应用key', " +
- "locked TINYINT DEFAULT 0 COMMENT '是否锁定置顶(1=锁定,0=普通)', " +
- "sort_order INT DEFAULT 0 COMMENT '手动排序序号', " +
- "created_at DATETIME DEFAULT CURRENT_TIMESTAMP, " +
- "updated_at DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, " +
- "UNIQUE KEY uk_user_app (user_id, app_key)" +
- ") ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='首页快捷方式自定义配置'");
- log.info("迁移327: 已创建 user_shortcut_config 表");
- } catch (Exception e) {
- log.warn("迁移327: 创建 user_shortcut_config 表失败: {}", e.getMessage());
- }
- }
- // 迁移327: 创建 user_app_usage 表(应用使用频度记录)
- private void createUserAppUsageTable() {
- try {
- jdbcTemplate.execute("CREATE TABLE IF NOT EXISTS user_app_usage (" +
- "id BIGINT AUTO_INCREMENT PRIMARY KEY, " +
- "user_id BIGINT NOT NULL COMMENT '用户ID', " +
- "app_key VARCHAR(64) NOT NULL COMMENT '应用key', " +
- "enter_count INT DEFAULT 0 COMMENT '累计进入次数', " +
- "last_enter_at DATETIME COMMENT '最近进入时间', " +
- "UNIQUE KEY uk_user_app (user_id, app_key)" +
- ") ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='应用使用频度记录'");
- log.info("迁移327: 已创建 user_app_usage 表");
- } catch (Exception e) {
- log.warn("迁移327: 创建 user_app_usage 表失败: {}", e.getMessage());
- }
- }
- // 迁移333: 预置引导弹窗种子数据(parent_first_login,首次登录家长端)
- private void seedGuidePopupData() {
- try {
- Long count = jdbcTemplate.queryForObject(
- "SELECT COUNT(*) FROM guide_popup WHERE slug = 'parent_first_login'", Long.class);
- if (count != null && count > 0L) {
- log.info("迁移333: parent_first_login 种子弹窗已存在,跳过");
- return;
- }
- jdbcTemplate.update(
- "INSERT INTO guide_popup (slug, title, description, icon, icon_type, theme_color, btn_text, btn_action, status, sort_order, created_at, updated_at) "
- + "VALUES ('parent_first_login', '欢迎使用浠艾福', '身·智·富·行·心,全家健康一站式管理', '🌏', 0, '#F97316', '开始体验', '', 1, 0, NOW(), NOW())");
- Long popupId = jdbcTemplate.queryForObject("SELECT LAST_INSERT_ID()", Long.class);
- // 步骤
- jdbcTemplate.update("INSERT INTO guide_popup_step (popup_id, step_order, title, desc, icon, icon_type, is_intro, btn_text, btn_action) VALUES (?, 0, '你真的了解家人的状态吗?', '情绪低落没察觉、习惯变化没注意、能力短板没发现——什么问题等爆发了才看见。浠艾福帮你在问题发生前,看清每位家人的真实状态。', '🔍', 0, 1, '下一步', '')", popupId);
- jdbcTemplate.update("INSERT INTO guide_popup_step (popup_id, step_order, title, desc, icon, icon_type, is_intro, btn_text, btn_action) VALUES (?, 1, '五维能量,看见全家', '身·心·智·行·富,一张全景图看清每位家人。谁的身体需要关注、谁的情绪需要疏导、谁的状态正在下滑——不用猜,看得见。', '🌏', 0, 0, '下一步', '')", popupId);
- jdbcTemplate.update("INSERT INTO guide_popup_step (popup_id, step_order, title, desc, icon, icon_type, is_intro, btn_text, btn_action) VALUES (?, 2, '小行动,大改变', '全家一起完成每日小任务,各自攒星星。每天一点点积累,好习惯自然养成。', '📋', 0, 0, '下一步', '')", popupId);
- jdbcTemplate.update("INSERT INTO guide_popup_step (popup_id, step_order, title, desc, icon, icon_type, is_intro, btn_text, btn_action) VALUES (?, 3, '心愿墙,看得见努力', '每位家人用自己的星星兑换心愿。努力有回报,全家人都能看到彼此的坚持。', '🎁', 0, 0, '开始体验', '')", popupId);
- // 触发条件:首次登录,parent 角色
- jdbcTemplate.update("INSERT INTO guide_popup_trigger (popup_id, trigger_type, target_page, target_action, min_role, extra_config, created_at) VALUES (?, 'first_login', '', '', 'parent', NULL, NOW())", popupId);
- log.info("迁移333: 已预置 parent_first_login 种子弹窗 (popup_id={})", popupId);
- } catch (Exception e) {
- log.warn("预置引导弹窗种子数据失败: {}", e.getMessage());
- }
- }
- // 迁移337: product_orders 表添加 gift_amount 列(送会员商品原价金额,分)
- private void addGiftAmountColumn() {
- ensureColumn("product_orders", "gift_amount", "INT DEFAULT 0 COMMENT '送会员商品原价金额(分)'");
- }
- // 迁移338: 创建 gift_membership_progress 表(家庭维度送会员累计进度)
- private void createGiftMembershipProgressTable() {
- try {
- jdbcTemplate.execute("CREATE TABLE IF NOT EXISTS gift_membership_progress (" +
- "id BIGINT AUTO_INCREMENT PRIMARY KEY, " +
- "family_id BIGINT NOT NULL COMMENT '家庭ID', " +
- "accumulated_amount INT DEFAULT 0 COMMENT '累计送会员商品原价金额(分)', " +
- "granted INT DEFAULT 0 COMMENT '是否已赠送 1=已赠送 0=未赠送', " +
- "granted_level VARCHAR(50) COMMENT '已赠送会员等级', " +
- "granted_order_no VARCHAR(64) COMMENT '触发赠送的订单号', " +
- "granted_at DATETIME COMMENT '赠送时间', " +
- "created_at DATETIME DEFAULT CURRENT_TIMESTAMP, " +
- "updated_at DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, " +
- "UNIQUE KEY uk_family (family_id)" +
- ") ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='送会员累计进度'");
- log.info("迁移338: 已创建 gift_membership_progress 表");
- } catch (Exception e) {
- log.warn("迁移338: 创建 gift_membership_progress 表失败: {}", e.getMessage());
- }
- }
- // 迁移339: tasks 表加 is_flow_task / successor_task_ids 列(TASK-SYS-248 流程任务前置/后置逻辑)
- private void addFlowTaskColumnsToTasksTable() {
- ensureColumn("tasks", "is_flow_task", "TINYINT DEFAULT 0 COMMENT '是否为流程任务:1=是(有前置/后置任务),0=否'");
- ensureColumn("tasks", "successor_task_ids", "TEXT NULL COMMENT '后置任务IDs JSON: [1,2,3],任意一个完成则解锁(OR 逻辑)'");
- }
- // 迁移339: insurance_policies 表加 policy_terms 列(保单原始条款,AI 解析后原样保存)
- private void addPolicyTermsColumn() {
- ensureColumn("insurance_policies", "policy_terms", "TEXT COMMENT '保单原始条款(AI 解析后原样保存)'");
- }
- // 迁移340: 创建 home_banners 表(首页广告栏配置)
- private void createHomeBannersTable() {
- try {
- jdbcTemplate.execute("CREATE TABLE IF NOT EXISTS home_banners (" +
- "id BIGINT AUTO_INCREMENT PRIMARY KEY COMMENT '主键ID'," +
- "title VARCHAR(100) NOT NULL COMMENT '展示标题'," +
- "banner_type VARCHAR(20) NOT NULL COMMENT '类型: activity/product/link'," +
- "ref_id BIGINT DEFAULT NULL COMMENT '关联活动/商品ID(banner_type=activity/product时有效)'," +
- "cover_image VARCHAR(500) DEFAULT NULL COMMENT '封面图URL(可覆盖默认封面)'," +
- "link_url VARCHAR(500) DEFAULT NULL COMMENT '跳转链接(banner_type=link时有效)'," +
- "sort_order INT NOT NULL DEFAULT 0 COMMENT '排序,值越小越靠前'," +
- "enabled TINYINT NOT NULL DEFAULT 1 COMMENT '启用状态:1=启用 0=禁用'," +
- "start_time DATETIME DEFAULT NULL COMMENT '投放开始时间(NULL=不限)'," +
- "end_time DATETIME DEFAULT NULL COMMENT '投放结束时间(NULL=不限)'," +
- "created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间'," +
- "updated_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间'" +
- ") ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='首页广告栏配置表'");
- log.info("迁移340: 已创建 home_banners 表");
- } catch (Exception e) {
- log.warn("迁移340: 创建 home_banners 表失败: {}", e.getMessage());
- }
- }
- /** 迁移343: health_report_drafts 补 member_id 列(commit 21f8f6740 遗漏,schema.sql 已含但无迁移脚本) */
- /** 迁移345: health_plans 补 total_days 列(方案周期天数,驱动服务任务周期边界与截止日期) */
- private void ensureHealthPlansTotalDaysColumn() {
- ensureColumn("health_plans", "total_days",
- "INT DEFAULT NULL COMMENT '方案周期天数,驱动服务任务周期边界与截止日期'");
- log.info("迁移345: health_plans.total_days 已确保存在");
- }
- /** 迁移346: 未打卡任务提醒通知模板(原有种子仅在空表时插入,此处单独幂等补种) */
- /** 迁移346: 未打卡任务提醒通知模板(原有种子仅在空表时插入,此处单独幂等补种) */
- private void seedTaskReminderTemplate() {
- try {
- Integer cnt = jdbcTemplate.queryForObject(
- "SELECT COUNT(*) FROM notification_templates WHERE type = ?",
- Integer.class, "task_reminder");
- if (cnt != null && cnt == 0) {
- jdbcTemplate.update("INSERT INTO notification_templates (type, title, content, channels) VALUES (?,?,?,?)",
- "task_reminder",
- "任务待完成提醒",
- "任务「{title}」{status},完成后记得打卡,可获得积分与 CF 值奖励。",
- "in_app,sms");
- log.info("迁移346: task_reminder 通知模板已插入");
- }
- } catch (Exception e) {
- log.warn("迁移346: 插入 task_reminder 通知模板失败: " + e.getMessage());
- }
- try {
- Integer c2 = jdbcTemplate.queryForObject(
- "SELECT COUNT(*) FROM notification_templates WHERE type = ?",
- Integer.class, "task_completion_praise");
- if (c2 != null && c2 == 0) {
- jdbcTemplate.update("INSERT INTO notification_templates (type, title, content, channels) VALUES (?,?,?,?)",
- "task_completion_praise",
- "任务完成,太棒了!",
- "太棒了!任务「{title}」已完成,继续保持,加油~",
- "in_app,wechat");
- log.info("迁移346+: task_completion_praise 通知模板已插入");
- }
- } catch (Exception e) {
- log.warn("迁移346+: 插入 task_completion_praise 通知模板失败: " + e.getMessage());
- }
- }
- /**
- * 迁移347: 回填健康方案任务的依赖链。
- * 历史上使用任务只写了 parent_task_id,从未写 prerequisite_task_id,导致"采购完成才激活服务任务"的闸门失效。
- * 这里补写依赖并把前置未完成的使用任务置为 locked。
- * 幂等:每次启动只处理 prerequisite IS NULL / status=pending 的行,可重复执行。
- */
- /**
- * 迁移348: 回填周期实例的 family_member_id。
- * RepeatTaskGenerator 过去未透传该字段,导致周期实例在"今日任务"列表查不到、
- * 且 completeTask 抛 NPE。存量实例按其模板回填,模板缺失时按 child_id 兜底。
- */
- private void backfillTemplateInstanceFamilyMemberId() {
- try {
- int fromTpl = jdbcTemplate.update(
- "UPDATE tasks i JOIN tasks tpl ON tpl.id = i.template_id " +
- "SET i.family_member_id = tpl.family_member_id " +
- "WHERE i.family_member_id IS NULL AND tpl.family_member_id IS NOT NULL");
- int fromChild = jdbcTemplate.update(
- "UPDATE tasks SET family_member_id = child_id " +
- "WHERE family_member_id IS NULL AND child_id IS NOT NULL AND is_template = 0");
- if (fromTpl > 0 || fromChild > 0) {
- log.info("迁移348: 周期实例 family_member_id 回填 模板来源={}条 child_id兜底={}条",
- fromTpl, fromChild);
- }
- } catch (Exception e) {
- log.warn("迁移348: 周期实例 family_member_id 回填失败: " + e.getMessage());
- }
- }
- private void backfillHealthPlanTaskPrerequisite() {
- try {
- int linked = jdbcTemplate.update(
- "UPDATE tasks SET prerequisite_task_id = parent_task_id " +
- "WHERE source_type = 'health_plan' AND category = '使用任务' " +
- "AND parent_task_id IS NOT NULL AND prerequisite_task_id IS NULL");
- // 注意:单表 UPDATE 的 WHERE 子查询不能引用目标表本身(MySQL 1093),必须用 LEFT JOIN 自连接
- int locked = jdbcTemplate.update(
- "UPDATE tasks t LEFT JOIN tasks p " +
- "ON p.id = t.prerequisite_task_id AND p.status = 'completed' " +
- "SET t.status = 'locked' " +
- "WHERE t.source_type = 'health_plan' AND t.category = '使用任务' " +
- "AND t.prerequisite_task_id IS NOT NULL AND t.status = 'pending' AND p.id IS NULL");
- if (linked > 0 || locked > 0) {
- log.info("迁移347: 健康方案任务依赖回填 prerequisite={}条, 置locked={}条", linked, locked);
- }
- } catch (Exception e) {
- log.warn("迁移347: 健康方案任务依赖回填失败: " + e.getMessage());
- }
- }
- private void ensureHealthReportDraftsMemberIdColumn() {
- ensureColumn("health_report_drafts", "member_id",
- "BIGINT DEFAULT NULL COMMENT '报告归属成员ID(family_members.id),A2/DAN自动采集必需'");
- log.info("迁移343: health_report_drafts.member_id 已确保存在");
- }
- // 迁移344: 创建 supply_customer_relation 表(供应商团队客户归属记录)
- private void createSupplyCustomerRelationTable() {
- try {
- jdbcTemplate.execute("CREATE TABLE IF NOT EXISTS supply_customer_relation (" +
- "id BIGINT AUTO_INCREMENT PRIMARY KEY COMMENT '主键ID'," +
- "system_id BIGINT NOT NULL COMMENT '供应商体系ID(supply_system.id)'," +
- "parent_user_id BIGINT NOT NULL COMMENT '上级供应商用户ID(supply_hierarchy.parent_id)'," +
- "family_id BIGINT NOT NULL COMMENT '客户家庭ID(family_members.family_id)'," +
- "child_user_id BIGINT DEFAULT NULL COMMENT '下单用户ID(users.id),可为空'," +
- "relation_type VARCHAR(32) NOT NULL DEFAULT 'family' COMMENT '归属类型: family=家庭归属 order=下单自动 referral=推荐码'," +
- "source VARCHAR(32) NOT NULL DEFAULT 'manual' COMMENT '来源: manual=人工绑定 order=下单自动 referral=推荐码'," +
- "created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间'," +
- "INDEX idx_system_family (system_id, family_id)," +
- "INDEX idx_parent_user (parent_user_id)," +
- "INDEX idx_family_id (family_id)," +
- "UNIQUE KEY uk_system_family (system_id, family_id)" +
- ") ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='供应商团队客户归属关系表'");
- log.info("迁移344: 已创建 supply_customer_relation 表");
- } catch (Exception e) {
- log.warn("迁移344: 创建 supply_customer_relation 表失败: {}", e.getMessage());
- }
- }
- // 迁移345: 创建 product_dimension_mapping 表(商品-维度关联)
- private void createProductDimensionMappingTable() {
- try {
- jdbcTemplate.execute("CREATE TABLE IF NOT EXISTS product_dimension_mapping (" +
- "id BIGINT AUTO_INCREMENT PRIMARY KEY COMMENT '主键ID'," +
- "product_id BIGINT NOT NULL COMMENT '商品ID(products.id)'," +
- "dimension_code VARCHAR(32) NOT NULL COMMENT '维度编码: body/wisdom/mind/action/wealth'," +
- "match_score INT DEFAULT 100 COMMENT '匹配度 0-100'," +
- "match_reason VARCHAR(200) COMMENT '匹配原因说明'," +
- "tags VARCHAR(500) COMMENT '推荐标签JSON'," +
- "enabled TINYINT DEFAULT 1 COMMENT '启用状态 1=启用 0=禁用'," +
- "created_at DATETIME DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间'," +
- "updated_at DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间'," +
- "INDEX idx_product_id (product_id)," +
- "INDEX idx_dimension (dimension_code)" +
- ") ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='商品维度关联表'");
- log.info("迁移345: 已创建 product_dimension_mapping 表");
- } catch (Exception e) {
- log.warn("迁移345: 创建 product_dimension_mapping 表失败: {}", e.getMessage());
- }
- }
- // 迁移346: 创建 product_recommendation_log 表(推荐曝光日志)
- private void createProductRecommendationLogTable() {
- try {
- jdbcTemplate.execute("CREATE TABLE IF NOT EXISTS product_recommendation_log (" +
- "id BIGINT AUTO_INCREMENT PRIMARY KEY COMMENT '主键ID'," +
- "user_id BIGINT DEFAULT NULL COMMENT '用户ID(users.id)'," +
- "family_id BIGINT DEFAULT NULL COMMENT '家庭ID(family_members.family_id)'," +
- "product_id BIGINT NOT NULL COMMENT '商品ID'," +
- "scene VARCHAR(32) NOT NULL COMMENT '场景: dimension_page/ai_chat/report_upload/repurchase'," +
- "reason VARCHAR(200) COMMENT '推荐理由'," +
- "match_score INT DEFAULT NULL COMMENT '匹配分'," +
- "was_clicked TINYINT DEFAULT 0 COMMENT '是否点击 1=是 0=否'," +
- "was_purchased TINYINT DEFAULT 0 COMMENT '是否购买 1=是 0=否'," +
- "clicked_at DATETIME DEFAULT NULL COMMENT '点击时间'," +
- "purchased_at DATETIME DEFAULT NULL COMMENT '购买时间'," +
- "created_at DATETIME DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间'," +
- "INDEX idx_user_scene (user_id, scene)," +
- "INDEX idx_product_purchased (product_id, was_purchased)" +
- ") ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='推荐曝光日志表'");
- log.info("迁移346: 已创建 product_recommendation_log 表");
- } catch (Exception e) {
- log.warn("迁移346: 创建 product_recommendation_log 表失败: {}", e.getMessage());
- }
- }
- // 迁移347: 创建 repurchase_reminder_record 和 repurchase_reminder_config 表
- private void createRepurchaseReminderTables() {
- try {
- jdbcTemplate.execute("CREATE TABLE IF NOT EXISTS repurchase_reminder_record (" +
- "id BIGINT AUTO_INCREMENT PRIMARY KEY COMMENT '主键ID'," +
- "user_id BIGINT NOT NULL COMMENT '用户ID(users.id)'," +
- "product_id BIGINT NOT NULL COMMENT '商品ID'," +
- "order_id BIGINT DEFAULT NULL COMMENT '关联订单ID'," +
- "reminder_days INT DEFAULT 30 COMMENT '提醒天数'," +
- "sent_at DATETIME DEFAULT NULL COMMENT '发送时间'," +
- "clicked TINYINT DEFAULT 0 COMMENT '是否点击 1=是 0=否'," +
- "purchased TINYINT DEFAULT 0 COMMENT '是否购买 1=是 0=否'," +
- "INDEX idx_user_pending (user_id, purchased)" +
- ") ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='复购提醒记录表'");
- log.info("迁移347a: 已创建 repurchase_reminder_record 表");
- } catch (Exception e) {
- log.warn("迁移347a: 创建 repurchase_reminder_record 表失败: {}", e.getMessage());
- }
- try {
- jdbcTemplate.execute("CREATE TABLE IF NOT EXISTS repurchase_reminder_config (" +
- "id BIGINT AUTO_INCREMENT PRIMARY KEY COMMENT '主键ID'," +
- "product_category VARCHAR(100) DEFAULT NULL COMMENT '商品类目'," +
- "product_id BIGINT DEFAULT NULL COMMENT '特定商品ID(优先于category)'," +
- "reminder_days INT DEFAULT 30 COMMENT '提醒天数'," +
- "reminder_template VARCHAR(500) DEFAULT NULL COMMENT '提醒话术模板'," +
- "max_reminders INT DEFAULT 3 COMMENT '最大提醒次数'," +
- "enabled TINYINT DEFAULT 1 COMMENT '启用状态 1=启用 0=禁用'," +
- "created_at DATETIME DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间'" +
- ") ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='复购提醒配置表'");
- log.info("迁移347b: 已创建 repurchase_reminder_config 表");
- } catch (Exception e) {
- log.warn("迁移347b: 创建 repurchase_reminder_config 表失败: {}", e.getMessage());
- }
- }
- // 迁移348: 创建 recommendation_product_associations 表(商品推荐关联关系)
- private void createRecommendationProductAssociationsTable() {
- try {
- jdbcTemplate.execute("CREATE TABLE IF NOT EXISTS recommendation_product_associations (" +
- "id BIGINT AUTO_INCREMENT PRIMARY KEY COMMENT '主键ID'," +
- "source_product_id BIGINT NOT NULL COMMENT '源商品ID(已购/关注商品)'," +
- "target_product_id BIGINT NOT NULL COMMENT '目标商品ID(被推荐商品)'," +
- "association_type VARCHAR(32) NOT NULL COMMENT '类型: co_purchase/repurchase/profile_gap/manual'," +
- "strength DECIMAL(5,2) DEFAULT 0.50 COMMENT '关联强度 0.00-1.00'," +
- "manual_override TINYINT DEFAULT 0 COMMENT '是否运营手动覆盖 1=是 0=否'," +
- "reason VARCHAR(500) DEFAULT NULL COMMENT '关联原因说明'," +
- "dimension_code VARCHAR(32) DEFAULT NULL COMMENT '关联维度(body/mind/wisdom/action/wealth)'," +
- "repurchase_days INT DEFAULT NULL COMMENT '复购周期天数(repurchase场景)'," +
- "co_purchase_window_days INT DEFAULT 30 COMMENT '共购时间窗口天数(co_purchase场景)'," +
- "co_purchase_count INT DEFAULT 0 COMMENT '共购次数(co_purchase场景自动累计)'," +
- "enabled TINYINT DEFAULT 1 COMMENT '启用状态 1=启用 0=禁用'," +
- "created_by BIGINT DEFAULT NULL COMMENT '创建人用户ID(manual场景记录运营)'," +
- "created_at DATETIME DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间'," +
- "updated_at DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间'," +
- "INDEX idx_source_target (source_product_id, target_product_id)," +
- "INDEX idx_type_enabled (association_type, enabled)," +
- "INDEX idx_dimension (dimension_code)" +
- ") ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='商品推荐关联关系表'");
- log.info("迁移348: 已创建 recommendation_product_associations 表");
- } catch (Exception e) {
- log.warn("迁移348: 创建 recommendation_product_associations 表失败: {}", e.getMessage());
- }
- }
- // 迁移349: product_recommendation_log 表添加 AB测试分组与推荐来源类型列
- private void addAbTestColumnsToRecommendationLog() {
- ensureColumn("product_recommendation_log", "ab_test_group",
- "VARCHAR(32) DEFAULT NULL COMMENT 'AB测试分组(A/B/C/控制组)'");
- ensureColumn("product_recommendation_log", "recommendation_type",
- "VARCHAR(32) DEFAULT NULL COMMENT '推荐来源类型:dimension/co_purchase/repurchase/profile_gap/manual'");
- log.info("迁移349: product_recommendation_log 已添加 ab_test_group / recommendation_type 列");
- }
- // 迁移349: 创建 report_transfer_requests 表(健康报告跨家庭转移申请)
- // 同一份报告(file_hash 相同)被其他家庭上传时,申请方家庭发起转移申请,
- // 原归属家庭同意后 health_reports.family_id 转移至申请方,原家庭不再可见。
- private void createReportTransferRequestsTable() {
- try {
- jdbcTemplate.execute("CREATE TABLE IF NOT EXISTS report_transfer_requests (" +
- "id BIGINT AUTO_INCREMENT PRIMARY KEY COMMENT '主键ID'," +
- "report_id BIGINT NOT NULL COMMENT '报告ID(health_reports.id)'," +
- "from_family_id BIGINT NOT NULL COMMENT '原归属家庭ID(需同意方)'," +
- "to_family_id BIGINT NOT NULL COMMENT '申请转入家庭ID(申请方)'," +
- "requester_user_id BIGINT NOT NULL COMMENT '申请人用户ID(users.id)'," +
- "status VARCHAR(20) NOT NULL DEFAULT 'pending' COMMENT '状态: pending=待同意 approved=已同意 denied=已拒绝'," +
- "handled_by BIGINT DEFAULT NULL COMMENT '处理人用户ID'," +
- "handled_at DATETIME DEFAULT NULL COMMENT '处理时间'," +
- "remark VARCHAR(200) DEFAULT NULL COMMENT '备注(拒绝原因)'," +
- "created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间'," +
- "updated_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间'," +
- "INDEX idx_report_id (report_id)," +
- "INDEX idx_from_family_status (from_family_id, status)," +
- "INDEX idx_to_family_status (to_family_id, status)" +
- ") ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='健康报告跨家庭转移申请'");
- log.info("迁移349: 已创建 report_transfer_requests 表");
- } catch (Exception e) {
- log.warn("迁移349: 创建 report_transfer_requests 表失败: {}", e.getMessage());
- }
- }
- // 迁移350: products 表添加 min_discount_level 列(商品粒度控制会员价门槛,NULL=所有会员均可享会员价)
- private void addProductMinDiscountLevel() {
- try {
- jdbcTemplate.execute("ALTER TABLE products ADD COLUMN min_discount_level VARCHAR(50) DEFAULT NULL " +
- "COMMENT '享受会员价所需的最低会员级别编码(FREE/FAMILY/PREMIUM/LIFETIME,NULL=所有会员均可)' AFTER min_visible_level");
- log.info("迁移350: 已添加 products.min_discount_level 列");
- } catch (Exception e) {
- // 列已存在,忽略
- log.debug("迁移350: products.min_discount_level 列已存在,跳过");
- }
- }
- // 迁移351: membership_levels.level_order 顺延,为 BASIC 让出独立档位
- // 会员级别包含关系(数字越大级别越高): FREE=1 BASIC=2 FAMILY=3 PREMIUM=4 LIFETIME=5
- // PROVIDER 为服务商独立角色(不走会员权益),与 FREE 同级=1;最终值由迁移381 fixMembershipLevelOrder 兜底
- private void backfillMembershipLevelOrder() {
- try {
- int updated = jdbcTemplate.update("UPDATE membership_levels SET level_order = CASE level_code " +
- "WHEN 'FREE' THEN 1 WHEN 'BASIC' THEN 2 WHEN 'FAMILY' THEN 3 WHEN 'PROVIDER' THEN 1 " +
- "WHEN 'PREMIUM' THEN 4 WHEN 'LIFETIME' THEN 5 ELSE level_order END");
- log.info("迁移351: 已回填 membership_levels.level_order({} 行):FREE=1 BASIC=2 FAMILY=3 PREMIUM=4 LIFETIME=5; PROVIDER=1(非会员)", updated);
- } catch (Exception e) {
- log.warn("迁移351: 回填 membership_levels.level_order 失败: {}", e.getMessage());
- }
- }
- // 迁移381: 修正 membership_levels.level_order(迁移351/360 执行顺序导致的 FAMILY 被重置为 2 的问题;
- // 同时 PROVIDER 为服务商独立角色,不走会员权益,降至与 FREE 同级,避免其参与 getMemberLevel / resolveEffectiveLevel 的等级比较)
- // level_order 语义:数字越大级别越高; FREE=1 BASIC=2 FAMILY=3 PREMIUM=4 LIFETIME=5; PROVIDER=1(非会员)
- private void fixMembershipLevelOrder() {
- try {
- int updated = jdbcTemplate.update("UPDATE membership_levels SET level_order = CASE level_code " +
- "WHEN 'FREE' THEN 1 WHEN 'BASIC' THEN 2 WHEN 'FAMILY' THEN 3 " +
- "WHEN 'PREMIUM' THEN 4 WHEN 'LIFETIME' THEN 5 WHEN 'PROVIDER' THEN 1 ELSE level_order END " +
- "WHERE level_code IN ('FREE','BASIC','FAMILY','PREMIUM','LIFETIME','PROVIDER')");
- log.info("迁移381: 已修正 membership_levels.level_order({} 行):" +
- "FREE=1 BASIC=2 FAMILY=3 PREMIUM=4 LIFETIME=5 PROVIDER=1(非会员)", updated);
- } catch (Exception e) {
- log.warn("迁移381: 修正 membership_levels.level_order 失败: {}", e.getMessage());
- }
- }
- /**
- * 迁移360: 恢复 9.9 元/月 BASIC 体验会员等级与权益(幂等)。
- * 反向还原提交 f86dd164b 删除的内容;level_order 由 fixMembershipLevelOrder(迁移381)统一回填。
- */
- private void restoreBasicMembershipLevel() {
- try {
- jdbcTemplate.execute(
- "INSERT IGNORE INTO membership_levels " +
- "(level_code, level_name, level_desc, price_monthly, features, max_children, max_tasks_per_day) " +
- "VALUES ('BASIC', '体验会员', " +
- "'9.9元/月体验版,解锁核心功能,限次使用AI与报告解读', " +
- "990, " +
- "'[\"free_activities\",\"free_courses\",\"discount_purchase\",\"invite_family\",\"mascot_ai_lite\",\"ai_report_reading_lite\",\"assessment_quota_lite\",\"health_report_upload_lite\",\"growth_plan_lite\",\"task_template_lite\",\"referral_commission\"]', " +
- "3, 3)");
- log.info("迁移360: 已恢复 BASIC 体验会员等级");
- } catch (Exception e) {
- log.warn("迁移360: 恢复 BASIC 会员等级失败: {}", e.getMessage());
- }
- // 兼容历史数据:早期 BASIC features 缺少 referral_commission,补齐
- try {
- jdbcTemplate.update("UPDATE membership_levels SET features = ? WHERE level_code = 'BASIC' AND features NOT LIKE '%referral_commission%'",
- "[\"free_activities\",\"free_courses\",\"discount_purchase\",\"invite_family\",\"mascot_ai_lite\",\"ai_report_reading_lite\",\"assessment_quota_lite\",\"health_report_upload_lite\",\"growth_plan_lite\",\"task_template_lite\",\"referral_commission\"]");
- } catch (Exception e) {
- log.warn("迁移360: 补齐 BASIC features 失败: {}", e.getMessage());
- }
- try {
- jdbcTemplate.execute(
- "INSERT IGNORE INTO membership_benefits " +
- "(level_code, benefit_code, benefit_name, benefit_desc, icon, bg_color, sort_order) VALUES " +
- "('BASIC', 'AI_CHAT_LITE', 'AI健康顾问(限3次/月)', '每月3次AI健康顾问问答', '🤖', '#DBEAFE', 1), " +
- "('BASIC', 'REPORT_READING_LITE', '报告解读(限1次/月)', '每月1次体检/测评报告AI解读', '📊', '#FCE7F3', 4), " +
- "('BASIC', 'GROWTH_PLAN_LITE', '基础成长计划', '创建成长计划但模板有限', '📋', '#FEF3C7', 3), " +
- "('BASIC', 'ASSESSMENT_QUOTA_LITE', '测评预约(限1次)', '每月可预约1次测试', '📅', '#E0E7FF', 8)");
- log.info("迁移360: 已恢复 BASIC 会员权益");
- } catch (Exception e) {
- log.warn("迁移360: 恢复 BASIC 会员权益失败: {}", e.getMessage());
- }
- }
- private void removeReportUploadQuota() {
- try {
- int deletedBenefits = jdbcTemplate.update(
- "DELETE FROM membership_benefits WHERE level_code = 'BASIC' AND benefit_code = 'REPORT_UPLOAD_LITE'");
- log.info("迁移352: 已清理遗留权益记录({} 行)", deletedBenefits);
- } catch (Exception e) {
- log.warn("迁移352: 移除 REPORT_UPLOAD_LITE 权益失败: {}", e.getMessage());
- }
- try {
- int deletedUsage = jdbcTemplate.update(
- "DELETE FROM basic_quota_usage WHERE quota_type = 'REPORT_UPLOAD'");
- log.info("迁移352: 已清理 REPORT_UPLOAD 配额记录({} 行)", deletedUsage);
- } catch (Exception e) {
- log.warn("迁移352: 清理 REPORT_UPLOAD 配额记录失败: {}", e.getMessage());
- }
- }
- /**
- * 迁移405: 9.9 元体验会员(BASIC)权益口径修正(2026-10-23 用户定稿,单一事实源)。
- * <ul>
- * <li>AI 对话:每天 3 次(原"AI健康顾问"月配额口径,展示名同步改为"AI 对话");</li>
- * <li>饮食拍照识别:每天 3 次;</li>
- * <li>报告解读:保留每月 1 次;</li>
- * <li>测评预约:不是特殊功能 → 移除 assessment_quota_lite 与 ASSESSMENT_QUOTA 配额(预约凭已购测评额度);</li>
- * <li>报告上传:全员开放 → 移除 health_report_upload_lite;</li>
- * <li>每日任务:不限 → max_tasks_per_day=0;</li>
- * <li>指定商品会员价:discount_purchase 生效(products.member_price + min_discount_level 机制)。</li>
- * </ul>
- * 配额周期粒度(日/月)由 BasicQuotaService.QUOTA_PERIODS 按类型判定,本迁移同步 features/权益/描述。
- */
- private void applyBasicMembership20261023Spec() {
- try {
- jdbcTemplate.update(
- "UPDATE membership_levels SET level_desc = ?, features = ?, max_tasks_per_day = 0 " +
- "WHERE level_code = 'BASIC'",
- "9.9元/月体验版:AI对话每天3次、饮食识别每天3次、报告解读每月1次,享指定商品会员价",
- "[\"free_activities\",\"free_courses\",\"discount_purchase\",\"invite_family\",\"mascot_ai_lite\",\"ai_report_reading_lite\",\"growth_plan_lite\",\"task_template_lite\",\"referral_commission\"]");
- log.info("迁移405: 已更新 BASIC 等级口径(features/max_tasks_per_day/level_desc)");
- } catch (Exception e) {
- log.warn("迁移405: 更新 BASIC 等级失败: {}", e.getMessage());
- }
- try {
- jdbcTemplate.update(
- "UPDATE membership_benefits SET benefit_name = ?, benefit_desc = ? " +
- "WHERE level_code = 'BASIC' AND benefit_code = 'AI_CHAT_LITE'",
- "AI对话(限3次/天)", "每天3次AI对话问答");
- jdbcTemplate.update(
- "DELETE FROM membership_benefits WHERE level_code = 'BASIC' AND benefit_code = 'ASSESSMENT_QUOTA_LITE'");
- jdbcTemplate.execute(
- "INSERT IGNORE INTO membership_benefits (level_code, benefit_code, benefit_name, benefit_desc, icon, bg_color, sort_order) VALUES " +
- "('BASIC', 'FOOD_RECOGNIZE_LITE', '饮食拍照识别(限3次/天)', '每天3次食材图片识别', '📷', '#FEF9C3', 2), " +
- "('BASIC', 'MEMBER_PRICE', '指定商品会员价', '购买指定商品享受会员价', '🏷️', '#FEF3C7', 5)");
- log.info("迁移405: 已更新 BASIC 权益列表(AI对话更名/去测评预约/增识别与会员价)");
- } catch (Exception e) {
- log.warn("迁移405: 更新 BASIC 权益失败: {}", e.getMessage());
- }
- // 配额记录切新制度:ASSESSMENT_QUOTA 整体移除;AI_CHAT/FOOD_RECOGNIZE 月记录改按日懒重建(删除旧记录避免月初日/月周期重合碰撞)
- try {
- int deleted = jdbcTemplate.update(
- "DELETE FROM basic_quota_usage WHERE quota_type IN ('ASSESSMENT_QUOTA','AI_CHAT','FOOD_RECOGNIZE')");
- log.info("迁移405: 已清理旧口径配额记录({} 行)", deleted);
- } catch (Exception e) {
- log.warn("迁移405: 清理配额记录失败: {}", e.getMessage());
- }
- }
- // 迁移353: 任务提醒配置表(用户级,一行/用户;提醒渠道/时机/偏移分钟可配置)
- private void createTaskReminderConfigTable() {
- try {
- jdbcTemplate.execute("CREATE TABLE IF NOT EXISTS task_reminder_config (" +
- "id BIGINT AUTO_INCREMENT PRIMARY KEY COMMENT '主键ID'," +
- "user_id BIGINT NOT NULL COMMENT '用户ID(users.id),家长账号'," +
- "channels VARCHAR(100) DEFAULT 'wechat' COMMENT '提醒渠道(逗号分隔): in_app/wechat/sms/phone/email'," +
- "timing_mode VARCHAR(20) DEFAULT 'after_timeout' COMMENT '提醒时机: before_deadline=截止前提醒 after_timeout=超时后提醒'," +
- "offset_minutes INT DEFAULT 60 COMMENT '偏移分钟: before_deadline=截止前N分钟; after_timeout=超时后N分钟'," +
- "enabled TINYINT DEFAULT 1 COMMENT '是否启用 1=启用 0=停用'," +
- "created_at DATETIME DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间'," +
- "updated_at DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间'," +
- "UNIQUE KEY uk_user (user_id)" +
- ") ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='任务提醒配置表'");
- log.info("迁移353: 已创建 task_reminder_config 表");
- try { ensureColumn("task_reminder_config", "completion_praise_enabled", "TINYINT DEFAULT 0 COMMENT '任务正常完成时是否发送表扬鼓励消息 1=启用 0=停用'"); ensureColumn("task_reminder_config", "completion_praise_channels", "VARCHAR(100) DEFAULT 'wechat,in_app' COMMENT '完成表扬渠道(逗号分隔)'"); } catch (Exception ex) { log.warn("迁移353补充列失败: {}", ex.getMessage()); }
- } catch (Exception e) {
- log.warn("迁移353: 创建 task_reminder_config 表失败: {}", e.getMessage());
- }
- }
- // 迁移359: 跨小程序单点登录一次性票据表(cfc ↔ train 双向免登)
- // 详见 schema.sql。此处自愈建表,避免 schema.sql 建表语句因故静默失败导致登录报错。
- private void createSsoTicketTable() {
- try {
- jdbcTemplate.execute("CREATE TABLE IF NOT EXISTS sso_ticket (" +
- "id BIGINT AUTO_INCREMENT PRIMARY KEY COMMENT '主键ID'," +
- "ticket VARCHAR(64) NOT NULL COMMENT '一次性票据(SecureRandom 生成)'," +
- "source_system VARCHAR(16) NOT NULL COMMENT '签发方系统: cfc / train'," +
- "target_system VARCHAR(16) NOT NULL COMMENT '目标系统: cfc / train'," +
- "source_user_id BIGINT NOT NULL COMMENT '签发方用户ID(cfc=users.id / train=train_user.id)'," +
- "cfc_user_id BIGINT NOT NULL COMMENT 'cfc 统一会员ID(users.id),跨系统换票依据'," +
- "used TINYINT NOT NULL DEFAULT 0 COMMENT '是否已使用: 0=未用 1=已用'," +
- "expire_at DATETIME NOT NULL COMMENT '过期时间'," +
- "created_at DATETIME DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间'," +
- "UNIQUE KEY uk_ticket (ticket)," +
- "INDEX idx_expire (expire_at)" +
- ") ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='跨小程序单点登录一次性票据'");
- log.info("迁移359: 已创建 sso_ticket 表");
- } catch (Exception e) {
- log.warn("迁移359: 创建 sso_ticket 表失败: {}", e.getMessage());
- }
- }
- // 迁移355: product_recommendation_log 补齐缺失列
- // 旧库该表由更早版本的建表语句创建(CREATE TABLE IF NOT EXISTS 不会补列),
- // 缺 family_id/was_clicked/was_purchased/clicked_at/purchased_at/updated_at,
- // 导致 stats/ab-summary、stats/by-type 查询报 Unknown column 返回 500
- private void addMissingColumnsToRecommendationLog() {
- ensureColumn("product_recommendation_log", "family_id",
- "BIGINT DEFAULT NULL COMMENT '家庭ID(family_members.family_id)'");
- ensureColumn("product_recommendation_log", "was_clicked",
- "TINYINT DEFAULT 0 COMMENT '是否点击 1=是 0=否'");
- ensureColumn("product_recommendation_log", "was_purchased",
- "TINYINT DEFAULT 0 COMMENT '是否购买 1=是 0=否'");
- ensureColumn("product_recommendation_log", "clicked_at",
- "DATETIME DEFAULT NULL COMMENT '点击时间'");
- ensureColumn("product_recommendation_log", "purchased_at",
- "DATETIME DEFAULT NULL COMMENT '购买时间'");
- ensureColumn("product_recommendation_log", "updated_at",
- "DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间'");
- log.info("迁移355: product_recommendation_log 已补齐缺失列");
- }
- // 迁移354: 报告采集指标项定义(report_type_registry.expected_indicators)+ 草稿采集校验结果列(health_report_drafts.collect_validation)
- // 每个报告类型定义「应采集指标项」清单(JSON 数组),ReportCollectService 采集完成后按此校验完整性,结果写入草稿 collect_validation。
- // DAN 指标项按 测评数据_优化提取.csv 各报告类型列(填充率>=30%)总结;菌群指标项按 docs/参考资料 已解析报告的指标/区块总结。
- private void registerCollectExpectedIndicators() {
- ensureColumn("report_type_registry", "expected_indicators",
- "TEXT NULL COMMENT '该报告类型应采集的指标项定义(JSON数组),用于数据采集完整性校验'");
- ensureColumn("health_report_drafts", "collect_validation",
- "TEXT NULL COMMENT '采集指标校验结果(JSON: typeId/expected/matched/pass/missing)'");
- // 确保注册表存在各 DAN 类型 + 北京菌群类型的行(parser_path 空 = 保持走 LangGraph 检测,不影响既有路由)
- ensureReportTypeRow("dan_a1", "dan", "素质测评A1", "核心认知发展评估报告:感知觉/注意力/记忆力/推理能力/空间能力/加工速度");
- ensureReportTypeRow("dan_b2", "dan", "素质测评B2", "儿童行为评估报告:自我概念/行为问题");
- ensureReportTypeRow("dan_b3", "dan", "素质测评B3", "学习能力评估报告:执行功能/学习动机/学习策略/自我效能感");
- ensureReportTypeRow("dan_b4", "dan", "素质测评B4", "核心素养评估报告:认知能力/成长型思维/自我概念");
- ensureReportTypeRow("dan_b5", "dan", "素质测评B5", "青春期发展评估报告:情绪调节/人际关系/睡眠/运动/网络依赖/学业压力");
- ensureReportTypeRow("dan_b6", "dan", "素质测评B6", "职业发展评估报告:霍兰德兴趣/多元智能/职业价值观");
- ensureReportTypeRow("dan_c1", "dan", "素质测评C1", "校园版核心素养评估报告:认知能力/大五人格/自我概念/学习动机");
- ensureReportTypeRow("gut_flora_beijing", "gut_flora", "北京菌群检测", "北京肠道菌群检测报告(FMT移植前/后评估)");
- // 种子各类型「应采集指标项」定义(每次启动覆盖,保证与代码一致)
- seedExpectedIndicator("dan_a1", jsonArray(
- "感知觉_pct", "感知觉_score", "注意力_pct", "注意力_score",
- "记忆力_pct", "记忆力_score", "推理能力_pct", "推理能力_score",
- "空间能力_pct", "空间能力_score", "加工速度_pct", "加工速度_score",
- "总分", "百分位"));
- seedExpectedIndicator("dan_a2", jsonArray(
- "BMI", "感知觉_pct", "记忆力_pct", "注意力_pct",
- "外倾性", "宜人性", "开放性", "责任心", "神经质",
- "情绪总分", "焦虑-安详", "抑郁-愉快", "无力感-掌控感", "自卑-自尊",
- "与同伴亲近", "与同伴信任", "与同伴沟通",
- "与母亲亲近", "与母亲信任", "与母亲沟通",
- "与父亲亲近", "与父亲信任", "与父亲沟通",
- "身高", "体重", "睡眠_小时", "运动_小时", "饮食_小时",
- "总分", "百分位"));
- seedExpectedIndicator("dan_b2", jsonArray(
- "自我概念_合群", "自我概念_幸福与满足", "自我概念_情绪状态", "自我概念_能力与学校",
- "自我概念_行为表现", "自我概念_躯体外貌",
- "行为_品行问题", "行为_学习问题", "行为_情绪问题", "行为_生活习惯", "行为_社交问题"));
- seedExpectedIndicator("dan_b3", jsonArray(
- "工作记忆_pct", "认知灵活性_pct", "抑制控制_pct",
- "表面动机", "深层动机", "学习自我调节", "表面方法与策略", "深层方法与策略",
- "自我效能感", "总分"));
- seedExpectedIndicator("dan_b4", jsonArray(
- "感知觉_pct", "感知觉_score", "注意力_pct", "注意力_score",
- "记忆力_pct", "记忆力_score", "推理能力_pct", "推理能力_score",
- "空间能力_pct", "空间能力_score", "加工速度_pct", "加工速度_score",
- "成长型思维",
- "自我概念_合群", "自我概念_幸福与满足", "自我概念_情绪状态", "自我概念_能力与学校",
- "自我概念_行为表现", "自我概念_躯体外貌",
- "自主性", "胜任感", "归属感",
- "总分", "百分位"));
- seedExpectedIndicator("dan_b5", jsonArray(
- "表达抑制", "认知重评",
- "人际_父亲关系", "人际_父亲关系_过度期待", "人际_父亲关系_价值冲突",
- "人际_父亲关系_控制压迫", "人际_父亲关系_情感疏离", "人际_父亲关系_双重标准",
- "人际_母亲关系", "人际_母亲关系_过度期待", "人际_母亲关系_价值冲突",
- "人际_母亲关系_控制压迫", "人际_母亲关系_情感疏离", "人际_母亲关系_双重标准",
- "人际_师生关系", "人际_师生关系_敌对型", "人际_师生关系_高控制型",
- "人际_师生关系_关系疏离型", "人际_师生关系_回避型",
- "人际_同伴关系", "人际_同伴关系_被排斥型", "人际_同伴关系_边缘型",
- "人际_同伴关系_攻击型", "人际_同伴关系_回避型", "人际_同伴关系_特殊因素型",
- "社交_表达影响", "社交_冲突解决", "社交_情感表达", "社交_情感支持", "社交_主动交往",
- "入睡时间_min", "睡眠_催眠药物", "睡眠_日间功能", "睡眠_时长", "睡眠_效率",
- "睡眠_障碍", "睡眠_主观质量",
- "网络依赖_戒断反应", "网络依赖_强迫使用", "网络依赖_时间管理", "网络依赖_总分",
- "学业压力_家庭期望", "学业压力_师生关系", "学业压力_同伴竞争", "学业压力_学业负担", "学业压力_自我期望",
- "运动_步行_频率", "运动_步行_时长", "运动_高强度_频率", "运动_高强度_时长",
- "运动_久坐_频率", "运动_久坐_时长", "运动_中等强度_频率", "运动_中等强度_时长",
- "总分", "百分位"));
- seedExpectedIndicator("dan_b6", jsonArray(
- "能力_内省", "能力_人际关系", "能力_音乐", "能力_自然",
- "兴趣_常规型", "兴趣_社会型", "兴趣_事业型", "兴趣_现实型", "兴趣_研究型", "兴趣_艺术型",
- "职业价值观"));
- seedExpectedIndicator("dan_c1", jsonArray(
- "BMI",
- "感知觉_pct", "感知觉_score", "注意力_pct", "注意力_score",
- "记忆力_pct", "记忆力_score", "推理能力_pct", "推理能力_score",
- "空间能力_pct", "空间能力_score", "加工速度_pct", "加工速度_score",
- "外倾性", "宜人性", "开放性", "责任心", "神经质", "情绪总分",
- "自我概念_合群", "自我概念_幸福与满足", "自我概念_情绪状态", "自我概念_能力与学校",
- "自我概念_行为表现", "自我概念_躯体外貌",
- "表面动机", "深层动机", "学习自我调节", "表面方法与策略", "深层方法与策略", "自我效能感",
- "自主性", "胜任感", "归属感",
- "与同伴亲近", "与同伴信任", "与同伴沟通",
- "与母亲亲近", "与母亲信任", "与母亲沟通",
- "与父亲亲近", "与父亲信任", "与父亲沟通",
- "身高", "体重", "睡眠时长", "运动时间",
- "总分", "百分位"));
- seedExpectedIndicator("gut_flora", jsonArray(
- "肠道预测年龄", "肠型", "肠道菌群平衡", "菌群多样性", "有益菌", "有害菌", "核心菌属",
- "健康总分", "菌群健康", "慢病控制", "营养均衡",
- "疾病风险评估", "主要营养评估", "氨基酸评估", "维生素评估", "微量元素评估",
- "主要消化道致病菌", "抗生素风险评估", "肠道屏障及代谢物", "短链脂肪酸",
- "神经递质及激素", "菌群检出详细列表", "个体化食物推荐表"));
- seedExpectedIndicator("gut_flora_beijing", jsonArray(
- "肠型", "肠道微生物健康指数", "肠道菌群平衡评估", "肠道黏膜屏障", "肠道菌群多样性",
- "肠道菌群表型评估", "肠道抗炎能力", "肠道免疫力", "肠道膳食纤维需求", "短链脂肪酸合成能力",
- "核心菌属综合评分", "有益菌综合评分", "有害菌综合评分",
- "疾病风险评估", "营养物质及营养素代谢评估", "抗生素风险评估",
- "毒性物质清除能力评估", "胃肠道感染病原体评估", "趣味肠菌评估",
- "菌群检出详细列表", "个体化食物推荐表"));
- }
- /** 注册报告类型行(INSERT IF 不存在,幂等;parser_path=null 保持走 LangGraph 检测) */
- private void ensureReportTypeRow(String typeId, String family, String displayName, String description) {
- try {
- Integer count = jdbcTemplate.queryForObject(
- "SELECT COUNT(*) FROM report_type_registry WHERE type_id = ?", Integer.class, typeId);
- if (count == null || count == 0) {
- jdbcTemplate.update(
- "INSERT INTO report_type_registry (type_id, family, display_name, description, parser_path, " +
- "min_confidence, is_active, source, review_status, version, created_at, updated_at) " +
- "VALUES (?, ?, ?, ?, NULL, 30, 1, 'prebuilt', 'approved', '1.0', NOW(), NOW())",
- typeId, family, displayName, description);
- log.info("迁移354: 已注册报告类型 {}", typeId);
- }
- } catch (Exception e) {
- log.warn("迁移354: 注册报告类型 {} 失败: {}", typeId, e.getMessage());
- }
- }
- /** 写入类型「应采集指标项」定义(仅当为空时写入,保留管理员编辑) */
- void seedExpectedIndicator(String typeId, String expectedJson) {
- try {
- int updated = jdbcTemplate.update(
- "UPDATE report_type_registry SET expected_indicators = ?, updated_at = NOW() " +
- "WHERE type_id = ? AND (expected_indicators IS NULL OR expected_indicators = '')",
- expectedJson, typeId);
- if (updated == 0) {
- log.info("迁移354: 类型 {} 已有指标项定义,跳过种子", typeId);
- } else {
- log.info("迁移354: 已为类型 {} 写入指标项定义", typeId);
- }
- } catch (Exception e) {
- log.warn("迁移354: 为类型 {} 写入指标项定义失败: {}", typeId, e.getMessage());
- }
- }
- /** 简易 JSON 字符串数组序列化(指标项均为中文/ASCII,无转义风险) */
- private String jsonArray(String... items) {
- StringBuilder sb = new StringBuilder("[");
- for (int i = 0; i < items.length; i++) {
- if (i > 0) {
- sb.append(',');
- }
- sb.append('"').append(items[i]).append('"');
- }
- return sb.append(']').toString();
- }
- // 迁移359: 家庭共餐选中食材表(持久化共餐场景食材选择)
- private void createDietFamilySelectedIngredientsTable() {
- try {
- jdbcTemplate.execute("CREATE TABLE IF NOT EXISTS diet_family_selected_ingredients (" +
- "id BIGINT AUTO_INCREMENT PRIMARY KEY COMMENT '主键ID'," +
- "family_id BIGINT NOT NULL COMMENT '家庭ID'," +
- "food_id BIGINT NOT NULL COMMENT '食材ID'," +
- "food_name VARCHAR(100) COMMENT '食材名称'," +
- "score INT COMMENT '推荐分'," +
- "reason VARCHAR(255) COMMENT '推荐理由'," +
- "category VARCHAR(50) COMMENT '分类'," +
- "calories_per_100g INT COMMENT '每100克卡路里'," +
- "cooking_level TINYINT COMMENT '烹饪难度(1=快手,2=中等,3=耗时)'," +
- "nutrition_score INT COMMENT '营养密度分(0-100)'," +
- "balance_reason VARCHAR(255) COMMENT '均衡化推荐理由'," +
- "sort_order INT DEFAULT 0 COMMENT '排序'," +
- "created_at DATETIME DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间'," +
- "updated_at DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间'," +
- "UNIQUE KEY uk_family_food (family_id, food_id)," +
- "INDEX idx_family (family_id)" +
- ") ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='家庭共餐选中食材'");
- log.info("迁移359: 已创建 diet_family_selected_ingredients 表");
- } catch (Exception e) {
- log.warn("迁移359: 创建 diet_family_selected_ingredients 表失败: {}", e.getMessage());
- }
- }
- // 迁移356: indicator_definitions 升级为统一指标层(指标变化曲线/速查/关联 需求)
- private void migrateIndicatorDefinitionsColumns() {
- ensureColumn("indicator_definitions", "source_kind",
- "VARCHAR(32) DEFAULT NULL COMMENT '指标来源: report_indicator/bacteria/food_index/dimension_score'");
- ensureColumn("indicator_definitions", "report_type_scope",
- "VARCHAR(32) DEFAULT NULL COMMENT '适用报告类型(type_id,空=通用)'");
- ensureColumn("indicator_definitions", "value_type",
- "VARCHAR(16) DEFAULT NULL COMMENT '语义类型: numeric/categorical/level'");
- ensureColumn("indicator_definitions", "good_direction",
- "VARCHAR(16) DEFAULT 'neutral' COMMENT '变化优劣方向: higher_is_better/lower_is_better/in_range_is_better/neutral'");
- ensureColumn("indicator_definitions", "dimension_code",
- "VARCHAR(16) DEFAULT NULL COMMENT '关联五维(body/mind/wisdom/action/wealth,可空)'");
- log.info("迁移356: indicator_definitions 已补齐统一指标层 5 列");
- }
- // 迁移357: indicator_values 升级为统一观测时序(subject_id/family_id/report_id/report_date/unit/status)
- private void migrateIndicatorValuesColumns() {
- ensureColumn("indicator_values", "subject_id", "BIGINT DEFAULT NULL COMMENT '观测对象(family_members.id)'");
- ensureColumn("indicator_values", "family_id", "BIGINT DEFAULT NULL COMMENT '所属家庭(family_members.family_id)'");
- ensureColumn("indicator_values", "report_id", "BIGINT DEFAULT NULL COMMENT '来源报告(health_reports.id,食物指数可空)'");
- ensureColumn("indicator_values", "report_date", "DATE DEFAULT NULL COMMENT '冗余真实报告日期(时序查询键)'");
- ensureColumn("indicator_values", "unit", "VARCHAR(32) DEFAULT NULL COMMENT '观测单位'");
- ensureColumn("indicator_values", "status", "VARCHAR(32) DEFAULT NULL COMMENT '观测状态快照(正常/偏高/偏低等)'");
- log.info("迁移357: indicator_values 已补齐统一观测时序 6 列");
- }
- // 迁移358: indicator_values 幂等唯一键(source_type+source_id+definition_id)+ 时序查询索引
- private void addIndicatorValuesKeys() {
- try {
- // 先清理极端重复(保留最小 id),避免建唯一键失败;正常数据本无重复
- jdbcTemplate.execute(
- "DELETE v FROM indicator_values v " +
- "JOIN (SELECT MIN(id) keep_id, source_type, source_id, definition_id " +
- " FROM indicator_values " +
- " GROUP BY source_type, source_id, definition_id " +
- " HAVING COUNT(*) > 1) d " +
- " ON v.source_type = d.source_type AND v.source_id = d.source_id " +
- " AND v.definition_id = d.definition_id AND v.id <> d.keep_id");
- log.info("迁移358: 已清理 indicator_values 重复行(如有)");
- } catch (Exception e) {
- log.warn("迁移358: 清理 indicator_values 重复行失败(表可能为空): {}", e.getMessage());
- }
- try {
- jdbcTemplate.execute("ALTER TABLE indicator_values ADD UNIQUE KEY uk_source_definition (source_type, source_id, definition_id)");
- log.info("迁移358: 已添加 uk_source_definition 唯一键");
- } catch (Exception e) {
- log.info("迁移358: uk_source_definition 已存在,跳过");
- }
- try {
- jdbcTemplate.execute("ALTER TABLE indicator_values ADD INDEX idx_subject_def_date (subject_id, definition_id, report_date)");
- log.info("迁移358: 已添加 idx_subject_def_date 索引");
- } catch (Exception e) {
- log.info("迁移358: idx_subject_def_date 已存在,跳过");
- }
- }
- // 迁移359: 新建 indicator_mapping_rules(指标归一化映射规则)
- private void createIndicatorMappingRulesTable() {
- try {
- jdbcTemplate.execute("CREATE TABLE IF NOT EXISTS indicator_mapping_rules (" +
- "id BIGINT AUTO_INCREMENT PRIMARY KEY," +
- "source_type VARCHAR(32) NOT NULL COMMENT '来源: gut_flora|physical_exam|dan_a2|food_recommend|five_dimension|health_dimension'," +
- "raw_name VARCHAR(200) NOT NULL COMMENT '报告中的原始名称(指标名/菌名/食材名)'," +
- "alias_text TEXT COMMENT '别名 JSON 数组,用于模糊匹配'," +
- "definition_id BIGINT NOT NULL COMMENT 'FK → indicator_definitions.id'," +
- "match_type VARCHAR(20) DEFAULT 'exact' COMMENT 'exact|fuzzy|ai_fallback|auto_created'," +
- "priority INT DEFAULT 0 COMMENT '多规则冲突时取最高'," +
- "status TINYINT DEFAULT 1," +
- "created_at DATETIME DEFAULT CURRENT_TIMESTAMP," +
- "updated_at DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP," +
- "UNIQUE KEY uk_source_raw (source_type, raw_name)," +
- "INDEX idx_def (definition_id)" +
- ") ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='指标归一化映射规则'");
- log.info("迁移359: 已创建 indicator_mapping_rules 表");
- } catch (Exception e) {
- log.warn("迁移359: 创建 indicator_mapping_rules 表失败: {}", e.getMessage());
- }
- }
- // 迁移360: 新建 indicator_correlations(指标关联关系:人工定义 + 数据校准)
- private void createIndicatorCorrelationsTable() {
- try {
- jdbcTemplate.execute("CREATE TABLE IF NOT EXISTS indicator_correlations (" +
- "id BIGINT AUTO_INCREMENT PRIMARY KEY," +
- "definition_id_a BIGINT NOT NULL COMMENT 'code 字典序较小的指标'," +
- "definition_id_b BIGINT NOT NULL COMMENT 'code 字典序较大的指标'," +
- "relation_type VARCHAR(20) DEFAULT 'correlation' COMMENT 'correlation|influence|same_source'," +
- "direction VARCHAR(10) NOT NULL COMMENT 'positive|negative'," +
- "manual_strength DECIMAL(3,2) NOT NULL COMMENT '人工标定强度 0~1'," +
- "computed_strength DECIMAL(4,3) NULL COMMENT '统计计算相关系数(Spearman rho)'," +
- "method VARCHAR(20) DEFAULT 'spearman' COMMENT 'pearson|spearman'," +
- "sample_size INT DEFAULT 0 COMMENT '参与计算的聚合有效样本数'," +
- "confidence VARCHAR(10) COMMENT 'high(>=30)|medium(10~29)|low(<10)'," +
- "effective_strength DECIMAL(4,3) NOT NULL COMMENT '展示值: 样本>=30 时取 computed 否则取 manual'," +
- "manual_locked TINYINT DEFAULT 0 COMMENT '1=人工锁定,校准不覆盖'," +
- "cohort_scope VARCHAR(20) DEFAULT 'subject' COMMENT 'subject|family|population'," +
- "status VARCHAR(20) DEFAULT 'active' COMMENT 'active|inactive|pending'," +
- "computed_at DATETIME NULL COMMENT '最后校准时间'," +
- "created_by BIGINT NULL COMMENT '创建者'," +
- "description VARCHAR(500) NULL COMMENT '给用户看的解释'," +
- "source_note VARCHAR(255) NULL COMMENT '依据/出处'," +
- "created_at DATETIME DEFAULT CURRENT_TIMESTAMP," +
- "updated_at DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP," +
- "UNIQUE KEY uk_pair (definition_id_a, definition_id_b)," +
- "INDEX idx_def_a (definition_id_a)," +
- "INDEX idx_def_b (definition_id_b)" +
- ") ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='指标关联关系(人工定义+数据校准)'");
- log.info("迁移360: 已创建 indicator_correlations 表");
- } catch (Exception e) {
- log.warn("迁移360: 创建 indicator_correlations 表失败: {}", e.getMessage());
- }
- }
- // 迁移361: health_indicators.collection_date 生产库兜底(schema.sql 已有该列,仅确保老库补齐)
- private void ensureIndicatorCollectionDate() {
- ensureColumn("health_indicators", "collection_date",
- "DATETIME COMMENT '指标采集日期(报告检测日期)'");
- log.info("迁移361: health_indicators.collection_date 已确认存在");
- }
- // 迁移362: 预置五维展示定义(dim.body 等,仅作展示标签,不写 indicator_values)
- private void seedDimensionDefinitions() {
- Object[][] dims = {
- {"dim.body", "身", "身体", "higher_is_better", "higher_is_better"},
- {"dim.mind", "心", "心理", "higher_is_better", "higher_is_better"},
- {"dim.wisdom", "智", "智慧", "higher_is_better", "higher_is_better"},
- {"dim.action", "行", "行动", "higher_is_better", "higher_is_better"},
- {"dim.wealth", "富", "财富", "higher_is_better", "higher_is_better"},
- };
- for (Object[] d : dims) {
- try {
- Integer exists = jdbcTemplate.queryForObject(
- "SELECT COUNT(*) FROM indicator_definitions WHERE code = ?", Integer.class, (String) d[0]);
- if (exists == null || exists == 0) {
- jdbcTemplate.update(
- "INSERT INTO indicator_definitions (domain, category, code, name, data_type, unit, " +
- "source_kind, value_type, good_direction, dimension_code, sort_order, status) " +
- "VALUES ('physical', ?, ?, ?, 'number', '分', 'dimension_score', 'numeric', ?, ?, 0, 1)",
- d[1], d[0], d[2], d[4], d[3]);
- log.info("迁移362: 已预置维度定义 {}", d[0]);
- }
- } catch (Exception e) {
- log.warn("迁移362: 预置维度定义 {} 失败: {}", d[0], e.getMessage());
- }
- }
- }
- // 迁移355: 创建减脂专题体重记录表
- private void createFatWeightRecordTable() {
- try {
- jdbcTemplate.execute("CREATE TABLE IF NOT EXISTS fat_weight_record (" +
- "id BIGINT AUTO_INCREMENT PRIMARY KEY, " +
- "member_id BIGINT NOT NULL, " +
- "weight DECIMAL(5,2) NOT NULL, " +
- "body_fat_percent DECIMAL(4,2) DEFAULT NULL, " +
- "waist_circumference DECIMAL(4,2) DEFAULT NULL, " +
- "measure_date DATE NOT NULL, " +
- "remark VARCHAR(255) DEFAULT NULL, " +
- "created_at DATETIME DEFAULT CURRENT_TIMESTAMP, " +
- "INDEX idx_fat_weight_member (member_id, measure_date)) " +
- "ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='减脂专题-体重体脂记录'");
- log.info("迁移355: 已创建fat_weight_record表");
- } catch (Exception e) {
- log.warn("迁移355: 创建fat_weight_record表失败: {}", e.getMessage());
- }
- }
- // 迁移356: 创建减脂专题饮食记录表
- private void createFatDietRecordTable() {
- try {
- jdbcTemplate.execute("CREATE TABLE IF NOT EXISTS fat_diet_record (" +
- "id BIGINT AUTO_INCREMENT PRIMARY KEY, " +
- "member_id BIGINT NOT NULL, " +
- "meal_type VARCHAR(20) NOT NULL, " +
- "food_name VARCHAR(255) NOT NULL, " +
- "food_weight DECIMAL(6,2) DEFAULT NULL, " +
- "calories INT DEFAULT 0, " +
- "protein DECIMAL(6,2) DEFAULT 0, " +
- "fat DECIMAL(6,2) DEFAULT 0, " +
- "carbs DECIMAL(6,2) DEFAULT 0, " +
- "eat_date DATE NOT NULL, " +
- "remark VARCHAR(255) DEFAULT NULL, " +
- "created_at DATETIME DEFAULT CURRENT_TIMESTAMP, " +
- "INDEX idx_fat_diet_member (member_id, eat_date)) " +
- "ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='减脂专题-饮食记录'");
- log.info("迁移356: 已创建fat_diet_record表");
- } catch (Exception e) {
- log.warn("迁移356: 创建fat_diet_record表失败: {}", e.getMessage());
- }
- }
- // 迁移357: 创建减脂专题运动记录表
- private void createFatExerciseRecordTable() {
- try {
- jdbcTemplate.execute("CREATE TABLE IF NOT EXISTS fat_exercise_record (" +
- "id BIGINT AUTO_INCREMENT PRIMARY KEY, " +
- "member_id BIGINT NOT NULL, " +
- "exercise_type VARCHAR(50) NOT NULL, " +
- "duration_min INT DEFAULT 0, " +
- "distance_km DECIMAL(5,2) DEFAULT NULL, " +
- "calories_burned INT DEFAULT 0, " +
- "exercise_date DATE NOT NULL, " +
- "remark VARCHAR(255) DEFAULT NULL, " +
- "created_at DATETIME DEFAULT CURRENT_TIMESTAMP, " +
- "INDEX idx_fat_exercise_member (member_id, exercise_date)) " +
- "ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='减脂专题-运动记录'");
- log.info("迁移357: 已创建fat_exercise_record表");
- } catch (Exception e) {
- log.warn("迁移357: 创建fat_exercise_record表失败: {}", e.getMessage());
- }
- }
- // 迁移358: 创建减脂专题目标表
- private void createFatGoalTable() {
- try {
- jdbcTemplate.execute("CREATE TABLE IF NOT EXISTS fat_goal (" +
- "id BIGINT AUTO_INCREMENT PRIMARY KEY, " +
- "member_id BIGINT NOT NULL, " +
- "target_weight DECIMAL(5,2) NOT NULL, " +
- "start_weight DECIMAL(5,2) NOT NULL, " +
- "target_body_fat DECIMAL(4,2) DEFAULT NULL, " +
- "target_date DATE DEFAULT NULL, " +
- "status VARCHAR(20) DEFAULT 'active', " +
- "start_date DATE NOT NULL, " +
- "created_at DATETIME DEFAULT CURRENT_TIMESTAMP, " +
- "updated_at DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, " +
- "INDEX idx_fat_goal_member (member_id)) " +
- "ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='减脂专题-减脂目标'");
- log.info("迁移358: 已创建fat_goal表");
- } catch (Exception e) {
- log.warn("迁移358: 创建fat_goal表失败: {}", e.getMessage());
- }
- }
- // 迁移384: products 表添加 vendor_visible 列(仅供应商体系内可见开关)
- private void addProductVendorVisibleColumn() {
- ensureColumn("products", "vendor_visible", "TINYINT(1) DEFAULT NULL COMMENT '是否仅供应商体系内用户可见(列表接口生效,直接分享详情页可访问)'");
- }
- // 迁移367: products 表添加健康约束字段(基础疾病禁忌/过敏原/药物相互作用标记)
- private void addProductHealthConstraintColumns() {
- ensureColumn("products", "contraindicated_diseases", "VARCHAR(255) DEFAULT NULL COMMENT '禁忌基础疾病,逗号分隔(如\"慢性肾病,肝病\")'");
- ensureColumn("products", "allergen_tags", "VARCHAR(255) DEFAULT NULL COMMENT '过敏原标签,逗号分隔(如\"花生,小麦\")'");
- ensureColumn("products", "drug_interaction_flags", "TINYINT DEFAULT 0 COMMENT '是否命中药物-食物相互作用(1=是,需对照drug_interactions表)'");
- }
- // 迁移368: 创建 drug_interactions 表(药物-食物禁忌对照表)
- private void createDrugInteractionsTable() {
- try {
- jdbcTemplate.execute("CREATE TABLE IF NOT EXISTS drug_interactions (" +
- "id BIGINT AUTO_INCREMENT PRIMARY KEY, " +
- "drug_class VARCHAR(50) NOT NULL COMMENT '药物类别(如CCB/他汀/抗凝药/抗生素/铁剂/甲状腺药/利尿剂/MAOI/头孢/降糖药)', " +
- "drug_specifics VARCHAR(100) NOT NULL COMMENT '具体药名(如硝苯地平/华法林)', " +
- "interacting_subst VARCHAR(100) NOT NULL COMMENT '禁忌/需间隔的食材或成分(如西柚/维生素K)', " +
- "risk_level TINYINT NOT NULL COMMENT '1=拦截 2=警告 3=需间隔', " +
- "action_type VARCHAR(20) NOT NULL COMMENT 'block=硬拦截 warn=软警告 space=间隔', " +
- "interval_minutes INT DEFAULT NULL COMMENT '需间隔时长(分钟)', " +
- "note VARCHAR(255) COMMENT '风险说明', " +
- "UNIQUE KEY uk_drug_food (drug_class, drug_specifics, interacting_subst), " +
- "INDEX idx_interacting (interacting_subst)" +
- ") ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='药物-食物禁忌对照表'");
- log.info("迁移368: 已创建drug_interactions表");
- } catch (Exception e) {
- log.warn("迁移368: 创建drug_interactions表失败: {}", e.getMessage());
- }
- }
- // 迁移369: 创建 disease_intake_limits 表(疾病-饮食摄入阈值)
- private void createDiseaseIntakeLimitsTable() {
- try {
- jdbcTemplate.execute("CREATE TABLE IF NOT EXISTS disease_intake_limits (" +
- "id BIGINT AUTO_INCREMENT PRIMARY KEY, " +
- "disease_type VARCHAR(50) NOT NULL COMMENT '疾病类型(慢性肾病/糖尿病/高血压/痛风/肝病/甲状腺疾病)', " +
- "nutrient VARCHAR(20) NOT NULL COMMENT '校验营养素(protein/potassium/phosphorus/sodium/carb/glycemic/fructose/purine/alcohol/iodine)', " +
- "unit VARCHAR(20) NOT NULL COMMENT '单位', " +
- "threshold_max DECIMAL(8,2) DEFAULT NULL COMMENT '每日上限', " +
- "threshold_min DECIMAL(8,2) DEFAULT NULL COMMENT '每日下限', " +
- "block_foods VARCHAR(500) DEFAULT NULL COMMENT '硬拦截食材列表,逗号分隔(如杨桃)', " +
- "stage_dependent TINYINT(1) DEFAULT 0 COMMENT '是否按病情分期(1=是 0=否)', " +
- "INDEX idx_disease_nutrient (disease_type, nutrient)" +
- ") ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='疾病-饮食摄入校验阈值表'");
- log.info("迁移369: 已创建disease_intake_limits表");
- } catch (Exception e) {
- log.warn("迁移369: 创建disease_intake_limits表失败: {}", e.getMessage());
- }
- }
- // 迁移370: 初始化 drug_interactions 种子数据(高危组合,幂等)
- private void seedDrugInteractionsData() {
- String[][] seed = {
- {"CCB", "硝苯地平", "西柚", "1", "block", "0", "西柚抑制药物代谢酶,血药浓度升高数倍,低血压风险,服药期间完全避免"},
- {"CCB", "氨氯地平", "西柚", "1", "block", "0", "西柚抑制药物代谢酶,血药浓度升高数倍,低血压风险,服药期间完全避免"},
- {"他汀类", "辛伐他汀", "西柚", "1", "block", "0", "增加肌肉损伤、肝损伤风险"},
- {"他汀类", "阿托伐他汀", "西柚", "1", "block", "0", "增加肌肉损伤、肝损伤风险"},
- {"抗凝药", "华法林", "维生素K", "2", "warn", "0", "维K拮抗药效,摄入量保持稳定,调整饮食结构前先与医生沟通"},
- {"抗凝药", "华法林", "大蒜", "2", "warn", "0", "大蒜可能增强抗凝作用,增加出血风险"},
- {"抗生素", "四环素", "牛奶", "3", "space", "120", "钙与药物结合影响吸收,需间隔2小时以上"},
- {"抗生素", "喹诺酮类", "牛奶", "3", "space", "120", "钙与药物结合影响吸收,需间隔2小时以上"},
- {"抗生素", "喹诺酮类", "钙剂", "3", "space", "120", "钙与药物结合影响吸收,需间隔2小时以上"},
- {"铁剂", "硫酸亚铁", "茶", "3", "space", "60", "鞣酸抑制铁吸收,需间隔1-2小时"},
- {"铁剂", "硫酸亚铁", "咖啡", "3", "space", "60", "鞣酸抑制铁吸收,需间隔1-2小时"},
- {"铁剂", "硫酸亚铁", "钙剂", "3", "space", "60", "钙抑制铁吸收,需间隔1-2小时"},
- {"铁剂", "硫酸亚铁", "牛奶", "3", "space", "60", "钙抑制铁吸收,需间隔1-2小时"},
- {"甲状腺药", "左甲状腺素", "豆浆", "3", "space", "240", "影响吸收,需间隔4小时"},
- {"甲状腺药", "左甲状腺素", "高纤维食物", "3", "space", "240", "影响吸收,需间隔4小时"},
- {"甲状腺药", "左甲状腺素", "钙剂", "3", "space", "240", "影响吸收,需间隔4小时"},
- {"保钾利尿剂", "螺内酯", "高钾食物", "2", "warn", "0", "高钾血症风险,避免香蕉、橙汁、低钠盐大量集中摄入"},
- {"MAOI", "苯乙肼", "发酵食品", "2", "warn", "0", "酪胺反应可致血压骤升,避免奶酪/豆豉/腌制品/陈年食品"},
- {"头孢类", "头孢类", "酒精", "1", "block", "0", "双硫仑样反应,严重可危及生命"},
- {"甲硝唑", "甲硝唑", "酒精", "1", "block", "0", "双硫仑样反应,严重可危及生命"},
- {"降糖药", "磺脲类", "酒精", "1", "block", "0", "低血糖风险,避免空腹饮酒"}
- };
- for (String[] row : seed) {
- try {
- Integer cnt = jdbcTemplate.queryForObject(
- "SELECT COUNT(*) FROM drug_interactions WHERE drug_class=? AND drug_specifics=? AND interacting_subst=?",
- Integer.class, row[0], row[1], row[2]);
- if (cnt != null && cnt > 0) continue;
- jdbcTemplate.update(
- "INSERT INTO drug_interactions (drug_class, drug_specifics, interacting_subst, risk_level, action_type, interval_minutes, note) VALUES (?,?,?,?,?,?,?)",
- row[0], row[1], row[2], Integer.valueOf(row[3]), row[4], Integer.valueOf(row[5]), row[6]);
- } catch (Exception e) {
- log.warn("迁移370: 插入drug_interactions种子失败: {}", e.getMessage());
- }
- }
- log.info("迁移370: drug_interactions种子数据初始化完成");
- }
- // 迁移371: 初始化 disease_intake_limits 种子数据(幂等)
- private void seedDiseaseIntakeLimitsData() {
- String[][] seed = {
- {"慢性肾病", "protein", "g/kg/day", "0.8", "0.6", "杨桃", "1"},
- {"慢性肾病", "potassium", "mmol/L/day", "5.0", "3.5", "杨桃,香蕉,橙子,土豆,菠菜,菌菇", "1"},
- {"慢性肾病", "phosphorus", "mg/day", "800", "600", "杨桃,奶制品,坚果,加工食品", "1"},
- {"糖尿病", "carb", "g/meal", "75", "30", "", "0"},
- {"糖尿病", "fructose", "g/day", "50", "0", "含糖饮料,果汁,蜂蜜,甜点", "0"},
- {"高血压", "sodium", "g/day", "5", "2", "腌菜,酱菜,酱油,加工肉制品,挂面,咸味零食", "0"},
- {"痛风", "purine", "mg/day", "300", "150", "动物内脏,部分海鲜,浓肉汤", "0"},
- {"痛风", "fructose", "g/day", "50", "0", "含糖饮料,蜂蜜", "0"},
- {"肝病", "protein", "g/kg/day", "1.0", "0.5", "", "1"},
- {"甲状腺疾病", "iodine", "ug/day", "300", "150", "海带,紫菜", "0"}
- };
- for (String[] row : seed) {
- try {
- Integer cnt = jdbcTemplate.queryForObject(
- "SELECT COUNT(*) FROM disease_intake_limits WHERE disease_type=? AND nutrient=?",
- Integer.class, row[0], row[1]);
- if (cnt != null && cnt > 0) continue;
- jdbcTemplate.update(
- "INSERT INTO disease_intake_limits (disease_type, nutrient, unit, threshold_max, threshold_min, block_foods, stage_dependent) VALUES (?,?,?,?,?,?,?)",
- row[0], row[1], row[2], row[3], row[4], row[5], Integer.valueOf(row[6]));
- } catch (Exception e) {
- log.warn("迁移371: 插入disease_intake_limits种子失败: {}", e.getMessage());
- }
- }
- log.info("迁移371: disease_intake_limits种子数据初始化完成");
- }
- // 迁移363: 创建采购任务表(商品组合推荐与采购任务生命周期管理)
- private void createProcurementTaskTable() {
- try {
- jdbcTemplate.execute("CREATE TABLE IF NOT EXISTS procurement_tasks (" +
- "id BIGINT AUTO_INCREMENT PRIMARY KEY, " +
- "task_id VARCHAR(64) NOT NULL COMMENT '任务唯一标识', " +
- "user_id BIGINT NOT NULL COMMENT '所属用户ID', " +
- "family_member_id BIGINT NOT NULL COMMENT '所属家庭成员ID', " +
- "combo_tier VARCHAR(20) NOT NULL COMMENT '选择的套餐等级: high|medium|low', " +
- "original_combo_tier VARCHAR(20) DEFAULT NULL COMMENT '原始套餐等级(用于自定义追踪)', " +
- "selected_product_sku_ids TEXT COMMENT '最终选中的产品SKU列表(JSON数组)', " +
- "custom_addition_sku_ids TEXT COMMENT '用户自新增的产品SKU列表(JSON数组)', " +
- "custom_removal_sku_ids TEXT COMMENT '用户移除的产品SKU列表(JSON数组)', " +
- "status VARCHAR(32) NOT NULL COMMENT '任务状态: SELECTED|PROCURING|PROCURED|EXECUTING|COMPLETED|CANCELLED', " +
- "create_time DATETIME NOT NULL COMMENT '创建时间', " +
- "complete_time DATETIME DEFAULT NULL COMMENT '完成时间', " +
- "message_reminder_sent TINYINT DEFAULT 0 COMMENT '是否已发送消息提醒', " +
- "execution_task_id VARCHAR(64) DEFAULT NULL COMMENT '关联的执行任务ID', " +
- "update_time DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, " +
- "UNIQUE KEY uk_task_id (task_id), " +
- "INDEX idx_user_id (user_id), " +
- "INDEX idx_family_member_id (family_member_id), " +
- "INDEX idx_status (status), " +
- "INDEX idx_create_time (create_time)" +
- ") ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='采购任务表(商品组合推荐与采购任务生命周期管理)'");
- log.info("已创建procurement_tasks表");
- } catch (Exception e) {
- log.warn("创建procurement_tasks表失败: {}", e.getMessage());
- }
- }
- // 迁移364: health_report_drafts 添加解析进度/重试字段(INFORMATION_SCHEMA 检查后再 ALTER,幂等)
- private void addDraftParseProgressColumns() {
- if (!columnExists("health_report_drafts", "parse_attempt")) {
- jdbcTemplate.execute("ALTER TABLE health_report_drafts ADD COLUMN parse_attempt INT DEFAULT 0 COMMENT '已知类型解析重试次数'");
- log.info("已添加 parse_attempt 列到 health_report_drafts 表");
- }
- if (!columnExists("health_report_drafts", "next_retry_at")) {
- jdbcTemplate.execute("ALTER TABLE health_report_drafts ADD COLUMN next_retry_at DATETIME NULL COMMENT '下次自动重试时间(指数退避,上限5分钟)'");
- log.info("已添加 next_retry_at 列到 health_report_drafts 表");
- }
- if (!columnExists("health_report_drafts", "parse_started_at")) {
- jdbcTemplate.execute("ALTER TABLE health_report_drafts ADD COLUMN parse_started_at DATETIME NULL COMMENT '最近一次解析开始时间'");
- log.info("已添加 parse_started_at 列到 health_report_drafts 表");
- }
- if (!columnExists("health_report_drafts", "parse_finished_at")) {
- jdbcTemplate.execute("ALTER TABLE health_report_drafts ADD COLUMN parse_finished_at DATETIME NULL COMMENT '最近一次解析结束时间(成功/失败)'");
- log.info("已添加 parse_finished_at 列到 health_report_drafts 表");
- }
- }
- // 迁移374: 创建 user_content_weights 表(用户内容偏好权重表)
- /**
- * 迁移378: 创建 member_indicator_current 表
- * 成员维度「当前指标 + 相对上次变化」:
- * - 报告确认入库后,取该成员最新一份报告 vs 次新报告的同名指标,计算 delta
- * - 方案生成(LangGraph)与前端指标卡片可直接读取,无需每次跨表 diff
- * 幂等:CREATE TABLE IF NOT EXISTS
- */
- private void addMemberIndicatorCurrentTable() {
- try {
- jdbcTemplate.execute(
- "CREATE TABLE IF NOT EXISTS `member_indicator_current` (" +
- "`id` bigint NOT NULL AUTO_INCREMENT," +
- "`member_id` bigint NOT NULL COMMENT '家庭成员ID(family_members.id)'," +
- "`definition_id` bigint NOT NULL COMMENT '指标定义ID(indicator_definitions.id)'," +
- "`domain` varchar(50) NOT NULL DEFAULT 'physical' COMMENT '指标域(body/mind/physical/wisdom)'," +
- "`category` varchar(50) DEFAULT NULL COMMENT '指标分类'," +
- "`name` varchar(100) NOT NULL COMMENT '指标名称'," +
- "`value` varchar(200) DEFAULT NULL COMMENT '最新值'," +
- "`numeric_value` decimal(12,4) DEFAULT NULL COMMENT '最新数值(可计算)'," +
- "`previous_value` varchar(200) DEFAULT NULL COMMENT '上一份报告的同类指标值'," +
- "`previous_numeric` decimal(12,4) DEFAULT NULL COMMENT '上一份报告数值'," +
- "`delta` decimal(12,4) DEFAULT NULL COMMENT 'numeric_value - previous_numeric'," +
- "`trend` varchar(16) DEFAULT NULL COMMENT '变化方向: up/down/flat/new/na'," +
- "`latest_report_id` bigint DEFAULT NULL COMMENT '最新来源报告ID'," +
- "`previous_report_id` bigint DEFAULT NULL COMMENT '上次来源报告ID'," +
- "`report_date` date DEFAULT NULL COMMENT '最新报告日期'," +
- "`recorded_at` datetime DEFAULT CURRENT_TIMESTAMP," +
- "`updated_at` datetime DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP," +
- "PRIMARY KEY (`id`)," +
- "UNIQUE KEY `uk_member_definition` (`member_id`, `definition_id`)," +
- "KEY `idx_member_domain` (`member_id`, `domain`)," +
- "KEY `idx_report` (`latest_report_id`)" +
- ") ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='成员指标当前值+相对上次变化'");
- log.info("迁移378: member_indicator_current 表就绪");
- } catch (Exception e) {
- log.warn("迁移378: 创建 member_indicator_current 表失败或已存在: {}", e.getMessage());
- }
- }
- private void addUserContentWeightsTable() {
- try {
- jdbcTemplate.execute("CREATE TABLE IF NOT EXISTS user_content_weights (" +
- "id BIGINT AUTO_INCREMENT PRIMARY KEY COMMENT '主键ID'," +
- "user_id BIGINT NOT NULL COMMENT '用户ID(users.id)'," +
- "family_id BIGINT NOT NULL COMMENT '家庭ID'," +
- "content_type VARCHAR(16) NOT NULL COMMENT '内容类型: product/activity/article'," +
- "item_id BIGINT NOT NULL COMMENT '内容ID(商品ID/活动ID/文章ID)'," +
- "dimension_code VARCHAR(32) DEFAULT NULL COMMENT '维度编码: body/mind/wisdom/action/wealth'," +
- "weight DECIMAL(5,2) NOT NULL DEFAULT 1.00 COMMENT '权重值 0.00-10.00,越大越优先'," +
- "reason VARCHAR(200) DEFAULT NULL COMMENT '权重来源: user_set/auto_click/auto_purchase/manual/auto_profile'," +
- "created_by BIGINT DEFAULT NULL COMMENT '创建人ID(manual场景记录运营人员)'," +
- "created_at DATETIME DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间'," +
- "updated_at DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间'," +
- "INDEX idx_user_content (user_id, content_type, item_id)," +
- "INDEX idx_family_dimension (family_id, dimension_code)," +
- "UNIQUE KEY uk_user_item (user_id, content_type, item_id)" +
- ") ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='用户内容偏好权重表'");
- log.info("迁移374: 已创建/确认 user_content_weights 表");
- } catch (Exception e) {
- log.warn("迁移374: 创建 user_content_weights 表失败: {}", e.getMessage());
- }
- }
- // 迁移375: 修复 article_quiz_records 表结构(旧表由迁移299创建时已存在导致缺列)
- private void fixArticleQuizRecordsTable() {
- try {
- if (!columnExists("article_quiz_records", "member_id")) {
- jdbcTemplate.execute("ALTER TABLE article_quiz_records ADD COLUMN member_id BIGINT DEFAULT NULL COMMENT '家庭成员ID(作答人)'");
- log.info("迁移375: 已添加 member_id 列到 article_quiz_records");
- }
- if (!columnExists("article_quiz_records", "total_questions")) {
- jdbcTemplate.execute("ALTER TABLE article_quiz_records ADD COLUMN total_questions INT DEFAULT 0 COMMENT '题目总数'");
- log.info("迁移375: 已添加 total_questions 列到 article_quiz_records");
- }
- if (!columnExists("article_quiz_records", "cf_earned")) {
- jdbcTemplate.execute("ALTER TABLE article_quiz_records ADD COLUMN cf_earned INT DEFAULT 0 COMMENT '发放的CF值'");
- log.info("迁移375: 已添加 cf_earned 列到 article_quiz_records");
- }
- if (!columnExists("article_quiz_records", "energy_earned")) {
- jdbcTemplate.execute("ALTER TABLE article_quiz_records ADD COLUMN energy_earned INT DEFAULT 0 COMMENT '发放的能量值'");
- log.info("迁移375: 已添加 energy_earned 列到 article_quiz_records");
- }
- // questions/answers 旧表为 VARCHAR(255),实体存 JSON 快照会超长,与 schema.sql 对齐为 TEXT
- if (!columnTypeStartsWith("article_quiz_records", "questions", "text")) {
- jdbcTemplate.execute("ALTER TABLE article_quiz_records MODIFY COLUMN questions TEXT COMMENT '题目快照JSON(含正确答案与维度)'");
- log.info("迁移375: questions 列已改为 TEXT");
- }
- if (!columnTypeStartsWith("article_quiz_records", "answers", "text")) {
- jdbcTemplate.execute("ALTER TABLE article_quiz_records MODIFY COLUMN answers TEXT COMMENT '用户作答JSON(含每题所选选项),作答前为NULL'");
- log.info("迁移375: answers 列已改为 TEXT");
- }
- // 补充索引(幂等;重复创建会报 Duplicate key name,捕获忽略)
- try {
- jdbcTemplate.execute("ALTER TABLE article_quiz_records ADD INDEX idx_member (member_id)");
- } catch (Exception ignored) {
- }
- log.info("迁移375: article_quiz_records 表结构修复完成");
- } catch (Exception e) {
- log.warn("迁移375: 修复 article_quiz_records 表结构失败: {}", e.getMessage());
- }
- }
- private boolean columnTypeStartsWith(String table, String column, String typePrefix) {
- try {
- String t = jdbcTemplate.queryForObject(
- "SELECT DATA_TYPE FROM information_schema.COLUMNS WHERE TABLE_SCHEMA = DATABASE() AND TABLE_NAME = ? AND COLUMN_NAME = ?",
- String.class, table, column);
- return t != null && t.toLowerCase().startsWith(typePrefix);
- } catch (Exception e) {
- return false;
- }
- }
- // 迁移372: nutrition_product 添加 efficacy_tags 列(成分功效标签)
- private void addNutritionProductEfficacyTags() {
- if (!columnExists("nutrition_product", "efficacy_tags")) {
- jdbcTemplate.execute("ALTER TABLE nutrition_product ADD COLUMN efficacy_tags TEXT COMMENT '成分功效标签JSON数组(如 [\"补钙\",\"补锌\",\"益生菌\",\"维生素D\"])'");
- log.info("已添加 efficacy_tags 列到 nutrition_product 表");
- }
- }
- // 迁移373: products 添加 tags 列(文章-商品标签关联推荐)
- private void addProductTagsColumn() {
- ensureColumn("products", "tags", "VARCHAR(500) DEFAULT '' COMMENT '商品标签 JSON 数组,用于文章-商品标签匹配推荐'");
- log.info("迁移373: products.tags 已确认存在");
- }
- // 迁移379: 创建 report_member_permissions 表(报告成员级查看权限)
- private void addReportMemberPermissionsTable() {
- try {
- jdbcTemplate.execute("CREATE TABLE IF NOT EXISTS report_member_permissions (" +
- "id BIGINT NOT NULL AUTO_INCREMENT COMMENT '主键ID'," +
- "family_id BIGINT NOT NULL COMMENT '家庭ID(隔离作用域)'," +
- "authorizer_user_id BIGINT NOT NULL COMMENT '授权人(家庭管理员)的用户ID'," +
- "target_member_id BIGINT NOT NULL COMMENT '被授权查看的成员ID(family_members.id)'," +
- "grantee_user_id BIGINT NOT NULL COMMENT '被授权用户的ID(能看报告的人)'," +
- "created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '授权时间'," +
- "PRIMARY KEY (id)," +
- "UNIQUE KEY uk_perm (family_id, target_member_id, grantee_user_id)," +
- "KEY idx_target_member (target_member_id)," +
- "KEY idx_grantee_user (grantee_user_id)," +
- "KEY idx_family_id (family_id)" +
- ") ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci COMMENT='报告成员级查看权限(管理员授权某用户查看某成员的全部报告)'");
- log.info("迁移379: report_member_permissions 表就绪");
- } catch (Exception e) {
- log.warn("迁移379: 创建 report_member_permissions 表失败或已存在: {}", e.getMessage());
- }
- }
- // 迁移380: 修正 FAMILY 会员虚拟道具映射为 member1980(幂等;仅纠正历史错误值 member1314,不覆盖后台后续配置)
- private void fixFamilyMembershipVirtualGoodsMapping() {
- try {
- int updated = jdbcTemplate.update(
- "UPDATE virtual_goods_config SET product_id = 'member1980' " +
- "WHERE goods_type = 'MEMBERSHIP' AND biz_key = 'FAMILY' AND product_id = 'member1314'");
- if (updated > 0) {
- log.info("迁移380: FAMILY 会员虚拟道具映射已修正为 member1980,受影响 {} 行", updated);
- }
- } catch (Exception e) {
- log.warn("迁移380: 修正 FAMILY 会员道具映射失败: {}", e.getMessage());
- }
- }
- // 迁移381: 创建活动海报模板表 + 初始化默认模板(Web 管理端定义,小程序端海报模板选择)
- private void createActivityPosterTemplatesTable() {
- try {
- jdbcTemplate.execute("CREATE TABLE IF NOT EXISTS activity_poster_templates (" +
- "id BIGINT AUTO_INCREMENT PRIMARY KEY COMMENT '主键ID'," +
- "name VARCHAR(50) NOT NULL COMMENT '模板名称(管理端展示,如 暖橙渐变)'," +
- "template_key VARCHAR(30) NOT NULL COMMENT '模板类型: gradient/minimal/card 小程序绘制引擎'," +
- "main_color VARCHAR(20) DEFAULT '#F97316' COMMENT '主色(渐变起点/卡片顶栏)'," +
- "dark_color VARCHAR(20) DEFAULT '#EA580C' COMMENT '深色(渐变终点/深色模板背景)'," +
- "light_color VARCHAR(20) DEFAULT '#FFF7ED' COMMENT '浅色(内容卡背景)'," +
- "bg_colors VARCHAR(500) DEFAULT '[\"#F97316\"]' COMMENT '可选背景色板 JSON 数组'," +
- "qr_size INT DEFAULT 100 COMMENT '二维码尺寸(rpx)'," +
- "sort_order INT DEFAULT 0 COMMENT '排序(越小越前)'," +
- "status TINYINT DEFAULT 1 COMMENT '1启用 0停用'," +
- "created_at DATETIME DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间'," +
- "updated_at DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间'," +
- "UNIQUE KEY uk_name (name)" +
- ") ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='活动海报模板(Web管理端定义)'");
- log.info("迁移381: 已创建/确认 activity_poster_templates 表");
- } catch (Exception e) {
- log.warn("迁移381: 创建 activity_poster_templates 表失败: {}", e.getMessage());
- }
- // 初始化默认模板(幂等:按名称 INSERT IGNORE,重复执行不会产生重复行)
- try {
- jdbcTemplate.execute("INSERT IGNORE INTO activity_poster_templates (name, template_key, main_color, dark_color, light_color, bg_colors, qr_size, sort_order, status) VALUES " +
- "('暖橙渐变', 'gradient', '#F97316', '#EA580C', '#FFF7ED', '[\"#F97316\",\"#EA580C\",\"#F59E0B\",\"#10B981\",\"#FF6B9D\",\"#6366F1\",\"#1E293B\",\"#FFFFFF\"]', 100, 0, 1), " +
- "('深色极简', 'minimal', '#F97316', '#1E293B', '#F3F4F6', '[\"#1E293B\",\"#0F172A\",\"#111827\"]', 100, 1, 1), " +
- "('白色卡片', 'card', '#F97316', '#EA580C', '#FFF7ED', '[\"#FFFFFF\",\"#FFF7ED\",\"#F8FAFC\"]', 100, 2, 1)");
- log.info("迁移381: 默认海报模板种子数据已确认");
- } catch (Exception e) {
- log.warn("迁移381: 初始化默认海报模板失败: {}", e.getMessage());
- }
- }
- // 迁移385: 活动报名添加驳回原因列(审核中心详情页驳回时写入)
- private void addActivityRegistrationRejectReasonColumn() {
- try {
- if (!columnExists("activity_registrations", "reject_reason")) {
- jdbcTemplate.execute("ALTER TABLE activity_registrations ADD COLUMN reject_reason VARCHAR(500) NULL COMMENT '驳回原因'");
- log.info("迁移385: 已添加 activity_registrations.reject_reason 列");
- }
- } catch (Exception e) {
- log.warn("迁移385: 添加活动报名驳回原因列失败: {}", e.getMessage());
- }
- }
- // 迁移386: 历史 teacher_status='pending' 占位假数据清理,并把默认值改为 NULL
- private void cleanPlannerPlaceholderData() {
- try {
- int cleaned = jdbcTemplate.update(
- "UPDATE users SET teacher_status = NULL WHERE teacher_status = 'pending' AND (role IS NULL OR role <> 'teacher')");
- jdbcTemplate.execute("ALTER TABLE users MODIFY COLUMN teacher_status VARCHAR(20) DEFAULT NULL COMMENT '成长规划师审核状态'");
- log.info("迁移386: 已清理规划师占位数据 {} 行,teacher_status 默认值改为 NULL", cleaned);
- } catch (Exception e) {
- log.warn("迁移386: 清理规划师占位数据失败: {}", e.getMessage());
- }
- }
- // 迁移387: 下线独立“服务角色申请审核”菜单,服务角色申请按角色分流合并进审核中心
- private void hideServiceRoleApplicationMenu() {
- try {
- int updated = jdbcTemplate.update(
- "UPDATE sys_menu SET visible = 0 WHERE path = '/service-role-applications'");
- int removed = jdbcTemplate.update(
- "DELETE FROM sys_role_menu WHERE menu_id IN (SELECT id FROM sys_menu WHERE path = '/service-role-applications')");
- log.info("迁移387: 服务角色申请菜单已下线(菜单 {} 行,授权 {} 行)", updated, removed);
- } catch (Exception e) {
- log.warn("迁移387: 下线服务角色申请菜单失败: {}", e.getMessage());
- }
- }
- // 迁移394: 创建 planner_cert_categories 表(学习种类/认证类别,管理端可配置)+ 首批种子数据
- private void createPlannerCertCategoriesTable() {
- try {
- jdbcTemplate.execute("CREATE TABLE IF NOT EXISTS planner_cert_categories (" +
- "id INT NOT NULL AUTO_INCREMENT COMMENT '主键'," +
- "code VARCHAR(32) NOT NULL COMMENT '类别编码: planner/nutritionist/assessor...'," +
- "name VARCHAR(64) NOT NULL COMMENT '类别名称'," +
- "description VARCHAR(255) DEFAULT '' COMMENT '类别说明'," +
- "required_hours DECIMAL(6,2) NOT NULL DEFAULT 0 COMMENT '该类别要求总课时(线上+线下)'," +
- "is_base TINYINT(1) DEFAULT 0 COMMENT '1=基础类别(获得即 Guide.status=active)'," +
- "order_index INT DEFAULT 0 COMMENT '排序(越小越前)'," +
- "status TINYINT DEFAULT 1 COMMENT '1启用 0停用'," +
- "created_at DATETIME DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间'," +
- "updated_at DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间'," +
- "PRIMARY KEY (id)," +
- "UNIQUE KEY uk_code (code)" +
- ") ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='规划师学习种类/认证类别'");
- log.info("迁移394: 已创建/确认 planner_cert_categories 表");
- } catch (Exception e) {
- log.warn("迁移394: 创建 planner_cert_categories 表失败: {}", e.getMessage());
- }
- // 首批种子类别(幂等)
- try {
- jdbcTemplate.execute("INSERT IGNORE INTO planner_cert_categories (code, name, description, required_hours, is_base, order_index, status) VALUES " +
- "('planner', '成长规划师', '规划师基础身份认证,默认必学必考;获得后即可接基础服务', 10, 1, 0, 1), " +
- "('nutritionist', '营养师', '营养方案/饮食建议类服务前置认证', 8, 0, 1, 1), " +
- "('assessor', '测评师', '测评录入/解读类服务前置认证', 8, 0, 2, 1)");
- log.info("迁移394: 首批学习种类种子数据已确认");
- } catch (Exception e) {
- log.warn("迁移394: 初始化首批学习种类种子数据失败: {}", e.getMessage());
- }
- }
- // 迁移395: 创建 planner_courses 表(线上课程目录,映射小鹅通课程/专栏)
- private void createPlannerCoursesTable() {
- try {
- jdbcTemplate.execute("CREATE TABLE IF NOT EXISTS planner_courses (" +
- "id INT NOT NULL AUTO_INCREMENT COMMENT '主键'," +
- "category_id INT NOT NULL COMMENT '所属学习种类(planner_cert_categories.id)'," +
- "xiaoe_resource_id VARCHAR(64) DEFAULT '' COMMENT '小鹅通课程/商品ID'," +
- "title VARCHAR(120) NOT NULL COMMENT '课程标题'," +
- "required_hours DECIMAL(6,2) NOT NULL DEFAULT 0 COMMENT '该课程要求课时'," +
- "order_index INT DEFAULT 0 COMMENT '排序'," +
- "is_required TINYINT(1) DEFAULT 1 COMMENT '是否必修'," +
- "status TINYINT DEFAULT 1 COMMENT '1启用 0停用'," +
- "created_at DATETIME DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间'," +
- "updated_at DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间'," +
- "PRIMARY KEY (id)," +
- "KEY idx_category (category_id)," +
- "KEY idx_status (status)" +
- ") ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='规划师线上课程目录(小鹅通)'");
- log.info("迁移395: 已创建/确认 planner_courses 表");
- } catch (Exception e) {
- log.warn("迁移395: 创建 planner_courses 表失败: {}", e.getMessage());
- }
- }
- // 迁移396: 创建 planner_course_progress 表(每用户每线上课程学习进度,小鹅通回传/手动同步写入)
- private void createPlannerCourseProgressTable() {
- try {
- jdbcTemplate.execute("CREATE TABLE IF NOT EXISTS planner_course_progress (" +
- "id BIGINT NOT NULL AUTO_INCREMENT COMMENT '主键'," +
- "user_id BIGINT NOT NULL COMMENT '规划师用户ID'," +
- "course_id INT NOT NULL COMMENT 'planner_courses.id'," +
- "category_id INT NOT NULL COMMENT '所属学习种类'," +
- "xiaoe_resource_id VARCHAR(64) DEFAULT '' COMMENT '小鹅通资源ID'," +
- "progress_percent INT DEFAULT 0 COMMENT '学习进度百分比'," +
- "completed_hours DECIMAL(6,2) DEFAULT 0 COMMENT '已完成课时'," +
- "completed TINYINT(1) DEFAULT 0 COMMENT '是否完成'," +
- "last_synced_at DATETIME DEFAULT NULL COMMENT '最近同步时间'," +
- "created_at DATETIME DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间'," +
- "updated_at DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间'," +
- "PRIMARY KEY (id)," +
- "UNIQUE KEY uk_user_course (user_id, course_id)," +
- "KEY idx_user (user_id)" +
- ") ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='规划师线上课程学习进度'");
- log.info("迁移396: 已创建/确认 planner_course_progress 表");
- } catch (Exception e) {
- log.warn("迁移396: 创建 planner_course_progress 表失败: {}", e.getMessage());
- }
- }
- // 迁移397: 创建 planner_offline_course_map 表(线下培训活动↔认证类别桥接,复用 activities 模块)
- private void createPlannerOfflineCourseMapTable() {
- try {
- jdbcTemplate.execute("CREATE TABLE IF NOT EXISTS planner_offline_course_map (" +
- "id BIGINT NOT NULL AUTO_INCREMENT COMMENT '主键'," +
- "activity_id BIGINT NOT NULL COMMENT '关联 activities.id(线下培训活动)'," +
- "category_id INT NOT NULL COMMENT '关联 planner_cert_categories.id'," +
- "duration_hours DECIMAL(6,2) DEFAULT 0 COMMENT '该活动签到后计入的课时'," +
- "is_required TINYINT(1) DEFAULT 0 COMMENT '是否必修环节'," +
- "status TINYINT DEFAULT 1 COMMENT '1启用 0停用'," +
- "created_at DATETIME DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间'," +
- "updated_at DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间'," +
- "PRIMARY KEY (id)," +
- "UNIQUE KEY uk_activity (activity_id)," +
- "KEY idx_category (category_id)" +
- ") ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='线下培训活动↔认证类别映射(桥接)'");
- log.info("迁移397: 已创建/确认 planner_offline_course_map 表");
- } catch (Exception e) {
- log.warn("迁移397: 创建 planner_offline_course_map 表失败: {}", e.getMessage());
- }
- }
- // 迁移398: 创建 activity_checkins 表(活动签到记录,供培训课时桥接;修复"签到审核形同虚设"旧患)
- private void createActivityCheckinsTable() {
- try {
- jdbcTemplate.execute("CREATE TABLE IF NOT EXISTS activity_checkins (" +
- "id BIGINT NOT NULL AUTO_INCREMENT COMMENT '主键'," +
- "activity_id BIGINT NOT NULL COMMENT '活动ID(activities.id)'," +
- "user_id BIGINT NOT NULL COMMENT '签到人用户ID(规划师=userId)'," +
- "registration_id BIGINT DEFAULT NULL COMMENT '关联报名记录(activity_registrations.id)'," +
- "check_in_at DATETIME DEFAULT CURRENT_TIMESTAMP COMMENT '签到时间'," +
- "check_in_type VARCHAR(16) DEFAULT 'qr' COMMENT 'qr扫码/admin手动/gps'," +
- "check_in_by BIGINT DEFAULT NULL COMMENT 'admin手动补录人ID'," +
- "review_status VARCHAR(16) DEFAULT 'approved' COMMENT 'approved/pending/rejected(对应原requireCheckinReview)'," +
- "created_at DATETIME DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间'," +
- "PRIMARY KEY (id)," +
- "UNIQUE KEY uk_activity_user (activity_id, user_id)," +
- "KEY idx_user (user_id)," +
- "KEY idx_activity (activity_id)" +
- ") ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='活动签到记录(通用,供培训课时桥接)'");
- log.info("迁移398: 已创建/确认 activity_checkins 表");
- } catch (Exception e) {
- log.warn("迁移398: 创建 activity_checkins 表失败: {}", e.getMessage());
- }
- }
- // 迁移399: 创建 planner_certifications 表(规划师每类别培训认证主表,认证闸门依据)
- private void createPlannerCertificationsTable() {
- try {
- jdbcTemplate.execute("CREATE TABLE IF NOT EXISTS planner_certifications (" +
- "id BIGINT NOT NULL AUTO_INCREMENT COMMENT '主键'," +
- "user_id BIGINT NOT NULL COMMENT '规划师用户ID'," +
- "guide_id BIGINT DEFAULT NULL COMMENT '关联 guides.id'," +
- "category_id INT NOT NULL COMMENT '所属学习种类(planner_cert_categories.id)'," +
- "category_code VARCHAR(32) DEFAULT '' COMMENT '冗余类别编码便于查询'," +
- "required_hours DECIMAL(6,2) DEFAULT 0 COMMENT '该类别总要求课时'," +
- "completed_hours DECIMAL(6,2) DEFAULT 0 COMMENT '已完成课时(线上+线下汇总)'," +
- "learning_status VARCHAR(20) DEFAULT 'pending_learning' COMMENT 'pending_learning/learning_completed'," +
- "exam_status VARCHAR(20) DEFAULT 'none' COMMENT 'none/exam_failed/exam_passed'," +
- "exam_score INT DEFAULT NULL COMMENT '考核分数'," +
- "exam_attempts INT DEFAULT 0 COMMENT '考核尝试次数'," +
- "cert_no VARCHAR(64) DEFAULT '' COMMENT '证书编号'," +
- "certified_at DATETIME DEFAULT NULL COMMENT '认证通过时间'," +
- "expire_at DATETIME DEFAULT NULL COMMENT '有效期(NULL=不过期)'," +
- "status VARCHAR(20) DEFAULT 'pending_cert' COMMENT 'pending_cert/learning_completed/certified/suspended'," +
- "created_at DATETIME DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间'," +
- "updated_at DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间'," +
- "PRIMARY KEY (id)," +
- "UNIQUE KEY uk_user_category (user_id, category_id)," +
- "KEY idx_status (status)," +
- "KEY idx_user (user_id)" +
- ") ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='规划师每类别培训认证主表'");
- log.info("迁移399: 已创建/确认 planner_certifications 表");
- } catch (Exception e) {
- log.warn("迁移399: 创建 planner_certifications 表失败: {}", e.getMessage());
- }
- }
- // 迁移400: 创建考核三表(试卷/题库/作答记录,考核本系统自建)
- private void createPlannerExamTables() {
- try {
- jdbcTemplate.execute("CREATE TABLE IF NOT EXISTS planner_exam_papers (" +
- "id INT NOT NULL AUTO_INCREMENT COMMENT '主键'," +
- "category_id INT NOT NULL COMMENT '所属学习种类'," +
- "title VARCHAR(120) NOT NULL COMMENT '试卷标题'," +
- "pass_score INT NOT NULL DEFAULT 60 COMMENT '及格分'," +
- "duration_minutes INT DEFAULT 30 COMMENT '限时(分钟)'," +
- "question_count INT DEFAULT 0 COMMENT '题目数'," +
- "status TINYINT DEFAULT 1 COMMENT '1启用 0停用'," +
- "created_at DATETIME DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间'," +
- "updated_at DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间'," +
- "PRIMARY KEY (id)," +
- "KEY idx_category (category_id)" +
- ") ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='规划师考核试卷'");
- jdbcTemplate.execute("CREATE TABLE IF NOT EXISTS planner_exam_questions (" +
- "id BIGINT NOT NULL AUTO_INCREMENT COMMENT '主键'," +
- "paper_id INT NOT NULL COMMENT '试卷ID(planner_exam_papers.id)'," +
- "question TEXT NOT NULL COMMENT '题干'," +
- "options TEXT COMMENT '选项JSON数组'," +
- "answer VARCHAR(10) COMMENT '正确选项'," +
- "score INT DEFAULT 1 COMMENT '分值'," +
- "order_index INT DEFAULT 0 COMMENT '排序'," +
- "PRIMARY KEY (id)," +
- "KEY idx_paper (paper_id)" +
- ") ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='考核题库'");
- jdbcTemplate.execute("CREATE TABLE IF NOT EXISTS planner_exam_attempts (" +
- "id BIGINT NOT NULL AUTO_INCREMENT COMMENT '主键'," +
- "user_id BIGINT NOT NULL COMMENT '规划师用户ID'," +
- "category_id INT NOT NULL COMMENT '所属学习种类'," +
- "paper_id INT NOT NULL COMMENT '试卷ID'," +
- "score INT DEFAULT 0 COMMENT '得分'," +
- "passed TINYINT(1) DEFAULT 0 COMMENT '是否通过'," +
- "answers TEXT COMMENT 'JSON作答明细'," +
- "started_at DATETIME DEFAULT CURRENT_TIMESTAMP COMMENT '开始时间'," +
- "submitted_at DATETIME DEFAULT NULL COMMENT '提交时间'," +
- "PRIMARY KEY (id)," +
- "KEY idx_user (user_id)" +
- ") ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='规划师考核作答记录'");
- log.info("迁移400: 已创建/确认考核三表(试卷/题库/作答记录)");
- } catch (Exception e) {
- log.warn("迁移400: 创建考核三表失败: {}", e.getMessage());
- }
- }
- // 迁移401: 存量规划师补登认证(D6:active guide 按已持有基础类别 planner certified 补登,其余类别 pending_learning)
- private void backfillExistingPlannerCertifications() {
- try {
- int updated = jdbcTemplate.update(
- "INSERT IGNORE INTO planner_certifications (user_id, guide_id, category_id, category_code, required_hours, completed_hours, learning_status, exam_status, exam_score, cert_no, certified_at, status) " +
- "SELECT g.user_id, g.id, c.id, c.code, c.required_hours, c.required_hours, 'learning_completed', 'exam_passed', 100, CONCAT('CERT-', DATE_FORMAT(NOW(), '%Y%m%d'), '-', g.user_id), NOW(), 'certified' " +
- "FROM guides g " +
- "JOIN planner_cert_categories c ON c.code = 'planner' AND c.status = 1 " +
- "WHERE g.status = 'active'");
- log.info("迁移401: 存量规划师基础类别认证补登 {} 行", updated);
- // 同步存量 active guide 状态为 active(保持不变),并确保新增认证不影响接单
- } catch (Exception e) {
- log.warn("迁移401: 存量规划师认证补登失败: {}", e.getMessage());
- }
- }
- /**
- * 迁移402: delivery_method COMMENT 增加 6=同城配送(履约多渠道,规格 2026-10-22-fulfillment-multi-mode-design)。
- * 仅 COMMENT 变更,无数据变更,幂等可重复执行(MODIFY COLUMN 天然幂等 + 双层 try-catch)。
- */
- private void addDeliveryMethod6Comments() {
- try {
- jdbcTemplate.execute("ALTER TABLE products MODIFY COLUMN delivery_method TINYINT DEFAULT 1 "
- + "COMMENT '配送方式: 1=快递 2=自提 3=两者皆可 4=线上-可选人 5=线上-单人 6=同城配送'");
- log.info("迁移402: products.delivery_method COMMENT 已增加 6=同城配送");
- } catch (Exception e) {
- // COMMENT 变更失败(如列不存在)不影响启动
- log.warn("迁移402: products.delivery_method COMMENT 更新失败: {}", e.getMessage());
- }
- try {
- jdbcTemplate.execute("ALTER TABLE product_orders MODIFY COLUMN delivery_method INT DEFAULT NULL "
- + "COMMENT '配送方式: 1=快递 2=自提 3=两者皆可 4=线上-可选人 5=线上-单人 6=同城配送'");
- log.info("迁移402: product_orders.delivery_method COMMENT 已增加 6=同城配送");
- } catch (Exception e) {
- // COMMENT 变更失败(如列不存在)不影响启动
- log.warn("迁移402: product_orders.delivery_method COMMENT 更新失败: {}", e.getMessage());
- }
- }
- // 迁移403: withdrawal_requests 增加 reject_reason 列(审核中心统一详情:提现驳回原因口径统一)
- private void addWithdrawalRejectReasonColumn() {
- try {
- if (!columnExists("withdrawal_requests", "reject_reason")) {
- jdbcTemplate.execute("ALTER TABLE withdrawal_requests ADD COLUMN reject_reason VARCHAR(500) NULL COMMENT '驳回原因'");
- log.info("迁移403: 已添加 withdrawal_requests.reject_reason 列");
- }
- } catch (Exception e) {
- log.warn("迁移403: 添加 withdrawal_requests.reject_reason 列失败: {}", e.getMessage());
- }
- }
- // 迁移404: articles.audio_url 由 varchar(500) 改为 MEDIUMTEXT
- // 多段音频 JSON 数组(每段 OSS URL ~94 字符)在长文中可超 2900 字符,
- // 超出 varchar(500) 导致 DataIntegrityViolationException("数据不完整或格式错误")
- private void alterArticleAudioUrlToMediumtext() {
- try {
- jdbcTemplate.execute("ALTER TABLE articles MODIFY COLUMN audio_url MEDIUMTEXT NULL COMMENT '音频URL(单URL或JSON数组,多段)'");
- log.info("迁移404: articles.audio_url 已改为 MEDIUMTEXT");
- } catch (Exception e) {
- log.warn("迁移404: articles.audio_url 修改失败: {}", e.getMessage());
- }
- }
- // 迁移406: activity_poster_templates 增加 layout_config 列(活动海报模板元素位置/样式可配置;空值=小程序端内置默认布局)
- private void addActivityPosterTemplateLayoutConfigColumn() {
- try {
- if (!columnExists("activity_poster_templates", "layout_config")) {
- jdbcTemplate.execute("ALTER TABLE activity_poster_templates "
- + "ADD COLUMN layout_config TEXT NULL COMMENT '布局配置JSON(元素位置/样式可配置,空=默认布局)'");
- log.info("迁移406: 已添加 activity_poster_templates.layout_config 列");
- }
- } catch (Exception e) {
- log.warn("迁移406: 添加 activity_poster_templates.layout_config 列失败: {}", e.getMessage());
- }
- }
- // ==================================================================================
- // F-1 数据层(CF 值体系统一):cf_balance / cf_balance_log / cf_earn_rule /
- // cf_earn_rule_history / cf_direct_sales_tier / cf_indirect_sales_tier / cf_earn_daily_limit
- // 口径:docs/superpowers/plans/2026-10-24-cf-value-unification-plan.md §五 + §3.6
- // 仅建表 + 种子,不含任何发放/扣减业务逻辑(F-2 批)
- // ==================================================================================
- // 迁移407: 创建 cf_balance / cf_balance_log(个人 CF 值账本,成员维度)
- private void createCfBalanceTables() {
- try {
- jdbcTemplate.execute("CREATE TABLE IF NOT EXISTS `cf_balance` ("
- + "`id` BIGINT AUTO_INCREMENT PRIMARY KEY, "
- + "`member_id` BIGINT NOT NULL COMMENT '家庭成员ID(family_members.id)', "
- + "`user_id` BIGINT NULL COMMENT '关联登录用户ID(可为空,不能登录成员无账号)', "
- + "`family_id` BIGINT NULL COMMENT '所属家庭ID(冗余,便于聚合)', "
- + "`total_earned` INT NOT NULL DEFAULT 0 COMMENT '累计获得', "
- + "`available` INT NOT NULL DEFAULT 0 COMMENT '可用', "
- + "`frozen` INT NOT NULL DEFAULT 0 COMMENT '提现冻结', "
- + "`withdrawn` INT NOT NULL DEFAULT 0 COMMENT '已提现', "
- + "`exchanged` INT NOT NULL DEFAULT 0 COMMENT '已兑换消费', "
- + "`created_at` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, "
- + "`updated_at` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, "
- + "UNIQUE KEY `uk_member` (`member_id`), "
- + "KEY `idx_family` (`family_id`) "
- + ") ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='个人CF值余额(家庭成员维度)'");
- log.info("迁移407: 已创建 cf_balance 表");
- } catch (Exception e) {
- log.warn("迁移407: 创建 cf_balance 表失败(可能已存在): {}", e.getMessage());
- }
- try {
- jdbcTemplate.execute("CREATE TABLE IF NOT EXISTS `cf_balance_log` ("
- + "`id` BIGINT AUTO_INCREMENT PRIMARY KEY, "
- + "`member_id` BIGINT NOT NULL, "
- + "`user_id` BIGINT NULL, "
- + "`family_id` BIGINT NULL, "
- + "`type` VARCHAR(20) NOT NULL COMMENT 'earn/spend/freeze/unfreeze/withdraw/adjust/skip', "
- + "`ref_type` VARCHAR(50) NOT NULL COMMENT '规则编码或迁移标识(F02-F24 / migration_*)', "
- + "`ref_id` BIGINT NULL, "
- + "`amount` INT NOT NULL COMMENT '变动数量(正负)', "
- + "`balance_after` INT NOT NULL COMMENT '变动后可用余额', "
- + "`operator_user_id` BIGINT NULL COMMENT '操作人用户ID(管理员调整等人工操作留痕)', "
- + "`remark` VARCHAR(255) NULL, "
- + "`created_at` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, "
- + "KEY `idx_member` (`member_id`), "
- + "KEY `idx_family` (`family_id`), "
- + "UNIQUE KEY `uk_ref_type` (`member_id`, `type`, `ref_type`, `ref_id`) "
- + ") ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='个人CF值流水'");
- log.info("迁移407: 已创建 cf_balance_log 表");
- } catch (Exception e) {
- log.warn("迁移407: 创建 cf_balance_log 表失败(可能已存在): {}", e.getMessage());
- }
- }
- // 迁移408: 创建 cf_earn_rule / cf_earn_rule_history(CF 值获取规则 + 变更审计)
- private void createCfEarnRuleTables() {
- try {
- jdbcTemplate.execute("CREATE TABLE IF NOT EXISTS `cf_earn_rule` ("
- + "`id` BIGINT AUTO_INCREMENT PRIMARY KEY, "
- + "`code` VARCHAR(20) NOT NULL COMMENT '规则编码 F02~F24(F03 拆 F03a/F03b)', "
- + "`name` VARCHAR(100) NOT NULL COMMENT '途径名称', "
- + "`category` VARCHAR(20) NOT NULL COMMENT 'consume消费返佣/behavior成长行为/admin管理员', "
- + "`enabled` TINYINT NOT NULL DEFAULT 1 COMMENT '1启用 0停用(随时开关)', "
- + "`rule_type` VARCHAR(20) NOT NULL COMMENT 'fixed固定/formula公式/per_item逐人发放', "
- + "`amount` INT NULL COMMENT '固定数量(rule_type=fixed)', "
- + "`formula` VARCHAR(500) NULL COMMENT '计算公式(rule_type=formula)', "
- + "`params_json` TEXT NULL COMMENT '公式参数(bps/倍数/档位表等,JSON)', "
- + "`daily_limit_per_member` INT NULL COMMENT '单成员每日上限,空=不限', "
- + "`total_limit_per_member` INT NULL COMMENT '单成员累计上限,空=不限', "
- + "`cooldown_seconds` INT NULL COMMENT '同一成员冷却秒数,空=不限', "
- + "`dedup_ref_type` VARCHAR(50) NOT NULL COMMENT '幂等refType,写入cf_balance_log', "
- + "`effective_from` DATETIME NULL COMMENT '生效起,空=立即', "
- + "`effective_to` DATETIME NULL COMMENT '生效止,空=长期', "
- + "`sort_order` INT NOT NULL DEFAULT 0, "
- + "`remark` VARCHAR(255) NULL, "
- + "`created_at` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, "
- + "`updated_at` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, "
- + "`updated_by` BIGINT NULL COMMENT '最后修改人', "
- + "UNIQUE KEY `uk_code` (`code`) "
- + ") ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='CF值获取规则(运营可配置)'");
- log.info("迁移408: 已创建 cf_earn_rule 表");
- } catch (Exception e) {
- log.warn("迁移408: 创建 cf_earn_rule 表失败(可能已存在): {}", e.getMessage());
- }
- try {
- jdbcTemplate.execute("CREATE TABLE IF NOT EXISTS `cf_earn_rule_history` ("
- + "`id` BIGINT AUTO_INCREMENT PRIMARY KEY, "
- + "`rule_code` VARCHAR(20) NOT NULL, "
- + "`old_json` TEXT NULL COMMENT '变更前整行快照', "
- + "`new_json` TEXT NULL COMMENT '变更后整行快照', "
- + "`changed_by` BIGINT NOT NULL, "
- + "`change_reason` VARCHAR(255) NULL, "
- + "`created_at` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, "
- + "KEY `idx_rule` (`rule_code`, `created_at`) "
- + ") ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='CF值规则变更历史'");
- log.info("迁移408: 已创建 cf_earn_rule_history 表");
- } catch (Exception e) {
- log.warn("迁移408: 创建 cf_earn_rule_history 表失败(可能已存在): {}", e.getMessage());
- }
- }
- /**
- * 24 行规则种子(F02–F24,F03 拆 F03a/F03b),列序:
- * code, name, category, ruleType, amount, formula, paramsJson, dailyLimit, totalLimit,
- * cooldownSeconds, dedupRefType, sortOrder, remark
- *
- * 数值口径:§三 各条「参数来源」+ §九 修正后数值(F06=1990、F18 与 F02 同口径)。
- * 注意:§三 公式写作 floor(实付(分)/100)×100,与 §九「19.9 元=1990 CF」不自洽;
- * 按 §九 修正口径执行 —— 1 元=100 CF 即 实付(分)→CF 1:1,公式记作 floor(amt)。
- */
- private static final String[][] CF_EARN_RULE_SEEDS = {
- {"F02", "消费返CF(所有订单)", "consume", "formula", "",
- "floor(amt)",
- "{\"cf_per_yuan\":100,\"amt_unit\":\"cent\",\"note\":\"1元=100CF,实付分→CF 1:1\"}",
- "", "", "", "cf_consume_order", "10",
- "所有消费订单(商品/套餐/测评/会员/订阅/调研)均返"},
- {"F03a", "直接销售奖", "consume", "formula", "",
- "floor(base_ppoints * rate_percent / 100)",
- "{\"base\":\"team_annual_ppoints\",\"tier_table\":\"cf_direct_sales_tier\",\"window\":\"1y_rolling\",\"team_depth\":8,\"max_rate_percent\":70,\"downgrade\":true}",
- "", "", "", "F03a", "20",
- "基数=直接推荐人消费P点;档位按团队8层内年度累计P点(cf_direct_sales_tier);幂等ref_type与规则编码一致(F-4)"},
- {"F03b", "间接销售奖", "consume", "formula", "",
- "floor(base_ppoints * rate_percent / 100)",
- "{\"base\":\"self_annual_ppoints\",\"tier_table\":\"cf_indirect_sales_tier\",\"window\":\"1y_rolling\",\"max_rate_percent\":20,\"downgrade\":true}",
- "", "", "", "F03b", "30",
- "基数=间接推荐人消费P点;档位按自身年度累计消费P点(cf_indirect_sales_tier);幂等ref_type与规则编码一致(F-4)"},
- {"F04", "商品赠品CF", "consume", "per_item", "",
- "",
- "{\"source\":\"product_order_gift.cf_value\"}",
- "", "", "", "product_order_gift", "40",
- "赠品规则管理端配置"},
- {"F05", "活动签到CF", "consume", "per_item", "",
- "",
- "{\"source\":\"activities.platform_points\"}",
- "", "", "", "activity_checkin", "50",
- "活动配置数量"},
- {"F06", "菌群健康调研打卡返还", "consume", "formula", "",
- "floor(amt)",
- "{\"cf_per_yuan\":100,\"amt_unit\":\"cent\",\"fallback\":\"gut_health_project.cf_refund_amount\",\"example\":\"19.9元=1990CF\"}",
- "7", "1", "", "gut_health_refund", "60",
- "cf_refund_amount 为空时按项目价×100(19.9元=1990CF,§九修正值);一周项目只发一次"},
- {"F07", "文章答题奖励", "behavior", "formula", "",
- "score + (all_correct ? 3 : 0)",
- "{\"bonus_all_correct\":3,\"dedup\":\"article\"}",
- "5", "", "", "article_answer", "70",
- "答对题数+全对3;日限5次(N9)"},
- {"F08", "AI阅读答题奖励", "behavior", "fixed", "5",
- "",
- "{\"require_all_correct\":true}",
- "3", "", "", "ai_reading_answer", "80",
- "仅全部答对才发(原硬编码已外置)"},
- {"F09", "完成测评建档", "behavior", "formula", "",
- "first_report ? 50 : 10",
- "{\"first_amount\":50,\"repeat_amount\":10,\"dedup_key\":\"report_hash\"}",
- "3", "", "", "assessment_report", "90",
- "第1份50/后续每份10,重复报告不计(N10)"},
- {"F10", "家庭挑战完成", "behavior", "per_item", "",
- "",
- "{\"source\":\"family_challenge.reward_points\",\"distribution\":\"per_completed_member\"}",
- "1", "1", "", "family_challenge", "100",
- "按挑战配置数量(Q1 已定:每个完成成员各发一次)"},
- {"F11", "邀请里程碑", "behavior", "per_item", "",
- "",
- "{\"source\":\"invite_milestone.reward_points\"}",
- "", "", "", "invite_milestone", "110",
- "按里程碑配置"},
- {"F12", "成长任务", "behavior", "per_item", "",
- "",
- "{\"source\":\"growth_task.reward_points\"}",
- "", "", "", "growth_task", "120",
- "按任务模板配置"},
- {"F13", "分享阅读奖励", "behavior", "fixed", "5",
- "",
- "{\"source\":\"cf.rate.share_read_reward_points\"}",
- "5", "", "", "share_read", "130",
- "默认5CF;日限5次"},
- {"F14", "喝水打卡", "behavior", "fixed", "5",
- "",
- "{\"daily_limit_by_tier\":\"cf_earn_daily_limit\"}",
- "", "", "", "health_water", "140",
- "5CF/次;日限按会员等级(N11)"},
- {"F15", "饮食打卡", "behavior", "fixed", "5",
- "",
- "{\"daily_limit_by_tier\":\"cf_earn_daily_limit\"}",
- "", "", "", "health_meal", "150",
- "5CF/次;日限按会员等级(N11)"},
- {"F16", "每日健康打卡", "behavior", "fixed", "10",
- "",
- "{\"daily_limit_by_tier\":\"cf_earn_daily_limit\"}",
- "", "", "", "daily_health_checkin", "160",
- "10CF/次;日限按会员等级(N11)"},
- {"F17", "挑战赛子任务完成", "behavior", "per_item", "",
- "",
- "{\"source\":\"challenge_task.reward_cf\"}",
- "", "", "", "challenge_sub_task", "170",
- "按挑战赛任务配置"},
- {"F18", "健康启航计划付费", "consume", "formula", "",
- "floor(amt)",
- "{\"cf_per_yuan\":100,\"amt_unit\":\"cent\",\"note\":\"与F02同口径\"}",
- "1", "1", "", "health_start_plan", "180",
- "实付分→CF 1:1(§九修正口径,原硬编码已外置)"},
- {"F19", "管理员手动调整", "admin", "per_item", "",
- "",
- "{\"require_parent_password\":true,\"allow_negative\":true}",
- "", "", "", "admin_adjust", "190",
- "正负任意,需家长密码校验,必须留痕(Q9)"},
- {"F20", "问题域全部任务完成", "behavior", "per_item", "",
- "",
- "{\"source\":\"problem_domain.reward_cf_value\"}",
- "", "", "", "problem_domain", "200",
- "按管理端配置"},
- {"F21", "关卡通关奖励", "behavior", "per_item", "",
- "",
- "{\"source\":\"unlock_gate.reward_points\"}",
- "", "", "", "unlock_gate", "210",
- "按管理端配置"},
- {"F22", "任务完成CF", "behavior", "per_item", "",
- "",
- "{\"source\":\"task.cf_amount\",\"daily_limit_by_tier\":\"cf_earn_daily_limit\"}",
- "", "", "", "task_complete", "220",
- "保留个人发放,取消家庭池那条;日限按会员等级(N11)"},
- {"F23", "成员间转让(收款方)", "behavior", "per_item", "",
- "",
- "{\"direction\":\"transfer_in\",\"pair_type\":\"transfer_out\"}",
- "", "", "", "cf_transfer", "230",
- "转入+X / 转出−X,同一事务"},
- {"F24", "分享奖励(推荐注册)", "behavior", "fixed", "20",
- "",
- "{\"trigger\":\"referral_register\"}",
- "10", "", "", "referral_register", "240",
- "N8 新增:推荐他人注册即得,建议20CF/人,日限10次"}
- };
- /** 幂等写入 24 行规则种子(已存在则跳过,不覆盖运营改过的配置) */
- private void seedCfEarnRules() {
- int inserted = 0;
- for (String[] r : CF_EARN_RULE_SEEDS) {
- try {
- Integer cnt = jdbcTemplate.queryForObject(
- "SELECT COUNT(*) FROM cf_earn_rule WHERE code = ?", Integer.class, r[0]);
- if (cnt != null && cnt > 0) {
- continue;
- }
- jdbcTemplate.update("INSERT INTO cf_earn_rule (code, name, category, enabled, rule_type, amount, "
- + "formula, params_json, daily_limit_per_member, total_limit_per_member, cooldown_seconds, "
- + "dedup_ref_type, sort_order, remark, created_at, updated_at) "
- + "VALUES (?, ?, ?, 1, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, NOW(), NOW())",
- r[0], r[1], r[2], r[3], cfIntOrNull(r[4]), cfTextOrNull(r[5]), cfTextOrNull(r[6]),
- cfIntOrNull(r[7]), cfIntOrNull(r[8]), cfIntOrNull(r[9]), r[10],
- cfIntOrNull(r[11]) != null ? cfIntOrNull(r[11]) : 0, cfTextOrNull(r[12]));
- inserted++;
- } catch (Exception e) {
- log.warn("迁移408: 写入规则种子 {} 失败: {}", r[0], e.getMessage());
- }
- }
- if (inserted > 0) {
- log.info("迁移408: cf_earn_rule 规则种子新增 {} 行(共 {} 条定义)", inserted, CF_EARN_RULE_SEEDS.length);
- }
- }
- // 迁移409: 创建 cf_direct_sales_tier / cf_indirect_sales_tier(两级销售奖档位,数值由运营配置)
- private void createCfSalesTierTables() {
- try {
- jdbcTemplate.execute("CREATE TABLE IF NOT EXISTS `cf_direct_sales_tier` ("
- + "`id` BIGINT AUTO_INCREMENT PRIMARY KEY, "
- + "`tier_name` VARCHAR(50) NOT NULL COMMENT '档位名称', "
- + "`min_annual_ppoints` INT NOT NULL COMMENT '团队8层内年度累计P点下限', "
- + "`rate_percent` INT NOT NULL COMMENT '分润比例(%)', "
- + "`sort_order` INT NOT NULL DEFAULT 0, "
- + "`enabled` TINYINT NOT NULL DEFAULT 1 COMMENT '1启用 0停用', "
- + "`remark` VARCHAR(255) NULL, "
- + "`created_at` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, "
- + "`updated_at` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, "
- + "UNIQUE KEY `uk_min` (`min_annual_ppoints`) "
- + ") ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='直接销售奖档位(按团队8层内年度P点累计)'");
- log.info("迁移409: 已创建 cf_direct_sales_tier 表");
- } catch (Exception e) {
- log.warn("迁移409: 创建 cf_direct_sales_tier 表失败(可能已存在): {}", e.getMessage());
- }
- try {
- jdbcTemplate.execute("CREATE TABLE IF NOT EXISTS `cf_indirect_sales_tier` ("
- + "`id` BIGINT AUTO_INCREMENT PRIMARY KEY, "
- + "`tier_name` VARCHAR(50) NOT NULL, "
- + "`min_self_ppoints` INT NOT NULL COMMENT '自身年度累计消费P点下限', "
- + "`rate_percent` INT NOT NULL COMMENT '分润比例(%)', "
- + "`sort_order` INT NOT NULL DEFAULT 0, "
- + "`enabled` TINYINT NOT NULL DEFAULT 1 COMMENT '1启用 0停用', "
- + "`remark` VARCHAR(255) NULL, "
- + "`created_at` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, "
- + "`updated_at` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, "
- + "UNIQUE KEY `uk_min` (`min_self_ppoints`) "
- + ") ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='间接销售奖档位(按自身年度消费P点累计)'");
- log.info("迁移409: 已创建 cf_indirect_sales_tier 表");
- } catch (Exception e) {
- log.warn("迁移409: 创建 cf_indirect_sales_tier 表失败(可能已存在): {}", e.getMessage());
- }
- }
- // 迁移410: 创建 cf_earn_daily_limit(规则 × 会员等级 每日次数上限,N11)
- private void createCfEarnDailyLimitTable() {
- try {
- jdbcTemplate.execute("CREATE TABLE IF NOT EXISTS `cf_earn_daily_limit` ("
- + "`id` BIGINT AUTO_INCREMENT PRIMARY KEY, "
- + "`rule_code` VARCHAR(20) NOT NULL COMMENT '规则编码(cf_earn_rule.code)', "
- + "`level_code` VARCHAR(32) NOT NULL COMMENT '会员等级编码 FREE/BASIC/FAMILY/LIFETIME/PREMIUM', "
- + "`daily_limit` INT NOT NULL COMMENT '每日次数上限', "
- + "`remark` VARCHAR(255) NULL, "
- + "`created_at` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, "
- + "`updated_at` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, "
- + "UNIQUE KEY `uk_rule_level` (`rule_code`, `level_code`) "
- + ") ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='CF值规则×会员等级每日次数上限(N11)'");
- log.info("迁移410: 已创建 cf_earn_daily_limit 表");
- } catch (Exception e) {
- log.warn("迁移410: 创建 cf_earn_daily_limit 表失败(可能已存在): {}", e.getMessage());
- }
- }
- /** N11 会员等级日限:FREE=1(建议) / BASIC=3 / FAMILY=10 / LIFETIME=10 / PREMIUM=20,PROVIDER 待定不落行 */
- private static final String[][] CF_DAILY_LIMIT_LEVELS = {
- {"FREE", "1", "免费会员(建议值,Q11)"},
- {"BASIC", "3", "9.9体验会员"},
- {"FAMILY", "10", "家庭会员"},
- {"LIFETIME", "10", "终身会员"},
- {"PREMIUM", "20", "高级会员"}
- };
- /** 按会员等级限次的规则(任务/打卡类):F14 喝水 / F15 饮食 / F16 每日健康 / F22 任务完成 */
- private static final String[] CF_DAILY_LIMIT_RULES = {"F14", "F15", "F16", "F22"};
- private void seedCfEarnDailyLimits() {
- int inserted = 0;
- for (String ruleCode : CF_DAILY_LIMIT_RULES) {
- for (String[] lv : CF_DAILY_LIMIT_LEVELS) {
- try {
- Integer cnt = jdbcTemplate.queryForObject(
- "SELECT COUNT(*) FROM cf_earn_daily_limit WHERE rule_code = ? AND level_code = ?",
- Integer.class, ruleCode, lv[0]);
- if (cnt != null && cnt > 0) {
- continue;
- }
- jdbcTemplate.update("INSERT INTO cf_earn_daily_limit (rule_code, level_code, daily_limit, remark, created_at, updated_at) "
- + "VALUES (?, ?, ?, ?, NOW(), NOW())", ruleCode, lv[0], Integer.valueOf(lv[1]), lv[2]);
- inserted++;
- } catch (Exception e) {
- log.warn("迁移410: 写入日限种子 {}/{} 失败: {}", ruleCode, lv[0], e.getMessage());
- }
- }
- }
- if (inserted > 0) {
- log.info("迁移410: cf_earn_daily_limit 日限种子新增 {} 行", inserted);
- }
- }
- // 迁移411: coupon / user_coupon 补 family_id(优惠券家庭化 N3)
- private void addCouponFamilyIdColumns() {
- if (!tableExists("coupon") || !tableExists("user_coupon")) {
- log.warn("迁移411: coupon / user_coupon 表不存在,跳过 family_id 迁移");
- return;
- }
- ensureColumn("coupon", "family_id", "BIGINT DEFAULT NULL COMMENT '模板块绑定家庭ID(NULL=全平台模板)'");
- ensureColumn("user_coupon", "family_id", "BIGINT DEFAULT NULL COMMENT '券实例归属家庭ID(家庭内任意成员可用)'");
- try {
- if (!indexExists("user_coupon", "idx_family")) {
- jdbcTemplate.execute("ALTER TABLE user_coupon ADD INDEX idx_family (family_id)");
- log.info("迁移411: 已添加 user_coupon.idx_family 索引");
- }
- } catch (Exception e) {
- log.warn("迁移411: 添加 user_coupon.idx_family 失败(可能已存在): {}", e.getMessage());
- }
- try {
- if (!indexExists("coupon", "idx_family")) {
- jdbcTemplate.execute("ALTER TABLE coupon ADD INDEX idx_family (family_id)");
- log.info("迁移411: 已添加 coupon.idx_family 索引");
- }
- } catch (Exception e) {
- log.warn("迁移411: 添加 coupon.idx_family 失败(可能已存在): {}", e.getMessage());
- }
- }
- // 迁移412: 创建 service_config(L2 配置中心;W0.1)
- private void createServiceConfigTable() {
- try {
- jdbcTemplate.execute("CREATE TABLE IF NOT EXISTS `service_config` ("
- + "`id` BIGINT AUTO_INCREMENT PRIMARY KEY, "
- + "`group_key` VARCHAR(64) NOT NULL DEFAULT 'general' COMMENT '配置分组(如 cf.rate / cf.commission)', "
- + "`config_key` VARCHAR(128) NOT NULL COMMENT '配置键(如 cf.rate.points_multiplier)', "
- + "`config_value` TEXT NULL COMMENT '配置值(统一字符串存储,按 value_type 解析)', "
- + "`value_type` VARCHAR(16) NOT NULL DEFAULT 'string' COMMENT 'string/int/double/bool/json', "
- + "`description` VARCHAR(255) NULL, "
- + "`editable` TINYINT NOT NULL DEFAULT 1 COMMENT '1允许后台编辑 0系统托管', "
- + "`updated_by` BIGINT NULL, "
- + "`created_at` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, "
- + "`updated_at` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, "
- + "UNIQUE KEY `uk_config_key` (`config_key`), "
- + "KEY `idx_group_key` (`group_key`) "
- + ") ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='服务配置中心(L2,CF通用运营参数归口)'");
- log.info("迁移412: 已创建 service_config 表");
- } catch (Exception e) {
- log.warn("迁移412: 创建 service_config 表失败(可能已存在): {}", e.getMessage());
- }
- }
- /** W0.1 §3.6.6(2) 10 项 CF 通用运营参数 + cf.version 版本号 */
- private void seedCfServiceConfig() {
- ensureServiceConfig("cf.rate.points_multiplier", "1.0", "double",
- "积分倍数(消费→CF 换算);原散落 products.points_multiplier / sys_config", 1);
- ensureServiceConfig("cf.rate.repeat_rate_default", "1.0", "double",
- "复购率默认系数(未匹配规则时不加权)", 1);
- ensureServiceConfig("cf.commission.settle_days", "7", "int",
- "佣金结算周期(天),财务口径常用", 1);
- ensureServiceConfig("cf.invite.valid_days", "7", "int",
- "邀请码有效期(天)", 1);
- ensureServiceConfig("cf.price.default_factor", "1.0", "double",
- "默认价格系数(未定价时原价)", 1);
- ensureServiceConfig("cf.stock.deduction_mode", "immediate", "string",
- "库存扣减策略 immediate/on_pay(Phase 4 定稿)", 1);
- ensureServiceConfig("cf.rate.share_read_reward_points", "5", "int",
- "分享阅读奖励 CF;原散落 sys_config.share_read_reward_points", 1);
- ensureServiceConfig("cf.commission.service_rate", "1000", "int",
- "佣金服务费率(bps);原散落 sys_config.commission_service_rate", 1);
- ensureServiceConfig("cf.product.gut_health_price", "1990", "int",
- "肠道健康项目定价(分);原散落 GutHealthProjectService.DEFAULT_PRICE / sys_config", 1);
- ensureServiceConfig("cf.product.platform_points_share", "1000", "int",
- "产品平台积分分成比例(bps);原散落 sys_config.product_platform_points_share", 1);
- ensureServiceConfig("cf.version", "1", "int",
- "CF 配置版本号(单调递增,前端据此局部刷新提示)", 0);
- }
- /**
- * 迁移416:CF 账本流水表结构调整(F-2)
- * <ul>
- * <li>补 operator_user_id:管理员调整等人工操作留痕操作人;</li>
- * <li>幂等键升级:旧 uk_ref(member_id,ref_type,ref_id) 会让同一业务对象的
- * freeze→unfreeze/withdraw 互相冲突,升级为 uk_ref_type(member_id,type,ref_type,ref_id),
- * earn 幂等口径不变(type='earn');</li>
- * <li>种子:提现开关(默认关闭)+ 人民币→CF 汇率(1 元 = 100 CF,F02 口径)。</li>
- * </ul>
- * 幂等:列/索引/配置均先探测后变更,可重复执行。
- */
- private void migrateCfBalanceLogForLedger() {
- if (!tableExists("cf_balance_log")) {
- log.warn("迁移416: cf_balance_log 表不存在,跳过流水表结构调整");
- return;
- }
- // 1) 操作人留痕列
- ensureColumn("cf_balance_log", "operator_user_id",
- "BIGINT NULL COMMENT '操作人用户ID(管理员调整等人工操作留痕)'");
- // 2) 幂等键升级:先删旧唯一索引,再加新唯一索引
- try {
- if (indexExists("cf_balance_log", "uk_ref")) {
- jdbcTemplate.execute("ALTER TABLE cf_balance_log DROP INDEX uk_ref");
- log.info("迁移416: 已删除 cf_balance_log.uk_ref 旧唯一索引");
- }
- } catch (Exception e) {
- log.warn("迁移416: 删除 cf_balance_log.uk_ref 失败: {}", e.getMessage());
- }
- try {
- if (!indexExists("cf_balance_log", "uk_ref_type")) {
- jdbcTemplate.execute("ALTER TABLE cf_balance_log "
- + "ADD UNIQUE KEY uk_ref_type (member_id, type, ref_type, ref_id)");
- log.info("迁移416: 已添加 cf_balance_log.uk_ref_type 唯一索引");
- }
- } catch (Exception e) {
- log.warn("迁移416: 添加 cf_balance_log.uk_ref_type 失败: {}", e.getMessage());
- }
- // 3) CF 运营参数种子
- ensureServiceConfig("cf.withdraw_enabled", "false", "bool",
- "CF值提现开关(false=禁止提现;Q4 合规审查通过后再开启)", 1);
- ensureServiceConfig("cf.rate.cf_per_yuan", "100", "int",
- "人民币→CF值换算比例(1元=100CF,F02 消费返 CF 口径)", 1);
- }
- /**
- * 迁移散落在 sys_config / products 的 CF 参数到 service_config:
- * 优先取旧位置的实际值(存在则沿用),旧位置仅做标记清理,不删值(F-2/F-3 收口前保持兼容)。
- * 幂等:新 key 已存在则跳过;旧位置标记带唯一前缀,重复执行不重复追加。
- */
- private void migrateScatteredCfConfigs() {
- migrateSysConfigToServiceConfig("commission_service_rate", "cf.commission.service_rate", "1000");
- migrateSysConfigToServiceConfig("product_platform_points_share", "cf.product.platform_points_share", "1000");
- migrateSysConfigToServiceConfig("share_read_reward_points", "cf.rate.share_read_reward_points", "5");
- migrateSysConfigToServiceConfig("gut_health_project_price", "cf.product.gut_health_price", "1990");
- // products.points_multiplier(列)→ cf.rate.points_multiplier,并标记列注释
- try {
- if (!serviceConfigExists("cf.rate.points_multiplier")) {
- String val = null;
- try {
- // 仅当所有商品倍数一致时才沿用该值,否则回落到 1.0(避免把某个商品的特殊倍数当成全局口径)
- Integer distinct = jdbcTemplate.queryForObject(
- "SELECT COUNT(DISTINCT points_multiplier) FROM products WHERE points_multiplier IS NOT NULL",
- Integer.class);
- if (distinct != null && distinct == 1) {
- val = jdbcTemplate.queryForObject(
- "SELECT CAST(points_multiplier AS CHAR) FROM products WHERE points_multiplier IS NOT NULL LIMIT 1",
- String.class);
- }
- } catch (Exception ignored) {
- // 列不存在或表为空
- }
- ensureServiceConfig("cf.rate.points_multiplier", val != null ? val : "1.0", "double",
- "积分倍数(消费→CF 换算);原散落 products.points_multiplier", 1);
- }
- markProductsPointsMultiplierMigrated();
- } catch (Exception e) {
- log.warn("迁移412: products.points_multiplier 迁移失败: {}", e.getMessage());
- }
- }
- private void migrateSysConfigToServiceConfig(String oldKey, String newKey, String defaultVal) {
- try {
- if (serviceConfigExists(newKey)) {
- markSysConfigMigrated(oldKey, newKey);
- return;
- }
- String val = null;
- try {
- val = jdbcTemplate.queryForObject(
- "SELECT config_value FROM sys_config WHERE config_key = ? LIMIT 1", String.class, oldKey);
- } catch (Exception ignored) {
- // 旧配置不存在,使用默认值
- }
- ensureServiceConfig(newKey, val != null && !val.trim().isEmpty() ? val : defaultVal,
- cfValueTypeOf(defaultVal), "由 sys_config." + oldKey + " 迁入(W0.1)", 1);
- markSysConfigMigrated(oldKey, newKey);
- } catch (Exception e) {
- log.warn("迁移412: {} → {} 迁移失败: {}", oldKey, newKey, e.getMessage());
- }
- }
- /** 旧 sys_config 行标记为已迁移(保留值做过渡兼容,不物理删除) */
- private void markSysConfigMigrated(String oldKey, String newKey) {
- try {
- int updated = jdbcTemplate.update(
- "UPDATE sys_config SET description = CONCAT(COALESCE(description, ''), ?) "
- + "WHERE config_key = ? AND (description IS NULL OR description NOT LIKE '%【已迁移→service_config%')",
- " 【已迁移→service_config " + newKey + ",本行仅过渡期兼容】", oldKey);
- if (updated > 0) {
- log.info("迁移412: 已标记 sys_config.{} 迁往 service_config.{}", oldKey, newKey);
- }
- } catch (Exception e) {
- log.warn("迁移412: 标记 sys_config.{} 失败: {}", oldKey, e.getMessage());
- }
- }
- /** products.points_multiplier 列注释标记(列保留兼容,不再作为权威来源) */
- private void markProductsPointsMultiplierMigrated() {
- try {
- String comment = jdbcTemplate.queryForObject(
- "SELECT COLUMN_COMMENT FROM information_schema.COLUMNS "
- + "WHERE TABLE_SCHEMA = DATABASE() AND TABLE_NAME = 'products' AND COLUMN_NAME = 'points_multiplier'",
- String.class);
- if (comment != null && comment.contains("已迁移")) {
- return;
- }
- jdbcTemplate.execute("ALTER TABLE products MODIFY COLUMN points_multiplier DECIMAL(10,2) NOT NULL DEFAULT 1.00 "
- + "COMMENT '积分倍数【已迁移→service_config cf.rate.points_multiplier,本列仅过渡期兼容】'");
- log.info("迁移412: 已标记 products.points_multiplier 迁往 service_config.cf.rate.points_multiplier");
- } catch (Exception e) {
- log.warn("迁移412: 标记 products.points_multiplier 失败: {}", e.getMessage());
- }
- }
- private boolean serviceConfigExists(String key) {
- try {
- Integer cnt = jdbcTemplate.queryForObject(
- "SELECT COUNT(*) FROM service_config WHERE config_key = ?", Integer.class, key);
- return cnt != null && cnt > 0;
- } catch (Exception e) {
- return false;
- }
- }
- /** 幂等写入 service_config(已存在则跳过,不覆盖运营改过的值) */
- private void ensureServiceConfig(String key, String value, String valueType, String description, int editable) {
- try {
- if (serviceConfigExists(key)) {
- return;
- }
- jdbcTemplate.update("INSERT INTO service_config (group_key, config_key, config_value, value_type, description, editable, created_at, updated_at) "
- + "VALUES (?, ?, ?, ?, ?, ?, NOW(), NOW())",
- groupKeyOfConfig(key), key, value, valueType, description, editable);
- } catch (Exception e) {
- log.warn("写入 service_config.{} 失败: {}", key, e.getMessage());
- }
- }
- /** cf.rate.points_multiplier → cf.rate */
- private String groupKeyOfConfig(String key) {
- if (key == null) {
- return "general";
- }
- int idx = key.lastIndexOf('.');
- return idx > 0 ? key.substring(0, idx) : "general";
- }
- // ==================== 迁移413:W0.2 双配置中心合并(service_config 收编 product_config) ====================
- /**
- * 迁移413-1:service_config 增加 product_key 列(product 侧配置关联键;NULL = 全局/CF 通用参数)。
- *
- * 幂等:service_config 不存在 / 列已存在 / 索引已存在均跳过,可重复执行。
- */
- private void addServiceConfigProductKeyColumn() {
- try {
- if (!tableExists("service_config")) {
- log.warn("迁移413: service_config 表不存在(迁移412 未执行?),跳过 product_key 列/索引");
- return;
- }
- if (columnExists("service_config", "product_key")) {
- log.info("迁移413: service_config.product_key 已存在,跳过加列");
- } else {
- ensureColumn("service_config", "product_key",
- "VARCHAR(64) DEFAULT NULL COMMENT '关联产品标识(product 侧配置;NULL=全局/CF 通用参数)'");
- log.info("迁移413: 已为 service_config 添加 product_key 列");
- }
- if (!indexExists("service_config", "idx_product_key")) {
- jdbcTemplate.execute("ALTER TABLE service_config ADD KEY idx_product_key (product_key)");
- log.info("迁移413: 已创建 service_config.idx_product_key 索引");
- }
- } catch (Exception e) {
- log.warn("迁移413: 添加 product_key 列/索引失败: {}", e.getMessage());
- }
- }
- /**
- * 迁移413-2:把 product_config 表数据并入 service_config(product_key 填对应产品标识)。
- *
- * <p>product_key 标识规则(按 product_config 实际列优先级取第一个存在的列):</p>
- * <ol>
- * <li>{@code product_key}(字符串产品标识)</li>
- * <li>{@code product_code}(产品编码)</li>
- * <li>{@code product_id}(产品主键,CAST 为字符串;唯一索引冲突风险由 idempotentKey 拼接规避)</li>
- * <li>group_key(计划文档 §3.6.6 中唯一保证存在的关联字段,作为兜底产品标识)</li>
- * <li>以上皆缺 → {@code default}</li>
- * </ol>
- *
- * <p>config_key 写入规则:product 侧键统一加 {@code product.{productKey}.} 前缀
- * (见 {@link com.etotem.cfc.constant.ProductConfigKeys}),保证 config_key 全局唯一
- * (service_config 唯一键 uk_config_key 保留不动,W0.2 不改 W0.1 种子与索引)。</p>
- *
- * <p>幂等:本仓库基线(origin/cfclub / 测试库)不存在 product_config 表,迁移为空跑;
- * 若目标环境存在该表,按「service_config 中不存在同 key 才插入」幂等,INSERT IGNORE 兜底唯一键冲突。</p>
- */
- private void migrateProductConfigToServiceConfig() {
- if (!tableExists("product_config")) {
- log.info("迁移413: product_config 表不存在(本基线无该表),跳过数据并入(幂等空跑)");
- return;
- }
- try {
- String groupCol = firstExistingColumn("product_config", "group_key", "groupKey");
- String keyCol = firstExistingColumn("product_config", "config_key", "configKey");
- String valueCol = firstExistingColumn("product_config", "config_value", "configValue");
- String typeCol = firstExistingColumn("product_config", "value_type", "valueType");
- String descCol = firstExistingColumn("product_config", "description");
- String editableCol = firstExistingColumn("product_config", "editable");
- String upByCol = firstExistingColumn("product_config", "updated_by", "updatedBy");
- String createdAtCol = firstExistingColumn("product_config", "created_at", "createdAt");
- String updatedAtCol = firstExistingColumn("product_config", "updated_at", "updatedAt");
- if (keyCol == null) {
- log.warn("迁移413: product_config 缺少配置键列(config_key/configKey),无法并入,跳过");
- return;
- }
- // 产品标识表达式(优先级:product_key → product_code → product_id → group_key → 'default')
- // 统一加 p. 前缀限定,避免与 service_config 同名列歧义
- String productKeyExpr = null;
- for (String c : new String[]{"product_key", "product_code"}) {
- if (columnExists("product_config", c)) {
- productKeyExpr = "NULLIF(TRIM(p.`" + c + "`), '')";
- break;
- }
- }
- if (productKeyExpr == null && columnExists("product_config", "product_id")) {
- productKeyExpr = "CAST(p.`product_id` AS CHAR)";
- }
- if (productKeyExpr == null && groupCol != null) {
- productKeyExpr = "NULLIF(TRIM(p.`" + groupCol + "`), '')";
- }
- if (productKeyExpr == null) {
- productKeyExpr = "'" + com.etotem.cfc.constant.ProductConfigKeys.DEFAULT_PRODUCT_KEY + "'";
- }
- StringBuilder sql = new StringBuilder("INSERT IGNORE INTO service_config ")
- .append("(group_key, config_key, config_value, value_type, description, editable, product_key, updated_by, created_at, updated_at) ")
- .append("SELECT ");
- sql.append("'").append(com.etotem.cfc.constant.ProductConfigKeys.PRODUCT_GROUP_KEY).append("', ");
- // config_key = product.{productKey}.{原key}(保证全局唯一;截断到 128 防超长)
- sql.append("LEFT(CONCAT('").append(com.etotem.cfc.constant.ProductConfigKeys.PRODUCT_KEY_PREFIX)
- .append("', COALESCE(").append(productKeyExpr).append(", '")
- .append(com.etotem.cfc.constant.ProductConfigKeys.DEFAULT_PRODUCT_KEY).append("'), '.', p.`")
- .append(keyCol).append("`), 128), ");
- sql.append(valueCol != null ? "p.`" + valueCol + "`, " : "NULL, ");
- sql.append(typeCol != null ? "COALESCE(NULLIF(TRIM(p.`" + typeCol + "`), ''), 'string'), " : "'string', ");
- sql.append(descCol != null ? "LEFT(p.`" + descCol + "`, 255), " : "NULL, ");
- sql.append(editableCol != null ? "COALESCE(p.`" + editableCol + "`, 1), " : "1, ");
- sql.append("LEFT(COALESCE(").append(productKeyExpr).append(", '")
- .append(com.etotem.cfc.constant.ProductConfigKeys.DEFAULT_PRODUCT_KEY).append("'), 64), ");
- sql.append(upByCol != null ? "p.`" + upByCol + "`, " : "NULL, ");
- sql.append(createdAtCol != null ? "COALESCE(p.`" + createdAtCol + "`, NOW()), " : "NOW(), ");
- sql.append(updatedAtCol != null ? "COALESCE(p.`" + updatedAtCol + "`, NOW()) " : "NOW() ");
- sql.append("FROM product_config p WHERE NOT EXISTS "
- + "(SELECT 1 FROM service_config s WHERE s.config_key = CONCAT('"
- + com.etotem.cfc.constant.ProductConfigKeys.PRODUCT_KEY_PREFIX
- + "', COALESCE(").append(productKeyExpr).append(", '")
- .append(com.etotem.cfc.constant.ProductConfigKeys.DEFAULT_PRODUCT_KEY).append("'), '.', p.`")
- .append(keyCol).append("`))");
- int rows = jdbcTemplate.update(sql.toString());
- log.info("迁移413: product_config → service_config 并入完成,新增 {} 行", rows);
- } catch (Exception e) {
- log.warn("迁移413: product_config 数据并入失败(跳过,可重试):{}", e.getMessage());
- }
- }
- /** 返回 product_config 中第一个实际存在的列名(蛇形优先,兼容驼峰),全部缺失返回 null */
- private String firstExistingColumn(String table, String... candidates) {
- for (String c : candidates) {
- if (columnExists(table, c)) {
- return c;
- }
- }
- return null;
- }
- private String cfValueTypeOf(String value) {
- if (value == null) {
- return "string";
- }
- try {
- Integer.parseInt(value.trim());
- return "int";
- } catch (NumberFormatException ignored) {
- // 非整数
- }
- try {
- Double.parseDouble(value.trim());
- return "double";
- } catch (NumberFormatException ignored) {
- // 非数值
- }
- return "string";
- }
- private Integer cfIntOrNull(String v) {
- if (v == null || v.trim().isEmpty()) {
- return null;
- }
- try {
- return Integer.valueOf(v.trim());
- } catch (NumberFormatException e) {
- return null;
- }
- }
- private String cfTextOrNull(String v) {
- return (v == null || v.trim().isEmpty()) ? null : v;
- }
- private boolean indexExists(String table, String indexName) {
- try {
- Integer cnt = jdbcTemplate.queryForObject(
- "SELECT COUNT(*) FROM information_schema.STATISTICS WHERE TABLE_SCHEMA = DATABASE() "
- + "AND TABLE_NAME = ? AND INDEX_NAME = ?",
- Integer.class, table, indexName);
- return cnt != null && cnt > 0;
- } catch (Exception e) {
- return false;
- }
- }
- /**
- * 迁移414: 创建 audit_log 表(管理端写操作统一审计,W0.3;表只增不删改)。
- * 幂等:CREATE TABLE IF NOT EXISTS + 索引存在性检查,可重复执行。
- */
- private void createAuditLogTable() {
- try {
- jdbcTemplate.execute("CREATE TABLE IF NOT EXISTS audit_log (" +
- "id BIGINT AUTO_INCREMENT PRIMARY KEY," +
- "operator_id BIGINT COMMENT '操作人ID(关联 users.id)'," +
- "operator_name VARCHAR(100) COMMENT '操作人姓名'," +
- "module VARCHAR(64) NOT NULL COMMENT '审计模块(config/earn_rule/tier/balance)'," +
- "action VARCHAR(32) NOT NULL COMMENT '动作类型(save/update/toggle/delete/adjust)'," +
- "target_type VARCHAR(32) NOT NULL COMMENT '目标类型(service_config/cf_earn_rule/cf_rate_tier/cf_balance)'," +
- "target_id VARCHAR(128) COMMENT '目标标识(配置key / 规则code / 主键ID)'," +
- "before_value TEXT COMMENT '变更前值(JSON)'," +
- "after_value TEXT COMMENT '变更后值(JSON)'," +
- "ip VARCHAR(45) COMMENT '操作来源IP'," +
- "created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '发生时间'," +
- "KEY idx_module_target_created (module, target_id, created_at)" +
- ") ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='审计日志(管理端写操作前后值,W0.3)'");
- log.info("迁移414: audit_log 表已就绪");
- } catch (Exception e) {
- log.warn("迁移414: 创建 audit_log 表失败(表可能已存在): {}", e.getMessage());
- }
- // 索引兜底:表已存在但索引缺失时补建(幂等)
- try {
- if (!indexExists("audit_log", "idx_module_target_created")) {
- jdbcTemplate.execute("ALTER TABLE audit_log ADD INDEX idx_module_target_created (module, target_id, created_at)");
- log.info("迁移414: 已为 audit_log 补建索引 idx_module_target_created");
- }
- } catch (Exception e) {
- log.warn("迁移414: audit_log 索引检查/补建失败(可能已存在): {}", e.getMessage());
- }
- }
- /**
- * 迁移415: 创建 family_food_overview 表(家庭×食材 的持久化最小值总览,T28)。
- * 家庭总览口径:家庭成员对该项食材最新推荐指数的最小值(最保守值),落库持久化。
- * 幂等:CREATE TABLE IF NOT EXISTS + 索引存在性检查,可重复执行。
- */
- private void createFamilyFoodOverviewTable() {
- try {
- jdbcTemplate.execute("CREATE TABLE IF NOT EXISTS family_food_overview (" +
- "id BIGINT AUTO_INCREMENT PRIMARY KEY," +
- "family_id BIGINT NOT NULL COMMENT '家庭ID'," +
- "food_id BIGINT NOT NULL COMMENT '食材ID'," +
- "food_name VARCHAR(100) COMMENT '食材名称快照'," +
- "category VARCHAR(50) COMMENT '分类快照'," +
- "min_index_score INT NOT NULL COMMENT '家庭成员该项食材最新推荐指数的最小值'," +
- "member_count INT NOT NULL DEFAULT 0 COMMENT '参与计算的成员数(有该食材记录的)'," +
- "member_scores JSON COMMENT '成员明细 [{userId,nickname,score}]'," +
- "source_fingerprint VARCHAR(64) COMMENT '源数据指纹(成员指数快照哈希),用于幂等与变更检测'," +
- "calculated_at DATETIME NOT NULL COMMENT '本次计算时间'," +
- "created_at DATETIME DEFAULT CURRENT_TIMESTAMP," +
- "updated_at DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP," +
- "UNIQUE KEY uk_family_food (family_id, food_id)," +
- "INDEX idx_family (family_id)" +
- ") ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='家庭×食材持久化最小值总览(T28)'");
- log.info("迁移415: family_food_overview 表已就绪");
- } catch (Exception e) {
- log.warn("迁移415: 创建 family_food_overview 表失败(表可能已存在): {}", e.getMessage());
- }
- // 索引兜底:表已存在但索引缺失时补建(幂等)
- try {
- if (!indexExists("family_food_overview", "idx_family")) {
- jdbcTemplate.execute("ALTER TABLE family_food_overview ADD INDEX idx_family (family_id)");
- log.info("迁移415: 已为 family_food_overview 补建索引 idx_family");
- }
- } catch (Exception e) {
- log.warn("迁移415: family_food_overview 索引检查/补建失败(可能已存在): {}", e.getMessage());
- }
- }
- // ==================== 迁移418:F-4 分佣收口 ====================
- /**
- * 直接销售奖档位种子(F03a;初始运营值,管理端可调)。
- * 列序:tier_name, min_annual_ppoints, rate_percent, sort_order, remark
- */
- private static final String[][] CF_DIRECT_SALES_TIER_SEEDS = {
- {"新秀", "10000", "10", "1", "团队8层内年度P点≥10000,直接销售奖10%"},
- {"小成", "50000", "25", "2", "团队8层内年度P点≥50000,直接销售奖25%"},
- {"中富", "200000", "40", "3", "团队8层内年度P点≥200000,直接销售奖40%"},
- {"大成", "500000", "55", "4", "团队8层内年度P点≥500000,直接销售奖55%"},
- {"至富", "1000000", "70", "5", "团队8层内年度P点≥1000000,直接销售奖70%"}
- };
- /**
- * 间接销售奖档位种子(F03b;初始运营值,管理端可调)。
- * 列序:tier_name, min_self_ppoints, rate_percent, sort_order, remark
- */
- private static final String[][] CF_INDIRECT_SALES_TIER_SEEDS = {
- {"新秀", "10000", "2", "1", "自身年度消费P点≥10000,间接销售奖2%"},
- {"小成", "50000", "5", "2", "自身年度消费P点≥50000,间接销售奖5%"},
- {"中富", "200000", "10", "3", "自身年度消费P点≥200000,间接销售奖10%"},
- {"大成", "500000", "15", "4", "自身年度消费P点≥500000,间接销售奖15%"},
- {"至富", "1000000", "20", "5", "自身年度消费P点≥1000000,间接销售奖20%"}
- };
- // 迁移418: 两级销售奖档位种子(幂等:min 下限唯一键已存在则跳过,不覆盖运营改过的档位)
- // 迁移418(F-4):缺行才补的档位种子,包内可见以便单测直接调用
- void seedCfSalesTiers() {
- // 迁移431(×100) 后清理旧档残留:旧种子青铜(100)/白银(500)/黄金(2000)/铂金(5000)/钻石(10000)
- // 的 min 阈值与迁移431 新档位(10000/50000/...)不同,缺行才补逻辑会在每次重启重新插入旧档,
- // 导致新旧档并存、按 min DESC 匹配时旧档(如5000,55%)抢在新秀(10000,10%)前。
- // 此处删除不在新档阈值集合内的旧档行(幂等,不影响已改名为新秀/小成/...的 1-5 行)。
- try {
- int cleaned = jdbcTemplate.update(
- "DELETE FROM cf_direct_sales_tier "
- + "WHERE min_annual_ppoints NOT IN (10000, 50000, 200000, 500000, 1000000)");
- int cleaned2 = jdbcTemplate.update(
- "DELETE FROM cf_indirect_sales_tier "
- + "WHERE min_self_ppoints NOT IN (10000, 50000, 200000, 500000, 1000000)");
- if (cleaned > 0 || cleaned2 > 0) {
- log.info("迁移418: 清理旧档位残留 {} 行(直接)/{} 行(间接)", cleaned, cleaned2);
- }
- } catch (Exception e) {
- log.warn("迁移418: 清理旧档位残留失败(幂等容忍): {}", e.getMessage());
- }
- int inserted = 0;
- for (String[] t : CF_DIRECT_SALES_TIER_SEEDS) {
- try {
- Integer cnt = jdbcTemplate.queryForObject(
- "SELECT COUNT(*) FROM cf_direct_sales_tier WHERE min_annual_ppoints = ?",
- Integer.class, Integer.valueOf(t[1]));
- if (cnt != null && cnt > 0) {
- continue;
- }
- jdbcTemplate.update("INSERT INTO cf_direct_sales_tier "
- + "(tier_name, min_annual_ppoints, rate_percent, sort_order, enabled, remark, created_at, updated_at) "
- + "VALUES (?, ?, ?, ?, 1, ?, NOW(), NOW())",
- t[0], Integer.valueOf(t[1]), Integer.valueOf(t[2]), Integer.valueOf(t[3]), t[4]);
- inserted++;
- } catch (Exception e) {
- log.warn("迁移418: 写入直接销售奖档位种子 {} 失败: {}", t[0], e.getMessage());
- }
- }
- for (String[] t : CF_INDIRECT_SALES_TIER_SEEDS) {
- try {
- Integer cnt = jdbcTemplate.queryForObject(
- "SELECT COUNT(*) FROM cf_indirect_sales_tier WHERE min_self_ppoints = ?",
- Integer.class, Integer.valueOf(t[1]));
- if (cnt != null && cnt > 0) {
- continue;
- }
- jdbcTemplate.update("INSERT INTO cf_indirect_sales_tier "
- + "(tier_name, min_self_ppoints, rate_percent, sort_order, enabled, remark, created_at, updated_at) "
- + "VALUES (?, ?, ?, ?, 1, ?, NOW(), NOW())",
- t[0], Integer.valueOf(t[1]), Integer.valueOf(t[2]), Integer.valueOf(t[3]), t[4]);
- inserted++;
- } catch (Exception e) {
- log.warn("迁移418: 写入间接销售奖档位种子 {} 失败: {}", t[0], e.getMessage());
- }
- }
- if (inserted > 0) {
- log.info("迁移418: 两级销售奖档位种子新增 {} 行(缺行才补,已存在不覆盖)", inserted);
- }
- }
- // 迁移418: F03a/F03b 幂等 ref_type 对齐为规则编码(计划 §5.2 ref_type 见 F02-F24 编码;F-4)
- // 迁移418(F-4):ref_type 对齐,包内可见以便单测直接调用
- void alignSalesRuleRefTypes() {
- try {
- int updated = jdbcTemplate.update(
- "UPDATE cf_earn_rule SET dedup_ref_type = 'F03a', updated_at = NOW() "
- + "WHERE code = 'F03a' AND dedup_ref_type <> 'F03a'");
- if (updated > 0) {
- log.info("迁移418: F03a dedup_ref_type 已对齐为 F03a({} 行)", updated);
- }
- } catch (Exception e) {
- log.warn("迁移418: F03a dedup_ref_type 对齐失败: {}", e.getMessage());
- }
- try {
- int updated = jdbcTemplate.update(
- "UPDATE cf_earn_rule SET dedup_ref_type = 'F03b', updated_at = NOW() "
- + "WHERE code = 'F03b' AND dedup_ref_type <> 'F03b'");
- if (updated > 0) {
- log.info("迁移418: F03b dedup_ref_type 已对齐为 F03b({} 行)", updated);
- }
- } catch (Exception e) {
- log.warn("迁移418: F03b dedup_ref_type 对齐失败: {}", e.getMessage());
- }
- }
- // ==================== 迁移420:P 点消费日志(F-4 滚动一年档位数据源) ====================
- // 迁移420: 创建 ppoint_consumption_log(消费支付成功时落一行,按消费时间滚动一年聚合)
- // 迁移420(F-4):建表幂等,包内可见以便单测直接调用
- /**
- * 迁移422 前置:确保 health_indicators.collection_date 物理列存在。
- * 放开 HealthIndicator.collectionDate 的 @TableField(exist=false) 之前必须先跑这一步;
- * 任一环境缺列都会让所有 health_indicators 查询报 Unknown column(报告详情页整体崩溃)。
- */
- void ensureHealthIndicatorsCollectionDateColumn() {
- if (!columnExists("health_indicators", "collection_date")) {
- ensureColumn("health_indicators", "collection_date",
- "DATE DEFAULT NULL COMMENT '指标采集日期(从报告提取的检测日期,非入库时间)'");
- log.info("迁移422: 已补建 health_indicators.collection_date 列");
- } else {
- log.info("迁移422: health_indicators.collection_date 已存在,放开实体映射的前置条件满足");
- }
- }
- /**
- * 迁移422: indicator_values 增加日期来源标记 + 参考范围快照。
- * date_source 区分采样日/上传日;ref_range_snapshot 保证历史判定可复现(字典改了不影响历史)。
- */
- void migrateIndicatorValuesTimelineColumns() {
- ensureColumn("indicator_values", "date_source",
- "VARCHAR(16) DEFAULT 'unknown' COMMENT 'report_date 来源: real=采样日/upload=上传日/confirm=确认日/unknown=未知(存量)'");
- ensureColumn("indicator_values", "ref_range_snapshot",
- "VARCHAR(100) DEFAULT NULL COMMENT '该期参考范围快照(为空时回落 indicator_definitions.ref_range)'");
- log.info("迁移422: indicator_values 已补齐日期来源与参考范围快照 2 列");
- }
- // ==================== 迁移423:体脂秤测量记录表 ====================
- /**
- * 迁移423: 体脂秤测量记录表(一次测量一条记录)。
- * 现状 sourceId=memberId + insert 会在第二次测量时撞 uk_source_definition 导致整批保存失败;
- * 本表提供 recordId 作为 indicator_values.source_id 的新取值语义(服务层改造见 T05)。
- */
- void createBodyFatScaleRecordsTable() {
- try {
- jdbcTemplate.execute("CREATE TABLE IF NOT EXISTS `body_fat_scale_records` ("
- + "`id` BIGINT AUTO_INCREMENT PRIMARY KEY, "
- + "`member_id` BIGINT NOT NULL COMMENT '家庭成员ID(family_members.id)', "
- + "`family_id` BIGINT NULL COMMENT '所属家庭', "
- + "`image_url` VARCHAR(500) NULL COMMENT '体脂秤照片', "
- + "`measured_at` DATE NULL COMMENT '测量日期(取不到为NULL,观测 report_date 记为上传日)', "
- + "`created_at` DATETIME DEFAULT CURRENT_TIMESTAMP, "
- + "INDEX `idx_member_date` (`member_id`, `measured_at`) "
- + ") ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='体脂秤测量记录'");
- log.info("迁移423: 已创建 body_fat_scale_records 表");
- } catch (Exception e) {
- log.warn("迁移423: 创建 body_fat_scale_records 表失败(可能已存在): {}", e.getMessage());
- }
- }
- /**
- * 迁移427: 内容维度阈值显隐配置表(A-后端)
- * - content_type: 'PRODUCT'|'ARTICLE'|'ACTIVITY'|'MINIGAME'
- * - dimension_code: 五维码(body/mind/wisdom/action/wealth)/子项码/自定义指标码
- * - threshold: 0-100,与 FiveDimensionScore.score 同量纲
- * - 显隐规则(OR 逻辑,在前端/列表服务层计算):任一维度得分 < 阈值 → 显示;全部维度得分 >= 阈值 → 隐藏
- * 幂等:CREATE TABLE IF NOT EXISTS + 异常兜底。
- */
- void createContentDimensionThresholdTable() {
- try {
- jdbcTemplate.execute("CREATE TABLE IF NOT EXISTS `content_dimension_threshold` ("
- + "`id` BIGINT AUTO_INCREMENT PRIMARY KEY, "
- + "`content_type` VARCHAR(20) NOT NULL COMMENT '内容类型: PRODUCT/ARTICLE/ACTIVITY/MINIGAME', "
- + "`content_id` BIGINT NOT NULL COMMENT '内容ID(products/articles/activities/mini_games 主键)', "
- + "`dimension_code` VARCHAR(50) NOT NULL COMMENT '维度码: 五维码/子项码/自定义指标码', "
- + "`dimension_name` VARCHAR(100) NULL COMMENT '展示名(自定义指标即自定义名)', "
- + "`threshold` INT NOT NULL COMMENT '阈值 0-100', "
- + "`is_custom` TINYINT DEFAULT 0 COMMENT '1=管理员自定义指标', "
- + "`enabled` TINYINT DEFAULT 1 COMMENT '1=启用', "
- + "`created_at` DATETIME DEFAULT CURRENT_TIMESTAMP, "
- + "`updated_at` DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, "
- + "UNIQUE KEY `uk_content_dim` (`content_type`, `content_id`, `dimension_code`) "
- + ") ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='内容维度阈值显隐配置'");
- log.info("迁移427: 已创建 content_dimension_threshold 表");
- } catch (Exception e) {
- log.warn("迁移427: 创建 content_dimension_threshold 表失败(可能已存在): {}", e.getMessage());
- }
- }
- // ==================== 迁移428:会员优惠券转赠 ====================
- /**
- * 迁移428: 会员优惠券转赠。
- * <p>- 新建 coupon_gift_transfer 表(记录券实例从赠送方家庭转移到接收方家庭的过程,支持撤回)。
- * - family_coupon 增加 is_gift 列(1=来自转赠,接收方可被非会员用于购买会员)。</p>
- * 幂等:建表用 IF NOT EXISTS,加列前检查 information_schema。
- */
- void createCouponGiftTransferTable() {
- try {
- jdbcTemplate.execute("CREATE TABLE IF NOT EXISTS `coupon_gift_transfer` ("
- + "`id` BIGINT AUTO_INCREMENT PRIMARY KEY, "
- + "`giver_user_id` BIGINT NOT NULL COMMENT '赠送人用户ID(users.id)', "
- + "`giver_family_id` BIGINT NOT NULL COMMENT '赠送人家庭ID', "
- + "`receiver_user_id` BIGINT NOT NULL COMMENT '接收人用户ID(users.id)', "
- + "`receiver_family_id` BIGINT NOT NULL COMMENT '接收人家庭ID', "
- + "`coupon_id` BIGINT NOT NULL COMMENT '券模板ID(coupon.id)', "
- + "`family_coupon_id` BIGINT NOT NULL COMMENT '被转移的 family_coupon 实例ID', "
- + "`gift_level` INT NOT NULL COMMENT '推荐层级 1=直推 2=隔级', "
- + "`status` VARCHAR(16) NOT NULL DEFAULT 'GIFTED' COMMENT 'GIFTED/USED/REVOKED', "
- + "`remark` VARCHAR(255) NULL COMMENT '赠送留言', "
- + "`created_at` DATETIME DEFAULT CURRENT_TIMESTAMP, "
- + "`used_at` DATETIME NULL, "
- + "`revoked_at` DATETIME NULL, "
- + "INDEX `idx_receiver` (`receiver_family_id`, `status`), "
- + "INDEX `idx_giver` (`giver_user_id`), "
- + "INDEX `idx_family_coupon` (`family_coupon_id`) "
- + ") ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='会员优惠券转赠记录'");
- log.info("迁移428: 已创建 coupon_gift_transfer 表");
- } catch (Exception e) {
- log.warn("迁移428: 创建 coupon_gift_transfer 表失败(可能已存在): {}", e.getMessage());
- }
- // family_coupon.is_gift 列(1=来自转赠)
- try {
- Integer colExists = jdbcTemplate.queryForObject(
- "SELECT COUNT(*) FROM information_schema.COLUMNS WHERE TABLE_SCHEMA = DATABASE() AND TABLE_NAME = 'family_coupon' AND COLUMN_NAME = 'is_gift'",
- Integer.class);
- if (colExists == null || colExists == 0) {
- jdbcTemplate.execute("ALTER TABLE family_coupon ADD COLUMN is_gift INT DEFAULT 0 COMMENT '1=该券实例来自他人转赠'");
- log.info("迁移428: 已为 family_coupon 增加 is_gift 列");
- } else {
- log.info("迁移428: family_coupon.is_gift 列已存在,跳过");
- }
- } catch (Exception e) {
- log.warn("迁移428: 检查/添加 family_coupon.is_gift 列失败: {}", e.getMessage());
- }
- }
- // ==================== 迁移434:SKU 商品组合 ====================
- void migrateSkuCombo() {
- try {
- jdbcTemplate.execute("CREATE TABLE IF NOT EXISTS `product_sku_combo_items` ("
- + "`id` BIGINT AUTO_INCREMENT PRIMARY KEY, "
- + "`sku_id` BIGINT NOT NULL COMMENT '组合SKU ID(product_skus.id)', "
- + "`child_product_id` BIGINT NOT NULL COMMENT '组成商品ID', "
- + "`child_sku_id` BIGINT NULL COMMENT '组成商品SKU ID(组成商品有SKU时必填)', "
- + "`quantity` INT NOT NULL DEFAULT 1 COMMENT '该组成商品数量', "
- + "`created_at` DATETIME DEFAULT CURRENT_TIMESTAMP, "
- + "INDEX `idx_sku` (`sku_id`), "
- + "INDEX `idx_child_product` (`child_product_id`) "
- + ") ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='商品SKU组合明细'");
- log.info("迁移434: 已创建 product_sku_combo_items 表");
- } catch (Exception e) {
- log.warn("迁移434: 创建 product_sku_combo_items 表失败(可能已存在): {}", e.getMessage());
- }
- try {
- Integer colExists = jdbcTemplate.queryForObject(
- "SELECT COUNT(*) FROM information_schema.COLUMNS WHERE TABLE_SCHEMA = DATABASE() AND TABLE_NAME = 'product_orders' AND COLUMN_NAME = 'sku_id'",
- Integer.class);
- if (colExists == null || colExists == 0) {
- jdbcTemplate.execute("ALTER TABLE product_orders ADD COLUMN sku_id BIGINT NULL COMMENT '下单SKU ID(组合SKU识别用)' AFTER product_type");
- log.info("迁移434: 已为 product_orders 增加 sku_id 列");
- } else {
- log.info("迁移434: product_orders.sku_id 列已存在,跳过");
- }
- } catch (Exception e) {
- log.warn("迁移434: 检查/添加 product_orders.sku_id 列失败: {}", e.getMessage());
- }
- }
- // ==================== 迁移429:用户关注指标收藏(T04 方案A) ====================
- /**
- * 迁移429: 用户关注指标收藏表(跨端同步;5维+指标唯一)。
- * 幂等:CREATE TABLE IF NOT EXISTS + 异常兜底。
- */
- void createIndicatorFavoriteTable() {
- try {
- jdbcTemplate.execute("CREATE TABLE IF NOT EXISTS `indicator_favorite` ("
- + "`id` BIGINT AUTO_INCREMENT PRIMARY KEY, "
- + "`user_id` BIGINT NOT NULL COMMENT '收藏用户ID(users.id)', "
- + "`family_id` BIGINT NOT NULL COMMENT '所属家庭ID', "
- + "`member_id` BIGINT NOT NULL COMMENT '观测对象ID(family_members.id)', "
- + "`source_type` VARCHAR(50) NOT NULL COMMENT '报告类型', "
- + "`definition_id` BIGINT NOT NULL COMMENT '指标定义ID(indicator_definitions.id)', "
- + "`created_at` DATETIME DEFAULT CURRENT_TIMESTAMP, "
- + "`updated_at` DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, "
- + "UNIQUE KEY `uk_user_family_member_source_def` (`user_id`, `family_id`, `member_id`, `source_type`, `definition_id`), "
- + "KEY `idx_member_source` (`member_id`, `source_type`), "
- + "KEY `idx_user_created` (`user_id`, `created_at`) "
- + ") ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='用户关注指标收藏'");
- log.info("迁移429: 已创建 indicator_favorite 表");
- } catch (Exception e) {
- log.warn("迁移429: 创建 indicator_favorite 表失败(可能已存在): {}", e.getMessage());
- }
- }
- // ==================== 迁移433:家庭指标聚合方式配置(五维能量汇总/珍珠图/章鱼图汇集配置) ====================
- /**
- * 迁移433: 家庭指标聚合方式配置表 family_metric_agg_config。
- * 平台级全局一份,按数据项一行一条。
- * 预置 8 行数据:五维各维度 AVG + 综合 AVG + pearl.resources COLLECT + octopus.effects COLLECT
- */
- void createFamilyMetricAggConfigTable() {
- try {
- jdbcTemplate.execute("CREATE TABLE IF NOT EXISTS `family_metric_agg_config` ("
- + "`id` BIGINT AUTO_INCREMENT PRIMARY KEY, "
- + "`metric_key` VARCHAR(64) NOT NULL COMMENT '数据项唯一标识,如 energy.body、pearl.resources', "
- + "`metric_name` VARCHAR(64) NOT NULL COMMENT '中文名,如「身体维度能量分」', "
- + "`dimension_code` VARCHAR(32) COMMENT '所属维度:body/mind/wisdom/action/wealth/common', "
- + "`agg_type` VARCHAR(16) NOT NULL COMMENT '汇总方式:SUM=合计 / AVG=平均 / COLLECT=汇集', "
- + "`value_type` VARCHAR(16) NOT NULL COMMENT '值类型:NUMBER(数值)/ COLLECTION(集合)', "
- + "`display_mode` VARCHAR(32) COMMENT '展示形态:number/bar/radar/merge_graph', "
- + "`sort_order` INT DEFAULT 0 COMMENT '排序', "
- + "`status` VARCHAR(16) DEFAULT 'active' COMMENT '状态:active/disabled', "
- + "`remark` VARCHAR(255) COMMENT '备注', "
- + "`created_at` DATETIME DEFAULT CURRENT_TIMESTAMP, "
- + "`updated_at` DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, "
- + "UNIQUE KEY `uk_metric_key` (`metric_key`) "
- + ") ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='家庭指标聚合方式配置'");
- log.info("迁移433: 已创建 family_metric_agg_config 表");
- } catch (Exception e) {
- log.warn("迁移433: 创建 family_metric_agg_config 表失败(可能已存在): {}", e.getMessage());
- }
- // 预置数据(幂等 upsert)
- upsertFamilyMetricAggConfig("energy.body", "身体维度能量分", "body", "AVG", "NUMBER", "number", 1, "active", "五维身体维度家庭汇总,默认平均值");
- upsertFamilyMetricAggConfig("energy.mind", "心理维度能量分", "mind", "AVG", "NUMBER", "number", 2, "active", "五维心理维度家庭汇总,默认平均值");
- upsertFamilyMetricAggConfig("energy.wisdom", "认知维度能量分", "wisdom", "AVG", "NUMBER", "number", 3, "active", "五维认知维度家庭汇总,默认平均值");
- upsertFamilyMetricAggConfig("energy.action", "社交维度能量分", "action", "AVG", "NUMBER", "number", 4, "active", "五维社交维度家庭汇总,默认平均值");
- upsertFamilyMetricAggConfig("energy.wealth", "成就维度能量分", "wealth", "AVG", "NUMBER", "number", 5, "active", "五维成就维度家庭汇总,默认平均值");
- upsertFamilyMetricAggConfig("energy.overall", "综合能量分", "common", "AVG", "NUMBER", "number", 6, "active", "五维综合能量分家庭汇总,默认平均值");
- upsertFamilyMetricAggConfig("pearl.resources", "珍珠图资源(我有什么)", "action", "COLLECT", "COLLECTION", "merge_graph", 7, "active", "珍珠图家庭汇集,合并成一张图按成员着色");
- upsertFamilyMetricAggConfig("octopus.effects", "章鱼图关键人/成效", "action", "COLLECT", "COLLECTION", "merge_graph", 8, "active", "章鱼图家庭汇集,合并成一张图按成员着色");
- }
- private void upsertFamilyMetricAggConfig(String metricKey, String metricName, String dimensionCode,
- String aggType, String valueType, String displayMode,
- Integer sortOrder, String status, String remark) {
- try {
- // 先尝试更新
- int updated = jdbcTemplate.update(
- "UPDATE `family_metric_agg_config` SET metric_name=?, dimension_code=?, agg_type=?, value_type=?, display_mode=?, sort_order=?, status=?, remark=?, updated_at=CURRENT_TIMESTAMP WHERE metric_key=?",
- metricName, dimensionCode, aggType, valueType, displayMode, sortOrder, status, remark, metricKey);
- if (updated == 0) {
- // 不存在则插入
- jdbcTemplate.update(
- "INSERT INTO `family_metric_agg_config` (metric_key, metric_name, dimension_code, agg_type, value_type, display_mode, sort_order, status, remark) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)",
- metricKey, metricName, dimensionCode, aggType, valueType, displayMode, sortOrder, status, remark);
- log.info("迁移433: 已插入预置配置 {}", metricKey);
- } else {
- log.info("迁移433: 已更新预置配置 {}", metricKey);
- }
- } catch (Exception e) {
- log.warn("迁移433: 预置配置 {} 处理失败(忽略): {}", metricKey, e.getMessage());
- }
- }
- void createPpointConsumptionLogTable() {
- try {
- jdbcTemplate.execute("CREATE TABLE IF NOT EXISTS `ppoint_consumption_log` ("
- + "`id` BIGINT AUTO_INCREMENT PRIMARY KEY, "
- + "`user_id` BIGINT NOT NULL COMMENT '消费人用户ID(users.id)', "
- + "`member_id` BIGINT NULL COMMENT '消费人家庭成员ID(family_members.id)', "
- + "`family_id` BIGINT NULL COMMENT '消费家庭ID', "
- + "`order_id` BIGINT NOT NULL COMMENT '订单ID', "
- + "`order_type` VARCHAR(32) NOT NULL COMMENT '订单类型(product/package/assessment/membership/subscription)', "
- + "`product_id` BIGINT NULL COMMENT '商品ID(商品订单)', "
- + "`amount_cent` INT NULL COMMENT '实付金额(分)', "
- + "`ppoints` INT NOT NULL DEFAULT 0 COMMENT '本单产生的P点', "
- + "`remark` VARCHAR(255) NULL, "
- + "`created_at` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '消费时间(订单支付成功时刻)', "
- + "UNIQUE KEY `uk_order` (`order_type`, `order_id`), "
- + "KEY `idx_user_time` (`user_id`, `created_at`) "
- + ") ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='P点消费日志(滚动一年销售奖档位聚合数据源,F-4)'");
- log.info("迁移420: 已创建 ppoint_consumption_log 表");
- } catch (Exception e) {
- log.warn("迁移420: 创建 ppoint_consumption_log 表失败(可能已存在): {}", e.getMessage());
- }
- }
- // ==================== 迁移424:家庭归因阶段一·数据地基 ====================
- /**
- * 迁移424: health_plans 加 plan_type,区分健康方案与家庭归因干预方案(G5)。
- * 默认 'health_plan',存量行自动命中默认值;幂等(列已存在则跳过)。
- */
- void migrateHealthPlansPlanType() {
- ensureColumn("health_plans", "plan_type",
- "VARCHAR(32) DEFAULT 'health_plan' COMMENT '方案类型: health_plan=健康方案, family_attribution_plan=家庭归因方案'");
- }
- // ==================== 迁移425:家庭归因常模值表(G10) ====================
- /** 菌群常模 JSON 种子路径(classpath) */
- static final String NORM_GUT_FLORA_CLASSPATH = "norm/attribution_norm_gut_flora.json";
- /** 家庭归因确认问卷模板 JSON 种子路径(classpath) */
- static final String SURVEY_FAMILY_ATTRIBUTION_CLASSPATH = "survey/family_attribution_questions.json";
- /** 家庭归因确认问卷模板标题(幂等判重键) */
- static final String SURVEY_FAMILY_ATTRIBUTION_TITLE = "家庭归因分析问卷(第八章16条)";
- /**
- * 迁移425: 家庭归因常模值表 attribution_norm_values。
- * 菌群 710 常模落点;DAN 常模优先复用 cognitive_norms / health_norm_reference,不重复建。
- */
- void createAttributionNormValuesTable() {
- try {
- jdbcTemplate.execute("CREATE TABLE IF NOT EXISTS `attribution_norm_values` ("
- + "`id` BIGINT AUTO_INCREMENT PRIMARY KEY, "
- + "`norm_type` VARCHAR(20) NOT NULL COMMENT '常模类型: gut_flora(菌群)/dan(DAN测评)', "
- + "`definition_id` BIGINT NULL COMMENT 'FK→indicator_definitions.id(归一化后关联,可空)', "
- + "`code` VARCHAR(64) NOT NULL COMMENT '指标编码(菌种学名如Bifidobacterium 或 DAN维度key如emotionScore)', "
- + "`display_name` VARCHAR(100) COMMENT '显示名', "
- + "`member_age_band` VARCHAR(32) NOT NULL COMMENT '年龄段带: 0-3/4-6/7-12/13-18/19-45/46-65/66+/all', "
- + "`gender` VARCHAR(10) DEFAULT 'all' COMMENT '性别: male/female/all', "
- + "`p5` DECIMAL(10,4) NULL COMMENT 'P5百分位值', "
- + "`p25` DECIMAL(10,4) NULL COMMENT 'P25百分位值', "
- + "`p50` DECIMAL(10,4) NULL COMMENT 'P50百分位值(中位数)', "
- + "`p75` DECIMAL(10,4) NULL COMMENT 'P75百分位值', "
- + "`p95` DECIMAL(10,4) NULL COMMENT 'P95百分位值', "
- + "`unit` VARCHAR(20) COMMENT '单位(如 %)', "
- + "`norm_version` VARCHAR(20) NOT NULL DEFAULT 'v1' COMMENT '常模版本', "
- + "`source` VARCHAR(200) COMMENT '数据来源(谷禾710常模/DAN常模/导入文件名)', "
- + "`status` TINYINT DEFAULT 1 COMMENT '0=禁用 1=启用', "
- + "`created_at` DATETIME DEFAULT CURRENT_TIMESTAMP, "
- + "`updated_at` DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, "
- + "UNIQUE KEY `uk_type_code_band_gender_ver` (`norm_type`, `code`, `member_age_band`, `gender`, `norm_version`), "
- + "INDEX `idx_type_code` (`norm_type`, `code`), "
- + "INDEX `idx_def` (`definition_id`) "
- + ") ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='家庭归因常模值'");
- log.info("迁移425: 已创建 attribution_norm_values 表");
- } catch (Exception e) {
- log.warn("迁移425: 创建 attribution_norm_values 表失败(可能已存在): {}", e.getMessage());
- }
- }
- /**
- * 迁移425-导入: 菌群常模可配置 JSON 导入(norm_type=gut_flora)。
- * 空壳模板(placeholder=true / data 为空)→ 0 行导入且不报错;
- * 真实数据到位后填充 classpath:norm/attribution_norm_gut_flora.json 即可增量导入。
- */
- void importAttributionNormGutFlora() {
- try {
- ClassPathResource resource = new ClassPathResource(NORM_GUT_FLORA_CLASSPATH);
- if (!resource.exists()) {
- log.warn("迁移425: 未找到常模种子文件 {},跳过导入", NORM_GUT_FLORA_CLASSPATH);
- return;
- }
- String content = StreamUtils.copyToString(resource.getInputStream(), StandardCharsets.UTF_8);
- if (content.trim().isEmpty()) {
- log.info("迁移425: 常模种子文件为空,0 行导入");
- return;
- }
- ObjectMapper mapper = new ObjectMapper();
- JsonNode root = mapper.readTree(content);
- if (root == null || !root.isObject()) {
- log.warn("迁移425: 常模种子文件格式非法(期望 JSON 对象),0 行导入");
- return;
- }
- if (root.path("placeholder").asBoolean(false)) {
- log.info("迁移425: 常模种子为占位模板(placeholder=true),0 行导入,等待业务侧填充真实常模");
- return;
- }
- JsonNode data = root.get("data");
- if (data == null || !data.isArray() || data.size() == 0) {
- log.info("迁移425: 常模种子 data 为空,0 行导入");
- return;
- }
- int inserted = 0;
- for (JsonNode row : data) {
- String code = text(row, "code");
- if (code == null || code.trim().isEmpty()) {
- continue;
- }
- String ageBand = defaultIfBlank(text(row, "memberAgeBand"), "all");
- String gender = defaultIfBlank(text(row, "gender"), "all");
- String normVersion = defaultIfBlank(text(row, "normVersion"), "v1");
- jdbcTemplate.update(
- "INSERT INTO attribution_norm_values "
- + "(norm_type, definition_id, code, display_name, member_age_band, gender, "
- + "p5, p25, p50, p75, p95, unit, norm_version, source, status, created_at, updated_at) "
- + "VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, 1, NOW(), NOW()) "
- + "ON DUPLICATE KEY UPDATE definition_id=VALUES(definition_id), display_name=VALUES(display_name), "
- + "p5=VALUES(p5), p25=VALUES(p25), p50=VALUES(p50), p75=VALUES(p75), p95=VALUES(p95), "
- + "unit=VALUES(unit), source=VALUES(source), status=VALUES(status), updated_at=NOW()",
- "gut_flora",
- longOrNull(row.get("definitionId")),
- code.trim(),
- text(row, "displayName"),
- ageBand,
- gender,
- decimalOrNull(row.get("p5")),
- decimalOrNull(row.get("p25")),
- decimalOrNull(row.get("p50")),
- decimalOrNull(row.get("p75")),
- decimalOrNull(row.get("p95")),
- text(row, "unit"),
- normVersion,
- text(row, "source"));
- inserted++;
- }
- log.info("迁移425: 菌群常模导入完成,共 {} 行(norm_type=gut_flora,幂等 upsert)", inserted);
- } catch (Exception e) {
- log.warn("迁移425: 菌群常模导入失败(忽略): {}", e.getMessage());
- }
- }
- /**
- * 迁移425-种子: 家庭归因确认问卷模板种子(A2:第八章 16 条 / 45 采集项)。
- * 幂等:按 title 判重;已存在则只确保 enabled=1(不覆盖管理员改过的 questions_json);
- * questionsJson 存 §3.2 扩展结构(schemaVersion/target/questions)。
- */
- void seedFamilyAttributionSurveyTemplate() {
- try {
- ClassPathResource resource = new ClassPathResource(SURVEY_FAMILY_ATTRIBUTION_CLASSPATH);
- if (!resource.exists()) {
- log.warn("迁移425-种子: 未找到问卷模板种子 {},跳过", SURVEY_FAMILY_ATTRIBUTION_CLASSPATH);
- return;
- }
- Integer cnt = jdbcTemplate.queryForObject(
- "SELECT COUNT(*) FROM survey_templates WHERE title = ?",
- Integer.class, SURVEY_FAMILY_ATTRIBUTION_TITLE);
- if (cnt != null && cnt > 0) {
- // 已存在:幂等启用(不覆盖管理员可能改过的 questions_json)
- jdbcTemplate.update(
- "UPDATE survey_templates SET enabled = 1, updated_at = NOW() WHERE title = ?",
- SURVEY_FAMILY_ATTRIBUTION_TITLE);
- log.info("迁移425-种子: 家庭归因问卷模板已存在(title={}),幂等启用(enabled=1)", SURVEY_FAMILY_ATTRIBUTION_TITLE);
- return;
- }
- String content = StreamUtils.copyToString(resource.getInputStream(), StandardCharsets.UTF_8);
- // 校验 JSON 合法后再落库,避免脏种子进库
- ObjectMapper mapper = new ObjectMapper();
- JsonNode root = mapper.readTree(content);
- if (root == null || !root.isObject() || root.get("questions") == null) {
- log.warn("迁移425-种子: 问卷模板种子格式非法,跳过");
- return;
- }
- jdbcTemplate.update(
- "INSERT INTO survey_templates (title, dimension, questions_json, cycle_type, ai_generated, enabled, created_at, updated_at) "
- + "VALUES (?, ?, ?, ?, 0, 1, NOW(), NOW())",
- SURVEY_FAMILY_ATTRIBUTION_TITLE,
- "family_attribution",
- content,
- "once");
- log.info("迁移425-种子: 已写入家庭归因问卷模板种子 title={} questions={}(enabled=1,已启用)",
- SURVEY_FAMILY_ATTRIBUTION_TITLE, root.get("questions").size());
- } catch (Exception e) {
- log.warn("迁移425-种子: 家庭归因问卷模板种子失败(忽略): {}", e.getMessage());
- }
- }
- // ==================== 迁移426:家庭归因报告表(B1 核心闭环③) ====================
- /**
- * 迁移426: 家庭归因报告表 attribution_reports。
- * 存十节结构 report_json + 归因中间态 intermediate_state(供 B2 条件路由 / B3 回填)
- * + 红线命中 redline_hits + 状态机 status(ANALYZING/READY/ADJUSTING/ADJUSTED/CONFIRMED/GENERATED/INVALIDATED)。
- * 幂等:CREATE TABLE IF NOT EXISTS。
- */
- void createAttributionReportsTable() {
- try {
- jdbcTemplate.execute("CREATE TABLE IF NOT EXISTS `attribution_reports` ("
- + "`id` BIGINT AUTO_INCREMENT PRIMARY KEY, "
- + "`family_id` BIGINT NOT NULL COMMENT '家庭ID', "
- + "`member_ids` VARCHAR(200) NULL COMMENT '目标成员ID列表(逗号分隔;E6: 妈妈/女儿/儿子)', "
- + "`plan_id` BIGINT NULL COMMENT '关联健康方案ID(health_plans.id)', "
- + "`status` VARCHAR(20) NOT NULL DEFAULT 'ANALYZING' "
- + "COMMENT '状态机: ANALYZING/READY/ADJUSTING/ADJUSTED/CONFIRMED/GENERATED/INVALIDATED', "
- + "`report_json` MEDIUMTEXT NULL COMMENT '十节结构化归因报告(JSON)', "
- + "`redline_hits` VARCHAR(255) NULL COMMENT '红线命中编号(JSON数组, 如 [\"RL02\"])', "
- + "`intermediate_state` MEDIUMTEXT NULL COMMENT '归因中间态(JSON, 供B2条件路由/B3回填)', "
- + "`created_at` DATETIME DEFAULT CURRENT_TIMESTAMP, "
- + "`updated_at` DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, "
- + "INDEX `idx_family` (`family_id`), "
- + "INDEX `idx_status` (`status`), "
- + "INDEX `idx_plan` (`plan_id`) "
- + ") ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='家庭归因分析报告(阶段二核心闭环)'");
- log.info("迁移426: 已创建 attribution_reports 表");
- } catch (Exception e) {
- log.warn("迁移426: 创建 attribution_reports 表失败(可能已存在): {}", e.getMessage());
- }
- // 迁移435: product_bundle_items 增加套餐SKU锚点列(订单套餐自动定价)
- // bundle_sku_id 非空 = 定价套餐行(套餐价取该SKU.price,CartService/ProductOrderService 匹配用);
- // 为空 = 仅出库联动行(InventoryService 语义,不参与定价)。
- ensureColumn("product_bundle_items", "bundle_sku_id",
- "BIGINT NULL COMMENT '套餐SKU锚点:非空=订单套餐自动定价(套餐价取该SKU.price);空=仅出库联动'");
- // 同时放开 bundle_product_id 可空:定价套餐行置 NULL,避免 InventoryService
- // (按 bundle_product_id 关联、仅 productType=bundle 商品)误触发出库联动。
- try {
- jdbcTemplate.execute("ALTER TABLE product_bundle_items MODIFY COLUMN bundle_product_id BIGINT NULL COMMENT '套餐商品ID(出库联动行=套餐商品ID;定价套餐行置NULL)'");
- log.info("迁移435: product_bundle_items.bundle_product_id 已放开可空");
- } catch (Exception e) {
- log.warn("迁移427: 放开 bundle_product_id 可空失败(可能已放开): {}", e.getMessage());
- }
- }
- private static String text(JsonNode row, String field) {
- if (row == null) return null;
- JsonNode n = row.get(field);
- if (n == null || n.isNull()) return null;
- String s = n.asText();
- return s == null || s.trim().isEmpty() ? null : s;
- }
- private static String defaultIfBlank(String v, String def) {
- return (v == null || v.trim().isEmpty()) ? def : v;
- }
- private static Long longOrNull(JsonNode n) {
- if (n == null || n.isNull()) return null;
- return n.isNumber() ? n.longValue() : null;
- }
- private static java.math.BigDecimal decimalOrNull(JsonNode n) {
- if (n == null || n.isNull() || n.asText().trim().isEmpty()) return null;
- try {
- return new java.math.BigDecimal(n.asText().trim());
- } catch (NumberFormatException e) {
- return null;
- }
- }
- }
|