|
|
@@ -432,6 +432,13 @@ public class WbsTreeContractController extends BladeController {
|
|
|
}
|
|
|
}
|
|
|
}
|
|
|
+ // 1. 工具方法:统一主表名匹配格式(保留原逻辑,确保无字符差异)
|
|
|
+ private String getMatchableMainName(String name) {
|
|
|
+ if (name == null) return "";
|
|
|
+ String cleanName = name.replaceAll("\\s+", ""); // 去空格
|
|
|
+ cleanName = cleanName.replaceAll("[\\\\/:*?\"<>|_-]", ""); // 去特殊字符
|
|
|
+ return cleanName.toLowerCase(); // 转小写
|
|
|
+ }
|
|
|
@SneakyThrows
|
|
|
@GetMapping("/download-node-excel")
|
|
|
@ApiOperationSupport(order = 14)
|
|
|
@@ -444,45 +451,96 @@ public class WbsTreeContractController extends BladeController {
|
|
|
if (ObjectUtil.isEmpty(formList)) {
|
|
|
throw new ServiceException("该节点下没有找到对应的表单数据");
|
|
|
}
|
|
|
- // 对formList进行排序:主表优先,复制表按数字顺序排列
|
|
|
- formList.sort((contract1, contract2) -> {
|
|
|
- String name1 = contract1.getNodeName().replaceAll("\\s+", "");
|
|
|
- String name2 = contract2.getNodeName().replaceAll("\\s+", "");
|
|
|
-
|
|
|
- boolean isCopy1 = name1.contains("__");
|
|
|
- boolean isCopy2 = name2.contains("__");
|
|
|
+ // 2. 第一步:批量收集所有主表,创建分组(按主表在formList中的原始顺序)
|
|
|
+ Map<String, List<WbsTreeContract>> mainCopyGroupMap = new LinkedHashMap<>(); // 保持主表原始顺序
|
|
|
+ List<WbsTreeContract> copyTableList = new ArrayList<>(); // 临时存储所有复制表
|
|
|
+
|
|
|
+ // 2.1 第一次遍历:分离主表和复制表
|
|
|
+ for (WbsTreeContract contract : formList) {
|
|
|
+ String nodeName = contract.getNodeName();
|
|
|
+ boolean isCopy = nodeName.contains("__");
|
|
|
+
|
|
|
+ if (isCopy) {
|
|
|
+ // 复制表:先暂存,不立即匹配
|
|
|
+ copyTableList.add(contract);
|
|
|
+ } else {
|
|
|
+ // 主表:创建分组(按原始顺序插入)
|
|
|
+ String matchableMainName = getMatchableMainName(nodeName);
|
|
|
+ if (!mainCopyGroupMap.containsKey(matchableMainName)) {
|
|
|
+ List<WbsTreeContract> groupList = new ArrayList<>();
|
|
|
+ groupList.add(contract); // 主表放在分组第一位
|
|
|
+ mainCopyGroupMap.put(matchableMainName, groupList);
|
|
|
+ } else {
|
|
|
+ // 重复主表:仍放在分组第一位,保证主表优先
|
|
|
+ mainCopyGroupMap.get(matchableMainName).add(0, contract);
|
|
|
+ }
|
|
|
+ }
|
|
|
+ }
|
|
|
|
|
|
- // 如果一个是主表,一个是复制表,主表排在前面
|
|
|
- if (isCopy1 != isCopy2) {
|
|
|
- return isCopy1 ? 1 : -1;
|
|
|
+ // 3. 第二步:批量处理所有复制表,匹配主表分组
|
|
|
+ List<WbsTreeContract> orphanCopyList = new ArrayList<>();
|
|
|
+ for (WbsTreeContract copyContract : copyTableList) {
|
|
|
+ String nodeName = copyContract.getNodeName();
|
|
|
+ String[] parts = nodeName.split("__", 2);
|
|
|
+ if (parts.length < 2) {
|
|
|
+ orphanCopyList.add(copyContract);
|
|
|
+ continue;
|
|
|
}
|
|
|
|
|
|
- // 如果都是主表或都是复制表
|
|
|
- if (!isCopy1 && !isCopy2) {
|
|
|
- // 都是主表,按字母顺序排序
|
|
|
- return name1.compareTo(name2);
|
|
|
- } else {
|
|
|
- // 都是复制表,按主表名称和数字排序
|
|
|
- String[] parts1 = name1.split("__", 2);
|
|
|
- String[] parts2 = name2.split("__", 2);
|
|
|
-
|
|
|
- // 先比较主表名称
|
|
|
- int mainNameCompare = parts1[0].compareTo(parts2[0]);
|
|
|
- if (mainNameCompare != 0) {
|
|
|
- return mainNameCompare;
|
|
|
+ // 复制表拆分+统一格式,匹配主表分组
|
|
|
+ String rawMainName = parts[0];
|
|
|
+ String matchableMainName = getMatchableMainName(rawMainName);
|
|
|
+ boolean foundMatch = false;
|
|
|
+
|
|
|
+ // 遍历主表分组,找匹配的分组
|
|
|
+ for (Map.Entry<String, List<WbsTreeContract>> groupEntry : mainCopyGroupMap.entrySet()) {
|
|
|
+ if (groupEntry.getKey().equals(matchableMainName)) {
|
|
|
+ groupEntry.getValue().add(copyContract); // 加入对应主表分组
|
|
|
+ foundMatch = true;
|
|
|
+ break;
|
|
|
}
|
|
|
+ }
|
|
|
|
|
|
- // 主表名称相同,比较数字部分
|
|
|
+ if (!foundMatch) {
|
|
|
+ orphanCopyList.add(copyContract);
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ // 4. 第三步:处理每个主表分组,对复制表排序(保持主表原始顺序)
|
|
|
+ List<WbsTreeContract> sortedList = new ArrayList<>();
|
|
|
+ for (Map.Entry<String, List<WbsTreeContract>> entry : mainCopyGroupMap.entrySet()) {
|
|
|
+ List<WbsTreeContract> groupList = entry.getValue();
|
|
|
+ if (groupList.size() <= 1) {
|
|
|
+ // 只有主表,直接加入
|
|
|
+ sortedList.addAll(groupList);
|
|
|
+ continue;
|
|
|
+ }
|
|
|
+
|
|
|
+ // 拆分主表(第一位)和复制表(剩余)
|
|
|
+ WbsTreeContract mainContract = groupList.get(0);
|
|
|
+ List<WbsTreeContract> copyList = groupList.subList(1, groupList.size());
|
|
|
+
|
|
|
+ // 复制表按数字排序
|
|
|
+ copyList.sort((c1, c2) -> {
|
|
|
+ String numStr1 = c1.getNodeName().split("__", 2)[1];
|
|
|
+ String numStr2 = c2.getNodeName().split("__", 2)[1];
|
|
|
try {
|
|
|
- int num1 = Integer.parseInt(parts1[1]);
|
|
|
- int num2 = Integer.parseInt(parts2[1]);
|
|
|
- return Integer.compare(num1, num2);
|
|
|
+ return Integer.compare(Integer.parseInt(numStr1), Integer.parseInt(numStr2));
|
|
|
} catch (NumberFormatException e) {
|
|
|
- // 如果解析数字失败,按字符串比较
|
|
|
- return parts1[1].compareTo(parts2[1]);
|
|
|
+ return numStr1.compareTo(numStr2);
|
|
|
}
|
|
|
- }
|
|
|
- });
|
|
|
+ });
|
|
|
+
|
|
|
+ // 主表 + 排序后的复制表,加入最终列表
|
|
|
+ sortedList.add(mainContract);
|
|
|
+ sortedList.addAll(copyList);
|
|
|
+ }
|
|
|
+
|
|
|
+ // 5. 第四步:加入无对应主表的复制表(排在最后)
|
|
|
+ sortedList.addAll(orphanCopyList);
|
|
|
+
|
|
|
+ // 6. 替换原列表
|
|
|
+ formList = sortedList;
|
|
|
|
|
|
// 获取节点及祖先节点信息用于构建文件名
|
|
|
WbsTreeContract node = wbsTreeContractServiceImpl.getById(nodeId);
|
|
|
@@ -553,21 +611,15 @@ public class WbsTreeContractController extends BladeController {
|
|
|
throw new ServiceException("所有表单均无法生成有效Excel内容");
|
|
|
}
|
|
|
|
|
|
- // 处理包含特殊符号的文件名(如#、空格、中文等)
|
|
|
- String originalFileName = excelName + ".xlsx";
|
|
|
- // 使用UTF-8编码,并替换特殊字符(符合RFC 5987标准)
|
|
|
+ String originalFileName = excelName + ".xlsx";
|
|
|
+ // 使用UTF-8编码,并替换特殊字符(符合RFC 5987标准)
|
|
|
String encodedFileName = URLEncoder.encode(originalFileName, StandardCharsets.UTF_8.name())
|
|
|
.replaceAll("\\+", "%20") // 空格编码为%20(而非+)
|
|
|
- .replaceAll("%23", "#")
|
|
|
- .replaceAll("%26", "&"); // 保留#不编码(或根据需求调整)
|
|
|
+ .replaceAll("%23", "#"); // 保留#不编码(或根据需求调整)
|
|
|
|
|
|
// 设置响应头,采用RFC 5987标准格式(指定字符集)
|
|
|
- // 双格式响应头:同时支持旧版和新版浏览器
|
|
|
- response.setHeader("Content-Disposition",
|
|
|
- "attachment; " +
|
|
|
- "filename=\"" + encodedFileName + "\"; " + // 旧格式(部分浏览器依赖)
|
|
|
- "filename*=UTF-8''" + encodedFileName // 新标准格式
|
|
|
- );
|
|
|
+ response.setHeader("Content-Disposition",
|
|
|
+ "attachment; filename*=" + encodedFileName);
|
|
|
response.setContentType("application/vnd.openxmlformats-officedocument.spreadsheetml.sheet");
|
|
|
|
|
|
// 写入输出流
|
|
|
@@ -829,12 +881,10 @@ public class WbsTreeContractController extends BladeController {
|
|
|
continue;
|
|
|
}
|
|
|
Map<String, String> dataMap = getDataMap(sheetResult);
|
|
|
- // 对所有value中的单引号进行转义处理
|
|
|
for (Map.Entry<String, String> entry : dataMap.entrySet()) {
|
|
|
String value = entry.getValue();
|
|
|
- if (value != null && value.contains("''")) {
|
|
|
- // 将单引号转义为 \'
|
|
|
- value = value.replaceAll("'", "''");
|
|
|
+ if (value != null) {
|
|
|
+ value = value.replace("'", "''");
|
|
|
entry.setValue(value);
|
|
|
}
|
|
|
}
|