feat(amazon):优化商标筛查重试机制和进度显示- 添加防抖控制,避免频繁点击重试按钮

- 优化重试逻辑,增加时间间隔限制和状态检查
- 移除表格上方冗余的进度条显示
- 更新取消状态下的提示文案和操作引导- 修复品牌统计数据显示逻辑,确保准确性- 调整用户界面元素间距和样式细节
- 完善后端接口调用,支持信号中断和错误处理
-优化SSE连接管理,防止连接泄漏
- 改进任务取消机制,提升用户体验
- 更新用户信息展示,增加注册时间显示
This commit is contained in:
2025-11-14 17:22:25 +08:00
parent dd23d9fe90
commit f9d1848280
15 changed files with 1245 additions and 641 deletions

View File

@@ -1,13 +0,0 @@
package com.tashow.erp.config;
import com.tashow.erp.fx.controller.JavaBridge;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
@Configuration
public class JavaBridgeConfig {
@Bean
public JavaBridge javaBridge() {
return new JavaBridge();
}
}

View File

@@ -9,6 +9,7 @@ import com.tashow.erp.service.IFangzhouApiService;
import com.tashow.erp.utils.ExcelParseUtil;
import com.tashow.erp.utils.JsonData;
import com.tashow.erp.utils.LoggerUtil;
import com.tashow.erp.utils.ProxyPool;
import com.tashow.erp.utils.TrademarkCheckUtil;
import cn.hutool.core.io.FileUtil;
import cn.hutool.http.HttpUtil;
@@ -18,9 +19,11 @@ import org.slf4j.Logger;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.*;
import org.springframework.web.multipart.MultipartFile;
import org.springframework.web.servlet.mvc.method.annotation.SseEmitter;
import java.time.LocalDateTime;
import java.util.*;
import java.util.concurrent.ConcurrentHashMap;
import java.util.stream.Collectors;
/**
@@ -34,7 +37,7 @@ public class TrademarkController {
private static final ObjectMapper objectMapper = new ObjectMapper();
@Autowired
private TrademarkCheckUtil util;
private ProxyPool proxyPool;
@Autowired
private BrandTrademarkCacheService cacheService;
@@ -45,58 +48,170 @@ public class TrademarkController {
@Autowired
private IFangzhouApiService fangzhouApi;
// 进度追踪
private final Map<String, Integer> progressMap = new java.util.concurrent.ConcurrentHashMap<>();
private static final Map<String, Integer> progressMap = new ConcurrentHashMap<>();
private static final Map<String, Boolean> cancelMap = new ConcurrentHashMap<>();
private static final Map<String, SseEmitter> sseEmitters = new ConcurrentHashMap<>();
private static final Map<String, java.util.concurrent.ExecutorService> taskExecutors = new ConcurrentHashMap<>();
private static volatile String currentTaskId = null;
private static final Object taskLock = new Object();
private static volatile boolean isUploadingFile = false;
private static final Object uploadLock = new Object();
// 任务取消标志
private final Map<String, Boolean> cancelMap = new java.util.concurrent.ConcurrentHashMap<>();
/**
* 批量品牌商标筛查
*/
@GetMapping("/progress/{taskId}")
public SseEmitter getProgress(@PathVariable String taskId) {
SseEmitter emitter = new SseEmitter(300000L);
sseEmitters.put(taskId, emitter);
emitter.onCompletion(() -> sseEmitters.remove(taskId));
emitter.onTimeout(() -> sseEmitters.remove(taskId));
emitter.onError((e) -> sseEmitters.remove(taskId));
return emitter;
}
@PostMapping("/brandCheck")
public JsonData brandCheck(@RequestBody Map<String, Object> request) {
@SuppressWarnings("unchecked")
List<String> brands = (List<String>) request.get("brands");
String taskId = (String) request.get("taskId");
synchronized (taskLock) {
if (currentTaskId != null && !currentTaskId.equals(taskId)) {
logger.info("检测到新任务 {},终止旧任务 {}", taskId, currentTaskId);
forceTerminateTask(currentTaskId);
}
currentTaskId = taskId;
cancelMap.remove(taskId);
}
try {
List<String> list = brands.stream()
.filter(b -> !b.trim().isEmpty())
.map(String::trim)
.collect(Collectors.toList());
long start = System.currentTimeMillis();
// 1. 先从全局缓存获取
Map<String, Boolean> cached = cacheService.getCached(list);
// 2. 找出缓存未命中的品牌
List<String> toQuery = list.stream()
.filter(b -> !cached.containsKey(b))
.collect(Collectors.toList());
Map<String, Boolean> queried = new HashMap<>();
Map<String, Boolean> queried = new java.util.concurrent.ConcurrentHashMap<>();
if (!toQuery.isEmpty()) {
for (int i = 0; i < toQuery.size(); i++) {
List<List<String>> chunks = new ArrayList<>();
int totalBrands = toQuery.size();
if (totalBrands <= 100) {
chunks.add(toQuery);
} else {
int chunkSize = 100;
int numChunks = (totalBrands + chunkSize - 1) / chunkSize;
int baseSize = totalBrands / numChunks;
int remainder = totalBrands % numChunks;
int startIndex = 0;
for (int i = 0; i < numChunks; i++) {
int currentChunkSize = baseSize + (i < remainder ? 1 : 0);
chunks.add(toQuery.subList(startIndex, startIndex + currentChunkSize));
startIndex += currentChunkSize;
}
}
// 根据实际线程数获取代理,不浪费
int proxyCount = chunks.size();
List<String> proxies = proxyPool.getProxies(proxyCount);
if (proxies.size() < chunks.size()) {
logger.warn("代理数量不足,需要{}个,实际获取{}个", chunks.size(), proxies.size());
}
logger.info("获取到{}个代理,分配给{}个线程", proxies.size(), chunks.size());
java.util.concurrent.ExecutorService executor = java.util.concurrent.Executors.newFixedThreadPool(chunks.size());
taskExecutors.put(taskId, executor);
List<java.util.concurrent.Future<Map<String, Boolean>>> futures = new ArrayList<>();
for (int i = 0; i < chunks.size(); i++) {
if (cancelMap.getOrDefault(taskId, false)) {
logger.info("任务 {} 已被取消,停止查询", taskId);
logger.info("任务 {} 已被取消", taskId);
break;
}
String brand = toQuery.get(i);
logger.info("处理第 {} 个: {}", i + 1, brand);
List<String> chunk = chunks.get(i);
String proxy = proxies.isEmpty() ? null : proxies.get(i % proxies.size());
Map<String, Boolean> results = util.batchCheck(Collections.singletonList(brand), queried);
queried.putAll(results);
final int chunkIndex = i;
futures.add(executor.submit(() -> {
if (cancelMap.getOrDefault(taskId, false)) {
return new HashMap<String, Boolean>();
}
if (taskId != null) progressMap.put(taskId, cached.size() + queried.size());
logger.info("线程 {} 开始处理 {} 个品牌,使用代理: {}", chunkIndex, chunk.size(), proxy);
Map<String, Boolean> result = TrademarkCheckUtil.batchCheck(chunk, proxy, taskId, cancelMap, chunkIndex, sseEmitters);
if (cancelMap.getOrDefault(taskId, false)) {
return new HashMap<String, Boolean>();
}
return result;
}));
}
if (!queried.isEmpty()) cacheService.saveResults(queried);
for (java.util.concurrent.Future<Map<String, Boolean>> future : futures) {
if (cancelMap.getOrDefault(taskId, false)) {
logger.info("任务 {} 已被取消,停止收集结果", taskId);
break;
}
try {
Map<String, Boolean> result = future.get();
if (!result.isEmpty()) {
queried.putAll(result);
}
} catch (java.util.concurrent.CancellationException e) {
logger.info("线程任务已被取消: {}", taskId);
} catch (InterruptedException e) {
logger.info("线程任务被中断: {}", taskId);
Thread.currentThread().interrupt();
} catch (Exception e) {
logger.error("获取线程结果失败", e);
}
}
taskExecutors.remove(taskId);
executor.shutdown();
try {
if (!executor.awaitTermination(60, java.util.concurrent.TimeUnit.SECONDS)) {
logger.warn("线程池未能在60秒内正常关闭强制关闭");
executor.shutdownNow();
if (!executor.awaitTermination(10, java.util.concurrent.TimeUnit.SECONDS)) {
logger.error("线程池强制关闭失败");
}
}
} catch (InterruptedException e) {
logger.warn("等待线程池关闭时被中断,强制关闭");
executor.shutdownNow();
Thread.currentThread().interrupt();
}
if (!queried.isEmpty()) {
cacheService.saveResults(queried);
}
}
// 检查任务是否已被取消
if (cancelMap.getOrDefault(taskId, false)) {
logger.info("任务 {} 已被取消,停止处理结果", taskId);
synchronized (taskLock) {
if (taskId.equals(currentTaskId)) {
currentTaskId = null;
}
}
progressMap.remove(taskId);
cancelMap.remove(taskId);
return JsonData.buildError("任务已取消");
}
// 5. 合并缓存和新查询结果
Map<String, Boolean> allResults = new HashMap<>(cached);
allResults.putAll(queried);
// 6. 统计结果
List<Map<String, Object>> unregistered = new ArrayList<>();
int registeredCount = 0;
@@ -112,42 +227,37 @@ public class TrademarkController {
}
long t = (System.currentTimeMillis() - start) / 1000;
int checkedCount = list.size();
int failedCount = 0;
Map<String, Object> res = new HashMap<>();
res.put("total", list.size());
res.put("checked", checkedCount);
res.put("checked", list.size());
res.put("registered", registeredCount);
res.put("unregistered", unregistered.size());
res.put("failed", failedCount);
res.put("failed", 0);
res.put("data", unregistered);
res.put("duration", t + "");
logger.info("完成: 共{}个,成功查询{}个(已注册{}个,未注册{}个),查询失败{}个,耗时{}秒",
list.size(), checkedCount, registeredCount, unregistered.size(), failedCount, t);
logger.info("完成: 共{}个,已注册{}个,未注册{}个,耗时{}秒",
list.size(), registeredCount, unregistered.size(), t);
if (taskId != null) {
new Thread(() -> {
try { Thread.sleep(30000); } catch (InterruptedException ignored) {}
progressMap.remove(taskId);
cancelMap.remove(taskId);
}).start();
synchronized (taskLock) {
if (taskId.equals(currentTaskId)) {
currentTaskId = null;
}
}
progressMap.remove(taskId);
cancelMap.remove(taskId);
return JsonData.buildSuccess(res);
} catch (Exception e) {
logger.error("筛查失败", e);
return JsonData.buildError("筛查失败: " + e.getMessage());
} finally {
if (util != null && util.driver != null) util.driver.quit();
cacheService.cleanExpired();
}
}
/**
* 查询品牌筛查进度
*/
@GetMapping("/brandCheckProgress")
public JsonData getBrandCheckProgress(@RequestParam("taskId") String taskId) {
Integer current = progressMap.get(taskId);
@@ -159,23 +269,58 @@ public class TrademarkController {
return JsonData.buildSuccess(result);
}
/**
* 取消品牌筛查任务
*/
@PostMapping("/cancelBrandCheck")
public JsonData cancelBrandCheck(@RequestBody Map<String, String> request) {
String taskId = request.get("taskId");
if (taskId != null) {
cancelMap.put(taskId, true);
logger.info("任务 {} 已标记为取消", taskId);
return JsonData.buildSuccess("任务已取消");
forceTerminateTask(taskId);
}
return JsonData.buildError("缺少taskId参数");
return JsonData.buildSuccess("任务已取消");
}
private void forceTerminateTask(String taskId) {
logger.info("开始强制终止任务: {}", taskId);
cancelMap.put(taskId, true);
java.util.concurrent.ExecutorService executor = taskExecutors.remove(taskId);
if (executor != null && !executor.isShutdown()) {
logger.info("强制关闭任务 {} 的线程池", taskId);
executor.shutdownNow();
try {
if (!executor.awaitTermination(5, java.util.concurrent.TimeUnit.SECONDS)) {
logger.warn("任务 {} 的线程池未能在5秒内关闭", taskId);
} else {
logger.info("任务 {} 的线程池已成功关闭", taskId);
}
} catch (InterruptedException e) {
logger.warn("等待线程池关闭时被中断");
Thread.currentThread().interrupt();
}
}
SseEmitter emitter = sseEmitters.remove(taskId);
if (emitter != null) {
try {
emitter.send(SseEmitter.event().name("cancelled").data("任务已取消"));
emitter.complete();
} catch (Exception e) {
logger.warn("关闭SSE连接失败: {}", e.getMessage());
}
}
progressMap.remove(taskId);
synchronized (taskLock) {
if (taskId.equals(currentTaskId)) {
currentTaskId = null;
}
}
logger.info("任务 {} 强制终止完成", taskId);
}
/**
* 验证Excel表头
*/
@PostMapping("/validateHeaders")
public JsonData validateHeaders(@RequestParam("file") MultipartFile file,
@RequestParam(value = "requiredHeaders", required = false) String requiredHeadersJson) {
@@ -191,7 +336,6 @@ public class TrademarkController {
Map<String, Object> result = new HashMap<>();
result.put("headers", headers);
// 如果提供了必需表头,进行验证
if (requiredHeadersJson != null && !requiredHeadersJson.trim().isEmpty()) {
List<String> requiredHeaders = objectMapper.readValue(requiredHeadersJson,
objectMapper.getTypeFactory().constructCollectionType(List.class, String.class));
@@ -497,6 +641,15 @@ public class TrademarkController {
*/
@PostMapping("/newTask")
public JsonData newTask(@RequestParam("file") MultipartFile file) {
// 防止重复上传:如果已有上传任务在进行,直接拒绝
synchronized (uploadLock) {
if (isUploadingFile) {
logger.warn("文件上传被拒绝:已有上传任务正在进行中");
return JsonData.buildError("请勿重复点击,上传任务进行中");
}
isUploadingFile = true;
}
try {
// 1. 获取 Token 并上传文件
String token = fangzhouApi.getToken();
@@ -512,6 +665,11 @@ public class TrademarkController {
} catch (Exception e) {
logger.error("创建任务失败", e);
return JsonData.buildError("创建任务失败: " + e.getMessage());
} finally {
// 释放上传锁
synchronized (uploadLock) {
isUploadingFile = false;
}
}
}
}

View File

@@ -1,79 +0,0 @@
package com.tashow.erp.fx.controller;
import lombok.extern.slf4j.Slf4j;
import java.awt.*;
import java.awt.datatransfer.Clipboard;
import java.awt.datatransfer.StringSelection;
import java.io.File;
import java.io.FileOutputStream;
import java.io.IOException;
@Slf4j
public class JavaBridge {
/**
* 直接保存字节数组为Excel文件到桌面纯 Spring Boot 环境,无文件对话框)
*/
public String saveExcelFileToDesktop(byte[] data, String fileName) {
try {
if (data == null || data.length == 0) {
log.warn("文件数据为空,无法保存文件");
return null;
}
String userHome = System.getProperty("user.home");
File desktop = new File(userHome, "Desktop");
if (!desktop.exists()) {
// 回退到用户目录
desktop = new File(userHome);
}
File file = new File(desktop, fileName);
int counter = 1;
if (fileName != null && fileName.contains(".")) {
String baseName = fileName.substring(0, fileName.lastIndexOf('.'));
String extension = fileName.substring(fileName.lastIndexOf('.'));
while (file.exists()) {
file = new File(desktop, baseName + "_" + counter + extension);
counter++;
}
} else {
while (file.exists()) {
file = new File(desktop, fileName + "_" + counter);
counter++;
}
}
try (FileOutputStream fos = new FileOutputStream(file)) {
fos.write(data);
fos.flush();
}
String filePath = file.getAbsolutePath();
log.info("Excel文件已保存: {}", filePath);
return filePath;
} catch (IOException e) {
log.error("保存Excel文件失败: {}", e.getMessage(), e);
return null;
}
}
/**
* 复制文本到系统剪贴板
*/
public boolean copyToClipboard(String text) {
try {
if (text == null || text.trim().isEmpty()) {
return false;
}
Clipboard clipboard = Toolkit.getDefaultToolkit().getSystemClipboard();
StringSelection selection = new StringSelection(text);
clipboard.setContents(selection, null);
return true;
} catch (Exception e) {
log.error("复制到剪贴板失败: {}", e.getMessage());
return false;
}
}
}

View File

@@ -34,7 +34,7 @@ public class UsptoApiTest {
TrademarkCheckUtil trademarkUtil = context.getBean(TrademarkCheckUtil.class);
// 测试单品牌查询(获取详细结果)
testSingleBrandWithDetailedResults("Remorlet");
testSingleBrandWithDetailedResults("SummitFlare");
} catch (Exception e) {
System.err.println("测试失败: " + e.getMessage());
@@ -209,7 +209,7 @@ public class UsptoApiTest {
}
// 极简输出 - 只显示最终结果
System.out.println(isRegistered);
System.out.println(results);
} catch (Exception e) {
System.err.println("=== 测试失败 ===");

View File

@@ -2,30 +2,49 @@ package com.tashow.erp.utils;
import cn.hutool.http.HttpUtil;
import org.springframework.stereotype.Component;
import java.util.ArrayList;
import java.util.List;
/**
* 代理IP池
*/
@Component
public class ProxyPool {
private static final String API_URL = "http://api.tianqiip.com/getip?secret=h6x09x0eenxuf4s7&num=1&type=txt&port=2&time=3&mr=1&sign=620719f6b7d66744b0216a4f61a6bcee";
private static final String API_URL = "http://api.tianqiip.com/getip?secret=y0thbcco1rgxn9e9&num=%d&type=txt&port=2&time=3&mr=1&sign=a8a42f3cd3f22a7fbf84530deb91c1d8";
/**
* 获取一个代理IP
* @return 代理地址格式host:port如 123.96.236.32:40016
*/
public String getProxy() {
List<String> proxies = getProxies(1);
return proxies.isEmpty() ? null : proxies.get(0);
}
/**
* 批量获取代理IP
* @param num 需要获取的代理数量
* @return 代理地址列表
*/
public List<String> getProxies(int num) {
List<String> proxies = new ArrayList<>();
try {
String response = HttpUtil.get(API_URL);
String url = String.format(API_URL, num);
String response = HttpUtil.get(url);
if (response != null && !response.trim().isEmpty()) {
String proxy = response.trim();
System.out.println("获取到代理: " + proxy);
return proxy;
String[] lines = response.trim().split("\n");
for (String line : lines) {
String proxy = line.trim();
if (!proxy.isEmpty()) {
proxies.add(proxy);
}
}
System.out.println("获取到 " + proxies.size() + " 个代理");
}
} catch (Exception e) {
System.err.println("获取代理失败: " + e.getMessage());
}
return null;
return proxies;
}
}

View File

@@ -1,100 +1,217 @@
package com.tashow.erp.utils;
import com.tashow.erp.service.BrandTrademarkCacheService;
import jakarta.annotation.PreDestroy;
import org.openqa.selenium.JavascriptExecutor;
import org.openqa.selenium.chrome.ChromeDriver;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
import java.util.*;
/**
* 商标检查工具
* 检测到403时自动切换代理并重试
* 商标检查工具 - 无状态设计
* 每次调用使用独立的Driver和代理
*/
@Component
public class TrademarkCheckUtil {
@Autowired
private ProxyPool proxyPool;
@Autowired
private BrandTrademarkCacheService cacheService;
public ChromeDriver driver;
private final int maxRetries = 3;
private String normalize(String name) {
private static String normalize(String name) {
return name.toLowerCase().replaceAll("[^a-z0-9]", "");
}
private synchronized void ensureInit() {
if (driver == null) {
driver = SeleniumUtil.createDriver(true, proxyPool.getProxy());
driver.get("https://tmsearch.uspto.gov/search/search-results");
try { Thread.sleep(6000); } catch (InterruptedException ignored) {}
}
/**
* 批量检查商标(使用指定代理)
* @param brands 品牌列表
* @param proxy 代理地址格式host:port
* @return 检查结果 Map<品牌, 是否已注册>
*/
public static Map<String, Boolean> batchCheck(List<String> brands, String proxy) {
return batchCheck(brands, proxy, null, null);
}
public synchronized Map<String, Boolean> batchCheck(List<String> brands, Map<String, Boolean> alreadyQueried) {
/**
* 批量检查商标(使用指定代理,支持取消检查)
* @param brands 品牌列表
* @param proxy 代理地址格式host:port
* @param taskId 任务ID用于取消检查
* @param cancelMap 取消状态映射
* @return 检查结果 Map<品牌, 是否已注册>
*/
public static Map<String, Boolean> batchCheck(List<String> brands, String proxy, String taskId, java.util.Map<String, Boolean> cancelMap) {
return batchCheck(brands, proxy, taskId, cancelMap, -1, null);
}
public static Map<String, Boolean> batchCheck(List<String> brands, String proxy, String taskId, java.util.Map<String, Boolean> cancelMap, int processIndex) {
return batchCheck(brands, proxy, taskId, cancelMap, processIndex, null);
}
/**
* 批量检查商标(使用指定代理,支持取消检查和进程标识)
* @param brands 品牌列表
* @param proxy 代理地址格式host:port
* @param taskId 任务ID用于取消检查
* @param cancelMap 取消状态映射
* @param processIndex 进程索引,用于日志标识
* @return 检查结果 Map<品牌, 是否已注册>
*/
public static Map<String, Boolean> batchCheck(List<String> brands, String proxy, String taskId, java.util.Map<String, Boolean> cancelMap, int processIndex, java.util.Map<String, org.springframework.web.servlet.mvc.method.annotation.SseEmitter> sseEmitters) {
Map<String, Boolean> resultMap = new HashMap<>();
for (String brand : brands) {
int retryCount = 0;
boolean success = false;
while (retryCount < 5 && !success) {
try {
ensureInit();
String script = "fetch('https://tmsearch.uspto.gov/prod-stage-v1-0-0/tmsearch',{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify({query:{bool:{must:[{bool:{should:[{match_phrase:{WM:{query:'" + brand.replace("'", "\\'")+"',boost:5}}}]}}]}},size:100})}).then(r=>{if(!r.ok){return arguments[arguments.length-1]({hits:[],error:'HTTP '+r.status+': '+r.statusText});}return r.text().then(text=>{if(text.startsWith('<!DOCTYPE')||text.startsWith('<html')){return arguments[arguments.length-1]({hits:[],error:'HTML response detected, likely blocked'});}try{const d=JSON.parse(text);return arguments[arguments.length-1]({hits:d?.hits?.hits||[],error:null});}catch(e){return arguments[arguments.length-1]({hits:[],error:'JSON parse error: '+e.message});}});}).catch(e=>arguments[arguments.length-1]({hits:[],error:e.message}));";
@SuppressWarnings("unchecked") Map<String, Object> result = (Map<String, Object>) ((JavascriptExecutor) driver).executeAsyncScript(script);
String error = (String) result.get("error");
if (error != null && (error.contains("HTTP 403") || error.contains("Failed to fetch") || error.contains("NetworkError") || error.contains("TypeError") || error.contains("script timeout"))) {
System.err.println(brand + " 查询失败(" + (retryCount + 1) + "/3): " + error + ",切换代理...");
if (driver != null) driver.quit();
driver = null;
retryCount++;
continue;
}
if (error == null) {
@SuppressWarnings("unchecked") List<Map<String, Object>> hits = (List<Map<String, Object>>) result.get("hits");
String input = normalize(brand);
boolean registered = false;
for (Map<String, Object> hit : hits) {
@SuppressWarnings("unchecked") Map<String, Object> source = (Map<String, Object>) hit.get("source");
if (source != null && input.equals(normalize((String) source.get("wordmark")))) {
Number code = (Number) source.get("statusCode");
if (code != null && (code.intValue() == 688 || code.intValue() == 700)) {
registered = true;
break;
}
}
}
resultMap.put(brand, registered);
System.out.println(brand + " -> " + (registered ? "" : ""));
success = true;
} else {
System.err.println(brand + " -> [查询失败: " + error + "]");
resultMap.put(brand, true);
success = true;
}
} catch (Exception e) {
System.err.println(brand + " 查询异常(" + (retryCount + 1) + "/3): " + e.getMessage());
if (driver != null) driver.quit();
String processPrefix = processIndex >= 0 ? "进程" + processIndex + "" : "";
ChromeDriver driver = null;
String currentProxy = proxy; // 当前使用的代理
// 初始化Driver失败时重试并换IP
int initRetryCount = 0;
while (initRetryCount < 5 && driver == null) {
try {
driver = SeleniumUtil.createDriver(true, currentProxy);
driver.get("https://tmsearch.uspto.gov/search/search-results");
Thread.sleep(6000);
break; // 成功则跳出循环
} catch (Exception initError) {
System.err.println(processPrefix + "Driver初始化失败(" + (initRetryCount + 1) + "/5): " + initError.getMessage());
if (driver != null) {
try { driver.quit(); } catch (Exception ignored) {}
driver = null;
retryCount++;
}
// 获取新代理重试
if (initRetryCount < 2) {
try {
ProxyPool proxyPool = new ProxyPool();
String newProxy = proxyPool.getProxy();
if (newProxy != null) {
currentProxy = newProxy;
System.out.println(processPrefix + "初始化失败,切换到新代理: " + currentProxy);
} else {
System.err.println(processPrefix + "获取新代理失败,使用原代理重试");
}
} catch (Exception e) {
System.err.println(processPrefix + "获取新代理异常: " + e.getMessage());
}
}
initRetryCount++;
if (initRetryCount < 3) {
try { Thread.sleep(2000); } catch (InterruptedException ignored) {}
}
}
}
if (driver == null) {
System.err.println(processPrefix + "Driver初始化失败已重试3次跳过该批次");
return resultMap;
}
try {
if (!success) {
System.err.println(brand + " -> [查询失败: 已重试3次]");
resultMap.put(brand, true);
for (String brand : brands) {
// 检查是否已取消
if (taskId != null && cancelMap != null && cancelMap.getOrDefault(taskId, false)) {
System.out.println("检测到任务已取消,停止处理品牌: " + brand);
break;
}
int retryCount = 0;
boolean success = false;
while (retryCount < 5 && !success) {
// 在重试循环中也检查取消状态
if (taskId != null && cancelMap != null && cancelMap.getOrDefault(taskId, false)) {
System.out.println("检测到任务已取消,停止重试品牌: " + brand);
return resultMap;
}
try {
String script = "fetch('https://tmsearch.uspto.gov/prod-stage-v1-0-0/tmsearch',{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify({query:{bool:{must:[{bool:{should:[{match_phrase:{WM:{query:'" + brand.replace("'", "\\'")+"',boost:5}}}]}}]}},size:100})}).then(r=>{if(!r.ok){return arguments[arguments.length-1]({hits:[],error:'HTTP '+r.status+': '+r.statusText});}return r.text().then(text=>{if(text.startsWith('<!DOCTYPE')||text.startsWith('<html')){return arguments[arguments.length-1]({hits:[],error:'HTML response detected, likely blocked'});}try{const d=JSON.parse(text);return arguments[arguments.length-1]({hits:d?.hits?.hits||[],error:null});}catch(e){return arguments[arguments.length-1]({hits:[],error:'JSON parse error: '+e.message});}});}).catch(e=>arguments[arguments.length-1]({hits:[],error:e.message}));";
@SuppressWarnings("unchecked") Map<String, Object> result = (Map<String, Object>) ((JavascriptExecutor) driver).executeAsyncScript(script);
String error = (String) result.get("error");
if (error != null && (error.contains("HTTP 403") || error.contains("Failed to fetch") || error.contains("NetworkError") || error.contains("TypeError") || error.contains("script timeout"))) {
System.err.println(processPrefix + brand + " 查询失败(" + (retryCount + 1) + "/5): " + error + ",切换代理...");
// 对于网络错误获取新代理并重新创建Driver
if (error.contains("Failed to fetch") || error.contains("HTTP 403") || error.contains("NetworkError") || error.contains("ERR_CONNECTION_RESET")) {
try {
if (driver != null) driver.quit();
// 获取新的代理IP
ProxyPool proxyPool = new ProxyPool();
String newProxy = proxyPool.getProxy();
if (newProxy != null) {
currentProxy = newProxy;
System.out.println(processPrefix + "切换到新代理: " + currentProxy);
} else {
System.err.println(processPrefix + "获取新代理失败,使用原代理");
}
driver = SeleniumUtil.createDriver(true, currentProxy);
driver.get("https://tmsearch.uspto.gov/search/search-results");
Thread.sleep(3000); // 缩短等待时间
} catch (Exception e) {
System.err.println(processPrefix + "重新创建Driver失败: " + e.getMessage());
}
}
retryCount++;
continue;
}
if (error == null) {
@SuppressWarnings("unchecked") List<Map<String, Object>> hits = (List<Map<String, Object>>) result.get("hits");
String input = normalize(brand);
boolean registered = false;
for (Map<String, Object> hit : hits) {
@SuppressWarnings("unchecked") Map<String, Object> source = (Map<String, Object>) hit.get("source");
if (source != null && input.equals(normalize((String) source.get("wordmark")))) {
Number code = (Number) source.get("statusCode");
if (code != null && (code.intValue() == 688 || code.intValue() == 700 || code.intValue() == 686)) {
registered = true;
break;
}
}
}
resultMap.put(brand, registered);
System.out.println(processPrefix + brand + " -> " + (registered ? "" : ""));
success = true;
// 推送SSE进度
if (taskId != null && sseEmitters != null) {
org.springframework.web.servlet.mvc.method.annotation.SseEmitter emitter = sseEmitters.get(taskId);
if (emitter != null) {
try {
emitter.send(org.springframework.web.servlet.mvc.method.annotation.SseEmitter.event()
.name("progress")
.data(processPrefix + brand + " -> " + (registered ? "" : "")));
} catch (Exception ignored) {}
}
}
} else {
System.err.println(processPrefix + brand + " -> [查询失败: " + error + "]");
resultMap.put(brand, true);
success = true;
}
} catch (Exception e) {
System.err.println(processPrefix + brand + " 查询异常(" + (retryCount + 1) + "/5): " + e.getMessage());
retryCount++;
}
}
if (!success) {
System.err.println(processPrefix + brand + " -> [查询失败: 已重试5次]");
resultMap.put(brand, true);
}
}
} catch (Exception e) {
System.err.println("Driver初始化失败: " + e.getMessage());
} finally {
if (driver != null) {
try {
driver.quit();
} catch (Exception ignored) {}
}
}
return resultMap;
}
@PreDestroy
public void cleanup() {
if (driver != null) driver.quit();
}
}