提交优化更新
This commit is contained in:
1
app/assets/appearance-patent-BoTBnU7J.css
Normal file
1
app/assets/appearance-patent-BoTBnU7J.css
Normal file
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 +1 @@
|
||||
import{bN as r}from"./java-modules-DzYrjxib.js";const n="";function s(e){return r(`${n}/api/brand/expand-folder-recursive`,{folder:e})}export{s as e};
|
||||
import{bN as r}from"./java-modules-B8c-YG5x.js";const n="";function s(e){return r(`${n}/api/brand/expand-folder-recursive`,{folder:e})}export{s as e};
|
||||
File diff suppressed because one or more lines are too long
1
app/assets/collect-data-DPGAQszU.css
Normal file
1
app/assets/collect-data-DPGAQszU.css
Normal file
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
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
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
app/assets/similar-asin-Bl4LsbT8.css
Normal file
1
app/assets/similar-asin-Bl4LsbT8.css
Normal file
File diff suppressed because one or more lines are too long
@@ -1 +0,0 @@
|
||||
.module-page[data-v-39de0e76]{min-height:100vh;background:#1a1a1a}.main-content[data-v-39de0e76]{display:flex;height:calc(100vh - 56px);min-height:calc(100vh - 56px)}.left-panel[data-v-39de0e76]{width:400px;background:#1e1e1e;padding:20px;overflow-y:auto;border-right:1px solid #2a2a2a}.right-panel[data-v-39de0e76]{flex:1;min-width:0;background:#1a1a1a;display:flex;flex-direction:column}.section-title[data-v-39de0e76],.subsection-title[data-v-39de0e76]{font-size:13px;color:#bbb;margin-bottom:10px}.upload-zone[data-v-39de0e76]{border:1px dashed #3a3a3a;border-radius:10px;padding:18px;background:#252525;margin-bottom:18px}.hint[data-v-39de0e76],.loading-msg[data-v-39de0e76],.files[data-v-39de0e76],.muted[data-v-39de0e76]{color:#888;font-size:12px;line-height:1.5}.link[data-v-39de0e76]{color:#6ea8fe;text-decoration:none}.link[data-v-39de0e76]:hover{color:#9fc5ff}.btns[data-v-39de0e76],.run-row[data-v-39de0e76]{display:flex;gap:10px;flex-wrap:wrap}.opt-btn[data-v-39de0e76],.btn-run[data-v-39de0e76],.btn-delete[data-v-39de0e76],.download[data-v-39de0e76]{border:none;cursor:pointer;border-radius:7px}.opt-btn[data-v-39de0e76]{padding:8px 14px;color:#ccc;background:#2a2a2a;border:1px solid #3a3a3a}.btn-run[data-v-39de0e76]{padding:10px 18px;color:#fff;background:#3498db;font-weight:600}.btn-queue[data-v-39de0e76]{background:#27ae60}.btn-run[data-v-39de0e76]:disabled{opacity:.55;cursor:not-allowed}.selected-files[data-v-39de0e76]{margin-top:14px;color:#999;font-size:12px;word-break:break-all}.selected-files span[data-v-39de0e76]{display:block;margin:4px 0}.parse-card[data-v-39de0e76],.queue-payload[data-v-39de0e76]{margin-top:14px;padding:12px;border:1px solid #2a2a2a;border-radius:8px;background:#202020;color:#b8c1cc;font-size:12px}.queue-payload[data-v-39de0e76]{max-height:220px;overflow:auto;color:#8fd3ff;white-space:pre-wrap}.panel-header[data-v-39de0e76]{padding:16px 20px;border-bottom:1px solid #2a2a2a;font-size:15px;font-weight:600;color:#ddd}.task-list-wrap[data-v-39de0e76]{flex:1;padding:16px 20px;overflow:auto}.clean-result-summary[data-v-39de0e76]{display:grid;grid-template-columns:repeat(4,minmax(0,1fr));gap:12px;margin-bottom:16px}.summary-card[data-v-39de0e76]{padding:14px 16px;border:1px solid #2a2a2a;border-radius:8px;background:#1e1e1e}.summary-card strong[data-v-39de0e76]{display:block;margin-top:8px;color:#eaf4ff;font-size:22px}.summary-label[data-v-39de0e76]{color:#8d8d8d;font-size:12px}.result-list-wrap[data-v-39de0e76]{border:1px solid #2a2a2a;border-radius:8px;background:#1e1e1e;min-height:180px;margin:0 0 16px}.result-list-header[data-v-39de0e76]{display:flex;justify-content:space-between;padding:12px 16px;border-bottom:1px solid #2a2a2a;color:#ddd;font-size:14px}.empty-tasks[data-v-39de0e76]{color:#666;font-size:13px;padding:18px;text-align:center}.task-list[data-v-39de0e76]{list-style:none;margin:0;padding:12px}.task-item[data-v-39de0e76]{display:flex;align-items:flex-start;justify-content:space-between;gap:12px;padding:12px 14px;border:1px solid #2a2a2a;border-radius:8px;margin-bottom:8px;background:#222}.left[data-v-39de0e76]{flex:1;min-width:0}.id[data-v-39de0e76]{color:#e0e0e0;font-size:13px;font-weight:600}.task-right[data-v-39de0e76]{display:flex;gap:8px;align-items:center;flex-wrap:wrap}.status[data-v-39de0e76]{padding:4px 10px;border-radius:6px;font-size:12px}.status.success[data-v-39de0e76]{background:#2ecc712e;color:#2ecc71}.status.failed[data-v-39de0e76]{background:#e74c3c2e;color:#ff6b6b}.status.running[data-v-39de0e76]{background:#3498db2e;color:#3498db}.status.pending[data-v-39de0e76]{background:#95a5a62e;color:#bdc3c7}.result-hint[data-v-39de0e76]{margin-top:6px;color:#e0b96d}.file-progress[data-v-39de0e76]{margin-top:8px;max-width:520px}.file-progress-meta[data-v-39de0e76]{display:flex;justify-content:space-between;gap:12px;color:#d8c278;font-size:12px}.file-progress-track[data-v-39de0e76]{margin-top:5px;height:8px;border-radius:999px;overflow:hidden;background:#303030;border:1px solid #3b3b3b}.file-progress-bar[data-v-39de0e76]{height:100%;border-radius:inherit;background:linear-gradient(90deg,#4aa3ff,#f0c75e);transition:width .25s ease}.file-progress-count[data-v-39de0e76]{margin-top:4px;color:#858585;font-size:11px}.download[data-v-39de0e76]{padding:6px 10px;color:#d6ecff;background:#3498db2e}.btn-delete[data-v-39de0e76]{padding:6px 10px;color:#ff8f8f;background:#e74c3c1f}@media(max-width:1100px){.main-content[data-v-39de0e76]{flex-direction:column;height:auto}.left-panel[data-v-39de0e76]{width:100%;border-right:none;border-bottom:1px solid #2a2a2a}.clean-result-summary[data-v-39de0e76]{grid-template-columns:repeat(2,minmax(0,1fr))}}
|
||||
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
@@ -5,8 +5,8 @@
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<title>格式转换 - 数富AI</title>
|
||||
<script type="module" crossorigin src="/assets/convert.js"></script>
|
||||
<link rel="modulepreload" crossorigin href="/assets/java-modules-DzYrjxib.js">
|
||||
<link rel="modulepreload" crossorigin href="/assets/brand-jr47vQUd.js">
|
||||
<link rel="modulepreload" crossorigin href="/assets/java-modules-B8c-YG5x.js">
|
||||
<link rel="modulepreload" crossorigin href="/assets/brand-COze15GJ.js">
|
||||
<link rel="stylesheet" crossorigin href="/assets/java-modules-l5anrOZ2.css">
|
||||
<link rel="stylesheet" crossorigin href="/assets/convert-6oxZNMye.css">
|
||||
</head>
|
||||
|
||||
@@ -5,8 +5,8 @@
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<title>数据去重 - 数富AI</title>
|
||||
<script type="module" crossorigin src="/assets/dedupe.js"></script>
|
||||
<link rel="modulepreload" crossorigin href="/assets/java-modules-DzYrjxib.js">
|
||||
<link rel="modulepreload" crossorigin href="/assets/brand-jr47vQUd.js">
|
||||
<link rel="modulepreload" crossorigin href="/assets/java-modules-B8c-YG5x.js">
|
||||
<link rel="modulepreload" crossorigin href="/assets/brand-COze15GJ.js">
|
||||
<link rel="stylesheet" crossorigin href="/assets/java-modules-l5anrOZ2.css">
|
||||
<link rel="stylesheet" crossorigin href="/assets/dedupe-DpfPYQDv.css">
|
||||
</head>
|
||||
|
||||
@@ -5,8 +5,8 @@
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<title>删除品牌 - 数富AI</title>
|
||||
<script type="module" crossorigin src="/assets/delete-brand.js"></script>
|
||||
<link rel="modulepreload" crossorigin href="/assets/java-modules-DzYrjxib.js">
|
||||
<link rel="modulepreload" crossorigin href="/assets/brand-jr47vQUd.js">
|
||||
<link rel="modulepreload" crossorigin href="/assets/java-modules-B8c-YG5x.js">
|
||||
<link rel="modulepreload" crossorigin href="/assets/brand-COze15GJ.js">
|
||||
<link rel="modulepreload" crossorigin href="/assets/categorized-timers-JPA-olTr.js">
|
||||
<link rel="stylesheet" crossorigin href="/assets/java-modules-l5anrOZ2.css">
|
||||
<link rel="stylesheet" crossorigin href="/assets/delete-brand-DZ3x5hOX.css">
|
||||
|
||||
@@ -5,8 +5,8 @@
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<title>数据拆分 - 数富AI</title>
|
||||
<script type="module" crossorigin src="/assets/split.js"></script>
|
||||
<link rel="modulepreload" crossorigin href="/assets/java-modules-DzYrjxib.js">
|
||||
<link rel="modulepreload" crossorigin href="/assets/brand-jr47vQUd.js">
|
||||
<link rel="modulepreload" crossorigin href="/assets/java-modules-B8c-YG5x.js">
|
||||
<link rel="modulepreload" crossorigin href="/assets/brand-COze15GJ.js">
|
||||
<link rel="stylesheet" crossorigin href="/assets/java-modules-l5anrOZ2.css">
|
||||
<link rel="stylesheet" crossorigin href="/assets/split-CR1PtDQE.css">
|
||||
</head>
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
package com.nanri.aiimage.modules.collectdata.model.dto;
|
||||
|
||||
import io.swagger.v3.oas.annotations.media.Schema;
|
||||
import com.fasterxml.jackson.annotation.JsonAlias;
|
||||
import lombok.Data;
|
||||
|
||||
import java.math.BigDecimal;
|
||||
@@ -14,8 +15,16 @@ public class CollectDataFiltersDto {
|
||||
@Schema(description = "金额(手动输入),可为空", example = "59.99")
|
||||
private BigDecimal amount;
|
||||
|
||||
@Schema(description = "排名(手动输入),可为空", example = "100000")
|
||||
private Integer rank;
|
||||
@JsonAlias({"min_amount"})
|
||||
@Schema(description = "最小金额,可为空", example = "10.00")
|
||||
private BigDecimal minAmount;
|
||||
|
||||
@JsonAlias({"max_amount"})
|
||||
@Schema(description = "最大金额,可为空", example = "59.99")
|
||||
private BigDecimal maxAmount;
|
||||
|
||||
@Schema(description = "是否启用排名条件")
|
||||
private Boolean rank;
|
||||
|
||||
@Schema(description = "是否包含 FBA 商品")
|
||||
private Boolean fba;
|
||||
@@ -25,4 +34,8 @@ public class CollectDataFiltersDto {
|
||||
|
||||
@Schema(description = "国家代码列表,按用户拖拽顺序保存", example = "[\"DE\",\"UK\"]")
|
||||
private List<String> countryCodes = new ArrayList<>();
|
||||
|
||||
@JsonAlias({"country_code"})
|
||||
@Schema(description = "单选国家代码,采集数据页面当前只选择一个国家", example = "DE")
|
||||
private String countryCode;
|
||||
}
|
||||
|
||||
@@ -42,6 +42,10 @@ public class SimilarAsinCozeClient {
|
||||
private volatile RestClient sharedRestClient;
|
||||
|
||||
public List<SimilarAsinResultRowDto> inspect(List<SimilarAsinResultRowDto> rows, String prompt, String apiKey) {
|
||||
return inspect(rows, prompt, apiKey, false);
|
||||
}
|
||||
|
||||
public List<SimilarAsinResultRowDto> inspect(List<SimilarAsinResultRowDto> rows, String prompt, String apiKey, boolean imgSwitch) {
|
||||
if (rows == null || rows.isEmpty()) {
|
||||
return List.of();
|
||||
}
|
||||
@@ -50,7 +54,7 @@ public class SimilarAsinCozeClient {
|
||||
return rows.stream().map(this::copy).toList();
|
||||
}
|
||||
try {
|
||||
return inspectWithFallback(rows, prompt, apiKey);
|
||||
return inspectWithFallback(rows, prompt, apiKey, imgSwitch);
|
||||
} catch (Exception ex) {
|
||||
String failureMessage = failureMessage(ex);
|
||||
log.warn("[similar-asin] coze batch failed size={} err={}", rows.size(), failureMessage);
|
||||
@@ -59,15 +63,20 @@ public class SimilarAsinCozeClient {
|
||||
}
|
||||
|
||||
public CozeSubmitResponse submitWorkflow(List<SimilarAsinResultRowDto> rows, String prompt, String apiKey) throws Exception {
|
||||
return submitWorkflow(rows, prompt, apiKey, nextCredential());
|
||||
return submitWorkflow(rows, prompt, apiKey, false, nextCredential());
|
||||
}
|
||||
|
||||
public CozeSubmitResponse submitWorkflow(List<SimilarAsinResultRowDto> rows, String prompt, String apiKey, boolean imgSwitch) throws Exception {
|
||||
return submitWorkflow(rows, prompt, apiKey, imgSwitch, nextCredential());
|
||||
}
|
||||
|
||||
public CozeSubmitResponse submitWorkflow(List<SimilarAsinResultRowDto> rows,
|
||||
String prompt,
|
||||
String apiKey,
|
||||
boolean imgSwitch,
|
||||
CozeCredentialRef credential) throws Exception {
|
||||
CozeCredentialRef resolvedCredential = resolveCredential(credential);
|
||||
JsonNode submitRoot = objectMapper.readTree(postWorkflow(rows, prompt, apiKey, resolvedCredential));
|
||||
JsonNode submitRoot = objectMapper.readTree(postWorkflow(rows, prompt, apiKey, imgSwitch, resolvedCredential));
|
||||
ensureSuccess(submitRoot);
|
||||
return new CozeSubmitResponse(
|
||||
extractExecuteId(submitRoot),
|
||||
@@ -109,12 +118,12 @@ public class SimilarAsinCozeClient {
|
||||
return rows.stream().map(this::copy).map(row -> markFailed(row, failureMessage)).toList();
|
||||
}
|
||||
|
||||
private List<SimilarAsinResultRowDto> inspectWithFallback(List<SimilarAsinResultRowDto> rows, String prompt, String apiKey) {
|
||||
private List<SimilarAsinResultRowDto> inspectWithFallback(List<SimilarAsinResultRowDto> rows, String prompt, String apiKey, boolean imgSwitch) {
|
||||
try {
|
||||
if (rows.size() == 1) {
|
||||
return inspectSingleRowWithRetry(rows, prompt, apiKey);
|
||||
return inspectSingleRowWithRetry(rows, prompt, apiKey, imgSwitch);
|
||||
}
|
||||
InspectAttempt attempt = inspectOnce(rows, prompt, apiKey);
|
||||
InspectAttempt attempt = inspectOnce(rows, prompt, apiKey, imgSwitch);
|
||||
if (attempt.resolvedCount() < rows.size()) {
|
||||
throw new PartialCozeResultException(attempt.resolvedCount(), rows.size(), attempt.rawResultCount());
|
||||
}
|
||||
@@ -125,17 +134,17 @@ public class SimilarAsinCozeClient {
|
||||
log.warn("[similar-asin] coze batch fallback split size={} left={} right={} err={}",
|
||||
rows.size(), middle, rows.size() - middle, failureMessage(ex));
|
||||
List<SimilarAsinResultRowDto> merged = new ArrayList<>(rows.size());
|
||||
merged.addAll(inspectPartitionWithFailureFallback(rows.subList(0, middle), prompt, apiKey));
|
||||
merged.addAll(inspectPartitionWithFailureFallback(rows.subList(middle, rows.size()), prompt, apiKey));
|
||||
merged.addAll(inspectPartitionWithFailureFallback(rows.subList(0, middle), prompt, apiKey, imgSwitch));
|
||||
merged.addAll(inspectPartitionWithFailureFallback(rows.subList(middle, rows.size()), prompt, apiKey, imgSwitch));
|
||||
return merged;
|
||||
}
|
||||
throw propagate(ex);
|
||||
}
|
||||
}
|
||||
|
||||
private List<SimilarAsinResultRowDto> inspectPartitionWithFailureFallback(List<SimilarAsinResultRowDto> rows, String prompt, String apiKey) {
|
||||
private List<SimilarAsinResultRowDto> inspectPartitionWithFailureFallback(List<SimilarAsinResultRowDto> rows, String prompt, String apiKey, boolean imgSwitch) {
|
||||
try {
|
||||
return inspectWithFallback(rows, prompt, apiKey);
|
||||
return inspectWithFallback(rows, prompt, apiKey, imgSwitch);
|
||||
} catch (Exception ex) {
|
||||
String failureMessage = failureMessage(ex);
|
||||
log.warn("[similar-asin] coze partition failed size={} err={}", rows.size(), failureMessage);
|
||||
@@ -143,12 +152,12 @@ public class SimilarAsinCozeClient {
|
||||
}
|
||||
}
|
||||
|
||||
private List<SimilarAsinResultRowDto> inspectSingleRowWithRetry(List<SimilarAsinResultRowDto> rows, String prompt, String apiKey) throws Exception {
|
||||
private List<SimilarAsinResultRowDto> inspectSingleRowWithRetry(List<SimilarAsinResultRowDto> rows, String prompt, String apiKey, boolean imgSwitch) throws Exception {
|
||||
SimilarAsinResultRowDto row = rows.getFirst();
|
||||
PartialCozeResultException lastFailure = null;
|
||||
for (int attemptIndex = 1; attemptIndex <= 3; attemptIndex++) {
|
||||
try {
|
||||
InspectAttempt attempt = inspectOnce(rows, prompt, apiKey);
|
||||
InspectAttempt attempt = inspectOnce(rows, prompt, apiKey, imgSwitch);
|
||||
if (attempt.resolvedCount() == rows.size()) {
|
||||
return attempt.mergedRows();
|
||||
}
|
||||
@@ -179,16 +188,16 @@ public class SimilarAsinCozeClient {
|
||||
throw lastFailure == null ? new PartialCozeResultException(0, rows.size(), 0) : lastFailure;
|
||||
}
|
||||
|
||||
private InspectAttempt inspectOnce(List<SimilarAsinResultRowDto> rows, String prompt, String apiKey) throws Exception {
|
||||
String raw = runWorkflowAsyncAndWait(rows, prompt, apiKey);
|
||||
private InspectAttempt inspectOnce(List<SimilarAsinResultRowDto> rows, String prompt, String apiKey, boolean imgSwitch) throws Exception {
|
||||
String raw = runWorkflowAsyncAndWait(rows, prompt, apiKey, imgSwitch);
|
||||
List<CozeResult> results = parseResults(raw);
|
||||
List<SimilarAsinResultRowDto> merged = mergeRows(rows, results);
|
||||
return new InspectAttempt(raw, merged, resolvedCount(merged), results.size());
|
||||
}
|
||||
|
||||
private String runWorkflowAsyncAndWait(List<SimilarAsinResultRowDto> rows, String prompt, String apiKey) throws Exception {
|
||||
private String runWorkflowAsyncAndWait(List<SimilarAsinResultRowDto> rows, String prompt, String apiKey, boolean imgSwitch) throws Exception {
|
||||
CozeCredentialRef credential = nextCredential();
|
||||
JsonNode submitRoot = objectMapper.readTree(postWorkflow(rows, prompt, apiKey, credential));
|
||||
JsonNode submitRoot = objectMapper.readTree(postWorkflow(rows, prompt, apiKey, imgSwitch, credential));
|
||||
ensureSuccess(submitRoot);
|
||||
|
||||
String immediateData = extractResultDataText(submitRoot);
|
||||
@@ -232,8 +241,9 @@ public class SimilarAsinCozeClient {
|
||||
private String postWorkflow(List<SimilarAsinResultRowDto> rows,
|
||||
String prompt,
|
||||
String apiKey,
|
||||
boolean imgSwitch,
|
||||
CozeCredentialRef credential) {
|
||||
Map<String, Object> parameters = buildParameters(rows, prompt, apiKey);
|
||||
Map<String, Object> parameters = buildParameters(rows, prompt, apiKey, imgSwitch);
|
||||
Map<String, Object> body = new LinkedHashMap<>();
|
||||
body.put("workflow_id", credential.workflowId());
|
||||
body.put("parameters", parameters);
|
||||
@@ -354,7 +364,7 @@ public class SimilarAsinCozeClient {
|
||||
return credentials;
|
||||
}
|
||||
|
||||
private Map<String, Object> buildParameters(List<SimilarAsinResultRowDto> rows, String prompt, String apiKey) {
|
||||
private Map<String, Object> buildParameters(List<SimilarAsinResultRowDto> rows, String prompt, String apiKey, boolean imgSwitch) {
|
||||
List<String> asins = rows.stream().map(row -> safeText(row.getAsin())).toList();
|
||||
List<String> titles = rows.stream().map(row -> safeText(firstNonBlank(row.getTitle(), row.getAsin()))).toList();
|
||||
List<String> skus = rows.stream().map(row -> safeText(row.getSku())).toList();
|
||||
@@ -375,6 +385,7 @@ public class SimilarAsinCozeClient {
|
||||
if (includeLegacyApiKey && apiKey != null && !apiKey.isBlank()) {
|
||||
parameters.put("api_key", apiKey.trim());
|
||||
}
|
||||
parameters.put("img_switch", imgSwitch);
|
||||
return parameters;
|
||||
}
|
||||
|
||||
|
||||
@@ -32,4 +32,9 @@ public class SimilarAsinParseRequest {
|
||||
@Schema(description = "传递给 Coze workflow parameters.api_key 的任务级密钥。")
|
||||
@NotBlank(message = "密钥不能为空")
|
||||
private String apiKey;
|
||||
|
||||
@JsonProperty("img_switch")
|
||||
@JsonAlias({"imgSwitch"})
|
||||
@Schema(description = "传递给 Coze workflow parameters.img_switch 的图片检测开关,true 为开启,false 为关闭。")
|
||||
private Boolean imgSwitch = Boolean.FALSE;
|
||||
}
|
||||
|
||||
@@ -17,6 +17,9 @@ public class SimilarAsinParsedPayloadDto {
|
||||
@Schema(description = "传递给 Coze workflow parameters.api_key 的任务级密钥")
|
||||
private String apiKey;
|
||||
|
||||
@Schema(description = "传递给 Coze workflow parameters.img_switch 的图片检测开关")
|
||||
private Boolean imgSwitch = Boolean.FALSE;
|
||||
|
||||
@Schema(description = "本次解析的源文件列表")
|
||||
private List<SimilarAsinSourceFileDto> sourceFiles = new ArrayList<>();
|
||||
|
||||
|
||||
@@ -33,6 +33,9 @@ public class SimilarAsinParseVo {
|
||||
@Schema(description = "本任务最终使用的 AI 提示词")
|
||||
private String aiPrompt;
|
||||
|
||||
@Schema(description = "图片检测开关,true 为开启,false 为关闭")
|
||||
private Boolean imgSwitch = Boolean.FALSE;
|
||||
|
||||
@Schema(description = "兼容旧前端的平铺有效行列表,现为全部有效行")
|
||||
private List<SimilarAsinParsedRowVo> items = new ArrayList<>();
|
||||
|
||||
|
||||
@@ -401,11 +401,11 @@ public class SimilarAsinTaskService {
|
||||
|
||||
String aggregateScopeKey = buildAggregateScopeKey(sourceFiles);
|
||||
String sourceScopeHash = DigestUtil.sha256Hex(aggregateScopeKey);
|
||||
String parsedPayload = buildParsedPayloadJson(request.getAiPrompt(), request.getApiKey(), sourceFiles, mergedHeaders, groups, allRows);
|
||||
String parsedPayload = buildParsedPayloadJson(request.getAiPrompt(), request.getApiKey(), request.getImgSwitch(), sourceFiles, mergedHeaders, groups, allRows);
|
||||
long payloadBuiltAt = System.nanoTime();
|
||||
String parsedPayloadPointer = storeParsedPayload(task.getId(), sourceScopeHash, parsedPayload);
|
||||
long payloadStoredAt = System.nanoTime();
|
||||
task.setResultJson(buildTaskResultJson(request.getAiPrompt(), request.getApiKey(), sourceFiles, parsedPayloadPointer));
|
||||
task.setResultJson(buildTaskResultJson(request.getAiPrompt(), request.getApiKey(), request.getImgSwitch(), sourceFiles, parsedPayloadPointer));
|
||||
task.setUpdatedAt(LocalDateTime.now());
|
||||
fileTaskMapper.updateById(task);
|
||||
|
||||
@@ -444,6 +444,7 @@ public class SimilarAsinTaskService {
|
||||
vo.setDroppedRows(droppedRows);
|
||||
vo.setGroupCount(groups.size());
|
||||
vo.setAiPrompt(normalize(request.getAiPrompt()));
|
||||
vo.setImgSwitch(Boolean.TRUE.equals(request.getImgSwitch()));
|
||||
vo.setItems(new ArrayList<>(allRows));
|
||||
vo.setGroups(groups);
|
||||
long finishedAt = System.nanoTime();
|
||||
@@ -1229,10 +1230,11 @@ public class SimilarAsinTaskService {
|
||||
}
|
||||
String prompt = readAiPrompt(task);
|
||||
String apiKey = readApiKey(task);
|
||||
boolean imgSwitch = readImgSwitch(task);
|
||||
int batchSize = Math.max(1, properties.getCozeBatchSize());
|
||||
List<SimilarAsinResultRowDto> result = new ArrayList<>();
|
||||
for (int i = 0; i < items.size(); i += batchSize) {
|
||||
result.addAll(cozeClient.inspect(items.subList(i, Math.min(i + batchSize, items.size())), prompt, apiKey));
|
||||
result.addAll(cozeClient.inspect(items.subList(i, Math.min(i + batchSize, items.size())), prompt, apiKey, imgSwitch));
|
||||
if (progressHook != null) {
|
||||
progressHook.run();
|
||||
}
|
||||
@@ -1543,6 +1545,14 @@ public class SimilarAsinTaskService {
|
||||
}
|
||||
}
|
||||
|
||||
private boolean readImgSwitch(FileTaskEntity task) {
|
||||
try {
|
||||
return Boolean.TRUE.equals(readParsedPayload(task).getImgSwitch());
|
||||
} catch (Exception ignored) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
private void finalizeTask(FileTaskEntity task, String error, int rowCount, boolean assembleWorkbook) {
|
||||
String finalError = error;
|
||||
FileResultEntity result = null;
|
||||
@@ -1910,6 +1920,7 @@ public class SimilarAsinTaskService {
|
||||
}
|
||||
String prompt = readAiPrompt(task);
|
||||
String apiKey = readApiKey(task);
|
||||
boolean imgSwitch = readImgSwitch(task);
|
||||
int batchSize = Math.max(1, properties.getCozeBatchSize());
|
||||
// P1-1:检测到 720712008 风暴时强制把 batch 降到 1,隔离毒行;
|
||||
// 持续 5+ 次提交命中率 ≥ 40% 才会触发,正常波动不影响吞吐。
|
||||
@@ -1974,7 +1985,7 @@ public class SimilarAsinTaskService {
|
||||
int batchIndex = 1;
|
||||
for (int i = 0; i < submitLimit; i += batchSize) {
|
||||
List<CozeCandidate> batchCandidates = readyCandidates.subList(i, Math.min(i + batchSize, submitLimit));
|
||||
pending |= submitCozeBatch(task, result, job, batchCandidates, batchIndex, batchTotal, prompt, apiKey, allRowsByBaseId);
|
||||
pending |= submitCozeBatch(task, result, job, batchCandidates, batchIndex, batchTotal, prompt, apiKey, imgSwitch, allRowsByBaseId);
|
||||
batchIndex++;
|
||||
}
|
||||
return pending || countPendingCozeStates(task.getId()) > 0;
|
||||
@@ -2089,6 +2100,7 @@ public class SimilarAsinTaskService {
|
||||
int batchTotal,
|
||||
String prompt,
|
||||
String apiKey,
|
||||
boolean imgSwitch,
|
||||
Map<String, List<SimilarAsinParsedRowVo>> allRowsByBaseId) {
|
||||
if (batchCandidates == null || batchCandidates.isEmpty()) {
|
||||
return false;
|
||||
@@ -2115,7 +2127,7 @@ public class SimilarAsinTaskService {
|
||||
SimilarAsinCozeClient.CozeCredentialRef credential = cozeClient.nextCredential();
|
||||
try {
|
||||
SimilarAsinCozeClient.CozeSubmitResponse submit = submitCozeWorkflowThrottled(
|
||||
batchRows, prompt, apiKey, credential, true);
|
||||
batchRows, prompt, apiKey, imgSwitch, credential, true);
|
||||
if (submit.immediateData() != null && !submit.immediateData().isBlank()) {
|
||||
List<SimilarAsinResultRowDto> cozeRows = cozeClient.mergeRowsFromDataText(batchRows, submit.immediateData());
|
||||
String emptyResultMessage = emptyCozeResultMessage(cozeRows, batchRows.size());
|
||||
@@ -2426,7 +2438,7 @@ public class SimilarAsinTaskService {
|
||||
}
|
||||
try {
|
||||
SimilarAsinCozeClient.CozeSubmitResponse submit =
|
||||
submitCozeWorkflowThrottled(batchRows, readAiPrompt(task), readApiKey(task),
|
||||
submitCozeWorkflowThrottled(batchRows, readAiPrompt(task), readApiKey(task), readImgSwitch(task),
|
||||
cozeClient.credentialByName(context.credentialName()), false);
|
||||
Map<String, List<SimilarAsinParsedRowVo>> allRowsByBaseId = allRowsByBaseIdForPoll(task);
|
||||
if (submit.immediateData() != null && !submit.immediateData().isBlank()) {
|
||||
@@ -2488,6 +2500,7 @@ public class SimilarAsinTaskService {
|
||||
List<SimilarAsinResultRowDto> rows,
|
||||
String prompt,
|
||||
String apiKey,
|
||||
boolean imgSwitch,
|
||||
SimilarAsinCozeClient.CozeCredentialRef credential,
|
||||
boolean allowCredentialFallback) throws Exception {
|
||||
int attempts = allowCredentialFallback ? Math.max(1, cozeClient.configuredCredentialCount()) : 1;
|
||||
@@ -2521,7 +2534,7 @@ public class SimilarAsinTaskService {
|
||||
}
|
||||
try (lockHandle; borrowedCredential) {
|
||||
SimilarAsinCozeClient.CozeSubmitResponse response =
|
||||
cozeClient.submitWorkflow(rows, prompt, apiKey, currentCredential);
|
||||
cozeClient.submitWorkflow(rows, prompt, apiKey, imgSwitch, currentCredential);
|
||||
// 提交完成立即记录时间戳,用于下次进入循环时计算节流等待。
|
||||
lastCozeSubmitAtByCredential.put(credentialKey, System.currentTimeMillis());
|
||||
return response;
|
||||
@@ -2713,7 +2726,7 @@ public class SimilarAsinTaskService {
|
||||
int partIndex = 1;
|
||||
for (List<SimilarAsinResultRowDto> partRows : partitions) {
|
||||
SimilarAsinCozeClient.CozeSubmitResponse submit =
|
||||
submitCozeWorkflowThrottled(partRows, readAiPrompt(task), readApiKey(task),
|
||||
submitCozeWorkflowThrottled(partRows, readAiPrompt(task), readApiKey(task), readImgSwitch(task),
|
||||
cozeClient.credentialByName(context.credentialName()), false);
|
||||
if (submit.immediateData() != null && !submit.immediateData().isBlank()) {
|
||||
List<SimilarAsinResultRowDto> cozeRows =
|
||||
@@ -2847,7 +2860,7 @@ public class SimilarAsinTaskService {
|
||||
try {
|
||||
taskFileJobService.touchRunning(context.jobId());
|
||||
SimilarAsinCozeClient.CozeSubmitResponse submit =
|
||||
submitCozeWorkflowThrottled(batchRows, readAiPrompt(task), readApiKey(task),
|
||||
submitCozeWorkflowThrottled(batchRows, readAiPrompt(task), readApiKey(task), readImgSwitch(task),
|
||||
cozeClient.credentialByName(context.credentialName()), false);
|
||||
Map<String, List<SimilarAsinParsedRowVo>> allRowsByBaseId = allRowsByBaseIdForPoll(task);
|
||||
if (submit.immediateData() != null && !submit.immediateData().isBlank()) {
|
||||
@@ -3691,13 +3704,26 @@ public class SimilarAsinTaskService {
|
||||
if (taskId == null || taskId <= 0) {
|
||||
return false;
|
||||
}
|
||||
Long count = taskScopeStateMapper.selectCount(new LambdaQueryWrapper<TaskScopeStateEntity>()
|
||||
List<TaskScopeStateEntity> states = taskScopeStateMapper.selectList(new LambdaQueryWrapper<TaskScopeStateEntity>()
|
||||
.eq(TaskScopeStateEntity::getTaskId, taskId)
|
||||
.eq(TaskScopeStateEntity::getModuleType, MODULE_TYPE)
|
||||
.isNull(TaskScopeStateEntity::getCozeStatus)
|
||||
.isNotNull(TaskScopeStateEntity::getLastChunkAt)
|
||||
.eq(TaskScopeStateEntity::getCompleted, 1));
|
||||
return count != null && count > 0;
|
||||
.isNotNull(TaskScopeStateEntity::getLastChunkAt));
|
||||
if (states == null || states.isEmpty()) {
|
||||
return false;
|
||||
}
|
||||
boolean hasCompletedScope = false;
|
||||
for (TaskScopeStateEntity state : states) {
|
||||
if (state == null) {
|
||||
continue;
|
||||
}
|
||||
if (Integer.valueOf(1).equals(state.getCompleted())) {
|
||||
hasCompletedScope = true;
|
||||
continue;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
return hasCompletedScope;
|
||||
}
|
||||
|
||||
private boolean isJavaSideProcessing(Long taskId) {
|
||||
@@ -3737,6 +3763,9 @@ public class SimilarAsinTaskService {
|
||||
SimilarAsinResultRowDto row) {
|
||||
}
|
||||
|
||||
private record PythonUploadProgress(int current, int total, String unit) {
|
||||
}
|
||||
|
||||
private String buildTaskOwnerScopeKey(FileTaskEntity task) {
|
||||
Long taskId = task == null ? null : task.getId();
|
||||
return "task:" + taskId + ":owner:" + firstNonBlank(ownerFromTask(task), currentInstanceId());
|
||||
@@ -4812,8 +4841,8 @@ public class SimilarAsinTaskService {
|
||||
vo.setSourceFilename(row.getSourceFilename());
|
||||
vo.setResultFilename(row.getResultFilename());
|
||||
vo.setDownloadUrl(buildFreshDownloadUrl(row));
|
||||
attachFileJobState(vo, row, job);
|
||||
vo.setTaskStatus(task == null ? null : task.getStatus());
|
||||
attachFileJobState(vo, row, job, task);
|
||||
vo.setSuccess(row.getSuccess() != null && row.getSuccess() == 1);
|
||||
vo.setError(row.getErrorMessage());
|
||||
vo.setRowCount(row.getRowCount());
|
||||
@@ -4838,20 +4867,20 @@ public class SimilarAsinTaskService {
|
||||
}
|
||||
}
|
||||
|
||||
private void attachFileJobState(SimilarAsinHistoryItemVo vo, FileResultEntity row, TaskFileJobEntity job) {
|
||||
private void attachFileJobState(SimilarAsinHistoryItemVo vo, FileResultEntity row, TaskFileJobEntity job, FileTaskEntity task) {
|
||||
vo.setFileReady(row.getResultFileUrl() != null && !row.getResultFileUrl().isBlank());
|
||||
if (job == null) {
|
||||
vo.setFileStatus(Boolean.TRUE.equals(vo.getFileReady()) ? "SUCCESS" : null);
|
||||
attachFileProgress(vo, row, job);
|
||||
attachFileProgress(vo, row, job, task);
|
||||
return;
|
||||
}
|
||||
vo.setFileJobId(job.getId());
|
||||
vo.setFileStatus(job.getStatus());
|
||||
vo.setFileError(job.getErrorMessage());
|
||||
attachFileProgress(vo, row, job);
|
||||
attachFileProgress(vo, row, job, task);
|
||||
}
|
||||
|
||||
private void attachFileProgress(SimilarAsinHistoryItemVo vo, FileResultEntity row, TaskFileJobEntity job) {
|
||||
private void attachFileProgress(SimilarAsinHistoryItemVo vo, FileResultEntity row, TaskFileJobEntity job, FileTaskEntity task) {
|
||||
if (row == null || row.getTaskId() == null) {
|
||||
return;
|
||||
}
|
||||
@@ -4866,22 +4895,22 @@ public class SimilarAsinTaskService {
|
||||
int cozeCompleted = countCompletedCozeStates(taskId);
|
||||
int cozePending = countPendingCozeStates(taskId);
|
||||
boolean uploadComplete = isResultSubmissionComplete(taskId);
|
||||
if (!uploadComplete) {
|
||||
if (task != null && !STATUS_RUNNING.equals(task.getStatus())) {
|
||||
return;
|
||||
}
|
||||
attachPythonUploadProgress(vo, taskId);
|
||||
return;
|
||||
}
|
||||
TaskProgressSnapshotEntity snapshot = taskProgressSnapshotService.find(taskId, MODULE_TYPE);
|
||||
if (cozeCompleted + cozePending > 0) {
|
||||
int total = calculateCozeDisplayTotal(snapshot, cozeCompleted + cozePending);
|
||||
if (!uploadComplete) {
|
||||
total = Math.max(total, cozeCompleted + cozePending + 1);
|
||||
}
|
||||
int current = Math.max(0, Math.min(cozeCompleted, total));
|
||||
vo.setFileProgressCurrent(current);
|
||||
vo.setFileProgressTotal(total);
|
||||
int percent = calculateDisplayProgressPercent(current, total, job, snapshot == null ? null : snapshot.getUpdatedAt());
|
||||
percent = Math.max(percent, calculateSnapshotDisplayPercent(snapshot, job));
|
||||
vo.setFileProgressPercent(Math.min(uploadComplete ? 99 : 98, percent));
|
||||
if (!uploadComplete) {
|
||||
vo.setFileProgressMessage(buildUploadingCozeProgressMessage(current, total, cozePending));
|
||||
return;
|
||||
}
|
||||
vo.setFileProgressPercent(Math.min(99, percent));
|
||||
vo.setFileProgressMessage(cozePending > 0
|
||||
? buildCozeProgressMessage(current, total, cozePending)
|
||||
: "Coze 已回流 " + current + "/" + total + " 批次,正在生成结果文件");
|
||||
@@ -4905,6 +4934,121 @@ public class SimilarAsinTaskService {
|
||||
vo.setFileProgressMessage(snapshot.getMessage());
|
||||
}
|
||||
|
||||
private void attachPythonUploadProgress(SimilarAsinHistoryItemVo vo, Long taskId) {
|
||||
PythonUploadProgress progress = resolvePythonUploadProgress(taskId);
|
||||
int total = progress.total() > 0 ? progress.total() : Math.max(1, progress.current() + 1);
|
||||
int current = Math.max(0, Math.min(progress.current(), total));
|
||||
int percent = current <= 0 ? 1 : Math.min(95, (int) Math.floor(current * 100.0 / total));
|
||||
vo.setFileProgressCurrent(current);
|
||||
vo.setFileProgressTotal(total);
|
||||
vo.setFileProgressPercent(percent);
|
||||
vo.setFileProgressMessage(buildPythonUploadProgressMessage(current, total, progress.unit()));
|
||||
}
|
||||
|
||||
private PythonUploadProgress resolvePythonUploadProgress(Long taskId) {
|
||||
PythonUploadProgress chunkProgress = resolvePythonChunkProgress(taskId);
|
||||
if (chunkProgress.total() > 0) {
|
||||
return chunkProgress;
|
||||
}
|
||||
if (chunkProgress.current() > 0) {
|
||||
return new PythonUploadProgress(chunkProgress.current(), chunkProgress.current() + 1, chunkProgress.unit());
|
||||
}
|
||||
int totalRows = 0;
|
||||
try {
|
||||
FileTaskEntity task = fileTaskMapper.selectById(taskId);
|
||||
totalRows = allRowCount(task);
|
||||
} catch (Exception ignored) {
|
||||
totalRows = 0;
|
||||
}
|
||||
int uploadedRows = countSubmittedRows(taskId);
|
||||
if (totalRows <= 0 && uploadedRows > 0) {
|
||||
totalRows = uploadedRows + 1;
|
||||
}
|
||||
return new PythonUploadProgress(uploadedRows, totalRows, "行");
|
||||
}
|
||||
|
||||
private PythonUploadProgress resolvePythonChunkProgress(Long taskId) {
|
||||
if (taskId == null || taskId <= 0) {
|
||||
return new PythonUploadProgress(0, 0, "分片");
|
||||
}
|
||||
List<TaskScopeStateEntity> states = taskScopeStateMapper.selectList(new LambdaQueryWrapper<TaskScopeStateEntity>()
|
||||
.eq(TaskScopeStateEntity::getTaskId, taskId)
|
||||
.eq(TaskScopeStateEntity::getModuleType, MODULE_TYPE)
|
||||
.isNull(TaskScopeStateEntity::getCozeStatus)
|
||||
.orderByDesc(TaskScopeStateEntity::getUpdatedAt));
|
||||
if (states == null || states.isEmpty()) {
|
||||
return resolvePythonChunkProgressFromChunks(taskId);
|
||||
}
|
||||
int current = 0;
|
||||
int total = 0;
|
||||
for (TaskScopeStateEntity state : states) {
|
||||
if (state == null) {
|
||||
continue;
|
||||
}
|
||||
int stateTotal = state.getChunkTotal() == null ? 0 : state.getChunkTotal();
|
||||
int stateCurrent = state.getReceivedChunkCount() == null ? 0 : state.getReceivedChunkCount();
|
||||
if (stateTotal > total || total <= 0 && stateCurrent > current) {
|
||||
total = stateTotal;
|
||||
current = stateCurrent;
|
||||
}
|
||||
}
|
||||
return new PythonUploadProgress(current, total, "分片");
|
||||
}
|
||||
|
||||
private PythonUploadProgress resolvePythonChunkProgressFromChunks(Long taskId) {
|
||||
List<TaskChunkEntity> chunks = taskChunkMapper.selectList(new LambdaQueryWrapper<TaskChunkEntity>()
|
||||
.eq(TaskChunkEntity::getTaskId, taskId)
|
||||
.eq(TaskChunkEntity::getModuleType, MODULE_TYPE));
|
||||
if (chunks == null || chunks.isEmpty()) {
|
||||
return new PythonUploadProgress(0, 0, "分片");
|
||||
}
|
||||
Map<String, Integer> receivedByScope = new LinkedHashMap<>();
|
||||
Map<String, Integer> totalByScope = new LinkedHashMap<>();
|
||||
for (TaskChunkEntity chunk : chunks) {
|
||||
if (chunk == null) {
|
||||
continue;
|
||||
}
|
||||
String scopeHash = firstNonBlank(chunk.getScopeHash(), "");
|
||||
receivedByScope.merge(scopeHash, 1, Integer::sum);
|
||||
if (chunk.getChunkTotal() != null && chunk.getChunkTotal() > 0) {
|
||||
totalByScope.merge(scopeHash, chunk.getChunkTotal(), Math::max);
|
||||
}
|
||||
}
|
||||
int current = 0;
|
||||
int total = 0;
|
||||
for (Map.Entry<String, Integer> entry : receivedByScope.entrySet()) {
|
||||
int scopeCurrent = entry.getValue() == null ? 0 : entry.getValue();
|
||||
int scopeTotal = totalByScope.getOrDefault(entry.getKey(), 0);
|
||||
if (scopeTotal > total || total <= 0 && scopeCurrent > current) {
|
||||
current = scopeCurrent;
|
||||
total = scopeTotal;
|
||||
}
|
||||
}
|
||||
return new PythonUploadProgress(current, total, "分片");
|
||||
}
|
||||
|
||||
private int countSubmittedRows(Long taskId) {
|
||||
if (taskId == null || taskId <= 0) {
|
||||
return 0;
|
||||
}
|
||||
int total = 0;
|
||||
List<TaskChunkEntity> chunks = taskChunkMapper.selectList(new LambdaQueryWrapper<TaskChunkEntity>()
|
||||
.eq(TaskChunkEntity::getTaskId, taskId)
|
||||
.eq(TaskChunkEntity::getModuleType, MODULE_TYPE));
|
||||
if (chunks == null || chunks.isEmpty()) {
|
||||
return 0;
|
||||
}
|
||||
for (TaskChunkEntity chunk : chunks) {
|
||||
total += readChunkRows(chunk).size();
|
||||
}
|
||||
return total;
|
||||
}
|
||||
|
||||
private String buildPythonUploadProgressMessage(int current, int total, String unit) {
|
||||
String safeUnit = unit == null || unit.isBlank() ? "分片" : unit;
|
||||
return "等待 Python 回传,已回传 " + Math.max(0, current) + "/" + Math.max(1, total) + " " + safeUnit;
|
||||
}
|
||||
|
||||
private int calculateCozeDisplayTotal(TaskProgressSnapshotEntity snapshot, int observedCozeStates) {
|
||||
int observed = Math.max(0, observedCozeStates);
|
||||
// 小任务 totalCount <= 3 时直接使用 observed,避免 totalCount - 3 → 0 引起进度条 0/0。
|
||||
@@ -4924,15 +5068,6 @@ public class SimilarAsinTaskService {
|
||||
}
|
||||
return "Coze 已回流 " + safeCompleted + "/" + safeTotal + " 批次,等待结果文件";
|
||||
}
|
||||
private String buildUploadingCozeProgressMessage(int completed, int total, int pending) {
|
||||
int safeTotal = Math.max(1, total);
|
||||
int safeCompleted = Math.max(0, Math.min(completed, safeTotal));
|
||||
int submitted = Math.max(safeCompleted, Math.min(safeTotal, safeCompleted + Math.max(0, pending)));
|
||||
if (pending > 0) {
|
||||
return "Python 仍在回传,Coze 已提交 " + submitted + "/" + safeTotal + " 批次";
|
||||
}
|
||||
return "Python 仍在回传,Coze 已回流 " + safeCompleted + "/" + safeTotal + " 批次";
|
||||
}
|
||||
|
||||
private String fmt(LocalDateTime t) {
|
||||
return t == null ? null : t.toString();
|
||||
@@ -4968,10 +5103,11 @@ public class SimilarAsinTaskService {
|
||||
return val == null ? "" : val.replace(String.valueOf((char) 0xFEFF), "").replace((char) 0x3000, ' ').trim().replaceAll("\\s+", " ");
|
||||
}
|
||||
|
||||
private String buildParsedPayloadJson(String aiPrompt, String apiKey, List<SimilarAsinSourceFileDto> sourceFiles, List<String> headers, List<SimilarAsinParsedGroupVo> groups, List<SimilarAsinParsedRowVo> allRows) {
|
||||
private String buildParsedPayloadJson(String aiPrompt, String apiKey, Boolean imgSwitch, List<SimilarAsinSourceFileDto> sourceFiles, List<String> headers, List<SimilarAsinParsedGroupVo> groups, List<SimilarAsinParsedRowVo> allRows) {
|
||||
SimilarAsinParsedPayloadDto payload = new SimilarAsinParsedPayloadDto();
|
||||
payload.setAiPrompt(normalize(aiPrompt));
|
||||
payload.setApiKey(normalize(apiKey));
|
||||
payload.setImgSwitch(Boolean.TRUE.equals(imgSwitch));
|
||||
payload.setSourceFiles(sourceFiles == null ? List.of() : sourceFiles);
|
||||
payload.setHeaders(headers == null ? List.of() : headers);
|
||||
payload.setItems(allRows == null ? List.of() : new ArrayList<>(allRows));
|
||||
@@ -4980,10 +5116,11 @@ public class SimilarAsinTaskService {
|
||||
return writeJson(payload, "保存解析结果失败");
|
||||
}
|
||||
|
||||
private String buildTaskResultJson(String aiPrompt, String apiKey, List<SimilarAsinSourceFileDto> sourceFiles, String parsedPayloadPointer) {
|
||||
private String buildTaskResultJson(String aiPrompt, String apiKey, Boolean imgSwitch, List<SimilarAsinSourceFileDto> sourceFiles, String parsedPayloadPointer) {
|
||||
Map<String, Object> payload = new LinkedHashMap<>();
|
||||
payload.put("aiPrompt", normalize(aiPrompt));
|
||||
payload.put("apiKey", normalize(apiKey));
|
||||
payload.put("imgSwitch", Boolean.TRUE.equals(imgSwitch));
|
||||
payload.put("sourceFileKeys", sourceFiles == null ? List.of() : sourceFiles.stream()
|
||||
.map(SimilarAsinSourceFileDto::getFileKey)
|
||||
.filter(Objects::nonNull)
|
||||
|
||||
@@ -17,7 +17,7 @@ CREATE TABLE IF NOT EXISTS biz_coze_credential (
|
||||
INSERT INTO biz_coze_credential (module_type, credential_name, workflow_id, token, enabled, max_concurrent, sort_order)
|
||||
VALUES
|
||||
('APPEARANCE_PATENT', 'appearance-1', '7639685157562089513', 'Bearer sat_CztofPRhIKFKeaPZBIRxocfckqhsdCZZ45NPhkf7WOZjbX36vnVSfVV30T6u2rSX', 1, 0, 10),
|
||||
('APPEARANCE_PATENT', 'appearance-2', '7639688221823778850', 'Bearer sat_2VsTpfWXYG3EkCo8OQfLwJEWqhduCL4NEw7sSCsABppkW61kkSRPIac9coNlEnHE', 1, 0, 20),
|
||||
('APPEARANCE_PATENT', 'appearance-2', '7644952624353329202', 'Bearer sat_2VsTpfWXYG3EkCo8OQfLwJEWqhduCL4NEw7sSCsABppkW61kkSRPIac9coNlEnHE', 1, 0, 20),
|
||||
('SIMILAR_ASIN', 'similar-1', '7639708860686024756', 'Bearer sat_vqmb97BiXbMW3XVyG9htWQKjZ2F55CaW2FRrT9bDdPha7a3hxmXnMNCE1XNHdCVU', 1, 0, 10),
|
||||
('SIMILAR_ASIN', 'similar-2', '7635328462404583478', 'Bearer sat_gsInckdsN0qqhnZSK7K4Zevw6neJCHmYHyIbm85oGgJIGEmSdkH5OFctSKLsAHvT', 1, 0, 20)
|
||||
ON DUPLICATE KEY UPDATE
|
||||
|
||||
@@ -0,0 +1,4 @@
|
||||
UPDATE biz_coze_credential
|
||||
SET workflow_id = '7644952624353329202'
|
||||
WHERE module_type = 'APPEARANCE_PATENT'
|
||||
AND credential_name = 'appearance-2';
|
||||
@@ -25,28 +25,32 @@
|
||||
<div class="section-title">筛选条件</div>
|
||||
<div class="filter-zone">
|
||||
<div class="filter-row">
|
||||
<label class="filter-label">金额(手动输入)</label>
|
||||
<input
|
||||
v-model.number="filters.amount"
|
||||
type="number"
|
||||
step="0.01"
|
||||
min="0"
|
||||
class="filter-input"
|
||||
placeholder="例如:59.99"
|
||||
/>
|
||||
</div>
|
||||
<div class="filter-row">
|
||||
<label class="filter-label">排名</label>
|
||||
<input
|
||||
v-model.number="filters.rank"
|
||||
type="number"
|
||||
step="1"
|
||||
min="0"
|
||||
class="filter-input"
|
||||
placeholder="例如:100000"
|
||||
/>
|
||||
<label class="filter-label">金额区间</label>
|
||||
<div class="range-inputs">
|
||||
<input
|
||||
v-model.number="filters.minAmount"
|
||||
type="number"
|
||||
step="0.01"
|
||||
min="0"
|
||||
class="filter-input"
|
||||
placeholder="最低"
|
||||
/>
|
||||
<span class="range-separator">至</span>
|
||||
<input
|
||||
v-model.number="filters.maxAmount"
|
||||
type="number"
|
||||
step="0.01"
|
||||
min="0"
|
||||
class="filter-input"
|
||||
placeholder="最高"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div class="filter-checks">
|
||||
<label class="filter-check-row">
|
||||
<input type="checkbox" class="filter-check-input" v-model="filters.rank" />
|
||||
<span class="filter-check-text">排名</span>
|
||||
</label>
|
||||
<label class="filter-check-row">
|
||||
<input type="checkbox" class="filter-check-input" v-model="filters.fba" />
|
||||
<span class="filter-check-text">FBA</span>
|
||||
@@ -59,43 +63,29 @@
|
||||
</div>
|
||||
|
||||
<div class="section-title country-section-title">
|
||||
<span>国家与顺序</span>
|
||||
<span>采集国家</span>
|
||||
<span v-if="countryPrefSaving" class="country-pref-saving">保存中…</span>
|
||||
</div>
|
||||
<p class="hint country-pref-hint">
|
||||
勾选需要采集的国家(暂定 5 国,至少保留 1 个);上方勾选区<strong>先按已选顺序</strong>排列,其余未选国家排在后面。拖拽下方列表可调整顺序。
|
||||
每次任务只采集一个国家,选择后会自动保存,下次进入默认使用该国家。
|
||||
</p>
|
||||
<div class="country-pref-checks">
|
||||
<label v-for="row in countryCheckboxRows" :key="row.code" class="country-check-row">
|
||||
<div class="country-pref-radios">
|
||||
<label
|
||||
v-for="row in COUNTRY_OPTIONS"
|
||||
:key="row.code"
|
||||
class="country-check-row"
|
||||
:class="{ selected: selectedCountryCode === row.code }"
|
||||
>
|
||||
<input
|
||||
type="checkbox"
|
||||
type="radio"
|
||||
name="collect-data-country"
|
||||
class="country-check-input"
|
||||
:checked="isCountrySelected(row.code)"
|
||||
:disabled="isCountrySelectionLocked(row.code)"
|
||||
@change="onCountryNativeChange(row.code, $event)"
|
||||
:checked="selectedCountryCode === row.code"
|
||||
@change="selectCountry(row.code)"
|
||||
/>
|
||||
<span class="country-check-text">{{ row.label }}({{ row.code }})</span>
|
||||
</label>
|
||||
</div>
|
||||
<div v-if="orderedCountryCodes.length" class="country-order-panel">
|
||||
<div class="country-order-caption">已选顺序(拖拽 ⋮⋮ 调整)</div>
|
||||
<div class="country-order-list">
|
||||
<div
|
||||
v-for="(code, idx) in orderedCountryCodes"
|
||||
:key="code"
|
||||
class="country-drag-row"
|
||||
:class="{ dragging: dragCountryIndex === idx }"
|
||||
draggable="true"
|
||||
@dragstart="onCountryDragStart(idx)"
|
||||
@dragend="onCountryDragEnd"
|
||||
@dragover.prevent
|
||||
@drop.prevent="onCountryDrop(idx)"
|
||||
>
|
||||
<span class="drag-handle" title="拖动排序">⋮⋮</span>
|
||||
<span class="country-drag-label">{{ countryLabel(code) }}({{ code }})</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="run-row">
|
||||
<button
|
||||
@@ -235,19 +225,21 @@ const submitting = ref(false)
|
||||
const pushing = ref(false)
|
||||
|
||||
const filters = reactive<{
|
||||
amount: number | null
|
||||
rank: number | null
|
||||
minAmount: number | null
|
||||
maxAmount: number | null
|
||||
rank: boolean
|
||||
fba: boolean
|
||||
fbm: boolean
|
||||
}>({
|
||||
amount: null,
|
||||
rank: null,
|
||||
minAmount: null,
|
||||
maxAmount: null,
|
||||
rank: false,
|
||||
fba: false,
|
||||
fbm: false,
|
||||
})
|
||||
|
||||
const orderedCountryCodes = ref<string[]>(['DE', 'UK', 'FR', 'IT', 'ES'])
|
||||
const dragCountryIndex = ref<number | null>(null)
|
||||
const selectedCountryCode = ref<string>('DE')
|
||||
const selectedCountryCodes = computed(() => selectedCountryCode.value ? [selectedCountryCode.value] : [])
|
||||
/** 防止接口慢回包覆盖用户已经做出的修改 */
|
||||
const countryPrefUserTouched = ref(false)
|
||||
const countryPrefSaving = ref(false)
|
||||
@@ -304,65 +296,11 @@ function countryLabel(code: string) {
|
||||
return row?.label ?? code
|
||||
}
|
||||
|
||||
const countryCheckboxRows = computed(() => {
|
||||
const sel = orderedCountryCodes.value
|
||||
const selectedSet = new Set(sel)
|
||||
const selectedPart = sel.map((code) => ({ code, label: countryLabel(code) }))
|
||||
const rest = COUNTRY_OPTIONS.filter((o) => !selectedSet.has(o.code)).map((o) => ({
|
||||
code: o.code,
|
||||
label: o.label,
|
||||
}))
|
||||
return [...selectedPart, ...rest]
|
||||
})
|
||||
|
||||
function isCountrySelected(code: string) {
|
||||
return orderedCountryCodes.value.includes(code)
|
||||
}
|
||||
|
||||
function isCountrySelectionLocked(code: string) {
|
||||
return orderedCountryCodes.value.length === 1 && orderedCountryCodes.value[0] === code
|
||||
}
|
||||
|
||||
function onCountryNativeChange(code: string, e: Event) {
|
||||
const el = e.target as HTMLInputElement | null
|
||||
if (!el) return
|
||||
if (!el.checked && isCountrySelectionLocked(code)) {
|
||||
el.checked = true
|
||||
ElMessage.warning('至少保留 1 个国家')
|
||||
return
|
||||
}
|
||||
function selectCountry(code: string) {
|
||||
if (!COUNTRY_OPTIONS.some((row) => row.code === code)) return
|
||||
if (selectedCountryCode.value === code) return
|
||||
countryPrefUserTouched.value = true
|
||||
if (el.checked) {
|
||||
if (!orderedCountryCodes.value.includes(code)) {
|
||||
orderedCountryCodes.value = [...orderedCountryCodes.value, code]
|
||||
}
|
||||
} else {
|
||||
if (orderedCountryCodes.value.length <= 1) {
|
||||
ElMessage.warning('至少保留 1 个国家')
|
||||
return
|
||||
}
|
||||
orderedCountryCodes.value = orderedCountryCodes.value.filter((c) => c !== code)
|
||||
}
|
||||
scheduleSaveCountryPref()
|
||||
}
|
||||
|
||||
function onCountryDragStart(index: number) {
|
||||
dragCountryIndex.value = index
|
||||
}
|
||||
|
||||
function onCountryDragEnd() {
|
||||
dragCountryIndex.value = null
|
||||
}
|
||||
|
||||
function onCountryDrop(toIndex: number) {
|
||||
const from = dragCountryIndex.value
|
||||
dragCountryIndex.value = null
|
||||
if (from == null || from === toIndex) return
|
||||
countryPrefUserTouched.value = true
|
||||
const arr = [...orderedCountryCodes.value]
|
||||
const [item] = arr.splice(from, 1)
|
||||
arr.splice(toIndex, 0, item)
|
||||
orderedCountryCodes.value = arr
|
||||
selectedCountryCode.value = code
|
||||
scheduleSaveCountryPref()
|
||||
}
|
||||
|
||||
@@ -377,10 +315,10 @@ function scheduleSaveCountryPref() {
|
||||
}
|
||||
|
||||
async function persistCountryPref() {
|
||||
if (!orderedCountryCodes.value.length) return
|
||||
if (!selectedCountryCode.value) return
|
||||
countryPrefSaving.value = true
|
||||
try {
|
||||
await putCollectDataCountryPreference([...orderedCountryCodes.value])
|
||||
await putCollectDataCountryPreference([selectedCountryCode.value])
|
||||
} catch (error) {
|
||||
ElMessage.error(error instanceof Error ? error.message : '保存国家顺序失败')
|
||||
} finally {
|
||||
@@ -394,7 +332,7 @@ async function loadCountryPreference() {
|
||||
if (countryPrefUserTouched.value) return
|
||||
const codes = vo?.country_codes
|
||||
if (Array.isArray(codes) && codes.length) {
|
||||
orderedCountryCodes.value = [...codes]
|
||||
selectedCountryCode.value = String(codes[0] || 'DE')
|
||||
}
|
||||
} catch {
|
||||
/* 接口失败时保留本地默认 */
|
||||
@@ -464,16 +402,24 @@ async function selectFolder() {
|
||||
}
|
||||
|
||||
function buildFiltersPayload() {
|
||||
const minAmount = filters.minAmount === null || filters.minAmount === undefined || Number.isNaN(filters.minAmount)
|
||||
? null
|
||||
: filters.minAmount
|
||||
const maxAmount = filters.maxAmount === null || filters.maxAmount === undefined || Number.isNaN(filters.maxAmount)
|
||||
? null
|
||||
: filters.maxAmount
|
||||
return {
|
||||
amount: filters.amount === null || filters.amount === undefined || Number.isNaN(filters.amount)
|
||||
? null
|
||||
: filters.amount,
|
||||
rank: filters.rank === null || filters.rank === undefined || Number.isNaN(filters.rank)
|
||||
? null
|
||||
: filters.rank,
|
||||
amount: maxAmount ?? minAmount,
|
||||
minAmount,
|
||||
maxAmount,
|
||||
min_amount: minAmount,
|
||||
max_amount: maxAmount,
|
||||
rank: !!filters.rank,
|
||||
fba: !!filters.fba,
|
||||
fbm: !!filters.fbm,
|
||||
countryCodes: [...orderedCountryCodes.value],
|
||||
countryCodes: [...selectedCountryCodes.value],
|
||||
countryCode: selectedCountryCode.value,
|
||||
country_code: selectedCountryCode.value,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -482,8 +428,8 @@ async function submitCollect() {
|
||||
ElMessage.warning('请先选择 Excel 文件')
|
||||
return
|
||||
}
|
||||
if (!orderedCountryCodes.value.length) {
|
||||
ElMessage.warning('至少保留 1 个国家')
|
||||
if (!selectedCountryCode.value) {
|
||||
ElMessage.warning('请选择采集国家')
|
||||
return
|
||||
}
|
||||
submitting.value = true
|
||||
@@ -722,6 +668,7 @@ onBeforeUnmount(() => {
|
||||
}
|
||||
.filter-input {
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
padding: 6px 10px;
|
||||
background: #1e1e1e;
|
||||
border: 1px solid #3a3a3a;
|
||||
@@ -731,6 +678,8 @@ onBeforeUnmount(() => {
|
||||
outline: none;
|
||||
}
|
||||
.filter-input:focus { border-color: #3498db; }
|
||||
.range-inputs { flex: 1; min-width: 0; display: flex; align-items: center; gap: 8px; }
|
||||
.range-separator { color: #777; font-size: 12px; white-space: nowrap; }
|
||||
.filter-checks { display: flex; gap: 16px; }
|
||||
.filter-check-row { display: inline-flex; align-items: center; gap: 6px; cursor: pointer; user-select: none; color: #ccc; font-size: 12px; }
|
||||
.filter-check-input { width: 16px; height: 16px; accent-color: #409eff; cursor: pointer; }
|
||||
@@ -738,17 +687,11 @@ onBeforeUnmount(() => {
|
||||
.country-pref-hint { margin-top: -4px; }
|
||||
.country-section-title { display: flex; align-items: center; justify-content: space-between; gap: 12px; }
|
||||
.country-pref-saving { color: #3498db; font-size: 11px; }
|
||||
.country-pref-checks { display: flex; flex-wrap: wrap; gap: 10px 16px; margin-bottom: 12px; }
|
||||
.country-check-row { display: inline-flex; align-items: center; gap: 8px; cursor: pointer; user-select: none; font-size: 12px; color: #ccc; }
|
||||
.country-pref-radios { display: grid; grid-template-columns: repeat(2, minmax(0, 1fr)); gap: 8px; margin-bottom: 14px; }
|
||||
.country-check-row { display: inline-flex; align-items: center; gap: 8px; cursor: pointer; user-select: none; font-size: 12px; color: #ccc; padding: 8px 10px; border: 1px solid #333; border-radius: 8px; background: #252525; }
|
||||
.country-check-row.selected { border-color: #409eff; background: rgba(64, 158, 255, .12); color: #e6f2ff; }
|
||||
.country-check-input { width: 16px; height: 16px; accent-color: #409eff; cursor: pointer; flex-shrink: 0; }
|
||||
.country-check-text { line-height: 1.3; }
|
||||
.country-order-panel { border: 1px solid #333; border-radius: 8px; padding: 10px 12px; background: #252525; margin-bottom: 14px; }
|
||||
.country-order-caption { font-size: 11px; color: #777; margin-bottom: 8px; }
|
||||
.country-order-list { display: flex; flex-direction: column; gap: 6px; }
|
||||
.country-drag-row { display: flex; align-items: center; gap: 10px; padding: 8px 10px; background: #2c2c2c; border-radius: 6px; border: 1px solid #3a3a3a; cursor: grab; user-select: none; }
|
||||
.country-drag-row.dragging { opacity: 0.55; border-color: #3498db; }
|
||||
.drag-handle { color: #666; font-size: 12px; letter-spacing: -2px; }
|
||||
.country-drag-label { font-size: 12px; color: #ccc; }
|
||||
|
||||
.queue-debug-card { margin-top: 14px; border: 1px solid #2a2a2a; border-radius: 8px; padding: 10px 12px; background: #1f1f1f; }
|
||||
.queue-debug-title { margin-top: 0; }
|
||||
|
||||
@@ -17,6 +17,18 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="condition-card">
|
||||
<div class="section-title">检测条件</div>
|
||||
<label class="switch-row">
|
||||
<span>
|
||||
<strong>图片检测</strong>
|
||||
<em>开启后请求 Coze 时携带 img_switch=true</em>
|
||||
</span>
|
||||
<input v-model="imgSwitch" type="checkbox" />
|
||||
<i></i>
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<div class="run-row">
|
||||
<button type="button" class="btn-run" :disabled="parsing || !uploadedFiles.length" @click="parseFiles">
|
||||
{{ parsing ? '解析中...' : '解析并创建任务' }}
|
||||
@@ -171,6 +183,7 @@ type TaskSummary = Pick<SimilarAsinParseVo, 'taskId' | 'totalRows' | 'acceptedRo
|
||||
const queuedTaskSummary = ref<TaskSummary | null>(null)
|
||||
const parsing = ref(false)
|
||||
const pushing = ref(false)
|
||||
const imgSwitch = ref(false)
|
||||
const queuePayloadText = ref('')
|
||||
const pollingTaskIds = ref<number[]>([])
|
||||
const pendingFileTaskIds = ref<number[]>([])
|
||||
@@ -374,7 +387,7 @@ async function parseFiles() {
|
||||
originalFilename: f.originalFilename,
|
||||
relativePath: f.relativePath,
|
||||
}))
|
||||
const res = await parseSimilarAsin(files, effectiveCozeApiKey())
|
||||
const res = await parseSimilarAsin(files, effectiveCozeApiKey(), imgSwitch.value)
|
||||
parseResult.value = res
|
||||
queuedTaskSummary.value = null
|
||||
queuePayloadText.value = ''
|
||||
@@ -874,6 +887,16 @@ onUnmounted(() => {
|
||||
.right-panel { flex: 1; min-width: 0; background: #1a1a1a; display: flex; flex-direction: column; }
|
||||
.section-title, .subsection-title { font-size: 13px; color: #bbb; margin-bottom: 10px; }
|
||||
.upload-zone { border: 1px dashed #3a3a3a; border-radius: 10px; padding: 18px; background: #252525; margin-bottom: 18px; }
|
||||
.condition-card { margin-bottom: 18px; padding: 14px 16px; border: 1px solid #2f3a34; border-radius: 10px; background: linear-gradient(135deg, #222a25, #202020); }
|
||||
.switch-row { display: flex; align-items: center; justify-content: space-between; gap: 14px; color: #d8e4dc; cursor: pointer; user-select: none; }
|
||||
.switch-row span { display: flex; flex-direction: column; gap: 4px; min-width: 0; }
|
||||
.switch-row strong { font-size: 13px; font-weight: 600; }
|
||||
.switch-row em { color: #8d9a91; font-size: 12px; font-style: normal; line-height: 1.4; }
|
||||
.switch-row input { position: absolute; opacity: 0; pointer-events: none; }
|
||||
.switch-row i { position: relative; width: 46px; height: 24px; flex: 0 0 auto; border-radius: 999px; background: #3a3a3a; box-shadow: inset 0 0 0 1px #4b4b4b; transition: background .2s ease, box-shadow .2s ease; }
|
||||
.switch-row i::after { content: ''; position: absolute; top: 3px; left: 3px; width: 18px; height: 18px; border-radius: 50%; background: #c7c7c7; transition: transform .2s ease, background .2s ease; }
|
||||
.switch-row input:checked + i { background: #27ae60; box-shadow: inset 0 0 0 1px #42d17a; }
|
||||
.switch-row input:checked + i::after { transform: translateX(22px); background: #fff; }
|
||||
.hint, .loading-msg, .files, .muted { color: #888; font-size: 12px; line-height: 1.5; }
|
||||
.link { color: #6ea8fe; text-decoration: none; }
|
||||
.link:hover { color: #9fc5ff; }
|
||||
|
||||
@@ -1718,6 +1718,7 @@ export interface SimilarAsinParseVo {
|
||||
droppedRows: number;
|
||||
groupCount?: number;
|
||||
aiPrompt?: string;
|
||||
imgSwitch?: boolean;
|
||||
items: SimilarAsinParsedRow[];
|
||||
groups?: SimilarAsinParsedGroup[];
|
||||
}
|
||||
@@ -1776,15 +1777,16 @@ export interface SimilarAsinTaskBatchVo {
|
||||
missingTaskIds?: number[];
|
||||
}
|
||||
|
||||
export function parseSimilarAsin(files: UploadedFileRef[], apiKey?: string) {
|
||||
export function parseSimilarAsin(files: UploadedFileRef[], apiKey?: string, imgSwitch = false) {
|
||||
return unwrapJavaResponse(
|
||||
post<
|
||||
JavaApiResponse<SimilarAsinParseVo>,
|
||||
{ user_id: number; files: UploadedFileRef[]; api_key?: string }
|
||||
{ user_id: number; files: UploadedFileRef[]; api_key?: string; img_switch: boolean }
|
||||
>(`${JAVA_API_PREFIX}/similar-asin/parse`, {
|
||||
user_id: getCurrentUserId(),
|
||||
files,
|
||||
api_key: apiKey,
|
||||
img_switch: imgSwitch,
|
||||
}),
|
||||
);
|
||||
}
|
||||
@@ -2231,10 +2233,16 @@ export interface CollectDataSourceFile {
|
||||
|
||||
export interface CollectDataFilters {
|
||||
amount?: number | string | null;
|
||||
rank?: number | null;
|
||||
minAmount?: number | string | null;
|
||||
maxAmount?: number | string | null;
|
||||
min_amount?: number | string | null;
|
||||
max_amount?: number | string | null;
|
||||
rank?: boolean | null;
|
||||
fba?: boolean | null;
|
||||
fbm?: boolean | null;
|
||||
countryCodes?: string[];
|
||||
countryCode?: string | null;
|
||||
country_code?: string | null;
|
||||
}
|
||||
|
||||
export interface CollectDataParseRequest {
|
||||
|
||||
Reference in New Issue
Block a user