修复BUG,完善匹配店铺
This commit is contained in:
1
app/.env
1
app/.env
@@ -12,6 +12,7 @@ zn_username=%E8%87%AA%E5%8A%A8%E5%8C%96_Robot
|
|||||||
client_name=ShuFuAI
|
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:18081
|
# java_api_base=http://8.136.19.173:18081
|
||||||
# java_api_base=http://8.136.19.173:18080
|
# java_api_base=http://8.136.19.173:18080
|
||||||
|
|||||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
BIN
app/amazon/__pycache__/match_action.cpython-312.pyc
Normal file
BIN
app/amazon/__pycache__/match_action.cpython-312.pyc
Normal file
Binary file not shown.
BIN
app/amazon/__pycache__/tool.cpython-312.pyc
Normal file
BIN
app/amazon/__pycache__/tool.cpython-312.pyc
Normal file
Binary file not shown.
@@ -226,6 +226,9 @@ class MatchTak:
|
|||||||
items = data.get("items", [])
|
items = data.get("items", [])
|
||||||
country_codes = data.get("country_codes", [])
|
country_codes = data.get("country_codes", [])
|
||||||
risk_listing_filter = data.get("risk_listing_filter", "All")
|
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:
|
if not task_id:
|
||||||
self.log("任务ID为空,跳过", "WARNING")
|
self.log("任务ID为空,跳过", "WARNING")
|
||||||
@@ -269,7 +272,7 @@ class MatchTak:
|
|||||||
show_notification(f"开始处理店铺: {shop_name}", "info")
|
show_notification(f"开始处理店铺: {shop_name}", "info")
|
||||||
|
|
||||||
try:
|
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:
|
if task_id in runing_task:
|
||||||
runing_task[task_id]["processed_shops"] += 1
|
runing_task[task_id]["processed_shops"] += 1
|
||||||
@@ -296,7 +299,7 @@ class MatchTak:
|
|||||||
runing_task[task_id]["status"] = "failed"
|
runing_task[task_id]["status"] = "failed"
|
||||||
runing_task[task_id]["error"] = str(e)
|
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:
|
Args:
|
||||||
@@ -406,9 +409,11 @@ class MatchTak:
|
|||||||
import traceback
|
import traceback
|
||||||
self.log(f"处理国家 {country_code} 失败: {str(e)}", "ERROR")
|
self.log(f"处理国家 {country_code} 失败: {str(e)}", "ERROR")
|
||||||
self.log(traceback.format_exc(), "ERROR")
|
self.log(traceback.format_exc(), "ERROR")
|
||||||
# 最后回传,标记完成
|
|
||||||
try:
|
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:
|
except Exception as e:
|
||||||
self.log(f"回传结果失败: {str(e)}", "ERROR")
|
self.log(f"回传结果失败: {str(e)}", "ERROR")
|
||||||
finally:
|
finally:
|
||||||
@@ -560,6 +565,44 @@ class MatchTak:
|
|||||||
|
|
||||||
self.log(f"国家 {country_name} 处理完成")
|
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):
|
def post_result(self, task_id: int, shop_name: str, country_code: str, asin: str, status: str,is_done: bool = False):
|
||||||
"""回传处理结果到API
|
"""回传处理结果到API
|
||||||
|
|
||||||
@@ -596,8 +639,8 @@ class MatchTak:
|
|||||||
max_retries = 3
|
max_retries = 3
|
||||||
for retry in range(max_retries):
|
for retry in range(max_retries):
|
||||||
try:
|
try:
|
||||||
print("================【匹配】=====================")
|
print("================回传处理结果====================")
|
||||||
self.log(f"尝试回传结果 (第 {retry + 1}/{max_retries} 次)")
|
self.log(f"尝试回传结果 (第{retry + 1}/{max_retries} 次)")
|
||||||
self.log(f"回传URL: {url}")
|
self.log(f"回传URL: {url}")
|
||||||
self.log(f"回传数据: {payload}")
|
self.log(f"回传数据: {payload}")
|
||||||
response = requests.post(
|
response = requests.post(
|
||||||
@@ -608,12 +651,11 @@ class MatchTak:
|
|||||||
verify=False
|
verify=False
|
||||||
)
|
)
|
||||||
self.log(f"回传结果: {response.text}")
|
self.log(f"回传结果: {response.text}")
|
||||||
|
data = response.json() if response.text else {}
|
||||||
if response.status_code == 200:
|
if response.status_code == 200 and isinstance(data, dict) and data.get("success"):
|
||||||
self.log(f"结果回传成功: {asin} - {status}")
|
self.log(f"回传结果成功: {asin} - {status}")
|
||||||
return
|
return
|
||||||
else:
|
self.log(f"回传结果失败,状态码: {response.status_code}", "WARNING")
|
||||||
self.log(f"结果回传失败,状态码: {response.status_code}", "WARNING")
|
|
||||||
print("=====================================")
|
print("=====================================")
|
||||||
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
@@ -624,9 +666,7 @@ class MatchTak:
|
|||||||
if retry < max_retries - 1:
|
if retry < max_retries - 1:
|
||||||
time.sleep(2)
|
time.sleep(2)
|
||||||
|
|
||||||
self.log(f"已达到最大重试次数,结果回传最终失败", "ERROR")
|
raise RuntimeError("已达到最大重试次数,结果回传最终失败")
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
if __name__ == "__main__":
|
||||||
user_info = {
|
user_info = {
|
||||||
|
|||||||
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
@@ -5,8 +5,8 @@
|
|||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||||
<title>格式转换 - 数富AI</title>
|
<title>格式转换 - 数富AI</title>
|
||||||
<script type="module" crossorigin src="/assets/convert.js"></script>
|
<script type="module" crossorigin src="/assets/convert.js"></script>
|
||||||
<link rel="modulepreload" crossorigin href="/assets/pywebview-Cq_E2BnJ.js">
|
<link rel="modulepreload" crossorigin href="/assets/pywebview-C_Si_gmj.js">
|
||||||
<link rel="modulepreload" crossorigin href="/assets/brand-DZ5_PGnw.js">
|
<link rel="modulepreload" crossorigin href="/assets/brand-CyZr-IbT.js">
|
||||||
<link rel="stylesheet" crossorigin href="/assets/pywebview-BbIq-QPS.css">
|
<link rel="stylesheet" crossorigin href="/assets/pywebview-BbIq-QPS.css">
|
||||||
<link rel="stylesheet" crossorigin href="/assets/convert-7wWJ02Tw.css">
|
<link rel="stylesheet" crossorigin href="/assets/convert-7wWJ02Tw.css">
|
||||||
</head>
|
</head>
|
||||||
|
|||||||
@@ -5,8 +5,8 @@
|
|||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||||
<title>数据去重 - 数富AI</title>
|
<title>数据去重 - 数富AI</title>
|
||||||
<script type="module" crossorigin src="/assets/dedupe.js"></script>
|
<script type="module" crossorigin src="/assets/dedupe.js"></script>
|
||||||
<link rel="modulepreload" crossorigin href="/assets/pywebview-Cq_E2BnJ.js">
|
<link rel="modulepreload" crossorigin href="/assets/pywebview-C_Si_gmj.js">
|
||||||
<link rel="modulepreload" crossorigin href="/assets/brand-DZ5_PGnw.js">
|
<link rel="modulepreload" crossorigin href="/assets/brand-CyZr-IbT.js">
|
||||||
<link rel="stylesheet" crossorigin href="/assets/pywebview-BbIq-QPS.css">
|
<link rel="stylesheet" crossorigin href="/assets/pywebview-BbIq-QPS.css">
|
||||||
<link rel="stylesheet" crossorigin href="/assets/dedupe-BpNHwt51.css">
|
<link rel="stylesheet" crossorigin href="/assets/dedupe-BpNHwt51.css">
|
||||||
</head>
|
</head>
|
||||||
|
|||||||
@@ -5,8 +5,8 @@
|
|||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||||
<title>删除品牌 - 数富AI</title>
|
<title>删除品牌 - 数富AI</title>
|
||||||
<script type="module" crossorigin src="/assets/delete-brand.js"></script>
|
<script type="module" crossorigin src="/assets/delete-brand.js"></script>
|
||||||
<link rel="modulepreload" crossorigin href="/assets/pywebview-Cq_E2BnJ.js">
|
<link rel="modulepreload" crossorigin href="/assets/pywebview-C_Si_gmj.js">
|
||||||
<link rel="modulepreload" crossorigin href="/assets/brand-DZ5_PGnw.js">
|
<link rel="modulepreload" crossorigin href="/assets/brand-CyZr-IbT.js">
|
||||||
<link rel="stylesheet" crossorigin href="/assets/pywebview-BbIq-QPS.css">
|
<link rel="stylesheet" crossorigin href="/assets/pywebview-BbIq-QPS.css">
|
||||||
<link rel="stylesheet" crossorigin href="/assets/delete-brand-Dl7T0pmm.css">
|
<link rel="stylesheet" crossorigin href="/assets/delete-brand-Dl7T0pmm.css">
|
||||||
</head>
|
</head>
|
||||||
|
|||||||
@@ -5,8 +5,8 @@
|
|||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||||
<title>数据拆分 - 数富AI</title>
|
<title>数据拆分 - 数富AI</title>
|
||||||
<script type="module" crossorigin src="/assets/split.js"></script>
|
<script type="module" crossorigin src="/assets/split.js"></script>
|
||||||
<link rel="modulepreload" crossorigin href="/assets/pywebview-Cq_E2BnJ.js">
|
<link rel="modulepreload" crossorigin href="/assets/pywebview-C_Si_gmj.js">
|
||||||
<link rel="modulepreload" crossorigin href="/assets/brand-DZ5_PGnw.js">
|
<link rel="modulepreload" crossorigin href="/assets/brand-CyZr-IbT.js">
|
||||||
<link rel="stylesheet" crossorigin href="/assets/pywebview-BbIq-QPS.css">
|
<link rel="stylesheet" crossorigin href="/assets/pywebview-BbIq-QPS.css">
|
||||||
<link rel="stylesheet" crossorigin href="/assets/split-CRUIYKS6.css">
|
<link rel="stylesheet" crossorigin href="/assets/split-CRUIYKS6.css">
|
||||||
</head>
|
</head>
|
||||||
|
|||||||
BIN
backend-java/aiimage-backend.log.2026-04-10.0.gz
Normal file
BIN
backend-java/aiimage-backend.log.2026-04-10.0.gz
Normal file
Binary file not shown.
@@ -20,4 +20,10 @@ public class DeleteBrandProgressProperties {
|
|||||||
*/
|
*/
|
||||||
private long productRiskStaleTimeoutMinutes = 10;
|
private long productRiskStaleTimeoutMinutes = 10;
|
||||||
private long productRiskInitialTimeoutMinutes = 10;
|
private long productRiskInitialTimeoutMinutes = 10;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Shop match RUNNING timeout auto-finalize/fail threshold in minutes.
|
||||||
|
*/
|
||||||
|
private long shopMatchStaleTimeoutMinutes = 10;
|
||||||
|
private long shopMatchInitialTimeoutMinutes = 10;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
package com.nanri.aiimage.modules.deletebrand.controller;
|
package com.nanri.aiimage.modules.deletebrand.controller;
|
||||||
|
|
||||||
import com.nanri.aiimage.common.api.ApiResponse;
|
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.DeleteBrandRunRequest;
|
||||||
import com.nanri.aiimage.modules.deletebrand.model.dto.DeleteBrandSubmitResultRequest;
|
import com.nanri.aiimage.modules.deletebrand.model.dto.DeleteBrandSubmitResultRequest;
|
||||||
import com.nanri.aiimage.modules.deletebrand.model.dto.DeleteBrandTaskBatchRequest;
|
import com.nanri.aiimage.modules.deletebrand.model.dto.DeleteBrandTaskBatchRequest;
|
||||||
@@ -97,9 +98,18 @@ public class DeleteBrandRunController {
|
|||||||
@Operation(summary = "提交删除品牌处理结果", description = "插件/前端分片回传处理结果;后端累计分片并推进进度,全部完成后再执行最终组装与落库。")
|
@Operation(summary = "提交删除品牌处理结果", description = "插件/前端分片回传处理结果;后端累计分片并推进进度,全部完成后再执行最终组装与落库。")
|
||||||
public ApiResponse<Void> submitResult(
|
public ApiResponse<Void> submitResult(
|
||||||
@PathVariable Long taskId,
|
@PathVariable Long taskId,
|
||||||
@Valid @RequestBody DeleteBrandSubmitResultRequest request) {
|
@Valid @RequestBody DeleteBrandSubmitResultRequest request,
|
||||||
deleteBrandRunService.submitResult(taskId, request);
|
jakarta.servlet.http.HttpServletResponse response) {
|
||||||
return ApiResponse.success(null);
|
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")
|
@GetMapping("/tasks/{taskId}/download")
|
||||||
|
|||||||
@@ -5,6 +5,8 @@ import com.baomidou.mybatisplus.core.conditions.update.LambdaUpdateWrapper;
|
|||||||
import com.nanri.aiimage.config.DeleteBrandProgressProperties;
|
import com.nanri.aiimage.config.DeleteBrandProgressProperties;
|
||||||
import com.nanri.aiimage.modules.productrisk.service.ProductRiskTaskCacheService;
|
import com.nanri.aiimage.modules.productrisk.service.ProductRiskTaskCacheService;
|
||||||
import com.nanri.aiimage.modules.productrisk.service.ProductRiskTaskService;
|
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.mapper.FileTaskMapper;
|
||||||
import com.nanri.aiimage.modules.task.model.entity.FileTaskEntity;
|
import com.nanri.aiimage.modules.task.model.entity.FileTaskEntity;
|
||||||
import lombok.RequiredArgsConstructor;
|
import lombok.RequiredArgsConstructor;
|
||||||
@@ -18,7 +20,7 @@ import java.time.ZoneId;
|
|||||||
import java.util.Map;
|
import java.util.Map;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 删除品牌任务的超时回收与 finalize 补偿;并与删除品牌共用同一套定时调度,对商品风险(PRODUCT_RISK_RESOLVE)长时间无回传做失败收尾。
|
* 鍒犻櫎鍝佺墝浠诲姟鐨勮秴鏃跺洖鏀朵笌 finalize 琛ュ伩锛涘苟涓庡垹闄ゅ搧鐗屽叡鐢ㄥ悓涓€濂楀畾鏃惰皟搴︼紝瀵瑰晢鍝侀闄╋紙PRODUCT_RISK_RESOLVE锛夐暱鏃堕棿鏃犲洖浼犲仛澶辫触鏀跺熬銆?
|
||||||
*/
|
*/
|
||||||
@Service
|
@Service
|
||||||
@RequiredArgsConstructor
|
@RequiredArgsConstructor
|
||||||
@@ -27,12 +29,15 @@ public class DeleteBrandStaleTaskService {
|
|||||||
|
|
||||||
private static final String MODULE_TYPE_DELETE_BRAND = "DELETE_BRAND";
|
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_PRODUCT_RISK = "PRODUCT_RISK_RESOLVE";
|
||||||
|
private static final String MODULE_TYPE_SHOP_MATCH = "SHOP_MATCH";
|
||||||
|
|
||||||
private final FileTaskMapper fileTaskMapper;
|
private final FileTaskMapper fileTaskMapper;
|
||||||
private final DeleteBrandTaskCacheService deleteBrandTaskCacheService;
|
private final DeleteBrandTaskCacheService deleteBrandTaskCacheService;
|
||||||
private final DeleteBrandRunService deleteBrandRunService;
|
private final DeleteBrandRunService deleteBrandRunService;
|
||||||
private final ProductRiskTaskService productRiskTaskService;
|
private final ProductRiskTaskService productRiskTaskService;
|
||||||
private final ProductRiskTaskCacheService productRiskTaskCacheService;
|
private final ProductRiskTaskCacheService productRiskTaskCacheService;
|
||||||
|
private final ShopMatchTaskService shopMatchTaskService;
|
||||||
|
private final ShopMatchTaskCacheService shopMatchTaskCacheService;
|
||||||
private final DeleteBrandProgressProperties deleteBrandProgressProperties;
|
private final DeleteBrandProgressProperties deleteBrandProgressProperties;
|
||||||
|
|
||||||
@Scheduled(cron = "${aiimage.delete-brand-progress.stale-check-cron:0 */2 * * * *}")
|
@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());
|
log.info("[stale-check] scan started thread={}", Thread.currentThread().getName());
|
||||||
failStaleDeleteBrandTasks();
|
failStaleDeleteBrandTasks();
|
||||||
ProductRiskStaleCheckStats stats = failStaleProductRiskResolveTasks();
|
ProductRiskStaleCheckStats stats = failStaleProductRiskResolveTasks();
|
||||||
|
ShopMatchStaleCheckStats shopMatchStats = failStaleShopMatchTasks();
|
||||||
log.info("[stale-check] product-risk summary scanned={} finalized={} failed={} skipped={} elapsedMs={} thread={}",
|
log.info("[stale-check] product-risk summary scanned={} finalized={} failed={} skipped={} elapsedMs={} thread={}",
|
||||||
stats.scannedTaskCount,
|
stats.scannedTaskCount,
|
||||||
stats.finalizedTaskCount,
|
stats.finalizedTaskCount,
|
||||||
@@ -48,12 +54,19 @@ public class DeleteBrandStaleTaskService {
|
|||||||
stats.skippedTaskCount,
|
stats.skippedTaskCount,
|
||||||
System.currentTimeMillis() - startedAt,
|
System.currentTimeMillis() - startedAt,
|
||||||
Thread.currentThread().getName());
|
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() {
|
private void failStaleDeleteBrandTasks() {
|
||||||
LocalDateTime threshold = LocalDateTime.now().minusMinutes(deleteBrandProgressProperties.getHeartbeatTimeoutMinutes());
|
LocalDateTime threshold = LocalDateTime.now().minusMinutes(deleteBrandProgressProperties.getHeartbeatTimeoutMinutes());
|
||||||
|
|
||||||
// 先按 DB updatedAt 粗筛,避免全表扫描
|
// 鍏堟寜 DB updatedAt 绮楃瓫锛岄伩鍏嶅叏琛ㄦ壂鎻?
|
||||||
java.util.List<FileTaskEntity> runningTasks = fileTaskMapper.selectList(new LambdaQueryWrapper<FileTaskEntity>()
|
java.util.List<FileTaskEntity> runningTasks = fileTaskMapper.selectList(new LambdaQueryWrapper<FileTaskEntity>()
|
||||||
.eq(FileTaskEntity::getModuleType, MODULE_TYPE_DELETE_BRAND)
|
.eq(FileTaskEntity::getModuleType, MODULE_TYPE_DELETE_BRAND)
|
||||||
.eq(FileTaskEntity::getStatus, "RUNNING")
|
.eq(FileTaskEntity::getStatus, "RUNNING")
|
||||||
@@ -83,19 +96,19 @@ public class DeleteBrandStaleTaskService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
LocalDateTime lastActivityTime;
|
LocalDateTime lastActivityTime;
|
||||||
// 只有当 has_progress 为 true 且超过 15 分钟没心跳,才认为是异常卡死
|
// 鍙湁褰?has_progress 涓?true 涓旇秴杩?15 鍒嗛挓娌″績璺筹紝鎵嶈涓烘槸寮傚父鍗℃
|
||||||
if (lastHeartbeatAt > 0 && isActive) {
|
if (lastHeartbeatAt > 0 && isActive) {
|
||||||
// 有心跳,用最后心跳时间对比 15 分钟 (threshold 是 now - 15)
|
// 鏈夊績璺筹紝鐢ㄦ渶鍚庡績璺虫椂闂村姣?15 鍒嗛挓 (threshold 鏄?now - 15)
|
||||||
lastActivityTime = LocalDateTime.ofInstant(Instant.ofEpochMilli(lastHeartbeatAt), ZoneId.systemDefault());
|
lastActivityTime = LocalDateTime.ofInstant(Instant.ofEpochMilli(lastHeartbeatAt), ZoneId.systemDefault());
|
||||||
if (lastActivityTime.isAfter(threshold)) {
|
if (lastActivityTime.isAfter(threshold)) {
|
||||||
continue; // 没超时
|
continue; // 娌¤秴鏃?
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
// 没有心跳,或者当前处在 has_progress: false (等待用户点击下一个任务推送的静默期)
|
// 娌℃湁蹇冭烦锛屾垨鑰呭綋鍓嶅鍦?has_progress: false 锛堢瓑寰呯敤鎴风偣鍑讳笅涓€涓换鍔℃帹閫佺殑闈欓粯鏈燂級
|
||||||
// 静默期和排队期的宽限期一样,设为 12 个小时
|
// 闈欓粯鏈熷拰鎺掗槦鏈熺殑瀹介檺鏈熶竴鏍凤紝璁句负 12 涓皬鏃?
|
||||||
LocalDateTime queuedThreshold = LocalDateTime.now().minusHours(12);
|
LocalDateTime queuedThreshold = LocalDateTime.now().minusHours(12);
|
||||||
if (task.getCreatedAt() != null && task.getCreatedAt().isAfter(queuedThreshold)) {
|
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::getModuleType, MODULE_TYPE_DELETE_BRAND)
|
||||||
.eq(FileTaskEntity::getStatus, "RUNNING")
|
.eq(FileTaskEntity::getStatus, "RUNNING")
|
||||||
.set(FileTaskEntity::getStatus, "FAILED")
|
.set(FileTaskEntity::getStatus, "FAILED")
|
||||||
.set(FileTaskEntity::getErrorMessage, "结果回传长时间无响应,任务已自动失败")
|
.set(FileTaskEntity::getErrorMessage, "缁撴灉鍥炰紶闀挎椂闂存棤鍝嶅簲锛屼换鍔″凡鑷姩澶辫触")
|
||||||
.set(FileTaskEntity::getUpdatedAt, LocalDateTime.now())
|
.set(FileTaskEntity::getUpdatedAt, LocalDateTime.now())
|
||||||
.set(FileTaskEntity::getFinishedAt, LocalDateTime.now()));
|
.set(FileTaskEntity::getFinishedAt, LocalDateTime.now()));
|
||||||
|
|
||||||
@@ -157,7 +170,7 @@ public class DeleteBrandStaleTaskService {
|
|||||||
.eq(FileTaskEntity::getModuleType, MODULE_TYPE_PRODUCT_RISK)
|
.eq(FileTaskEntity::getModuleType, MODULE_TYPE_PRODUCT_RISK)
|
||||||
.eq(FileTaskEntity::getStatus, "RUNNING")
|
.eq(FileTaskEntity::getStatus, "RUNNING")
|
||||||
.set(FileTaskEntity::getStatus, "FAILED")
|
.set(FileTaskEntity::getStatus, "FAILED")
|
||||||
.set(FileTaskEntity::getErrorMessage, "长时间未收到 Python 结果,任务已自动失败")
|
.set(FileTaskEntity::getErrorMessage, "闀挎椂闂存湭鏀跺埌 Python 缁撴灉锛屼换鍔″凡鑷姩澶辫触")
|
||||||
.set(FileTaskEntity::getUpdatedAt, LocalDateTime.now())
|
.set(FileTaskEntity::getUpdatedAt, LocalDateTime.now())
|
||||||
.set(FileTaskEntity::getFinishedAt, LocalDateTime.now()));
|
.set(FileTaskEntity::getFinishedAt, LocalDateTime.now()));
|
||||||
if (updated > 0) {
|
if (updated > 0) {
|
||||||
@@ -170,6 +183,63 @@ public class DeleteBrandStaleTaskService {
|
|||||||
return stats;
|
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<FileTaskEntity> runningTasks = fileTaskMapper.selectList(new LambdaQueryWrapper<FileTaskEntity>()
|
||||||
|
.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<FileTaskEntity>()
|
||||||
|
.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 * * * *}")
|
@Scheduled(cron = "${aiimage.delete-brand-progress.finalize-check-cron:30 */2 * * * *}")
|
||||||
public void finalizeCompletedRunningTasks() {
|
public void finalizeCompletedRunningTasks() {
|
||||||
java.util.List<FileTaskEntity> runningTasks = fileTaskMapper.selectList(new LambdaQueryWrapper<FileTaskEntity>()
|
java.util.List<FileTaskEntity> runningTasks = fileTaskMapper.selectList(new LambdaQueryWrapper<FileTaskEntity>()
|
||||||
@@ -182,7 +252,7 @@ public class DeleteBrandStaleTaskService {
|
|||||||
try {
|
try {
|
||||||
deleteBrandRunService.tryFinalizeTask(task.getId(), true);
|
deleteBrandRunService.tryFinalizeTask(task.getId(), true);
|
||||||
} catch (Exception ignored) {
|
} catch (Exception ignored) {
|
||||||
// 定时补偿不影响主流程;下次调度或人工重试时再处理
|
// 瀹氭椂琛ュ伩涓嶅奖鍝嶄富娴佺▼锛涗笅娆¤皟搴︽垨浜哄伐閲嶈瘯鏃跺啀澶勭悊
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -193,4 +263,12 @@ public class DeleteBrandStaleTaskService {
|
|||||||
private int failedTaskCount;
|
private int failedTaskCount;
|
||||||
private int skippedTaskCount;
|
private int skippedTaskCount;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private static final class ShopMatchStaleCheckStats {
|
||||||
|
private int scannedTaskCount;
|
||||||
|
private int finalizedTaskCount;
|
||||||
|
private int failedTaskCount;
|
||||||
|
private int skippedTaskCount;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,40 +1,60 @@
|
|||||||
package com.nanri.aiimage.modules.productrisk.model.vo;
|
package com.nanri.aiimage.modules.productrisk.model.vo;
|
||||||
|
|
||||||
import com.fasterxml.jackson.annotation.JsonProperty;
|
import com.fasterxml.jackson.annotation.JsonProperty;
|
||||||
|
import com.nanri.aiimage.modules.shopmatch.model.vo.ShopMatchTaskStageVo;
|
||||||
import io.swagger.v3.oas.annotations.media.Schema;
|
import io.swagger.v3.oas.annotations.media.Schema;
|
||||||
import lombok.Data;
|
import lombok.Data;
|
||||||
|
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
@Data
|
@Data
|
||||||
@Schema(description = "商品风险任务概要(biz_file_task 核心字段)")
|
@Schema(description = "Task summary")
|
||||||
public class ProductRiskTaskItemVo {
|
public class ProductRiskTaskItemVo {
|
||||||
|
|
||||||
@Schema(description = "任务主键", example = "200")
|
@Schema(description = "Task primary key", example = "200")
|
||||||
private Long id;
|
private Long id;
|
||||||
|
|
||||||
@JsonProperty("taskNo")
|
@JsonProperty("taskNo")
|
||||||
@Schema(description = "业务单号,如 PRODUCT_RISK_RESOLVE-雪花 id")
|
@Schema(description = "Task number")
|
||||||
private String taskNo;
|
private String taskNo;
|
||||||
|
|
||||||
@Schema(description = "任务状态:RUNNING 执行中;SUCCESS 已结束且至少一店成功;FAILED 已结束且全部失败或整体失败")
|
@Schema(description = "Task status")
|
||||||
private String status;
|
private String status;
|
||||||
|
|
||||||
@JsonProperty("errorMessage")
|
@JsonProperty("errorMessage")
|
||||||
@Schema(description = "失败或部分失败时的汇总错误说明;成功且无附加信息时可能为空")
|
@Schema(description = "Task error message")
|
||||||
private String errorMessage;
|
private String errorMessage;
|
||||||
|
|
||||||
@JsonProperty("createdAt")
|
@JsonProperty("createdAt")
|
||||||
@Schema(description = "创建时间(字符串格式,服务端 LocalDateTime.toString())")
|
@Schema(description = "Created at")
|
||||||
private String createdAt;
|
private String createdAt;
|
||||||
|
|
||||||
@JsonProperty("updatedAt")
|
@JsonProperty("updatedAt")
|
||||||
@Schema(description = "最后更新时间")
|
@Schema(description = "Updated at")
|
||||||
private String updatedAt;
|
private String updatedAt;
|
||||||
|
|
||||||
@JsonProperty("finishedAt")
|
@JsonProperty("finishedAt")
|
||||||
@Schema(description = "结束时间;RUNNING 时通常为空")
|
@Schema(description = "Finished at")
|
||||||
private String finishedAt;
|
private String finishedAt;
|
||||||
|
|
||||||
@JsonProperty("scheduledAt")
|
@JsonProperty("scheduledAt")
|
||||||
@Schema(description = "定时执行时间,未设置时为空")
|
@Schema(description = "Next scheduled execution time")
|
||||||
private String scheduledAt;
|
private String scheduledAt;
|
||||||
|
|
||||||
|
@JsonProperty("countryCodes")
|
||||||
|
@Schema(description = "Country codes used by shop match task")
|
||||||
|
private List<String> countryCodes = new ArrayList<>();
|
||||||
|
|
||||||
|
@JsonProperty("scheduleStages")
|
||||||
|
@Schema(description = "Stage schedule of shop match task")
|
||||||
|
private List<ShopMatchTaskStageVo> 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;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,7 +3,6 @@ package com.nanri.aiimage.modules.shopmatch.controller;
|
|||||||
import com.nanri.aiimage.common.api.ApiResponse;
|
import com.nanri.aiimage.common.api.ApiResponse;
|
||||||
import com.nanri.aiimage.modules.productrisk.model.dto.ProductRiskCandidateAddRequest;
|
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.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.ProductRiskMatchShopsRequest;
|
||||||
import com.nanri.aiimage.modules.productrisk.model.dto.ProductRiskTaskBatchRequest;
|
import com.nanri.aiimage.modules.productrisk.model.dto.ProductRiskTaskBatchRequest;
|
||||||
import com.nanri.aiimage.modules.productrisk.model.vo.ProductRiskCandidateVo;
|
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.ProductRiskHistoryVo;
|
||||||
import com.nanri.aiimage.modules.productrisk.model.vo.ProductRiskMatchShopsVo;
|
import com.nanri.aiimage.modules.productrisk.model.vo.ProductRiskMatchShopsVo;
|
||||||
import com.nanri.aiimage.modules.productrisk.model.vo.ProductRiskTaskBatchVo;
|
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.model.dto.ShopMatchSubmitResultRequest;
|
||||||
import com.nanri.aiimage.modules.shopmatch.service.ShopMatchResolveService;
|
import com.nanri.aiimage.modules.shopmatch.service.ShopMatchResolveService;
|
||||||
import com.nanri.aiimage.modules.shopmatch.service.ShopMatchTaskService;
|
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 jakarta.validation.Valid;
|
||||||
import lombok.RequiredArgsConstructor;
|
import lombok.RequiredArgsConstructor;
|
||||||
import org.springframework.http.HttpHeaders;
|
import org.springframework.http.HttpHeaders;
|
||||||
@@ -40,95 +45,143 @@ import java.util.List;
|
|||||||
@RestController
|
@RestController
|
||||||
@RequiredArgsConstructor
|
@RequiredArgsConstructor
|
||||||
@RequestMapping("/api/shop-match")
|
@RequestMapping("/api/shop-match")
|
||||||
|
@Tag(name = "匹配店铺", description = "店铺候选、国家顺序、批量匹配、定时任务、Python 回传结果与下载接口。")
|
||||||
public class ShopMatchController {
|
public class ShopMatchController {
|
||||||
|
|
||||||
private final ShopMatchResolveService shopMatchResolveService;
|
private final ShopMatchResolveService shopMatchResolveService;
|
||||||
private final ShopMatchTaskService shopMatchTaskService;
|
private final ShopMatchTaskService shopMatchTaskService;
|
||||||
|
|
||||||
@GetMapping("/candidates")
|
@GetMapping("/candidates")
|
||||||
public ApiResponse<List<ProductRiskCandidateVo>> listCandidates(@RequestParam("user_id") Long userId) {
|
@Operation(summary = "查询候选店铺", description = "返回当前用户在匹配店铺模块下保存的候选店铺列表。")
|
||||||
|
public ApiResponse<List<ProductRiskCandidateVo>> 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));
|
return ApiResponse.success(shopMatchResolveService.listCandidates(userId));
|
||||||
}
|
}
|
||||||
|
|
||||||
@PostMapping("/candidates")
|
@PostMapping("/candidates")
|
||||||
|
@Operation(summary = "新增候选店铺", description = "新增一条候选店铺记录,供前端后续匹配与创建任务使用。")
|
||||||
public ApiResponse<ProductRiskCandidateVo> addCandidate(@Valid @RequestBody ProductRiskCandidateAddRequest request) {
|
public ApiResponse<ProductRiskCandidateVo> addCandidate(@Valid @RequestBody ProductRiskCandidateAddRequest request) {
|
||||||
return ApiResponse.success(shopMatchResolveService.addCandidate(request));
|
return ApiResponse.success(shopMatchResolveService.addCandidate(request));
|
||||||
}
|
}
|
||||||
|
|
||||||
@DeleteMapping("/candidates/{id}")
|
@DeleteMapping("/candidates/{id}")
|
||||||
public ApiResponse<Void> deleteCandidate(@PathVariable Long id, @RequestParam("user_id") Long userId) {
|
@Operation(summary = "删除候选店铺", description = "删除当前用户名下的一条候选店铺记录。")
|
||||||
|
public ApiResponse<Void> 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);
|
shopMatchResolveService.deleteCandidate(userId, id);
|
||||||
return ApiResponse.success(null);
|
return ApiResponse.success(null);
|
||||||
}
|
}
|
||||||
|
|
||||||
@GetMapping("/country-preference")
|
@GetMapping("/country-preference")
|
||||||
public ApiResponse<ProductRiskCountryPreferenceVo> getCountryPreference(@RequestParam("user_id") Long userId) {
|
@Operation(summary = "查询国家配置顺序", description = "返回当前用户保存的国家勾选与执行顺序。")
|
||||||
|
public ApiResponse<ProductRiskCountryPreferenceVo> 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));
|
return ApiResponse.success(shopMatchResolveService.getCountryPreference(userId));
|
||||||
}
|
}
|
||||||
|
|
||||||
@PutMapping("/country-preference")
|
@PutMapping("/country-preference")
|
||||||
|
@Operation(summary = "保存国家配置顺序", description = "保存当前用户在匹配店铺模块中的国家勾选与执行顺序。")
|
||||||
public ApiResponse<ProductRiskCountryPreferenceVo> saveCountryPreference(@Valid @RequestBody ProductRiskCountryPreferenceSaveRequest request) {
|
public ApiResponse<ProductRiskCountryPreferenceVo> saveCountryPreference(@Valid @RequestBody ProductRiskCountryPreferenceSaveRequest request) {
|
||||||
return ApiResponse.success(shopMatchResolveService.saveCountryPreference(request));
|
return ApiResponse.success(shopMatchResolveService.saveCountryPreference(request));
|
||||||
}
|
}
|
||||||
|
|
||||||
@PostMapping("/match-shops")
|
@PostMapping("/match-shops")
|
||||||
|
@Operation(summary = "批量匹配店铺", description = "按店铺名批量匹配紫鸟索引,返回店铺 ID、平台、公司与匹配状态。")
|
||||||
public ApiResponse<ProductRiskMatchShopsVo> matchShops(@Valid @RequestBody ProductRiskMatchShopsRequest request) {
|
public ApiResponse<ProductRiskMatchShopsVo> matchShops(@Valid @RequestBody ProductRiskMatchShopsRequest request) {
|
||||||
return ApiResponse.success(shopMatchResolveService.matchShops(request));
|
return ApiResponse.success(shopMatchResolveService.matchShops(request));
|
||||||
}
|
}
|
||||||
|
|
||||||
@GetMapping("/dashboard")
|
@GetMapping("/dashboard")
|
||||||
public ApiResponse<ProductRiskDashboardVo> dashboard(@RequestParam("user_id") Long userId) {
|
@Operation(summary = "统计看板", description = "返回候选店铺数、已结束任务数、成功数和失败数。")
|
||||||
|
public ApiResponse<ProductRiskDashboardVo> 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));
|
return ApiResponse.success(shopMatchTaskService.dashboard(userId));
|
||||||
}
|
}
|
||||||
|
|
||||||
@GetMapping("/history")
|
@GetMapping("/history")
|
||||||
public ApiResponse<ProductRiskHistoryVo> history(@RequestParam("user_id") Long userId) {
|
@Operation(summary = "查询任务记录", description = "返回当前用户的运行中与历史任务记录。")
|
||||||
|
public ApiResponse<ProductRiskHistoryVo> 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));
|
return ApiResponse.success(shopMatchTaskService.listHistory(userId));
|
||||||
}
|
}
|
||||||
|
|
||||||
@DeleteMapping("/history/{resultId}")
|
@DeleteMapping("/history/{resultId}")
|
||||||
public ApiResponse<Void> deleteHistory(@PathVariable Long resultId, @RequestParam("user_id") Long userId) {
|
@Operation(summary = "删除结果记录", description = "删除一条任务结果记录,并同步重算任务状态。")
|
||||||
|
public ApiResponse<Void> 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);
|
shopMatchTaskService.deleteHistory(resultId, userId);
|
||||||
return ApiResponse.success(null);
|
return ApiResponse.success(null);
|
||||||
}
|
}
|
||||||
|
|
||||||
@PostMapping("/tasks")
|
@PostMapping("/tasks")
|
||||||
public ApiResponse<ProductRiskCreateTaskVo> createTask(@Valid @RequestBody ProductRiskCreateTaskRequest request) {
|
@Operation(summary = "创建匹配店铺任务", description = "创建任务与各店铺占位结果,支持立即执行或多时间点定时执行。")
|
||||||
|
public ApiResponse<ProductRiskCreateTaskVo> createTask(@Valid @RequestBody ShopMatchCreateTaskRequest request) {
|
||||||
return ApiResponse.success(shopMatchTaskService.createTask(request));
|
return ApiResponse.success(shopMatchTaskService.createTask(request));
|
||||||
}
|
}
|
||||||
|
|
||||||
@DeleteMapping("/tasks/{taskId}")
|
@DeleteMapping("/tasks/{taskId}")
|
||||||
public ApiResponse<Void> deleteTask(@PathVariable Long taskId, @RequestParam("user_id") Long userId) {
|
@Operation(summary = "删除任务", description = "删除整条任务及其下所有结果记录。")
|
||||||
|
public ApiResponse<Void> 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);
|
shopMatchTaskService.deleteTask(taskId, userId);
|
||||||
return ApiResponse.success(null);
|
return ApiResponse.success(null);
|
||||||
}
|
}
|
||||||
|
|
||||||
@PostMapping("/tasks/{taskId}/activate")
|
@PostMapping("/tasks/{taskId}/activate")
|
||||||
public ApiResponse<Void> activateTask(@PathVariable Long taskId, @RequestParam("user_id") Long userId) {
|
@Operation(summary = "激活定时阶段", description = "在某个定时阶段开始执行前,将任务切换到运行中状态。")
|
||||||
shopMatchTaskService.activateTask(taskId, userId);
|
public ApiResponse<Void> 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<Void> 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);
|
return ApiResponse.success(null);
|
||||||
}
|
}
|
||||||
|
|
||||||
@PostMapping("/tasks/batch")
|
@PostMapping("/tasks/batch")
|
||||||
|
@Operation(summary = "批量查询任务详情", description = "供前端轮询任务状态与阶段进度使用。")
|
||||||
public ApiResponse<ProductRiskTaskBatchVo> tasksBatch(@Valid @RequestBody ProductRiskTaskBatchRequest request) {
|
public ApiResponse<ProductRiskTaskBatchVo> tasksBatch(@Valid @RequestBody ProductRiskTaskBatchRequest request) {
|
||||||
return ApiResponse.success(shopMatchTaskService.getTaskDetailsBatch(request.getTaskIds()));
|
return ApiResponse.success(shopMatchTaskService.getTaskDetailsBatch(request.getTaskIds()));
|
||||||
}
|
}
|
||||||
|
|
||||||
@PostMapping("/tasks/{taskId}/result")
|
@PostMapping("/tasks/{taskId}/result")
|
||||||
public ApiResponse<Void> submitResult(@PathVariable Long taskId, @Valid @RequestBody ShopMatchSubmitResultRequest request) {
|
@Operation(summary = "提交匹配结果", description = "Python 可多次分批回传结果;后端会组装文件并在超时场景自动补偿收尾。")
|
||||||
|
public ApiResponse<Void> submitResult(
|
||||||
|
@Parameter(description = "任务主键", example = "200") @PathVariable Long taskId,
|
||||||
|
@Valid @RequestBody ShopMatchSubmitResultRequest request) {
|
||||||
shopMatchTaskService.submitResult(taskId, request);
|
shopMatchTaskService.submitResult(taskId, request);
|
||||||
return ApiResponse.success(null);
|
return ApiResponse.success(null);
|
||||||
}
|
}
|
||||||
|
|
||||||
@GetMapping("/results/{resultId}/download")
|
@GetMapping("/results/{resultId}/download")
|
||||||
|
@Operation(summary = "下载结果文件", description = "按结果记录下载后端已生成的 Excel 文件。")
|
||||||
public void downloadResult(
|
public void downloadResult(
|
||||||
@PathVariable Long resultId,
|
@Parameter(description = "结果记录主键", example = "1001") @PathVariable Long resultId,
|
||||||
@RequestParam("user_id") Long userId,
|
@Parameter(name = "user_id", description = "当前用户 ID", required = true, in = ParameterIn.QUERY, example = "1") @RequestParam("user_id") Long userId,
|
||||||
jakarta.servlet.http.HttpServletResponse response) {
|
jakarta.servlet.http.HttpServletResponse response) {
|
||||||
String url = shopMatchTaskService.resolveResultDownloadUrl(resultId, userId);
|
String url = shopMatchTaskService.resolveResultDownloadUrl(resultId, userId);
|
||||||
String filename = shopMatchTaskService.resolveResultDownloadFilename(resultId, userId);
|
String filename = shopMatchTaskService.resolveResultDownloadFilename(resultId, userId);
|
||||||
if (url == null || url.isBlank()) {
|
if (url == null || url.isBlank()) {
|
||||||
throw new ResponseStatusException(HttpStatus.NOT_FOUND, "暂无可下载结果");
|
throw new ResponseStatusException(HttpStatus.NOT_FOUND, "No downloadable result");
|
||||||
}
|
}
|
||||||
try {
|
try {
|
||||||
String encodedFilename = URLEncoder.encode(filename, StandardCharsets.UTF_8).replace("+", "%20");
|
String encodedFilename = URLEncoder.encode(filename, StandardCharsets.UTF_8).replace("+", "%20");
|
||||||
@@ -144,7 +197,7 @@ public class ShopMatchController {
|
|||||||
response.getOutputStream().flush();
|
response.getOutputStream().flush();
|
||||||
}
|
}
|
||||||
} catch (Exception ex) {
|
} catch (Exception ex) {
|
||||||
throw new ResponseStatusException(HttpStatus.INTERNAL_SERVER_ERROR, "下载失败");
|
throw new ResponseStatusException(HttpStatus.INTERNAL_SERVER_ERROR, "Download failed");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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<ProductRiskShopQueueItemVo> items = new ArrayList<>();
|
||||||
|
|
||||||
|
@NotEmpty(message = "country_codes cannot be empty")
|
||||||
|
@JsonProperty("country_codes")
|
||||||
|
@Schema(description = "每个阶段都要执行的国家代码列表", requiredMode = Schema.RequiredMode.REQUIRED)
|
||||||
|
private List<String> countryCodes = new ArrayList<>();
|
||||||
|
|
||||||
|
@JsonProperty("schedule_times")
|
||||||
|
@Schema(description = "可选的多时间点执行时间;为空表示立即执行", example = "[\"2026-04-10T18:00:00\",\"2026-04-10T19:00:00\"]")
|
||||||
|
private List<LocalDateTime> scheduleTimes = new ArrayList<>();
|
||||||
|
|
||||||
|
@JsonProperty("current_stage_index")
|
||||||
|
@Schema(description = "下一次待执行的阶段下标,由后端维护")
|
||||||
|
private Integer currentStageIndex;
|
||||||
|
|
||||||
|
@JsonProperty("active_stage_index")
|
||||||
|
@Schema(description = "当前运行中的阶段下标,由后端维护")
|
||||||
|
private Integer activeStageIndex;
|
||||||
|
}
|
||||||
|
|
||||||
@@ -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;
|
||||||
|
}
|
||||||
|
|
||||||
@@ -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;
|
||||||
|
}
|
||||||
|
|
||||||
@@ -6,7 +6,7 @@ import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
|
|||||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||||
import com.nanri.aiimage.common.exception.BusinessException;
|
import com.nanri.aiimage.common.exception.BusinessException;
|
||||||
import com.nanri.aiimage.modules.file.service.oss.OssStorageService;
|
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.ProductRiskCreateTaskVo;
|
||||||
import com.nanri.aiimage.modules.productrisk.model.vo.ProductRiskDashboardVo;
|
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.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.ProductRiskTaskBatchVo;
|
||||||
import com.nanri.aiimage.modules.productrisk.model.vo.ProductRiskTaskDetailVo;
|
import com.nanri.aiimage.modules.productrisk.model.vo.ProductRiskTaskDetailVo;
|
||||||
import com.nanri.aiimage.modules.productrisk.model.vo.ProductRiskTaskItemVo;
|
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.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.ShopMatchRowDto;
|
||||||
import com.nanri.aiimage.modules.shopmatch.model.dto.ShopMatchShopPayloadDto;
|
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.dto.ShopMatchSubmitResultRequest;
|
||||||
import com.nanri.aiimage.modules.shopmatch.model.entity.ShopMatchShopCandidateEntity;
|
import com.nanri.aiimage.modules.shopmatch.model.entity.ShopMatchShopCandidateEntity;
|
||||||
import com.nanri.aiimage.modules.task.mapper.FileResultMapper;
|
import com.nanri.aiimage.modules.task.mapper.FileResultMapper;
|
||||||
@@ -34,6 +37,7 @@ import java.io.File;
|
|||||||
import java.time.LocalDateTime;
|
import java.time.LocalDateTime;
|
||||||
import java.util.ArrayList;
|
import java.util.ArrayList;
|
||||||
import java.util.LinkedHashMap;
|
import java.util.LinkedHashMap;
|
||||||
|
import java.util.LinkedHashSet;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
import java.util.Locale;
|
import java.util.Locale;
|
||||||
import java.util.Map;
|
import java.util.Map;
|
||||||
@@ -57,7 +61,7 @@ public class ShopMatchTaskService {
|
|||||||
|
|
||||||
public ProductRiskDashboardVo dashboard(Long userId) {
|
public ProductRiskDashboardVo dashboard(Long userId) {
|
||||||
if (userId == null || userId <= 0) {
|
if (userId == null || userId <= 0) {
|
||||||
throw new BusinessException("user_id 不合法");
|
throw new BusinessException("user_id is invalid");
|
||||||
}
|
}
|
||||||
ProductRiskDashboardVo vo = new ProductRiskDashboardVo();
|
ProductRiskDashboardVo vo = new ProductRiskDashboardVo();
|
||||||
vo.setCandidateCount(candidateMapper.selectCount(new LambdaQueryWrapper<ShopMatchShopCandidateEntity>()
|
vo.setCandidateCount(candidateMapper.selectCount(new LambdaQueryWrapper<ShopMatchShopCandidateEntity>()
|
||||||
@@ -82,7 +86,7 @@ public class ShopMatchTaskService {
|
|||||||
|
|
||||||
public ProductRiskHistoryVo listHistory(Long userId) {
|
public ProductRiskHistoryVo listHistory(Long userId) {
|
||||||
if (userId == null || userId <= 0) {
|
if (userId == null || userId <= 0) {
|
||||||
throw new BusinessException("user_id 不合法");
|
throw new BusinessException("user_id is invalid");
|
||||||
}
|
}
|
||||||
ProductRiskHistoryVo vo = new ProductRiskHistoryVo();
|
ProductRiskHistoryVo vo = new ProductRiskHistoryVo();
|
||||||
List<FileResultEntity> entities = fileResultMapper.selectList(new LambdaQueryWrapper<FileResultEntity>()
|
List<FileResultEntity> entities = fileResultMapper.selectList(new LambdaQueryWrapper<FileResultEntity>()
|
||||||
@@ -114,11 +118,11 @@ public class ShopMatchTaskService {
|
|||||||
@Transactional
|
@Transactional
|
||||||
public void deleteHistory(Long resultId, Long userId) {
|
public void deleteHistory(Long resultId, Long userId) {
|
||||||
if (userId == null || userId <= 0) {
|
if (userId == null || userId <= 0) {
|
||||||
throw new BusinessException("user_id 不合法");
|
throw new BusinessException("user_id is invalid");
|
||||||
}
|
}
|
||||||
FileResultEntity entity = fileResultMapper.selectById(resultId);
|
FileResultEntity entity = fileResultMapper.selectById(resultId);
|
||||||
if (entity == null || !MODULE_TYPE.equals(entity.getModuleType()) || !userId.equals(entity.getUserId())) {
|
if (entity == null || !MODULE_TYPE.equals(entity.getModuleType()) || !userId.equals(entity.getUserId())) {
|
||||||
throw new BusinessException("记录不存在");
|
throw new BusinessException("Invalid request");
|
||||||
}
|
}
|
||||||
Long taskId = entity.getTaskId();
|
Long taskId = entity.getTaskId();
|
||||||
fileResultMapper.deleteById(resultId);
|
fileResultMapper.deleteById(resultId);
|
||||||
@@ -130,11 +134,11 @@ public class ShopMatchTaskService {
|
|||||||
@Transactional
|
@Transactional
|
||||||
public void deleteTask(Long taskId, Long userId) {
|
public void deleteTask(Long taskId, Long userId) {
|
||||||
if (userId == null || userId <= 0) {
|
if (userId == null || userId <= 0) {
|
||||||
throw new BusinessException("user_id 不合法");
|
throw new BusinessException("user_id is invalid");
|
||||||
}
|
}
|
||||||
FileTaskEntity task = fileTaskMapper.selectById(taskId);
|
FileTaskEntity task = fileTaskMapper.selectById(taskId);
|
||||||
if (task == null || !MODULE_TYPE.equals(task.getModuleType()) || !userId.equals(task.getUserId())) {
|
if (task == null || !MODULE_TYPE.equals(task.getModuleType()) || !userId.equals(task.getUserId())) {
|
||||||
throw new BusinessException("任务不存在");
|
throw new BusinessException("Invalid request");
|
||||||
}
|
}
|
||||||
fileResultMapper.delete(new LambdaQueryWrapper<FileResultEntity>()
|
fileResultMapper.delete(new LambdaQueryWrapper<FileResultEntity>()
|
||||||
.eq(FileResultEntity::getTaskId, taskId)
|
.eq(FileResultEntity::getTaskId, taskId)
|
||||||
@@ -182,10 +186,10 @@ public class ShopMatchTaskService {
|
|||||||
public String resolveResultDownloadUrl(Long resultId, Long userId) {
|
public String resolveResultDownloadUrl(Long resultId, Long userId) {
|
||||||
FileResultEntity entity = fileResultMapper.selectById(resultId);
|
FileResultEntity entity = fileResultMapper.selectById(resultId);
|
||||||
if (entity == null || !MODULE_TYPE.equals(entity.getModuleType()) || !userId.equals(entity.getUserId())) {
|
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()) {
|
if (entity.getResultFileUrl() == null || entity.getResultFileUrl().isBlank()) {
|
||||||
throw new BusinessException("暂无可下载文件");
|
throw new BusinessException("Invalid request");
|
||||||
}
|
}
|
||||||
return ossStorageService.generateFreshDownloadUrl(entity.getResultFileUrl());
|
return ossStorageService.generateFreshDownloadUrl(entity.getResultFileUrl());
|
||||||
}
|
}
|
||||||
@@ -193,7 +197,7 @@ public class ShopMatchTaskService {
|
|||||||
public String resolveResultDownloadFilename(Long resultId, Long userId) {
|
public String resolveResultDownloadFilename(Long resultId, Long userId) {
|
||||||
FileResultEntity entity = fileResultMapper.selectById(resultId);
|
FileResultEntity entity = fileResultMapper.selectById(resultId);
|
||||||
if (entity == null || !MODULE_TYPE.equals(entity.getModuleType()) || !userId.equals(entity.getUserId())) {
|
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()
|
return entity.getResultFilename() != null && !entity.getResultFilename().isBlank()
|
||||||
? entity.getResultFilename()
|
? entity.getResultFilename()
|
||||||
@@ -201,34 +205,41 @@ public class ShopMatchTaskService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Transactional
|
@Transactional
|
||||||
public ProductRiskCreateTaskVo createTask(ProductRiskCreateTaskRequest request) {
|
public ProductRiskCreateTaskVo createTask(ShopMatchCreateTaskRequest request) {
|
||||||
if (request.getUserId() == null || request.getUserId() <= 0) {
|
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()) {
|
if (request.getItems() == null || request.getItems().isEmpty()) {
|
||||||
throw new BusinessException("items 不能为空");
|
throw new BusinessException("items 涓嶈兘涓虹┖");
|
||||||
}
|
}
|
||||||
List<ProductRiskShopQueueItemVo> uniqueItems = dedupeQueueItems(request.getItems());
|
List<ProductRiskShopQueueItemVo> uniqueItems = dedupeQueueItems(request.getItems());
|
||||||
if (uniqueItems.isEmpty()) {
|
if (uniqueItems.isEmpty()) {
|
||||||
throw new BusinessException("items 不能为空");
|
throw new BusinessException("items 涓嶈兘涓虹┖");
|
||||||
}
|
}
|
||||||
for (ProductRiskShopQueueItemVo item : uniqueItems) {
|
for (ProductRiskShopQueueItemVo item : uniqueItems) {
|
||||||
if (item == null || !item.isMatched()) {
|
if (item == null || !item.isMatched()) {
|
||||||
throw new BusinessException("存在未匹配店铺,无法创建任务");
|
throw new BusinessException("瀛樺湪鏈尮閰嶅簵閾猴紝鏃犳硶鍒涘缓浠诲姟");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
List<String> countryCodes = normalizeCountryCodes(request.getCountryCodes());
|
||||||
|
List<LocalDateTime> scheduleTimes = normalizeScheduleTimes(request.getScheduleTimes());
|
||||||
LocalDateTime now = LocalDateTime.now();
|
LocalDateTime now = LocalDateTime.now();
|
||||||
LocalDateTime scheduledAt = request.getScheduleTime();
|
|
||||||
if (scheduledAt != null && !scheduledAt.isAfter(now)) {
|
ShopMatchCreateTaskRequest persistedRequest = new ShopMatchCreateTaskRequest();
|
||||||
throw new BusinessException("schedule_time 必须晚于当前时间");
|
persistedRequest.setUserId(request.getUserId());
|
||||||
|
persistedRequest.setItems(uniqueItems);
|
||||||
|
persistedRequest.setCountryCodes(countryCodes);
|
||||||
|
persistedRequest.setScheduleTimes(scheduleTimes);
|
||||||
|
if (!scheduleTimes.isEmpty()) {
|
||||||
|
persistedRequest.setCurrentStageIndex(0);
|
||||||
}
|
}
|
||||||
|
|
||||||
FileTaskEntity task = new FileTaskEntity();
|
FileTaskEntity task = new FileTaskEntity();
|
||||||
task.setTaskNo(MODULE_TYPE + "-" + IdUtil.getSnowflakeNextIdStr());
|
task.setTaskNo(MODULE_TYPE + "-" + IdUtil.getSnowflakeNextIdStr());
|
||||||
task.setModuleType(MODULE_TYPE);
|
task.setModuleType(MODULE_TYPE);
|
||||||
task.setTaskMode("PYTHON_QUEUE");
|
task.setTaskMode("PYTHON_QUEUE");
|
||||||
task.setStatus(scheduledAt != null ? "SCHEDULED" : "RUNNING");
|
task.setStatus(scheduleTimes.isEmpty() ? "RUNNING" : "SCHEDULED");
|
||||||
task.setSourceFileCount(uniqueItems.size());
|
task.setSourceFileCount(uniqueItems.size());
|
||||||
task.setSuccessFileCount(0);
|
task.setSuccessFileCount(0);
|
||||||
task.setFailedFileCount(0);
|
task.setFailedFileCount(0);
|
||||||
@@ -236,14 +247,14 @@ public class ShopMatchTaskService {
|
|||||||
task.setUserId(request.getUserId());
|
task.setUserId(request.getUserId());
|
||||||
task.setCreatedAt(now);
|
task.setCreatedAt(now);
|
||||||
task.setUpdatedAt(now);
|
task.setUpdatedAt(now);
|
||||||
task.setScheduledAt(scheduledAt);
|
task.setScheduledAt(scheduleTimes.isEmpty() ? null : scheduleTimes.get(0));
|
||||||
fileTaskMapper.insert(task);
|
fileTaskMapper.insert(task);
|
||||||
|
|
||||||
List<ProductRiskResultItemVo> snapshot = new ArrayList<>();
|
List<ProductRiskResultItemVo> snapshot = new ArrayList<>();
|
||||||
for (ProductRiskShopQueueItemVo item : uniqueItems) {
|
for (ProductRiskShopQueueItemVo item : uniqueItems) {
|
||||||
String normalized = ziniaoShopSwitchService.normalizeShopName(item.getShopName());
|
String normalized = ziniaoShopSwitchService.normalizeShopName(item.getShopName());
|
||||||
if (normalized.isBlank()) {
|
if (normalized.isBlank()) {
|
||||||
throw new BusinessException("店铺名无效");
|
throw new BusinessException("Invalid request");
|
||||||
}
|
}
|
||||||
FileResultEntity result = new FileResultEntity();
|
FileResultEntity result = new FileResultEntity();
|
||||||
result.setTaskId(task.getId());
|
result.setTaskId(task.getId());
|
||||||
@@ -257,10 +268,10 @@ public class ShopMatchTaskService {
|
|||||||
snapshot.add(toSnapshotVo(result, item, task.getStatus()));
|
snapshot.add(toSnapshotVo(result, item, task.getStatus()));
|
||||||
}
|
}
|
||||||
try {
|
try {
|
||||||
task.setRequestJson(objectMapper.writeValueAsString(request));
|
task.setRequestJson(objectMapper.writeValueAsString(persistedRequest));
|
||||||
task.setResultJson(objectMapper.writeValueAsString(snapshot));
|
task.setResultJson(objectMapper.writeValueAsString(snapshot));
|
||||||
} catch (Exception ex) {
|
} catch (Exception ex) {
|
||||||
throw new BusinessException("序列化任务数据失败");
|
throw new BusinessException("Invalid request");
|
||||||
}
|
}
|
||||||
fileTaskMapper.updateById(task);
|
fileTaskMapper.updateById(task);
|
||||||
|
|
||||||
@@ -271,49 +282,96 @@ public class ShopMatchTaskService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Transactional
|
@Transactional
|
||||||
public void activateTask(Long taskId, Long userId) {
|
public void activateTask(Long taskId, Long userId, Integer stageIndex) {
|
||||||
if (taskId == null || taskId <= 0) {
|
if (taskId == null || taskId <= 0) {
|
||||||
throw new BusinessException("taskId 不合法");
|
throw new BusinessException("taskId is invalid");
|
||||||
}
|
}
|
||||||
if (userId == null || userId <= 0) {
|
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);
|
FileTaskEntity task = fileTaskMapper.selectById(taskId);
|
||||||
if (task == null || !MODULE_TYPE.equals(task.getModuleType()) || !userId.equals(task.getUserId())) {
|
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())) {
|
if ("SUCCESS".equals(task.getStatus()) || "FAILED".equals(task.getStatus())) {
|
||||||
throw new BusinessException("任务已结束");
|
throw new BusinessException("Invalid request");
|
||||||
}
|
|
||||||
if ("RUNNING".equals(task.getStatus())) {
|
|
||||||
return;
|
|
||||||
}
|
}
|
||||||
if (!"SCHEDULED".equals(task.getStatus())) {
|
if (!"SCHEDULED".equals(task.getStatus())) {
|
||||||
throw new BusinessException("当前任务状态不允许激活");
|
throw new BusinessException("Invalid request");
|
||||||
}
|
}
|
||||||
if (task.getScheduledAt() != null && LocalDateTime.now().isBefore(task.getScheduledAt())) {
|
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.setStatus("RUNNING");
|
||||||
task.setUpdatedAt(LocalDateTime.now());
|
task.setUpdatedAt(LocalDateTime.now());
|
||||||
|
persistTaskRequest(task, state);
|
||||||
fileTaskMapper.updateById(task);
|
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
|
@Transactional
|
||||||
public void submitResult(Long taskId, ShopMatchSubmitResultRequest request) {
|
public void submitResult(Long taskId, ShopMatchSubmitResultRequest request) {
|
||||||
if (taskId == null || taskId <= 0) {
|
if (taskId == null || taskId <= 0) {
|
||||||
throw new BusinessException("taskId 不合法");
|
throw new BusinessException("taskId is invalid");
|
||||||
}
|
}
|
||||||
if (request == null || request.getShops() == null || request.getShops().isEmpty()) {
|
if (request == null || request.getShops() == null || request.getShops().isEmpty()) {
|
||||||
throw new BusinessException("shops 不能为空");
|
throw new BusinessException("shops 涓嶈兘涓虹┖");
|
||||||
}
|
}
|
||||||
|
|
||||||
FileTaskEntity task = fileTaskMapper.selectById(taskId);
|
FileTaskEntity task = fileTaskMapper.selectById(taskId);
|
||||||
if (task == null || !MODULE_TYPE.equals(task.getModuleType())) {
|
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())) {
|
if ("SUCCESS".equals(task.getStatus()) || "FAILED".equals(task.getStatus())) {
|
||||||
throw new BusinessException("任务已结束,拒绝重复提交");
|
throw new BusinessException("浠诲姟宸茬粨鏉燂紝鎷掔粷閲嶅鎻愪氦");
|
||||||
}
|
}
|
||||||
|
|
||||||
List<FileResultEntity> resultRows = fileResultMapper.selectList(new LambdaQueryWrapper<FileResultEntity>()
|
List<FileResultEntity> resultRows = fileResultMapper.selectList(new LambdaQueryWrapper<FileResultEntity>()
|
||||||
@@ -350,7 +408,7 @@ public class ShopMatchTaskService {
|
|||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
if (countPayloadRows(merged) <= 0) {
|
if (countPayloadRows(merged) <= 0) {
|
||||||
markResultFailed(result, "未收到可组装的结果数据");
|
markResultFailed(result, "No result rows received for assembly");
|
||||||
shopMatchTaskCacheService.removeShopMergedPayload(taskId, shopKey);
|
shopMatchTaskCacheService.removeShopMergedPayload(taskId, shopKey);
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
@@ -373,6 +431,102 @@ public class ShopMatchTaskService {
|
|||||||
updateTaskStatusFromLatestRows(task, latest);
|
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<FileResultEntity> resultRows = fileResultMapper.selectList(new LambdaQueryWrapper<FileResultEntity>()
|
||||||
|
.eq(FileResultEntity::getTaskId, taskId)
|
||||||
|
.eq(FileResultEntity::getModuleType, MODULE_TYPE)
|
||||||
|
.orderByAsc(FileResultEntity::getId));
|
||||||
|
if (resultRows.isEmpty()) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
Map<String, ShopMatchShopPayloadDto> cachedPayloadByShop = shopMatchTaskCacheService.getAllShopMergedPayload(taskId);
|
||||||
|
File workRoot = FileUtil.mkdir(FileUtil.file(System.getProperty("java.io.tmpdir"), "shop-match-result", String.valueOf(taskId)));
|
||||||
|
List<String> 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<FileResultEntity> latest = fileResultMapper.selectList(new LambdaQueryWrapper<FileResultEntity>()
|
||||||
|
.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) {
|
private void assembleShopResult(FileResultEntity result, String shopKey, ShopMatchShopPayloadDto payload, File workRoot) {
|
||||||
Map<String, List<ShopMatchRowDto>> countries = excelAssemblyService.normalizeCountriesMap(payload.getCountries());
|
Map<String, List<ShopMatchRowDto>> countries = excelAssemblyService.normalizeCountriesMap(payload.getCountries());
|
||||||
String displayName = payload.getShopName() != null && !payload.getShopName().isBlank() ? payload.getShopName().trim() : shopKey;
|
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<FileResultEntity> latest) {
|
private void updateTaskStatusFromLatestRows(FileTaskEntity task, List<FileResultEntity> latest) {
|
||||||
|
updateTaskStatusFromLatestRows(task, latest, List.of());
|
||||||
|
}
|
||||||
|
|
||||||
|
private void updateTaskStatusFromLatestRows(FileTaskEntity task,
|
||||||
|
List<FileResultEntity> latest,
|
||||||
|
List<String> batchErrors) {
|
||||||
int ok = 0;
|
int ok = 0;
|
||||||
int fail = 0;
|
int fail = 0;
|
||||||
List<String> errors = new ArrayList<>();
|
List<String> errors = new ArrayList<>();
|
||||||
@@ -435,7 +595,7 @@ public class ShopMatchTaskService {
|
|||||||
task.setFinishedAt(LocalDateTime.now());
|
task.setFinishedAt(LocalDateTime.now());
|
||||||
} else if (ok > 0) {
|
} else if (ok > 0) {
|
||||||
task.setStatus("SUCCESS");
|
task.setStatus("SUCCESS");
|
||||||
task.setErrorMessage(String.join("; ", errors));
|
task.setErrorMessage(String.join("; ", errors.isEmpty() ? batchErrors : errors));
|
||||||
task.setFinishedAt(LocalDateTime.now());
|
task.setFinishedAt(LocalDateTime.now());
|
||||||
} else {
|
} else {
|
||||||
task.setStatus("FAILED");
|
task.setStatus("FAILED");
|
||||||
@@ -473,7 +633,7 @@ public class ShopMatchTaskService {
|
|||||||
.orderByAsc(FileResultEntity::getId));
|
.orderByAsc(FileResultEntity::getId));
|
||||||
List<ProductRiskResultItemVo> items = new ArrayList<>();
|
List<ProductRiskResultItemVo> items = new ArrayList<>();
|
||||||
for (FileResultEntity row : rows) {
|
for (FileResultEntity row : rows) {
|
||||||
items.add(toHistoryItemVo(row, taskStatus));
|
throw new BusinessException("Task request payload is missing");
|
||||||
}
|
}
|
||||||
return items;
|
return items;
|
||||||
}
|
}
|
||||||
@@ -488,10 +648,20 @@ public class ShopMatchTaskService {
|
|||||||
vo.setUpdatedAt(fmt(task.getUpdatedAt()));
|
vo.setUpdatedAt(fmt(task.getUpdatedAt()));
|
||||||
vo.setFinishedAt(fmt(task.getFinishedAt()));
|
vo.setFinishedAt(fmt(task.getFinishedAt()));
|
||||||
vo.setScheduledAt(fmt(task.getScheduledAt()));
|
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;
|
return vo;
|
||||||
}
|
}
|
||||||
|
|
||||||
private ProductRiskResultItemVo toSnapshotVo(FileResultEntity result, ProductRiskShopQueueItemVo item, String taskStatus) {
|
private ProductRiskResultItemVo toSnapshotVo(FileResultEntity result,
|
||||||
|
ProductRiskShopQueueItemVo item,
|
||||||
|
String taskStatus) {
|
||||||
ProductRiskResultItemVo vo = new ProductRiskResultItemVo();
|
ProductRiskResultItemVo vo = new ProductRiskResultItemVo();
|
||||||
vo.setResultId(result.getId());
|
vo.setResultId(result.getId());
|
||||||
vo.setTaskId(result.getTaskId());
|
vo.setTaskId(result.getTaskId());
|
||||||
@@ -503,8 +673,10 @@ public class ShopMatchTaskService {
|
|||||||
vo.setMatchStatus(item.getMatchStatus());
|
vo.setMatchStatus(item.getMatchStatus());
|
||||||
vo.setMatchMessage(item.getMatchMessage());
|
vo.setMatchMessage(item.getMatchMessage());
|
||||||
vo.setTaskStatus(taskStatus);
|
vo.setTaskStatus(taskStatus);
|
||||||
vo.setScheduledAt(fmt(result.getTaskId() == null ? null : loadScheduledAt(result.getTaskId())));
|
|
||||||
vo.setSuccess(false);
|
vo.setSuccess(false);
|
||||||
|
vo.setError(null);
|
||||||
|
vo.setOutputFilename(null);
|
||||||
|
vo.setDownloadUrl(null);
|
||||||
return vo;
|
return vo;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -515,22 +687,66 @@ public class ShopMatchTaskService {
|
|||||||
vo.setShopName(entity.getSourceFilename());
|
vo.setShopName(entity.getSourceFilename());
|
||||||
vo.setShopId(entity.getSourceFileUrl());
|
vo.setShopId(entity.getSourceFileUrl());
|
||||||
vo.setTaskStatus(taskStatus);
|
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.setError(entity.getErrorMessage());
|
||||||
vo.setOutputFilename(entity.getResultFilename());
|
vo.setOutputFilename(entity.getResultFilename());
|
||||||
vo.setDownloadUrl(entity.getResultFileUrl() == null || entity.getResultFileUrl().isBlank()
|
vo.setDownloadUrl(entity.getResultFileUrl() == null || entity.getResultFileUrl().isBlank()
|
||||||
? null
|
? null
|
||||||
: ossStorageService.generateFreshDownloadUrl(entity.getResultFileUrl()));
|
: ossStorageService.generateFreshDownloadUrl(entity.getResultFileUrl()));
|
||||||
vo.setMatched(true);
|
vo.setMatched(true);
|
||||||
if (entity.getTaskId() != null) {
|
|
||||||
vo.setScheduledAt(fmt(loadScheduledAt(entity.getTaskId())));
|
|
||||||
}
|
|
||||||
if (entity.getTaskId() != null) {
|
if (entity.getTaskId() != null) {
|
||||||
mergeQueueFieldsFromRequest(vo, entity.getTaskId());
|
mergeQueueFieldsFromRequest(vo, entity.getTaskId());
|
||||||
}
|
}
|
||||||
return vo;
|
return vo;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private List<ShopMatchTaskStageVo> buildStageVos(FileTaskEntity task, ShopMatchCreateTaskRequest request) {
|
||||||
|
List<ShopMatchTaskStageVo> stages = new ArrayList<>();
|
||||||
|
List<LocalDateTime> 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) {
|
private LocalDateTime loadScheduledAt(Long taskId) {
|
||||||
FileTaskEntity task = fileTaskMapper.selectById(taskId);
|
FileTaskEntity task = fileTaskMapper.selectById(taskId);
|
||||||
return task == null ? null : task.getScheduledAt();
|
return task == null ? null : task.getScheduledAt();
|
||||||
@@ -538,27 +754,60 @@ public class ShopMatchTaskService {
|
|||||||
|
|
||||||
private void mergeQueueFieldsFromRequest(ProductRiskResultItemVo vo, Long taskId) {
|
private void mergeQueueFieldsFromRequest(ProductRiskResultItemVo vo, Long taskId) {
|
||||||
FileTaskEntity task = fileTaskMapper.selectById(taskId);
|
FileTaskEntity task = fileTaskMapper.selectById(taskId);
|
||||||
if (task == null || task.getRequestJson() == null || task.getRequestJson().isBlank()) {
|
ShopMatchCreateTaskRequest request = parseTaskRequestSilently(task);
|
||||||
|
if (request == null) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
try {
|
String key = ziniaoShopSwitchService.normalizeShopName(vo.getShopName());
|
||||||
ProductRiskCreateTaskRequest request = objectMapper.readValue(task.getRequestJson(), ProductRiskCreateTaskRequest.class);
|
for (ProductRiskShopQueueItemVo item : request.getItems()) {
|
||||||
String key = ziniaoShopSwitchService.normalizeShopName(vo.getShopName());
|
if (item == null) {
|
||||||
for (ProductRiskShopQueueItemVo item : request.getItems()) {
|
continue;
|
||||||
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;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
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) {
|
} 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());
|
return new ArrayList<>(byNorm.values());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private List<String> normalizeCountryCodes(List<String> raw) {
|
||||||
|
if (raw == null || raw.isEmpty()) {
|
||||||
|
throw new BusinessException("country_codes 涓嶈兘涓虹┖");
|
||||||
|
}
|
||||||
|
LinkedHashSet<String> 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<LocalDateTime> normalizeScheduleTimes(List<LocalDateTime> raw) {
|
||||||
|
if (raw == null || raw.isEmpty()) {
|
||||||
|
return List.of();
|
||||||
|
}
|
||||||
|
List<LocalDateTime> 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) {
|
private static String safeFileStem(String shopName) {
|
||||||
String value = shopName == null ? "shop" : shopName.trim();
|
String value = shopName == null ? "shop" : shopName.trim();
|
||||||
if (value.isBlank()) {
|
if (value.isBlank()) {
|
||||||
@@ -716,3 +1014,5 @@ public class ShopMatchTaskService {
|
|||||||
return row == null || row.getAsin() == null || row.getAsin().trim().isEmpty();
|
return row == null || row.getAsin() == null || row.getAsin().trim().isEmpty();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -13,13 +13,18 @@ import org.springframework.scheduling.annotation.Scheduled;
|
|||||||
import org.springframework.stereotype.Service;
|
import org.springframework.stereotype.Service;
|
||||||
import org.springframework.transaction.annotation.Transactional;
|
import org.springframework.transaction.annotation.Transactional;
|
||||||
|
|
||||||
|
import java.util.ArrayList;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
|
import java.util.Locale;
|
||||||
|
import java.util.Set;
|
||||||
|
|
||||||
@Slf4j
|
@Slf4j
|
||||||
@Service
|
@Service
|
||||||
@RequiredArgsConstructor
|
@RequiredArgsConstructor
|
||||||
public class ModuleHistoryCleanupService {
|
public class ModuleHistoryCleanupService {
|
||||||
|
|
||||||
|
private static final Set<String> TERMINAL_STATUSES = Set.of("SUCCESS", "FAILED", "CANCELLED", "CANCELED");
|
||||||
|
|
||||||
private final ModuleCleanupProperties moduleCleanupProperties;
|
private final ModuleCleanupProperties moduleCleanupProperties;
|
||||||
private final FileTaskMapper fileTaskMapper;
|
private final FileTaskMapper fileTaskMapper;
|
||||||
private final FileResultMapper fileResultMapper;
|
private final FileResultMapper fileResultMapper;
|
||||||
@@ -35,18 +40,50 @@ public class ModuleHistoryCleanupService {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
List<FileTaskEntity> moduleTasks = fileTaskMapper.selectList(new LambdaQueryWrapper<FileTaskEntity>()
|
||||||
|
.in(FileTaskEntity::getModuleType, moduleTypes)
|
||||||
|
.select(FileTaskEntity::getId, FileTaskEntity::getModuleType, FileTaskEntity::getStatus));
|
||||||
|
|
||||||
|
List<Long> cleanupTaskIds = new ArrayList<>();
|
||||||
|
List<Long> 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<FileResultEntity>()
|
int deletedResults = fileResultMapper.delete(new LambdaQueryWrapper<FileResultEntity>()
|
||||||
.in(FileResultEntity::getModuleType, moduleTypes));
|
.in(FileResultEntity::getModuleType, moduleTypes)
|
||||||
|
.in(FileResultEntity::getTaskId, cleanupTaskIds));
|
||||||
|
|
||||||
int resetTasks = fileTaskMapper.update(null, new LambdaUpdateWrapper<FileTaskEntity>()
|
int resetTasks = fileTaskMapper.update(null, new LambdaUpdateWrapper<FileTaskEntity>()
|
||||||
.in(FileTaskEntity::getModuleType, moduleTypes)
|
.in(FileTaskEntity::getId, cleanupTaskIds)
|
||||||
.set(FileTaskEntity::getResultJson, null)
|
.set(FileTaskEntity::getResultJson, null)
|
||||||
.set(FileTaskEntity::getRequestJson, null)
|
.set(FileTaskEntity::getRequestJson, null)
|
||||||
.set(FileTaskEntity::getErrorMessage, null));
|
.set(FileTaskEntity::getErrorMessage, null));
|
||||||
|
|
||||||
int deletedTasks = fileTaskMapper.delete(new LambdaQueryWrapper<FileTaskEntity>()
|
int deletedTasks = fileTaskMapper.delete(new LambdaQueryWrapper<FileTaskEntity>()
|
||||||
.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));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -84,6 +84,8 @@ aiimage:
|
|||||||
finalize-check-cron: ${AIIMAGE_DELETE_BRAND_FINALIZE_CHECK_CRON:30 */2 * * * *}
|
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-stale-timeout-minutes: ${AIIMAGE_PRODUCT_RISK_STALE_TIMEOUT_MINUTES:10}
|
||||||
product-risk-initial-timeout-minutes: ${AIIMAGE_PRODUCT_RISK_INITIAL_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:
|
module-cleanup:
|
||||||
enabled: ${AIIMAGE_MODULE_CLEANUP_ENABLED:true}
|
enabled: ${AIIMAGE_MODULE_CLEANUP_ENABLED:true}
|
||||||
cron: ${AIIMAGE_MODULE_CLEANUP_CRON:0 0 0 * * *}
|
cron: ${AIIMAGE_MODULE_CLEANUP_CRON:0 0 0 * * *}
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
@@ -640,6 +640,10 @@ export interface ProductRiskTaskSummary {
|
|||||||
updatedAt?: string;
|
updatedAt?: string;
|
||||||
finishedAt?: string;
|
finishedAt?: string;
|
||||||
scheduledAt?: string;
|
scheduledAt?: string;
|
||||||
|
countryCodes?: string[];
|
||||||
|
currentStageIndex?: number;
|
||||||
|
activeStageIndex?: number;
|
||||||
|
scheduleStages?: ShopMatchTaskStage[];
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface ProductRiskTaskDetailVo {
|
export interface ProductRiskTaskDetailVo {
|
||||||
@@ -671,6 +675,12 @@ export type ShopMatchTaskDetailVo = ProductRiskTaskDetailVo;
|
|||||||
export type ShopMatchTaskBatchVo = ProductRiskTaskBatchVo;
|
export type ShopMatchTaskBatchVo = ProductRiskTaskBatchVo;
|
||||||
export type ShopMatchCreateTaskVo = ProductRiskCreateTaskVo;
|
export type ShopMatchCreateTaskVo = ProductRiskCreateTaskVo;
|
||||||
|
|
||||||
|
export interface ShopMatchTaskStage {
|
||||||
|
stageIndex?: number;
|
||||||
|
scheduledAt?: string;
|
||||||
|
status?: string;
|
||||||
|
}
|
||||||
|
|
||||||
export function getProductRiskDashboard() {
|
export function getProductRiskDashboard() {
|
||||||
return unwrapJavaResponse(
|
return unwrapJavaResponse(
|
||||||
get<JavaApiResponse<ProductRiskDashboardVo>>(
|
get<JavaApiResponse<ProductRiskDashboardVo>>(
|
||||||
@@ -851,16 +861,18 @@ export function deleteShopMatchHistory(resultId: number) {
|
|||||||
|
|
||||||
export function createShopMatchTask(
|
export function createShopMatchTask(
|
||||||
items: ShopMatchShopQueueItem[],
|
items: ShopMatchShopQueueItem[],
|
||||||
scheduleTime?: string,
|
countryCodes: string[],
|
||||||
|
scheduleTimes?: string[],
|
||||||
) {
|
) {
|
||||||
return unwrapJavaResponse(
|
return unwrapJavaResponse(
|
||||||
post<
|
post<
|
||||||
JavaApiResponse<ShopMatchCreateTaskVo>,
|
JavaApiResponse<ShopMatchCreateTaskVo>,
|
||||||
{ user_id: number; items: ShopMatchShopQueueItem[]; schedule_time?: string }
|
{ user_id: number; items: ShopMatchShopQueueItem[]; country_codes: string[]; schedule_times?: string[] }
|
||||||
>(`${JAVA_API_PREFIX}/shop-match/tasks`, {
|
>(`${JAVA_API_PREFIX}/shop-match/tasks`, {
|
||||||
user_id: getCurrentUserId(),
|
user_id: getCurrentUserId(),
|
||||||
items,
|
items,
|
||||||
schedule_time: scheduleTime,
|
country_codes: countryCodes,
|
||||||
|
schedule_times: scheduleTimes,
|
||||||
}),
|
}),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -873,15 +885,24 @@ export function deleteShopMatchTask(taskId: number) {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
export function activateShopMatchTask(taskId: number) {
|
export function activateShopMatchTask(taskId: number, stageIndex: number) {
|
||||||
return unwrapJavaResponse(
|
return unwrapJavaResponse(
|
||||||
post<JavaApiResponse<null>, undefined>(
|
post<JavaApiResponse<null>, undefined>(
|
||||||
`${JAVA_API_PREFIX}/shop-match/tasks/${taskId}/activate?user_id=${encodeURIComponent(String(getCurrentUserId()))}`,
|
`${JAVA_API_PREFIX}/shop-match/tasks/${taskId}/activate?user_id=${encodeURIComponent(String(getCurrentUserId()))}&stage_index=${encodeURIComponent(String(stageIndex))}`,
|
||||||
undefined,
|
undefined,
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function completeShopMatchTaskStage(taskId: number, stageIndex: number) {
|
||||||
|
return unwrapJavaResponse(
|
||||||
|
post<JavaApiResponse<null>, { stage_index: number }>(
|
||||||
|
`${JAVA_API_PREFIX}/shop-match/tasks/${taskId}/stage-finished?user_id=${encodeURIComponent(String(getCurrentUserId()))}`,
|
||||||
|
{ stage_index: stageIndex },
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
export function getShopMatchTasksBatch(taskIds: number[]) {
|
export function getShopMatchTasksBatch(taskIds: number[]) {
|
||||||
return unwrapJavaResponse(
|
return unwrapJavaResponse(
|
||||||
post<JavaApiResponse<ShopMatchTaskBatchVo>, { taskIds: number[] }>(
|
post<JavaApiResponse<ShopMatchTaskBatchVo>, { taskIds: number[] }>(
|
||||||
|
|||||||
@@ -1,7 +1,12 @@
|
|||||||
import { createApp } from 'vue'
|
import { createApp } from 'vue'
|
||||||
import ElementPlus from 'element-plus'
|
import ElementPlus from 'element-plus'
|
||||||
|
import zhCn from 'element-plus/es/locale/lang/zh-cn'
|
||||||
|
import dayjs from 'dayjs'
|
||||||
|
import 'dayjs/locale/zh-cn'
|
||||||
import 'element-plus/dist/index.css'
|
import 'element-plus/dist/index.css'
|
||||||
import '@/styles/main.css'
|
import '@/styles/main.css'
|
||||||
import BrandShopMatchTab from '@/pages/brand/components/BrandShopMatchTab.vue'
|
import BrandShopMatchTab from '@/pages/brand/components/BrandShopMatchTab.vue'
|
||||||
|
|
||||||
createApp(BrandShopMatchTab).use(ElementPlus).mount('#app')
|
dayjs.locale('zh-cn')
|
||||||
|
|
||||||
|
createApp(BrandShopMatchTab).use(ElementPlus, { locale: zhCn }).mount('#app')
|
||||||
|
|||||||
Reference in New Issue
Block a user