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:
2025-11-06 14:39:58 +08:00
parent cfb70d5830
commit 2f00fde3be
45 changed files with 593 additions and 311 deletions

View File

@@ -113,6 +113,12 @@
<scope>compile</scope>
</dependency>
<dependency>
<groupId>org.apache.httpcomponents</groupId>
<artifactId>httpclient</artifactId>
<version>4.5.14</version>
</dependency>
</dependencies>
<build>

View File

@@ -1,11 +1,15 @@
package com.ruoyi.framework.config;
import org.apache.http.client.HttpClient;
import org.apache.http.impl.client.HttpClientBuilder;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.http.client.HttpComponentsClientHttpRequestFactory;
import org.springframework.web.client.RestTemplate;
/**
* 程序注解配置
* RestTemplate 配置
* 添加连接池和超时设置,防止连接泄漏
*
* @author ruoyi
*/
@@ -13,10 +17,24 @@ import org.springframework.web.client.RestTemplate;
public class BeanRestConfig
{
/**
* 创建RestTemplate Bean
* 创建 RestTemplate Bean(带连接池)
* 适用于小规模并发场景10人以内
*/
@Bean
public RestTemplate restTemplate() {
return new RestTemplate();
HttpComponentsClientHttpRequestFactory factory = new HttpComponentsClientHttpRequestFactory();
factory.setConnectTimeout(10000); // 连接超时 10秒
factory.setReadTimeout(30000); // 读取超时 30秒
factory.setConnectionRequestTimeout(5000); // 从连接池获取连接超时 5秒
// 连接池配置(降低资源占用)
HttpClient httpClient = HttpClientBuilder.create()
.setMaxConnTotal(20) // 最大连接数 20降低内存
.setMaxConnPerRoute(10) // 每个路由最大 10 连接
.build();
factory.setHttpClient(httpClient);
return new RestTemplate(factory);
}
}

View File

@@ -1,217 +1,107 @@
package com.ruoyi.web.controller.tool;
import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.ruoyi.common.annotation.Anonymous;
import com.ruoyi.common.constant.CacheConstants;
import com.ruoyi.common.core.domain.AjaxResult;
import com.ruoyi.common.core.redis.RedisCache;
import com.ruoyi.system.service.IMarkService;
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.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.HttpEntity;
import org.springframework.http.HttpHeaders;
import org.springframework.http.MediaType;
import org.springframework.util.LinkedMultiValueMap;
import org.springframework.util.MultiValueMap;
import org.springframework.web.bind.annotation.*;
import org.springframework.web.client.RestTemplate;
import org.springframework.web.multipart.MultipartFile;
import java.util.*;
import java.util.Map;
/**
* 方舟商标 Token 管理控制器
* 职责:仅负责 Token 的获取、刷新和管理
* 重型任务Excel 下载、解析、商标检查)已转移到 erp_client_sb
*/
@RequestMapping("/tool/mark")
@RestController
@Anonymous
public class MarkController {
private static final String API_SECRET = "e10adc3949ba59abbe56e057f20f883e";
private static final String ERP_CLIENT_BASE_URL = "http://127.0.0.1:8081";
private final RestTemplate restTemplate = new RestTemplate();
private final ObjectMapper objectMapper = new ObjectMapper();
@Autowired
private RedisCache redisCache;
@Autowired
private IMarkService markService;
@Autowired
private RedisCache redisCache;
/**
* 获取任务列表
* 获取 Token
* 如果 Redis 中不存在 Token自动注册新账号
*
* @return Token 字符串
*/
@GetMapping("/task")
public AjaxResult Task() {
@GetMapping("/token")
public AjaxResult getToken() {
try {
// 先尝试从 Redis 获取现有 Token
String token = redisCache.getCacheMapValue(CacheConstants.MARK_ACCOUNT_KEY, "token");
String d = "{\"name\":\"\",\"page_size\":20,\"current_page\":1}";
long ts = System.currentTimeMillis();
MultiValueMap<String, String> formData = new LinkedMultiValueMap<>();
formData.add("c", "TaskPageList");
formData.add("d", d);
formData.add("t", token);
formData.add("s", markService.md5(ts + d + 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("https://api.fangzhoujingxuan.com/Task", requestEntity, String.class);
JsonNode json = objectMapper.readTree(result);
if(json.get("S").asInt()==-1006){
token= markService.login();
formData.add("t", token);
requestEntity = new HttpEntity<>(formData, headers);
result = restTemplate.postForObject("https://api.fangzhoujingxuan.com/Task", requestEntity, String.class);
json= objectMapper.readTree(result);
if (token != null && !token.isEmpty()) {
return AjaxResult.success("获取成功", token);
}
JsonNode dNode = json.get("D").get("items").get(0);
// 获取下载链接并处理Excel数据
String downloadUrl = dNode.get("download_url").asText();
for (int i = 0; i < 6 && downloadUrl.isEmpty(); i++) {
Thread.sleep(5000);
long reTs = System.currentTimeMillis();
MultiValueMap<String, String> reFormData = new LinkedMultiValueMap<>();
reFormData.add("c", "TaskPageList");
reFormData.add("d", d);
reFormData.add("t", token);
reFormData.add("s", markService.md5(reTs + d + API_SECRET));
reFormData.add("ts", String.valueOf(reTs));
reFormData.add("website", "1");
HttpEntity<MultiValueMap<String, String>> reRequestEntity = new HttpEntity<>(reFormData, headers);
String reResult = restTemplate.postForObject("https://api.fangzhoujingxuan.com/Task", reRequestEntity, String.class);
JsonNode reJson = objectMapper.readTree(reResult);
dNode = reJson.get("D").get("items").get(0);
downloadUrl = reJson.get("D").get("items").get(0).get("download_url").asText();
}
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);
}
Map<String, Object> combinedResult = new HashMap<>();
combinedResult.put("original", dNode);
combinedResult.put("filtered", filteredData);
combinedResult.put("headers", excelHeaders);
return AjaxResult.success(combinedResult);
// Token 不存在,自动注册新账号
token = markService.reg();
return AjaxResult.success("注册成功", token);
} catch (Exception e) {
throw new RuntimeException(e);
}
}
// 新建任务
@PostMapping("newTask")
public AjaxResult newTask(@RequestParam("file") MultipartFile file) {
try {
String token = redisCache.getCacheMapValue(CacheConstants.MARK_ACCOUNT_KEY, "token");
if (token == null) token = markService.reg();
String data =String.format("{\"name\":\"%s\",\"type\":1}", file.getOriginalFilename()) ;
MultiValueMap<String, Object> formData = new LinkedMultiValueMap<>();
formData.add("c", "Create");
formData.add("t", token);
formData.add("ts",System.currentTimeMillis());
formData.add("d", data);
formData.add("s", markService.md5(System.currentTimeMillis() + 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("https://api.fangzhoujingxuan.com/Task", requestEntity, String.class);
JsonNode jsonNode = objectMapper.readTree(result);
if(jsonNode.get("S").asInt()==-1006){
token= markService.login();
formData.add("t", token);
requestEntity = new HttpEntity<>(formData, headers);
result = restTemplate.postForObject("https://api.fangzhoujingxuan.com/Task", requestEntity, String.class);
jsonNode= objectMapper.readTree(result);
}
return jsonNode.get("S").asInt()==1?AjaxResult.success(result): AjaxResult.error( jsonNode.get("S").asText());
} catch (Exception e) {
throw new RuntimeException(e);
return AjaxResult.error("获取 Token 失败: " + e.getMessage());
}
}
/**
* 品牌商标筛查(调用 erp_client_sb 服务)
* @param brands 品牌列表JSON数组
* @return 筛查结果
* 刷新 Token
* 使用已保存的账号密码重新登录,更新 Token
*
* @return 新的 Token
*/
@PostMapping("brandCheck")
public AjaxResult brandCheck(@RequestBody List<String> brands) {
@PostMapping("/refreshToken")
public AjaxResult refreshToken() {
try {
if (brands == null || brands.isEmpty()) {
return AjaxResult.error("品牌列表不能为空");
// 检查是否有账号信息
Map<String, String> accountData = redisCache.getCacheMap(CacheConstants.MARK_ACCOUNT_KEY);
if (accountData == null || accountData.isEmpty()) {
// 没有账号信息,需要先注册
String token = markService.reg();
return AjaxResult.success("账号不存在,已自动注册", token);
}
// 调用 erp_client_sb 的商标检查接口
String url = ERP_CLIENT_BASE_URL + "/api/trademark/brandCheck";
HttpHeaders headers = new HttpHeaders();
headers.setContentType(MediaType.APPLICATION_JSON);
// 使用现有账号重新登录
String token = markService.login();
return AjaxResult.success("Token 刷新成功", token);
HttpEntity<List<String>> requestEntity = new HttpEntity<>(brands, headers);
// 调用远程服务
String result = restTemplate.postForObject(url, requestEntity, String.class);
JsonNode jsonNode = objectMapper.readTree(result);
// 判断返回状态 (erp_client_sb 的 JsonData: code=0 成功, code=-1 失败)
if (jsonNode.get("code").asInt() == 0) {
// 转换数据格式以适配前端
JsonNode data = jsonNode.get("data");
return AjaxResult.success(objectMapper.convertValue(data, Map.class));
} else {
String msg = jsonNode.has("msg") ? jsonNode.get("msg").asText() : "品牌筛查失败";
return AjaxResult.error(msg);
}
} catch (Exception e) {
e.printStackTrace();
return AjaxResult.error("品牌筛查失败: " + e.getMessage());
return AjaxResult.error("刷新 Token 失败: " + e.getMessage());
}
}
/**
* 获取账号信息(用于调试)
*
* @return 账号信息(不含密码)
*/
@GetMapping("/accountInfo")
public AjaxResult getAccountInfo() {
try {
Map<String, String> accountData = redisCache.getCacheMap(CacheConstants.MARK_ACCOUNT_KEY);
if (accountData == null || accountData.isEmpty()) {
return AjaxResult.error("未找到账号信息");
}
// 不返回密码,仅返回账号和 Token 状态
String account = accountData.get("account");
String token = accountData.get("token");
boolean hasToken = token != null && !token.isEmpty();
return AjaxResult.success()
.put("account", account)
.put("hasToken", hasToken);
} catch (Exception e) {
return AjaxResult.error("获取账号信息失败: " + e.getMessage());
}
}
}

View File

@@ -20,17 +20,17 @@ spring:
username:
password:
# 初始连接数(增加预热连接)
initialSize: 8
initialSize: 2
# 最小连接池数量
minIdle: 10
minIdle: 2
# 最大连接池数量
maxActive: 25
# 配置获取连接等待超时的时间(5秒)
maxWait: 5000
# 配置连接超时时间(5秒)
connectTimeout: 5000
# 配置网络超时时间(5秒)
socketTimeout: 5000
maxActive: 10
# 配置获取连接等待超时的时间(10秒)
maxWait: 10000
# 配置连接超时时间(10秒)
connectTimeout: 10000
# 配置网络超时时间(10秒)
socketTimeout: 10000
# 配置间隔多久才进行一次检测检测需要关闭的空闲连接单位是毫秒30秒检测一次
timeBetweenEvictionRunsMillis: 30000
# 配置一个连接在池中最小生存的时间,单位是毫秒

View File

@@ -35,17 +35,17 @@ server:
# tomcat的URI编码
uri-encoding: UTF-8
# 连接数满后的排队数默认为100
accept-count: 1000
accept-count: 50
threads:
# tomcat最大线程数默认为200
max: 800
max: 30
# Tomcat启动初始化的线程数默认值10
min-spare: 100
min-spare: 5
# 日志配置
logging:
level:
com.ruoyi: debug
com.ruoyi: info
org.springframework: warn
# 用户配置
@@ -82,7 +82,7 @@ spring:
devtools:
restart:
# 热部署开关
enabled: true
enabled: false
# redis 配置
redis:
# 地址
@@ -99,13 +99,13 @@ spring:
lettuce:
pool:
# 连接池中的最小空闲连接(保持预热连接,避免临时建连)
min-idle: 5
min-idle: 1
# 连接池中的最大空闲连接
max-idle: 20
max-idle: 5
# 连接池的最大数据库连接数
max-active: 50
max-active: 10
# 连接池最大阻塞等待时间
max-wait: 10s
max-wait: 15s
# 关闭超时时间
shutdown-timeout: 100ms
# token配置
@@ -135,7 +135,7 @@ pagehelper:
# Swagger配置
swagger:
# 是否开启swagger
enabled: true
enabled: false
# 请求前缀
pathMapping: /dev-api