From 3c628eb3914419c8e139662ef6347cf5593adcb3 Mon Sep 17 00:00:00 2001 From: tradewind Date: Tue, 19 May 2026 14:57:11 +0800 Subject: [PATCH] #feature: update SQL generator --- .../php/document_symbols_cache_v23-06-25.pkl | Bin 169139 -> 257056 bytes .serena/project.yml | 162 +++++++++--- app/Http/Controllers/JiraController.php | 62 +++-- .../Controllers/SqlGeneratorController.php | 208 +++++++++++++++- app/Services/JiraService.php | 186 ++++++++------ resources/js/components/jira/WeeklyReport.vue | 33 ++- .../js/components/tools/SqlGenerator.vue | 231 +++++++++++++++++- routes/api.php | 1 + tests/Feature/SqlGeneratorTest.php | 133 ++++++++++ tests/Unit/JiraServiceTest.php | 192 +++++++++++++-- 10 files changed, 1043 insertions(+), 165 deletions(-) create mode 100644 tests/Feature/SqlGeneratorTest.php diff --git a/.serena/cache/php/document_symbols_cache_v23-06-25.pkl b/.serena/cache/php/document_symbols_cache_v23-06-25.pkl index 420c1a251d2201ff9dece89f4726bd8186d6443b..888989e70d4ec7eabcfa8c83f26d5435e20cbdb9 100644 GIT binary patch literal 257056 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

jCv) z_N4gF#&;$x8}Cr~x5Ppz0trXUrCzKZC=X$gX#SH7F@7A}G`88h=t`Eu#2}KyL)z;> z%>cVu1K?!);TKS$c?>-79-tNGvxl~5g08+Q*iyYM2=2rQu+kz9T5_QuCU|z1H5|$$ zOG!eMXvJ-csp}$wdS}p;tre;pksPj|QuiRHij+G0mUf<&7hwk@-?_NlUE2g}GO^lf z$x!d<^?iFlEjo1XTi{fG@DGm4OFOAsp=fjPiGeqE*I$3Rey|U24FRwQA9$?+j@oyj zR}<>ycIf$g=xq$*t+M-r_9KM=&gxNz6#;I;sk35cS*aLkGaKCh@bbWs2L_M6V|YOe zBfXH6+05$zRJl;YgXHkFJQSeR8MI`k%}h-Lum9;tk$_)`{tmSUZ|p;dz64vG$M4@W zcow)=GDut zU!7X=u@oOHi`OibfWt3}4yE!!9Y|418LX&mM5laduay*$X)lERbQ%ztnSxEjE8u^% zHxLlmw5^qtzM>8b)WVWCP02`xbezYC8e(q9LzDZYzW&;6rqxfa_)&kG(c@ z5O$Lu5e=0t!hZj?V!IAPTm^>R07p@|dUF?61*njC)}oG5@79*-WF3z2f@YCW8v*8nH`T&<_J@MJbW6#K)KnM>%-~L(lJ#2LR8Q0o4x2T@tAqUE< zMSsG?#DbiU!mNF!rx;-vrYRIC zFmI_JVi13!GL3F4^Ln68jn1pozdl@PL6$_iutakK7?gIt_~MIGcT4HFkuJn(<$uHP zlkABu5C5`#2h*lIazV$P;EFQr?)eIio26VtY9WP%cBFG0IJUnom07=KLk2GgT%Xzi zOCPxq^51GjW7Q6# z^s5-UuuAH4ayE1*)OYQ#A3F+6_4s42Qi~%YFj;Ggx$xf0K289Q6bE|-7y=1*Q-*FN zmndvf)}?snq)*X6T=Vk0(hQQN$*s*ON7Ufrb4m}!SZsW)45SZ;uZ7(}M1?&;L~$96 zbq4-D)4!phqQ5o2F~_Q1*f{jmgpyc--^DJKc==xpYi0iNEv%J$&qrA;fwd|vf;@$? zcw>#iUKioN{ELaqMGSSD7a_rV1|w~9sM>UFIfE%<1bRA7#P$l_-g7Jh0QLg>rMt_e zT5r&~1PTH35?2G!Lc#7QzD48v(0CP!Ybz~g$n|7Erd?16)(N-}Dt2ZIs2?^jeYVL; z141YyMYcso?&iZ)AzMf$thq2lfTzCqtBq&i zn8{(diD}^2mj|D@=Xn3)Lx=Eu(UMxX(5lzpctSQ)37 zpZ>$XiPw0o%(e%pcF$P4lAMmTI6(>?+%yX{E_xhZ2%V9O3{pa|JI8&?e-#) zAgk3rvJKQ&dTLWxEwp6oX}%%Plm*7A1w-}MyKnB73Z-P+Y}uj$oc{S_3YEDMCO-zcHkjtyFDF_pg42^lt zrhj#WKy?J&iDxMIs~DZh)&Ctok_Y#HL4JhY>~}oLG}uU-I^rl)isJP>=Ss$@!pz0g z!W-A2YQiuit*`Z5E(VTgu&>bfV?xfNZUqq$joOatQ)!rlZS@M{$Jo`dTEJN;P^6Do zB`$XaBN|+u;fus9xTE2QQKOTID*p%#nqv4Idd+^qSLaBQ(ShttT_0RROFn(5aQZxhrnapeo zp=1D5!X84(DYeKFe+`Ql77(@GPWW8MqzrA@?2bR)ny=W?1F z649N+`qv+89Q;O7x~t~Q5$Ql;lT`@`N%5N^DISO0`*3nO=gyxqH%xT!spF5|Pgg?8 zN#`w)z6V8R>#LWJ)t`DWp{R4_&b>l>6_Um-zEof@pFbx|Dw{|tpFB}h8)0h@E=j?- zpq%XpS)*oWBN-*YpAvWE16dxfyw8y36%H0OpGa2sVlqA%@Kn)tTsVgyeHe0S@FPzq zO&k1RaqtHtf*+;c4l5Zt2^_!oExOY-cS zWW!!eG7zS7%yKPH!A!M{woz1 zFK5?W6|k(qz6r&Spwm=Do*QP~tjeGB}?}+kbo>iLWKzUbV9X;%49`dYSH^qRqV}MAR}jFlWfSAV z293=N5Z7$~< z8j(ubA%9^^lxDEJ*i~YTC2qiC5yYoK8W`Mvq<;S^&3wJR*1d*qVsP6*_I=2Oya>N8 z*HizSt0yDXfNW5&*L*I^!jtl*%%s<0p0S-!;9?bZB9$ozpRDD}0b1!};Unc(t-c>_ zL4*YSbFkUxQYQYLAUw+DOmhJ{QnScC)=w^h z{&yNgyIQtzLw<`Puf^fJFt^xd6O%ihA*$rGGvfNkF`UUumlu+`B~Kj8XAJ8TJ$;5R z#f%(unB zi_jKD%lwTnql)V*NhQKF!1R^WGW`1Drtu)lWfK_WOZfIga_38H(+BwB515$DPOEfC zGIVO&h{h(gOVJ1v0Gg;TooHx6`gaqFV17XSyV;R&K3z1KG6WYAA}eh&oxGbxpo8QR z=WikxiG|ahLXwRX+hl1to29VvP!k0Ze3hmY!6uSYM!FGnFWjYt#9Et3j~X@l)o`T| z)q*vV5jvWP$><{s^SaJW@3LeR=SsCQy?qw;^oyM>lAw=YSknvh?3w`Fnsio>5zd*O zYE8}m5USJWX(~Nq_L#@U3TCnqOxZCPrOe=r&GeBbt4}kwW!N;|hQn|#6RULg4%fxDg*+$G^xG*I zmJ8AP2~H<8Bl$25JVQ|}1$!4)3Qlr@rGPmGwGzqzF(~hO<>LDXKWc13KGf#PZ-r*> zc5y7%ri8&{xLkkz6*%Dp%D3Z>{r~NK3v^t?nWh7NzaYkeBolBOoU~-rEx&C{FxVIe zuz_InN@Sz7q${bLmRjkDA7C@)VK5-T1VL~N0fTKA0=5YWb}%-X?4F#Q%*@HmnM`(e zPBteqkz{*z!tBoM+2OF+GxLA-SKUY5x_w)+u3CH6I`GS?d#i5M_y6_RU$6f^c>2{B zjN3!*FUWtBek!IrOO}7p5ij^F=S{Jz>2Y{#s;N`MZ@<$2?sL4j;9kk23cIgZyQ*rg zqEivq6j)zMrPU>Iy#r%)O5f2H1jpIKy672O%e-pavjdU_QsW0 zt88rMiLZ;bF0};aPB%%RZ0)%& znzJV9GX6&LE12ZkCAq9?HgDeCfMM&Zz9%2(e{pB;(YJZ-UzbcKHyCG~s6RP%Zfm6N zM2aL8uNT>C8t$%JzhT4Wwe_^s;%XHAA>On6KAm0t$lhp>?>5X0SX*+smdeeim?Ws| z|HcCm$;Y2Vx4 zS_jrqNmXz)mB_ljV1%#|32~KCfrzNKl@kvyD-`|*9;E#pT8%&X{-OToX$*RcI?c&F zbXRXbyQlVvxcZcEkJ;a5F)*z* zyUG$;$+6uu^{Bt7)K{6;bZw?u9-O6N4xI)^cZY4~T|{?@l^YAWOhRhKKZe0KyI_xe zi5yP!?fTyDw(rtZ(eJiDbLzw0{jVIs7K%7!>-I$vjGZjjC!+2Hz*<{n7B9omp6(x! zD-Eg|sY}+6R4Llp;S~H^=wX|okPu%lpqEKY(#soWa~ZYtZ=o*bXG1!I;!jW~a5{jC z88VZ@Ecwi5U92!||A!bDlxH0lJk&a68S}ztSkTb}?j2Of=@;H}A65uvX*nYRXBkXZ zg<7)LSRGnakUb%{^ZX|dxY~_s3=&I0veSyLEfNyTgl~SIP(os`AJ%5w0pI;FfZXqU zCDJqI*;*NyD=ybmpyA-Rw@Qu_p}P2SNgDnH1nw*}_9{i>w9QyY>SXe)R`>HP4&>v9 z!Z>)kl==iU8BSn}!lwYRJceTmlS z>5vz2@U@XY!!-|WqxiL+Xv7?xUV_&k3vY(DV!S&44bU9uIwQFiPh@ zOa=e(yKqwh+X#cO6mas-wiNg(mE9``WhscGga%6iJ1_%Vc;J%G)KI{8-F5>0E@vhv z736x)G@46uM41QnSJZ$k5=?ULE?+Cy2EGENRxzuDnA43=F;Q2`F-9fj8xqu63S8xE zh_h8qhxbn%`a%B-Ptig|y#MZ@iJMxfcJIS5sPlfbqr@qeD$IVgo94j3C0ife0jV0wiM)bxthpIwEEuA{oOJKZEb ziubPfy!<~izH%RQ_x%v21{@F?z*G{4w3(~^j}Mlm;^4P|rBIp!<0Y5mg;=sup6{$& zGxJTz+hiS(k5**7MWe6E;?+b{T%SZH_aI{@mK$kMn35Mgga5OR;2@u8j;g9+Ho z_VDm`xeBO{yWaDD*Yg$6P*+a&QJhweX=JNdnc1h*xRely*H4|57j<=xf-iKioNM1B zw1f4Q%I_NxJjdd+;Z|kRR1hXmVo#;gH4s&f@8ggV#a#80v$SYe%|7mGSb2!ibs9@k zimIrJzBiN-V4qE_NCJGv1Ck>U=Kxlef0FRCuoDGd{^{1{os*< z*2>uJ2Zhq$6j8)|ToAE@^xw(XkDcDVj{_6)$9PL3HZ$Ep>#qsgAV_v~w#|&uebLy= z_F}QCu(ZBjrP?K_FA+<`UTWT=14I<`EnYv9_6TNfxO{!GhKkvm>B9e3+t#*p>(-`@ zuG-p}^=V9Gu*<4y>wsxjuy5aYdXMiWKx>$DZux9dRP`#q-)?Puh$9VVQ!bKQb^ zo$hJWW|=tgN*rs-<+7p(55L#}JUPIXp3g4@CSDit?C2bC&;j$;pCGJGeYl6PB8hTu zG-2=)tB0H&YltttK^s#P&Bcx`N^6LHJ9nQtxQ`met+rjyJIJRyX_H>KlwT+6Zt^H^ z*G4;*BdCza={x#kIVK!^TYtc&@u|ZPoqVN-8fJb>#qt}$j}JZEcW`guV{(B? z7s2(%eJ6I%?jNnK>knk54xTu5aId#gWYyY$8G(0kR72m9D&_UxE=kJn%yg$(W}szU zIxkIq$l=G^NX&r{k5r6N_wE49pgcHNZQeaxLgX_YU= zLQIJki<(SdN0c`TQn8;7B#`@tz7M}mTUa*ws4{gs4vXP_iEm4h4#lZD&+9Pp=*k?O zfTzPx>V~JRvak?3n((YEM>}?6wjrNU-}Bb1HU^(mttqnZy%A4QDcaC-f5Zcf{uR%| zvY*jK178X{+UIFBVRZU|5BqjJdHT^O$)T5=9POf^W4j&X%aZ{p>C!CY(+N zs5Ypn`tg@5A_}Q;ZGGmc5R|JJq*NRTpWb~0|5|CaQnWHMSN)Qw)rkiRs+I^4qWq~j z2WVtv#M9vFh{yOa6x$a3gUhX?24=V31i(6WtX<}-rKW_qhJp{f{%x1&|GbbA#uh%? zY+XSJ${Qx+S=}*nlmHMf32_{?J+|-k_G70`JVQQC`aaxodhaoN#lU}e4mXt5%{mAH zhY#<0qkrcSkE95U)dZWG>r_i6vi9;%)kRKw-sZ__rjHo zuvi*BmHO(yrxiR*b_uc`Mb?7T?#2MxYs97HY2o8BfI;lp%nLALABlP3~uLzB>MeF zC{)@jwy#+2IJ?R%^sfz?$Hofk5sLe^k3za=N;=K0COzde2x-u4O!a+VVK%4B6==WY z?vyC?tIjF#4Tb~2~GN)6?fQiJmTV*kv$R$Gt3zrc%ngLoO!?!1@M55fyjwA zMmAIrF>jrir;x!3=6OTUaP+qRcOKNu62RyRg5tqURmYE!PnD=ClC4cdRd7eEB9-FG z&RWIJK5*4y2h+t|@wOa;a!Ve6tw|SXBgMr``@6(X`;&;SD+M}`pEf&jYEQjQa|%&N zB@FC6{)iU80!-^U(YIqi8(MJ9{LV3-a&!XpR?<8bRieTcN%PFyCY(8NTI%Vb0rF}~ zsJo?TrZ6vT#Uds8rTu-6yh}?^K@I0G>T^&}JpvVl^e!^^sHUQu@V8*Fqnq$wsz~^m z@uWDH$?b`OvuVR_XH3qCNwB^u0#LKIa8DPU&ttwIo>yd`qwYdWdx|#k3~$nCU~`5{ zBk`HaL=(A(SUgcAD^Y_+#6x#&X_e7eVY#qq?n@|OKo!Q+gsyc4_9hf)+SxT!9)s?V z_j->#FKg%uSOH!A(su>$^>!CB&*O%?1y1c_HehYOmBH|@*u0)D$R7eG~>8RwUE2sJ4v<>=@v`)FTPAJU;@lHuhIfW zJbsb`vKz2|DD|6CZdh~in**{Cxk_bHj^58Wx>4F;bg4LCXmPF61%KD(FQen=5=pVKuQ2k`+J_RtD7;00~=8ShW874-Sgsi5Od}qogiSV@Q?5|8?8=zLtEW?TF znN>-gt1hw^~;9EE#a&HBA^CC7+uj$x8 z?}m?x1<#KUw1FUijm!w#q~_x3^&*X5|KvX>jXt8v2~Rs-=TGM!{#rl?ep;ykeOUr zI$JCe zNm=ha1uK;&#~L0Ml<%~>q2v{DJG)!Bs2Q?*J$3>In08Vm_-Febdy@MN)Yjpb=mIP^ z)$U<^`(DSv7T259TBqCj`<&K9dploSGZl-=+6F`8{TrvA(aQeWj^^)x|ZC5xrs|zz^4MOz<3)#`^C%RuAx?yxXmxj z&`4($i6c+8`W=aigsuQNK^4MIj zI1S0*W|GH{LCrXzxiqkR?j6VmFo?xngOqQ~b3HQ;VDU2^yoPMx(g!U$U~QYYt>?Lq zkBkyO?X5SLGJiaA)Tm51$Zn7WAn$^F0P+_gzXJIk$o~NOzaSGQ5xEGY24pVC3Xs(x zcY&lq@*sZ<(hag343fvg2-1nC0V0`dsRvmmd4ybba`$e)4y66EheJ_7kaAmcwx zAoncXh2t`RkqX3O(JU?@4#I^sGL&drUsn z*=F@1#~50z$`{2yM`zQWR?q5lx+gTZ(}GSj&47DWk1BPaOV0N2LgspE&gyeAH{jQd zQmK1tffiAki|JhFT2)?V6+P77omk4;Xh^=KRBCUaciT5~-$WB`i|%jKx92*ndJ4(Y zVr`)!FKc@;*HY^0F`1PT6%S|=7~oMFXpeC6r%I*FnljnOYT0CI(}`WY_@-Evc zrG@J@c~*wxPOe1BnvFBpY!X-Dv)U(Q@~ohmg=TAS&D>5+icT0(D19h}A+wH36nu7= zJgE_Wkm%@jskta|oM6Ur3gb9h!|{Ah9utJ4%r;%iCX=Z{W;Rf*Hi)h&#LmRpDP^k^ zGnc(2q)K&x*@(6GM9SKY5wbQFxnOogQ+r1!r?*+1R-V>p?zF6XvRl?#UE~Z`O6OR! zC`>m?=ll@qyojIc(kYW`Z0W=}#hPdQEcm{+3aUN}h*o#8-n9EqZJU`w_xtQSGBOes)Atdq)LL zk^u*C5wQHwvXCV}H+;Uv$$^H?5`L}^Ei&0LdqQCu_}C|0&4 zvDu8FOb%R<2+bBbhAV~x!wKZbGi!-GG2U_suy?kYQIyGnk{FsTaujbK4irP}o%)Op$)Us@;gEk${}pEI)Ct{4LPCPNA2%-<|wDPNo9_5rCR`fId~~oYgt1Wkz9OD1T>by&srTnjC7 ziJm&HYUtcyM(54pLg#=P9g28PagD1kJ8RA8+&*0B95SK9C+MmzIIGOi$Yk0Jh)x%Y zZgtcI&E!p3R$gsJM@WZGYhSTXmR;F;W<>)LWtDt`}F)~i0OKh6-*?uSP)zr@M3`w=ljADJKu z*mMxmR6XnYWiwQNGF(von-MDWkLSB@_B;$}aw6-R!&XnA?lhy+ zGF&K4FrnlzimJD4@+^aJo~D;%a`dWunhBfnMQd|$dEQbp`Rc**9K$nJgGVM`8M)g2 zBNIGhg=ZNxk*^*&`xqSBs`QL$2RM0rGhCV1QnWYv?L zt>$helV?W`%_0*tBSa%V#MtN>StduXk*_vkGoB`5v~fe%E~~xH|4p;#+{?)mBNv_P zP4J9v%C&5%`V@YX!oaI=ws7*~$f3E_1kJd1i|(!F)o4|{Iefv4&PGn26gfIyH=#3@ z7v-xiI(aiVGWoNSgVSsRXXFg!n^hO`_AHr9S0W;wy=P7EOpW6nJME<>;6OWGbr~v} zL6XVI$U!QZLAon(?YgzA61Uy5`nsE|4^qhtl1#QD2kCwjNF&22T+h(xb1j)1y;1n6 z37QdN6s8y(-6)jF(Hn)kOxTPZqp-m&Ix?9q>PM`fcN^g;lK)RcY?7LJeARZ`$+r!y zY0}9e@9=dECr4`fh(jEsdieOut0tJnwWK#DyAq44j?$+X4*k@|)tnsZa)v;OR=t$G zi0$ULOlXY|JIMoizcm&sp_QhBl5IU*&H{ILni>0K_=7R{NH>>yC94&4|t zbTWDFutWEA6Len&zD;e3P3`HTmCVyQqZX?rK>}H5Pto3FqR^3SqKK$gUj9l5$(%9TRGHC~!Yz!uqocJd; zB^Fg*%BL`_`eCcjaPrj1r94a+g&6;f3C?-hRFjoWHqo7ddswTGf)=(zht-lUb-2bs zsZ**y4o+oQ$)%uYCr&2c8adi#5fm8z!GtmSRHUec$xVq?igJ#$G*sU1XN|OE{UX;6}G1BSu@J3=4(yc@vXH7rIjFkYM#d`vRk*AE=PYk)AdY zpjAm!1zTo~T((?nW{XUY{ti?%*^)G|g%_cUBVZBgGG>jwcCtiqENV3~6h{3L6U4La zF*s8y6w|F+5=~Z-u9|epF}a!c{b3I11pUd& zv0}Da=%_l1ix@6_n3KtCBUb@oLMAlA{U&G^V`QVfFVesCKgnsH_Hsx}y*oHa#0Jg? z65J~pa@|tCij)7}SpME=;>c9FGYQN`g0B5CBdTi%nH>Fg?%*X_M9=hI!v z$xHPXNLDbm`azbrIC(2XZCIN|2jC?f_{7X$Scx$h{yBfpmjB z2l8EzH$c7*@&U+CLH-it??8G%J_7j|Q&rmv_B1&EtCM3$E}@hp1|+FUcInFY*#(4BzxNJ%mpq z@$NK!m;AcD<4$&pP>#YDROn&K0|=rty+!4c4V(yb0i|VZ>YnNax5pE6=?yBi+r};P zCjGg3a^`!o4W1^>DmF@XoO}vV>JGu8faY4&0eIT zVTGU(n(d6y_^29av~#odp0YgzjnHgojK(KJf`+hqUZ#LrFrdiLvo~{9nYf=($H9y(2kir z1@Y{j0bev?!69afw0UKZ+w5Yk?i+IjHmwP4`otLh+$SjhHLKX3E+m%0^|37=pWu2q-4%=89D*>J!jkWC>&>d-|eG zMqmhIf055AFhoE$5~y1v?Jw)26sQ?%InHNH1^ldC8oRMTQKOZo1v~DTz}g)@q8!+t zXD-t2ng=zR46pr0utT#&4t6?9u)ksl1dUOLlMz-E%fvC%nz(YL7q5+7y);JNoGF-1 zO&q;6DcxH);+1%q< zpXZJmRX=++kF~R7CwpG*-?^*z=;M9Qyg~oFl!G_%6EBpS zDLO+T9=mE;tTmM_Sas(2XmVFbwOQi5_^Crj`k&s_dt%SYo;Uh;9_iclWZai)LtSja zoH?~@)9}Y)v2EVgoS72vmQ{JXJ#D7g^oAzK;YnV!H2^k_jV#`fjR?R^2?4*V{t9?m z$ze-lswgsCo60xkI+rf(%xwZATXXr2RIw)hwM0jvC3gMN^wL7S){nd=WrK?a+hP<6 zqq#j+v;Mlx%~ltJV#I_F@Z=aIk^#~a_ec(ROzx6>``$VAgZ)cm@mP{-0i|EOiKVQ@ z+b9wxy2;^ghp4aHQ1%Ag6+=OQeY}u|0)0yPHBJtkQZC`=x_vB@ohjvj?PFPjKA)7y z2)Jt;n&k5uZA5djkv{(xG;U;p(_V^DpLa)OA1`Mv>h`fr4z!O$vqcW}n^A)8SXSkS zD%}9~#PKa>eyuRWE|U?**C^wm*&>JiNR+T&?}R}n+?A>)_nrl-#y3k55tAEfShC`^ zG!lqEZ)shb+EU}QhFx)i%Q59zGgD+Tt&N(Q5}GY?ru^q9nX*C!RSstf-HdY>LVpCX z!yRF`96zoz^Ft;F>ZZ_ak@MrPqU6U-c6?^&CgW7GcxNtu&zf9Y!O>%NF{f|Qzlk|3 zspt55O6a_C9rHw=H_GI|d1Gj{$a(UcD0wox^TwVCn{wAJV?6W5Q%@b~d;0mlBm4Wm z{objAhx?8{$CmNzpn0Psdjp#;I{JNMDw~E{*CuGrvJ_@!aV%P;>5${rLJ`-S6YX|HKfW&7tD~6%_eN4mdac!M*uy*O}t4I}D?ACaDq3Unjc%2)kb$DwPk%17b#2q`fCsR&L(`GS!I7CddW-*n?XRx&@ zOVGDgWpd!ws(h|*tvcDrI{gHK9of>+n9oOe^yJe%OHAW@rJJca=9|7)B$ERtWTDw2 zhj&_(@K$v*b+efzGC6QFH8fk~ESV7{ONMqcb;2xMUEt7i<<={Fcne%NX}FtMp|InH;!`5t=P>R$UP#tA=+O;}21qil@BW-@QklICcCu@3Qqj|7h>A z_sW`yCQGQsH;QUDNj$V$f*^!g4Nmea6x?a#R~P3IU2X>3)Kx{Rla#Z+^92l>CbGNy*9t1cBg$m&np zO^#nmr?u?)K=pUM-TiS0=GRtqmieVoYN=b#8#Ssqn<^AyI5JB!(_%iC&06^wNd>L3 z6k<-&M4p_|__>i*ZN&TP%&hx#v0RAM&+|liI(H)dK*-OYHUbK|PN0>;v!{&!jQta` zYJ&I5t(=JGXHOdm7%Dic7U5|lU$^(KotCw-=y;R8QRy?Tyr;i4Q|s(zku#V>5YBEE zReHp8D3s6H%_4c=(zGfKal}vj>}JuRj&KGj)w8=C-VKtGcxmakT#Ga z$O9la5}w%u@&?EcL4E@A3y@!f{2m1Rf0=V>^)+)o$i*PDK^7Ac&L1Y?4eYK99}lJE Fe*l$Z9{m6S literal 169139 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 = $jiraService; } - - /** - * 生成上周周报 + * 生成周报 */ public function generateWeeklyReport(Request $request): JsonResponse { try { $username = $request->input('username') ?: config('jira.default_user'); + $period = $request->input('period', 'this_week'); - if (!$username) { + if (! $username) { return response()->json([ 'success' => false, - 'message' => '请提供用户名' + 'message' => '请提供用户名', ], 400); } - $report = $this->jiraService->generateWeeklyReport($username); + if (! in_array($period, self::WEEKLY_REPORT_PERIODS, true)) { + return response()->json([ + 'success' => false, + 'message' => '无效的周报周期', + ], 400); + } + + $report = $this->jiraService->generateWeeklyReport($username, $period); return response()->json([ 'success' => true, 'data' => [ 'report' => $report, 'username' => $username, - 'generated_at' => Carbon::now()->format('Y-m-d H:i:s') - ] + 'period' => $period, + 'generated_at' => Carbon::now()->format('Y-m-d H:i:s'), + ], ]); } catch (\Exception $e) { return response()->json([ 'success' => false, - 'message' => '生成周报失败: ' . $e->getMessage() + 'message' => '生成周报失败: '.$e->getMessage(), ], 500); } } @@ -77,14 +86,14 @@ class JiraController extends Controller 'total_records' => $workLogs->count(), 'date_range' => [ 'start' => $startDate->format('Y-m-d'), - 'end' => $endDate->format('Y-m-d') - ] - ] + 'end' => $endDate->format('Y-m-d'), + ], + ], ]); } catch (\Exception $e) { return response()->json([ 'success' => false, - 'message' => '获取工时记录失败: ' . $e->getMessage() + 'message' => '获取工时记录失败: '.$e->getMessage(), ], 500); } } @@ -98,8 +107,8 @@ class JiraController extends Controller 'success' => true, 'data' => [ 'default_user' => config('jira.default_user', ''), - 'host' => config('jira.host', '') - ] + 'host' => config('jira.host', ''), + ], ]); } @@ -110,26 +119,33 @@ class JiraController extends Controller { try { $username = $request->input('username') ?: config('jira.default_user'); + $period = $request->input('period', 'this_week'); - if (!$username) { + if (! $username) { return response()->json([ 'success' => false, - 'message' => '请提供用户名' + 'message' => '请提供用户名', ], 400); } - $report = $this->jiraService->generateWeeklyReport($username); - $filename = sprintf('weekly_report_%s_%s.md', $username, Carbon::now()->subWeek()->format('Y-m-d')); + if (! in_array($period, self::WEEKLY_REPORT_PERIODS, true)) { + return response()->json([ + 'success' => false, + 'message' => '无效的周报周期', + ], 400); + } + + $report = $this->jiraService->generateWeeklyReport($username, $period); + $filename = sprintf('weekly_report_%s_%s_%s.md', $username, $period, Carbon::now()->format('Y-m-d')); return response($report) ->header('Content-Type', 'text/markdown') - ->header('Content-Disposition', 'attachment; filename="' . $filename . '"'); + ->header('Content-Disposition', 'attachment; filename="'.$filename.'"'); } catch (\Exception $e) { return response()->json([ 'success' => false, - 'message' => '下载周报失败: ' . $e->getMessage() + 'message' => '下载周报失败: '.$e->getMessage(), ], 500); } } - } diff --git a/app/Http/Controllers/SqlGeneratorController.php b/app/Http/Controllers/SqlGeneratorController.php index d0b55de..fe16182 100644 --- a/app/Http/Controllers/SqlGeneratorController.php +++ b/app/Http/Controllers/SqlGeneratorController.php @@ -25,7 +25,7 @@ class SqlGeneratorController extends Controller if (empty($caseCodes)) { return response()->json([ 'success' => false, - 'message' => '请提供有效的 case_id 列表' + 'message' => '请提供有效的 case_id 列表', ], 400); } @@ -59,8 +59,212 @@ class SqlGeneratorController extends Controller } catch (\Exception $e) { return response()->json([ 'success' => false, - 'message' => '查询 case_extras 失败: ' . $e->getMessage(), + 'message' => '查询 case_extras 失败: '.$e->getMessage(), ], 500); } } + + /** + * 查询 CRM 加工单关联地址国家,用于区分 PP-CN / PP-US。 + */ + public function checkProductionCountries(Request $request): JsonResponse + { + try { + $request->validate([ + 'production_codes' => 'required|array|min:1', + 'production_codes.*' => 'required|string|max:255', + ]); + + $productionCodes = array_values(array_unique(array_filter(array_map('trim', $request->input('production_codes'))))); + + if (empty($productionCodes)) { + return response()->json([ + 'success' => false, + 'message' => '请提供有效的加工单列表', + ], 400); + } + + $productionCountries = $this->getProductionCountries($productionCodes); + + return response()->json([ + 'success' => true, + 'data' => [ + 'production_countries' => $productionCountries, + ], + ]); + } catch (ValidationException $e) { + return response()->json([ + 'success' => false, + 'message' => '请求参数验证失败', + 'errors' => $e->errors(), + ], 422); + } catch (\Exception $e) { + return response()->json([ + 'success' => false, + 'message' => '查询 CRM 加工单国家失败: '.$e->getMessage(), + ], 500); + } + } + + private function getProductionCountries(array $productionCodes): array + { + $productionCountries = []; + + foreach (array_chunk($productionCodes, 1000) as $chunk) { + $productions = DB::connection('crmslave') + ->table('ea_production as ep') + ->join('ea_production_cstm as epc', 'ep.id', '=', 'epc.id_c') + ->where('ep.deleted', 0) + ->whereIn('ep.name', $chunk) + ->select([ + 'ep.name as production_code', + 'epc.ea_case_id_c', + 'epc.ea_businessorder_id_c', + 'epc.ea_salesorder_id_c', + ]) + ->get(); + + $caseIds = $productions->pluck('ea_case_id_c')->filter()->unique()->values()->all(); + $businessOrderIds = $productions->pluck('ea_businessorder_id_c')->filter()->unique()->values()->all(); + $salesOrderIds = $productions->pluck('ea_salesorder_id_c')->filter()->unique()->values()->all(); + + $caseCountries = $this->getCountriesByCaseIds($caseIds); + $businessOrderCountries = $this->getCountriesByBusinessOrderIds($businessOrderIds); + $salesOrderCountries = $this->getCountriesBySalesOrderIds($salesOrderIds); + + foreach ($productions as $production) { + $countries = array_merge( + $caseCountries[(string) $production->ea_case_id_c] ?? [], + $businessOrderCountries[(string) $production->ea_businessorder_id_c] ?? [], + $salesOrderCountries[(string) $production->ea_salesorder_id_c] ?? [] + ); + + $productionCountries[(string) $production->production_code] = array_values(array_unique(array_filter($countries))); + } + } + + return $productionCountries; + } + + private function getCountriesByCaseIds(array $caseIds): array + { + if (empty($caseIds)) { + return []; + } + + $results = DB::connection('crmslave') + ->table('ea_case as ec') + ->join('accounts_ea_case_1_c as aec1c', function ($join) { + $join->on('aec1c.accounts_ea_case_1ea_case_idb', '=', 'ec.id') + ->where('aec1c.deleted', '=', 0); + }) + ->join('accounts as a', 'a.id', '=', 'aec1c.accounts_ea_case_1accounts_ida') + ->join('accounts_cstm as ac', 'ac.id_c', '=', 'a.id') + ->where('ec.deleted', 0) + ->where('a.deleted', 0) + ->whereIn('ec.id', $caseIds) + ->whereNotNull('ac.country_c') + ->select([ + 'ec.id as entity_id', + 'ac.country_c as country', + 'ac.province_c as province', + ]) + ->get(); + + return $this->groupCountriesByEntityId($results); + } + + private function getCountriesByBusinessOrderIds(array $businessOrderIds): array + { + if (empty($businessOrderIds)) { + return []; + } + + $results = DB::connection('crmslave') + ->table('ea_businessorder as eb') + ->join('accounts_ea_businessorder_1_c as aeb1c', function ($join) { + $join->on('aeb1c.accounts_ea_businessorder_1ea_businessorder_idb', '=', 'eb.id') + ->where('aeb1c.deleted', '=', 0); + }) + ->join('accounts as a', 'a.id', '=', 'aeb1c.accounts_ea_businessorder_1accounts_ida') + ->join('accounts_cstm as ac', 'ac.id_c', '=', 'a.id') + ->where('eb.deleted', 0) + ->where('a.deleted', 0) + ->whereIn('eb.id', $businessOrderIds) + ->whereNotNull('ac.country_c') + ->select([ + 'eb.id as entity_id', + 'ac.country_c as country', + 'ac.province_c as province', + ]) + ->get(); + + return $this->groupCountriesByEntityId($results); + } + + private function getCountriesBySalesOrderIds(array $salesOrderIds): array + { + if (empty($salesOrderIds)) { + return []; + } + + $results = DB::connection('crmslave') + ->table('ea_salesorder as es') + ->join('accounts_ea_salesorder_1_c as aes1c', function ($join) { + $join->on('aes1c.accounts_ea_salesorder_1ea_salesorder_idb', '=', 'es.id') + ->where('aes1c.deleted', '=', 0); + }) + ->join('accounts as a_base', 'a_base.id', '=', 'aes1c.accounts_ea_salesorder_1accounts_ida') + ->join('accounts_cstm as ac', 'ac.id_c', '=', 'aes1c.accounts_ea_salesorder_1accounts_ida') + ->where('es.deleted', 0) + ->where('a_base.deleted', 0) + ->whereIn('es.id', $salesOrderIds) + ->whereNotNull('ac.country_c') + ->select([ + 'es.id as entity_id', + 'ac.country_c as country', + 'ac.province_c as province', + ]) + ->get(); + + return $this->groupCountriesByEntityId($results); + } + + private function groupCountriesByEntityId($results): array + { + $countriesByEntityId = []; + + foreach ($results as $result) { + $countryCode = $this->getCountryCode($result->country, $result->province); + if (! $countryCode) { + continue; + } + + $entityId = (string) $result->entity_id; + $countriesByEntityId[$entityId] ??= []; + $countriesByEntityId[$entityId][] = $countryCode; + } + + return array_map(fn ($countries) => array_values(array_unique($countries)), $countriesByEntityId); + } + + private function getCountryCode(?string $country, ?string $province): ?string + { + if (! $country) { + return null; + } + + if (in_array($country, ['1', '156'], true) && ! in_array((string) $province, ['710000', '810000', '820000'], true)) { + return 'CN'; + } + + return [ + '840' => 'US', + 'US' => 'US', + '316' => 'GU', + 'GU' => 'GU', + '630' => 'PR', + 'PR' => 'PR', + ][strtoupper($country)] ?? strtoupper($country); + } } diff --git a/app/Services/JiraService.php b/app/Services/JiraService.php index 7c635ff..17c9ff7 100644 --- a/app/Services/JiraService.php +++ b/app/Services/JiraService.php @@ -2,17 +2,19 @@ namespace App\Services; +use Carbon\Carbon; +use Illuminate\Support\Collection; use JiraRestApi\Configuration\ArrayConfiguration; use JiraRestApi\Issue\IssueService; use JiraRestApi\JiraException; use JiraRestApi\Project\ProjectService; -use Carbon\Carbon; -use Illuminate\Support\Collection; class JiraService { private IssueService $issueService; + private ProjectService $projectService; + private array $config; public function __construct() @@ -39,7 +41,6 @@ class JiraService $this->projectService = new ProjectService($clientConfig); } - /** * 按项目组织任务数据 */ @@ -51,7 +52,7 @@ class JiraService $projectKey = $issue->fields->project->key; $isSubtask = $issue->fields->issuetype->subtask ?? false; - if (!$organized->has($projectKey)) { + if (! $organized->has($projectKey)) { $organized->put($projectKey, [ 'name' => $issue->fields->project->name, 'tasks' => collect(), @@ -81,7 +82,7 @@ class JiraService 'summary', 'status', 'project', - 'issuetype' + 'issuetype', ]); } catch (JiraException) { return null; @@ -93,14 +94,14 @@ class JiraService $tasks->put($issue->key, [ 'key' => $issue->key, 'summary' => $issue->fields->summary, - 'url' => $this->config['host'] . '/browse/' . $issue->key, + 'url' => $this->config['host'].'/browse/'.$issue->key, 'subtasks' => collect(), ]); } private function addSubtask(Collection $tasks, string $parentKey, $issue): void { - if (!$tasks->has($parentKey)) { + if (! $tasks->has($parentKey)) { // 获取父任务的真实信息 $parentDetails = $this->getIssueDetails($parentKey); $parentSummary = $parentDetails ? $parentDetails->fields->summary : '父任务'; @@ -108,7 +109,7 @@ class JiraService $tasks->put($parentKey, [ 'key' => $parentKey, 'summary' => $parentSummary, - 'url' => $this->config['host'] . '/browse/' . $parentKey, + 'url' => $this->config['host'].'/browse/'.$parentKey, 'subtasks' => collect(), ]); } @@ -116,7 +117,7 @@ class JiraService $tasks[$parentKey]['subtasks']->put($issue->key, [ 'key' => $issue->key, 'summary' => $issue->fields->summary, - 'url' => $this->config['host'] . '/browse/' . $issue->key, + 'url' => $this->config['host'].'/browse/'.$issue->key, 'created' => $issue->fields->created ?? null, ]); } @@ -128,7 +129,7 @@ class JiraService { $username = $username ?: $this->config['default_user']; - if (!$username) { + if (! $username) { throw new \InvalidArgumentException('用户名不能为空'); } @@ -153,14 +154,14 @@ class JiraService 'status', 'project', 'issuetype', - 'created' + 'created', ]); - if (!empty($issues->issues)) { + if (! empty($issues->issues)) { return $this->organizeIssuesByProject($issues->issues); } } catch (JiraException $e) { - throw new \RuntimeException('获取未来任务失败: ' . $e->getMessage()); + throw new \RuntimeException('获取未来任务失败: '.$e->getMessage()); } return collect(); @@ -169,24 +170,21 @@ class JiraService /** * 生成 Markdown 格式的周报 */ - public function generateWeeklyReport(?string $username = null): string + public function generateWeeklyReport(?string $username = null, string $period = 'this_week'): string { $username = $username ?: $this->config['default_user']; - // 获取上周的工时记录 - $now = Carbon::now(); - $startOfWeek = $now->copy()->subWeek()->startOfWeek(); - $endOfWeek = $now->copy()->subWeek()->endOfWeek(); + $reportPeriod = $this->resolveWeeklyReportRange($period); - $workLogs = $this->getWorkLogs($username, $startOfWeek, $endOfWeek); + $workLogs = $this->getWorkLogs($username, $reportPeriod['start'], $reportPeriod['end']); $organizedTasks = $this->organizeTasksForReport($workLogs, $username); $nextWeekTasks = $this->getNextWeekTasks($username); - $markdown = "# 过去一周的任务\n\n"; + $markdown = "# {$reportPeriod['title']}\n\n"; if ($organizedTasks->isEmpty()) { - $markdown .= "本周暂无工时记录的任务。\n\n"; + $markdown .= "{$reportPeriod['empty_message']}\n\n"; } else { // 按Sprint分类的需求 if ($organizedTasks->has('sprints') && $organizedTasks['sprints']->isNotEmpty()) { @@ -311,6 +309,33 @@ class JiraService return $markdown; } + /** + * 解析周报统计周期 + * + * @return array{start: Carbon, end: Carbon, title: string, empty_message: string} + */ + private function resolveWeeklyReportRange(string $period): array + { + $normalizedPeriod = $period === 'this_week' ? 'this_week' : 'last_week'; + $now = Carbon::now(); + + if ($normalizedPeriod === 'this_week') { + return [ + 'start' => $now->copy()->startOfWeek(), + 'end' => $now->copy()->endOfDay(), + 'title' => '本周完成的任务', + 'empty_message' => '本周暂无工时记录的任务。', + ]; + } + + return [ + 'start' => $now->copy()->subWeek()->startOfWeek(), + 'end' => $now->copy()->subWeek()->endOfWeek(), + 'title' => '上周完成的任务', + 'empty_message' => '上周暂无工时记录的任务。', + ]; + } + /** * 获取指定日期范围内的工时记录 */ @@ -345,7 +370,7 @@ class JiraService 'customfield_14305', // 需求类型 ]); - if (!empty($issues->issues)) { + if (! empty($issues->issues)) { $workLogs = $this->extractWorkLogs($issues->issues, $username, $startDate, $endDate); if ($workLogs->isNotEmpty()) { @@ -353,8 +378,9 @@ class JiraService } } } catch (JiraException $e) { - throw new \RuntimeException('获取工时记录失败: ' . $e->getMessage()); + throw new \RuntimeException('获取工时记录失败: '.$e->getMessage()); } + // 如果所有查询都没有结果,返回空集合 return collect(); } @@ -377,7 +403,7 @@ class JiraService // 处理 author 可能是数组或对象的情况 $authorName = is_array($worklog->author) ? ($worklog->author['name'] ?? '') : ($worklog->author->name ?? ''); - if (!empty($authorName) && $authorName === $username && + if (! empty($authorName) && $authorName === $username && $worklogDate->between($startDate, $endDate)) { // 获取父任务信息 @@ -411,7 +437,7 @@ class JiraService 'project_key' => $issue->fields->project->key ?? '', 'issue_key' => $issue->key, 'issue_summary' => $issue->fields->summary ?? '', - 'issue_url' => $this->config['host'] . '/browse/' . $issue->key, + 'issue_url' => $this->config['host'].'/browse/'.$issue->key, 'issue_status' => $issue->fields->status->name ?? 'Unknown', 'issue_type' => $issue->fields->issuetype->name ?? 'Unknown', 'issue_created' => $issue->fields->created ?? null, @@ -464,7 +490,7 @@ class JiraService $sprintField = $issue->fields->customFields['customfield_10004']; // 处理数组情况 - if (is_array($sprintField) && !empty($sprintField)) { + if (is_array($sprintField) && ! empty($sprintField)) { $lastSprint = end($sprintField); if (is_string($lastSprint)) { // 解析Sprint字符串,格式通常为: com.atlassian.greenhopper.service.sprint.Sprint@xxx[name=十月中需求,...] @@ -486,13 +512,14 @@ class JiraService if (preg_match('/name=([^,\]]+)/', $sprintField, $matches)) { return $matches[1]; } + // 如果是纯文本,直接返回 return $sprintField; } } // 尝试从fixVersions获取版本信息作为备选 - if (isset($issue->fields->fixVersions) && is_array($issue->fields->fixVersions) && !empty($issue->fields->fixVersions)) { + if (isset($issue->fields->fixVersions) && is_array($issue->fields->fixVersions) && ! empty($issue->fields->fixVersions)) { return $issue->fields->fixVersions[0]->name ?? null; } @@ -526,7 +553,7 @@ class JiraService $stageValue = null; } - if ($stageValue && !empty($stageValue)) { + if ($stageValue && ! empty($stageValue)) { // 标准化阶段名称 if (str_contains($stageValue, 'SIT') || str_contains($stageValue, 'sit') || $stageValue === '测试阶段') { return 'SIT环境BUG'; @@ -537,8 +564,9 @@ class JiraService if (str_contains($stageValue, 'UAT') || str_contains($stageValue, 'uat')) { return 'UAT环境BUG'; } + // 如果不匹配标准格式,直接返回原值 - return $stageValue . 'BUG'; + return $stageValue.'BUG'; } } @@ -572,7 +600,7 @@ class JiraService // 处理对象类型 if (is_object($type) && isset($type->value)) { return $type->value; - } elseif (is_string($type) && !empty($type)) { + } elseif (is_string($type) && ! empty($type)) { return $type; } } @@ -601,7 +629,7 @@ class JiraService if (isset($issue->fields->customFields['customfield_10115'])) { $description = $issue->fields->customFields['customfield_10115']; - if (is_string($description) && !empty($description)) { + if (is_string($description) && ! empty($description)) { return $description; } } @@ -617,7 +645,7 @@ class JiraService // 从customfield_14305获取需求类型 if (isset($issue->fields->customFields['customfield_14305'])) { $type = $issue->fields->customFields['customfield_14305']; - if (is_array($type) && !empty($type)) { + if (is_array($type) && ! empty($type)) { $firstType = $type[0]; if (is_object($firstType) && isset($firstType->value)) { return $firstType->value; @@ -657,16 +685,8 @@ class JiraService */ private function extractDeveloper($issue): ?string { - // 从customfield_11000获取开发人 - if (isset($issue->fields->customFields['customfield_11000'])) { - $developer = $issue->fields->customFields['customfield_11000']; - - if (is_string($developer) && !empty($developer)) { - return $developer; - } - } - - return null; + // 从customfield_11000获取开发人(User 类型字段返回对象,兼容字符串/数组) + return $this->extractUserFieldName($issue, 'customfield_11000'); } /** @@ -674,13 +694,32 @@ class JiraService */ private function extractActualFixer($issue): ?string { - // 从customfield_11301获取实际修复人 - if (isset($issue->fields->customFields['customfield_11301'])) { - $fixer = $issue->fields->customFields['customfield_11301']; + // 从customfield_11301获取实际修复人(User 类型字段返回对象,兼容字符串/数组) + return $this->extractUserFieldName($issue, 'customfield_11301'); + } - if (is_string($fixer) && !empty($fixer)) { - return $fixer; - } + /** + * 通用 User 类型自定义字段解析:兼容对象、关联数组、字符串 + */ + private function extractUserFieldName($issue, string $fieldKey): ?string + { + if (! isset($issue->fields->customFields[$fieldKey])) { + return null; + } + + $value = $issue->fields->customFields[$fieldKey]; + + if (is_object($value)) { + return $value->name ?? $value->key ?? null; + } + + if (is_array($value)) { + // JIRA 用户字段以关联数组形式返回时 + return $value['name'] ?? $value['key'] ?? null; + } + + if (is_string($value) && $value !== '') { + return $value; } return null; @@ -695,6 +734,7 @@ class JiraService $summary = preg_replace('/!([^!]+\.(png|jpg|jpeg|gif|bmp))!/i', '', $summary); // 移除多余的空格和换行 $summary = preg_replace('/\s+/', ' ', $summary); + return trim($summary); } @@ -709,6 +749,9 @@ class JiraService '未开始', '需求已确认', '开发中', + 'Open', + 'To Do', + 'In Progress', '需求调研中', '需求已调研', '需求已评审', @@ -718,7 +761,7 @@ class JiraService ]; // 如果状态不在"未完成"列表中,则标记为已完成 - return !in_array($status, $incompleteStatuses, true); + return ! in_array($status, $incompleteStatuses, true); } /** @@ -775,13 +818,13 @@ class JiraService || ($actualFixer === $username); // 如果不是当前用户相关的Bug,跳过 - if (!$isUserRelated) { + if (! $isUserRelated) { continue; } // Bug按发现阶段分类 $stage = $workLog['bug_stage']; - if (!$organized['bugs']->has($stage)) { + if (! $organized['bugs']->has($stage)) { $organized['bugs']->put($stage, collect()); } @@ -804,15 +847,15 @@ class JiraService } elseif (($isStory || $isSubtask) && $workLog['sprint']) { // Story类型或子任务,且有Sprint的,按Sprint分类(需求) $sprintName = $workLog['sprint']; - if (!$organized['sprints']->has($sprintName)) { + if (! $organized['sprints']->has($sprintName)) { $organized['sprints']->put($sprintName, collect()); } $this->addTaskToSprintOrTaskList($organized['sprints'][$sprintName], $workLog); - } elseif ($isStory && !$workLog['sprint']) { + } elseif ($isStory && ! $workLog['sprint']) { // Story类型但没有Sprint的,放入需求分类 $this->addTaskToSprintOrTaskList($organized['stories'], $workLog); - } elseif ($isSubtask && !$workLog['sprint'] && $workLog['parent_task']) { + } elseif ($isSubtask && ! $workLog['sprint'] && $workLog['parent_task']) { // 子任务没有Sprint,检查父任务类型来决定分类 $parentKey = $workLog['parent_task']['key']; $parentDetails = $this->getIssueDetails($parentKey); @@ -846,7 +889,7 @@ class JiraService // 子任务 $parentKey = $workLog['parent_task']['key']; - if (!$taskList->has($parentKey)) { + if (! $taskList->has($parentKey)) { // 获取父任务的真实信息 $parentDetails = $this->getIssueDetails($parentKey); $parentSummary = $parentDetails ? $parentDetails->fields->summary : $workLog['parent_task']['summary']; @@ -855,7 +898,7 @@ class JiraService $taskList->put($parentKey, [ 'key' => $parentKey, 'summary' => $parentSummary, - 'url' => $this->config['host'] . '/browse/' . $parentKey, + 'url' => $this->config['host'].'/browse/'.$parentKey, 'status' => $parentStatus, 'subtasks' => collect(), ]); @@ -870,7 +913,7 @@ class JiraService ]); } else { // 主任务 - if (!$taskList->has($workLog['issue_key'])) { + if (! $taskList->has($workLog['issue_key'])) { $taskList->put($workLog['issue_key'], [ 'key' => $workLog['issue_key'], 'summary' => $workLog['issue_summary'], @@ -886,15 +929,15 @@ class JiraService * 获取下一个 release 版本 * 根据当前版本号,在 Jira 版本列表中找到下一个版本 * - * @param string $projectKey Jira 项目 key - * @param string|null $currentVersion 当前版本号(来自 master 分支的 version.txt) + * @param string $projectKey Jira 项目 key + * @param string|null $currentVersion 当前版本号(来自 master 分支的 version.txt) */ public function getUpcomingReleaseVersion(string $projectKey, ?string $currentVersion = null): ?array { try { $versions = $this->projectService->getVersions($projectKey); } catch (JiraException $e) { - throw new \RuntimeException('获取 release 版本失败: ' . $e->getMessage(), previous: $e); + throw new \RuntimeException('获取 release 版本失败: '.$e->getMessage(), previous: $e); } if (empty($versions)) { @@ -903,8 +946,8 @@ class JiraService // 按版本名称排序(假设版本号格式一致,如 1.0.0, 1.0.1, 1.1.0) $sortedVersions = collect($versions) - ->filter(fn($version) => !empty($version->name)) - ->sortBy(fn($version) => $version->name, SORT_NATURAL) + ->filter(fn ($version) => ! empty($version->name)) + ->sortBy(fn ($version) => $version->name, SORT_NATURAL) ->values(); if ($sortedVersions->isEmpty()) { @@ -914,17 +957,17 @@ class JiraService // 如果没有提供当前版本,返回第一个未发布的版本 if (empty($currentVersion)) { $candidate = $sortedVersions - ->filter(fn($version) => !($version->released ?? false)) + ->filter(fn ($version) => ! ($version->released ?? false)) ->first(); - if (!$candidate) { + if (! $candidate) { return null; } return [ 'version' => $candidate->name, 'description' => $candidate->description ?? null, - 'release_date' => !empty($candidate->releaseDate) + 'release_date' => ! empty($candidate->releaseDate) ? Carbon::parse($candidate->releaseDate)->toDateString() : null, ]; @@ -932,7 +975,7 @@ class JiraService // 找到当前版本在列表中的位置,返回下一个版本 $currentIndex = $sortedVersions->search( - fn($version) => $version->name === $currentVersion + fn ($version) => $version->name === $currentVersion ); // 如果找不到当前版本,尝试找到第一个大于当前版本的未发布版本 @@ -942,18 +985,19 @@ class JiraService if ($version->released ?? false) { return false; } + return version_compare($version->name, $currentVersion, '>'); }) ->first(); - if (!$candidate) { + if (! $candidate) { return null; } return [ 'version' => $candidate->name, 'description' => $candidate->description ?? null, - 'release_date' => !empty($candidate->releaseDate) + 'release_date' => ! empty($candidate->releaseDate) ? Carbon::parse($candidate->releaseDate)->toDateString() : null, ]; @@ -962,17 +1006,17 @@ class JiraService // 从当前版本的下一个开始,找到第一个未发布的版本 $candidate = $sortedVersions ->slice($currentIndex + 1) - ->filter(fn($version) => !($version->released ?? false)) + ->filter(fn ($version) => ! ($version->released ?? false)) ->first(); - if (!$candidate) { + if (! $candidate) { return null; } return [ 'version' => $candidate->name, 'description' => $candidate->description ?? null, - 'release_date' => !empty($candidate->releaseDate) + 'release_date' => ! empty($candidate->releaseDate) ? Carbon::parse($candidate->releaseDate)->toDateString() : null, ]; diff --git a/resources/js/components/jira/WeeklyReport.vue b/resources/js/components/jira/WeeklyReport.vue index 32e0e47..9fb75bd 100644 --- a/resources/js/components/jira/WeeklyReport.vue +++ b/resources/js/components/jira/WeeklyReport.vue @@ -3,12 +3,12 @@

生成周报

-

生成上周的工作周报

+

按周选择统计范围,生成对应周期的工作周报

-

生成上周周报

+

生成{{ selectedPeriodLabel }}周报

@@ -20,6 +20,17 @@ placeholder="输入 JIRA 用户名" >
+
+ + +
-
+
+
+

生成结果

+
+ 共 {{ stats.total }} 条,更新 {{ stats.update }} 条 +
+
+
+
+
+
{{ section.label }}
+ +
+ +
+
+
+

结果仅用于复制执行,请确认无误后使用

+
+
+ {{ copyStatus.message }} +
+
+ +

生成结果

@@ -143,6 +186,11 @@ export default { selectedTool: 'ob-external-id', inputText: '', outputSql: '', + splitOutputSql: { + sp: '', + ppCn: '', + ppUs: '' + }, querySql: '', loading: false, errors: [], @@ -161,7 +209,14 @@ export default { { value: 'ob-external-id', label: 'OB外部ID', - description: '每行输入 case_id 与 ob_id,系统会判断是否生成更新或插入 SQL。' + description: '每行输入 case_id 与 ob_id,系统会判断是否生成更新或插入 SQL。', + placeholder: '示例: X0X60F 17141\nC01008446046, 11894' + }, + { + value: 'new-factory-return-redelivery', + label: '新区工厂退库重新出库', + description: '每行输入加工单、病例号、正确运单,只读取前三列;根据 CRM 加工单关联地址国家拆分 PP-CN / PP-US SQL。', + placeholder: '示例: M20260508006905 225KF9 SF123456789\nM20260509005949 C01005934247 UPS987654321' } ] } @@ -169,12 +224,29 @@ export default { computed: { currentTool() { return this.toolOptions.find((tool) => tool.value === this.selectedTool) || this.toolOptions[0]; + }, + + showSplitOutput() { + return this.selectedTool === 'new-factory-return-redelivery'; + }, + + showQuerySql() { + return !this.showSplitOutput; + }, + + splitOutputSections() { + return [ + { key: 'sp', label: 'SP' }, + { key: 'ppCn', label: 'PP-CN' }, + { key: 'ppUs', label: 'PP-US' } + ]; } }, methods: { clearInput() { this.inputText = ''; this.outputSql = ''; + this.resetSplitOutputSql(); this.querySql = ''; this.errors = []; this.warnings = []; @@ -249,6 +321,7 @@ export default { async generateSql() { this.errors = []; this.outputSql = ''; + this.resetSplitOutputSql(); this.querySql = ''; this.warnings = []; this.resetCopyStatus(); @@ -259,6 +332,11 @@ export default { return; } + if (this.selectedTool === 'new-factory-return-redelivery') { + await this.generateNewFactoryReturnRedeliverySql(); + return; + } + this.errors = ['未识别的功能类型,请重新选择。']; }, @@ -321,6 +399,113 @@ export default { } }, + parseNewFactoryReturnRedeliveryInput() { + const lines = this.inputText.split(/\r?\n/); + const errors = []; + const rows = []; + + lines.forEach((line, index) => { + const trimmed = line.trim(); + if (!trimmed) { + return; + } + + const parts = trimmed.split(/[\s,,]+/).filter(Boolean); + if (parts.length < 3) { + errors.push(`第 ${index + 1} 行格式不正确,请提供加工单、病例号、正确运单三列`); + return; + } + + rows.push({ + productionCode: parts[0], + caseCode: parts[1], + expressNo: parts[2], + lineNumber: index + 1 + }); + }); + + if (rows.length === 0 && errors.length === 0) { + errors.push('请输入至少一行加工单、病例号、正确运单。'); + } + + return { + rows, + errors + }; + }, + + async generateNewFactoryReturnRedeliverySql() { + const { rows, errors } = this.parseNewFactoryReturnRedeliveryInput(); + if (errors.length) { + this.errors = errors; + return; + } + + this.loading = true; + + try { + const productionCodes = [...new Set(rows.map((row) => row.productionCode))]; + this.querySql = this.buildProductionCountriesSelect(productionCodes); + const response = await fetch('/api/sql-generator/production-countries/check', { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + 'X-CSRF-TOKEN': document.querySelector('meta[name="csrf-token"]').getAttribute('content') + }, + body: JSON.stringify({ + production_codes: productionCodes + }) + }); + + const data = await response.json(); + + if (!response.ok || !data.success) { + this.errors = [data.message || '查询 CRM 加工单国家失败,请稍后重试。']; + return; + } + + const productionCountries = data.data.production_countries || {}; + const missingRows = rows.filter((row) => !productionCountries[row.productionCode] || productionCountries[row.productionCode].length === 0); + if (missingRows.length) { + this.errors = missingRows.map((row) => `第 ${row.lineNumber} 行加工单 ${row.productionCode} 未在 CRM 查询到地址国家`); + return; + } + + const spSql = []; + const ppCnSql = []; + const ppUsSql = []; + + rows.forEach((row) => { + const productionCode = this.escapeSqlValue(row.productionCode); + const expressNo = this.escapeSqlValue(row.expressNo); + const ppSql = this.isUsProductionCountry(productionCountries[row.productionCode]) ? ppUsSql : ppCnSql; + + spSql.push(`update case_deliveries set express_no = '${expressNo}' where production_code = '${productionCode}';`); + ppSql.push(`update delivery_records set express_no = '${expressNo}' where production_code = '${productionCode}';`); + ppSql.push(`update receives set express_no = '${expressNo}' where production_code = '${productionCode}';`); + }); + + const sections = [ + this.buildSqlSection('SP', spSql), + this.buildSqlSection('PP-CN', ppCnSql), + this.buildSqlSection('PP-US', ppUsSql) + ].filter(Boolean); + + this.stats.update = spSql.length + ppCnSql.length + ppUsSql.length; + this.stats.total = this.stats.update; + this.splitOutputSql = { + sp: spSql.join('\n'), + ppCn: ppCnSql.join('\n'), + ppUs: ppUsSql.join('\n') + }; + this.outputSql = sections.join('\n\n'); + } catch (error) { + this.errors = ['网络请求失败: ' + error.message]; + } finally { + this.loading = false; + } + }, + escapeSqlValue(value) { return String(value).replace(/'/g, "''"); }, @@ -334,6 +519,32 @@ export default { return `select * from case_extras where case_code in (${inValues})`; }, + buildProductionCountriesSelect(productionCodes) { + if (!productionCodes.length) { + return ''; + } + + const inValues = productionCodes.map((productionCode) => `'${this.escapeSqlValue(productionCode)}'`).join(', '); + return [ + 'select ep.name, epc.ea_case_id_c, epc.ea_businessorder_id_c, epc.ea_salesorder_id_c', + 'from ea_production ep', + 'join ea_production_cstm epc on ep.id = epc.id_c', + `where ep.deleted = 0 and ep.name in (${inValues})` + ].join('\n'); + }, + + isUsProductionCountry(countryCodes) { + return (countryCodes || []).some((countryCode) => ['US', 'GU', 'PR'].includes(String(countryCode).toUpperCase())); + }, + + buildSqlSection(title, sqlLines) { + if (!sqlLines.length) { + return ''; + } + + return [`-- ${title}`, ...sqlLines].join('\n'); + }, + buildDuplicateWarnings(duplicates) { return duplicates.map((duplicate) => { const lines = duplicate.lineNumbers.join('、'); @@ -348,6 +559,14 @@ export default { }; }, + resetSplitOutputSql() { + this.splitOutputSql = { + sp: '', + ppCn: '', + ppUs: '' + }; + }, + setCopyStatus(message, type) { this.copyStatus = { message, @@ -433,6 +652,10 @@ export default { await this.copyToClipboard(this.outputSql, '复制失败,请手动复制结果。', 'outputTextarea'); }, + async copySplitOutput(key) { + await this.copyToClipboard(this.splitOutputSql[key], '复制失败,请手动复制结果。'); + }, + async copyQuery() { await this.copyToClipboard(this.querySql, '复制失败,请手动复制查询SQL。', 'queryTextarea'); } diff --git a/routes/api.php b/routes/api.php index 91f294c..40ec2ce 100644 --- a/routes/api.php +++ b/routes/api.php @@ -33,6 +33,7 @@ Route::prefix('env')->group(function () { // SQL 生成器 API 路由 Route::prefix('sql-generator')->group(function () { Route::post('/ob-external-id/check', [SqlGeneratorController::class, 'checkObExternalId']); + Route::post('/production-countries/check', [SqlGeneratorController::class, 'checkProductionCountries']); }); // JIRA API路由 diff --git a/tests/Feature/SqlGeneratorTest.php b/tests/Feature/SqlGeneratorTest.php new file mode 100644 index 0000000..e7b9c55 --- /dev/null +++ b/tests/Feature/SqlGeneratorTest.php @@ -0,0 +1,133 @@ +postJson('/api/sql-generator/production-countries/check', []); + + $response->assertStatus(422); + $response->assertJson([ + 'success' => false, + 'message' => '请求参数验证失败', + ]); + } + + public function test_check_production_countries_returns_country_codes_from_crm(): void + { + $connection = Mockery::mock(); + DB::shouldReceive('connection') + ->times(2) + ->with('crmslave') + ->andReturn($connection); + + $productionBuilder = Mockery::mock(); + $productionBuilder->shouldReceive('join') + ->once() + ->with('ea_production_cstm as epc', 'ep.id', '=', 'epc.id_c') + ->andReturnSelf(); + $productionBuilder->shouldReceive('where') + ->once() + ->with('ep.deleted', 0) + ->andReturnSelf(); + $productionBuilder->shouldReceive('whereIn') + ->once() + ->with('ep.name', ['M20260508006905']) + ->andReturnSelf(); + $productionBuilder->shouldReceive('select') + ->once() + ->with([ + 'ep.name as production_code', + 'epc.ea_case_id_c', + 'epc.ea_businessorder_id_c', + 'epc.ea_salesorder_id_c', + ]) + ->andReturnSelf(); + $productionBuilder->shouldReceive('get') + ->once() + ->andReturn(collect([ + (object) [ + 'production_code' => 'M20260508006905', + 'ea_case_id_c' => '', + 'ea_businessorder_id_c' => '', + 'ea_salesorder_id_c' => 'sales-order-1', + ], + ])); + + $salesOrderBuilder = Mockery::mock(); + $salesOrderBuilder->shouldReceive('join') + ->once() + ->with('accounts_ea_salesorder_1_c as aes1c', Mockery::type('Closure')) + ->andReturnSelf(); + $salesOrderBuilder->shouldReceive('join') + ->once() + ->with('accounts as a_base', 'a_base.id', '=', 'aes1c.accounts_ea_salesorder_1accounts_ida') + ->andReturnSelf(); + $salesOrderBuilder->shouldReceive('join') + ->once() + ->with('accounts_cstm as ac', 'ac.id_c', '=', 'aes1c.accounts_ea_salesorder_1accounts_ida') + ->andReturnSelf(); + $salesOrderBuilder->shouldReceive('where') + ->once() + ->with('es.deleted', 0) + ->andReturnSelf(); + $salesOrderBuilder->shouldReceive('where') + ->once() + ->with('a_base.deleted', 0) + ->andReturnSelf(); + $salesOrderBuilder->shouldReceive('whereIn') + ->once() + ->with('es.id', ['sales-order-1']) + ->andReturnSelf(); + $salesOrderBuilder->shouldReceive('whereNotNull') + ->once() + ->with('ac.country_c') + ->andReturnSelf(); + $salesOrderBuilder->shouldReceive('select') + ->once() + ->with([ + 'es.id as entity_id', + 'ac.country_c as country', + 'ac.province_c as province', + ]) + ->andReturnSelf(); + $salesOrderBuilder->shouldReceive('get') + ->once() + ->andReturn(collect([ + (object) [ + 'entity_id' => 'sales-order-1', + 'country' => '840', + 'province' => '', + ], + ])); + + $connection->shouldReceive('table') + ->once() + ->with('ea_production as ep') + ->andReturn($productionBuilder); + $connection->shouldReceive('table') + ->once() + ->with('ea_salesorder as es') + ->andReturn($salesOrderBuilder); + + $response = $this->postJson('/api/sql-generator/production-countries/check', [ + 'production_codes' => ['M20260508006905'], + ]); + + $response->assertStatus(200); + $response->assertJson([ + 'success' => true, + 'data' => [ + 'production_countries' => [ + 'M20260508006905' => ['US'], + ], + ], + ]); + } +} diff --git a/tests/Unit/JiraServiceTest.php b/tests/Unit/JiraServiceTest.php index daac7de..9edd908 100644 --- a/tests/Unit/JiraServiceTest.php +++ b/tests/Unit/JiraServiceTest.php @@ -2,9 +2,10 @@ namespace Tests\Unit; -use Tests\TestCase; use App\Services\JiraService; +use Carbon\Carbon; use Illuminate\Support\Collection; +use Tests\TestCase; class JiraServiceTest extends TestCase { @@ -19,12 +20,19 @@ class JiraServiceTest extends TestCase 'jira.host' => 'https://test-jira.example.com', 'jira.username' => 'test-user', 'jira.password' => 'test-password', - 'jira.default_user' => 'test-user' + 'jira.default_user' => 'test-user', ]); $this->jiraService = app(JiraService::class); } + protected function tearDown(): void + { + Carbon::setTestNow(); + + parent::tearDown(); + } + public function test_is_task_completed_returns_false_for_incomplete_statuses() { $reflection = new \ReflectionClass($this->jiraService); @@ -37,7 +45,7 @@ class JiraServiceTest extends TestCase '需求已评审', 'In Progress', 'To Do', - 'Open' + 'Open', ]; foreach ($incompleteStatuses as $status) { @@ -59,7 +67,7 @@ class JiraServiceTest extends TestCase 'Closed', 'Resolved', '已完成', - 'Complete' + 'Complete', ]; foreach ($completeStatuses as $status) { @@ -76,10 +84,11 @@ class JiraServiceTest extends TestCase $method = $reflection->getMethod('organizeTasksForReport'); $emptyWorkLogs = collect(); - $result = $method->invoke($this->jiraService, $emptyWorkLogs); + $result = $method->invoke($this->jiraService, $emptyWorkLogs, 'test-user'); $this->assertInstanceOf(Collection::class, $result); $this->assertTrue($result->has('sprints')); + $this->assertTrue($result->has('stories')); $this->assertTrue($result->has('tasks')); $this->assertTrue($result->has('bugs')); } @@ -100,10 +109,10 @@ class JiraServiceTest extends TestCase 'bug_stage' => null, 'bug_type' => null, 'parent_task' => null, - ] + ], ]); - $result = $method->invoke($this->jiraService, $workLogs); + $result = $method->invoke($this->jiraService, $workLogs, 'test-user'); $this->assertTrue($result['sprints']->has('十月中需求')); $this->assertCount(1, $result['sprints']['十月中需求']); @@ -124,28 +133,62 @@ class JiraServiceTest extends TestCase 'sprint' => null, 'bug_stage' => 'SIT环境BUG', 'bug_type' => '需求未说明', + 'bug_description' => null, 'parent_task' => null, - ] + 'assignee' => 'test-user', + 'developer' => null, + 'actual_fixer' => null, + ], ]); - $result = $method->invoke($this->jiraService, $workLogs); + $result = $method->invoke($this->jiraService, $workLogs, 'test-user'); $this->assertTrue($result['bugs']->has('SIT环境BUG')); $this->assertCount(1, $result['bugs']['SIT环境BUG']); $this->assertEquals('需求未说明', $result['bugs']['SIT环境BUG'][0]['bug_type']); } + public function test_resolve_weekly_report_range_for_last_week() + { + Carbon::setTestNow('2026-04-02 10:00:00'); + + $reflection = new \ReflectionClass($this->jiraService); + $method = $reflection->getMethod('resolveWeeklyReportRange'); + + $result = $method->invoke($this->jiraService, 'last_week'); + + $this->assertEquals('2026-03-23 00:00:00', $result['start']->format('Y-m-d H:i:s')); + $this->assertEquals('2026-03-29 23:59:59', $result['end']->format('Y-m-d H:i:s')); + $this->assertEquals('上周完成的任务', $result['title']); + } + + public function test_resolve_weekly_report_range_for_this_week() + { + Carbon::setTestNow('2026-04-02 10:00:00'); + + $reflection = new \ReflectionClass($this->jiraService); + $method = $reflection->getMethod('resolveWeeklyReportRange'); + + $result = $method->invoke($this->jiraService, 'this_week'); + + $this->assertEquals('2026-03-30 00:00:00', $result['start']->format('Y-m-d H:i:s')); + $this->assertEquals('2026-04-02 23:59:59', $result['end']->format('Y-m-d H:i:s')); + $this->assertEquals('本周完成的任务', $result['title']); + } + public function test_extract_sprint_info_from_string() { $reflection = new \ReflectionClass($this->jiraService); $method = $reflection->getMethod('extractSprintInfo'); - $issue = (object)[ - 'fields' => (object)[ - 'customfield_10020' => [ - 'com.atlassian.greenhopper.service.sprint.Sprint@xxx[name=十月中需求,state=ACTIVE]' - ] - ] + $issue = (object) [ + 'fields' => (object) [ + 'customFields' => [ + 'customfield_10004' => [ + 'com.atlassian.greenhopper.service.sprint.Sprint@xxx[name=十月中需求,state=ACTIVE]', + ], + ], + ], ]; $result = $method->invoke($this->jiraService, $issue); @@ -157,10 +200,10 @@ class JiraServiceTest extends TestCase $reflection = new \ReflectionClass($this->jiraService); $method = $reflection->getMethod('extractBugStage'); - $issue = (object)[ - 'fields' => (object)[ - 'labels' => ['SIT', 'bug'] - ] + $issue = (object) [ + 'fields' => (object) [ + 'labels' => ['SIT', 'bug'], + ], ]; $result = $method->invoke($this->jiraService, $issue); @@ -172,13 +215,116 @@ class JiraServiceTest extends TestCase $reflection = new \ReflectionClass($this->jiraService); $method = $reflection->getMethod('extractBugType'); - $issue = (object)[ - 'fields' => (object)[ - 'labels' => ['需求未说明', 'bug'] - ] + $issue = (object) [ + 'fields' => (object) [ + 'labels' => ['需求未说明', 'bug'], + ], ]; $result = $method->invoke($this->jiraService, $issue); $this->assertEquals('需求未说明', $result); } + + public function test_extract_developer_from_user_object() + { + $reflection = new \ReflectionClass($this->jiraService); + $method = $reflection->getMethod('extractDeveloper'); + + $issue = (object) [ + 'fields' => (object) [ + 'customFields' => [ + 'customfield_11000' => (object) [ + 'name' => 'zhangsan', + 'displayName' => '张三', + 'emailAddress' => 'zhangsan@example.com', + ], + ], + ], + ]; + + $this->assertEquals('zhangsan', $method->invoke($this->jiraService, $issue)); + } + + public function test_extract_developer_from_associative_array() + { + $reflection = new \ReflectionClass($this->jiraService); + $method = $reflection->getMethod('extractDeveloper'); + + $issue = (object) [ + 'fields' => (object) [ + 'customFields' => [ + 'customfield_11000' => [ + 'name' => 'lisi', + 'displayName' => '李四', + ], + ], + ], + ]; + + $this->assertEquals('lisi', $method->invoke($this->jiraService, $issue)); + } + + public function test_extract_actual_fixer_from_user_object() + { + $reflection = new \ReflectionClass($this->jiraService); + $method = $reflection->getMethod('extractActualFixer'); + + $issue = (object) [ + 'fields' => (object) [ + 'customFields' => [ + 'customfield_11301' => (object) [ + 'name' => 'wangwu', + 'displayName' => '王五', + ], + ], + ], + ]; + + $this->assertEquals('wangwu', $method->invoke($this->jiraService, $issue)); + } + + public function test_extract_developer_returns_null_when_field_missing() + { + $reflection = new \ReflectionClass($this->jiraService); + $method = $reflection->getMethod('extractDeveloper'); + + $issue = (object) [ + 'fields' => (object) [ + 'customFields' => [], + ], + ]; + + $this->assertNull($method->invoke($this->jiraService, $issue)); + } + + public function test_organize_tasks_for_report_includes_bug_when_user_is_developer_only() + { + $reflection = new \ReflectionClass($this->jiraService); + $method = $reflection->getMethod('organizeTasksForReport'); + + // 模拟 WP-7158 场景:经办人是测试同学,开发人才是当前用户 + $workLogs = collect([ + [ + 'issue_key' => 'WP-7158', + 'issue_summary' => '生产 Bug 修复', + 'issue_url' => 'https://test-jira.example.com/browse/WP-7158', + 'issue_status' => 'Done', + 'issue_type' => 'Bug', + 'sprint' => null, + 'bug_stage' => '生产环境BUG', + 'bug_type' => '代码错误', + 'bug_description' => null, + 'parent_task' => null, + 'assignee' => 'tester-user', + 'developer' => 'test-user', + 'actual_fixer' => null, + ], + ]); + + $result = $method->invoke($this->jiraService, $workLogs, 'test-user'); + + $this->assertTrue($result['bugs']->has('生产环境BUG')); + $this->assertCount(1, $result['bugs']['生产环境BUG']); + $this->assertEquals('WP-7158', $result['bugs']['生产环境BUG'][0]['key']); + } }