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
@@ -63,22 +63,23 @@ class AppearancePatentLlmClientHttpTest {
String model = String.valueOf(request.get("model"));
callCounts.computeIfAbsent(model, ignored -> new AtomicInteger()).incrementAndGet();
// 标题提取与外观检测现同用 gemini-3.8-flash,按请求是否带 image_url 区分两路:
// 外观检测消息带 image_url parts,标题商标提取仅纯文本。
String content;
if (model.contains("deepseek")) {
// 商标提取:按标题内容返回品牌词或"无"
String messages = String.valueOf(request.get("messages"));
if (messages.contains("Apple")) {
content = "AppleApple";
} else {
content = "";
}
} else {
if (body.contains("\"type\":\"image_url\"")) {
// 外观检测:返回 JSON(带 ```json 包裹与换行,模拟脏输出)
if (messagesBodyContains(exchange, "原创个性杯")) {
if (body.contains("原创个性杯")) {
content = "```json\n{\"appearance_status\": \"侵权\", \"appearance_reason\": \"【视觉拆解】:特殊造型\\n【判定依据】:高度相似知名设计\"}\n```";
} else {
content = "{\"appearance_status\": \"无侵权\", \"appearance_reason\": \"【视觉拆解】:普通直筒杯。\\n【对比评估】:行业通用基础形状。\\n【判定依据】:无侵权。\"}";
}
} else {
// 商标提取:按标题内容返回品牌词或"无"
if (String.valueOf(request.get("messages")).contains("Apple")) {
content = "AppleApple";
} else {
content = "";
}
}
String response = objectMapper.writeValueAsString(Map.of(
"model", model,
@@ -125,8 +126,8 @@ class AppearancePatentLlmClientHttpTest {
assertThat(result.getAppearanceRisk()).isEqualTo("无侵权");
assertThat(result.getAppearanceReason()).contains("视觉拆解");
assertThat(result.getTitleReason()).contains("Apple");
assertThat(callCounts.get("deepseek-v4-flash")).hasValue(1);
assertThat(callCounts.get("gemini-3.8-flash")).hasValue(1);
assertThat(callCounts.get("gemini-3.8-flash")).hasValue(2);
assertThat(callCounts.get("deepseek-v4-flash")).isNull();
}
@Test
@@ -182,7 +183,7 @@ class AppearancePatentLlmClientHttpTest {
client.inspectRows(List.of(row("6", "B006", "普通数据线", "", "https://img.example.com/6.jpg")), null, "test-key");
String appearanceBody = capturedBodies.stream()
.filter(b -> b.contains("gemini-3.8-flash"))
.filter(b -> b.contains("\"type\":\"image_url\""))
.findFirst()
.orElseThrow();
assertThat(appearanceBody).contains("https://img.example.com/6.jpg");
@@ -1,9 +1,15 @@
package com.nanri.aiimage.modules.brand.client;
import com.nanri.aiimage.config.BrandCheckProperties;
import com.sun.net.httpserver.HttpServer;
import org.junit.jupiter.api.Test;
import java.io.IOException;
import java.io.OutputStream;
import java.net.InetSocketAddress;
import java.nio.charset.StandardCharsets;
import java.util.List;
import java.util.concurrent.atomic.AtomicInteger;
import static org.assertj.core.api.Assertions.assertThat;
@@ -26,4 +32,93 @@ class BrandCheckClientTest {
assertThat(brands).containsExactly("任天堂", "Sony");
}
@Test
void queryFailureRetriesUntilSuccess() throws Exception {
AtomicInteger attempts = new AtomicInteger();
HttpServer server = startBrandCheckServer(exchange -> {
int count = attempts.incrementAndGet();
String body = count < 3
? "{\"faild_data\":[],\"query_faild_data\":[\"Apple\"]}"
: "{\"faild_data\":[],\"query_faild_data\":[]}";
respond(exchange, body);
});
try {
BrandCheckClient client = newClient(server, 3);
BrandCheckClient.BrandCheckBatchResult result = client.checkAll(List.of("Apple"), "Terms");
assertThat(attempts.get()).as("前两次失败后应重试到成功").isEqualTo(3);
assertThat(result.hasFailedData()).isFalse();
assertThat(result.hasQueryFailedData()).isFalse();
} finally {
server.stop(0);
}
}
@Test
void persistentQueryFailureIsReportedAfterExhaustingRetries() throws Exception {
AtomicInteger attempts = new AtomicInteger();
HttpServer server = startBrandCheckServer(exchange -> {
attempts.incrementAndGet();
respond(exchange, "{\"faild_data\":[],\"query_faild_data\":[\"Apple\"]}");
});
try {
BrandCheckClient client = newClient(server, 3);
BrandCheckClient.BrandCheckBatchResult result = client.checkAll(List.of("Apple"), "Terms");
assertThat(attempts.get()).as("持续失败应耗尽重试次数").isEqualTo(3);
assertThat(result.hasFailedData()).isFalse();
assertThat(result.hasQueryFailedData()).isTrue();
assertThat(result.queryFaildData()).contains("Apple");
} finally {
server.stop(0);
}
}
@Test
void confirmedInfringementIsNotRetried() throws Exception {
AtomicInteger attempts = new AtomicInteger();
HttpServer server = startBrandCheckServer(exchange -> {
attempts.incrementAndGet();
respond(exchange, "{\"faild_data\":[\"Apple\"],\"query_faild_data\":[]}");
});
try {
BrandCheckClient client = newClient(server, 3);
BrandCheckClient.BrandCheckBatchResult result = client.checkAll(List.of("Apple"), "Terms");
assertThat(attempts.get()).as("命中侵权属有效结论,不应重试").isEqualTo(1);
assertThat(result.hasFailedData()).isTrue();
assertThat(result.hasQueryFailedData()).isFalse();
} finally {
server.stop(0);
}
}
private BrandCheckClient newClient(HttpServer server, int retryTimes) {
BrandCheckProperties props = new BrandCheckProperties();
props.setBaseUrl("http://127.0.0.1:" + server.getAddress().getPort());
props.setPath("/brand_check");
props.setRetryTimes(retryTimes);
props.setRetryIntervalMillis(1);
return new BrandCheckClient(props, null);
}
private HttpServer startBrandCheckServer(com.sun.net.httpserver.HttpHandler handler) throws IOException {
HttpServer server = HttpServer.create(new InetSocketAddress("127.0.0.1", 0), 0);
server.createContext("/brand_check", handler);
server.start();
return server;
}
private void respond(com.sun.net.httpserver.HttpExchange exchange, String body) throws IOException {
byte[] bytes = body.getBytes(StandardCharsets.UTF_8);
exchange.getResponseHeaders().set("Content-Type", "application/json; charset=utf-8");
exchange.sendResponseHeaders(200, bytes.length);
try (OutputStream os = exchange.getResponseBody()) {
os.write(bytes);
}
}
}
@@ -229,12 +229,13 @@ class SimilarAsinImageEmbedderTest {
var row = workbook.createSheet().createRow(1);
SimilarAsinImageEmbedder.ImageDim result = failedEmbedder.embedAsExcelCellImage(
1, 9, url, row, new HashMap<>(), WpsCellImageWriter.createSession(), spool);
1, 9, url, row, new HashMap<>(), ExcelCellImageWriter.createSession(), spool);
assertEquals(SimilarAsinImageEmbedder.DOWNLOAD_MAX_RETRY + 1, callsAfterPrefetch);
assertEquals(callsAfterPrefetch, networkCalls.get());
assertNull(result);
assertNull(row.getCell(9), "预取失败不应把 URL 文本写进单元格,应留空");
assertTrue(row.getCell(9) != null, "预取失败应回退为 URL 文本便于人工核对");
assertEquals(url, row.getCell(9).getStringCellValue());
} finally {
failedEmbedder.shutdown();
}