From a1376b51b01d2b9ff94665e0aae2ca71696e52b3 Mon Sep 17 00:00:00 2001 From: super <2903208875@qq.com> Date: Sun, 24 May 2026 02:31:58 +0800 Subject: [PATCH] =?UTF-8?q?=E4=BC=98=E5=8C=96=E5=9B=BE=E7=89=87=E5=B0=BA?= =?UTF-8?q?=E5=AF=B8?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../client/AppearancePatentCozeClient.java | 17 ++-- .../dto/AppearancePatentResultRowDto.java | 4 + .../service/AppearancePatentTaskService.java | 24 +++-- .../client/SimilarAsinCozeClient.java | 4 +- .../service/SimilarAsinTaskService.java | 16 ++-- .../util/SimilarAsinImageEmbedder.java | 88 +++++++++++++------ .../util/SimilarAsinImageEmbedderTest.java | 19 ++-- 7 files changed, 114 insertions(+), 58 deletions(-) diff --git a/backend-java/src/main/java/com/nanri/aiimage/modules/appearancepatent/client/AppearancePatentCozeClient.java b/backend-java/src/main/java/com/nanri/aiimage/modules/appearancepatent/client/AppearancePatentCozeClient.java index 60582b4..9a1a7b9 100644 --- a/backend-java/src/main/java/com/nanri/aiimage/modules/appearancepatent/client/AppearancePatentCozeClient.java +++ b/backend-java/src/main/java/com/nanri/aiimage/modules/appearancepatent/client/AppearancePatentCozeClient.java @@ -465,7 +465,9 @@ public class AppearancePatentCozeClient { text(firstNonNull(node.get("patent_reason"), firstNonNull(firstNonNull(node.get("patentReason"), node.get("patent reason")), firstNonNull(itemNode.get("patent_reason"), - firstNonNull(itemNode.get("patentReason"), itemNode.get("patent reason")))))) + firstNonNull(itemNode.get("patentReason"), itemNode.get("patent reason")))))), + text(firstNonNull(firstNonNull(node.get("score"), node.get("Score")), + firstNonNull(itemNode.get("score"), itemNode.get("Score")))) )); } } @@ -570,6 +572,7 @@ public class AppearancePatentCozeClient { row.setTitleReason(result.titleReason()); row.setAppearanceReason(result.appearanceReason()); row.setPatentReason(result.patentReason()); + row.setScore(result.score()); } private RestClient restClient() { @@ -600,13 +603,14 @@ public class AppearancePatentCozeClient { row.setTitleReason(source.getTitleReason()); row.setAppearanceReason(source.getAppearanceReason()); row.setPatentReason(source.getPatentReason()); + row.setScore(source.getScore()); return row; } private AppearancePatentResultRowDto markFailed(AppearancePatentResultRowDto row, String failureMessage) { String reviewMessage = failureMessage == null || failureMessage.isBlank() - ? "Coze 检测失败,待人工复核" - : "Coze 检测失败,待人工复核:" + failureMessage; + ? "Coze 检测失败" + : "Coze 检测失败:" + failureMessage; if (row.getError() == null || row.getError().isBlank()) { row.setError(failureMessage); } @@ -797,7 +801,9 @@ public class AppearancePatentCozeClient { || item.has("appearanceReason") || item.has("patent_reason") || item.has("patentReason") - || item.has("patent reason")); + || item.has("patent reason") + || item.has("score") + || item.has("Score")); } private boolean isSuccessfulWorkflowStatus(String status) { @@ -1054,7 +1060,8 @@ public class AppearancePatentCozeClient { String status, String titleReason, String appearanceReason, - String patentReason + String patentReason, + String score ) { } diff --git a/backend-java/src/main/java/com/nanri/aiimage/modules/appearancepatent/model/dto/AppearancePatentResultRowDto.java b/backend-java/src/main/java/com/nanri/aiimage/modules/appearancepatent/model/dto/AppearancePatentResultRowDto.java index 7198252..76effac 100644 --- a/backend-java/src/main/java/com/nanri/aiimage/modules/appearancepatent/model/dto/AppearancePatentResultRowDto.java +++ b/backend-java/src/main/java/com/nanri/aiimage/modules/appearancepatent/model/dto/AppearancePatentResultRowDto.java @@ -78,6 +78,10 @@ public class AppearancePatentResultRowDto { @Schema(description = "Coze patent reason", accessMode = Schema.AccessMode.READ_ONLY) private String patentReason; + @JsonAlias({"score", "Score", "评分"}) + @Schema(description = "Coze 回流的评分(外观维度),透传到最终 xlsx 第一张 sheet 的“评分”列。", accessMode = Schema.AccessMode.READ_ONLY) + private String score; + /** * 仅内存生命周期标记,标识当前 status 是 markFailed 时合成出来的(而不是用户/Python 真实回传)。 * 不入库、不参与 chunk 序列化(@JsonIgnore),用于导出 / 重新上传判定时区分"系统合成 FAILED"与"用户真正失败", diff --git a/backend-java/src/main/java/com/nanri/aiimage/modules/appearancepatent/service/AppearancePatentTaskService.java b/backend-java/src/main/java/com/nanri/aiimage/modules/appearancepatent/service/AppearancePatentTaskService.java index 2dbcce1..0bfb7a2 100644 --- a/backend-java/src/main/java/com/nanri/aiimage/modules/appearancepatent/service/AppearancePatentTaskService.java +++ b/backend-java/src/main/java/com/nanri/aiimage/modules/appearancepatent/service/AppearancePatentTaskService.java @@ -128,7 +128,8 @@ public class AppearancePatentTaskService { "外观维度(外观设计专利)", "专利维度(发明/实用新型专利)", "结论", - "状态" + "状态", + "评分" ); private final LocalFileStorageService localFileStorageService; @@ -1265,6 +1266,7 @@ public class AppearancePatentTaskService { row.setTitleReason(representative.getTitleReason()); row.setAppearanceReason(representative.getAppearanceReason()); row.setPatentReason(representative.getPatentReason()); + row.setScore(representative.getScore()); row.setDone(representative.getDone()); return row; } @@ -3240,7 +3242,8 @@ public class AppearancePatentTaskService { row.createCell(col++).setCellValue(resultRow == null ? missingReason : userFacingCozeCellValue(resultRow, resultRow.getAppearanceRisk())); row.createCell(col++).setCellValue(resultRow == null ? missingReason : userFacingCozeCellValue(resultRow, resultRow.getPatentRisk())); row.createCell(col++).setCellValue(resultRow == null ? "未送检" : userFacingConclusion(resultRow)); - row.createCell(col).setCellValue(resultRow == null ? "" : userFacingStatus(resultRow)); + row.createCell(col++).setCellValue(resultRow == null ? "" : userFacingStatus(resultRow)); + row.createCell(col).setCellValue(resultRow == null ? "" : firstNonBlank(resultRow.getScore(), "")); } writeReasonSheet(workbook, headerStyle, rowsToWrite, resultMap); workbook.write(fos); @@ -4041,11 +4044,12 @@ public class AppearancePatentTaskService { if (!normalizedValue.isBlank() && !isTechnicalCozeFailure(normalizedValue)) { return value; } + // coze 技术性失败:有错误信息则放入错误信息,没有则留空 if (row != null && isTechnicalCozeFailure(row.getError())) { - return "待人工复核"; + return firstNonBlank(row.getError(), ""); } if (row != null && isFailedCozeStatusValue(row.getStatus())) { - return "待人工复核"; + return firstNonBlank(row.getError(), ""); } return firstNonBlank(value, ""); } @@ -4058,11 +4062,12 @@ public class AppearancePatentTaskService { if (!conclusion.isBlank() && !isTechnicalCozeFailure(conclusion)) { return row.getConclusion(); } + // coze 技术性失败:有错误信息则放入错误信息,没有则留空 if (isTechnicalCozeFailure(row.getError())) { - return "待人工复核"; + return firstNonBlank(row.getError(), ""); } if (isFailedCozeStatusValue(row.getStatus())) { - return "待人工复核"; + return firstNonBlank(row.getError(), ""); } return firstNonBlank(row.getConclusion(), ""); } @@ -4070,8 +4075,8 @@ public class AppearancePatentTaskService { /** * 导出最终 xlsx 时的状态展示: * - 如果 status 是 markFailed 合成的(failureSyntheticStatus=true), - * 且 row 实际并没有任何风险维度结果,导出层降级为"待人工复核",避免用户原样 - * 重新上传时该行被反复识别为失败行重新触发 Coze。 + * 且 row 实际并没有任何风险维度结果,导出层展示错误信息(有则放入,无则留空), + * 避免用户原样重新上传时该行被反复识别为失败行重新触发 Coze。 * - 真正业务侧失败 / Python 真实回传 FAILED 仍按原值显示。 */ private String userFacingStatus(AppearancePatentResultRowDto row) { @@ -4082,7 +4087,8 @@ public class AppearancePatentTaskService { if (!row.isFailureSyntheticStatus()) { return original; } - return "待人工复核"; + // 合成状态:有错误信息则放入错误信息,没有则留空 + return firstNonBlank(row.getError(), ""); } private boolean isTechnicalCozeFailure(String value) { diff --git a/backend-java/src/main/java/com/nanri/aiimage/modules/similarasin/client/SimilarAsinCozeClient.java b/backend-java/src/main/java/com/nanri/aiimage/modules/similarasin/client/SimilarAsinCozeClient.java index 38b6a8b..8a024c0 100644 --- a/backend-java/src/main/java/com/nanri/aiimage/modules/similarasin/client/SimilarAsinCozeClient.java +++ b/backend-java/src/main/java/com/nanri/aiimage/modules/similarasin/client/SimilarAsinCozeClient.java @@ -766,8 +766,8 @@ public class SimilarAsinCozeClient { private SimilarAsinResultRowDto markFailed(SimilarAsinResultRowDto row, String failureMessage) { String reviewMessage = failureMessage == null || failureMessage.isBlank() - ? "Coze 检测失败,待人工复核" - : "Coze 检测失败,待人工复核:" + failureMessage; + ? "Coze 检测失败" + : "Coze 检测失败:" + failureMessage; if (row.getError() == null || row.getError().isBlank()) { row.setError(failureMessage); } diff --git a/backend-java/src/main/java/com/nanri/aiimage/modules/similarasin/service/SimilarAsinTaskService.java b/backend-java/src/main/java/com/nanri/aiimage/modules/similarasin/service/SimilarAsinTaskService.java index 59fa4cd..5203003 100644 --- a/backend-java/src/main/java/com/nanri/aiimage/modules/similarasin/service/SimilarAsinTaskService.java +++ b/backend-java/src/main/java/com/nanri/aiimage/modules/similarasin/service/SimilarAsinTaskService.java @@ -3341,11 +3341,12 @@ public class SimilarAsinTaskService { font.setBold(true); headerStyle.setFont(font); + Drawing patriarch = sheet.createDrawingPatriarch(); + Map taskImageCache = new ConcurrentHashMap<>(); + sheet.setColumnWidth(IMG_COL_MAIN, SimilarAsinImageEmbedder.IMAGE_COL_WIDTH_CHARS * 256); sheet.setColumnWidth(IMG_COL_PUZZLE1, SimilarAsinImageEmbedder.IMAGE_COL_WIDTH_CHARS * 256); sheet.setColumnWidth(IMG_COL_PUZZLE2, SimilarAsinImageEmbedder.IMAGE_COL_WIDTH_CHARS * 256); - Drawing patriarch = sheet.createDrawingPatriarch(); - Map taskImageCache = new ConcurrentHashMap<>(); List resultHeaders = new ArrayList<>(RESULT_HEADERS); Row header = sheet.createRow(0); @@ -3359,7 +3360,6 @@ public class SimilarAsinTaskService { for (SimilarAsinParsedRowVo parsedRow : rowsToWrite == null ? List.of() : rowsToWrite) { SimilarAsinResultRowDto resultRow = findResultRow(parsedRow, resultMap); Row row = sheet.createRow(rowIndex); - row.setHeightInPoints(SimilarAsinImageEmbedder.IMAGE_ROW_HEIGHT_POINTS); int col = 0; row.createCell(col++).setCellValue(firstNonBlank(parsedRow.getDisplayId(), parsedRow.getSourceId())); row.createCell(col++).setCellValue(firstNonBlank(parsedRow.getAsin(), "")); @@ -3371,6 +3371,10 @@ public class SimilarAsinTaskService { row.createCell(col++).setCellValue(resultRow == null ? "" : userFacingCozeCellValue(resultRow, resultRow.getCategory())); row.createCell(col).setCellValue(resultRow == null ? "" : firstNonBlank(resultRow.getStatus(), "")); if (resultRow != null) { + // 行高在 embed 前先固定到 IMAGE_ROW_HEIGHT_POINTS(Excel 上限 409pt): + // embed() 用 IMAGE_COL_WIDTH_CHARS / IMAGE_ROW_HEIGHT_POINTS 常量推导 EMU anchor,与运行时单元格尺寸解耦, + // 顺序前置只是为了让 sheet 实际行高与 anchor 假设一致,避免后续维护误读。 + row.setHeightInPoints(SimilarAsinImageEmbedder.IMAGE_ROW_HEIGHT_POINTS); imageEmbedder.embed(sheet, patriarch, workbook, rowIndex, IMG_COL_MAIN, resultRow.getMainUrl(), row, taskImageCache); imageEmbedder.embed(sheet, patriarch, workbook, rowIndex, IMG_COL_PUZZLE1, resultRow.getPuzzleImg1(), row, taskImageCache); imageEmbedder.embed(sheet, patriarch, workbook, rowIndex, IMG_COL_PUZZLE2, resultRow.getPuzzleImg2(), row, taskImageCache); @@ -4135,8 +4139,9 @@ public class SimilarAsinTaskService { if (!normalizedValue.isBlank() && !isTechnicalCozeFailure(normalizedValue)) { return value; } + // coze 技术性失败:有错误信息则放入错误信息,没有则留空 if (row != null && isTechnicalCozeFailure(row.getError())) { - return "待人工复核"; + return firstNonBlank(row.getError(), ""); } return firstNonBlank(value, ""); } @@ -4167,8 +4172,9 @@ public class SimilarAsinTaskService { if (!conclusion.isBlank() && !isTechnicalCozeFailure(conclusion)) { return row.getConclusion(); } + // coze 技术性失败:有错误信息则放入错误信息,没有则留空 if (isTechnicalCozeFailure(row.getError())) { - return "待人工复核"; + return firstNonBlank(row.getError(), ""); } return firstNonBlank(row.getConclusion(), ""); } diff --git a/backend-java/src/main/java/com/nanri/aiimage/modules/similarasin/util/SimilarAsinImageEmbedder.java b/backend-java/src/main/java/com/nanri/aiimage/modules/similarasin/util/SimilarAsinImageEmbedder.java index 2465101..97018ef 100644 --- a/backend-java/src/main/java/com/nanri/aiimage/modules/similarasin/util/SimilarAsinImageEmbedder.java +++ b/backend-java/src/main/java/com/nanri/aiimage/modules/similarasin/util/SimilarAsinImageEmbedder.java @@ -12,6 +12,7 @@ import org.apache.poi.ss.usermodel.Drawing; import org.apache.poi.ss.usermodel.Row; import org.apache.poi.ss.usermodel.Sheet; import org.apache.poi.ss.usermodel.Workbook; +import org.apache.poi.util.Units; import org.apache.poi.xssf.streaming.SXSSFWorkbook; import org.springframework.stereotype.Component; @@ -58,10 +59,9 @@ public class SimilarAsinImageEmbedder { static final int DOWNLOAD_TIMEOUT_SECONDS = 5; static final int DOWNLOAD_MAX_RETRY = 1; static final int DOWNLOAD_POOL_SIZE = 8; - // B 方案:单元格放大到 Excel 上限附近(255 char ≈ 1785 px / 409.5 pt ≈ 546 px),保持 16:9 显示空间。 + // 单元格固定尺寸,图片在其中等比缩放(不拉伸);resize 仍按长边 1280 px 控制堆体积。 public static final float IMAGE_ROW_HEIGHT_POINTS = 409f; public static final int IMAGE_COL_WIDTH_CHARS = 80; - // 源图长边 1280 px 给 Excel 缩放/打印留余量;q=0.75 + 300KB 出口硬上限主动控制单图体积。 static final int TARGET_LONG_EDGE_PX = 1280; static final float JPEG_QUALITY = 0.75f; static final int MAX_THUMB_SIZE_BYTES = 300 * 1024; @@ -92,21 +92,21 @@ public class SimilarAsinImageEmbedder { /** * 嵌入单元格:下载 → resize → POI addPicture → createPicture,全链路任一失败即 URL 文本兜底。 - * taskImageCache 由调用方在 writeResultWorkbook 入口创建(任务级 ConcurrentHashMap),方法返回随栈帧释放。 + * 返回值:成功时为缩略图实际像素(width/height),失败兜底为 null;调用方据此自适应行高/列宽。 */ - public void embed(Sheet sheet, - Drawing patriarch, - SXSSFWorkbook wb, - int rowIdx, - int colIdx, - String url, - Row row, - Map taskImageCache) { + public ImageDim embed(Sheet sheet, + Drawing patriarch, + SXSSFWorkbook wb, + int rowIdx, + int colIdx, + String url, + Row row, + Map taskImageCache) { if (url == null || url.isBlank()) { - return; + return null; } String trimmedUrl = url.trim(); - byte[] thumb; + ResizedImage thumb; try { thumb = taskImageCache.get(trimmedUrl); boolean cacheHit = thumb != null; @@ -114,50 +114,74 @@ public class SimilarAsinImageEmbedder { byte[] raw = downloadWithRetry(trimmedUrl); thumb = resizeImage(trimmedUrl, raw); taskImageCache.put(trimmedUrl, thumb); - log.info("[similar-asin][image] cache-miss url={} bytes={}", trimmedUrl, thumb.length); + log.info("[similar-asin][image] cache-miss url={} bytes={} dims={}x{}", + trimmedUrl, thumb.bytes().length, thumb.width(), thumb.height()); } else { - log.debug("[similar-asin][image] cache-hit url={} bytes={}", trimmedUrl, thumb.length); + log.debug("[similar-asin][image] cache-hit url={} bytes={} dims={}x{}", + trimmedUrl, thumb.bytes().length, thumb.width(), thumb.height()); } } catch (UnsupportedUrlException ex) { log.warn("[similar-asin][image] unsupported-url url={} reason={}", trimmedUrl, ex.getMessage()); row.createCell(colIdx).setCellValue(trimmedUrl); - return; + return null; } catch (DownloadOversizeException ex) { log.warn("[similar-asin][image] download-oversize url={} bytes={}", trimmedUrl, ex.size()); row.createCell(colIdx).setCellValue(trimmedUrl); - return; + return null; } catch (ResizeOversizeException ex) { log.warn("[similar-asin][image] resize-oversize url={} bytes={}", ex.url(), ex.size()); row.createCell(colIdx).setCellValue(trimmedUrl); - return; + return null; } catch (ResizeException ex) { log.warn("[similar-asin][image] resize-fail url={} err={}", trimmedUrl, ex.getMessage()); row.createCell(colIdx).setCellValue(trimmedUrl); - return; + return null; } catch (IOException | TimeoutException ex) { log.warn("[similar-asin][image] download-fail url={} err={}", trimmedUrl, ex.getMessage()); row.createCell(colIdx).setCellValue(trimmedUrl); - return; + return null; } catch (RuntimeException ex) { log.warn("[similar-asin][image] embed-fail url={} err={}", trimmedUrl, ex.getMessage()); row.createCell(colIdx).setCellValue(trimmedUrl); - return; + return null; } try { - int picIdx = wb.addPicture(thumb, Workbook.PICTURE_TYPE_JPEG); + int picIdx = wb.addPicture(thumb.bytes(), Workbook.PICTURE_TYPE_JPEG); ClientAnchor anchor = wb.getCreationHelper().createClientAnchor(); + anchor.setAnchorType(ClientAnchor.AnchorType.MOVE_DONT_RESIZE); anchor.setCol1(colIdx); anchor.setRow1(rowIdx); - anchor.setCol2(colIdx + 1); - anchor.setRow2(rowIdx + 1); - anchor.setAnchorType(ClientAnchor.AnchorType.MOVE_AND_RESIZE); + anchor.setCol2(colIdx); + anchor.setRow2(rowIdx); + // 显式 EMU anchor:col1==col2 / row1==row2,仅靠 dx/dy 居中等比缩放。 + // picture.resize() 会按"调用时刻"的实际行高/列宽推算 col2/row2, + // 但 service 端可能在 embed 之后才 setHeightInPoints(409),导致 anchor 跨多行被纵向拉伸。 + // 这里彻底脱离运行时单元格尺寸依赖,单元格大小通过 IMAGE_COL_WIDTH_CHARS / IMAGE_ROW_HEIGHT_POINTS 常量推导。 + double cellWidthEmu = IMAGE_COL_WIDTH_CHARS * 7.0 * Units.EMU_PER_PIXEL; + double cellHeightEmu = IMAGE_ROW_HEIGHT_POINTS * (double) Units.EMU_PER_POINT; + double picWidthEmu = thumb.width() * (double) Units.EMU_PER_PIXEL; + double picHeightEmu = thumb.height() * (double) Units.EMU_PER_PIXEL; + double scale = Math.min(cellWidthEmu / picWidthEmu, cellHeightEmu / picHeightEmu) * 0.98; + if (scale > 1.0) { + scale = 1.0; + } + double displayedW = picWidthEmu * scale; + double displayedH = picHeightEmu * scale; + int dx1 = (int) Math.round((cellWidthEmu - displayedW) / 2.0); + int dy1 = (int) Math.round((cellHeightEmu - displayedH) / 2.0); + anchor.setDx1(dx1); + anchor.setDy1(dy1); + anchor.setDx2((int) Math.round(dx1 + displayedW)); + anchor.setDy2((int) Math.round(dy1 + displayedH)); patriarch.createPicture(anchor, picIdx); + ImageDim dim = new ImageDim(thumb.width(), thumb.height()); // POI 已经把缩略图字节复制进 picture pool(B 副本),此时移除 taskImageCache 里的 A 副本可立刻释放。 - // 同 URL 行内重复出现概率较低,且 cache 仍承担同任务跨行去重收益(首次 miss 时已经 put 过)。 taskImageCache.remove(trimmedUrl); + return dim; } catch (RuntimeException ex) { log.warn("[similar-asin][image] embed-fail url={} err={}", trimmedUrl, ex.getMessage()); row.createCell(colIdx).setCellValue(trimmedUrl); + return null; } } @@ -272,10 +296,10 @@ public class SimilarAsinImageEmbedder { } /** - * 长边 300px 等比缩放 + JPEG q=0.85 输出;ImageWriter / ImageOutputStream 双层 try-finally 释放 native 资源。 + * 等比缩放到长边 TARGET_LONG_EDGE_PX,JPEG q=0.75 输出;返回字节 + 实际像素,供调用方按图自适应单元格尺寸。 * 出口校验字节数 ≤ MAX_THUMB_SIZE_BYTES,超限抛 ResizeOversizeException 触发文本兜底。 */ - byte[] resizeImage(String sourceUrl, byte[] raw) throws IOException { + ResizedImage resizeImage(String sourceUrl, byte[] raw) throws IOException { guardImageDimensions(sourceUrl, raw); BufferedImage src = ImageIO.read(new ByteArrayInputStream(raw)); if (src == null) { @@ -318,9 +342,15 @@ public class SimilarAsinImageEmbedder { if (baos.size() > MAX_THUMB_SIZE_BYTES) { throw new ResizeOversizeException(sourceUrl, baos.size()); } - return baos.toByteArray(); + return new ResizedImage(baos.toByteArray(), dstW, dstH); } + /** 缩略图字节 + 实际像素,A 副本随 taskImageCache 生命周期,B 副本进 picture pool。 */ + public record ResizedImage(byte[] bytes, int width, int height) { } + + /** embed() 成功时返回的缩略图实际像素,调用方据此自适应行高/列宽。 */ + public record ImageDim(int width, int height) { } + /** 在解码整张位图前用 ImageReader 仅读取头部尺寸,避免“图像炸弹”导致 heap OOM。 */ private static void guardImageDimensions(String sourceUrl, byte[] raw) throws IOException { try (ImageInputStream iis = ImageIO.createImageInputStream(new ByteArrayInputStream(raw))) { diff --git a/backend-java/src/test/java/com/nanri/aiimage/modules/similarasin/util/SimilarAsinImageEmbedderTest.java b/backend-java/src/test/java/com/nanri/aiimage/modules/similarasin/util/SimilarAsinImageEmbedderTest.java index 86b4b0f..53484d8 100644 --- a/backend-java/src/test/java/com/nanri/aiimage/modules/similarasin/util/SimilarAsinImageEmbedderTest.java +++ b/backend-java/src/test/java/com/nanri/aiimage/modules/similarasin/util/SimilarAsinImageEmbedderTest.java @@ -35,18 +35,21 @@ class SimilarAsinImageEmbedderTest { ByteArrayOutputStream baos = new ByteArrayOutputStream(); ImageIO.write(src, "jpg", baos); - byte[] thumb = embedder.resizeImage("https://example.com/big.jpg", baos.toByteArray()); + SimilarAsinImageEmbedder.ResizedImage thumb = embedder.resizeImage("https://example.com/big.jpg", baos.toByteArray()); assertNotNull(thumb); - assertTrue(thumb.length > 0, "thumb 不可为空"); - assertTrue(thumb.length <= SimilarAsinImageEmbedder.MAX_THUMB_SIZE_BYTES, - "thumb 字节应 <= 40KB 上限,实际=" + thumb.length); + assertNotNull(thumb.bytes()); + assertTrue(thumb.bytes().length > 0, "thumb 不可为空"); + assertTrue(thumb.bytes().length <= SimilarAsinImageEmbedder.MAX_THUMB_SIZE_BYTES, + "thumb 字节应 <= 300KB 上限,实际=" + thumb.bytes().length); - BufferedImage decoded = ImageIO.read(new java.io.ByteArrayInputStream(thumb)); + // 验证返回的像素尺寸与实际解码一致 + assertEquals(SimilarAsinImageEmbedder.TARGET_LONG_EDGE_PX, Math.max(thumb.width(), thumb.height()), + "长边应等于 TARGET_LONG_EDGE_PX=" + SimilarAsinImageEmbedder.TARGET_LONG_EDGE_PX); + BufferedImage decoded = ImageIO.read(new java.io.ByteArrayInputStream(thumb.bytes())); assertNotNull(decoded, "thumb 应可被 ImageIO 重新解码"); - int longEdge = Math.max(decoded.getWidth(), decoded.getHeight()); - assertEquals(SimilarAsinImageEmbedder.TARGET_LONG_EDGE_PX, longEdge, - "长边应等于 TARGET_LONG_EDGE_PX=300"); + assertEquals(thumb.width(), decoded.getWidth(), "返回宽度应与解码宽度一致"); + assertEquals(thumb.height(), decoded.getHeight(), "返回高度应与解码高度一致"); } @Test