fix: 商标查询失败自动重试、货源结果图切回Excel内嵌防WPS #REF、外观专利标题模型改gemini-3.8-flash

- brand-check: 单品牌商标查询失败(异常/空响应/query_faild_data 非空)自动重试,
  次数与间隔可配(retry-times 默认3含首次 / retry-interval-millis 1000),命中侵权不重试;
  collectdata 品牌过滤与外观专利商标核验两侧同时受益
- similarasin: 货源结果Excel图片由WPS DISPIMG 切回 Excel 365 richData 单元格内嵌,
  修复 WPS 打开 DISPIMG 显示 #REF 的兼容回归;下载失败单元格回退为URL文本
- appearance-patent: 标题商标提取模型由 deepseek-v4-flash 切 gemini-3.8-flash
This commit is contained in:
2026-09-08 19:01:08 +08:00
parent ba581ed095
commit 197643a8fa
10 changed files with 191 additions and 53 deletions
@@ -14,7 +14,7 @@ public class AppearancePatentProperties {
/**
* 商标关键词提取模型
*/
private String titleModel = "deepseek-v4-flash";
private String titleModel = "gemini-3.8-flash";
/**
* 外观检测模型(视觉)
*/
@@ -10,6 +10,10 @@ public class BrandCheckProperties {
private String path = "/brand_check";
private String token = "";
private String defaultStrategy = "Terms";
/** 单品牌商标查询的重试次数(含首次),连续失败超过该次数才判定为查询失败。 */
private int retryTimes = 3;
/** 每次查询失败后到下一次重试前的等待毫秒数。 */
private int retryIntervalMillis = 1000;
private int connectTimeoutMillis = 10000;
private int readTimeoutMillis = 60000;
}
@@ -105,15 +105,49 @@ public class BrandCheckClient {
}
private BrandCheckOutcome checkOneBrand(String brand, String strategy) {
try {
BrandCheckResponse response = check(brand, strategy);
if (response == null) {
return new BrandCheckOutcome(List.of(), List.of(brand));
int attempts = Math.max(1, properties.getRetryTimes());
BrandCheckResponse response = null;
Exception lastFailure = null;
for (int attempt = 1; attempt <= attempts; attempt++) {
try {
response = check(brand, strategy);
} catch (Exception ex) {
lastFailure = ex;
response = null;
if (attempt < attempts) {
log.warn("[brand-check] 请求异常将重试 brand={} attempt={}/{} err={}",
brand, attempt, attempts, ex.getMessage());
sleepBeforeRetry();
}
continue;
}
return new BrandCheckOutcome(nullToEmpty(response.getFaildData()), nullToEmpty(response.getQueryFaildData()));
} catch (Exception ex) {
log.warn("[brand-check] request failed brand={} strategy={} err={}", brand, strategy, ex.getMessage());
return new BrandCheckOutcome(List.of(), List.of(brand));
if (response != null && nullToEmpty(response.getQueryFaildData()).isEmpty()) {
// 本次查询完成且无失败项:命中侵权(faild_data)属有效结论,不重试。
return new BrandCheckOutcome(nullToEmpty(response.getFaildData()), List.of());
}
if (attempt < attempts) {
log.warn("[brand-check] 服务端返回查询失败将重试 brand={} attempt={}/{}",
brand, attempt, attempts);
sleepBeforeRetry();
}
}
log.warn("[brand-check] 重试耗尽判定查询失败 brand={} attempts={} lastErr={}",
brand, attempts,
lastFailure == null ? "query_faild_data 持续非空" : lastFailure.getMessage());
return new BrandCheckOutcome(List.of(),
response == null ? List.of(brand) : nullToEmpty(response.getQueryFaildData()));
}
private void sleepBeforeRetry() {
long delayMillis = Math.max(0L, properties.getRetryIntervalMillis());
if (delayMillis <= 0L) {
return;
}
try {
Thread.sleep(delayMillis);
} catch (InterruptedException interruptedException) {
Thread.currentThread().interrupt();
throw new IllegalStateException("商标查询重试被中断", interruptedException);
}
}
@@ -41,7 +41,7 @@ import com.nanri.aiimage.modules.similarasin.service.support.SimilarAsinExcelPar
import com.nanri.aiimage.modules.similarasin.service.support.SimilarAsinGroupingConverter;
import com.nanri.aiimage.modules.similarasin.service.support.SimilarAsinHistoryAssembler;
import com.nanri.aiimage.modules.similarasin.util.BoundedImageCache;
import com.nanri.aiimage.modules.similarasin.util.WpsCellImageWriter;
import com.nanri.aiimage.modules.similarasin.util.ExcelCellImageWriter;
import com.nanri.aiimage.modules.similarasin.util.SimilarAsinImageEmbedder;
import com.nanri.aiimage.modules.similarasin.util.SimilarAsinLogSupport;
import com.nanri.aiimage.modules.file.service.LocalFileStorageService;
@@ -4553,8 +4553,8 @@ public class SimilarAsinTaskService {
Map<String, SimilarAsinResultRowDto> resultMap,
Map<String, SimilarAsinImageEmbedder.ResizedImage> taskImageCache) {
ensureResultAssemblyNotInterrupted();
// 单元格内嵌图片写完 workbook patch WPS DISPIMG 结构WPS/钉钉/飞书/Excel365 均可显示
WpsCellImageWriter.Session cellImageSession = WpsCellImageWriter.createSession();
// 单元格内嵌图片写完 workbook patch Excel 365 richData 单元格图片结构 Excel/WPS 打开均可见
ExcelCellImageWriter.Session cellImageSession = ExcelCellImageWriter.createSession();
SimilarAsinImageEmbedder.ImageSpool imageSpool;
try {
Path spoolDir = Files.createTempDirectory(xlsx.getParentFile().toPath(), "similar-asin-images-");
@@ -4670,7 +4670,7 @@ public class SimilarAsinTaskService {
if (!cellImageSession.isEmpty()) {
try {
ensureResultAssemblyNotInterrupted();
WpsCellImageWriter.patchXlsxFile(xlsx, cellImageSession);
ExcelCellImageWriter.patchXlsxFile(xlsx, cellImageSession);
} catch (IOException ex) {
String cause = ex.toString();
log.error("[similar-asin] excel cell image patch failed xlsx={} images={} err={}",
@@ -611,7 +611,7 @@ public class SimilarAsinImageEmbedder {
String url,
Row row,
Map<String, ResizedImage> taskImageCache,
WpsCellImageWriter.Session excelCellImageSession) {
ExcelCellImageWriter.Session excelCellImageSession) {
return embedAsExcelCellImage(rowIdx, colIdx, url, row, taskImageCache, excelCellImageSession, null);
}
@@ -620,39 +620,40 @@ public class SimilarAsinImageEmbedder {
String url,
Row row,
Map<String, ResizedImage> taskImageCache,
WpsCellImageWriter.Session excelCellImageSession,
ExcelCellImageWriter.Session excelCellImageSession,
ImageSpool imageSpool) {
if (url == null || url.isBlank() || excelCellImageSession == null) {
return null;
}
String trimmedUrl = url.trim();
SpoolImage spooled = imageSpool == null ? null : imageSpool.get(trimmedUrl);
String imageId = null;
ImageDim dim = null;
if (spooled != null) {
// 预取已落盘:按路径注册,patch 阶段再读文件,避免整份图片字节驻留堆内存。
imageId = excelCellImageSession.registerImageByPath(spooled.path());
dim = new ImageDim(spooled.width(), spooled.height());
} else if (imageSpool != null) {
// 预取未命中说明该 URL 下载失败:不重试、不把 URL 文本写进单元格,留空并打日志。
log.warn("[similar-asin][image] cell-image-miss url={} colIdx={} (prefetch did not spool), leave blank",
trimmedUrl, colIdx);
row.createCell(colIdx).setCellValue("#VALUE!");
excelCellImageSession.registerImage(rowIdx, colIdx, spooled.path());
return new ImageDim(spooled.width(), spooled.height());
}
if (imageSpool != null) {
// 预取未命中说明该 URL 下载失败:不重试,单元格回退为 URL 文本便于人工核对。
row.createCell(colIdx).setCellValue(trimmedUrl);
return null;
} else {
ResizedImage thumb = downloadAndResize(trimmedUrl, taskImageCache);
if (thumb == null) {
log.warn("[similar-asin][image] cell-image-miss url={} colIdx={} (download failed), leave blank",
trimmedUrl, colIdx);
return null;
}
imageId = excelCellImageSession.registerImage(thumb.bytes());
}
ResizedImage thumb = downloadAndResize(trimmedUrl, taskImageCache);
if (thumb == null) {
return null;
}
try {
row.createCell(colIdx).setCellValue("#VALUE!");
excelCellImageSession.registerImage(rowIdx, colIdx, thumb.bytes());
taskImageCache.remove(trimmedUrl);
dim = new ImageDim(thumb.width(), thumb.height());
return new ImageDim(thumb.width(), thumb.height());
} catch (RuntimeException ex) {
taskImageCache.remove(trimmedUrl);
log.warn("[similar-asin][image] excel-cell-image-fail url={} colIdx={} err={}",
trimmedUrl, colIdx, ex.getMessage());
row.createCell(colIdx).setCellValue(trimmedUrl);
return null;
}
if (imageId != null) {
row.createCell(colIdx).setCellFormula(WpsCellImageWriter.dispImgFormula(imageId));
}
return dim;
}
private boolean hasLocalCachedThumb(String url) {
@@ -62,7 +62,7 @@ AIIMAGE_MODULE_CLEANUP_CRON=0 0 0 * * *
AIIMAGE_MODULE_CLEANUP_MODULE_TYPES=DEDUPE,SPLIT,CONVERT,DELETE_BRAND
AIIMAGE_APPEARANCE_PATENT_LLM_HOST=https://ai.t8star.org
AIIMAGE_APPEARANCE_PATENT_TITLE_MODEL=deepseek-v4-flash
AIIMAGE_APPEARANCE_PATENT_TITLE_MODEL=gemini-3.8-flash
AIIMAGE_APPEARANCE_PATENT_APPEARANCE_MODEL=gemini-3.8-flash
AIIMAGE_APPEARANCE_PATENT_LLM_MAX_TOKENS=64000
AIIMAGE_APPEARANCE_PATENT_LLM_BATCH_SIZE=10
@@ -237,11 +237,13 @@ aiimage:
path: ${AIIMAGE_BRAND_CHECK_PATH:/brand_check}
token: ${AIIMAGE_BRAND_CHECK_TOKEN:}
default-strategy: ${AIIMAGE_BRAND_CHECK_DEFAULT_STRATEGY:Terms}
retry-times: ${AIIMAGE_BRAND_CHECK_RETRY_TIMES:3}
retry-interval-millis: ${AIIMAGE_BRAND_CHECK_RETRY_INTERVAL_MILLIS:1000}
connect-timeout-millis: ${AIIMAGE_BRAND_CHECK_CONNECT_TIMEOUT_MILLIS:10000}
read-timeout-millis: ${AIIMAGE_BRAND_CHECK_READ_TIMEOUT_MILLIS:60000}
appearance-patent:
llm-host: ${AIIMAGE_APPEARANCE_PATENT_LLM_HOST:https://ai.t8star.org}
title-model: ${AIIMAGE_APPEARANCE_PATENT_TITLE_MODEL:deepseek-v4-flash}
title-model: ${AIIMAGE_APPEARANCE_PATENT_TITLE_MODEL:gemini-3.8-flash}
appearance-model: ${AIIMAGE_APPEARANCE_PATENT_APPEARANCE_MODEL:gemini-3.8-flash}
llm-max-tokens: ${AIIMAGE_APPEARANCE_PATENT_LLM_MAX_TOKENS:64000}
llm-connect-timeout-millis: ${AIIMAGE_APPEARANCE_PATENT_LLM_CONNECT_TIMEOUT_MILLIS:10000}