暂存
This commit is contained in:
@@ -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,8 +781,11 @@ 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