完善删除品牌asin
This commit is contained in:
4
app/.env
4
app/.env
@@ -7,7 +7,7 @@ proxy_url=https://api.jikip.com/ip-get?num=1&minute=1&format=json&area=all&proto
|
|||||||
proxy_mode=2
|
proxy_mode=2
|
||||||
|
|
||||||
client_name=NanriAI
|
client_name=NanriAI
|
||||||
java_api_base=http://8.136.19.173:18080
|
# java_api_base=http://8.136.19.173:18080
|
||||||
# java_api_base=http://127.0.0.1:18080
|
java_api_base=http://127.0.0.1:18080
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
Binary file not shown.
@@ -18,8 +18,8 @@ proxy_url = os.getenv("proxy_url")
|
|||||||
proxy_mode = int(os.getenv("proxy_mode",1))
|
proxy_mode = int(os.getenv("proxy_mode",1))
|
||||||
|
|
||||||
client_name=os.getenv("client_name") + ".exe"
|
client_name=os.getenv("client_name") + ".exe"
|
||||||
JAVA_API_BASE = os.getenv("java_api_base", "http://8.136.19.173:18080")
|
# JAVA_API_BASE = os.getenv("java_api_base", "http://8.136.19.173:18080")
|
||||||
# JAVA_API_BASE = os.getenv("java_api_base", "http://127.0.0.1:18080")
|
JAVA_API_BASE = os.getenv("java_api_base", "http://127.0.0.1:18080")
|
||||||
|
|
||||||
cache_path = "./user_data"
|
cache_path = "./user_data"
|
||||||
|
|
||||||
|
|||||||
@@ -62,4 +62,7 @@ public class DeleteBrandResultItemVo {
|
|||||||
|
|
||||||
@Schema(description = "错误信息")
|
@Schema(description = "错误信息")
|
||||||
private String error;
|
private String error;
|
||||||
|
|
||||||
|
@Schema(description = "由于列表来自 history 接口,增加所属任务的状态(RUNNING/SUCCESS/FAILED),协助前端准确定位执行中任务")
|
||||||
|
private String taskStatus;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -32,6 +32,7 @@ import com.nanri.aiimage.modules.task.mapper.FileTaskMapper;
|
|||||||
import com.nanri.aiimage.modules.task.model.entity.FileResultEntity;
|
import com.nanri.aiimage.modules.task.model.entity.FileResultEntity;
|
||||||
import com.nanri.aiimage.modules.task.model.entity.FileTaskEntity;
|
import com.nanri.aiimage.modules.task.model.entity.FileTaskEntity;
|
||||||
import lombok.RequiredArgsConstructor;
|
import lombok.RequiredArgsConstructor;
|
||||||
|
import lombok.extern.slf4j.Slf4j;
|
||||||
import org.apache.poi.ss.usermodel.Cell;
|
import org.apache.poi.ss.usermodel.Cell;
|
||||||
import org.apache.poi.ss.usermodel.DataFormatter;
|
import org.apache.poi.ss.usermodel.DataFormatter;
|
||||||
import org.apache.poi.ss.usermodel.Row;
|
import org.apache.poi.ss.usermodel.Row;
|
||||||
@@ -55,6 +56,7 @@ import java.util.Set;
|
|||||||
|
|
||||||
@Service
|
@Service
|
||||||
@RequiredArgsConstructor
|
@RequiredArgsConstructor
|
||||||
|
@Slf4j
|
||||||
public class DeleteBrandRunService {
|
public class DeleteBrandRunService {
|
||||||
|
|
||||||
private static final String MODULE_TYPE = "DELETE_BRAND";
|
private static final String MODULE_TYPE = "DELETE_BRAND";
|
||||||
@@ -86,6 +88,7 @@ public class DeleteBrandRunService {
|
|||||||
task.setCreatedAt(LocalDateTime.now());
|
task.setCreatedAt(LocalDateTime.now());
|
||||||
task.setUpdatedAt(LocalDateTime.now());
|
task.setUpdatedAt(LocalDateTime.now());
|
||||||
fileTaskMapper.insert(task);
|
fileTaskMapper.insert(task);
|
||||||
|
deleteBrandTaskCacheService.saveTaskCache(task);
|
||||||
|
|
||||||
List<DeleteBrandResultItemVo> items = new ArrayList<>();
|
List<DeleteBrandResultItemVo> items = new ArrayList<>();
|
||||||
int parsedSuccessCount = 0;
|
int parsedSuccessCount = 0;
|
||||||
@@ -128,7 +131,8 @@ public class DeleteBrandRunService {
|
|||||||
if (message != null && (message.contains("code=40004")
|
if (message != null && (message.contains("code=40004")
|
||||||
|| message.contains("userId存在无效的参数值")
|
|| message.contains("userId存在无效的参数值")
|
||||||
|| message.contains("无权限")
|
|| message.contains("无权限")
|
||||||
|| message.contains("没有权限"))) {
|
|| message.contains("没有权限")) && !message.contains("白名单")) {
|
||||||
|
log.warn("[ziniao-match] suppressed upstream permission error for {}, msg: {}", item.getShopName(), message);
|
||||||
item.setMatched(false);
|
item.setMatched(false);
|
||||||
} else {
|
} else {
|
||||||
throw ex;
|
throw ex;
|
||||||
@@ -193,6 +197,9 @@ public class DeleteBrandRunService {
|
|||||||
task.setFinishedAt(LocalDateTime.now());
|
task.setFinishedAt(LocalDateTime.now());
|
||||||
}
|
}
|
||||||
fileTaskMapper.updateById(task);
|
fileTaskMapper.updateById(task);
|
||||||
|
if ("RUNNING".equals(task.getStatus())) {
|
||||||
|
deleteBrandTaskCacheService.saveTaskCache(task);
|
||||||
|
}
|
||||||
|
|
||||||
if (!parsedPayloadByFileIdentity.isEmpty()) {
|
if (!parsedPayloadByFileIdentity.isEmpty()) {
|
||||||
deleteBrandTaskCacheService.saveParsedPayload(task.getId(), parsedPayloadByFileIdentity);
|
deleteBrandTaskCacheService.saveParsedPayload(task.getId(), parsedPayloadByFileIdentity);
|
||||||
@@ -202,7 +209,7 @@ public class DeleteBrandRunService {
|
|||||||
vo.setTotal(request.getFiles().size());
|
vo.setTotal(request.getFiles().size());
|
||||||
vo.setSuccessCount(parsedSuccessCount);
|
vo.setSuccessCount(parsedSuccessCount);
|
||||||
vo.setFailedCount(parsedFailedCount);
|
vo.setFailedCount(parsedFailedCount);
|
||||||
vo.setItems(items);
|
vo.setItems(items.stream().peek(item -> item.setTaskStatus("RUNNING")).toList());
|
||||||
return vo;
|
return vo;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -214,8 +221,9 @@ public class DeleteBrandRunService {
|
|||||||
.orderByDesc(FileResultEntity::getCreatedAt)
|
.orderByDesc(FileResultEntity::getCreatedAt)
|
||||||
.last("limit 100"));
|
.last("limit 100"));
|
||||||
|
|
||||||
// 补充从 task.resultJson 还原匹配信息/真实失败原因(历史表本身不存这些字段)
|
// 补充信息
|
||||||
Map<Long, List<DeleteBrandResultItemVo>> itemsByTaskId = new LinkedHashMap<>();
|
Map<Long, List<DeleteBrandResultItemVo>> itemsByTaskId = new LinkedHashMap<>();
|
||||||
|
Map<Long, String> statusByTaskId = new LinkedHashMap<>();
|
||||||
List<Long> taskIds = entities.stream()
|
List<Long> taskIds = entities.stream()
|
||||||
.map(FileResultEntity::getTaskId)
|
.map(FileResultEntity::getTaskId)
|
||||||
.filter(id -> id != null && id > 0)
|
.filter(id -> id != null && id > 0)
|
||||||
@@ -224,13 +232,13 @@ public class DeleteBrandRunService {
|
|||||||
if (!taskIds.isEmpty()) {
|
if (!taskIds.isEmpty()) {
|
||||||
List<FileTaskEntity> tasks = fileTaskMapper.selectBatchIds(taskIds);
|
List<FileTaskEntity> tasks = fileTaskMapper.selectBatchIds(taskIds);
|
||||||
for (FileTaskEntity task : tasks) {
|
for (FileTaskEntity task : tasks) {
|
||||||
if (task == null || task.getResultJson() == null || task.getResultJson().isBlank()) {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
try {
|
try {
|
||||||
|
statusByTaskId.put(task.getId(), task.getStatus());
|
||||||
|
if (task.getResultJson() != null && !task.getResultJson().isBlank()) {
|
||||||
List<DeleteBrandResultItemVo> taskItems = objectMapper.readValue(task.getResultJson(), new TypeReference<List<DeleteBrandResultItemVo>>() {
|
List<DeleteBrandResultItemVo> taskItems = objectMapper.readValue(task.getResultJson(), new TypeReference<List<DeleteBrandResultItemVo>>() {
|
||||||
});
|
});
|
||||||
itemsByTaskId.put(task.getId(), taskItems);
|
itemsByTaskId.put(task.getId(), taskItems);
|
||||||
|
}
|
||||||
} catch (Exception ignored) {
|
} catch (Exception ignored) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -250,7 +258,9 @@ public class DeleteBrandRunService {
|
|||||||
item.setError(entity.getErrorMessage());
|
item.setError(entity.getErrorMessage());
|
||||||
|
|
||||||
// 以 task.resultJson 中的项为准补全 matched/shopId/platform/openStoreUrl/错误信息
|
// 以 task.resultJson 中的项为准补全 matched/shopId/platform/openStoreUrl/错误信息
|
||||||
|
// 真实补充
|
||||||
if (entity.getTaskId() != null) {
|
if (entity.getTaskId() != null) {
|
||||||
|
item.setTaskStatus(statusByTaskId.get(entity.getTaskId()));
|
||||||
List<DeleteBrandResultItemVo> taskItems = itemsByTaskId.get(entity.getTaskId());
|
List<DeleteBrandResultItemVo> taskItems = itemsByTaskId.get(entity.getTaskId());
|
||||||
if (taskItems != null && entity.getSourceFilename() != null && !entity.getSourceFilename().isBlank()) {
|
if (taskItems != null && entity.getSourceFilename() != null && !entity.getSourceFilename().isBlank()) {
|
||||||
for (DeleteBrandResultItemVo candidate : taskItems) {
|
for (DeleteBrandResultItemVo candidate : taskItems) {
|
||||||
@@ -456,9 +466,27 @@ public class DeleteBrandRunService {
|
|||||||
return vo;
|
return vo;
|
||||||
}
|
}
|
||||||
|
|
||||||
java.util.Map<Long, FileTaskEntity> taskById = fileTaskMapper.selectBatchIds(normalizedTaskIds).stream()
|
java.util.Map<Long, FileTaskEntity> cachedTasks = deleteBrandTaskCacheService.getTaskCacheBatch(normalizedTaskIds);
|
||||||
|
java.util.List<Long> missingTaskIds = new java.util.ArrayList<>();
|
||||||
|
for (Long taskId : normalizedTaskIds) {
|
||||||
|
if (!cachedTasks.containsKey(taskId)) {
|
||||||
|
missingTaskIds.add(taskId);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
java.util.Map<Long, FileTaskEntity> taskById = new java.util.LinkedHashMap<>(cachedTasks);
|
||||||
|
if (!missingTaskIds.isEmpty()) {
|
||||||
|
java.util.List<FileTaskEntity> dbTasks = fileTaskMapper.selectBatchIds(missingTaskIds).stream()
|
||||||
.filter(task -> task != null && MODULE_TYPE.equals(task.getModuleType()))
|
.filter(task -> task != null && MODULE_TYPE.equals(task.getModuleType()))
|
||||||
.collect(java.util.stream.Collectors.toMap(FileTaskEntity::getId, task -> task, (left, right) -> left, java.util.LinkedHashMap::new));
|
.toList();
|
||||||
|
for (FileTaskEntity dbTask : dbTasks) {
|
||||||
|
taskById.put(dbTask.getId(), dbTask);
|
||||||
|
// Save back to cache
|
||||||
|
if ("RUNNING".equals(dbTask.getStatus())) {
|
||||||
|
deleteBrandTaskCacheService.saveTaskCache(dbTask);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
java.util.Map<Long, java.util.Map<Object, Object>> progressByTaskId = deleteBrandTaskCacheService.getProgressBatch(normalizedTaskIds);
|
java.util.Map<Long, java.util.Map<Object, Object>> progressByTaskId = deleteBrandTaskCacheService.getProgressBatch(normalizedTaskIds);
|
||||||
|
|
||||||
@@ -858,6 +886,7 @@ public class DeleteBrandRunService {
|
|||||||
task.setUpdatedAt(LocalDateTime.now());
|
task.setUpdatedAt(LocalDateTime.now());
|
||||||
task.setFinishedAt(LocalDateTime.now());
|
task.setFinishedAt(LocalDateTime.now());
|
||||||
fileTaskMapper.updateById(task);
|
fileTaskMapper.updateById(task);
|
||||||
|
deleteBrandTaskCacheService.saveTaskCache(task);
|
||||||
|
|
||||||
deleteBrandTaskCacheService.saveProgress(task.getId(), Map.of(
|
deleteBrandTaskCacheService.saveProgress(task.getId(), Map.of(
|
||||||
"phase", "success",
|
"phase", "success",
|
||||||
@@ -872,6 +901,7 @@ public class DeleteBrandRunService {
|
|||||||
task.setUpdatedAt(LocalDateTime.now());
|
task.setUpdatedAt(LocalDateTime.now());
|
||||||
task.setFinishedAt(LocalDateTime.now());
|
task.setFinishedAt(LocalDateTime.now());
|
||||||
fileTaskMapper.updateById(task);
|
fileTaskMapper.updateById(task);
|
||||||
|
deleteBrandTaskCacheService.saveTaskCache(task);
|
||||||
deleteBrandTaskCacheService.saveProgress(task.getId(), Map.of(
|
deleteBrandTaskCacheService.saveProgress(task.getId(), Map.of(
|
||||||
"phase", DeleteBrandTaskCacheService.PHASE_FAILED,
|
"phase", DeleteBrandTaskCacheService.PHASE_FAILED,
|
||||||
"finished_files", String.valueOf(task.getSuccessFileCount() == null ? 0 : task.getSuccessFileCount()),
|
"finished_files", String.valueOf(task.getSuccessFileCount() == null ? 0 : task.getSuccessFileCount()),
|
||||||
|
|||||||
@@ -37,22 +37,30 @@ public class DeleteBrandStaleTaskService {
|
|||||||
|
|
||||||
for (FileTaskEntity task : runningTasks) {
|
for (FileTaskEntity task : runningTasks) {
|
||||||
Map<Object, Object> progress = deleteBrandTaskCacheService.getProgress(task.getId());
|
Map<Object, Object> progress = deleteBrandTaskCacheService.getProgress(task.getId());
|
||||||
if (progress == null || progress.isEmpty()) {
|
boolean hasProgress = progress != null && !progress.isEmpty() && progress.containsKey("last_heartbeat_at");
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
long lastHeartbeatAt = 0L;
|
long lastHeartbeatAt = 0L;
|
||||||
|
if (hasProgress) {
|
||||||
try {
|
try {
|
||||||
lastHeartbeatAt = Long.parseLong(String.valueOf(progress.getOrDefault("last_heartbeat_at", "0")));
|
lastHeartbeatAt = Long.parseLong(String.valueOf(progress.get("last_heartbeat_at")));
|
||||||
} catch (Exception ignored) {
|
} catch (Exception ignored) {
|
||||||
}
|
}
|
||||||
if (lastHeartbeatAt <= 0) {
|
|
||||||
continue;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
LocalDateTime lastHeartbeat = LocalDateTime.ofInstant(Instant.ofEpochMilli(lastHeartbeatAt), ZoneId.systemDefault());
|
LocalDateTime lastActivityTime;
|
||||||
if (lastHeartbeat.isAfter(threshold)) {
|
if (lastHeartbeatAt > 0) {
|
||||||
continue;
|
// 有心跳,用最后心跳时间对比 15 分钟
|
||||||
|
lastActivityTime = LocalDateTime.ofInstant(Instant.ofEpochMilli(lastHeartbeatAt), ZoneId.systemDefault());
|
||||||
|
if (lastActivityTime.isAfter(threshold)) {
|
||||||
|
continue; // 没超时
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// 没有心跳,说明任务一直没被 Python 取出来执行(处在排队状态中)
|
||||||
|
// 排队的宽限期我们设为 12 个小时,如果建了 12 个小时都没执行,再杀死
|
||||||
|
LocalDateTime queuedThreshold = LocalDateTime.now().minusHours(12);
|
||||||
|
if (task.getCreatedAt() != null && task.getCreatedAt().isAfter(queuedThreshold)) {
|
||||||
|
continue; // 排队中,不要杀它!
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
int updated = fileTaskMapper.update(null, new com.baomidou.mybatisplus.core.conditions.update.LambdaUpdateWrapper<FileTaskEntity>()
|
int updated = fileTaskMapper.update(null, new com.baomidou.mybatisplus.core.conditions.update.LambdaUpdateWrapper<FileTaskEntity>()
|
||||||
@@ -65,10 +73,7 @@ public class DeleteBrandStaleTaskService {
|
|||||||
.set(FileTaskEntity::getFinishedAt, LocalDateTime.now()));
|
.set(FileTaskEntity::getFinishedAt, LocalDateTime.now()));
|
||||||
|
|
||||||
if (updated > 0) {
|
if (updated > 0) {
|
||||||
deleteBrandTaskCacheService.saveProgress(task.getId(), java.util.Map.of(
|
deleteBrandTaskCacheService.delete(task.getId());
|
||||||
"phase", DeleteBrandTaskCacheService.PHASE_FAILED,
|
|
||||||
"updated_at", String.valueOf(System.currentTimeMillis())
|
|
||||||
));
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -181,6 +181,42 @@ public class DeleteBrandTaskCacheService {
|
|||||||
stringRedisTemplate.delete(buildPayloadKey(taskId));
|
stringRedisTemplate.delete(buildPayloadKey(taskId));
|
||||||
stringRedisTemplate.delete(buildProgressKey(taskId));
|
stringRedisTemplate.delete(buildProgressKey(taskId));
|
||||||
stringRedisTemplate.delete(buildResultChunksKey(taskId));
|
stringRedisTemplate.delete(buildResultChunksKey(taskId));
|
||||||
|
stringRedisTemplate.delete(buildTaskEntityKey(taskId));
|
||||||
|
}
|
||||||
|
|
||||||
|
public void saveTaskCache(com.nanri.aiimage.modules.task.model.entity.FileTaskEntity task) {
|
||||||
|
if (task == null || task.getId() == null) return;
|
||||||
|
try {
|
||||||
|
stringRedisTemplate.opsForValue().set(buildTaskEntityKey(task.getId()), objectMapper.writeValueAsString(task), Duration.ofHours(PAYLOAD_TTL_HOURS));
|
||||||
|
} catch (Exception ignored) {
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public java.util.Map<Long, com.nanri.aiimage.modules.task.model.entity.FileTaskEntity> getTaskCacheBatch(java.util.List<Long> taskIds) {
|
||||||
|
java.util.Map<Long, com.nanri.aiimage.modules.task.model.entity.FileTaskEntity> result = new java.util.LinkedHashMap<>();
|
||||||
|
if (taskIds == null || taskIds.isEmpty()) return result;
|
||||||
|
|
||||||
|
java.util.List<Long> normalized = taskIds.stream().filter(id -> id != null && id > 0).distinct().toList();
|
||||||
|
if (normalized.isEmpty()) return result;
|
||||||
|
|
||||||
|
java.util.List<String> keys = normalized.stream().map(this::buildTaskEntityKey).toList();
|
||||||
|
java.util.List<String> values = stringRedisTemplate.opsForValue().multiGet(keys);
|
||||||
|
|
||||||
|
for (int i = 0; i < normalized.size(); i++) {
|
||||||
|
Long taskId = normalized.get(i);
|
||||||
|
String val = values != null && i < values.size() ? values.get(i) : null;
|
||||||
|
if (val != null && !val.isBlank()) {
|
||||||
|
try {
|
||||||
|
result.put(taskId, objectMapper.readValue(val, com.nanri.aiimage.modules.task.model.entity.FileTaskEntity.class));
|
||||||
|
} catch (Exception ignored) {
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
private String buildTaskEntityKey(Long taskId) {
|
||||||
|
return "delete-brand:task:entity:" + taskId;
|
||||||
}
|
}
|
||||||
|
|
||||||
private record LocalParsedPayloadCacheEntry(long cachedAtMillis, Object value) {
|
private record LocalParsedPayloadCacheEntry(long cachedAtMillis, Object value) {
|
||||||
|
|||||||
@@ -472,6 +472,9 @@ public class ZiniaoAuthService {
|
|||||||
if (message == null || message.isBlank()) {
|
if (message == null || message.isBlank()) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
if (message.contains("白名单") || message.contains("IP") || message.contains("whitelist")) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
return message.contains("isv.invalid-api-key")
|
return message.contains("isv.invalid-api-key")
|
||||||
|| message.contains("无效的apiKey参数")
|
|| message.contains("无效的apiKey参数")
|
||||||
|| message.contains("非法的参数");
|
|| message.contains("非法的参数");
|
||||||
|
|||||||
Binary file not shown.
@@ -128,8 +128,8 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="task-right split-result-actions">
|
<div class="task-right split-result-actions">
|
||||||
<span class="status" :class="item.success ? 'success' : 'failed'">
|
<span class="status" :class="getTaskStatusInfo(item).className">
|
||||||
{{ item.success ? '已完成' : '失败' }}
|
{{ getTaskStatusInfo(item).text }}
|
||||||
</span>
|
</span>
|
||||||
<button
|
<button
|
||||||
v-if="item.openStoreUrl"
|
v-if="item.openStoreUrl"
|
||||||
@@ -192,8 +192,8 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="task-right split-result-actions">
|
<div class="task-right split-result-actions">
|
||||||
<span class="status" :class="item.success ? 'success' : 'failed'">
|
<span class="status" :class="getTaskStatusInfo(item).className">
|
||||||
{{ item.success ? '已完成' : '失败' }}
|
{{ getTaskStatusInfo(item).text }}
|
||||||
</span>
|
</span>
|
||||||
<button
|
<button
|
||||||
v-if="item.openStoreUrl"
|
v-if="item.openStoreUrl"
|
||||||
@@ -262,17 +262,41 @@ const taskDetails = ref<Record<number, DeleteBrandTaskDetailVo>>({})
|
|||||||
const pollTimer = ref<number | null>(null)
|
const pollTimer = ref<number | null>(null)
|
||||||
const pollingInFlight = ref(false)
|
const pollingInFlight = ref(false)
|
||||||
const displayPaths = computed(() => selectedPaths.value.slice(0, 10))
|
const displayPaths = computed(() => selectedPaths.value.slice(0, 10))
|
||||||
const currentSectionItems = computed(() => currentRunItems.value)
|
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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
return allCurrent;
|
||||||
|
});
|
||||||
|
|
||||||
const historySectionItems = computed(() =>
|
const historySectionItems = computed(() =>
|
||||||
historyItems.value.filter(
|
historyItems.value.filter(
|
||||||
(item) =>
|
(item) => !currentSectionItems.value.some(c => c.taskId === item.taskId || (c.resultId && c.resultId === item.resultId))
|
||||||
!currentRunItems.value.some(
|
|
||||||
(current) =>
|
|
||||||
(current.resultId || current.taskId || current.fileKey || current.sourceFilename) ===
|
|
||||||
(item.resultId || item.taskId || item.fileKey || item.sourceFilename),
|
|
||||||
),
|
),
|
||||||
),
|
);
|
||||||
)
|
|
||||||
const hasVisibleItems = computed(() => currentSectionItems.value.length > 0 || historySectionItems.value.length > 0)
|
const hasVisibleItems = computed(() => currentSectionItems.value.length > 0 || historySectionItems.value.length > 0)
|
||||||
|
|
||||||
function getTaskStatus(taskId?: number) {
|
function getTaskStatus(taskId?: number) {
|
||||||
@@ -289,8 +313,9 @@ function getDisplayError(item: DeleteBrandResultItem) {
|
|||||||
|
|
||||||
function shouldShowProgress(item: DeleteBrandResultItem) {
|
function shouldShowProgress(item: DeleteBrandResultItem) {
|
||||||
const taskId = item.taskId
|
const taskId = item.taskId
|
||||||
if (!taskId || item.matched !== true) return false
|
if (!taskId) return false
|
||||||
const status = getTaskStatus(taskId)
|
const status = getTaskStatus(taskId)
|
||||||
|
// 只要状态是运行中就显示进度条,不管 matched 了(matched 不对的状态应该在后端就标记为 FAILED)
|
||||||
return status === 'RUNNING'
|
return status === 'RUNNING'
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -301,7 +326,6 @@ function normalizeDeleteBrandItems(items: DeleteBrandResultItem[]) {
|
|||||||
...item,
|
...item,
|
||||||
success: false,
|
success: false,
|
||||||
error: item.error || '未匹配到紫鸟店铺',
|
error: item.error || '未匹配到紫鸟店铺',
|
||||||
taskId: undefined,
|
|
||||||
openStoreUrl: undefined,
|
openStoreUrl: undefined,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -344,10 +368,10 @@ function formatProgress(taskId: number) {
|
|||||||
if (info.phase) parts.push(`阶段:${info.phase}`)
|
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_index && info.file_total) parts.push(`文件:${info.file_index}/${info.file_total}`)
|
||||||
if (info.file_name) parts.push(`当前文件:${info.file_name}`)
|
if (info.file_name) parts.push(`当前文件:${info.file_name}`)
|
||||||
if (info.current_line !== undefined && info.total_lines !== undefined) 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_country) parts.push(`国家:${info.current_country}`)
|
||||||
if (info.current_asin) parts.push(`ASIN:${info.current_asin}`)
|
if (info.current_asin) parts.push(`ASIN:${info.current_asin}`)
|
||||||
if (info.finished_files !== undefined) parts.push(`已完成文件:${info.finished_files}`)
|
if (info.finished_files != null) parts.push(`已完成文件:${info.finished_files}`)
|
||||||
return parts.join('|')
|
return parts.join('|')
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -356,10 +380,10 @@ function formatProgressPercent(taskId: number) {
|
|||||||
const info = taskDetails.value[taskId]?.line_progress?.info
|
const info = taskDetails.value[taskId]?.line_progress?.info
|
||||||
if (!info) return 0
|
if (!info) return 0
|
||||||
|
|
||||||
if (info.current_line !== undefined && info.total_lines && info.total_lines > 0) {
|
if (info.current_line != null && info.total_lines && info.total_lines > 0) {
|
||||||
return Math.max(0, Math.min(100, Math.round((info.current_line / info.total_lines) * 100)))
|
return Math.max(0, Math.min(100, Math.round((info.current_line / info.total_lines) * 100)))
|
||||||
}
|
}
|
||||||
if (info.finished_files !== undefined && info.file_total && info.file_total > 0) {
|
if (info.finished_files != null && info.file_total && info.file_total > 0) {
|
||||||
return Math.max(0, Math.min(100, Math.round((info.finished_files / info.file_total) * 100)))
|
return Math.max(0, Math.min(100, Math.round((info.finished_files / info.file_total) * 100)))
|
||||||
}
|
}
|
||||||
const status = taskDetails.value[taskId]?.task?.status
|
const status = taskDetails.value[taskId]?.task?.status
|
||||||
@@ -377,14 +401,34 @@ function isTaskTerminal(taskId: number) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function getPollingTaskIds() {
|
function getPollingTaskIds() {
|
||||||
return Array.from(
|
const ids = new Set<number>()
|
||||||
new Set(
|
|
||||||
currentRunItems.value
|
// 1. 检查本会话触发的任务
|
||||||
.filter((item) => item.matched && (item.taskId || 0) > 0)
|
currentRunItems.value.forEach((item) => {
|
||||||
.map(getItemTaskId)
|
if (item.taskId && !isTaskTerminal(item.taskId)) {
|
||||||
.filter((id) => id > 0 && !isTaskTerminal(id)),
|
ids.add(item.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 canDownloadTaskResult(item: DeleteBrandResultItem) {
|
||||||
@@ -415,9 +459,14 @@ function getPollIntervalMs() {
|
|||||||
return document.visibilityState === 'visible' ? 4500 : 10000
|
return document.visibilityState === 'visible' ? 4500 : 10000
|
||||||
}
|
}
|
||||||
|
|
||||||
function scheduleNextPoll() {
|
function scheduleNextPoll(immediate = false) {
|
||||||
if (pollTimer.value) return
|
if (pollTimer.value) {
|
||||||
pollTimer.value = window.setTimeout(async () => {
|
if (!immediate) return
|
||||||
|
clearTimeout(pollTimer.value)
|
||||||
|
pollTimer.value = null
|
||||||
|
}
|
||||||
|
|
||||||
|
const executePoll = async () => {
|
||||||
pollTimer.value = null
|
pollTimer.value = null
|
||||||
if (pollingInFlight.value) {
|
if (pollingInFlight.value) {
|
||||||
scheduleNextPoll()
|
scheduleNextPoll()
|
||||||
@@ -438,12 +487,35 @@ function scheduleNextPoll() {
|
|||||||
if (stillRunning) {
|
if (stillRunning) {
|
||||||
scheduleNextPoll()
|
scheduleNextPoll()
|
||||||
}
|
}
|
||||||
}, getPollIntervalMs())
|
}
|
||||||
|
|
||||||
|
if (immediate) {
|
||||||
|
executePoll()
|
||||||
|
} else {
|
||||||
|
pollTimer.value = window.setTimeout(executePoll, getPollIntervalMs())
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function ensurePolling() {
|
function ensurePolling(immediate = false) {
|
||||||
if (pollTimer.value) return
|
if (pollTimer.value && !immediate) return
|
||||||
scheduleNextPoll()
|
scheduleNextPoll(immediate)
|
||||||
|
}
|
||||||
|
|
||||||
|
function getTaskStatusInfo(item: DeleteBrandResultItem) {
|
||||||
|
// 1. 优先查实时详情中的状态
|
||||||
|
const status = item.taskId ? taskDetails.value[item.taskId]?.task?.status : null
|
||||||
|
if (status === 'SUCCESS') return { className: 'success', text: '已完成' }
|
||||||
|
if (status === 'FAILED') return { className: 'failed', text: '失败' }
|
||||||
|
if (status === 'RUNNING') return { className: 'running', text: '执行中' }
|
||||||
|
|
||||||
|
// 2. 其次看 history 接口带回来的后端任务状态
|
||||||
|
if (item.taskStatus === 'SUCCESS') return { className: 'success', text: '已完成' }
|
||||||
|
if (item.taskStatus === 'FAILED') return { className: 'failed', text: '失败' }
|
||||||
|
if (item.taskStatus === 'RUNNING') return { className: 'running', text: '执行中' }
|
||||||
|
|
||||||
|
// 3. 最后根据 item 本身的 success 标识兜底
|
||||||
|
if (item.success) return { className: 'success', text: '已完成' }
|
||||||
|
return { className: 'failed', text: '失败' }
|
||||||
}
|
}
|
||||||
|
|
||||||
function stopPolling() {
|
function stopPolling() {
|
||||||
@@ -597,8 +669,12 @@ async function loadHistory() {
|
|||||||
const response = await getDeleteBrandHistory()
|
const response = await getDeleteBrandHistory()
|
||||||
historyItems.value = normalizeDeleteBrandItems(response.items || [])
|
historyItems.value = normalizeDeleteBrandItems(response.items || [])
|
||||||
syncResultState()
|
syncResultState()
|
||||||
} catch {
|
|
||||||
// ignore history load errors
|
// 关键:页面加载历史后,立刻拉取详情触发状态机
|
||||||
|
await refreshTaskDetails()
|
||||||
|
ensurePolling(true)
|
||||||
|
} catch (err) {
|
||||||
|
console.error('loadHistory error:', err)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -712,6 +788,7 @@ onUnmounted(() => {
|
|||||||
.status { padding: 4px 10px; border-radius: 6px; font-size: 12px; white-space: nowrap; }
|
.status { padding: 4px 10px; border-radius: 6px; font-size: 12px; white-space: nowrap; }
|
||||||
.status.success { background: rgba(46, 204, 113, 0.18); color: #2ecc71; }
|
.status.success { background: rgba(46, 204, 113, 0.18); color: #2ecc71; }
|
||||||
.status.failed { background: rgba(231, 76, 60, 0.18); color: #ff6b6b; }
|
.status.failed { background: rgba(231, 76, 60, 0.18); color: #ff6b6b; }
|
||||||
|
.status.running { background: rgba(52, 152, 219, 0.18); color: #3498db; }
|
||||||
.download { padding: 6px 10px; border-radius: 6px; font-size: 12px; background: rgba(52, 152, 219, 0.18); color: #69b6ff; }
|
.download { padding: 6px 10px; border-radius: 6px; font-size: 12px; background: rgba(52, 152, 219, 0.18); color: #69b6ff; }
|
||||||
.download:hover { background: rgba(52, 152, 219, 0.28); }
|
.download:hover { background: rgba(52, 152, 219, 0.28); }
|
||||||
.btn-delete { padding: 6px 10px; border-radius: 6px; font-size: 12px; background: rgba(231, 76, 60, 0.12); color: #ff8f8f; }
|
.btn-delete { padding: 6px 10px; border-radius: 6px; font-size: 12px; background: rgba(231, 76, 60, 0.12); color: #ff8f8f; }
|
||||||
|
|||||||
@@ -229,6 +229,7 @@ export interface DeleteBrandResultItem {
|
|||||||
previewRows?: DeleteBrandPreviewRow[]
|
previewRows?: DeleteBrandPreviewRow[]
|
||||||
success: boolean
|
success: boolean
|
||||||
error?: string
|
error?: string
|
||||||
|
taskStatus?: string
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface DeleteBrandRunVo {
|
export interface DeleteBrandRunVo {
|
||||||
|
|||||||
Reference in New Issue
Block a user