修复BUG,完善匹配店铺
This commit is contained in:
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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")
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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.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();
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
@@ -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));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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 * * *}
|
||||
|
||||
Reference in New Issue
Block a user