feat(web): 前端 SPA 化 + 工具页任务面板统一与历史批量删除

- SPA 化:22 个 MPA html 入口与 *-main.ts 合并为 index.html + vue-router(URL 无 .html 后缀),
  页面跳转全部 router-link,/new_web_source/xxx.html 旧路径归一为 /xxx
- 任务面板统一:共享 TaskCenterPanel/TaskItemCard/TaskStatCards/HistoryTaskLayer,
  16 个工具页右侧统一为统计卡 + 当前任务 + 历史任务弹层(任务ID/开始/结束/状态必展示)
- 历史记录支持单条删除 + 批量勾选删除(确认框/全选/失败提示)
- Java 7 模块(dedupe/convert/split/productrisk/shopmatch/pricetrack/deletebrand)
  history 接口补齐任务时间字段(VO+Service,复用 biz_file_task 列)
- 图片工作台/API 层(brand/permission/user)既有未提交改动一并提交
This commit is contained in:
2026-09-08 10:02:55 +08:00
parent abcfa5bef7
commit e248b6e43a
125 changed files with 7482 additions and 4304 deletions
@@ -24,4 +24,19 @@ public class ConvertResultItemVo {
@Schema(description = "下载地址")
private String downloadUrl;
@Schema(description = "所属任务IDbiz_file_task.id")
private Long taskId;
@Schema(description = "任务状态:PENDING / RUNNING / SUCCESS / FAILED")
private String taskStatus;
@Schema(description = "任务创建时间(biz_file_task.created_at,格式化字符串)")
private String createdAt;
@Schema(description = "任务开始时间(biz_file_task.created_at")
private String startedAt;
@Schema(description = "任务结束时间(biz_file_task.finished_at,未结束为 null")
private String finishedAt;
}
@@ -201,12 +201,14 @@ public class ConvertRunService {
}
public List<ConvertResultItemVo> listHistory(Long userId) {
return fileResultMapper.selectList(new LambdaQueryWrapper<FileResultEntity>()
List<FileResultEntity> entities = fileResultMapper.selectList(new LambdaQueryWrapper<FileResultEntity>()
.eq(FileResultEntity::getModuleType, MODULE_TYPE)
.eq(FileResultEntity::getUserId, userId)
.orderByDesc(FileResultEntity::getCreatedAt)
.last("limit 200"))
.stream()
.last("limit 200"));
// 任务级信息(状态/开始/结束时间)来自 biz_file_task,按 task_id 批量联查
Map<Long, FileTaskEntity> taskMap = loadHistoryTaskMap(entities);
return entities.stream()
.map(entity -> {
ConvertResultItemVo vo = new ConvertResultItemVo();
vo.setResultId(entity.getId());
@@ -215,11 +217,46 @@ public class ConvertRunService {
vo.setDownloadUrl(entity.getResultFileUrl() == null ? null : ossStorageService.generateFreshDownloadUrl(entity.getResultFileUrl()));
vo.setSuccess(entity.getSuccess() != null && entity.getSuccess() == 1);
vo.setError(entity.getErrorMessage());
FileTaskEntity task = entity.getTaskId() == null ? null : taskMap.get(entity.getTaskId());
vo.setTaskId(entity.getTaskId());
vo.setTaskStatus(task == null ? null : task.getStatus());
vo.setCreatedAt(fmt(task == null ? entity.getCreatedAt() : task.getCreatedAt()));
// 任务开始时间复用 biz_file_task.created_at;缺 task 时回退到 result.createdAt 兜底
vo.setStartedAt(fmt(task == null ? entity.getCreatedAt() : task.getCreatedAt()));
vo.setFinishedAt(fmt(task == null ? null : task.getFinishedAt()));
return vo;
})
.toList();
}
/** 批量加载转换结果关联的任务信息(列裁剪,只取状态与时间) */
private Map<Long, FileTaskEntity> loadHistoryTaskMap(List<FileResultEntity> entities) {
Map<Long, FileTaskEntity> taskMap = new LinkedHashMap<>();
List<Long> taskIds = entities.stream()
.map(FileResultEntity::getTaskId)
.filter(id -> id != null && id > 0)
.distinct()
.toList();
if (taskIds.isEmpty()) {
return taskMap;
}
for (FileTaskEntity task : fileTaskMapper.selectList(new LambdaQueryWrapper<FileTaskEntity>()
.select(FileTaskEntity::getId,
FileTaskEntity::getStatus,
FileTaskEntity::getCreatedAt,
FileTaskEntity::getFinishedAt)
.in(FileTaskEntity::getId, taskIds))) {
if (task != null) {
taskMap.put(task.getId(), task);
}
}
return taskMap;
}
private static String fmt(LocalDateTime t) {
return t == null ? null : t.toString();
}
public void deleteHistory(Long resultId, Long userId) {
FileResultEntity entity = fileResultMapper.selectById(resultId);
if (entity == null || !MODULE_TYPE.equals(entity.getModuleType()) || !userId.equals(entity.getUserId())) {
@@ -27,4 +27,19 @@ public class DedupeResultItemVo {
@Schema(description = "结果文件大小(字节)")
private Long resultFileSize;
@Schema(description = "所属任务IDbiz_file_task.id")
private Long taskId;
@Schema(description = "任务状态:PENDING / RUNNING / SUCCESS / FAILED")
private String taskStatus;
@Schema(description = "结果记录创建时间(格式化字符串)")
private String createdAt;
@Schema(description = "任务开始时间(biz_file_task.created_at")
private String startedAt;
@Schema(description = "任务结束时间(biz_file_task.finished_at,未结束为 null")
private String finishedAt;
}
@@ -389,12 +389,14 @@ public class DedupeRunService {
}
public List<DedupeResultItemVo> listHistory(Long userId) {
return fileResultMapper.selectList(new LambdaQueryWrapper<FileResultEntity>()
List<FileResultEntity> entities = fileResultMapper.selectList(new LambdaQueryWrapper<FileResultEntity>()
.eq(FileResultEntity::getModuleType, "DEDUPE")
.eq(FileResultEntity::getUserId, userId)
.orderByDesc(FileResultEntity::getCreatedAt)
.last("limit 50"))
.stream()
.last("limit 50"));
// 任务级信息(状态/开始/结束时间)来自 biz_file_task,按 task_id 批量联查
Map<Long, FileTaskEntity> taskMap = loadHistoryTaskMap(entities);
return entities.stream()
.map(entity -> {
DedupeResultItemVo vo = new DedupeResultItemVo();
vo.setResultId(entity.getId());
@@ -403,11 +405,46 @@ public class DedupeRunService {
vo.setDownloadUrl(entity.getResultFileUrl() == null ? null : ossStorageService.generateFreshDownloadUrl(entity.getResultFileUrl()));
vo.setSuccess(entity.getSuccess() != null && entity.getSuccess() == 1);
vo.setError(entity.getErrorMessage());
FileTaskEntity task = entity.getTaskId() == null ? null : taskMap.get(entity.getTaskId());
vo.setTaskId(entity.getTaskId());
vo.setTaskStatus(task == null ? null : task.getStatus());
vo.setCreatedAt(fmt(task == null ? entity.getCreatedAt() : task.getCreatedAt()));
// 任务开始时间复用 biz_file_task.created_at;缺 task 时回退到 result.createdAt 兜底
vo.setStartedAt(fmt(task == null ? entity.getCreatedAt() : task.getCreatedAt()));
vo.setFinishedAt(fmt(task == null ? null : task.getFinishedAt()));
return vo;
})
.toList();
}
/** 批量加载去重结果关联的任务信息(复用 appearancepatent 的列裁剪写法) */
private Map<Long, FileTaskEntity> loadHistoryTaskMap(List<FileResultEntity> entities) {
Map<Long, FileTaskEntity> taskMap = new LinkedHashMap<>();
List<Long> taskIds = entities.stream()
.map(FileResultEntity::getTaskId)
.filter(id -> id != null && id > 0)
.distinct()
.toList();
if (taskIds.isEmpty()) {
return taskMap;
}
for (FileTaskEntity task : fileTaskMapper.selectList(new LambdaQueryWrapper<FileTaskEntity>()
.select(FileTaskEntity::getId,
FileTaskEntity::getStatus,
FileTaskEntity::getCreatedAt,
FileTaskEntity::getFinishedAt)
.in(FileTaskEntity::getId, taskIds))) {
if (task != null) {
taskMap.put(task.getId(), task);
}
}
return taskMap;
}
private static String fmt(LocalDateTime t) {
return t == null ? null : t.toString();
}
public void deleteHistory(Long resultId, Long userId) {
FileResultEntity entity = fileResultMapper.selectById(resultId);
if (entity == null || !"DEDUPE".equals(entity.getModuleType()) || !userId.equals(entity.getUserId())) {
@@ -78,4 +78,14 @@ public class DeleteBrandResultItemVo {
@Schema(description = "由于列表来自 history 接口,增加所属任务的状态(RUNNING/SUCCESS/FAILED),协助前端准确定位执行中任务")
private String taskStatus;
/**
* 任务创建时间(biz_file_task.created_at,格式化字符串),列表接口返回。
*/
private String createdAt;
/**
* 任务结束时间(biz_file_task.finished_at,未结束为 null),列表接口返回。
*/
private String finishedAt;
}
@@ -293,6 +293,7 @@ public class DeleteBrandRunService {
List<FileTaskEntity> recentTouchedTasks = fileTaskMapper.selectList(new LambdaQueryWrapper<FileTaskEntity>()
.select(FileTaskEntity::getId,
FileTaskEntity::getStatus,
FileTaskEntity::getCreatedAt,
FileTaskEntity::getUpdatedAt,
FileTaskEntity::getFinishedAt)
.eq(FileTaskEntity::getModuleType, MODULE_TYPE)
@@ -375,6 +376,9 @@ public class DeleteBrandRunService {
// 真实补充
if (entity.getTaskId() != null) {
item.setTaskStatus(statusByTaskId.get(entity.getTaskId()));
FileTaskEntity taskEntity = taskById.get(entity.getTaskId());
item.setCreatedAt(fmt(taskEntity == null ? null : taskEntity.getCreatedAt()));
item.setFinishedAt(fmt(taskEntity == null ? null : taskEntity.getFinishedAt()));
List<DeleteBrandResultItemVo> taskItems = null;
if (taskItems != null && entity.getSourceFilename() != null && !entity.getSourceFilename().isBlank()) {
for (DeleteBrandResultItemVo candidate : taskItems) {
@@ -1234,6 +1238,7 @@ public class DeleteBrandRunService {
tasks.addAll(fileTaskMapper.selectList(new LambdaQueryWrapper<FileTaskEntity>()
.select(FileTaskEntity::getId,
FileTaskEntity::getStatus,
FileTaskEntity::getCreatedAt,
FileTaskEntity::getUpdatedAt,
FileTaskEntity::getFinishedAt)
.in(FileTaskEntity::getId, taskIds.subList(start, end))));
@@ -1282,6 +1287,10 @@ public class DeleteBrandRunService {
return Math.max(0L, (endExclusive - startInclusive) / 1_000_000L);
}
private static String fmt(LocalDateTime t) {
return t == null ? null : t.toString();
}
@Transactional(propagation = org.springframework.transaction.annotation.Propagation.NEVER)
public void submitResult(Long taskId, DeleteBrandSubmitResultRequest request) {
if (taskId == null || taskId <= 0) {
@@ -62,4 +62,14 @@ public class PriceTrackResultItemVo {
@Schema(description = "所属轮次,1-based", example = "2")
private Integer roundIndex;
/**
* 任务创建时间(biz_file_task.created_at,格式化字符串),列表接口返回。
*/
private String createdAt;
/**
* 任务结束时间(biz_file_task.finished_at,未结束为 null),列表接口返回。
*/
private String finishedAt;
}
@@ -142,7 +142,7 @@ public class PriceTrackTaskService {
return tasks;
}
private List<FileTaskEntity> selectTaskStatusesByIdsInBatches(List<Long> taskIds) {
private List<FileTaskEntity> selectTaskHistoryFieldsByIdsInBatches(List<Long> taskIds) {
List<FileTaskEntity> tasks = new ArrayList<>();
if (taskIds == null || taskIds.isEmpty()) {
return tasks;
@@ -151,7 +151,8 @@ public class PriceTrackTaskService {
for (int start = 0; start < taskIds.size(); start += batchSize) {
int end = Math.min(start + batchSize, taskIds.size());
tasks.addAll(fileTaskMapper.selectList(new LambdaQueryWrapper<FileTaskEntity>()
.select(FileTaskEntity::getId, FileTaskEntity::getStatus)
.select(FileTaskEntity::getId, FileTaskEntity::getStatus,
FileTaskEntity::getCreatedAt, FileTaskEntity::getFinishedAt)
.in(FileTaskEntity::getId, taskIds.subList(start, end))));
}
return tasks;
@@ -208,16 +209,16 @@ public class PriceTrackTaskService {
userId, elapsedMs(startedAt, resultRowsLoadedAt), elapsedMs(startedAt, resultRowsLoadedAt));
return vo;
}
Map<Long, String> statusByTaskId = new LinkedHashMap<>();
Map<Long, FileTaskEntity> taskById = new LinkedHashMap<>();
List<Long> taskIds = entities.stream()
.map(FileResultEntity::getTaskId)
.filter(id -> id != null && id > 0)
.distinct()
.toList();
if (!taskIds.isEmpty()) {
List<FileTaskEntity> tasks = selectTaskStatusesByIdsInBatches(taskIds);
List<FileTaskEntity> tasks = selectTaskHistoryFieldsByIdsInBatches(taskIds);
for (FileTaskEntity t : tasks) {
if (t != null) statusByTaskId.put(t.getId(), t.getStatus());
if (t != null) taskById.put(t.getId(), t);
}
}
long tasksLoadedAt = System.nanoTime();
@@ -229,14 +230,14 @@ public class PriceTrackTaskService {
long jobsLoadedAt = System.nanoTime();
List<PriceTrackResultItemVo> items = new ArrayList<>();
for (FileResultEntity entity : entities) {
items.add(toHistoryItemVo(entity, statusByTaskId.get(entity.getTaskId()), jobMap.get(entity.getId()), false));
items.add(toHistoryItemVo(entity, taskById.get(entity.getTaskId()), jobMap.get(entity.getId()), false));
}
vo.setItems(items);
long finishedAt = System.nanoTime();
log.info("[price-track] history timing userId={} rows={} tasks={} jobs={} totalMs={} resultQueryMs={} taskQueryMs={} jobQueryMs={} buildMs={}",
userId,
entities.size(),
statusByTaskId.size(),
taskById.size(),
jobMap.size(),
elapsedMs(startedAt, finishedAt),
elapsedMs(startedAt, resultRowsLoadedAt),
@@ -1720,16 +1721,32 @@ public class PriceTrackTaskService {
}
private PriceTrackResultItemVo toHistoryItemVo(FileResultEntity entity, String taskStatus) {
return toHistoryItemVo(entity, taskStatus, null, true);
// 快照路径:仅有任务状态字符串,无任务实体可提供时间字段
return buildHistoryItemVo(entity, taskStatus, null, null, true);
}
private PriceTrackResultItemVo toHistoryItemVo(FileResultEntity entity, FileTaskEntity task) {
return buildHistoryItemVo(entity, task == null ? null : task.getStatus(), task, null, true);
}
private PriceTrackResultItemVo toHistoryItemVo(FileResultEntity entity, String taskStatus, TaskFileJobEntity job, boolean mergeRequestFields) {
return buildHistoryItemVo(entity, taskStatus, null, job, mergeRequestFields);
}
private PriceTrackResultItemVo toHistoryItemVo(FileResultEntity entity, FileTaskEntity task, TaskFileJobEntity job, boolean mergeRequestFields) {
return buildHistoryItemVo(entity, task == null ? null : task.getStatus(), task, job, mergeRequestFields);
}
private PriceTrackResultItemVo buildHistoryItemVo(FileResultEntity entity, String taskStatus, FileTaskEntity task,
TaskFileJobEntity job, boolean mergeRequestFields) {
PriceTrackResultItemVo vo = new PriceTrackResultItemVo();
vo.setResultId(entity.getId());
vo.setTaskId(entity.getTaskId());
vo.setShopName(entity.getSourceFilename());
vo.setShopId(entity.getSourceFileUrl());
vo.setTaskStatus(taskStatus);
vo.setCreatedAt(fmt(task == null ? null : task.getCreatedAt()));
vo.setFinishedAt(fmt(task == null ? null : task.getFinishedAt()));
boolean ok = entity.getSuccess() != null && entity.getSuccess() == 1;
vo.setSuccess(ok);
vo.setError(entity.getErrorMessage());
@@ -1767,7 +1784,7 @@ public class PriceTrackTaskService {
.eq(FileResultEntity::getModuleType, MODULE_TYPE)
.orderByAsc(FileResultEntity::getId));
for (FileResultEntity fr : rows) {
detail.getItems().add(toHistoryItemVo(fr, task.getStatus()));
detail.getItems().add(toHistoryItemVo(fr, task));
}
return detail;
}
@@ -79,4 +79,12 @@ public class ProductRiskResultItemVo {
@JsonAlias("skip_asin_details_by_country")
@JsonProperty("skipAsinDetailsByCountry")
private Map<String, List<SkipPriceAsinDetailDto>> skipAsinDetailsByCountry = new LinkedHashMap<>();
@JsonProperty("createdAt")
@Schema(description = "任务创建时间(biz_file_task.created_at,格式化字符串)")
private String createdAt;
@JsonProperty("finishedAt")
@Schema(description = "任务结束时间(biz_file_task.finished_at,未结束为 null")
private String finishedAt;
}
@@ -133,7 +133,7 @@ public class ProductRiskTaskService {
return tasks;
}
private List<FileTaskEntity> selectTaskStatusesByIdsInBatches(List<Long> taskIds) {
private List<FileTaskEntity> selectTaskHistoryFieldsByIdsInBatches(List<Long> taskIds) {
List<FileTaskEntity> tasks = new ArrayList<>();
if (taskIds == null || taskIds.isEmpty()) {
return tasks;
@@ -142,7 +142,8 @@ public class ProductRiskTaskService {
for (int start = 0; start < taskIds.size(); start += batchSize) {
int end = Math.min(start + batchSize, taskIds.size());
tasks.addAll(fileTaskMapper.selectList(new LambdaQueryWrapper<FileTaskEntity>()
.select(FileTaskEntity::getId, FileTaskEntity::getStatus)
.select(FileTaskEntity::getId, FileTaskEntity::getStatus,
FileTaskEntity::getCreatedAt, FileTaskEntity::getFinishedAt)
.in(FileTaskEntity::getId, taskIds.subList(start, end))));
}
return tasks;
@@ -202,17 +203,17 @@ public class ProductRiskTaskService {
userId, elapsedMs(startedAt, resultRowsLoadedAt), elapsedMs(startedAt, resultRowsLoadedAt));
return vo;
}
Map<Long, String> statusByTaskId = new LinkedHashMap<>();
Map<Long, FileTaskEntity> taskById = new LinkedHashMap<>();
List<Long> taskIds = entities.stream()
.map(FileResultEntity::getTaskId)
.filter(id -> id != null && id > 0)
.distinct()
.toList();
if (!taskIds.isEmpty()) {
List<FileTaskEntity> tasks = selectTaskStatusesByIdsInBatches(taskIds);
List<FileTaskEntity> tasks = selectTaskHistoryFieldsByIdsInBatches(taskIds);
for (FileTaskEntity t : tasks) {
if (t != null) {
statusByTaskId.put(t.getId(), t.getStatus());
taskById.put(t.getId(), t);
}
}
}
@@ -225,14 +226,14 @@ public class ProductRiskTaskService {
long jobsLoadedAt = System.nanoTime();
List<ProductRiskResultItemVo> items = new ArrayList<>();
for (FileResultEntity entity : entities) {
items.add(toHistoryItemVo(entity, statusByTaskId.get(entity.getTaskId()), jobMap.get(entity.getId()), false));
items.add(toHistoryItemVo(entity, taskById.get(entity.getTaskId()), jobMap.get(entity.getId()), false));
}
vo.setItems(items);
long finishedAt = System.nanoTime();
log.info("[product-risk] history timing userId={} rows={} tasks={} jobs={} totalMs={} resultQueryMs={} taskQueryMs={} jobQueryMs={} buildMs={}",
userId,
entities.size(),
statusByTaskId.size(),
taskById.size(),
jobMap.size(),
elapsedMs(startedAt, finishedAt),
elapsedMs(startedAt, resultRowsLoadedAt),
@@ -999,7 +1000,7 @@ public class ProductRiskTaskService {
.eq(FileResultEntity::getModuleType, MODULE_TYPE)
.orderByAsc(FileResultEntity::getId));
for (FileResultEntity fr : rows) {
detail.getItems().add(toHistoryItemVo(fr, task.getStatus()));
detail.getItems().add(toHistoryItemVo(fr, task));
}
return detail;
}
@@ -1048,16 +1049,32 @@ public class ProductRiskTaskService {
}
private ProductRiskResultItemVo toHistoryItemVo(FileResultEntity entity, String taskStatus) {
return toHistoryItemVo(entity, taskStatus, null, true);
// 快照路径:仅有任务状态字符串,无任务实体可提供时间字段
return buildHistoryItemVo(entity, taskStatus, null, null, true);
}
private ProductRiskResultItemVo toHistoryItemVo(FileResultEntity entity, FileTaskEntity task) {
return buildHistoryItemVo(entity, task == null ? null : task.getStatus(), task, null, true);
}
private ProductRiskResultItemVo toHistoryItemVo(FileResultEntity entity, String taskStatus, TaskFileJobEntity job, boolean mergeRequestFields) {
return buildHistoryItemVo(entity, taskStatus, null, job, mergeRequestFields);
}
private ProductRiskResultItemVo toHistoryItemVo(FileResultEntity entity, FileTaskEntity task, TaskFileJobEntity job, boolean mergeRequestFields) {
return buildHistoryItemVo(entity, task == null ? null : task.getStatus(), task, job, mergeRequestFields);
}
private ProductRiskResultItemVo buildHistoryItemVo(FileResultEntity entity, String taskStatus, FileTaskEntity task,
TaskFileJobEntity job, boolean mergeRequestFields) {
ProductRiskResultItemVo vo = new ProductRiskResultItemVo();
vo.setResultId(entity.getId());
vo.setTaskId(entity.getTaskId());
vo.setShopName(entity.getSourceFilename());
vo.setShopId(entity.getSourceFileUrl());
vo.setTaskStatus(taskStatus);
vo.setCreatedAt(fmt(task == null ? null : task.getCreatedAt()));
vo.setFinishedAt(fmt(task == null ? null : task.getFinishedAt()));
boolean ok = entity.getSuccess() != null && entity.getSuccess() == 1;
vo.setSuccess(ok);
vo.setError(entity.getErrorMessage());
@@ -168,7 +168,7 @@ public class ShopMatchTaskService {
return tasks;
}
private List<FileTaskEntity> selectTaskStatusesByIdsInBatches(List<Long> taskIds) {
private List<FileTaskEntity> selectTaskHistoryFieldsByIdsInBatches(List<Long> taskIds) {
List<FileTaskEntity> tasks = new ArrayList<>();
if (taskIds == null || taskIds.isEmpty()) {
return tasks;
@@ -177,7 +177,8 @@ public class ShopMatchTaskService {
for (int start = 0; start < taskIds.size(); start += batchSize) {
int end = Math.min(start + batchSize, taskIds.size());
tasks.addAll(fileTaskMapper.selectList(new LambdaQueryWrapper<FileTaskEntity>()
.select(FileTaskEntity::getId, FileTaskEntity::getStatus)
.select(FileTaskEntity::getId, FileTaskEntity::getStatus,
FileTaskEntity::getCreatedAt, FileTaskEntity::getFinishedAt)
.in(FileTaskEntity::getId, taskIds.subList(start, end))));
}
return tasks;
@@ -237,16 +238,16 @@ public class ShopMatchTaskService {
userId, elapsedMs(startedAt, resultRowsLoadedAt), elapsedMs(startedAt, resultRowsLoadedAt));
return vo;
}
Map<Long, String> statusByTaskId = new LinkedHashMap<>();
Map<Long, FileTaskEntity> taskById = new LinkedHashMap<>();
List<Long> taskIds = entities.stream()
.map(FileResultEntity::getTaskId)
.filter(id -> id != null && id > 0)
.distinct()
.toList();
if (!taskIds.isEmpty()) {
for (FileTaskEntity task : selectTaskStatusesByIdsInBatches(taskIds)) {
for (FileTaskEntity task : selectTaskHistoryFieldsByIdsInBatches(taskIds)) {
if (task != null) {
statusByTaskId.put(task.getId(), task.getStatus());
taskById.put(task.getId(), task);
}
}
}
@@ -259,14 +260,14 @@ public class ShopMatchTaskService {
long jobsLoadedAt = System.nanoTime();
List<ProductRiskResultItemVo> items = new ArrayList<>();
for (FileResultEntity entity : entities) {
items.add(toHistoryItemVo(entity, statusByTaskId.get(entity.getTaskId()), jobMap.get(entity.getId()), false));
items.add(toHistoryItemVo(entity, taskById.get(entity.getTaskId()), jobMap.get(entity.getId()), false));
}
vo.setItems(items);
long finishedAt = System.nanoTime();
log.info("[shop-match] history timing userId={} rows={} tasks={} jobs={} totalMs={} resultQueryMs={} taskQueryMs={} jobQueryMs={} buildMs={}",
userId,
entities.size(),
statusByTaskId.size(),
taskById.size(),
jobMap.size(),
elapsedMs(startedAt, finishedAt),
elapsedMs(startedAt, resultRowsLoadedAt),
@@ -963,7 +964,7 @@ public class ShopMatchTaskService {
.eq(FileResultEntity::getModuleType, MODULE_TYPE)
.orderByAsc(FileResultEntity::getId));
for (FileResultEntity row : rows) {
detail.getItems().add(toHistoryItemVo(row, task.getStatus()));
detail.getItems().add(toHistoryItemVo(row, task));
}
return detail;
}
@@ -1031,16 +1032,32 @@ public class ShopMatchTaskService {
}
private ProductRiskResultItemVo toHistoryItemVo(FileResultEntity entity, String taskStatus) {
return toHistoryItemVo(entity, taskStatus, null, true);
// 快照路径:仅有任务状态字符串,无任务实体可提供时间字段
return buildHistoryItemVo(entity, taskStatus, null, null, true);
}
private ProductRiskResultItemVo toHistoryItemVo(FileResultEntity entity, FileTaskEntity task) {
return buildHistoryItemVo(entity, task == null ? null : task.getStatus(), task, null, true);
}
private ProductRiskResultItemVo toHistoryItemVo(FileResultEntity entity, String taskStatus, TaskFileJobEntity job, boolean mergeRequestFields) {
return buildHistoryItemVo(entity, taskStatus, null, job, mergeRequestFields);
}
private ProductRiskResultItemVo toHistoryItemVo(FileResultEntity entity, FileTaskEntity task, TaskFileJobEntity job, boolean mergeRequestFields) {
return buildHistoryItemVo(entity, task == null ? null : task.getStatus(), task, job, mergeRequestFields);
}
private ProductRiskResultItemVo buildHistoryItemVo(FileResultEntity entity, String taskStatus, FileTaskEntity task,
TaskFileJobEntity job, boolean mergeRequestFields) {
ProductRiskResultItemVo vo = new ProductRiskResultItemVo();
vo.setResultId(entity.getId());
vo.setTaskId(entity.getTaskId());
vo.setShopName(entity.getSourceFilename());
vo.setShopId(entity.getSourceFileUrl());
vo.setTaskStatus(taskStatus);
vo.setCreatedAt(fmt(task == null ? null : task.getCreatedAt()));
vo.setFinishedAt(fmt(task == null ? null : task.getFinishedAt()));
boolean success = entity.getSuccess() != null && entity.getSuccess() == 1;
vo.setSuccess(success);
vo.setError(entity.getErrorMessage());
@@ -35,4 +35,19 @@ public class SplitResultItemVo {
@Schema(description = "压缩包内文件列表")
private List<SplitArchiveEntryVo> entries;
@Schema(description = "所属任务IDbiz_file_task.id")
private Long taskId;
@Schema(description = "任务状态:PENDING / RUNNING / SUCCESS / FAILED")
private String taskStatus;
@Schema(description = "任务创建时间(biz_file_task.created_at,格式化字符串)")
private String createdAt;
@Schema(description = "任务开始时间(biz_file_task.created_at")
private String startedAt;
@Schema(description = "任务结束时间(biz_file_task.finished_at,未结束为 null")
private String finishedAt;
}
@@ -29,6 +29,7 @@ import java.time.LocalDateTime;
import java.time.format.DateTimeFormatter;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.concurrent.atomic.AtomicReference;
@@ -161,12 +162,14 @@ public class SplitRunService {
}
public List<SplitResultItemVo> listHistory(Long userId) {
return fileResultMapper.selectList(new LambdaQueryWrapper<FileResultEntity>()
List<FileResultEntity> entities = fileResultMapper.selectList(new LambdaQueryWrapper<FileResultEntity>()
.eq(FileResultEntity::getModuleType, MODULE_TYPE)
.eq(FileResultEntity::getUserId, userId)
.orderByDesc(FileResultEntity::getCreatedAt)
.last("limit 100"))
.stream()
.last("limit 100"));
// 任务级信息(状态/开始/结束时间)来自 biz_file_task,按 task_id 批量联查
Map<Long, FileTaskEntity> taskMap = loadHistoryTaskMap(entities);
return entities.stream()
.map(entity -> {
SplitResultItemVo vo = new SplitResultItemVo();
vo.setResultId(entity.getId());
@@ -178,11 +181,46 @@ public class SplitRunService {
vo.setError(entity.getErrorMessage());
vo.setEntryCount(0);
vo.setEntries(new ArrayList<>());
FileTaskEntity task = entity.getTaskId() == null ? null : taskMap.get(entity.getTaskId());
vo.setTaskId(entity.getTaskId());
vo.setTaskStatus(task == null ? null : task.getStatus());
vo.setCreatedAt(fmt(task == null ? entity.getCreatedAt() : task.getCreatedAt()));
// 任务开始时间复用 biz_file_task.created_at;缺 task 时回退到 result.createdAt 兜底
vo.setStartedAt(fmt(task == null ? entity.getCreatedAt() : task.getCreatedAt()));
vo.setFinishedAt(fmt(task == null ? null : task.getFinishedAt()));
return vo;
})
.toList();
}
/** 批量加载拆分结果关联的任务信息(列裁剪,只取状态与时间) */
private Map<Long, FileTaskEntity> loadHistoryTaskMap(List<FileResultEntity> entities) {
Map<Long, FileTaskEntity> taskMap = new LinkedHashMap<>();
List<Long> taskIds = entities.stream()
.map(FileResultEntity::getTaskId)
.filter(id -> id != null && id > 0)
.distinct()
.toList();
if (taskIds.isEmpty()) {
return taskMap;
}
for (FileTaskEntity task : fileTaskMapper.selectList(new LambdaQueryWrapper<FileTaskEntity>()
.select(FileTaskEntity::getId,
FileTaskEntity::getStatus,
FileTaskEntity::getCreatedAt,
FileTaskEntity::getFinishedAt)
.in(FileTaskEntity::getId, taskIds))) {
if (task != null) {
taskMap.put(task.getId(), task);
}
}
return taskMap;
}
private static String fmt(LocalDateTime t) {
return t == null ? null : t.toString();
}
public void deleteHistory(Long resultId, Long userId) {
FileResultEntity entity = fileResultMapper.selectById(resultId);
if (entity == null || !MODULE_TYPE.equals(entity.getModuleType()) || !userId.equals(entity.getUserId())) {