feat(amazon): 实现商标筛查功能并优化用户体验
- 添加商标筛查面板和相关API接口- 实现Excel文件解析和数据过滤功能 - 添加文件上传进度跟踪和错误处理-优化空状态显示和操作引导- 实现tab状态持久化存储 - 添加订阅会员弹窗和付费入口 -优化文件选择和删除功能 - 改进UI样式和响应式布局
This commit is contained in:
@@ -10,7 +10,7 @@
|
||||
</parent>
|
||||
<groupId>com.tashow.erp</groupId>
|
||||
<artifactId>erp_client_sb</artifactId>
|
||||
<version>2.5.6</version>
|
||||
<version>2.6.0</version>
|
||||
<name>erp_client_sb</name>
|
||||
<description>erp客户端</description>
|
||||
<properties>
|
||||
|
||||
@@ -29,7 +29,7 @@ public class ChromeDriverPreloader implements ApplicationRunner {
|
||||
@Bean
|
||||
public ChromeDriver chromeDriver() {
|
||||
// 为兼容性保留 Bean,但不自动创建
|
||||
if (globalDriver == null) globalDriver = SeleniumUtil.createDriver(false);
|
||||
if (globalDriver == null) globalDriver = SeleniumUtil.createDriver(true);
|
||||
return globalDriver;
|
||||
}
|
||||
|
||||
|
||||
@@ -1,4 +1,8 @@
|
||||
package com.tashow.erp.controller;
|
||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
import com.tashow.erp.entity.TrademarkSessionEntity;
|
||||
import com.tashow.erp.repository.TrademarkSessionRepository;
|
||||
import com.tashow.erp.service.BrandTrademarkCacheService;
|
||||
import com.tashow.erp.utils.ExcelParseUtil;
|
||||
import com.tashow.erp.utils.JsonData;
|
||||
import com.tashow.erp.utils.LoggerUtil;
|
||||
@@ -7,6 +11,7 @@ import org.slf4j.Logger;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.web.bind.annotation.*;
|
||||
import org.springframework.web.multipart.MultipartFile;
|
||||
import java.time.LocalDateTime;
|
||||
import java.util.*;
|
||||
import java.util.stream.Collectors;
|
||||
/**
|
||||
@@ -17,52 +22,99 @@ import java.util.stream.Collectors;
|
||||
@CrossOrigin
|
||||
public class TrademarkController {
|
||||
private static final Logger logger = LoggerUtil.getLogger(TrademarkController.class);
|
||||
private static final ObjectMapper objectMapper = new ObjectMapper();
|
||||
|
||||
@Autowired
|
||||
private TrademarkCheckUtil util;
|
||||
|
||||
@Autowired
|
||||
private BrandTrademarkCacheService cacheService;
|
||||
|
||||
@Autowired
|
||||
private TrademarkSessionRepository sessionRepository;
|
||||
|
||||
// 进度追踪
|
||||
private final Map<String, Integer> progressMap = new java.util.concurrent.ConcurrentHashMap<>();
|
||||
|
||||
// 任务取消标志
|
||||
private final Map<String, Boolean> cancelMap = new java.util.concurrent.ConcurrentHashMap<>();
|
||||
|
||||
/**
|
||||
* 批量品牌商标筛查(浏览器内并发,极速版)
|
||||
* 批量品牌商标筛查
|
||||
*/
|
||||
@PostMapping("/brandCheck")
|
||||
public JsonData brandCheck(@RequestBody List<String> brands) {
|
||||
public JsonData brandCheck(@RequestBody Map<String, Object> request) {
|
||||
@SuppressWarnings("unchecked")
|
||||
List<String> brands = (List<String>) request.get("brands");
|
||||
String taskId = (String) request.get("taskId");
|
||||
|
||||
try {
|
||||
List<String> list = brands.stream()
|
||||
.filter(b -> b != null && !b.trim().isEmpty())
|
||||
.map(String::trim)
|
||||
.distinct()
|
||||
.collect(Collectors.toList());
|
||||
|
||||
long start = System.currentTimeMillis();
|
||||
// 串行查询(不加延迟)
|
||||
|
||||
// 1. 先从全局缓存获取
|
||||
Map<String, Boolean> cached = cacheService.getCached(list);
|
||||
|
||||
// 2. 找出缓存未命中的品牌
|
||||
List<String> toQuery = list.stream()
|
||||
.filter(b -> !cached.containsKey(b))
|
||||
.collect(Collectors.toList());
|
||||
|
||||
logger.info("全局缓存命中: {}/{},需查询: {}", cached.size(), list.size(), toQuery.size());
|
||||
|
||||
// 3. 查询未命中的品牌
|
||||
Map<String, Boolean> queried = new HashMap<>();
|
||||
if (!toQuery.isEmpty()) {
|
||||
for (int i = 0; i < toQuery.size(); i++) {
|
||||
// 检查任务是否被取消
|
||||
if (taskId != null && cancelMap.getOrDefault(taskId, false)) {
|
||||
logger.info("任务 {} 已被取消,停止查询", taskId);
|
||||
break;
|
||||
}
|
||||
|
||||
String brand = toQuery.get(i);
|
||||
logger.info("处理第 {} 个: {}", i + 1, brand);
|
||||
|
||||
Map<String, Boolean> results = util.batchCheck(Collections.singletonList(brand), queried);
|
||||
queried.putAll(results);
|
||||
|
||||
// 更新进度
|
||||
if (taskId != null) {
|
||||
progressMap.put(taskId, cached.size() + queried.size());
|
||||
}
|
||||
}
|
||||
|
||||
// 查询结束,保存所有品牌
|
||||
if (!queried.isEmpty())
|
||||
cacheService.saveResults(queried);
|
||||
}
|
||||
|
||||
// 5. 合并缓存和新查询结果
|
||||
Map<String, Boolean> allResults = new HashMap<>(cached);
|
||||
allResults.putAll(queried);
|
||||
|
||||
// 6. 统计结果
|
||||
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++;
|
||||
}
|
||||
for (Map.Entry<String, Boolean> entry : allResults.entrySet()) {
|
||||
if (!entry.getValue()) {
|
||||
Map<String, Object> m = new HashMap<>();
|
||||
m.put("brand", entry.getKey());
|
||||
m.put("status", "未注册");
|
||||
unregistered.add(m);
|
||||
} else {
|
||||
registeredCount++;
|
||||
}
|
||||
}
|
||||
|
||||
long t = (System.currentTimeMillis() - start) / 1000;
|
||||
int checkedCount = allResults.size();
|
||||
int failedCount = list.size() - checkedCount;
|
||||
|
||||
Map<String, Object> res = new HashMap<>();
|
||||
@@ -76,30 +128,281 @@ 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);
|
||||
}).start();
|
||||
}
|
||||
|
||||
return JsonData.buildSuccess(res);
|
||||
} catch (Exception e) {
|
||||
logger.error("筛查失败", e);
|
||||
return JsonData.buildError("筛查失败: " + e.getMessage());
|
||||
} finally {
|
||||
// 采集完成或失败后关闭浏览器
|
||||
util.closeDriver();
|
||||
cacheService.cleanExpired();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 查询品牌筛查进度
|
||||
*/
|
||||
@GetMapping("/brandCheckProgress")
|
||||
public JsonData getBrandCheckProgress(@RequestParam("taskId") String taskId) {
|
||||
Integer current = progressMap.get(taskId);
|
||||
if (current == null) {
|
||||
return JsonData.buildError("任务不存在或已完成");
|
||||
}
|
||||
Map<String, Integer> result = new HashMap<>();
|
||||
result.put("current", current);
|
||||
return JsonData.buildSuccess(result);
|
||||
}
|
||||
|
||||
/**
|
||||
* 取消品牌筛查任务
|
||||
*/
|
||||
@PostMapping("/cancelBrandCheck")
|
||||
public JsonData cancelBrandCheck(@RequestBody Map<String, String> request) {
|
||||
String taskId = request.get("taskId");
|
||||
if (taskId != null) {
|
||||
cancelMap.put(taskId, true);
|
||||
logger.info("任务 {} 已标记为取消", taskId);
|
||||
return JsonData.buildSuccess("任务已取消");
|
||||
}
|
||||
return JsonData.buildError("缺少taskId参数");
|
||||
}
|
||||
|
||||
/**
|
||||
* 从Excel提取品牌列表
|
||||
* 验证Excel表头
|
||||
*/
|
||||
@PostMapping("/validateHeaders")
|
||||
public JsonData validateHeaders(@RequestParam("file") MultipartFile file,
|
||||
@RequestParam(value = "requiredHeaders", required = false) String requiredHeadersJson) {
|
||||
try {
|
||||
Map<String, Object> fullData = ExcelParseUtil.parseFullExcel(file);
|
||||
@SuppressWarnings("unchecked")
|
||||
List<String> headers = (List<String>) fullData.get("headers");
|
||||
|
||||
if (headers == null || headers.isEmpty()) {
|
||||
return JsonData.buildError("无法读取Excel表头");
|
||||
}
|
||||
|
||||
Map<String, Object> result = new HashMap<>();
|
||||
result.put("headers", headers);
|
||||
|
||||
// 如果提供了必需表头,进行验证
|
||||
if (requiredHeadersJson != null && !requiredHeadersJson.trim().isEmpty()) {
|
||||
List<String> requiredHeaders = objectMapper.readValue(requiredHeadersJson,
|
||||
objectMapper.getTypeFactory().constructCollectionType(List.class, String.class));
|
||||
|
||||
List<String> missing = new ArrayList<>();
|
||||
for (String required : requiredHeaders) {
|
||||
if (!headers.contains(required)) {
|
||||
missing.add(required);
|
||||
}
|
||||
}
|
||||
|
||||
result.put("valid", missing.isEmpty());
|
||||
result.put("missing", missing);
|
||||
|
||||
if (!missing.isEmpty()) {
|
||||
return JsonData.buildError("缺少必需的列: " + String.join(", ", missing));
|
||||
}
|
||||
}
|
||||
|
||||
return JsonData.buildSuccess(result);
|
||||
} catch (Exception e) {
|
||||
logger.error("验证表头失败", e);
|
||||
return JsonData.buildError("验证失败: " + e.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 从Excel提取品牌列表(同时返回完整Excel数据)
|
||||
*/
|
||||
@PostMapping("/extractBrands")
|
||||
public JsonData extractBrands(@RequestParam("file") MultipartFile file) {
|
||||
try {
|
||||
List<String> brands = ExcelParseUtil.parseColumnByName(file, "品牌");
|
||||
if (brands.isEmpty()) return JsonData.buildError("未找到品牌列或品牌数据为空");
|
||||
|
||||
// 读取完整Excel数据
|
||||
Map<String, Object> fullData = ExcelParseUtil.parseFullExcel(file);
|
||||
|
||||
Map<String, Object> result = new HashMap<>();
|
||||
result.put("total", brands.size());
|
||||
result.put("brands", brands);
|
||||
result.put("headers", fullData.get("headers"));
|
||||
result.put("allRows", fullData.get("rows"));
|
||||
return JsonData.buildSuccess(result);
|
||||
} catch (Exception e) {
|
||||
return JsonData.buildError("提取失败: " + e.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 根据ASIN列表从Excel中过滤完整行数据
|
||||
*/
|
||||
@PostMapping("/filterByAsins")
|
||||
public JsonData filterByAsins(@RequestParam("file") MultipartFile file, @RequestParam("asins") String asinsJson) {
|
||||
try {
|
||||
if (asinsJson == null || asinsJson.trim().isEmpty()) {
|
||||
return JsonData.buildError("ASIN列表不能为空");
|
||||
}
|
||||
|
||||
// 使用Jackson解析JSON数组
|
||||
List<String> asins;
|
||||
try {
|
||||
asins = objectMapper.readValue(asinsJson,
|
||||
objectMapper.getTypeFactory().constructCollectionType(List.class, String.class));
|
||||
} catch (Exception e) {
|
||||
logger.error("解析ASIN列表JSON失败: {}", asinsJson, e);
|
||||
return JsonData.buildError("ASIN列表格式错误: " + e.getMessage());
|
||||
}
|
||||
|
||||
if (asins == null || asins.isEmpty()) {
|
||||
return JsonData.buildError("ASIN列表不能为空");
|
||||
}
|
||||
|
||||
logger.info("接收到ASIN过滤请求,ASIN数量: {}", asins.size());
|
||||
|
||||
Map<String, Object> result = ExcelParseUtil.filterExcelByAsins(file, asins);
|
||||
|
||||
@SuppressWarnings("unchecked")
|
||||
List<Map<String, Object>> filteredRows = (List<Map<String, Object>>) result.get("filteredRows");
|
||||
|
||||
Map<String, Object> response = new HashMap<>();
|
||||
response.put("headers", result.get("headers"));
|
||||
response.put("filteredRows", filteredRows);
|
||||
response.put("total", filteredRows.size());
|
||||
|
||||
logger.info("ASIN过滤完成,过滤出 {} 行数据", filteredRows.size());
|
||||
|
||||
return JsonData.buildSuccess(response);
|
||||
} catch (Exception e) {
|
||||
logger.error("根据ASIN过滤失败", e);
|
||||
return JsonData.buildError("过滤失败: " + e.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 根据品牌列表从Excel中过滤完整行数据
|
||||
*/
|
||||
@PostMapping("/filterByBrands")
|
||||
public JsonData filterByBrands(@RequestParam("file") MultipartFile file, @RequestParam("brands") String brandsJson) {
|
||||
try {
|
||||
if (brandsJson == null || brandsJson.trim().isEmpty()) {
|
||||
return JsonData.buildError("品牌列表不能为空");
|
||||
}
|
||||
|
||||
// 使用Jackson解析JSON数组
|
||||
List<String> brands;
|
||||
try {
|
||||
brands = objectMapper.readValue(brandsJson,
|
||||
objectMapper.getTypeFactory().constructCollectionType(List.class, String.class));
|
||||
} catch (Exception e) {
|
||||
logger.error("解析品牌列表JSON失败: {}", brandsJson, e);
|
||||
return JsonData.buildError("品牌列表格式错误: " + e.getMessage());
|
||||
}
|
||||
|
||||
if (brands == null || brands.isEmpty()) {
|
||||
return JsonData.buildError("品牌列表不能为空");
|
||||
}
|
||||
|
||||
logger.info("接收到品牌过滤请求,品牌数量: {}", brands.size());
|
||||
|
||||
Map<String, Object> result = ExcelParseUtil.filterExcelByBrands(file, brands);
|
||||
|
||||
@SuppressWarnings("unchecked")
|
||||
List<Map<String, Object>> filteredRows = (List<Map<String, Object>>) result.get("filteredRows");
|
||||
|
||||
Map<String, Object> response = new HashMap<>();
|
||||
response.put("headers", result.get("headers"));
|
||||
response.put("filteredRows", filteredRows);
|
||||
response.put("total", filteredRows.size());
|
||||
|
||||
logger.info("品牌过滤完成,过滤出 {} 行数据", filteredRows.size());
|
||||
|
||||
return JsonData.buildSuccess(response);
|
||||
} catch (Exception e) {
|
||||
logger.error("根据品牌过滤失败", e);
|
||||
return JsonData.buildError("过滤失败: " + e.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 保存商标查询会话
|
||||
*/
|
||||
@PostMapping("/saveSession")
|
||||
public JsonData saveSession(@RequestBody Map<String, Object> sessionData,
|
||||
@RequestHeader(value = "username", required = false) String username) {
|
||||
try {
|
||||
if (username == null || username.trim().isEmpty()) {
|
||||
username = "default";
|
||||
}
|
||||
|
||||
String sessionId = UUID.randomUUID().toString();
|
||||
TrademarkSessionEntity entity = new TrademarkSessionEntity();
|
||||
entity.setSessionId(sessionId);
|
||||
entity.setUsername(username);
|
||||
entity.setFileName((String) sessionData.get("fileName"));
|
||||
entity.setResultData(objectMapper.writeValueAsString(sessionData.get("resultData")));
|
||||
entity.setFullData(objectMapper.writeValueAsString(sessionData.get("fullData")));
|
||||
entity.setHeaders(objectMapper.writeValueAsString(sessionData.get("headers")));
|
||||
entity.setTaskProgress(objectMapper.writeValueAsString(sessionData.get("taskProgress")));
|
||||
entity.setQueryStatus((String) sessionData.get("queryStatus"));
|
||||
|
||||
sessionRepository.save(entity);
|
||||
|
||||
// 清理7天前的数据
|
||||
sessionRepository.deleteByCreatedAtBefore(LocalDateTime.now().minusDays(7));
|
||||
|
||||
logger.info("保存商标查询会话: {} (用户: {})", sessionId, username);
|
||||
|
||||
Map<String, String> result = new HashMap<>();
|
||||
result.put("sessionId", sessionId);
|
||||
return JsonData.buildSuccess(result);
|
||||
} catch (Exception e) {
|
||||
logger.error("保存会话失败", e);
|
||||
return JsonData.buildError("保存失败: " + e.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 根据sessionId恢复查询会话
|
||||
*/
|
||||
@GetMapping("/getSession")
|
||||
public JsonData getSession(@RequestParam("sessionId") String sessionId,
|
||||
@RequestHeader(value = "username", required = false) String username) {
|
||||
try {
|
||||
if (username == null || username.trim().isEmpty()) {
|
||||
username = "default";
|
||||
}
|
||||
|
||||
Optional<TrademarkSessionEntity> opt = sessionRepository.findBySessionIdAndUsername(sessionId, username);
|
||||
if (!opt.isPresent()) {
|
||||
return JsonData.buildError("会话不存在或已过期");
|
||||
}
|
||||
|
||||
TrademarkSessionEntity entity = opt.get();
|
||||
Map<String, Object> result = new HashMap<>();
|
||||
result.put("fileName", entity.getFileName());
|
||||
result.put("resultData", objectMapper.readValue(entity.getResultData(), List.class));
|
||||
result.put("fullData", objectMapper.readValue(entity.getFullData(), List.class));
|
||||
result.put("headers", objectMapper.readValue(entity.getHeaders(), List.class));
|
||||
result.put("taskProgress", objectMapper.readValue(entity.getTaskProgress(), Map.class));
|
||||
result.put("queryStatus", entity.getQueryStatus());
|
||||
|
||||
logger.info("恢复商标查询会话: {} (用户: {})", sessionId, username);
|
||||
return JsonData.buildSuccess(result);
|
||||
} catch (Exception e) {
|
||||
logger.error("恢复会话失败", e);
|
||||
return JsonData.buildError("恢复失败: " + e.getMessage());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,35 @@
|
||||
package com.tashow.erp.entity;
|
||||
|
||||
import jakarta.persistence.*;
|
||||
import lombok.Data;
|
||||
import java.time.LocalDateTime;
|
||||
|
||||
@Entity
|
||||
@Table(name = "brand_trademark_cache",
|
||||
uniqueConstraints = @UniqueConstraint(columnNames = {"brand"}))
|
||||
@Data
|
||||
public class BrandTrademarkCacheEntity {
|
||||
|
||||
@Id
|
||||
@GeneratedValue(strategy = GenerationType.IDENTITY)
|
||||
private Long id;
|
||||
|
||||
@Column(nullable = false, unique = true)
|
||||
private String brand;
|
||||
|
||||
@Column(nullable = false)
|
||||
private Boolean registered;
|
||||
|
||||
@Column
|
||||
private String username;
|
||||
|
||||
@Column(name = "created_at", nullable = false)
|
||||
private LocalDateTime createdAt;
|
||||
|
||||
@PrePersist
|
||||
protected void onCreate() {
|
||||
createdAt = LocalDateTime.now();
|
||||
username = "global"; // 全局缓存
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,48 @@
|
||||
package com.tashow.erp.entity;
|
||||
|
||||
import jakarta.persistence.*;
|
||||
import lombok.Data;
|
||||
import java.time.LocalDateTime;
|
||||
|
||||
@Entity
|
||||
@Table(name = "trademark_sessions")
|
||||
@Data
|
||||
public class TrademarkSessionEntity {
|
||||
|
||||
@Id
|
||||
@GeneratedValue(strategy = GenerationType.IDENTITY)
|
||||
private Long id;
|
||||
|
||||
@Column(name = "session_id", unique = true, nullable = false)
|
||||
private String sessionId;
|
||||
|
||||
@Column(nullable = false)
|
||||
private String username;
|
||||
|
||||
@Column(name = "file_name")
|
||||
private String fileName;
|
||||
|
||||
@Column(name = "result_data", columnDefinition = "TEXT")
|
||||
private String resultData;
|
||||
|
||||
@Column(name = "full_data", columnDefinition = "TEXT")
|
||||
private String fullData;
|
||||
|
||||
@Column(columnDefinition = "TEXT")
|
||||
private String headers;
|
||||
|
||||
@Column(name = "task_progress", columnDefinition = "TEXT")
|
||||
private String taskProgress;
|
||||
|
||||
@Column(name = "query_status")
|
||||
private String queryStatus;
|
||||
|
||||
@Column(name = "created_at", nullable = false)
|
||||
private LocalDateTime createdAt;
|
||||
|
||||
@PrePersist
|
||||
protected void onCreate() {
|
||||
createdAt = LocalDateTime.now();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,29 @@
|
||||
package com.tashow.erp.repository;
|
||||
|
||||
import com.tashow.erp.entity.BrandTrademarkCacheEntity;
|
||||
import org.springframework.data.jpa.repository.JpaRepository;
|
||||
import org.springframework.data.jpa.repository.Modifying;
|
||||
import org.springframework.data.jpa.repository.Query;
|
||||
import org.springframework.stereotype.Repository;
|
||||
import org.springframework.transaction.annotation.Transactional;
|
||||
import java.time.LocalDateTime;
|
||||
import java.util.List;
|
||||
import java.util.Optional;
|
||||
|
||||
@Repository
|
||||
public interface BrandTrademarkCacheRepository extends JpaRepository<BrandTrademarkCacheEntity, Long> {
|
||||
|
||||
boolean existsByBrand(String brand);
|
||||
|
||||
Optional<BrandTrademarkCacheEntity> findByBrandAndCreatedAtAfter(
|
||||
String brand, LocalDateTime cutoffTime);
|
||||
|
||||
List<BrandTrademarkCacheEntity> findByBrandInAndCreatedAtAfter(
|
||||
List<String> brands, LocalDateTime cutoffTime);
|
||||
|
||||
@Modifying
|
||||
@Transactional
|
||||
@Query("DELETE FROM BrandTrademarkCacheEntity WHERE createdAt < ?1")
|
||||
void deleteByCreatedAtBefore(LocalDateTime cutoffTime);
|
||||
}
|
||||
|
||||
@@ -0,0 +1,22 @@
|
||||
package com.tashow.erp.repository;
|
||||
|
||||
import com.tashow.erp.entity.TrademarkSessionEntity;
|
||||
import org.springframework.data.jpa.repository.JpaRepository;
|
||||
import org.springframework.data.jpa.repository.Modifying;
|
||||
import org.springframework.data.jpa.repository.Query;
|
||||
import org.springframework.stereotype.Repository;
|
||||
import org.springframework.transaction.annotation.Transactional;
|
||||
import java.time.LocalDateTime;
|
||||
import java.util.Optional;
|
||||
|
||||
@Repository
|
||||
public interface TrademarkSessionRepository extends JpaRepository<TrademarkSessionEntity, Long> {
|
||||
|
||||
Optional<TrademarkSessionEntity> findBySessionIdAndUsername(String sessionId, String username);
|
||||
|
||||
@Modifying
|
||||
@Transactional
|
||||
@Query("DELETE FROM TrademarkSessionEntity WHERE createdAt < ?1")
|
||||
void deleteByCreatedAtBefore(LocalDateTime cutoffTime);
|
||||
}
|
||||
|
||||
@@ -0,0 +1,23 @@
|
||||
package com.tashow.erp.service;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
public interface BrandTrademarkCacheService {
|
||||
|
||||
/**
|
||||
* 批量获取缓存(1天内有效,全局共享)
|
||||
*/
|
||||
Map<String, Boolean> getCached(List<String> brands);
|
||||
|
||||
/**
|
||||
* 批量保存查询结果(全局共享)
|
||||
*/
|
||||
void saveResults(Map<String, Boolean> results);
|
||||
|
||||
/**
|
||||
* 清理1天前的过期数据
|
||||
*/
|
||||
void cleanExpired();
|
||||
}
|
||||
|
||||
@@ -30,6 +30,10 @@ public class CacheService {
|
||||
private CacheDataRepository cacheDataRepository;
|
||||
@Autowired
|
||||
private UpdateStatusRepository updateStatusRepository;
|
||||
@Autowired
|
||||
private BrandTrademarkCacheRepository brandTrademarkCacheRepository;
|
||||
@Autowired
|
||||
private TrademarkSessionRepository trademarkSessionRepository;
|
||||
|
||||
public void saveAuthToken(String service, String token, long expireTimeMillis) {
|
||||
try {
|
||||
@@ -46,25 +50,14 @@ public class CacheService {
|
||||
|
||||
@Transactional
|
||||
public void clearCache() {
|
||||
|
||||
|
||||
// 清理所有产品数据
|
||||
rakutenProductRepository.deleteAll();
|
||||
|
||||
|
||||
amazonProductRepository.deleteAll();
|
||||
|
||||
|
||||
alibaba1688ProductRepository.deleteAll();
|
||||
|
||||
|
||||
// 清理所有订单数据
|
||||
banmaOrderRepository.deleteAll();
|
||||
|
||||
zebraOrderRepository.deleteAll();
|
||||
// 清理通用缓存和更新状态
|
||||
cacheDataRepository.deleteAll();
|
||||
|
||||
brandTrademarkCacheRepository.deleteAll();
|
||||
trademarkSessionRepository.deleteAll();
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,56 @@
|
||||
package com.tashow.erp.service.impl;
|
||||
|
||||
import com.tashow.erp.entity.BrandTrademarkCacheEntity;
|
||||
import com.tashow.erp.repository.BrandTrademarkCacheRepository;
|
||||
import com.tashow.erp.service.BrandTrademarkCacheService;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.stereotype.Service;
|
||||
import org.springframework.transaction.annotation.Transactional;
|
||||
import java.time.LocalDateTime;
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
@Slf4j
|
||||
@Service
|
||||
public class BrandTrademarkCacheServiceImpl implements BrandTrademarkCacheService {
|
||||
|
||||
@Autowired
|
||||
private BrandTrademarkCacheRepository repository;
|
||||
|
||||
@Override
|
||||
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);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@Override
|
||||
@Transactional
|
||||
public void cleanExpired() {
|
||||
LocalDateTime cutoffTime = LocalDateTime.now().minusDays(1);
|
||||
repository.deleteByCreatedAtBefore(cutoffTime);
|
||||
log.info("清理1天前的品牌商标缓存");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -6,8 +6,8 @@ import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.web.multipart.MultipartFile;
|
||||
|
||||
import java.io.InputStream;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.*;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
/**
|
||||
* Excel 解析工具类
|
||||
@@ -18,6 +18,23 @@ import java.util.List;
|
||||
@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)) {
|
||||
if (cell != null && columnName.equals(cell.toString().replaceAll("\\s+", ""))) {
|
||||
return r;
|
||||
}
|
||||
}
|
||||
}
|
||||
return -1;
|
||||
}
|
||||
|
||||
/**
|
||||
* 解析 Excel 文件第一列数据
|
||||
* 通用方法,适用于店铺名、ASIN、订单号等标识符解析
|
||||
@@ -102,7 +119,6 @@ public class ExcelParseUtil {
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
* 根据列名解析数据(自动适配第1行或第2行为表头)
|
||||
*/
|
||||
public static List<String> parseColumnByName(MultipartFile file, String columnName) {
|
||||
@@ -112,23 +128,18 @@ public class ExcelParseUtil {
|
||||
List<List<Object>> rows = reader.read();
|
||||
if (rows.isEmpty()) return result;
|
||||
|
||||
// 查找表头行和列索引
|
||||
int headerRow = -1, colIdx = -1;
|
||||
for (int r = 0; r < Math.min(2, rows.size()); r++) {
|
||||
for (int c = 0; c < rows.get(r).size(); c++) {
|
||||
String col = rows.get(r).get(c).toString().replaceAll("\\s+", "");
|
||||
if (col.equals(columnName)) {
|
||||
headerRow = r;
|
||||
colIdx = c;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (colIdx != -1) break;
|
||||
}
|
||||
|
||||
if (colIdx == -1) return result;
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
||||
// 从表头下一行开始读数据
|
||||
for (int i = headerRow + 1; i < rows.size(); i++) {
|
||||
List<Object> row = rows.get(i);
|
||||
if (row.size() > colIdx && row.get(colIdx) != null) {
|
||||
@@ -141,4 +152,186 @@ 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<>();
|
||||
List<Map<String, Object>> rows = new ArrayList<>();
|
||||
|
||||
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() : "");
|
||||
}
|
||||
|
||||
for (int i = headerRowIndex + 1; i < allRows.size(); i++) {
|
||||
List<Object> row = allRows.get(i);
|
||||
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));
|
||||
}
|
||||
rows.add(rowMap);
|
||||
}
|
||||
|
||||
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);
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
/**
|
||||
* 根据品牌列表从Excel中过滤完整行数据(自动适配第1行或第2行为表头)
|
||||
* @param file Excel文件
|
||||
* @param brands 品牌列表
|
||||
* @return Map包含headers(表头)和filteredRows(过滤后的完整行数据)
|
||||
*/
|
||||
public static Map<String, Object> filterExcelByBrands(MultipartFile file, List<String> brands) {
|
||||
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 = findHeaderRow(allRows, "品牌");
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
||||
for (Object cell : headerRow) {
|
||||
headers.add(cell != null ? cell.toString().trim() : "");
|
||||
}
|
||||
|
||||
Set<String> brandSet = brands.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())) {
|
||||
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("品牌过滤: {}, {}个品牌 -> {}行数据", file.getOriginalFilename(), brands.size(), filteredRows.size());
|
||||
|
||||
} catch (Exception e) {
|
||||
log.error("品牌过滤失败: {}", e.getMessage(), e);
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
}
|
||||
@@ -2,9 +2,6 @@ package com.tashow.erp.utils;
|
||||
import io.github.bonigarcia.wdm.WebDriverManager;
|
||||
import org.openqa.selenium.chrome.ChromeDriver;
|
||||
import org.openqa.selenium.chrome.ChromeOptions;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.web.client.RestTemplate;
|
||||
|
||||
import java.net.URL;
|
||||
import java.util.Collections;
|
||||
import java.util.Map;
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
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.*;
|
||||
|
||||
/**
|
||||
@@ -15,12 +15,15 @@ import java.util.*;
|
||||
public class TrademarkCheckUtil {
|
||||
@Autowired
|
||||
private ProxyPool proxyPool;
|
||||
@Autowired
|
||||
private BrandTrademarkCacheService cacheService;
|
||||
private ChromeDriver driver;
|
||||
|
||||
private synchronized void ensureInit() {
|
||||
if (driver == null) {
|
||||
for (int i = 0; i < 5; i++) {
|
||||
try {
|
||||
driver = SeleniumUtil.createDriver(false, proxyPool.getProxy());
|
||||
driver = SeleniumUtil.createDriver(true, proxyPool.getProxy());
|
||||
driver.get("https://tmsearch.uspto.gov/search/search-results");
|
||||
Thread.sleep(6000);
|
||||
return; // 成功则返回
|
||||
@@ -36,7 +39,7 @@ public class TrademarkCheckUtil {
|
||||
}
|
||||
}
|
||||
|
||||
public synchronized Map<String, Boolean> batchCheck(List<String> brands) {
|
||||
public synchronized Map<String, Boolean> batchCheck(List<String> brands, Map<String, Boolean> alreadyQueried) {
|
||||
ensureInit();
|
||||
|
||||
// 构建批量查询脚本(带错误诊断)
|
||||
@@ -80,16 +83,32 @@ public class TrademarkCheckUtil {
|
||||
List<Map<String, Object>> results = (List<Map<String, Object>>)
|
||||
((JavascriptExecutor) driver).executeAsyncScript(script, brands);
|
||||
|
||||
// 检测是否有403错误
|
||||
boolean has403 = results.stream()
|
||||
// 检测是否有网络错误(包括403、Failed to fetch等)
|
||||
boolean hasNetworkError = results.stream()
|
||||
.anyMatch(item -> {
|
||||
String error = (String) item.get("error");
|
||||
return error != null && error.contains("HTTP 403");
|
||||
return error != null && (
|
||||
error.contains("HTTP 403") ||
|
||||
error.contains("Failed to fetch") ||
|
||||
error.contains("NetworkError") ||
|
||||
error.contains("TypeError")
|
||||
);
|
||||
});
|
||||
|
||||
// 如果有403,切换代理并重试
|
||||
if (has403) {
|
||||
System.err.println("检测到403,切换代理并重试...");
|
||||
// 如果有网络错误,切换代理并重试
|
||||
if (hasNetworkError) {
|
||||
System.err.println("检测到网络错误,切换代理并重试...");
|
||||
|
||||
// 切换代理前保存已查询的品牌
|
||||
if (alreadyQueried != null && !alreadyQueried.isEmpty()) {
|
||||
try {
|
||||
cacheService.saveResults(alreadyQueried);
|
||||
System.out.println("代理切换,已保存 " + alreadyQueried.size() + " 个品牌到缓存");
|
||||
} catch (Exception e) {
|
||||
System.err.println("保存缓存失败: " + e.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
try { driver.quit(); } catch (Exception e) {}
|
||||
driver = null;
|
||||
ensureInit();
|
||||
|
||||
Reference in New Issue
Block a user