From 025ca6d4fd120dae82999b3107dee6db7949d882 Mon Sep 17 00:00:00 2001 From: super <2903208875@qq.com> Date: Tue, 14 Apr 2026 10:04:50 +0800 Subject: [PATCH] =?UTF-8?q?=E5=90=8E=E5=8F=B0=E7=AE=A1=E7=90=86=E8=B7=B3?= =?UTF-8?q?=E8=BF=87asin=E3=80=81=E5=90=8E=E5=8F=B0=E7=9B=B8=E5=85=B3?= =?UTF-8?q?=E6=95=B0=E6=8D=AE=E5=81=9A=E6=95=B0=E6=8D=AE=E9=9A=94=E7=A6=BB?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/.env | 4 +- .../__pycache__/del_brand.cpython-312.pyc | Bin 29042 -> 29056 bytes .../__pycache__/match_action.cpython-312.pyc | Bin 33227 -> 33252 bytes 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 | 6 +- app/new_web_source/dedupe.html | 6 +- app/new_web_source/delete-brand.html | 6 +- app/new_web_source/split.html | 6 +- .../aiimage-backend.log.2026-04-12.0.gz | Bin 0 -> 89796 bytes .../aiimage-backend.log.2026-04-13.0.gz | Bin 0 -> 48156 bytes .../config/DeleteBrandProgressProperties.java | 6 + .../config/ModuleCleanupProperties.java | 2 +- .../service/DeleteBrandStaleTaskService.java | 81 ++++ .../controller/PriceTrackController.java | 112 ++++-- .../dto/PriceTrackCandidateAddRequest.java | 6 +- ...riceTrackCountryPreferenceSaveRequest.java | 5 + .../dto/PriceTrackCreateTaskRequest.java | 27 +- .../dto/PriceTrackMatchShopsRequest.java | 5 + .../dto/PriceTrackSubmitResultRequest.java | 22 ++ .../model/dto/PriceTrackTaskBatchRequest.java | 16 + .../model/vo/PriceTrackCandidateVo.java | 5 + .../vo/PriceTrackCountryPreferenceVo.java | 6 + .../model/vo/PriceTrackCreateTaskVo.java | 9 +- .../model/vo/PriceTrackDashboardVo.java | 17 +- .../model/vo/PriceTrackHistoryVo.java | 4 + .../model/vo/PriceTrackMatchShopsVo.java | 23 +- .../model/vo/PriceTrackPendingDeleteVo.java | 11 + .../model/vo/PriceTrackResultItemVo.java | 29 ++ .../model/vo/PriceTrackTaskBatchVo.java | 17 + .../model/vo/PriceTrackTaskDetailVo.java | 17 + .../model/vo/PriceTrackTaskItemVo.java | 29 ++ .../PriceTrackExcelAssemblyService.java | 85 +++++ .../pricetrack/service/PriceTrackService.java | 122 +++--- .../service/PriceTrackTaskCacheService.java | 53 +++ .../service/PriceTrackTaskService.java | 355 ++++++++++++++---- .../controller/ShopManageController.java | 81 ++-- .../controller/SkipPriceAsinController.java | 11 + .../model/dto/ShopManageCreateRequest.java | 14 +- .../dto/ShopManageGroupCreateRequest.java | 7 + .../dto/ShopManageGroupUpdateRequest.java | 2 + .../model/dto/ShopManageUpdateRequest.java | 10 +- .../SkipPriceAsinCountryUpdateRequest.java | 13 + .../model/entity/ShopManageEntity.java | 2 + .../model/entity/ShopManageGroupEntity.java | 3 + .../service/ShopManageGroupService.java | 53 ++- .../shopkey/service/ShopManageService.java | 56 ++- .../shopkey/service/SkipPriceAsinService.java | 18 + .../src/main/resources/application.yml | 4 +- .../db/V22__shop_manage_add_created_by_id.sql | 33 ++ ...3__shop_manage_group_add_created_by_id.sql | 33 ++ backend/__pycache__/config.cpython-312.pyc | Bin 1513 -> 1510 bytes .../__pycache__/admin_api.cpython-312.pyc | Bin 64810 -> 67590 bytes backend/blueprints/admin_api.py | 100 ++++- backend/config.py | 4 +- backend/web_source/admin.html | 25 ++ .../brand/components/BrandPriceTrackTab.vue | 295 ++++++++++----- .../brand/components/BrandShopMatchTab.vue | 3 +- .../pages/brand/components/listingFilters.ts | 1 + frontend-vue/src/shared/api/java-modules.ts | 63 +++- frontend-vue/src/shared/bridges/pywebview.ts | 9 + new_web_source/convert.html | 16 + new_web_source/dedupe.html | 16 + new_web_source/delete-brand.html | 16 + new_web_source/price-track.html | 16 + new_web_source/product-risk.html | 18 + new_web_source/shop-match.html | 18 + new_web_source/split.html | 16 + 70 files changed, 1675 insertions(+), 381 deletions(-) create mode 100644 backend-java/aiimage-backend.log.2026-04-12.0.gz create mode 100644 backend-java/aiimage-backend.log.2026-04-13.0.gz create mode 100644 backend-java/src/main/java/com/nanri/aiimage/modules/pricetrack/model/dto/PriceTrackTaskBatchRequest.java create mode 100644 backend-java/src/main/java/com/nanri/aiimage/modules/pricetrack/model/vo/PriceTrackPendingDeleteVo.java create mode 100644 backend-java/src/main/java/com/nanri/aiimage/modules/pricetrack/model/vo/PriceTrackTaskBatchVo.java create mode 100644 backend-java/src/main/java/com/nanri/aiimage/modules/pricetrack/model/vo/PriceTrackTaskDetailVo.java create mode 100644 backend-java/src/main/java/com/nanri/aiimage/modules/pricetrack/model/vo/PriceTrackTaskItemVo.java create mode 100644 backend-java/src/main/java/com/nanri/aiimage/modules/pricetrack/service/PriceTrackExcelAssemblyService.java create mode 100644 backend-java/src/main/java/com/nanri/aiimage/modules/pricetrack/service/PriceTrackTaskCacheService.java create mode 100644 backend-java/src/main/java/com/nanri/aiimage/modules/shopkey/model/dto/SkipPriceAsinCountryUpdateRequest.java create mode 100644 backend-java/src/main/resources/db/V22__shop_manage_add_created_by_id.sql create mode 100644 backend-java/src/main/resources/db/V23__shop_manage_group_add_created_by_id.sql create mode 100644 new_web_source/convert.html create mode 100644 new_web_source/dedupe.html create mode 100644 new_web_source/delete-brand.html create mode 100644 new_web_source/price-track.html create mode 100644 new_web_source/product-risk.html create mode 100644 new_web_source/shop-match.html create mode 100644 new_web_source/split.html diff --git a/app/.env b/app/.env index 8a4270d..a8104b3 100644 --- a/app/.env +++ b/app/.env @@ -13,7 +13,7 @@ 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:18080 +java_api_base=http://127.0.0.1:18080 +# java_api_base=http://8.136.19.173:18080 diff --git a/app/amazon/__pycache__/del_brand.cpython-312.pyc b/app/amazon/__pycache__/del_brand.cpython-312.pyc index eabb2992b378addedf649f2ead8f44436bb28386..755c69a67fab7a4de56cde5cdea8ceabd5a2d4b7 100644 GIT binary patch delta 495 zcmezLh_T@@BkyTmUM>b8_*!u%bJj-Q)nQEcm?yc&Ozuh%o%}jXi%qPCIfX@XGH-%7 zM-5{N>l(JzOq2Z*$mc5R>oUxptf;p0*oKaI~@{A}$u8@_Cw^);N zQWJ|NpNv|vIWL-%k#WZ4iWobN4JsD|l&%PvZ{8Ub!zg3uX322W#E}I^9kb+g*I_=+ zq~Na3d|aK?U1zdkoIc~N&53a)ER2^oPfNPM%zhu}WVfPwlXKI$Sc@(IIg`@oh+YQL zMIIo+6GS`!5tk-!OxI(4y!k=80Sn{3$$~kHxWVFkL6Vm@@5`xTg4lz`i##f|Ia%H6h4-(ZTK7{d=g09Vk<6>FD^|k kLYVXtBb8Se|q{vwtJ+>M*8T%#&PXCeKL`o%}jXi%q14IfX@XvV4O0 z*Ca8yv3TFlbTpG znJ;~g=tYnQ4-nxABJP2R3zPS!>oGpu{4U*qh4Id0`J6@EVDY^m$%~uM` zPc|qJXD_-1V&9sam8ZsdYx2ZAH?}JvfyWHa^HWN5QtgTuHU}5z GG6Dc2L7wRV diff --git a/app/amazon/__pycache__/match_action.cpython-312.pyc b/app/amazon/__pycache__/match_action.cpython-312.pyc index 6555d3d7b8acc1793cd02b19e2257324e0e425bb..18b3b8a8e6f4c7df0cbdaa0e1ed7373769efcd27 100644 GIT binary patch delta 365 zcmX@z%=Dz0iT5-wFBbz4e66^X`EMg{yBOCR4h9Bq21bVI43ixsRVN=7Ghx!^oGd6U zG5IZr;$|c9SSCi7$rB`H*iwYou&-vCyiRf|b6gGAWXBj$cBvFLuvnZ_H{<5bucXu& zncP{RdPO$d%djvqYE5>L4Q2G3JWIBc(E~`zZ#I_GU}Lo29HUajDD{DXA(A<}b zn*n(z^qd6!c$rSw>j62Zbr}Oi7*FeI0@-K8LF_XU)O8)?pr006uvU-AF| delta 340 zcmaFT%yhb$iT5-wFBbz4EKj`6##ArBxY1F;pN-*=kdvyf1j7+?XCol*gqD+_A1Bi(Yds+6 zv?gPq0OM&bO(6S>Fo=Cd#2Uyx%g+jApA};QkrJGKY|LjBrTiF~&swtqWzR9P0!7ZT zF@xBgoc_$L=Y-V!>{!oPfMxAifgv.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-DiP0HdY6.js";import{e as Y}from"./brand-BATjYfEL.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 f4c0cff..3574d23 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-D808cNhy.js";import{e as se}from"./brand-DRn3N4FA.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-DiP0HdY6.js";import{e as se}from"./brand-BATjYfEL.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 84bdd87..cebfc65 100644 --- a/app/assets/delete-brand.js +++ b/app/assets/delete-brand.js @@ -1 +1 @@ -import{d as We,o as Ye,I as Ze,a as d,c as f,b as es,B as ss,e as l,F as H,r as de,t as v,f as h,J as ts,K as Ae,n as De,g as y,h as J,L as ns,i as G,E as p,M as as,N as ls,O as rs,P as os,_ as us,p as is,q as cs}from"./pywebview-D808cNhy.js";import{e as ds}from"./brand-DRn3N4FA.js";const fs={class:"page-shell module-page"},vs={class:"main-content"},hs={class:"left-panel"},ps={class:"upload-zone"},_s={class:"selected-files clean-placeholder split-selected-files"},ys={key:0,class:"more-line"},gs={key:1},ms={class:"run-row"},ks=["disabled"],ws=["disabled"],Is={class:"loading-msg"},bs={key:0,class:"queue-debug-card"},Ss={key:0,class:"queue-debug-line"},Ts={key:1,class:"queue-debug-payload"},Cs={class:"right-panel"},Fs={class:"task-list-wrap"},Es={class:"clean-result-summary"},Ps={class:"summary-card"},Rs={class:"summary-card"},xs={class:"summary-card"},Ns={class:"result-list-wrap"},$s={key:0,class:"empty-tasks"},As={key:0,class:"result-subsection"},Ds={class:"task-list clean-result-list"},Ms={class:"left split-result-main"},Bs=["title"],qs={class:"files"},Us={class:"files"},Ls={key:0,class:"files"},Os={key:1,class:"files"},Ks={key:2,class:"files"},Hs={key:3,class:"time"},Js={key:4,class:"delete-brand-progress-block"},Gs={class:"delete-brand-progress-header"},js={class:"delete-brand-progress-bar"},zs={class:"files delete-brand-progress"},Vs={key:5,class:"files split-entry-list delete-brand-preview"},Qs={key:6,class:"files"},Xs={class:"task-right split-result-actions"},Ws=["onClick"],Ys=["onClick"],Zs={key:1,class:"result-subsection"},et={class:"task-list clean-result-list"},st={class:"left split-result-main"},tt=["title"],nt={class:"files"},at={class:"files"},lt={key:0,class:"files"},rt={key:1,class:"files"},ot={key:2,class:"time"},ut={key:3,class:"delete-brand-progress-block"},it={class:"delete-brand-progress-header"},ct={class:"delete-brand-progress-bar"},dt={class:"files delete-brand-progress"},ft={key:4,class:"files split-entry-list delete-brand-preview"},vt={key:5,class:"files"},ht={class:"task-right split-result-actions"},pt=["onClick"],_t=["onClick"],yt=We({__name:"BrandDeleteBrandTab",setup(mt){function N(){return`delete-brand:current-tasks:${typeof window<"u"&&window.localStorage.getItem("uid")||"0"}`}const $=y([]),j=y([]),B=y(!1),fe=y([]),i=y([]),q=y([]),z=y({total:0,successCount:0,failedCount:0,items:[]}),b=y(""),S=y(""),g=y({}),T=y(null),ee=y(!1),V=y(!1),A=y(!1),D=y(""),Q=y(!1),U=y(null),se=J(()=>$.value.slice(0,10)),F=J(()=>{const e=[];return i.value.forEach(s=>{e.push(...s.items)}),e});function Me(){try{const e=typeof window<"u"?window.localStorage.getItem(N()):null;if(e){if(i.value=JSON.parse(e),i.value.length>0){const s=i.value[i.value.length-1];b.value=s.queuePushResult||"",S.value=s.queuePayloadText||""}}else i.value=[]}catch{i.value=[]}}function te(e,s,t,n){if(!s?.length)return;const a=i.value.findIndex(c=>c.taskId===e),o=a>=0?i.value[a]:null,r=s.map(c=>{const _=o?.items.find(Xe=>M(Xe,c)),k=Array.isArray(c.countries)?c.countries:void 0,m=Array.isArray(_?.countries)?_?.countries:void 0,w=k&&k.length>0?k:m&&m.length>0?m:k??m,I=Array.isArray(c.previewRows)?c.previewRows:void 0,C=Array.isArray(_?.previewRows)?_?.previewRows:void 0,ce=I&&I.length>0?I:C&&C.length>0?C:I??C;return{...c,countries:w,previewRows:ce,_pushed:_?._pushed??c._pushed??!1,_completed:_?._completed??c._completed??!1}}),u={taskId:e,items:r,createdAt:o?.createdAt||Date.now(),queuePushResult:t??o?.queuePushResult,queuePayloadText:n??o?.queuePayloadText};a>=0?i.value[a]=u:i.value.push(u),typeof window<"u"&&window.localStorage.setItem(N(),JSON.stringify(i.value))}function ve(e){const s=i.value.length;i.value=i.value.filter(t=>t.taskId!==e),i.value.length!==s&&typeof window<"u"&&window.localStorage.setItem(N(),JSON.stringify(i.value))}function he(e){return e==="SUCCESS"||e==="FAILED"||e==="COMPLETED"}function Be(e){if(!e?.length)return!1;const s=new Set;if(e.forEach(n=>{n.taskId&&he(n.taskStatus)&&s.add(n.taskId)}),!s.size)return!1;const t=i.value.length;return i.value=i.value.filter(n=>!s.has(n.taskId)),i.value.length!==t?(ne(),!0):!1}const X=J(()=>q.value.filter(e=>!F.value.some(s=>M(s,e)))),qe=J(()=>F.value.length>0||X.value.length>0),Ue=J(()=>F.value.filter(e=>Re(e)&&E(e)==="未入队"));function ne(){typeof window<"u"&&(i.value.length===0?window.localStorage.removeItem(N()):window.localStorage.setItem(N(),JSON.stringify(i.value)))}function P(e){return e?(i.value.find(t=>t.taskId===e)?.items.length||0)>1:!1}function ae(e){return e&&i.value.find(s=>s.taskId===e)||null}function M(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 R(e,s){const t=ae(e);return t&&t.items.find(n=>M(n,s))||null}function pe(e){if(!e)return!1;const s=ae(e);if(!s)return!1;let t=!1;const n=g.value[e]?.line_progress?.info,a=P(e);if(n?.file_name&&n.current_line!=null&&n.total_lines!=null&&n.total_lines>0&&n.current_line>=n.total_lines){const o=s.items.find(r=>r.sourceFilename===n.file_name);o&&!o._completed&&(o._completed=!0,t=!0)}if(!a){const o=g.value[e]?.task?.status;if((o==="SUCCESS"||o==="FAILED")&&s.items.length===1){const r=s.items[0];r&&!r._completed&&(r._completed=!0,t=!0)}}return t&&ne(),t}function _e(e){return e&&g.value[e]?.task?.status||""}function W(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 ye(e){const s=e.taskId;if(!s)return!1;const t=g.value[s]?.line_progress,n=_e(s),a=P(s),o=E(e);if(a){const r=R(s,e),u=K(e)===D.value,c=!!(t?.has_progress&&t?.info?.file_name===e.sourceFilename);return u||c||r?._pushed||(n==="RUNNING"||n==="PENDING"||!n)&&r?._completed?!0:{success:!1,retryable:!0}}return o==="本文件解析完毕"||o==="已被全局判定为终态"?!1:t?.has_progress||t?.info?!0:o==="已入队"||o==="处理中"}function ge(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 me(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 Le(){fe.value=[...F.value,...X.value]}function x(){Le(),Oe(fe.value)}function Oe(e){z.value={total:e.length,successCount:e.filter(s=>s.success).length,failedCount:e.filter(s=>!s.success).length,items:e}}function le(e){const s=e.taskId;return s&&(g.value[s]?.fileProgress||[]).find(a=>a.fileKey&&e.fileKey?a.fileKey===e.fileKey:a.sourceFilename&&e.sourceFilename?a.sourceFilename===e.sourceFilename:!1)||null}function ke(e){const s=e.taskId;if(!s)return"";const t=g.value[s],n=t?.line_progress?.info,a=le(e);if(P(s)){const u=R(s,e),c=t?.task?.status,_=!!(n?.file_name&&n.file_name===e.sourceFilename);let k=u?._completed?"COMPLETED":(u?._pushed,"PENDING"),m=0,w=Math.max(0,e.totalRows??a?.totalRows??0);c==="SUCCESS"||c==="FAILED"&&u?._completed?(k="COMPLETED",m=w):_?(k="RUNNING",m=Math.max(0,Math.min(w||Number.MAX_SAFE_INTEGER,n?.current_line??0)),!w&&n?.total_lines&&n.total_lines>0&&(w=n.total_lines)):u?._completed?(k="COMPLETED",m=w):u?._pushed&&(k="PENDING",m=0);const I=[];if(I.push(`文件状态:${k}`),I.push(`文件进度:${m}/${w}`),n?.finished_files!=null){const C=n.file_total&&n.file_total>0?`/${n.file_total}`:"";I.push(`已完成文件:${n.finished_files}${C}`)}return n?.phase&&I.push(`阶段:${n.phase}`),I.join("|")}if(a){const u=[],c=a.processedRows??0,_=a.totalRows??0;if(a.status&&u.push(`文件状态:${a.status}`),u.push(`文件进度:${c}/${_}`),n?.finished_files!=null){const k=n.file_total&&n.file_total>0?`/${n.file_total}`:"";u.push(`已完成文件:${n.finished_files}${k}`)}return n?.phase&&u.push(`阶段:${n.phase}`),u.join("|")}if(!t?.line_progress?.has_progress||!n){const u=t?.task?.status;return u?`任务状态:${u}`:""}const r=[];if(n.phase&&r.push(`阶段:${n.phase}`),n.file_index&&n.file_total&&r.push(`文件:${n.file_index}/${n.file_total}`),n.file_name&&r.push(`当前文件:${n.file_name}`),n.current_line!=null&&n.total_lines!=null&&r.push(`${P(s)?"当前文件进度":"进度"}:${n.current_line}/${n.total_lines}`),n.current_country&&r.push(`国家:${n.current_country}`),n.current_asin&&r.push(`ASIN:${n.current_asin}`),n.finished_files!=null){const u=n.file_total&&n.file_total>0?`/${n.file_total}`:"";r.push(`已完成文件:${n.finished_files}${u}`)}return r.join("|")}function Y(e){const s=e.taskId;if(!s)return 0;const t=g.value[s]?.line_progress?.info;if(P(s)){const r=R(s,e);if(g.value[s]?.task?.status==="SUCCESS"||r?._completed)return 100;const c=Math.max(0,e.totalRows??le(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&&c>0)return Math.max(0,Math.min(100,Math.round(t.current_line/c*100)))}return 0}const a=le(e);return a?Math.max(0,Math.min(100,a.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 we(e){const s=g.value[e]?.task?.status;return he(s)}function L(){const e=new Set;return i.value.forEach(s=>{s.items.some(t=>t._pushed)&&e.add(s.taskId)}),Array.from(e)}function E(e){const s=e.taskId;if(!s||!ae(s))return"";const n=R(s,e),a=_e(s),o=g.value[s]?.line_progress,r=P(s);if(!r&&(a==="SUCCESS"||a==="FAILED"||e.taskStatus==="SUCCESS"||e.taskStatus==="FAILED"))return"已被全局判定为终态";if(n?._completed)return"本文件解析完毕";if(a==="RUNNING"&&o?.has_progress&&o?.info?.file_name===e.sourceFilename)return"处理中";if(r&&!n?._pushed&&!n?._completed)return"未入队";if(r&&a==="SUCCESS")return"本文件解析完毕";if(!r){const u=g.value[s];if(u?.items&&u.items.find(_=>M(_,e))&&a==="SUCCESS")return"本文件解析完毕"}return n?._pushed?"已入队":"未入队"}function Ie(e){if(e.downloadUrl)return!0;const s=e.taskId;return s?g.value[s]?.task?.status==="SUCCESS":{success:!1,retryable:!1}}function Ke(e){const s=e?.task?.id;if(!s)return!1;const t=i.value.find(r=>r.taskId===s);if(!t)return{success:!1,retryable:!1};const n=e?.items||[];let a=!1;const o=t.items.map(r=>{const u=n.find(ce=>M(ce,r));if(!u)return r;const c=Array.isArray(u.countries)?u.countries:void 0,_=Array.isArray(r.countries)?r.countries:void 0,k=c&&c.length>0?c:_&&_.length>0?_:c??_,m=Array.isArray(u.previewRows)?u.previewRows:void 0,w=Array.isArray(r.previewRows)?r.previewRows:void 0,I=m&&m.length>0?m:w&&w.length>0?w:m??w,C={...r,...u,countries:k,previewRows:I,_pushed:r._pushed,_completed:r._completed};return JSON.stringify(C)!==JSON.stringify(r)&&(a=!0),C});for(const r of n)o.some(u=>M(u,r))||(o.push({...r,_pushed:!1,_completed:!1}),a=!0);return JSON.stringify(o)!==JSON.stringify(t.items)&&(t.items=o,a=!0),a&&ne(),a}async function re(e){const s=e||L();if(s.length)try{const t=await ns(s);let n=!1;for(const a of t.items||[]){const o=a?.task?.id;typeof o=="number"&&o>0&&(g.value[o]=a,Ke(a)&&(n=!0),pe(o)&&(n=!0))}n&&x()}catch{}}function He(){return document.visibilityState==="visible"?4500:1e4}function oe(e=!1){if(T.value){if(!e)return;clearTimeout(T.value),T.value=null}const s=async()=>{if(T.value=null,ee.value){oe();return}const t=L();if(!t.length)return;ee.value=!0;let n=!1,a=!1;try{await re(t),await $e();for(const o of t)we(o)&&(ve(o),n=!0,a=!0)}finally{ee.value=!1}a?await O():n&&x(),L().length>0&&oe()};e?s():T.value=window.setTimeout(s,He())}function ue(e=!1){T.value&&!e||oe(e)}function Z(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 be(){T.value&&(window.clearTimeout(T.value),T.value=null),ie()}async function Se(e){const s=G();if(!s?.upload_file_to_java)throw new Error("当前桌面端未提供文件上传桥接能力");const t=[];for(const n of e){const a=typeof n=="string"?n:n.absolutePath,o=typeof n=="string"?void 0:n.relativePath,r=await s.upload_file_to_java(a,o);if(!r?.success||!r.data)throw new Error(r?.error||r?.message||`上传失败:${a}`);t.push(r.data)}return t}async function Je(){const e=G();if(!e?.select_brand_xlsx_files){p.warning("当前环境不支持文件选择,请在本机客户端中打开");return}try{const s=await e.select_brand_xlsx_files();if(!s?.length)return;$.value=s,j.value=await Se(s),p.success(`已选择 ${s.length} 个删除品牌文件`)}catch(s){p.error(s instanceof Error?s.message:"选择失败")}}async function Ge(){const e=G();if(!e?.select_brand_folder){p.warning("当前环境不支持文件夹选择,请在本机客户端中打开");return}try{const s=await e.select_brand_folder();if(!s)return;const t=await ds(s);if(!t.success||!t.items?.length){p.warning(t.error||"该文件夹下没有 xlsx 文件");return}$.value=t.items.map(n=>n.relativePath||n.absolutePath),j.value=await Se(t.items),p.success(`已选择文件夹内 ${t.items.length} 个 xlsx 文件`)}catch(s){p.error(s instanceof Error?s.message:"选择失败")}}async function je(){if(!j.value.length){p.warning("请先选择待处理文件");return}try{B.value=!0;const e=await as({files:j.value.map(a=>({fileKey:a.fileKey,originalFilename:a.originalFilename,relativePath:a.relativePath}))}),s=ge(e.items||[]),t=s.some(a=>a.matchStatus==="MATCHED"),n=s.some(a=>a.matchStatus&&a.matchStatus!=="MATCHED");b.value=t?"解析完成,可在左侧点击“推送到 Python 队列”开始串行处理":"解析完成,当前没有可推送的已匹配文件",S.value="",s.length&&s[0].taskId&&te(s[0].taskId,s,b.value,S.value),x(),n?p.warning("部分文件尚未命中可用店铺索引,已保留状态信息,请等待后台刷新后重试。"):t?p.success("删除品牌解析完成,请在左侧推送到 Python 队列"):p.warning("当前没有可推送的已匹配文件,请先等待店铺索引刷新。"),await O()}catch(e){p.error(e instanceof Error?e.message:"执行失败")}finally{B.value=!1}}async function Te(e){const s=e.taskId;if(!s){p.warning("未找到任务ID");return}const t=G(),n=ls(s),a=e.outputFilename||e.sourceFilename||`delete-brand_${s}.xlsx`;if(t?.save_file_from_url_new){const o=await t.save_file_from_url_new(n,a);o.success?p.success(`已保存:${o.path||a}`):o.error&&o.error!=="用户取消"&&p.error(o.error);return}}async function O(){try{const e=await rs();q.value=ge(e.items||[]),Be(q.value),x(),await re(),ue(!0)}catch(e){console.error("loadHistory error:",e)}}function Ce(e){i.value.forEach(t=>{t.items=t.items.filter(n=>n.resultId!==e)});const s=i.value.length;i.value=i.value.filter(t=>t.items.length>0),i.value.length!==s&&typeof window<"u"&&window.localStorage.setItem(N(),JSON.stringify(i.value)),q.value=q.value.filter(t=>t.resultId!==e),x(),L().length===0&&be()}async function Fe(e){try{await os(e),Ce(e),await O(),p.success("已删除")}catch(s){const t=s instanceof Error?s.message:"删除失败";if(typeof t=="string"&&t.includes("记录不存在")){Ce(e),p.success("已删除");return}p.error(t)}}function Ee(e){const s=e.slice(0,5).map(n=>`${n.country}:${n.asin}${n.status?`(${n.status})`:""}`);return e.length-s.length>0?`${s.join("、")} 等 ${e.length} 条`:s.join("、")}function kt(e,s){}function ie(){U.value&&(window.clearTimeout(U.value),U.value=null)}function ze(e=3e3){ie(),U.value=window.setTimeout(()=>{U.value=null,$e().catch(()=>{})},e)}function Ve(e){return e.includes("当前店铺正在执行中")}function Pe(){for(const e of i.value)for(const s of e.items){const t=E(s);if(t==="已入队"||t==="处理中")return!0}return!1}function K(e){return e.resultId!=null?`result:${e.resultId}`:`task:${e.taskId||0}:${e.sourceFilename||""}`}function Re(e){return!!(e.taskId&&(e.matchStatus==="MATCHED"||e.matched))}function xe(){return F.value.find(e=>!Re(e)||K(e)===D.value||R(e.taskId,e)?._completed?!1:E(e)==="未入队")}async function Ne(e,s){if(s?.auto&&Pe())return e.taskId,e.sourceFilename,{success:!1,retryable:!0};const t=G(),n=e.taskId;if(!n)return{success:!1,retryable:!1};const a=i.value.find(c=>c.taskId===n);if(!a)return{success:!1,retryable:!1};if(!t?.enqueue_json)return p.error("当前环境未启用 pywebview enqueue_json(浏览器环境不会推送)"),{success:!1,retryable:!1};const o={type:"delete-brand-run",ts:Date.now(),data:{taskId:n,items:[e]}};S.value=JSON.stringify(o,null,2);const r=await t.enqueue_json(o);let u="";if(r?.success){u=`已推送文件 ${e.sourceFilename},当前队列长度:${r.queue_size??"-"}`;const c=R(n,e);return c&&(c._pushed=!0,c._completed=!1),D.value=K(e),te(n,a.items,u,S.value),ue(!0),b.value=u,{success:!0,retryable:!1}}else{const c=r?.error||"未知错误";if(u=`推送到 Python 队列失败:${c}`,b.value=u,te(n,a.items,u,S.value),s?.auto&&Ve(c))return{success:!1,retryable:!0}}return b.value=u,{success:!1,retryable:!1}}async function Qe(){const e=xe();if(!e){p.warning("当前没有可推送的已匹配文件");return}V.value=!0,A.value=!0,ie();const s=await Ne(e);if(V.value=!1,!s.success){A.value=!1,D.value="";return}p.success("已开始按顺序推送到 Python 队列")}async function $e(){if(!A.value||Q.value){A.value,Q.value;return}if(Pe())return;const e=D.value;if(e){const t=F.value.find(n=>K(n)===e);if(t){const n=t.taskId,a=P(n);a&&(pe(n),E(t),void 0);const o=E(t);if(o==="已入队"||o==="处理中"){t.sourceFilename;return}if(a){const r=R(n,t);if(r&&!r._completed){t.sourceFilename,r._pushed,r._completed;return}}}}const s=xe();if(!s){D.value="",A.value=!1;return}Q.value=!0;try{s.taskId,s.sourceFilename,K(s);const t=await Ne(s,{auto:!0});if(!t.success){if(t.retryable){s.taskId,s.sourceFilename,ze();return}s.taskId,s.sourceFilename,A.value=!1}}finally{Q.value=!1}}return Ye(()=>{Me();const e=L();e.length>0?re(e).then(async()=>{let s=!1,t=!1;for(const n of e)we(n)&&(ve(n),s=!0,t=!0);t?await O():s&&x(),ue(!0)}):x(),O().catch(()=>{})}),Ze(()=>{be()}),(e,s)=>(d(),f("div",fs,[es(ss,{active:"delete-brand"}),l("div",vs,[l("aside",hs,[s[2]||(s[2]=l("div",{class:"section-title"},"选择文件",-1)),l("div",ps,[s[0]||(s[0]=l("div",{class:"hint"},"上传删除品牌 Excel;无论是直接上传文件还是上传文件夹批量导入,都会按每个 Excel 文件名作为店铺名。",-1)),l("div",{class:"btns"},[l("button",{type:"button",class:"opt-btn",onClick:Je},"选择 Excel 文件"),l("button",{type:"button",class:"opt-btn",onClick:Ge},"选择文件夹")]),l("div",_s,[$.value.length?(d(),f(H,{key:0},[(d(!0),f(H,null,de(se.value,t=>(d(),f("span",{key:t},v(t),1))),128)),$.value.length>se.value.length?(d(),f("span",ys," 还有 "+v($.value.length-se.value.length)+" 个文件未展开显示 ",1)):h("",!0)],64)):(d(),f("span",gs,"暂未选择删除品牌文件"))])]),s[3]||(s[3]=ts('
执行说明
',2)),l("div",ms,[l("button",{type:"button",class:"btn-run",disabled:B.value,onClick:je},v(B.value?"解析中...":"开始解析"),9,ks),l("button",{type:"button",class:"btn-run btn-queue",disabled:V.value||!Ue.value.length,onClick:Qe},v(V.value?"推送中...":"推送到 Python 队列"),9,ws),l("span",Is,v(B.value?"正在读取删除品牌数据,请稍候…":"解析后会在右侧展示按国家分组的去重结果"),1)]),b.value||S.value?(d(),f("div",bs,[s[1]||(s[1]=l("div",{class:"section-title queue-debug-title"},"Python 队列推送结果",-1)),b.value?(d(),f("div",Ss,v(b.value),1)):h("",!0),S.value?(d(),f("pre",Ts,v(S.value),1)):h("",!0)])):h("",!0)]),l("section",Cs,[s[12]||(s[12]=l("div",{class:"panel-header"},"删除品牌结果",-1)),l("div",Fs,[l("div",Es,[l("div",Ps,[s[4]||(s[4]=l("span",{class:"summary-label"},"已处理文件",-1)),l("strong",null,v(z.value.total),1)]),l("div",Rs,[s[5]||(s[5]=l("span",{class:"summary-label"},"成功结果",-1)),l("strong",null,v(z.value.successCount),1)]),l("div",xs,[s[6]||(s[6]=l("span",{class:"summary-label"},"失败文件",-1)),l("strong",null,v(z.value.failedCount),1)])]),l("div",Ns,[s[11]||(s[11]=l("div",{class:"result-list-header"},[l("span",null,"删除品牌结果列表")],-1)),qe.value?(d(),f(H,{key:1},[F.value.length?(d(),f("div",As,[s[8]||(s[8]=l("div",{class:"result-subsection-title"},"当前任务",-1)),l("ul",Ds,[(d(!0),f(H,null,de(F.value,t=>(d(),f("li",{key:`current-${t.taskId||t.resultId||t.sourceFilename}`,class:"task-item split-result-item"},[l("div",Ms,[l("span",{class:"id",title:t.sourceFilename},v(t.sourceFilename||"-"),9,Bs),l("div",qs,"店铺名:"+v(t.shopName||"-"),1),l("div",Us,"匹配结果:"+v(me(t)),1),t.platform?(d(),f("div",Ls,"平台:"+v(t.platform),1)):h("",!0),E(t)?(d(),f("div",Os,"队列状态:"+v(E(t)),1)):h("",!0),t.countryCount!==void 0&&t.matched?(d(),f("div",Ks,"国家数:"+v(t.countryCount),1)):h("",!0),t.totalRows!==void 0&&t.matched?(d(),f("div",Hs,"去重后 "+v(t.totalRows)+" 条 ",1)):h("",!0),ye(t)?(d(),f("div",Js,[l("div",Gs,[s[7]||(s[7]=l("span",null,"任务进度",-1)),l("span",null,v(Y(t))+"%",1)]),l("div",js,[l("div",{class:"delete-brand-progress-bar-fill",style:Ae({width:`${Y(t)}%`})},null,4)]),l("div",zs,v(ke(t)),1)])):h("",!0),t.previewRows?.length?(d(),f("div",Vs,v(Ee(t.previewRows)),1)):h("",!0),W(t)?(d(),f("div",Qs,"错误信息:"+v(W(t)),1)):h("",!0)]),l("div",Xs,[l("span",{class:De(["status",Z(t).className])},v(Z(t).text),3),Ie(t)?(d(),f("button",{key:0,type:"button",class:"download",onClick:n=>Te(t)}," 下载 ",8,Ws)):h("",!0),t.resultId?(d(),f("button",{key:1,type:"button",class:"btn-delete",onClick:n=>Fe(t.resultId)}," 删除 ",8,Ys)):h("",!0)])]))),128))])])):h("",!0),X.value.length?(d(),f("div",Zs,[s[10]||(s[10]=l("div",{class:"result-subsection-title"},"历史记录",-1)),l("ul",et,[(d(!0),f(H,null,de(X.value,t=>(d(),f("li",{key:`history-${t.taskId||t.resultId||t.sourceFilename}`,class:"task-item split-result-item"},[l("div",st,[l("span",{class:"id",title:t.sourceFilename},v(t.sourceFilename||"-"),9,tt),l("div",nt,"店铺名:"+v(t.shopName||"-"),1),l("div",at,"匹配结果:"+v(me(t)),1),t.platform?(d(),f("div",lt,"平台:"+v(t.platform),1)):h("",!0),t.countryCount!==void 0&&t.matched?(d(),f("div",rt,"国家数:"+v(t.countryCount),1)):h("",!0),t.totalRows!==void 0&&t.matched?(d(),f("div",ot,"去重后 "+v(t.totalRows)+" 条 ",1)):h("",!0),ye(t)?(d(),f("div",ut,[l("div",it,[s[9]||(s[9]=l("span",null,"任务进度",-1)),l("span",null,v(Y(t))+"%",1)]),l("div",ct,[l("div",{class:"delete-brand-progress-bar-fill",style:Ae({width:`${Y(t)}%`})},null,4)]),l("div",dt,v(ke(t)),1)])):h("",!0),t.previewRows?.length?(d(),f("div",ft,v(Ee(t.previewRows)),1)):h("",!0),W(t)?(d(),f("div",vt,"错误信息:"+v(W(t)),1)):h("",!0)]),l("div",ht,[l("span",{class:De(["status",Z(t).className])},v(Z(t).text),3),Ie(t)?(d(),f("button",{key:0,type:"button",class:"download",onClick:n=>Te(t)}," 下载 ",8,pt)):h("",!0),t.resultId?(d(),f("button",{key:1,type:"button",class:"btn-delete",onClick:n=>Fe(t.resultId)}," 删除 ",8,_t)):h("",!0)])]))),128))])])):h("",!0)],64)):(d(),f("div",$s," 暂无删除品牌结果,完成解析后会在这里展示按国家分组的去重结果 "))])])])])]))}}),gt=us(yt,[["__scopeId","data-v-04852f65"]]);is(gt).use(cs).mount("#app"); +import{d as We,o as Ye,I as Ze,a as d,c as f,b as es,B as ss,e as l,F as H,r as de,t as v,f as h,J as ts,K as Ae,n as De,g as y,h as J,L as ns,i as G,E as p,M as as,N as ls,O as rs,P as os,_ as us,p as is,q as cs}from"./pywebview-DiP0HdY6.js";import{e as ds}from"./brand-BATjYfEL.js";const fs={class:"page-shell module-page"},vs={class:"main-content"},hs={class:"left-panel"},ps={class:"upload-zone"},_s={class:"selected-files clean-placeholder split-selected-files"},ys={key:0,class:"more-line"},gs={key:1},ms={class:"run-row"},ks=["disabled"],ws=["disabled"],Is={class:"loading-msg"},bs={key:0,class:"queue-debug-card"},Ss={key:0,class:"queue-debug-line"},Ts={key:1,class:"queue-debug-payload"},Cs={class:"right-panel"},Fs={class:"task-list-wrap"},Es={class:"clean-result-summary"},Ps={class:"summary-card"},Rs={class:"summary-card"},xs={class:"summary-card"},Ns={class:"result-list-wrap"},$s={key:0,class:"empty-tasks"},As={key:0,class:"result-subsection"},Ds={class:"task-list clean-result-list"},Ms={class:"left split-result-main"},Bs=["title"],qs={class:"files"},Us={class:"files"},Ls={key:0,class:"files"},Os={key:1,class:"files"},Ks={key:2,class:"files"},Hs={key:3,class:"time"},Js={key:4,class:"delete-brand-progress-block"},Gs={class:"delete-brand-progress-header"},js={class:"delete-brand-progress-bar"},zs={class:"files delete-brand-progress"},Vs={key:5,class:"files split-entry-list delete-brand-preview"},Qs={key:6,class:"files"},Xs={class:"task-right split-result-actions"},Ws=["onClick"],Ys=["onClick"],Zs={key:1,class:"result-subsection"},et={class:"task-list clean-result-list"},st={class:"left split-result-main"},tt=["title"],nt={class:"files"},at={class:"files"},lt={key:0,class:"files"},rt={key:1,class:"files"},ot={key:2,class:"time"},ut={key:3,class:"delete-brand-progress-block"},it={class:"delete-brand-progress-header"},ct={class:"delete-brand-progress-bar"},dt={class:"files delete-brand-progress"},ft={key:4,class:"files split-entry-list delete-brand-preview"},vt={key:5,class:"files"},ht={class:"task-right split-result-actions"},pt=["onClick"],_t=["onClick"],yt=We({__name:"BrandDeleteBrandTab",setup(mt){function N(){return`delete-brand:current-tasks:${typeof window<"u"&&window.localStorage.getItem("uid")||"0"}`}const $=y([]),j=y([]),B=y(!1),fe=y([]),i=y([]),q=y([]),z=y({total:0,successCount:0,failedCount:0,items:[]}),b=y(""),S=y(""),g=y({}),T=y(null),ee=y(!1),V=y(!1),A=y(!1),D=y(""),Q=y(!1),U=y(null),se=J(()=>$.value.slice(0,10)),F=J(()=>{const e=[];return i.value.forEach(s=>{e.push(...s.items)}),e});function Me(){try{const e=typeof window<"u"?window.localStorage.getItem(N()):null;if(e){if(i.value=JSON.parse(e),i.value.length>0){const s=i.value[i.value.length-1];b.value=s.queuePushResult||"",S.value=s.queuePayloadText||""}}else i.value=[]}catch{i.value=[]}}function te(e,s,t,n){if(!s?.length)return;const a=i.value.findIndex(c=>c.taskId===e),o=a>=0?i.value[a]:null,r=s.map(c=>{const _=o?.items.find(Xe=>M(Xe,c)),k=Array.isArray(c.countries)?c.countries:void 0,m=Array.isArray(_?.countries)?_?.countries:void 0,w=k&&k.length>0?k:m&&m.length>0?m:k??m,I=Array.isArray(c.previewRows)?c.previewRows:void 0,C=Array.isArray(_?.previewRows)?_?.previewRows:void 0,ce=I&&I.length>0?I:C&&C.length>0?C:I??C;return{...c,countries:w,previewRows:ce,_pushed:_?._pushed??c._pushed??!1,_completed:_?._completed??c._completed??!1}}),u={taskId:e,items:r,createdAt:o?.createdAt||Date.now(),queuePushResult:t??o?.queuePushResult,queuePayloadText:n??o?.queuePayloadText};a>=0?i.value[a]=u:i.value.push(u),typeof window<"u"&&window.localStorage.setItem(N(),JSON.stringify(i.value))}function ve(e){const s=i.value.length;i.value=i.value.filter(t=>t.taskId!==e),i.value.length!==s&&typeof window<"u"&&window.localStorage.setItem(N(),JSON.stringify(i.value))}function he(e){return e==="SUCCESS"||e==="FAILED"||e==="COMPLETED"}function Be(e){if(!e?.length)return!1;const s=new Set;if(e.forEach(n=>{n.taskId&&he(n.taskStatus)&&s.add(n.taskId)}),!s.size)return!1;const t=i.value.length;return i.value=i.value.filter(n=>!s.has(n.taskId)),i.value.length!==t?(ne(),!0):!1}const X=J(()=>q.value.filter(e=>!F.value.some(s=>M(s,e)))),qe=J(()=>F.value.length>0||X.value.length>0),Ue=J(()=>F.value.filter(e=>Re(e)&&E(e)==="未入队"));function ne(){typeof window<"u"&&(i.value.length===0?window.localStorage.removeItem(N()):window.localStorage.setItem(N(),JSON.stringify(i.value)))}function P(e){return e?(i.value.find(t=>t.taskId===e)?.items.length||0)>1:!1}function ae(e){return e&&i.value.find(s=>s.taskId===e)||null}function M(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 R(e,s){const t=ae(e);return t&&t.items.find(n=>M(n,s))||null}function pe(e){if(!e)return!1;const s=ae(e);if(!s)return!1;let t=!1;const n=g.value[e]?.line_progress?.info,a=P(e);if(n?.file_name&&n.current_line!=null&&n.total_lines!=null&&n.total_lines>0&&n.current_line>=n.total_lines){const o=s.items.find(r=>r.sourceFilename===n.file_name);o&&!o._completed&&(o._completed=!0,t=!0)}if(!a){const o=g.value[e]?.task?.status;if((o==="SUCCESS"||o==="FAILED")&&s.items.length===1){const r=s.items[0];r&&!r._completed&&(r._completed=!0,t=!0)}}return t&&ne(),t}function _e(e){return e&&g.value[e]?.task?.status||""}function W(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 ye(e){const s=e.taskId;if(!s)return!1;const t=g.value[s]?.line_progress,n=_e(s),a=P(s),o=E(e);if(a){const r=R(s,e),u=K(e)===D.value,c=!!(t?.has_progress&&t?.info?.file_name===e.sourceFilename);return u||c||r?._pushed||(n==="RUNNING"||n==="PENDING"||!n)&&r?._completed?!0:{success:!1,retryable:!0}}return o==="本文件解析完毕"||o==="已被全局判定为终态"?!1:t?.has_progress||t?.info?!0:o==="已入队"||o==="处理中"}function ge(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 me(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 Le(){fe.value=[...F.value,...X.value]}function x(){Le(),Oe(fe.value)}function Oe(e){z.value={total:e.length,successCount:e.filter(s=>s.success).length,failedCount:e.filter(s=>!s.success).length,items:e}}function le(e){const s=e.taskId;return s&&(g.value[s]?.fileProgress||[]).find(a=>a.fileKey&&e.fileKey?a.fileKey===e.fileKey:a.sourceFilename&&e.sourceFilename?a.sourceFilename===e.sourceFilename:!1)||null}function ke(e){const s=e.taskId;if(!s)return"";const t=g.value[s],n=t?.line_progress?.info,a=le(e);if(P(s)){const u=R(s,e),c=t?.task?.status,_=!!(n?.file_name&&n.file_name===e.sourceFilename);let k=u?._completed?"COMPLETED":(u?._pushed,"PENDING"),m=0,w=Math.max(0,e.totalRows??a?.totalRows??0);c==="SUCCESS"||c==="FAILED"&&u?._completed?(k="COMPLETED",m=w):_?(k="RUNNING",m=Math.max(0,Math.min(w||Number.MAX_SAFE_INTEGER,n?.current_line??0)),!w&&n?.total_lines&&n.total_lines>0&&(w=n.total_lines)):u?._completed?(k="COMPLETED",m=w):u?._pushed&&(k="PENDING",m=0);const I=[];if(I.push(`文件状态:${k}`),I.push(`文件进度:${m}/${w}`),n?.finished_files!=null){const C=n.file_total&&n.file_total>0?`/${n.file_total}`:"";I.push(`已完成文件:${n.finished_files}${C}`)}return n?.phase&&I.push(`阶段:${n.phase}`),I.join("|")}if(a){const u=[],c=a.processedRows??0,_=a.totalRows??0;if(a.status&&u.push(`文件状态:${a.status}`),u.push(`文件进度:${c}/${_}`),n?.finished_files!=null){const k=n.file_total&&n.file_total>0?`/${n.file_total}`:"";u.push(`已完成文件:${n.finished_files}${k}`)}return n?.phase&&u.push(`阶段:${n.phase}`),u.join("|")}if(!t?.line_progress?.has_progress||!n){const u=t?.task?.status;return u?`任务状态:${u}`:""}const r=[];if(n.phase&&r.push(`阶段:${n.phase}`),n.file_index&&n.file_total&&r.push(`文件:${n.file_index}/${n.file_total}`),n.file_name&&r.push(`当前文件:${n.file_name}`),n.current_line!=null&&n.total_lines!=null&&r.push(`${P(s)?"当前文件进度":"进度"}:${n.current_line}/${n.total_lines}`),n.current_country&&r.push(`国家:${n.current_country}`),n.current_asin&&r.push(`ASIN:${n.current_asin}`),n.finished_files!=null){const u=n.file_total&&n.file_total>0?`/${n.file_total}`:"";r.push(`已完成文件:${n.finished_files}${u}`)}return r.join("|")}function Y(e){const s=e.taskId;if(!s)return 0;const t=g.value[s]?.line_progress?.info;if(P(s)){const r=R(s,e);if(g.value[s]?.task?.status==="SUCCESS"||r?._completed)return 100;const c=Math.max(0,e.totalRows??le(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&&c>0)return Math.max(0,Math.min(100,Math.round(t.current_line/c*100)))}return 0}const a=le(e);return a?Math.max(0,Math.min(100,a.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 we(e){const s=g.value[e]?.task?.status;return he(s)}function L(){const e=new Set;return i.value.forEach(s=>{s.items.some(t=>t._pushed)&&e.add(s.taskId)}),Array.from(e)}function E(e){const s=e.taskId;if(!s||!ae(s))return"";const n=R(s,e),a=_e(s),o=g.value[s]?.line_progress,r=P(s);if(!r&&(a==="SUCCESS"||a==="FAILED"||e.taskStatus==="SUCCESS"||e.taskStatus==="FAILED"))return"已被全局判定为终态";if(n?._completed)return"本文件解析完毕";if(a==="RUNNING"&&o?.has_progress&&o?.info?.file_name===e.sourceFilename)return"处理中";if(r&&!n?._pushed&&!n?._completed)return"未入队";if(r&&a==="SUCCESS")return"本文件解析完毕";if(!r){const u=g.value[s];if(u?.items&&u.items.find(_=>M(_,e))&&a==="SUCCESS")return"本文件解析完毕"}return n?._pushed?"已入队":"未入队"}function Ie(e){if(e.downloadUrl)return!0;const s=e.taskId;return s?g.value[s]?.task?.status==="SUCCESS":{success:!1,retryable:!1}}function Ke(e){const s=e?.task?.id;if(!s)return!1;const t=i.value.find(r=>r.taskId===s);if(!t)return{success:!1,retryable:!1};const n=e?.items||[];let a=!1;const o=t.items.map(r=>{const u=n.find(ce=>M(ce,r));if(!u)return r;const c=Array.isArray(u.countries)?u.countries:void 0,_=Array.isArray(r.countries)?r.countries:void 0,k=c&&c.length>0?c:_&&_.length>0?_:c??_,m=Array.isArray(u.previewRows)?u.previewRows:void 0,w=Array.isArray(r.previewRows)?r.previewRows:void 0,I=m&&m.length>0?m:w&&w.length>0?w:m??w,C={...r,...u,countries:k,previewRows:I,_pushed:r._pushed,_completed:r._completed};return JSON.stringify(C)!==JSON.stringify(r)&&(a=!0),C});for(const r of n)o.some(u=>M(u,r))||(o.push({...r,_pushed:!1,_completed:!1}),a=!0);return JSON.stringify(o)!==JSON.stringify(t.items)&&(t.items=o,a=!0),a&&ne(),a}async function re(e){const s=e||L();if(s.length)try{const t=await ns(s);let n=!1;for(const a of t.items||[]){const o=a?.task?.id;typeof o=="number"&&o>0&&(g.value[o]=a,Ke(a)&&(n=!0),pe(o)&&(n=!0))}n&&x()}catch{}}function He(){return document.visibilityState==="visible"?4500:1e4}function oe(e=!1){if(T.value){if(!e)return;clearTimeout(T.value),T.value=null}const s=async()=>{if(T.value=null,ee.value){oe();return}const t=L();if(!t.length)return;ee.value=!0;let n=!1,a=!1;try{await re(t),await $e();for(const o of t)we(o)&&(ve(o),n=!0,a=!0)}finally{ee.value=!1}a?await O():n&&x(),L().length>0&&oe()};e?s():T.value=window.setTimeout(s,He())}function ue(e=!1){T.value&&!e||oe(e)}function Z(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 be(){T.value&&(window.clearTimeout(T.value),T.value=null),ie()}async function Se(e){const s=G();if(!s?.upload_file_to_java)throw new Error("当前桌面端未提供文件上传桥接能力");const t=[];for(const n of e){const a=typeof n=="string"?n:n.absolutePath,o=typeof n=="string"?void 0:n.relativePath,r=await s.upload_file_to_java(a,o);if(!r?.success||!r.data)throw new Error(r?.error||r?.message||`上传失败:${a}`);t.push(r.data)}return t}async function Je(){const e=G();if(!e?.select_brand_xlsx_files){p.warning("当前环境不支持文件选择,请在本机客户端中打开");return}try{const s=await e.select_brand_xlsx_files();if(!s?.length)return;$.value=s,j.value=await Se(s),p.success(`已选择 ${s.length} 个删除品牌文件`)}catch(s){p.error(s instanceof Error?s.message:"选择失败")}}async function Ge(){const e=G();if(!e?.select_brand_folder){p.warning("当前环境不支持文件夹选择,请在本机客户端中打开");return}try{const s=await e.select_brand_folder();if(!s)return;const t=await ds(s);if(!t.success||!t.items?.length){p.warning(t.error||"该文件夹下没有 xlsx 文件");return}$.value=t.items.map(n=>n.relativePath||n.absolutePath),j.value=await Se(t.items),p.success(`已选择文件夹内 ${t.items.length} 个 xlsx 文件`)}catch(s){p.error(s instanceof Error?s.message:"选择失败")}}async function je(){if(!j.value.length){p.warning("请先选择待处理文件");return}try{B.value=!0;const e=await as({files:j.value.map(a=>({fileKey:a.fileKey,originalFilename:a.originalFilename,relativePath:a.relativePath}))}),s=ge(e.items||[]),t=s.some(a=>a.matchStatus==="MATCHED"),n=s.some(a=>a.matchStatus&&a.matchStatus!=="MATCHED");b.value=t?"解析完成,可在左侧点击“推送到 Python 队列”开始串行处理":"解析完成,当前没有可推送的已匹配文件",S.value="",s.length&&s[0].taskId&&te(s[0].taskId,s,b.value,S.value),x(),n?p.warning("部分文件尚未命中可用店铺索引,已保留状态信息,请等待后台刷新后重试。"):t?p.success("删除品牌解析完成,请在左侧推送到 Python 队列"):p.warning("当前没有可推送的已匹配文件,请先等待店铺索引刷新。"),await O()}catch(e){p.error(e instanceof Error?e.message:"执行失败")}finally{B.value=!1}}async function Te(e){const s=e.taskId;if(!s){p.warning("未找到任务ID");return}const t=G(),n=ls(s),a=e.outputFilename||e.sourceFilename||`delete-brand_${s}.xlsx`;if(t?.save_file_from_url_new){const o=await t.save_file_from_url_new(n,a);o.success?p.success(`已保存:${o.path||a}`):o.error&&o.error!=="用户取消"&&p.error(o.error);return}}async function O(){try{const e=await rs();q.value=ge(e.items||[]),Be(q.value),x(),await re(),ue(!0)}catch(e){console.error("loadHistory error:",e)}}function Ce(e){i.value.forEach(t=>{t.items=t.items.filter(n=>n.resultId!==e)});const s=i.value.length;i.value=i.value.filter(t=>t.items.length>0),i.value.length!==s&&typeof window<"u"&&window.localStorage.setItem(N(),JSON.stringify(i.value)),q.value=q.value.filter(t=>t.resultId!==e),x(),L().length===0&&be()}async function Fe(e){try{await os(e),Ce(e),await O(),p.success("已删除")}catch(s){const t=s instanceof Error?s.message:"删除失败";if(typeof t=="string"&&t.includes("记录不存在")){Ce(e),p.success("已删除");return}p.error(t)}}function Ee(e){const s=e.slice(0,5).map(n=>`${n.country}:${n.asin}${n.status?`(${n.status})`:""}`);return e.length-s.length>0?`${s.join("、")} 等 ${e.length} 条`:s.join("、")}function kt(e,s){}function ie(){U.value&&(window.clearTimeout(U.value),U.value=null)}function ze(e=3e3){ie(),U.value=window.setTimeout(()=>{U.value=null,$e().catch(()=>{})},e)}function Ve(e){return e.includes("当前店铺正在执行中")}function Pe(){for(const e of i.value)for(const s of e.items){const t=E(s);if(t==="已入队"||t==="处理中")return!0}return!1}function K(e){return e.resultId!=null?`result:${e.resultId}`:`task:${e.taskId||0}:${e.sourceFilename||""}`}function Re(e){return!!(e.taskId&&(e.matchStatus==="MATCHED"||e.matched))}function xe(){return F.value.find(e=>!Re(e)||K(e)===D.value||R(e.taskId,e)?._completed?!1:E(e)==="未入队")}async function Ne(e,s){if(s?.auto&&Pe())return e.taskId,e.sourceFilename,{success:!1,retryable:!0};const t=G(),n=e.taskId;if(!n)return{success:!1,retryable:!1};const a=i.value.find(c=>c.taskId===n);if(!a)return{success:!1,retryable:!1};if(!t?.enqueue_json)return p.error("当前环境未启用 pywebview enqueue_json(浏览器环境不会推送)"),{success:!1,retryable:!1};const o={type:"delete-brand-run",ts:Date.now(),data:{taskId:n,items:[e]}};S.value=JSON.stringify(o,null,2);const r=await t.enqueue_json(o);let u="";if(r?.success){u=`已推送文件 ${e.sourceFilename},当前队列长度:${r.queue_size??"-"}`;const c=R(n,e);return c&&(c._pushed=!0,c._completed=!1),D.value=K(e),te(n,a.items,u,S.value),ue(!0),b.value=u,{success:!0,retryable:!1}}else{const c=r?.error||"未知错误";if(u=`推送到 Python 队列失败:${c}`,b.value=u,te(n,a.items,u,S.value),s?.auto&&Ve(c))return{success:!1,retryable:!0}}return b.value=u,{success:!1,retryable:!1}}async function Qe(){const e=xe();if(!e){p.warning("当前没有可推送的已匹配文件");return}V.value=!0,A.value=!0,ie();const s=await Ne(e);if(V.value=!1,!s.success){A.value=!1,D.value="";return}p.success("已开始按顺序推送到 Python 队列")}async function $e(){if(!A.value||Q.value){A.value,Q.value;return}if(Pe())return;const e=D.value;if(e){const t=F.value.find(n=>K(n)===e);if(t){const n=t.taskId,a=P(n);a&&(pe(n),E(t),void 0);const o=E(t);if(o==="已入队"||o==="处理中"){t.sourceFilename;return}if(a){const r=R(n,t);if(r&&!r._completed){t.sourceFilename,r._pushed,r._completed;return}}}}const s=xe();if(!s){D.value="",A.value=!1;return}Q.value=!0;try{s.taskId,s.sourceFilename,K(s);const t=await Ne(s,{auto:!0});if(!t.success){if(t.retryable){s.taskId,s.sourceFilename,ze();return}s.taskId,s.sourceFilename,A.value=!1}}finally{Q.value=!1}}return Ye(()=>{Me();const e=L();e.length>0?re(e).then(async()=>{let s=!1,t=!1;for(const n of e)we(n)&&(ve(n),s=!0,t=!0);t?await O():s&&x(),ue(!0)}):x(),O().catch(()=>{})}),Ze(()=>{be()}),(e,s)=>(d(),f("div",fs,[es(ss,{active:"delete-brand"}),l("div",vs,[l("aside",hs,[s[2]||(s[2]=l("div",{class:"section-title"},"选择文件",-1)),l("div",ps,[s[0]||(s[0]=l("div",{class:"hint"},"上传删除品牌 Excel;无论是直接上传文件还是上传文件夹批量导入,都会按每个 Excel 文件名作为店铺名。",-1)),l("div",{class:"btns"},[l("button",{type:"button",class:"opt-btn",onClick:Je},"选择 Excel 文件"),l("button",{type:"button",class:"opt-btn",onClick:Ge},"选择文件夹")]),l("div",_s,[$.value.length?(d(),f(H,{key:0},[(d(!0),f(H,null,de(se.value,t=>(d(),f("span",{key:t},v(t),1))),128)),$.value.length>se.value.length?(d(),f("span",ys," 还有 "+v($.value.length-se.value.length)+" 个文件未展开显示 ",1)):h("",!0)],64)):(d(),f("span",gs,"暂未选择删除品牌文件"))])]),s[3]||(s[3]=ts('
执行说明
',2)),l("div",ms,[l("button",{type:"button",class:"btn-run",disabled:B.value,onClick:je},v(B.value?"解析中...":"开始解析"),9,ks),l("button",{type:"button",class:"btn-run btn-queue",disabled:V.value||!Ue.value.length,onClick:Qe},v(V.value?"推送中...":"推送到 Python 队列"),9,ws),l("span",Is,v(B.value?"正在读取删除品牌数据,请稍候…":"解析后会在右侧展示按国家分组的去重结果"),1)]),b.value||S.value?(d(),f("div",bs,[s[1]||(s[1]=l("div",{class:"section-title queue-debug-title"},"Python 队列推送结果",-1)),b.value?(d(),f("div",Ss,v(b.value),1)):h("",!0),S.value?(d(),f("pre",Ts,v(S.value),1)):h("",!0)])):h("",!0)]),l("section",Cs,[s[12]||(s[12]=l("div",{class:"panel-header"},"删除品牌结果",-1)),l("div",Fs,[l("div",Es,[l("div",Ps,[s[4]||(s[4]=l("span",{class:"summary-label"},"已处理文件",-1)),l("strong",null,v(z.value.total),1)]),l("div",Rs,[s[5]||(s[5]=l("span",{class:"summary-label"},"成功结果",-1)),l("strong",null,v(z.value.successCount),1)]),l("div",xs,[s[6]||(s[6]=l("span",{class:"summary-label"},"失败文件",-1)),l("strong",null,v(z.value.failedCount),1)])]),l("div",Ns,[s[11]||(s[11]=l("div",{class:"result-list-header"},[l("span",null,"删除品牌结果列表")],-1)),qe.value?(d(),f(H,{key:1},[F.value.length?(d(),f("div",As,[s[8]||(s[8]=l("div",{class:"result-subsection-title"},"当前任务",-1)),l("ul",Ds,[(d(!0),f(H,null,de(F.value,t=>(d(),f("li",{key:`current-${t.taskId||t.resultId||t.sourceFilename}`,class:"task-item split-result-item"},[l("div",Ms,[l("span",{class:"id",title:t.sourceFilename},v(t.sourceFilename||"-"),9,Bs),l("div",qs,"店铺名:"+v(t.shopName||"-"),1),l("div",Us,"匹配结果:"+v(me(t)),1),t.platform?(d(),f("div",Ls,"平台:"+v(t.platform),1)):h("",!0),E(t)?(d(),f("div",Os,"队列状态:"+v(E(t)),1)):h("",!0),t.countryCount!==void 0&&t.matched?(d(),f("div",Ks,"国家数:"+v(t.countryCount),1)):h("",!0),t.totalRows!==void 0&&t.matched?(d(),f("div",Hs,"去重后 "+v(t.totalRows)+" 条 ",1)):h("",!0),ye(t)?(d(),f("div",Js,[l("div",Gs,[s[7]||(s[7]=l("span",null,"任务进度",-1)),l("span",null,v(Y(t))+"%",1)]),l("div",js,[l("div",{class:"delete-brand-progress-bar-fill",style:Ae({width:`${Y(t)}%`})},null,4)]),l("div",zs,v(ke(t)),1)])):h("",!0),t.previewRows?.length?(d(),f("div",Vs,v(Ee(t.previewRows)),1)):h("",!0),W(t)?(d(),f("div",Qs,"错误信息:"+v(W(t)),1)):h("",!0)]),l("div",Xs,[l("span",{class:De(["status",Z(t).className])},v(Z(t).text),3),Ie(t)?(d(),f("button",{key:0,type:"button",class:"download",onClick:n=>Te(t)}," 下载 ",8,Ws)):h("",!0),t.resultId?(d(),f("button",{key:1,type:"button",class:"btn-delete",onClick:n=>Fe(t.resultId)}," 删除 ",8,Ys)):h("",!0)])]))),128))])])):h("",!0),X.value.length?(d(),f("div",Zs,[s[10]||(s[10]=l("div",{class:"result-subsection-title"},"历史记录",-1)),l("ul",et,[(d(!0),f(H,null,de(X.value,t=>(d(),f("li",{key:`history-${t.taskId||t.resultId||t.sourceFilename}`,class:"task-item split-result-item"},[l("div",st,[l("span",{class:"id",title:t.sourceFilename},v(t.sourceFilename||"-"),9,tt),l("div",nt,"店铺名:"+v(t.shopName||"-"),1),l("div",at,"匹配结果:"+v(me(t)),1),t.platform?(d(),f("div",lt,"平台:"+v(t.platform),1)):h("",!0),t.countryCount!==void 0&&t.matched?(d(),f("div",rt,"国家数:"+v(t.countryCount),1)):h("",!0),t.totalRows!==void 0&&t.matched?(d(),f("div",ot,"去重后 "+v(t.totalRows)+" 条 ",1)):h("",!0),ye(t)?(d(),f("div",ut,[l("div",it,[s[9]||(s[9]=l("span",null,"任务进度",-1)),l("span",null,v(Y(t))+"%",1)]),l("div",ct,[l("div",{class:"delete-brand-progress-bar-fill",style:Ae({width:`${Y(t)}%`})},null,4)]),l("div",dt,v(ke(t)),1)])):h("",!0),t.previewRows?.length?(d(),f("div",ft,v(Ee(t.previewRows)),1)):h("",!0),W(t)?(d(),f("div",vt,"错误信息:"+v(W(t)),1)):h("",!0)]),l("div",ht,[l("span",{class:De(["status",Z(t).className])},v(Z(t).text),3),Ie(t)?(d(),f("button",{key:0,type:"button",class:"download",onClick:n=>Te(t)}," 下载 ",8,pt)):h("",!0),t.resultId?(d(),f("button",{key:1,type:"button",class:"btn-delete",onClick:n=>Fe(t.resultId)}," 删除 ",8,_t)):h("",!0)])]))),128))])])):h("",!0)],64)):(d(),f("div",$s," 暂无删除品牌结果,完成解析后会在这里展示按国家分组的去重结果 "))])])])])]))}}),gt=us(yt,[["__scopeId","data-v-04852f65"]]);is(gt).use(cs).mount("#app"); diff --git a/app/assets/split.js b/app/assets/split.js index 61f6d5c..94ea5cf 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-D808cNhy.js";import{e as as}from"./brand-DRn3N4FA.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-DiP0HdY6.js";import{e as as}from"./brand-BATjYfEL.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 6eeb71d..dd3590c 100644 --- a/app/new_web_source/convert.html +++ b/app/new_web_source/convert.html @@ -5,9 +5,9 @@ 格式转换 - 数富AI - - - + + + diff --git a/app/new_web_source/dedupe.html b/app/new_web_source/dedupe.html index b272de0..419ddc9 100644 --- a/app/new_web_source/dedupe.html +++ b/app/new_web_source/dedupe.html @@ -5,9 +5,9 @@ 数据去重 - 数富AI - - - + + + diff --git a/app/new_web_source/delete-brand.html b/app/new_web_source/delete-brand.html index e210015..a9f1ac1 100644 --- a/app/new_web_source/delete-brand.html +++ b/app/new_web_source/delete-brand.html @@ -5,9 +5,9 @@ 删除品牌 - 数富AI - - - + + + diff --git a/app/new_web_source/split.html b/app/new_web_source/split.html index c4bcbe5..6e0fdd4 100644 --- a/app/new_web_source/split.html +++ b/app/new_web_source/split.html @@ -5,9 +5,9 @@ 数据拆分 - 数富AI - - - + + + diff --git a/backend-java/aiimage-backend.log.2026-04-12.0.gz b/backend-java/aiimage-backend.log.2026-04-12.0.gz new file mode 100644 index 0000000000000000000000000000000000000000..e5cb5c451133dcff42edf31482a0a6a5d88f08a7 GIT binary patch literal 89796 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 Ot^I$rADR_0A`Ji{ta>E? literal 0 HcmV?d00001 diff --git a/backend-java/aiimage-backend.log.2026-04-13.0.gz b/backend-java/aiimage-backend.log.2026-04-13.0.gz new file mode 100644 index 0000000000000000000000000000000000000000..a6fe2299c55594c35a1425e9d6dd123b78d44e10 GIT binary patch literal 48156 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'; } function bindSkipPriceAsinActions() { + document.querySelectorAll('[data-skip-price-asin-edit]').forEach(function(btn) { + btn.onclick = function() { + var shopName = (btn.dataset.shopName || '').replace(/"/g, '"'); + var countryCode = btn.dataset.country || ''; + var currentAsin = (btn.dataset.asin || '').replace(/"/g, '"'); + var nextAsin = prompt('请输入店铺“' + shopName + '”在 ' + countryCode + ' 的 ASIN', currentAsin); + if (nextAsin === null) return; + nextAsin = (nextAsin || '').trim(); + if (!nextAsin) { + alert('ASIN 不能为空'); + return; + } + fetch('/api/admin/skip-price-asin/' + btn.dataset.skipPriceAsinEdit + '/country/' + countryCode, { + method: 'PUT', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ asin: nextAsin }) + }) + .then(function(r) { return r.json(); }) + .then(function(res) { + if (res.success) loadSkipPriceAsin(skipPriceAsinPage); + else alert(res.error || '保存失败'); + }); + }; + }); document.querySelectorAll('[data-skip-price-asin-delete]').forEach(function(btn) { btn.onclick = function() { var shopName = (btn.dataset.shopName || '').replace(/"/g, '"'); diff --git a/frontend-vue/src/pages/brand/components/BrandPriceTrackTab.vue b/frontend-vue/src/pages/brand/components/BrandPriceTrackTab.vue index 2cb4e75..3b4f9a8 100644 --- a/frontend-vue/src/pages/brand/components/BrandPriceTrackTab.vue +++ b/frontend-vue/src/pages/brand/components/BrandPriceTrackTab.vue @@ -90,26 +90,6 @@
保存中…
- -
价格监控设置
-
-
- 最低价 - - EUR -
-
- 最高价 - - EUR -
-
- 价差比例 - - % -
-
-
@@ -257,6 +237,7 @@ import { getPriceTrackDashboard, getPriceTrackHistory, getPriceTrackResultDownloadUrl, + getPriceTrackTasksBatch, listPriceTrackCandidates, matchPriceTrackShops, putPriceTrackCountryPreference, @@ -264,6 +245,7 @@ import { type PriceTrackDashboardVo, type PriceTrackHistoryItem, type PriceTrackShopQueueItem, + type PriceTrackTaskDetailVo, } from '@/shared/api/java-modules' // ========== 类型定义 ========== @@ -293,11 +275,6 @@ const asinModeEnabled = ref(false) // 按指定ASIN文档(自定义) // 上传文件 const asinFiles = ref([]) // ASIN文档 -// 价格监控 -const minPrice = ref(undefined) -const maxPrice = ref(undefined) -const priceDiffPercent = ref(undefined) - // 是否有有效模式 const hasValidMode = computed(() => statusModeEnabled.value || asinModeEnabled.value) @@ -317,10 +294,113 @@ const dashboard = ref({ const historyItems = ref([]) const pollingTaskIds = ref([]) const taskDetails = ref>({}) -const taskSnapshots = ref>({}) +const taskSnapshots = ref>({}) const pollTimer = ref(null) const pollingInFlight = ref(false) +function uidForStorage() { + return typeof window !== 'undefined' ? window.localStorage.getItem('uid') || '0' : '0' +} + +function sessionKey() { + return `price-track:tasks:${uidForStorage()}` +} + +function taskStatusStorageKey() { + return `price-track:task-status:${uidForStorage()}` +} + +function taskSnapshotsStorageKey() { + return `price-track:tasks-batch-snapshot:${uidForStorage()}` +} + +function matchedItemsStorageKey() { + return `price-track:matched-items:${uidForStorage()}` +} + +function setStorageJson(key: string, value: unknown, shouldRemove: boolean) { + if (typeof window === 'undefined') return + try { + if (shouldRemove) window.localStorage.removeItem(key) + else window.localStorage.setItem(key, JSON.stringify(value)) + } catch { + /* quota */ + } +} + +function loadPollingIdsFromStorage() { + try { + const raw = typeof window !== 'undefined' ? window.localStorage.getItem(sessionKey()) : null + if (!raw) { + pollingTaskIds.value = [] + return + } + const parsed = JSON.parse(raw) as unknown + pollingTaskIds.value = Array.isArray(parsed) ? parsed.filter((n): n is number => typeof n === 'number' && n > 0) : [] + } catch { + pollingTaskIds.value = [] + } +} + +function savePollingIds() { + setStorageJson(sessionKey(), pollingTaskIds.value, pollingTaskIds.value.length === 0) +} + +function loadTaskDetailsFromStorage() { + try { + const raw = typeof window !== 'undefined' ? window.localStorage.getItem(taskStatusStorageKey()) : null + if (!raw) return + const parsed = JSON.parse(raw) as Record + const out: Record = {} + for (const [k, v] of Object.entries(parsed)) { + const id = Number(k) + if (id > 0 && typeof v === 'string') out[id] = v + } + taskDetails.value = out + } catch { + /* ignore */ + } +} + +function saveTaskDetailsToStorage() { + setStorageJson(taskStatusStorageKey(), taskDetails.value, Object.keys(taskDetails.value).length === 0) +} + +function loadTaskSnapshotsFromStorage() { + try { + const raw = typeof window !== 'undefined' ? window.localStorage.getItem(taskSnapshotsStorageKey()) : null + if (!raw) return + const parsed = JSON.parse(raw) as Record + const out: Record = {} + for (const [k, v] of Object.entries(parsed)) { + const id = Number(k) + if (id > 0 && v && typeof v === 'object') out[id] = v as PriceTrackTaskDetailVo + } + taskSnapshots.value = out + } catch { + /* ignore */ + } +} + +function saveTaskSnapshotsToStorage() { + setStorageJson(taskSnapshotsStorageKey(), taskSnapshots.value, Object.keys(taskSnapshots.value).length === 0) +} + +function loadMatchedItemsFromStorage() { + try { + const raw = typeof window !== 'undefined' ? window.localStorage.getItem(matchedItemsStorageKey()) : null + if (!raw) return + const parsed = JSON.parse(raw) as unknown + if (Array.isArray(parsed)) matchedItems.value = parsed as PriceTrackShopQueueItem[] + } catch { + /* ignore */ + } +} + +function saveMatchedItemsToStorage() { + setStorageJson(matchedItemsStorageKey(), matchedItems.value, matchedItems.value.length === 0) +} + // ========== 文件上传 ========== async function selectAsinFile() { const api = getPywebviewApi() @@ -463,6 +543,15 @@ async function loadHistory() { } } +function syncPollingIdsWithHistory() { + for (const taskId of [...pollingTaskIds.value]) { + const any = historyItems.value.some((r) => r.taskId === taskId) + if (!any && !taskSnapshots.value[taskId]) { + removePollingTask(taskId) + } + } +} + function onSelectionChange(rows: PriceTrackCandidateVo[]) { selectedCandidates.value = rows || [] } @@ -491,6 +580,7 @@ async function removeCandidate(row: PriceTrackCandidateVo) { await deletePriceTrackCandidate(row.id) await loadCandidates() matchedItems.value = [] + saveMatchedItemsToStorage() ElMessage.success('已删除') } catch (e) { ElMessage.error(e instanceof Error ? e.message : '删除失败') @@ -519,6 +609,7 @@ async function runMatch() { const res = await matchPriceTrackShops(names) const batch = res.items || [] matchedItems.value = [...matchedItems.value, ...batch.filter((n) => !matchedItems.value.some((e) => e.shopName === n.shopName))] + saveMatchedItemsToStorage() ElMessage.success(`匹配 ${batch.length} 个店铺`) } catch (e) { ElMessage.error(e instanceof Error ? e.message : '匹配失败') @@ -549,11 +640,32 @@ function removeMatchedRowsLocally(rows: Array<{ shopName?: string; shopId?: numb if (!rows.length) return const keys = new Set(rows.map((row) => rowKeyForMatch(row))) matchedItems.value = matchedItems.value.filter((row) => !keys.has(rowKeyForMatch(row))) + saveMatchedItemsToStorage() } async function removeMatchedRow(row: PriceTrackShopQueueItem) { - removeMatchedRowsLocally([row]) - ElMessage.success('已从匹配结果中移除') + const key = rowKeyForMatch(row) + const backup = [...matchedItems.value] + matchedItems.value = matchedItems.value.filter((r) => rowKeyForMatch(r) !== key) + saveMatchedItemsToStorage() + const name = (row.shopName || '').trim() + if (!name) { + ElMessage.success('已从匹配结果中移除') + return + } + try { + const res = await deletePendingPriceTrackShopResult(name) + if (res?.removed) { + await loadHistory() + await loadDashboard() + syncPollingIdsWithHistory() + } + ElMessage.success('已从匹配结果中移除') + } catch (e) { + matchedItems.value = backup + saveMatchedItemsToStorage() + ElMessage.error(e instanceof Error ? e.message : '同步后端失败') + } } async function pushToPythonQueue() { @@ -582,11 +694,21 @@ async function pushToPythonQueue() { items: matchedRows as unknown as Record[], asinFiles: asinFiles.value, countryCodes: [...orderedCountryCodes.value], - minPrice: minPrice.value, - maxPrice: maxPrice.value, - priceDiffPercent: priceDiffPercent.value, } const taskVo = await createPriceTrackTask(taskReq) + taskSnapshots.value = { + ...taskSnapshots.value, + [taskVo.taskId]: { + task: { id: taskVo.taskId, status: 'RUNNING' }, + items: taskVo.items, + }, + } + taskDetails.value = { + ...taskDetails.value, + [taskVo.taskId]: 'RUNNING', + } + saveTaskSnapshotsToStorage() + saveTaskDetailsToStorage() // 2. 推送 taskId 给 Python,Python 会从 Java 拉取完整任务上下文 const queuePayload = { @@ -605,6 +727,7 @@ async function pushToPythonQueue() { if (pushResult?.success) { queuePushResult.value = `任务 ${taskVo.taskId} 已入队,等待执行完成...` addPollingTask(taskVo.taskId) + scheduleNextPoll(true) ElMessage.success('已推送到 Python 队列') } else { queuePushResult.value = `推送失败:${pushResult?.error || '未知错误'}` @@ -627,24 +750,37 @@ async function refreshTaskBatch() { const ids = pollingTaskIds.value.filter((id) => id > 0) if (!ids.length) return try { - await loadHistory() - // 检查是否有任务结束,停止轮询 - const terminalIds = ids.filter((id) => isTaskTerminal(id)) - for (const id of terminalIds) { - removePollingTask(id) + const batch = await getPriceTrackTasksBatch(ids) + let changed = false + const nextSnapshots = { ...taskSnapshots.value } + for (const missingId of batch.missingTaskIds || []) { + removePollingTask(missingId) + delete nextSnapshots[missingId] } + for (const detail of batch.items || []) { + const taskId = detail.task?.id + const status = detail.task?.status + if (typeof taskId !== 'number' || taskId <= 0) continue + nextSnapshots[taskId] = detail + if (status) { + taskDetails.value[taskId] = status + if (status === 'SUCCESS' || status === 'FAILED') { + removePollingTask(taskId) + changed = true + } + } + } + taskSnapshots.value = nextSnapshots + saveTaskSnapshotsToStorage() + saveTaskDetailsToStorage() + await loadHistory() + syncPollingIdsWithHistory() + if (changed) await loadDashboard() } catch { /* polling noise */ } } -function isTaskTerminal(taskId: number) { - const rows = historyItems.value.filter((r) => r.taskId === taskId) - if (!rows.length) return false - // 全部行都有终态才停止 - return rows.every((r) => r.taskStatus === 'SUCCESS' || r.taskStatus === 'FAILED') -} - function scheduleNextPoll(immediate = false) { if (pollTimer.value) { if (!immediate) return @@ -670,17 +806,33 @@ function stopPolling() { function addPollingTask(taskId: number) { if (!pollingTaskIds.value.includes(taskId)) { pollingTaskIds.value = [...pollingTaskIds.value, taskId] + savePollingIds() } } function removePollingTask(taskId: number) { pollingTaskIds.value = pollingTaskIds.value.filter((id) => id !== taskId) + savePollingIds() delete taskDetails.value[taskId] + saveTaskDetailsToStorage() + if (taskSnapshots.value[taskId]) { + const next = { ...taskSnapshots.value } + delete next[taskId] + taskSnapshots.value = next + saveTaskSnapshotsToStorage() + } } -const currentSectionItems = computed(() => - historyItems.value.filter((row) => row.taskId && pollingTaskIds.value.includes(row.taskId) && !isTaskTerminalById(row.taskId)) -) +const currentSectionItems = computed(() => { + const out = historyItems.value.filter((row) => row.taskId && pollingTaskIds.value.includes(row.taskId) && !isTaskTerminalById(row.taskId)) + const existingTaskIds = new Set(out.map((row) => row.taskId).filter((id): id is number => typeof id === 'number')) + for (const taskId of pollingTaskIds.value) { + if (existingTaskIds.has(taskId)) continue + const first = taskSnapshots.value[taskId]?.items?.[0] + if (first) out.push(first) + } + return out +}) const historySectionItems = computed(() => historyItems.value.filter((row) => !row.taskId || !pollingTaskIds.value.includes(row.taskId) || isTaskTerminalById(row.taskId)) @@ -690,7 +842,7 @@ const hasVisibleList = computed(() => currentSectionItems.value.length > 0 || hi function taskStatusOf(taskId?: number) { if (!taskId) return '' - return taskDetails.value[taskId] || '' + return taskDetails.value[taskId] || taskSnapshots.value[taskId]?.task?.status || '' } function isTaskTerminalById(taskId?: number) { @@ -729,7 +881,7 @@ function canDownload(item: PriceTrackHistoryItem) { async function downloadResult(item: PriceTrackHistoryItem) { if (!item.resultId) return const api = getPywebviewApi() - const filename = item.outputFilename || `${item.shopName || 'result'}.zip` + const filename = item.outputFilename || `${item.shopName || 'result'}.xlsx` if (!api?.save_file_from_url_new) { ElMessage.error('当前客户端未提供 save_file_from_url_new,无法下载文件') return @@ -747,10 +899,14 @@ async function deleteTaskRecord(item: PriceTrackHistoryItem) { try { if (item.resultId != null) { await deletePriceTrackHistory(item.resultId) - if (item.taskId != null) removePollingTask(item.taskId) + } else if (item.taskId != null) { + await deletePriceTrackTask(item.taskId) } + if (item.taskId != null) removePollingTask(item.taskId) + removeMatchedRowsLocally([item]) await loadHistory() await loadDashboard() + syncPollingIdsWithHistory() ElMessage.success('已删除') } catch (e) { ElMessage.error(e instanceof Error ? e.message : '删除失败') @@ -759,6 +915,10 @@ async function deleteTaskRecord(item: PriceTrackHistoryItem) { // ========== 生命周期 ========== onMounted(() => { + loadPollingIdsFromStorage() + loadTaskDetailsFromStorage() + loadTaskSnapshotsFromStorage() + loadMatchedItemsFromStorage() loadCandidates().catch(() => undefined) loadCountryPreference().catch(() => undefined) loadHistory().catch(() => undefined) @@ -1085,49 +1245,12 @@ onUnmounted(() => { color: #ccc; } -.price-filter-zone { - border: 1px dashed #3a3a3a; - border-radius: 10px; - padding: 14px; - background: #252525; - margin-bottom: 16px; - display: flex; - flex-direction: column; - gap: 10px; -} -.price-input-row { - display: flex; - align-items: center; - gap: 10px; -} -.price-label { - font-size: 12px; - color: #aaa; - width: 60px; - flex-shrink: 0; -} -.price-input { - flex: 1; -} -.price-unit { - font-size: 12px; - color: #888; - width: 30px; - flex-shrink: 0; -} -.price-input-row :deep(.el-input__wrapper) { - background-color: #2a2a2a; - box-shadow: 0 0 0 1px #3a3a3a inset; -} -.price-input-row :deep(.el-input__inner) { - color: #ccc; -} .empty-candidates { color: #666; diff --git a/frontend-vue/src/pages/brand/components/BrandShopMatchTab.vue b/frontend-vue/src/pages/brand/components/BrandShopMatchTab.vue index 9df24a1..ec6f4bb 100644 --- a/frontend-vue/src/pages/brand/components/BrandShopMatchTab.vue +++ b/frontend-vue/src/pages/brand/components/BrandShopMatchTab.vue @@ -215,6 +215,7 @@ async function loadCandidates() { candidates.value = await listShopMatchCandidat async function loadDashboard() { dashboard.value = await getShopMatchDashboard() } async function loadHistory() { const data = await getShopMatchHistory(); historyItems.value = data.items || [] } async function loadCountryPreference() { try { const data = await getShopMatchCountryPreference(); if (countryPrefUserTouched.value) return; const codes = (data.country_codes || []).filter((item) => COUNTRY_OPTIONS.some((option) => option.code === item)); if (codes.length) orderedCountryCodes.value = codes } catch {} } +async function refreshTaskViewsBestEffort() { await Promise.allSettled([loadHistory(), loadDashboard()]) } function scheduleSaveCountryPreference() { if (countryPrefSaveTimer) clearTimeout(countryPrefSaveTimer); countryPrefSaveTimer = setTimeout(() => { countryPrefSaveTimer = null; void saveCountryPreference() }, 450) } async function saveCountryPreference() { if (!orderedCountryCodes.value.length) return; countryPrefSaving.value = true; try { await putShopMatchCountryPreference(orderedCountryCodes.value) } catch (error) { ElMessage.error(error instanceof Error ? error.message : '保存国家配置失败') } finally { countryPrefSaving.value = false } } function onSelectionChange(rows: ShopMatchCandidateVo[]) { selectedCandidates.value = rows } @@ -252,7 +253,7 @@ function getPollIntervalMs() { return document.visibilityState === 'visible' ? 4 function scheduleNextPoll(immediate = false) { if (pollTimer.value) { if (!immediate) return; window.clearTimeout(pollTimer.value); pollTimer.value = null } const run = async () => { pollTimer.value = null; if (pollingInFlight.value || !pollingTaskIds.value.length) { if (pollingTaskIds.value.length) scheduleNextPoll(); return } pollingInFlight.value = true; try { const { settledTaskIds } = await refreshTaskBatch(); if (settledTaskIds.length) await Promise.allSettled([loadHistory(), loadDashboard()]) } finally { pollingInFlight.value = false } if (pollingTaskIds.value.length) pollTimer.value = window.setTimeout(run, getPollIntervalMs()) }; if (immediate) void run(); else pollTimer.value = window.setTimeout(run, getPollIntervalMs()) } function ensurePolling(immediate = false) { scheduleNextPoll(immediate) } function stopPolling() { if (pollTimer.value) { window.clearTimeout(pollTimer.value); pollTimer.value = null } } -async function waitForTaskTerminal(taskId: number) { while (true) { const batch = await getShopMatchTasksBatch([taskId]); if ((batch.missingTaskIds || []).includes(taskId)) { removeTaskLocally(taskId); await loadHistory(); await loadDashboard(); restoreScheduledDispatches(); return 'FAILED' } const detail = (batch.items || []).find((item) => item.task?.id === taskId); const status = detail?.task?.status || ''; if (detail) { taskSnapshots.value = { ...taskSnapshots.value, [taskId]: detail }; saveTaskSnapshotsToStorage() } if (status) { taskDetails.value[taskId] = status; saveTaskDetailsToStorage() } if (isTaskTerminalStatus(status)) { removePollingTask(taskId); await loadHistory(); await loadDashboard(); restoreScheduledDispatches(); return status } await new Promise((resolve) => { window.setTimeout(() => resolve(), getPollIntervalMs()) }) } } +async function waitForTaskTerminal(taskId: number) { while (true) { const batch = await getShopMatchTasksBatch([taskId]); if ((batch.missingTaskIds || []).includes(taskId)) { removeTaskLocally(taskId); await refreshTaskViewsBestEffort(); restoreScheduledDispatches(); return 'FAILED' } const detail = (batch.items || []).find((item) => item.task?.id === taskId); const status = detail?.task?.status || ''; if (detail) { taskSnapshots.value = { ...taskSnapshots.value, [taskId]: detail }; saveTaskSnapshotsToStorage() } if (status) { taskDetails.value[taskId] = status; saveTaskDetailsToStorage() } if (isTaskTerminalStatus(status)) { removePollingTask(taskId); await refreshTaskViewsBestEffort(); restoreScheduledDispatches(); return status } await new Promise((resolve) => { window.setTimeout(() => resolve(), getPollIntervalMs()) }) } } function resolvedTaskStatus(item: ShopMatchHistoryItem) { const taskId = normalizeTaskId(item.taskId); if (!taskId) return item.taskStatus || ''; return taskDetails.value[taskId] || taskSnapshots.value[taskId]?.task?.status || item.taskStatus || '' } function taskSnapshotOf(item: ShopMatchHistoryItem) { const taskId = normalizeTaskId(item.taskId); return taskId ? taskSnapshots.value[taskId] : undefined } function currentTaskStageText(item: ShopMatchHistoryItem) { const snapshot = taskSnapshotOf(item); const task = snapshot?.task; const stages = task?.scheduleStages || []; if (!stages.length) return ''; const total = stages.length; const activeIndex = typeof task?.activeStageIndex === 'number' ? task.activeStageIndex : undefined; const currentIndex = typeof task?.currentStageIndex === 'number' ? task.currentStageIndex : undefined; if (typeof activeIndex === 'number') return `执行进度: 第 ${activeIndex + 1}/${total} 次执行中`; if (typeof currentIndex === 'number') return `执行进度: 等待第 ${currentIndex + 1}/${total} 次`; return `执行进度: 共 ${total} 次`} diff --git a/frontend-vue/src/pages/brand/components/listingFilters.ts b/frontend-vue/src/pages/brand/components/listingFilters.ts index 34e4f84..5bfff80 100644 --- a/frontend-vue/src/pages/brand/components/listingFilters.ts +++ b/frontend-vue/src/pages/brand/components/listingFilters.ts @@ -2,6 +2,7 @@ export const LISTING_FILTER_OPTIONS = [ { value: 'SearchSuppressed', label: '在搜索结果中禁止显示' }, { value: 'ApprovalRequired', label: '需要批准' }, { value: 'Active', label: '在售' }, + { value: 'DetailPageRemoved', label: '详情页面已删除' }, ] as const export type ListingFilterValue = (typeof LISTING_FILTER_OPTIONS)[number]['value'] diff --git a/frontend-vue/src/shared/api/java-modules.ts b/frontend-vue/src/shared/api/java-modules.ts index 24973cf..9d1bc8a 100644 --- a/frontend-vue/src/shared/api/java-modules.ts +++ b/frontend-vue/src/shared/api/java-modules.ts @@ -972,9 +972,38 @@ export interface PriceTrackHistoryVo { export interface PriceTrackCreateTaskVo { taskId: number; - items: PriceTrackShopQueueItem[]; + items: PriceTrackHistoryItem[]; } +export interface PriceTrackTaskSummary { + id?: number; + taskNo?: string; + status?: string; + errorMessage?: string; + createdAt?: string; + updatedAt?: string; + finishedAt?: string; +} + +export interface PriceTrackTaskDetailVo { + task?: PriceTrackTaskSummary; + items?: PriceTrackHistoryItem[]; +} + +export interface PriceTrackTaskBatchVo { + items: PriceTrackTaskDetailVo[]; + missingTaskIds?: number[]; +} + +export interface PriceTrackPendingDeleteVo { + removed: boolean; +} + +export type PriceTrackCreateTaskPayload = Omit< + PriceTrackCreateTaskRequest, + "userId" +>; + export function listPriceTrackCandidates() { return unwrapJavaResponse( get>( @@ -985,9 +1014,9 @@ export function listPriceTrackCandidates() { export function addPriceTrackCandidate(shopName: string) { return unwrapJavaResponse( - post, { user_id: number; shopName: string }>( + post, { userId: number; shopName: string }>( `${JAVA_API_PREFIX}/price-track/candidates`, - { user_id: getCurrentUserId(), shopName }, + { userId: getCurrentUserId(), shopName }, ), ); } @@ -1010,18 +1039,18 @@ export function getPriceTrackCountryPreference() { export function putPriceTrackCountryPreference(countryCodes: string[]) { return unwrapJavaResponse( - put, { user_id: number; countryCodes: string[] }>( + put, { userId: number; countryCodes: string[] }>( `${JAVA_API_PREFIX}/price-track/country-preference`, - { user_id: getCurrentUserId(), countryCodes }, + { userId: getCurrentUserId(), countryCodes }, ), ); } export function matchPriceTrackShops(shopNames: string[]) { return unwrapJavaResponse( - post, { user_id: number; shopNames: string[] }>( + post, { userId: number; shopNames: string[] }>( `${JAVA_API_PREFIX}/price-track/match-shops`, - { user_id: getCurrentUserId(), shopNames }, + { userId: getCurrentUserId(), shopNames }, ), ); } @@ -1057,16 +1086,15 @@ export interface PriceTrackCreateTaskRequest { items: PriceTrackShopQueueItem[]; asinFiles: string[]; countryCodes: string[]; - minPrice?: number; - maxPrice?: number; - priceDiffPercent?: number; } -export function createPriceTrackTask(request: PriceTrackCreateTaskRequest) { +export function createPriceTrackTask( + request: PriceTrackCreateTaskPayload | PriceTrackCreateTaskRequest, +) { return unwrapJavaResponse( post, PriceTrackCreateTaskRequest>( `${JAVA_API_PREFIX}/price-track/tasks`, - { ...request, user_id: getCurrentUserId() }, + { ...request, userId: getCurrentUserId() }, ), ); } @@ -1079,13 +1107,22 @@ export function deletePriceTrackTask(taskId: number) { ); } +export function getPriceTrackTasksBatch(taskIds: number[]) { + return unwrapJavaResponse( + post, { taskIds: number[] }>( + `${JAVA_API_PREFIX}/price-track/tasks/batch`, + { taskIds }, + ), + ); +} + export function getPriceTrackResultDownloadUrl(resultId: number) { return getJavaDownloadUrl(`/price-track/results/${resultId}/download`); } export function deletePendingPriceTrackShopResult(shopName: string) { return unwrapJavaResponse( - del>( + del>( `${JAVA_API_PREFIX}/price-track/pending-shop-result?user_id=${encodeURIComponent(String(getCurrentUserId()))}&shop_name=${encodeURIComponent(shopName)}`, ), ); diff --git a/frontend-vue/src/shared/bridges/pywebview.ts b/frontend-vue/src/shared/bridges/pywebview.ts index 4bc2ffe..d642612 100644 --- a/frontend-vue/src/shared/bridges/pywebview.ts +++ b/frontend-vue/src/shared/bridges/pywebview.ts @@ -11,6 +11,15 @@ export interface PywebviewApi { minimize?: () => Promise; maximize?: () => Promise; toggle_maximize?: () => Promise; + select_files?: (options?: { + filters?: Array<{ + name?: string; + extensions?: string; + }>; + multiple?: boolean; + }) => Promise<{ + paths?: string[]; + } | null>; save_image?: ( urlOrData: string, filename?: string, diff --git a/new_web_source/convert.html b/new_web_source/convert.html new file mode 100644 index 0000000..dd3590c --- /dev/null +++ b/new_web_source/convert.html @@ -0,0 +1,16 @@ + + + + + + 格式转换 - 数富AI + + + + + + + +
+ + diff --git a/new_web_source/dedupe.html b/new_web_source/dedupe.html new file mode 100644 index 0000000..419ddc9 --- /dev/null +++ b/new_web_source/dedupe.html @@ -0,0 +1,16 @@ + + + + + + 数据去重 - 数富AI + + + + + + + +
+ + diff --git a/new_web_source/delete-brand.html b/new_web_source/delete-brand.html new file mode 100644 index 0000000..a9f1ac1 --- /dev/null +++ b/new_web_source/delete-brand.html @@ -0,0 +1,16 @@ + + + + + + 删除品牌 - 数富AI + + + + + + + +
+ + diff --git a/new_web_source/price-track.html b/new_web_source/price-track.html new file mode 100644 index 0000000..6a65d34 --- /dev/null +++ b/new_web_source/price-track.html @@ -0,0 +1,16 @@ + + + + + + 跟价 - 数富AI + + + + + + + +
+ + diff --git a/new_web_source/product-risk.html b/new_web_source/product-risk.html new file mode 100644 index 0000000..05698af --- /dev/null +++ b/new_web_source/product-risk.html @@ -0,0 +1,18 @@ + + + + + + 商品风险解决 - 数富AI + + + + + + + + + +
+ + diff --git a/new_web_source/shop-match.html b/new_web_source/shop-match.html new file mode 100644 index 0000000..a085585 --- /dev/null +++ b/new_web_source/shop-match.html @@ -0,0 +1,18 @@ + + + + + + 匹配店铺 - 数富AI + + + + + + + + + +
+ + diff --git a/new_web_source/split.html b/new_web_source/split.html new file mode 100644 index 0000000..6e0fdd4 --- /dev/null +++ b/new_web_source/split.html @@ -0,0 +1,16 @@ + + + + + + 数据拆分 - 数富AI + + + + + + + +
+ +

j)wGr0^9JxQ!nmLI=`bIj6?9z)wlEt_))nh=n=i!cE{- zR-ol(1*}9_uTzDgGo6~trCRwRcIFcy**#CshBq)(0Ap15=xZEoY=BQ)fB^v8evS3zC$tEekRqM~{q~fKR zJ}GwSkY0eXxpw6BqBPlAOs!|#Kzhf)=}4P1`emv<&QLF7i`1lul@`9N!`BZ&YHe3q zE;0VP9m zqcj;QX7TR_##G#o8ULD{=c9LdpM^1e=w?R{J>7=`BO92$i@ zh8^JI!DF<$@5PvLNB;U#x8Aa>!);sPq@_$KcX`KcvB$Z|_?L`6lP$Iko~KzR>~bOS zhyD!>B~8a`>U?gleAS;cBOjzDFb@RD&%9+{Sa&R6@yb+OF~4-;Pss!~FN-O>m?Ana zcBZlF^W-;@uBq3;a*X-sT(wyveIwXxK_XY8zE!Y=bt%#yYyGfNwBpsyj?iG?w4~hd z3tbHZ+hH3q(e1`663>H@;8xV$pi4lj7X;k>>S7}9@DUP20!VC8?7GpleOwcF4v9Q( z+b?muFP;-P%E@k_Cigh8NzCN(St{z`Y{@(G8h8JrK2~?~uxq>wSJg2GCL3_TQ%$uDmwllkXR$PG52lIPj)u9~WTz;~=u^@=Klnzfdjq6K4`udE>~D1TjuDnedP6A#gRI&9auT-jEya}Fk^eji#XlgPNB ze(lWp!OfF``W~0@qpB2ur~XKZuQu&XKta;cNLiluJkuNN0T+0cogmlqjB*QY_#O1> zl_W*2yu$_NS`=|aWF+G5=AvF~*A&)tRCfDdMII>PNWnbcB%Vbd;ZxBmn9-gA;yymu zy|-)+jux}}oQ?_TJ7r&9pmU^enl?`LUP=`{7g*xU-@SpC(QA!axqV46FhDH9@2{SV zRT#5h?sr{v5!@Jzk|U|sbj8h$l!NICq{SO=I%yXY3*3`317+Wio^_HO=qw@~z)JS_ z-;M119R{z0`(HF9d_dBt2>Up{^aSjFCJ88q`LX` zE{Ui#(|8T`;;|kg>dj5IOWw(BU#_a`#Rrm~?y3@w*=@t zrI#m}67LtpECU?VAUVkAv-_ev^M8V)HJneJ%t}34bX#yDUmXupV5uVyJ8iOW^G z+T7beoYWGcMm-i?D@nPN6Y|#NySWa%Xen4=JMS5uTx&a$TdGSRO}zk$h-2y=tz0j) zb0JDw8la{*zqKt>t&9^IItBE3bMx3^mebp8GsKgJ3*P*$D5!}(`2U7Vb9DUQQ0Y%E zPX14*v@2hr%*x`R@WC4PrS0{qyM48D_dBdG51lr*wa8R>t|xZGiPxgy)bad8!pEfl zhNZo677gSc{XgN-)6bTfmCED?0S0GxJr92R434f?EmYp~arVAJ!Ibr{0*sN z4fDlbjako`tbQt<8n60igca^oEWgh!K4JqbWvZ9}w{R#faJ-uEPN`EUmY>RzR4MP% z$mbHUegfQ4ZBd1w3}5{fJ+eFXYfID}Fh63)dab|l4fzRp9nOf><$VUSjzs!+I=(No z5<;t>IBqu%q8QHrSr$H5d0kS`Y)*LhDe+y>IM5`6m zcVY=7<2xiS;+rQbCN?tobME4s*Y#!w6dFM`1Po@HJI7B_7e+n-?F&L=)lb#iscf6M zoIaAy%&($t#i7#Y!ZuU!V-*wm(8DRT9ul4T)0e3Ed1>6{b4VRP^t4D7qW7t%Dm8U# z@s(npFyWTUE5a9reISI@&vq(d(&C}BzaDr2d}h*FQ(p^3u}*apn%Ao^7~E#*{E$Tx z%yeYgO}J*U7mxB{55bqNQko6a#RJTPceHsPL_A#ggEOmD!8-U$?rCVw+Y;PeWH*QSh^ zZFA2aF-Y~u9sw2AD5W9;g{LxSqkF~2UH1ZoZ|we{v#lBHuN6@2?BU3wiy`i5;iW`- z|BDMtrvHlzqrLvch0!Sy`g)xqSh zUH+|Wvi*`zcSy{0JS0F{w?EXs6Hqhm^Rwh@u8=5}aBGSQ%XhEjI|q$BYv6?dJzJ(3 zmKEQh$}zXBdV77{z=m?B!r4j&kF0upeO+*Ad5-P%nm769`k6`btnz#63^hgl_GAzs z{c-7%z8Zywtw;H=&jdwU)LsLWEC0c6cr{we{Y$!Upr35(%#shv!#)idU9Pbd#K_?l zhSrB|7`~fvG`9+}*~ZmH?@kv-9T#u86lbZIIW*#&selrOl|I|?0BJp~iT8Zy54((p1 zbe5c1{v01sVFI1bns0B2^egu?jb>PwikjU6-^G&f<*XXD#0$Z4N9`wFDuM4M3uyX< z0DV+0?B$NHm=m*+XtuKU&rwzxvD~#nRjsFBMY$|3e2Hwki=_7a9hRrj>3dMnxwV== z_`7>q&%Zl7aFMR`X9RJbt@1sb3l1moa!_wWJldUv&SPuE8g-(9WL!TnZ zw?GFQW23~Xx4ATg588mG#eNlc;1aa4XY56!29}i2LtzV>AwEYETR(cVP&ZeCJmnM9 zsdM=&9N@7Ohlhv*?+5J<0mZYJu<*&3kpUG+OSs|~r<4Q5_~r(H-)HWrJ`*s$nQ^*S zdbxn_V!aiIG6oeEacC8$11~EBZ>KVMOH-W}2cKi^-S-=`ve=U_mRjOXJGRQ@JV&i_ zO`kNjXIiDiuU6I{?5(}wASo6#nz<0!-K7cV?JfGs(+N6>lPgWhUY(QM7R(C>Vu~`fO-Kwm{#3Ua7ohztysSW zl8)HkrlgnFvQ&ZvwUDM#uJvdODFtA3cY}RNZ-BW^!2+$6Ia$0CQRXl^71ll z?Xu-n{Zb4J(_sP2@Dcju1VQIO6e8PB%Ljg1e(0HD^`Ke#*9%S89pqn=a@F6hyBV)K z4Wi1hUCWP382ndEd`s(|zDlDp>L3i-6%~XDHhNU= z$V;09m3Kk4SN=Mmlf9Z6&Y3^R27HQ#I`$&`B}(g852yt~HV=+%@rXs%4rEz%eV+f^ z!=q1+0p0?}-urxc@-YS$UQY~@5GelcDR<-|RrhPd8CTyR&G8lxv%DmTvZmz59jGD~ zd#Wo1c^C5mhPDW9(12HQ^=~WDJ_#Una~>>wh)-3$99ha=I|#+@z-W=9Gb-wl zdM4DyCEnHmRk3i@l1b~$HtC~z+eT}V3%qUhsaR_McfdGz<~p`qUPV$%pSU1LmKp|W zgou179+B!~@ryXg&JJb-(1lcpVw{EyE0Tph4}E7zK3(sE-JX4JVoR_z7Qt3WOaFbm$;s8@bT~b9?@zfG1GPNe#H7y=LuH*6PHW6WCvGAl@6u4 z%MbfO;2Ktu-_A4$Yui-?>A_#h)sZb%{&?9U?AlB?m$_52gnlProq#OYWVP|`C&n7>x?B5e{cQ=#AT zs5F2`ni`npxm}O|tNU&Zr;EK14B;XNa4XF*Rsr2;U|HtTUCuG#IOl@A_8uNjy=D}x zZDz{i*U!_0qw<$DK3Y?w+U){NQHy;@=9~{?y-V3mSU*>Jv>uH}&;7Q8DZKj>ppr_< zaT)Stnd;j39pb65X8(Kia8P?~L6Dtg!}gRjJ6!E|((0uM`*+#AGdN5DJOY7%Uktuh zXaY$14ZVmJOn-@{m4hSNO?E zM!`IYke1|R5f8vLkXA5HXK8&-M&8s^NBE7Uf#vC2v%im`l)DfS7~7QJ{9lZ$n{TD2ILL3kA~zcrM2*y{f9sUsZkJ)D?b!W8C{BuKk99IjhUNH1zlozT@KLIrj+}GKBuC& zPD?ltd*MqP8#_r?TQ@d47Z*F{tm83gtm2Hjk^kX%Kkv8}I}Y5k{5Pb;UC3H+li!uI z9&?4LJO#tUc&L0w!oOi544s8LaivUmCSd>sDUo}JO-#XL#!-~> z&_R&l!2k6Qki6L)@BpB;%cW2bcq#b!i&|fxDYF9?`s|9DS&EKF{Z?pCjEkKth69j= z@impqqSf^~DcU|feU6_2)=$wJg}xT>LE{es%0oX8F>QNcaUBk3hOIPKlkR5gjqprn z$4N$?h_Xvqj-8cVKeMC#lP+hIb)fC1C*~#A9S!|0?9NxO~?^!97>+c%0Uw*{{ zkQrsfPtqN*5g<0qJh19{{)XF=3#8}(&gOxlh0@;Z8_V5wbmMK5gs#x@vMig@n-xZr zOooG9Xq54OH zs=uE4&#okl<~gx>JS@JH9$#d(RScY6QJ4SViU-8@XCqbc2;Rg$FrR*yj-6HN8}M$d z)@947TLqP3Tev+9?;S<&JTroZ0*Hm#ZbiQ%l5|eQ-YT8f=2CXZ zlvJ8*I@=Q8`o3sbXloJFFa#h`fCz4KC6^<=(;O}~StoliuzS)tKq8*(`CeQa)1JOZ znW&l_w3fb@&qfE*CL@W37`C(Z2r?ks6(_e8hNQ;=rNNH4Cks)hAj3 z_|wi~KQWNhzujp*FxEmoa2cibO1w5OC|cIfe5QhVXU)8W=~$qW_>oeW*c8N)QbhbX z;pNVO@Rr|0ZVYt@slo(K&;UN!L!Jy=5**;ZJU>{R4hPHx|FDIW;>7_zs$P4lFrg`{ z9V19{I{7g+pyDG#e~TR*PIu$)Zon?jSlAPEsCq9WG$!peje|?ntYlyAR}qIk*JY<1 zWG8*4BCxE>#lQOfTs6ip^hA&qGmh5i&!vJ;YJTS9eO3=y3g;g=&8^69_OR{5x`F6* zYh|i)e)Jg?gtOq53!^-J0<7J{KxFXGfxW=xdpMiNoQycl^a8AC&r0*Xt>$`LTdM$@ zNAL&ZFs1`VrctubgPa(5a{?i+4r;?&SyJ*VBT0&IwIoHU2J*guxyLxXtn#Gexk|D< z`aE*0DYmgc2ehX8^KhJ=hvH`gS1>}2w^Jp_u#I54TUK1-MSJ-efUs7JVSF`Xg7-GT z1W#twA%6>zdd&UX<~cbCJeo?PN~tp z{wLBGyuc>sGRSUto*mE*PCoAnBKVj45h0-H@Neb`z5ZRi*AI{ZSy68tq{v*oWO%`N z)k50O<8Rs=H~_lA9=l!mIL!RTRyYVc=>H+AW`Fv*MW&Kqag*=YRO3b{N0KrXc=gSa zc3wb5-1ZqRiU^jlQZyWOHNucS0P3hx;F;Hd7N}gLCwnPQwRP4)zE9j{UE8_d@S|Sh zz;P2pj>oL$deDD%^|+DiqR}#KMDKkB)3BD0?+DW;j`YiQUg|Bs2f4gPcUIN>GNCb! z?*$&djYVG~sWc5d+Wr?&Zy8oc({zmncM0wUcbDMqPH+wG?iO4(lHd~DEx2yngS)%C zdm!W+?&m$%`2m}$-8MZvU8`1A7Y6-FSNz&L6WaDva|A;Is+*{Md1Yo^{XaalH+YL9 z%U0*0I+W1V4(+A)h!6O_m`qN%S3@}RUJ%ndTUK!F%Dlr3P8YXEFrxOZLc}C9N2f&?(u9($2(z%N zjmx5rInwSYr$zW2EC?Ei$(?e3CY*)Si}HncszmlQ`MI>)bb%X6y3}F63`&epW4$TpUF)ta~P3;^$DZa zmIdN*l=PLU>&k|vJ;ch~S+=Rco(-#5e?vmiFC&{qA-(t~B!{3n9MtYGYb|%I_?I{c zieg9`EbTy@|M1Ai)4XL{x{D+r?mPx`XRM~ygVMZh+qaL>VN$%z`E7sZip;a!u0AKA zDXbDYcL*LQIWWX26F3Y0YvF0=*B*>ILNQ6^$<(0mIVgDwR zJ4yGb$tN4@l?yF0B3F}*X(W^pbQ9BCp-zFGZ1n~%Z@@9Ub(9N@8nZ8-3)WB+g1A8J zQn55GT|p#~PRbm-U)jqjC5dVACRJNQ9M-=8sL}Z|tG%752A11~3p?)-Ir`sp3l||| zuL7)*bt3`iKvXa?5k*mYCK^;HI<252|CXLqdPo)Ijf*w)p}YkT=5Iq>CZv)e|1^Tp zHkZ$=DD9ZopNs{tKNGwCdiQPuLqHf%IV|2DABT?fno*dDfMCI9ZtU?woL4ES9BM+! z6-*fFnQ%`Ev~sFS%!I}Wm{pi)mPAIJ+g9a=uk5i;=po*9Fw#X#iZgSHrQn^1)7fyu zU)yxgy(mfsv}*m9KVxYeZBYLjN&`d7KdbcTDxD;fe06zB?-GU!1H0t(F7FVln~S}D zNau?viv!p3(n0OUxss-N5v?f7>w$PrRjZyKQ-a6~bf;8_*(8!1t-)HMwKnGsR*oJ@ zIxL?^GOa8+Kjn*H??C0Aze>UxP#5%ml-z(}vZKU=^f~>#w5>T@SR&2lnRcXkhxvJW z)sPQPF!_s{^H>&=sR}5V#4>hvPQC=yKs|Lf*foZWl;DKg=|_TtZm1#WQLd8dBzf3G9MtlF?%@=2CZv_k@tY}cxdGHh_#c5dks75=mu$)5B9;r=-M%SLi$j#XsWUYH0>l1|b_ zE6Ku~Jm<(JVnqIocd6uMgc52(Bbeij8NUa+p&n|3f6+cE;2mECz}d#-npp-fWTJ!% z%Pj0L!CbfP9ipPxSq485Ae1l6tzKO1b-%7#pTJ-g#}$`Jo8txuhZmDrt6zKNMJ|K7 z9M?W&bd555(7W}6$mB=kQCeR8iK&ySL)D2?w zGzi;}hT;>!z$UghVjsY%{gkV#$Ti5{@sYC%n;|F=au!^PJT1!S!Wr01{(*c5Mkk@d zIO)IlqJO7VOcY#_`sg{geo_0AzxWUZ0B??}O+pWZO?TkN4O2QVX8MKYCVVNrAIgkyzkUPG(Z6=YFUc3l zxMKw@X1=&01&?Om@KzAt_03(cVFxUhcO$T>=JaQWOlXkvnmE52xUxd2hX1;ON#Tp? zP)`uuee(T=6Mg267T}5D%ZU9SpKPBMEdo#%Zd8iI3#djtl<#PMveUU4O0-cQgvBA z++)a#2>HrEf22t{r!c4rbGcwg09DF4T;7o*I}v7~^o+^`G~`VbGZR`c5f>W~RwHH3 zg1th#@64JY8%K0yk4xAFWC99j&=-UXG=%msfB|t)oj9}Vqw9a*6md-7LAet#QOhTR zGexe-h7fUdReo<0$Ud-}i)fS$&AYhjWBWc12?frHT_ZSwa%uyo?$yX3iHCch?;)ze z2uY^c+|3{ERQg4LMha*LWE1d|sdGYexeQj1?-t#KX8DyW3Xn*EA*e; zo+XKFBsxzL(UyBUNs!keD9dlp&@cr2B~Gg(dGjbmqg-nL`~n>Bef9;wHV9q=IY28c zn2U7R)S{o_*NTX;R49FT7X##C9_R+*uIUG>ZRi8Wv?*u@AaR^RYA@21?I%QW$*~(- z@!lMe*f7-pk^s;`&vT0E)-l2w@w`kdQ1$!uO7|Py$vrT3z#0Q#!DyuMd_21M=uwPGM+^ z`|@he%F7apfGMo0zlU4lv}!LCOG95r~s_sh#Vnm62=vC6$31UO<8AYGyq?s#rcZHY{232;x`eV?-B zssY@cuOrGDarvRciuCNds_*gLfcK`6$`hC!J8X7x@r{_yT*dY-i9u2q!xee-svx=x zyinJ5@ih=wY;y;6wiXhd2y@&C>mz5?x2h4S+6I z+#qc2P|ueu_{|SR5`kf5+^e=<6_l-@ZjI$_selxvd;RUiv@(8G9$*K4^Y2Q0!Akuo zl&V;=W|+})3&NgJNm68)0C9V#8te&;k0o zpkr6e^TQ(un<_Q+bRUH3V21KA$Pjz3`2-(E?+8C7SmLZ?s4U{@^FH@Cdv#W0Y z<-M+vF9b*i$|$ki2}&kFBGph#a!@`*u7{e35a#0yUuo^%E#&GUHqWXSizV#NZ|KYL z=Tz*AyJ`H4uD{Q6r`U0ER^-)aNe}o25YCrdFB%W*^Zu9tjBBf_XtL!qGHw z;m#~my04aEi%8A|%F;v{KMocW4GRgaolrvStyB~97c5qYyp9VTQl=>YSIme@gbX|r zXcr_}LZ_v#_9NPDd|KGuG8`aAy2CA}v-VJz&J%<6ec=KxH>(GQHQK-V!Wd4BKn5Vs!-wo^|*xEO~(W7Gsa& z=i<)B6ESr2CVH=;pc zDp4n;9N$hMfeZQ{bMqYvN1dsm*`WU*NDwu4Y9kxO>qyA3m4S^hA}m$;!Pf1@PXAO> z%=hQJ*Wf|}I;BH`0@jX|QcmC}rt(pJ$b>rmeiNY4)pgNhjFF~ffA;g`{ih`R1IaO89t;k-!gs}QjBeqjyIJUu`91ZO~niHAJ^M1}`V zZRIwL=MWwZLrY0sdzR5k&gbt5t+T-yz|HMo#^R~;zoGGNt#Wr@uIxtLy<>-G+1gQk z{=0PkJi!bZ1#O~$RDu*)$ulZunM(`h#+VjIUHV(6H1 zMdn57FgvjSp5^Kx#>3vx-c(_g@W9G{Bf~}Y`E>CKC}Xe`unl-bQXct>6RKk{FeG== zz7O&O&mhhwpZ=LZK{cqs@^y4AOKo_uJr@$w6TMdvs#YUPZqJswMJn#W!zDUo0u-|3 z+?@3LQqJnJ%IKZXi}GWn9bId_DC?0!{>}R(tJv3I&i+t;z|-K_u{eGMp|O@bpwRiMX92 zCM9(=YEgM{CKuteVia(GdPY;Csb#;Vf%hEmD$G;#i~wLNwK(HjMgVOvsvkrjGgUiw z%%t%xPGTc=3zmONF#K0Yww!}Hk@shX6|q&?BNCRZGxEzQsR=?0Cm6^UWw#jqge z#r*i&a9Y(|eePaBRnYqi#E&z@wG--1VziDm?ne;0$*Bz|L;;>AtGJo{no<4S#bsme zW`F`;mvry2IC&>507a_v#vE6kKsvhlPO_l+wEy-lsl%O86_nd+pB;ws9rtZ|qPyvQ zJGDBvC_MOrz#46G0Y+cLQIKHt8u9%9Wgpg^s-aDB@0z$Sb4xIca0b@gAyV@|v#vR? zi6P7hvbd4P7at5%k}A#M+=ej_$bjkj&|%c_2{R|ZIHW2;=fEVbjk(35l>O6MN%KK& z1ZcJdpTjYgH<;dR8JHfC3A6vW4%R)@h4vfbw)Y7LVFy@p*@_siV1xiZsIh$n>o|!u$fVq|TG}0I=6nZYdzHg%?dmP>llyp9wN)$3z5jO)Rru&yUP;zzKkL!6nSj4z!vI@VlPQ_7zhjy%A?EC)g)l?T zm%mp!3TBJh>=kniL*hAN&t*=t009(+q{_;4FHPkUkXxv+oU{Ipiz_Byjt%n8W4IXB zCs$V^L+4LTFH)Gn!HVQF5kOZ2kqm9>h;o(JfU{163_nAVngb;=Y>HHk1B1PGOhXfw zkdh1(IpjAVz}tXlnvnSi;E+oUC(y`sJ2+!M1RO>!fx`6k;}YK#v5kgMy?_w-VD@EM zsuLv8zBP{aUhP%wQ=15INmEPH?E3D5<_Bygsq>P(SI>dX)zQqvC5G3rb0F`p?3-7uOc1Z%&gl%{j0LGZe*b9)l?AlX_a=x@)=H04eE)!nad-vS)AHwXG0^TJ_ zzBP!K1NatGtIUB|L@%^$O$gq&zFpw$^01etvh}Py=M(t~q&iDzC5%cWggQFk@jwM5 zP}%}+Q~={}YZ10~PdA0k9G^~$FS+xZUJxnZhnZZA;i?_n-Gk2M+)1NaU8&C;LW3_5 z^4+G~*{4j?_*q(*s$vMQHn2(H9lAwZO^qr#n)OvlDme4&G#U2+%f+HksfQ)EE6a)&j4qK|o{hB4KjRMi22oEot>Pw1B`Ns} z!fjBmn z(bUu|wL-O3%2kIidrOvpyj0)LdtSEt#bokT2J^3$hn1R`KlvtEae|F1MPLqB%$cW7 z!zmLiv4^5ySQ^B<#X?ImsgK$TV3@O2iRf-wB0VZMfBh6s2P2tu9B}x0Qq&BO<*@r^b zE)*IMrkxC&PP!X9v36iH@=ZLs$%;bm|0l*^rQMjgeftf=<;sV<2(TdX+URZ zsiqQ2^v7tXXEziTBgXRL?ov&tcaXuhppr10Ao_7?aU{P;3C7`;R`w1fD&UEyE~eW- zozm!04X?bRV&=te?~c;^LVh8M>x?|Mr}BJfCb5Qui8fNa)`Gbsxc}8I_42jMqETu0 zJ9BlDo%4~x3m*Q85_%Xb>tZv*K$CX}4^EdKwmNk}KhDq&_A8v{(kxD~wwR?ra_BrQ zPBl*f502vm;z7g&RD@uZg%v~9IdyCZ$&tG##_%4<^jZ!n>Wcy7(a%B)n03l8_S|s; z3sO}?*|M{4v36!XvbJmgu0{;k>z#DI`_fo3;s%B^p#KgKA>(HCkWv{)Lz9Mb~q?W z&3uFDO_=y(`CIKv^b@|fu+y1K9f;?j2{8nhn;Hc^ss?h0)^e)-z${k;c*MUD3+YPUZe zo?_?R?DOKg*)nL7PqV747R>Wln&(*j%#ycCQw$*p!nExx6GQQuDSxXEt%CY98E7|;qqs=ci!vPL$YXfGZk;wARs-AB-WC?Tf31J7o6JgB$OR52<(rG}snqUhI! z*K@*ZuJ=y${P~pmn*difGS&jCZP5Gky_3Jwm!TEz9^(HwnM3gqKs0Gj?RGf4GByg7 zWWcKt>SEnQ-v6#n`jucAjhLx2p6obNut6Kw6VyzVd!F{(&f)$8i_TO zU}LBfZ;3@C6^$ih<2yR>F`7b|W$33&;n@_d%0C7La@~z%En(kvlR0NZO%CFSQqx8c z>!SJ}u_woL(~2Q?7@~)k*Ph-3Y^MbIRDpIL9W7bpAI|@3QMs}50~BfxA_QMMc~X`g zgY$FVPZ36ijO8vIzK_4hncxMp`Va9;tnB`BRm$#$Ibh}hE=Eyg#cwJg-EDE|SVcdj z{*2igOUXO%4_LBSOF&BZ!#d8efFV#ON*gS!QNw)FNI4Z=J2Mme8AB zla;(__k9u$`jwCwbMt-_b2gFH{b<=0JFZX6!o#~IB`c(hwTHKa5o5#6jn+hnp>iXk zwMr@G6GE)VA?lK~SY z32fw%WOOi(}4FOid zLBn$5ej}-p0<;z^Kd_!0Am6GvHS*UF!ue=W?H!uvJ%Ltv*zUlZK>pElcfDkO{G*SV zOIU*{5}g6(SNG7NzpKjE=n%{nqa1nO4~@0$^vkGZnHUYp2YBr0KH|fME5K?5yb%Y6?sf~s|_FtAe}13NLcaj*3L4N&R~+#%p?n4 zx-@w)3Q*9()aOw+raYQN1tMu=<0|>P z8dDZ|9w>E&XmyZ*D@0PN0~rk zT#H9^O-2|P!Kr;9>ulvFK6VpZ4lSGv`lG03Tb8P^s*{b1TBaAYDe;|3&n_obJhE24 zh#I>j|As82t``}84 zwNgXpg&T@Lwsa7=Wyu*=)6$PM(yM&r$MIS1I&Y{kW7pMYQ%@rl`zT4ucK|NxjiLn) z0=U$H5(n)0wfVT#Qv_+Wd}H@$8PE`w{g|lg1!0ZV-RDfV{;~yO?vV{cGW2UQIyw|q z(ZRCkgc54oQb-Kx$WwUqj8XkmT^JsND~HsvIGVtOkHN||N+1jZFgb?9x}hc^h@?J9 z25-HkBk@__2q;1yb)g!n%}#~~B4^zcH7*EC22ijGJXCg`+hwFOX#}UPb`jtrEEKv(!jY?<2ZlB(P5?y@SvZdoHhm`Rjzm950U;Y2G5qN@_;^ZWx?f;XtQ z!y#rf0l3t_3!CkQJnBbUCqbiu5)8*g>cehY6s+n3T6;H9F&8`H^bf>3lqAgtRtj89 z!v9CrD8)d{TN$6zg_@pvSuZC)`X+fYOU zm3-d?%tUw^0+wIURJ6(8kOWyFcm(^Qm+)ycV_brF`g;%6-Gfym0gnt%E%8;U#A6%~ zV**DT6|TTgg)N6olfc26Vyb>YS;~CZ<{&3NR{@ahad6 z6ai|BrY`CgatI^D&H`QDSQ{@A4C_+GnI@pd3QPif7{n2H<&b114YKcRq;dS6eD zfT>bECOFI4#8)n6qyh3MTV>{dkBu<$;Nb@mT*ohv#6><%3hY~C!e}E@=Xlg5aAh&L zB_^9(;-ms)Km#clhiRL~>CxR>p}=?aKuP`a{h7N4SLE3F3f4@Xig<%08fzMibb({Z zG<@CGVn70kydCb()T#*(J>w{m#__;9QbMLy3_^-f75dr?#BT(V^hFsraAfj#m)OY> z%$G_?80_-YT7-$w1wpuB1S&zri}-3nu5R;Q=LHe(WwyY8a13f^e+6DaCv}7M{5Wxd z2olI^ks+h&SZ@4ODfBv&XuzCejP|S!E+!Ab8`?oh6wA|YfA~QF(Dmn8w9(BFVlAvP zyJTFbe9Dq1TyX=0_R!^&kRrGcM|oD45;_8I{EMY6Uw2g-901(bfoXmnjvbt*)YedP z1mH-}gHqA}VHtkhK%c&2t=E5TQbt$X3aNqg4`faoqPb0sqTn9^_?LT4RN`Pf;bl}s zm;j3N22iE7J_gsKad&g6-G82i@PWR6PWcOOitFJ%G$H)qy@cgIdXWFb_$szKm3ro5 zk?^$l4nN|3pBxcIOSV>w?)9V#@@_r@3Ei)PK7w-r^nvh#68m5LF)AkbA&EZKov7&i28KEQ;XBJfhe)YoakZRCX#4)|S6|D7 zpvn#?YTNCK9K0T9TO@ZJMsU9Vxv_|WLUtYEX z!hClEV|vO3Qvt(A9KjQn>{C5JxG#>S)FwyZaL!Dd5&IIOhMn8In@@YmNBQ6QwRCgW zAcUN0S7d^z63B3dqUdIMoN4*D#HY~e&dy^nF-@V%v*AS^iE=p;jDXOJ-TtJyUKk$XntFW8q-9W zW0Wy|lnzU0<#GQMc)`o=;-XCtZ1R3T5PG;q#Be%QoM|GG6$Ik}J$4o;&EzU{%nv81 zF3K|uQf*qK+ogqzRKA)LMN{piWRO}mwDjN~y!Tm!*vb8AW& z?eKSGh?p)F>>_96r+giy7ol*a286a47sRDR=*pV=%b@QX=Hz)2V9$cDnL6mW7wrP z`NX4{O6KX&yZ!pX@nJUsbYE2h)ga~nlQbS!zFq^cKL8m&qblfoTNm}M-oHhuaJRV+g*JEctC-Joe8uaiI>DIyCeb_=}Y?lwTl z2r2MwRnfLj1@E|c`LP?JfILxdeh0SXn%(5VO-G?Y=Fzn|kV zC7b(264(f-!3U)f)q)6{fOnlpIv>lhdt-b}cY))-X&}XrjU*&vna~z({{L)81jbzAc&ml_IZ*DLIR14@7qMH4 zT)6@fS&E!aqX-rdS=kBrve-DIECl5VM*~9*p>y@Zia2wD_g7N3hxJhKLkIEYnh07N z%6kbTY3RPVGkr8Wi9ATHcY!#Pdhh4k<9?o_{}c2jOQ3q;;9Zd^Wdgm@Bbu{jqX%vX zow!V)$O3eMvx!CxWb%S>p6?-;$thW(ddSH{5?*q^c?o-C^>iC(lv+r>NGFKss_-1u zNzRDQgj{J8 zK=`BiQb`Fdo93ODL*L51wXGmPTHYWZFkEvH-|i^rgja=gBY1+Gvq-!Ux-Q$F=^7$i+n=n zUKS8c zpkP8y1R{^$BF144>dnh>=McJ>l^Gkt6OB$UN2=2pBfRauI@`_Ag-;Og_PNr zV`!D@dx`J>@AV5<9QO-cVm-3d0u2yG+pu(Tf7D=E}# zIj9v>=V&;KVeCX7R5o!0ZaseSbh?jTMxYTMhD+sxN>dy>rcd2I!g0)3-M~>%loQ_m zm@6c>aqUzuX-G5$=dhUMM;fS8V`OSOPQ@8x~dveR<* z@KLy2{vLTrY@Ne2ans^U1<#EInU=P^b_22?_p!06=p9!B@Xy`G$pzZ>IOQ=bytFVV zbys3-EeXM2?PBSZ_q43^30tlmqzE5XtPnGmE2>n&Wv!1&?Bi<{pZmiV8oiA42y()i;@TnQIKHZgs%x! zcjpGYd}4py2w@Ohaf|f8yR2U{CZ>>Nw7p<&wSp3M!PJcYzJ^o|w{jD-yqcsO6Z~Uf z5V4xoHGW4^fIm7aY%Z3Z%%*Gz1ce1$^^6THm8PZ18}#^?pwXs;E5|@)NLXly)^UuT{3zVbBamw{ArJw>?5^6Q${EmGWnX}I@i>zw1D!+k4Fg`l5hPfh`Y!Umsg z1pWVq}dC5|QY zl|JoVccNTbf`+V+w-%c}7YFXHpvEePsw;$z>I%P$312;Ir0%x=(f4)S@P=P`tA`_% zxSQNajeOf8R-!K0`NsC7$LougutXEz{Md|pY1s8=nlRZi>=84v?N1LsUzXa6whw|$*A<6|AzOwPxkdnQGSZpyel$e3T~M%fD`A&9Wg*&`_On(#mLc1*S7 zid>L9X`0QE+Ju=-Z0^dO)M_w``Xd~v(tlIQ+9mFhLwkLtS`x0?{p2VVi;bbcn7C!F zGfCem*pj+akaqt)mjFVleN2K{H>}`uVmZOH<8}^> z_4uP1OUCzSxEJ_0zm2oTcAfoi!GCdcxTavwhU;{6Vt*b?u$%OVelqSRNU7{G|9M;ZcL4nnzyw2OzhIAR-2hbz_to0q z;A*8koH#S}CUz$&;9TJEO*s*cU)x04Pu$M!nkfSm`B3%@39{w$8cNV{T;&owF9)&2 z-&@TeMAuN|SDK~R32mDPyoDHMnNoJ+vC=aLxMw5i@vNXkBc8Q9GIAkRg8c5f*r9Kp zyJC#@U83P+1q(1LnegV>UQ>ymP!^Kq?sj}|j3%o~yezHYf*C7$PL^O+M3H9D5JmF| z$v#wZ=l=XXgBk|OF{so46`GItb3T+E0W#Mpi#AT^OoCq|vTmtTB2Jb*2l}~@V-3lX&JIe`dn-I#Pj*oG zA<$ap%BDMLYzwQv3nTR1P-bo{K0`}QYeOhhbyr*~fpl)@p+Y+c$7bwdT3+aRV;O-P z!^eIpS~){3n@A^(p-u{!=a`URvz>}Yfqsg8L3O}K^y_m+2`;QhMAF8sg=foV4j$Av2aPl(@KEFvnJvdGcML-$` z9qUJn$5C&vwp_j4PyTyb6#fSXyF6=q0ta(R*#dFtNY65Ev0oCbJVi!Zs)bumDyk)| ze<`rRg7%q$g(dvhkMq=!|LSed|Aq#ixBvUIWWwm4I%~B{z=`FXGh5k70x1Cz*L6>8 zr1-7uV^BoHW5?o@146<>sGPSxZ@Vk&Cki~UF&F8fu#XXU^Z^+fi8)72p+SXV(;DMH z!@T_Xjm$wB4X%)?+2K&kT7@I$_|$tUL8>X_-fw@-p0`=5H@Nm>=5dd zqrmPW(#eO+;8DGOup;}-U;XWpmsFNTo9YR;*IMFY9aVX{A|a|4<>T&uXSi>bd~oMC zn_R*hxKIwCe+E`R6-LuJ7tkKe6lP2ae6(5LWiv1H7f&^=SusBm51E=vlg=m!L&NmW zESY_sQD-UkSogoH$ldYOAQddqxWBeTzP*vdZP#K|%1wJ64thM@ri4M-xcLhYBXlEM zc*HBgez6rG?Jr&T@3eGe<|A5U-{p7zh|aGbrmcm+1JAj?UGensUW!^#Z8#V@#~OTb zcPfK|Q>w#gUt%GthFA&r1WEMe1kU5iLO&j6L}JK^;F_C-3B}^+VI>TpnEEUc%y>oYRn*tCkbyH}}`x-}#x6#`eK9z0#wt^3nN#9E|N31jvAK=I+i zAXcnEdQjx;vT#Iyi44<9mOb)9{I?K$l!L>ulsEtDnkRFzoS_(p@6TJ?W!Fp5GP)n_ z;OiaMU_Nu*b5g}`=%fo*u5q+>&hdQmvAS<5L(f73&R}79gmq;+diLHK%5gf!lXW`n z_0beCYou-Nh;PlYl=)w674VC&D319w4p^BcCT zQ^K+3qI@?ZuqA#Dh%T$RxB{N1(qBEgz4&m@hK_lK_5BPo`@jpA8WU{dGxJPg^XIN? zS2?k_vM};{6)t|7q|u2|O850Feh^_nQZ=xVT7d~{PqrmB@sJ2gPmXA$J>j<5!DT+Q zr-IiZf$MJe=N`$QRyWG&2PtfsJmf*3(Co6Q4QcVqgos@0JW`G^u4aP6#%4GW+-kH> zUZ(yoMUJEb_?2ddU}1NWrNdL&u|uA)KEi)V8u^7*&hMb$sv>o=#9VNDLZ9t?;Wpy= zL>i=u6eLW6aVnK%GhMoQ>76X*8hliYz^AL*UR`Tj=FPU zd&lZ(1<|bg@{Mm|6`9UEs=j zdwa~+FQoX%bm4V+-=I&~o>Q1vplqq=I~24pb5;-+czwdCDo-;voxQjzkmZ=&!+QHz zS9-u3)8@Sxc5X%}m5-B`V$ON-OwISl#0J#P7=*?ukf$>A?x?foNT<5esR*y8ZrE@& zBr@Yf;G!3u&mL~+PLclN3mRXBqPV^+0%Llz|3)35op;tG)WfH0q_X1ejW0)AG-D@7 zb=m`Qo&}GvP`0x|%+JCi{wjrNX0`qD!w*nIL2e!|m8MxtilOB23 zp?-#0UZu09vE3c`eTAZ`hrd+tr~U`g-M3{kq&F=A$+4 zspC0ay?nlB@pZXfoFx9Y@`)t1e?EQ2_sSoe9W0)`Us~BnUHx3N#BEFFdL>ReIE0by z7&$@(HD*_q1#t;a^K2Wx$YXsHzP{Y3-`&mm&R_N%#)9gT^KBS}AS~$v@e-~|(e!n& zWc;#K%MGpTUsZ}_H|jDIkiF@KxYdH2g(Tfj?mz-_i- z$^2Z1bEe^yDd$pZ;VaL=V>^XmhS%10C11`VYv$`qMPl?d|7;sDeU z;kOOi-c3TuC;ps~fUkBwBtG66bGs27xx}0j=+_qwSD^48w|8O!Tb33&?r){My5HuS z{+5n^`3R!EI}->yf4rO4dqY^f;4lPv&$&N>y-!@J+dCj=qaPvEi{C^S9C_dRl`Gop zI-BW8rPFAq22V~kQpQWh$zH5~;w+i*DuS!$c92-j;5LTqwI<^>ybaU;{Z78SysF2? z@c;8>3M&N3;~oI_dD_?i_14z$?~(WTzm9|690wz z3#3&@2M6nY-0=)}fVG}zy%&P#y7mA2_vP{pm$hha#7Ahm``G^3#a3 z;7t|)KpNgS5%y9{M+2j zZda#Q??3(3RRAd(BXOvl{dzW>rRzHp>cPsyQhZow+wtr5^>TWimd{@KO&FH)WXBT% zaoNH0CpWY)%WrJ*b-3Y=OAx8S+ljE(XaG4Yn5$)T`3(DLc!OoPo=sT;XIPF}g1I@L z9Km#jdl<6_*4o|hV7}efmpx;VT)y@XbYjaGQrEZ?CU?G~*zTa05JR>W>-i9_Z=F`C zQNg%-@dZ^}SAL=tB-%#Ym+YOsRlns(H?oztQ6^imw-v+cQ}XFABw|$>*<4XE(Nw*T zw|akzxbpv+L8GuNfPAX|1y9`<7Nq=C?G)hd=Z@;$b=wOO|6O*MlcM%SHmF;<5P?6r z+nT9pyJ0mBbL=?~Kv=}e1oqcOIUe+q{n|^ZZTc`{<6?bZKFm_@qTc-LwUK^E@r!0+ zuO}nl&M|BBW5$Clp5g4<{#R*Isr?U~%yU=AFqNL>9%%ver{rBWRx6v2Rel9oA81!1 z3rPjg=@g`eJA9t^dP0meZvI)u+dKs*{@KMhowb2ptt(>w&Md5cvN*%m}=t ztI+cf-1phda}++as5d7iV)M*;bw-xQZYj(e+i{KmAm5HR92Y}6_9sKJqa74<7mG7n zC=t3cT4?pNFRj(+>c3dmCkZp=^GcW0jO$h6Dl=RkXYUA-k>YIB{M4`TCb;|QnAl(( zcuLQ23kPv9^P=nbukLt&M$GU?Q)&;SY3D5Q=O>bRn#1&6xuCxP;R%UY(Tw=@&E#&S zm!6ya_DPF;PSdE0TS=(XIP0~YgW>IB+;zx5+LC&2z66h!n~Pqb>f}e?tSRf0IJ{NKksI?KUZWZZ7pJ_#B=ec*{>Q%=F_5$PlQDRqqV<7L%yZ$mk|RxtSBt_Ea^}Wb=a#6#a<_6M`fD*o zG6wwyrgWOpy@#7&g}1ug6^?<#>~S6Q`0thDy1B^TY)6VeHVEx;f**b}w5f2$y8Z2{ zR$di3P19CF!ITZIxpl0Ek__IF&tBG3hK6;l=*%B0K{#$Qi|^ptOenNGPcijYUgRXS zJ@)_k)R08DazV{ptM|;jb6;X-E$;n%)+4vb9;)Cn{gp0G`FD1zxbjnP$#7D}#m1GJ$NZ*;|XWAvxo_udPBg zr_jdqk%`o`DTg=}pV^S&D}9&kqMT-?^<Gcupl1)HviuNFzKTqRrn-MY1DlRvbq(Gt*qi{6acXxs>x zvWUVo$l#?9N!l8P`;}73K>5`FweoMV-OtTA_Kp>c%iA}TniLu5#mJ4Wpit7AqwA>)9D5Kc_72M+iLiX95>%PS{``GVB5G}DG!poXWB zikrUUd<;wF>y{kOd^>kd^ZBD5Dh!{~oZmiHhUc6bB1 z%U)W{1ghf>cRPwcgRyAo^L!e&lLrZya>QKgCMAgx3^z+Z*jN1Z99Q;_9;`2xB7_g~ z*bLEu(WCHQnt)nvpSOJrvTDmSocG6KA3vD=qriEy_01On6Zs}BqCo;K`j7#!_n$5k zg8kQa%1U!?J7ifX)}HE<6`?OOT0E}za>+2zavj|XS7B`(F{3pf;*K!9+QM#EU~vN) z#&TS3hccxN*|=2z6!QB)aK3S@r7lTyh+%KrlCd06nglUK4uObQ)tYLlMbT z^;-KWx$lC8Oi;kZ#}BAK?};nFMS0FH9y{jcgR>TjKJ?>hkP2~*^zSd5-eL2UlIN=PR%oSYjUFKfQT5qUPxS#2;U_u^amy`KtJlbVe(lm`) zYIfSRjC$2!g z&T`=!t6#4Gntib-9{oardRw_UOI|Q)3?z?TFwSP`qEtxyuS-u|5}-W{(M&Ah!|<4h zcdXt(cEC9Mc;<|C@=4-ogcBI~eAu}?n^C=OP zVQRKNyE!-4XDtdfO*(j_#yR2iuH7^6k5A*|cd1!c{$tE2I`l5w+-c zl}JjZLTTwORsz5LWz(qt8_q?805D3bU>-W)qhp5G1*nz)g3_8TS`MnXj+#og=J~&Y zHz8Ct;S>}w{B)kOLDd}>t>Crdg8OxlJ5SV*5u#a-q8qD9)vz-9c{1RE&x%WRiDsX)TypMcLs5I5z<~!Pa(|&4&#oUoNql$bOv1=JvBZ20pVVSy_Y21{Tib57 z`Kv`|S3Y}?1vdWq-Gzw#RChyLAm+LBm3@UEs3WCLjxypCj@L)Ok7vhMU&-6lytF;V z@aA=|f;?PNQtLh8>L8Ttrz(?_zcE^>g@1~%i2U?Vn!PduAED6_E>Ly}SH4BWi#~6| zBjE)Z*oCd(Lnf|1r5G&by1*?#=gKDBe1)>ri`>l48nqPPy_M0*W~G_?@G6+Pvq{ag z8b#9X6YMGNHTL6t1MCKiC9Rw{%=an^8rdcZB%fCZ8MrPfXf9aeu+cu!rYEx(^(5?! z$H1!(dd_M8e3Pbj^i5SE#*+UrB@lLd{)}S#8Sw~F^g=YPPUGb0XDOMI)5g&Yb(s|q z2f2PueKR!%QCk71%e7CB{U%6J&h29a{i}IVnDDKy6%Y0H&Rl1j|EzR8->v?+-~Hn> zsN7cKt6DSa(0TuPNU2WuuKYA_*90~Eu8>bzj(U{uWcITb*+D$d_gRcqv4-471zC)1 zwcx5mjk|{22Mw_cui;uY6E5c~>2E>^2w#k(zSANt;1X>+FQ4kMaHcn1I6oKaoW4Q! z1CMoT4jbRQS9&@S$8JTugd<#R zCZn8r`^&ipE!DFJof}HYt)GVV^3wmZktUmV^*&;O@UQ^^I+s~%EB-E^^4|nAEP=)P&hai5RTz^ulXb8B|@+ z>LgY+yNY}Jd<^ECsv;N)IQI&oSvCf^_XfTFJRd_p*9Z7v(4=g}ZFH_rGJc%*g{$Z0 zbtfe>;S^yHK^>sq9NY~PsYCrkpqf}{!!1VMVc2?B;kEU2i!f)47I%|IhyU>Uh7H%z z_cAt9?6$GXwoCYCHFl@r=JY2)?9kn~Eh#5_E~g>qGM_HBzQ}LZi2{OyP5$Ppv~32e z$AyzE$V7#m^E72t4jWlJoWLJ=bc9z#5OEpY1+3j!J9Nq1-ZQQDID$Fd9d6Y3rF|@{ za&ET57`t@`k>w$=?SGCRPKXBIIzoTiciRWA{1@{WpBP<0vk;fqI#(){b%YsOF=zS1 zpN^Ahk0a7k<3y$Fz^HMWXD!7i=5u%CLph-qE?Iyr=qphio6m`u2^O1>q3-_9)i5z= z#V3)E6$8bp^t9xI$u0XWu!L=s?~t?X-nAyAjwvK!kQ9E}>_nGZNG}UPXG%plQ%Hzi z6uxx8irR}%M<#3yhY+Q z_~2XrsJbE~dG=;@#z6gZ1S^ik2CnUOv1G(*8+`Ep;71FjVHv3_+dlPcb_7tUm=|-}x!W)ivPx72LCO8=h z;A+Hz-)dGWs$#_xP1woXtPea}@uozYa?;frA%U{8U;XZrg`XtcZSMWCQ6q zRSVpw7#VC!#6!{QHgz$XSVlQ4jMtJKrx*##|8qtb@#Q22NlmTqNK{C$6O8jq%Pc*N zfmpU@OB5NMD`Z@{$THaqa_>8eA8MJh-2i5#9IIa*q<9B;Z+;i<{I6-=*QR7tFXbhILXk3CM za5oB8im(~rZ$I84q3?E4#J#$ZaQ#6+Mn%uvFz){QEjubJsk+{6@VaaIb^S@F)KD+b07GDmpTS!6%GdH#hNAv4o1@FG1RAVwr2?Pg^SR@eRf@1P51tM5ONd_4 zC?v{K&Q|j?@12S&ylL1+l(j=yy(!fLZEK#6;{N>Fa8Xlwb-e34cG2!$&70JH`Fxk=sJR z^oH4~0+hLdHIkYqpNR?XVz_d~$K-5#7e%Cv{AT^jJdSU)Lb{A|dVp6EGH9Ki{(2sj*Pu+1}qxV;Z_aBCs>u+RMZ)DZ; zT?;V_B0&WO9UsSH9w(U3i)LYu{(J((Bk=8*OF++9|xDu^zRf2}`tA-V=JAI9W zGh0N*zRb+ChF{<91(h~`uc}r*Ic{O z$2-U#vkg;nUoR(?-HtA$5Bo`8u8DyUjp!p5;o~aX(&!n6!taL*FI&=M#qbfJ9LSb= zW*A75A4Zv)fLr@Tm4f?#wU=#a%l9geYHz@VqeTk~&&F}f@lE~Uo{NWJwe8HZTiatI z#~YwgkJ+p2)G(mx%f=MA{#g8B>$qhlMRUn;wuKw$@7>GJ6u9`O?1ABI?H8E>$>jxX z-zjyijka=zK9})>Uiz*E{x^?}P&r$3Be%KqeYZ~P?jdPM+wev!hAQ3co>-sbT8FNg zJ&D_oQm&vmeiV5?AuE<0=#7(7%Ch27trCj_H0YNwB;WC?=UIVaH$Qevq`l`SI_%x) zeA_OOFCbp+sA?tDKl*`xmk;+dYrN47(QAE1Zut9J);-l5A}jfZAoBzq(xLoW{!5}T zqs?(!2VaY|aa(t{$c=GZ7OX(2YF(S3`Lhhx?t45KZT;JcLYJA_M%kh(w4Yq}cvRk! zc*jhAw%?DDs&-m5oL+-Fni+u`U*4>a8HiZBu= z`SXEKushE21JQSV{&Yc};m1ki=3K1OqRrfjgs;L9*Hm#x6a%Fcn!W6X@@alE>FuiG zykMsQ2C$<6uwy{RpzYdb5ljyLFYFkQ0qhh5|G`edDRNyE=O65V^e=Xbf&XBaxb_cr z3Qm8qONRRgJB2v_yX4@1uv5kP2Rp@X06WEQ06SWf7wl-e0PK?C{$iI5_kx`&&I@+` z-C-vNWPJ$*t1tLk#kWA6GzMe}B79~PMKc}M0~AF=iE?I!r%d}+W`^YyvrQC5R;)l~ z21A>lc*#uG?(oG`dDF!n&4H8uH759CmA3{G<$uS-%-|Nej-r_TcRm2=%b0+Bf5!wg z{&!43nU^uO1gifX6JVw7;9KD2KVw2se0Z4;1`_)%7KRf0=So$1uE3Z8n`~aqf$A?~ z0+N?8p#anx-hVpm24B3$#PGhRAdeg9<9B9;7z`NC&*~?$jXPEy2v0wDjUPoTR#ttE zcYT-n^XoQ#fY6}Rr`5WC?MTl7?|Z7|`C~`EFgWUabHGA+(Um0%m1Ka{z)(9d>{?f)^ke%0%500b0hF%r0mM@xPSxmr$R~ctTlli2rB5uMpKK}H|$NEn_)C4*T1p4vqxCuvR z0-xgH{NkmlX7OFramZ}mDD61;fLf;$2}1bm9~2|>K1CNj^XGec5Ke5A83mHLZow&& zY#I}msW_erTslh;*+vKki8X`_o<^FkLJp-pgU5%Ov|TL=%aqO~c~JMQ-NZG`xGCv# zP+w2@m3aOUOMR$Y1??3QwHOIk%wMn(xz62pD zEj-7(g?+rNC*kQhrOXW9?PJKEmcj7lMCqBE7F(t=TN6{c9uwey8&_!zyhUf0GsKJ> zF+x}wSHOu8k!`!Pn521nmSkVRsoap%MbL>6HF&MFICa5gOw9xn7>@3bQ<#%E%1s-vE4b&`k)mIY)?tyF{W`+Zl1h(7jhK_(T zDhpqdFbswmOWlU{JB!_`y)g_9K4-8g#qc|o`@W5eW^U;Mj)efX!5-jUphqR@@!L1Ac0m{?lP# zHEB{;UC?9=1;(YL>MaCcTu+}FyoyU6oN&3Ib@^+y%3?nq^Vn5;%a%<=#-(PooSrcbRuaTci^>q)A-eZ-{SsNmmR~E^r$z9Zow%siW?Xcrav9|Z~|JZ;sQCw-o((>8C6YOxwy(W+r$n1ySKt3je%DN@b9`nDAN4m z$Ik0oCC^YL&!{Bx^@7kY?_w`DemZk*J)hv{`*E}XPJ7%x$@b=D_X?s?+I3qm-n4Po zQzn2Wje8j5!7}pn(j6<=HXemVDr6GkD)h93Mw7r-Os9oL>$bGS^+QVZr`v&!_JwVM zM&ktW*P0g=5_B4~6j%_G($zc!y5UgigJ*S*bd{;vPILKdKf~VD#ZMGgS3lmRoh2!TMD|ItWR~u-WvIQcZ@JSd5FIkHF#=A|yX6_bkFpHEN&&u^vrp-KhX^+ROCab^h(| z7SoWF2Ssh;(ui820e`6JJmm3EVcQ3Tr5HY8k)YJ((Z~$eq2+ohXb&qdVo~!0nm(;1 z&`s-gPi0*d9x4U25>BC+y>a#>2EYCct`f+I86OITJcE|aIppLd9U^Z532|!!L;WU* z!AWbL#XtKX9Mf%UZ><_0Q|r;3k;EkY)*^nO!y(r#+X)I;QW};Tm3)wj68#XI zFi>MMqw~qsC+2N*HHk0c;gVc=Zlg>;uUmoRAjohRH%b3I;^EAU$;b5WY)vQ0E$PBp z0vkSz-jwe=LWgMlI->kEVtc(|1f#Ft$26J|IR+ogMI3KP0rZPr$k@;p+Qh^(840Q4 z#+DfF@C)3ZMN7k<^!;!Krb$Wa#)dBL(jjl{qPh?7dRf*>4K379ghDlMd(Z7(0Ncei zGn%$cd`7Oo4e1^LlL5~-9SVwa(m~6FelT>gn}OuO>FWhGZQ@dg!WLE_Rh@o1k%v?< z9L_->dPnKYr|)mfcwiIFWKiMM0wDUBqOrxO$)M&Qej149jWuDoiSX!biG3T599ff} zkraGKJcC`+*KK>(%MbP081#nU&l5H zwA>oks{WDCuJe(nS?u|m$M?ekjoe(7`%1m}ME z!C+LSt%*I?8#Dm-@#7e#FYNL&*t~AF*81K{Q~8Fz@Gi{dagkzVG!h^ip?Q3RL`6ZT zD9XDnWd!-DT_HEse$O`m;V8y2m}8_N0*;OKaED!4)ilZ+8F~w!nM+W@bXuWRR8S;1$M|Rt; zwS`)1PopRjqor#pB0gLC1%NNA?#|i?kxm6DRDC8xrkd42A0o`8ykLUg$Mp=!1FWO6 zH^4gPsil>8b2jK3)1Bx@^qst04inGSJKm69>kl+Nr$=0=4nq%q`}2kxbPGX|iOhLL znV7TwKWQKbdIMI;pD;dk{O)&AfRDP6NZ93QCb4;PscK>{UuZ*ho)MS$IV+j`BpZ-u zmX3Z-hRH_DwzJtC{^55&X)f5H)6urD5YxXf8;H;QB1 zF7S7h{oy>Xi+djMfkOH2d5g34m=-@SJQ!LYN!Lm_T0Kw9vB&i7i?ZZ^#%+^>^q>0l zE{sZtn1OX4Z9~q6@rj$&Pk)x#M?%-6IN3^kNLEQBc6MwI=#xg2&bM_v=3R zvE}=TG*XTn;9*v0@6oe9BZ6H>)z!z855wjpAG8>WrbjS_j}^Mg}Ir zp(QSKg%(p{N-}pm+}43-)Yh@OkJNyHe)bj%-ILjPp4|91lU zHFnP<+^VH0N{9EIkKPRK@i5+$gNR>xwQmt2j~UJ(v**QAjt(O?eYkyQUSVgz1*}6Q0fQwE8%bOi}dMvT0fR|J$K6?mjrQB-zaJyFv@Os7ktN?k)!+? zohOlrDfMUb2|ML?R+LgC0}bAhuO70QyqX@ISHfL%jYNOwU0bHf7v%o2J#$kfchRok z>A-M2*dblyo@flju1}e^wVRi2l4gxata8u@d@JN@5;>S*^8mN@o7*RQ+*BXUJk?T- zY@^!}yBjvO_hv}^OJh`PqRhc3_(tVM6S6+e$VcI5)GRgET)NLJ$oUDGrnN_&NgY^3 zqd^j6Tsu7*sc0MPZ8(`W>;*Uru}}N&^NRsSBL_19U^HALVe=UaG=y5l3+U_F5EB0- zQ-4Gc)jE9}=T}V0cDOPv*|wI4kiSmeIeS_V|6-w6b_($TAs(Z$qMQ@1gNDugH$1^h z^!b!2)%Z-;aYC_CEJd;i3W-H(d-R+Ub%7WsNz>+iU<0d!Zqeg8ork%o_kC;i6EBSA zxF$xUqE%yXt$e}+_ZwWX7~R;pr-jI}C0h)5^SZ*+9awD+#ZOs%QpzW@1tnS50F{E{ zhlb0m97T27)5Rg@>{M-$sczMXSZj)7p+;gs!863nwf8A8^8n1M-aOmbD~&I)`QKcn zsalUPtn#{%n2_i*5bGrK-7S^=$$VNxI?FN(s`|rwpM}Sqk1{u>4=0*{fKyj)s=(n~ z_=)eA^Mh?tX}i@Qe*#vgv%hpCt0BxmbqCk?*|)Q(9d%7w?Z++#621@XE13j+M|BFW zBcGNHS9fz?cdROU+(Gu--abkGsakZ)zM5=)Y&{TNJQM@Xb|4@>MD8pKD{CFH%-BDA z-B{l|@!CUh6;8aAL<;zhD5`y#G>>Z6&gquxTm_W~Mou#(zrg=&p0)i*($TyybPnM`(6%h;u;rU!`3%dVU^v{jx>x@bV4Z3Ep(r z3Ct(8P8kXM#VBI-{S@oTd}>T@9I1%?erf&j!u?-)7m2=6#IOI8yo<~VkavL^ncuNJ zs(FhJP9c7+^%KALuIBw@66pGH@71}xbg(AOYHaQ$_y~KpKB1-PlcxXf zOtzMM=D@gFcCR{O`TTWf;DF!Yu73GoNnLJOx6@I7zbbgH)h?~AQ-PUnMh6c+g zTX(%8NY^#6%UbCf=yUCAzlDgBoX65S)|V=O-m{S@*UIk8f01|rV?E{ke@VQeV*gCS zi2TnIuPUYQP6$(syDnQQQ0Stpwiv{>c3E0pXFpww)Ft~qZXivSuf&ep^28}3uM|XC z2^!86ztR;lOeMk=Kxjjr{8%_5IZo#Fo*=R$q3X}gl{K*yDelj5lT!Nylg|XVdj42F zgm1ojQ^l+4UyYQ=2r_5JJ1F7L>tC=cc+WTpq~5r`sjEpnZi;xD#!>htqgXboW4q&eW%2;zF7VQb8`0vr|(j1 za-y`raQ3F?|1R}9RM2DBI}x+0>y2O=X{}OJJlzh`R+_=X@!4RXd9K^9&pdzND$!Z| z{b}VL+6)e(gMWPDx5u&$>Z7Iw3N9U=&mYxBBCCc=nWviXtv|nFv4wb5Gv0`oG2V*r zRhFfdv+3(U8PpG$-_tAzk(}D9)Xr}(R}W#9Mk!{A^lkc1hS&bmP0=buOuL;nR7)ue z+xexUL0jwiRd}b)Wh<^r&09m8RPP}uspNPw@vK!aC+;S7Wlue?EcoD8s1A{By${OB z57Cn}TLmIK53yg*-*@Oc7T4wpPKlef#lN3)RRmTh2;1!j*sUD!{1LBTR;{)XL&>nX zu>M3x;EhHC0t(sT- zrT3$Tsv^k$jpF=or1yuoUdbv$Ppv}-a#y>TJz=IT?Dy?r1&NZFbfP6FYWdq=VLW+l zEHnLbK$|@Z^IC(Ad<33e9$d&B%M!nFOj>XLJ?97*=$3Z2hZ@IbjEwhyBWNgkeIqMJ zn3Q@&FRHbkc|!QLKcK78$|%(IgB8ze-Z)A6EFS~6N4Ed6De94IXv^#@vV1~*^Ejya z=M06^vTZyiBZNUdX5Q-|85Uow+p7{glhtbehG@fpk=vF%-=(u44)@!*r1@-RL~Vw< zpQFF{)03Gemb7m;o_Z9iOODj&he0n4ffL(vJQA0cs@MD^E;;a$PJ>=b_5xeNv?CY- zW0bq!1bM@{fDhjvl)Lt{Nv+vMeJg zR8WAV+BNp2cSRfV7l5^Y`|OB1=v82oFc-q{fjr^jzF~1f0_!$CbfwyUWWpEyK54A$ z_7k`gM)c-GheOu5?wL7CD3tO+*_zDTDy^HG=(%44u%h_p(C^nU&i z6>bY%X8M;3&vx`Usp6#~hTB5q{2n4vHa3J@W`of3O)QB0C>NiiUIMx<_Yd*{Y6;)D zthLC;&qmQ-xcw&DPKiL@;O;m1Q3xq13!zh^$9=j7_{FkVG10*Yw|^j*ICGHL4c78v zfkWjmw{Ec2prDpK>A*D3%%*tQ6;g3hb6zupXY9tSw zljpiyR)+97P5CFJ_Crzcki)VYg08@0{qS?s{R(_;gO=De05u+Vus(+q@K;UL0beo?v#jlfmEx&ZT>@EazLRy$zcE5Yft`99~ARR;S0^M*7W{kZ#JivlPwA4x5=kUv3CcorT;n`k^1o_m#A z+Chd-2t*tOuiDMP=@PyPJKaj*8dZkQ3S%uQAxI5)2h%jYV9` z76H#)At!xIdgSkxZ4Vp65J}B7QyzFl==g)kkse>%3IW_d8N?+rVNI5p_?)Hk+<(?L1;W84#e zH&TKCb&zR~n<37*9e5ys)k1}tM7$6wl~1btCP*CpkOuOXp<=*b;EcTfa%*vpTe-Na z&x|kL;}0j>RedY&BBk5o4V!3>c-TaUY6Zqj($}4Dr^AQRj!C~S&8lIwALGMCg(@jS z*5Jpgf%5VXT2W`oYhpBdtL<3v2defNHS4fmFhI>mU^I$-pfCPmODXtiq80M)ytKok zuEgsHF-TXi8kL{(g;qZZraI#D!cX=d6P|sE-J0_%yPyIH}`%bZV);5L^pcOc`)wI<4!ZYHAoz zIS2{Ak!SrhLQ;i{f}l%CAysv>$VtJOM_u#KNAT&HXQ!}K?=l^8-6|QB2Az7Nuh*0} z98qTysD@t1C|+|D655D_ju8xbM|LLgz#LAK}@Womk+PKzIAb@^b#rfe`z)Zh(l~@so z6sx@C&)CXVFcF1m$2dN@O2=*9C(UJ7Q{x1s6$VQbX&l-6WlC3S+5 zJ1Y92yz^x%fSx-lwo)hiYqM1(tdL8o^@B#+$ zp@v2zDm}w}0 z!tNo=kbazD5LRND?_9M3LGg{&7P$K&R@jbQA?1&&uG5C4dm2_wgzCOS_aC=~p}4dO z=m+8_PBZRC+gsB)Rvefc-Ye%aF2IQw*cXp2K@OzjLmX+bb-2g_BV7@qaWhO>*`R74 z?T?`3FKA7bcx#QIXebp-HSm*XByQa{!~$>uJ$OqgmfA zEKxsskU>Aht}U6CwX6ow*UV>lY6Jjhm5p@R`pgK4a0(@4<F>^xcSoj{m8z?U*Ci=EqO9PFbqyPd zemI>9k_5>0!Zm!NZ?c^&k_Z20pn4JIQyxAw)R~CX(}t+B<9EVD!Y0u;8;hu>x5+y@ zm|APWMFq|wC1ZgHo<6zscvsA^=mBkWAShW5danxEfDyj}b4ITQDwL5+=oRMsJ1iJ$ zJ?JJ|pk7aOMjwo%f`XvKSuv~b(xoXh3mW5V7qs_z!GUf?;_LPGOo0>p=ArsT3hMry zX|XIonQmy3Fx*#nz4^aArPh?f8i(B~ouE(^?1H-^-j6Q?x+&V1`%j`Ea)8cVc&KM+ z0c)oB?q5hd*hsLdrR+5aU2UW7nMjNWHBn{0(TbmbHj<|tq}t=>G)TCGBc@b8nQ%R$Bc)7*s*Cd^f-|tMndGyXibex$Dvb@9NuW^TYOv+Ii9+6j z!0+8xl)D0SU`>PJag~l4uqwFVwKKB$G{*f`drUzX;g~SxA-lVqGfQfD4b36R(xD%+ zLfQ1p#EtHpMf!+XBgaOW5e45Nz$HegptJGK|vZ5mc=FD7E>g62H zo`D1_-;D=Gq(mOVMgrlA556*=50_< z1_^bMb_T`aa+QXX#v7f!gQ!p`4Q*V)5HduD>z?K+9B1KHX!Lyx%f7_`ab^;l_(LB) ziR2iIEa6WMy91?AYHFtOD&(e9aln;O2$4yhEOx~?hc&F~v#7>UhY6rY?B>34<_lG?hD3h@7Z)XWHUR5lr9sb%RornTQPwZlN<$@%2|@jjzoNTf*c- zwV-qskzFA9P@%>mn#r~fueLf_asx^?G6jC=o7-vJp|$c30Bd|ztBK_u8W8qE-hEj{ zG^mp_cwOc(^z3smEU<6EKm_(Jk=*i17q5`0I!gl>DdiLK=)k2-xTwpT{8038#9neh zte$OMNY!}+>xd-)GN{4}-JZKCr~05&Y*nzCW{KnS4engaK+Ngss3 zvRC6lE{411B>#9EvMJXkU3(StFo{-{!^*x7LYH?L|70CwZhrYT#|O2h!MPHLS#I-4 zB3g_k{GEIXROVx$Irtq)2Q}2LKy{x&r%oV%(YinW-8jw~sLct=E*YEa#{;dj%PWC9 zgak~+v1pE#XB==ENh->gl#EwU_F!0YA{=qR4$R@x&k4B01ge2ml`Sbn*igtstOCNH z+1jd3geKg(_;VN4!a!B9JS|%w)F)*-Xz3{wL?cRUCCM^#eOao`FHNT;_ zO7u>pzAe^pok~gImo%aKmbJsGcXCq>aYQ(zl0tyhnP1$W97jpD>t&te4I#SgeSIp` zCHTAD&cx922n`!G2D0XV^}PHy9EzzG{Vip&Gt$pe)(RJqeFt?*EhH#- z4u{E>8mb0dZ%cJ0b!-*|v@{1e8Ez~Bwf#h@} z(r_Eh($Z3)A2RjUvTllI8X!b;k0VjlyjKp8v_w5NBdk)2-sAeUA?i?`y5BF32}7e+ zBd~;QCyBZk4!iH-X-H^(x46I=zoW$8o?@%ThX)%W3CWcu4h36C!yT83fUyOyxeDN@ z3s2v&e2vHA&t1$+Sd0je=6)?K81WvA4YRA^Y;3`345Q;&cz?!&LawW!@)c+#Fkb5V z6@*Mdbq(&Yh6;I9y)zg|!^URke%2Rh2DeW)zt|sHjfBp^SIq~DX-7~a&q^O99V;X~ z6CS2hU@un`8KNZ9o(mMh9^2z~LGJ2i2Fo>RNVk;}%7d^JV+xiM;HGpS7`ed7{!A)e z9t~&aVFC*R6<9|+OAlzMkDIsqj^=gN+sO?dz1q*;e~F7nU3f7^EzTB8Q*d<#AH8lY zPe$S@u|lmZ(;YH{uHW#E^w6H79PHlWeeH84Nx+Igi%y8)nA}_Q@1oq*q_$F910_pB zpRWs3gog44Qq@w~Gnt8j;byHp)kWjA@)BDo%hCFc+^{JL@zGg&(eLl?ms4$5u3!kA zr3-o)+IoQ143qzC)YixlRw%-9oXe8p( zWqzCQRRy}?>jJSaHt-RKk#=#xa&2fb3sj=O&PxS~+9Kf9Z_2b5SEU|oa@LwtZ|G>k z-&wjYbm;>78==YbKVD2BM-9Ue+LZg5@sK)8Z{PePu0Cun=>Zg6MX`jGvQhd4b3K04 zJfssXQhxgwPsC;W9OjA?qD&A^kI-9&L3Ih?-!Nwh_5Lx*8#Le4_+MlUPX6LPnjl_q zoaR3HbMc7Ru9m-Jd2vBq`!8TE(W3dxx3=Lid@^1z_Hw`i82b*w2C2VqmSae=wM5pqb&37IY83eZ 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 f14c26b..b3500b0 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 @@ -21,6 +21,12 @@ public class DeleteBrandProgressProperties { private long productRiskStaleTimeoutMinutes = 10; private long productRiskInitialTimeoutMinutes = 10; + /** + * Price track RUNNING timeout auto-finalize/fail threshold in minutes. + */ + private long priceTrackStaleTimeoutMinutes = 10; + private long priceTrackInitialTimeoutMinutes = 10; + /** * Shop match RUNNING timeout auto-finalize/fail threshold in minutes. */ diff --git a/backend-java/src/main/java/com/nanri/aiimage/config/ModuleCleanupProperties.java b/backend-java/src/main/java/com/nanri/aiimage/config/ModuleCleanupProperties.java index 1e26bab..f5ae7e8 100644 --- a/backend-java/src/main/java/com/nanri/aiimage/config/ModuleCleanupProperties.java +++ b/backend-java/src/main/java/com/nanri/aiimage/config/ModuleCleanupProperties.java @@ -11,5 +11,5 @@ import java.util.List; public class ModuleCleanupProperties { private boolean enabled = true; private String cron = "0 0 0 * * *"; - private List moduleTypes = new ArrayList<>(List.of("DEDUPE", "SPLIT", "CONVERT", "DELETE_BRAND", "PRODUCT_RISK_RESOLVE")); + private List moduleTypes = new ArrayList<>(List.of("DEDUPE", "SPLIT", "CONVERT", "DELETE_BRAND", "PRODUCT_RISK_RESOLVE", "PRICE_TRACK")); } 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 0a8d949..703a2ce 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 @@ -6,6 +6,8 @@ import com.nanri.aiimage.common.service.DistributedJobLockService; 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.pricetrack.service.PriceTrackTaskCacheService; +import com.nanri.aiimage.modules.pricetrack.service.PriceTrackTaskService; import com.nanri.aiimage.modules.shopmatch.service.ShopMatchTaskCacheService; import com.nanri.aiimage.modules.shopmatch.service.ShopMatchTaskService; import com.nanri.aiimage.modules.task.mapper.FileTaskMapper; @@ -28,6 +30,7 @@ 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_PRICE_TRACK = "PRICE_TRACK"; private static final String MODULE_TYPE_SHOP_MATCH = "SHOP_MATCH"; private static final Duration STALE_CHECK_LOCK_TTL = Duration.ofMinutes(10); private static final Duration FINALIZE_CHECK_LOCK_TTL = Duration.ofMinutes(10); @@ -37,6 +40,8 @@ public class DeleteBrandStaleTaskService { private final DeleteBrandRunService deleteBrandRunService; private final ProductRiskTaskService productRiskTaskService; private final ProductRiskTaskCacheService productRiskTaskCacheService; + private final PriceTrackTaskService priceTrackTaskService; + private final PriceTrackTaskCacheService priceTrackTaskCacheService; private final ShopMatchTaskService shopMatchTaskService; private final ShopMatchTaskCacheService shopMatchTaskCacheService; private final DeleteBrandProgressProperties deleteBrandProgressProperties; @@ -55,6 +60,7 @@ public class DeleteBrandStaleTaskService { log.info("[stale-check] scan started thread={}", Thread.currentThread().getName()); failStaleDeleteBrandTasks(); ProductRiskStaleCheckStats stats = failStaleProductRiskResolveTasks(); + PriceTrackStaleCheckStats priceTrackStats = failStalePriceTrackTasks(); ShopMatchStaleCheckStats shopMatchStats = failStaleShopMatchTasks(); log.info("[stale-check] product-risk summary scanned={} finalized={} failed={} skipped={} elapsedMs={} thread={}", stats.scannedTaskCount, @@ -63,6 +69,13 @@ public class DeleteBrandStaleTaskService { stats.skippedTaskCount, System.currentTimeMillis() - startedAt, Thread.currentThread().getName()); + log.info("[stale-check] price-track summary scanned={} finalized={} failed={} skipped={} elapsedMs={} thread={}", + priceTrackStats.scannedTaskCount, + priceTrackStats.finalizedTaskCount, + priceTrackStats.failedTaskCount, + priceTrackStats.skippedTaskCount, + System.currentTimeMillis() - startedAt, + Thread.currentThread().getName()); log.info("[stale-check] shop-match summary scanned={} finalized={} failed={} skipped={} elapsedMs={} thread={}", shopMatchStats.scannedTaskCount, shopMatchStats.finalizedTaskCount, @@ -196,6 +209,67 @@ public class DeleteBrandStaleTaskService { return stats; } + private PriceTrackStaleCheckStats failStalePriceTrackTasks() { + PriceTrackStaleCheckStats stats = new PriceTrackStaleCheckStats(); + long minutes = Math.max(1L, deleteBrandProgressProperties.getPriceTrackStaleTimeoutMinutes()); + long initialMinutes = Math.max(minutes, deleteBrandProgressProperties.getPriceTrackInitialTimeoutMinutes()); + long staleTimeoutMillis = Duration.ofMinutes(minutes).toMillis(); + long nowMillis = System.currentTimeMillis(); + 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_PRICE_TRACK) + .eq(FileTaskEntity::getStatus, "RUNNING") + .lt(FileTaskEntity::getUpdatedAt, threshold) + .last("limit 200")); + stats.scannedTaskCount = runningTasks.size(); + if (runningTasks.isEmpty()) { + return stats; + } + log.info("[stale-check] price-track candidates={} threshold={} timeoutMinutes={} initialThreshold={} initialTimeoutMinutes={}", + runningTasks.size(), threshold, minutes, initialThreshold, initialMinutes); + for (FileTaskEntity task : runningTasks) { + long lastHeartbeatMillis = priceTrackTaskCacheService.getTaskHeartbeatMillis(task.getId()); + boolean hasFinishedRows = (task.getSuccessFileCount() != null && task.getSuccessFileCount() > 0) + || (task.getFailedFileCount() != null && task.getFailedFileCount() > 0); + boolean hasStartedProgress = lastHeartbeatMillis > 0L || hasFinishedRows; + if (lastHeartbeatMillis > 0L && nowMillis - lastHeartbeatMillis < staleTimeoutMillis) { + stats.skippedTaskCount++; + continue; + } + if (!hasStartedProgress && task.getCreatedAt() != null && task.getCreatedAt().isAfter(initialThreshold)) { + stats.skippedTaskCount++; + continue; + } + try { + if (priceTrackTaskService.tryFinalizeTask(task.getId(), true)) { + stats.finalizedTaskCount++; + log.info("[stale-check] price-track finalized taskId={}", task.getId()); + continue; + } + } catch (Exception ex) { + log.warn("[stale-check] price-track 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_PRICE_TRACK) + .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++; + priceTrackTaskCacheService.deleteTaskCache(task.getId()); + log.warn("[stale-check] price-track failed taskId={} reason=timeout", task.getId()); + } else { + stats.skippedTaskCount++; + } + } + return stats; + } + private ShopMatchStaleCheckStats failStaleShopMatchTasks() { ShopMatchStaleCheckStats stats = new ShopMatchStaleCheckStats(); long minutes = Math.max(1L, deleteBrandProgressProperties.getShopMatchStaleTimeoutMinutes()); @@ -291,4 +365,11 @@ public class DeleteBrandStaleTaskService { private int failedTaskCount; private int skippedTaskCount; } + + private static final class PriceTrackStaleCheckStats { + private int scannedTaskCount; + private int finalizedTaskCount; + private int failedTaskCount; + private int skippedTaskCount; + } } diff --git a/backend-java/src/main/java/com/nanri/aiimage/modules/pricetrack/controller/PriceTrackController.java b/backend-java/src/main/java/com/nanri/aiimage/modules/pricetrack/controller/PriceTrackController.java index fbaf64d..cf1b812 100644 --- a/backend-java/src/main/java/com/nanri/aiimage/modules/pricetrack/controller/PriceTrackController.java +++ b/backend-java/src/main/java/com/nanri/aiimage/modules/pricetrack/controller/PriceTrackController.java @@ -2,16 +2,19 @@ package com.nanri.aiimage.modules.pricetrack.controller; import com.nanri.aiimage.common.api.ApiResponse; import com.nanri.aiimage.modules.pricetrack.model.dto.PriceTrackCandidateAddRequest; +import com.nanri.aiimage.modules.pricetrack.model.dto.PriceTrackCountryPreferenceSaveRequest; import com.nanri.aiimage.modules.pricetrack.model.dto.PriceTrackCreateTaskRequest; import com.nanri.aiimage.modules.pricetrack.model.dto.PriceTrackMatchShopsRequest; import com.nanri.aiimage.modules.pricetrack.model.dto.PriceTrackSubmitResultRequest; -import com.nanri.aiimage.modules.pricetrack.model.dto.PriceTrackCountryPreferenceSaveRequest; +import com.nanri.aiimage.modules.pricetrack.model.dto.PriceTrackTaskBatchRequest; import com.nanri.aiimage.modules.pricetrack.model.vo.PriceTrackCandidateVo; import com.nanri.aiimage.modules.pricetrack.model.vo.PriceTrackCountryPreferenceVo; import com.nanri.aiimage.modules.pricetrack.model.vo.PriceTrackCreateTaskVo; import com.nanri.aiimage.modules.pricetrack.model.vo.PriceTrackDashboardVo; import com.nanri.aiimage.modules.pricetrack.model.vo.PriceTrackHistoryVo; import com.nanri.aiimage.modules.pricetrack.model.vo.PriceTrackMatchShopsVo; +import com.nanri.aiimage.modules.pricetrack.model.vo.PriceTrackPendingDeleteVo; +import com.nanri.aiimage.modules.pricetrack.model.vo.PriceTrackTaskBatchVo; import com.nanri.aiimage.modules.pricetrack.service.PriceTrackService; import com.nanri.aiimage.modules.pricetrack.service.PriceTrackTaskService; import io.swagger.v3.oas.annotations.Operation; @@ -20,127 +23,164 @@ 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.web.bind.annotation.*; +import org.springframework.http.HttpHeaders; +import org.springframework.http.HttpStatus; +import org.springframework.web.bind.annotation.DeleteMapping; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.PathVariable; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.PutMapping; +import org.springframework.web.bind.annotation.RequestBody; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RequestParam; +import org.springframework.web.bind.annotation.RestController; import org.springframework.web.server.ResponseStatusException; -import java.net.URI; -import java.nio.charset.StandardCharsets; -import java.net.URLEncoder; import java.io.InputStream; +import java.net.URI; +import java.net.URLEncoder; +import java.nio.charset.StandardCharsets; +import java.util.List; @RestController @RequiredArgsConstructor @RequestMapping("/api/price-track") -@Tag(name = "跟价", description = "亚马逊跟价处理:备选店铺、紫鸟匹配、创建任务、Python队列与回传。") +@Tag( + name = "跟价", + description = "亚马逊跟价处理模块:包含备选店铺、紫鸟匹配、任务创建、Python 回传、任务轮询和结果下载接口。查询类接口需要携带 Query 参数 user_id。") public class PriceTrackController { private final PriceTrackService priceTrackService; private final PriceTrackTaskService priceTrackTaskService; @GetMapping("/candidates") - @Operation(summary = "查询备选店铺列表") - public ApiResponse> listCandidates( - @Parameter(name = "user_id", description = "当前用户 ID", required = true, in = ParameterIn.QUERY) + @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(priceTrackService.listCandidates(userId)); } @PostMapping("/candidates") - @Operation(summary = "新增备选店铺") + @Operation(summary = "新增备选店铺", description = "把店铺名称写入当前用户的备选店铺列表,供后续匹配和创建任务使用。") public ApiResponse addCandidate(@Valid @RequestBody PriceTrackCandidateAddRequest request) { return ApiResponse.success(priceTrackService.addCandidate(request)); } @DeleteMapping("/candidates/{id}") - @Operation(summary = "删除备选店铺") + @Operation(summary = "删除备选店铺", description = "删除一条备选店铺记录,id 必须属于当前 user_id。") public ApiResponse deleteCandidate( - @PathVariable Long id, - @Parameter(name = "user_id", required = true, in = ParameterIn.QUERY) @RequestParam("user_id") Long userId) { + @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) { priceTrackService.deleteCandidate(userId, id); return ApiResponse.success(null); } @GetMapping("/country-preference") - @Operation(summary = "查询国家处理顺序") + @Operation(summary = "查询国家处理顺序", description = "返回当前用户保存的国家代码列表,顺序即跟价任务处理顺序。") public ApiResponse getCountryPreference( - @Parameter(name = "user_id", required = true, in = ParameterIn.QUERY) @RequestParam("user_id") Long userId) { + @Parameter(name = "user_id", description = "当前用户 ID", required = true, in = ParameterIn.QUERY, example = "1") + @RequestParam("user_id") Long userId) { return ApiResponse.success(priceTrackService.getCountryPreference(userId)); } @PutMapping("/country-preference") - @Operation(summary = "保存国家处理顺序") + @Operation(summary = "保存国家处理顺序", description = "保存当前用户在跟价模块中的国家勾选和处理顺序。") public ApiResponse saveCountryPreference( @Valid @RequestBody PriceTrackCountryPreferenceSaveRequest request) { return ApiResponse.success(priceTrackService.saveCountryPreference(request)); } @PostMapping("/match-shops") - @Operation(summary = "批量匹配店铺") + @Operation(summary = "批量匹配店铺", description = "按店铺名查询紫鸟索引,返回 shopId、platform、companyName、matchStatus 等信息。") public ApiResponse matchShops(@Valid @RequestBody PriceTrackMatchShopsRequest request) { return ApiResponse.success(priceTrackService.matchShops(request)); } @GetMapping("/dashboard") - @Operation(summary = "统计看板") + @Operation(summary = "统计看板", description = "返回备选店铺数、已结束任务数、成功任务数和失败任务数。") public ApiResponse dashboard( - @Parameter(name = "user_id", required = true, in = ParameterIn.QUERY) @RequestParam("user_id") Long userId) { + @Parameter(name = "user_id", description = "当前用户 ID", required = true, in = ParameterIn.QUERY, example = "1") + @RequestParam("user_id") Long userId) { return ApiResponse.success(priceTrackTaskService.dashboard(userId)); } @GetMapping("/history") - @Operation(summary = "处理记录列表") + @Operation(summary = "处理记录列表", description = "返回当前用户跟价模块下各店铺结果行,包含运行中和历史记录。") public ApiResponse history( - @Parameter(name = "user_id", required = true, in = ParameterIn.QUERY) @RequestParam("user_id") Long userId) { + @Parameter(name = "user_id", description = "当前用户 ID", required = true, in = ParameterIn.QUERY, example = "1") + @RequestParam("user_id") Long userId) { return ApiResponse.success(priceTrackTaskService.listHistory(userId)); } @DeleteMapping("/history/{resultId}") - @Operation(summary = "删除单条结果记录") + @Operation(summary = "删除单条结果记录", description = "删除一条 biz_file_result,并同步重算父任务状态。") public ApiResponse deleteHistory( - @PathVariable Long resultId, - @Parameter(name = "user_id", required = true, in = ParameterIn.QUERY) @RequestParam("user_id") Long userId) { + @Parameter(description = "结果行主键 biz_file_result.id", example = "1001") @PathVariable Long resultId, + @Parameter(name = "user_id", description = "当前用户 ID", required = true, in = ParameterIn.QUERY, example = "1") + @RequestParam("user_id") Long userId) { priceTrackTaskService.deleteHistory(resultId, userId); return ApiResponse.success(null); } @PostMapping("/tasks") - @Operation(summary = "创建跟价处理任务") + @Operation(summary = "创建跟价任务", description = "落库 RUNNING 任务和各店铺占位结果,返回 taskId 和初始 items 快照。") public ApiResponse createTask(@Valid @RequestBody PriceTrackCreateTaskRequest request) { return ApiResponse.success(priceTrackTaskService.createTask(request)); } @DeleteMapping("/tasks/{taskId}") - @Operation(summary = "删除整条任务") + @Operation(summary = "删除整条任务", description = "删除该任务以及其下属全部结果行,RUNNING、SUCCESS、FAILED 均可。") public ApiResponse deleteTask( - @PathVariable Long taskId, - @Parameter(name = "user_id", required = true, in = ParameterIn.QUERY) @RequestParam("user_id") Long userId) { + @Parameter(description = "任务主键 biz_file_task.id", example = "200") @PathVariable Long taskId, + @Parameter(name = "user_id", description = "当前用户 ID", required = true, in = ParameterIn.QUERY, example = "1") + @RequestParam("user_id") Long userId) { priceTrackTaskService.deleteTask(taskId, userId); return ApiResponse.success(null); } + @DeleteMapping("/pending-shop-result") + @Operation(summary = "按店铺名删除运行中结果", description = "仅在 RUNNING 任务下按规范化店名删除一条 biz_file_result,并同步重算任务状态。") + public ApiResponse deletePendingShopResult( + @Parameter(name = "user_id", description = "当前用户 ID", required = true, in = ParameterIn.QUERY, example = "1") + @RequestParam("user_id") Long userId, + @Parameter(name = "shop_name", description = "店铺名称", required = true, in = ParameterIn.QUERY, example = "某某店铺") + @RequestParam("shop_name") String shopName) { + return ApiResponse.success(priceTrackTaskService.deletePendingShopResult(userId, shopName)); + } + + @PostMapping("/tasks/batch") + @Operation(summary = "批量查询任务详情", description = "按 taskIds 返回任务概要和各店铺结果,供前端轮询使用。") + public ApiResponse tasksBatch(@Valid @RequestBody PriceTrackTaskBatchRequest request) { + return ApiResponse.success(priceTrackTaskService.getTaskDetailsBatch(request.getTaskIds())); + } + @PostMapping("/tasks/{taskId}/result") - @Operation(summary = "Python回传处理结果") - public ApiResponse submitResult(@PathVariable Long taskId, + @Operation(summary = "Python 回传处理结果", description = "同一 taskId 可多次回传。成功时按 countries 生成结果文件,所有店铺进入终态后任务会变为 SUCCESS 或 FAILED。") + public ApiResponse submitResult( + @Parameter(description = "任务主键,必须处于 RUNNING 状态", example = "200") @PathVariable Long taskId, @Valid @RequestBody PriceTrackSubmitResultRequest request) { priceTrackTaskService.submitResult(taskId, request); return ApiResponse.success(null); } @GetMapping("/results/{resultId}/download") - @Operation(summary = "下载结果文件") + @Operation(summary = "下载结果文件", description = "按结果记录下载后端生成的跟价 Excel 文件。") public void downloadResult( - @PathVariable Long resultId, - @Parameter(name = "user_id", required = true, in = ParameterIn.QUERY) @RequestParam("user_id") Long userId, + @Parameter(description = "结果记录主键 biz_file_result.id", 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 = priceTrackTaskService.resolveResultDownloadUrl(resultId, userId); String filename = priceTrackTaskService.resolveResultDownloadFilename(resultId, userId); if (url == null || url.isBlank()) { - throw new ResponseStatusException(org.springframework.http.HttpStatus.NOT_FOUND, "暂无可下载结果"); + throw new ResponseStatusException(HttpStatus.NOT_FOUND, "暂无可下载结果"); } try { String encodedFilename = URLEncoder.encode(filename, StandardCharsets.UTF_8).replace("+", "%20"); response.setContentType("application/octet-stream"); - response.setHeader(org.springframework.http.HttpHeaders.CONTENT_DISPOSITION, + response.setHeader(HttpHeaders.CONTENT_DISPOSITION, "attachment; filename=\"" + filename + "\"; filename*=UTF-8''" + encodedFilename); try (InputStream in = URI.create(url).toURL().openStream()) { byte[] buffer = new byte[65536]; @@ -151,7 +191,7 @@ public class PriceTrackController { response.getOutputStream().flush(); } } catch (Exception ex) { - throw new ResponseStatusException(org.springframework.http.HttpStatus.INTERNAL_SERVER_ERROR, "下载失败"); + throw new ResponseStatusException(HttpStatus.INTERNAL_SERVER_ERROR, "下载失败"); } } } diff --git a/backend-java/src/main/java/com/nanri/aiimage/modules/pricetrack/model/dto/PriceTrackCandidateAddRequest.java b/backend-java/src/main/java/com/nanri/aiimage/modules/pricetrack/model/dto/PriceTrackCandidateAddRequest.java index 52130b5..903cc95 100644 --- a/backend-java/src/main/java/com/nanri/aiimage/modules/pricetrack/model/dto/PriceTrackCandidateAddRequest.java +++ b/backend-java/src/main/java/com/nanri/aiimage/modules/pricetrack/model/dto/PriceTrackCandidateAddRequest.java @@ -1,14 +1,18 @@ package com.nanri.aiimage.modules.pricetrack.model.dto; +import io.swagger.v3.oas.annotations.media.Schema; import jakarta.validation.constraints.NotBlank; import jakarta.validation.constraints.NotNull; import lombok.Data; @Data +@Schema(description = "新增备选店铺请求体:写入用户维度的待处理店铺列表,供前端勾选与匹配") public class PriceTrackCandidateAddRequest { @NotNull(message = "user_id不能为空") + @Schema(description = "当前用户 ID,数据按用户隔离", requiredMode = Schema.RequiredMode.REQUIRED, example = "1") private Long userId; - @NotBlank(message = "店铺名不能为空") + @NotBlank(message = "店铺名称不能为空") + @Schema(description = "店铺名称,入库前会先做规范化和去重", requiredMode = Schema.RequiredMode.REQUIRED, example = "测试店铺A") private String shopName; } diff --git a/backend-java/src/main/java/com/nanri/aiimage/modules/pricetrack/model/dto/PriceTrackCountryPreferenceSaveRequest.java b/backend-java/src/main/java/com/nanri/aiimage/modules/pricetrack/model/dto/PriceTrackCountryPreferenceSaveRequest.java index 398bf35..e12acf0 100644 --- a/backend-java/src/main/java/com/nanri/aiimage/modules/pricetrack/model/dto/PriceTrackCountryPreferenceSaveRequest.java +++ b/backend-java/src/main/java/com/nanri/aiimage/modules/pricetrack/model/dto/PriceTrackCountryPreferenceSaveRequest.java @@ -1,15 +1,20 @@ package com.nanri.aiimage.modules.pricetrack.model.dto; +import io.swagger.v3.oas.annotations.media.Schema; import jakarta.validation.constraints.NotEmpty; import jakarta.validation.constraints.NotNull; import lombok.Data; + import java.util.List; @Data +@Schema(description = "保存用户五国处理顺序:代码仅支持 DE、UK、FR、IT、ES,且不能重复") public class PriceTrackCountryPreferenceSaveRequest { @NotNull(message = "user_id不能为空") + @Schema(description = "当前用户 ID", requiredMode = Schema.RequiredMode.REQUIRED, example = "1") private Long userId; @NotEmpty(message = "国家列表不能为空") + @Schema(description = "国家代码列表,顺序即任务处理顺序", requiredMode = Schema.RequiredMode.REQUIRED, example = "[\"DE\",\"UK\",\"FR\"]") private List countryCodes; } diff --git a/backend-java/src/main/java/com/nanri/aiimage/modules/pricetrack/model/dto/PriceTrackCreateTaskRequest.java b/backend-java/src/main/java/com/nanri/aiimage/modules/pricetrack/model/dto/PriceTrackCreateTaskRequest.java index b57fdf2..66d4e04 100644 --- a/backend-java/src/main/java/com/nanri/aiimage/modules/pricetrack/model/dto/PriceTrackCreateTaskRequest.java +++ b/backend-java/src/main/java/com/nanri/aiimage/modules/pricetrack/model/dto/PriceTrackCreateTaskRequest.java @@ -1,35 +1,36 @@ package com.nanri.aiimage.modules.pricetrack.model.dto; +import com.nanri.aiimage.modules.pricetrack.model.vo.PriceTrackMatchShopsVo; +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.util.List; -import java.util.Map; @Data +@Schema(description = "创建跟价任务请求体:在推送 Python 队列前调用,用于落库任务和店铺结果占位记录") public class PriceTrackCreateTaskRequest { @NotNull(message = "user_id不能为空") + @Schema(description = "任务所属用户 ID,会写入 biz_file_task.user_id", requiredMode = Schema.RequiredMode.REQUIRED, example = "1") private Long userId; - /** 跟价模式:status_mode=按商品状态(全量), asin_mode=按指定ASIN(自定义) */ + @Schema(description = "是否启用按商品状态处理模式") private boolean statusMode; + + @Schema(description = "是否启用按指定 ASIN 文件处理模式") private boolean asinMode; - /** 匹配好的店铺队列项 */ @NotEmpty(message = "店铺列表不能为空") - private List> items; + @Valid + @Schema(description = "已匹配的店铺列表,每项都应为 matched=true;服务端会按规范化店名去重", requiredMode = Schema.RequiredMode.REQUIRED) + private List items; - /** ASIN文档路径(asinMode时使用) */ + @Schema(description = "ASIN 文件路径列表;asinMode=true 时由前端上传或选择后传入", example = "[\"/tmp/a.xlsx\",\"/tmp/b.csv\"]") private List asinFiles; - /** 国家代码列表 */ @NotEmpty(message = "国家列表不能为空") + @Schema(description = "国家代码列表,顺序即跟价处理顺序", requiredMode = Schema.RequiredMode.REQUIRED, example = "[\"DE\",\"UK\",\"FR\"]") private List countryCodes; - - /** 价格区间 */ - private Double minPrice; - private Double maxPrice; - - /** 价差比例(%) */ - private Double priceDiffPercent; } diff --git a/backend-java/src/main/java/com/nanri/aiimage/modules/pricetrack/model/dto/PriceTrackMatchShopsRequest.java b/backend-java/src/main/java/com/nanri/aiimage/modules/pricetrack/model/dto/PriceTrackMatchShopsRequest.java index 56ff150..a743000 100644 --- a/backend-java/src/main/java/com/nanri/aiimage/modules/pricetrack/model/dto/PriceTrackMatchShopsRequest.java +++ b/backend-java/src/main/java/com/nanri/aiimage/modules/pricetrack/model/dto/PriceTrackMatchShopsRequest.java @@ -1,15 +1,20 @@ package com.nanri.aiimage.modules.pricetrack.model.dto; +import io.swagger.v3.oas.annotations.media.Schema; import jakarta.validation.constraints.NotEmpty; import jakarta.validation.constraints.NotNull; import lombok.Data; + import java.util.List; @Data +@Schema(description = "批量匹配店铺请求体:按店铺名查询紫鸟索引,返回是否命中及店铺元数据") public class PriceTrackMatchShopsRequest { @NotNull(message = "user_id不能为空") + @Schema(description = "当前用户 ID,用于审计和数据归属", requiredMode = Schema.RequiredMode.REQUIRED, example = "1") private Long userId; @NotEmpty(message = "店铺列表不能为空") + @Schema(description = "待匹配的店铺名称列表,至少 1 个;返回体 items 与列表顺序对应", requiredMode = Schema.RequiredMode.REQUIRED, example = "[\"店铺甲\",\"店铺乙\"]") private List shopNames; } diff --git a/backend-java/src/main/java/com/nanri/aiimage/modules/pricetrack/model/dto/PriceTrackSubmitResultRequest.java b/backend-java/src/main/java/com/nanri/aiimage/modules/pricetrack/model/dto/PriceTrackSubmitResultRequest.java index 4ea279b..5663628 100644 --- a/backend-java/src/main/java/com/nanri/aiimage/modules/pricetrack/model/dto/PriceTrackSubmitResultRequest.java +++ b/backend-java/src/main/java/com/nanri/aiimage/modules/pricetrack/model/dto/PriceTrackSubmitResultRequest.java @@ -1,27 +1,49 @@ package com.nanri.aiimage.modules.pricetrack.model.dto; +import io.swagger.v3.oas.annotations.media.Schema; import lombok.Data; + import java.util.List; import java.util.Map; @Data +@Schema(description = "Python 回传跟价结果请求体:按店铺提交各国家 ASIN 跟价结果或错误信息") public class PriceTrackSubmitResultRequest { + @Schema(description = "本次回传涉及的店铺列表,至少 1 个,店名需要能和任务结果行匹配", requiredMode = Schema.RequiredMode.REQUIRED) private List shops; @Data + @Schema(description = "单个店铺回传结果:成功时提供 countries,失败时可只提供 error") public static class ShopResult { + @Schema(description = "店铺名称,用于和任务内结果行匹配", example = "测试店铺A") private String shopName; + + @Schema(description = "按国家分组的结果行,key 为国家代码,例如 DE、UK、FR、IT、ES") private Map> countries; + + @Schema(description = "失败时的错误信息;非空则该店铺记为失败") private String error; + + @Schema(description = "是否成功;false 按失败处理,true 或 null 通常表示结合 countries 判定") private Boolean success; } @Data + @Schema(description = "单条 ASIN 跟价结果") public static class AsinResult { + @Schema(description = "ASIN", example = "B0ABCDE123") private String asin; + + @Schema(description = "当前店铺价格", example = "19.99") private String currentPrice; + + @Schema(description = "竞争对手价格", example = "18.50") private String competitorPrice; + + @Schema(description = "建议动作或跟价动作", example = "LOWER_PRICE") private String action; + + @Schema(description = "该行是否已处理完成") private Boolean done; } } diff --git a/backend-java/src/main/java/com/nanri/aiimage/modules/pricetrack/model/dto/PriceTrackTaskBatchRequest.java b/backend-java/src/main/java/com/nanri/aiimage/modules/pricetrack/model/dto/PriceTrackTaskBatchRequest.java new file mode 100644 index 0000000..c82a121 --- /dev/null +++ b/backend-java/src/main/java/com/nanri/aiimage/modules/pricetrack/model/dto/PriceTrackTaskBatchRequest.java @@ -0,0 +1,16 @@ +package com.nanri.aiimage.modules.pricetrack.model.dto; + +import io.swagger.v3.oas.annotations.media.Schema; +import jakarta.validation.constraints.NotEmpty; +import lombok.Data; + +import java.util.ArrayList; +import java.util.List; + +@Data +@Schema(description = "批量查询任务详情请求体:一次拉取多个 taskId 的概要和下属店铺结果,供轮询使用") +public class PriceTrackTaskBatchRequest { + @NotEmpty(message = "taskIds不能为空") + @Schema(description = "待查询的任务主键列表,无效或非本模块 taskId 会出现在 missingTaskIds", requiredMode = Schema.RequiredMode.REQUIRED, example = "[200,201]") + private List taskIds = new ArrayList<>(); +} diff --git a/backend-java/src/main/java/com/nanri/aiimage/modules/pricetrack/model/vo/PriceTrackCandidateVo.java b/backend-java/src/main/java/com/nanri/aiimage/modules/pricetrack/model/vo/PriceTrackCandidateVo.java index df9da27..a6a13b0 100644 --- a/backend-java/src/main/java/com/nanri/aiimage/modules/pricetrack/model/vo/PriceTrackCandidateVo.java +++ b/backend-java/src/main/java/com/nanri/aiimage/modules/pricetrack/model/vo/PriceTrackCandidateVo.java @@ -1,11 +1,16 @@ package com.nanri.aiimage.modules.pricetrack.model.vo; +import io.swagger.v3.oas.annotations.media.Schema; import lombok.Data; import lombok.experimental.Accessors; @Data @Accessors(chain = true) +@Schema(description = "备选店铺列表单项") public class PriceTrackCandidateVo { + @Schema(description = "记录主键,删除备选店铺时作为路径参数", example = "10") private Long id; + + @Schema(description = "店铺名称", example = "测试店铺A") private String shopName; } diff --git a/backend-java/src/main/java/com/nanri/aiimage/modules/pricetrack/model/vo/PriceTrackCountryPreferenceVo.java b/backend-java/src/main/java/com/nanri/aiimage/modules/pricetrack/model/vo/PriceTrackCountryPreferenceVo.java index ebd9010..08f6457 100644 --- a/backend-java/src/main/java/com/nanri/aiimage/modules/pricetrack/model/vo/PriceTrackCountryPreferenceVo.java +++ b/backend-java/src/main/java/com/nanri/aiimage/modules/pricetrack/model/vo/PriceTrackCountryPreferenceVo.java @@ -1,10 +1,16 @@ package com.nanri.aiimage.modules.pricetrack.model.vo; +import io.swagger.v3.oas.annotations.media.Schema; import lombok.Data; + import java.util.List; @Data +@Schema(description = "用户五国处理偏好:按顺序返回国家代码列表,例如 DE、UK、FR、IT、ES") public class PriceTrackCountryPreferenceVo { + @Schema(description = "当前用户 ID", example = "1") private Long userId; + + @Schema(description = "已选国家代码列表;若用户未保存,则服务端返回默认顺序 DE、UK、FR、IT、ES") private List countryCodes; } diff --git a/backend-java/src/main/java/com/nanri/aiimage/modules/pricetrack/model/vo/PriceTrackCreateTaskVo.java b/backend-java/src/main/java/com/nanri/aiimage/modules/pricetrack/model/vo/PriceTrackCreateTaskVo.java index 322031d..7acb2a7 100644 --- a/backend-java/src/main/java/com/nanri/aiimage/modules/pricetrack/model/vo/PriceTrackCreateTaskVo.java +++ b/backend-java/src/main/java/com/nanri/aiimage/modules/pricetrack/model/vo/PriceTrackCreateTaskVo.java @@ -1,11 +1,16 @@ package com.nanri.aiimage.modules.pricetrack.model.vo; +import io.swagger.v3.oas.annotations.media.Schema; import lombok.Data; + import java.util.List; -import java.util.Map; @Data +@Schema(description = "创建任务响应:包含任务 id 和各店铺的初始结果快照") public class PriceTrackCreateTaskVo { + @Schema(description = "新建任务主键 biz_file_task.id,前端推送队列时需要回传该值", example = "200") private Long taskId; - private List> items; + + @Schema(description = "各店铺一条初始结果快照,包含 resultId、taskId、店铺信息和初始任务状态") + private List items; } diff --git a/backend-java/src/main/java/com/nanri/aiimage/modules/pricetrack/model/vo/PriceTrackDashboardVo.java b/backend-java/src/main/java/com/nanri/aiimage/modules/pricetrack/model/vo/PriceTrackDashboardVo.java index 451d1e8..fb47093 100644 --- a/backend-java/src/main/java/com/nanri/aiimage/modules/pricetrack/model/vo/PriceTrackDashboardVo.java +++ b/backend-java/src/main/java/com/nanri/aiimage/modules/pricetrack/model/vo/PriceTrackDashboardVo.java @@ -1,13 +1,22 @@ package com.nanri.aiimage.modules.pricetrack.model.vo; +import io.swagger.v3.oas.annotations.media.Schema; import lombok.Data; import lombok.experimental.Accessors; @Data @Accessors(chain = true) +@Schema(description = "跟价模块统计看板,按当前用户维度汇总") public class PriceTrackDashboardVo { - private int candidateCount; - private int processedTaskCount; - private int successTaskCount; - private int failedTaskCount; + @Schema(description = "备选店铺数量") + private Long candidateCount; + + @Schema(description = "已结束任务数,状态为 SUCCESS 或 FAILED") + private Long processedTaskCount; + + @Schema(description = "成功结束的任务数") + private Long successTaskCount; + + @Schema(description = "失败结束的任务数") + private Long failedTaskCount; } diff --git a/backend-java/src/main/java/com/nanri/aiimage/modules/pricetrack/model/vo/PriceTrackHistoryVo.java b/backend-java/src/main/java/com/nanri/aiimage/modules/pricetrack/model/vo/PriceTrackHistoryVo.java index d56f5e5..8590dd3 100644 --- a/backend-java/src/main/java/com/nanri/aiimage/modules/pricetrack/model/vo/PriceTrackHistoryVo.java +++ b/backend-java/src/main/java/com/nanri/aiimage/modules/pricetrack/model/vo/PriceTrackHistoryVo.java @@ -1,9 +1,13 @@ package com.nanri.aiimage.modules.pricetrack.model.vo; +import io.swagger.v3.oas.annotations.media.Schema; import lombok.Data; + import java.util.List; @Data +@Schema(description = "跟价结果列表响应:items 中每项对应一个店铺结果行") public class PriceTrackHistoryVo { + @Schema(description = "结果列表,包含运行中和历史记录") private List items; } diff --git a/backend-java/src/main/java/com/nanri/aiimage/modules/pricetrack/model/vo/PriceTrackMatchShopsVo.java b/backend-java/src/main/java/com/nanri/aiimage/modules/pricetrack/model/vo/PriceTrackMatchShopsVo.java index 37090bc..e039a29 100644 --- a/backend-java/src/main/java/com/nanri/aiimage/modules/pricetrack/model/vo/PriceTrackMatchShopsVo.java +++ b/backend-java/src/main/java/com/nanri/aiimage/modules/pricetrack/model/vo/PriceTrackMatchShopsVo.java @@ -1,20 +1,39 @@ package com.nanri.aiimage.modules.pricetrack.model.vo; +import io.swagger.v3.oas.annotations.media.Schema; import lombok.Data; + +import java.util.ArrayList; import java.util.List; @Data +@Schema(description = "批量匹配店铺响应") public class PriceTrackMatchShopsVo { - private List items; + @Schema(description = "逐店返回的匹配结果,顺序与请求 shopNames 对应") + private List items = new ArrayList<>(); @Data + @Schema(description = "单店匹配结果,同时也是创建任务时 items 的结构") public static class PriceTrackShopQueueItem { + @Schema(description = "店铺名称", example = "某某店铺") private String shopName; - private Long shopId; + + @Schema(description = "紫鸟店铺 ID,未命中时可能为空") + private Object shopId; + + @Schema(description = "平台,例如亚马逊", example = "亚马逊") private String platform; + + @Schema(description = "公司名称或主体标识") private String companyName; + + @Schema(description = "是否命中紫鸟索引;false 时不可用于创建任务") private Boolean matched; + + @Schema(description = "匹配状态码,例如 MATCHED、PENDING、CONFLICT、INDEX_STALE") private String matchStatus; + + @Schema(description = "人类可读说明,供前端展示") private String matchMessage; } } diff --git a/backend-java/src/main/java/com/nanri/aiimage/modules/pricetrack/model/vo/PriceTrackPendingDeleteVo.java b/backend-java/src/main/java/com/nanri/aiimage/modules/pricetrack/model/vo/PriceTrackPendingDeleteVo.java new file mode 100644 index 0000000..892c29c --- /dev/null +++ b/backend-java/src/main/java/com/nanri/aiimage/modules/pricetrack/model/vo/PriceTrackPendingDeleteVo.java @@ -0,0 +1,11 @@ +package com.nanri.aiimage.modules.pricetrack.model.vo; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; + +@Data +@Schema(description = "按店铺名删除运行中结果的响应") +public class PriceTrackPendingDeleteVo { + @Schema(description = "是否实际删除到一条运行中的结果记录") + private boolean removed; +} diff --git a/backend-java/src/main/java/com/nanri/aiimage/modules/pricetrack/model/vo/PriceTrackResultItemVo.java b/backend-java/src/main/java/com/nanri/aiimage/modules/pricetrack/model/vo/PriceTrackResultItemVo.java index 9159012..4e91d6e 100644 --- a/backend-java/src/main/java/com/nanri/aiimage/modules/pricetrack/model/vo/PriceTrackResultItemVo.java +++ b/backend-java/src/main/java/com/nanri/aiimage/modules/pricetrack/model/vo/PriceTrackResultItemVo.java @@ -1,23 +1,52 @@ package com.nanri.aiimage.modules.pricetrack.model.vo; +import io.swagger.v3.oas.annotations.media.Schema; import lombok.Data; import lombok.experimental.Accessors; @Data @Accessors(chain = true) +@Schema(description = "单店结果项:列表接口与任务详情共用,对应 biz_file_result 一行以及请求衍生字段") public class PriceTrackResultItemVo { + @Schema(description = "结果行主键 biz_file_result.id", example = "1001") private Long resultId; + + @Schema(description = "所属任务主键 biz_file_task.id", example = "200") private Long taskId; + + @Schema(description = "店铺名称,详情页会尽量恢复创建任务时的展示名") private String shopName; + + @Schema(description = "店铺 ID(紫鸟)") private Object shopId; + + @Schema(description = "平台") private String platform; + + @Schema(description = "公司名称") private String companyName; + + @Schema(description = "创建任务时是否已匹配") private Boolean matched; + + @Schema(description = "匹配状态码,含义同 PriceTrackShopQueueItem.matchStatus") private String matchStatus; + + @Schema(description = "匹配说明") private String matchMessage; + + @Schema(description = "所属任务当前状态:RUNNING、SUCCESS、FAILED") private String taskStatus; + + @Schema(description = "该店铺是否成功生成结果文件;null 或 false 可视为未完成或失败") private Boolean success; + + @Schema(description = "失败时的错误信息") private String error; + + @Schema(description = "生成的 xlsx 文件名") private String outputFilename; + + @Schema(description = "预签名下载 URL,列表可能带回,也可通过下载接口获取") private String downloadUrl; } diff --git a/backend-java/src/main/java/com/nanri/aiimage/modules/pricetrack/model/vo/PriceTrackTaskBatchVo.java b/backend-java/src/main/java/com/nanri/aiimage/modules/pricetrack/model/vo/PriceTrackTaskBatchVo.java new file mode 100644 index 0000000..5260fee --- /dev/null +++ b/backend-java/src/main/java/com/nanri/aiimage/modules/pricetrack/model/vo/PriceTrackTaskBatchVo.java @@ -0,0 +1,17 @@ +package com.nanri.aiimage.modules.pricetrack.model.vo; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; + +import java.util.ArrayList; +import java.util.List; + +@Data +@Schema(description = "批量任务查询响应:每个 taskId 返回一条详情;无效 id 放到 missingTaskIds") +public class PriceTrackTaskBatchVo { + @Schema(description = "存在且属于跟价模块的任务详情列表") + private List items = new ArrayList<>(); + + @Schema(description = "请求中不存在或不属于跟价模块的 taskId 列表") + private List missingTaskIds = new ArrayList<>(); +} diff --git a/backend-java/src/main/java/com/nanri/aiimage/modules/pricetrack/model/vo/PriceTrackTaskDetailVo.java b/backend-java/src/main/java/com/nanri/aiimage/modules/pricetrack/model/vo/PriceTrackTaskDetailVo.java new file mode 100644 index 0000000..71a11d8 --- /dev/null +++ b/backend-java/src/main/java/com/nanri/aiimage/modules/pricetrack/model/vo/PriceTrackTaskDetailVo.java @@ -0,0 +1,17 @@ +package com.nanri.aiimage.modules.pricetrack.model.vo; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; + +import java.util.ArrayList; +import java.util.List; + +@Data +@Schema(description = "单个任务详情:任务级概要和下属店铺结果列表") +public class PriceTrackTaskDetailVo { + @Schema(description = "任务级字段,例如状态、错误信息、时间等") + private PriceTrackTaskItemVo task = new PriceTrackTaskItemVo(); + + @Schema(description = "该任务下每个店铺对应一条结果记录") + private List items = new ArrayList<>(); +} diff --git a/backend-java/src/main/java/com/nanri/aiimage/modules/pricetrack/model/vo/PriceTrackTaskItemVo.java b/backend-java/src/main/java/com/nanri/aiimage/modules/pricetrack/model/vo/PriceTrackTaskItemVo.java new file mode 100644 index 0000000..80c86c8 --- /dev/null +++ b/backend-java/src/main/java/com/nanri/aiimage/modules/pricetrack/model/vo/PriceTrackTaskItemVo.java @@ -0,0 +1,29 @@ +package com.nanri.aiimage.modules.pricetrack.model.vo; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; + +@Data +@Schema(description = "跟价任务概要") +public class PriceTrackTaskItemVo { + @Schema(description = "任务主键", example = "200") + private Long id; + + @Schema(description = "任务编号") + private String taskNo; + + @Schema(description = "任务状态:RUNNING、SUCCESS、FAILED") + private String status; + + @Schema(description = "任务错误信息;部分成功或失败时可能有值") + private String errorMessage; + + @Schema(description = "创建时间") + private String createdAt; + + @Schema(description = "更新时间") + private String updatedAt; + + @Schema(description = "完成时间;未结束时为空") + private String finishedAt; +} diff --git a/backend-java/src/main/java/com/nanri/aiimage/modules/pricetrack/service/PriceTrackExcelAssemblyService.java b/backend-java/src/main/java/com/nanri/aiimage/modules/pricetrack/service/PriceTrackExcelAssemblyService.java new file mode 100644 index 0000000..9fc5029 --- /dev/null +++ b/backend-java/src/main/java/com/nanri/aiimage/modules/pricetrack/service/PriceTrackExcelAssemblyService.java @@ -0,0 +1,85 @@ +package com.nanri.aiimage.modules.pricetrack.service; + +import com.nanri.aiimage.common.exception.BusinessException; +import com.nanri.aiimage.modules.pricetrack.model.dto.PriceTrackSubmitResultRequest; +import com.nanri.aiimage.modules.productrisk.model.enums.ProductRiskCountryCode; +import lombok.extern.slf4j.Slf4j; +import org.apache.poi.ss.usermodel.Row; +import org.apache.poi.ss.usermodel.Sheet; +import org.apache.poi.xssf.usermodel.XSSFWorkbook; +import org.springframework.stereotype.Service; + +import java.io.File; +import java.io.FileOutputStream; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; + +@Service +@Slf4j +public class PriceTrackExcelAssemblyService { + + private static final String[] HEADER = {"ASIN", "当前店铺价格", "竞争对手价格", "建议动作", "是否完成"}; + + public void writeWorkbook(File outputXlsx, Map> countries) { + Map> safe = countries == null ? Map.of() : countries; + try (XSSFWorkbook wb = new XSSFWorkbook(); FileOutputStream fos = new FileOutputStream(outputXlsx)) { + for (ProductRiskCountryCode code : ProductRiskCountryCode.values()) { + String sheetName = code.name(); + Sheet sheet = wb.createSheet(sheetName); + Row headerRow = sheet.createRow(0); + for (int i = 0; i < HEADER.length; i++) { + headerRow.createCell(i).setCellValue(HEADER[i]); + } + List rows = safe.getOrDefault(sheetName, List.of()); + int rowIdx = 1; + for (PriceTrackSubmitResultRequest.AsinResult item : rows) { + if (item == null) { + continue; + } + Row row = sheet.createRow(rowIdx++); + row.createCell(0).setCellValue(item.getAsin() == null ? "" : item.getAsin()); + row.createCell(1).setCellValue(item.getCurrentPrice() == null ? "" : item.getCurrentPrice()); + row.createCell(2).setCellValue(item.getCompetitorPrice() == null ? "" : item.getCompetitorPrice()); + row.createCell(3).setCellValue(item.getAction() == null ? "" : item.getAction()); + row.createCell(4).setCellValue(item.getDone() == null ? "" : String.valueOf(item.getDone())); + } + for (int i = 0; i < HEADER.length; i++) { + sheet.autoSizeColumn(i); + } + } + wb.write(fos); + } catch (Exception ex) { + log.warn("[price-track] write workbook failed: {}", ex.getMessage()); + throw new BusinessException("生成 Excel 失败: " + ex.getMessage()); + } + } + + public int countRows(Map> countries) { + if (countries == null || countries.isEmpty()) { + return 0; + } + int count = 0; + for (List rows : countries.values()) { + if (rows != null) { + count += rows.size(); + } + } + return count; + } + + public Map> normalizeCountriesMap( + Map> raw) { + if (raw == null || raw.isEmpty()) { + return new LinkedHashMap<>(); + } + Map> out = new LinkedHashMap<>(); + for (ProductRiskCountryCode code : ProductRiskCountryCode.values()) { + List rows = raw.get(code.name()); + if (rows != null) { + out.put(code.name(), rows); + } + } + return out; + } +} diff --git a/backend-java/src/main/java/com/nanri/aiimage/modules/pricetrack/service/PriceTrackService.java b/backend-java/src/main/java/com/nanri/aiimage/modules/pricetrack/service/PriceTrackService.java index 730fca8..d9876b7 100644 --- a/backend-java/src/main/java/com/nanri/aiimage/modules/pricetrack/service/PriceTrackService.java +++ b/backend-java/src/main/java/com/nanri/aiimage/modules/pricetrack/service/PriceTrackService.java @@ -14,7 +14,6 @@ import com.nanri.aiimage.modules.pricetrack.model.entity.PriceTrackShopCandidate import com.nanri.aiimage.modules.pricetrack.model.vo.PriceTrackCandidateVo; import com.nanri.aiimage.modules.pricetrack.model.vo.PriceTrackCountryPreferenceVo; import com.nanri.aiimage.modules.pricetrack.model.vo.PriceTrackMatchShopsVo; -import com.nanri.aiimage.modules.pricetrack.model.vo.PriceTrackShopQueueItem; import com.nanri.aiimage.modules.shopkey.service.SkipPriceAsinService; import com.nanri.aiimage.modules.ziniao.model.vo.ZiniaoShopMatchResultVo; import com.nanri.aiimage.modules.ziniao.service.ZiniaoShopIndexService; @@ -52,7 +51,9 @@ public class PriceTrackService { .orderByDesc(PriceTrackShopCandidateEntity::getId)); List list = new ArrayList<>(); for (PriceTrackShopCandidateEntity row : rows) { - if (row == null) continue; + if (row == null) { + continue; + } PriceTrackCandidateVo vo = new PriceTrackCandidateVo(); vo.setId(row.getId()); vo.setShopName(row.getShopName()); @@ -68,20 +69,22 @@ public class PriceTrackService { } String normalized = ziniaoShopSwitchService.normalizeShopName(request.getShopName()); if (normalized.isBlank()) { - throw new BusinessException("店铺名不能为空"); + throw new BusinessException("店铺名称不能为空"); } - // 店铺需在紫鸟索引中命中才能加入备选 + ZiniaoShopMatchResultVo indexHit = ziniaoShopSwitchService.findIndexedStoreByName(normalized, false); if (indexHit == null || !indexHit.isMatched()) { - String hint = (indexHit != null && indexHit.getMatchMessage() != null) - ? indexHit.getMatchMessage() : "店铺索引未命中,无法加入备选"; + String hint = indexHit != null && indexHit.getMatchMessage() != null + ? indexHit.getMatchMessage() + : "店铺未命中紫鸟索引,无法加入备选列表"; throw new BusinessException(hint); } if (ZiniaoShopIndexService.MATCH_STATUS_CONFLICT.equals(indexHit.getMatchStatus())) { throw new BusinessException(indexHit.getMatchMessage() != null - ? indexHit.getMatchMessage() : "存在多个同名店铺,请人工确认"); + ? indexHit.getMatchMessage() + : "存在多个同名店铺,请先人工确认"); } - // 查重 + PriceTrackShopCandidateEntity existing = candidateMapper.selectOne( new LambdaQueryWrapper() .eq(PriceTrackShopCandidateEntity::getUserId, request.getUserId()) @@ -93,11 +96,13 @@ public class PriceTrackService { vo.setShopName(existing.getShopName()); return vo; } + PriceTrackShopCandidateEntity entity = new PriceTrackShopCandidateEntity(); entity.setUserId(request.getUserId()); entity.setShopName(normalized); entity.setCreatedAt(LocalDateTime.now()); candidateMapper.insert(entity); + PriceTrackCandidateVo vo = new PriceTrackCandidateVo(); vo.setId(entity.getId()); vo.setShopName(entity.getShopName()); @@ -106,8 +111,12 @@ public class PriceTrackService { @Transactional public void deleteCandidate(Long userId, Long id) { - if (userId == null || userId <= 0) throw new BusinessException("user_id 不合法"); - if (id == null || id <= 0) throw new BusinessException("id 不合法"); + if (userId == null || userId <= 0) { + throw new BusinessException("user_id 不合法"); + } + if (id == null || id <= 0) { + throw new BusinessException("id 不合法"); + } PriceTrackShopCandidateEntity row = candidateMapper.selectById(id); if (row == null || !userId.equals(row.getUserId())) { throw new BusinessException("记录不存在"); @@ -121,12 +130,15 @@ public class PriceTrackService { } LinkedHashSet ordered = new LinkedHashSet<>(); for (String raw : request.getShopNames()) { - String n = ziniaoShopSwitchService.normalizeShopName(raw); - if (!n.isBlank()) ordered.add(n); + String normalized = ziniaoShopSwitchService.normalizeShopName(raw); + if (!normalized.isBlank()) { + ordered.add(normalized); + } } if (ordered.isEmpty()) { - throw new BusinessException("shop_names 无有效店铺名"); + throw new BusinessException("shop_names 没有有效店铺名"); } + PriceTrackMatchShopsVo vo = new PriceTrackMatchShopsVo(); for (String shopName : ordered) { vo.getItems().add(matchOneShop(shopName)); @@ -135,7 +147,9 @@ public class PriceTrackService { } public PriceTrackCountryPreferenceVo getCountryPreference(Long userId) { - if (userId == null || userId <= 0) throw new BusinessException("user_id 不合法"); + if (userId == null || userId <= 0) { + throw new BusinessException("user_id 不合法"); + } PriceTrackCountryPrefEntity row = countryPrefMapper.selectById(userId); PriceTrackCountryPreferenceVo vo = new PriceTrackCountryPreferenceVo(); if (row == null || row.getCountryCodesJson() == null || row.getCountryCodesJson().isBlank()) { @@ -156,14 +170,17 @@ public class PriceTrackService { @Transactional public PriceTrackCountryPreferenceVo saveCountryPreference(PriceTrackCountryPreferenceSaveRequest request) { - if (request.getUserId() == null || request.getUserId() <= 0) throw new BusinessException("user_id 不合法"); + if (request.getUserId() == null || request.getUserId() <= 0) { + throw new BusinessException("user_id 不合法"); + } List normalized = validateCountryCodesForSave(request.getCountryCodes()); String json; try { json = objectMapper.writeValueAsString(normalized); } catch (Exception ex) { - throw new BusinessException("保存偏好失败"); + throw new BusinessException("保存国家偏好失败"); } + PriceTrackCountryPrefEntity row = countryPrefMapper.selectById(request.getUserId()); if (row == null) { row = new PriceTrackCountryPrefEntity(); @@ -174,29 +191,30 @@ public class PriceTrackService { row.setCountryCodesJson(json); countryPrefMapper.updateById(row); } + PriceTrackCountryPreferenceVo vo = new PriceTrackCountryPreferenceVo(); vo.setUserId(request.getUserId()); vo.setCountryCodes(normalized); return vo; } - private PriceTrackShopQueueItem matchOneShop(String shopName) { - PriceTrackShopQueueItem item = new PriceTrackShopQueueItem(); + private PriceTrackMatchShopsVo.PriceTrackShopQueueItem matchOneShop(String shopName) { + PriceTrackMatchShopsVo.PriceTrackShopQueueItem item = new PriceTrackMatchShopsVo.PriceTrackShopQueueItem(); item.setShopName(shopName); try { - ZiniaoShopMatchResultVo m = ziniaoShopSwitchService.findIndexedStoreByName(shopName, false); - if (m == null) { + ZiniaoShopMatchResultVo match = ziniaoShopSwitchService.findIndexedStoreByName(shopName, false); + if (match == null) { item.setMatched(false); item.setMatchStatus("PENDING"); - item.setMatchMessage("匹配结果为空"); + item.setMatchMessage("未查询到匹配结果"); return item; } - item.setMatched(m.isMatched()); - item.setShopId(m.getShopId()); - item.setPlatform(m.getPlatform()); - item.setCompanyName(m.getCompanyName()); - item.setMatchStatus(m.getMatchStatus()); - item.setMatchMessage(m.getMatchMessage()); + item.setMatched(match.isMatched()); + item.setShopId(match.getShopId()); + item.setPlatform(match.getPlatform()); + item.setCompanyName(match.getCompanyName()); + item.setMatchStatus(match.getMatchStatus()); + item.setMatchMessage(match.getMatchMessage()); } catch (BusinessException ex) { item.setMatched(false); item.setMatchStatus("PENDING"); @@ -211,34 +229,54 @@ public class PriceTrackService { private static List sanitizeStoredCodes(List raw) { List parsed = parseValidCountryCodes(raw); - if (parsed.isEmpty()) return new ArrayList<>(DEFAULT_COUNTRY_PREFERENCE_ORDER); + if (parsed.isEmpty()) { + return new ArrayList<>(DEFAULT_COUNTRY_PREFERENCE_ORDER); + } return parsed; } private static List validateCountryCodesForSave(List raw) { - if (raw == null || raw.isEmpty()) throw new BusinessException("country_codes 至少选择 1 个国家"); + if (raw == null || raw.isEmpty()) { + throw new BusinessException("country_codes 至少选择 1 个国家"); + } LinkedHashSet seen = new LinkedHashSet<>(); List out = new ArrayList<>(); - for (String s : raw) { - if (s == null || s.isBlank()) throw new BusinessException("country_codes 含空项"); - String u = s.trim().toUpperCase(); - if (!SUPPORTED_COUNTRIES.contains(u)) throw new BusinessException("非法国家代码: " + s); - if (!seen.add(u)) throw new BusinessException("country_codes 存在重复: " + u); - out.add(u); + for (String code : raw) { + if (code == null || code.isBlank()) { + throw new BusinessException("country_codes 不能包含空值"); + } + String normalized = code.trim().toUpperCase(); + if (!SUPPORTED_COUNTRIES.contains(normalized)) { + throw new BusinessException("非法国家代码: " + code); + } + if (!seen.add(normalized)) { + throw new BusinessException("country_codes 存在重复值: " + normalized); + } + out.add(normalized); + } + if (out.isEmpty()) { + throw new BusinessException("country_codes 至少选择 1 个国家"); } - if (out.isEmpty()) throw new BusinessException("country_codes 至少选择 1 个国家"); return out; } private static List parseValidCountryCodes(List raw) { - if (raw == null || raw.isEmpty()) return new ArrayList<>(); + if (raw == null || raw.isEmpty()) { + return new ArrayList<>(); + } LinkedHashSet seen = new LinkedHashSet<>(); List out = new ArrayList<>(); - for (String s : raw) { - if (s == null || s.isBlank()) continue; - String u = s.trim().toUpperCase(); - if (!SUPPORTED_COUNTRIES.contains(u)) continue; - if (seen.add(u)) out.add(u); + for (String code : raw) { + if (code == null || code.isBlank()) { + continue; + } + String normalized = code.trim().toUpperCase(); + if (!SUPPORTED_COUNTRIES.contains(normalized)) { + continue; + } + if (seen.add(normalized)) { + out.add(normalized); + } } return out; } diff --git a/backend-java/src/main/java/com/nanri/aiimage/modules/pricetrack/service/PriceTrackTaskCacheService.java b/backend-java/src/main/java/com/nanri/aiimage/modules/pricetrack/service/PriceTrackTaskCacheService.java new file mode 100644 index 0000000..5116aea --- /dev/null +++ b/backend-java/src/main/java/com/nanri/aiimage/modules/pricetrack/service/PriceTrackTaskCacheService.java @@ -0,0 +1,53 @@ +package com.nanri.aiimage.modules.pricetrack.service; + +import lombok.RequiredArgsConstructor; +import org.springframework.data.redis.core.StringRedisTemplate; +import org.springframework.stereotype.Service; + +import java.time.Duration; +import java.time.Instant; + +@Service +@RequiredArgsConstructor +public class PriceTrackTaskCacheService { + + private static final long HEARTBEAT_TTL_HOURS = 24; + + private final StringRedisTemplate stringRedisTemplate; + + public void touchTaskHeartbeat(Long taskId) { + if (taskId == null || taskId <= 0) { + return; + } + stringRedisTemplate.opsForValue().set( + buildTaskHeartbeatKey(taskId), + String.valueOf(Instant.now().toEpochMilli()), + Duration.ofHours(HEARTBEAT_TTL_HOURS)); + } + + public long getTaskHeartbeatMillis(Long taskId) { + if (taskId == null || taskId <= 0) { + return 0L; + } + String raw = stringRedisTemplate.opsForValue().get(buildTaskHeartbeatKey(taskId)); + if (raw == null || raw.isBlank()) { + return 0L; + } + try { + return Long.parseLong(raw); + } catch (NumberFormatException ignored) { + return 0L; + } + } + + public void deleteTaskCache(Long taskId) { + if (taskId == null || taskId <= 0) { + return; + } + stringRedisTemplate.delete(buildTaskHeartbeatKey(taskId)); + } + + private String buildTaskHeartbeatKey(Long taskId) { + return "price-track:task:heartbeat:" + taskId; + } +} diff --git a/backend-java/src/main/java/com/nanri/aiimage/modules/pricetrack/service/PriceTrackTaskService.java b/backend-java/src/main/java/com/nanri/aiimage/modules/pricetrack/service/PriceTrackTaskService.java index a3a4e98..da04c7e 100644 --- a/backend-java/src/main/java/com/nanri/aiimage/modules/pricetrack/service/PriceTrackTaskService.java +++ b/backend-java/src/main/java/com/nanri/aiimage/modules/pricetrack/service/PriceTrackTaskService.java @@ -1,6 +1,7 @@ package com.nanri.aiimage.modules.pricetrack.service; import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper; +import com.fasterxml.jackson.core.type.TypeReference; import com.fasterxml.jackson.databind.ObjectMapper; import com.nanri.aiimage.common.exception.BusinessException; import com.nanri.aiimage.modules.file.service.oss.OssStorageService; @@ -11,28 +12,28 @@ import com.nanri.aiimage.modules.pricetrack.model.entity.PriceTrackShopCandidate import com.nanri.aiimage.modules.pricetrack.model.vo.PriceTrackCreateTaskVo; import com.nanri.aiimage.modules.pricetrack.model.vo.PriceTrackDashboardVo; import com.nanri.aiimage.modules.pricetrack.model.vo.PriceTrackHistoryVo; +import com.nanri.aiimage.modules.pricetrack.model.vo.PriceTrackMatchShopsVo; +import com.nanri.aiimage.modules.pricetrack.model.vo.PriceTrackPendingDeleteVo; import com.nanri.aiimage.modules.pricetrack.model.vo.PriceTrackResultItemVo; -import com.nanri.aiimage.modules.pricetrack.model.vo.PriceTrackShopQueueItem; +import com.nanri.aiimage.modules.pricetrack.model.vo.PriceTrackTaskBatchVo; +import com.nanri.aiimage.modules.pricetrack.model.vo.PriceTrackTaskDetailVo; +import com.nanri.aiimage.modules.pricetrack.model.vo.PriceTrackTaskItemVo; import com.nanri.aiimage.modules.shopkey.service.SkipPriceAsinService; import com.nanri.aiimage.modules.task.mapper.FileResultMapper; import com.nanri.aiimage.modules.task.mapper.FileTaskMapper; import com.nanri.aiimage.modules.task.model.entity.FileResultEntity; import com.nanri.aiimage.modules.task.model.entity.FileTaskEntity; import com.nanri.aiimage.modules.ziniao.service.ZiniaoShopSwitchService; -import cn.hutool.core.io.FileUtil; import cn.hutool.core.util.IdUtil; -import cn.hutool.core.util.ZipUtil; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; -import java.io.File; import java.time.LocalDateTime; import java.util.ArrayList; import java.util.LinkedHashMap; import java.util.List; -import java.util.Locale; import java.util.Map; import java.util.Objects; @@ -42,7 +43,7 @@ import java.util.Objects; public class PriceTrackTaskService { private static final String MODULE_TYPE = "PRICE_TRACK"; - private static final String CONTENT_TYPE_ZIP = "application/zip"; + private static final String CONTENT_TYPE_XLSX = "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet"; private final FileTaskMapper fileTaskMapper; private final FileResultMapper fileResultMapper; @@ -51,6 +52,8 @@ public class PriceTrackTaskService { private final OssStorageService ossStorageService; private final ObjectMapper objectMapper; private final SkipPriceAsinService skipPriceAsinService; + private final PriceTrackExcelAssemblyService excelAssemblyService; + private final PriceTrackTaskCacheService priceTrackTaskCacheService; public PriceTrackDashboardVo dashboard(Long userId) { if (userId == null || userId <= 0) throw new BusinessException("user_id 不合法"); @@ -129,6 +132,51 @@ public class PriceTrackTaskService { .eq(FileResultEntity::getTaskId, taskId) .eq(FileResultEntity::getModuleType, MODULE_TYPE)); fileTaskMapper.deleteById(taskId); + priceTrackTaskCacheService.deleteTaskCache(taskId); + } + + @Transactional + public PriceTrackPendingDeleteVo deletePendingShopResult(Long userId, String shopName) { + if (userId == null || userId <= 0) throw new BusinessException("user_id 不合法"); + if (shopName == null || shopName.isBlank()) throw new BusinessException("shop_name 涓嶈兘涓虹┖"); + String norm = ziniaoShopSwitchService.normalizeShopName(shopName); + if (norm.isBlank()) throw new BusinessException("店铺名称无效"); + + List candidates = fileResultMapper.selectList(new LambdaQueryWrapper() + .eq(FileResultEntity::getUserId, userId) + .eq(FileResultEntity::getModuleType, MODULE_TYPE) + .eq(FileResultEntity::getSourceFilename, norm) + .orderByDesc(FileResultEntity::getId) + .last("limit 30")); + + PriceTrackPendingDeleteVo vo = new PriceTrackPendingDeleteVo(); + vo.setRemoved(false); + for (FileResultEntity fr : candidates) { + if (fr.getTaskId() == null) continue; + FileTaskEntity task = fileTaskMapper.selectById(fr.getTaskId()); + if (task == null || !MODULE_TYPE.equals(task.getModuleType()) || !userId.equals(task.getUserId())) continue; + if (!"RUNNING".equals(task.getStatus())) continue; + fileResultMapper.deleteById(fr.getId()); + reconcileTaskAfterResultRemoval(fr.getTaskId()); + vo.setRemoved(true); + return vo; + } + return vo; + } + + public PriceTrackTaskBatchVo getTaskDetailsBatch(List taskIds) { + PriceTrackTaskBatchVo batch = new PriceTrackTaskBatchVo(); + if (taskIds == null || taskIds.isEmpty()) return batch; + for (Long taskId : taskIds) { + if (taskId == null || taskId <= 0) continue; + FileTaskEntity task = fileTaskMapper.selectById(taskId); + if (task == null || !MODULE_TYPE.equals(task.getModuleType())) { + batch.getMissingTaskIds().add(taskId); + continue; + } + batch.getItems().add(buildTaskDetail(task)); + } + return batch; } @Transactional @@ -137,18 +185,18 @@ public class PriceTrackTaskService { if (request.getItems() == null || request.getItems().isEmpty()) throw new BusinessException("items 不能为空"); // 去重并校验匹配状态 - List uniqueItems = dedupeQueueItems(request.getItems()); + List uniqueItems = dedupeQueueItems(request.getItems()); if (uniqueItems.isEmpty()) throw new BusinessException("items 不能为空"); - for (PriceTrackShopQueueItem item : uniqueItems) { + for (PriceTrackMatchShopsVo.PriceTrackShopQueueItem item : uniqueItems) { if (item == null || !Boolean.TRUE.equals(item.getMatched())) { String name = item == null ? "" : Objects.toString(item.getShopName(), ""); throw new BusinessException("存在未匹配店铺,无法创建任务: " + name); } } - // 收集店铺名,查询跳过ASIN + // 汇总店铺名并查询需要跳过的 ASIN List shopNames = uniqueItems.stream() - .map(PriceTrackShopQueueItem::getShopName) + .map(PriceTrackMatchShopsVo.PriceTrackShopQueueItem::getShopName) .filter(s -> s != null && !s.isBlank()) .distinct() .toList(); @@ -169,11 +217,12 @@ public class PriceTrackTaskService { task.setCreatedAt(LocalDateTime.now()); task.setUpdatedAt(LocalDateTime.now()); fileTaskMapper.insert(task); + priceTrackTaskCacheService.touchTaskHeartbeat(task.getId()); List snapshot = new ArrayList<>(); - for (PriceTrackShopQueueItem item : uniqueItems) { + for (PriceTrackMatchShopsVo.PriceTrackShopQueueItem item : uniqueItems) { String norm = ziniaoShopSwitchService.normalizeShopName(item.getShopName()); - if (norm.isBlank()) throw new BusinessException("店铺名无效"); + if (norm.isBlank()) throw new BusinessException("店铺名称无效"); FileResultEntity fr = new FileResultEntity(); fr.setTaskId(task.getId()); fr.setModuleType(MODULE_TYPE); @@ -186,16 +235,13 @@ public class PriceTrackTaskService { snapshot.add(toSnapshotVo(fr, item, task.getStatus())); } - // 保存请求上下文(含skipASIN)用于Python后续查询 + // 保存请求上下文快照,供 Python 消费和前端轮询展示使用 try { Map ctx = new LinkedHashMap<>(); ctx.put("userId", request.getUserId()); ctx.put("statusMode", request.isStatusMode()); ctx.put("asinMode", request.isAsinMode()); ctx.put("countryCodes", request.getCountryCodes()); - ctx.put("minPrice", request.getMinPrice()); - ctx.put("maxPrice", request.getMaxPrice()); - ctx.put("priceDiffPercent", request.getPriceDiffPercent()); ctx.put("skipAsinsByShop", skipAsinsByShop); ctx.put("items", uniqueItems); task.setRequestJson(objectMapper.writeValueAsString(ctx)); @@ -205,8 +251,8 @@ public class PriceTrackTaskService { } fileTaskMapper.updateById(task); - // TODO: Python侧定时轮询 RUNNING 状态任务, pickup 后开始处理 - // Java侧通过 submitResult 接口接收Python回传结果 + // TODO: Python 侧轮询 RUNNING 任务,pickup 后开始处理 + // Java 侧通过 submitResult 接口接收 Python 回传结果 PriceTrackCreateTaskVo vo = new PriceTrackCreateTaskVo(); vo.setTaskId(task.getId()); @@ -229,6 +275,7 @@ public class PriceTrackTaskService { throw new BusinessException("任务已结束,拒绝重复提交"); } + priceTrackTaskCacheService.touchTaskHeartbeat(taskId); List resultRows = fileResultMapper.selectList( new LambdaQueryWrapper() .eq(FileResultEntity::getTaskId, taskId) @@ -242,38 +289,26 @@ public class PriceTrackTaskService { if (!key.isBlank()) payloadByShop.put(key, sr); } - int ok = 0, fail = 0; - List allErrors = new ArrayList<>(); - boolean allDone = true; - for (FileResultEntity fr : resultRows) { String shopKey = fr.getSourceFilename(); PriceTrackSubmitResultRequest.ShopResult payload = shopKey == null ? null : payloadByShop.get(shopKey); if (payload == null) { - // 未匹配到payload的店铺保持待处理 - allDone = false; + // 当前批次没有该店铺的 payload,保持待处理 continue; } if (payload.getError() != null && !payload.getError().isBlank()) { markResultFailed(fr, payload.getError()); - fail++; - allErrors.add(shopKey + ": " + payload.getError()); continue; } if (Boolean.FALSE.equals(payload.getSuccess())) { - markResultFailed(fr, "处理失败"); - fail++; - allErrors.add(shopKey + ": 处理失败"); + markResultFailed(fr, "店铺处理失败"); continue; } - // 成功 - fr.setSuccess(1); - fr.setErrorMessage(null); - fr.setResultFilename(shopKey + "-price-track.xlsx"); - fr.setResultFileSize(0L); - fr.setRowCount(countAsinResults(payload)); - fileResultMapper.updateById(fr); - ok++; + try { + assembleShopResult(fr, shopKey, payload); + } catch (Exception ex) { + markResultFailed(fr, ex.getMessage() == null ? "组装结果失败" : ex.getMessage()); + } } List latest = fileResultMapper.selectList( @@ -282,33 +317,57 @@ public class PriceTrackTaskService { .eq(FileResultEntity::getModuleType, MODULE_TYPE) .orderByAsc(FileResultEntity::getId)); - allDone = latest.stream().noneMatch(fr -> - fr.getSuccess() == null || fr.getSuccess() == 0); + updateTaskStatusFromLatestRows(task, latest); + } - task.setSuccessFileCount(ok); - task.setFailedFileCount(fail); - task.setUpdatedAt(LocalDateTime.now()); + @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 (!"RUNNING".equals(task.getStatus())) { + log.warn("[price-track] stale finalize skipped taskId={} status={} fromCompensation={}", + taskId, task.getStatus(), fromCompensation); + cleanupTaskCacheIfTerminal(taskId, task.getStatus()); + return false; + } - if (!allDone) { - task.setStatus("RUNNING"); - } else if (ok > 0 && fail == 0) { - task.setStatus("SUCCESS"); - task.setFinishedAt(LocalDateTime.now()); - } else if (ok > 0) { - task.setStatus("SUCCESS"); - task.setErrorMessage(String.join(";", allErrors)); - task.setFinishedAt(LocalDateTime.now()); - } else { + List latest = fileResultMapper.selectList( + new LambdaQueryWrapper() + .eq(FileResultEntity::getTaskId, taskId) + .eq(FileResultEntity::getModuleType, MODULE_TYPE) + .orderByAsc(FileResultEntity::getId)); + if (latest.isEmpty()) { task.setStatus("FAILED"); - task.setErrorMessage(String.join(";", allErrors)); + task.setErrorMessage("任务明细缺失,无法自动收尾"); + task.setUpdatedAt(LocalDateTime.now()); task.setFinishedAt(LocalDateTime.now()); + fileTaskMapper.updateById(task); + cleanupTaskCacheIfTerminal(taskId, task.getStatus()); + return true; } - try { - task.setResultJson(objectMapper.writeValueAsString(buildSnapshotFromDb(taskId, task.getStatus()))); - } catch (Exception ex) { - log.warn("[price-track] compact result json failed: {}", ex.getMessage()); + + boolean changed = false; + for (FileResultEntity fr : latest) { + boolean success = fr.getSuccess() != null && fr.getSuccess() == 1; + boolean failed = fr.getErrorMessage() != null && !fr.getErrorMessage().isBlank(); + if (success || failed) continue; + markResultFailed(fr, "Python interrupted before this shop finished uploading results"); + changed = true; + log.warn("[price-track] stale finalize failed taskId={} shop={} fromCompensation={}", + taskId, fr.getSourceFilename(), fromCompensation); } - fileTaskMapper.updateById(task); + if (!changed) { + return false; + } + + List refreshed = fileResultMapper.selectList( + new LambdaQueryWrapper() + .eq(FileResultEntity::getTaskId, taskId) + .eq(FileResultEntity::getModuleType, MODULE_TYPE) + .orderByAsc(FileResultEntity::getId)); + updateTaskStatusFromLatestRows(task, refreshed); + return true; } public String resolveResultDownloadUrl(Long resultId, Long userId) { @@ -345,21 +404,10 @@ public class PriceTrackTaskService { .orderByAsc(FileResultEntity::getId)); if (latest.isEmpty()) { fileTaskMapper.deleteById(taskId); + cleanupTaskCacheIfTerminal(taskId, "DELETE_EMPTY"); return; } - int ok = 0, fail = 0; - for (FileResultEntity fr : latest) { - if (Boolean.TRUE.equals(fr.getSuccess())) ok++; - else if (fr.getErrorMessage() != null && !fr.getErrorMessage().isBlank()) fail++; - } - task.setSuccessFileCount(ok); - task.setFailedFileCount(fail); - task.setUpdatedAt(LocalDateTime.now()); - boolean allDone = latest.stream().noneMatch(fr -> fr.getSuccess() == null || fr.getSuccess() == 0); - if (!allDone) return; - task.setStatus(ok > 0 ? "SUCCESS" : "FAILED"); - task.setFinishedAt(LocalDateTime.now()); - fileTaskMapper.updateById(task); + updateTaskStatusFromLatestRows(task, latest); } private void markResultFailed(FileResultEntity fr, String message) { @@ -369,12 +417,44 @@ public class PriceTrackTaskService { fr.setResultFileUrl(null); fr.setResultFileSize(0L); fr.setRowCount(0); + fr.setResultContentType(null); fileResultMapper.updateById(fr); } - private List dedupeQueueItems(List raw) { - Map byNorm = new LinkedHashMap<>(); - for (PriceTrackShopQueueItem item : raw) { + private void assembleShopResult(FileResultEntity result, + String shopKey, + PriceTrackSubmitResultRequest.ShopResult payload) { + Map> countries = + excelAssemblyService.normalizeCountriesMap(payload.getCountries()); + if (excelAssemblyService.countRows(countries) <= 0) { + throw new BusinessException("未收到可用结果数据"); + } + String displayName = payload.getShopName() != null && !payload.getShopName().isBlank() + ? payload.getShopName().trim() + : shopKey; + String stem = safeFileStem(displayName); + java.io.File workRoot = cn.hutool.core.io.FileUtil.mkdir( + cn.hutool.core.io.FileUtil.file(System.getProperty("java.io.tmpdir"), "price-track-result", String.valueOf(result.getTaskId()))); + java.io.File xlsx = cn.hutool.core.io.FileUtil.file(workRoot, stem + ".xlsx"); + try { + excelAssemblyService.writeWorkbook(xlsx, countries); + String objectKey = ossStorageService.uploadResultFile(xlsx, MODULE_TYPE); + result.setSuccess(1); + result.setErrorMessage(null); + result.setResultFilename(stem + ".xlsx"); + result.setResultFileUrl(objectKey); + result.setResultFileSize(xlsx.length()); + result.setResultContentType(CONTENT_TYPE_XLSX); + result.setRowCount(excelAssemblyService.countRows(countries)); + fileResultMapper.updateById(result); + } finally { + cn.hutool.core.io.FileUtil.del(xlsx); + } + } + + private List dedupeQueueItems(List raw) { + Map byNorm = new LinkedHashMap<>(); + for (PriceTrackMatchShopsVo.PriceTrackShopQueueItem item : raw) { if (item == null) continue; String k = ziniaoShopSwitchService.normalizeShopName(item.getShopName()); if (k.isBlank()) continue; @@ -383,7 +463,7 @@ public class PriceTrackTaskService { return new ArrayList<>(byNorm.values()); } - private PriceTrackResultItemVo toSnapshotVo(FileResultEntity fr, PriceTrackShopQueueItem item, String taskStatus) { + private PriceTrackResultItemVo toSnapshotVo(FileResultEntity fr, PriceTrackMatchShopsVo.PriceTrackShopQueueItem item, String taskStatus) { PriceTrackResultItemVo vo = new PriceTrackResultItemVo(); vo.setResultId(fr.getId()); vo.setTaskId(fr.getTaskId()); @@ -416,9 +496,65 @@ public class PriceTrackTaskService { vo.setDownloadUrl(entity.getResultFileUrl() == null || entity.getResultFileUrl().isBlank() ? null : ossStorageService.generateFreshDownloadUrl(entity.getResultFileUrl())); vo.setMatched(true); + if (entity.getTaskId() != null) { + mergeQueueFieldsFromRequest(vo, entity.getTaskId()); + } return vo; } + private PriceTrackTaskDetailVo buildTaskDetail(FileTaskEntity task) { + PriceTrackTaskDetailVo detail = new PriceTrackTaskDetailVo(); + detail.setTask(toTaskItemVo(task)); + List rows = fileResultMapper.selectList( + new LambdaQueryWrapper() + .eq(FileResultEntity::getTaskId, task.getId()) + .eq(FileResultEntity::getModuleType, MODULE_TYPE) + .orderByAsc(FileResultEntity::getId)); + for (FileResultEntity fr : rows) { + detail.getItems().add(toHistoryItemVo(fr, task.getStatus())); + } + return detail; + } + + private PriceTrackTaskItemVo toTaskItemVo(FileTaskEntity task) { + PriceTrackTaskItemVo vo = new PriceTrackTaskItemVo(); + vo.setId(task.getId()); + vo.setTaskNo(task.getTaskNo()); + vo.setStatus(task.getStatus()); + vo.setErrorMessage(task.getErrorMessage()); + vo.setCreatedAt(fmt(task.getCreatedAt())); + vo.setUpdatedAt(fmt(task.getUpdatedAt())); + vo.setFinishedAt(fmt(task.getFinishedAt())); + return vo; + } + + private void mergeQueueFieldsFromRequest(PriceTrackResultItemVo vo, Long taskId) { + FileTaskEntity task = fileTaskMapper.selectById(taskId); + if (task == null || task.getRequestJson() == null || task.getRequestJson().isBlank()) return; + try { + Map payload = objectMapper.readValue(task.getRequestJson(), new TypeReference>() {}); + Object rawItems = payload.get("items"); + if (rawItems == null) return; + List items = objectMapper.convertValue( + rawItems, + new TypeReference>() {}); + String key = ziniaoShopSwitchService.normalizeShopName(vo.getShopName()); + for (PriceTrackMatchShopsVo.PriceTrackShopQueueItem item : items) { + 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; + } + } + } catch (Exception ignored) { + } + } + private List buildSnapshotFromDb(Long taskId, String taskStatus) { List rows = fileResultMapper.selectList( new LambdaQueryWrapper() @@ -430,17 +566,70 @@ public class PriceTrackTaskService { return list; } - private int countAsinResults(PriceTrackSubmitResultRequest.ShopResult payload) { - if (payload == null || payload.getCountries() == null) return 0; - return payload.getCountries().values().stream() - .filter(l -> l != null) - .mapToInt(List::size) - .sum(); - } - private static String safeFileStem(String shopName) { String s = shopName == null ? "shop" : shopName.trim(); if (s.isBlank()) s = "shop"; return s.replaceAll("[\\\\/:*?\"<>|]", "_"); } + + private static String fmt(LocalDateTime t) { + return t == null ? null : t.toString(); + } + + private void updateTaskStatusFromLatestRows(FileTaskEntity task, List latest) { + String oldStatus = task.getStatus(); + int ok = 0; + int fail = 0; + List allErrors = new ArrayList<>(); + boolean allDone = true; + for (FileResultEntity fr : latest) { + boolean success = fr.getSuccess() != null && fr.getSuccess() == 1; + boolean failed = fr.getErrorMessage() != null && !fr.getErrorMessage().isBlank(); + if (success) { + ok++; + } else if (failed) { + fail++; + allErrors.add(fr.getSourceFilename() + ": " + fr.getErrorMessage()); + } else { + allDone = false; + } + } + task.setSourceFileCount(latest.size()); + task.setSuccessFileCount(ok); + task.setFailedFileCount(fail); + task.setUpdatedAt(LocalDateTime.now()); + if (!allDone) { + task.setStatus("RUNNING"); + task.setErrorMessage(null); + task.setFinishedAt(null); + } else if (ok > 0 && fail == 0) { + task.setStatus("SUCCESS"); + task.setErrorMessage(null); + task.setFinishedAt(LocalDateTime.now()); + } else if (ok > 0) { + task.setStatus("SUCCESS"); + task.setErrorMessage(String.join("; ", allErrors)); + task.setFinishedAt(LocalDateTime.now()); + } else { + task.setStatus("FAILED"); + task.setErrorMessage(allErrors.isEmpty() ? "全部店铺处理失败" : String.join("; ", allErrors)); + task.setFinishedAt(LocalDateTime.now()); + } + try { + task.setResultJson(objectMapper.writeValueAsString(buildSnapshotFromDb(task.getId(), task.getStatus()))); + } catch (Exception ex) { + log.warn("[price-track] compact result json failed: {}", ex.getMessage()); + } + fileTaskMapper.updateById(task); + log.warn("[price-track] status updated from latest rows taskId={} oldStatus={} newStatus={} ok={} fail={} allDone={} errorMessage={}", + task.getId(), oldStatus, task.getStatus(), ok, fail, allDone, task.getErrorMessage()); + cleanupTaskCacheIfTerminal(task.getId(), task.getStatus()); + } + + private void cleanupTaskCacheIfTerminal(Long taskId, String taskStatus) { + if (!"SUCCESS".equals(taskStatus) && !"FAILED".equals(taskStatus) && !"DELETE_EMPTY".equals(taskStatus)) { + return; + } + priceTrackTaskCacheService.deleteTaskCache(taskId); + } } diff --git a/backend-java/src/main/java/com/nanri/aiimage/modules/shopkey/controller/ShopManageController.java b/backend-java/src/main/java/com/nanri/aiimage/modules/shopkey/controller/ShopManageController.java index 136bde4..4d50b06 100644 --- a/backend-java/src/main/java/com/nanri/aiimage/modules/shopkey/controller/ShopManageController.java +++ b/backend-java/src/main/java/com/nanri/aiimage/modules/shopkey/controller/ShopManageController.java @@ -2,15 +2,15 @@ package com.nanri.aiimage.modules.shopkey.controller; import com.nanri.aiimage.common.api.ApiResponse; import com.nanri.aiimage.modules.shopkey.model.dto.ShopManageCreateRequest; -import com.nanri.aiimage.modules.shopkey.model.dto.ShopManageUpdateRequest; import com.nanri.aiimage.modules.shopkey.model.dto.ShopManageGroupCreateRequest; import com.nanri.aiimage.modules.shopkey.model.dto.ShopManageGroupUpdateRequest; +import com.nanri.aiimage.modules.shopkey.model.dto.ShopManageUpdateRequest; +import com.nanri.aiimage.modules.shopkey.model.vo.ShopManageCredentialVo; +import com.nanri.aiimage.modules.shopkey.model.vo.ShopManageGroupItemVo; import com.nanri.aiimage.modules.shopkey.model.vo.ShopManageItemVo; import com.nanri.aiimage.modules.shopkey.model.vo.ShopManagePageVo; -import com.nanri.aiimage.modules.shopkey.model.vo.ShopManageGroupItemVo; -import com.nanri.aiimage.modules.shopkey.model.vo.ShopManageCredentialVo; -import com.nanri.aiimage.modules.shopkey.service.ShopManageService; import com.nanri.aiimage.modules.shopkey.service.ShopManageGroupService; +import com.nanri.aiimage.modules.shopkey.service.ShopManageService; import io.swagger.v3.oas.annotations.Operation; import io.swagger.v3.oas.annotations.Parameter; import io.swagger.v3.oas.annotations.media.Content; @@ -34,7 +34,7 @@ import org.springframework.web.bind.annotation.RestController; @RestController @RequiredArgsConstructor @RequestMapping("/api/admin/shop-manages") -@Tag(name = "店铺管理", description = "维护店铺信息,支持增删改查。") +@Tag(name = "店铺管理", description = "维护店铺信息,支持查询、新建、修改、删除") public class ShopManageController { @Value("${aiimage.security.internal-token:}") @@ -44,68 +44,89 @@ public class ShopManageController { private final ShopManageGroupService shopManageGroupService; @GetMapping - @Operation(summary = "分页查询店铺", description = "分页查询店铺列表。") + @Operation(summary = "分页查询店铺", description = "支持按分组、店铺名称、创建人进行筛选") @ApiResponses({ - @io.swagger.v3.oas.annotations.responses.ApiResponse(responseCode = "200", description = "查询成功", content = @Content(schema = @Schema(implementation = ShopManagePageVo.class))) + @io.swagger.v3.oas.annotations.responses.ApiResponse( + responseCode = "200", + description = "查询成功", + content = @Content(schema = @Schema(implementation = ShopManagePageVo.class))) }) public ApiResponse page( @Parameter(description = "页码") @RequestParam(defaultValue = "1") Long page, @Parameter(description = "每页数量") @RequestParam(defaultValue = "15") Long pageSize, - @Parameter(description = "分组ID") @RequestParam(required = false) Long groupId, - @Parameter(description = "店铺名") @RequestParam(required = false) String shopName) { - return ApiResponse.success(shopManageService.page(page, pageSize, groupId, shopName)); + @Parameter(description = "分组 ID") @RequestParam(required = false) Long groupId, + @Parameter(description = "店铺名称") @RequestParam(required = false) String shopName, + @Parameter(description = "创建人用户 ID,普通管理员只传自己") @RequestParam(required = false) Long createdById) { + return ApiResponse.success(shopManageService.page(page, pageSize, groupId, shopName, createdById)); } @PostMapping - @Operation(summary = "新增店铺", description = "新增一条店铺记录。") - public ApiResponse create(@Valid @RequestBody ShopManageCreateRequest request) { - return ApiResponse.success("创建成功", shopManageService.create(request)); + @Operation(summary = "新增店铺", description = "创建店铺记录,并写入创建人") + public ApiResponse create( + @Parameter(description = "当前操作人用户 ID") @RequestParam Long operatorId, + @Parameter(description = "是否超级管理员") @RequestParam(defaultValue = "false") Boolean superAdmin, + @Valid @RequestBody ShopManageCreateRequest request) { + return ApiResponse.success("创建成功", + shopManageService.create(request, operatorId, Boolean.TRUE.equals(superAdmin))); } @PutMapping("/{id}") - @Operation(summary = "更新店铺", description = "按 ID 更新一条店铺记录。") + @Operation(summary = "更新店铺", description = "按 ID 更新店铺,普通管理员只能修改自己创建的数据") public ApiResponse update( - @Parameter(description = "主键ID", required = true) @PathVariable Long id, + @Parameter(description = "主键 ID", required = true) @PathVariable Long id, + @Parameter(description = "当前操作人用户 ID") @RequestParam Long operatorId, + @Parameter(description = "是否超级管理员") @RequestParam(defaultValue = "false") Boolean superAdmin, @Valid @RequestBody ShopManageUpdateRequest request) { - return ApiResponse.success("更新成功", shopManageService.update(id, request)); + return ApiResponse.success("更新成功", + shopManageService.update(id, operatorId, Boolean.TRUE.equals(superAdmin), request)); } @DeleteMapping("/{id}") - @Operation(summary = "删除店铺", description = "按 ID 删除一条店铺记录。") - public ApiResponse delete(@Parameter(description = "主键ID", required = true) @PathVariable Long id) { - shopManageService.delete(id); + @Operation(summary = "删除店铺", description = "按 ID 删除店铺,普通管理员只能删除自己创建的数据") + public ApiResponse delete( + @Parameter(description = "主键 ID", required = true) @PathVariable Long id, + @Parameter(description = "当前操作人用户 ID") @RequestParam Long operatorId, + @Parameter(description = "是否超级管理员") @RequestParam(defaultValue = "false") Boolean superAdmin) { + shopManageService.delete(id, operatorId, Boolean.TRUE.equals(superAdmin)); return ApiResponse.success("删除成功", null); } @GetMapping("/groups") - @Operation(summary = "查询分组列表", description = "用于店铺管理下拉选择。") - public ApiResponse> listGroups() { - return ApiResponse.success(shopManageGroupService.list()); + @Operation(summary = "查询分组列表", description = "普通管理员只返回自己创建的分组,超级管理员返回全部") + public ApiResponse> listGroups( + @Parameter(description = "创建人用户 ID,普通管理员只传自己") @RequestParam(required = false) Long createdById) { + return ApiResponse.success(shopManageGroupService.list(createdById)); } @PostMapping("/groups") - @Operation(summary = "新增分组", description = "新增一条店铺分组。") + @Operation(summary = "新增分组", description = "创建分组并写入创建人") public ApiResponse createGroup(@Valid @RequestBody ShopManageGroupCreateRequest request) { return ApiResponse.success("创建成功", shopManageGroupService.create(request)); } @PutMapping("/groups/{id}") - @Operation(summary = "更新分组", description = "按 ID 更新分组名称。") + @Operation(summary = "更新分组", description = "普通管理员只能修改自己创建的分组") public ApiResponse updateGroup( - @Parameter(description = "主键ID", required = true) @PathVariable Long id, + @Parameter(description = "主键 ID", required = true) @PathVariable Long id, + @Parameter(description = "当前操作人用户 ID") @RequestParam Long operatorId, + @Parameter(description = "是否超级管理员") @RequestParam(defaultValue = "false") Boolean superAdmin, @Valid @RequestBody ShopManageGroupUpdateRequest request) { - return ApiResponse.success("更新成功", shopManageGroupService.update(id, request)); + return ApiResponse.success("更新成功", + shopManageGroupService.update(id, operatorId, Boolean.TRUE.equals(superAdmin), request)); } @DeleteMapping("/groups/{id}") - @Operation(summary = "删除分组", description = "删除分组(分组下有店铺时不允许删除)。") - public ApiResponse deleteGroup(@Parameter(description = "主键ID", required = true) @PathVariable Long id) { - shopManageGroupService.delete(id); + @Operation(summary = "删除分组", description = "普通管理员只能删除自己创建的分组") + public ApiResponse deleteGroup( + @Parameter(description = "主键 ID", required = true) @PathVariable Long id, + @Parameter(description = "当前操作人用户 ID") @RequestParam Long operatorId, + @Parameter(description = "是否超级管理员") @RequestParam(defaultValue = "false") Boolean superAdmin) { + shopManageGroupService.delete(id, operatorId, Boolean.TRUE.equals(superAdmin)); return ApiResponse.success("删除成功", null); } @GetMapping("/credential") - @Operation(summary = "按店铺名获取明文凭据(内部)", description = "仅供 Python 自动化内部调用,需传 X-Internal-Token。") + @Operation(summary = "按店铺名获取明文凭据(内部)", description = "仅供 Python 自动化内部调用,需要传 X-Internal-Token") public ApiResponse getCredentialByShopName( @RequestParam("shopName") String shopName, @RequestHeader(value = "X-Internal-Token", required = false) String token) { diff --git a/backend-java/src/main/java/com/nanri/aiimage/modules/shopkey/controller/SkipPriceAsinController.java b/backend-java/src/main/java/com/nanri/aiimage/modules/shopkey/controller/SkipPriceAsinController.java index fadfbe9..d840a87 100644 --- a/backend-java/src/main/java/com/nanri/aiimage/modules/shopkey/controller/SkipPriceAsinController.java +++ b/backend-java/src/main/java/com/nanri/aiimage/modules/shopkey/controller/SkipPriceAsinController.java @@ -2,6 +2,7 @@ package com.nanri.aiimage.modules.shopkey.controller; import com.nanri.aiimage.common.api.ApiResponse; import com.nanri.aiimage.modules.shopkey.model.dto.SkipPriceAsinCreateRequest; +import com.nanri.aiimage.modules.shopkey.model.dto.SkipPriceAsinCountryUpdateRequest; import com.nanri.aiimage.modules.shopkey.model.vo.SkipPriceAsinItemVo; import com.nanri.aiimage.modules.shopkey.model.vo.SkipPriceAsinPageVo; import com.nanri.aiimage.modules.shopkey.service.SkipPriceAsinService; @@ -14,6 +15,7 @@ import org.springframework.web.bind.annotation.DeleteMapping; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.PathVariable; import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.PutMapping; import org.springframework.web.bind.annotation.RequestBody; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RequestParam; @@ -44,6 +46,15 @@ public class SkipPriceAsinController { return ApiResponse.success("保存成功", skipPriceAsinService.createOrUpdate(request)); } + @PutMapping("/{id}/countries/{country}") + @Operation(summary = "编辑指定国家的跳过跟价 ASIN") + public ApiResponse updateCountry( + @Parameter(description = "主键ID", required = true) @PathVariable Long id, + @Parameter(description = "国家编码", required = true) @PathVariable String country, + @Valid @RequestBody SkipPriceAsinCountryUpdateRequest request) { + return ApiResponse.success("保存成功", skipPriceAsinService.updateCountry(id, country, request.getAsin())); + } + @DeleteMapping("/{id}/countries/{country}") @Operation(summary = "删除指定国家的跳过跟价 ASIN") public ApiResponse deleteCountry( diff --git a/backend-java/src/main/java/com/nanri/aiimage/modules/shopkey/model/dto/ShopManageCreateRequest.java b/backend-java/src/main/java/com/nanri/aiimage/modules/shopkey/model/dto/ShopManageCreateRequest.java index b9148a0..607250c 100644 --- a/backend-java/src/main/java/com/nanri/aiimage/modules/shopkey/model/dto/ShopManageCreateRequest.java +++ b/backend-java/src/main/java/com/nanri/aiimage/modules/shopkey/model/dto/ShopManageCreateRequest.java @@ -1,5 +1,6 @@ package com.nanri.aiimage.modules.shopkey.model.dto; +import io.swagger.v3.oas.annotations.media.Schema; import jakarta.validation.constraints.NotNull; import jakarta.validation.constraints.NotBlank; import lombok.Data; @@ -7,18 +8,27 @@ import lombok.Data; @Data public class ShopManageCreateRequest { + @Schema(description = "分组 ID", requiredMode = Schema.RequiredMode.REQUIRED) @NotNull(message = "分组不能为空") private Long groupId; - @NotBlank(message = "店铺名不能为空") + @Schema(description = "店铺名称", requiredMode = Schema.RequiredMode.REQUIRED) + @NotBlank(message = "店铺名称不能为空") private String shopName; - @NotBlank(message = "店铺商城名不能为空") + @Schema(description = "商城名称", requiredMode = Schema.RequiredMode.REQUIRED) + @NotBlank(message = "商城名称不能为空") private String mallName; + @Schema(description = "登录账号", requiredMode = Schema.RequiredMode.REQUIRED) @NotBlank(message = "账号不能为空") private String account; + @Schema(description = "登录密码", requiredMode = Schema.RequiredMode.REQUIRED) @NotBlank(message = "密码不能为空") private String password; + + @Schema(description = "创建人用户 ID,由后台代理透传", requiredMode = Schema.RequiredMode.REQUIRED) + @NotNull(message = "创建人不能为空") + private Long createdById; } diff --git a/backend-java/src/main/java/com/nanri/aiimage/modules/shopkey/model/dto/ShopManageGroupCreateRequest.java b/backend-java/src/main/java/com/nanri/aiimage/modules/shopkey/model/dto/ShopManageGroupCreateRequest.java index 705e1e1..62d2d5b 100644 --- a/backend-java/src/main/java/com/nanri/aiimage/modules/shopkey/model/dto/ShopManageGroupCreateRequest.java +++ b/backend-java/src/main/java/com/nanri/aiimage/modules/shopkey/model/dto/ShopManageGroupCreateRequest.java @@ -1,11 +1,18 @@ package com.nanri.aiimage.modules.shopkey.model.dto; +import io.swagger.v3.oas.annotations.media.Schema; import jakarta.validation.constraints.NotBlank; +import jakarta.validation.constraints.NotNull; import lombok.Data; @Data public class ShopManageGroupCreateRequest { + @Schema(description = "分组名称", requiredMode = Schema.RequiredMode.REQUIRED) @NotBlank(message = "分组名称不能为空") private String groupName; + + @Schema(description = "创建人用户 ID,由后台代理透传", requiredMode = Schema.RequiredMode.REQUIRED) + @NotNull(message = "创建人不能为空") + private Long createdById; } diff --git a/backend-java/src/main/java/com/nanri/aiimage/modules/shopkey/model/dto/ShopManageGroupUpdateRequest.java b/backend-java/src/main/java/com/nanri/aiimage/modules/shopkey/model/dto/ShopManageGroupUpdateRequest.java index 007b045..1469aca 100644 --- a/backend-java/src/main/java/com/nanri/aiimage/modules/shopkey/model/dto/ShopManageGroupUpdateRequest.java +++ b/backend-java/src/main/java/com/nanri/aiimage/modules/shopkey/model/dto/ShopManageGroupUpdateRequest.java @@ -1,11 +1,13 @@ package com.nanri.aiimage.modules.shopkey.model.dto; +import io.swagger.v3.oas.annotations.media.Schema; import jakarta.validation.constraints.NotBlank; import lombok.Data; @Data public class ShopManageGroupUpdateRequest { + @Schema(description = "分组名称", requiredMode = Schema.RequiredMode.REQUIRED) @NotBlank(message = "分组名称不能为空") private String groupName; } diff --git a/backend-java/src/main/java/com/nanri/aiimage/modules/shopkey/model/dto/ShopManageUpdateRequest.java b/backend-java/src/main/java/com/nanri/aiimage/modules/shopkey/model/dto/ShopManageUpdateRequest.java index 2f2fe5e..e31f957 100644 --- a/backend-java/src/main/java/com/nanri/aiimage/modules/shopkey/model/dto/ShopManageUpdateRequest.java +++ b/backend-java/src/main/java/com/nanri/aiimage/modules/shopkey/model/dto/ShopManageUpdateRequest.java @@ -1,5 +1,6 @@ package com.nanri.aiimage.modules.shopkey.model.dto; +import io.swagger.v3.oas.annotations.media.Schema; import jakarta.validation.constraints.NotNull; import jakarta.validation.constraints.NotBlank; import lombok.Data; @@ -7,18 +8,23 @@ import lombok.Data; @Data public class ShopManageUpdateRequest { + @Schema(description = "分组 ID", requiredMode = Schema.RequiredMode.REQUIRED) @NotNull(message = "分组不能为空") private Long groupId; - @NotBlank(message = "店铺名不能为空") + @Schema(description = "店铺名称", requiredMode = Schema.RequiredMode.REQUIRED) + @NotBlank(message = "店铺名称不能为空") private String shopName; - @NotBlank(message = "店铺商城名不能为空") + @Schema(description = "商城名称", requiredMode = Schema.RequiredMode.REQUIRED) + @NotBlank(message = "商城名称不能为空") private String mallName; + @Schema(description = "登录账号", requiredMode = Schema.RequiredMode.REQUIRED) @NotBlank(message = "账号不能为空") private String account; + @Schema(description = "登录密码", requiredMode = Schema.RequiredMode.REQUIRED) @NotBlank(message = "密码不能为空") private String password; } diff --git a/backend-java/src/main/java/com/nanri/aiimage/modules/shopkey/model/dto/SkipPriceAsinCountryUpdateRequest.java b/backend-java/src/main/java/com/nanri/aiimage/modules/shopkey/model/dto/SkipPriceAsinCountryUpdateRequest.java new file mode 100644 index 0000000..be37835 --- /dev/null +++ b/backend-java/src/main/java/com/nanri/aiimage/modules/shopkey/model/dto/SkipPriceAsinCountryUpdateRequest.java @@ -0,0 +1,13 @@ +package com.nanri.aiimage.modules.shopkey.model.dto; + +import io.swagger.v3.oas.annotations.media.Schema; +import jakarta.validation.constraints.NotBlank; +import lombok.Data; + +@Data +public class SkipPriceAsinCountryUpdateRequest { + + @Schema(description = "ASIN", requiredMode = Schema.RequiredMode.REQUIRED) + @NotBlank(message = "ASIN 不能为空") + private String asin; +} diff --git a/backend-java/src/main/java/com/nanri/aiimage/modules/shopkey/model/entity/ShopManageEntity.java b/backend-java/src/main/java/com/nanri/aiimage/modules/shopkey/model/entity/ShopManageEntity.java index 502110c..918ff1d 100644 --- a/backend-java/src/main/java/com/nanri/aiimage/modules/shopkey/model/entity/ShopManageEntity.java +++ b/backend-java/src/main/java/com/nanri/aiimage/modules/shopkey/model/entity/ShopManageEntity.java @@ -17,6 +17,8 @@ public class ShopManageEntity { private Long groupId; private String groupName; private String shopName; + @TableField("created_by_id") + private Long createdById; @TableField("mall_name") private String mallName; private String account; diff --git a/backend-java/src/main/java/com/nanri/aiimage/modules/shopkey/model/entity/ShopManageGroupEntity.java b/backend-java/src/main/java/com/nanri/aiimage/modules/shopkey/model/entity/ShopManageGroupEntity.java index 3655103..623de76 100644 --- a/backend-java/src/main/java/com/nanri/aiimage/modules/shopkey/model/entity/ShopManageGroupEntity.java +++ b/backend-java/src/main/java/com/nanri/aiimage/modules/shopkey/model/entity/ShopManageGroupEntity.java @@ -1,6 +1,7 @@ package com.nanri.aiimage.modules.shopkey.model.entity; import com.baomidou.mybatisplus.annotation.IdType; +import com.baomidou.mybatisplus.annotation.TableField; import com.baomidou.mybatisplus.annotation.TableId; import com.baomidou.mybatisplus.annotation.TableName; import lombok.Data; @@ -14,6 +15,8 @@ public class ShopManageGroupEntity { @TableId(type = IdType.AUTO) private Long id; private String groupName; + @TableField("created_by_id") + private Long createdById; private LocalDateTime createdAt; private LocalDateTime updatedAt; } diff --git a/backend-java/src/main/java/com/nanri/aiimage/modules/shopkey/service/ShopManageGroupService.java b/backend-java/src/main/java/com/nanri/aiimage/modules/shopkey/service/ShopManageGroupService.java index 151ea57..95d2314 100644 --- a/backend-java/src/main/java/com/nanri/aiimage/modules/shopkey/service/ShopManageGroupService.java +++ b/backend-java/src/main/java/com/nanri/aiimage/modules/shopkey/service/ShopManageGroupService.java @@ -6,8 +6,8 @@ import com.nanri.aiimage.modules.shopkey.mapper.ShopManageGroupMapper; import com.nanri.aiimage.modules.shopkey.mapper.ShopManageMapper; import com.nanri.aiimage.modules.shopkey.model.dto.ShopManageGroupCreateRequest; import com.nanri.aiimage.modules.shopkey.model.dto.ShopManageGroupUpdateRequest; -import com.nanri.aiimage.modules.shopkey.model.entity.ShopManageGroupEntity; import com.nanri.aiimage.modules.shopkey.model.entity.ShopManageEntity; +import com.nanri.aiimage.modules.shopkey.model.entity.ShopManageGroupEntity; import com.nanri.aiimage.modules.shopkey.model.vo.ShopManageGroupItemVo; import lombok.RequiredArgsConstructor; import org.springframework.stereotype.Service; @@ -23,7 +23,13 @@ public class ShopManageGroupService { private final ShopManageMapper shopManageMapper; public List list() { - return groupMapper.selectList(new LambdaQueryWrapper().orderByAsc(ShopManageGroupEntity::getId)) + return list(null); + } + + public List list(Long createdById) { + return groupMapper.selectList(new LambdaQueryWrapper() + .eq(createdById != null && createdById > 0, ShopManageGroupEntity::getCreatedById, createdById) + .orderByAsc(ShopManageGroupEntity::getId)) .stream() .map(this::toVo) .toList(); @@ -31,34 +37,45 @@ public class ShopManageGroupService { @Transactional public ShopManageGroupItemVo create(ShopManageGroupCreateRequest request) { + Long createdById = normalizePositiveId(request.getCreatedById(), "创建人不能为空"); String groupName = normalizeRequired(request.getGroupName(), "分组名称不能为空"); - ensureGroupNameUnique(groupName, null); + ensureGroupNameUnique(groupName, null, createdById, false); ShopManageGroupEntity entity = new ShopManageGroupEntity(); entity.setGroupName(groupName); + entity.setCreatedById(createdById); groupMapper.insert(entity); return toVo(getById(entity.getId())); } @Transactional - public ShopManageGroupItemVo update(Long id, ShopManageGroupUpdateRequest request) { + public ShopManageGroupItemVo update(Long id, Long operatorId, boolean superAdmin, ShopManageGroupUpdateRequest request) { ShopManageGroupEntity entity = getById(id); + validateOwnership(entity, operatorId, superAdmin); String groupName = normalizeRequired(request.getGroupName(), "分组名称不能为空"); - ensureGroupNameUnique(groupName, id); + ensureGroupNameUnique(groupName, id, operatorId, superAdmin); entity.setGroupName(groupName); groupMapper.updateById(entity); return toVo(getById(id)); } @Transactional - public void delete(Long id) { + public void delete(Long id, Long operatorId, boolean superAdmin) { ShopManageGroupEntity entity = getById(id); - Long useCount = shopManageMapper.selectCount(new LambdaQueryWrapper().eq(ShopManageEntity::getGroupId, entity.getId())); + validateOwnership(entity, operatorId, superAdmin); + Long useCount = shopManageMapper.selectCount(new LambdaQueryWrapper() + .eq(ShopManageEntity::getGroupId, entity.getId())); if (useCount != null && useCount > 0) { throw new BusinessException("该分组下存在店铺,无法删除"); } groupMapper.deleteById(entity.getId()); } + public ShopManageGroupEntity getAccessibleById(Long id, Long operatorId, boolean superAdmin) { + ShopManageGroupEntity entity = getById(id); + validateOwnership(entity, operatorId, superAdmin); + return entity; + } + public ShopManageGroupEntity getById(Long id) { ShopManageGroupEntity entity = groupMapper.selectById(id); if (entity == null) { @@ -67,9 +84,12 @@ public class ShopManageGroupService { return entity; } - private void ensureGroupNameUnique(String groupName, Long excludeId) { + private void ensureGroupNameUnique(String groupName, Long excludeId, Long operatorId, boolean superAdmin) { LambdaQueryWrapper query = new LambdaQueryWrapper() .eq(ShopManageGroupEntity::getGroupName, groupName); + if (!superAdmin) { + query.eq(ShopManageGroupEntity::getCreatedById, normalizePositiveId(operatorId, "操作人不能为空")); + } if (excludeId != null) { query.ne(ShopManageGroupEntity::getId, excludeId); } @@ -79,6 +99,16 @@ public class ShopManageGroupService { } } + private void validateOwnership(ShopManageGroupEntity entity, Long operatorId, boolean superAdmin) { + if (superAdmin) { + return; + } + Long normalizedOperatorId = normalizePositiveId(operatorId, "操作人不能为空"); + if (entity.getCreatedById() == null || !entity.getCreatedById().equals(normalizedOperatorId)) { + throw new BusinessException("只能操作自己创建的分组数据"); + } + } + private String normalizeRequired(String value, String message) { String normalized = value == null ? "" : value.trim(); if (normalized.isEmpty()) { @@ -87,6 +117,13 @@ public class ShopManageGroupService { return normalized; } + private Long normalizePositiveId(Long value, String message) { + if (value == null || value <= 0) { + throw new BusinessException(message); + } + return value; + } + private ShopManageGroupItemVo toVo(ShopManageGroupEntity entity) { ShopManageGroupItemVo vo = new ShopManageGroupItemVo(); vo.setId(entity.getId()); diff --git a/backend-java/src/main/java/com/nanri/aiimage/modules/shopkey/service/ShopManageService.java b/backend-java/src/main/java/com/nanri/aiimage/modules/shopkey/service/ShopManageService.java index 967a7ba..f6ab333 100644 --- a/backend-java/src/main/java/com/nanri/aiimage/modules/shopkey/service/ShopManageService.java +++ b/backend-java/src/main/java/com/nanri/aiimage/modules/shopkey/service/ShopManageService.java @@ -8,9 +8,9 @@ import com.nanri.aiimage.modules.shopkey.model.dto.ShopManageCreateRequest; import com.nanri.aiimage.modules.shopkey.model.dto.ShopManageUpdateRequest; import com.nanri.aiimage.modules.shopkey.model.entity.ShopManageEntity; import com.nanri.aiimage.modules.shopkey.model.entity.ShopManageGroupEntity; +import com.nanri.aiimage.modules.shopkey.model.vo.ShopManageCredentialVo; import com.nanri.aiimage.modules.shopkey.model.vo.ShopManageItemVo; import com.nanri.aiimage.modules.shopkey.model.vo.ShopManagePageVo; -import com.nanri.aiimage.modules.shopkey.model.vo.ShopManageCredentialVo; import lombok.RequiredArgsConstructor; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; @@ -28,16 +28,21 @@ public class ShopManageService { private final ShopCredentialCryptoService shopCredentialCryptoService; public ShopManagePageVo page(long page, long pageSize) { - return page(page, pageSize, null, null); + return page(page, pageSize, null, null, null); } public ShopManagePageVo page(long page, long pageSize, Long groupId, String shopName) { + return page(page, pageSize, groupId, shopName, null); + } + + public ShopManagePageVo page(long page, long pageSize, Long groupId, String shopName, Long createdById) { long safePage = Math.max(page, 1); long safePageSize = Math.min(Math.max(pageSize, 1), 100); String safeShopName = shopName == null ? "" : shopName.trim(); LambdaQueryWrapper query = new LambdaQueryWrapper() .eq(groupId != null && groupId > 0, ShopManageEntity::getGroupId, groupId) + .eq(createdById != null && createdById > 0, ShopManageEntity::getCreatedById, createdById) .like(!safeShopName.isEmpty(), ShopManageEntity::getShopName, safeShopName) .orderByDesc(ShopManageEntity::getId); Long total = shopManageMapper.selectCount(query); @@ -62,16 +67,18 @@ public class ShopManageService { } @Transactional - public ShopManageItemVo create(ShopManageCreateRequest request) { - ShopManageGroupEntity group = shopManageGroupService.getById(request.getGroupId()); - String shopName = normalizeRequired(request.getShopName(), "店铺名不能为空"); - String mallName = normalizeRequired(request.getMallName(), "店铺商城名不能为空"); + public ShopManageItemVo create(ShopManageCreateRequest request, Long operatorId, boolean superAdmin) { + Long createdById = normalizePositiveId(request.getCreatedById(), "创建人不能为空"); + ShopManageGroupEntity group = shopManageGroupService.getAccessibleById(request.getGroupId(), operatorId, superAdmin); + String shopName = normalizeRequired(request.getShopName(), "店铺名称不能为空"); + String mallName = normalizeRequired(request.getMallName(), "商城名称不能为空"); ensureShopNameUnique(shopName, null); ShopManageEntity entity = new ShopManageEntity(); entity.setGroupId(group.getId()); entity.setGroupName(group.getGroupName()); entity.setShopName(shopName); + entity.setCreatedById(createdById); entity.setMallName(mallName); entity.setAccount(normalizeRequired(request.getAccount(), "账号不能为空")); entity.setPassword(shopCredentialCryptoService.encrypt(normalizeRequired(request.getPassword(), "密码不能为空"))); @@ -81,11 +88,12 @@ public class ShopManageService { } @Transactional - public ShopManageItemVo update(Long id, ShopManageUpdateRequest request) { + public ShopManageItemVo update(Long id, Long operatorId, boolean superAdmin, ShopManageUpdateRequest request) { ShopManageEntity entity = getById(id); - ShopManageGroupEntity group = shopManageGroupService.getById(request.getGroupId()); - String shopName = normalizeRequired(request.getShopName(), "店铺名不能为空"); - String mallName = normalizeRequired(request.getMallName(), "店铺商城名不能为空"); + validateOwnership(entity, operatorId, superAdmin); + ShopManageGroupEntity group = shopManageGroupService.getAccessibleById(request.getGroupId(), operatorId, superAdmin); + String shopName = normalizeRequired(request.getShopName(), "店铺名称不能为空"); + String mallName = normalizeRequired(request.getMallName(), "商城名称不能为空"); ensureShopNameUnique(shopName, id); entity.setGroupId(group.getId()); @@ -100,13 +108,14 @@ public class ShopManageService { } @Transactional - public void delete(Long id) { + public void delete(Long id, Long operatorId, boolean superAdmin) { ShopManageEntity entity = getById(id); + validateOwnership(entity, operatorId, superAdmin); shopManageMapper.deleteById(entity.getId()); } public ShopManageCredentialVo getCredentialByShopName(String shopName) { - String normalizedShopName = normalizeRequired(shopName, "店铺名不能为空"); + String normalizedShopName = normalizeRequired(shopName, "店铺名称不能为空"); ShopManageEntity entity = shopManageMapper.selectOne(new LambdaQueryWrapper() .eq(ShopManageEntity::getShopName, normalizedShopName) .last("limit 1")); @@ -137,6 +146,16 @@ public class ShopManageService { return entity; } + private void validateOwnership(ShopManageEntity entity, Long operatorId, boolean superAdmin) { + if (superAdmin) { + return; + } + Long normalizedOperatorId = normalizePositiveId(operatorId, "操作人不能为空"); + if (entity.getCreatedById() == null || !entity.getCreatedById().equals(normalizedOperatorId)) { + throw new BusinessException("只能操作自己创建的店铺数据"); + } + } + private String normalizeRequired(String value, String message) { String normalized = value == null ? "" : value.trim(); if (normalized.isEmpty()) { @@ -145,6 +164,13 @@ public class ShopManageService { return normalized; } + private Long normalizePositiveId(Long value, String message) { + if (value == null || value <= 0) { + throw new BusinessException(message); + } + return value; + } + private void ensureShopNameUnique(String shopName, Long excludeId) { LambdaQueryWrapper query = new LambdaQueryWrapper() .eq(ShopManageEntity::getShopName, shopName); @@ -153,7 +179,7 @@ public class ShopManageService { } Long count = shopManageMapper.selectCount(query); if (count != null && count > 0) { - throw new BusinessException("店铺名已存在"); + throw new BusinessException("店铺名称已存在"); } } @@ -164,8 +190,8 @@ public class ShopManageService { LinkedHashMap map = new LinkedHashMap<>(); for (Long groupId : groupIds) { try { - ShopManageGroupEntity g = shopManageGroupService.getById(groupId); - map.put(groupId, g.getGroupName()); + ShopManageGroupEntity group = shopManageGroupService.getById(groupId); + map.put(groupId, group.getGroupName()); } catch (Exception ignored) { map.put(groupId, ""); } diff --git a/backend-java/src/main/java/com/nanri/aiimage/modules/shopkey/service/SkipPriceAsinService.java b/backend-java/src/main/java/com/nanri/aiimage/modules/shopkey/service/SkipPriceAsinService.java index 16c2438..8a95f2d 100644 --- a/backend-java/src/main/java/com/nanri/aiimage/modules/shopkey/service/SkipPriceAsinService.java +++ b/backend-java/src/main/java/com/nanri/aiimage/modules/shopkey/service/SkipPriceAsinService.java @@ -105,6 +105,24 @@ public class SkipPriceAsinService { skipPriceAsinMapper.updateById(entity); } + @Transactional + public SkipPriceAsinItemVo updateCountry(Long id, String country, String asin) { + SkipPriceAsinEntity entity = getById(id); + String normalizedCountry = normalizeCountry(country); + String normalizedAsin = normalizeAsin(asin); + setCountryAsin(entity, normalizedCountry, normalizedAsin); + skipPriceAsinMapper.updateById(entity); + SkipPriceAsinEntity saved = getById(entity.getId()); + String groupName = ""; + try { + ShopManageGroupEntity group = shopManageGroupService.getById(saved.getGroupId()); + groupName = group.getGroupName(); + } catch (Exception ignored) { + groupName = ""; + } + return toItemVo(saved, groupName); + } + private SkipPriceAsinEntity getById(Long id) { SkipPriceAsinEntity entity = skipPriceAsinMapper.selectById(id); if (entity == null) { diff --git a/backend-java/src/main/resources/application.yml b/backend-java/src/main/resources/application.yml index e92b5b7..066b61c 100644 --- a/backend-java/src/main/resources/application.yml +++ b/backend-java/src/main/resources/application.yml @@ -84,12 +84,14 @@ 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} + price-track-stale-timeout-minutes: ${AIIMAGE_PRICE_TRACK_STALE_TIMEOUT_MINUTES:10} + price-track-initial-timeout-minutes: ${AIIMAGE_PRICE_TRACK_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 * * *} - module-types: ${AIIMAGE_MODULE_CLEANUP_MODULE_TYPES:DEDUPE,SPLIT,CONVERT,DELETE_BRAND,PRODUCT_RISK_RESOLVE,SHOP_MATCH} + module-types: ${AIIMAGE_MODULE_CLEANUP_MODULE_TYPES:DEDUPE,SPLIT,CONVERT,DELETE_BRAND,PRODUCT_RISK_RESOLVE,PRICE_TRACK,SHOP_MATCH} security: shop-credential-key: ${AIIMAGE_SHOP_CREDENTIAL_KEY:change-me-shop-credential-key} internal-token: ${AIIMAGE_INTERNAL_TOKEN:} diff --git a/backend-java/src/main/resources/db/V22__shop_manage_add_created_by_id.sql b/backend-java/src/main/resources/db/V22__shop_manage_add_created_by_id.sql new file mode 100644 index 0000000..e4aaa56 --- /dev/null +++ b/backend-java/src/main/resources/db/V22__shop_manage_add_created_by_id.sql @@ -0,0 +1,33 @@ +SET @db_name = DATABASE(); + +SET @created_by_id_col_exists := ( + SELECT COUNT(*) + FROM information_schema.COLUMNS + WHERE TABLE_SCHEMA = @db_name + AND TABLE_NAME = 'biz_shop_manage' + AND COLUMN_NAME = 'created_by_id' +); + +SET @sql_add_created_by_id_col := IF(@created_by_id_col_exists = 0, + 'ALTER TABLE biz_shop_manage ADD COLUMN created_by_id BIGINT NULL COMMENT ''创建人用户ID'' AFTER shop_name', + 'SELECT 1' +); +PREPARE stmt_add_created_by_id_col FROM @sql_add_created_by_id_col; +EXECUTE stmt_add_created_by_id_col; +DEALLOCATE PREPARE stmt_add_created_by_id_col; + +SET @created_by_id_idx_exists := ( + SELECT COUNT(*) + FROM information_schema.STATISTICS + WHERE TABLE_SCHEMA = @db_name + AND TABLE_NAME = 'biz_shop_manage' + AND INDEX_NAME = 'idx_created_by_id' +); + +SET @sql_add_created_by_id_idx := IF(@created_by_id_idx_exists = 0, + 'ALTER TABLE biz_shop_manage ADD INDEX idx_created_by_id (created_by_id)', + 'SELECT 1' +); +PREPARE stmt_add_created_by_id_idx FROM @sql_add_created_by_id_idx; +EXECUTE stmt_add_created_by_id_idx; +DEALLOCATE PREPARE stmt_add_created_by_id_idx; diff --git a/backend-java/src/main/resources/db/V23__shop_manage_group_add_created_by_id.sql b/backend-java/src/main/resources/db/V23__shop_manage_group_add_created_by_id.sql new file mode 100644 index 0000000..448656b --- /dev/null +++ b/backend-java/src/main/resources/db/V23__shop_manage_group_add_created_by_id.sql @@ -0,0 +1,33 @@ +SET @db_name = DATABASE(); + +SET @created_by_id_col_exists := ( + SELECT COUNT(*) + FROM information_schema.COLUMNS + WHERE TABLE_SCHEMA = @db_name + AND TABLE_NAME = 'biz_shop_manage_group' + AND COLUMN_NAME = 'created_by_id' +); + +SET @sql_add_created_by_id_col := IF(@created_by_id_col_exists = 0, + 'ALTER TABLE biz_shop_manage_group ADD COLUMN created_by_id BIGINT NULL COMMENT ''创建人用户ID'' AFTER group_name', + 'SELECT 1' +); +PREPARE stmt_add_created_by_id_col FROM @sql_add_created_by_id_col; +EXECUTE stmt_add_created_by_id_col; +DEALLOCATE PREPARE stmt_add_created_by_id_col; + +SET @created_by_id_idx_exists := ( + SELECT COUNT(*) + FROM information_schema.STATISTICS + WHERE TABLE_SCHEMA = @db_name + AND TABLE_NAME = 'biz_shop_manage_group' + AND INDEX_NAME = 'idx_created_by_id' +); + +SET @sql_add_created_by_id_idx := IF(@created_by_id_idx_exists = 0, + 'ALTER TABLE biz_shop_manage_group ADD INDEX idx_created_by_id (created_by_id)', + 'SELECT 1' +); +PREPARE stmt_add_created_by_id_idx FROM @sql_add_created_by_id_idx; +EXECUTE stmt_add_created_by_id_idx; +DEALLOCATE PREPARE stmt_add_created_by_id_idx; diff --git a/backend/__pycache__/config.cpython-312.pyc b/backend/__pycache__/config.cpython-312.pyc index 02e3b90d1887b98ce1d899b3a9482e987b8c35d7..57fc226f5d7255b16ddbf4efe022298ae6eb1d8c 100644 GIT binary patch delta 84 zcmaFK{fwLYG%qg~0}#BBy1SA4JCmSTMoCG5mA<~Ak-44$5E^b4V-93exgjAnpKB)9 ne7>1{7u4-8I7D2Qh`cV5a8V-RLSo8giPQ@Wq7No_v&I1cNxmF8 delta 87 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 delta 3911 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 A0RR91 diff --git a/backend/blueprints/admin_api.py b/backend/blueprints/admin_api.py index 2988c8e..c225db2 100644 --- a/backend/blueprints/admin_api.py +++ b/backend/blueprints/admin_api.py @@ -32,7 +32,7 @@ def _ensure_dedupe_total_data_access(): return _ensure_admin_menu_access('dedupe-total-data') role, current_row = get_current_admin_role() if not _can_access_dedupe_total_data(role, current_row): - return None, None, (jsonify({'success': False, 'error': '无权限访问数据去重总数据'}), 403) + return None, None, (jsonify({'success': False, 'error': '无权访问数据去重汇总数据'}), 403) return role, current_row, None from ali_oss import upload_file as oss_upload_file @@ -48,23 +48,23 @@ ADMIN_MENU_ACCESS_CONFIG = { 'dedupe-total-data': { 'column_key': 'admin_dedupe_total_data', 'route_path': 'dedupe-total-data', - 'error': '鏃犳潈闄愯闂暟鎹幓閲嶆€绘暟鎹?', + 'error': '无权访问数据去重汇总数据', }, 'shop-manage': { 'column_key': 'admin_shop_manage', 'route_path': 'shop-manage', - 'error': '鏃犳潈闄愯闂簵閾虹鐞嗘ā鍧?', + 'error': '无权访问店铺管理模块', }, 'skip-price-asin': { 'column_key': 'admin_skip_price_asin', 'route_path': 'skip-price-asin', - 'error': '鏃犳潈闄愯闂烦杩囪窡浠稟SIN 妯″潡', + 'error': '无权访问跳过跟价 ASIN 模块', }, } def _safe_version_key(version): - """将版本号转为安全的 OSS 对象名部分""" + """将版本号转换为安全的 OSS 对象名片段""" s = (version or '').strip() s = re.sub(r'[^\w.\-]', '_', s) return s or 'unknown' @@ -138,7 +138,7 @@ def _ensure_admin_menu_access(*menu_names): if role == 'super_admin': return role, current_row, None if role != 'admin' or not current_row: - return role, current_row, (jsonify({'success': False, 'error': '闇€瑕佺鐞嗗憳鏉冮檺'}), 403) + return role, current_row, (jsonify({'success': False, 'error': '需要管理员权限'}), 403) key_set, route_set = _get_current_admin_permission_sets() for menu_name in menu_names: config = ADMIN_MENU_ACCESS_CONFIG.get(menu_name) or {} @@ -148,7 +148,7 @@ def _ensure_admin_menu_access(*menu_names): return role, current_row, None fallback_menu = menu_names[0] if menu_names else '' fallback_config = ADMIN_MENU_ACCESS_CONFIG.get(fallback_menu) or {} - error_message = fallback_config.get('error') or '鏃犳潈闄愯闂綋鍓嶆ā鍧?' + error_message = fallback_config.get('error') or '无权访问当前模块' return role, current_row, (jsonify({'success': False, 'error': error_message}), 403) @@ -157,7 +157,7 @@ def _ensure_admin_menu_access(*menu_names): @admin_api.route('/users') @admin_required def list_users(): - """分页获取用户列表;支持用户名模糊搜索、指定管理员所属普通用户筛选""" + """分页获取用户列表,支持用户名模糊搜索和按管理员归属筛选普通用户""" role, current_row = get_current_admin_role() if not role: return jsonify({'success': False, 'error': '需要管理员权限'}), 403 @@ -998,7 +998,7 @@ def delete_dedupe_total_data(item_id): @admin_api.route('/shop-manages') @admin_required def list_shop_manages(): - _, _, denied = _ensure_admin_menu_access('shop-manage') + role, current_row, denied = _ensure_admin_menu_access('shop-manage') if denied: return denied page = max(1, int(request.args.get('page', 1))) @@ -1007,6 +1007,8 @@ def list_shop_manages(): shop_name = (request.args.get('shop_name') or '').strip() params = {'page': page, 'pageSize': page_size} + if role != 'super_admin' and current_row and current_row.get('id'): + params['createdById'] = current_row.get('id') if group_id_raw: try: group_id = int(group_id_raw) @@ -1051,7 +1053,7 @@ def list_shop_manages(): @admin_api.route('/shop-manage', methods=['POST']) @admin_required def create_shop_manage(): - _, _, denied = _ensure_admin_menu_access('shop-manage') + role, current_row, denied = _ensure_admin_menu_access('shop-manage') if denied: return denied data = request.get_json() or {} @@ -1061,10 +1063,15 @@ def create_shop_manage(): 'mallName': (data.get('mall_name') or '').strip(), 'account': (data.get('account') or '').strip(), 'password': (data.get('password') or '').strip(), + 'createdById': current_row.get('id') if current_row else None, } result, error_response, status = _proxy_backend_java( 'POST', '/api/admin/shop-manages', + params={ + 'operatorId': current_row.get('id') if current_row else None, + 'superAdmin': 'true' if role == 'super_admin' else 'false', + }, json_data=payload, ) if error_response is not None: @@ -1090,7 +1097,7 @@ def create_shop_manage(): @admin_api.route('/shop-manage/', methods=['PUT']) @admin_required def update_shop_manage(item_id): - _, _, denied = _ensure_admin_menu_access('shop-manage') + role, current_row, denied = _ensure_admin_menu_access('shop-manage') if denied: return denied data = request.get_json() or {} @@ -1104,6 +1111,10 @@ def update_shop_manage(item_id): result, error_response, status = _proxy_backend_java( 'PUT', f'/api/admin/shop-manages/{item_id}', + params={ + 'operatorId': current_row.get('id') if current_row else None, + 'superAdmin': 'true' if role == 'super_admin' else 'false', + }, json_data=payload, ) if error_response is not None: @@ -1128,12 +1139,16 @@ def update_shop_manage(item_id): @admin_api.route('/shop-manage/', methods=['DELETE']) @admin_required def delete_shop_manage(item_id): - _, _, denied = _ensure_admin_menu_access('shop-manage') + role, current_row, denied = _ensure_admin_menu_access('shop-manage') if denied: return denied result, error_response, status = _proxy_backend_java( 'DELETE', f'/api/admin/shop-manages/{item_id}', + params={ + 'operatorId': current_row.get('id') if current_row else None, + 'superAdmin': 'true' if role == 'super_admin' else 'false', + }, ) if error_response is not None: return error_response, status @@ -1146,12 +1161,13 @@ def delete_shop_manage(item_id): @admin_api.route('/shop-manage-groups') @admin_required def list_shop_manage_groups(): - _, _, denied = _ensure_admin_menu_access('shop-manage', 'skip-price-asin') + role, current_row, denied = _ensure_admin_menu_access('shop-manage', 'skip-price-asin') if denied: return denied result, error_response, status = _proxy_backend_java( 'GET', '/api/admin/shop-manages/groups', + params={'createdById': current_row.get('id')} if role != 'super_admin' and current_row and current_row.get('id') else None, ) if error_response is not None: return error_response, status @@ -1170,11 +1186,14 @@ def list_shop_manage_groups(): @admin_api.route('/shop-manage-group', methods=['POST']) @admin_required def create_shop_manage_group(): - _, _, denied = _ensure_admin_menu_access('shop-manage', 'skip-price-asin') + _, current_row, denied = _ensure_admin_menu_access('shop-manage', 'skip-price-asin') if denied: return denied data = request.get_json() or {} - payload = {'groupName': (data.get('group_name') or '').strip()} + payload = { + 'groupName': (data.get('group_name') or '').strip(), + 'createdById': current_row.get('id') if current_row else None, + } result, error_response, status = _proxy_backend_java( 'POST', '/api/admin/shop-manages/groups', @@ -1198,7 +1217,7 @@ def create_shop_manage_group(): @admin_api.route('/shop-manage-group/', methods=['PUT']) @admin_required def update_shop_manage_group(item_id): - _, _, denied = _ensure_admin_menu_access('shop-manage', 'skip-price-asin') + role, current_row, denied = _ensure_admin_menu_access('shop-manage', 'skip-price-asin') if denied: return denied data = request.get_json() or {} @@ -1206,6 +1225,10 @@ def update_shop_manage_group(item_id): result, error_response, status = _proxy_backend_java( 'PUT', f'/api/admin/shop-manages/groups/{item_id}', + params={ + 'operatorId': current_row.get('id') if current_row else None, + 'superAdmin': 'true' if role == 'super_admin' else 'false', + }, json_data=payload, ) if error_response is not None: @@ -1302,7 +1325,7 @@ def create_skip_price_asin(): item = result.get('data') or {} return jsonify({ 'success': True, - 'msg': result.get('message') or '淇濆瓨鎴愬姛', + 'msg': result.get('message') or '保存成功', 'item': { 'id': item.get('id'), 'group_id': item.get('groupId'), @@ -1331,19 +1354,58 @@ def delete_skip_price_asin_country(item_id, country): ) if error_response is not None: return error_response, status - return jsonify({'success': True, 'msg': result.get('message') or '鍒犻櫎鎴愬姛'}) + return jsonify({'success': True, 'msg': result.get('message') or '删除成功'}) @admin_api.route('/shop-manage-group/', methods=['DELETE']) @admin_required def delete_shop_manage_group(item_id): - _, _, denied = _ensure_admin_menu_access('shop-manage', 'skip-price-asin') + role, current_row, denied = _ensure_admin_menu_access('shop-manage', 'skip-price-asin') if denied: return denied result, error_response, status = _proxy_backend_java( 'DELETE', f'/api/admin/shop-manages/groups/{item_id}', + params={ + 'operatorId': current_row.get('id') if current_row else None, + 'superAdmin': 'true' if role == 'super_admin' else 'false', + }, ) if error_response is not None: return error_response, status return jsonify({'success': True, 'msg': result.get('message') or '删除成功'}) + + +@admin_api.route('/skip-price-asin//country/', methods=['PUT']) +@admin_required +def update_skip_price_asin_country(item_id, country): + _, _, denied = _ensure_admin_menu_access('skip-price-asin') + if denied: + return denied + data = request.get_json() or {} + payload = {'asin': (data.get('asin') or '').strip()} + result, error_response, status = _proxy_backend_java( + 'PUT', + f'/api/admin/skip-price-asins/{item_id}/countries/{country}', + json_data=payload, + ) + if error_response is not None: + return error_response, status + item = result.get('data') or {} + return jsonify({ + 'success': True, + 'msg': result.get('message') or '保存成功', + 'item': { + 'id': item.get('id'), + 'group_id': item.get('groupId'), + 'group_name': item.get('groupName') or '', + 'shop_name': item.get('shopName') or '', + 'asin_de': item.get('asinDe') or '', + 'asin_uk': item.get('asinUk') or '', + 'asin_fr': item.get('asinFr') or '', + 'asin_it': item.get('asinIt') or '', + 'asin_es': item.get('asinEs') or '', + 'created_at': (item.get('createdAt') or '').replace('T', ' ')[:16], + 'updated_at': (item.get('updatedAt') or '').replace('T', ' ')[:16], + }, + }) diff --git a/backend/config.py b/backend/config.py index 6692838..97a3eb7 100644 --- a/backend/config.py +++ b/backend/config.py @@ -23,8 +23,8 @@ bucket_path = "nanri-image/" file_url_pre = f"https://{bucket}.oss-cn-hangzhou.aliyuncs.com/" import os -# backend_java_base_url = os.environ.get('BACKEND_JAVA_BASE_URL', 'http://127.0.0.1:18080').rstrip('/') -backend_java_base_url = os.environ.get('BACKEND_JAVA_BASE_URL', 'http://8.136.19.173:18080').rstrip('/') +backend_java_base_url = os.environ.get('BACKEND_JAVA_BASE_URL', 'http://127.0.0.1:18080').rstrip('/') +# backend_java_base_url = os.environ.get('BACKEND_JAVA_BASE_URL', 'http://8.136.19.173:18080').rstrip('/') os.environ['OSS_ACCESS_KEY_ID'] = accessKeyId os.environ['OSS_ACCESS_KEY_SECRET'] = accessKeySecret os.environ['SECRET_KEY'] = "ddffc7c1d02121d9554d7b080b2511b6" diff --git a/backend/web_source/admin.html b/backend/web_source/admin.html index d450729..1fc9406 100644 --- a/backend/web_source/admin.html +++ b/backend/web_source/admin.html @@ -1995,10 +1995,35 @@ if (!value) return '-'; return '