添加通知管理功能

This commit is contained in:
dengqichen 2025-10-23 13:59:03 +08:00
parent 175e33ce4f
commit 813a4e4350
15 changed files with 1078 additions and 105 deletions

View File

@ -0,0 +1,70 @@
package com.qqchen.deploy.backend.workflow.controller;
import com.qqchen.deploy.backend.framework.api.Response;
import com.qqchen.deploy.backend.workflow.dto.request.ApprovalTaskRequest;
import com.qqchen.deploy.backend.workflow.dto.response.ApprovalTaskDTO;
import com.qqchen.deploy.backend.workflow.service.IApprovalTaskService;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.Parameter;
import io.swagger.v3.oas.annotations.tags.Tag;
import jakarta.annotation.Resource;
import jakarta.validation.Valid;
import lombok.extern.slf4j.Slf4j;
import org.springframework.web.bind.annotation.*;
import java.util.List;
/**
* 审批任务管理接口
*
* @author qqchen
* @date 2025-10-23
*/
@Slf4j
@RestController
@RequestMapping("/api/v1/approval-tasks")
@Tag(name = "审批任务管理", description = "审批任务相关接口")
public class ApprovalTaskApiController {
@Resource
private IApprovalTaskService approvalTaskService;
@Operation(summary = "查询我的待办审批任务")
@GetMapping("/my-tasks")
public Response<List<ApprovalTaskDTO>> getMyTasks(
@Parameter(description = "用户名", required = true) @RequestParam String username
) {
List<ApprovalTaskDTO> tasks = approvalTaskService.getMyTasks(username);
return Response.success(tasks);
}
@Operation(summary = "查询流程实例的审批任务")
@GetMapping("/process/{processInstanceId}")
public Response<List<ApprovalTaskDTO>> getTasksByProcessInstance(
@Parameter(description = "流程实例ID", required = true)
@PathVariable String processInstanceId
) {
List<ApprovalTaskDTO> tasks = approvalTaskService.getTasksByProcessInstance(processInstanceId);
return Response.success(tasks);
}
@Operation(summary = "查询审批任务详情")
@GetMapping("/{taskId}")
public Response<ApprovalTaskDTO> getTaskById(
@Parameter(description = "任务ID", required = true)
@PathVariable String taskId
) {
ApprovalTaskDTO task = approvalTaskService.getTaskById(taskId);
return Response.success(task);
}
@Operation(summary = "完成审批任务(通过/拒绝)")
@PostMapping("/complete")
public Response<Void> completeTask(
@Valid @RequestBody ApprovalTaskRequest request
) {
approvalTaskService.completeTask(request);
return Response.success();
}
}

View File

@ -1,32 +1,227 @@
package com.qqchen.deploy.backend.workflow.delegate;
import com.qqchen.deploy.backend.workflow.constants.WorkFlowConstants;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.qqchen.deploy.backend.workflow.dto.inputmapping.ApprovalInputMapping;
import com.qqchen.deploy.backend.workflow.dto.outputs.ApprovalOutputs;
import jakarta.annotation.Resource;
import com.qqchen.deploy.backend.workflow.enums.ApprovalResultEnum;
import com.qqchen.deploy.backend.workflow.enums.ApproverTypeEnum;
import com.qqchen.deploy.backend.workflow.enums.NodeExecutionStatusEnum;
import lombok.extern.slf4j.Slf4j;
import org.flowable.engine.TaskService;
import org.flowable.engine.delegate.BpmnError;
import org.flowable.common.engine.api.delegate.Expression;
import org.flowable.engine.delegate.DelegateExecution;
import org.flowable.task.api.Task;
import org.flowable.engine.delegate.TaskListener;
import org.flowable.task.service.delegate.DelegateTask;
import org.springframework.stereotype.Component;
import java.time.LocalDateTime;
import java.util.Date;
import java.util.Map;
/**
* 审批节点委派
* 负责创建Flowable用户任务并配置审批相关参数
*
* 实现 TaskListener 接口在任务创建时create event被调用
*
* @author qqchen
* @since 2025-10-22
* @since 2025-10-23
*/
@Slf4j
@Component("approvalDelegate")
public class ApprovalNodeDelegate extends BaseNodeDelegate<ApprovalInputMapping, ApprovalOutputs> {
public class ApprovalNodeDelegate extends BaseNodeDelegate<ApprovalInputMapping, ApprovalOutputs> implements TaskListener {
@Resource
private TaskService taskService;
/**
* TaskListener 接口方法
* 在任务创建时被 Flowable 调用
*
* 重要必须在这里直接设置 Task assignee/candidateGroups
* 而不是设置流程变量因为任务已经创建了
*
* @param delegateTask Flowable 任务代理对象
*/
@Override
public void notify(DelegateTask delegateTask) {
log.info("TaskListener: Approval task created - taskId: {}", delegateTask.getId());
try {
// 1. 解析 field 字段中的 configs inputMapping
// DelegateTask 继承了 VariableScope可以直接传递给 Expression.getValue()
Map<String, Object> configs = parseJsonFieldFromTask(this.configs, delegateTask);
ApprovalInputMapping input = parseInputMappingFromTask(delegateTask);
// 2. 直接设置任务的审批人而不是设置流程变量
configureTaskAssignee(delegateTask, input);
// 3. 设置任务的基本信息
if (configs != null && configs.get("nodeName") != null) {
delegateTask.setName((String) configs.get("nodeName"));
}
if (input.getApprovalTitle() != null) {
delegateTask.setDescription(input.getApprovalTitle());
}
// 4. 设置超时时间
if (input.getTimeoutDuration() != null && input.getTimeoutDuration() > 0) {
java.util.Date dueDate = java.sql.Timestamp.valueOf(
LocalDateTime.now().plusHours(input.getTimeoutDuration())
);
delegateTask.setDueDate(dueDate);
}
// 5. 将审批相关信息保存为任务的局部变量方便前端查询时使用
saveApprovalInfoToTaskVariables(delegateTask, input);
log.info("Approval task configuration completed - assignee: {}", delegateTask.getAssignee());
} catch (Exception e) {
log.error("Failed to configure approval task", e);
throw new RuntimeException("Failed to configure approval task: " + e.getMessage(), e);
}
}
/**
* 将审批相关信息保存为任务变量
* 这样前端查询任务时可以获取到这些信息
*/
private void saveApprovalInfoToTaskVariables(DelegateTask delegateTask, ApprovalInputMapping input) {
// 保存审批标题和内容
if (input.getApprovalTitle() != null) {
delegateTask.setVariableLocal("approvalTitle", input.getApprovalTitle());
}
if (input.getApprovalContent() != null) {
delegateTask.setVariableLocal("approvalContent", input.getApprovalContent());
}
// 保存审批模式
if (input.getApprovalMode() != null) {
delegateTask.setVariableLocal("approvalMode", input.getApprovalMode().getCode());
}
// 保存审批配置
if (input.getAllowDelegate() != null) {
delegateTask.setVariableLocal("allowDelegate", input.getAllowDelegate());
}
if (input.getAllowAddSign() != null) {
delegateTask.setVariableLocal("allowAddSign", input.getAllowAddSign());
}
if (input.getRequireComment() != null) {
delegateTask.setVariableLocal("requireComment", input.getRequireComment());
}
// 保存候选组如果有
if (input.getApproverRoles() != null && !input.getApproverRoles().isEmpty()) {
delegateTask.setVariableLocal("candidateGroups", String.join(",", input.getApproverRoles()));
} else if (input.getApproverDepartments() != null && !input.getApproverDepartments().isEmpty()) {
delegateTask.setVariableLocal("candidateGroups", String.join(",", input.getApproverDepartments()));
}
log.debug("Saved approval info to task variables for taskId: {}", delegateTask.getId());
}
/**
* TaskListener 中解析 JSON field
* DelegateTask 继承了 VariableScope可以直接传递给 Expression.getValue()
*/
private Map<String, Object> parseJsonFieldFromTask(Expression expression, DelegateTask task) {
if (expression == null) {
return new java.util.HashMap<>();
}
String jsonStr = expression.getValue(task) != null ? expression.getValue(task).toString() : null;
if (jsonStr == null || jsonStr.isEmpty()) {
return new java.util.HashMap<>();
}
try {
ObjectMapper objectMapper = new ObjectMapper();
return objectMapper.readValue(jsonStr, new com.fasterxml.jackson.core.type.TypeReference<Map<String, Object>>() {});
} catch (Exception e) {
log.error("Failed to parse JSON field: {}", jsonStr, e);
return new java.util.HashMap<>();
}
}
/**
* TaskListener 中解析 InputMapping
*/
private ApprovalInputMapping parseInputMappingFromTask(DelegateTask task) {
try {
String inputMappingJson = this.inputMapping != null && this.inputMapping.getValue(task) != null
? this.inputMapping.getValue(task).toString()
: null;
if (inputMappingJson == null || inputMappingJson.isEmpty()) {
return new ApprovalInputMapping();
}
ObjectMapper objectMapper = new ObjectMapper();
return objectMapper.readValue(inputMappingJson, ApprovalInputMapping.class);
} catch (Exception e) {
log.error("Failed to parse input mapping", e);
return new ApprovalInputMapping();
}
}
/**
* 配置任务的审批人
* 根据 approverType 不同设置不同的审批人
*/
private void configureTaskAssignee(DelegateTask task, ApprovalInputMapping input) {
if (input.getApproverType() == null) {
log.warn("ApproverType is null, skip setting assignee");
return;
}
switch (input.getApproverType()) {
case USER:
// 指定用户审批
if (input.getApprovers() != null && !input.getApprovers().isEmpty()) {
// 单人审批设置 assignee
if ("SINGLE".equals(input.getApprovalMode() != null ? input.getApprovalMode().getCode() : "")) {
task.setAssignee(input.getApprovers().get(0));
log.info("Set single assignee: {}", input.getApprovers().get(0));
} else {
// 会签/或签设置候选人
for (String approver : input.getApprovers()) {
task.addCandidateUser(approver);
}
log.info("Set candidate users: {}", input.getApprovers());
}
}
break;
case ROLE:
// 指定角色审批设置候选组
if (input.getApproverRoles() != null && !input.getApproverRoles().isEmpty()) {
for (String role : input.getApproverRoles()) {
task.addCandidateGroup(role);
}
log.info("Set candidate groups (roles): {}", input.getApproverRoles());
}
break;
case DEPARTMENT:
// 指定部门审批设置候选组
if (input.getApproverDepartments() != null && !input.getApproverDepartments().isEmpty()) {
for (String dept : input.getApproverDepartments()) {
task.addCandidateGroup(dept);
}
log.info("Set candidate groups (departments): {}", input.getApproverDepartments());
}
break;
case VARIABLE:
// 从流程变量中获取审批人
if (input.getApproverVariable() != null) {
Object approverValue = task.getVariable(input.getApproverVariable());
if (approverValue != null) {
task.setAssignee(approverValue.toString());
log.info("Set assignee from variable {}: {}", input.getApproverVariable(), approverValue);
}
}
break;
}
}
@Override
protected ApprovalOutputs executeInternal(
@ -34,58 +229,119 @@ public class ApprovalNodeDelegate extends BaseNodeDelegate<ApprovalInputMapping,
Map<String, Object> configs,
ApprovalInputMapping input
) {
log.info("Creating approval task - assignee: {}, candidateGroup: {}",
input.getAssignee(), input.getCandidateGroup());
log.info("Creating approval task - mode: {}, approverType: {}",
input.getApprovalMode(), input.getApproverType());
try {
// 创建用户任务
Task task = taskService.createTaskQuery()
.processInstanceId(execution.getProcessInstanceId())
.taskDefinitionKey(execution.getCurrentActivityId())
.singleResult();
// 设置审批任务的相关变量到流程中供Flowable UserTask使用
setApprovalVariables(execution, configs, input);
if (task == null) {
throw new BpmnError(WorkFlowConstants.WORKFLOW_EXEC_ERROR, "Failed to create approval task");
}
// 从configs中获取任务名称和描述
String taskName = (String) configs.get("nodeName");
String taskDesc = (String) configs.get("description");
task.setName(taskName != null ? taskName : "审批任务");
task.setDescription(taskDesc);
// 设置审批人
if (input.getAssignee() != null) {
task.setAssignee(input.getAssignee());
}
// 设置候选组
if (input.getCandidateGroup() != null) {
taskService.addCandidateGroup(task.getId(), input.getCandidateGroup());
}
// 设置超时时间
if (input.getTimeoutHours() != null) {
Date dueDate = java.sql.Timestamp.valueOf(
LocalDateTime.now().plusHours(input.getTimeoutHours()));
task.setDueDate(dueDate);
}
// 更新任务
taskService.saveTask(task);
log.info("Created approval task: {}", task.getId());
// 返回输出审批任务是异步的这里先返回pending状态
// 审批任务是异步的这里先返回PENDING状态
ApprovalOutputs outputs = new ApprovalOutputs();
outputs.setResult("PENDING");
outputs.setApprovalResult(ApprovalResultEnum.PENDING);
outputs.setStatus(NodeExecutionStatusEnum.SUCCESS); // 节点创建成功
log.info("Created approval task configuration successfully");
return outputs;
} catch (Exception e) {
log.error("Failed to create approval task", e);
throw new BpmnError(WorkFlowConstants.WORKFLOW_EXEC_ERROR,
"Failed to create approval task: " + e.getMessage());
log.error("Failed to create approval task configuration", e);
ApprovalOutputs outputs = new ApprovalOutputs();
outputs.setApprovalResult(ApprovalResultEnum.REJECTED);
outputs.setStatus(NodeExecutionStatusEnum.FAILURE);
return outputs;
}
}
}
/**
* 设置审批任务相关变量到流程执行上下文
*/
private void setApprovalVariables(DelegateExecution execution,
Map<String, Object> configs,
ApprovalInputMapping input) {
// 审批模式
if (input.getApprovalMode() != null) {
execution.setVariable("approvalMode", input.getApprovalMode().getCode());
}
// 审批人类型
if (input.getApproverType() != null) {
execution.setVariable("approverType", input.getApproverType().getCode());
}
// 根据审批人类型设置对应的审批人
if (input.getApproverType() != null) {
switch (input.getApproverType()) {
case USER:
if (input.getApprovers() != null && !input.getApprovers().isEmpty()) {
// 设置审批人列表用户名
execution.setVariable("approvers",
String.join(",", input.getApprovers()));
// 设置第一个审批人为默认assignee单人审批时
execution.setVariable("assignee", input.getApprovers().get(0));
}
break;
case ROLE:
if (input.getApproverRoles() != null && !input.getApproverRoles().isEmpty()) {
// 设置审批角色列表
execution.setVariable("approverRoles",
String.join(",", input.getApproverRoles()));
// 设置候选组角色
execution.setVariable("candidateGroups",
String.join(",", input.getApproverRoles()));
}
break;
case DEPARTMENT:
if (input.getApproverDepartments() != null && !input.getApproverDepartments().isEmpty()) {
// 设置审批部门列表
execution.setVariable("approverDepartments",
String.join(",", input.getApproverDepartments()));
// 设置候选组部门
execution.setVariable("candidateGroups",
String.join(",", input.getApproverDepartments()));
}
break;
case VARIABLE:
if (input.getApproverVariable() != null) {
execution.setVariable("approverVariable", input.getApproverVariable());
}
break;
}
}
// 审批标题和内容
if (input.getApprovalTitle() != null) {
execution.setVariable("approvalTitle", input.getApprovalTitle());
}
if (input.getApprovalContent() != null) {
execution.setVariable("approvalContent", input.getApprovalContent());
}
// 超时配置
if (input.getTimeoutDuration() != null) {
execution.setVariable("timeoutDuration", input.getTimeoutDuration());
}
if (input.getTimeoutAction() != null) {
execution.setVariable("timeoutAction", input.getTimeoutAction().getCode());
}
// 审批配置
if (input.getAllowDelegate() != null) {
execution.setVariable("allowDelegate", input.getAllowDelegate());
}
if (input.getAllowAddSign() != null) {
execution.setVariable("allowAddSign", input.getAllowAddSign());
}
if (input.getRequireComment() != null) {
execution.setVariable("requireComment", input.getRequireComment());
}
// 从configs设置任务名称和描述
if (configs.get("nodeName") != null) {
execution.setVariable("taskName", configs.get("nodeName"));
}
if (configs.get("description") != null) {
execution.setVariable("taskDescription", configs.get("description"));
}
}
}

View File

@ -72,7 +72,7 @@ public abstract class BaseNodeDelegate<I, O> implements JavaDelegate {
}
// 7. 设置节点执行状态为成功
setExecutionStatus(execution, WORKFLOW_NODE_EXECUTION_STATE_SUCCESS);
setExecutionStatus(execution, WORKFLOW_NODE_EXECUTION_STATE_SUCCESS);
} catch (Exception e) {
setExecutionStatus(execution, WORKFLOW_NODE_EXECUTION_STATE_FAILURE);

View File

@ -1,29 +1,87 @@
package com.qqchen.deploy.backend.workflow.dto.inputmapping;
import com.qqchen.deploy.backend.workflow.enums.ApprovalModeEnum;
import com.qqchen.deploy.backend.workflow.enums.ApproverTypeEnum;
import com.qqchen.deploy.backend.workflow.enums.TimeoutActionEnum;
import lombok.Data;
import java.util.List;
/**
* 审批节点输入映射
*
*
* @author qqchen
* @since 2025-10-22
* @date 2025-10-23
*/
@Data
public class ApprovalInputMapping {
/**
* 审批人
*/
private String assignee;
/**
* 候选组
*/
private String candidateGroup;
/**
* 超时时间小时
*/
private Integer timeoutHours;
}
/**
* 审批模式
*/
private ApprovalModeEnum approvalMode;
/**
* 审批人类型
*/
private ApproverTypeEnum approverType;
/**
* 审批人列表 approverType = USER 时使用
* 使用用户名 ["admin", "zhangsan"]
*/
private List<String> approvers;
/**
* 审批角色列表 approverType = ROLE 时使用
* 使用角色编码 ["ROLE_ADMIN", "ROLE_MANAGER"]
*/
private List<String> approverRoles;
/**
* 审批部门列表 approverType = DEPARTMENT 时使用
* 使用部门编码 ["DEPT_RD", "DEPT_OPS"]
*/
private List<String> approverDepartments;
/**
* 审批人变量 approverType = VARIABLE 时使用
* 格式${变量名}
*/
private String approverVariable;
/**
* 审批标题
*/
private String approvalTitle;
/**
* 审批内容
*/
private String approvalContent;
/**
* 超时时间小时0表示不限制
*/
private Integer timeoutDuration;
/**
* 超时处理方式
*/
private TimeoutActionEnum timeoutAction;
/**
* 是否允许转交
*/
private Boolean allowDelegate;
/**
* 是否允许加签
*/
private Boolean allowAddSign;
/**
* 是否必须填写意见
*/
private Boolean requireComment;
}

View File

@ -1,31 +1,66 @@
package com.qqchen.deploy.backend.workflow.dto.outputs;
import com.qqchen.deploy.backend.workflow.enums.ApprovalResultEnum;
import lombok.Data;
import lombok.EqualsAndHashCode;
import java.time.LocalDateTime;
import java.util.List;
/**
* 审批节点输出
*
*
* @author qqchen
* @since 2025-10-22
* @date 2025-10-23
*/
@Data
@EqualsAndHashCode(callSuper = true)
public class ApprovalOutputs extends BaseNodeOutputs {
/**
* 审批结果
*/
private String result;
private ApprovalResultEnum approvalResult;
/**
* 审批人姓名
*/
private String approver;
/**
* 审批人ID
*/
private Long approverUserId;
/**
* 审批时间
*/
private LocalDateTime approvalTime;
/**
* 审批意见
*/
private String comment;
/**
* 审批人
*/
private String approver;
}
private String approvalComment;
/**
* 审批用时
*/
private Long approvalDuration;
/**
* 所有审批人列表会签/或签时
*/
private List<ApproverInfo> allApprovers;
/**
* 审批人信息
*/
@Data
public static class ApproverInfo {
private Long userId;
private String userName;
private String result;
private String comment;
private LocalDateTime time;
}
}

View File

@ -0,0 +1,29 @@
package com.qqchen.deploy.backend.workflow.dto.request;
import io.swagger.v3.oas.annotations.media.Schema;
import jakarta.validation.constraints.NotBlank;
import jakarta.validation.constraints.NotNull;
import lombok.Data;
/**
* 审批任务请求
*
* @author qqchen
* @date 2025-10-23
*/
@Data
@Schema(description = "审批任务请求")
public class ApprovalTaskRequest {
@NotBlank(message = "任务ID不能为空")
@Schema(description = "Flowable任务ID", required = true)
private String taskId;
@NotNull(message = "审批结果不能为空")
@Schema(description = "审批结果: true=通过, false=拒绝", required = true)
private Boolean approved;
@Schema(description = "审批意见")
private String comment;
}

View File

@ -0,0 +1,63 @@
package com.qqchen.deploy.backend.workflow.dto.response;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
import java.time.LocalDateTime;
/**
* 审批任务响应DTO
*
* @author qqchen
* @date 2025-10-23
*/
@Data
@Schema(description = "审批任务信息")
public class ApprovalTaskDTO {
@Schema(description = "任务ID")
private String taskId;
@Schema(description = "任务名称")
private String taskName;
@Schema(description = "任务描述")
private String taskDescription;
@Schema(description = "流程实例ID")
private String processInstanceId;
@Schema(description = "流程定义ID")
private String processDefinitionId;
@Schema(description = "审批人")
private String assignee;
@Schema(description = "候选组")
private String candidateGroups;
@Schema(description = "创建时间")
private LocalDateTime createTime;
@Schema(description = "到期时间")
private LocalDateTime dueDate;
@Schema(description = "审批标题")
private String approvalTitle;
@Schema(description = "审批内容")
private String approvalContent;
@Schema(description = "审批模式")
private String approvalMode;
@Schema(description = "是否允许转交")
private Boolean allowDelegate;
@Schema(description = "是否允许加签")
private Boolean allowAddSign;
@Schema(description = "是否必须填写意见")
private Boolean requireComment;
}

View File

@ -0,0 +1,36 @@
package com.qqchen.deploy.backend.workflow.enums;
import com.fasterxml.jackson.annotation.JsonValue;
import lombok.AllArgsConstructor;
import lombok.Getter;
/**
* 审批模式枚举
*
* @author qqchen
* @date 2025-10-23
*/
@Getter
@AllArgsConstructor
public enum ApprovalModeEnum {
/**
* 单人审批
*/
SINGLE("SINGLE", "单人审批"),
/**
* 会签所有人同意
*/
ALL("ALL", "会签"),
/**
* 或签任一人同意
*/
ANY("ANY", "或签");
@JsonValue
private final String code;
private final String description;
}

View File

@ -0,0 +1,41 @@
package com.qqchen.deploy.backend.workflow.enums;
import com.fasterxml.jackson.annotation.JsonValue;
import lombok.AllArgsConstructor;
import lombok.Getter;
/**
* 审批结果枚举
*
* @author qqchen
* @date 2025-10-23
*/
@Getter
@AllArgsConstructor
public enum ApprovalResultEnum {
/**
* 待审批
*/
PENDING("PENDING", "待审批"),
/**
* 通过
*/
APPROVED("APPROVED", "通过"),
/**
* 拒绝
*/
REJECTED("REJECTED", "拒绝"),
/**
* 超时
*/
TIMEOUT("TIMEOUT", "超时");
@JsonValue
private final String code;
private final String description;
}

View File

@ -0,0 +1,41 @@
package com.qqchen.deploy.backend.workflow.enums;
import com.fasterxml.jackson.annotation.JsonValue;
import lombok.AllArgsConstructor;
import lombok.Getter;
/**
* 审批人类型枚举
*
* @author qqchen
* @date 2025-10-23
*/
@Getter
@AllArgsConstructor
public enum ApproverTypeEnum {
/**
* 指定用户
*/
USER("USER", "指定用户"),
/**
* 指定角色
*/
ROLE("ROLE", "指定角色"),
/**
* 指定部门
*/
DEPARTMENT("DEPARTMENT", "指定部门"),
/**
* 流程变量
*/
VARIABLE("VARIABLE", "流程变量");
@JsonValue
private final String code;
private final String description;
}

View File

@ -85,12 +85,12 @@ public enum NodeTypeEnums {
NodeCategoryEnums.TASK,
"通知节点"
),
APPROVAL_NODE(
"APPROVAL_NODE",
APPROVAL(
"APPROVAL",
"审批节点",
BpmnNodeTypeEnums.USER_TASK,
NodeCategoryEnums.TASK,
"审批任务"
"人工审批节点,支持多种审批模式"
);
//
// /**

View File

@ -0,0 +1,41 @@
package com.qqchen.deploy.backend.workflow.enums;
import com.fasterxml.jackson.annotation.JsonValue;
import lombok.AllArgsConstructor;
import lombok.Getter;
/**
* 超时处理方式枚举
*
* @author qqchen
* @date 2025-10-23
*/
@Getter
@AllArgsConstructor
public enum TimeoutActionEnum {
/**
* 无操作
*/
NONE("NONE", "无操作"),
/**
* 自动通过
*/
AUTO_APPROVE("AUTO_APPROVE", "自动通过"),
/**
* 自动拒绝
*/
AUTO_REJECT("AUTO_REJECT", "自动拒绝"),
/**
* 仅通知
*/
NOTIFY("NOTIFY", "仅通知");
@JsonValue
private final String code;
private final String description;
}

View File

@ -0,0 +1,47 @@
package com.qqchen.deploy.backend.workflow.service;
import com.qqchen.deploy.backend.workflow.dto.request.ApprovalTaskRequest;
import com.qqchen.deploy.backend.workflow.dto.response.ApprovalTaskDTO;
import java.util.List;
/**
* 审批任务服务接口
*
* @author qqchen
* @date 2025-10-23
*/
public interface IApprovalTaskService {
/**
* 查询当前用户的待办审批任务
*
* @param username 用户名
* @return 待办任务列表
*/
List<ApprovalTaskDTO> getMyTasks(String username);
/**
* 查询流程实例的所有审批任务
*
* @param processInstanceId 流程实例ID
* @return 任务列表
*/
List<ApprovalTaskDTO> getTasksByProcessInstance(String processInstanceId);
/**
* 根据任务ID查询审批任务详情
*
* @param taskId 任务ID
* @return 任务详情
*/
ApprovalTaskDTO getTaskById(String taskId);
/**
* 处理审批任务通过/拒绝
*
* @param request 审批请求
*/
void completeTask(ApprovalTaskRequest request);
}

View File

@ -0,0 +1,161 @@
package com.qqchen.deploy.backend.workflow.service.impl;
import com.qqchen.deploy.backend.framework.exception.BusinessException;
import com.qqchen.deploy.backend.framework.enums.ResponseCode;
import com.qqchen.deploy.backend.workflow.dto.request.ApprovalTaskRequest;
import com.qqchen.deploy.backend.workflow.dto.response.ApprovalTaskDTO;
import com.qqchen.deploy.backend.workflow.service.IApprovalTaskService;
import jakarta.annotation.Resource;
import lombok.extern.slf4j.Slf4j;
import org.flowable.engine.TaskService;
import org.flowable.task.api.Task;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import java.time.LocalDateTime;
import java.time.ZoneId;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.stream.Collectors;
/**
* 审批任务服务实现
*
* @author qqchen
* @date 2025-10-23
*/
@Slf4j
@Service
public class ApprovalTaskServiceImpl implements IApprovalTaskService {
@Resource
private TaskService taskService;
@Override
public List<ApprovalTaskDTO> getMyTasks(String username) {
log.info("查询用户 {} 的待办任务", username);
List<Task> tasks = taskService.createTaskQuery()
.taskAssignee(username)
.orderByTaskCreateTime()
.desc()
.list();
return tasks.stream()
.map(this::convertToDTO)
.collect(Collectors.toList());
}
@Override
public List<ApprovalTaskDTO> getTasksByProcessInstance(String processInstanceId) {
log.info("查询流程实例 {} 的所有任务", processInstanceId);
List<Task> tasks = taskService.createTaskQuery()
.processInstanceId(processInstanceId)
.orderByTaskCreateTime()
.desc()
.list();
return tasks.stream()
.map(this::convertToDTO)
.collect(Collectors.toList());
}
@Override
public ApprovalTaskDTO getTaskById(String taskId) {
log.info("查询任务详情: {}", taskId);
Task task = taskService.createTaskQuery()
.taskId(taskId)
.singleResult();
if (task == null) {
throw new BusinessException(ResponseCode.DATA_NOT_FOUND);
}
return convertToDTO(task);
}
@Override
@Transactional
public void completeTask(ApprovalTaskRequest request) {
log.info("处理审批任务: taskId={}, approved={}", request.getTaskId(), request.getApproved());
// 检查任务是否存在
Task task = taskService.createTaskQuery()
.taskId(request.getTaskId())
.singleResult();
if (task == null) {
throw new BusinessException(ResponseCode.DATA_NOT_FOUND);
}
// 获取节点IDtaskDefinitionKey 就是 BPMN 中的节点 ID
String nodeId = task.getTaskDefinitionKey();
// 构建审批输出对象ApprovalOutputs
Map<String, Object> approvalOutputs = new HashMap<>();
approvalOutputs.put("status", "SUCCESS");
approvalOutputs.put("approvalResult", request.getApproved() ? "APPROVED" : "REJECTED");
approvalOutputs.put("approver", task.getAssignee());
approvalOutputs.put("approvalTime", LocalDateTime.now().toString());
approvalOutputs.put("approvalComment", request.getComment());
// 以节点ID为变量名存储审批结果这样条件表达式才能正确解析
// 例如${sid_7353fb85_562b_441d_add6_23915ab8e843.approvalResult == 'APPROVED'}
Map<String, Object> variables = new HashMap<>();
variables.put(nodeId, approvalOutputs);
// 添加任务评论
if (request.getComment() != null) {
taskService.addComment(request.getTaskId(), task.getProcessInstanceId(), request.getComment());
}
// 完成任务
taskService.complete(request.getTaskId(), variables);
log.info("审批任务完成: taskId={}, nodeId={}, result={}",
request.getTaskId(), nodeId, request.getApproved() ? "APPROVED" : "REJECTED");
}
/**
* 转换 Task DTO
*/
private ApprovalTaskDTO convertToDTO(Task task) {
ApprovalTaskDTO dto = new ApprovalTaskDTO();
dto.setTaskId(task.getId());
dto.setTaskName(task.getName());
dto.setTaskDescription(task.getDescription());
dto.setProcessInstanceId(task.getProcessInstanceId());
dto.setProcessDefinitionId(task.getProcessDefinitionId());
dto.setAssignee(task.getAssignee());
if (task.getCreateTime() != null) {
dto.setCreateTime(LocalDateTime.ofInstant(
task.getCreateTime().toInstant(),
ZoneId.systemDefault()));
}
if (task.getDueDate() != null) {
dto.setDueDate(LocalDateTime.ofInstant(
task.getDueDate().toInstant(),
ZoneId.systemDefault()));
}
// 获取任务变量
Map<String, Object> variables = taskService.getVariables(task.getId());
if (variables != null) {
dto.setApprovalTitle((String) variables.get("approvalTitle"));
dto.setApprovalContent((String) variables.get("approvalContent"));
dto.setApprovalMode((String) variables.get("approvalMode"));
dto.setCandidateGroups((String) variables.get("candidateGroups"));
dto.setAllowDelegate((Boolean) variables.get("allowDelegate"));
dto.setAllowAddSign((Boolean) variables.get("allowAddSign"));
dto.setRequireComment((Boolean) variables.get("requireComment"));
}
return dto;
}
}

View File

@ -128,23 +128,32 @@ public class BpmnConverter {
*/
private void convertSingleNode(WorkflowDefinitionGraphNode node, Process process, Map<String, String> idMapping) {
try {
// 步骤2.3获取节点对应的BPMN元素类型
@SuppressWarnings("unchecked")
Class<? extends FlowElement> instanceClass = (Class<? extends FlowElement>) NodeTypeEnums.valueOf(node.getNodeCode())
.getBpmnType()
.getInstance();
// 步骤2.4创建节点实例并设置基本属性
FlowElement element = instanceClass.getDeclaredConstructor().newInstance();
String validId = sanitizeId(node.getId());
idMapping.put(node.getId(), validId);
// 如果是网关节点需要特殊处理
if (element instanceof Gateway) {
element = createGatewayElement(node, validId);
FlowElement element;
// 步骤2.3检查是否为审批节点特殊处理
if ("APPROVAL".equals(node.getNodeCode()) || "APPROVAL_NODE".equals(node.getNodeCode())) {
// 创建 UserTask 而不是 ServiceTask
element = createUserTask(node, validId);
} else {
element.setId(validId);
element.setName(node.getNodeName());
// 其他节点按原有逻辑处理
@SuppressWarnings("unchecked")
Class<? extends FlowElement> instanceClass = (Class<? extends FlowElement>) NodeTypeEnums.valueOf(node.getNodeCode())
.getBpmnType()
.getInstance();
// 步骤2.4创建节点实例并设置基本属性
element = instanceClass.getDeclaredConstructor().newInstance();
// 如果是网关节点需要特殊处理
if (element instanceof Gateway) {
element = createGatewayElement(node, validId);
} else {
element.setId(validId);
element.setName(node.getNodeName());
}
}
// 步骤2.5配置节点的特定属性传递原始节点ID和sanitized ID
@ -182,7 +191,10 @@ public class BpmnConverter {
Map<String, List<ExtensionElement>> extensionElements = configureExecutionListeners(element);
// 步骤2根据节点类型进行特定配置
if (element instanceof ServiceTask) {
if (element instanceof UserTask) {
// 审批节点UserTask的特殊配置
configureUserTask((UserTask) element, node, extensionElements, validId);
} else if (element instanceof ServiceTask) {
configureServiceTask((ServiceTask) element, node, process, extensionElements, validId);
} else {
element.setExtensionElements(extensionElements);
@ -233,6 +245,88 @@ public class BpmnConverter {
return extensionElements;
}
/**
* 创建审批节点UserTask
*
* @param node 工作流节点定义
* @param validId sanitized 后的节点 ID
* @return UserTask 实例
*/
private UserTask createUserTask(WorkflowDefinitionGraphNode node, String validId) {
UserTask userTask = new UserTask();
userTask.setId(validId);
userTask.setName(node.getNodeName());
// 不在这里设置 assignee而是在 TaskListener 中动态设置
// TaskListener 会解析 inputMapping 并直接调用 task.setAssignee()
return userTask;
}
/**
* 配置审批节点UserTask
*
* @param userTask 审批任务节点
* @param node 工作流节点定义
* @param extensionElements 扩展元素
* @param validId sanitized 后的节点 ID
*/
private void configureUserTask(UserTask userTask, WorkflowDefinitionGraphNode node, Map<String, List<ExtensionElement>> extensionElements, String validId) {
try {
// 1. 创建 TaskListener在任务创建时调用 ApprovalNodeDelegate
ExtensionElement taskListener = new ExtensionElement();
taskListener.setName("taskListener");
taskListener.setNamespace("http://flowable.org/bpmn");
taskListener.setNamespacePrefix("flowable");
taskListener.addAttribute(createAttribute("event", "create"));
taskListener.addAttribute(createAttribute("delegateExpression", "${approvalDelegate}"));
// 2. field 字段作为 TaskListener 的子元素添加而不是 UserTask 的子元素
// 这样 ApprovalNodeDelegate 才能通过 @field 注解注入这些字段
addFieldsToTaskListener(taskListener, node, validId);
// 3. TaskListener 添加到扩展元素
extensionElements.computeIfAbsent("taskListener", k -> new ArrayList<>()).add(taskListener);
// 4. 设置扩展元素
userTask.setExtensionElements(extensionElements);
log.debug("配置审批节点完成: {}", node.getNodeName());
} catch (Exception e) {
log.error("配置审批节点失败: {}", node.getNodeName(), e);
throw new RuntimeException("配置审批节点失败: " + e.getMessage(), e);
}
}
/**
* TaskListener 添加 field 子元素
*
* @param taskListener TaskListener 扩展元素
* @param node 工作流节点定义
* @param validId sanitized 后的节点 ID
*/
private void addFieldsToTaskListener(ExtensionElement taskListener, WorkflowDefinitionGraphNode node, String validId) {
try {
// 1. 添加 nodeId field
taskListener.addChildElement(createFieldElement("nodeId", validId));
// 2. 添加 configs field
if (node.getConfigs() != null) {
String configsJson = objectMapper.writeValueAsString(node.getConfigs());
taskListener.addChildElement(createFieldElement("configs", configsJson));
}
// 3. 添加 inputMapping field
if (node.getInputMapping() != null) {
String inputMappingJson = objectMapper.writeValueAsString(node.getInputMapping());
taskListener.addChildElement(createFieldElement("inputMapping", inputMappingJson));
}
} catch (Exception e) {
log.error("添加 TaskListener field 字段失败: {}", node.getNodeName(), e);
throw new RuntimeException("添加 TaskListener field 字段失败: " + e.getMessage(), e);
}
}
/**
* 配置服务任务节点
*
@ -274,6 +368,7 @@ public class BpmnConverter {
return "${notificationDelegate}";
case "SCRIPT_NODE":
return "${shellDelegate}";
case "APPROVAL":
case "APPROVAL_NODE":
return "${approvalDelegate}";
case "DEPLOY_NODE":