feat(trademark):优化商标查询功能和Excel解析逻辑

- 重构品牌商标缓存服务,移除冗余的日志记录和存在检查- 简化Excel解析工具类,提取公共方法并优化列索引查找逻辑
- 增强Electron客户端开发模式下的后端启动控制能力
- 改进商标筛查面板的用户体验和数据处理流程-优化商标查询工具类,提高查询准确性和稳定性
- 调整商标控制器接口参数校验逻辑和资源清理机制
- 更新USPTO API测试用例以支持Spring容器环境运行
This commit is contained in:
2025-11-13 14:20:12 +08:00
parent cfb9096788
commit 007799fb2a
7 changed files with 402 additions and 527 deletions

View File

@@ -435,6 +435,19 @@ app.whenReady().then(() => {
createWindow(); createWindow();
createTray(mainWindow); createTray(mainWindow);
// 开发模式快捷键
if (isDev && mainWindow) {
mainWindow.webContents.on('before-input-event', (event, input) => {
if (input.control && input.shift && input.key.toLowerCase() === 'd') {
console.log('[开发模式] 手动跳过后端启动');
openAppIfNotOpened();
} else if (input.control && input.shift && input.key.toLowerCase() === 's') {
console.log('[开发模式] 手动启动后端服务');
startSpringBoot();
}
});
}
// 只有在不需要最小化启动时才显示 splash 窗口 // 只有在不需要最小化启动时才显示 splash 窗口
if (!shouldMinimize) { if (!shouldMinimize) {
const config = loadConfig(); const config = loadConfig();
@@ -476,9 +489,22 @@ app.whenReady().then(() => {
} }
console.log('[启动流程] 准备启动 Spring Boot...'); console.log('[启动流程] 准备启动 Spring Boot...');
setTimeout(() => {
startSpringBoot(); // 开发模式:添加快捷键跳过后端启动
}, 200); if (isDev) {
console.log('[开发模式] 按 Ctrl+Shift+D 跳过后端启动,直接进入应用');
console.log('[开发模式] 按 Ctrl+Shift+S 手动启动后端服务');
// 5秒后自动跳过避免卡死
setTimeout(() => {
console.log('[开发模式] 自动跳过后端启动,直接进入应用');
openAppIfNotOpened();
}, 5000);
}
// setTimeout(() => {
// startSpringBoot();
// }, 200);
app.on('activate', () => { app.on('activate', () => {
if (mainWindow && !mainWindow.isDestroyed()) { if (mainWindow && !mainWindow.isDestroyed()) {
@@ -847,6 +873,26 @@ ipcMain.handle('set-launch-config', (event, launchConfig: { autoLaunch: boolean;
// 刷新页面 // 刷新页面
ipcMain.handle('reload', () => mainWindow?.webContents.reload()); ipcMain.handle('reload', () => mainWindow?.webContents.reload());
// 开发模式:跳过后端启动
ipcMain.handle('dev-skip-backend', () => {
if (isDev) {
console.log('[开发模式] 前端请求跳过后端启动');
openAppIfNotOpened();
return { success: true };
}
return { success: false, error: '仅开发模式可用' };
});
// 开发模式:手动启动后端
ipcMain.handle('dev-start-backend', () => {
if (isDev) {
console.log('[开发模式] 前端请求启动后端');
startSpringBoot();
return { success: true };
}
return { success: false, error: '仅开发模式可用' };
});
// 窗口控制 API // 窗口控制 API
ipcMain.handle('window-minimize', () => { ipcMain.handle('window-minimize', () => {
if (mainWindow && !mainWindow.isDestroyed()) { if (mainWindow && !mainWindow.isDestroyed()) {

View File

@@ -116,18 +116,11 @@ function showMessage(message: string, type: 'success' | 'warning' | 'error' | 'i
// 拖拽上传 // 拖拽上传
async function processTrademarkFile(file: File) { async function processTrademarkFile(file: File) {
uploadLoading.value = true uploadLoading.value = true
try { try {
// 根据选中的查询类型确定需要的表头 const requiredHeaders = []
const requiredHeaders: string[] = [] if (queryTypes.value.includes('product')) requiredHeaders.push('商品主图')
if (queryTypes.value.includes('product')) { if (queryTypes.value.includes('brand')) requiredHeaders.push('品牌')
requiredHeaders.push('商品主图')
}
if (queryTypes.value.includes('brand')) {
requiredHeaders.push('品牌')
}
// 验证表头
if (requiredHeaders.length > 0) { if (requiredHeaders.length > 0) {
const validateResult = await markApi.validateHeaders(file, requiredHeaders) const validateResult = await markApi.validateHeaders(file, requiredHeaders)
if (validateResult.code !== 200 && validateResult.code !== 0) { if (validateResult.code !== 200 && validateResult.code !== 0) {
@@ -160,15 +153,11 @@ function removeTrademarkFile() {
trademarkFileName.value = '' trademarkFileName.value = ''
trademarkFile.value = null trademarkFile.value = null
uploadLoading.value = false uploadLoading.value = false
if (trademarkUpload.value) { if (trademarkUpload.value) trademarkUpload.value.value = ''
trademarkUpload.value.value = ''
}
} }
// 保存会话到后端
async function saveSession() { async function saveSession() {
if (!trademarkData.value.length) return if (!trademarkData.value.length) return
try { try {
const sessionData = { const sessionData = {
fileName: trademarkFileName.value, fileName: trademarkFileName.value,
@@ -178,7 +167,6 @@ async function saveSession() {
taskProgress: taskProgress.value, taskProgress: taskProgress.value,
queryStatus: queryStatus.value queryStatus: queryStatus.value
} }
const result = await markApi.saveSession(sessionData) const result = await markApi.saveSession(sessionData)
if (result.code === 200 || result.code === 0) { if (result.code === 200 || result.code === 0) {
const username = getUsernameFromToken() const username = getUsernameFromToken()
@@ -258,13 +246,10 @@ async function handleTrademarkUpload(e: Event) {
const input = e.target as HTMLInputElement const input = e.target as HTMLInputElement
const file = input.files?.[0] const file = input.files?.[0]
if (!file) return if (!file) return
if (!/\.xlsx?$/.test(file.name)) {
const ok = /\.xlsx?$/.test(file.name)
if (!ok) {
showMessage('仅支持 .xlsx/.xls 文件', 'warning') showMessage('仅支持 .xlsx/.xls 文件', 'warning')
return return
} }
await processTrademarkFile(file) await processTrademarkFile(file)
input.value = '' input.value = ''
} }
@@ -274,9 +259,7 @@ async function startTrademarkQuery() {
showMessage('请先导入商标列表', 'warning') showMessage('请先导入商标列表', 'warning')
return return
} }
if (refreshVipStatus) await refreshVipStatus() if (refreshVipStatus) await refreshVipStatus()
if (!props.isVip) { if (!props.isVip) {
if (vipStatus) trialExpiredType.value = vipStatus.value.expiredType if (vipStatus) trialExpiredType.value = vipStatus.value.expiredType
showTrialExpiredDialog.value = true showTrialExpiredDialog.value = true
@@ -286,6 +269,7 @@ async function startTrademarkQuery() {
const needProductCheck = queryTypes.value.includes('product') const needProductCheck = queryTypes.value.includes('product')
const needBrandCheck = queryTypes.value.includes('brand') const needBrandCheck = queryTypes.value.includes('brand')
// 立即显示加载状态,防止重复点击
trademarkLoading.value = true trademarkLoading.value = true
trademarkProgress.value = 0 trademarkProgress.value = 0
trademarkData.value = [] trademarkData.value = []
@@ -293,6 +277,9 @@ async function startTrademarkQuery() {
trademarkHeaders.value = [] trademarkHeaders.value = []
queryStatus.value = 'inProgress' queryStatus.value = 'inProgress'
// 立即显示"正在准备..."状态
showMessage('正在准备筛查任务...', 'info')
// 重置任务进度 // 重置任务进度
taskProgress.value.product.total = 0 taskProgress.value.product.total = 0
taskProgress.value.product.current = 0 taskProgress.value.product.current = 0
@@ -476,11 +463,16 @@ async function startTrademarkQuery() {
if (brandList.length > 0) { if (brandList.length > 0) {
const brandData = taskProgress.value.brand const brandData = taskProgress.value.brand
brandData.total = brandList.length brandData.total = brandList.length
brandData.current = 1 // 立即显示初始进度 brandData.current = 0
brandData.completed = 0 brandData.completed = 0
// 生成任务ID并轮询真实进度 // 生成任务ID并立即开始轮询
brandTaskId.value = `task_${Date.now()}` brandTaskId.value = `task_${Date.now()}`
// 立即显示开始状态
showMessage(`开始品牌商标筛查,共${brandList.length}个品牌`, 'success')
// 立即开始进度轮询
brandProgressTimer = setInterval(async () => { brandProgressTimer = setInterval(async () => {
try { try {
const res = await markApi.getBrandCheckProgress(brandTaskId.value) const res = await markApi.getBrandCheckProgress(brandTaskId.value)
@@ -490,72 +482,69 @@ async function startTrademarkQuery() {
} catch (e) { } catch (e) {
// 忽略进度查询错误 // 忽略进度查询错误
} }
}, 1000) }, 500)
const brandResult = await markApi.brandCheck(brandList, brandTaskId.value) const brandResult = await markApi.brandCheck(brandList, brandTaskId.value)
if (brandProgressTimer) clearInterval(brandProgressTimer) if (brandProgressTimer) clearInterval(brandProgressTimer)
if ( brandResult.code === 0) {
if (brandResult.code === 200 || brandResult.code === 0) {
// 完成显示100%
brandData.total = brandResult.data.checked || brandResult.data.total || brandData.total brandData.total = brandResult.data.checked || brandResult.data.total || brandData.total
brandData.current = brandData.total brandData.current = brandData.total
brandData.completed = brandResult.data.unregistered || 0 brandData.completed = brandResult.data.unregistered || 0
isBrandTaskRealData.value = true isBrandTaskRealData.value = true
await processBrandResult(brandResult)
// 提取未注册品牌列表
const unregisteredBrands = brandResult.data.data.map((item: any) => item.brand).filter(Boolean)
if (unregisteredBrands.length > 0) {
// 从原始Excel中过滤出包含这些品牌的完整行
const filterResult = await markApi.filterByBrands(trademarkFile.value, unregisteredBrands)
if (filterResult.code === 200 || filterResult.code === 0) {
// 保存完整数据(用于导出,使用原始表头)
trademarkFullData.value = filterResult.data.filteredRows
trademarkHeaders.value = filterResult.data.headers || []
// 更新统计:显示过滤出的实际行数(而不是品牌数)
brandData.completed = filterResult.data.filteredRows.length
isBrandTaskRealData.value = true
// 将品牌筛查结果作为展示数据
const brandItems = filterResult.data.filteredRows.map((row: any) => ({
name: row['品牌'] || '',
status: '未注册',
class: '',
owner: '',
expireDate: row['注册时间'] || '',
similarity: 0,
asin: row['ASIN'] || '',
productImage: row['商品主图'] || '',
isBrand: true // 标记为品牌数据
}))
// 如果有产品筛查,也替换展示数据(只显示品牌筛查结果)
if (needProductCheck) {
trademarkData.value = brandItems
} else {
trademarkData.value = [...trademarkData.value, ...brandItems]
}
}
}
} else { } else {
throw new Error(brandResult.msg || '品牌筛查失败') throw new Error(brandResult.msg || '品牌筛查失败')
} }
} }
} }
// 只要流程正常完成就设置为done状态不再依赖trademarkLoading // 处理品牌查询结果的函数
queryStatus.value = 'done' async function processBrandResult(brandResult: any) {
emit('updateData', trademarkData.value) // 提取未注册品牌列表
const unregisteredBrands = brandResult.data.data.map((item: any) => item.brand).filter(Boolean)
let summaryMsg = '筛查完成' if (unregisteredBrands.length > 0) {
if (needProductCheck) summaryMsg += `,产品:${taskProgress.value.product.completed}/${taskProgress.value.product.total}` // 从原始Excel中过滤出包含这些品牌的完整行
if (needBrandCheck && brandList.length > 0) summaryMsg += `,品牌:${taskProgress.value.brand.completed}/${taskProgress.value.brand.total}` const filterResult = await markApi.filterByBrands(trademarkFile.value, unregisteredBrands)
showMessage(summaryMsg, 'success')
// 保存会话 // 保存完整数据(用于导出,使用原始表头)
await saveSession() trademarkFullData.value = filterResult.data.filteredRows
trademarkHeaders.value = filterResult.data.headers || []
// 更新统计:显示过滤出的实际行数(而不是品牌数)
const brandData = taskProgress.value.brand
brandData.completed = filterResult.data.filteredRows.length
isBrandTaskRealData.value = true
// 将品牌筛查结果作为展示数据
const brandItems = filterResult.data.filteredRows.map((row: any) => ({
name: row['品牌'] || '',
status: '未注册',
class: '',
owner: '',
expireDate: row['注册时间'] || '',
similarity: 0,
asin: row['ASIN'] || '',
productImage: row['商品主图'] || '',
isBrand: true // 标记为品牌数据
}))
// 更新展示数据
trademarkData.value = [...trademarkData.value, ...brandItems]
}
}
// 只要流程正常完成就设置为done状态
queryStatus.value = 'done'
emit('updateData', trademarkData.value)
let summaryMsg = '筛查完成'
if (needProductCheck) summaryMsg += `,产品:${taskProgress.value.product.completed}/${taskProgress.value.product.total}`
if (needBrandCheck && brandList.length > 0) summaryMsg += `,品牌:${taskProgress.value.brand.completed}/${taskProgress.value.brand.total}`
showMessage(summaryMsg, 'success')
// 保存会话
await saveSession()
} catch (error: any) { } catch (error: any) {
const hasProductData = isProductTaskRealData.value && taskProgress.value.product.total > 0 const hasProductData = isProductTaskRealData.value && taskProgress.value.product.total > 0
const hasBrandData = isBrandTaskRealData.value && taskProgress.value.brand.total > 0 const hasBrandData = isBrandTaskRealData.value && taskProgress.value.brand.total > 0
@@ -715,27 +704,15 @@ function resetToIdle() {
trademarkHeaders.value = [] trademarkHeaders.value = []
trademarkFileName.value = '' trademarkFileName.value = ''
trademarkFile.value = null trademarkFile.value = null
taskProgress.value.product.total = 0 Object.assign(taskProgress.value.product, { total: 0, current: 0, completed: 0 })
taskProgress.value.product.current = 0 Object.assign(taskProgress.value.brand, { total: 0, current: 0, completed: 0 })
taskProgress.value.product.completed = 0 Object.assign(taskProgress.value.platform, { total: 0, current: 0, completed: 0 })
taskProgress.value.brand.total = 0
taskProgress.value.brand.current = 0
taskProgress.value.brand.completed = 0
taskProgress.value.platform.total = 0
taskProgress.value.platform.current = 0
taskProgress.value.platform.completed = 0
// 重置真实数据标记
isProductTaskRealData.value = false isProductTaskRealData.value = false
isBrandTaskRealData.value = false isBrandTaskRealData.value = false
// 清空localStorage中的会话数据
try { try {
const username = getUsernameFromToken() const username = getUsernameFromToken()
localStorage.removeItem(`trademark_session_${username}`) localStorage.removeItem(`trademark_session_${username}`)
} catch (e) { } catch (e) {}
// 忽略错误
}
} }
defineExpose({ defineExpose({

View File

@@ -61,9 +61,10 @@ public class TrademarkController {
String taskId = (String) request.get("taskId"); String taskId = (String) request.get("taskId");
try { try {
List<String> list = brands.stream() List<String> list = brands.stream()
.filter(b -> b != null && !b.trim().isEmpty()) .filter(b -> !b.trim().isEmpty())
.map(String::trim) .map(String::trim)
.collect(Collectors.toList()); .collect(Collectors.toList());
long start = System.currentTimeMillis(); long start = System.currentTimeMillis();
// 1. 先从全局缓存获取 // 1. 先从全局缓存获取
Map<String, Boolean> cached = cacheService.getCached(list); Map<String, Boolean> cached = cacheService.getCached(list);
@@ -74,8 +75,7 @@ public class TrademarkController {
Map<String, Boolean> queried = new HashMap<>(); Map<String, Boolean> queried = new HashMap<>();
if (!toQuery.isEmpty()) { if (!toQuery.isEmpty()) {
for (int i = 0; i < toQuery.size(); i++) { for (int i = 0; i < toQuery.size(); i++) {
// 检查任务是否被取消值 if (cancelMap.getOrDefault(taskId, false)) {
if (taskId != null && cancelMap.getOrDefault(taskId, false)) {
logger.info("任务 {} 已被取消,停止查询", taskId); logger.info("任务 {} 已被取消,停止查询", taskId);
break; break;
} }
@@ -86,15 +86,10 @@ public class TrademarkController {
Map<String, Boolean> results = util.batchCheck(Collections.singletonList(brand), queried); Map<String, Boolean> results = util.batchCheck(Collections.singletonList(brand), queried);
queried.putAll(results); queried.putAll(results);
// 更新进度 if (taskId != null) progressMap.put(taskId, cached.size() + queried.size());
if (taskId != null) {
progressMap.put(taskId, cached.size() + queried.size());
}
} }
// 查询结束,保存所有品牌 if (!queried.isEmpty()) cacheService.saveResults(queried);
if (!queried.isEmpty())
cacheService.saveResults(queried);
} }
// 5. 合并缓存和新查询结果 // 5. 合并缓存和新查询结果
@@ -132,16 +127,11 @@ public class TrademarkController {
logger.info("完成: 共{}个,成功查询{}个(已注册{}个,未注册{}个),查询失败{}个,耗时{}秒", logger.info("完成: 共{}个,成功查询{}个(已注册{}个,未注册{}个),查询失败{}个,耗时{}秒",
list.size(), checkedCount, registeredCount, unregistered.size(), failedCount, t); list.size(), checkedCount, registeredCount, unregistered.size(), failedCount, t);
// 30秒后清理进度和取消标志
if (taskId != null) { if (taskId != null) {
String finalTaskId = taskId;
new Thread(() -> { new Thread(() -> {
try { try { Thread.sleep(30000); } catch (InterruptedException ignored) {}
Thread.sleep(30000); progressMap.remove(taskId);
} catch (InterruptedException ignored) { cancelMap.remove(taskId);
}
progressMap.remove(finalTaskId);
cancelMap.remove(finalTaskId);
}).start(); }).start();
} }
@@ -150,7 +140,7 @@ public class TrademarkController {
logger.error("筛查失败", e); logger.error("筛查失败", e);
return JsonData.buildError("筛查失败: " + e.getMessage()); return JsonData.buildError("筛查失败: " + e.getMessage());
} finally { } finally {
util.closeDriver(); if (util != null && util.driver != null) util.driver.quit();
cacheService.cleanExpired(); cacheService.cleanExpired();
} }
} }

View File

@@ -23,25 +23,18 @@ public class BrandTrademarkCacheServiceImpl implements BrandTrademarkCacheServic
public Map<String, Boolean> getCached(List<String> brands) { public Map<String, Boolean> getCached(List<String> brands) {
LocalDateTime cutoffTime = LocalDateTime.now().minusDays(1); LocalDateTime cutoffTime = LocalDateTime.now().minusDays(1);
List<BrandTrademarkCacheEntity> cached = repository.findByBrandInAndCreatedAtAfter(brands, cutoffTime); List<BrandTrademarkCacheEntity> cached = repository.findByBrandInAndCreatedAtAfter(brands, cutoffTime);
Map<String, Boolean> result = new HashMap<>(); Map<String, Boolean> result = new HashMap<>();
cached.forEach(e -> result.put(e.getBrand(), e.getRegistered())); cached.forEach(e -> result.put(e.getBrand(), e.getRegistered()));
if (!result.isEmpty()) {
log.info("从全局缓存获取 {} 个品牌数据", result.size());
}
return result; return result;
} }
@Override @Override
public void saveResults(Map<String, Boolean> results) { public void saveResults(Map<String, Boolean> results) {
results.forEach((brand, registered) -> { results.forEach((brand, registered) -> {
if (!repository.existsByBrand(brand)) { BrandTrademarkCacheEntity entity = new BrandTrademarkCacheEntity();
BrandTrademarkCacheEntity entity = new BrandTrademarkCacheEntity(); entity.setBrand(brand);
entity.setBrand(brand); entity.setRegistered(registered);
entity.setRegistered(registered); repository.save(entity);
repository.save(entity);
}
}); });
} }
@@ -50,7 +43,6 @@ public class BrandTrademarkCacheServiceImpl implements BrandTrademarkCacheServic
public void cleanExpired() { public void cleanExpired() {
LocalDateTime cutoffTime = LocalDateTime.now().minusDays(1); LocalDateTime cutoffTime = LocalDateTime.now().minusDays(1);
repository.deleteByCreatedAtBefore(cutoffTime); repository.deleteByCreatedAtBefore(cutoffTime);
log.info("清理1天前的品牌商标缓存");
} }
} }

View File

@@ -2,6 +2,12 @@ package com.tashow.erp.test;
import com.fasterxml.jackson.databind.JsonNode; import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.ObjectMapper; import com.fasterxml.jackson.databind.ObjectMapper;
import com.tashow.erp.ErpClientSbApplication;
import com.tashow.erp.utils.TrademarkCheckUtil;
import org.openqa.selenium.JavascriptExecutor;
import org.openqa.selenium.chrome.ChromeDriver;
import org.springframework.boot.SpringApplication;
import org.springframework.context.ConfigurableApplicationContext;
import org.springframework.http.*; import org.springframework.http.*;
import org.springframework.web.client.RestTemplate; import org.springframework.web.client.RestTemplate;
@@ -15,145 +21,211 @@ public class UsptoApiTest {
private static final ObjectMapper mapper = new ObjectMapper(); private static final ObjectMapper mapper = new ObjectMapper();
public static void main(String[] args) { public static void main(String[] args) {
System.out.println("=== USPTO API 功能测试 ===\n"); System.out.println("=== 商标查询测试使用Spring容器 ===\n");
System.out.println("API Key: " + API_KEY + "\n");
// 测试1通过序列号查询已知可用
testBySerialNumber();
System.out.println("\n" + "=".repeat(60) + "\n");
// 测试2尝试通过品牌名称搜索
testByBrandName();
System.out.println("\n" + "=".repeat(60) + "\n");
// 测试3对比当前实现的tmsearch
testCurrentImplementation();
}
/**
* 测试1通过序列号查询官方TSDR API
*/
private static void testBySerialNumber() {
System.out.println("【测试1】通过序列号查询");
System.out.println("端点: https://tsdrapi.uspto.gov/ts/cd/casestatus/sn88123456/info.json");
// 启动Spring Boot应用上下文
ConfigurableApplicationContext context = null;
try { try {
HttpHeaders headers = new HttpHeaders(); System.out.println("正在启动Spring Boot应用...");
headers.set("USPTO-API-KEY", API_KEY); context = SpringApplication.run(ErpClientSbApplication.class, args);
HttpEntity<String> entity = new HttpEntity<>(headers); System.out.println("Spring Boot应用启动成功\n");
ResponseEntity<String> response = rest.exchange( // 获取TrademarkCheckUtil Bean
"https://tsdrapi.uspto.gov/ts/cd/casestatus/sn88123456/info.json", TrademarkCheckUtil trademarkUtil = context.getBean(TrademarkCheckUtil.class);
HttpMethod.GET, entity, String.class
);
JsonNode json = mapper.readTree(response.getBody()); // 测试单品牌查询(获取详细结果)
String markElement = json.get("trademarks").get(0).get("status").get("markElement").asText(); testSingleBrandWithDetailedResults("Remorlet");
int status = json.get("trademarks").get(0).get("status").get("status").asInt();
String regNumber = json.get("trademarks").get(0).get("status").get("usRegistrationNumber").asText();
System.out.println("✓ 成功!");
System.out.println(" 商标名称: " + markElement);
System.out.println(" 状态码: " + status);
System.out.println(" 注册号: " + (regNumber.isEmpty() ? "未注册" : regNumber));
System.out.println("\n结论: ✅ 可以查询,但必须知道序列号");
} catch (Exception e) { } catch (Exception e) {
System.out.println("失败: " + e.getMessage()); System.err.println("测试失败: " + e.getMessage());
e.printStackTrace();
} finally {
} }
} }
/** /**
* 测试2尝试通过品牌名称搜索 * 标准化品牌名称用于比较(移除特殊字符,转换为小写)
*/ */
private static void testByBrandName() { private static String normalizeBrandName(String name) {
System.out.println("【测试2】尝试通过品牌名称搜索"); if (name == null) return "";
return name.toLowerCase()
.replaceAll("[\\s\\-_\\.]", "") // 移除空格、连字符、下划线、点号
.replaceAll("[^a-z0-9]", ""); // 只保留字母和数字
}
String[] brands = {"Nike", "MYLIFE", "TestBrand123"}; /**
String[] searchUrls = { * 直接执行JavaScript获取详细结果
"https://tsdrapi.uspto.gov/ts/cd/search?q=%s", *
"https://tsdrapi.uspto.gov/search?keyword=%s", * USPTO API返回格式说明:
"https://api.uspto.gov/trademark/search?q=%s", * {
"https://api.uspto.gov/tmsearch/search?q=%s", * "hits": {
}; * "hits": [
* {
* "id": "商标ID",
* "score": 匹配分数,
* "source": {
* "alive": true/false, // 关键字段:是否有效注册
* "wordmark": "品牌名", // 商标名称
* "statusCode": 状态码, // 600=已放弃, 700=已注册
* "statusDescription": "状态描述", // ABANDONED/REGISTERED
* "registrationId": "注册号", // 注册号(如果已注册)
* "registrationDate": "注册日期",
* "abandonDate": "放弃日期",
* "filedDate": "申请日期",
* "goodsAndServices": ["商品服务类别"],
* "internationalClass": ["IC 018"], // 国际分类
* "ownerName": ["所有者名称"],
* "attorney": "代理律师"
* }
* }
* ],
* "totalValue": 总匹配数
* }
* }
*
* 判定逻辑: 只要有任何一个记录的 alive=true就判定为已注册(true)
*/
private static void testSingleBrandWithDetailedResults(String brandName) {
System.out.println("【单品牌测试】直接执行JavaScript获取详细结果");
System.out.println("品牌: " + brandName);
boolean foundSearchApi = false; ChromeDriver driver = null;
try {
System.out.println("=== 开始测试品牌: " + brandName + " ===");
for (String brand : brands) { // 初始化Chrome驱动
System.out.println("\n测试品牌: " + brand); System.out.println("正在初始化Chrome驱动...");
driver = com.tashow.erp.utils.SeleniumUtil.createDriver(false, null);
driver.get("https://tmsearch.uspto.gov/search/search-results");
Thread.sleep(6000); // 等待页面加载
System.out.println("Chrome驱动初始化完成");
for (String urlTemplate : searchUrls) { long startTime = System.currentTimeMillis();
String url = String.format(urlTemplate, brand);
System.out.println(" 尝试: " + url);
try { // 构建JavaScript脚本 - 获取所有结果
HttpHeaders headers = new HttpHeaders(); String script = "const brands = ['" + brandName.replace("'", "\\'") + "'];\n" +
headers.set("USPTO-API-KEY", API_KEY); "const callback = arguments[arguments.length - 1];\n" +
HttpEntity<String> entity = new HttpEntity<>(headers); "Promise.all(brands.map(b => \n" +
" fetch('https://tmsearch.uspto.gov/prod-stage-v1-0-0/tmsearch', {\n" +
" method: 'POST',\n" +
" headers: {'Content-Type': 'application/json'},\n" +
" body: JSON.stringify({\n" +
" query: {bool: {must: [{bool: {should: [\n" +
" {match_phrase: {WM: {query: b, boost: 5}}},\n" +
" {match: {WM: {query: b, boost: 2}}},\n" +
" {match_phrase: {PM: {query: b, boost: 2}}}\n" +
" ]}}]}},\n" +
" size: 100\n" +
" })\n" +
" })\n" +
" .then(r => {\n" +
" if (!r.ok) {\n" +
" return {brand: b, error: 'HTTP ' + r.status + ': ' + r.statusText, allResults: []};\n" +
" }\n" +
" return r.json().then(d => {\n" +
" console.log('API Response:', d);\n" +
" return {\n" +
" brand: b,\n" +
" error: null,\n" +
" totalHits: d?.hits?.total?.value || d?.hits?.totalValue || 0,\n" +
" allResults: d?.hits?.hits || [],\n" +
" rawData: d\n" +
" };\n" +
" });\n" +
" })\n" +
" .catch(e => ({\n" +
" brand: b,\n" +
" error: e.name + ': ' + e.message,\n" +
" allResults: []\n" +
" }))\n" +
")).then(callback);";
ResponseEntity<String> response = rest.exchange(url, HttpMethod.GET, entity, String.class); System.out.println("正在执行JavaScript脚本...");
System.out.println(" ✓✓✓ 成功! 找到品牌搜索API!"); // 执行JavaScript脚本
System.out.println(" 响应: " + response.getBody().substring(0, Math.min(200, response.getBody().length()))); @SuppressWarnings("unchecked")
foundSearchApi = true; java.util.List<java.util.Map<String, Object>> results =
break; (java.util.List<java.util.Map<String, Object>>)
} catch (Exception e) { ((JavascriptExecutor) driver).executeAsyncScript(script);
System.out.println(" ✗ 404/失败");
long duration = System.currentTimeMillis() - startTime;
// 极简输出 - 只返回 true/false 判定结果
boolean isRegistered = false;
if (results != null && !results.isEmpty()) {
java.util.Map<String, Object> result = results.get(0);
// 获取所有匹配的结果
@SuppressWarnings("unchecked")
java.util.List<java.util.Map<String, Object>> allResults =
(java.util.List<java.util.Map<String, Object>>) result.get("allResults");
// 如果allResults为空从rawData中获取
if ((allResults == null || allResults.isEmpty()) && result.get("rawData") != null) {
@SuppressWarnings("unchecked")
java.util.Map<String, Object> rawData = (java.util.Map<String, Object>) result.get("rawData");
@SuppressWarnings("unchecked")
java.util.Map<String, Object> hits = (java.util.Map<String, Object>) rawData.get("hits");
if (hits != null) {
@SuppressWarnings("unchecked")
java.util.List<java.util.Map<String, Object>> hitsArray =
(java.util.List<java.util.Map<String, Object>>) hits.get("hits");
if (hitsArray != null) {
allResults = hitsArray;
}
}
}
// 核心判定逻辑:品牌名称匹配 且 statusCode为686/700 才判定为已注册
String normalizedInputBrand = normalizeBrandName(brandName);
if (allResults != null && !allResults.isEmpty()) {
for (java.util.Map<String, Object> hit : allResults) {
@SuppressWarnings("unchecked")
java.util.Map<String, Object> source = (java.util.Map<String, Object>) hit.get("_source");
if (source == null) {
source = (java.util.Map<String, Object>) hit.get("source");
}
if (source != null) {
String wordmark = (String) source.get("wordmark");
String normalizedWordmark = normalizeBrandName(wordmark);
// 首先检查品牌名称是否匹配
if (normalizedInputBrand.equals(normalizedWordmark)) {
Number statusCodeNum = (Number) source.get("statusCode");
// 只有statusCode为688或700才返回true
if (statusCodeNum != null && (statusCodeNum.intValue() == 688 || statusCodeNum.intValue() == 700)) {
isRegistered = true;
break; // 找到一个符合条件的就够了
}
}
}
}
} }
} }
if (foundSearchApi) break; // 极简输出 - 只显示最终结果
} System.out.println(isRegistered);
if (!foundSearchApi) {
System.out.println("\n结论: ❌ USPTO官方API不支持品牌名称搜索");
}
}
/**
* 测试3当前实现的tmsearch方式
*/
private static void testCurrentImplementation() {
System.out.println("【测试3】当前实现方式tmsearch内部API");
System.out.println("端点: https://tmsearch.uspto.gov/prod-stage-v1-0-0/tmsearch");
String brand = "Nike";
String requestBody = String.format(
"{\"query\":{\"bool\":{\"must\":[{\"bool\":{\"should\":[" +
"{\"match_phrase\":{\"WM\":{\"query\":\"%s\",\"boost\":5}}}," +
"{\"match\":{\"WM\":{\"query\":\"%s\",\"boost\":2}}}," +
"{\"match_phrase\":{\"PM\":{\"query\":\"%s\",\"boost\":2}}}" +
"]}}]}},\"size\":1,\"_source\":[\"alive\"]}",
brand, brand, brand
);
try {
HttpHeaders headers = new HttpHeaders();
headers.setContentType(MediaType.APPLICATION_JSON);
HttpEntity<String> entity = new HttpEntity<>(requestBody, headers);
ResponseEntity<String> response = rest.postForEntity(
"https://tmsearch.uspto.gov/prod-stage-v1-0-0/tmsearch",
entity, String.class
);
JsonNode json = mapper.readTree(response.getBody());
boolean hasResults = json.get("hits").get("hits").size() > 0;
System.out.println("✓ 成功!");
System.out.println(" 品牌: " + brand);
System.out.println(" 找到结果: " + (hasResults ? "" : ""));
if (hasResults) {
boolean alive = json.get("hits").get("hits").get(0).get("_source").get("alive").asBoolean();
System.out.println(" 是否注册: " + (alive ? "已注册" : "未注册"));
}
System.out.println("\n结论: ✅ 支持品牌名称直接搜索(无需序列号)");
System.out.println(" ⚠️ 但这是内部API非官方公开接口");
} catch (Exception e) { } catch (Exception e) {
System.out.println("✗ 失败: " + e.getMessage()); System.err.println("=== 测试失败 ===");
System.err.println("品牌: " + brandName);
System.err.println("错误: " + e.getMessage());
System.err.println("================");
e.printStackTrace();
} finally {
// 确保清理资源
if (driver != null) {
try {
driver.quit();
} catch (Exception e) {
System.err.println("清理Chrome驱动时出错: " + e.getMessage());
}
}
} }
} }
} }

View File

@@ -11,19 +11,10 @@ import java.util.stream.Collectors;
/** /**
* Excel 解析工具类 * Excel 解析工具类
* 统一处理各种平台的 Excel 文件解析需求
*
* @author Claude Code
*/ */
@Slf4j @Slf4j
public class ExcelParseUtil { public class ExcelParseUtil {
/**
* 自动查找表头行索引在前2行中查找
* @param rows Excel所有行
* @param columnName 列名(如"品牌"
* @return 表头行索引,未找到返回-1
*/
private static int findHeaderRow(List<List<Object>> rows, String columnName) { private static int findHeaderRow(List<List<Object>> rows, String columnName) {
for (int r = 0; r < Math.min(2, rows.size()); r++) { for (int r = 0; r < Math.min(2, rows.size()); r++) {
for (Object cell : rows.get(r)) { for (Object cell : rows.get(r)) {
@@ -35,92 +26,37 @@ public class ExcelParseUtil {
return -1; return -1;
} }
/** private static int findColumnIndex(List<Object> headerRow, String columnName) {
* 解析 Excel 文件第一列数据 for (int c = 0; c < headerRow.size(); c++) {
* 通用方法适用于店铺名、ASIN、订单号等标识符解析 if (headerRow.get(c) != null && columnName.equals(headerRow.get(c).toString().replaceAll("\\s+", ""))) {
* return c;
* @param file Excel 文件
* @return 解析出的字符串列表,跳过表头,过滤空值
*/
public static List<String> parseFirstColumn(MultipartFile file) {
List<String> result = new ArrayList<>();
if (file == null || file.isEmpty()) {
log.warn("Excel 文件为空");
return result;
}
try (InputStream in = file.getInputStream()) {
ExcelReader reader = ExcelUtil.getReader(in, 0);
List<List<Object>> rows = reader.read();
for (int i = 1; i < rows.size(); i++) { // 从第2行开始跳过表头
List<Object> row = rows.get(i);
if (row != null && !row.isEmpty()) {
Object cell = row.get(0); // 获取第一列
if (cell != null) {
String value = cell.toString().trim();
if (!value.isEmpty()) {
result.add(value);
}
}
}
} }
log.info("成功解析 Excel 文件: {}, 共解析出 {} 条数据",
file.getOriginalFilename(), result.size());
} catch (Exception e) {
log.error("解析 Excel 文件失败: {}, 文件名: {}", e.getMessage(),
file.getOriginalFilename(), e);
} }
return result; return -1;
}
public static List<String> parseFirstColumn(MultipartFile file) {
return parseColumn(file, 0);
} }
/**
* 解析指定列的数据
*
* @param file Excel 文件
* @param columnIndex 列索引从0开始
* @return 解析出的字符串列表
*/
public static List<String> parseColumn(MultipartFile file, int columnIndex) { public static List<String> parseColumn(MultipartFile file, int columnIndex) {
List<String> result = new ArrayList<>(); List<String> result = new ArrayList<>();
if (file == null || file.isEmpty()) {
log.warn("Excel 文件为空");
return result;
}
try (InputStream in = file.getInputStream()) { try (InputStream in = file.getInputStream()) {
ExcelReader reader = ExcelUtil.getReader(in, 0); ExcelReader reader = ExcelUtil.getReader(in, 0);
List<List<Object>> rows = reader.read(); List<List<Object>> rows = reader.read();
for (int i = 1; i < rows.size(); i++) {
for (int i = 1; i < rows.size(); i++) { // 从第2行开始跳过表头
List<Object> row = rows.get(i); List<Object> row = rows.get(i);
if (row != null && row.size() > columnIndex) { if (row.size() > columnIndex && row.get(columnIndex) != null) {
Object cell = row.get(columnIndex); String value = row.get(columnIndex).toString().trim();
if (cell != null) { if (!value.isEmpty()) result.add(value);
String value = cell.toString().trim();
if (!value.isEmpty()) {
result.add(value);
}
}
} }
} }
log.info("成功解析 Excel 文件第{}列: {}, 共解析出 {} 条数据",
columnIndex + 1, file.getOriginalFilename(), result.size());
} catch (Exception e) { } catch (Exception e) {
log.error("解析 Excel 文件第{}列失败: {}, 文件名: {}", log.error("解析Excel失败", e);
columnIndex + 1, e.getMessage(), file.getOriginalFilename(), e);
} }
return result; return result;
} }
/**
* 根据列名解析数据自动适配第1行或第2行为表头
*/
public static List<String> parseColumnByName(MultipartFile file, String columnName) { public static List<String> parseColumnByName(MultipartFile file, String columnName) {
List<String> result = new ArrayList<>(); List<String> result = new ArrayList<>();
try (InputStream in = file.getInputStream()) { try (InputStream in = file.getInputStream()) {
@@ -131,14 +67,8 @@ public class ExcelParseUtil {
int headerRow = findHeaderRow(rows, columnName); int headerRow = findHeaderRow(rows, columnName);
if (headerRow < 0) return result; if (headerRow < 0) return result;
int colIdx = -1; int colIdx = findColumnIndex(rows.get(headerRow), columnName);
for (int c = 0; c < rows.get(headerRow).size(); c++) { if (colIdx < 0) return result;
if (rows.get(headerRow).get(c) != null &&
columnName.equals(rows.get(headerRow).get(c).toString().replaceAll("\\s+", ""))) {
colIdx = c;
break;
}
}
for (int i = headerRow + 1; i < rows.size(); i++) { for (int i = headerRow + 1; i < rows.size(); i++) {
List<Object> row = rows.get(i); List<Object> row = rows.get(i);
@@ -153,11 +83,6 @@ public class ExcelParseUtil {
return result; return result;
} }
/**
* 读取Excel的完整数据包含表头和所有行自动适配第1行或第2行为表头
* @param file Excel文件
* @return Map包含headers表头列表和rows数据行列表每行是Map
*/
public static Map<String, Object> parseFullExcel(MultipartFile file) { public static Map<String, Object> parseFullExcel(MultipartFile file) {
Map<String, Object> result = new HashMap<>(); Map<String, Object> result = new HashMap<>();
List<String> headers = new ArrayList<>(); List<String> headers = new ArrayList<>();
@@ -166,17 +91,13 @@ public class ExcelParseUtil {
try (InputStream in = file.getInputStream()) { try (InputStream in = file.getInputStream()) {
ExcelReader reader = ExcelUtil.getReader(in, 0); ExcelReader reader = ExcelUtil.getReader(in, 0);
List<List<Object>> allRows = reader.read(); List<List<Object>> allRows = reader.read();
if (allRows.isEmpty()) { if (allRows.isEmpty()) {
log.warn("Excel文件为空");
result.put("headers", headers); result.put("headers", headers);
result.put("rows", rows); result.put("rows", rows);
return result; return result;
} }
int headerRowIndex = Math.max(0, findHeaderRow(allRows, "品牌")); int headerRowIndex = Math.max(0, findHeaderRow(allRows, "品牌"));
log.info("检测到表头行:第{}行", headerRowIndex + 1);
for (Object cell : allRows.get(headerRowIndex)) { for (Object cell : allRows.get(headerRowIndex)) {
headers.add(cell != null ? cell.toString().trim() : ""); headers.add(cell != null ? cell.toString().trim() : "");
} }
@@ -192,89 +113,21 @@ public class ExcelParseUtil {
result.put("headers", headers); result.put("headers", headers);
result.put("rows", rows); result.put("rows", rows);
log.info("解析Excel: {}, 表头{}列, 数据{}行", file.getOriginalFilename(), headers.size(), rows.size());
} catch (Exception e) { } catch (Exception e) {
log.error("解析Excel失败: {}", e.getMessage(), e); log.error("解析Excel失败", e);
} }
return result; return result;
} }
/**
* 根据ASIN列表从Excel中过滤完整行数据自动适配第1行或第2行为表头
* @param file Excel文件
* @param asins ASIN列表
* @return Map包含headers表头和filteredRows过滤后的完整行数据
*/
public static Map<String, Object> filterExcelByAsins(MultipartFile file, List<String> asins) { public static Map<String, Object> filterExcelByAsins(MultipartFile file, List<String> asins) {
Map<String, Object> result = new HashMap<>(); return filterExcelByColumn(file, "ASIN", asins);
List<String> headers = new ArrayList<>();
List<Map<String, Object>> filteredRows = new ArrayList<>();
try (InputStream in = file.getInputStream()) {
ExcelReader reader = ExcelUtil.getReader(in, 0);
List<List<Object>> allRows = reader.read();
if (allRows.isEmpty()) {
result.put("headers", headers);
result.put("filteredRows", filteredRows);
return result;
}
int headerRowIndex = Math.max(0, findHeaderRow(allRows, "ASIN"));
if (headerRowIndex < 0) {
log.warn("未找到'ASIN'列");
result.put("headers", headers);
result.put("filteredRows", filteredRows);
return result;
}
int asinColIndex = -1;
List<Object> headerRow = allRows.get(headerRowIndex);
for (int c = 0; c < headerRow.size(); c++) {
if (headerRow.get(c) != null && "ASIN".equals(headerRow.get(c).toString().replaceAll("\\s+", ""))) {
asinColIndex = c;
break;
}
}
for (Object cell : headerRow) {
headers.add(cell != null ? cell.toString().trim() : "");
}
Set<String> asinSet = asins.stream().map(String::trim).collect(Collectors.toSet());
for (int i = headerRowIndex + 1; i < allRows.size(); i++) {
List<Object> row = allRows.get(i);
if (row.size() > asinColIndex && row.get(asinColIndex) != null
&& asinSet.contains(row.get(asinColIndex).toString().trim())) {
Map<String, Object> rowMap = new HashMap<>();
for (int j = 0; j < Math.min(headers.size(), row.size()); j++) {
rowMap.put(headers.get(j), row.get(j));
}
filteredRows.add(rowMap);
}
}
result.put("headers", headers);
result.put("filteredRows", filteredRows);
log.info("ASIN过滤: {}, {}个ASIN -> {}行数据", file.getOriginalFilename(), asins.size(), filteredRows.size());
} catch (Exception e) {
log.error("ASIN过滤失败: {}", e.getMessage(), e);
}
return result;
} }
/**
* 根据品牌列表从Excel中过滤完整行数据自动适配第1行或第2行为表头
* @param file Excel文件
* @param brands 品牌列表
* @return Map包含headers表头和filteredRows过滤后的完整行数据
*/
public static Map<String, Object> filterExcelByBrands(MultipartFile file, List<String> brands) { public static Map<String, Object> filterExcelByBrands(MultipartFile file, List<String> brands) {
return filterExcelByColumn(file, "品牌", brands);
}
private static Map<String, Object> filterExcelByColumn(MultipartFile file, String columnName, List<String> values) {
Map<String, Object> result = new HashMap<>(); Map<String, Object> result = new HashMap<>();
List<String> headers = new ArrayList<>(); List<String> headers = new ArrayList<>();
List<Map<String, Object>> filteredRows = new ArrayList<>(); List<Map<String, Object>> filteredRows = new ArrayList<>();
@@ -282,40 +135,37 @@ public class ExcelParseUtil {
try (InputStream in = file.getInputStream()) { try (InputStream in = file.getInputStream()) {
ExcelReader reader = ExcelUtil.getReader(in, 0); ExcelReader reader = ExcelUtil.getReader(in, 0);
List<List<Object>> allRows = reader.read(); List<List<Object>> allRows = reader.read();
if (allRows.isEmpty()) { if (allRows.isEmpty()) {
result.put("headers", headers); result.put("headers", headers);
result.put("filteredRows", filteredRows); result.put("filteredRows", filteredRows);
return result; return result;
} }
int headerRowIndex = findHeaderRow(allRows, "品牌"); int headerRowIndex = findHeaderRow(allRows, columnName);
if (headerRowIndex < 0) { if (headerRowIndex < 0) {
log.warn("未找到'品牌'列");
result.put("headers", headers); result.put("headers", headers);
result.put("filteredRows", filteredRows); result.put("filteredRows", filteredRows);
return result; return result;
} }
int brandColIndex = -1;
List<Object> headerRow = allRows.get(headerRowIndex); List<Object> headerRow = allRows.get(headerRowIndex);
for (int c = 0; c < headerRow.size(); c++) { int colIndex = findColumnIndex(headerRow, columnName);
if (headerRow.get(c) != null && "品牌".equals(headerRow.get(c).toString().replaceAll("\\s+", ""))) { if (colIndex < 0) {
brandColIndex = c; result.put("headers", headers);
break; result.put("filteredRows", filteredRows);
} return result;
} }
for (Object cell : headerRow) { for (Object cell : headerRow) {
headers.add(cell != null ? cell.toString().trim() : ""); headers.add(cell != null ? cell.toString().trim() : "");
} }
Set<String> brandSet = brands.stream().map(String::trim).collect(Collectors.toSet()); Set<String> valueSet = values.stream().map(String::trim).collect(Collectors.toSet());
for (int i = headerRowIndex + 1; i < allRows.size(); i++) { for (int i = headerRowIndex + 1; i < allRows.size(); i++) {
List<Object> row = allRows.get(i); List<Object> row = allRows.get(i);
if (row.size() > brandColIndex && row.get(brandColIndex) != null if (row.size() > colIndex && row.get(colIndex) != null
&& brandSet.contains(row.get(brandColIndex).toString().trim())) { && valueSet.contains(row.get(colIndex).toString().trim())) {
Map<String, Object> rowMap = new HashMap<>(); Map<String, Object> rowMap = new HashMap<>();
for (int j = 0; j < Math.min(headers.size(), row.size()); j++) { for (int j = 0; j < Math.min(headers.size(), row.size()); j++) {
rowMap.put(headers.get(j), row.get(j)); rowMap.put(headers.get(j), row.get(j));
@@ -326,12 +176,9 @@ public class ExcelParseUtil {
result.put("headers", headers); result.put("headers", headers);
result.put("filteredRows", filteredRows); result.put("filteredRows", filteredRows);
log.info("品牌过滤: {}, {}个品牌 -> {}行数据", file.getOriginalFilename(), brands.size(), filteredRows.size());
} catch (Exception e) { } catch (Exception e) {
log.error("品牌过滤失败: {}", e.getMessage(), e); log.error("Excel过滤失败", e);
} }
return result; return result;
} }
} }

View File

@@ -1,10 +1,12 @@
package com.tashow.erp.utils; package com.tashow.erp.utils;
import com.tashow.erp.service.BrandTrademarkCacheService; import com.tashow.erp.service.BrandTrademarkCacheService;
import jakarta.annotation.PreDestroy; import jakarta.annotation.PreDestroy;
import org.openqa.selenium.JavascriptExecutor; import org.openqa.selenium.JavascriptExecutor;
import org.openqa.selenium.chrome.ChromeDriver; import org.openqa.selenium.chrome.ChromeDriver;
import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component; import org.springframework.stereotype.Component;
import java.util.*; import java.util.*;
/** /**
@@ -17,133 +19,82 @@ public class TrademarkCheckUtil {
private ProxyPool proxyPool; private ProxyPool proxyPool;
@Autowired @Autowired
private BrandTrademarkCacheService cacheService; private BrandTrademarkCacheService cacheService;
private ChromeDriver driver; public ChromeDriver driver;
private final int maxRetries = 3;
private String normalize(String name) {
return name.toLowerCase().replaceAll("[^a-z0-9]", "");
}
private synchronized void ensureInit() { private synchronized void ensureInit() {
if (driver == null) { if (driver == null) {
for (int i = 0; i < 5; i++) { driver = SeleniumUtil.createDriver(true, proxyPool.getProxy());
try { driver.get("https://tmsearch.uspto.gov/search/search-results");
driver = SeleniumUtil.createDriver(true, proxyPool.getProxy()); try { Thread.sleep(6000); } catch (InterruptedException ignored) {}
driver.get("https://tmsearch.uspto.gov/search/search-results");
Thread.sleep(6000);
return; // 成功则返回
} catch (Exception e) {
System.err.println("初始化失败(尝试" + (i+1) + "/5: " + e.getMessage());
if (driver != null) {
try { driver.quit(); } catch (Exception ex) {}
driver = null;
}
if (i == 2) throw new RuntimeException("初始化失败已重试3次", e);
}
}
} }
} }
public synchronized Map<String, Boolean> batchCheck(List<String> brands, Map<String, Boolean> alreadyQueried) { public synchronized Map<String, Boolean> batchCheck(List<String> brands, Map<String, Boolean> alreadyQueried) {
Map<String, Boolean> resultMap = new HashMap<>(); Map<String, Boolean> resultMap = new HashMap<>();
int maxRetries = 5;
for (String brand : brands) { for (String brand : brands) {
int retryCount = 0; int retryCount = 0;
boolean success = false; boolean success = false;
while (retryCount < 5 && !success) {
while (retryCount < maxRetries && !success) {
try { try {
ensureInit(); 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");
String script = "const brands = ['" + brand.replace("'", "\\'") + "'];\n" + if (error != null && (error.contains("HTTP 403") || error.contains("Failed to fetch") || error.contains("NetworkError") || error.contains("TypeError") || error.contains("script timeout"))) {
"const callback = arguments[arguments.length - 1];\n" + System.err.println(brand + " 查询失败(" + (retryCount + 1) + "/3): " + error + ",切换代理...");
"Promise.all(brands.map(b => \n" + if (driver != null) driver.quit();
" fetch('https://tmsearch.uspto.gov/prod-stage-v1-0-0/tmsearch', {\n" +
" method: 'POST',\n" +
" headers: {'Content-Type': 'application/json'},\n" +
" body: JSON.stringify({\n" +
" query: {bool: {must: [{bool: {should: [\n" +
" {match_phrase: {WM: {query: b, boost: 5}}},\n" +
" {match: {WM: {query: b, boost: 2}}},\n" +
" {match_phrase: {PM: {query: b, boost: 2}}}\n" +
" ]}}]}},\n" +
" size: 1, _source: ['alive']\n" +
" })\n" +
" })\n" +
" .then(r => {\n" +
" if (!r.ok) {\n" +
" return {brand: b, alive: false, error: 'HTTP ' + r.status + ': ' + r.statusText};\n" +
" }\n" +
" return r.json().then(d => ({\n" +
" brand: b,\n" +
" alive: d?.hits?.hits?.[0]?.source?.alive || false,\n" +
" error: null\n" +
" }));\n" +
" })\n" +
" .catch(e => ({\n" +
" brand: b,\n" +
" alive: false,\n" +
" error: e.name + ': ' + e.message\n" +
" }))\n" +
")).then(callback);";
@SuppressWarnings("unchecked")
List<Map<String, Object>> results = (List<Map<String, Object>>)
((JavascriptExecutor) driver).executeAsyncScript(script);
Map<String, Object> item = results.get(0);
String error = (String) item.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) + "/" + maxRetries + "): " + error + ",切换代理...");
// 切换代理
try { driver.quit(); } catch (Exception e) {}
driver = null; driver = null;
retryCount++; retryCount++;
continue; continue;
} }
// 成功或非网络错误
if (error == null) { if (error == null) {
Boolean alive = (Boolean) item.get("alive"); @SuppressWarnings("unchecked") List<Map<String, Object>> hits = (List<Map<String, Object>>) result.get("hits");
resultMap.put(brand, alive); String input = normalize(brand);
System.out.println(brand + " -> " + (alive ? "✓ 已注册" : "✗ 未注册")); 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; success = true;
} else { } else {
System.err.println(brand + " -> [查询失败: " + error + "]"); System.err.println(brand + " -> [查询失败: " + error + "]");
resultMap.put(brand, false); // 失败也记录为未注册 resultMap.put(brand, true);
success = true; success = true;
} }
} catch (Exception e) { } catch (Exception e) {
System.err.println(brand + " 查询异常(" + (retryCount + 1) + "/" + maxRetries + "): " + e.getMessage()); System.err.println(brand + " 查询异常(" + (retryCount + 1) + "/3): " + e.getMessage());
try { driver.quit(); } catch (Exception ex) {} if (driver != null) driver.quit();
driver = null; driver = null;
retryCount++; retryCount++;
} }
} }
if (!success) { if (!success) {
System.err.println(brand + " -> [查询失败: 已重试" + maxRetries + "次]"); System.err.println(brand + " -> [查询失败: 已重试3次]");
resultMap.put(brand, false); // 失败也记录为未注册 resultMap.put(brand, true);
} }
} }
return resultMap; return resultMap;
} }
public synchronized void closeDriver() {
if (driver != null) {
try { driver.quit(); } catch (Exception e) {}
driver = null;
}
}
@PreDestroy @PreDestroy
public void cleanup() { public void cleanup() {
closeDriver(); if (driver != null) driver.quit();
} }
} }