cr 18 цаг өмнө
parent
commit
7844e7c797

+ 5 - 0
blade-service/blade-manager/pom.xml

@@ -229,6 +229,11 @@
             <artifactId>jsoup</artifactId>
             <version>1.16.1</version>
         </dependency>
+        <dependency>
+            <groupId>com.github.ben-manes.caffeine</groupId>
+            <artifactId>caffeine</artifactId>
+            <version>2.9.3</version> <!-- 2.x版本适配Java 8,3.x仅支持Java 11+ -->
+        </dependency>
     </dependencies>
     <build>
         <plugins>

+ 259 - 57
blade-service/blade-manager/src/main/java/org/springblade/manager/controller/WbsTreePrivateController.java

@@ -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(); // 恢复中断状态
+        }
+    }
 
 
     /**