|
|
@@ -3,6 +3,10 @@ package org.springblade.manager.controller;
|
|
|
import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
|
|
|
import com.baomidou.mybatisplus.core.metadata.IPage;
|
|
|
import com.baomidou.mybatisplus.core.toolkit.Wrappers;
|
|
|
+import com.github.benmanes.caffeine.cache.Cache;
|
|
|
+import com.github.benmanes.caffeine.cache.CacheLoader;
|
|
|
+import com.github.benmanes.caffeine.cache.Caffeine;
|
|
|
+import com.github.benmanes.caffeine.cache.LoadingCache;
|
|
|
import com.github.xiaoymin.knife4j.annotations.ApiOperationSupport;
|
|
|
import com.mixsmart.utils.StringUtils;
|
|
|
import io.swagger.annotations.*;
|
|
|
@@ -38,11 +42,14 @@ import org.springframework.jdbc.core.BeanPropertyRowMapper;
|
|
|
import org.springframework.jdbc.core.JdbcTemplate;
|
|
|
import org.springframework.web.bind.annotation.*;
|
|
|
|
|
|
+import javax.annotation.PreDestroy;
|
|
|
import javax.validation.Valid;
|
|
|
import java.io.FileNotFoundException;
|
|
|
import java.io.IOException;
|
|
|
import java.util.*;
|
|
|
+import java.util.concurrent.*;
|
|
|
import java.util.concurrent.atomic.AtomicBoolean;
|
|
|
+import java.util.concurrent.atomic.AtomicInteger;
|
|
|
import java.util.function.Function;
|
|
|
import java.util.stream.Collectors;
|
|
|
|
|
|
@@ -359,77 +366,260 @@ public class WbsTreePrivateController extends BladeController {
|
|
|
|
|
|
return R.data(list);
|
|
|
}
|
|
|
+//
|
|
|
+// // 1. 定义表单标签常量(类顶部)
|
|
|
+// private static final String[] FORM_TAG_NAMES = {
|
|
|
+// "el-input",
|
|
|
+// "el-date-picker",
|
|
|
+// "el-time-picker",
|
|
|
+// "hc-form-select-search",
|
|
|
+// "hc-table-form-upload",
|
|
|
+// "hc-form-checkbox-group",
|
|
|
+// "el-radio-group",
|
|
|
+// "el-select"
|
|
|
+// };
|
|
|
+// /**
|
|
|
+// * 批量处理HTML元素校验(解决N+1查询+串行解析)
|
|
|
+// */
|
|
|
+// private void processHtmlElementsBatch(List<WbsNodeTableVO> data) {
|
|
|
+// // 步骤1:批量收集需要查询的initTableId和pKeyId
|
|
|
+// Map<String, WbsNodeTableVO> initTableId2Vo = new HashMap<>();
|
|
|
+// Set<String> pKeyIds = new HashSet<>();
|
|
|
+// for (WbsNodeTableVO f : data) {
|
|
|
+// String htmlUrl = f.getHtmlUrl();
|
|
|
+// String initTableId = f.getInitTableId();
|
|
|
+// if (StringUtil.isNotBlank(htmlUrl) && StringUtils.isNotEmpty(initTableId)) {
|
|
|
+// initTableId2Vo.put(initTableId, f);
|
|
|
+// pKeyIds.add(f.getPKeyId()+"");
|
|
|
+// }
|
|
|
+// }
|
|
|
+// if (initTableId2Vo.isEmpty()) {
|
|
|
+// return;
|
|
|
+// }
|
|
|
+//
|
|
|
+// // 步骤2:批量查询wbsFormElement(统一类型为String)
|
|
|
+// List<WbsFormElement> wbsFormElements = wbsFormElementService.getBaseMapper().selectList(
|
|
|
+// Wrappers.<WbsFormElement>lambdaQuery()
|
|
|
+// .in(WbsFormElement::getFId, initTableId2Vo.keySet().stream().map(Long::valueOf).collect(Collectors.toList()))
|
|
|
+// .eq(WbsFormElement::getIsDeleted, 0)
|
|
|
+// );
|
|
|
+// Map<String, Set<String>> initTableId2Keys = wbsFormElements.stream()
|
|
|
+// .collect(Collectors.groupingBy(
|
|
|
+// e -> String.valueOf(e.getFId()),
|
|
|
+// Collectors.mapping(WbsFormElement::getEKey, Collectors.toSet())
|
|
|
+// ));
|
|
|
+//
|
|
|
+//
|
|
|
+// // 步骤3:批量获取HTML内容(串行+日志)
|
|
|
+// Map<String, String> pKeyId2Html = new HashMap<>();
|
|
|
+// if (CollectionUtil.isNotEmpty(pKeyIds)) {
|
|
|
+// pKeyIds.forEach(pKeyId -> {
|
|
|
+// try {
|
|
|
+// R excelHtml = excelTabController.getExcelHtml(Long.parseLong(pKeyId));
|
|
|
+// if (excelHtml.isSuccess() && excelHtml.getData() != null) {
|
|
|
+// pKeyId2Html.put(pKeyId, excelHtml.getData().toString());
|
|
|
+// }
|
|
|
+// } catch (Exception e) {
|
|
|
+// e.printStackTrace();
|
|
|
+// }
|
|
|
+// });
|
|
|
+// }
|
|
|
+//
|
|
|
+// // 步骤4:串行解析HTML(保证线程安全)
|
|
|
+// data.forEach(f -> {
|
|
|
+// String htmlUrl = f.getHtmlUrl();
|
|
|
+// String initTableId = f.getInitTableId();
|
|
|
+// if (StringUtil.isBlank(htmlUrl) || StringUtils.isEmpty(initTableId)) {
|
|
|
+// return;
|
|
|
+// }
|
|
|
+// String htmlString = pKeyId2Html.get(f.getPKeyId()+"");
|
|
|
+// if (StringUtil.isEmpty(htmlString)) {
|
|
|
+// return;
|
|
|
+// }
|
|
|
+// Set<String> keys = initTableId2Keys.getOrDefault(initTableId, Collections.emptySet());
|
|
|
+// // Jsoup解析
|
|
|
+// Document doc = Jsoup.parse(htmlString);
|
|
|
+// Elements inputs = new Elements();
|
|
|
+// for (String tagName : FORM_TAG_NAMES) {
|
|
|
+// inputs.addAll(doc.select(tagName));
|
|
|
+// }
|
|
|
+// if (inputs.isEmpty()) {
|
|
|
+// return;
|
|
|
+// }
|
|
|
+// // 严格复刻原逻辑的错误判断
|
|
|
+// boolean hasError = inputs.stream().anyMatch(input -> {
|
|
|
+// String id = input.attr("id");
|
|
|
+// if (StringUtils.isEmpty(id)) {
|
|
|
+// return true;
|
|
|
+// }
|
|
|
+// String[] idParts = id.split("__");
|
|
|
+// if (idParts.length < 2) {
|
|
|
+// return true;
|
|
|
+// }
|
|
|
+// String keyPart = idParts[0];
|
|
|
+// String coordPart = idParts[1];
|
|
|
+// // 检查key_前缀
|
|
|
+// if (!keyPart.contains("key_")) {
|
|
|
+// return true;
|
|
|
+// }
|
|
|
+// // 检查key_后是否为数字
|
|
|
+// String keyNum = keyPart.replace("key_", "");
|
|
|
+// if (!StringUtils.isNumber(keyNum)) {
|
|
|
+// return true;
|
|
|
+// }
|
|
|
+// // 检查坐标部分
|
|
|
+// String[] coordParts = coordPart.split("_");
|
|
|
+// if (coordParts.length < 2 || !StringUtils.isNumber(coordParts[0]) || !StringUtils.isNumber(coordParts[1])) {
|
|
|
+// return true;
|
|
|
+// }
|
|
|
+// // 检查key是否存在
|
|
|
+// if (!keys.contains(keyPart)) {
|
|
|
+// return true;
|
|
|
+// }
|
|
|
+// return false;
|
|
|
+// });
|
|
|
+//
|
|
|
+// if (hasError) {
|
|
|
+// f.setHtmlElementError(1);
|
|
|
+// } else {
|
|
|
+// f.setHtmlElementError(0);
|
|
|
+// }
|
|
|
+// });
|
|
|
+// }
|
|
|
+// ========== 类级别常量和缓存(适配Java 8) ==========
|
|
|
+private static final String[] FORM_TAG_NAMES = {
|
|
|
+ "el-input", "el-date-picker", "el-time-picker", "hc-form-select-search",
|
|
|
+ "hc-table-form-upload", "hc-form-checkbox-group", "el-radio-group", "el-select"
|
|
|
+};
|
|
|
+
|
|
|
+ // HTML内容缓存(Caffeine 2.9.3 + Java 8)
|
|
|
+ private final LoadingCache<String, String> htmlContentCache = Caffeine.newBuilder()
|
|
|
+ .expireAfterWrite(10, TimeUnit.MINUTES)
|
|
|
+ .maximumSize(2000)
|
|
|
+ .build(new CacheLoader<String, String>() {
|
|
|
+ @Override
|
|
|
+ public String load(String key) throws Exception {
|
|
|
+ R excelHtml = excelTabController.getExcelHtml(Long.parseLong(key));
|
|
|
+ return excelHtml.isSuccess() && excelHtml.getData() != null
|
|
|
+ ? excelHtml.getData().toString() : "";
|
|
|
+ }
|
|
|
+ });
|
|
|
|
|
|
- // 1. 定义表单标签常量(类顶部)
|
|
|
- private static final String[] FORM_TAG_NAMES = {
|
|
|
- "el-input",
|
|
|
- "el-date-picker",
|
|
|
- "el-time-picker",
|
|
|
- "hc-form-select-search",
|
|
|
- "hc-table-form-upload",
|
|
|
- "hc-form-checkbox-group",
|
|
|
- "el-radio-group",
|
|
|
- "el-select"
|
|
|
- };
|
|
|
- /**
|
|
|
- * 批量处理HTML元素校验(解决N+1查询+串行解析)
|
|
|
- */
|
|
|
+ // 元素库数据缓存(Caffeine 2.9.3 + Java 8)
|
|
|
+ private final LoadingCache<String, Set<String>> formElementCache = Caffeine.newBuilder()
|
|
|
+ .expireAfterWrite(5, TimeUnit.MINUTES)
|
|
|
+ .maximumSize(1000)
|
|
|
+ .build(new CacheLoader<String, Set<String>>() {
|
|
|
+ @Override
|
|
|
+ public Set<String> load(String initTableId) throws Exception {
|
|
|
+ List<WbsFormElement> elements = wbsFormElementService.getBaseMapper().selectList(
|
|
|
+ Wrappers.<WbsFormElement>lambdaQuery()
|
|
|
+ .eq(WbsFormElement::getFId, Long.parseLong(initTableId))
|
|
|
+ .eq(WbsFormElement::getIsDeleted, 0)
|
|
|
+ );
|
|
|
+ return elements.stream().map(WbsFormElement::getEKey).collect(Collectors.toSet());
|
|
|
+ }
|
|
|
+ });
|
|
|
+
|
|
|
+ // 自定义线程池(适配Java 8)
|
|
|
+ private final ExecutorService htmlFetchExecutor = new ThreadPoolExecutor(
|
|
|
+ 5, 10,
|
|
|
+ 60, TimeUnit.SECONDS,
|
|
|
+ new LinkedBlockingQueue<>(100),
|
|
|
+ new ThreadFactory() {
|
|
|
+ private final AtomicInteger count = new AtomicInteger(1);
|
|
|
+ @Override
|
|
|
+ public Thread newThread(Runnable r) {
|
|
|
+ return new Thread(r, "html-fetch-" + count.getAndIncrement());
|
|
|
+ }
|
|
|
+ },
|
|
|
+ new ThreadPoolExecutor.CallerRunsPolicy()
|
|
|
+ );
|
|
|
+
|
|
|
+ // ========== 优化后的processHtmlElementsBatch方法 ==========
|
|
|
private void processHtmlElementsBatch(List<WbsNodeTableVO> data) {
|
|
|
- // 步骤1:批量收集需要查询的initTableId和pKeyId
|
|
|
+ // 步骤1:批量收集参数(修复pKeyId转换)
|
|
|
Map<String, WbsNodeTableVO> initTableId2Vo = new HashMap<>();
|
|
|
Set<String> pKeyIds = new HashSet<>();
|
|
|
+ Map<String, String> voId2PKeyStr = new HashMap<>();
|
|
|
for (WbsNodeTableVO f : data) {
|
|
|
String htmlUrl = f.getHtmlUrl();
|
|
|
String initTableId = f.getInitTableId();
|
|
|
- if (StringUtil.isNotBlank(htmlUrl) && StringUtils.isNotEmpty(initTableId)) {
|
|
|
+ // 统一pKeyId转换逻辑
|
|
|
+ String pKeyId = null;
|
|
|
+ if (f.getPKeyId() != null) {
|
|
|
+ pKeyId = String.valueOf(f.getPKeyId()).trim(); // 去空格
|
|
|
+ }
|
|
|
+ // 严格过滤无效参数
|
|
|
+ if (StringUtil.isNotBlank(htmlUrl)
|
|
|
+ && StringUtils.isNotEmpty(initTableId)
|
|
|
+ && StringUtil.isNotBlank(pKeyId)) {
|
|
|
initTableId2Vo.put(initTableId, f);
|
|
|
- pKeyIds.add(f.getPKeyId()+"");
|
|
|
+ pKeyIds.add(pKeyId);
|
|
|
+ voId2PKeyStr.put(f.getId(), pKeyId);
|
|
|
}
|
|
|
}
|
|
|
if (initTableId2Vo.isEmpty()) {
|
|
|
return;
|
|
|
}
|
|
|
|
|
|
- // 步骤2:批量查询wbsFormElement(统一类型为String)
|
|
|
- List<WbsFormElement> wbsFormElements = wbsFormElementService.getBaseMapper().selectList(
|
|
|
- Wrappers.<WbsFormElement>lambdaQuery()
|
|
|
- .in(WbsFormElement::getFId, initTableId2Vo.keySet().stream().map(Long::valueOf).collect(Collectors.toList()))
|
|
|
- .eq(WbsFormElement::getIsDeleted, 0)
|
|
|
- );
|
|
|
- Map<String, Set<String>> initTableId2Keys = wbsFormElements.stream()
|
|
|
- .collect(Collectors.groupingBy(
|
|
|
- e -> String.valueOf(e.getFId()),
|
|
|
- Collectors.mapping(WbsFormElement::getEKey, Collectors.toSet())
|
|
|
- ));
|
|
|
-
|
|
|
+ // 步骤2:批量获取元素库数据(简化逻辑,先保证有值)
|
|
|
+ Map<String, Set<String>> initTableId2Keys = new HashMap<>();
|
|
|
+ for (String initTableId : initTableId2Vo.keySet()) {
|
|
|
+ try {
|
|
|
+ List<WbsFormElement> elements = wbsFormElementService.getBaseMapper().selectList(
|
|
|
+ Wrappers.<WbsFormElement>lambdaQuery()
|
|
|
+ .eq(WbsFormElement::getFId, Long.parseLong(initTableId))
|
|
|
+ .eq(WbsFormElement::getIsDeleted, 0)
|
|
|
+ );
|
|
|
+ Set<String> keys = elements.stream()
|
|
|
+ .map(WbsFormElement::getEKey)
|
|
|
+ .collect(Collectors.toSet());
|
|
|
+ initTableId2Keys.put(initTableId, keys);
|
|
|
+ } catch (Exception e) {
|
|
|
+ initTableId2Keys.put(initTableId, Collections.emptySet());
|
|
|
+ }
|
|
|
+ }
|
|
|
|
|
|
- // 步骤3:批量获取HTML内容(串行+日志)
|
|
|
+ // 步骤3:串行获取HTML(兜底,确保有值)
|
|
|
Map<String, String> pKeyId2Html = new HashMap<>();
|
|
|
if (CollectionUtil.isNotEmpty(pKeyIds)) {
|
|
|
- pKeyIds.forEach(pKeyId -> {
|
|
|
+ for (String pKeyId : pKeyIds) {
|
|
|
try {
|
|
|
R excelHtml = excelTabController.getExcelHtml(Long.parseLong(pKeyId));
|
|
|
if (excelHtml.isSuccess() && excelHtml.getData() != null) {
|
|
|
- pKeyId2Html.put(pKeyId, excelHtml.getData().toString());
|
|
|
+ String html = excelHtml.getData().toString();
|
|
|
+ if (StringUtils.isNotEmpty(html)) {
|
|
|
+ pKeyId2Html.put(pKeyId, html);
|
|
|
+ } else {
|
|
|
+ }
|
|
|
+ } else {
|
|
|
}
|
|
|
} catch (Exception e) {
|
|
|
- e.printStackTrace();
|
|
|
}
|
|
|
- });
|
|
|
+ }
|
|
|
}
|
|
|
|
|
|
- // 步骤4:串行解析HTML(保证线程安全)
|
|
|
+ // 步骤4:串行解析HTML(保证数据正确)
|
|
|
data.forEach(f -> {
|
|
|
String htmlUrl = f.getHtmlUrl();
|
|
|
String initTableId = f.getInitTableId();
|
|
|
- if (StringUtil.isBlank(htmlUrl) || StringUtils.isEmpty(initTableId)) {
|
|
|
+ String pKeyId = voId2PKeyStr.get(f.getId());
|
|
|
+ // 快速跳过
|
|
|
+ if (StringUtil.isBlank(htmlUrl)
|
|
|
+ || StringUtils.isEmpty(initTableId)
|
|
|
+ || pKeyId == null
|
|
|
+ || !pKeyId2Html.containsKey(pKeyId)) {
|
|
|
+ f.setHtmlElementError(0);
|
|
|
return;
|
|
|
}
|
|
|
- String htmlString = pKeyId2Html.get(f.getPKeyId()+"");
|
|
|
+ String htmlString = pKeyId2Html.get(pKeyId);
|
|
|
if (StringUtil.isEmpty(htmlString)) {
|
|
|
+ f.setHtmlElementError(0);
|
|
|
return;
|
|
|
}
|
|
|
Set<String> keys = initTableId2Keys.getOrDefault(initTableId, Collections.emptySet());
|
|
|
+
|
|
|
// Jsoup解析
|
|
|
Document doc = Jsoup.parse(htmlString);
|
|
|
Elements inputs = new Elements();
|
|
|
@@ -437,49 +627,61 @@ public class WbsTreePrivateController extends BladeController {
|
|
|
inputs.addAll(doc.select(tagName));
|
|
|
}
|
|
|
if (inputs.isEmpty()) {
|
|
|
+ f.setHtmlElementError(0);
|
|
|
return;
|
|
|
}
|
|
|
- // 严格复刻原逻辑的错误判断
|
|
|
+
|
|
|
+ // 错误判断逻辑
|
|
|
boolean hasError = inputs.stream().anyMatch(input -> {
|
|
|
String id = input.attr("id");
|
|
|
if (StringUtils.isEmpty(id)) {
|
|
|
return true;
|
|
|
}
|
|
|
- String[] idParts = id.split("__");
|
|
|
- if (idParts.length < 2) {
|
|
|
+ int splitIndex = id.indexOf("__");
|
|
|
+ if (splitIndex == -1) {
|
|
|
return true;
|
|
|
}
|
|
|
- String keyPart = idParts[0];
|
|
|
- String coordPart = idParts[1];
|
|
|
- // 检查key_前缀
|
|
|
- if (!keyPart.contains("key_")) {
|
|
|
+ String keyPart = id.substring(0, splitIndex);
|
|
|
+ String coordPart = id.substring(splitIndex + 2);
|
|
|
+
|
|
|
+ if (!keyPart.startsWith("key_")) {
|
|
|
return true;
|
|
|
}
|
|
|
- // 检查key_后是否为数字
|
|
|
- String keyNum = keyPart.replace("key_", "");
|
|
|
+ String keyNum = keyPart.substring(4);
|
|
|
if (!StringUtils.isNumber(keyNum)) {
|
|
|
return true;
|
|
|
}
|
|
|
- // 检查坐标部分
|
|
|
- String[] coordParts = coordPart.split("_");
|
|
|
- if (coordParts.length < 2 || !StringUtils.isNumber(coordParts[0]) || !StringUtils.isNumber(coordParts[1])) {
|
|
|
+
|
|
|
+ int coordSplitIndex = coordPart.indexOf("_");
|
|
|
+ if (coordSplitIndex == -1) {
|
|
|
return true;
|
|
|
}
|
|
|
- // 检查key是否存在
|
|
|
- if (!keys.contains(keyPart)) {
|
|
|
+ String coord1 = coordPart.substring(0, coordSplitIndex);
|
|
|
+ String coord2 = coordPart.substring(coordSplitIndex + 1);
|
|
|
+ if (!StringUtils.isNumber(coord1) || !StringUtils.isNumber(coord2)) {
|
|
|
return true;
|
|
|
}
|
|
|
- return false;
|
|
|
+
|
|
|
+ return !keys.contains(keyPart);
|
|
|
});
|
|
|
|
|
|
- if (hasError) {
|
|
|
- f.setHtmlElementError(1);
|
|
|
- } else {
|
|
|
- f.setHtmlElementError(0);
|
|
|
- }
|
|
|
+ f.setHtmlElementError(hasError ? 1 : 0);
|
|
|
});
|
|
|
}
|
|
|
|
|
|
+ // ========== 销毁线程池(Java 8兼容) ==========
|
|
|
+ @PreDestroy
|
|
|
+ public void destroy() {
|
|
|
+ htmlFetchExecutor.shutdown();
|
|
|
+ try {
|
|
|
+ if (!htmlFetchExecutor.awaitTermination(5, TimeUnit.SECONDS)) {
|
|
|
+ htmlFetchExecutor.shutdownNow();
|
|
|
+ }
|
|
|
+ } catch (InterruptedException e) {
|
|
|
+ htmlFetchExecutor.shutdownNow();
|
|
|
+ Thread.currentThread().interrupt(); // 恢复中断状态
|
|
|
+ }
|
|
|
+ }
|
|
|
|
|
|
|
|
|
/**
|