|
|
@@ -39,6 +39,7 @@ import org.springblade.manager.feign.ContractClient;
|
|
|
import org.springblade.manager.service.IWbsParamService;
|
|
|
import org.springblade.manager.service.IWbsTreeContractService;
|
|
|
import org.springblade.manager.service.impl.WbsTreeContractServiceImpl;
|
|
|
+import org.springblade.manager.util.DataStructureFormatUtils;
|
|
|
import org.springblade.manager.utils.FileUtils;
|
|
|
import org.springblade.manager.utils.RandomNumberHolder;
|
|
|
import org.springblade.manager.vo.*;
|
|
|
@@ -79,6 +80,7 @@ import static com.aspose.cells.LoadDataFilterOptions.FORMULA;
|
|
|
import static com.aspose.cells.PropertyType.BOOLEAN;
|
|
|
import static com.aspose.cells.PropertyType.STRING;
|
|
|
import static java.sql.Types.NUMERIC;
|
|
|
+import static java.util.stream.Collectors.toMap;
|
|
|
|
|
|
|
|
|
@RestController
|
|
|
@@ -682,7 +684,365 @@ public class WbsTreeContractController extends BladeController {
|
|
|
cellStyle.setBorderRight(BorderStyle.THIN);
|
|
|
cellStyle.setRightBorderColor(IndexedColors.BLACK.getIndex());
|
|
|
}
|
|
|
+ @PostMapping("/import-node-excel")
|
|
|
+ @ApiOperationSupport(order = 13)
|
|
|
+ @ApiOperation(value = "客户端-导入多sheet excel到对应节点下的表单", notes = "传入节点ID、分类和多sheet excel文件")
|
|
|
+ public R<Map<String, Object>> importNodeExcel(
|
|
|
+ @RequestPart MultipartFile file,
|
|
|
+ @RequestParam Long nodeId,
|
|
|
+ @RequestParam Integer classify) throws Exception {
|
|
|
+
|
|
|
+ // 1. 获取节点下所有表单,并建立 sheet名 -> WbsTreeContract 的映射(处理特殊字符)
|
|
|
+ List<WbsTreeContract> wbsTreeContracts = wbsTreeContractServiceImpl.selectAllPkeyIdByNodeId(nodeId, classify);
|
|
|
+ if (wbsTreeContracts.isEmpty()) {
|
|
|
+ return R.fail("该节点下没有找到对应的表单数据");
|
|
|
+ }
|
|
|
+
|
|
|
+ // 处理表单名称(与下载时的sheet名处理逻辑一致,确保匹配)
|
|
|
+ Map<String, WbsTreeContract> nodeNameToContractMap = new HashMap<>();
|
|
|
+ for (WbsTreeContract contract : wbsTreeContracts) {
|
|
|
+ String processedNodeName = contract.getNodeName().replaceAll("[\\\\/:*?\"<>|]", "_");
|
|
|
+ nodeNameToContractMap.put(processedNodeName, contract);
|
|
|
+ }
|
|
|
+
|
|
|
+ // 2. 加载上传的多sheet Excel文件
|
|
|
+ com.spire.xls.Workbook mainWorkbook = new com.spire.xls.Workbook();
|
|
|
+ try {
|
|
|
+ mainWorkbook.loadFromStream(file.getInputStream());
|
|
|
+ } catch (Exception e) {
|
|
|
+ logger.error("加载Excel文件失败", e);
|
|
|
+ return R.fail("Excel文件解析失败:" + e.getMessage());
|
|
|
+ }
|
|
|
+
|
|
|
+ // 3. 遍历所有sheet,逐个处理
|
|
|
+ Map<String, Object> allResults = new LinkedHashMap<>(); // 按sheet顺序保存结果
|
|
|
+ int sheetCount = mainWorkbook.getWorksheets().getCount();
|
|
|
+
|
|
|
+ for (int i = 0; i < sheetCount; i++) {
|
|
|
+ com.spire.xls.Worksheet sheet = mainWorkbook.getWorksheets().get(i);
|
|
|
+ String sheetName = sheet.getName();
|
|
|
+ String processedSheetName = sheetName.replaceAll("[\\\\/:*?\"<>|]", "_"); // 处理sheet名特殊字符
|
|
|
+
|
|
|
+ // 匹配对应的表单
|
|
|
+ WbsTreeContract matchedContract = nodeNameToContractMap.get(processedSheetName);
|
|
|
+ if (matchedContract == null) {
|
|
|
+ allResults.put(sheetName, "未找到匹配的表单(节点名:" + sheetName + ")");
|
|
|
+ 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);
|
|
|
+ Map<String, String> dataMap = getDataMap(sheetResult);
|
|
|
+ String delSql = "delete from " + matchedContract.getInitTableName() + " where p_key_id=" + matchedContract.getPKeyId();
|
|
|
+ dataMap.put("p_key_id", matchedContract.getPKeyId()+"");
|
|
|
+ String sqlInfo = buildMTableInsertSql(matchedContract.getInitTableName(), dataMap, SnowFlakeUtil.getId(), null, null).toString();
|
|
|
+ jdbcTemplate.execute(delSql);
|
|
|
+ jdbcTemplate.execute(sqlInfo);
|
|
|
+ } catch (Exception e) {
|
|
|
+ String errorMsg = "处理sheet[" + sheetName + "]失败:" + e.getMessage();
|
|
|
+ allResults.put(sheetName, errorMsg);
|
|
|
+ logger.error(errorMsg, e);
|
|
|
+ } finally {
|
|
|
+ // 关闭流并删除临时文件
|
|
|
+ if (tempInputStream != null) {
|
|
|
+ tempInputStream.close();
|
|
|
+ }
|
|
|
+ if (tempFile != null && !tempFile.delete()) {
|
|
|
+ logger.warn("临时文件[{}]删除失败", tempFile.getAbsolutePath());
|
|
|
+ }
|
|
|
+ }
|
|
|
+ }
|
|
|
+ mainWorkbook.dispose();
|
|
|
+ return R.data(allResults);
|
|
|
+ }
|
|
|
+
|
|
|
+ public StringBuilder buildMTableInsertSql(String tabName, Map<String, String> dataMap2, Object id, Object groupId, Object pKeyId) {
|
|
|
+ if (dataMap2 == null || dataMap2.isEmpty() || tabName == null || tabName.isEmpty()) {
|
|
|
+ return new StringBuilder();
|
|
|
+ }
|
|
|
+ //拼接SQL
|
|
|
+ StringBuilder sql = new StringBuilder("INSERT INTO " + tabName),
|
|
|
+ keySql = new StringBuilder(),
|
|
|
+ valSql = new StringBuilder();
|
|
|
+ if (id == null) {
|
|
|
+ if (dataMap2.containsKey("id")) {
|
|
|
+ id = dataMap2.get("id");
|
|
|
+ }
|
|
|
+ }
|
|
|
+ keySql.append("id");
|
|
|
+ valSql.append(id == null ? SnowFlakeUtil.getId() : id);
|
|
|
+ if (groupId == null) {
|
|
|
+ groupId = dataMap2.get("group_id");
|
|
|
+ }
|
|
|
+ if (groupId != null) {
|
|
|
+ keySql.append(", group_id");
|
|
|
+ valSql.append(", ").append(groupId);
|
|
|
+ }
|
|
|
+ if (pKeyId == null) {
|
|
|
+ pKeyId = dataMap2.get("p_key_id");
|
|
|
+ }
|
|
|
+ if (pKeyId != null) {
|
|
|
+ keySql.append(", p_key_id");
|
|
|
+ valSql.append(", ").append(pKeyId);
|
|
|
+ }
|
|
|
+ //参数
|
|
|
+ Map<String, String> opsParamMap = new HashMap<>();
|
|
|
+ dataMap2.remove("id");
|
|
|
+ dataMap2.remove("group_id");
|
|
|
+ dataMap2.remove("p_key_id");
|
|
|
+ String key201 = dataMap2.remove("key_201");
|
|
|
+ String fields = dataMap2.keySet().stream().map(key -> "'" + key + "'").collect(Collectors.joining(","));
|
|
|
+ Map<String, Integer> map = new HashMap<>();
|
|
|
+ if (!fields.isEmpty()) {
|
|
|
+ try {
|
|
|
+ fields = fields + ", 'key_201'";
|
|
|
+ List<Map<String, Object>> fieldMap = jdbcTemplate.queryForList("select distinct COLUMN_NAME as fieldName, CHARACTER_MAXIMUM_LENGTH as fieldLength from information_schema.COLUMNS where TABLE_NAME = '" + tabName +
|
|
|
+ "' and COLUMN_NAME in (" + fields + ")");
|
|
|
+ map = fieldMap.stream().collect(toMap(k -> k.get("fieldName") + "", v -> {
|
|
|
+ try {
|
|
|
+ return Integer.parseInt(v.get("fieldLength") + "");
|
|
|
+ } catch (Exception e) {
|
|
|
+ return 0;
|
|
|
+ }
|
|
|
+ }, Math::min));
|
|
|
+ } catch (Exception e) {
|
|
|
+ e.printStackTrace();
|
|
|
+ }
|
|
|
+ }
|
|
|
+ if (key201 != null) {
|
|
|
+ Map<String, String> map1 = DataStructureFormatUtils.parseDataByKey(key201);
|
|
|
+ if (!map1.isEmpty()) {
|
|
|
+ opsParamMap.putAll(map1);
|
|
|
+ }
|
|
|
+ }
|
|
|
+ for (String key : dataMap2.keySet()) {
|
|
|
+ String[] split = key.split("_");
|
|
|
+ if (split.length > 1 && Integer.parseInt(split[1]) > 80) {
|
|
|
+ // 大于80则保留在扩展字段中
|
|
|
+ opsParamMap.put(key, dataMap2.get(key));
|
|
|
+ } else {
|
|
|
+ String value = dataMap2.get(key);
|
|
|
+ if (value != null) {
|
|
|
+ Integer i = map.get(key);
|
|
|
+ // 长度超过数据库长度也保留在扩展字段中
|
|
|
+ if (i != null && value.length() > i) {
|
|
|
+ opsParamMap.put(key, dataMap2.get(key));
|
|
|
+ continue;
|
|
|
+ }
|
|
|
+ }
|
|
|
+ keySql.append(", ").append(key);
|
|
|
+ valSql.append(", '").append(value).append("'");
|
|
|
+ }
|
|
|
+ }
|
|
|
+ if (!opsParamMap.isEmpty()) {
|
|
|
+ keySql.append(", key_201");
|
|
|
+ String data = DataStructureFormatUtils.buildData(opsParamMap);
|
|
|
+ try {
|
|
|
+ if (!map.containsKey( "key_201")) {
|
|
|
+ jdbcTemplate.execute("alter table " + tabName + " add column key_201 text");
|
|
|
+ } else {
|
|
|
+ Integer i = map.get("key_201");
|
|
|
+ if (data.length() > i) {
|
|
|
+ if (i < 10000) {
|
|
|
+ // 65535 byte
|
|
|
+ jdbcTemplate.execute("alter table " + tabName + " modify column key_201 text");
|
|
|
+ }else {
|
|
|
+ // 16777215 byte
|
|
|
+ jdbcTemplate.execute("alter table " + tabName + " modify column key_201 mediumtext");
|
|
|
+ }
|
|
|
+ }
|
|
|
+ }
|
|
|
+ } catch (Exception e) {
|
|
|
+ e.printStackTrace();
|
|
|
+ }
|
|
|
+ valSql.append(", '").append(data).append("'");
|
|
|
+ }
|
|
|
+ sql.append("(").append(keySql).append(")").append(" values(").append(valSql).append(")");
|
|
|
+ return sql;
|
|
|
+ }
|
|
|
+
|
|
|
+ public Map<String,String> getDataMap(Map<String, Object> originalMap){
|
|
|
+ // 用于存储合并后的结果
|
|
|
+ Map<String, String> mergedMap = new HashMap<>();
|
|
|
+
|
|
|
+ // 正则表达式:匹配"key_前缀__剩余坐标"的格式
|
|
|
+ Pattern pattern = Pattern.compile("key_(\\w+)__([\\w_]+)");
|
|
|
+
|
|
|
+ for (Map.Entry<String, Object> entry : originalMap.entrySet()) {
|
|
|
+ String key = entry.getKey();
|
|
|
+ String value = entry.getValue()+"";
|
|
|
+ Matcher matcher = pattern.matcher(key);
|
|
|
+
|
|
|
+ if (matcher.matches()) {
|
|
|
+ String prefix = matcher.group(1); // 提取__前面的前缀(如7、17)
|
|
|
+ String suffix = matcher.group(2); // 提取__后面的剩余坐标(如11_9、12_5)
|
|
|
+
|
|
|
+ // 构建合并后的键(简化为prefix,如7、17)
|
|
|
+ String mergedKey = "key_" + prefix;
|
|
|
+ // 构建合并后的值(值_^_剩余坐标)
|
|
|
+ String mergedValue = value + "_^_" + suffix;
|
|
|
+
|
|
|
+ // 若该键已存在,用☆拼接新值;否则直接存入
|
|
|
+ if (mergedMap.containsKey(mergedKey)) {
|
|
|
+ mergedMap.put(mergedKey, mergedMap.get(mergedKey) + "☆" + mergedValue);
|
|
|
+ } else {
|
|
|
+ mergedMap.put(mergedKey, mergedValue);
|
|
|
+ }
|
|
|
+ }
|
|
|
+ }
|
|
|
+ return mergedMap;
|
|
|
+ }
|
|
|
+ /**
|
|
|
+ * 复用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;
|
|
|
+ }
|
|
|
+
|
|
|
+ Element td2 = tdElements2.size() > j ? tdElements2.get(j) : null;
|
|
|
+ if (td2 == null) break;
|
|
|
+
|
|
|
+ String keyName = getKeyNameFromChildElement(td1); // 复用原方法获取key
|
|
|
+ if (StringUtils.isNotEmpty(keyName)) {
|
|
|
+ String divValue = td2.text();
|
|
|
+ if (StringUtils.isNotEmpty(divValue)) {
|
|
|
+ // 日期范围处理
|
|
|
+ if (parseDateRange(divValue).size() == 2) {
|
|
|
+ resultMap.put(keyName, 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);
|
|
|
+ }
|
|
|
+ }
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ // 排序结果(复用原逻辑)
|
|
|
+ if (!resultMap.isEmpty()) {
|
|
|
+ List<Map.Entry<String, Object>> entryList = new ArrayList<>(resultMap.entrySet());
|
|
|
+ entryList.sort(new KeyComparator());
|
|
|
+ LinkedHashMap<String, Object> sortedMap = new LinkedHashMap<>();
|
|
|
+ for (Map.Entry<String, Object> entry : entryList) {
|
|
|
+ sortedMap.put(entry.getKey(), entry.getValue());
|
|
|
+ }
|
|
|
+ resultMap = sortedMap;
|
|
|
+ }
|
|
|
+
|
|
|
+ } finally {
|
|
|
+ if (workbook != null) {
|
|
|
+ workbook.dispose();
|
|
|
+ }
|
|
|
+ // 删除临时文件(复用原逻辑)
|
|
|
+ if (deleteFolder(Paths.get(importExcelTOHtmlPath))) {
|
|
|
+ logger.info("表单[{}]临时文件删除成功", pkeyId);
|
|
|
+ } else {
|
|
|
+ logger.warn("表单[{}]临时文件删除失败", pkeyId);
|
|
|
+ }
|
|
|
+ String url_1 = importExcelTOHtmlPath.split("pdf//")[0];
|
|
|
+ if (deleteFolderAndContents(Paths.get(url_1 + "/pdf/" + id + "_files"))) {
|
|
|
+ logger.info("表单[{}]临时文件夹删除成功", pkeyId);
|
|
|
+ } else {
|
|
|
+ logger.warn("表单[{}]临时文件夹删除失败", pkeyId);
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ return resultMap.isEmpty() ? Collections.singletonMap("message", "未获取到有效数据") : resultMap;
|
|
|
+ }
|
|
|
/**
|
|
|
* 客户端-导入excel数据到对应元素表中
|
|
|
*
|
|
|
@@ -1145,6 +1505,8 @@ public class WbsTreeContractController extends BladeController {
|
|
|
|
|
|
}
|
|
|
|
|
|
+
|
|
|
+
|
|
|
/**
|
|
|
* 判断日期范围格式数据,以下12种格式
|
|
|
* 2023-01-01-2023-01-30 或 2023-01-01~2023-01-30
|