|
@@ -1,26 +1,22 @@
|
|
|
package com.qqflow.engine.common.service;
|
|
package com.qqflow.engine.common.service;
|
|
|
|
|
|
|
|
|
|
+import com.alibaba.excel.EasyExcel;
|
|
|
import com.fasterxml.jackson.databind.ObjectMapper;
|
|
import com.fasterxml.jackson.databind.ObjectMapper;
|
|
|
import com.qqflow.engine.common.exception.BusinessException;
|
|
import com.qqflow.engine.common.exception.BusinessException;
|
|
|
import com.qqflow.engine.domain.flow.mapper.ProcessDefinitionMapper;
|
|
import com.qqflow.engine.domain.flow.mapper.ProcessDefinitionMapper;
|
|
|
|
|
+import com.qqflow.engine.domain.flow.model.FlowModel;
|
|
|
|
|
+import com.qqflow.engine.domain.flow.model.FlowNode;
|
|
|
import com.qqflow.engine.domain.flow.po.ProcessDefinition;
|
|
import com.qqflow.engine.domain.flow.po.ProcessDefinition;
|
|
|
|
|
+import com.qqflow.engine.domain.system.entity.SysUser;
|
|
|
|
|
+import com.qqflow.engine.domain.system.mapper.SysUserMapper;
|
|
|
import lombok.RequiredArgsConstructor;
|
|
import lombok.RequiredArgsConstructor;
|
|
|
-import org.apache.poi.hssf.usermodel.HSSFWorkbook;
|
|
|
|
|
-import org.apache.poi.ss.usermodel.Cell;
|
|
|
|
|
-import org.apache.poi.ss.usermodel.CellType;
|
|
|
|
|
-import org.apache.poi.ss.usermodel.DateUtil;
|
|
|
|
|
-import org.apache.poi.ss.usermodel.Row;
|
|
|
|
|
-import org.apache.poi.ss.usermodel.Sheet;
|
|
|
|
|
-import org.apache.poi.ss.usermodel.Workbook;
|
|
|
|
|
-import org.apache.poi.xssf.usermodel.XSSFWorkbook;
|
|
|
|
|
import org.springframework.stereotype.Service;
|
|
import org.springframework.stereotype.Service;
|
|
|
import org.springframework.web.multipart.MultipartFile;
|
|
import org.springframework.web.multipart.MultipartFile;
|
|
|
|
|
|
|
|
import java.io.ByteArrayOutputStream;
|
|
import java.io.ByteArrayOutputStream;
|
|
|
import java.io.IOException;
|
|
import java.io.IOException;
|
|
|
-import java.io.InputStream;
|
|
|
|
|
|
|
+import java.util.ArrayList;
|
|
|
import java.util.Collections;
|
|
import java.util.Collections;
|
|
|
-import java.util.HashMap;
|
|
|
|
|
import java.util.LinkedHashMap;
|
|
import java.util.LinkedHashMap;
|
|
|
import java.util.List;
|
|
import java.util.List;
|
|
|
import java.util.Map;
|
|
import java.util.Map;
|
|
@@ -33,15 +29,19 @@ public class ExcelParseService {
|
|
|
private static final ObjectMapper OBJECT_MAPPER = new ObjectMapper();
|
|
private static final ObjectMapper OBJECT_MAPPER = new ObjectMapper();
|
|
|
|
|
|
|
|
private final ProcessDefinitionMapper processDefinitionMapper;
|
|
private final ProcessDefinitionMapper processDefinitionMapper;
|
|
|
|
|
+ private final SysUserMapper sysUserMapper;
|
|
|
|
|
|
|
|
- /**
|
|
|
|
|
- * 根据字段映射关系解析 Excel,返回表单数据 Map。
|
|
|
|
|
- *
|
|
|
|
|
- * @param file Excel 文件
|
|
|
|
|
- * @param definitionId 流程定义 ID
|
|
|
|
|
- * @param mappings Excel 列名 -> 系统字段名 的映射
|
|
|
|
|
- */
|
|
|
|
|
|
|
+ /** 单行解析(兼容旧接口) */
|
|
|
public Map<String, Object> parseExcel(MultipartFile file, Long definitionId, Map<String, String> mappings) {
|
|
public Map<String, Object> parseExcel(MultipartFile file, Long definitionId, Map<String, String> mappings) {
|
|
|
|
|
+ List<Map<String, Object>> rows = this.parseExcelRows(file, definitionId, mappings);
|
|
|
|
|
+ if (rows.isEmpty()) {
|
|
|
|
|
+ throw new BusinessException("Excel 无有效数据");
|
|
|
|
|
+ }
|
|
|
|
|
+ return rows.get(0);
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ /** 批量解析 Excel,返回每行数据的 Map 列表 */
|
|
|
|
|
+ public List<Map<String, Object>> parseExcelRows(MultipartFile file, Long definitionId, Map<String, String> mappings) {
|
|
|
if (file == null || file.isEmpty()) {
|
|
if (file == null || file.isEmpty()) {
|
|
|
throw new BusinessException("文件不能为空");
|
|
throw new BusinessException("文件不能为空");
|
|
|
}
|
|
}
|
|
@@ -53,93 +53,178 @@ public class ExcelParseService {
|
|
|
throw new BusinessException("流程定义不存在");
|
|
throw new BusinessException("流程定义不存在");
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
- try (InputStream is = file.getInputStream();
|
|
|
|
|
- Workbook workbook = createWorkbook(is, file.getOriginalFilename())) {
|
|
|
|
|
- Sheet sheet = workbook.getSheetAt(0);
|
|
|
|
|
- if (sheet.getPhysicalNumberOfRows() < 2) {
|
|
|
|
|
|
|
+ try {
|
|
|
|
|
+ List<Object> rawRows = EasyExcel.read(file.getInputStream()).headRowNumber(0).sheet(0).doReadSync();
|
|
|
|
|
+ if (rawRows.size() < 2) {
|
|
|
throw new BusinessException("Excel 至少需要包含表头和一行数据");
|
|
throw new BusinessException("Excel 至少需要包含表头和一行数据");
|
|
|
}
|
|
}
|
|
|
- Row headerRow = sheet.getRow(0);
|
|
|
|
|
- if (headerRow == null) {
|
|
|
|
|
- throw new BusinessException("Excel 表头为空");
|
|
|
|
|
- }
|
|
|
|
|
|
|
|
|
|
- // 读取表头:列索引 -> Excel 列名
|
|
|
|
|
- Map<Integer, String> columnIndexMap = new HashMap<>();
|
|
|
|
|
- for (int i = 0; i < headerRow.getLastCellNum(); i++) {
|
|
|
|
|
- Cell cell = headerRow.getCell(i);
|
|
|
|
|
- if (cell == null) continue;
|
|
|
|
|
- String value = getCellStringValue(cell);
|
|
|
|
|
- if (value != null && !value.isBlank()) {
|
|
|
|
|
- columnIndexMap.put(i, value.trim());
|
|
|
|
|
|
|
+ @SuppressWarnings("unchecked")
|
|
|
|
|
+ Map<Integer, Object> headerRow = (Map<Integer, Object>) rawRows.get(0);
|
|
|
|
|
+
|
|
|
|
|
+ String nodeFieldName = definition.getNodeFieldName();
|
|
|
|
|
+ String ownerFieldName = definition.getOwnerFieldName();
|
|
|
|
|
+ String bizFieldName = definition.getBizFieldName();
|
|
|
|
|
+
|
|
|
|
|
+ List<Map<String, Object>> results = new ArrayList<>();
|
|
|
|
|
+ // 从第二行开始(索引 1)遍历数据行
|
|
|
|
|
+ for (int rowIdx = 1; rowIdx < rawRows.size(); rowIdx++) {
|
|
|
|
|
+ @SuppressWarnings("unchecked")
|
|
|
|
|
+ Map<Integer, Object> dataRow = (Map<Integer, Object>) rawRows.get(rowIdx);
|
|
|
|
|
+ // 跳过全空行
|
|
|
|
|
+ if (dataRow.values().stream().allMatch(v -> v == null || String.valueOf(v).trim().isEmpty())) continue;
|
|
|
|
|
+
|
|
|
|
|
+ Map<String, Object> formData = new LinkedHashMap<>();
|
|
|
|
|
+ String nodeName = null;
|
|
|
|
|
+ String ownerName = null;
|
|
|
|
|
+ String bizValue = null;
|
|
|
|
|
+
|
|
|
|
|
+ for (Map.Entry<Integer, Object> entry : headerRow.entrySet()) {
|
|
|
|
|
+ int col = entry.getKey();
|
|
|
|
|
+ String excelColumn = entry.getValue() != null
|
|
|
|
|
+ ? String.valueOf(entry.getValue()).replace("\r\n", " ").replace("\r", " ").replace("\n", " ").trim()
|
|
|
|
|
+ : "";
|
|
|
|
|
+ if (excelColumn.isEmpty()) continue;
|
|
|
|
|
+
|
|
|
|
|
+ Object cellValue = dataRow.get(col);
|
|
|
|
|
+ String value = cellValue == null ? "" : String.valueOf(cellValue).trim();
|
|
|
|
|
+ String systemField = mappings.get(excelColumn);
|
|
|
|
|
+
|
|
|
|
|
+ if (nodeFieldName != null && !nodeFieldName.isBlank() && excelColumn.equals(nodeFieldName)) {
|
|
|
|
|
+ nodeName = value;
|
|
|
|
|
+ }
|
|
|
|
|
+ if (ownerFieldName != null && !ownerFieldName.isBlank() && excelColumn.equals(ownerFieldName)) {
|
|
|
|
|
+ ownerName = value;
|
|
|
|
|
+ }
|
|
|
|
|
+ if (systemField != null && !systemField.isBlank()) {
|
|
|
|
|
+ formData.put(systemField, value);
|
|
|
|
|
+ }
|
|
|
|
|
+ if (bizFieldName != null && !bizFieldName.isBlank() && excelColumn.equals(bizFieldName)) {
|
|
|
|
|
+ bizValue = value;
|
|
|
|
|
+ formData.put(bizFieldName, value);
|
|
|
|
|
+ }
|
|
|
}
|
|
}
|
|
|
- }
|
|
|
|
|
|
|
|
|
|
- // 读取第一行数据
|
|
|
|
|
- Row dataRow = sheet.getRow(1);
|
|
|
|
|
- if (dataRow == null) {
|
|
|
|
|
- throw new BusinessException("Excel 数据行为空");
|
|
|
|
|
- }
|
|
|
|
|
|
|
+ Long ownerId = null;
|
|
|
|
|
+ if (ownerName != null && !ownerName.isBlank()) {
|
|
|
|
|
+ SysUser user = sysUserMapper.selectByRealName(ownerName.trim());
|
|
|
|
|
+ if (user != null) {
|
|
|
|
|
+ ownerId = user.getId();
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
|
|
|
- Map<String, Object> formData = new LinkedHashMap<>();
|
|
|
|
|
- for (Map.Entry<Integer, String> entry : columnIndexMap.entrySet()) {
|
|
|
|
|
- String excelColumn = entry.getValue();
|
|
|
|
|
- String systemField = mappings.get(excelColumn);
|
|
|
|
|
- if (systemField == null || systemField.isBlank()) {
|
|
|
|
|
- continue;
|
|
|
|
|
|
|
+ // 提取所有节点的审批时间字段(一行可能对应多个节点的时间)
|
|
|
|
|
+ Map<String, String> nodeApproveTimes = new LinkedHashMap<>();
|
|
|
|
|
+ FlowModel flowModel = parseFlowModel(definition);
|
|
|
|
|
+ if (flowModel != null && flowModel.getNodes() != null) {
|
|
|
|
|
+ for (FlowNode n : flowModel.getNodes()) {
|
|
|
|
|
+ if (n.getProperties() == null) continue;
|
|
|
|
|
+ Object atf = n.getProperties().get("approveTimeField");
|
|
|
|
|
+ if (atf == null || atf.toString().isBlank()) continue;
|
|
|
|
|
+ for (Map.Entry<Integer, Object> entry : headerRow.entrySet()) {
|
|
|
|
|
+ String col = normalizeHeader(entry.getValue());
|
|
|
|
|
+ if (col.equals(atf.toString().trim())) {
|
|
|
|
|
+ Object v = dataRow.get(entry.getKey());
|
|
|
|
|
+ nodeApproveTimes.put(n.getName(), toDateString(v));
|
|
|
|
|
+ break;
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
}
|
|
}
|
|
|
- Cell cell = dataRow.getCell(entry.getKey());
|
|
|
|
|
- Object value = getCellValue(cell);
|
|
|
|
|
- formData.put(systemField, value);
|
|
|
|
|
|
|
+
|
|
|
|
|
+ Map<String, Object> rowResult = new LinkedHashMap<>();
|
|
|
|
|
+ rowResult.put("formData", formData);
|
|
|
|
|
+ rowResult.put("nodeName", nodeName);
|
|
|
|
|
+ rowResult.put("ownerId", ownerId);
|
|
|
|
|
+ rowResult.put("ownerName", ownerName);
|
|
|
|
|
+ rowResult.put("bizValue", bizValue);
|
|
|
|
|
+ rowResult.put("nodeApproveTimes", nodeApproveTimes);
|
|
|
|
|
+ results.add(rowResult);
|
|
|
}
|
|
}
|
|
|
- return formData;
|
|
|
|
|
|
|
+ return results;
|
|
|
} catch (IOException e) {
|
|
} catch (IOException e) {
|
|
|
throw new BusinessException("Excel 解析失败: " + e.getMessage());
|
|
throw new BusinessException("Excel 解析失败: " + e.getMessage());
|
|
|
}
|
|
}
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
- /**
|
|
|
|
|
- * 生成 Excel 模板文件字节数组
|
|
|
|
|
- */
|
|
|
|
|
|
|
+ /** 生成 Excel 模板 */
|
|
|
public byte[] generateTemplate(Long definitionId) {
|
|
public byte[] generateTemplate(Long definitionId) {
|
|
|
ProcessDefinition definition = this.processDefinitionMapper.selectById(definitionId);
|
|
ProcessDefinition definition = this.processDefinitionMapper.selectById(definitionId);
|
|
|
if (definition == null) {
|
|
if (definition == null) {
|
|
|
throw new BusinessException("流程定义不存在");
|
|
throw new BusinessException("流程定义不存在");
|
|
|
}
|
|
}
|
|
|
- String formSchema = definition.getFormSchema();
|
|
|
|
|
- List<Map<String, Object>> fields = parseFormSchema(formSchema);
|
|
|
|
|
-
|
|
|
|
|
- try (Workbook workbook = new XSSFWorkbook()) {
|
|
|
|
|
- Sheet sheet = workbook.createSheet("模板");
|
|
|
|
|
- // 表头行
|
|
|
|
|
- Row headerRow = sheet.createRow(0);
|
|
|
|
|
- for (int i = 0; i < fields.size(); i++) {
|
|
|
|
|
- Map<String, Object> field = fields.get(i);
|
|
|
|
|
- String label = Objects.toString(field.get("label"), "");
|
|
|
|
|
- String name = Objects.toString(field.get("name"), "");
|
|
|
|
|
- Cell cell = headerRow.createCell(i);
|
|
|
|
|
- cell.setCellValue(label + "(" + name + ")");
|
|
|
|
|
- }
|
|
|
|
|
- // 示例数据行
|
|
|
|
|
- Row exampleRow = sheet.createRow(1);
|
|
|
|
|
- for (int i = 0; i < fields.size(); i++) {
|
|
|
|
|
- Cell cell = exampleRow.createCell(i);
|
|
|
|
|
- cell.setCellValue("示例值");
|
|
|
|
|
|
|
+ List<Map<String, Object>> fields = parseFormSchema(definition.getFormSchema());
|
|
|
|
|
+ String nodeFieldName = definition.getNodeFieldName();
|
|
|
|
|
+ String ownerFieldName = definition.getOwnerFieldName();
|
|
|
|
|
+
|
|
|
|
|
+ List<List<String>> head = new ArrayList<>();
|
|
|
|
|
+ java.util.Set<String> usedHeaders = new java.util.HashSet<>();
|
|
|
|
|
+ if (nodeFieldName != null && !nodeFieldName.isBlank()) {
|
|
|
|
|
+ head.add(Collections.singletonList(nodeFieldName));
|
|
|
|
|
+ usedHeaders.add(nodeFieldName);
|
|
|
|
|
+ }
|
|
|
|
|
+ if (ownerFieldName != null && !ownerFieldName.isBlank()) {
|
|
|
|
|
+ if (!usedHeaders.add(ownerFieldName)) {
|
|
|
|
|
+ throw new BusinessException("模板表头重复:" + ownerFieldName + ",请检查流程定义的节点字段/负责人字段配置");
|
|
|
}
|
|
}
|
|
|
- // 自动调整列宽
|
|
|
|
|
- for (int i = 0; i < fields.size(); i++) {
|
|
|
|
|
- sheet.autoSizeColumn(i);
|
|
|
|
|
|
|
+ head.add(Collections.singletonList(ownerFieldName));
|
|
|
|
|
+ }
|
|
|
|
|
+ for (Map<String, Object> field : fields) {
|
|
|
|
|
+ String label = Objects.toString(field.get("label"), Objects.toString(field.get("name"), ""));
|
|
|
|
|
+ if (label.isBlank()) continue;
|
|
|
|
|
+ if (!usedHeaders.add(label)) {
|
|
|
|
|
+ throw new BusinessException("模板表头重复:" + label + ",请检查表单字段 label 是否与节点字段/负责人字段同名");
|
|
|
}
|
|
}
|
|
|
|
|
+ head.add(Collections.singletonList(label));
|
|
|
|
|
+ }
|
|
|
|
|
|
|
|
- try (ByteArrayOutputStream bos = new ByteArrayOutputStream()) {
|
|
|
|
|
- workbook.write(bos);
|
|
|
|
|
- return bos.toByteArray();
|
|
|
|
|
- }
|
|
|
|
|
|
|
+ List<List<String>> data = new ArrayList<>();
|
|
|
|
|
+ List<String> exampleRow = new ArrayList<>();
|
|
|
|
|
+ for (int i = 0; i < head.size(); i++) {
|
|
|
|
|
+ exampleRow.add("示例值");
|
|
|
|
|
+ }
|
|
|
|
|
+ data.add(exampleRow);
|
|
|
|
|
+
|
|
|
|
|
+ try (ByteArrayOutputStream bos = new ByteArrayOutputStream()) {
|
|
|
|
|
+ EasyExcel.write(bos).head(head).sheet("模板").doWrite(data);
|
|
|
|
|
+ return bos.toByteArray();
|
|
|
} catch (IOException e) {
|
|
} catch (IOException e) {
|
|
|
throw new BusinessException("模板生成失败: " + e.getMessage());
|
|
throw new BusinessException("模板生成失败: " + e.getMessage());
|
|
|
}
|
|
}
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
|
|
+ private FlowModel parseFlowModel(ProcessDefinition definition) {
|
|
|
|
|
+ try {
|
|
|
|
|
+ return OBJECT_MAPPER.readValue(definition.getModelJson(), FlowModel.class);
|
|
|
|
|
+ } catch (Exception ignored) {
|
|
|
|
|
+ return null;
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ private String normalizeHeader(Object value) {
|
|
|
|
|
+ if (value == null) return "";
|
|
|
|
|
+ return String.valueOf(value).replace("\r\n", " ").replace("\r", " ").replace("\n", " ").trim();
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ /** 将 Excel 单元格值转为日期字符串(处理数值型日期) */
|
|
|
|
|
+ private String toDateString(Object cellValue) {
|
|
|
|
|
+ if (cellValue == null) return null;
|
|
|
|
|
+ if (cellValue instanceof java.util.Date) {
|
|
|
|
|
+ return java.time.LocalDateTime.ofInstant(((java.util.Date) cellValue).toInstant(),
|
|
|
|
|
+ java.time.ZoneId.systemDefault()).format(java.time.format.DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss"));
|
|
|
|
|
+ }
|
|
|
|
|
+ if (cellValue instanceof Number) {
|
|
|
|
|
+ // Excel 日期序列号(1900-01-01 = 1)
|
|
|
|
|
+ double days = ((Number) cellValue).doubleValue();
|
|
|
|
|
+ if (days > 1 && days < 100000) {
|
|
|
|
|
+ java.time.LocalDateTime base = java.time.LocalDateTime.of(1899, 12, 30, 0, 0, 0);
|
|
|
|
|
+ return base.plusDays((long) days).format(java.time.format.DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss"));
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+ String s = String.valueOf(cellValue).trim();
|
|
|
|
|
+ return s.isEmpty() ? null : s;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
@SuppressWarnings("unchecked")
|
|
@SuppressWarnings("unchecked")
|
|
|
private List<Map<String, Object>> parseFormSchema(String formSchema) {
|
|
private List<Map<String, Object>> parseFormSchema(String formSchema) {
|
|
|
if (formSchema == null || formSchema.isBlank()) {
|
|
if (formSchema == null || formSchema.isBlank()) {
|
|
@@ -151,51 +236,4 @@ public class ExcelParseService {
|
|
|
throw new BusinessException("表单字段定义格式错误");
|
|
throw new BusinessException("表单字段定义格式错误");
|
|
|
}
|
|
}
|
|
|
}
|
|
}
|
|
|
-
|
|
|
|
|
- private Workbook createWorkbook(InputStream is, String filename) throws IOException {
|
|
|
|
|
- if (filename != null && filename.toLowerCase().endsWith(".xls")) {
|
|
|
|
|
- return new HSSFWorkbook(is);
|
|
|
|
|
- }
|
|
|
|
|
- return new XSSFWorkbook(is);
|
|
|
|
|
- }
|
|
|
|
|
-
|
|
|
|
|
- private String getCellStringValue(Cell cell) {
|
|
|
|
|
- if (cell == null) return "";
|
|
|
|
|
- return switch (cell.getCellType()) {
|
|
|
|
|
- case STRING -> cell.getStringCellValue();
|
|
|
|
|
- case NUMERIC -> {
|
|
|
|
|
- if (DateUtil.isCellDateFormatted(cell)) {
|
|
|
|
|
- yield cell.getLocalDateTimeCellValue().toString();
|
|
|
|
|
- }
|
|
|
|
|
- double num = cell.getNumericCellValue();
|
|
|
|
|
- if (num == Math.rint(num)) {
|
|
|
|
|
- yield String.valueOf((long) num);
|
|
|
|
|
- }
|
|
|
|
|
- yield String.valueOf(num);
|
|
|
|
|
- }
|
|
|
|
|
- case BOOLEAN -> String.valueOf(cell.getBooleanCellValue());
|
|
|
|
|
- case FORMULA -> cell.getCellFormula();
|
|
|
|
|
- default -> "";
|
|
|
|
|
- };
|
|
|
|
|
- }
|
|
|
|
|
-
|
|
|
|
|
- private Object getCellValue(Cell cell) {
|
|
|
|
|
- if (cell == null) return "";
|
|
|
|
|
- return switch (cell.getCellType()) {
|
|
|
|
|
- case STRING -> cell.getStringCellValue();
|
|
|
|
|
- case NUMERIC -> {
|
|
|
|
|
- if (DateUtil.isCellDateFormatted(cell)) {
|
|
|
|
|
- yield cell.getLocalDateTimeCellValue().toString();
|
|
|
|
|
- }
|
|
|
|
|
- double num = cell.getNumericCellValue();
|
|
|
|
|
- if (num == Math.rint(num)) {
|
|
|
|
|
- yield (long) num;
|
|
|
|
|
- }
|
|
|
|
|
- yield num;
|
|
|
|
|
- }
|
|
|
|
|
- case BOOLEAN -> cell.getBooleanCellValue();
|
|
|
|
|
- case FORMULA -> getCellStringValue(cell);
|
|
|
|
|
- default -> "";
|
|
|
|
|
- };
|
|
|
|
|
- }
|
|
|
|
|
}
|
|
}
|