提交打包
This commit is contained in:
4
app/.env
4
app/.env
@@ -12,8 +12,8 @@ client_name=ShuFuAI
|
|||||||
|
|
||||||
|
|
||||||
# java_api_base=http://47.111.163.154:18080
|
# java_api_base=http://47.111.163.154:18080
|
||||||
java_api_base=http://127.0.0.1:18080
|
# java_api_base=http://127.0.0.1:18080
|
||||||
# java_api_base=http://8.136.19.173:18080
|
# java_api_base=http://8.136.19.173:18080
|
||||||
# java_api_base=http://121.196.149.225:18080
|
java_api_base=http://121.196.149.225:18080
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -218,15 +218,17 @@ def proxy(path):
|
|||||||
f"{JAVA_API_BASE}/api/product-risk-resolve/tasks/progress/batch",
|
f"{JAVA_API_BASE}/api/product-risk-resolve/tasks/progress/batch",
|
||||||
]
|
]
|
||||||
proxy_timeout = 180 if target_url.endswith("/api/delete-brand/run") else 30
|
proxy_timeout = 180 if target_url.endswith("/api/delete-brand/run") else 30
|
||||||
if target_url not in ignore_url:
|
if target_url not in ignore_url:
|
||||||
try:
|
try:
|
||||||
print("=============================")
|
print("=============================")
|
||||||
print("target_url:",target_url)
|
print("target_url:", target_url)
|
||||||
print("params:",params)
|
print("params:", params)
|
||||||
print("data:",request.get_data())
|
print("data:", request.get_data())
|
||||||
print("=============================")
|
print("method:", request.method)
|
||||||
except Exception as e:
|
print("timeout:", proxy_timeout)
|
||||||
print("打印失败",e)
|
print("=============================")
|
||||||
|
except Exception as e:
|
||||||
|
print("打印失败", e)
|
||||||
|
|
||||||
try:
|
try:
|
||||||
# 使用流式请求
|
# 使用流式请求
|
||||||
@@ -257,12 +259,12 @@ def proxy(path):
|
|||||||
|
|
||||||
return response
|
return response
|
||||||
|
|
||||||
except requests.exceptions.Timeout:
|
except requests.exceptions.Timeout:
|
||||||
print("后端服务超时")
|
print(f"后端服务超时:target_url={target_url}, timeout={proxy_timeout}")
|
||||||
return Response("后端服务超时", status=504)
|
return Response("后端服务超时", status=504)
|
||||||
except requests.exceptions.ConnectionError:
|
except requests.exceptions.ConnectionError as e:
|
||||||
print("无法连接到后端服务")
|
print(f"无法连接到后端服务:target_url={target_url}, error={e}")
|
||||||
return Response("无法连接到后端服务", status=502)
|
return Response("无法连接到后端服务", status=502)
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
print(f"代理请求失败: {e}")
|
print(f"代理请求失败:target_url={target_url}, error={e}")
|
||||||
return Response(f"代理错误: {str(e)}", status=500)
|
return Response(f"代理错误: {str(e)}", status=500)
|
||||||
|
|||||||
@@ -1,14 +1,17 @@
|
|||||||
base_url = "https://api.coze.cn/v1"
|
base_url = "https://api.coze.cn/v1"
|
||||||
coze_token = "sat_12nW40INoJxArrDXbY4lSCoudbqkOYTcphC99BP2efWyzxmsk4q81WDX3ezWgqZ5"
|
coze_token = "sat_12nW40INoJxArrDXbY4lSCoudbqkOYTcphC99BP2efWyzxmsk4q81WDX3ezWgqZ5"
|
||||||
workflow_id = "7608812635877900322"
|
workflow_id = "7608812635877900322"
|
||||||
STITCH_WORKFLOW_ID = "7608813873483300907"
|
STITCH_WORKFLOW_ID = "7608813873483300907"
|
||||||
|
|
||||||
import os
|
import os
|
||||||
from dotenv import load_dotenv
|
from dotenv import load_dotenv
|
||||||
from queue import Queue
|
from queue import Queue
|
||||||
load_dotenv()
|
|
||||||
_base_url = os.getenv("base_url","http://159.75.121.33:15124")
|
|
||||||
base_dir = os.path.dirname(os.path.abspath(__file__))
|
base_dir = os.path.dirname(os.path.abspath(__file__))
|
||||||
|
dotenv_path = os.path.join(base_dir, ".env")
|
||||||
|
load_dotenv(dotenv_path=dotenv_path, override=True)
|
||||||
|
|
||||||
|
_base_url = os.getenv("base_url","http://159.75.121.33:15124")
|
||||||
|
|
||||||
# MySQL 配置
|
# MySQL 配置
|
||||||
mysql_host = os.getenv("mysql_host")
|
mysql_host = os.getenv("mysql_host")
|
||||||
|
|||||||
@@ -3,6 +3,8 @@ package com.nanri.aiimage.modules.appearancepatent.controller;
|
|||||||
import com.nanri.aiimage.common.api.ApiResponse;
|
import com.nanri.aiimage.common.api.ApiResponse;
|
||||||
import com.nanri.aiimage.common.util.DownloadHeaderUtil;
|
import com.nanri.aiimage.common.util.DownloadHeaderUtil;
|
||||||
import com.nanri.aiimage.modules.appearancepatent.model.dto.AppearancePatentParseRequest;
|
import com.nanri.aiimage.modules.appearancepatent.model.dto.AppearancePatentParseRequest;
|
||||||
|
import com.nanri.aiimage.modules.appearancepatent.model.dto.AppearancePatentParsedGroupPageDto;
|
||||||
|
import com.nanri.aiimage.modules.appearancepatent.model.dto.AppearancePatentParsedPayloadDto;
|
||||||
import com.nanri.aiimage.modules.appearancepatent.model.dto.AppearancePatentSubmitResultRequest;
|
import com.nanri.aiimage.modules.appearancepatent.model.dto.AppearancePatentSubmitResultRequest;
|
||||||
import com.nanri.aiimage.modules.appearancepatent.model.dto.AppearancePatentTaskBatchRequest;
|
import com.nanri.aiimage.modules.appearancepatent.model.dto.AppearancePatentTaskBatchRequest;
|
||||||
import com.nanri.aiimage.modules.appearancepatent.model.vo.AppearancePatentDashboardVo;
|
import com.nanri.aiimage.modules.appearancepatent.model.vo.AppearancePatentDashboardVo;
|
||||||
@@ -44,6 +46,34 @@ public class AppearancePatentController {
|
|||||||
return ApiResponse.success(service.parseAndCreateTask(request));
|
return ApiResponse.success(service.parseAndCreateTask(request));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@GetMapping("/tasks/{taskId}/parsed-payload")
|
||||||
|
@Operation(summary = "获取外观专利完整解析载荷", description = "供 Python 队列消费端使用。parse 接口仅返回轻量结果,完整行数据通过本接口按 taskId 拉取。")
|
||||||
|
public ApiResponse<AppearancePatentParsedPayloadDto> parsedPayload(
|
||||||
|
@Parameter(description = "外观专利任务 ID", required = true, example = "7004")
|
||||||
|
@PathVariable Long taskId,
|
||||||
|
@Parameter(description = "当前用户 ID", required = true, example = "1")
|
||||||
|
@RequestParam("user_id") Long userId) {
|
||||||
|
return ApiResponse.success(service.parsedPayload(taskId, userId));
|
||||||
|
}
|
||||||
|
|
||||||
|
@GetMapping("/tasks/{taskId}/queue-payload")
|
||||||
|
@Operation(summary = "获取外观专利 Python 队列载荷", description = "只返回 Python 当前需要的 groups,避免前端搬运完整解析大数据。")
|
||||||
|
public ApiResponse<AppearancePatentParsedPayloadDto> queuePayload(
|
||||||
|
@PathVariable Long taskId,
|
||||||
|
@RequestParam("user_id") Long userId) {
|
||||||
|
return ApiResponse.success(service.queuePayload(taskId, userId));
|
||||||
|
}
|
||||||
|
|
||||||
|
@GetMapping("/tasks/{taskId}/parsed-groups")
|
||||||
|
@Operation(summary = "分页获取外观专利解析分组", description = "供后续优化使用,按页返回解析分组。")
|
||||||
|
public ApiResponse<AppearancePatentParsedGroupPageDto> parsedGroups(
|
||||||
|
@PathVariable Long taskId,
|
||||||
|
@RequestParam("user_id") Long userId,
|
||||||
|
@RequestParam(value = "page", required = false, defaultValue = "1") Integer page,
|
||||||
|
@RequestParam(value = "page_size", required = false, defaultValue = "50") Integer pageSize) {
|
||||||
|
return ApiResponse.success(service.parsedGroupPage(taskId, userId, page, pageSize));
|
||||||
|
}
|
||||||
|
|
||||||
@GetMapping("/dashboard")
|
@GetMapping("/dashboard")
|
||||||
@Operation(summary = "查询外观专利检测总览", description = "查询当前用户的运行中、成功、失败和已结束任务数量。页面进入时请求一次即可,不需要持续轮询。")
|
@Operation(summary = "查询外观专利检测总览", description = "查询当前用户的运行中、成功、失败和已结束任务数量。页面进入时请求一次即可,不需要持续轮询。")
|
||||||
public ApiResponse<AppearancePatentDashboardVo> dashboard(
|
public ApiResponse<AppearancePatentDashboardVo> dashboard(
|
||||||
|
|||||||
@@ -0,0 +1,23 @@
|
|||||||
|
package com.nanri.aiimage.modules.appearancepatent.model.dto;
|
||||||
|
|
||||||
|
import lombok.Data;
|
||||||
|
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
@Data
|
||||||
|
public class AppearancePatentParsedGroupManifestDto {
|
||||||
|
private String aiPrompt;
|
||||||
|
private String apiKey;
|
||||||
|
private Integer pageSize;
|
||||||
|
private Integer totalGroups;
|
||||||
|
private Integer totalRows;
|
||||||
|
private List<PageRef> pages = new ArrayList<>();
|
||||||
|
|
||||||
|
@Data
|
||||||
|
public static class PageRef {
|
||||||
|
private Integer page;
|
||||||
|
private Integer groupCount;
|
||||||
|
private String ref;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,19 @@
|
|||||||
|
package com.nanri.aiimage.modules.appearancepatent.model.dto;
|
||||||
|
|
||||||
|
import com.nanri.aiimage.modules.appearancepatent.model.vo.AppearancePatentParsedGroupVo;
|
||||||
|
import lombok.Data;
|
||||||
|
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
@Data
|
||||||
|
public class AppearancePatentParsedGroupPageDto {
|
||||||
|
private String aiPrompt;
|
||||||
|
private String apiKey;
|
||||||
|
private Integer page;
|
||||||
|
private Integer pageSize;
|
||||||
|
private Integer totalGroups;
|
||||||
|
private Integer totalRows;
|
||||||
|
private Boolean hasNext;
|
||||||
|
private List<AppearancePatentParsedGroupVo> groups = new ArrayList<>();
|
||||||
|
}
|
||||||
@@ -3,9 +3,6 @@ package com.nanri.aiimage.modules.appearancepatent.model.vo;
|
|||||||
import io.swagger.v3.oas.annotations.media.Schema;
|
import io.swagger.v3.oas.annotations.media.Schema;
|
||||||
import lombok.Data;
|
import lombok.Data;
|
||||||
|
|
||||||
import java.util.LinkedHashMap;
|
|
||||||
import java.util.Map;
|
|
||||||
|
|
||||||
@Data
|
@Data
|
||||||
@Schema(description = "外观专利检测解析出的单行数据")
|
@Schema(description = "外观专利检测解析出的单行数据")
|
||||||
public class AppearancePatentParsedRowVo {
|
public class AppearancePatentParsedRowVo {
|
||||||
@@ -36,12 +33,12 @@ public class AppearancePatentParsedRowVo {
|
|||||||
@Schema(description = "国家或站点。", example = "英国")
|
@Schema(description = "国家或站点。", example = "英国")
|
||||||
private String country;
|
private String country;
|
||||||
|
|
||||||
|
@Schema(description = "价格。", example = "12.99")
|
||||||
|
private String price;
|
||||||
|
|
||||||
@Schema(description = "商品图片 URL 或商品 URL,供 Coze 检测使用。", example = "https://webstatic.aiproxy.vip/output/demo.jpg")
|
@Schema(description = "商品图片 URL 或商品 URL,供 Coze 检测使用。", example = "https://webstatic.aiproxy.vip/output/demo.jpg")
|
||||||
private String url;
|
private String url;
|
||||||
|
|
||||||
@Schema(description = "商品标题。", example = "Women Floral Dress Summer Casual")
|
@Schema(description = "商品标题。", example = "Women Floral Dress Summer Casual")
|
||||||
private String title;
|
private String title;
|
||||||
|
|
||||||
@Schema(description = "该 Excel 行的原始列值映射。最终生成 xlsx 时可以从这里读取价格等字段。", example = "{\"id\":\"2_1\",\"asin\":\"B0CJ8SNXXV\",\"国家\":\"英国\",\"价格\":\"12.99\"}")
|
|
||||||
private Map<String, String> values = new LinkedHashMap<>();
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -13,6 +13,7 @@ import com.nanri.aiimage.config.InstanceMetadata;
|
|||||||
import com.nanri.aiimage.config.StorageProperties;
|
import com.nanri.aiimage.config.StorageProperties;
|
||||||
import com.nanri.aiimage.modules.appearancepatent.client.AppearancePatentCozeClient;
|
import com.nanri.aiimage.modules.appearancepatent.client.AppearancePatentCozeClient;
|
||||||
import com.nanri.aiimage.modules.appearancepatent.model.dto.AppearancePatentParseRequest;
|
import com.nanri.aiimage.modules.appearancepatent.model.dto.AppearancePatentParseRequest;
|
||||||
|
import com.nanri.aiimage.modules.appearancepatent.model.dto.AppearancePatentParsedGroupPageDto;
|
||||||
import com.nanri.aiimage.modules.appearancepatent.model.dto.AppearancePatentParsedPayloadDto;
|
import com.nanri.aiimage.modules.appearancepatent.model.dto.AppearancePatentParsedPayloadDto;
|
||||||
import com.nanri.aiimage.modules.appearancepatent.model.dto.AppearancePatentResultGroupDto;
|
import com.nanri.aiimage.modules.appearancepatent.model.dto.AppearancePatentResultGroupDto;
|
||||||
import com.nanri.aiimage.modules.appearancepatent.model.dto.AppearancePatentResultRowDto;
|
import com.nanri.aiimage.modules.appearancepatent.model.dto.AppearancePatentResultRowDto;
|
||||||
@@ -175,7 +176,7 @@ public class AppearancePatentTaskService {
|
|||||||
throw new BusinessException("未解析到有效 ASIN 数据");
|
throw new BusinessException("未解析到有效 ASIN 数据");
|
||||||
}
|
}
|
||||||
long parsedAt = System.nanoTime();
|
long parsedAt = System.nanoTime();
|
||||||
List<AppearancePatentParsedGroupVo> groups = buildParsedGroups(allRows);
|
int groupCount = countParsedGroups(allRows);
|
||||||
long groupedAt = System.nanoTime();
|
long groupedAt = System.nanoTime();
|
||||||
|
|
||||||
FileTaskEntity task = new FileTaskEntity();
|
FileTaskEntity task = new FileTaskEntity();
|
||||||
@@ -201,7 +202,7 @@ public class AppearancePatentTaskService {
|
|||||||
|
|
||||||
String aggregateScopeKey = buildAggregateScopeKey(sourceFiles);
|
String aggregateScopeKey = buildAggregateScopeKey(sourceFiles);
|
||||||
String sourceScopeHash = DigestUtil.sha256Hex(aggregateScopeKey);
|
String sourceScopeHash = DigestUtil.sha256Hex(aggregateScopeKey);
|
||||||
String parsedPayload = buildParsedPayloadJson(request.getAiPrompt(), request.getApiKey(), sourceFiles, mergedHeaders, groups, allRows);
|
String parsedPayload = buildParsedPayloadJson(request.getAiPrompt(), request.getApiKey(), sourceFiles, mergedHeaders, allRows);
|
||||||
long payloadBuiltAt = System.nanoTime();
|
long payloadBuiltAt = System.nanoTime();
|
||||||
String parsedPayloadPointer = storeParsedPayload(task.getId(), sourceScopeHash, parsedPayload);
|
String parsedPayloadPointer = storeParsedPayload(task.getId(), sourceScopeHash, parsedPayload);
|
||||||
long payloadStoredAt = System.nanoTime();
|
long payloadStoredAt = System.nanoTime();
|
||||||
@@ -242,16 +243,16 @@ public class AppearancePatentTaskService {
|
|||||||
vo.setTotalRows(totalRows);
|
vo.setTotalRows(totalRows);
|
||||||
vo.setAcceptedRows(allRows.size());
|
vo.setAcceptedRows(allRows.size());
|
||||||
vo.setDroppedRows(droppedRows);
|
vo.setDroppedRows(droppedRows);
|
||||||
vo.setGroupCount(groups.size());
|
vo.setGroupCount(groupCount);
|
||||||
vo.setAiPrompt(normalize(request.getAiPrompt()));
|
vo.setAiPrompt(normalize(request.getAiPrompt()));
|
||||||
vo.setItems(allRows);
|
vo.setItems(List.of());
|
||||||
vo.setGroups(groups);
|
vo.setGroups(List.of());
|
||||||
long finishedAt = System.nanoTime();
|
long finishedAt = System.nanoTime();
|
||||||
log.info("[appearance-patent] parse timing taskId={} files={} rows={} groups={} totalMs={} parseMs={} groupMs={} taskInsertMs={} payloadJsonMs={} payloadStoreMs={} persistMs={} responseMs={}",
|
log.info("[appearance-patent] parse timing taskId={} files={} rows={} groups={} totalMs={} parseMs={} groupMs={} taskInsertMs={} payloadJsonMs={} payloadStoreMs={} persistMs={} responseMs={}",
|
||||||
task.getId(),
|
task.getId(),
|
||||||
sourceFiles.size(),
|
sourceFiles.size(),
|
||||||
allRows.size(),
|
allRows.size(),
|
||||||
groups.size(),
|
groupCount,
|
||||||
elapsedMs(startedAt, finishedAt),
|
elapsedMs(startedAt, finishedAt),
|
||||||
elapsedMs(parseStartedAt, parsedAt),
|
elapsedMs(parseStartedAt, parsedAt),
|
||||||
elapsedMs(parsedAt, groupedAt),
|
elapsedMs(parsedAt, groupedAt),
|
||||||
@@ -322,15 +323,7 @@ public class AppearancePatentTaskService {
|
|||||||
.map(FileResultEntity::getId)
|
.map(FileResultEntity::getId)
|
||||||
.filter(Objects::nonNull)
|
.filter(Objects::nonNull)
|
||||||
.toList());
|
.toList());
|
||||||
List<FileResultEntity> sortedRows = new ArrayList<>();
|
List<FileResultEntity> sortedRows = new ArrayList<>(rows);
|
||||||
for (FileResultEntity row : rows) {
|
|
||||||
FileTaskEntity task = taskMap.get(row.getTaskId());
|
|
||||||
String taskStatus = task == null ? null : task.getStatus();
|
|
||||||
if (STATUS_PENDING.equals(taskStatus)) {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
sortedRows.add(row);
|
|
||||||
}
|
|
||||||
sortedRows.sort(Comparator
|
sortedRows.sort(Comparator
|
||||||
.comparingInt((FileResultEntity row) -> historyPriority(row, taskMap.get(row.getTaskId()), jobMap.get(row.getId())))
|
.comparingInt((FileResultEntity row) -> historyPriority(row, taskMap.get(row.getTaskId()), jobMap.get(row.getId())))
|
||||||
.thenComparing((FileResultEntity row) -> historyActivityTime(row, taskMap.get(row.getTaskId()), jobMap.get(row.getId())),
|
.thenComparing((FileResultEntity row) -> historyActivityTime(row, taskMap.get(row.getTaskId()), jobMap.get(row.getId())),
|
||||||
@@ -968,6 +961,10 @@ public class AppearancePatentTaskService {
|
|||||||
return groups;
|
return groups;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private int countParsedGroups(List<AppearancePatentParsedRowVo> rows) {
|
||||||
|
return groupRowsByBaseId(rows).size();
|
||||||
|
}
|
||||||
|
|
||||||
private Map<String, List<AppearancePatentParsedRowVo>> groupRowsByBaseId(List<AppearancePatentParsedRowVo> rows) {
|
private Map<String, List<AppearancePatentParsedRowVo>> groupRowsByBaseId(List<AppearancePatentParsedRowVo> rows) {
|
||||||
Map<String, List<AppearancePatentParsedRowVo>> result = new LinkedHashMap<>();
|
Map<String, List<AppearancePatentParsedRowVo>> result = new LinkedHashMap<>();
|
||||||
if (rows == null) {
|
if (rows == null) {
|
||||||
@@ -1997,7 +1994,7 @@ public class AppearancePatentTaskService {
|
|||||||
row.createCell(col++).setCellValue(firstNonBlank(parsedRow.getDisplayId(), parsedRow.getSourceId()));
|
row.createCell(col++).setCellValue(firstNonBlank(parsedRow.getDisplayId(), parsedRow.getSourceId()));
|
||||||
row.createCell(col++).setCellValue(firstNonBlank(parsedRow.getAsin(), ""));
|
row.createCell(col++).setCellValue(firstNonBlank(parsedRow.getAsin(), ""));
|
||||||
row.createCell(col++).setCellValue(firstNonBlank(parsedRow.getCountry(), ""));
|
row.createCell(col++).setCellValue(firstNonBlank(parsedRow.getCountry(), ""));
|
||||||
row.createCell(col++).setCellValue(readValueByHeader(parsedRow, "价格", "price"));
|
row.createCell(col++).setCellValue(firstNonBlank(parsedRow.getPrice(), ""));
|
||||||
row.createCell(col++).setCellValue(resultRow == null ? firstNonBlank(parsedRow.getTitle(), "") : firstNonBlank(resultRow.getTitle(), parsedRow.getTitle()));
|
row.createCell(col++).setCellValue(resultRow == null ? firstNonBlank(parsedRow.getTitle(), "") : firstNonBlank(resultRow.getTitle(), parsedRow.getTitle()));
|
||||||
row.createCell(col++).setCellValue(resultRow == null ? firstNonBlank(parsedRow.getUrl(), "") : firstNonBlank(resultRow.getUrl(), parsedRow.getUrl()));
|
row.createCell(col++).setCellValue(resultRow == null ? firstNonBlank(parsedRow.getUrl(), "") : firstNonBlank(resultRow.getUrl(), parsedRow.getUrl()));
|
||||||
row.createCell(col++).setCellValue(resultRow == null ? missingReason : userFacingCozeCellValue(resultRow, resultRow.getTitleRisk()));
|
row.createCell(col++).setCellValue(resultRow == null ? missingReason : userFacingCozeCellValue(resultRow, resultRow.getTitleRisk()));
|
||||||
@@ -2081,10 +2078,10 @@ public class AppearancePatentTaskService {
|
|||||||
throw new BusinessException("Excel 表头为空");
|
throw new BusinessException("Excel 表头为空");
|
||||||
}
|
}
|
||||||
Map<String, Integer> headerMap = buildHeaderMap(header, formatter);
|
Map<String, Integer> headerMap = buildHeaderMap(header, formatter);
|
||||||
List<String> headers = readHeaders(header, formatter);
|
|
||||||
int idCol = findRequiredHeader(headerMap, "id");
|
int idCol = findRequiredHeader(headerMap, "id");
|
||||||
int asinCol = findRequiredHeader(headerMap, "asin");
|
int asinCol = findRequiredHeader(headerMap, "asin");
|
||||||
int countryCol = findRequiredHeader(headerMap, "国家", "country");
|
int countryCol = findRequiredHeader(headerMap, "国家", "country");
|
||||||
|
int priceCol = findOptionalHeaderExact(headerMap, "价格", "price");
|
||||||
int urlCol = findOptionalHeaderExact(headerMap,
|
int urlCol = findOptionalHeaderExact(headerMap,
|
||||||
"url", "rul", "link", "image", "img", "pic", "picture",
|
"url", "rul", "link", "image", "img", "pic", "picture",
|
||||||
"链接", "商品链接", "图片", "商品图片", "主图", "商品主图", "图片链接", "主图链接");
|
"链接", "商品链接", "图片", "商品图片", "主图", "商品主图", "图片链接", "主图链接");
|
||||||
@@ -2127,16 +2124,16 @@ public class AppearancePatentTaskService {
|
|||||||
vo.setRowToken(buildRowToken(source.getFileKey(), vo.getRowIndex()));
|
vo.setRowToken(buildRowToken(source.getFileKey(), vo.getRowIndex()));
|
||||||
vo.setAsin(asin);
|
vo.setAsin(asin);
|
||||||
vo.setCountry(country);
|
vo.setCountry(country);
|
||||||
|
vo.setPrice(priceCol >= 0 ? cell(row, priceCol, formatter) : "");
|
||||||
vo.setUrl(urlCol >= 0 ? cell(row, urlCol, formatter) : "");
|
vo.setUrl(urlCol >= 0 ? cell(row, urlCol, formatter) : "");
|
||||||
vo.setTitle(titleCol >= 0 ? cell(row, titleCol, formatter) : "");
|
vo.setTitle(titleCol >= 0 ? cell(row, titleCol, formatter) : "");
|
||||||
vo.setValues(readRowValues(row, headers, formatter));
|
|
||||||
allRows.add(vo);
|
allRows.add(vo);
|
||||||
}
|
}
|
||||||
hydratePromptFields(allRows);
|
hydratePromptFields(allRows);
|
||||||
if (allRows.isEmpty()) {
|
if (allRows.isEmpty()) {
|
||||||
throw new BusinessException("no valid appearance patent rows");
|
throw new BusinessException("no valid appearance patent rows");
|
||||||
}
|
}
|
||||||
return new ParsedWorkbook(total, dropped, headers, allRows);
|
return new ParsedWorkbook(total, dropped, List.of(), allRows);
|
||||||
} catch (BusinessException ex) {
|
} catch (BusinessException ex) {
|
||||||
throw ex;
|
throw ex;
|
||||||
} catch (Exception ex) {
|
} catch (Exception ex) {
|
||||||
@@ -2202,23 +2199,6 @@ public class AppearancePatentTaskService {
|
|||||||
return map;
|
return map;
|
||||||
}
|
}
|
||||||
|
|
||||||
private List<String> readHeaders(Row header, DataFormatter formatter) {
|
|
||||||
List<String> headers = new ArrayList<>();
|
|
||||||
for (int i = 0; i < header.getLastCellNum(); i++) {
|
|
||||||
String val = normalize(formatter.formatCellValue(header.getCell(i)));
|
|
||||||
headers.add(val.isBlank() ? "列" + (i + 1) : val);
|
|
||||||
}
|
|
||||||
return headers;
|
|
||||||
}
|
|
||||||
|
|
||||||
private Map<String, String> readRowValues(Row row, List<String> headers, DataFormatter formatter) {
|
|
||||||
Map<String, String> values = new LinkedHashMap<>();
|
|
||||||
for (int i = 0; i < headers.size(); i++) {
|
|
||||||
values.put(headers.get(i), cell(row, i, formatter));
|
|
||||||
}
|
|
||||||
return values;
|
|
||||||
}
|
|
||||||
|
|
||||||
private int findRequiredHeader(Map<String, Integer> map, String... names) {
|
private int findRequiredHeader(Map<String, Integer> map, String... names) {
|
||||||
int idx = findOptionalHeader(map, names);
|
int idx = findOptionalHeader(map, names);
|
||||||
if (idx < 0) {
|
if (idx < 0) {
|
||||||
@@ -2368,7 +2348,7 @@ public class AppearancePatentTaskService {
|
|||||||
|
|
||||||
private int historyPriority(FileResultEntity row, FileTaskEntity task, TaskFileJobEntity job) {
|
private int historyPriority(FileResultEntity row, FileTaskEntity task, TaskFileJobEntity job) {
|
||||||
String taskStatus = task == null ? null : task.getStatus();
|
String taskStatus = task == null ? null : task.getStatus();
|
||||||
if (STATUS_RUNNING.equals(taskStatus) || isHistoryFileBuilding(row, taskStatus, job)) {
|
if (STATUS_PENDING.equals(taskStatus) || STATUS_RUNNING.equals(taskStatus) || isHistoryFileBuilding(row, taskStatus, job)) {
|
||||||
return 0;
|
return 0;
|
||||||
}
|
}
|
||||||
return 1;
|
return 1;
|
||||||
@@ -2476,15 +2456,15 @@ public class AppearancePatentTaskService {
|
|||||||
return val == null ? "" : val.replace(String.valueOf((char) 0xFEFF), "").replace((char) 0x3000, ' ').trim().replaceAll("\\s+", " ");
|
return val == null ? "" : val.replace(String.valueOf((char) 0xFEFF), "").replace((char) 0x3000, ' ').trim().replaceAll("\\s+", " ");
|
||||||
}
|
}
|
||||||
|
|
||||||
private String buildParsedPayloadJson(String aiPrompt, String apiKey, List<AppearancePatentSourceFileDto> sourceFiles, List<String> headers, List<AppearancePatentParsedGroupVo> groups, List<AppearancePatentParsedRowVo> allRows) {
|
private String buildParsedPayloadJson(String aiPrompt, String apiKey, List<AppearancePatentSourceFileDto> sourceFiles, List<String> headers, List<AppearancePatentParsedRowVo> allRows) {
|
||||||
AppearancePatentParsedPayloadDto payload = new AppearancePatentParsedPayloadDto();
|
AppearancePatentParsedPayloadDto payload = new AppearancePatentParsedPayloadDto();
|
||||||
payload.setAiPrompt(normalize(aiPrompt));
|
payload.setAiPrompt(normalize(aiPrompt));
|
||||||
payload.setApiKey(normalize(apiKey));
|
payload.setApiKey(normalize(apiKey));
|
||||||
payload.setSourceFiles(sourceFiles == null ? List.of() : sourceFiles);
|
payload.setSourceFiles(sourceFiles == null ? List.of() : sourceFiles);
|
||||||
payload.setHeaders(headers == null ? List.of() : headers);
|
payload.setHeaders(headers == null ? List.of() : headers);
|
||||||
payload.setItems(List.of());
|
payload.setItems(List.of());
|
||||||
payload.setGroups(groups == null ? List.of() : groups);
|
payload.setGroups(List.of());
|
||||||
payload.setAllItems(List.of());
|
payload.setAllItems(allRows == null ? List.of() : allRows);
|
||||||
return writeJson(payload, "保存解析结果失败");
|
return writeJson(payload, "保存解析结果失败");
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -2552,6 +2532,57 @@ public class AppearancePatentTaskService {
|
|||||||
return hydrateParsedPayloadRows(new AppearancePatentParsedPayloadDto());
|
return hydrateParsedPayloadRows(new AppearancePatentParsedPayloadDto());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public AppearancePatentParsedPayloadDto parsedPayload(Long taskId, Long userId) {
|
||||||
|
FileTaskEntity task = fileTaskMapper.selectById(taskId);
|
||||||
|
if (task == null || !MODULE_TYPE.equals(task.getModuleType()) || userId != null && !Objects.equals(userId, task.getUserId())) {
|
||||||
|
throw new BusinessException("任务不存在");
|
||||||
|
}
|
||||||
|
return readParsedPayload(task);
|
||||||
|
}
|
||||||
|
|
||||||
|
public AppearancePatentParsedPayloadDto queuePayload(Long taskId, Long userId) {
|
||||||
|
FileTaskEntity task = fileTaskMapper.selectById(taskId);
|
||||||
|
if (task == null || !MODULE_TYPE.equals(task.getModuleType()) || userId != null && !Objects.equals(userId, task.getUserId())) {
|
||||||
|
throw new BusinessException("任务不存在");
|
||||||
|
}
|
||||||
|
AppearancePatentParsedPayloadDto payload = readParsedPayload(task);
|
||||||
|
AppearancePatentParsedPayloadDto queuePayload = new AppearancePatentParsedPayloadDto();
|
||||||
|
queuePayload.setAiPrompt(payload.getAiPrompt());
|
||||||
|
queuePayload.setApiKey(payload.getApiKey());
|
||||||
|
queuePayload.setGroups(payload.getGroups() == null ? List.of() : payload.getGroups());
|
||||||
|
queuePayload.setItems(List.of());
|
||||||
|
queuePayload.setAllItems(List.of());
|
||||||
|
queuePayload.setHeaders(List.of());
|
||||||
|
queuePayload.setSourceFiles(List.of());
|
||||||
|
return queuePayload;
|
||||||
|
}
|
||||||
|
|
||||||
|
public AppearancePatentParsedGroupPageDto parsedGroupPage(Long taskId, Long userId, Integer page, Integer pageSize) {
|
||||||
|
FileTaskEntity task = fileTaskMapper.selectById(taskId);
|
||||||
|
if (task == null || !MODULE_TYPE.equals(task.getModuleType()) || userId != null && !Objects.equals(userId, task.getUserId())) {
|
||||||
|
throw new BusinessException("任务不存在");
|
||||||
|
}
|
||||||
|
int safePage = Math.max(1, page == null ? 1 : page);
|
||||||
|
int safePageSize = Math.max(1, Math.min(pageSize == null ? 50 : pageSize, 200));
|
||||||
|
AppearancePatentParsedPayloadDto payload = readParsedPayload(task);
|
||||||
|
List<AppearancePatentParsedGroupVo> groups = payload.getGroups() == null ? List.of() : payload.getGroups();
|
||||||
|
int totalGroups = groups.size();
|
||||||
|
int totalRows = payload.getAllItems() == null ? 0 : payload.getAllItems().size();
|
||||||
|
int fromIndex = Math.min((safePage - 1) * safePageSize, totalGroups);
|
||||||
|
int toIndex = Math.min(fromIndex + safePageSize, totalGroups);
|
||||||
|
|
||||||
|
AppearancePatentParsedGroupPageDto vo = new AppearancePatentParsedGroupPageDto();
|
||||||
|
vo.setAiPrompt(payload.getAiPrompt());
|
||||||
|
vo.setApiKey(payload.getApiKey());
|
||||||
|
vo.setPage(safePage);
|
||||||
|
vo.setPageSize(safePageSize);
|
||||||
|
vo.setTotalGroups(totalGroups);
|
||||||
|
vo.setTotalRows(totalRows);
|
||||||
|
vo.setHasNext(toIndex < totalGroups);
|
||||||
|
vo.setGroups(fromIndex >= toIndex ? List.of() : groups.subList(fromIndex, toIndex));
|
||||||
|
return vo;
|
||||||
|
}
|
||||||
|
|
||||||
private AppearancePatentParsedPayloadDto hydrateParsedPayloadRows(AppearancePatentParsedPayloadDto payload) {
|
private AppearancePatentParsedPayloadDto hydrateParsedPayloadRows(AppearancePatentParsedPayloadDto payload) {
|
||||||
if (payload == null) {
|
if (payload == null) {
|
||||||
return new AppearancePatentParsedPayloadDto();
|
return new AppearancePatentParsedPayloadDto();
|
||||||
@@ -2575,6 +2606,9 @@ public class AppearancePatentTaskService {
|
|||||||
if (payload.getItems() == null || payload.getItems().isEmpty()) {
|
if (payload.getItems() == null || payload.getItems().isEmpty()) {
|
||||||
payload.setItems(rows);
|
payload.setItems(rows);
|
||||||
}
|
}
|
||||||
|
if ((payload.getGroups() == null || payload.getGroups().isEmpty()) && !rows.isEmpty()) {
|
||||||
|
payload.setGroups(buildParsedGroups(rows));
|
||||||
|
}
|
||||||
return payload;
|
return payload;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -2656,22 +2690,6 @@ public class AppearancePatentTaskService {
|
|||||||
return normalize(id) + "::" + normalize(asin).toUpperCase(Locale.ROOT) + "::" + normalize(country);
|
return normalize(id) + "::" + normalize(asin).toUpperCase(Locale.ROOT) + "::" + normalize(country);
|
||||||
}
|
}
|
||||||
|
|
||||||
private String readValueByHeader(AppearancePatentParsedRowVo row, String... candidates) {
|
|
||||||
if (row == null || row.getValues() == null || row.getValues().isEmpty() || candidates == null) {
|
|
||||||
return "";
|
|
||||||
}
|
|
||||||
for (Map.Entry<String, String> entry : row.getValues().entrySet()) {
|
|
||||||
String header = normalize(entry.getKey()).toLowerCase(Locale.ROOT);
|
|
||||||
for (String candidate : candidates) {
|
|
||||||
String expected = normalize(candidate).toLowerCase(Locale.ROOT);
|
|
||||||
if (!expected.isBlank() && header.contains(expected)) {
|
|
||||||
return entry.getValue() == null ? "" : entry.getValue();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return "";
|
|
||||||
}
|
|
||||||
|
|
||||||
private String userFacingCozeCellValue(AppearancePatentResultRowDto row, String value) {
|
private String userFacingCozeCellValue(AppearancePatentResultRowDto row, String value) {
|
||||||
String normalizedValue = normalize(value);
|
String normalizedValue = normalize(value);
|
||||||
if (!normalizedValue.isBlank() && !isTechnicalCozeFailure(normalizedValue)) {
|
if (!normalizedValue.isBlank() && !isTechnicalCozeFailure(normalizedValue)) {
|
||||||
|
|||||||
@@ -221,107 +221,113 @@ public class BrandTaskService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
public void submitCrawlResult(Long taskId, BrandCrawlResultRequest request) {
|
public void submitCrawlResult(Long taskId, BrandCrawlResultRequest request) {
|
||||||
long startedAt = System.currentTimeMillis();
|
if (taskId == null || taskId <= 0) {
|
||||||
BrandCrawlTaskEntity task = requireActiveTask(taskId);
|
throw new BusinessException("taskId invalid");
|
||||||
List<BrandSourceFileDto> sourceFiles = parseSourceFiles(task.getFilePaths());
|
|
||||||
if (sourceFiles.isEmpty()) {
|
|
||||||
throw new BusinessException("任务没有源文件");
|
|
||||||
}
|
}
|
||||||
|
try (TaskDistributedLockService.LockHandle ignored = acquireTaskLockOrThrow(taskId, TaskDistributedLockService.DEFAULT_WAIT_MILLIS)) {
|
||||||
Map<String, BrandSourceFileDto> sourceByUrl = new LinkedHashMap<>();
|
long startedAt = System.currentTimeMillis();
|
||||||
Map<String, Integer> sourceIndexByUrl = new LinkedHashMap<>();
|
BrandCrawlTaskEntity task = requireActiveTask(taskId);
|
||||||
for (int i = 0; i < sourceFiles.size(); i++) {
|
List<BrandSourceFileDto> sourceFiles = parseSourceFiles(task.getFilePaths());
|
||||||
BrandSourceFileDto file = sourceFiles.get(i);
|
if (sourceFiles.isEmpty()) {
|
||||||
sourceByUrl.put(file.getFileUrl(), file);
|
throw new BusinessException("task source files missing");
|
||||||
sourceIndexByUrl.put(file.getFileUrl(), i + 1);
|
|
||||||
}
|
|
||||||
List<BrandParsedFileCacheDto> cachedFiles = brandTaskStorageService.getParsedPayload(taskId);
|
|
||||||
if (cachedFiles == null || cachedFiles.isEmpty()) {
|
|
||||||
throw new BusinessException("任务原始数据已过期,请重新创建任务");
|
|
||||||
}
|
|
||||||
Map<String, BrandParsedFileCacheDto> cachedByUrl = new LinkedHashMap<>();
|
|
||||||
for (BrandParsedFileCacheDto cachedFile : cachedFiles) {
|
|
||||||
cachedByUrl.put(cachedFile.getFileUrl(), cachedFile);
|
|
||||||
}
|
|
||||||
|
|
||||||
List<BrandCrawlResultFileDto> resultFiles = request.getFiles() == null ? List.of() : request.getFiles();
|
|
||||||
if (resultFiles.isEmpty()) {
|
|
||||||
throw new BusinessException("files 不能为空");
|
|
||||||
}
|
|
||||||
ensureNoDuplicateResultFiles(resultFiles);
|
|
||||||
|
|
||||||
int totalCount = sourceFiles.size();
|
|
||||||
markTaskRunning(taskId, totalCount);
|
|
||||||
afterTaskStarted(taskId, totalCount);
|
|
||||||
|
|
||||||
log.info("[brand-submit] taskId={} chunks={} files={} thread={}",
|
|
||||||
taskId,
|
|
||||||
resultFiles.size(),
|
|
||||||
totalCount,
|
|
||||||
Thread.currentThread().getName());
|
|
||||||
|
|
||||||
int finishedCount = brandTaskStorageService.countCompletedFiles(taskId);
|
|
||||||
for (BrandCrawlResultFileDto resultFile : resultFiles) {
|
|
||||||
String fileUrl = blankToNull(resultFile.getFileUrl());
|
|
||||||
if (fileUrl == null) {
|
|
||||||
throw new BusinessException("fileUrl 不能为空");
|
|
||||||
}
|
}
|
||||||
BrandSourceFileDto sourceFile = sourceByUrl.get(fileUrl);
|
|
||||||
if (sourceFile == null) {
|
Map<String, BrandSourceFileDto> sourceByUrl = new LinkedHashMap<>();
|
||||||
throw new BusinessException("存在未知 fileUrl: " + fileUrl);
|
Map<String, Integer> sourceIndexByUrl = new LinkedHashMap<>();
|
||||||
|
for (int i = 0; i < sourceFiles.size(); i++) {
|
||||||
|
BrandSourceFileDto file = sourceFiles.get(i);
|
||||||
|
sourceByUrl.put(file.getFileUrl(), file);
|
||||||
|
sourceIndexByUrl.put(file.getFileUrl(), i + 1);
|
||||||
}
|
}
|
||||||
BrandParsedFileCacheDto cachedFile = cachedByUrl.get(fileUrl);
|
List<BrandParsedFileCacheDto> cachedFiles = brandTaskStorageService.getParsedPayload(taskId);
|
||||||
if (cachedFile == null) {
|
if (cachedFiles == null || cachedFiles.isEmpty()) {
|
||||||
throw new BusinessException("缺少原始缓存数据: " + fileUrl);
|
throw new BusinessException("task parsed payload missing");
|
||||||
}
|
}
|
||||||
int totalLines = cachedFile.getRows() == null ? 0 : cachedFile.getRows().size();
|
Map<String, BrandParsedFileCacheDto> cachedByUrl = new LinkedHashMap<>();
|
||||||
if (resultFile.getTotalLines() == null || resultFile.getTotalLines() <= 0) {
|
for (BrandParsedFileCacheDto cachedFile : cachedFiles) {
|
||||||
resultFile.setTotalLines(totalLines);
|
cachedByUrl.put(cachedFile.getFileUrl(), cachedFile);
|
||||||
} else if (totalLines > 0 && resultFile.getTotalLines() > totalLines) {
|
|
||||||
throw new BusinessException("totalLines 超过原始文件行数: " + fileUrl);
|
|
||||||
}
|
}
|
||||||
BrandTaskStorageService.ChunkStoreResult storeResult = brandTaskStorageService.storeChunk(taskId, resultFile);
|
|
||||||
BrandTaskStorageService.ChunkStoreResult refreshResult = brandTaskStorageService.refreshFileAggregate(taskId, fileUrl);
|
List<BrandCrawlResultFileDto> resultFiles = request.getFiles() == null ? List.of() : request.getFiles();
|
||||||
BrandFileAggregateCacheDto aggregate = refreshResult.aggregate() == null ? storeResult.aggregate() : refreshResult.aggregate();
|
if (resultFiles.isEmpty()) {
|
||||||
finishedCount = Math.max(finishedCount, storeResult.finishedFiles());
|
throw new BusinessException("files is empty");
|
||||||
finishedCount = Math.max(finishedCount, refreshResult.finishedFiles());
|
}
|
||||||
log.info("[brand-submit] taskId={} fileUrl={} chunk={}/{} stored={} newlyCompletedFile={} finishedFiles={} receivedChunks={} processedLines={} totalLines={}",
|
ensureNoDuplicateResultFiles(resultFiles);
|
||||||
|
|
||||||
|
int totalCount = sourceFiles.size();
|
||||||
|
markTaskRunning(taskId, totalCount);
|
||||||
|
afterTaskStarted(taskId, totalCount);
|
||||||
|
|
||||||
|
log.info("[brand-submit] taskId={} chunks={} files={} thread={}",
|
||||||
taskId,
|
taskId,
|
||||||
fileUrl,
|
resultFiles.size(),
|
||||||
resultFile.getChunkIndex(),
|
|
||||||
resultFile.getChunkTotal(),
|
|
||||||
storeResult.stored(),
|
|
||||||
storeResult.newlyCompletedFile() || refreshResult.newlyCompletedFile(),
|
|
||||||
finishedCount,
|
|
||||||
aggregate == null ? 0 : defaultInteger(aggregate.getReceivedChunkCount()),
|
|
||||||
aggregate == null ? 0 : defaultInteger(aggregate.getProcessedLineCount()),
|
|
||||||
aggregate == null ? 0 : defaultInteger(aggregate.getTotalLines()));
|
|
||||||
String fileName = blankToDefault(sourceFile.getOriginalFilename(), sourceFile.getFileUrl());
|
|
||||||
int fileIndex = sourceIndexByUrl.getOrDefault(fileUrl, 0);
|
|
||||||
int currentLine = aggregate == null ? 0 : defaultInteger(aggregate.getProcessedLineCount());
|
|
||||||
int currentTotalLines = aggregate == null ? 0 : Math.max(defaultInteger(aggregate.getTotalLines()), currentLine);
|
|
||||||
brandTaskProgressCacheService.saveProgressFromResult(taskId,
|
|
||||||
fileUrl,
|
|
||||||
fileIndex,
|
|
||||||
totalCount,
|
totalCount,
|
||||||
fileName,
|
Thread.currentThread().getName());
|
||||||
currentLine,
|
|
||||||
currentTotalLines,
|
|
||||||
finishedCount);
|
|
||||||
updateTaskProgress(taskId, finishedCount, totalCount);
|
|
||||||
saveBrandProgressSnapshot(taskId, STATUS_RUNNING, totalCount, finishedCount, 0, null);
|
|
||||||
}
|
|
||||||
|
|
||||||
tryFinalizeTask(taskId, request.getStrategy(), sourceFiles, cachedByUrl, finishedCount, totalCount);
|
int finishedCount = brandTaskStorageService.countCompletedFiles(taskId);
|
||||||
log.info("[brand-submit] taskId={} finishedFiles={}/{} elapsedMs={} thread={}",
|
for (BrandCrawlResultFileDto resultFile : resultFiles) {
|
||||||
taskId,
|
String fileUrl = blankToNull(resultFile.getFileUrl());
|
||||||
finishedCount,
|
if (fileUrl == null) {
|
||||||
totalCount,
|
throw new BusinessException("fileUrl is blank");
|
||||||
System.currentTimeMillis() - startedAt,
|
}
|
||||||
Thread.currentThread().getName());
|
BrandSourceFileDto sourceFile = sourceByUrl.get(fileUrl);
|
||||||
|
if (sourceFile == null) {
|
||||||
|
throw new BusinessException("unknown fileUrl: " + fileUrl);
|
||||||
|
}
|
||||||
|
BrandParsedFileCacheDto cachedFile = cachedByUrl.get(fileUrl);
|
||||||
|
if (cachedFile == null) {
|
||||||
|
throw new BusinessException("parsed cache missing: " + fileUrl);
|
||||||
|
}
|
||||||
|
int totalLines = cachedFile.getRows() == null ? 0 : cachedFile.getRows().size();
|
||||||
|
if (resultFile.getTotalLines() == null || resultFile.getTotalLines() <= 0) {
|
||||||
|
resultFile.setTotalLines(totalLines);
|
||||||
|
} else if (totalLines > 0 && resultFile.getTotalLines() > totalLines) {
|
||||||
|
throw new BusinessException("totalLines exceeds source rows: " + fileUrl);
|
||||||
|
}
|
||||||
|
BrandTaskStorageService.ChunkStoreResult storeResult = brandTaskStorageService.storeChunk(taskId, resultFile);
|
||||||
|
BrandTaskStorageService.ChunkStoreResult refreshResult = brandTaskStorageService.refreshFileAggregate(taskId, fileUrl);
|
||||||
|
BrandFileAggregateCacheDto aggregate = refreshResult.aggregate() == null ? storeResult.aggregate() : refreshResult.aggregate();
|
||||||
|
finishedCount = Math.max(finishedCount, storeResult.finishedFiles());
|
||||||
|
finishedCount = Math.max(finishedCount, refreshResult.finishedFiles());
|
||||||
|
log.info("[brand-submit] taskId={} fileUrl={} chunk={}/{} stored={} newlyCompletedFile={} finishedFiles={} receivedChunks={} processedLines={} totalLines={}",
|
||||||
|
taskId,
|
||||||
|
fileUrl,
|
||||||
|
resultFile.getChunkIndex(),
|
||||||
|
resultFile.getChunkTotal(),
|
||||||
|
storeResult.stored(),
|
||||||
|
storeResult.newlyCompletedFile() || refreshResult.newlyCompletedFile(),
|
||||||
|
finishedCount,
|
||||||
|
aggregate == null ? 0 : defaultInteger(aggregate.getReceivedChunkCount()),
|
||||||
|
aggregate == null ? 0 : defaultInteger(aggregate.getProcessedLineCount()),
|
||||||
|
aggregate == null ? 0 : defaultInteger(aggregate.getTotalLines()));
|
||||||
|
String fileName = blankToDefault(sourceFile.getOriginalFilename(), sourceFile.getFileUrl());
|
||||||
|
int fileIndex = sourceIndexByUrl.getOrDefault(fileUrl, 0);
|
||||||
|
int currentLine = aggregate == null ? 0 : defaultInteger(aggregate.getProcessedLineCount());
|
||||||
|
int currentTotalLines = aggregate == null ? 0 : Math.max(defaultInteger(aggregate.getTotalLines()), currentLine);
|
||||||
|
brandTaskProgressCacheService.saveProgressFromResult(taskId,
|
||||||
|
fileUrl,
|
||||||
|
fileIndex,
|
||||||
|
totalCount,
|
||||||
|
fileName,
|
||||||
|
currentLine,
|
||||||
|
currentTotalLines,
|
||||||
|
finishedCount);
|
||||||
|
updateTaskProgress(taskId, finishedCount, totalCount);
|
||||||
|
saveBrandProgressSnapshot(taskId, STATUS_RUNNING, totalCount, finishedCount, 0, null);
|
||||||
|
}
|
||||||
|
|
||||||
|
tryFinalizeTask(taskId, request.getStrategy(), sourceFiles, cachedByUrl, finishedCount, totalCount);
|
||||||
|
log.info("[brand-submit] taskId={} finishedFiles={}/{} elapsedMs={} thread={}",
|
||||||
|
taskId,
|
||||||
|
finishedCount,
|
||||||
|
totalCount,
|
||||||
|
System.currentTimeMillis() - startedAt,
|
||||||
|
Thread.currentThread().getName());
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public void cancelTask(Long taskId) {
|
public void cancelTask(Long taskId) {
|
||||||
|
try (TaskDistributedLockService.LockHandle ignored = acquireTaskLockOrThrow(taskId, TaskDistributedLockService.DEFAULT_WAIT_MILLIS)) {
|
||||||
requireTask(taskId);
|
requireTask(taskId);
|
||||||
int updated = brandCrawlTaskMapper.update(null, new LambdaUpdateWrapper<BrandCrawlTaskEntity>()
|
int updated = brandCrawlTaskMapper.update(null, new LambdaUpdateWrapper<BrandCrawlTaskEntity>()
|
||||||
.eq(BrandCrawlTaskEntity::getId, taskId)
|
.eq(BrandCrawlTaskEntity::getId, taskId)
|
||||||
@@ -332,9 +338,11 @@ public class BrandTaskService {
|
|||||||
}
|
}
|
||||||
brandTaskStorageService.deleteTaskData(taskId);
|
brandTaskStorageService.deleteTaskData(taskId);
|
||||||
brandTaskProgressCacheService.delete(taskId);
|
brandTaskProgressCacheService.delete(taskId);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public void deleteTask(Long taskId) {
|
public void deleteTask(Long taskId) {
|
||||||
|
try (TaskDistributedLockService.LockHandle ignored = acquireTaskLockOrThrow(taskId, TaskDistributedLockService.DEFAULT_WAIT_MILLIS)) {
|
||||||
requireTask(taskId);
|
requireTask(taskId);
|
||||||
int deleted = brandCrawlTaskMapper.delete(new LambdaQueryWrapper<BrandCrawlTaskEntity>()
|
int deleted = brandCrawlTaskMapper.delete(new LambdaQueryWrapper<BrandCrawlTaskEntity>()
|
||||||
.eq(BrandCrawlTaskEntity::getId, taskId)
|
.eq(BrandCrawlTaskEntity::getId, taskId)
|
||||||
@@ -344,6 +352,7 @@ public class BrandTaskService {
|
|||||||
}
|
}
|
||||||
brandTaskStorageService.deleteTaskData(taskId);
|
brandTaskStorageService.deleteTaskData(taskId);
|
||||||
brandTaskProgressCacheService.delete(taskId);
|
brandTaskProgressCacheService.delete(taskId);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public String resolveDownloadUrl(Long taskId) {
|
public String resolveDownloadUrl(Long taskId) {
|
||||||
@@ -1253,6 +1262,14 @@ public class BrandTaskService {
|
|||||||
return lockHandle;
|
return lockHandle;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private TaskDistributedLockService.LockHandle acquireTaskLockOrThrow(Long taskId, long waitMillis) {
|
||||||
|
TaskDistributedLockService.LockHandle lockHandle = acquireTaskLock(taskId, waitMillis);
|
||||||
|
if (lockHandle == null) {
|
||||||
|
throw new BusinessException("task lock is busy");
|
||||||
|
}
|
||||||
|
return lockHandle;
|
||||||
|
}
|
||||||
|
|
||||||
private boolean tryRecoverCompletedRunningTask(BrandCrawlTaskEntity task) {
|
private boolean tryRecoverCompletedRunningTask(BrandCrawlTaskEntity task) {
|
||||||
if (task == null || task.getId() == null) {
|
if (task == null || task.getId() == null) {
|
||||||
return false;
|
return false;
|
||||||
|
|||||||
@@ -35,6 +35,7 @@ import com.nanri.aiimage.modules.task.mapper.FileTaskMapper;
|
|||||||
import com.nanri.aiimage.modules.task.model.entity.FileResultEntity;
|
import com.nanri.aiimage.modules.task.model.entity.FileResultEntity;
|
||||||
import com.nanri.aiimage.modules.task.model.entity.FileTaskEntity;
|
import com.nanri.aiimage.modules.task.model.entity.FileTaskEntity;
|
||||||
import com.nanri.aiimage.modules.task.model.entity.TaskFileJobEntity;
|
import com.nanri.aiimage.modules.task.model.entity.TaskFileJobEntity;
|
||||||
|
import com.nanri.aiimage.modules.task.service.TaskDistributedLockService;
|
||||||
import com.nanri.aiimage.modules.task.service.TaskFileJobService;
|
import com.nanri.aiimage.modules.task.service.TaskFileJobService;
|
||||||
import com.nanri.aiimage.modules.ziniao.model.vo.ZiniaoShopMatchResultVo;
|
import com.nanri.aiimage.modules.ziniao.model.vo.ZiniaoShopMatchResultVo;
|
||||||
import com.nanri.aiimage.modules.ziniao.service.ZiniaoShopIndexService;
|
import com.nanri.aiimage.modules.ziniao.service.ZiniaoShopIndexService;
|
||||||
@@ -54,6 +55,7 @@ import org.springframework.transaction.annotation.Transactional;
|
|||||||
import java.io.File;
|
import java.io.File;
|
||||||
import java.io.FileInputStream;
|
import java.io.FileInputStream;
|
||||||
import java.io.FileOutputStream;
|
import java.io.FileOutputStream;
|
||||||
|
import java.time.Duration;
|
||||||
import java.time.LocalDateTime;
|
import java.time.LocalDateTime;
|
||||||
import java.util.ArrayList;
|
import java.util.ArrayList;
|
||||||
import java.util.Comparator;
|
import java.util.Comparator;
|
||||||
@@ -71,6 +73,8 @@ public class DeleteBrandRunService {
|
|||||||
|
|
||||||
private static final String MODULE_TYPE = "DELETE_BRAND";
|
private static final String MODULE_TYPE = "DELETE_BRAND";
|
||||||
private static final String CONTENT_TYPE_XLSX = "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet";
|
private static final String CONTENT_TYPE_XLSX = "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet";
|
||||||
|
private static final Duration TASK_LOCK_TTL = TaskDistributedLockService.DEFAULT_LOCK_TTL;
|
||||||
|
private static final long TASK_LOCK_WAIT_MILLIS = TaskDistributedLockService.DEFAULT_WAIT_MILLIS;
|
||||||
|
|
||||||
private final FileTaskMapper fileTaskMapper;
|
private final FileTaskMapper fileTaskMapper;
|
||||||
private final FileResultMapper fileResultMapper;
|
private final FileResultMapper fileResultMapper;
|
||||||
@@ -82,6 +86,7 @@ public class DeleteBrandRunService {
|
|||||||
private final ObjectMapper objectMapper;
|
private final ObjectMapper objectMapper;
|
||||||
private final DeleteBrandProgressProperties deleteBrandProgressProperties;
|
private final DeleteBrandProgressProperties deleteBrandProgressProperties;
|
||||||
private final TaskPressureProperties taskPressureProperties;
|
private final TaskPressureProperties taskPressureProperties;
|
||||||
|
private final TaskDistributedLockService taskDistributedLockService;
|
||||||
private final TaskFileJobService taskFileJobService;
|
private final TaskFileJobService taskFileJobService;
|
||||||
|
|
||||||
private boolean isUsableIndexedMatch(ZiniaoShopMatchResultVo matchResult) {
|
private boolean isUsableIndexedMatch(ZiniaoShopMatchResultVo matchResult) {
|
||||||
@@ -493,13 +498,15 @@ public class DeleteBrandRunService {
|
|||||||
throw new BusinessException("记录不存在");
|
throw new BusinessException("记录不存在");
|
||||||
}
|
}
|
||||||
Long taskId = entity.getTaskId();
|
Long taskId = entity.getTaskId();
|
||||||
fileResultMapper.deleteById(resultId);
|
try (TaskDistributedLockService.LockHandle ignored = acquireTaskLockOrThrow(taskId)) {
|
||||||
if (taskId != null && taskId > 0) {
|
fileResultMapper.deleteById(resultId);
|
||||||
Long remaining = fileResultMapper.selectCount(new LambdaQueryWrapper<FileResultEntity>()
|
if (taskId != null && taskId > 0) {
|
||||||
.eq(FileResultEntity::getTaskId, taskId)
|
Long remaining = fileResultMapper.selectCount(new LambdaQueryWrapper<FileResultEntity>()
|
||||||
.eq(FileResultEntity::getModuleType, MODULE_TYPE));
|
.eq(FileResultEntity::getTaskId, taskId)
|
||||||
if (remaining == null || remaining <= 0) {
|
.eq(FileResultEntity::getModuleType, MODULE_TYPE));
|
||||||
deleteBrandTaskStorageService.deleteTaskData(taskId);
|
if (remaining == null || remaining <= 0) {
|
||||||
|
deleteBrandTaskStorageService.deleteTaskData(taskId);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1249,99 +1256,96 @@ public class DeleteBrandRunService {
|
|||||||
|
|
||||||
@Transactional
|
@Transactional
|
||||||
public void submitResult(Long taskId, DeleteBrandSubmitResultRequest request) {
|
public void submitResult(Long taskId, DeleteBrandSubmitResultRequest request) {
|
||||||
log.info("[DeleteBrand] Received submitResult for taskId: {}, files count: {}", taskId, request == null || request.getFiles() == null ? 0 : request.getFiles().size());
|
|
||||||
|
|
||||||
if (taskId == null || taskId <= 0) {
|
if (taskId == null || taskId <= 0) {
|
||||||
throw new BusinessException("taskId 不合法");
|
throw new BusinessException("taskId invalid");
|
||||||
}
|
|
||||||
if (request == null || request.getFiles() == null || request.getFiles().isEmpty()) {
|
|
||||||
throw new BusinessException("files 不能为空");
|
|
||||||
}
|
}
|
||||||
|
try (TaskDistributedLockService.LockHandle ignored = acquireTaskLockOrThrow(taskId, TASK_LOCK_WAIT_MILLIS)) {
|
||||||
|
log.info("[DeleteBrand] Received submitResult for taskId: {}, files count: {}", taskId, request == null || request.getFiles() == null ? 0 : request.getFiles().size());
|
||||||
|
|
||||||
FileTaskEntity task = loadTaskForExecution(taskId);
|
if (request == null || request.getFiles() == null || request.getFiles().isEmpty()) {
|
||||||
if (task == null || !MODULE_TYPE.equals(task.getModuleType())) {
|
throw new BusinessException("files is empty");
|
||||||
throw new BusinessException("任务不存在");
|
|
||||||
}
|
|
||||||
if ("SUCCESS".equals(task.getStatus()) || "FAILED".equals(task.getStatus())) {
|
|
||||||
log.warn("[DeleteBrand] submitResult rejected because task is terminal -> taskId: {}, status: {}, createdAt: {}, updatedAt: {}, finishedAt: {}, error: {}, sourceFiles: {}, successFiles: {}, failedFiles: {}",
|
|
||||||
task.getId(),
|
|
||||||
task.getStatus(),
|
|
||||||
task.getCreatedAt(),
|
|
||||||
task.getUpdatedAt(),
|
|
||||||
task.getFinishedAt(),
|
|
||||||
task.getErrorMessage(),
|
|
||||||
task.getSourceFileCount(),
|
|
||||||
task.getSuccessFileCount(),
|
|
||||||
task.getFailedFileCount());
|
|
||||||
throw new BusinessException("任务已结束,拒绝继续提交结果");
|
|
||||||
}
|
|
||||||
|
|
||||||
Map<String, DeleteBrandParsedFileCacheDto> parsedPayload = deleteBrandTaskStorageService.loadParsedPayload(taskId);
|
|
||||||
if (parsedPayload == null || parsedPayload.isEmpty()) {
|
|
||||||
throw new BusinessException("任务原始数据已过期,请重新执行解析");
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!"RUNNING".equals(task.getStatus())) {
|
|
||||||
task.setStatus("RUNNING");
|
|
||||||
task.setErrorMessage(null);
|
|
||||||
task.setUpdatedAt(LocalDateTime.now());
|
|
||||||
task.setFinishedAt(null);
|
|
||||||
fileTaskMapper.updateById(task);
|
|
||||||
deleteBrandTaskCacheService.saveTaskCache(task);
|
|
||||||
}
|
|
||||||
|
|
||||||
boolean shouldTryFinalize = false;
|
|
||||||
|
|
||||||
for (DeleteBrandResultFileDto fileDto : request.getFiles()) {
|
|
||||||
log.info("[DeleteBrand] submitResult processing chunk -> taskId: {}, file: {}, chunkIndex: {}/{}, processedRows: {}/{}",
|
|
||||||
taskId, fileDto.getSourceFilename(), fileDto.getChunkIndex(), fileDto.getChunkTotal(),
|
|
||||||
fileDto.getProcessedRows(), fileDto.getTotalRows());
|
|
||||||
|
|
||||||
String fileIdentity = resolveFileIdentity(fileDto);
|
|
||||||
if (fileIdentity.isBlank()) {
|
|
||||||
throw new BusinessException("fileKey/sourceFilename 不能为空");
|
|
||||||
}
|
|
||||||
DeleteBrandParsedFileCacheDto parsedFile = parsedPayload.get(fileIdentity);
|
|
||||||
if (parsedFile == null) {
|
|
||||||
log.warn("[DeleteBrand] parsed payload not found for identity: {}", fileIdentity);
|
|
||||||
throw new BusinessException("文件标识不匹配: " + fileIdentity);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// 更早执行 duplicate fast-path,尽量在较重校验和进度写入前直接短路
|
FileTaskEntity task = loadTaskForExecution(taskId);
|
||||||
validateChunk(fileDto, parsedFile);
|
if (task == null || !MODULE_TYPE.equals(task.getModuleType())) {
|
||||||
validateResultFileAgainstParsed(fileDto, parsedFile);
|
throw new BusinessException("task not found");
|
||||||
boolean stored = deleteBrandTaskStorageService.storeResultChunkIfChanged(taskId, fileIdentity, fileDto.getChunkIndex(), fileDto);
|
}
|
||||||
if (!stored) {
|
if ("SUCCESS".equals(task.getStatus()) || "FAILED".equals(task.getStatus())) {
|
||||||
log.info("[DeleteBrand] Duplicate chunk ignored for taskId: {}, file: {}, chunkIndex: {}", taskId, fileIdentity, fileDto.getChunkIndex());
|
log.warn("[DeleteBrand] submitResult rejected because task is terminal -> taskId: {}, status: {}, createdAt: {}, updatedAt: {}, finishedAt: {}, error: {}, sourceFiles: {}, successFiles: {}, failedFiles: {}",
|
||||||
continue;
|
task.getId(),
|
||||||
|
task.getStatus(),
|
||||||
|
task.getCreatedAt(),
|
||||||
|
task.getUpdatedAt(),
|
||||||
|
task.getFinishedAt(),
|
||||||
|
task.getErrorMessage(),
|
||||||
|
task.getSourceFileCount(),
|
||||||
|
task.getSuccessFileCount(),
|
||||||
|
task.getFailedFileCount());
|
||||||
|
throw new BusinessException("task already finished");
|
||||||
}
|
}
|
||||||
|
|
||||||
Map<String, String> progress = new LinkedHashMap<>();
|
Map<String, DeleteBrandParsedFileCacheDto> parsedPayload = deleteBrandTaskStorageService.loadParsedPayload(taskId);
|
||||||
progress.put("phase", DeleteBrandTaskCacheService.PHASE_CRAWLING);
|
if (parsedPayload == null || parsedPayload.isEmpty()) {
|
||||||
progress.put("file_index", String.valueOf(defaultInt(fileDto.getFileIndex(), 0)));
|
throw new BusinessException("task parsed payload missing");
|
||||||
progress.put("file_total", String.valueOf(defaultInt(fileDto.getFileTotal(), parsedPayload.size())));
|
|
||||||
progress.put("file_name", blankToEmpty(fileDto.getSourceFilename()));
|
|
||||||
progress.put("current_line", String.valueOf(defaultInt(fileDto.getProcessedRows(), 0)));
|
|
||||||
progress.put("total_lines", String.valueOf(defaultInt(fileDto.getTotalRows(), parsedFile.getTotalRows() == null ? 0 : parsedFile.getTotalRows())));
|
|
||||||
progress.put("current_country", blankToEmpty(fileDto.getCurrentCountry()));
|
|
||||||
progress.put("current_asin", blankToEmpty(fileDto.getCurrentAsin()));
|
|
||||||
progress.put("updated_at", String.valueOf(System.currentTimeMillis()));
|
|
||||||
progress.put("last_heartbeat_at", String.valueOf(System.currentTimeMillis()));
|
|
||||||
deleteBrandTaskCacheService.saveProgress(taskId, progress, true);
|
|
||||||
|
|
||||||
// 如果该文件已完成,立即更新成品计数的 Redis 提示,让后续 tryFinalizeTask 更准确
|
|
||||||
if (isFileCompleted(fileDto)) {
|
|
||||||
log.info("[DeleteBrand] File chunk transmission fully reached via chunkTotal for taskId: {}, file: {}", taskId, fileDto.getSourceFilename());
|
|
||||||
enqueueCompletedFileAssembleJob(task, fileIdentity, parsedFile);
|
|
||||||
shouldTryFinalize = true;
|
|
||||||
// 这里暂不直接 increment,而是标记需要在 tryFinalizeTask 中重新计算
|
|
||||||
// 为了保险,直接在 tryFinalizeTask 里重新遍历分片状态
|
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
if (!"RUNNING".equals(task.getStatus())) {
|
||||||
|
task.setStatus("RUNNING");
|
||||||
|
task.setErrorMessage(null);
|
||||||
|
task.setUpdatedAt(LocalDateTime.now());
|
||||||
|
task.setFinishedAt(null);
|
||||||
|
fileTaskMapper.updateById(task);
|
||||||
|
deleteBrandTaskCacheService.saveTaskCache(task);
|
||||||
|
}
|
||||||
|
|
||||||
if (shouldTryFinalize) {
|
boolean shouldTryFinalize = false;
|
||||||
tryFinalizeTask(taskId, false);
|
|
||||||
|
for (DeleteBrandResultFileDto fileDto : request.getFiles()) {
|
||||||
|
log.info("[DeleteBrand] submitResult processing chunk -> taskId: {}, file: {}, chunkIndex: {}/{}, processedRows: {}/{}",
|
||||||
|
taskId, fileDto.getSourceFilename(), fileDto.getChunkIndex(), fileDto.getChunkTotal(),
|
||||||
|
fileDto.getProcessedRows(), fileDto.getTotalRows());
|
||||||
|
|
||||||
|
String fileIdentity = resolveFileIdentity(fileDto);
|
||||||
|
if (fileIdentity.isBlank()) {
|
||||||
|
throw new BusinessException("file identity is blank");
|
||||||
|
}
|
||||||
|
DeleteBrandParsedFileCacheDto parsedFile = parsedPayload.get(fileIdentity);
|
||||||
|
if (parsedFile == null) {
|
||||||
|
log.warn("[DeleteBrand] parsed payload not found for identity: {}", fileIdentity);
|
||||||
|
throw new BusinessException("parsed file not found: " + fileIdentity);
|
||||||
|
}
|
||||||
|
|
||||||
|
validateChunk(fileDto, parsedFile);
|
||||||
|
validateResultFileAgainstParsed(fileDto, parsedFile);
|
||||||
|
boolean stored = deleteBrandTaskStorageService.storeResultChunkIfChanged(taskId, fileIdentity, fileDto.getChunkIndex(), fileDto);
|
||||||
|
if (!stored) {
|
||||||
|
log.info("[DeleteBrand] Duplicate chunk ignored for taskId: {}, file: {}, chunkIndex: {}", taskId, fileIdentity, fileDto.getChunkIndex());
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
Map<String, String> progress = new LinkedHashMap<>();
|
||||||
|
progress.put("phase", DeleteBrandTaskCacheService.PHASE_CRAWLING);
|
||||||
|
progress.put("file_index", String.valueOf(defaultInt(fileDto.getFileIndex(), 0)));
|
||||||
|
progress.put("file_total", String.valueOf(defaultInt(fileDto.getFileTotal(), parsedPayload.size())));
|
||||||
|
progress.put("file_name", blankToEmpty(fileDto.getSourceFilename()));
|
||||||
|
progress.put("current_line", String.valueOf(defaultInt(fileDto.getProcessedRows(), 0)));
|
||||||
|
progress.put("total_lines", String.valueOf(defaultInt(fileDto.getTotalRows(), parsedFile.getTotalRows() == null ? 0 : parsedFile.getTotalRows())));
|
||||||
|
progress.put("current_country", blankToEmpty(fileDto.getCurrentCountry()));
|
||||||
|
progress.put("current_asin", blankToEmpty(fileDto.getCurrentAsin()));
|
||||||
|
progress.put("updated_at", String.valueOf(System.currentTimeMillis()));
|
||||||
|
progress.put("last_heartbeat_at", String.valueOf(System.currentTimeMillis()));
|
||||||
|
deleteBrandTaskCacheService.saveProgress(taskId, progress, true);
|
||||||
|
|
||||||
|
if (isFileCompleted(fileDto)) {
|
||||||
|
log.info("[DeleteBrand] File chunk transmission fully reached via chunkTotal for taskId: {}, file: {}", taskId, fileDto.getSourceFilename());
|
||||||
|
enqueueCompletedFileAssembleJob(task, fileIdentity, parsedFile);
|
||||||
|
shouldTryFinalize = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (shouldTryFinalize) {
|
||||||
|
tryFinalizeTask(taskId, false);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1398,44 +1402,45 @@ public class DeleteBrandRunService {
|
|||||||
if (taskId == null || taskId <= 0) {
|
if (taskId == null || taskId <= 0) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
try (TaskDistributedLockService.LockHandle ignored = acquireTaskLockOrThrow(taskId, 0L)) {
|
||||||
|
FileTaskEntity task = loadTaskForExecution(taskId);
|
||||||
|
if (task == null || !MODULE_TYPE.equals(task.getModuleType())) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if ("SUCCESS".equals(task.getStatus()) || "FAILED".equals(task.getStatus())) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
FileTaskEntity task = loadTaskForExecution(taskId);
|
int expectedFiles = deleteBrandTaskStorageService.countParsedPayloadScopes(taskId);
|
||||||
if (task == null || !MODULE_TYPE.equals(task.getModuleType())) {
|
if (expectedFiles <= 0) {
|
||||||
return;
|
tryFinalizeTaskFromTerminalResults(task);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
int finishedFiles = deleteBrandTaskStorageService.countCompletedScopes(taskId);
|
||||||
|
if (finishedFiles < expectedFiles) {
|
||||||
|
updateTaskFinalizeProgress(task, finishedFiles, fromCompensation);
|
||||||
|
log.info("[DeleteBrand] tryFinalizeTask skipping -> finishedFiles ({}) < expectedFiles ({})", finishedFiles, expectedFiles);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
updateTaskFinalizeProgress(task, finishedFiles, true);
|
||||||
|
Map<String, DeleteBrandParsedFileCacheDto> parsedPayload = deleteBrandTaskStorageService.loadParsedPayload(taskId);
|
||||||
|
if (parsedPayload == null || parsedPayload.isEmpty()) {
|
||||||
|
tryFinalizeTaskFromTerminalResults(task);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
Map<String, List<DeleteBrandResultFileDto>> mergedByFile = deleteBrandTaskStorageService.loadMergedChunks(taskId);
|
||||||
|
|
||||||
|
log.info("[DeleteBrand] tryFinalizeTask calling finalizeTask -> finishedFiles: {}, expectedFiles: {}", finishedFiles, expectedFiles);
|
||||||
|
finalizeTask(task, parsedPayload, mergedByFile);
|
||||||
|
} catch (BusinessException ex) {
|
||||||
|
if (ex.getMessage() != null && ex.getMessage().contains("task lock is busy")) {
|
||||||
|
log.info("[DeleteBrand] tryFinalizeTask skipped because task lock is busy taskId={} fromCompensation={}", taskId, fromCompensation);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
throw ex;
|
||||||
}
|
}
|
||||||
if ("SUCCESS".equals(task.getStatus()) || "FAILED".equals(task.getStatus())) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
int expectedFiles = deleteBrandTaskStorageService.countParsedPayloadScopes(taskId);
|
|
||||||
if (expectedFiles <= 0) {
|
|
||||||
tryFinalizeTaskFromTerminalResults(task);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
// 移除 finishedFilesHint 的强依赖,因为 Redis 里的 finished_files 可能是旧值
|
|
||||||
// 既然进入 tryFinalizeTask,就说明有新分片到达,应该以 loadMergedChunks 为准
|
|
||||||
|
|
||||||
|
|
||||||
int finishedFiles = deleteBrandTaskStorageService.countCompletedScopes(taskId);
|
|
||||||
|
|
||||||
if (finishedFiles < expectedFiles) {
|
|
||||||
updateTaskFinalizeProgress(task, finishedFiles, fromCompensation);
|
|
||||||
|
|
||||||
log.info("[DeleteBrand] tryFinalizeTask skipping -> finishedFiles ({}) < expectedFiles ({})", finishedFiles, expectedFiles);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
updateTaskFinalizeProgress(task, finishedFiles, true);
|
|
||||||
Map<String, DeleteBrandParsedFileCacheDto> parsedPayload = deleteBrandTaskStorageService.loadParsedPayload(taskId);
|
|
||||||
if (parsedPayload == null || parsedPayload.isEmpty()) {
|
|
||||||
tryFinalizeTaskFromTerminalResults(task);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
Map<String, List<DeleteBrandResultFileDto>> mergedByFile = deleteBrandTaskStorageService.loadMergedChunks(taskId);
|
|
||||||
|
|
||||||
log.info("[DeleteBrand] tryFinalizeTask calling finalizeTask -> finishedFiles: {}, expectedFiles: {}", finishedFiles, expectedFiles);
|
|
||||||
|
|
||||||
finalizeTask(task, parsedPayload, mergedByFile);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private void updateTaskFinalizeProgress(FileTaskEntity task, int finishedFiles, boolean fromCompensation) {
|
private void updateTaskFinalizeProgress(FileTaskEntity task, int finishedFiles, boolean fromCompensation) {
|
||||||
@@ -1974,6 +1979,27 @@ public class DeleteBrandRunService {
|
|||||||
String normalized = blankToEmpty(value);
|
String normalized = blankToEmpty(value);
|
||||||
return normalized.isBlank() ? defaultValue : normalized;
|
return normalized.isBlank() ? defaultValue : normalized;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private TaskDistributedLockService.LockHandle acquireTaskLockOrThrow(Long taskId) {
|
||||||
|
return acquireTaskLockOrThrow(taskId, TASK_LOCK_WAIT_MILLIS);
|
||||||
|
}
|
||||||
|
|
||||||
|
private TaskDistributedLockService.LockHandle acquireTaskLockOrThrow(Long taskId, long waitMillis) {
|
||||||
|
TaskDistributedLockService.LockHandle lockHandle = acquireTaskLock(taskId, waitMillis);
|
||||||
|
if (lockHandle == null) {
|
||||||
|
throw new BusinessException("task lock is busy");
|
||||||
|
}
|
||||||
|
return lockHandle;
|
||||||
|
}
|
||||||
|
|
||||||
|
private TaskDistributedLockService.LockHandle acquireTaskLock(Long taskId, long waitMillis) {
|
||||||
|
TaskDistributedLockService.LockHandle lockHandle =
|
||||||
|
taskDistributedLockService.acquire(MODULE_TYPE, taskId, TASK_LOCK_TTL, waitMillis);
|
||||||
|
if (lockHandle == null) {
|
||||||
|
log.info("[DeleteBrand] task lock busy taskId={} waitMillis={}", taskId, waitMillis);
|
||||||
|
}
|
||||||
|
return lockHandle;
|
||||||
|
}
|
||||||
/**
|
/**
|
||||||
* 落库用 result_json:去掉国家分组、预览行等大字段,保留匹配摘要与计数。
|
* 落库用 result_json:去掉国家分组、预览行等大字段,保留匹配摘要与计数。
|
||||||
* 索引命中后单次任务 JSON 体积会明显膨胀,容易触发 max_allowed_packet 或更新失败。
|
* 索引命中后单次任务 JSON 体积会明显膨胀,容易触发 max_allowed_packet 或更新失败。
|
||||||
|
|||||||
@@ -5,6 +5,8 @@ import com.baomidou.mybatisplus.core.conditions.update.LambdaUpdateWrapper;
|
|||||||
import com.nanri.aiimage.common.service.DistributedJobLockService;
|
import com.nanri.aiimage.common.service.DistributedJobLockService;
|
||||||
import com.nanri.aiimage.config.DeleteBrandProgressProperties;
|
import com.nanri.aiimage.config.DeleteBrandProgressProperties;
|
||||||
import com.nanri.aiimage.modules.productrisk.service.ProductRiskTaskCacheService;
|
import com.nanri.aiimage.modules.productrisk.service.ProductRiskTaskCacheService;
|
||||||
|
import com.nanri.aiimage.modules.queryasin.service.QueryAsinTaskCacheService;
|
||||||
|
import com.nanri.aiimage.modules.queryasin.service.QueryAsinTaskService;
|
||||||
import com.nanri.aiimage.modules.productrisk.service.ProductRiskTaskService;
|
import com.nanri.aiimage.modules.productrisk.service.ProductRiskTaskService;
|
||||||
import com.nanri.aiimage.modules.patroldelete.service.PatrolDeleteTaskCacheService;
|
import com.nanri.aiimage.modules.patroldelete.service.PatrolDeleteTaskCacheService;
|
||||||
import com.nanri.aiimage.modules.patroldelete.service.PatrolDeleteTaskService;
|
import com.nanri.aiimage.modules.patroldelete.service.PatrolDeleteTaskService;
|
||||||
@@ -42,6 +44,7 @@ public class DeleteBrandStaleTaskService {
|
|||||||
private static final String MODULE_TYPE_PRICE_TRACK = "PRICE_TRACK";
|
private static final String MODULE_TYPE_PRICE_TRACK = "PRICE_TRACK";
|
||||||
private static final String MODULE_TYPE_SHOP_MATCH = "SHOP_MATCH";
|
private static final String MODULE_TYPE_SHOP_MATCH = "SHOP_MATCH";
|
||||||
private static final String MODULE_TYPE_PATROL_DELETE = "PATROL_DELETE";
|
private static final String MODULE_TYPE_PATROL_DELETE = "PATROL_DELETE";
|
||||||
|
private static final String MODULE_TYPE_QUERY_ASIN = "QUERY_ASIN";
|
||||||
private static final Duration STALE_CHECK_LOCK_TTL = Duration.ofMinutes(10);
|
private static final Duration STALE_CHECK_LOCK_TTL = Duration.ofMinutes(10);
|
||||||
private static final Duration FINALIZE_CHECK_LOCK_TTL = Duration.ofMinutes(10);
|
private static final Duration FINALIZE_CHECK_LOCK_TTL = Duration.ofMinutes(10);
|
||||||
private static final Duration TEMP_DIR_CLEANUP_LOCK_TTL = Duration.ofMinutes(10);
|
private static final Duration TEMP_DIR_CLEANUP_LOCK_TTL = Duration.ofMinutes(10);
|
||||||
@@ -58,6 +61,8 @@ public class DeleteBrandStaleTaskService {
|
|||||||
private final ShopMatchTaskCacheService shopMatchTaskCacheService;
|
private final ShopMatchTaskCacheService shopMatchTaskCacheService;
|
||||||
private final PatrolDeleteTaskService patrolDeleteTaskService;
|
private final PatrolDeleteTaskService patrolDeleteTaskService;
|
||||||
private final PatrolDeleteTaskCacheService patrolDeleteTaskCacheService;
|
private final PatrolDeleteTaskCacheService patrolDeleteTaskCacheService;
|
||||||
|
private final QueryAsinTaskService queryAsinTaskService;
|
||||||
|
private final QueryAsinTaskCacheService queryAsinTaskCacheService;
|
||||||
private final DeleteBrandProgressProperties deleteBrandProgressProperties;
|
private final DeleteBrandProgressProperties deleteBrandProgressProperties;
|
||||||
private final DistributedJobLockService distributedJobLockService;
|
private final DistributedJobLockService distributedJobLockService;
|
||||||
private final TaskDistributedLockService taskDistributedLockService;
|
private final TaskDistributedLockService taskDistributedLockService;
|
||||||
@@ -82,6 +87,7 @@ public class DeleteBrandStaleTaskService {
|
|||||||
PriceTrackStaleCheckStats priceTrackStats = failStalePriceTrackTasks();
|
PriceTrackStaleCheckStats priceTrackStats = failStalePriceTrackTasks();
|
||||||
ShopMatchStaleCheckStats shopMatchStats = failStaleShopMatchTasks();
|
ShopMatchStaleCheckStats shopMatchStats = failStaleShopMatchTasks();
|
||||||
ShopMatchStaleCheckStats patrolDeleteStats = failStalePatrolDeleteTasks();
|
ShopMatchStaleCheckStats patrolDeleteStats = failStalePatrolDeleteTasks();
|
||||||
|
ShopMatchStaleCheckStats queryAsinStats = failStaleQueryAsinTasks();
|
||||||
log.info("[stale-check] product-risk summary scanned={} finalized={} failed={} skipped={} elapsedMs={} thread={}",
|
log.info("[stale-check] product-risk summary scanned={} finalized={} failed={} skipped={} elapsedMs={} thread={}",
|
||||||
stats.scannedTaskCount,
|
stats.scannedTaskCount,
|
||||||
stats.finalizedTaskCount,
|
stats.finalizedTaskCount,
|
||||||
@@ -110,6 +116,13 @@ public class DeleteBrandStaleTaskService {
|
|||||||
patrolDeleteStats.skippedTaskCount,
|
patrolDeleteStats.skippedTaskCount,
|
||||||
System.currentTimeMillis() - startedAt,
|
System.currentTimeMillis() - startedAt,
|
||||||
Thread.currentThread().getName());
|
Thread.currentThread().getName());
|
||||||
|
log.info("[stale-check] query-asin summary scanned={} finalized={} failed={} skipped={} elapsedMs={} thread={}",
|
||||||
|
queryAsinStats.scannedTaskCount,
|
||||||
|
queryAsinStats.finalizedTaskCount,
|
||||||
|
queryAsinStats.failedTaskCount,
|
||||||
|
queryAsinStats.skippedTaskCount,
|
||||||
|
System.currentTimeMillis() - startedAt,
|
||||||
|
Thread.currentThread().getName());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -525,6 +538,85 @@ public class DeleteBrandStaleTaskService {
|
|||||||
return stats;
|
return stats;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private ShopMatchStaleCheckStats failStaleQueryAsinTasks() {
|
||||||
|
ShopMatchStaleCheckStats stats = new ShopMatchStaleCheckStats();
|
||||||
|
long minutes = Math.max(1L, deleteBrandProgressProperties.getPatrolDeleteStaleTimeoutMinutes());
|
||||||
|
long initialMinutes = Math.max(minutes, deleteBrandProgressProperties.getPatrolDeleteInitialTimeoutMinutes());
|
||||||
|
long staleTimeoutMillis = Duration.ofMinutes(minutes).toMillis();
|
||||||
|
long nowMillis = System.currentTimeMillis();
|
||||||
|
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_QUERY_ASIN)
|
||||||
|
.eq(FileTaskEntity::getStatus, "RUNNING")
|
||||||
|
.lt(FileTaskEntity::getUpdatedAt, threshold)
|
||||||
|
.last("limit 200"));
|
||||||
|
stats.scannedTaskCount = runningTasks.size();
|
||||||
|
if (runningTasks.isEmpty()) {
|
||||||
|
return stats;
|
||||||
|
}
|
||||||
|
Map<Long, Long> heartbeatByTaskId = queryAsinTaskCacheService.getTaskHeartbeatMillisBatch(
|
||||||
|
runningTasks.stream().map(FileTaskEntity::getId).toList());
|
||||||
|
for (FileTaskEntity task : runningTasks) {
|
||||||
|
long lastHeartbeatMillis = heartbeatByTaskId.getOrDefault(task.getId(), 0L);
|
||||||
|
boolean hasFinishedRows = (task.getSuccessFileCount() != null && task.getSuccessFileCount() > 0)
|
||||||
|
|| (task.getFailedFileCount() != null && task.getFailedFileCount() > 0);
|
||||||
|
if (lastHeartbeatMillis > 0L && nowMillis - lastHeartbeatMillis < staleTimeoutMillis) {
|
||||||
|
stats.skippedTaskCount++;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
boolean hasStartedProgress = hasFinishedRows;
|
||||||
|
if (!hasStartedProgress) {
|
||||||
|
hasStartedProgress = queryAsinTaskCacheService.hasAnyShopMergedPayload(task.getId());
|
||||||
|
}
|
||||||
|
if (hasPendingAssembleJobs(task.getId(), MODULE_TYPE_QUERY_ASIN)) {
|
||||||
|
stats.skippedTaskCount++;
|
||||||
|
log.info("[stale-check] query-asin skip pending-assemble-jobs taskId={} unfinishedJobs={} activeJobs={}",
|
||||||
|
task.getId(),
|
||||||
|
taskFileJobService.countUnfinishedAssembleJobs(task.getId(), MODULE_TYPE_QUERY_ASIN),
|
||||||
|
taskFileJobService.countActiveAssembleJobs(task.getId(), MODULE_TYPE_QUERY_ASIN));
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if (!hasStartedProgress && isWithinInitialGrace(task, initialThreshold)) {
|
||||||
|
stats.skippedTaskCount++;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
TaskDistributedLockService.LockHandle taskLockHandle = acquireTaskLock(MODULE_TYPE_QUERY_ASIN, task.getId());
|
||||||
|
if (taskLockHandle == null) {
|
||||||
|
stats.skippedTaskCount++;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
try (taskLockHandle) {
|
||||||
|
try {
|
||||||
|
if (queryAsinTaskService.tryFinalizeTask(task.getId(), true)) {
|
||||||
|
stats.finalizedTaskCount++;
|
||||||
|
log.info("[stale-check] query-asin finalized taskId={}", task.getId());
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
} catch (Exception ex) {
|
||||||
|
log.warn("[stale-check] query-asin 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_QUERY_ASIN)
|
||||||
|
.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++;
|
||||||
|
queryAsinTaskCacheService.deleteTaskCache(task.getId());
|
||||||
|
log.warn("[stale-check] query-asin failed taskId={} reason=timeout", task.getId());
|
||||||
|
} else {
|
||||||
|
stats.skippedTaskCount++;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return stats;
|
||||||
|
}
|
||||||
|
|
||||||
private boolean isWithinInitialGrace(FileTaskEntity task, LocalDateTime initialThreshold) {
|
private boolean isWithinInitialGrace(FileTaskEntity task, LocalDateTime initialThreshold) {
|
||||||
if (task == null || initialThreshold == null) {
|
if (task == null || initialThreshold == null) {
|
||||||
return false;
|
return false;
|
||||||
@@ -595,7 +687,8 @@ public class DeleteBrandStaleTaskService {
|
|||||||
"patrol-delete-result",
|
"patrol-delete-result",
|
||||||
"delete-brand-result",
|
"delete-brand-result",
|
||||||
"product-risk-result",
|
"product-risk-result",
|
||||||
"price-track-result"
|
"price-track-result",
|
||||||
|
"query-asin-result"
|
||||||
);
|
);
|
||||||
for (String dirName : dirNames) {
|
for (String dirName : dirNames) {
|
||||||
cleanupTempRoot(dirName, cutoff);
|
cleanupTempRoot(dirName, cutoff);
|
||||||
|
|||||||
@@ -330,6 +330,7 @@ public class PatrolDeleteTaskService {
|
|||||||
throw new BusinessException("shops 不能为空");
|
throw new BusinessException("shops 不能为空");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
try (TaskDistributedLockService.LockHandle ignored = acquireTaskLockOrThrow(taskId)) {
|
||||||
FileTaskEntity task = loadTaskForExecution(taskId);
|
FileTaskEntity task = loadTaskForExecution(taskId);
|
||||||
if (task == null || !MODULE_TYPE.equals(task.getModuleType())) {
|
if (task == null || !MODULE_TYPE.equals(task.getModuleType())) {
|
||||||
log.info("[patrol-delete] ignore result callback for deleted task taskId={}", taskId);
|
log.info("[patrol-delete] ignore result callback for deleted task taskId={}", taskId);
|
||||||
@@ -373,12 +374,19 @@ public class PatrolDeleteTaskService {
|
|||||||
fileTaskMapper.updateById(task);
|
fileTaskMapper.updateById(task);
|
||||||
taskCacheService.saveTaskCache(task);
|
taskCacheService.saveTaskCache(task);
|
||||||
tryFinalizeTask(taskId, false);
|
tryFinalizeTask(taskId, false);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public boolean tryFinalizeTask(Long taskId, boolean fromCompensation) {
|
public boolean tryFinalizeTask(Long taskId, boolean fromCompensation) {
|
||||||
if (taskId == null || taskId <= 0) {
|
if (taskId == null || taskId <= 0) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
TaskDistributedLockService.LockHandle lockHandle = acquireTaskLock(taskId);
|
||||||
|
if (lockHandle == null) {
|
||||||
|
log.info("[patrol-delete] tryFinalizeTask skipped because task lock is busy taskId={} fromCompensation={}", taskId, fromCompensation);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
try (lockHandle) {
|
||||||
FileTaskEntity task = loadTaskForExecution(taskId);
|
FileTaskEntity task = loadTaskForExecution(taskId);
|
||||||
if (task == null || !MODULE_TYPE.equals(task.getModuleType())) {
|
if (task == null || !MODULE_TYPE.equals(task.getModuleType())) {
|
||||||
return false;
|
return false;
|
||||||
@@ -449,6 +457,7 @@ public class PatrolDeleteTaskService {
|
|||||||
fileTaskMapper.updateById(task);
|
fileTaskMapper.updateById(task);
|
||||||
taskCacheService.saveTaskCache(task);
|
taskCacheService.saveTaskCache(task);
|
||||||
return changed;
|
return changed;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@Transactional
|
@Transactional
|
||||||
@@ -459,6 +468,7 @@ public class PatrolDeleteTaskService {
|
|||||||
if (task == null || !MODULE_TYPE.equals(task.getModuleType()) || !userId.equals(task.getUserId())) {
|
if (task == null || !MODULE_TYPE.equals(task.getModuleType()) || !userId.equals(task.getUserId())) {
|
||||||
throw new BusinessException("任务不存在");
|
throw new BusinessException("任务不存在");
|
||||||
}
|
}
|
||||||
|
try (TaskDistributedLockService.LockHandle ignored = acquireTaskLockOrThrow(taskId)) {
|
||||||
long taskLoadedAt = System.nanoTime();
|
long taskLoadedAt = System.nanoTime();
|
||||||
fileResultMapper.delete(new LambdaQueryWrapper<FileResultEntity>()
|
fileResultMapper.delete(new LambdaQueryWrapper<FileResultEntity>()
|
||||||
.eq(FileResultEntity::getTaskId, taskId)
|
.eq(FileResultEntity::getTaskId, taskId)
|
||||||
@@ -479,6 +489,7 @@ public class PatrolDeleteTaskService {
|
|||||||
elapsedMs(resultsDeletedAt, auxiliaryDeletedAt),
|
elapsedMs(resultsDeletedAt, auxiliaryDeletedAt),
|
||||||
elapsedMs(auxiliaryDeletedAt, taskDeletedAt),
|
elapsedMs(auxiliaryDeletedAt, taskDeletedAt),
|
||||||
elapsedMs(taskDeletedAt, finishedAt));
|
elapsedMs(taskDeletedAt, finishedAt));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@Transactional
|
@Transactional
|
||||||
|
|||||||
@@ -258,16 +258,18 @@ public class PriceTrackTaskService {
|
|||||||
@Transactional
|
@Transactional
|
||||||
public void deleteTask(Long taskId, Long userId) {
|
public void deleteTask(Long taskId, Long userId) {
|
||||||
if (userId == null || userId <= 0) throw new BusinessException("user_id 不合法");
|
if (userId == null || userId <= 0) throw new BusinessException("user_id 不合法");
|
||||||
FileTaskEntity task = loadTaskForExecution(taskId);
|
try (TaskDistributedLockService.LockHandle ignored = acquireTaskLockOrThrow(taskId)) {
|
||||||
if (task == null || !MODULE_TYPE.equals(task.getModuleType()) || !userId.equals(task.getUserId())) {
|
FileTaskEntity task = loadTaskForExecution(taskId);
|
||||||
throw new BusinessException("任务不存在");
|
if (task == null || !MODULE_TYPE.equals(task.getModuleType()) || !userId.equals(task.getUserId())) {
|
||||||
|
throw new BusinessException("任务不存在");
|
||||||
|
}
|
||||||
|
fileResultMapper.delete(new LambdaQueryWrapper<FileResultEntity>()
|
||||||
|
.eq(FileResultEntity::getTaskId, taskId)
|
||||||
|
.eq(FileResultEntity::getModuleType, MODULE_TYPE));
|
||||||
|
fileTaskMapper.deleteById(taskId);
|
||||||
|
priceTrackTaskCacheService.deleteTaskCache(taskId);
|
||||||
|
priceTrackLoopRunService.syncLoopRunAfterChildRemoved(taskId);
|
||||||
}
|
}
|
||||||
fileResultMapper.delete(new LambdaQueryWrapper<FileResultEntity>()
|
|
||||||
.eq(FileResultEntity::getTaskId, taskId)
|
|
||||||
.eq(FileResultEntity::getModuleType, MODULE_TYPE));
|
|
||||||
fileTaskMapper.deleteById(taskId);
|
|
||||||
priceTrackTaskCacheService.deleteTaskCache(taskId);
|
|
||||||
priceTrackLoopRunService.syncLoopRunAfterChildRemoved(taskId);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@Transactional
|
@Transactional
|
||||||
@@ -458,154 +460,163 @@ public class PriceTrackTaskService {
|
|||||||
if (request == null || request.getShops() == null || request.getShops().isEmpty()) {
|
if (request == null || request.getShops() == null || request.getShops().isEmpty()) {
|
||||||
throw new BusinessException("shops 不能为空");
|
throw new BusinessException("shops 不能为空");
|
||||||
}
|
}
|
||||||
FileTaskEntity task = loadTaskForExecution(taskId);
|
try (TaskDistributedLockService.LockHandle ignored = acquireTaskLockOrThrow(taskId)) {
|
||||||
if (task == null || !MODULE_TYPE.equals(task.getModuleType())) {
|
FileTaskEntity task = loadTaskForExecution(taskId);
|
||||||
throw new BusinessException("任务不存在");
|
if (task == null || !MODULE_TYPE.equals(task.getModuleType())) {
|
||||||
|
throw new BusinessException("任务不存在");
|
||||||
|
}
|
||||||
|
if ("SUCCESS".equals(task.getStatus()) || "FAILED".equals(task.getStatus())) {
|
||||||
|
log.warn("[price-track] submitResult rejected taskId={} status={}", taskId, task.getStatus());
|
||||||
|
throw new BusinessException(40901, "任务已结束,拒绝重复提交");
|
||||||
|
}
|
||||||
|
priceTrackTaskCacheService.touchTaskHeartbeat(taskId);
|
||||||
|
List<FileResultEntity> resultRows = fileResultMapper.selectList(
|
||||||
|
new LambdaQueryWrapper<FileResultEntity>()
|
||||||
|
.eq(FileResultEntity::getTaskId, taskId)
|
||||||
|
.eq(FileResultEntity::getModuleType, MODULE_TYPE)
|
||||||
|
.orderByAsc(FileResultEntity::getId));
|
||||||
|
Map<String, PriceTrackSubmitResultRequest.ShopResult> payloadByShop = new LinkedHashMap<>();
|
||||||
|
for (PriceTrackSubmitResultRequest.ShopResult sr : request.getShops()) {
|
||||||
|
if (sr == null) continue;
|
||||||
|
String key = ziniaoShopSwitchService.normalizeShopName(sr.getShopName());
|
||||||
|
if (!key.isBlank()) payloadByShop.put(key, sr);
|
||||||
|
}
|
||||||
|
log.info("[price-track] submitResult received taskId={} payloadShopCount={} payloadKeys={}",
|
||||||
|
taskId, payloadByShop.size(), payloadByShop.keySet());
|
||||||
|
boolean changed = false;
|
||||||
|
for (FileResultEntity fr : resultRows) {
|
||||||
|
String shopKey = fr.getSourceFilename();
|
||||||
|
PriceTrackSubmitResultRequest.ShopResult payload = shopKey == null ? null : payloadByShop.get(shopKey);
|
||||||
|
if (payload == null) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
changed = true;
|
||||||
|
if (payload.getError() != null && !payload.getError().isBlank()) {
|
||||||
|
markResultFailed(fr, payload.getError());
|
||||||
|
priceTrackTaskCacheService.removeShopMergedPayload(taskId, shopKey);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if (Boolean.FALSE.equals(payload.getSuccess())) {
|
||||||
|
markResultFailed(fr, "shop processing failed");
|
||||||
|
priceTrackTaskCacheService.removeShopMergedPayload(taskId, shopKey);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
handleSkipAsinDeletionSignals(shopKey, payload);
|
||||||
|
PriceTrackSubmitResultRequest.ShopResult merged = mergeShopPayload(taskId, shopKey, payload);
|
||||||
|
if (!Boolean.TRUE.equals(merged.getSuccess())) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if (countPayloadRows(merged) <= 0) {
|
||||||
|
markResultFailed(fr, "no usable price-track rows received");
|
||||||
|
priceTrackTaskCacheService.removeShopMergedPayload(taskId, shopKey);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
enqueueResultFileAssembly(fr, shopKey, merged);
|
||||||
|
priceTrackTaskCacheService.removeShopMergedPayload(taskId, shopKey);
|
||||||
|
} catch (Exception ex) {
|
||||||
|
markResultFailed(fr, ex.getMessage() == null ? "assemble price-track result failed" : ex.getMessage());
|
||||||
|
priceTrackTaskCacheService.removeShopMergedPayload(taskId, shopKey);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (!changed) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
List<FileResultEntity> latest = fileResultMapper.selectList(
|
||||||
|
new LambdaQueryWrapper<FileResultEntity>()
|
||||||
|
.eq(FileResultEntity::getTaskId, taskId)
|
||||||
|
.eq(FileResultEntity::getModuleType, MODULE_TYPE)
|
||||||
|
.orderByAsc(FileResultEntity::getId));
|
||||||
|
updateTaskStatusFromLatestRows(task, latest);
|
||||||
}
|
}
|
||||||
if ("SUCCESS".equals(task.getStatus()) || "FAILED".equals(task.getStatus())) {
|
|
||||||
log.warn("[price-track] submitResult rejected taskId={} status={}", taskId, task.getStatus());
|
|
||||||
throw new BusinessException(40901, "任务已结束,拒绝重复提交");
|
|
||||||
}
|
|
||||||
priceTrackTaskCacheService.touchTaskHeartbeat(taskId);
|
|
||||||
List<FileResultEntity> resultRows = fileResultMapper.selectList(
|
|
||||||
new LambdaQueryWrapper<FileResultEntity>()
|
|
||||||
.eq(FileResultEntity::getTaskId, taskId)
|
|
||||||
.eq(FileResultEntity::getModuleType, MODULE_TYPE)
|
|
||||||
.orderByAsc(FileResultEntity::getId));
|
|
||||||
Map<String, PriceTrackSubmitResultRequest.ShopResult> payloadByShop = new LinkedHashMap<>();
|
|
||||||
for (PriceTrackSubmitResultRequest.ShopResult sr : request.getShops()) {
|
|
||||||
if (sr == null) continue;
|
|
||||||
String key = ziniaoShopSwitchService.normalizeShopName(sr.getShopName());
|
|
||||||
if (!key.isBlank()) payloadByShop.put(key, sr);
|
|
||||||
}
|
|
||||||
log.info("[price-track] submitResult received taskId={} payloadShopCount={} payloadKeys={}",
|
|
||||||
taskId, payloadByShop.size(), payloadByShop.keySet());
|
|
||||||
boolean changed = false;
|
|
||||||
for (FileResultEntity fr : resultRows) {
|
|
||||||
String shopKey = fr.getSourceFilename();
|
|
||||||
PriceTrackSubmitResultRequest.ShopResult payload = shopKey == null ? null : payloadByShop.get(shopKey);
|
|
||||||
if (payload == null) {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
changed = true;
|
|
||||||
if (payload.getError() != null && !payload.getError().isBlank()) {
|
|
||||||
markResultFailed(fr, payload.getError());
|
|
||||||
priceTrackTaskCacheService.removeShopMergedPayload(taskId, shopKey);
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
if (Boolean.FALSE.equals(payload.getSuccess())) {
|
|
||||||
markResultFailed(fr, "shop processing failed");
|
|
||||||
priceTrackTaskCacheService.removeShopMergedPayload(taskId, shopKey);
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
handleSkipAsinDeletionSignals(shopKey, payload);
|
|
||||||
PriceTrackSubmitResultRequest.ShopResult merged = mergeShopPayload(taskId, shopKey, payload);
|
|
||||||
if (!Boolean.TRUE.equals(merged.getSuccess())) {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
if (countPayloadRows(merged) <= 0) {
|
|
||||||
markResultFailed(fr, "no usable price-track rows received");
|
|
||||||
priceTrackTaskCacheService.removeShopMergedPayload(taskId, shopKey);
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
try {
|
|
||||||
enqueueResultFileAssembly(fr, shopKey, merged);
|
|
||||||
priceTrackTaskCacheService.removeShopMergedPayload(taskId, shopKey);
|
|
||||||
} catch (Exception ex) {
|
|
||||||
markResultFailed(fr, ex.getMessage() == null ? "assemble price-track result failed" : ex.getMessage());
|
|
||||||
priceTrackTaskCacheService.removeShopMergedPayload(taskId, shopKey);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (!changed) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
List<FileResultEntity> latest = fileResultMapper.selectList(
|
|
||||||
new LambdaQueryWrapper<FileResultEntity>()
|
|
||||||
.eq(FileResultEntity::getTaskId, taskId)
|
|
||||||
.eq(FileResultEntity::getModuleType, MODULE_TYPE)
|
|
||||||
.orderByAsc(FileResultEntity::getId));
|
|
||||||
updateTaskStatusFromLatestRows(task, latest);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public boolean tryFinalizeTask(Long taskId, boolean fromCompensation) {
|
public boolean tryFinalizeTask(Long taskId, boolean fromCompensation) {
|
||||||
if (taskId == null || taskId <= 0) return false;
|
if (taskId == null || taskId <= 0) return false;
|
||||||
FileTaskEntity task = loadTaskForExecution(taskId);
|
TaskDistributedLockService.LockHandle lockHandle = acquireTaskLock(taskId);
|
||||||
if (task == null || !MODULE_TYPE.equals(task.getModuleType())) return false;
|
if (lockHandle == null) {
|
||||||
if (!"RUNNING".equals(task.getStatus())) {
|
log.info("[price-track] tryFinalizeTask skipped because task lock is busy taskId={} fromCompensation={}", taskId, fromCompensation);
|
||||||
log.warn("[price-track] stale finalize skipped taskId={} status={} fromCompensation={}",
|
|
||||||
taskId, task.getStatus(), fromCompensation);
|
|
||||||
cleanupTaskCacheIfTerminal(taskId, task.getStatus());
|
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
List<FileResultEntity> latest = fileResultMapper.selectList(
|
try (lockHandle) {
|
||||||
new LambdaQueryWrapper<FileResultEntity>()
|
FileTaskEntity task = loadTaskForExecution(taskId);
|
||||||
.eq(FileResultEntity::getTaskId, taskId)
|
if (task == null || !MODULE_TYPE.equals(task.getModuleType())) return false;
|
||||||
.eq(FileResultEntity::getModuleType, MODULE_TYPE)
|
if (!"RUNNING".equals(task.getStatus())) {
|
||||||
.orderByAsc(FileResultEntity::getId));
|
log.warn("[price-track] stale finalize skipped taskId={} status={} fromCompensation={}",
|
||||||
if (latest.isEmpty()) {
|
taskId, task.getStatus(), fromCompensation);
|
||||||
task.setStatus("FAILED");
|
cleanupTaskCacheIfTerminal(taskId, task.getStatus());
|
||||||
task.setErrorMessage("task details missing, cannot finalize automatically");
|
return false;
|
||||||
task.setUpdatedAt(LocalDateTime.now());
|
}
|
||||||
task.setFinishedAt(LocalDateTime.now());
|
List<FileResultEntity> latest = fileResultMapper.selectList(
|
||||||
fileTaskMapper.updateById(task);
|
new LambdaQueryWrapper<FileResultEntity>()
|
||||||
cleanupTaskCacheIfTerminal(taskId, task.getStatus());
|
.eq(FileResultEntity::getTaskId, taskId)
|
||||||
|
.eq(FileResultEntity::getModuleType, MODULE_TYPE)
|
||||||
|
.orderByAsc(FileResultEntity::getId));
|
||||||
|
if (latest.isEmpty()) {
|
||||||
|
task.setStatus("FAILED");
|
||||||
|
task.setErrorMessage("task details missing, cannot finalize automatically");
|
||||||
|
task.setUpdatedAt(LocalDateTime.now());
|
||||||
|
task.setFinishedAt(LocalDateTime.now());
|
||||||
|
fileTaskMapper.updateById(task);
|
||||||
|
cleanupTaskCacheIfTerminal(taskId, task.getStatus());
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
Map<String, PriceTrackSubmitResultRequest.ShopResult> cachedPayloadByShop =
|
||||||
|
priceTrackTaskCacheService.getAllShopMergedPayload(taskId);
|
||||||
|
boolean changed = false;
|
||||||
|
for (FileResultEntity fr : latest) {
|
||||||
|
boolean success = fr.getSuccess() != null && fr.getSuccess() == 1;
|
||||||
|
boolean failed = fr.getErrorMessage() != null && !fr.getErrorMessage().isBlank();
|
||||||
|
if (success || failed) continue;
|
||||||
|
String shopKey = fr.getSourceFilename();
|
||||||
|
PriceTrackSubmitResultRequest.ShopResult cachedPayload = shopKey == null ? null : cachedPayloadByShop.get(shopKey);
|
||||||
|
if (cachedPayload == null) {
|
||||||
|
markResultFailed(fr, "Python interrupted before this shop finished uploading results");
|
||||||
|
changed = true;
|
||||||
|
log.warn("[price-track] stale finalize failed without cache taskId={} shop={} fromCompensation={}",
|
||||||
|
taskId, shopKey, fromCompensation);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if (cachedPayload.getError() != null && !cachedPayload.getError().isBlank()) {
|
||||||
|
markResultFailed(fr, cachedPayload.getError());
|
||||||
|
priceTrackTaskCacheService.removeShopMergedPayload(taskId, shopKey);
|
||||||
|
changed = true;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if (countPayloadRows(cachedPayload) <= 0) {
|
||||||
|
markResultFailed(fr, "no usable price-track rows received before interruption");
|
||||||
|
priceTrackTaskCacheService.removeShopMergedPayload(taskId, shopKey);
|
||||||
|
changed = true;
|
||||||
|
log.warn("[price-track] stale finalize finished without data taskId={} shop={} fromCompensation={}",
|
||||||
|
taskId, shopKey, fromCompensation);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
enqueueResultFileAssembly(fr, shopKey, cachedPayload);
|
||||||
|
priceTrackTaskCacheService.removeShopMergedPayload(taskId, shopKey);
|
||||||
|
changed = true;
|
||||||
|
log.warn("[price-track] stale finalize assembled taskId={} shop={} fromCompensation={} completedFlag={} rowCount={}",
|
||||||
|
taskId, shopKey, fromCompensation, Boolean.TRUE.equals(cachedPayload.getSuccess()), countPayloadRows(cachedPayload));
|
||||||
|
} catch (Exception ex) {
|
||||||
|
String message = ex.getMessage() == null ? "assemble price-track result failed" : ex.getMessage();
|
||||||
|
markResultFailed(fr, message);
|
||||||
|
priceTrackTaskCacheService.removeShopMergedPayload(taskId, shopKey);
|
||||||
|
changed = true;
|
||||||
|
log.warn("[price-track] stale finalize assemble failed taskId={} shop={} msg={}",
|
||||||
|
taskId, shopKey, message);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (!changed && cachedPayloadByShop.isEmpty()) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
List<FileResultEntity> refreshed = fileResultMapper.selectList(
|
||||||
|
new LambdaQueryWrapper<FileResultEntity>()
|
||||||
|
.eq(FileResultEntity::getTaskId, taskId)
|
||||||
|
.eq(FileResultEntity::getModuleType, MODULE_TYPE)
|
||||||
|
.orderByAsc(FileResultEntity::getId));
|
||||||
|
updateTaskStatusFromLatestRows(task, refreshed);
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
Map<String, PriceTrackSubmitResultRequest.ShopResult> cachedPayloadByShop =
|
|
||||||
priceTrackTaskCacheService.getAllShopMergedPayload(taskId);
|
|
||||||
boolean changed = false;
|
|
||||||
for (FileResultEntity fr : latest) {
|
|
||||||
boolean success = fr.getSuccess() != null && fr.getSuccess() == 1;
|
|
||||||
boolean failed = fr.getErrorMessage() != null && !fr.getErrorMessage().isBlank();
|
|
||||||
if (success || failed) continue;
|
|
||||||
String shopKey = fr.getSourceFilename();
|
|
||||||
PriceTrackSubmitResultRequest.ShopResult cachedPayload = shopKey == null ? null : cachedPayloadByShop.get(shopKey);
|
|
||||||
if (cachedPayload == null) {
|
|
||||||
markResultFailed(fr, "Python interrupted before this shop finished uploading results");
|
|
||||||
changed = true;
|
|
||||||
log.warn("[price-track] stale finalize failed without cache taskId={} shop={} fromCompensation={}",
|
|
||||||
taskId, shopKey, fromCompensation);
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
if (cachedPayload.getError() != null && !cachedPayload.getError().isBlank()) {
|
|
||||||
markResultFailed(fr, cachedPayload.getError());
|
|
||||||
priceTrackTaskCacheService.removeShopMergedPayload(taskId, shopKey);
|
|
||||||
changed = true;
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
if (countPayloadRows(cachedPayload) <= 0) {
|
|
||||||
markResultFailed(fr, "no usable price-track rows received before interruption");
|
|
||||||
priceTrackTaskCacheService.removeShopMergedPayload(taskId, shopKey);
|
|
||||||
changed = true;
|
|
||||||
log.warn("[price-track] stale finalize finished without data taskId={} shop={} fromCompensation={}",
|
|
||||||
taskId, shopKey, fromCompensation);
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
try {
|
|
||||||
enqueueResultFileAssembly(fr, shopKey, cachedPayload);
|
|
||||||
priceTrackTaskCacheService.removeShopMergedPayload(taskId, shopKey);
|
|
||||||
changed = true;
|
|
||||||
log.warn("[price-track] stale finalize assembled taskId={} shop={} fromCompensation={} completedFlag={} rowCount={}",
|
|
||||||
taskId, shopKey, fromCompensation, Boolean.TRUE.equals(cachedPayload.getSuccess()), countPayloadRows(cachedPayload));
|
|
||||||
} catch (Exception ex) {
|
|
||||||
String message = ex.getMessage() == null ? "assemble price-track result failed" : ex.getMessage();
|
|
||||||
markResultFailed(fr, message);
|
|
||||||
priceTrackTaskCacheService.removeShopMergedPayload(taskId, shopKey);
|
|
||||||
changed = true;
|
|
||||||
log.warn("[price-track] stale finalize assemble failed taskId={} shop={} msg={}",
|
|
||||||
taskId, shopKey, message);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (!changed && cachedPayloadByShop.isEmpty()) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
List<FileResultEntity> refreshed = fileResultMapper.selectList(
|
|
||||||
new LambdaQueryWrapper<FileResultEntity>()
|
|
||||||
.eq(FileResultEntity::getTaskId, taskId)
|
|
||||||
.eq(FileResultEntity::getModuleType, MODULE_TYPE)
|
|
||||||
.orderByAsc(FileResultEntity::getId));
|
|
||||||
updateTaskStatusFromLatestRows(task, refreshed);
|
|
||||||
return true;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public String resolveResultDownloadUrl(Long resultId, Long userId) {
|
public String resolveResultDownloadUrl(Long resultId, Long userId) {
|
||||||
|
|||||||
@@ -267,15 +267,17 @@ public class ProductRiskTaskService {
|
|||||||
if (userId == null || userId <= 0) {
|
if (userId == null || userId <= 0) {
|
||||||
throw new BusinessException("user_id 不合法");
|
throw new BusinessException("user_id 不合法");
|
||||||
}
|
}
|
||||||
FileTaskEntity task = loadTaskForExecution(taskId);
|
try (TaskDistributedLockService.LockHandle ignored = acquireTaskLockOrThrow(taskId)) {
|
||||||
if (task == null || !MODULE_TYPE.equals(task.getModuleType()) || !userId.equals(task.getUserId())) {
|
FileTaskEntity task = loadTaskForExecution(taskId);
|
||||||
throw new BusinessException("任务不存在");
|
if (task == null || !MODULE_TYPE.equals(task.getModuleType()) || !userId.equals(task.getUserId())) {
|
||||||
|
throw new BusinessException("任务不存在");
|
||||||
|
}
|
||||||
|
fileResultMapper.delete(new LambdaQueryWrapper<FileResultEntity>()
|
||||||
|
.eq(FileResultEntity::getTaskId, taskId)
|
||||||
|
.eq(FileResultEntity::getModuleType, MODULE_TYPE));
|
||||||
|
fileTaskMapper.deleteById(taskId);
|
||||||
|
cleanupTaskCacheIfTerminal(taskId, "DELETE_EMPTY");
|
||||||
}
|
}
|
||||||
fileResultMapper.delete(new LambdaQueryWrapper<FileResultEntity>()
|
|
||||||
.eq(FileResultEntity::getTaskId, taskId)
|
|
||||||
.eq(FileResultEntity::getModuleType, MODULE_TYPE));
|
|
||||||
fileTaskMapper.deleteById(taskId);
|
|
||||||
cleanupTaskCacheIfTerminal(taskId, "DELETE_EMPTY");
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -550,146 +552,150 @@ public class ProductRiskTaskService {
|
|||||||
if (request == null || request.getShops() == null || request.getShops().isEmpty()) {
|
if (request == null || request.getShops() == null || request.getShops().isEmpty()) {
|
||||||
throw new BusinessException("shops 不能为空");
|
throw new BusinessException("shops 不能为空");
|
||||||
}
|
}
|
||||||
|
try (TaskDistributedLockService.LockHandle ignored = acquireTaskLockOrThrow(taskId)) {
|
||||||
|
FileTaskEntity task = loadTaskForExecution(taskId);
|
||||||
|
if (task == null || !MODULE_TYPE.equals(task.getModuleType())) {
|
||||||
|
throw new BusinessException("任务不存在");
|
||||||
|
}
|
||||||
|
if ("SUCCESS".equals(task.getStatus()) || "FAILED".equals(task.getStatus())) {
|
||||||
|
log.warn("[product-risk] submitResult rejected taskId={} status={} payloadShopCount={}",
|
||||||
|
taskId, task.getStatus(), request.getShops() == null ? 0 : request.getShops().size());
|
||||||
|
throw new BusinessException(40901, "任务已结束,拒绝重复提交");
|
||||||
|
}
|
||||||
|
String oldStatus = task.getStatus();
|
||||||
|
|
||||||
FileTaskEntity task = loadTaskForExecution(taskId);
|
List<FileResultEntity> resultRows = fileResultMapper.selectList(new LambdaQueryWrapper<FileResultEntity>()
|
||||||
if (task == null || !MODULE_TYPE.equals(task.getModuleType())) {
|
.eq(FileResultEntity::getTaskId, taskId)
|
||||||
throw new BusinessException("任务不存在");
|
.eq(FileResultEntity::getModuleType, MODULE_TYPE)
|
||||||
|
.orderByAsc(FileResultEntity::getId));
|
||||||
|
|
||||||
|
Map<String, ProductRiskShopPayloadDto> payloadByShop = new LinkedHashMap<>();
|
||||||
|
for (ProductRiskShopPayloadDto p : request.getShops()) {
|
||||||
|
if (p == null) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
String key = ziniaoShopSwitchService.normalizeShopName(p.getShopName());
|
||||||
|
if (!key.isBlank()) {
|
||||||
|
payloadByShop.put(key, p);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
log.info("[product-risk] submitResult received taskId={} payloadShopCount={} payloadKeys={}",
|
||||||
|
taskId, payloadByShop.size(), payloadByShop.keySet());
|
||||||
|
|
||||||
|
File workRoot = FileUtil.mkdir(FileUtil.file(System.getProperty("java.io.tmpdir"), "product-risk-result", String.valueOf(taskId)));
|
||||||
|
|
||||||
|
List<String> batchErrors = new ArrayList<>();
|
||||||
|
int matchedShopCount = 0;
|
||||||
|
int skippedUnmatchedCount = 0;
|
||||||
|
int waitingCount = 0;
|
||||||
|
int assembledCount = 0;
|
||||||
|
int fallbackMatchedCount = 0;
|
||||||
|
|
||||||
|
for (FileResultEntity fr : resultRows) {
|
||||||
|
String shopKey = fr.getSourceFilename();
|
||||||
|
ProductRiskShopPayloadDto payload = shopKey == null ? null : payloadByShop.get(shopKey);
|
||||||
|
|
||||||
|
if (payload == null && resultRows.size() == 1 && payloadByShop.size() == 1) {
|
||||||
|
payload = payloadByShop.values().iterator().next();
|
||||||
|
fallbackMatchedCount++;
|
||||||
|
log.warn("[product-risk] single-shop fallback matched taskId={} dbShop={} payloadShop={}",
|
||||||
|
taskId, shopKey, payload.getShopName());
|
||||||
|
}
|
||||||
|
|
||||||
|
if (payload == null) {
|
||||||
|
skippedUnmatchedCount++;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
matchedShopCount++;
|
||||||
|
if (payload.getError() != null && !payload.getError().isBlank()) {
|
||||||
|
markResultFailed(fr, payload.getError());
|
||||||
|
batchErrors.add(shopKey + ": " + payload.getError());
|
||||||
|
productRiskTaskCacheService.removeShopMergedPayload(taskId, shopKey);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
ProductRiskShopPayloadDto mergedPayload = mergeShopPayload(taskId, shopKey, payload);
|
||||||
|
boolean incomingShopDone = hasShopDoneSignal(payload);
|
||||||
|
int mergedRows = countPayloadRows(mergedPayload);
|
||||||
|
boolean shopDone = isShopPayloadCompleted(mergedPayload);
|
||||||
|
log.info("[product-risk] shop merged taskId={} shop={} incomingShopDone={} mergedDone={} mergedRows={} payloadCountries={} mergedShopDoneFlag={}",
|
||||||
|
taskId,
|
||||||
|
shopKey,
|
||||||
|
incomingShopDone,
|
||||||
|
shopDone,
|
||||||
|
mergedRows,
|
||||||
|
mergedPayload.getCountries() == null ? 0 : mergedPayload.getCountries().size(),
|
||||||
|
mergedPayload.getShopDone());
|
||||||
|
if (incomingShopDone && mergedRows <= 0) {
|
||||||
|
markResultNoData(fr, "没有数据");
|
||||||
|
productRiskTaskCacheService.removeShopMergedPayload(taskId, shopKey);
|
||||||
|
log.info("[product-risk] shop finished without data taskId={} shop={}", taskId, shopKey);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if (!shopDone) {
|
||||||
|
waitingCount++;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
enqueueResultFileAssembly(fr, shopKey, mergedPayload);
|
||||||
|
assembledCount++;
|
||||||
|
log.info("[product-risk] shop assembled taskId={} shop={} rowCount={} file={}",
|
||||||
|
taskId, shopKey, fr.getRowCount(), fr.getResultFilename());
|
||||||
|
productRiskTaskCacheService.removeShopMergedPayload(taskId, shopKey);
|
||||||
|
} catch (Exception ex) {
|
||||||
|
log.warn("[product-risk] shop assemble failed taskId={} shop={} msg={}", taskId, shopKey, ex.getMessage());
|
||||||
|
markResultFailed(fr, ex.getMessage() == null ? "组装失败" : ex.getMessage());
|
||||||
|
batchErrors.add(shopKey + ": " + ex.getMessage());
|
||||||
|
productRiskTaskCacheService.removeShopMergedPayload(taskId, shopKey);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
List<FileResultEntity> latest = fileResultMapper.selectList(new LambdaQueryWrapper<FileResultEntity>()
|
||||||
|
.eq(FileResultEntity::getTaskId, taskId)
|
||||||
|
.eq(FileResultEntity::getModuleType, MODULE_TYPE)
|
||||||
|
.orderByAsc(FileResultEntity::getId));
|
||||||
|
|
||||||
|
updateTaskStatusFromLatestRows(task, latest, batchErrors);
|
||||||
|
log.warn("[product-risk] submitResult status evaluated taskId={} oldStatus={} newStatus={} matchedShopCount={} skippedUnmatchedCount={} waitingCount={} assembledCount={} fallbackMatchedCount={} batchErrors={}",
|
||||||
|
taskId, oldStatus, task.getStatus(), matchedShopCount, skippedUnmatchedCount,
|
||||||
|
waitingCount, assembledCount, fallbackMatchedCount, batchErrors);
|
||||||
}
|
}
|
||||||
if ("SUCCESS".equals(task.getStatus()) || "FAILED".equals(task.getStatus())) {
|
|
||||||
log.warn("[product-risk] submitResult rejected taskId={} status={} payloadShopCount={}",
|
|
||||||
taskId, task.getStatus(), request.getShops() == null ? 0 : request.getShops().size());
|
|
||||||
throw new BusinessException(40901, "任务已结束,拒绝重复提交");
|
|
||||||
}
|
|
||||||
String oldStatus = task.getStatus();
|
|
||||||
|
|
||||||
List<FileResultEntity> resultRows = fileResultMapper.selectList(new LambdaQueryWrapper<FileResultEntity>()
|
|
||||||
.eq(FileResultEntity::getTaskId, taskId)
|
|
||||||
.eq(FileResultEntity::getModuleType, MODULE_TYPE)
|
|
||||||
.orderByAsc(FileResultEntity::getId));
|
|
||||||
|
|
||||||
Map<String, ProductRiskShopPayloadDto> payloadByShop = new LinkedHashMap<>();
|
|
||||||
for (ProductRiskShopPayloadDto p : request.getShops()) {
|
|
||||||
if (p == null) {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
String key = ziniaoShopSwitchService.normalizeShopName(p.getShopName());
|
|
||||||
if (!key.isBlank()) {
|
|
||||||
payloadByShop.put(key, p);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
log.info("[product-risk] submitResult received taskId={} payloadShopCount={} payloadKeys={}",
|
|
||||||
taskId, payloadByShop.size(), payloadByShop.keySet());
|
|
||||||
|
|
||||||
File workRoot = FileUtil.mkdir(FileUtil.file(System.getProperty("java.io.tmpdir"), "product-risk-result", String.valueOf(taskId)));
|
|
||||||
|
|
||||||
List<String> batchErrors = new ArrayList<>();
|
|
||||||
int matchedShopCount = 0;
|
|
||||||
int skippedUnmatchedCount = 0;
|
|
||||||
int waitingCount = 0;
|
|
||||||
int assembledCount = 0;
|
|
||||||
int fallbackMatchedCount = 0;
|
|
||||||
|
|
||||||
for (FileResultEntity fr : resultRows) {
|
|
||||||
String shopKey = fr.getSourceFilename();
|
|
||||||
ProductRiskShopPayloadDto payload = shopKey == null ? null : payloadByShop.get(shopKey);
|
|
||||||
|
|
||||||
// 兼容单店任务:如果规范化店名偶发不一致导致未命中,且任务与回传都只有 1 个店时按唯一店铺回填。
|
|
||||||
if (payload == null && resultRows.size() == 1 && payloadByShop.size() == 1) {
|
|
||||||
payload = payloadByShop.values().iterator().next();
|
|
||||||
fallbackMatchedCount++;
|
|
||||||
log.warn("[product-risk] single-shop fallback matched taskId={} dbShop={} payloadShop={}",
|
|
||||||
taskId, shopKey, payload.getShopName());
|
|
||||||
}
|
|
||||||
|
|
||||||
if (payload == null) {
|
|
||||||
skippedUnmatchedCount++;
|
|
||||||
// 与删除品牌一致:支持队列逐条处理、分次回传,本次 payload 未包含的店铺保持待处理。
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
matchedShopCount++;
|
|
||||||
if (payload.getError() != null && !payload.getError().isBlank()) {
|
|
||||||
markResultFailed(fr, payload.getError());
|
|
||||||
batchErrors.add(shopKey + ": " + payload.getError());
|
|
||||||
productRiskTaskCacheService.removeShopMergedPayload(taskId, shopKey);
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
// 分次上报累积:合并到 Redis 缓存,再按“合并后是否完成”决定是否出包。
|
|
||||||
ProductRiskShopPayloadDto mergedPayload = mergeShopPayload(taskId, shopKey, payload);
|
|
||||||
boolean incomingShopDone = hasShopDoneSignal(payload);
|
|
||||||
int mergedRows = countPayloadRows(mergedPayload);
|
|
||||||
boolean shopDone = isShopPayloadCompleted(mergedPayload);
|
|
||||||
log.info("[product-risk] shop merged taskId={} shop={} incomingShopDone={} mergedDone={} mergedRows={} payloadCountries={} mergedShopDoneFlag={}",
|
|
||||||
taskId,
|
|
||||||
shopKey,
|
|
||||||
incomingShopDone,
|
|
||||||
shopDone,
|
|
||||||
mergedRows,
|
|
||||||
mergedPayload.getCountries() == null ? 0 : mergedPayload.getCountries().size(),
|
|
||||||
mergedPayload.getShopDone());
|
|
||||||
if (incomingShopDone && mergedRows <= 0) {
|
|
||||||
markResultNoData(fr, "没有数据");
|
|
||||||
productRiskTaskCacheService.removeShopMergedPayload(taskId, shopKey);
|
|
||||||
log.info("[product-risk] shop finished without data taskId={} shop={}", taskId, shopKey);
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
if (!shopDone) {
|
|
||||||
waitingCount++;
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
|
||||||
enqueueResultFileAssembly(fr, shopKey, mergedPayload);
|
|
||||||
assembledCount++;
|
|
||||||
log.info("[product-risk] shop assembled taskId={} shop={} rowCount={} file={}",
|
|
||||||
taskId, shopKey, fr.getRowCount(), fr.getResultFilename());
|
|
||||||
productRiskTaskCacheService.removeShopMergedPayload(taskId, shopKey);
|
|
||||||
} catch (Exception ex) {
|
|
||||||
log.warn("[product-risk] shop assemble failed taskId={} shop={} msg={}", taskId, shopKey, ex.getMessage());
|
|
||||||
markResultFailed(fr, ex.getMessage() == null ? "组装失败" : ex.getMessage());
|
|
||||||
batchErrors.add(shopKey + ": " + ex.getMessage());
|
|
||||||
productRiskTaskCacheService.removeShopMergedPayload(taskId, shopKey);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
List<FileResultEntity> latest = fileResultMapper.selectList(new LambdaQueryWrapper<FileResultEntity>()
|
|
||||||
.eq(FileResultEntity::getTaskId, taskId)
|
|
||||||
.eq(FileResultEntity::getModuleType, MODULE_TYPE)
|
|
||||||
.orderByAsc(FileResultEntity::getId));
|
|
||||||
|
|
||||||
updateTaskStatusFromLatestRows(task, latest, batchErrors);
|
|
||||||
log.warn("[product-risk] submitResult status evaluated taskId={} oldStatus={} newStatus={} matchedShopCount={} skippedUnmatchedCount={} waitingCount={} assembledCount={} fallbackMatchedCount={} batchErrors={}",
|
|
||||||
taskId, oldStatus, task.getStatus(), matchedShopCount, skippedUnmatchedCount,
|
|
||||||
waitingCount, assembledCount, fallbackMatchedCount, batchErrors);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public boolean tryFinalizeTask(Long taskId, boolean fromCompensation) {
|
public boolean tryFinalizeTask(Long taskId, boolean fromCompensation) {
|
||||||
if (taskId == null || taskId <= 0) {
|
if (taskId == null || taskId <= 0) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
FileTaskEntity task = loadTaskForExecution(taskId);
|
TaskDistributedLockService.LockHandle lockHandle = acquireTaskLock(taskId);
|
||||||
if (task == null || !MODULE_TYPE.equals(task.getModuleType())) {
|
if (lockHandle == null) {
|
||||||
|
log.info("[product-risk] tryFinalizeTask skipped because task lock is busy taskId={} fromCompensation={}", taskId, fromCompensation);
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
if ("SUCCESS".equals(task.getStatus()) || "FAILED".equals(task.getStatus())) {
|
try (lockHandle) {
|
||||||
log.warn("[product-risk] stale finalize skipped taskId={} status={} fromCompensation={}",
|
FileTaskEntity task = loadTaskForExecution(taskId);
|
||||||
taskId, task.getStatus(), fromCompensation);
|
if (task == null || !MODULE_TYPE.equals(task.getModuleType())) {
|
||||||
return true;
|
return false;
|
||||||
}
|
}
|
||||||
String oldStatus = task.getStatus();
|
if ("SUCCESS".equals(task.getStatus()) || "FAILED".equals(task.getStatus())) {
|
||||||
|
log.warn("[product-risk] stale finalize skipped taskId={} status={} fromCompensation={}",
|
||||||
|
taskId, task.getStatus(), fromCompensation);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
String oldStatus = task.getStatus();
|
||||||
|
|
||||||
List<FileResultEntity> resultRows = fileResultMapper.selectList(new LambdaQueryWrapper<FileResultEntity>()
|
List<FileResultEntity> resultRows = fileResultMapper.selectList(new LambdaQueryWrapper<FileResultEntity>()
|
||||||
.eq(FileResultEntity::getTaskId, taskId)
|
.eq(FileResultEntity::getTaskId, taskId)
|
||||||
.eq(FileResultEntity::getModuleType, MODULE_TYPE)
|
.eq(FileResultEntity::getModuleType, MODULE_TYPE)
|
||||||
.orderByAsc(FileResultEntity::getId));
|
.orderByAsc(FileResultEntity::getId));
|
||||||
if (resultRows.isEmpty()) {
|
if (resultRows.isEmpty()) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
Map<String, ProductRiskShopPayloadDto> cachedPayloadByShop = productRiskTaskCacheService.getAllShopMergedPayload(taskId);
|
Map<String, ProductRiskShopPayloadDto> cachedPayloadByShop = productRiskTaskCacheService.getAllShopMergedPayload(taskId);
|
||||||
File workRoot = FileUtil.mkdir(FileUtil.file(System.getProperty("java.io.tmpdir"), "product-risk-result", String.valueOf(taskId)));
|
File workRoot = FileUtil.mkdir(FileUtil.file(System.getProperty("java.io.tmpdir"), "product-risk-result", String.valueOf(taskId)));
|
||||||
List<String> batchErrors = new ArrayList<>();
|
List<String> batchErrors = new ArrayList<>();
|
||||||
boolean changed = false;
|
boolean changed = false;
|
||||||
|
|
||||||
for (FileResultEntity fr : resultRows) {
|
for (FileResultEntity fr : resultRows) {
|
||||||
boolean success = fr.getSuccess() != null && fr.getSuccess() == 1;
|
boolean success = fr.getSuccess() != null && fr.getSuccess() == 1;
|
||||||
@@ -754,18 +760,19 @@ public class ProductRiskTaskService {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!changed && cachedPayloadByShop.isEmpty()) {
|
if (!changed && cachedPayloadByShop.isEmpty()) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
List<FileResultEntity> latest = fileResultMapper.selectList(new LambdaQueryWrapper<FileResultEntity>()
|
List<FileResultEntity> latest = fileResultMapper.selectList(new LambdaQueryWrapper<FileResultEntity>()
|
||||||
.eq(FileResultEntity::getTaskId, taskId)
|
.eq(FileResultEntity::getTaskId, taskId)
|
||||||
.eq(FileResultEntity::getModuleType, MODULE_TYPE)
|
.eq(FileResultEntity::getModuleType, MODULE_TYPE)
|
||||||
.orderByAsc(FileResultEntity::getId));
|
.orderByAsc(FileResultEntity::getId));
|
||||||
updateTaskStatusFromLatestRows(task, latest, batchErrors);
|
updateTaskStatusFromLatestRows(task, latest, batchErrors);
|
||||||
log.warn("[product-risk] stale finalize status evaluated taskId={} oldStatus={} newStatus={} changed={} cachedPayloadCount={} fromCompensation={} batchErrors={}",
|
log.warn("[product-risk] stale finalize status evaluated taskId={} oldStatus={} newStatus={} changed={} cachedPayloadCount={} fromCompensation={} batchErrors={}",
|
||||||
taskId, oldStatus, task.getStatus(), changed, cachedPayloadByShop.size(), fromCompensation, batchErrors);
|
taskId, oldStatus, task.getStatus(), changed, cachedPayloadByShop.size(), fromCompensation, batchErrors);
|
||||||
return true;
|
return true;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private void markResultFailed(FileResultEntity fr, String message) {
|
private void markResultFailed(FileResultEntity fr, String message) {
|
||||||
|
|||||||
@@ -14,6 +14,7 @@ import java.time.Duration;
|
|||||||
import java.time.Instant;
|
import java.time.Instant;
|
||||||
import java.util.ArrayList;
|
import java.util.ArrayList;
|
||||||
import java.util.LinkedHashMap;
|
import java.util.LinkedHashMap;
|
||||||
|
import java.util.List;
|
||||||
import java.util.Map;
|
import java.util.Map;
|
||||||
import java.util.concurrent.ConcurrentHashMap;
|
import java.util.concurrent.ConcurrentHashMap;
|
||||||
|
|
||||||
@@ -79,6 +80,41 @@ public class QueryAsinTaskCacheService {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public Map<Long, Long> getTaskHeartbeatMillisBatch(List<Long> taskIds) {
|
||||||
|
Map<Long, Long> result = new LinkedHashMap<>();
|
||||||
|
if (taskIds == null || taskIds.isEmpty()) {
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
List<Long> normalized = taskIds.stream()
|
||||||
|
.filter(id -> id != null && id > 0)
|
||||||
|
.distinct()
|
||||||
|
.toList();
|
||||||
|
if (normalized.isEmpty()) {
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
List<String> keys = normalized.stream().map(this::buildTaskHeartbeatKey).toList();
|
||||||
|
List<String> values;
|
||||||
|
try {
|
||||||
|
values = stringRedisTemplate.opsForValue().multiGet(keys);
|
||||||
|
} catch (Exception ex) {
|
||||||
|
log.warn("[query-asin-cache] batch get heartbeat degraded taskIds={} msg={}", normalized, ex.getMessage());
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
for (int i = 0; i < normalized.size(); i++) {
|
||||||
|
String raw = values != null && i < values.size() ? values.get(i) : null;
|
||||||
|
if (raw == null || raw.isBlank()) {
|
||||||
|
result.put(normalized.get(i), 0L);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
result.put(normalized.get(i), Long.parseLong(raw));
|
||||||
|
} catch (NumberFormatException ignored) {
|
||||||
|
result.put(normalized.get(i), 0L);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
public void touchTaskHeartbeat(Long taskId) {
|
public void touchTaskHeartbeat(Long taskId) {
|
||||||
try {
|
try {
|
||||||
stringRedisTemplate.opsForValue().set(
|
stringRedisTemplate.opsForValue().set(
|
||||||
|
|||||||
@@ -328,6 +328,7 @@ public class QueryAsinTaskService {
|
|||||||
throw new BusinessException("shops 不能为空");
|
throw new BusinessException("shops 不能为空");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
try (TaskDistributedLockService.LockHandle ignored = acquireTaskLockOrThrow(taskId)) {
|
||||||
FileTaskEntity task = loadTaskForExecution(taskId);
|
FileTaskEntity task = loadTaskForExecution(taskId);
|
||||||
if (task == null || !MODULE_TYPE.equals(task.getModuleType())) {
|
if (task == null || !MODULE_TYPE.equals(task.getModuleType())) {
|
||||||
throw new BusinessException("任务不存在");
|
throw new BusinessException("任务不存在");
|
||||||
@@ -370,12 +371,19 @@ public class QueryAsinTaskService {
|
|||||||
fileTaskMapper.updateById(task);
|
fileTaskMapper.updateById(task);
|
||||||
taskCacheService.saveTaskCache(task);
|
taskCacheService.saveTaskCache(task);
|
||||||
tryFinalizeTask(taskId, false);
|
tryFinalizeTask(taskId, false);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public boolean tryFinalizeTask(Long taskId, boolean fromCompensation) {
|
public boolean tryFinalizeTask(Long taskId, boolean fromCompensation) {
|
||||||
if (taskId == null || taskId <= 0) {
|
if (taskId == null || taskId <= 0) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
TaskDistributedLockService.LockHandle lockHandle = acquireTaskLock(taskId);
|
||||||
|
if (lockHandle == null) {
|
||||||
|
log.info("[query-asin] tryFinalizeTask skipped because task lock is busy taskId={} fromCompensation={}", taskId, fromCompensation);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
try (lockHandle) {
|
||||||
FileTaskEntity task = loadTaskForExecution(taskId);
|
FileTaskEntity task = loadTaskForExecution(taskId);
|
||||||
if (task == null || !MODULE_TYPE.equals(task.getModuleType())) {
|
if (task == null || !MODULE_TYPE.equals(task.getModuleType())) {
|
||||||
return false;
|
return false;
|
||||||
@@ -446,6 +454,7 @@ public class QueryAsinTaskService {
|
|||||||
fileTaskMapper.updateById(task);
|
fileTaskMapper.updateById(task);
|
||||||
taskCacheService.saveTaskCache(task);
|
taskCacheService.saveTaskCache(task);
|
||||||
return changed;
|
return changed;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@Transactional
|
@Transactional
|
||||||
@@ -455,11 +464,13 @@ public class QueryAsinTaskService {
|
|||||||
if (task == null || !MODULE_TYPE.equals(task.getModuleType()) || !userId.equals(task.getUserId())) {
|
if (task == null || !MODULE_TYPE.equals(task.getModuleType()) || !userId.equals(task.getUserId())) {
|
||||||
throw new BusinessException("任务不存在");
|
throw new BusinessException("任务不存在");
|
||||||
}
|
}
|
||||||
|
try (TaskDistributedLockService.LockHandle ignored = acquireTaskLockOrThrow(taskId)) {
|
||||||
fileResultMapper.delete(new LambdaQueryWrapper<FileResultEntity>()
|
fileResultMapper.delete(new LambdaQueryWrapper<FileResultEntity>()
|
||||||
.eq(FileResultEntity::getTaskId, taskId)
|
.eq(FileResultEntity::getTaskId, taskId)
|
||||||
.eq(FileResultEntity::getModuleType, MODULE_TYPE));
|
.eq(FileResultEntity::getModuleType, MODULE_TYPE));
|
||||||
fileTaskMapper.deleteById(taskId);
|
fileTaskMapper.deleteById(taskId);
|
||||||
taskCacheService.deleteTaskCache(taskId);
|
taskCacheService.deleteTaskCache(taskId);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@Transactional
|
@Transactional
|
||||||
|
|||||||
@@ -275,11 +275,13 @@ public class ShopMatchTaskService {
|
|||||||
if (task == null || !MODULE_TYPE.equals(task.getModuleType()) || !userId.equals(task.getUserId())) {
|
if (task == null || !MODULE_TYPE.equals(task.getModuleType()) || !userId.equals(task.getUserId())) {
|
||||||
throw new BusinessException("任务不存在");
|
throw new BusinessException("任务不存在");
|
||||||
}
|
}
|
||||||
fileResultMapper.delete(new LambdaQueryWrapper<FileResultEntity>()
|
try (TaskDistributedLockService.LockHandle ignored = acquireTaskLockOrThrow(taskId)) {
|
||||||
.eq(FileResultEntity::getTaskId, taskId)
|
fileResultMapper.delete(new LambdaQueryWrapper<FileResultEntity>()
|
||||||
.eq(FileResultEntity::getModuleType, MODULE_TYPE));
|
.eq(FileResultEntity::getTaskId, taskId)
|
||||||
fileTaskMapper.deleteById(taskId);
|
.eq(FileResultEntity::getModuleType, MODULE_TYPE));
|
||||||
shopMatchTaskCacheService.deleteTaskCache(taskId);
|
fileTaskMapper.deleteById(taskId);
|
||||||
|
shopMatchTaskCacheService.deleteTaskCache(taskId);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private void reconcileTaskAfterResultRemoval(Long taskId) {
|
private void reconcileTaskAfterResultRemoval(Long taskId) {
|
||||||
@@ -452,42 +454,44 @@ public class ShopMatchTaskService {
|
|||||||
if (stageIndex == null || stageIndex < 0) {
|
if (stageIndex == null || stageIndex < 0) {
|
||||||
throw new BusinessException("stage_index 不合法");
|
throw new BusinessException("stage_index 不合法");
|
||||||
}
|
}
|
||||||
FileTaskEntity task = loadTaskForExecution(taskId);
|
try (TaskDistributedLockService.LockHandle ignored = acquireTaskLockOrThrow(taskId)) {
|
||||||
if (task == null || !MODULE_TYPE.equals(task.getModuleType()) || !userId.equals(task.getUserId())) {
|
FileTaskEntity task = loadTaskForExecution(taskId);
|
||||||
throw new BusinessException("任务不存在");
|
if (task == null || !MODULE_TYPE.equals(task.getModuleType()) || !userId.equals(task.getUserId())) {
|
||||||
|
throw new BusinessException("任务不存在");
|
||||||
|
}
|
||||||
|
if ("SUCCESS".equals(task.getStatus()) || "FAILED".equals(task.getStatus())) {
|
||||||
|
throw new BusinessException("任务状态不正确");
|
||||||
|
}
|
||||||
|
if (!"SCHEDULED".equals(task.getStatus())) {
|
||||||
|
throw new BusinessException("任务状态不正确");
|
||||||
|
}
|
||||||
|
LocalDateTime now = now();
|
||||||
|
if (task.getScheduledAt() != null && now.isBefore(task.getScheduledAt().minusSeconds(90))) {
|
||||||
|
log.warn("[shop-match] activate rejected taskId={} stageIndex={} now={} scheduledAt={} earliestActivateAt={}",
|
||||||
|
taskId, stageIndex, now, task.getScheduledAt(), task.getScheduledAt().minusSeconds(90));
|
||||||
|
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("轮次配置缺失");
|
||||||
|
}
|
||||||
|
FileTaskEntity runningTask = findOtherRunningTask(userId, taskId);
|
||||||
|
if (runningTask != null) {
|
||||||
|
log.warn("[shop-match] activate serialized taskId={} stageIndex={} blockedByTaskId={} blockedUpdatedAt={}",
|
||||||
|
taskId, stageIndex, runningTask.getId(), runningTask.getUpdatedAt());
|
||||||
|
throw new BusinessException("当前已有定时匹配任务执行中,请等待上一任务执行结束后再重试");
|
||||||
|
}
|
||||||
|
state.setActiveStageIndex(stageIndex);
|
||||||
|
task.setStatus("RUNNING");
|
||||||
|
task.setUpdatedAt(now);
|
||||||
|
persistTaskRequest(task, state);
|
||||||
|
updateTaskAndRefreshCache(task);
|
||||||
|
shopMatchTaskCacheService.touchTaskHeartbeat(taskId);
|
||||||
}
|
}
|
||||||
if ("SUCCESS".equals(task.getStatus()) || "FAILED".equals(task.getStatus())) {
|
|
||||||
throw new BusinessException("任务状态不正确");
|
|
||||||
}
|
|
||||||
if (!"SCHEDULED".equals(task.getStatus())) {
|
|
||||||
throw new BusinessException("任务状态不正确");
|
|
||||||
}
|
|
||||||
LocalDateTime now = now();
|
|
||||||
if (task.getScheduledAt() != null && now.isBefore(task.getScheduledAt().minusSeconds(90))) {
|
|
||||||
log.warn("[shop-match] activate rejected taskId={} stageIndex={} now={} scheduledAt={} earliestActivateAt={}",
|
|
||||||
taskId, stageIndex, now, task.getScheduledAt(), task.getScheduledAt().minusSeconds(90));
|
|
||||||
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("轮次配置缺失");
|
|
||||||
}
|
|
||||||
FileTaskEntity runningTask = findOtherRunningTask(userId, taskId);
|
|
||||||
if (runningTask != null) {
|
|
||||||
log.warn("[shop-match] activate serialized taskId={} stageIndex={} blockedByTaskId={} blockedUpdatedAt={}",
|
|
||||||
taskId, stageIndex, runningTask.getId(), runningTask.getUpdatedAt());
|
|
||||||
throw new BusinessException("当前已有定时匹配任务执行中,请等待上一任务执行结束后再重试");
|
|
||||||
}
|
|
||||||
state.setActiveStageIndex(stageIndex);
|
|
||||||
task.setStatus("RUNNING");
|
|
||||||
task.setUpdatedAt(now);
|
|
||||||
persistTaskRequest(task, state);
|
|
||||||
updateTaskAndRefreshCache(task);
|
|
||||||
shopMatchTaskCacheService.touchTaskHeartbeat(taskId);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@Transactional
|
@Transactional
|
||||||
@@ -605,26 +609,32 @@ public class ShopMatchTaskService {
|
|||||||
if (taskId == null || taskId <= 0) {
|
if (taskId == null || taskId <= 0) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
FileTaskEntity task = loadTaskForExecution(taskId);
|
TaskDistributedLockService.LockHandle lockHandle = acquireTaskLock(taskId);
|
||||||
if (task == null || !MODULE_TYPE.equals(task.getModuleType())) {
|
if (lockHandle == null) {
|
||||||
|
log.info("[shop-match] tryFinalizeTask skipped because task lock is busy taskId={} fromCompensation={}", taskId, fromCompensation);
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
if ("SUCCESS".equals(task.getStatus()) || "FAILED".equals(task.getStatus())) {
|
try (lockHandle) {
|
||||||
return true;
|
FileTaskEntity task = loadTaskForExecution(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>()
|
List<FileResultEntity> resultRows = fileResultMapper.selectList(new LambdaQueryWrapper<FileResultEntity>()
|
||||||
.eq(FileResultEntity::getTaskId, taskId)
|
.eq(FileResultEntity::getTaskId, taskId)
|
||||||
.eq(FileResultEntity::getModuleType, MODULE_TYPE)
|
.eq(FileResultEntity::getModuleType, MODULE_TYPE)
|
||||||
.orderByAsc(FileResultEntity::getId));
|
.orderByAsc(FileResultEntity::getId));
|
||||||
if (resultRows.isEmpty()) {
|
if (resultRows.isEmpty()) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
Map<String, ShopMatchShopPayloadDto> cachedPayloadByShop = shopMatchTaskCacheService.getAllShopMergedPayload(taskId);
|
Map<String, ShopMatchShopPayloadDto> cachedPayloadByShop = shopMatchTaskCacheService.getAllShopMergedPayload(taskId);
|
||||||
File workRoot = FileUtil.mkdir(FileUtil.file(System.getProperty("java.io.tmpdir"), "shop-match-result", String.valueOf(taskId)));
|
File workRoot = FileUtil.mkdir(FileUtil.file(System.getProperty("java.io.tmpdir"), "shop-match-result", String.valueOf(taskId)));
|
||||||
List<String> batchErrors = new ArrayList<>();
|
List<String> batchErrors = new ArrayList<>();
|
||||||
boolean changed = false;
|
boolean changed = false;
|
||||||
|
|
||||||
for (FileResultEntity result : resultRows) {
|
for (FileResultEntity result : resultRows) {
|
||||||
boolean success = result.getSuccess() != null && result.getSuccess() == 1;
|
boolean success = result.getSuccess() != null && result.getSuccess() == 1;
|
||||||
@@ -689,16 +699,17 @@ public class ShopMatchTaskService {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!changed && cachedPayloadByShop.isEmpty()) {
|
if (!changed && cachedPayloadByShop.isEmpty()) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
List<FileResultEntity> latest = fileResultMapper.selectList(new LambdaQueryWrapper<FileResultEntity>()
|
List<FileResultEntity> latest = fileResultMapper.selectList(new LambdaQueryWrapper<FileResultEntity>()
|
||||||
.eq(FileResultEntity::getTaskId, taskId)
|
.eq(FileResultEntity::getTaskId, taskId)
|
||||||
.eq(FileResultEntity::getModuleType, MODULE_TYPE)
|
.eq(FileResultEntity::getModuleType, MODULE_TYPE)
|
||||||
.orderByAsc(FileResultEntity::getId));
|
.orderByAsc(FileResultEntity::getId));
|
||||||
updateTaskStatusFromLatestRows(task, latest, batchErrors);
|
updateTaskStatusFromLatestRows(task, latest, batchErrors);
|
||||||
return true;
|
return true;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private void assembleShopResult(FileResultEntity result, String shopKey, ShopMatchShopPayloadDto payload, File workRoot) {
|
private void assembleShopResult(FileResultEntity result, String shopKey, ShopMatchShopPayloadDto payload, File workRoot) {
|
||||||
|
|||||||
@@ -316,6 +316,7 @@ public class SimilarAsinTaskService {
|
|||||||
|
|
||||||
@Transactional
|
@Transactional
|
||||||
public void activateTask(Long taskId, Long userId) {
|
public void activateTask(Long taskId, Long userId) {
|
||||||
|
try (TaskDistributedLockService.LockHandle ignored = requireTaskLock(taskId, TASK_LOCK_WAIT_MILLIS)) {
|
||||||
FileTaskEntity task = fileTaskMapper.selectById(taskId);
|
FileTaskEntity task = fileTaskMapper.selectById(taskId);
|
||||||
if (task == null || !MODULE_TYPE.equals(task.getModuleType()) || userId != null && !Objects.equals(userId, task.getUserId())) {
|
if (task == null || !MODULE_TYPE.equals(task.getModuleType()) || userId != null && !Objects.equals(userId, task.getUserId())) {
|
||||||
throw new BusinessException("任务不存在");
|
throw new BusinessException("任务不存在");
|
||||||
@@ -327,6 +328,7 @@ public class SimilarAsinTaskService {
|
|||||||
task.setUpdatedAt(LocalDateTime.now());
|
task.setUpdatedAt(LocalDateTime.now());
|
||||||
fileTaskMapper.updateById(task);
|
fileTaskMapper.updateById(task);
|
||||||
taskCacheService.touchTaskHeartbeat(taskId);
|
taskCacheService.touchTaskHeartbeat(taskId);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public SimilarAsinDashboardVo dashboard(Long userId) {
|
public SimilarAsinDashboardVo dashboard(Long userId) {
|
||||||
@@ -546,6 +548,7 @@ public class SimilarAsinTaskService {
|
|||||||
|
|
||||||
@Transactional
|
@Transactional
|
||||||
public void deleteTask(Long taskId, Long userId) {
|
public void deleteTask(Long taskId, Long userId) {
|
||||||
|
try (TaskDistributedLockService.LockHandle ignored = requireTaskLock(taskId, TASK_LOCK_WAIT_MILLIS)) {
|
||||||
FileTaskEntity task = fileTaskMapper.selectById(taskId);
|
FileTaskEntity task = fileTaskMapper.selectById(taskId);
|
||||||
if (task == null || !MODULE_TYPE.equals(task.getModuleType()) || !Objects.equals(userId, task.getUserId())) {
|
if (task == null || !MODULE_TYPE.equals(task.getModuleType()) || !Objects.equals(userId, task.getUserId())) {
|
||||||
throw new BusinessException("任务不存在");
|
throw new BusinessException("任务不存在");
|
||||||
@@ -556,6 +559,7 @@ public class SimilarAsinTaskService {
|
|||||||
taskChunkMapper.delete(new LambdaQueryWrapper<TaskChunkEntity>().eq(TaskChunkEntity::getTaskId, taskId).eq(TaskChunkEntity::getModuleType, MODULE_TYPE));
|
taskChunkMapper.delete(new LambdaQueryWrapper<TaskChunkEntity>().eq(TaskChunkEntity::getTaskId, taskId).eq(TaskChunkEntity::getModuleType, MODULE_TYPE));
|
||||||
taskCacheService.deleteTaskCache(taskId);
|
taskCacheService.deleteTaskCache(taskId);
|
||||||
fileTaskMapper.deleteById(taskId);
|
fileTaskMapper.deleteById(taskId);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public void deleteHistory(Long resultId, Long userId) {
|
public void deleteHistory(Long resultId, Long userId) {
|
||||||
@@ -1568,6 +1572,14 @@ public class SimilarAsinTaskService {
|
|||||||
return lockHandle;
|
return lockHandle;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private TaskDistributedLockService.LockHandle requireTaskLock(Long taskId, long waitMillis) {
|
||||||
|
TaskDistributedLockService.LockHandle lockHandle = acquireTaskLock(taskId, waitMillis);
|
||||||
|
if (lockHandle == null) {
|
||||||
|
throw new BusinessException(40902, "Task is busy, please retry later");
|
||||||
|
}
|
||||||
|
return lockHandle;
|
||||||
|
}
|
||||||
|
|
||||||
private void completeCozeFileJob(FileTaskEntity task,
|
private void completeCozeFileJob(FileTaskEntity task,
|
||||||
FileResultEntity result,
|
FileResultEntity result,
|
||||||
TaskFileJobEntity job,
|
TaskFileJobEntity job,
|
||||||
@@ -2279,6 +2291,7 @@ public class SimilarAsinTaskService {
|
|||||||
if (row == null || row.getTaskId() == null) {
|
if (row == null || row.getTaskId() == null) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
Long taskId = row.getTaskId();
|
||||||
if (Boolean.TRUE.equals(vo.getFileReady())) {
|
if (Boolean.TRUE.equals(vo.getFileReady())) {
|
||||||
vo.setFileProgressCurrent(1);
|
vo.setFileProgressCurrent(1);
|
||||||
vo.setFileProgressTotal(1);
|
vo.setFileProgressTotal(1);
|
||||||
@@ -2286,8 +2299,29 @@ public class SimilarAsinTaskService {
|
|||||||
vo.setFileProgressMessage("结果文件已生成");
|
vo.setFileProgressMessage("结果文件已生成");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
TaskProgressSnapshotEntity snapshot = taskProgressSnapshotService.find(row.getTaskId(), MODULE_TYPE);
|
int cozeCompleted = countCompletedCozeStates(taskId);
|
||||||
|
int cozePending = countPendingCozeStates(taskId);
|
||||||
|
if (job != null && STATUS_RUNNING.equals(job.getStatus()) && cozePending > 0) {
|
||||||
|
int total = Math.max(1, cozeCompleted + cozePending);
|
||||||
|
int current = Math.max(0, Math.min(cozeCompleted, total));
|
||||||
|
vo.setFileProgressCurrent(current);
|
||||||
|
vo.setFileProgressTotal(total);
|
||||||
|
vo.setFileProgressPercent(Math.max(0, Math.min(100, (int) Math.floor(current * 100.0 / total))));
|
||||||
|
vo.setFileProgressMessage("Coze 已完成 " + current + "/" + total + ",等待回流");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
TaskProgressSnapshotEntity snapshot = taskProgressSnapshotService.find(taskId, MODULE_TYPE);
|
||||||
if (snapshot == null) {
|
if (snapshot == null) {
|
||||||
|
if (job != null && STATUS_RUNNING.equals(job.getStatus()) && cozeCompleted + cozePending > 0) {
|
||||||
|
int total = Math.max(1, cozeCompleted + cozePending);
|
||||||
|
int current = Math.max(0, Math.min(cozeCompleted, total));
|
||||||
|
vo.setFileProgressCurrent(current);
|
||||||
|
vo.setFileProgressTotal(total);
|
||||||
|
vo.setFileProgressPercent(Math.max(0, Math.min(100, (int) Math.floor(current * 100.0 / total))));
|
||||||
|
vo.setFileProgressMessage(cozePending > 0
|
||||||
|
? "Coze 已完成 " + current + "/" + total + ",等待回流"
|
||||||
|
: "Coze 处理中");
|
||||||
|
}
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
int total = snapshot.getTotalCount() == null ? 0 : snapshot.getTotalCount();
|
int total = snapshot.getTotalCount() == null ? 0 : snapshot.getTotalCount();
|
||||||
@@ -2296,22 +2330,12 @@ public class SimilarAsinTaskService {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
current = Math.max(0, Math.min(current, total));
|
current = Math.max(0, Math.min(current, total));
|
||||||
int percent = Math.max(1, Math.min(99, (int) Math.floor(current * 100.0 / total)));
|
int percent = Math.max(0, Math.min(100, (int) Math.floor(current * 100.0 / total)));
|
||||||
if (job != null && STATUS_RUNNING.equals(job.getStatus())) {
|
|
||||||
LocalDateTime baseTime = snapshot.getUpdatedAt() != null ? snapshot.getUpdatedAt() : job.getUpdatedAt();
|
|
||||||
long elapsedSeconds = baseTime == null ? 0 : Math.max(0, Duration.between(baseTime, LocalDateTime.now()).getSeconds());
|
|
||||||
if (current <= 0) {
|
|
||||||
percent = Math.max(percent, Math.min(35, 8 + (int) (elapsedSeconds / 6)));
|
|
||||||
} else if (current < total) {
|
|
||||||
percent = Math.max(percent, Math.min(92, percent + (int) (elapsedSeconds / 10)));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
vo.setFileProgressCurrent(current);
|
vo.setFileProgressCurrent(current);
|
||||||
vo.setFileProgressTotal(total);
|
vo.setFileProgressTotal(total);
|
||||||
vo.setFileProgressPercent(percent);
|
vo.setFileProgressPercent(percent);
|
||||||
vo.setFileProgressMessage(snapshot.getMessage());
|
vo.setFileProgressMessage(snapshot.getMessage());
|
||||||
}
|
}
|
||||||
|
|
||||||
private String fmt(LocalDateTime t) {
|
private String fmt(LocalDateTime t) {
|
||||||
return t == null ? null : t.toString();
|
return t == null ? null : t.toString();
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -13,8 +13,14 @@ import java.nio.charset.StandardCharsets;
|
|||||||
import java.nio.file.Files;
|
import java.nio.file.Files;
|
||||||
import java.nio.file.Path;
|
import java.nio.file.Path;
|
||||||
import java.nio.file.StandardOpenOption;
|
import java.nio.file.StandardOpenOption;
|
||||||
|
import java.io.ByteArrayInputStream;
|
||||||
|
import java.io.ByteArrayOutputStream;
|
||||||
|
import java.util.Base64;
|
||||||
import java.util.Locale;
|
import java.util.Locale;
|
||||||
|
import java.util.Objects;
|
||||||
import java.util.UUID;
|
import java.util.UUID;
|
||||||
|
import java.util.zip.GZIPInputStream;
|
||||||
|
import java.util.zip.GZIPOutputStream;
|
||||||
|
|
||||||
@Service
|
@Service
|
||||||
@RequiredArgsConstructor
|
@RequiredArgsConstructor
|
||||||
@@ -57,6 +63,10 @@ public class TransientPayloadStorageService {
|
|||||||
return store("task-parsed", moduleType, taskId, scopeHash, "latest", content, encodeAsJsonString, false);
|
return store("task-parsed", moduleType, taskId, scopeHash, "latest", content, encodeAsJsonString, false);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public String storeParsedPayloadEntry(String moduleType, Long taskId, String scopeHash, String entryKey, String content, boolean encodeAsJsonString) {
|
||||||
|
return store("task-parsed", moduleType, taskId, scopeHash, entryKey, content, encodeAsJsonString, false);
|
||||||
|
}
|
||||||
|
|
||||||
public String storeResultPayload(String moduleType, Long taskId, String scopeHash, String submissionId, String content) {
|
public String storeResultPayload(String moduleType, Long taskId, String scopeHash, String submissionId, String content) {
|
||||||
return store("task-result-payload", moduleType, taskId, scopeHash, submissionId, content, true);
|
return store("task-result-payload", moduleType, taskId, scopeHash, submissionId, content, true);
|
||||||
}
|
}
|
||||||
@@ -84,13 +94,13 @@ public class TransientPayloadStorageService {
|
|||||||
}
|
}
|
||||||
try {
|
try {
|
||||||
if (pointer.startsWith(LOCAL_POINTER_PREFIX)) {
|
if (pointer.startsWith(LOCAL_POINTER_PREFIX)) {
|
||||||
return readLocalPayload(pointer.substring(LOCAL_POINTER_PREFIX.length()));
|
return decodeStoredPayload(readLocalPayload(pointer.substring(LOCAL_POINTER_PREFIX.length())));
|
||||||
}
|
}
|
||||||
if (pointer.startsWith(RUSTFS_POINTER_PREFIX)) {
|
if (pointer.startsWith(RUSTFS_POINTER_PREFIX)) {
|
||||||
return rustfsObjectStorageService.readObjectAsString(pointer.substring(RUSTFS_POINTER_PREFIX.length()));
|
return decodeStoredPayload(rustfsObjectStorageService.readObjectAsString(pointer.substring(RUSTFS_POINTER_PREFIX.length())));
|
||||||
}
|
}
|
||||||
if (pointer.startsWith(OSS_POINTER_PREFIX)) {
|
if (pointer.startsWith(OSS_POINTER_PREFIX)) {
|
||||||
return ossStorageService.readObjectAsString(pointer.substring(OSS_POINTER_PREFIX.length()));
|
return decodeStoredPayload(ossStorageService.readObjectAsString(pointer.substring(OSS_POINTER_PREFIX.length())));
|
||||||
}
|
}
|
||||||
} catch (Exception ex) {
|
} catch (Exception ex) {
|
||||||
throw new IllegalStateException(errorMessage + ": " + pointer, ex);
|
throw new IllegalStateException(errorMessage + ": " + pointer, ex);
|
||||||
@@ -175,10 +185,11 @@ public class TransientPayloadStorageService {
|
|||||||
return content;
|
return content;
|
||||||
}
|
}
|
||||||
String objectKey = buildObjectKey(category, moduleType, taskId, scopeHash, entryKey);
|
String objectKey = buildObjectKey(category, moduleType, taskId, scopeHash, entryKey);
|
||||||
|
String storedContent = encodeStoredPayload(content);
|
||||||
String pointer = null;
|
String pointer = null;
|
||||||
if (rustfsObjectStorageService.isConfigured()) {
|
if (rustfsObjectStorageService.isConfigured()) {
|
||||||
try {
|
try {
|
||||||
pointer = RUSTFS_POINTER_PREFIX + rustfsObjectStorageService.uploadText(objectKey, content, verifyAfterUpload);
|
pointer = RUSTFS_POINTER_PREFIX + rustfsObjectStorageService.uploadText(objectKey, storedContent, verifyAfterUpload);
|
||||||
} catch (Exception ex) {
|
} catch (Exception ex) {
|
||||||
log.warn("[transient-payload] rustfs upload failed objectKey={} err={}",
|
log.warn("[transient-payload] rustfs upload failed objectKey={} err={}",
|
||||||
objectKey, ex.getMessage());
|
objectKey, ex.getMessage());
|
||||||
@@ -186,7 +197,7 @@ public class TransientPayloadStorageService {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (pointer == null) {
|
if (pointer == null) {
|
||||||
pointer = storeLocal(objectKey, content);
|
pointer = storeLocal(objectKey, storedContent);
|
||||||
}
|
}
|
||||||
if (pointer == null) {
|
if (pointer == null) {
|
||||||
return content;
|
return content;
|
||||||
@@ -231,6 +242,36 @@ public class TransientPayloadStorageService {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private String encodeStoredPayload(String content) {
|
||||||
|
try {
|
||||||
|
byte[] raw = Objects.requireNonNullElse(content, "").getBytes(StandardCharsets.UTF_8);
|
||||||
|
ByteArrayOutputStream baos = new ByteArrayOutputStream();
|
||||||
|
try (GZIPOutputStream gzip = new GZIPOutputStream(baos)) {
|
||||||
|
gzip.write(raw);
|
||||||
|
}
|
||||||
|
return "gzip64:" + Base64.getEncoder().encodeToString(baos.toByteArray());
|
||||||
|
} catch (Exception ex) {
|
||||||
|
throw new IllegalStateException("failed to encode transient payload", ex);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private String decodeStoredPayload(String storedContent) {
|
||||||
|
if (storedContent == null || storedContent.isBlank()) {
|
||||||
|
return storedContent;
|
||||||
|
}
|
||||||
|
if (!storedContent.startsWith("gzip64:")) {
|
||||||
|
return storedContent;
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
byte[] compressed = Base64.getDecoder().decode(storedContent.substring("gzip64:".length()));
|
||||||
|
try (GZIPInputStream gzip = new GZIPInputStream(new ByteArrayInputStream(compressed))) {
|
||||||
|
return new String(gzip.readAllBytes(), StandardCharsets.UTF_8);
|
||||||
|
}
|
||||||
|
} catch (Exception ex) {
|
||||||
|
throw new IllegalStateException("failed to decode transient payload", ex);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private void deleteLocalPayload(String objectKey) {
|
private void deleteLocalPayload(String objectKey) {
|
||||||
try {
|
try {
|
||||||
Files.deleteIfExists(resolveLocalPayloadPath(objectKey));
|
Files.deleteIfExists(resolveLocalPayloadPath(objectKey));
|
||||||
|
|||||||
@@ -28,11 +28,11 @@
|
|||||||
<button type="button" class="btn-run" :disabled="parsing || !uploadedFiles.length" @click="parseFiles">
|
<button type="button" class="btn-run" :disabled="parsing || !uploadedFiles.length" @click="parseFiles">
|
||||||
{{ parsing ? '解析中...' : '解析并创建任务' }}
|
{{ parsing ? '解析中...' : '解析并创建任务' }}
|
||||||
</button>
|
</button>
|
||||||
<button type="button" class="btn-run btn-queue" :disabled="pushing || !parsedRows.length" @click="pushToPythonQueue">
|
<button type="button" class="btn-run btn-queue" :disabled="pushing || !parseResult?.taskId" @click="pushToPythonQueue">
|
||||||
{{ pushing ? '推送中...' : '推送到 Python 队列' }}
|
{{ pushing ? '推送中...' : '推送到 Python 队列' }}
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
<p class="loading-msg">Python 回传字段按 asin、国家、url、标题提交;后端接收分片后按 10 条一批调用 Coze。</p>
|
<p class="loading-msg">Python 回传字段按 asin、国家、url、标题提交;后端接收分片后按 10 条一批调用 Coze,结果区展示真实 Coze 批次进度。</p>
|
||||||
|
|
||||||
<div v-if="visibleTaskSummary" class="parse-card">
|
<div v-if="visibleTaskSummary" class="parse-card">
|
||||||
<div>任务 ID:{{ visibleTaskSummary.taskId }}</div>
|
<div>任务 ID:{{ visibleTaskSummary.taskId }}</div>
|
||||||
@@ -64,21 +64,6 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="subsection-title">匹配任务</div>
|
<div class="subsection-title">匹配任务</div>
|
||||||
<div class="result-list-wrap preview-wrap">
|
|
||||||
<div class="result-list-header">
|
|
||||||
<span>解析结果</span>
|
|
||||||
<span v-if="parsedRows.length" class="muted">显示首组 {{ previewRows.length }} 条 / 共 {{ parseResult?.groupCount || parsedGroups.length }} 组,{{ parsedRows.length }} 条</span>
|
|
||||||
</div>
|
|
||||||
<div v-if="!parsedRows.length" class="empty-tasks">暂无解析结果</div>
|
|
||||||
<el-table v-else :data="previewRows" height="120" class="result-table">
|
|
||||||
<el-table-column prop="displayId" label="ID" width="90" />
|
|
||||||
<el-table-column prop="asin" label="ASIN" width="130" />
|
|
||||||
<el-table-column prop="country" label="国家" width="100" />
|
|
||||||
<el-table-column prop="title" label="标题" min-width="160" show-overflow-tooltip />
|
|
||||||
<el-table-column prop="url" label="URL" min-width="160" show-overflow-tooltip />
|
|
||||||
</el-table>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="result-list-wrap">
|
<div class="result-list-wrap">
|
||||||
<div class="result-list-header">
|
<div class="result-list-header">
|
||||||
<span>当前任务</span>
|
<span>当前任务</span>
|
||||||
@@ -151,13 +136,13 @@ import {
|
|||||||
deleteAppearancePatentTask,
|
deleteAppearancePatentTask,
|
||||||
getAppearancePatentDashboard,
|
getAppearancePatentDashboard,
|
||||||
getAppearancePatentHistory,
|
getAppearancePatentHistory,
|
||||||
|
getAppearancePatentQueuePayload,
|
||||||
getAppearancePatentResultDownloadUrl,
|
getAppearancePatentResultDownloadUrl,
|
||||||
getAppearancePatentTaskProgressBatch,
|
getAppearancePatentTaskProgressBatch,
|
||||||
parseAppearancePatent,
|
parseAppearancePatent,
|
||||||
type AppearancePatentParsedGroup,
|
type AppearancePatentParsedPayloadDto,
|
||||||
type AppearancePatentDashboardVo,
|
type AppearancePatentDashboardVo,
|
||||||
type AppearancePatentHistoryItem,
|
type AppearancePatentHistoryItem,
|
||||||
type AppearancePatentParsedRow,
|
|
||||||
type AppearancePatentParseVo,
|
type AppearancePatentParseVo,
|
||||||
type UploadedFileRef,
|
type UploadedFileRef,
|
||||||
type UploadFileVo,
|
type UploadFileVo,
|
||||||
@@ -171,7 +156,6 @@ import { createCategorizedTimers } from '@/shared/utils/categorized-timers'
|
|||||||
const selectedFileNames = ref<string[]>([])
|
const selectedFileNames = ref<string[]>([])
|
||||||
const uploadedFiles = ref<UploadFileVo[]>([])
|
const uploadedFiles = ref<UploadFileVo[]>([])
|
||||||
const parseResult = ref<AppearancePatentParseVo | null>(null)
|
const parseResult = ref<AppearancePatentParseVo | null>(null)
|
||||||
const parsedGroups = ref<AppearancePatentParsedGroup[]>([])
|
|
||||||
type TaskSummary = Pick<AppearancePatentParseVo, 'taskId' | 'totalRows' | 'acceptedRows' | 'groupCount' | 'droppedRows'>
|
type TaskSummary = Pick<AppearancePatentParseVo, 'taskId' | 'totalRows' | 'acceptedRows' | 'groupCount' | 'droppedRows'>
|
||||||
const queuedTaskSummary = ref<TaskSummary | null>(null)
|
const queuedTaskSummary = ref<TaskSummary | null>(null)
|
||||||
const defaultAiPrompt = `请帮我排查以下亚马逊商品在英国及欧洲地区是否存在知识产权侵权风险。
|
const defaultAiPrompt = `请帮我排查以下亚马逊商品在英国及欧洲地区是否存在知识产权侵权风险。
|
||||||
@@ -202,12 +186,24 @@ const dashboard = ref<AppearancePatentDashboardVo>({
|
|||||||
})
|
})
|
||||||
const historyItems = ref<AppearancePatentHistoryItem[]>([])
|
const historyItems = ref<AppearancePatentHistoryItem[]>([])
|
||||||
|
|
||||||
const parsedRows = computed<AppearancePatentParsedRow[]>(() =>
|
|
||||||
parsedGroups.value.flatMap((group) => group.items || []),
|
|
||||||
)
|
|
||||||
const previewRows = computed(() => parsedGroups.value[0]?.items || [])
|
|
||||||
const visibleTaskSummary = computed(() => parseResult.value || queuedTaskSummary.value)
|
const visibleTaskSummary = computed(() => parseResult.value || queuedTaskSummary.value)
|
||||||
const currentItems = computed(() => historyItems.value.filter((i) => i.taskId != null && pollingTaskIds.value.includes(i.taskId)))
|
const pendingParseItem = computed<AppearancePatentHistoryItem | null>(() => {
|
||||||
|
const result = parseResult.value
|
||||||
|
if (!result?.taskId) return null
|
||||||
|
const exists = historyItems.value.some((item) => item.taskId === result.taskId)
|
||||||
|
if (exists) return null
|
||||||
|
return {
|
||||||
|
taskId: result.taskId,
|
||||||
|
sourceFilename: result.sourceFilename,
|
||||||
|
rowCount: result.acceptedRows,
|
||||||
|
taskStatus: 'PENDING',
|
||||||
|
success: false,
|
||||||
|
}
|
||||||
|
})
|
||||||
|
const currentItems = computed(() => {
|
||||||
|
const runningItems = historyItems.value.filter((i) => i.taskId != null && pollingTaskIds.value.includes(i.taskId))
|
||||||
|
return pendingParseItem.value ? [pendingParseItem.value, ...runningItems] : runningItems
|
||||||
|
})
|
||||||
const historyOnlyItems = computed(() =>
|
const historyOnlyItems = computed(() =>
|
||||||
historyItems.value.filter((i) => {
|
historyItems.value.filter((i) => {
|
||||||
const status = normalizeTaskStatus(i)
|
const status = normalizeTaskStatus(i)
|
||||||
@@ -309,7 +305,6 @@ async function selectFiles() {
|
|||||||
uploadedFiles.value = files
|
uploadedFiles.value = files
|
||||||
selectedFileNames.value = files.map((f) => f.relativePath || f.originalFilename || f.fileKey)
|
selectedFileNames.value = files.map((f) => f.relativePath || f.originalFilename || f.fileKey)
|
||||||
parseResult.value = null
|
parseResult.value = null
|
||||||
parsedGroups.value = []
|
|
||||||
queuedTaskSummary.value = null
|
queuedTaskSummary.value = null
|
||||||
queuePayloadText.value = ''
|
queuePayloadText.value = ''
|
||||||
}
|
}
|
||||||
@@ -332,7 +327,6 @@ async function selectFolder() {
|
|||||||
uploadedFiles.value = files
|
uploadedFiles.value = files
|
||||||
selectedFileNames.value = result.items.map((item) => item.relativePath || item.absolutePath)
|
selectedFileNames.value = result.items.map((item) => item.relativePath || item.absolutePath)
|
||||||
parseResult.value = null
|
parseResult.value = null
|
||||||
parsedGroups.value = []
|
|
||||||
queuedTaskSummary.value = null
|
queuedTaskSummary.value = null
|
||||||
queuePayloadText.value = ''
|
queuePayloadText.value = ''
|
||||||
ElMessage.success(`已选择文件夹内 ${result.items.length} 个 Excel 文件`)
|
ElMessage.success(`已选择文件夹内 ${result.items.length} 个 Excel 文件`)
|
||||||
@@ -360,19 +354,6 @@ async function parseFiles() {
|
|||||||
const res = await parseAppearancePatent(files, effectiveAiPrompt(), effectiveCozeApiKey())
|
const res = await parseAppearancePatent(files, effectiveAiPrompt(), effectiveCozeApiKey())
|
||||||
parseResult.value = res
|
parseResult.value = res
|
||||||
queuedTaskSummary.value = null
|
queuedTaskSummary.value = null
|
||||||
parsedGroups.value = res.groups?.length
|
|
||||||
? res.groups
|
|
||||||
: (res.items?.length
|
|
||||||
? [{
|
|
||||||
sourceFileKey: res.items[0]?.sourceFileKey,
|
|
||||||
sourceFilename: res.items[0]?.sourceFilename,
|
|
||||||
groupKey: res.items[0]?.groupKey,
|
|
||||||
baseId: '',
|
|
||||||
displayId: res.items[0]?.displayId,
|
|
||||||
itemCount: res.items.length,
|
|
||||||
items: res.items,
|
|
||||||
}]
|
|
||||||
: [])
|
|
||||||
queuePayloadText.value = ''
|
queuePayloadText.value = ''
|
||||||
ElMessage.success(`解析完成,共 ${res.groupCount || 0} 组 / ${res.acceptedRows} 条`)
|
ElMessage.success(`解析完成,共 ${res.groupCount || 0} 组 / ${res.acceptedRows} 条`)
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
@@ -386,7 +367,7 @@ async function pushToPythonQueue() {
|
|||||||
const api = getPywebviewApi()
|
const api = getPywebviewApi()
|
||||||
const currentParseResult = parseResult.value
|
const currentParseResult = parseResult.value
|
||||||
const taskId = currentParseResult?.taskId
|
const taskId = currentParseResult?.taskId
|
||||||
if (!taskId || !parsedRows.value.length) {
|
if (!taskId) {
|
||||||
ElMessage.warning('请先解析文件')
|
ElMessage.warning('请先解析文件')
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@@ -400,14 +381,20 @@ async function pushToPythonQueue() {
|
|||||||
}
|
}
|
||||||
pushing.value = true
|
pushing.value = true
|
||||||
try {
|
try {
|
||||||
|
const parsedPayload: AppearancePatentParsedPayloadDto = await getAppearancePatentQueuePayload(taskId)
|
||||||
|
const groups = parsedPayload.groups || []
|
||||||
const payload = {
|
const payload = {
|
||||||
type: 'appearance-patent-run',
|
type: 'appearance-patent-run',
|
||||||
ts: Date.now(),
|
ts: Date.now(),
|
||||||
data: {
|
data: {
|
||||||
taskId,
|
taskId,
|
||||||
prompt: currentParseResult.aiPrompt || effectiveAiPrompt(),
|
prompt: parsedPayload.aiPrompt || currentParseResult.aiPrompt || effectiveAiPrompt(),
|
||||||
api_key: effectiveCozeApiKey(),
|
api_key: effectiveCozeApiKey(),
|
||||||
groups: parsedGroups.value.map((group) => ({
|
sourceFileCount: currentParseResult.sourceFileCount || 0,
|
||||||
|
totalRows: currentParseResult.totalRows || 0,
|
||||||
|
acceptedRows: currentParseResult.acceptedRows || 0,
|
||||||
|
groupCount: currentParseResult.groupCount || 0,
|
||||||
|
groups: groups.map((group) => ({
|
||||||
sourceFileKey: group.sourceFileKey || '',
|
sourceFileKey: group.sourceFileKey || '',
|
||||||
sourceFilename: group.sourceFilename || '',
|
sourceFilename: group.sourceFilename || '',
|
||||||
groupKey: group.groupKey || '',
|
groupKey: group.groupKey || '',
|
||||||
@@ -425,20 +412,9 @@ async function pushToPythonQueue() {
|
|||||||
title: row.title || '',
|
title: row.title || '',
|
||||||
})),
|
})),
|
||||||
})),
|
})),
|
||||||
rows: parsedRows.value.map((row) => ({
|
|
||||||
sourceFileKey: row.sourceFileKey || '',
|
|
||||||
sourceFilename: row.sourceFilename || '',
|
|
||||||
rowToken: row.rowToken || '',
|
|
||||||
groupKey: row.groupKey || '',
|
|
||||||
id: row.displayId,
|
|
||||||
asin: row.asin,
|
|
||||||
country: row.country,
|
|
||||||
url: row.url || '',
|
|
||||||
title: row.title || '',
|
|
||||||
})),
|
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
queuePayloadText.value = JSON.stringify(payloadForDisplay(payload), null, 2)
|
queuePayloadText.value = `任务 ${taskId} 已准备推送,共 ${groups.length} 组`
|
||||||
await activateAppearancePatentTask(taskId)
|
await activateAppearancePatentTask(taskId)
|
||||||
const result = await api.enqueue_json(payload)
|
const result = await api.enqueue_json(payload)
|
||||||
if (!result?.success) {
|
if (!result?.success) {
|
||||||
@@ -458,7 +434,6 @@ async function pushToPythonQueue() {
|
|||||||
|
|
||||||
function clearParsedTask() {
|
function clearParsedTask() {
|
||||||
parseResult.value = null
|
parseResult.value = null
|
||||||
parsedGroups.value = []
|
|
||||||
queuePayloadText.value = ''
|
queuePayloadText.value = ''
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -681,10 +656,10 @@ function pendingResultHint(item: AppearancePatentHistoryItem) {
|
|||||||
if (!isResultPreparing(item)) return ''
|
if (!isResultPreparing(item)) return ''
|
||||||
const fileStatus = (item.fileStatus || '').toUpperCase()
|
const fileStatus = (item.fileStatus || '').toUpperCase()
|
||||||
if (fileStatus === 'RUNNING') {
|
if (fileStatus === 'RUNNING') {
|
||||||
return '任务已完成,正在请求 Coze 或组装结果文件,下载按钮稍后出现'
|
return '任务已完成,正在等待 Coze 回流并组装结果文件,下载按钮稍后出现'
|
||||||
}
|
}
|
||||||
if (fileStatus === 'PENDING') {
|
if (fileStatus === 'PENDING') {
|
||||||
return '任务已完成,结果文件已入队,正在等待后端生成'
|
return '任务已完成,结果文件已入队,正在等待后端开始处理'
|
||||||
}
|
}
|
||||||
return '任务已完成,正在生成结果文件,下载按钮稍后出现'
|
return '任务已完成,正在生成结果文件,下载按钮稍后出现'
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -52,7 +52,7 @@
|
|||||||
{{ pushing ? '推送中...' : '推送到 Python 队列' }}
|
{{ pushing ? '推送中...' : '推送到 Python 队列' }}
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
<p class="loading-msg">Python 回传字段按 id、asin、国家、价格、标题、图片链接数组提交;后端接收分片后按 10 条一批送检。</p>
|
<p class="loading-msg">Python 回传字段按 id、asin、国家、价格、标题、图片链接数组提交;后端接收分片后按 10 条一批送检,结果区展示真实 Coze 批次进度。</p>
|
||||||
|
|
||||||
<div v-if="visibleTaskSummary" class="parse-card">
|
<div v-if="visibleTaskSummary" class="parse-card">
|
||||||
<div>任务 ID:{{ visibleTaskSummary.taskId }}</div>
|
<div>任务 ID:{{ visibleTaskSummary.taskId }}</div>
|
||||||
@@ -186,6 +186,7 @@ import {
|
|||||||
type SimilarAsinParseVo,
|
type SimilarAsinParseVo,
|
||||||
type UploadedFileRef,
|
type UploadedFileRef,
|
||||||
type UploadFileVo,
|
type UploadFileVo,
|
||||||
|
uploadTempFileToJava,
|
||||||
} from '@/shared/api/java-modules'
|
} from '@/shared/api/java-modules'
|
||||||
import { expandBrandFolderRecursive, type BrandExpandFolderItem } from '@/shared/api/brand'
|
import { expandBrandFolderRecursive, type BrandExpandFolderItem } from '@/shared/api/brand'
|
||||||
import { getPywebviewApi } from '@/shared/bridges/pywebview'
|
import { getPywebviewApi } from '@/shared/bridges/pywebview'
|
||||||
@@ -326,7 +327,7 @@ async function uploadAppearancePathsToJava(paths: Array<string | BrandExpandFold
|
|||||||
async function selectFiles() {
|
async function selectFiles() {
|
||||||
const api = getPywebviewApi()
|
const api = getPywebviewApi()
|
||||||
if (!api?.select_brand_xlsx_files || !api.upload_file_to_java) {
|
if (!api?.select_brand_xlsx_files || !api.upload_file_to_java) {
|
||||||
ElMessage.warning('当前环境不支持文件选择或上传')
|
ElMessage.warning('当前桌面端未提供文件选择或上传能力')
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
const paths = await api.select_brand_xlsx_files()
|
const paths = await api.select_brand_xlsx_files()
|
||||||
@@ -342,8 +343,8 @@ async function selectFiles() {
|
|||||||
|
|
||||||
async function selectFolder() {
|
async function selectFolder() {
|
||||||
const api = getPywebviewApi()
|
const api = getPywebviewApi()
|
||||||
if (!api?.select_brand_folder) {
|
if (!api?.select_brand_folder || !api.upload_file_to_java) {
|
||||||
ElMessage.warning('当前环境不支持文件夹选择,请在本地客户端中打开')
|
ElMessage.warning('当前桌面端未提供文件夹选择或上传能力')
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
try {
|
try {
|
||||||
@@ -721,10 +722,10 @@ function pendingResultHint(item: SimilarAsinHistoryItem) {
|
|||||||
if (!isResultPreparing(item)) return ''
|
if (!isResultPreparing(item)) return ''
|
||||||
const fileStatus = (item.fileStatus || '').toUpperCase()
|
const fileStatus = (item.fileStatus || '').toUpperCase()
|
||||||
if (fileStatus === 'RUNNING') {
|
if (fileStatus === 'RUNNING') {
|
||||||
return '任务已完成,正在请求 Coze 或组装结果文件,下载按钮稍后出现'
|
return '任务已完成,正在等待 Coze 回流并组装结果文件,下载按钮稍后出现'
|
||||||
}
|
}
|
||||||
if (fileStatus === 'PENDING') {
|
if (fileStatus === 'PENDING') {
|
||||||
return '任务已完成,结果文件已入队,正在等待后端生成'
|
return '任务已完成,结果文件已入队,正在等待后端开始处理'
|
||||||
}
|
}
|
||||||
return '任务已完成,正在生成结果文件,下载按钮稍后出现'
|
return '任务已完成,正在生成结果文件,下载按钮稍后出现'
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -102,6 +102,25 @@ export interface UploadFileVo {
|
|||||||
relativePath?: string;
|
relativePath?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export async function uploadTempFileToJava(file: File, relativePath?: string) {
|
||||||
|
const formData = new FormData()
|
||||||
|
formData.append('file', file)
|
||||||
|
if (relativePath) {
|
||||||
|
formData.append('relativePath', relativePath)
|
||||||
|
}
|
||||||
|
const response = await http.post<JavaApiResponse<UploadFileVo>>(
|
||||||
|
`${JAVA_API_PREFIX}/files/upload`,
|
||||||
|
formData,
|
||||||
|
{
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'multipart/form-data',
|
||||||
|
},
|
||||||
|
timeout: 120000,
|
||||||
|
},
|
||||||
|
)
|
||||||
|
return response.data
|
||||||
|
}
|
||||||
|
|
||||||
export interface DedupeResultItem {
|
export interface DedupeResultItem {
|
||||||
resultId?: number;
|
resultId?: number;
|
||||||
sourceFilename: string;
|
sourceFilename: string;
|
||||||
@@ -1452,6 +1471,7 @@ export interface AppearancePatentParsedRow {
|
|||||||
groupKey?: string;
|
groupKey?: string;
|
||||||
asin: string;
|
asin: string;
|
||||||
country: string;
|
country: string;
|
||||||
|
price?: string;
|
||||||
url?: string;
|
url?: string;
|
||||||
title?: string;
|
title?: string;
|
||||||
}
|
}
|
||||||
@@ -1479,6 +1499,16 @@ export interface AppearancePatentParseVo {
|
|||||||
groups?: AppearancePatentParsedGroup[];
|
groups?: AppearancePatentParsedGroup[];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface AppearancePatentParsedPayloadDto {
|
||||||
|
aiPrompt?: string;
|
||||||
|
apiKey?: string;
|
||||||
|
sourceFiles?: UploadedFileRef[];
|
||||||
|
headers?: string[];
|
||||||
|
items?: AppearancePatentParsedRow[];
|
||||||
|
groups?: AppearancePatentParsedGroup[];
|
||||||
|
allItems?: AppearancePatentParsedRow[];
|
||||||
|
}
|
||||||
|
|
||||||
export interface AppearancePatentDashboardVo {
|
export interface AppearancePatentDashboardVo {
|
||||||
pendingTaskCount: number;
|
pendingTaskCount: number;
|
||||||
processedTaskCount: number;
|
processedTaskCount: number;
|
||||||
@@ -1545,6 +1575,24 @@ export function parseAppearancePatent(files: UploadedFileRef[], aiPrompt: string
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function getAppearancePatentParsedPayload(taskId: number) {
|
||||||
|
return unwrapJavaResponse(
|
||||||
|
get<JavaApiResponse<AppearancePatentParsedPayloadDto>>(
|
||||||
|
`${JAVA_API_PREFIX}/appearance-patent/tasks/${taskId}/parsed-payload`,
|
||||||
|
{ params: { user_id: getCurrentUserId() } },
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function getAppearancePatentQueuePayload(taskId: number) {
|
||||||
|
return unwrapJavaResponse(
|
||||||
|
get<JavaApiResponse<AppearancePatentParsedPayloadDto>>(
|
||||||
|
`${JAVA_API_PREFIX}/appearance-patent/tasks/${taskId}/queue-payload`,
|
||||||
|
{ params: { user_id: getCurrentUserId() } },
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
export function getAppearancePatentDashboard() {
|
export function getAppearancePatentDashboard() {
|
||||||
return unwrapJavaResponse(
|
return unwrapJavaResponse(
|
||||||
get<JavaApiResponse<AppearancePatentDashboardVo>>(
|
get<JavaApiResponse<AppearancePatentDashboardVo>>(
|
||||||
|
|||||||
@@ -5,12 +5,11 @@
|
|||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||||
<title>外观专利检测</title>
|
<title>外观专利检测</title>
|
||||||
<script type="module" crossorigin src="/assets/appearance-patent.js"></script>
|
<script type="module" crossorigin src="/assets/appearance-patent.js"></script>
|
||||||
<link rel="modulepreload" crossorigin href="/assets/pywebview-RBTI4RuH.js">
|
<link rel="modulepreload" crossorigin href="/assets/pywebview-CcblbV5J.js">
|
||||||
<link rel="modulepreload" crossorigin href="/assets/brand-1lGM3Wzk.js">
|
<link rel="modulepreload" crossorigin href="/assets/brand-CEi7uRFN.js">
|
||||||
<link rel="modulepreload" crossorigin href="/assets/categorized-timers-JPA-olTr.js">
|
<link rel="modulepreload" crossorigin href="/assets/categorized-timers-JPA-olTr.js">
|
||||||
<link rel="stylesheet" crossorigin href="/assets/pywebview-C-qhaDJK.css">
|
<link rel="stylesheet" crossorigin href="/assets/pywebview-DNJAt1gR.css">
|
||||||
<link rel="stylesheet" crossorigin href="/assets/appearance-patent-DaODE9AU.css">
|
<link rel="stylesheet" crossorigin href="/assets/appearance-patent-D7zGgGOj.css">
|
||||||
<link rel="stylesheet" crossorigin href="/assets/el-table-column-Cy4YJvw0.css">
|
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
<div id="app"></div>
|
<div id="app"></div>
|
||||||
|
|||||||
1
new_web_source/assets/appearance-patent-D7zGgGOj.css
Normal file
1
new_web_source/assets/appearance-patent-D7zGgGOj.css
Normal file
File diff suppressed because one or more lines are too long
6
new_web_source/assets/appearance-patent.js
Normal file
6
new_web_source/assets/appearance-patent.js
Normal file
File diff suppressed because one or more lines are too long
1
new_web_source/assets/brand-CEi7uRFN.js
Normal file
1
new_web_source/assets/brand-CEi7uRFN.js
Normal file
@@ -0,0 +1 @@
|
|||||||
|
import{bb as r}from"./pywebview-CcblbV5J.js";const n="";function s(e){return r(`${n}/api/brand/expand-folder-recursive`,{folder:e})}export{s as e};
|
||||||
1
new_web_source/assets/categorized-timers-JPA-olTr.js
Normal file
1
new_web_source/assets/categorized-timers-JPA-olTr.js
Normal file
@@ -0,0 +1 @@
|
|||||||
|
const r=new Map;function c(n){let t=r.get(n);return t||(t=new Map,r.set(n,t)),t}function u(n,t){const e=r.get(n);e&&(e.delete(t),e.size||r.delete(n))}function l(n,t,e){const i=window.setTimeout(()=>{u(n,i),t()},e);return c(n).set(i,{id:i,kind:"timeout",category:n}),i}function a(n,t,e){const i=window.setInterval(t,e);return c(n).set(i,{id:i,kind:"interval",category:n}),i}function f(n,t){if(t==null)return;const i=r.get(n)?.get(t);i?.kind==="interval"?window.clearInterval(t):window.clearTimeout(t),i?.cancel?.(),u(n,t)}function s(n){const t=r.get(n);if(t){for(const e of t.values())e.kind==="interval"?window.clearInterval(e.id):window.clearTimeout(e.id),e.cancel?.();r.delete(n)}}function d(n,t){return new Promise(e=>{const i=window.setTimeout(()=>{u(n,i),e()},t);c(n).set(i,{id:i,kind:"timeout",category:n,cancel:e})})}function m(n){const t=`${n}:`;for(const e of Array.from(r.keys()))(e===n||e.startsWith(t))&&s(e)}function w(n){const t=e=>`${n}:${e}`;return{setTimeout(e,i,o){return l(t(e),i,o)},setInterval(e,i,o){return a(t(e),i,o)},clearTimer(e,i){f(t(e),i)},clearCategory(e){s(t(e))},clearScope(){m(n)},sleep(e,i){return d(t(e),i)}}}export{w as c};
|
||||||
1
new_web_source/assets/convert-BKSNvX8i.css
Normal file
1
new_web_source/assets/convert-BKSNvX8i.css
Normal file
File diff suppressed because one or more lines are too long
1
new_web_source/assets/convert.js
Normal file
1
new_web_source/assets/convert.js
Normal file
File diff suppressed because one or more lines are too long
1
new_web_source/assets/dedupe-DNlVfFj-.css
Normal file
1
new_web_source/assets/dedupe-DNlVfFj-.css
Normal file
File diff suppressed because one or more lines are too long
1
new_web_source/assets/dedupe.js
Normal file
1
new_web_source/assets/dedupe.js
Normal file
File diff suppressed because one or more lines are too long
1
new_web_source/assets/delete-brand-BJod9K65.css
Normal file
1
new_web_source/assets/delete-brand-BJod9K65.css
Normal file
File diff suppressed because one or more lines are too long
1
new_web_source/assets/delete-brand.js
Normal file
1
new_web_source/assets/delete-brand.js
Normal file
File diff suppressed because one or more lines are too long
1
new_web_source/assets/el-input-xaztPnzw.css
Normal file
1
new_web_source/assets/el-input-xaztPnzw.css
Normal file
File diff suppressed because one or more lines are too long
1
new_web_source/assets/el-table-column-BoPoyBFZ.css
Normal file
1
new_web_source/assets/el-table-column-BoPoyBFZ.css
Normal file
File diff suppressed because one or more lines are too long
1
new_web_source/assets/listingFilters-BpGOU_pJ.js
Normal file
1
new_web_source/assets/listingFilters-BpGOU_pJ.js
Normal file
@@ -0,0 +1 @@
|
|||||||
|
const e=[{value:"SearchSuppressed",label:"在搜索结果中禁止显示"},{value:"ApprovalRequired",label:"需要批准"},{value:"Active",label:"在售"},{value:"DetailPageRemoved",label:"详情页面已删除"}];export{e as L};
|
||||||
1
new_web_source/assets/listingFilters-CK58rX4v.css
Normal file
1
new_web_source/assets/listingFilters-CK58rX4v.css
Normal file
File diff suppressed because one or more lines are too long
1
new_web_source/assets/patrol-delete-CBBdonF4.css
Normal file
1
new_web_source/assets/patrol-delete-CBBdonF4.css
Normal file
File diff suppressed because one or more lines are too long
1
new_web_source/assets/patrol-delete.js
Normal file
1
new_web_source/assets/patrol-delete.js
Normal file
File diff suppressed because one or more lines are too long
1
new_web_source/assets/price-track-Hozgt_em.css
Normal file
1
new_web_source/assets/price-track-Hozgt_em.css
Normal file
File diff suppressed because one or more lines are too long
1
new_web_source/assets/price-track.js
Normal file
1
new_web_source/assets/price-track.js
Normal file
File diff suppressed because one or more lines are too long
1
new_web_source/assets/product-risk-DV5AzjVc.css
Normal file
1
new_web_source/assets/product-risk-DV5AzjVc.css
Normal file
File diff suppressed because one or more lines are too long
1
new_web_source/assets/product-risk.js
Normal file
1
new_web_source/assets/product-risk.js
Normal file
File diff suppressed because one or more lines are too long
55
new_web_source/assets/pywebview-CcblbV5J.js
Normal file
55
new_web_source/assets/pywebview-CcblbV5J.js
Normal file
File diff suppressed because one or more lines are too long
1
new_web_source/assets/pywebview-DNJAt1gR.css
Normal file
1
new_web_source/assets/pywebview-DNJAt1gR.css
Normal file
File diff suppressed because one or more lines are too long
1
new_web_source/assets/query-asin-B4RsOiza.css
Normal file
1
new_web_source/assets/query-asin-B4RsOiza.css
Normal file
File diff suppressed because one or more lines are too long
1
new_web_source/assets/query-asin.js
Normal file
1
new_web_source/assets/query-asin.js
Normal file
File diff suppressed because one or more lines are too long
1
new_web_source/assets/shop-match-B2HWAgBY.css
Normal file
1
new_web_source/assets/shop-match-B2HWAgBY.css
Normal file
File diff suppressed because one or more lines are too long
1
new_web_source/assets/shop-match.js
Normal file
1
new_web_source/assets/shop-match.js
Normal file
File diff suppressed because one or more lines are too long
1
new_web_source/assets/similar-asin-D__NXzTu.css
Normal file
1
new_web_source/assets/similar-asin-D__NXzTu.css
Normal file
File diff suppressed because one or more lines are too long
1
new_web_source/assets/similar-asin.js
Normal file
1
new_web_source/assets/similar-asin.js
Normal file
File diff suppressed because one or more lines are too long
1
new_web_source/assets/split-BoBVrLdC.css
Normal file
1
new_web_source/assets/split-BoBVrLdC.css
Normal file
File diff suppressed because one or more lines are too long
1
new_web_source/assets/split.js
Normal file
1
new_web_source/assets/split.js
Normal file
File diff suppressed because one or more lines are too long
1
new_web_source/assets/zh-cn-DCaAv7Wt.js
Normal file
1
new_web_source/assets/zh-cn-DCaAv7Wt.js
Normal file
File diff suppressed because one or more lines are too long
@@ -1,16 +1,16 @@
|
|||||||
<!doctype html>
|
<!doctype html>
|
||||||
<html lang="zh-CN">
|
<html lang="zh-CN">
|
||||||
<head>
|
<head>
|
||||||
<meta charset="UTF-8" />
|
<meta charset="UTF-8" />
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||||
<title>格式转换 - 数富AI</title>
|
<title>格式转换 - 数富AI</title>
|
||||||
<script type="module" crossorigin src="/assets/convert.js"></script>
|
<script type="module" crossorigin src="/assets/convert.js"></script>
|
||||||
<link rel="modulepreload" crossorigin href="/assets/pywebview-BWNiqXug.js">
|
<link rel="modulepreload" crossorigin href="/assets/pywebview-CcblbV5J.js">
|
||||||
<link rel="modulepreload" crossorigin href="/assets/brand-CABpTGPu.js">
|
<link rel="modulepreload" crossorigin href="/assets/brand-CEi7uRFN.js">
|
||||||
<link rel="stylesheet" crossorigin href="/assets/pywebview-BFZO8_3q.css">
|
<link rel="stylesheet" crossorigin href="/assets/pywebview-DNJAt1gR.css">
|
||||||
<link rel="stylesheet" crossorigin href="/assets/convert-r7MSXq7I.css">
|
<link rel="stylesheet" crossorigin href="/assets/convert-BKSNvX8i.css">
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
<div id="app"></div>
|
<div id="app"></div>
|
||||||
|
</body>
|
||||||
</body>
|
</html>
|
||||||
|
|||||||
@@ -1,16 +1,16 @@
|
|||||||
<!doctype html>
|
<!doctype html>
|
||||||
<html lang="zh-CN">
|
<html lang="zh-CN">
|
||||||
<head>
|
<head>
|
||||||
<meta charset="UTF-8" />
|
<meta charset="UTF-8" />
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||||
<title>数据去重 - 数富AI</title>
|
<title>数据去重 - 数富AI</title>
|
||||||
<script type="module" crossorigin src="/assets/dedupe.js"></script>
|
<script type="module" crossorigin src="/assets/dedupe.js"></script>
|
||||||
<link rel="modulepreload" crossorigin href="/assets/pywebview-BWNiqXug.js">
|
<link rel="modulepreload" crossorigin href="/assets/pywebview-CcblbV5J.js">
|
||||||
<link rel="modulepreload" crossorigin href="/assets/brand-CABpTGPu.js">
|
<link rel="modulepreload" crossorigin href="/assets/brand-CEi7uRFN.js">
|
||||||
<link rel="stylesheet" crossorigin href="/assets/pywebview-BFZO8_3q.css">
|
<link rel="stylesheet" crossorigin href="/assets/pywebview-DNJAt1gR.css">
|
||||||
<link rel="stylesheet" crossorigin href="/assets/dedupe-DMeaPUMZ.css">
|
<link rel="stylesheet" crossorigin href="/assets/dedupe-DNlVfFj-.css">
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
<div id="app"></div>
|
<div id="app"></div>
|
||||||
|
</body>
|
||||||
</body>
|
</html>
|
||||||
|
|||||||
@@ -1,17 +1,17 @@
|
|||||||
<!doctype html>
|
<!doctype html>
|
||||||
<html lang="zh-CN">
|
<html lang="zh-CN">
|
||||||
<head>
|
<head>
|
||||||
<meta charset="UTF-8" />
|
<meta charset="UTF-8" />
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||||
<title>删除品牌 - 数富AI</title>
|
<title>删除品牌 - 数富AI</title>
|
||||||
<script type="module" crossorigin src="/assets/delete-brand.js"></script>
|
<script type="module" crossorigin src="/assets/delete-brand.js"></script>
|
||||||
<link rel="modulepreload" crossorigin href="/assets/pywebview-BWNiqXug.js">
|
<link rel="modulepreload" crossorigin href="/assets/pywebview-CcblbV5J.js">
|
||||||
<link rel="modulepreload" crossorigin href="/assets/brand-CABpTGPu.js">
|
<link rel="modulepreload" crossorigin href="/assets/brand-CEi7uRFN.js">
|
||||||
<link rel="modulepreload" crossorigin href="/assets/categorized-timers-JPA-olTr.js">
|
<link rel="modulepreload" crossorigin href="/assets/categorized-timers-JPA-olTr.js">
|
||||||
<link rel="stylesheet" crossorigin href="/assets/pywebview-BFZO8_3q.css">
|
<link rel="stylesheet" crossorigin href="/assets/pywebview-DNJAt1gR.css">
|
||||||
<link rel="stylesheet" crossorigin href="/assets/delete-brand-B5ChOXZY.css">
|
<link rel="stylesheet" crossorigin href="/assets/delete-brand-BJod9K65.css">
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
<div id="app"></div>
|
<div id="app"></div>
|
||||||
|
</body>
|
||||||
</body>
|
</html>
|
||||||
|
|||||||
@@ -1,19 +1,19 @@
|
|||||||
<!doctype html>
|
<!doctype html>
|
||||||
<html lang="zh-CN">
|
<html lang="zh-CN">
|
||||||
<head>
|
<head>
|
||||||
<meta charset="UTF-8" />
|
<meta charset="UTF-8" />
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||||
<title>巡店删除 - 数富AI</title>
|
<title>巡店删除 - 数富AI</title>
|
||||||
<script type="module" crossorigin src="/assets/patrol-delete.js"></script>
|
<script type="module" crossorigin src="/assets/patrol-delete.js"></script>
|
||||||
<link rel="modulepreload" crossorigin href="/assets/pywebview-BWNiqXug.js">
|
<link rel="modulepreload" crossorigin href="/assets/pywebview-CcblbV5J.js">
|
||||||
<link rel="modulepreload" crossorigin href="/assets/zh-cn-BbuLS91u.js">
|
<link rel="modulepreload" crossorigin href="/assets/zh-cn-DCaAv7Wt.js">
|
||||||
<link rel="modulepreload" crossorigin href="/assets/categorized-timers-JPA-olTr.js">
|
<link rel="modulepreload" crossorigin href="/assets/categorized-timers-JPA-olTr.js">
|
||||||
<link rel="stylesheet" crossorigin href="/assets/pywebview-BFZO8_3q.css">
|
<link rel="stylesheet" crossorigin href="/assets/pywebview-DNJAt1gR.css">
|
||||||
<link rel="stylesheet" crossorigin href="/assets/patrol-delete-DyumfGaZ.css">
|
<link rel="stylesheet" crossorigin href="/assets/patrol-delete-CBBdonF4.css">
|
||||||
<link rel="stylesheet" crossorigin href="/assets/el-table-column-Cy4YJvw0.css">
|
<link rel="stylesheet" crossorigin href="/assets/el-table-column-BoPoyBFZ.css">
|
||||||
<link rel="stylesheet" crossorigin href="/assets/el-input-xaztPnzw.css">
|
<link rel="stylesheet" crossorigin href="/assets/el-input-xaztPnzw.css">
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
<div id="app"></div>
|
<div id="app"></div>
|
||||||
|
</body>
|
||||||
</body>
|
</html>
|
||||||
|
|||||||
@@ -5,12 +5,12 @@
|
|||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||||
<title>跟价 - 数富AI</title>
|
<title>跟价 - 数富AI</title>
|
||||||
<script type="module" crossorigin src="/assets/price-track.js"></script>
|
<script type="module" crossorigin src="/assets/price-track.js"></script>
|
||||||
<link rel="modulepreload" crossorigin href="/assets/pywebview-BWNiqXug.js">
|
<link rel="modulepreload" crossorigin href="/assets/pywebview-CcblbV5J.js">
|
||||||
<link rel="modulepreload" crossorigin href="/assets/brand-CABpTGPu.js">
|
<link rel="modulepreload" crossorigin href="/assets/brand-CEi7uRFN.js">
|
||||||
<link rel="modulepreload" crossorigin href="/assets/categorized-timers-JPA-olTr.js">
|
<link rel="modulepreload" crossorigin href="/assets/categorized-timers-JPA-olTr.js">
|
||||||
<link rel="stylesheet" crossorigin href="/assets/pywebview-BFZO8_3q.css">
|
<link rel="stylesheet" crossorigin href="/assets/pywebview-DNJAt1gR.css">
|
||||||
<link rel="stylesheet" crossorigin href="/assets/price-track-DMhhVz4u.css">
|
<link rel="stylesheet" crossorigin href="/assets/price-track-Hozgt_em.css">
|
||||||
<link rel="stylesheet" crossorigin href="/assets/el-table-column-Cy4YJvw0.css">
|
<link rel="stylesheet" crossorigin href="/assets/el-table-column-BoPoyBFZ.css">
|
||||||
<link rel="stylesheet" crossorigin href="/assets/el-input-xaztPnzw.css">
|
<link rel="stylesheet" crossorigin href="/assets/el-input-xaztPnzw.css">
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
|
|||||||
@@ -5,13 +5,13 @@
|
|||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||||
<title>商品风险解决 - 数富AI</title>
|
<title>商品风险解决 - 数富AI</title>
|
||||||
<script type="module" crossorigin src="/assets/product-risk.js"></script>
|
<script type="module" crossorigin src="/assets/product-risk.js"></script>
|
||||||
<link rel="modulepreload" crossorigin href="/assets/pywebview-BWNiqXug.js">
|
<link rel="modulepreload" crossorigin href="/assets/pywebview-CcblbV5J.js">
|
||||||
<link rel="modulepreload" crossorigin href="/assets/listingFilters-BpGOU_pJ.js">
|
<link rel="modulepreload" crossorigin href="/assets/listingFilters-BpGOU_pJ.js">
|
||||||
<link rel="modulepreload" crossorigin href="/assets/categorized-timers-JPA-olTr.js">
|
<link rel="modulepreload" crossorigin href="/assets/categorized-timers-JPA-olTr.js">
|
||||||
<link rel="stylesheet" crossorigin href="/assets/pywebview-BFZO8_3q.css">
|
<link rel="stylesheet" crossorigin href="/assets/pywebview-DNJAt1gR.css">
|
||||||
<link rel="stylesheet" crossorigin href="/assets/listingFilters-CK58rX4v.css">
|
<link rel="stylesheet" crossorigin href="/assets/listingFilters-CK58rX4v.css">
|
||||||
<link rel="stylesheet" crossorigin href="/assets/product-risk-CI6qY5U2.css">
|
<link rel="stylesheet" crossorigin href="/assets/product-risk-DV5AzjVc.css">
|
||||||
<link rel="stylesheet" crossorigin href="/assets/el-table-column-Cy4YJvw0.css">
|
<link rel="stylesheet" crossorigin href="/assets/el-table-column-BoPoyBFZ.css">
|
||||||
<link rel="stylesheet" crossorigin href="/assets/el-input-xaztPnzw.css">
|
<link rel="stylesheet" crossorigin href="/assets/el-input-xaztPnzw.css">
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
|
|||||||
@@ -1,20 +1,20 @@
|
|||||||
<!doctype html>
|
<!doctype html>
|
||||||
<html lang="zh-CN">
|
<html lang="zh-CN">
|
||||||
<head>
|
<head>
|
||||||
<meta charset="UTF-8" />
|
<meta charset="UTF-8" />
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||||
<title>巡店删除 - 数富AI</title>
|
<title>巡店删除 - 数富AI</title>
|
||||||
<script type="module" crossorigin src="/assets/query-asin.js"></script>
|
<script type="module" crossorigin src="/assets/query-asin.js"></script>
|
||||||
<link rel="modulepreload" crossorigin href="/assets/pywebview-BWNiqXug.js">
|
<link rel="modulepreload" crossorigin href="/assets/pywebview-CcblbV5J.js">
|
||||||
<link rel="modulepreload" crossorigin href="/assets/zh-cn-BbuLS91u.js">
|
<link rel="modulepreload" crossorigin href="/assets/zh-cn-DCaAv7Wt.js">
|
||||||
<link rel="modulepreload" crossorigin href="/assets/categorized-timers-JPA-olTr.js">
|
<link rel="modulepreload" crossorigin href="/assets/categorized-timers-JPA-olTr.js">
|
||||||
<link rel="stylesheet" crossorigin href="/assets/pywebview-BFZO8_3q.css">
|
<link rel="stylesheet" crossorigin href="/assets/pywebview-DNJAt1gR.css">
|
||||||
<link rel="stylesheet" crossorigin href="/assets/query-asin-Bw6BQsRN.css">
|
<link rel="stylesheet" crossorigin href="/assets/query-asin-B4RsOiza.css">
|
||||||
<link rel="stylesheet" crossorigin href="/assets/el-table-column-Cy4YJvw0.css">
|
<link rel="stylesheet" crossorigin href="/assets/el-table-column-BoPoyBFZ.css">
|
||||||
<link rel="stylesheet" crossorigin href="/assets/el-input-xaztPnzw.css">
|
<link rel="stylesheet" crossorigin href="/assets/el-input-xaztPnzw.css">
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
<div id="app"></div>
|
<div id="app"></div>
|
||||||
|
</body>
|
||||||
</body>
|
</html>
|
||||||
</html>
|
|
||||||
|
|||||||
@@ -1,21 +1,21 @@
|
|||||||
<!doctype html>
|
<!doctype html>
|
||||||
<html lang="zh-CN">
|
<html lang="zh-CN">
|
||||||
<head>
|
<head>
|
||||||
<meta charset="UTF-8" />
|
<meta charset="UTF-8" />
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||||
<title>匹配店铺 - 数富AI</title>
|
<title>匹配店铺 - 数富AI</title>
|
||||||
<script type="module" crossorigin src="/assets/shop-match.js"></script>
|
<script type="module" crossorigin src="/assets/shop-match.js"></script>
|
||||||
<link rel="modulepreload" crossorigin href="/assets/pywebview-BWNiqXug.js">
|
<link rel="modulepreload" crossorigin href="/assets/pywebview-CcblbV5J.js">
|
||||||
<link rel="modulepreload" crossorigin href="/assets/zh-cn-BbuLS91u.js">
|
<link rel="modulepreload" crossorigin href="/assets/zh-cn-DCaAv7Wt.js">
|
||||||
<link rel="modulepreload" crossorigin href="/assets/listingFilters-BpGOU_pJ.js">
|
<link rel="modulepreload" crossorigin href="/assets/listingFilters-BpGOU_pJ.js">
|
||||||
<link rel="modulepreload" crossorigin href="/assets/categorized-timers-JPA-olTr.js">
|
<link rel="modulepreload" crossorigin href="/assets/categorized-timers-JPA-olTr.js">
|
||||||
<link rel="stylesheet" crossorigin href="/assets/pywebview-BFZO8_3q.css">
|
<link rel="stylesheet" crossorigin href="/assets/pywebview-DNJAt1gR.css">
|
||||||
<link rel="stylesheet" crossorigin href="/assets/listingFilters-CK58rX4v.css">
|
<link rel="stylesheet" crossorigin href="/assets/listingFilters-CK58rX4v.css">
|
||||||
<link rel="stylesheet" crossorigin href="/assets/shop-match-CxCKUsqS.css">
|
<link rel="stylesheet" crossorigin href="/assets/shop-match-B2HWAgBY.css">
|
||||||
<link rel="stylesheet" crossorigin href="/assets/el-table-column-Cy4YJvw0.css">
|
<link rel="stylesheet" crossorigin href="/assets/el-table-column-BoPoyBFZ.css">
|
||||||
<link rel="stylesheet" crossorigin href="/assets/el-input-xaztPnzw.css">
|
<link rel="stylesheet" crossorigin href="/assets/el-input-xaztPnzw.css">
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
<div id="app"></div>
|
<div id="app"></div>
|
||||||
|
</body>
|
||||||
</body>
|
</html>
|
||||||
|
|||||||
@@ -3,14 +3,14 @@
|
|||||||
<head>
|
<head>
|
||||||
<meta charset="UTF-8" />
|
<meta charset="UTF-8" />
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||||
<title>相似ASIN检测</title>
|
<title>货源查询</title>
|
||||||
<script type="module" crossorigin src="/assets/similar-asin.js"></script>
|
<script type="module" crossorigin src="/assets/similar-asin.js"></script>
|
||||||
<link rel="modulepreload" crossorigin href="/assets/pywebview-RBTI4RuH.js">
|
<link rel="modulepreload" crossorigin href="/assets/pywebview-CcblbV5J.js">
|
||||||
<link rel="modulepreload" crossorigin href="/assets/brand-1lGM3Wzk.js">
|
<link rel="modulepreload" crossorigin href="/assets/brand-CEi7uRFN.js">
|
||||||
<link rel="modulepreload" crossorigin href="/assets/categorized-timers-JPA-olTr.js">
|
<link rel="modulepreload" crossorigin href="/assets/categorized-timers-JPA-olTr.js">
|
||||||
<link rel="stylesheet" crossorigin href="/assets/pywebview-C-qhaDJK.css">
|
<link rel="stylesheet" crossorigin href="/assets/pywebview-DNJAt1gR.css">
|
||||||
<link rel="stylesheet" crossorigin href="/assets/similar-asin-B2xrJRis.css">
|
<link rel="stylesheet" crossorigin href="/assets/similar-asin-CVTb4S1E.css">
|
||||||
<link rel="stylesheet" crossorigin href="/assets/el-table-column-Cy4YJvw0.css">
|
<link rel="stylesheet" crossorigin href="/assets/el-table-column-BoPoyBFZ.css">
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
<div id="app"></div>
|
<div id="app"></div>
|
||||||
|
|||||||
@@ -1,16 +1,16 @@
|
|||||||
<!doctype html>
|
<!doctype html>
|
||||||
<html lang="zh-CN">
|
<html lang="zh-CN">
|
||||||
<head>
|
<head>
|
||||||
<meta charset="UTF-8" />
|
<meta charset="UTF-8" />
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||||
<title>数据拆分 - 数富AI</title>
|
<title>数据拆分 - 数富AI</title>
|
||||||
<script type="module" crossorigin src="/assets/split.js"></script>
|
<script type="module" crossorigin src="/assets/split.js"></script>
|
||||||
<link rel="modulepreload" crossorigin href="/assets/pywebview-BWNiqXug.js">
|
<link rel="modulepreload" crossorigin href="/assets/pywebview-CcblbV5J.js">
|
||||||
<link rel="modulepreload" crossorigin href="/assets/brand-CABpTGPu.js">
|
<link rel="modulepreload" crossorigin href="/assets/brand-CEi7uRFN.js">
|
||||||
<link rel="stylesheet" crossorigin href="/assets/pywebview-BFZO8_3q.css">
|
<link rel="stylesheet" crossorigin href="/assets/pywebview-DNJAt1gR.css">
|
||||||
<link rel="stylesheet" crossorigin href="/assets/split-Cw6sXhAg.css">
|
<link rel="stylesheet" crossorigin href="/assets/split-BoBVrLdC.css">
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
<div id="app"></div>
|
<div id="app"></div>
|
||||||
|
</body>
|
||||||
</body>
|
</html>
|
||||||
|
|||||||
Reference in New Issue
Block a user