修复菜单消失
This commit is contained in:
@@ -38,4 +38,8 @@ public class CollectDataSubmitResultRequest {
|
||||
@JsonAlias({"rows", "data", "items"})
|
||||
@Schema(description = "本分片回传数据行")
|
||||
private List<CollectDataSubmitRowDto> items = new ArrayList<>();
|
||||
|
||||
@JsonAlias({"summary", "summary_rows", "汇总"})
|
||||
@Schema(description = "Python 端按关键词聚合的配送方式 / 页数统计;通常仅在 done=true 时携带,空数组等同于不携带,Java 回退到自聚合 rawItems。")
|
||||
private List<CollectDataSummaryRowDto> summaries = new ArrayList<>();
|
||||
}
|
||||
|
||||
@@ -0,0 +1,54 @@
|
||||
package com.nanri.aiimage.modules.collectdata.model.dto;
|
||||
|
||||
import com.fasterxml.jackson.annotation.JsonAlias;
|
||||
import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
|
||||
import com.fasterxml.jackson.annotation.JsonProperty;
|
||||
import io.swagger.v3.oas.annotations.media.Schema;
|
||||
import lombok.Data;
|
||||
|
||||
@Data
|
||||
@JsonIgnoreProperties(ignoreUnknown = true)
|
||||
@Schema(description = "采集数据 Python 关键词级聚合统计行,通常仅在 done=true 时携带")
|
||||
public class CollectDataSummaryRowDto {
|
||||
|
||||
/**
|
||||
* 关键词原文。
|
||||
*/
|
||||
@JsonAlias({"关键词", "key_word", "key word"})
|
||||
@Schema(description = "关键词原文", example = "phone case")
|
||||
private String keyword;
|
||||
|
||||
/**
|
||||
* 该关键词命中 FBA 配送方式的行数。
|
||||
*/
|
||||
@Schema(description = "FBA 行数", example = "12")
|
||||
private Integer fba;
|
||||
|
||||
/**
|
||||
* 该关键词命中 FBM 配送方式的行数。
|
||||
*/
|
||||
@Schema(description = "FBM 行数", example = "8")
|
||||
private Integer fbm;
|
||||
|
||||
/**
|
||||
* 该关键词命中 AMZ 配送方式的行数。
|
||||
*/
|
||||
@Schema(description = "AMZ 行数", example = "5")
|
||||
private Integer amz;
|
||||
|
||||
/**
|
||||
* 该关键词「无配送方式」(未识别)行数。
|
||||
*/
|
||||
@JsonProperty("none_count")
|
||||
@JsonAlias({"none", "unknown", "无配送方式", "noneCount"})
|
||||
@Schema(description = "无配送方式(未识别 FBA/FBM/AMZ)行数", example = "3")
|
||||
private Integer noneCount;
|
||||
|
||||
/**
|
||||
* 该关键词所有命中行的最大页码,由 Python 抓取端汇总。
|
||||
*/
|
||||
@JsonProperty("total_page")
|
||||
@JsonAlias({"page", "totalPage", "页数", "max_page", "maxPage"})
|
||||
@Schema(description = "该关键词总页数(Python 端取所有命中行的最大页码)", example = "10")
|
||||
private Integer totalPage;
|
||||
}
|
||||
@@ -1,6 +1,7 @@
|
||||
package com.nanri.aiimage.modules.collectdata.service;
|
||||
|
||||
import com.nanri.aiimage.common.exception.BusinessException;
|
||||
import com.nanri.aiimage.modules.collectdata.model.dto.CollectDataSummaryRowDto;
|
||||
import com.nanri.aiimage.modules.collectdata.model.vo.CollectDataResultRowVo;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.apache.poi.ss.usermodel.Row;
|
||||
@@ -30,14 +31,18 @@ public class CollectDataExcelAssemblyService {
|
||||
*
|
||||
* @param outputXlsx 目标文件
|
||||
* @param items 经过 ASIN 去重 / 无效品牌过滤 / 品牌检测后保留下来的明细行,写入「采集数据结果」sheet
|
||||
* @param rawItems Python 回传的全量原始行(不经后端任何过滤),写入「结果文件」sheet 的关键词聚合统计
|
||||
* @param summaries Python 在 done=true 时携带的关键词级聚合统计;非空时优先使用,直接落「结果文件」sheet
|
||||
* @param rawItems Python 回传的全量原始行(不经后端任何过滤);仅在 summaries 为空时作为 fallback 自聚合
|
||||
*/
|
||||
public void writeWorkbook(File outputXlsx, List<CollectDataResultRowVo> items, List<CollectDataResultRowVo> rawItems) {
|
||||
public void writeWorkbook(File outputXlsx,
|
||||
List<CollectDataResultRowVo> items,
|
||||
List<CollectDataSummaryRowDto> summaries,
|
||||
List<CollectDataResultRowVo> rawItems) {
|
||||
SXSSFWorkbook workbook = new SXSSFWorkbook(200);
|
||||
workbook.setCompressTempFiles(true);
|
||||
try (FileOutputStream outputStream = new FileOutputStream(outputXlsx)) {
|
||||
writeDetailSheet(workbook, items);
|
||||
writeSummarySheet(workbook, rawItems);
|
||||
writeSummarySheet(workbook, summaries, rawItems);
|
||||
workbook.write(outputStream);
|
||||
} catch (Exception ex) {
|
||||
log.warn("[collect-data] write workbook failed: {}", ex.getMessage());
|
||||
@@ -76,11 +81,14 @@ public class CollectDataExcelAssemblyService {
|
||||
}
|
||||
|
||||
/**
|
||||
* 「结果文件」sheet 基于 Python 回传的全量原始数据按关键词聚合:
|
||||
* - FBA / FBM / AMZ / 无配送方式 列为各分类的命中行数;
|
||||
* - 页数列取该关键词所有原始行中页码的最大值,反映 Python 实际抓取到的总页数。
|
||||
* 「结果文件」sheet 的关键词级聚合:
|
||||
* - 优先使用 Python 在 done=true 时携带的 summaries,避免 Java 端二次聚合带来的口径漂移;
|
||||
* - 当 summaries 为空(Python 端尚未接入或解析失败)时,回退到基于 rawItems 自聚合,
|
||||
* 保证 Excel 不会因迁移过渡期而出现空表。等 Python 端全部接入并稳定后,可移除 fallback 分支。
|
||||
*/
|
||||
private void writeSummarySheet(SXSSFWorkbook workbook, List<CollectDataResultRowVo> rawItems) {
|
||||
private void writeSummarySheet(SXSSFWorkbook workbook,
|
||||
List<CollectDataSummaryRowDto> summaries,
|
||||
List<CollectDataResultRowVo> rawItems) {
|
||||
Sheet sheet = workbook.createSheet(SHEET_SUMMARY_NAME);
|
||||
Row headerRow = sheet.createRow(0);
|
||||
for (int i = 0; i < SHEET_SUMMARY_HEADER.length; i++) {
|
||||
@@ -88,6 +96,30 @@ public class CollectDataExcelAssemblyService {
|
||||
sheet.setColumnWidth(i, (i == 0 ? 28 : 12) * 256);
|
||||
}
|
||||
|
||||
if (summaries != null && !summaries.isEmpty()) {
|
||||
// 优先分支:Python 携带的关键词级聚合直接落表,null 字段统一兜底为 0;页数 ≤0 写空字符串。
|
||||
int rowIndex = 1;
|
||||
for (CollectDataSummaryRowDto item : summaries) {
|
||||
if (item == null) {
|
||||
continue;
|
||||
}
|
||||
Row row = sheet.createRow(rowIndex++);
|
||||
row.createCell(0).setCellValue(safe(item.getKeyword()));
|
||||
row.createCell(1).setCellValue(intOrZero(item.getFba()));
|
||||
row.createCell(2).setCellValue(intOrZero(item.getFbm()));
|
||||
row.createCell(3).setCellValue(intOrZero(item.getAmz()));
|
||||
row.createCell(4).setCellValue(intOrZero(item.getNoneCount()));
|
||||
Integer totalPage = item.getTotalPage();
|
||||
if (totalPage != null && totalPage > 0) {
|
||||
row.createCell(5).setCellValue(totalPage);
|
||||
} else {
|
||||
row.createCell(5).setCellValue("");
|
||||
}
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
// Fallback 分支:基于 rawItems 自聚合(Python 端未接入时使用)。
|
||||
Map<String, KeywordSummary> grouped = new LinkedHashMap<>();
|
||||
if (rawItems != null) {
|
||||
for (CollectDataResultRowVo item : rawItems) {
|
||||
@@ -131,6 +163,10 @@ public class CollectDataExcelAssemblyService {
|
||||
return value == null ? "" : value;
|
||||
}
|
||||
|
||||
private int intOrZero(Integer value) {
|
||||
return value == null ? 0 : value;
|
||||
}
|
||||
|
||||
private static class KeywordSummary {
|
||||
int fba;
|
||||
int fbm;
|
||||
|
||||
@@ -18,6 +18,7 @@ import com.nanri.aiimage.modules.collectdata.model.dto.CollectDataParseRequest;
|
||||
import com.nanri.aiimage.modules.collectdata.model.dto.CollectDataSourceFileDto;
|
||||
import com.nanri.aiimage.modules.collectdata.model.dto.CollectDataSubmitResultRequest;
|
||||
import com.nanri.aiimage.modules.collectdata.model.dto.CollectDataSubmitRowDto;
|
||||
import com.nanri.aiimage.modules.collectdata.model.dto.CollectDataSummaryRowDto;
|
||||
import com.nanri.aiimage.modules.collectdata.model.entity.CollectDataCountryPrefEntity;
|
||||
import com.nanri.aiimage.modules.collectdata.model.entity.CollectDataItemEntity;
|
||||
import com.nanri.aiimage.modules.collectdata.model.vo.CollectDataCountryPreferenceVo;
|
||||
@@ -454,6 +455,11 @@ public class CollectDataService {
|
||||
persistScope(taskId, scopeKey, scopeHash, chunkTotal, request);
|
||||
|
||||
stats.finalRowCount = countFinalRows(taskId);
|
||||
// Python 在 done=true 那次回传携带关键词级聚合统计;非空时按"最后一次为准"覆盖。
|
||||
List<CollectDataSummaryRowDto> incomingSummaries = request.getSummaries();
|
||||
if (incomingSummaries != null && !incomingSummaries.isEmpty()) {
|
||||
stats.summaries = new ArrayList<>(incomingSummaries);
|
||||
}
|
||||
persistStats(task, stats);
|
||||
|
||||
if (request.getError() != null && !request.getError().isBlank()) {
|
||||
@@ -812,6 +818,8 @@ public class CollectDataService {
|
||||
if (result == null || !MODULE_TYPE.equals(result.getModuleType())) {
|
||||
throw new BusinessException("结果记录不存在");
|
||||
}
|
||||
// 先加载 stats,使 Python 携带的 summaries 可优先用于「结果文件」sheet;rawRows 仅作为 fallback。
|
||||
CollectDataStats stats = loadStats(task);
|
||||
List<CollectDataResultRowVo> rows = loadFinalRows(task.getId());
|
||||
// Sheet「结果文件」按需求基于 Python 回传的全量数据聚合,不经后端 ASIN/品牌过滤丢弃,
|
||||
// 因此从 biz_task_chunk 反序列化全部原始行。
|
||||
@@ -820,7 +828,7 @@ public class CollectDataService {
|
||||
String filename = buildResultFilename(task, result);
|
||||
File xlsx = FileUtil.file(workRoot, filename);
|
||||
try {
|
||||
excelAssemblyService.writeWorkbook(xlsx, rows, rawRows);
|
||||
excelAssemblyService.writeWorkbook(xlsx, rows, stats.summaries, rawRows);
|
||||
String objectKey = ossStorageService.uploadResultFile(xlsx, MODULE_TYPE);
|
||||
result.setResultFilename(filename);
|
||||
result.setResultFileUrl(objectKey);
|
||||
@@ -831,7 +839,7 @@ public class CollectDataService {
|
||||
result.setErrorMessage(null);
|
||||
fileResultMapper.updateById(result);
|
||||
|
||||
CollectDataStats stats = loadStats(task);
|
||||
// 复用同一份 stats 更新 finalRowCount 后再持久化,避免重复 loadStats 丢失 summaries。
|
||||
stats.finalRowCount = rows.size();
|
||||
persistStats(task, stats);
|
||||
task.setStatus(STATUS_SUCCESS);
|
||||
@@ -998,6 +1006,16 @@ public class CollectDataService {
|
||||
stats.brandRejectedCount = root.path("brandRejectedCount").asInt(0);
|
||||
stats.brandQueryFailedCount = root.path("brandQueryFailedCount").asInt(0);
|
||||
stats.finalRowCount = root.path("finalRowCount").asInt(0);
|
||||
// 反序列化 Python 携带的关键词级聚合;解析失败保持空 list,仅 warn 不抛,避免影响其他统计
|
||||
JsonNode summariesNode = root.get("summaries");
|
||||
if (summariesNode != null && summariesNode.isArray()) {
|
||||
try {
|
||||
stats.summaries = objectMapper.convertValue(summariesNode,
|
||||
new TypeReference<List<CollectDataSummaryRowDto>>() {});
|
||||
} catch (Exception convertEx) {
|
||||
log.warn("[collect-data] parse summaries failed taskId={} err={}", task.getId(), convertEx.getMessage());
|
||||
}
|
||||
}
|
||||
} catch (Exception ex) {
|
||||
log.warn("[collect-data] parse result stats failed taskId={} err={}", task.getId(), ex.getMessage());
|
||||
}
|
||||
@@ -1017,6 +1035,8 @@ public class CollectDataService {
|
||||
payload.put("brandRejectedCount", stats.brandRejectedCount);
|
||||
payload.put("brandQueryFailedCount", stats.brandQueryFailedCount);
|
||||
payload.put("finalRowCount", stats.finalRowCount);
|
||||
// 持久化 Python 携带的关键词级聚合;后续 processResultFileJob 阶段读取用于落 Excel
|
||||
payload.put("summaries", stats.summaries == null ? List.of() : stats.summaries);
|
||||
task.setResultJson(objectMapper.writeValueAsString(payload));
|
||||
} catch (Exception ex) {
|
||||
throw new BusinessException("保存采集统计失败");
|
||||
@@ -1129,6 +1149,8 @@ public class CollectDataService {
|
||||
private int brandRejectedCount;
|
||||
private int brandQueryFailedCount;
|
||||
private int finalRowCount;
|
||||
// Python 在 done=true 时携带的关键词级聚合统计;空表示由 Java 端用 rawItems 自聚合兜底
|
||||
private List<CollectDataSummaryRowDto> summaries = new ArrayList<>();
|
||||
}
|
||||
|
||||
@Transactional
|
||||
|
||||
Reference in New Issue
Block a user