From 8ab647c4199ac55f28ca15fddcbaba90b8a21999 Mon Sep 17 00:00:00 2001 From: super <2903208875@qq.com> Date: Fri, 5 Jun 2026 21:25:03 +0800 Subject: [PATCH] =?UTF-8?q?=E6=8F=90=E4=BA=A4=E6=9B=B4=E6=94=B9?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/.env | 4 +- app/assets/appearance-patent-26DWDYwd.css | 1 + app/assets/appearance-patent-Cb_Fx4da.css | 1 - app/assets/appearance-patent.js | 2 +- app/config.py | 6 +- backend-java/pom.xml | 7 + .../config/TaskOperationLockConfig.java | 3 + .../client/AppearancePatentCozeClient.java | 13 +- .../dto/AppearancePatentResultRowDto.java | 2 +- .../model/vo/AppearancePatentParsedRowVo.java | 6 + .../service/AppearancePatentTaskService.java | 109 ++- .../controller/CollectDataController.java | 11 + .../dto/CollectDataSubmitResultRequest.java | 41 + .../model/dto/CollectDataSubmitRowDto.java | 32 + .../model/vo/CollectDataHistoryItemVo.java | 12 + .../model/vo/CollectDataResultRowVo.java | 12 + .../model/vo/CollectDataSubmitResultVo.java | 22 + .../CollectDataExcelAssemblyService.java | 61 ++ .../service/CollectDataService.java | 730 ++++++++++++++++++ .../convert/service/ConvertRunService.java | 7 +- .../service/DeleteBrandTaskCacheService.java | 2 +- .../service/PriceTrackTaskService.java | 10 +- .../client/SimilarAsinCozeClient.java | 18 +- .../service/SimilarAsinTaskService.java | 79 +- .../split/service/SplitRunService.java | 7 +- .../controller/TaskHeartbeatController.java | 2 +- .../task/model/dto/TaskHeartbeatRequest.java | 7 +- .../task/service/TaskHeartbeatService.java | 24 +- .../task/service/TaskResultFileJobWorker.java | 7 + .../AppearancePatentCozeClientTest.java | 72 +- .../AppearancePatentTaskServiceTest.java | 16 + .../client/SimilarAsinCozeClientTest.java | 97 +++ .../service/SimilarAsinTaskServiceTest.java | 33 + .../components/BrandAppearancePatentTab.vue | 18 +- .../brand/components/BrandCollectDataTab.vue | 4 + frontend-vue/src/shared/api/java-modules.ts | 4 + user_data/api_logs/api_2026_05_07.log | 0 xlsx/1-1.xlsx | Bin 16071 -> 0 bytes xlsx/1.xlsx | Bin 15829 -> 0 bytes 39 files changed, 1390 insertions(+), 92 deletions(-) create mode 100644 app/assets/appearance-patent-26DWDYwd.css delete mode 100644 app/assets/appearance-patent-Cb_Fx4da.css create mode 100644 backend-java/src/main/java/com/nanri/aiimage/modules/collectdata/model/dto/CollectDataSubmitResultRequest.java create mode 100644 backend-java/src/main/java/com/nanri/aiimage/modules/collectdata/model/dto/CollectDataSubmitRowDto.java create mode 100644 backend-java/src/main/java/com/nanri/aiimage/modules/collectdata/model/vo/CollectDataResultRowVo.java create mode 100644 backend-java/src/main/java/com/nanri/aiimage/modules/collectdata/model/vo/CollectDataSubmitResultVo.java create mode 100644 backend-java/src/main/java/com/nanri/aiimage/modules/collectdata/service/CollectDataExcelAssemblyService.java create mode 100644 backend-java/src/test/java/com/nanri/aiimage/modules/appearancepatent/service/AppearancePatentTaskServiceTest.java create mode 100644 backend-java/src/test/java/com/nanri/aiimage/modules/similarasin/service/SimilarAsinTaskServiceTest.java delete mode 100644 user_data/api_logs/api_2026_05_07.log delete mode 100644 xlsx/1-1.xlsx delete mode 100644 xlsx/1.xlsx diff --git a/app/.env b/app/.env index d84ea08..84c6c08 100644 --- a/app/.env +++ b/app/.env @@ -12,8 +12,8 @@ zn_username=%E8%87%AA%E5%8A%A8%E5%8C%96_Robot client_name=ShuFuAI # java_api_base=http://api.aishufu.top:18080/ -# java_api_base=http://api.aishufu.top:18080/ -java_api_base=http://127.0.0.1:18080/ +java_api_base=http://api.aishufu.top:18080/ +# java_api_base=http://127.0.0.1:18080/ # 与 Java 后端共享的 JWT 签名密钥,必须与 backend-java 的 AIIMAGE_JWT_SECRET 完全一致 AIIMAGE_JWT_SECRET=please-change-this-secret-please-rotate-at-least-32-bytes diff --git a/app/assets/appearance-patent-26DWDYwd.css b/app/assets/appearance-patent-26DWDYwd.css new file mode 100644 index 0000000..13af4ca --- /dev/null +++ b/app/assets/appearance-patent-26DWDYwd.css @@ -0,0 +1 @@ +.module-page[data-v-aad32577]{min-height:100vh;background:#1a1a1a}.main-content[data-v-aad32577]{display:flex;height:calc(100vh - 56px);min-height:calc(100vh - 56px)}.left-panel[data-v-aad32577]{width:400px;background:#1e1e1e;padding:20px;overflow-y:auto;border-right:1px solid #2a2a2a}.right-panel[data-v-aad32577]{flex:1;min-width:0;background:#1a1a1a;display:flex;flex-direction:column}.section-title[data-v-aad32577],.subsection-title[data-v-aad32577]{font-size:13px;color:#bbb;margin-bottom:10px}.upload-zone[data-v-aad32577]{border:1px dashed #3a3a3a;border-radius:10px;padding:18px;background:#252525;margin-bottom:18px}.hint[data-v-aad32577],.loading-msg[data-v-aad32577],.files[data-v-aad32577],.muted[data-v-aad32577]{color:#888;font-size:12px;line-height:1.5}.link[data-v-aad32577]{color:#6ea8fe;text-decoration:none}.link[data-v-aad32577]:hover{color:#9fc5ff}.btns[data-v-aad32577],.run-row[data-v-aad32577]{display:flex;gap:10px;flex-wrap:wrap}.opt-btn[data-v-aad32577],.btn-run[data-v-aad32577],.btn-delete[data-v-aad32577],.download[data-v-aad32577]{border:none;cursor:pointer;border-radius:7px}.opt-btn[data-v-aad32577]{padding:8px 14px;color:#ccc;background:#2a2a2a;border:1px solid #3a3a3a}.btn-run[data-v-aad32577]{padding:10px 18px;color:#fff;background:#3498db;font-weight:600}.btn-queue[data-v-aad32577]{background:#27ae60}.btn-run[data-v-aad32577]:disabled{opacity:.55;cursor:not-allowed}.selected-files[data-v-aad32577]{margin-top:14px;color:#999;font-size:12px;word-break:break-all}.selected-files span[data-v-aad32577]{display:block;margin:4px 0}.prompt-card[data-v-aad32577],.secret-card[data-v-aad32577]{margin-bottom:18px}.secret-card[data-v-aad32577]{display:none}.prompt-input[data-v-aad32577]{width:100%;box-sizing:border-box;resize:vertical;min-height:180px;padding:10px 12px;border:1px solid #333;border-radius:8px;background:#202020;color:#d8d8d8;font-size:12px;line-height:1.6;outline:none}.secret-input[data-v-aad32577]{width:100%;box-sizing:border-box;height:38px;padding:0 12px;border:1px solid #333;border-radius:8px;background:#202020;color:#d8d8d8;font-size:12px;outline:none}.prompt-input[data-v-aad32577]:focus,.secret-input[data-v-aad32577]:focus{border-color:#3498db}.parse-card[data-v-aad32577],.queue-payload[data-v-aad32577]{margin-top:14px;padding:12px;border:1px solid #2a2a2a;border-radius:8px;background:#202020;color:#b8c1cc;font-size:12px}.queue-payload[data-v-aad32577]{max-height:220px;overflow:auto;color:#8fd3ff;white-space:pre-wrap}.panel-header[data-v-aad32577]{padding:16px 20px;border-bottom:1px solid #2a2a2a;font-size:15px;font-weight:600;color:#ddd}.task-list-wrap[data-v-aad32577]{flex:1;padding:16px 20px;overflow:auto}.clean-result-summary[data-v-aad32577]{display:grid;grid-template-columns:repeat(4,minmax(0,1fr));gap:12px;margin-bottom:16px}.summary-card[data-v-aad32577]{padding:14px 16px;border:1px solid #2a2a2a;border-radius:8px;background:#1e1e1e}.summary-card strong[data-v-aad32577]{display:block;margin-top:8px;color:#eaf4ff;font-size:22px}.summary-label[data-v-aad32577]{color:#8d8d8d;font-size:12px}.result-list-wrap[data-v-aad32577]{border:1px solid #2a2a2a;border-radius:8px;background:#1e1e1e;min-height:180px;margin:0 0 16px}.result-list-header[data-v-aad32577]{display:flex;justify-content:space-between;padding:12px 16px;border-bottom:1px solid #2a2a2a;color:#ddd;font-size:14px}.empty-tasks[data-v-aad32577]{color:#666;font-size:13px;padding:18px;text-align:center}.result-table[data-v-aad32577]{--el-table-bg-color: #222;--el-table-tr-bg-color: #222;--el-table-header-bg-color: #2a2a2a;--el-table-text-color: #ccc;--el-table-border-color: #333}.task-list[data-v-aad32577]{list-style:none;margin:0;padding:12px}.task-item[data-v-aad32577]{display:flex;align-items:flex-start;justify-content:space-between;gap:12px;padding:12px 14px;border:1px solid #2a2a2a;border-radius:8px;margin-bottom:8px;background:#222}.left[data-v-aad32577]{flex:1;min-width:0}.id[data-v-aad32577]{color:#e0e0e0;font-size:13px;font-weight:600}.task-right[data-v-aad32577]{display:flex;gap:8px;align-items:center;flex-wrap:wrap}.status[data-v-aad32577]{padding:4px 10px;border-radius:6px;font-size:12px}.status.success[data-v-aad32577]{background:#2ecc712e;color:#2ecc71}.status.failed[data-v-aad32577]{background:#e74c3c2e;color:#ff6b6b}.status.running[data-v-aad32577]{background:#3498db2e;color:#3498db}.status.pending[data-v-aad32577]{background:#95a5a62e;color:#bdc3c7}.result-hint[data-v-aad32577]{margin-top:6px;color:#e0b96d}.file-progress[data-v-aad32577]{margin-top:8px;max-width:520px}.file-progress-meta[data-v-aad32577]{display:flex;justify-content:space-between;gap:12px;color:#d8c278;font-size:12px}.file-progress-track[data-v-aad32577]{margin-top:5px;height:8px;border-radius:999px;overflow:hidden;background:#303030;border:1px solid #3b3b3b}.file-progress-bar[data-v-aad32577]{height:100%;border-radius:inherit;background:linear-gradient(90deg,#4aa3ff,#f0c75e);transition:width .25s ease}.download[data-v-aad32577]{padding:6px 10px;color:#d6ecff;background:#3498db2e}.btn-delete[data-v-aad32577]{padding:6px 10px;color:#ff8f8f;background:#e74c3c1f}@media(max-width:1100px){.main-content[data-v-aad32577]{flex-direction:column;height:auto}.left-panel[data-v-aad32577]{width:100%;border-right:none;border-bottom:1px solid #2a2a2a}.clean-result-summary[data-v-aad32577]{grid-template-columns:repeat(2,minmax(0,1fr))}} diff --git a/app/assets/appearance-patent-Cb_Fx4da.css b/app/assets/appearance-patent-Cb_Fx4da.css deleted file mode 100644 index 02ec43b..0000000 --- a/app/assets/appearance-patent-Cb_Fx4da.css +++ /dev/null @@ -1 +0,0 @@ -.module-page[data-v-95ba428f]{min-height:100vh;background:#1a1a1a}.main-content[data-v-95ba428f]{display:flex;height:calc(100vh - 56px);min-height:calc(100vh - 56px)}.left-panel[data-v-95ba428f]{width:400px;background:#1e1e1e;padding:20px;overflow-y:auto;border-right:1px solid #2a2a2a}.right-panel[data-v-95ba428f]{flex:1;min-width:0;background:#1a1a1a;display:flex;flex-direction:column}.section-title[data-v-95ba428f],.subsection-title[data-v-95ba428f]{font-size:13px;color:#bbb;margin-bottom:10px}.upload-zone[data-v-95ba428f]{border:1px dashed #3a3a3a;border-radius:10px;padding:18px;background:#252525;margin-bottom:18px}.hint[data-v-95ba428f],.loading-msg[data-v-95ba428f],.files[data-v-95ba428f],.muted[data-v-95ba428f]{color:#888;font-size:12px;line-height:1.5}.link[data-v-95ba428f]{color:#6ea8fe;text-decoration:none}.link[data-v-95ba428f]:hover{color:#9fc5ff}.btns[data-v-95ba428f],.run-row[data-v-95ba428f]{display:flex;gap:10px;flex-wrap:wrap}.opt-btn[data-v-95ba428f],.btn-run[data-v-95ba428f],.btn-delete[data-v-95ba428f],.download[data-v-95ba428f]{border:none;cursor:pointer;border-radius:7px}.opt-btn[data-v-95ba428f]{padding:8px 14px;color:#ccc;background:#2a2a2a;border:1px solid #3a3a3a}.btn-run[data-v-95ba428f]{padding:10px 18px;color:#fff;background:#3498db;font-weight:600}.btn-queue[data-v-95ba428f]{background:#27ae60}.btn-run[data-v-95ba428f]:disabled{opacity:.55;cursor:not-allowed}.selected-files[data-v-95ba428f]{margin-top:14px;color:#999;font-size:12px;word-break:break-all}.selected-files span[data-v-95ba428f]{display:block;margin:4px 0}.prompt-card[data-v-95ba428f],.secret-card[data-v-95ba428f]{margin-bottom:18px}.secret-card[data-v-95ba428f]{display:none}.prompt-input[data-v-95ba428f]{width:100%;box-sizing:border-box;resize:vertical;min-height:180px;padding:10px 12px;border:1px solid #333;border-radius:8px;background:#202020;color:#d8d8d8;font-size:12px;line-height:1.6;outline:none}.secret-input[data-v-95ba428f]{width:100%;box-sizing:border-box;height:38px;padding:0 12px;border:1px solid #333;border-radius:8px;background:#202020;color:#d8d8d8;font-size:12px;outline:none}.prompt-input[data-v-95ba428f]:focus,.secret-input[data-v-95ba428f]:focus{border-color:#3498db}.parse-card[data-v-95ba428f],.queue-payload[data-v-95ba428f]{margin-top:14px;padding:12px;border:1px solid #2a2a2a;border-radius:8px;background:#202020;color:#b8c1cc;font-size:12px}.queue-payload[data-v-95ba428f]{max-height:220px;overflow:auto;color:#8fd3ff;white-space:pre-wrap}.panel-header[data-v-95ba428f]{padding:16px 20px;border-bottom:1px solid #2a2a2a;font-size:15px;font-weight:600;color:#ddd}.task-list-wrap[data-v-95ba428f]{flex:1;padding:16px 20px;overflow:auto}.clean-result-summary[data-v-95ba428f]{display:grid;grid-template-columns:repeat(4,minmax(0,1fr));gap:12px;margin-bottom:16px}.summary-card[data-v-95ba428f]{padding:14px 16px;border:1px solid #2a2a2a;border-radius:8px;background:#1e1e1e}.summary-card strong[data-v-95ba428f]{display:block;margin-top:8px;color:#eaf4ff;font-size:22px}.summary-label[data-v-95ba428f]{color:#8d8d8d;font-size:12px}.result-list-wrap[data-v-95ba428f]{border:1px solid #2a2a2a;border-radius:8px;background:#1e1e1e;min-height:180px;margin:0 0 16px}.result-list-header[data-v-95ba428f]{display:flex;justify-content:space-between;padding:12px 16px;border-bottom:1px solid #2a2a2a;color:#ddd;font-size:14px}.empty-tasks[data-v-95ba428f]{color:#666;font-size:13px;padding:18px;text-align:center}.result-table[data-v-95ba428f]{--el-table-bg-color: #222;--el-table-tr-bg-color: #222;--el-table-header-bg-color: #2a2a2a;--el-table-text-color: #ccc;--el-table-border-color: #333}.task-list[data-v-95ba428f]{list-style:none;margin:0;padding:12px}.task-item[data-v-95ba428f]{display:flex;align-items:flex-start;justify-content:space-between;gap:12px;padding:12px 14px;border:1px solid #2a2a2a;border-radius:8px;margin-bottom:8px;background:#222}.left[data-v-95ba428f]{flex:1;min-width:0}.id[data-v-95ba428f]{color:#e0e0e0;font-size:13px;font-weight:600}.task-right[data-v-95ba428f]{display:flex;gap:8px;align-items:center;flex-wrap:wrap}.status[data-v-95ba428f]{padding:4px 10px;border-radius:6px;font-size:12px}.status.success[data-v-95ba428f]{background:#2ecc712e;color:#2ecc71}.status.failed[data-v-95ba428f]{background:#e74c3c2e;color:#ff6b6b}.status.running[data-v-95ba428f]{background:#3498db2e;color:#3498db}.status.pending[data-v-95ba428f]{background:#95a5a62e;color:#bdc3c7}.result-hint[data-v-95ba428f]{margin-top:6px;color:#e0b96d}.file-progress[data-v-95ba428f]{margin-top:8px;max-width:520px}.file-progress-meta[data-v-95ba428f]{display:flex;justify-content:space-between;gap:12px;color:#d8c278;font-size:12px}.file-progress-track[data-v-95ba428f]{margin-top:5px;height:8px;border-radius:999px;overflow:hidden;background:#303030;border:1px solid #3b3b3b}.file-progress-bar[data-v-95ba428f]{height:100%;border-radius:inherit;background:linear-gradient(90deg,#4aa3ff,#f0c75e);transition:width .25s ease}.download[data-v-95ba428f]{padding:6px 10px;color:#d6ecff;background:#3498db2e}.btn-delete[data-v-95ba428f]{padding:6px 10px;color:#ff8f8f;background:#e74c3c1f}@media(max-width:1100px){.main-content[data-v-95ba428f]{flex-direction:column;height:auto}.left-panel[data-v-95ba428f]{width:100%;border-right:none;border-bottom:1px solid #2a2a2a}.clean-result-summary[data-v-95ba428f]{grid-template-columns:repeat(2,minmax(0,1fr))}} diff --git a/app/assets/appearance-patent.js b/app/assets/appearance-patent.js index d18e58a..cff41de 100644 --- a/app/assets/appearance-patent.js +++ b/app/assets/appearance-patent.js @@ -1 +1 @@ -import{d as Je,o as qe,M as je,a as i,c,b as Oe,B as Ve,e as a,F as se,r as te,t as r,w as We,H as Ye,f as _,O as he,n as Qe,g as v,h as q,i as j,E as p,W as Xe,X as Ze,Y as es,V as ye,Z as ss,$ as ts,a0 as as,s as ns,a1 as ls,a2 as rs,a3 as ke,a4 as os,_ as us,q as is,u as cs}from"./java-modules-WSWRTEaR.js";import{e as ds}from"./brand-5GpfAyHD.js";import{c as fs}from"./categorized-timers-JPA-olTr.js";const ps={class:"page-shell module-page"},vs={class:"main-content"},gs={class:"left-panel"},hs={class:"upload-zone"},ys={class:"selected-files"},ks={key:0},Is={class:"prompt-card"},_s={class:"run-row"},Ps=["disabled"],ws=["disabled"],Cs={key:0,class:"parse-card"},Ss={key:1,class:"queue-payload"},bs={class:"right-panel"},ms={class:"task-list-wrap"},Ns={class:"clean-result-summary"},Fs={class:"summary-card"},Ts={class:"summary-card"},Rs={class:"summary-card"},As={class:"summary-card"},Es={class:"result-list-wrap"},xs={key:0,class:"empty-tasks"},Us={key:1,class:"task-list clean-result-list"},Ds={class:"left"},$s={class:"id"},Ms={class:"files"},zs={class:"files"},Gs={class:"files"},Bs={class:"files"},Hs={key:0,class:"files result-hint"},Ls={key:1,class:"file-progress"},Ks={class:"file-progress-meta"},Js={class:"file-progress-track"},qs={class:"task-right"},js={class:"status running"},Os=["onClick"],Vs={class:"result-list-wrap"},Ws={key:0,class:"empty-tasks"},Ys={key:1,class:"task-list clean-result-list"},Qs={class:"left"},Xs={class:"id"},Zs={class:"files"},et={class:"files"},st={class:"files"},tt={key:0,class:"files"},at={key:1,class:"files result-hint"},nt={key:2,class:"file-progress"},lt={class:"file-progress-meta"},rt={class:"file-progress-track"},ot={key:3,class:"files"},ut={class:"task-right"},it=["onClick"],ct=["onClick"],dt=3e3,ft=Je({__name:"BrandAppearancePatentTab",setup(vt){const D=v([]),F=v([]),P=v(null),T=v(null),$=v(""),M=v(!1),z=v(!1),C=v(""),f=v([]),u=v([]),h=v(null),O=v(!1);let R=null,ae=0,A=!1;const E=fs("appearance-patent"),x=v({pendingTaskCount:0,processedTaskCount:0,successTaskCount:0,failedTaskCount:0}),g=v([]),w=v({}),b=q(()=>P.value||T.value),V=q(()=>{const e=P.value;return!e?.taskId||g.value.some(t=>t.taskId===e.taskId)?null:{taskId:e.taskId,sourceFilename:e.sourceFilename,rowCount:e.acceptedRows,taskStatus:"PENDING",success:!1}}),ne=q(()=>{const e=new Set([...f.value,...u.value]),s=g.value.filter(n=>{const l=k(n);return n.taskId!=null&&(e.has(n.taskId)||l==="RUNNING"||l==="PENDING")}).map(n=>re(n));for(const[n,l]of Object.entries(w.value)){const o=Number(n);!Number.isFinite(o)||o<=0||!e.has(o)&&k(l)!=="RUNNING"&&k(l)!=="PENDING"||s.some(d=>d.taskId===o)||s.push(re(l))}return V.value?s.some(n=>n.taskId===V.value?.taskId)?s:[V.value,...s]:s}),le=q(()=>g.value.filter(e=>{const s=k(e);return s!=="RUNNING"&&s!=="PENDING"}));function re(e){if(e.taskId==null)return e;const s=w.value[e.taskId];return s?{...e,...s,sourceFilename:s.sourceFilename||e.sourceFilename,resultFilename:s.resultFilename||e.resultFilename,rowCount:s.rowCount??e.rowCount,createdAt:s.createdAt||e.createdAt}:e}function Ie(){return $.value.trim()}function _e(){return $.value.trim()}function G(){return ke("appearance-patent").trim()}function oe(){return ke("appearance-patent-token").trim()}function Pe(){return typeof window<"u"&&window.localStorage.getItem("uid")||"0"}function W(){return`appearance-patent:tasks:${Pe()}`}function we(e){return{taskId:e.taskId,totalRows:e.totalRows,acceptedRows:e.acceptedRows,groupCount:e.groupCount,droppedRows:e.droppedRows}}function S(){if(typeof window>"u")return;const e=Array.from(new Set([...f.value,...u.value]));e.length?window.localStorage.setItem(W(),JSON.stringify(e)):window.localStorage.removeItem(W())}function Ce(){if(!(typeof window>"u"))try{const e=window.localStorage.getItem(W()),s=e?JSON.parse(e):[];f.value=Array.isArray(s)?s.filter(t=>typeof t=="number"&&t>0):[]}catch{f.value=[]}}function B(e){if(!e)return"-";const s=new Date(e);if(Number.isNaN(s.getTime()))return e;const t=s.getFullYear(),n=String(s.getMonth()+1).padStart(2,"0"),l=String(s.getDate()).padStart(2,"0"),o=String(s.getHours()).padStart(2,"0"),d=String(s.getMinutes()).padStart(2,"0"),y=String(s.getSeconds()).padStart(2,"0");return`${t}-${n}-${l} ${o}:${d}:${y}`}async function ue(e){const s=j();if(!s?.upload_file_to_java)throw new Error("当前桌面端未提供文件上传能力");const t=[];for(const n of e){const l=typeof n=="string"?n:n.absolutePath,o=typeof n=="string"?void 0:n.relativePath,d=await s.upload_file_to_java(l,o);if(!d?.success||!d.data)throw new Error(d?.error||d?.message||`上传失败:${l}`);t.push(d.data)}return t}async function Se(){const e=j();if(!e?.select_brand_xlsx_files||!e.upload_file_to_java){p.warning("当前环境不支持文件选择或上传");return}const s=await e.select_brand_xlsx_files();if(!s?.length)return;const t=await ue(s);F.value=t,D.value=t.map(n=>n.relativePath||n.originalFilename||n.fileKey),P.value=null,T.value=null,C.value=""}async function be(){const e=j();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}const n=await ue(t.items);F.value=n,D.value=t.items.map(l=>l.relativePath||l.absolutePath),P.value=null,T.value=null,C.value="",p.success(`已选择文件夹内 ${t.items.length} 个 Excel 文件`)}catch(s){p.error(s instanceof Error?s.message:"选择失败")}}async function me(){if(!F.value.length){p.warning("请先选择 Excel");return}if(!G()){p.warning("请先在左上角设置中填写外观专利密钥");return}M.value=!0;try{const e=F.value.map(t=>({fileKey:t.fileKey,originalFilename:t.originalFilename,relativePath:t.relativePath})),s=await Xe(e,Ie(),G(),oe());P.value=s,T.value=null,C.value="",p.success(`解析完成,共 ${s.groupCount||0} 组 / ${s.acceptedRows} 条`)}catch(e){p.error(e instanceof Error?e.message:"解析失败")}finally{M.value=!1}}async function Ne(){const e=j(),s=P.value,t=s?.taskId;if(!t){p.warning("请先解析文件");return}if(!e?.enqueue_json){p.error("当前环境未启用 pywebview enqueue_json");return}if(!G()){p.warning("请先在左上角设置中填写外观专利密钥");return}z.value=!0;try{const l=(await Ze(t)).groups||[],o={type:"appearance-patent-run",ts:Date.now(),data:{taskId:t,prompt:_e(),api_key:G(),patent_token:oe(),sourceFileCount:s.sourceFileCount||0,totalRows:s.totalRows||0,acceptedRows:s.acceptedRows||0,groupCount:s.groupCount||0,groups:l.map(y=>({sourceFileKey:y.sourceFileKey||"",sourceFilename:y.sourceFilename||"",groupKey:y.groupKey||"",baseId:y.baseId||"",displayId:y.displayId||"",items:(y.items||[]).map(I=>({sourceFileKey:I.sourceFileKey||"",sourceFilename:I.sourceFilename||"",rowToken:I.rowToken||"",groupKey:I.groupKey||"",id:I.displayId,asin:I.asin,country:I.country,sku:I.sku||"",url:I.url||"",title:I.title||""}))}))}};C.value=`任务 ${t} 已准备推送,共 ${l.length} 组`,await es(t);const d=await e.enqueue_json(o);if(!d?.success){p.error(d?.error||"推送失败");return}T.value=we(s),Te(t),Fe(),await H(),await L({force:!0}),p.success("已推送到 Python 队列")}finally{z.value=!1}}function Fe(){P.value=null,C.value=""}function Te(e){f.value.includes(e)||(f.value=[...f.value,e],S()),m()}function Y(e){if(f.value=f.value.filter(s=>s!==e),!u.value.includes(e)){const s={...w.value};delete s[e],w.value=s}S()}function Re(e){u.value.includes(e)||(u.value=[...u.value,e],S()),m()}function Q(e){if(u.value=u.value.filter(s=>s!==e),!f.value.includes(e)){const s={...w.value};delete s[e],w.value=s}S()}function m(){A||h.value==null&&Ae(!0)}function ie(){h.value!=null&&(E.clearTimer("task-poll",h.value),h.value=null)}function Ae(e=!1){if(A)return;if(h.value!=null){if(!e)return;E.clearTimer("task-poll",h.value),h.value=null}const s=async()=>{h.value=null,!A&&(!f.value.length&&!u.value.length||(await Ee(),!A&&h.value==null&&(f.value.length||u.value.length)&&(h.value=E.setTimeout("task-poll",s,ye()))))};e?s():h.value=E.setTimeout("task-poll",s,ye())}async function Ee(){if(O.value||!f.value.length&&!u.value.length){!f.value.length&&!u.value.length&&ie();return}O.value=!0;try{let e=!1,s=!1;const t=Array.from(new Set([...f.value,...u.value]));if(t.length){const n=await os(t,{force:u.value.length>0});for(const l of n.items||[]){const o=l.task;if(!o?.id)continue;const d=l.items?.[0];if(d&&(w.value={...w.value,[o.id]:d},xe(d)),(o.status==="SUCCESS"||o.status==="FAILED")&&(Y(o.id),e=!0,o.status==="SUCCESS"?Re(o.id):Q(o.id)),d&&o.status==="SUCCESS"&&u.value.includes(o.id)){const y=(d.fileStatus||"").toUpperCase();(d.fileReady||y==="SUCCESS"||y==="FAILED")&&(Q(o.id),e=!0,s=!0)}}n.missingTaskIds?.length&&(n.missingTaskIds.forEach(l=>{Y(l),Q(l)}),s=!0,e=!0)}e&&await H(),s&&await L(),Ue()}finally{O.value=!1}}function xe(e){if(!e.taskId&&!e.resultId)return;const s=g.value.findIndex(t=>e.resultId!=null&&t.resultId===e.resultId||e.taskId!=null&&t.taskId===e.taskId);s>=0?g.value[s]={...g.value[s],...e}:g.value=[e,...g.value]}function Ue(){if(!u.value.length)return;const e=[];for(const s of u.value){const t=g.value.find(o=>o.taskId===s);if(!t){e.push(s);continue}const n=(t.taskStatus||"").toUpperCase(),l=(t.fileStatus||"").toUpperCase();n==="FAILED"||t.fileReady||l==="SUCCESS"||l==="FAILED"||n==="SUCCESS"&&e.push(s)}u.value=e,S(),u.value.length&&m()}function De(){const e=g.value.filter(s=>s.taskId!=null&&N(s)).map(s=>s.taskId);u.value=Array.from(new Set(e)),S(),u.value.length&&m()}function $e(){const e=g.value.filter(s=>s.taskId!=null&&k(s)==="RUNNING").map(s=>s.taskId);e.length&&(f.value=Array.from(new Set([...f.value,...e])),S(),m())}async function H(){x.value=await ss()}async function L(e={}){const s=Date.now();if(!(!e.force&&g.value.length&&s-ae{g.value=Me(g.value,t.items||[]),ae=Date.now()}).finally(()=>{R=null}),R)}function Me(e,s){const t=new Map,n=new Map;for(const l of e||[])l.taskId!=null&&t.set(l.taskId,l),l.resultId!=null&&n.set(l.resultId,l);return(s||[]).map(l=>{const o=l.resultId!=null?n.get(l.resultId):l.taskId!=null?t.get(l.taskId):void 0;return ze(o,l)})}function ze(e,s){return!e||!((k(e)==="RUNNING"||N(e)||U(e))&&!U(s)&&!ee(s))?s:{...s,fileJobId:s.fileJobId??e.fileJobId,fileStatus:s.fileStatus||e.fileStatus,fileError:s.fileError||e.fileError,fileReady:s.fileReady||e.fileReady,fileProgressPercent:X(s.fileProgressPercent)?s.fileProgressPercent:e.fileProgressPercent,fileProgressCurrent:X(s.fileProgressCurrent)?s.fileProgressCurrent:e.fileProgressCurrent,fileProgressTotal:X(s.fileProgressTotal)?s.fileProgressTotal:e.fileProgressTotal,fileProgressMessage:(s.fileProgressMessage||"").trim()?s.fileProgressMessage:e.fileProgressMessage}}function X(e){const s=Number(e);return Number.isFinite(s)&&s>0}function ce(e){const s=k(e);return s==="RUNNING"?"执行中":s==="PENDING"?"已解析待推送":N(e)?"结果生成中":Z(e)?"结果生成失败":s==="SUCCESS"||e.success?"已完成":s==="FAILED"?"失败":"等待中"}function Ge(e){const s=k(e);return s==="RUNNING"||N(e)?"running":Z(e)||s==="FAILED"?"failed":s==="SUCCESS"||e.success?"success":"pending"}function N(e){const s=k(e),t=(e.fileStatus||"").toUpperCase();return!(s!=="SUCCESS"||ee(e)||t==="FAILED"||t==="SUCCESS")}function k(e){return(e.taskStatus||"").toUpperCase()}function Z(e){return(e.fileStatus||"").toUpperCase()==="FAILED"}function K(e){if(Z(e))return e.fileError||"结果文件生成失败,请稍后重试或检查后端日志";if(U(e)||!N(e))return"";const s=(e.fileStatus||"").toUpperCase();return s==="RUNNING"?"任务已完成,正在等待 Coze 回流并组装结果文件,下载按钮稍后出现":s==="PENDING"?"任务已完成,结果文件已入队,正在等待后端开始处理":"任务已完成,正在生成结果文件,下载按钮稍后出现"}function ee(e){return!!(e.resultId&&(e.fileReady||e.downloadUrl))}function J(e){const s=Number(e.fileProgressPercent||0);return Number.isFinite(s)&&s>0?Math.max(0,Math.min(100,Math.round(s))):ve(e)?pe(e)==="PENDING"?6:10:0}function de(e,s){const t=(e.fileProgressMessage||"").trim();return t?Be(t):s}function Be(e){const s=e.match(/^Coze submitted (\d+) batches, completed (\d+), waiting for results$/);if(s)return`已提交 Coze ${s[1]} 批,已完成 ${s[2]} 批,等待结果回流`;const t=e.match(/^Coze completed (\d+) batches, waiting for remaining Python data$/);return t?`Coze 已完成 ${t[1]} 批,等待 Python 继续回传数据`:{"Receiving Python data, waiting for 50 rows before Coze":"正在接收 Python 数据,累计 50 条后提交 Coze","Submitting Coze":"正在提交 Coze","Coze submitted, waiting for result":"Coze 已提交,等待结果回流","Waiting for Python upload":"等待 Python 继续回传数据","Coze results ready, assembling xlsx":"Coze 结果已回流,正在组装 xlsx","Assembling xlsx":"正在组装 xlsx","Uploading result file":"正在上传结果文件","Result file generated":"结果文件已生成"}[e]||e}function U(e){return(k(e)==="RUNNING"||N(e))&&(fe(e)||ve(e))}function fe(e){return He(e)||Number(e.fileProgressPercent||0)>0||!!(e.fileProgressMessage||"").trim()}function He(e){return(e.fileProgressTotal||0)>0}function pe(e){return(e.fileStatus||"").toUpperCase()}function Le(e){const s=pe(e);return e.fileJobId!=null||s==="PENDING"||s==="RUNNING"}function ve(e){return Le(e)&&!fe(e)}async function Ke(e){if(!e.resultId)return;const s=e.downloadUrl||as(e.resultId),t=e.resultFilename||`${e.sourceFilename||"appearance-patent"}.xlsx`,n=await ns(s,t,`appearance-patent:${e.resultId}`);n.success?p.success(`已保存: ${n.path||t}`):n.error&&n.error!=="用户取消"&&p.error(n.error)}async function ge(e){try{e.taskStatus==="RUNNING"&&e.taskId?(await ls(e.taskId),Y(e.taskId)):e.resultId&&await rs(e.resultId),await H(),await L({force:!0}),p.success("已删除")}catch(s){p.error(s instanceof Error?s.message:"删除失败")}}return qe(async()=>{Ce(),await Promise.all([H().catch(()=>{}),L().catch(()=>{})]),$e(),De(),(f.value.length||u.value.length)&&m()}),je(()=>{A=!0,ie(),E.clearScope()}),(e,s)=>(i(),c("div",ps,[Oe(Ve,{active:"appearance-patent"}),a("div",vs,[a("aside",gs,[s[3]||(s[3]=a("div",{class:"section-title"},"上传文件",-1)),a("div",hs,[s[1]||(s[1]=a("div",{class:"hint"},"选择 Excel 后点击解析,后端会提取 id、ASIN、国家,并保留整数 id 与子数据的第一条。",-1)),a("div",{class:"btns"},[a("button",{type:"button",class:"opt-btn",onClick:Se},"选择 Excel"),a("button",{type:"button",class:"opt-btn",onClick:be},"选择文件夹")]),a("div",ys,[D.value.length?(i(!0),c(se,{key:1},te(D.value,t=>(i(),c("span",{key:t},r(t),1))),128)):(i(),c("span",ks,"暂未选择文件"))])]),a("div",Is,[s[2]||(s[2]=a("div",{class:"section-title"},"新增条件要求",-1)),We(a("textarea",{"onUpdate:modelValue":s[0]||(s[0]=t=>$.value=t),class:"prompt-input",rows:"8",placeholder:"可选,留空则不向 Coze 传 prompt 字段;填写后将作为 prompt 透传"},null,512),[[Ye,$.value]])]),a("div",_s,[a("button",{type:"button",class:"btn-run",disabled:M.value||!F.value.length,onClick:me},r(M.value?"解析中...":"解析并创建任务"),9,Ps),a("button",{type:"button",class:"btn-run btn-queue",disabled:z.value||!P.value?.taskId,onClick:Ne},r(z.value?"推送中...":"推送到 Python 队列"),9,ws)]),s[4]||(s[4]=a("p",{class:"loading-msg"},"Python 回传字段按 asin、国家、url、标题提交;后端接收分片后累计 50 条调用 Coze,当前任务区展示 Coze 回流和结果组装进度。",-1)),b.value?(i(),c("div",Cs,[a("div",null,"任务 ID:"+r(b.value.taskId),1),a("div",null,"总行数:"+r(b.value.totalRows)+",有效:"+r(b.value.acceptedRows)+",分组:"+r(b.value.groupCount||0)+",过滤:"+r(b.value.droppedRows),1)])):_("",!0),C.value?(i(),c("pre",Ss,r(C.value),1)):_("",!0)]),a("section",bs,[s[12]||(s[12]=a("div",{class:"panel-header"},"外观专利检测",-1)),a("div",ms,[a("div",Ns,[a("div",Fs,[s[5]||(s[5]=a("span",{class:"summary-label"},"运行中任务",-1)),a("strong",null,r(x.value.pendingTaskCount),1)]),a("div",Ts,[s[6]||(s[6]=a("span",{class:"summary-label"},"已结束任务",-1)),a("strong",null,r(x.value.processedTaskCount),1)]),a("div",Rs,[s[7]||(s[7]=a("span",{class:"summary-label"},"成功任务",-1)),a("strong",null,r(x.value.successTaskCount),1)]),a("div",As,[s[8]||(s[8]=a("span",{class:"summary-label"},"失败任务",-1)),a("strong",null,r(x.value.failedTaskCount),1)])]),s[11]||(s[11]=a("div",{class:"subsection-title"},"匹配任务",-1)),a("div",Es,[s[9]||(s[9]=a("div",{class:"result-list-header"},[a("span",null,"当前任务")],-1)),ne.value.length?(i(),c("ul",Us,[(i(!0),c(se,null,te(ne.value,t=>(i(),c("li",{key:`cur-${t.taskId}`,class:"task-item"},[a("div",Ds,[a("span",$s,r(t.sourceFilename||"外观专利检测"),1),a("div",Ms,"任务 ID:"+r(t.taskId),1),a("div",zs,"开始时间:"+r(B(t.startedAt||t.createdAt)),1),a("div",Gs,"结束时间:"+r(B(t.finishedAt)),1),a("div",Bs,"行数:"+r(t.rowCount??"-"),1),K(t)?(i(),c("div",Hs,r(K(t)),1)):_("",!0),U(t)?(i(),c("div",Ls,[a("div",Ks,[a("span",null,r(de(t,"处理中")),1),a("span",null,r(J(t))+"%",1)]),a("div",Js,[a("div",{class:"file-progress-bar",style:he({width:`${J(t)}%`})},null,4)])])):_("",!0)]),a("div",qs,[a("span",js,r(ce(t)),1),a("button",{type:"button",class:"btn-delete",onClick:n=>ge(t)},"删除",8,Os)])]))),128))])):(i(),c("div",xs,"暂无当前任务"))]),a("div",Vs,[s[10]||(s[10]=a("div",{class:"result-list-header"},[a("span",null,"历史记录")],-1)),le.value.length?(i(),c("ul",Ys,[(i(!0),c(se,null,te(le.value,t=>(i(),c("li",{key:`his-${t.resultId}-${t.taskId}`,class:"task-item"},[a("div",Qs,[a("span",Xs,r(t.sourceFilename||"外观专利检测"),1),a("div",Zs,"任务 ID:"+r(t.taskId??"-"),1),a("div",et,"开始时间:"+r(B(t.startedAt||t.createdAt)),1),a("div",st,"结束时间:"+r(B(t.finishedAt)),1),t.resultFilename?(i(),c("div",tt,r(t.resultFilename||"下载结果"),1)):_("",!0),K(t)?(i(),c("div",at,r(K(t)),1)):_("",!0),U(t)?(i(),c("div",nt,[a("div",lt,[a("span",null,r(de(t,"结果生成中")),1),a("span",null,r(J(t))+"%",1)]),a("div",rt,[a("div",{class:"file-progress-bar",style:he({width:`${J(t)}%`})},null,4)])])):_("",!0),t.error?(i(),c("div",ot,"错误:"+r(t.error),1)):_("",!0)]),a("div",ut,[a("span",{class:Qe(["status",Ge(t)])},r(ce(t)),3),ee(t)?(i(),c("button",{key:0,type:"button",class:"download",onClick:n=>Ke(t)},"下载",8,it)):_("",!0),t.resultId?(i(),c("button",{key:1,type:"button",class:"btn-delete",onClick:n=>ge(t)},"删除",8,ct)):_("",!0)])]))),128))])):(i(),c("div",Ws,"暂无历史记录"))])])])])]))}}),pt=us(ft,[["__scopeId","data-v-95ba428f"]]);is(pt).use(cs).mount("#app"); +import{d as qe,o as je,M as Oe,a as i,c,b as Ve,B as We,e as a,F as se,r as te,t as r,w as Ye,H as Qe,f as _,O as ke,n as Xe,g as v,h as j,i as O,E as p,W as Ze,X as es,Y as ss,V as Ie,Z as ts,$ as as,a0 as ns,s as ls,a1 as rs,a2 as os,a3 as _e,a4 as us,_ as is,q as cs,u as ds}from"./java-modules-WSWRTEaR.js";import{e as fs}from"./brand-5GpfAyHD.js";import{c as ps}from"./categorized-timers-JPA-olTr.js";const vs={class:"page-shell module-page"},gs={class:"main-content"},hs={class:"left-panel"},ys={class:"upload-zone"},ks={class:"selected-files"},Is={key:0},_s={class:"prompt-card"},Ps={class:"run-row"},ws=["disabled"],Cs=["disabled"],Ss={key:0,class:"parse-card"},bs={key:1,class:"queue-payload"},ms={class:"right-panel"},Fs={class:"task-list-wrap"},Ns={class:"clean-result-summary"},Ts={class:"summary-card"},Rs={class:"summary-card"},As={class:"summary-card"},Es={class:"summary-card"},xs={class:"result-list-wrap"},Us={key:0,class:"empty-tasks"},Ds={key:1,class:"task-list clean-result-list"},$s={class:"left"},Ms={class:"id"},zs={class:"files"},Gs={class:"files"},Ls={class:"files"},Bs={class:"files"},Hs={key:0,class:"files result-hint"},Ks={key:1,class:"file-progress"},Js={class:"file-progress-meta"},qs={class:"file-progress-track"},js={class:"task-right"},Os={class:"status running"},Vs=["onClick"],Ws={class:"result-list-wrap"},Ys={key:0,class:"empty-tasks"},Qs={key:1,class:"task-list clean-result-list"},Xs={class:"left"},Zs={class:"id"},et={class:"files"},st={class:"files"},tt={class:"files"},at={key:0,class:"files"},nt={key:1,class:"files result-hint"},lt={key:2,class:"file-progress"},rt={class:"file-progress-meta"},ot={class:"file-progress-track"},ut={key:3,class:"files"},it={class:"task-right"},ct=["onClick"],dt=["onClick"],ft=3e3,pt=qe({__name:"BrandAppearancePatentTab",setup(gt){const D=v([]),N=v([]),P=v(null),T=v(null),$=v(""),M=v(!1),z=v(!1),C=v(""),f=v([]),u=v([]),h=v(null),V=v(!1);let R=null,ae=0,A=!1;const E=ps("appearance-patent"),x=v({pendingTaskCount:0,processedTaskCount:0,successTaskCount:0,failedTaskCount:0}),g=v([]),w=v({}),m=j(()=>P.value||T.value),W=j(()=>{const e=P.value;return!e?.taskId||g.value.some(t=>t.taskId===e.taskId)?null:{taskId:e.taskId,sourceFilename:e.sourceFilename,rowCount:e.acceptedRows,taskStatus:"PENDING",success:!1}}),ne=j(()=>{const e=new Set([...f.value,...u.value]),s=g.value.filter(n=>{const l=k(n);return n.taskId!=null&&(e.has(n.taskId)||l==="RUNNING"||l==="PENDING")}).map(n=>re(n));for(const[n,l]of Object.entries(w.value)){const o=Number(n);!Number.isFinite(o)||o<=0||!e.has(o)&&k(l)!=="RUNNING"&&k(l)!=="PENDING"||s.some(d=>d.taskId===o)||s.push(re(l))}return W.value?s.some(n=>n.taskId===W.value?.taskId)?s:[W.value,...s]:s}),le=j(()=>g.value.filter(e=>{const s=k(e);return s!=="RUNNING"&&s!=="PENDING"}));function re(e){if(e.taskId==null)return e;const s=w.value[e.taskId];return s?{...e,...s,sourceFilename:s.sourceFilename||e.sourceFilename,resultFilename:s.resultFilename||e.resultFilename,rowCount:s.rowCount??e.rowCount,createdAt:s.createdAt||e.createdAt}:e}function Pe(){return $.value.trim()}function we(){return $.value.trim()}function G(){return _e("appearance-patent").trim()}function oe(){return _e("appearance-patent-token").trim()}function Ce(){return typeof window<"u"&&window.localStorage.getItem("uid")||"0"}function Y(){return`appearance-patent:tasks:${Ce()}`}function Se(e){return{taskId:e.taskId,totalRows:e.totalRows,acceptedRows:e.acceptedRows,groupCount:e.groupCount,droppedRows:e.droppedRows}}function S(){if(typeof window>"u")return;const e=Array.from(new Set([...f.value,...u.value]));e.length?window.localStorage.setItem(Y(),JSON.stringify(e)):window.localStorage.removeItem(Y())}function be(){if(!(typeof window>"u"))try{const e=window.localStorage.getItem(Y()),s=e?JSON.parse(e):[];f.value=Array.isArray(s)?s.filter(t=>typeof t=="number"&&t>0):[]}catch{f.value=[]}}function L(e){if(!e)return"-";const s=new Date(e);if(Number.isNaN(s.getTime()))return e;const t=s.getFullYear(),n=String(s.getMonth()+1).padStart(2,"0"),l=String(s.getDate()).padStart(2,"0"),o=String(s.getHours()).padStart(2,"0"),d=String(s.getMinutes()).padStart(2,"0"),y=String(s.getSeconds()).padStart(2,"0");return`${t}-${n}-${l} ${o}:${d}:${y}`}async function ue(e){const s=O();if(!s?.upload_file_to_java)throw new Error("当前桌面端未提供文件上传能力");const t=[];for(const n of e){const l=typeof n=="string"?n:n.absolutePath,o=typeof n=="string"?void 0:n.relativePath,d=await s.upload_file_to_java(l,o);if(!d?.success||!d.data)throw new Error(d?.error||d?.message||`上传失败:${l}`);t.push(d.data)}return t}async function me(){const e=O();if(!e?.select_brand_xlsx_files||!e.upload_file_to_java){p.warning("当前环境不支持文件选择或上传");return}const s=await e.select_brand_xlsx_files();if(!s?.length)return;const t=await ue(s);N.value=t,D.value=t.map(n=>n.relativePath||n.originalFilename||n.fileKey),P.value=null,T.value=null,C.value=""}async function Fe(){const e=O();if(!e?.select_brand_folder){p.warning("当前环境不支持文件夹选择,请在本地客户端中打开");return}try{const s=await e.select_brand_folder();if(!s)return;const t=await fs(s);if(!t.success||!t.items?.length){p.warning(t.error||"该文件夹下没有可用的 xlsx 文件");return}const n=await ue(t.items);N.value=n,D.value=t.items.map(l=>l.relativePath||l.absolutePath),P.value=null,T.value=null,C.value="",p.success(`已选择文件夹内 ${t.items.length} 个 Excel 文件`)}catch(s){p.error(s instanceof Error?s.message:"选择失败")}}async function Ne(){if(!N.value.length){p.warning("请先选择 Excel");return}if(!G()){p.warning("请先在左上角设置中填写外观专利密钥");return}M.value=!0;try{const e=N.value.map(t=>({fileKey:t.fileKey,originalFilename:t.originalFilename,relativePath:t.relativePath})),s=await Ze(e,Pe(),G(),oe());P.value=s,T.value=null,C.value="",p.success(`解析完成,共 ${s.groupCount||0} 组 / ${s.acceptedRows} 条`)}catch(e){p.error(e instanceof Error?e.message:"解析失败")}finally{M.value=!1}}async function Te(){const e=O(),s=P.value,t=s?.taskId;if(!t){p.warning("请先解析文件");return}if(!e?.enqueue_json){p.error("当前环境未启用 pywebview enqueue_json");return}if(!G()){p.warning("请先在左上角设置中填写外观专利密钥");return}z.value=!0;try{const l=(await es(t)).groups||[],o={type:"appearance-patent-run",ts:Date.now(),data:{taskId:t,prompt:we(),api_key:G(),patent_token:oe(),sourceFileCount:s.sourceFileCount||0,totalRows:s.totalRows||0,acceptedRows:s.acceptedRows||0,groupCount:s.groupCount||0,groups:l.map(y=>({sourceFileKey:y.sourceFileKey||"",sourceFilename:y.sourceFilename||"",groupKey:y.groupKey||"",baseId:y.baseId||"",displayId:y.displayId||"",items:(y.items||[]).map(I=>({sourceFileKey:I.sourceFileKey||"",sourceFilename:I.sourceFilename||"",rowToken:I.rowToken||"",groupKey:I.groupKey||"",id:I.displayId,asin:I.asin,country:I.country,sku:I.sku||"",url:I.url||"",title:I.title||""}))}))}};C.value=`任务 ${t} 已准备推送,共 ${l.length} 组`,await ss(t);const d=await e.enqueue_json(o);if(!d?.success){p.error(d?.error||"推送失败");return}T.value=Se(s),Ae(t),Re(),await B(),await H({force:!0}),p.success("已推送到 Python 队列")}finally{z.value=!1}}function Re(){P.value=null,C.value=""}function Ae(e){f.value.includes(e)||(f.value=[...f.value,e],S()),F()}function Q(e){if(f.value=f.value.filter(s=>s!==e),!u.value.includes(e)){const s={...w.value};delete s[e],w.value=s}S()}function Ee(e){u.value.includes(e)||(u.value=[...u.value,e],S()),F()}function X(e){if(u.value=u.value.filter(s=>s!==e),!f.value.includes(e)){const s={...w.value};delete s[e],w.value=s}S()}function F(){A||h.value==null&&xe(!0)}function ie(){h.value!=null&&(E.clearTimer("task-poll",h.value),h.value=null)}function xe(e=!1){if(A)return;if(h.value!=null){if(!e)return;E.clearTimer("task-poll",h.value),h.value=null}const s=async()=>{h.value=null,!A&&(!f.value.length&&!u.value.length||(await Ue(),!A&&h.value==null&&(f.value.length||u.value.length)&&(h.value=E.setTimeout("task-poll",s,Ie()))))};e?s():h.value=E.setTimeout("task-poll",s,Ie())}async function Ue(){if(V.value||!f.value.length&&!u.value.length){!f.value.length&&!u.value.length&&ie();return}V.value=!0;try{let e=!1,s=!1;const t=Array.from(new Set([...f.value,...u.value]));if(t.length){const n=await us(t,{force:u.value.length>0});for(const l of n.items||[]){const o=l.task;if(!o?.id)continue;const d=l.items?.[0];if(d&&(w.value={...w.value,[o.id]:d},De(d)),(o.status==="SUCCESS"||o.status==="FAILED")&&(Q(o.id),e=!0,d&&b(d)?Ee(o.id):X(o.id)),d&&(o.status==="SUCCESS"||o.status==="FAILED")&&u.value.includes(o.id)){const y=(d.fileStatus||"").toUpperCase();(d.fileReady||y==="SUCCESS"||y==="FAILED")&&(X(o.id),e=!0,s=!0)}}n.missingTaskIds?.length&&(n.missingTaskIds.forEach(l=>{Q(l),X(l)}),s=!0,e=!0)}e&&await B(),s&&await H(),$e()}finally{V.value=!1}}function De(e){if(!e.taskId&&!e.resultId)return;const s=g.value.findIndex(t=>e.resultId!=null&&t.resultId===e.resultId||e.taskId!=null&&t.taskId===e.taskId);s>=0?g.value[s]={...g.value[s],...e}:g.value=[e,...g.value]}function $e(){if(!u.value.length)return;const e=[];for(const s of u.value){const t=g.value.find(o=>o.taskId===s);if(!t){e.push(s);continue}const n=(t.taskStatus||"").toUpperCase(),l=(t.fileStatus||"").toUpperCase();t.fileReady||l==="SUCCESS"||l==="FAILED"||(n==="SUCCESS"||n==="FAILED")&&e.push(s)}u.value=e,S(),u.value.length&&F()}function Me(){const e=g.value.filter(s=>s.taskId!=null&&b(s)).map(s=>s.taskId);u.value=Array.from(new Set(e)),S(),u.value.length&&F()}function ze(){const e=g.value.filter(s=>s.taskId!=null&&k(s)==="RUNNING").map(s=>s.taskId);e.length&&(f.value=Array.from(new Set([...f.value,...e])),S(),F())}async function B(){x.value=await ts()}async function H(e={}){const s=Date.now();if(!(!e.force&&g.value.length&&s-ae{g.value=Ge(g.value,t.items||[]),ae=Date.now()}).finally(()=>{R=null}),R)}function Ge(e,s){const t=new Map,n=new Map;for(const l of e||[])l.taskId!=null&&t.set(l.taskId,l),l.resultId!=null&&n.set(l.resultId,l);return(s||[]).map(l=>{const o=l.resultId!=null?n.get(l.resultId):l.taskId!=null?t.get(l.taskId):void 0;return Le(o,l)})}function Le(e,s){return!e||!((k(e)==="RUNNING"||b(e)||U(e))&&!U(s)&&!J(s))?s:{...s,fileJobId:s.fileJobId??e.fileJobId,fileStatus:s.fileStatus||e.fileStatus,fileError:s.fileError||e.fileError,fileReady:s.fileReady||e.fileReady,fileProgressPercent:Z(s.fileProgressPercent)?s.fileProgressPercent:e.fileProgressPercent,fileProgressCurrent:Z(s.fileProgressCurrent)?s.fileProgressCurrent:e.fileProgressCurrent,fileProgressTotal:Z(s.fileProgressTotal)?s.fileProgressTotal:e.fileProgressTotal,fileProgressMessage:(s.fileProgressMessage||"").trim()?s.fileProgressMessage:e.fileProgressMessage}}function Z(e){const s=Number(e);return Number.isFinite(s)&&s>0}function ce(e){const s=k(e);return s==="RUNNING"?"执行中":s==="PENDING"?"已解析待推送":b(e)?"结果生成中":ee(e)?"结果生成失败":de(e)||s==="SUCCESS"||e.success?"已完成":s==="FAILED"?"失败":"等待中"}function Be(e){const s=k(e);return s==="RUNNING"||b(e)?"running":de(e)?"success":ee(e)||s==="FAILED"?"failed":s==="SUCCESS"||e.success?"success":"pending"}function b(e){const s=k(e),t=(e.fileStatus||"").toUpperCase();return s!=="SUCCESS"&&s!=="FAILED"||J(e)||t==="FAILED"||t==="SUCCESS"?!1:s==="SUCCESS"||ge(e)}function k(e){return(e.taskStatus||"").toUpperCase()}function ee(e){return(e.fileStatus||"").toUpperCase()==="FAILED"}function K(e){if(ee(e))return e.fileError||"结果文件生成失败,请稍后重试或检查后端日志";if(U(e)||!b(e))return"";const s=(e.fileStatus||"").toUpperCase();return s==="RUNNING"?"任务已完成,正在等待 Coze 回流并组装结果文件,下载按钮稍后出现":s==="PENDING"?"任务已完成,结果文件已入队,正在等待后端开始处理":"任务已完成,正在生成结果文件,下载按钮稍后出现"}function J(e){return!!(e.resultId&&(e.fileReady||e.downloadUrl))}function de(e){return J(e)}function q(e){const s=Number(e.fileProgressPercent||0);return Number.isFinite(s)&&s>0?Math.max(0,Math.min(100,Math.round(s))):he(e)?ve(e)==="PENDING"?6:10:0}function fe(e,s){const t=(e.fileProgressMessage||"").trim();return t?He(t):s}function He(e){const s=e.match(/^Coze submitted (\d+) batches, completed (\d+), waiting for results$/);if(s)return`已提交 Coze ${s[1]} 批,已完成 ${s[2]} 批,等待结果回流`;const t=e.match(/^Coze completed (\d+) batches, waiting for remaining Python data$/);return t?`Coze 已完成 ${t[1]} 批,等待 Python 继续回传数据`:{"Receiving Python data, waiting for 50 rows before Coze":"正在接收 Python 数据,累计 50 条后提交 Coze","Submitting Coze":"正在提交 Coze","Coze submitted, waiting for result":"Coze 已提交,等待结果回流","Waiting for Python upload":"等待 Python 继续回传数据","Coze results ready, assembling xlsx":"Coze 结果已回流,正在组装 xlsx","Assembling xlsx":"正在组装 xlsx","Uploading result file":"正在上传结果文件","Result file generated":"结果文件已生成"}[e]||e}function U(e){return(k(e)==="RUNNING"||b(e))&&(pe(e)||he(e))}function pe(e){return Ke(e)||Number(e.fileProgressPercent||0)>0||!!(e.fileProgressMessage||"").trim()}function Ke(e){return(e.fileProgressTotal||0)>0}function ve(e){return(e.fileStatus||"").toUpperCase()}function ge(e){const s=ve(e);return e.fileJobId!=null||s==="PENDING"||s==="RUNNING"}function he(e){return ge(e)&&!pe(e)}async function Je(e){if(!e.resultId)return;const s=e.downloadUrl||ns(e.resultId),t=e.resultFilename||`${e.sourceFilename||"appearance-patent"}.xlsx`,n=await ls(s,t,`appearance-patent:${e.resultId}`);n.success?p.success(`已保存: ${n.path||t}`):n.error&&n.error!=="用户取消"&&p.error(n.error)}async function ye(e){try{e.taskStatus==="RUNNING"&&e.taskId?(await rs(e.taskId),Q(e.taskId)):e.resultId&&await os(e.resultId),await B(),await H({force:!0}),p.success("已删除")}catch(s){p.error(s instanceof Error?s.message:"删除失败")}}return je(async()=>{be(),await Promise.all([B().catch(()=>{}),H().catch(()=>{})]),ze(),Me(),(f.value.length||u.value.length)&&F()}),Oe(()=>{A=!0,ie(),E.clearScope()}),(e,s)=>(i(),c("div",vs,[Ve(We,{active:"appearance-patent"}),a("div",gs,[a("aside",hs,[s[3]||(s[3]=a("div",{class:"section-title"},"上传文件",-1)),a("div",ys,[s[1]||(s[1]=a("div",{class:"hint"},"选择 Excel 后点击解析,后端会提取 id、ASIN、国家,并保留整数 id 与子数据的第一条。",-1)),a("div",{class:"btns"},[a("button",{type:"button",class:"opt-btn",onClick:me},"选择 Excel"),a("button",{type:"button",class:"opt-btn",onClick:Fe},"选择文件夹")]),a("div",ks,[D.value.length?(i(!0),c(se,{key:1},te(D.value,t=>(i(),c("span",{key:t},r(t),1))),128)):(i(),c("span",Is,"暂未选择文件"))])]),a("div",_s,[s[2]||(s[2]=a("div",{class:"section-title"},"新增条件要求",-1)),Ye(a("textarea",{"onUpdate:modelValue":s[0]||(s[0]=t=>$.value=t),class:"prompt-input",rows:"8",placeholder:"可选,留空则不向 Coze 传 prompt 字段;填写后将作为 prompt 透传"},null,512),[[Qe,$.value]])]),a("div",Ps,[a("button",{type:"button",class:"btn-run",disabled:M.value||!N.value.length,onClick:Ne},r(M.value?"解析中...":"解析并创建任务"),9,ws),a("button",{type:"button",class:"btn-run btn-queue",disabled:z.value||!P.value?.taskId,onClick:Te},r(z.value?"推送中...":"推送到 Python 队列"),9,Cs)]),s[4]||(s[4]=a("p",{class:"loading-msg"},"Python 回传字段按 asin、国家、url、标题提交;后端接收分片后累计 50 条调用 Coze,当前任务区展示 Coze 回流和结果组装进度。",-1)),m.value?(i(),c("div",Ss,[a("div",null,"任务 ID:"+r(m.value.taskId),1),a("div",null,"总行数:"+r(m.value.totalRows)+",有效:"+r(m.value.acceptedRows)+",分组:"+r(m.value.groupCount||0)+",过滤:"+r(m.value.droppedRows),1)])):_("",!0),C.value?(i(),c("pre",bs,r(C.value),1)):_("",!0)]),a("section",ms,[s[12]||(s[12]=a("div",{class:"panel-header"},"外观专利检测",-1)),a("div",Fs,[a("div",Ns,[a("div",Ts,[s[5]||(s[5]=a("span",{class:"summary-label"},"运行中任务",-1)),a("strong",null,r(x.value.pendingTaskCount),1)]),a("div",Rs,[s[6]||(s[6]=a("span",{class:"summary-label"},"已结束任务",-1)),a("strong",null,r(x.value.processedTaskCount),1)]),a("div",As,[s[7]||(s[7]=a("span",{class:"summary-label"},"成功任务",-1)),a("strong",null,r(x.value.successTaskCount),1)]),a("div",Es,[s[8]||(s[8]=a("span",{class:"summary-label"},"失败任务",-1)),a("strong",null,r(x.value.failedTaskCount),1)])]),s[11]||(s[11]=a("div",{class:"subsection-title"},"匹配任务",-1)),a("div",xs,[s[9]||(s[9]=a("div",{class:"result-list-header"},[a("span",null,"当前任务")],-1)),ne.value.length?(i(),c("ul",Ds,[(i(!0),c(se,null,te(ne.value,t=>(i(),c("li",{key:`cur-${t.taskId}`,class:"task-item"},[a("div",$s,[a("span",Ms,r(t.sourceFilename||"外观专利检测"),1),a("div",zs,"任务 ID:"+r(t.taskId),1),a("div",Gs,"开始时间:"+r(L(t.startedAt||t.createdAt)),1),a("div",Ls,"结束时间:"+r(L(t.finishedAt)),1),a("div",Bs,"行数:"+r(t.rowCount??"-"),1),K(t)?(i(),c("div",Hs,r(K(t)),1)):_("",!0),U(t)?(i(),c("div",Ks,[a("div",Js,[a("span",null,r(fe(t,"处理中")),1),a("span",null,r(q(t))+"%",1)]),a("div",qs,[a("div",{class:"file-progress-bar",style:ke({width:`${q(t)}%`})},null,4)])])):_("",!0)]),a("div",js,[a("span",Os,r(ce(t)),1),a("button",{type:"button",class:"btn-delete",onClick:n=>ye(t)},"删除",8,Vs)])]))),128))])):(i(),c("div",Us,"暂无当前任务"))]),a("div",Ws,[s[10]||(s[10]=a("div",{class:"result-list-header"},[a("span",null,"历史记录")],-1)),le.value.length?(i(),c("ul",Qs,[(i(!0),c(se,null,te(le.value,t=>(i(),c("li",{key:`his-${t.resultId}-${t.taskId}`,class:"task-item"},[a("div",Xs,[a("span",Zs,r(t.sourceFilename||"外观专利检测"),1),a("div",et,"任务 ID:"+r(t.taskId??"-"),1),a("div",st,"开始时间:"+r(L(t.startedAt||t.createdAt)),1),a("div",tt,"结束时间:"+r(L(t.finishedAt)),1),t.resultFilename?(i(),c("div",at,r(t.resultFilename||"下载结果"),1)):_("",!0),K(t)?(i(),c("div",nt,r(K(t)),1)):_("",!0),U(t)?(i(),c("div",lt,[a("div",rt,[a("span",null,r(fe(t,"结果生成中")),1),a("span",null,r(q(t))+"%",1)]),a("div",ot,[a("div",{class:"file-progress-bar",style:ke({width:`${q(t)}%`})},null,4)])])):_("",!0),t.error?(i(),c("div",ut,"错误:"+r(t.error),1)):_("",!0)]),a("div",it,[a("span",{class:Xe(["status",Be(t)])},r(ce(t)),3),J(t)?(i(),c("button",{key:0,type:"button",class:"download",onClick:n=>Je(t)},"下载",8,ct)):_("",!0),t.resultId?(i(),c("button",{key:1,type:"button",class:"btn-delete",onClick:n=>ye(t)},"删除",8,dt)):_("",!0)])]))),128))])):(i(),c("div",Ys,"暂无历史记录"))])])])])]))}}),vt=is(pt,[["__scopeId","data-v-aad32577"]]);cs(vt).use(ds).mount("#app"); diff --git a/app/config.py b/app/config.py index 86a8d2f..182b87e 100644 --- a/app/config.py +++ b/app/config.py @@ -22,7 +22,7 @@ proxy_url = os.getenv("proxy_url") proxy_mode = int(os.getenv("proxy_mode",1)) client_name=os.getenv("client_name") + ".exe" -JAVA_API_BASE = os.getenv("java_api_base", "http://api.aishufu.top:18080/").rstrip("/") +JAVA_API_BASE = os.getenv("java_api_base", "http://api.aishufu.top:18080/").rstrip("/") # 紫鸟浏览器配置 from urllib.parse import unquote @@ -32,7 +32,7 @@ ZN_USERNAME = unquote(ZN_USERNAME) ZN_PASSWORD = os.getenv("zn_password", "#20zsg25") # 删除品牌API配置 -DELETE_BRAND_API_BASE = os.getenv("DELETE_BRAND_API_BASE", JAVA_API_BASE).rstrip("/") +DELETE_BRAND_API_BASE = os.getenv("DELETE_BRAND_API_BASE", JAVA_API_BASE).rstrip("/") cache_path = "./user_data" @@ -52,7 +52,7 @@ os.environ['OSS_ACCESS_KEY_ID'] = accessKeyId os.environ['OSS_ACCESS_KEY_SECRET'] = accessKeySecret -debug = True +debug = False version = "1.0.13" APP_UPDATE_URL = f"{_base_url}/api/version/latest" # 检测更新接口地址,返回 { "file_url": "...", "version": "x.x.x" } os.environ['APP_VERSION'] = version diff --git a/backend-java/pom.xml b/backend-java/pom.xml index c7edf68..ea9047b 100644 --- a/backend-java/pom.xml +++ b/backend-java/pom.xml @@ -171,6 +171,13 @@ + + org.apache.maven.plugins + maven-surefire-plugin + + -XX:+EnableDynamicAgentLoading -Xshare:off + + diff --git a/backend-java/src/main/java/com/nanri/aiimage/config/TaskOperationLockConfig.java b/backend-java/src/main/java/com/nanri/aiimage/config/TaskOperationLockConfig.java index 66fd751..0c47319 100644 --- a/backend-java/src/main/java/com/nanri/aiimage/config/TaskOperationLockConfig.java +++ b/backend-java/src/main/java/com/nanri/aiimage/config/TaskOperationLockConfig.java @@ -64,6 +64,9 @@ public class TaskOperationLockConfig implements WebMvcConfigurer { return true; } String uri = request.getRequestURI(); + if ("POST".equals(request.getMethod()) && TASK_RESULT_PATH.matcher(uri == null ? "" : uri).matches()) { + return true; + } Matcher matcher = TASK_PATH.matcher(uri == null ? "" : uri); if (!matcher.matches()) { return true; diff --git a/backend-java/src/main/java/com/nanri/aiimage/modules/appearancepatent/client/AppearancePatentCozeClient.java b/backend-java/src/main/java/com/nanri/aiimage/modules/appearancepatent/client/AppearancePatentCozeClient.java index 5248765..b79124c 100644 --- a/backend-java/src/main/java/com/nanri/aiimage/modules/appearancepatent/client/AppearancePatentCozeClient.java +++ b/backend-java/src/main/java/com/nanri/aiimage/modules/appearancepatent/client/AppearancePatentCozeClient.java @@ -587,9 +587,11 @@ public class AppearancePatentCozeClient { } if (isNoTitleRisk(result)) { row.setTitleRisk(NO_INFRINGEMENT); - } else { + } else if (shouldCheckBrand(result)) { BrandCheckClient.BrandCheckBatchResult brandCheck = brandCheckClient.checkTitleText(result.title(), "Terms"); row.setTitleRisk(buildTitleRisk(result.title(), brandCheck)); + } else { + row.setTitleRisk(""); } row.setAppearanceRisk(result.appearance()); row.setPatentRisk(result.patent()); @@ -603,8 +605,13 @@ public class AppearancePatentCozeClient { private boolean isNoTitleRisk(CozeResult result) { return result != null - && "无".equals(normalize(result.title())) - && "无".equals(normalize(result.titleReason())); + && "无".equals(normalize(result.title())); + } + + private boolean shouldCheckBrand(CozeResult result) { + return result != null + && !normalize(result.title()).isBlank() + && !normalize(result.appearance()).isBlank(); } String buildTitleRisk(String cozeTitle, BrandCheckClient.BrandCheckBatchResult brandCheck) { diff --git a/backend-java/src/main/java/com/nanri/aiimage/modules/appearancepatent/model/dto/AppearancePatentResultRowDto.java b/backend-java/src/main/java/com/nanri/aiimage/modules/appearancepatent/model/dto/AppearancePatentResultRowDto.java index b59ea53..80deb27 100644 --- a/backend-java/src/main/java/com/nanri/aiimage/modules/appearancepatent/model/dto/AppearancePatentResultRowDto.java +++ b/backend-java/src/main/java/com/nanri/aiimage/modules/appearancepatent/model/dto/AppearancePatentResultRowDto.java @@ -83,7 +83,7 @@ public class AppearancePatentResultRowDto { private String patentReason; @JsonAlias({"score", "Score", "评分"}) - @Schema(description = "Coze 回流的评分(外观维度),透传到最终 xlsx 第一张 sheet 的“评分”列。", accessMode = Schema.AccessMode.READ_ONLY) + @Schema(description = "Coze 回流的评分(外观维度),仅保留回流值,不写入最终 xlsx。", accessMode = Schema.AccessMode.READ_ONLY) private String score; /** diff --git a/backend-java/src/main/java/com/nanri/aiimage/modules/appearancepatent/model/vo/AppearancePatentParsedRowVo.java b/backend-java/src/main/java/com/nanri/aiimage/modules/appearancepatent/model/vo/AppearancePatentParsedRowVo.java index 5684eaa..87e7c27 100644 --- a/backend-java/src/main/java/com/nanri/aiimage/modules/appearancepatent/model/vo/AppearancePatentParsedRowVo.java +++ b/backend-java/src/main/java/com/nanri/aiimage/modules/appearancepatent/model/vo/AppearancePatentParsedRowVo.java @@ -3,6 +3,9 @@ package com.nanri.aiimage.modules.appearancepatent.model.vo; import io.swagger.v3.oas.annotations.media.Schema; import lombok.Data; +import java.util.LinkedHashMap; +import java.util.Map; + @Data @Schema(description = "外观专利检测解析出的单行数据") public class AppearancePatentParsedRowVo { @@ -44,4 +47,7 @@ public class AppearancePatentParsedRowVo { @Schema(description = "商品标题。", example = "Women Floral Dress Summer Casual") private String title; + + @Schema(description = "该 Excel 行的原始列值映射。最终生成 xlsx 时可从这里读取卖家名称、品牌等扩展字段。", example = "{\"id\":\"2_1\",\"asin\":\"B0CJ8SNXXV\",\"国家\":\"英国\"}") + private Map values = new LinkedHashMap<>(); } diff --git a/backend-java/src/main/java/com/nanri/aiimage/modules/appearancepatent/service/AppearancePatentTaskService.java b/backend-java/src/main/java/com/nanri/aiimage/modules/appearancepatent/service/AppearancePatentTaskService.java index ef0e97b..53f8269 100644 --- a/backend-java/src/main/java/com/nanri/aiimage/modules/appearancepatent/service/AppearancePatentTaskService.java +++ b/backend-java/src/main/java/com/nanri/aiimage/modules/appearancepatent/service/AppearancePatentTaskService.java @@ -125,12 +125,13 @@ public class AppearancePatentTaskService { "id", "asin", "国家", + "卖家名称", + "品牌", "价格", "标题维度(商标)", "外观维度(外观设计专利)", "结论", - "状态", - "评分" + "状态" ); private final LocalFileStorageService localFileStorageService; @@ -420,13 +421,7 @@ public class AppearancePatentTaskService { } public void submitResult(Long taskId, AppearancePatentSubmitResultRequest request) { - TaskDistributedLockService.LockHandle lockHandle = acquireTaskLock(taskId, TASK_LOCK_WAIT_MILLIS); - if (lockHandle == null) { - throw new BusinessException(40902, "任务正在处理上一批结果,请稍后重试"); - } - try (lockHandle) { - submitResultLocked(taskId, request); - } + submitResultLocked(taskId, request); } private void submitResultLocked(Long taskId, AppearancePatentSubmitResultRequest request) { @@ -1328,7 +1323,8 @@ public class AppearancePatentTaskService { } } } - boolean failed = finalError != null && !finalError.isBlank(); + boolean failedByConclusion = hasInfringingPersistedConclusion(task.getId()); + boolean failed = finalError != null && !finalError.isBlank() || failedByConclusion; boolean waitingForAssemble = !failed && assembleWorkbook && !shouldAssembleSynchronously(); task.setStatus(waitingForAssemble ? STATUS_RUNNING : (failed ? STATUS_FAILED : STATUS_SUCCESS)); task.setSuccessFileCount(failed ? 0 : 1); @@ -2407,7 +2403,8 @@ public class AppearancePatentTaskService { saveFileBuildProgress(task, job, totalProgressUnits, totalProgressUnits - 1, "正在上传结果文件"); fileResultMapper.updateById(result); boolean taskAlreadyFailed = STATUS_FAILED.equals(task.getStatus()) - || (task.getErrorMessage() != null && !task.getErrorMessage().isBlank()); + || (task.getErrorMessage() != null && !task.getErrorMessage().isBlank()) + || hasInfringingPersistedConclusion(task.getId()); String existingError = task.getErrorMessage(); task.setStatus(taskAlreadyFailed ? STATUS_FAILED : STATUS_SUCCESS); task.setSuccessFileCount(taskAlreadyFailed ? 0 : 1); @@ -3300,9 +3297,9 @@ public class AppearancePatentTaskService { headerStyle.setFont(font); List resultHeaders = new ArrayList<>(RESULT_HEADERS); - resultHeaders.add(4, "标题"); - resultHeaders.add(5, "图片链接"); - resultHeaders.add(6, "sku"); + resultHeaders.add(6, "标题"); + resultHeaders.add(7, "图片链接"); + resultHeaders.add(8, "sku"); Row header = sheet.createRow(0); for (int i = 0; i < resultHeaders.size(); i++) { Cell cell = header.createCell(i); @@ -3314,24 +3311,21 @@ public class AppearancePatentTaskService { List rowsToWrite = receivedRows == null ? List.of() : receivedRows; for (AppearancePatentParsedRowVo parsedRow : rowsToWrite) { AppearancePatentResultRowDto resultRow = findResultRow(parsedRow, resultMap); - String missingReason = ""; - if (resultRow == null) { - missingReason = hasPromptFields(parsedRow) ? "未匹配到检测结果" : "未送检:缺少标题或图片"; - } Row row = sheet.createRow(rowIndex++); int col = 0; row.createCell(col++).setCellValue(firstNonBlank(parsedRow.getDisplayId(), parsedRow.getSourceId())); row.createCell(col++).setCellValue(firstNonBlank(parsedRow.getAsin(), "")); row.createCell(col++).setCellValue(firstNonBlank(parsedRow.getCountry(), "")); + row.createCell(col++).setCellValue(readValueByHeader(parsedRow, "卖家名称", "卖家名", "卖家", "店铺名称", "店铺名", "seller name", "seller_name", "seller-name", "sellername", "store name", "shop name")); + row.createCell(col++).setCellValue(readValueByHeader(parsedRow, "品牌", "brand")); row.createCell(col++).setCellValue(firstNonBlank(parsedRow.getPrice(), "")); row.createCell(col++).setCellValue(resultRow == null ? firstNonBlank(parsedRow.getTitle(), "") : firstNonBlank(resultRow.getTitle(), parsedRow.getTitle())); row.createCell(col++).setCellValue(resultRow == null ? firstNonBlank(parsedRow.getUrl(), "") : firstNonBlank(resultRow.getUrl(), parsedRow.getUrl())); row.createCell(col++).setCellValue(resultRow == null ? firstNonBlank(parsedRow.getSku(), "") : firstNonBlank(resultRow.getSku(), parsedRow.getSku())); - row.createCell(col++).setCellValue(resultRow == null ? missingReason : userFacingCozeCellValue(resultRow, resultRow.getTitleRisk())); - row.createCell(col++).setCellValue(resultRow == null ? missingReason : userFacingCozeCellValue(resultRow, resultRow.getAppearanceRisk())); - row.createCell(col++).setCellValue(resultRow == null ? "未送检" : userFacingConclusion(resultRow)); - row.createCell(col++).setCellValue(resultRow == null ? "" : userFacingStatus(resultRow)); - row.createCell(col).setCellValue(resultRow == null ? "" : firstNonBlank(resultRow.getScore(), "")); + row.createCell(col++).setCellValue(resultRow == null ? "" : userFacingCozeCellValue(resultRow, resultRow.getTitleRisk())); + row.createCell(col++).setCellValue(resultRow == null ? "" : userFacingCozeCellValue(resultRow, resultRow.getAppearanceRisk())); + row.createCell(col++).setCellValue(resultRow == null ? "" : userFacingConclusion(resultRow)); + row.createCell(col).setCellValue(resultRow == null ? "" : userFacingStatus(resultRow)); } writeReasonSheet(workbook, headerStyle, rowsToWrite, resultMap); workbook.write(fos); @@ -3519,6 +3513,7 @@ public class AppearancePatentTaskService { vo.setSku(skuCol >= 0 ? cell(row, skuCol, formatter) : ""); vo.setUrl(urlCol >= 0 ? cell(row, urlCol, formatter) : ""); vo.setTitle(titleCol >= 0 ? cell(row, titleCol, formatter) : ""); + vo.setValues(readRowValues(row, headers, formatter)); parsedRows.add(new ParsedAppearanceRow(vo, statusCol >= 0 ? cell(row, statusCol, formatter) : "")); } List allRows = parsedRows.stream() @@ -3590,6 +3585,14 @@ public class AppearancePatentTaskService { return headers; } + private Map readRowValues(Row row, List headers, DataFormatter formatter) { + Map values = new LinkedHashMap<>(); + for (int i = 0; i < headers.size(); i++) { + values.put(headers.get(i), cell(row, i, formatter)); + } + return values; + } + private boolean isAppearanceResultWorkbook(List headers) { return hasHeader(headers, "状态") && (hasHeader(headers, "结论") @@ -3790,7 +3793,8 @@ public class AppearancePatentTaskService { vo.setDownloadUrl(null); attachFileJobState(vo, row, job); vo.setTaskStatus(task == null ? null : task.getStatus()); - vo.setSuccess(row.getSuccess() != null && row.getSuccess() == 1); + vo.setSuccess(row.getResultFileUrl() != null && !row.getResultFileUrl().isBlank() + || row.getSuccess() != null && row.getSuccess() == 1); vo.setError(row.getErrorMessage()); vo.setRowCount(row.getRowCount()); vo.setCreatedAt(fmt(row.getCreatedAt())); @@ -4143,6 +4147,51 @@ public class AppearancePatentTaskService { return normalize(id) + "::" + normalize(asin).toUpperCase(Locale.ROOT) + "::" + normalize(country); } + private String readValueByHeader(AppearancePatentParsedRowVo row, String... candidates) { + if (row == null || row.getValues() == null || row.getValues().isEmpty() || candidates == null) { + return ""; + } + for (Map.Entry entry : row.getValues().entrySet()) { + String header = normalize(entry.getKey()).toLowerCase(Locale.ROOT); + for (String candidate : candidates) { + String expected = normalize(candidate).toLowerCase(Locale.ROOT); + if (!expected.isBlank() && header.contains(expected)) { + return entry.getValue() == null ? "" : entry.getValue(); + } + } + } + return ""; + } + + private boolean hasInfringingPersistedConclusion(Long taskId) { + if (taskId == null) { + return false; + } + return loadPersistedResultRows(taskId).values().stream() + .map(AppearancePatentResultRowDto::getConclusion) + .anyMatch(this::isInfringingConclusion); + } + + private boolean isInfringingConclusion(String value) { + String normalized = normalize(value); + return normalized.contains("侵权") + && !isNoInfringementConclusion(normalized); + } + + private boolean isNoInfringementConclusion(String value) { + String normalized = normalize(value); + if (normalized.isBlank()) { + return false; + } + return normalized.contains("无侵权") + || normalized.contains("未侵权") + || normalized.contains("不侵权") + || normalized.contains("未发现") && normalized.contains("侵权") + || normalized.contains("未见") && normalized.contains("侵权") + || normalized.contains("不存在") && normalized.contains("侵权") + || normalized.contains("无明显") && normalized.contains("侵权"); + } + private String userFacingCozeCellValue(AppearancePatentResultRowDto row, String value) { String normalizedValue = normalize(value); if (!normalizedValue.isBlank() && !isTechnicalCozeFailure(normalizedValue)) { @@ -4180,11 +4229,13 @@ public class AppearancePatentTaskService { if (row == null) { return ""; } - String original = firstNonBlank(row.getStatus(), ""); - if (!row.isFailureSyntheticStatus()) { - return original; - } - return firstNonBlank(row.getError(), ""); + String conclusion = userFacingConclusion(row); + return resolveResultStatus(conclusion); + } + + static String resolveResultStatus(String conclusion) { + String normalized = conclusion == null ? "" : conclusion.trim(); + return normalized.isBlank() ? "\u5931\u8d25" : "\u6210\u529f"; } private boolean isTechnicalCozeFailure(String value) { diff --git a/backend-java/src/main/java/com/nanri/aiimage/modules/collectdata/controller/CollectDataController.java b/backend-java/src/main/java/com/nanri/aiimage/modules/collectdata/controller/CollectDataController.java index f3b334c..1bbcbae 100644 --- a/backend-java/src/main/java/com/nanri/aiimage/modules/collectdata/controller/CollectDataController.java +++ b/backend-java/src/main/java/com/nanri/aiimage/modules/collectdata/controller/CollectDataController.java @@ -3,12 +3,14 @@ package com.nanri.aiimage.modules.collectdata.controller; import com.nanri.aiimage.common.api.ApiResponse; import com.nanri.aiimage.modules.collectdata.model.dto.CollectDataCountryPreferenceSaveRequest; import com.nanri.aiimage.modules.collectdata.model.dto.CollectDataParseRequest; +import com.nanri.aiimage.modules.collectdata.model.dto.CollectDataSubmitResultRequest; import com.nanri.aiimage.modules.collectdata.model.dto.CollectDataTaskBatchRequest; import com.nanri.aiimage.modules.collectdata.model.vo.CollectDataCountryPreferenceVo; import com.nanri.aiimage.modules.collectdata.model.vo.CollectDataDashboardVo; import com.nanri.aiimage.modules.collectdata.model.vo.CollectDataHistoryVo; import com.nanri.aiimage.modules.collectdata.model.vo.CollectDataItemsPageVo; import com.nanri.aiimage.modules.collectdata.model.vo.CollectDataParseVo; +import com.nanri.aiimage.modules.collectdata.model.vo.CollectDataSubmitResultVo; import com.nanri.aiimage.modules.collectdata.model.vo.CollectDataTaskBatchVo; import com.nanri.aiimage.modules.collectdata.service.CollectDataService; import io.swagger.v3.oas.annotations.Operation; @@ -65,6 +67,15 @@ public class CollectDataController { return ApiResponse.success(service.getItemsPage(taskId, userId, page, pageSize)); } + @PostMapping("/tasks/{taskId}/result") + @Operation(summary = "Python 分片回传采集结果", description = "接收任意条数的 Python 回传行;后端按 ASIN 筛库、调用品牌检测并保存最终结果。") + public ApiResponse submitResult( + @Parameter(description = "采集任务 ID", required = true, example = "9001") + @PathVariable Long taskId, + @Valid @RequestBody CollectDataSubmitResultRequest request) { + return ApiResponse.success(service.submitResult(taskId, request)); + } + @GetMapping("/dashboard") @Operation(summary = "查询采集任务总览") public ApiResponse dashboard( diff --git a/backend-java/src/main/java/com/nanri/aiimage/modules/collectdata/model/dto/CollectDataSubmitResultRequest.java b/backend-java/src/main/java/com/nanri/aiimage/modules/collectdata/model/dto/CollectDataSubmitResultRequest.java new file mode 100644 index 0000000..71cdabc --- /dev/null +++ b/backend-java/src/main/java/com/nanri/aiimage/modules/collectdata/model/dto/CollectDataSubmitResultRequest.java @@ -0,0 +1,41 @@ +package com.nanri.aiimage.modules.collectdata.model.dto; + +import com.fasterxml.jackson.annotation.JsonAlias; +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import com.fasterxml.jackson.annotation.JsonProperty; +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; + +import java.util.ArrayList; +import java.util.List; + +@Data +@JsonIgnoreProperties(ignoreUnknown = true) +@Schema(description = "采集数据 Python 分片回传请求") +public class CollectDataSubmitResultRequest { + + @JsonProperty("submission_id") + @JsonAlias("submissionId") + @Schema(description = "分片提交批次标识;同一任务可固定一个 submission_id", example = "collect-11877") + private String submissionId; + + @JsonProperty("chunk_index") + @JsonAlias("chunkIndex") + @Schema(description = "分片序号,从 1 开始;未传时后端按当前任务顺序递增", example = "1") + private Integer chunkIndex; + + @JsonProperty("chunk_total") + @JsonAlias("chunkTotal") + @Schema(description = "总分片数;未知可不传,done=true 时以后端已收数量兜底", example = "10") + private Integer chunkTotal; + + @Schema(description = "是否为最后一次回传", example = "false") + private Boolean done; + + @Schema(description = "Python 端错误信息;传入时任务会标记失败") + private String error; + + @JsonAlias({"rows", "data", "items"}) + @Schema(description = "本分片回传数据行") + private List items = new ArrayList<>(); +} diff --git a/backend-java/src/main/java/com/nanri/aiimage/modules/collectdata/model/dto/CollectDataSubmitRowDto.java b/backend-java/src/main/java/com/nanri/aiimage/modules/collectdata/model/dto/CollectDataSubmitRowDto.java new file mode 100644 index 0000000..ac99732 --- /dev/null +++ b/backend-java/src/main/java/com/nanri/aiimage/modules/collectdata/model/dto/CollectDataSubmitRowDto.java @@ -0,0 +1,32 @@ +package com.nanri.aiimage.modules.collectdata.model.dto; + +import com.fasterxml.jackson.annotation.JsonAlias; +import com.fasterxml.jackson.annotation.JsonProperty; +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; + +@Data +@Schema(description = "采集数据 Python 回传行") +public class CollectDataSubmitRowDto { + + @JsonAlias({"品牌", "brand_name"}) + @Schema(description = "品牌", example = "Nike") + private String brand; + + @JsonAlias({"ASIN", "asin"}) + @Schema(description = "ASIN", example = "B0XXXXXXX") + private String asin; + + @JsonAlias({"价格", "price"}) + @Schema(description = "价格", example = "19.99") + private String price; + + @JsonProperty("seller_name") + @JsonAlias({"卖家名称", "sellerName", "seller"}) + @Schema(description = "卖家名称", example = "Demo Seller") + private String sellerName; + + @JsonAlias({"关键词", "key_word"}) + @Schema(description = "关键词", example = "phone case") + private String keyword; +} diff --git a/backend-java/src/main/java/com/nanri/aiimage/modules/collectdata/model/vo/CollectDataHistoryItemVo.java b/backend-java/src/main/java/com/nanri/aiimage/modules/collectdata/model/vo/CollectDataHistoryItemVo.java index 0f1f344..860ed94 100644 --- a/backend-java/src/main/java/com/nanri/aiimage/modules/collectdata/model/vo/CollectDataHistoryItemVo.java +++ b/backend-java/src/main/java/com/nanri/aiimage/modules/collectdata/model/vo/CollectDataHistoryItemVo.java @@ -37,6 +37,18 @@ public class CollectDataHistoryItemVo { @Schema(description = "落库行数") private Integer rowCount; + @Schema(description = "被数据去重总数据过滤的行数") + private Integer dedupeFilteredCount; + + @Schema(description = "被不符合 ASIN 数据库过滤的行数") + private Integer invalidFilteredCount; + + @Schema(description = "品牌检测不符合的行数") + private Integer brandRejectedCount; + + @Schema(description = "最终结果行数") + private Integer finalRowCount; + @Schema(description = "记录创建时间") private String createdAt; diff --git a/backend-java/src/main/java/com/nanri/aiimage/modules/collectdata/model/vo/CollectDataResultRowVo.java b/backend-java/src/main/java/com/nanri/aiimage/modules/collectdata/model/vo/CollectDataResultRowVo.java new file mode 100644 index 0000000..9d2d932 --- /dev/null +++ b/backend-java/src/main/java/com/nanri/aiimage/modules/collectdata/model/vo/CollectDataResultRowVo.java @@ -0,0 +1,12 @@ +package com.nanri.aiimage.modules.collectdata.model.vo; + +import lombok.Data; + +@Data +public class CollectDataResultRowVo { + private String brand; + private String asin; + private String price; + private String sellerName; + private String keyword; +} diff --git a/backend-java/src/main/java/com/nanri/aiimage/modules/collectdata/model/vo/CollectDataSubmitResultVo.java b/backend-java/src/main/java/com/nanri/aiimage/modules/collectdata/model/vo/CollectDataSubmitResultVo.java new file mode 100644 index 0000000..51056b9 --- /dev/null +++ b/backend-java/src/main/java/com/nanri/aiimage/modules/collectdata/model/vo/CollectDataSubmitResultVo.java @@ -0,0 +1,22 @@ +package com.nanri.aiimage.modules.collectdata.model.vo; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; + +@Data +@Schema(description = "采集数据回传处理结果") +public class CollectDataSubmitResultVo { + private Long taskId; + private Long resultId; + private Integer chunkIndex; + private Integer chunkTotal; + private Boolean done; + private Integer receivedRows; + private Integer currentChunkRows; + private Integer dedupeFilteredCount; + private Integer invalidFilteredCount; + private Integer brandRejectedCount; + private Integer brandQueryFailedCount; + private Integer finalRowCount; + private String taskStatus; +} diff --git a/backend-java/src/main/java/com/nanri/aiimage/modules/collectdata/service/CollectDataExcelAssemblyService.java b/backend-java/src/main/java/com/nanri/aiimage/modules/collectdata/service/CollectDataExcelAssemblyService.java new file mode 100644 index 0000000..8bbb044 --- /dev/null +++ b/backend-java/src/main/java/com/nanri/aiimage/modules/collectdata/service/CollectDataExcelAssemblyService.java @@ -0,0 +1,61 @@ +package com.nanri.aiimage.modules.collectdata.service; + +import com.nanri.aiimage.common.exception.BusinessException; +import com.nanri.aiimage.modules.collectdata.model.vo.CollectDataResultRowVo; +import lombok.extern.slf4j.Slf4j; +import org.apache.poi.ss.usermodel.Row; +import org.apache.poi.ss.usermodel.Sheet; +import org.apache.poi.xssf.streaming.SXSSFWorkbook; +import org.springframework.stereotype.Service; + +import java.io.File; +import java.io.FileOutputStream; +import java.util.List; + +@Service +@Slf4j +public class CollectDataExcelAssemblyService { + + private static final String[] HEADER = {"品牌", "ASIN", "价格", "卖家名称", "关键词"}; + + public void writeWorkbook(File outputXlsx, List items) { + SXSSFWorkbook workbook = new SXSSFWorkbook(200); + workbook.setCompressTempFiles(true); + try (FileOutputStream outputStream = new FileOutputStream(outputXlsx)) { + Sheet sheet = workbook.createSheet("采集数据结果"); + Row headerRow = sheet.createRow(0); + for (int i = 0; i < HEADER.length; i++) { + headerRow.createCell(i).setCellValue(HEADER[i]); + sheet.setColumnWidth(i, (i == 3 ? 24 : 18) * 256); + } + int rowIndex = 1; + if (items != null) { + for (CollectDataResultRowVo item : items) { + if (item == null) { + continue; + } + Row row = sheet.createRow(rowIndex++); + row.createCell(0).setCellValue(safe(item.getBrand())); + row.createCell(1).setCellValue(safe(item.getAsin())); + row.createCell(2).setCellValue(safe(item.getPrice())); + row.createCell(3).setCellValue(safe(item.getSellerName())); + row.createCell(4).setCellValue(safe(item.getKeyword())); + } + } + workbook.write(outputStream); + } catch (Exception ex) { + log.warn("[collect-data] write workbook failed: {}", ex.getMessage()); + throw new BusinessException("生成采集数据 Excel 失败: " + ex.getMessage()); + } finally { + try { + workbook.close(); + } catch (Exception ignored) { + } + workbook.dispose(); + } + } + + private String safe(String value) { + return value == null ? "" : value; + } +} diff --git a/backend-java/src/main/java/com/nanri/aiimage/modules/collectdata/service/CollectDataService.java b/backend-java/src/main/java/com/nanri/aiimage/modules/collectdata/service/CollectDataService.java index df4cb67..2d606d2 100644 --- a/backend-java/src/main/java/com/nanri/aiimage/modules/collectdata/service/CollectDataService.java +++ b/backend-java/src/main/java/com/nanri/aiimage/modules/collectdata/service/CollectDataService.java @@ -1,17 +1,23 @@ package com.nanri.aiimage.modules.collectdata.service; import cn.hutool.core.util.IdUtil; +import cn.hutool.core.io.FileUtil; +import cn.hutool.crypto.digest.DigestUtil; import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper; +import com.baomidou.mybatisplus.core.conditions.update.LambdaUpdateWrapper; import com.fasterxml.jackson.core.type.TypeReference; import com.fasterxml.jackson.databind.JsonNode; import com.fasterxml.jackson.databind.ObjectMapper; import com.nanri.aiimage.common.exception.BusinessException; +import com.nanri.aiimage.modules.brand.client.BrandCheckClient; import com.nanri.aiimage.modules.collectdata.mapper.CollectDataCountryPrefMapper; import com.nanri.aiimage.modules.collectdata.mapper.CollectDataItemMapper; import com.nanri.aiimage.modules.collectdata.model.dto.CollectDataCountryPreferenceSaveRequest; import com.nanri.aiimage.modules.collectdata.model.dto.CollectDataFiltersDto; import com.nanri.aiimage.modules.collectdata.model.dto.CollectDataParseRequest; import com.nanri.aiimage.modules.collectdata.model.dto.CollectDataSourceFileDto; +import com.nanri.aiimage.modules.collectdata.model.dto.CollectDataSubmitResultRequest; +import com.nanri.aiimage.modules.collectdata.model.dto.CollectDataSubmitRowDto; import com.nanri.aiimage.modules.collectdata.model.entity.CollectDataCountryPrefEntity; import com.nanri.aiimage.modules.collectdata.model.entity.CollectDataItemEntity; import com.nanri.aiimage.modules.collectdata.model.vo.CollectDataCountryPreferenceVo; @@ -21,14 +27,30 @@ import com.nanri.aiimage.modules.collectdata.model.vo.CollectDataHistoryVo; import com.nanri.aiimage.modules.collectdata.model.vo.CollectDataItemVo; import com.nanri.aiimage.modules.collectdata.model.vo.CollectDataItemsPageVo; import com.nanri.aiimage.modules.collectdata.model.vo.CollectDataParseVo; +import com.nanri.aiimage.modules.collectdata.model.vo.CollectDataResultRowVo; +import com.nanri.aiimage.modules.collectdata.model.vo.CollectDataSubmitResultVo; import com.nanri.aiimage.modules.collectdata.model.vo.CollectDataTaskBatchVo; import com.nanri.aiimage.modules.collectdata.model.vo.CollectDataTaskDetailVo; import com.nanri.aiimage.modules.collectdata.model.vo.CollectDataTaskSummaryVo; +import com.nanri.aiimage.modules.dedupe.mapper.DedupeTotalDataMapper; +import com.nanri.aiimage.modules.invalidasin.mapper.InvalidAsinDataMapper; +import com.nanri.aiimage.modules.invalidasin.model.entity.InvalidAsinDataEntity; import com.nanri.aiimage.modules.file.service.LocalFileStorageService; +import com.nanri.aiimage.modules.file.service.oss.OssStorageService; import com.nanri.aiimage.modules.task.mapper.FileResultMapper; import com.nanri.aiimage.modules.task.mapper.FileTaskMapper; +import com.nanri.aiimage.modules.task.mapper.TaskChunkMapper; +import com.nanri.aiimage.modules.task.mapper.TaskResultItemMapper; +import com.nanri.aiimage.modules.task.mapper.TaskScopeStateMapper; import com.nanri.aiimage.modules.task.model.entity.FileResultEntity; import com.nanri.aiimage.modules.task.model.entity.FileTaskEntity; +import com.nanri.aiimage.modules.task.model.entity.TaskChunkEntity; +import com.nanri.aiimage.modules.task.model.entity.TaskFileJobEntity; +import com.nanri.aiimage.modules.task.model.entity.TaskResultItemEntity; +import com.nanri.aiimage.modules.task.model.entity.TaskScopeStateEntity; +import com.nanri.aiimage.modules.task.service.TaskDistributedLockService; +import com.nanri.aiimage.modules.task.service.TaskFileJobService; +import com.nanri.aiimage.modules.task.service.TransientPayloadStorageService; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; import org.apache.poi.ss.usermodel.Cell; @@ -40,18 +62,23 @@ import org.apache.poi.ss.usermodel.WorkbookFactory; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; import org.springframework.transaction.support.TransactionTemplate; +import org.springframework.dao.DuplicateKeyException; import java.io.File; import java.io.FileInputStream; +import java.nio.charset.StandardCharsets; +import java.security.MessageDigest; import java.time.LocalDateTime; import java.time.format.DateTimeFormatter; import java.util.ArrayList; +import java.util.HashSet; import java.util.LinkedHashMap; import java.util.LinkedHashSet; import java.util.List; import java.util.Locale; import java.util.Map; import java.util.Objects; +import java.util.Set; import java.util.regex.Pattern; @Service @@ -74,6 +101,9 @@ public class CollectDataService { private static final String DEFAULT_TASK_TYPE = "collect-data"; private static final int ITEM_INSERT_BATCH_SIZE = 500; + private static final int BRAND_CHECK_BATCH_SIZE = 10; + private static final long TASK_LOCK_WAIT_MILLIS = 5000L; + private static final String CONTENT_TYPE_XLSX = "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet"; private static final List KEYWORD_HEADER_ALIASES = List.of("关键词", "keyword", "key word"); private static final List STATUS_HEADER_ALIASES = List.of("状态", "status"); @@ -86,6 +116,17 @@ public class CollectDataService { private final FileResultMapper fileResultMapper; private final CollectDataItemMapper collectDataItemMapper; private final CollectDataCountryPrefMapper collectDataCountryPrefMapper; + private final DedupeTotalDataMapper dedupeTotalDataMapper; + private final InvalidAsinDataMapper invalidAsinDataMapper; + private final TaskChunkMapper taskChunkMapper; + private final TaskScopeStateMapper taskScopeStateMapper; + private final TaskResultItemMapper taskResultItemMapper; + private final TaskDistributedLockService taskDistributedLockService; + private final TaskFileJobService taskFileJobService; + private final TransientPayloadStorageService transientPayloadStorageService; + private final BrandCheckClient brandCheckClient; + private final CollectDataExcelAssemblyService excelAssemblyService; + private final OssStorageService ossStorageService; private final ObjectMapper objectMapper; private final TransactionTemplate transactionTemplate; @@ -343,6 +384,649 @@ public class CollectDataService { return vo; } + public CollectDataSubmitResultVo submitResult(Long taskId, CollectDataSubmitResultRequest request) { + if (request == null) { + throw new BusinessException("request is empty"); + } + ensureRustfsPayloadStorageEnabled(); + try (TaskDistributedLockService.LockHandle ignored = acquireTaskLockOrThrow(taskId)) { + FileTaskEntity task = fileTaskMapper.selectById(taskId); + if (task == null || !MODULE_TYPE.equals(task.getModuleType())) { + throw new BusinessException("任务不存在"); + } + if (STATUS_SUCCESS.equals(task.getStatus()) || STATUS_FAILED.equals(task.getStatus())) { + throw new BusinessException(40901, "任务已结束,拒绝重复提交"); + } + if (!STATUS_RUNNING.equals(task.getStatus())) { + task.setStatus(STATUS_RUNNING); + task.setUpdatedAt(LocalDateTime.now()); + task.setFinishedAt(null); + fileTaskMapper.updateById(task); + } + + FileResultEntity result = ensureTaskResult(task); + String scopeKey = firstNonBlank(request.getSubmissionId(), "task:" + taskId); + String scopeHash = DigestUtil.sha256Hex(scopeKey); + int chunkIndex = request.getChunkIndex() == null || request.getChunkIndex() <= 0 + ? nextChunkIndex(taskId, scopeHash) + : request.getChunkIndex(); + int chunkTotal = request.getChunkTotal() == null || request.getChunkTotal() <= 0 + ? Math.max(chunkIndex, 1) + : request.getChunkTotal(); + + TaskChunkEntity existing = taskChunkMapper.selectOne(new LambdaQueryWrapper() + .eq(TaskChunkEntity::getTaskId, taskId) + .eq(TaskChunkEntity::getModuleType, MODULE_TYPE) + .eq(TaskChunkEntity::getScopeHash, scopeHash) + .eq(TaskChunkEntity::getChunkIndex, chunkIndex) + .last("limit 1")); + if (existing != null) { + log.info("[collect-data] duplicate result chunk ignored taskId={} scope={} chunk={}", + taskId, scopeKey, chunkIndex); + return buildSubmitVo(task, result, chunkIndex, chunkTotal, request, 0); + } + + List rows = normalizeSubmitRows(request.getItems()); + CollectDataStats stats = loadStats(task); + stats.receivedRows += rows.size(); + stats.currentChunkRows = rows.size(); + + List candidates = filterByExistingAsin(rows, stats); + List accepted = filterByBrandCheck(candidates, stats); + for (CollectDataResultRowVo row : accepted) { + upsertResultItem(task.getId(), result.getId(), scopeKey, row); + } + + String payloadJson = writeJson(rows, "采集结果序列化失败"); + String storedPayload = transientPayloadStorageService.storeChunkPayloadVersioned( + MODULE_TYPE, taskId, scopeHash, chunkIndex, payloadJson); + requireRustfsPayload(storedPayload, "采集结果分片必须写入 RustFS"); + persistChunk(taskId, scopeKey, scopeHash, chunkIndex, chunkTotal, storedPayload, payloadJson); + persistScope(taskId, scopeKey, scopeHash, chunkTotal, request); + + stats.finalRowCount = countFinalRows(taskId); + persistStats(task, stats); + + if (request.getError() != null && !request.getError().isBlank()) { + markTaskFailed(task, result, request.getError(), stats); + } else if (Boolean.TRUE.equals(request.getDone())) { + enqueueFinalWorkbook(task, result, stats); + } else { + task.setUpdatedAt(LocalDateTime.now()); + fileTaskMapper.updateById(task); + } + + log.info("[collect-data] submit result taskId={} chunk={}/{} rows={} accepted={} dedupeFiltered={} invalidFiltered={} brandRejected={} finalRows={} done={}", + taskId, chunkIndex, chunkTotal, rows.size(), accepted.size(), stats.dedupeFilteredCount, + stats.invalidFilteredCount, stats.brandRejectedCount, stats.finalRowCount, request.getDone()); + return buildSubmitVo(fileTaskMapper.selectById(taskId), result, chunkIndex, chunkTotal, request, rows.size()); + } + } + + private FileResultEntity ensureTaskResult(FileTaskEntity task) { + FileResultEntity result = fileResultMapper.selectOne(new LambdaQueryWrapper() + .eq(FileResultEntity::getTaskId, task.getId()) + .eq(FileResultEntity::getModuleType, MODULE_TYPE) + .orderByAsc(FileResultEntity::getId) + .last("limit 1")); + if (result != null) { + return result; + } + result = new FileResultEntity(); + result.setTaskId(task.getId()); + result.setModuleType(MODULE_TYPE); + result.setSourceFilename(""); + result.setRowCount(0); + result.setUserId(task.getUserId()); + result.setCreatedAt(LocalDateTime.now()); + fileResultMapper.insert(result); + return result; + } + + private List normalizeSubmitRows(List rawRows) { + List rows = new ArrayList<>(); + if (rawRows == null) { + return rows; + } + for (CollectDataSubmitRowDto raw : rawRows) { + if (raw == null) { + continue; + } + String asin = normalizeAsin(raw.getAsin()); + if (asin.isBlank()) { + continue; + } + CollectDataResultRowVo row = new CollectDataResultRowVo(); + row.setBrand(normalize(raw.getBrand())); + row.setAsin(asin); + row.setPrice(normalize(raw.getPrice())); + row.setSellerName(normalize(raw.getSellerName())); + row.setKeyword(normalize(raw.getKeyword())); + rows.add(row); + } + return rows; + } + + private List filterByExistingAsin(List rows, CollectDataStats stats) { + if (rows == null || rows.isEmpty()) { + return List.of(); + } + List asins = rows.stream() + .map(CollectDataResultRowVo::getAsin) + .filter(value -> value != null && !value.isBlank()) + .distinct() + .toList(); + Set dedupeValues = new HashSet<>(); + if (!asins.isEmpty()) { + List existingDedupeValues = dedupeTotalDataMapper.selectExistingDataValues(asins); + if (existingDedupeValues != null) { + dedupeValues.addAll(existingDedupeValues.stream() + .map(this::normalizeAsin) + .toList()); + } + } + Set invalidValues = new HashSet<>(); + if (!asins.isEmpty()) { + List invalidRows = invalidAsinDataMapper.selectList(new LambdaQueryWrapper() + .in(InvalidAsinDataEntity::getDataValue, asins)); + if (invalidRows != null) { + for (InvalidAsinDataEntity row : invalidRows) { + invalidValues.add(normalizeAsin(row.getDataValue())); + } + } + } + + List out = new ArrayList<>(); + for (CollectDataResultRowVo row : rows) { + String asin = row.getAsin(); + if (dedupeValues.contains(asin)) { + stats.dedupeFilteredCount++; + continue; + } + if (invalidValues.contains(asin)) { + stats.invalidFilteredCount++; + continue; + } + out.add(row); + } + return out; + } + + private List filterByBrandCheck(List rows, CollectDataStats stats) { + if (rows == null || rows.isEmpty()) { + return List.of(); + } + List accepted = new ArrayList<>(); + for (int start = 0; start < rows.size(); start += BRAND_CHECK_BATCH_SIZE) { + int end = Math.min(start + BRAND_CHECK_BATCH_SIZE, rows.size()); + List batch = rows.subList(start, end); + List brands = batch.stream() + .map(CollectDataResultRowVo::getBrand) + .filter(value -> value != null && !value.isBlank()) + .distinct() + .toList(); + BrandCheckClient.BrandCheckBatchResult check = brandCheckClient.checkAll(brands, "Terms"); + Set failedBrands = normalizeObjectSet(check == null ? null : check.faildData()); + Set queryFailedBrands = normalizeObjectSet(check == null ? null : check.queryFaildData()); + for (CollectDataResultRowVo row : batch) { + String brand = normalizeBrand(row.getBrand()); + if (brand.isBlank()) { + stats.brandRejectedCount++; + insertInvalidAsin(row); + continue; + } + if (failedBrands.contains(brand)) { + stats.brandRejectedCount++; + insertInvalidAsin(row); + continue; + } + if (queryFailedBrands.contains(brand)) { + stats.brandQueryFailedCount++; + insertInvalidAsin(row); + continue; + } + accepted.add(row); + } + } + return accepted; + } + + private Set normalizeObjectSet(List values) { + Set out = new HashSet<>(); + if (values == null) { + return out; + } + for (Object value : values) { + String normalized = normalizeBrand(extractBrandValue(value)); + if (!normalized.isBlank()) { + out.add(normalized); + } + } + return out; + } + + private String extractBrandValue(Object value) { + if (value == null) { + return ""; + } + if (value instanceof Map map) { + for (String key : List.of("brand", "brandName", "brand_name", "name", "value", "data_value")) { + Object candidate = map.get(key); + if (candidate != null && !String.valueOf(candidate).isBlank()) { + return String.valueOf(candidate); + } + } + } + return String.valueOf(value); + } + + private void insertInvalidAsin(CollectDataResultRowVo row) { + if (row == null || row.getAsin() == null || row.getAsin().isBlank()) { + return; + } + InvalidAsinDataEntity entity = new InvalidAsinDataEntity(); + entity.setDataValue(row.getAsin()); + entity.setBrand(row.getBrand()); + try { + invalidAsinDataMapper.insert(entity); + } catch (DuplicateKeyException ignored) { + } + } + + private void upsertResultItem(Long taskId, Long resultId, String scopeKey, CollectDataResultRowVo row) { + String itemKey = "asin:" + row.getAsin(); + String scopeHash = hash(scopeKey); + String payloadJson = writeJson(row, "采集结果明细序列化失败"); + String payloadHash = hash(payloadJson); + TaskResultItemEntity existing = taskResultItemMapper.selectOne(new LambdaQueryWrapper() + .eq(TaskResultItemEntity::getTaskId, taskId) + .eq(TaskResultItemEntity::getModuleType, MODULE_TYPE) + .eq(TaskResultItemEntity::getScopeHash, scopeHash) + .eq(TaskResultItemEntity::getItemKey, itemKey) + .last("limit 1")); + if (existing != null && Objects.equals(existing.getPayloadHash(), payloadHash)) { + return; + } + String storedPayload = transientPayloadStorageService.storeResultItemPayload( + MODULE_TYPE, taskId, scopeHash, itemKey, payloadJson); + requireRustfsPayload(storedPayload, "采集结果明细必须写入 RustFS"); + LocalDateTime now = LocalDateTime.now(); + if (existing == null) { + TaskResultItemEntity entity = new TaskResultItemEntity(); + entity.setTaskId(taskId); + entity.setModuleType(MODULE_TYPE); + entity.setResultId(resultId); + entity.setScopeKey(scopeKey); + entity.setScopeHash(scopeHash); + entity.setItemKey(itemKey); + entity.setAsin(row.getAsin()); + entity.setStatus("ACCEPTED"); + entity.setPayloadJson(storedPayload); + entity.setPayloadHash(payloadHash); + entity.setCreatedAt(now); + entity.setUpdatedAt(now); + try { + taskResultItemMapper.insert(entity); + return; + } catch (DuplicateKeyException ignored) { + existing = taskResultItemMapper.selectOne(new LambdaQueryWrapper() + .eq(TaskResultItemEntity::getTaskId, taskId) + .eq(TaskResultItemEntity::getModuleType, MODULE_TYPE) + .eq(TaskResultItemEntity::getScopeHash, scopeHash) + .eq(TaskResultItemEntity::getItemKey, itemKey) + .last("limit 1")); + } + } + if (existing == null) { + throw new BusinessException("保存采集结果明细失败"); + } + transientPayloadStorageService.deleteReplacedPayloadIfNeeded(existing.getPayloadJson(), storedPayload); + taskResultItemMapper.update(null, new LambdaUpdateWrapper() + .eq(TaskResultItemEntity::getId, existing.getId()) + .set(TaskResultItemEntity::getResultId, resultId) + .set(TaskResultItemEntity::getScopeKey, scopeKey) + .set(TaskResultItemEntity::getAsin, row.getAsin()) + .set(TaskResultItemEntity::getStatus, "ACCEPTED") + .set(TaskResultItemEntity::getPayloadJson, storedPayload) + .set(TaskResultItemEntity::getPayloadHash, payloadHash) + .set(TaskResultItemEntity::getUpdatedAt, now)); + } + + private void persistChunk(Long taskId, + String scopeKey, + String scopeHash, + int chunkIndex, + int chunkTotal, + String storedPayload, + String payloadJson) { + TaskChunkEntity chunk = new TaskChunkEntity(); + chunk.setTaskId(taskId); + chunk.setModuleType(MODULE_TYPE); + chunk.setScopeKey(scopeKey); + chunk.setScopeHash(scopeHash); + chunk.setChunkIndex(chunkIndex); + chunk.setChunkTotal(chunkTotal); + chunk.setPayloadJson(storedPayload); + chunk.setPayloadHash(DigestUtil.sha256Hex(payloadJson)); + chunk.setCreatedAt(LocalDateTime.now()); + chunk.setUpdatedAt(LocalDateTime.now()); + try { + taskChunkMapper.insert(chunk); + } catch (DuplicateKeyException ex) { + transientPayloadStorageService.deletePayloadIfPresent(storedPayload); + log.info("[collect-data] duplicate chunk inserted concurrently taskId={} scope={} chunk={}", + taskId, scopeKey, chunkIndex); + } + } + + private void persistScope(Long taskId, String scopeKey, String scopeHash, int chunkTotal, CollectDataSubmitResultRequest request) { + TaskScopeStateEntity scope = taskScopeStateMapper.selectOne(new LambdaQueryWrapper() + .eq(TaskScopeStateEntity::getTaskId, taskId) + .eq(TaskScopeStateEntity::getModuleType, MODULE_TYPE) + .eq(TaskScopeStateEntity::getScopeHash, scopeHash) + .last("limit 1")); + if (scope == null) { + scope = new TaskScopeStateEntity(); + scope.setTaskId(taskId); + scope.setModuleType(MODULE_TYPE); + scope.setScopeKey(scopeKey); + scope.setScopeHash(scopeHash); + scope.setCreatedAt(LocalDateTime.now()); + } + scope.setChunkTotal(chunkTotal); + scope.setReceivedChunkCount(countReceivedChunks(taskId, scopeHash)); + scope.setLastChunkAt(LocalDateTime.now()); + scope.setLastError(request.getError()); + scope.setCompleted(Boolean.TRUE.equals(request.getDone()) ? 1 : 0); + scope.setStateJson("{\"phase\":\"RECEIVED\"}"); + scope.setUpdatedAt(LocalDateTime.now()); + if (scope.getId() == null) { + taskScopeStateMapper.insert(scope); + } else { + taskScopeStateMapper.updateById(scope); + } + } + + private void enqueueFinalWorkbook(FileTaskEntity task, FileResultEntity result, CollectDataStats stats) { + String filename = buildResultFilename(task); + result.setResultFilename(filename); + result.setResultFileUrl(null); + result.setResultFileSize(0L); + result.setResultContentType(CONTENT_TYPE_XLSX); + result.setRowCount(stats.finalRowCount); + result.setErrorMessage(null); + fileResultMapper.updateById(result); + task.setUpdatedAt(LocalDateTime.now()); + fileTaskMapper.updateById(task); + taskFileJobService.enqueueAssembleResult(task.getId(), MODULE_TYPE, result.getId(), "task:" + task.getId()); + } + + private void markTaskFailed(FileTaskEntity task, FileResultEntity result, String error, CollectDataStats stats) { + result.setSuccess(0); + result.setErrorMessage(error); + result.setRowCount(stats.finalRowCount); + fileResultMapper.updateById(result); + task.setStatus(STATUS_FAILED); + task.setErrorMessage(error); + task.setFailedFileCount(1); + task.setUpdatedAt(LocalDateTime.now()); + task.setFinishedAt(LocalDateTime.now()); + persistStats(task, stats); + fileTaskMapper.updateById(task); + } + + public void processResultFileJob(TaskFileJobEntity job) { + if (job == null || job.getTaskId() == null) { + throw new BusinessException("结果文件任务参数不完整"); + } + FileTaskEntity task = fileTaskMapper.selectById(job.getTaskId()); + if (task == null || !MODULE_TYPE.equals(task.getModuleType())) { + throw new BusinessException("任务不存在"); + } + FileResultEntity result = fileResultMapper.selectById(job.getResultId()); + if (result == null || !MODULE_TYPE.equals(result.getModuleType())) { + throw new BusinessException("结果记录不存在"); + } + List rows = loadFinalRows(task.getId()); + File workRoot = FileUtil.mkdir(FileUtil.file(System.getProperty("java.io.tmpdir"), "collect-data-result", String.valueOf(task.getId()))); + String filename = buildResultFilename(task); + File xlsx = FileUtil.file(workRoot, filename); + try { + excelAssemblyService.writeWorkbook(xlsx, rows); + String objectKey = ossStorageService.uploadResultFile(xlsx, MODULE_TYPE); + result.setResultFilename(filename); + result.setResultFileUrl(objectKey); + result.setResultFileSize(xlsx.length()); + result.setResultContentType(CONTENT_TYPE_XLSX); + result.setRowCount(rows.size()); + result.setSuccess(1); + result.setErrorMessage(null); + fileResultMapper.updateById(result); + + CollectDataStats stats = loadStats(task); + stats.finalRowCount = rows.size(); + persistStats(task, stats); + task.setStatus(STATUS_SUCCESS); + task.setSuccessFileCount(1); + task.setFailedFileCount(0); + task.setErrorMessage(null); + task.setUpdatedAt(LocalDateTime.now()); + task.setFinishedAt(LocalDateTime.now()); + fileTaskMapper.updateById(task); + } finally { + FileUtil.del(xlsx); + } + } + + private List loadFinalRows(Long taskId) { + List rows = taskResultItemMapper.selectList(new LambdaQueryWrapper() + .eq(TaskResultItemEntity::getTaskId, taskId) + .eq(TaskResultItemEntity::getModuleType, MODULE_TYPE) + .orderByAsc(TaskResultItemEntity::getId)); + List out = new ArrayList<>(); + if (rows == null) { + return out; + } + for (TaskResultItemEntity row : rows) { + try { + String payloadJson = transientPayloadStorageService.resolvePayload(row.getPayloadJson(), "read collect data result item failed"); + CollectDataResultRowVo value = objectMapper.readValue(payloadJson, CollectDataResultRowVo.class); + if (value != null) { + out.add(value); + } + } catch (Exception ex) { + throw new BusinessException("读取采集结果明细失败"); + } + } + return out; + } + + private int countFinalRows(Long taskId) { + if (taskId == null || taskId <= 0) { + return 0; + } + Long count = taskResultItemMapper.selectCount(new LambdaQueryWrapper() + .eq(TaskResultItemEntity::getTaskId, taskId) + .eq(TaskResultItemEntity::getModuleType, MODULE_TYPE)); + return count == null ? 0 : count.intValue(); + } + + private void ensureRustfsPayloadStorageEnabled() { + if (!transientPayloadStorageService.isSharedWriteEnabled()) { + throw new BusinessException("RustFS 未配置,采集结果回传暂不可接收"); + } + } + + private void requireRustfsPayload(String storedPayload, String message) { + String pointer = transientPayloadStorageService.extractPointer(storedPayload); + if (pointer != null && pointer.startsWith("rustfs:")) { + return; + } + transientPayloadStorageService.deletePayloadIfPresent(storedPayload); + throw new BusinessException(message); + } + + private TaskDistributedLockService.LockHandle acquireTaskLockOrThrow(Long taskId) { + TaskDistributedLockService.LockHandle lockHandle = taskDistributedLockService.acquire(MODULE_TYPE, taskId, TASK_LOCK_WAIT_MILLIS); + if (lockHandle == null) { + throw new BusinessException(40901, "任务正在处理,请稍后再试"); + } + return lockHandle; + } + + private int nextChunkIndex(Long taskId, String scopeHash) { + TaskChunkEntity latest = taskChunkMapper.selectOne(new LambdaQueryWrapper() + .eq(TaskChunkEntity::getTaskId, taskId) + .eq(TaskChunkEntity::getModuleType, MODULE_TYPE) + .eq(TaskChunkEntity::getScopeHash, scopeHash) + .orderByDesc(TaskChunkEntity::getChunkIndex) + .last("limit 1")); + return latest == null || latest.getChunkIndex() == null ? 1 : latest.getChunkIndex() + 1; + } + + private int countReceivedChunks(Long taskId, String scopeHash) { + Long count = taskChunkMapper.selectCount(new LambdaQueryWrapper() + .eq(TaskChunkEntity::getTaskId, taskId) + .eq(TaskChunkEntity::getModuleType, MODULE_TYPE) + .eq(TaskChunkEntity::getScopeHash, scopeHash)); + return count == null ? 0 : count.intValue(); + } + + private CollectDataSubmitResultVo buildSubmitVo(FileTaskEntity task, + FileResultEntity result, + int chunkIndex, + int chunkTotal, + CollectDataSubmitResultRequest request, + int currentChunkRows) { + CollectDataStats stats = loadStats(task); + stats.finalRowCount = countFinalRows(task == null ? null : task.getId()); + CollectDataSubmitResultVo vo = new CollectDataSubmitResultVo(); + vo.setTaskId(task == null ? null : task.getId()); + vo.setResultId(result == null ? null : result.getId()); + vo.setChunkIndex(chunkIndex); + vo.setChunkTotal(chunkTotal); + vo.setDone(request != null && Boolean.TRUE.equals(request.getDone())); + vo.setReceivedRows(stats.receivedRows); + vo.setCurrentChunkRows(currentChunkRows); + vo.setDedupeFilteredCount(stats.dedupeFilteredCount); + vo.setInvalidFilteredCount(stats.invalidFilteredCount); + vo.setBrandRejectedCount(stats.brandRejectedCount); + vo.setBrandQueryFailedCount(stats.brandQueryFailedCount); + vo.setFinalRowCount(stats.finalRowCount); + vo.setTaskStatus(task == null ? null : task.getStatus()); + return vo; + } + + private CollectDataStats loadStats(FileTaskEntity task) { + CollectDataStats stats = new CollectDataStats(); + if (task == null || task.getResultJson() == null || task.getResultJson().isBlank()) { + return stats; + } + try { + JsonNode root = objectMapper.readTree(task.getResultJson()); + stats.receivedRows = root.path("receivedRows").asInt(0); + stats.currentChunkRows = root.path("currentChunkRows").asInt(0); + stats.dedupeFilteredCount = root.path("dedupeFilteredCount").asInt(0); + stats.invalidFilteredCount = root.path("invalidFilteredCount").asInt(0); + stats.brandRejectedCount = root.path("brandRejectedCount").asInt(0); + stats.brandQueryFailedCount = root.path("brandQueryFailedCount").asInt(0); + stats.finalRowCount = root.path("finalRowCount").asInt(0); + } catch (Exception ex) { + log.warn("[collect-data] parse result stats failed taskId={} err={}", task.getId(), ex.getMessage()); + } + return stats; + } + + private void persistStats(FileTaskEntity task, CollectDataStats stats) { + if (task == null || stats == null) { + return; + } + try { + Map payload = new LinkedHashMap<>(); + payload.put("receivedRows", stats.receivedRows); + payload.put("currentChunkRows", stats.currentChunkRows); + payload.put("dedupeFilteredCount", stats.dedupeFilteredCount); + payload.put("invalidFilteredCount", stats.invalidFilteredCount); + payload.put("brandRejectedCount", stats.brandRejectedCount); + payload.put("brandQueryFailedCount", stats.brandQueryFailedCount); + payload.put("finalRowCount", stats.finalRowCount); + task.setResultJson(objectMapper.writeValueAsString(payload)); + } catch (Exception ex) { + throw new BusinessException("保存采集统计失败"); + } + } + + private String writeJson(Object value, String message) { + try { + return objectMapper.writeValueAsString(value); + } catch (Exception ex) { + throw new BusinessException(message); + } + } + + private String hash(String value) { + try { + MessageDigest digest = MessageDigest.getInstance("SHA-256"); + byte[] bytes = digest.digest((value == null ? "" : value).getBytes(StandardCharsets.UTF_8)); + StringBuilder sb = new StringBuilder(bytes.length * 2); + for (byte b : bytes) { + sb.append(String.format("%02x", b)); + } + return sb.toString(); + } catch (Exception ex) { + throw new IllegalStateException("failed to hash collect data payload", ex); + } + } + + private String normalizeAsin(String value) { + return normalize(value).toUpperCase(Locale.ROOT); + } + + private String normalizeBrand(String value) { + return normalize(value).toLowerCase(Locale.ROOT); + } + + private String buildResultFilename(FileTaskEntity task) { + String base = task == null || task.getTaskNo() == null || task.getTaskNo().isBlank() + ? "collect-data-" + (task == null ? "result" : task.getId()) + : task.getTaskNo(); + return base.replaceAll("[\\\\/:*?\"<>|]", "_") + "-result.xlsx"; + } + + private void deleteTransientTaskPayloads(Long taskId) { + if (taskId == null || taskId <= 0) { + return; + } + List chunks = taskChunkMapper.selectList(new LambdaQueryWrapper() + .select(TaskChunkEntity::getPayloadJson) + .eq(TaskChunkEntity::getTaskId, taskId) + .eq(TaskChunkEntity::getModuleType, MODULE_TYPE)); + if (chunks != null) { + for (TaskChunkEntity chunk : chunks) { + transientPayloadStorageService.deletePayloadIfPresent(chunk.getPayloadJson()); + } + } + List items = taskResultItemMapper.selectList(new LambdaQueryWrapper() + .select(TaskResultItemEntity::getPayloadJson) + .eq(TaskResultItemEntity::getTaskId, taskId) + .eq(TaskResultItemEntity::getModuleType, MODULE_TYPE)); + if (items != null) { + for (TaskResultItemEntity item : items) { + transientPayloadStorageService.deletePayloadIfPresent(item.getPayloadJson()); + } + } + } + + private static class CollectDataStats { + private int receivedRows; + private int currentChunkRows; + private int dedupeFilteredCount; + private int invalidFilteredCount; + private int brandRejectedCount; + private int brandQueryFailedCount; + private int finalRowCount; + } + @Transactional public void deleteTask(Long taskId, Long userId) { FileTaskEntity task = requireTask(taskId, userId); @@ -351,6 +1035,17 @@ public class CollectDataService { fileResultMapper.delete(new LambdaQueryWrapper() .eq(FileResultEntity::getTaskId, task.getId()) .eq(FileResultEntity::getModuleType, MODULE_TYPE)); + deleteTransientTaskPayloads(task.getId()); + taskScopeStateMapper.delete(new LambdaQueryWrapper() + .eq(TaskScopeStateEntity::getTaskId, task.getId()) + .eq(TaskScopeStateEntity::getModuleType, MODULE_TYPE)); + taskChunkMapper.delete(new LambdaQueryWrapper() + .eq(TaskChunkEntity::getTaskId, task.getId()) + .eq(TaskChunkEntity::getModuleType, MODULE_TYPE)); + taskResultItemMapper.delete(new LambdaQueryWrapper() + .eq(TaskResultItemEntity::getTaskId, task.getId()) + .eq(TaskResultItemEntity::getModuleType, MODULE_TYPE)); + taskFileJobService.deleteTaskJobs(task.getId(), MODULE_TYPE); fileTaskMapper.deleteById(task.getId()); } @@ -359,6 +1054,21 @@ public class CollectDataService { if (row == null || !MODULE_TYPE.equals(row.getModuleType()) || !Objects.equals(userId, row.getUserId())) { throw new BusinessException("记录不存在"); } + List resultItems = taskResultItemMapper.selectList(new LambdaQueryWrapper() + .select(TaskResultItemEntity::getPayloadJson) + .eq(TaskResultItemEntity::getTaskId, row.getTaskId()) + .eq(TaskResultItemEntity::getModuleType, MODULE_TYPE) + .eq(TaskResultItemEntity::getResultId, row.getId())); + if (resultItems != null) { + for (TaskResultItemEntity item : resultItems) { + transientPayloadStorageService.deletePayloadIfPresent(item.getPayloadJson()); + } + } + taskResultItemMapper.delete(new LambdaQueryWrapper() + .eq(TaskResultItemEntity::getTaskId, row.getTaskId()) + .eq(TaskResultItemEntity::getModuleType, MODULE_TYPE) + .eq(TaskResultItemEntity::getResultId, row.getId())); + taskFileJobService.deleteResultJobs(row.getTaskId(), MODULE_TYPE, row.getId()); fileResultMapper.deleteById(resultId); } @@ -503,21 +1213,41 @@ public class CollectDataService { vo.setTaskId(row.getTaskId()); vo.setSourceFilename(row.getSourceFilename()); vo.setResultFilename(row.getResultFilename()); + vo.setDownloadUrl(buildFreshDownloadUrl(row)); vo.setRowCount(row.getRowCount()); vo.setSuccess(row.getSuccess() != null && row.getSuccess() == 1); vo.setError(row.getErrorMessage()); vo.setCreatedAt(formatTime(row.getCreatedAt())); if (task != null) { + CollectDataStats stats = loadStats(task); vo.setTaskNo(task.getTaskNo()); vo.setTaskStatus(task.getStatus()); vo.setStartedAt(formatTime(task.getCreatedAt())); vo.setFinishedAt(formatTime(task.getFinishedAt())); vo.setTaskType(extractTaskTypeFromRequest(task)); vo.setFilters(extractFiltersFromRequest(task)); + vo.setDedupeFilteredCount(stats.dedupeFilteredCount); + vo.setInvalidFilteredCount(stats.invalidFilteredCount); + vo.setBrandRejectedCount(stats.brandRejectedCount); + vo.setFinalRowCount(stats.finalRowCount); } return vo; } + private String buildFreshDownloadUrl(FileResultEntity row) { + String resultFileUrl = row == null ? null : row.getResultFileUrl(); + if (resultFileUrl == null || resultFileUrl.isBlank()) { + return null; + } + try { + return ossStorageService.generateFreshDownloadUrl(resultFileUrl); + } catch (Exception ex) { + log.warn("[collect-data] generate fresh download url failed resultId={} err={}", + row.getId(), ex.getMessage()); + return null; + } + } + private CollectDataTaskSummaryVo toTaskSummary(FileTaskEntity task) { CollectDataTaskSummaryVo vo = new CollectDataTaskSummaryVo(); vo.setId(task.getId()); 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 f950335..83568dc 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 @@ -35,6 +35,7 @@ import java.util.HashMap; import java.util.LinkedHashMap; import java.util.List; import java.util.Map; +import java.util.concurrent.atomic.AtomicReference; import java.util.zip.ZipEntry; import java.util.zip.ZipOutputStream; @@ -291,7 +292,7 @@ public class ConvertRunService { long batchId = System.currentTimeMillis(); int[] rowIndex = new int[]{1}; boolean[] headerRead = new boolean[]{false}; - Map[] headerMapHolder = new Map[]{null}; + AtomicReference> headerMapHolder = new AtomicReference<>(); ExcelStreamReader.readFirstSheet(inputFile, new ExcelStreamReader.SheetRowHandler() { @Override @@ -310,7 +311,7 @@ public class ConvertRunService { throw new BusinessException("Missing required columns: " + String.join(", ", missing)); } headerRead[0] = true; - headerMapHolder[0] = resolvedHeaderMap; + headerMapHolder.set(resolvedHeaderMap); for (String line : preambleLines) { writeLineToAll(writers, line); } @@ -322,7 +323,7 @@ public class ConvertRunService { @Override public void onRow(String sheetName, Integer sheetNo, int excelRowIndex, Map headerMap, Map rowMap) throws Exception { - Map resolvedHeaderMap = headerMapHolder[0]; + Map resolvedHeaderMap = headerMapHolder.get(); if (resolvedHeaderMap == null) { throw new BusinessException("Excel header row is empty."); } 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 39bd653..5fc6c8a 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 @@ -117,7 +117,7 @@ public class DeleteBrandTaskCacheService { org.springframework.data.redis.serializer.RedisSerializer serializer = stringRedisTemplate.getStringSerializer(); for (Long taskId : missingTaskIds) { - connection.hGetAll(serializer.serialize(buildProgressKey(taskId))); + connection.hashCommands().hGetAll(serializer.serialize(buildProgressKey(taskId))); } return 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 dc57773..fd60a86 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 @@ -50,6 +50,7 @@ import java.util.List; import java.util.Map; import java.util.Objects; import java.util.Locale; +import java.util.concurrent.atomic.AtomicReference; @Service @RequiredArgsConstructor @@ -792,7 +793,7 @@ public class PriceTrackTaskService { private Map>> parseWorkbookAsinRows(File file, List countryCodes) { Map>> out = new LinkedHashMap<>(); - Map[] headerIndexHolder = new Map[]{Map.of()}; + AtomicReference> headerIndexHolder = new AtomicReference<>(Map.of()); String[] countryCodeHolder = new String[]{null}; int[] sheetCount = new int[]{0}; try { @@ -801,7 +802,7 @@ public class PriceTrackTaskService { public void onHeader(String sheetName, Integer sheetNo, Map headerMap) { sheetCount[0]++; countryCodeHolder[0] = resolveCountryCode(sheetName, countryCodes, false); - headerIndexHolder[0] = buildHeaderIndex(headerMap); + headerIndexHolder.set(buildHeaderIndex(headerMap)); } @Override @@ -813,10 +814,11 @@ public class PriceTrackTaskService { if (countryCode == null) { return; } - if (headerIndexHolder[0].isEmpty()) { + Map headerIndex = headerIndexHolder.get(); + if (headerIndex.isEmpty()) { return; } - Map item = extractAsinRow(rowMap, headerIndexHolder[0]); + Map item = extractAsinRow(rowMap, headerIndex); if (!item.isEmpty()) { out.computeIfAbsent(countryCode, key -> new ArrayList<>()).add(item); } diff --git a/backend-java/src/main/java/com/nanri/aiimage/modules/similarasin/client/SimilarAsinCozeClient.java b/backend-java/src/main/java/com/nanri/aiimage/modules/similarasin/client/SimilarAsinCozeClient.java index 23181af..89cd8c6 100644 --- a/backend-java/src/main/java/com/nanri/aiimage/modules/similarasin/client/SimilarAsinCozeClient.java +++ b/backend-java/src/main/java/com/nanri/aiimage/modules/similarasin/client/SimilarAsinCozeClient.java @@ -1264,25 +1264,33 @@ public class SimilarAsinCozeClient { private Object cozePrice(Object value) { if (value == null) { - return ""; + return BigDecimal.ZERO; } if (value instanceof BigDecimal decimal) { - return decimal.stripTrailingZeros(); + return normalizeCozeDecimal(decimal); } if (value instanceof Number number) { - return new BigDecimal(number.toString()).stripTrailingZeros(); + return normalizeCozeDecimal(new BigDecimal(number.toString())); } String text = safeText(String.valueOf(value)); if (text.isBlank()) { - return ""; + return BigDecimal.ZERO; } try { - return new BigDecimal(text).stripTrailingZeros(); + return normalizeCozeDecimal(new BigDecimal(text)); } catch (NumberFormatException ex) { return text; } } + private BigDecimal normalizeCozeDecimal(BigDecimal value) { + BigDecimal normalized = value.stripTrailingZeros(); + if (normalized.scale() < 0) { + return normalized.setScale(0); + } + return normalized; + } + private boolean isBlankPrice(Object value) { return value == null || value instanceof String text && text.isBlank(); } diff --git a/backend-java/src/main/java/com/nanri/aiimage/modules/similarasin/service/SimilarAsinTaskService.java b/backend-java/src/main/java/com/nanri/aiimage/modules/similarasin/service/SimilarAsinTaskService.java index 2a248cf..a1c5a80 100644 --- a/backend-java/src/main/java/com/nanri/aiimage/modules/similarasin/service/SimilarAsinTaskService.java +++ b/backend-java/src/main/java/com/nanri/aiimage/modules/similarasin/service/SimilarAsinTaskService.java @@ -606,13 +606,7 @@ public class SimilarAsinTaskService { } public void submitResult(Long taskId, SimilarAsinSubmitResultRequest request) { - TaskDistributedLockService.LockHandle lockHandle = acquireTaskLock(taskId, TASK_LOCK_WAIT_MILLIS); - if (lockHandle == null) { - throw new BusinessException(40902, "任务正在处理上一批结果,请稍后重试"); - } - try (lockHandle) { - submitResultLocked(taskId, request); - } + submitResultLocked(taskId, request); } private void submitResultLocked(Long taskId, SimilarAsinSubmitResultRequest request) { @@ -4532,12 +4526,20 @@ public class SimilarAsinTaskService { row.createCell(col++).setCellValue(firstNonBlank(parsedRow.getCountry(), "")); row.createCell(col++).setCellValue(readValueByHeader(parsedRow, "卖家名称", "卖家名", "卖家", "店铺名称", "店铺名", "seller name", "seller_name", "seller-name", "sellername", "store name", "shop name")); row.createCell(col++).setCellValue(readValueByHeader(parsedRow, "品牌", "brand")); - row.createCell(col++).setCellValue(resultRow == null ? "" : userFacingCozeCellValue(resultRow, resultRow.getIsStock())); - row.createCell(col++).setCellValue(resultRow == null ? "" : userFacingCozeCellValue(resultRow, resultRow.getSimilarity())); - row.createCell(col++).setCellValue(resultRow == null ? "" : userFacingCozeCellValue(resultRow, resultRow.getIsConform())); - row.createCell(col++).setCellValue(resultRow == null ? "" : userFacingCozeCellValue(resultRow, resultRow.getReason())); - row.createCell(col++).setCellValue(resultRow == null ? "" : userFacingCozeCellValue(resultRow, resultRow.getCategory())); - row.createCell(col).setCellValue(resultRow == null ? "" : firstNonBlank(resultRow.getStatus(), "")); + String isStock = resultRow == null ? "" : userFacingCozeCellValue(resultRow, resultRow.getIsStock()); + String similarity = resultRow == null ? "" : userFacingCozeCellValue(resultRow, resultRow.getSimilarity()); + String isConform = resultRow == null ? "" : userFacingCozeCellValue(resultRow, resultRow.getIsConform()); + String reason = resultRow == null ? "" : userFacingCozeCellValue(resultRow, resultRow.getReason()); + String category = resultRow == null ? "" : userFacingCozeCellValue(resultRow, resultRow.getCategory()); + String status = resultRow == null || isSilentOutputFailure(resultRow) + ? "" + : resolveResultStatus(resultRow, isStock, similarity, isConform, reason, category); + row.createCell(col++).setCellValue(isStock); + row.createCell(col++).setCellValue(similarity); + row.createCell(col++).setCellValue(isConform); + row.createCell(col++).setCellValue(reason); + row.createCell(col++).setCellValue(category); + row.createCell(col).setCellValue(status); if (resultRow != null) { // 行高固定到 IMAGE_ROW_HEIGHT_POINTS(Excel 上限 409pt): row.setHeightInPoints(SimilarAsinImageEmbedder.IMAGE_ROW_HEIGHT_POINTS); @@ -4733,6 +4735,39 @@ public class SimilarAsinTaskService { || hasUsableCozeField(row.getStatus()); } + static String resolveResultStatus(SimilarAsinResultRowDto row, String... visibleResultValues) { + if (row == null) { + return "\u5931\u8d25"; + } + if (hasText(row.getMainUrl()) || hasText(row.getPuzzleImg1()) || hasText(row.getPuzzleImg2())) { + return "\u6210\u529f"; + } + String rawStatus = row.getStatus() == null ? "" : row.getStatus().trim(); + if (!rawStatus.isBlank()) { + return isFailedStatus(rawStatus) ? "\u5931\u8d25" : "\u6210\u529f"; + } + if (visibleResultValues != null) { + for (String value : visibleResultValues) { + if (hasText(value)) { + return "\u6210\u529f"; + } + } + } + return hasText(row.getConclusion()) ? "\u6210\u529f" : "\u5931\u8d25"; + } + + private static boolean isFailedStatus(String status) { + String normalized = status == null ? "" : status.trim().toLowerCase(Locale.ROOT); + return normalized.contains("fail") + || normalized.contains("error") + || normalized.contains("\u5931\u8d25") + || normalized.contains("\u9519\u8bef"); + } + + private static boolean hasText(String value) { + return value != null && !value.trim().isBlank(); + } + private boolean isExportableResultRow(SimilarAsinResultRowDto row) { if (row == null) { return false; @@ -4902,7 +4937,7 @@ public class SimilarAsinTaskService { .eq(FileTaskEntity::getUserId, userId) .eq(FileTaskEntity::getStatus, STATUS_RUNNING) .inSql(FileTaskEntity::getId, - "select distinct task_id from file_result " + + "select distinct task_id from biz_file_result " + "where module_type = '" + MODULE_TYPE + "' " + "and user_id = " + userId + " " + "and result_file_url is not null and result_file_url <> ''")); @@ -5665,6 +5700,9 @@ public class SimilarAsinTaskService { private String userFacingCozeCellValue(SimilarAsinResultRowDto row, String value) { String normalizedValue = normalize(value); + if (isSilentOutputFailure(row) || isSilentOutputFailure(normalizedValue)) { + return ""; + } if (!normalizedValue.isBlank() && !isTechnicalCozeFailure(normalizedValue)) { return value; } @@ -5690,6 +5728,19 @@ public class SimilarAsinTaskService { return firstNonBlank(row.getConclusion(), ""); } + private boolean isSilentOutputFailure(SimilarAsinResultRowDto row) { + return row != null + && (isSilentOutputFailure(row.getError()) + || isSilentOutputFailure(row.getReason()) + || isSilentOutputFailure(row.getConclusion())); + } + + private boolean isSilentOutputFailure(String value) { + String normalized = normalize(value).toLowerCase(Locale.ROOT); + return normalized.contains("required field missing") + && normalized.contains("skip coze"); + } + private boolean isTechnicalCozeFailure(String value) { String normalized = normalize(value).toLowerCase(Locale.ROOT); if (normalized.isBlank()) { 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 aa1cde9..443119c 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 @@ -31,6 +31,7 @@ import java.util.ArrayList; import java.util.HashMap; import java.util.List; import java.util.Map; +import java.util.concurrent.atomic.AtomicReference; import java.util.zip.ZipEntry; import java.util.zip.ZipOutputStream; @@ -221,7 +222,7 @@ public class SplitRunService { results.add(new SplitChunkResult(outputFile, chunkSizes.get(i))); } - Map[] headerMapHolder = new Map[]{null}; + AtomicReference> headerMapHolder = new AtomicReference<>(); int[] chunkIndex = new int[]{0}; ExcelStreamReader.readFirstSheet(inputFile, new ExcelStreamReader.SheetRowHandler() { @Override @@ -233,7 +234,7 @@ public class SplitRunService { if (!missing.isEmpty()) { throw new BusinessException("Missing required columns: " + String.join(", ", missing)); } - headerMapHolder[0] = resolvedHeaderMap; + headerMapHolder.set(resolvedHeaderMap); } @Override @@ -241,7 +242,7 @@ public class SplitRunService { if (chunkIndex[0] >= writers.size()) { return; } - Map resolvedHeaderMap = headerMapHolder[0]; + Map resolvedHeaderMap = headerMapHolder.get(); if (resolvedHeaderMap == null) { throw new BusinessException("Excel header row is empty."); } diff --git a/backend-java/src/main/java/com/nanri/aiimage/modules/task/controller/TaskHeartbeatController.java b/backend-java/src/main/java/com/nanri/aiimage/modules/task/controller/TaskHeartbeatController.java index ede6019..8f0f25a 100644 --- a/backend-java/src/main/java/com/nanri/aiimage/modules/task/controller/TaskHeartbeatController.java +++ b/backend-java/src/main/java/com/nanri/aiimage/modules/task/controller/TaskHeartbeatController.java @@ -26,7 +26,7 @@ public class TaskHeartbeatController { @PostMapping("/{taskId}/heartbeat") @Operation( summary = "Task heartbeat", - description = "Python calls this during execution. The backend resolves biz_file_task or brand task by taskId. user_id is optional.") + description = "Python calls this during execution. The backend resolves biz_file_task or brand task by taskId only.") public ApiResponse heartbeat( @Parameter(description = "Task ID", required = true, example = "200") @PathVariable Long taskId, diff --git a/backend-java/src/main/java/com/nanri/aiimage/modules/task/model/dto/TaskHeartbeatRequest.java b/backend-java/src/main/java/com/nanri/aiimage/modules/task/model/dto/TaskHeartbeatRequest.java index 2adc8cb..b7c0a90 100644 --- a/backend-java/src/main/java/com/nanri/aiimage/modules/task/model/dto/TaskHeartbeatRequest.java +++ b/backend-java/src/main/java/com/nanri/aiimage/modules/task/model/dto/TaskHeartbeatRequest.java @@ -1,17 +1,14 @@ package com.nanri.aiimage.modules.task.model.dto; -import com.fasterxml.jackson.annotation.JsonProperty; +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; import io.swagger.v3.oas.annotations.media.Schema; import lombok.Data; @Data +@JsonIgnoreProperties(ignoreUnknown = true) @Schema(description = "Task heartbeat request") public class TaskHeartbeatRequest { - @JsonProperty("user_id") - @Schema(description = "Optional task owner user ID. Python workers can omit it.", example = "453") - private Long userId; - @Schema(description = "Optional phase label", example = "crawling") private String phase; diff --git a/backend-java/src/main/java/com/nanri/aiimage/modules/task/service/TaskHeartbeatService.java b/backend-java/src/main/java/com/nanri/aiimage/modules/task/service/TaskHeartbeatService.java index b722ad3..ebe815f 100644 --- a/backend-java/src/main/java/com/nanri/aiimage/modules/task/service/TaskHeartbeatService.java +++ b/backend-java/src/main/java/com/nanri/aiimage/modules/task/service/TaskHeartbeatService.java @@ -18,6 +18,7 @@ import com.nanri.aiimage.modules.task.model.dto.TaskHeartbeatRequest; import com.nanri.aiimage.modules.task.model.entity.FileTaskEntity; import com.nanri.aiimage.modules.task.model.vo.TaskHeartbeatVo; import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; import org.springframework.stereotype.Service; import java.time.LocalDateTime; @@ -26,6 +27,7 @@ import java.util.Map; @Service @RequiredArgsConstructor +@Slf4j public class TaskHeartbeatService { private static final String STATUS_RUNNING = "RUNNING"; @@ -55,24 +57,18 @@ public class TaskHeartbeatService { public TaskHeartbeatVo heartbeat(Long taskId, TaskHeartbeatRequest request) { if (taskId == null || taskId <= 0) { + log.warn("[task-heartbeat] ignored invalid heartbeat taskId={}", taskId); return TaskHeartbeatVo.notAlive(null, null, "invalid taskId"); } - Long userId = request == null ? null : request.getUserId(); LambdaQueryWrapper fileQuery = new LambdaQueryWrapper() .eq(FileTaskEntity::getId, taskId) .last("limit 1"); - if (userId != null && userId > 0) { - fileQuery.eq(FileTaskEntity::getUserId, userId); - } FileTaskEntity fileTask = fileTaskMapper.selectOne(fileQuery); LambdaQueryWrapper brandQuery = new LambdaQueryWrapper() .eq(BrandCrawlTaskEntity::getId, taskId) .last("limit 1"); - if (userId != null && userId > 0) { - brandQuery.eq(BrandCrawlTaskEntity::getUserId, userId); - } BrandCrawlTaskEntity brandTask = brandCrawlTaskMapper.selectOne(brandQuery); TaskHeartbeatVo fileResult = touchFileTaskIfRunning(fileTask, request); @@ -93,6 +89,7 @@ public class TaskHeartbeatService { if (brandResult != null) { return brandResult; } + log.warn("[task-heartbeat] task not found taskId={} fileFound=false brandFound=false", taskId); return TaskHeartbeatVo.notAlive(null, null, "task not found"); } @@ -103,16 +100,20 @@ public class TaskHeartbeatService { String moduleType = task.getModuleType() == null ? "" : task.getModuleType(); String status = task.getStatus(); if (!STATUS_RUNNING.equals(status)) { + log.warn("[task-heartbeat] file task is not running taskId={} actualUserId={} moduleType={} status={}", + task.getId(), task.getUserId(), moduleType, status); return TaskHeartbeatVo.notAlive(moduleType, status, "task is not running"); } LocalDateTime now = LocalDateTime.now(); int updated = fileTaskMapper.update(null, new LambdaUpdateWrapper() .eq(FileTaskEntity::getId, task.getId()) - .eq(FileTaskEntity::getUserId, task.getUserId()) .eq(FileTaskEntity::getStatus, STATUS_RUNNING) .set(FileTaskEntity::getUpdatedAt, now)); if (updated <= 0) { FileTaskEntity latest = fileTaskMapper.selectById(task.getId()); + log.warn("[task-heartbeat] file task heartbeat update missed taskId={} actualUserId={} moduleType={} status={} latestStatus={}", + task.getId(), task.getUserId(), moduleType, status, + latest == null ? null : latest.getStatus()); return TaskHeartbeatVo.notAlive(moduleType, latest == null ? status : latest.getStatus(), "task is not running"); } touchModuleHeartbeat(moduleType, task.getId(), request); @@ -127,15 +128,19 @@ public class TaskHeartbeatService { } String status = task.getStatus(); if (!BRAND_STATUS_RUNNING.equals(status)) { + log.warn("[task-heartbeat] brand task is not running taskId={} actualUserId={} status={}", + task.getId(), task.getUserId(), status); return TaskHeartbeatVo.notAlive(MODULE_BRAND, status, "task is not running"); } int updated = brandCrawlTaskMapper.update(null, new LambdaUpdateWrapper() .eq(BrandCrawlTaskEntity::getId, task.getId()) - .eq(BrandCrawlTaskEntity::getUserId, task.getUserId()) .eq(BrandCrawlTaskEntity::getStatus, BRAND_STATUS_RUNNING) .set(BrandCrawlTaskEntity::getUpdatedAt, LocalDateTime.now())); if (updated <= 0) { BrandCrawlTaskEntity latest = brandCrawlTaskMapper.selectById(task.getId()); + log.warn("[task-heartbeat] brand task heartbeat update missed taskId={} actualUserId={} status={} latestStatus={}", + task.getId(), task.getUserId(), status, + latest == null ? null : latest.getStatus()); return TaskHeartbeatVo.notAlive(MODULE_BRAND, latest == null ? status : latest.getStatus(), "task is not running"); } brandTaskProgressCacheService.touchHeartbeat( @@ -208,4 +213,5 @@ public class TaskHeartbeatService { values.put(key, String.valueOf(value)); } } + } diff --git a/backend-java/src/main/java/com/nanri/aiimage/modules/task/service/TaskResultFileJobWorker.java b/backend-java/src/main/java/com/nanri/aiimage/modules/task/service/TaskResultFileJobWorker.java index 40fa053..9905d40 100644 --- a/backend-java/src/main/java/com/nanri/aiimage/modules/task/service/TaskResultFileJobWorker.java +++ b/backend-java/src/main/java/com/nanri/aiimage/modules/task/service/TaskResultFileJobWorker.java @@ -4,6 +4,7 @@ import com.nanri.aiimage.common.exception.BusinessException; import com.nanri.aiimage.modules.appearancepatent.service.AppearancePatentTaskService; import com.nanri.aiimage.config.InstanceMetadata; import com.nanri.aiimage.modules.brand.service.BrandTaskService; +import com.nanri.aiimage.modules.collectdata.service.CollectDataService; import com.nanri.aiimage.modules.deletebrand.service.DeleteBrandRunService; import com.nanri.aiimage.modules.patroldelete.service.PatrolDeleteTaskService; import com.nanri.aiimage.modules.pricetrack.service.PriceTrackTaskService; @@ -45,6 +46,7 @@ public class TaskResultFileJobWorker { private final SimilarAsinTaskService similarAsinTaskService; private final DeleteBrandRunService deleteBrandRunService; private final BrandTaskService brandTaskService; + private final CollectDataService collectDataService; @Autowired @Qualifier("cozeTaskExecutor") private TaskExecutor cozeTaskExecutor; @@ -274,6 +276,10 @@ public class TaskResultFileJobWorker { brandTaskService.processResultFileJob(job); return true; } + if ("COLLECT_DATA".equals(moduleType)) { + collectDataService.processResultFileJob(job); + return true; + } throw new IllegalArgumentException("unsupported result file job module: " + moduleType); } @@ -297,6 +303,7 @@ public class TaskResultFileJobWorker { } if ("DELETE_BRAND".equals(moduleType)) { deleteBrandRunService.cleanupResultFileJob(job); + return; } } } diff --git a/backend-java/src/test/java/com/nanri/aiimage/modules/appearancepatent/client/AppearancePatentCozeClientTest.java b/backend-java/src/test/java/com/nanri/aiimage/modules/appearancepatent/client/AppearancePatentCozeClientTest.java index 2ec59aa..775b96c 100644 --- a/backend-java/src/test/java/com/nanri/aiimage/modules/appearancepatent/client/AppearancePatentCozeClientTest.java +++ b/backend-java/src/test/java/com/nanri/aiimage/modules/appearancepatent/client/AppearancePatentCozeClientTest.java @@ -14,6 +14,7 @@ import static org.mockito.ArgumentMatchers.anyString; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.never; import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; class AppearancePatentCozeClientTest { @@ -40,7 +41,7 @@ class AppearancePatentCozeClientTest { } @Test - void skipBrandCheckWhenCozeTitleAndReasonAreNone() throws Exception { + void skipBrandCheckWhenCozeTitleIsNone() throws Exception { BrandCheckClient brandCheckClient = mock(BrandCheckClient.class); AppearancePatentCozeClient cozeClient = new AppearancePatentCozeClient( new AppearancePatentProperties(), @@ -61,4 +62,73 @@ class AppearancePatentCozeClientTest { assertThat(merged.get(0).getConclusion()).isEqualTo("无侵权"); verify(brandCheckClient, never()).checkTitleText(anyString(), anyString()); } + + @Test + void callBrandCheckOnlyWhenTitleAndAppearanceArePresent() throws Exception { + BrandCheckClient brandCheckClient = mock(BrandCheckClient.class); + when(brandCheckClient.checkTitleText("阿凡达", "Terms")) + .thenReturn(new BrandCheckClient.BrandCheckBatchResult(List.of("阿凡达"), List.of(), List.of())); + AppearancePatentCozeClient cozeClient = new AppearancePatentCozeClient( + new AppearancePatentProperties(), + new ObjectMapper(), + null, + brandCheckClient + ); + AppearancePatentResultRowDto row = new AppearancePatentResultRowDto(); + row.setId("1"); + String dataText = """ + {"data":[{"row_id":"1","title":"阿凡达","appearance":"无侵权","result":"无侵权"}]} + """; + + List merged = cozeClient.mergeRowsFromDataText(List.of(row), dataText); + + assertThat(merged).hasSize(1); + assertThat(merged.get(0).getTitleRisk()).isEqualTo("无侵权"); + verify(brandCheckClient).checkTitleText("阿凡达", "Terms"); + } + + @Test + void leaveTitleRiskBlankWhenAppearanceIsMissing() throws Exception { + BrandCheckClient brandCheckClient = mock(BrandCheckClient.class); + AppearancePatentCozeClient cozeClient = new AppearancePatentCozeClient( + new AppearancePatentProperties(), + new ObjectMapper(), + null, + brandCheckClient + ); + AppearancePatentResultRowDto row = new AppearancePatentResultRowDto(); + row.setId("1"); + String dataText = """ + {"data":[{"row_id":"1","title":"阿凡达","result":""}]} + """; + + List merged = cozeClient.mergeRowsFromDataText(List.of(row), dataText); + + assertThat(merged).hasSize(1); + assertThat(merged.get(0).getTitleRisk()).isEmpty(); + verify(brandCheckClient, never()).checkTitleText(anyString(), anyString()); + } + + @Test + void leaveTitleRiskBlankWhenTitleAndAppearanceAreMissing() throws Exception { + BrandCheckClient brandCheckClient = mock(BrandCheckClient.class); + AppearancePatentCozeClient cozeClient = new AppearancePatentCozeClient( + new AppearancePatentProperties(), + new ObjectMapper(), + null, + brandCheckClient + ); + AppearancePatentResultRowDto row = new AppearancePatentResultRowDto(); + row.setId("1"); + String dataText = """ + {"data":[{"row_id":"1","result":""}]} + """; + + List merged = cozeClient.mergeRowsFromDataText(List.of(row), dataText); + + assertThat(merged).hasSize(1); + assertThat(merged.get(0).getTitleRisk()).isEmpty(); + assertThat(merged.get(0).getAppearanceRisk()).isNullOrEmpty(); + verify(brandCheckClient, never()).checkTitleText(anyString(), anyString()); + } } diff --git a/backend-java/src/test/java/com/nanri/aiimage/modules/appearancepatent/service/AppearancePatentTaskServiceTest.java b/backend-java/src/test/java/com/nanri/aiimage/modules/appearancepatent/service/AppearancePatentTaskServiceTest.java new file mode 100644 index 0000000..4ca45a9 --- /dev/null +++ b/backend-java/src/test/java/com/nanri/aiimage/modules/appearancepatent/service/AppearancePatentTaskServiceTest.java @@ -0,0 +1,16 @@ +package com.nanri.aiimage.modules.appearancepatent.service; + +import org.junit.jupiter.api.Test; + +import static org.junit.jupiter.api.Assertions.assertEquals; + +class AppearancePatentTaskServiceTest { + + @Test + void resultStatusFailsOnlyWhenConclusionIsEmpty() { + assertEquals("\u5931\u8d25", AppearancePatentTaskService.resolveResultStatus(null)); + assertEquals("\u5931\u8d25", AppearancePatentTaskService.resolveResultStatus(" ")); + assertEquals("\u6210\u529f", AppearancePatentTaskService.resolveResultStatus("\u4fb5\u6743")); + assertEquals("\u6210\u529f", AppearancePatentTaskService.resolveResultStatus("\u65e0\u4fb5\u6743")); + } +} diff --git a/backend-java/src/test/java/com/nanri/aiimage/modules/similarasin/client/SimilarAsinCozeClientTest.java b/backend-java/src/test/java/com/nanri/aiimage/modules/similarasin/client/SimilarAsinCozeClientTest.java index 0e0a80a..0464bf7 100644 --- a/backend-java/src/test/java/com/nanri/aiimage/modules/similarasin/client/SimilarAsinCozeClientTest.java +++ b/backend-java/src/test/java/com/nanri/aiimage/modules/similarasin/client/SimilarAsinCozeClientTest.java @@ -12,6 +12,7 @@ import java.util.List; import java.util.Map; import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; import static org.junit.jupiter.api.Assertions.assertTrue; class SimilarAsinCozeClientTest { @@ -83,4 +84,100 @@ class SimilarAsinCozeClientTest { assertEquals("https://cbu01.alicdn.com/img/ibank/legacy-a.jpg", alibaba.getFirst().get("url")); assertEquals(0, new BigDecimal("8.5").compareTo((BigDecimal) alibaba.getFirst().get("price"))); } + + @Test + @SuppressWarnings("unchecked") + void buildParametersConvertsBlankAlibabaPricesToZero() throws Exception { + List rows = objectMapper.readValue(""" + [ + { + "asin": "B0BLANK123", + "url": "https://m.media-amazon.com/images/I/main.jpg", + "alibaba": [ + {"url": "https://cbu01.alicdn.com/img/ibank/blank.jpg", "price": ""}, + {"url": "https://cbu01.alicdn.com/img/ibank/null.jpg", "price": null} + ] + } + ] + """, new TypeReference<>() { + }); + SimilarAsinCozeClient client = new SimilarAsinCozeClient(new SimilarAsinProperties(), objectMapper, null); + + Method method = SimilarAsinCozeClient.class.getDeclaredMethod( + "buildParameters", List.class, String.class, String.class, boolean.class); + method.setAccessible(true); + Map parameters = (Map) method.invoke(client, rows, "", "", true); + + List> items = (List>) parameters.get("items"); + List> alibaba = (List>) items.getFirst().get("alibaba"); + String json = objectMapper.writeValueAsString(parameters); + + assertEquals(2, alibaba.size()); + assertEquals(0, BigDecimal.ZERO.compareTo((BigDecimal) alibaba.get(0).get("price"))); + assertEquals(0, BigDecimal.ZERO.compareTo((BigDecimal) alibaba.get(1).get("price"))); + assertTrue(json.contains("\"price\":0")); + assertFalse(json.contains("\"price\":\"\"")); + } + + @Test + @SuppressWarnings("unchecked") + void buildParametersConvertsBlankFallbackRowPriceToZero() throws Exception { + SimilarAsinResultRowDto row = new SimilarAsinResultRowDto(); + row.setAsin("B0BLANKROW"); + row.setUrl("https://m.media-amazon.com/images/I/main.jpg"); + row.setUrls(List.of("https://cbu01.alicdn.com/img/ibank/fallback.jpg")); + row.setPrice(""); + SimilarAsinCozeClient client = new SimilarAsinCozeClient(new SimilarAsinProperties(), objectMapper, null); + + Method method = SimilarAsinCozeClient.class.getDeclaredMethod( + "buildParameters", List.class, String.class, String.class, boolean.class); + method.setAccessible(true); + Map parameters = (Map) method.invoke(client, List.of(row), "", "", true); + + List> items = (List>) parameters.get("items"); + List> alibaba = (List>) items.getFirst().get("alibaba"); + String json = objectMapper.writeValueAsString(parameters); + + assertEquals(1, alibaba.size()); + assertEquals(0, BigDecimal.ZERO.compareTo((BigDecimal) alibaba.getFirst().get("price"))); + assertTrue(json.contains("\"price\":0")); + assertFalse(json.contains("\"price\":\"\"")); + } + + @Test + @SuppressWarnings("unchecked") + void buildParametersSerializesWholeNumberPricesWithoutScientificNotation() throws Exception { + List rows = objectMapper.readValue(""" + [ + { + "asin": "B0SCI123", + "url": "https://m.media-amazon.com/images/I/main.jpg", + "alibaba": [ + {"url": "https://cbu01.alicdn.com/img/ibank/ten.jpg", "price": 10.0}, + {"url": "https://cbu01.alicdn.com/img/ibank/eighty.jpg", "price": 80.0}, + {"url": "https://cbu01.alicdn.com/img/ibank/fraction.jpg", "price": 7.5800} + ] + } + ] + """, new TypeReference<>() { + }); + SimilarAsinCozeClient client = new SimilarAsinCozeClient(new SimilarAsinProperties(), objectMapper, null); + + Method method = SimilarAsinCozeClient.class.getDeclaredMethod( + "buildParameters", List.class, String.class, String.class, boolean.class); + method.setAccessible(true); + Map parameters = (Map) method.invoke(client, rows, "", "", true); + + List> items = (List>) parameters.get("items"); + List> alibaba = (List>) items.getFirst().get("alibaba"); + String json = objectMapper.writeValueAsString(parameters); + + assertEquals("10", ((BigDecimal) alibaba.get(0).get("price")).toPlainString()); + assertEquals("80", ((BigDecimal) alibaba.get(1).get("price")).toPlainString()); + assertEquals("7.58", ((BigDecimal) alibaba.get(2).get("price")).toPlainString()); + assertFalse(json.contains("1E+1")); + assertFalse(json.contains("8E+1")); + assertTrue(json.contains("\"price\":10")); + assertTrue(json.contains("\"price\":80")); + } } diff --git a/backend-java/src/test/java/com/nanri/aiimage/modules/similarasin/service/SimilarAsinTaskServiceTest.java b/backend-java/src/test/java/com/nanri/aiimage/modules/similarasin/service/SimilarAsinTaskServiceTest.java new file mode 100644 index 0000000..3edd55e --- /dev/null +++ b/backend-java/src/test/java/com/nanri/aiimage/modules/similarasin/service/SimilarAsinTaskServiceTest.java @@ -0,0 +1,33 @@ +package com.nanri.aiimage.modules.similarasin.service; + +import com.nanri.aiimage.modules.similarasin.model.dto.SimilarAsinResultRowDto; +import org.junit.jupiter.api.Test; + +import static org.junit.jupiter.api.Assertions.assertEquals; + +class SimilarAsinTaskServiceTest { + + @Test + void resultStatusUsesReturnedCozeDataAndImages() { + assertEquals("\u5931\u8d25", SimilarAsinTaskService.resolveResultStatus(null)); + + SimilarAsinResultRowDto empty = new SimilarAsinResultRowDto(); + assertEquals("\u5931\u8d25", SimilarAsinTaskService.resolveResultStatus(empty)); + + SimilarAsinResultRowDto withImage = new SimilarAsinResultRowDto(); + withImage.setStatus("FAILED"); + withImage.setMainUrl("https://example.com/main.jpg"); + assertEquals("\u6210\u529f", SimilarAsinTaskService.resolveResultStatus(withImage)); + + SimilarAsinResultRowDto withCozeStatus = new SimilarAsinResultRowDto(); + withCozeStatus.setStatus("success"); + assertEquals("\u6210\u529f", SimilarAsinTaskService.resolveResultStatus(withCozeStatus)); + + SimilarAsinResultRowDto failedStatus = new SimilarAsinResultRowDto(); + failedStatus.setStatus("FAILED"); + assertEquals("\u5931\u8d25", SimilarAsinTaskService.resolveResultStatus(failedStatus)); + + SimilarAsinResultRowDto withVisibleResultData = new SimilarAsinResultRowDto(); + assertEquals("\u6210\u529f", SimilarAsinTaskService.resolveResultStatus(withVisibleResultData, "", "80%", "", "", "")); + } +} diff --git a/frontend-vue/src/pages/brand/components/BrandAppearancePatentTab.vue b/frontend-vue/src/pages/brand/components/BrandAppearancePatentTab.vue index 496ee8a..71677b0 100644 --- a/frontend-vue/src/pages/brand/components/BrandAppearancePatentTab.vue +++ b/frontend-vue/src/pages/brand/components/BrandAppearancePatentTab.vue @@ -594,10 +594,10 @@ async function refreshTaskProgress() { if (task.status === 'SUCCESS' || task.status === 'FAILED') { removePollingTask(task.id) shouldRefreshDashboard = true - if (task.status === 'SUCCESS') addPendingFileTask(task.id) + if (item && isResultPreparing(item)) addPendingFileTask(task.id) else removePendingFileTask(task.id) } - if (item && task.status === 'SUCCESS' && pendingFileTaskIds.value.includes(task.id)) { + if (item && (task.status === 'SUCCESS' || task.status === 'FAILED') && pendingFileTaskIds.value.includes(task.id)) { const fileStatus = (item.fileStatus || '').toUpperCase() if (item.fileReady || fileStatus === 'SUCCESS' || fileStatus === 'FAILED') { removePendingFileTask(task.id) @@ -651,10 +651,10 @@ function settlePendingFileTasks() { } const taskStatus = (item.taskStatus || '').toUpperCase() const fileStatus = (item.fileStatus || '').toUpperCase() - if (taskStatus === 'FAILED' || item.fileReady || fileStatus === 'SUCCESS' || fileStatus === 'FAILED') { + if (item.fileReady || fileStatus === 'SUCCESS' || fileStatus === 'FAILED') { continue } - if (taskStatus === 'SUCCESS') { + if (taskStatus === 'SUCCESS' || taskStatus === 'FAILED') { remaining.push(taskId) } } @@ -767,6 +767,7 @@ function statusText(item: AppearancePatentHistoryItem) { if (status === 'PENDING') return '已解析待推送' if (isResultPreparing(item)) return '结果生成中' if (isResultBuildFailed(item)) return '结果生成失败' + if (hasDownloadableResult(item)) return '已完成' if (status === 'SUCCESS' || item.success) return '已完成' if (status === 'FAILED') return '失败' return '等待中' @@ -775,6 +776,7 @@ function statusText(item: AppearancePatentHistoryItem) { function statusClass(item: AppearancePatentHistoryItem) { const status = normalizeTaskStatus(item) if (status === 'RUNNING' || isResultPreparing(item)) return 'running' + if (hasDownloadableResult(item)) return 'success' if (isResultBuildFailed(item) || status === 'FAILED') return 'failed' if (status === 'SUCCESS' || item.success) return 'success' return 'pending' @@ -783,10 +785,10 @@ function statusClass(item: AppearancePatentHistoryItem) { function isResultPreparing(item: AppearancePatentHistoryItem) { const status = normalizeTaskStatus(item) const fileStatus = (item.fileStatus || '').toUpperCase() - if (status !== 'SUCCESS') return false + if (status !== 'SUCCESS' && status !== 'FAILED') return false if (canDownload(item)) return false if (fileStatus === 'FAILED' || fileStatus === 'SUCCESS') return false - return true + return status === 'SUCCESS' || hasActiveFileJob(item) } function normalizeTaskStatus(item: AppearancePatentHistoryItem) { @@ -819,6 +821,10 @@ function canDownload(item: AppearancePatentHistoryItem) { return Boolean(item.resultId && (item.fileReady || item.downloadUrl)) } +function hasDownloadableResult(item: AppearancePatentHistoryItem) { + return canDownload(item) +} + function fileProgressPercent(item: AppearancePatentHistoryItem) { const percent = Number(item.fileProgressPercent || 0) if (Number.isFinite(percent) && percent > 0) { diff --git a/frontend-vue/src/pages/brand/components/BrandCollectDataTab.vue b/frontend-vue/src/pages/brand/components/BrandCollectDataTab.vue index 3d3ec6b..49d3386 100644 --- a/frontend-vue/src/pages/brand/components/BrandCollectDataTab.vue +++ b/frontend-vue/src/pages/brand/components/BrandCollectDataTab.vue @@ -149,6 +149,8 @@
任务 ID:{{ item.taskId }}
开始时间:{{ formatDateTime(item.startedAt || item.createdAt) }}
行数:{{ item.rowCount ?? '-' }}
+
总数据去重过滤:{{ item.dedupeFilteredCount ?? 0 }}
+
不符合 ASIN 过滤:{{ item.invalidFilteredCount ?? 0 }}
{{ statusText(item) }} @@ -170,6 +172,8 @@
任务 ID:{{ item.taskId ?? '-' }}
开始时间:{{ formatDateTime(item.startedAt || item.createdAt) }}
结束时间:{{ formatDateTime(item.finishedAt) }}
+
总数据去重过滤:{{ item.dedupeFilteredCount ?? 0 }}
+
不符合 ASIN 过滤:{{ item.invalidFilteredCount ?? 0 }}
{{ item.resultFilename }}
错误:{{ item.error }}
diff --git a/frontend-vue/src/shared/api/java-modules.ts b/frontend-vue/src/shared/api/java-modules.ts index 1eaf901..ff120b9 100644 --- a/frontend-vue/src/shared/api/java-modules.ts +++ b/frontend-vue/src/shared/api/java-modules.ts @@ -2283,6 +2283,10 @@ export interface CollectDataHistoryItem { success?: boolean; error?: string; rowCount?: number; + dedupeFilteredCount?: number; + invalidFilteredCount?: number; + brandRejectedCount?: number; + finalRowCount?: number; createdAt?: string; startedAt?: string; finishedAt?: string; diff --git a/user_data/api_logs/api_2026_05_07.log b/user_data/api_logs/api_2026_05_07.log deleted file mode 100644 index e69de29..0000000 diff --git a/xlsx/1-1.xlsx b/xlsx/1-1.xlsx deleted file mode 100644 index 18bff1c28ecb69883bda9164cb9d464467f8d646..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 16071 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 diff --git a/xlsx/1.xlsx b/xlsx/1.xlsx deleted file mode 100644 index a6e124d0dbe2f6c1357bab05ac304a389d469c0b..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 15829 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