feat(electron):优化商标筛查面板与资源加载逻辑
- 将多个 v-if 条件渲染改为 v-show,提升组件切换性能 - 优化商标任务完成状态判断逻辑,确保准确显示采集完成图标- 调整任务统计数据显示条件,支持零数据展示- 更新 API 配置地址,切换至本地开发环境地址 - 降低 Spring Boot 线程池与数据库连接池配置,适应小规模并发- 禁用 devtools 热部署与 Swagger 接口文档,优化生产环境性能 - 配置 RestTemplate 使用 HttpClient 连接池,增强 HTTP 请求稳定性 - 改进静态资源拷贝脚本,确保 icon 与 image 文件夹正确复制 - 更新 electron-builder 配置,优化资源打包路径与应用图标 - 修改 HTTP 路由规则,明确区分客户端与管理端接口路径- 注册文件协议拦截器,解决生产环境下 icon/image 资源加载问题 - 调整商标 API 接口路径,指向 erp_client_sb服务 -重构 MarkController 控制器,专注 Token 管理功能 - 优化线程池参数,适配低并发业务场景- 强化商标筛查流程控制,完善任务取消与异常处理机制 - 新增方舟精选任务管理接口,实现 Excel 下载与数据解析功能
This commit is contained in:
@@ -1,12 +1,18 @@
|
||||
package com.tashow.erp.controller;
|
||||
import com.fasterxml.jackson.databind.JsonNode;
|
||||
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.service.IFangzhouApiService;
|
||||
import com.tashow.erp.utils.ExcelParseUtil;
|
||||
import com.tashow.erp.utils.JsonData;
|
||||
import com.tashow.erp.utils.LoggerUtil;
|
||||
import com.tashow.erp.utils.TrademarkCheckUtil;
|
||||
import cn.hutool.core.io.FileUtil;
|
||||
import cn.hutool.http.HttpUtil;
|
||||
import cn.hutool.poi.excel.ExcelReader;
|
||||
import cn.hutool.poi.excel.ExcelUtil;
|
||||
import org.slf4j.Logger;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.web.bind.annotation.*;
|
||||
@@ -33,6 +39,9 @@ public class TrademarkController {
|
||||
@Autowired
|
||||
private TrademarkSessionRepository sessionRepository;
|
||||
|
||||
@Autowired
|
||||
private IFangzhouApiService fangzhouApi;
|
||||
|
||||
// 进度追踪
|
||||
private final Map<String, Integer> progressMap = new java.util.concurrent.ConcurrentHashMap<>();
|
||||
|
||||
@@ -66,12 +75,11 @@ public class TrademarkController {
|
||||
.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;
|
||||
@@ -405,4 +413,117 @@ public class TrademarkController {
|
||||
return JsonData.buildError("恢复失败: " + e.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
// ==================== 方舟精选任务管理接口 ====================
|
||||
|
||||
/**
|
||||
* 获取方舟精选任务列表
|
||||
* 从第三方 API 下载 Excel 并解析过滤数据
|
||||
*/
|
||||
@PostMapping("/task")
|
||||
public JsonData getTask() {
|
||||
try {
|
||||
// 1. 获取 Token 并轮询等待下载链接
|
||||
String token = fangzhouApi.getToken();
|
||||
JsonNode dNode = fangzhouApi.pollTask(token, 6, 5000);
|
||||
String downloadUrl = dNode.get("download_url").asText();
|
||||
|
||||
if (downloadUrl == null || downloadUrl.isEmpty()) {
|
||||
return JsonData.buildError("下载链接生成超时");
|
||||
}
|
||||
|
||||
// 2. 下载并解析 Excel
|
||||
String tempFilePath = System.getProperty("java.io.tmpdir") + "/trademark_" + System.currentTimeMillis() + ".xlsx";
|
||||
HttpUtil.downloadFile(downloadUrl, FileUtil.file(tempFilePath));
|
||||
|
||||
List<Map<String, Object>> filteredData = new ArrayList<>();
|
||||
List<String> excelHeaders = new ArrayList<>();
|
||||
ExcelReader reader = null;
|
||||
|
||||
try {
|
||||
reader = ExcelUtil.getReader(FileUtil.file(tempFilePath));
|
||||
List<List<Object>> rows = reader.read();
|
||||
|
||||
if (rows.isEmpty()) {
|
||||
throw new RuntimeException("Excel文件为空");
|
||||
}
|
||||
|
||||
// 读取表头
|
||||
List<Object> headerRow = rows.get(0);
|
||||
for (Object cell : headerRow) {
|
||||
excelHeaders.add(cell != null ? cell.toString().trim() : "");
|
||||
}
|
||||
|
||||
// 找到商标类型列的索引
|
||||
int trademarkTypeIndex = -1;
|
||||
for (int i = 0; i < excelHeaders.size(); i++) {
|
||||
if ("商标类型".equals(excelHeaders.get(i))) {
|
||||
trademarkTypeIndex = i;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (trademarkTypeIndex < 0) {
|
||||
throw new RuntimeException("未找到'商标类型'列");
|
||||
}
|
||||
|
||||
// 过滤TM和未注册数据
|
||||
for (int i = 1; i < rows.size(); i++) {
|
||||
List<Object> row = rows.get(i);
|
||||
if (row.size() > trademarkTypeIndex) {
|
||||
String trademarkType = row.get(trademarkTypeIndex).toString().trim();
|
||||
if ("TM".equals(trademarkType) || "未注册".equals(trademarkType)) {
|
||||
Map<String, Object> item = new HashMap<>();
|
||||
for (int j = 0; j < excelHeaders.size() && j < row.size(); j++) {
|
||||
item.put(excelHeaders.get(j), row.get(j));
|
||||
}
|
||||
filteredData.add(item);
|
||||
}
|
||||
}
|
||||
}
|
||||
} finally {
|
||||
if (reader != null) {
|
||||
reader.close();
|
||||
}
|
||||
FileUtil.del(tempFilePath);
|
||||
}
|
||||
|
||||
// 6. 返回结果
|
||||
Map<String, Object> combinedResult = new HashMap<>();
|
||||
combinedResult.put("original", dNode);
|
||||
combinedResult.put("filtered", filteredData);
|
||||
combinedResult.put("headers", excelHeaders);
|
||||
|
||||
logger.info("任务获取成功,过滤出 {} 条数据", filteredData.size());
|
||||
return JsonData.buildSuccess(combinedResult);
|
||||
|
||||
} catch (Exception e) {
|
||||
logger.error("获取任务失败", e);
|
||||
return JsonData.buildError("获取任务失败: " + e.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 创建新任务
|
||||
* 上传文件到方舟精选
|
||||
*/
|
||||
@PostMapping("/newTask")
|
||||
public JsonData newTask(@RequestParam("file") MultipartFile file) {
|
||||
try {
|
||||
// 1. 获取 Token 并上传文件
|
||||
String token = fangzhouApi.getToken();
|
||||
JsonNode jsonNode = fangzhouApi.uploadFile(file, token);
|
||||
|
||||
// 2. 返回结果
|
||||
if (jsonNode.get("S").asInt() == 1) {
|
||||
logger.info("任务创建成功: {}", file.getOriginalFilename());
|
||||
return JsonData.buildSuccess(jsonNode.toString());
|
||||
}
|
||||
|
||||
return JsonData.buildError(jsonNode.get("M").asText());
|
||||
} catch (Exception e) {
|
||||
logger.error("创建任务失败", e);
|
||||
return JsonData.buildError("创建任务失败: " + e.getMessage());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,48 @@
|
||||
package com.tashow.erp.service;
|
||||
|
||||
import com.fasterxml.jackson.databind.JsonNode;
|
||||
import org.springframework.web.multipart.MultipartFile;
|
||||
|
||||
/**
|
||||
* 方舟精选 API 服务接口
|
||||
*/
|
||||
public interface IFangzhouApiService {
|
||||
|
||||
/**
|
||||
* 从服务器获取 Token
|
||||
*/
|
||||
String getToken();
|
||||
|
||||
/**
|
||||
* 刷新 Token
|
||||
*/
|
||||
String refreshToken();
|
||||
|
||||
/**
|
||||
* 调用方舟精选 API(表单提交,自动处理 Token 过期)
|
||||
* @param command 命令
|
||||
* @param data 数据
|
||||
* @param token Token
|
||||
* @return 响应结果
|
||||
*/
|
||||
JsonNode callApi(String command, String data, String token);
|
||||
|
||||
/**
|
||||
* 上传文件到方舟精选(自动处理 Token 过期)
|
||||
* @param file 文件
|
||||
* @param token Token
|
||||
* @return 响应结果
|
||||
*/
|
||||
JsonNode uploadFile(MultipartFile file, String token);
|
||||
|
||||
/**
|
||||
* 轮询获取任务(等待下载链接生成)
|
||||
* @param token Token
|
||||
* @param maxRetries 最大重试次数
|
||||
* @param intervalMs 重试间隔(毫秒)
|
||||
* @return 任务节点
|
||||
* @throws InterruptedException 中断异常
|
||||
*/
|
||||
JsonNode pollTask(String token, int maxRetries, long intervalMs) throws InterruptedException;
|
||||
}
|
||||
|
||||
@@ -0,0 +1,198 @@
|
||||
package com.tashow.erp.service.impl;
|
||||
|
||||
import com.fasterxml.jackson.databind.JsonNode;
|
||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
import com.tashow.erp.service.IFangzhouApiService;
|
||||
import com.tashow.erp.utils.ApiForwarder;
|
||||
import com.tashow.erp.utils.LoggerUtil;
|
||||
import org.slf4j.Logger;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.http.HttpEntity;
|
||||
import org.springframework.http.HttpHeaders;
|
||||
import org.springframework.http.MediaType;
|
||||
import org.springframework.http.ResponseEntity;
|
||||
import org.springframework.stereotype.Service;
|
||||
import org.springframework.util.LinkedMultiValueMap;
|
||||
import org.springframework.util.MultiValueMap;
|
||||
import org.springframework.web.client.RestTemplate;
|
||||
import org.springframework.web.multipart.MultipartFile;
|
||||
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.security.MessageDigest;
|
||||
import java.util.Map;
|
||||
|
||||
/**
|
||||
* 方舟精选 API 服务实现
|
||||
*/
|
||||
@Service
|
||||
public class FangzhouApiServiceImpl implements IFangzhouApiService {
|
||||
private static final Logger logger = LoggerUtil.getLogger(FangzhouApiServiceImpl.class);
|
||||
private static final String API_SECRET = "e10adc3949ba59abbe56e057f20f883e";
|
||||
private static final String FANGZHOU_API_URL = "https://api.fangzhoujingxuan.com/Task";
|
||||
private static final int TOKEN_EXPIRED_CODE = -1006;
|
||||
|
||||
@Autowired
|
||||
private RestTemplate restTemplate;
|
||||
|
||||
@Autowired
|
||||
private ObjectMapper objectMapper;
|
||||
|
||||
@Autowired
|
||||
private ApiForwarder apiForwarder;
|
||||
|
||||
@Override
|
||||
public String getToken() {
|
||||
try {
|
||||
logger.info("从服务器获取 Token");
|
||||
ResponseEntity<?> response = apiForwarder.get("/tool/mark/token", null);
|
||||
|
||||
@SuppressWarnings("unchecked")
|
||||
Map<String, Object> body = (Map<String, Object>) response.getBody();
|
||||
|
||||
if (body != null && (Integer) body.get("code") == 200) {
|
||||
return body.get("data").toString();
|
||||
}
|
||||
throw new RuntimeException("获取 Token 失败");
|
||||
} catch (Exception e) {
|
||||
logger.error("获取 Token 失败", e);
|
||||
throw new RuntimeException("获取 Token 失败: " + e.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public String refreshToken() {
|
||||
try {
|
||||
logger.info("刷新 Token");
|
||||
ResponseEntity<?> response = apiForwarder.post("/tool/mark/refreshToken", null, null);
|
||||
|
||||
@SuppressWarnings("unchecked")
|
||||
Map<String, Object> body = (Map<String, Object>) response.getBody();
|
||||
|
||||
if (body != null && (Integer) body.get("code") == 200) {
|
||||
return body.get("data").toString();
|
||||
}
|
||||
throw new RuntimeException("刷新 Token 失败");
|
||||
} catch (Exception e) {
|
||||
logger.error("刷新 Token 失败", e);
|
||||
throw new RuntimeException("刷新 Token 失败: " + e.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public JsonNode callApi(String command, String data, String token) {
|
||||
try {
|
||||
long ts = System.currentTimeMillis();
|
||||
|
||||
MultiValueMap<String, String> formData = new LinkedMultiValueMap<>();
|
||||
formData.add("c", command);
|
||||
formData.add("d", data);
|
||||
formData.add("t", token);
|
||||
formData.add("s", md5(ts + data + API_SECRET));
|
||||
formData.add("ts", String.valueOf(ts));
|
||||
formData.add("website", "1");
|
||||
|
||||
HttpHeaders headers = new HttpHeaders();
|
||||
headers.setContentType(MediaType.APPLICATION_FORM_URLENCODED);
|
||||
HttpEntity<MultiValueMap<String, String>> requestEntity = new HttpEntity<>(formData, headers);
|
||||
|
||||
String result = restTemplate.postForObject(FANGZHOU_API_URL, requestEntity, String.class);
|
||||
JsonNode json = objectMapper.readTree(result);
|
||||
|
||||
// 处理 Token 过期,自动刷新重试
|
||||
if (json.get("S").asInt() == TOKEN_EXPIRED_CODE) {
|
||||
logger.info("Token 过期,刷新后重试");
|
||||
String newToken = refreshToken();
|
||||
formData.set("t", newToken);
|
||||
formData.set("s", md5(ts + data + API_SECRET));
|
||||
requestEntity = new HttpEntity<>(formData, headers);
|
||||
result = restTemplate.postForObject(FANGZHOU_API_URL, requestEntity, String.class);
|
||||
json = objectMapper.readTree(result);
|
||||
}
|
||||
|
||||
return json;
|
||||
} catch (Exception e) {
|
||||
logger.error("调用方舟精选 API 失败", e);
|
||||
throw new RuntimeException("调用 API 失败: " + e.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public JsonNode uploadFile(MultipartFile file, String token) {
|
||||
try {
|
||||
String data = String.format("{\"name\":\"%s\",\"type\":1}", file.getOriginalFilename());
|
||||
long ts = System.currentTimeMillis();
|
||||
|
||||
MultiValueMap<String, Object> formData = new LinkedMultiValueMap<>();
|
||||
formData.add("c", "Create");
|
||||
formData.add("t", token);
|
||||
formData.add("ts", ts);
|
||||
formData.add("d", data);
|
||||
formData.add("s", md5(ts + data + API_SECRET));
|
||||
formData.add("website", "1");
|
||||
formData.add("files", file.getResource());
|
||||
|
||||
HttpHeaders headers = new HttpHeaders();
|
||||
headers.setContentType(MediaType.MULTIPART_FORM_DATA);
|
||||
HttpEntity<MultiValueMap<String, Object>> requestEntity = new HttpEntity<>(formData, headers);
|
||||
|
||||
String result = restTemplate.postForObject(FANGZHOU_API_URL, requestEntity, String.class);
|
||||
JsonNode json = objectMapper.readTree(result);
|
||||
|
||||
// 处理 Token 过期
|
||||
if (json.get("S").asInt() == TOKEN_EXPIRED_CODE) {
|
||||
logger.info("Token 过期,刷新后重试");
|
||||
String newToken = refreshToken();
|
||||
formData.set("t", newToken);
|
||||
formData.set("s", md5(ts + data + API_SECRET));
|
||||
requestEntity = new HttpEntity<>(formData, headers);
|
||||
result = restTemplate.postForObject(FANGZHOU_API_URL, requestEntity, String.class);
|
||||
json = objectMapper.readTree(result);
|
||||
}
|
||||
|
||||
return json;
|
||||
} catch (Exception e) {
|
||||
logger.error("上传文件失败", e);
|
||||
throw new RuntimeException("上传文件失败: " + e.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public JsonNode pollTask(String token, int maxRetries, long intervalMs) throws InterruptedException {
|
||||
String data = "{\"name\":\"\",\"page_size\":20,\"current_page\":1}";
|
||||
|
||||
for (int i = 0; i < maxRetries; i++) {
|
||||
JsonNode json = callApi("TaskPageList", data, token);
|
||||
JsonNode dNode = json.get("D").get("items").get(0);
|
||||
String downloadUrl = dNode.get("download_url").asText();
|
||||
|
||||
if (downloadUrl != null && !downloadUrl.isEmpty()) {
|
||||
return dNode;
|
||||
}
|
||||
|
||||
if (i < maxRetries - 1) {
|
||||
logger.info("下载链接未生成,等待 {}ms 后重试 ({}/{})", intervalMs, i + 1, maxRetries);
|
||||
Thread.sleep(intervalMs);
|
||||
}
|
||||
}
|
||||
|
||||
throw new RuntimeException("等待下载链接超时");
|
||||
}
|
||||
|
||||
/**
|
||||
* MD5 加密
|
||||
*/
|
||||
private String md5(String input) {
|
||||
try {
|
||||
MessageDigest md = MessageDigest.getInstance("MD5");
|
||||
byte[] digest = md.digest(input.getBytes(StandardCharsets.UTF_8));
|
||||
StringBuilder sb = new StringBuilder();
|
||||
for (byte b : digest) {
|
||||
sb.append(String.format("%02x", b));
|
||||
}
|
||||
return sb.toString();
|
||||
} catch (Exception e) {
|
||||
throw new RuntimeException("MD5加密失败", e);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -47,8 +47,8 @@ server:
|
||||
api:
|
||||
server:
|
||||
# 主服务器API配置
|
||||
base-url: "http://8.138.23.49:8085"
|
||||
#base-url: "http://192.168.1.89:8085"
|
||||
#base-url: "http://8.138.23.49:8085"
|
||||
base-url: "http://192.168.1.89:8085"
|
||||
paths:
|
||||
monitor: "/monitor/client/api"
|
||||
login: "/monitor/account/login"
|
||||
|
||||
Reference in New Issue
Block a user