From b400b494adf18b2e5e0bad11e7d1277b0ab86361 Mon Sep 17 00:00:00 2001 From: super <2903208875@qq.com> Date: Sat, 11 Apr 2026 00:57:34 +0800 Subject: [PATCH] =?UTF-8?q?=E4=BF=AE=E5=A4=8DBUG=EF=BC=8C=E5=AE=8C?= =?UTF-8?q?=E5=96=84=E5=8C=B9=E9=85=8D=E5=BA=97=E9=93=BA?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/.env | 1 + .../__pycache__/approve.cpython-312.pyc | Bin 40725 -> 45215 bytes .../__pycache__/del_brand.cpython-312.pyc | Bin 27142 -> 28509 bytes app/amazon/__pycache__/main.cpython-312.pyc | Bin 27901 -> 28953 bytes .../__pycache__/match_action.cpython-312.pyc | Bin 0 -> 32650 bytes app/amazon/__pycache__/tool.cpython-312.pyc | Bin 0 -> 3404 bytes app/amazon/match_action.py | 68 +- .../config/DeleteBrandProgressProperties.java | 6 + .../controller/DeleteBrandRunController.java | 16 +- .../service/DeleteBrandStaleTaskService.java | 100 +- .../model/vo/ProductRiskTaskItemVo.java | 38 +- .../controller/ShopMatchController.java | 85 +- .../model/dto/ShopMatchCreateTaskRequest.java | 46 + .../dto/ShopMatchStageCompleteRequest.java | 17 + .../model/vo/ShopMatchTaskStageVo.java | 22 + .../service/ShopMatchTaskService.java | 426 +++- .../service/ModuleHistoryCleanupService.java | 45 +- .../src/main/resources/application.yml | 2 + .../brand/components/BrandShopMatchTab.vue | 1810 +++-------------- frontend-vue/src/shared/api/java-modules.ts | 31 +- frontend-vue/src/shop-match-main.ts | 7 +- 21 files changed, 1057 insertions(+), 1663 deletions(-) create mode 100644 app/amazon/__pycache__/match_action.cpython-312.pyc create mode 100644 app/amazon/__pycache__/tool.cpython-312.pyc create mode 100644 backend-java/src/main/java/com/nanri/aiimage/modules/shopmatch/model/dto/ShopMatchCreateTaskRequest.java create mode 100644 backend-java/src/main/java/com/nanri/aiimage/modules/shopmatch/model/dto/ShopMatchStageCompleteRequest.java create mode 100644 backend-java/src/main/java/com/nanri/aiimage/modules/shopmatch/model/vo/ShopMatchTaskStageVo.java diff --git a/app/.env b/app/.env index 123ec51..4748534 100644 --- a/app/.env +++ b/app/.env @@ -12,6 +12,7 @@ zn_username=%E8%87%AA%E5%8A%A8%E5%8C%96_Robot client_name=ShuFuAI +# java_api_base=http://47.111.163.154:18080 java_api_base=http://127.0.0.1:18080 # java_api_base=http://8.136.19.173:18081 # java_api_base=http://8.136.19.173:18080 diff --git a/app/amazon/__pycache__/approve.cpython-312.pyc b/app/amazon/__pycache__/approve.cpython-312.pyc index cb0c49ba7abe241a3cfb549f48d71a321f57d1e0..99830ef623758dfca88b4ee8284e391f29e08dd9 100644 GIT binary patch delta 14254 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 delta 10360 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 Q%o?W1JUm|l`JOxf1I>7-<^TWy delta 2431 zcmZ`(eM}Tr5Z||Zd$;!u+y@-rL{SeAL9r$%Du{AKtQc&A#1`o7swd|`b`LCeDaKf1 z6HsJKYBgHzhac56HP>q5AB`HVUumO;sJ)`GwkF!7hAL@N)7ZY*E8Jn)m+bFNW_~ko zX5Q?(dyi}zBDQN*tBIj&jqO&TY2``VFOy80Scdt4F)|{78x8JA%H%Qvm)1Fj!Ef!t3z)0 zONtWZ>)Sh|knE!jxfW?UIUVIXgk=<>oPUF;=%@r23+J$Q0xt@I&CG=pMgEduA?3D^ zc0)+p;}{aMh6T$`)BC1@(tfLwLLIg&sAQRQY-LiFi#zWm6d_ogM^3?x;zc^bs0^%_ z?}Xf`LP`%S651h-I%z9xgrAG^;83*_zFBLusHANg!ziRLP37QVwF^dOSdz!dgb5KH zMys!OW+;~Mc%MaGLPK=w;5ru@V_{pF|qcwKhLqht9KH{2vl_!00zdVa%M_!b7vDqJ@ zb1Fg_dg0LI(j_OMY6lEfOePEAbj3U}7#Xd&&XPg6xUeX%pISGiHw8k0*7j;S&?U*m zIFrqRj%NQl$=|j-l2X}7Tv}$kF;brv4xt}YN(2xV z=R>r_Ve2+0#Iit%FV$*IxC1SsUbJCE=v(Zu+vCP=PUV5n_kd13OBiU+$et`F#q9}! zljGW0l6o;k3t7CxV|T=jG^GBekgxntNT*8nG5guLujoRJ=-$uu7-EN%i>aysNfY`Z zsDffo$Sv*VKr{(qED0N75~vNgeSB<1Fd2v6!@}87WjN#k_`tMr@$U6lVv$t}Z&%&JN%!u|v#JQD8bh}`w%%R)G zqAEhSOU20)FEdEHY%%&4aF<=VzH;tzIqzE#`KqCe(BJs+%6@VovhlUO9Em{6s=Ab8 ztUM2EFv_g~MQWC08T_j@*J>s5flxjZQt+D#9QYp$Ra;)Uju(QR_J|dCPmNz)VZE9O0--1T*sr6ebucaSd_ZL8eRPgG= z81Ye%?@*3>1>qV3_A+V^rJxk1Q*j6)t?xdnd( z-kGT)i;=fuxjP!>HPY_<}_7Lh2 z9ulYv=8+)?1()c*N9}iTCOCuKhhKxmW^JPLP`d>tZP?zcU6SUj9qB;JHUvAuZUlVQ zqXvI_XDBRp%lA=r7FBv>W3$rfm*aUoKwAw0wkXOeZJn`Pe?#5GgrmZ|4eg?#GbqiM jpEFPzs>xi&6VLAq;%JMhhY;65(I|ua=(`;34qf>N*ndrU diff --git a/app/amazon/__pycache__/main.cpython-312.pyc b/app/amazon/__pycache__/main.cpython-312.pyc index 73334f83e24e31be2e9b01a11e97bb3418db7c06..3d9cbe0e3b555820c3cf6e7c3dc54bf4ef551a7f 100644 GIT binary patch delta 5695 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 Of5vjyH!-gcxBVAb>pvL) diff --git a/app/amazon/__pycache__/match_action.cpython-312.pyc b/app/amazon/__pycache__/match_action.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..c770364f28c0f159252f50cecca50ba9cc2b0bf6 GIT binary patch literal 32650 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 F{{ne(&949e literal 0 HcmV?d00001 diff --git a/app/amazon/__pycache__/tool.cpython-312.pyc b/app/amazon/__pycache__/tool.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..f2537c6fb61749490b0fc1d17ffd30a75f0d9fbf GIT binary patch literal 3404 zcmbUkZ)_9S{hj~BF>!zpR3M(VM^YNQjU5vY)7UF0iPDy-qiTz^fucp1doRv~^WEv) zIkvO7VyN9hS9Kkllu*i=PE<`t+riqk8&uQ8KJE(-XzxVvt*48ZuM)b2~c_Dz%Kln#&c&oteBmi^Bj|d`yf=(dfB`)eB zZt5EM5YG$V6Yhf0g{bf#VqO7gPN3uH<@OxPyN{!s5N=xIf{t3g3^fcfO|3u`7&Qwz zm0VRdeDcB14Zqtn0#|ABHG8l#U&E8@IcoU4AI+f2n#t8wJ`sVqE>$@i;@!lvDc(z( zYWSjy_((JHlP#p>bx+Yf)(bg0HYL*>UVHPS(v{O|r(Roq_ zuYCT~-;`cjT={6Wbn(g;pPbo(Vc4$$mNt?$#FwgRPvQzc(69XXxA$Z1qDbNja`no! z(yxAoLq=ATpm=(7B2#5G-AftMSg)*(Yhjz;61t~dBi)wJ2~(#fY=KFg_H`u=4)w(c z02odT^u>n&c>F+OD8YgLSbw~~KkTu5RpFMeD&6wh;w=xCpM4Z+r*0$nA~c>=B~#Ya zP#FK{9t_9!_TtsIudSWCSo-*PrT6AbuP?5?_Cm#92kn%|FwM(`2`(MgOnDqXhKVL+ zGgLLBX=)BDl!JqXBP7&a&p|}xq_QM(8@j_Ohggskx<_myJGrg*5JefyXj3%QKw#0K z!m$8umuAnGu3fFTT6+JFrE`l8D(GDQOPil(Y$H%Ylh5&m&D_8o{|(cH9d*@VtPJ>6*_Jv@qySrSY@JWjB6~Dtbhub z`GZgdaN!j0#*aAM<1y_X6Qz@Bre#%<#MyUX!=|vLDH==S+=Oh>6xKyTAmK^8zfaFs zwT@|wP!?e#k+Vh;f16vfu?8(4F(yQ!<&rptxe!|tOOJ`6SdaZg<6z=+L{wuCnJ0zg^6tFidcX^Dill> z6*;X&z{?CcAwlU#y7XVipx7=oaHp}Z`zSK;VsU}L3PluvHwE*8M5H_`%xT zdn+%WSv&i_gNpkjDVjkC&Hu-t8f#QmRmz?`_PwXLKYy8P;7py=u`8r3GkBTmnp{cT z`g(bkSERj=qjGuzw#b;KKmlY#mX_5>W!G$!Vq@$v0gxp1-N6I zux#L%Grc`H9*Z@IDA(&eYl=P4jve&H@ckzhur$i+SU4C@Zu>8Y$iR%2Au)I2hEn&G0lZAbZ-%ptb zURJ;m$LVP8R_nLh{Afnht>&@xsQqR6CJlfHwFGMLt0H$1#@(LBlOsCQCaGkONKDKr z(EW8K3wvnUt3Z0wqG8c zJAC$V*_HIiKMQugaroTf#rS3K&0ykI@X%85&>xIH58n)a_g3)PrQowSp8Nid<3GF^ z9G!it+>E}lZ#nqj@}AD+-Fv_Ed-rUeJyH&!-FxSXzbGz@T<*WQ>#^A<%T0k`XL&d3 zdSs!ajGB5vKoUN%@O&9PkZ3LM3+>uI`^24N0&3m)^QX@|JwLD%_~xy^!%KmO7Y}|K zh?flk?d&?0`|6G~Rb2fAj44GB79z@p+?+_Zb8lM hV6o7?ymiOvCpb&{T?76uq0^r2=v7~b`<&3>{tu7yr=|b^ literal 0 HcmV?d00001 diff --git a/app/amazon/match_action.py b/app/amazon/match_action.py index 260ff27..e0ad574 100644 --- a/app/amazon/match_action.py +++ b/app/amazon/match_action.py @@ -226,6 +226,9 @@ class MatchTak: items = data.get("items", []) country_codes = data.get("country_codes", []) risk_listing_filter = data.get("risk_listing_filter", "All") + user_id = data.get("user_id") + stage_index = data.get("stage_index") + final_stage = bool(data.get("final_stage", True)) if not task_id: self.log("任务ID为空,跳过", "WARNING") @@ -269,7 +272,7 @@ class MatchTak: show_notification(f"开始处理店铺: {shop_name}", "info") try: - self.process_shop(shop_item, country_codes, task_id,risk_listing_filter) + self.process_shop(shop_item, country_codes, task_id, risk_listing_filter, user_id, stage_index, final_stage) # 更新已处理店铺数 if task_id in runing_task: runing_task[task_id]["processed_shops"] += 1 @@ -296,7 +299,7 @@ class MatchTak: runing_task[task_id]["status"] = "failed" runing_task[task_id]["error"] = str(e) - def process_shop(self, shop_item: dict, country_codes: list, task_id: int, risk_listing_filter: str): + def process_shop(self, shop_item: dict, country_codes: list, task_id: int, risk_listing_filter: str, user_id=None, stage_index=None, final_stage: bool = True): """处理单个店铺 Args: @@ -406,9 +409,11 @@ class MatchTak: import traceback self.log(f"处理国家 {country_code} 失败: {str(e)}", "ERROR") self.log(traceback.format_exc(), "ERROR") - # 最后回传,标记完成 try: - self.post_result(task_id, shop_name, country_code, "", "", is_done=True) + if final_stage: + self.post_result(task_id, shop_name, country_code, "", "", is_done=True) + else: + self.post_stage_finished(task_id, user_id, stage_index) except Exception as e: self.log(f"回传结果失败: {str(e)}", "ERROR") finally: @@ -560,6 +565,44 @@ class MatchTak: self.log(f"国家 {country_name} 处理完成") + def post_stage_finished(self, task_id: int, user_id, stage_index): + import requests + from config import DELETE_BRAND_API_BASE + + if user_id in (None, "", 0): + raise ValueError("user_id is required for stage completion callback") + if stage_index is None: + raise ValueError("stage_index is required for stage completion callback") + + url = f"{DELETE_BRAND_API_BASE}/api/shop-match/tasks/{task_id}/stage-finished" + payload = {"stage_index": stage_index} + params = {"user_id": user_id} + + max_retries = 3 + for retry in range(max_retries): + try: + self.log(f"Attempting stage completion callback ({retry + 1}/{max_retries})") + response = requests.post( + url, + params=params, + json=payload, + headers={"Content-Type": "application/json"}, + timeout=30, + verify=False, + ) + self.log(f"Stage completion callback response: {response.text}") + data = response.json() if response.text else {} + if response.status_code == 200 and isinstance(data, dict) and data.get("success"): + self.log(f"Stage completion callback succeeded: task={task_id}, stage={stage_index}") + return + self.log(f"Stage completion callback failed, status={response.status_code}", "WARNING") + except Exception as e: + self.log(f"Stage completion callback exception: {str(e)}", "ERROR") + if retry < max_retries - 1: + time.sleep(2) + + raise RuntimeError(f"Stage completion callback failed after retries: task={task_id}, stage={stage_index}") + def post_result(self, task_id: int, shop_name: str, country_code: str, asin: str, status: str,is_done: bool = False): """回传处理结果到API @@ -596,8 +639,8 @@ class MatchTak: max_retries = 3 for retry in range(max_retries): try: - print("================【匹配】=====================") - self.log(f"尝试回传结果 (第 {retry + 1}/{max_retries} 次)") + print("================回传处理结果====================") + self.log(f"尝试回传结果 (第{retry + 1}/{max_retries} 次)") self.log(f"回传URL: {url}") self.log(f"回传数据: {payload}") response = requests.post( @@ -608,12 +651,11 @@ class MatchTak: verify=False ) self.log(f"回传结果: {response.text}") - - if response.status_code == 200: - self.log(f"结果回传成功: {asin} - {status}") + data = response.json() if response.text else {} + if response.status_code == 200 and isinstance(data, dict) and data.get("success"): + self.log(f"回传结果成功: {asin} - {status}") return - else: - self.log(f"结果回传失败,状态码: {response.status_code}", "WARNING") + self.log(f"回传结果失败,状态码: {response.status_code}", "WARNING") print("=====================================") except Exception as e: @@ -624,9 +666,7 @@ class MatchTak: if retry < max_retries - 1: time.sleep(2) - self.log(f"已达到最大重试次数,结果回传最终失败", "ERROR") - - + raise RuntimeError("已达到最大重试次数,结果回传最终失败") if __name__ == "__main__": user_info = { diff --git a/backend-java/src/main/java/com/nanri/aiimage/config/DeleteBrandProgressProperties.java b/backend-java/src/main/java/com/nanri/aiimage/config/DeleteBrandProgressProperties.java index 2435149..f14c26b 100644 --- a/backend-java/src/main/java/com/nanri/aiimage/config/DeleteBrandProgressProperties.java +++ b/backend-java/src/main/java/com/nanri/aiimage/config/DeleteBrandProgressProperties.java @@ -20,4 +20,10 @@ public class DeleteBrandProgressProperties { */ private long productRiskStaleTimeoutMinutes = 10; private long productRiskInitialTimeoutMinutes = 10; + + /** + * Shop match RUNNING timeout auto-finalize/fail threshold in minutes. + */ + private long shopMatchStaleTimeoutMinutes = 10; + private long shopMatchInitialTimeoutMinutes = 10; } diff --git a/backend-java/src/main/java/com/nanri/aiimage/modules/deletebrand/controller/DeleteBrandRunController.java b/backend-java/src/main/java/com/nanri/aiimage/modules/deletebrand/controller/DeleteBrandRunController.java index 54e0a83..b9492bc 100644 --- a/backend-java/src/main/java/com/nanri/aiimage/modules/deletebrand/controller/DeleteBrandRunController.java +++ b/backend-java/src/main/java/com/nanri/aiimage/modules/deletebrand/controller/DeleteBrandRunController.java @@ -1,6 +1,7 @@ package com.nanri.aiimage.modules.deletebrand.controller; import com.nanri.aiimage.common.api.ApiResponse; +import com.nanri.aiimage.common.exception.BusinessException; import com.nanri.aiimage.modules.deletebrand.model.dto.DeleteBrandRunRequest; import com.nanri.aiimage.modules.deletebrand.model.dto.DeleteBrandSubmitResultRequest; import com.nanri.aiimage.modules.deletebrand.model.dto.DeleteBrandTaskBatchRequest; @@ -97,9 +98,18 @@ public class DeleteBrandRunController { @Operation(summary = "提交删除品牌处理结果", description = "插件/前端分片回传处理结果;后端累计分片并推进进度,全部完成后再执行最终组装与落库。") public ApiResponse submitResult( @PathVariable Long taskId, - @Valid @RequestBody DeleteBrandSubmitResultRequest request) { - deleteBrandRunService.submitResult(taskId, request); - return ApiResponse.success(null); + @Valid @RequestBody DeleteBrandSubmitResultRequest request, + jakarta.servlet.http.HttpServletResponse response) { + try { + deleteBrandRunService.submitResult(taskId, request); + return ApiResponse.success(null); + } catch (BusinessException ex) { + response.setStatus(HttpStatus.BAD_REQUEST.value()); + return ApiResponse.fail(ex.getMessage()); + } catch (Exception ex) { + response.setStatus(HttpStatus.INTERNAL_SERVER_ERROR.value()); + return ApiResponse.fail("服务异常:" + ex.getMessage()); + } } @GetMapping("/tasks/{taskId}/download") diff --git a/backend-java/src/main/java/com/nanri/aiimage/modules/deletebrand/service/DeleteBrandStaleTaskService.java b/backend-java/src/main/java/com/nanri/aiimage/modules/deletebrand/service/DeleteBrandStaleTaskService.java index 4e4ebf0..2623dfc 100644 --- a/backend-java/src/main/java/com/nanri/aiimage/modules/deletebrand/service/DeleteBrandStaleTaskService.java +++ b/backend-java/src/main/java/com/nanri/aiimage/modules/deletebrand/service/DeleteBrandStaleTaskService.java @@ -5,6 +5,8 @@ import com.baomidou.mybatisplus.core.conditions.update.LambdaUpdateWrapper; import com.nanri.aiimage.config.DeleteBrandProgressProperties; import com.nanri.aiimage.modules.productrisk.service.ProductRiskTaskCacheService; import com.nanri.aiimage.modules.productrisk.service.ProductRiskTaskService; +import com.nanri.aiimage.modules.shopmatch.service.ShopMatchTaskCacheService; +import com.nanri.aiimage.modules.shopmatch.service.ShopMatchTaskService; import com.nanri.aiimage.modules.task.mapper.FileTaskMapper; import com.nanri.aiimage.modules.task.model.entity.FileTaskEntity; import lombok.RequiredArgsConstructor; @@ -18,7 +20,7 @@ import java.time.ZoneId; import java.util.Map; /** - * 删除品牌任务的超时回收与 finalize 补偿;并与删除品牌共用同一套定时调度,对商品风险(PRODUCT_RISK_RESOLVE)长时间无回传做失败收尾。 + * 鍒犻櫎鍝佺墝浠诲姟鐨勮秴鏃跺洖鏀朵笌 finalize 琛ュ伩锛涘苟涓庡垹闄ゅ搧鐗屽叡鐢ㄥ悓涓€濂楀畾鏃惰皟搴︼紝瀵瑰晢鍝侀闄╋紙PRODUCT_RISK_RESOLVE锛夐暱鏃堕棿鏃犲洖浼犲仛澶辫触鏀跺熬銆? */ @Service @RequiredArgsConstructor @@ -27,12 +29,15 @@ public class DeleteBrandStaleTaskService { private static final String MODULE_TYPE_DELETE_BRAND = "DELETE_BRAND"; private static final String MODULE_TYPE_PRODUCT_RISK = "PRODUCT_RISK_RESOLVE"; + private static final String MODULE_TYPE_SHOP_MATCH = "SHOP_MATCH"; private final FileTaskMapper fileTaskMapper; private final DeleteBrandTaskCacheService deleteBrandTaskCacheService; private final DeleteBrandRunService deleteBrandRunService; private final ProductRiskTaskService productRiskTaskService; private final ProductRiskTaskCacheService productRiskTaskCacheService; + private final ShopMatchTaskService shopMatchTaskService; + private final ShopMatchTaskCacheService shopMatchTaskCacheService; private final DeleteBrandProgressProperties deleteBrandProgressProperties; @Scheduled(cron = "${aiimage.delete-brand-progress.stale-check-cron:0 */2 * * * *}") @@ -41,6 +46,7 @@ public class DeleteBrandStaleTaskService { log.info("[stale-check] scan started thread={}", Thread.currentThread().getName()); failStaleDeleteBrandTasks(); ProductRiskStaleCheckStats stats = failStaleProductRiskResolveTasks(); + ShopMatchStaleCheckStats shopMatchStats = failStaleShopMatchTasks(); log.info("[stale-check] product-risk summary scanned={} finalized={} failed={} skipped={} elapsedMs={} thread={}", stats.scannedTaskCount, stats.finalizedTaskCount, @@ -48,12 +54,19 @@ public class DeleteBrandStaleTaskService { stats.skippedTaskCount, System.currentTimeMillis() - startedAt, Thread.currentThread().getName()); + log.info("[stale-check] shop-match summary scanned={} finalized={} failed={} skipped={} elapsedMs={} thread={}", + shopMatchStats.scannedTaskCount, + shopMatchStats.finalizedTaskCount, + shopMatchStats.failedTaskCount, + shopMatchStats.skippedTaskCount, + System.currentTimeMillis() - startedAt, + Thread.currentThread().getName()); } private void failStaleDeleteBrandTasks() { LocalDateTime threshold = LocalDateTime.now().minusMinutes(deleteBrandProgressProperties.getHeartbeatTimeoutMinutes()); - // 先按 DB updatedAt 粗筛,避免全表扫描 + // 鍏堟寜 DB updatedAt 绮楃瓫锛岄伩鍏嶅叏琛ㄦ壂鎻? java.util.List runningTasks = fileTaskMapper.selectList(new LambdaQueryWrapper() .eq(FileTaskEntity::getModuleType, MODULE_TYPE_DELETE_BRAND) .eq(FileTaskEntity::getStatus, "RUNNING") @@ -83,19 +96,19 @@ public class DeleteBrandStaleTaskService { } LocalDateTime lastActivityTime; - // 只有当 has_progress 为 true 且超过 15 分钟没心跳,才认为是异常卡死 + // 鍙湁褰?has_progress 涓?true 涓旇秴杩?15 鍒嗛挓娌″績璺筹紝鎵嶈涓烘槸寮傚父鍗℃ if (lastHeartbeatAt > 0 && isActive) { - // 有心跳,用最后心跳时间对比 15 分钟 (threshold 是 now - 15) + // 鏈夊績璺筹紝鐢ㄦ渶鍚庡績璺虫椂闂村姣?15 鍒嗛挓 (threshold 鏄?now - 15) lastActivityTime = LocalDateTime.ofInstant(Instant.ofEpochMilli(lastHeartbeatAt), ZoneId.systemDefault()); if (lastActivityTime.isAfter(threshold)) { - continue; // 没超时 + continue; // 娌¤秴鏃? } } else { - // 没有心跳,或者当前处在 has_progress: false (等待用户点击下一个任务推送的静默期) - // 静默期和排队期的宽限期一样,设为 12 个小时 + // 娌℃湁蹇冭烦锛屾垨鑰呭綋鍓嶅鍦?has_progress: false 锛堢瓑寰呯敤鎴风偣鍑讳笅涓€涓换鍔℃帹閫佺殑闈欓粯鏈燂級 + // 闈欓粯鏈熷拰鎺掗槦鏈熺殑瀹介檺鏈熶竴鏍凤紝璁句负 12 涓皬鏃? LocalDateTime queuedThreshold = LocalDateTime.now().minusHours(12); if (task.getCreatedAt() != null && task.getCreatedAt().isAfter(queuedThreshold)) { - continue; // 等待下个队列推送中,不要杀它! + continue; // 绛夊緟涓嬩釜闃熷垪鎺ㄩ€佷腑锛屼笉瑕佹潃瀹冿紒 } } @@ -104,7 +117,7 @@ public class DeleteBrandStaleTaskService { .eq(FileTaskEntity::getModuleType, MODULE_TYPE_DELETE_BRAND) .eq(FileTaskEntity::getStatus, "RUNNING") .set(FileTaskEntity::getStatus, "FAILED") - .set(FileTaskEntity::getErrorMessage, "结果回传长时间无响应,任务已自动失败") + .set(FileTaskEntity::getErrorMessage, "缁撴灉鍥炰紶闀挎椂闂存棤鍝嶅簲锛屼换鍔″凡鑷姩澶辫触") .set(FileTaskEntity::getUpdatedAt, LocalDateTime.now()) .set(FileTaskEntity::getFinishedAt, LocalDateTime.now())); @@ -157,7 +170,7 @@ public class DeleteBrandStaleTaskService { .eq(FileTaskEntity::getModuleType, MODULE_TYPE_PRODUCT_RISK) .eq(FileTaskEntity::getStatus, "RUNNING") .set(FileTaskEntity::getStatus, "FAILED") - .set(FileTaskEntity::getErrorMessage, "长时间未收到 Python 结果,任务已自动失败") + .set(FileTaskEntity::getErrorMessage, "闀挎椂闂存湭鏀跺埌 Python 缁撴灉锛屼换鍔″凡鑷姩澶辫触") .set(FileTaskEntity::getUpdatedAt, LocalDateTime.now()) .set(FileTaskEntity::getFinishedAt, LocalDateTime.now())); if (updated > 0) { @@ -170,6 +183,63 @@ public class DeleteBrandStaleTaskService { return stats; } + private ShopMatchStaleCheckStats failStaleShopMatchTasks() { + ShopMatchStaleCheckStats stats = new ShopMatchStaleCheckStats(); + long minutes = Math.max(1L, deleteBrandProgressProperties.getShopMatchStaleTimeoutMinutes()); + long initialMinutes = Math.max(minutes, deleteBrandProgressProperties.getShopMatchInitialTimeoutMinutes()); + LocalDateTime now = LocalDateTime.now(); + LocalDateTime threshold = now.minusMinutes(minutes); + LocalDateTime initialThreshold = now.minusMinutes(initialMinutes); + java.util.List runningTasks = fileTaskMapper.selectList(new LambdaQueryWrapper() + .eq(FileTaskEntity::getModuleType, MODULE_TYPE_SHOP_MATCH) + .eq(FileTaskEntity::getStatus, "RUNNING") + .lt(FileTaskEntity::getUpdatedAt, threshold) + .last("limit 200")); + stats.scannedTaskCount = runningTasks.size(); + if (runningTasks.isEmpty()) { + return stats; + } + log.info("[stale-check] shop-match candidates={} threshold={} timeoutMinutes={} initialThreshold={} initialTimeoutMinutes={}", + runningTasks.size(), threshold, minutes, initialThreshold, initialMinutes); + for (FileTaskEntity task : runningTasks) { + boolean hasUploadedPayload = !shopMatchTaskCacheService.getAllShopMergedPayload(task.getId()).isEmpty(); + boolean hasFinishedRows = (task.getSuccessFileCount() != null && task.getSuccessFileCount() > 0) + || (task.getFailedFileCount() != null && task.getFailedFileCount() > 0); + boolean hasStartedProgress = hasUploadedPayload || hasFinishedRows; + if (!hasStartedProgress && task.getCreatedAt() != null && task.getCreatedAt().isAfter(initialThreshold)) { + stats.skippedTaskCount++; + log.info("[stale-check] shop-match skip initial-grace taskId={} createdAt={} initialThreshold={}", + task.getId(), task.getCreatedAt(), initialThreshold); + continue; + } + try { + if (shopMatchTaskService.tryFinalizeTask(task.getId(), true)) { + stats.finalizedTaskCount++; + log.info("[stale-check] shop-match finalized taskId={}", task.getId()); + continue; + } + } catch (Exception ex) { + log.warn("[stale-check] shop-match finalize threw taskId={} msg={}", task.getId(), ex.getMessage()); + } + int updated = fileTaskMapper.update(null, new LambdaUpdateWrapper() + .eq(FileTaskEntity::getId, task.getId()) + .eq(FileTaskEntity::getModuleType, MODULE_TYPE_SHOP_MATCH) + .eq(FileTaskEntity::getStatus, "RUNNING") + .set(FileTaskEntity::getStatus, "FAILED") + .set(FileTaskEntity::getErrorMessage, "长时间未收到 Python 结果回传,任务已自动失败") + .set(FileTaskEntity::getUpdatedAt, LocalDateTime.now()) + .set(FileTaskEntity::getFinishedAt, LocalDateTime.now())); + if (updated > 0) { + stats.failedTaskCount++; + shopMatchTaskCacheService.deleteTaskCache(task.getId()); + log.warn("[stale-check] shop-match failed taskId={} reason=timeout", task.getId()); + } else { + stats.skippedTaskCount++; + } + } + return stats; + } + @Scheduled(cron = "${aiimage.delete-brand-progress.finalize-check-cron:30 */2 * * * *}") public void finalizeCompletedRunningTasks() { java.util.List runningTasks = fileTaskMapper.selectList(new LambdaQueryWrapper() @@ -182,7 +252,7 @@ public class DeleteBrandStaleTaskService { try { deleteBrandRunService.tryFinalizeTask(task.getId(), true); } catch (Exception ignored) { - // 定时补偿不影响主流程;下次调度或人工重试时再处理 + // 瀹氭椂琛ュ伩涓嶅奖鍝嶄富娴佺▼锛涗笅娆¤皟搴︽垨浜哄伐閲嶈瘯鏃跺啀澶勭悊 } } } @@ -193,4 +263,12 @@ public class DeleteBrandStaleTaskService { private int failedTaskCount; private int skippedTaskCount; } + + private static final class ShopMatchStaleCheckStats { + private int scannedTaskCount; + private int finalizedTaskCount; + private int failedTaskCount; + private int skippedTaskCount; + } } + diff --git a/backend-java/src/main/java/com/nanri/aiimage/modules/productrisk/model/vo/ProductRiskTaskItemVo.java b/backend-java/src/main/java/com/nanri/aiimage/modules/productrisk/model/vo/ProductRiskTaskItemVo.java index 971ce12..d63cce6 100644 --- a/backend-java/src/main/java/com/nanri/aiimage/modules/productrisk/model/vo/ProductRiskTaskItemVo.java +++ b/backend-java/src/main/java/com/nanri/aiimage/modules/productrisk/model/vo/ProductRiskTaskItemVo.java @@ -1,40 +1,60 @@ package com.nanri.aiimage.modules.productrisk.model.vo; import com.fasterxml.jackson.annotation.JsonProperty; +import com.nanri.aiimage.modules.shopmatch.model.vo.ShopMatchTaskStageVo; import io.swagger.v3.oas.annotations.media.Schema; import lombok.Data; +import java.util.ArrayList; +import java.util.List; + @Data -@Schema(description = "商品风险任务概要(biz_file_task 核心字段)") +@Schema(description = "Task summary") public class ProductRiskTaskItemVo { - @Schema(description = "任务主键", example = "200") + @Schema(description = "Task primary key", example = "200") private Long id; @JsonProperty("taskNo") - @Schema(description = "业务单号,如 PRODUCT_RISK_RESOLVE-雪花 id") + @Schema(description = "Task number") private String taskNo; - @Schema(description = "任务状态:RUNNING 执行中;SUCCESS 已结束且至少一店成功;FAILED 已结束且全部失败或整体失败") + @Schema(description = "Task status") private String status; @JsonProperty("errorMessage") - @Schema(description = "失败或部分失败时的汇总错误说明;成功且无附加信息时可能为空") + @Schema(description = "Task error message") private String errorMessage; @JsonProperty("createdAt") - @Schema(description = "创建时间(字符串格式,服务端 LocalDateTime.toString())") + @Schema(description = "Created at") private String createdAt; @JsonProperty("updatedAt") - @Schema(description = "最后更新时间") + @Schema(description = "Updated at") private String updatedAt; @JsonProperty("finishedAt") - @Schema(description = "结束时间;RUNNING 时通常为空") + @Schema(description = "Finished at") private String finishedAt; @JsonProperty("scheduledAt") - @Schema(description = "定时执行时间,未设置时为空") + @Schema(description = "Next scheduled execution time") private String scheduledAt; + + @JsonProperty("countryCodes") + @Schema(description = "Country codes used by shop match task") + private List countryCodes = new ArrayList<>(); + + @JsonProperty("scheduleStages") + @Schema(description = "Stage schedule of shop match task") + private List scheduleStages = new ArrayList<>(); + + @JsonProperty("currentStageIndex") + @Schema(description = "Next pending stage index") + private Integer currentStageIndex; + + @JsonProperty("activeStageIndex") + @Schema(description = "Current running stage index") + private Integer activeStageIndex; } diff --git a/backend-java/src/main/java/com/nanri/aiimage/modules/shopmatch/controller/ShopMatchController.java b/backend-java/src/main/java/com/nanri/aiimage/modules/shopmatch/controller/ShopMatchController.java index 63952d3..0e4d174 100644 --- a/backend-java/src/main/java/com/nanri/aiimage/modules/shopmatch/controller/ShopMatchController.java +++ b/backend-java/src/main/java/com/nanri/aiimage/modules/shopmatch/controller/ShopMatchController.java @@ -3,7 +3,6 @@ package com.nanri.aiimage.modules.shopmatch.controller; import com.nanri.aiimage.common.api.ApiResponse; import com.nanri.aiimage.modules.productrisk.model.dto.ProductRiskCandidateAddRequest; import com.nanri.aiimage.modules.productrisk.model.dto.ProductRiskCountryPreferenceSaveRequest; -import com.nanri.aiimage.modules.productrisk.model.dto.ProductRiskCreateTaskRequest; import com.nanri.aiimage.modules.productrisk.model.dto.ProductRiskMatchShopsRequest; import com.nanri.aiimage.modules.productrisk.model.dto.ProductRiskTaskBatchRequest; import com.nanri.aiimage.modules.productrisk.model.vo.ProductRiskCandidateVo; @@ -13,9 +12,15 @@ import com.nanri.aiimage.modules.productrisk.model.vo.ProductRiskDashboardVo; import com.nanri.aiimage.modules.productrisk.model.vo.ProductRiskHistoryVo; import com.nanri.aiimage.modules.productrisk.model.vo.ProductRiskMatchShopsVo; import com.nanri.aiimage.modules.productrisk.model.vo.ProductRiskTaskBatchVo; +import com.nanri.aiimage.modules.shopmatch.model.dto.ShopMatchCreateTaskRequest; +import com.nanri.aiimage.modules.shopmatch.model.dto.ShopMatchStageCompleteRequest; import com.nanri.aiimage.modules.shopmatch.model.dto.ShopMatchSubmitResultRequest; import com.nanri.aiimage.modules.shopmatch.service.ShopMatchResolveService; import com.nanri.aiimage.modules.shopmatch.service.ShopMatchTaskService; +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.Parameter; +import io.swagger.v3.oas.annotations.enums.ParameterIn; +import io.swagger.v3.oas.annotations.tags.Tag; import jakarta.validation.Valid; import lombok.RequiredArgsConstructor; import org.springframework.http.HttpHeaders; @@ -40,95 +45,143 @@ import java.util.List; @RestController @RequiredArgsConstructor @RequestMapping("/api/shop-match") +@Tag(name = "匹配店铺", description = "店铺候选、国家顺序、批量匹配、定时任务、Python 回传结果与下载接口。") public class ShopMatchController { private final ShopMatchResolveService shopMatchResolveService; private final ShopMatchTaskService shopMatchTaskService; @GetMapping("/candidates") - public ApiResponse> listCandidates(@RequestParam("user_id") Long userId) { + @Operation(summary = "查询候选店铺", description = "返回当前用户在匹配店铺模块下保存的候选店铺列表。") + public ApiResponse> listCandidates( + @Parameter(name = "user_id", description = "当前用户 ID", required = true, in = ParameterIn.QUERY, example = "1") + @RequestParam("user_id") Long userId) { return ApiResponse.success(shopMatchResolveService.listCandidates(userId)); } @PostMapping("/candidates") + @Operation(summary = "新增候选店铺", description = "新增一条候选店铺记录,供前端后续匹配与创建任务使用。") public ApiResponse addCandidate(@Valid @RequestBody ProductRiskCandidateAddRequest request) { return ApiResponse.success(shopMatchResolveService.addCandidate(request)); } @DeleteMapping("/candidates/{id}") - public ApiResponse deleteCandidate(@PathVariable Long id, @RequestParam("user_id") Long userId) { + @Operation(summary = "删除候选店铺", description = "删除当前用户名下的一条候选店铺记录。") + public ApiResponse deleteCandidate( + @Parameter(description = "候选店铺记录主键", example = "10") @PathVariable Long id, + @Parameter(name = "user_id", description = "当前用户 ID", required = true, in = ParameterIn.QUERY, example = "1") + @RequestParam("user_id") Long userId) { shopMatchResolveService.deleteCandidate(userId, id); return ApiResponse.success(null); } @GetMapping("/country-preference") - public ApiResponse getCountryPreference(@RequestParam("user_id") Long userId) { + @Operation(summary = "查询国家配置顺序", description = "返回当前用户保存的国家勾选与执行顺序。") + public ApiResponse getCountryPreference( + @Parameter(name = "user_id", description = "当前用户 ID", required = true, in = ParameterIn.QUERY, example = "1") + @RequestParam("user_id") Long userId) { return ApiResponse.success(shopMatchResolveService.getCountryPreference(userId)); } @PutMapping("/country-preference") + @Operation(summary = "保存国家配置顺序", description = "保存当前用户在匹配店铺模块中的国家勾选与执行顺序。") public ApiResponse saveCountryPreference(@Valid @RequestBody ProductRiskCountryPreferenceSaveRequest request) { return ApiResponse.success(shopMatchResolveService.saveCountryPreference(request)); } @PostMapping("/match-shops") + @Operation(summary = "批量匹配店铺", description = "按店铺名批量匹配紫鸟索引,返回店铺 ID、平台、公司与匹配状态。") public ApiResponse matchShops(@Valid @RequestBody ProductRiskMatchShopsRequest request) { return ApiResponse.success(shopMatchResolveService.matchShops(request)); } @GetMapping("/dashboard") - public ApiResponse dashboard(@RequestParam("user_id") Long userId) { + @Operation(summary = "统计看板", description = "返回候选店铺数、已结束任务数、成功数和失败数。") + public ApiResponse dashboard( + @Parameter(name = "user_id", description = "当前用户 ID", required = true, in = ParameterIn.QUERY, example = "1") + @RequestParam("user_id") Long userId) { return ApiResponse.success(shopMatchTaskService.dashboard(userId)); } @GetMapping("/history") - public ApiResponse history(@RequestParam("user_id") Long userId) { + @Operation(summary = "查询任务记录", description = "返回当前用户的运行中与历史任务记录。") + public ApiResponse history( + @Parameter(name = "user_id", description = "当前用户 ID", required = true, in = ParameterIn.QUERY, example = "1") + @RequestParam("user_id") Long userId) { return ApiResponse.success(shopMatchTaskService.listHistory(userId)); } @DeleteMapping("/history/{resultId}") - public ApiResponse deleteHistory(@PathVariable Long resultId, @RequestParam("user_id") Long userId) { + @Operation(summary = "删除结果记录", description = "删除一条任务结果记录,并同步重算任务状态。") + public ApiResponse deleteHistory( + @Parameter(description = "结果记录主键", example = "1001") @PathVariable Long resultId, + @Parameter(name = "user_id", description = "当前用户 ID", required = true, in = ParameterIn.QUERY, example = "1") + @RequestParam("user_id") Long userId) { shopMatchTaskService.deleteHistory(resultId, userId); return ApiResponse.success(null); } @PostMapping("/tasks") - public ApiResponse createTask(@Valid @RequestBody ProductRiskCreateTaskRequest request) { + @Operation(summary = "创建匹配店铺任务", description = "创建任务与各店铺占位结果,支持立即执行或多时间点定时执行。") + public ApiResponse createTask(@Valid @RequestBody ShopMatchCreateTaskRequest request) { return ApiResponse.success(shopMatchTaskService.createTask(request)); } @DeleteMapping("/tasks/{taskId}") - public ApiResponse deleteTask(@PathVariable Long taskId, @RequestParam("user_id") Long userId) { + @Operation(summary = "删除任务", description = "删除整条任务及其下所有结果记录。") + public ApiResponse deleteTask( + @Parameter(description = "任务主键", example = "200") @PathVariable Long taskId, + @Parameter(name = "user_id", description = "当前用户 ID", required = true, in = ParameterIn.QUERY, example = "1") + @RequestParam("user_id") Long userId) { shopMatchTaskService.deleteTask(taskId, userId); return ApiResponse.success(null); } @PostMapping("/tasks/{taskId}/activate") - public ApiResponse activateTask(@PathVariable Long taskId, @RequestParam("user_id") Long userId) { - shopMatchTaskService.activateTask(taskId, userId); + @Operation(summary = "激活定时阶段", description = "在某个定时阶段开始执行前,将任务切换到运行中状态。") + public ApiResponse activateTask( + @Parameter(description = "任务主键", example = "200") @PathVariable Long taskId, + @Parameter(name = "user_id", description = "当前用户 ID", required = true, in = ParameterIn.QUERY, example = "1") @RequestParam("user_id") Long userId, + @Parameter(name = "stage_index", description = "阶段下标,从 0 开始", required = true, in = ParameterIn.QUERY, example = "0") @RequestParam("stage_index") Integer stageIndex) { + shopMatchTaskService.activateTask(taskId, userId, stageIndex); + return ApiResponse.success(null); + } + + @PostMapping("/tasks/{taskId}/stage-finished") + @Operation(summary = "标记阶段完成", description = "某个定时阶段完成后推进到下一阶段,或等待最终收尾。") + public ApiResponse completeStage( + @Parameter(description = "任务主键", example = "200") @PathVariable Long taskId, + @Parameter(name = "user_id", description = "当前用户 ID", required = true, in = ParameterIn.QUERY, example = "1") @RequestParam("user_id") Long userId, + @Valid @RequestBody ShopMatchStageCompleteRequest request) { + shopMatchTaskService.completeStage(taskId, userId, request); return ApiResponse.success(null); } @PostMapping("/tasks/batch") + @Operation(summary = "批量查询任务详情", description = "供前端轮询任务状态与阶段进度使用。") public ApiResponse tasksBatch(@Valid @RequestBody ProductRiskTaskBatchRequest request) { return ApiResponse.success(shopMatchTaskService.getTaskDetailsBatch(request.getTaskIds())); } @PostMapping("/tasks/{taskId}/result") - public ApiResponse submitResult(@PathVariable Long taskId, @Valid @RequestBody ShopMatchSubmitResultRequest request) { + @Operation(summary = "提交匹配结果", description = "Python 可多次分批回传结果;后端会组装文件并在超时场景自动补偿收尾。") + public ApiResponse submitResult( + @Parameter(description = "任务主键", example = "200") @PathVariable Long taskId, + @Valid @RequestBody ShopMatchSubmitResultRequest request) { shopMatchTaskService.submitResult(taskId, request); return ApiResponse.success(null); } @GetMapping("/results/{resultId}/download") + @Operation(summary = "下载结果文件", description = "按结果记录下载后端已生成的 Excel 文件。") public void downloadResult( - @PathVariable Long resultId, - @RequestParam("user_id") Long userId, + @Parameter(description = "结果记录主键", example = "1001") @PathVariable Long resultId, + @Parameter(name = "user_id", description = "当前用户 ID", required = true, in = ParameterIn.QUERY, example = "1") @RequestParam("user_id") Long userId, jakarta.servlet.http.HttpServletResponse response) { String url = shopMatchTaskService.resolveResultDownloadUrl(resultId, userId); String filename = shopMatchTaskService.resolveResultDownloadFilename(resultId, userId); if (url == null || url.isBlank()) { - throw new ResponseStatusException(HttpStatus.NOT_FOUND, "暂无可下载结果"); + throw new ResponseStatusException(HttpStatus.NOT_FOUND, "No downloadable result"); } try { String encodedFilename = URLEncoder.encode(filename, StandardCharsets.UTF_8).replace("+", "%20"); @@ -144,7 +197,7 @@ public class ShopMatchController { response.getOutputStream().flush(); } } catch (Exception ex) { - throw new ResponseStatusException(HttpStatus.INTERNAL_SERVER_ERROR, "下载失败"); + throw new ResponseStatusException(HttpStatus.INTERNAL_SERVER_ERROR, "Download failed"); } } } diff --git a/backend-java/src/main/java/com/nanri/aiimage/modules/shopmatch/model/dto/ShopMatchCreateTaskRequest.java b/backend-java/src/main/java/com/nanri/aiimage/modules/shopmatch/model/dto/ShopMatchCreateTaskRequest.java new file mode 100644 index 0000000..4367eeb --- /dev/null +++ b/backend-java/src/main/java/com/nanri/aiimage/modules/shopmatch/model/dto/ShopMatchCreateTaskRequest.java @@ -0,0 +1,46 @@ +package com.nanri.aiimage.modules.shopmatch.model.dto; + +import com.fasterxml.jackson.annotation.JsonProperty; +import com.nanri.aiimage.modules.productrisk.model.vo.ProductRiskShopQueueItemVo; +import io.swagger.v3.oas.annotations.media.Schema; +import jakarta.validation.Valid; +import jakarta.validation.constraints.NotEmpty; +import jakarta.validation.constraints.NotNull; +import lombok.Data; + +import java.time.LocalDateTime; +import java.util.ArrayList; +import java.util.List; + +@Data +@Schema(description = "创建匹配店铺任务请求") +public class ShopMatchCreateTaskRequest { + + @NotNull(message = "user_id cannot be null") + @JsonProperty("user_id") + @Schema(description = "任务所属用户 ID", requiredMode = Schema.RequiredMode.REQUIRED, example = "1") + private Long userId; + + @NotEmpty(message = "items cannot be empty") + @Valid + @Schema(description = "已匹配店铺列表", requiredMode = Schema.RequiredMode.REQUIRED) + private List items = new ArrayList<>(); + + @NotEmpty(message = "country_codes cannot be empty") + @JsonProperty("country_codes") + @Schema(description = "每个阶段都要执行的国家代码列表", requiredMode = Schema.RequiredMode.REQUIRED) + private List countryCodes = new ArrayList<>(); + + @JsonProperty("schedule_times") + @Schema(description = "可选的多时间点执行时间;为空表示立即执行", example = "[\"2026-04-10T18:00:00\",\"2026-04-10T19:00:00\"]") + private List scheduleTimes = new ArrayList<>(); + + @JsonProperty("current_stage_index") + @Schema(description = "下一次待执行的阶段下标,由后端维护") + private Integer currentStageIndex; + + @JsonProperty("active_stage_index") + @Schema(description = "当前运行中的阶段下标,由后端维护") + private Integer activeStageIndex; +} + diff --git a/backend-java/src/main/java/com/nanri/aiimage/modules/shopmatch/model/dto/ShopMatchStageCompleteRequest.java b/backend-java/src/main/java/com/nanri/aiimage/modules/shopmatch/model/dto/ShopMatchStageCompleteRequest.java new file mode 100644 index 0000000..0f0963e --- /dev/null +++ b/backend-java/src/main/java/com/nanri/aiimage/modules/shopmatch/model/dto/ShopMatchStageCompleteRequest.java @@ -0,0 +1,17 @@ +package com.nanri.aiimage.modules.shopmatch.model.dto; + +import com.fasterxml.jackson.annotation.JsonProperty; +import io.swagger.v3.oas.annotations.media.Schema; +import jakarta.validation.constraints.NotNull; +import lombok.Data; + +@Data +@Schema(description = "匹配店铺任务阶段完成回调") +public class ShopMatchStageCompleteRequest { + + @NotNull(message = "stage_index cannot be null") + @JsonProperty("stage_index") + @Schema(description = "已完成的阶段下标,从 0 开始", requiredMode = Schema.RequiredMode.REQUIRED, example = "0") + private Integer stageIndex; +} + diff --git a/backend-java/src/main/java/com/nanri/aiimage/modules/shopmatch/model/vo/ShopMatchTaskStageVo.java b/backend-java/src/main/java/com/nanri/aiimage/modules/shopmatch/model/vo/ShopMatchTaskStageVo.java new file mode 100644 index 0000000..23414c2 --- /dev/null +++ b/backend-java/src/main/java/com/nanri/aiimage/modules/shopmatch/model/vo/ShopMatchTaskStageVo.java @@ -0,0 +1,22 @@ +package com.nanri.aiimage.modules.shopmatch.model.vo; + +import com.fasterxml.jackson.annotation.JsonProperty; +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; + +@Data +@Schema(description = "匹配店铺任务阶段信息") +public class ShopMatchTaskStageVo { + + @JsonProperty("stageIndex") + @Schema(description = "阶段下标,从 0 开始", example = "0") + private Integer stageIndex; + + @JsonProperty("scheduledAt") + @Schema(description = "该阶段的计划执行时间") + private String scheduledAt; + + @Schema(description = "阶段状态:SCHEDULED / RUNNING / COMPLETED / FAILED / PENDING") + private String status; +} + diff --git a/backend-java/src/main/java/com/nanri/aiimage/modules/shopmatch/service/ShopMatchTaskService.java b/backend-java/src/main/java/com/nanri/aiimage/modules/shopmatch/service/ShopMatchTaskService.java index 3c0c067..8fcad42 100644 --- a/backend-java/src/main/java/com/nanri/aiimage/modules/shopmatch/service/ShopMatchTaskService.java +++ b/backend-java/src/main/java/com/nanri/aiimage/modules/shopmatch/service/ShopMatchTaskService.java @@ -6,7 +6,7 @@ import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper; import com.fasterxml.jackson.databind.ObjectMapper; import com.nanri.aiimage.common.exception.BusinessException; import com.nanri.aiimage.modules.file.service.oss.OssStorageService; -import com.nanri.aiimage.modules.productrisk.model.dto.ProductRiskCreateTaskRequest; +import com.nanri.aiimage.modules.productrisk.model.enums.ProductRiskCountryCode; import com.nanri.aiimage.modules.productrisk.model.vo.ProductRiskCreateTaskVo; import com.nanri.aiimage.modules.productrisk.model.vo.ProductRiskDashboardVo; import com.nanri.aiimage.modules.productrisk.model.vo.ProductRiskHistoryVo; @@ -15,9 +15,12 @@ import com.nanri.aiimage.modules.productrisk.model.vo.ProductRiskShopQueueItemVo import com.nanri.aiimage.modules.productrisk.model.vo.ProductRiskTaskBatchVo; import com.nanri.aiimage.modules.productrisk.model.vo.ProductRiskTaskDetailVo; import com.nanri.aiimage.modules.productrisk.model.vo.ProductRiskTaskItemVo; +import com.nanri.aiimage.modules.shopmatch.model.vo.ShopMatchTaskStageVo; import com.nanri.aiimage.modules.shopmatch.mapper.ShopMatchShopCandidateMapper; +import com.nanri.aiimage.modules.shopmatch.model.dto.ShopMatchCreateTaskRequest; import com.nanri.aiimage.modules.shopmatch.model.dto.ShopMatchRowDto; import com.nanri.aiimage.modules.shopmatch.model.dto.ShopMatchShopPayloadDto; +import com.nanri.aiimage.modules.shopmatch.model.dto.ShopMatchStageCompleteRequest; import com.nanri.aiimage.modules.shopmatch.model.dto.ShopMatchSubmitResultRequest; import com.nanri.aiimage.modules.shopmatch.model.entity.ShopMatchShopCandidateEntity; import com.nanri.aiimage.modules.task.mapper.FileResultMapper; @@ -34,6 +37,7 @@ import java.io.File; import java.time.LocalDateTime; import java.util.ArrayList; import java.util.LinkedHashMap; +import java.util.LinkedHashSet; import java.util.List; import java.util.Locale; import java.util.Map; @@ -57,7 +61,7 @@ public class ShopMatchTaskService { public ProductRiskDashboardVo dashboard(Long userId) { if (userId == null || userId <= 0) { - throw new BusinessException("user_id 不合法"); + throw new BusinessException("user_id is invalid"); } ProductRiskDashboardVo vo = new ProductRiskDashboardVo(); vo.setCandidateCount(candidateMapper.selectCount(new LambdaQueryWrapper() @@ -82,7 +86,7 @@ public class ShopMatchTaskService { public ProductRiskHistoryVo listHistory(Long userId) { if (userId == null || userId <= 0) { - throw new BusinessException("user_id 不合法"); + throw new BusinessException("user_id is invalid"); } ProductRiskHistoryVo vo = new ProductRiskHistoryVo(); List entities = fileResultMapper.selectList(new LambdaQueryWrapper() @@ -114,11 +118,11 @@ public class ShopMatchTaskService { @Transactional public void deleteHistory(Long resultId, Long userId) { if (userId == null || userId <= 0) { - throw new BusinessException("user_id 不合法"); + throw new BusinessException("user_id is invalid"); } FileResultEntity entity = fileResultMapper.selectById(resultId); if (entity == null || !MODULE_TYPE.equals(entity.getModuleType()) || !userId.equals(entity.getUserId())) { - throw new BusinessException("记录不存在"); + throw new BusinessException("Invalid request"); } Long taskId = entity.getTaskId(); fileResultMapper.deleteById(resultId); @@ -130,11 +134,11 @@ public class ShopMatchTaskService { @Transactional public void deleteTask(Long taskId, Long userId) { if (userId == null || userId <= 0) { - throw new BusinessException("user_id 不合法"); + throw new BusinessException("user_id is invalid"); } FileTaskEntity task = fileTaskMapper.selectById(taskId); if (task == null || !MODULE_TYPE.equals(task.getModuleType()) || !userId.equals(task.getUserId())) { - throw new BusinessException("任务不存在"); + throw new BusinessException("Invalid request"); } fileResultMapper.delete(new LambdaQueryWrapper() .eq(FileResultEntity::getTaskId, taskId) @@ -182,10 +186,10 @@ public class ShopMatchTaskService { public String resolveResultDownloadUrl(Long resultId, Long userId) { FileResultEntity entity = fileResultMapper.selectById(resultId); if (entity == null || !MODULE_TYPE.equals(entity.getModuleType()) || !userId.equals(entity.getUserId())) { - throw new BusinessException("记录不存在"); + throw new BusinessException("Invalid request"); } if (entity.getResultFileUrl() == null || entity.getResultFileUrl().isBlank()) { - throw new BusinessException("暂无可下载文件"); + throw new BusinessException("Invalid request"); } return ossStorageService.generateFreshDownloadUrl(entity.getResultFileUrl()); } @@ -193,7 +197,7 @@ public class ShopMatchTaskService { public String resolveResultDownloadFilename(Long resultId, Long userId) { FileResultEntity entity = fileResultMapper.selectById(resultId); if (entity == null || !MODULE_TYPE.equals(entity.getModuleType()) || !userId.equals(entity.getUserId())) { - throw new BusinessException("记录不存在"); + throw new BusinessException("Invalid request"); } return entity.getResultFilename() != null && !entity.getResultFilename().isBlank() ? entity.getResultFilename() @@ -201,34 +205,41 @@ public class ShopMatchTaskService { } @Transactional - public ProductRiskCreateTaskVo createTask(ProductRiskCreateTaskRequest request) { + public ProductRiskCreateTaskVo createTask(ShopMatchCreateTaskRequest request) { if (request.getUserId() == null || request.getUserId() <= 0) { - throw new BusinessException("user_id 不合法"); + throw new BusinessException("user_id is invalid"); } if (request.getItems() == null || request.getItems().isEmpty()) { - throw new BusinessException("items 不能为空"); + throw new BusinessException("items 涓嶈兘涓虹┖"); } List uniqueItems = dedupeQueueItems(request.getItems()); if (uniqueItems.isEmpty()) { - throw new BusinessException("items 不能为空"); + throw new BusinessException("items 涓嶈兘涓虹┖"); } for (ProductRiskShopQueueItemVo item : uniqueItems) { if (item == null || !item.isMatched()) { - throw new BusinessException("存在未匹配店铺,无法创建任务"); + throw new BusinessException("瀛樺湪鏈尮閰嶅簵閾猴紝鏃犳硶鍒涘缓浠诲姟"); } } + List countryCodes = normalizeCountryCodes(request.getCountryCodes()); + List scheduleTimes = normalizeScheduleTimes(request.getScheduleTimes()); LocalDateTime now = LocalDateTime.now(); - LocalDateTime scheduledAt = request.getScheduleTime(); - if (scheduledAt != null && !scheduledAt.isAfter(now)) { - throw new BusinessException("schedule_time 必须晚于当前时间"); + + ShopMatchCreateTaskRequest persistedRequest = new ShopMatchCreateTaskRequest(); + persistedRequest.setUserId(request.getUserId()); + persistedRequest.setItems(uniqueItems); + persistedRequest.setCountryCodes(countryCodes); + persistedRequest.setScheduleTimes(scheduleTimes); + if (!scheduleTimes.isEmpty()) { + persistedRequest.setCurrentStageIndex(0); } FileTaskEntity task = new FileTaskEntity(); task.setTaskNo(MODULE_TYPE + "-" + IdUtil.getSnowflakeNextIdStr()); task.setModuleType(MODULE_TYPE); task.setTaskMode("PYTHON_QUEUE"); - task.setStatus(scheduledAt != null ? "SCHEDULED" : "RUNNING"); + task.setStatus(scheduleTimes.isEmpty() ? "RUNNING" : "SCHEDULED"); task.setSourceFileCount(uniqueItems.size()); task.setSuccessFileCount(0); task.setFailedFileCount(0); @@ -236,14 +247,14 @@ public class ShopMatchTaskService { task.setUserId(request.getUserId()); task.setCreatedAt(now); task.setUpdatedAt(now); - task.setScheduledAt(scheduledAt); + task.setScheduledAt(scheduleTimes.isEmpty() ? null : scheduleTimes.get(0)); fileTaskMapper.insert(task); List snapshot = new ArrayList<>(); for (ProductRiskShopQueueItemVo item : uniqueItems) { String normalized = ziniaoShopSwitchService.normalizeShopName(item.getShopName()); if (normalized.isBlank()) { - throw new BusinessException("店铺名无效"); + throw new BusinessException("Invalid request"); } FileResultEntity result = new FileResultEntity(); result.setTaskId(task.getId()); @@ -257,10 +268,10 @@ public class ShopMatchTaskService { snapshot.add(toSnapshotVo(result, item, task.getStatus())); } try { - task.setRequestJson(objectMapper.writeValueAsString(request)); + task.setRequestJson(objectMapper.writeValueAsString(persistedRequest)); task.setResultJson(objectMapper.writeValueAsString(snapshot)); } catch (Exception ex) { - throw new BusinessException("序列化任务数据失败"); + throw new BusinessException("Invalid request"); } fileTaskMapper.updateById(task); @@ -271,49 +282,96 @@ public class ShopMatchTaskService { } @Transactional - public void activateTask(Long taskId, Long userId) { + public void activateTask(Long taskId, Long userId, Integer stageIndex) { if (taskId == null || taskId <= 0) { - throw new BusinessException("taskId 不合法"); + throw new BusinessException("taskId is invalid"); } if (userId == null || userId <= 0) { - throw new BusinessException("user_id 不合法"); + throw new BusinessException("user_id is invalid"); + } + if (stageIndex == null || stageIndex < 0) { + throw new BusinessException("stage_index is invalid"); } FileTaskEntity task = fileTaskMapper.selectById(taskId); if (task == null || !MODULE_TYPE.equals(task.getModuleType()) || !userId.equals(task.getUserId())) { - throw new BusinessException("任务不存在"); + throw new BusinessException("Invalid request"); } if ("SUCCESS".equals(task.getStatus()) || "FAILED".equals(task.getStatus())) { - throw new BusinessException("任务已结束"); - } - if ("RUNNING".equals(task.getStatus())) { - return; + throw new BusinessException("Invalid request"); } if (!"SCHEDULED".equals(task.getStatus())) { - throw new BusinessException("当前任务状态不允许激活"); + throw new BusinessException("Invalid request"); } if (task.getScheduledAt() != null && LocalDateTime.now().isBefore(task.getScheduledAt())) { - throw new BusinessException("未到定时执行时间"); + throw new BusinessException("鏈埌瀹氭椂鎵ц鏃堕棿"); } + ShopMatchCreateTaskRequest state = parseTaskRequest(task); + int currentStageIndex = state.getCurrentStageIndex() == null ? 0 : state.getCurrentStageIndex(); + if (currentStageIndex != stageIndex) { + throw new BusinessException("褰撳墠杞宸插彉鍖栵紝璇峰埛鏂板悗閲嶈瘯"); + } + if (state.getScheduleTimes() == null || stageIndex >= state.getScheduleTimes().size()) { + throw new BusinessException("Invalid request"); + } + state.setActiveStageIndex(stageIndex); task.setStatus("RUNNING"); task.setUpdatedAt(LocalDateTime.now()); + persistTaskRequest(task, state); fileTaskMapper.updateById(task); } + @Transactional + public void completeStage(Long taskId, Long userId, ShopMatchStageCompleteRequest request) { + if (taskId == null || taskId <= 0) { + throw new BusinessException("taskId is invalid"); + } + if (userId == null || userId <= 0) { + throw new BusinessException("user_id is invalid"); + } + if (request == null || request.getStageIndex() == null || request.getStageIndex() < 0) { + throw new BusinessException("stage_index is invalid"); + } + FileTaskEntity task = fileTaskMapper.selectById(taskId); + if (task == null || !MODULE_TYPE.equals(task.getModuleType()) || !userId.equals(task.getUserId())) { + throw new BusinessException("Invalid request"); + } + if (!"RUNNING".equals(task.getStatus())) { + throw new BusinessException("Invalid request"); + } + ShopMatchCreateTaskRequest state = parseTaskRequest(task); + if (state.getActiveStageIndex() == null || !state.getActiveStageIndex().equals(request.getStageIndex())) { + throw new BusinessException("杞鐘舵€佷笉鍖归厤"); + } + int nextIndex = request.getStageIndex() + 1; + if (state.getScheduleTimes() == null || nextIndex >= state.getScheduleTimes().size()) { + throw new BusinessException("Current stage is already the final stage"); + } + state.setCurrentStageIndex(nextIndex); + state.setActiveStageIndex(null); + task.setStatus("SCHEDULED"); + task.setScheduledAt(state.getScheduleTimes().get(nextIndex)); + task.setUpdatedAt(LocalDateTime.now()); + task.setFinishedAt(null); + persistTaskRequest(task, state); + fileTaskMapper.updateById(task); + } + + @Transactional public void submitResult(Long taskId, ShopMatchSubmitResultRequest request) { if (taskId == null || taskId <= 0) { - throw new BusinessException("taskId 不合法"); + throw new BusinessException("taskId is invalid"); } if (request == null || request.getShops() == null || request.getShops().isEmpty()) { - throw new BusinessException("shops 不能为空"); + throw new BusinessException("shops 涓嶈兘涓虹┖"); } FileTaskEntity task = fileTaskMapper.selectById(taskId); if (task == null || !MODULE_TYPE.equals(task.getModuleType())) { - throw new BusinessException("任务不存在"); + throw new BusinessException("Invalid request"); } if ("SUCCESS".equals(task.getStatus()) || "FAILED".equals(task.getStatus())) { - throw new BusinessException("任务已结束,拒绝重复提交"); + throw new BusinessException("浠诲姟宸茬粨鏉燂紝鎷掔粷閲嶅鎻愪氦"); } List resultRows = fileResultMapper.selectList(new LambdaQueryWrapper() @@ -350,7 +408,7 @@ public class ShopMatchTaskService { continue; } if (countPayloadRows(merged) <= 0) { - markResultFailed(result, "未收到可组装的结果数据"); + markResultFailed(result, "No result rows received for assembly"); shopMatchTaskCacheService.removeShopMergedPayload(taskId, shopKey); continue; } @@ -373,6 +431,102 @@ public class ShopMatchTaskService { updateTaskStatusFromLatestRows(task, latest); } + @Transactional + public boolean tryFinalizeTask(Long taskId, boolean fromCompensation) { + if (taskId == null || taskId <= 0) { + return false; + } + FileTaskEntity task = fileTaskMapper.selectById(taskId); + if (task == null || !MODULE_TYPE.equals(task.getModuleType())) { + return false; + } + if ("SUCCESS".equals(task.getStatus()) || "FAILED".equals(task.getStatus())) { + return true; + } + + List resultRows = fileResultMapper.selectList(new LambdaQueryWrapper() + .eq(FileResultEntity::getTaskId, taskId) + .eq(FileResultEntity::getModuleType, MODULE_TYPE) + .orderByAsc(FileResultEntity::getId)); + if (resultRows.isEmpty()) { + return false; + } + + Map cachedPayloadByShop = shopMatchTaskCacheService.getAllShopMergedPayload(taskId); + File workRoot = FileUtil.mkdir(FileUtil.file(System.getProperty("java.io.tmpdir"), "shop-match-result", String.valueOf(taskId))); + List batchErrors = new ArrayList<>(); + boolean changed = false; + + for (FileResultEntity result : resultRows) { + boolean success = result.getSuccess() != null && result.getSuccess() == 1; + boolean failed = result.getErrorMessage() != null && !result.getErrorMessage().isBlank(); + if (success || failed) { + continue; + } + + String shopKey = result.getSourceFilename(); + ShopMatchShopPayloadDto cachedPayload = shopKey == null ? null : cachedPayloadByShop.get(shopKey); + + if (cachedPayload == null) { + markResultFailed(result, "Python interrupted before this shop finished uploading results"); + batchErrors.add(shopKey + ": Python interrupted before this shop finished uploading results"); + changed = true; + continue; + } + + if (cachedPayload.getError() != null && !cachedPayload.getError().isBlank()) { + markResultFailed(result, cachedPayload.getError()); + batchErrors.add(shopKey + ": " + cachedPayload.getError()); + shopMatchTaskCacheService.removeShopMergedPayload(taskId, shopKey); + changed = true; + continue; + } + + if (countPayloadRows(cachedPayload) <= 0 && payloadHasAnyDoneTrue(cachedPayload)) { + markResultFailed(result, "No result rows received for assembly"); + shopMatchTaskCacheService.removeShopMergedPayload(taskId, shopKey); + changed = true; + log.warn("[shop-match] stale finalize finished without data taskId={} shop={} fromCompensation={}", + taskId, shopKey, fromCompensation); + continue; + } + + if (countPayloadRows(cachedPayload) <= 0) { + markResultFailed(result, "Python interrupted before any assembleable rows were uploaded"); + batchErrors.add(shopKey + ": Python interrupted before any assembleable rows were uploaded"); + shopMatchTaskCacheService.removeShopMergedPayload(taskId, shopKey); + changed = true; + continue; + } + + try { + assembleShopResult(result, shopKey, cachedPayload, workRoot); + shopMatchTaskCacheService.removeShopMergedPayload(taskId, shopKey); + changed = true; + log.warn("[shop-match] stale finalize assembled taskId={} shop={} fromCompensation={} completedFlag={}", + taskId, shopKey, fromCompensation, isShopPayloadCompleted(cachedPayload)); + } catch (Exception ex) { + String message = ex.getMessage() == null ? "assemble failed" : ex.getMessage(); + markResultFailed(result, message); + batchErrors.add(shopKey + ": " + message); + shopMatchTaskCacheService.removeShopMergedPayload(taskId, shopKey); + changed = true; + log.warn("[shop-match] stale finalize failed taskId={} shop={} msg={}", taskId, shopKey, message); + } + } + + if (!changed && cachedPayloadByShop.isEmpty()) { + return false; + } + + List latest = fileResultMapper.selectList(new LambdaQueryWrapper() + .eq(FileResultEntity::getTaskId, taskId) + .eq(FileResultEntity::getModuleType, MODULE_TYPE) + .orderByAsc(FileResultEntity::getId)); + updateTaskStatusFromLatestRows(task, latest, batchErrors); + return true; + } + private void assembleShopResult(FileResultEntity result, String shopKey, ShopMatchShopPayloadDto payload, File workRoot) { Map> countries = excelAssemblyService.normalizeCountriesMap(payload.getCountries()); String displayName = payload.getShopName() != null && !payload.getShopName().isBlank() ? payload.getShopName().trim() : shopKey; @@ -406,6 +560,12 @@ public class ShopMatchTaskService { } private void updateTaskStatusFromLatestRows(FileTaskEntity task, List latest) { + updateTaskStatusFromLatestRows(task, latest, List.of()); + } + + private void updateTaskStatusFromLatestRows(FileTaskEntity task, + List latest, + List batchErrors) { int ok = 0; int fail = 0; List errors = new ArrayList<>(); @@ -435,7 +595,7 @@ public class ShopMatchTaskService { task.setFinishedAt(LocalDateTime.now()); } else if (ok > 0) { task.setStatus("SUCCESS"); - task.setErrorMessage(String.join("; ", errors)); + task.setErrorMessage(String.join("; ", errors.isEmpty() ? batchErrors : errors)); task.setFinishedAt(LocalDateTime.now()); } else { task.setStatus("FAILED"); @@ -473,7 +633,7 @@ public class ShopMatchTaskService { .orderByAsc(FileResultEntity::getId)); List items = new ArrayList<>(); for (FileResultEntity row : rows) { - items.add(toHistoryItemVo(row, taskStatus)); + throw new BusinessException("Task request payload is missing"); } return items; } @@ -488,10 +648,20 @@ public class ShopMatchTaskService { vo.setUpdatedAt(fmt(task.getUpdatedAt())); vo.setFinishedAt(fmt(task.getFinishedAt())); vo.setScheduledAt(fmt(task.getScheduledAt())); + + ShopMatchCreateTaskRequest request = parseTaskRequestSilently(task); + if (request != null) { + vo.setCountryCodes(new ArrayList<>(request.getCountryCodes() == null ? List.of() : request.getCountryCodes())); + vo.setCurrentStageIndex(request.getCurrentStageIndex()); + vo.setActiveStageIndex(request.getActiveStageIndex()); + vo.setScheduleStages(buildStageVos(task, request)); + } return vo; } - private ProductRiskResultItemVo toSnapshotVo(FileResultEntity result, ProductRiskShopQueueItemVo item, String taskStatus) { + private ProductRiskResultItemVo toSnapshotVo(FileResultEntity result, + ProductRiskShopQueueItemVo item, + String taskStatus) { ProductRiskResultItemVo vo = new ProductRiskResultItemVo(); vo.setResultId(result.getId()); vo.setTaskId(result.getTaskId()); @@ -503,8 +673,10 @@ public class ShopMatchTaskService { vo.setMatchStatus(item.getMatchStatus()); vo.setMatchMessage(item.getMatchMessage()); vo.setTaskStatus(taskStatus); - vo.setScheduledAt(fmt(result.getTaskId() == null ? null : loadScheduledAt(result.getTaskId()))); vo.setSuccess(false); + vo.setError(null); + vo.setOutputFilename(null); + vo.setDownloadUrl(null); return vo; } @@ -515,22 +687,66 @@ public class ShopMatchTaskService { vo.setShopName(entity.getSourceFilename()); vo.setShopId(entity.getSourceFileUrl()); vo.setTaskStatus(taskStatus); - vo.setSuccess(entity.getSuccess() != null && entity.getSuccess() == 1); + boolean success = entity.getSuccess() != null && entity.getSuccess() == 1; + vo.setSuccess(success); vo.setError(entity.getErrorMessage()); vo.setOutputFilename(entity.getResultFilename()); vo.setDownloadUrl(entity.getResultFileUrl() == null || entity.getResultFileUrl().isBlank() ? null : ossStorageService.generateFreshDownloadUrl(entity.getResultFileUrl())); vo.setMatched(true); - if (entity.getTaskId() != null) { - vo.setScheduledAt(fmt(loadScheduledAt(entity.getTaskId()))); - } if (entity.getTaskId() != null) { mergeQueueFieldsFromRequest(vo, entity.getTaskId()); } return vo; } + private List buildStageVos(FileTaskEntity task, ShopMatchCreateTaskRequest request) { + List stages = new ArrayList<>(); + List scheduleTimes = request.getScheduleTimes() == null ? List.of() : request.getScheduleTimes(); + if (scheduleTimes.isEmpty()) { + return stages; + } + Integer currentStageIndex = request.getCurrentStageIndex(); + Integer activeStageIndex = request.getActiveStageIndex(); + for (int i = 0; i < scheduleTimes.size(); i++) { + ShopMatchTaskStageVo stage = new ShopMatchTaskStageVo(); + stage.setStageIndex(i); + stage.setScheduledAt(fmt(scheduleTimes.get(i))); + if ("SUCCESS".equals(task.getStatus())) { + stage.setStatus("COMPLETED"); + } else if ("FAILED".equals(task.getStatus())) { + if (activeStageIndex != null && activeStageIndex == i) { + stage.setStatus("FAILED"); + } else if (currentStageIndex != null && currentStageIndex > i) { + stage.setStatus("COMPLETED"); + } else { + stage.setStatus("PENDING"); + } + } else if ("RUNNING".equals(task.getStatus()) && activeStageIndex != null) { + if (i < activeStageIndex) { + stage.setStatus("COMPLETED"); + } else if (i == activeStageIndex) { + stage.setStatus("RUNNING"); + } else { + stage.setStatus("PENDING"); + } + } else if ("SCHEDULED".equals(task.getStatus()) && currentStageIndex != null) { + if (i < currentStageIndex) { + stage.setStatus("COMPLETED"); + } else if (i == currentStageIndex) { + stage.setStatus("SCHEDULED"); + } else { + stage.setStatus("PENDING"); + } + } else { + stage.setStatus("PENDING"); + } + stages.add(stage); + } + return stages; + } + private LocalDateTime loadScheduledAt(Long taskId) { FileTaskEntity task = fileTaskMapper.selectById(taskId); return task == null ? null : task.getScheduledAt(); @@ -538,27 +754,60 @@ public class ShopMatchTaskService { private void mergeQueueFieldsFromRequest(ProductRiskResultItemVo vo, Long taskId) { FileTaskEntity task = fileTaskMapper.selectById(taskId); - if (task == null || task.getRequestJson() == null || task.getRequestJson().isBlank()) { + ShopMatchCreateTaskRequest request = parseTaskRequestSilently(task); + if (request == null) { return; } - try { - ProductRiskCreateTaskRequest request = objectMapper.readValue(task.getRequestJson(), ProductRiskCreateTaskRequest.class); - String key = ziniaoShopSwitchService.normalizeShopName(vo.getShopName()); - for (ProductRiskShopQueueItemVo item : request.getItems()) { - if (item == null) { - continue; - } - if (key.equals(ziniaoShopSwitchService.normalizeShopName(item.getShopName()))) { - vo.setShopName(item.getShopName()); - vo.setShopId(item.getShopId()); - vo.setPlatform(item.getPlatform()); - vo.setCompanyName(item.getCompanyName()); - vo.setMatchStatus(item.getMatchStatus()); - vo.setMatchMessage(item.getMatchMessage()); - break; - } + String key = ziniaoShopSwitchService.normalizeShopName(vo.getShopName()); + for (ProductRiskShopQueueItemVo item : request.getItems()) { + if (item == null) { + continue; } + if (key.equals(ziniaoShopSwitchService.normalizeShopName(item.getShopName()))) { + vo.setShopName(item.getShopName()); + vo.setShopId(item.getShopId()); + vo.setPlatform(item.getPlatform()); + vo.setCompanyName(item.getCompanyName()); + vo.setMatchStatus(item.getMatchStatus()); + vo.setMatchMessage(item.getMatchMessage()); + break; + } + } + } + + private ShopMatchCreateTaskRequest parseTaskRequest(FileTaskEntity task) { + ShopMatchCreateTaskRequest request = parseTaskRequestSilently(task); + if (request == null) { + throw new BusinessException("浠诲姟閰嶇疆缂哄け"); + } + if (request.getItems() == null) { + request.setItems(new ArrayList<>()); + } + if (request.getCountryCodes() == null) { + request.setCountryCodes(new ArrayList<>()); + } + if (request.getScheduleTimes() == null) { + request.setScheduleTimes(new ArrayList<>()); + } + return request; + } + + private ShopMatchCreateTaskRequest parseTaskRequestSilently(FileTaskEntity task) { + if (task == null || task.getRequestJson() == null || task.getRequestJson().isBlank()) { + return null; + } + try { + return objectMapper.readValue(task.getRequestJson(), ShopMatchCreateTaskRequest.class); } catch (Exception ignored) { + return null; + } + } + + private void persistTaskRequest(FileTaskEntity task, ShopMatchCreateTaskRequest request) { + try { + task.setRequestJson(objectMapper.writeValueAsString(request)); + } catch (Exception ex) { + throw new BusinessException("Invalid request"); } } @@ -580,6 +829,55 @@ public class ShopMatchTaskService { return new ArrayList<>(byNorm.values()); } + private List normalizeCountryCodes(List raw) { + if (raw == null || raw.isEmpty()) { + throw new BusinessException("country_codes 涓嶈兘涓虹┖"); + } + LinkedHashSet out = new LinkedHashSet<>(); + for (String code : raw) { + if (code == null || code.isBlank()) { + continue; + } + String normalized = code.trim().toUpperCase(Locale.ROOT); + try { + ProductRiskCountryCode.valueOf(normalized); + } catch (IllegalArgumentException ex) { + throw new BusinessException("闈炴硶鍥藉浠g爜: " + code); + } + out.add(normalized); + } + if (out.isEmpty()) { + throw new BusinessException("country_codes 涓嶈兘涓虹┖"); + } + return new ArrayList<>(out); + } + + private List normalizeScheduleTimes(List raw) { + if (raw == null || raw.isEmpty()) { + return List.of(); + } + List out = new ArrayList<>(); + LocalDateTime now = LocalDateTime.now(); + LocalDateTime previous = null; + for (LocalDateTime time : raw) { + if (time == null) { + continue; + } + if (!time.isAfter(now)) { + throw new BusinessException("schedule_times 蹇呴』鏅氫簬褰撳墠鏃堕棿"); + } + if (previous != null && !time.isAfter(previous)) { + throw new BusinessException("schedule_times 蹇呴』鎸夋椂闂撮€掑"); + } + out.add(time); + previous = time; + } + if (out.isEmpty()) { + throw new BusinessException("schedule_times 涓嶈兘涓虹┖"); + } + return out; + } + private static String safeFileStem(String shopName) { String value = shopName == null ? "shop" : shopName.trim(); if (value.isBlank()) { @@ -716,3 +1014,5 @@ public class ShopMatchTaskService { return row == null || row.getAsin() == null || row.getAsin().trim().isEmpty(); } } + + diff --git a/backend-java/src/main/java/com/nanri/aiimage/modules/task/service/ModuleHistoryCleanupService.java b/backend-java/src/main/java/com/nanri/aiimage/modules/task/service/ModuleHistoryCleanupService.java index ee9471f..17177b4 100644 --- a/backend-java/src/main/java/com/nanri/aiimage/modules/task/service/ModuleHistoryCleanupService.java +++ b/backend-java/src/main/java/com/nanri/aiimage/modules/task/service/ModuleHistoryCleanupService.java @@ -13,13 +13,18 @@ import org.springframework.scheduling.annotation.Scheduled; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; +import java.util.ArrayList; import java.util.List; +import java.util.Locale; +import java.util.Set; @Slf4j @Service @RequiredArgsConstructor public class ModuleHistoryCleanupService { + private static final Set TERMINAL_STATUSES = Set.of("SUCCESS", "FAILED", "CANCELLED", "CANCELED"); + private final ModuleCleanupProperties moduleCleanupProperties; private final FileTaskMapper fileTaskMapper; private final FileResultMapper fileResultMapper; @@ -35,18 +40,50 @@ public class ModuleHistoryCleanupService { return; } + List moduleTasks = fileTaskMapper.selectList(new LambdaQueryWrapper() + .in(FileTaskEntity::getModuleType, moduleTypes) + .select(FileTaskEntity::getId, FileTaskEntity::getModuleType, FileTaskEntity::getStatus)); + + List cleanupTaskIds = new ArrayList<>(); + List skippedActiveTaskIds = new ArrayList<>(); + for (FileTaskEntity task : moduleTasks) { + if (task == null || task.getId() == null) { + continue; + } + if (isTerminalStatus(task.getStatus())) { + cleanupTaskIds.add(task.getId()); + } else { + skippedActiveTaskIds.add(task.getId()); + } + } + + if (cleanupTaskIds.isEmpty()) { + log.info("模块历史清理跳过: moduleTypes={}, activeTaskIds={}, reason=no-terminal-tasks", + moduleTypes, skippedActiveTaskIds); + return; + } + int deletedResults = fileResultMapper.delete(new LambdaQueryWrapper() - .in(FileResultEntity::getModuleType, moduleTypes)); + .in(FileResultEntity::getModuleType, moduleTypes) + .in(FileResultEntity::getTaskId, cleanupTaskIds)); int resetTasks = fileTaskMapper.update(null, new LambdaUpdateWrapper() - .in(FileTaskEntity::getModuleType, moduleTypes) + .in(FileTaskEntity::getId, cleanupTaskIds) .set(FileTaskEntity::getResultJson, null) .set(FileTaskEntity::getRequestJson, null) .set(FileTaskEntity::getErrorMessage, null)); int deletedTasks = fileTaskMapper.delete(new LambdaQueryWrapper() - .in(FileTaskEntity::getModuleType, moduleTypes)); + .in(FileTaskEntity::getId, cleanupTaskIds)); - log.info("模块历史清理完成: moduleTypes={}, deletedResults={}, resetTasks={}, deletedTasks={}", moduleTypes, deletedResults, resetTasks, deletedTasks); + log.info("模块历史清理完成: moduleTypes={}, deletedResults={}, resetTasks={}, deletedTasks={}, skippedActiveTaskIds={}", + moduleTypes, deletedResults, resetTasks, deletedTasks, skippedActiveTaskIds); + } + + private boolean isTerminalStatus(String status) { + if (status == null || status.isBlank()) { + return false; + } + return TERMINAL_STATUSES.contains(status.trim().toUpperCase(Locale.ROOT)); } } diff --git a/backend-java/src/main/resources/application.yml b/backend-java/src/main/resources/application.yml index 4113b6d..e92b5b7 100644 --- a/backend-java/src/main/resources/application.yml +++ b/backend-java/src/main/resources/application.yml @@ -84,6 +84,8 @@ aiimage: finalize-check-cron: ${AIIMAGE_DELETE_BRAND_FINALIZE_CHECK_CRON:30 */2 * * * *} product-risk-stale-timeout-minutes: ${AIIMAGE_PRODUCT_RISK_STALE_TIMEOUT_MINUTES:10} product-risk-initial-timeout-minutes: ${AIIMAGE_PRODUCT_RISK_INITIAL_TIMEOUT_MINUTES:10} + shop-match-stale-timeout-minutes: ${AIIMAGE_SHOP_MATCH_STALE_TIMEOUT_MINUTES:10} + shop-match-initial-timeout-minutes: ${AIIMAGE_SHOP_MATCH_INITIAL_TIMEOUT_MINUTES:10} module-cleanup: enabled: ${AIIMAGE_MODULE_CLEANUP_ENABLED:true} cron: ${AIIMAGE_MODULE_CLEANUP_CRON:0 0 0 * * *} diff --git a/frontend-vue/src/pages/brand/components/BrandShopMatchTab.vue b/frontend-vue/src/pages/brand/components/BrandShopMatchTab.vue index 23ee183..2d52ca5 100644 --- a/frontend-vue/src/pages/brand/components/BrandShopMatchTab.vue +++ b/frontend-vue/src/pages/brand/components/BrandShopMatchTab.vue @@ -1,233 +1,95 @@ -