From 73392a9b83b85ad51c226a77d165f42143bd3a78 Mon Sep 17 00:00:00 2001 From: super <2903208875@qq.com> Date: Sat, 11 Apr 2026 00:57:34 +0800 Subject: [PATCH] =?UTF-8?q?=E4=BF=AE=E5=A4=8DBUG=EF=BC=8C=E5=AE=8C?= =?UTF-8?q?=E5=96=84=E5=8C=B9=E9=85=8D=E5=BA=97=E9=93=BA?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/.env | 1 + .../__pycache__/approve.cpython-312.pyc | Bin 40725 -> 45215 bytes .../__pycache__/del_brand.cpython-312.pyc | Bin 27142 -> 28509 bytes app/amazon/__pycache__/main.cpython-312.pyc | Bin 27901 -> 28953 bytes .../__pycache__/match_action.cpython-312.pyc | Bin 0 -> 32650 bytes app/amazon/__pycache__/tool.cpython-312.pyc | Bin 0 -> 3404 bytes app/amazon/match_action.py | 68 +- app/assets/convert.js | 2 +- app/assets/dedupe.js | 2 +- app/assets/delete-brand.js | 2 +- app/assets/split.js | 2 +- app/new_web_source/convert.html | 4 +- app/new_web_source/dedupe.html | 4 +- app/new_web_source/delete-brand.html | 4 +- app/new_web_source/split.html | 4 +- .../aiimage-backend.log.2026-04-10.0.gz | Bin 0 -> 26140 bytes .../config/DeleteBrandProgressProperties.java | 6 + .../controller/DeleteBrandRunController.java | 16 +- .../service/DeleteBrandStaleTaskService.java | 100 +- .../model/vo/ProductRiskTaskItemVo.java | 38 +- .../controller/ShopMatchController.java | 85 +- .../model/dto/ShopMatchCreateTaskRequest.java | 46 + .../dto/ShopMatchStageCompleteRequest.java | 17 + .../model/vo/ShopMatchTaskStageVo.java | 22 + .../service/ShopMatchTaskService.java | 426 +++- .../service/ModuleHistoryCleanupService.java | 45 +- .../src/main/resources/application.yml | 2 + .../brand/components/BrandShopMatchTab.vue | 1810 +++-------------- frontend-vue/src/shared/api/java-modules.ts | 31 +- frontend-vue/src/shop-match-main.ts | 7 +- 30 files changed, 1069 insertions(+), 1675 deletions(-) create mode 100644 app/amazon/__pycache__/match_action.cpython-312.pyc create mode 100644 app/amazon/__pycache__/tool.cpython-312.pyc create mode 100644 backend-java/aiimage-backend.log.2026-04-10.0.gz create mode 100644 backend-java/src/main/java/com/nanri/aiimage/modules/shopmatch/model/dto/ShopMatchCreateTaskRequest.java create mode 100644 backend-java/src/main/java/com/nanri/aiimage/modules/shopmatch/model/dto/ShopMatchStageCompleteRequest.java create mode 100644 backend-java/src/main/java/com/nanri/aiimage/modules/shopmatch/model/vo/ShopMatchTaskStageVo.java diff --git a/app/.env b/app/.env index 123ec51..4748534 100644 --- a/app/.env +++ b/app/.env @@ -12,6 +12,7 @@ zn_username=%E8%87%AA%E5%8A%A8%E5%8C%96_Robot client_name=ShuFuAI +# java_api_base=http://47.111.163.154:18080 java_api_base=http://127.0.0.1:18080 # java_api_base=http://8.136.19.173:18081 # java_api_base=http://8.136.19.173:18080 diff --git a/app/amazon/__pycache__/approve.cpython-312.pyc b/app/amazon/__pycache__/approve.cpython-312.pyc index cb0c49ba7abe241a3cfb549f48d71a321f57d1e0..99830ef623758dfca88b4ee8284e391f29e08dd9 100644 GIT binary patch delta 14254 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 delta 10360 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 Q%o?W1JUm|l`JOxf1I>7-<^TWy delta 2431 zcmZ`(eM}Tr5Z||Zd$;!u+y@-rL{SeAL9r$%Du{AKtQc&A#1`o7swd|`b`LCeDaKf1 z6HsJKYBgHzhac56HP>q5AB`HVUumO;sJ)`GwkF!7hAL@N)7ZY*E8Jn)m+bFNW_~ko zX5Q?(dyi}zBDQN*tBIj&jqO&TY2``VFOy80Scdt4F)|{78x8JA%H%Qvm)1Fj!Ef!t3z)0 zONtWZ>)Sh|knE!jxfW?UIUVIXgk=<>oPUF;=%@r23+J$Q0xt@I&CG=pMgEduA?3D^ zc0)+p;}{aMh6T$`)BC1@(tfLwLLIg&sAQRQY-LiFi#zWm6d_ogM^3?x;zc^bs0^%_ z?}Xf`LP`%S651h-I%z9xgrAG^;83*_zFBLusHANg!ziRLP37QVwF^dOSdz!dgb5KH zMys!OW+;~Mc%MaGLPK=w;5ru@V_{pF|qcwKhLqht9KH{2vl_!00zdVa%M_!b7vDqJ@ zb1Fg_dg0LI(j_OMY6lEfOePEAbj3U}7#Xd&&XPg6xUeX%pISGiHw8k0*7j;S&?U*m zIFrqRj%NQl$=|j-l2X}7Tv}$kF;brv4xt}YN(2xV z=R>r_Ve2+0#Iit%FV$*IxC1SsUbJCE=v(Zu+vCP=PUV5n_kd13OBiU+$et`F#q9}! zljGW0l6o;k3t7CxV|T=jG^GBekgxntNT*8nG5guLujoRJ=-$uu7-EN%i>aysNfY`Z zsDffo$Sv*VKr{(qED0N75~vNgeSB<1Fd2v6!@}87WjN#k_`tMr@$U6lVv$t}Z&%&JN%!u|v#JQD8bh}`w%%R)G zqAEhSOU20)FEdEHY%%&4aF<=VzH;tzIqzE#`KqCe(BJs+%6@VovhlUO9Em{6s=Ab8 ztUM2EFv_g~MQWC08T_j@*J>s5flxjZQt+D#9QYp$Ra;)Uju(QR_J|dCPmNz)VZE9O0--1T*sr6ebucaSd_ZL8eRPgG= z81Ye%?@*3>1>qV3_A+V^rJxk1Q*j6)t?xdnd( z-kGT)i;=fuxjP!>HPY_<}_7Lh2 z9ulYv=8+)?1()c*N9}iTCOCuKhhKxmW^JPLP`d>tZP?zcU6SUj9qB;JHUvAuZUlVQ zqXvI_XDBRp%lA=r7FBv>W3$rfm*aUoKwAw0wkXOeZJn`Pe?#5GgrmZ|4eg?#GbqiM jpEFPzs>xi&6VLAq;%JMhhY;65(I|ua=(`;34qf>N*ndrU diff --git a/app/amazon/__pycache__/main.cpython-312.pyc b/app/amazon/__pycache__/main.cpython-312.pyc index 73334f83e24e31be2e9b01a11e97bb3418db7c06..3d9cbe0e3b555820c3cf6e7c3dc54bf4ef551a7f 100644 GIT binary patch delta 5695 zcmb7I3v`p$k^ZlwUvJ5>C0UYf*^(dF!r0gjPa9*5378c8074fhV1)nQ;Fn~sWQ>V~ zp&@~=Kuqo?z$O$ENDF}mSdo^bNtZNfa+)SVZa5pzW_zF;ldzkfkfu4^SN818|NEi% z^l|n-$DiiTotZm#?#zAoo15g=Z;AO=CX=3nuk_Z9jRZOa;eSKC;>^piH$_^O3)+!0RXT8O~4CjM9iw0c9gC!6 zo6s4AfdsqAqghEY9cTyqIiZbeX@U#XQYD!TF>OK-^w3F3@7oTdmR`sNEvwXO+z7M<2>v_4g@59z~jq})hNFq#1gCKPZL8v*ryPEj{4Z=YG-BvkO^gXaPy9o zHU3c7%3z=)9Hgbd@Uk3D4Jl-;8W;bR3gXRfr@GiPnvE5LO5g?cbBd@!$3?hKEtr_{ z4H0VAabd$ao9*6UOA|DL_PlO99iqyJlJ(93wq7tyFjWziU}T+IH!-nAdJj9JEjB18 zdU^I6t;5crgVo26aDgomRYb9kqbpgj)m4%pii1gu2W{ahXwcFO*6Ts}t^MK$5f@peQr zu0xB3v}8W}Uwxj^8qQ=rt&U9Fq?hX;)1sQ4ngh?vzih1`Z>lHuBv1Oe?aFmrKk>Pq zGCuk@UKsdmtg265{oyFvefQL%yT{IszI5`=6R&)21or5GSMEM`@ROhZ_^!v>Bw5&O0fFBNwxMs0mo4B5e{mPT~b=r0)Z-3GP2R3RD?C`e^PTRB2X4s6j7CegW~y<<$GQ= zo`z9A!n2bu8|$~HFx5ued@uvb(ib7)QcyhnapiamwsI{bIp&Y_L6TQPp@0*vKc}r` zhvsCnU1_F5&9_op8&&UA4`>DHx1UTTuZ}&KK4|tS=qwn4&PHfv|CD|o$zgg&%b*-# z9mUM1Rs<;xS0R;yuo~fhgf$2c0{ApiK1wS{*8*cKvdbHmHYT)Ekn2{gP8j{Yuv*(h zd99?i!GKKUD4HX6?B|ZsrU1~Pe*oCU-7^|>+sP+|6(4(jkgYEBzUB9{oGJCm`X}m# z`k$l2zGtIHgYl9F^~9L4+UZ)WD55iSJI2CipTvqC4eSJckt-k7ah zT%~+t;eCteD1SbOUtAS?$+?p^R>4pWG#foAX7jSQlNxp@+d&H0FSD!2F~;ZICK>G0 zoE$QPX*{`PGn?+oPsdQvZh(X)6b|D1EHL{X5*okQchOCHA zaCFTD#~diH}bC#z>buj2%3 zG&RC6;nqXuN7EvTFmAun2S;Z4WJ}r+*j8=2GG8cQ|5@nOOp7SMWFhkwVFKOGu-df| zFaR3{iKL!W%SNK9%wOcS&%h8ISBwXda1m{1F}vN7Wt{4^p=45s{bO-e-pu5<4d@5- zuy16Hxrm2janyp5T;~b9$GH*9xD6?`Ni)XS!f~jNT6S6>Hf1t3xob?Zy(wymq_NwD zc2kky8;@JmisN~wN31aY;y*JgUNd{LyoSPYpE`?J`Si^BNxmfcvF5M%Q6$gNRGK^* zJ6P;Cz&sB8QPyT7(|!MsK<6X{IxuU>#Q)-e0qxj;FBjkM`quLFa}(Qd8MDrKN{#y) z4YG4H1X9H;C3aHGrj=~7xaqU)weM>XEzb zRZ!}~$H@(_1SneQ)&2l}Zn- zX13CWq?@fS?Ic^-&q^O`Ngvw`L4$5Xz|)z^yCPC}?M?91AJ{C)$BhLUx?ryuXvKR{ zLLU%Cp%w1f9f1Td{m6&WQr0rFaK*$j+2n0i-V|||osw0!I`wsj zTVZ$IjcsFFu&37_7-$XndqgU-_hge#r$QzCLgg`<);USmflly+4OGn(LAG*ti^f#^U8#d>W z`or}{`-caHyW-_dad-1|+o~Hm-l6(i`O|;6;^c~p&JXh|hgRNnJJ>3UZ4%`ES&@t!4Dzk79gyr3c8wC;LV%Z&oxv8A_?hq<^u?ybAJ?z*$_ zhPU9T`ksZe*oPh&ZazGCJ!RU*7TXd1Vg1qaXJWrEe@HPn40C?dQd_u!DBhaC*bYDM zSjqr@sjlT3Gq`s(=_}?b-gPUG_Ea@Y6|?#G^qv)E{Cj0Q((`l;dHnmN*3pp7 zf8bPu=m*(6us+CBB0Wvl=umzzGrKWOc`c0x`dS8Qu25dfs%mmpsp1Z7#GSfkohI(h zZdPgHH9T-eR6KA-bUvWz)8Hljdjy8y0cg{(OHNzNHv5Xkun(C}AZ%bCEpU?__Vt2% zvWI0|K0mv(fEQbujWe!MTx zz3kbAMR1pRYhlU4t-$c5CY41$N=UWww?HM7VgIIthKlWwty}1m*g1qCUmw`anuTP5 z1!`RN(zSPTW{;qdG(9QpJ=l~cwgIUl0N`MM2fR8a()lR%G_Y$m#i{cC2BMS!KU~9Lel?q<)0(Qf%vzs7`$f=}Pv8m8Io-kUWhr zHGIhXKEeqE=^6%?s8eipgFx}IQ))>hRZdm_a04fh4){QzNVjDeR> zheSVZ+d})~zj)0VSQfTl{pl>3`Sq7BPB=4aU(WTOaCv$7ctfjz6%knPT zkuI*%$uV(r1wtb~%FeLI9`X@q?7~Bb2zfWQaQ(|lsJ1_B^cs>2&EwB^&H-ES#7w9< zL%~2ovqkg^A`Q`b>|uYW7qh1?6<~Dl9WEj zWgJF4q3-JU)6Jo8KWn$Kt70CBuusGaQpgas70tlSPhU+b_Bw72R8AnriT$;M8K;q zJ%b>nl2mUAXrARLUjO7-+L1!Qt^q7cI2e-+sVc7mAb3e zEA^Ll-s7+xZ(9A8MBJMm`>)Nbh*~mfkFDx>j8x)I+yF};8AtCEsoxO?HU<4uSRM$& zv#hT-EC&buk&|4$Ikg z&N<)tzVqC>hdv?SzEA9zZ8kFp&)$}IJ2!0^w&#&^!(~66&U5oQ$+(A;Oec60bKJT- z$-IYK=aus%OLsZPQ6taE-V>UTBisNPAR+7Uzd!awMtJI1_Q>MPyV*A+kNBCJpE20M zOQt=#br#7W8)Z>8b?{zL1FjjkR>?AvD_Lb*hfcDA#4g!ohvc|bDmi7_Eqk(Hk5$Ts zJx+B`&Uf|a&jn5F zWn(QHG`WiZ4;$=NQ*os&DIDe``w>BMjGjukq0f_KF1JsN@SC^@zuwJpDKROsQ!`x5 zZ?;*)VSdP?<5DCgvO;67p7Q(2kcDl@_7E31;AcbTYC(6>&CZ$wtlMm98OVw71T3Y* zey*5{=Z(5k!qKLbK3*`oPU=VfqLq?!BA-=Qifvin;e0*Ym#g2QA9_m4Nx^qN>F2fy zYq@?>3~9*w{+pObbwVCiW!R>^@LrYL9LZQ(3_N-OyO81gzUuQf@YgASRNR@vO_K;&X(Xn?O z1N!U1rnkIJY_+pgcsn4a_d3mF@E*{5)i%GLd(GNfFfYixUPfT?dbtkiD)+pA?mVwU z)bqeOZ?uB+LO=xiLeMn7R=7}m`}_*wVyOuHixnbpF4hW2*Ed_+8U;EJ>_36LCT zgpUB9FYBS84y2vg6}*22te1xL6ho2WJEe>@Mxzl~QNnTXing$IE}u_r5;R8_0w`!> zGzhejJ>m+Id)aSYFL=KHv<=Lf-OAoG*w|`Qj{T%Cns#guw)0*$o04m1m&`6!k)0RN z9T}6ud^11+siP83rff*kexTA?Zn^!Zf6G-I{z)tR_)nM92-m^OQwLRFjzyAq(G;}z1FnYau^>z2% z?^xITwIF!P_3OeH3*RYdVnHu;7dHpF*Mg>&D&e)-hL$qncV%KrRr)#a4$(RtEH0%! zJg0(9E80e8u~S7JQo{aFG?VOOI{(MS%RcoNkxHfyOeXiSP@p6i-K8=>#-PNbv6w7{ z2wl%2fz7V<$Q5Nuqcr58vrusg`!L|sS0P!=_{mkZYA{-HJsuI$ctq%Gv|+%y33p0# z4O>6C$g9T11Og}+?NNJ&E@RydUJ-Q%(&@=B@%pW3H_m!Xz52b-X$LrVs?@`dv{}mOT`%*noN|xL#3e0Bvy`(PYNQ(~T_Hmcvmsw0I~%g{L&w;a zP~9Dql4h8Bz9XVkFh27J%q9*di9IRK<<_r&iRVc2am>2HW?4#aEcvW61W4Bk{SseOK-J1u@Q2Dtd_rc1qOxe)wnfY=2MO2@pZPX;1pXi=s zP8p#Q%qiOr8%*mOjbhh*UU6f}mUOWDW_c{dqv1$7(Q=?9=>!)`|A&2@RKR+C0Sk@- z_30{M&cgh{iN{Vja@F{eLb-NzTlqxO>}Ivk0@oY*YD9Btb?Ap9<(JAv{e{3!y(Kp-OTK@l1FW*5Xo302}2 z>mpt3mAcjQT_atEwxS3)Myb|bNLc_fB5m*6Bx?i6i40RTCiiJ~(b|vo)R!$D>%WX8 z(i2tWFtjt3Ls?ogYORDe`&AS01D5ECV^40!wKWp$jkWg;g!|fiWx9cVSRb;e6Aycp z7O|=Z|D-G+M;mtRTkBi}I@f1NgM#vz!1gt~MgGV(PCb@CJn!ao>HN|Iy{cgKs;6 ze|EYKn1{^=>mE&ieA_+zAkM(wJDSTDIQZA6djK!Enz_a0+(mcpLOp-6l1I8`=AxvEld^i4ZGUpBOUDTO(i77oXvjc4rGs&1|4Kg&2wNl?QCw>tAlno`&;uv z{%N3KGRa&*aZy8p*DO`7(-BNWe-ppA>O_* zW1w;eBy=;~i^88Ej1)#`YvV?WYnXUlyhjioMHp#U zIC;SeiPW>_S4<%@*oP~sNQ7Be&Lq28+sY*#2~-nSfEy1S{p`>&tvl&s?BvQ&=|*55 z=c#%&{`vtu7J%a>dH`7TAp3e{C0WaYs{&*@Yg={qOf@-UDbZdW4x{Q52)E`0cp1GS zx6{aG8e{LTDhcEK)M{-!9^<)~5E;}{vw|1Qm}s;=+}S6|+bG^uP0&a}P^?$F6X70( zM%cZpm+RF$994f{F-5!5Z>}y9$(r;R>$V8j3sKu&=2kVdi2B8I^6(D5O3UDmKRCRj1_HOZ94UT%F zb7*2A3x{(xzn*0GheO1kekS}dA?MOl+kYiMKYlI+EMC;aDGjZdE0^Ro068zD%jmk4 zXkW&#S#FnPs?gim3b_mV^c6W@Uys_m*$46*C>~!&{viIh4p}Ic4Bdk;1t4Rj@|Fbr zEmShPSX7DA8f04FRX?6k!jUML{hq{!jGD`;qvvpkkf1%|nM12eEZV2Y+FLcgomY*{ zT5Z%~47YnG{=LBC!`eKbm;R*VCEl+V6rPdMclWo`O^PDG0_|p1+ia_l~*Q~)(RKwJ1x{Hv~A&3Z#2u%pf05U=+ zG`fOICj!m@dK3X)hUk+BYCWipa~+n)IT}Q+TB<|AHg;u}A6M%_-lVMf>6-q}b01mqreTl6to zftK*QaKlCRUi46dW*#-@TL|wWjJcRG&gzXyi5|I;egXoFxpEhKHCE;LN}n*1>MuCN Of5vjyH!-gcxBVAb>pvL) diff --git a/app/amazon/__pycache__/match_action.cpython-312.pyc b/app/amazon/__pycache__/match_action.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..c770364f28c0f159252f50cecca50ba9cc2b0bf6 GIT binary patch literal 32650 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 F{{ne(&949e literal 0 HcmV?d00001 diff --git a/app/amazon/__pycache__/tool.cpython-312.pyc b/app/amazon/__pycache__/tool.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..f2537c6fb61749490b0fc1d17ffd30a75f0d9fbf GIT binary patch literal 3404 zcmbUkZ)_9S{hj~BF>!zpR3M(VM^YNQjU5vY)7UF0iPDy-qiTz^fucp1doRv~^WEv) zIkvO7VyN9hS9Kkllu*i=PE<`t+riqk8&uQ8KJE(-XzxVvt*48ZuM)b2~c_Dz%Kln#&c&oteBmi^Bj|d`yf=(dfB`)eB zZt5EM5YG$V6Yhf0g{bf#VqO7gPN3uH<@OxPyN{!s5N=xIf{t3g3^fcfO|3u`7&Qwz zm0VRdeDcB14Zqtn0#|ABHG8l#U&E8@IcoU4AI+f2n#t8wJ`sVqE>$@i;@!lvDc(z( zYWSjy_((JHlP#p>bx+Yf)(bg0HYL*>UVHPS(v{O|r(Roq_ zuYCT~-;`cjT={6Wbn(g;pPbo(Vc4$$mNt?$#FwgRPvQzc(69XXxA$Z1qDbNja`no! z(yxAoLq=ATpm=(7B2#5G-AftMSg)*(Yhjz;61t~dBi)wJ2~(#fY=KFg_H`u=4)w(c z02odT^u>n&c>F+OD8YgLSbw~~KkTu5RpFMeD&6wh;w=xCpM4Z+r*0$nA~c>=B~#Ya zP#FK{9t_9!_TtsIudSWCSo-*PrT6AbuP?5?_Cm#92kn%|FwM(`2`(MgOnDqXhKVL+ zGgLLBX=)BDl!JqXBP7&a&p|}xq_QM(8@j_Ohggskx<_myJGrg*5JefyXj3%QKw#0K z!m$8umuAnGu3fFTT6+JFrE`l8D(GDQOPil(Y$H%Ylh5&m&D_8o{|(cH9d*@VtPJ>6*_Jv@qySrSY@JWjB6~Dtbhub z`GZgdaN!j0#*aAM<1y_X6Qz@Bre#%<#MyUX!=|vLDH==S+=Oh>6xKyTAmK^8zfaFs zwT@|wP!?e#k+Vh;f16vfu?8(4F(yQ!<&rptxe!|tOOJ`6SdaZg<6z=+L{wuCnJ0zg^6tFidcX^Dill> z6*;X&z{?CcAwlU#y7XVipx7=oaHp}Z`zSK;VsU}L3PluvHwE*8M5H_`%xT zdn+%WSv&i_gNpkjDVjkC&Hu-t8f#QmRmz?`_PwXLKYy8P;7py=u`8r3GkBTmnp{cT z`g(bkSERj=qjGuzw#b;KKmlY#mX_5>W!G$!Vq@$v0gxp1-N6I zux#L%Grc`H9*Z@IDA(&eYl=P4jve&H@ckzhur$i+SU4C@Zu>8Y$iR%2Au)I2hEn&G0lZAbZ-%ptb zURJ;m$LVP8R_nLh{Afnht>&@xsQqR6CJlfHwFGMLt0H$1#@(LBlOsCQCaGkONKDKr z(EW8K3wvnUt3Z0wqG8c zJAC$V*_HIiKMQugaroTf#rS3K&0ykI@X%85&>xIH58n)a_g3)PrQowSp8Nid<3GF^ z9G!it+>E}lZ#nqj@}AD+-Fv_Ed-rUeJyH&!-FxSXzbGz@T<*WQ>#^A<%T0k`XL&d3 zdSs!ajGB5vKoUN%@O&9PkZ3LM3+>uI`^24N0&3m)^QX@|JwLD%_~xy^!%KmO7Y}|K zh?flk?d&?0`|6G~Rb2fAj44GB79z@p+?+_Zb8lM hV6o7?ymiOvCpb&{T?76uq0^r2=v7~b`<&3>{tu7yr=|b^ literal 0 HcmV?d00001 diff --git a/app/amazon/match_action.py b/app/amazon/match_action.py index 260ff27..e0ad574 100644 --- a/app/amazon/match_action.py +++ b/app/amazon/match_action.py @@ -226,6 +226,9 @@ class MatchTak: items = data.get("items", []) country_codes = data.get("country_codes", []) risk_listing_filter = data.get("risk_listing_filter", "All") + user_id = data.get("user_id") + stage_index = data.get("stage_index") + final_stage = bool(data.get("final_stage", True)) if not task_id: self.log("任务ID为空,跳过", "WARNING") @@ -269,7 +272,7 @@ class MatchTak: show_notification(f"开始处理店铺: {shop_name}", "info") try: - self.process_shop(shop_item, country_codes, task_id,risk_listing_filter) + self.process_shop(shop_item, country_codes, task_id, risk_listing_filter, user_id, stage_index, final_stage) # 更新已处理店铺数 if task_id in runing_task: runing_task[task_id]["processed_shops"] += 1 @@ -296,7 +299,7 @@ class MatchTak: runing_task[task_id]["status"] = "failed" runing_task[task_id]["error"] = str(e) - def process_shop(self, shop_item: dict, country_codes: list, task_id: int, risk_listing_filter: str): + def process_shop(self, shop_item: dict, country_codes: list, task_id: int, risk_listing_filter: str, user_id=None, stage_index=None, final_stage: bool = True): """处理单个店铺 Args: @@ -406,9 +409,11 @@ class MatchTak: import traceback self.log(f"处理国家 {country_code} 失败: {str(e)}", "ERROR") self.log(traceback.format_exc(), "ERROR") - # 最后回传,标记完成 try: - self.post_result(task_id, shop_name, country_code, "", "", is_done=True) + if final_stage: + self.post_result(task_id, shop_name, country_code, "", "", is_done=True) + else: + self.post_stage_finished(task_id, user_id, stage_index) except Exception as e: self.log(f"回传结果失败: {str(e)}", "ERROR") finally: @@ -560,6 +565,44 @@ class MatchTak: self.log(f"国家 {country_name} 处理完成") + def post_stage_finished(self, task_id: int, user_id, stage_index): + import requests + from config import DELETE_BRAND_API_BASE + + if user_id in (None, "", 0): + raise ValueError("user_id is required for stage completion callback") + if stage_index is None: + raise ValueError("stage_index is required for stage completion callback") + + url = f"{DELETE_BRAND_API_BASE}/api/shop-match/tasks/{task_id}/stage-finished" + payload = {"stage_index": stage_index} + params = {"user_id": user_id} + + max_retries = 3 + for retry in range(max_retries): + try: + self.log(f"Attempting stage completion callback ({retry + 1}/{max_retries})") + response = requests.post( + url, + params=params, + json=payload, + headers={"Content-Type": "application/json"}, + timeout=30, + verify=False, + ) + self.log(f"Stage completion callback response: {response.text}") + data = response.json() if response.text else {} + if response.status_code == 200 and isinstance(data, dict) and data.get("success"): + self.log(f"Stage completion callback succeeded: task={task_id}, stage={stage_index}") + return + self.log(f"Stage completion callback failed, status={response.status_code}", "WARNING") + except Exception as e: + self.log(f"Stage completion callback exception: {str(e)}", "ERROR") + if retry < max_retries - 1: + time.sleep(2) + + raise RuntimeError(f"Stage completion callback failed after retries: task={task_id}, stage={stage_index}") + def post_result(self, task_id: int, shop_name: str, country_code: str, asin: str, status: str,is_done: bool = False): """回传处理结果到API @@ -596,8 +639,8 @@ class MatchTak: max_retries = 3 for retry in range(max_retries): try: - print("================【匹配】=====================") - self.log(f"尝试回传结果 (第 {retry + 1}/{max_retries} 次)") + print("================回传处理结果====================") + self.log(f"尝试回传结果 (第{retry + 1}/{max_retries} 次)") self.log(f"回传URL: {url}") self.log(f"回传数据: {payload}") response = requests.post( @@ -608,12 +651,11 @@ class MatchTak: verify=False ) self.log(f"回传结果: {response.text}") - - if response.status_code == 200: - self.log(f"结果回传成功: {asin} - {status}") + data = response.json() if response.text else {} + if response.status_code == 200 and isinstance(data, dict) and data.get("success"): + self.log(f"回传结果成功: {asin} - {status}") return - else: - self.log(f"结果回传失败,状态码: {response.status_code}", "WARNING") + self.log(f"回传结果失败,状态码: {response.status_code}", "WARNING") print("=====================================") except Exception as e: @@ -624,9 +666,7 @@ class MatchTak: if retry < max_retries - 1: time.sleep(2) - self.log(f"已达到最大重试次数,结果回传最终失败", "ERROR") - - + raise RuntimeError("已达到最大重试次数,结果回传最终失败") if __name__ == "__main__": user_info = { diff --git a/app/assets/convert.js b/app/assets/convert.js index 6cf91a3..d4284b4 100644 --- a/app/assets/convert.js +++ b/app/assets/convert.js @@ -1 +1 @@ -import{d as A,o as H,a as n,c as o,b as z,B as K,e as s,F as w,r as F,t as r,f as _,n as E,w as M,s as j,u as q,g as u,h as T,x as J,i as k,E as i,y as L,z as O,A as G,_ as Q,p as W,q as X}from"./pywebview-Cq_E2BnJ.js";import{e as Y}from"./brand-DZ5_PGnw.js";const Z={class:"page-shell module-page"},ee={class:"main-content"},se={class:"left-panel"},te={class:"upload-zone"},ae={class:"selected-files clean-placeholder"},le={key:0,class:"more-line"},ne={key:1},oe={class:"option-group"},ie=["value"],re={class:"template-item-content"},ce={class:"label"},ue={key:0,class:"template-tag"},de={key:1,class:"template-tag muted"},ve={key:2,class:"template-tag custom"},pe={class:"desc"},_e={key:0,class:"empty-option"},fe={class:"option-group"},me={class:"radio-item active"},he={class:"desc"},ye={class:"run-row"},ge=["disabled"],be={class:"loading-msg"},we={class:"right-panel"},ke={class:"task-list-wrap"},Ce={class:"clean-result-summary"},xe={class:"summary-card"},Fe={class:"summary-card"},Pe={class:"summary-card"},Ee={class:"result-list-wrap"},Te={key:0,class:"empty-tasks"},Ie={key:1,class:"task-list clean-result-list"},Be={class:"left"},Ne=["title"],$e={key:0,class:"files"},De={key:1,class:"files"},Re={class:"task-right"},Se=["onClick"],Ue=["onClick"],Ve=A({__name:"BrandConvertTab",setup(He){const v=u([]),g=u(""),f=u([]),m=u(!1),b=u([]),h=u({total:0,successCount:0,failedCount:0,items:[]}),d=u([]),c=u(""),C=T(()=>v.value.slice(0,8)),I=T(()=>d.value.find(a=>a.id===c.value)||null);async function P(a){const e=k();if(!e?.upload_file_to_java)throw new Error("当前桌面端未提供文件上传桥接能力");const t=[];for(const l of a){const p=typeof l=="string"?l:l.absolutePath,V=typeof l=="string"?void 0:l.relativePath,y=await e.upload_file_to_java(p,V);if(!y?.success||!y.data)throw new Error(y?.error||y?.message||`上传失败:${p}`);t.push(y.data)}return t}async function B(a){const e=await J();d.value=e||[];const t=d.value.find(p=>p.id===c.value);if(t){c.value=t.id;return}const l=d.value.find(p=>p.isDefault);if(l){c.value=l.id;return}c.value=d.value[0]?.id||""}async function N(a,e){v.value=a,g.value="",f.value=await P(a),i.success(e)}async function $(){const a=k();if(!a?.select_brand_xlsx_files){i.warning("当前环境不支持文件选择,请在本机客户端中打开");return}try{const e=await a.select_brand_xlsx_files();if(!e?.length)return;await N(e,`已选择 ${e.length} 个待转换文件`)}catch(e){i.error(e instanceof Error?e.message:"选择失败")}}async function D(){const a=k();if(!a?.select_brand_folder){i.warning("当前环境不支持文件夹选择,请在本机客户端中打开");return}try{const e=await a.select_brand_folder();if(!e)return;g.value=e.split(/[/\\]/).filter(Boolean).pop()||"";const t=await Y(e);if(t.success&&t.items?.length){v.value=t.items.map(l=>l.relativePath),f.value=await P(t.items),i.success(`已选择文件夹内 ${t.items.length} 个 xlsx 文件`);return}i.warning(t.error||"该文件夹下没有 xlsx 文件")}catch(e){i.error(e instanceof Error?e.message:"选择失败")}}async function R(){if(!f.value.length){i.warning("请先选择待转换 Excel 文件或文件夹");return}if(!c.value){i.warning("请先选择模板");return}try{m.value=!0;const a=await L({files:f.value.map(e=>({fileKey:e.fileKey,originalFilename:e.originalFilename,relativePath:e.relativePath})),templateId:c.value,archiveName:f.value.some(e=>!!e.relativePath)&&g.value?g.value:void 0});h.value=a,b.value=a.items||[],await x(),i.success("格式转换完成")}catch(a){i.error(a instanceof Error?a.message:"格式转换失败")}finally{m.value=!1}}async function x(){try{const a=await O();b.value=a.items||[],h.value={total:a.items?.length||0,successCount:a.items?.filter(e=>e.success).length||0,failedCount:a.items?.filter(e=>!e.success).length||0,items:a.items||[]}}catch{}}async function S(a){try{await G(a),await x(),i.success("已删除")}catch(e){i.error(e instanceof Error?e.message:"删除失败")}}async function U(a){const e=k();if(!a.downloadUrl){i.warning("当前结果没有下载地址");return}const t=a.outputFilename||`${new Date().toISOString().slice(0,10).replace(/-/g,"")}.txt`;if(e?.save_file_from_url_new){const l=await e.save_file_from_url_new(a.downloadUrl,t);l.success?i.success(`已保存:${l.path||t}`):l.error&&l.error!=="用户取消"&&i.error(l.error);return}}return H(()=>{B().catch(()=>{}),x().catch(()=>{})}),(a,e)=>(n(),o("div",Z,[z(K,{active:"convert"}),s("div",ee,[s("aside",se,[e[5]||(e[5]=s("div",{class:"section-title"},"选择文件",-1)),s("div",te,[e[1]||(e[1]=s("div",{class:"hint"},"选择需要转换的 Excel 文件或文件夹,将按所选模板生成 txt 文件。",-1)),s("div",{class:"btns"},[s("button",{type:"button",class:"opt-btn",onClick:$},"选择 Excel 文件"),s("button",{type:"button",class:"opt-btn",onClick:D},"选择文件夹")]),s("div",ae,[v.value.length?(n(),o(w,{key:0},[(n(!0),o(w,null,F(C.value,t=>(n(),o("span",{key:t},r(t),1))),128)),v.value.length>C.value.length?(n(),o("span",le," 还有 "+r(v.value.length-C.value.length)+" 个文件未展开显示 ",1)):_("",!0)],64)):(n(),o("span",ne,"暂未选择待转换文件"))])]),e[6]||(e[6]=s("div",{class:"section-title"},"模板选择",-1)),s("div",oe,[(n(!0),o(w,null,F(d.value,t=>(n(),o("label",{key:t.id,class:E(["radio-item",{active:c.value===t.id}])},[M(s("input",{"onUpdate:modelValue":e[0]||(e[0]=l=>c.value=l),type:"radio",value:t.id,name:"convert-template"},null,8,ie),[[j,c.value]]),s("div",re,[s("div",null,[s("span",ce,[q(r(t.templateName)+" ",1),t.isDefault?(n(),o("span",ue,"默认")):t.builtIn?(n(),o("span",de,"内置")):(n(),o("span",ve,"自定义"))]),s("div",pe,"输出文件:"+r(t.outputFilename),1)])])],2))),128)),d.value.length?_("",!0):(n(),o("div",_e,"暂无可用模板"))]),e[7]||(e[7]=s("div",{class:"section-title"},"转换说明",-1)),s("div",fe,[s("label",me,[e[3]||(e[3]=s("input",{type:"checkbox",checked:"",disabled:""},null,-1)),s("div",null,[e[2]||(e[2]=s("span",{class:"label"},"按当前模板输出",-1)),s("div",he,"当前模板:"+r(I.value?.templateName||"未加载模板"),1)])]),e[4]||(e[4]=s("label",{class:"radio-item active"},[s("input",{type:"checkbox",checked:"",disabled:""}),s("div",null,[s("span",{class:"label"},"SKU 使用雪花ID-序号"),s("div",{class:"desc"},"例如 1773816146796-1")])],-1))]),s("div",ye,[s("button",{type:"button",class:"btn-run",disabled:m.value,onClick:R},r(m.value?"转换中...":"开始转换"),9,ge),s("span",be,r(m.value?"正在生成并上传结果,请稍候…":"选择文件后即可执行格式转换,结果会显示在右侧供下载"),1)])]),s("section",we,[e[12]||(e[12]=s("div",{class:"panel-header"},"转换结果",-1)),s("div",ke,[s("div",Ce,[s("div",xe,[e[8]||(e[8]=s("span",{class:"summary-label"},"已处理文件",-1)),s("strong",null,r(h.value.total),1)]),s("div",Fe,[e[9]||(e[9]=s("span",{class:"summary-label"},"成功结果",-1)),s("strong",null,r(h.value.successCount),1)]),s("div",Pe,[e[10]||(e[10]=s("span",{class:"summary-label"},"失败文件",-1)),s("strong",null,r(h.value.failedCount),1)])]),s("div",Ee,[e[11]||(e[11]=s("div",{class:"result-list-header"},[s("span",null,"生成的结果压缩包列表")],-1)),b.value.length===0?(n(),o("div",Te," 暂无转换结果,完成格式转换后会在这里展示结果压缩包 ")):(n(),o("ul",Ie,[(n(!0),o(w,null,F(b.value,t=>(n(),o("li",{key:`${t.resultId||t.outputFilename||t.sourceFilename}`,class:"task-item"},[s("div",Be,[s("span",{class:"id",title:t.sourceFilename},r(t.sourceFilename||"-"),9,Ne),t.outputFilename?(n(),o("div",$e,"压缩包文件:"+r(t.outputFilename),1)):_("",!0),t.error?(n(),o("div",De,"错误信息:"+r(t.error),1)):_("",!0)]),s("div",Re,[s("span",{class:E(["status",t.success?"success":"failed"])},r(t.success?"已完成":"失败"),3),t.success&&t.downloadUrl?(n(),o("button",{key:0,type:"button",class:"download",onClick:l=>U(t)}," 下载压缩包 ",8,Se)):_("",!0),t.resultId?(n(),o("button",{key:1,type:"button",class:"btn-delete",onClick:l=>S(t.resultId)}," 删除 ",8,Ue)):_("",!0)])]))),128))]))])])])])]))}}),Ae=Q(Ve,[["__scopeId","data-v-7c280b44"]]);W(Ae).use(X).mount("#app"); +import{d as A,o as H,a as n,c as o,b as z,B as K,e as s,F as w,r as F,t as r,f as _,n as E,w as M,s as j,u as q,g as u,h as T,x as J,i as k,E as i,y as L,z as O,A as G,_ as Q,p as W,q as X}from"./pywebview-C_Si_gmj.js";import{e as Y}from"./brand-CyZr-IbT.js";const Z={class:"page-shell module-page"},ee={class:"main-content"},se={class:"left-panel"},te={class:"upload-zone"},ae={class:"selected-files clean-placeholder"},le={key:0,class:"more-line"},ne={key:1},oe={class:"option-group"},ie=["value"],re={class:"template-item-content"},ce={class:"label"},ue={key:0,class:"template-tag"},de={key:1,class:"template-tag muted"},ve={key:2,class:"template-tag custom"},pe={class:"desc"},_e={key:0,class:"empty-option"},fe={class:"option-group"},me={class:"radio-item active"},he={class:"desc"},ye={class:"run-row"},ge=["disabled"],be={class:"loading-msg"},we={class:"right-panel"},ke={class:"task-list-wrap"},Ce={class:"clean-result-summary"},xe={class:"summary-card"},Fe={class:"summary-card"},Pe={class:"summary-card"},Ee={class:"result-list-wrap"},Te={key:0,class:"empty-tasks"},Ie={key:1,class:"task-list clean-result-list"},Be={class:"left"},Ne=["title"],$e={key:0,class:"files"},De={key:1,class:"files"},Re={class:"task-right"},Se=["onClick"],Ue=["onClick"],Ve=A({__name:"BrandConvertTab",setup(He){const v=u([]),g=u(""),f=u([]),m=u(!1),b=u([]),h=u({total:0,successCount:0,failedCount:0,items:[]}),d=u([]),c=u(""),C=T(()=>v.value.slice(0,8)),I=T(()=>d.value.find(a=>a.id===c.value)||null);async function P(a){const e=k();if(!e?.upload_file_to_java)throw new Error("当前桌面端未提供文件上传桥接能力");const t=[];for(const l of a){const p=typeof l=="string"?l:l.absolutePath,V=typeof l=="string"?void 0:l.relativePath,y=await e.upload_file_to_java(p,V);if(!y?.success||!y.data)throw new Error(y?.error||y?.message||`上传失败:${p}`);t.push(y.data)}return t}async function B(a){const e=await J();d.value=e||[];const t=d.value.find(p=>p.id===c.value);if(t){c.value=t.id;return}const l=d.value.find(p=>p.isDefault);if(l){c.value=l.id;return}c.value=d.value[0]?.id||""}async function N(a,e){v.value=a,g.value="",f.value=await P(a),i.success(e)}async function $(){const a=k();if(!a?.select_brand_xlsx_files){i.warning("当前环境不支持文件选择,请在本机客户端中打开");return}try{const e=await a.select_brand_xlsx_files();if(!e?.length)return;await N(e,`已选择 ${e.length} 个待转换文件`)}catch(e){i.error(e instanceof Error?e.message:"选择失败")}}async function D(){const a=k();if(!a?.select_brand_folder){i.warning("当前环境不支持文件夹选择,请在本机客户端中打开");return}try{const e=await a.select_brand_folder();if(!e)return;g.value=e.split(/[/\\]/).filter(Boolean).pop()||"";const t=await Y(e);if(t.success&&t.items?.length){v.value=t.items.map(l=>l.relativePath),f.value=await P(t.items),i.success(`已选择文件夹内 ${t.items.length} 个 xlsx 文件`);return}i.warning(t.error||"该文件夹下没有 xlsx 文件")}catch(e){i.error(e instanceof Error?e.message:"选择失败")}}async function R(){if(!f.value.length){i.warning("请先选择待转换 Excel 文件或文件夹");return}if(!c.value){i.warning("请先选择模板");return}try{m.value=!0;const a=await L({files:f.value.map(e=>({fileKey:e.fileKey,originalFilename:e.originalFilename,relativePath:e.relativePath})),templateId:c.value,archiveName:f.value.some(e=>!!e.relativePath)&&g.value?g.value:void 0});h.value=a,b.value=a.items||[],await x(),i.success("格式转换完成")}catch(a){i.error(a instanceof Error?a.message:"格式转换失败")}finally{m.value=!1}}async function x(){try{const a=await O();b.value=a.items||[],h.value={total:a.items?.length||0,successCount:a.items?.filter(e=>e.success).length||0,failedCount:a.items?.filter(e=>!e.success).length||0,items:a.items||[]}}catch{}}async function S(a){try{await G(a),await x(),i.success("已删除")}catch(e){i.error(e instanceof Error?e.message:"删除失败")}}async function U(a){const e=k();if(!a.downloadUrl){i.warning("当前结果没有下载地址");return}const t=a.outputFilename||`${new Date().toISOString().slice(0,10).replace(/-/g,"")}.txt`;if(e?.save_file_from_url_new){const l=await e.save_file_from_url_new(a.downloadUrl,t);l.success?i.success(`已保存:${l.path||t}`):l.error&&l.error!=="用户取消"&&i.error(l.error);return}}return H(()=>{B().catch(()=>{}),x().catch(()=>{})}),(a,e)=>(n(),o("div",Z,[z(K,{active:"convert"}),s("div",ee,[s("aside",se,[e[5]||(e[5]=s("div",{class:"section-title"},"选择文件",-1)),s("div",te,[e[1]||(e[1]=s("div",{class:"hint"},"选择需要转换的 Excel 文件或文件夹,将按所选模板生成 txt 文件。",-1)),s("div",{class:"btns"},[s("button",{type:"button",class:"opt-btn",onClick:$},"选择 Excel 文件"),s("button",{type:"button",class:"opt-btn",onClick:D},"选择文件夹")]),s("div",ae,[v.value.length?(n(),o(w,{key:0},[(n(!0),o(w,null,F(C.value,t=>(n(),o("span",{key:t},r(t),1))),128)),v.value.length>C.value.length?(n(),o("span",le," 还有 "+r(v.value.length-C.value.length)+" 个文件未展开显示 ",1)):_("",!0)],64)):(n(),o("span",ne,"暂未选择待转换文件"))])]),e[6]||(e[6]=s("div",{class:"section-title"},"模板选择",-1)),s("div",oe,[(n(!0),o(w,null,F(d.value,t=>(n(),o("label",{key:t.id,class:E(["radio-item",{active:c.value===t.id}])},[M(s("input",{"onUpdate:modelValue":e[0]||(e[0]=l=>c.value=l),type:"radio",value:t.id,name:"convert-template"},null,8,ie),[[j,c.value]]),s("div",re,[s("div",null,[s("span",ce,[q(r(t.templateName)+" ",1),t.isDefault?(n(),o("span",ue,"默认")):t.builtIn?(n(),o("span",de,"内置")):(n(),o("span",ve,"自定义"))]),s("div",pe,"输出文件:"+r(t.outputFilename),1)])])],2))),128)),d.value.length?_("",!0):(n(),o("div",_e,"暂无可用模板"))]),e[7]||(e[7]=s("div",{class:"section-title"},"转换说明",-1)),s("div",fe,[s("label",me,[e[3]||(e[3]=s("input",{type:"checkbox",checked:"",disabled:""},null,-1)),s("div",null,[e[2]||(e[2]=s("span",{class:"label"},"按当前模板输出",-1)),s("div",he,"当前模板:"+r(I.value?.templateName||"未加载模板"),1)])]),e[4]||(e[4]=s("label",{class:"radio-item active"},[s("input",{type:"checkbox",checked:"",disabled:""}),s("div",null,[s("span",{class:"label"},"SKU 使用雪花ID-序号"),s("div",{class:"desc"},"例如 1773816146796-1")])],-1))]),s("div",ye,[s("button",{type:"button",class:"btn-run",disabled:m.value,onClick:R},r(m.value?"转换中...":"开始转换"),9,ge),s("span",be,r(m.value?"正在生成并上传结果,请稍候…":"选择文件后即可执行格式转换,结果会显示在右侧供下载"),1)])]),s("section",we,[e[12]||(e[12]=s("div",{class:"panel-header"},"转换结果",-1)),s("div",ke,[s("div",Ce,[s("div",xe,[e[8]||(e[8]=s("span",{class:"summary-label"},"已处理文件",-1)),s("strong",null,r(h.value.total),1)]),s("div",Fe,[e[9]||(e[9]=s("span",{class:"summary-label"},"成功结果",-1)),s("strong",null,r(h.value.successCount),1)]),s("div",Pe,[e[10]||(e[10]=s("span",{class:"summary-label"},"失败文件",-1)),s("strong",null,r(h.value.failedCount),1)])]),s("div",Ee,[e[11]||(e[11]=s("div",{class:"result-list-header"},[s("span",null,"生成的结果压缩包列表")],-1)),b.value.length===0?(n(),o("div",Te," 暂无转换结果,完成格式转换后会在这里展示结果压缩包 ")):(n(),o("ul",Ie,[(n(!0),o(w,null,F(b.value,t=>(n(),o("li",{key:`${t.resultId||t.outputFilename||t.sourceFilename}`,class:"task-item"},[s("div",Be,[s("span",{class:"id",title:t.sourceFilename},r(t.sourceFilename||"-"),9,Ne),t.outputFilename?(n(),o("div",$e,"压缩包文件:"+r(t.outputFilename),1)):_("",!0),t.error?(n(),o("div",De,"错误信息:"+r(t.error),1)):_("",!0)]),s("div",Re,[s("span",{class:E(["status",t.success?"success":"failed"])},r(t.success?"已完成":"失败"),3),t.success&&t.downloadUrl?(n(),o("button",{key:0,type:"button",class:"download",onClick:l=>U(t)}," 下载压缩包 ",8,Se)):_("",!0),t.resultId?(n(),o("button",{key:1,type:"button",class:"btn-delete",onClick:l=>S(t.resultId)}," 删除 ",8,Ue)):_("",!0)])]))),128))]))])])])])]))}}),Ae=Q(Ve,[["__scopeId","data-v-7c280b44"]]);W(Ae).use(X).mount("#app"); diff --git a/app/assets/dedupe.js b/app/assets/dedupe.js index 3002d31..57728d1 100644 --- a/app/assets/dedupe.js +++ b/app/assets/dedupe.js @@ -1 +1 @@ -import{d as z,o as W,a as n,c as o,b as q,B as J,e as s,F as C,r as F,t as i,f as b,n as I,w as P,v as B,g as u,h as L,i as x,E as r,j as O,k as G,l as Q,m as X,_ as Y,p as Z,q as ee}from"./pywebview-Cq_E2BnJ.js";import{e as se}from"./brand-DZ5_PGnw.js";const te={class:"page-shell module-page"},le={class:"main-content"},ae={class:"left-panel"},ne={class:"upload-zone"},oe={class:"selected-files clean-placeholder"},re={key:0,class:"more-line"},ie={key:1},ue={class:"option-group column-option-group"},ce={class:"column-toolbar"},de={class:"column-count"},ve={key:0,class:"column-grid"},pe=["value"],fe={key:1,class:"empty-column-state"},_e={class:"option-group"},me={class:"run-row"},he=["disabled"],ge={class:"loading-msg"},ye={class:"right-panel"},be={class:"task-list-wrap"},we={class:"clean-result-summary"},ke={class:"summary-card"},Ce={class:"summary-card"},Ie={class:"summary-card"},xe={class:"result-list-wrap"},Ee={key:0,class:"empty-tasks"},De={key:1,class:"task-list clean-result-list"},Fe={class:"left"},Pe=["title"],Be={key:0,class:"files"},Se={key:1,class:"files"},Ue={class:"task-right"},Ne=["onClick"],$e=["onClick"],Ae=z({__name:"BrandDedupeTab",setup(Ke){const p=u([]),c=u([]),f=u([]),w=u(""),d=u([]),S=u(!1),_=u(!0),m=u(!0),h=u(!1),k=u([]),g=u({total:0,successCount:0,failedCount:0,items:[]}),E=L(()=>f.value.slice(0,8));function $(){c.value=[...p.value]}function A(){c.value=[]}async function U(l){const e=x();if(!e?.upload_file_to_java)throw new Error("当前桌面端未提供文件上传桥接能力");const t=[];for(const a of l){const v=typeof a=="string"?a:a.absolutePath,j=typeof a=="string"?void 0:a.relativePath,y=await e.upload_file_to_java(v,j);if(!y?.success||!y.data)throw new Error(y?.error||y?.message||`上传失败:${v}`);t.push(y.data)}return t}async function N(l){const e=await X(l);if(!e.headers?.length){r.error("读取 Excel 表头失败"),p.value=[],c.value=[];return}const t=["id","ASIN","国家","价格","卖家名称"],a=[...t.filter(v=>e.headers?.includes(v)),...(e.headers||[]).filter(v=>!t.includes(v))];p.value=a,c.value=t.filter(v=>a.includes(v))}async function H(l,e){f.value=l,w.value="",d.value=await U(l),d.value.length>0&&await N(d.value[0].fileKey),r.success(e)}async function K(){const l=x();if(!l?.select_brand_xlsx_files){r.warning("当前环境不支持文件选择,请在本机客户端中打开");return}try{const e=await l.select_brand_xlsx_files();if(!e?.length)return;await H(e,`已选择 ${e.length} 个待清洗文件`)}catch(e){r.error(e instanceof Error?e.message:"选择失败")}}async function R(){const l=x();if(!l?.select_brand_folder){r.warning("当前环境不支持文件夹选择,请在本机客户端中打开");return}try{const e=await l.select_brand_folder();if(!e)return;w.value=e.split(/[/\\]/).filter(Boolean).pop()||"";const t=await se(e);if(t.success&&t.items?.length){f.value=t.items.map(a=>a.relativePath),d.value=await U(t.items),d.value.length>0&&await N(d.value[0].fileKey),r.success(`已选择文件夹内 ${t.items.length} 个 xlsx 文件`);return}r.warning(t.error||"该文件夹下没有 xlsx 文件")}catch(e){r.error(e instanceof Error?e.message:"选择失败")}}async function V(){if(!d.value.length){r.warning("请先选择待清洗 Excel 文件或文件夹");return}if(!c.value.length){r.warning("请至少选择一列保留列");return}if(!S.value&&!_.value&&!m.value){r.warning("请至少选择一种 ID 保留规则");return}try{h.value=!0;const l=await O({files:d.value.map(e=>({fileKey:e.fileKey,originalFilename:e.originalFilename,relativePath:e.relativePath})),selectedColumns:c.value,keepIntegerIds:S.value,keepUnderscoreIds:_.value,keepIntegerMainIdsWhenNoSubIds:m.value,archiveName:d.value.some(e=>!!e.relativePath)&&w.value?w.value:void 0});g.value=l,k.value=l.items||[],await D(),r.success("数据去重完成")}catch(l){r.error(l instanceof Error?l.message:"去重失败")}finally{h.value=!1}}async function D(){try{const l=await G();k.value=l.items||[],g.value={total:l.items?.length||0,successCount:l.items?.filter(e=>e.success).length||0,failedCount:l.items?.filter(e=>!e.success).length||0,items:l.items||[]}}catch{}}async function M(l){try{await Q(l),await D(),r.success("已删除")}catch(e){r.error(e instanceof Error?e.message:"删除失败")}}async function T(l){const e=x();if(!l.downloadUrl){r.warning("当前结果没有下载地址");return}const t=l.outputFilename||`${new Date().toISOString().slice(0,10).replace(/-/g,"")}_cleaned.xlsx`;if(e?.save_file_from_url_new){const a=await e.save_file_from_url_new(l.downloadUrl,t);a.success?r.success(`已保存:${a.path||t}`):a.error&&a.error!=="用户取消"&&r.error(a.error);return}}return W(()=>{D().catch(()=>{})}),(l,e)=>(n(),o("div",te,[q(J,{active:"dedupe"}),s("div",le,[s("aside",ae,[e[7]||(e[7]=s("div",{class:"section-title"},"选择文件",-1)),s("div",ne,[e[3]||(e[3]=s("div",{class:"hint"},"选择需要清洗的 Excel 文件或文件夹,后续将按所选规则输出处理结果。",-1)),s("div",{class:"btns"},[s("button",{type:"button",class:"opt-btn",onClick:K},"选择 Excel 文件"),s("button",{type:"button",class:"opt-btn",onClick:R},"选择文件夹")]),s("div",oe,[f.value.length?(n(),o(C,{key:0},[(n(!0),o(C,null,F(E.value,t=>(n(),o("span",{key:t},i(t),1))),128)),f.value.length>E.value.length?(n(),o("span",re," 还有 "+i(f.value.length-E.value.length)+" 个文件未展开显示 ",1)):b("",!0)],64)):(n(),o("span",ie,"暂未选择待清洗文件"))])]),e[8]||(e[8]=s("div",{class:"section-title"},"保留列设置",-1)),s("div",ue,[s("div",ce,[s("span",de,"已选择 "+i(c.value.length)+" / "+i(p.value.length)+" 列",1),s("div",{class:"column-actions"},[s("button",{type:"button",class:"opt-btn small",onClick:$},"全选"),s("button",{type:"button",class:"opt-btn small",onClick:A},"清空")])]),p.value.length?(n(),o("div",ve,[(n(!0),o(C,null,F(p.value,t=>(n(),o("label",{key:t,class:I(["column-item",{active:c.value.includes(t)}])},[P(s("input",{"onUpdate:modelValue":e[0]||(e[0]=a=>c.value=a),type:"checkbox",value:t},null,8,pe),[[B,c.value]]),s("span",null,i(t),1)],2))),128))])):(n(),o("div",fe,"请选择 Excel 文件后读取表头并填充可保留列")),e[4]||(e[4]=s("div",{class:"desc"},"读取首个 Excel 文件的第一行作为表头,用于动态选择需要保留的列。",-1))]),e[9]||(e[9]=s("div",{class:"section-title"},"ID 保留规则",-1)),s("div",_e,[s("label",{class:I(["radio-item",{active:_.value}])},[P(s("input",{"onUpdate:modelValue":e[1]||(e[1]=t=>_.value=t),type:"checkbox"},null,512),[[B,_.value]]),e[5]||(e[5]=s("div",null,[s("span",{class:"label"},"保留子链接"),s("div",{class:"desc"},"例如 1_1、2_3 这种带下划线的子项 ID")],-1))],2),s("label",{class:I(["radio-item",{active:m.value}])},[P(s("input",{"onUpdate:modelValue":e[2]||(e[2]=t=>m.value=t),type:"checkbox"},null,512),[[B,m.value]]),e[6]||(e[6]=s("div",null,[s("span",{class:"label"},"仅剩主链接无子链接"),s("div",{class:"desc"},"当某个主链接只有 13 这类主 ID、没有 13_1 这类对应子 ID 时,自动保留该主 ID")],-1))],2)]),s("div",me,[s("button",{type:"button",class:"btn-run",disabled:h.value,onClick:V},i(h.value?"清洗中...":"开始清洗"),9,he),s("span",ge,i(h.value?"正在处理文件并上传结果,请稍候…":"选择文件、列和 ID 规则后即可执行清洗,结果会显示在右侧供下载"),1)])]),s("section",ye,[e[14]||(e[14]=s("div",{class:"panel-header"},"去重结果",-1)),s("div",be,[s("div",we,[s("div",ke,[e[10]||(e[10]=s("span",{class:"summary-label"},"已处理文件",-1)),s("strong",null,i(g.value.total),1)]),s("div",Ce,[e[11]||(e[11]=s("span",{class:"summary-label"},"成功结果",-1)),s("strong",null,i(g.value.successCount),1)]),s("div",Ie,[e[12]||(e[12]=s("span",{class:"summary-label"},"失败文件",-1)),s("strong",null,i(g.value.failedCount),1)])]),s("div",xe,[e[13]||(e[13]=s("div",{class:"result-list-header"},[s("span",null,"处理后的 Excel 列表")],-1)),k.value.length===0?(n(),o("div",Ee," 暂无去重结果,完成数据去重后会在这里展示输出文件 ")):(n(),o("ul",De,[(n(!0),o(C,null,F(k.value,t=>(n(),o("li",{key:`${t.resultId||t.outputFilename||t.sourceFilename}`,class:"task-item"},[s("div",Fe,[s("span",{class:"id",title:t.sourceFilename},i(t.sourceFilename||"-"),9,Pe),t.outputFilename?(n(),o("div",Be,"输出文件:"+i(t.outputFilename),1)):b("",!0),t.error?(n(),o("div",Se,"错误信息:"+i(t.error),1)):b("",!0)]),s("div",Ue,[s("span",{class:I(["status",t.success?"success":"failed"])},i(t.success?"已完成":"失败"),3),t.success&&t.downloadUrl?(n(),o("button",{key:0,type:"button",class:"download",onClick:a=>T(t)}," 下载文件 ",8,Ne)):b("",!0),t.resultId?(n(),o("button",{key:1,type:"button",class:"btn-delete",onClick:a=>M(t.resultId)}," 删除 ",8,$e)):b("",!0)])]))),128))]))])])])])]))}}),He=Y(Ae,[["__scopeId","data-v-bdaefe1c"]]);Z(He).use(ee).mount("#app"); +import{d as z,o as W,a as n,c as o,b as q,B as J,e as s,F as C,r as F,t as i,f as b,n as I,w as P,v as B,g as u,h as L,i as x,E as r,j as O,k as G,l as Q,m as X,_ as Y,p as Z,q as ee}from"./pywebview-C_Si_gmj.js";import{e as se}from"./brand-CyZr-IbT.js";const te={class:"page-shell module-page"},le={class:"main-content"},ae={class:"left-panel"},ne={class:"upload-zone"},oe={class:"selected-files clean-placeholder"},re={key:0,class:"more-line"},ie={key:1},ue={class:"option-group column-option-group"},ce={class:"column-toolbar"},de={class:"column-count"},ve={key:0,class:"column-grid"},pe=["value"],fe={key:1,class:"empty-column-state"},_e={class:"option-group"},me={class:"run-row"},he=["disabled"],ge={class:"loading-msg"},ye={class:"right-panel"},be={class:"task-list-wrap"},we={class:"clean-result-summary"},ke={class:"summary-card"},Ce={class:"summary-card"},Ie={class:"summary-card"},xe={class:"result-list-wrap"},Ee={key:0,class:"empty-tasks"},De={key:1,class:"task-list clean-result-list"},Fe={class:"left"},Pe=["title"],Be={key:0,class:"files"},Se={key:1,class:"files"},Ue={class:"task-right"},Ne=["onClick"],$e=["onClick"],Ae=z({__name:"BrandDedupeTab",setup(Ke){const p=u([]),c=u([]),f=u([]),w=u(""),d=u([]),S=u(!1),_=u(!0),m=u(!0),h=u(!1),k=u([]),g=u({total:0,successCount:0,failedCount:0,items:[]}),E=L(()=>f.value.slice(0,8));function $(){c.value=[...p.value]}function A(){c.value=[]}async function U(l){const e=x();if(!e?.upload_file_to_java)throw new Error("当前桌面端未提供文件上传桥接能力");const t=[];for(const a of l){const v=typeof a=="string"?a:a.absolutePath,j=typeof a=="string"?void 0:a.relativePath,y=await e.upload_file_to_java(v,j);if(!y?.success||!y.data)throw new Error(y?.error||y?.message||`上传失败:${v}`);t.push(y.data)}return t}async function N(l){const e=await X(l);if(!e.headers?.length){r.error("读取 Excel 表头失败"),p.value=[],c.value=[];return}const t=["id","ASIN","国家","价格","卖家名称"],a=[...t.filter(v=>e.headers?.includes(v)),...(e.headers||[]).filter(v=>!t.includes(v))];p.value=a,c.value=t.filter(v=>a.includes(v))}async function H(l,e){f.value=l,w.value="",d.value=await U(l),d.value.length>0&&await N(d.value[0].fileKey),r.success(e)}async function K(){const l=x();if(!l?.select_brand_xlsx_files){r.warning("当前环境不支持文件选择,请在本机客户端中打开");return}try{const e=await l.select_brand_xlsx_files();if(!e?.length)return;await H(e,`已选择 ${e.length} 个待清洗文件`)}catch(e){r.error(e instanceof Error?e.message:"选择失败")}}async function R(){const l=x();if(!l?.select_brand_folder){r.warning("当前环境不支持文件夹选择,请在本机客户端中打开");return}try{const e=await l.select_brand_folder();if(!e)return;w.value=e.split(/[/\\]/).filter(Boolean).pop()||"";const t=await se(e);if(t.success&&t.items?.length){f.value=t.items.map(a=>a.relativePath),d.value=await U(t.items),d.value.length>0&&await N(d.value[0].fileKey),r.success(`已选择文件夹内 ${t.items.length} 个 xlsx 文件`);return}r.warning(t.error||"该文件夹下没有 xlsx 文件")}catch(e){r.error(e instanceof Error?e.message:"选择失败")}}async function V(){if(!d.value.length){r.warning("请先选择待清洗 Excel 文件或文件夹");return}if(!c.value.length){r.warning("请至少选择一列保留列");return}if(!S.value&&!_.value&&!m.value){r.warning("请至少选择一种 ID 保留规则");return}try{h.value=!0;const l=await O({files:d.value.map(e=>({fileKey:e.fileKey,originalFilename:e.originalFilename,relativePath:e.relativePath})),selectedColumns:c.value,keepIntegerIds:S.value,keepUnderscoreIds:_.value,keepIntegerMainIdsWhenNoSubIds:m.value,archiveName:d.value.some(e=>!!e.relativePath)&&w.value?w.value:void 0});g.value=l,k.value=l.items||[],await D(),r.success("数据去重完成")}catch(l){r.error(l instanceof Error?l.message:"去重失败")}finally{h.value=!1}}async function D(){try{const l=await G();k.value=l.items||[],g.value={total:l.items?.length||0,successCount:l.items?.filter(e=>e.success).length||0,failedCount:l.items?.filter(e=>!e.success).length||0,items:l.items||[]}}catch{}}async function M(l){try{await Q(l),await D(),r.success("已删除")}catch(e){r.error(e instanceof Error?e.message:"删除失败")}}async function T(l){const e=x();if(!l.downloadUrl){r.warning("当前结果没有下载地址");return}const t=l.outputFilename||`${new Date().toISOString().slice(0,10).replace(/-/g,"")}_cleaned.xlsx`;if(e?.save_file_from_url_new){const a=await e.save_file_from_url_new(l.downloadUrl,t);a.success?r.success(`已保存:${a.path||t}`):a.error&&a.error!=="用户取消"&&r.error(a.error);return}}return W(()=>{D().catch(()=>{})}),(l,e)=>(n(),o("div",te,[q(J,{active:"dedupe"}),s("div",le,[s("aside",ae,[e[7]||(e[7]=s("div",{class:"section-title"},"选择文件",-1)),s("div",ne,[e[3]||(e[3]=s("div",{class:"hint"},"选择需要清洗的 Excel 文件或文件夹,后续将按所选规则输出处理结果。",-1)),s("div",{class:"btns"},[s("button",{type:"button",class:"opt-btn",onClick:K},"选择 Excel 文件"),s("button",{type:"button",class:"opt-btn",onClick:R},"选择文件夹")]),s("div",oe,[f.value.length?(n(),o(C,{key:0},[(n(!0),o(C,null,F(E.value,t=>(n(),o("span",{key:t},i(t),1))),128)),f.value.length>E.value.length?(n(),o("span",re," 还有 "+i(f.value.length-E.value.length)+" 个文件未展开显示 ",1)):b("",!0)],64)):(n(),o("span",ie,"暂未选择待清洗文件"))])]),e[8]||(e[8]=s("div",{class:"section-title"},"保留列设置",-1)),s("div",ue,[s("div",ce,[s("span",de,"已选择 "+i(c.value.length)+" / "+i(p.value.length)+" 列",1),s("div",{class:"column-actions"},[s("button",{type:"button",class:"opt-btn small",onClick:$},"全选"),s("button",{type:"button",class:"opt-btn small",onClick:A},"清空")])]),p.value.length?(n(),o("div",ve,[(n(!0),o(C,null,F(p.value,t=>(n(),o("label",{key:t,class:I(["column-item",{active:c.value.includes(t)}])},[P(s("input",{"onUpdate:modelValue":e[0]||(e[0]=a=>c.value=a),type:"checkbox",value:t},null,8,pe),[[B,c.value]]),s("span",null,i(t),1)],2))),128))])):(n(),o("div",fe,"请选择 Excel 文件后读取表头并填充可保留列")),e[4]||(e[4]=s("div",{class:"desc"},"读取首个 Excel 文件的第一行作为表头,用于动态选择需要保留的列。",-1))]),e[9]||(e[9]=s("div",{class:"section-title"},"ID 保留规则",-1)),s("div",_e,[s("label",{class:I(["radio-item",{active:_.value}])},[P(s("input",{"onUpdate:modelValue":e[1]||(e[1]=t=>_.value=t),type:"checkbox"},null,512),[[B,_.value]]),e[5]||(e[5]=s("div",null,[s("span",{class:"label"},"保留子链接"),s("div",{class:"desc"},"例如 1_1、2_3 这种带下划线的子项 ID")],-1))],2),s("label",{class:I(["radio-item",{active:m.value}])},[P(s("input",{"onUpdate:modelValue":e[2]||(e[2]=t=>m.value=t),type:"checkbox"},null,512),[[B,m.value]]),e[6]||(e[6]=s("div",null,[s("span",{class:"label"},"仅剩主链接无子链接"),s("div",{class:"desc"},"当某个主链接只有 13 这类主 ID、没有 13_1 这类对应子 ID 时,自动保留该主 ID")],-1))],2)]),s("div",me,[s("button",{type:"button",class:"btn-run",disabled:h.value,onClick:V},i(h.value?"清洗中...":"开始清洗"),9,he),s("span",ge,i(h.value?"正在处理文件并上传结果,请稍候…":"选择文件、列和 ID 规则后即可执行清洗,结果会显示在右侧供下载"),1)])]),s("section",ye,[e[14]||(e[14]=s("div",{class:"panel-header"},"去重结果",-1)),s("div",be,[s("div",we,[s("div",ke,[e[10]||(e[10]=s("span",{class:"summary-label"},"已处理文件",-1)),s("strong",null,i(g.value.total),1)]),s("div",Ce,[e[11]||(e[11]=s("span",{class:"summary-label"},"成功结果",-1)),s("strong",null,i(g.value.successCount),1)]),s("div",Ie,[e[12]||(e[12]=s("span",{class:"summary-label"},"失败文件",-1)),s("strong",null,i(g.value.failedCount),1)])]),s("div",xe,[e[13]||(e[13]=s("div",{class:"result-list-header"},[s("span",null,"处理后的 Excel 列表")],-1)),k.value.length===0?(n(),o("div",Ee," 暂无去重结果,完成数据去重后会在这里展示输出文件 ")):(n(),o("ul",De,[(n(!0),o(C,null,F(k.value,t=>(n(),o("li",{key:`${t.resultId||t.outputFilename||t.sourceFilename}`,class:"task-item"},[s("div",Fe,[s("span",{class:"id",title:t.sourceFilename},i(t.sourceFilename||"-"),9,Pe),t.outputFilename?(n(),o("div",Be,"输出文件:"+i(t.outputFilename),1)):b("",!0),t.error?(n(),o("div",Se,"错误信息:"+i(t.error),1)):b("",!0)]),s("div",Ue,[s("span",{class:I(["status",t.success?"success":"failed"])},i(t.success?"已完成":"失败"),3),t.success&&t.downloadUrl?(n(),o("button",{key:0,type:"button",class:"download",onClick:a=>T(t)}," 下载文件 ",8,Ne)):b("",!0),t.resultId?(n(),o("button",{key:1,type:"button",class:"btn-delete",onClick:a=>M(t.resultId)}," 删除 ",8,$e)):b("",!0)])]))),128))]))])])])])]))}}),He=Y(Ae,[["__scopeId","data-v-bdaefe1c"]]);Z(He).use(ee).mount("#app"); diff --git a/app/assets/delete-brand.js b/app/assets/delete-brand.js index ccccdd1..e1a175e 100644 --- a/app/assets/delete-brand.js +++ b/app/assets/delete-brand.js @@ -1 +1 @@ -import{d as Oe,o as He,I as Je,a as u,c,b as Ge,B as je,e as r,F as K,r as re,t as f,f as p,J as Ve,K as Fe,n as Ne,g as m,h as W,L as ze,i as O,E as h,M as Qe,N as Xe,O as We,P as Ye,_ as Ze,p as es,q as ss}from"./pywebview-Cq_E2BnJ.js";import{e as ts}from"./brand-DZ5_PGnw.js";const as={class:"page-shell module-page"},ns={class:"main-content"},ls={class:"left-panel"},rs={class:"upload-zone"},os={class:"selected-files clean-placeholder split-selected-files"},is={key:0,class:"more-line"},us={key:1},cs={class:"run-row"},ds=["disabled"],fs={class:"loading-msg"},vs={key:0,class:"queue-debug-card"},ps={key:0,class:"queue-debug-line"},hs={key:1,class:"queue-debug-payload"},_s={class:"right-panel"},gs={class:"task-list-wrap"},ys={class:"clean-result-summary"},ks={class:"summary-card"},ms={class:"summary-card"},ws={class:"summary-card"},Is={class:"result-list-wrap"},Ss={key:0,class:"empty-tasks"},bs={key:0,class:"result-subsection"},Cs={class:"task-list clean-result-list"},Ts={class:"left split-result-main"},Fs=["title"],Ns={class:"files"},Es={class:"files"},Rs={key:0,class:"files"},xs={key:1,class:"files"},$s={key:2,class:"files"},Ps={key:3,class:"time"},As={key:4,class:"delete-brand-progress-block"},Ds={class:"delete-brand-progress-header"},Ms={class:"delete-brand-progress-bar"},Bs={class:"files delete-brand-progress"},Us={key:5,class:"files split-entry-list delete-brand-preview"},qs={key:6,class:"files"},Ls={class:"task-right split-result-actions"},Ks=["onClick"],Os=["onClick"],Hs=["onClick"],Js={key:1,class:"result-subsection"},Gs={class:"task-list clean-result-list"},js={class:"left split-result-main"},Vs=["title"],zs={class:"files"},Qs={class:"files"},Xs={key:0,class:"files"},Ws={key:1,class:"files"},Ys={key:2,class:"time"},Zs={key:3,class:"delete-brand-progress-block"},et={class:"delete-brand-progress-header"},st={class:"delete-brand-progress-bar"},tt={class:"files delete-brand-progress"},at={key:4,class:"files split-entry-list delete-brand-preview"},nt={key:5,class:"files"},lt={class:"task-right split-result-actions"},rt=["onClick"],ot=["onClick"],it=Oe({__name:"BrandDeleteBrandTab",setup(ct){function M(){return`delete-brand:current-tasks:${typeof window<"u"&&window.localStorage.getItem("uid")||"0"}`}const $=m([]),H=m([]),B=m(!1),oe=m([]),v=m([]),J=m([]),G=m({total:0,successCount:0,failedCount:0,items:[]}),T=m(""),C=m(""),g=m({}),S=m(null),Y=m(!1),P=m(!1),A=m(""),j=m(!1),Z=W(()=>$.value.slice(0,10)),F=W(()=>{const e=[];return v.value.forEach(s=>{e.push(...s.items)}),e});function Ee(){try{const e=typeof window<"u"?window.localStorage.getItem(M()):null;if(e){if(v.value=JSON.parse(e),v.value.length>0){const s=v.value[v.value.length-1];T.value=s.queuePushResult||"",C.value=s.queuePayloadText||""}}else v.value=[]}catch{v.value=[]}}function ie(e,s,t,a){if(!s?.length)return;const n=v.value.findIndex(d=>d.taskId===e),o=n>=0?v.value[n]:null,l=s.map(d=>{const _=o?.items.find(Ke=>D(Ke,d)),k=Array.isArray(d.countries)?d.countries:void 0,y=Array.isArray(_?.countries)?_?.countries:void 0,w=k&&k.length>0?k:y&&y.length>0?y:k??y,I=Array.isArray(d.previewRows)?d.previewRows:void 0,b=Array.isArray(_?.previewRows)?_?.previewRows:void 0,le=I&&I.length>0?I:b&&b.length>0?b:I??b;return{...d,countries:w,previewRows:le,_pushed:_?._pushed??d._pushed??!1,_completed:_?._completed??d._completed??!1}}),i={taskId:e,items:l,createdAt:o?.createdAt||Date.now(),queuePushResult:t??o?.queuePushResult,queuePayloadText:a??o?.queuePayloadText};n>=0?v.value[n]=i:v.value.push(i),typeof window<"u"&&window.localStorage.setItem(M(),JSON.stringify(v.value))}function ue(e){const s=v.value.length;v.value=v.value.filter(t=>t.taskId!==e),v.value.length!==s&&typeof window<"u"&&window.localStorage.setItem(M(),JSON.stringify(v.value))}const V=W(()=>J.value.filter(e=>!F.value.some(s=>D(s,e)))),Re=W(()=>F.value.length>0||V.value.length>0);function ce(){typeof window<"u"&&window.localStorage.setItem(M(),JSON.stringify(v.value))}function N(e){return e?(v.value.find(t=>t.taskId===e)?.items.length||0)>1:!1}function ee(e){return e&&v.value.find(s=>s.taskId===e)||null}function D(e,s){return!e||!s?!1:e.resultId!=null&&s.resultId!=null?e.resultId===s.resultId:e.fileKey&&s.fileKey?e.fileKey===s.fileKey:e.sourceFilename===s.sourceFilename}function E(e,s){const t=ee(e);return t&&t.items.find(a=>D(a,s))||null}function de(e){if(!e)return!1;const s=ee(e);if(!s)return!1;let t=!1;const a=g.value[e]?.line_progress?.info,n=N(e);if(a?.file_name&&a.current_line!=null&&a.total_lines!=null&&a.total_lines>0&&a.current_line>=a.total_lines){const o=s.items.find(l=>l.sourceFilename===a.file_name);o&&!o._completed&&(o._completed=!0,t=!0)}if(!n){const o=g.value[e]?.task?.status;if((o==="SUCCESS"||o==="FAILED")&&s.items.length===1){const l=s.items[0];l&&!l._completed&&(l._completed=!0,t=!0)}}return t&&ce(),t}function fe(e){return e&&g.value[e]?.task?.status||""}function z(e){if(e.error)return e.error;if(e.matchStatus&&e.matchStatus!=="MATCHED")return e.matchMessage||"";const s=e.taskId;return s&&g.value[s]?.task?.errorMessage||""}function ve(e){const s=e.taskId;if(!s)return!1;const t=g.value[s]?.line_progress,a=fe(s),n=N(s),o=x(e);if(n){const l=E(s,e),i=L(e)===A.value,d=!!(t?.has_progress&&t?.info?.file_name===e.sourceFilename);return!!(i||d||l?._pushed||(a==="RUNNING"||a==="PENDING"||!a)&&l?._completed)}return o==="本文件解析完毕"||o==="已被全局判定为终态"?!1:t?.has_progress||t?.info?!0:o==="已入队"||o==="处理中"}function pe(e){return e.map(s=>s.matchStatus==="MATCHED"?{...s,_pushed:!1,_completed:!1}:{...s,matched:!1,openStoreUrl:void 0,error:s.error||void 0,_pushed:!1,_completed:!1})}function he(e){return e.matchStatus==="MATCHED"?`已匹配 ${e.shopId||""}`.trim():e.matchMessage?e.matchMessage:e.matchStatus==="CONFLICT"?"存在多个同名店铺,请人工确认":e.matchStatus==="PENDING"||e.matchStatus==="INDEX_STALE"?"店铺索引暂未就绪":e.matchStatus?e.matchStatus:e.matched?`已匹配 ${e.shopId||""}`.trim():"待匹配"}function xe(){oe.value=[...F.value,...V.value]}function R(){xe(),$e(oe.value)}function $e(e){G.value={total:e.length,successCount:e.filter(s=>s.success).length,failedCount:e.filter(s=>!s.success).length,items:e}}function se(e){const s=e.taskId;return s&&(g.value[s]?.fileProgress||[]).find(n=>n.fileKey&&e.fileKey?n.fileKey===e.fileKey:n.sourceFilename&&e.sourceFilename?n.sourceFilename===e.sourceFilename:!1)||null}function _e(e){const s=e.taskId;if(!s)return"";const t=g.value[s],a=t?.line_progress?.info,n=se(e);if(N(s)){const i=E(s,e),d=t?.task?.status,_=!!(a?.file_name&&a.file_name===e.sourceFilename);let k=i?._completed?"COMPLETED":(i?._pushed,"PENDING"),y=0,w=Math.max(0,e.totalRows??n?.totalRows??0);d==="SUCCESS"||d==="FAILED"&&i?._completed?(k="COMPLETED",y=w):_?(k="RUNNING",y=Math.max(0,Math.min(w||Number.MAX_SAFE_INTEGER,a?.current_line??0)),!w&&a?.total_lines&&a.total_lines>0&&(w=a.total_lines)):i?._completed?(k="COMPLETED",y=w):i?._pushed&&(k="PENDING",y=0);const I=[];if(I.push(`文件状态:${k}`),I.push(`文件进度:${y}/${w}`),a?.finished_files!=null){const b=a.file_total&&a.file_total>0?`/${a.file_total}`:"";I.push(`已完成文件:${a.finished_files}${b}`)}return a?.phase&&I.push(`阶段:${a.phase}`),I.join("|")}if(n){const i=[],d=n.processedRows??0,_=n.totalRows??0;if(n.status&&i.push(`文件状态:${n.status}`),i.push(`文件进度:${d}/${_}`),a?.finished_files!=null){const k=a.file_total&&a.file_total>0?`/${a.file_total}`:"";i.push(`已完成文件:${a.finished_files}${k}`)}return a?.phase&&i.push(`阶段:${a.phase}`),i.join("|")}if(!t?.line_progress?.has_progress||!a){const i=t?.task?.status;return i?`任务状态:${i}`:""}const l=[];if(a.phase&&l.push(`阶段:${a.phase}`),a.file_index&&a.file_total&&l.push(`文件:${a.file_index}/${a.file_total}`),a.file_name&&l.push(`当前文件:${a.file_name}`),a.current_line!=null&&a.total_lines!=null&&l.push(`${N(s)?"当前文件进度":"进度"}:${a.current_line}/${a.total_lines}`),a.current_country&&l.push(`国家:${a.current_country}`),a.current_asin&&l.push(`ASIN:${a.current_asin}`),a.finished_files!=null){const i=a.file_total&&a.file_total>0?`/${a.file_total}`:"";l.push(`已完成文件:${a.finished_files}${i}`)}return l.join("|")}function Q(e){const s=e.taskId;if(!s)return 0;const t=g.value[s]?.line_progress?.info;if(N(s)){const l=E(s,e);if(g.value[s]?.task?.status==="SUCCESS"||l?._completed)return 100;const d=Math.max(0,e.totalRows??se(e)?.totalRows??0);if(!!(t?.file_name&&t.file_name===e.sourceFilename)){if(t?.current_line!=null&&t?.total_lines&&t.total_lines>0)return Math.max(0,Math.min(100,Math.round(t.current_line/t.total_lines*100)));if(t?.current_line!=null&&d>0)return Math.max(0,Math.min(100,Math.round(t.current_line/d*100)))}return 0}const n=se(e);return n?Math.max(0,Math.min(100,n.percent??0)):t?t.current_line!=null&&t.total_lines&&t.total_lines>0?Math.max(0,Math.min(100,Math.round(t.current_line/t.total_lines*100))):t.finished_files!=null&&t.file_total&&t.file_total>0?Math.max(0,Math.min(100,Math.round(t.finished_files/t.file_total*100))):g.value[s]?.task?.status==="SUCCESS"?100:0:0}function ge(e){const s=g.value[e]?.task?.status;return s==="SUCCESS"||s==="FAILED"}function U(){const e=new Set;return v.value.forEach(s=>{s.items.some(t=>t._pushed)&&e.add(s.taskId)}),Array.from(e)}function x(e){const s=e.taskId;if(!s||!ee(s))return"";const a=E(s,e),n=fe(s),o=g.value[s]?.line_progress,l=N(s);if(!l&&(n==="SUCCESS"||n==="FAILED"||e.taskStatus==="SUCCESS"||e.taskStatus==="FAILED"))return"已被全局判定为终态";if(a?._completed)return"本文件解析完毕";if(n==="RUNNING"&&o?.has_progress&&o?.info?.file_name===e.sourceFilename)return"处理中";if(l&&!a?._pushed&&!a?._completed)return"未入队";if(l&&n==="SUCCESS")return"本文件解析完毕";if(!l){const i=g.value[s];if(i?.items&&i.items.find(_=>D(_,e))&&n==="SUCCESS")return"本文件解析完毕"}return a?._pushed?"已入队":"未入队"}function ye(e){if(e.downloadUrl)return!0;const s=e.taskId;return s?g.value[s]?.task?.status==="SUCCESS":!1}function Pe(e){const s=e?.task?.id;if(!s)return!1;const t=v.value.find(l=>l.taskId===s);if(!t)return!1;const a=e?.items||[];let n=!1;const o=t.items.map(l=>{const i=a.find(le=>D(le,l));if(!i)return l;const d=Array.isArray(i.countries)?i.countries:void 0,_=Array.isArray(l.countries)?l.countries:void 0,k=d&&d.length>0?d:_&&_.length>0?_:d??_,y=Array.isArray(i.previewRows)?i.previewRows:void 0,w=Array.isArray(l.previewRows)?l.previewRows:void 0,I=y&&y.length>0?y:w&&w.length>0?w:y??w,b={...l,...i,countries:k,previewRows:I,_pushed:l._pushed,_completed:l._completed};return JSON.stringify(b)!==JSON.stringify(l)&&(n=!0),b});for(const l of a)o.some(i=>D(i,l))||(o.push({...l,_pushed:!1,_completed:!1}),n=!0);return JSON.stringify(o)!==JSON.stringify(t.items)&&(t.items=o,n=!0),n&&ce(),n}async function te(e){const s=e||U();if(s.length)try{const t=await ze(s);let a=!1;for(const n of t.items||[]){const o=n?.task?.id;typeof o=="number"&&o>0&&(g.value[o]=n,Pe(n)&&(a=!0),de(o)&&(a=!0))}a&&R()}catch{}}function Ae(){return document.visibilityState==="visible"?4500:1e4}function ae(e=!1){if(S.value){if(!e)return;clearTimeout(S.value),S.value=null}const s=async()=>{if(S.value=null,Y.value){ae();return}const t=U();if(!t.length)return;Y.value=!0;let a=!1,n=!1;try{await te(t),await qe();for(const o of t)ge(o)&&(ue(o),a=!0,n=!0)}finally{Y.value=!1}n?await q():a&&R(),U().length>0&&ae()};e?s():S.value=window.setTimeout(s,Ae())}function ne(e=!1){S.value&&!e||ae(e)}function X(e){const s=e.taskId?g.value[e.taskId]?.task?.status:null;return s==="SUCCESS"?{className:"success",text:"已完成"}:s==="FAILED"?{className:"failed",text:"失败"}:s==="RUNNING"?{className:"running",text:"执行中"}:e.taskStatus==="SUCCESS"?{className:"success",text:"已完成"}:e.taskStatus==="FAILED"?{className:"failed",text:"失败"}:e.taskStatus==="RUNNING"?{className:"running",text:"执行中"}:e.success?{className:"success",text:"已完成"}:{className:"failed",text:"失败"}}function ke(){S.value&&(window.clearTimeout(S.value),S.value=null)}async function me(e){const s=O();if(!s?.upload_file_to_java)throw new Error("当前桌面端未提供文件上传桥接能力");const t=[];for(const a of e){const n=typeof a=="string"?a:a.absolutePath,o=typeof a=="string"?void 0:a.relativePath,l=await s.upload_file_to_java(n,o);if(!l?.success||!l.data)throw new Error(l?.error||l?.message||`上传失败:${n}`);t.push(l.data)}return t}async function De(){const e=O();if(!e?.select_brand_xlsx_files){h.warning("当前环境不支持文件选择,请在本机客户端中打开");return}try{const s=await e.select_brand_xlsx_files();if(!s?.length)return;$.value=s,H.value=await me(s),h.success(`已选择 ${s.length} 个删除品牌文件`)}catch(s){h.error(s instanceof Error?s.message:"选择失败")}}async function Me(){const e=O();if(!e?.select_brand_folder){h.warning("当前环境不支持文件夹选择,请在本机客户端中打开");return}try{const s=await e.select_brand_folder();if(!s)return;const t=await ts(s);if(!t.success||!t.items?.length){h.warning(t.error||"该文件夹下没有 xlsx 文件");return}$.value=t.items.map(a=>a.relativePath||a.absolutePath),H.value=await me(t.items),h.success(`已选择文件夹内 ${t.items.length} 个 xlsx 文件`)}catch(s){h.error(s instanceof Error?s.message:"选择失败")}}async function Be(){if(!H.value.length){h.warning("请先选择待处理文件");return}try{B.value=!0;const e=await Qe({files:H.value.map(n=>({fileKey:n.fileKey,originalFilename:n.originalFilename,relativePath:n.relativePath}))}),s=pe(e.items||[]),t=s.some(n=>n.matchStatus==="MATCHED"),a=s.some(n=>n.matchStatus&&n.matchStatus!=="MATCHED");T.value="解析完成,等待手动点击打开紫鸟进行单任务处理",C.value="",s.length&&s[0].taskId&&ie(s[0].taskId,s,T.value,C.value),R(),a?h.warning("部分文件尚未命中可用店铺索引,已保留状态信息,请等待后台刷新后重试。"):t?h.success("删除品牌解析完成,请手动推送"):h.warning("当前没有可推送的已匹配文件,请先等待店铺索引刷新。"),await q()}catch(e){h.error(e instanceof Error?e.message:"执行失败")}finally{B.value=!1}}async function we(e){const s=e.taskId;if(!s){h.warning("未找到任务ID");return}const t=O(),a=Xe(s),n=e.outputFilename||e.sourceFilename||`delete-brand_${s}.xlsx`;if(t?.save_file_from_url_new){const o=await t.save_file_from_url_new(a,n);o.success?h.success(`已保存:${o.path||n}`):o.error&&o.error!=="用户取消"&&h.error(o.error);return}}async function q(){try{const e=await We();J.value=pe(e.items||[]),R(),await te(),ne(!0)}catch(e){console.error("loadHistory error:",e)}}function Ie(e){v.value.forEach(t=>{t.items=t.items.filter(a=>a.resultId!==e)});const s=v.value.length;v.value=v.value.filter(t=>t.items.length>0),v.value.length!==s&&typeof window<"u"&&window.localStorage.setItem(M(),JSON.stringify(v.value)),J.value=J.value.filter(t=>t.resultId!==e),R(),U().length===0&&ke()}async function Se(e){try{await Ye(e),Ie(e),await q(),h.success("已删除")}catch(s){const t=s instanceof Error?s.message:"删除失败";if(typeof t=="string"&&t.includes("记录不存在")){Ie(e),h.success("已删除");return}h.error(t)}}function be(e){const s=e.slice(0,5).map(a=>`${a.country}:${a.asin}${a.status?`(${a.status})`:""}`);return e.length-s.length>0?`${s.join("、")} 等 ${e.length} 条`:s.join("、")}function dt(e,s){}function Ce(){for(const e of v.value)for(const s of e.items){const t=x(s);if(t==="已入队"||t==="处理中")return!0}return!1}function L(e){return e.resultId!=null?`result:${e.resultId}`:`task:${e.taskId||0}:${e.sourceFilename||""}`}function Ue(){return F.value.find(e=>!e.openStoreUrl||L(e)===A.value||E(e.taskId,e)?._completed?!1:x(e)==="未入队")}async function Te(e,s){if(s?.auto&&Ce())return e.taskId,e.sourceFilename,!1;const t=O(),a=e.taskId;if(!a)return!1;const n=v.value.find(d=>d.taskId===a);if(!n)return!1;if(!t?.enqueue_json)return h.error("当前环境未启用 pywebview enqueue_json(浏览器环境不会推送)"),!1;const o={type:"delete-brand-run",ts:Date.now(),data:{taskId:a,items:[e]}};C.value=JSON.stringify(o,null,2);const l=await t.enqueue_json(o);let i="";if(l?.success){i=`已推送文件 ${e.sourceFilename},当前队列长度:${l.queue_size??"-"}`;const d=E(a,e);d&&(d._pushed=!0,d._completed=!1),A.value=L(e),ie(a,n.items,i,C.value),ne(!0)}else i=`推送到 Python 队列失败:${l?.error||"未知错误"}`;return T.value=i,!!l?.success}async function qe(){if(!P.value||j.value){P.value,j.value;return}if(Ce())return;const e=A.value;if(e){const t=F.value.find(a=>L(a)===e);if(t){const a=t.taskId,n=N(a);n&&(de(a),x(t),void 0);const o=x(t);if(o==="已入队"||o==="处理中"){t.sourceFilename;return}if(n){const l=E(a,t);if(l&&!l._completed){t.sourceFilename,l._pushed,l._completed;return}}}}const s=Ue();if(!s){A.value="",P.value=!1;return}j.value=!0;try{s.taskId,s.sourceFilename,L(s),await Te(s,{auto:!0})||(s.taskId,s.sourceFilename,P.value=!1)}finally{j.value=!1}}async function Le(e){P.value=!0,await Te(e)||(P.value=!1,A.value="")}return He(()=>{Ee();const e=U();e.length>0?te(e).then(async()=>{let s=!1,t=!1;for(const a of e)ge(a)&&(ue(a),s=!0,t=!0);t?await q():s&&R(),ne(!0)}):R(),q().catch(()=>{})}),Je(()=>{ke()}),(e,s)=>(u(),c("div",as,[Ge(je,{active:"delete-brand"}),r("div",ns,[r("aside",ls,[s[2]||(s[2]=r("div",{class:"section-title"},"选择文件",-1)),r("div",rs,[s[0]||(s[0]=r("div",{class:"hint"},"上传删除品牌 Excel;无论是直接上传文件还是上传文件夹批量导入,都会按每个 Excel 文件名作为店铺名。",-1)),r("div",{class:"btns"},[r("button",{type:"button",class:"opt-btn",onClick:De},"选择 Excel 文件"),r("button",{type:"button",class:"opt-btn",onClick:Me},"选择文件夹")]),r("div",os,[$.value.length?(u(),c(K,{key:0},[(u(!0),c(K,null,re(Z.value,t=>(u(),c("span",{key:t},f(t),1))),128)),$.value.length>Z.value.length?(u(),c("span",is," 还有 "+f($.value.length-Z.value.length)+" 个文件未展开显示 ",1)):p("",!0)],64)):(u(),c("span",us,"暂未选择删除品牌文件"))])]),s[3]||(s[3]=Ve('
执行说明
',2)),r("div",cs,[r("button",{type:"button",class:"btn-run",disabled:B.value,onClick:Be},f(B.value?"解析中...":"开始解析"),9,ds),r("span",fs,f(B.value?"正在读取删除品牌数据,请稍候…":"解析后会在右侧展示按国家分组的去重结果"),1)]),T.value||C.value?(u(),c("div",vs,[s[1]||(s[1]=r("div",{class:"section-title queue-debug-title"},"Python 队列推送结果",-1)),T.value?(u(),c("div",ps,f(T.value),1)):p("",!0),C.value?(u(),c("pre",hs,f(C.value),1)):p("",!0)])):p("",!0)]),r("section",_s,[s[12]||(s[12]=r("div",{class:"panel-header"},"删除品牌结果",-1)),r("div",gs,[r("div",ys,[r("div",ks,[s[4]||(s[4]=r("span",{class:"summary-label"},"已处理文件",-1)),r("strong",null,f(G.value.total),1)]),r("div",ms,[s[5]||(s[5]=r("span",{class:"summary-label"},"成功结果",-1)),r("strong",null,f(G.value.successCount),1)]),r("div",ws,[s[6]||(s[6]=r("span",{class:"summary-label"},"失败文件",-1)),r("strong",null,f(G.value.failedCount),1)])]),r("div",Is,[s[11]||(s[11]=r("div",{class:"result-list-header"},[r("span",null,"删除品牌结果列表")],-1)),Re.value?(u(),c(K,{key:1},[F.value.length?(u(),c("div",bs,[s[8]||(s[8]=r("div",{class:"result-subsection-title"},"当前任务",-1)),r("ul",Cs,[(u(!0),c(K,null,re(F.value,t=>(u(),c("li",{key:`current-${t.taskId||t.resultId||t.sourceFilename}`,class:"task-item split-result-item"},[r("div",Ts,[r("span",{class:"id",title:t.sourceFilename},f(t.sourceFilename||"-"),9,Fs),r("div",Ns,"店铺名:"+f(t.shopName||"-"),1),r("div",Es,"匹配结果:"+f(he(t)),1),t.platform?(u(),c("div",Rs,"平台:"+f(t.platform),1)):p("",!0),x(t)?(u(),c("div",xs,"队列状态:"+f(x(t)),1)):p("",!0),t.countryCount!==void 0&&t.matched?(u(),c("div",$s,"国家数:"+f(t.countryCount),1)):p("",!0),t.totalRows!==void 0&&t.matched?(u(),c("div",Ps,"去重后 "+f(t.totalRows)+" 条 ",1)):p("",!0),ve(t)?(u(),c("div",As,[r("div",Ds,[s[7]||(s[7]=r("span",null,"任务进度",-1)),r("span",null,f(Q(t))+"%",1)]),r("div",Ms,[r("div",{class:"delete-brand-progress-bar-fill",style:Fe({width:`${Q(t)}%`})},null,4)]),r("div",Bs,f(_e(t)),1)])):p("",!0),t.previewRows?.length?(u(),c("div",Us,f(be(t.previewRows)),1)):p("",!0),z(t)?(u(),c("div",qs,"错误信息:"+f(z(t)),1)):p("",!0)]),r("div",Ls,[r("span",{class:Ne(["status",X(t).className])},f(X(t).text),3),t.openStoreUrl?(u(),c("button",{key:0,type:"button",class:"download",onClick:a=>Le(t)}," 打开紫鸟 ",8,Ks)):p("",!0),ye(t)?(u(),c("button",{key:1,type:"button",class:"download",onClick:a=>we(t)}," 下载 ",8,Os)):p("",!0),t.resultId?(u(),c("button",{key:2,type:"button",class:"btn-delete",onClick:a=>Se(t.resultId)}," 删除 ",8,Hs)):p("",!0)])]))),128))])])):p("",!0),V.value.length?(u(),c("div",Js,[s[10]||(s[10]=r("div",{class:"result-subsection-title"},"历史记录",-1)),r("ul",Gs,[(u(!0),c(K,null,re(V.value,t=>(u(),c("li",{key:`history-${t.taskId||t.resultId||t.sourceFilename}`,class:"task-item split-result-item"},[r("div",js,[r("span",{class:"id",title:t.sourceFilename},f(t.sourceFilename||"-"),9,Vs),r("div",zs,"店铺名:"+f(t.shopName||"-"),1),r("div",Qs,"匹配结果:"+f(he(t)),1),t.platform?(u(),c("div",Xs,"平台:"+f(t.platform),1)):p("",!0),t.countryCount!==void 0&&t.matched?(u(),c("div",Ws,"国家数:"+f(t.countryCount),1)):p("",!0),t.totalRows!==void 0&&t.matched?(u(),c("div",Ys,"去重后 "+f(t.totalRows)+" 条 ",1)):p("",!0),ve(t)?(u(),c("div",Zs,[r("div",et,[s[9]||(s[9]=r("span",null,"任务进度",-1)),r("span",null,f(Q(t))+"%",1)]),r("div",st,[r("div",{class:"delete-brand-progress-bar-fill",style:Fe({width:`${Q(t)}%`})},null,4)]),r("div",tt,f(_e(t)),1)])):p("",!0),t.previewRows?.length?(u(),c("div",at,f(be(t.previewRows)),1)):p("",!0),z(t)?(u(),c("div",nt,"错误信息:"+f(z(t)),1)):p("",!0)]),r("div",lt,[r("span",{class:Ne(["status",X(t).className])},f(X(t).text),3),ye(t)?(u(),c("button",{key:0,type:"button",class:"download",onClick:a=>we(t)}," 下载 ",8,rt)):p("",!0),t.resultId?(u(),c("button",{key:1,type:"button",class:"btn-delete",onClick:a=>Se(t.resultId)}," 删除 ",8,ot)):p("",!0)])]))),128))])])):p("",!0)],64)):(u(),c("div",Ss," 暂无删除品牌结果,完成解析后会在这里展示按国家分组的去重结果 "))])])])])]))}}),ut=Ze(it,[["__scopeId","data-v-468f2aa7"]]);es(ut).use(ss).mount("#app"); +import{d as Oe,o as He,I as Je,a as u,c,b as Ge,B as je,e as r,F as K,r as re,t as f,f as p,J as Ve,K as Fe,n as Ne,g as m,h as W,L as ze,i as O,E as h,M as Qe,N as Xe,O as We,P as Ye,_ as Ze,p as es,q as ss}from"./pywebview-C_Si_gmj.js";import{e as ts}from"./brand-CyZr-IbT.js";const as={class:"page-shell module-page"},ns={class:"main-content"},ls={class:"left-panel"},rs={class:"upload-zone"},os={class:"selected-files clean-placeholder split-selected-files"},is={key:0,class:"more-line"},us={key:1},cs={class:"run-row"},ds=["disabled"],fs={class:"loading-msg"},vs={key:0,class:"queue-debug-card"},ps={key:0,class:"queue-debug-line"},hs={key:1,class:"queue-debug-payload"},_s={class:"right-panel"},gs={class:"task-list-wrap"},ys={class:"clean-result-summary"},ks={class:"summary-card"},ms={class:"summary-card"},ws={class:"summary-card"},Is={class:"result-list-wrap"},Ss={key:0,class:"empty-tasks"},bs={key:0,class:"result-subsection"},Cs={class:"task-list clean-result-list"},Ts={class:"left split-result-main"},Fs=["title"],Ns={class:"files"},Es={class:"files"},Rs={key:0,class:"files"},xs={key:1,class:"files"},$s={key:2,class:"files"},Ps={key:3,class:"time"},As={key:4,class:"delete-brand-progress-block"},Ds={class:"delete-brand-progress-header"},Ms={class:"delete-brand-progress-bar"},Bs={class:"files delete-brand-progress"},Us={key:5,class:"files split-entry-list delete-brand-preview"},qs={key:6,class:"files"},Ls={class:"task-right split-result-actions"},Ks=["onClick"],Os=["onClick"],Hs=["onClick"],Js={key:1,class:"result-subsection"},Gs={class:"task-list clean-result-list"},js={class:"left split-result-main"},Vs=["title"],zs={class:"files"},Qs={class:"files"},Xs={key:0,class:"files"},Ws={key:1,class:"files"},Ys={key:2,class:"time"},Zs={key:3,class:"delete-brand-progress-block"},et={class:"delete-brand-progress-header"},st={class:"delete-brand-progress-bar"},tt={class:"files delete-brand-progress"},at={key:4,class:"files split-entry-list delete-brand-preview"},nt={key:5,class:"files"},lt={class:"task-right split-result-actions"},rt=["onClick"],ot=["onClick"],it=Oe({__name:"BrandDeleteBrandTab",setup(ct){function M(){return`delete-brand:current-tasks:${typeof window<"u"&&window.localStorage.getItem("uid")||"0"}`}const $=m([]),H=m([]),B=m(!1),oe=m([]),v=m([]),J=m([]),G=m({total:0,successCount:0,failedCount:0,items:[]}),T=m(""),C=m(""),g=m({}),S=m(null),Y=m(!1),P=m(!1),A=m(""),j=m(!1),Z=W(()=>$.value.slice(0,10)),F=W(()=>{const e=[];return v.value.forEach(s=>{e.push(...s.items)}),e});function Ee(){try{const e=typeof window<"u"?window.localStorage.getItem(M()):null;if(e){if(v.value=JSON.parse(e),v.value.length>0){const s=v.value[v.value.length-1];T.value=s.queuePushResult||"",C.value=s.queuePayloadText||""}}else v.value=[]}catch{v.value=[]}}function ie(e,s,t,a){if(!s?.length)return;const n=v.value.findIndex(d=>d.taskId===e),o=n>=0?v.value[n]:null,l=s.map(d=>{const _=o?.items.find(Ke=>D(Ke,d)),k=Array.isArray(d.countries)?d.countries:void 0,y=Array.isArray(_?.countries)?_?.countries:void 0,w=k&&k.length>0?k:y&&y.length>0?y:k??y,I=Array.isArray(d.previewRows)?d.previewRows:void 0,b=Array.isArray(_?.previewRows)?_?.previewRows:void 0,le=I&&I.length>0?I:b&&b.length>0?b:I??b;return{...d,countries:w,previewRows:le,_pushed:_?._pushed??d._pushed??!1,_completed:_?._completed??d._completed??!1}}),i={taskId:e,items:l,createdAt:o?.createdAt||Date.now(),queuePushResult:t??o?.queuePushResult,queuePayloadText:a??o?.queuePayloadText};n>=0?v.value[n]=i:v.value.push(i),typeof window<"u"&&window.localStorage.setItem(M(),JSON.stringify(v.value))}function ue(e){const s=v.value.length;v.value=v.value.filter(t=>t.taskId!==e),v.value.length!==s&&typeof window<"u"&&window.localStorage.setItem(M(),JSON.stringify(v.value))}const V=W(()=>J.value.filter(e=>!F.value.some(s=>D(s,e)))),Re=W(()=>F.value.length>0||V.value.length>0);function ce(){typeof window<"u"&&window.localStorage.setItem(M(),JSON.stringify(v.value))}function N(e){return e?(v.value.find(t=>t.taskId===e)?.items.length||0)>1:!1}function ee(e){return e&&v.value.find(s=>s.taskId===e)||null}function D(e,s){return!e||!s?!1:e.resultId!=null&&s.resultId!=null?e.resultId===s.resultId:e.fileKey&&s.fileKey?e.fileKey===s.fileKey:e.sourceFilename===s.sourceFilename}function E(e,s){const t=ee(e);return t&&t.items.find(a=>D(a,s))||null}function de(e){if(!e)return!1;const s=ee(e);if(!s)return!1;let t=!1;const a=g.value[e]?.line_progress?.info,n=N(e);if(a?.file_name&&a.current_line!=null&&a.total_lines!=null&&a.total_lines>0&&a.current_line>=a.total_lines){const o=s.items.find(l=>l.sourceFilename===a.file_name);o&&!o._completed&&(o._completed=!0,t=!0)}if(!n){const o=g.value[e]?.task?.status;if((o==="SUCCESS"||o==="FAILED")&&s.items.length===1){const l=s.items[0];l&&!l._completed&&(l._completed=!0,t=!0)}}return t&&ce(),t}function fe(e){return e&&g.value[e]?.task?.status||""}function z(e){if(e.error)return e.error;if(e.matchStatus&&e.matchStatus!=="MATCHED")return e.matchMessage||"";const s=e.taskId;return s&&g.value[s]?.task?.errorMessage||""}function ve(e){const s=e.taskId;if(!s)return!1;const t=g.value[s]?.line_progress,a=fe(s),n=N(s),o=x(e);if(n){const l=E(s,e),i=L(e)===A.value,d=!!(t?.has_progress&&t?.info?.file_name===e.sourceFilename);return!!(i||d||l?._pushed||(a==="RUNNING"||a==="PENDING"||!a)&&l?._completed)}return o==="本文件解析完毕"||o==="已被全局判定为终态"?!1:t?.has_progress||t?.info?!0:o==="已入队"||o==="处理中"}function pe(e){return e.map(s=>s.matchStatus==="MATCHED"?{...s,_pushed:!1,_completed:!1}:{...s,matched:!1,openStoreUrl:void 0,error:s.error||void 0,_pushed:!1,_completed:!1})}function he(e){return e.matchStatus==="MATCHED"?`已匹配 ${e.shopId||""}`.trim():e.matchMessage?e.matchMessage:e.matchStatus==="CONFLICT"?"存在多个同名店铺,请人工确认":e.matchStatus==="PENDING"||e.matchStatus==="INDEX_STALE"?"店铺索引暂未就绪":e.matchStatus?e.matchStatus:e.matched?`已匹配 ${e.shopId||""}`.trim():"待匹配"}function xe(){oe.value=[...F.value,...V.value]}function R(){xe(),$e(oe.value)}function $e(e){G.value={total:e.length,successCount:e.filter(s=>s.success).length,failedCount:e.filter(s=>!s.success).length,items:e}}function se(e){const s=e.taskId;return s&&(g.value[s]?.fileProgress||[]).find(n=>n.fileKey&&e.fileKey?n.fileKey===e.fileKey:n.sourceFilename&&e.sourceFilename?n.sourceFilename===e.sourceFilename:!1)||null}function _e(e){const s=e.taskId;if(!s)return"";const t=g.value[s],a=t?.line_progress?.info,n=se(e);if(N(s)){const i=E(s,e),d=t?.task?.status,_=!!(a?.file_name&&a.file_name===e.sourceFilename);let k=i?._completed?"COMPLETED":(i?._pushed,"PENDING"),y=0,w=Math.max(0,e.totalRows??n?.totalRows??0);d==="SUCCESS"||d==="FAILED"&&i?._completed?(k="COMPLETED",y=w):_?(k="RUNNING",y=Math.max(0,Math.min(w||Number.MAX_SAFE_INTEGER,a?.current_line??0)),!w&&a?.total_lines&&a.total_lines>0&&(w=a.total_lines)):i?._completed?(k="COMPLETED",y=w):i?._pushed&&(k="PENDING",y=0);const I=[];if(I.push(`文件状态:${k}`),I.push(`文件进度:${y}/${w}`),a?.finished_files!=null){const b=a.file_total&&a.file_total>0?`/${a.file_total}`:"";I.push(`已完成文件:${a.finished_files}${b}`)}return a?.phase&&I.push(`阶段:${a.phase}`),I.join("|")}if(n){const i=[],d=n.processedRows??0,_=n.totalRows??0;if(n.status&&i.push(`文件状态:${n.status}`),i.push(`文件进度:${d}/${_}`),a?.finished_files!=null){const k=a.file_total&&a.file_total>0?`/${a.file_total}`:"";i.push(`已完成文件:${a.finished_files}${k}`)}return a?.phase&&i.push(`阶段:${a.phase}`),i.join("|")}if(!t?.line_progress?.has_progress||!a){const i=t?.task?.status;return i?`任务状态:${i}`:""}const l=[];if(a.phase&&l.push(`阶段:${a.phase}`),a.file_index&&a.file_total&&l.push(`文件:${a.file_index}/${a.file_total}`),a.file_name&&l.push(`当前文件:${a.file_name}`),a.current_line!=null&&a.total_lines!=null&&l.push(`${N(s)?"当前文件进度":"进度"}:${a.current_line}/${a.total_lines}`),a.current_country&&l.push(`国家:${a.current_country}`),a.current_asin&&l.push(`ASIN:${a.current_asin}`),a.finished_files!=null){const i=a.file_total&&a.file_total>0?`/${a.file_total}`:"";l.push(`已完成文件:${a.finished_files}${i}`)}return l.join("|")}function Q(e){const s=e.taskId;if(!s)return 0;const t=g.value[s]?.line_progress?.info;if(N(s)){const l=E(s,e);if(g.value[s]?.task?.status==="SUCCESS"||l?._completed)return 100;const d=Math.max(0,e.totalRows??se(e)?.totalRows??0);if(!!(t?.file_name&&t.file_name===e.sourceFilename)){if(t?.current_line!=null&&t?.total_lines&&t.total_lines>0)return Math.max(0,Math.min(100,Math.round(t.current_line/t.total_lines*100)));if(t?.current_line!=null&&d>0)return Math.max(0,Math.min(100,Math.round(t.current_line/d*100)))}return 0}const n=se(e);return n?Math.max(0,Math.min(100,n.percent??0)):t?t.current_line!=null&&t.total_lines&&t.total_lines>0?Math.max(0,Math.min(100,Math.round(t.current_line/t.total_lines*100))):t.finished_files!=null&&t.file_total&&t.file_total>0?Math.max(0,Math.min(100,Math.round(t.finished_files/t.file_total*100))):g.value[s]?.task?.status==="SUCCESS"?100:0:0}function ge(e){const s=g.value[e]?.task?.status;return s==="SUCCESS"||s==="FAILED"}function U(){const e=new Set;return v.value.forEach(s=>{s.items.some(t=>t._pushed)&&e.add(s.taskId)}),Array.from(e)}function x(e){const s=e.taskId;if(!s||!ee(s))return"";const a=E(s,e),n=fe(s),o=g.value[s]?.line_progress,l=N(s);if(!l&&(n==="SUCCESS"||n==="FAILED"||e.taskStatus==="SUCCESS"||e.taskStatus==="FAILED"))return"已被全局判定为终态";if(a?._completed)return"本文件解析完毕";if(n==="RUNNING"&&o?.has_progress&&o?.info?.file_name===e.sourceFilename)return"处理中";if(l&&!a?._pushed&&!a?._completed)return"未入队";if(l&&n==="SUCCESS")return"本文件解析完毕";if(!l){const i=g.value[s];if(i?.items&&i.items.find(_=>D(_,e))&&n==="SUCCESS")return"本文件解析完毕"}return a?._pushed?"已入队":"未入队"}function ye(e){if(e.downloadUrl)return!0;const s=e.taskId;return s?g.value[s]?.task?.status==="SUCCESS":!1}function Pe(e){const s=e?.task?.id;if(!s)return!1;const t=v.value.find(l=>l.taskId===s);if(!t)return!1;const a=e?.items||[];let n=!1;const o=t.items.map(l=>{const i=a.find(le=>D(le,l));if(!i)return l;const d=Array.isArray(i.countries)?i.countries:void 0,_=Array.isArray(l.countries)?l.countries:void 0,k=d&&d.length>0?d:_&&_.length>0?_:d??_,y=Array.isArray(i.previewRows)?i.previewRows:void 0,w=Array.isArray(l.previewRows)?l.previewRows:void 0,I=y&&y.length>0?y:w&&w.length>0?w:y??w,b={...l,...i,countries:k,previewRows:I,_pushed:l._pushed,_completed:l._completed};return JSON.stringify(b)!==JSON.stringify(l)&&(n=!0),b});for(const l of a)o.some(i=>D(i,l))||(o.push({...l,_pushed:!1,_completed:!1}),n=!0);return JSON.stringify(o)!==JSON.stringify(t.items)&&(t.items=o,n=!0),n&&ce(),n}async function te(e){const s=e||U();if(s.length)try{const t=await ze(s);let a=!1;for(const n of t.items||[]){const o=n?.task?.id;typeof o=="number"&&o>0&&(g.value[o]=n,Pe(n)&&(a=!0),de(o)&&(a=!0))}a&&R()}catch{}}function Ae(){return document.visibilityState==="visible"?4500:1e4}function ae(e=!1){if(S.value){if(!e)return;clearTimeout(S.value),S.value=null}const s=async()=>{if(S.value=null,Y.value){ae();return}const t=U();if(!t.length)return;Y.value=!0;let a=!1,n=!1;try{await te(t),await qe();for(const o of t)ge(o)&&(ue(o),a=!0,n=!0)}finally{Y.value=!1}n?await q():a&&R(),U().length>0&&ae()};e?s():S.value=window.setTimeout(s,Ae())}function ne(e=!1){S.value&&!e||ae(e)}function X(e){const s=e.taskId?g.value[e.taskId]?.task?.status:null;return s==="SUCCESS"?{className:"success",text:"已完成"}:s==="FAILED"?{className:"failed",text:"失败"}:s==="RUNNING"?{className:"running",text:"执行中"}:e.taskStatus==="SUCCESS"?{className:"success",text:"已完成"}:e.taskStatus==="FAILED"?{className:"failed",text:"失败"}:e.taskStatus==="RUNNING"?{className:"running",text:"执行中"}:e.success?{className:"success",text:"已完成"}:{className:"failed",text:"失败"}}function ke(){S.value&&(window.clearTimeout(S.value),S.value=null)}async function me(e){const s=O();if(!s?.upload_file_to_java)throw new Error("当前桌面端未提供文件上传桥接能力");const t=[];for(const a of e){const n=typeof a=="string"?a:a.absolutePath,o=typeof a=="string"?void 0:a.relativePath,l=await s.upload_file_to_java(n,o);if(!l?.success||!l.data)throw new Error(l?.error||l?.message||`上传失败:${n}`);t.push(l.data)}return t}async function De(){const e=O();if(!e?.select_brand_xlsx_files){h.warning("当前环境不支持文件选择,请在本机客户端中打开");return}try{const s=await e.select_brand_xlsx_files();if(!s?.length)return;$.value=s,H.value=await me(s),h.success(`已选择 ${s.length} 个删除品牌文件`)}catch(s){h.error(s instanceof Error?s.message:"选择失败")}}async function Me(){const e=O();if(!e?.select_brand_folder){h.warning("当前环境不支持文件夹选择,请在本机客户端中打开");return}try{const s=await e.select_brand_folder();if(!s)return;const t=await ts(s);if(!t.success||!t.items?.length){h.warning(t.error||"该文件夹下没有 xlsx 文件");return}$.value=t.items.map(a=>a.relativePath||a.absolutePath),H.value=await me(t.items),h.success(`已选择文件夹内 ${t.items.length} 个 xlsx 文件`)}catch(s){h.error(s instanceof Error?s.message:"选择失败")}}async function Be(){if(!H.value.length){h.warning("请先选择待处理文件");return}try{B.value=!0;const e=await Qe({files:H.value.map(n=>({fileKey:n.fileKey,originalFilename:n.originalFilename,relativePath:n.relativePath}))}),s=pe(e.items||[]),t=s.some(n=>n.matchStatus==="MATCHED"),a=s.some(n=>n.matchStatus&&n.matchStatus!=="MATCHED");T.value="解析完成,等待手动点击打开紫鸟进行单任务处理",C.value="",s.length&&s[0].taskId&&ie(s[0].taskId,s,T.value,C.value),R(),a?h.warning("部分文件尚未命中可用店铺索引,已保留状态信息,请等待后台刷新后重试。"):t?h.success("删除品牌解析完成,请手动推送"):h.warning("当前没有可推送的已匹配文件,请先等待店铺索引刷新。"),await q()}catch(e){h.error(e instanceof Error?e.message:"执行失败")}finally{B.value=!1}}async function we(e){const s=e.taskId;if(!s){h.warning("未找到任务ID");return}const t=O(),a=Xe(s),n=e.outputFilename||e.sourceFilename||`delete-brand_${s}.xlsx`;if(t?.save_file_from_url_new){const o=await t.save_file_from_url_new(a,n);o.success?h.success(`已保存:${o.path||n}`):o.error&&o.error!=="用户取消"&&h.error(o.error);return}}async function q(){try{const e=await We();J.value=pe(e.items||[]),R(),await te(),ne(!0)}catch(e){console.error("loadHistory error:",e)}}function Ie(e){v.value.forEach(t=>{t.items=t.items.filter(a=>a.resultId!==e)});const s=v.value.length;v.value=v.value.filter(t=>t.items.length>0),v.value.length!==s&&typeof window<"u"&&window.localStorage.setItem(M(),JSON.stringify(v.value)),J.value=J.value.filter(t=>t.resultId!==e),R(),U().length===0&&ke()}async function Se(e){try{await Ye(e),Ie(e),await q(),h.success("已删除")}catch(s){const t=s instanceof Error?s.message:"删除失败";if(typeof t=="string"&&t.includes("记录不存在")){Ie(e),h.success("已删除");return}h.error(t)}}function be(e){const s=e.slice(0,5).map(a=>`${a.country}:${a.asin}${a.status?`(${a.status})`:""}`);return e.length-s.length>0?`${s.join("、")} 等 ${e.length} 条`:s.join("、")}function dt(e,s){}function Ce(){for(const e of v.value)for(const s of e.items){const t=x(s);if(t==="已入队"||t==="处理中")return!0}return!1}function L(e){return e.resultId!=null?`result:${e.resultId}`:`task:${e.taskId||0}:${e.sourceFilename||""}`}function Ue(){return F.value.find(e=>!e.openStoreUrl||L(e)===A.value||E(e.taskId,e)?._completed?!1:x(e)==="未入队")}async function Te(e,s){if(s?.auto&&Ce())return e.taskId,e.sourceFilename,!1;const t=O(),a=e.taskId;if(!a)return!1;const n=v.value.find(d=>d.taskId===a);if(!n)return!1;if(!t?.enqueue_json)return h.error("当前环境未启用 pywebview enqueue_json(浏览器环境不会推送)"),!1;const o={type:"delete-brand-run",ts:Date.now(),data:{taskId:a,items:[e]}};C.value=JSON.stringify(o,null,2);const l=await t.enqueue_json(o);let i="";if(l?.success){i=`已推送文件 ${e.sourceFilename},当前队列长度:${l.queue_size??"-"}`;const d=E(a,e);d&&(d._pushed=!0,d._completed=!1),A.value=L(e),ie(a,n.items,i,C.value),ne(!0)}else i=`推送到 Python 队列失败:${l?.error||"未知错误"}`;return T.value=i,!!l?.success}async function qe(){if(!P.value||j.value){P.value,j.value;return}if(Ce())return;const e=A.value;if(e){const t=F.value.find(a=>L(a)===e);if(t){const a=t.taskId,n=N(a);n&&(de(a),x(t),void 0);const o=x(t);if(o==="已入队"||o==="处理中"){t.sourceFilename;return}if(n){const l=E(a,t);if(l&&!l._completed){t.sourceFilename,l._pushed,l._completed;return}}}}const s=Ue();if(!s){A.value="",P.value=!1;return}j.value=!0;try{s.taskId,s.sourceFilename,L(s),await Te(s,{auto:!0})||(s.taskId,s.sourceFilename,P.value=!1)}finally{j.value=!1}}async function Le(e){P.value=!0,await Te(e)||(P.value=!1,A.value="")}return He(()=>{Ee();const e=U();e.length>0?te(e).then(async()=>{let s=!1,t=!1;for(const a of e)ge(a)&&(ue(a),s=!0,t=!0);t?await q():s&&R(),ne(!0)}):R(),q().catch(()=>{})}),Je(()=>{ke()}),(e,s)=>(u(),c("div",as,[Ge(je,{active:"delete-brand"}),r("div",ns,[r("aside",ls,[s[2]||(s[2]=r("div",{class:"section-title"},"选择文件",-1)),r("div",rs,[s[0]||(s[0]=r("div",{class:"hint"},"上传删除品牌 Excel;无论是直接上传文件还是上传文件夹批量导入,都会按每个 Excel 文件名作为店铺名。",-1)),r("div",{class:"btns"},[r("button",{type:"button",class:"opt-btn",onClick:De},"选择 Excel 文件"),r("button",{type:"button",class:"opt-btn",onClick:Me},"选择文件夹")]),r("div",os,[$.value.length?(u(),c(K,{key:0},[(u(!0),c(K,null,re(Z.value,t=>(u(),c("span",{key:t},f(t),1))),128)),$.value.length>Z.value.length?(u(),c("span",is," 还有 "+f($.value.length-Z.value.length)+" 个文件未展开显示 ",1)):p("",!0)],64)):(u(),c("span",us,"暂未选择删除品牌文件"))])]),s[3]||(s[3]=Ve('
执行说明
',2)),r("div",cs,[r("button",{type:"button",class:"btn-run",disabled:B.value,onClick:Be},f(B.value?"解析中...":"开始解析"),9,ds),r("span",fs,f(B.value?"正在读取删除品牌数据,请稍候…":"解析后会在右侧展示按国家分组的去重结果"),1)]),T.value||C.value?(u(),c("div",vs,[s[1]||(s[1]=r("div",{class:"section-title queue-debug-title"},"Python 队列推送结果",-1)),T.value?(u(),c("div",ps,f(T.value),1)):p("",!0),C.value?(u(),c("pre",hs,f(C.value),1)):p("",!0)])):p("",!0)]),r("section",_s,[s[12]||(s[12]=r("div",{class:"panel-header"},"删除品牌结果",-1)),r("div",gs,[r("div",ys,[r("div",ks,[s[4]||(s[4]=r("span",{class:"summary-label"},"已处理文件",-1)),r("strong",null,f(G.value.total),1)]),r("div",ms,[s[5]||(s[5]=r("span",{class:"summary-label"},"成功结果",-1)),r("strong",null,f(G.value.successCount),1)]),r("div",ws,[s[6]||(s[6]=r("span",{class:"summary-label"},"失败文件",-1)),r("strong",null,f(G.value.failedCount),1)])]),r("div",Is,[s[11]||(s[11]=r("div",{class:"result-list-header"},[r("span",null,"删除品牌结果列表")],-1)),Re.value?(u(),c(K,{key:1},[F.value.length?(u(),c("div",bs,[s[8]||(s[8]=r("div",{class:"result-subsection-title"},"当前任务",-1)),r("ul",Cs,[(u(!0),c(K,null,re(F.value,t=>(u(),c("li",{key:`current-${t.taskId||t.resultId||t.sourceFilename}`,class:"task-item split-result-item"},[r("div",Ts,[r("span",{class:"id",title:t.sourceFilename},f(t.sourceFilename||"-"),9,Fs),r("div",Ns,"店铺名:"+f(t.shopName||"-"),1),r("div",Es,"匹配结果:"+f(he(t)),1),t.platform?(u(),c("div",Rs,"平台:"+f(t.platform),1)):p("",!0),x(t)?(u(),c("div",xs,"队列状态:"+f(x(t)),1)):p("",!0),t.countryCount!==void 0&&t.matched?(u(),c("div",$s,"国家数:"+f(t.countryCount),1)):p("",!0),t.totalRows!==void 0&&t.matched?(u(),c("div",Ps,"去重后 "+f(t.totalRows)+" 条 ",1)):p("",!0),ve(t)?(u(),c("div",As,[r("div",Ds,[s[7]||(s[7]=r("span",null,"任务进度",-1)),r("span",null,f(Q(t))+"%",1)]),r("div",Ms,[r("div",{class:"delete-brand-progress-bar-fill",style:Fe({width:`${Q(t)}%`})},null,4)]),r("div",Bs,f(_e(t)),1)])):p("",!0),t.previewRows?.length?(u(),c("div",Us,f(be(t.previewRows)),1)):p("",!0),z(t)?(u(),c("div",qs,"错误信息:"+f(z(t)),1)):p("",!0)]),r("div",Ls,[r("span",{class:Ne(["status",X(t).className])},f(X(t).text),3),t.openStoreUrl?(u(),c("button",{key:0,type:"button",class:"download",onClick:a=>Le(t)}," 打开紫鸟 ",8,Ks)):p("",!0),ye(t)?(u(),c("button",{key:1,type:"button",class:"download",onClick:a=>we(t)}," 下载 ",8,Os)):p("",!0),t.resultId?(u(),c("button",{key:2,type:"button",class:"btn-delete",onClick:a=>Se(t.resultId)}," 删除 ",8,Hs)):p("",!0)])]))),128))])])):p("",!0),V.value.length?(u(),c("div",Js,[s[10]||(s[10]=r("div",{class:"result-subsection-title"},"历史记录",-1)),r("ul",Gs,[(u(!0),c(K,null,re(V.value,t=>(u(),c("li",{key:`history-${t.taskId||t.resultId||t.sourceFilename}`,class:"task-item split-result-item"},[r("div",js,[r("span",{class:"id",title:t.sourceFilename},f(t.sourceFilename||"-"),9,Vs),r("div",zs,"店铺名:"+f(t.shopName||"-"),1),r("div",Qs,"匹配结果:"+f(he(t)),1),t.platform?(u(),c("div",Xs,"平台:"+f(t.platform),1)):p("",!0),t.countryCount!==void 0&&t.matched?(u(),c("div",Ws,"国家数:"+f(t.countryCount),1)):p("",!0),t.totalRows!==void 0&&t.matched?(u(),c("div",Ys,"去重后 "+f(t.totalRows)+" 条 ",1)):p("",!0),ve(t)?(u(),c("div",Zs,[r("div",et,[s[9]||(s[9]=r("span",null,"任务进度",-1)),r("span",null,f(Q(t))+"%",1)]),r("div",st,[r("div",{class:"delete-brand-progress-bar-fill",style:Fe({width:`${Q(t)}%`})},null,4)]),r("div",tt,f(_e(t)),1)])):p("",!0),t.previewRows?.length?(u(),c("div",at,f(be(t.previewRows)),1)):p("",!0),z(t)?(u(),c("div",nt,"错误信息:"+f(z(t)),1)):p("",!0)]),r("div",lt,[r("span",{class:Ne(["status",X(t).className])},f(X(t).text),3),ye(t)?(u(),c("button",{key:0,type:"button",class:"download",onClick:a=>we(t)}," 下载 ",8,rt)):p("",!0),t.resultId?(u(),c("button",{key:1,type:"button",class:"btn-delete",onClick:a=>Se(t.resultId)}," 删除 ",8,ot)):p("",!0)])]))),128))])])):p("",!0)],64)):(u(),c("div",Ss," 暂无删除品牌结果,完成解析后会在这里展示按国家分组的去重结果 "))])])])])]))}}),ut=Ze(it,[["__scopeId","data-v-468f2aa7"]]);es(ut).use(ss).mount("#app"); diff --git a/app/assets/split.js b/app/assets/split.js index 97d282c..8c34021 100644 --- a/app/assets/split.js +++ b/app/assets/split.js @@ -1 +1 @@ -import{d as G,o as J,a,c as n,b as L,B as O,e,F as S,r as $,t as i,f as v,n as x,w as k,s as V,C as M,g as c,h as Q,i as F,E as r,D as W,G as X,m as Y,v as Z,H as ss,_ as es,p as ts,q as ls}from"./pywebview-Cq_E2BnJ.js";import{e as as}from"./brand-DZ5_PGnw.js";const ns={class:"page-shell module-page"},os={class:"main-content"},is={class:"left-panel"},rs={class:"upload-zone"},us={class:"selected-files clean-placeholder split-selected-files"},cs={key:0,class:"more-line"},ds={key:1},ps={class:"option-group column-option-group"},vs={class:"column-toolbar"},fs={class:"column-count"},_s={class:"column-actions"},ms={key:0,class:"column-grid"},ys=["value"],gs={key:1,class:"empty-column-state"},hs={class:"option-group"},bs={class:"radio-item active"},ws={class:"desc"},ks={class:"option-group"},Cs={class:"option-group"},Ss={key:0,class:"split-input-card"},xs={key:1,class:"split-input-card"},Fs={class:"run-row"},Es=["disabled"],Ps={class:"loading-msg"},$s={class:"right-panel"},Bs={class:"task-list-wrap"},Rs={class:"clean-result-summary"},Is={class:"summary-card"},Us={class:"summary-card"},Vs={class:"summary-card"},Ms={class:"result-list-wrap"},Ts={key:0,class:"empty-tasks"},Ds={key:1,class:"task-list clean-result-list"},Hs={class:"left split-result-main"},Ns=["title"],js={key:0,class:"files"},As={key:1,class:"time"},Ks={key:2,class:"files"},zs={key:3,class:"files split-entry-list"},qs={key:4,class:"files"},Gs={class:"task-right split-result-actions"},Js=["onClick"],Ls=["onClick"],Os=G({__name:"BrandSplitTab",setup(Ws){const f=c([]),C=c(""),d=c([]),_=c([]),p=c([]),B=c(0),u=c("rows_per_file"),m=c(2e3),y=c(5),g=c(!1),h=c([]),b=c({total:0,successCount:0,failedCount:0,items:[]}),E=Q(()=>f.value.slice(0,8));function T(l){b.value={total:l.length,successCount:l.filter(s=>s.success).length,failedCount:l.filter(s=>!s.success).length,items:l}}async function R(l){const s=F();if(!s?.upload_file_to_java)throw new Error("当前桌面端未提供文件上传桥接能力");const t=[];for(const o of l){const U=typeof o=="string"?o:o.absolutePath,q=typeof o=="string"?void 0:o.relativePath,w=await s.upload_file_to_java(U,q);if(!w?.success||!w.data)throw new Error(w?.error||w?.message||`上传失败:${U}`);t.push(w.data)}return t}async function I(l){const s=await Y(l);_.value=s.headers||[],p.value=[...s.headers||[]],B.value=s.totalRows||0}async function D(l,s){f.value=l,C.value="",d.value=await R(l),d.value.length>0&&await I(d.value[0].fileKey),r.success(s)}async function H(){const l=F();if(!l?.select_brand_xlsx_files){r.warning("当前环境不支持文件选择,请在本机客户端中打开");return}try{const s=await l.select_brand_xlsx_files();if(!s?.length)return;await D(s,`已选择 ${s.length} 个待拆分文件`)}catch(s){r.error(s instanceof Error?s.message:"选择失败")}}async function N(){const l=F();if(!l?.select_brand_folder){r.warning("当前环境不支持文件夹选择,请在本机客户端中打开");return}try{const s=await l.select_brand_folder();if(!s)return;C.value=s.split(/[/\\]/).filter(Boolean).pop()||"";const t=await as(s);if(t.success&&t.items?.length){f.value=t.items.map(o=>o.relativePath),d.value=await R(t.items),d.value.length>0&&await I(d.value[0].fileKey),r.success(`已选择文件夹内 ${t.items.length} 个 xlsx 文件`);return}r.warning(t.error||"该文件夹下没有 xlsx 文件")}catch(s){r.error(s instanceof Error?s.message:"选择失败")}}async function j(){if(!d.value.length){r.warning("请先选择待拆分 Excel 文件或文件夹");return}if(!p.value.length){r.warning("请至少选择一列保留列");return}if(u.value==="rows_per_file"&&(!m.value||m.value<=0)){r.warning("请输入有效的每份条数");return}if(u.value==="parts"&&(!y.value||y.value<=0)){r.warning("请输入有效的拆分份数");return}try{g.value=!0;const l=await W({files:d.value.map(s=>({fileKey:s.fileKey,originalFilename:s.originalFilename,relativePath:s.relativePath})),selectedColumns:p.value,splitMode:u.value,rowsPerFile:u.value==="rows_per_file"&&m.value||void 0,parts:u.value==="parts"&&y.value||void 0,archiveName:d.value.some(s=>!!s.relativePath)&&C.value?C.value:void 0});b.value=l,h.value=l.items||[],await P(),r.success("数据拆分完成")}catch(l){r.error(l instanceof Error?l.message:"数据拆分失败")}finally{g.value=!1}}async function P(){try{const l=await X();h.value=l.items||[],T(h.value)}catch{}}async function A(l){try{await ss(l),await P(),r.success("已删除")}catch(s){r.error(s instanceof Error?s.message:"删除失败")}}function K(l){const s=l.slice(0,4).map(o=>o.rowCount!==void 0?`${o.filename}(${o.rowCount}条)`:o.filename);return l.length-s.length>0?`${s.join("、")} 等 ${l.length} 个文件`:s.join("、")}async function z(l){const s=F();if(!l.downloadUrl){r.warning("当前结果没有下载地址");return}const t=l.outputFilename||`${new Date().toISOString().slice(0,10).replace(/-/g,"")}.zip`;if(s?.save_file_from_url_new){const o=await s.save_file_from_url_new(l.downloadUrl,t);o.success?r.success(`已保存:${o.path||t}`):o.error&&o.error!=="用户取消"&&r.error(o.error);return}}return J(()=>{P().catch(()=>{})}),(l,s)=>(a(),n("div",ns,[L(O,{active:"split"}),e("div",os,[e("aside",is,[s[14]||(s[14]=e("div",{class:"section-title"},"选择文件",-1)),e("div",rs,[s[7]||(s[7]=e("div",{class:"hint"},"选择需要拆分的 Excel 文件或文件夹,按规则拆成多个 Excel 文件。",-1)),e("div",{class:"btns"},[e("button",{type:"button",class:"opt-btn",onClick:H},"选择 Excel 文件"),e("button",{type:"button",class:"opt-btn",onClick:N},"选择文件夹")]),e("div",us,[f.value.length?(a(),n(S,{key:0},[(a(!0),n(S,null,$(E.value,t=>(a(),n("span",{key:t},i(t),1))),128)),f.value.length>E.value.length?(a(),n("span",cs," 还有 "+i(f.value.length-E.value.length)+" 个文件未展开显示 ",1)):v("",!0)],64)):(a(),n("span",ds,"暂未选择待拆分文件"))])]),s[15]||(s[15]=e("div",{class:"section-title"},"保留列设置",-1)),e("div",ps,[e("div",vs,[e("span",fs,"已选择 "+i(p.value.length)+" / "+i(_.value.length)+" 列",1),e("div",_s,[e("button",{type:"button",class:"opt-btn small",onClick:s[0]||(s[0]=t=>p.value=[..._.value])},"全选"),e("button",{type:"button",class:"opt-btn small",onClick:s[1]||(s[1]=t=>p.value=[])},"清空")])]),_.value.length?(a(),n("div",ms,[(a(!0),n(S,null,$(_.value,t=>(a(),n("label",{key:t,class:x(["column-item",{active:p.value.includes(t)}])},[k(e("input",{"onUpdate:modelValue":s[2]||(s[2]=o=>p.value=o),type:"checkbox",value:t},null,8,ys),[[Z,p.value]]),e("span",null,i(t),1)],2))),128))])):(a(),n("div",gs,"请选择 Excel 文件后读取表头并填充可保留列"))]),s[16]||(s[16]=e("div",{class:"section-title"},"总条数",-1)),e("div",hs,[e("label",bs,[s[9]||(s[9]=e("input",{type:"checkbox",checked:"",disabled:""},null,-1)),e("div",null,[s[8]||(s[8]=e("span",{class:"label"},"当前数据总条数",-1)),e("div",ws,i(B.value)+" 条(不含表头)",1)])])]),s[17]||(s[17]=e("div",{class:"section-title"},"拆分模式",-1)),e("div",ks,[e("label",{class:x(["radio-item",{active:u.value==="rows_per_file"}])},[k(e("input",{"onUpdate:modelValue":s[3]||(s[3]=t=>u.value=t),type:"radio",value:"rows_per_file",name:"split-mode"},null,512),[[V,u.value]]),s[10]||(s[10]=e("div",null,[e("span",{class:"label"},"按每份条数"),e("div",{class:"desc"},"例如每份 2000 条,最后一份放剩余数据")],-1))],2),e("label",{class:x(["radio-item",{active:u.value==="parts"}])},[k(e("input",{"onUpdate:modelValue":s[4]||(s[4]=t=>u.value=t),type:"radio",value:"parts",name:"split-mode"},null,512),[[V,u.value]]),s[11]||(s[11]=e("div",null,[e("span",{class:"label"},"按份数平均拆分"),e("div",{class:"desc"},"例如拆 5 份,按总条数尽量平均分配")],-1))],2)]),s[18]||(s[18]=e("div",{class:"section-title"},"拆分参数",-1)),e("div",Cs,[u.value==="rows_per_file"?(a(),n("label",Ss,[s[12]||(s[12]=e("span",{class:"label"},"每份条数",-1)),k(e("input",{"onUpdate:modelValue":s[5]||(s[5]=t=>m.value=t),class:"split-number-input",type:"number",min:"1",placeholder:"例如 2000"},null,512),[[M,m.value,void 0,{number:!0}]])])):(a(),n("label",xs,[s[13]||(s[13]=e("span",{class:"label"},"拆分份数",-1)),k(e("input",{"onUpdate:modelValue":s[6]||(s[6]=t=>y.value=t),class:"split-number-input",type:"number",min:"1",placeholder:"例如 5"},null,512),[[M,y.value,void 0,{number:!0}]])]))]),e("div",Fs,[e("button",{type:"button",class:"btn-run",disabled:g.value,onClick:j},i(g.value?"拆分中...":"开始拆分"),9,Es),e("span",Ps,i(g.value?"正在拆分并上传结果,请稍候…":"选择文件、保留列和拆分规则后即可执行拆分,结果会显示在右侧供下载"),1)])]),e("section",$s,[s[23]||(s[23]=e("div",{class:"panel-header"},"拆分结果",-1)),e("div",Bs,[e("div",Rs,[e("div",Is,[s[19]||(s[19]=e("span",{class:"summary-label"},"已处理文件",-1)),e("strong",null,i(b.value.total),1)]),e("div",Us,[s[20]||(s[20]=e("span",{class:"summary-label"},"成功结果",-1)),e("strong",null,i(b.value.successCount),1)]),e("div",Vs,[s[21]||(s[21]=e("span",{class:"summary-label"},"失败文件",-1)),e("strong",null,i(b.value.failedCount),1)])]),e("div",Ms,[s[22]||(s[22]=e("div",{class:"result-list-header"},[e("span",null,"拆分结果压缩包列表")],-1)),h.value.length===0?(a(),n("div",Ts," 暂无拆分结果,完成数据拆分后会在这里展示压缩包文件 ")):(a(),n("ul",Ds,[(a(!0),n(S,null,$(h.value,t=>(a(),n("li",{key:`${t.resultId||t.outputFilename||t.sourceFilename}-${t.rowCount||0}`,class:"task-item split-result-item"},[e("div",Hs,[e("span",{class:"id",title:t.sourceFilename},i(t.sourceFilename||"-"),9,Ns),t.outputFilename?(a(),n("div",js,"压缩包文件:"+i(t.outputFilename),1)):v("",!0),t.rowCount!==void 0?(a(),n("div",As,"压缩包共包含 "+i(t.rowCount)+" 条数据",1)):v("",!0),t.entryCount&&t.entryCount>0?(a(),n("div",Ks,"压缩包内共 "+i(t.entryCount)+" 个拆分文件 ",1)):v("",!0),t.entries&&t.entries.length?(a(),n("div",zs," 压缩包内容:"+i(K(t.entries)),1)):v("",!0),t.error?(a(),n("div",qs,"错误信息:"+i(t.error),1)):v("",!0)]),e("div",Gs,[e("span",{class:x(["status",t.success?"success":"failed"])},i(t.success?"已完成":"失败"),3),t.success&&t.downloadUrl?(a(),n("button",{key:0,type:"button",class:"download",onClick:o=>z(t)}," 下载压缩包 ",8,Js)):v("",!0),t.resultId?(a(),n("button",{key:1,type:"button",class:"btn-delete",onClick:o=>A(t.resultId)}," 删除 ",8,Ls)):v("",!0)])]))),128))]))])])])])]))}}),Qs=es(Os,[["__scopeId","data-v-c5a98d6b"]]);ts(Qs).use(ls).mount("#app"); +import{d as G,o as J,a,c as n,b as L,B as O,e,F as S,r as $,t as i,f as v,n as x,w as k,s as V,C as M,g as c,h as Q,i as F,E as r,D as W,G as X,m as Y,v as Z,H as ss,_ as es,p as ts,q as ls}from"./pywebview-C_Si_gmj.js";import{e as as}from"./brand-CyZr-IbT.js";const ns={class:"page-shell module-page"},os={class:"main-content"},is={class:"left-panel"},rs={class:"upload-zone"},us={class:"selected-files clean-placeholder split-selected-files"},cs={key:0,class:"more-line"},ds={key:1},ps={class:"option-group column-option-group"},vs={class:"column-toolbar"},fs={class:"column-count"},_s={class:"column-actions"},ms={key:0,class:"column-grid"},ys=["value"],gs={key:1,class:"empty-column-state"},hs={class:"option-group"},bs={class:"radio-item active"},ws={class:"desc"},ks={class:"option-group"},Cs={class:"option-group"},Ss={key:0,class:"split-input-card"},xs={key:1,class:"split-input-card"},Fs={class:"run-row"},Es=["disabled"],Ps={class:"loading-msg"},$s={class:"right-panel"},Bs={class:"task-list-wrap"},Rs={class:"clean-result-summary"},Is={class:"summary-card"},Us={class:"summary-card"},Vs={class:"summary-card"},Ms={class:"result-list-wrap"},Ts={key:0,class:"empty-tasks"},Ds={key:1,class:"task-list clean-result-list"},Hs={class:"left split-result-main"},Ns=["title"],js={key:0,class:"files"},As={key:1,class:"time"},Ks={key:2,class:"files"},zs={key:3,class:"files split-entry-list"},qs={key:4,class:"files"},Gs={class:"task-right split-result-actions"},Js=["onClick"],Ls=["onClick"],Os=G({__name:"BrandSplitTab",setup(Ws){const f=c([]),C=c(""),d=c([]),_=c([]),p=c([]),B=c(0),u=c("rows_per_file"),m=c(2e3),y=c(5),g=c(!1),h=c([]),b=c({total:0,successCount:0,failedCount:0,items:[]}),E=Q(()=>f.value.slice(0,8));function T(l){b.value={total:l.length,successCount:l.filter(s=>s.success).length,failedCount:l.filter(s=>!s.success).length,items:l}}async function R(l){const s=F();if(!s?.upload_file_to_java)throw new Error("当前桌面端未提供文件上传桥接能力");const t=[];for(const o of l){const U=typeof o=="string"?o:o.absolutePath,q=typeof o=="string"?void 0:o.relativePath,w=await s.upload_file_to_java(U,q);if(!w?.success||!w.data)throw new Error(w?.error||w?.message||`上传失败:${U}`);t.push(w.data)}return t}async function I(l){const s=await Y(l);_.value=s.headers||[],p.value=[...s.headers||[]],B.value=s.totalRows||0}async function D(l,s){f.value=l,C.value="",d.value=await R(l),d.value.length>0&&await I(d.value[0].fileKey),r.success(s)}async function H(){const l=F();if(!l?.select_brand_xlsx_files){r.warning("当前环境不支持文件选择,请在本机客户端中打开");return}try{const s=await l.select_brand_xlsx_files();if(!s?.length)return;await D(s,`已选择 ${s.length} 个待拆分文件`)}catch(s){r.error(s instanceof Error?s.message:"选择失败")}}async function N(){const l=F();if(!l?.select_brand_folder){r.warning("当前环境不支持文件夹选择,请在本机客户端中打开");return}try{const s=await l.select_brand_folder();if(!s)return;C.value=s.split(/[/\\]/).filter(Boolean).pop()||"";const t=await as(s);if(t.success&&t.items?.length){f.value=t.items.map(o=>o.relativePath),d.value=await R(t.items),d.value.length>0&&await I(d.value[0].fileKey),r.success(`已选择文件夹内 ${t.items.length} 个 xlsx 文件`);return}r.warning(t.error||"该文件夹下没有 xlsx 文件")}catch(s){r.error(s instanceof Error?s.message:"选择失败")}}async function j(){if(!d.value.length){r.warning("请先选择待拆分 Excel 文件或文件夹");return}if(!p.value.length){r.warning("请至少选择一列保留列");return}if(u.value==="rows_per_file"&&(!m.value||m.value<=0)){r.warning("请输入有效的每份条数");return}if(u.value==="parts"&&(!y.value||y.value<=0)){r.warning("请输入有效的拆分份数");return}try{g.value=!0;const l=await W({files:d.value.map(s=>({fileKey:s.fileKey,originalFilename:s.originalFilename,relativePath:s.relativePath})),selectedColumns:p.value,splitMode:u.value,rowsPerFile:u.value==="rows_per_file"&&m.value||void 0,parts:u.value==="parts"&&y.value||void 0,archiveName:d.value.some(s=>!!s.relativePath)&&C.value?C.value:void 0});b.value=l,h.value=l.items||[],await P(),r.success("数据拆分完成")}catch(l){r.error(l instanceof Error?l.message:"数据拆分失败")}finally{g.value=!1}}async function P(){try{const l=await X();h.value=l.items||[],T(h.value)}catch{}}async function A(l){try{await ss(l),await P(),r.success("已删除")}catch(s){r.error(s instanceof Error?s.message:"删除失败")}}function K(l){const s=l.slice(0,4).map(o=>o.rowCount!==void 0?`${o.filename}(${o.rowCount}条)`:o.filename);return l.length-s.length>0?`${s.join("、")} 等 ${l.length} 个文件`:s.join("、")}async function z(l){const s=F();if(!l.downloadUrl){r.warning("当前结果没有下载地址");return}const t=l.outputFilename||`${new Date().toISOString().slice(0,10).replace(/-/g,"")}.zip`;if(s?.save_file_from_url_new){const o=await s.save_file_from_url_new(l.downloadUrl,t);o.success?r.success(`已保存:${o.path||t}`):o.error&&o.error!=="用户取消"&&r.error(o.error);return}}return J(()=>{P().catch(()=>{})}),(l,s)=>(a(),n("div",ns,[L(O,{active:"split"}),e("div",os,[e("aside",is,[s[14]||(s[14]=e("div",{class:"section-title"},"选择文件",-1)),e("div",rs,[s[7]||(s[7]=e("div",{class:"hint"},"选择需要拆分的 Excel 文件或文件夹,按规则拆成多个 Excel 文件。",-1)),e("div",{class:"btns"},[e("button",{type:"button",class:"opt-btn",onClick:H},"选择 Excel 文件"),e("button",{type:"button",class:"opt-btn",onClick:N},"选择文件夹")]),e("div",us,[f.value.length?(a(),n(S,{key:0},[(a(!0),n(S,null,$(E.value,t=>(a(),n("span",{key:t},i(t),1))),128)),f.value.length>E.value.length?(a(),n("span",cs," 还有 "+i(f.value.length-E.value.length)+" 个文件未展开显示 ",1)):v("",!0)],64)):(a(),n("span",ds,"暂未选择待拆分文件"))])]),s[15]||(s[15]=e("div",{class:"section-title"},"保留列设置",-1)),e("div",ps,[e("div",vs,[e("span",fs,"已选择 "+i(p.value.length)+" / "+i(_.value.length)+" 列",1),e("div",_s,[e("button",{type:"button",class:"opt-btn small",onClick:s[0]||(s[0]=t=>p.value=[..._.value])},"全选"),e("button",{type:"button",class:"opt-btn small",onClick:s[1]||(s[1]=t=>p.value=[])},"清空")])]),_.value.length?(a(),n("div",ms,[(a(!0),n(S,null,$(_.value,t=>(a(),n("label",{key:t,class:x(["column-item",{active:p.value.includes(t)}])},[k(e("input",{"onUpdate:modelValue":s[2]||(s[2]=o=>p.value=o),type:"checkbox",value:t},null,8,ys),[[Z,p.value]]),e("span",null,i(t),1)],2))),128))])):(a(),n("div",gs,"请选择 Excel 文件后读取表头并填充可保留列"))]),s[16]||(s[16]=e("div",{class:"section-title"},"总条数",-1)),e("div",hs,[e("label",bs,[s[9]||(s[9]=e("input",{type:"checkbox",checked:"",disabled:""},null,-1)),e("div",null,[s[8]||(s[8]=e("span",{class:"label"},"当前数据总条数",-1)),e("div",ws,i(B.value)+" 条(不含表头)",1)])])]),s[17]||(s[17]=e("div",{class:"section-title"},"拆分模式",-1)),e("div",ks,[e("label",{class:x(["radio-item",{active:u.value==="rows_per_file"}])},[k(e("input",{"onUpdate:modelValue":s[3]||(s[3]=t=>u.value=t),type:"radio",value:"rows_per_file",name:"split-mode"},null,512),[[V,u.value]]),s[10]||(s[10]=e("div",null,[e("span",{class:"label"},"按每份条数"),e("div",{class:"desc"},"例如每份 2000 条,最后一份放剩余数据")],-1))],2),e("label",{class:x(["radio-item",{active:u.value==="parts"}])},[k(e("input",{"onUpdate:modelValue":s[4]||(s[4]=t=>u.value=t),type:"radio",value:"parts",name:"split-mode"},null,512),[[V,u.value]]),s[11]||(s[11]=e("div",null,[e("span",{class:"label"},"按份数平均拆分"),e("div",{class:"desc"},"例如拆 5 份,按总条数尽量平均分配")],-1))],2)]),s[18]||(s[18]=e("div",{class:"section-title"},"拆分参数",-1)),e("div",Cs,[u.value==="rows_per_file"?(a(),n("label",Ss,[s[12]||(s[12]=e("span",{class:"label"},"每份条数",-1)),k(e("input",{"onUpdate:modelValue":s[5]||(s[5]=t=>m.value=t),class:"split-number-input",type:"number",min:"1",placeholder:"例如 2000"},null,512),[[M,m.value,void 0,{number:!0}]])])):(a(),n("label",xs,[s[13]||(s[13]=e("span",{class:"label"},"拆分份数",-1)),k(e("input",{"onUpdate:modelValue":s[6]||(s[6]=t=>y.value=t),class:"split-number-input",type:"number",min:"1",placeholder:"例如 5"},null,512),[[M,y.value,void 0,{number:!0}]])]))]),e("div",Fs,[e("button",{type:"button",class:"btn-run",disabled:g.value,onClick:j},i(g.value?"拆分中...":"开始拆分"),9,Es),e("span",Ps,i(g.value?"正在拆分并上传结果,请稍候…":"选择文件、保留列和拆分规则后即可执行拆分,结果会显示在右侧供下载"),1)])]),e("section",$s,[s[23]||(s[23]=e("div",{class:"panel-header"},"拆分结果",-1)),e("div",Bs,[e("div",Rs,[e("div",Is,[s[19]||(s[19]=e("span",{class:"summary-label"},"已处理文件",-1)),e("strong",null,i(b.value.total),1)]),e("div",Us,[s[20]||(s[20]=e("span",{class:"summary-label"},"成功结果",-1)),e("strong",null,i(b.value.successCount),1)]),e("div",Vs,[s[21]||(s[21]=e("span",{class:"summary-label"},"失败文件",-1)),e("strong",null,i(b.value.failedCount),1)])]),e("div",Ms,[s[22]||(s[22]=e("div",{class:"result-list-header"},[e("span",null,"拆分结果压缩包列表")],-1)),h.value.length===0?(a(),n("div",Ts," 暂无拆分结果,完成数据拆分后会在这里展示压缩包文件 ")):(a(),n("ul",Ds,[(a(!0),n(S,null,$(h.value,t=>(a(),n("li",{key:`${t.resultId||t.outputFilename||t.sourceFilename}-${t.rowCount||0}`,class:"task-item split-result-item"},[e("div",Hs,[e("span",{class:"id",title:t.sourceFilename},i(t.sourceFilename||"-"),9,Ns),t.outputFilename?(a(),n("div",js,"压缩包文件:"+i(t.outputFilename),1)):v("",!0),t.rowCount!==void 0?(a(),n("div",As,"压缩包共包含 "+i(t.rowCount)+" 条数据",1)):v("",!0),t.entryCount&&t.entryCount>0?(a(),n("div",Ks,"压缩包内共 "+i(t.entryCount)+" 个拆分文件 ",1)):v("",!0),t.entries&&t.entries.length?(a(),n("div",zs," 压缩包内容:"+i(K(t.entries)),1)):v("",!0),t.error?(a(),n("div",qs,"错误信息:"+i(t.error),1)):v("",!0)]),e("div",Gs,[e("span",{class:x(["status",t.success?"success":"failed"])},i(t.success?"已完成":"失败"),3),t.success&&t.downloadUrl?(a(),n("button",{key:0,type:"button",class:"download",onClick:o=>z(t)}," 下载压缩包 ",8,Js)):v("",!0),t.resultId?(a(),n("button",{key:1,type:"button",class:"btn-delete",onClick:o=>A(t.resultId)}," 删除 ",8,Ls)):v("",!0)])]))),128))]))])])])])]))}}),Qs=es(Os,[["__scopeId","data-v-c5a98d6b"]]);ts(Qs).use(ls).mount("#app"); diff --git a/app/new_web_source/convert.html b/app/new_web_source/convert.html index ebf8bf8..194e09e 100644 --- a/app/new_web_source/convert.html +++ b/app/new_web_source/convert.html @@ -5,8 +5,8 @@ 格式转换 - 数富AI - - + + diff --git a/app/new_web_source/dedupe.html b/app/new_web_source/dedupe.html index 14c18c0..399a780 100644 --- a/app/new_web_source/dedupe.html +++ b/app/new_web_source/dedupe.html @@ -5,8 +5,8 @@ 数据去重 - 数富AI - - + + diff --git a/app/new_web_source/delete-brand.html b/app/new_web_source/delete-brand.html index 5827e06..8d2427b 100644 --- a/app/new_web_source/delete-brand.html +++ b/app/new_web_source/delete-brand.html @@ -5,8 +5,8 @@ 删除品牌 - 数富AI - - + + diff --git a/app/new_web_source/split.html b/app/new_web_source/split.html index 79158d2..88e08d6 100644 --- a/app/new_web_source/split.html +++ b/app/new_web_source/split.html @@ -5,8 +5,8 @@ 数据拆分 - 数富AI - - + + diff --git a/backend-java/aiimage-backend.log.2026-04-10.0.gz b/backend-java/aiimage-backend.log.2026-04-10.0.gz new file mode 100644 index 0000000000000000000000000000000000000000..ffb9d4ce0095a03f4017726a1ffb13fb7b02ec73 GIT binary patch literal 26140 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 literal 0 HcmV?d00001 diff --git a/backend-java/src/main/java/com/nanri/aiimage/config/DeleteBrandProgressProperties.java b/backend-java/src/main/java/com/nanri/aiimage/config/DeleteBrandProgressProperties.java index 2435149..f14c26b 100644 --- a/backend-java/src/main/java/com/nanri/aiimage/config/DeleteBrandProgressProperties.java +++ b/backend-java/src/main/java/com/nanri/aiimage/config/DeleteBrandProgressProperties.java @@ -20,4 +20,10 @@ public class DeleteBrandProgressProperties { */ private long productRiskStaleTimeoutMinutes = 10; private long productRiskInitialTimeoutMinutes = 10; + + /** + * Shop match RUNNING timeout auto-finalize/fail threshold in minutes. + */ + private long shopMatchStaleTimeoutMinutes = 10; + private long shopMatchInitialTimeoutMinutes = 10; } diff --git a/backend-java/src/main/java/com/nanri/aiimage/modules/deletebrand/controller/DeleteBrandRunController.java b/backend-java/src/main/java/com/nanri/aiimage/modules/deletebrand/controller/DeleteBrandRunController.java index 54e0a83..b9492bc 100644 --- a/backend-java/src/main/java/com/nanri/aiimage/modules/deletebrand/controller/DeleteBrandRunController.java +++ b/backend-java/src/main/java/com/nanri/aiimage/modules/deletebrand/controller/DeleteBrandRunController.java @@ -1,6 +1,7 @@ package com.nanri.aiimage.modules.deletebrand.controller; import com.nanri.aiimage.common.api.ApiResponse; +import com.nanri.aiimage.common.exception.BusinessException; import com.nanri.aiimage.modules.deletebrand.model.dto.DeleteBrandRunRequest; import com.nanri.aiimage.modules.deletebrand.model.dto.DeleteBrandSubmitResultRequest; import com.nanri.aiimage.modules.deletebrand.model.dto.DeleteBrandTaskBatchRequest; @@ -97,9 +98,18 @@ public class DeleteBrandRunController { @Operation(summary = "提交删除品牌处理结果", description = "插件/前端分片回传处理结果;后端累计分片并推进进度,全部完成后再执行最终组装与落库。") public ApiResponse submitResult( @PathVariable Long taskId, - @Valid @RequestBody DeleteBrandSubmitResultRequest request) { - deleteBrandRunService.submitResult(taskId, request); - return ApiResponse.success(null); + @Valid @RequestBody DeleteBrandSubmitResultRequest request, + jakarta.servlet.http.HttpServletResponse response) { + try { + deleteBrandRunService.submitResult(taskId, request); + return ApiResponse.success(null); + } catch (BusinessException ex) { + response.setStatus(HttpStatus.BAD_REQUEST.value()); + return ApiResponse.fail(ex.getMessage()); + } catch (Exception ex) { + response.setStatus(HttpStatus.INTERNAL_SERVER_ERROR.value()); + return ApiResponse.fail("服务异常:" + ex.getMessage()); + } } @GetMapping("/tasks/{taskId}/download") diff --git a/backend-java/src/main/java/com/nanri/aiimage/modules/deletebrand/service/DeleteBrandStaleTaskService.java b/backend-java/src/main/java/com/nanri/aiimage/modules/deletebrand/service/DeleteBrandStaleTaskService.java index 4e4ebf0..2623dfc 100644 --- a/backend-java/src/main/java/com/nanri/aiimage/modules/deletebrand/service/DeleteBrandStaleTaskService.java +++ b/backend-java/src/main/java/com/nanri/aiimage/modules/deletebrand/service/DeleteBrandStaleTaskService.java @@ -5,6 +5,8 @@ import com.baomidou.mybatisplus.core.conditions.update.LambdaUpdateWrapper; import com.nanri.aiimage.config.DeleteBrandProgressProperties; import com.nanri.aiimage.modules.productrisk.service.ProductRiskTaskCacheService; import com.nanri.aiimage.modules.productrisk.service.ProductRiskTaskService; +import com.nanri.aiimage.modules.shopmatch.service.ShopMatchTaskCacheService; +import com.nanri.aiimage.modules.shopmatch.service.ShopMatchTaskService; import com.nanri.aiimage.modules.task.mapper.FileTaskMapper; import com.nanri.aiimage.modules.task.model.entity.FileTaskEntity; import lombok.RequiredArgsConstructor; @@ -18,7 +20,7 @@ import java.time.ZoneId; import java.util.Map; /** - * 删除品牌任务的超时回收与 finalize 补偿;并与删除品牌共用同一套定时调度,对商品风险(PRODUCT_RISK_RESOLVE)长时间无回传做失败收尾。 + * 鍒犻櫎鍝佺墝浠诲姟鐨勮秴鏃跺洖鏀朵笌 finalize 琛ュ伩锛涘苟涓庡垹闄ゅ搧鐗屽叡鐢ㄥ悓涓€濂楀畾鏃惰皟搴︼紝瀵瑰晢鍝侀闄╋紙PRODUCT_RISK_RESOLVE锛夐暱鏃堕棿鏃犲洖浼犲仛澶辫触鏀跺熬銆? */ @Service @RequiredArgsConstructor @@ -27,12 +29,15 @@ public class DeleteBrandStaleTaskService { private static final String MODULE_TYPE_DELETE_BRAND = "DELETE_BRAND"; private static final String MODULE_TYPE_PRODUCT_RISK = "PRODUCT_RISK_RESOLVE"; + private static final String MODULE_TYPE_SHOP_MATCH = "SHOP_MATCH"; private final FileTaskMapper fileTaskMapper; private final DeleteBrandTaskCacheService deleteBrandTaskCacheService; private final DeleteBrandRunService deleteBrandRunService; private final ProductRiskTaskService productRiskTaskService; private final ProductRiskTaskCacheService productRiskTaskCacheService; + private final ShopMatchTaskService shopMatchTaskService; + private final ShopMatchTaskCacheService shopMatchTaskCacheService; private final DeleteBrandProgressProperties deleteBrandProgressProperties; @Scheduled(cron = "${aiimage.delete-brand-progress.stale-check-cron:0 */2 * * * *}") @@ -41,6 +46,7 @@ public class DeleteBrandStaleTaskService { log.info("[stale-check] scan started thread={}", Thread.currentThread().getName()); failStaleDeleteBrandTasks(); ProductRiskStaleCheckStats stats = failStaleProductRiskResolveTasks(); + ShopMatchStaleCheckStats shopMatchStats = failStaleShopMatchTasks(); log.info("[stale-check] product-risk summary scanned={} finalized={} failed={} skipped={} elapsedMs={} thread={}", stats.scannedTaskCount, stats.finalizedTaskCount, @@ -48,12 +54,19 @@ public class DeleteBrandStaleTaskService { stats.skippedTaskCount, System.currentTimeMillis() - startedAt, Thread.currentThread().getName()); + log.info("[stale-check] shop-match summary scanned={} finalized={} failed={} skipped={} elapsedMs={} thread={}", + shopMatchStats.scannedTaskCount, + shopMatchStats.finalizedTaskCount, + shopMatchStats.failedTaskCount, + shopMatchStats.skippedTaskCount, + System.currentTimeMillis() - startedAt, + Thread.currentThread().getName()); } private void failStaleDeleteBrandTasks() { LocalDateTime threshold = LocalDateTime.now().minusMinutes(deleteBrandProgressProperties.getHeartbeatTimeoutMinutes()); - // 先按 DB updatedAt 粗筛,避免全表扫描 + // 鍏堟寜 DB updatedAt 绮楃瓫锛岄伩鍏嶅叏琛ㄦ壂鎻? java.util.List runningTasks = fileTaskMapper.selectList(new LambdaQueryWrapper() .eq(FileTaskEntity::getModuleType, MODULE_TYPE_DELETE_BRAND) .eq(FileTaskEntity::getStatus, "RUNNING") @@ -83,19 +96,19 @@ public class DeleteBrandStaleTaskService { } LocalDateTime lastActivityTime; - // 只有当 has_progress 为 true 且超过 15 分钟没心跳,才认为是异常卡死 + // 鍙湁褰?has_progress 涓?true 涓旇秴杩?15 鍒嗛挓娌″績璺筹紝鎵嶈涓烘槸寮傚父鍗℃ if (lastHeartbeatAt > 0 && isActive) { - // 有心跳,用最后心跳时间对比 15 分钟 (threshold 是 now - 15) + // 鏈夊績璺筹紝鐢ㄦ渶鍚庡績璺虫椂闂村姣?15 鍒嗛挓 (threshold 鏄?now - 15) lastActivityTime = LocalDateTime.ofInstant(Instant.ofEpochMilli(lastHeartbeatAt), ZoneId.systemDefault()); if (lastActivityTime.isAfter(threshold)) { - continue; // 没超时 + continue; // 娌¤秴鏃? } } else { - // 没有心跳,或者当前处在 has_progress: false (等待用户点击下一个任务推送的静默期) - // 静默期和排队期的宽限期一样,设为 12 个小时 + // 娌℃湁蹇冭烦锛屾垨鑰呭綋鍓嶅鍦?has_progress: false 锛堢瓑寰呯敤鎴风偣鍑讳笅涓€涓换鍔℃帹閫佺殑闈欓粯鏈燂級 + // 闈欓粯鏈熷拰鎺掗槦鏈熺殑瀹介檺鏈熶竴鏍凤紝璁句负 12 涓皬鏃? LocalDateTime queuedThreshold = LocalDateTime.now().minusHours(12); if (task.getCreatedAt() != null && task.getCreatedAt().isAfter(queuedThreshold)) { - continue; // 等待下个队列推送中,不要杀它! + continue; // 绛夊緟涓嬩釜闃熷垪鎺ㄩ€佷腑锛屼笉瑕佹潃瀹冿紒 } } @@ -104,7 +117,7 @@ public class DeleteBrandStaleTaskService { .eq(FileTaskEntity::getModuleType, MODULE_TYPE_DELETE_BRAND) .eq(FileTaskEntity::getStatus, "RUNNING") .set(FileTaskEntity::getStatus, "FAILED") - .set(FileTaskEntity::getErrorMessage, "结果回传长时间无响应,任务已自动失败") + .set(FileTaskEntity::getErrorMessage, "缁撴灉鍥炰紶闀挎椂闂存棤鍝嶅簲锛屼换鍔″凡鑷姩澶辫触") .set(FileTaskEntity::getUpdatedAt, LocalDateTime.now()) .set(FileTaskEntity::getFinishedAt, LocalDateTime.now())); @@ -157,7 +170,7 @@ public class DeleteBrandStaleTaskService { .eq(FileTaskEntity::getModuleType, MODULE_TYPE_PRODUCT_RISK) .eq(FileTaskEntity::getStatus, "RUNNING") .set(FileTaskEntity::getStatus, "FAILED") - .set(FileTaskEntity::getErrorMessage, "长时间未收到 Python 结果,任务已自动失败") + .set(FileTaskEntity::getErrorMessage, "闀挎椂闂存湭鏀跺埌 Python 缁撴灉锛屼换鍔″凡鑷姩澶辫触") .set(FileTaskEntity::getUpdatedAt, LocalDateTime.now()) .set(FileTaskEntity::getFinishedAt, LocalDateTime.now())); if (updated > 0) { @@ -170,6 +183,63 @@ public class DeleteBrandStaleTaskService { return stats; } + private ShopMatchStaleCheckStats failStaleShopMatchTasks() { + ShopMatchStaleCheckStats stats = new ShopMatchStaleCheckStats(); + long minutes = Math.max(1L, deleteBrandProgressProperties.getShopMatchStaleTimeoutMinutes()); + long initialMinutes = Math.max(minutes, deleteBrandProgressProperties.getShopMatchInitialTimeoutMinutes()); + LocalDateTime now = LocalDateTime.now(); + LocalDateTime threshold = now.minusMinutes(minutes); + LocalDateTime initialThreshold = now.minusMinutes(initialMinutes); + java.util.List runningTasks = fileTaskMapper.selectList(new LambdaQueryWrapper() + .eq(FileTaskEntity::getModuleType, MODULE_TYPE_SHOP_MATCH) + .eq(FileTaskEntity::getStatus, "RUNNING") + .lt(FileTaskEntity::getUpdatedAt, threshold) + .last("limit 200")); + stats.scannedTaskCount = runningTasks.size(); + if (runningTasks.isEmpty()) { + return stats; + } + log.info("[stale-check] shop-match candidates={} threshold={} timeoutMinutes={} initialThreshold={} initialTimeoutMinutes={}", + runningTasks.size(), threshold, minutes, initialThreshold, initialMinutes); + for (FileTaskEntity task : runningTasks) { + boolean hasUploadedPayload = !shopMatchTaskCacheService.getAllShopMergedPayload(task.getId()).isEmpty(); + boolean hasFinishedRows = (task.getSuccessFileCount() != null && task.getSuccessFileCount() > 0) + || (task.getFailedFileCount() != null && task.getFailedFileCount() > 0); + boolean hasStartedProgress = hasUploadedPayload || hasFinishedRows; + if (!hasStartedProgress && task.getCreatedAt() != null && task.getCreatedAt().isAfter(initialThreshold)) { + stats.skippedTaskCount++; + log.info("[stale-check] shop-match skip initial-grace taskId={} createdAt={} initialThreshold={}", + task.getId(), task.getCreatedAt(), initialThreshold); + continue; + } + try { + if (shopMatchTaskService.tryFinalizeTask(task.getId(), true)) { + stats.finalizedTaskCount++; + log.info("[stale-check] shop-match finalized taskId={}", task.getId()); + continue; + } + } catch (Exception ex) { + log.warn("[stale-check] shop-match finalize threw taskId={} msg={}", task.getId(), ex.getMessage()); + } + int updated = fileTaskMapper.update(null, new LambdaUpdateWrapper() + .eq(FileTaskEntity::getId, task.getId()) + .eq(FileTaskEntity::getModuleType, MODULE_TYPE_SHOP_MATCH) + .eq(FileTaskEntity::getStatus, "RUNNING") + .set(FileTaskEntity::getStatus, "FAILED") + .set(FileTaskEntity::getErrorMessage, "长时间未收到 Python 结果回传,任务已自动失败") + .set(FileTaskEntity::getUpdatedAt, LocalDateTime.now()) + .set(FileTaskEntity::getFinishedAt, LocalDateTime.now())); + if (updated > 0) { + stats.failedTaskCount++; + shopMatchTaskCacheService.deleteTaskCache(task.getId()); + log.warn("[stale-check] shop-match failed taskId={} reason=timeout", task.getId()); + } else { + stats.skippedTaskCount++; + } + } + return stats; + } + @Scheduled(cron = "${aiimage.delete-brand-progress.finalize-check-cron:30 */2 * * * *}") public void finalizeCompletedRunningTasks() { java.util.List runningTasks = fileTaskMapper.selectList(new LambdaQueryWrapper() @@ -182,7 +252,7 @@ public class DeleteBrandStaleTaskService { try { deleteBrandRunService.tryFinalizeTask(task.getId(), true); } catch (Exception ignored) { - // 定时补偿不影响主流程;下次调度或人工重试时再处理 + // 瀹氭椂琛ュ伩涓嶅奖鍝嶄富娴佺▼锛涗笅娆¤皟搴︽垨浜哄伐閲嶈瘯鏃跺啀澶勭悊 } } } @@ -193,4 +263,12 @@ public class DeleteBrandStaleTaskService { private int failedTaskCount; private int skippedTaskCount; } + + private static final class ShopMatchStaleCheckStats { + private int scannedTaskCount; + private int finalizedTaskCount; + private int failedTaskCount; + private int skippedTaskCount; + } } + diff --git a/backend-java/src/main/java/com/nanri/aiimage/modules/productrisk/model/vo/ProductRiskTaskItemVo.java b/backend-java/src/main/java/com/nanri/aiimage/modules/productrisk/model/vo/ProductRiskTaskItemVo.java index 971ce12..d63cce6 100644 --- a/backend-java/src/main/java/com/nanri/aiimage/modules/productrisk/model/vo/ProductRiskTaskItemVo.java +++ b/backend-java/src/main/java/com/nanri/aiimage/modules/productrisk/model/vo/ProductRiskTaskItemVo.java @@ -1,40 +1,60 @@ package com.nanri.aiimage.modules.productrisk.model.vo; import com.fasterxml.jackson.annotation.JsonProperty; +import com.nanri.aiimage.modules.shopmatch.model.vo.ShopMatchTaskStageVo; import io.swagger.v3.oas.annotations.media.Schema; import lombok.Data; +import java.util.ArrayList; +import java.util.List; + @Data -@Schema(description = "商品风险任务概要(biz_file_task 核心字段)") +@Schema(description = "Task summary") public class ProductRiskTaskItemVo { - @Schema(description = "任务主键", example = "200") + @Schema(description = "Task primary key", example = "200") private Long id; @JsonProperty("taskNo") - @Schema(description = "业务单号,如 PRODUCT_RISK_RESOLVE-雪花 id") + @Schema(description = "Task number") private String taskNo; - @Schema(description = "任务状态:RUNNING 执行中;SUCCESS 已结束且至少一店成功;FAILED 已结束且全部失败或整体失败") + @Schema(description = "Task status") private String status; @JsonProperty("errorMessage") - @Schema(description = "失败或部分失败时的汇总错误说明;成功且无附加信息时可能为空") + @Schema(description = "Task error message") private String errorMessage; @JsonProperty("createdAt") - @Schema(description = "创建时间(字符串格式,服务端 LocalDateTime.toString())") + @Schema(description = "Created at") private String createdAt; @JsonProperty("updatedAt") - @Schema(description = "最后更新时间") + @Schema(description = "Updated at") private String updatedAt; @JsonProperty("finishedAt") - @Schema(description = "结束时间;RUNNING 时通常为空") + @Schema(description = "Finished at") private String finishedAt; @JsonProperty("scheduledAt") - @Schema(description = "定时执行时间,未设置时为空") + @Schema(description = "Next scheduled execution time") private String scheduledAt; + + @JsonProperty("countryCodes") + @Schema(description = "Country codes used by shop match task") + private List countryCodes = new ArrayList<>(); + + @JsonProperty("scheduleStages") + @Schema(description = "Stage schedule of shop match task") + private List scheduleStages = new ArrayList<>(); + + @JsonProperty("currentStageIndex") + @Schema(description = "Next pending stage index") + private Integer currentStageIndex; + + @JsonProperty("activeStageIndex") + @Schema(description = "Current running stage index") + private Integer activeStageIndex; } diff --git a/backend-java/src/main/java/com/nanri/aiimage/modules/shopmatch/controller/ShopMatchController.java b/backend-java/src/main/java/com/nanri/aiimage/modules/shopmatch/controller/ShopMatchController.java index 63952d3..0e4d174 100644 --- a/backend-java/src/main/java/com/nanri/aiimage/modules/shopmatch/controller/ShopMatchController.java +++ b/backend-java/src/main/java/com/nanri/aiimage/modules/shopmatch/controller/ShopMatchController.java @@ -3,7 +3,6 @@ package com.nanri.aiimage.modules.shopmatch.controller; import com.nanri.aiimage.common.api.ApiResponse; import com.nanri.aiimage.modules.productrisk.model.dto.ProductRiskCandidateAddRequest; import com.nanri.aiimage.modules.productrisk.model.dto.ProductRiskCountryPreferenceSaveRequest; -import com.nanri.aiimage.modules.productrisk.model.dto.ProductRiskCreateTaskRequest; import com.nanri.aiimage.modules.productrisk.model.dto.ProductRiskMatchShopsRequest; import com.nanri.aiimage.modules.productrisk.model.dto.ProductRiskTaskBatchRequest; import com.nanri.aiimage.modules.productrisk.model.vo.ProductRiskCandidateVo; @@ -13,9 +12,15 @@ import com.nanri.aiimage.modules.productrisk.model.vo.ProductRiskDashboardVo; import com.nanri.aiimage.modules.productrisk.model.vo.ProductRiskHistoryVo; import com.nanri.aiimage.modules.productrisk.model.vo.ProductRiskMatchShopsVo; import com.nanri.aiimage.modules.productrisk.model.vo.ProductRiskTaskBatchVo; +import com.nanri.aiimage.modules.shopmatch.model.dto.ShopMatchCreateTaskRequest; +import com.nanri.aiimage.modules.shopmatch.model.dto.ShopMatchStageCompleteRequest; import com.nanri.aiimage.modules.shopmatch.model.dto.ShopMatchSubmitResultRequest; import com.nanri.aiimage.modules.shopmatch.service.ShopMatchResolveService; import com.nanri.aiimage.modules.shopmatch.service.ShopMatchTaskService; +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.Parameter; +import io.swagger.v3.oas.annotations.enums.ParameterIn; +import io.swagger.v3.oas.annotations.tags.Tag; import jakarta.validation.Valid; import lombok.RequiredArgsConstructor; import org.springframework.http.HttpHeaders; @@ -40,95 +45,143 @@ import java.util.List; @RestController @RequiredArgsConstructor @RequestMapping("/api/shop-match") +@Tag(name = "匹配店铺", description = "店铺候选、国家顺序、批量匹配、定时任务、Python 回传结果与下载接口。") public class ShopMatchController { private final ShopMatchResolveService shopMatchResolveService; private final ShopMatchTaskService shopMatchTaskService; @GetMapping("/candidates") - public ApiResponse> listCandidates(@RequestParam("user_id") Long userId) { + @Operation(summary = "查询候选店铺", description = "返回当前用户在匹配店铺模块下保存的候选店铺列表。") + public ApiResponse> listCandidates( + @Parameter(name = "user_id", description = "当前用户 ID", required = true, in = ParameterIn.QUERY, example = "1") + @RequestParam("user_id") Long userId) { return ApiResponse.success(shopMatchResolveService.listCandidates(userId)); } @PostMapping("/candidates") + @Operation(summary = "新增候选店铺", description = "新增一条候选店铺记录,供前端后续匹配与创建任务使用。") public ApiResponse addCandidate(@Valid @RequestBody ProductRiskCandidateAddRequest request) { return ApiResponse.success(shopMatchResolveService.addCandidate(request)); } @DeleteMapping("/candidates/{id}") - public ApiResponse deleteCandidate(@PathVariable Long id, @RequestParam("user_id") Long userId) { + @Operation(summary = "删除候选店铺", description = "删除当前用户名下的一条候选店铺记录。") + public ApiResponse deleteCandidate( + @Parameter(description = "候选店铺记录主键", example = "10") @PathVariable Long id, + @Parameter(name = "user_id", description = "当前用户 ID", required = true, in = ParameterIn.QUERY, example = "1") + @RequestParam("user_id") Long userId) { shopMatchResolveService.deleteCandidate(userId, id); return ApiResponse.success(null); } @GetMapping("/country-preference") - public ApiResponse getCountryPreference(@RequestParam("user_id") Long userId) { + @Operation(summary = "查询国家配置顺序", description = "返回当前用户保存的国家勾选与执行顺序。") + public ApiResponse getCountryPreference( + @Parameter(name = "user_id", description = "当前用户 ID", required = true, in = ParameterIn.QUERY, example = "1") + @RequestParam("user_id") Long userId) { return ApiResponse.success(shopMatchResolveService.getCountryPreference(userId)); } @PutMapping("/country-preference") + @Operation(summary = "保存国家配置顺序", description = "保存当前用户在匹配店铺模块中的国家勾选与执行顺序。") public ApiResponse saveCountryPreference(@Valid @RequestBody ProductRiskCountryPreferenceSaveRequest request) { return ApiResponse.success(shopMatchResolveService.saveCountryPreference(request)); } @PostMapping("/match-shops") + @Operation(summary = "批量匹配店铺", description = "按店铺名批量匹配紫鸟索引,返回店铺 ID、平台、公司与匹配状态。") public ApiResponse matchShops(@Valid @RequestBody ProductRiskMatchShopsRequest request) { return ApiResponse.success(shopMatchResolveService.matchShops(request)); } @GetMapping("/dashboard") - public ApiResponse dashboard(@RequestParam("user_id") Long userId) { + @Operation(summary = "统计看板", description = "返回候选店铺数、已结束任务数、成功数和失败数。") + public ApiResponse dashboard( + @Parameter(name = "user_id", description = "当前用户 ID", required = true, in = ParameterIn.QUERY, example = "1") + @RequestParam("user_id") Long userId) { return ApiResponse.success(shopMatchTaskService.dashboard(userId)); } @GetMapping("/history") - public ApiResponse history(@RequestParam("user_id") Long userId) { + @Operation(summary = "查询任务记录", description = "返回当前用户的运行中与历史任务记录。") + public ApiResponse history( + @Parameter(name = "user_id", description = "当前用户 ID", required = true, in = ParameterIn.QUERY, example = "1") + @RequestParam("user_id") Long userId) { return ApiResponse.success(shopMatchTaskService.listHistory(userId)); } @DeleteMapping("/history/{resultId}") - public ApiResponse deleteHistory(@PathVariable Long resultId, @RequestParam("user_id") Long userId) { + @Operation(summary = "删除结果记录", description = "删除一条任务结果记录,并同步重算任务状态。") + public ApiResponse deleteHistory( + @Parameter(description = "结果记录主键", example = "1001") @PathVariable Long resultId, + @Parameter(name = "user_id", description = "当前用户 ID", required = true, in = ParameterIn.QUERY, example = "1") + @RequestParam("user_id") Long userId) { shopMatchTaskService.deleteHistory(resultId, userId); return ApiResponse.success(null); } @PostMapping("/tasks") - public ApiResponse createTask(@Valid @RequestBody ProductRiskCreateTaskRequest request) { + @Operation(summary = "创建匹配店铺任务", description = "创建任务与各店铺占位结果,支持立即执行或多时间点定时执行。") + public ApiResponse createTask(@Valid @RequestBody ShopMatchCreateTaskRequest request) { return ApiResponse.success(shopMatchTaskService.createTask(request)); } @DeleteMapping("/tasks/{taskId}") - public ApiResponse deleteTask(@PathVariable Long taskId, @RequestParam("user_id") Long userId) { + @Operation(summary = "删除任务", description = "删除整条任务及其下所有结果记录。") + public ApiResponse deleteTask( + @Parameter(description = "任务主键", example = "200") @PathVariable Long taskId, + @Parameter(name = "user_id", description = "当前用户 ID", required = true, in = ParameterIn.QUERY, example = "1") + @RequestParam("user_id") Long userId) { shopMatchTaskService.deleteTask(taskId, userId); return ApiResponse.success(null); } @PostMapping("/tasks/{taskId}/activate") - public ApiResponse activateTask(@PathVariable Long taskId, @RequestParam("user_id") Long userId) { - shopMatchTaskService.activateTask(taskId, userId); + @Operation(summary = "激活定时阶段", description = "在某个定时阶段开始执行前,将任务切换到运行中状态。") + public ApiResponse activateTask( + @Parameter(description = "任务主键", example = "200") @PathVariable Long taskId, + @Parameter(name = "user_id", description = "当前用户 ID", required = true, in = ParameterIn.QUERY, example = "1") @RequestParam("user_id") Long userId, + @Parameter(name = "stage_index", description = "阶段下标,从 0 开始", required = true, in = ParameterIn.QUERY, example = "0") @RequestParam("stage_index") Integer stageIndex) { + shopMatchTaskService.activateTask(taskId, userId, stageIndex); + return ApiResponse.success(null); + } + + @PostMapping("/tasks/{taskId}/stage-finished") + @Operation(summary = "标记阶段完成", description = "某个定时阶段完成后推进到下一阶段,或等待最终收尾。") + public ApiResponse completeStage( + @Parameter(description = "任务主键", example = "200") @PathVariable Long taskId, + @Parameter(name = "user_id", description = "当前用户 ID", required = true, in = ParameterIn.QUERY, example = "1") @RequestParam("user_id") Long userId, + @Valid @RequestBody ShopMatchStageCompleteRequest request) { + shopMatchTaskService.completeStage(taskId, userId, request); return ApiResponse.success(null); } @PostMapping("/tasks/batch") + @Operation(summary = "批量查询任务详情", description = "供前端轮询任务状态与阶段进度使用。") public ApiResponse tasksBatch(@Valid @RequestBody ProductRiskTaskBatchRequest request) { return ApiResponse.success(shopMatchTaskService.getTaskDetailsBatch(request.getTaskIds())); } @PostMapping("/tasks/{taskId}/result") - public ApiResponse submitResult(@PathVariable Long taskId, @Valid @RequestBody ShopMatchSubmitResultRequest request) { + @Operation(summary = "提交匹配结果", description = "Python 可多次分批回传结果;后端会组装文件并在超时场景自动补偿收尾。") + public ApiResponse submitResult( + @Parameter(description = "任务主键", example = "200") @PathVariable Long taskId, + @Valid @RequestBody ShopMatchSubmitResultRequest request) { shopMatchTaskService.submitResult(taskId, request); return ApiResponse.success(null); } @GetMapping("/results/{resultId}/download") + @Operation(summary = "下载结果文件", description = "按结果记录下载后端已生成的 Excel 文件。") public void downloadResult( - @PathVariable Long resultId, - @RequestParam("user_id") Long userId, + @Parameter(description = "结果记录主键", example = "1001") @PathVariable Long resultId, + @Parameter(name = "user_id", description = "当前用户 ID", required = true, in = ParameterIn.QUERY, example = "1") @RequestParam("user_id") Long userId, jakarta.servlet.http.HttpServletResponse response) { String url = shopMatchTaskService.resolveResultDownloadUrl(resultId, userId); String filename = shopMatchTaskService.resolveResultDownloadFilename(resultId, userId); if (url == null || url.isBlank()) { - throw new ResponseStatusException(HttpStatus.NOT_FOUND, "暂无可下载结果"); + throw new ResponseStatusException(HttpStatus.NOT_FOUND, "No downloadable result"); } try { String encodedFilename = URLEncoder.encode(filename, StandardCharsets.UTF_8).replace("+", "%20"); @@ -144,7 +197,7 @@ public class ShopMatchController { response.getOutputStream().flush(); } } catch (Exception ex) { - throw new ResponseStatusException(HttpStatus.INTERNAL_SERVER_ERROR, "下载失败"); + throw new ResponseStatusException(HttpStatus.INTERNAL_SERVER_ERROR, "Download failed"); } } } diff --git a/backend-java/src/main/java/com/nanri/aiimage/modules/shopmatch/model/dto/ShopMatchCreateTaskRequest.java b/backend-java/src/main/java/com/nanri/aiimage/modules/shopmatch/model/dto/ShopMatchCreateTaskRequest.java new file mode 100644 index 0000000..4367eeb --- /dev/null +++ b/backend-java/src/main/java/com/nanri/aiimage/modules/shopmatch/model/dto/ShopMatchCreateTaskRequest.java @@ -0,0 +1,46 @@ +package com.nanri.aiimage.modules.shopmatch.model.dto; + +import com.fasterxml.jackson.annotation.JsonProperty; +import com.nanri.aiimage.modules.productrisk.model.vo.ProductRiskShopQueueItemVo; +import io.swagger.v3.oas.annotations.media.Schema; +import jakarta.validation.Valid; +import jakarta.validation.constraints.NotEmpty; +import jakarta.validation.constraints.NotNull; +import lombok.Data; + +import java.time.LocalDateTime; +import java.util.ArrayList; +import java.util.List; + +@Data +@Schema(description = "创建匹配店铺任务请求") +public class ShopMatchCreateTaskRequest { + + @NotNull(message = "user_id cannot be null") + @JsonProperty("user_id") + @Schema(description = "任务所属用户 ID", requiredMode = Schema.RequiredMode.REQUIRED, example = "1") + private Long userId; + + @NotEmpty(message = "items cannot be empty") + @Valid + @Schema(description = "已匹配店铺列表", requiredMode = Schema.RequiredMode.REQUIRED) + private List items = new ArrayList<>(); + + @NotEmpty(message = "country_codes cannot be empty") + @JsonProperty("country_codes") + @Schema(description = "每个阶段都要执行的国家代码列表", requiredMode = Schema.RequiredMode.REQUIRED) + private List countryCodes = new ArrayList<>(); + + @JsonProperty("schedule_times") + @Schema(description = "可选的多时间点执行时间;为空表示立即执行", example = "[\"2026-04-10T18:00:00\",\"2026-04-10T19:00:00\"]") + private List scheduleTimes = new ArrayList<>(); + + @JsonProperty("current_stage_index") + @Schema(description = "下一次待执行的阶段下标,由后端维护") + private Integer currentStageIndex; + + @JsonProperty("active_stage_index") + @Schema(description = "当前运行中的阶段下标,由后端维护") + private Integer activeStageIndex; +} + diff --git a/backend-java/src/main/java/com/nanri/aiimage/modules/shopmatch/model/dto/ShopMatchStageCompleteRequest.java b/backend-java/src/main/java/com/nanri/aiimage/modules/shopmatch/model/dto/ShopMatchStageCompleteRequest.java new file mode 100644 index 0000000..0f0963e --- /dev/null +++ b/backend-java/src/main/java/com/nanri/aiimage/modules/shopmatch/model/dto/ShopMatchStageCompleteRequest.java @@ -0,0 +1,17 @@ +package com.nanri.aiimage.modules.shopmatch.model.dto; + +import com.fasterxml.jackson.annotation.JsonProperty; +import io.swagger.v3.oas.annotations.media.Schema; +import jakarta.validation.constraints.NotNull; +import lombok.Data; + +@Data +@Schema(description = "匹配店铺任务阶段完成回调") +public class ShopMatchStageCompleteRequest { + + @NotNull(message = "stage_index cannot be null") + @JsonProperty("stage_index") + @Schema(description = "已完成的阶段下标,从 0 开始", requiredMode = Schema.RequiredMode.REQUIRED, example = "0") + private Integer stageIndex; +} + diff --git a/backend-java/src/main/java/com/nanri/aiimage/modules/shopmatch/model/vo/ShopMatchTaskStageVo.java b/backend-java/src/main/java/com/nanri/aiimage/modules/shopmatch/model/vo/ShopMatchTaskStageVo.java new file mode 100644 index 0000000..23414c2 --- /dev/null +++ b/backend-java/src/main/java/com/nanri/aiimage/modules/shopmatch/model/vo/ShopMatchTaskStageVo.java @@ -0,0 +1,22 @@ +package com.nanri.aiimage.modules.shopmatch.model.vo; + +import com.fasterxml.jackson.annotation.JsonProperty; +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; + +@Data +@Schema(description = "匹配店铺任务阶段信息") +public class ShopMatchTaskStageVo { + + @JsonProperty("stageIndex") + @Schema(description = "阶段下标,从 0 开始", example = "0") + private Integer stageIndex; + + @JsonProperty("scheduledAt") + @Schema(description = "该阶段的计划执行时间") + private String scheduledAt; + + @Schema(description = "阶段状态:SCHEDULED / RUNNING / COMPLETED / FAILED / PENDING") + private String status; +} + diff --git a/backend-java/src/main/java/com/nanri/aiimage/modules/shopmatch/service/ShopMatchTaskService.java b/backend-java/src/main/java/com/nanri/aiimage/modules/shopmatch/service/ShopMatchTaskService.java index 3c0c067..8fcad42 100644 --- a/backend-java/src/main/java/com/nanri/aiimage/modules/shopmatch/service/ShopMatchTaskService.java +++ b/backend-java/src/main/java/com/nanri/aiimage/modules/shopmatch/service/ShopMatchTaskService.java @@ -6,7 +6,7 @@ import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper; import com.fasterxml.jackson.databind.ObjectMapper; import com.nanri.aiimage.common.exception.BusinessException; import com.nanri.aiimage.modules.file.service.oss.OssStorageService; -import com.nanri.aiimage.modules.productrisk.model.dto.ProductRiskCreateTaskRequest; +import com.nanri.aiimage.modules.productrisk.model.enums.ProductRiskCountryCode; import com.nanri.aiimage.modules.productrisk.model.vo.ProductRiskCreateTaskVo; import com.nanri.aiimage.modules.productrisk.model.vo.ProductRiskDashboardVo; import com.nanri.aiimage.modules.productrisk.model.vo.ProductRiskHistoryVo; @@ -15,9 +15,12 @@ import com.nanri.aiimage.modules.productrisk.model.vo.ProductRiskShopQueueItemVo import com.nanri.aiimage.modules.productrisk.model.vo.ProductRiskTaskBatchVo; import com.nanri.aiimage.modules.productrisk.model.vo.ProductRiskTaskDetailVo; import com.nanri.aiimage.modules.productrisk.model.vo.ProductRiskTaskItemVo; +import com.nanri.aiimage.modules.shopmatch.model.vo.ShopMatchTaskStageVo; import com.nanri.aiimage.modules.shopmatch.mapper.ShopMatchShopCandidateMapper; +import com.nanri.aiimage.modules.shopmatch.model.dto.ShopMatchCreateTaskRequest; import com.nanri.aiimage.modules.shopmatch.model.dto.ShopMatchRowDto; import com.nanri.aiimage.modules.shopmatch.model.dto.ShopMatchShopPayloadDto; +import com.nanri.aiimage.modules.shopmatch.model.dto.ShopMatchStageCompleteRequest; import com.nanri.aiimage.modules.shopmatch.model.dto.ShopMatchSubmitResultRequest; import com.nanri.aiimage.modules.shopmatch.model.entity.ShopMatchShopCandidateEntity; import com.nanri.aiimage.modules.task.mapper.FileResultMapper; @@ -34,6 +37,7 @@ import java.io.File; import java.time.LocalDateTime; import java.util.ArrayList; import java.util.LinkedHashMap; +import java.util.LinkedHashSet; import java.util.List; import java.util.Locale; import java.util.Map; @@ -57,7 +61,7 @@ public class ShopMatchTaskService { public ProductRiskDashboardVo dashboard(Long userId) { if (userId == null || userId <= 0) { - throw new BusinessException("user_id 不合法"); + throw new BusinessException("user_id is invalid"); } ProductRiskDashboardVo vo = new ProductRiskDashboardVo(); vo.setCandidateCount(candidateMapper.selectCount(new LambdaQueryWrapper() @@ -82,7 +86,7 @@ public class ShopMatchTaskService { public ProductRiskHistoryVo listHistory(Long userId) { if (userId == null || userId <= 0) { - throw new BusinessException("user_id 不合法"); + throw new BusinessException("user_id is invalid"); } ProductRiskHistoryVo vo = new ProductRiskHistoryVo(); List entities = fileResultMapper.selectList(new LambdaQueryWrapper() @@ -114,11 +118,11 @@ public class ShopMatchTaskService { @Transactional public void deleteHistory(Long resultId, Long userId) { if (userId == null || userId <= 0) { - throw new BusinessException("user_id 不合法"); + throw new BusinessException("user_id is invalid"); } FileResultEntity entity = fileResultMapper.selectById(resultId); if (entity == null || !MODULE_TYPE.equals(entity.getModuleType()) || !userId.equals(entity.getUserId())) { - throw new BusinessException("记录不存在"); + throw new BusinessException("Invalid request"); } Long taskId = entity.getTaskId(); fileResultMapper.deleteById(resultId); @@ -130,11 +134,11 @@ public class ShopMatchTaskService { @Transactional public void deleteTask(Long taskId, Long userId) { if (userId == null || userId <= 0) { - throw new BusinessException("user_id 不合法"); + throw new BusinessException("user_id is invalid"); } FileTaskEntity task = fileTaskMapper.selectById(taskId); if (task == null || !MODULE_TYPE.equals(task.getModuleType()) || !userId.equals(task.getUserId())) { - throw new BusinessException("任务不存在"); + throw new BusinessException("Invalid request"); } fileResultMapper.delete(new LambdaQueryWrapper() .eq(FileResultEntity::getTaskId, taskId) @@ -182,10 +186,10 @@ public class ShopMatchTaskService { public String resolveResultDownloadUrl(Long resultId, Long userId) { FileResultEntity entity = fileResultMapper.selectById(resultId); if (entity == null || !MODULE_TYPE.equals(entity.getModuleType()) || !userId.equals(entity.getUserId())) { - throw new BusinessException("记录不存在"); + throw new BusinessException("Invalid request"); } if (entity.getResultFileUrl() == null || entity.getResultFileUrl().isBlank()) { - throw new BusinessException("暂无可下载文件"); + throw new BusinessException("Invalid request"); } return ossStorageService.generateFreshDownloadUrl(entity.getResultFileUrl()); } @@ -193,7 +197,7 @@ public class ShopMatchTaskService { public String resolveResultDownloadFilename(Long resultId, Long userId) { FileResultEntity entity = fileResultMapper.selectById(resultId); if (entity == null || !MODULE_TYPE.equals(entity.getModuleType()) || !userId.equals(entity.getUserId())) { - throw new BusinessException("记录不存在"); + throw new BusinessException("Invalid request"); } return entity.getResultFilename() != null && !entity.getResultFilename().isBlank() ? entity.getResultFilename() @@ -201,34 +205,41 @@ public class ShopMatchTaskService { } @Transactional - public ProductRiskCreateTaskVo createTask(ProductRiskCreateTaskRequest request) { + public ProductRiskCreateTaskVo createTask(ShopMatchCreateTaskRequest request) { if (request.getUserId() == null || request.getUserId() <= 0) { - throw new BusinessException("user_id 不合法"); + throw new BusinessException("user_id is invalid"); } if (request.getItems() == null || request.getItems().isEmpty()) { - throw new BusinessException("items 不能为空"); + throw new BusinessException("items 涓嶈兘涓虹┖"); } List uniqueItems = dedupeQueueItems(request.getItems()); if (uniqueItems.isEmpty()) { - throw new BusinessException("items 不能为空"); + throw new BusinessException("items 涓嶈兘涓虹┖"); } for (ProductRiskShopQueueItemVo item : uniqueItems) { if (item == null || !item.isMatched()) { - throw new BusinessException("存在未匹配店铺,无法创建任务"); + throw new BusinessException("瀛樺湪鏈尮閰嶅簵閾猴紝鏃犳硶鍒涘缓浠诲姟"); } } + List countryCodes = normalizeCountryCodes(request.getCountryCodes()); + List scheduleTimes = normalizeScheduleTimes(request.getScheduleTimes()); LocalDateTime now = LocalDateTime.now(); - LocalDateTime scheduledAt = request.getScheduleTime(); - if (scheduledAt != null && !scheduledAt.isAfter(now)) { - throw new BusinessException("schedule_time 必须晚于当前时间"); + + ShopMatchCreateTaskRequest persistedRequest = new ShopMatchCreateTaskRequest(); + persistedRequest.setUserId(request.getUserId()); + persistedRequest.setItems(uniqueItems); + persistedRequest.setCountryCodes(countryCodes); + persistedRequest.setScheduleTimes(scheduleTimes); + if (!scheduleTimes.isEmpty()) { + persistedRequest.setCurrentStageIndex(0); } FileTaskEntity task = new FileTaskEntity(); task.setTaskNo(MODULE_TYPE + "-" + IdUtil.getSnowflakeNextIdStr()); task.setModuleType(MODULE_TYPE); task.setTaskMode("PYTHON_QUEUE"); - task.setStatus(scheduledAt != null ? "SCHEDULED" : "RUNNING"); + task.setStatus(scheduleTimes.isEmpty() ? "RUNNING" : "SCHEDULED"); task.setSourceFileCount(uniqueItems.size()); task.setSuccessFileCount(0); task.setFailedFileCount(0); @@ -236,14 +247,14 @@ public class ShopMatchTaskService { task.setUserId(request.getUserId()); task.setCreatedAt(now); task.setUpdatedAt(now); - task.setScheduledAt(scheduledAt); + task.setScheduledAt(scheduleTimes.isEmpty() ? null : scheduleTimes.get(0)); fileTaskMapper.insert(task); List snapshot = new ArrayList<>(); for (ProductRiskShopQueueItemVo item : uniqueItems) { String normalized = ziniaoShopSwitchService.normalizeShopName(item.getShopName()); if (normalized.isBlank()) { - throw new BusinessException("店铺名无效"); + throw new BusinessException("Invalid request"); } FileResultEntity result = new FileResultEntity(); result.setTaskId(task.getId()); @@ -257,10 +268,10 @@ public class ShopMatchTaskService { snapshot.add(toSnapshotVo(result, item, task.getStatus())); } try { - task.setRequestJson(objectMapper.writeValueAsString(request)); + task.setRequestJson(objectMapper.writeValueAsString(persistedRequest)); task.setResultJson(objectMapper.writeValueAsString(snapshot)); } catch (Exception ex) { - throw new BusinessException("序列化任务数据失败"); + throw new BusinessException("Invalid request"); } fileTaskMapper.updateById(task); @@ -271,49 +282,96 @@ public class ShopMatchTaskService { } @Transactional - public void activateTask(Long taskId, Long userId) { + public void activateTask(Long taskId, Long userId, Integer stageIndex) { if (taskId == null || taskId <= 0) { - throw new BusinessException("taskId 不合法"); + throw new BusinessException("taskId is invalid"); } if (userId == null || userId <= 0) { - throw new BusinessException("user_id 不合法"); + throw new BusinessException("user_id is invalid"); + } + if (stageIndex == null || stageIndex < 0) { + throw new BusinessException("stage_index is invalid"); } FileTaskEntity task = fileTaskMapper.selectById(taskId); if (task == null || !MODULE_TYPE.equals(task.getModuleType()) || !userId.equals(task.getUserId())) { - throw new BusinessException("任务不存在"); + throw new BusinessException("Invalid request"); } if ("SUCCESS".equals(task.getStatus()) || "FAILED".equals(task.getStatus())) { - throw new BusinessException("任务已结束"); - } - if ("RUNNING".equals(task.getStatus())) { - return; + throw new BusinessException("Invalid request"); } if (!"SCHEDULED".equals(task.getStatus())) { - throw new BusinessException("当前任务状态不允许激活"); + throw new BusinessException("Invalid request"); } if (task.getScheduledAt() != null && LocalDateTime.now().isBefore(task.getScheduledAt())) { - throw new BusinessException("未到定时执行时间"); + throw new BusinessException("鏈埌瀹氭椂鎵ц鏃堕棿"); } + ShopMatchCreateTaskRequest state = parseTaskRequest(task); + int currentStageIndex = state.getCurrentStageIndex() == null ? 0 : state.getCurrentStageIndex(); + if (currentStageIndex != stageIndex) { + throw new BusinessException("褰撳墠杞宸插彉鍖栵紝璇峰埛鏂板悗閲嶈瘯"); + } + if (state.getScheduleTimes() == null || stageIndex >= state.getScheduleTimes().size()) { + throw new BusinessException("Invalid request"); + } + state.setActiveStageIndex(stageIndex); task.setStatus("RUNNING"); task.setUpdatedAt(LocalDateTime.now()); + persistTaskRequest(task, state); fileTaskMapper.updateById(task); } + @Transactional + public void completeStage(Long taskId, Long userId, ShopMatchStageCompleteRequest request) { + if (taskId == null || taskId <= 0) { + throw new BusinessException("taskId is invalid"); + } + if (userId == null || userId <= 0) { + throw new BusinessException("user_id is invalid"); + } + if (request == null || request.getStageIndex() == null || request.getStageIndex() < 0) { + throw new BusinessException("stage_index is invalid"); + } + FileTaskEntity task = fileTaskMapper.selectById(taskId); + if (task == null || !MODULE_TYPE.equals(task.getModuleType()) || !userId.equals(task.getUserId())) { + throw new BusinessException("Invalid request"); + } + if (!"RUNNING".equals(task.getStatus())) { + throw new BusinessException("Invalid request"); + } + ShopMatchCreateTaskRequest state = parseTaskRequest(task); + if (state.getActiveStageIndex() == null || !state.getActiveStageIndex().equals(request.getStageIndex())) { + throw new BusinessException("杞鐘舵€佷笉鍖归厤"); + } + int nextIndex = request.getStageIndex() + 1; + if (state.getScheduleTimes() == null || nextIndex >= state.getScheduleTimes().size()) { + throw new BusinessException("Current stage is already the final stage"); + } + state.setCurrentStageIndex(nextIndex); + state.setActiveStageIndex(null); + task.setStatus("SCHEDULED"); + task.setScheduledAt(state.getScheduleTimes().get(nextIndex)); + task.setUpdatedAt(LocalDateTime.now()); + task.setFinishedAt(null); + persistTaskRequest(task, state); + fileTaskMapper.updateById(task); + } + + @Transactional public void submitResult(Long taskId, ShopMatchSubmitResultRequest request) { if (taskId == null || taskId <= 0) { - throw new BusinessException("taskId 不合法"); + throw new BusinessException("taskId is invalid"); } if (request == null || request.getShops() == null || request.getShops().isEmpty()) { - throw new BusinessException("shops 不能为空"); + throw new BusinessException("shops 涓嶈兘涓虹┖"); } FileTaskEntity task = fileTaskMapper.selectById(taskId); if (task == null || !MODULE_TYPE.equals(task.getModuleType())) { - throw new BusinessException("任务不存在"); + throw new BusinessException("Invalid request"); } if ("SUCCESS".equals(task.getStatus()) || "FAILED".equals(task.getStatus())) { - throw new BusinessException("任务已结束,拒绝重复提交"); + throw new BusinessException("浠诲姟宸茬粨鏉燂紝鎷掔粷閲嶅鎻愪氦"); } List resultRows = fileResultMapper.selectList(new LambdaQueryWrapper() @@ -350,7 +408,7 @@ public class ShopMatchTaskService { continue; } if (countPayloadRows(merged) <= 0) { - markResultFailed(result, "未收到可组装的结果数据"); + markResultFailed(result, "No result rows received for assembly"); shopMatchTaskCacheService.removeShopMergedPayload(taskId, shopKey); continue; } @@ -373,6 +431,102 @@ public class ShopMatchTaskService { updateTaskStatusFromLatestRows(task, latest); } + @Transactional + public boolean tryFinalizeTask(Long taskId, boolean fromCompensation) { + if (taskId == null || taskId <= 0) { + return false; + } + FileTaskEntity task = fileTaskMapper.selectById(taskId); + if (task == null || !MODULE_TYPE.equals(task.getModuleType())) { + return false; + } + if ("SUCCESS".equals(task.getStatus()) || "FAILED".equals(task.getStatus())) { + return true; + } + + List resultRows = fileResultMapper.selectList(new LambdaQueryWrapper() + .eq(FileResultEntity::getTaskId, taskId) + .eq(FileResultEntity::getModuleType, MODULE_TYPE) + .orderByAsc(FileResultEntity::getId)); + if (resultRows.isEmpty()) { + return false; + } + + Map cachedPayloadByShop = shopMatchTaskCacheService.getAllShopMergedPayload(taskId); + File workRoot = FileUtil.mkdir(FileUtil.file(System.getProperty("java.io.tmpdir"), "shop-match-result", String.valueOf(taskId))); + List batchErrors = new ArrayList<>(); + boolean changed = false; + + for (FileResultEntity result : resultRows) { + boolean success = result.getSuccess() != null && result.getSuccess() == 1; + boolean failed = result.getErrorMessage() != null && !result.getErrorMessage().isBlank(); + if (success || failed) { + continue; + } + + String shopKey = result.getSourceFilename(); + ShopMatchShopPayloadDto cachedPayload = shopKey == null ? null : cachedPayloadByShop.get(shopKey); + + if (cachedPayload == null) { + markResultFailed(result, "Python interrupted before this shop finished uploading results"); + batchErrors.add(shopKey + ": Python interrupted before this shop finished uploading results"); + changed = true; + continue; + } + + if (cachedPayload.getError() != null && !cachedPayload.getError().isBlank()) { + markResultFailed(result, cachedPayload.getError()); + batchErrors.add(shopKey + ": " + cachedPayload.getError()); + shopMatchTaskCacheService.removeShopMergedPayload(taskId, shopKey); + changed = true; + continue; + } + + if (countPayloadRows(cachedPayload) <= 0 && payloadHasAnyDoneTrue(cachedPayload)) { + markResultFailed(result, "No result rows received for assembly"); + shopMatchTaskCacheService.removeShopMergedPayload(taskId, shopKey); + changed = true; + log.warn("[shop-match] stale finalize finished without data taskId={} shop={} fromCompensation={}", + taskId, shopKey, fromCompensation); + continue; + } + + if (countPayloadRows(cachedPayload) <= 0) { + markResultFailed(result, "Python interrupted before any assembleable rows were uploaded"); + batchErrors.add(shopKey + ": Python interrupted before any assembleable rows were uploaded"); + shopMatchTaskCacheService.removeShopMergedPayload(taskId, shopKey); + changed = true; + continue; + } + + try { + assembleShopResult(result, shopKey, cachedPayload, workRoot); + shopMatchTaskCacheService.removeShopMergedPayload(taskId, shopKey); + changed = true; + log.warn("[shop-match] stale finalize assembled taskId={} shop={} fromCompensation={} completedFlag={}", + taskId, shopKey, fromCompensation, isShopPayloadCompleted(cachedPayload)); + } catch (Exception ex) { + String message = ex.getMessage() == null ? "assemble failed" : ex.getMessage(); + markResultFailed(result, message); + batchErrors.add(shopKey + ": " + message); + shopMatchTaskCacheService.removeShopMergedPayload(taskId, shopKey); + changed = true; + log.warn("[shop-match] stale finalize failed taskId={} shop={} msg={}", taskId, shopKey, message); + } + } + + if (!changed && cachedPayloadByShop.isEmpty()) { + return false; + } + + List latest = fileResultMapper.selectList(new LambdaQueryWrapper() + .eq(FileResultEntity::getTaskId, taskId) + .eq(FileResultEntity::getModuleType, MODULE_TYPE) + .orderByAsc(FileResultEntity::getId)); + updateTaskStatusFromLatestRows(task, latest, batchErrors); + return true; + } + private void assembleShopResult(FileResultEntity result, String shopKey, ShopMatchShopPayloadDto payload, File workRoot) { Map> countries = excelAssemblyService.normalizeCountriesMap(payload.getCountries()); String displayName = payload.getShopName() != null && !payload.getShopName().isBlank() ? payload.getShopName().trim() : shopKey; @@ -406,6 +560,12 @@ public class ShopMatchTaskService { } private void updateTaskStatusFromLatestRows(FileTaskEntity task, List latest) { + updateTaskStatusFromLatestRows(task, latest, List.of()); + } + + private void updateTaskStatusFromLatestRows(FileTaskEntity task, + List latest, + List batchErrors) { int ok = 0; int fail = 0; List errors = new ArrayList<>(); @@ -435,7 +595,7 @@ public class ShopMatchTaskService { task.setFinishedAt(LocalDateTime.now()); } else if (ok > 0) { task.setStatus("SUCCESS"); - task.setErrorMessage(String.join("; ", errors)); + task.setErrorMessage(String.join("; ", errors.isEmpty() ? batchErrors : errors)); task.setFinishedAt(LocalDateTime.now()); } else { task.setStatus("FAILED"); @@ -473,7 +633,7 @@ public class ShopMatchTaskService { .orderByAsc(FileResultEntity::getId)); List items = new ArrayList<>(); for (FileResultEntity row : rows) { - items.add(toHistoryItemVo(row, taskStatus)); + throw new BusinessException("Task request payload is missing"); } return items; } @@ -488,10 +648,20 @@ public class ShopMatchTaskService { vo.setUpdatedAt(fmt(task.getUpdatedAt())); vo.setFinishedAt(fmt(task.getFinishedAt())); vo.setScheduledAt(fmt(task.getScheduledAt())); + + ShopMatchCreateTaskRequest request = parseTaskRequestSilently(task); + if (request != null) { + vo.setCountryCodes(new ArrayList<>(request.getCountryCodes() == null ? List.of() : request.getCountryCodes())); + vo.setCurrentStageIndex(request.getCurrentStageIndex()); + vo.setActiveStageIndex(request.getActiveStageIndex()); + vo.setScheduleStages(buildStageVos(task, request)); + } return vo; } - private ProductRiskResultItemVo toSnapshotVo(FileResultEntity result, ProductRiskShopQueueItemVo item, String taskStatus) { + private ProductRiskResultItemVo toSnapshotVo(FileResultEntity result, + ProductRiskShopQueueItemVo item, + String taskStatus) { ProductRiskResultItemVo vo = new ProductRiskResultItemVo(); vo.setResultId(result.getId()); vo.setTaskId(result.getTaskId()); @@ -503,8 +673,10 @@ public class ShopMatchTaskService { vo.setMatchStatus(item.getMatchStatus()); vo.setMatchMessage(item.getMatchMessage()); vo.setTaskStatus(taskStatus); - vo.setScheduledAt(fmt(result.getTaskId() == null ? null : loadScheduledAt(result.getTaskId()))); vo.setSuccess(false); + vo.setError(null); + vo.setOutputFilename(null); + vo.setDownloadUrl(null); return vo; } @@ -515,22 +687,66 @@ public class ShopMatchTaskService { vo.setShopName(entity.getSourceFilename()); vo.setShopId(entity.getSourceFileUrl()); vo.setTaskStatus(taskStatus); - vo.setSuccess(entity.getSuccess() != null && entity.getSuccess() == 1); + boolean success = entity.getSuccess() != null && entity.getSuccess() == 1; + vo.setSuccess(success); vo.setError(entity.getErrorMessage()); vo.setOutputFilename(entity.getResultFilename()); vo.setDownloadUrl(entity.getResultFileUrl() == null || entity.getResultFileUrl().isBlank() ? null : ossStorageService.generateFreshDownloadUrl(entity.getResultFileUrl())); vo.setMatched(true); - if (entity.getTaskId() != null) { - vo.setScheduledAt(fmt(loadScheduledAt(entity.getTaskId()))); - } if (entity.getTaskId() != null) { mergeQueueFieldsFromRequest(vo, entity.getTaskId()); } return vo; } + private List buildStageVos(FileTaskEntity task, ShopMatchCreateTaskRequest request) { + List stages = new ArrayList<>(); + List scheduleTimes = request.getScheduleTimes() == null ? List.of() : request.getScheduleTimes(); + if (scheduleTimes.isEmpty()) { + return stages; + } + Integer currentStageIndex = request.getCurrentStageIndex(); + Integer activeStageIndex = request.getActiveStageIndex(); + for (int i = 0; i < scheduleTimes.size(); i++) { + ShopMatchTaskStageVo stage = new ShopMatchTaskStageVo(); + stage.setStageIndex(i); + stage.setScheduledAt(fmt(scheduleTimes.get(i))); + if ("SUCCESS".equals(task.getStatus())) { + stage.setStatus("COMPLETED"); + } else if ("FAILED".equals(task.getStatus())) { + if (activeStageIndex != null && activeStageIndex == i) { + stage.setStatus("FAILED"); + } else if (currentStageIndex != null && currentStageIndex > i) { + stage.setStatus("COMPLETED"); + } else { + stage.setStatus("PENDING"); + } + } else if ("RUNNING".equals(task.getStatus()) && activeStageIndex != null) { + if (i < activeStageIndex) { + stage.setStatus("COMPLETED"); + } else if (i == activeStageIndex) { + stage.setStatus("RUNNING"); + } else { + stage.setStatus("PENDING"); + } + } else if ("SCHEDULED".equals(task.getStatus()) && currentStageIndex != null) { + if (i < currentStageIndex) { + stage.setStatus("COMPLETED"); + } else if (i == currentStageIndex) { + stage.setStatus("SCHEDULED"); + } else { + stage.setStatus("PENDING"); + } + } else { + stage.setStatus("PENDING"); + } + stages.add(stage); + } + return stages; + } + private LocalDateTime loadScheduledAt(Long taskId) { FileTaskEntity task = fileTaskMapper.selectById(taskId); return task == null ? null : task.getScheduledAt(); @@ -538,27 +754,60 @@ public class ShopMatchTaskService { private void mergeQueueFieldsFromRequest(ProductRiskResultItemVo vo, Long taskId) { FileTaskEntity task = fileTaskMapper.selectById(taskId); - if (task == null || task.getRequestJson() == null || task.getRequestJson().isBlank()) { + ShopMatchCreateTaskRequest request = parseTaskRequestSilently(task); + if (request == null) { return; } - try { - ProductRiskCreateTaskRequest request = objectMapper.readValue(task.getRequestJson(), ProductRiskCreateTaskRequest.class); - String key = ziniaoShopSwitchService.normalizeShopName(vo.getShopName()); - for (ProductRiskShopQueueItemVo item : request.getItems()) { - if (item == null) { - continue; - } - if (key.equals(ziniaoShopSwitchService.normalizeShopName(item.getShopName()))) { - vo.setShopName(item.getShopName()); - vo.setShopId(item.getShopId()); - vo.setPlatform(item.getPlatform()); - vo.setCompanyName(item.getCompanyName()); - vo.setMatchStatus(item.getMatchStatus()); - vo.setMatchMessage(item.getMatchMessage()); - break; - } + String key = ziniaoShopSwitchService.normalizeShopName(vo.getShopName()); + for (ProductRiskShopQueueItemVo item : request.getItems()) { + if (item == null) { + continue; } + if (key.equals(ziniaoShopSwitchService.normalizeShopName(item.getShopName()))) { + vo.setShopName(item.getShopName()); + vo.setShopId(item.getShopId()); + vo.setPlatform(item.getPlatform()); + vo.setCompanyName(item.getCompanyName()); + vo.setMatchStatus(item.getMatchStatus()); + vo.setMatchMessage(item.getMatchMessage()); + break; + } + } + } + + private ShopMatchCreateTaskRequest parseTaskRequest(FileTaskEntity task) { + ShopMatchCreateTaskRequest request = parseTaskRequestSilently(task); + if (request == null) { + throw new BusinessException("浠诲姟閰嶇疆缂哄け"); + } + if (request.getItems() == null) { + request.setItems(new ArrayList<>()); + } + if (request.getCountryCodes() == null) { + request.setCountryCodes(new ArrayList<>()); + } + if (request.getScheduleTimes() == null) { + request.setScheduleTimes(new ArrayList<>()); + } + return request; + } + + private ShopMatchCreateTaskRequest parseTaskRequestSilently(FileTaskEntity task) { + if (task == null || task.getRequestJson() == null || task.getRequestJson().isBlank()) { + return null; + } + try { + return objectMapper.readValue(task.getRequestJson(), ShopMatchCreateTaskRequest.class); } catch (Exception ignored) { + return null; + } + } + + private void persistTaskRequest(FileTaskEntity task, ShopMatchCreateTaskRequest request) { + try { + task.setRequestJson(objectMapper.writeValueAsString(request)); + } catch (Exception ex) { + throw new BusinessException("Invalid request"); } } @@ -580,6 +829,55 @@ public class ShopMatchTaskService { return new ArrayList<>(byNorm.values()); } + private List normalizeCountryCodes(List raw) { + if (raw == null || raw.isEmpty()) { + throw new BusinessException("country_codes 涓嶈兘涓虹┖"); + } + LinkedHashSet out = new LinkedHashSet<>(); + for (String code : raw) { + if (code == null || code.isBlank()) { + continue; + } + String normalized = code.trim().toUpperCase(Locale.ROOT); + try { + ProductRiskCountryCode.valueOf(normalized); + } catch (IllegalArgumentException ex) { + throw new BusinessException("闈炴硶鍥藉浠g爜: " + code); + } + out.add(normalized); + } + if (out.isEmpty()) { + throw new BusinessException("country_codes 涓嶈兘涓虹┖"); + } + return new ArrayList<>(out); + } + + private List normalizeScheduleTimes(List raw) { + if (raw == null || raw.isEmpty()) { + return List.of(); + } + List out = new ArrayList<>(); + LocalDateTime now = LocalDateTime.now(); + LocalDateTime previous = null; + for (LocalDateTime time : raw) { + if (time == null) { + continue; + } + if (!time.isAfter(now)) { + throw new BusinessException("schedule_times 蹇呴』鏅氫簬褰撳墠鏃堕棿"); + } + if (previous != null && !time.isAfter(previous)) { + throw new BusinessException("schedule_times 蹇呴』鎸夋椂闂撮€掑"); + } + out.add(time); + previous = time; + } + if (out.isEmpty()) { + throw new BusinessException("schedule_times 涓嶈兘涓虹┖"); + } + return out; + } + private static String safeFileStem(String shopName) { String value = shopName == null ? "shop" : shopName.trim(); if (value.isBlank()) { @@ -716,3 +1014,5 @@ public class ShopMatchTaskService { return row == null || row.getAsin() == null || row.getAsin().trim().isEmpty(); } } + + diff --git a/backend-java/src/main/java/com/nanri/aiimage/modules/task/service/ModuleHistoryCleanupService.java b/backend-java/src/main/java/com/nanri/aiimage/modules/task/service/ModuleHistoryCleanupService.java index ee9471f..17177b4 100644 --- a/backend-java/src/main/java/com/nanri/aiimage/modules/task/service/ModuleHistoryCleanupService.java +++ b/backend-java/src/main/java/com/nanri/aiimage/modules/task/service/ModuleHistoryCleanupService.java @@ -13,13 +13,18 @@ import org.springframework.scheduling.annotation.Scheduled; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; +import java.util.ArrayList; import java.util.List; +import java.util.Locale; +import java.util.Set; @Slf4j @Service @RequiredArgsConstructor public class ModuleHistoryCleanupService { + private static final Set TERMINAL_STATUSES = Set.of("SUCCESS", "FAILED", "CANCELLED", "CANCELED"); + private final ModuleCleanupProperties moduleCleanupProperties; private final FileTaskMapper fileTaskMapper; private final FileResultMapper fileResultMapper; @@ -35,18 +40,50 @@ public class ModuleHistoryCleanupService { return; } + List moduleTasks = fileTaskMapper.selectList(new LambdaQueryWrapper() + .in(FileTaskEntity::getModuleType, moduleTypes) + .select(FileTaskEntity::getId, FileTaskEntity::getModuleType, FileTaskEntity::getStatus)); + + List cleanupTaskIds = new ArrayList<>(); + List skippedActiveTaskIds = new ArrayList<>(); + for (FileTaskEntity task : moduleTasks) { + if (task == null || task.getId() == null) { + continue; + } + if (isTerminalStatus(task.getStatus())) { + cleanupTaskIds.add(task.getId()); + } else { + skippedActiveTaskIds.add(task.getId()); + } + } + + if (cleanupTaskIds.isEmpty()) { + log.info("模块历史清理跳过: moduleTypes={}, activeTaskIds={}, reason=no-terminal-tasks", + moduleTypes, skippedActiveTaskIds); + return; + } + int deletedResults = fileResultMapper.delete(new LambdaQueryWrapper() - .in(FileResultEntity::getModuleType, moduleTypes)); + .in(FileResultEntity::getModuleType, moduleTypes) + .in(FileResultEntity::getTaskId, cleanupTaskIds)); int resetTasks = fileTaskMapper.update(null, new LambdaUpdateWrapper() - .in(FileTaskEntity::getModuleType, moduleTypes) + .in(FileTaskEntity::getId, cleanupTaskIds) .set(FileTaskEntity::getResultJson, null) .set(FileTaskEntity::getRequestJson, null) .set(FileTaskEntity::getErrorMessage, null)); int deletedTasks = fileTaskMapper.delete(new LambdaQueryWrapper() - .in(FileTaskEntity::getModuleType, moduleTypes)); + .in(FileTaskEntity::getId, cleanupTaskIds)); - log.info("模块历史清理完成: moduleTypes={}, deletedResults={}, resetTasks={}, deletedTasks={}", moduleTypes, deletedResults, resetTasks, deletedTasks); + log.info("模块历史清理完成: moduleTypes={}, deletedResults={}, resetTasks={}, deletedTasks={}, skippedActiveTaskIds={}", + moduleTypes, deletedResults, resetTasks, deletedTasks, skippedActiveTaskIds); + } + + private boolean isTerminalStatus(String status) { + if (status == null || status.isBlank()) { + return false; + } + return TERMINAL_STATUSES.contains(status.trim().toUpperCase(Locale.ROOT)); } } diff --git a/backend-java/src/main/resources/application.yml b/backend-java/src/main/resources/application.yml index 4113b6d..e92b5b7 100644 --- a/backend-java/src/main/resources/application.yml +++ b/backend-java/src/main/resources/application.yml @@ -84,6 +84,8 @@ aiimage: finalize-check-cron: ${AIIMAGE_DELETE_BRAND_FINALIZE_CHECK_CRON:30 */2 * * * *} product-risk-stale-timeout-minutes: ${AIIMAGE_PRODUCT_RISK_STALE_TIMEOUT_MINUTES:10} product-risk-initial-timeout-minutes: ${AIIMAGE_PRODUCT_RISK_INITIAL_TIMEOUT_MINUTES:10} + shop-match-stale-timeout-minutes: ${AIIMAGE_SHOP_MATCH_STALE_TIMEOUT_MINUTES:10} + shop-match-initial-timeout-minutes: ${AIIMAGE_SHOP_MATCH_INITIAL_TIMEOUT_MINUTES:10} module-cleanup: enabled: ${AIIMAGE_MODULE_CLEANUP_ENABLED:true} cron: ${AIIMAGE_MODULE_CLEANUP_CRON:0 0 0 * * *} diff --git a/frontend-vue/src/pages/brand/components/BrandShopMatchTab.vue b/frontend-vue/src/pages/brand/components/BrandShopMatchTab.vue index 23ee183..2d52ca5 100644 --- a/frontend-vue/src/pages/brand/components/BrandShopMatchTab.vue +++ b/frontend-vue/src/pages/brand/components/BrandShopMatchTab.vue @@ -1,233 +1,95 @@ -