feat(trademark):优化商标查询功能和Excel解析逻辑
- 重构品牌商标缓存服务,移除冗余的日志记录和存在检查- 简化Excel解析工具类,提取公共方法并优化列索引查找逻辑 - 增强Electron客户端开发模式下的后端启动控制能力 - 改进商标筛查面板的用户体验和数据处理流程-优化商标查询工具类,提高查询准确性和稳定性 - 调整商标控制器接口参数校验逻辑和资源清理机制 - 更新USPTO API测试用例以支持Spring容器环境运行
This commit is contained in:
@@ -61,9 +61,10 @@ public class TrademarkController {
|
||||
String taskId = (String) request.get("taskId");
|
||||
try {
|
||||
List<String> list = brands.stream()
|
||||
.filter(b -> b != null && !b.trim().isEmpty())
|
||||
.filter(b -> !b.trim().isEmpty())
|
||||
.map(String::trim)
|
||||
.collect(Collectors.toList());
|
||||
|
||||
long start = System.currentTimeMillis();
|
||||
// 1. 先从全局缓存获取
|
||||
Map<String, Boolean> cached = cacheService.getCached(list);
|
||||
@@ -74,8 +75,7 @@ public class TrademarkController {
|
||||
Map<String, Boolean> queried = new HashMap<>();
|
||||
if (!toQuery.isEmpty()) {
|
||||
for (int i = 0; i < toQuery.size(); i++) {
|
||||
// 检查任务是否被取消值
|
||||
if (taskId != null && cancelMap.getOrDefault(taskId, false)) {
|
||||
if (cancelMap.getOrDefault(taskId, false)) {
|
||||
logger.info("任务 {} 已被取消,停止查询", taskId);
|
||||
break;
|
||||
}
|
||||
@@ -86,15 +86,10 @@ public class TrademarkController {
|
||||
Map<String, Boolean> results = util.batchCheck(Collections.singletonList(brand), queried);
|
||||
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. 合并缓存和新查询结果
|
||||
@@ -132,16 +127,11 @@ public class TrademarkController {
|
||||
logger.info("完成: 共{}个,成功查询{}个(已注册{}个,未注册{}个),查询失败{}个,耗时{}秒",
|
||||
list.size(), checkedCount, registeredCount, unregistered.size(), failedCount, t);
|
||||
|
||||
// 30秒后清理进度和取消标志
|
||||
if (taskId != null) {
|
||||
String finalTaskId = taskId;
|
||||
new Thread(() -> {
|
||||
try {
|
||||
Thread.sleep(30000);
|
||||
} catch (InterruptedException ignored) {
|
||||
}
|
||||
progressMap.remove(finalTaskId);
|
||||
cancelMap.remove(finalTaskId);
|
||||
try { Thread.sleep(30000); } catch (InterruptedException ignored) {}
|
||||
progressMap.remove(taskId);
|
||||
cancelMap.remove(taskId);
|
||||
}).start();
|
||||
}
|
||||
|
||||
@@ -150,7 +140,7 @@ public class TrademarkController {
|
||||
logger.error("筛查失败", e);
|
||||
return JsonData.buildError("筛查失败: " + e.getMessage());
|
||||
} finally {
|
||||
util.closeDriver();
|
||||
if (util != null && util.driver != null) util.driver.quit();
|
||||
cacheService.cleanExpired();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -23,25 +23,18 @@ public class BrandTrademarkCacheServiceImpl implements BrandTrademarkCacheServic
|
||||
public Map<String, Boolean> getCached(List<String> brands) {
|
||||
LocalDateTime cutoffTime = LocalDateTime.now().minusDays(1);
|
||||
List<BrandTrademarkCacheEntity> cached = repository.findByBrandInAndCreatedAtAfter(brands, cutoffTime);
|
||||
|
||||
Map<String, Boolean> result = new HashMap<>();
|
||||
cached.forEach(e -> result.put(e.getBrand(), e.getRegistered()));
|
||||
|
||||
if (!result.isEmpty()) {
|
||||
log.info("从全局缓存获取 {} 个品牌数据", result.size());
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void saveResults(Map<String, Boolean> results) {
|
||||
results.forEach((brand, registered) -> {
|
||||
if (!repository.existsByBrand(brand)) {
|
||||
BrandTrademarkCacheEntity entity = new BrandTrademarkCacheEntity();
|
||||
entity.setBrand(brand);
|
||||
entity.setRegistered(registered);
|
||||
repository.save(entity);
|
||||
}
|
||||
BrandTrademarkCacheEntity entity = new BrandTrademarkCacheEntity();
|
||||
entity.setBrand(brand);
|
||||
entity.setRegistered(registered);
|
||||
repository.save(entity);
|
||||
});
|
||||
}
|
||||
|
||||
@@ -50,7 +43,6 @@ public class BrandTrademarkCacheServiceImpl implements BrandTrademarkCacheServic
|
||||
public void cleanExpired() {
|
||||
LocalDateTime cutoffTime = LocalDateTime.now().minusDays(1);
|
||||
repository.deleteByCreatedAtBefore(cutoffTime);
|
||||
log.info("清理1天前的品牌商标缓存");
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -2,6 +2,12 @@ package com.tashow.erp.test;
|
||||
|
||||
import com.fasterxml.jackson.databind.JsonNode;
|
||||
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.web.client.RestTemplate;
|
||||
|
||||
@@ -15,145 +21,211 @@ public class UsptoApiTest {
|
||||
private static final ObjectMapper mapper = new ObjectMapper();
|
||||
|
||||
public static void main(String[] args) {
|
||||
System.out.println("=== USPTO API 功能测试 ===\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");
|
||||
System.out.println("=== 商标查询测试(使用Spring容器) ===\n");
|
||||
|
||||
// 启动Spring Boot应用上下文
|
||||
ConfigurableApplicationContext context = null;
|
||||
try {
|
||||
HttpHeaders headers = new HttpHeaders();
|
||||
headers.set("USPTO-API-KEY", API_KEY);
|
||||
HttpEntity<String> entity = new HttpEntity<>(headers);
|
||||
System.out.println("正在启动Spring Boot应用...");
|
||||
context = SpringApplication.run(ErpClientSbApplication.class, args);
|
||||
System.out.println("Spring Boot应用启动成功!\n");
|
||||
|
||||
ResponseEntity<String> response = rest.exchange(
|
||||
"https://tsdrapi.uspto.gov/ts/cd/casestatus/sn88123456/info.json",
|
||||
HttpMethod.GET, entity, String.class
|
||||
);
|
||||
// 获取TrademarkCheckUtil Bean
|
||||
TrademarkCheckUtil trademarkUtil = context.getBean(TrademarkCheckUtil.class);
|
||||
|
||||
JsonNode json = mapper.readTree(response.getBody());
|
||||
String markElement = json.get("trademarks").get(0).get("status").get("markElement").asText();
|
||||
int status = json.get("trademarks").get(0).get("status").get("status").asInt();
|
||||
String regNumber = json.get("trademarks").get(0).get("status").get("usRegistrationNumber").asText();
|
||||
// 测试单品牌查询(获取详细结果)
|
||||
testSingleBrandWithDetailedResults("Remorlet");
|
||||
|
||||
System.out.println("✓ 成功!");
|
||||
System.out.println(" 商标名称: " + markElement);
|
||||
System.out.println(" 状态码: " + status);
|
||||
System.out.println(" 注册号: " + (regNumber.isEmpty() ? "未注册" : regNumber));
|
||||
System.out.println("\n结论: ✅ 可以查询,但必须知道序列号");
|
||||
} catch (Exception e) {
|
||||
System.out.println("✗ 失败: " + e.getMessage());
|
||||
System.err.println("测试失败: " + e.getMessage());
|
||||
e.printStackTrace();
|
||||
} finally {
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* 测试2:尝试通过品牌名称搜索
|
||||
* 标准化品牌名称用于比较(移除特殊字符,转换为小写)
|
||||
*/
|
||||
private static void testByBrandName() {
|
||||
System.out.println("【测试2】尝试通过品牌名称搜索");
|
||||
|
||||
String[] brands = {"Nike", "MYLIFE", "TestBrand123"};
|
||||
String[] searchUrls = {
|
||||
"https://tsdrapi.uspto.gov/ts/cd/search?q=%s",
|
||||
"https://tsdrapi.uspto.gov/search?keyword=%s",
|
||||
"https://api.uspto.gov/trademark/search?q=%s",
|
||||
"https://api.uspto.gov/tmsearch/search?q=%s",
|
||||
};
|
||||
|
||||
boolean foundSearchApi = false;
|
||||
|
||||
for (String brand : brands) {
|
||||
System.out.println("\n测试品牌: " + brand);
|
||||
private static String normalizeBrandName(String name) {
|
||||
if (name == null) return "";
|
||||
return name.toLowerCase()
|
||||
.replaceAll("[\\s\\-_\\.]", "") // 移除空格、连字符、下划线、点号
|
||||
.replaceAll("[^a-z0-9]", ""); // 只保留字母和数字
|
||||
}
|
||||
|
||||
/**
|
||||
* 直接执行JavaScript获取详细结果
|
||||
*
|
||||
* USPTO API返回格式说明:
|
||||
* {
|
||||
* "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);
|
||||
|
||||
ChromeDriver driver = null;
|
||||
try {
|
||||
System.out.println("=== 开始测试品牌: " + brandName + " ===");
|
||||
|
||||
for (String urlTemplate : searchUrls) {
|
||||
String url = String.format(urlTemplate, brand);
|
||||
System.out.println(" 尝试: " + url);
|
||||
// 初始化Chrome驱动
|
||||
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驱动初始化完成");
|
||||
|
||||
long startTime = System.currentTimeMillis();
|
||||
|
||||
// 构建JavaScript脚本 - 获取所有结果
|
||||
String script = "const brands = ['" + brandName.replace("'", "\\'") + "'];\n" +
|
||||
"const callback = arguments[arguments.length - 1];\n" +
|
||||
"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);";
|
||||
|
||||
System.out.println("正在执行JavaScript脚本...");
|
||||
|
||||
// 执行JavaScript脚本
|
||||
@SuppressWarnings("unchecked")
|
||||
java.util.List<java.util.Map<String, Object>> results =
|
||||
(java.util.List<java.util.Map<String, Object>>)
|
||||
((JavascriptExecutor) driver).executeAsyncScript(script);
|
||||
|
||||
long duration = System.currentTimeMillis() - startTime;
|
||||
|
||||
// 极简输出 - 只返回 true/false 判定结果
|
||||
boolean isRegistered = false;
|
||||
|
||||
if (results != null && !results.isEmpty()) {
|
||||
java.util.Map<String, Object> result = results.get(0);
|
||||
|
||||
try {
|
||||
HttpHeaders headers = new HttpHeaders();
|
||||
headers.set("USPTO-API-KEY", API_KEY);
|
||||
HttpEntity<String> entity = new HttpEntity<>(headers);
|
||||
|
||||
ResponseEntity<String> response = rest.exchange(url, HttpMethod.GET, entity, String.class);
|
||||
|
||||
System.out.println(" ✓✓✓ 成功! 找到品牌搜索API!");
|
||||
System.out.println(" 响应: " + response.getBody().substring(0, Math.min(200, response.getBody().length())));
|
||||
foundSearchApi = true;
|
||||
break;
|
||||
} catch (Exception e) {
|
||||
System.out.println(" ✗ 404/失败");
|
||||
// 获取所有匹配的结果
|
||||
@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;
|
||||
}
|
||||
|
||||
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);
|
||||
// 极简输出 - 只显示最终结果
|
||||
System.out.println(isRegistered);
|
||||
|
||||
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) {
|
||||
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());
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -11,19 +11,10 @@ import java.util.stream.Collectors;
|
||||
|
||||
/**
|
||||
* Excel 解析工具类
|
||||
* 统一处理各种平台的 Excel 文件解析需求
|
||||
*
|
||||
* @author Claude Code
|
||||
*/
|
||||
@Slf4j
|
||||
public class ExcelParseUtil {
|
||||
|
||||
/**
|
||||
* 自动查找表头行索引(在前2行中查找)
|
||||
* @param rows Excel所有行
|
||||
* @param columnName 列名(如"品牌")
|
||||
* @return 表头行索引,未找到返回-1
|
||||
*/
|
||||
private static int findHeaderRow(List<List<Object>> rows, String columnName) {
|
||||
for (int r = 0; r < Math.min(2, rows.size()); r++) {
|
||||
for (Object cell : rows.get(r)) {
|
||||
@@ -34,93 +25,38 @@ public class ExcelParseUtil {
|
||||
}
|
||||
return -1;
|
||||
}
|
||||
|
||||
/**
|
||||
* 解析 Excel 文件第一列数据
|
||||
* 通用方法,适用于店铺名、ASIN、订单号等标识符解析
|
||||
*
|
||||
* @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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static int findColumnIndex(List<Object> headerRow, String columnName) {
|
||||
for (int c = 0; c < headerRow.size(); c++) {
|
||||
if (headerRow.get(c) != null && columnName.equals(headerRow.get(c).toString().replaceAll("\\s+", ""))) {
|
||||
return c;
|
||||
}
|
||||
|
||||
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) {
|
||||
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行开始,跳过表头
|
||||
for (int i = 1; i < rows.size(); i++) {
|
||||
List<Object> row = rows.get(i);
|
||||
if (row != null && row.size() > columnIndex) {
|
||||
Object cell = row.get(columnIndex);
|
||||
if (cell != null) {
|
||||
String value = cell.toString().trim();
|
||||
if (!value.isEmpty()) {
|
||||
result.add(value);
|
||||
}
|
||||
}
|
||||
if (row.size() > columnIndex && row.get(columnIndex) != null) {
|
||||
String value = row.get(columnIndex).toString().trim();
|
||||
if (!value.isEmpty()) result.add(value);
|
||||
}
|
||||
}
|
||||
|
||||
log.info("成功解析 Excel 文件第{}列: {}, 共解析出 {} 条数据",
|
||||
columnIndex + 1, file.getOriginalFilename(), result.size());
|
||||
|
||||
} catch (Exception e) {
|
||||
log.error("解析 Excel 文件第{}列失败: {}, 文件名: {}",
|
||||
columnIndex + 1, e.getMessage(), file.getOriginalFilename(), e);
|
||||
log.error("解析Excel失败", e);
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
/**
|
||||
* 根据列名解析数据(自动适配第1行或第2行为表头)
|
||||
*/
|
||||
public static List<String> parseColumnByName(MultipartFile file, String columnName) {
|
||||
List<String> result = new ArrayList<>();
|
||||
try (InputStream in = file.getInputStream()) {
|
||||
@@ -131,14 +67,8 @@ public class ExcelParseUtil {
|
||||
int headerRow = findHeaderRow(rows, columnName);
|
||||
if (headerRow < 0) return result;
|
||||
|
||||
int colIdx = -1;
|
||||
for (int c = 0; c < rows.get(headerRow).size(); c++) {
|
||||
if (rows.get(headerRow).get(c) != null &&
|
||||
columnName.equals(rows.get(headerRow).get(c).toString().replaceAll("\\s+", ""))) {
|
||||
colIdx = c;
|
||||
break;
|
||||
}
|
||||
}
|
||||
int colIdx = findColumnIndex(rows.get(headerRow), columnName);
|
||||
if (colIdx < 0) return result;
|
||||
|
||||
for (int i = headerRow + 1; i < rows.size(); i++) {
|
||||
List<Object> row = rows.get(i);
|
||||
@@ -153,11 +83,6 @@ public class ExcelParseUtil {
|
||||
return result;
|
||||
}
|
||||
|
||||
/**
|
||||
* 读取Excel的完整数据(包含表头和所有行,自动适配第1行或第2行为表头)
|
||||
* @param file Excel文件
|
||||
* @return Map包含headers(表头列表)和rows(数据行列表,每行是Map)
|
||||
*/
|
||||
public static Map<String, Object> parseFullExcel(MultipartFile file) {
|
||||
Map<String, Object> result = new HashMap<>();
|
||||
List<String> headers = new ArrayList<>();
|
||||
@@ -166,17 +91,13 @@ public class ExcelParseUtil {
|
||||
try (InputStream in = file.getInputStream()) {
|
||||
ExcelReader reader = ExcelUtil.getReader(in, 0);
|
||||
List<List<Object>> allRows = reader.read();
|
||||
|
||||
if (allRows.isEmpty()) {
|
||||
log.warn("Excel文件为空");
|
||||
result.put("headers", headers);
|
||||
result.put("rows", rows);
|
||||
return result;
|
||||
}
|
||||
|
||||
int headerRowIndex = Math.max(0, findHeaderRow(allRows, "品牌"));
|
||||
log.info("检测到表头行:第{}行", headerRowIndex + 1);
|
||||
|
||||
for (Object cell : allRows.get(headerRowIndex)) {
|
||||
headers.add(cell != null ? cell.toString().trim() : "");
|
||||
}
|
||||
@@ -192,89 +113,21 @@ public class ExcelParseUtil {
|
||||
|
||||
result.put("headers", headers);
|
||||
result.put("rows", rows);
|
||||
log.info("解析Excel: {}, 表头{}列, 数据{}行", file.getOriginalFilename(), headers.size(), rows.size());
|
||||
|
||||
} catch (Exception e) {
|
||||
log.error("解析Excel失败: {}", e.getMessage(), e);
|
||||
log.error("解析Excel失败", e);
|
||||
}
|
||||
|
||||
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) {
|
||||
Map<String, Object> result = new HashMap<>();
|
||||
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;
|
||||
return filterExcelByColumn(file, "ASIN", asins);
|
||||
}
|
||||
|
||||
/**
|
||||
* 根据品牌列表从Excel中过滤完整行数据(自动适配第1行或第2行为表头)
|
||||
* @param file Excel文件
|
||||
* @param brands 品牌列表
|
||||
* @return Map包含headers(表头)和filteredRows(过滤后的完整行数据)
|
||||
*/
|
||||
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<>();
|
||||
List<String> headers = new ArrayList<>();
|
||||
List<Map<String, Object>> filteredRows = new ArrayList<>();
|
||||
@@ -282,40 +135,37 @@ public class ExcelParseUtil {
|
||||
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 = findHeaderRow(allRows, "品牌");
|
||||
int headerRowIndex = findHeaderRow(allRows, columnName);
|
||||
if (headerRowIndex < 0) {
|
||||
log.warn("未找到'品牌'列");
|
||||
result.put("headers", headers);
|
||||
result.put("filteredRows", filteredRows);
|
||||
return result;
|
||||
}
|
||||
|
||||
int brandColIndex = -1;
|
||||
List<Object> headerRow = allRows.get(headerRowIndex);
|
||||
for (int c = 0; c < headerRow.size(); c++) {
|
||||
if (headerRow.get(c) != null && "品牌".equals(headerRow.get(c).toString().replaceAll("\\s+", ""))) {
|
||||
brandColIndex = c;
|
||||
break;
|
||||
}
|
||||
int colIndex = findColumnIndex(headerRow, columnName);
|
||||
if (colIndex < 0) {
|
||||
result.put("headers", headers);
|
||||
result.put("filteredRows", filteredRows);
|
||||
return result;
|
||||
}
|
||||
|
||||
for (Object cell : headerRow) {
|
||||
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++) {
|
||||
List<Object> row = allRows.get(i);
|
||||
if (row.size() > brandColIndex && row.get(brandColIndex) != null
|
||||
&& brandSet.contains(row.get(brandColIndex).toString().trim())) {
|
||||
if (row.size() > colIndex && row.get(colIndex) != null
|
||||
&& valueSet.contains(row.get(colIndex).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));
|
||||
@@ -326,12 +176,9 @@ public class ExcelParseUtil {
|
||||
|
||||
result.put("headers", headers);
|
||||
result.put("filteredRows", filteredRows);
|
||||
log.info("品牌过滤: {}, {}个品牌 -> {}行数据", file.getOriginalFilename(), brands.size(), filteredRows.size());
|
||||
|
||||
} catch (Exception e) {
|
||||
log.error("品牌过滤失败: {}", e.getMessage(), e);
|
||||
log.error("Excel过滤失败", e);
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
}
|
||||
@@ -1,10 +1,12 @@
|
||||
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.*;
|
||||
|
||||
/**
|
||||
@@ -17,133 +19,82 @@ public class TrademarkCheckUtil {
|
||||
private ProxyPool proxyPool;
|
||||
@Autowired
|
||||
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() {
|
||||
if (driver == null) {
|
||||
for (int i = 0; i < 5; i++) {
|
||||
try {
|
||||
driver = SeleniumUtil.createDriver(true, proxyPool.getProxy());
|
||||
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);
|
||||
}
|
||||
}
|
||||
driver = SeleniumUtil.createDriver(true, proxyPool.getProxy());
|
||||
driver.get("https://tmsearch.uspto.gov/search/search-results");
|
||||
try { Thread.sleep(6000); } catch (InterruptedException ignored) {}
|
||||
}
|
||||
}
|
||||
|
||||
public synchronized Map<String, Boolean> batchCheck(List<String> brands, Map<String, Boolean> alreadyQueried) {
|
||||
Map<String, Boolean> resultMap = new HashMap<>();
|
||||
int maxRetries = 5;
|
||||
|
||||
for (String brand : brands) {
|
||||
int retryCount = 0;
|
||||
boolean success = false;
|
||||
|
||||
while (retryCount < maxRetries && !success) {
|
||||
while (retryCount < 5 && !success) {
|
||||
try {
|
||||
ensureInit();
|
||||
|
||||
String script = "const brands = ['" + brand.replace("'", "\\'") + "'];\n" +
|
||||
"const callback = arguments[arguments.length - 1];\n" +
|
||||
"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: 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) {}
|
||||
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) {
|
||||
Boolean alive = (Boolean) item.get("alive");
|
||||
resultMap.put(brand, alive);
|
||||
System.out.println(brand + " -> " + (alive ? "✓ 已注册" : "✗ 未注册"));
|
||||
@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, false); // 失败也记录为未注册
|
||||
resultMap.put(brand, true);
|
||||
success = true;
|
||||
}
|
||||
|
||||
} catch (Exception e) {
|
||||
System.err.println(brand + " 查询异常(" + (retryCount + 1) + "/" + maxRetries + "): " + e.getMessage());
|
||||
try { driver.quit(); } catch (Exception ex) {}
|
||||
System.err.println(brand + " 查询异常(" + (retryCount + 1) + "/3): " + e.getMessage());
|
||||
if (driver != null) driver.quit();
|
||||
driver = null;
|
||||
retryCount++;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
if (!success) {
|
||||
System.err.println(brand + " -> [查询失败: 已重试" + maxRetries + "次]");
|
||||
resultMap.put(brand, false); // 失败也记录为未注册
|
||||
System.err.println(brand + " -> [查询失败: 已重试3次]");
|
||||
resultMap.put(brand, true);
|
||||
}
|
||||
}
|
||||
|
||||
return resultMap;
|
||||
}
|
||||
|
||||
public synchronized void closeDriver() {
|
||||
if (driver != null) {
|
||||
try { driver.quit(); } catch (Exception e) {}
|
||||
driver = null;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@PreDestroy
|
||||
public void cleanup() {
|
||||
closeDriver();
|
||||
if (driver != null) driver.quit();
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user