Merge remote-tracking branch 'origin/feature/bpm' into feature/bpm

This commit is contained in:
jason 2025-01-19 18:33:32 +08:00
commit d36fc98f01
22 changed files with 144 additions and 26 deletions

View File

@ -128,7 +128,7 @@ public class YudaoWebSecurityConfigurerAdapter {
// 全局共享规则
.authorizeHttpRequests(c -> c
// 1.1 静态资源可匿名访问
.requestMatchers(HttpMethod.GET, "/*.html", "/*.html", "/*.css", "/*.js").permitAll()
.requestMatchers(HttpMethod.GET, "/*.html", "/*.css", "/*.js").permitAll()
// 1.2 设置 @PermitAll 无需认证
.requestMatchers(HttpMethod.GET, permitAllUrls.get(HttpMethod.GET).toArray(new String[0])).permitAll()
.requestMatchers(HttpMethod.POST, permitAllUrls.get(HttpMethod.POST).toArray(new String[0])).permitAll()

View File

@ -38,6 +38,7 @@ public interface ErrorCodeConstants {
ErrorCode PROCESS_INSTANCE_START_USER_SELECT_ASSIGNEES_NOT_CONFIG = new ErrorCode(1_009_004_003, "任务({})的候选人未配置");
ErrorCode PROCESS_INSTANCE_START_USER_SELECT_ASSIGNEES_NOT_EXISTS = new ErrorCode(1_009_004_004, "任务({})的候选人({})不存在");
ErrorCode PROCESS_INSTANCE_START_USER_CAN_START = new ErrorCode(1_009_004_005, "发起流程失败,你没有权限发起该流程");
ErrorCode PROCESS_INSTANCE_CANCEL_FAIL_NOT_ALLOW = new ErrorCode(1_009_004_005, "流程取消失败,该流程不允许取消");
// ========== 流程任务 1-009-005-000 ==========
ErrorCode TASK_OPERATE_FAIL_ASSIGN_NOT_SELF = new ErrorCode(1_009_005_001, "操作失败,原因:该任务的审批人不是你");
@ -55,6 +56,7 @@ public interface ErrorCodeConstants {
ErrorCode TASK_TRANSFER_FAIL_USER_NOT_EXISTS = new ErrorCode(1_009_005_014, "任务转办失败,转办人不存在");
ErrorCode TASK_CREATE_FAIL_NO_CANDIDATE_USER = new ErrorCode(1_009_006_003, "操作失败,原因:找不到任务的审批人!");
ErrorCode TASK_SIGNATURE_NOT_EXISTS = new ErrorCode(1_009_005_015, "签名不能为空!");
ErrorCode TASK_REASON_REQUIRE = new ErrorCode(1_009_005_016, "审批意见不能为空!");
// ========== 动态表单模块 1-009-010-000 ==========
ErrorCode FORM_NOT_EXISTS = new ErrorCode(1_009_010_000, "动态表单不存在");

View File

@ -161,6 +161,15 @@ public class BpmModelController {
return success(true);
}
@DeleteMapping("/clean")
@Operation(summary = "清理模型")
@Parameter(name = "id", description = "编号", required = true, example = "1024")
@PreAuthorize("@ss.hasPermission('bpm:model:clean')")
public CommonResult<Boolean> cleanModel(@RequestParam("id") String id) {
modelService.cleanModel(getLoginUserId(), id);
return success(true);
}
// ========== 仿钉钉/飞书的精简模型 =========
@GetMapping("/simple/get")

View File

@ -62,4 +62,7 @@ public class BpmModelMetaInfoVO {
@Schema(description = "排序", example = "1")
private Long sort; // 创建时后端自动生成
@Schema(description = "允许撤销审批中的申请", example = "true")
private Boolean allowCancelRunningProcess;
}

View File

@ -63,6 +63,9 @@ public class BpmSimpleModelNodeVO {
@Schema(description = "是否需要签名", example = "false")
private Boolean signEnable;
@Schema(description = "是否填写审批意见", example = "false")
private Boolean reasonRequire;
/**
* 审批节点拒绝处理
*/

View File

@ -101,9 +101,8 @@ public class BpmApprovalDetailRespVO {
@Schema(description = "审批意见", example = "同意")
private String reason;
// TODO @lesan改成 signPicUrl 会好点
@Schema(description = "签名", example = "https://www.iocoder.cn/sign.png")
private String sign;
private String signPicUrl;
}

View File

@ -14,13 +14,11 @@ public class BpmTaskApproveReqVO {
@NotEmpty(message = "任务编号不能为空")
private String id;
@Schema(description = "审批意见", requiredMode = Schema.RequiredMode.REQUIRED, example = "不错不错!")
@NotEmpty(message = "审批意见不能为空")
@Schema(description = "审批意见", example = "不错不错!")
private String reason;
// TODO @lesan改成 signPicUrl 会好点
@Schema(description = "签名", example = "https://www.iocoder.cn/sign.png")
private String sign;
private String signPicUrl;
@Schema(description = "变量实例(动态表单)", requiredMode = Schema.RequiredMode.REQUIRED)
private Map<String, Object> variables;

View File

@ -14,7 +14,6 @@ public class BpmTaskRejectReqVO {
private String id;
@Schema(description = "审批意见", requiredMode = Schema.RequiredMode.REQUIRED, example = "不错不错!")
@NotEmpty(message = "审批意见不能为空")
private String reason;
}

View File

@ -78,9 +78,12 @@ public class BpmTaskRespVO {
@Schema(description = "操作按钮设置值")
private Map<Integer, OperationButtonSetting> buttonsSetting;
@Schema(description = "是否需要签名")
@Schema(description = "是否需要签名", example = "false")
private Boolean signEnable;
@Schema(description = "是否填写审批意见", example = "false")
private Boolean reasonRequire;
@Data
@Schema(description = "流程实例")
public static class ProcessInstance {

View File

@ -187,7 +187,7 @@ public interface BpmProcessInstanceConvert {
}
return BeanUtils.toBean(task, BpmApprovalDetailRespVO.ActivityNodeTask.class)
.setStatus(FlowableUtils.getTaskStatus(task)).setReason(FlowableUtils.getTaskReason(task))
.setSign(FlowableUtils.getTaskSign(task));
.setSignPicUrl(FlowableUtils.getTaskSignPicUrl(task));
}
default Set<Long> parseUserIds(HistoricProcessInstance processInstance,

View File

@ -150,4 +150,9 @@ public class BpmProcessDefinitionInfoDO extends BaseDO {
@TableField(typeHandler = StringListTypeHandler.class) // 为了可以使用 find_in_set 进行过滤
private List<Long> managerUserIds;
/**
* 是否允许撤销审批中的申请
*/
private Boolean allowCancelRunningProcess;
}

View File

@ -115,4 +115,9 @@ public interface BpmnModelConstants {
*/
String SIGN_ENABLE = "signEnable";
/**
* 审批意见是否必填
*/
String REASON_REQUIRE = "reasonRequire";
}

View File

@ -43,6 +43,13 @@ public class BpmnVariableConstants {
* @see ProcessInstance#getProcessVariables()
*/
public static final String PROCESS_INSTANCE_VARIABLE_RETURN_FLAG = "RETURN_FLAG_%s";
/**
* 流程实例的变量 - 是否跳过表达式
*
* @see ProcessInstance#getProcessVariables()
* @see <a href="https://blog.csdn.net/weixin_42065235/article/details/126039993">Flowable/Activiti之SkipExpression 完成自动审批</a>
*/
public static final String PROCESS_INSTANCE_SKIP_EXPRESSION_ENABLED = "_FLOWABLE_SKIP_EXPRESSION_ENABLED";
/**
* 任务的变量 - 状态
@ -58,8 +65,9 @@ public class BpmnVariableConstants {
* @see org.flowable.task.api.Task#getTaskLocalVariables()
*/
public static final String TASK_VARIABLE_REASON = "TASK_REASON";
// TODO @lesanTASK_SIGN_PIC_URL 虽然长一点嘿嘿
public static final String TASK_VARIABLE_SIGN = "TASK_SIGN";
/**
* 任务变量 - 签名图片 URL
*/
public static final String TASK_SIGN_PIC_URL = "TASK_SIGN_PIC_URL";
}

View File

@ -39,10 +39,10 @@ public class BpmProcessInstanceEventListener extends AbstractFlowableEngineEvent
processInstanceService.processProcessInstanceCompleted((ProcessInstance)event.getEntity());
}
@Override
// 特殊情况当跳转到 EndEvent 流程实例未结束, 会执行 deleteProcessInstance 方法
@Override // 特殊情况当跳转到 EndEvent 流程实例未结束, 会执行 deleteProcessInstance 方法
protected void processCancelled(FlowableCancelledEvent event) {
ProcessInstance processInstance = processInstanceService.getProcessInstance(event.getProcessInstanceId());
processInstanceService.processProcessInstanceCompleted(processInstance);
}
}

View File

@ -365,6 +365,23 @@ public class BpmnModelUtils {
return Convert.toBool(extensionElements.get(0).getElementText(), false);
}
public static void addReasonRequire(Boolean reasonRequire, FlowElement userTask) {
addExtensionElement(userTask, REASON_REQUIRE,
ObjUtil.isNotNull(reasonRequire) ? reasonRequire.toString() : Boolean.FALSE.toString());
}
public static Boolean parseReasonRequire(BpmnModel bpmnModel, String flowElementId) {
FlowElement flowElement = getFlowElementById(bpmnModel, flowElementId);
if (flowElement == null) {
return false;
}
List<ExtensionElement> extensionElements = flowElement.getExtensionElements().get(REASON_REQUIRE);
if (CollUtil.isEmpty(extensionElements)) {
return false;
}
return Convert.toBool(extensionElements.get(0).getElementText(), false);
}
public static void addListenerConfig(FlowableListener flowableListener, BpmSimpleModelNodeVO.ListenerHandler handler) {
FieldExtension fieldExtension = new FieldExtension();
fieldExtension.setFieldName("listenerConfig");

View File

@ -213,9 +213,14 @@ public class FlowableUtils {
return (String) task.getTaskLocalVariables().get(BpmnVariableConstants.TASK_VARIABLE_REASON);
}
// TODO @lesan这个方法名也改咧
public static String getTaskSign(TaskInfo task) {
return (String) task.getTaskLocalVariables().get(BpmnVariableConstants.TASK_VARIABLE_SIGN);
/**
* 获得任务的签名图片 URL
*
* @param task 任务
* @return 签名图片 URL
*/
public static String getTaskSignPicUrl(TaskInfo task) {
return (String) task.getTaskLocalVariables().get(BpmnVariableConstants.TASK_SIGN_PIC_URL);
}
/**

View File

@ -441,6 +441,8 @@ public class SimpleModelUtils {
addUserTaskListener(node, userTask);
// 添加是否需要签名
addSignEnable(node.getSignEnable(), userTask);
// 审批意见
addReasonRequire(node.getReasonRequire(), userTask);
return userTask;
}

View File

@ -88,6 +88,14 @@ public interface BpmModelService {
*/
void deleteModel(Long userId, String id);
/**
* 清理模型包括流程实例
*
* @param userId 用户编号
* @param id 编号
*/
void cleanModel(Long userId, String id);
/**
* 修改模型的状态实际更新的部署的流程定义的状态
*

View File

@ -14,6 +14,7 @@ import cn.iocoder.yudao.module.bpm.convert.definition.BpmModelConvert;
import cn.iocoder.yudao.module.bpm.dal.dataobject.definition.BpmFormDO;
import cn.iocoder.yudao.module.bpm.enums.definition.BpmModelFormTypeEnum;
import cn.iocoder.yudao.module.bpm.enums.definition.BpmModelTypeEnum;
import cn.iocoder.yudao.module.bpm.enums.task.BpmReasonEnum;
import cn.iocoder.yudao.module.bpm.framework.flowable.core.candidate.BpmTaskCandidateInvoker;
import cn.iocoder.yudao.module.bpm.framework.flowable.core.util.BpmnModelUtils;
import cn.iocoder.yudao.module.bpm.framework.flowable.core.util.FlowableUtils;
@ -25,10 +26,14 @@ import org.flowable.bpmn.model.BpmnModel;
import org.flowable.bpmn.model.StartEvent;
import org.flowable.bpmn.model.UserTask;
import org.flowable.common.engine.impl.db.SuspensionState;
import org.flowable.engine.HistoryService;
import org.flowable.engine.RepositoryService;
import org.flowable.engine.RuntimeService;
import org.flowable.engine.history.HistoricProcessInstance;
import org.flowable.engine.repository.Model;
import org.flowable.engine.repository.ModelQuery;
import org.flowable.engine.repository.ProcessDefinition;
import org.flowable.engine.runtime.ProcessInstance;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import org.springframework.validation.annotation.Validated;
@ -63,6 +68,11 @@ public class BpmModelServiceImpl implements BpmModelService {
@Resource
private BpmTaskCandidateInvoker taskCandidateInvoker;
@Resource
private HistoryService historyService;
@Resource
private RuntimeService runtimeService;
@Override
public List<Model> getModelList(String name) {
ModelQuery modelQuery = repositoryService.createModelQuery();
@ -246,6 +256,31 @@ public class BpmModelServiceImpl implements BpmModelService {
updateProcessDefinitionSuspended(model.getDeploymentId());
}
@Override
public void cleanModel(Long userId, String id) {
// 1. 校验流程模型存在
Model model = validateModelManager(id, userId);
// 2. 清理所有流程数据
// TODO @芋艿这里没有找到批量操作的方法会不会有性能问题~
// TODO @lesan建议按照顺序1List<ProcessInstance> processInstances 循环处理然后删除删除一个示实例接着删除它的 history
// 2.1 先取消所有正在运行的流程
List<ProcessInstance> processInstances = runtimeService.createProcessInstanceQuery()
.processDefinitionKey(model.getKey()).list();
processInstances.forEach(processInstance -> {
runtimeService.deleteProcessInstance(processInstance.getId(),
BpmReasonEnum.CANCEL_BY_SYSTEM.getReason());
});
// 2.2 再从历史中删除所有相关的流程数据
List<HistoricProcessInstance> historicProcessInstances = historyService.createHistoricProcessInstanceQuery()
.processDefinitionKey(model.getKey()).list();
historicProcessInstances.forEach(historicProcessInstance -> {
historyService.deleteHistoricProcessInstance(historicProcessInstance.getId());
});
// TODO @lesan流程任务是不是也要清理哈
// TODO @lesan抄送是不是也要清理
}
@Override
public void updateModelState(Long userId, String id, Integer state) {
// 1.1 校验流程模型存在

View File

@ -595,6 +595,7 @@ public class BpmProcessInstanceServiceImpl implements BpmProcessInstanceService
variables.put(BpmnVariableConstants.PROCESS_INSTANCE_VARIABLE_START_USER_ID, userId); // 设置流程变量发起人 ID
variables.put(BpmnVariableConstants.PROCESS_INSTANCE_VARIABLE_STATUS, // 流程实例状态审批中
BpmProcessInstanceStatusEnum.RUNNING.getStatus());
variables.put(BpmnVariableConstants.PROCESS_INSTANCE_SKIP_EXPRESSION_ENABLED, true); // 跳过表达式需要添加此变量为 true不影响没配置 skipExpression 的节点
if (CollUtil.isNotEmpty(startUserSelectAssignees)) {
variables.put(BpmnVariableConstants.PROCESS_INSTANCE_VARIABLE_START_USER_SELECT_ASSIGNEES, startUserSelectAssignees);
}
@ -641,6 +642,13 @@ public class BpmProcessInstanceServiceImpl implements BpmProcessInstanceService
if (!Objects.equals(instance.getStartUserId(), String.valueOf(userId))) {
throw exception(PROCESS_INSTANCE_CANCEL_FAIL_NOT_SELF);
}
// 1.3 校验允许撤销审批中的申请
BpmProcessDefinitionInfoDO processDefinitionInfo = processDefinitionService.getProcessDefinitionInfo(instance.getProcessDefinitionId());
Assert.notNull(processDefinitionInfo, "流程定义({})不存在", processDefinitionInfo);
if (processDefinitionInfo.getAllowCancelRunningProcess() != null // 防止未配置 AllowCancelRunningProcess , 默认为可取消
&& Boolean.FALSE.equals(processDefinitionInfo.getAllowCancelRunningProcess())) {
throw exception(PROCESS_INSTANCE_CANCEL_FAIL_NOT_ALLOW);
}
// 2. 取消流程
updateProcessInstanceCancel(cancelReqVO.getId(),

View File

@ -64,6 +64,7 @@ import static cn.iocoder.yudao.framework.common.exception.util.ServiceExceptionU
import static cn.iocoder.yudao.framework.common.util.collection.CollectionUtils.*;
import static cn.iocoder.yudao.module.bpm.enums.ErrorCodeConstants.*;
import static cn.iocoder.yudao.module.bpm.framework.flowable.core.enums.BpmnVariableConstants.PROCESS_INSTANCE_VARIABLE_RETURN_FLAG;
import static cn.iocoder.yudao.module.bpm.framework.flowable.core.util.BpmnModelUtils.parseReasonRequire;
import static cn.iocoder.yudao.module.bpm.framework.flowable.core.util.BpmnModelUtils.parseSignEnable;
/**
@ -163,6 +164,7 @@ public class BpmTaskServiceImpl implements BpmTaskService {
Map<Integer, BpmTaskRespVO.OperationButtonSetting> buttonsSetting = BpmnModelUtils.parseButtonsSetting(
bpmnModel, todoTask.getTaskDefinitionKey());
Boolean signEnable = parseSignEnable(bpmnModel, todoTask.getTaskDefinitionKey());
Boolean reasonRequire = parseReasonRequire(bpmnModel, todoTask.getTaskDefinitionKey());
// 4. 任务表单
BpmFormDO taskForm = null;
@ -171,7 +173,8 @@ public class BpmTaskServiceImpl implements BpmTaskService {
}
return BpmTaskConvert.INSTANCE.buildTodoTask(todoTask, childrenTasks, buttonsSetting, taskForm)
.setSignEnable(signEnable);
.setSignEnable(signEnable)
.setReasonRequire(reasonRequire);
}
@Override
@ -485,9 +488,14 @@ public class BpmTaskServiceImpl implements BpmTaskService {
// 1.3 校验签名
BpmnModel bpmnModel = modelService.getBpmnModelByDefinitionId(task.getProcessDefinitionId());
Boolean signEnable = parseSignEnable(bpmnModel, task.getTaskDefinitionKey());
if (signEnable && StrUtil.isEmpty(reqVO.getSign())) {
if (signEnable && StrUtil.isEmpty(reqVO.getSignPicUrl())) {
throw exception(TASK_SIGNATURE_NOT_EXISTS);
}
// 1.4 校验审批意见
Boolean reasonRequire = parseReasonRequire(bpmnModel, task.getTaskDefinitionKey());
if (reasonRequire && StrUtil.isEmpty(reqVO.getReason())) {
throw exception(TASK_REASON_REQUIRE);
}
// 情况一被委派的任务不调用 complete 去完成任务
if (DelegationState.PENDING.equals(task.getDelegationState())) {
@ -505,7 +513,7 @@ public class BpmTaskServiceImpl implements BpmTaskService {
// 2.1 更新 task 状态原因签字
updateTaskStatusAndReason(task.getId(), BpmTaskStatusEnum.APPROVE.getStatus(), reqVO.getReason());
if (signEnable) {
taskService.setVariableLocal(task.getId(), BpmnVariableConstants.TASK_VARIABLE_SIGN, reqVO.getSign());
taskService.setVariableLocal(task.getId(), BpmnVariableConstants.TASK_SIGN_PIC_URL, reqVO.getSignPicUrl());
}
// 2.2 添加评论
taskService.addComment(task.getId(), task.getProcessInstanceId(), BpmCommentTypeEnum.APPROVE.getType(),
@ -859,10 +867,11 @@ public class BpmTaskServiceImpl implements BpmTaskService {
.moveActivityIdsToSingleActivityId(activityIds, endEvent.getId())
.changeState();
// 3. 如果跳转到 EndEvent 流程还未结束 执行 deleteProcessInstance 方法
List<Execution> executionList = runtimeService.createExecutionQuery().processInstanceId(processInstanceId).list();
if (CollUtil.isNotEmpty(executionList)) {
log.warn("执行跳转到 EndEvent 后, 流程实例未结束。执行 [deleteProcessInstance] 方法");
// 3. 特殊如果跳转到 EndEvent 流程还未结束 执行 deleteProcessInstance 方法
// TODO 芋艿目前发现并行分支情况下会存在这个情况后续看看有没更好的方案
List<Execution> executions = runtimeService.createExecutionQuery().processInstanceId(processInstanceId).list();
if (CollUtil.isNotEmpty(executions)) {
log.warn("[moveTaskToEnd][执行跳转到 EndEvent 后, 流程实例未结束,强制执行 deleteProcessInstance 方法]");
runtimeService.deleteProcessInstance(processInstanceId, reason);
}
}

View File

@ -48,7 +48,7 @@ import static cn.iocoder.yudao.framework.pay.core.client.impl.weixin.WxPayClient
/**
* 微信支付抽象类实现微信统一的接口以及部分实现退款
*
* @author 遇到源码
* @author 芋道源码
*/
@Slf4j
public abstract class AbstractWxPayClient extends AbstractPayClient<WxPayClientConfig> {