提交货源采集更新

This commit is contained in:
super
2026-05-10 01:00:08 +08:00
parent a4e4745921
commit 5c990e651e
23 changed files with 308 additions and 37 deletions

View File

@@ -13,8 +13,8 @@ client_name=ShuFuAI
# java_api_base=http://47.111.163.154:18080 # java_api_base=http://47.111.163.154:18080
# java_api_base=http://127.0.0.1:18080 java_api_base=http://127.0.0.1:18080
# java_api_base=http://8.136.19.173:18080 # java_api_base=http://8.136.19.173:18080
java_api_base=http://121.196.149.225:18080 # java_api_base=http://121.196.149.225:18080

View File

@@ -9,7 +9,7 @@ public class SimilarAsinProperties {
private String cozeBaseUrl = "https://api.coze.cn"; private String cozeBaseUrl = "https://api.coze.cn";
private String cozeWorkflowPath = "/v1/workflow/run"; private String cozeWorkflowPath = "/v1/workflow/run";
private String cozeWorkflowHistoryPath = "/v1/workflows/{workflow_id}/run_histories/{execute_id}"; private String cozeWorkflowHistoryPath = "/v1/workflows/{workflow_id}/run_histories/{execute_id}";
private String cozeWorkflowId = "7632683471312355338"; private String cozeWorkflowId = "7635328462404583478";
private String cozeToken = ""; private String cozeToken = "";
private int cozeBatchSize = 50; private int cozeBatchSize = 50;
private int cozeConnectTimeoutMillis = 10000; private int cozeConnectTimeoutMillis = 10000;

View File

@@ -556,7 +556,6 @@ public class AppearancePatentTaskService {
: row.getResultFilename(); : row.getResultFilename();
} }
@Scheduled(cron = "${aiimage.appearance-patent.stale-finalize-cron:0 */2 * * * *}")
public void finalizeStaleTasks() { public void finalizeStaleTasks() {
if (transactionManager != null) { if (transactionManager != null) {
LocalDateTime threshold = LocalDateTime.now().minusMinutes(Math.max(1, properties.getStaleTimeoutMinutes())); LocalDateTime threshold = LocalDateTime.now().minusMinutes(Math.max(1, properties.getStaleTimeoutMinutes()));

View File

@@ -1209,7 +1209,6 @@ public class BrandTaskService {
} }
@Transactional @Transactional
@org.springframework.scheduling.annotation.Scheduled(cron = "${aiimage.brand-progress.stale-check-cron:0 */2 * * * *}")
public void failStaleRunningTasks() { public void failStaleRunningTasks() {
DistributedJobLockService.LockHandle lockHandle = distributedJobLockService.tryLock("brand:stale-check", STALE_CHECK_LOCK_TTL); DistributedJobLockService.LockHandle lockHandle = distributedJobLockService.tryLock("brand:stale-check", STALE_CHECK_LOCK_TTL);
if (lockHandle == null) { if (lockHandle == null) {

View File

@@ -4,6 +4,8 @@ import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.baomidou.mybatisplus.core.conditions.update.LambdaUpdateWrapper; import com.baomidou.mybatisplus.core.conditions.update.LambdaUpdateWrapper;
import com.nanri.aiimage.common.service.DistributedJobLockService; import com.nanri.aiimage.common.service.DistributedJobLockService;
import com.nanri.aiimage.config.DeleteBrandProgressProperties; import com.nanri.aiimage.config.DeleteBrandProgressProperties;
import com.nanri.aiimage.modules.appearancepatent.service.AppearancePatentTaskService;
import com.nanri.aiimage.modules.brand.service.BrandTaskService;
import com.nanri.aiimage.modules.productrisk.service.ProductRiskTaskCacheService; import com.nanri.aiimage.modules.productrisk.service.ProductRiskTaskCacheService;
import com.nanri.aiimage.modules.queryasin.service.QueryAsinTaskCacheService; import com.nanri.aiimage.modules.queryasin.service.QueryAsinTaskCacheService;
import com.nanri.aiimage.modules.queryasin.service.QueryAsinTaskService; import com.nanri.aiimage.modules.queryasin.service.QueryAsinTaskService;
@@ -12,6 +14,7 @@ import com.nanri.aiimage.modules.patroldelete.service.PatrolDeleteTaskCacheServi
import com.nanri.aiimage.modules.patroldelete.service.PatrolDeleteTaskService; import com.nanri.aiimage.modules.patroldelete.service.PatrolDeleteTaskService;
import com.nanri.aiimage.modules.pricetrack.service.PriceTrackTaskCacheService; import com.nanri.aiimage.modules.pricetrack.service.PriceTrackTaskCacheService;
import com.nanri.aiimage.modules.pricetrack.service.PriceTrackTaskService; import com.nanri.aiimage.modules.pricetrack.service.PriceTrackTaskService;
import com.nanri.aiimage.modules.similarasin.service.SimilarAsinTaskService;
import com.nanri.aiimage.modules.shopmatch.service.ShopMatchTaskCacheService; import com.nanri.aiimage.modules.shopmatch.service.ShopMatchTaskCacheService;
import com.nanri.aiimage.modules.shopmatch.service.ShopMatchTaskService; import com.nanri.aiimage.modules.shopmatch.service.ShopMatchTaskService;
import com.nanri.aiimage.modules.task.mapper.FileTaskMapper; import com.nanri.aiimage.modules.task.mapper.FileTaskMapper;
@@ -63,6 +66,9 @@ public class DeleteBrandStaleTaskService {
private final PatrolDeleteTaskCacheService patrolDeleteTaskCacheService; private final PatrolDeleteTaskCacheService patrolDeleteTaskCacheService;
private final QueryAsinTaskService queryAsinTaskService; private final QueryAsinTaskService queryAsinTaskService;
private final QueryAsinTaskCacheService queryAsinTaskCacheService; private final QueryAsinTaskCacheService queryAsinTaskCacheService;
private final BrandTaskService brandTaskService;
private final AppearancePatentTaskService appearancePatentTaskService;
private final SimilarAsinTaskService similarAsinTaskService;
private final DeleteBrandProgressProperties deleteBrandProgressProperties; private final DeleteBrandProgressProperties deleteBrandProgressProperties;
private final DistributedJobLockService distributedJobLockService; private final DistributedJobLockService distributedJobLockService;
private final TaskDistributedLockService taskDistributedLockService; private final TaskDistributedLockService taskDistributedLockService;
@@ -88,6 +94,9 @@ public class DeleteBrandStaleTaskService {
ShopMatchStaleCheckStats shopMatchStats = failStaleShopMatchTasks(); ShopMatchStaleCheckStats shopMatchStats = failStaleShopMatchTasks();
ShopMatchStaleCheckStats patrolDeleteStats = failStalePatrolDeleteTasks(); ShopMatchStaleCheckStats patrolDeleteStats = failStalePatrolDeleteTasks();
ShopMatchStaleCheckStats queryAsinStats = failStaleQueryAsinTasks(); ShopMatchStaleCheckStats queryAsinStats = failStaleQueryAsinTasks();
runModuleStaleCheck("brand", brandTaskService::failStaleRunningTasks);
runModuleStaleCheck("appearance-patent", appearancePatentTaskService::finalizeStaleTasks);
runModuleStaleCheck("similar-asin", similarAsinTaskService::finalizeStaleTasks);
log.info("[stale-check] product-risk summary scanned={} finalized={} failed={} skipped={} elapsedMs={} thread={}", log.info("[stale-check] product-risk summary scanned={} finalized={} failed={} skipped={} elapsedMs={} thread={}",
stats.scannedTaskCount, stats.scannedTaskCount,
stats.finalizedTaskCount, stats.finalizedTaskCount,
@@ -126,6 +135,23 @@ public class DeleteBrandStaleTaskService {
} }
} }
private void runModuleStaleCheck(String moduleName, Runnable action) {
long startedAt = System.currentTimeMillis();
try {
action.run();
log.info("[stale-check] {} delegated stale check completed elapsedMs={} thread={}",
moduleName,
System.currentTimeMillis() - startedAt,
Thread.currentThread().getName());
} catch (Exception ex) {
log.warn("[stale-check] {} delegated stale check failed elapsedMs={} msg={}",
moduleName,
System.currentTimeMillis() - startedAt,
ex.getMessage(),
ex);
}
}
private void failStaleDeleteBrandTasks() { private void failStaleDeleteBrandTasks() {
long minutes = Math.max(1L, deleteBrandProgressProperties.getHeartbeatTimeoutMinutes()); long minutes = Math.max(1L, deleteBrandProgressProperties.getHeartbeatTimeoutMinutes());
long initialMinutes = Math.max(minutes, deleteBrandProgressProperties.getDeleteBrandInitialTimeoutMinutes()); long initialMinutes = Math.max(minutes, deleteBrandProgressProperties.getDeleteBrandInitialTimeoutMinutes());

View File

@@ -210,6 +210,9 @@ public class SimilarAsinCozeClient {
Map<String, Object> body = new LinkedHashMap<>(); Map<String, Object> body = new LinkedHashMap<>();
body.put("workflow_id", properties.getCozeWorkflowId()); body.put("workflow_id", properties.getCozeWorkflowId());
body.put("parameters", parameters); body.put("parameters", parameters);
if (apiKey != null && !apiKey.isBlank()) {
body.put("api_key", apiKey.trim());
}
body.put("is_async", Boolean.TRUE); body.put("is_async", Boolean.TRUE);
log.info("[similar-asin] coze request url={} body={}", log.info("[similar-asin] coze request url={} body={}",
joinUrl(properties.getCozeBaseUrl(), properties.getCozeWorkflowPath()), joinUrl(properties.getCozeBaseUrl(), properties.getCozeWorkflowPath()),
@@ -256,20 +259,12 @@ public class SimilarAsinCozeClient {
} }
private Map<String, Object> buildParameters(List<SimilarAsinResultRowDto> rows, String prompt, String apiKey) { private Map<String, Object> buildParameters(List<SimilarAsinResultRowDto> rows, String prompt, String apiKey) {
List<String> groupKeys = rows.stream().map(row -> nonBlank(row.getGroupKey(), rowKey(row))).toList();
List<String> rowIds = rows.stream().map(row -> nonBlank(row.getId(), "")).toList();
List<String> asins = rows.stream().map(row -> nonBlank(row.getAsin(), "")).toList(); List<String> asins = rows.stream().map(row -> nonBlank(row.getAsin(), "")).toList();
List<String> countries = rows.stream().map(row -> nonBlank(row.getCountry(), "")).toList();
List<String> prices = rows.stream().map(row -> nonBlank(row.getPrice(), "")).toList();
List<String> titles = rows.stream().map(row -> nonBlank(row.getTitle(), row.getAsin())).toList();
List<String> urls = rows.stream().map(row -> nonBlank(row.getUrl(), "")).toList(); List<String> urls = rows.stream().map(row -> nonBlank(row.getUrl(), "")).toList();
List<List<String>> urlLists = rows.stream().map(SimilarAsinResultRowDto::getUrls).toList(); List<List<String>> urlLists = rows.stream().map(SimilarAsinResultRowDto::getUrls).toList();
Map<String, Object> parameters = new LinkedHashMap<>(); Map<String, Object> parameters = new LinkedHashMap<>();
parameters.put("title_list", titles); parameters.put("items", buildItemObjects(rows, asins, urls, urlLists));
parameters.put("url_list", urls);
parameters.put("url_lists", urlLists);
parameters.put("items", buildItemObjects(rows, groupKeys, rowIds, asins, countries, prices, titles, urls, urlLists));
parameters.put("prompt", prompt == null ? "" : prompt); parameters.put("prompt", prompt == null ? "" : prompt);
if (apiKey != null && !apiKey.isBlank()) { if (apiKey != null && !apiKey.isBlank()) {
parameters.put("api_key", apiKey.trim()); parameters.put("api_key", apiKey.trim());
@@ -280,6 +275,10 @@ public class SimilarAsinCozeClient {
@SuppressWarnings("unchecked") @SuppressWarnings("unchecked")
private Map<String, Object> maskCozeRequestBody(Map<String, Object> body) { private Map<String, Object> maskCozeRequestBody(Map<String, Object> body) {
Map<String, Object> masked = new LinkedHashMap<>(body); Map<String, Object> masked = new LinkedHashMap<>(body);
Object topLevelApiKey = masked.get("api_key");
if (topLevelApiKey instanceof String apiKeyText && !apiKeyText.isBlank()) {
masked.put("api_key", maskSecret(apiKeyText));
}
Object parametersObj = masked.get("parameters"); Object parametersObj = masked.get("parameters");
if (parametersObj instanceof Map<?, ?> parameters) { if (parametersObj instanceof Map<?, ?> parameters) {
Map<String, Object> maskedParameters = new LinkedHashMap<>((Map<String, Object>) parameters); Map<String, Object> maskedParameters = new LinkedHashMap<>((Map<String, Object>) parameters);
@@ -304,25 +303,14 @@ public class SimilarAsinCozeClient {
} }
private List<Map<String, Object>> buildItemObjects(List<SimilarAsinResultRowDto> rows, private List<Map<String, Object>> buildItemObjects(List<SimilarAsinResultRowDto> rows,
List<String> groupKeys,
List<String> rowIds,
List<String> asins, List<String> asins,
List<String> countries,
List<String> prices,
List<String> titles,
List<String> urls, List<String> urls,
List<List<String>> urlLists) { List<List<String>> urlLists) {
List<Map<String, Object>> items = new ArrayList<>(rows.size()); List<Map<String, Object>> items = new ArrayList<>(rows.size());
for (int i = 0; i < rows.size(); i++) { for (int i = 0; i < rows.size(); i++) {
Map<String, Object> item = new LinkedHashMap<>(); Map<String, Object> item = new LinkedHashMap<>();
item.put("group_key", groupKeys.get(i));
item.put("row_id", rowIds.get(i));
item.put("asin", asins.get(i)); item.put("asin", asins.get(i));
item.put("country", countries.get(i));
item.put("price", prices.get(i));
item.put("title", titles.get(i));
item.put("url", urls.get(i)); item.put("url", urls.get(i));
item.put("urls", urlLists.get(i));
item.put("target_urls", urlLists.get(i)); item.put("target_urls", urlLists.get(i));
items.add(item); items.add(item);
} }

View File

@@ -616,7 +616,6 @@ public class SimilarAsinTaskService {
: row.getResultFilename(); : row.getResultFilename();
} }
@Scheduled(cron = "${aiimage.similar-asin.stale-finalize-cron:0 */2 * * * *}")
public void finalizeStaleTasks() { public void finalizeStaleTasks() {
if (transactionManager != null) { if (transactionManager != null) {
LocalDateTime threshold = LocalDateTime.now().minusMinutes(Math.max(1, properties.getStaleTimeoutMinutes())); LocalDateTime threshold = LocalDateTime.now().minusMinutes(Math.max(1, properties.getStaleTimeoutMinutes()));
@@ -1429,6 +1428,10 @@ public class SimilarAsinTaskService {
SimilarAsinCozeClient.CozeSubmitResponse submit = submitCozeWorkflowThrottled(batchRows, prompt, apiKey); SimilarAsinCozeClient.CozeSubmitResponse submit = submitCozeWorkflowThrottled(batchRows, prompt, apiKey);
if (submit.immediateData() != null && !submit.immediateData().isBlank()) { if (submit.immediateData() != null && !submit.immediateData().isBlank()) {
List<SimilarAsinResultRowDto> cozeRows = cozeClient.mergeRowsFromDataText(batchRows, submit.immediateData()); List<SimilarAsinResultRowDto> cozeRows = cozeClient.mergeRowsFromDataText(batchRows, submit.immediateData());
String emptyResultMessage = emptyCozeResultMessage(cozeRows, batchRows.size());
if (!emptyResultMessage.isBlank()) {
throw new IllegalStateException(emptyResultMessage);
}
mergeCozeRowsIntoChunk(task, chunk.getScopeHash(), chunk.getChunkIndex(), cozeRows, allRowsByBaseId); mergeCozeRowsIntoChunk(task, chunk.getScopeHash(), chunk.getChunkIndex(), cozeRows, allRowsByBaseId);
return false; return false;
} }
@@ -1570,15 +1573,22 @@ public class SimilarAsinTaskService {
if (batchRows.isEmpty() && failureMessage.isBlank()) { if (batchRows.isEmpty() && failureMessage.isBlank()) {
failureMessage = "Coze batch payload missing"; failureMessage = "Coze batch payload missing";
} }
List<SimilarAsinResultRowDto> cozeRows;
if (failureMessage.isBlank()) {
cozeRows = cozeClient.mergeRowsFromDataText(batchRows, poll.resolvedPayloadText());
failureMessage = emptyCozeResultMessage(cozeRows, batchRows.size());
} else {
cozeRows = List.of();
}
if (!failureMessage.isBlank() && splitRetryFailedCozeBatchState(state, context, batchRows, failureMessage)) { if (!failureMessage.isBlank() && splitRetryFailedCozeBatchState(state, context, batchRows, failureMessage)) {
return; return;
} }
if (!failureMessage.isBlank() && retryFailedCozeBatchState(state, context, batchRows, failureMessage)) { if (!failureMessage.isBlank() && retryFailedCozeBatchState(state, context, batchRows, failureMessage)) {
return; return;
} }
List<SimilarAsinResultRowDto> cozeRows = failureMessage.isBlank() if (!failureMessage.isBlank()) {
? cozeClient.mergeRowsFromDataText(batchRows, poll.resolvedPayloadText()) cozeRows = cozeClient.markRowsFailed(batchRows, failureMessage);
: cozeClient.markRowsFailed(batchRows, failureMessage); }
FileTaskEntity task = fileTaskMapper.selectById(state.getTaskId()); FileTaskEntity task = fileTaskMapper.selectById(state.getTaskId());
if (task != null) { if (task != null) {
Map<String, List<SimilarAsinParsedRowVo>> allRowsByBaseId = loadAllRowsByBaseId(task); Map<String, List<SimilarAsinParsedRowVo>> allRowsByBaseId = loadAllRowsByBaseId(task);
@@ -1819,6 +1829,7 @@ public class SimilarAsinTaskService {
String normalized = normalize(failureMessage).toLowerCase(Locale.ROOT); String normalized = normalize(failureMessage).toLowerCase(Locale.ROOT);
return normalized.contains("timeout") return normalized.contains("timeout")
|| normalized.contains("timed out") || normalized.contains("timed out")
|| normalized.contains("empty result rows")
|| normalized.contains("out of limit") || normalized.contains("out of limit")
|| normalized.contains("execution limit") || normalized.contains("execution limit")
|| normalized.contains("720712008") || normalized.contains("720712008")
@@ -1834,6 +1845,7 @@ public class SimilarAsinTaskService {
|| normalized.contains("retry later") || normalized.contains("retry later")
|| normalized.contains("timeout") || normalized.contains("timeout")
|| normalized.contains("timed out") || normalized.contains("timed out")
|| normalized.contains("empty result rows")
|| normalized.contains("out of limit") || normalized.contains("out of limit")
|| normalized.contains("execution limit") || normalized.contains("execution limit")
|| normalized.contains("702093018") || normalized.contains("702093018")
@@ -1846,6 +1858,21 @@ public class SimilarAsinTaskService {
|| normalized.contains("\u8c03\u7528\u8d85\u65f6"); || normalized.contains("\u8c03\u7528\u8d85\u65f6");
} }
private String emptyCozeResultMessage(List<SimilarAsinResultRowDto> cozeRows, int expectedRows) {
if (cozeRows == null || cozeRows.isEmpty()) {
return expectedRows > 0 ? "Coze async workflow returned empty result rows" : "";
}
long unresolved = cozeRows.stream()
.filter(row -> row != null
&& !hasResolvedCozeFields(row)
&& !isTechnicalCozeFailure(row.getError()))
.count();
if (unresolved <= 0) {
return "";
}
return "Coze async workflow returned empty result rows: " + unresolved + "/" + Math.max(expectedRows, cozeRows.size());
}
private void updateCozeStateRunning(TaskScopeStateEntity state, String error) { private void updateCozeStateRunning(TaskScopeStateEntity state, String error) {
int updated = taskScopeStateMapper.update(null, new LambdaUpdateWrapper<TaskScopeStateEntity>() int updated = taskScopeStateMapper.update(null, new LambdaUpdateWrapper<TaskScopeStateEntity>()
.eq(TaskScopeStateEntity::getId, state.getId()) .eq(TaskScopeStateEntity::getId, state.getId())

View File

@@ -36,7 +36,7 @@ AIIMAGE_APPEARANCE_PATENT_STALE_TIMEOUT_MINUTES=20
AIIMAGE_SIMILAR_ASIN_COZE_BASE_URL=https://api.coze.cn AIIMAGE_SIMILAR_ASIN_COZE_BASE_URL=https://api.coze.cn
AIIMAGE_SIMILAR_ASIN_COZE_WORKFLOW_PATH=/v1/workflow/run AIIMAGE_SIMILAR_ASIN_COZE_WORKFLOW_PATH=/v1/workflow/run
AIIMAGE_SIMILAR_ASIN_COZE_WORKFLOW_ID=7632683471312355338 AIIMAGE_SIMILAR_ASIN_COZE_WORKFLOW_ID=7635328462404583478
AIIMAGE_SIMILAR_ASIN_COZE_TOKEN= AIIMAGE_SIMILAR_ASIN_COZE_TOKEN=
AIIMAGE_SIMILAR_ASIN_COZE_BATCH_SIZE=50 AIIMAGE_SIMILAR_ASIN_COZE_BATCH_SIZE=50
AIIMAGE_SIMILAR_ASIN_COZE_READ_TIMEOUT_MILLIS=60000 AIIMAGE_SIMILAR_ASIN_COZE_READ_TIMEOUT_MILLIS=60000

View File

@@ -157,7 +157,7 @@ aiimage:
similar-asin: similar-asin:
coze-base-url: ${AIIMAGE_SIMILAR_ASIN_COZE_BASE_URL:https://api.coze.cn} coze-base-url: ${AIIMAGE_SIMILAR_ASIN_COZE_BASE_URL:https://api.coze.cn}
coze-workflow-path: ${AIIMAGE_SIMILAR_ASIN_COZE_WORKFLOW_PATH:/v1/workflow/run} coze-workflow-path: ${AIIMAGE_SIMILAR_ASIN_COZE_WORKFLOW_PATH:/v1/workflow/run}
coze-workflow-id: ${AIIMAGE_SIMILAR_ASIN_COZE_WORKFLOW_ID:7632683471312355338} coze-workflow-id: ${AIIMAGE_SIMILAR_ASIN_COZE_WORKFLOW_ID:7635328462404583478}
coze-token: ${AIIMAGE_SIMILAR_ASIN_COZE_TOKEN:Bearer sat_gsInckdsN0qqhnZSK7K4Zevw6neJCHmYHyIbm85oGgJIGEmSdkH5OFctSKLsAHvT} coze-token: ${AIIMAGE_SIMILAR_ASIN_COZE_TOKEN:Bearer sat_gsInckdsN0qqhnZSK7K4Zevw6neJCHmYHyIbm85oGgJIGEmSdkH5OFctSKLsAHvT}
coze-batch-size: ${AIIMAGE_SIMILAR_ASIN_COZE_BATCH_SIZE:50} coze-batch-size: ${AIIMAGE_SIMILAR_ASIN_COZE_BATCH_SIZE:50}
coze-connect-timeout-millis: ${AIIMAGE_SIMILAR_ASIN_COZE_CONNECT_TIMEOUT_MILLIS:10000} coze-connect-timeout-millis: ${AIIMAGE_SIMILAR_ASIN_COZE_CONNECT_TIMEOUT_MILLIS:10000}

View File

@@ -44,6 +44,29 @@
</ul> </ul>
</div> </div>
<div class="category-card">
<div class="category-card-head">
<div class="section-title">商品类目</div>
<div class="category-actions">
<button type="button" class="opt-btn" @click="openCategoryDialog">选择类目</button>
<button
v-if="selectedProductCategoryIds.length"
type="button"
class="link-danger"
@click="clearSelectedProductCategories"
>
清空
</button>
</div>
</div>
<div v-if="!selectedProductCategories.length" class="empty-conditions">暂未选择商品类目</div>
<div v-else class="selected-category-list">
<span v-for="category in selectedProductCategories" :key="category.id" class="category-chip">
{{ category.path || category.name }}
</span>
</div>
</div>
<div class="run-row"> <div class="run-row">
<button type="button" class="btn-run" :disabled="parsing || !uploadedFiles.length" @click="parseFiles"> <button type="button" class="btn-run" :disabled="parsing || !uploadedFiles.length" @click="parseFiles">
{{ parsing ? '解析中...' : '解析并创建任务' }} {{ parsing ? '解析中...' : '解析并创建任务' }}
@@ -159,6 +182,41 @@
</div> </div>
</section> </section>
</div> </div>
<el-dialog
v-model="categoryDialogVisible"
title="选择商品类目"
width="560px"
class="category-dialog"
>
<div class="category-dialog-toolbar">
<span class="muted">可多选确认后会追加到筛选条件 prompt 后面</span>
<button type="button" class="link-danger" @click="clearCategoryTreeSelection">清空选择</button>
</div>
<div v-if="loadingProductCategories" class="empty-tasks">正在加载商品类目...</div>
<div v-else-if="!productCategoryTree.length" class="empty-tasks">暂无商品类目</div>
<el-tree
v-else
ref="categoryTreeRef"
class="category-tree"
:data="productCategoryTree"
node-key="id"
show-checkbox
default-expand-all
:props="{ label: 'name', children: 'children' }"
>
<template #default="{ data }">
<span class="category-tree-node">
<span>{{ data.name }}</span>
<span v-if="data.description" class="category-description">{{ data.description }}</span>
</span>
</template>
</el-tree>
<template #footer>
<button type="button" class="opt-btn" @click="categoryDialogVisible = false">取消</button>
<button type="button" class="btn-run category-confirm" @click="confirmCategorySelection">确定</button>
</template>
</el-dialog>
</div> </div>
</template> </template>
@@ -176,8 +234,10 @@ import {
getSimilarAsinHistory, getSimilarAsinHistory,
getSimilarAsinResultDownloadUrl, getSimilarAsinResultDownloadUrl,
getSimilarAsinTaskProgressBatch, getSimilarAsinTaskProgressBatch,
listProductCategories,
listSimilarAsinFilterConditions, listSimilarAsinFilterConditions,
parseSimilarAsin, parseSimilarAsin,
type ProductCategoryItemVo,
type SimilarAsinFilterConditionVo, type SimilarAsinFilterConditionVo,
type SimilarAsinParsedGroup, type SimilarAsinParsedGroup,
type SimilarAsinDashboardVo, type SimilarAsinDashboardVo,
@@ -203,6 +263,12 @@ const queuedTaskSummary = ref<TaskSummary | null>(null)
const filterConditionInput = ref('') const filterConditionInput = ref('')
const filterConditions = ref<SimilarAsinFilterConditionVo[]>([]) const filterConditions = ref<SimilarAsinFilterConditionVo[]>([])
const selectedFilterConditionId = ref<number | null>(null) const selectedFilterConditionId = ref<number | null>(null)
const productCategoryTree = ref<ProductCategoryItemVo[]>([])
const productCategoryItems = ref<ProductCategoryItemVo[]>([])
const selectedProductCategoryIds = ref<number[]>([])
const categoryDialogVisible = ref(false)
const loadingProductCategories = ref(false)
const categoryTreeRef = ref<any>(null)
const parsing = ref(false) const parsing = ref(false)
const pushing = ref(false) const pushing = ref(false)
const savingCondition = ref(false) const savingCondition = ref(false)
@@ -243,13 +309,80 @@ function selectedFilterConditionText() {
} }
function effectiveFilterCondition() { function effectiveFilterCondition() {
return filterConditionInput.value.trim() || selectedFilterConditionText() return appendProductCategoriesToPrompt(filterConditionInput.value.trim() || selectedFilterConditionText())
} }
function effectiveCozeApiKey() { function effectiveCozeApiKey() {
return getStoredApiSecret('similar-asin').trim() return getStoredApiSecret('similar-asin').trim()
} }
const selectedProductCategories = computed(() => {
const selected = new Set(selectedProductCategoryIds.value)
return productCategoryItems.value.filter((item) => selected.has(item.id))
})
function appendProductCategoriesToPrompt(prompt: string) {
const lines = selectedProductCategories.value
.map((item) => {
const path = item.path || item.name
const description = item.description?.trim()
return description ? `${path}${description}` : path
})
.filter(Boolean)
if (!lines.length) return prompt
const categoryBlock = [
'商品类目要求:',
...lines.map((line) => `- ${line}`),
].join('\n')
return prompt ? `${prompt}\n\n${categoryBlock}` : categoryBlock
}
async function loadProductCategories() {
if (loadingProductCategories.value) return
loadingProductCategories.value = true
try {
const res = await listProductCategories()
productCategoryTree.value = res.tree || []
productCategoryItems.value = res.items || []
} catch (error) {
ElMessage.error(error instanceof Error ? error.message : '加载商品类目失败')
} finally {
loadingProductCategories.value = false
}
}
async function openCategoryDialog() {
categoryDialogVisible.value = true
if (!productCategoryTree.value.length) {
await loadProductCategories()
}
requestAnimationFrame(() => {
categoryTreeRef.value?.setCheckedKeys?.(selectedProductCategoryIds.value)
})
}
function hasCheckedDescendant(item: ProductCategoryItemVo, checkedIds: Set<number>): boolean {
return Boolean(item.children?.some((child) => checkedIds.has(child.id) || hasCheckedDescendant(child, checkedIds)))
}
function confirmCategorySelection() {
const checkedNodes = (categoryTreeRef.value?.getCheckedNodes?.(false, false) || []) as ProductCategoryItemVo[]
const checkedIds = new Set(checkedNodes.map((item) => item.id))
selectedProductCategoryIds.value = checkedNodes
.filter((item) => !hasCheckedDescendant(item, checkedIds))
.map((item) => item.id)
categoryDialogVisible.value = false
}
function clearCategoryTreeSelection() {
categoryTreeRef.value?.setCheckedKeys?.([])
}
function clearSelectedProductCategories() {
selectedProductCategoryIds.value = []
categoryTreeRef.value?.setCheckedKeys?.([])
}
function maskSecret(secret: string) { function maskSecret(secret: string) {
if (!secret) return '' if (!secret) return ''
if (secret.length <= 10) return '***' if (secret.length <= 10) return '***'
@@ -781,6 +914,7 @@ onMounted(async () => {
loadPollingIds() loadPollingIds()
await Promise.all([ await Promise.all([
loadFilterConditions().catch(() => undefined), loadFilterConditions().catch(() => undefined),
loadProductCategories().catch(() => undefined),
loadDashboard().catch(() => undefined), loadDashboard().catch(() => undefined),
loadHistory().catch(() => undefined), loadHistory().catch(() => undefined),
]) ])
@@ -827,6 +961,18 @@ onUnmounted(() => {
.condition-check input { width: 16px; height: 16px; margin: 0; flex: 0 0 auto; } .condition-check input { width: 16px; height: 16px; margin: 0; flex: 0 0 auto; }
.condition-check span { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; } .condition-check span { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.link-danger { border: none; background: transparent; color: #ff8f8f; cursor: pointer; font-size: 12px; padding: 2px 0; white-space: nowrap; } .link-danger { border: none; background: transparent; color: #ff8f8f; cursor: pointer; font-size: 12px; padding: 2px 0; white-space: nowrap; }
.category-card { margin: 0 0 14px; padding: 12px; border: 1px solid #2a2a2a; border-radius: 8px; background: #202020; }
.category-card-head { display: flex; align-items: center; justify-content: space-between; gap: 10px; }
.category-card-head .section-title { margin-bottom: 0; }
.category-actions { display: flex; align-items: center; gap: 10px; }
.category-actions .opt-btn { padding: 6px 12px; }
.selected-category-list { display: flex; flex-wrap: wrap; gap: 6px; margin-top: 10px; max-height: 96px; overflow: auto; }
.category-chip { max-width: 100%; padding: 4px 8px; border-radius: 6px; background: rgba(52, 152, 219, .16); color: #cfe7ff; font-size: 12px; line-height: 1.4; }
.category-dialog-toolbar { display: flex; align-items: center; justify-content: space-between; gap: 12px; margin-bottom: 10px; }
.category-tree { max-height: 420px; overflow: auto; padding: 8px; border: 1px solid #2f2f2f; border-radius: 8px; background: #202020; --el-tree-bg-color: #202020; --el-tree-text-color: #d8d8d8; --el-tree-node-hover-bg-color: #2a2a2a; }
.category-tree-node { display: inline-flex; align-items: center; gap: 8px; min-width: 0; }
.category-description { color: #888; font-size: 12px; }
.category-confirm { padding: 8px 16px; }
.parse-card, .queue-payload { margin-top: 14px; padding: 12px; border: 1px solid #2a2a2a; border-radius: 8px; background: #202020; color: #b8c1cc; font-size: 12px; } .parse-card, .queue-payload { margin-top: 14px; padding: 12px; border: 1px solid #2a2a2a; border-radius: 8px; background: #202020; color: #b8c1cc; font-size: 12px; }
.queue-payload { max-height: 220px; overflow: auto; color: #8fd3ff; white-space: pre-wrap; } .queue-payload { max-height: 220px; overflow: auto; color: #8fd3ff; white-space: pre-wrap; }
.panel-header { padding: 16px 20px; border-bottom: 1px solid #2a2a2a; font-size: 15px; font-weight: 600; color: #ddd; } .panel-header { padding: 16px 20px; border-bottom: 1px solid #2a2a2a; font-size: 15px; font-weight: 600; color: #ddd; }

View File

@@ -1680,6 +1680,31 @@ export function getAppearancePatentResultDownloadUrl(resultId: number) {
// ========== 货源查询 ========== // ========== 货源查询 ==========
export interface ProductCategoryItemVo {
id: number;
parentId?: number | null;
name: string;
categoryKey?: string;
sortOrder?: number;
description?: string;
isBuiltin?: boolean;
childCount?: number;
level?: number;
path?: string;
children?: ProductCategoryItemVo[];
}
export interface ProductCategoryListVo {
tree: ProductCategoryItemVo[];
items: ProductCategoryItemVo[];
}
export function listProductCategories() {
return unwrapJavaResponse(
get<JavaApiResponse<ProductCategoryListVo>>(`${JAVA_API_PREFIX}/admin/product-categories`),
);
}
export interface SimilarAsinFilterConditionVo { export interface SimilarAsinFilterConditionVo {
id: number; id: number;
conditionText: string; conditionText: string;

View File

@@ -0,0 +1 @@
import{be as r}from"./pywebview-DNfEzGOK.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

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@@ -5,12 +5,12 @@
<meta name="viewport" content="width=device-width, initial-scale=1.0" /> <meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>货源查询</title> <title>货源查询</title>
<script type="module" crossorigin src="/assets/similar-asin.js"></script> <script type="module" crossorigin src="/assets/similar-asin.js"></script>
<link rel="modulepreload" crossorigin href="/assets/pywebview-CcblbV5J.js"> <link rel="modulepreload" crossorigin href="/assets/pywebview-DNfEzGOK.js">
<link rel="modulepreload" crossorigin href="/assets/brand-CEi7uRFN.js"> <link rel="modulepreload" crossorigin href="/assets/brand-D4E3c93-.js">
<link rel="modulepreload" crossorigin href="/assets/categorized-timers-JPA-olTr.js"> <link rel="modulepreload" crossorigin href="/assets/categorized-timers-JPA-olTr.js">
<link rel="stylesheet" crossorigin href="/assets/pywebview-DNJAt1gR.css"> <link rel="stylesheet" crossorigin href="/assets/pywebview-DNJAt1gR.css">
<link rel="stylesheet" crossorigin href="/assets/similar-asin-CVTb4S1E.css"> <link rel="stylesheet" crossorigin href="/assets/similar-asin-0mttxPNV.css">
<link rel="stylesheet" crossorigin href="/assets/el-table-column-BoPoyBFZ.css"> <link rel="stylesheet" crossorigin href="/assets/el-table-column-CZbviO2X.css">
</head> </head>
<body> <body>
<div id="app"></div> <div id="app"></div>

Binary file not shown.

Binary file not shown.

Binary file not shown.