feat(trademark): 实现商标筛查功能并优化相关配置

- 新增商标筛查进度展示界面与交互逻辑
- 实现产品、品牌及平台跟卖许可的分项任务进度追踪
- 添加商标数据导出与任务重试、取消功能
- 调整Redis连接池配置以提升并发性能
- 禁用ChromeDriver预加载,改为按需启动以节省资源- 支持品牌商标远程筛查接口调用与结果解析
- 增加Hutool工具库依赖用于简化IO与Excel处理- 更新USPTO商标查询脚本实现自动化检测
- 修改Ruoyi后台Redis依赖版本并添加集群心跳配置- 切换本地开发环境API地址指向内网测试服务器
This commit is contained in:
2025-11-04 15:39:15 +08:00
parent c9874f1786
commit a62d7b6147
22 changed files with 2063 additions and 168 deletions

View File

@@ -11,7 +11,7 @@ import org.springframework.core.annotation.Order;
/**
* ChromeDriver 配置类
* 启动时后台预加载驱动,提供全局单例 Bean
* 已禁用预加载,由 TrademarkCheckUtil 按需创建
*/
@Slf4j
@Configuration
@@ -22,21 +22,26 @@ public class ChromeDriverPreloader implements ApplicationRunner {
@Override
public void run(ApplicationArguments args) {
new Thread(() -> {
globalDriver = SeleniumUtil.createDriver(true);
log.info("ChromeDriver 预加载完成");
}, "ChromeDriver-Preloader").start();
// 不再预加载,节省资源
log.info("ChromeDriver 配置已加载(按需启动)");
}
@Bean
public ChromeDriver chromeDriver() {
if (globalDriver == null) globalDriver = SeleniumUtil.createDriver(true);
// 为兼容性保留 Bean但不自动创建
if (globalDriver == null) globalDriver = SeleniumUtil.createDriver(false);
return globalDriver;
}
@PreDestroy
public void cleanup() {
globalDriver.quit();
if (globalDriver != null) {
try {
globalDriver.quit();
} catch (Exception e) {
log.error("关闭ChromeDriver失败", e);
}
}
}
}

View File

@@ -0,0 +1,90 @@
package com.tashow.erp.controller;
import com.tashow.erp.utils.JsonData;
import com.tashow.erp.utils.LoggerUtil;
import com.tashow.erp.utils.TrademarkCheckUtil;
import org.slf4j.Logger;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.*;
import java.util.*;
import java.util.concurrent.ThreadLocalRandom;
import java.util.stream.Collectors;
/**
* 商标检查控制器 - 极速版(浏览器内并发)
*/
@RestController
@RequestMapping("/api/trademark")
public class TrademarkController {
private static final Logger logger = LoggerUtil.getLogger(TrademarkController.class);
@Autowired
private TrademarkCheckUtil util;
/**
* 批量品牌商标筛查(浏览器内并发,极速版)
*/
@PostMapping("/brandCheck")
public JsonData brandCheck(@RequestBody List<String> brands) {
try {
List<String> list = brands.stream()
.filter(b -> b != null && !b.trim().isEmpty())
.map(String::trim)
.distinct()
.collect(Collectors.toList());
logger.info("开始检查 {}个品牌", list.size());
long start = System.currentTimeMillis();
// 串行查询(不加延迟)
List<Map<String, Object>> unregistered = new ArrayList<>();
int checkedCount = 0;
int registeredCount = 0;
for (int i = 0; i < list.size(); i++) {
String brand = list.get(i);
logger.info("处理第 {} 个: {}", i + 1, brand);
Map<String, Boolean> results = util.batchCheck(Collections.singletonList(brand));
results.forEach((b, isReg) -> {
if (!isReg) {
Map<String, Object> m = new HashMap<>();
m.put("brand", b);
m.put("status", "未注册");
unregistered.add(m);
}
});
// 统计成功查询的数量
if (!results.isEmpty()) {
checkedCount++;
if (results.values().iterator().next()) {
registeredCount++;
}
}
}
long t = (System.currentTimeMillis() - start) / 1000;
int failedCount = list.size() - checkedCount;
Map<String, Object> res = new HashMap<>();
res.put("total", list.size());
res.put("checked", checkedCount);
res.put("registered", registeredCount);
res.put("unregistered", unregistered.size());
res.put("failed", failedCount);
res.put("data", unregistered);
res.put("duration", t + "");
logger.info("完成: 共{}个,成功查询{}个(已注册{}个,未注册{}个),查询失败{}个,耗时{}秒",
list.size(), checkedCount, registeredCount, unregistered.size(), failedCount, t);
return JsonData.buildSuccess(res);
} catch (Exception e) {
logger.error("筛查失败", e);
return JsonData.buildError("筛查失败: " + e.getMessage());
} finally {
// 采集完成或失败后关闭浏览器
util.closeDriver();
}
}
}

View File

@@ -0,0 +1,160 @@
package com.tashow.erp.test;
import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.ObjectMapper;
import org.springframework.http.*;
import org.springframework.web.client.RestTemplate;
/**
* USPTO API 完整测试
* 测试目的:验证是否可以通过品牌名称搜索商标
*/
public class UsptoApiTest {
private static final String API_KEY = "TGP31sjvuxOb5bV21iYIihDpnXI4mFlM";
private static final RestTemplate rest = new RestTemplate();
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");
try {
HttpHeaders headers = new HttpHeaders();
headers.set("USPTO-API-KEY", API_KEY);
HttpEntity<String> entity = new HttpEntity<>(headers);
ResponseEntity<String> response = rest.exchange(
"https://tsdrapi.uspto.gov/ts/cd/casestatus/sn88123456/info.json",
HttpMethod.GET, entity, String.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();
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());
}
}
/**
* 测试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);
for (String urlTemplate : searchUrls) {
String url = String.format(urlTemplate, brand);
System.out.println(" 尝试: " + url);
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/失败");
}
}
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);
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());
}
}
}

View File

@@ -1,15 +1,53 @@
package com.tashow.erp.test;
import com.tashow.erp.utils.DeviceUtils;
import com.tashow.erp.utils.TrademarkCheckUtil;
import org.openqa.selenium.JavascriptExecutor;
import org.openqa.selenium.chrome.ChromeDriver;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.client.RestTemplate;
@RestController
public class aa {
@Autowired
private ChromeDriver driver;
@Autowired
RestTemplate restTemplate;
@Autowired
TrademarkCheckUtil trademarkCheckUtil;
@GetMapping("/aa")
public String aa() {
DeviceUtils deviceUtils = new DeviceUtils();
return deviceUtils.generateDeviceId();
public boolean aa() {
return checkTrademark("HEIBAGO");
}
public boolean checkTrademark(String brandName) {
try {
driver.get("https://tmsearch.uspto.gov/search/search-results");
Thread.sleep(2000);
String script = String.format("""
return 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: '%s', boost: 5}}},
{match: {WM: {query: '%s', boost: 2}}},
{match_phrase: {PM: {query: '%s', boost: 2}}}
]}}]}},
size: 1, _source: ['alive']
})
}).then(r => r.json()).then(d => d?.hits?.hits?.[0]?.source?.alive || false);
""", brandName, brandName, brandName);
Object result = ((JavascriptExecutor) driver).executeAsyncScript("var callback = arguments[arguments.length - 1];" + script.replace("return", "").replace(";", ".then(callback);"));
return Boolean.TRUE.equals(result);
} catch (Exception e) {
System.err.println("检测失败: " + e.getMessage());
return false;
}
}
}

View File

@@ -0,0 +1,32 @@
package com.tashow.erp.utils;
import cn.hutool.http.HttpUtil;
import org.springframework.stereotype.Component;
/**
* 代理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";
/**
* 获取一个代理IP
* @return 代理地址格式host:port如 123.96.236.32:40016
*/
public String getProxy() {
try {
String response = HttpUtil.get(API_URL);
if (response != null && !response.trim().isEmpty()) {
String proxy = response.trim();
System.out.println("获取到代理: " + proxy);
return proxy;
}
} catch (Exception e) {
System.err.println("获取代理失败: " + e.getMessage());
}
return null;
}
}

View File

@@ -21,6 +21,16 @@ public class SeleniumUtil {
* @return 配置好的 ChromeDriver
*/
public static ChromeDriver createDriver(boolean headless) {
return createDriver(headless, null);
}
/**
* 创建防检测的 ChromeDriver支持代理
* @param headless 是否启用无头模式
* @param proxy 代理地址格式host:port如 123.96.236.32:40016
* @return 配置好的 ChromeDriver
*/
public static ChromeDriver createDriver(boolean headless, String proxy) {
try {
WebDriverManager.chromedriver()
.driverRepositoryUrl(new URL("https://registry.npmmirror.com/-/binary/chromedriver/"))
@@ -37,6 +47,12 @@ public class SeleniumUtil {
options.addArguments("--no-sandbox", "--disable-gpu", "--disable-dev-shm-usage");
options.addArguments("user-agent=Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36");
// 配置代理
if (proxy != null && !proxy.trim().isEmpty()) {
options.addArguments("--proxy-server=http://" + proxy);
options.addArguments("--proxy-bypass-list=<-loopback>");
System.out.println("ChromeDriver使用代理: http://" + proxy);
}
// 无头模式
if (headless) {
options.addArguments("--headless=new");

View File

@@ -1,42 +1,128 @@
package com.tashow.erp.utils;
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 org.springframework.web.client.RestTemplate;
import java.util.*;
import java.util.concurrent.atomic.AtomicInteger;
/**
* 商标检查工具
* 支持批量并发查询每100次切换代理
*/
@Component
public class TrademarkCheckUtil {
@Autowired
private ProxyPool proxyPool;
private ChromeDriver driver;
@Autowired
RestTemplate restTemplate;
public boolean checkTrademark(String brandName) {
try {
driver.get("https://tmsearch.uspto.gov/search/search-results");
Thread.sleep(2000);
String script = String.format("""
return 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: '%s', boost: 5}}},
{match: {WM: {query: '%s', boost: 2}}},
{match_phrase: {PM: {query: '%s', boost: 2}}}
]}}]}},
size: 1, _source: ['alive']
})
}).then(r => r.json()).then(d => d?.hits?.hits?.[0]?.source?.alive || false);
""", brandName, brandName, brandName);
Object result = ((JavascriptExecutor) driver).executeAsyncScript("var callback = arguments[arguments.length - 1];" + script.replace("return", "").replace(";", ".then(callback);"));
return Boolean.TRUE.equals(result);
} catch (Exception e) {
System.err.println("检测失败: " + e.getMessage());
return false;
private final AtomicInteger checkCount = new AtomicInteger(0);
private static final int PROXY_SWITCH_THRESHOLD = 100;
private synchronized void ensureInit() {
if (driver == null) {
for (int i = 0; i < 3; i++) {
try {
driver = SeleniumUtil.createDriver(false, proxyPool.getProxy());
driver.get("https://tmsearch.uspto.gov/search/search-results");
Thread.sleep(5000);
return; // 成功则返回
} catch (Exception e) {
System.err.println("初始化失败(尝试" + (i+1) + "/3: " + 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) {
ensureInit();
// 每100个切换代理
if (checkCount.addAndGet(brands.size()) >= PROXY_SWITCH_THRESHOLD) {
try { driver.quit(); } catch (Exception e) {}
driver = null;
checkCount.set(0);
ensureInit();
}
// 构建批量查询脚本(带错误诊断)
String script = """
const brands = arguments[0];
const callback = arguments[arguments.length - 1];
Promise.all(brands.map(brand =>
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, boost: 5}}},
{match: {WM: {query: brand, boost: 2}}},
{match_phrase: {PM: {query: brand, boost: 2}}}
]}}]}},
size: 1, _source: ['alive']
})
})
.then(r => {
if (!r.ok) {
return {brand, alive: false, error: `HTTP ${r.status}: ${r.statusText}`};
}
return r.json().then(d => ({
brand,
alive: d?.hits?.hits?.[0]?.source?.alive || false,
error: null
}));
})
.catch(e => ({
brand,
alive: false,
error: e.name + ': ' + e.message
}))
)).then(callback);
""";
try {
@SuppressWarnings("unchecked")
List<Map<String, Object>> results = (List<Map<String, Object>>)
((JavascriptExecutor) driver).executeAsyncScript(script, brands);
Map<String, Boolean> resultMap = new HashMap<>();
for (Map<String, Object> item : results) {
String brand = (String) item.get("brand");
Boolean alive = (Boolean) item.get("alive");
String error = (String) item.get("error");
if (error != null) {
// 查询失败,不放入结果,只打印错误
System.err.println(brand + " -> [查询失败: " + error + "]");
} else {
// 查询成功,放入结果
resultMap.put(brand, alive);
System.out.println(brand + " -> " + (alive ? "✓ 已注册" : "✗ 未注册"));
}
}
return resultMap;
} catch (Exception e) {
System.err.println("批量查询失败: " + e.getMessage());
return new HashMap<>();
}
}
public synchronized void closeDriver() {
if (driver != null) {
try { driver.quit(); } catch (Exception e) {}
driver = null;
checkCount.set(0);
}
}
@PreDestroy
public void cleanup() {
closeDriver();
}
}