From ef0e0df0ac0f0d24f13bafda0d131a4c8d5a7603 Mon Sep 17 00:00:00 2001 From: super <2903208875@qq.com> Date: Fri, 17 Apr 2026 12:52:16 +0800 Subject: [PATCH] =?UTF-8?q?=E5=AE=8C=E6=88=90=E5=8C=B9=E9=85=8D=E3=80=81?= =?UTF-8?q?=E8=B7=9F=E4=BB=B7=E3=80=81=E6=9D=83=E9=99=90=E9=83=A8=E5=88=86?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/assets/convert.js | 2 +- app/assets/dedupe.js | 2 +- app/assets/delete-brand.js | 2 +- app/assets/split.js | 2 +- .../__pycache__/brand.cpython-312.pyc | Bin 35720 -> 35720 bytes app/new_web_source/convert.html | 4 +- app/new_web_source/dedupe.html | 4 +- app/new_web_source/delete-brand.html | 6 +- app/new_web_source/split.html | 4 +- .../aiimage-backend.log.2026-04-11.0.gz | Bin 73639 -> 0 bytes .../aiimage-backend.log.2026-04-12.0.gz | Bin 89796 -> 0 bytes .../aiimage-backend.log.2026-04-13.0.gz | Bin 48156 -> 0 bytes .../aiimage-backend.log.2026-04-16.0.gz | Bin 0 -> 29895 bytes .../nanri/aiimage/common/api/ApiResponse.java | 20 +- .../common/exception/BusinessException.java | 11 + .../exception/GlobalExceptionHandler.java | 4 +- .../common/util/ExcelStreamReader.java | 126 + .../config/DeleteBrandProgressProperties.java | 12 +- .../BrandTaskProgressCacheService.java | 67 +- .../brand/service/BrandTaskService.java | 1 + .../convert/service/ConvertRunService.java | 132 +- .../dedupe/service/DedupeRunService.java | 19 +- .../service/DedupeTotalDataService.java | 78 +- .../controller/DeleteBrandRunController.java | 6 + .../service/DeleteBrandRunService.java | 77 + .../service/DeleteBrandStaleTaskService.java | 77 +- .../service/DeleteBrandTaskCacheService.java | 78 +- .../dto/PermissionMenuCreateRequest.java | 2 + .../dto/PermissionMenuUpdateRequest.java | 2 + .../model/entity/AdminUserEntity.java | 4 + .../model/entity/PermissionMenuEntity.java | 1 + .../model/vo/PermissionMenuItemVo.java | 1 + .../PermissionMenuSchemaInitializer.java | 39 +- .../service/PermissionMenuService.java | 33 + .../controller/PriceTrackController.java | 21 +- .../dto/PriceTrackMatchShopsRequest.java | 17 +- .../dto/PriceTrackSubmitResultRequest.java | 54 +- .../model/vo/PriceTrackCreateTaskVo.java | 13 +- .../model/vo/PriceTrackMatchShopsVo.java | 32 +- .../PriceTrackExcelAssemblyService.java | 54 +- .../pricetrack/service/PriceTrackService.java | 24 +- .../service/PriceTrackTaskService.java | 261 +- .../ProductRiskResolveController.java | 6 + .../service/ProductRiskTaskService.java | 24 +- .../controller/ShopManageController.java | 50 +- .../controller/SkipPriceAsinController.java | 38 +- .../dto/ShopManageGroupCreateRequest.java | 6 +- .../dto/ShopManageGroupUpdateRequest.java | 5 + .../model/dto/SkipPriceAsinCreateRequest.java | 10 +- .../model/entity/ShopManageGroupEntity.java | 2 + .../model/vo/ShopManageGroupItemVo.java | 2 + .../service/ShopManageGroupService.java | 109 +- .../shopkey/service/ShopManageService.java | 39 +- .../shopkey/service/SkipPriceAsinService.java | 90 +- .../controller/ShopMatchController.java | 6 + .../service/ShopMatchTaskCacheService.java | 133 +- .../service/ShopMatchTaskService.java | 45 +- .../split/service/SplitRunService.java | 245 +- .../src/main/resources/application.yml | 12 +- .../resources/db/V24__price_track_tables.sql | 14 + .../db/V25__permission_menu_sort_order.sql | 6 + .../db/V25__shop_manage_group_bind_user.sql | 55 + backend/__pycache__/config.cpython-312.pyc | Bin 1510 -> 1510 bytes .../__pycache__/admin_api.cpython-312.pyc | Bin 67590 -> 84968 bytes backend/blueprints/admin_api.py | 446 ++- backend/utils/__pycache__/db.cpython-312.pyc | Bin 8233 -> 8671 bytes backend/utils/db.py | 8 + backend/web_source/_admin_extracted.js | 2337 +++++++++++++ backend/web_source/_tmp_prefix.js | 2000 +++++++++++ backend/web_source/admin.html | 2993 +++++++++++------ .../brand/components/BrandDeleteBrandTab.vue | 5 +- .../brand/components/BrandPriceTrackTab.vue | 229 +- .../brand/components/BrandProductRiskTab.vue | 13 +- .../brand/components/BrandShopMatchTab.vue | 62 +- frontend-vue/src/shared/api/java-modules.ts | 75 +- xlsx/40-1.xlsx | Bin 120232 -> 0 bytes xlsx/40-2.xlsx | Bin 132182 -> 0 bytes xlsx/40.xlsx | Bin 768354 -> 0 bytes xlsx/5.xlsx | Bin 812195 -> 0 bytes 79 files changed, 8871 insertions(+), 1486 deletions(-) delete mode 100644 backend-java/aiimage-backend.log.2026-04-11.0.gz delete mode 100644 backend-java/aiimage-backend.log.2026-04-12.0.gz delete mode 100644 backend-java/aiimage-backend.log.2026-04-13.0.gz create mode 100644 backend-java/aiimage-backend.log.2026-04-16.0.gz create mode 100644 backend-java/src/main/java/com/nanri/aiimage/common/util/ExcelStreamReader.java create mode 100644 backend-java/src/main/resources/db/V24__price_track_tables.sql create mode 100644 backend-java/src/main/resources/db/V25__permission_menu_sort_order.sql create mode 100644 backend-java/src/main/resources/db/V25__shop_manage_group_bind_user.sql create mode 100644 backend/web_source/_admin_extracted.js create mode 100644 backend/web_source/_tmp_prefix.js delete mode 100644 xlsx/40-1.xlsx delete mode 100644 xlsx/40-2.xlsx delete mode 100644 xlsx/40.xlsx delete mode 100644 xlsx/5.xlsx diff --git a/app/assets/convert.js b/app/assets/convert.js index 5785fc1..94e0332 100644 --- a/app/assets/convert.js +++ b/app/assets/convert.js @@ -1 +1 @@ -import{d as A,o as H,a as n,c as o,b as z,B as K,e as s,F as w,r as F,t as r,f as _,n as E,w as M,s as j,u as q,g as u,h as T,x as J,i as k,E as i,y as L,z as O,A as G,_ as Q,p as W,q as X}from"./pywebview-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"); +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-D4gpiFjY.js";import{e as Y}from"./brand-CXHemZwJ.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 3574d23..2f4ccf9 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-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"); +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-D4gpiFjY.js";import{e as se}from"./brand-CXHemZwJ.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 cebfc65..4aae9ce 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-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"); +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 as,i as G,E as p,M as ns,N as ls,O as rs,P as os,_ as us,p as is,q as cs}from"./pywebview-D4gpiFjY.js";import{e as ds}from"./brand-CXHemZwJ.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"],at={class:"files"},nt={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,a){if(!s?.length)return;const n=i.value.findIndex(c=>c.taskId===e),o=n>=0?i.value[n]: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:a??o?.queuePayloadText};n>=0?i.value[n]=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(a=>{a.taskId&&he(a.taskStatus)&&s.add(a.taskId)}),!s.size)return!1;const t=i.value.length;return i.value=i.value.filter(a=>!s.has(a.taskId)),i.value.length!==t?(ae(),!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 ae(){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 ne(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=ne(e);return t&&t.items.find(a=>M(a,s))||null}function pe(e){if(!e)return!1;const s=ne(e);if(!s)return!1;let t=!1;const a=g.value[e]?.line_progress?.info,n=P(e);if(a?.file_name&&a.current_line!=null&&a.total_lines!=null&&a.total_lines>0&&a.current_line>=a.total_lines){const o=s.items.find(r=>r.sourceFilename===a.file_name);o&&!o._completed&&(o._completed=!0,t=!0)}if(!n){const o=g.value[e]?.task?.status;if((o==="SUCCESS"||o==="FAILED")&&s.items.length===1){const r=s.items[0];r&&!r._completed&&(r._completed=!0,t=!0)}}return t&&ae(),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,a=_e(s),n=P(s),o=E(e);if(n){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||(a==="RUNNING"||a==="PENDING"||!a)&&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(n=>n.fileKey&&e.fileKey?n.fileKey===e.fileKey:n.sourceFilename&&e.sourceFilename?n.sourceFilename===e.sourceFilename:!1)||null}function ke(e){const s=e.taskId;if(!s)return"";const t=g.value[s],a=t?.line_progress?.info,n=le(e);if(P(s)){const u=R(s,e),c=t?.task?.status,_=!!(a?.file_name&&a.file_name===e.sourceFilename);let k=u?._completed?"COMPLETED":(u?._pushed,"PENDING"),m=0,w=Math.max(0,e.totalRows??n?.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,a?.current_line??0)),!w&&a?.total_lines&&a.total_lines>0&&(w=a.total_lines)):u?._completed?(k="COMPLETED",m=w):u?._pushed&&(k="PENDING",m=0);const I=[];if(I.push(`文件状态:${k}`),I.push(`文件进度:${m}/${w}`),a?.finished_files!=null){const C=a.file_total&&a.file_total>0?`/${a.file_total}`:"";I.push(`已完成文件:${a.finished_files}${C}`)}return a?.phase&&I.push(`阶段:${a.phase}`),I.join("|")}if(n){const u=[],c=n.processedRows??0,_=n.totalRows??0;if(n.status&&u.push(`文件状态:${n.status}`),u.push(`文件进度:${c}/${_}`),a?.finished_files!=null){const k=a.file_total&&a.file_total>0?`/${a.file_total}`:"";u.push(`已完成文件:${a.finished_files}${k}`)}return a?.phase&&u.push(`阶段:${a.phase}`),u.join("|")}if(!t?.line_progress?.has_progress||!a){const u=t?.task?.status;return u?`任务状态:${u}`:""}const r=[];if(a.phase&&r.push(`阶段:${a.phase}`),a.file_index&&a.file_total&&r.push(`文件:${a.file_index}/${a.file_total}`),a.file_name&&r.push(`当前文件:${a.file_name}`),a.current_line!=null&&a.total_lines!=null&&r.push(`${P(s)?"当前文件进度":"进度"}:${a.current_line}/${a.total_lines}`),a.current_country&&r.push(`国家:${a.current_country}`),a.current_asin&&r.push(`ASIN:${a.current_asin}`),a.finished_files!=null){const u=a.file_total&&a.file_total>0?`/${a.file_total}`:"";r.push(`已完成文件:${a.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 n=le(e);return n?Math.max(0,Math.min(100,n.percent??0)):t?t.current_line!=null&&t.total_lines&&t.total_lines>0?Math.max(0,Math.min(100,Math.round(t.current_line/t.total_lines*100))):t.finished_files!=null&&t.file_total&&t.file_total>0?Math.max(0,Math.min(100,Math.round(t.finished_files/t.file_total*100))):g.value[s]?.task?.status==="SUCCESS"?100:0:0}function 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||!ne(s))return"";const a=R(s,e),n=_e(s),o=g.value[s]?.line_progress,r=P(s);if(!r&&(n==="SUCCESS"||n==="FAILED"||e.taskStatus==="SUCCESS"||e.taskStatus==="FAILED"))return"已被全局判定为终态";if(a?._completed)return"本文件解析完毕";if(n==="RUNNING"&&o?.has_progress&&o?.info?.file_name===e.sourceFilename)return"处理中";if(r&&!a?._pushed&&!a?._completed)return"未入队";if(r&&n==="SUCCESS")return"本文件解析完毕";if(!r){const u=g.value[s];if(u?.items&&u.items.find(_=>M(_,e))&&n==="SUCCESS")return"本文件解析完毕"}return a?._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 a=e?.items||[];let n=!1;const o=t.items.map(r=>{const u=a.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)&&(n=!0),C});for(const r of a)o.some(u=>M(u,r))||(o.push({...r,_pushed:!1,_completed:!1}),n=!0);return JSON.stringify(o)!==JSON.stringify(t.items)&&(t.items=o,n=!0),n&&ae(),n}async function re(e){const s=e||L();if(s.length)try{const t=await as(s);let a=!1;for(const n of t.items||[]){const o=n?.task?.id;typeof o=="number"&&o>0&&(g.value[o]=n,Ke(n)&&(a=!0),pe(o)&&(a=!0))}a&&x()}catch{}}function He(){return document.visibilityState==="visible"?6e3:2e4}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 a=!1,n=!1;try{await re(t),await $e();for(const o of t)we(o)&&(ve(o),a=!0,n=!0)}finally{ee.value=!1}n?await O():a&&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 a of e){const n=typeof a=="string"?a:a.absolutePath,o=typeof a=="string"?void 0:a.relativePath,r=await s.upload_file_to_java(n,o);if(!r?.success||!r.data)throw new Error(r?.error||r?.message||`上传失败:${n}`);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(a=>a.relativePath||a.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 ns({files:j.value.map(n=>({fileKey:n.fileKey,originalFilename:n.originalFilename,relativePath:n.relativePath}))}),s=ge(e.items||[]),t=s.some(n=>n.matchStatus==="MATCHED"),a=s.some(n=>n.matchStatus&&n.matchStatus!=="MATCHED");b.value=t?"解析完成,可在左侧点击“推送到 Python 队列”开始串行处理":"解析完成,当前没有可推送的已匹配文件",S.value="",s.length&&s[0].taskId&&te(s[0].taskId,s,b.value,S.value),x(),a?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(),a=ls(s),n=e.outputFilename||e.sourceFilename||`delete-brand_${s}.xlsx`;if(t?.save_file_from_url_new){const o=await t.save_file_from_url_new(a,n);o.success?p.success(`已保存:${o.path||n}`):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(a=>a.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(a=>`${a.country}:${a.asin}${a.status?`(${a.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(),a=e.taskId;if(!a)return{success:!1,retryable:!1};const n=i.value.find(c=>c.taskId===a);if(!n)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:a,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(a,e);return c&&(c._pushed=!0,c._completed=!1),D.value=K(e),te(a,n.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(a,n.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(a=>K(a)===e);if(t){const a=t.taskId,n=P(a);n&&(pe(a),E(t),void 0);const o=E(t);if(o==="已入队"||o==="处理中"){t.sourceFilename;return}if(n){const r=R(a,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 a of e)we(a)&&(ve(a),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:a=>Te(t)}," 下载 ",8,Ws)):h("",!0),t.resultId?(d(),f("button",{key:1,type:"button",class:"btn-delete",onClick:a=>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",at,"店铺名:"+v(t.shopName||"-"),1),l("div",nt,"匹配结果:"+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:a=>Te(t)}," 下载 ",8,pt)):h("",!0),t.resultId?(d(),f("button",{key:1,type:"button",class:"btn-delete",onClick:a=>Fe(t.resultId)}," 删除 ",8,_t)):h("",!0)])]))),128))])])):h("",!0)],64)):(d(),f("div",$s," 暂无删除品牌结果,完成解析后会在这里展示按国家分组的去重结果 "))])])])])]))}}),gt=us(yt,[["__scopeId","data-v-4a6c870e"]]);is(gt).use(cs).mount("#app"); diff --git a/app/assets/split.js b/app/assets/split.js index 94ea5cf..493db88 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-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"); +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-D4gpiFjY.js";import{e as as}from"./brand-CXHemZwJ.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/blueprints/__pycache__/brand.cpython-312.pyc b/app/blueprints/__pycache__/brand.cpython-312.pyc index 2fef1f49d257e0cbc1ceffe9abdfb9bf8346ec55..406fa564102af5abb321eb10829ea4556ef7b6a0 100644 GIT binary patch delta 22 ccmeB}&eSoTiTgA!FBbz41mAtIk-N4V087#b5dZ)H delta 22 ccmeB}&eSoTiTgA!FBbz4v`C-b$X(kF07yOtZ2$lO diff --git a/app/new_web_source/convert.html b/app/new_web_source/convert.html index dd3590c..6907a43 100644 --- a/app/new_web_source/convert.html +++ b/app/new_web_source/convert.html @@ -5,8 +5,8 @@ 格式转换 - 数富AI - - + + diff --git a/app/new_web_source/dedupe.html b/app/new_web_source/dedupe.html index 419ddc9..57060f5 100644 --- a/app/new_web_source/dedupe.html +++ b/app/new_web_source/dedupe.html @@ -5,8 +5,8 @@ 数据去重 - 数富AI - - + + diff --git a/app/new_web_source/delete-brand.html b/app/new_web_source/delete-brand.html index a9f1ac1..481cfec 100644 --- a/app/new_web_source/delete-brand.html +++ b/app/new_web_source/delete-brand.html @@ -5,10 +5,10 @@ 删除品牌 - 数富AI - - + + - +
diff --git a/app/new_web_source/split.html b/app/new_web_source/split.html index 6e0fdd4..c053338 100644 --- a/app/new_web_source/split.html +++ b/app/new_web_source/split.html @@ -5,8 +5,8 @@ 数据拆分 - 数富AI - - + + diff --git a/backend-java/aiimage-backend.log.2026-04-11.0.gz b/backend-java/aiimage-backend.log.2026-04-11.0.gz deleted file mode 100644 index 7617170bcbb368f0c2dd15d2b582751ab5e4581e..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 73639 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

2)0PD3J-IjNNdz5 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%0A!_@PkjDWW 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 Ot^I$rADR_0A`Ji{ta>E? diff --git a/backend-java/aiimage-backend.log.2026-04-13.0.gz b/backend-java/aiimage-backend.log.2026-04-13.0.gz deleted file mode 100644 index a6fe2299c55594c35a1425e9d6dd123b78d44e10..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 48156 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 diff --git a/backend-java/aiimage-backend.log.2026-04-16.0.gz b/backend-java/aiimage-backend.log.2026-04-16.0.gz new file mode 100644 index 0000000000000000000000000000000000000000..029fbc595d8b9d61300fe527e839732d33b13d5f GIT binary patch literal 29895 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 WKR?|aeu=Ml4^*cIzpug29{eA3?EPi{ literal 0 HcmV?d00001 diff --git a/backend-java/src/main/java/com/nanri/aiimage/common/api/ApiResponse.java b/backend-java/src/main/java/com/nanri/aiimage/common/api/ApiResponse.java index a2db9aa..81bd4a7 100644 --- a/backend-java/src/main/java/com/nanri/aiimage/common/api/ApiResponse.java +++ b/backend-java/src/main/java/com/nanri/aiimage/common/api/ApiResponse.java @@ -1,13 +1,11 @@ package com.nanri.aiimage.common.api; import io.swagger.v3.oas.annotations.media.Schema; -import lombok.AllArgsConstructor; import lombok.Data; import lombok.NoArgsConstructor; @Data @NoArgsConstructor -@AllArgsConstructor @Schema(description = "统一响应体") public class ApiResponse { @@ -20,6 +18,20 @@ public class ApiResponse { @Schema(description = "响应数据") private T data; + @Schema(description = "业务错误码,成功时可为空") + private Integer code; + + public ApiResponse(boolean success, String message, T data) { + this(success, message, data, null); + } + + public ApiResponse(boolean success, String message, T data, Integer code) { + this.success = success; + this.message = message; + this.data = data; + this.code = code; + } + public static ApiResponse success(T data) { return new ApiResponse<>(true, "操作成功", data); } @@ -31,4 +43,8 @@ public class ApiResponse { public static ApiResponse fail(String message) { return new ApiResponse<>(false, message, null); } + + public static ApiResponse fail(Integer code, String message) { + return new ApiResponse<>(false, message, null, code); + } } diff --git a/backend-java/src/main/java/com/nanri/aiimage/common/exception/BusinessException.java b/backend-java/src/main/java/com/nanri/aiimage/common/exception/BusinessException.java index 0240697..a4d039a 100644 --- a/backend-java/src/main/java/com/nanri/aiimage/common/exception/BusinessException.java +++ b/backend-java/src/main/java/com/nanri/aiimage/common/exception/BusinessException.java @@ -2,7 +2,18 @@ package com.nanri.aiimage.common.exception; public class BusinessException extends RuntimeException { + private final Integer code; + public BusinessException(String message) { + this(null, message); + } + + public BusinessException(Integer code, String message) { super(message); + this.code = code; + } + + public Integer getCode() { + return code; } } diff --git a/backend-java/src/main/java/com/nanri/aiimage/common/exception/GlobalExceptionHandler.java b/backend-java/src/main/java/com/nanri/aiimage/common/exception/GlobalExceptionHandler.java index 3512bc8..73d65c6 100644 --- a/backend-java/src/main/java/com/nanri/aiimage/common/exception/GlobalExceptionHandler.java +++ b/backend-java/src/main/java/com/nanri/aiimage/common/exception/GlobalExceptionHandler.java @@ -13,7 +13,9 @@ public class GlobalExceptionHandler { @ExceptionHandler(BusinessException.class) public ApiResponse handleBusinessException(BusinessException ex) { - return ApiResponse.fail(ex.getMessage()); + return ex.getCode() == null + ? ApiResponse.fail(ex.getMessage()) + : ApiResponse.fail(ex.getCode(), ex.getMessage()); } @ExceptionHandler(MethodArgumentNotValidException.class) diff --git a/backend-java/src/main/java/com/nanri/aiimage/common/util/ExcelStreamReader.java b/backend-java/src/main/java/com/nanri/aiimage/common/util/ExcelStreamReader.java new file mode 100644 index 0000000..77725f9 --- /dev/null +++ b/backend-java/src/main/java/com/nanri/aiimage/common/util/ExcelStreamReader.java @@ -0,0 +1,126 @@ +package com.nanri.aiimage.common.util; + +import com.alibaba.excel.EasyExcel; +import com.alibaba.excel.context.AnalysisContext; +import com.alibaba.excel.event.AnalysisEventListener; + +import java.io.File; +import java.io.FileInputStream; +import java.io.IOException; +import java.io.InputStream; +import java.util.LinkedHashMap; +import java.util.Map; + +public final class ExcelStreamReader { + + private ExcelStreamReader() { + } + + public static void readFirstSheet(File file, SheetRowHandler handler) throws IOException { + try (InputStream inputStream = new FileInputStream(file)) { + readFirstSheet(inputStream, handler); + } + } + + public static void readFirstSheet(InputStream inputStream, SheetRowHandler handler) throws IOException { + runRead(inputStream, false, handler); + } + + public static void readAllSheets(File file, SheetRowHandler handler) throws IOException { + try (InputStream inputStream = new FileInputStream(file)) { + readAllSheets(inputStream, handler); + } + } + + public static void readAllSheets(InputStream inputStream, SheetRowHandler handler) throws IOException { + runRead(inputStream, true, handler); + } + + private static void runRead(InputStream inputStream, boolean readAllSheets, SheetRowHandler handler) throws IOException { + StreamingListener listener = new StreamingListener(handler); + try { + if (readAllSheets) { + EasyExcel.read(inputStream, listener).headRowNumber(1).doReadAll(); + } else { + EasyExcel.read(inputStream, listener).sheet(0).headRowNumber(1).doRead(); + } + } catch (WrappedRuntimeException ex) { + if (ex.getCause() instanceof IOException ioException) { + throw ioException; + } + throw ex; + } + } + + public interface SheetRowHandler { + default void onHeader(String sheetName, Integer sheetNo, Map headerMap) throws Exception { + } + + void onRow(String sheetName, Integer sheetNo, int rowIndex, Map headerMap, Map rowMap) throws Exception; + } + + private static final class StreamingListener extends AnalysisEventListener> { + + private final SheetRowHandler handler; + private Map currentHeaderMap = Map.of(); + + private StreamingListener(SheetRowHandler handler) { + this.handler = handler; + } + + @Override + public void invokeHeadMap(Map headMap, AnalysisContext context) { + Map normalizedHeadMap = new LinkedHashMap<>(); + for (Map.Entry entry : headMap.entrySet()) { + normalizedHeadMap.put(entry.getKey(), entry.getValue()); + } + currentHeaderMap = normalizedHeadMap; + try { + handler.onHeader(sheetName(context), sheetNo(context), currentHeaderMap); + } catch (RuntimeException ex) { + throw ex; + } catch (Exception ex) { + throw new WrappedRuntimeException(ex); + } + } + + @Override + public void invoke(Map data, AnalysisContext context) { + Map normalizedRow = new LinkedHashMap<>(); + for (Map.Entry entry : data.entrySet()) { + normalizedRow.put(entry.getKey(), entry.getValue()); + } + try { + handler.onRow( + sheetName(context), + sheetNo(context), + context.readRowHolder().getRowIndex(), + currentHeaderMap, + normalizedRow + ); + } catch (RuntimeException ex) { + throw ex; + } catch (Exception ex) { + throw new WrappedRuntimeException(ex); + } + } + + @Override + public void doAfterAllAnalysed(AnalysisContext context) { + } + + private String sheetName(AnalysisContext context) { + return context.readSheetHolder() == null ? "" : context.readSheetHolder().getSheetName(); + } + + private Integer sheetNo(AnalysisContext context) { + return context.readSheetHolder() == null ? null : context.readSheetHolder().getSheetNo(); + } + } + + private static final class WrappedRuntimeException extends RuntimeException { + private WrappedRuntimeException(Throwable cause) { + super(cause); + } + } +} 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 b3500b0..b7e4a78 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 @@ -18,18 +18,18 @@ public class DeleteBrandProgressProperties { * 商品风险(PRODUCT_RISK_RESOLVE)RUNNING 超时自动收尾/失败,按分钟计算; * 与删除品牌共用同一定时调度。 */ - private long productRiskStaleTimeoutMinutes = 10; - private long productRiskInitialTimeoutMinutes = 10; + private long productRiskStaleTimeoutMinutes = 20; + private long productRiskInitialTimeoutMinutes = 20; /** * Price track RUNNING timeout auto-finalize/fail threshold in minutes. */ - private long priceTrackStaleTimeoutMinutes = 10; - private long priceTrackInitialTimeoutMinutes = 10; + private long priceTrackStaleTimeoutMinutes = 20; + private long priceTrackInitialTimeoutMinutes = 20; /** * Shop match RUNNING timeout auto-finalize/fail threshold in minutes. */ - private long shopMatchStaleTimeoutMinutes = 10; - private long shopMatchInitialTimeoutMinutes = 10; + private long shopMatchStaleTimeoutMinutes = 20; + private long shopMatchInitialTimeoutMinutes = 20; } diff --git a/backend-java/src/main/java/com/nanri/aiimage/modules/brand/service/BrandTaskProgressCacheService.java b/backend-java/src/main/java/com/nanri/aiimage/modules/brand/service/BrandTaskProgressCacheService.java index b5bdd07..b7d0612 100644 --- a/backend-java/src/main/java/com/nanri/aiimage/modules/brand/service/BrandTaskProgressCacheService.java +++ b/backend-java/src/main/java/com/nanri/aiimage/modules/brand/service/BrandTaskProgressCacheService.java @@ -9,7 +9,11 @@ import com.nanri.aiimage.modules.brand.model.dto.BrandFileAggregateCacheDto; import org.springframework.data.redis.core.StringRedisTemplate; import org.springframework.stereotype.Service; +import java.io.IOException; import java.nio.charset.StandardCharsets; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.StandardOpenOption; import java.security.MessageDigest; import java.time.Duration; import java.time.Instant; @@ -94,21 +98,28 @@ public class BrandTaskProgressCacheService { public void saveParsedPayload(Long taskId, Object payload) { try { - stringRedisTemplate.opsForValue().set(buildPayloadKey(taskId), objectMapper.writeValueAsString(payload), ttl()); + Files.createDirectories(buildTaskDir(taskId)); + Files.writeString( + buildPayloadFile(taskId), + objectMapper.writeValueAsString(payload), + StandardOpenOption.CREATE, + StandardOpenOption.TRUNCATE_EXISTING, + StandardOpenOption.WRITE + ); } catch (Exception ex) { - throw new BusinessException("暂存品牌原始数据失败"); + throw new BusinessException("鏆傚瓨鍝佺墝鍘熷鏁版嵁澶辫触"); } } public T getParsedPayload(Long taskId, TypeReference typeReference) { - String raw = stringRedisTemplate.opsForValue().get(buildPayloadKey(taskId)); - if (raw == null || raw.isBlank()) { + Path payloadFile = buildPayloadFile(taskId); + if (!Files.isRegularFile(payloadFile)) { return null; } try { - return objectMapper.readValue(raw, typeReference); + return objectMapper.readValue(Files.readString(payloadFile), typeReference); } catch (Exception ex) { - throw new BusinessException("读取品牌原始数据失败"); + throw new BusinessException("璇诲彇鍝佺墝鍘熷鏁版嵁澶辫触"); } } @@ -126,7 +137,7 @@ public class BrandTaskProgressCacheService { int finishedFiles = countCompletedFiles(taskId); return new ChunkStoreResult(false, false, finishedFiles, aggregate); } - throw new BusinessException("同一分片重复提交但内容不一致: " + fileUrl + "#" + file.getChunkIndex()); + throw new BusinessException("鍚屼竴鍒嗙墖閲嶅鎻愪氦浣嗗唴瀹逛笉涓€鑷? " + fileUrl + "#" + file.getChunkIndex()); } BrandFileAggregateCacheDto aggregate = getFileAggregate(taskId, fileUrl); @@ -174,7 +185,7 @@ public class BrandTaskProgressCacheService { try { return objectMapper.readValue(json, BrandFileAggregateCacheDto.class); } catch (Exception ex) { - throw new BusinessException("读取品牌文件聚合缓存失败"); + throw new BusinessException("璇诲彇鍝佺墝鏂囦欢鑱氬悎缂撳瓨澶辫触"); } } @@ -209,11 +220,11 @@ public class BrandTaskProgressCacheService { public void delete(Long taskId) { stringRedisTemplate.delete(buildKey(taskId)); - stringRedisTemplate.delete(buildPayloadKey(taskId)); stringRedisTemplate.delete(buildFileAggregateKey(taskId)); stringRedisTemplate.delete(buildCompletedFilesKey(taskId)); stringRedisTemplate.delete(buildFinalizeLockKey(taskId)); stringRedisTemplate.delete(buildLegacyResultKey(taskId)); + deleteTaskDirQuietly(taskId); } public void deleteFileState(Long taskId, String fileUrl) { @@ -236,19 +247,19 @@ public class BrandTaskProgressCacheService { stringRedisTemplate.opsForHash().put(buildFileAggregateKey(taskId), normalizeFileUrl(aggregate.getFileUrl()), objectMapper.writeValueAsString(aggregate)); refreshTaskKey(buildFileAggregateKey(taskId)); } catch (Exception ex) { - throw new BusinessException("暂存品牌文件聚合结果失败"); + throw new BusinessException("鏆傚瓨鍝佺墝鏂囦欢鑱氬悎缁撴灉澶辫触"); } } private void validateChunk(BrandCrawlResultFileDto file) { if (file == null) { - throw new BusinessException("分片不能为空"); + throw new BusinessException("鍒嗙墖涓嶈兘涓虹┖"); } if (file.getChunkIndex() == null || file.getChunkTotal() == null || file.getChunkIndex() <= 0 || file.getChunkTotal() <= 0 || file.getChunkIndex() > file.getChunkTotal()) { - throw new BusinessException("分片参数不合法"); + throw new BusinessException("鍒嗙墖鍙傛暟涓嶅悎娉?"); } if (file.getFileUrl() == null || file.getFileUrl().isBlank()) { - throw new BusinessException("fileUrl 不能为空"); + throw new BusinessException("fileUrl 涓嶈兘涓虹┖"); } } @@ -270,7 +281,7 @@ public class BrandTaskProgressCacheService { private void ensureChunkConsistency(BrandFileAggregateCacheDto aggregate, BrandCrawlResultFileDto file) { if (aggregate.getChunkTotal() != null && file.getChunkTotal() != null && !aggregate.getChunkTotal().equals(file.getChunkTotal())) { - throw new BusinessException("同一文件的 chunkTotal 不一致: " + aggregate.getFileUrl()); + throw new BusinessException("鍚屼竴鏂囦欢鐨?chunkTotal 涓嶄竴鑷? " + aggregate.getFileUrl()); } if (aggregate.getChunkTotal() == null) { aggregate.setChunkTotal(file.getChunkTotal()); @@ -322,7 +333,7 @@ public class BrandTaskProgressCacheService { } return sb.toString(); } catch (Exception ex) { - throw new BusinessException("计算品牌结果分片摘要失败"); + throw new BusinessException("璁$畻鍝佺墝缁撴灉鍒嗙墖鎽樿澶辫触"); } } @@ -340,8 +351,28 @@ public class BrandTaskProgressCacheService { return Duration.ofHours(brandProgressProperties.getTtlHours()); } - private String buildPayloadKey(Long taskId) { - return "brand:task:parsed-payload:" + taskId; + private Path buildTaskDir(Long taskId) { + return Path.of(System.getProperty("java.io.tmpdir"), "brand-task-cache", String.valueOf(taskId)); + } + + private Path buildPayloadFile(Long taskId) { + return buildTaskDir(taskId).resolve("parsed-payload.json"); + } + + private void deleteTaskDirQuietly(Long taskId) { + Path taskDir = buildTaskDir(taskId); + if (!Files.exists(taskDir)) { + return; + } + try (var walk = Files.walk(taskDir)) { + walk.sorted(java.util.Comparator.reverseOrder()).forEach(path -> { + try { + Files.deleteIfExists(path); + } catch (IOException ignored) { + } + }); + } catch (IOException ignored) { + } } private String buildFileAggregateKey(Long taskId) { @@ -383,7 +414,7 @@ public class BrandTaskProgressCacheService { } return sb.toString(); } catch (Exception ex) { - throw new BusinessException("生成文件缓存键失败"); + throw new BusinessException("鐢熸垚鏂囦欢缂撳瓨閿け璐?"); } } diff --git a/backend-java/src/main/java/com/nanri/aiimage/modules/brand/service/BrandTaskService.java b/backend-java/src/main/java/com/nanri/aiimage/modules/brand/service/BrandTaskService.java index 8b8dbc1..f9e7067 100644 --- a/backend-java/src/main/java/com/nanri/aiimage/modules/brand/service/BrandTaskService.java +++ b/backend-java/src/main/java/com/nanri/aiimage/modules/brand/service/BrandTaskService.java @@ -327,6 +327,7 @@ public class BrandTaskService { if (deleted == 0) { throw new BusinessException("任务不存在或正在执行中无法删除"); } + brandTaskProgressCacheService.delete(taskId); } public String resolveDownloadUrl(Long taskId) { diff --git a/backend-java/src/main/java/com/nanri/aiimage/modules/convert/service/ConvertRunService.java b/backend-java/src/main/java/com/nanri/aiimage/modules/convert/service/ConvertRunService.java index b524dde..65a01e6 100644 --- a/backend-java/src/main/java/com/nanri/aiimage/modules/convert/service/ConvertRunService.java +++ b/backend-java/src/main/java/com/nanri/aiimage/modules/convert/service/ConvertRunService.java @@ -7,6 +7,7 @@ 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.common.util.ExcelStreamReader; import com.nanri.aiimage.config.StorageProperties; import com.nanri.aiimage.modules.convert.model.dto.ConvertRunRequest; import com.nanri.aiimage.modules.convert.model.dto.UploadedSourceFileDto; @@ -19,15 +20,10 @@ 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 lombok.RequiredArgsConstructor; -import org.apache.poi.ss.usermodel.DataFormatter; -import org.apache.poi.ss.usermodel.Row; -import org.apache.poi.ss.usermodel.Sheet; -import org.apache.poi.ss.usermodel.Workbook; -import org.apache.poi.ss.usermodel.WorkbookFactory; import org.springframework.stereotype.Service; +import java.io.BufferedWriter; import java.io.File; -import java.io.FileInputStream; import java.io.FileOutputStream; import java.io.IOException; import java.nio.charset.StandardCharsets; @@ -242,14 +238,30 @@ public class ConvertRunService { private List generateOutputFiles(File inputFile, ConvertTemplateEntity templateEntity) throws IOException { List outputFilenames = resolveOutputFilenames(templateEntity); - List rows = buildTxtRows(inputFile, templateEntity); File outputDir = FileUtil.mkdir(FileUtil.file(storageProperties.getLocalTempDir(), "convert-result")); - List generatedFiles = new ArrayList<>(); + Map writers = new LinkedHashMap<>(); for (String outputFilename : outputFilenames) { File outputFile = buildNamedOutputFile(outputDir, outputFilename); - FileUtil.writeLines(rows, outputFile, StandardCharsets.UTF_8); generatedFiles.add(new GeneratedConvertFile(outputFilename, outputFile)); + writers.put(outputFilename, Files.newBufferedWriter(outputFile.toPath(), StandardCharsets.UTF_8)); + } + try { + streamTxtRowsToOutputs(inputFile, templateEntity, writers); + } finally { + IOException closeException = null; + for (BufferedWriter writer : writers.values()) { + try { + writer.close(); + } catch (IOException ex) { + if (closeException == null) { + closeException = ex; + } + } + } + if (closeException != null) { + throw closeException; + } } return generatedFiles; } @@ -266,7 +278,9 @@ public class ConvertRunService { return List.of(outputFilename); } - private List buildTxtRows(File inputFile, ConvertTemplateEntity templateEntity) throws IOException { + private void streamTxtRowsToOutputs(File inputFile, + ConvertTemplateEntity templateEntity, + Map writers) throws IOException { List requiredColumns = readStringList(templateEntity.getRequiredSourceColumnsJson()); List headerColumns = readStringList(templateEntity.getHeaderColumnsJson()); Map fieldMapping = readStringMap(templateEntity.getFieldMappingJson()); @@ -275,65 +289,60 @@ public class ConvertRunService { int blankHeaderRowsAfterSchema = templateEntity.getBlankHeaderRowsAfterSchema() == null ? 0 : templateEntity.getBlankHeaderRowsAfterSchema(); + long batchId = System.currentTimeMillis(); + int[] rowIndex = new int[]{1}; + boolean[] headerRead = new boolean[]{false}; + Map[] headerMapHolder = new Map[]{null}; - DataFormatter formatter = new DataFormatter(); - try (FileInputStream fis = new FileInputStream(inputFile); - Workbook workbook = WorkbookFactory.create(fis)) { - Sheet sheet = workbook.getSheetAt(0); - Row headerRow = sheet.getRow(0); - if (headerRow == null) { - throw new BusinessException("Excel header row is empty."); - } - - Map headerMap = new LinkedHashMap<>(); - for (int i = 0; i < headerRow.getLastCellNum(); i++) { - String value = normalizeCellText(formatter.formatCellValue(headerRow.getCell(i))); - if (!value.isBlank() && !headerMap.containsKey(value)) { - headerMap.put(value, i); + ExcelStreamReader.readFirstSheet(inputFile, new ExcelStreamReader.SheetRowHandler() { + @Override + public void onHeader(String sheetName, Integer sheetNo, Map headerMap) throws Exception { + Map resolvedHeaderMap = new LinkedHashMap<>(); + for (Map.Entry entry : headerMap.entrySet()) { + String value = normalizeCellText(entry.getValue()); + if (!value.isBlank() && !resolvedHeaderMap.containsKey(value)) { + resolvedHeaderMap.put(value, entry.getKey()); + } + } + List missing = requiredColumns.stream() + .filter(column -> !resolvedHeaderMap.containsKey(column)) + .toList(); + if (!missing.isEmpty()) { + throw new BusinessException("Missing required columns: " + String.join(", ", missing)); + } + headerRead[0] = true; + headerMapHolder[0] = resolvedHeaderMap; + for (String line : preambleLines) { + writeLineToAll(writers, line); + } + writeLineToAll(writers, String.join("\t", headerColumns)); + for (int i = 0; i < blankHeaderRowsAfterSchema; i++) { + writeLineToAll(writers, "\t".repeat(Math.max(0, headerColumns.size() - 1))); } } - List missing = requiredColumns.stream() - .filter(column -> !headerMap.containsKey(column)) - .toList(); - if (!missing.isEmpty()) { - throw new BusinessException("Missing required columns: " + String.join(", ", missing)); - } - - List rows = new ArrayList<>(); - rows.addAll(preambleLines); - rows.add(String.join("\t", headerColumns)); - for (int i = 0; i < blankHeaderRowsAfterSchema; i++) { - rows.add("\t".repeat(Math.max(0, headerColumns.size() - 1))); - } - - long batchId = System.currentTimeMillis(); - int rowIndex = 1; - int asinIndex = headerMap.getOrDefault("ASIN", -1); - for (int rowNum = 1; rowNum <= sheet.getLastRowNum(); rowNum++) { - Row row = sheet.getRow(rowNum); - if (row == null) { - continue; + @Override + public void onRow(String sheetName, Integer sheetNo, int excelRowIndex, Map headerMap, Map rowMap) throws Exception { + Map resolvedHeaderMap = headerMapHolder[0]; + if (resolvedHeaderMap == null) { + throw new BusinessException("Excel header row is empty."); } - - String asin = asinIndex >= 0 ? normalizeCellText(formatter.formatCellValue(row.getCell(asinIndex))) : ""; + int asinIndex = resolvedHeaderMap.getOrDefault("ASIN", -1); + String asin = asinIndex >= 0 ? normalizeCellText(rowMap.get(asinIndex)) : ""; if (asin.isBlank()) { - continue; + return; } List lineValues = new ArrayList<>(); for (String column : headerColumns) { if ("sku".equals(column)) { - lineValues.add(batchId + "-" + rowIndex); + lineValues.add(batchId + "-" + rowIndex[0]); continue; } if (fieldMapping.containsKey(column)) { String sourceColumn = fieldMapping.get(column); - Integer sourceIndex = headerMap.get(sourceColumn); - String value = sourceIndex == null - ? "" - : normalizeCellText(formatter.formatCellValue(row.getCell(sourceIndex))); - lineValues.add(value); + Integer sourceIndex = resolvedHeaderMap.get(sourceColumn); + lineValues.add(sourceIndex == null ? "" : normalizeCellText(rowMap.get(sourceIndex))); continue; } if (defaults.containsKey(column)) { @@ -343,10 +352,19 @@ public class ConvertRunService { lineValues.add(""); } - rows.add(String.join("\t", lineValues)); - rowIndex++; + writeLineToAll(writers, String.join("\t", lineValues)); + rowIndex[0]++; } - return rows; + }); + if (!headerRead[0]) { + throw new BusinessException("Excel header row is empty."); + } + } + + private void writeLineToAll(Map writers, String line) throws IOException { + for (BufferedWriter writer : writers.values()) { + writer.write(line); + writer.newLine(); } } diff --git a/backend-java/src/main/java/com/nanri/aiimage/modules/dedupe/service/DedupeRunService.java b/backend-java/src/main/java/com/nanri/aiimage/modules/dedupe/service/DedupeRunService.java index c82eff7..11a676a 100644 --- a/backend-java/src/main/java/com/nanri/aiimage/modules/dedupe/service/DedupeRunService.java +++ b/backend-java/src/main/java/com/nanri/aiimage/modules/dedupe/service/DedupeRunService.java @@ -20,7 +20,7 @@ import org.apache.poi.ss.usermodel.DataFormatter; import org.apache.poi.ss.usermodel.Row; import org.apache.poi.ss.usermodel.Sheet; import org.apache.poi.ss.usermodel.Workbook; -import org.apache.poi.xssf.usermodel.XSSFWorkbook; +import org.apache.poi.xssf.streaming.SXSSFWorkbook; import org.springframework.core.io.FileSystemResource; import org.springframework.core.io.Resource; import org.springframework.stereotype.Service; @@ -232,7 +232,7 @@ public class DedupeRunService { DataFormatter formatter = new DataFormatter(); try (FileInputStream fis = new FileInputStream(inputFile); Workbook workbook = org.apache.poi.ss.usermodel.WorkbookFactory.create(fis); - XSSFWorkbook outputWorkbook = new XSSFWorkbook()) { + SXSSFWorkbook outputWorkbook = new SXSSFWorkbook(200)) { Sheet sheet = workbook.getSheetAt(0); Row headerRow = sheet.getRow(0); if (headerRow == null) { @@ -275,7 +275,6 @@ public class DedupeRunService { // The preliminary global scan for mainIdHasSubIdMap was removed. // We now determine subset existence contextually (per group) during the main loop. } - List candidateRows = new ArrayList<>(); Set candidateAsinValues = new HashSet<>(); for (int rowNum = 1; rowNum <= sheet.getLastRowNum(); rowNum++) { Row row = sheet.getRow(rowNum); @@ -288,7 +287,6 @@ public class DedupeRunService { continue; } } - candidateRows.add(row); if (asinColumnIndex != null) { String asinValue = dedupeTotalDataService.normalizeComparableValueOrBlank(formatter.formatCellValue(row.getCell(asinColumnIndex))); if (!asinValue.isBlank()) { @@ -300,7 +298,17 @@ public class DedupeRunService { Set matchedAsinValues = dedupeTotalDataService.findExistingComparableValues(candidateAsinValues); Set writtenAsinValues = new HashSet<>(); int outputRowIndex = 1; - for (Row row : candidateRows) { + for (int rowNum = 1; rowNum <= sheet.getLastRowNum(); rowNum++) { + Row row = sheet.getRow(rowNum); + if (row == null) { + continue; + } + if (idColumnIndex != null) { + String idValue = normalizeCellText(formatter.formatCellValue(row.getCell(idColumnIndex))); + if (!shouldKeepId(idValue, keepIntegerIds, keepUnderscoreIds, keepIntegerMainIdsWhenNoSubIds, sheet, rowNum, idColumnIndex, formatter)) { + continue; + } + } if (asinColumnIndex != null) { String asinValue = dedupeTotalDataService.normalizeComparableValueOrBlank(formatter.formatCellValue(row.getCell(asinColumnIndex))); if (!asinValue.isBlank()) { @@ -324,6 +332,7 @@ public class DedupeRunService { try (FileOutputStream fos = new FileOutputStream(outputFile)) { outputWorkbook.write(fos); } + outputWorkbook.dispose(); } } diff --git a/backend-java/src/main/java/com/nanri/aiimage/modules/dedupe/service/DedupeTotalDataService.java b/backend-java/src/main/java/com/nanri/aiimage/modules/dedupe/service/DedupeTotalDataService.java index 33b623c..763b53e 100644 --- a/backend-java/src/main/java/com/nanri/aiimage/modules/dedupe/service/DedupeTotalDataService.java +++ b/backend-java/src/main/java/com/nanri/aiimage/modules/dedupe/service/DedupeTotalDataService.java @@ -23,7 +23,9 @@ import org.springframework.transaction.annotation.Transactional; import org.springframework.web.multipart.MultipartFile; import java.io.ByteArrayInputStream; +import java.io.File; import java.io.InputStream; +import java.nio.file.Files; import java.util.Collection; import java.util.HashMap; import java.util.HashSet; @@ -114,9 +116,9 @@ public class DedupeTotalDataService { importProgressMap.put(importId, progress); try { - byte[] fileBytes = file.getBytes(); + File tempFile = saveMultipartToTempFile(file); String filename = file.getOriginalFilename(); - Thread.ofVirtual().start(() -> runImportTask(importId, fileBytes, filename)); + Thread.ofVirtual().start(() -> runImportTask(importId, tempFile, filename)); } catch (Exception e) { importProgressMap.remove(importId); throw new BusinessException("读取上传文件失败"); @@ -150,9 +152,9 @@ public class DedupeTotalDataService { deleteImportProgressMap.put(importId, progress); try { - byte[] fileBytes = file.getBytes(); + File tempFile = saveMultipartToTempFile(file); String filename = file.getOriginalFilename(); - Thread.ofVirtual().start(() -> runDeleteImportTask(importId, fileBytes, filename)); + Thread.ofVirtual().start(() -> runDeleteImportTask(importId, tempFile, filename)); } catch (Exception e) { deleteImportProgressMap.remove(importId); throw new BusinessException("读取上传文件失败"); @@ -211,6 +213,74 @@ public class DedupeTotalDataService { } } + private void runDeleteImportTask(String importId, File tempFile, String filename) { + DedupeTotalDataImportProgressVo progress = deleteImportProgressMap.get(importId); + if (progress == null) { + return; + } + progress.setStatus("running"); + try (InputStream inputStream = Files.newInputStream(tempFile.toPath())) { + DedupeTotalDataImportVo result = deleteFromExcelInternal(inputStream, filename, progress); + progress.setTotalRows(result.getTotalRows()); + progress.setAsinCount(result.getAsinCount()); + progress.setInsertedCount(result.getInsertedCount()); + progress.setSkippedCount(result.getSkippedCount()); + progress.setProcessedRows(result.getTotalRows()); + progress.setStatus("success"); + } catch (Exception e) { + progress.setStatus("failed"); + progress.setErrorMessage(e instanceof BusinessException ? e.getMessage() : "鍒犻櫎 Excel 鍖归厤鏁版嵁澶辫触"); + } finally { + deleteQuietly(tempFile); + } + } + + private void runImportTask(String importId, File tempFile, String filename) { + DedupeTotalDataImportProgressVo progress = importProgressMap.get(importId); + if (progress == null) { + return; + } + progress.setStatus("running"); + try (InputStream inputStream = Files.newInputStream(tempFile.toPath())) { + DedupeTotalDataImportVo result = importFromExcelInternal(inputStream, filename, progress); + progress.setTotalRows(result.getTotalRows()); + progress.setAsinCount(result.getAsinCount()); + progress.setInsertedCount(result.getInsertedCount()); + progress.setSkippedCount(result.getSkippedCount()); + progress.setProcessedRows(result.getTotalRows()); + progress.setStatus("success"); + } catch (Exception e) { + progress.setStatus("failed"); + progress.setErrorMessage(e instanceof BusinessException ? e.getMessage() : "瀵煎叆 Excel 澶辫触"); + } finally { + deleteQuietly(tempFile); + } + } + + private File saveMultipartToTempFile(MultipartFile file) throws Exception { + String suffix = ".xlsx"; + String name = file.getOriginalFilename(); + if (name != null) { + int idx = name.lastIndexOf('.'); + if (idx >= 0) { + suffix = name.substring(idx); + } + } + File tempFile = File.createTempFile("dedupe-total-data-", suffix); + file.transferTo(tempFile); + return tempFile; + } + + private void deleteQuietly(File file) { + if (file == null) { + return; + } + try { + Files.deleteIfExists(file.toPath()); + } catch (Exception ignored) { + } + } + @Transactional public DedupeTotalDataImportVo importFromExcel(MultipartFile file) { if (file == null || file.isEmpty()) { diff --git a/backend-java/src/main/java/com/nanri/aiimage/modules/deletebrand/controller/DeleteBrandRunController.java b/backend-java/src/main/java/com/nanri/aiimage/modules/deletebrand/controller/DeleteBrandRunController.java index b9492bc..45b895c 100644 --- a/backend-java/src/main/java/com/nanri/aiimage/modules/deletebrand/controller/DeleteBrandRunController.java +++ b/backend-java/src/main/java/com/nanri/aiimage/modules/deletebrand/controller/DeleteBrandRunController.java @@ -85,6 +85,12 @@ public class DeleteBrandRunController { return ApiResponse.success(deleteBrandRunService.getTaskDetails(request.getTaskIds())); } + @PostMapping("/tasks/progress/batch") + @Operation(summary = "批量获取删除品牌任务进度摘要", description = "仅返回任务基础状态和行进度摘要,用于前端轮询降载。") + public ApiResponse getTaskProgressBatch(@Valid @RequestBody DeleteBrandTaskBatchRequest request) { + return ApiResponse.success(deleteBrandRunService.getTaskProgress(request.getTaskIds())); + } + @GetMapping("/tasks/{taskId}/deletion-status") @Operation(summary = "获取删除品牌任务删除状态", description = "供 Python/插件按 taskId 查询该删除品牌任务是否已被前端删空。") public ApiResponse getTaskDeletionStatus( diff --git a/backend-java/src/main/java/com/nanri/aiimage/modules/deletebrand/service/DeleteBrandRunService.java b/backend-java/src/main/java/com/nanri/aiimage/modules/deletebrand/service/DeleteBrandRunService.java index c080b5d..1e357ae 100644 --- a/backend-java/src/main/java/com/nanri/aiimage/modules/deletebrand/service/DeleteBrandRunService.java +++ b/backend-java/src/main/java/com/nanri/aiimage/modules/deletebrand/service/DeleteBrandRunService.java @@ -544,10 +544,87 @@ public class DeleteBrandRunService { return vo; } + public com.nanri.aiimage.modules.deletebrand.model.vo.DeleteBrandTaskBatchVo getTaskProgress(java.util.List taskIds) { + com.nanri.aiimage.modules.deletebrand.model.vo.DeleteBrandTaskBatchVo vo = new com.nanri.aiimage.modules.deletebrand.model.vo.DeleteBrandTaskBatchVo(); + if (taskIds == null || taskIds.isEmpty()) { + return vo; + } + + java.util.List normalizedTaskIds = taskIds.stream() + .filter(id -> id != null && id > 0) + .distinct() + .limit(50) + .toList(); + if (normalizedTaskIds.isEmpty()) { + return vo; + } + + java.util.Map cachedTasks = deleteBrandTaskCacheService.getTaskCacheBatch(normalizedTaskIds); + java.util.List missingTaskIds = new java.util.ArrayList<>(); + for (Long taskId : normalizedTaskIds) { + if (!cachedTasks.containsKey(taskId)) { + missingTaskIds.add(taskId); + } + } + + java.util.Map taskById = new java.util.LinkedHashMap<>(cachedTasks); + if (!missingTaskIds.isEmpty()) { + java.util.List dbTasks = fileTaskMapper.selectBatchIds(missingTaskIds).stream() + .filter(task -> task != null && MODULE_TYPE.equals(task.getModuleType())) + .toList(); + for (FileTaskEntity dbTask : dbTasks) { + taskById.put(dbTask.getId(), dbTask); + if ("RUNNING".equals(dbTask.getStatus())) { + deleteBrandTaskCacheService.saveTaskCache(dbTask); + } + } + } + + java.util.Map> progressByTaskId = deleteBrandTaskCacheService.getProgressBatch(normalizedTaskIds); + + for (Long taskId : normalizedTaskIds) { + FileTaskEntity task = taskById.get(taskId); + if (task == null) { + vo.getMissingTaskIds().add(taskId); + continue; + } + vo.getItems().add(buildTaskProgressDetail(task, progressByTaskId.get(taskId))); + } + return vo; + } + private DeleteBrandTaskDetailVo buildTaskDetail(FileTaskEntity task) { return buildTaskDetail(task, null); } + private DeleteBrandTaskDetailVo buildTaskProgressDetail(FileTaskEntity task, java.util.Map progress) { + task = refreshIndexedMatchesIfNeeded(task); + DeleteBrandTaskItemVo taskVo = new DeleteBrandTaskItemVo(); + taskVo.setId(task.getId()); + taskVo.setTaskNo(task.getTaskNo()); + taskVo.setStatus(task.getStatus()); + taskVo.setSourceFileCount(task.getSourceFileCount()); + taskVo.setSuccessFileCount(task.getSuccessFileCount()); + taskVo.setFailedFileCount(task.getFailedFileCount()); + taskVo.setErrorMessage(task.getErrorMessage()); + taskVo.setCreatedAt(task.getCreatedAt()); + taskVo.setUpdatedAt(task.getUpdatedAt()); + taskVo.setFinishedAt(task.getFinishedAt()); + if ("SUCCESS".equals(task.getStatus())) { + taskVo.setDownloadUrl(resolveTaskDownloadUrl(task.getId())); + taskVo.setDownloadFilename(resolveTaskDownloadFilename(task.getId())); + } else { + taskVo.setDownloadUrl(null); + taskVo.setDownloadFilename(null); + } + + DeleteBrandTaskDetailVo detail = new DeleteBrandTaskDetailVo(); + detail.setTask(taskVo); + detail.setLine_progress(buildLineProgress(task.getId(), progress)); + detail.setFileProgress(java.util.List.of()); + return detail; + } + private DeleteBrandTaskDetailVo buildTaskDetail(FileTaskEntity task, java.util.Map progress) { task = refreshIndexedMatchesIfNeeded(task); DeleteBrandTaskItemVo taskVo = new DeleteBrandTaskItemVo(); 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 703a2ce..a003416 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 @@ -17,10 +17,15 @@ import lombok.extern.slf4j.Slf4j; import org.springframework.scheduling.annotation.Scheduled; import org.springframework.stereotype.Service; +import java.io.IOException; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.attribute.FileTime; import java.time.Duration; import java.time.Instant; import java.time.LocalDateTime; import java.time.ZoneId; +import java.util.List; import java.util.Map; @Service @@ -34,6 +39,8 @@ public class DeleteBrandStaleTaskService { 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); + private static final Duration TEMP_DIR_CLEANUP_LOCK_TTL = Duration.ofMinutes(10); + private static final Duration TEMP_DIR_RETENTION = Duration.ofHours(48); private final FileTaskMapper fileTaskMapper; private final DeleteBrandTaskCacheService deleteBrandTaskCacheService; @@ -289,7 +296,7 @@ public class DeleteBrandStaleTaskService { log.info("[stale-check] shop-match candidates={} threshold={} timeoutMinutes={} initialThreshold={} initialTimeoutMinutes={}", runningTasks.size(), threshold, minutes, initialThreshold, initialMinutes); for (FileTaskEntity task : runningTasks) { - boolean hasUploadedPayload = !shopMatchTaskCacheService.getAllShopMergedPayload(task.getId()).isEmpty(); + boolean hasUploadedPayload = shopMatchTaskCacheService.hasAnyShopMergedPayload(task.getId()); boolean hasFinishedRows = (task.getSuccessFileCount() != null && task.getSuccessFileCount() > 0) || (task.getFailedFileCount() != null && task.getFailedFileCount() > 0); boolean hasStartedProgress = hasUploadedPayload || hasFinishedRows; @@ -352,6 +359,74 @@ public class DeleteBrandStaleTaskService { } } + @Scheduled(cron = "${aiimage.temp-dir.cleanup-cron:15 */30 * * * *}") + public void cleanupExpiredTempDirs() { + DistributedJobLockService.LockHandle lockHandle = + distributedJobLockService.tryLock("temp-dir:cleanup", TEMP_DIR_CLEANUP_LOCK_TTL); + if (lockHandle == null) { + log.info("[temp-dir-cleanup] skip because another instance holds the distributed lock"); + return; + } + try (lockHandle) { + Instant cutoff = Instant.now().minus(TEMP_DIR_RETENTION); + List dirNames = List.of( + "shop-match-cache", + "brand-task-cache", + "delete-brand-task-cache", + "shop-match-result", + "delete-brand-result", + "product-risk-result", + "price-track-result" + ); + for (String dirName : dirNames) { + cleanupTempRoot(dirName, cutoff); + } + } + } + + private void cleanupTempRoot(String dirName, Instant cutoff) { + Path root = Path.of(System.getProperty("java.io.tmpdir"), dirName); + if (!Files.isDirectory(root)) { + return; + } + try (var stream = Files.list(root)) { + for (Path child : stream.toList()) { + try { + FileTime lastModified = Files.getLastModifiedTime(child); + if (lastModified.toInstant().isAfter(cutoff)) { + continue; + } + deleteRecursively(child); + log.info("[temp-dir-cleanup] deleted expired temp path dir={} path={}", dirName, child); + } catch (Exception ex) { + log.warn("[temp-dir-cleanup] delete failed dir={} path={} msg={}", dirName, child, ex.getMessage()); + } + } + } catch (IOException ex) { + log.warn("[temp-dir-cleanup] scan failed dir={} msg={}", dirName, ex.getMessage()); + } + } + + private void deleteRecursively(Path path) throws IOException { + if (!Files.exists(path)) { + return; + } + try (var walk = Files.walk(path)) { + walk.sorted(java.util.Comparator.reverseOrder()).forEach(item -> { + try { + Files.deleteIfExists(item); + } catch (IOException ex) { + throw new RuntimeException(ex); + } + }); + } catch (RuntimeException ex) { + if (ex.getCause() instanceof IOException ioException) { + throw ioException; + } + throw ex; + } + } + private static final class ProductRiskStaleCheckStats { private int scannedTaskCount; private int finalizedTaskCount; diff --git a/backend-java/src/main/java/com/nanri/aiimage/modules/deletebrand/service/DeleteBrandTaskCacheService.java b/backend-java/src/main/java/com/nanri/aiimage/modules/deletebrand/service/DeleteBrandTaskCacheService.java index 65a8802..75e38ae 100644 --- a/backend-java/src/main/java/com/nanri/aiimage/modules/deletebrand/service/DeleteBrandTaskCacheService.java +++ b/backend-java/src/main/java/com/nanri/aiimage/modules/deletebrand/service/DeleteBrandTaskCacheService.java @@ -7,6 +7,9 @@ import lombok.RequiredArgsConstructor; import org.springframework.data.redis.core.StringRedisTemplate; import org.springframework.stereotype.Service; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.StandardOpenOption; import java.time.Duration; import java.util.concurrent.ConcurrentHashMap; @@ -28,10 +31,17 @@ public class DeleteBrandTaskCacheService { public void saveParsedPayload(Long taskId, Object payload) { try { - stringRedisTemplate.opsForValue().set(buildPayloadKey(taskId), objectMapper.writeValueAsString(payload), Duration.ofHours(PAYLOAD_TTL_HOURS)); + Files.createDirectories(buildTaskDir(taskId)); + Files.writeString( + buildPayloadFile(taskId), + objectMapper.writeValueAsString(payload), + StandardOpenOption.CREATE, + StandardOpenOption.TRUNCATE_EXISTING, + StandardOpenOption.WRITE + ); parsedPayloadLocalCache.put(taskId, new LocalParsedPayloadCacheEntry(System.currentTimeMillis(), payload)); } catch (Exception ex) { - throw new BusinessException("暂存删除品牌原始数据失败"); + throw new BusinessException("鏆傚瓨鍒犻櫎鍝佺墝鍘熷鏁版嵁澶辫触"); } } @@ -46,21 +56,21 @@ public class DeleteBrandTaskCacheService { try { return objectMapper.convertValue(cached.value(), typeReference); } catch (Exception ignored) { - // fallthrough to redis + // fall through to file } } - String raw = stringRedisTemplate.opsForValue().get(buildPayloadKey(taskId)); - if (raw == null || raw.isBlank()) { + Path payloadFile = buildPayloadFile(taskId); + if (!Files.isRegularFile(payloadFile)) { parsedPayloadLocalCache.remove(taskId); return null; } try { - T parsed = objectMapper.readValue(raw, typeReference); + T parsed = objectMapper.readValue(Files.readString(payloadFile), typeReference); parsedPayloadLocalCache.put(taskId, new LocalParsedPayloadCacheEntry(now, parsed)); return parsed; } catch (Exception ex) { - throw new BusinessException("读取删除品牌原始数据失败"); + throw new BusinessException("璇诲彇鍒犻櫎鍝佺墝鍘熷鏁版嵁澶辫触"); } } @@ -114,17 +124,17 @@ public class DeleteBrandTaskCacheService { public boolean hasSameResultChunk(Long taskId, String fileIdentity, Integer chunkIndex, Object chunk) { if (chunkIndex == null || chunkIndex <= 0) { - throw new BusinessException("chunkIndex 不合法"); + throw new BusinessException("chunkIndex 涓嶅悎娉?"); } if (fileIdentity == null || fileIdentity.isBlank()) { - throw new BusinessException("fileIdentity 不能为空"); + throw new BusinessException("fileIdentity 涓嶈兘涓虹┖"); } String field = fileIdentity.trim() + "#" + chunkIndex; String chunkJson; try { chunkJson = objectMapper.writeValueAsString(chunk); } catch (Exception ex) { - throw new BusinessException("读取删除品牌结果分片失败"); + throw new BusinessException("璇诲彇鍒犻櫎鍝佺墝缁撴灉鍒嗙墖澶辫触"); } Object existing = stringRedisTemplate.opsForHash().get(buildResultChunksKey(taskId), field); if (chunkJson.equals(existing)) { @@ -137,17 +147,17 @@ public class DeleteBrandTaskCacheService { public boolean mergeResultChunk(Long taskId, String fileIdentity, Integer chunkIndex, Object chunk) { String resultKey = buildResultChunksKey(taskId); if (chunkIndex == null || chunkIndex <= 0) { - throw new BusinessException("chunkIndex 不合法"); + throw new BusinessException("chunkIndex 涓嶅悎娉?"); } if (fileIdentity == null || fileIdentity.isBlank()) { - throw new BusinessException("fileIdentity 不能为空"); + throw new BusinessException("fileIdentity 涓嶈兘涓虹┖"); } String field = fileIdentity.trim() + "#" + chunkIndex; String chunkJson; try { chunkJson = objectMapper.writeValueAsString(chunk); } catch (Exception ex) { - throw new BusinessException("暂存删除品牌结果分片失败"); + throw new BusinessException("鏆傚瓨鍒犻櫎鍝佺墝缁撴灉鍒嗙墖澶辫触"); } Object existing = stringRedisTemplate.opsForHash().get(resultKey, field); if (chunkJson.equals(existing)) { @@ -178,14 +188,16 @@ public class DeleteBrandTaskCacheService { public void delete(Long taskId) { parsedPayloadLocalCache.remove(taskId); - stringRedisTemplate.delete(buildPayloadKey(taskId)); stringRedisTemplate.delete(buildProgressKey(taskId)); stringRedisTemplate.delete(buildResultChunksKey(taskId)); stringRedisTemplate.delete(buildTaskEntityKey(taskId)); + deleteTaskDirQuietly(taskId); } public void saveTaskCache(com.nanri.aiimage.modules.task.model.entity.FileTaskEntity task) { - if (task == null || task.getId() == null) return; + if (task == null || task.getId() == null) { + return; + } try { stringRedisTemplate.opsForValue().set(buildTaskEntityKey(task.getId()), objectMapper.writeValueAsString(task), Duration.ofHours(PAYLOAD_TTL_HOURS)); } catch (Exception ignored) { @@ -194,10 +206,14 @@ public class DeleteBrandTaskCacheService { public java.util.Map getTaskCacheBatch(java.util.List taskIds) { java.util.Map result = new java.util.LinkedHashMap<>(); - if (taskIds == null || taskIds.isEmpty()) return result; + if (taskIds == null || taskIds.isEmpty()) { + return result; + } java.util.List normalized = taskIds.stream().filter(id -> id != null && id > 0).distinct().toList(); - if (normalized.isEmpty()) return result; + if (normalized.isEmpty()) { + return result; + } java.util.List keys = normalized.stream().map(this::buildTaskEntityKey).toList(); java.util.List values = stringRedisTemplate.opsForValue().multiGet(keys); @@ -215,6 +231,30 @@ public class DeleteBrandTaskCacheService { return result; } + private Path buildTaskDir(Long taskId) { + return Path.of(System.getProperty("java.io.tmpdir"), "delete-brand-task-cache", String.valueOf(taskId)); + } + + private Path buildPayloadFile(Long taskId) { + return buildTaskDir(taskId).resolve("parsed-payload.json"); + } + + private void deleteTaskDirQuietly(Long taskId) { + Path taskDir = buildTaskDir(taskId); + if (!Files.exists(taskDir)) { + return; + } + try (var walk = Files.walk(taskDir)) { + walk.sorted(java.util.Comparator.reverseOrder()).forEach(path -> { + try { + Files.deleteIfExists(path); + } catch (Exception ignored) { + } + }); + } catch (Exception ignored) { + } + } + private String buildTaskEntityKey(Long taskId) { return "delete-brand:task:entity:" + taskId; } @@ -225,10 +265,6 @@ public class DeleteBrandTaskCacheService { } } - private String buildPayloadKey(Long taskId) { - return "delete-brand:task:parsed-payload:" + taskId; - } - private String buildProgressKey(Long taskId) { return "delete-brand:task:progress:" + taskId; } diff --git a/backend-java/src/main/java/com/nanri/aiimage/modules/permission/model/dto/PermissionMenuCreateRequest.java b/backend-java/src/main/java/com/nanri/aiimage/modules/permission/model/dto/PermissionMenuCreateRequest.java index b40ab05..4cbdfe7 100644 --- a/backend-java/src/main/java/com/nanri/aiimage/modules/permission/model/dto/PermissionMenuCreateRequest.java +++ b/backend-java/src/main/java/com/nanri/aiimage/modules/permission/model/dto/PermissionMenuCreateRequest.java @@ -17,4 +17,6 @@ public class PermissionMenuCreateRequest { @NotBlank(message = "菜单路由不能为空") private String routePath; + + private Integer sortOrder; } diff --git a/backend-java/src/main/java/com/nanri/aiimage/modules/permission/model/dto/PermissionMenuUpdateRequest.java b/backend-java/src/main/java/com/nanri/aiimage/modules/permission/model/dto/PermissionMenuUpdateRequest.java index 38ef597..e2b6927 100644 --- a/backend-java/src/main/java/com/nanri/aiimage/modules/permission/model/dto/PermissionMenuUpdateRequest.java +++ b/backend-java/src/main/java/com/nanri/aiimage/modules/permission/model/dto/PermissionMenuUpdateRequest.java @@ -17,4 +17,6 @@ public class PermissionMenuUpdateRequest { @NotBlank(message = "菜单路由不能为空") private String routePath; + + private Integer sortOrder; } diff --git a/backend-java/src/main/java/com/nanri/aiimage/modules/permission/model/entity/AdminUserEntity.java b/backend-java/src/main/java/com/nanri/aiimage/modules/permission/model/entity/AdminUserEntity.java index b3af65b..9ac7b4e 100644 --- a/backend-java/src/main/java/com/nanri/aiimage/modules/permission/model/entity/AdminUserEntity.java +++ b/backend-java/src/main/java/com/nanri/aiimage/modules/permission/model/entity/AdminUserEntity.java @@ -1,6 +1,7 @@ package com.nanri.aiimage.modules.permission.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; @@ -11,5 +12,8 @@ public class AdminUserEntity { @TableId(type = IdType.AUTO) private Long id; + private String username; private String role; + @TableField("created_by_id") + private Long createdById; } diff --git a/backend-java/src/main/java/com/nanri/aiimage/modules/permission/model/entity/PermissionMenuEntity.java b/backend-java/src/main/java/com/nanri/aiimage/modules/permission/model/entity/PermissionMenuEntity.java index c8e4262..cbebbf8 100644 --- a/backend-java/src/main/java/com/nanri/aiimage/modules/permission/model/entity/PermissionMenuEntity.java +++ b/backend-java/src/main/java/com/nanri/aiimage/modules/permission/model/entity/PermissionMenuEntity.java @@ -17,5 +17,6 @@ public class PermissionMenuEntity { private String columnKey; private String menuType; private String routePath; + private Integer sortOrder; private LocalDateTime createdAt; } diff --git a/backend-java/src/main/java/com/nanri/aiimage/modules/permission/model/vo/PermissionMenuItemVo.java b/backend-java/src/main/java/com/nanri/aiimage/modules/permission/model/vo/PermissionMenuItemVo.java index 875dc87..2db9e38 100644 --- a/backend-java/src/main/java/com/nanri/aiimage/modules/permission/model/vo/PermissionMenuItemVo.java +++ b/backend-java/src/main/java/com/nanri/aiimage/modules/permission/model/vo/PermissionMenuItemVo.java @@ -12,5 +12,6 @@ public class PermissionMenuItemVo { private String columnKey; private String menuType; private String routePath; + private Integer sortOrder; private LocalDateTime createdAt; } diff --git a/backend-java/src/main/java/com/nanri/aiimage/modules/permission/service/PermissionMenuSchemaInitializer.java b/backend-java/src/main/java/com/nanri/aiimage/modules/permission/service/PermissionMenuSchemaInitializer.java index 3645a2e..41681e7 100644 --- a/backend-java/src/main/java/com/nanri/aiimage/modules/permission/service/PermissionMenuSchemaInitializer.java +++ b/backend-java/src/main/java/com/nanri/aiimage/modules/permission/service/PermissionMenuSchemaInitializer.java @@ -14,10 +14,16 @@ import java.util.List; public class PermissionMenuSchemaInitializer { private final JdbcTemplate jdbcTemplate; + private static final List DEFAULT_ADMIN_MENUS = List.of( - new DefaultAdminMenu("数据去重总数据", "admin_dedupe_total_data", "dedupe-total-data"), - new DefaultAdminMenu("店铺管理", "admin_shop_manage", "shop-manage"), - new DefaultAdminMenu("跳过跟价ASIN", "admin_skip_price_asin", "skip-price-asin") + new DefaultAdminMenu("用户管理", "admin_users", "users", 10), + new DefaultAdminMenu("栏目权限配置", "admin_columns", "columns", 20), + new DefaultAdminMenu("数据去重总数据", "admin_dedupe_total_data", "dedupe-total-data", 30), + new DefaultAdminMenu("店铺密钥管理", "admin_shop_keys", "shop-keys", 40), + new DefaultAdminMenu("店铺管理", "admin_shop_manage", "shop-manage", 50), + new DefaultAdminMenu("跳过跟价ASIN", "admin_skip_price_asin", "skip-price-asin", 60), + new DefaultAdminMenu("查看生成记录", "admin_history", "history", 70), + new DefaultAdminMenu("版本管理", "admin_version", "version", 80) ); @PostConstruct @@ -25,23 +31,19 @@ public class PermissionMenuSchemaInitializer { executeQuietly(""" CREATE TABLE IF NOT EXISTS columns ( id INT AUTO_INCREMENT PRIMARY KEY, - name VARCHAR(128) NOT NULL COMMENT '栏目名', - column_key VARCHAR(64) NOT NULL COMMENT '栏目标识', + name VARCHAR(128) NOT NULL COMMENT '菜单标题', + column_key VARCHAR(64) NOT NULL COMMENT '菜单唯一标识', created_at DATETIME DEFAULT CURRENT_TIMESTAMP, UNIQUE KEY uk_column_key (column_key) ) """); executeQuietly("ALTER TABLE columns ADD COLUMN menu_type VARCHAR(20) NOT NULL DEFAULT 'app' COMMENT '菜单类型: app/admin' AFTER column_key"); executeQuietly("ALTER TABLE columns ADD COLUMN route_path VARCHAR(255) NOT NULL DEFAULT '' COMMENT '菜单路由或页面标识' AFTER menu_type"); + executeQuietly("ALTER TABLE columns ADD COLUMN sort_order INT NOT NULL DEFAULT 0 COMMENT '菜单排序' AFTER route_path"); executeQuietly("UPDATE columns SET menu_type = 'app' WHERE menu_type IS NULL OR menu_type = ''"); + executeQuietly("UPDATE columns SET sort_order = id WHERE sort_order IS NULL OR sort_order = 0"); + executeQuietly("ALTER TABLE columns ADD UNIQUE KEY uk_menu_type_route_path (menu_type, route_path)"); ensureDefaultAdminMenus(); - executeQuietly(""" - INSERT INTO columns (name, column_key, menu_type, route_path) - SELECT '店铺管理', 'admin_shop_manage', 'admin', 'shop-manage' - WHERE NOT EXISTS ( - SELECT 1 FROM columns WHERE column_key = 'admin_shop_manage' - ) - """); executeQuietly(""" CREATE TABLE IF NOT EXISTS user_column_permission ( user_id INT NOT NULL, @@ -56,12 +58,17 @@ public class PermissionMenuSchemaInitializer { private void ensureDefaultAdminMenus() { for (DefaultAdminMenu menu : DEFAULT_ADMIN_MENUS) { executeQuietly(""" - INSERT INTO columns (name, column_key, menu_type, route_path) - SELECT '%s', '%s', 'admin', '%s' + INSERT INTO columns (name, column_key, menu_type, route_path, sort_order) + SELECT '%s', '%s', 'admin', '%s', %d WHERE NOT EXISTS ( SELECT 1 FROM columns WHERE column_key = '%s' ) - """.formatted(menu.name(), menu.columnKey(), menu.routePath(), menu.columnKey())); + """.formatted(menu.name(), menu.columnKey(), menu.routePath(), menu.sortOrder(), menu.columnKey())); + executeQuietly(""" + UPDATE columns + SET sort_order = %d + WHERE column_key = '%s' AND (sort_order IS NULL OR sort_order = 0) + """.formatted(menu.sortOrder(), menu.columnKey())); } } @@ -73,6 +80,6 @@ public class PermissionMenuSchemaInitializer { } } - private record DefaultAdminMenu(String name, String columnKey, String routePath) { + private record DefaultAdminMenu(String name, String columnKey, String routePath, int sortOrder) { } } diff --git a/backend-java/src/main/java/com/nanri/aiimage/modules/permission/service/PermissionMenuService.java b/backend-java/src/main/java/com/nanri/aiimage/modules/permission/service/PermissionMenuService.java index a0ef8f0..54ed17e 100644 --- a/backend-java/src/main/java/com/nanri/aiimage/modules/permission/service/PermissionMenuService.java +++ b/backend-java/src/main/java/com/nanri/aiimage/modules/permission/service/PermissionMenuService.java @@ -19,6 +19,7 @@ import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; import java.util.ArrayList; +import java.util.Comparator; import java.util.LinkedHashSet; import java.util.List; import java.util.Locale; @@ -41,6 +42,7 @@ public class PermissionMenuService { public List list(String menuType) { LambdaQueryWrapper query = new LambdaQueryWrapper() .eq(isValidMenuType(menuType), PermissionMenuEntity::getMenuType, normalizeMenuType(menuType)) + .orderByAsc(PermissionMenuEntity::getSortOrder) .orderByAsc(PermissionMenuEntity::getId); return permissionMenuMapper.selectList(query).stream() .map(this::toItemVo) @@ -54,12 +56,14 @@ public class PermissionMenuService { String menuType = normalizeMenuTypeRequired(request.getMenuType()); String routePath = normalizeRequired(request.getRoutePath(), "菜单路由不能为空"); ensureUniqueColumnKey(columnKey, null); + ensureUniqueRoutePath(menuType, routePath, null); PermissionMenuEntity entity = new PermissionMenuEntity(); entity.setName(name); entity.setColumnKey(columnKey); entity.setMenuType(menuType); entity.setRoutePath(routePath); + entity.setSortOrder(resolveSortOrder(request.getSortOrder(), null)); permissionMenuMapper.insert(entity); return toItemVo(getMenuById(entity.getId())); } @@ -72,11 +76,13 @@ public class PermissionMenuService { String menuType = normalizeMenuTypeRequired(request.getMenuType()); String routePath = normalizeRequired(request.getRoutePath(), "菜单路由不能为空"); ensureUniqueColumnKey(columnKey, id); + ensureUniqueRoutePath(menuType, routePath, id); entity.setName(name); entity.setColumnKey(columnKey); entity.setMenuType(menuType); entity.setRoutePath(routePath); + entity.setSortOrder(resolveSortOrder(request.getSortOrder(), id)); permissionMenuMapper.updateById(entity); return toItemVo(getMenuById(id)); } @@ -141,6 +147,9 @@ public class PermissionMenuService { .map(menuMap::get) .filter(menu -> menu != null) .filter(menu -> safeMenuType == null || safeMenuType.equals(menu.getMenuType())) + .sorted(Comparator + .comparing(PermissionMenuEntity::getSortOrder, Comparator.nullsLast(Integer::compareTo)) + .thenComparing(PermissionMenuEntity::getId, Comparator.nullsLast(Long::compareTo))) .map(this::toItemVo) .toList(); } @@ -196,6 +205,16 @@ public class PermissionMenuService { } } + private void ensureUniqueRoutePath(String menuType, String routePath, Long excludeId) { + PermissionMenuEntity exists = permissionMenuMapper.selectOne(new LambdaQueryWrapper() + .eq(PermissionMenuEntity::getMenuType, menuType) + .eq(PermissionMenuEntity::getRoutePath, routePath) + .last("LIMIT 1")); + if (exists != null && (excludeId == null || !excludeId.equals(exists.getId()))) { + throw new BusinessException("同一菜单类型下的菜单路由已存在"); + } + } + private String normalizeRequired(String value, String message) { String normalized = value == null ? "" : value.trim(); if (normalized.isEmpty() && !message.isEmpty()) { @@ -247,7 +266,21 @@ public class PermissionMenuService { vo.setColumnKey(entity.getColumnKey()); vo.setMenuType(entity.getMenuType()); vo.setRoutePath(entity.getRoutePath()); + vo.setSortOrder(entity.getSortOrder()); vo.setCreatedAt(entity.getCreatedAt()); return vo; } + + private Integer resolveSortOrder(Integer sortOrder, Long excludeId) { + if (sortOrder != null) { + return sortOrder; + } + PermissionMenuEntity tail = permissionMenuMapper.selectOne(new LambdaQueryWrapper() + .ne(excludeId != null, PermissionMenuEntity::getId, excludeId) + .orderByDesc(PermissionMenuEntity::getSortOrder) + .orderByDesc(PermissionMenuEntity::getId) + .last("LIMIT 1")); + int currentMax = tail == null || tail.getSortOrder() == null ? 0 : tail.getSortOrder(); + return currentMax + 1; + } } 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 cf1b812..e0619cb 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 @@ -47,7 +47,8 @@ import java.util.List; @RequestMapping("/api/price-track") @Tag( name = "跟价", - description = "亚马逊跟价处理模块:包含备选店铺、紫鸟匹配、任务创建、Python 回传、任务轮询和结果下载接口。查询类接口需要携带 Query 参数 user_id。") + description = "亚马逊跟价处理模块,包含备选店铺、紫鸟匹配、任务创建、Python 回传、任务轮询和结果下载接口。查询类接口需携带 query 参数 user_id。" +) public class PriceTrackController { private final PriceTrackService priceTrackService; @@ -62,13 +63,13 @@ public class PriceTrackController { } @PostMapping("/candidates") - @Operation(summary = "新增备选店铺", description = "把店铺名称写入当前用户的备选店铺列表,供后续匹配和创建任务使用。") + @Operation(summary = "新增备选店铺", description = "新增一条备选店铺记录,供后续匹配和创建任务使用。") public ApiResponse addCandidate(@Valid @RequestBody PriceTrackCandidateAddRequest request) { return ApiResponse.success(priceTrackService.addCandidate(request)); } @DeleteMapping("/candidates/{id}") - @Operation(summary = "删除备选店铺", description = "删除一条备选店铺记录,id 必须属于当前 user_id。") + @Operation(summary = "删除备选店铺", description = "删除一条备选店铺记录,记录必须属于当前 user_id。") public ApiResponse deleteCandidate( @Parameter(description = "备选店铺记录主键", example = "10") @PathVariable Long id, @Parameter(name = "user_id", description = "当前用户 ID", required = true, in = ParameterIn.QUERY, example = "1") @@ -107,7 +108,7 @@ public class PriceTrackController { } @GetMapping("/history") - @Operation(summary = "处理记录列表", description = "返回当前用户跟价模块下各店铺结果行,包含运行中和历史记录。") + @Operation(summary = "处理记录列表", description = "返回当前用户跟价模块下各店铺结果记录,包含运行中和历史记录。") public ApiResponse history( @Parameter(name = "user_id", description = "当前用户 ID", required = true, in = ParameterIn.QUERY, example = "1") @RequestParam("user_id") Long userId) { @@ -115,7 +116,7 @@ public class PriceTrackController { } @DeleteMapping("/history/{resultId}") - @Operation(summary = "删除单条结果记录", description = "删除一条 biz_file_result,并同步重算父任务状态。") + @Operation(summary = "删除单条结果记录", description = "删除一条 biz_file_result 结果记录,并同步重算父任务状态。") public ApiResponse deleteHistory( @Parameter(description = "结果行主键 biz_file_result.id", example = "1001") @PathVariable Long resultId, @Parameter(name = "user_id", description = "当前用户 ID", required = true, in = ParameterIn.QUERY, example = "1") @@ -131,7 +132,7 @@ public class PriceTrackController { } @DeleteMapping("/tasks/{taskId}") - @Operation(summary = "删除整条任务", description = "删除该任务以及其下属全部结果行,RUNNING、SUCCESS、FAILED 均可。") + @Operation(summary = "删除整条任务", description = "删除该任务以及其下全部结果行,RUNNING、SUCCESS、FAILED 均可删除。") public ApiResponse deleteTask( @Parameter(description = "任务主键 biz_file_task.id", example = "200") @PathVariable Long taskId, @Parameter(name = "user_id", description = "当前用户 ID", required = true, in = ParameterIn.QUERY, example = "1") @@ -151,11 +152,17 @@ public class PriceTrackController { } @PostMapping("/tasks/batch") - @Operation(summary = "批量查询任务详情", description = "按 taskIds 返回任务概要和各店铺结果,供前端轮询使用。") + @Operation(summary = "批量查询任务详情", description = "按 taskIds 返回任务概要和各店铺结果,供前端查询使用。") public ApiResponse tasksBatch(@Valid @RequestBody PriceTrackTaskBatchRequest request) { return ApiResponse.success(priceTrackTaskService.getTaskDetailsBatch(request.getTaskIds())); } + @PostMapping("/tasks/progress/batch") + @Operation(summary = "批量查询任务进度摘要", description = "仅返回任务状态、时间和错误等轻量信息,供前端轮询降载。") + public ApiResponse taskProgressBatch(@Valid @RequestBody PriceTrackTaskBatchRequest request) { + return ApiResponse.success(priceTrackTaskService.getTaskProgressBatch(request.getTaskIds())); + } + @PostMapping("/tasks/{taskId}/result") @Operation(summary = "Python 回传处理结果", description = "同一 taskId 可多次回传。成功时按 countries 生成结果文件,所有店铺进入终态后任务会变为 SUCCESS 或 FAILED。") public ApiResponse submitResult( 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 a743000..c405caf 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 @@ -8,13 +8,20 @@ import lombok.Data; import java.util.List; @Data -@Schema(description = "批量匹配店铺请求体:按店铺名查询紫鸟索引,返回是否命中及店铺元数据") +@Schema(description = "Price track match shops request") public class PriceTrackMatchShopsRequest { - @NotNull(message = "user_id不能为空") - @Schema(description = "当前用户 ID,用于审计和数据归属", requiredMode = Schema.RequiredMode.REQUIRED, example = "1") + + @NotNull(message = "userId is required") + @Schema(description = "Current user id", requiredMode = Schema.RequiredMode.REQUIRED, example = "1") private Long userId; - @NotEmpty(message = "店铺列表不能为空") - @Schema(description = "待匹配的店铺名称列表,至少 1 个;返回体 items 与列表顺序对应", requiredMode = Schema.RequiredMode.REQUIRED, example = "[\"店铺甲\",\"店铺乙\"]") + @NotEmpty(message = "shopNames cannot be empty") + @Schema(description = "Shop names to match", requiredMode = Schema.RequiredMode.REQUIRED) private List shopNames; + + @Schema(description = "Selected ASIN file local paths") + private List asinFiles; + + @Schema(description = "Selected country codes") + private List countryCodes; } 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 5663628..37c5849 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 @@ -7,43 +7,73 @@ import java.util.List; import java.util.Map; @Data -@Schema(description = "Python 回传跟价结果请求体:按店铺提交各国家 ASIN 跟价结果或错误信息") +@Schema(description = "Python submit payload for price track task results") public class PriceTrackSubmitResultRequest { - @Schema(description = "本次回传涉及的店铺列表,至少 1 个,店名需要能和任务结果行匹配", requiredMode = Schema.RequiredMode.REQUIRED) + @Schema(description = "Shop result list", requiredMode = Schema.RequiredMode.REQUIRED) private List shops; @Data - @Schema(description = "单个店铺回传结果:成功时提供 countries,失败时可只提供 error") + @Schema(description = "Per-shop submit result") public static class ShopResult { - @Schema(description = "店铺名称,用于和任务内结果行匹配", example = "测试店铺A") + @Schema(description = "Shop name used to match task rows", example = "Test Shop A") private String shopName; - @Schema(description = "按国家分组的结果行,key 为国家代码,例如 DE、UK、FR、IT、ES") + @Schema(description = "Rows grouped by country code, for example DE/UK/FR/IT/ES") private Map> countries; - @Schema(description = "失败时的错误信息;非空则该店铺记为失败") + @Schema(description = "Failure message") private String error; - @Schema(description = "是否成功;false 按失败处理,true 或 null 通常表示结合 countries 判定") + @Schema(description = "Whether the shop finished successfully") private Boolean success; } @Data - @Schema(description = "单条 ASIN 跟价结果") + @Schema(description = "Per-ASIN price track result row") public static class AsinResult { + @Schema(description = "Shop mall name", example = "Amazon.de") + private String shopMallName; + @Schema(description = "ASIN", example = "B0ABCDE123") private String asin; - @Schema(description = "当前店铺价格", example = "19.99") + @Schema(description = "Price", example = "19.99") + private String price; + + @Schema(description = "Recommended price", example = "18.99") + private String recommendedPrice; + + @Schema(description = "Lowest price", example = "18.50") + private String minimumPrice; + + @Schema(description = "First place", example = "Competitor A") + private String firstPlace; + + @Schema(description = "Second place", example = "Competitor B") + private String secondPlace; + + @Schema(description = "Buy box shop name", example = "Shop A") + private String cartShopName; + + @Schema(description = "Price change status", example = "UPDATED") + private String priceChangeStatus; + + @Schema(description = "Modify count", example = "2") + private String modifyCount; + + @Schema(description = "Status", example = "SUCCESS") + private String status; + + @Schema(description = "Legacy current price", example = "19.99") private String currentPrice; - @Schema(description = "竞争对手价格", example = "18.50") + @Schema(description = "Legacy competitor price", example = "18.50") private String competitorPrice; - @Schema(description = "建议动作或跟价动作", example = "LOWER_PRICE") + @Schema(description = "Legacy action", example = "LOWER_PRICE") private String action; - @Schema(description = "该行是否已处理完成") + @Schema(description = "Legacy done flag") private Boolean done; } } 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 7acb2a7..1e6b686 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 @@ -4,13 +4,20 @@ import io.swagger.v3.oas.annotations.media.Schema; import lombok.Data; import java.util.List; +import java.util.Map; @Data -@Schema(description = "创建任务响应:包含任务 id 和各店铺的初始结果快照") +@Schema(description = "price track create task response") public class PriceTrackCreateTaskVo { - @Schema(description = "新建任务主键 biz_file_task.id,前端推送队列时需要回传该值", example = "200") + @Schema(description = "task id", example = "200") private Long taskId; - @Schema(description = "各店铺一条初始结果快照,包含 resultId、taskId、店铺信息和初始任务状态") + @Schema(description = "initial task items") private List items; + + @Schema(description = "all skip asins grouped by country") + private Map> skipAsinsByCountry; + + @Schema(description = "parsed asin rows by country") + private Map>> asinRowsByCountry; } 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 e039a29..8e5a86c 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 @@ -5,35 +5,47 @@ import lombok.Data; import java.util.ArrayList; import java.util.List; +import java.util.Map; @Data -@Schema(description = "批量匹配店铺响应") +@Schema(description = "Price track shop match response") public class PriceTrackMatchShopsVo { - @Schema(description = "逐店返回的匹配结果,顺序与请求 shopNames 对应") + + @Schema(description = "Match results in the same order as requested shop names") private List items = new ArrayList<>(); + @Schema(description = "All skip ASINs grouped by country code") + private Map> skipAsinsByCountry; + + @Schema(description = "Parsed asin rows grouped by country code") + private Map>> asinRowsByCountry; + @Data - @Schema(description = "单店匹配结果,同时也是创建任务时 items 的结构") + @Schema(description = "Single matched shop item") public static class PriceTrackShopQueueItem { - @Schema(description = "店铺名称", example = "某某店铺") + + @Schema(description = "Shop name", example = "Demo Shop") private String shopName; - @Schema(description = "紫鸟店铺 ID,未命中时可能为空") + @Schema(description = "Matched shop id") private Object shopId; - @Schema(description = "平台,例如亚马逊", example = "亚马逊") + @Schema(description = "Platform", example = "Amazon") private String platform; - @Schema(description = "公司名称或主体标识") + @Schema(description = "Company name") private String companyName; - @Schema(description = "是否命中紫鸟索引;false 时不可用于创建任务") + @Schema(description = "Whether the shop matched") private Boolean matched; - @Schema(description = "匹配状态码,例如 MATCHED、PENDING、CONFLICT、INDEX_STALE") + @Schema(description = "Match status, such as MATCHED or PENDING") private String matchStatus; - @Schema(description = "人类可读说明,供前端展示") + @Schema(description = "Readable match message") private String matchMessage; + + @Schema(description = "All skip ASINs grouped by country code") + private Map> skipAsins; } } 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 index 9fc5029..4b4d03f 100644 --- 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 @@ -19,7 +19,19 @@ import java.util.Map; @Slf4j public class PriceTrackExcelAssemblyService { - private static final String[] HEADER = {"ASIN", "当前店铺价格", "竞争对手价格", "建议动作", "是否完成"}; + private static final String[] RESULT_HEADER = { + "\u5e97\u94fa\u5546\u57ce\u540d\u79f0", + "ASIN", + "\u4ef7\u683c", + "\u63a8\u8350\u4ef7", + "\u6700\u4f4e\u4ef7", + "\u7b2c\u4e00\u540d", + "\u7b2c\u4e8c\u540d", + "\u8d2d\u7269\u8f66\u5e97\u94fa\u540d", + "\u6539\u4ef7\u60c5\u51b5", + "\u4fee\u6539\u6b21\u6570", + "\u72b6\u6001" + }; public void writeWorkbook(File outputXlsx, Map> countries) { Map> safe = countries == null ? Map.of() : countries; @@ -28,8 +40,8 @@ public class PriceTrackExcelAssemblyService { 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]); + for (int i = 0; i < RESULT_HEADER.length; i++) { + headerRow.createCell(i).setCellValue(RESULT_HEADER[i]); } List rows = safe.getOrDefault(sheetName, List.of()); int rowIdx = 1; @@ -38,20 +50,26 @@ public class PriceTrackExcelAssemblyService { 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())); + row.createCell(0).setCellValue(valueOf(item.getShopMallName())); + row.createCell(1).setCellValue(valueOf(item.getAsin())); + row.createCell(2).setCellValue(firstNonBlank(item.getPrice(), item.getCurrentPrice())); + row.createCell(3).setCellValue(valueOf(item.getRecommendedPrice())); + row.createCell(4).setCellValue(firstNonBlank(item.getMinimumPrice(), item.getCompetitorPrice())); + row.createCell(5).setCellValue(valueOf(item.getFirstPlace())); + row.createCell(6).setCellValue(valueOf(item.getSecondPlace())); + row.createCell(7).setCellValue(valueOf(item.getCartShopName())); + row.createCell(8).setCellValue(firstNonBlank(item.getPriceChangeStatus(), item.getAction())); + row.createCell(9).setCellValue(valueOf(item.getModifyCount())); + row.createCell(10).setCellValue(firstNonBlank(item.getStatus(), item.getDone() == null ? null : String.valueOf(item.getDone()))); } - for (int i = 0; i < HEADER.length; i++) { + for (int i = 0; i < RESULT_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()); + throw new BusinessException("鐢熸垚 Excel 澶辫触: " + ex.getMessage()); } } @@ -82,4 +100,20 @@ public class PriceTrackExcelAssemblyService { } return out; } + + private static String valueOf(String value) { + return value == null ? "" : value; + } + + private static String firstNonBlank(String... values) { + if (values == null || values.length == 0) { + return ""; + } + for (String value : values) { + if (value != null && !value.isBlank()) { + return value; + } + } + return ""; + } } 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 d9876b7..9f0f285 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 @@ -24,8 +24,10 @@ import org.springframework.transaction.annotation.Transactional; import java.time.LocalDateTime; import java.util.ArrayList; +import java.util.LinkedHashMap; import java.util.LinkedHashSet; import java.util.List; +import java.util.Map; import java.util.Objects; @Service @@ -40,6 +42,7 @@ public class PriceTrackService { private final ObjectMapper objectMapper; private final ZiniaoShopSwitchService ziniaoShopSwitchService; private final SkipPriceAsinService skipPriceAsinService; + private final PriceTrackTaskService priceTrackTaskService; public List listCandidates(Long userId) { if (userId == null || userId <= 0) { @@ -139,9 +142,22 @@ public class PriceTrackService { throw new BusinessException("shop_names 没有有效店铺名"); } + boolean asinMode = request.getAsinFiles() != null && !request.getAsinFiles().isEmpty(); + Map> skipAsinsByCountry = asinMode + ? new LinkedHashMap<>() + : skipPriceAsinService.listAllSkipAsinsByCountry(); + Map>> asinRowsByCountry = + !asinMode + ? new LinkedHashMap<>() + : priceTrackTaskService.parseMatchAsinRowsByCountry( + request.getAsinFiles(), + request.getCountryCodes()); + PriceTrackMatchShopsVo vo = new PriceTrackMatchShopsVo(); + vo.setSkipAsinsByCountry(skipAsinsByCountry); + vo.setAsinRowsByCountry(asinRowsByCountry); for (String shopName : ordered) { - vo.getItems().add(matchOneShop(shopName)); + vo.getItems().add(matchOneShop(shopName, skipAsinsByCountry)); } return vo; } @@ -198,9 +214,13 @@ public class PriceTrackService { return vo; } - private PriceTrackMatchShopsVo.PriceTrackShopQueueItem matchOneShop(String shopName) { + private PriceTrackMatchShopsVo.PriceTrackShopQueueItem matchOneShop( + String shopName, + Map> skipAsins + ) { PriceTrackMatchShopsVo.PriceTrackShopQueueItem item = new PriceTrackMatchShopsVo.PriceTrackShopQueueItem(); item.setShopName(shopName); + item.setSkipAsins(skipAsins == null ? new LinkedHashMap<>() : new LinkedHashMap<>(skipAsins)); try { ZiniaoShopMatchResultVo match = ziniaoShopSwitchService.findIndexedStoreByName(shopName, false); if (match == null) { 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 da04c7e..649d53c 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 @@ -4,7 +4,9 @@ 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.common.util.ExcelStreamReader; import com.nanri.aiimage.modules.file.service.oss.OssStorageService; +import com.nanri.aiimage.modules.productrisk.model.enums.ProductRiskCountryCode; import com.nanri.aiimage.modules.pricetrack.mapper.PriceTrackShopCandidateMapper; import com.nanri.aiimage.modules.pricetrack.model.dto.PriceTrackCreateTaskRequest; import com.nanri.aiimage.modules.pricetrack.model.dto.PriceTrackSubmitResultRequest; @@ -30,12 +32,19 @@ import lombok.extern.slf4j.Slf4j; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; +import java.io.BufferedReader; +import java.io.File; +import java.io.FileInputStream; +import java.io.InputStreamReader; +import java.nio.charset.StandardCharsets; import java.time.LocalDateTime; import java.util.ArrayList; +import java.util.Arrays; import java.util.LinkedHashMap; import java.util.List; import java.util.Map; import java.util.Objects; +import java.util.Locale; @Service @RequiredArgsConstructor @@ -179,8 +188,33 @@ public class PriceTrackTaskService { return batch; } + public PriceTrackTaskBatchVo getTaskProgressBatch(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; + } + PriceTrackTaskDetailVo detail = new PriceTrackTaskDetailVo(); + detail.setTask(toTaskItemVo(task)); + batch.getItems().add(detail); + } + return batch; + } + @Transactional public PriceTrackCreateTaskVo createTask(PriceTrackCreateTaskRequest request) { + if (request.isStatusMode() == request.isAsinMode()) throw new BusinessException("跟价模式必须二选一"); + if (request.isAsinMode() && (request.getAsinFiles() == null || request.getAsinFiles().isEmpty())) { + throw new BusinessException("自定义 ASIN 模式必须上传 ASIN 文件"); + } if (request.getUserId() == null || request.getUserId() <= 0) throw new BusinessException("user_id 不合法"); if (request.getItems() == null || request.getItems().isEmpty()) throw new BusinessException("items 不能为空"); @@ -195,13 +229,14 @@ public class PriceTrackTaskService { } // 汇总店铺名并查询需要跳过的 ASIN - List shopNames = uniqueItems.stream() - .map(PriceTrackMatchShopsVo.PriceTrackShopQueueItem::getShopName) - .filter(s -> s != null && !s.isBlank()) - .distinct() - .toList(); - Map> skipAsinsByShop = skipPriceAsinService.listSkipAsinsByShops(shopNames); - log.info("[price-track] createTask skipAsins shops={}", skipAsinsByShop.keySet()); + Map> skipAsinsByCountry = request.isAsinMode() + ? new LinkedHashMap<>() + : skipPriceAsinService.listAllSkipAsinsByCountry(); + Map>> asinRowsByCountry = request.isAsinMode() + ? parseAsinRowsByCountry(request.getAsinFiles(), request.getCountryCodes()) + : new LinkedHashMap<>(); + log.info("[price-track] createTask skipAsins countries={} asinMode={}", + skipAsinsByCountry.keySet(), request.isAsinMode()); // 创建任务记录 FileTaskEntity task = new FileTaskEntity(); @@ -242,7 +277,8 @@ public class PriceTrackTaskService { ctx.put("statusMode", request.isStatusMode()); ctx.put("asinMode", request.isAsinMode()); ctx.put("countryCodes", request.getCountryCodes()); - ctx.put("skipAsinsByShop", skipAsinsByShop); + ctx.put("skipAsinsByCountry", skipAsinsByCountry); + ctx.put("asinRowsByCountry", asinRowsByCountry); ctx.put("items", uniqueItems); task.setRequestJson(objectMapper.writeValueAsString(ctx)); task.setResultJson(objectMapper.writeValueAsString(snapshot)); @@ -257,6 +293,8 @@ public class PriceTrackTaskService { PriceTrackCreateTaskVo vo = new PriceTrackCreateTaskVo(); vo.setTaskId(task.getId()); vo.setItems(snapshot); + vo.setSkipAsinsByCountry(skipAsinsByCountry); + vo.setAsinRowsByCountry(asinRowsByCountry); return vo; } @@ -272,7 +310,7 @@ public class PriceTrackTaskService { } if ("SUCCESS".equals(task.getStatus()) || "FAILED".equals(task.getStatus())) { log.warn("[price-track] submitResult rejected taskId={} status={}", taskId, task.getStatus()); - throw new BusinessException("任务已结束,拒绝重复提交"); + throw new BusinessException(40901, "任务已结束,拒绝重复提交"); } priceTrackTaskCacheService.touchTaskHeartbeat(taskId); @@ -452,6 +490,211 @@ public class PriceTrackTaskService { } } + public Map>> parseMatchAsinRowsByCountry(List asinFiles, List countryCodes) { + return parseAsinRowsByCountry(asinFiles, countryCodes); + } + + private Map>> parseAsinRowsByCountry(List asinFiles, List countryCodes) { + Map>> merged = new LinkedHashMap<>(); + if (asinFiles == null || asinFiles.isEmpty()) { + return merged; + } + for (String rawPath : asinFiles) { + if (rawPath == null || rawPath.isBlank()) { + continue; + } + File file = new File(rawPath); + if (!file.isFile()) { + throw new BusinessException("ASIN file not found: " + rawPath); + } + String lowerName = file.getName().toLowerCase(Locale.ROOT); + if (lowerName.endsWith(".csv")) { + mergeCountryRows(merged, parseCsvAsinRows(file, countryCodes)); + continue; + } + if (lowerName.endsWith(".xlsx") || lowerName.endsWith(".xls")) { + mergeCountryRows(merged, parseWorkbookAsinRows(file, countryCodes)); + continue; + } + throw new BusinessException("Unsupported ASIN file type: " + file.getName()); + } + return merged; + } + + private Map>> parseWorkbookAsinRows(File file, List countryCodes) { + Map>> out = new LinkedHashMap<>(); + Map[] headerIndexHolder = new Map[]{Map.of()}; + String[] countryCodeHolder = new String[]{null}; + int[] sheetCount = new int[]{0}; + try { + ExcelStreamReader.readAllSheets(file, new ExcelStreamReader.SheetRowHandler() { + @Override + public void onHeader(String sheetName, Integer sheetNo, Map headerMap) { + sheetCount[0]++; + countryCodeHolder[0] = resolveCountryCode(sheetName, countryCodes, false); + headerIndexHolder[0] = buildHeaderIndex(headerMap); + } + + @Override + public void onRow(String sheetName, Integer sheetNo, int rowIndex, Map headerMap, Map rowMap) { + String countryCode = countryCodeHolder[0]; + if (countryCode == null && sheetCount[0] == 1 && countryCodes != null && countryCodes.size() == 1) { + countryCode = countryCodes.get(0); + } + if (countryCode == null) { + return; + } + if (headerIndexHolder[0].isEmpty()) { + return; + } + Map item = extractAsinRow(rowMap, headerIndexHolder[0]); + if (!item.isEmpty()) { + out.computeIfAbsent(countryCode, key -> new ArrayList<>()).add(item); + } + } + }); + } catch (BusinessException ex) { + throw ex; + } catch (Exception ex) { + throw new BusinessException("Failed to parse ASIN workbook"); + } + return out; + } + + private Map>> parseCsvAsinRows(File file, List countryCodes) { + if (countryCodes == null || countryCodes.size() != 1) { + throw new BusinessException("CSV ASIN file requires exactly one selected country"); + } + Map>> out = new LinkedHashMap<>(); + List> rows = out.computeIfAbsent(countryCodes.get(0), key -> new ArrayList<>()); + try (BufferedReader reader = new BufferedReader(new InputStreamReader(new FileInputStream(file), StandardCharsets.UTF_8))) { + String headerLine = reader.readLine(); + if (headerLine == null) { + return out; + } + List headers = splitCsvLine(headerLine); + Map headerIndex = buildHeaderIndex(headers); + String line; + while ((line = reader.readLine()) != null) { + List values = splitCsvLine(line); + Map item = extractAsinRow(values, headerIndex); + if (!item.isEmpty()) { + rows.add(item); + } + } + } catch (BusinessException ex) { + throw ex; + } catch (Exception ex) { + throw new BusinessException("Failed to parse ASIN csv"); + } + return out; + } + + private void mergeCountryRows(Map>> target, Map>> incoming) { + for (Map.Entry>> entry : incoming.entrySet()) { + target.computeIfAbsent(entry.getKey(), key -> new ArrayList<>()).addAll(entry.getValue()); + } + } + + private String resolveCountryCode(String sheetName, List countryCodes, boolean singleSheetFallback) { + String normalized = normalizeHeaderText(sheetName).toUpperCase(Locale.ROOT); + for (ProductRiskCountryCode code : ProductRiskCountryCode.values()) { + if (code.name().equals(normalized) || normalizeHeaderText(code.getSheetName()).equals(normalizeHeaderText(sheetName))) { + return code.name(); + } + } + if (singleSheetFallback && countryCodes != null && countryCodes.size() == 1) { + return countryCodes.get(0); + } + return null; + } + + private Map buildHeaderIndex(Map headerRow) { + List headers = new ArrayList<>(); + int maxIndex = headerRow.keySet().stream().mapToInt(Integer::intValue).max().orElse(-1); + for (int i = 0; i <= maxIndex; i++) { + headers.add(normalizeCellText(headerRow.get(i))); + } + return buildHeaderIndex(headers); + } + + private Map buildHeaderIndex(List headers) { + Map index = new LinkedHashMap<>(); + for (int i = 0; i < headers.size(); i++) { + String key = mapHeaderKey(headers.get(i)); + if (key != null && !index.containsKey(key)) { + index.put(key, i); + } + } + return index; + } + + private Map extractAsinRow(Map row, Map headerIndex) { + Map item = new LinkedHashMap<>(); + for (Map.Entry entry : headerIndex.entrySet()) { + item.put(entry.getKey(), normalizeCellText(row.get(entry.getValue()))); + } + return isEmptyAsinRow(item) ? Map.of() : item; + } + + private Map extractAsinRow(List values, Map headerIndex) { + Map item = new LinkedHashMap<>(); + for (Map.Entry entry : headerIndex.entrySet()) { + int idx = entry.getValue(); + item.put(entry.getKey(), idx >= 0 && idx < values.size() ? normalizeCellText(values.get(idx)) : ""); + } + return isEmptyAsinRow(item) ? Map.of() : item; + } + + private boolean isEmptyAsinRow(Map row) { + return row.values().stream().allMatch(value -> value == null || value.isBlank()); + } + + private String mapHeaderKey(String rawHeader) { + String header = normalizeHeaderText(rawHeader); + if (header.isEmpty()) { + return null; + } + return switch (header) { + case "店铺商城名称", "shopmallname" -> "shopMallName"; + case "asin" -> "asin"; + case "价格", "price" -> "price"; + case "推荐价", "recommendedprice" -> "recommendedPrice"; + case "最低价", "minimumprice" -> "minimumPrice"; + case "第一名", "firstplace" -> "firstPlace"; + case "第二名", "secondplace" -> "secondPlace"; + case "购物车店铺名", "cartshopname" -> "cartShopName"; + case "改价情况", "pricechangestatus" -> "priceChangeStatus"; + case "修改次数", "modifycount" -> "modifyCount"; + case "状态", "status" -> "status"; + default -> null; + }; + } + + private String normalizeCellText(String value) { + if (value == null) { + return ""; + } + return value.replace("\ufeff", "") + .replace("\u3000", " ") + .replace("\r", " ") + .replace("\n", " ") + .trim() + .replaceAll("\\s+", " "); + } + + private String normalizeHeaderText(String value) { + return normalizeCellText(value) + .replaceAll("\\s+", "") + .toLowerCase(Locale.ROOT); + } + + private List splitCsvLine(String line) { + return Arrays.stream((line == null ? "" : line).split(",", -1)) + .map(String::trim) + .toList(); + } + private List dedupeQueueItems(List raw) { Map byNorm = new LinkedHashMap<>(); for (PriceTrackMatchShopsVo.PriceTrackShopQueueItem item : raw) { diff --git a/backend-java/src/main/java/com/nanri/aiimage/modules/productrisk/controller/ProductRiskResolveController.java b/backend-java/src/main/java/com/nanri/aiimage/modules/productrisk/controller/ProductRiskResolveController.java index e401ccb..dad10af 100644 --- a/backend-java/src/main/java/com/nanri/aiimage/modules/productrisk/controller/ProductRiskResolveController.java +++ b/backend-java/src/main/java/com/nanri/aiimage/modules/productrisk/controller/ProductRiskResolveController.java @@ -159,6 +159,12 @@ public class ProductRiskResolveController { return ApiResponse.success(productRiskTaskService.getTaskDetailsBatch(request.getTaskIds())); } + @PostMapping("/tasks/progress/batch") + @Operation(summary = "批量查询任务进度摘要", description = "仅返回任务状态、时间和错误等轻量信息,供前端轮询降载。") + public ApiResponse taskProgressBatch(@Valid @RequestBody ProductRiskTaskBatchRequest request) { + return ApiResponse.success(productRiskTaskService.getTaskProgressBatch(request.getTaskIds())); + } + @PostMapping("/tasks/{taskId}/result") @Operation( summary = "Python 回传处理结果", diff --git a/backend-java/src/main/java/com/nanri/aiimage/modules/productrisk/service/ProductRiskTaskService.java b/backend-java/src/main/java/com/nanri/aiimage/modules/productrisk/service/ProductRiskTaskService.java index f841151..837fd9f 100644 --- a/backend-java/src/main/java/com/nanri/aiimage/modules/productrisk/service/ProductRiskTaskService.java +++ b/backend-java/src/main/java/com/nanri/aiimage/modules/productrisk/service/ProductRiskTaskService.java @@ -147,6 +147,7 @@ public class ProductRiskTaskService { .eq(FileResultEntity::getTaskId, taskId) .eq(FileResultEntity::getModuleType, MODULE_TYPE)); fileTaskMapper.deleteById(taskId); + cleanupTaskCacheIfTerminal(taskId, "DELETE_EMPTY"); } /** @@ -283,6 +284,27 @@ public class ProductRiskTaskService { return batch; } + public ProductRiskTaskBatchVo getTaskProgressBatch(List taskIds) { + ProductRiskTaskBatchVo batch = new ProductRiskTaskBatchVo(); + 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; + } + ProductRiskTaskDetailVo detail = new ProductRiskTaskDetailVo(); + detail.setTask(toTaskItemVo(task)); + batch.getItems().add(detail); + } + return batch; + } + public String resolveResultDownloadUrl(Long resultId, Long userId) { FileResultEntity entity = fileResultMapper.selectById(resultId); if (entity == null || !MODULE_TYPE.equals(entity.getModuleType()) || !userId.equals(entity.getUserId())) { @@ -390,7 +412,7 @@ public class ProductRiskTaskService { if ("SUCCESS".equals(task.getStatus()) || "FAILED".equals(task.getStatus())) { log.warn("[product-risk] submitResult rejected taskId={} status={} payloadShopCount={}", taskId, task.getStatus(), request.getShops() == null ? 0 : request.getShops().size()); - throw new BusinessException("任务已结束,拒绝重复提交"); + throw new BusinessException(40901, "任务已结束,拒绝重复提交"); } String oldStatus = task.getStatus(); 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 4d50b06..1efa657 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 @@ -44,7 +44,7 @@ public class ShopManageController { private final ShopManageGroupService shopManageGroupService; @GetMapping - @Operation(summary = "分页查询店铺", description = "支持按分组、店铺名称、创建人进行筛选") + @Operation(summary = "分页查询店铺", description = "支持按分组、店铺名、当前操作者可见范围筛选") @ApiResponses({ @io.swagger.v3.oas.annotations.responses.ApiResponse( responseCode = "200", @@ -56,14 +56,21 @@ public class ShopManageController { @Parameter(description = "每页数量") @RequestParam(defaultValue = "15") Long pageSize, @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)); + @Parameter(description = "当前操作人用户ID") @RequestParam(required = false) Long operatorId, + @Parameter(description = "是否超级管理员") @RequestParam(defaultValue = "false") Boolean superAdmin) { + return ApiResponse.success(shopManageService.page( + page, + pageSize, + groupId, + shopName, + operatorId, + Boolean.TRUE.equals(superAdmin))); } @PostMapping @Operation(summary = "新增店铺", description = "创建店铺记录,并写入创建人") public ApiResponse create( - @Parameter(description = "当前操作人用户 ID") @RequestParam Long operatorId, + @Parameter(description = "当前操作人用户ID") @RequestParam Long operatorId, @Parameter(description = "是否超级管理员") @RequestParam(defaultValue = "false") Boolean superAdmin, @Valid @RequestBody ShopManageCreateRequest request) { return ApiResponse.success("创建成功", @@ -71,10 +78,10 @@ public class ShopManageController { } @PutMapping("/{id}") - @Operation(summary = "更新店铺", description = "按 ID 更新店铺,普通管理员只能修改自己创建的数据") + @Operation(summary = "更新店铺", description = "按 ID 更新店铺") public ApiResponse update( @Parameter(description = "主键 ID", required = true) @PathVariable Long id, - @Parameter(description = "当前操作人用户 ID") @RequestParam Long operatorId, + @Parameter(description = "当前操作人用户ID") @RequestParam Long operatorId, @Parameter(description = "是否超级管理员") @RequestParam(defaultValue = "false") Boolean superAdmin, @Valid @RequestBody ShopManageUpdateRequest request) { return ApiResponse.success("更新成功", @@ -82,33 +89,42 @@ public class ShopManageController { } @DeleteMapping("/{id}") - @Operation(summary = "删除店铺", description = "按 ID 删除店铺,普通管理员只能删除自己创建的数据") + @Operation(summary = "删除店铺", description = "按 ID 删除店铺") public ApiResponse delete( @Parameter(description = "主键 ID", required = true) @PathVariable Long id, - @Parameter(description = "当前操作人用户 ID") @RequestParam Long operatorId, + @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 = "普通管理员只返回自己创建的分组,超级管理员返回全部") + @Operation(summary = "查询分组列表", description = "普通管理员返回当前可访问的分组,超级管理员返回全部") public ApiResponse> listGroups( - @Parameter(description = "创建人用户 ID,普通管理员只传自己") @RequestParam(required = false) Long createdById) { - return ApiResponse.success(shopManageGroupService.list(createdById)); + @Parameter(description = "创建人用户ID") @RequestParam(required = false) Long createdById, + @Parameter(description = "当前操作人用户ID") @RequestParam(required = false) Long operatorId, + @Parameter(description = "是否超级管理员") @RequestParam(defaultValue = "false") Boolean superAdmin) { + if (Boolean.TRUE.equals(superAdmin)) { + return ApiResponse.success(shopManageGroupService.list(createdById)); + } + return ApiResponse.success(shopManageGroupService.listAccessible(operatorId, false)); } @PostMapping("/groups") @Operation(summary = "新增分组", description = "创建分组并写入创建人") - public ApiResponse createGroup(@Valid @RequestBody ShopManageGroupCreateRequest request) { - return ApiResponse.success("创建成功", shopManageGroupService.create(request)); + public ApiResponse createGroup( + @Parameter(description = "当前操作人用户ID") @RequestParam Long operatorId, + @Parameter(description = "是否超级管理员") @RequestParam(defaultValue = "false") Boolean superAdmin, + @Valid @RequestBody ShopManageGroupCreateRequest request) { + return ApiResponse.success("创建成功", + shopManageGroupService.create(request, operatorId, Boolean.TRUE.equals(superAdmin))); } @PutMapping("/groups/{id}") - @Operation(summary = "更新分组", description = "普通管理员只能修改自己创建的分组") + @Operation(summary = "更新分组", description = "普通管理员只能修改自己可访问的分组") public ApiResponse updateGroup( @Parameter(description = "主键 ID", required = true) @PathVariable Long id, - @Parameter(description = "当前操作人用户 ID") @RequestParam Long operatorId, + @Parameter(description = "当前操作人用户ID") @RequestParam Long operatorId, @Parameter(description = "是否超级管理员") @RequestParam(defaultValue = "false") Boolean superAdmin, @Valid @RequestBody ShopManageGroupUpdateRequest request) { return ApiResponse.success("更新成功", @@ -116,10 +132,10 @@ public class ShopManageController { } @DeleteMapping("/groups/{id}") - @Operation(summary = "删除分组", description = "普通管理员只能删除自己创建的分组") + @Operation(summary = "删除分组", description = "普通管理员只能删除自己可访问的分组") public ApiResponse deleteGroup( @Parameter(description = "主键 ID", required = true) @PathVariable Long id, - @Parameter(description = "当前操作人用户 ID") @RequestParam Long operatorId, + @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); 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 d840a87..74c951b 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 @@ -1,8 +1,8 @@ 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.dto.SkipPriceAsinCreateRequest; 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; @@ -36,14 +36,27 @@ public class SkipPriceAsinController { @Parameter(description = "每页数量") @RequestParam(defaultValue = "15") Long pageSize, @Parameter(description = "分组ID") @RequestParam(required = false) Long groupId, @Parameter(description = "店铺名") @RequestParam(required = false) String shopName, - @Parameter(description = "ASIN") @RequestParam(required = false) String asin) { - return ApiResponse.success(skipPriceAsinService.page(page, pageSize, groupId, shopName, asin)); + @Parameter(description = "ASIN") @RequestParam(required = false) String asin, + @Parameter(description = "当前操作人用户ID") @RequestParam(required = false) Long operatorId, + @Parameter(description = "是否超级管理员") @RequestParam(defaultValue = "false") Boolean superAdmin) { + return ApiResponse.success(skipPriceAsinService.page( + page, + pageSize, + groupId, + shopName, + asin, + operatorId, + Boolean.TRUE.equals(superAdmin))); } @PostMapping @Operation(summary = "新增或覆盖跳过跟价 ASIN") - public ApiResponse create(@Valid @RequestBody SkipPriceAsinCreateRequest request) { - return ApiResponse.success("保存成功", skipPriceAsinService.createOrUpdate(request)); + public ApiResponse create( + @Parameter(description = "当前操作人用户ID") @RequestParam Long operatorId, + @Parameter(description = "是否超级管理员") @RequestParam(defaultValue = "false") Boolean superAdmin, + @Valid @RequestBody SkipPriceAsinCreateRequest request) { + return ApiResponse.success("保存成功", + skipPriceAsinService.createOrUpdate(request, operatorId, Boolean.TRUE.equals(superAdmin))); } @PutMapping("/{id}/countries/{country}") @@ -51,16 +64,25 @@ public class SkipPriceAsinController { public ApiResponse updateCountry( @Parameter(description = "主键ID", required = true) @PathVariable Long id, @Parameter(description = "国家编码", required = true) @PathVariable String country, + @Parameter(description = "当前操作人用户ID") @RequestParam Long operatorId, + @Parameter(description = "是否超级管理员") @RequestParam(defaultValue = "false") Boolean superAdmin, @Valid @RequestBody SkipPriceAsinCountryUpdateRequest request) { - return ApiResponse.success("保存成功", skipPriceAsinService.updateCountry(id, country, request.getAsin())); + return ApiResponse.success("保存成功", skipPriceAsinService.updateCountry( + id, + country, + request.getAsin(), + operatorId, + Boolean.TRUE.equals(superAdmin))); } @DeleteMapping("/{id}/countries/{country}") @Operation(summary = "删除指定国家的跳过跟价 ASIN") public ApiResponse deleteCountry( @Parameter(description = "主键ID", required = true) @PathVariable Long id, - @Parameter(description = "国家编码", required = true) @PathVariable String country) { - skipPriceAsinService.deleteCountry(id, country); + @Parameter(description = "国家编码", required = true) @PathVariable String country, + @Parameter(description = "当前操作人用户ID") @RequestParam Long operatorId, + @Parameter(description = "是否超级管理员") @RequestParam(defaultValue = "false") Boolean superAdmin) { + skipPriceAsinService.deleteCountry(id, country, operatorId, Boolean.TRUE.equals(superAdmin)); return ApiResponse.success("删除成功", null); } } 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 62d2d5b..236cba7 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 @@ -12,7 +12,11 @@ public class ShopManageGroupCreateRequest { @NotBlank(message = "分组名称不能为空") private String groupName; - @Schema(description = "创建人用户 ID,由后台代理透传", requiredMode = Schema.RequiredMode.REQUIRED) + @Schema(description = "关联用户ID", requiredMode = Schema.RequiredMode.REQUIRED) + @NotNull(message = "关联用户不能为空") + private Long userId; + + @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 1469aca..cfa0f92 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 @@ -2,6 +2,7 @@ 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 @@ -10,4 +11,8 @@ public class ShopManageGroupUpdateRequest { @Schema(description = "分组名称", requiredMode = Schema.RequiredMode.REQUIRED) @NotBlank(message = "分组名称不能为空") private String groupName; + + @Schema(description = "关联用户ID", requiredMode = Schema.RequiredMode.REQUIRED) + @NotNull(message = "关联用户不能为空") + private Long userId; } diff --git a/backend-java/src/main/java/com/nanri/aiimage/modules/shopkey/model/dto/SkipPriceAsinCreateRequest.java b/backend-java/src/main/java/com/nanri/aiimage/modules/shopkey/model/dto/SkipPriceAsinCreateRequest.java index 537d03e..22d666d 100644 --- a/backend-java/src/main/java/com/nanri/aiimage/modules/shopkey/model/dto/SkipPriceAsinCreateRequest.java +++ b/backend-java/src/main/java/com/nanri/aiimage/modules/shopkey/model/dto/SkipPriceAsinCreateRequest.java @@ -6,6 +6,7 @@ import jakarta.validation.constraints.NotNull; import lombok.Data; import java.util.List; +import java.util.Map; @Data public class SkipPriceAsinCreateRequest { @@ -19,6 +20,13 @@ public class SkipPriceAsinCreateRequest { @NotEmpty(message = "请至少选择一个国家") private List countries; - @NotBlank(message = "ASIN 不能为空") + /** + * 兼容旧请求: 选中的国家共用同一个 ASIN。 + */ private String asin; + + /** + * 新请求: 每个国家对应独立 ASIN,例如 {"DE":"B0...", "UK":"B0..."}。 + */ + private Map asinMappings; } 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 623de76..33e4dee 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 @@ -15,6 +15,8 @@ public class ShopManageGroupEntity { @TableId(type = IdType.AUTO) private Long id; private String groupName; + @TableField("user_id") + private Long userId; @TableField("created_by_id") private Long createdById; private LocalDateTime createdAt; diff --git a/backend-java/src/main/java/com/nanri/aiimage/modules/shopkey/model/vo/ShopManageGroupItemVo.java b/backend-java/src/main/java/com/nanri/aiimage/modules/shopkey/model/vo/ShopManageGroupItemVo.java index a3da49e..3f6de7f 100644 --- a/backend-java/src/main/java/com/nanri/aiimage/modules/shopkey/model/vo/ShopManageGroupItemVo.java +++ b/backend-java/src/main/java/com/nanri/aiimage/modules/shopkey/model/vo/ShopManageGroupItemVo.java @@ -8,6 +8,8 @@ import java.time.LocalDateTime; public class ShopManageGroupItemVo { private Long id; private String groupName; + private Long userId; + private String username; 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 95d2314..b18f70d 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 @@ -2,6 +2,8 @@ package com.nanri.aiimage.modules.shopkey.service; import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper; import com.nanri.aiimage.common.exception.BusinessException; +import com.nanri.aiimage.modules.permission.mapper.AdminUserMapper; +import com.nanri.aiimage.modules.permission.model.entity.AdminUserEntity; 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; @@ -13,7 +15,9 @@ import lombok.RequiredArgsConstructor; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; +import java.util.LinkedHashSet; import java.util.List; +import java.util.Set; @Service @RequiredArgsConstructor @@ -21,6 +25,7 @@ public class ShopManageGroupService { private final ShopManageGroupMapper groupMapper; private final ShopManageMapper shopManageMapper; + private final AdminUserMapper adminUserMapper; public List list() { return list(null); @@ -35,13 +40,56 @@ public class ShopManageGroupService { .toList(); } + public List listAccessible(Long operatorId, boolean superAdmin) { + if (superAdmin) { + return list(); + } + Long normalizedOperatorId = normalizePositiveId(operatorId, "操作人不能为空"); + return groupMapper.selectList(new LambdaQueryWrapper() + .and(wrapper -> wrapper + .eq(ShopManageGroupEntity::getCreatedById, normalizedOperatorId) + .or() + .eq(ShopManageGroupEntity::getUserId, normalizedOperatorId)) + .orderByAsc(ShopManageGroupEntity::getId)) + .stream() + .map(this::toVo) + .toList(); + } + + public Set listAccessibleGroupIds(Long operatorId, boolean superAdmin) { + if (superAdmin) { + return groupMapper.selectList(new LambdaQueryWrapper() + .select(ShopManageGroupEntity::getId)) + .stream() + .map(ShopManageGroupEntity::getId) + .filter(id -> id != null && id > 0) + .collect(java.util.stream.Collectors.toCollection(LinkedHashSet::new)); + } + Long normalizedOperatorId = normalizePositiveId(operatorId, "操作人不能为空"); + return groupMapper.selectList(new LambdaQueryWrapper() + .select(ShopManageGroupEntity::getId) + .and(wrapper -> wrapper + .eq(ShopManageGroupEntity::getCreatedById, normalizedOperatorId) + .or() + .eq(ShopManageGroupEntity::getUserId, normalizedOperatorId))) + .stream() + .map(ShopManageGroupEntity::getId) + .filter(id -> id != null && id > 0) + .collect(java.util.stream.Collectors.toCollection(LinkedHashSet::new)); + } + @Transactional - public ShopManageGroupItemVo create(ShopManageGroupCreateRequest request) { + public ShopManageGroupItemVo create(ShopManageGroupCreateRequest request, Long operatorId, boolean superAdmin) { Long createdById = normalizePositiveId(request.getCreatedById(), "创建人不能为空"); + AdminUserEntity bindUser = getBindableUser(request.getUserId(), operatorId, superAdmin); String groupName = normalizeRequired(request.getGroupName(), "分组名称不能为空"); - ensureGroupNameUnique(groupName, null, createdById, false); + ensureGroupNameMatchesUser(groupName, bindUser); + ensureGroupNameUnique(groupName, null); + ensureUserUnique(request.getUserId(), null); + ShopManageGroupEntity entity = new ShopManageGroupEntity(); entity.setGroupName(groupName); + entity.setUserId(bindUser.getId()); entity.setCreatedById(createdById); groupMapper.insert(entity); return toVo(getById(entity.getId())); @@ -51,9 +99,14 @@ public class ShopManageGroupService { public ShopManageGroupItemVo update(Long id, Long operatorId, boolean superAdmin, ShopManageGroupUpdateRequest request) { ShopManageGroupEntity entity = getById(id); validateOwnership(entity, operatorId, superAdmin); + AdminUserEntity bindUser = getBindableUser(request.getUserId(), operatorId, superAdmin); String groupName = normalizeRequired(request.getGroupName(), "分组名称不能为空"); - ensureGroupNameUnique(groupName, id, operatorId, superAdmin); + ensureGroupNameMatchesUser(groupName, bindUser); + ensureGroupNameUnique(groupName, id); + ensureUserUnique(bindUser.getId(), id); + entity.setGroupName(groupName); + entity.setUserId(bindUser.getId()); groupMapper.updateById(entity); return toVo(getById(id)); } @@ -84,12 +137,9 @@ public class ShopManageGroupService { return entity; } - private void ensureGroupNameUnique(String groupName, Long excludeId, Long operatorId, boolean superAdmin) { + private void ensureGroupNameUnique(String groupName, Long excludeId) { LambdaQueryWrapper query = new LambdaQueryWrapper() .eq(ShopManageGroupEntity::getGroupName, groupName); - if (!superAdmin) { - query.eq(ShopManageGroupEntity::getCreatedById, normalizePositiveId(operatorId, "操作人不能为空")); - } if (excludeId != null) { query.ne(ShopManageGroupEntity::getId, excludeId); } @@ -99,16 +149,54 @@ public class ShopManageGroupService { } } + private void ensureUserUnique(Long userId, Long excludeId) { + LambdaQueryWrapper query = new LambdaQueryWrapper() + .eq(ShopManageGroupEntity::getUserId, userId); + if (excludeId != null) { + query.ne(ShopManageGroupEntity::getId, excludeId); + } + Long count = groupMapper.selectCount(query); + if (count != null && count > 0) { + throw new BusinessException("该用户已绑定分组"); + } + } + + private void ensureGroupNameMatchesUser(String groupName, AdminUserEntity bindUser) { + if (bindUser == null || bindUser.getUsername() == null || !groupName.equals(bindUser.getUsername().trim())) { + throw new BusinessException("分组名称必须与关联用户名一致"); + } + } + 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("只能操作自己创建的分组数据"); + boolean createdBySelf = entity.getCreatedById() != null && entity.getCreatedById().equals(normalizedOperatorId); + boolean boundToSelf = entity.getUserId() != null && entity.getUserId().equals(normalizedOperatorId); + if (!createdBySelf && !boundToSelf) { + throw new BusinessException("只能操作自己有权限的分组数据"); } } + private AdminUserEntity getBindableUser(Long userId, Long operatorId, boolean superAdmin) { + Long normalizedUserId = normalizePositiveId(userId, "关联用户不能为空"); + AdminUserEntity user = adminUserMapper.selectById(normalizedUserId); + if (user == null) { + throw new BusinessException("关联用户不存在"); + } + if (superAdmin) { + return user; + } + Long normalizedOperatorId = normalizePositiveId(operatorId, "操作人不能为空"); + boolean self = user.getId() != null && user.getId().equals(normalizedOperatorId); + boolean createdBySelf = user.getCreatedById() != null && user.getCreatedById().equals(normalizedOperatorId); + if (!self && !createdBySelf) { + throw new BusinessException("只能关联自己可见的用户"); + } + return user; + } + private String normalizeRequired(String value, String message) { String normalized = value == null ? "" : value.trim(); if (normalized.isEmpty()) { @@ -128,6 +216,9 @@ public class ShopManageGroupService { ShopManageGroupItemVo vo = new ShopManageGroupItemVo(); vo.setId(entity.getId()); vo.setGroupName(entity.getGroupName()); + vo.setUserId(entity.getUserId()); + AdminUserEntity bindUser = entity.getUserId() == null ? null : adminUserMapper.selectById(entity.getUserId()); + vo.setUsername(bindUser == null ? "" : (bindUser.getUsername() == null ? "" : bindUser.getUsername())); vo.setCreatedAt(entity.getCreatedAt()); vo.setUpdatedAt(entity.getUpdatedAt()); return vo; 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 f6ab333..4f72fb6 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 @@ -18,6 +18,7 @@ import org.springframework.transaction.annotation.Transactional; import java.util.LinkedHashMap; import java.util.List; import java.util.Map; +import java.util.Set; @Service @RequiredArgsConstructor @@ -27,24 +28,27 @@ public class ShopManageService { private final ShopManageGroupService shopManageGroupService; private final ShopCredentialCryptoService shopCredentialCryptoService; - public ShopManagePageVo page(long page, long pageSize) { - 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) { + public ShopManagePageVo page(long page, long pageSize, Long groupId, String shopName, Long operatorId, boolean superAdmin) { long safePage = Math.max(page, 1); long safePageSize = Math.min(Math.max(pageSize, 1), 100); String safeShopName = shopName == null ? "" : shopName.trim(); + Set accessibleGroupIds = superAdmin ? Set.of() : shopManageGroupService.listAccessibleGroupIds(operatorId, false); 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); + + if (!superAdmin) { + Long normalizedOperatorId = normalizePositiveId(operatorId, "操作人不能为空"); + query.and(wrapper -> { + wrapper.eq(ShopManageEntity::getCreatedById, normalizedOperatorId); + if (!accessibleGroupIds.isEmpty()) { + wrapper.or().in(ShopManageEntity::getGroupId, accessibleGroupIds); + } + }); + } + Long total = shopManageMapper.selectCount(query); List rows = shopManageMapper .selectList(query.last("LIMIT " + ((safePage - 1) * safePageSize) + ", " + safePageSize)); @@ -90,7 +94,7 @@ public class ShopManageService { @Transactional public ShopManageItemVo update(Long id, Long operatorId, boolean superAdmin, ShopManageUpdateRequest request) { ShopManageEntity entity = getById(id); - validateOwnership(entity, operatorId, superAdmin); + validateGroupAccess(entity, operatorId, superAdmin); ShopManageGroupEntity group = shopManageGroupService.getAccessibleById(request.getGroupId(), operatorId, superAdmin); String shopName = normalizeRequired(request.getShopName(), "店铺名称不能为空"); String mallName = normalizeRequired(request.getMallName(), "商城名称不能为空"); @@ -110,7 +114,7 @@ public class ShopManageService { @Transactional public void delete(Long id, Long operatorId, boolean superAdmin) { ShopManageEntity entity = getById(id); - validateOwnership(entity, operatorId, superAdmin); + validateGroupAccess(entity, operatorId, superAdmin); shopManageMapper.deleteById(entity.getId()); } @@ -146,14 +150,8 @@ 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 void validateGroupAccess(ShopManageEntity entity, Long operatorId, boolean superAdmin) { + shopManageGroupService.getAccessibleById(entity.getGroupId(), operatorId, superAdmin); } private String normalizeRequired(String value, String message) { @@ -208,7 +206,6 @@ public class ShopManageService { vo.setMallName(entity.getMallName()); vo.setAccount(entity.getAccount()); String masked = entity.getPassword() == null || entity.getPassword().isBlank() ? "" : "******"; - // 兼容旧前端字段:password 继续返回脱敏值 vo.setPassword(masked); vo.setPasswordMasked(masked); vo.setCreatedAt(entity.getCreatedAt()); 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 8a95f2d..0407766 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 @@ -28,11 +28,13 @@ public class SkipPriceAsinService { private final SkipPriceAsinMapper skipPriceAsinMapper; private final ShopManageGroupService shopManageGroupService; - public SkipPriceAsinPageVo page(long page, long pageSize, Long groupId, String shopName, String asin) { + public SkipPriceAsinPageVo page(long page, long pageSize, Long groupId, String shopName, String asin, + Long operatorId, boolean superAdmin) { long safePage = Math.max(page, 1); long safePageSize = Math.min(Math.max(pageSize, 1), 100); String safeShopName = normalizeBlank(shopName); String safeAsin = normalizeBlank(asin); + Set accessibleGroupIds = superAdmin ? Set.of() : shopManageGroupService.listAccessibleGroupIds(operatorId, false); LambdaQueryWrapper query = new LambdaQueryWrapper() .eq(groupId != null && groupId > 0, SkipPriceAsinEntity::getGroupId, groupId) @@ -45,6 +47,14 @@ public class SkipPriceAsinService { .or().like(SkipPriceAsinEntity::getAsinEs, safeAsin)) .orderByDesc(SkipPriceAsinEntity::getId); + if (!superAdmin) { + if (accessibleGroupIds.isEmpty()) { + query.eq(SkipPriceAsinEntity::getId, -1L); + } else { + query.in(SkipPriceAsinEntity::getGroupId, accessibleGroupIds); + } + } + Long total = skipPriceAsinMapper.selectCount(query); List rows = skipPriceAsinMapper .selectList(query.last("LIMIT " + ((safePage - 1) * safePageSize) + ", " + safePageSize)); @@ -64,11 +74,11 @@ public class SkipPriceAsinService { } @Transactional - public SkipPriceAsinItemVo createOrUpdate(SkipPriceAsinCreateRequest request) { - ShopManageGroupEntity group = shopManageGroupService.getById(request.getGroupId()); + public SkipPriceAsinItemVo createOrUpdate(SkipPriceAsinCreateRequest request, Long operatorId, boolean superAdmin) { + ShopManageGroupEntity group = shopManageGroupService.getAccessibleById(request.getGroupId(), operatorId, superAdmin); String shopName = normalizeRequired(request.getShopName(), "店铺名不能为空"); - String asin = normalizeAsin(request.getAsin()); Set countries = normalizeCountries(request.getCountries()); + Map countryAsinMap = normalizeCountryAsinMap(countries, request); SkipPriceAsinEntity entity = skipPriceAsinMapper.selectOne(new LambdaQueryWrapper() .eq(SkipPriceAsinEntity::getGroupId, group.getId()) @@ -80,8 +90,8 @@ public class SkipPriceAsinService { entity.setShopName(shopName); } - for (String country : countries) { - setCountryAsin(entity, country, asin); + for (Map.Entry entry : countryAsinMap.entrySet()) { + setCountryAsin(entity, entry.getKey(), entry.getValue()); } if (entity.getId() == null) { @@ -94,8 +104,9 @@ public class SkipPriceAsinService { } @Transactional - public void deleteCountry(Long id, String country) { + public void deleteCountry(Long id, String country, Long operatorId, boolean superAdmin) { SkipPriceAsinEntity entity = getById(id); + validateGroupAccess(entity, operatorId, superAdmin); String normalizedCountry = normalizeCountry(country); setCountryAsin(entity, normalizedCountry, ""); if (isEmptyRow(entity)) { @@ -106,8 +117,9 @@ public class SkipPriceAsinService { } @Transactional - public SkipPriceAsinItemVo updateCountry(Long id, String country, String asin) { + public SkipPriceAsinItemVo updateCountry(Long id, String country, String asin, Long operatorId, boolean superAdmin) { SkipPriceAsinEntity entity = getById(id); + validateGroupAccess(entity, operatorId, superAdmin); String normalizedCountry = normalizeCountry(country); String normalizedAsin = normalizeAsin(asin); setCountryAsin(entity, normalizedCountry, normalizedAsin); @@ -131,6 +143,10 @@ public class SkipPriceAsinService { return entity; } + private void validateGroupAccess(SkipPriceAsinEntity entity, Long operatorId, boolean superAdmin) { + shopManageGroupService.getAccessibleById(entity.getGroupId(), operatorId, superAdmin); + } + private SkipPriceAsinItemVo toItemVo(SkipPriceAsinEntity entity, String groupName) { SkipPriceAsinItemVo vo = new SkipPriceAsinItemVo(); vo.setId(entity.getId()); @@ -176,6 +192,27 @@ public class SkipPriceAsinService { return normalized; } + private Map normalizeCountryAsinMap(Set countries, SkipPriceAsinCreateRequest request) { + LinkedHashMap normalized = new LinkedHashMap<>(); + Map requestMappings = request.getAsinMappings(); + if (requestMappings != null && !requestMappings.isEmpty()) { + for (String country : countries) { + String asin = requestMappings.get(country); + if (asin == null) { + asin = requestMappings.get(country.toLowerCase(Locale.ROOT)); + } + normalized.put(country, normalizeAsin(asin)); + } + return normalized; + } + + String asin = normalizeAsin(request.getAsin()); + for (String country : countries) { + normalized.put(country, asin); + } + return normalized; + } + private String normalizeCountry(String country) { String normalized = normalizeBlank(country).toUpperCase(Locale.ROOT); if (!SUPPORTED_COUNTRIES.contains(normalized)) { @@ -228,10 +265,6 @@ public class SkipPriceAsinService { && normalizeBlank(entity.getAsinEs()).isEmpty(); } - /** - * 按店铺名列表批量查询跳过跟价ASIN,返回 Map{shopName -> {countryCode -> asin列表}} - * 用于跟价任务创建时将skip ASIN数据下发给Python队列 - */ public Map> listSkipAsinsByShops(List shopNames) { if (shopNames == null || shopNames.isEmpty()) { return Map.of(); @@ -252,4 +285,37 @@ public class SkipPriceAsinService { } return result; } + + public Map> listAllSkipAsinsByCountry() { + List rows = skipPriceAsinMapper.selectList( + new LambdaQueryWrapper() + .orderByDesc(SkipPriceAsinEntity::getId)); + Map> grouped = new LinkedHashMap<>(); + grouped.put("DE", new LinkedHashSet<>()); + grouped.put("UK", new LinkedHashSet<>()); + grouped.put("FR", new LinkedHashSet<>()); + grouped.put("IT", new LinkedHashSet<>()); + grouped.put("ES", new LinkedHashSet<>()); + + for (SkipPriceAsinEntity row : rows) { + addCountryAsin(grouped.get("DE"), row.getAsinDe()); + addCountryAsin(grouped.get("UK"), row.getAsinUk()); + addCountryAsin(grouped.get("FR"), row.getAsinFr()); + addCountryAsin(grouped.get("IT"), row.getAsinIt()); + addCountryAsin(grouped.get("ES"), row.getAsinEs()); + } + + Map> result = new LinkedHashMap<>(); + for (Map.Entry> entry : grouped.entrySet()) { + result.put(entry.getKey(), List.copyOf(entry.getValue())); + } + return result; + } + + private void addCountryAsin(Set bucket, String asin) { + String normalized = normalizeBlank(asin); + if (!normalized.isEmpty()) { + bucket.add(normalized); + } + } } diff --git a/backend-java/src/main/java/com/nanri/aiimage/modules/shopmatch/controller/ShopMatchController.java b/backend-java/src/main/java/com/nanri/aiimage/modules/shopmatch/controller/ShopMatchController.java index 0e4d174..afd9300 100644 --- a/backend-java/src/main/java/com/nanri/aiimage/modules/shopmatch/controller/ShopMatchController.java +++ b/backend-java/src/main/java/com/nanri/aiimage/modules/shopmatch/controller/ShopMatchController.java @@ -163,6 +163,12 @@ public class ShopMatchController { return ApiResponse.success(shopMatchTaskService.getTaskDetailsBatch(request.getTaskIds())); } + @PostMapping("/tasks/progress/batch") + @Operation(summary = "批量查询任务进度摘要", description = "仅返回任务状态、阶段信息与时间等轻量字段,供前端轮询降载。") + public ApiResponse taskProgressBatch(@Valid @RequestBody ProductRiskTaskBatchRequest request) { + return ApiResponse.success(shopMatchTaskService.getTaskProgressBatch(request.getTaskIds())); + } + @PostMapping("/tasks/{taskId}/result") @Operation(summary = "提交匹配结果", description = "Python 可多次分批回传结果;后端会组装文件并在超时场景自动补偿收尾。") public ApiResponse submitResult( diff --git a/backend-java/src/main/java/com/nanri/aiimage/modules/shopmatch/service/ShopMatchTaskCacheService.java b/backend-java/src/main/java/com/nanri/aiimage/modules/shopmatch/service/ShopMatchTaskCacheService.java index 31a908a..1f98db7 100644 --- a/backend-java/src/main/java/com/nanri/aiimage/modules/shopmatch/service/ShopMatchTaskCacheService.java +++ b/backend-java/src/main/java/com/nanri/aiimage/modules/shopmatch/service/ShopMatchTaskCacheService.java @@ -5,10 +5,17 @@ import com.nanri.aiimage.common.exception.BusinessException; import com.nanri.aiimage.modules.shopmatch.model.dto.ShopMatchShopPayloadDto; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; -import org.springframework.data.redis.core.StringRedisTemplate; import org.springframework.stereotype.Service; +import java.io.IOException; +import java.nio.charset.StandardCharsets; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.StandardOpenOption; +import java.security.MessageDigest; import java.time.Duration; +import java.util.Comparator; +import java.util.HexFormat; import java.util.LinkedHashMap; import java.util.Map; @@ -19,21 +26,29 @@ public class ShopMatchTaskCacheService { private static final long PAYLOAD_TTL_HOURS = 24; - private final StringRedisTemplate stringRedisTemplate; private final ObjectMapper objectMapper; public ShopMatchShopPayloadDto getShopMergedPayload(Long taskId, String shopKey) { if (taskId == null || taskId <= 0 || shopKey == null || shopKey.isBlank()) { return null; } - Object raw = stringRedisTemplate.opsForHash().get(buildShopPayloadKey(taskId), shopKey); - if (!(raw instanceof String json) || json.isBlank()) { + Path file = buildShopPayloadFile(taskId, shopKey); + if (!Files.isRegularFile(file)) { return null; } try { - return objectMapper.readValue(json, ShopMatchShopPayloadDto.class); + PayloadFile payloadFile = objectMapper.readValue(Files.readString(file), PayloadFile.class); + if (payloadFile == null || payloadFile.payload == null) { + return null; + } + if (payloadFile.shopKey == null || !shopKey.equals(payloadFile.shopKey)) { + log.warn("[shop-match] payload file shop key mismatch taskId={} shopKey={} storedKey={}", + taskId, shopKey, payloadFile.shopKey); + return null; + } + return payloadFile.payload; } catch (Exception ex) { - throw new BusinessException("读取匹配店铺缓存失败"); + throw new BusinessException("璇诲彇鍖归厤搴楅摵缂撳瓨澶辫触"); } } @@ -42,11 +57,16 @@ public class ShopMatchTaskCacheService { return; } try { - String key = buildShopPayloadKey(taskId); - stringRedisTemplate.opsForHash().put(key, shopKey, objectMapper.writeValueAsString(payload)); - stringRedisTemplate.expire(key, Duration.ofHours(PAYLOAD_TTL_HOURS)); + Files.createDirectories(buildTaskDir(taskId)); + Files.writeString( + buildShopPayloadFile(taskId, shopKey), + objectMapper.writeValueAsString(new PayloadFile(shopKey, payload)), + StandardOpenOption.CREATE, + StandardOpenOption.TRUNCATE_EXISTING, + StandardOpenOption.WRITE + ); } catch (Exception ex) { - throw new BusinessException("暂存匹配店铺结果失败"); + throw new BusinessException("鏆傚瓨鍖归厤搴楅摵缁撴灉澶辫触"); } } @@ -54,25 +74,52 @@ public class ShopMatchTaskCacheService { if (taskId == null || taskId <= 0 || shopKey == null || shopKey.isBlank()) { return; } - stringRedisTemplate.opsForHash().delete(buildShopPayloadKey(taskId), shopKey); + try { + Files.deleteIfExists(buildShopPayloadFile(taskId, shopKey)); + } catch (IOException ex) { + log.warn("[shop-match] delete shop payload failed taskId={} shopKey={} msg={}", taskId, shopKey, ex.getMessage()); + } } public Map getAllShopMergedPayload(Long taskId) { try { - Map raw = stringRedisTemplate.opsForHash().entries(buildShopPayloadKey(taskId)); - if (raw == null || raw.isEmpty()) { + Path taskDir = buildTaskDir(taskId); + if (!Files.isDirectory(taskDir)) { return Map.of(); } LinkedHashMap out = new LinkedHashMap<>(); - for (Map.Entry entry : raw.entrySet()) { - if (!(entry.getKey() instanceof String key) || !(entry.getValue() instanceof String val) || val.isBlank()) { - continue; + try (var stream = Files.list(taskDir)) { + for (Path path : stream + .filter(file -> file.getFileName().toString().endsWith(".json")) + .sorted(Comparator.comparing(file -> file.getFileName().toString())) + .toList()) { + PayloadFile payloadFile = objectMapper.readValue(Files.readString(path), PayloadFile.class); + if (payloadFile == null || payloadFile.shopKey == null || payloadFile.shopKey.isBlank() || payloadFile.payload == null) { + continue; + } + out.put(payloadFile.shopKey, payloadFile.payload); } - out.put(key, objectMapper.readValue(val, ShopMatchShopPayloadDto.class)); } return out; } catch (Exception ex) { - throw new BusinessException("读取匹配店铺缓存失败"); + throw new BusinessException("璇诲彇鍖归厤搴楅摵缂撳瓨澶辫触"); + } + } + + public boolean hasAnyShopMergedPayload(Long taskId) { + if (taskId == null || taskId <= 0) { + return false; + } + Path taskDir = buildTaskDir(taskId); + if (!Files.isDirectory(taskDir)) { + return false; + } + try (var stream = Files.list(taskDir)) { + return stream.anyMatch(file -> Files.isRegularFile(file) + && file.getFileName().toString().endsWith(".json")); + } catch (IOException ex) { + log.warn("[shop-match] scan task cache failed taskId={} msg={}", taskId, ex.getMessage()); + return false; } } @@ -81,13 +128,57 @@ public class ShopMatchTaskCacheService { return; } try { - stringRedisTemplate.delete(buildShopPayloadKey(taskId)); + Path taskDir = buildTaskDir(taskId); + if (!Files.exists(taskDir)) { + return; + } + try (var walk = Files.walk(taskDir)) { + walk.sorted(Comparator.reverseOrder()).forEach(path -> { + try { + Files.deleteIfExists(path); + } catch (IOException ex) { + log.warn("[shop-match] delete task cache path failed taskId={} path={} msg={}", + taskId, path, ex.getMessage()); + } + }); + } } catch (Exception ex) { log.warn("[shop-match] delete task cache failed taskId={} msg={}", taskId, ex.getMessage()); } } - private String buildShopPayloadKey(Long taskId) { - return "shop-match:task:shop-payload:" + taskId; + public Duration getPayloadTtl() { + return Duration.ofHours(PAYLOAD_TTL_HOURS); + } + + private Path buildTaskDir(Long taskId) { + return Path.of(System.getProperty("java.io.tmpdir"), "shop-match-cache", String.valueOf(taskId)); + } + + private Path buildShopPayloadFile(Long taskId, String shopKey) { + return buildTaskDir(taskId).resolve(hashShopKey(shopKey) + ".json"); + } + + private String hashShopKey(String shopKey) { + try { + MessageDigest digest = MessageDigest.getInstance("SHA-256"); + byte[] bytes = digest.digest(shopKey.trim().getBytes(StandardCharsets.UTF_8)); + return HexFormat.of().formatHex(bytes); + } catch (Exception ex) { + throw new IllegalStateException("failed to hash shop key", ex); + } + } + + private static class PayloadFile { + public String shopKey; + public ShopMatchShopPayloadDto payload; + + public PayloadFile() { + } + + public PayloadFile(String shopKey, ShopMatchShopPayloadDto payload) { + this.shopKey = shopKey; + this.payload = payload; + } } } diff --git a/backend-java/src/main/java/com/nanri/aiimage/modules/shopmatch/service/ShopMatchTaskService.java b/backend-java/src/main/java/com/nanri/aiimage/modules/shopmatch/service/ShopMatchTaskService.java index b37b684..c308662 100644 --- a/backend-java/src/main/java/com/nanri/aiimage/modules/shopmatch/service/ShopMatchTaskService.java +++ b/backend-java/src/main/java/com/nanri/aiimage/modules/shopmatch/service/ShopMatchTaskService.java @@ -185,6 +185,25 @@ public class ShopMatchTaskService { return batch; } + public ProductRiskTaskBatchVo getTaskProgressBatch(List taskIds) { + ProductRiskTaskBatchVo batch = new ProductRiskTaskBatchVo(); + 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(buildTaskProgressDetail(task)); + } + return batch; + } + public String resolveResultDownloadUrl(Long resultId, Long userId) { FileResultEntity entity = fileResultMapper.selectById(resultId); if (entity == null || !MODULE_TYPE.equals(entity.getModuleType()) || !userId.equals(entity.getUserId())) { @@ -376,7 +395,7 @@ public class ShopMatchTaskService { throw new BusinessException("任务不存在"); } if ("SUCCESS".equals(task.getStatus()) || "FAILED".equals(task.getStatus())) { - throw new BusinessException("任务已结束,拒绝重复提交"); + throw new BusinessException(40901, "任务已结束,拒绝重复提交"); } List resultRows = fileResultMapper.selectList(new LambdaQueryWrapper() @@ -595,7 +614,11 @@ public class ShopMatchTaskService { task.setSuccessFileCount(ok); task.setFailedFileCount(fail); task.setUpdatedAt(now()); - if (!allDone) { + if (shouldRemainRunningUntilNextStage(task)) { + task.setStatus("RUNNING"); + task.setErrorMessage(null); + task.setFinishedAt(null); + } else if (!allDone) { task.setStatus("RUNNING"); task.setErrorMessage(null); task.setFinishedAt(null); @@ -623,6 +646,18 @@ public class ShopMatchTaskService { } } + private boolean shouldRemainRunningUntilNextStage(FileTaskEntity task) { + ShopMatchCreateTaskRequest request = parseTaskRequestSilently(task); + if (request == null || request.getScheduleTimes() == null || request.getScheduleTimes().isEmpty()) { + return false; + } + Integer activeStageIndex = request.getActiveStageIndex(); + if (activeStageIndex == null) { + return false; + } + return activeStageIndex < request.getScheduleTimes().size() - 1; + } + private ProductRiskTaskDetailVo buildTaskDetail(FileTaskEntity task) { ProductRiskTaskDetailVo detail = new ProductRiskTaskDetailVo(); detail.setTask(toTaskItemVo(task)); @@ -636,6 +671,12 @@ public class ShopMatchTaskService { return detail; } + private ProductRiskTaskDetailVo buildTaskProgressDetail(FileTaskEntity task) { + ProductRiskTaskDetailVo detail = new ProductRiskTaskDetailVo(); + detail.setTask(toTaskItemVo(task)); + return detail; + } + private List buildSnapshotFromDb(Long taskId, String taskStatus) { List rows = fileResultMapper.selectList(new LambdaQueryWrapper() .eq(FileResultEntity::getTaskId, taskId) diff --git a/backend-java/src/main/java/com/nanri/aiimage/modules/split/service/SplitRunService.java b/backend-java/src/main/java/com/nanri/aiimage/modules/split/service/SplitRunService.java index 524de90..ad2477e 100644 --- a/backend-java/src/main/java/com/nanri/aiimage/modules/split/service/SplitRunService.java +++ b/backend-java/src/main/java/com/nanri/aiimage/modules/split/service/SplitRunService.java @@ -5,6 +5,7 @@ import cn.hutool.core.util.IdUtil; import cn.hutool.json.JSONUtil; import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper; import com.nanri.aiimage.common.exception.BusinessException; +import com.nanri.aiimage.common.util.ExcelStreamReader; import com.nanri.aiimage.config.StorageProperties; import com.nanri.aiimage.modules.file.service.oss.OssStorageService; import com.nanri.aiimage.modules.split.model.dto.SplitRunRequest; @@ -17,20 +18,14 @@ 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 lombok.RequiredArgsConstructor; -import org.apache.poi.ss.usermodel.Cell; -import org.apache.poi.ss.usermodel.DataFormatter; -import org.apache.poi.ss.usermodel.Row; -import org.apache.poi.ss.usermodel.Sheet; -import org.apache.poi.ss.usermodel.Workbook; -import org.apache.poi.ss.usermodel.WorkbookFactory; -import org.apache.poi.xssf.usermodel.XSSFWorkbook; +import org.apache.poi.xssf.streaming.SXSSFWorkbook; import org.springframework.core.io.FileSystemResource; import org.springframework.core.io.Resource; import org.springframework.stereotype.Service; import java.io.File; -import java.io.FileInputStream; import java.io.FileOutputStream; +import java.io.IOException; import java.nio.file.Files; import java.time.LocalDateTime; import java.time.format.DateTimeFormatter; @@ -217,84 +212,119 @@ public class SplitRunService { String originalFilename, SplitRunRequest request ) throws Exception { - DataFormatter formatter = new DataFormatter(); - try (FileInputStream fis = new FileInputStream(inputFile); - Workbook workbook = WorkbookFactory.create(fis)) { - Sheet sheet = workbook.getSheetAt(0); - Row headerRow = sheet.getRow(0); - if (headerRow == null) { - throw new BusinessException("Excel header row is empty."); - } - - Map headerMap = buildHeaderMap(headerRow, formatter); - List missing = request.getSelectedColumns().stream() - .filter(column -> !headerMap.containsKey(column)) - .toList(); - if (!missing.isEmpty()) { - throw new BusinessException("Missing required columns: " + String.join(", ", missing)); - } - - List> rowsData = new ArrayList<>(); - for (int rowNum = 1; rowNum <= sheet.getLastRowNum(); rowNum++) { - Row row = sheet.getRow(rowNum); - if (row == null) { - continue; - } - List projected = new ArrayList<>(); - for (String column : request.getSelectedColumns()) { - Integer sourceIndex = headerMap.get(column); - String value = sourceIndex == null - ? "" - : normalizeCellText(formatter.formatCellValue(row.getCell(sourceIndex))); - projected.add(value); - } - rowsData.add(projected); - } - - if (rowsData.isEmpty()) { - throw new BusinessException("No data rows available for splitting."); - } - - List>> chunks = buildChunks(rowsData, request); - File outputDir = FileUtil.mkdir(FileUtil.file(storageProperties.getLocalTempDir(), "split-result")); - String sourceStem = FileUtil.mainName(originalFilename); - List results = new ArrayList<>(); - - for (int i = 0; i < chunks.size(); i++) { - File outputFile = buildNamedOutputFile(outputDir, sourceStem + "_split_" + (i + 1) + ".xlsx"); - try (XSSFWorkbook outputWorkbook = new XSSFWorkbook()) { - Sheet outputSheet = outputWorkbook.createSheet(sheet.getSheetName()); - Row outputHeaderRow = outputSheet.createRow(0); - for (int colIndex = 0; colIndex < request.getSelectedColumns().size(); colIndex++) { - outputHeaderRow.createCell(colIndex).setCellValue(request.getSelectedColumns().get(colIndex)); - } - int outputRowIndex = 1; - for (List rowValues : chunks.get(i)) { - Row outputRow = outputSheet.createRow(outputRowIndex++); - for (int colIndex = 0; colIndex < rowValues.size(); colIndex++) { - outputRow.createCell(colIndex).setCellValue(rowValues.get(colIndex)); - } - } - try (FileOutputStream fos = new FileOutputStream(outputFile)) { - outputWorkbook.write(fos); - } - } - results.add(new SplitChunkResult(outputFile, chunks.get(i).size())); - } - - return results; + CountResult countResult = countSplitRows(inputFile, request.getSelectedColumns()); + if (countResult.rowCount() <= 0) { + throw new BusinessException("No data rows available for splitting."); } + + List chunkSizes = buildChunkSizes(countResult.rowCount(), request); + File outputDir = FileUtil.mkdir(FileUtil.file(storageProperties.getLocalTempDir(), "split-result")); + String sourceStem = FileUtil.mainName(originalFilename); + List results = new ArrayList<>(); + List writers = new ArrayList<>(); + try { + for (int i = 0; i < chunkSizes.size(); i++) { + File outputFile = buildNamedOutputFile(outputDir, sourceStem + "_split_" + (i + 1) + ".xlsx"); + ChunkWriter writer = new ChunkWriter(outputFile, countResult.sheetName(), request.getSelectedColumns(), chunkSizes.get(i)); + writers.add(writer); + results.add(new SplitChunkResult(outputFile, chunkSizes.get(i))); + } + + Map[] headerMapHolder = new Map[]{null}; + int[] chunkIndex = new int[]{0}; + ExcelStreamReader.readFirstSheet(inputFile, new ExcelStreamReader.SheetRowHandler() { + @Override + public void onHeader(String sheetName, Integer sheetNo, Map headerMap) { + Map resolvedHeaderMap = buildHeaderMap(headerMap); + List missing = request.getSelectedColumns().stream() + .filter(column -> !resolvedHeaderMap.containsKey(column)) + .toList(); + if (!missing.isEmpty()) { + throw new BusinessException("Missing required columns: " + String.join(", ", missing)); + } + headerMapHolder[0] = resolvedHeaderMap; + } + + @Override + public void onRow(String sheetName, Integer sheetNo, int rowIndex, Map headerMap, Map rowMap) throws Exception { + if (chunkIndex[0] >= writers.size()) { + return; + } + Map resolvedHeaderMap = headerMapHolder[0]; + if (resolvedHeaderMap == null) { + throw new BusinessException("Excel header row is empty."); + } + List projected = new ArrayList<>(); + for (String column : request.getSelectedColumns()) { + Integer sourceIndex = resolvedHeaderMap.get(column); + projected.add(sourceIndex == null ? "" : normalizeCellText(rowMap.get(sourceIndex))); + } + ChunkWriter writer = writers.get(chunkIndex[0]); + writer.writeRow(projected); + if (writer.isFull()) { + chunkIndex[0]++; + } + } + }); + } finally { + IOException closeException = null; + for (ChunkWriter writer : writers) { + try { + writer.close(); + } catch (IOException ex) { + if (closeException == null) { + closeException = ex; + } + } + } + if (closeException != null) { + throw closeException; + } + } + + return results; } - private Map buildHeaderMap(Row headerRow, DataFormatter formatter) { + private CountResult countSplitRows(File inputFile, List selectedColumns) throws IOException { + int[] rowCount = new int[]{0}; + String[] sheetName = new String[]{"Sheet1"}; + boolean[] headerRead = new boolean[]{false}; + ExcelStreamReader.readFirstSheet(inputFile, new ExcelStreamReader.SheetRowHandler() { + @Override + public void onHeader(String currentSheetName, Integer sheetNo, Map headerMap) { + Map resolvedHeaderMap = buildHeaderMap(headerMap); + List missing = selectedColumns.stream() + .filter(column -> !resolvedHeaderMap.containsKey(column)) + .toList(); + if (!missing.isEmpty()) { + throw new BusinessException("Missing required columns: " + String.join(", ", missing)); + } + sheetName[0] = currentSheetName == null || currentSheetName.isBlank() ? "Sheet1" : currentSheetName; + headerRead[0] = true; + } + + @Override + public void onRow(String currentSheetName, Integer sheetNo, int rowIndex, Map headerMap, Map rowMap) { + if (!headerRead[0]) { + throw new BusinessException("Excel header row is empty."); + } + rowCount[0]++; + } + }); + if (!headerRead[0]) { + throw new BusinessException("Excel header row is empty."); + } + return new CountResult(rowCount[0], sheetName[0]); + } + + private Map buildHeaderMap(Map headerRow) { Map headerMap = new HashMap<>(); - for (int i = 0; i < headerRow.getLastCellNum(); i++) { - Cell cell = headerRow.getCell(i); - String value = normalizeHeaderName(cell == null ? null : formatter.formatCellValue(cell)); + for (Map.Entry entry : headerRow.entrySet()) { + String value = normalizeHeaderName(entry.getValue()); if (value.isBlank() || headerMap.containsKey(value)) { continue; } - headerMap.put(value, i); + headerMap.put(value, entry.getKey()); if (HEADER_STOP_COLUMN.equals(value)) { break; } @@ -302,14 +332,13 @@ public class SplitRunService { return headerMap; } - private List>> buildChunks(List> rowsData, SplitRunRequest request) { - int totalRows = rowsData.size(); - List>> chunks = new ArrayList<>(); + private List buildChunkSizes(int totalRows, SplitRunRequest request) { + List chunks = new ArrayList<>(); if (SPLIT_MODE_ROWS_PER_FILE.equals(request.getSplitMode())) { int chunkSize = request.getRowsPerFile(); for (int i = 0; i < totalRows; i += chunkSize) { - chunks.add(rowsData.subList(i, Math.min(i + chunkSize, totalRows))); + chunks.add(Math.min(chunkSize, totalRows - i)); } return chunks; } @@ -317,12 +346,8 @@ public class SplitRunService { int actualParts = Math.min(request.getParts(), totalRows); int base = totalRows / actualParts; int remainder = totalRows % actualParts; - int start = 0; for (int i = 0; i < actualParts; i++) { - int size = base + (i < remainder ? 1 : 0); - int end = start + size; - chunks.add(rowsData.subList(start, end)); - start = end; + chunks.add(base + (i < remainder ? 1 : 0)); } return chunks; } @@ -448,4 +473,48 @@ public class SplitRunService { private record TaskSplitChunkResult(String relativePath, String sourceFilename, File outputFile, int rowCount) { } + + private record CountResult(int rowCount, String sheetName) { + } + + private static final class ChunkWriter implements AutoCloseable { + private final File outputFile; + private final SXSSFWorkbook workbook; + private final org.apache.poi.ss.usermodel.Sheet sheet; + private final int rowLimit; + private int writtenRows; + + private ChunkWriter(File outputFile, String sheetName, List columns, int rowLimit) { + this.outputFile = outputFile; + this.rowLimit = rowLimit; + this.workbook = new SXSSFWorkbook(200); + this.sheet = workbook.createSheet(sheetName == null || sheetName.isBlank() ? "Sheet1" : sheetName); + org.apache.poi.ss.usermodel.Row headerRow = sheet.createRow(0); + for (int i = 0; i < columns.size(); i++) { + headerRow.createCell(i).setCellValue(columns.get(i)); + } + } + + private void writeRow(List values) { + org.apache.poi.ss.usermodel.Row outputRow = sheet.createRow(writtenRows + 1); + for (int colIndex = 0; colIndex < values.size(); colIndex++) { + outputRow.createCell(colIndex).setCellValue(values.get(colIndex)); + } + writtenRows++; + } + + private boolean isFull() { + return writtenRows >= rowLimit; + } + + @Override + public void close() throws IOException { + try (FileOutputStream fos = new FileOutputStream(outputFile)) { + workbook.write(fos); + } finally { + workbook.dispose(); + workbook.close(); + } + } + } } diff --git a/backend-java/src/main/resources/application.yml b/backend-java/src/main/resources/application.yml index 066b61c..26247eb 100644 --- a/backend-java/src/main/resources/application.yml +++ b/backend-java/src/main/resources/application.yml @@ -82,12 +82,12 @@ aiimage: heartbeat-timeout-minutes: ${AIIMAGE_DELETE_BRAND_HEARTBEAT_TIMEOUT_MINUTES:15} stale-check-cron: ${AIIMAGE_DELETE_BRAND_STALE_CHECK_CRON:*/30 * * * * *} 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} + product-risk-stale-timeout-minutes: ${AIIMAGE_PRODUCT_RISK_STALE_TIMEOUT_MINUTES:20} + product-risk-initial-timeout-minutes: ${AIIMAGE_PRODUCT_RISK_INITIAL_TIMEOUT_MINUTES:20} + price-track-stale-timeout-minutes: ${AIIMAGE_PRICE_TRACK_STALE_TIMEOUT_MINUTES:20} + price-track-initial-timeout-minutes: ${AIIMAGE_PRICE_TRACK_INITIAL_TIMEOUT_MINUTES:20} + shop-match-stale-timeout-minutes: ${AIIMAGE_SHOP_MATCH_STALE_TIMEOUT_MINUTES:20} + shop-match-initial-timeout-minutes: ${AIIMAGE_SHOP_MATCH_INITIAL_TIMEOUT_MINUTES:20} module-cleanup: enabled: ${AIIMAGE_MODULE_CLEANUP_ENABLED:true} cron: ${AIIMAGE_MODULE_CLEANUP_CRON:0 0 0 * * *} diff --git a/backend-java/src/main/resources/db/V24__price_track_tables.sql b/backend-java/src/main/resources/db/V24__price_track_tables.sql new file mode 100644 index 0000000..f10c436 --- /dev/null +++ b/backend-java/src/main/resources/db/V24__price_track_tables.sql @@ -0,0 +1,14 @@ +CREATE TABLE IF NOT EXISTS biz_price_track_shop_candidate ( + id BIGINT PRIMARY KEY AUTO_INCREMENT COMMENT 'primary key', + user_id BIGINT NOT NULL COMMENT 'user id', + shop_name VARCHAR(255) NOT NULL COMMENT 'normalized shop name', + created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT 'created time', + UNIQUE KEY uk_user_shop (user_id, shop_name), + KEY idx_user_id (user_id) +) COMMENT='price track candidate shops'; + +CREATE TABLE IF NOT EXISTS biz_price_track_country_pref ( + user_id BIGINT NOT NULL PRIMARY KEY COMMENT 'user id', + country_codes_json VARCHAR(256) NOT NULL COMMENT 'country code order json', + updated_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT 'updated time' +) COMMENT='price track country preference'; diff --git a/backend-java/src/main/resources/db/V25__permission_menu_sort_order.sql b/backend-java/src/main/resources/db/V25__permission_menu_sort_order.sql new file mode 100644 index 0000000..74322ce --- /dev/null +++ b/backend-java/src/main/resources/db/V25__permission_menu_sort_order.sql @@ -0,0 +1,6 @@ +ALTER TABLE `columns` + ADD COLUMN `sort_order` INT NOT NULL DEFAULT 0 COMMENT '菜单排序' AFTER `route_path`; + +UPDATE `columns` +SET `sort_order` = `id` +WHERE `sort_order` = 0; diff --git a/backend-java/src/main/resources/db/V25__shop_manage_group_bind_user.sql b/backend-java/src/main/resources/db/V25__shop_manage_group_bind_user.sql new file mode 100644 index 0000000..af919d6 --- /dev/null +++ b/backend-java/src/main/resources/db/V25__shop_manage_group_bind_user.sql @@ -0,0 +1,55 @@ +SET @db_name = DATABASE(); + +SET @user_id_col_exists := ( + SELECT COUNT(*) + FROM information_schema.COLUMNS + WHERE TABLE_SCHEMA = @db_name + AND TABLE_NAME = 'biz_shop_manage_group' + AND COLUMN_NAME = 'user_id' +); + +SET @sql_add_user_id_col := IF(@user_id_col_exists = 0, + 'ALTER TABLE biz_shop_manage_group ADD COLUMN user_id BIGINT NULL COMMENT ''关联用户ID'' AFTER group_name', + 'SELECT 1' +); +PREPARE stmt_add_user_id_col FROM @sql_add_user_id_col; +EXECUTE stmt_add_user_id_col; +DEALLOCATE PREPARE stmt_add_user_id_col; + +UPDATE biz_shop_manage_group g +LEFT JOIN users u ON u.username = g.group_name +SET g.user_id = u.id +WHERE g.user_id IS NULL + AND u.id IS NOT NULL; + +SET @user_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_user_id' +); + +SET @sql_add_user_id_idx := IF(@user_id_idx_exists = 0, + 'ALTER TABLE biz_shop_manage_group ADD INDEX idx_user_id (user_id)', + 'SELECT 1' +); +PREPARE stmt_add_user_id_idx FROM @sql_add_user_id_idx; +EXECUTE stmt_add_user_id_idx; +DEALLOCATE PREPARE stmt_add_user_id_idx; + +SET @user_id_uk_exists := ( + SELECT COUNT(*) + FROM information_schema.STATISTICS + WHERE TABLE_SCHEMA = @db_name + AND TABLE_NAME = 'biz_shop_manage_group' + AND INDEX_NAME = 'uk_user_id' +); + +SET @sql_add_user_id_uk := IF(@user_id_uk_exists = 0, + 'ALTER TABLE biz_shop_manage_group ADD UNIQUE KEY uk_user_id (user_id)', + 'SELECT 1' +); +PREPARE stmt_add_user_id_uk FROM @sql_add_user_id_uk; +EXECUTE stmt_add_user_id_uk; +DEALLOCATE PREPARE stmt_add_user_id_uk; diff --git a/backend/__pycache__/config.cpython-312.pyc b/backend/__pycache__/config.cpython-312.pyc index 57fc226f5d7255b16ddbf4efe022298ae6eb1d8c..8a02d24c8df3cd8ab635c7c3caa642aafd4bd0b3 100644 GIT binary patch delta 20 acmaFH{fwLYG%qg~0}!;&d$^JNHY)%^ng)LW delta 20 acmaFH{fwLYG%qg~0}#BBy1SA4HY)%^6$V%U diff --git a/backend/blueprints/__pycache__/admin_api.cpython-312.pyc b/backend/blueprints/__pycache__/admin_api.cpython-312.pyc index 268c8ae2e9b88c92e6366e1ba14175d0e1c509ac..4b38d6bfa5382dc85954c23c0ea5130358131aae 100644 GIT binary patch literal 84968 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

+N3w{}WXZ3^K+3 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 diff --git a/backend/blueprints/admin_api.py b/backend/blueprints/admin_api.py index c225db2..c5a0e46 100644 --- a/backend/blueprints/admin_api.py +++ b/backend/blueprints/admin_api.py @@ -62,6 +62,34 @@ ADMIN_MENU_ACCESS_CONFIG = { }, } +ADMIN_MENU_ACCESS_CONFIG.update({ + 'users': { + 'column_key': 'admin_users', + 'route_path': 'users', + 'error': '无权访问用户管理模块', + }, + 'columns': { + 'column_key': 'admin_columns', + 'route_path': 'columns', + 'error': '无权访问栏目权限配置模块', + }, + 'shop-keys': { + 'column_key': 'admin_shop_keys', + 'route_path': 'shop-keys', + 'error': '无权访问店铺密钥管理模块', + }, + 'history': { + 'column_key': 'admin_history', + 'route_path': 'history', + 'error': '无权访问查看生成记录模块', + }, + 'version': { + 'column_key': 'admin_version', + 'route_path': 'version', + 'error': '无权访问版本管理模块', + }, +}) + def _safe_version_key(version): """将版本号转换为安全的 OSS 对象名片段""" @@ -98,10 +126,155 @@ def _format_permission_item(item): 'column_key': item.get('columnKey') or '', 'menu_type': item.get('menuType') or 'app', 'route_path': item.get('routePath') or '', + 'sort_order': item.get('sortOrder') if item.get('sortOrder') is not None else 0, 'created_at': (item.get('createdAt') or '').replace('T', ' ')[:16], } +def _parse_optional_int(value): + if value is None: + return None + text = str(value).strip() + if text == '': + return None + return int(text) + + +def _ensure_column_sort_schema(): + conn = get_db() + try: + with conn.cursor() as cur: + try: + cur.execute("ALTER TABLE columns ADD COLUMN sort_order INT NOT NULL DEFAULT 0 COMMENT '菜单排序' AFTER route_path") + except Exception: + pass + try: + cur.execute("UPDATE columns SET sort_order = id WHERE sort_order IS NULL OR sort_order = 0") + except Exception: + pass + conn.commit() + finally: + conn.close() + + +def _load_local_column_sort_map(): + _ensure_column_sort_schema() + conn = get_db() + try: + with conn.cursor() as cur: + cur.execute("SELECT id, sort_order FROM columns") + rows = cur.fetchall() + return {int(row['id']): int(row.get('sort_order') or 0) for row in rows if row.get('id') is not None} + finally: + conn.close() + + +def _load_local_columns(menu_type=None): + _ensure_column_sort_schema() + conn = get_db() + try: + with conn.cursor() as cur: + sql = """ + SELECT id, name, column_key, menu_type, route_path, sort_order, created_at + FROM columns + """ + params = [] + if menu_type: + sql += " WHERE menu_type = %s" + params.append(menu_type) + sql += " ORDER BY sort_order ASC, id ASC" + cur.execute(sql, params) + return cur.fetchall() + finally: + conn.close() + + +def _sync_local_columns_from_backend(items): + if not items: + return + _ensure_column_sort_schema() + conn = get_db() + try: + with conn.cursor() as cur: + for item in items: + item_id = item.get('id') + if item_id is None: + continue + sort_order = item.get('sort_order') + cur.execute( + """ + INSERT INTO columns (id, name, column_key, menu_type, route_path, sort_order, created_at) + VALUES (%s, %s, %s, %s, %s, %s, COALESCE(%s, NOW())) + ON DUPLICATE KEY UPDATE + name = VALUES(name), + column_key = VALUES(column_key), + menu_type = VALUES(menu_type), + route_path = VALUES(route_path), + sort_order = CASE + WHEN columns.sort_order IS NULL OR columns.sort_order = 0 THEN VALUES(sort_order) + ELSE columns.sort_order + END + """, + ( + int(item_id), + item.get('name') or '', + item.get('column_key') or '', + item.get('menu_type') or 'app', + item.get('route_path') or '', + int(sort_order if sort_order not in (None, '') else item_id), + item.get('created_at') or None, + ), + ) + conn.commit() + finally: + conn.close() + + +def _set_local_column_sort_order(column_id, sort_order): + _ensure_column_sort_schema() + conn = get_db() + try: + with conn.cursor() as cur: + cur.execute( + "UPDATE columns SET sort_order = %s WHERE id = %s", + (int(sort_order), int(column_id)), + ) + conn.commit() + finally: + conn.close() + + +def _get_next_local_column_sort_order(): + _ensure_column_sort_schema() + conn = get_db() + try: + with conn.cursor() as cur: + cur.execute("SELECT COALESCE(MAX(sort_order), 0) AS max_sort FROM columns") + row = cur.fetchone() or {} + return int(row.get('max_sort') or 0) + 1 + finally: + conn.close() + + +def _swap_local_column_sort_order(column_id, target_id): + _ensure_column_sort_schema() + conn = get_db() + try: + with conn.cursor() as cur: + cur.execute("SELECT id, sort_order FROM columns WHERE id IN (%s, %s) ORDER BY id", (int(column_id), int(target_id))) + rows = cur.fetchall() + if len(rows) != 2: + raise ValueError('菜单不存在') + by_id = {int(row['id']): int(row.get('sort_order') or 0) for row in rows} + left_sort = by_id.get(int(column_id), 0) or int(column_id) + right_sort = by_id.get(int(target_id), 0) or int(target_id) + cur.execute("UPDATE columns SET sort_order = %s WHERE id = %s", (right_sort, int(column_id))) + cur.execute("UPDATE columns SET sort_order = %s WHERE id = %s", (left_sort, int(target_id))) + conn.commit() + finally: + conn.close() + + def _sync_user_column_permissions(user_id, column_ids): _, error_response, status = _proxy_backend_java( 'PUT', @@ -111,8 +284,14 @@ def _sync_user_column_permissions(user_id, column_ids): return error_response, status +def _get_current_admin_id(current_row=None): + if current_row and current_row.get('id'): + return current_row.get('id') + return session.get('user_id') + + def _get_current_admin_permission_sets(): - user_id = session.get('user_id') + user_id = _get_current_admin_id() if not user_id: return set(), set() conn = get_db() @@ -152,6 +331,60 @@ def _ensure_admin_menu_access(*menu_names): return role, current_row, (jsonify({'success': False, 'error': error_message}), 403) +def _ensure_dedupe_total_data_access(): + return _ensure_admin_menu_access('dedupe-total-data') + + +def _load_current_admin_menu_items(): + role, current_row = get_current_admin_role() + if not role or not current_row: + return None, None, None, (jsonify({'success': False, 'error': '需要管理员权限'}), 403) + user_id = _get_current_admin_id(current_row) + if not user_id: + return role, current_row, None, (jsonify({'success': False, 'error': '当前登录用户缺少有效ID'}), 400) + result, error_response, status = _proxy_backend_java( + 'GET', + f'/api/admin/permission-users/{user_id}/column-permissions', + params={'menuType': 'admin'}, + ) + if error_response is not None: + return role, current_row, None, (error_response, status) + items = [_format_permission_item(item) for item in (result.get('data') or [])] + return role, current_row, items, None + + +@admin_api.route('/current-user') +@admin_required +def get_admin_current_user(): + role, current_row = get_current_admin_role() + if not role or not current_row: + return jsonify({'success': False, 'error': '需要管理员权限'}), 403 + return jsonify({ + 'success': True, + 'item': { + 'id': current_row.get('id'), + 'username': current_row.get('username') or session.get('username') or '', + 'role': role, + } + }) + + +@admin_api.route('/current-user/menus') +@admin_required +def get_admin_current_user_menus(): + _, _, items, denied = _load_current_admin_menu_items() + if denied: + return denied + return jsonify({'success': True, 'items': items}) + + +@admin_api.route('/logout', methods=['POST']) +@admin_required +def admin_logout(): + session.clear() + return jsonify({'success': True, 'msg': '退出成功', 'redirect': '/login'}) + + # ---------- 用户管理 ---------- @admin_api.route('/users') @@ -162,7 +395,13 @@ def list_users(): if not role: return jsonify({'success': False, 'error': '需要管理员权限'}), 403 page = max(1, int(request.args.get('page', 1))) - page_size = min(50, max(5, int(request.args.get('page_size', 15)))) + _, _, denied = _ensure_admin_menu_access('users', 'history', 'shop-manage', 'skip-price-asin') + if denied: + return denied + _, _, denied = _ensure_admin_menu_access('users', 'history', 'shop-manage', 'skip-price-asin') + if denied: + return denied + page_size = min(999, max(5, int(request.args.get('page_size', 15)))) offset = (page - 1) * page_size search_username = (request.args.get('username') or request.args.get('search') or '').strip() created_by_id_arg = request.args.get('created_by_id') or request.args.get('admin_id') @@ -249,6 +488,9 @@ def list_users(): @admin_api.route('/user', methods=['POST']) @admin_required def create_user(): + _, _, denied = _ensure_admin_menu_access('users') + if denied: + return denied data = request.get_json() or {} username = (data.get('username') or '').strip() password = data.get('password') or '' @@ -312,6 +554,9 @@ def create_user(): def update_user(uid): """更新用户(密码、角色)""" data = request.get_json() or {} + _, _, denied = _ensure_admin_menu_access('users') + if denied: + return denied password = data.get('password') want_role = (data.get('role') or '').strip() or data.get('role') role, current_row = get_current_admin_role() @@ -368,6 +613,9 @@ def update_user(uid): @admin_required def delete_user(uid): """删除用户""" + _, _, denied = _ensure_admin_menu_access('users') + if denied: + return denied if session.get('user_id') == uid: return jsonify({'success': False, 'error': '不能删除当前登录账号'}) role, current_row = get_current_admin_role() @@ -405,6 +653,9 @@ def delete_user(uid): @admin_required def history(): """管理员分页获取所有生成记录""" + _, _, denied = _ensure_admin_menu_access('history') + if denied: + return denied page = max(1, int(request.args.get('page', 1))) page_size = min(50, max(10, int(request.args.get('page_size', 15)))) offset = (page - 1) * page_size @@ -473,28 +724,49 @@ def history(): @admin_api.route('/columns') @admin_required def list_columns(): - params = {} + _, _, denied = _ensure_admin_menu_access('columns', 'users') + if denied: + return denied menu_type = (request.args.get('menu_type') or '').strip() - if menu_type: - params['menuType'] = menu_type + params = {'menuType': menu_type} if menu_type else None result, error_response, status = _proxy_backend_java( 'GET', '/api/admin/permission-menus', - params=params or None, + params=params, ) - if error_response is not None: - return error_response, status - return jsonify({'success': True, 'items': [_format_permission_item(item) for item in (result.get('data') or [])]}) + if error_response is None: + _sync_local_columns_from_backend([_format_permission_item(item) for item in (result.get('data') or [])]) + local_rows = _load_local_columns(menu_type or None) + items = [ + { + 'id': row.get('id'), + 'name': row.get('name') or '', + 'column_key': row.get('column_key') or '', + 'menu_type': row.get('menu_type') or 'app', + 'route_path': row.get('route_path') or '', + 'sort_order': int(row.get('sort_order') or 0), + 'created_at': str(row.get('created_at') or '')[:16].replace('T', ' '), + } + for row in local_rows + ] + return jsonify({'success': True, 'items': items}) @admin_api.route('/column', methods=['POST']) @admin_required def create_column(): + _, _, denied = _ensure_admin_menu_access('columns') + if denied: + return denied data = request.get_json() or {} name = (data.get('name') or '').strip() column_key = (data.get('column_key') or '').strip() - menu_type = (data.get('menu_type') or 'app').strip() or 'app' + menu_type = (data.get('menu_type') or '').strip() or 'admin' route_path = (data.get('route_path') or '').strip() + try: + sort_order = _parse_optional_int(data.get('sort_order')) + except (TypeError, ValueError): + return jsonify({'success': False, 'error': '排序必须是数字'}) if not name: return jsonify({'success': False, 'error': '栏目名不能为空'}) if not column_key: @@ -514,17 +786,27 @@ def create_column(): if error_response is not None: return error_response, status item = result.get('data') or {} + _sync_local_columns_from_backend([_format_permission_item(item)]) + if item.get('id') is not None: + _set_local_column_sort_order(item.get('id'), sort_order if sort_order is not None else _get_next_local_column_sort_order()) return jsonify({'success': True, 'msg': result.get('message') or '创建成功', 'id': item.get('id')}) @admin_api.route('/column/', methods=['PUT']) @admin_required def update_column(cid): + _, _, denied = _ensure_admin_menu_access('columns') + if denied: + return denied data = request.get_json() or {} name = (data.get('name') or '').strip() column_key = (data.get('column_key') or '').strip() - menu_type = (data.get('menu_type') or 'app').strip() or 'app' + menu_type = (data.get('menu_type') or '').strip() or 'admin' route_path = (data.get('route_path') or '').strip() + try: + sort_order = _parse_optional_int(data.get('sort_order')) + except (TypeError, ValueError): + return jsonify({'success': False, 'error': '排序必须是数字'}) if not name: return jsonify({'success': False, 'error': '栏目名不能为空'}) if not column_key: @@ -543,32 +825,76 @@ def update_column(cid): ) if error_response is not None: return error_response, status + _sync_local_columns_from_backend([{ + 'id': cid, + 'name': name, + 'column_key': column_key, + 'menu_type': menu_type, + 'route_path': route_path, + 'sort_order': sort_order if sort_order is not None else cid, + 'created_at': None, + }]) + if sort_order is not None: + _set_local_column_sort_order(cid, sort_order) return jsonify({'success': True, 'msg': result.get('message') or '更新成功'}) +@admin_api.route('/column/reorder', methods=['POST']) +@admin_required +def reorder_column(): + _, _, denied = _ensure_admin_menu_access('columns') + if denied: + return denied + data = request.get_json() or {} + try: + column_id = int(data.get('column_id')) + target_id = int(data.get('target_id')) + except (TypeError, ValueError): + return jsonify({'success': False, 'error': '菜单重排参数无效'}), 400 + if column_id == target_id: + return jsonify({'success': True, 'msg': '排序未变化'}) + try: + _swap_local_column_sort_order(column_id, target_id) + except ValueError as exc: + return jsonify({'success': False, 'error': str(exc)}), 400 + except Exception as exc: + return jsonify({'success': False, 'error': str(exc)}), 500 + return jsonify({'success': True, 'msg': '排序更新成功'}) + + @admin_api.route('/column/', methods=['DELETE']) @admin_required def delete_column(cid): + _, _, denied = _ensure_admin_menu_access('columns') + if denied: + return denied result, error_response, status = _proxy_backend_java( 'DELETE', f'/api/admin/permission-menus/{cid}', ) if error_response is not None: return error_response, status + conn = get_db() + try: + with conn.cursor() as cur: + cur.execute("DELETE FROM columns WHERE id = %s", (cid,)) + conn.commit() + finally: + conn.close() return jsonify({'success': True, 'msg': result.get('message') or '删除成功'}) @admin_api.route('/user//columns') @admin_required def get_user_columns(uid): - params = {} + _, _, denied = _ensure_admin_menu_access('users') + if denied: + return denied menu_type = (request.args.get('menu_type') or '').strip() - if menu_type: - params['menuType'] = menu_type result, error_response, status = _proxy_backend_java( 'GET', f'/api/admin/permission-users/{uid}/columns', - params=params or None, + params={'menuType': menu_type} if menu_type else None, ) if error_response is not None: return error_response, status @@ -579,14 +905,14 @@ def get_user_columns(uid): @admin_api.route('/user//column-permissions') @admin_required def get_user_column_permissions(uid): - params = {} + _, _, denied = _ensure_admin_menu_access('users') + if denied: + return denied menu_type = (request.args.get('menu_type') or '').strip() - if menu_type: - params['menuType'] = menu_type result, error_response, status = _proxy_backend_java( 'GET', f'/api/admin/permission-users/{uid}/column-permissions', - params=params or None, + params={'menuType': menu_type} if menu_type else None, ) if error_response is not None: return error_response, status @@ -610,6 +936,9 @@ def _set_user_column_permissions(cur, user_id, column_ids): @admin_api.route('/versions') @admin_required def list_versions(): + _, _, denied = _ensure_admin_menu_access('version') + if denied: + return denied """获取版本列表""" try: conn = get_db() @@ -637,6 +966,9 @@ def list_versions(): @admin_required def upload_version(): """接收版本号 + zip 文件,上传到 OSS,写入 web_config""" + _, _, denied = _ensure_admin_menu_access('version') + if denied: + return denied version = (request.form.get('version') or '').strip() if not version: return jsonify({'success': False, 'error': '请填写版本号'}) @@ -675,6 +1007,9 @@ def upload_version(): @admin_api.route('/shop-keys') @admin_required def list_shop_keys(): + _, _, denied = _ensure_admin_menu_access('shop-keys') + if denied: + return denied page = max(1, int(request.args.get('page', 1))) page_size = min(100, max(1, int(request.args.get('page_size', 15)))) result, error_response, status = _proxy_backend_java( @@ -707,6 +1042,9 @@ def list_shop_keys(): @admin_api.route('/shop-key', methods=['POST']) @admin_required def create_shop_key(): + _, _, denied = _ensure_admin_menu_access('shop-keys') + if denied: + return denied data = request.get_json() or {} payload = { 'ziniaoAccountName': (data.get('ziniao_account_name') or '').strip(), @@ -736,6 +1074,9 @@ def create_shop_key(): @admin_api.route('/shop-key/', methods=['PUT']) @admin_required def update_shop_key(item_id): + _, _, denied = _ensure_admin_menu_access('shop-keys') + if denied: + return denied data = request.get_json() or {} payload = { 'ziniaoAccountName': (data.get('ziniao_account_name') or '').strip(), @@ -765,6 +1106,9 @@ def update_shop_key(item_id): @admin_api.route('/shop-key/', methods=['DELETE']) @admin_required def delete_shop_key(item_id): + _, _, denied = _ensure_admin_menu_access('shop-keys') + if denied: + return denied result, error_response, status = _proxy_backend_java( 'DELETE', f'/api/admin/shop-keys/{item_id}', @@ -1007,8 +1351,9 @@ 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 current_row and current_row.get('id'): + params['operatorId'] = current_row.get('id') + params['superAdmin'] = 'true' if role == 'super_admin' else 'false' if group_id_raw: try: group_id = int(group_id_raw) @@ -1164,10 +1509,16 @@ def list_shop_manage_groups(): role, current_row, denied = _ensure_admin_menu_access('shop-manage', 'skip-price-asin') if denied: return denied + params = {} + if current_row and current_row.get('id'): + params['operatorId'] = current_row.get('id') + params['superAdmin'] = 'true' if role == 'super_admin' else 'false' + if role != 'super_admin' and current_row and current_row.get('id'): + params['createdById'] = current_row.get('id') 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, + params=params, ) if error_response is not None: return error_response, status @@ -1175,6 +1526,8 @@ def list_shop_manage_groups(): { 'id': item.get('id'), 'group_name': item.get('groupName') or '', + 'user_id': item.get('userId'), + 'username': item.get('username') or '', 'created_at': (item.get('createdAt') or '').replace('T', ' ')[:16], 'updated_at': (item.get('updatedAt') or '').replace('T', ' ')[:16], } @@ -1186,17 +1539,22 @@ def list_shop_manage_groups(): @admin_api.route('/shop-manage-group', methods=['POST']) @admin_required def create_shop_manage_group(): - _, current_row, 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 {} payload = { 'groupName': (data.get('group_name') or '').strip(), + 'userId': data.get('user_id'), 'createdById': current_row.get('id') if current_row else None, } result, error_response, status = _proxy_backend_java( 'POST', '/api/admin/shop-manages/groups', + 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: @@ -1208,6 +1566,8 @@ def create_shop_manage_group(): 'item': { 'id': item.get('id'), 'group_name': item.get('groupName') or '', + 'user_id': item.get('userId'), + 'username': item.get('username') or '', 'created_at': (item.get('createdAt') or '').replace('T', ' ')[:16], 'updated_at': (item.get('updatedAt') or '').replace('T', ' ')[:16], }, @@ -1221,7 +1581,10 @@ def update_shop_manage_group(item_id): 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(), + 'userId': data.get('user_id'), + } result, error_response, status = _proxy_backend_java( 'PUT', f'/api/admin/shop-manages/groups/{item_id}', @@ -1240,6 +1603,8 @@ def update_shop_manage_group(item_id): 'item': { 'id': item.get('id'), 'group_name': item.get('groupName') or '', + 'user_id': item.get('userId'), + 'username': item.get('username') or '', 'created_at': (item.get('createdAt') or '').replace('T', ' ')[:16], 'updated_at': (item.get('updatedAt') or '').replace('T', ' ')[:16], }, @@ -1249,7 +1614,7 @@ def update_shop_manage_group(item_id): @admin_api.route('/skip-price-asins') @admin_required def list_skip_price_asins(): - _, _, denied = _ensure_admin_menu_access('skip-price-asin') + role, current_row, denied = _ensure_admin_menu_access('skip-price-asin') if denied: return denied page = max(1, int(request.args.get('page', 1))) @@ -1258,6 +1623,9 @@ def list_skip_price_asins(): shop_name = (request.args.get('shop_name') or '').strip() asin = (request.args.get('asin') or '').strip() params = {'page': page, 'pageSize': page_size} + if current_row and current_row.get('id'): + params['operatorId'] = current_row.get('id') + params['superAdmin'] = 'true' if role == 'super_admin' else 'false' if group_id_raw: try: group_id = int(group_id_raw) @@ -1305,19 +1673,31 @@ def list_skip_price_asins(): @admin_api.route('/skip-price-asin', methods=['POST']) @admin_required def create_skip_price_asin(): - _, _, denied = _ensure_admin_menu_access('skip-price-asin') + role, current_row, denied = _ensure_admin_menu_access('skip-price-asin') if denied: return denied data = request.get_json() or {} + asin_mappings = data.get('asin_mappings') or {} + fallback_asin = (data.get('asin') or '').strip() + if not fallback_asin and isinstance(asin_mappings, dict): + for value in asin_mappings.values(): + fallback_asin = (value or '').strip() + if fallback_asin: + break payload = { 'groupId': data.get('group_id'), 'shopName': (data.get('shop_name') or '').strip(), 'countries': data.get('countries') or [], - 'asin': (data.get('asin') or '').strip(), + 'asin': fallback_asin, + 'asinMappings': asin_mappings, } result, error_response, status = _proxy_backend_java( 'POST', '/api/admin/skip-price-asins', + 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: @@ -1345,12 +1725,16 @@ def create_skip_price_asin(): @admin_api.route('/skip-price-asin//country/', methods=['DELETE']) @admin_required def delete_skip_price_asin_country(item_id, country): - _, _, denied = _ensure_admin_menu_access('skip-price-asin') + role, current_row, denied = _ensure_admin_menu_access('skip-price-asin') if denied: return denied result, error_response, status = _proxy_backend_java( 'DELETE', f'/api/admin/skip-price-asins/{item_id}/countries/{country}', + 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 @@ -1379,7 +1763,7 @@ def delete_shop_manage_group(item_id): @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') + role, current_row, denied = _ensure_admin_menu_access('skip-price-asin') if denied: return denied data = request.get_json() or {} @@ -1387,6 +1771,10 @@ def update_skip_price_asin_country(item_id, country): result, error_response, status = _proxy_backend_java( 'PUT', f'/api/admin/skip-price-asins/{item_id}/countries/{country}', + 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: diff --git a/backend/utils/__pycache__/db.cpython-312.pyc b/backend/utils/__pycache__/db.cpython-312.pyc index 74e09849dc3c7efc744df9d3918552722660420a..7b0e8ace4856a93b43747b263bab1ed37729d453 100644 GIT binary patch delta 919 zcmYk3O-vI(6oBWaYr8EiUCYu!TUekC^+14F0tXN(6nB@OBD5+-ZIEI>DwwteH6TWf z!30E^cp=7cQ^N()gNgBA_=(*mnixHKF`8gJnI1gp#oa*@W-@QSdGqqV-OP)&FYUHn ztJMs&F~9zG>`v*1Z5S6iE)ETkk4z+Lr!r&7bZvG8CmFQPkPE0SE3&1CTsD`G>z0%H z4tes_QUQy}8%vmXu0g#O0I6d$DBXcjMzUS4!#0R3<)|N{plg zsW&S0Mzj^{?iM=2ouOEFRHzf$BRxG~?W+36(v!W#$KM|;@4Z^87DAoTaKDh6oXU*E zuMTBKNuRSXa-%2K*Aa?_h2hEM)Rl>}Fc6Lk2NW&BSVA~0hWo<@v(mu6Gtz%B+alCu zSDgzOej_#R3OJIDxXsX(2f%v*{Q`g!QT`CwaF-dzKiQKc?jfc*1Z?hn2PVHgr6`6- zv)500y$)@4|Jxm8#p{?0VXbcd{GYst0c%|{-#0H7{7#?KUi6v03xk&-UT(yjR!(%G z&2ml*pv`(tY(iU@lT2uY+!Qrl!H*`2FgvW#4rlfSusGJ8OkPB z;u(dbTm*IIDZ5z7&nUV8D?X?*?XLt_sew^weQnmX<}uDQK>*RK%J>THL)_! zs8pa9L7k~Ub+NLaQK>-nL7k~U4X|>MQR|T=R*yoRgZf9PjZ7CXs?A32?|1RxE*>AT Te>TSB3G%b-HJl)8(8$VEYJ&2y9G&M<+DACx)BqbHp zdQ?y%4u}U&Uc`$adk{QH637m{c<|z>e}T|ru@`3tgv<-W?|C2Qd6?bHpD(C8isC{1 z^!9((-J!2)MLLnXdbZGPZQQR_I)zHB+-=lasb*vIUVXi=R<1m#tyc@1oqD5Pu%1f| z8ID_jCRZ^1?g`Mc_ZHpvF47$@@wJwo;OcaUM?aNli3H#XE&1jk;M}z6JLIm0oS{E_ zMF`Qj5+G|`v?VR0E(lc`S8#{ESMnq`z9Xl{6=Z2njfTJc-yK64)P^nww2XkyD~mSO z49r*`)d-;{gEw*c9o?Opw+@0U61=6iBMDfw9!EUj{uycf@;{_jlumA7q=xr8nBIuZ z;47HEiOtbotVs3QsqIIO_hR*6Zwn)$^gJ&;uYG^paX{#k-^`E=gt*k08i{Mp' + (item.name || item.route_path) + ''; + }).join(''); + } + document.querySelectorAll('#adminTabs .tab').forEach(function (tab) { + tab.onclick = function () { + activateAdminTab(tab.dataset.tab); + }; + }); + var fallbackTab = activeAdminTabName && knownItems.some(function (item) { return item.route_path === activeAdminTabName; }) + ? activeAdminTabName + : knownItems[0].route_path; + activateAdminTab(fallbackTab); + } + function loadAdminMenus(preferredTab) { + if (preferredTab) activeAdminTabName = preferredTab; + fetch('/api/admin/current-user/menus') + .then(function (r) { return r.json(); }) + .then(function (res) { + if (!res.success) { + if (adminTabsEl) adminTabsEl.innerHTML = '

'; + hideAllAdminPanels(); + return; + } + renderAdminTabs(res.items || []); + }) + .catch(function () { + if (adminTabsEl) adminTabsEl.innerHTML = '
菜单加载失败
'; + hideAllAdminPanels(); + }); + } + hideAllAdminPanels(); + + // ========== 用户管理 ========== + var userPage = 1, userPageSize = 15; + var currentUserId = null; + var currentUserRole = 'admin'; + var currentUserUsername = ''; + var adminsList = []; + function roleLabel(role) { + if (role === 'super_admin') return '超级管理员'; + if (role === 'admin') return '管理员'; + return '普通账号'; + } + function buildUserListQuery(page) { + var q = 'page=' + (page || 1) + '&page_size=' + userPageSize; + var kw = (document.getElementById('searchUsername').value || '').trim(); + if (kw) q += '&username=' + encodeURIComponent(kw); + var cby = document.getElementById('filterCreatedBy').value; + if (cby) q += '&created_by_id=' + encodeURIComponent(cby); + return q; + } + function loadUsers(page) { + userPage = page || 1; + fetch('/api/admin/users?' + buildUserListQuery(userPage)) + .then(function (r) { return r.json(); }) + .then(function (res) { + var tbody = document.getElementById('userListBody'); + if (!res.success) { + tbody.innerHTML = '加载失败: ' + (res.error || '') + ''; + return; + } + currentUserId = res.current_user_id || null; + currentUserRole = res.current_user_role || 'admin'; + currentUserUsername = (res.current_user_username || '').trim(); + adminsList = res.admins || []; + var items = res.items || []; + if (items.length === 0) { + tbody.innerHTML = '暂无用户'; + } else { + tbody.innerHTML = items.map(function (u) { + return '' + u.id + '' + (u.username || '') + '' + + roleLabel(u.role || 'normal') + '' + (u.creator_username || '-') + '' + (u.created_at || '') + '' + + ' ' + + '' + + ''; + }).join(''); + } + renderPagination('userPagination', res.total, res.page, res.page_size, loadUsers); + bindUserActions(); + updateCreateFormByRole(); + updateUserFilterByRole(); + }) + .catch(function () { + document.getElementById('userListBody').innerHTML = '请求失败'; + }); + } + function updateUserFilterByRole() { + var grp = document.getElementById('filterCreatedByGroup'); + var sel = document.getElementById('filterCreatedBy'); + if (currentUserRole === 'super_admin') { + grp.style.display = 'block'; + var cur = sel.value; + sel.innerHTML = ''; + adminsList.forEach(function (a) { + var opt = document.createElement('option'); + opt.value = a.id; + opt.textContent = a.username; + sel.appendChild(opt); + }); + sel.value = cur || ''; + } else { + grp.style.display = 'none'; + } + } + + function updateDedupeTotalDataAccess() { + var tab = document.querySelector('.tab[data-tab="dedupe-total-data"]'); + var panel = document.getElementById('panel-dedupe-total-data'); + var canUse = currentUserRole === 'super_admin' || (currentUserRole === 'admin' && currentUserUsername === ''); + if (tab) tab.style.display = canUse ? '' : 'none'; + if (panel) panel.style.display = canUse ? '' : 'none'; + if (!canUse && tab && tab.classList.contains('active')) { + tab.classList.remove('active'); + if (panel) panel.classList.remove('active'); + var usersTab = document.querySelector('.tab[data-tab="users"]'); + var usersPanel = document.getElementById('panel-users'); + if (usersTab) usersTab.classList.add('active'); + if (usersPanel) usersPanel.classList.add('active'); + } + } + function updateShopManageAccess() { + var tab = document.querySelector('.tab[data-tab="shop-manage"]'); + var panel = document.getElementById('panel-shop-manage'); + if (!tab || !panel) return; + var canUse = currentUserRole === 'super_admin' || + !!currentUserAdminPermissionKeys['admin_shop_manage'] || + !!currentUserAdminPermissionRoutes['shop-manage']; + tab.style.display = canUse ? '' : 'none'; + panel.style.display = canUse ? '' : 'none'; + if (!canUse && tab.classList.contains('active')) { + tab.classList.remove('active'); + panel.classList.remove('active'); + var usersTab = document.querySelector('.tab[data-tab="users"]'); + var usersPanel = document.getElementById('panel-users'); + if (usersTab) usersTab.classList.add('active'); + if (usersPanel) usersPanel.classList.add('active'); + } + } + function loadCurrentUserAdminPermissions() { + currentUserAdminPermissionKeys = {}; + currentUserAdminPermissionRoutes = {}; + if (!currentUserId || currentUserRole === 'super_admin') { + updateShopManageAccess(); + return; + } + fetch('/api/admin/user/' + currentUserId + '/column-permissions?menu_type=admin') + .then(function (r) { return r.json(); }) + .then(function (res) { + if (!res.success) { + updateShopManageAccess(); + return; + } + (res.items || []).forEach(function (item) { + var columnKey = (item.column_key || '').trim(); + var routePath = (item.route_path || '').trim(); + if (columnKey) currentUserAdminPermissionKeys[columnKey] = true; + if (routePath) currentUserAdminPermissionRoutes[routePath] = true; + }); + updateShopManageAccess(); + }) + .catch(function () { + updateShopManageAccess(); + }); + } + function applyTabAccess(tabName, canUse) { + var tab = document.querySelector('.tab[data-tab="' + tabName + '"]'); + var panel = document.getElementById('panel-' + tabName); + if (tab) tab.style.display = canUse ? '' : 'none'; + if (panel) panel.style.display = canUse ? '' : 'none'; + if (!canUse && tab && tab.classList.contains('active')) { + tab.classList.remove('active'); + if (panel) panel.classList.remove('active'); + var usersTab = document.querySelector('.tab[data-tab="users"]'); + var usersPanel = document.getElementById('panel-users'); + if (usersTab) usersTab.classList.add('active'); + if (usersPanel) usersPanel.classList.add('active'); + } + } + function hasAdminTabAccess(tabName) { + var config = ADMIN_TAB_ACCESS_CONFIG[tabName]; + if (!config) return true; + if (currentUserRole === 'super_admin') return true; + if (config.superAdminOnly) return false; + return !!currentUserAdminPermissionKeys[config.columnKey] || + !!currentUserAdminPermissionRoutes[config.routePath]; + } + function refreshAdminTabAccess() { + Object.keys(ADMIN_TAB_ACCESS_CONFIG).forEach(function (tabName) { + applyTabAccess(tabName, hasAdminTabAccess(tabName)); + }); + } + function loadCurrentUserAdminPermissions() { + currentUserAdminPermissionKeys = {}; + currentUserAdminPermissionRoutes = {}; + if (!currentUserId || currentUserRole === 'super_admin') { + refreshAdminTabAccess(); + return; + } + fetch('/api/admin/user/' + currentUserId + '/column-permissions?menu_type=admin') + .then(function (r) { return r.json(); }) + .then(function (res) { + if (!res.success) { + refreshAdminTabAccess(); + return; + } + (res.items || []).forEach(function (item) { + var columnKey = (item.column_key || '').trim(); + var routePath = (item.route_path || '').trim(); + if (columnKey) currentUserAdminPermissionKeys[columnKey] = true; + if (routePath) currentUserAdminPermissionRoutes[routePath] = true; + }); + refreshAdminTabAccess(); + }) + .catch(function () { + refreshAdminTabAccess(); + }); + } + var allColumnsList = []; + function loadColumnsForPermission() { + fetch('/api/admin/columns?menu_type=admin') + .then(function (r) { return r.json(); }) + .then(function (res) { + if (!res.success) return; + allColumnsList = res.items || []; + renderColumnPermissionWrap('createColumnPermissionWrap'); + renderColumnPermissionWrap('editColumnPermissionWrap'); + var selCreate = document.getElementById('createColumnPermissionWrap'); + var selEdit = document.getElementById('editColumnPermissionWrap'); + if (selCreate && !selCreate._colCardsBound) { + selCreate._colCardsBound = true; + selCreate.onchange = function () { renderColumnCards('createColumnPermissionWrap'); }; + } + if (selEdit && !selEdit._colCardsBound) { + selEdit._colCardsBound = true; + selEdit.onchange = function () { renderColumnCards('editColumnPermissionWrap'); }; + } + }) + .catch(function () { }); + } + var columnCardsContainerMap = { createColumnPermissionWrap: 'createColumnCards', editColumnPermissionWrap: 'editColumnCards' }; + function renderColumnCards(selectId) { + var sel = document.getElementById(selectId); + var cardsId = columnCardsContainerMap[selectId]; + var cardsEl = cardsId ? document.getElementById(cardsId) : null; + if (!sel || sel.tagName !== 'SELECT' || !cardsEl) return; + cardsEl.innerHTML = ''; + for (var i = 0; i < sel.options.length; i++) { + var opt = sel.options[i]; + if (opt.disabled || !opt.selected) continue; + var card = document.createElement('span'); + card.className = 'column-permission-card'; + card.textContent = opt.textContent; + var btn = document.createElement('button'); + btn.type = 'button'; + btn.className = 'col-card-remove'; + btn.setAttribute('aria-label', '移除'); + btn.textContent = '×'; + (function (option, seldId) { + btn.onclick = function () { + option.selected = false; + renderColumnCards(seldId); + }; + })(opt, selectId); + card.appendChild(btn); + cardsEl.appendChild(card); + } + } + function renderColumnPermissionWrap(wrapId) { + var sel = document.getElementById(wrapId); + if (!sel || sel.tagName !== 'SELECT') return; + sel.innerHTML = ''; + allColumnsList.forEach(function (c) { + var opt = document.createElement('option'); + opt.value = c.id; + opt.textContent = c.name + ' (' + c.column_key + ')'; + sel.appendChild(opt); + }); + if (allColumnsList.length === 0) { + var opt = document.createElement('option'); + opt.disabled = true; + opt.textContent = '暂无菜单,请先在“栏目权限配置”中新增菜单'; + sel.appendChild(opt); + } + renderColumnCards(wrapId); + } + function getSelectedColumnIds(wrapId) { + var sel = document.getElementById(wrapId); + if (!sel || sel.tagName !== 'SELECT') return []; + var ids = []; + for (var i = 0; i < sel.options.length; i++) { + if (sel.options[i].selected) { + var v = parseInt(sel.options[i].value, 10); + if (!isNaN(v)) ids.push(v); + } + } + return ids; + } + function setColumnPermissionCheckboxes(wrapId, columnIds) { + var sel = document.getElementById(wrapId); + if (!sel || sel.tagName !== 'SELECT') return; + var set = {}; + (columnIds || []).forEach(function (id) { set[id] = true; }); + for (var i = 0; i < sel.options.length; i++) { + var opt = sel.options[i]; + if (opt.disabled) continue; + opt.selected = set[parseInt(opt.value, 10)] || false; + } + renderColumnCards(wrapId); + } + function updateCreateFormByRole() { + var roleSel = document.getElementById('createRole'); + var optAdmin = document.getElementById('optAdmin'); + var formCreatedBy = document.getElementById('formGroupCreatedBy'); + var selCreatedBy = document.getElementById('createCreatedBy'); + if (currentUserRole === 'super_admin') { + if (optAdmin) optAdmin.style.display = ''; + formCreatedBy.style.display = (roleSel.value === 'normal') ? 'block' : 'none'; + selCreatedBy.innerHTML = ''; + adminsList.forEach(function (a) { + var opt = document.createElement('option'); + opt.value = a.id; + opt.textContent = a.username; + selCreatedBy.appendChild(opt); + }); + } else { + if (optAdmin) optAdmin.style.display = 'none'; + roleSel.value = 'normal'; + formCreatedBy.style.display = 'none'; + } + } + function bindUserActions() { + document.querySelectorAll('[data-edit]').forEach(function (btn) { + btn.onclick = function () { + var raw = (btn.getAttribute('data-user') || '{}').replace(/"/g, '"'); + var u; + try { u = JSON.parse(raw); } catch (e) { u = {}; } + document.getElementById('editUserId').value = u.id || ''; + document.getElementById('editUsername').value = u.username || ''; + document.getElementById('editPassword').value = ''; + var editRole = document.getElementById('editRole'); + var editFormGroupRole = document.getElementById('editFormGroupRole'); + var editFormGroupCreator = document.getElementById('editFormGroupCreator'); + var editCreatorName = document.getElementById('editCreatorName'); + editFormGroupRole.style.display = (currentUserRole === 'super_admin' && u.role !== 'super_admin') ? 'block' : 'none'; + editFormGroupCreator.style.display = (u.role === 'normal' && u.creator_username) ? 'block' : 'none'; + editCreatorName.value = u.creator_username || ''; + if (u.role !== 'super_admin') { editRole.value = u.role || 'normal'; } + document.getElementById('msgEdit').textContent = ''; + if (allColumnsList.length) { + renderColumnPermissionWrap('editColumnPermissionWrap'); + fetch('/api/admin/user/' + (u.id) + '/columns') + .then(function (r) { return r.json(); }) + .then(function (res) { + if (res.success && res.column_ids) setColumnPermissionCheckboxes('editColumnPermissionWrap', res.column_ids); + }); + } + document.getElementById('editUserModal').classList.add('show'); + }; + }); + document.querySelectorAll('[data-delete]').forEach(function (btn) { + btn.onclick = function () { + if (!confirm('确定删除用户 "' + (btn.dataset.name || '') + '" 吗?')) return; + fetch('/api/admin/user/' + btn.dataset.delete, { method: 'DELETE' }) + .then(function (r) { return r.json(); }) + .then(function (res) { + if (res.success) { loadUsers(userPage); } + else { alert(res.error || '删除失败'); } + }); + }; + }); + } + + document.getElementById('btnSearchUsers').onclick = function () { loadUsers(1); }; + document.getElementById('createRole').onchange = function () { updateCreateFormByRole(); }; + document.getElementById('btnCreate').onclick = function () { + var username = (document.getElementById('username').value || '').trim(); + var password = document.getElementById('password').value || ''; + var role = document.getElementById('createRole').value || 'normal'; + var createdById = document.getElementById('createCreatedBy').value ? parseInt(document.getElementById('createCreatedBy').value, 10) : null; + var msgEl = document.getElementById('msgCreate'); + msgEl.textContent = ''; + msgEl.className = 'msg'; + if (!username || username.length < 2) { + msgEl.textContent = '用户名至少 2 个字符'; + msgEl.classList.add('err'); + return; + } + if (!password || password.length < 6) { + msgEl.textContent = '密码至少 6 个字符'; + msgEl.classList.add('err'); + return; + } + var body = { username: username, password: password, role: role }; + if (role === 'normal' && currentUserRole === 'super_admin' && createdById) body.created_by_id = createdById; + body.column_ids = getSelectedColumnIds('createColumnPermissionWrap'); + fetch('/api/admin/user', { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify(body) + }) + .then(function (r) { return r.json(); }) + .then(function (res) { + if (res.success) { + msgEl.textContent = res.msg || '创建成功'; + msgEl.classList.add('ok'); + document.getElementById('username').value = ''; + document.getElementById('password').value = ''; + loadUsers(1); + } else { + msgEl.textContent = res.error || '创建失败'; + msgEl.classList.add('err'); + } + }) + .catch(function () { + msgEl.textContent = '请求失败'; + msgEl.classList.add('err'); + }); + }; + + document.getElementById('btnSaveUser').onclick = function () { + var uid = document.getElementById('editUserId').value; + var password = document.getElementById('editPassword').value; + var editRoleEl = document.getElementById('editRole'); + var msgEl = document.getElementById('msgEdit'); + msgEl.textContent = ''; + msgEl.className = 'msg'; + var body = {}; + if (password) body.password = password; + if (currentUserRole === 'super_admin' && editRoleEl && editRoleEl.offsetParent !== null) + body.role = editRoleEl.value || 'normal'; + body.column_ids = getSelectedColumnIds('editColumnPermissionWrap'); + fetch('/api/admin/user/' + uid, { + method: 'PUT', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify(body) + }) + .then(function (r) { return r.json(); }) + .then(function (res) { + if (res.success) { + msgEl.textContent = res.msg || '保存成功'; + msgEl.classList.add('ok'); + document.getElementById('editUserModal').classList.remove('show'); + loadUsers(userPage); + if (String(uid || '') === String(currentUserId || '')) { + loadAdminMenus(getActiveAdminTabName()); + } + } else { + msgEl.textContent = res.error || '保存失败'; + msgEl.classList.add('err'); + } + }); + }; + document.getElementById('btnCloseEdit').onclick = function () { + document.getElementById('editUserModal').classList.remove('show'); + }; + + // ========== 生成记录 ========== + var historyPage = 1, historyPageSize = 15; + function toSqlDatetime(val) { + if (!val) return ''; + return val.replace('T', ' '); + } + function buildHistoryQuery(page) { + var q = 'page=' + (page || 1) + '&page_size=' + historyPageSize; + var uid = document.getElementById('filterUser').value; + var start = toSqlDatetime(document.getElementById('filterTimeStart').value); + var end = toSqlDatetime(document.getElementById('filterTimeEnd').value); + if (uid) q += '&user_id=' + uid; + if (start) q += '&time_start=' + encodeURIComponent(start); + if (end) q += '&time_end=' + encodeURIComponent(end); + return q; + } + function loadHistory(page) { + historyPage = page || 1; + fetch('/api/admin/history?' + buildHistoryQuery(historyPage)) + .then(function (r) { return r.json(); }) + .then(function (res) { + var tbody = document.getElementById('historyListBody'); + if (!res.success) { + tbody.innerHTML = '加载失败: ' + (res.error || '') + ''; + return; + } + var items = res.items || []; + if (items.length === 0) { + tbody.innerHTML = '暂无记录'; + } else { + tbody.innerHTML = items.map(function (h) { + var urls = (h.result_urls || []); + var thumbUrls = (h.long_image_url ? [h.long_image_url] : []).concat(urls); + var thumbs = thumbUrls.slice(0, 3).map(function (url) { + return ''; + }).join(''); + return '' + h.id + '' + (h.username || '-') + '' + + (h.panel_type || '-') + '' + (h.created_at || '') + '' + + '
' + (thumbs || '-') + '
'; + }).join(''); + } + renderPagination('historyPagination', res.total, res.page, res.page_size, loadHistory); + }) + .catch(function () { + document.getElementById('historyListBody').innerHTML = '请求失败'; + }); + } + function loadUserOptions() { + fetch('/api/admin/users?page=1&page_size=999') + .then(function (r) { return r.json(); }) + .then(function (res) { + var sel = document.getElementById('filterUser'); + var cur = sel.value; + sel.innerHTML = ''; + (res.items || []).forEach(function (u) { + var opt = document.createElement('option'); + opt.value = u.id; + opt.textContent = u.username + ' (' + roleLabel(u.role || 'normal') + ')'; + sel.appendChild(opt); + }); + sel.value = cur || ''; + }); + } + var shopManageGroupUsers = []; + function refreshShopManageGroupUserSelect(selectedUserId) { + var sel = document.getElementById('shopManageGroupUserSelect'); + if (!sel) return; + var opts = ['']; + shopManageGroupUsers.forEach(function (u) { + opts.push(''); + }); + sel.innerHTML = opts.join(''); + if (selectedUserId != null && selectedUserId !== '') { + sel.value = String(selectedUserId); + } + syncShopManageGroupNameWithUser(); + } + function syncShopManageGroupNameWithUser() { + var sel = document.getElementById('shopManageGroupUserSelect'); + var input = document.getElementById('shopManageGroupInput'); + if (!sel || !input) return; + var selectedUser = null; + shopManageGroupUsers.some(function (u) { + if (String(u.id) === String(sel.value || '')) { + selectedUser = u; + return true; + } + return false; + }); + input.value = selectedUser ? (selectedUser.username || '') : ''; + } + function loadUserOptions() { + return fetch('/api/admin/users?page=1&page_size=999') + .then(function (r) { return r.json(); }) + .then(function (res) { + var items = res.items || []; + shopManageGroupUsers = items.map(function (u) { + return { id: u.id, username: u.username || '', role: u.role || 'normal' }; + }); + refreshShopManageGroupUserSelect(document.getElementById('shopManageGroupUserSelect') ? document.getElementById('shopManageGroupUserSelect').value : ''); + var sel = document.getElementById('filterUser'); + var cur = sel.value; + sel.innerHTML = ''; + items.forEach(function (u) { + var opt = document.createElement('option'); + opt.value = u.id; + opt.textContent = u.username + ' (' + roleLabel(u.role || 'normal') + ')'; + sel.appendChild(opt); + }); + sel.value = cur || ''; + }); + } + document.getElementById('btnFilterHistory').onclick = function () { loadHistory(1); }; + + // ========== 数据去重总数据 ========== + var dedupeTotalDataPage = 1, dedupeTotalDataPageSize = 15; + function buildDedupeTotalDataQuery(page) { + var q = 'page=' + (page || 1) + '&page_size=' + dedupeTotalDataPageSize; + var keyword = (document.getElementById('searchDedupeTotalData').value || '').trim(); + if (keyword) q += '&keyword=' + encodeURIComponent(keyword); + return q; + } + function loadDedupeTotalData(page) { + dedupeTotalDataPage = page || 1; + fetch('/api/admin/dedupe-total-data?' + buildDedupeTotalDataQuery(dedupeTotalDataPage)) + .then(function (r) { return r.json(); }) + .then(function (res) { + var tbody = document.getElementById('dedupeTotalDataListBody'); + if (!res.success) { + tbody.innerHTML = '加载失败: ' + (res.error || '') + ''; + return; + } + var items = res.items || []; + if (items.length === 0) { + tbody.innerHTML = '暂无总数据'; + } else { + tbody.innerHTML = items.map(function (item) { + return '' + item.id + '' + (item.data_value || '') + '' + (item.created_at || '') + '' + + ' ' + + '' + + ''; + }).join(''); + } + renderPagination('dedupeTotalDataPagination', res.total, res.page, res.page_size, loadDedupeTotalData); + bindDedupeTotalDataActions(); + }) + .catch(function () { + document.getElementById('dedupeTotalDataListBody').innerHTML = '请求失败'; + }); + } + function bindDedupeTotalDataActions() { + document.querySelectorAll('[data-dedupe-total-edit]').forEach(function (btn) { + btn.onclick = function () { + document.getElementById('editDedupeTotalDataId').value = btn.dataset.dedupeTotalEdit || ''; + document.getElementById('editDedupeTotalDataValue').value = (btn.dataset.value || '').replace(/"/g, '"'); + document.getElementById('msgEditDedupeTotalData').textContent = ''; + document.getElementById('msgEditDedupeTotalData').className = 'msg'; + document.getElementById('editDedupeTotalDataModal').classList.add('show'); + }; + }); + document.querySelectorAll('[data-dedupe-total-delete]').forEach(function (btn) { + btn.onclick = function () { + var value = (btn.dataset.value || '').replace(/"/g, '"'); + if (!confirm('确定删除总数据“' + value + '”吗?')) return; + fetch('/api/admin/dedupe-total-data/' + btn.dataset.dedupeTotalDelete, { method: 'DELETE' }) + .then(function (r) { return r.json(); }) + .then(function (res) { + if (res.success) { loadDedupeTotalData(dedupeTotalDataPage); } + else { alert(res.error || '删除失败'); } + }); + }; + }); + } + document.getElementById('btnSearchDedupeTotalData').onclick = function () { loadDedupeTotalData(1); }; + var dedupeImportPollTimer = null; + var dedupeDeleteImportPollTimer = null; + function stopDedupeImportProgress() { + if (dedupeImportPollTimer) { + clearInterval(dedupeImportPollTimer); + dedupeImportPollTimer = null; + } + } + function stopDedupeDeleteImportProgress() { + if (dedupeDeleteImportPollTimer) { + clearInterval(dedupeDeleteImportPollTimer); + dedupeDeleteImportPollTimer = null; + } + } + function setDedupeImportProgress(percent, text) { + var wrap = document.getElementById('dedupeTotalDataProgressWrap'); + var fill = document.getElementById('dedupeTotalDataProgressFill'); + var textEl = document.getElementById('dedupeTotalDataProgressText'); + wrap.style.display = 'block'; + fill.style.width = Math.max(0, Math.min(100, percent || 0)) + '%'; + textEl.textContent = text || ''; + } + function setDedupeDeleteImportProgress(percent, text) { + var wrap = document.getElementById('dedupeTotalDataDeleteProgressWrap'); + var fill = document.getElementById('dedupeTotalDataDeleteProgressFill'); + var textEl = document.getElementById('dedupeTotalDataDeleteProgressText'); + wrap.style.display = 'block'; + fill.style.width = Math.max(0, Math.min(100, percent || 0)) + '%'; + textEl.textContent = text || ''; + } + function pollDedupeImport(importId) { + stopDedupeImportProgress(); + function tick() { + fetch('/api/admin/dedupe-total-data/import/' + encodeURIComponent(importId)) + .then(function (r) { return r.json(); }) + .then(function (res) { + if (!res.success) { + stopDedupeImportProgress(); + document.getElementById('msgDedupeTotalData').textContent = res.error || '查询导入进度失败'; + document.getElementById('msgDedupeTotalData').className = 'msg err'; + return; + } + var progress = res.progress || {}; + var totalRows = progress.total_rows || 0; + var processedRows = progress.processed_rows || 0; + var percent = totalRows > 0 ? Math.round(processedRows * 100 / totalRows) : 0; + setDedupeImportProgress(percent, '处理中:已处理 ' + processedRows + ' / ' + totalRows + ',新增 ' + (progress.inserted_count || 0) + ',跳过 ' + (progress.skipped_count || 0)); + if (progress.status === 'success') { + stopDedupeImportProgress(); + document.getElementById('msgDedupeTotalData').textContent = '导入成功:总行数 ' + (progress.total_rows || 0) + ',ASIN 数量 ' + (progress.asin_count || 0) + ',新增 ' + (progress.inserted_count || 0) + ',跳过 ' + (progress.skipped_count || 0); + document.getElementById('msgDedupeTotalData').className = 'msg ok'; + loadDedupeTotalData(1); + } else if (progress.status === 'failed') { + stopDedupeImportProgress(); + document.getElementById('msgDedupeTotalData').textContent = progress.error_message || '导入失败'; + document.getElementById('msgDedupeTotalData').className = 'msg err'; + } + }) + .catch(function () { + stopDedupeImportProgress(); + document.getElementById('msgDedupeTotalData').textContent = '查询导入进度失败'; + document.getElementById('msgDedupeTotalData').className = 'msg err'; + }); + } + tick(); + dedupeImportPollTimer = setInterval(tick, 1000); + } + function pollDedupeDeleteImport(importId) { + stopDedupeDeleteImportProgress(); + function tick() { + fetch('/api/admin/dedupe-total-data/delete-import/' + encodeURIComponent(importId)) + .then(function (r) { return r.json(); }) + .then(function (res) { + if (!res.success) { + stopDedupeDeleteImportProgress(); + document.getElementById('msgDeleteDedupeTotalData').textContent = res.error || '查询删除进度失败'; + document.getElementById('msgDeleteDedupeTotalData').className = 'msg err'; + return; + } + var progress = res.progress || {}; + var totalRows = progress.total_rows || 0; + var processedRows = progress.processed_rows || 0; + var percent = totalRows > 0 ? Math.round(processedRows * 100 / totalRows) : 0; + setDedupeDeleteImportProgress(percent, '处理中:已处理 ' + processedRows + ' / ' + totalRows + ',删除 ' + (progress.deleted_count || 0) + ',跳过 ' + (progress.skipped_count || 0)); + if (progress.status === 'success') { + stopDedupeDeleteImportProgress(); + document.getElementById('msgDeleteDedupeTotalData').textContent = '删除成功:总行数 ' + (progress.total_rows || 0) + ',ASIN 数量 ' + (progress.asin_count || 0) + ',删除 ' + (progress.deleted_count || 0) + ',跳过 ' + (progress.skipped_count || 0); + document.getElementById('msgDeleteDedupeTotalData').className = 'msg ok'; + loadDedupeTotalData(1); + } else if (progress.status === 'failed') { + stopDedupeDeleteImportProgress(); + document.getElementById('msgDeleteDedupeTotalData').textContent = progress.error_message || '删除失败'; + document.getElementById('msgDeleteDedupeTotalData').className = 'msg err'; + } + }) + .catch(function () { + stopDedupeDeleteImportProgress(); + document.getElementById('msgDeleteDedupeTotalData').textContent = '查询删除进度失败'; + document.getElementById('msgDeleteDedupeTotalData').className = 'msg err'; + }); + } + tick(); + dedupeDeleteImportPollTimer = setInterval(tick, 1000); + } + + document.getElementById('btnAddDedupeTotalData').onclick = function () { + var fileInput = document.getElementById('dedupeTotalDataFile'); + var msgEl = document.getElementById('msgDedupeTotalData'); + msgEl.textContent = ''; + msgEl.className = 'msg'; + stopDedupeImportProgress(); + document.getElementById('dedupeTotalDataProgressWrap').style.display = 'none'; + if (!fileInput.files || fileInput.files.length === 0) { + msgEl.textContent = '请选择 Excel 文件'; + msgEl.classList.add('err'); + return; + } + var file = fileInput.files[0]; + var lowerName = (file.name || '').toLowerCase(); + if (!(lowerName.endsWith('.xlsx') || lowerName.endsWith('.xls'))) { + msgEl.textContent = '仅支持 .xlsx 或 .xls 文件'; + msgEl.classList.add('err'); + return; + } + var formData = new FormData(); + formData.append('file', file); + var xhr = new XMLHttpRequest(); + xhr.open('POST', '/api/admin/dedupe-total-data/import', true); + xhr.upload.onprogress = function (event) { + if (event.lengthComputable) { + var percent = Math.round(event.loaded * 100 / event.total); + setDedupeImportProgress(percent, '上传中:' + percent + '%'); + } + }; + xhr.onreadystatechange = function () { + if (xhr.readyState !== 4) return; + if (xhr.status < 200 || xhr.status >= 300) { + msgEl.textContent = '请求失败'; + msgEl.className = 'msg err'; + return; + } + var res; + try { res = JSON.parse(xhr.responseText || '{}'); } catch (e) { res = { success: false, error: '返回格式错误' }; } + if (!res.success) { + msgEl.textContent = res.error || '导入失败'; + msgEl.className = 'msg err'; + return; + } + setDedupeImportProgress(100, '上传完成,后端处理中...'); + pollDedupeImport(res.import_id); + fileInput.value = ''; + }; + xhr.onerror = function () { + msgEl.textContent = '请求失败'; + msgEl.className = 'msg err'; + }; + xhr.send(formData); + }; + document.getElementById('btnDeleteImportDedupeTotalData').onclick = function () { + var fileInput = document.getElementById('dedupeTotalDataDeleteFile'); + var msgEl = document.getElementById('msgDeleteDedupeTotalData'); + msgEl.textContent = ''; + msgEl.className = 'msg'; + stopDedupeDeleteImportProgress(); + document.getElementById('dedupeTotalDataDeleteProgressWrap').style.display = 'none'; + if (!fileInput.files || fileInput.files.length === 0) { + msgEl.textContent = '请选择 Excel 文件'; + msgEl.classList.add('err'); + return; + } + var file = fileInput.files[0]; + var lowerName = (file.name || '').toLowerCase(); + if (!(lowerName.endsWith('.xlsx') || lowerName.endsWith('.xls'))) { + msgEl.textContent = '仅支持 .xlsx 或 .xls 文件'; + msgEl.classList.add('err'); + return; + } + if (!confirm('确定按 Excel 中的 ASIN 批量删除匹配的总数据吗?')) { + return; + } + var formData = new FormData(); + formData.append('file', file); + var xhr = new XMLHttpRequest(); + xhr.open('POST', '/api/admin/dedupe-total-data/delete-import', true); + xhr.upload.onprogress = function (event) { + if (event.lengthComputable) { + var percent = Math.round(event.loaded * 100 / event.total); + setDedupeDeleteImportProgress(percent, '上传中:' + percent + '%'); + } + }; + xhr.onreadystatechange = function () { + if (xhr.readyState !== 4) return; + if (xhr.status < 200 || xhr.status >= 300) { + msgEl.textContent = '请求失败'; + msgEl.className = 'msg err'; + return; + } + var res; + try { res = JSON.parse(xhr.responseText || '{}'); } catch (e) { res = { success: false, error: '返回格式错误' }; } + if (!res.success) { + msgEl.textContent = res.error || '删除失败'; + msgEl.className = 'msg err'; + return; + } + setDedupeDeleteImportProgress(100, '上传完成,后端处理中...'); + pollDedupeDeleteImport(res.import_id); + fileInput.value = ''; + }; + xhr.onerror = function () { + msgEl.textContent = '请求失败'; + msgEl.className = 'msg err'; + }; + xhr.send(formData); + }; + document.getElementById('btnSaveDedupeTotalData').onclick = function () { + var itemId = document.getElementById('editDedupeTotalDataId').value; + var value = (document.getElementById('editDedupeTotalDataValue').value || '').trim(); + var msgEl = document.getElementById('msgEditDedupeTotalData'); + msgEl.textContent = ''; + msgEl.className = 'msg'; + if (!value) { + msgEl.textContent = '请填写总数据值'; + msgEl.classList.add('err'); + return; + } + fetch('/api/admin/dedupe-total-data/' + itemId, { + method: 'PUT', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ data_value: value }) + }) + .then(function (r) { return r.json(); }) + .then(function (res) { + if (res.success) { + document.getElementById('editDedupeTotalDataModal').classList.remove('show'); + loadDedupeTotalData(dedupeTotalDataPage); + } else { + msgEl.textContent = res.error || '保存失败'; + msgEl.classList.add('err'); + } + }); + }; + document.getElementById('btnCloseEditDedupeTotalData').onclick = function () { + document.getElementById('editDedupeTotalDataModal').classList.remove('show'); + }; + + // ========== 店铺密钥管理 ========== + var shopKeyPage = 1, shopKeyPageSize = 15; + function buildShopKeyQuery(page) { + return 'page=' + (page || 1) + '&page_size=' + shopKeyPageSize; + } + function loadShopKeys(page) { + shopKeyPage = page || 1; + fetch('/api/admin/shop-keys?' + buildShopKeyQuery(shopKeyPage)) + .then(function (r) { return r.json(); }) + .then(function (res) { + var tbody = document.getElementById('shopKeyListBody'); + if (!res.success) { + tbody.innerHTML = '加载失败: ' + (res.error || '') + ''; + return; + } + var items = res.items || []; + if (items.length === 0) { + tbody.innerHTML = '暂无店铺密钥'; + } else { + tbody.innerHTML = items.map(function (item, index) { + var rowNo = (shopKeyPage - 1) * shopKeyPageSize + index + 1; + return '' + rowNo + '' + (item.ziniao_account_name || '') + '' + (item.ziniao_token || '') + '' + (item.created_at || '') + '' + (item.updated_at || '') + '' + + ' ' + + '' + + ''; + }).join(''); + } + renderPagination('shopKeyPagination', res.total, res.page, res.page_size, loadShopKeys); + bindShopKeyActions(); + }) + .catch(function () { + document.getElementById('shopKeyListBody').innerHTML = '请求失败'; + }); + } + function bindShopKeyActions() { + document.querySelectorAll('[data-shop-key-edit]').forEach(function (btn) { + btn.onclick = function () { + var item = {}; + try { item = JSON.parse((btn.dataset.shopKey || '').replace(/"/g, '"')); } catch (e) { item = {}; } + document.getElementById('editShopKeyId').value = item.id || ''; + document.getElementById('editShopKeyZiniaoAccountName').value = item.ziniao_account_name || ''; + document.getElementById('editShopKeyZiniaoToken').value = item.ziniao_token || ''; + document.getElementById('msgEditShopKey').textContent = ''; + document.getElementById('msgEditShopKey').className = 'msg'; + document.getElementById('editShopKeyModal').classList.add('show'); + }; + }); + document.querySelectorAll('[data-shop-key-delete]').forEach(function (btn) { + btn.onclick = function () { + var name = (btn.dataset.ziniaoAccountName || '').replace(/"/g, '"'); + if (!confirm('确定删除店铺密钥“' + name + '”吗?')) return; + fetch('/api/admin/shop-key/' + btn.dataset.shopKeyDelete, { method: 'DELETE' }) + .then(function (r) { return r.json(); }) + .then(function (res) { + if (res.success) { loadShopKeys(shopKeyPage); } + else { alert(res.error || '删除失败'); } + }); + }; + }); + } + document.getElementById('btnCreateShopKey').onclick = function () { + var ziniaoAccountName = (document.getElementById('shopKeyZiniaoAccountName').value || '').trim(); + var ziniaoToken = (document.getElementById('shopKeyZiniaoToken').value || '').trim(); + var msgEl = document.getElementById('msgShopKey'); + msgEl.textContent = ''; + msgEl.className = 'msg'; + if (!ziniaoAccountName || !ziniaoToken) { + msgEl.textContent = '请完整填写紫鸟账号名称、紫鸟令牌'; + msgEl.classList.add('err'); + return; + } + fetch('/api/admin/shop-key', { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ + ziniao_account_name: ziniaoAccountName, + ziniao_token: ziniaoToken + }) + }) + .then(function (r) { return r.json(); }) + .then(function (res) { + if (res.success) { + document.getElementById('shopKeyZiniaoAccountName').value = ''; + document.getElementById('shopKeyZiniaoToken').value = ''; + msgEl.textContent = res.msg || '创建成功'; + msgEl.className = 'msg ok'; + loadShopKeys(1); + } else { + msgEl.textContent = res.error || '创建失败'; + msgEl.className = 'msg err'; + } + }) + .catch(function () { + msgEl.textContent = '请求失败'; + msgEl.className = 'msg err'; + }); + }; + document.getElementById('btnSaveShopKey').onclick = function () { + var itemId = document.getElementById('editShopKeyId').value; + var ziniaoAccountName = (document.getElementById('editShopKeyZiniaoAccountName').value || '').trim(); + var ziniaoToken = (document.getElementById('editShopKeyZiniaoToken').value || '').trim(); + var msgEl = document.getElementById('msgEditShopKey'); + msgEl.textContent = ''; + msgEl.className = 'msg'; + if (!ziniaoAccountName || !ziniaoToken) { + msgEl.textContent = '请完整填写紫鸟账号名称、紫鸟令牌'; + msgEl.classList.add('err'); + return; + } + fetch('/api/admin/shop-key/' + itemId, { + method: 'PUT', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ + ziniao_account_name: ziniaoAccountName, + ziniao_token: ziniaoToken + }) + }) + .then(function (r) { return r.json(); }) + .then(function (res) { + if (res.success) { + document.getElementById('editShopKeyModal').classList.remove('show'); + loadShopKeys(shopKeyPage); + } else { + msgEl.textContent = res.error || '保存失败'; + msgEl.classList.add('err'); + } + }) + .catch(function () { + msgEl.textContent = '请求失败'; + msgEl.classList.add('err'); + }); + }; + document.getElementById('btnCloseEditShopKey').onclick = function () { + document.getElementById('editShopKeyModal').classList.remove('show'); + }; + + // ========== 店铺管理 ========== + var shopManagePage = 1, shopManagePageSize = 15; + var shopManageGroups = []; + + function buildShopManageQuery(page) { + var query = 'page=' + (page || 1) + '&page_size=' + shopManagePageSize; + var groupId = (document.getElementById('shopManageFilterGroupId').value || '').trim(); + var shopName = (document.getElementById('shopManageFilterShopName').value || '').trim(); + if (groupId) query += '&group_id=' + encodeURIComponent(groupId); + if (shopName) query += '&shop_name=' + encodeURIComponent(shopName); + return query; + } + + function refreshShopGroupSelects(selectedCreateId, selectedEditId) { + var createSel = document.getElementById('shopManageGroupSelect'); + var editSel = document.getElementById('editShopManageGroupSelect'); + var filterSel = document.getElementById('shopManageFilterGroupId'); + var skipCreateSel = document.getElementById('skipPriceAsinGroupSelect'); + var skipFilterSel = document.getElementById('skipPriceAsinFilterGroupId'); + var chooseSkipShopSel = document.getElementById('chooseSkipPriceAsinShopGroupId'); + var selectedFilterId = filterSel ? filterSel.value : ''; + var selectedSkipCreateId = skipCreateSel ? skipCreateSel.value : ''; + var selectedSkipFilterId = skipFilterSel ? skipFilterSel.value : ''; + var selectedChooseSkipShopId = chooseSkipShopSel ? chooseSkipShopSel.value : ''; + var createOpts = ['']; + var filterOpts = ['']; + shopManageGroups.forEach(function (g) { + var option = ''; + createOpts.push(option); + filterOpts.push(option); + }); + createSel.innerHTML = createOpts.join(''); + editSel.innerHTML = createOpts.join(''); + if (skipCreateSel) skipCreateSel.innerHTML = createOpts.join(''); + if (filterSel) filterSel.innerHTML = filterOpts.join(''); + if (skipFilterSel) skipFilterSel.innerHTML = filterOpts.join(''); + if (chooseSkipShopSel) chooseSkipShopSel.innerHTML = filterOpts.join(''); + if (selectedCreateId != null) createSel.value = String(selectedCreateId); + if (selectedEditId != null) editSel.value = String(selectedEditId); + if (filterSel && selectedFilterId) filterSel.value = selectedFilterId; + if (skipCreateSel && selectedSkipCreateId) skipCreateSel.value = selectedSkipCreateId; + if (skipFilterSel && selectedSkipFilterId) skipFilterSel.value = selectedSkipFilterId; + if (chooseSkipShopSel && selectedChooseSkipShopId) chooseSkipShopSel.value = selectedChooseSkipShopId; + } + + function loadShopManageGroups(selectedCreateId, selectedEditId) { + return fetch('/api/admin/shop-manage-groups') + .then(function (r) { return r.json(); }) + .then(function (res) { + if (!res.success) throw new Error(res.error || '加载分组失败'); + shopManageGroups = res.items || []; + refreshShopGroupSelects(selectedCreateId, selectedEditId); + return shopManageGroups; + }) + .catch(function () { + shopManageGroups = []; + refreshShopGroupSelects(); + return []; + }); + } + + function openShopManageGroupModal() { + document.getElementById('shopManageGroupEditId').value = ''; + document.getElementById('shopManageGroupInput').value = ''; + document.getElementById('msgShopManageGroup').textContent = ''; + document.getElementById('msgShopManageGroup').className = 'msg'; + loadShopManageGroups().then(function () { + renderShopManageGroupRows(); + document.getElementById('shopManageGroupModal').classList.add('show'); + }); + } + + function renderShopManageGroupRows() { + var tbody = document.getElementById('shopManageGroupListBody'); + if (!shopManageGroups.length) { + tbody.innerHTML = '暂无分组'; + return; + } + tbody.innerHTML = shopManageGroups.map(function (item, index) { + return '' + (index + 1) + '' + (item.group_name || '') + '' + (item.created_at || '') + '' + (item.updated_at || '') + '' + + ' ' + + '' + + ''; + }).join(''); + + document.querySelectorAll('[data-shop-group-edit]').forEach(function (btn) { + btn.onclick = function () { + document.getElementById('shopManageGroupEditId').value = btn.dataset.shopGroupEdit; + document.getElementById('shopManageGroupInput').value = (btn.dataset.shopGroupName || '').replace(/"/g, '"'); + }; + }); + + document.querySelectorAll('[data-shop-group-delete]').forEach(function (btn) { + btn.onclick = function () { + var gid = btn.dataset.shopGroupDelete; + var gname = (btn.dataset.shopGroupName || '').replace(/"/g, '"'); + if (!confirm('确定删除分组“' + gname + '”吗?')) return; + fetch('/api/admin/shop-manage-group/' + gid, { method: 'DELETE' }) + .then(function (r) { return r.json(); }) + .then(function (res) { + if (!res.success) { + alert(res.error || '删除失败'); + return; + } + loadShopManageGroups().then(function () { + renderShopManageGroupRows(); + loadShopManage(shopManagePage); + loadSkipPriceAsin(skipPriceAsinPage); + }); + }); + }; + }); + } + + function loadShopManage(page) { + shopManagePage = page || 1; + fetch('/api/admin/shop-manages?' + buildShopManageQuery(shopManagePage)) + .then(function (r) { return r.json(); }) + .then(function (res) { + var tbody = document.getElementById('shopManageListBody'); + if (!res.success) { + tbody.innerHTML = '加载失败: ' + (res.error || '') + ''; + return; + } + var items = res.items || []; + if (items.length === 0) { + tbody.innerHTML = '暂无店铺'; + } else { + tbody.innerHTML = items.map(function (item, index) { + var rowNo = (shopManagePage - 1) * shopManagePageSize + index + 1; + return '' + rowNo + '' + (item.group_name || '') + '' + (item.shop_name || '') + '' + (item.mall_name || '') + '' + (item.account || '') + '' + (item.password || '') + '' + (item.created_at || '') + '' + (item.updated_at || '') + '' + + ' ' + + '' + + ''; + }).join(''); + } + renderPagination('shopManagePagination', res.total, res.page, res.page_size, loadShopManage); + bindShopManageActions(); + }) + .catch(function () { + document.getElementById('shopManageListBody').innerHTML = '请求失败'; + }); + } + + function bindShopManageActions() { + document.querySelectorAll('[data-shop-manage-edit]').forEach(function (btn) { + btn.onclick = function () { + var item = {}; + try { item = JSON.parse((btn.dataset.shopManage || '').replace(/"/g, '"')); } catch (e) { item = {}; } + document.getElementById('editShopManageId').value = item.id || ''; + document.getElementById('editShopManageShopName').value = item.shop_name || ''; + document.getElementById('editShopManageMallName').value = item.mall_name || ''; + document.getElementById('editShopManageAccount').value = item.account || ''; + document.getElementById('editShopManagePassword').value = item.password || ''; + document.getElementById('msgEditShopManage').textContent = ''; + document.getElementById('msgEditShopManage').className = 'msg'; + loadShopManageGroups(null, item.group_id).then(function () { + document.getElementById('editShopManageModal').classList.add('show'); + }); + }; + }); + document.querySelectorAll('[data-shop-manage-delete]').forEach(function (btn) { + btn.onclick = function () { + var name = (btn.dataset.shopManageName || '').replace(/"/g, '"'); + if (!confirm('确定删除店铺“' + name + '”吗?')) return; + fetch('/api/admin/shop-manage/' + btn.dataset.shopManageDelete, { method: 'DELETE' }) + .then(function (r) { return r.json(); }) + .then(function (res) { + if (res.success) { loadShopManage(shopManagePage); } + else { alert(res.error || '删除失败'); } + }); + }; + }); + } + + document.getElementById('btnManageShopGroups').onclick = openShopManageGroupModal; + document.getElementById('btnManageShopGroupsFromEdit').onclick = openShopManageGroupModal; + document.getElementById('btnSearchShopManage').onclick = function () { + loadShopManage(1); + }; + document.getElementById('shopManageFilterShopName').addEventListener('keydown', function (e) { + if (e.key === 'Enter') { + e.preventDefault(); + loadShopManage(1); + } + }); + document.getElementById('btnCloseShopManageGroupModal').onclick = function () { + document.getElementById('shopManageGroupModal').classList.remove('show'); + }; + document.getElementById('btnCancelShopManageGroupEdit').onclick = function () { + document.getElementById('shopManageGroupEditId').value = ''; + document.getElementById('shopManageGroupInput').value = ''; + }; + document.getElementById('btnSaveShopManageGroup').onclick = function () { + var editId = (document.getElementById('shopManageGroupEditId').value || '').trim(); + var groupName = (document.getElementById('shopManageGroupInput').value || '').trim(); + var msgEl = document.getElementById('msgShopManageGroup'); + msgEl.textContent = ''; + msgEl.className = 'msg'; + if (!groupName) { + msgEl.textContent = '请输入分组名称'; + msgEl.className = 'msg err'; + return; + } + var method = editId ? 'PUT' : 'POST'; + var url = editId ? ('/api/admin/shop-manage-group/' + editId) : '/api/admin/shop-manage-group'; + fetch(url, { + method: method, + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ group_name: groupName }) + }) + .then(function (r) { return r.json(); }) + .then(function (res) { + if (!res.success) { + msgEl.textContent = res.error || '保存失败'; + msgEl.className = 'msg err'; + return; + } + document.getElementById('shopManageGroupEditId').value = ''; + document.getElementById('shopManageGroupInput').value = ''; + msgEl.textContent = res.msg || '保存成功'; + msgEl.className = 'msg ok'; + loadShopManageGroups().then(function () { + renderShopManageGroupRows(); + loadShopManage(shopManagePage); + loadSkipPriceAsin(skipPriceAsinPage); + }); + }) + .catch(function () { + msgEl.textContent = '请求失败'; + msgEl.className = 'msg err'; + }); + }; + + function refreshShopGroupSelects(selectedCreateId, selectedEditId) { + var createSel = document.getElementById('shopManageGroupSelect'); + var editSel = document.getElementById('editShopManageGroupSelect'); + var filterSel = document.getElementById('shopManageFilterGroupId'); + var skipCreateSel = document.getElementById('skipPriceAsinGroupSelect'); + var skipFilterSel = document.getElementById('skipPriceAsinFilterGroupId'); + var chooseSkipShopSel = document.getElementById('chooseSkipPriceAsinShopGroupId'); + var selectedFilterId = filterSel ? filterSel.value : ''; + var selectedSkipCreateId = skipCreateSel ? skipCreateSel.value : ''; + var selectedSkipFilterId = skipFilterSel ? skipFilterSel.value : ''; + var selectedChooseSkipShopId = chooseSkipShopSel ? chooseSkipShopSel.value : ''; + var createOpts = ['']; + var filterOpts = ['']; + shopManageGroups.forEach(function (g) { + var option = ''; + createOpts.push(option); + filterOpts.push(option); + }); + createSel.innerHTML = createOpts.join(''); + editSel.innerHTML = createOpts.join(''); + if (skipCreateSel) skipCreateSel.innerHTML = createOpts.join(''); + if (filterSel) filterSel.innerHTML = filterOpts.join(''); + if (skipFilterSel) skipFilterSel.innerHTML = filterOpts.join(''); + if (chooseSkipShopSel) chooseSkipShopSel.innerHTML = filterOpts.join(''); + if (selectedCreateId != null) createSel.value = String(selectedCreateId); + if (selectedEditId != null) editSel.value = String(selectedEditId); + if (filterSel && selectedFilterId) filterSel.value = selectedFilterId; + if (skipCreateSel && selectedSkipCreateId) skipCreateSel.value = selectedSkipCreateId; + if (skipFilterSel && selectedSkipFilterId) skipFilterSel.value = selectedSkipFilterId; + if (chooseSkipShopSel && selectedChooseSkipShopId) chooseSkipShopSel.value = selectedChooseSkipShopId; + } + + function loadShopManageGroups(selectedCreateId, selectedEditId) { + return fetch('/api/admin/shop-manage-groups') + .then(function (r) { return r.json(); }) + .then(function (res) { + if (!res.success) throw new Error(res.error || '加载分组失败'); + shopManageGroups = res.items || []; + refreshShopGroupSelects(selectedCreateId, selectedEditId); + return shopManageGroups; + }) + .catch(function () { + shopManageGroups = []; + refreshShopGroupSelects(); + return []; + }); + } + + function resetShopManageGroupForm() { + document.getElementById('shopManageGroupEditId').value = ''; + document.getElementById('shopManageGroupInput').value = ''; + document.getElementById('msgShopManageGroup').textContent = ''; + document.getElementById('msgShopManageGroup').className = 'msg'; + refreshShopManageGroupUserSelect(''); + } + + function openShopManageGroupModal() { + resetShopManageGroupForm(); + loadUserOptions() + .then(function () { return loadShopManageGroups(); }) + .then(function () { + renderShopManageGroupRows(); + document.getElementById('shopManageGroupModal').classList.add('show'); + }); + } + + function renderShopManageGroupRows() { + var tbody = document.getElementById('shopManageGroupListBody'); + if (!shopManageGroups.length) { + tbody.innerHTML = '暂无分组'; + return; + } + tbody.innerHTML = shopManageGroups.map(function (item, index) { + return '' + (index + 1) + '' + (item.group_name || '') + '' + (item.username || '') + '' + (item.created_at || '') + '' + (item.updated_at || '') + '' + + ' ' + + '' + + ''; + }).join(''); + + document.querySelectorAll('[data-shop-group-edit]').forEach(function (btn) { + btn.onclick = function () { + document.getElementById('shopManageGroupEditId').value = btn.dataset.shopGroupEdit; + refreshShopManageGroupUserSelect(btn.dataset.shopGroupUserId || ''); + }; + }); + + document.querySelectorAll('[data-shop-group-delete]').forEach(function (btn) { + btn.onclick = function () { + var gid = btn.dataset.shopGroupDelete; + var gname = (btn.dataset.shopGroupName || '').replace(/"/g, '"'); + if (!confirm('确定删除分组“' + gname + '”吗?')) return; + fetch('/api/admin/shop-manage-group/' + gid, { method: 'DELETE' }) + .then(function (r) { return r.json(); }) + .then(function (res) { + if (!res.success) { + alert(res.error || '删除失败'); + return; + } + loadShopManageGroups().then(function () { + renderShopManageGroupRows(); + loadShopManage(shopManagePage); + loadSkipPriceAsin(skipPriceAsinPage); + }); + }); + }; + }); + } + + document.getElementById('shopManageGroupUserSelect').onchange = syncShopManageGroupNameWithUser; + document.getElementById('btnManageShopGroups').onclick = openShopManageGroupModal; + document.getElementById('btnManageShopGroupsFromEdit').onclick = openShopManageGroupModal; + document.getElementById('btnManageSkipPriceAsinGroups').onclick = openShopManageGroupModal; + document.getElementById('btnCloseShopManageGroupModal').onclick = function () { + document.getElementById('shopManageGroupModal').classList.remove('show'); + }; + document.getElementById('btnCancelShopManageGroupEdit').onclick = resetShopManageGroupForm; + document.getElementById('btnSaveShopManageGroup').onclick = function () { + var editId = (document.getElementById('shopManageGroupEditId').value || '').trim(); + var groupName = (document.getElementById('shopManageGroupInput').value || '').trim(); + var userId = (document.getElementById('shopManageGroupUserSelect').value || '').trim(); + var msgEl = document.getElementById('msgShopManageGroup'); + msgEl.textContent = ''; + msgEl.className = 'msg'; + if (!userId || !groupName) { + msgEl.textContent = '请选择关联用户'; + msgEl.className = 'msg err'; + return; + } + var method = editId ? 'PUT' : 'POST'; + var url = editId ? ('/api/admin/shop-manage-group/' + editId) : '/api/admin/shop-manage-group'; + fetch(url, { + method: method, + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ group_name: groupName, user_id: Number(userId) }) + }) + .then(function (r) { return r.json(); }) + .then(function (res) { + if (!res.success) { + msgEl.textContent = res.error || '保存失败'; + msgEl.className = 'msg err'; + return; + } + resetShopManageGroupForm(); + msgEl.textContent = res.msg || '保存成功'; + msgEl.className = 'msg ok'; + loadShopManageGroups().then(function () { + renderShopManageGroupRows(); + loadShopManage(shopManagePage); + loadSkipPriceAsin(skipPriceAsinPage); + }); + }) + .catch(function () { + msgEl.textContent = '请求失败'; + msgEl.className = 'msg err'; + }); + }; + + document.getElementById('btnCreateShopManage').onclick = function () { + var groupId = (document.getElementById('shopManageGroupSelect').value || '').trim(); + var shopName = (document.getElementById('shopManageShopName').value || '').trim(); + var mallName = (document.getElementById('shopManageMallName').value || '').trim(); + var account = (document.getElementById('shopManageAccount').value || '').trim(); + var password = (document.getElementById('shopManagePassword').value || '').trim(); + var msgEl = document.getElementById('msgShopManage'); + msgEl.textContent = ''; + msgEl.className = 'msg'; + if (!groupId || !shopName || !mallName || !account || !password) { + msgEl.textContent = '请完整填写分组、店铺名、店铺商城名、账号、密码'; + msgEl.classList.add('err'); + return; + } + fetch('/api/admin/shop-manage', { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ group_id: Number(groupId), shop_name: shopName, mall_name: mallName, account: account, password: password }) + }) + .then(function (r) { return r.json(); }) + .then(function (res) { + if (res.success) { + document.getElementById('shopManageGroupSelect').value = ''; + document.getElementById('shopManageShopName').value = ''; + document.getElementById('shopManageMallName').value = ''; + document.getElementById('shopManageAccount').value = ''; + document.getElementById('shopManagePassword').value = ''; + msgEl.textContent = res.msg || '创建成功'; + msgEl.className = 'msg ok'; + loadShopManage(1); + } else { + msgEl.textContent = res.error || '创建失败'; + msgEl.className = 'msg err'; + } + }) + .catch(function () { + msgEl.textContent = '请求失败'; + msgEl.className = 'msg err'; + }); + }; + + document.getElementById('btnSaveShopManage').onclick = function () { + var itemId = document.getElementById('editShopManageId').value; + var groupId = (document.getElementById('editShopManageGroupSelect').value || '').trim(); + var shopName = (document.getElementById('editShopManageShopName').value || '').trim(); + var mallName = (document.getElementById('editShopManageMallName').value || '').trim(); + var account = (document.getElementById('editShopManageAccount').value || '').trim(); + var password = (document.getElementById('editShopManagePassword').value || '').trim(); + var msgEl = document.getElementById('msgEditShopManage'); + msgEl.textContent = ''; + msgEl.className = 'msg'; + if (!groupId || !shopName || !mallName || !account || !password) { + msgEl.textContent = '请完整填写分组、店铺名、店铺商城名、账号、密码'; + msgEl.classList.add('err'); + return; + } + fetch('/api/admin/shop-manage/' + itemId, { + method: 'PUT', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ group_id: Number(groupId), shop_name: shopName, mall_name: mallName, account: account, password: password }) + }) + .then(function (r) { return r.json(); }) + .then(function (res) { + if (res.success) { + document.getElementById('editShopManageModal').classList.remove('show'); + loadShopManage(shopManagePage); + } else { + msgEl.textContent = res.error || '保存失败'; + msgEl.classList.add('err'); + } + }) + .catch(function () { + msgEl.textContent = '请求失败'; + msgEl.classList.add('err'); + }); + }; + document.getElementById('btnCloseEditShopManage').onclick = function () { + document.getElementById('editShopManageModal').classList.remove('show'); + }; + + // ========== 版本管理 ========== + // ========== 跳过跟价 ASIN ========== + var skipPriceAsinPage = 1, skipPriceAsinPageSize = 15; + var chooseSkipPriceAsinShopPage = 1, chooseSkipPriceAsinShopPageSize = 10; + var skipPriceCountryColumns = [ + { code: 'DE', field: 'asin_de', label: '德国' }, + { code: 'UK', field: 'asin_uk', label: '英国' }, + { code: 'FR', field: 'asin_fr', label: '法国' }, + { code: 'IT', field: 'asin_it', label: '意大利' }, + { code: 'ES', field: 'asin_es', label: '西班牙' } + ]; + function buildChooseSkipPriceAsinShopQuery(page) { + var query = 'page=' + (page || 1) + '&page_size=' + chooseSkipPriceAsinShopPageSize; + var groupId = (document.getElementById('chooseSkipPriceAsinShopGroupId').value || '').trim(); + var shopName = (document.getElementById('chooseSkipPriceAsinShopKeyword').value || '').trim(); + if (groupId) query += '&group_id=' + encodeURIComponent(groupId); + if (shopName) query += '&shop_name=' + encodeURIComponent(shopName); + return query; + } + function loadChooseSkipPriceAsinShops(page) { + chooseSkipPriceAsinShopPage = page || 1; + fetch('/api/admin/shop-manages?' + buildChooseSkipPriceAsinShopQuery(chooseSkipPriceAsinShopPage)) + .then(function (r) { return r.json(); }) + .then(function (res) { + var tbody = document.getElementById('chooseSkipPriceAsinShopListBody'); + if (!res.success) { + tbody.innerHTML = '加载失败: ' + (res.error || '') + ''; + return; + } + var items = res.items || []; + if (!items.length) { + tbody.innerHTML = '暂无店铺'; + } else { + tbody.innerHTML = items.map(function (item, index) { + var rowNo = (chooseSkipPriceAsinShopPage - 1) * chooseSkipPriceAsinShopPageSize + index + 1; + return '' + rowNo + '' + (item.group_name || '') + '' + (item.shop_name || '') + '' + (item.mall_name || '') + '' + (item.account || '') + '' + + '' + + ''; + }).join(''); + } + renderPagination('chooseSkipPriceAsinShopPagination', res.total, res.page, res.page_size, loadChooseSkipPriceAsinShops); + bindChooseSkipPriceAsinShopActions(); + }) + .catch(function () { + document.getElementById('chooseSkipPriceAsinShopListBody').innerHTML = '请求失败'; + }); + } + function bindChooseSkipPriceAsinShopActions() { + document.querySelectorAll('[data-choose-skip-price-asin-shop]').forEach(function (btn) { + btn.onclick = function () { + document.getElementById('skipPriceAsinShopName').value = (btn.dataset.shopName || '').replace(/"/g, '"'); + if (!document.getElementById('skipPriceAsinGroupSelect').value && btn.dataset.groupId) { + document.getElementById('skipPriceAsinGroupSelect').value = btn.dataset.groupId; + } + document.getElementById('chooseSkipPriceAsinShopModal').classList.remove('show'); + }; + }); + } + function openChooseSkipPriceAsinShopModal() { + var currentGroupId = (document.getElementById('skipPriceAsinGroupSelect').value || '').trim(); + if (currentGroupId) { + document.getElementById('chooseSkipPriceAsinShopGroupId').value = currentGroupId; + } + document.getElementById('chooseSkipPriceAsinShopKeyword').value = (document.getElementById('skipPriceAsinShopName').value || '').trim(); + document.getElementById('chooseSkipPriceAsinShopModal').classList.add('show'); + loadChooseSkipPriceAsinShops(1); + } + function setupSkipPriceAsinShopPicker() { + var input = document.getElementById('skipPriceAsinShopName'); + var button = document.getElementById('btnChooseSkipPriceAsinShop'); + if (!input || !button) return; + var inputParent = input.parentNode; + if (inputParent && inputParent.style && inputParent.style.display === 'flex') return; + var legacyWrap = button.parentNode; + var wrapper = document.createElement('div'); + wrapper.style.display = 'flex'; + wrapper.style.gap = '8px'; + wrapper.style.alignItems = 'center'; + input.parentNode.insertBefore(wrapper, input); + wrapper.appendChild(input); + wrapper.appendChild(button); + button.style.whiteSpace = 'nowrap'; + button.style.marginTop = '0'; + if (legacyWrap && legacyWrap !== wrapper) { + legacyWrap.style.display = 'none'; + } + } + function getSelectedSkipPriceCountries() { + return Array.from(document.getElementById('skipPriceAsinCountries').selectedOptions).map(function (option) { + return option.value; + }); + } + function renderSkipPriceAsinInputs() { + var container = document.getElementById('skipPriceAsinInputs'); + var selectedCountries = getSelectedSkipPriceCountries(); + var existingValues = {}; + container.querySelectorAll('[data-skip-price-country-input]').forEach(function (input) { + existingValues[input.getAttribute('data-skip-price-country-input')] = input.value; + }); + if (!selectedCountries.length) { + container.innerHTML = '
请选择国家后输入 ASIN
'; + return; + } + container.innerHTML = selectedCountries.map(function (countryCode) { + var country = skipPriceCountryColumns.find(function (item) { return item.code === countryCode; }); + var label = country ? country.label : countryCode; + var value = existingValues[countryCode] || ''; + return '
' + + '' + label + '' + + '' + + '
'; + }).join(''); + } + function collectSkipPriceAsinMappings(countries) { + var mappings = {}; + for (var i = 0; i < countries.length; i++) { + var countryCode = countries[i]; + var input = document.querySelector('[data-skip-price-country-input="' + countryCode + '"]'); + var asin = input ? (input.value || '').trim().toUpperCase() : ''; + if (!asin) { + var country = skipPriceCountryColumns.find(function (item) { return item.code === countryCode; }); + var label = country ? country.label : countryCode; + throw new Error(label + ' ASIN 不能为空'); + } + mappings[countryCode] = asin; + } + return mappings; + } + function buildSkipPriceAsinQuery(page) { + var query = 'page=' + (page || 1) + '&page_size=' + skipPriceAsinPageSize; + var groupId = (document.getElementById('skipPriceAsinFilterGroupId').value || '').trim(); + var shopName = (document.getElementById('skipPriceAsinFilterShopName').value || '').trim(); + var asin = (document.getElementById('skipPriceAsinFilterAsin').value || '').trim(); + if (groupId) query += '&group_id=' + encodeURIComponent(groupId); + if (shopName) query += '&shop_name=' + encodeURIComponent(shopName); + if (asin) query += '&asin=' + encodeURIComponent(asin); + return query; + } + function renderSkipPriceAsinCell(item, country) { + var value = item[country.field] || ''; + if (!value) return '-'; + return '
' + + '' + value + '' + + '' + + '' + + '
'; + } + 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, '"'); + var countryCode = btn.dataset.country || ''; + if (!confirm('确定删除店铺“' + shopName + '”在 ' + countryCode + ' 的 ASIN 吗?')) return; + fetch('/api/admin/skip-price-asin/' + btn.dataset.skipPriceAsinDelete + '/country/' + countryCode, { + method: 'DELETE' + }) + .then(function (r) { return r.json(); }) + .then(function (res) { + if (res.success) loadSkipPriceAsin(skipPriceAsinPage); + else alert(res.error || '删除失败'); + }); + }; + }); + } + function loadSkipPriceAsin(page) { + skipPriceAsinPage = page || 1; + fetch('/api/admin/skip-price-asins?' + buildSkipPriceAsinQuery(skipPriceAsinPage)) + .then(function (r) { return r.json(); }) + .then(function (res) { + var tbody = document.getElementById('skipPriceAsinListBody'); + if (!res.success) { + tbody.innerHTML = '加载失败: ' + (res.error || '') + ''; + return; + } + var items = res.items || []; + if (items.length === 0) { + tbody.innerHTML = '暂无数据'; + } else { + tbody.innerHTML = items.map(function (item, index) { + var rowNo = (skipPriceAsinPage - 1) * skipPriceAsinPageSize + index + 1; + return '' + rowNo + '' + (item.group_name || '') + '' + (item.shop_name || '') + '' + + skipPriceCountryColumns.map(function (country) { + return '' + renderSkipPriceAsinCell(item, country) + ''; + }).join('') + + ''; + }).join(''); + } + renderPagination('skipPriceAsinPagination', res.total, res.page, res.page_size, loadSkipPriceAsin); + bindSkipPriceAsinActions(); + }) + .catch(function () { + document.getElementById('skipPriceAsinListBody').innerHTML = '请求失败'; + }); + } + document.getElementById('btnManageSkipPriceAsinGroups').onclick = openShopManageGroupModal; + document.getElementById('btnChooseSkipPriceAsinShop').onclick = openChooseSkipPriceAsinShopModal; + document.getElementById('btnSearchChooseSkipPriceAsinShop').onclick = function () { + loadChooseSkipPriceAsinShops(1); + }; + document.getElementById('chooseSkipPriceAsinShopGroupId').onchange = function () { + loadChooseSkipPriceAsinShops(1); + }; + document.getElementById('chooseSkipPriceAsinShopKeyword').addEventListener('keydown', function (e) { + if (e.key === 'Enter') { + e.preventDefault(); + loadChooseSkipPriceAsinShops(1); + } + }); + document.getElementById('btnCloseChooseSkipPriceAsinShopModal').onclick = function () { + document.getElementById('chooseSkipPriceAsinShopModal').classList.remove('show'); + }; + document.getElementById('skipPriceAsinCountries').addEventListener('change', renderSkipPriceAsinInputs); + document.getElementById('btnSearchSkipPriceAsin').onclick = function () { + loadSkipPriceAsin(1); + }; + document.getElementById('skipPriceAsinFilterShopName').addEventListener('keydown', function (e) { + if (e.key === 'Enter') { + e.preventDefault(); + loadSkipPriceAsin(1); + } + }); + document.getElementById('skipPriceAsinFilterAsin').addEventListener('keydown', function (e) { + if (e.key === 'Enter') { + e.preventDefault(); + loadSkipPriceAsin(1); + } + }); + document.getElementById('btnCreateSkipPriceAsin').onclick = function () { + var groupId = (document.getElementById('skipPriceAsinGroupSelect').value || '').trim(); + var shopName = (document.getElementById('skipPriceAsinShopName').value || '').trim(); + var countries = getSelectedSkipPriceCountries(); + var msgEl = document.getElementById('msgSkipPriceAsin'); + msgEl.textContent = ''; + msgEl.className = 'msg'; + if (!groupId || !shopName || !countries.length) { + msgEl.textContent = '请完整填写分组、店铺名、国家和 ASIN'; + msgEl.className = 'msg err'; + return; + } + var asinMappings = {}; + try { + asinMappings = collectSkipPriceAsinMappings(countries); + } catch (err) { + msgEl.textContent = err.message || '请输入 ASIN'; + msgEl.className = 'msg err'; + return; + } + var fallbackAsin = ''; + Object.keys(asinMappings).some(function (countryCode) { + fallbackAsin = asinMappings[countryCode] || ''; + return !!fallbackAsin; + }); + fetch('/api/admin/skip-price-asin', { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ + group_id: Number(groupId), + shop_name: shopName, + countries: countries, + asin: fallbackAsin, + asin_mappings: asinMappings + }) + }) + .then(function (r) { return r.json(); }) + .then(function (res) { + if (!res.success) { + msgEl.textContent = res.error || '保存失败'; + msgEl.className = 'msg err'; + return; + } + document.getElementById('skipPriceAsinGroupSelect').value = ''; + document.getElementById('skipPriceAsinShopName').value = ''; + Array.from(document.getElementById('skipPriceAsinCountries').options).forEach(function (option) { + option.selected = false; + }); + renderSkipPriceAsinInputs(); + msgEl.textContent = res.msg || '保存成功'; + msgEl.className = 'msg ok'; + loadSkipPriceAsin(1); + }) + .catch(function () { + msgEl.textContent = '请求失败'; + msgEl.className = 'msg err'; + }); + }; + setupSkipPriceAsinShopPicker(); + renderSkipPriceAsinInputs(); + function loadVersions() { + fetch('/api/admin/versions') + .then(function (r) { return r.json(); }) + .then(function (res) { + var tbody = document.getElementById('versionListBody'); + if (!res.success) { + tbody.innerHTML = '加载失败: ' + (res.error || '') + ''; + return; + } + var items = res.items || []; + if (items.length === 0) { + tbody.innerHTML = '暂无版本记录'; + } else { + tbody.innerHTML = items.map(function (v) { + var url = (v.file_url || '').replace(/"/g, '"'); + return '' + (v.version || '') + '
' + url + '' + (v.created_at || '') + '下载'; + }).join(''); + } + }) + .catch(function () { + document.getElementById('versionListBody').innerHTML = '请求失败'; + }); + } + document.getElementById('btnUploadVersion').onclick = function () { + var version = (document.getElementById('versionNumber').value || '').trim(); + var fileInput = document.getElementById('versionZip'); + var msgEl = document.getElementById('msgVersion'); + msgEl.textContent = ''; + msgEl.className = 'msg'; + if (!version) { + msgEl.textContent = '请填写版本号'; + msgEl.classList.add('err'); + return; + } + if (!fileInput.files || fileInput.files.length === 0) { + msgEl.textContent = '请选择 zip 压缩包'; + msgEl.classList.add('err'); + return; + } + var file = fileInput.files[0]; + if (!(file.name || '').toLowerCase().endsWith('.zip')) { + msgEl.textContent = '仅支持 .zip 格式'; + msgEl.classList.add('err'); + return; + } + var formData = new FormData(); + formData.append('version', version); + formData.append('file', file); + msgEl.textContent = '上传中...'; + msgEl.classList.remove('err', 'ok'); + fetch('/api/admin/version', { + method: 'POST', + body: formData + }) + .then(function (r) { return r.json(); }) + .then(function (res) { + if (res.success) { + msgEl.textContent = '发布成功。版本:' + res.version + ',链接:' + (res.file_url || ''); + msgEl.classList.add('ok'); + document.getElementById('versionNumber').value = ''; + fileInput.value = ''; + loadVersions(); + } else { + msgEl.textContent = res.error || '上传失败'; + msgEl.classList.add('err'); + } + }) + .catch(function () { + msgEl.textContent = '请求失败'; + msgEl.classList.add('err'); + }); + }; + + // ========== 栏目权限配置 ========== + function loadColumns() { + fetch('/api/admin/columns') + .then(function (r) { return r.json(); }) + .then(function (res) { + var tbody = document.getElementById('columnListBody'); + if (!res.success) { + tbody.innerHTML = '加载失败: ' + (res.error || '') + ''; + return; + } + allColumnsList = res.items || []; + if (allColumnsList.length === 0) { + tbody.innerHTML = '暂无菜单,请在上方新增'; + } else { + tbody.innerHTML = allColumnsList.map(function (c) { + return '' + c.id + '' + (c.name || '') + '' + (c.column_key || '') + '' + (c.created_at || '') + '' + + ' ' + + ''; + }).join(''); + } + bindColumnActions(); + }) + .catch(function () { + document.getElementById('columnListBody').innerHTML = '请求失败'; + }); + } + function bindColumnActions() { + document.querySelectorAll('[data-column-edit]').forEach(function (btn) { + btn.onclick = function () { + document.getElementById('editColumnId').value = btn.dataset.columnEdit || ''; + document.getElementById('editColumnName').value = (btn.dataset.name || '').replace(/"/g, '"'); + document.getElementById('editColumnKey').value = (btn.dataset.key || '').replace(/"/g, '"'); + document.getElementById('msgEditColumn').textContent = ''; + document.getElementById('editColumnModal').classList.add('show'); + }; + }); + document.querySelectorAll('[data-column-delete]').forEach(function (btn) { + btn.onclick = function () { + if (!confirm('确定删除菜单“' + (btn.dataset.name || '').replace(/"/g, '"') + '”吗?')) return; + fetch('/api/admin/column/' + btn.dataset.columnDelete, { method: 'DELETE' }) + .then(function (r) { return r.json(); }) + .then(function (res) { + if (res.success) { loadColumns(); loadColumnsForPermission(); loadAdminMenus(getActiveAdminTabName()); } + else { alert(res.error || '删除失败'); } + }); + }; + }); + } + document.getElementById('btnAddColumn').onclick = function () { + var name = (document.getElementById('columnName').value || '').trim(); + var key = (document.getElementById('columnKey').value || '').trim(); + var msgEl = document.getElementById('msgColumn'); + msgEl.textContent = ''; + msgEl.className = 'msg'; + if (!name) { msgEl.textContent = '请填写菜单名称'; msgEl.classList.add('err'); return; } + if (!key) { + msgEl.textContent = '请填写栏目标识'; msgEl.classList.add('err'); return; + } + fetch('/api/admin/column', { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ name: name, column_key: key }) + }) + .then(function (r) { return r.json(); }) + .then(function (res) { + if (res.success) { + msgEl.textContent = res.msg || '新增成功'; + msgEl.classList.add('ok'); + document.getElementById('columnName').value = ''; + document.getElementById('columnKey').value = ''; + loadColumns(); + loadColumnsForPermission(); + loadAdminMenus(getActiveAdminTabName()); + } else { + msgEl.textContent = res.error || '新增失败'; + msgEl.classList.add('err'); + } + }) + .catch(function () { msgEl.textContent = '请求失败'; msgEl.classList.add('err'); }); + }; + document.getElementById('btnSaveColumn').onclick = function () { + var cid = document.getElementById('editColumnId').value; + var name = (document.getElementById('editColumnName').value || '').trim(); + var key = (document.getElementById('editColumnKey').value || '').trim(); + var msgEl = document.getElementById('msgEditColumn'); + msgEl.textContent = ''; + msgEl.className = 'msg'; + if (!name) { msgEl.textContent = '请填写菜单名称'; msgEl.classList.add('err'); return; } + if (!key) { + msgEl.textContent = '请填写栏目标识'; msgEl.classList.add('err'); return; + } + fetch('/api/admin/column/' + cid, { + method: 'PUT', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ name: name, column_key: key }) + }) + .then(function (r) { return r.json(); }) + .then(function (res) { + if (res.success) { + document.getElementById('editColumnModal').classList.remove('show'); + loadColumns(); + loadColumnsForPermission(); + loadAdminMenus(getActiveAdminTabName()); + } else { + msgEl.textContent = res.error || '保存失败'; + msgEl.classList.add('err'); + } + }); + }; + document.getElementById('btnCloseEditColumn').onclick = function () { + document.getElementById('editColumnModal').classList.remove('show'); + }; + + function loadColumns() { + fetch('/api/admin/columns') + .then(function (r) { return r.json(); }) + .then(function (res) { + var tbody = document.getElementById('columnListBody'); + if (!res.success) { + tbody.innerHTML = '加载失败: ' + (res.error || '') + ''; + return; + } + allColumnsList = res.items || []; + if (allColumnsList.length === 0) { + tbody.innerHTML = '暂无菜单,请先在上方新增'; + } else { + tbody.innerHTML = allColumnsList.map(function (c, index) { + var moveButtons = + ' ' + + ' '; + return '' + c.id + '' + (c.name || '') + '' + (c.column_key || '') + '' + ((c.menu_type || '') === 'admin' ? '后台(admin)' : '软件(app)') + '' + (c.sort_order != null ? c.sort_order : 0) + '' + (c.route_path || '') + '' + (c.created_at || '') + '' + + moveButtons + + ' ' + + ''; + }).join(''); + } + bindColumnActions(); + }) + .catch(function () { + document.getElementById('columnListBody').innerHTML = '请求失败'; + }); + } + function bindColumnActions() { + document.querySelectorAll('[data-column-move-up]').forEach(function (btn) { + btn.onclick = function () { + if (btn.disabled) return; + moveColumnItem(btn.dataset.columnMoveUp, -1, btn); + }; + }); + document.querySelectorAll('[data-column-move-down]').forEach(function (btn) { + btn.onclick = function () { + if (btn.disabled) return; + moveColumnItem(btn.dataset.columnMoveDown, 1, btn); + }; + }); + document.querySelectorAll('[data-column-edit]').forEach(function (btn) { + btn.onclick = function () { + document.getElementById('editColumnId').value = btn.dataset.columnEdit || ''; + document.getElementById('editColumnName').value = (btn.dataset.name || '').replace(/"/g, '"'); + document.getElementById('editColumnKey').value = (btn.dataset.key || '').replace(/"/g, '"'); + document.getElementById('editColumnRoutePath').value = (btn.dataset.route || '').replace(/"/g, '"'); + document.getElementById('editColumnMenuType').value = ((btn.dataset.menuType || 'app').replace(/"/g, '"') || 'app'); + document.getElementById('editColumnSortOrder').value = ((btn.dataset.sortOrder || '0').replace(/"/g, '"') || '0'); + document.getElementById('msgEditColumn').textContent = ''; + document.getElementById('editColumnModal').classList.add('show'); + }; + }); + document.querySelectorAll('[data-column-delete]').forEach(function (btn) { + btn.onclick = function () { + if (!confirm('确定删除菜单“' + (btn.dataset.name || '').replace(/"/g, '"') + '”吗?')) return; + fetch('/api/admin/column/' + btn.dataset.columnDelete, { method: 'DELETE' }) + .then(function (r) { return r.json(); }) + .then(function (res) { + if (res.success) { loadColumns(); loadColumnsForPermission(); loadAdminMenus(getActiveAdminTabName()); } + else { alert(res.error || '删除失败'); } + }); + }; + }); + } + function updateColumnSortOrder(column, sortOrder) { + return fetch('/api/admin/column/' + column.id, { + method: 'PUT', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ + name: column.name || '', + column_key: column.column_key || '', + route_path: column.route_path || '', + menu_type: column.menu_type || 'app', + sort_order: sortOrder + }) + }).then(function (r) { return r.json(); }); + } + function moveColumnItem(columnId, delta, triggerBtn) { + var currentIndex = -1; + for (var i = 0; i < allColumnsList.length; i++) { + if (String(allColumnsList[i].id) === String(columnId)) { + currentIndex = i; + break; + } + } + if (currentIndex < 0) return; + var targetIndex = currentIndex + delta; + if (targetIndex < 0 || targetIndex >= allColumnsList.length) return; + var currentItem = allColumnsList[currentIndex]; + var targetItem = allColumnsList[targetIndex]; + if (!currentItem || !targetItem) return; + if (triggerBtn) triggerBtn.disabled = true; + var currentSort = currentItem.sort_order != null ? currentItem.sort_order : (currentIndex + 1); + var targetSort = targetItem.sort_order != null ? targetItem.sort_order : (targetIndex + 1); + Promise.all([ + updateColumnSortOrder(currentItem, targetSort), + updateColumnSortOrder(targetItem, currentSort) + ]).then(function (results) { + var failed = (results || []).find(function (res) { return !res || !res.success; }); + if (failed) { + alert((failed && failed.error) || '排序保存失败'); + return; + } + loadColumns(); + loadColumnsForPermission(); + loadAdminMenus(getActiveAdminTabName()); + }).catch(function () { + alert('排序保存失败'); + }).finally(function () { + if (triggerBtn) triggerBtn.disabled = false; + }); + } + document.getElementById('btnAddColumn').onclick = function () { + var name = (document.getElementById('columnName').value || '').trim(); + var key = (document.getElementById('columnKey').value || '').trim(); + var routePath = (document.getElementById('columnRoutePath').value || '').trim(); + var sortOrderValue = (document.getElementById('columnSortOrder').value || '').trim(); + var menuType = (document.getElementById('columnMenuType').value || 'admin').trim() || 'admin'; + var sortOrder = sortOrderValue === '' ? null : parseInt(sortOrderValue, 10); + var msgEl = document.getElementById('msgColumn'); + msgEl.textContent = ''; + msgEl.className = 'msg'; + if (!name) { msgEl.textContent = '请填写菜单名称'; msgEl.classList.add('err'); return; } + if (!key) { + msgEl.textContent = '请填写栏目标识'; msgEl.classList.add('err'); return; + } + if (!routePath) { + msgEl.textContent = '请填写菜单路由'; msgEl.classList.add('err'); return; + } + if (sortOrderValue !== '' && isNaN(sortOrder)) { + msgEl.textContent = '排序必须是数字'; msgEl.classList.add('err'); return; + } + fetch('/api/admin/column', { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ name: name, column_key: key, route_path: routePath, menu_type: menuType, sort_order: sortOrder }) + }) + .then(function (r) { return r.json(); }) + .then(function (res) { + if (res.success) { + msgEl.textContent = res.msg || '新增成功'; + msgEl.classList.add('ok'); + document.getElementById('columnName').value = ''; + document.getElementById('columnKey').value = ''; + document.getElementById('columnRoutePath').value = ''; + document.getElementById('columnSortOrder').value = ''; + document.getElementById('columnMenuType').value = 'admin'; + loadColumns(); + loadColumnsForPermission(); + loadAdminMenus(getActiveAdminTabName()); + } else { + msgEl.textContent = res.error || '新增失败'; + msgEl.classList.add('err'); + } + }) + .catch(function () { msgEl.textContent = '请求失败'; msgEl.classList.add('err'); }); + }; + document.getElementById('btnSaveColumn').onclick = function () { + var cid = document.getElementById('editColumnId').value; + var name = (document.getElementById('editColumnName').value || '').trim(); + var key = (document.getElementById('editColumnKey').value || '').trim(); + var routePath = (document.getElementById('editColumnRoutePath').value || '').trim(); + var sortOrderValue = (document.getElementById('editColumnSortOrder').value || '').trim(); + var menuType = (document.getElementById('editColumnMenuType').value || 'admin').trim() || 'admin'; + var sortOrder = sortOrderValue === '' ? null : parseInt(sortOrderValue, 10); + var msgEl = document.getElementById('msgEditColumn'); + msgEl.textContent = ''; + msgEl.className = 'msg'; + if (!name) { msgEl.textContent = '请填写菜单名称'; msgEl.classList.add('err'); return; } + if (!key) { + msgEl.textContent = '请填写栏目标识'; msgEl.classList.add('err'); return; + } + if (!routePath) { + msgEl.textContent = '请填写菜单路由'; msgEl.classList.add('err'); return; + } + if (sortOrderValue !== '' && isNaN(sortOrder)) { + msgEl.textContent = '排序必须是数字'; msgEl.classList.add('err'); return; + } + fetch('/api/admin/column/' + cid, { + method: 'PUT', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ name: name, column_key: key, route_path: routePath, menu_type: menuType, sort_order: sortOrder }) + }) + .then(function (r) { return r.json(); }) + .then(function (res) { + if (res.success) { + document.getElementById('editColumnModal').classList.remove('show'); + loadColumns(); + loadColumnsForPermission(); + loadAdminMenus(getActiveAdminTabName()); + } else { + msgEl.textContent = res.error || '保存失败'; + msgEl.classList.add('err'); + } + }); + }; + + // ========== 分页 ========== + function renderPagination(elId, total, page, pageSize, onPage) { + var el = document.getElementById(elId); + if (!el) return; + var totalPages = Math.max(1, Math.ceil(total / pageSize)); + el.innerHTML = '共' + total + ' 条' + + '' + + '第' + page + ' / ' + totalPages + ' 页' + + ''; + el.querySelectorAll('[data-p]').forEach(function (b) { + if (!b.disabled) b.onclick = function () { onPage(parseInt(b.dataset.p, 10)); }; + }); + } + + // 初始化 + function loadAdminCurrentUser() { + fetch('/api/admin/current-user') + .then(function (r) { + if (r.status === 404) { + return fetch('/api/auth/check') + .then(function (fallbackResp) { return fallbackResp.json(); }) + .then(function (fallbackRes) { + if (fallbackRes && fallbackRes.logged_in) { + return { + success: true, + item: { + username: '当前用户', + role: '' + } + }; + } + return { success: false }; + }); + } + return r.json(); + }) + .then(function (res) { + var nameEl = document.getElementById('adminCurrentUsername'); + var roleEl = document.getElementById('adminCurrentUserRole'); + if (!res.success) { + nameEl.textContent = '未登录'; + roleEl.style.display = 'none'; + return; + } + var item = res.item || {}; + nameEl.textContent = item.username || '管理员'; + if (item.role) { + roleEl.textContent = item.role === 'super_admin' ? '超级管理员' : '管理员'; + roleEl.style.display = 'inline-block'; + } else { + roleEl.style.display = 'none'; + } + }) + .catch(function () { + fetch('/api/auth/check') + .then(function (r) { return r.json(); }) + .then(function (res) { + document.getElementById('adminCurrentUsername').textContent = res && res.logged_in ? '当前用户' : '未登录'; + document.getElementById('adminCurrentUserRole').style.display = 'none'; + }) + .catch(function () { + document.getElementById('adminCurrentUsername').textContent = '当前用户'; + document.getElementById('adminCurrentUserRole').style.display = 'none'; + }); + }); + } + + document.getElementById('btnAdminLogout').onclick = function () { + fetch('/api/admin/logout', { + method: 'POST', + headers: { 'X-Requested-With': 'XMLHttpRequest' } + }) + .then(function (r) { + if (r.status === 404) { + window.location.href = '/logout'; + return null; + } + return r.json(); + }) + .then(function (res) { + if (!res) return; + if (!res.success) { + alert(res.error || '退出失败'); + return; + } + window.location.href = res.redirect || '/login'; + }) + .catch(function () { + window.location.href = '/logout'; + }); + }; + + loadAdminCurrentUser(); + loadUserOptions(); + loadColumnsForPermission(); + loadShopManageGroups(); + loadAdminMenus(); + }) (); + \ No newline at end of file diff --git a/backend/web_source/_tmp_prefix.js b/backend/web_source/_tmp_prefix.js new file mode 100644 index 0000000..c578064 --- /dev/null +++ b/backend/web_source/_tmp_prefix.js @@ -0,0 +1,2000 @@ + + (function () { + // Tab 鍒囨崲 + var adminTabsEl = document.getElementById('adminTabs'); + if (adminTabsEl) adminTabsEl.innerHTML = ''; + var activeAdminTabName = ''; + var ADMIN_PANEL_MAP = { + 'users': 'panel-users', + 'columns': 'panel-columns', + 'dedupe-total-data': 'panel-dedupe-total-data', + 'shop-keys': 'panel-shop-keys', + 'shop-manage': 'panel-shop-manage', + 'skip-price-asin': 'panel-skip-price-asin', + 'history': 'panel-history', + 'version': 'panel-version' + }; + function runTabLoader(tabName) { + if (tabName === 'users') { loadUsers(1); loadColumnsForPermission(); } + else if (tabName === 'columns') loadColumns(); + else if (tabName === 'dedupe-total-data') loadDedupeTotalData(1); + else if (tabName === 'shop-keys') loadShopKeys(1); + else if (tabName === 'shop-manage') loadShopManage(1); + else if (tabName === 'skip-price-asin') loadSkipPriceAsin(1); + else if (tabName === 'history') loadHistory(1); + else if (tabName === 'version') loadVersions(); + } + function hideAllAdminPanels() { + document.querySelectorAll('.tab-panel').forEach(function (panel) { + panel.classList.remove('active'); + panel.style.display = 'none'; + }); + } + function getActiveAdminTabName() { + return activeAdminTabName; + } + function activateAdminTab(tabName) { + var panelId = ADMIN_PANEL_MAP[tabName]; + var panel = panelId ? document.getElementById(panelId) : null; + if (!panel) return; + activeAdminTabName = tabName; + document.querySelectorAll('#adminTabs .tab').forEach(function (tab) { + tab.classList.toggle('active', tab.dataset.tab === tabName); + }); + hideAllAdminPanels(); + panel.style.display = ''; + panel.classList.add('active'); + runTabLoader(tabName); + } + function renderAdminTabs(items) { + var knownItems = (items || []).filter(function (item) { + return !!ADMIN_PANEL_MAP[item.route_path]; + }); + if (!knownItems.length) { + activeAdminTabName = ''; + if (adminTabsEl) adminTabsEl.innerHTML = '
鏆傛棤鍙敤鑿滃崟
'; + hideAllAdminPanels(); + return; + } + if (adminTabsEl) { + adminTabsEl.innerHTML = knownItems.map(function (item) { + return '
' + (item.name || item.route_path) + '
'; + }).join(''); + } + document.querySelectorAll('#adminTabs .tab').forEach(function (tab) { + tab.onclick = function () { + activateAdminTab(tab.dataset.tab); + }; + }); + var fallbackTab = activeAdminTabName && knownItems.some(function (item) { return item.route_path === activeAdminTabName; }) + ? activeAdminTabName + : knownItems[0].route_path; + activateAdminTab(fallbackTab); + } + function loadAdminMenus(preferredTab) { + if (preferredTab) activeAdminTabName = preferredTab; + fetch('/api/admin/current-user/menus') + .then(function (r) { return r.json(); }) + .then(function (res) { + if (!res.success) { + if (adminTabsEl) adminTabsEl.innerHTML = '
鑿滃崟鍔犺浇澶辫触
'; + hideAllAdminPanels(); + return; + } + renderAdminTabs(res.items || []); + }) + .catch(function () { + if (adminTabsEl) adminTabsEl.innerHTML = '
鑿滃崟鍔犺浇澶辫触
'; + hideAllAdminPanels(); + }); + } + hideAllAdminPanels(); + + // ========== 鐢ㄦ埛绠$悊 ========== + var userPage = 1, userPageSize = 15; + var currentUserId = null; + var currentUserRole = 'admin'; + var currentUserUsername = ''; + var adminsList = []; + function roleLabel(role) { + if (role === 'super_admin') return '瓒呯骇绠$悊鍛?; + if (role === 'admin') return '绠$悊鍛?; + return '鏅€氳处鍙?; + } + function buildUserListQuery(page) { + var q = 'page=' + (page || 1) + '&page_size=' + userPageSize; + var kw = (document.getElementById('searchUsername').value || '').trim(); + if (kw) q += '&username=' + encodeURIComponent(kw); + var cby = document.getElementById('filterCreatedBy').value; + if (cby) q += '&created_by_id=' + encodeURIComponent(cby); + return q; + } + function loadUsers(page) { + userPage = page || 1; + fetch('/api/admin/users?' + buildUserListQuery(userPage)) + .then(function (r) { return r.json(); }) + .then(function (res) { + var tbody = document.getElementById('userListBody'); + if (!res.success) { + tbody.innerHTML = '鍔犺浇澶辫触: ' + (res.error || '') + ''; + return; + } + currentUserId = res.current_user_id || null; + currentUserRole = res.current_user_role || 'admin'; + currentUserUsername = (res.current_user_username || '').trim(); + adminsList = res.admins || []; + var items = res.items || []; + if (items.length === 0) { + tbody.innerHTML = '鏆傛棤鐢ㄦ埛'; + } else { + tbody.innerHTML = items.map(function (u) { + return '' + u.id + '' + (u.username || '') + '' + + roleLabel(u.role || 'normal') + '' + (u.creator_username || '-') + '' + (u.created_at || '') + '' + + ' ' + + '' + + ''; + }).join(''); + } + renderPagination('userPagination', res.total, res.page, res.page_size, loadUsers); + bindUserActions(); + updateCreateFormByRole(); + updateUserFilterByRole(); + }) + .catch(function () { + document.getElementById('userListBody').innerHTML = '璇锋眰澶辫触'; + }); + } + function updateUserFilterByRole() { + var grp = document.getElementById('filterCreatedByGroup'); + var sel = document.getElementById('filterCreatedBy'); + if (currentUserRole === 'super_admin') { + grp.style.display = 'block'; + var cur = sel.value; + sel.innerHTML = ''; + adminsList.forEach(function (a) { + var opt = document.createElement('option'); + opt.value = a.id; + opt.textContent = a.username; + sel.appendChild(opt); + }); + sel.value = cur || ''; + } else { + grp.style.display = 'none'; + } + } + + function updateDedupeTotalDataAccess() { + var tab = document.querySelector('.tab[data-tab="dedupe-total-data"]'); + var panel = document.getElementById('panel-dedupe-total-data'); + var canUse = currentUserRole === 'super_admin' || (currentUserRole === 'admin' && currentUserUsername === ''); + if (tab) tab.style.display = canUse ? '' : 'none'; + if (panel) panel.style.display = canUse ? '' : 'none'; + if (!canUse && tab && tab.classList.contains('active')) { + tab.classList.remove('active'); + if (panel) panel.classList.remove('active'); + var usersTab = document.querySelector('.tab[data-tab="users"]'); + var usersPanel = document.getElementById('panel-users'); + if (usersTab) usersTab.classList.add('active'); + if (usersPanel) usersPanel.classList.add('active'); + } + } + function updateShopManageAccess() { + var tab = document.querySelector('.tab[data-tab="shop-manage"]'); + var panel = document.getElementById('panel-shop-manage'); + if (!tab || !panel) return; + var canUse = currentUserRole === 'super_admin' || + !!currentUserAdminPermissionKeys['admin_shop_manage'] || + !!currentUserAdminPermissionRoutes['shop-manage']; + tab.style.display = canUse ? '' : 'none'; + panel.style.display = canUse ? '' : 'none'; + if (!canUse && tab.classList.contains('active')) { + tab.classList.remove('active'); + panel.classList.remove('active'); + var usersTab = document.querySelector('.tab[data-tab="users"]'); + var usersPanel = document.getElementById('panel-users'); + if (usersTab) usersTab.classList.add('active'); + if (usersPanel) usersPanel.classList.add('active'); + } + } + function loadCurrentUserAdminPermissions() { + currentUserAdminPermissionKeys = {}; + currentUserAdminPermissionRoutes = {}; + if (!currentUserId || currentUserRole === 'super_admin') { + updateShopManageAccess(); + return; + } + fetch('/api/admin/user/' + currentUserId + '/column-permissions?menu_type=admin') + .then(function (r) { return r.json(); }) + .then(function (res) { + if (!res.success) { + updateShopManageAccess(); + return; + } + (res.items || []).forEach(function (item) { + var columnKey = (item.column_key || '').trim(); + var routePath = (item.route_path || '').trim(); + if (columnKey) currentUserAdminPermissionKeys[columnKey] = true; + if (routePath) currentUserAdminPermissionRoutes[routePath] = true; + }); + updateShopManageAccess(); + }) + .catch(function () { + updateShopManageAccess(); + }); + } + function applyTabAccess(tabName, canUse) { + var tab = document.querySelector('.tab[data-tab="' + tabName + '"]'); + var panel = document.getElementById('panel-' + tabName); + if (tab) tab.style.display = canUse ? '' : 'none'; + if (panel) panel.style.display = canUse ? '' : 'none'; + if (!canUse && tab && tab.classList.contains('active')) { + tab.classList.remove('active'); + if (panel) panel.classList.remove('active'); + var usersTab = document.querySelector('.tab[data-tab="users"]'); + var usersPanel = document.getElementById('panel-users'); + if (usersTab) usersTab.classList.add('active'); + if (usersPanel) usersPanel.classList.add('active'); + } + } + function hasAdminTabAccess(tabName) { + var config = ADMIN_TAB_ACCESS_CONFIG[tabName]; + if (!config) return true; + if (currentUserRole === 'super_admin') return true; + if (config.superAdminOnly) return false; + return !!currentUserAdminPermissionKeys[config.columnKey] || + !!currentUserAdminPermissionRoutes[config.routePath]; + } + function refreshAdminTabAccess() { + Object.keys(ADMIN_TAB_ACCESS_CONFIG).forEach(function (tabName) { + applyTabAccess(tabName, hasAdminTabAccess(tabName)); + }); + } + function loadCurrentUserAdminPermissions() { + currentUserAdminPermissionKeys = {}; + currentUserAdminPermissionRoutes = {}; + if (!currentUserId || currentUserRole === 'super_admin') { + refreshAdminTabAccess(); + return; + } + fetch('/api/admin/user/' + currentUserId + '/column-permissions?menu_type=admin') + .then(function (r) { return r.json(); }) + .then(function (res) { + if (!res.success) { + refreshAdminTabAccess(); + return; + } + (res.items || []).forEach(function (item) { + var columnKey = (item.column_key || '').trim(); + var routePath = (item.route_path || '').trim(); + if (columnKey) currentUserAdminPermissionKeys[columnKey] = true; + if (routePath) currentUserAdminPermissionRoutes[routePath] = true; + }); + refreshAdminTabAccess(); + }) + .catch(function () { + refreshAdminTabAccess(); + }); + } + var allColumnsList = []; + function loadColumnsForPermission() { + fetch('/api/admin/columns?menu_type=admin') + .then(function (r) { return r.json(); }) + .then(function (res) { + if (!res.success) return; + allColumnsList = res.items || []; + renderColumnPermissionWrap('createColumnPermissionWrap'); + renderColumnPermissionWrap('editColumnPermissionWrap'); + var selCreate = document.getElementById('createColumnPermissionWrap'); + var selEdit = document.getElementById('editColumnPermissionWrap'); + if (selCreate && !selCreate._colCardsBound) { + selCreate._colCardsBound = true; + selCreate.onchange = function () { renderColumnCards('createColumnPermissionWrap'); }; + } + if (selEdit && !selEdit._colCardsBound) { + selEdit._colCardsBound = true; + selEdit.onchange = function () { renderColumnCards('editColumnPermissionWrap'); }; + } + }) + .catch(function () { }); + } + var columnCardsContainerMap = { createColumnPermissionWrap: 'createColumnCards', editColumnPermissionWrap: 'editColumnCards' }; + function renderColumnCards(selectId) { + var sel = document.getElementById(selectId); + var cardsId = columnCardsContainerMap[selectId]; + var cardsEl = cardsId ? document.getElementById(cardsId) : null; + if (!sel || sel.tagName !== 'SELECT' || !cardsEl) return; + cardsEl.innerHTML = ''; + for (var i = 0; i < sel.options.length; i++) { + var opt = sel.options[i]; + if (opt.disabled || !opt.selected) continue; + var card = document.createElement('span'); + card.className = 'column-permission-card'; + card.textContent = opt.textContent; + var btn = document.createElement('button'); + btn.type = 'button'; + btn.className = 'col-card-remove'; + btn.setAttribute('aria-label', '绉婚櫎'); + btn.textContent = '脳'; + (function (option, seldId) { + btn.onclick = function () { + option.selected = false; + renderColumnCards(seldId); + }; + })(opt, selectId); + card.appendChild(btn); + cardsEl.appendChild(card); + } + } + function renderColumnPermissionWrap(wrapId) { + var sel = document.getElementById(wrapId); + if (!sel || sel.tagName !== 'SELECT') return; + sel.innerHTML = ''; + allColumnsList.forEach(function (c) { + var opt = document.createElement('option'); + opt.value = c.id; + opt.textContent = c.name + ' (' + c.column_key + ')'; + sel.appendChild(opt); + }); + if (allColumnsList.length === 0) { + var opt = document.createElement('option'); + opt.disabled = true; + opt.textContent = '鏆傛棤鑿滃崟锛岃鍏堝湪鈥滄爮鐩潈闄愰厤缃€濅腑鏂板鑿滃崟'; + sel.appendChild(opt); + } + renderColumnCards(wrapId); + } + function getSelectedColumnIds(wrapId) { + var sel = document.getElementById(wrapId); + if (!sel || sel.tagName !== 'SELECT') return []; + var ids = []; + for (var i = 0; i < sel.options.length; i++) { + if (sel.options[i].selected) { + var v = parseInt(sel.options[i].value, 10); + if (!isNaN(v)) ids.push(v); + } + } + return ids; + } + function setColumnPermissionCheckboxes(wrapId, columnIds) { + var sel = document.getElementById(wrapId); + if (!sel || sel.tagName !== 'SELECT') return; + var set = {}; + (columnIds || []).forEach(function (id) { set[id] = true; }); + for (var i = 0; i < sel.options.length; i++) { + var opt = sel.options[i]; + if (opt.disabled) continue; + opt.selected = set[parseInt(opt.value, 10)] || false; + } + renderColumnCards(wrapId); + } + function updateCreateFormByRole() { + var roleSel = document.getElementById('createRole'); + var optAdmin = document.getElementById('optAdmin'); + var formCreatedBy = document.getElementById('formGroupCreatedBy'); + var selCreatedBy = document.getElementById('createCreatedBy'); + if (currentUserRole === 'super_admin') { + if (optAdmin) optAdmin.style.display = ''; + formCreatedBy.style.display = (roleSel.value === 'normal') ? 'block' : 'none'; + selCreatedBy.innerHTML = ''; + (res.items || []).forEach(function (u) { + var opt = document.createElement('option'); + opt.value = u.id; + opt.textContent = u.username + ' (' + roleLabel(u.role || 'normal') + ')'; + sel.appendChild(opt); + }); + sel.value = cur || ''; + }); + } + var shopManageGroupUsers = []; + function refreshShopManageGroupUserSelect(selectedUserId) { + var sel = document.getElementById('shopManageGroupUserSelect'); + if (!sel) return; + var opts = ['']; + shopManageGroupUsers.forEach(function (u) { + opts.push(''); + }); + sel.innerHTML = opts.join(''); + if (selectedUserId != null && selectedUserId !== '') { + sel.value = String(selectedUserId); + } + syncShopManageGroupNameWithUser(); + } + function syncShopManageGroupNameWithUser() { + var sel = document.getElementById('shopManageGroupUserSelect'); + var input = document.getElementById('shopManageGroupInput'); + if (!sel || !input) return; + var selectedUser = null; + shopManageGroupUsers.some(function (u) { + if (String(u.id) === String(sel.value || '')) { + selectedUser = u; + return true; + } + return false; + }); + input.value = selectedUser ? (selectedUser.username || '') : ''; + } + function loadUserOptions() { + return fetch('/api/admin/users?page=1&page_size=999') + .then(function (r) { return r.json(); }) + .then(function (res) { + var items = res.items || []; + shopManageGroupUsers = items.map(function (u) { + return { id: u.id, username: u.username || '', role: u.role || 'normal' }; + }); + refreshShopManageGroupUserSelect(document.getElementById('shopManageGroupUserSelect') ? document.getElementById('shopManageGroupUserSelect').value : ''); + var sel = document.getElementById('filterUser'); + var cur = sel.value; + sel.innerHTML = ''; + items.forEach(function (u) { + var opt = document.createElement('option'); + opt.value = u.id; + opt.textContent = u.username + ' (' + roleLabel(u.role || 'normal') + ')'; + sel.appendChild(opt); + }); + sel.value = cur || ''; + }); + } + document.getElementById('btnFilterHistory').onclick = function () { loadHistory(1); }; + + // ========== 鏁版嵁鍘婚噸鎬绘暟鎹?========== + var dedupeTotalDataPage = 1, dedupeTotalDataPageSize = 15; + function buildDedupeTotalDataQuery(page) { + var q = 'page=' + (page || 1) + '&page_size=' + dedupeTotalDataPageSize; + var keyword = (document.getElementById('searchDedupeTotalData').value || '').trim(); + if (keyword) q += '&keyword=' + encodeURIComponent(keyword); + return q; + } + function loadDedupeTotalData(page) { + dedupeTotalDataPage = page || 1; + fetch('/api/admin/dedupe-total-data?' + buildDedupeTotalDataQuery(dedupeTotalDataPage)) + .then(function (r) { return r.json(); }) + .then(function (res) { + var tbody = document.getElementById('dedupeTotalDataListBody'); + if (!res.success) { + tbody.innerHTML = '鍔犺浇澶辫触: ' + (res.error || '') + ''; + return; + } + var items = res.items || []; + if (items.length === 0) { + tbody.innerHTML = '鏆傛棤鎬绘暟鎹?/td>'; + } else { + tbody.innerHTML = items.map(function (item) { + return '' + item.id + '' + (item.data_value || '') + '' + (item.created_at || '') + '' + + ' ' + + '' + + ''; + }).join(''); + } + renderPagination('dedupeTotalDataPagination', res.total, res.page, res.page_size, loadDedupeTotalData); + bindDedupeTotalDataActions(); + }) + .catch(function () { + document.getElementById('dedupeTotalDataListBody').innerHTML = '璇锋眰澶辫触'; + }); + } + function bindDedupeTotalDataActions() { + document.querySelectorAll('[data-dedupe-total-edit]').forEach(function (btn) { + btn.onclick = function () { + document.getElementById('editDedupeTotalDataId').value = btn.dataset.dedupeTotalEdit || ''; + document.getElementById('editDedupeTotalDataValue').value = (btn.dataset.value || '').replace(/"/g, '"'); + document.getElementById('msgEditDedupeTotalData').textContent = ''; + document.getElementById('msgEditDedupeTotalData').className = 'msg'; + document.getElementById('editDedupeTotalDataModal').classList.add('show'); + }; + }); + document.querySelectorAll('[data-dedupe-total-delete]').forEach(function (btn) { + btn.onclick = function () { + var value = (btn.dataset.value || '').replace(/"/g, '"'); + if (!confirm('纭畾鍒犻櫎鎬绘暟鎹€? + value + '鈥濆悧锛?)) return; + fetch('/api/admin/dedupe-total-data/' + btn.dataset.dedupeTotalDelete, { method: 'DELETE' }) + .then(function (r) { return r.json(); }) + .then(function (res) { + if (res.success) { loadDedupeTotalData(dedupeTotalDataPage); } + else { alert(res.error || '鍒犻櫎澶辫触'); } + }); + }; + }); + } + document.getElementById('btnSearchDedupeTotalData').onclick = function () { loadDedupeTotalData(1); }; + var dedupeImportPollTimer = null; + var dedupeDeleteImportPollTimer = null; + function stopDedupeImportProgress() { + if (dedupeImportPollTimer) { + clearInterval(dedupeImportPollTimer); + dedupeImportPollTimer = null; + } + } + function stopDedupeDeleteImportProgress() { + if (dedupeDeleteImportPollTimer) { + clearInterval(dedupeDeleteImportPollTimer); + dedupeDeleteImportPollTimer = null; + } + } + function setDedupeImportProgress(percent, text) { + var wrap = document.getElementById('dedupeTotalDataProgressWrap'); + var fill = document.getElementById('dedupeTotalDataProgressFill'); + var textEl = document.getElementById('dedupeTotalDataProgressText'); + wrap.style.display = 'block'; + fill.style.width = Math.max(0, Math.min(100, percent || 0)) + '%'; + textEl.textContent = text || ''; + } + function setDedupeDeleteImportProgress(percent, text) { + var wrap = document.getElementById('dedupeTotalDataDeleteProgressWrap'); + var fill = document.getElementById('dedupeTotalDataDeleteProgressFill'); + var textEl = document.getElementById('dedupeTotalDataDeleteProgressText'); + wrap.style.display = 'block'; + fill.style.width = Math.max(0, Math.min(100, percent || 0)) + '%'; + textEl.textContent = text || ''; + } + function pollDedupeImport(importId) { + stopDedupeImportProgress(); + function tick() { + fetch('/api/admin/dedupe-total-data/import/' + encodeURIComponent(importId)) + .then(function (r) { return r.json(); }) + .then(function (res) { + if (!res.success) { + stopDedupeImportProgress(); + document.getElementById('msgDedupeTotalData').textContent = res.error || '鏌ヨ瀵煎叆杩涘害澶辫触'; + document.getElementById('msgDedupeTotalData').className = 'msg err'; + return; + } + var progress = res.progress || {}; + var totalRows = progress.total_rows || 0; + var processedRows = progress.processed_rows || 0; + var percent = totalRows > 0 ? Math.round(processedRows * 100 / totalRows) : 0; + setDedupeImportProgress(percent, '澶勭悊涓細宸插鐞?' + processedRows + ' / ' + totalRows + '锛屾柊澧?' + (progress.inserted_count || 0) + '锛岃烦杩?' + (progress.skipped_count || 0)); + if (progress.status === 'success') { + stopDedupeImportProgress(); + document.getElementById('msgDedupeTotalData').textContent = '瀵煎叆鎴愬姛锛氭€昏鏁?' + (progress.total_rows || 0) + '锛孉SIN 鏁伴噺 ' + (progress.asin_count || 0) + '锛屾柊澧?' + (progress.inserted_count || 0) + '锛岃烦杩?' + (progress.skipped_count || 0); + document.getElementById('msgDedupeTotalData').className = 'msg ok'; + loadDedupeTotalData(1); + } else if (progress.status === 'failed') { + stopDedupeImportProgress(); + document.getElementById('msgDedupeTotalData').textContent = progress.error_message || '瀵煎叆澶辫触'; + document.getElementById('msgDedupeTotalData').className = 'msg err'; + } + }) + .catch(function () { + stopDedupeImportProgress(); + document.getElementById('msgDedupeTotalData').textContent = '鏌ヨ瀵煎叆杩涘害澶辫触'; + document.getElementById('msgDedupeTotalData').className = 'msg err'; + }); + } + tick(); + dedupeImportPollTimer = setInterval(tick, 1000); + } + function pollDedupeDeleteImport(importId) { + stopDedupeDeleteImportProgress(); + function tick() { + fetch('/api/admin/dedupe-total-data/delete-import/' + encodeURIComponent(importId)) + .then(function (r) { return r.json(); }) + .then(function (res) { + if (!res.success) { + stopDedupeDeleteImportProgress(); + document.getElementById('msgDeleteDedupeTotalData').textContent = res.error || '鏌ヨ鍒犻櫎杩涘害澶辫触'; + document.getElementById('msgDeleteDedupeTotalData').className = 'msg err'; + return; + } + var progress = res.progress || {}; + var totalRows = progress.total_rows || 0; + var processedRows = progress.processed_rows || 0; + var percent = totalRows > 0 ? Math.round(processedRows * 100 / totalRows) : 0; + setDedupeDeleteImportProgress(percent, '澶勭悊涓細宸插鐞?' + processedRows + ' / ' + totalRows + '锛屽垹闄?' + (progress.deleted_count || 0) + '锛岃烦杩?' + (progress.skipped_count || 0)); + if (progress.status === 'success') { + stopDedupeDeleteImportProgress(); + document.getElementById('msgDeleteDedupeTotalData').textContent = '鍒犻櫎鎴愬姛锛氭€昏鏁?' + (progress.total_rows || 0) + '锛孉SIN 鏁伴噺 ' + (progress.asin_count || 0) + '锛屽垹闄?' + (progress.deleted_count || 0) + '锛岃烦杩?' + (progress.skipped_count || 0); + document.getElementById('msgDeleteDedupeTotalData').className = 'msg ok'; + loadDedupeTotalData(1); + } else if (progress.status === 'failed') { + stopDedupeDeleteImportProgress(); + document.getElementById('msgDeleteDedupeTotalData').textContent = progress.error_message || '鍒犻櫎澶辫触'; + document.getElementById('msgDeleteDedupeTotalData').className = 'msg err'; + } + }) + .catch(function () { + stopDedupeDeleteImportProgress(); + document.getElementById('msgDeleteDedupeTotalData').textContent = '鏌ヨ鍒犻櫎杩涘害澶辫触'; + document.getElementById('msgDeleteDedupeTotalData').className = 'msg err'; + }); + } + tick(); + dedupeDeleteImportPollTimer = setInterval(tick, 1000); + } + + document.getElementById('btnAddDedupeTotalData').onclick = function () { + var fileInput = document.getElementById('dedupeTotalDataFile'); + var msgEl = document.getElementById('msgDedupeTotalData'); + msgEl.textContent = ''; + msgEl.className = 'msg'; + stopDedupeImportProgress(); + document.getElementById('dedupeTotalDataProgressWrap').style.display = 'none'; + if (!fileInput.files || fileInput.files.length === 0) { + msgEl.textContent = '璇烽€夋嫨 Excel 鏂囦欢'; + msgEl.classList.add('err'); + return; + } + var file = fileInput.files[0]; + var lowerName = (file.name || '').toLowerCase(); + if (!(lowerName.endsWith('.xlsx') || lowerName.endsWith('.xls'))) { + msgEl.textContent = '浠呮敮鎸?.xlsx 鎴?.xls 鏂囦欢'; + msgEl.classList.add('err'); + return; + } + var formData = new FormData(); + formData.append('file', file); + var xhr = new XMLHttpRequest(); + xhr.open('POST', '/api/admin/dedupe-total-data/import', true); + xhr.upload.onprogress = function (event) { + if (event.lengthComputable) { + var percent = Math.round(event.loaded * 100 / event.total); + setDedupeImportProgress(percent, '涓婁紶涓細' + percent + '%'); + } + }; + xhr.onreadystatechange = function () { + if (xhr.readyState !== 4) return; + if (xhr.status < 200 || xhr.status >= 300) { + msgEl.textContent = '璇锋眰澶辫触'; + msgEl.className = 'msg err'; + return; + } + var res; + try { res = JSON.parse(xhr.responseText || '{}'); } catch (e) { res = { success: false, error: '杩斿洖鏍煎紡閿欒' }; } + if (!res.success) { + msgEl.textContent = res.error || '瀵煎叆澶辫触'; + msgEl.className = 'msg err'; + return; + } + setDedupeImportProgress(100, '涓婁紶瀹屾垚锛屽悗绔鐞嗕腑...'); + pollDedupeImport(res.import_id); + fileInput.value = ''; + }; + xhr.onerror = function () { + msgEl.textContent = '璇锋眰澶辫触'; + msgEl.className = 'msg err'; + }; + xhr.send(formData); + }; + document.getElementById('btnDeleteImportDedupeTotalData').onclick = function () { + var fileInput = document.getElementById('dedupeTotalDataDeleteFile'); + var msgEl = document.getElementById('msgDeleteDedupeTotalData'); + msgEl.textContent = ''; + msgEl.className = 'msg'; + stopDedupeDeleteImportProgress(); + document.getElementById('dedupeTotalDataDeleteProgressWrap').style.display = 'none'; + if (!fileInput.files || fileInput.files.length === 0) { + msgEl.textContent = '璇烽€夋嫨 Excel 鏂囦欢'; + msgEl.classList.add('err'); + return; + } + var file = fileInput.files[0]; + var lowerName = (file.name || '').toLowerCase(); + if (!(lowerName.endsWith('.xlsx') || lowerName.endsWith('.xls'))) { + msgEl.textContent = '浠呮敮鎸?.xlsx 鎴?.xls 鏂囦欢'; + msgEl.classList.add('err'); + return; + } + if (!confirm('纭畾鎸?Excel 涓殑 ASIN 鎵归噺鍒犻櫎鍖归厤鐨勬€绘暟鎹悧锛?)) { + return; + } + var formData = new FormData(); + formData.append('file', file); + var xhr = new XMLHttpRequest(); + xhr.open('POST', '/api/admin/dedupe-total-data/delete-import', true); + xhr.upload.onprogress = function (event) { + if (event.lengthComputable) { + var percent = Math.round(event.loaded * 100 / event.total); + setDedupeDeleteImportProgress(percent, '涓婁紶涓細' + percent + '%'); + } + }; + xhr.onreadystatechange = function () { + if (xhr.readyState !== 4) return; + if (xhr.status < 200 || xhr.status >= 300) { + msgEl.textContent = '璇锋眰澶辫触'; + msgEl.className = 'msg err'; + return; + } + var res; + try { res = JSON.parse(xhr.responseText || '{}'); } catch (e) { res = { success: false, error: '杩斿洖鏍煎紡閿欒' }; } + if (!res.success) { + msgEl.textContent = res.error || '鍒犻櫎澶辫触'; + msgEl.className = 'msg err'; + return; + } + setDedupeDeleteImportProgress(100, '涓婁紶瀹屾垚锛屽悗绔鐞嗕腑...'); + pollDedupeDeleteImport(res.import_id); + fileInput.value = ''; + }; + xhr.onerror = function () { + msgEl.textContent = '璇锋眰澶辫触'; + msgEl.className = 'msg err'; + }; + xhr.send(formData); + }; + document.getElementById('btnSaveDedupeTotalData').onclick = function () { + var itemId = document.getElementById('editDedupeTotalDataId').value; + var value = (document.getElementById('editDedupeTotalDataValue').value || '').trim(); + var msgEl = document.getElementById('msgEditDedupeTotalData'); + msgEl.textContent = ''; + msgEl.className = 'msg'; + if (!value) { + msgEl.textContent = '璇峰~鍐欐€绘暟鎹€?; + msgEl.classList.add('err'); + return; + } + fetch('/api/admin/dedupe-total-data/' + itemId, { + method: 'PUT', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ data_value: value }) + }) + .then(function (r) { return r.json(); }) + .then(function (res) { + if (res.success) { + document.getElementById('editDedupeTotalDataModal').classList.remove('show'); + loadDedupeTotalData(dedupeTotalDataPage); + } else { + msgEl.textContent = res.error || '淇濆瓨澶辫触'; + msgEl.classList.add('err'); + } + }); + }; + document.getElementById('btnCloseEditDedupeTotalData').onclick = function () { + document.getElementById('editDedupeTotalDataModal').classList.remove('show'); + }; + + // ========== 搴楅摵瀵嗛挜绠$悊 ========== + var shopKeyPage = 1, shopKeyPageSize = 15; + function buildShopKeyQuery(page) { + return 'page=' + (page || 1) + '&page_size=' + shopKeyPageSize; + } + function loadShopKeys(page) { + shopKeyPage = page || 1; + fetch('/api/admin/shop-keys?' + buildShopKeyQuery(shopKeyPage)) + .then(function (r) { return r.json(); }) + .then(function (res) { + var tbody = document.getElementById('shopKeyListBody'); + if (!res.success) { + tbody.innerHTML = '鍔犺浇澶辫触: ' + (res.error || '') + ''; + return; + } + var items = res.items || []; + if (items.length === 0) { + tbody.innerHTML = '鏆傛棤搴楅摵瀵嗛挜'; + } else { + tbody.innerHTML = items.map(function (item, index) { + var rowNo = (shopKeyPage - 1) * shopKeyPageSize + index + 1; + return '' + rowNo + '' + (item.ziniao_account_name || '') + '' + (item.ziniao_token || '') + '' + (item.created_at || '') + '' + (item.updated_at || '') + '' + + ' ' + + '' + + ''; + }).join(''); + } + renderPagination('shopKeyPagination', res.total, res.page, res.page_size, loadShopKeys); + bindShopKeyActions(); + }) + .catch(function () { + document.getElementById('shopKeyListBody').innerHTML = '璇锋眰澶辫触'; + }); + } + function bindShopKeyActions() { + document.querySelectorAll('[data-shop-key-edit]').forEach(function (btn) { + btn.onclick = function () { + var item = {}; + try { item = JSON.parse((btn.dataset.shopKey || '').replace(/"/g, '"')); } catch (e) { item = {}; } + document.getElementById('editShopKeyId').value = item.id || ''; + document.getElementById('editShopKeyZiniaoAccountName').value = item.ziniao_account_name || ''; + document.getElementById('editShopKeyZiniaoToken').value = item.ziniao_token || ''; + document.getElementById('msgEditShopKey').textContent = ''; + document.getElementById('msgEditShopKey').className = 'msg'; + document.getElementById('editShopKeyModal').classList.add('show'); + }; + }); + document.querySelectorAll('[data-shop-key-delete]').forEach(function (btn) { + btn.onclick = function () { + var name = (btn.dataset.ziniaoAccountName || '').replace(/"/g, '"'); + if (!confirm('纭畾鍒犻櫎搴楅摵瀵嗛挜鈥? + name + '鈥濆悧锛?)) return; + fetch('/api/admin/shop-key/' + btn.dataset.shopKeyDelete, { method: 'DELETE' }) + .then(function (r) { return r.json(); }) + .then(function (res) { + if (res.success) { loadShopKeys(shopKeyPage); } + else { alert(res.error || '鍒犻櫎澶辫触'); } + }); + }; + }); + } + document.getElementById('btnCreateShopKey').onclick = function () { + var ziniaoAccountName = (document.getElementById('shopKeyZiniaoAccountName').value || '').trim(); + var ziniaoToken = (document.getElementById('shopKeyZiniaoToken').value || '').trim(); + var msgEl = document.getElementById('msgShopKey'); + msgEl.textContent = ''; + msgEl.className = 'msg'; + if (!ziniaoAccountName || !ziniaoToken) { + msgEl.textContent = '璇峰畬鏁村~鍐欑传楦熻处鍙峰悕绉般€佺传楦熶护鐗?; + msgEl.classList.add('err'); + return; + } + fetch('/api/admin/shop-key', { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ + ziniao_account_name: ziniaoAccountName, + ziniao_token: ziniaoToken + }) + }) + .then(function (r) { return r.json(); }) + .then(function (res) { + if (res.success) { + document.getElementById('shopKeyZiniaoAccountName').value = ''; + document.getElementById('shopKeyZiniaoToken').value = ''; + msgEl.textContent = res.msg || '鍒涘缓鎴愬姛'; + msgEl.className = 'msg ok'; + loadShopKeys(1); + } else { + msgEl.textContent = res.error || '鍒涘缓澶辫触'; + msgEl.className = 'msg err'; + } + }) + .catch(function () { + msgEl.textContent = '璇锋眰澶辫触'; + msgEl.className = 'msg err'; + }); + }; + document.getElementById('btnSaveShopKey').onclick = function () { + var itemId = document.getElementById('editShopKeyId').value; + var ziniaoAccountName = (document.getElementById('editShopKeyZiniaoAccountName').value || '').trim(); + var ziniaoToken = (document.getElementById('editShopKeyZiniaoToken').value || '').trim(); + var msgEl = document.getElementById('msgEditShopKey'); + msgEl.textContent = ''; + msgEl.className = 'msg'; + if (!ziniaoAccountName || !ziniaoToken) { + msgEl.textContent = '璇峰畬鏁村~鍐欑传楦熻处鍙峰悕绉般€佺传楦熶护鐗?; + msgEl.classList.add('err'); + return; + } + fetch('/api/admin/shop-key/' + itemId, { + method: 'PUT', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ + ziniao_account_name: ziniaoAccountName, + ziniao_token: ziniaoToken + }) + }) + .then(function (r) { return r.json(); }) + .then(function (res) { + if (res.success) { + document.getElementById('editShopKeyModal').classList.remove('show'); + loadShopKeys(shopKeyPage); + } else { + msgEl.textContent = res.error || '淇濆瓨澶辫触'; + msgEl.classList.add('err'); + } + }) + .catch(function () { + msgEl.textContent = '璇锋眰澶辫触'; + msgEl.classList.add('err'); + }); + }; + document.getElementById('btnCloseEditShopKey').onclick = function () { + document.getElementById('editShopKeyModal').classList.remove('show'); + }; + + // ========== 搴楅摵绠$悊 ========== + var shopManagePage = 1, shopManagePageSize = 15; + var shopManageGroups = []; + + function buildShopManageQuery(page) { + var query = 'page=' + (page || 1) + '&page_size=' + shopManagePageSize; + var groupId = (document.getElementById('shopManageFilterGroupId').value || '').trim(); + var shopName = (document.getElementById('shopManageFilterShopName').value || '').trim(); + if (groupId) query += '&group_id=' + encodeURIComponent(groupId); + if (shopName) query += '&shop_name=' + encodeURIComponent(shopName); + return query; + } + + function refreshShopGroupSelects(selectedCreateId, selectedEditId) { + var createSel = document.getElementById('shopManageGroupSelect'); + var editSel = document.getElementById('editShopManageGroupSelect'); + var filterSel = document.getElementById('shopManageFilterGroupId'); + var skipCreateSel = document.getElementById('skipPriceAsinGroupSelect'); + var skipFilterSel = document.getElementById('skipPriceAsinFilterGroupId'); + var chooseSkipShopSel = document.getElementById('chooseSkipPriceAsinShopGroupId'); + var selectedFilterId = filterSel ? filterSel.value : ''; + var selectedSkipCreateId = skipCreateSel ? skipCreateSel.value : ''; + var selectedSkipFilterId = skipFilterSel ? skipFilterSel.value : ''; + var selectedChooseSkipShopId = chooseSkipShopSel ? chooseSkipShopSel.value : ''; + var createOpts = ['']; + var filterOpts = ['']; + shopManageGroups.forEach(function (g) { + var option = ''; + createOpts.push(option); + filterOpts.push(option); + }); + createSel.innerHTML = createOpts.join(''); + editSel.innerHTML = createOpts.join(''); + if (skipCreateSel) skipCreateSel.innerHTML = createOpts.join(''); + if (filterSel) filterSel.innerHTML = filterOpts.join(''); + if (skipFilterSel) skipFilterSel.innerHTML = filterOpts.join(''); + if (chooseSkipShopSel) chooseSkipShopSel.innerHTML = filterOpts.join(''); + if (selectedCreateId != null) createSel.value = String(selectedCreateId); + if (selectedEditId != null) editSel.value = String(selectedEditId); + if (filterSel && selectedFilterId) filterSel.value = selectedFilterId; + if (skipCreateSel && selectedSkipCreateId) skipCreateSel.value = selectedSkipCreateId; + if (skipFilterSel && selectedSkipFilterId) skipFilterSel.value = selectedSkipFilterId; + if (chooseSkipShopSel && selectedChooseSkipShopId) chooseSkipShopSel.value = selectedChooseSkipShopId; + } + + function loadShopManageGroups(selectedCreateId, selectedEditId) { + return fetch('/api/admin/shop-manage-groups') + .then(function (r) { return r.json(); }) + .then(function (res) { + if (!res.success) throw new Error(res.error || '鍔犺浇鍒嗙粍澶辫触'); + shopManageGroups = res.items || []; + refreshShopGroupSelects(selectedCreateId, selectedEditId); + return shopManageGroups; + }) + .catch(function () { + shopManageGroups = []; + refreshShopGroupSelects(); + return []; + }); + } + + function openShopManageGroupModal() { + document.getElementById('shopManageGroupEditId').value = ''; + document.getElementById('shopManageGroupInput').value = ''; + document.getElementById('msgShopManageGroup').textContent = ''; + document.getElementById('msgShopManageGroup').className = 'msg'; + loadShopManageGroups().then(function () { + renderShopManageGroupRows(); + document.getElementById('shopManageGroupModal').classList.add('show'); + }); + } + + function renderShopManageGroupRows() { + var tbody = document.getElementById('shopManageGroupListBody'); + if (!shopManageGroups.length) { + tbody.innerHTML = '鏆傛棤鍒嗙粍'; + return; + } + tbody.innerHTML = shopManageGroups.map(function (item, index) { + return '' + (index + 1) + '' + (item.group_name || '') + '' + (item.created_at || '') + '' + (item.updated_at || '') + '' + + ' ' + + '' + + ''; + }).join(''); + + document.querySelectorAll('[data-shop-group-edit]').forEach(function (btn) { + btn.onclick = function () { + document.getElementById('shopManageGroupEditId').value = btn.dataset.shopGroupEdit; + document.getElementById('shopManageGroupInput').value = (btn.dataset.shopGroupName || '').replace(/"/g, '"'); + }; + }); + + document.querySelectorAll('[data-shop-group-delete]').forEach(function (btn) { + btn.onclick = function () { + var gid = btn.dataset.shopGroupDelete; + var gname = (btn.dataset.shopGroupName || '').replace(/"/g, '"'); + if (!confirm('纭畾鍒犻櫎鍒嗙粍鈥? + gname + '鈥濆悧锛?)) return; + fetch('/api/admin/shop-manage-group/' + gid, { method: 'DELETE' }) + .then(function (r) { return r.json(); }) + .then(function (res) { + if (!res.success) { + alert(res.error || '鍒犻櫎澶辫触'); + return; + } + loadShopManageGroups().then(function () { + renderShopManageGroupRows(); + loadShopManage(shopManagePage); + loadSkipPriceAsin(skipPriceAsinPage); + }); + }); + }; + }); + } + + function loadShopManage(page) { + shopManagePage = page || 1; + fetch('/api/admin/shop-manages?' + buildShopManageQuery(shopManagePage)) + .then(function (r) { return r.json(); }) + .then(function (res) { + var tbody = document.getElementById('shopManageListBody'); + if (!res.success) { + tbody.innerHTML = '鍔犺浇澶辫触: ' + (res.error || '') + ''; + return; + } + var items = res.items || []; + if (items.length === 0) { + tbody.innerHTML = '鏆傛棤搴楅摵'; + } else { + tbody.innerHTML = items.map(function (item, index) { + var rowNo = (shopManagePage - 1) * shopManagePageSize + index + 1; + return '' + rowNo + '' + (item.group_name || '') + '' + (item.shop_name || '') + '' + (item.mall_name || '') + '' + (item.account || '') + '' + (item.password || '') + '' + (item.created_at || '') + '' + (item.updated_at || '') + '' + + ' ' + + '' + + ''; + }).join(''); + } + renderPagination('shopManagePagination', res.total, res.page, res.page_size, loadShopManage); + bindShopManageActions(); + }) + .catch(function () { + document.getElementById('shopManageListBody').innerHTML = '璇锋眰澶辫触'; + }); + } + + function bindShopManageActions() { + document.querySelectorAll('[data-shop-manage-edit]').forEach(function (btn) { + btn.onclick = function () { + var item = {}; + try { item = JSON.parse((btn.dataset.shopManage || '').replace(/"/g, '"')); } catch (e) { item = {}; } + document.getElementById('editShopManageId').value = item.id || ''; + document.getElementById('editShopManageShopName').value = item.shop_name || ''; + document.getElementById('editShopManageMallName').value = item.mall_name || ''; + document.getElementById('editShopManageAccount').value = item.account || ''; + document.getElementById('editShopManagePassword').value = item.password || ''; + document.getElementById('msgEditShopManage').textContent = ''; + document.getElementById('msgEditShopManage').className = 'msg'; + loadShopManageGroups(null, item.group_id).then(function () { + document.getElementById('editShopManageModal').classList.add('show'); + }); + }; + }); + document.querySelectorAll('[data-shop-manage-delete]').forEach(function (btn) { + btn.onclick = function () { + var name = (btn.dataset.shopManageName || '').replace(/"/g, '"'); + if (!confirm('纭畾鍒犻櫎搴楅摵鈥? + name + '鈥濆悧锛?)) return; + fetch('/api/admin/shop-manage/' + btn.dataset.shopManageDelete, { method: 'DELETE' }) + .then(function (r) { return r.json(); }) + .then(function (res) { + if (res.success) { loadShopManage(shopManagePage); } + else { alert(res.error || '鍒犻櫎澶辫触'); } + }); + }; + }); + } + + document.getElementById('btnManageShopGroups').onclick = openShopManageGroupModal; + document.getElementById('btnManageShopGroupsFromEdit').onclick = openShopManageGroupModal; + document.getElementById('btnSearchShopManage').onclick = function () { + loadShopManage(1); + }; + document.getElementById('shopManageFilterShopName').addEventListener('keydown', function (e) { + if (e.key === 'Enter') { + e.preventDefault(); + loadShopManage(1); + } + }); + document.getElementById('btnCloseShopManageGroupModal').onclick = function () { + document.getElementById('shopManageGroupModal').classList.remove('show'); + }; + document.getElementById('btnCancelShopManageGroupEdit').onclick = function () { + document.getElementById('shopManageGroupEditId').value = ''; + document.getElementById('shopManageGroupInput').value = ''; + }; + document.getElementById('btnSaveShopManageGroup').onclick = function () { + var editId = (document.getElementById('shopManageGroupEditId').value || '').trim(); + var groupName = (document.getElementById('shopManageGroupInput').value || '').trim(); + var msgEl = document.getElementById('msgShopManageGroup'); + msgEl.textContent = ''; + msgEl.className = 'msg'; + if (!groupName) { + msgEl.textContent = '璇疯緭鍏ュ垎缁勫悕绉?; + msgEl.className = 'msg err'; + return; + } + var method = editId ? 'PUT' : 'POST'; + var url = editId ? ('/api/admin/shop-manage-group/' + editId) : '/api/admin/shop-manage-group'; + fetch(url, { + method: method, + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ group_name: groupName }) + }) + .then(function (r) { return r.json(); }) + .then(function (res) { + if (!res.success) { + msgEl.textContent = res.error || '淇濆瓨澶辫触'; + msgEl.className = 'msg err'; + return; + } + document.getElementById('shopManageGroupEditId').value = ''; + document.getElementById('shopManageGroupInput').value = ''; + msgEl.textContent = res.msg || '淇濆瓨鎴愬姛'; + msgEl.className = 'msg ok'; + loadShopManageGroups().then(function () { + renderShopManageGroupRows(); + loadShopManage(shopManagePage); + loadSkipPriceAsin(skipPriceAsinPage); + }); + }) + .catch(function () { + msgEl.textContent = '璇锋眰澶辫触'; + msgEl.className = 'msg err'; + }); + }; + + function refreshShopGroupSelects(selectedCreateId, selectedEditId) { + var createSel = document.getElementById('shopManageGroupSelect'); + var editSel = document.getElementById('editShopManageGroupSelect'); + var filterSel = document.getElementById('shopManageFilterGroupId'); + var skipCreateSel = document.getElementById('skipPriceAsinGroupSelect'); + var skipFilterSel = document.getElementById('skipPriceAsinFilterGroupId'); + var chooseSkipShopSel = document.getElementById('chooseSkipPriceAsinShopGroupId'); + var selectedFilterId = filterSel ? filterSel.value : ''; + var selectedSkipCreateId = skipCreateSel ? skipCreateSel.value : ''; + var selectedSkipFilterId = skipFilterSel ? skipFilterSel.value : ''; + var selectedChooseSkipShopId = chooseSkipShopSel ? chooseSkipShopSel.value : ''; + var createOpts = ['']; + var filterOpts = ['']; + shopManageGroups.forEach(function (g) { + var option = ''; + createOpts.push(option); + filterOpts.push(option); + }); + createSel.innerHTML = createOpts.join(''); + editSel.innerHTML = createOpts.join(''); + if (skipCreateSel) skipCreateSel.innerHTML = createOpts.join(''); + if (filterSel) filterSel.innerHTML = filterOpts.join(''); + if (skipFilterSel) skipFilterSel.innerHTML = filterOpts.join(''); + if (chooseSkipShopSel) chooseSkipShopSel.innerHTML = filterOpts.join(''); + if (selectedCreateId != null) createSel.value = String(selectedCreateId); + if (selectedEditId != null) editSel.value = String(selectedEditId); + if (filterSel && selectedFilterId) filterSel.value = selectedFilterId; + if (skipCreateSel && selectedSkipCreateId) skipCreateSel.value = selectedSkipCreateId; + if (skipFilterSel && selectedSkipFilterId) skipFilterSel.value = selectedSkipFilterId; + if (chooseSkipShopSel && selectedChooseSkipShopId) chooseSkipShopSel.value = selectedChooseSkipShopId; + } + + function loadShopManageGroups(selectedCreateId, selectedEditId) { + return fetch('/api/admin/shop-manage-groups') + .then(function (r) { return r.json(); }) + .then(function (res) { + if (!res.success) throw new Error(res.error || '鍔犺浇鍒嗙粍澶辫触'); + shopManageGroups = res.items || []; + refreshShopGroupSelects(selectedCreateId, selectedEditId); + return shopManageGroups; + }) + .catch(function () { + shopManageGroups = []; + refreshShopGroupSelects(); + return []; + }); + } + + function resetShopManageGroupForm() { + document.getElementById('shopManageGroupEditId').value = ''; + document.getElementById('shopManageGroupInput').value = ''; + document.getElementById('msgShopManageGroup').textContent = ''; + document.getElementById('msgShopManageGroup').className = 'msg'; + refreshShopManageGroupUserSelect(''); + } + + function openShopManageGroupModal() { + resetShopManageGroupForm(); + loadUserOptions() + .then(function () { return loadShopManageGroups(); }) + .then(function () { + renderShopManageGroupRows(); + document.getElementById('shopManageGroupModal').classList.add('show'); + }); + } + + function renderShopManageGroupRows() { + var tbody = document.getElementById('shopManageGroupListBody'); + if (!shopManageGroups.length) { + tbody.innerHTML = '鏆傛棤鍒嗙粍'; + return; + } + tbody.innerHTML = shopManageGroups.map(function (item, index) { + return '' + (index + 1) + '' + (item.group_name || '') + '' + (item.username || '') + '' + (item.created_at || '') + '' + (item.updated_at || '') + '' + + ' ' + + '' + + ''; + }).join(''); + + document.querySelectorAll('[data-shop-group-edit]').forEach(function (btn) { + btn.onclick = function () { + document.getElementById('shopManageGroupEditId').value = btn.dataset.shopGroupEdit; + refreshShopManageGroupUserSelect(btn.dataset.shopGroupUserId || ''); + }; + }); + + document.querySelectorAll('[data-shop-group-delete]').forEach(function (btn) { + btn.onclick = function () { + var gid = btn.dataset.shopGroupDelete; + var gname = (btn.dataset.shopGroupName || '').replace(/"/g, '"'); + if (!confirm('纭畾鍒犻櫎鍒嗙粍鈥? + gname + '鈥濆悧锛?)) return; + fetch('/api/admin/shop-manage-group/' + gid, { method: 'DELETE' }) + .then(function (r) { return r.json(); }) + .then(function (res) { + if (!res.success) { + alert(res.error || '鍒犻櫎澶辫触'); + return; + } + loadShopManageGroups().then(function () { + renderShopManageGroupRows(); + loadShopManage(shopManagePage); + loadSkipPriceAsin(skipPriceAsinPage); + }); + }); + }; + }); + } + + document.getElementById('shopManageGroupUserSelect').onchange = syncShopManageGroupNameWithUser; + document.getElementById('btnManageShopGroups').onclick = openShopManageGroupModal; + document.getElementById('btnManageShopGroupsFromEdit').onclick = openShopManageGroupModal; + document.getElementById('btnManageSkipPriceAsinGroups').onclick = openShopManageGroupModal; + document.getElementById('btnCloseShopManageGroupModal').onclick = function () { + document.getElementById('shopManageGroupModal').classList.remove('show'); + }; + document.getElementById('btnCancelShopManageGroupEdit').onclick = resetShopManageGroupForm; + document.getElementById('btnSaveShopManageGroup').onclick = function () { + var editId = (document.getElementById('shopManageGroupEditId').value || '').trim(); + var groupName = (document.getElementById('shopManageGroupInput').value || '').trim(); + var userId = (document.getElementById('shopManageGroupUserSelect').value || '').trim(); + var msgEl = document.getElementById('msgShopManageGroup'); + msgEl.textContent = ''; + msgEl.className = 'msg'; + if (!userId || !groupName) { + msgEl.textContent = '璇烽€夋嫨鍏宠仈鐢ㄦ埛'; + msgEl.className = 'msg err'; + return; + } + var method = editId ? 'PUT' : 'POST'; + var url = editId ? ('/api/admin/shop-manage-group/' + editId) : '/api/admin/shop-manage-group'; + fetch(url, { + method: method, + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ group_name: groupName, user_id: Number(userId) }) + }) + .then(function (r) { return r.json(); }) + .then(function (res) { + if (!res.success) { + msgEl.textContent = res.error || '淇濆瓨澶辫触'; + msgEl.className = 'msg err'; + return; + } + resetShopManageGroupForm(); + msgEl.textContent = res.msg || '淇濆瓨鎴愬姛'; + msgEl.className = 'msg ok'; + loadShopManageGroups().then(function () { + renderShopManageGroupRows(); + loadShopManage(shopManagePage); + loadSkipPriceAsin(skipPriceAsinPage); + }); + }) + .catch(function () { + msgEl.textContent = '璇锋眰澶辫触'; + msgEl.className = 'msg err'; + }); + }; + + document.getElementById('btnCreateShopManage').onclick = function () { + var groupId = (document.getElementById('shopManageGroupSelect').value || '').trim(); + var shopName = (document.getElementById('shopManageShopName').value || '').trim(); + var mallName = (document.getElementById('shopManageMallName').value || '').trim(); + var account = (document.getElementById('shopManageAccount').value || '').trim(); + var password = (document.getElementById('shopManagePassword').value || '').trim(); + var msgEl = document.getElementById('msgShopManage'); + msgEl.textContent = ''; + msgEl.className = 'msg'; + if (!groupId || !shopName || !mallName || !account || !password) { + msgEl.textContent = '璇峰畬鏁村~鍐欏垎缁勩€佸簵閾哄悕銆佸簵閾哄晢鍩庡悕銆佽处鍙枫€佸瘑鐮?; + msgEl.classList.add('err'); + return; + } + fetch('/api/admin/shop-manage', { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ group_id: Number(groupId), shop_name: shopName, mall_name: mallName, account: account, password: password }) + }) + .then(function (r) { return r.json(); }) + .then(function (res) { + if (res.success) { + document.getElementById('shopManageGroupSelect').value = ''; + document.getElementById('shopManageShopName').value = ''; + document.getElementById('shopManageMallName').value = ''; + document.getElementById('shopManageAccount').value = ''; + document.getElementById('shopManagePassword').value = ''; + msgEl.textContent = res.msg || '鍒涘缓鎴愬姛'; + msgEl.className = 'msg ok'; + loadShopManage(1); + } else { + msgEl.textContent = res.error || '鍒涘缓澶辫触'; + msgEl.className = 'msg err'; + } + }) + .catch(function () { + msgEl.textContent = '璇锋眰澶辫触'; + msgEl.className = 'msg err'; + }); + }; + + document.getElementById('btnSaveShopManage').onclick = function () { + var itemId = document.getElementById('editShopManageId').value; + var groupId = (document.getElementById('editShopManageGroupSelect').value || '').trim(); + var shopName = (document.getElementById('editShopManageShopName').value || '').trim(); + var mallName = (document.getElementById('editShopManageMallName').value || '').trim(); + var account = (document.getElementById('editShopManageAccount').value || '').trim(); + var password = (document.getElementById('editShopManagePassword').value || '').trim(); + var msgEl = document.getElementById('msgEditShopManage'); + msgEl.textContent = ''; + msgEl.className = 'msg'; + if (!groupId || !shopName || !mallName || !account || !password) { + msgEl.textContent = '璇峰畬鏁村~鍐欏垎缁勩€佸簵閾哄悕銆佸簵閾哄晢鍩庡悕銆佽处鍙枫€佸瘑鐮?; + msgEl.classList.add('err'); + return; + } + fetch('/api/admin/shop-manage/' + itemId, { + method: 'PUT', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ group_id: Number(groupId), shop_name: shopName, mall_name: mallName, account: account, password: password }) + }) + .then(function (r) { return r.json(); }) + .then(function (res) { + if (res.success) { + document.getElementById('editShopManageModal').classList.remove('show'); + loadShopManage(shopManagePage); + } else { + msgEl.textContent = res.error || '淇濆瓨澶辫触'; + msgEl.classList.add('err'); + } + }) + .catch(function () { + msgEl.textContent = '璇锋眰澶辫触'; + msgEl.classList.add('err'); + }); + }; + document.getElementById('btnCloseEditShopManage').onclick = function () { + document.getElementById('editShopManageModal').classList.remove('show'); + }; + + // ========== 鐗堟湰绠$悊 ========== + // ========== 璺宠繃璺熶环 ASIN ========== + var skipPriceAsinPage = 1, skipPriceAsinPageSize = 15; + var chooseSkipPriceAsinShopPage = 1, chooseSkipPriceAsinShopPageSize = 10; + var skipPriceCountryColumns = [ + { code: 'DE', field: 'asin_de', label: '寰峰浗' }, + { code: 'UK', field: 'asin_uk', label: '鑻卞浗' }, + { code: 'FR', field: 'asin_fr', label: '娉曞浗' }, + { code: 'IT', field: 'asin_it', label: '鎰忓ぇ鍒? }, + { code: 'ES', field: 'asin_es', label: '瑗跨彮鐗? } + ]; + function buildChooseSkipPriceAsinShopQuery(page) { + var query = 'page=' + (page || 1) + '&page_size=' + chooseSkipPriceAsinShopPageSize; + var groupId = (document.getElementById('chooseSkipPriceAsinShopGroupId').value || '').trim(); + var shopName = (document.getElementById('chooseSkipPriceAsinShopKeyword').value || '').trim(); + if (groupId) query += '&group_id=' + encodeURIComponent(groupId); + if (shopName) query += '&shop_name=' + encodeURIComponent(shopName); + return query; + } + function loadChooseSkipPriceAsinShops(page) { + chooseSkipPriceAsinShopPage = page || 1; + fetch('/api/admin/shop-manages?' + buildChooseSkipPriceAsinShopQuery(chooseSkipPriceAsinShopPage)) + .then(function (r) { return r.json(); }) + .then(function (res) { + var tbody = document.getElementById('chooseSkipPriceAsinShopListBody'); + if (!res.success) { + tbody.innerHTML = '鍔犺浇澶辫触: ' + (res.error || '') + ''; + return; + } + var items = res.items || []; + if (!items.length) { + tbody.innerHTML = '鏆傛棤搴楅摵'; + } else { + tbody.innerHTML = items.map(function (item, index) { + var rowNo = (chooseSkipPriceAsinShopPage - 1) * chooseSkipPriceAsinShopPageSize + index + 1; + return '' + rowNo + '' + (item.group_name || '') + '' + (item.shop_name || '') + '' + (item.mall_name || '') + '' + (item.account || '') + '' + + '' + + ''; + }).join(''); + } + renderPagination('chooseSkipPriceAsinShopPagination', res.total, res.page, res.page_size, loadChooseSkipPriceAsinShops); + bindChooseSkipPriceAsinShopActions(); + }) + .catch(function () { + document.getElementById('chooseSkipPriceAsinShopListBody').innerHTML = '璇锋眰澶辫触'; + }); + } + function bindChooseSkipPriceAsinShopActions() { + document.querySelectorAll('[data-choose-skip-price-asin-shop]').forEach(function (btn) { + btn.onclick = function () { + document.getElementById('skipPriceAsinShopName').value = (btn.dataset.shopName || '').replace(/"/g, '"'); + if (!document.getElementById('skipPriceAsinGroupSelect').value && btn.dataset.groupId) { + document.getElementById('skipPriceAsinGroupSelect').value = btn.dataset.groupId; + } + document.getElementById('chooseSkipPriceAsinShopModal').classList.remove('show'); + }; + }); + } + function openChooseSkipPriceAsinShopModal() { + var currentGroupId = (document.getElementById('skipPriceAsinGroupSelect').value || '').trim(); + if (currentGroupId) { + document.getElementById('chooseSkipPriceAsinShopGroupId').value = currentGroupId; + } + document.getElementById('chooseSkipPriceAsinShopKeyword').value = (document.getElementById('skipPriceAsinShopName').value || '').trim(); + document.getElementById('chooseSkipPriceAsinShopModal').classList.add('show'); + loadChooseSkipPriceAsinShops(1); + } + function setupSkipPriceAsinShopPicker() { + var input = document.getElementById('skipPriceAsinShopName'); + var button = document.getElementById('btnChooseSkipPriceAsinShop'); + if (!input || !button) return; + var inputParent = input.parentNode; + if (inputParent && inputParent.style && inputParent.style.display === 'flex') return; + var legacyWrap = button.parentNode; + var wrapper = document.createElement('div'); + wrapper.style.display = 'flex'; + wrapper.style.gap = '8px'; + wrapper.style.alignItems = 'center'; + input.parentNode.insertBefore(wrapper, input); + wrapper.appendChild(input); + wrapper.appendChild(button); + button.style.whiteSpace = 'nowrap'; + button.style.marginTop = '0'; + if (legacyWrap && legacyWrap !== wrapper) { + legacyWrap.style.display = 'none'; + } + } + function getSelectedSkipPriceCountries() { + return Array.from(document.getElementById('skipPriceAsinCountries').selectedOptions).map(function (option) { + return option.value; + }); + } + function renderSkipPriceAsinInputs() { + var container = document.getElementById('skipPriceAsinInputs'); + var selectedCountries = getSelectedSkipPriceCountries(); + var existingValues = {}; + container.querySelectorAll('[data-skip-price-country-input]').forEach(function (input) { + existingValues[input.getAttribute('data-skip-price-country-input')] = input.value; + }); + if (!selectedCountries.length) { + container.innerHTML = '
璇烽€夋嫨鍥藉鍚庤緭鍏?ASIN
'; + return; + } + container.innerHTML = selectedCountries.map(function (countryCode) { + var country = skipPriceCountryColumns.find(function (item) { return item.code === countryCode; }); + var label = country ? country.label : countryCode; + var value = existingValues[countryCode] || ''; + return '
' + + '' + label + '' + + '' + + '
'; + }).join(''); + } + function collectSkipPriceAsinMappings(countries) { + var mappings = {}; + for (var i = 0; i < countries.length; i++) { + var countryCode = countries[i]; + var input = document.querySelector('[data-skip-price-country-input="' + countryCode + '"]'); + var asin = input ? (input.value || '').trim().toUpperCase() : ''; + if (!asin) { + var country = skipPriceCountryColumns.find(function (item) { return item.code === countryCode; }); + var label = country ? country.label : countryCode; + throw new Error(label + ' ASIN 涓嶈兘涓虹┖'); + } + mappings[countryCode] = asin; + } + return mappings; + } + function buildSkipPriceAsinQuery(page) { + var query = 'page=' + (page || 1) + '&page_size=' + skipPriceAsinPageSize; + var groupId = (document.getElementById('skipPriceAsinFilterGroupId').value || '').trim(); + var shopName = (document.getElementById('skipPriceAsinFilterShopName').value || '').trim(); + var asin = (document.getElementById('skipPriceAsinFilterAsin').value || '').trim(); + if (groupId) query += '&group_id=' + encodeURIComponent(groupId); + if (shopName) query += '&shop_name=' + encodeURIComponent(shopName); + if (asin) query += '&asin=' + encodeURIComponent(asin); + return query; + } + function renderSkipPriceAsinCell(item, country) { + var value = item[country.field] || ''; + if (!value) return '-'; + return '
' + + '' + value + '' + + '' + + '' + + '
'; + } + 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, '"'); + var countryCode = btn.dataset.country || ''; + if (!confirm('纭畾鍒犻櫎搴楅摵鈥? + shopName + '鈥濆湪 ' + countryCode + ' 鐨?ASIN 鍚楋紵')) return; + fetch('/api/admin/skip-price-asin/' + btn.dataset.skipPriceAsinDelete + '/country/' + countryCode, { + method: 'DELETE' + }) + .then(function (r) { return r.json(); }) + .then(function (res) { + if (res.success) loadSkipPriceAsin(skipPriceAsinPage); + else alert(res.error || '鍒犻櫎澶辫触'); + }); + }; + }); + } + function loadSkipPriceAsin(page) { + skipPriceAsinPage = page || 1; + fetch('/api/admin/skip-price-asins?' + buildSkipPriceAsinQuery(skipPriceAsinPage)) + .then(function (r) { return r.json(); }) + .then(function (res) { + var tbody = document.getElementById('skipPriceAsinListBody'); + if (!res.success) { + tbody.innerHTML = '鍔犺浇澶辫触: ' + (res.error || '') + ''; + return; + } + var items = res.items || []; + if (items.length === 0) { + tbody.innerHTML = '鏆傛棤鏁版嵁'; + } else { + tbody.innerHTML = items.map(function (item, index) { + var rowNo = (skipPriceAsinPage - 1) * skipPriceAsinPageSize + index + 1; + return '' + rowNo + '' + (item.group_name || '') + '' + (item.shop_name || '') + '' + + skipPriceCountryColumns.map(function (country) { + return '' + renderSkipPriceAsinCell(item, country) + ''; + }).join('') + + ''; + }).join(''); + } + renderPagination('skipPriceAsinPagination', res.total, res.page, res.page_size, loadSkipPriceAsin); + bindSkipPriceAsinActions(); + }) + .catch(function () { + document.getElementById('skipPriceAsinListBody').innerHTML = '璇锋眰澶辫触'; + }); + } + document.getElementById('btnManageSkipPriceAsinGroups').onclick = openShopManageGroupModal; + document.getElementById('btnChooseSkipPriceAsinShop').onclick = openChooseSkipPriceAsinShopModal; + document.getElementById('btnSearchChooseSkipPriceAsinShop').onclick = function () { + loadChooseSkipPriceAsinShops(1); + }; + document.getElementById('chooseSkipPriceAsinShopGroupId').onchange = function () { + loadChooseSkipPriceAsinShops(1); + }; + document.getElementById('chooseSkipPriceAsinShopKeyword').addEventListener('keydown', function (e) { + if (e.key === 'Enter') { + e.preventDefault(); + loadChooseSkipPriceAsinShops(1); + } + }); + document.getElementById('btnCloseChooseSkipPriceAsinShopModal').onclick = function () { + document.getElementById('chooseSkipPriceAsinShopModal').classList.remove('show'); + }; + document.getElementById('skipPriceAsinCountries').addEventListener('change', renderSkipPriceAsinInputs); + document.getElementById('btnSearchSkipPriceAsin').onclick = function () { + loadSkipPriceAsin(1); + }; + document.getElementById('skipPriceAsinFilterShopName').addEventListener('keydown', function (e) { + if (e.key === 'Enter') { + e.preventDefault(); + loadSkipPriceAsin(1); + } + }); + document.getElementById('skipPriceAsinFilterAsin').addEventListener('keydown', function (e) { + if (e.key === 'Enter') { + e.preventDefault(); + loadSkipPriceAsin(1); + } + }); + document.getElementById('btnCreateSkipPriceAsin').onclick = function () { + var groupId = (document.getElementById('skipPriceAsinGroupSelect').value || '').trim(); + var shopName = (document.getElementById('skipPriceAsinShopName').value || '').trim(); + var countries = getSelectedSkipPriceCountries(); + var msgEl = document.getElementById('msgSkipPriceAsin'); + msgEl.textContent = ''; + msgEl.className = 'msg'; + if (!groupId || !shopName || !countries.length) { + msgEl.textContent = '璇峰畬鏁村~鍐欏垎缁勩€佸簵閾哄悕銆佸浗瀹跺拰 ASIN'; + msgEl.className = 'msg err'; + return; + } + var asinMappings = {}; + try { + asinMappings = collectSkipPriceAsinMappings(countries); + } catch (err) { + msgEl.textContent = err.message || '璇疯緭鍏?ASIN'; + msgEl.className = 'msg err'; + return; + } + var fallbackAsin = ''; + Object.keys(asinMappings).some(function (countryCode) { + fallbackAsin = asinMappings[countryCode] || ''; + return !!fallbackAsin; + }); + fetch('/api/admin/skip-price-asin', { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ + group_id: Number(groupId), + shop_name: shopName, + countries: countries, + asin: fallbackAsin, + asin_mappings: asinMappings + }) + }) + .then(function (r) { return r.json(); }) + .then(function (res) { + if (!res.success) { + msgEl.textContent = res.error || '淇濆瓨澶辫触'; + msgEl.className = 'msg err'; + return; + } + document.getElementById('skipPriceAsinGroupSelect').value = ''; + document.getElementById('skipPriceAsinShopName').value = ''; + Array.from(document.getElementById('skipPriceAsinCountries').options).forEach(function (option) { + option.selected = false; + }); + renderSkipPriceAsinInputs(); + msgEl.textContent = res.msg || '淇濆瓨鎴愬姛'; + msgEl.className = 'msg ok'; + loadSkipPriceAsin(1); + }) + .catch(function () { + msgEl.textContent = '璇锋眰澶辫触'; + msgEl.className = 'msg err'; + }); + }; + setupSkipPriceAsinShopPicker(); + renderSkipPriceAsinInputs(); + function loadVersions() { + fetch('/api/admin/versions') + .then(function (r) { return r.json(); }) + .then(function (res) { + var tbody = document.getElementById('versionListBody'); + if (!res.success) { + tbody.innerHTML = '鍔犺浇澶辫触: ' + (res.error || '') + ''; + return; + } + var items = res.items || []; + if (items.length === 0) { + tbody.innerHTML = '鏆傛棤鐗堟湰璁板綍'; + } else { + tbody.innerHTML = items.map(function (v) { + var url = (v.file_url || '').replace(/"/g, '"'); + return '' + (v.version || '') + '' + url + '' + (v.created_at || '') + '涓嬭浇'; + }).join(''); + } + }) + .catch(function () { + document.getElementById('versionListBody').innerHTML = '璇锋眰澶辫触'; + }); + } + document.getElementById('btnUploadVersion').onclick = function () { + var version = (document.getElementById('versionNumber').value || '').trim(); + var fileInput = document.getElementById('versionZip'); + var msgEl = document.getElementById('msgVersion'); + msgEl.textContent = ''; + msgEl.className = 'msg'; + if (!version) { + msgEl.textContent = '璇峰~鍐欑増鏈彿'; + msgEl.classList.add('err'); + return; + } + if (!fileInput.files || fileInput.files.length === 0) { + msgEl.textContent = '璇烽€夋嫨 zip 鍘嬬缉鍖?; + msgEl.classList.add('err'); + return; + } + var file = fileInput.files[0]; + if (!(file.name || '').toLowerCase().endsWith('.zip')) { + msgEl.textContent = '浠呮敮鎸?.zip 鏍煎紡'; + msgEl.classList.add('err'); + return; + } + var formData = new FormData(); + formData.append('version', version); + formData.append('file', file); + msgEl.textContent = '涓婁紶涓?..'; + msgEl.classList.remove('err', 'ok'); + fetch('/api/admin/version', { + method: 'POST', + body: formData + }) + .then(function (r) { return r.json(); }) + .then(function (res) { + if (res.success) { + msgEl.textContent = '鍙戝竷鎴愬姛銆傜増鏈細' + res.version + '锛岄摼鎺ワ細' + (res.file_url || ''); + msgEl.classList.add('ok'); + document.getElementById('versionNumber').value = ''; + fileInput.value = ''; + loadVersions(); + } else { + msgEl.textContent = res.error || '涓婁紶澶辫触'; + msgEl.classList.add('err'); + } + }) + .catch(function () { + msgEl.textContent = '璇锋眰澶辫触'; + msgEl.classList.add('err'); + }); + }; + + // ========== 鏍忕洰鏉冮檺閰嶇疆 ========== + function loadColumns() { + fetch('/api/admin/columns') + .then(function (r) { return r.json(); }) + .then(function (res) { + var tbody = document.getElementById('columnListBody'); + if (!res.success) { + tbody.innerHTML = '鍔犺浇澶辫触: ' + (res.error || '') + ''; + return; + } + allColumnsList = res.items || []; + if (allColumnsList.length === 0) { + tbody.innerHTML = '鏆傛棤鑿滃崟锛岃鍦ㄤ笂鏂规柊澧?/td>'; + } else { + tbody.innerHTML = allColumnsList.map(function (c) { + return '' + c.id + '' + (c.name || '') + '' + (c.column_key || '') + '' + (c.created_at || '') + '' + + ' ' + + ''; + }).join(''); + } + bindColumnActions(); + }) + .catch(function () { + document.getElementById('columnListBody').innerHTML = '璇锋眰澶辫触'; + }); + } + function bindColumnActions() { + document.querySelectorAll('[data-column-edit]').forEach(function (btn) { + btn.onclick = function () { + document.getElementById('editColumnId').value = btn.dataset.columnEdit || ''; + document.getElementById('editColumnName').value = (btn.dataset.name || '').replace(/"/g, '"'); + document.getElementById('editColumnKey').value = (btn.dataset.key || '').replace(/"/g, '"'); + document.getElementById('msgEditColumn').textContent = ''; + document.getElementById('editColumnModal').classList.add('show'); + }; + }); + document.querySelectorAll('[data-column-delete]').forEach(function (btn) { + btn.onclick = function () { + if (!confirm('纭畾鍒犻櫎鑿滃崟鈥? + (btn.dataset.name || '').replace(/"/g, '"') + '鈥濆悧锛?)) return; + fetch('/api/admin/column/' + btn.dataset.columnDelete, { method: 'DELETE' }) + .then(function (r) { return r.json(); }) + .then(function (res) { + if (res.success) { loadColumns(); loadColumnsForPermission(); loadAdminMenus(getActiveAdminTabName()); } + else { alert(res.error || '鍒犻櫎澶辫触'); } + }); + }; + }); + } + document.getElementById('btnAddColumn').onclick = function () { + var name = (document.getElementById('columnName').value || '').trim(); + var key = (document.getElementById('columnKey').value || '').trim(); + var msgEl = document.getElementById('msgColumn'); + msgEl.textContent = ''; + msgEl.className = 'msg'; + if (!name) { msgEl.textContent = '璇峰~鍐欒彍鍗曞悕绉?; msgEl.classList.add('err'); return; } + if (!key) { + msgEl.textContent = '璇峰~鍐欐爮鐩爣璇?; msgEl.classList.add('err'); return; + } + fetch('/api/admin/column', { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ name: name, column_key: key }) + }) + .then(function (r) { return r.json(); }) + .then(function (res) { + if (res.success) { + msgEl.textContent = res.msg || '鏂板鎴愬姛'; + msgEl.classList.add('ok'); + document.getElementById('columnName').value = ''; + document.getElementById('columnKey').value = ''; + loadColumns(); + loadColumnsForPermission(); + loadAdminMenus(getActiveAdminTabName()); + } else { diff --git a/backend/web_source/admin.html b/backend/web_source/admin.html index 1fc9406..82837a5 100644 --- a/backend/web_source/admin.html +++ b/backend/web_source/admin.html @@ -1,20 +1,85 @@ - + + 管理后台 - 南日AI + - +

管理后台

-
+
+ 加载中... + + +
+ +
用户管理
栏目权限配置
数据去重总数据
@@ -120,22 +414,23 @@
-
+

创建用户

-

无注册入口,仅管理员可在此创建用户。层级:超级管理员 → 管理员 → 普通号。

+

+ 当前没有开放注册入口,仅管理员可在此创建用户。层级关系:超级管理员 -> 管理员 -> 普通账号。

- +
- +
@@ -146,7 +441,7 @@
- +
@@ -189,21 +484,32 @@
-

新增栏目

+

新增菜单

- - + +
-
+
- +
- +
+ + +
+
+ + +
+

@@ -213,8 +519,10 @@ ID - 栏目名 + 菜单名称 栏目标识 + 菜单类型 + 排序 菜单路由 创建时间 操作 @@ -271,7 +579,9 @@

@@ -286,7 +596,9 @@

@@ -299,7 +611,7 @@
- + @@ -325,11 +637,11 @@ -
+
- +

@@ -370,7 +682,7 @@ -
+
@@ -378,11 +690,11 @@
-
+
- +

@@ -439,8 +751,12 @@ -
+
+
+ + +
-
+
- +
+
请选择国家后输入对应 ASIN
+
@@ -544,13 +862,13 @@
- +
@@ -559,7 +877,7 @@
- +
@@ -580,7 +898,7 @@ -

+

@@ -594,10 +912,10 @@

编辑栏目

- - + +
-
+
@@ -605,6 +923,17 @@
+
+ + +
+
+ + +

@@ -619,9 +948,16 @@

分组管理

+
+ + +
- +
@@ -629,7 +965,14 @@

- + + + + + + + +
序号分组名称创建时间修改时间操作
序号分组名称关联用户创建时间修改时间操作
@@ -640,6 +983,44 @@ + +