Forráskód Böngészése

feat: 订单系统按照流程节点、负责人、业务订单号映射导入到“我的流程”

wuwenyi 2 hete
szülő
commit
befb87d3c1
25 módosított fájl, 564 hozzáadás és 232 törlés
  1. 3 8
      pom.xml
  2. 9 0
      src/main/java/com/qqflow/engine/common/PageRequest.java
  3. 0 44
      src/main/java/com/qqflow/engine/common/controller/FileUploadController.java
  4. 100 128
      src/main/java/com/qqflow/engine/common/service/ExcelParseService.java
  5. 6 0
      src/main/java/com/qqflow/engine/domain/flow/assembler/ProcessDefinitionAssembler.java
  6. 7 0
      src/main/java/com/qqflow/engine/domain/flow/controller/ApprovalTaskController.java
  7. 163 0
      src/main/java/com/qqflow/engine/domain/flow/controller/FlowImportController.java
  8. 1 0
      src/main/java/com/qqflow/engine/domain/flow/controller/ProcessDefinitionController.java
  9. 10 6
      src/main/java/com/qqflow/engine/domain/flow/controller/ProcessInstanceController.java
  10. 15 0
      src/main/java/com/qqflow/engine/domain/flow/dto/ProcessDefinitionDTO.java
  11. 6 0
      src/main/java/com/qqflow/engine/domain/flow/dto/ProcessInstanceDTO.java
  12. 6 0
      src/main/java/com/qqflow/engine/domain/flow/dto/StartProcessDTO.java
  13. 10 0
      src/main/java/com/qqflow/engine/domain/flow/mapper/ProcessDefinitionMapper.java
  14. 9 1
      src/main/java/com/qqflow/engine/domain/flow/mapper/ProcessInstanceMapper.java
  15. 12 0
      src/main/java/com/qqflow/engine/domain/flow/po/ProcessDefinition.java
  16. 4 0
      src/main/java/com/qqflow/engine/domain/flow/service/ApprovalTaskService.java
  17. 3 0
      src/main/java/com/qqflow/engine/domain/flow/service/FlowEngineService.java
  18. 7 1
      src/main/java/com/qqflow/engine/domain/flow/service/ProcessInstanceService.java
  19. 28 1
      src/main/java/com/qqflow/engine/domain/flow/service/impl/ApprovalTaskServiceImpl.java
  20. 42 5
      src/main/java/com/qqflow/engine/domain/flow/service/impl/FlowEngineServiceImpl.java
  21. 31 22
      src/main/java/com/qqflow/engine/domain/flow/service/impl/ProcessDefinitionServiceImpl.java
  22. 77 7
      src/main/java/com/qqflow/engine/domain/flow/service/impl/ProcessInstanceServiceImpl.java
  23. 7 0
      src/main/java/com/qqflow/engine/domain/system/mapper/SysUserMapper.java
  24. 0 8
      src/main/resources/application.yml
  25. 8 1
      src/main/resources/mapper/flow/ProcessInstanceMapper.xml

+ 3 - 8
pom.xml

@@ -106,14 +106,9 @@
             <version>1.4.3</version>
         </dependency>
         <dependency>
-            <groupId>org.apache.poi</groupId>
-            <artifactId>poi</artifactId>
-            <version>5.2.5</version>
-        </dependency>
-        <dependency>
-            <groupId>org.apache.poi</groupId>
-            <artifactId>poi-ooxml</artifactId>
-            <version>5.2.5</version>
+            <groupId>com.alibaba</groupId>
+            <artifactId>easyexcel</artifactId>
+            <version>3.3.4</version>
         </dependency>
 
         <!-- JSON解析 -->

+ 9 - 0
src/main/java/com/qqflow/engine/common/PageRequest.java

@@ -0,0 +1,9 @@
+package com.qqflow.engine.common;
+
+import lombok.Data;
+
+@Data
+public class PageRequest {
+    private Integer pageNum = 1;
+    private Integer pageSize = 10;
+}

+ 0 - 44
src/main/java/com/qqflow/engine/common/controller/FileUploadController.java

@@ -1,16 +1,10 @@
 package com.qqflow.engine.common.controller;
 
 import com.qqflow.engine.common.Result;
-import com.qqflow.engine.common.service.ExcelParseService;
 import io.swagger.v3.oas.annotations.Operation;
 import io.swagger.v3.oas.annotations.tags.Tag;
-import jakarta.servlet.http.HttpServletResponse;
 import lombok.RequiredArgsConstructor;
 import org.springframework.beans.factory.annotation.Value;
-import org.springframework.http.MediaType;
-import org.springframework.security.access.prepost.PreAuthorize;
-import org.springframework.web.bind.annotation.GetMapping;
-import org.springframework.web.bind.annotation.PathVariable;
 import org.springframework.web.bind.annotation.PostMapping;
 import org.springframework.web.bind.annotation.RequestMapping;
 import org.springframework.web.bind.annotation.RequestParam;
@@ -25,19 +19,15 @@ import java.nio.file.StandardCopyOption;
 import java.time.LocalDateTime;
 import java.time.format.DateTimeFormatter;
 import java.util.Locale;
-import java.util.Map;
 import java.util.Set;
 import java.util.UUID;
 
 @RestController
 @RequestMapping("/file")
 @Tag(name = "文件上传")
-@PreAuthorize("isAuthenticated()")
 @RequiredArgsConstructor
 public class FileUploadController {
 
-    private final ExcelParseService excelParseService;
-
     private static final String UPLOAD_DIR = "uploads";
 
     private static final Set<String> ALLOWED_EXTENSIONS = Set.of(
@@ -69,7 +59,6 @@ public class FileUploadController {
         if (originalFilename == null || originalFilename.isBlank()) {
             return Result.error("文件名不能为空");
         }
-        // 防止路径穿越
         if (originalFilename.contains("..") || originalFilename.contains("/") || originalFilename.contains("\\")) {
             return Result.error("文件名包含非法字符");
         }
@@ -108,37 +97,4 @@ public class FileUploadController {
         }
         return filename.substring(lastDot + 1);
     }
-
-    @PostMapping(value = "/parse-excel", consumes = MediaType.MULTIPART_FORM_DATA_VALUE)
-    @Operation(summary = "解析 Excel 表单数据")
-    public Result<Map<String, Object>> parseExcel(
-            @RequestParam("file") MultipartFile file,
-            @RequestParam("definitionId") Long definitionId,
-            @RequestParam("mappings") String mappingsJson) {
-        try {
-            com.fasterxml.jackson.databind.ObjectMapper mapper = new com.fasterxml.jackson.databind.ObjectMapper();
-            Map<String, String> mappings = mapper.readValue(mappingsJson, new com.fasterxml.jackson.core.type.TypeReference<>() {});
-            Map<String, Object> formData = this.excelParseService.parseExcel(file, definitionId, mappings);
-            return Result.ok(formData);
-        } catch (com.fasterxml.jackson.core.JsonProcessingException e) {
-            return Result.error("字段映射格式错误");
-        } catch (Exception e) {
-            return Result.error(e.getMessage());
-        }
-    }
-
-    @GetMapping("/template/{definitionId}")
-    @Operation(summary = "下载流程表单 Excel 模板")
-    public void downloadTemplate(@PathVariable Long definitionId, HttpServletResponse response) {
-        try {
-            byte[] bytes = this.excelParseService.generateTemplate(definitionId);
-            response.setContentType("application/vnd.openxmlformats-officedocument.spreadsheetml.sheet");
-            response.setHeader("Content-Disposition", "attachment; filename=template.xlsx");
-            response.setContentLength(bytes.length);
-            response.getOutputStream().write(bytes);
-            response.getOutputStream().flush();
-        } catch (Exception e) {
-            response.setStatus(HttpServletResponse.SC_INTERNAL_SERVER_ERROR);
-        }
-    }
 }

+ 100 - 128
src/main/java/com/qqflow/engine/common/service/ExcelParseService.java

@@ -1,26 +1,20 @@
 package com.qqflow.engine.common.service;
 
+import com.alibaba.excel.EasyExcel;
 import com.fasterxml.jackson.databind.ObjectMapper;
 import com.qqflow.engine.common.exception.BusinessException;
 import com.qqflow.engine.domain.flow.mapper.ProcessDefinitionMapper;
 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 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.web.multipart.MultipartFile;
 
 import java.io.ByteArrayOutputStream;
 import java.io.IOException;
-import java.io.InputStream;
+import java.util.ArrayList;
 import java.util.Collections;
-import java.util.HashMap;
 import java.util.LinkedHashMap;
 import java.util.List;
 import java.util.Map;
@@ -33,15 +27,19 @@ public class ExcelParseService {
     private static final ObjectMapper OBJECT_MAPPER = new ObjectMapper();
 
     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) {
+        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()) {
             throw new BusinessException("文件不能为空");
         }
@@ -53,88 +51,109 @@ public class ExcelParseService {
             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 至少需要包含表头和一行数据");
             }
-            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, String> headerRow = (Map<Integer, String>) 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, String> dataRow = (Map<Integer, String>) rawRows.get(rowIdx);
+                // 跳过全空行
+                if (dataRow.values().stream().allMatch(v -> v == null || v.isBlank())) continue;
+
+                Map<String, Object> formData = new LinkedHashMap<>();
+                String nodeName = null;
+                String ownerName = null;
+                String bizValue = null;
+
+                for (Map.Entry<Integer, String> entry : headerRow.entrySet()) {
+                    int col = entry.getKey();
+                    String excelColumn = entry.getValue() != null ? entry.getValue().trim() : "";
+                    if (excelColumn.isEmpty()) continue;
+
+                    String value = dataRow.get(col);
+                    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 数据行为空");
-            }
 
-            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;
+                Long ownerId = null;
+                if (ownerName != null && !ownerName.isBlank()) {
+                    SysUser user = sysUserMapper.selectByRealName(ownerName.trim());
+                    if (user != null) {
+                        ownerId = user.getId();
+                    }
                 }
-                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);
+                results.add(rowResult);
             }
-            return formData;
+            return results;
         } catch (IOException e) {
             throw new BusinessException("Excel 解析失败: " + e.getMessage());
         }
     }
 
-    /**
-     * 生成 Excel 模板文件字节数组
-     */
+    /** 生成 Excel 模板 */
     public byte[] generateTemplate(Long definitionId) {
         ProcessDefinition definition = this.processDefinitionMapper.selectById(definitionId);
         if (definition == null) {
             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("示例值");
-            }
-            // 自动调整列宽
-            for (int i = 0; i < fields.size(); i++) {
-                sheet.autoSizeColumn(i);
-            }
+        List<Map<String, Object>> fields = parseFormSchema(definition.getFormSchema());
+        String nodeFieldName = definition.getNodeFieldName();
+        String ownerFieldName = definition.getOwnerFieldName();
 
-            try (ByteArrayOutputStream bos = new ByteArrayOutputStream()) {
-                workbook.write(bos);
-                return bos.toByteArray();
-            }
+        List<List<String>> head = new ArrayList<>();
+        if (nodeFieldName != null && !nodeFieldName.isBlank()) {
+            head.add(Collections.singletonList(nodeFieldName));
+        }
+        if (ownerFieldName != null && !ownerFieldName.isBlank()) {
+            head.add(Collections.singletonList(ownerFieldName));
+        }
+        for (Map<String, Object> field : fields) {
+            String label = Objects.toString(field.get("label"), Objects.toString(field.get("name"), ""));
+            head.add(Collections.singletonList(label));
+        }
+
+        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) {
             throw new BusinessException("模板生成失败: " + e.getMessage());
         }
@@ -151,51 +170,4 @@ public class ExcelParseService {
             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 -> "";
-        };
-    }
 }

+ 6 - 0
src/main/java/com/qqflow/engine/domain/flow/assembler/ProcessDefinitionAssembler.java

@@ -13,6 +13,9 @@ public class ProcessDefinitionAssembler {
         po.setCategory(dto.getCategory());
         po.setFormId(dto.getFormId());
         po.setFormSchema(dto.getFormSchema());
+        po.setNodeFieldName(dto.getNodeFieldName());
+        po.setOwnerFieldName(dto.getOwnerFieldName());
+        po.setBizFieldName(dto.getBizFieldName());
         po.setModelJson(dto.getModelJson());
         po.setVersion(1);
         po.setStatus(0);
@@ -29,6 +32,9 @@ public class ProcessDefinitionAssembler {
         po.setCategory(dto.getCategory());
         po.setFormId(dto.getFormId());
         po.setFormSchema(dto.getFormSchema());
+        po.setNodeFieldName(dto.getNodeFieldName());
+        po.setOwnerFieldName(dto.getOwnerFieldName());
+        po.setBizFieldName(dto.getBizFieldName());
         po.setModelJson(dto.getModelJson());
         po.setDescription(dto.getDescription());
         return po;

+ 7 - 0
src/main/java/com/qqflow/engine/domain/flow/controller/ApprovalTaskController.java

@@ -9,6 +9,7 @@ import com.qqflow.engine.domain.flow.dto.ApprovalRecordDTO;
 import com.qqflow.engine.domain.flow.dto.ApprovalTaskDTO;
 import com.qqflow.engine.domain.flow.dto.ApproveTaskDTO;
 import com.qqflow.engine.domain.flow.dto.BatchTaskDTO;
+import com.qqflow.engine.domain.flow.dto.NextNodeDTO;
 import com.qqflow.engine.domain.flow.dto.TransferTaskDTO;
 import com.qqflow.engine.domain.flow.service.ApprovalTaskService;
 import io.swagger.v3.oas.annotations.Operation;
@@ -55,6 +56,12 @@ public class ApprovalTaskController {
         return Result.ok(this.approvalTaskService.handledList(assigneeId, assigneeType, processName, pageNum, pageSize));
     }
 
+    @GetMapping("/{taskId}/next-nodes")
+    @Operation(summary = "获取当前任务的下游节点列表(条件节点选分支用)")
+    public Result<List<NextNodeDTO>> nextNodes(@PathVariable Long taskId) {
+        return Result.ok(this.approvalTaskService.getNextNodes(taskId));
+    }
+
     @PostMapping("/{taskId}/approve")
     @Operation(summary = "审批通过")
     public Result<Void> approve(@PathVariable Long taskId, @RequestBody @Valid ApproveTaskDTO dto) {

+ 163 - 0
src/main/java/com/qqflow/engine/domain/flow/controller/FlowImportController.java

@@ -0,0 +1,163 @@
+package com.qqflow.engine.domain.flow.controller;
+
+import com.qqflow.engine.common.Result;
+import com.qqflow.engine.common.service.ExcelParseService;
+import com.qqflow.engine.common.util.SecurityUtils;
+import com.qqflow.engine.config.security.LoginUser;
+import com.qqflow.engine.domain.flow.dto.StartProcessDTO;
+import com.qqflow.engine.domain.flow.mapper.ProcessDefinitionMapper;
+import com.qqflow.engine.domain.flow.mapper.ProcessInstanceMapper;
+import com.qqflow.engine.domain.flow.po.ProcessDefinition;
+import com.qqflow.engine.domain.flow.po.ProcessInstance;
+import com.qqflow.engine.domain.flow.service.ProcessInstanceService;
+import io.swagger.v3.oas.annotations.Operation;
+import io.swagger.v3.oas.annotations.tags.Tag;
+import jakarta.servlet.http.HttpServletResponse;
+import lombok.RequiredArgsConstructor;
+import org.springframework.http.MediaType;
+import org.springframework.web.bind.annotation.GetMapping;
+import org.springframework.web.bind.annotation.PathVariable;
+import org.springframework.web.bind.annotation.PostMapping;
+import org.springframework.web.bind.annotation.RequestMapping;
+import org.springframework.web.bind.annotation.RequestParam;
+import org.springframework.web.bind.annotation.RestController;
+import org.springframework.web.multipart.MultipartFile;
+
+import java.util.ArrayList;
+import java.util.LinkedHashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.Objects;
+
+@Tag(name = "流程导入")
+@RestController
+@RequestMapping("/flow/import")
+@RequiredArgsConstructor
+public class FlowImportController {
+
+    private final ExcelParseService excelParseService;
+    private final ProcessInstanceService processInstanceService;
+    private final ProcessDefinitionMapper processDefinitionMapper;
+    private final ProcessInstanceMapper processInstanceMapper;
+
+    @GetMapping("/template/{definitionId}")
+    @Operation(summary = "下载流程 Excel 导入模板")
+    public void downloadTemplate(@PathVariable Long definitionId, HttpServletResponse response) {
+        try {
+            byte[] bytes = this.excelParseService.generateTemplate(definitionId);
+            response.setContentType("application/vnd.openxmlformats-officedocument.spreadsheetml.sheet");
+            response.setHeader("Content-Disposition", "attachment; filename=template.xlsx");
+            response.setContentLength(bytes.length);
+            response.getOutputStream().write(bytes);
+            response.getOutputStream().flush();
+        } catch (Exception e) {
+            response.setStatus(HttpServletResponse.SC_INTERNAL_SERVER_ERROR);
+        }
+    }
+
+    @PostMapping(value = "/parse", consumes = MediaType.MULTIPART_FORM_DATA_VALUE)
+    @Operation(summary = "解析 Excel 单行表单数据(用于发起流程时填充表单)")
+    public Result<Map<String, Object>> parseExcel(
+            @RequestParam("file") MultipartFile file,
+            @RequestParam("definitionId") Long definitionId,
+            @RequestParam("mappings") String mappingsJson) {
+        try {
+            com.fasterxml.jackson.databind.ObjectMapper mapper = new com.fasterxml.jackson.databind.ObjectMapper();
+            Map<String, String> mappings = mapper.readValue(mappingsJson,
+                    new com.fasterxml.jackson.core.type.TypeReference<>() {});
+            Map<String, Object> formData = this.excelParseService.parseExcel(file, definitionId, mappings);
+            return Result.ok(formData);
+        } catch (com.fasterxml.jackson.core.JsonProcessingException e) {
+            return Result.error("字段映射格式错误");
+        } catch (Exception e) {
+            return Result.error(e.getMessage());
+        }
+    }
+
+    @PostMapping(value = "/execute", consumes = MediaType.MULTIPART_FORM_DATA_VALUE)
+    @Operation(summary = "批量导入流程:解析 Excel 并发起/更新流程")
+    public Result<Map<String, Object>> importFlow(
+            @RequestParam("file") MultipartFile file,
+            @RequestParam("definitionId") Long definitionId,
+            @RequestParam("mappings") String mappingsJson) {
+        try {
+            com.fasterxml.jackson.databind.ObjectMapper mapper = new com.fasterxml.jackson.databind.ObjectMapper();
+            Map<String, String> mappings = mapper.readValue(mappingsJson,
+                    new com.fasterxml.jackson.core.type.TypeReference<>() {});
+
+            ProcessDefinition definition = this.processDefinitionMapper.selectById(definitionId);
+            if (definition == null) {
+                return Result.error("流程定义不存在");
+            }
+
+            List<Map<String, Object>> rows = this.excelParseService.parseExcelRows(file, definitionId, mappings);
+            LoginUser loginUser = SecurityUtils.getLoginUser();
+            boolean isAdmin = loginUser != null && loginUser.isAdmin();
+            String titlePrefix = definition.getProcessName() + "-"
+                    + java.time.LocalDateTime.now().format(java.time.format.DateTimeFormatter.ofPattern("MMddHHmm"));
+
+            List<Long> instanceIds = new ArrayList<>();
+            int successCount = 0;
+            int updateCount = 0;
+            for (int i = 0; i < rows.size(); i++) {
+                Map<String, Object> row = rows.get(i);
+                @SuppressWarnings("unchecked")
+                Map<String, Object> formData = (Map<String, Object>) row.get("formData");
+                String nodeName = (String) row.get("nodeName");
+                Long ownerId = (Long) row.get("ownerId");
+                if (Objects.isNull(ownerId)) {
+                    continue;
+                }
+                String bizValue = (String) row.get("bizValue");
+
+                Long effectiveOwnerId = null;
+                if (isAdmin) {
+                    effectiveOwnerId = ownerId;
+                } else if (ownerId.equals(loginUser.getUserId())) {
+                    effectiveOwnerId = ownerId;
+                }
+                if (effectiveOwnerId == null) continue;
+
+                if (bizValue != null && !bizValue.isBlank()) {
+                    ProcessInstance existing = this.processInstanceMapper.selectByInstanceNo(bizValue);
+                    if (existing != null) {
+                        this.processInstanceService.updateFormData(
+                                existing.getId(), mapper.writeValueAsString(formData), nodeName);
+                        instanceIds.add(existing.getId());
+                        updateCount++;
+                        continue;
+                    }
+                }
+
+                StartProcessDTO dto = new StartProcessDTO();
+                dto.setProcessDefinitionId(definitionId);
+                dto.setTitle(titlePrefix + "-" + (i + 1));
+                dto.setFormData(mapper.writeValueAsString(formData));
+                if (bizValue != null && !bizValue.isBlank()) {
+                    dto.setInstanceNo(bizValue);
+                }
+                dto.setApplicantId(effectiveOwnerId);
+
+                Long instanceId;
+                if (nodeName != null && !nodeName.isBlank()) {
+                    instanceId = this.processInstanceService.startProcessAtNode(dto, nodeName);
+                } else {
+                    instanceId = this.processInstanceService.startProcess(dto);
+                }
+                instanceIds.add(instanceId);
+                successCount++;
+            }
+
+            Map<String, Object> response = new LinkedHashMap<>();
+            response.put("total", rows.size());
+            response.put("successCount", successCount);
+            response.put("updateCount", updateCount);
+            response.put("instanceIds", instanceIds);
+            return Result.ok(response);
+        } catch (com.fasterxml.jackson.core.JsonProcessingException e) {
+            return Result.error("字段映射格式错误");
+        } catch (Exception e) {
+            return Result.error("导入失败: " + e.getMessage());
+        }
+    }
+}

+ 1 - 0
src/main/java/com/qqflow/engine/domain/flow/controller/ProcessDefinitionController.java

@@ -58,6 +58,7 @@ public class ProcessDefinitionController {
 
     @GetMapping("/{id}")
     @Operation(summary = "流程定义详情")
+    @PreAuthorize("isAuthenticated()")
     public Result<ProcessDefinitionDTO> getById(@PathVariable Long id) {
         return Result.ok(this.processDefinitionService.getById(id));
     }

+ 10 - 6
src/main/java/com/qqflow/engine/domain/flow/controller/ProcessInstanceController.java

@@ -40,13 +40,15 @@ public class ProcessInstanceController {
     }
 
     @GetMapping("/page")
-    @Operation(summary = "流程实例分页列表")
+    @Operation(summary = "流程实例分页列表(管理员)")
     public Result<PageResult<ProcessInstanceDTO>> page(
             @RequestParam(defaultValue = "1") Integer pageNum,
             @RequestParam(defaultValue = "10") Integer pageSize,
-            @RequestParam(required = false) Integer status) {
-        Long applicantId = SecurityUtils.getUserId();
-        return Result.ok(this.processInstanceService.list(applicantId, status, pageNum, pageSize));
+            @RequestParam(required = false) Integer status,
+            @RequestParam(required = false) String definitionName,
+            @RequestParam(required = false) String currentNodeName,
+            @RequestParam(required = false) String applicantName) {
+        return Result.ok(this.processInstanceService.list(null, status, definitionName, currentNodeName, applicantName, pageNum, pageSize));
     }
 
     @GetMapping("/mine")
@@ -54,9 +56,11 @@ public class ProcessInstanceController {
     public Result<PageResult<ProcessInstanceDTO>> mine(
             @RequestParam(defaultValue = "1") Integer pageNum,
             @RequestParam(defaultValue = "10") Integer pageSize,
-            @RequestParam(required = false) Integer status) {
+            @RequestParam(required = false) Integer status,
+            @RequestParam(required = false) String definitionName,
+            @RequestParam(required = false) String currentNodeName) {
         Long applicantId = SecurityUtils.getUserId();
-        return Result.ok(this.processInstanceService.list(applicantId, status, pageNum, pageSize));
+        return Result.ok(this.processInstanceService.list(applicantId, status, definitionName, currentNodeName, null, pageNum, pageSize));
     }
 
     @GetMapping("/{id}")

+ 15 - 0
src/main/java/com/qqflow/engine/domain/flow/dto/ProcessDefinitionDTO.java

@@ -36,6 +36,18 @@ public class ProcessDefinitionDTO {
     @Schema(description = "表单字段定义JSON")
     private String formSchema;
 
+    @JsonProperty("nodeFieldName")
+    @Schema(description = "节点名称字段:导入时按此字段匹配流程节点")
+    private String nodeFieldName;
+
+    @JsonProperty("ownerFieldName")
+    @Schema(description = "归属人字段:导入时按此字段匹配 sys_user 作为发起人")
+    private String ownerFieldName;
+
+    @JsonProperty("bizFieldName")
+    @Schema(description = "业务编号字段:列表中显示的业务标识对应的表单字段名")
+    private String bizFieldName;
+
     @JsonProperty("flowJson")
     @Schema(description = "模型JSON")
     private String modelJson;
@@ -69,6 +81,9 @@ public class ProcessDefinitionDTO {
         dto.setCategory(po.getCategory());
         dto.setFormId(po.getFormId());
         dto.setFormSchema(po.getFormSchema());
+        dto.setNodeFieldName(po.getNodeFieldName());
+        dto.setOwnerFieldName(po.getOwnerFieldName());
+        dto.setBizFieldName(po.getBizFieldName());
         dto.setModelJson(po.getModelJson());
         dto.setVersion(po.getVersion());
         dto.setStatus(po.getStatus());

+ 6 - 0
src/main/java/com/qqflow/engine/domain/flow/dto/ProcessInstanceDTO.java

@@ -67,6 +67,12 @@ public class ProcessInstanceDTO {
     @Schema(description = "更新时间")
     private LocalDateTime updateTime;
 
+    @Schema(description = "业务编号值(从formData中按bizFieldName提取)")
+    private String bizValue;
+
+    @Schema(description = "发起人姓名")
+    private String applicantName;
+
     @Schema(description = "流程名称")
     private String definitionName;
 

+ 6 - 0
src/main/java/com/qqflow/engine/domain/flow/dto/StartProcessDTO.java

@@ -20,4 +20,10 @@ public class StartProcessDTO {
 
     @Schema(description = "附件URL列表JSON")
     private String attachmentUrls;
+
+    @Schema(description = "指定发起人ID(导入时使用),为空则取当前登录用户")
+    private Long applicantId;
+
+    @Schema(description = "指定实例编号(导入时使用),为空则自动生成")
+    private String instanceNo;
 }

+ 10 - 0
src/main/java/com/qqflow/engine/domain/flow/mapper/ProcessDefinitionMapper.java

@@ -35,4 +35,14 @@ public interface ProcessDefinitionMapper extends BaseMapper<ProcessDefinition> {
         wrapper.eq(ProcessDefinition::getStatus, 1);
         return this.selectList(wrapper);
     }
+
+    default Integer selectMaxVersion(String processCode) {
+        LambdaQueryWrapper<ProcessDefinition> wrapper = new LambdaQueryWrapper<>();
+        wrapper.select(ProcessDefinition::getVersion)
+                .eq(ProcessDefinition::getProcessCode, processCode)
+                .orderByDesc(ProcessDefinition::getVersion)
+                .last("limit 1");
+        ProcessDefinition result = this.selectOne(wrapper);
+        return result != null ? result.getVersion() : null;
+    }
 }

+ 9 - 1
src/main/java/com/qqflow/engine/domain/flow/mapper/ProcessInstanceMapper.java

@@ -11,10 +11,18 @@ public interface ProcessInstanceMapper extends BaseMapper<ProcessInstance> {
     com.baomidou.mybatisplus.extension.plugins.pagination.Page<ProcessInstance> selectInstanceList(
             com.baomidou.mybatisplus.extension.plugins.pagination.Page<ProcessInstance> page,
             @Param("applicantId") Long applicantId,
-            @Param("status") Integer status);
+            @Param("status") Integer status,
+            @Param("definitionName") String definitionName,
+            @Param("currentNodeName") String currentNodeName,
+            @Param("applicantName") String applicantName);
 
     com.baomidou.mybatisplus.extension.plugins.pagination.Page<ProcessInstance> selectParticipatedList(
             com.baomidou.mybatisplus.extension.plugins.pagination.Page<ProcessInstance> page,
             @Param("userId") Long userId,
             @Param("userType") String userType);
+
+    default ProcessInstance selectByInstanceNo(String instanceNo) {
+        return this.selectOne(new com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper<ProcessInstance>()
+                .eq(ProcessInstance::getInstanceNo, instanceNo));
+    }
 }

+ 12 - 0
src/main/java/com/qqflow/engine/domain/flow/po/ProcessDefinition.java

@@ -39,6 +39,18 @@ public class ProcessDefinition {
     @Schema(description = "表单字段定义JSON")
     private String formSchema;
 
+    @TableField("node_field_name")
+    @Schema(description = "节点名称字段:导入时按此字段匹配流程节点")
+    private String nodeFieldName;
+
+    @TableField("owner_field_name")
+    @Schema(description = "归属人字段:导入时按此字段匹配 sys_user 作为发起人")
+    private String ownerFieldName;
+
+    @TableField("biz_field_name")
+    @Schema(description = "业务编号字段:列表中显示的业务标识对应的表单字段名")
+    private String bizFieldName;
+
     @TableField("model_json")
     @Schema(description = "模型JSON")
     private String modelJson;

+ 4 - 0
src/main/java/com/qqflow/engine/domain/flow/service/ApprovalTaskService.java

@@ -5,6 +5,7 @@ import com.qqflow.engine.domain.flow.dto.ApprovalRecordDTO;
 import com.qqflow.engine.domain.flow.dto.ApprovalTaskDTO;
 import com.qqflow.engine.domain.flow.dto.ApproveTaskDTO;
 import com.qqflow.engine.domain.flow.dto.BatchTaskDTO;
+import com.qqflow.engine.domain.flow.dto.NextNodeDTO;
 import com.qqflow.engine.domain.flow.dto.TransferTaskDTO;
 
 import java.util.List;
@@ -36,4 +37,7 @@ public interface ApprovalTaskService {
     PageResult<ApprovalTaskDTO> ccList(Long assigneeId, String assigneeType, String processName, Integer pageNum, Integer pageSize);
 
     void readCc(Long taskId);
+
+    /** 获取下游节点列表(条件节点分支选择用) */
+    List<NextNodeDTO> getNextNodes(Long taskId);
 }

+ 3 - 0
src/main/java/com/qqflow/engine/domain/flow/service/FlowEngineService.java

@@ -28,4 +28,7 @@ public interface FlowEngineService {
     void executeTransition(ProcessInstance instance, ApprovalTask currentTask, ApprovalAction action, String comment, String targetNodeId);
 
     void startInstance(ProcessInstance instance, ProcessDefinition definition);
+
+    /** 从指定节点发起流程(导入时使用),跳过前置节点 */
+    void startInstanceAtNode(ProcessInstance instance, ProcessDefinition definition, String nodeName);
 }

+ 7 - 1
src/main/java/com/qqflow/engine/domain/flow/service/ProcessInstanceService.java

@@ -12,7 +12,10 @@ public interface ProcessInstanceService {
 
     Long startProcess(StartProcessDTO dto);
 
-    PageResult<ProcessInstanceDTO> list(Long applicantId, Integer status, Integer pageNum, Integer pageSize);
+    /** 从指定节点发起流程(导入时使用),前置节点自动跳过 */
+    Long startProcessAtNode(StartProcessDTO dto, String nodeName);
+
+    PageResult<ProcessInstanceDTO> list(Long applicantId, Integer status, String definitionName, String currentNodeName, String applicantName, Integer pageNum, Integer pageSize);
 
     ProcessInstanceDTO getDetail(Long id);
 
@@ -25,4 +28,7 @@ public interface ProcessInstanceService {
     PageResult<ProcessInstanceDTO> participatedList(Long userId, String userType, Integer pageNum, Integer pageSize);
 
     List<AttachmentDTO> listAttachments(Long instanceId);
+
+    /** 更新已有流程实例的表单数据,并可推进到指定节点(导入更新用) */
+    void updateFormData(Long instanceId, String formData, String nodeName);
 }

+ 28 - 1
src/main/java/com/qqflow/engine/domain/flow/service/impl/ApprovalTaskServiceImpl.java

@@ -13,6 +13,7 @@ import com.qqflow.engine.domain.flow.dto.ApprovalRecordDTO;
 import com.qqflow.engine.domain.flow.dto.ApprovalTaskDTO;
 import com.qqflow.engine.domain.flow.dto.ApproveTaskDTO;
 import com.qqflow.engine.domain.flow.dto.BatchTaskDTO;
+import com.qqflow.engine.domain.flow.dto.NextNodeDTO;
 import com.qqflow.engine.domain.flow.dto.TransferTaskDTO;
 import com.qqflow.engine.domain.flow.enums.ApprovalAction;
 import com.qqflow.engine.domain.flow.enums.ApprovalResult;
@@ -23,10 +24,13 @@ import com.qqflow.engine.domain.flow.event.TaskCompletedEvent;
 import com.qqflow.engine.domain.flow.mapper.ApprovalRecordMapper;
 import com.qqflow.engine.domain.flow.mapper.ApprovalTaskMapper;
 import com.qqflow.engine.domain.flow.mapper.AttachmentMapper;
+import com.qqflow.engine.domain.flow.mapper.ProcessDefinitionMapper;
 import com.qqflow.engine.domain.flow.mapper.ProcessInstanceMapper;
+import com.qqflow.engine.domain.flow.model.FlowModel;
 import com.qqflow.engine.domain.flow.po.ApprovalRecord;
 import com.qqflow.engine.domain.flow.po.ApprovalTask;
 import com.qqflow.engine.domain.flow.po.Attachment;
+import com.qqflow.engine.domain.flow.po.ProcessDefinition;
 import com.qqflow.engine.domain.flow.po.ProcessInstance;
 import com.qqflow.engine.domain.flow.service.ApprovalTaskService;
 import com.qqflow.engine.domain.flow.service.FlowEngineService;
@@ -72,6 +76,7 @@ public class ApprovalTaskServiceImpl implements ApprovalTaskService {
     private final ApprovalTaskDTOAssembler approvalTaskDTOAssembler;
     private final ApprovalTaskHelper approvalTaskHelper;
     private final FlowEngineService flowEngineService;
+    private final ProcessDefinitionMapper processDefinitionMapper;
     private final ApplicationEventPublisher eventPublisher;
     private final SysUserRoleMapper sysUserRoleMapper;
     private final SysUserMapper sysUserMapper;
@@ -126,7 +131,7 @@ public class ApprovalTaskServiceImpl implements ApprovalTaskService {
         ProcessInstance instance = this.getActiveInstance(task.getInstanceId());
         Long operatorId = SecurityUtils.getUserId();
         this.saveRecord(task, instance, operatorId, ApprovalAction.APPROVE, ApprovalResult.PASS.getCode(), dto.getComment(), dto.getAttachmentUrls());
-        this.flowEngineService.executeTransition(instance, task, ApprovalAction.APPROVE, dto.getComment());
+        this.flowEngineService.executeTransition(instance, task, ApprovalAction.APPROVE, dto.getComment(), dto.getTargetNodeId());
         this.publishTaskCompletedEvent(task, instance, operatorId, ApprovalAction.APPROVE.getCode(), ApprovalResult.PASS.getCode());
     }
 
@@ -551,4 +556,26 @@ public class ApprovalTaskServiceImpl implements ApprovalTaskService {
         }
         return Collections.emptyList();
     }
+
+    @Override
+    public List<NextNodeDTO> getNextNodes(Long taskId) {
+        ApprovalTask task = this.approvalTaskMapper.selectById(taskId);
+        if (task == null) throw new BusinessException("任务不存在");
+        ProcessInstance instance = this.processInstanceMapper.selectById(task.getInstanceId());
+        if (instance == null) throw new BusinessException("流程实例不存在");
+        ProcessDefinition definition = this.processDefinitionMapper.selectById(instance.getProcessDefinitionId());
+        if (definition == null) throw new BusinessException("流程定义不存在");
+        FlowModel model = this.flowEngineService.parseModel(definition.getModelJson());
+        // 返回所有下游节点(忽略条件),审批人手动选分支
+        return this.flowEngineService.getNextNodes(model, task.getNodeId()).stream()
+                .filter(n -> !NodeType.START.getCode().equals(n.getType())
+                        && !NodeType.END.getCode().equals(n.getType()))
+                .map(n -> {
+                    NextNodeDTO dto = new NextNodeDTO();
+                    dto.setNodeId(n.getId());
+                    dto.setNodeName(n.getName());
+                    dto.setNodeType(n.getType());
+                    return dto;
+                }).collect(Collectors.toList());
+    }
 }

+ 42 - 5
src/main/java/com/qqflow/engine/domain/flow/service/impl/FlowEngineServiceImpl.java

@@ -208,7 +208,7 @@ public class FlowEngineServiceImpl implements FlowEngineService {
     @Transactional(rollbackFor = Exception.class)
     public void executeTransition(ProcessInstance instance, ApprovalTask currentTask, ApprovalAction action, String comment, String targetNodeId) {
         switch (action) {
-            case APPROVE -> this.handleApprove(instance, currentTask, comment);
+            case APPROVE -> this.handleApprove(instance, currentTask, comment, targetNodeId);
             case REJECT -> this.handleReject(instance, currentTask, comment);
             case RETURN -> this.handleReturn(instance, currentTask, comment, targetNodeId);
             default -> throw new BusinessException("不支持的操作类型");
@@ -221,7 +221,6 @@ public class FlowEngineServiceImpl implements FlowEngineService {
         if (instance == null || instance.getId() == null) {
             return;
         }
-        // 防御重复启动:已存在任务则直接返回
         long existingTaskCount = this.approvalTaskMapper.selectCount(
                 new LambdaQueryWrapper<ApprovalTask>()
                         .eq(ApprovalTask::getInstanceId, instance.getId()));
@@ -235,6 +234,34 @@ public class FlowEngineServiceImpl implements FlowEngineService {
         this.updateInstanceNode(instance, nextNodes);
     }
 
+    @Override
+    @Transactional(rollbackFor = Exception.class)
+    public void startInstanceAtNode(ProcessInstance instance, ProcessDefinition definition, String nodeName) {
+        if (instance == null || instance.getId() == null) {
+            return;
+        }
+        long existingTaskCount = this.approvalTaskMapper.selectCount(
+                new LambdaQueryWrapper<ApprovalTask>()
+                        .eq(ApprovalTask::getInstanceId, instance.getId()));
+        if (existingTaskCount > 0) {
+            return;
+        }
+        FlowModel model = this.parseModel(definition.getModelJson());
+        // 按名称匹配目标节点
+        FlowNode targetNode = safeNodes(model).stream()
+                .filter(n -> nodeName.equals(n.getName()))
+                .findFirst()
+                .orElse(null);
+        if (targetNode == null) {
+            this.startInstance(instance, definition);
+            return;
+        }
+        // 直接在目标节点创建待处理任务,不处理前置节点
+        List<FlowNode> targetNodes = Collections.singletonList(targetNode);
+        this.createTasksForNodes(instance, targetNodes, model);
+        this.updateInstanceNode(instance, targetNodes);
+    }
+
     private List<Long> doCalculateAssignees(String assigneeType, String assigneeValue, ProcessInstance instance) {
         if ("USER".equals(assigneeType) && assigneeValue != null) {
             List<Long> list = new ArrayList<>();
@@ -274,7 +301,7 @@ public class FlowEngineServiceImpl implements FlowEngineService {
         return Collections.emptyList();
     }
 
-    private void handleApprove(ProcessInstance instance, ApprovalTask currentTask, String comment) {
+    private void handleApprove(ProcessInstance instance, ApprovalTask currentTask, String comment, String targetNodeId) {
         if (instance == null || instance.getId() == null || currentTask == null || currentTask.getId() == null) {
             throw new BusinessException("参数不能为空");
         }
@@ -323,7 +350,18 @@ public class FlowEngineServiceImpl implements FlowEngineService {
                             .set(ApprovalTask::getTaskStatus, TaskStatus.SKIPPED.getCode()));
         }
 
-        List<FlowNode> nextNodes = this.getNextNodes(model, lockedTask.getNodeId(), instance);
+        // 指定了目标节点 → 跳过条件判断,直接走该分支
+        List<FlowNode> nextNodes;
+        if (targetNodeId != null && !targetNodeId.isBlank()) {
+            FlowNode targetNode = safeNodes(model).stream()
+                    .filter(n -> n.getId().equals(targetNodeId) && !NodeType.START.getCode().equals(n.getType()))
+                    .findFirst()
+                    .orElse(null);
+            if (targetNode == null) throw new BusinessException("指定的目标节点不存在");
+            nextNodes = Collections.singletonList(targetNode);
+        } else {
+            nextNodes = this.getNextNodes(model, lockedTask.getNodeId(), instance);
+        }
         if (this.isEndNode(nextNodes)) {
             this.completeInstance(instance);
             return;
@@ -552,7 +590,6 @@ public class FlowEngineServiceImpl implements FlowEngineService {
                 taskAssigneeType = ASSIGNEE_TYPE_USER;
             }
 
-            String approveMode = this.getApproveMode(node);
             LocalDateTime timeoutTime = this.calculateTimeoutTime(node);
             String timeoutAction = this.calculateTimeoutAction(node);
             List<Long> finalAssignees = assignees;

+ 31 - 22
src/main/java/com/qqflow/engine/domain/flow/service/impl/ProcessDefinitionServiceImpl.java

@@ -51,34 +51,43 @@ public class ProcessDefinitionServiceImpl implements ProcessDefinitionService {
         if (existing == null) {
             throw new BusinessException("流程定义不存在");
         }
-        if (DefinitionStatus.DESIGNING.getCode().equals(existing.getStatus())) {
-            // 设计中的流程直接更新
-            this.processDefinitionMapper.update(null, new LambdaUpdateWrapper<ProcessDefinition>()
+        // 只有 modelJson(流程设计)真正变化 + 非设计状态 才创建新版本
+        // formSchema、元信息等变化直接更新
+        boolean modelChanged = po.getModelJson() != null && !po.getModelJson().equals(existing.getModelJson());
+
+        if (!modelChanged || DefinitionStatus.DESIGNING.getCode().equals(existing.getStatus())) {
+            LambdaUpdateWrapper<ProcessDefinition> wrapper = new LambdaUpdateWrapper<ProcessDefinition>()
                     .eq(ProcessDefinition::getId, po.getId())
                     .set(ProcessDefinition::getProcessCode, po.getProcessCode())
                     .set(ProcessDefinition::getProcessName, po.getProcessName())
                     .set(ProcessDefinition::getCategory, po.getCategory())
-                    .set(ProcessDefinition::getFormId, po.getFormId())
-                    .set(ProcessDefinition::getFormSchema, po.getFormSchema())
-                    .set(ProcessDefinition::getModelJson, po.getModelJson())
-                    .set(ProcessDefinition::getDescription, po.getDescription()));
+                    .set(ProcessDefinition::getDescription, po.getDescription());
+            if (po.getFormSchema() != null) wrapper.set(ProcessDefinition::getFormSchema, po.getFormSchema());
+            if (po.getModelJson() != null) wrapper.set(ProcessDefinition::getModelJson, po.getModelJson());
+            if (po.getNodeFieldName() != null) wrapper.set(ProcessDefinition::getNodeFieldName, po.getNodeFieldName());
+            if (po.getOwnerFieldName() != null) wrapper.set(ProcessDefinition::getOwnerFieldName, po.getOwnerFieldName());
+            if (po.getBizFieldName() != null) wrapper.set(ProcessDefinition::getBizFieldName, po.getBizFieldName());
+            this.processDefinitionMapper.update(null, wrapper);
             return po.getId();
-        } else {
-            // 已发布/已停用的流程,复制为新版本进行设计
-            ProcessDefinition newDef = new ProcessDefinition();
-            newDef.setProcessCode(po.getProcessCode());
-            newDef.setProcessName(po.getProcessName());
-            newDef.setCategory(po.getCategory());
-            newDef.setFormId(po.getFormId());
-            newDef.setFormSchema(po.getFormSchema());
-            newDef.setModelJson(po.getModelJson());
-            newDef.setDescription(po.getDescription());
-            newDef.setVersion(existing.getVersion() + 1);
-            newDef.setStatus(DefinitionStatus.DESIGNING.getCode());
-            newDef.setCreateBy(existing.getCreateBy());
-            this.processDefinitionMapper.insert(newDef);
-            return newDef.getId();
         }
+
+        // 已发布/已停用的流程,且设计有变化:复制为新版本
+        ProcessDefinition newDef = new ProcessDefinition();
+        newDef.setProcessCode(po.getProcessCode());
+        newDef.setProcessName(po.getProcessName());
+        newDef.setCategory(po.getCategory());
+        newDef.setFormSchema(po.getFormSchema());
+        newDef.setNodeFieldName(po.getNodeFieldName() != null ? po.getNodeFieldName() : existing.getNodeFieldName());
+        newDef.setOwnerFieldName(po.getOwnerFieldName() != null ? po.getOwnerFieldName() : existing.getOwnerFieldName());
+        newDef.setBizFieldName(po.getBizFieldName() != null ? po.getBizFieldName() : existing.getBizFieldName());
+        newDef.setModelJson(po.getModelJson());
+        newDef.setDescription(po.getDescription());
+        Integer maxVersion = this.processDefinitionMapper.selectMaxVersion(po.getProcessCode());
+        newDef.setVersion((maxVersion != null ? maxVersion : existing.getVersion()) + 1);
+        newDef.setStatus(DefinitionStatus.DESIGNING.getCode());
+        newDef.setCreateBy(existing.getCreateBy());
+        this.processDefinitionMapper.insert(newDef);
+        return newDef.getId();
     }
 
     @Override

+ 77 - 7
src/main/java/com/qqflow/engine/domain/flow/service/impl/ProcessInstanceServiceImpl.java

@@ -93,10 +93,29 @@ public class ProcessInstanceServiceImpl implements ProcessInstanceService {
     }
 
     @Override
-    public PageResult<ProcessInstanceDTO> list(Long applicantId, Integer status, Integer pageNum, Integer pageSize) {
+    @Transactional
+    public Long startProcessAtNode(StartProcessDTO dto, String nodeName) {
+        ProcessDefinition definition = this.getEnabledDefinition(dto.getProcessDefinitionId());
+        ProcessInstance instance = this.buildInstance(dto, definition);
+        instance.setAttachmentUrls(dto.getAttachmentUrls());
+        this.processInstanceMapper.insert(instance);
+        this.flowEngineService.startInstanceAtNode(instance, definition, nodeName);
+        LoginUser loginUser = SecurityUtils.getLoginUser();
+        this.saveAttachments(instance.getId(), null, null, nodeName, nodeName,
+                dto.getAttachmentUrls(), loginUser);
+        return instance.getId();
+    }
+
+    @Override
+    public PageResult<ProcessInstanceDTO> list(Long applicantId, Integer status, String definitionName, String currentNodeName, String applicantName, Integer pageNum, Integer pageSize) {
         Page<ProcessInstance> page = new Page<>(pageNum, pageSize);
-        this.processInstanceMapper.selectInstanceList(page, applicantId, status);
+        this.processInstanceMapper.selectInstanceList(page, applicantId, status, definitionName, currentNodeName, applicantName);
         List<ProcessInstanceDTO> records = this.enrichInstanceDtos(page.getRecords());
+        if (currentNodeName != null && !currentNodeName.isBlank()) {
+            records = records.stream()
+                    .filter(r -> currentNodeName.equals(r.getCurrentNodeName()))
+                    .collect(Collectors.toList());
+        }
         return PageResult.of(page.getTotal(), records);
     }
 
@@ -106,7 +125,9 @@ public class ProcessInstanceServiceImpl implements ProcessInstanceService {
         if (po == null) {
             throw new BusinessException("流程实例不存在");
         }
-        return ProcessInstanceDTO.of(po);
+        ProcessInstanceDTO dto = ProcessInstanceDTO.of(po);
+        this.fillBizValue(dto, po.getProcessDefinitionId());
+        return dto;
     }
 
     @Override
@@ -172,7 +193,9 @@ public class ProcessInstanceServiceImpl implements ProcessInstanceService {
         int remainingCount = 0;
 
         for (FlowNode node : model.getNodes()) {
-            if (NodeType.START.getCode().equals(node.getType()) || NodeType.END.getCode().equals(node.getType())) {
+            String type = node.getType();
+            if (NodeType.START.getCode().equals(type) || NodeType.END.getCode().equals(type)
+                    || NodeType.CONDITION.getCode().equals(type) || NodeType.CC.getCode().equals(type)) {
                 continue;
             }
             NodeProgressDTO dto = new NodeProgressDTO();
@@ -225,7 +248,10 @@ public class ProcessInstanceServiceImpl implements ProcessInstanceService {
         progress.setInstance(ProcessInstanceDTO.of(instance));
         progress.setDefinition(com.qqflow.engine.domain.flow.dto.ProcessDefinitionDTO.of(definition));
         progress.setNodes(nodeProgressList);
-        progress.setRecords(records.stream().map(ApprovalRecordDTO::of).collect(Collectors.toList()));
+        // 过滤掉条件节点和抄送节点的审批记录
+        progress.setRecords(records.stream()
+                .filter(r -> nodeProgressList.stream().anyMatch(n -> n.getNodeId().equals(r.getNodeId())))
+                .map(ApprovalRecordDTO::of).collect(Collectors.toList()));
         progress.setRemainingNodeCount(remainingCount);
         return progress;
     }
@@ -252,6 +278,11 @@ public class ProcessInstanceServiceImpl implements ProcessInstanceService {
                             dto.setCurrentNodeName(nodeMap.get(po.getCurrentNodeId()));
                         }
                     }
+                    this.fillBizValue(dto, po.getProcessDefinitionId());
+                    if (po.getApplicantId() != null) {
+                        SysUser applicant = this.sysUserMapper.selectById(po.getApplicantId());
+                        if (applicant != null) dto.setApplicantName(applicant.getRealName());
+                    }
                     return dto;
                 })
                 .collect(Collectors.toList());
@@ -292,11 +323,12 @@ public class ProcessInstanceServiceImpl implements ProcessInstanceService {
     }
 
     private ProcessInstance buildInstance(StartProcessDTO dto, ProcessDefinition definition) {
-        Long userId = SecurityUtils.getUserId();
+        Long userId = dto.getApplicantId() != null ? dto.getApplicantId() : SecurityUtils.getUserId();
         SysUser user = this.sysUserMapper.selectById(userId);
         Long deptId = user != null ? user.getDeptId() : null;
+        String instanceNo = dto.getInstanceNo() != null ? dto.getInstanceNo() : this.generateInstanceNo();
         return this.processInstanceAssembler.buildNew(
-                this.generateInstanceNo(),
+                instanceNo,
                 definition.getId(),
                 definition.getVersion(),
                 dto.getTitle(),
@@ -449,6 +481,44 @@ public class ProcessInstanceServiceImpl implements ProcessInstanceService {
         return List.of(trimmed);
     }
 
+    @Override
+    @Transactional
+    public void updateFormData(Long instanceId, String formData, String nodeName) {
+        ProcessInstance instance = this.processInstanceMapper.selectById(instanceId);
+        if (instance == null) throw new BusinessException("流程实例不存在");
+        // 更新表单数据
+        this.processInstanceMapper.update(null, Wrappers.<ProcessInstance>lambdaUpdate()
+                .eq(ProcessInstance::getId, instanceId)
+                .set(ProcessInstance::getFormData, formData));
+        // 如果指定了节点且与当前节点不同,取消当前任务,创建新节点的任务
+        if (nodeName != null && !nodeName.isBlank()) {
+            ProcessDefinition definition = this.processDefinitionMapper.selectById(instance.getProcessDefinitionId());
+            if (definition != null) {
+                FlowModel model = this.flowEngineService.parseModel(definition.getModelJson());
+                FlowNode targetNode = model.getNodes().stream()
+                        .filter(n -> nodeName.equals(n.getName())
+                                && !NodeType.START.getCode().equals(n.getType())
+                                && !NodeType.END.getCode().equals(n.getType()))
+                        .findFirst().orElse(null);
+                if (targetNode != null) {
+                    this.approvalTaskHelper.cancelPendingTasks(instanceId, LocalDateTime.now());
+                    this.flowEngineService.startInstanceAtNode(instance, definition, nodeName);
+                }
+            }
+        }
+    }
+
+    private void fillBizValue(ProcessInstanceDTO dto, Long definitionId) {
+        if (definitionId == null || dto.getFormData() == null) return;
+        try {
+            ProcessDefinition def = this.processDefinitionMapper.selectById(definitionId);
+            if (def == null || def.getBizFieldName() == null) return;
+            Map<String, Object> formData = objectMapper.readValue(dto.getFormData(), Map.class);
+            Object val = formData.get(def.getBizFieldName());
+            if (val != null) dto.setBizValue(val.toString());
+        } catch (Exception ignored) { /* ignore */ }
+    }
+
     private boolean hasRole(Long userId, Long roleId) {
         return this.sysUserRoleMapper.exists(
                 new com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper<SysUserRole>()

+ 7 - 0
src/main/java/com/qqflow/engine/domain/system/mapper/SysUserMapper.java

@@ -17,5 +17,12 @@ public interface SysUserMapper extends BaseMapper<SysUser> {
         );
     }
 
+    default SysUser selectByRealName(String realName) {
+        return this.selectOne(
+                new com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper<SysUser>()
+                        .eq(SysUser::getRealName, realName)
+        );
+    }
+
     List<String> selectRoleCodesByUserId(@Param("userId") Long userId);
 }

+ 0 - 8
src/main/resources/application.yml

@@ -6,14 +6,6 @@ spring:
     active: test
   application:
     name: qqflowengine-backend
-  datasource:
-    url: jdbc:mysql://localhost:3306/qqflowengine?useUnicode=true&characterEncoding=UTF-8&connectionCollation=utf8mb4_unicode_ci&serverTimezone=Asia/Shanghai&createDatabaseIfNotExist=true
-    driver-class-name: com.mysql.cj.jdbc.Driver
-    username: root
-    password: root
-  sql:
-    init:
-      mode: never
 #      schema-locations: classpath:schema-mysql.sql
 #      data-locations: classpath:data-mysql.sql
   redis:

+ 8 - 1
src/main/resources/mapper/flow/ProcessInstanceMapper.xml

@@ -23,9 +23,10 @@
     </resultMap>
 
     <select id="selectInstanceList" resultMap="BaseResultMap">
-        SELECT pi.*, pd.process_name as process_name
+        SELECT pi.*, pd.process_name as process_name, su.real_name as applicant_name
         FROM bpm_process_instance pi
         LEFT JOIN bpm_process_definition pd ON pi.process_definition_id = pd.id
+        LEFT JOIN sys_user su ON pi.applicant_id = su.id
         WHERE pi.deleted = 0
         <if test="applicantId != null">
             AND pi.applicant_id = #{applicantId}
@@ -33,6 +34,12 @@
         <if test="status != null">
             AND pi.status = #{status}
         </if>
+        <if test="definitionName != null and definitionName != ''">
+            AND pd.process_name = #{definitionName}
+        </if>
+        <if test="applicantName != null and applicantName != ''">
+            AND (su.username LIKE CONCAT('%', #{applicantName}, '%') OR su.real_name LIKE CONCAT('%', #{applicantName}, '%'))
+        </if>
         ORDER BY pi.create_time DESC
     </select>