增加rufts
This commit is contained in:
4
app/.env
4
app/.env
@@ -13,8 +13,8 @@ client_name=ShuFuAI
|
||||
|
||||
|
||||
# java_api_base=http://47.111.163.154:18080
|
||||
# java_api_base=http://127.0.0.1:18080
|
||||
java_api_base=http://8.136.19.173:18080
|
||||
java_api_base=http://127.0.0.1:18080
|
||||
# java_api_base=http://8.136.19.173:18080
|
||||
# java_api_base=http://121.196.149.225:18080
|
||||
|
||||
|
||||
|
||||
Binary file not shown.
BIN
app/amazon/__pycache__/asin_status.cpython-312.pyc
Normal file
BIN
app/amazon/__pycache__/asin_status.cpython-312.pyc
Normal file
Binary file not shown.
BIN
app/amazon/__pycache__/detail_spider.cpython-312.pyc
Normal file
BIN
app/amazon/__pycache__/detail_spider.cpython-312.pyc
Normal file
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
@@ -390,19 +390,20 @@ class SpiderTask:
|
||||
runing_task[task_id]["status"] = "failed"
|
||||
runing_task[task_id]["error"] = str(e)
|
||||
|
||||
def post_result(self, task_id: int, chunkIndex:int,chunkTotal: int, asin: str, error:str="",
|
||||
item_data:dict={},
|
||||
is_done: bool = False):
|
||||
def post_result(self, task_id: int, chunkIndex:int,chunkTotal: int, asin: str, error:str="",
|
||||
item_data:dict={},
|
||||
is_done: bool = False):
|
||||
"""回传处理结果到API
|
||||
"""
|
||||
|
||||
url = f"{DELETE_BRAND_API_BASE}/api/appearance-patent/tasks/{task_id}/result"
|
||||
|
||||
payload ={
|
||||
"submissionId": f"{int(time.time())}",
|
||||
"chunkIndex": chunkIndex,
|
||||
"chunkTotal": chunkTotal,
|
||||
"error": error,
|
||||
submission_id = f"appearance-patent:{task_id}"
|
||||
payload ={
|
||||
"submissionId": submission_id,
|
||||
"chunkIndex": chunkIndex,
|
||||
"chunkTotal": chunkTotal,
|
||||
"error": error,
|
||||
"items": [
|
||||
item_data
|
||||
],
|
||||
@@ -410,8 +411,9 @@ class SpiderTask:
|
||||
}
|
||||
|
||||
max_retries = 3
|
||||
for retry in range(max_retries):
|
||||
try:
|
||||
for retry in range(max_retries):
|
||||
try:
|
||||
request_timeout = 300 if is_done else 30
|
||||
print("================【详情采集】=====================")
|
||||
self.log(f"尝试回传结果 (第 {retry + 1}/{max_retries} 次)")
|
||||
self.log(f"回传URL: {url}")
|
||||
@@ -420,7 +422,7 @@ class SpiderTask:
|
||||
url,
|
||||
json=payload,
|
||||
headers={"Content-Type": "application/json"},
|
||||
timeout=30,
|
||||
timeout=request_timeout,
|
||||
verify=False
|
||||
)
|
||||
self.log(f"回传结果: {response.text}")
|
||||
@@ -6904,4 +6906,4 @@ if __name__ == '__main__':
|
||||
},
|
||||
"code": None
|
||||
}
|
||||
spide.process_task(task_data)
|
||||
spide.process_task(task_data)
|
||||
|
||||
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
@@ -1,19 +1,16 @@
|
||||
<!doctype html>
|
||||
<html lang="zh-CN">
|
||||
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<title>格式转换 - 数富AI</title>
|
||||
<script type="module" crossorigin src="/assets/convert.js"></script>
|
||||
<link rel="modulepreload" crossorigin href="/assets/pywebview-C66x_2Dh.js">
|
||||
<link rel="modulepreload" crossorigin href="/assets/brand-BZije8D7.js">
|
||||
<link rel="stylesheet" crossorigin href="/assets/pywebview-Dp5dN8OO.css">
|
||||
<link rel="stylesheet" crossorigin href="/assets/convert-BKSNvX8i.css">
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<div id="app"></div>
|
||||
</body>
|
||||
|
||||
</html>
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<title>格式转换 - 数富AI</title>
|
||||
<script type="module" crossorigin src="/assets/convert.js"></script>
|
||||
<link rel="modulepreload" crossorigin href="/assets/pywebview-C66x_2Dh.js">
|
||||
<link rel="modulepreload" crossorigin href="/assets/brand-BZije8D7.js">
|
||||
<link rel="stylesheet" crossorigin href="/assets/pywebview-Dp5dN8OO.css">
|
||||
<link rel="stylesheet" crossorigin href="/assets/convert-BKSNvX8i.css">
|
||||
</head>
|
||||
<body>
|
||||
<div id="app"></div>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
@@ -1,19 +1,16 @@
|
||||
<!doctype html>
|
||||
<html lang="zh-CN">
|
||||
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<title>数据去重 - 数富AI</title>
|
||||
<script type="module" crossorigin src="/assets/dedupe.js"></script>
|
||||
<link rel="modulepreload" crossorigin href="/assets/pywebview-C66x_2Dh.js">
|
||||
<link rel="modulepreload" crossorigin href="/assets/brand-BZije8D7.js">
|
||||
<link rel="stylesheet" crossorigin href="/assets/pywebview-Dp5dN8OO.css">
|
||||
<link rel="stylesheet" crossorigin href="/assets/dedupe-DNlVfFj-.css">
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<div id="app"></div>
|
||||
</body>
|
||||
|
||||
</html>
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<title>数据去重 - 数富AI</title>
|
||||
<script type="module" crossorigin src="/assets/dedupe.js"></script>
|
||||
<link rel="modulepreload" crossorigin href="/assets/pywebview-C66x_2Dh.js">
|
||||
<link rel="modulepreload" crossorigin href="/assets/brand-BZije8D7.js">
|
||||
<link rel="stylesheet" crossorigin href="/assets/pywebview-Dp5dN8OO.css">
|
||||
<link rel="stylesheet" crossorigin href="/assets/dedupe-DNlVfFj-.css">
|
||||
</head>
|
||||
<body>
|
||||
<div id="app"></div>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
@@ -1,19 +1,16 @@
|
||||
<!doctype html>
|
||||
<html lang="zh-CN">
|
||||
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<title>删除品牌 - 数富AI</title>
|
||||
<script type="module" crossorigin src="/assets/delete-brand.js"></script>
|
||||
<link rel="modulepreload" crossorigin href="/assets/pywebview-C66x_2Dh.js">
|
||||
<link rel="modulepreload" crossorigin href="/assets/brand-BZije8D7.js">
|
||||
<link rel="stylesheet" crossorigin href="/assets/pywebview-Dp5dN8OO.css">
|
||||
<link rel="stylesheet" crossorigin href="/assets/delete-brand-BP3XWKAC.css">
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<div id="app"></div>
|
||||
</body>
|
||||
|
||||
</html>
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<title>删除品牌 - 数富AI</title>
|
||||
<script type="module" crossorigin src="/assets/delete-brand.js"></script>
|
||||
<link rel="modulepreload" crossorigin href="/assets/pywebview-C66x_2Dh.js">
|
||||
<link rel="modulepreload" crossorigin href="/assets/brand-BZije8D7.js">
|
||||
<link rel="stylesheet" crossorigin href="/assets/pywebview-Dp5dN8OO.css">
|
||||
<link rel="stylesheet" crossorigin href="/assets/delete-brand-BP3XWKAC.css">
|
||||
</head>
|
||||
<body>
|
||||
<div id="app"></div>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
@@ -1,19 +1,16 @@
|
||||
<!doctype html>
|
||||
<html lang="zh-CN">
|
||||
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<title>数据拆分 - 数富AI</title>
|
||||
<script type="module" crossorigin src="/assets/split.js"></script>
|
||||
<link rel="modulepreload" crossorigin href="/assets/pywebview-C66x_2Dh.js">
|
||||
<link rel="modulepreload" crossorigin href="/assets/brand-BZije8D7.js">
|
||||
<link rel="stylesheet" crossorigin href="/assets/pywebview-Dp5dN8OO.css">
|
||||
<link rel="stylesheet" crossorigin href="/assets/split-BoBVrLdC.css">
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<div id="app"></div>
|
||||
</body>
|
||||
|
||||
</html>
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<title>数据拆分 - 数富AI</title>
|
||||
<script type="module" crossorigin src="/assets/split.js"></script>
|
||||
<link rel="modulepreload" crossorigin href="/assets/pywebview-C66x_2Dh.js">
|
||||
<link rel="modulepreload" crossorigin href="/assets/brand-BZije8D7.js">
|
||||
<link rel="stylesheet" crossorigin href="/assets/pywebview-Dp5dN8OO.css">
|
||||
<link rel="stylesheet" crossorigin href="/assets/split-BoBVrLdC.css">
|
||||
</head>
|
||||
<body>
|
||||
<div id="app"></div>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
1056
backend-java/jstack-6488.txt
Normal file
1056
backend-java/jstack-6488.txt
Normal file
File diff suppressed because it is too large
Load Diff
@@ -11,6 +11,8 @@ public class AppearancePatentProperties {
|
||||
private String cozeWorkflowId = "7632683471312355338";
|
||||
private String cozeToken = "";
|
||||
private int cozeBatchSize = 10;
|
||||
private int cozeConnectTimeoutMillis = 10000;
|
||||
private int cozeReadTimeoutMillis = 60000;
|
||||
private int staleTimeoutMinutes = 20;
|
||||
private String staleFinalizeCron = "0 */2 * * * *";
|
||||
}
|
||||
|
||||
@@ -7,6 +7,7 @@ import org.springframework.boot.context.properties.ConfigurationProperties;
|
||||
@ConfigurationProperties(prefix = "aiimage.delete-brand-progress")
|
||||
public class DeleteBrandProgressProperties {
|
||||
private long heartbeatTimeoutMinutes = 15;
|
||||
private long deleteBrandInitialTimeoutMinutes = 20;
|
||||
private String staleCheckCron = "*/30 * * * * *";
|
||||
|
||||
/**
|
||||
|
||||
@@ -9,11 +9,14 @@ import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.http.MediaType;
|
||||
import org.springframework.http.client.SimpleClientHttpRequestFactory;
|
||||
import org.springframework.stereotype.Component;
|
||||
import org.springframework.util.StreamUtils;
|
||||
import org.springframework.web.client.RestClient;
|
||||
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.util.ArrayList;
|
||||
import java.util.LinkedHashMap;
|
||||
import java.util.List;
|
||||
import java.util.Locale;
|
||||
import java.util.Map;
|
||||
|
||||
@Component
|
||||
@@ -33,29 +36,72 @@ public class AppearancePatentCozeClient {
|
||||
return rows.stream().map(this::copy).toList();
|
||||
}
|
||||
try {
|
||||
String raw = postWorkflow(rows, prompt);
|
||||
List<CozeResult> results = parseResults(raw);
|
||||
List<AppearancePatentResultRowDto> merged = new ArrayList<>();
|
||||
for (int i = 0; i < rows.size(); i++) {
|
||||
AppearancePatentResultRowDto row = copy(rows.get(i));
|
||||
if (i < results.size()) {
|
||||
CozeResult result = results.get(i);
|
||||
row.setTitleRisk(result.title());
|
||||
row.setAppearanceRisk(result.appearance());
|
||||
row.setPatentRisk(result.patent());
|
||||
row.setConclusion(result.result());
|
||||
}
|
||||
merged.add(row);
|
||||
}
|
||||
return merged;
|
||||
return inspectWithFallback(rows, prompt);
|
||||
} catch (Exception ex) {
|
||||
log.warn("[appearance-patent] coze batch failed size={} err={}", rows.size(), ex.getMessage());
|
||||
return rows.stream().map(this::copy).toList();
|
||||
String failureMessage = failureMessage(ex);
|
||||
log.warn("[appearance-patent] coze batch failed size={} err={}", rows.size(), failureMessage);
|
||||
return rows.stream().map(this::copy).map(row -> markFailed(row, failureMessage)).toList();
|
||||
}
|
||||
}
|
||||
|
||||
private List<AppearancePatentResultRowDto> inspectWithFallback(List<AppearancePatentResultRowDto> rows, String prompt) {
|
||||
try {
|
||||
if (rows.size() == 1) {
|
||||
return inspectSingleRowWithRetry(rows, prompt);
|
||||
}
|
||||
InspectAttempt attempt = inspectOnce(rows, prompt);
|
||||
if (attempt.resolvedCount() < rows.size()) {
|
||||
throw new PartialCozeResultException(attempt.resolvedCount(), rows.size(), attempt.rawResultCount());
|
||||
}
|
||||
return attempt.mergedRows();
|
||||
} catch (Exception ex) {
|
||||
if (shouldSplitBatch(rows, ex)) {
|
||||
int middle = rows.size() / 2;
|
||||
log.warn("[appearance-patent] coze batch fallback split size={} left={} right={} err={}",
|
||||
rows.size(), middle, rows.size() - middle, failureMessage(ex));
|
||||
List<AppearancePatentResultRowDto> merged = new ArrayList<>(rows.size());
|
||||
merged.addAll(inspectWithFallback(rows.subList(0, middle), prompt));
|
||||
merged.addAll(inspectWithFallback(rows.subList(middle, rows.size()), prompt));
|
||||
return merged;
|
||||
}
|
||||
throw propagate(ex);
|
||||
}
|
||||
}
|
||||
|
||||
private List<AppearancePatentResultRowDto> inspectSingleRowWithRetry(List<AppearancePatentResultRowDto> rows, String prompt) throws Exception {
|
||||
AppearancePatentResultRowDto row = rows.getFirst();
|
||||
PartialCozeResultException lastFailure = null;
|
||||
for (int attemptIndex = 1; attemptIndex <= 3; attemptIndex++) {
|
||||
InspectAttempt attempt = inspectOnce(rows, prompt);
|
||||
if (attempt.resolvedCount() == rows.size()) {
|
||||
return attempt.mergedRows();
|
||||
}
|
||||
log.warn("[appearance-patent] coze single unresolved attempt={} rowId={} asin={} country={} title={} url={} raw={}",
|
||||
attemptIndex,
|
||||
row.getId(),
|
||||
row.getAsin(),
|
||||
row.getCountry(),
|
||||
abbreviate(row.getTitle(), 120),
|
||||
abbreviate(row.getUrl(), 120),
|
||||
abbreviate(attempt.raw(), 500));
|
||||
lastFailure = new PartialCozeResultException(attempt.resolvedCount(), rows.size(), attempt.rawResultCount());
|
||||
}
|
||||
throw lastFailure == null ? new PartialCozeResultException(0, rows.size(), 0) : lastFailure;
|
||||
}
|
||||
|
||||
private InspectAttempt inspectOnce(List<AppearancePatentResultRowDto> rows, String prompt) throws Exception {
|
||||
String raw = postWorkflow(rows, prompt);
|
||||
List<CozeResult> results = parseResults(raw);
|
||||
List<AppearancePatentResultRowDto> merged = mergeRows(rows, results);
|
||||
return new InspectAttempt(raw, merged, resolvedCount(merged), results.size());
|
||||
}
|
||||
|
||||
private String postWorkflow(List<AppearancePatentResultRowDto> rows, String prompt) {
|
||||
Map<String, Object> parameters = new LinkedHashMap<>();
|
||||
parameters.put("row_id_list", rows.stream().map(row -> nonBlank(row.getId(), "")).toList());
|
||||
parameters.put("asin_list", rows.stream().map(row -> nonBlank(row.getAsin(), "")).toList());
|
||||
parameters.put("country_list", rows.stream().map(row -> nonBlank(row.getCountry(), "")).toList());
|
||||
parameters.put("row_key_list", rows.stream().map(this::rowKey).toList());
|
||||
parameters.put("title_list", rows.stream().map(row -> nonBlank(row.getTitle(), row.getAsin())).toList());
|
||||
parameters.put("url_list", rows.stream().map(row -> nonBlank(row.getUrl(), "")).toList());
|
||||
parameters.put("prompt", prompt == null ? "" : prompt);
|
||||
@@ -71,7 +117,10 @@ public class AppearancePatentCozeClient {
|
||||
headers.setContentType(MediaType.APPLICATION_JSON);
|
||||
});
|
||||
request.body(body);
|
||||
return request.retrieve().body(String.class);
|
||||
return request.exchange((clientRequest, clientResponse) -> {
|
||||
byte[] responseBytes = StreamUtils.copyToByteArray(clientResponse.getBody());
|
||||
return responseBytes.length == 0 ? "" : new String(responseBytes, StandardCharsets.UTF_8);
|
||||
});
|
||||
}
|
||||
|
||||
private List<CozeResult> parseResults(String raw) throws Exception {
|
||||
@@ -89,6 +138,9 @@ public class AppearancePatentCozeClient {
|
||||
if (array.isArray()) {
|
||||
for (JsonNode node : array) {
|
||||
results.add(new CozeResult(
|
||||
text(firstNonNull(node.get("row_id"), firstNonNull(node.get("rowId"), node.get("id")))),
|
||||
text(node.get("asin")),
|
||||
text(firstNonNull(node.get("country"), node.get("site"))),
|
||||
text(node.get("title")),
|
||||
text(node.get("appearance")),
|
||||
text(firstNonNull(node.get("patent"), node.get("patent "))),
|
||||
@@ -99,10 +151,51 @@ public class AppearancePatentCozeClient {
|
||||
return results;
|
||||
}
|
||||
|
||||
private List<AppearancePatentResultRowDto> mergeRows(List<AppearancePatentResultRowDto> rows, List<CozeResult> results) {
|
||||
Map<String, CozeResult> resultByCompositeKey = new LinkedHashMap<>();
|
||||
Map<String, CozeResult> resultByRowId = new LinkedHashMap<>();
|
||||
for (CozeResult result : results) {
|
||||
String compositeKey = rowKey(result.rowId(), result.asin(), result.country());
|
||||
if (!compositeKey.isBlank()) {
|
||||
resultByCompositeKey.putIfAbsent(compositeKey, result);
|
||||
}
|
||||
String rowIdKey = normalize(result.rowId());
|
||||
if (!rowIdKey.isBlank()) {
|
||||
resultByRowId.putIfAbsent(rowIdKey, result);
|
||||
}
|
||||
}
|
||||
|
||||
List<AppearancePatentResultRowDto> merged = new ArrayList<>(rows.size());
|
||||
boolean allowIndexFallback = results.size() == rows.size();
|
||||
for (int i = 0; i < rows.size(); i++) {
|
||||
AppearancePatentResultRowDto row = copy(rows.get(i));
|
||||
CozeResult result = resultByCompositeKey.get(rowKey(row.getId(), row.getAsin(), row.getCountry()));
|
||||
if (result == null) {
|
||||
result = resultByRowId.get(normalize(row.getId()));
|
||||
}
|
||||
if (result == null && allowIndexFallback && i < results.size()) {
|
||||
result = results.get(i);
|
||||
}
|
||||
applyResult(row, result);
|
||||
merged.add(row);
|
||||
}
|
||||
return merged;
|
||||
}
|
||||
|
||||
private void applyResult(AppearancePatentResultRowDto row, CozeResult result) {
|
||||
if (row == null || result == null) {
|
||||
return;
|
||||
}
|
||||
row.setTitleRisk(result.title());
|
||||
row.setAppearanceRisk(result.appearance());
|
||||
row.setPatentRisk(result.patent());
|
||||
row.setConclusion(result.result());
|
||||
}
|
||||
|
||||
private RestClient restClient() {
|
||||
SimpleClientHttpRequestFactory requestFactory = new SimpleClientHttpRequestFactory();
|
||||
requestFactory.setConnectTimeout(10000);
|
||||
requestFactory.setReadTimeout(60000);
|
||||
requestFactory.setConnectTimeout(properties.getCozeConnectTimeoutMillis());
|
||||
requestFactory.setReadTimeout(properties.getCozeReadTimeoutMillis());
|
||||
return RestClient.builder().requestFactory(requestFactory).build();
|
||||
}
|
||||
|
||||
@@ -122,6 +215,67 @@ public class AppearancePatentCozeClient {
|
||||
return row;
|
||||
}
|
||||
|
||||
private AppearancePatentResultRowDto markFailed(AppearancePatentResultRowDto row, String failureMessage) {
|
||||
if (row.getError() == null || row.getError().isBlank()) {
|
||||
row.setError(failureMessage);
|
||||
}
|
||||
if (row.getConclusion() == null || row.getConclusion().isBlank()) {
|
||||
row.setConclusion(failureMessage);
|
||||
}
|
||||
return row;
|
||||
}
|
||||
|
||||
private boolean shouldSplitBatch(List<AppearancePatentResultRowDto> rows, Exception ex) {
|
||||
return rows != null && rows.size() > 1 && isRetryableBatchFailure(ex);
|
||||
}
|
||||
|
||||
private boolean isRetryableBatchFailure(Exception ex) {
|
||||
if (ex instanceof PartialCozeResultException) {
|
||||
return true;
|
||||
}
|
||||
String message = ex == null ? "" : nonBlank(ex.getMessage(), "");
|
||||
return message.contains("Workflow node execution limit exceeded")
|
||||
|| message.contains("Read timed out")
|
||||
|| message.contains("Connection reset")
|
||||
|| message.contains("I/O error on POST request")
|
||||
|| message.toLowerCase(Locale.ROOT).contains("timeout");
|
||||
}
|
||||
|
||||
private int resolvedCount(List<AppearancePatentResultRowDto> rows) {
|
||||
int resolved = 0;
|
||||
for (AppearancePatentResultRowDto row : rows) {
|
||||
if (hasResolvedCozeFields(row)) {
|
||||
resolved++;
|
||||
}
|
||||
}
|
||||
return resolved;
|
||||
}
|
||||
|
||||
private boolean hasResolvedCozeFields(AppearancePatentResultRowDto row) {
|
||||
if (row == null) {
|
||||
return false;
|
||||
}
|
||||
return !normalize(row.getTitleRisk()).isBlank()
|
||||
|| !normalize(row.getAppearanceRisk()).isBlank()
|
||||
|| !normalize(row.getPatentRisk()).isBlank()
|
||||
|| !normalize(row.getConclusion()).isBlank();
|
||||
}
|
||||
|
||||
private String abbreviate(String value, int maxLength) {
|
||||
String normalized = value == null ? "" : value.trim();
|
||||
if (normalized.length() <= maxLength) {
|
||||
return normalized;
|
||||
}
|
||||
return normalized.substring(0, Math.max(0, maxLength - 3)) + "...";
|
||||
}
|
||||
|
||||
private RuntimeException propagate(Exception ex) {
|
||||
if (ex instanceof RuntimeException runtimeException) {
|
||||
return runtimeException;
|
||||
}
|
||||
return new IllegalStateException(nonBlank(ex.getMessage(), "Coze call failed"), ex);
|
||||
}
|
||||
|
||||
private JsonNode firstNonNull(JsonNode left, JsonNode right) {
|
||||
return left == null || left.isNull() ? right : left;
|
||||
}
|
||||
@@ -134,6 +288,38 @@ public class AppearancePatentCozeClient {
|
||||
return value == null || value.isBlank() ? fallback : value;
|
||||
}
|
||||
|
||||
private String normalize(String value) {
|
||||
return value == null ? "" : value.replace("\ufeff", "").replace("\u3000", " ").trim();
|
||||
}
|
||||
|
||||
private String rowKey(AppearancePatentResultRowDto row) {
|
||||
if (row == null) {
|
||||
return "";
|
||||
}
|
||||
return rowKey(row.getId(), row.getAsin(), row.getCountry());
|
||||
}
|
||||
|
||||
private String rowKey(String rowId, String asin, String country) {
|
||||
return normalize(rowId) + "::" + normalize(asin).toUpperCase(Locale.ROOT) + "::" + normalize(country);
|
||||
}
|
||||
|
||||
private String failureMessage(Exception ex) {
|
||||
if (ex instanceof PartialCozeResultException partial) {
|
||||
return "Coze返回结果不完整(" + partial.resolvedCount() + "/" + partial.expectedCount() + ")";
|
||||
}
|
||||
String message = ex == null ? null : ex.getMessage();
|
||||
if (message == null || message.isBlank()) {
|
||||
return "Coze调用失败";
|
||||
}
|
||||
if (message.contains("Workflow node execution limit exceeded")) {
|
||||
return "Coze工作流节点执行超限,请检查工作流配置";
|
||||
}
|
||||
if (message.contains("Read timed out")) {
|
||||
return "Coze调用超时,请稍后重试";
|
||||
}
|
||||
return "Coze调用失败: " + message;
|
||||
}
|
||||
|
||||
private String stripBearer(String token) {
|
||||
String normalized = token == null ? "" : token.trim();
|
||||
return normalized.regionMatches(true, 0, "Bearer ", 0, 7) ? normalized.substring(7).trim() : normalized;
|
||||
@@ -151,6 +337,49 @@ public class AppearancePatentCozeClient {
|
||||
return base + suffix;
|
||||
}
|
||||
|
||||
private record CozeResult(String title, String appearance, String patent, String result) {
|
||||
private record CozeResult(
|
||||
String rowId,
|
||||
String asin,
|
||||
String country,
|
||||
String title,
|
||||
String appearance,
|
||||
String patent,
|
||||
String result
|
||||
) {
|
||||
}
|
||||
|
||||
private record InspectAttempt(
|
||||
String raw,
|
||||
List<AppearancePatentResultRowDto> mergedRows,
|
||||
int resolvedCount,
|
||||
int rawResultCount
|
||||
) {
|
||||
}
|
||||
|
||||
private static final class PartialCozeResultException extends RuntimeException {
|
||||
|
||||
private final int resolvedCount;
|
||||
private final int expectedCount;
|
||||
private final int rawResultCount;
|
||||
|
||||
private PartialCozeResultException(int resolvedCount, int expectedCount, int rawResultCount) {
|
||||
super("partial-result resolved=" + resolvedCount + "/" + expectedCount + " raw=" + rawResultCount);
|
||||
this.resolvedCount = resolvedCount;
|
||||
this.expectedCount = expectedCount;
|
||||
this.rawResultCount = rawResultCount;
|
||||
}
|
||||
|
||||
private int resolvedCount() {
|
||||
return resolvedCount;
|
||||
}
|
||||
|
||||
private int expectedCount() {
|
||||
return expectedCount;
|
||||
}
|
||||
|
||||
@SuppressWarnings("unused")
|
||||
private int rawResultCount() {
|
||||
return rawResultCount;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -16,6 +16,7 @@ public class AppearancePatentParsedRowVo {
|
||||
@Schema(description = "前端展示和 Python 回传使用的 id。整数 id 原样保留,子数据第一条如 2_1 原样保留。", example = "2_1")
|
||||
private String displayId;
|
||||
@Schema(description = "亚马逊 ASIN。", example = "B0CJ8SNXXV")
|
||||
private String groupKey;
|
||||
private String asin;
|
||||
@Schema(description = "国家或站点。", example = "英国")
|
||||
private String country;
|
||||
|
||||
@@ -22,14 +22,14 @@ public class AppearancePatentTaskCacheService {
|
||||
private final StringRedisTemplate stringRedisTemplate;
|
||||
private final ObjectMapper objectMapper;
|
||||
|
||||
public void appendPendingRow(Long taskId, Integer chunkIndex, AppearancePatentResultRowDto row) {
|
||||
if (taskId == null || taskId <= 0 || chunkIndex == null || row == null) {
|
||||
public void appendPendingRow(Long taskId, String scopeHash, Integer chunkIndex, AppearancePatentResultRowDto row) {
|
||||
if (taskId == null || taskId <= 0 || scopeHash == null || scopeHash.isBlank() || chunkIndex == null || row == null) {
|
||||
return;
|
||||
}
|
||||
try {
|
||||
stringRedisTemplate.opsForList().rightPush(
|
||||
pendingRowsKey(taskId),
|
||||
objectMapper.writeValueAsString(new PendingRow(chunkIndex, row))
|
||||
objectMapper.writeValueAsString(new PendingRow(scopeHash, chunkIndex, row))
|
||||
);
|
||||
stringRedisTemplate.expire(pendingRowsKey(taskId), Duration.ofHours(TTL_HOURS));
|
||||
touchTaskHeartbeat(taskId);
|
||||
@@ -139,6 +139,6 @@ public class AppearancePatentTaskCacheService {
|
||||
return "appearance-patent:task:heartbeat:" + taskId;
|
||||
}
|
||||
|
||||
public record PendingRow(Integer chunkIndex, AppearancePatentResultRowDto row) {
|
||||
public record PendingRow(String scopeHash, Integer chunkIndex, AppearancePatentResultRowDto row) {
|
||||
}
|
||||
}
|
||||
|
||||
@@ -49,7 +49,10 @@ import org.apache.poi.xssf.streaming.SXSSFWorkbook;
|
||||
import org.springframework.dao.DuplicateKeyException;
|
||||
import org.springframework.scheduling.annotation.Scheduled;
|
||||
import org.springframework.stereotype.Service;
|
||||
import org.springframework.transaction.PlatformTransactionManager;
|
||||
import org.springframework.transaction.annotation.Transactional;
|
||||
import org.springframework.transaction.TransactionDefinition;
|
||||
import org.springframework.transaction.support.TransactionTemplate;
|
||||
|
||||
import java.io.File;
|
||||
import java.io.FileInputStream;
|
||||
@@ -65,6 +68,7 @@ import java.util.Locale;
|
||||
import java.util.Map;
|
||||
import java.util.Objects;
|
||||
import java.util.Set;
|
||||
import java.util.function.Supplier;
|
||||
|
||||
@Service
|
||||
@RequiredArgsConstructor
|
||||
@@ -101,6 +105,7 @@ public class AppearancePatentTaskService {
|
||||
private final AppearancePatentProperties properties;
|
||||
private final TaskFileJobService taskFileJobService;
|
||||
private final TransientPayloadStorageService transientPayloadStorageService;
|
||||
private final PlatformTransactionManager transactionManager;
|
||||
|
||||
@Transactional
|
||||
public AppearancePatentParseVo parseAndCreateTask(AppearancePatentParseRequest request) {
|
||||
@@ -259,8 +264,16 @@ public class AppearancePatentTaskService {
|
||||
return vo;
|
||||
}
|
||||
|
||||
@Transactional
|
||||
public void submitResult(Long taskId, AppearancePatentSubmitResultRequest request) {
|
||||
if (transactionManager != null) {
|
||||
SubmitContext context = inNewTransaction(() -> persistSubmittedChunk(taskId, request));
|
||||
flushPendingRowsIfNeeded(context.task(), context.forceFlush());
|
||||
inNewTransaction(() -> {
|
||||
completeSubmittedChunk(context);
|
||||
return null;
|
||||
});
|
||||
return;
|
||||
}
|
||||
FileTaskEntity task = fileTaskMapper.selectById(taskId);
|
||||
if (task == null || !MODULE_TYPE.equals(task.getModuleType())) {
|
||||
throw new BusinessException("任务不存在");
|
||||
@@ -302,19 +315,21 @@ public class AppearancePatentTaskService {
|
||||
taskChunkMapper.insert(chunk);
|
||||
} catch (DuplicateKeyException ex) {
|
||||
inserted = false;
|
||||
transientPayloadStorageService.deletePayloadIfPresent(storedPayload);
|
||||
// storeChunkPayload uses a deterministic key like chunk-{index}. When two
|
||||
// concurrent callbacks submit the same chunk, deleting the loser payload here
|
||||
// can also remove the winner's shared object and break later assembly.
|
||||
log.info("[appearance-patent] duplicate chunk inserted concurrently taskId={} scope={} chunk={}", taskId, scopeKey, chunkIndex);
|
||||
}
|
||||
if (inserted) {
|
||||
for (AppearancePatentResultRowDto rawRow : rawRows) {
|
||||
taskCacheService.appendPendingRow(taskId, chunkIndex, rawRow);
|
||||
taskCacheService.appendPendingRow(taskId, scopeHash, chunkIndex, rawRow);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
log.info("[appearance-patent] duplicate chunk ignored taskId={} scope={} chunk={}", taskId, scopeKey, chunkIndex);
|
||||
}
|
||||
|
||||
flushPendingRowsIfNeeded(task, scopeHash, Boolean.TRUE.equals(request.getDone()));
|
||||
flushPendingRowsIfNeeded(task, Boolean.TRUE.equals(request.getDone()));
|
||||
|
||||
TaskScopeStateEntity scope = taskScopeStateMapper.selectOne(new LambdaQueryWrapper<TaskScopeStateEntity>()
|
||||
.eq(TaskScopeStateEntity::getTaskId, taskId)
|
||||
@@ -394,8 +409,28 @@ public class AppearancePatentTaskService {
|
||||
}
|
||||
|
||||
@Scheduled(cron = "${aiimage.appearance-patent.stale-finalize-cron:0 */2 * * * *}")
|
||||
@Transactional
|
||||
public void finalizeStaleTasks() {
|
||||
if (transactionManager != null) {
|
||||
LocalDateTime threshold = LocalDateTime.now().minusMinutes(Math.max(1, properties.getStaleTimeoutMinutes()));
|
||||
long thresholdMillis = threshold.atZone(ZoneId.systemDefault()).toInstant().toEpochMilli();
|
||||
List<FileTaskEntity> tasks = fileTaskMapper.selectList(new LambdaQueryWrapper<FileTaskEntity>()
|
||||
.eq(FileTaskEntity::getModuleType, MODULE_TYPE)
|
||||
.eq(FileTaskEntity::getStatus, STATUS_RUNNING)
|
||||
.lt(FileTaskEntity::getUpdatedAt, threshold)
|
||||
.last("limit 50"));
|
||||
for (FileTaskEntity task : tasks) {
|
||||
long heartbeatMillis = taskCacheService.getTaskHeartbeatMillis(task.getId());
|
||||
if (heartbeatMillis > thresholdMillis) {
|
||||
continue;
|
||||
}
|
||||
flushPendingRowsIfNeeded(task, true);
|
||||
inNewTransaction(() -> {
|
||||
finalizeStaleTask(task.getId(), "Python interrupted before uploading final appearance patent result");
|
||||
return null;
|
||||
});
|
||||
}
|
||||
return;
|
||||
}
|
||||
LocalDateTime threshold = LocalDateTime.now().minusMinutes(Math.max(1, properties.getStaleTimeoutMinutes()));
|
||||
long thresholdMillis = threshold.atZone(ZoneId.systemDefault()).toInstant().toEpochMilli();
|
||||
List<FileTaskEntity> tasks = fileTaskMapper.selectList(new LambdaQueryWrapper<FileTaskEntity>()
|
||||
@@ -408,11 +443,168 @@ public class AppearancePatentTaskService {
|
||||
if (heartbeatMillis > thresholdMillis) {
|
||||
continue;
|
||||
}
|
||||
flushPendingRowsIfNeeded(task, DigestUtil.sha256Hex("appearance-patent-" + task.getId()), true);
|
||||
flushPendingRowsIfNeeded(task, true);
|
||||
finalizeTask(task, "Python interrupted before uploading final appearance patent result", allRowCount(task), true);
|
||||
}
|
||||
}
|
||||
|
||||
private SubmitContext persistSubmittedChunk(Long taskId, AppearancePatentSubmitResultRequest request) {
|
||||
FileTaskEntity task = fileTaskMapper.selectById(taskId);
|
||||
if (task == null || !MODULE_TYPE.equals(task.getModuleType())) {
|
||||
throw new BusinessException("浠诲姟涓嶅瓨鍦?");
|
||||
}
|
||||
if (!STATUS_RUNNING.equals(task.getStatus())) {
|
||||
throw new BusinessException("浠诲姟涓嶆槸杩愯涓姸鎬?");
|
||||
}
|
||||
|
||||
int chunkIndex = request.getChunkIndex() == null ? 0 : request.getChunkIndex();
|
||||
int chunkTotal = request.getChunkTotal() == null ? 1 : request.getChunkTotal();
|
||||
boolean done = Boolean.TRUE.equals(request.getDone());
|
||||
String scopeKey = firstNonBlank(request.getSubmissionId(), "task:" + taskId);
|
||||
String scopeHash = DigestUtil.sha256Hex(scopeKey);
|
||||
taskCacheService.touchTaskHeartbeat(taskId);
|
||||
|
||||
TaskChunkEntity existing = taskChunkMapper.selectOne(new LambdaQueryWrapper<TaskChunkEntity>()
|
||||
.eq(TaskChunkEntity::getTaskId, taskId)
|
||||
.eq(TaskChunkEntity::getModuleType, MODULE_TYPE)
|
||||
.eq(TaskChunkEntity::getScopeHash, scopeHash)
|
||||
.eq(TaskChunkEntity::getChunkIndex, chunkIndex)
|
||||
.last("limit 1"));
|
||||
if (existing == null) {
|
||||
List<AppearancePatentResultRowDto> rawRows = request.getItems() == null ? List.of() : request.getItems();
|
||||
String payloadJson = writeJson(rawRows, "缁撴灉搴忓垪鍖栧け璐?");
|
||||
|
||||
TaskChunkEntity chunk = new TaskChunkEntity();
|
||||
chunk.setTaskId(taskId);
|
||||
chunk.setModuleType(MODULE_TYPE);
|
||||
chunk.setScopeKey(scopeKey);
|
||||
chunk.setScopeHash(scopeHash);
|
||||
chunk.setChunkIndex(chunkIndex);
|
||||
chunk.setChunkTotal(chunkTotal);
|
||||
String storedPayload = transientPayloadStorageService.storeChunkPayload(MODULE_TYPE, taskId, scopeHash, chunkIndex, payloadJson);
|
||||
chunk.setPayloadJson(storedPayload);
|
||||
chunk.setPayloadHash(DigestUtil.sha256Hex(payloadJson));
|
||||
chunk.setCreatedAt(LocalDateTime.now());
|
||||
chunk.setUpdatedAt(LocalDateTime.now());
|
||||
boolean inserted = true;
|
||||
try {
|
||||
taskChunkMapper.insert(chunk);
|
||||
} catch (DuplicateKeyException ex) {
|
||||
inserted = false;
|
||||
log.info("[appearance-patent] duplicate chunk inserted concurrently taskId={} scope={} chunk={}", taskId, scopeKey, chunkIndex);
|
||||
}
|
||||
if (inserted) {
|
||||
for (AppearancePatentResultRowDto rawRow : rawRows) {
|
||||
taskCacheService.appendPendingRow(taskId, scopeHash, chunkIndex, rawRow);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
log.info("[appearance-patent] duplicate chunk ignored taskId={} scope={} chunk={}", taskId, scopeKey, chunkIndex);
|
||||
}
|
||||
|
||||
upsertScopeState(taskId, scopeKey, scopeHash, chunkTotal, request.getError(), done, false);
|
||||
task.setUpdatedAt(LocalDateTime.now());
|
||||
fileTaskMapper.updateById(task);
|
||||
return new SubmitContext(task, scopeKey, scopeHash, done, request.getError());
|
||||
}
|
||||
|
||||
private void completeSubmittedChunk(SubmitContext context) {
|
||||
FileTaskEntity task = fileTaskMapper.selectById(context.task().getId());
|
||||
if (task == null || !MODULE_TYPE.equals(task.getModuleType())) {
|
||||
throw new BusinessException("浠诲姟涓嶅瓨鍦?");
|
||||
}
|
||||
if (!STATUS_RUNNING.equals(task.getStatus())) {
|
||||
log.info("[appearance-patent] skip completion because task already finalized taskId={} status={}",
|
||||
task.getId(), task.getStatus());
|
||||
return;
|
||||
}
|
||||
upsertScopeState(task.getId(), context.scopeKey(), context.scopeHash(), null, context.error(), context.forceFlush(), true);
|
||||
if (context.forceFlush() || context.error() != null && !context.error().isBlank()) {
|
||||
finalizeTask(task, context.error(), allRowCount(task), true);
|
||||
return;
|
||||
}
|
||||
task.setUpdatedAt(LocalDateTime.now());
|
||||
fileTaskMapper.updateById(task);
|
||||
}
|
||||
|
||||
private void finalizeStaleTask(Long taskId, String error) {
|
||||
FileTaskEntity task = fileTaskMapper.selectById(taskId);
|
||||
if (task == null || !MODULE_TYPE.equals(task.getModuleType()) || !STATUS_RUNNING.equals(task.getStatus())) {
|
||||
return;
|
||||
}
|
||||
finalizeTask(task, error, allRowCount(task), true);
|
||||
}
|
||||
|
||||
private void upsertScopeState(Long taskId,
|
||||
String scopeKey,
|
||||
String scopeHash,
|
||||
Integer chunkTotal,
|
||||
String error,
|
||||
boolean completed,
|
||||
boolean cozeDone) {
|
||||
TaskScopeStateEntity scope = taskScopeStateMapper.selectOne(new LambdaQueryWrapper<TaskScopeStateEntity>()
|
||||
.eq(TaskScopeStateEntity::getTaskId, taskId)
|
||||
.eq(TaskScopeStateEntity::getModuleType, MODULE_TYPE)
|
||||
.eq(TaskScopeStateEntity::getScopeHash, scopeHash)
|
||||
.last("limit 1"));
|
||||
LocalDateTime now = LocalDateTime.now();
|
||||
if (scope == null) {
|
||||
scope = new TaskScopeStateEntity();
|
||||
scope.setTaskId(taskId);
|
||||
scope.setModuleType(MODULE_TYPE);
|
||||
scope.setScopeKey(scopeKey);
|
||||
scope.setScopeHash(scopeHash);
|
||||
scope.setCreatedAt(now);
|
||||
}
|
||||
if (chunkTotal != null) {
|
||||
scope.setChunkTotal(chunkTotal);
|
||||
}
|
||||
scope.setReceivedChunkCount(countChunks(taskId, scopeHash));
|
||||
scope.setLastChunkAt(now);
|
||||
scope.setLastError(error);
|
||||
scope.setCompleted(completed ? 1 : 0);
|
||||
scope.setUpdatedAt(now);
|
||||
scope.setStateJson(cozeDone
|
||||
? "{\"phase\":\"RECEIVED\",\"coze\":\"DONE\"}"
|
||||
: "{\"phase\":\"RECEIVED\",\"coze\":\"PENDING\"}");
|
||||
if (scope.getId() == null) {
|
||||
try {
|
||||
taskScopeStateMapper.insert(scope);
|
||||
return;
|
||||
} catch (DuplicateKeyException ex) {
|
||||
log.info("[appearance-patent] duplicate scope state inserted concurrently taskId={} scope={}", taskId, scopeKey);
|
||||
scope = taskScopeStateMapper.selectOne(new LambdaQueryWrapper<TaskScopeStateEntity>()
|
||||
.eq(TaskScopeStateEntity::getTaskId, taskId)
|
||||
.eq(TaskScopeStateEntity::getModuleType, MODULE_TYPE)
|
||||
.eq(TaskScopeStateEntity::getScopeHash, scopeHash)
|
||||
.last("limit 1"));
|
||||
if (scope == null) {
|
||||
log.info("[appearance-patent] scope state winner not committed yet, skip duplicate updater taskId={} scope={}",
|
||||
taskId, scopeKey);
|
||||
return;
|
||||
}
|
||||
if (chunkTotal != null) {
|
||||
scope.setChunkTotal(chunkTotal);
|
||||
}
|
||||
scope.setReceivedChunkCount(countChunks(taskId, scopeHash));
|
||||
scope.setLastChunkAt(now);
|
||||
scope.setLastError(error);
|
||||
scope.setCompleted(completed ? 1 : 0);
|
||||
scope.setUpdatedAt(now);
|
||||
scope.setStateJson(cozeDone
|
||||
? "{\"phase\":\"RECEIVED\",\"coze\":\"DONE\"}"
|
||||
: "{\"phase\":\"RECEIVED\",\"coze\":\"PENDING\"}");
|
||||
}
|
||||
}
|
||||
taskScopeStateMapper.updateById(scope);
|
||||
}
|
||||
|
||||
private <T> T inNewTransaction(Supplier<T> action) {
|
||||
TransactionTemplate template = new TransactionTemplate(transactionManager);
|
||||
template.setPropagationBehavior(TransactionDefinition.PROPAGATION_REQUIRES_NEW);
|
||||
return template.execute(status -> action.get());
|
||||
}
|
||||
|
||||
private List<AppearancePatentResultRowDto> applyCozeInBatches(List<AppearancePatentResultRowDto> items, FileTaskEntity task) {
|
||||
if (items == null || items.isEmpty()) {
|
||||
return List.of();
|
||||
@@ -426,8 +618,10 @@ public class AppearancePatentTaskService {
|
||||
return result;
|
||||
}
|
||||
|
||||
private void flushPendingRowsIfNeeded(FileTaskEntity task, String scopeHash, boolean force) {
|
||||
private void flushPendingRowsIfNeeded(FileTaskEntity task, boolean force) {
|
||||
int batchSize = Math.max(1, properties.getCozeBatchSize());
|
||||
Map<String, List<AppearancePatentParsedRowVo>> allRowsByBaseId = loadAllRowsByBaseId(task);
|
||||
String prompt = readAiPrompt(task);
|
||||
while (force ? taskCacheService.pendingRowCount(task.getId()) > 0 : taskCacheService.pendingRowCount(task.getId()) >= batchSize) {
|
||||
int drainSize = force ? batchSize : Math.max(1, batchSize);
|
||||
List<AppearancePatentTaskCacheService.PendingRow> pendingRows = taskCacheService.drainPendingRows(task.getId(), drainSize);
|
||||
@@ -437,27 +631,47 @@ public class AppearancePatentTaskService {
|
||||
List<AppearancePatentResultRowDto> rawRows = pendingRows.stream()
|
||||
.map(AppearancePatentTaskCacheService.PendingRow::row)
|
||||
.toList();
|
||||
List<AppearancePatentResultRowDto> cozeRows = cozeClient.inspect(rawRows, readAiPrompt(task));
|
||||
List<AppearancePatentResultRowDto> cozeRows = cozeClient.inspect(rawRows, prompt);
|
||||
Map<String, Map<String, AppearancePatentResultRowDto>> chunkRows = new LinkedHashMap<>();
|
||||
for (int i = 0; i < pendingRows.size(); i++) {
|
||||
AppearancePatentTaskCacheService.PendingRow pending = pendingRows.get(i);
|
||||
AppearancePatentResultRowDto resultRow = i < cozeRows.size() ? cozeRows.get(i) : pending.row();
|
||||
List<AppearancePatentResultRowDto> persistedRows = expandRows(task, List.of(resultRow));
|
||||
updateChunkPayload(task.getId(), scopeHash, pending.chunkIndex(), persistedRows);
|
||||
List<AppearancePatentResultRowDto> expandedRows = expandRows(List.of(resultRow), allRowsByBaseId);
|
||||
String chunkKey = pending.scopeHash() + "::" + pending.chunkIndex();
|
||||
Map<String, AppearancePatentResultRowDto> mergedRows =
|
||||
chunkRows.computeIfAbsent(chunkKey, key -> new LinkedHashMap<>());
|
||||
for (AppearancePatentResultRowDto expandedRow : expandedRows) {
|
||||
mergedRows.put(rowKey(expandedRow.getId(), expandedRow.getAsin(), expandedRow.getCountry()), expandedRow);
|
||||
}
|
||||
}
|
||||
for (Map.Entry<String, Map<String, AppearancePatentResultRowDto>> entry : chunkRows.entrySet()) {
|
||||
int delimiter = entry.getKey().lastIndexOf("::");
|
||||
if (delimiter <= 0) {
|
||||
continue;
|
||||
}
|
||||
String scopeHash = entry.getKey().substring(0, delimiter);
|
||||
Integer chunkIndex = Integer.parseInt(entry.getKey().substring(delimiter + 2));
|
||||
mergeChunkPayload(task.getId(), scopeHash, chunkIndex, new ArrayList<>(entry.getValue().values()));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void updateChunkPayload(Long taskId, String scopeHash, Integer chunkIndex, List<AppearancePatentResultRowDto> rows) {
|
||||
private void mergeChunkPayload(Long taskId, String scopeHash, Integer chunkIndex, List<AppearancePatentResultRowDto> rows) {
|
||||
TaskChunkEntity chunk = taskChunkMapper.selectOne(new LambdaQueryWrapper<TaskChunkEntity>()
|
||||
.eq(TaskChunkEntity::getTaskId, taskId)
|
||||
.eq(TaskChunkEntity::getModuleType, MODULE_TYPE)
|
||||
.eq(TaskChunkEntity::getScopeHash, scopeHash)
|
||||
.eq(TaskChunkEntity::getChunkIndex, chunkIndex)
|
||||
.last("limit 1"));
|
||||
if (chunk == null) {
|
||||
if (chunk == null || rows == null || rows.isEmpty()) {
|
||||
return;
|
||||
}
|
||||
Map<String, AppearancePatentResultRowDto> persistedRows = readChunkRows(chunk);
|
||||
for (AppearancePatentResultRowDto row : rows) {
|
||||
persistedRows.put(rowKey(row.getId(), row.getAsin(), row.getCountry()), row);
|
||||
}
|
||||
String payloadJson = writeJson(rows == null ? List.of() : rows, "结果序列化失败");
|
||||
payloadJson = writeJson(new ArrayList<>(persistedRows.values()), "appearance patent chunk payload merge failed");
|
||||
String storedPayload = transientPayloadStorageService.storeChunkPayload(MODULE_TYPE, taskId, scopeHash, chunkIndex, payloadJson);
|
||||
transientPayloadStorageService.deleteReplacedPayloadIfNeeded(chunk.getPayloadJson(), storedPayload);
|
||||
chunk.setPayloadJson(storedPayload);
|
||||
@@ -466,16 +680,15 @@ public class AppearancePatentTaskService {
|
||||
taskChunkMapper.updateById(chunk);
|
||||
}
|
||||
|
||||
private List<AppearancePatentResultRowDto> expandRows(FileTaskEntity task, List<AppearancePatentResultRowDto> representatives) {
|
||||
private List<AppearancePatentResultRowDto> expandRows(List<AppearancePatentResultRowDto> representatives,
|
||||
Map<String, List<AppearancePatentParsedRowVo>> allRowsByBaseId) {
|
||||
if (representatives == null || representatives.isEmpty()) {
|
||||
return List.of();
|
||||
}
|
||||
Map<String, List<AppearancePatentParsedRowVo>> allRowsByBaseId = loadAllRowsByBaseId(task);
|
||||
List<AppearancePatentResultRowDto> result = new ArrayList<>();
|
||||
Set<String> emittedKeys = new LinkedHashSet<>();
|
||||
for (AppearancePatentResultRowDto representative : representatives) {
|
||||
String baseId = baseId(representative.getId());
|
||||
List<AppearancePatentParsedRowVo> siblings = allRowsByBaseId.get(baseId);
|
||||
List<AppearancePatentParsedRowVo> siblings = resolveSiblingRows(representative, allRowsByBaseId);
|
||||
if (siblings == null || siblings.isEmpty()) {
|
||||
result.add(representative);
|
||||
continue;
|
||||
@@ -491,14 +704,40 @@ public class AppearancePatentTaskService {
|
||||
return result;
|
||||
}
|
||||
|
||||
private Map<String, List<AppearancePatentParsedRowVo>> loadAllRowsByBaseId(FileTaskEntity task) {
|
||||
Map<String, List<AppearancePatentParsedRowVo>> result = new LinkedHashMap<>();
|
||||
try {
|
||||
for (AppearancePatentParsedRowVo row : readParsedPayload(task).getAllItems()) {
|
||||
result.computeIfAbsent(baseId(row.getDisplayId()), key -> new ArrayList<>()).add(row);
|
||||
private List<AppearancePatentParsedRowVo> resolveSiblingRows(AppearancePatentResultRowDto representative,
|
||||
Map<String, List<AppearancePatentParsedRowVo>> groupedRows) {
|
||||
if (representative == null || groupedRows == null || groupedRows.isEmpty()) {
|
||||
return List.of();
|
||||
}
|
||||
String representativeKey = rowKey(representative.getId(), representative.getAsin(), representative.getCountry());
|
||||
for (List<AppearancePatentParsedRowVo> rows : groupedRows.values()) {
|
||||
for (AppearancePatentParsedRowVo row : rows) {
|
||||
String rowKey = rowKey(row.getDisplayId(), row.getAsin(), row.getCountry());
|
||||
if (Objects.equals(representativeKey, rowKey)) {
|
||||
return rows;
|
||||
}
|
||||
}
|
||||
}
|
||||
return List.of();
|
||||
}
|
||||
|
||||
private Map<String, List<AppearancePatentParsedRowVo>> loadAllRowsByBaseId(FileTaskEntity task) {
|
||||
try {
|
||||
return groupRowsByBaseId(readParsedPayload(task).getAllItems());
|
||||
} catch (Exception ex) {
|
||||
log.warn("[appearance-patent] read all rows failed taskId={} err={}", task.getId(), ex.getMessage());
|
||||
return new LinkedHashMap<>();
|
||||
}
|
||||
}
|
||||
|
||||
private Map<String, List<AppearancePatentParsedRowVo>> groupRowsByBaseId(List<AppearancePatentParsedRowVo> rows) {
|
||||
Map<String, List<AppearancePatentParsedRowVo>> result = new LinkedHashMap<>();
|
||||
if (rows == null) {
|
||||
return result;
|
||||
}
|
||||
for (AppearancePatentParsedRowVo row : rows) {
|
||||
String key = firstNonBlank(normalize(row.getGroupKey()), baseId(row.getDisplayId()));
|
||||
result.computeIfAbsent(key, ignored -> new ArrayList<>()).add(row);
|
||||
}
|
||||
return result;
|
||||
}
|
||||
@@ -517,8 +756,8 @@ public class AppearancePatentTaskService {
|
||||
row.setId(sibling.getDisplayId());
|
||||
row.setAsin(sibling.getAsin());
|
||||
row.setCountry(sibling.getCountry());
|
||||
row.setUrl(sibling.getUrl());
|
||||
row.setTitle(sibling.getTitle());
|
||||
row.setUrl(firstNonBlank(representative.getUrl(), sibling.getUrl()));
|
||||
row.setTitle(firstNonBlank(representative.getTitle(), sibling.getTitle()));
|
||||
row.setError(representative.getError());
|
||||
row.setTitleRisk(representative.getTitleRisk());
|
||||
row.setAppearanceRisk(representative.getAppearanceRisk());
|
||||
@@ -644,22 +883,7 @@ public class AppearancePatentTaskService {
|
||||
.eq(TaskChunkEntity::getModuleType, MODULE_TYPE)
|
||||
.orderByAsc(TaskChunkEntity::getChunkIndex));
|
||||
for (TaskChunkEntity chunk : chunks) {
|
||||
if (chunk.getPayloadJson() == null || chunk.getPayloadJson().isBlank()) {
|
||||
continue;
|
||||
}
|
||||
try {
|
||||
String payloadJson = transientPayloadStorageService.resolvePayload(chunk.getPayloadJson(), "read appearance patent chunk failed");
|
||||
JsonNode array = objectMapper.readTree(payloadJson);
|
||||
if (!array.isArray()) {
|
||||
continue;
|
||||
}
|
||||
for (JsonNode node : array) {
|
||||
AppearancePatentResultRowDto row = objectMapper.treeToValue(node, AppearancePatentResultRowDto.class);
|
||||
result.put(rowKey(row.getId(), row.getAsin(), row.getCountry()), row);
|
||||
}
|
||||
} catch (Exception ex) {
|
||||
log.warn("[appearance-patent] read chunk payload failed taskId={} chunk={} err={}", taskId, chunk.getChunkIndex(), ex.getMessage());
|
||||
}
|
||||
result.putAll(readChunkRows(chunk));
|
||||
}
|
||||
return result;
|
||||
}
|
||||
@@ -672,26 +896,35 @@ public class AppearancePatentTaskService {
|
||||
font.setBold(true);
|
||||
headerStyle.setFont(font);
|
||||
|
||||
List<String> resultHeaders = new ArrayList<>(RESULT_HEADERS);
|
||||
resultHeaders.add(4, "title");
|
||||
resultHeaders.add(5, "url");
|
||||
Row header = sheet.createRow(0);
|
||||
for (int i = 0; i < RESULT_HEADERS.size(); i++) {
|
||||
for (int i = 0; i < resultHeaders.size(); i++) {
|
||||
Cell cell = header.createCell(i);
|
||||
cell.setCellValue(RESULT_HEADERS.get(i));
|
||||
cell.setCellValue(resultHeaders.get(i));
|
||||
cell.setCellStyle(headerStyle);
|
||||
}
|
||||
|
||||
int rowIndex = 1;
|
||||
for (AppearancePatentParsedRowVo parsedRow : parsed.getAllItems()) {
|
||||
AppearancePatentResultRowDto resultRow = resultMap.get(rowKey(parsedRow.getDisplayId(), parsedRow.getAsin(), parsedRow.getCountry()));
|
||||
String missingReason = "";
|
||||
if (resultRow == null) {
|
||||
missingReason = hasPromptFields(parsedRow) ? "未匹配到检测结果" : "未送检:缺少标题或图片";
|
||||
}
|
||||
Row row = sheet.createRow(rowIndex++);
|
||||
int col = 0;
|
||||
row.createCell(col++).setCellValue(firstNonBlank(parsedRow.getDisplayId(), parsedRow.getSourceId()));
|
||||
row.createCell(col++).setCellValue(firstNonBlank(parsedRow.getAsin(), ""));
|
||||
row.createCell(col++).setCellValue(firstNonBlank(parsedRow.getCountry(), ""));
|
||||
row.createCell(col++).setCellValue(readValueByHeader(parsedRow, "价格", "price"));
|
||||
row.createCell(col++).setCellValue(resultRow == null ? "" : firstNonBlank(resultRow.getTitleRisk(), ""));
|
||||
row.createCell(col++).setCellValue(resultRow == null ? "" : firstNonBlank(resultRow.getAppearanceRisk(), ""));
|
||||
row.createCell(col++).setCellValue(resultRow == null ? "" : firstNonBlank(resultRow.getPatentRisk(), ""));
|
||||
row.createCell(col).setCellValue(resultRow == null ? "" : firstNonBlank(resultRow.getConclusion(), ""));
|
||||
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 ? missingReason : firstNonBlank(resultRow.getTitleRisk(), ""));
|
||||
row.createCell(col++).setCellValue(resultRow == null ? missingReason : firstNonBlank(resultRow.getAppearanceRisk(), ""));
|
||||
row.createCell(col++).setCellValue(resultRow == null ? missingReason : firstNonBlank(resultRow.getPatentRisk(), ""));
|
||||
row.createCell(col).setCellValue(resultRow == null ? "未送检" : firstNonBlank(resultRow.getConclusion(), ""));
|
||||
}
|
||||
workbook.write(fos);
|
||||
workbook.dispose();
|
||||
@@ -713,13 +946,18 @@ public class AppearancePatentTaskService {
|
||||
int idCol = findRequiredHeader(headerMap, "id");
|
||||
int asinCol = findRequiredHeader(headerMap, "asin");
|
||||
int countryCol = findRequiredHeader(headerMap, "国家", "country");
|
||||
int urlCol = findOptionalHeader(headerMap, "url", "rul", "链接", "商品链接");
|
||||
int titleCol = findOptionalHeader(headerMap, "标题", "title", "商品标题");
|
||||
int urlCol = findOptionalHeaderExact(headerMap,
|
||||
"url", "rul", "link", "image", "img", "pic", "picture",
|
||||
"链接", "商品链接", "图片", "商品图片", "主图", "商品主图", "图片链接", "主图链接");
|
||||
int titleCol = findOptionalHeaderExact(headerMap,
|
||||
"标题", "title", "listing title", "product title", "商品标题", "商品名称", "产品名称");
|
||||
|
||||
List<AppearancePatentParsedRowVo> accepted = new ArrayList<>();
|
||||
List<AppearancePatentParsedRowVo> allRows = new ArrayList<>();
|
||||
int total = 0;
|
||||
int dropped = 0;
|
||||
String currentBlockBaseId = "";
|
||||
String currentGroupKey = "";
|
||||
for (int i = 1; i <= sheet.getLastRowNum(); i++) {
|
||||
Row row = sheet.getRow(i);
|
||||
if (row == null) {
|
||||
@@ -740,6 +978,12 @@ public class AppearancePatentTaskService {
|
||||
vo.setRowIndex(i + 1);
|
||||
vo.setSourceId(id);
|
||||
vo.setDisplayId(normalizeDisplayId(id));
|
||||
String rowBaseId = baseId(vo.getDisplayId());
|
||||
if (!Objects.equals(currentBlockBaseId, rowBaseId)) {
|
||||
currentBlockBaseId = rowBaseId;
|
||||
currentGroupKey = rowBaseId + "@" + vo.getRowIndex();
|
||||
}
|
||||
vo.setGroupKey(currentGroupKey);
|
||||
vo.setAsin(asin);
|
||||
vo.setCountry(country);
|
||||
vo.setUrl(urlCol >= 0 ? cell(row, urlCol, formatter) : "");
|
||||
@@ -752,6 +996,10 @@ public class AppearancePatentTaskService {
|
||||
dropped++;
|
||||
}
|
||||
}
|
||||
hydratePromptFields(allRows);
|
||||
if (accepted.isEmpty()) {
|
||||
throw new BusinessException("no valid appearance patent rows");
|
||||
}
|
||||
return new ParsedWorkbook(total, dropped, headers, accepted, allRows);
|
||||
} catch (BusinessException ex) {
|
||||
throw ex;
|
||||
@@ -761,6 +1009,33 @@ public class AppearancePatentTaskService {
|
||||
}
|
||||
}
|
||||
|
||||
private void hydratePromptFields(List<AppearancePatentParsedRowVo> rows) {
|
||||
if (rows == null || rows.isEmpty()) {
|
||||
return;
|
||||
}
|
||||
Map<String, List<AppearancePatentParsedRowVo>> rowsByBaseId = groupRowsByBaseId(rows);
|
||||
for (List<AppearancePatentParsedRowVo> siblings : rowsByBaseId.values()) {
|
||||
String title = "";
|
||||
String url = "";
|
||||
for (AppearancePatentParsedRowVo sibling : siblings) {
|
||||
title = firstNonBlank(title, sibling.getTitle());
|
||||
url = firstNonBlank(url, sibling.getUrl());
|
||||
}
|
||||
for (AppearancePatentParsedRowVo sibling : siblings) {
|
||||
if (normalize(sibling.getTitle()).isBlank()) {
|
||||
sibling.setTitle(title);
|
||||
}
|
||||
if (normalize(sibling.getUrl()).isBlank()) {
|
||||
sibling.setUrl(url);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private boolean hasPromptFields(AppearancePatentParsedRowVo row) {
|
||||
return row != null && (!normalize(row.getTitle()).isBlank() || !normalize(row.getUrl()).isBlank());
|
||||
}
|
||||
|
||||
private Map<String, Integer> buildHeaderMap(Row header, DataFormatter formatter) {
|
||||
Map<String, Integer> map = new LinkedHashMap<>();
|
||||
for (int i = 0; i < header.getLastCellNum(); i++) {
|
||||
@@ -808,6 +1083,23 @@ public class AppearancePatentTaskService {
|
||||
return -1;
|
||||
}
|
||||
|
||||
private int findOptionalHeaderExact(Map<String, Integer> map, String... names) {
|
||||
for (Map.Entry<String, Integer> entry : map.entrySet()) {
|
||||
String normalizedHeader = normalizeHeaderAlias(entry.getKey());
|
||||
for (String name : names) {
|
||||
if (normalizedHeader.equals(normalizeHeaderAlias(name))) {
|
||||
return entry.getValue();
|
||||
}
|
||||
}
|
||||
}
|
||||
return -1;
|
||||
}
|
||||
|
||||
private String normalizeHeaderAlias(String value) {
|
||||
String normalized = normalize(value).toLowerCase(Locale.ROOT);
|
||||
return normalized.replaceAll("[\\s_\\-()()\\[\\]{}::/\\\\]+", "");
|
||||
}
|
||||
|
||||
private String cell(Row row, int col, DataFormatter formatter) {
|
||||
return col < 0 ? "" : normalize(formatter.formatCellValue(row.getCell(col)));
|
||||
}
|
||||
@@ -933,6 +1225,28 @@ public class AppearancePatentTaskService {
|
||||
return new AppearancePatentParsedPayloadDto();
|
||||
}
|
||||
|
||||
private Map<String, AppearancePatentResultRowDto> readChunkRows(TaskChunkEntity chunk) {
|
||||
Map<String, AppearancePatentResultRowDto> rows = new LinkedHashMap<>();
|
||||
if (chunk == null || chunk.getPayloadJson() == null || chunk.getPayloadJson().isBlank()) {
|
||||
return rows;
|
||||
}
|
||||
try {
|
||||
String payloadJson = transientPayloadStorageService.resolvePayload(chunk.getPayloadJson(), "read appearance patent chunk failed");
|
||||
JsonNode array = objectMapper.readTree(payloadJson);
|
||||
if (!array.isArray()) {
|
||||
return rows;
|
||||
}
|
||||
for (JsonNode node : array) {
|
||||
AppearancePatentResultRowDto row = objectMapper.treeToValue(node, AppearancePatentResultRowDto.class);
|
||||
rows.put(rowKey(row.getId(), row.getAsin(), row.getCountry()), row);
|
||||
}
|
||||
} catch (Exception ex) {
|
||||
log.warn("[appearance-patent] read chunk payload failed taskId={} chunk={} err={}",
|
||||
chunk.getTaskId(), chunk.getChunkIndex(), ex.getMessage());
|
||||
}
|
||||
return rows;
|
||||
}
|
||||
|
||||
private String rowKey(String id, String asin, String country) {
|
||||
return normalize(id) + "::" + normalize(asin).toUpperCase(Locale.ROOT) + "::" + normalize(country);
|
||||
}
|
||||
@@ -996,6 +1310,13 @@ public class AppearancePatentTaskService {
|
||||
}
|
||||
}
|
||||
|
||||
private record SubmitContext(FileTaskEntity task,
|
||||
String scopeKey,
|
||||
String scopeHash,
|
||||
boolean forceFlush,
|
||||
String error) {
|
||||
}
|
||||
|
||||
private record ParsedWorkbook(int totalRows, int droppedRows, List<String> headers, List<AppearancePatentParsedRowVo> acceptedRows, List<AppearancePatentParsedRowVo> allRows) {
|
||||
}
|
||||
}
|
||||
|
||||
@@ -27,7 +27,6 @@ import java.nio.file.attribute.FileTime;
|
||||
import java.time.Duration;
|
||||
import java.time.Instant;
|
||||
import java.time.LocalDateTime;
|
||||
import java.time.ZoneId;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
@@ -47,6 +46,7 @@ public class DeleteBrandStaleTaskService {
|
||||
|
||||
private final FileTaskMapper fileTaskMapper;
|
||||
private final DeleteBrandTaskCacheService deleteBrandTaskCacheService;
|
||||
private final DeleteBrandTaskStorageService deleteBrandTaskStorageService;
|
||||
private final DeleteBrandRunService deleteBrandRunService;
|
||||
private final ProductRiskTaskService productRiskTaskService;
|
||||
private final ProductRiskTaskCacheService productRiskTaskCacheService;
|
||||
@@ -110,7 +110,13 @@ public class DeleteBrandStaleTaskService {
|
||||
}
|
||||
|
||||
private void failStaleDeleteBrandTasks() {
|
||||
LocalDateTime threshold = LocalDateTime.now().minusMinutes(deleteBrandProgressProperties.getHeartbeatTimeoutMinutes());
|
||||
long minutes = Math.max(1L, deleteBrandProgressProperties.getHeartbeatTimeoutMinutes());
|
||||
long initialMinutes = Math.max(minutes, deleteBrandProgressProperties.getDeleteBrandInitialTimeoutMinutes());
|
||||
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_DELETE_BRAND)
|
||||
@@ -120,36 +126,35 @@ public class DeleteBrandStaleTaskService {
|
||||
|
||||
for (FileTaskEntity task : runningTasks) {
|
||||
Map<Object, Object> progress = deleteBrandTaskCacheService.getProgress(task.getId());
|
||||
boolean hasProgress = progress != null && !progress.isEmpty() && progress.containsKey("last_heartbeat_at");
|
||||
|
||||
boolean isActive = false;
|
||||
if (progress != null && progress.containsKey("has_progress")) {
|
||||
Object hpObj = progress.get("has_progress");
|
||||
if (hpObj instanceof Boolean) {
|
||||
isActive = (Boolean) hpObj;
|
||||
} else if (hpObj instanceof String) {
|
||||
isActive = Boolean.parseBoolean((String) hpObj);
|
||||
}
|
||||
}
|
||||
|
||||
long lastHeartbeatAt = 0L;
|
||||
if (hasProgress) {
|
||||
if (progress != null && !progress.isEmpty() && progress.containsKey("last_heartbeat_at")) {
|
||||
try {
|
||||
lastHeartbeatAt = Long.parseLong(String.valueOf(progress.get("last_heartbeat_at")));
|
||||
} catch (Exception ignored) {
|
||||
}
|
||||
}
|
||||
boolean hasStartedProgress = lastHeartbeatAt > 0L;
|
||||
if (!hasStartedProgress) {
|
||||
hasStartedProgress = deleteBrandTaskStorageService.countCompletedScopes(task.getId()) > 0
|
||||
|| deleteBrandTaskStorageService.countParsedPayloadScopes(task.getId()) > 0;
|
||||
}
|
||||
|
||||
if (lastHeartbeatAt > 0 && isActive) {
|
||||
LocalDateTime lastActivityTime = LocalDateTime.ofInstant(Instant.ofEpochMilli(lastHeartbeatAt), ZoneId.systemDefault());
|
||||
if (lastActivityTime.isAfter(threshold)) {
|
||||
continue;
|
||||
}
|
||||
} else {
|
||||
LocalDateTime queuedThreshold = LocalDateTime.now().minusHours(12);
|
||||
if (task.getCreatedAt() != null && task.getCreatedAt().isAfter(queuedThreshold)) {
|
||||
if (lastHeartbeatAt > 0L && nowMillis - lastHeartbeatAt < staleTimeoutMillis) {
|
||||
continue;
|
||||
}
|
||||
if (!hasStartedProgress && task.getCreatedAt() != null && task.getCreatedAt().isAfter(initialThreshold)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
try {
|
||||
deleteBrandRunService.tryFinalizeTask(task.getId(), true);
|
||||
FileTaskEntity refreshed = fileTaskMapper.selectById(task.getId());
|
||||
if (refreshed != null && !"RUNNING".equals(refreshed.getStatus())) {
|
||||
deleteBrandTaskCacheService.saveTaskCache(refreshed);
|
||||
continue;
|
||||
}
|
||||
} catch (Exception ex) {
|
||||
log.warn("[stale-check] delete-brand finalize threw taskId={} msg={}", task.getId(), ex.getMessage());
|
||||
}
|
||||
|
||||
int updated = fileTaskMapper.update(null, new LambdaUpdateWrapper<FileTaskEntity>()
|
||||
|
||||
@@ -49,6 +49,9 @@ public class PatrolDeleteTaskService {
|
||||
|
||||
private static final String MODULE_TYPE = "PATROL_DELETE";
|
||||
private static final String CONTENT_TYPE_XLSX = "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet";
|
||||
private static final int RESULT_PENDING = -1;
|
||||
private static final int RESULT_FAILED = 0;
|
||||
private static final int RESULT_SUCCESS = 1;
|
||||
private static final String INTERRUPTED_MESSAGE = "Python 在该店铺结果提交完成前中断";
|
||||
|
||||
private final FileTaskMapper fileTaskMapper;
|
||||
@@ -264,12 +267,13 @@ public class PatrolDeleteTaskService {
|
||||
result.setSourceFilename(normalizedShopName);
|
||||
result.setSourceFileUrl(item.getShopId());
|
||||
result.setUserId(request.getUserId());
|
||||
result.setSuccess(null);
|
||||
result.setSuccess(RESULT_PENDING);
|
||||
result.setCreatedAt(now);
|
||||
fileResultMapper.insert(result);
|
||||
snapshots.add(toSnapshotVo(result, item, task.getStatus(), null));
|
||||
}
|
||||
persistTaskJson(task, uniqueItems, snapshots);
|
||||
taskCacheService.saveTaskCache(task);
|
||||
|
||||
PatrolDeleteCreateTaskVo vo = new PatrolDeleteCreateTaskVo();
|
||||
vo.setTaskId(task.getId());
|
||||
@@ -326,6 +330,7 @@ public class PatrolDeleteTaskService {
|
||||
|
||||
persistSnapshotJson(task, snapshots);
|
||||
fileTaskMapper.updateById(task);
|
||||
taskCacheService.saveTaskCache(task);
|
||||
tryFinalizeTask(taskId, false);
|
||||
}
|
||||
|
||||
@@ -402,6 +407,7 @@ public class PatrolDeleteTaskService {
|
||||
|
||||
persistSnapshotJson(task, snapshots);
|
||||
fileTaskMapper.updateById(task);
|
||||
taskCacheService.saveTaskCache(task);
|
||||
return changed;
|
||||
}
|
||||
|
||||
@@ -448,6 +454,7 @@ public class PatrolDeleteTaskService {
|
||||
updateTaskStatusFromRows(task, rows);
|
||||
persistSnapshotJson(task, buildSnapshotFromDb(task, rows));
|
||||
fileTaskMapper.updateById(task);
|
||||
taskCacheService.saveTaskCache(task);
|
||||
}
|
||||
|
||||
private long countTasks(Long userId, List<String> statuses) {
|
||||
@@ -502,9 +509,9 @@ public class PatrolDeleteTaskService {
|
||||
private Map<Long, Map<Long, PatrolDeleteResultItemVo>> buildSnapshotMap(Map<Long, FileTaskEntity> taskMap) {
|
||||
Map<Long, Map<Long, PatrolDeleteResultItemVo>> out = new LinkedHashMap<>();
|
||||
for (Map.Entry<Long, FileTaskEntity> entry : taskMap.entrySet()) {
|
||||
List<PatrolDeleteResultItemVo> snapshots = taskResultItemService.listResultSnapshots(entry.getKey(), MODULE_TYPE, PatrolDeleteResultItemVo.class);
|
||||
List<PatrolDeleteResultItemVo> snapshots = parseTaskSnapshots(entry.getValue().getResultJson());
|
||||
if (snapshots.isEmpty()) {
|
||||
snapshots = parseTaskSnapshots(entry.getValue().getResultJson());
|
||||
snapshots = taskResultItemService.listResultSnapshots(entry.getKey(), MODULE_TYPE, PatrolDeleteResultItemVo.class);
|
||||
}
|
||||
out.put(entry.getKey(), indexSnapshotByResultId(snapshots));
|
||||
}
|
||||
@@ -518,7 +525,7 @@ public class PatrolDeleteTaskService {
|
||||
item.setShopName(firstNonBlank(item.getShopName(), entity.getSourceFilename()));
|
||||
item.setShopId(firstNonBlank(item.getShopId(), entity.getSourceFileUrl()));
|
||||
item.setTaskStatus(task != null ? task.getStatus() : item.getTaskStatus());
|
||||
item.setSuccess(entity.getSuccess() == null ? item.getSuccess() : entity.getSuccess() == 1);
|
||||
item.setSuccess(toSuccessFlag(entity.getSuccess(), item.getSuccess()));
|
||||
item.setError(!blank(entity.getErrorMessage()) ? entity.getErrorMessage() : item.getError());
|
||||
item.setCreatedAt(entity.getCreatedAt());
|
||||
item.setFinishedAt(task != null ? task.getFinishedAt() : item.getFinishedAt());
|
||||
@@ -575,7 +582,7 @@ public class PatrolDeleteTaskService {
|
||||
vo.setMatchStatus(item.getMatchStatus());
|
||||
vo.setMatchMessage(item.getMatchMessage());
|
||||
vo.setTaskStatus(taskStatus);
|
||||
vo.setSuccess(result.getSuccess() == null ? null : result.getSuccess() == 1);
|
||||
vo.setSuccess(toSuccessFlag(result.getSuccess(), null));
|
||||
vo.setError(result.getErrorMessage());
|
||||
vo.setCreatedAt(result.getCreatedAt());
|
||||
vo.setFinishedAt(finishedAt);
|
||||
@@ -616,7 +623,7 @@ public class PatrolDeleteTaskService {
|
||||
item.setShopName(row.getSourceFilename());
|
||||
item.setShopId(row.getSourceFileUrl());
|
||||
item.setTaskStatus(task == null ? null : task.getStatus());
|
||||
item.setSuccess(row.getSuccess() == null ? null : row.getSuccess() == 1);
|
||||
item.setSuccess(toSuccessFlag(row.getSuccess(), null));
|
||||
item.setError(row.getErrorMessage());
|
||||
item.setCreatedAt(row.getCreatedAt());
|
||||
item.setFinishedAt(task == null ? null : task.getFinishedAt());
|
||||
@@ -642,8 +649,8 @@ public class PatrolDeleteTaskService {
|
||||
}
|
||||
|
||||
private void updateTaskStatusFromRows(FileTaskEntity task, List<FileResultEntity> rows) {
|
||||
long successCount = rows.stream().filter(row -> Integer.valueOf(1).equals(row.getSuccess())).count();
|
||||
long failedCount = rows.stream().filter(row -> Integer.valueOf(0).equals(row.getSuccess())).count();
|
||||
long successCount = rows.stream().filter(row -> Integer.valueOf(RESULT_SUCCESS).equals(row.getSuccess())).count();
|
||||
long failedCount = rows.stream().filter(row -> Integer.valueOf(RESULT_FAILED).equals(row.getSuccess())).count();
|
||||
long pendingCount = rows.stream().filter(row -> !isResultFinished(row)).count();
|
||||
task.setSuccessFileCount((int) successCount);
|
||||
task.setFailedFileCount((int) failedCount);
|
||||
@@ -796,11 +803,11 @@ public class PatrolDeleteTaskService {
|
||||
.filter(item -> Boolean.TRUE.equals(item.getSuccess()))
|
||||
.toList();
|
||||
if (!successItems.isEmpty()) {
|
||||
String filename = safeFileStem("patrol-delete-" + task.getId()) + ".xlsx";
|
||||
String filename = buildTaskWorkbookFilename(task);
|
||||
int rowCount = excelAssemblyService.countRows(successItems);
|
||||
FileResultEntity firstSuccessRow = null;
|
||||
for (FileResultEntity row : rows) {
|
||||
if (Integer.valueOf(1).equals(row.getSuccess())) {
|
||||
if (Integer.valueOf(RESULT_SUCCESS).equals(row.getSuccess())) {
|
||||
row.setResultFilename(filename);
|
||||
row.setResultFileUrl(null);
|
||||
row.setResultFileSize(0L);
|
||||
@@ -843,7 +850,7 @@ public class PatrolDeleteTaskService {
|
||||
throw new BusinessException("没有可生成的巡店删除结果");
|
||||
}
|
||||
File workRoot = FileUtil.mkdir(FileUtil.file(System.getProperty("java.io.tmpdir"), "patrol-delete-result", String.valueOf(task.getId())));
|
||||
String filename = safeFileStem("patrol-delete-" + task.getId()) + ".xlsx";
|
||||
String filename = buildTaskWorkbookFilename(task);
|
||||
File xlsx = FileUtil.file(workRoot, filename);
|
||||
try {
|
||||
excelAssemblyService.writeWorkbook(xlsx, successItems);
|
||||
@@ -851,7 +858,7 @@ public class PatrolDeleteTaskService {
|
||||
long fileSize = xlsx.length();
|
||||
int rowCount = excelAssemblyService.countRows(successItems);
|
||||
for (FileResultEntity row : rows) {
|
||||
if (Integer.valueOf(1).equals(row.getSuccess())) {
|
||||
if (Integer.valueOf(RESULT_SUCCESS).equals(row.getSuccess())) {
|
||||
row.setResultFilename(filename);
|
||||
row.setResultFileUrl(objectKey);
|
||||
row.setResultFileSize(fileSize);
|
||||
@@ -866,13 +873,13 @@ public class PatrolDeleteTaskService {
|
||||
}
|
||||
|
||||
private void markResultSuccess(FileResultEntity row) {
|
||||
row.setSuccess(1);
|
||||
row.setSuccess(RESULT_SUCCESS);
|
||||
row.setErrorMessage(null);
|
||||
fileResultMapper.updateById(row);
|
||||
}
|
||||
|
||||
private void markResultFailed(FileResultEntity row, String message) {
|
||||
row.setSuccess(0);
|
||||
row.setSuccess(RESULT_FAILED);
|
||||
row.setErrorMessage(blankToNull(message));
|
||||
row.setResultFilename(null);
|
||||
row.setResultFileUrl(null);
|
||||
@@ -883,7 +890,14 @@ public class PatrolDeleteTaskService {
|
||||
}
|
||||
|
||||
private boolean isResultFinished(FileResultEntity row) {
|
||||
return Integer.valueOf(1).equals(row.getSuccess()) || Integer.valueOf(0).equals(row.getSuccess());
|
||||
return Integer.valueOf(RESULT_SUCCESS).equals(row.getSuccess()) || Integer.valueOf(RESULT_FAILED).equals(row.getSuccess());
|
||||
}
|
||||
|
||||
private Boolean toSuccessFlag(Integer dbValue, Boolean fallback) {
|
||||
if (dbValue == null || Integer.valueOf(RESULT_PENDING).equals(dbValue)) {
|
||||
return fallback;
|
||||
}
|
||||
return Integer.valueOf(RESULT_SUCCESS).equals(dbValue);
|
||||
}
|
||||
|
||||
private List<PatrolDeleteResultItemVo> parseTaskSnapshots(String json) {
|
||||
@@ -972,6 +986,11 @@ public class PatrolDeleteTaskService {
|
||||
return !blank(first) ? first : second;
|
||||
}
|
||||
|
||||
private String buildTaskWorkbookFilename(FileTaskEntity task) {
|
||||
Long taskId = task == null ? null : task.getId();
|
||||
return safeFileStem("巡店删除-" + (taskId == null ? "result" : taskId)) + ".xlsx";
|
||||
}
|
||||
|
||||
private String safeFileStem(String value) {
|
||||
String raw = value == null ? "result" : value.trim();
|
||||
String safe = raw.replaceAll("[\\\\/:*?\"<>|]+", "_");
|
||||
|
||||
@@ -49,6 +49,9 @@ public class QueryAsinTaskService {
|
||||
|
||||
private static final String MODULE_TYPE = "QUERY_ASIN";
|
||||
private static final String CONTENT_TYPE_XLSX = "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet";
|
||||
private static final int RESULT_PENDING = -1;
|
||||
private static final int RESULT_FAILED = 0;
|
||||
private static final int RESULT_SUCCESS = 1;
|
||||
private static final String INTERRUPTED_MESSAGE = "Python 在该店铺结果提交完成前中断";
|
||||
|
||||
private final FileTaskMapper fileTaskMapper;
|
||||
@@ -270,12 +273,13 @@ public class QueryAsinTaskService {
|
||||
result.setSourceFilename(normalizedShopName);
|
||||
result.setSourceFileUrl(item.getShopId());
|
||||
result.setUserId(request.getUserId());
|
||||
result.setSuccess(null);
|
||||
result.setSuccess(RESULT_PENDING);
|
||||
result.setCreatedAt(now);
|
||||
fileResultMapper.insert(result);
|
||||
snapshots.add(toSnapshotVo(result, item, task.getStatus(), null));
|
||||
}
|
||||
persistTaskJson(task, uniqueItems, snapshots);
|
||||
taskCacheService.saveTaskCache(task);
|
||||
|
||||
QueryAsinCreateTaskVo vo = new QueryAsinCreateTaskVo();
|
||||
vo.setTaskId(task.getId());
|
||||
@@ -332,6 +336,7 @@ public class QueryAsinTaskService {
|
||||
|
||||
persistSnapshotJson(task, snapshots);
|
||||
fileTaskMapper.updateById(task);
|
||||
taskCacheService.saveTaskCache(task);
|
||||
tryFinalizeTask(taskId, false);
|
||||
}
|
||||
|
||||
@@ -408,6 +413,7 @@ public class QueryAsinTaskService {
|
||||
|
||||
persistSnapshotJson(task, snapshots);
|
||||
fileTaskMapper.updateById(task);
|
||||
taskCacheService.saveTaskCache(task);
|
||||
return changed;
|
||||
}
|
||||
|
||||
@@ -454,6 +460,7 @@ public class QueryAsinTaskService {
|
||||
updateTaskStatusFromRows(task, rows);
|
||||
persistSnapshotJson(task, buildSnapshotFromDb(task, rows));
|
||||
fileTaskMapper.updateById(task);
|
||||
taskCacheService.saveTaskCache(task);
|
||||
}
|
||||
|
||||
private long countTasks(Long userId, List<String> statuses) {
|
||||
@@ -508,9 +515,9 @@ public class QueryAsinTaskService {
|
||||
private Map<Long, Map<Long, QueryAsinResultItemVo>> buildSnapshotMap(Map<Long, FileTaskEntity> taskMap) {
|
||||
Map<Long, Map<Long, QueryAsinResultItemVo>> out = new LinkedHashMap<>();
|
||||
for (Map.Entry<Long, FileTaskEntity> entry : taskMap.entrySet()) {
|
||||
List<QueryAsinResultItemVo> snapshots = taskResultItemService.listResultSnapshots(entry.getKey(), MODULE_TYPE, QueryAsinResultItemVo.class);
|
||||
List<QueryAsinResultItemVo> snapshots = parseTaskSnapshots(entry.getValue().getResultJson());
|
||||
if (snapshots.isEmpty()) {
|
||||
snapshots = parseTaskSnapshots(entry.getValue().getResultJson());
|
||||
snapshots = taskResultItemService.listResultSnapshots(entry.getKey(), MODULE_TYPE, QueryAsinResultItemVo.class);
|
||||
}
|
||||
out.put(entry.getKey(), indexSnapshotByResultId(snapshots));
|
||||
}
|
||||
@@ -524,7 +531,7 @@ public class QueryAsinTaskService {
|
||||
item.setShopName(firstNonBlank(item.getShopName(), entity.getSourceFilename()));
|
||||
item.setShopId(firstNonBlank(item.getShopId(), entity.getSourceFileUrl()));
|
||||
item.setTaskStatus(task != null ? task.getStatus() : item.getTaskStatus());
|
||||
item.setSuccess(entity.getSuccess() == null ? item.getSuccess() : entity.getSuccess() == 1);
|
||||
item.setSuccess(toSuccessFlag(entity.getSuccess(), item.getSuccess()));
|
||||
item.setError(!blank(entity.getErrorMessage()) ? entity.getErrorMessage() : item.getError());
|
||||
item.setCreatedAt(entity.getCreatedAt());
|
||||
item.setFinishedAt(task != null ? task.getFinishedAt() : item.getFinishedAt());
|
||||
@@ -581,7 +588,7 @@ public class QueryAsinTaskService {
|
||||
vo.setMatchStatus(item.getMatchStatus());
|
||||
vo.setMatchMessage(item.getMatchMessage());
|
||||
vo.setTaskStatus(taskStatus);
|
||||
vo.setSuccess(result.getSuccess() == null ? null : result.getSuccess() == 1);
|
||||
vo.setSuccess(toSuccessFlag(result.getSuccess(), null));
|
||||
vo.setError(result.getErrorMessage());
|
||||
vo.setCreatedAt(result.getCreatedAt());
|
||||
vo.setFinishedAt(finishedAt);
|
||||
@@ -622,7 +629,7 @@ public class QueryAsinTaskService {
|
||||
item.setShopName(row.getSourceFilename());
|
||||
item.setShopId(row.getSourceFileUrl());
|
||||
item.setTaskStatus(task == null ? null : task.getStatus());
|
||||
item.setSuccess(row.getSuccess() == null ? null : row.getSuccess() == 1);
|
||||
item.setSuccess(toSuccessFlag(row.getSuccess(), null));
|
||||
item.setError(row.getErrorMessage());
|
||||
item.setCreatedAt(row.getCreatedAt());
|
||||
item.setFinishedAt(task == null ? null : task.getFinishedAt());
|
||||
@@ -648,8 +655,8 @@ public class QueryAsinTaskService {
|
||||
}
|
||||
|
||||
private void updateTaskStatusFromRows(FileTaskEntity task, List<FileResultEntity> rows) {
|
||||
long successCount = rows.stream().filter(row -> Integer.valueOf(1).equals(row.getSuccess())).count();
|
||||
long failedCount = rows.stream().filter(row -> Integer.valueOf(0).equals(row.getSuccess())).count();
|
||||
long successCount = rows.stream().filter(row -> Integer.valueOf(RESULT_SUCCESS).equals(row.getSuccess())).count();
|
||||
long failedCount = rows.stream().filter(row -> Integer.valueOf(RESULT_FAILED).equals(row.getSuccess())).count();
|
||||
long pendingCount = rows.stream().filter(row -> !isResultFinished(row)).count();
|
||||
task.setSuccessFileCount((int) successCount);
|
||||
task.setFailedFileCount((int) failedCount);
|
||||
@@ -816,11 +823,11 @@ public class QueryAsinTaskService {
|
||||
.filter(item -> Boolean.TRUE.equals(item.getSuccess()))
|
||||
.toList();
|
||||
if (!successItems.isEmpty()) {
|
||||
String filename = safeFileStem("query-asin-" + task.getId()) + ".xlsx";
|
||||
String filename = buildTaskWorkbookFilename(task);
|
||||
int rowCount = excelAssemblyService.countRows(successItems);
|
||||
FileResultEntity firstSuccessRow = null;
|
||||
for (FileResultEntity row : rows) {
|
||||
if (Integer.valueOf(1).equals(row.getSuccess())) {
|
||||
if (Integer.valueOf(RESULT_SUCCESS).equals(row.getSuccess())) {
|
||||
row.setResultFilename(filename);
|
||||
row.setResultFileUrl(null);
|
||||
row.setResultFileSize(0L);
|
||||
@@ -863,7 +870,7 @@ public class QueryAsinTaskService {
|
||||
throw new BusinessException("没有可生成的查询ASIN结果");
|
||||
}
|
||||
File workRoot = FileUtil.mkdir(FileUtil.file(System.getProperty("java.io.tmpdir"), "query-asin-result", String.valueOf(task.getId())));
|
||||
String filename = safeFileStem("query-asin-" + task.getId()) + ".xlsx";
|
||||
String filename = buildTaskWorkbookFilename(task);
|
||||
File xlsx = FileUtil.file(workRoot, filename);
|
||||
try {
|
||||
excelAssemblyService.writeWorkbook(xlsx, successItems);
|
||||
@@ -871,7 +878,7 @@ public class QueryAsinTaskService {
|
||||
long fileSize = xlsx.length();
|
||||
int rowCount = excelAssemblyService.countRows(successItems);
|
||||
for (FileResultEntity row : rows) {
|
||||
if (Integer.valueOf(1).equals(row.getSuccess())) {
|
||||
if (Integer.valueOf(RESULT_SUCCESS).equals(row.getSuccess())) {
|
||||
row.setResultFilename(filename);
|
||||
row.setResultFileUrl(objectKey);
|
||||
row.setResultFileSize(fileSize);
|
||||
@@ -886,13 +893,13 @@ public class QueryAsinTaskService {
|
||||
}
|
||||
|
||||
private void markResultSuccess(FileResultEntity row) {
|
||||
row.setSuccess(1);
|
||||
row.setSuccess(RESULT_SUCCESS);
|
||||
row.setErrorMessage(null);
|
||||
fileResultMapper.updateById(row);
|
||||
}
|
||||
|
||||
private void markResultFailed(FileResultEntity row, String message) {
|
||||
row.setSuccess(0);
|
||||
row.setSuccess(RESULT_FAILED);
|
||||
row.setErrorMessage(blankToNull(message));
|
||||
row.setResultFilename(null);
|
||||
row.setResultFileUrl(null);
|
||||
@@ -903,7 +910,14 @@ public class QueryAsinTaskService {
|
||||
}
|
||||
|
||||
private boolean isResultFinished(FileResultEntity row) {
|
||||
return Integer.valueOf(1).equals(row.getSuccess()) || Integer.valueOf(0).equals(row.getSuccess());
|
||||
return Integer.valueOf(RESULT_SUCCESS).equals(row.getSuccess()) || Integer.valueOf(RESULT_FAILED).equals(row.getSuccess());
|
||||
}
|
||||
|
||||
private Boolean toSuccessFlag(Integer dbValue, Boolean fallback) {
|
||||
if (dbValue == null || Integer.valueOf(RESULT_PENDING).equals(dbValue)) {
|
||||
return fallback;
|
||||
}
|
||||
return Integer.valueOf(RESULT_SUCCESS).equals(dbValue);
|
||||
}
|
||||
|
||||
private List<QueryAsinResultItemVo> parseTaskSnapshots(String json) {
|
||||
@@ -1034,6 +1048,11 @@ public class QueryAsinTaskService {
|
||||
return asin == null ? "" : asin.trim().toUpperCase();
|
||||
}
|
||||
|
||||
private String buildTaskWorkbookFilename(FileTaskEntity task) {
|
||||
Long taskId = task == null ? null : task.getId();
|
||||
return safeFileStem("查询ASIN-" + (taskId == null ? "result" : taskId)) + ".xlsx";
|
||||
}
|
||||
|
||||
private String safeFileStem(String value) {
|
||||
String raw = value == null ? "result" : value.trim();
|
||||
String safe = raw.replaceAll("[\\\\/:*?\"<>|]+", "_");
|
||||
|
||||
@@ -15,6 +15,7 @@ import com.nanri.aiimage.modules.shopkey.model.vo.QueryAsinItemVo;
|
||||
import com.nanri.aiimage.modules.shopkey.model.vo.QueryAsinPageVo;
|
||||
import com.nanri.aiimage.modules.shopkey.model.vo.ShopManageGroupItemVo;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.stereotype.Service;
|
||||
import org.springframework.transaction.annotation.Transactional;
|
||||
import org.springframework.web.multipart.MultipartFile;
|
||||
@@ -30,9 +31,11 @@ import java.util.Locale;
|
||||
import java.util.Map;
|
||||
import java.util.Set;
|
||||
import java.util.concurrent.ConcurrentHashMap;
|
||||
import java.util.function.Predicate;
|
||||
|
||||
@Service
|
||||
@RequiredArgsConstructor
|
||||
@Slf4j
|
||||
public class QueryAsinService {
|
||||
|
||||
private static final List<String> SUPPORTED_COUNTRIES = List.of("DE", "UK", "FR", "IT", "ES");
|
||||
@@ -364,13 +367,32 @@ public class QueryAsinService {
|
||||
}
|
||||
|
||||
private String normalizeCountry(String country) {
|
||||
String normalized = normalizeBlank(country).toUpperCase(Locale.ROOT);
|
||||
String normalized = normalizeCountryAlias(country);
|
||||
if (!SUPPORTED_COUNTRIES.contains(normalized)) {
|
||||
throw new BusinessException("国家参数不支持: " + country);
|
||||
}
|
||||
return normalized;
|
||||
}
|
||||
|
||||
private String normalizeCountryAlias(String country) {
|
||||
String normalized = normalizeBlank(country);
|
||||
if (normalized.isEmpty()) {
|
||||
return "";
|
||||
}
|
||||
String upper = normalized.toUpperCase(Locale.ROOT);
|
||||
if (SUPPORTED_COUNTRIES.contains(upper)) {
|
||||
return upper;
|
||||
}
|
||||
return switch (normalizeHeaderKey(normalized)) {
|
||||
case "\u5fb7\u56fd", "\u5fb7", "de", "germany", "german", "deutschland" -> "DE";
|
||||
case "\u82f1\u56fd", "\u82f1", "uk", "gb", "greatbritain", "britain", "unitedkingdom" -> "UK";
|
||||
case "\u6cd5\u56fd", "\u6cd5", "fr", "france", "french" -> "FR";
|
||||
case "\u610f\u5927\u5229", "\u610f", "it", "italy", "italian" -> "IT";
|
||||
case "\u897f\u73ed\u7259", "\u897f", "es", "spain", "spanish", "espana" -> "ES";
|
||||
default -> "";
|
||||
};
|
||||
}
|
||||
|
||||
private String normalizeAsin(String asin) {
|
||||
String normalized = normalizeRequired(asin, "ASIN 不能为空").toUpperCase(Locale.ROOT);
|
||||
if (normalized.length() > 64) {
|
||||
@@ -505,13 +527,19 @@ public class QueryAsinService {
|
||||
}
|
||||
}
|
||||
headerLoaded = true;
|
||||
if (findGroupIndex() == null && (fallbackGroupId == null || fallbackGroupId <= 0)) {
|
||||
Integer resolvedGroupIndex = resolveGroupHeaderIndex();
|
||||
Integer resolvedShopIndex = resolveShopHeaderIndex();
|
||||
boolean hasWideCountries = hasWideCountryColumns();
|
||||
boolean hasSingleCountryAsin = hasSingleCountryAsinColumns();
|
||||
log.info("[query-asin-import] headerMap={} normalizedKeys={} groupIndex={} shopIndex={} hasWideCountries={} hasSingleCountryAsin={}",
|
||||
headerMap, headerIndexByKey.keySet(), resolvedGroupIndex, resolvedShopIndex, hasWideCountries, hasSingleCountryAsin);
|
||||
if (resolvedGroupIndex == null && (fallbackGroupId == null || fallbackGroupId <= 0)) {
|
||||
throw new BusinessException("Excel 缺少分组列,请在页面选择分组或提供分组列");
|
||||
}
|
||||
if (findShopIndex() == null) {
|
||||
if (resolvedShopIndex == null) {
|
||||
throw new BusinessException("Excel 缺少店铺名列");
|
||||
}
|
||||
if (SUPPORTED_COUNTRIES.stream().noneMatch(country -> findCountryIndex(country) != null)) {
|
||||
if (!hasWideCountries && !hasSingleCountryAsin) {
|
||||
throw new BusinessException("Excel 至少需要包含一个国家 ASIN 列");
|
||||
}
|
||||
}
|
||||
@@ -524,7 +552,7 @@ public class QueryAsinService {
|
||||
progress.setProcessedRows(rowIndex);
|
||||
|
||||
Long groupId = resolveGroupId(rowMap);
|
||||
String shopName = cell(rowMap, findShopIndex());
|
||||
String shopName = cell(rowMap, resolveShopHeaderIndex());
|
||||
if (groupId == null || shopName.isEmpty()) {
|
||||
skippedCount++;
|
||||
updateProgress();
|
||||
@@ -532,27 +560,8 @@ public class QueryAsinService {
|
||||
}
|
||||
|
||||
QueryAsinEntity row = newImportEntity(groupId, shopName);
|
||||
int rowAsinCount = 0;
|
||||
int rowAcceptedCount = 0;
|
||||
for (String country : SUPPORTED_COUNTRIES) {
|
||||
Integer index = findCountryIndex(country);
|
||||
if (index == null) {
|
||||
continue;
|
||||
}
|
||||
String asin = normalizeBlank(cell(rowMap, index)).toUpperCase(Locale.ROOT);
|
||||
if (asin.isEmpty()) {
|
||||
continue;
|
||||
}
|
||||
if (asin.length() > 64) {
|
||||
skippedCount++;
|
||||
continue;
|
||||
}
|
||||
asinCount++;
|
||||
rowAsinCount++;
|
||||
setCountryAsin(row, country, asin);
|
||||
rowAcceptedCount++;
|
||||
}
|
||||
if (rowAsinCount == 0) {
|
||||
int rowAcceptedCount = collectRowCountryAsins(row, rowMap);
|
||||
if (countCountryCells(row) == 0) {
|
||||
skippedCount++;
|
||||
}
|
||||
if (rowAcceptedCount > 0) {
|
||||
@@ -754,7 +763,7 @@ public class QueryAsinService {
|
||||
}
|
||||
|
||||
private Long resolveGroupId(Map<Integer, String> rowMap) {
|
||||
Integer groupIndex = findGroupIndex();
|
||||
Integer groupIndex = resolveGroupHeaderIndex();
|
||||
String groupText = cell(rowMap, groupIndex);
|
||||
if (groupText.isEmpty()) {
|
||||
return fallbackGroupId != null && fallbackGroupId > 0 ? fallbackGroupId : null;
|
||||
@@ -767,6 +776,51 @@ public class QueryAsinService {
|
||||
return id;
|
||||
}
|
||||
|
||||
private int collectRowCountryAsins(QueryAsinEntity row, Map<Integer, String> rowMap) {
|
||||
int acceptedCount = 0;
|
||||
for (String country : SUPPORTED_COUNTRIES) {
|
||||
Integer index = resolveCountryHeaderIndex(country);
|
||||
if (index == null) {
|
||||
continue;
|
||||
}
|
||||
acceptedCount += acceptCountryAsin(row, country, cell(rowMap, index));
|
||||
}
|
||||
Integer singleCountryIndex = findSingleCountryIndex();
|
||||
Integer singleAsinIndex = findSingleAsinIndex();
|
||||
if (singleCountryIndex != null && singleAsinIndex != null) {
|
||||
acceptedCount += acceptCountryAsin(
|
||||
row,
|
||||
normalizeCountryAlias(cell(rowMap, singleCountryIndex)),
|
||||
cell(rowMap, singleAsinIndex));
|
||||
}
|
||||
return acceptedCount;
|
||||
}
|
||||
|
||||
private int acceptCountryAsin(QueryAsinEntity row, String country, String asin) {
|
||||
String normalizedAsin = normalizeBlank(asin).toUpperCase(Locale.ROOT);
|
||||
if (normalizedAsin.isEmpty()) {
|
||||
return 0;
|
||||
}
|
||||
asinCount++;
|
||||
if (country == null || country.isEmpty() || !SUPPORTED_COUNTRIES.contains(country)) {
|
||||
skippedCount++;
|
||||
return 0;
|
||||
}
|
||||
if (normalizedAsin.length() > 64) {
|
||||
skippedCount++;
|
||||
return 0;
|
||||
}
|
||||
String existing = getCountryAsin(row, country);
|
||||
if (!existing.isEmpty()) {
|
||||
if (!existing.equals(normalizedAsin)) {
|
||||
skippedCount++;
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
setCountryAsin(row, country, normalizedAsin);
|
||||
return 1;
|
||||
}
|
||||
|
||||
private void loadGroups() {
|
||||
if (groupsLoaded) {
|
||||
return;
|
||||
@@ -801,6 +855,88 @@ public class QueryAsinService {
|
||||
};
|
||||
}
|
||||
|
||||
private Integer resolveGroupHeaderIndex() {
|
||||
Integer index = findGroupIndex();
|
||||
if (index != null) {
|
||||
return index;
|
||||
}
|
||||
index = firstHeader("groupname", "\u5206\u7ec4id", "\u5206\u7ec4");
|
||||
if (index != null) {
|
||||
return index;
|
||||
}
|
||||
return findHeaderByPredicate(key -> key.contains("\u5206\u7ec4") || key.contains("group"));
|
||||
}
|
||||
|
||||
private Integer resolveShopHeaderIndex() {
|
||||
Integer index = findShopIndex();
|
||||
if (index != null) {
|
||||
return index;
|
||||
}
|
||||
index = firstHeader(
|
||||
"\u5e97\u94fa\u540d", "\u5e97\u94fa\u540d\u79f0", "\u5e97\u94fa\u8d26\u53f7", "\u8d26\u53f7",
|
||||
"account", "selleraccount", "storeaccount", "storename");
|
||||
if (index != null) {
|
||||
return index;
|
||||
}
|
||||
return findHeaderByPredicate(key ->
|
||||
(key.contains("\u5e97\u94fa") && (key.contains("\u540d") || key.contains("\u8d26\u53f7")))
|
||||
|| (key.contains("shop") && (key.contains("name") || key.contains("account")))
|
||||
|| key.contains("selleraccount")
|
||||
|| key.contains("storeaccount"));
|
||||
}
|
||||
|
||||
private Integer resolveCountryHeaderIndex(String country) {
|
||||
Integer index = findCountryIndex(country);
|
||||
if (index != null) {
|
||||
return index;
|
||||
}
|
||||
return switch (country) {
|
||||
case "DE" -> firstHeader("\u5fb7\u56fd", "\u5fb7", "\u5fb7\u56fdasin");
|
||||
case "UK" -> firstHeader("\u82f1\u56fd", "\u82f1", "\u82f1\u56fdasin");
|
||||
case "FR" -> firstHeader("\u6cd5\u56fd", "\u6cd5", "\u6cd5\u56fdasin");
|
||||
case "IT" -> firstHeader("\u610f\u5927\u5229", "\u610f", "\u610f\u5927\u5229asin");
|
||||
case "ES" -> firstHeader("\u897f\u73ed\u7259", "\u897f", "\u897f\u73ed\u7259asin");
|
||||
default -> null;
|
||||
};
|
||||
}
|
||||
|
||||
private Integer findSingleCountryIndex() {
|
||||
Integer index = firstHeader(
|
||||
"\u4e2d\u6587\u56fd\u5bb6\u540d", "\u56fd\u5bb6", "\u56fd\u5bb6\u540d", "\u56fd\u5bb6\u7ad9\u70b9",
|
||||
"\u7ad9\u70b9", "country", "countryname", "marketplace", "site");
|
||||
if (index != null) {
|
||||
return index;
|
||||
}
|
||||
return findHeaderByPredicate(key ->
|
||||
key.contains("\u56fd\u5bb6")
|
||||
|| key.contains("\u7ad9\u70b9")
|
||||
|| key.contains("country")
|
||||
|| key.contains("marketplace")
|
||||
|| key.equals("site"));
|
||||
}
|
||||
|
||||
private Integer findSingleAsinIndex() {
|
||||
return firstHeader("asin", "\u67e5\u8be2asin", "\u4ea7\u54c1asin", "\u94fe\u63a5asin");
|
||||
}
|
||||
|
||||
private boolean hasWideCountryColumns() {
|
||||
return SUPPORTED_COUNTRIES.stream().anyMatch(country -> resolveCountryHeaderIndex(country) != null);
|
||||
}
|
||||
|
||||
private boolean hasSingleCountryAsinColumns() {
|
||||
return findSingleCountryIndex() != null && findSingleAsinIndex() != null;
|
||||
}
|
||||
|
||||
private Integer findHeaderByPredicate(Predicate<String> predicate) {
|
||||
for (Map.Entry<String, Integer> entry : headerIndexByKey.entrySet()) {
|
||||
String key = entry.getKey();
|
||||
if (key != null && predicate.test(key)) {
|
||||
return entry.getValue();
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
private Integer firstHeader(String... names) {
|
||||
for (String name : names) {
|
||||
Integer index = headerIndexByKey.get(normalizeHeaderKey(name));
|
||||
|
||||
@@ -125,15 +125,18 @@ public class TaskResultItemService {
|
||||
String status,
|
||||
Object payload) {
|
||||
String payloadJson = writeJson(payload);
|
||||
String payloadHash = hash(payloadJson);
|
||||
String normalizedScopeKey = firstNonBlank(scopeKey, "task:" + taskId).trim();
|
||||
String normalizedItemKey = firstNonBlank(itemKey, "result:" + resultId).trim();
|
||||
String scopeHash = hash(normalizedScopeKey);
|
||||
String storedPayload = transientPayloadStorageService.storeResultItemPayload(
|
||||
moduleType, taskId, scopeHash, normalizedItemKey, payloadJson);
|
||||
String payloadHash = hash(payloadJson);
|
||||
LocalDateTime now = LocalDateTime.now();
|
||||
|
||||
TaskResultItemEntity existing = find(taskId, moduleType, scopeHash, normalizedItemKey);
|
||||
if (isUnchanged(existing, resultId, normalizedScopeKey, countryCode, asin, status, payloadHash)) {
|
||||
return;
|
||||
}
|
||||
String storedPayload = transientPayloadStorageService.storeResultItemPayload(
|
||||
moduleType, taskId, scopeHash, normalizedItemKey, payloadJson);
|
||||
if (existing == null) {
|
||||
TaskResultItemEntity entity = new TaskResultItemEntity();
|
||||
entity.setTaskId(taskId);
|
||||
@@ -159,6 +162,10 @@ public class TaskResultItemService {
|
||||
if (existing == null) {
|
||||
throw new BusinessException("保存任务结果明细失败");
|
||||
}
|
||||
if (isUnchanged(existing, resultId, normalizedScopeKey, countryCode, asin, status, payloadHash)) {
|
||||
transientPayloadStorageService.deleteReplacedPayloadIfNeeded(storedPayload, existing.getPayloadJson());
|
||||
return;
|
||||
}
|
||||
transientPayloadStorageService.deleteReplacedPayloadIfNeeded(existing.getPayloadJson(), storedPayload);
|
||||
taskResultItemMapper.update(null, new LambdaUpdateWrapper<TaskResultItemEntity>()
|
||||
.eq(TaskResultItemEntity::getId, existing.getId())
|
||||
@@ -172,6 +179,24 @@ public class TaskResultItemService {
|
||||
.set(TaskResultItemEntity::getUpdatedAt, now));
|
||||
}
|
||||
|
||||
private boolean isUnchanged(TaskResultItemEntity existing,
|
||||
Long resultId,
|
||||
String scopeKey,
|
||||
String countryCode,
|
||||
String asin,
|
||||
String status,
|
||||
String payloadHash) {
|
||||
if (existing == null) {
|
||||
return false;
|
||||
}
|
||||
return java.util.Objects.equals(existing.getResultId(), resultId)
|
||||
&& java.util.Objects.equals(existing.getScopeKey(), scopeKey)
|
||||
&& java.util.Objects.equals(existing.getCountryCode(), countryCode)
|
||||
&& java.util.Objects.equals(existing.getAsin(), asin)
|
||||
&& java.util.Objects.equals(existing.getStatus(), status)
|
||||
&& java.util.Objects.equals(existing.getPayloadHash(), payloadHash);
|
||||
}
|
||||
|
||||
private TaskResultItemEntity find(Long taskId, String moduleType, String scopeHash, String itemKey) {
|
||||
return taskResultItemMapper.selectOne(new LambdaQueryWrapper<TaskResultItemEntity>()
|
||||
.eq(TaskResultItemEntity::getTaskId, taskId)
|
||||
|
||||
@@ -31,6 +31,7 @@ AIIMAGE_APPEARANCE_PATENT_COZE_WORKFLOW_PATH=/v1/workflow/run
|
||||
AIIMAGE_APPEARANCE_PATENT_COZE_WORKFLOW_ID=7632683471312355338
|
||||
AIIMAGE_APPEARANCE_PATENT_COZE_TOKEN=
|
||||
AIIMAGE_APPEARANCE_PATENT_COZE_BATCH_SIZE=10
|
||||
AIIMAGE_APPEARANCE_PATENT_COZE_READ_TIMEOUT_MILLIS=60000
|
||||
AIIMAGE_APPEARANCE_PATENT_STALE_TIMEOUT_MINUTES=20
|
||||
|
||||
AIIMAGE_ROCKETMQ_NAME_SERVER=121.196.149.225:9876
|
||||
|
||||
@@ -136,6 +136,8 @@ aiimage:
|
||||
coze-workflow-id: ${AIIMAGE_APPEARANCE_PATENT_COZE_WORKFLOW_ID:7632683471312355338}
|
||||
coze-token: ${AIIMAGE_APPEARANCE_PATENT_COZE_TOKEN:}
|
||||
coze-batch-size: ${AIIMAGE_APPEARANCE_PATENT_COZE_BATCH_SIZE:10}
|
||||
coze-connect-timeout-millis: ${AIIMAGE_APPEARANCE_PATENT_COZE_CONNECT_TIMEOUT_MILLIS:10000}
|
||||
coze-read-timeout-millis: ${AIIMAGE_APPEARANCE_PATENT_COZE_READ_TIMEOUT_MILLIS:60000}
|
||||
stale-timeout-minutes: ${AIIMAGE_APPEARANCE_PATENT_STALE_TIMEOUT_MINUTES:20}
|
||||
stale-finalize-cron: ${AIIMAGE_APPEARANCE_PATENT_STALE_FINALIZE_CRON:0 */2 * * * *}
|
||||
security:
|
||||
|
||||
Binary file not shown.
45
backend/codex-flask-local.err.log
Normal file
45
backend/codex-flask-local.err.log
Normal file
@@ -0,0 +1,45 @@
|
||||
WARNING: This is a development server. Do not use it in a production deployment. Use a production WSGI server instead.
|
||||
* Running on all addresses (0.0.0.0)
|
||||
* Running on http://127.0.0.1:15124
|
||||
* Running on http://192.168.9.14:15124
|
||||
Press CTRL+C to quit
|
||||
127.0.0.1 - - [27/Apr/2026 18:32:53] "POST /login HTTP/1.1" 302 -
|
||||
127.0.0.1 - - [27/Apr/2026 18:32:54] "GET /api/admin/shop-manage-groups HTTP/1.1" 200 -
|
||||
127.0.0.1 - - [27/Apr/2026 18:32:55] "POST /api/admin/query-asins/import HTTP/1.1" 200 -
|
||||
127.0.0.1 - - [27/Apr/2026 18:32:55] "GET /api/admin/query-asins/import/3bda54f728ff447f9edc6d80aa47986c HTTP/1.1" 200 -
|
||||
127.0.0.1 - - [27/Apr/2026 18:32:56] "GET /api/admin/query-asins?page=1&page_size=20&group_id=13&shop_name=codex_qasin_wide_20260427181749 HTTP/1.1" 200 -
|
||||
127.0.0.1 - - [27/Apr/2026 18:32:57] "POST /api/admin/query-asins/import HTTP/1.1" 200 -
|
||||
127.0.0.1 - - [27/Apr/2026 18:32:57] "GET /api/admin/query-asins/import/7779a5fac0c840138579f549d297e016 HTTP/1.1" 200 -
|
||||
127.0.0.1 - - [27/Apr/2026 18:32:58] "GET /api/admin/query-asins?page=1&page_size=20&group_id=13&shop_name=codex_qasin_narrow_a_20260427181749 HTTP/1.1" 200 -
|
||||
127.0.0.1 - - [27/Apr/2026 18:32:59] "GET /api/admin/query-asins?page=1&page_size=20&group_id=13&shop_name=codex_qasin_narrow_b_20260427181749 HTTP/1.1" 200 -
|
||||
127.0.0.1 - - [27/Apr/2026 18:32:59] "POST /api/admin/query-asins/delete-import HTTP/1.1" 200 -
|
||||
127.0.0.1 - - [27/Apr/2026 18:33:00] "GET /api/admin/query-asins/delete-import/474b72f74c414d94a534069668cdbe69 HTTP/1.1" 200 -
|
||||
127.0.0.1 - - [27/Apr/2026 18:33:01] "GET /api/admin/query-asins?page=1&page_size=20&group_id=13&shop_name=codex_qasin_wide_20260427181749 HTTP/1.1" 200 -
|
||||
127.0.0.1 - - [27/Apr/2026 18:33:01] "POST /api/admin/query-asins/delete-import HTTP/1.1" 200 -
|
||||
127.0.0.1 - - [27/Apr/2026 18:33:02] "GET /api/admin/query-asins/delete-import/4cdd99881e174c638a531f0805612722 HTTP/1.1" 200 -
|
||||
127.0.0.1 - - [27/Apr/2026 18:33:03] "GET /api/admin/query-asins?page=1&page_size=20&group_id=13&shop_name=codex_qasin_narrow_a_20260427181749 HTTP/1.1" 200 -
|
||||
127.0.0.1 - - [27/Apr/2026 18:33:03] "GET /api/admin/query-asins?page=1&page_size=20&group_id=13&shop_name=codex_qasin_narrow_b_20260427181749 HTTP/1.1" 200 -
|
||||
127.0.0.1 - - [27/Apr/2026 18:35:51] "GET /admin HTTP/1.1" 200 -
|
||||
127.0.0.1 - - [27/Apr/2026 18:35:51] "GET /api/admin/current-user HTTP/1.1" 200 -
|
||||
127.0.0.1 - - [27/Apr/2026 18:35:51] "GET /api/admin/current-user/menus HTTP/1.1" 200 -
|
||||
127.0.0.1 - - [27/Apr/2026 18:35:52] "GET /api/admin/shop-manage-groups HTTP/1.1" 200 -
|
||||
127.0.0.1 - - [27/Apr/2026 18:35:52] "GET /api/admin/users?page=1&page_size=999 HTTP/1.1" 200 -
|
||||
127.0.0.1 - - [27/Apr/2026 18:35:53] "GET /api/admin/users?page=1&page_size=15 HTTP/1.1" 200 -
|
||||
127.0.0.1 - - [27/Apr/2026 18:35:54] "GET /api/admin/columns HTTP/1.1" 200 -
|
||||
127.0.0.1 - - [27/Apr/2026 18:35:54] "GET /api/admin/skip-price-asins?page=1&page_size=15 HTTP/1.1" 200 -
|
||||
127.0.0.1 - - [27/Apr/2026 18:35:56] "GET /api/admin/columns HTTP/1.1" 200 -
|
||||
127.0.0.1 - - [27/Apr/2026 18:38:01] "GET /api/admin/dedupe-total-data?page=1&page_size=15 HTTP/1.1" 200 -
|
||||
127.0.0.1 - - [27/Apr/2026 18:38:04] "GET /api/admin/skip-price-asins?page=1&page_size=15 HTTP/1.1" 200 -
|
||||
127.0.0.1 - - [27/Apr/2026 18:38:06] "GET /api/admin/query-asins?page=1&page_size=15 HTTP/1.1" 200 -
|
||||
127.0.0.1 - - [27/Apr/2026 18:39:08] "GET /admin HTTP/1.1" 200 -
|
||||
127.0.0.1 - - [27/Apr/2026 18:39:08] "GET /api/admin/current-user HTTP/1.1" 200 -
|
||||
127.0.0.1 - - [27/Apr/2026 18:39:09] "GET /api/admin/shop-manage-groups HTTP/1.1" 200 -
|
||||
127.0.0.1 - - [27/Apr/2026 18:39:09] "GET /api/admin/current-user/menus HTTP/1.1" 200 -
|
||||
127.0.0.1 - - [27/Apr/2026 18:39:09] "GET /api/admin/users?page=1&page_size=999 HTTP/1.1" 200 -
|
||||
127.0.0.1 - - [27/Apr/2026 18:39:10] "GET /api/admin/users?page=1&page_size=15 HTTP/1.1" 200 -
|
||||
127.0.0.1 - - [27/Apr/2026 18:39:11] "GET /api/admin/columns HTTP/1.1" 200 -
|
||||
127.0.0.1 - - [27/Apr/2026 18:39:11] "GET /api/admin/skip-price-asins?page=1&page_size=15 HTTP/1.1" 200 -
|
||||
127.0.0.1 - - [27/Apr/2026 18:39:14] "GET /api/admin/columns HTTP/1.1" 200 -
|
||||
127.0.0.1 - - [27/Apr/2026 18:39:19] "GET /api/admin/query-asins?page=1&page_size=15 HTTP/1.1" 200 -
|
||||
127.0.0.1 - - [27/Apr/2026 18:39:22] "GET /api/admin/skip-price-asins?page=1&page_size=15 HTTP/1.1" 200 -
|
||||
127.0.0.1 - - [27/Apr/2026 18:40:07] "GET /api/admin/query-asins?page=1&page_size=15 HTTP/1.1" 200 -
|
||||
2
backend/codex-flask-local.out.log
Normal file
2
backend/codex-flask-local.out.log
Normal file
@@ -0,0 +1,2 @@
|
||||
* Serving Flask app 'app'
|
||||
* Debug mode: off
|
||||
@@ -23,8 +23,8 @@ bucket_path = "nanri-image/"
|
||||
file_url_pre = f"https://{bucket}.oss-cn-hangzhou.aliyuncs.com/"
|
||||
|
||||
import os
|
||||
# backend_java_base_url = os.environ.get('BACKEND_JAVA_BASE_URL', 'http://127.0.0.1:18080').rstrip('/')
|
||||
backend_java_base_url = os.environ.get('BACKEND_JAVA_BASE_URL', 'http://8.136.19.173:18080').rstrip('/')
|
||||
backend_java_base_url = os.environ.get('BACKEND_JAVA_BASE_URL', 'http://127.0.0.1:18080').rstrip('/')
|
||||
# backend_java_base_url = os.environ.get('BACKEND_JAVA_BASE_URL', 'http://8.136.19.173:18080').rstrip('/')
|
||||
os.environ['OSS_ACCESS_KEY_ID'] = accessKeyId
|
||||
os.environ['OSS_ACCESS_KEY_SECRET'] = accessKeySecret
|
||||
os.environ['SECRET_KEY'] = "ddffc7c1d02121d9554d7b080b2511b6"
|
||||
|
||||
@@ -862,7 +862,7 @@
|
||||
<h3 style="margin-bottom:12px;font-size:14px;">导入添加</h3>
|
||||
<div class="form-row">
|
||||
<div class="form-group" style="min-width:320px;">
|
||||
<label>上传 Excel(列:分组、店铺名、德国/英国/法国/意大利/西班牙)</label>
|
||||
<label>上传 Excel(支持两种格式:1. 分组/店铺名/德国-西班牙;2. 店铺账号/ASIN/中文国家名。文件无分组列时取页面当前分组)</label>
|
||||
<input type="file" id="queryAsinImportFile" accept=".xlsx,.xls">
|
||||
</div>
|
||||
<button class="btn" id="btnImportQueryAsin" type="button">上传并添加</button>
|
||||
@@ -879,7 +879,7 @@
|
||||
<h3 style="margin-bottom:12px;font-size:14px;">导入删除</h3>
|
||||
<div class="form-row">
|
||||
<div class="form-group" style="min-width:320px;">
|
||||
<label>上传 Excel(匹配分组 + 店铺名 + 国家 ASIN 后删除)</label>
|
||||
<label>上传 Excel(支持按 分组/店铺名/国家列 或 店铺账号/ASIN/中文国家名 删除;文件无分组列时取页面当前分组)</label>
|
||||
<input type="file" id="queryAsinDeleteImportFile" accept=".xlsx,.xls">
|
||||
</div>
|
||||
<button class="btn btn-danger" id="btnDeleteImportQueryAsin" type="button">上传并删除</button>
|
||||
|
||||
Reference in New Issue
Block a user