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