暂存
This commit is contained in:
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
@@ -5,7 +5,7 @@
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<title>格式转换 - 数富AI</title>
|
||||
<script type="module" crossorigin src="/assets/convert.js"></script>
|
||||
<link rel="modulepreload" crossorigin href="/assets/pywebview-C8Bik-Sc.js">
|
||||
<link rel="modulepreload" crossorigin href="/assets/pywebview-Dee3nQjE.js">
|
||||
<link rel="stylesheet" crossorigin href="/assets/pywebview-MkjZQlBu.css">
|
||||
<link rel="stylesheet" crossorigin href="/assets/convert-Cfr3mUjs.css">
|
||||
</head>
|
||||
|
||||
@@ -5,7 +5,7 @@
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<title>数据去重 - 数富AI</title>
|
||||
<script type="module" crossorigin src="/assets/dedupe.js"></script>
|
||||
<link rel="modulepreload" crossorigin href="/assets/pywebview-C8Bik-Sc.js">
|
||||
<link rel="modulepreload" crossorigin href="/assets/pywebview-Dee3nQjE.js">
|
||||
<link rel="stylesheet" crossorigin href="/assets/pywebview-MkjZQlBu.css">
|
||||
<link rel="stylesheet" crossorigin href="/assets/dedupe-CS18ls2z.css">
|
||||
</head>
|
||||
|
||||
@@ -5,9 +5,9 @@
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<title>删除品牌 - 数富AI</title>
|
||||
<script type="module" crossorigin src="/assets/delete-brand.js"></script>
|
||||
<link rel="modulepreload" crossorigin href="/assets/pywebview-C8Bik-Sc.js">
|
||||
<link rel="modulepreload" crossorigin href="/assets/pywebview-Dee3nQjE.js">
|
||||
<link rel="stylesheet" crossorigin href="/assets/pywebview-MkjZQlBu.css">
|
||||
<link rel="stylesheet" crossorigin href="/assets/delete-brand-BRIDBaoM.css">
|
||||
<link rel="stylesheet" crossorigin href="/assets/delete-brand-Chu8awlt.css">
|
||||
</head>
|
||||
<body>
|
||||
<div id="app"></div>
|
||||
|
||||
@@ -5,7 +5,7 @@
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<title>数据拆分 - 数富AI</title>
|
||||
<script type="module" crossorigin src="/assets/split.js"></script>
|
||||
<link rel="modulepreload" crossorigin href="/assets/pywebview-C8Bik-Sc.js">
|
||||
<link rel="modulepreload" crossorigin href="/assets/pywebview-Dee3nQjE.js">
|
||||
<link rel="stylesheet" crossorigin href="/assets/pywebview-MkjZQlBu.css">
|
||||
<link rel="stylesheet" crossorigin href="/assets/split-mwLUJT1K.css">
|
||||
</head>
|
||||
|
||||
BIN
backend-java/aiimage-backend.log.2026-03-29.0.gz
Normal file
BIN
backend-java/aiimage-backend.log.2026-03-29.0.gz
Normal file
Binary file not shown.
@@ -40,6 +40,8 @@ import org.apache.poi.ss.usermodel.Sheet;
|
||||
import org.apache.poi.ss.usermodel.Workbook;
|
||||
import org.apache.poi.xssf.usermodel.XSSFWorkbook;
|
||||
import org.springframework.stereotype.Service;
|
||||
import org.springframework.transaction.annotation.Transactional;
|
||||
|
||||
|
||||
import java.io.File;
|
||||
import java.io.FileInputStream;
|
||||
@@ -125,6 +127,8 @@ public class DeleteBrandRunService {
|
||||
item.setShopId(matchResult.shopId());
|
||||
item.setPlatform(matchResult.platform());
|
||||
item.setOpenStoreUrl(matchResult.openStoreUrl());
|
||||
} else {
|
||||
throw new BusinessException("未匹配到紫鸟店铺,跳过处理");
|
||||
}
|
||||
} catch (BusinessException ex) {
|
||||
String message = ex.getMessage();
|
||||
@@ -191,9 +195,10 @@ public class DeleteBrandRunService {
|
||||
task.setFailedFileCount(parsedFailedCount);
|
||||
task.setResultJson(JSONUtil.toJsonStr(items));
|
||||
task.setUpdatedAt(LocalDateTime.now());
|
||||
task.setSourceFileCount(parsedSuccessCount);
|
||||
if (parsedSuccessCount <= 0) {
|
||||
task.setStatus("FAILED");
|
||||
task.setErrorMessage(parsedFailedCount > 0 ? "删除品牌文件解析失败" : null);
|
||||
task.setErrorMessage(parsedFailedCount > 0 ? "全部文件解析或匹配失败" : null);
|
||||
task.setFinishedAt(LocalDateTime.now());
|
||||
}
|
||||
fileTaskMapper.updateById(task);
|
||||
@@ -269,11 +274,20 @@ public class DeleteBrandRunService {
|
||||
}
|
||||
if (entity.getSourceFilename().equals(candidate.getSourceFilename())) {
|
||||
item.setMatched(candidate.isMatched());
|
||||
// 如果物理结果已经是成功了,说明已经处理过了,强制置为匹配成功
|
||||
if (item.isSuccess()) {
|
||||
item.setMatched(true);
|
||||
}
|
||||
|
||||
item.setShopId(candidate.getShopId());
|
||||
item.setPlatform(candidate.getPlatform());
|
||||
item.setOpenStoreUrl(candidate.getOpenStoreUrl());
|
||||
|
||||
// 历史表可能只有通用 errorMessage;优先返回任务当时的真实 error
|
||||
if ((item.getError() == null || item.getError().isBlank()) && candidate.getError() != null && !candidate.getError().isBlank()) {
|
||||
// 但是,如果物理结果是成功的,就不应该显示所谓的“未匹配”报错
|
||||
if (item.isSuccess()) {
|
||||
item.setError(null);
|
||||
} else if ((item.getError() == null || item.getError().isBlank()) && candidate.getError() != null && !candidate.getError().isBlank()) {
|
||||
item.setError(candidate.getError());
|
||||
}
|
||||
break;
|
||||
@@ -636,7 +650,10 @@ public class DeleteBrandRunService {
|
||||
}
|
||||
}
|
||||
|
||||
@Transactional
|
||||
public void submitResult(Long taskId, DeleteBrandSubmitResultRequest request) {
|
||||
log.info("[DeleteBrand] Received submitResult for taskId: {}, files count: {}", taskId, request == null || request.getFiles() == null ? 0 : request.getFiles().size());
|
||||
|
||||
if (taskId == null || taskId <= 0) {
|
||||
throw new BusinessException("taskId 不合法");
|
||||
}
|
||||
@@ -659,24 +676,33 @@ public class DeleteBrandRunService {
|
||||
throw new BusinessException("任务原始数据已过期,请重新执行解析");
|
||||
}
|
||||
|
||||
task.setStatus("RUNNING");
|
||||
task.setErrorMessage(null);
|
||||
task.setUpdatedAt(LocalDateTime.now());
|
||||
task.setFinishedAt(null);
|
||||
fileTaskMapper.updateById(task);
|
||||
if (!"RUNNING".equals(task.getStatus())) {
|
||||
task.setStatus("RUNNING");
|
||||
task.setErrorMessage(null);
|
||||
task.setUpdatedAt(LocalDateTime.now());
|
||||
task.setFinishedAt(null);
|
||||
fileTaskMapper.updateById(task);
|
||||
}
|
||||
|
||||
|
||||
for (DeleteBrandResultFileDto fileDto : request.getFiles()) {
|
||||
log.info("[DeleteBrand] submitResult processing chunk -> taskId: {}, file: {}, chunkIndex: {}/{}, processedRows: {}/{}",
|
||||
taskId, fileDto.getSourceFilename(), fileDto.getChunkIndex(), fileDto.getChunkTotal(),
|
||||
fileDto.getProcessedRows(), fileDto.getTotalRows());
|
||||
|
||||
String fileIdentity = resolveFileIdentity(fileDto);
|
||||
if (fileIdentity.isBlank()) {
|
||||
throw new BusinessException("fileKey/sourceFilename 不能为空");
|
||||
}
|
||||
DeleteBrandParsedFileCacheDto parsedFile = parsedPayload.get(fileIdentity);
|
||||
if (parsedFile == null) {
|
||||
log.warn("[DeleteBrand] parsed payload not found for identity: {}", fileIdentity);
|
||||
throw new BusinessException("文件标识不匹配: " + fileIdentity);
|
||||
}
|
||||
|
||||
// 更早的 duplicate fast-path:尽量在较重的校验/进度写入之前直接短路
|
||||
if (deleteBrandTaskCacheService.hasSameResultChunk(taskId, fileIdentity, fileDto.getChunkIndex(), fileDto)) {
|
||||
log.info("[DeleteBrand] Duplicate chunk ignored for taskId: {}, file: {}, chunkIndex: {}", taskId, fileIdentity, fileDto.getChunkIndex());
|
||||
continue;
|
||||
}
|
||||
|
||||
@@ -699,6 +725,16 @@ public class DeleteBrandRunService {
|
||||
progress.put("updated_at", String.valueOf(System.currentTimeMillis()));
|
||||
progress.put("last_heartbeat_at", String.valueOf(System.currentTimeMillis()));
|
||||
deleteBrandTaskCacheService.saveProgress(taskId, progress);
|
||||
|
||||
// 如果该文件已完成,立即更新成品计数的 Redis 缓存「提示」,让后续 tryFinalizeTask 能更精确
|
||||
if (isFileCompleted(fileDto)) {
|
||||
log.info("[DeleteBrand] File chunk transmission fully reached via chunkTotal for taskId: {}, file: {}", taskId, fileDto.getSourceFilename());
|
||||
java.util.Map<Object, Object> currentProgress = deleteBrandTaskCacheService.getProgress(taskId);
|
||||
int finishedCount = parseInt(currentProgress.get("finished_files")) == null ? 0 : parseInt(currentProgress.get("finished_files"));
|
||||
// 这里我们暂不直接 increment,而是标记需要重算或在 tryFinalizeTask 中重新计算
|
||||
// 为了保险,我们直接在 tryFinalizeTask 里重新遍历分片状态
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
tryFinalizeTask(taskId, false);
|
||||
@@ -724,12 +760,10 @@ public class DeleteBrandRunService {
|
||||
return;
|
||||
}
|
||||
|
||||
Map<Object, Object> progressSnapshot = deleteBrandTaskCacheService.getProgress(taskId);
|
||||
int expectedFiles = parsedPayload.size();
|
||||
Integer finishedFilesHint = parseInt(progressSnapshot == null ? null : progressSnapshot.get("finished_files"));
|
||||
if (finishedFilesHint != null && finishedFilesHint < expectedFiles) {
|
||||
return;
|
||||
}
|
||||
// 移除 finishedFilesHint 强行返回逻辑,因为 Redis 里的 finished_files 可能是旧的,
|
||||
// 既然进入了 tryFinalizeTask,就说明有新片到达,应该以 loadMergedChunks 为准
|
||||
|
||||
|
||||
Map<String, List<DeleteBrandResultFileDto>> mergedByFile = loadMergedChunks(taskId);
|
||||
int finishedFiles = countCompletedFiles(mergedByFile);
|
||||
@@ -747,9 +781,12 @@ public class DeleteBrandRunService {
|
||||
fileTaskMapper.updateById(task);
|
||||
|
||||
if (finishedFiles < expectedFiles) {
|
||||
log.info("[DeleteBrand] tryFinalizeTask skipping -> finishedFiles ({}) < expectedFiles ({})", finishedFiles, expectedFiles);
|
||||
return;
|
||||
}
|
||||
|
||||
log.info("[DeleteBrand] tryFinalizeTask calling finalizeTask -> finishedFiles: {}, expectedFiles: {}", finishedFiles, expectedFiles);
|
||||
|
||||
finalizeTask(task, parsedPayload, mergedByFile);
|
||||
}
|
||||
|
||||
|
||||
@@ -39,6 +39,16 @@ public class DeleteBrandStaleTaskService {
|
||||
Map<Object, Object> progress = deleteBrandTaskCacheService.getProgress(task.getId());
|
||||
boolean hasProgress = progress != null && !progress.isEmpty() && progress.containsKey("last_heartbeat_at");
|
||||
|
||||
boolean isActive = false;
|
||||
if (progress != null && progress.containsKey("has_progress")) {
|
||||
Object hpObj = progress.get("has_progress");
|
||||
if (hpObj instanceof Boolean) {
|
||||
isActive = (Boolean) hpObj;
|
||||
} else if (hpObj instanceof String) {
|
||||
isActive = Boolean.parseBoolean((String) hpObj);
|
||||
}
|
||||
}
|
||||
|
||||
long lastHeartbeatAt = 0L;
|
||||
if (hasProgress) {
|
||||
try {
|
||||
@@ -48,18 +58,19 @@ public class DeleteBrandStaleTaskService {
|
||||
}
|
||||
|
||||
LocalDateTime lastActivityTime;
|
||||
if (lastHeartbeatAt > 0) {
|
||||
// 有心跳,用最后心跳时间对比 15 分钟
|
||||
// 只有当 has_progress 为 true 且超过 15 分钟没心跳,才认为是异常卡死
|
||||
if (lastHeartbeatAt > 0 && isActive) {
|
||||
// 有心跳,用最后心跳时间对比 15 分钟 (threshold 是 now - 15)
|
||||
lastActivityTime = LocalDateTime.ofInstant(Instant.ofEpochMilli(lastHeartbeatAt), ZoneId.systemDefault());
|
||||
if (lastActivityTime.isAfter(threshold)) {
|
||||
continue; // 没超时
|
||||
}
|
||||
} else {
|
||||
// 没有心跳,说明任务一直没被 Python 取出来执行(处在排队状态中)
|
||||
// 排队的宽限期我们设为 12 个小时,如果建了 12 个小时都没执行,再杀死
|
||||
// 没有心跳,或者当前处在 has_progress: false (等待用户点击下一个任务推送的静默期)
|
||||
// 静默期和排队期的宽限期一样,设为 12 个小时
|
||||
LocalDateTime queuedThreshold = LocalDateTime.now().minusHours(12);
|
||||
if (task.getCreatedAt() != null && task.getCreatedAt().isAfter(queuedThreshold)) {
|
||||
continue; // 排队中,不要杀它!
|
||||
continue; // 等待下个队列推送中,不要杀它!
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -294,7 +294,7 @@ public class ZiniaoAuthService {
|
||||
.queryParam("storeId", storeId)
|
||||
.queryParam("openapiToken", loginToken)
|
||||
.queryParam("userId", userId)
|
||||
.queryParam("lanuchUrl", encodedLaunchUrl)
|
||||
.queryParam("launchUrl", encodedLaunchUrl)
|
||||
.queryParam("autoopen", Boolean.TRUE.equals(ziniaoProperties.getOpenStoreAutoOpen()))
|
||||
.queryParam("debuggPort", ziniaoProperties.getOpenStoreDebugPort())
|
||||
.queryParam("notPromptForDownload", Boolean.TRUE.equals(ziniaoProperties.getOpenStoreNotPromptForDownload()) ? 1 : 0);
|
||||
|
||||
@@ -29,7 +29,7 @@ AIIMAGE_ZINIAO_APP_TOKEN_PATH=/auth/get_app_token
|
||||
AIIMAGE_ZINIAO_STAFF_LIST_PATH=/superbrowser/rest/v1/erp/staff/list
|
||||
AIIMAGE_ZINIAO_USER_STORES_PATH=/superbrowser/rest/v1/erp/user/stores
|
||||
AIIMAGE_ZINIAO_USER_LOGIN_TOKEN_PATH=/superbrowser/rest/v1/token/user-login
|
||||
AIIMAGE_ZINIAO_OPEN_STORE_SCHEME=superbrowser://OpenStrore
|
||||
AIIMAGE_ZINIAO_OPEN_STORE_SCHEME=superbrowser://OpenStore
|
||||
AIIMAGE_ZINIAO_OPEN_STORE_USER_ID=
|
||||
AIIMAGE_ZINIAO_OPEN_STORE_LAUNCH_URL=https://www.baidu.com
|
||||
AIIMAGE_ZINIAO_OPEN_STORE_AUTO_OPEN=true
|
||||
|
||||
@@ -96,7 +96,7 @@ aiimage:
|
||||
staff-list-path: ${AIIMAGE_ZINIAO_STAFF_LIST_PATH:/superbrowser/rest/v1/erp/staff/list}
|
||||
user-stores-path: ${AIIMAGE_ZINIAO_USER_STORES_PATH:/superbrowser/rest/v1/erp/user/stores}
|
||||
user-login-token-path: ${AIIMAGE_ZINIAO_USER_LOGIN_TOKEN_PATH:/superbrowser/rest/v1/token/user-login}
|
||||
open-store-scheme: ${AIIMAGE_ZINIAO_OPEN_STORE_SCHEME:superbrowser://OpenStrore}
|
||||
open-store-scheme: ${AIIMAGE_ZINIAO_OPEN_STORE_SCHEME:superbrowser://OpenStore}
|
||||
open-store-user-id: ${AIIMAGE_ZINIAO_OPEN_STORE_USER_ID:}
|
||||
open-store-launch-url: ${AIIMAGE_ZINIAO_OPEN_STORE_LAUNCH_URL:https://www.baidu.com}
|
||||
open-store-auto-open: ${AIIMAGE_ZINIAO_OPEN_STORE_AUTO_OPEN:true}
|
||||
|
||||
@@ -101,7 +101,8 @@
|
||||
<ul class="task-list clean-result-list">
|
||||
<li
|
||||
v-for="item in currentSectionItems"
|
||||
:key="`current-${item.resultId || item.taskId || item.sourceFilename}`"
|
||||
:key="`current-${item.taskId || item.resultId || item.sourceFilename}`"
|
||||
|
||||
class="task-item split-result-item"
|
||||
>
|
||||
<div class="left split-result-main">
|
||||
@@ -109,6 +110,7 @@
|
||||
<div class="files">店铺名:{{ item.shopName || '-' }}</div>
|
||||
<div class="files">匹配结果:{{ item.matched ? `已匹配 ${item.shopId || ''}` : '未匹配到紫鸟店铺' }}</div>
|
||||
<div v-if="item.platform" class="files">平台:{{ item.platform }}</div>
|
||||
<div v-if="getQueueStatus(item)" class="files">队列状态:{{ getQueueStatus(item) }}</div>
|
||||
<div v-if="item.countryCount !== undefined && item.matched" class="files">国家数:{{ item.countryCount }}</div>
|
||||
<div v-if="item.totalRows !== undefined && item.matched" class="time">去重后 {{ item.totalRows }} 条</div>
|
||||
<div v-if="shouldShowProgress(item)" class="delete-brand-progress-block">
|
||||
@@ -135,7 +137,7 @@
|
||||
v-if="item.openStoreUrl"
|
||||
type="button"
|
||||
class="download"
|
||||
@click="openShop(item.openStoreUrl)"
|
||||
@click="triggerItemRun(item)"
|
||||
>
|
||||
打开紫鸟
|
||||
</button>
|
||||
@@ -165,7 +167,8 @@
|
||||
<ul class="task-list clean-result-list">
|
||||
<li
|
||||
v-for="item in historySectionItems"
|
||||
:key="`history-${item.resultId || item.taskId || item.sourceFilename}`"
|
||||
:key="`history-${item.taskId || item.resultId || item.sourceFilename}`"
|
||||
|
||||
class="task-item split-result-item"
|
||||
>
|
||||
<div class="left split-result-main">
|
||||
@@ -195,14 +198,7 @@
|
||||
<span class="status" :class="getTaskStatusInfo(item).className">
|
||||
{{ getTaskStatusInfo(item).text }}
|
||||
</span>
|
||||
<button
|
||||
v-if="item.openStoreUrl"
|
||||
type="button"
|
||||
class="download"
|
||||
@click="openShop(item.openStoreUrl)"
|
||||
>
|
||||
打开紫鸟
|
||||
</button>
|
||||
<!-- 历史记录区不再显示打开紫鸟按钮 -->
|
||||
<button
|
||||
v-if="canDownloadTaskResult(item)"
|
||||
type="button"
|
||||
@@ -249,11 +245,24 @@ import {
|
||||
} from '@/shared/api/java-modules'
|
||||
import { getPywebviewApi, type UploadedJavaFile } from '@/shared/bridges/pywebview'
|
||||
|
||||
interface StoredCurrentTask {
|
||||
taskId: number
|
||||
items: DeleteBrandResultItem[]
|
||||
createdAt: number
|
||||
queuePushResult?: string
|
||||
queuePayloadText?: string
|
||||
}
|
||||
|
||||
function getStorageKey() {
|
||||
const uid = typeof window !== 'undefined' ? window.localStorage.getItem('uid') || '0' : '0'
|
||||
return `delete-brand:current-tasks:${uid}`
|
||||
}
|
||||
|
||||
const selectedPaths = ref<string[]>([])
|
||||
const uploadedFiles = ref<UploadedJavaFile[]>([])
|
||||
const running = ref(false)
|
||||
const resultItems = ref<DeleteBrandResultItem[]>([])
|
||||
const currentRunItems = ref<DeleteBrandResultItem[]>([])
|
||||
const sessionTasks = ref<StoredCurrentTask[]>([])
|
||||
const historyItems = ref<DeleteBrandResultItem[]>([])
|
||||
const summary = ref<DeleteBrandRunVo>({ total: 0, successCount: 0, failedCount: 0, items: [] })
|
||||
const queuePushResult = ref('')
|
||||
@@ -262,36 +271,65 @@ const taskDetails = ref<Record<number, DeleteBrandTaskDetailVo>>({})
|
||||
const pollTimer = ref<number | null>(null)
|
||||
const pollingInFlight = ref(false)
|
||||
const displayPaths = computed(() => selectedPaths.value.slice(0, 10))
|
||||
const runningTaskIdsInHistory = computed(() => {
|
||||
return historyItems.value
|
||||
.filter(item => {
|
||||
const status = item.taskId ? taskDetails.value[item.taskId]?.task?.status : null;
|
||||
// 优先认后端的实时状态
|
||||
if (status) return status === 'RUNNING';
|
||||
// 如果还没拿到底层详情,看 history 接口带回来的全局任务状态
|
||||
if (item.taskStatus) return item.taskStatus === 'RUNNING';
|
||||
// 最后降级到根据 success/error 猜测(向下兼容)
|
||||
return (item.taskId || 0) > 0 && item.success === false && !item.error;
|
||||
})
|
||||
.map(item => item.taskId);
|
||||
});
|
||||
|
||||
const currentSectionItems = computed(() => {
|
||||
// 当前运行的任务 = 用户手动触发的 + 历史记录里还在跑的
|
||||
const allCurrent = [...currentRunItems.value];
|
||||
|
||||
historyItems.value.forEach(hItem => {
|
||||
if (hItem.taskId && runningTaskIdsInHistory.value.includes(hItem.taskId)) {
|
||||
// 避免重复
|
||||
if (!allCurrent.some(c => c.taskId === hItem.taskId)) {
|
||||
allCurrent.push(hItem);
|
||||
}
|
||||
}
|
||||
const allCurrent: DeleteBrandResultItem[] = [];
|
||||
sessionTasks.value.forEach(task => {
|
||||
allCurrent.push(...task.items);
|
||||
});
|
||||
|
||||
return allCurrent;
|
||||
});
|
||||
|
||||
function loadSessionTasksFromStorage() {
|
||||
try {
|
||||
const raw = typeof window !== 'undefined' ? window.localStorage.getItem(getStorageKey()) : null
|
||||
if (raw) {
|
||||
sessionTasks.value = JSON.parse(raw)
|
||||
if (sessionTasks.value.length > 0) {
|
||||
const lastTask = sessionTasks.value[sessionTasks.value.length - 1]
|
||||
queuePushResult.value = lastTask.queuePushResult || ''
|
||||
queuePayloadText.value = lastTask.queuePayloadText || ''
|
||||
}
|
||||
} else {
|
||||
sessionTasks.value = []
|
||||
}
|
||||
} catch {
|
||||
sessionTasks.value = []
|
||||
}
|
||||
}
|
||||
|
||||
function saveSessionTask(taskId: number, items: DeleteBrandResultItem[], pushResult?: string, payloadText?: string) {
|
||||
// 只将解析成功且匹配成功的任务放入当前队列中,失败的将留在历史记录里
|
||||
const validItems = items.filter(i => i.matched !== false && i.success !== false)
|
||||
if (validItems.length === 0) return
|
||||
|
||||
const newTask: StoredCurrentTask = {
|
||||
taskId,
|
||||
items: validItems,
|
||||
createdAt: Date.now(),
|
||||
queuePushResult: pushResult,
|
||||
queuePayloadText: payloadText
|
||||
}
|
||||
const existingIndex = sessionTasks.value.findIndex(t => t.taskId === taskId)
|
||||
if (existingIndex >= 0) {
|
||||
sessionTasks.value[existingIndex] = newTask
|
||||
} else {
|
||||
sessionTasks.value.push(newTask)
|
||||
}
|
||||
if (typeof window !== 'undefined') {
|
||||
window.localStorage.setItem(getStorageKey(), JSON.stringify(sessionTasks.value))
|
||||
}
|
||||
}
|
||||
|
||||
function removeSessionTaskFromStorage(taskId: number) {
|
||||
const oldLen = sessionTasks.value.length
|
||||
sessionTasks.value = sessionTasks.value.filter(t => t.taskId !== taskId)
|
||||
if (sessionTasks.value.length !== oldLen && typeof window !== 'undefined') {
|
||||
window.localStorage.setItem(getStorageKey(), JSON.stringify(sessionTasks.value))
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
const historySectionItems = computed(() =>
|
||||
historyItems.value.filter(
|
||||
(item) => !currentSectionItems.value.some(c => c.taskId === item.taskId || (c.resultId && c.resultId === item.resultId))
|
||||
@@ -312,19 +350,17 @@ function getDisplayError(item: DeleteBrandResultItem) {
|
||||
}
|
||||
|
||||
function shouldShowProgress(item: DeleteBrandResultItem) {
|
||||
const taskId = item.taskId
|
||||
if (!taskId) return false
|
||||
const status = getTaskStatus(taskId)
|
||||
// 只要状态是运行中就显示进度条,不管 matched 了(matched 不对的状态应该在后端就标记为 FAILED)
|
||||
return status === 'RUNNING'
|
||||
const qs = getQueueStatus(item)
|
||||
return qs === '已入队' || qs === '处理中'
|
||||
}
|
||||
|
||||
|
||||
function normalizeDeleteBrandItems(items: DeleteBrandResultItem[]) {
|
||||
return items.map((item) => {
|
||||
if (item.matched === false) {
|
||||
// 只有在既没成功,又显式标记为未匹配时,才做错误兜底
|
||||
if (!item.success && item.matched === false) {
|
||||
return {
|
||||
...item,
|
||||
success: false,
|
||||
error: item.error || '未匹配到紫鸟店铺',
|
||||
openStoreUrl: undefined,
|
||||
}
|
||||
@@ -368,7 +404,9 @@ function formatProgress(taskId: number) {
|
||||
if (info.phase) parts.push(`阶段:${info.phase}`)
|
||||
if (info.file_index && info.file_total) parts.push(`文件:${info.file_index}/${info.file_total}`)
|
||||
if (info.file_name) parts.push(`当前文件:${info.file_name}`)
|
||||
if (info.current_line != null && info.total_lines != null) parts.push(`进度:${info.current_line}/${info.total_lines}`)
|
||||
if (info.current_line != null && info.total_lines != null) {
|
||||
parts.push(`进度:${info.current_line}/${info.total_lines}`)
|
||||
}
|
||||
if (info.current_country) parts.push(`国家:${info.current_country}`)
|
||||
if (info.current_asin) parts.push(`ASIN:${info.current_asin}`)
|
||||
if (info.finished_files != null) parts.push(`已完成文件:${info.finished_files}`)
|
||||
@@ -402,38 +440,58 @@ function isTaskTerminal(taskId: number) {
|
||||
|
||||
function getPollingTaskIds() {
|
||||
const ids = new Set<number>()
|
||||
|
||||
// 1. 检查本会话触发的任务
|
||||
currentRunItems.value.forEach((item) => {
|
||||
if (item.taskId && !isTaskTerminal(item.taskId)) {
|
||||
ids.add(item.taskId)
|
||||
sessionTasks.value.forEach(task => {
|
||||
// 只轮询有文件被推给 Python 的任务
|
||||
if (task.items.some(i => (i as any)._pushed)) {
|
||||
ids.add(task.taskId)
|
||||
}
|
||||
})
|
||||
|
||||
// 2. 检查历史加载的任务
|
||||
historyItems.value.forEach((item) => {
|
||||
if (item.taskId) {
|
||||
if (taskDetails.value[item.taskId]) {
|
||||
// 如果已经拿到了详情,以后端详情为准
|
||||
if (!isTaskTerminal(item.taskId)) {
|
||||
ids.add(item.taskId)
|
||||
}
|
||||
} else {
|
||||
// 还没拿到详情,看 history 接口中的 taskStatus 或 success 标记
|
||||
const isRunning = item.taskStatus === 'RUNNING' || (!item.taskStatus && !item.success && !item.error);
|
||||
if (isRunning) {
|
||||
ids.add(item.taskId)
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
return Array.from(ids)
|
||||
}
|
||||
|
||||
function canDownloadTaskResult(item: DeleteBrandResultItem) {
|
||||
function getQueueStatus(item: DeleteBrandResultItem) {
|
||||
const taskId = item.taskId
|
||||
if (!taskId || !item.matched) return false
|
||||
if (!taskId) return ''
|
||||
const sessionTask = sessionTasks.value.find(t => t.taskId === taskId)
|
||||
if (!sessionTask) return ''
|
||||
|
||||
const status = getTaskStatus(taskId)
|
||||
if (status === 'SUCCESS' || status === 'FAILED' || item.taskStatus === 'SUCCESS' || item.taskStatus === 'FAILED') {
|
||||
return '已被全局判定为终态'
|
||||
}
|
||||
|
||||
const sessionItem = sessionTask.items.find(i => i.resultId === item.resultId || i.sourceFilename === item.sourceFilename)
|
||||
|
||||
const info = taskDetails.value[taskId]?.line_progress
|
||||
|
||||
// 此时这个特定文件是否正好被后端报了进度名
|
||||
if (status === 'RUNNING' && info?.has_progress && info?.info?.file_name === item.sourceFilename) {
|
||||
return '处理中'
|
||||
}
|
||||
|
||||
// 查查是否已经完结了这个特定文件
|
||||
const detail = taskDetails.value[taskId]
|
||||
if (detail?.items) {
|
||||
// 后端返回的 items 是 `resultJson` 解析来的,里面只含有已经做完的!
|
||||
const finished = detail.items.find((i: DeleteBrandResultItem) => i.sourceFilename === item.sourceFilename)
|
||||
if (finished) {
|
||||
return '本文件解析完毕'
|
||||
}
|
||||
}
|
||||
|
||||
if (sessionItem && (sessionItem as any)._pushed) {
|
||||
return '已入队'
|
||||
}
|
||||
|
||||
return '未入队'
|
||||
}
|
||||
|
||||
function canDownloadTaskResult(item: DeleteBrandResultItem) {
|
||||
// 核心修复:只要单个项目已经成功(后端返回了下载地址或标记为成功),就允许下载,哪怕整包任务还在“执行中”
|
||||
if (item.downloadUrl || item.success) return true
|
||||
|
||||
const taskId = item.taskId
|
||||
if (!taskId) return false
|
||||
const status = taskDetails.value[taskId]?.task?.status
|
||||
return status === 'SUCCESS'
|
||||
}
|
||||
@@ -477,14 +535,25 @@ function scheduleNextPoll(immediate = false) {
|
||||
if (!taskIds.length) return
|
||||
|
||||
pollingInFlight.value = true
|
||||
let stateChanged = false
|
||||
try {
|
||||
await refreshTaskDetails(taskIds)
|
||||
for (const taskId of taskIds) {
|
||||
if (isTaskTerminal(taskId)) {
|
||||
removeSessionTaskFromStorage(taskId)
|
||||
stateChanged = true
|
||||
}
|
||||
}
|
||||
} finally {
|
||||
pollingInFlight.value = false
|
||||
}
|
||||
|
||||
const stillRunning = taskIds.some((id) => isTaskRunning(id))
|
||||
if (stillRunning) {
|
||||
if (stateChanged) {
|
||||
loadHistory()
|
||||
syncResultState()
|
||||
}
|
||||
|
||||
if (getPollingTaskIds().length > 0) {
|
||||
scheduleNextPoll()
|
||||
}
|
||||
}
|
||||
@@ -598,39 +667,24 @@ async function submitRun() {
|
||||
})),
|
||||
})
|
||||
const normalizedItems = normalizeDeleteBrandItems(result.items || [])
|
||||
currentRunItems.value = normalizedItems
|
||||
const hasUnmatched = normalizedItems.some((item) => !item.matched)
|
||||
|
||||
queuePushResult.value = '解析完成,等待手动点击打开紫鸟进行单任务处理'
|
||||
queuePayloadText.value = ''
|
||||
|
||||
if (normalizedItems.length && normalizedItems[0].taskId) {
|
||||
saveSessionTask(normalizedItems[0].taskId, normalizedItems, queuePushResult.value, queuePayloadText.value)
|
||||
}
|
||||
syncResultState()
|
||||
|
||||
await refreshTaskDetails(getPollingTaskIds())
|
||||
ensurePolling()
|
||||
|
||||
const api = getPywebviewApi()
|
||||
const hasUnmatched = normalizedItems.some((item) => !item.matched)
|
||||
if (!hasUnmatched && api?.enqueue_json) {
|
||||
const payload = {
|
||||
type: 'delete-brand-run',
|
||||
ts: Date.now(),
|
||||
data: { ...result, items: normalizedItems },
|
||||
}
|
||||
queuePayloadText.value = JSON.stringify(payload, null, 2)
|
||||
const pushResult = await api.enqueue_json(payload)
|
||||
if (pushResult?.success) {
|
||||
queuePushResult.value = `已推送到 Python 队列,当前队列长度:${pushResult.queue_size ?? '-'}`
|
||||
} else {
|
||||
queuePushResult.value = `推送到 Python 队列失败:${pushResult?.error || '未知错误'}`
|
||||
}
|
||||
} else {
|
||||
queuePushResult.value = hasUnmatched
|
||||
? '存在未匹配到紫鸟店铺的文件,已跳过推送到 Python 队列'
|
||||
: '当前环境未启用 pywebview enqueue_json(浏览器环境不会推送)'
|
||||
queuePayloadText.value = ''
|
||||
}
|
||||
|
||||
if (hasUnmatched) {
|
||||
ElMessage.error('存在未匹配到紫鸟店铺的文件,已标记为失败')
|
||||
ElMessage.error('存在未匹配到紫鸟店铺的文件,请检查报错内容。这些文件已自动归档至历史记录。')
|
||||
} else {
|
||||
ElMessage.success('删除品牌解析完成')
|
||||
ElMessage.success('删除品牌解析完成,请手动推送')
|
||||
}
|
||||
|
||||
// 更新历史记录,以展示那些未进入队列的失败项
|
||||
await loadHistory()
|
||||
} catch (error) {
|
||||
ElMessage.error(error instanceof Error ? error.message : '执行失败')
|
||||
} finally {
|
||||
@@ -679,16 +733,17 @@ async function loadHistory() {
|
||||
}
|
||||
|
||||
function removeRecordFromView(resultId: number) {
|
||||
const removedFromCurrent = currentRunItems.value.find((item) => item.resultId === resultId)
|
||||
const removedTaskId = removedFromCurrent?.taskId
|
||||
|
||||
currentRunItems.value = currentRunItems.value.filter((item) => item.resultId !== resultId)
|
||||
historyItems.value = historyItems.value.filter((item) => item.resultId !== resultId)
|
||||
|
||||
if (removedTaskId && !currentRunItems.value.some((item) => item.taskId === removedTaskId)) {
|
||||
delete taskDetails.value[removedTaskId]
|
||||
sessionTasks.value.forEach(st => {
|
||||
st.items = st.items.filter(item => item.resultId !== resultId)
|
||||
})
|
||||
const oldLen = sessionTasks.value.length
|
||||
sessionTasks.value = sessionTasks.value.filter(st => st.items.length > 0)
|
||||
if (sessionTasks.value.length !== oldLen && typeof window !== 'undefined') {
|
||||
window.localStorage.setItem(getStorageKey(), JSON.stringify(sessionTasks.value))
|
||||
}
|
||||
|
||||
historyItems.value = historyItems.value.filter((item) => item.resultId !== resultId)
|
||||
|
||||
syncResultState()
|
||||
|
||||
if (getPollingTaskIds().length === 0) {
|
||||
@@ -720,11 +775,93 @@ function formatPreview(rows: DeleteBrandPreviewRow[]) {
|
||||
return hiddenCount > 0 ? `${preview.join('、')} 等 ${rows.length} 条` : preview.join('、')
|
||||
}
|
||||
|
||||
function isPythonQueueBusy() {
|
||||
for (const task of sessionTasks.value) {
|
||||
for (const item of task.items) {
|
||||
const qs = getQueueStatus(item)
|
||||
if (qs === '已入队' || qs === '处理中') {
|
||||
return true
|
||||
}
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
async function triggerItemRun(item: DeleteBrandResultItem) {
|
||||
if (isPythonQueueBusy()) {
|
||||
ElMessage.warning('当前存在正在处理的任务,请等待其完成后再推送下一条!')
|
||||
return
|
||||
}
|
||||
|
||||
if (item.openStoreUrl) {
|
||||
window.open(item.openStoreUrl, '_blank')
|
||||
}
|
||||
|
||||
const taskId = item.taskId
|
||||
if (!taskId) return
|
||||
|
||||
const sessionTask = sessionTasks.value.find(t => t.taskId === taskId)
|
||||
if (!sessionTask) return
|
||||
|
||||
const api = getPywebviewApi()
|
||||
if (!api?.enqueue_json) {
|
||||
ElMessage.error('当前环境未启用 pywebview enqueue_json(浏览器环境不会推送)')
|
||||
return
|
||||
}
|
||||
|
||||
const payload = {
|
||||
type: 'delete-brand-run',
|
||||
ts: Date.now(),
|
||||
data: {
|
||||
taskId: taskId,
|
||||
items: [item]
|
||||
},
|
||||
}
|
||||
|
||||
queuePayloadText.value = JSON.stringify(payload, null, 2)
|
||||
const pushResult = await api.enqueue_json(payload)
|
||||
let qpr = ''
|
||||
if (pushResult?.success) {
|
||||
qpr = `已单独推送文件 ${item.sourceFilename},当前队列长度:${pushResult.queue_size ?? '-'}`
|
||||
|
||||
// Mark the item as pushed
|
||||
const sessionItem = sessionTask.items.find(i => i.resultId === item.resultId || i.sourceFilename === item.sourceFilename)
|
||||
if (sessionItem) {
|
||||
(sessionItem as any)._pushed = true
|
||||
}
|
||||
saveSessionTask(taskId, sessionTask.items, qpr, queuePayloadText.value)
|
||||
|
||||
ensurePolling(true)
|
||||
} else {
|
||||
qpr = `推送到 Python 队列失败:${pushResult?.error || '未知错误'}`
|
||||
}
|
||||
queuePushResult.value = qpr
|
||||
}
|
||||
|
||||
function openShop(url: string) {
|
||||
window.open(url, '_blank')
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
loadSessionTasksFromStorage()
|
||||
const taskIds = getPollingTaskIds()
|
||||
if (taskIds.length > 0) {
|
||||
refreshTaskDetails(taskIds).then(() => {
|
||||
let stateChanged = false
|
||||
for (const taskId of taskIds) {
|
||||
if (isTaskTerminal(taskId)) {
|
||||
removeSessionTaskFromStorage(taskId)
|
||||
stateChanged = true
|
||||
}
|
||||
}
|
||||
if (stateChanged) {
|
||||
syncResultState()
|
||||
}
|
||||
ensurePolling(true)
|
||||
})
|
||||
} else {
|
||||
syncResultState()
|
||||
}
|
||||
loadHistory().catch(() => undefined)
|
||||
})
|
||||
|
||||
|
||||
@@ -203,6 +203,7 @@ export interface DeleteBrandTaskItem {
|
||||
export interface DeleteBrandTaskDetailVo {
|
||||
task: DeleteBrandTaskItem
|
||||
line_progress: DeleteBrandLineProgress
|
||||
items?: DeleteBrandResultItem[]
|
||||
}
|
||||
|
||||
export interface DeleteBrandTaskBatchVo {
|
||||
@@ -384,7 +385,11 @@ export function submitDeleteBrandResult(taskId: number, request: DeleteBrandSubm
|
||||
}
|
||||
|
||||
export function getJavaDownloadUrl(path: string) {
|
||||
const raw = path.startsWith('http://') || path.startsWith('https://') ? path : `${JAVA_API_PREFIX}${path}`
|
||||
let raw = path.startsWith('http://') || path.startsWith('https://') ? path : `${JAVA_API_PREFIX}${path}`
|
||||
// 核心修复:pywebview 的 save_file_from_url 需要完整带 schema 的 URL
|
||||
if (!raw.startsWith('http://') && !raw.startsWith('https://') && typeof window !== 'undefined') {
|
||||
raw = `${window.location.origin}${raw}`
|
||||
}
|
||||
const separator = raw.includes('?') ? '&' : '?'
|
||||
return `${raw}${separator}user_id=${encodeURIComponent(String(getCurrentUserId()))}`
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user