diff --git a/app/.env b/app/.env index 20a1f7e..ef3e12b 100644 --- a/app/.env +++ b/app/.env @@ -12,8 +12,8 @@ client_name=ShuFuAI # java_api_base=http://47.111.163.154:18080 -java_api_base=http://127.0.0.1:18080 +# java_api_base=http://127.0.0.1:18080 # java_api_base=http://8.136.19.173:18080 -# java_api_base=http://121.196.149.225:18080 +java_api_base=http://121.196.149.225:18080 diff --git a/app/assets/appearance-patent.js b/app/assets/appearance-patent.js index 8bf320c..f25f733 100644 --- a/app/assets/appearance-patent.js +++ b/app/assets/appearance-patent.js @@ -1,6 +1,6 @@ -import{d as Se,o as Pe,L as Fe,a as i,c as u,b,B as Te,e as a,F as j,r as O,t as n,w as Ee,G as Ne,f as _,U as Ae,V as Ue,W as Re,X as De,N as xe,n as Ke,g as f,h as B,i as R,E as d,Y as $e,Z as Be,T as oe,$ as Le,a0 as Ge,a1 as He,a2 as Me,a3 as qe,a4 as ze,_ as je,q as Oe,s as Ve}from"./pywebview-BtdhG28Q.js";/* empty css */import{e as Je}from"./brand-Lp7YWffz.js";import{c as Xe}from"./categorized-timers-JPA-olTr.js";const Ye={class:"page-shell module-page"},Qe={class:"main-content"},We={class:"left-panel"},Ze={class:"upload-zone"},es={class:"selected-files"},ss={key:0},ts={class:"prompt-card"},as={class:"run-row"},ls=["disabled"],ns=["disabled"],os={key:0,class:"parse-card"},rs={key:1,class:"queue-payload"},is={class:"right-panel"},us={class:"task-list-wrap"},cs={class:"clean-result-summary"},ds={class:"summary-card"},ps={class:"summary-card"},fs={class:"summary-card"},vs={class:"summary-card"},gs={class:"result-list-wrap preview-wrap"},hs={class:"result-list-header"},ys={key:0,class:"muted"},_s={key:0,class:"empty-tasks"},ks={class:"result-list-wrap"},ms={key:0,class:"empty-tasks"},ws={key:1,class:"task-list clean-result-list"},Is={class:"left"},bs={class:"id"},Cs={class:"files"},Ss={class:"files"},Ps={class:"task-right"},Fs={class:"status running"},Ts=["onClick"],Es={class:"result-list-wrap"},Ns={key:0,class:"empty-tasks"},As={key:1,class:"task-list clean-result-list"},Us={class:"left"},Rs={class:"id"},Ds={class:"files"},xs={key:0,class:"files"},Ks={key:1,class:"files result-hint"},$s={key:2,class:"file-progress"},Bs={class:"file-progress-meta"},Ls={class:"file-progress-track"},Gs={class:"file-progress-count"},Hs={key:3,class:"files"},Ms={class:"task-right"},qs=["onClick"],zs=["onClick"],re=`请帮我排查以下亚马逊商品在英国及欧洲地区是否存在知识产权侵权风险。 +import{d as Se,o as Pe,L as Fe,a as i,c as u,b,B as Te,e as a,F as j,r as O,t as n,w as Ee,G as Ne,f as _,U as Ae,V as Ue,W as Re,X as De,N as xe,n as Ke,g as f,h as B,i as A,E as d,Y as $e,Z as Be,T as oe,$ as Le,a0 as Ge,a1 as He,a2 as Me,a3 as qe,a4 as ze,_ as je,q as Oe,s as Ve}from"./pywebview-BtdhG28Q.js";/* empty css */import{e as Je}from"./brand-Lp7YWffz.js";import{c as Xe}from"./categorized-timers-JPA-olTr.js";const Ye={class:"page-shell module-page"},Qe={class:"main-content"},We={class:"left-panel"},Ze={class:"upload-zone"},es={class:"selected-files"},ss={key:0},ts={class:"prompt-card"},as={class:"run-row"},ls=["disabled"],ns=["disabled"],os={key:0,class:"parse-card"},rs={key:1,class:"queue-payload"},is={class:"right-panel"},us={class:"task-list-wrap"},cs={class:"clean-result-summary"},ds={class:"summary-card"},ps={class:"summary-card"},fs={class:"summary-card"},vs={class:"summary-card"},gs={class:"result-list-wrap preview-wrap"},hs={class:"result-list-header"},ys={key:0,class:"muted"},_s={key:0,class:"empty-tasks"},ks={class:"result-list-wrap"},ms={key:0,class:"empty-tasks"},ws={key:1,class:"task-list clean-result-list"},Is={class:"left"},bs={class:"id"},Cs={class:"files"},Ss={class:"files"},Ps={class:"task-right"},Fs={class:"status running"},Ts=["onClick"],Es={class:"result-list-wrap"},Ns={key:0,class:"empty-tasks"},As={key:1,class:"task-list clean-result-list"},Us={class:"left"},Rs={class:"id"},Ds={class:"files"},xs={key:0,class:"files"},Ks={key:1,class:"files result-hint"},$s={key:2,class:"file-progress"},Bs={class:"file-progress-meta"},Ls={class:"file-progress-track"},Gs={class:"file-progress-count"},Hs={key:3,class:"files"},Ms={class:"task-right"},qs=["onClick"],zs=["onClick"],re=`请帮我排查以下亚马逊商品在英国及欧洲地区是否存在知识产权侵权风险。 请直接给出明确结论(有侵权风险 或 未发现明显侵权风险),并严格按照以下三个维度提供精简的排查理由: 外观维度: 评估产品外形、图案设计是否与欧洲/英国常见外观专利雷同。 专利维度: 评估产品的核心技术或物理结构是否存在侵权可能。 标题维度: 排查标题中是否包含大牌商标、敏感词或版权保护词汇。 -结论:XX`,js=3e3,Os=Se({__name:"BrandAppearancePatentTab",setup(Js){const D=f([]),C=f([]),v=f(null),k=f([]),L=f(""),x=f(!1),K=f(!1),m=f(""),p=f([]),c=f([]),y=f(null),G=f(!1);let S=null,V=0,P=!1;const F=Xe("appearance-patent"),T=f({pendingTaskCount:0,processedTaskCount:0,successTaskCount:0,failedTaskCount:0}),g=f([]),w=B(()=>k.value.flatMap(s=>s.items||[])),J=B(()=>k.value[0]?.items||[]),X=B(()=>g.value.filter(s=>s.taskId!=null&&p.value.includes(s.taskId))),Y=B(()=>g.value.filter(s=>{const e=$(s);return e!=="RUNNING"&&e!=="PENDING"}));function Q(){return L.value.trim()||re}function ie(){return typeof window<"u"&&window.localStorage.getItem("uid")||"0"}function H(){return`appearance-patent:tasks:${ie()}`}function I(){if(typeof window>"u")return;const s=Array.from(new Set([...p.value,...c.value]));s.length?window.localStorage.setItem(H(),JSON.stringify(s)):window.localStorage.removeItem(H())}function ue(){if(!(typeof window>"u"))try{const s=window.localStorage.getItem(H()),e=s?JSON.parse(s):[];p.value=Array.isArray(e)?e.filter(l=>typeof l=="number"&&l>0):[]}catch{p.value=[]}}async function W(s){const e=R();if(!e?.upload_file_to_java)throw new Error("当前桌面端未提供文件上传能力");const l=[];for(const r of s){const t=typeof r=="string"?r:r.absolutePath,o=typeof r=="string"?void 0:r.relativePath,h=await e.upload_file_to_java(t,o);if(!h?.success||!h.data)throw new Error(h?.error||h?.message||`上传失败:${t}`);l.push(h.data)}return l}async function ce(){const s=R();if(!s?.select_brand_xlsx_files||!s.upload_file_to_java){d.warning("当前环境不支持文件选择或上传");return}const e=await s.select_brand_xlsx_files();if(!e?.length)return;const l=await W(e);C.value=l,D.value=l.map(r=>r.relativePath||r.originalFilename||r.fileKey),v.value=null,k.value=[],m.value=""}async function de(){const s=R();if(!s?.select_brand_folder){d.warning("当前环境不支持文件夹选择,请在本地客户端中打开");return}try{const e=await s.select_brand_folder();if(!e)return;const l=await Je(e);if(!l.success||!l.items?.length){d.warning(l.error||"该文件夹下没有可用的 xlsx 文件");return}const r=await W(l.items);C.value=r,D.value=l.items.map(t=>t.relativePath||t.absolutePath),v.value=null,k.value=[],m.value="",d.success(`已选择文件夹内 ${l.items.length} 个 Excel 文件`)}catch(e){d.error(e instanceof Error?e.message:"选择失败")}}async function pe(){if(!C.value.length){d.warning("请先选择 Excel");return}x.value=!0;try{const s=C.value.map(l=>({fileKey:l.fileKey,originalFilename:l.originalFilename,relativePath:l.relativePath})),e=await $e(s,Q());v.value=e,k.value=e.groups?.length?e.groups:e.items?.length?[{sourceFileKey:e.items[0]?.sourceFileKey,sourceFilename:e.items[0]?.sourceFilename,groupKey:e.items[0]?.groupKey,baseId:"",displayId:e.items[0]?.displayId,itemCount:e.items.length,items:e.items}]:[],m.value="",await N(),await A({force:!0}),d.success(`解析完成,共 ${e.groupCount||0} 组 / ${e.acceptedRows} 条`)}catch(s){d.error(s instanceof Error?s.message:"解析失败")}finally{x.value=!1}}async function fe(){const s=R(),e=v.value?.taskId;if(!e||!w.value.length){d.warning("请先解析文件");return}if(!s?.enqueue_json){d.error("当前环境未启用 pywebview enqueue_json");return}K.value=!0;try{const l={type:"appearance-patent-run",ts:Date.now(),data:{taskId:e,prompt:v.value?.aiPrompt||Q(),groups:k.value.map(t=>({sourceFileKey:t.sourceFileKey||"",sourceFilename:t.sourceFilename||"",groupKey:t.groupKey||"",baseId:t.baseId||"",displayId:t.displayId||"",items:(t.items||[]).map(o=>({sourceFileKey:o.sourceFileKey||"",sourceFilename:o.sourceFilename||"",rowToken:o.rowToken||"",groupKey:o.groupKey||"",id:o.displayId,asin:o.asin,country:o.country,url:o.url||"",title:o.title||""}))})),rows:w.value.map(t=>({sourceFileKey:t.sourceFileKey||"",sourceFilename:t.sourceFilename||"",rowToken:t.rowToken||"",groupKey:t.groupKey||"",id:t.displayId,asin:t.asin,country:t.country,url:t.url||"",title:t.title||""}))}};m.value=JSON.stringify(l,null,2),await Be(e);const r=await s.enqueue_json(l);if(!r?.success){d.error(r?.error||"推送失败");return}ge(e),ve(),await N(),await A({force:!0}),d.success("已推送到 Python 队列")}finally{K.value=!1}}function ve(){v.value=null,k.value=[],m.value=""}function ge(s){p.value.includes(s)||(p.value=[...p.value,s],I()),E()}function M(s){p.value=p.value.filter(e=>e!==s),I()}function he(s){c.value.includes(s)||(c.value=[...c.value,s],I()),E()}function q(s){c.value=c.value.filter(e=>e!==s),I()}function E(){P||y.value==null&&ye(!0)}function Z(){y.value!=null&&(F.clearTimer("task-poll",y.value),y.value=null)}function ye(s=!1){if(P)return;if(y.value!=null){if(!s)return;F.clearTimer("task-poll",y.value),y.value=null}const e=async()=>{y.value=null,!P&&(!p.value.length&&!c.value.length||(await _e(),!P&&(p.value.length||c.value.length)&&(y.value=F.setTimeout("task-poll",e,oe()))))};s?e():y.value=F.setTimeout("task-poll",e,oe())}async function _e(){if(G.value||!p.value.length&&!c.value.length){!p.value.length&&!c.value.length&&Z();return}G.value=!0;try{let s=!1,e=!1;const l=Array.from(new Set([...p.value,...c.value]));if(l.length){const r=await ze(l,{force:c.value.length>0});for(const t of r.items||[]){const o=t.task;if(!o?.id)continue;const h=t.items?.[0];if(h&&ke(h),(o.status==="SUCCESS"||o.status==="FAILED")&&(M(o.id),s=!0,o.status==="SUCCESS"?he(o.id):q(o.id)),h&&o.status==="SUCCESS"&&c.value.includes(o.id)){const ne=(h.fileStatus||"").toUpperCase();(h.fileReady||ne==="SUCCESS"||ne==="FAILED")&&(q(o.id),s=!0,e=!0)}}r.missingTaskIds?.length&&(r.missingTaskIds.forEach(t=>{M(t),q(t)}),e=!0,s=!0)}s&&await N(),e&&await A(),me()}finally{G.value=!1}}function ke(s){if(!s.taskId&&!s.resultId)return;const e=g.value.findIndex(l=>s.resultId!=null&&l.resultId===s.resultId||s.taskId!=null&&l.taskId===s.taskId);e>=0?g.value[e]={...g.value[e],...s}:g.value=[s,...g.value]}function me(){if(!c.value.length)return;const s=[];for(const e of c.value){const l=g.value.find(o=>o.taskId===e);if(!l){s.push(e);continue}const r=(l.taskStatus||"").toUpperCase(),t=(l.fileStatus||"").toUpperCase();r==="FAILED"||l.fileReady||t==="SUCCESS"||t==="FAILED"||r==="SUCCESS"&&s.push(e)}c.value=s,I(),c.value.length&&E()}function we(){const s=g.value.filter(e=>e.taskId!=null&&U(e)).map(e=>e.taskId);c.value=Array.from(new Set(s)),I(),c.value.length&&E()}async function N(){T.value=await Le()}async function A(s={}){const e=Date.now();if(!(!s.force&&g.value.length&&e-V{g.value=l.items||[],V=Date.now()}).finally(()=>{S=null}),S)}function ee(s){const e=$(s);return e==="RUNNING"?"执行中":e==="PENDING"?"已解析待推送":U(s)?"结果生成中":z(s)?"结果生成失败":e==="SUCCESS"||s.success?"已完成":e==="FAILED"?"失败":"等待中"}function Ie(s){const e=$(s);return e==="RUNNING"||U(s)?"running":z(s)||e==="FAILED"?"failed":e==="SUCCESS"||s.success?"success":"pending"}function U(s){const e=$(s),l=(s.fileStatus||"").toUpperCase();return!(e!=="SUCCESS"||te(s)||l==="FAILED"||l==="SUCCESS")}function $(s){return(s.taskStatus||"").toUpperCase()}function z(s){return(s.fileStatus||"").toUpperCase()==="FAILED"}function se(s){if(z(s))return s.fileError||"结果文件生成失败,请稍后重试或检查后端日志";if(!U(s))return"";const e=(s.fileStatus||"").toUpperCase();return e==="RUNNING"?"任务已完成,正在请求 Coze 或组装结果文件,下载按钮稍后出现":e==="PENDING"?"任务已完成,结果文件已入队,正在等待后端生成":"任务已完成,正在生成结果文件,下载按钮稍后出现"}function te(s){return!!(s.resultId&&(s.fileReady||s.downloadUrl))}function ae(s){const e=Number(s.fileProgressPercent||0);return Number.isFinite(e)?Math.max(0,Math.min(100,Math.round(e))):0}function be(s){return U(s)&&(s.fileProgressTotal||0)>0}async function Ce(s){if(!s.resultId)return;const e=R();if(!e?.save_file_from_url_new){d.error("当前客户端未提供下载能力");return}const l=s.downloadUrl||He(s.resultId),r=s.resultFilename||`${s.sourceFilename||"appearance-patent"}.xlsx`,t=await e.save_file_from_url_new(l,r);t.success?d.success(`已保存: ${t.path||r}`):t.error&&t.error!=="用户取消"&&d.error(t.error)}async function le(s){try{s.taskStatus==="RUNNING"&&s.taskId?(await Me(s.taskId),M(s.taskId)):s.resultId&&await qe(s.resultId),await N(),await A({force:!0}),d.success("已删除")}catch(e){d.error(e instanceof Error?e.message:"删除失败")}}return Pe(async()=>{ue(),await Promise.all([N().catch(()=>{}),A().catch(()=>{})]),we(),(p.value.length||c.value.length)&&E()}),Fe(()=>{P=!0,Z(),F.clearScope()}),(s,e)=>{const l=Re,r=De;return i(),u("div",Ye,[b(Te,{active:"appearance-patent"}),a("div",Qe,[a("aside",We,[e[4]||(e[4]=a("div",{class:"section-title"},"上传文件",-1)),a("div",Ze,[e[1]||(e[1]=a("div",{class:"hint"},"选择 Excel 后点击解析,后端会提取 id、ASIN、国家,并保留整数 id 与子数据的第一条。",-1)),a("div",{class:"btns"},[a("button",{type:"button",class:"opt-btn",onClick:ce},"选择 Excel"),a("button",{type:"button",class:"opt-btn",onClick:de},"选择文件夹")]),a("div",es,[D.value.length?(i(!0),u(j,{key:1},O(D.value,t=>(i(),u("span",{key:t},n(t),1))),128)):(i(),u("span",ss,"暂未选择文件"))])]),a("div",ts,[e[2]||(e[2]=a("div",{class:"section-title"},"AI 提示词",-1)),Ee(a("textarea",{"onUpdate:modelValue":e[0]||(e[0]=t=>L.value=t),class:"prompt-input",rows:"8",placeholder:"可选,留空时使用下方默认提示词"},null,512),[[Ne,L.value]]),e[3]||(e[3]=a("div",{class:"prompt-default-label"},"留空时默认使用以下提示词",-1)),a("div",{class:"prompt-preview"},n(re))]),a("div",as,[a("button",{type:"button",class:"btn-run",disabled:x.value||!C.value.length,onClick:pe},n(x.value?"解析中...":"解析并创建任务"),9,ls),a("button",{type:"button",class:"btn-run btn-queue",disabled:K.value||!w.value.length,onClick:fe},n(K.value?"推送中...":"推送到 Python 队列"),9,ns)]),e[5]||(e[5]=a("p",{class:"loading-msg"},"Python 回传字段按 asin、国家、url、标题提交;后端接收分片后按 10 条一批调用 Coze。",-1)),v.value?(i(),u("div",os,[a("div",null,"任务 ID:"+n(v.value.taskId),1),a("div",null,"总行数:"+n(v.value.totalRows)+",有效:"+n(v.value.acceptedRows)+",分组:"+n(v.value.groupCount||0)+",过滤:"+n(v.value.droppedRows),1)])):_("",!0),m.value?(i(),u("pre",rs,n(m.value),1)):_("",!0)]),a("section",is,[e[14]||(e[14]=a("div",{class:"panel-header"},"外观专利检测",-1)),a("div",us,[a("div",cs,[a("div",ds,[e[6]||(e[6]=a("span",{class:"summary-label"},"运行中任务",-1)),a("strong",null,n(T.value.pendingTaskCount),1)]),a("div",ps,[e[7]||(e[7]=a("span",{class:"summary-label"},"已结束任务",-1)),a("strong",null,n(T.value.processedTaskCount),1)]),a("div",fs,[e[8]||(e[8]=a("span",{class:"summary-label"},"成功任务",-1)),a("strong",null,n(T.value.successTaskCount),1)]),a("div",vs,[e[9]||(e[9]=a("span",{class:"summary-label"},"失败任务",-1)),a("strong",null,n(T.value.failedTaskCount),1)])]),e[13]||(e[13]=a("div",{class:"subsection-title"},"匹配任务",-1)),a("div",gs,[a("div",hs,[e[10]||(e[10]=a("span",null,"解析结果",-1)),w.value.length?(i(),u("span",ys,"显示首组 "+n(J.value.length)+" 条 / 共 "+n(v.value?.groupCount||k.value.length)+" 组,"+n(w.value.length)+" 条",1)):_("",!0)]),w.value.length?(i(),Ae(r,{key:1,data:J.value,height:"120",class:"result-table"},{default:Ue(()=>[b(l,{prop:"displayId",label:"ID",width:"90"}),b(l,{prop:"asin",label:"ASIN",width:"130"}),b(l,{prop:"country",label:"国家",width:"100"}),b(l,{prop:"title",label:"标题","min-width":"160","show-overflow-tooltip":""}),b(l,{prop:"url",label:"URL","min-width":"160","show-overflow-tooltip":""})]),_:1},8,["data"])):(i(),u("div",_s,"暂无解析结果"))]),a("div",ks,[e[11]||(e[11]=a("div",{class:"result-list-header"},[a("span",null,"当前任务")],-1)),X.value.length?(i(),u("ul",ws,[(i(!0),u(j,null,O(X.value,t=>(i(),u("li",{key:`cur-${t.taskId}`,class:"task-item"},[a("div",Is,[a("span",bs,n(t.sourceFilename||"外观专利检测"),1),a("div",Cs,"任务 ID:"+n(t.taskId),1),a("div",Ss,"行数:"+n(t.rowCount??"-"),1)]),a("div",Ps,[a("span",Fs,n(ee(t)),1),a("button",{type:"button",class:"btn-delete",onClick:o=>le(t)},"删除",8,Ts)])]))),128))])):(i(),u("div",ms,"暂无当前任务"))]),a("div",Es,[e[12]||(e[12]=a("div",{class:"result-list-header"},[a("span",null,"历史记录")],-1)),Y.value.length?(i(),u("ul",As,[(i(!0),u(j,null,O(Y.value,t=>(i(),u("li",{key:`his-${t.resultId}-${t.taskId}`,class:"task-item"},[a("div",Us,[a("span",Rs,n(t.sourceFilename||"外观专利检测"),1),a("div",Ds,"任务 ID:"+n(t.taskId??"-"),1),t.resultFilename?(i(),u("div",xs,n(t.resultFilename||"下载结果"),1)):_("",!0),se(t)?(i(),u("div",Ks,n(se(t)),1)):_("",!0),be(t)?(i(),u("div",$s,[a("div",Bs,[a("span",null,n(t.fileProgressMessage||"结果生成中"),1),a("span",null,n(ae(t))+"%",1)]),a("div",Ls,[a("div",{class:"file-progress-bar",style:xe({width:`${ae(t)}%`})},null,4)]),a("div",Gs,n(t.fileProgressCurrent||0)+"/"+n(t.fileProgressTotal||0),1)])):_("",!0),t.error?(i(),u("div",Hs,"错误:"+n(t.error),1)):_("",!0)]),a("div",Ms,[a("span",{class:Ke(["status",Ie(t)])},n(ee(t)),3),te(t)?(i(),u("button",{key:0,type:"button",class:"download",onClick:o=>Ce(t)},"下载",8,qs)):_("",!0),t.resultId?(i(),u("button",{key:1,type:"button",class:"btn-delete",onClick:o=>le(t)},"删除",8,zs)):_("",!0)])]))),128))])):(i(),u("div",Ns,"暂无历史记录"))])])])])])}}}),Vs=je(Os,[["__scopeId","data-v-12cbad16"]]);Oe(Vs).use(Ve).mount("#app"); +结论:XX`,js=3e3,Os=Se({__name:"BrandAppearancePatentTab",setup(Js){const U=f([]),C=f([]),v=f(null),k=f([]),L=f(""),R=f(!1),D=f(!1),m=f(""),p=f([]),c=f([]),g=f(null),G=f(!1);let S=null,V=0,P=!1;const F=Xe("appearance-patent"),T=f({pendingTaskCount:0,processedTaskCount:0,successTaskCount:0,failedTaskCount:0}),h=f([]),w=B(()=>k.value.flatMap(s=>s.items||[])),J=B(()=>k.value[0]?.items||[]),X=B(()=>h.value.filter(s=>s.taskId!=null&&p.value.includes(s.taskId))),Y=B(()=>h.value.filter(s=>{const e=$(s);return e!=="RUNNING"&&e!=="PENDING"}));function Q(){return L.value.trim()||re}function ie(){return typeof window<"u"&&window.localStorage.getItem("uid")||"0"}function H(){return`appearance-patent:tasks:${ie()}`}function I(){if(typeof window>"u")return;const s=Array.from(new Set([...p.value,...c.value]));s.length?window.localStorage.setItem(H(),JSON.stringify(s)):window.localStorage.removeItem(H())}function ue(){if(!(typeof window>"u"))try{const s=window.localStorage.getItem(H()),e=s?JSON.parse(s):[];p.value=Array.isArray(e)?e.filter(l=>typeof l=="number"&&l>0):[]}catch{p.value=[]}}async function W(s){const e=A();if(!e?.upload_file_to_java)throw new Error("当前桌面端未提供文件上传能力");const l=[];for(const r of s){const t=typeof r=="string"?r:r.absolutePath,o=typeof r=="string"?void 0:r.relativePath,y=await e.upload_file_to_java(t,o);if(!y?.success||!y.data)throw new Error(y?.error||y?.message||`上传失败:${t}`);l.push(y.data)}return l}async function ce(){const s=A();if(!s?.select_brand_xlsx_files||!s.upload_file_to_java){d.warning("当前环境不支持文件选择或上传");return}const e=await s.select_brand_xlsx_files();if(!e?.length)return;const l=await W(e);C.value=l,U.value=l.map(r=>r.relativePath||r.originalFilename||r.fileKey),v.value=null,k.value=[],m.value=""}async function de(){const s=A();if(!s?.select_brand_folder){d.warning("当前环境不支持文件夹选择,请在本地客户端中打开");return}try{const e=await s.select_brand_folder();if(!e)return;const l=await Je(e);if(!l.success||!l.items?.length){d.warning(l.error||"该文件夹下没有可用的 xlsx 文件");return}const r=await W(l.items);C.value=r,U.value=l.items.map(t=>t.relativePath||t.absolutePath),v.value=null,k.value=[],m.value="",d.success(`已选择文件夹内 ${l.items.length} 个 Excel 文件`)}catch(e){d.error(e instanceof Error?e.message:"选择失败")}}async function pe(){if(!C.value.length){d.warning("请先选择 Excel");return}R.value=!0;try{const s=C.value.map(l=>({fileKey:l.fileKey,originalFilename:l.originalFilename,relativePath:l.relativePath})),e=await $e(s,Q());v.value=e,k.value=e.groups?.length?e.groups:e.items?.length?[{sourceFileKey:e.items[0]?.sourceFileKey,sourceFilename:e.items[0]?.sourceFilename,groupKey:e.items[0]?.groupKey,baseId:"",displayId:e.items[0]?.displayId,itemCount:e.items.length,items:e.items}]:[],m.value="",d.success(`解析完成,共 ${e.groupCount||0} 组 / ${e.acceptedRows} 条`)}catch(s){d.error(s instanceof Error?s.message:"解析失败")}finally{R.value=!1}}async function fe(){const s=A(),e=v.value?.taskId;if(!e||!w.value.length){d.warning("请先解析文件");return}if(!s?.enqueue_json){d.error("当前环境未启用 pywebview enqueue_json");return}D.value=!0;try{const l={type:"appearance-patent-run",ts:Date.now(),data:{taskId:e,prompt:v.value?.aiPrompt||Q(),groups:k.value.map(t=>({sourceFileKey:t.sourceFileKey||"",sourceFilename:t.sourceFilename||"",groupKey:t.groupKey||"",baseId:t.baseId||"",displayId:t.displayId||"",items:(t.items||[]).map(o=>({sourceFileKey:o.sourceFileKey||"",sourceFilename:o.sourceFilename||"",rowToken:o.rowToken||"",groupKey:o.groupKey||"",id:o.displayId,asin:o.asin,country:o.country,url:o.url||"",title:o.title||""}))})),rows:w.value.map(t=>({sourceFileKey:t.sourceFileKey||"",sourceFilename:t.sourceFilename||"",rowToken:t.rowToken||"",groupKey:t.groupKey||"",id:t.displayId,asin:t.asin,country:t.country,url:t.url||"",title:t.title||""}))}};m.value=JSON.stringify(l,null,2),await Be(e);const r=await s.enqueue_json(l);if(!r?.success){d.error(r?.error||"推送失败");return}ge(e),ve(),await x(),await K({force:!0}),d.success("已推送到 Python 队列")}finally{D.value=!1}}function ve(){v.value=null,k.value=[],m.value=""}function ge(s){p.value.includes(s)||(p.value=[...p.value,s],I()),E()}function M(s){p.value=p.value.filter(e=>e!==s),I()}function he(s){c.value.includes(s)||(c.value=[...c.value,s],I()),E()}function q(s){c.value=c.value.filter(e=>e!==s),I()}function E(){P||g.value==null&&ye(!0)}function Z(){g.value!=null&&(F.clearTimer("task-poll",g.value),g.value=null)}function ye(s=!1){if(P)return;if(g.value!=null){if(!s)return;F.clearTimer("task-poll",g.value),g.value=null}const e=async()=>{g.value=null,!P&&(!p.value.length&&!c.value.length||(await _e(),!P&&g.value==null&&(p.value.length||c.value.length)&&(g.value=F.setTimeout("task-poll",e,oe()))))};s?e():g.value=F.setTimeout("task-poll",e,oe())}async function _e(){if(G.value||!p.value.length&&!c.value.length){!p.value.length&&!c.value.length&&Z();return}G.value=!0;try{let s=!1,e=!1;const l=Array.from(new Set([...p.value,...c.value]));if(l.length){const r=await ze(l,{force:c.value.length>0});for(const t of r.items||[]){const o=t.task;if(!o?.id)continue;const y=t.items?.[0];if(y&&ke(y),(o.status==="SUCCESS"||o.status==="FAILED")&&(M(o.id),s=!0,o.status==="SUCCESS"?he(o.id):q(o.id)),y&&o.status==="SUCCESS"&&c.value.includes(o.id)){const ne=(y.fileStatus||"").toUpperCase();(y.fileReady||ne==="SUCCESS"||ne==="FAILED")&&(q(o.id),s=!0,e=!0)}}r.missingTaskIds?.length&&(r.missingTaskIds.forEach(t=>{M(t),q(t)}),e=!0,s=!0)}s&&await x(),e&&await K(),me()}finally{G.value=!1}}function ke(s){if(!s.taskId&&!s.resultId)return;const e=h.value.findIndex(l=>s.resultId!=null&&l.resultId===s.resultId||s.taskId!=null&&l.taskId===s.taskId);e>=0?h.value[e]={...h.value[e],...s}:h.value=[s,...h.value]}function me(){if(!c.value.length)return;const s=[];for(const e of c.value){const l=h.value.find(o=>o.taskId===e);if(!l){s.push(e);continue}const r=(l.taskStatus||"").toUpperCase(),t=(l.fileStatus||"").toUpperCase();r==="FAILED"||l.fileReady||t==="SUCCESS"||t==="FAILED"||r==="SUCCESS"&&s.push(e)}c.value=s,I(),c.value.length&&E()}function we(){const s=h.value.filter(e=>e.taskId!=null&&N(e)).map(e=>e.taskId);c.value=Array.from(new Set(s)),I(),c.value.length&&E()}async function x(){T.value=await Le()}async function K(s={}){const e=Date.now();if(!(!s.force&&h.value.length&&e-V{h.value=l.items||[],V=Date.now()}).finally(()=>{S=null}),S)}function ee(s){const e=$(s);return e==="RUNNING"?"执行中":e==="PENDING"?"已解析待推送":N(s)?"结果生成中":z(s)?"结果生成失败":e==="SUCCESS"||s.success?"已完成":e==="FAILED"?"失败":"等待中"}function Ie(s){const e=$(s);return e==="RUNNING"||N(s)?"running":z(s)||e==="FAILED"?"failed":e==="SUCCESS"||s.success?"success":"pending"}function N(s){const e=$(s),l=(s.fileStatus||"").toUpperCase();return!(e!=="SUCCESS"||te(s)||l==="FAILED"||l==="SUCCESS")}function $(s){return(s.taskStatus||"").toUpperCase()}function z(s){return(s.fileStatus||"").toUpperCase()==="FAILED"}function se(s){if(z(s))return s.fileError||"结果文件生成失败,请稍后重试或检查后端日志";if(!N(s))return"";const e=(s.fileStatus||"").toUpperCase();return e==="RUNNING"?"任务已完成,正在请求 Coze 或组装结果文件,下载按钮稍后出现":e==="PENDING"?"任务已完成,结果文件已入队,正在等待后端生成":"任务已完成,正在生成结果文件,下载按钮稍后出现"}function te(s){return!!(s.resultId&&(s.fileReady||s.downloadUrl))}function ae(s){const e=Number(s.fileProgressPercent||0);return Number.isFinite(e)?Math.max(0,Math.min(100,Math.round(e))):0}function be(s){return N(s)&&(s.fileProgressTotal||0)>0}async function Ce(s){if(!s.resultId)return;const e=A();if(!e?.save_file_from_url_new){d.error("当前客户端未提供下载能力");return}const l=s.downloadUrl||He(s.resultId),r=s.resultFilename||`${s.sourceFilename||"appearance-patent"}.xlsx`,t=await e.save_file_from_url_new(l,r);t.success?d.success(`已保存: ${t.path||r}`):t.error&&t.error!=="用户取消"&&d.error(t.error)}async function le(s){try{s.taskStatus==="RUNNING"&&s.taskId?(await Me(s.taskId),M(s.taskId)):s.resultId&&await qe(s.resultId),await x(),await K({force:!0}),d.success("已删除")}catch(e){d.error(e instanceof Error?e.message:"删除失败")}}return Pe(async()=>{ue(),await Promise.all([x().catch(()=>{}),K().catch(()=>{})]),we(),(p.value.length||c.value.length)&&E()}),Fe(()=>{P=!0,Z(),F.clearScope()}),(s,e)=>{const l=Re,r=De;return i(),u("div",Ye,[b(Te,{active:"appearance-patent"}),a("div",Qe,[a("aside",We,[e[4]||(e[4]=a("div",{class:"section-title"},"上传文件",-1)),a("div",Ze,[e[1]||(e[1]=a("div",{class:"hint"},"选择 Excel 后点击解析,后端会提取 id、ASIN、国家,并保留整数 id 与子数据的第一条。",-1)),a("div",{class:"btns"},[a("button",{type:"button",class:"opt-btn",onClick:ce},"选择 Excel"),a("button",{type:"button",class:"opt-btn",onClick:de},"选择文件夹")]),a("div",es,[U.value.length?(i(!0),u(j,{key:1},O(U.value,t=>(i(),u("span",{key:t},n(t),1))),128)):(i(),u("span",ss,"暂未选择文件"))])]),a("div",ts,[e[2]||(e[2]=a("div",{class:"section-title"},"AI 提示词",-1)),Ee(a("textarea",{"onUpdate:modelValue":e[0]||(e[0]=t=>L.value=t),class:"prompt-input",rows:"8",placeholder:"可选,留空时使用下方默认提示词"},null,512),[[Ne,L.value]]),e[3]||(e[3]=a("div",{class:"prompt-default-label"},"留空时默认使用以下提示词",-1)),a("div",{class:"prompt-preview"},n(re))]),a("div",as,[a("button",{type:"button",class:"btn-run",disabled:R.value||!C.value.length,onClick:pe},n(R.value?"解析中...":"解析并创建任务"),9,ls),a("button",{type:"button",class:"btn-run btn-queue",disabled:D.value||!w.value.length,onClick:fe},n(D.value?"推送中...":"推送到 Python 队列"),9,ns)]),e[5]||(e[5]=a("p",{class:"loading-msg"},"Python 回传字段按 asin、国家、url、标题提交;后端接收分片后按 10 条一批调用 Coze。",-1)),v.value?(i(),u("div",os,[a("div",null,"任务 ID:"+n(v.value.taskId),1),a("div",null,"总行数:"+n(v.value.totalRows)+",有效:"+n(v.value.acceptedRows)+",分组:"+n(v.value.groupCount||0)+",过滤:"+n(v.value.droppedRows),1)])):_("",!0),m.value?(i(),u("pre",rs,n(m.value),1)):_("",!0)]),a("section",is,[e[14]||(e[14]=a("div",{class:"panel-header"},"外观专利检测",-1)),a("div",us,[a("div",cs,[a("div",ds,[e[6]||(e[6]=a("span",{class:"summary-label"},"运行中任务",-1)),a("strong",null,n(T.value.pendingTaskCount),1)]),a("div",ps,[e[7]||(e[7]=a("span",{class:"summary-label"},"已结束任务",-1)),a("strong",null,n(T.value.processedTaskCount),1)]),a("div",fs,[e[8]||(e[8]=a("span",{class:"summary-label"},"成功任务",-1)),a("strong",null,n(T.value.successTaskCount),1)]),a("div",vs,[e[9]||(e[9]=a("span",{class:"summary-label"},"失败任务",-1)),a("strong",null,n(T.value.failedTaskCount),1)])]),e[13]||(e[13]=a("div",{class:"subsection-title"},"匹配任务",-1)),a("div",gs,[a("div",hs,[e[10]||(e[10]=a("span",null,"解析结果",-1)),w.value.length?(i(),u("span",ys,"显示首组 "+n(J.value.length)+" 条 / 共 "+n(v.value?.groupCount||k.value.length)+" 组,"+n(w.value.length)+" 条",1)):_("",!0)]),w.value.length?(i(),Ae(r,{key:1,data:J.value,height:"120",class:"result-table"},{default:Ue(()=>[b(l,{prop:"displayId",label:"ID",width:"90"}),b(l,{prop:"asin",label:"ASIN",width:"130"}),b(l,{prop:"country",label:"国家",width:"100"}),b(l,{prop:"title",label:"标题","min-width":"160","show-overflow-tooltip":""}),b(l,{prop:"url",label:"URL","min-width":"160","show-overflow-tooltip":""})]),_:1},8,["data"])):(i(),u("div",_s,"暂无解析结果"))]),a("div",ks,[e[11]||(e[11]=a("div",{class:"result-list-header"},[a("span",null,"当前任务")],-1)),X.value.length?(i(),u("ul",ws,[(i(!0),u(j,null,O(X.value,t=>(i(),u("li",{key:`cur-${t.taskId}`,class:"task-item"},[a("div",Is,[a("span",bs,n(t.sourceFilename||"外观专利检测"),1),a("div",Cs,"任务 ID:"+n(t.taskId),1),a("div",Ss,"行数:"+n(t.rowCount??"-"),1)]),a("div",Ps,[a("span",Fs,n(ee(t)),1),a("button",{type:"button",class:"btn-delete",onClick:o=>le(t)},"删除",8,Ts)])]))),128))])):(i(),u("div",ms,"暂无当前任务"))]),a("div",Es,[e[12]||(e[12]=a("div",{class:"result-list-header"},[a("span",null,"历史记录")],-1)),Y.value.length?(i(),u("ul",As,[(i(!0),u(j,null,O(Y.value,t=>(i(),u("li",{key:`his-${t.resultId}-${t.taskId}`,class:"task-item"},[a("div",Us,[a("span",Rs,n(t.sourceFilename||"外观专利检测"),1),a("div",Ds,"任务 ID:"+n(t.taskId??"-"),1),t.resultFilename?(i(),u("div",xs,n(t.resultFilename||"下载结果"),1)):_("",!0),se(t)?(i(),u("div",Ks,n(se(t)),1)):_("",!0),be(t)?(i(),u("div",$s,[a("div",Bs,[a("span",null,n(t.fileProgressMessage||"结果生成中"),1),a("span",null,n(ae(t))+"%",1)]),a("div",Ls,[a("div",{class:"file-progress-bar",style:xe({width:`${ae(t)}%`})},null,4)]),a("div",Gs,n(t.fileProgressCurrent||0)+"/"+n(t.fileProgressTotal||0),1)])):_("",!0),t.error?(i(),u("div",Hs,"错误:"+n(t.error),1)):_("",!0)]),a("div",Ms,[a("span",{class:Ke(["status",Ie(t)])},n(ee(t)),3),te(t)?(i(),u("button",{key:0,type:"button",class:"download",onClick:o=>Ce(t)},"下载",8,qs)):_("",!0),t.resultId?(i(),u("button",{key:1,type:"button",class:"btn-delete",onClick:o=>le(t)},"删除",8,zs)):_("",!0)])]))),128))])):(i(),u("div",Ns,"暂无历史记录"))])])])])])}}}),Vs=je(Os,[["__scopeId","data-v-c78bd0f0"]]);Oe(Vs).use(Ve).mount("#app"); 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 efa4362..cc94432 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 @@ -149,9 +149,16 @@ public class AppearancePatentCozeClient { } String status = normalize(resolveWorkflowStatus(pollRoot)).toUpperCase(Locale.ROOT); - if (status.contains("FAIL") || status.contains("ERROR") || status.contains("CANCEL")) { + if (isFailedWorkflowStatus(status)) { throw new IllegalStateException(firstNonBlank(resolveFailureMessage(pollRoot), "Coze async workflow failed")); } + if (isSuccessfulWorkflowStatus(status)) { + String outputText = extractWorkflowOutputText(pollRoot); + if (!outputText.isBlank()) { + return wrapDataPayload(outputText); + } + throw new IllegalStateException("Coze async workflow completed without output"); + } sleepQuietly(Math.max(200, properties.getCozePollIntervalMillis())); } @@ -251,7 +258,7 @@ public class AppearancePatentCozeClient { return List.of(); } JsonNode dataRoot = objectMapper.readTree(dataText); - JsonNode array = dataRoot.path("data"); + JsonNode array = dataRoot.isArray() ? dataRoot : dataRoot.path("data"); List results = new ArrayList<>(); if (array.isArray()) { for (JsonNode node : array) { @@ -502,6 +509,23 @@ public class AppearancePatentCozeClient { return discoverEmbeddedData(root); } + private String extractWorkflowOutputText(JsonNode root) { + JsonNode outputNode = findFirstField(root, "output"); + if (outputNode == null || outputNode.isNull() || outputNode.isMissingNode()) { + return ""; + } + if (!outputNode.isTextual()) { + return outputNode.toString(); + } + String output = normalize(outputNode.asText("")); + if (output.isBlank()) { + return ""; + } + JsonNode parsedOutput = parseJsonOrMissing(output); + String nestedOutput = text(firstNonNull(parsedOutput.get("Output"), parsedOutput.get("output"))); + return nestedOutput == null || nestedOutput.isBlank() ? output : nestedOutput; + } + private String discoverEmbeddedData(JsonNode node) { if (node == null || node.isNull() || node.isMissingNode()) { return ""; @@ -577,6 +601,22 @@ public class AppearancePatentCozeClient { || item.has("patent reason")); } + private boolean isSuccessfulWorkflowStatus(String status) { + String normalized = normalize(status).toUpperCase(Locale.ROOT); + return normalized.equals("SUCCESS") + || normalized.equals("SUCCEEDED") + || normalized.equals("COMPLETED") + || normalized.equals("COMPLETE") + || normalized.equals("DONE"); + } + + private boolean isFailedWorkflowStatus(String status) { + String normalized = normalize(status).toUpperCase(Locale.ROOT); + return normalized.contains("FAIL") + || normalized.contains("ERROR") + || normalized.contains("CANCEL"); + } + private JsonNode parseJsonOrMissing(String value) { String normalized = normalize(value); if (!(normalized.startsWith("{") || normalized.startsWith("["))) { @@ -611,6 +651,38 @@ public class AppearancePatentCozeClient { return findTextByFieldName(root, "execute_id", "executeId"); } + private JsonNode findFirstField(JsonNode node, String name) { + if (node == null || node.isMissingNode() || node.isNull() || name == null || name.isBlank()) { + return objectMapper.missingNode(); + } + if (node.isTextual()) { + return findFirstField(parseJsonOrMissing(node.asText("")), name); + } + if (node.isArray()) { + for (JsonNode child : node) { + JsonNode found = findFirstField(child, name); + if (found != null && !found.isMissingNode() && !found.isNull()) { + return found; + } + } + return objectMapper.missingNode(); + } + if (node.isObject()) { + JsonNode direct = node.get(name); + if (direct != null && !direct.isMissingNode() && !direct.isNull()) { + return direct; + } + for (java.util.Iterator> it = node.fields(); it.hasNext(); ) { + Map.Entry entry = it.next(); + JsonNode found = findFirstField(entry.getValue(), name); + if (found != null && !found.isMissingNode() && !found.isNull()) { + return found; + } + } + } + return objectMapper.missingNode(); + } + private String findTextByFieldName(JsonNode node, String... names) { if (node == null || node.isMissingNode() || node.isNull()) { return ""; 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 ee9b483..d2ec25c 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 @@ -113,8 +113,8 @@ public class AppearancePatentTaskService { private final TransientPayloadStorageService transientPayloadStorageService; private final PlatformTransactionManager transactionManager; - @Transactional public AppearancePatentParseVo parseAndCreateTask(AppearancePatentParseRequest request) { + long startedAt = System.nanoTime(); if (request.getUserId() == null || request.getUserId() <= 0) { throw new BusinessException("user_id 不合法"); } @@ -131,6 +131,7 @@ public class AppearancePatentTaskService { List mergedHeaders = new ArrayList<>(); int totalRows = 0; int droppedRows = 0; + long parseStartedAt = System.nanoTime(); for (AppearancePatentSourceFileDto source : sourceFiles) { if (source.getFileKey() == null || source.getFileKey().isBlank()) { throw new BusinessException("fileKey 不能为空"); @@ -149,7 +150,9 @@ public class AppearancePatentTaskService { if (allRows.isEmpty()) { throw new BusinessException("未解析到有效 ASIN 数据"); } + long parsedAt = System.nanoTime(); List groups = buildParsedGroups(allRows); + long groupedAt = System.nanoTime(); FileTaskEntity task = new FileTaskEntity(); task.setTaskNo(MODULE_TYPE + "-" + IdUtil.getSnowflakeNextIdStr()); @@ -170,11 +173,14 @@ public class AppearancePatentTaskService { throw new BusinessException("序列化解析结果失败"); } fileTaskMapper.insert(task); + long taskInsertedAt = System.nanoTime(); String aggregateScopeKey = buildAggregateScopeKey(sourceFiles); String sourceScopeHash = DigestUtil.sha256Hex(aggregateScopeKey); String parsedPayload = buildParsedPayloadJson(request.getAiPrompt(), sourceFiles, mergedHeaders, groups, allRows); + long payloadBuiltAt = System.nanoTime(); String parsedPayloadPointer = storeParsedPayload(task.getId(), sourceScopeHash, parsedPayload); + long payloadStoredAt = System.nanoTime(); task.setResultJson(buildTaskResultJson(request.getAiPrompt(), sourceFiles, parsedPayloadPointer)); task.setUpdatedAt(LocalDateTime.now()); fileTaskMapper.updateById(task); @@ -203,6 +209,7 @@ public class AppearancePatentTaskService { scope.setCreatedAt(LocalDateTime.now()); scope.setUpdatedAt(LocalDateTime.now()); taskScopeStateMapper.insert(scope); + long persistedAt = System.nanoTime(); AppearancePatentParseVo vo = new AppearancePatentParseVo(); vo.setTaskId(task.getId()); @@ -215,6 +222,20 @@ public class AppearancePatentTaskService { vo.setAiPrompt(normalize(request.getAiPrompt())); vo.setItems(allRows); vo.setGroups(groups); + long finishedAt = System.nanoTime(); + log.info("[appearance-patent] parse timing taskId={} files={} rows={} groups={} totalMs={} parseMs={} groupMs={} taskInsertMs={} payloadJsonMs={} payloadStoreMs={} persistMs={} responseMs={}", + task.getId(), + sourceFiles.size(), + allRows.size(), + groups.size(), + elapsedMs(startedAt, finishedAt), + elapsedMs(parseStartedAt, parsedAt), + elapsedMs(parsedAt, groupedAt), + elapsedMs(groupedAt, taskInsertedAt), + elapsedMs(taskInsertedAt, payloadBuiltAt), + elapsedMs(payloadBuiltAt, payloadStoredAt), + elapsedMs(payloadStoredAt, persistedAt), + elapsedMs(persistedAt, finishedAt)); return vo; } @@ -1615,9 +1636,9 @@ public class AppearancePatentTaskService { payload.setAiPrompt(normalize(aiPrompt)); payload.setSourceFiles(sourceFiles == null ? List.of() : sourceFiles); payload.setHeaders(headers == null ? List.of() : headers); - payload.setItems(allRows == null ? List.of() : allRows); + payload.setItems(List.of()); payload.setGroups(groups == null ? List.of() : groups); - payload.setAllItems(allRows == null ? List.of() : allRows); + payload.setAllItems(List.of()); return writeJson(payload, "保存解析结果失败"); } @@ -1633,7 +1654,11 @@ public class AppearancePatentTaskService { } private String storeParsedPayload(Long taskId, String scopeHash, String parsedPayloadJson) { - return transientPayloadStorageService.storeParsedPayload(MODULE_TYPE, taskId, scopeHash, parsedPayloadJson, true); + return transientPayloadStorageService.storeParsedPayloadFast(MODULE_TYPE, taskId, scopeHash, parsedPayloadJson, true); + } + + private long elapsedMs(long start, long end) { + return (end - start) / 1_000_000L; } private AppearancePatentParsedPayloadDto readParsedPayload(FileTaskEntity task) { @@ -1642,15 +1667,41 @@ public class AppearancePatentTaskService { String pointer = root.path("parsedPayloadRef").asText(""); if (!pointer.isBlank()) { String json = transientPayloadStorageService.resolvePayload(pointer, "read appearance patent parsed payload failed"); - return objectMapper.readValue(json, AppearancePatentParsedPayloadDto.class); + return hydrateParsedPayloadRows(objectMapper.readValue(json, AppearancePatentParsedPayloadDto.class)); } if (root.path("allItems").isArray()) { - return objectMapper.treeToValue(root, AppearancePatentParsedPayloadDto.class); + return hydrateParsedPayloadRows(objectMapper.treeToValue(root, AppearancePatentParsedPayloadDto.class)); } } catch (Exception ex) { throw new BusinessException("读取解析载荷失败"); } - return new AppearancePatentParsedPayloadDto(); + return hydrateParsedPayloadRows(new AppearancePatentParsedPayloadDto()); + } + + private AppearancePatentParsedPayloadDto hydrateParsedPayloadRows(AppearancePatentParsedPayloadDto payload) { + if (payload == null) { + return new AppearancePatentParsedPayloadDto(); + } + List rows = payload.getAllItems(); + if (rows == null || rows.isEmpty()) { + rows = payload.getItems(); + } + if ((rows == null || rows.isEmpty()) && payload.getGroups() != null) { + rows = payload.getGroups().stream() + .filter(Objects::nonNull) + .flatMap(group -> group.getItems() == null ? java.util.stream.Stream.empty() : group.getItems().stream()) + .toList(); + } + if (rows == null) { + rows = List.of(); + } + if (payload.getAllItems() == null || payload.getAllItems().isEmpty()) { + payload.setAllItems(rows); + } + if (payload.getItems() == null || payload.getItems().isEmpty()) { + payload.setItems(rows); + } + return payload; } private Map readChunkRows(TaskChunkEntity chunk) { diff --git a/backend-java/src/main/java/com/nanri/aiimage/modules/brand/service/BrandTaskService.java b/backend-java/src/main/java/com/nanri/aiimage/modules/brand/service/BrandTaskService.java index 26e92f6..bb5a2d7 100644 --- a/backend-java/src/main/java/com/nanri/aiimage/modules/brand/service/BrandTaskService.java +++ b/backend-java/src/main/java/com/nanri/aiimage/modules/brand/service/BrandTaskService.java @@ -614,6 +614,9 @@ public class BrandTaskService { if (STATUS_FAILED.equalsIgnoreCase(freshStatus)) { throw new BusinessException("任务已结束,不能继续组装结果"); } + for (BrandSourceFileDto sourceFile : sourceFiles) { + brandTaskStorageService.refreshFileAggregate(taskId, sourceFile.getFileUrl()); + } Map aggregates = brandTaskStorageService.getAllFileAggregates(taskId); log.info("[brand-finalize] taskId={} aggregateFiles={} expectedFiles={} thread={}", taskId, @@ -621,12 +624,12 @@ public class BrandTaskService { totalCount, Thread.currentThread().getName()); if (aggregates.size() < totalCount) { - return; + throw new BusinessException("品牌检测结果分片尚未收齐,请稍后重试"); } for (BrandSourceFileDto sourceFile : sourceFiles) { BrandFileAggregateCacheDto aggregate = aggregates.get(sourceFile.getFileUrl()); if (aggregate == null || !Boolean.TRUE.equals(aggregate.getCompleted())) { - return; + throw new BusinessException("品牌检测结果分片尚未收齐,请稍后重试"); } } @@ -1208,6 +1211,9 @@ public class BrandTaskService { .eq(BrandCrawlTaskEntity::getStatus, STATUS_RUNNING) .lt(BrandCrawlTaskEntity::getUpdatedAt, threshold)); for (BrandCrawlTaskEntity task : runningTasks) { + if (tryRecoverCompletedRunningTask(task)) { + continue; + } Map progress = brandTaskProgressCacheService.getProgress(task.getId()); if (progress.isEmpty()) { failStaleRunningTask(task.getId(), "任务进度缓存已过期,任务已自动失败"); @@ -1231,6 +1237,34 @@ public class BrandTaskService { } } + private boolean tryRecoverCompletedRunningTask(BrandCrawlTaskEntity task) { + if (task == null || task.getId() == null) { + return false; + } + try { + List sourceFiles = parseSourceFiles(task.getFilePaths()); + if (sourceFiles.isEmpty()) { + return false; + } + List cachedFiles = brandTaskStorageService.getParsedPayload(task.getId()); + Map cachedByUrl = indexCachedFiles(cachedFiles); + for (BrandSourceFileDto sourceFile : sourceFiles) { + BrandTaskStorageService.ChunkStoreResult result = + brandTaskStorageService.refreshFileAggregate(task.getId(), sourceFile.getFileUrl()); + BrandFileAggregateCacheDto aggregate = result.aggregate(); + if (aggregate == null || !Boolean.TRUE.equals(aggregate.getCompleted())) { + return false; + } + } + log.info("[brand-stale-check] recover completed running taskId={} files={}", task.getId(), sourceFiles.size()); + finalizeTask(task.getId(), normalizeStrategy(task.getStrategy()), sourceFiles, cachedByUrl, sourceFiles.size()); + return true; + } catch (Exception ex) { + log.warn("[brand-stale-check] recover completed running taskId={} failed msg={}", task.getId(), ex.getMessage()); + return false; + } + } + private void failStaleRunningTask(Long taskId, String message) { brandCrawlTaskMapper.update(null, new LambdaUpdateWrapper() .eq(BrandCrawlTaskEntity::getId, taskId) diff --git a/backend-java/src/main/java/com/nanri/aiimage/modules/brand/service/BrandTaskStorageService.java b/backend-java/src/main/java/com/nanri/aiimage/modules/brand/service/BrandTaskStorageService.java index 873f9a4..2ff1fe5 100644 --- a/backend-java/src/main/java/com/nanri/aiimage/modules/brand/service/BrandTaskStorageService.java +++ b/backend-java/src/main/java/com/nanri/aiimage/modules/brand/service/BrandTaskStorageService.java @@ -278,20 +278,30 @@ public class BrandTaskStorageService { if (state == null) { throw new BusinessException("Brand task aggregate state missing: " + scopeKey); } - String storedAggregate = transientPayloadStorageService.storeScopePayload(MODULE_TYPE, taskId, scopeHash, aggregateJson, true); + String storedAggregate = transientPayloadStorageService.storeScopePayloadVersioned(MODULE_TYPE, taskId, scopeHash, aggregateJson, true); int chunkTotal = aggregate.getChunkTotal() == null ? 0 : aggregate.getChunkTotal(); int receivedCount = aggregate.getReceivedChunkCount() == null ? 0 : aggregate.getReceivedChunkCount(); - transientPayloadStorageService.deleteReplacedPayloadIfNeeded(state.getStateJson(), storedAggregate); - taskScopeStateMapper.update(null, new LambdaUpdateWrapper() + int completed = Boolean.TRUE.equals(aggregate.getCompleted()) ? 1 : 0; + int updated = taskScopeStateMapper.update(null, new LambdaUpdateWrapper() .eq(TaskScopeStateEntity::getId, state.getId()) + .and(wrapper -> wrapper + .lt(TaskScopeStateEntity::getReceivedChunkCount, receivedCount) + .or() + .eq(TaskScopeStateEntity::getReceivedChunkCount, receivedCount) + .le(TaskScopeStateEntity::getCompleted, completed)) .set(TaskScopeStateEntity::getScopeKey, scopeKey) .set(TaskScopeStateEntity::getStateJson, storedAggregate) .set(TaskScopeStateEntity::getChunkTotal, chunkTotal) .set(TaskScopeStateEntity::getReceivedChunkCount, receivedCount) - .set(TaskScopeStateEntity::getCompleted, Boolean.TRUE.equals(aggregate.getCompleted()) ? 1 : 0) + .set(TaskScopeStateEntity::getCompleted, completed) .set(TaskScopeStateEntity::getLastChunkAt, now) .set(TaskScopeStateEntity::getLastError, null) .set(TaskScopeStateEntity::getUpdatedAt, now)); + if (updated > 0) { + transientPayloadStorageService.deleteReplacedPayloadIfNeeded(state.getStateJson(), storedAggregate); + } else { + transientPayloadStorageService.deletePayloadIfPresent(storedAggregate); + } } private TaskScopeStateEntity getScopeState(Long taskId, String scopeHash) { diff --git a/backend-java/src/main/java/com/nanri/aiimage/modules/file/service/object/RustfsObjectStorageService.java b/backend-java/src/main/java/com/nanri/aiimage/modules/file/service/object/RustfsObjectStorageService.java index 8940235..faf75c9 100644 --- a/backend-java/src/main/java/com/nanri/aiimage/modules/file/service/object/RustfsObjectStorageService.java +++ b/backend-java/src/main/java/com/nanri/aiimage/modules/file/service/object/RustfsObjectStorageService.java @@ -32,6 +32,10 @@ public class RustfsObjectStorageService { } public String uploadText(String objectKey, String content) { + return uploadText(objectKey, content, true); + } + + public String uploadText(String objectKey, String content, boolean verifyAfterUpload) { if (!isConfigured()) { throw new IllegalStateException("transient storage is not configured"); } @@ -46,7 +50,9 @@ public class RustfsObjectStorageService { .stream(stream, bytes.length, -1) .contentType("application/json") .build()); - verifyObjectVisible(objectKey); + if (verifyAfterUpload) { + verifyObjectVisible(objectKey); + } return objectKey; } catch (Exception ex) { last = ex; diff --git a/backend-java/src/main/java/com/nanri/aiimage/modules/patroldelete/service/PatrolDeleteTaskCacheService.java b/backend-java/src/main/java/com/nanri/aiimage/modules/patroldelete/service/PatrolDeleteTaskCacheService.java index 083e02d..87dbdce 100644 --- a/backend-java/src/main/java/com/nanri/aiimage/modules/patroldelete/service/PatrolDeleteTaskCacheService.java +++ b/backend-java/src/main/java/com/nanri/aiimage/modules/patroldelete/service/PatrolDeleteTaskCacheService.java @@ -127,6 +127,11 @@ public class PatrolDeleteTaskCacheService { } public void deleteTaskCache(Long taskId) { + evictTaskCacheOnly(taskId); + taskScopePayloadStorageService.deleteTaskScopePayloads(taskId, MODULE_TYPE); + } + + public void evictTaskCacheOnly(Long taskId) { if (taskId == null || taskId <= 0) { return; } @@ -137,7 +142,6 @@ public class PatrolDeleteTaskCacheService { } catch (Exception ex) { log.warn("[patrol-delete-cache] delete cache degraded taskId={} msg={}", taskId, ex.getMessage()); } - taskScopePayloadStorageService.deleteTaskScopePayloads(taskId, MODULE_TYPE); } public void saveTaskCache(FileTaskEntity task) { diff --git a/backend-java/src/main/java/com/nanri/aiimage/modules/patroldelete/service/PatrolDeleteTaskService.java b/backend-java/src/main/java/com/nanri/aiimage/modules/patroldelete/service/PatrolDeleteTaskService.java index 5215cc8..91e1b9b 100644 --- a/backend-java/src/main/java/com/nanri/aiimage/modules/patroldelete/service/PatrolDeleteTaskService.java +++ b/backend-java/src/main/java/com/nanri/aiimage/modules/patroldelete/service/PatrolDeleteTaskService.java @@ -28,6 +28,7 @@ import com.nanri.aiimage.modules.task.model.entity.TaskFileJobEntity; import com.nanri.aiimage.modules.task.service.TaskFileJobService; import com.nanri.aiimage.modules.task.service.TaskProgressSnapshotService; import com.nanri.aiimage.modules.task.service.TaskResultItemService; +import com.nanri.aiimage.modules.task.service.TaskScopePayloadStorageService; import com.nanri.aiimage.modules.ziniao.service.ZiniaoShopSwitchService; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; @@ -66,6 +67,7 @@ public class PatrolDeleteTaskService { private final TaskFileJobService taskFileJobService; private final TaskResultItemService taskResultItemService; private final TaskProgressSnapshotService taskProgressSnapshotService; + private final TaskScopePayloadStorageService taskScopePayloadStorageService; private FileTaskEntity loadTaskForExecution(Long taskId) { Map cachedTasks = taskCacheService.getTaskCacheBatch(List.of(taskId)); @@ -135,32 +137,58 @@ public class PatrolDeleteTaskService { } public PatrolDeleteHistoryVo listHistory(Long userId) { + long startedAt = System.nanoTime(); validateUserId(userId); PatrolDeleteHistoryVo vo = new PatrolDeleteHistoryVo(); List entities = fileResultMapper.selectList(new LambdaQueryWrapper() + .select(FileResultEntity::getId, + FileResultEntity::getTaskId, + FileResultEntity::getModuleType, + FileResultEntity::getSourceFilename, + FileResultEntity::getSourceFileUrl, + FileResultEntity::getResultFilename, + FileResultEntity::getResultFileUrl, + FileResultEntity::getSuccess, + FileResultEntity::getErrorMessage, + FileResultEntity::getUserId, + FileResultEntity::getCreatedAt) .eq(FileResultEntity::getModuleType, MODULE_TYPE) .eq(FileResultEntity::getUserId, userId) .orderByDesc(FileResultEntity::getCreatedAt) .last("limit 100")); + long resultRowsLoadedAt = System.nanoTime(); if (entities.isEmpty()) { vo.setItems(List.of()); + log.info("[patrol-delete] history timing userId={} rows=0 totalMs={} resultQueryMs={} taskQueryMs=0 jobQueryMs=0 buildMs=0", + userId, elapsedMs(startedAt, resultRowsLoadedAt), elapsedMs(startedAt, resultRowsLoadedAt)); return vo; } - Map taskMap = loadTaskMap(entities); - Map> snapshotMap = buildSnapshotMap(taskMap); + Map taskMap = loadHistoryTaskMap(entities); + long tasksLoadedAt = System.nanoTime(); Map jobMap = taskFileJobService.findAssembleJobsByResultIds(MODULE_TYPE, entities.stream() .map(FileResultEntity::getId) .filter(id -> id != null && id > 0) .distinct() .toList()); + long jobsLoadedAt = System.nanoTime(); List items = new ArrayList<>(); for (FileResultEntity entity : entities) { FileTaskEntity task = taskMap.get(entity.getTaskId()); - PatrolDeleteResultItemVo snapshot = snapshotMap.getOrDefault(entity.getTaskId(), Map.of()).get(entity.getId()); - items.add(toHistoryItem(entity, task, snapshot, jobMap.get(entity.getId()))); + items.add(toHistoryItem(entity, task, null, jobMap.get(entity.getId()))); } vo.setItems(items); + long finishedAt = System.nanoTime(); + log.info("[patrol-delete] history timing userId={} rows={} tasks={} jobs={} totalMs={} resultQueryMs={} taskQueryMs={} jobQueryMs={} buildMs={}", + userId, + entities.size(), + taskMap.size(), + jobMap.size(), + elapsedMs(startedAt, finishedAt), + elapsedMs(startedAt, resultRowsLoadedAt), + elapsedMs(resultRowsLoadedAt, tasksLoadedAt), + elapsedMs(tasksLoadedAt, jobsLoadedAt), + elapsedMs(jobsLoadedAt, finishedAt)); return vo; } @@ -417,17 +445,32 @@ public class PatrolDeleteTaskService { @Transactional public void deleteTask(Long taskId, Long userId) { + long startedAt = System.nanoTime(); validateUserId(userId); FileTaskEntity task = loadTaskForExecution(taskId); if (task == null || !MODULE_TYPE.equals(task.getModuleType()) || !userId.equals(task.getUserId())) { throw new BusinessException("任务不存在"); } + long taskLoadedAt = System.nanoTime(); fileResultMapper.delete(new LambdaQueryWrapper() .eq(FileResultEntity::getTaskId, taskId) .eq(FileResultEntity::getModuleType, MODULE_TYPE)); - cleanupTaskAuxiliaryData(taskId); + long resultsDeletedAt = System.nanoTime(); + cleanupTaskAuxiliaryDataFast(taskId); + long auxiliaryDeletedAt = System.nanoTime(); fileTaskMapper.deleteById(taskId); - taskCacheService.deleteTaskCache(taskId); + long taskDeletedAt = System.nanoTime(); + taskCacheService.evictTaskCacheOnly(taskId); + long finishedAt = System.nanoTime(); + log.info("[patrol-delete] delete task timing taskId={} userId={} totalMs={} loadMs={} resultDeleteMs={} auxiliaryDeleteMs={} taskDeleteMs={} cacheMs={}", + taskId, + userId, + elapsedMs(startedAt, finishedAt), + elapsedMs(startedAt, taskLoadedAt), + elapsedMs(taskLoadedAt, resultsDeletedAt), + elapsedMs(resultsDeletedAt, auxiliaryDeletedAt), + elapsedMs(auxiliaryDeletedAt, taskDeletedAt), + elapsedMs(taskDeletedAt, finishedAt)); } @Transactional @@ -439,7 +482,7 @@ public class PatrolDeleteTaskService { } Long taskId = entity.getTaskId(); fileResultMapper.deleteById(resultId); - cleanupResultAuxiliaryData(taskId, resultId); + cleanupResultAuxiliaryDataFast(taskId, resultId); reconcileTaskAfterResultRemoval(taskId); } @@ -453,13 +496,12 @@ public class PatrolDeleteTaskService { } List rows = listTaskRows(taskId); if (rows.isEmpty()) { - cleanupTaskAuxiliaryData(taskId); + cleanupTaskAuxiliaryDataFast(taskId); fileTaskMapper.deleteById(taskId); - taskCacheService.deleteTaskCache(taskId); + taskCacheService.evictTaskCacheOnly(taskId); return; } updateTaskStatusFromRows(task, rows); - persistSnapshotJson(task, buildSnapshotFromDb(task, rows)); fileTaskMapper.updateById(task); taskCacheService.saveTaskCache(task); } @@ -470,11 +512,23 @@ public class PatrolDeleteTaskService { taskFileJobService.deleteTaskJobs(taskId, MODULE_TYPE); } + private void cleanupTaskAuxiliaryDataFast(Long taskId) { + taskResultItemService.deleteTaskItemsRowsOnly(taskId, MODULE_TYPE); + taskScopePayloadStorageService.deleteTaskScopePayloadRowsOnly(taskId, MODULE_TYPE); + taskProgressSnapshotService.delete(taskId, MODULE_TYPE); + taskFileJobService.deleteTaskJobs(taskId, MODULE_TYPE); + } + private void cleanupResultAuxiliaryData(Long taskId, Long resultId) { taskResultItemService.deleteResultItem(taskId, MODULE_TYPE, resultId); taskFileJobService.deleteResultJobs(taskId, MODULE_TYPE, resultId); } + private void cleanupResultAuxiliaryDataFast(Long taskId, Long resultId) { + taskResultItemService.deleteResultItemRowsOnly(taskId, MODULE_TYPE, resultId); + taskFileJobService.deleteResultJobs(taskId, MODULE_TYPE, resultId); + } + private long countTasks(Long userId, List statuses) { Long count = fileTaskMapper.selectCount(new LambdaQueryWrapper() .eq(FileTaskEntity::getModuleType, MODULE_TYPE) @@ -524,6 +578,35 @@ public class PatrolDeleteTaskService { return taskMap; } + private Map loadHistoryTaskMap(List entities) { + List taskIds = entities.stream() + .map(FileResultEntity::getTaskId) + .filter(id -> id != null && id > 0) + .distinct() + .toList(); + Map taskMap = new LinkedHashMap<>(); + if (taskIds.isEmpty()) { + return taskMap; + } + int batchSize = Math.max(1, taskPressureProperties.getDbSelectBatchSize()); + for (int start = 0; start < taskIds.size(); start += batchSize) { + int end = Math.min(start + batchSize, taskIds.size()); + List tasks = fileTaskMapper.selectList(new LambdaQueryWrapper() + .select(FileTaskEntity::getId, + FileTaskEntity::getModuleType, + FileTaskEntity::getStatus, + FileTaskEntity::getFinishedAt) + .eq(FileTaskEntity::getModuleType, MODULE_TYPE) + .in(FileTaskEntity::getId, taskIds.subList(start, end))); + for (FileTaskEntity task : tasks) { + if (task != null && task.getId() != null) { + taskMap.put(task.getId(), task); + } + } + } + return taskMap; + } + private Map> buildSnapshotMap(Map taskMap) { Map> out = new LinkedHashMap<>(); for (Map.Entry entry : taskMap.entrySet()) { @@ -925,6 +1008,10 @@ public class PatrolDeleteTaskService { return Integer.valueOf(RESULT_SUCCESS).equals(dbValue); } + private long elapsedMs(long start, long end) { + return (end - start) / 1_000_000L; + } + private List parseTaskSnapshots(String json) { if (blank(json)) { return new ArrayList<>(); diff --git a/backend-java/src/main/java/com/nanri/aiimage/modules/task/service/TaskResultItemService.java b/backend-java/src/main/java/com/nanri/aiimage/modules/task/service/TaskResultItemService.java index bfe9eb8..73d5f2e 100644 --- a/backend-java/src/main/java/com/nanri/aiimage/modules/task/service/TaskResultItemService.java +++ b/backend-java/src/main/java/com/nanri/aiimage/modules/task/service/TaskResultItemService.java @@ -115,6 +115,17 @@ public class TaskResultItemService { .eq(TaskResultItemEntity::getResultId, resultId)); } + @Transactional + public void deleteResultItemRowsOnly(Long taskId, String moduleType, Long resultId) { + if (taskId == null || taskId <= 0 || isBlank(moduleType) || resultId == null || resultId <= 0) { + return; + } + taskResultItemMapper.delete(new LambdaQueryWrapper() + .eq(TaskResultItemEntity::getTaskId, taskId) + .eq(TaskResultItemEntity::getModuleType, moduleType) + .eq(TaskResultItemEntity::getResultId, resultId)); + } + @Transactional public void deleteTaskItems(Long taskId, String moduleType) { if (taskId == null || taskId <= 0 || isBlank(moduleType)) { @@ -134,6 +145,16 @@ public class TaskResultItemService { .eq(TaskResultItemEntity::getModuleType, moduleType)); } + @Transactional + public void deleteTaskItemsRowsOnly(Long taskId, String moduleType) { + if (taskId == null || taskId <= 0 || isBlank(moduleType)) { + return; + } + taskResultItemMapper.delete(new LambdaQueryWrapper() + .eq(TaskResultItemEntity::getTaskId, taskId) + .eq(TaskResultItemEntity::getModuleType, moduleType)); + } + private void upsert(Long taskId, String moduleType, Long resultId, diff --git a/backend-java/src/main/java/com/nanri/aiimage/modules/task/service/TaskScopePayloadStorageService.java b/backend-java/src/main/java/com/nanri/aiimage/modules/task/service/TaskScopePayloadStorageService.java index 5c4aeb5..b2d1be1 100644 --- a/backend-java/src/main/java/com/nanri/aiimage/modules/task/service/TaskScopePayloadStorageService.java +++ b/backend-java/src/main/java/com/nanri/aiimage/modules/task/service/TaskScopePayloadStorageService.java @@ -188,6 +188,16 @@ public class TaskScopePayloadStorageService { } } + @Transactional + public void deleteTaskScopePayloadRowsOnly(Long taskId, String moduleType) { + if (taskId == null || taskId <= 0 || isBlank(moduleType)) { + return; + } + taskScopeStateMapper.delete(new LambdaQueryWrapper() + .eq(TaskScopeStateEntity::getTaskId, taskId) + .eq(TaskScopeStateEntity::getModuleType, moduleType)); + } + @Transactional public void saveParsedPayloadMap(Long taskId, String moduleType, Map payloadByScope) { if (taskId == null || taskId <= 0 || isBlank(moduleType) || payloadByScope == null || payloadByScope.isEmpty()) { diff --git a/backend-java/src/main/java/com/nanri/aiimage/modules/task/service/TransientPayloadStorageService.java b/backend-java/src/main/java/com/nanri/aiimage/modules/task/service/TransientPayloadStorageService.java index 4282fdf..d840320 100644 --- a/backend-java/src/main/java/com/nanri/aiimage/modules/task/service/TransientPayloadStorageService.java +++ b/backend-java/src/main/java/com/nanri/aiimage/modules/task/service/TransientPayloadStorageService.java @@ -30,10 +30,18 @@ public class TransientPayloadStorageService { return store("task-scope", moduleType, taskId, scopeHash, "latest", content, encodeAsJsonString); } + public String storeScopePayloadVersioned(String moduleType, Long taskId, String scopeHash, String content, boolean encodeAsJsonString) { + return store("task-scope", moduleType, taskId, scopeHash, UUID.randomUUID().toString(), content, encodeAsJsonString); + } + public String storeParsedPayload(String moduleType, Long taskId, String scopeHash, String content, boolean encodeAsJsonString) { return store("task-parsed", moduleType, taskId, scopeHash, "latest", content, encodeAsJsonString); } + public String storeParsedPayloadFast(String moduleType, Long taskId, String scopeHash, String content, boolean encodeAsJsonString) { + return store("task-parsed", moduleType, taskId, scopeHash, "latest", content, encodeAsJsonString, false); + } + public String storeResultPayload(String moduleType, Long taskId, String scopeHash, String submissionId, String content) { return store("task-result-payload", moduleType, taskId, scopeHash, submissionId, content, true); } @@ -114,11 +122,22 @@ public class TransientPayloadStorageService { String entryKey, String content, boolean encodeAsJsonString) { + return store(category, moduleType, taskId, scopeHash, entryKey, content, encodeAsJsonString, true); + } + + private String store(String category, + String moduleType, + Long taskId, + String scopeHash, + String entryKey, + String content, + boolean encodeAsJsonString, + boolean verifyAfterUpload) { if (!isWriteEnabled()) { return content; } String objectKey = buildObjectKey(category, moduleType, taskId, scopeHash, entryKey); - String pointer = RUSTFS_POINTER_PREFIX + rustfsObjectStorageService.uploadText(objectKey, content); + String pointer = RUSTFS_POINTER_PREFIX + rustfsObjectStorageService.uploadText(objectKey, content, verifyAfterUpload); if (!encodeAsJsonString) { return pointer; } diff --git a/backend-java/src/main/resources/db/V42__patrol_delete_history_hot_path_indexes.sql b/backend-java/src/main/resources/db/V42__patrol_delete_history_hot_path_indexes.sql new file mode 100644 index 0000000..7c606b2 --- /dev/null +++ b/backend-java/src/main/resources/db/V42__patrol_delete_history_hot_path_indexes.sql @@ -0,0 +1,35 @@ +SET @schema_name = DATABASE(); + +SET @idx_file_result_patrol_history_exists := ( + SELECT COUNT(1) + FROM information_schema.STATISTICS + WHERE TABLE_SCHEMA = @schema_name + AND TABLE_NAME = 'biz_file_result' + AND INDEX_NAME = 'idx_file_result_patrol_history' +); + +SET @sql_add_file_result_patrol_history_idx := IF( + @idx_file_result_patrol_history_exists = 0, + 'ALTER TABLE biz_file_result ADD INDEX idx_file_result_patrol_history (module_type, user_id, created_at, id)', + 'SELECT 1' +); +PREPARE stmt_add_file_result_patrol_history_idx FROM @sql_add_file_result_patrol_history_idx; +EXECUTE stmt_add_file_result_patrol_history_idx; +DEALLOCATE PREPARE stmt_add_file_result_patrol_history_idx; + +SET @idx_file_task_patrol_history_exists := ( + SELECT COUNT(1) + FROM information_schema.STATISTICS + WHERE TABLE_SCHEMA = @schema_name + AND TABLE_NAME = 'biz_file_task' + AND INDEX_NAME = 'idx_file_task_patrol_history' +); + +SET @sql_add_file_task_patrol_history_idx := IF( + @idx_file_task_patrol_history_exists = 0, + 'ALTER TABLE biz_file_task ADD INDEX idx_file_task_patrol_history (module_type, id, status, finished_at)', + 'SELECT 1' +); +PREPARE stmt_add_file_task_patrol_history_idx FROM @sql_add_file_task_patrol_history_idx; +EXECUTE stmt_add_file_task_patrol_history_idx; +DEALLOCATE PREPARE stmt_add_file_task_patrol_history_idx; diff --git a/frontend-vue/new_web_source.zip b/frontend-vue/new_web_source.zip deleted file mode 100644 index b2f955a..0000000 Binary files a/frontend-vue/new_web_source.zip and /dev/null differ diff --git a/frontend-vue/src/pages/brand/components/BrandAppearancePatentTab.vue b/frontend-vue/src/pages/brand/components/BrandAppearancePatentTab.vue index 5010055..e76a3c9 100644 --- a/frontend-vue/src/pages/brand/components/BrandAppearancePatentTab.vue +++ b/frontend-vue/src/pages/brand/components/BrandAppearancePatentTab.vue @@ -331,8 +331,6 @@ async function parseFiles() { }] : []) queuePayloadText.value = '' - await loadDashboard() - await loadHistory({ force: true }) ElMessage.success(`解析完成,共 ${res.groupCount || 0} 组 / ${res.acceptedRows} 条`) } catch (e) { ElMessage.error(e instanceof Error ? e.message : '解析失败') @@ -465,7 +463,7 @@ function scheduleNextPoll(immediate = false) { if (disposed) return if (!pollingTaskIds.value.length && !pendingFileTaskIds.value.length) return await refreshTaskProgress() - if (!disposed && (pollingTaskIds.value.length || pendingFileTaskIds.value.length)) { + if (!disposed && pollTimer.value == null && (pollingTaskIds.value.length || pendingFileTaskIds.value.length)) { pollTimer.value = timers.setTimeout('task-poll', run, getTaskPollIntervalMs()) } }