修复BUG,完善匹配店铺

This commit is contained in:
super
2026-04-11 00:57:34 +08:00
parent c47c03fde4
commit 73392a9b83
30 changed files with 1069 additions and 1675 deletions

View File

@@ -12,6 +12,7 @@ zn_username=%E8%87%AA%E5%8A%A8%E5%8C%96_Robot
client_name=ShuFuAI
# java_api_base=http://47.111.163.154:18080
java_api_base=http://127.0.0.1:18080
# java_api_base=http://8.136.19.173:18081
# java_api_base=http://8.136.19.173:18080

Binary file not shown.

Binary file not shown.

View File

@@ -226,6 +226,9 @@ class MatchTak:
items = data.get("items", [])
country_codes = data.get("country_codes", [])
risk_listing_filter = data.get("risk_listing_filter", "All")
user_id = data.get("user_id")
stage_index = data.get("stage_index")
final_stage = bool(data.get("final_stage", True))
if not task_id:
self.log("任务ID为空跳过", "WARNING")
@@ -269,7 +272,7 @@ class MatchTak:
show_notification(f"开始处理店铺: {shop_name}", "info")
try:
self.process_shop(shop_item, country_codes, task_id,risk_listing_filter)
self.process_shop(shop_item, country_codes, task_id, risk_listing_filter, user_id, stage_index, final_stage)
# 更新已处理店铺数
if task_id in runing_task:
runing_task[task_id]["processed_shops"] += 1
@@ -296,7 +299,7 @@ class MatchTak:
runing_task[task_id]["status"] = "failed"
runing_task[task_id]["error"] = str(e)
def process_shop(self, shop_item: dict, country_codes: list, task_id: int, risk_listing_filter: str):
def process_shop(self, shop_item: dict, country_codes: list, task_id: int, risk_listing_filter: str, user_id=None, stage_index=None, final_stage: bool = True):
"""处理单个店铺
Args:
@@ -406,9 +409,11 @@ class MatchTak:
import traceback
self.log(f"处理国家 {country_code} 失败: {str(e)}", "ERROR")
self.log(traceback.format_exc(), "ERROR")
# 最后回传,标记完成
try:
self.post_result(task_id, shop_name, country_code, "", "", is_done=True)
if final_stage:
self.post_result(task_id, shop_name, country_code, "", "", is_done=True)
else:
self.post_stage_finished(task_id, user_id, stage_index)
except Exception as e:
self.log(f"回传结果失败: {str(e)}", "ERROR")
finally:
@@ -560,6 +565,44 @@ class MatchTak:
self.log(f"国家 {country_name} 处理完成")
def post_stage_finished(self, task_id: int, user_id, stage_index):
import requests
from config import DELETE_BRAND_API_BASE
if user_id in (None, "", 0):
raise ValueError("user_id is required for stage completion callback")
if stage_index is None:
raise ValueError("stage_index is required for stage completion callback")
url = f"{DELETE_BRAND_API_BASE}/api/shop-match/tasks/{task_id}/stage-finished"
payload = {"stage_index": stage_index}
params = {"user_id": user_id}
max_retries = 3
for retry in range(max_retries):
try:
self.log(f"Attempting stage completion callback ({retry + 1}/{max_retries})")
response = requests.post(
url,
params=params,
json=payload,
headers={"Content-Type": "application/json"},
timeout=30,
verify=False,
)
self.log(f"Stage completion callback response: {response.text}")
data = response.json() if response.text else {}
if response.status_code == 200 and isinstance(data, dict) and data.get("success"):
self.log(f"Stage completion callback succeeded: task={task_id}, stage={stage_index}")
return
self.log(f"Stage completion callback failed, status={response.status_code}", "WARNING")
except Exception as e:
self.log(f"Stage completion callback exception: {str(e)}", "ERROR")
if retry < max_retries - 1:
time.sleep(2)
raise RuntimeError(f"Stage completion callback failed after retries: task={task_id}, stage={stage_index}")
def post_result(self, task_id: int, shop_name: str, country_code: str, asin: str, status: str,is_done: bool = False):
"""回传处理结果到API
@@ -596,8 +639,8 @@ class MatchTak:
max_retries = 3
for retry in range(max_retries):
try:
print("================【匹配】=====================")
self.log(f"尝试回传结果 (第 {retry + 1}/{max_retries} 次)")
print("================回传处理结果====================")
self.log(f"尝试回传结果 (第{retry + 1}/{max_retries} 次)")
self.log(f"回传URL: {url}")
self.log(f"回传数据: {payload}")
response = requests.post(
@@ -608,12 +651,11 @@ class MatchTak:
verify=False
)
self.log(f"回传结果: {response.text}")
if response.status_code == 200:
self.log(f"结果回传成功: {asin} - {status}")
data = response.json() if response.text else {}
if response.status_code == 200 and isinstance(data, dict) and data.get("success"):
self.log(f"回传结果成功: {asin} - {status}")
return
else:
self.log(f"结果回传失败,状态码: {response.status_code}", "WARNING")
self.log(f"回传结果失败,状态码: {response.status_code}", "WARNING")
print("=====================================")
except Exception as e:
@@ -624,9 +666,7 @@ class MatchTak:
if retry < max_retries - 1:
time.sleep(2)
self.log(f"已达到最大重试次数,结果回传最终失败", "ERROR")
raise RuntimeError("已达到最大重试次数,结果回传最终失败")
if __name__ == "__main__":
user_info = {

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

View File

@@ -5,8 +5,8 @@
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>格式转换 - 数富AI</title>
<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/brand-DZ5_PGnw.js">
<link rel="modulepreload" crossorigin href="/assets/pywebview-C_Si_gmj.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/convert-7wWJ02Tw.css">
</head>

View File

@@ -5,8 +5,8 @@
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>数据去重 - 数富AI</title>
<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/brand-DZ5_PGnw.js">
<link rel="modulepreload" crossorigin href="/assets/pywebview-C_Si_gmj.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/dedupe-BpNHwt51.css">
</head>

View File

@@ -5,8 +5,8 @@
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>删除品牌 - 数富AI</title>
<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/brand-DZ5_PGnw.js">
<link rel="modulepreload" crossorigin href="/assets/pywebview-C_Si_gmj.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/delete-brand-Dl7T0pmm.css">
</head>

View File

@@ -5,8 +5,8 @@
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>数据拆分 - 数富AI</title>
<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/brand-DZ5_PGnw.js">
<link rel="modulepreload" crossorigin href="/assets/pywebview-C_Si_gmj.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/split-CRUIYKS6.css">
</head>

Binary file not shown.

View File

@@ -20,4 +20,10 @@ public class DeleteBrandProgressProperties {
*/
private long productRiskStaleTimeoutMinutes = 10;
private long productRiskInitialTimeoutMinutes = 10;
/**
* Shop match RUNNING timeout auto-finalize/fail threshold in minutes.
*/
private long shopMatchStaleTimeoutMinutes = 10;
private long shopMatchInitialTimeoutMinutes = 10;
}

View File

@@ -1,6 +1,7 @@
package com.nanri.aiimage.modules.deletebrand.controller;
import com.nanri.aiimage.common.api.ApiResponse;
import com.nanri.aiimage.common.exception.BusinessException;
import com.nanri.aiimage.modules.deletebrand.model.dto.DeleteBrandRunRequest;
import com.nanri.aiimage.modules.deletebrand.model.dto.DeleteBrandSubmitResultRequest;
import com.nanri.aiimage.modules.deletebrand.model.dto.DeleteBrandTaskBatchRequest;
@@ -97,9 +98,18 @@ public class DeleteBrandRunController {
@Operation(summary = "提交删除品牌处理结果", description = "插件/前端分片回传处理结果;后端累计分片并推进进度,全部完成后再执行最终组装与落库。")
public ApiResponse<Void> submitResult(
@PathVariable Long taskId,
@Valid @RequestBody DeleteBrandSubmitResultRequest request) {
deleteBrandRunService.submitResult(taskId, request);
return ApiResponse.success(null);
@Valid @RequestBody DeleteBrandSubmitResultRequest request,
jakarta.servlet.http.HttpServletResponse response) {
try {
deleteBrandRunService.submitResult(taskId, request);
return ApiResponse.success(null);
} catch (BusinessException ex) {
response.setStatus(HttpStatus.BAD_REQUEST.value());
return ApiResponse.fail(ex.getMessage());
} catch (Exception ex) {
response.setStatus(HttpStatus.INTERNAL_SERVER_ERROR.value());
return ApiResponse.fail("服务异常:" + ex.getMessage());
}
}
@GetMapping("/tasks/{taskId}/download")

View File

@@ -5,6 +5,8 @@ import com.baomidou.mybatisplus.core.conditions.update.LambdaUpdateWrapper;
import com.nanri.aiimage.config.DeleteBrandProgressProperties;
import com.nanri.aiimage.modules.productrisk.service.ProductRiskTaskCacheService;
import com.nanri.aiimage.modules.productrisk.service.ProductRiskTaskService;
import com.nanri.aiimage.modules.shopmatch.service.ShopMatchTaskCacheService;
import com.nanri.aiimage.modules.shopmatch.service.ShopMatchTaskService;
import com.nanri.aiimage.modules.task.mapper.FileTaskMapper;
import com.nanri.aiimage.modules.task.model.entity.FileTaskEntity;
import lombok.RequiredArgsConstructor;
@@ -18,7 +20,7 @@ import java.time.ZoneId;
import java.util.Map;
/**
* 删除品牌任务的超时回收与 finalize 补偿并与删除品牌共用同一套定时调度对商品风险PRODUCT_RISK_RESOLVE长时间无回传做失败收尾。
* 鍒犻櫎鍝佺墝浠诲姟鐨勮秴鏃跺洖鏀朵笌 finalize 琛ュ伩锛涘苟涓庡垹闄ゅ搧鐗屽叡鐢ㄥ悓涓€濂楀畾鏃惰皟搴︼紝瀵瑰晢鍝侀闄╋紙PRODUCT_RISK_RESOLVE锛夐暱鏃堕棿鏃犲洖浼犲仛澶辫触鏀跺熬銆?
*/
@Service
@RequiredArgsConstructor
@@ -27,12 +29,15 @@ public class DeleteBrandStaleTaskService {
private static final String MODULE_TYPE_DELETE_BRAND = "DELETE_BRAND";
private static final String MODULE_TYPE_PRODUCT_RISK = "PRODUCT_RISK_RESOLVE";
private static final String MODULE_TYPE_SHOP_MATCH = "SHOP_MATCH";
private final FileTaskMapper fileTaskMapper;
private final DeleteBrandTaskCacheService deleteBrandTaskCacheService;
private final DeleteBrandRunService deleteBrandRunService;
private final ProductRiskTaskService productRiskTaskService;
private final ProductRiskTaskCacheService productRiskTaskCacheService;
private final ShopMatchTaskService shopMatchTaskService;
private final ShopMatchTaskCacheService shopMatchTaskCacheService;
private final DeleteBrandProgressProperties deleteBrandProgressProperties;
@Scheduled(cron = "${aiimage.delete-brand-progress.stale-check-cron:0 */2 * * * *}")
@@ -41,6 +46,7 @@ public class DeleteBrandStaleTaskService {
log.info("[stale-check] scan started thread={}", Thread.currentThread().getName());
failStaleDeleteBrandTasks();
ProductRiskStaleCheckStats stats = failStaleProductRiskResolveTasks();
ShopMatchStaleCheckStats shopMatchStats = failStaleShopMatchTasks();
log.info("[stale-check] product-risk summary scanned={} finalized={} failed={} skipped={} elapsedMs={} thread={}",
stats.scannedTaskCount,
stats.finalizedTaskCount,
@@ -48,12 +54,19 @@ public class DeleteBrandStaleTaskService {
stats.skippedTaskCount,
System.currentTimeMillis() - startedAt,
Thread.currentThread().getName());
log.info("[stale-check] shop-match summary scanned={} finalized={} failed={} skipped={} elapsedMs={} thread={}",
shopMatchStats.scannedTaskCount,
shopMatchStats.finalizedTaskCount,
shopMatchStats.failedTaskCount,
shopMatchStats.skippedTaskCount,
System.currentTimeMillis() - startedAt,
Thread.currentThread().getName());
}
private void failStaleDeleteBrandTasks() {
LocalDateTime threshold = LocalDateTime.now().minusMinutes(deleteBrandProgressProperties.getHeartbeatTimeoutMinutes());
// 先按 DB updatedAt 粗筛,避免全表扫描
// 鍏堟寜 DB updatedAt 绮楃瓫锛岄伩鍏嶅叏琛ㄦ壂鎻?
java.util.List<FileTaskEntity> runningTasks = fileTaskMapper.selectList(new LambdaQueryWrapper<FileTaskEntity>()
.eq(FileTaskEntity::getModuleType, MODULE_TYPE_DELETE_BRAND)
.eq(FileTaskEntity::getStatus, "RUNNING")
@@ -83,19 +96,19 @@ public class DeleteBrandStaleTaskService {
}
LocalDateTime lastActivityTime;
// 只有当 has_progress true 且超过 15 分钟没心跳,才认为是异常卡死
// 鍙湁褰?has_progress 涓?true 涓旇秴杩?15 鍒嗛挓娌″績璺筹紝鎵嶈涓烘槸寮傚父鍗℃
if (lastHeartbeatAt > 0 && isActive) {
// 有心跳,用最后心跳时间对比 15 分钟 (threshold now - 15)
// 鏈夊績璺筹紝鐢ㄦ渶鍚庡績璺虫椂闂村姣?15 鍒嗛挓 (threshold 鏄?now - 15)
lastActivityTime = LocalDateTime.ofInstant(Instant.ofEpochMilli(lastHeartbeatAt), ZoneId.systemDefault());
if (lastActivityTime.isAfter(threshold)) {
continue; // 没超时
continue; // 娌¤秴鏃?
}
} else {
// 没有心跳,或者当前处在 has_progress: false (等待用户点击下一个任务推送的静默期)
// 静默期和排队期的宽限期一样,设为 12 个小时
// 娌℃湁蹇冭烦锛屾垨鑰呭綋鍓嶅鍦?has_progress: false 锛堢瓑寰呯敤鎴风偣鍑讳笅涓€涓换鍔℃帹閫佺殑闈欓粯鏈燂級
// 闈欓粯鏈熷拰鎺掗槦鏈熺殑瀹介檺鏈熶竴鏍凤紝璁句负 12 涓皬鏃?
LocalDateTime queuedThreshold = LocalDateTime.now().minusHours(12);
if (task.getCreatedAt() != null && task.getCreatedAt().isAfter(queuedThreshold)) {
continue; // 等待下个队列推送中,不要杀它!
continue; // 绛夊緟涓嬩釜闃熷垪鎺ㄩ€佷腑锛屼笉瑕佹潃瀹冿紒
}
}
@@ -104,7 +117,7 @@ public class DeleteBrandStaleTaskService {
.eq(FileTaskEntity::getModuleType, MODULE_TYPE_DELETE_BRAND)
.eq(FileTaskEntity::getStatus, "RUNNING")
.set(FileTaskEntity::getStatus, "FAILED")
.set(FileTaskEntity::getErrorMessage, "结果回传长时间无响应,任务已自动失败")
.set(FileTaskEntity::getErrorMessage, "缁撴灉鍥炰紶闀挎椂闂存棤鍝嶅簲锛屼换鍔″凡鑷姩澶辫触")
.set(FileTaskEntity::getUpdatedAt, LocalDateTime.now())
.set(FileTaskEntity::getFinishedAt, LocalDateTime.now()));
@@ -157,7 +170,7 @@ public class DeleteBrandStaleTaskService {
.eq(FileTaskEntity::getModuleType, MODULE_TYPE_PRODUCT_RISK)
.eq(FileTaskEntity::getStatus, "RUNNING")
.set(FileTaskEntity::getStatus, "FAILED")
.set(FileTaskEntity::getErrorMessage, "长时间未收到 Python 结果,任务已自动失败")
.set(FileTaskEntity::getErrorMessage, "闀挎椂闂存湭鏀跺埌 Python 缁撴灉锛屼换鍔″凡鑷姩澶辫触")
.set(FileTaskEntity::getUpdatedAt, LocalDateTime.now())
.set(FileTaskEntity::getFinishedAt, LocalDateTime.now()));
if (updated > 0) {
@@ -170,6 +183,63 @@ public class DeleteBrandStaleTaskService {
return stats;
}
private ShopMatchStaleCheckStats failStaleShopMatchTasks() {
ShopMatchStaleCheckStats stats = new ShopMatchStaleCheckStats();
long minutes = Math.max(1L, deleteBrandProgressProperties.getShopMatchStaleTimeoutMinutes());
long initialMinutes = Math.max(minutes, deleteBrandProgressProperties.getShopMatchInitialTimeoutMinutes());
LocalDateTime now = LocalDateTime.now();
LocalDateTime threshold = now.minusMinutes(minutes);
LocalDateTime initialThreshold = now.minusMinutes(initialMinutes);
java.util.List<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 * * * *}")
public void finalizeCompletedRunningTasks() {
java.util.List<FileTaskEntity> runningTasks = fileTaskMapper.selectList(new LambdaQueryWrapper<FileTaskEntity>()
@@ -182,7 +252,7 @@ public class DeleteBrandStaleTaskService {
try {
deleteBrandRunService.tryFinalizeTask(task.getId(), true);
} catch (Exception ignored) {
// 定时补偿不影响主流程;下次调度或人工重试时再处理
// 瀹氭椂琛ュ伩涓嶅奖鍝嶄富娴佺▼锛涗笅娆¤皟搴︽垨浜哄伐閲嶈瘯鏃跺啀澶勭悊
}
}
}
@@ -193,4 +263,12 @@ public class DeleteBrandStaleTaskService {
private int failedTaskCount;
private int skippedTaskCount;
}
private static final class ShopMatchStaleCheckStats {
private int scannedTaskCount;
private int finalizedTaskCount;
private int failedTaskCount;
private int skippedTaskCount;
}
}

View File

@@ -1,40 +1,60 @@
package com.nanri.aiimage.modules.productrisk.model.vo;
import com.fasterxml.jackson.annotation.JsonProperty;
import com.nanri.aiimage.modules.shopmatch.model.vo.ShopMatchTaskStageVo;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
import java.util.ArrayList;
import java.util.List;
@Data
@Schema(description = "商品风险任务概要biz_file_task 核心字段)")
@Schema(description = "Task summary")
public class ProductRiskTaskItemVo {
@Schema(description = "任务主键", example = "200")
@Schema(description = "Task primary key", example = "200")
private Long id;
@JsonProperty("taskNo")
@Schema(description = "业务单号,如 PRODUCT_RISK_RESOLVE-雪花 id")
@Schema(description = "Task number")
private String taskNo;
@Schema(description = "任务状态RUNNING 执行中SUCCESS 已结束且至少一店成功FAILED 已结束且全部失败或整体失败")
@Schema(description = "Task status")
private String status;
@JsonProperty("errorMessage")
@Schema(description = "失败或部分失败时的汇总错误说明;成功且无附加信息时可能为空")
@Schema(description = "Task error message")
private String errorMessage;
@JsonProperty("createdAt")
@Schema(description = "创建时间(字符串格式,服务端 LocalDateTime.toString()")
@Schema(description = "Created at")
private String createdAt;
@JsonProperty("updatedAt")
@Schema(description = "最后更新时间")
@Schema(description = "Updated at")
private String updatedAt;
@JsonProperty("finishedAt")
@Schema(description = "结束时间RUNNING 时通常为空")
@Schema(description = "Finished at")
private String finishedAt;
@JsonProperty("scheduledAt")
@Schema(description = "定时执行时间,未设置时为空")
@Schema(description = "Next scheduled execution time")
private String scheduledAt;
@JsonProperty("countryCodes")
@Schema(description = "Country codes used by shop match task")
private List<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;
}

View File

@@ -3,7 +3,6 @@ package com.nanri.aiimage.modules.shopmatch.controller;
import com.nanri.aiimage.common.api.ApiResponse;
import com.nanri.aiimage.modules.productrisk.model.dto.ProductRiskCandidateAddRequest;
import com.nanri.aiimage.modules.productrisk.model.dto.ProductRiskCountryPreferenceSaveRequest;
import com.nanri.aiimage.modules.productrisk.model.dto.ProductRiskCreateTaskRequest;
import com.nanri.aiimage.modules.productrisk.model.dto.ProductRiskMatchShopsRequest;
import com.nanri.aiimage.modules.productrisk.model.dto.ProductRiskTaskBatchRequest;
import com.nanri.aiimage.modules.productrisk.model.vo.ProductRiskCandidateVo;
@@ -13,9 +12,15 @@ import com.nanri.aiimage.modules.productrisk.model.vo.ProductRiskDashboardVo;
import com.nanri.aiimage.modules.productrisk.model.vo.ProductRiskHistoryVo;
import com.nanri.aiimage.modules.productrisk.model.vo.ProductRiskMatchShopsVo;
import com.nanri.aiimage.modules.productrisk.model.vo.ProductRiskTaskBatchVo;
import com.nanri.aiimage.modules.shopmatch.model.dto.ShopMatchCreateTaskRequest;
import com.nanri.aiimage.modules.shopmatch.model.dto.ShopMatchStageCompleteRequest;
import com.nanri.aiimage.modules.shopmatch.model.dto.ShopMatchSubmitResultRequest;
import com.nanri.aiimage.modules.shopmatch.service.ShopMatchResolveService;
import com.nanri.aiimage.modules.shopmatch.service.ShopMatchTaskService;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.Parameter;
import io.swagger.v3.oas.annotations.enums.ParameterIn;
import io.swagger.v3.oas.annotations.tags.Tag;
import jakarta.validation.Valid;
import lombok.RequiredArgsConstructor;
import org.springframework.http.HttpHeaders;
@@ -40,95 +45,143 @@ import java.util.List;
@RestController
@RequiredArgsConstructor
@RequestMapping("/api/shop-match")
@Tag(name = "匹配店铺", description = "店铺候选、国家顺序、批量匹配、定时任务、Python 回传结果与下载接口。")
public class ShopMatchController {
private final ShopMatchResolveService shopMatchResolveService;
private final ShopMatchTaskService shopMatchTaskService;
@GetMapping("/candidates")
public ApiResponse<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));
}
@PostMapping("/candidates")
@Operation(summary = "新增候选店铺", description = "新增一条候选店铺记录,供前端后续匹配与创建任务使用。")
public ApiResponse<ProductRiskCandidateVo> addCandidate(@Valid @RequestBody ProductRiskCandidateAddRequest request) {
return ApiResponse.success(shopMatchResolveService.addCandidate(request));
}
@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);
return ApiResponse.success(null);
}
@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));
}
@PutMapping("/country-preference")
@Operation(summary = "保存国家配置顺序", description = "保存当前用户在匹配店铺模块中的国家勾选与执行顺序。")
public ApiResponse<ProductRiskCountryPreferenceVo> saveCountryPreference(@Valid @RequestBody ProductRiskCountryPreferenceSaveRequest request) {
return ApiResponse.success(shopMatchResolveService.saveCountryPreference(request));
}
@PostMapping("/match-shops")
@Operation(summary = "批量匹配店铺", description = "按店铺名批量匹配紫鸟索引,返回店铺 ID、平台、公司与匹配状态。")
public ApiResponse<ProductRiskMatchShopsVo> matchShops(@Valid @RequestBody ProductRiskMatchShopsRequest request) {
return ApiResponse.success(shopMatchResolveService.matchShops(request));
}
@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));
}
@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));
}
@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);
return ApiResponse.success(null);
}
@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));
}
@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);
return ApiResponse.success(null);
}
@PostMapping("/tasks/{taskId}/activate")
public ApiResponse<Void> activateTask(@PathVariable Long taskId, @RequestParam("user_id") Long userId) {
shopMatchTaskService.activateTask(taskId, userId);
@Operation(summary = "激活定时阶段", description = "在某个定时阶段开始执行前,将任务切换到运行中状态。")
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);
}
@PostMapping("/tasks/batch")
@Operation(summary = "批量查询任务详情", description = "供前端轮询任务状态与阶段进度使用。")
public ApiResponse<ProductRiskTaskBatchVo> tasksBatch(@Valid @RequestBody ProductRiskTaskBatchRequest request) {
return ApiResponse.success(shopMatchTaskService.getTaskDetailsBatch(request.getTaskIds()));
}
@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);
return ApiResponse.success(null);
}
@GetMapping("/results/{resultId}/download")
@Operation(summary = "下载结果文件", description = "按结果记录下载后端已生成的 Excel 文件。")
public void downloadResult(
@PathVariable Long resultId,
@RequestParam("user_id") Long userId,
@Parameter(description = "结果记录主键", example = "1001") @PathVariable Long resultId,
@Parameter(name = "user_id", description = "当前用户 ID", required = true, in = ParameterIn.QUERY, example = "1") @RequestParam("user_id") Long userId,
jakarta.servlet.http.HttpServletResponse response) {
String url = shopMatchTaskService.resolveResultDownloadUrl(resultId, userId);
String filename = shopMatchTaskService.resolveResultDownloadFilename(resultId, userId);
if (url == null || url.isBlank()) {
throw new ResponseStatusException(HttpStatus.NOT_FOUND, "暂无可下载结果");
throw new ResponseStatusException(HttpStatus.NOT_FOUND, "No downloadable result");
}
try {
String encodedFilename = URLEncoder.encode(filename, StandardCharsets.UTF_8).replace("+", "%20");
@@ -144,7 +197,7 @@ public class ShopMatchController {
response.getOutputStream().flush();
}
} catch (Exception ex) {
throw new ResponseStatusException(HttpStatus.INTERNAL_SERVER_ERROR, "下载失败");
throw new ResponseStatusException(HttpStatus.INTERNAL_SERVER_ERROR, "Download failed");
}
}
}

View File

@@ -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;
}

View File

@@ -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;
}

View File

@@ -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;
}

View File

@@ -6,7 +6,7 @@ import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.nanri.aiimage.common.exception.BusinessException;
import com.nanri.aiimage.modules.file.service.oss.OssStorageService;
import com.nanri.aiimage.modules.productrisk.model.dto.ProductRiskCreateTaskRequest;
import com.nanri.aiimage.modules.productrisk.model.enums.ProductRiskCountryCode;
import com.nanri.aiimage.modules.productrisk.model.vo.ProductRiskCreateTaskVo;
import com.nanri.aiimage.modules.productrisk.model.vo.ProductRiskDashboardVo;
import com.nanri.aiimage.modules.productrisk.model.vo.ProductRiskHistoryVo;
@@ -15,9 +15,12 @@ import com.nanri.aiimage.modules.productrisk.model.vo.ProductRiskShopQueueItemVo
import com.nanri.aiimage.modules.productrisk.model.vo.ProductRiskTaskBatchVo;
import com.nanri.aiimage.modules.productrisk.model.vo.ProductRiskTaskDetailVo;
import com.nanri.aiimage.modules.productrisk.model.vo.ProductRiskTaskItemVo;
import com.nanri.aiimage.modules.shopmatch.model.vo.ShopMatchTaskStageVo;
import com.nanri.aiimage.modules.shopmatch.mapper.ShopMatchShopCandidateMapper;
import com.nanri.aiimage.modules.shopmatch.model.dto.ShopMatchCreateTaskRequest;
import com.nanri.aiimage.modules.shopmatch.model.dto.ShopMatchRowDto;
import com.nanri.aiimage.modules.shopmatch.model.dto.ShopMatchShopPayloadDto;
import com.nanri.aiimage.modules.shopmatch.model.dto.ShopMatchStageCompleteRequest;
import com.nanri.aiimage.modules.shopmatch.model.dto.ShopMatchSubmitResultRequest;
import com.nanri.aiimage.modules.shopmatch.model.entity.ShopMatchShopCandidateEntity;
import com.nanri.aiimage.modules.task.mapper.FileResultMapper;
@@ -34,6 +37,7 @@ import java.io.File;
import java.time.LocalDateTime;
import java.util.ArrayList;
import java.util.LinkedHashMap;
import java.util.LinkedHashSet;
import java.util.List;
import java.util.Locale;
import java.util.Map;
@@ -57,7 +61,7 @@ public class ShopMatchTaskService {
public ProductRiskDashboardVo dashboard(Long userId) {
if (userId == null || userId <= 0) {
throw new BusinessException("user_id 不合法");
throw new BusinessException("user_id is invalid");
}
ProductRiskDashboardVo vo = new ProductRiskDashboardVo();
vo.setCandidateCount(candidateMapper.selectCount(new LambdaQueryWrapper<ShopMatchShopCandidateEntity>()
@@ -82,7 +86,7 @@ public class ShopMatchTaskService {
public ProductRiskHistoryVo listHistory(Long userId) {
if (userId == null || userId <= 0) {
throw new BusinessException("user_id 不合法");
throw new BusinessException("user_id is invalid");
}
ProductRiskHistoryVo vo = new ProductRiskHistoryVo();
List<FileResultEntity> entities = fileResultMapper.selectList(new LambdaQueryWrapper<FileResultEntity>()
@@ -114,11 +118,11 @@ public class ShopMatchTaskService {
@Transactional
public void deleteHistory(Long resultId, Long userId) {
if (userId == null || userId <= 0) {
throw new BusinessException("user_id 不合法");
throw new BusinessException("user_id is invalid");
}
FileResultEntity entity = fileResultMapper.selectById(resultId);
if (entity == null || !MODULE_TYPE.equals(entity.getModuleType()) || !userId.equals(entity.getUserId())) {
throw new BusinessException("记录不存在");
throw new BusinessException("Invalid request");
}
Long taskId = entity.getTaskId();
fileResultMapper.deleteById(resultId);
@@ -130,11 +134,11 @@ public class ShopMatchTaskService {
@Transactional
public void deleteTask(Long taskId, Long userId) {
if (userId == null || userId <= 0) {
throw new BusinessException("user_id 不合法");
throw new BusinessException("user_id is invalid");
}
FileTaskEntity task = fileTaskMapper.selectById(taskId);
if (task == null || !MODULE_TYPE.equals(task.getModuleType()) || !userId.equals(task.getUserId())) {
throw new BusinessException("任务不存在");
throw new BusinessException("Invalid request");
}
fileResultMapper.delete(new LambdaQueryWrapper<FileResultEntity>()
.eq(FileResultEntity::getTaskId, taskId)
@@ -182,10 +186,10 @@ public class ShopMatchTaskService {
public String resolveResultDownloadUrl(Long resultId, Long userId) {
FileResultEntity entity = fileResultMapper.selectById(resultId);
if (entity == null || !MODULE_TYPE.equals(entity.getModuleType()) || !userId.equals(entity.getUserId())) {
throw new BusinessException("记录不存在");
throw new BusinessException("Invalid request");
}
if (entity.getResultFileUrl() == null || entity.getResultFileUrl().isBlank()) {
throw new BusinessException("暂无可下载文件");
throw new BusinessException("Invalid request");
}
return ossStorageService.generateFreshDownloadUrl(entity.getResultFileUrl());
}
@@ -193,7 +197,7 @@ public class ShopMatchTaskService {
public String resolveResultDownloadFilename(Long resultId, Long userId) {
FileResultEntity entity = fileResultMapper.selectById(resultId);
if (entity == null || !MODULE_TYPE.equals(entity.getModuleType()) || !userId.equals(entity.getUserId())) {
throw new BusinessException("记录不存在");
throw new BusinessException("Invalid request");
}
return entity.getResultFilename() != null && !entity.getResultFilename().isBlank()
? entity.getResultFilename()
@@ -201,34 +205,41 @@ public class ShopMatchTaskService {
}
@Transactional
public ProductRiskCreateTaskVo createTask(ProductRiskCreateTaskRequest request) {
public ProductRiskCreateTaskVo createTask(ShopMatchCreateTaskRequest request) {
if (request.getUserId() == null || request.getUserId() <= 0) {
throw new BusinessException("user_id 不合法");
throw new BusinessException("user_id is invalid");
}
if (request.getItems() == null || request.getItems().isEmpty()) {
throw new BusinessException("items 不能为空");
throw new BusinessException("items 涓嶈兘涓虹┖");
}
List<ProductRiskShopQueueItemVo> uniqueItems = dedupeQueueItems(request.getItems());
if (uniqueItems.isEmpty()) {
throw new BusinessException("items 不能为空");
throw new BusinessException("items 涓嶈兘涓虹┖");
}
for (ProductRiskShopQueueItemVo item : uniqueItems) {
if (item == null || !item.isMatched()) {
throw new BusinessException("存在未匹配店铺,无法创建任务");
throw new BusinessException("瀛樺湪鏈尮閰嶅簵閾猴紝鏃犳硶鍒涘缓浠诲姟");
}
}
List<String> countryCodes = normalizeCountryCodes(request.getCountryCodes());
List<LocalDateTime> scheduleTimes = normalizeScheduleTimes(request.getScheduleTimes());
LocalDateTime now = LocalDateTime.now();
LocalDateTime scheduledAt = request.getScheduleTime();
if (scheduledAt != null && !scheduledAt.isAfter(now)) {
throw new BusinessException("schedule_time 必须晚于当前时间");
ShopMatchCreateTaskRequest persistedRequest = new ShopMatchCreateTaskRequest();
persistedRequest.setUserId(request.getUserId());
persistedRequest.setItems(uniqueItems);
persistedRequest.setCountryCodes(countryCodes);
persistedRequest.setScheduleTimes(scheduleTimes);
if (!scheduleTimes.isEmpty()) {
persistedRequest.setCurrentStageIndex(0);
}
FileTaskEntity task = new FileTaskEntity();
task.setTaskNo(MODULE_TYPE + "-" + IdUtil.getSnowflakeNextIdStr());
task.setModuleType(MODULE_TYPE);
task.setTaskMode("PYTHON_QUEUE");
task.setStatus(scheduledAt != null ? "SCHEDULED" : "RUNNING");
task.setStatus(scheduleTimes.isEmpty() ? "RUNNING" : "SCHEDULED");
task.setSourceFileCount(uniqueItems.size());
task.setSuccessFileCount(0);
task.setFailedFileCount(0);
@@ -236,14 +247,14 @@ public class ShopMatchTaskService {
task.setUserId(request.getUserId());
task.setCreatedAt(now);
task.setUpdatedAt(now);
task.setScheduledAt(scheduledAt);
task.setScheduledAt(scheduleTimes.isEmpty() ? null : scheduleTimes.get(0));
fileTaskMapper.insert(task);
List<ProductRiskResultItemVo> snapshot = new ArrayList<>();
for (ProductRiskShopQueueItemVo item : uniqueItems) {
String normalized = ziniaoShopSwitchService.normalizeShopName(item.getShopName());
if (normalized.isBlank()) {
throw new BusinessException("店铺名无效");
throw new BusinessException("Invalid request");
}
FileResultEntity result = new FileResultEntity();
result.setTaskId(task.getId());
@@ -257,10 +268,10 @@ public class ShopMatchTaskService {
snapshot.add(toSnapshotVo(result, item, task.getStatus()));
}
try {
task.setRequestJson(objectMapper.writeValueAsString(request));
task.setRequestJson(objectMapper.writeValueAsString(persistedRequest));
task.setResultJson(objectMapper.writeValueAsString(snapshot));
} catch (Exception ex) {
throw new BusinessException("序列化任务数据失败");
throw new BusinessException("Invalid request");
}
fileTaskMapper.updateById(task);
@@ -271,49 +282,96 @@ public class ShopMatchTaskService {
}
@Transactional
public void activateTask(Long taskId, Long userId) {
public void activateTask(Long taskId, Long userId, Integer stageIndex) {
if (taskId == null || taskId <= 0) {
throw new BusinessException("taskId 不合法");
throw new BusinessException("taskId is invalid");
}
if (userId == null || userId <= 0) {
throw new BusinessException("user_id 不合法");
throw new BusinessException("user_id is invalid");
}
if (stageIndex == null || stageIndex < 0) {
throw new BusinessException("stage_index is invalid");
}
FileTaskEntity task = fileTaskMapper.selectById(taskId);
if (task == null || !MODULE_TYPE.equals(task.getModuleType()) || !userId.equals(task.getUserId())) {
throw new BusinessException("任务不存在");
throw new BusinessException("Invalid request");
}
if ("SUCCESS".equals(task.getStatus()) || "FAILED".equals(task.getStatus())) {
throw new BusinessException("任务已结束");
}
if ("RUNNING".equals(task.getStatus())) {
return;
throw new BusinessException("Invalid request");
}
if (!"SCHEDULED".equals(task.getStatus())) {
throw new BusinessException("当前任务状态不允许激活");
throw new BusinessException("Invalid request");
}
if (task.getScheduledAt() != null && LocalDateTime.now().isBefore(task.getScheduledAt())) {
throw new BusinessException("未到定时执行时间");
throw new BusinessException("鏈埌瀹氭椂鎵ц鏃堕棿");
}
ShopMatchCreateTaskRequest state = parseTaskRequest(task);
int currentStageIndex = state.getCurrentStageIndex() == null ? 0 : state.getCurrentStageIndex();
if (currentStageIndex != stageIndex) {
throw new BusinessException("褰撳墠杞宸插彉鍖栵紝璇峰埛鏂板悗閲嶈瘯");
}
if (state.getScheduleTimes() == null || stageIndex >= state.getScheduleTimes().size()) {
throw new BusinessException("Invalid request");
}
state.setActiveStageIndex(stageIndex);
task.setStatus("RUNNING");
task.setUpdatedAt(LocalDateTime.now());
persistTaskRequest(task, state);
fileTaskMapper.updateById(task);
}
@Transactional
public void completeStage(Long taskId, Long userId, ShopMatchStageCompleteRequest request) {
if (taskId == null || taskId <= 0) {
throw new BusinessException("taskId is invalid");
}
if (userId == null || userId <= 0) {
throw new BusinessException("user_id is invalid");
}
if (request == null || request.getStageIndex() == null || request.getStageIndex() < 0) {
throw new BusinessException("stage_index is invalid");
}
FileTaskEntity task = fileTaskMapper.selectById(taskId);
if (task == null || !MODULE_TYPE.equals(task.getModuleType()) || !userId.equals(task.getUserId())) {
throw new BusinessException("Invalid request");
}
if (!"RUNNING".equals(task.getStatus())) {
throw new BusinessException("Invalid request");
}
ShopMatchCreateTaskRequest state = parseTaskRequest(task);
if (state.getActiveStageIndex() == null || !state.getActiveStageIndex().equals(request.getStageIndex())) {
throw new BusinessException("杞鐘舵€佷笉鍖归厤");
}
int nextIndex = request.getStageIndex() + 1;
if (state.getScheduleTimes() == null || nextIndex >= state.getScheduleTimes().size()) {
throw new BusinessException("Current stage is already the final stage");
}
state.setCurrentStageIndex(nextIndex);
state.setActiveStageIndex(null);
task.setStatus("SCHEDULED");
task.setScheduledAt(state.getScheduleTimes().get(nextIndex));
task.setUpdatedAt(LocalDateTime.now());
task.setFinishedAt(null);
persistTaskRequest(task, state);
fileTaskMapper.updateById(task);
}
@Transactional
public void submitResult(Long taskId, ShopMatchSubmitResultRequest request) {
if (taskId == null || taskId <= 0) {
throw new BusinessException("taskId 不合法");
throw new BusinessException("taskId is invalid");
}
if (request == null || request.getShops() == null || request.getShops().isEmpty()) {
throw new BusinessException("shops 不能为空");
throw new BusinessException("shops 涓嶈兘涓虹┖");
}
FileTaskEntity task = fileTaskMapper.selectById(taskId);
if (task == null || !MODULE_TYPE.equals(task.getModuleType())) {
throw new BusinessException("任务不存在");
throw new BusinessException("Invalid request");
}
if ("SUCCESS".equals(task.getStatus()) || "FAILED".equals(task.getStatus())) {
throw new BusinessException("任务已结束,拒绝重复提交");
throw new BusinessException("浠诲姟宸茬粨鏉燂紝鎷掔粷閲嶅鎻愪氦");
}
List<FileResultEntity> resultRows = fileResultMapper.selectList(new LambdaQueryWrapper<FileResultEntity>()
@@ -350,7 +408,7 @@ public class ShopMatchTaskService {
continue;
}
if (countPayloadRows(merged) <= 0) {
markResultFailed(result, "未收到可组装的结果数据");
markResultFailed(result, "No result rows received for assembly");
shopMatchTaskCacheService.removeShopMergedPayload(taskId, shopKey);
continue;
}
@@ -373,6 +431,102 @@ public class ShopMatchTaskService {
updateTaskStatusFromLatestRows(task, latest);
}
@Transactional
public boolean tryFinalizeTask(Long taskId, boolean fromCompensation) {
if (taskId == null || taskId <= 0) {
return false;
}
FileTaskEntity task = fileTaskMapper.selectById(taskId);
if (task == null || !MODULE_TYPE.equals(task.getModuleType())) {
return false;
}
if ("SUCCESS".equals(task.getStatus()) || "FAILED".equals(task.getStatus())) {
return true;
}
List<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) {
Map<String, List<ShopMatchRowDto>> countries = excelAssemblyService.normalizeCountriesMap(payload.getCountries());
String displayName = payload.getShopName() != null && !payload.getShopName().isBlank() ? payload.getShopName().trim() : shopKey;
@@ -406,6 +560,12 @@ public class ShopMatchTaskService {
}
private void updateTaskStatusFromLatestRows(FileTaskEntity task, List<FileResultEntity> latest) {
updateTaskStatusFromLatestRows(task, latest, List.of());
}
private void updateTaskStatusFromLatestRows(FileTaskEntity task,
List<FileResultEntity> latest,
List<String> batchErrors) {
int ok = 0;
int fail = 0;
List<String> errors = new ArrayList<>();
@@ -435,7 +595,7 @@ public class ShopMatchTaskService {
task.setFinishedAt(LocalDateTime.now());
} else if (ok > 0) {
task.setStatus("SUCCESS");
task.setErrorMessage(String.join("; ", errors));
task.setErrorMessage(String.join("; ", errors.isEmpty() ? batchErrors : errors));
task.setFinishedAt(LocalDateTime.now());
} else {
task.setStatus("FAILED");
@@ -473,7 +633,7 @@ public class ShopMatchTaskService {
.orderByAsc(FileResultEntity::getId));
List<ProductRiskResultItemVo> items = new ArrayList<>();
for (FileResultEntity row : rows) {
items.add(toHistoryItemVo(row, taskStatus));
throw new BusinessException("Task request payload is missing");
}
return items;
}
@@ -488,10 +648,20 @@ public class ShopMatchTaskService {
vo.setUpdatedAt(fmt(task.getUpdatedAt()));
vo.setFinishedAt(fmt(task.getFinishedAt()));
vo.setScheduledAt(fmt(task.getScheduledAt()));
ShopMatchCreateTaskRequest request = parseTaskRequestSilently(task);
if (request != null) {
vo.setCountryCodes(new ArrayList<>(request.getCountryCodes() == null ? List.of() : request.getCountryCodes()));
vo.setCurrentStageIndex(request.getCurrentStageIndex());
vo.setActiveStageIndex(request.getActiveStageIndex());
vo.setScheduleStages(buildStageVos(task, request));
}
return vo;
}
private ProductRiskResultItemVo toSnapshotVo(FileResultEntity result, ProductRiskShopQueueItemVo item, String taskStatus) {
private ProductRiskResultItemVo toSnapshotVo(FileResultEntity result,
ProductRiskShopQueueItemVo item,
String taskStatus) {
ProductRiskResultItemVo vo = new ProductRiskResultItemVo();
vo.setResultId(result.getId());
vo.setTaskId(result.getTaskId());
@@ -503,8 +673,10 @@ public class ShopMatchTaskService {
vo.setMatchStatus(item.getMatchStatus());
vo.setMatchMessage(item.getMatchMessage());
vo.setTaskStatus(taskStatus);
vo.setScheduledAt(fmt(result.getTaskId() == null ? null : loadScheduledAt(result.getTaskId())));
vo.setSuccess(false);
vo.setError(null);
vo.setOutputFilename(null);
vo.setDownloadUrl(null);
return vo;
}
@@ -515,22 +687,66 @@ public class ShopMatchTaskService {
vo.setShopName(entity.getSourceFilename());
vo.setShopId(entity.getSourceFileUrl());
vo.setTaskStatus(taskStatus);
vo.setSuccess(entity.getSuccess() != null && entity.getSuccess() == 1);
boolean success = entity.getSuccess() != null && entity.getSuccess() == 1;
vo.setSuccess(success);
vo.setError(entity.getErrorMessage());
vo.setOutputFilename(entity.getResultFilename());
vo.setDownloadUrl(entity.getResultFileUrl() == null || entity.getResultFileUrl().isBlank()
? null
: ossStorageService.generateFreshDownloadUrl(entity.getResultFileUrl()));
vo.setMatched(true);
if (entity.getTaskId() != null) {
vo.setScheduledAt(fmt(loadScheduledAt(entity.getTaskId())));
}
if (entity.getTaskId() != null) {
mergeQueueFieldsFromRequest(vo, entity.getTaskId());
}
return vo;
}
private List<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) {
FileTaskEntity task = fileTaskMapper.selectById(taskId);
return task == null ? null : task.getScheduledAt();
@@ -538,27 +754,60 @@ public class ShopMatchTaskService {
private void mergeQueueFieldsFromRequest(ProductRiskResultItemVo vo, Long taskId) {
FileTaskEntity task = fileTaskMapper.selectById(taskId);
if (task == null || task.getRequestJson() == null || task.getRequestJson().isBlank()) {
ShopMatchCreateTaskRequest request = parseTaskRequestSilently(task);
if (request == null) {
return;
}
try {
ProductRiskCreateTaskRequest request = objectMapper.readValue(task.getRequestJson(), ProductRiskCreateTaskRequest.class);
String key = ziniaoShopSwitchService.normalizeShopName(vo.getShopName());
for (ProductRiskShopQueueItemVo item : request.getItems()) {
if (item == null) {
continue;
}
if (key.equals(ziniaoShopSwitchService.normalizeShopName(item.getShopName()))) {
vo.setShopName(item.getShopName());
vo.setShopId(item.getShopId());
vo.setPlatform(item.getPlatform());
vo.setCompanyName(item.getCompanyName());
vo.setMatchStatus(item.getMatchStatus());
vo.setMatchMessage(item.getMatchMessage());
break;
}
String key = ziniaoShopSwitchService.normalizeShopName(vo.getShopName());
for (ProductRiskShopQueueItemVo item : request.getItems()) {
if (item == null) {
continue;
}
if (key.equals(ziniaoShopSwitchService.normalizeShopName(item.getShopName()))) {
vo.setShopName(item.getShopName());
vo.setShopId(item.getShopId());
vo.setPlatform(item.getPlatform());
vo.setCompanyName(item.getCompanyName());
vo.setMatchStatus(item.getMatchStatus());
vo.setMatchMessage(item.getMatchMessage());
break;
}
}
}
private ShopMatchCreateTaskRequest parseTaskRequest(FileTaskEntity task) {
ShopMatchCreateTaskRequest request = parseTaskRequestSilently(task);
if (request == null) {
throw new BusinessException("浠诲姟閰嶇疆缂哄け");
}
if (request.getItems() == null) {
request.setItems(new ArrayList<>());
}
if (request.getCountryCodes() == null) {
request.setCountryCodes(new ArrayList<>());
}
if (request.getScheduleTimes() == null) {
request.setScheduleTimes(new ArrayList<>());
}
return request;
}
private ShopMatchCreateTaskRequest parseTaskRequestSilently(FileTaskEntity task) {
if (task == null || task.getRequestJson() == null || task.getRequestJson().isBlank()) {
return null;
}
try {
return objectMapper.readValue(task.getRequestJson(), ShopMatchCreateTaskRequest.class);
} catch (Exception ignored) {
return null;
}
}
private void persistTaskRequest(FileTaskEntity task, ShopMatchCreateTaskRequest request) {
try {
task.setRequestJson(objectMapper.writeValueAsString(request));
} catch (Exception ex) {
throw new BusinessException("Invalid request");
}
}
@@ -580,6 +829,55 @@ public class ShopMatchTaskService {
return new ArrayList<>(byNorm.values());
}
private List<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) {
String value = shopName == null ? "shop" : shopName.trim();
if (value.isBlank()) {
@@ -716,3 +1014,5 @@ public class ShopMatchTaskService {
return row == null || row.getAsin() == null || row.getAsin().trim().isEmpty();
}
}

View File

@@ -13,13 +13,18 @@ import org.springframework.scheduling.annotation.Scheduled;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import java.util.ArrayList;
import java.util.List;
import java.util.Locale;
import java.util.Set;
@Slf4j
@Service
@RequiredArgsConstructor
public class ModuleHistoryCleanupService {
private static final Set<String> TERMINAL_STATUSES = Set.of("SUCCESS", "FAILED", "CANCELLED", "CANCELED");
private final ModuleCleanupProperties moduleCleanupProperties;
private final FileTaskMapper fileTaskMapper;
private final FileResultMapper fileResultMapper;
@@ -35,18 +40,50 @@ public class ModuleHistoryCleanupService {
return;
}
List<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>()
.in(FileResultEntity::getModuleType, moduleTypes));
.in(FileResultEntity::getModuleType, moduleTypes)
.in(FileResultEntity::getTaskId, cleanupTaskIds));
int resetTasks = fileTaskMapper.update(null, new LambdaUpdateWrapper<FileTaskEntity>()
.in(FileTaskEntity::getModuleType, moduleTypes)
.in(FileTaskEntity::getId, cleanupTaskIds)
.set(FileTaskEntity::getResultJson, null)
.set(FileTaskEntity::getRequestJson, null)
.set(FileTaskEntity::getErrorMessage, null));
int deletedTasks = fileTaskMapper.delete(new LambdaQueryWrapper<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));
}
}

View File

@@ -84,6 +84,8 @@ aiimage:
finalize-check-cron: ${AIIMAGE_DELETE_BRAND_FINALIZE_CHECK_CRON:30 */2 * * * *}
product-risk-stale-timeout-minutes: ${AIIMAGE_PRODUCT_RISK_STALE_TIMEOUT_MINUTES:10}
product-risk-initial-timeout-minutes: ${AIIMAGE_PRODUCT_RISK_INITIAL_TIMEOUT_MINUTES:10}
shop-match-stale-timeout-minutes: ${AIIMAGE_SHOP_MATCH_STALE_TIMEOUT_MINUTES:10}
shop-match-initial-timeout-minutes: ${AIIMAGE_SHOP_MATCH_INITIAL_TIMEOUT_MINUTES:10}
module-cleanup:
enabled: ${AIIMAGE_MODULE_CLEANUP_ENABLED:true}
cron: ${AIIMAGE_MODULE_CLEANUP_CRON:0 0 0 * * *}

File diff suppressed because it is too large Load Diff

View File

@@ -640,6 +640,10 @@ export interface ProductRiskTaskSummary {
updatedAt?: string;
finishedAt?: string;
scheduledAt?: string;
countryCodes?: string[];
currentStageIndex?: number;
activeStageIndex?: number;
scheduleStages?: ShopMatchTaskStage[];
}
export interface ProductRiskTaskDetailVo {
@@ -671,6 +675,12 @@ export type ShopMatchTaskDetailVo = ProductRiskTaskDetailVo;
export type ShopMatchTaskBatchVo = ProductRiskTaskBatchVo;
export type ShopMatchCreateTaskVo = ProductRiskCreateTaskVo;
export interface ShopMatchTaskStage {
stageIndex?: number;
scheduledAt?: string;
status?: string;
}
export function getProductRiskDashboard() {
return unwrapJavaResponse(
get<JavaApiResponse<ProductRiskDashboardVo>>(
@@ -851,16 +861,18 @@ export function deleteShopMatchHistory(resultId: number) {
export function createShopMatchTask(
items: ShopMatchShopQueueItem[],
scheduleTime?: string,
countryCodes: string[],
scheduleTimes?: string[],
) {
return unwrapJavaResponse(
post<
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`, {
user_id: getCurrentUserId(),
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(
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,
),
);
}
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[]) {
return unwrapJavaResponse(
post<JavaApiResponse<ShopMatchTaskBatchVo>, { taskIds: number[] }>(

View File

@@ -1,7 +1,12 @@
import { createApp } from 'vue'
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 '@/styles/main.css'
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')