提交货源采集更新
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://127.0.0.1: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
|
||||
|
||||
|
||||
|
||||
@@ -9,7 +9,7 @@ public class SimilarAsinProperties {
|
||||
private String cozeBaseUrl = "https://api.coze.cn";
|
||||
private String cozeWorkflowPath = "/v1/workflow/run";
|
||||
private String cozeWorkflowHistoryPath = "/v1/workflows/{workflow_id}/run_histories/{execute_id}";
|
||||
private String cozeWorkflowId = "7632683471312355338";
|
||||
private String cozeWorkflowId = "7635328462404583478";
|
||||
private String cozeToken = "";
|
||||
private int cozeBatchSize = 50;
|
||||
private int cozeConnectTimeoutMillis = 10000;
|
||||
|
||||
@@ -556,7 +556,6 @@ public class AppearancePatentTaskService {
|
||||
: row.getResultFilename();
|
||||
}
|
||||
|
||||
@Scheduled(cron = "${aiimage.appearance-patent.stale-finalize-cron:0 */2 * * * *}")
|
||||
public void finalizeStaleTasks() {
|
||||
if (transactionManager != null) {
|
||||
LocalDateTime threshold = LocalDateTime.now().minusMinutes(Math.max(1, properties.getStaleTimeoutMinutes()));
|
||||
|
||||
@@ -1209,7 +1209,6 @@ public class BrandTaskService {
|
||||
}
|
||||
|
||||
@Transactional
|
||||
@org.springframework.scheduling.annotation.Scheduled(cron = "${aiimage.brand-progress.stale-check-cron:0 */2 * * * *}")
|
||||
public void failStaleRunningTasks() {
|
||||
DistributedJobLockService.LockHandle lockHandle = distributedJobLockService.tryLock("brand:stale-check", STALE_CHECK_LOCK_TTL);
|
||||
if (lockHandle == null) {
|
||||
|
||||
@@ -4,6 +4,8 @@ import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
|
||||
import com.baomidou.mybatisplus.core.conditions.update.LambdaUpdateWrapper;
|
||||
import com.nanri.aiimage.common.service.DistributedJobLockService;
|
||||
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.queryasin.service.QueryAsinTaskCacheService;
|
||||
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.pricetrack.service.PriceTrackTaskCacheService;
|
||||
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.ShopMatchTaskService;
|
||||
import com.nanri.aiimage.modules.task.mapper.FileTaskMapper;
|
||||
@@ -63,6 +66,9 @@ public class DeleteBrandStaleTaskService {
|
||||
private final PatrolDeleteTaskCacheService patrolDeleteTaskCacheService;
|
||||
private final QueryAsinTaskService queryAsinTaskService;
|
||||
private final QueryAsinTaskCacheService queryAsinTaskCacheService;
|
||||
private final BrandTaskService brandTaskService;
|
||||
private final AppearancePatentTaskService appearancePatentTaskService;
|
||||
private final SimilarAsinTaskService similarAsinTaskService;
|
||||
private final DeleteBrandProgressProperties deleteBrandProgressProperties;
|
||||
private final DistributedJobLockService distributedJobLockService;
|
||||
private final TaskDistributedLockService taskDistributedLockService;
|
||||
@@ -88,6 +94,9 @@ public class DeleteBrandStaleTaskService {
|
||||
ShopMatchStaleCheckStats shopMatchStats = failStaleShopMatchTasks();
|
||||
ShopMatchStaleCheckStats patrolDeleteStats = failStalePatrolDeleteTasks();
|
||||
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={}",
|
||||
stats.scannedTaskCount,
|
||||
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() {
|
||||
long minutes = Math.max(1L, deleteBrandProgressProperties.getHeartbeatTimeoutMinutes());
|
||||
long initialMinutes = Math.max(minutes, deleteBrandProgressProperties.getDeleteBrandInitialTimeoutMinutes());
|
||||
|
||||
@@ -210,6 +210,9 @@ public class SimilarAsinCozeClient {
|
||||
Map<String, Object> body = new LinkedHashMap<>();
|
||||
body.put("workflow_id", properties.getCozeWorkflowId());
|
||||
body.put("parameters", parameters);
|
||||
if (apiKey != null && !apiKey.isBlank()) {
|
||||
body.put("api_key", apiKey.trim());
|
||||
}
|
||||
body.put("is_async", Boolean.TRUE);
|
||||
log.info("[similar-asin] coze request url={} body={}",
|
||||
joinUrl(properties.getCozeBaseUrl(), properties.getCozeWorkflowPath()),
|
||||
@@ -256,20 +259,12 @@ public class SimilarAsinCozeClient {
|
||||
}
|
||||
|
||||
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> 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<List<String>> urlLists = rows.stream().map(SimilarAsinResultRowDto::getUrls).toList();
|
||||
|
||||
Map<String, Object> parameters = new LinkedHashMap<>();
|
||||
parameters.put("title_list", titles);
|
||||
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("items", buildItemObjects(rows, asins, urls, urlLists));
|
||||
parameters.put("prompt", prompt == null ? "" : prompt);
|
||||
if (apiKey != null && !apiKey.isBlank()) {
|
||||
parameters.put("api_key", apiKey.trim());
|
||||
@@ -280,6 +275,10 @@ public class SimilarAsinCozeClient {
|
||||
@SuppressWarnings("unchecked")
|
||||
private Map<String, Object> maskCozeRequestBody(Map<String, Object> 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");
|
||||
if (parametersObj instanceof Map<?, ?> 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,
|
||||
List<String> groupKeys,
|
||||
List<String> rowIds,
|
||||
List<String> asins,
|
||||
List<String> countries,
|
||||
List<String> prices,
|
||||
List<String> titles,
|
||||
List<String> urls,
|
||||
List<List<String>> urlLists) {
|
||||
List<Map<String, Object>> items = new ArrayList<>(rows.size());
|
||||
for (int i = 0; i < rows.size(); i++) {
|
||||
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("country", countries.get(i));
|
||||
item.put("price", prices.get(i));
|
||||
item.put("title", titles.get(i));
|
||||
item.put("url", urls.get(i));
|
||||
item.put("urls", urlLists.get(i));
|
||||
item.put("target_urls", urlLists.get(i));
|
||||
items.add(item);
|
||||
}
|
||||
|
||||
@@ -616,7 +616,6 @@ public class SimilarAsinTaskService {
|
||||
: row.getResultFilename();
|
||||
}
|
||||
|
||||
@Scheduled(cron = "${aiimage.similar-asin.stale-finalize-cron:0 */2 * * * *}")
|
||||
public void finalizeStaleTasks() {
|
||||
if (transactionManager != null) {
|
||||
LocalDateTime threshold = LocalDateTime.now().minusMinutes(Math.max(1, properties.getStaleTimeoutMinutes()));
|
||||
@@ -1429,6 +1428,10 @@ public class SimilarAsinTaskService {
|
||||
SimilarAsinCozeClient.CozeSubmitResponse submit = submitCozeWorkflowThrottled(batchRows, prompt, apiKey);
|
||||
if (submit.immediateData() != null && !submit.immediateData().isBlank()) {
|
||||
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);
|
||||
return false;
|
||||
}
|
||||
@@ -1570,15 +1573,22 @@ public class SimilarAsinTaskService {
|
||||
if (batchRows.isEmpty() && failureMessage.isBlank()) {
|
||||
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)) {
|
||||
return;
|
||||
}
|
||||
if (!failureMessage.isBlank() && retryFailedCozeBatchState(state, context, batchRows, failureMessage)) {
|
||||
return;
|
||||
}
|
||||
List<SimilarAsinResultRowDto> cozeRows = failureMessage.isBlank()
|
||||
? cozeClient.mergeRowsFromDataText(batchRows, poll.resolvedPayloadText())
|
||||
: cozeClient.markRowsFailed(batchRows, failureMessage);
|
||||
if (!failureMessage.isBlank()) {
|
||||
cozeRows = cozeClient.markRowsFailed(batchRows, failureMessage);
|
||||
}
|
||||
FileTaskEntity task = fileTaskMapper.selectById(state.getTaskId());
|
||||
if (task != null) {
|
||||
Map<String, List<SimilarAsinParsedRowVo>> allRowsByBaseId = loadAllRowsByBaseId(task);
|
||||
@@ -1819,6 +1829,7 @@ public class SimilarAsinTaskService {
|
||||
String normalized = normalize(failureMessage).toLowerCase(Locale.ROOT);
|
||||
return normalized.contains("timeout")
|
||||
|| normalized.contains("timed out")
|
||||
|| normalized.contains("empty result rows")
|
||||
|| normalized.contains("out of limit")
|
||||
|| normalized.contains("execution limit")
|
||||
|| normalized.contains("720712008")
|
||||
@@ -1834,6 +1845,7 @@ public class SimilarAsinTaskService {
|
||||
|| normalized.contains("retry later")
|
||||
|| normalized.contains("timeout")
|
||||
|| normalized.contains("timed out")
|
||||
|| normalized.contains("empty result rows")
|
||||
|| normalized.contains("out of limit")
|
||||
|| normalized.contains("execution limit")
|
||||
|| normalized.contains("702093018")
|
||||
@@ -1846,6 +1858,21 @@ public class SimilarAsinTaskService {
|
||||
|| 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) {
|
||||
int updated = taskScopeStateMapper.update(null, new LambdaUpdateWrapper<TaskScopeStateEntity>()
|
||||
.eq(TaskScopeStateEntity::getId, state.getId())
|
||||
|
||||
@@ -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_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_BATCH_SIZE=50
|
||||
AIIMAGE_SIMILAR_ASIN_COZE_READ_TIMEOUT_MILLIS=60000
|
||||
|
||||
@@ -157,7 +157,7 @@ aiimage:
|
||||
similar-asin:
|
||||
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-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-batch-size: ${AIIMAGE_SIMILAR_ASIN_COZE_BATCH_SIZE:50}
|
||||
coze-connect-timeout-millis: ${AIIMAGE_SIMILAR_ASIN_COZE_CONNECT_TIMEOUT_MILLIS:10000}
|
||||
|
||||
@@ -44,6 +44,29 @@
|
||||
</ul>
|
||||
</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">
|
||||
<button type="button" class="btn-run" :disabled="parsing || !uploadedFiles.length" @click="parseFiles">
|
||||
{{ parsing ? '解析中...' : '解析并创建任务' }}
|
||||
@@ -159,6 +182,41 @@
|
||||
</div>
|
||||
</section>
|
||||
</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>
|
||||
</template>
|
||||
|
||||
@@ -176,8 +234,10 @@ import {
|
||||
getSimilarAsinHistory,
|
||||
getSimilarAsinResultDownloadUrl,
|
||||
getSimilarAsinTaskProgressBatch,
|
||||
listProductCategories,
|
||||
listSimilarAsinFilterConditions,
|
||||
parseSimilarAsin,
|
||||
type ProductCategoryItemVo,
|
||||
type SimilarAsinFilterConditionVo,
|
||||
type SimilarAsinParsedGroup,
|
||||
type SimilarAsinDashboardVo,
|
||||
@@ -203,6 +263,12 @@ const queuedTaskSummary = ref<TaskSummary | null>(null)
|
||||
const filterConditionInput = ref('')
|
||||
const filterConditions = ref<SimilarAsinFilterConditionVo[]>([])
|
||||
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 pushing = ref(false)
|
||||
const savingCondition = ref(false)
|
||||
@@ -243,13 +309,80 @@ function selectedFilterConditionText() {
|
||||
}
|
||||
|
||||
function effectiveFilterCondition() {
|
||||
return filterConditionInput.value.trim() || selectedFilterConditionText()
|
||||
return appendProductCategoriesToPrompt(filterConditionInput.value.trim() || selectedFilterConditionText())
|
||||
}
|
||||
|
||||
function effectiveCozeApiKey() {
|
||||
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) {
|
||||
if (!secret) return ''
|
||||
if (secret.length <= 10) return '***'
|
||||
@@ -781,6 +914,7 @@ onMounted(async () => {
|
||||
loadPollingIds()
|
||||
await Promise.all([
|
||||
loadFilterConditions().catch(() => undefined),
|
||||
loadProductCategories().catch(() => undefined),
|
||||
loadDashboard().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 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; }
|
||||
.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; }
|
||||
.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; }
|
||||
|
||||
@@ -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 {
|
||||
id: number;
|
||||
conditionText: string;
|
||||
|
||||
1
new_web_source/assets/brand-D4E3c93-.js
Normal file
1
new_web_source/assets/brand-D4E3c93-.js
Normal 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};
|
||||
1
new_web_source/assets/el-table-column-CZbviO2X.css
Normal file
1
new_web_source/assets/el-table-column-CZbviO2X.css
Normal file
File diff suppressed because one or more lines are too long
55
new_web_source/assets/pywebview-DNfEzGOK.js
Normal file
55
new_web_source/assets/pywebview-DNfEzGOK.js
Normal file
File diff suppressed because one or more lines are too long
1
new_web_source/assets/similar-asin-0mttxPNV.css
Normal file
1
new_web_source/assets/similar-asin-0mttxPNV.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
@@ -5,12 +5,12 @@
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<title>货源查询</title>
|
||||
<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/brand-CEi7uRFN.js">
|
||||
<link rel="modulepreload" crossorigin href="/assets/pywebview-DNfEzGOK.js">
|
||||
<link rel="modulepreload" crossorigin href="/assets/brand-D4E3c93-.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/similar-asin-CVTb4S1E.css">
|
||||
<link rel="stylesheet" crossorigin href="/assets/el-table-column-BoPoyBFZ.css">
|
||||
<link rel="stylesheet" crossorigin href="/assets/similar-asin-0mttxPNV.css">
|
||||
<link rel="stylesheet" crossorigin href="/assets/el-table-column-CZbviO2X.css">
|
||||
</head>
|
||||
<body>
|
||||
<div id="app"></div>
|
||||
|
||||
BIN
tmp/repair-7686/7686-current.xlsx
Normal file
BIN
tmp/repair-7686/7686-current.xlsx
Normal file
Binary file not shown.
BIN
tmp/repair-7686/7686-redownload-full.xlsx
Normal file
BIN
tmp/repair-7686/7686-redownload-full.xlsx
Normal file
Binary file not shown.
BIN
tmp/repair-7686/7686-redownload.xlsx
Normal file
BIN
tmp/repair-7686/7686-redownload.xlsx
Normal file
Binary file not shown.
BIN
tmp/repair-7686/魏振峰查询专利-7686-repaired-full.xlsx
Normal file
BIN
tmp/repair-7686/魏振峰查询专利-7686-repaired-full.xlsx
Normal file
Binary file not shown.
BIN
tmp/repair-7686/魏振峰查询专利-7686-repaired.xlsx
Normal file
BIN
tmp/repair-7686/魏振峰查询专利-7686-repaired.xlsx
Normal file
Binary file not shown.
BIN
tmp/repair-7686/魏振峰查询专利-7686-supply-repaired.xlsx
Normal file
BIN
tmp/repair-7686/魏振峰查询专利-7686-supply-repaired.xlsx
Normal file
Binary file not shown.
Reference in New Issue
Block a user