修复菜单消失

This commit is contained in:
super
2026-06-10 17:52:58 +08:00
parent c3d43aa26a
commit c9efdf8b0e
6 changed files with 193 additions and 26 deletions

View File

@@ -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<>();
}

View File

@@ -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;
}

View File

@@ -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;

View File

@@ -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 可优先用于「结果文件」sheetrawRows 仅作为 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