Browse Source

Merge remote-tracking branch 'origin/test-merge' into test-merge

cr 19 hours ago
parent
commit
9b733c7785

+ 69 - 0
blade-service-api/blade-manager-api/src/main/java/org/springblade/manager/entity/InformationImportRecord.java

@@ -0,0 +1,69 @@
+package org.springblade.manager.entity;
+
+import com.baomidou.mybatisplus.annotation.IdType;
+import com.baomidou.mybatisplus.annotation.TableField;
+import com.baomidou.mybatisplus.annotation.TableId;
+import com.baomidou.mybatisplus.annotation.TableName;
+import io.swagger.annotations.ApiModel;
+import io.swagger.annotations.ApiModelProperty;
+import lombok.Data;
+
+import java.io.Serializable;
+import java.time.LocalDateTime;
+
+@Data
+@TableName("u_information_import_record")
+@ApiModel(value = "Dict对象", description = "Dict对象")
+public class InformationImportRecord implements Serializable {
+    private static final long serialVersionUID = 1L;
+
+    @ApiModelProperty("主键id")
+    @TableId(
+            value = "id",
+            type = IdType.ASSIGN_ID
+    )
+    private Long id;
+
+    @ApiModelProperty("导入文件名称")
+    private String fileName;
+
+    @ApiModelProperty("导入文件路径")
+    private String filePath;
+
+    @ApiModelProperty("合同段id")
+    private Long contractId;
+
+    @ApiModelProperty("节点id")
+    private Long nodeId;
+
+    @ApiModelProperty("目标节点id")
+    private Long targetId;
+
+    @ApiModelProperty("所属方")
+    private Integer classify;
+
+    @ApiModelProperty("导入时间")
+    private LocalDateTime createTime;
+
+    @ApiModelProperty("更新时间")
+    private LocalDateTime updateTime;
+
+    @ApiModelProperty("导入人")
+    private Long createUser;
+
+    @ApiModelProperty("状态")
+    private Integer status;
+
+    @ApiModelProperty("进度")
+    private Integer process;
+
+    @ApiModelProperty("备注")
+    private String remark;
+
+    @ApiModelProperty("是否删除")
+    private Integer isDeleted;
+
+    @ApiModelProperty("用户名")
+    @TableField(exist = false)
+    private String createUserName;
+}

+ 3 - 0
blade-service-api/blade-manager-api/src/main/java/org/springblade/manager/feign/ExcelTabClient.java

@@ -107,4 +107,7 @@ public interface ExcelTabClient {
      */
     @PostMapping(API_PREFIX + "/saveExcelTableLink")
     boolean saveExcelTableLink(@RequestBody ExcelEditCallback excelEditCallback);
+
+    @PostMapping(API_PREFIX + "/feign/copeBussTab")
+    R copeBussTab(@RequestParam Long pKeyId, @RequestParam String header) throws Exception;
 }

+ 4 - 1
blade-service-api/blade-manager-api/src/main/java/org/springblade/manager/feign/ExcelTabClientFallBack.java

@@ -112,5 +112,8 @@ public class ExcelTabClientFallBack implements ExcelTabClient {
         return false;
     }
 
-
+    @Override
+    public R copeBussTab(Long pKeyId, String header) throws Exception {
+        return null;
+    }
 }

+ 35 - 0
blade-service-api/blade-manager-api/src/main/java/org/springblade/manager/vo/InformationImportRecordVO.java

@@ -0,0 +1,35 @@
+package org.springblade.manager.vo;
+
+import com.fasterxml.jackson.annotation.JsonFormat;
+import io.swagger.annotations.ApiModelProperty;
+import lombok.Data;
+
+import java.io.Serializable;
+import java.time.LocalDateTime;
+
+@Data
+public class InformationImportRecordVO implements Serializable {
+    private static final long serialVersionUID = 1L;
+
+    @ApiModelProperty("文件名")
+    private String fileName;
+
+    @ApiModelProperty("开始时间")
+    private String startTime;
+
+    @ApiModelProperty("结束时间")
+    private String endTime;
+
+    @ApiModelProperty("状态,0:未开始, 1:进行中, 2:成功, 3:失败")
+    private Integer status;
+
+    @ApiModelProperty("操作人ID")
+    private Long userId;
+
+    @ApiModelProperty("所属方,1:施工, 2:监理")
+    private Integer classify = 1;
+
+    @ApiModelProperty("合同段ID, 必填")
+    private Long contractId;
+
+}

+ 714 - 0
blade-service/blade-manager/src/main/java/org/springblade/manager/controller/InformationImportRecordController.java

@@ -0,0 +1,714 @@
+package org.springblade.manager.controller;
+
+import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
+import com.baomidou.mybatisplus.core.metadata.IPage;
+import com.baomidou.mybatisplus.core.toolkit.ObjectUtils;
+import com.baomidou.mybatisplus.core.toolkit.Wrappers;
+import com.github.xiaoymin.knife4j.annotations.ApiOperationSupport;
+import io.swagger.annotations.*;
+import lombok.AllArgsConstructor;
+import org.apache.commons.io.IOUtils;
+import org.apache.commons.lang.StringUtils;
+import org.jetbrains.annotations.NotNull;
+import org.jsoup.Jsoup;
+import org.jsoup.nodes.Document;
+import org.jsoup.nodes.Element;
+import org.jsoup.select.Elements;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+import org.springblade.business.entity.InformationQuery;
+import org.springblade.common.constant.CommonConstant;
+import org.springblade.common.utils.SnowFlakeUtil;
+import org.springblade.core.boot.ctrl.BladeController;
+import org.springblade.core.log.exception.ServiceException;
+import org.springblade.core.mp.support.Condition;
+import org.springblade.core.mp.support.Query;
+import org.springblade.core.secure.utils.AuthUtil;
+import org.springblade.core.tool.api.R;
+import org.springblade.core.tool.utils.IoUtil;
+import org.springblade.core.tool.utils.ResourceUtil;
+import org.springblade.core.tool.utils.StringUtil;
+import org.springblade.manager.dto.ServiceUserDto;
+import org.springblade.manager.entity.*;
+import org.springblade.manager.feign.ExcelTabClient;
+import org.springblade.manager.feign.ExcelTabClientImpl;
+import org.springblade.manager.service.IExcelTabService;
+import org.springblade.manager.service.IWbsTreeContractService;
+import org.springblade.manager.service.InformationImportRecordService;
+import org.springblade.manager.service.impl.WbsTreeContractServiceImpl;
+import org.springblade.manager.util.DataStructureFormatUtils;
+import org.springblade.manager.utils.DuplicateSheetRecognizer;
+import org.springblade.manager.utils.FileUtils;
+import org.springblade.manager.vo.InformationImportRecordVO;
+import org.springblade.system.cache.ParamCache;
+import org.springblade.system.user.feign.IUserClient;
+import org.springframework.beans.BeanUtils;
+import org.springframework.jdbc.core.BeanPropertyRowMapper;
+import org.springframework.jdbc.core.JdbcTemplate;
+import org.springframework.scheduling.annotation.Async;
+import org.springframework.scheduling.annotation.Scheduled;
+import org.springframework.web.bind.annotation.*;
+import org.springframework.web.multipart.MultipartFile;
+
+import java.io.File;
+import java.io.FileInputStream;
+import java.io.FileNotFoundException;
+import java.io.InputStream;
+import java.nio.file.Files;
+import java.nio.file.Path;
+import java.nio.file.Paths;
+import java.text.SimpleDateFormat;
+import java.time.LocalDateTime;
+import java.util.*;
+import java.util.regex.Matcher;
+import java.util.regex.Pattern;
+import java.util.stream.Collectors;
+
+import static java.util.stream.Collectors.toMap;
+
+/**
+ * 资料导入及记录查询控制器
+ */
+@RestController
+@AllArgsConstructor
+@RequestMapping("/informationImportRecord")
+@Api(tags = "资料导入及记录查询接口")
+public class InformationImportRecordController extends BladeController {
+
+    private final IExcelTabService excelTabService;
+
+    private final IWbsTreeContractService wbsTreeContractService;
+    private final WbsTreeContractServiceImpl wbsTreeContractServiceImpl;
+    private final ExcelTabClient excelTabClient;
+    private final InformationImportRecordService informationImportRecordService;
+    private final IUserClient userClient;
+    private final JdbcTemplate jdbcTemplate;
+
+    private static final Logger logger = LoggerFactory.getLogger(InformationImportRecordController.class);
+    private static final java.util.concurrent.atomic.AtomicBoolean RUNNING = new java.util.concurrent.atomic.AtomicBoolean(false);
+    private static final java.util.concurrent.ExecutorService IMPORT_EXECUTOR = new java.util.concurrent.ThreadPoolExecutor(
+            Math.max(2, Runtime.getRuntime().availableProcessors()),
+            Math.max(4, Runtime.getRuntime().availableProcessors() * 2),
+            60L,
+            java.util.concurrent.TimeUnit.SECONDS,
+            new java.util.concurrent.LinkedBlockingQueue<>(200),
+            new java.util.concurrent.ThreadPoolExecutor.CallerRunsPolicy()
+    );
+
+
+    @GetMapping("/page")
+    @ApiOperationSupport(order = 1)
+    @ApiOperation(value = "获取资料导入记录")
+    public R<IPage<InformationImportRecord>> page(InformationImportRecordVO record, Query query) {
+        LambdaQueryWrapper<InformationImportRecord> wrapper = Wrappers.lambdaQuery();
+        wrapper.eq(InformationImportRecord::getContractId, record.getContractId());
+        if (StringUtil.hasText(record.getFileName())) {
+            wrapper.like(InformationImportRecord::getFileName, record.getFileName());
+        }
+        if (record.getStatus() != null && (record.getStatus() >= 0 && record.getStatus() <= 3)) {
+            wrapper.eq(InformationImportRecord::getStatus, record.getStatus());
+        }
+        if (record.getUserId() != null && record.getUserId() > 0) {
+            wrapper.eq(InformationImportRecord::getCreateUser, record.getUserId());
+        }
+        if (record.getClassify() != null && (record.getClassify() == 1 || record.getClassify() == 2)) {
+            wrapper.eq(InformationImportRecord::getClassify, record.getClassify());
+        }
+        if (record.getStartTime() != null && record.getEndTime() != null) {
+            if (record.getStartTime().trim().matches("^\\d{4}-\\d{2}-\\d{2}$") && record.getEndTime().trim().matches("^\\d{4}-\\d{2}-\\d{2}$")) {
+                wrapper.between(InformationImportRecord::getCreateTime, record.getStartTime() + " 00:00:00", record.getEndTime() + " 23:59:59");
+            }
+        }
+        wrapper.orderByDesc(InformationImportRecord::getCreateTime);
+        IPage<InformationImportRecord> page = informationImportRecordService.page(Condition.getPage(query), wrapper);
+        List<InformationImportRecord> records = page.getRecords();
+        if (records != null && !records.isEmpty()) {
+            String userIds = records.stream().filter(item -> item.getCreateUser() != null && item.getCreateUser() > 0).map(item -> item.getCreateUser() + "").collect(Collectors.joining(","));
+            List<ServiceUserDto> userList = jdbcTemplate.query("select id as userId, name as userName from blade_user where id in (" + userIds + ")", new BeanPropertyRowMapper<>(ServiceUserDto.class));
+            Map<String, String> map = userList.stream().collect(Collectors.toMap(ServiceUserDto::getUserId, ServiceUserDto::getUserName, (k1, k2) -> k1));
+            records.forEach(item -> {
+                String username = map.get(item.getCreateUser() + "");
+                item.setCreateUserName(username);
+                item.setRemark(item.getRemark() == null ? "" : item.getRemark().split("::")[0]);
+            });
+        }
+        return R.data(page);
+    }
+
+    @GetMapping("/users")
+    @ApiOperationSupport(order = 1)
+    @ApiOperation(value = "获取资料导入操作人列表", notes = "传入合同段id,返回 id, name")
+    public R<List<ServiceUserDto>> page(@RequestParam Long contractId) {
+        List<ServiceUserDto> query = jdbcTemplate.query("select distinct b.id as userId, b.name as userName from u_information_import_record a left join blade_user b on a.create_user = b.id where contract_id = " + contractId,
+                new BeanPropertyRowMapper<>(ServiceUserDto.class));
+        return R.data(query);
+    }
+
+    @PostMapping("/import")
+    @ApiOperationSupport(order = 2)
+    @ApiOperation(value = "客户端-导入多sheet excel到对应节点下的表单", notes = "传入节点ID、分类和多sheet excel文件")
+    public R<Object> importNodeExcel(@RequestPart List<MultipartFile> files, @RequestParam Long nodeId, @RequestParam Integer classify) throws Exception {
+        if (files.isEmpty()) {
+            return R.fail("请选择文件");
+        }
+        if (nodeId == null) {
+            return R.fail("请选择节点");
+        }
+        List<WbsTreeContract> query = jdbcTemplate.query("select * from m_wbs_tree_contract where is_deleted = 0 and p_key_id = " + nodeId, new BeanPropertyRowMapper<>(WbsTreeContract.class));
+        if (query.isEmpty()) {
+            return R.fail("请选择节点");
+        }
+        WbsTreeContract wbsTreeContract = query.get(0);
+        long contractId;
+        try {
+            contractId = Long.parseLong(wbsTreeContract.getContractId());
+        } catch (NumberFormatException e) {
+            return R.fail("未找到对应的合同段id");
+        }
+        if (classify == null || classify < 0 || classify > 2) {
+            return R.fail("请选择所属方");
+        }
+        String localPath = ParamCache.getValue(CommonConstant.SYS_LOCAL_URL);
+        String base = localPath + File.separator + "import" + File.separator;
+        {
+            // 如果文件夹不存在则创建
+            File file1 = new File(base);
+            if (!file1.exists()) {
+                boolean mkdirs = file1.mkdirs();
+                if (!mkdirs) {
+                    return R.fail("创建文件夹失败");
+                }
+            }
+        }
+        LocalDateTime now = LocalDateTime.now();
+        List<InformationImportRecord> records = new ArrayList<>();
+        for (MultipartFile file : files) {
+            // 将file保存到本地
+            Long id = SnowFlakeUtil.getId();
+            String filename = file.getOriginalFilename();
+            if (filename == null) {
+                continue;
+            }
+            InputStream is = file.getInputStream();
+            // 避免重名以及文件名过长的问题
+            String path = base + id + filename.substring(filename.lastIndexOf("."));
+
+            Files.write(Paths.get(path), IOUtils.toByteArray(is));
+
+            // 创建记录
+            InformationImportRecord record = new InformationImportRecord();
+            record.setId(id);
+            record.setNodeId(nodeId);
+            record.setContractId(contractId);
+            record.setClassify(classify);
+            record.setFileName(filename.substring(0, filename.lastIndexOf(".")));
+            record.setFilePath(path);
+            record.setCreateTime(now);
+            record.setCreateUser(AuthUtil.getUserId());
+            records.add(record);
+        }
+        informationImportRecordService.saveBatch(records);
+        importInformationData();
+        return R.data(true);
+    }
+
+    /**
+     * 初始化: 匹配节点,创建子节点
+     * @param id 记录ID
+     * @param node 节点
+     */
+    public void init(@NotNull Long id, @NotNull WbsTreeContract node) {
+        InformationImportRecord record = informationImportRecordService.getById(id);
+        if (record == null || record.getStatus() != 0) {
+            logger.info("记录[{}]已经初始化,已跳过", id);
+            return;
+        }
+        String name = record.getFileName();
+        String[] split = name.split("》");
+        String lastName = split[split.length - 1];
+        String newName = "";
+        String[] split1 = lastName.split("【");
+        if (split1.length  > 1) {
+            lastName = split1[0];
+            newName = split1[1].replace("】", "");
+        }
+        WbsTreeContract target = null;
+        if (split.length > 1) {
+            // 获取节点下的所有子节点
+            Long pId = node.getPKeyId();
+            int i = 0;
+            for (; i < split.length - 1; i++) {
+                List<WbsTreeContract> query = jdbcTemplate.query("select * from m_wbs_tree_contract where is_deleted = 0 and p_id = " + pId + " and full_name = '" + split[i] + "'",
+                        new BeanPropertyRowMapper<>(WbsTreeContract.class));
+                if (query.isEmpty() || query.get(0) == null) {
+                    break;
+                }
+                target = query.get(0);
+                pId = target.getPKeyId();
+                if (pId == null ) {
+                    break;
+                }
+            }
+            if (target == null || i < split.length - 2) {
+                informationImportRecordService.updateProcess(record.getId(), null, 3, "节点未找到-客户端::" + split[i], record.getUpdateTime());
+                return;
+            }
+        } else {
+            target = node;
+        }
+
+        {
+            // todo 判断target节点下是否有该节点,如果有是否有资料,没有的 监理和施工
+            List<WbsTreeContract> query = jdbcTemplate.query("select * from m_wbs_tree_contract where is_deleted = 0 and p_id = " + target.getPKeyId() + " and full_name = '" + (StringUtil.hasText(newName) ? newName : lastName) + "'",
+                    new BeanPropertyRowMapper<>(WbsTreeContract.class));
+            if (!query.isEmpty()) {
+                for (WbsTreeContract wbsTreeContract : query) {
+                    List<InformationQuery> informationQueries = jdbcTemplate.query("select * from u_information_query where is_deleted = 0 and contract_id = " + record.getContractId() + " and wbs_id = " + wbsTreeContract.getPKeyId()
+                                    + " and classify = " + record.getClassify(), new BeanPropertyRowMapper<>(InformationQuery.class));
+                    if (informationQueries.isEmpty()) {
+                        record.setTargetId(wbsTreeContract.getPKeyId());
+                    }
+                }
+            }
+            if (record.getTargetId() != null && record.getTargetId() > 0) {
+                informationImportRecordService.updateProcess(record.getId(), 20, 1, null, record.getUpdateTime());
+                return;
+            }
+        }
+
+        if (target.getIsTypePrivatePid() == null) {
+            informationImportRecordService.updateProcess(record.getId(), null, 3, "节点未找到-后管::" + target.getFullName(), record.getUpdateTime());
+            return;
+        }
+        List<WbsTreePrivate> query = jdbcTemplate.query("select * from m_wbs_tree_private where is_deleted = 0 and p_id = " + target.getIsTypePrivatePid() + " and node_name = '" + lastName + "' limit 1",
+                new BeanPropertyRowMapper<>(WbsTreePrivate.class));
+        if (query.isEmpty()) {
+            informationImportRecordService.updateProcess(record.getId(), null, 3, "节点未找到-后管::" + target.getFullName(), record.getUpdateTime());
+            return;
+        }
+        informationImportRecordService.updateProcess(record.getId(), 7, 1, null, record.getUpdateTime());
+        WbsTreePrivate wbsTreePrivate = query.get(0);
+        List<WbsTreePrivate> tables = jdbcTemplate.query("select * from m_wbs_tree_private where is_deleted = 0 and p_id = " + wbsTreePrivate.getPKeyId(), new BeanPropertyRowMapper<>(WbsTreePrivate.class));
+
+        // 在target节点下创建子节点及表单
+        Long parentPKeyId = SnowFlakeUtil.getId();
+        Long parentId = SnowFlakeUtil.getId();
+        String ancestors = target.getAncestors() == null ? target.getId() + "" : target.getAncestors() + "," + target.getId();
+        String ancestorsPId = target.getAncestorsPId() == null ? target.getPKeyId() + "" : target.getAncestorsPId() + "," + target.getPKeyId();
+
+        List<WbsTreeContract> saveList = new ArrayList<>();
+        {
+            WbsTreeContract newData = new WbsTreeContract();
+            BeanUtils.copyProperties(wbsTreePrivate, newData);
+
+            //重塑pKeyId、id和parentId
+            newData.setPKeyId(parentPKeyId);
+            newData.setId(parentId);
+            newData.setParentId(target.getId());
+            newData.setPId(target.getPKeyId());
+            newData.setAncestors(ancestors);
+            newData.setAncestorsPId(ancestorsPId);
+
+            //设置合同段等信息
+            newData.setWbsType(target.getWbsType());
+            newData.setContractId(target.getContractId());
+            newData.setContractIdRelation(target.getContractIdRelation());
+            newData.setContractType(target.getContractType());
+            newData.setCreateTime(new Date());
+            newData.setIsTypePrivatePid(wbsTreePrivate.getPKeyId());
+            if (wbsTreePrivate.getType() != null && new Integer("2").equals(wbsTreePrivate.getType())) {
+                newData.setIsBussShow(1);
+            }
+            //获取当前所有复制的节点的最大sort
+            newData.setSort(ObjectUtils.isNotEmpty(wbsTreePrivate.getSort()) ? wbsTreePrivate.getSort() : 0);
+
+            //记录旧ID
+            newData.setOldId(wbsTreePrivate.getId().toString());
+
+            newData.setFullName(StringUtil.hasText(newName) ? newName : lastName);
+
+            saveList.add(newData);
+        }
+
+        //处理数据
+        for (WbsTreePrivate half : tables) {
+            //处理合同段数据
+            WbsTreeContract newData = new WbsTreeContract();
+            BeanUtils.copyProperties(half, newData);
+
+            //重塑pKeyId、id和parentId
+            newData.setPKeyId(SnowFlakeUtil.getId());
+            newData.setId(SnowFlakeUtil.getId());
+            newData.setParentId(parentId);
+            newData.setPId(parentPKeyId);
+            newData.setAncestors(ancestors + "," + parentId);
+            newData.setAncestorsPId(ancestorsPId + "," + parentPKeyId);
+
+            //记录旧ID
+            newData.setOldId(half.getId().toString());
+            //设置合同段等信息
+            newData.setWbsType(target.getWbsType());
+            newData.setContractId(target.getContractId());
+            newData.setContractIdRelation(target.getContractIdRelation());
+            newData.setContractType(target.getContractType());
+            newData.setCreateTime(new Date());
+            newData.setIsTypePrivatePid(half.getPKeyId());
+            if (half.getType() != null && new Integer("2").equals(half.getType())) {
+                if (half.getDefaultConceal() != null &&  half.getDefaultConceal() == 1) {
+                    // 后续如果此表格中有数据再修改成 1
+                    newData.setIsBussShow(2);
+                } else {
+                    //2023年8月1日14:41:03更改需求,isBussShow默认=1
+                    newData.setIsBussShow(1);
+                }
+            }
+            //获取当前所有复制的节点的最大sort
+            newData.setSort(ObjectUtils.isNotEmpty(half.getSort()) ? half.getSort() : 0);
+
+            //设置名称, 后续根据sheet 名称进行设置
+            newData.setFullName(half.getFullName());
+
+            //设置到保存集合中
+            saveList.add(newData);
+        }
+        wbsTreeContractService.saveBatch(saveList);
+        informationImportRecordService.update(Wrappers.<InformationImportRecord>lambdaUpdate().eq(InformationImportRecord::getId, record.getId())
+                .set(InformationImportRecord::getTargetId, parentPKeyId).set(InformationImportRecord::getProcess, 20));
+    }
+
+    public void importData(@NotNull Long id) {
+        InformationImportRecord record = informationImportRecordService.getById(id);
+        if (record == null || record.getStatus() != 1) {
+            logger.info("记录[{}]已经不存在或者状态错误,已跳过", id);
+            return;
+        }
+        if (record.getTargetId() == null) {
+            informationImportRecordService.updateProcess(record.getId(), null, 3, record.getRemark() + "::找不到目标节点", record.getUpdateTime());
+            return;
+        }
+
+        Path path1 = Paths.get(record.getFilePath());
+        if (!Files.exists(path1)) {
+            logger.info("文件[{}]不存在,已跳过", record.getFilePath());
+            informationImportRecordService.updateProcess(record.getId(), null, 3,  "文件不存在", record.getUpdateTime());
+            return;
+        }
+        try (InputStream is = Files.newInputStream(path1)) {
+            importNodeExcel(is, record);
+            informationImportRecordService.updateProcess(record.getId(), 65, null, null, record.getUpdateTime());
+        } catch (Exception e) {
+            logger.error("文件[{}]读取异常,已跳过", record.getFilePath(), e);
+            informationImportRecordService.updateProcess(record.getId(), null, 3, "文件读取异常", record.getUpdateTime());
+        }
+    }
+
+    public void saveAgain(Long  id, String projectId) {
+        InformationImportRecord record = informationImportRecordService.getById(id);
+        if (record == null || record.getStatus() != 1) {
+            logger.info("记录[{}]已经不存在或者状态错误,已跳过", id);
+            return;
+        }
+        if (record.getTargetId() == null) {
+            informationImportRecordService.updateProcess(record.getId(), null, 3, record.getRemark() + "::找不到目标节点", record.getUpdateTime());
+            return;
+        }
+        try {
+            excelTabClient.synPDFInfo(record.getContractId() + "", record.getTargetId() + "", record.getClassify() + "", projectId, userClient.getTokenByUser("admin"));
+        } catch (Exception e) {
+            logger.error("生成pdf失败", e);
+            informationImportRecordService.updateProcess(record.getId(), null, 3, record.getRemark() + "::生成pdf失败", record.getUpdateTime());
+            return;
+        }
+        // 检查是否成功
+        List<InformationQuery> query = jdbcTemplate.query("select * from u_information_query where contract_id = " + record.getContractId() + " and wbs_id = " + record.getTargetId() + " and classify = " + record.getClassify(),
+                new BeanPropertyRowMapper<>(InformationQuery.class));
+        if (!query.isEmpty()) {
+            informationImportRecordService.updateProcess(record.getId(), 100, 2, null, record.getUpdateTime());
+        }
+    }
+
+    public void importNodeExcel(InputStream is, InformationImportRecord record) throws Exception {
+
+        // 1. 获取节点下所有表单,并建立 sheet名 -> WbsTreeContract 的映射(处理特殊字符)
+        Object[] params;
+        if (record.getClassify() == 1) {
+            params = new Object[]{record.getTargetId(), 1, 2, 3};
+        } else {
+            params = new Object[]{record.getTargetId(), 4, 5, 6};
+        }
+        List<WbsTreeContract> wbsTreeContracts = jdbcTemplate.query(
+                "select * from m_wbs_tree_contract where p_id = ? and is_deleted = 0 and status!=0 and type = 2 and table_owner in (?,?,?) order by sort",
+                new BeanPropertyRowMapper<>(WbsTreeContract.class),
+                params);
+
+        if (wbsTreeContracts.isEmpty()) {
+            informationImportRecordService.updateProcess(record.getId(), null, 3, record.getRemark() + "::没有找到对应的表单数据", record.getUpdateTime());
+            return;
+        }
+        // 2. 加载上传的多sheet Excel文件
+        com.spire.xls.Workbook mainWorkbook = new com.spire.xls.Workbook();
+        try {
+            mainWorkbook.loadFromStream(is);
+        } catch (Exception e) {
+            logger.error("加载Excel文件失败", e);
+            informationImportRecordService.updateProcess(record.getId(), null, 3, record.getRemark() + "::Excel文件解析失败", record.getUpdateTime());
+            return ;
+        }
+
+        // 3. 遍历所有sheet,逐个处理
+        int sheetCount = mainWorkbook.getWorksheets().getCount();
+        // 处理表单名称(与下载时的sheet名处理逻辑一致,确保匹配)
+        Map<String, WbsTreeContract> nodeNameToContractMap = new HashMap<>();
+        for (WbsTreeContract contract : wbsTreeContracts) {
+            String processedNodeName = contract.getNodeName().replaceAll("[\\\\/:*?\"<>|]", "_").trim();
+            nodeNameToContractMap.put(processedNodeName, contract);
+        }
+        if(wbsTreeContracts.size()<sheetCount){
+            for (int i = 0; i < sheetCount; i++) {
+                com.spire.xls.Worksheet sheet = mainWorkbook.getWorksheets().get(i);
+                String sheetName = sheet.getName();
+                String processedSheetName = sheetName.replaceAll("[\\\\/:*?\"<>|]", "_").trim(); // 处理sheet名特殊字符
+                // 1. 识别当前sheet是否为复制表
+                DuplicateSheetRecognizer.DuplicateSheetResult result =
+                        DuplicateSheetRecognizer.recognize(processedSheetName);
+                if (result.isDuplicate()) {
+                    String originalName = result.getOriginalName();
+                    if(nodeNameToContractMap.containsKey(originalName)){
+                        WbsTreeContract contract = nodeNameToContractMap.get(originalName);
+                        R r = excelTabClient.copeBussTab(contract.getPKeyId(), userClient.getTokenByUser("admin"));
+                        if(r.isSuccess()){
+                            WbsTreeContract data = (WbsTreeContract) r.getData();
+                            nodeNameToContractMap.put(data.getNodeName(), data);
+                            sheet.setName(data.getNodeName());
+                        }
+                    }
+
+                }
+            }
+        }
+        for (int i = 0; i < sheetCount; i++) {
+            com.spire.xls.Worksheet sheet = mainWorkbook.getWorksheets().get(i);
+            String sheetName = sheet.getName();
+            String processedSheetName = sheetName.replaceAll("[\\\\/:*?\"<>|]", "_").trim(); // 处理sheet名特殊字符
+
+            // 匹配对应的表单
+            WbsTreeContract matchedContract = nodeNameToContractMap.get(processedSheetName);
+            if (matchedContract == null) {
+                logger.warn("sheet名[{}]未匹配到任何表单,已跳过", sheetName);
+                continue;
+            }
+
+            // 获取当前表单的pkeyId
+            String pkeyId = matchedContract.getPKeyId() + "";
+            logger.info("开始处理sheet[{}],对应表单pkeyId[{}]", sheetName, pkeyId);
+
+            // 4. 将当前sheet保存为临时Excel文件(模拟单个文件上传)
+            File tempFile = null;
+            InputStream tempInputStream = null;
+            try {
+                // 创建临时文件
+                tempFile = File.createTempFile("sheet_", ".xlsx");
+                // 创建仅包含当前sheet的新工作簿
+                com.spire.xls.Workbook singleSheetWorkbook = new com.spire.xls.Workbook();
+                singleSheetWorkbook.getWorksheets().clear();
+                singleSheetWorkbook.getWorksheets().addCopy(sheet); // 复制当前sheet到新工作簿
+                singleSheetWorkbook.saveToFile(tempFile.getAbsolutePath(), com.spire.xls.FileFormat.Version2016);
+                singleSheetWorkbook.dispose();
+
+                // 读取临时文件作为输入流,调用单表单导入逻辑
+                tempInputStream = new FileInputStream(tempFile);
+                Map<String, Object> sheetResult = processSingleSheetImport(pkeyId, tempInputStream);
+                if(sheetResult.isEmpty()){
+                    continue;
+                }
+                Map<String, String> dataMap = WbsTreeContractController.getDataMap(sheetResult);
+                for (Map.Entry<String, String> entry : dataMap.entrySet()) {
+                    String value = entry.getValue();
+                    if (value != null) {
+                        value = value.replace("'", "''");
+                        entry.setValue(value);
+                    }
+                }
+                String delSql = "delete from " + matchedContract.getInitTableName() + " where p_key_id=" + matchedContract.getPKeyId();
+                dataMap.put("p_key_id", matchedContract.getPKeyId()+"");
+                String  sqlInfo = excelTabService.buildMTableInsertSql(matchedContract.getInitTableName(), dataMap, SnowFlakeUtil.getId(), null, null).toString();
+                jdbcTemplate.execute(delSql);
+                jdbcTemplate.execute(sqlInfo);
+            } catch (Exception e) {
+                e.printStackTrace();
+                throw new ServiceException("处理sheet[" + sheetName + "]失败:" + e.getMessage());
+            } finally {
+                // 关闭流并删除临时文件
+                if (tempInputStream != null) {
+                    tempInputStream.close();
+                }
+                if (tempFile != null && !tempFile.delete()) {
+                    logger.warn("临时文件[{}]删除失败", tempFile.getAbsolutePath());
+                }
+            }
+        }
+        mainWorkbook.dispose();
+    }
+
+    /**
+     * 复用importExcel的核心逻辑,处理单个sheet的导入
+     * 抽取自原importExcel方法,参数改为pkeyId和输入流
+     */
+    private Map<String, Object> processSingleSheetImport(String pkeyId, InputStream inputStream) throws Exception {
+        // 获取当前表htmlString(模板)
+        String htmlString_1 = wbsTreeContractServiceImpl.getHtmlString(pkeyId);
+        if (StringUtils.isEmpty(htmlString_1)) {
+            throw new ServiceException("获取表单[" + pkeyId + "]的html模板失败");
+        }
+
+        // 结果集
+        Map<String, Object> resultMap = new HashMap<>();
+
+        // 日期格式正则(复用原逻辑)
+        String doubleSlashRegex_XG = ".*\\/[^\\/]*\\/.*";
+        String dateFormatRegex_yyyyMdd = "\\d{4}/\\d{1,2}/\\d{1,2}";
+        String dateFormatRegex_yyyyMMdd = "\\d{4}/\\d{2}/\\d{2}";
+        String dateFormatRegex_chinese = "(\\d{4}年\\d{1,2}月\\d{1,2}日|\\d{4}年\\d{2}月\\d{2}日)";
+        SimpleDateFormat inputDateFormat = new SimpleDateFormat("yyyy/M/dd");
+        SimpleDateFormat outputDateFormat = new SimpleDateFormat("yyyy年MM月dd日");
+
+        // 临时文件路径(复用原逻辑)
+        Long id = SnowFlakeUtil.getId();
+        String importExcelFilePath = FileUtils.getSysLocalFileUrl();
+        String importExcelTOHtmlPath = importExcelFilePath + "/pdf//" + id + ".html";
+
+        com.spire.xls.Workbook workbook = null;
+        try {
+
+            // 导入的excel转换为html(复用原逻辑)
+            workbook = new com.spire.xls.Workbook();
+            workbook.loadFromHtml(inputStream); // 加载单个sheet的输入流
+            workbook.saveToFile(importExcelTOHtmlPath, com.spire.xls.FileFormat.HTML);
+            com.spire.xls.Worksheet sheet = workbook.getWorksheets().get(0);
+
+            // 获取转换后的html路径
+            String url_1 = importExcelTOHtmlPath.split("pdf//")[0];
+            String excelToHtmlFileUrl = url_1 + "/pdf/" + id + "_files/" + sheet.getName() + ".html";
+            String htmlString_2 = IoUtil.readToString(new FileInputStream(ResourceUtil.getFile(excelToHtmlFileUrl)));
+
+            // 解析两张html的tr、td(复用原逻辑)
+            Document doc_1 = Jsoup.parse(htmlString_1); // 模板html
+            Document doc_2 = Jsoup.parse(htmlString_2); // 导入的excel转换的html
+            Elements trElements1 = doc_1.select("table tbody tr");
+            Elements trElements2 = doc_2.select("table tbody tr");
+
+            for (int i = 0; i < trElements1.size(); i++) {
+                Element tr1 = trElements1.get(i);
+                Element tr2 = trElements2.size() > i ? trElements2.get(i) : null;
+                if (tr2 == null) break;
+
+                Elements tdElements1 = tr1.select("td");
+                Elements tdElements2 = tr2.select("td");
+
+                for (int j = 0; j < tdElements1.size(); j++) {
+                    Element td1 = tdElements1.get(j);
+                    if (td1.attr("dqid").length() > 0) { // 跳过包含dqid的td
+                        continue;
+                    }
+                    // 跳过包含hc-table-form-upload子元素的td
+                    if (!td1.select("hc-table-form-upload").isEmpty()) {
+                        continue;
+                    }
+
+                    Element td2 = tdElements2.size() > j ? tdElements2.get(j) : null;
+                    if (td2 == null) break;
+
+                    String keyName = WbsTreeContractController.getKeyNameFromChildElement(td1); // 复用原方法获取key
+                    if (StringUtils.isNotEmpty(keyName)) {
+                        String divValue = td2.text();
+                        if (StringUtils.isNotEmpty(divValue)) {
+                            // 日期范围处理
+                            if (WbsTreeContractController.parseDateRange(divValue).size() == 2) {
+                                resultMap.put(keyName, WbsTreeContractController.parseDateRange(divValue));
+                                continue;
+                            }
+
+                            // 日期格式转换(复用原逻辑)
+                            Pattern pattern_XG = Pattern.compile(doubleSlashRegex_XG);
+                            Matcher matcher_XG = pattern_XG.matcher(divValue);
+                            if (matcher_XG.matches()) {
+                                Pattern pattern_yyyyMdd = Pattern.compile(dateFormatRegex_yyyyMdd);
+                                Pattern pattern_yyyyMMdd = Pattern.compile(dateFormatRegex_yyyyMMdd);
+                                Matcher matcher_yyyyMdd = pattern_yyyyMdd.matcher(divValue);
+                                Matcher matcher_yyyyMMdd = pattern_yyyyMMdd.matcher(divValue);
+
+                                if (matcher_yyyyMdd.matches() || matcher_yyyyMMdd.matches()) {
+                                    Date date = inputDateFormat.parse(divValue);
+                                    divValue = outputDateFormat.format(date);
+                                }
+                            } else if (divValue.contains("年") && divValue.contains("月") && divValue.contains("日")) {
+                                Pattern pattern_chinese = Pattern.compile(dateFormatRegex_chinese);
+                                Matcher matcher_chinese = pattern_chinese.matcher(divValue);
+                                if (matcher_chinese.matches()) {
+                                    Date date = outputDateFormat.parse(divValue);
+                                    divValue = outputDateFormat.format(date);
+                                }
+                            }
+
+                            resultMap.put(keyName, divValue);
+                        }
+                    }
+                }
+            }
+        } finally {
+            if (workbook != null) {
+                workbook.dispose();
+            }
+        }
+        return resultMap;
+    }
+
+
+    @Scheduled(fixedDelay = 1000L * 60 * 10)
+    @Async
+    public void importInformationData() {
+        if (!RUNNING.compareAndSet(false, true)) {
+            return;
+        }
+        try {
+            List<InformationImportRecord> records = informationImportRecordService.list(Wrappers.<InformationImportRecord>lambdaQuery()
+                            .select(InformationImportRecord::getId, InformationImportRecord::getNodeId, InformationImportRecord::getUpdateTime, InformationImportRecord::getProcess)
+                    .in(InformationImportRecord::getStatus, 0, 1).last(" order by create_time  limit 100"));
+            if (!records.isEmpty()) {
+                for (InformationImportRecord record : records) {
+                    IMPORT_EXECUTOR.submit(() -> {
+                        try {
+                            WbsTreeContract node = wbsTreeContractService.getOne(Wrappers.<WbsTreeContract>lambdaQuery().eq(WbsTreeContract::getPKeyId, record.getNodeId()));
+                            if (node == null) {
+                                informationImportRecordService.updateProcess(record.getId(), null, 3, "节点未找到-客户端", record.getUpdateTime());
+                                return;
+                            }
+                            if (record.getProcess() <= 25) {
+                                init(record.getId(), node);
+                            }
+                            if (record.getProcess() < 70) {
+                                importData(record.getId());
+                            }
+                            if (record.getProcess() < 100) {
+                                saveAgain(record.getId(), node.getProjectId());
+                            }
+                        } catch (Exception e) {
+                            logger.error("导入数据失败", e);
+                        }
+                    });
+                }
+            }
+            List<InformationImportRecord> list = informationImportRecordService.list(Wrappers.<InformationImportRecord>lambdaQuery().select(InformationImportRecord::getId, InformationImportRecord::getFilePath)
+                    .eq(InformationImportRecord::getStatus, 2).apply("update_time < DATE_SUB(NOW(), INTERVAL 3 DAY)"));
+            for (InformationImportRecord record : list) {
+                try {
+                    Files.deleteIfExists(Paths.get(record.getFilePath()));
+                } catch (Exception e) {
+                    logger.error("删除文件失败", e);
+                }
+            }
+        } finally {
+            RUNNING.set(false);
+        }
+    }
+}

+ 2 - 2
blade-service/blade-manager/src/main/java/org/springblade/manager/controller/WbsTreeContractController.java

@@ -1306,7 +1306,7 @@ public class WbsTreeContractController extends BladeController {
         return sql;
     }
 
-    public Map<String,String> getDataMap(Map<String, Object> originalMap){
+    public static Map<String,String> getDataMap(Map<String, Object> originalMap){
         // 用于存储合并后的结果
         Map<String, String> mergedMap = new HashMap<>();
 
@@ -2007,7 +2007,7 @@ public class WbsTreeContractController extends BladeController {
         return false;
     }
 
-    private static String getKeyNameFromChildElement(Element element) {
+    public static String getKeyNameFromChildElement(Element element) {
         //TODO Element UI的时间标签待补全
         String[] tagNames = {"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"};
         for (String tagName : tagNames) {

+ 5 - 0
blade-service/blade-manager/src/main/java/org/springblade/manager/feign/ExcelTabClientImpl.java

@@ -897,4 +897,9 @@ public class ExcelTabClientImpl implements ExcelTabClient {
         return R.success("成功");
     }
 
+    @Override
+    public R copeBussTab(Long pKeyId, String header) throws Exception {
+        return excelTabController.copeBussTab(pKeyId);
+    }
+
 }

+ 26 - 0
blade-service/blade-manager/src/main/java/org/springblade/manager/mapper/InformationImportRecordMapper.java

@@ -0,0 +1,26 @@
+/*
+ *      Copyright (c) 2018-2028, Chill Zhuang All rights reserved.
+ *
+ *  Redistribution and use in source and binary forms, with or without
+ *  modification, are permitted provided that the following conditions are met:
+ *
+ *  Redistributions of source code must retain the above copyright notice,
+ *  this list of conditions and the following disclaimer.
+ *  Redistributions in binary form must reproduce the above copyright
+ *  notice, this list of conditions and the following disclaimer in the
+ *  documentation and/or other materials provided with the distribution.
+ *  Neither the name of the dreamlu.net developer nor the names of its
+ *  contributors may be used to endorse or promote products derived from
+ *  this software without specific prior written permission.
+ *  Author: Chill 庄骞 (smallchill@163.com)
+ */
+package org.springblade.manager.mapper;
+
+
+import com.baomidou.mybatisplus.core.mapper.BaseMapper;
+import org.springblade.manager.entity.InformationImportRecord;
+
+public interface InformationImportRecordMapper extends BaseMapper<InformationImportRecord> {
+
+
+}

+ 2 - 0
blade-service/blade-manager/src/main/java/org/springblade/manager/service/IExcelTabService.java

@@ -235,4 +235,6 @@ public interface IExcelTabService extends BaseService<ExcelTab> {
     void setAutomatic(Long pkeyId, String string, Document doc);
 
     Map<String, String> getFormulaData(JSONArray dataArray, WbsTreeContract wbsTreeTable);
+
+    StringBuilder buildMTableInsertSql(String tabName, Map<String, String> dataMap2, Object id, Object groupId, Object pKeyId);
 }

+ 30 - 0
blade-service/blade-manager/src/main/java/org/springblade/manager/service/InformationImportRecordService.java

@@ -0,0 +1,30 @@
+/*
+ *      Copyright (c) 2018-2028, Chill Zhuang All rights reserved.
+ *
+ *  Redistribution and use in source and binary forms, with or without
+ *  modification, are permitted provided that the following conditions are met:
+ *
+ *  Redistributions of source code must retain the above copyright notice,
+ *  this list of conditions and the following disclaimer.
+ *  Redistributions in binary form must reproduce the above copyright
+ *  notice, this list of conditions and the following disclaimer in the
+ *  documentation and/or other materials provided with the distribution.
+ *  Neither the name of the dreamlu.net developer nor the names of its
+ *  contributors may be used to endorse or promote products derived from
+ *  this software without specific prior written permission.
+ *  Author: Chill 庄骞 (smallchill@163.com)
+ */
+package org.springblade.manager.service;
+
+import com.baomidou.mybatisplus.extension.service.IService;
+import org.springblade.manager.entity.InformationImportRecord;
+
+import java.time.LocalDateTime;
+
+
+public interface InformationImportRecordService extends IService<InformationImportRecord> {
+
+    Boolean updateProcess(Long id, Integer process, Integer status, String remake, LocalDateTime updateTime);
+
+
+}

+ 27 - 0
blade-service/blade-manager/src/main/java/org/springblade/manager/service/impl/InformationImportRecordServiceImpl.java

@@ -0,0 +1,27 @@
+package org.springblade.manager.service.impl;
+
+import com.baomidou.mybatisplus.core.toolkit.Wrappers;
+import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
+import org.springblade.manager.entity.InformationImportRecord;
+import org.springblade.manager.mapper.InformationImportRecordMapper;
+import org.springblade.manager.service.InformationImportRecordService;
+import org.springframework.stereotype.Service;
+
+import java.time.LocalDateTime;
+
+@Service
+public class InformationImportRecordServiceImpl extends ServiceImpl<InformationImportRecordMapper, InformationImportRecord> implements InformationImportRecordService {
+    @Override
+    public Boolean updateProcess(Long id, Integer process, Integer status, String remake, LocalDateTime updateTime) {
+        if (id == null || (process == null && status == null)) {
+            return false;
+        }
+        if (process != null && process < 100) {
+            process = (int) (Math.random() * 9 + process - 5);
+        }
+        return this.update(Wrappers.<InformationImportRecord>lambdaUpdate().eq(InformationImportRecord::getId, id).eq(InformationImportRecord::getUpdateTime, updateTime)
+                .set(process != null, InformationImportRecord::getProcess, process)
+                .set(status != null, InformationImportRecord::getStatus, status)
+                .set(remake != null, InformationImportRecord::getRemark, remake));
+    }
+}

+ 2 - 2
blade-service/blade-manager/src/main/java/org/springblade/manager/utils/NumberStringSubtraction.java

@@ -88,8 +88,8 @@ public class NumberStringSubtraction {
             return numbers;
         }
 
-        // 支持多种分隔符:/ , 空格
-        String[] parts = str.split("[/,、\\s]+");
+        // 支持多种分隔符:/ , 空格 \
+        String[] parts = str.split("[\\\\/,、\\s]+");
         for (String part : parts) {
             try {
                 numbers.add(Integer.parseInt(part.trim()));