package jnpf.flowable.service.dev; import com.alibaba.fastjson.JSON; import com.alibaba.fastjson.JSONObject; import jnpf.base.UserInfo; import jnpf.base.entity.VisualdevEntity; import jnpf.exception.WorkFlowException; import jnpf.flowable.entity.OperatorEntity; import jnpf.flowable.entity.TemplateNodeEntity; import jnpf.flowable.enums.NodeEnum; import jnpf.flowable.enums.OperatorStateEnum; import jnpf.flowable.enums.TaskStatusEnum; import jnpf.flowable.model.candidates.CandidateCheckFo; import jnpf.flowable.model.candidates.CandidateCheckVo; import jnpf.flowable.model.task.FlowModel; import jnpf.flowable.model.templatenode.nodejson.FormMappingConfig; import jnpf.flowable.model.templatenode.nodejson.NodeModel; import jnpf.flowable.model.util.*; import jnpf.flowable.service.OperatorService; import jnpf.flowable.service.TaskService; import jnpf.flowable.util.FlowUtil; import jnpf.flowable.util.OperatorUtil; import jnpf.flowable.util.ServiceUtil; import jnpf.permission.entity.UserEntity; import jnpf.util.UserProvider; import lombok.Data; import lombok.RequiredArgsConstructor; import org.springframework.stereotype.Service; import java.util.*; import java.util.concurrent.TimeUnit; /** Validates and advances one native operator; DevApprovalRuns owns the bounded background sequence. */ @Service @RequiredArgsConstructor @lombok.extern.slf4j.Slf4j public class DevApprovalService { private final DevApprovalProperties properties; private final FlowUtil flowUtil; private final OperatorUtil operatorUtil; private final OperatorService operatorService; private final TaskService taskService; private final ServiceUtil serviceUtil; private final DevApprovalLocks redisLock; private final DevApprovalStep step; private final DevApprovalSignatures signatures; private final jnpf.base.DictionaryDataApi dictionaries; @Data public static class State { private boolean enabled; private String status = "BLOCKED"; private String operatorId; private String nodeName; private List problems = new ArrayList<>(); } private static class Prepared { final State state = new State(); final FlowModel model = new FlowModel(); OperatorEntity operator; } public State inspect(String taskId, String signId) throws WorkFlowException { if (!allowed()) return new State(); try { DevApprovalSignature sign = signId == null || signId.isBlank() ? null : signatures.require(taskId, signId); return prepare(taskId, sign).state; } finally { clearContext(); } } boolean allowed() { UserInfo user = UserProvider.getUser(); return properties.available() && user != null && Boolean.TRUE.equals(user.getIsAdministrator()); } public State approve(String taskId, String expectedOperatorId, String signId) throws WorkFlowException { if (!allowed()) throw new WorkFlowException("开发审核未启用或当前用户不是管理员"); if (expectedOperatorId != null && expectedOperatorId.isBlank()) throw new WorkFlowException("缺少待办标识,请刷新后重试"); DevApprovalSignature signature = signatures.require(taskId, signId); String token = UUID.randomUUID().toString(); String taskLock = "dev-approve:" + taskId; if (!redisLock.lock(taskLock, token, 300, TimeUnit.SECONDS)) throw new WorkFlowException("此流程正在执行开发审核"); long started = System.nanoTime(); String nodeLock = null; try { Prepared prepared = prepare(taskId, signature, false); if (!"READY".equals(prepared.state.getStatus())) return prepared.state; if (expectedOperatorId != null && !expectedOperatorId.equals(prepared.operator.getId())) throw new WorkFlowException("待办已变化,请刷新后重试"); String key = taskId + prepared.operator.getNodeId(); if (!redisLock.lock(key, token, 300, TimeUnit.SECONDS)) throw new WorkFlowException("当前节点正在审批中,请稍后重试"); nodeLock = key; // Re-read under the same node lock used by normal approval. prepared = prepare(taskId, signature); if (!"READY".equals(prepared.state.getStatus())) return prepared.state; if (!key.equals(taskId + prepared.operator.getNodeId())) throw new WorkFlowException("待办节点已变化,请重新查看流程"); if (expectedOperatorId != null && !expectedOperatorId.equals(prepared.operator.getId())) throw new WorkFlowException("待办已变化,请刷新后重试"); FlowModel model = prepared.model; try { step.approve(prepared.operator, model, signature.getUserId()); } catch (Exception e) { try { operatorUtil.compensate(model.getTaskEntity()); } catch (Exception recoveryFailure) { e.addSuppressed(recoveryFailure); log.error("开发审批失败且待办恢复失败 task={}", taskId, e); throw new WorkFlowException("审批失败且待办恢复未完成,请先核对流程状态;原错误:" + e.getMessage()); } if (e instanceof WorkFlowException && "任务不存在".equals(e.getMessage())) { log.warn("开发审批使用的引擎待办已失效,原生补偿已完成 task={}", taskId, e); throw new WorkFlowException("当前引擎待办已失效,系统已执行待办恢复;本次未完成审批,请刷新流程并重新签名后重试"); } throw e; } operatorUtil.handleOperator(); operatorUtil.handleTaskStatusThenEvent(); // Each manual node is preflighted on the next background step; don't sweep unchecked nodes via autoAudit. operatorUtil.launchTrigger(model); operatorUtil.handleTaskStatus(); State result = new State(); result.setEnabled(true); result.setOperatorId(prepared.operator.getId()); result.setNodeName(prepared.state.getNodeName()); result.setStatus(Objects.equals(taskService.getInfo(taskId).getStatus(), TaskStatusEnum.PASSED.getCode()) ? "PASSED" : "ADVANCED"); return result; } finally { log.info("开发审批单步结束 task={} elapsedMs={}", taskId, (System.nanoTime() - started) / 1_000_000); clearContext(); if (nodeLock != null) redisLock.unlock(nodeLock, token); redisLock.unlock(taskLock, token); } } private Prepared prepare(String taskId, DevApprovalSignature signature) throws WorkFlowException { return prepare(taskId, signature, true); } private Prepared prepare(String taskId, DevApprovalSignature signature, boolean full) throws WorkFlowException { Prepared prepared = new Prepared(); flowUtil.setFlowModel(taskId, prepared.model); TemplateNodeEntity start = prepared.model.getNodeEntityList().stream() .filter(n -> NodeEnum.START.getType().equals(n.getNodeType())).findFirst().orElse(null); if (start == null || !properties.allowsForm(start.getFormId())) return prepared; prepared.state.setEnabled(true); Integer status = prepared.model.getTaskEntity().getStatus(); if (Objects.equals(status, TaskStatusEnum.PASSED.getCode())) { prepared.state.setStatus("PASSED"); return prepared; } if (!Objects.equals(status, TaskStatusEnum.RUNNING.getCode()) || prepared.model.getTaskEntity().getRejectDataId() != null) { prepared.state.getProblems().add("仅支持正常进行中的流程;请先处理暂停、退回或撤销状态"); return prepared; } // Button discovery must not depend on form data or candidate calculation. // Signed inspection and every approval still perform the full preflight below. if (signature == null) { prepared.state.setStatus("READY"); return prepared; } prepared.operator = operatorService.getList(taskId).stream().filter(DevApprovalService::pending) .sorted(Comparator.comparing(OperatorEntity::getId)).findFirst().orElse(null); if (prepared.operator == null) { prepared.state.getProblems().add("当前没有可自动推进的待办,可能正在等待子流程、触发器或外部事件"); return prepared; } OperatorEntity operator = prepared.operator; prepared.state.setOperatorId(operator.getId()); prepared.state.setNodeName(operator.getNodeName()); if (!full) { prepared.state.setStatus("READY"); return prepared; } try (DevApprovalActor ignored = DevApprovalActor.open(operator.getId(), signature.getUserId(), taskId)) { NodeModel node = prepared.model.getNodes().get(operator.getNodeCode()); TemplateNodeEntity definition = prepared.model.getNodeEntityList().stream() .filter(n -> operator.getNodeCode().equals(n.getNodeCode())).findFirst().orElse(null); List problems = prepared.state.getProblems(); if (node == null || definition == null || !properties.allowsForm(definition.getFormId())) { problems.add("当前节点表单不在开发审核允许范围内"); return prepared; } if (!NodeEnum.APPROVER.getType().equals(node.getType()) && !NodeEnum.PROCESSING.getType().equals(node.getType())) { problems.add("当前节点不是审批或办理节点"); return prepared; } if (Boolean.TRUE.equals(node.getHasSign())) problems.add("当前节点还要求手写签名图片,电子签名凭证不能代替图片,请通过正常入口完成此节点"); if (Boolean.TRUE.equals(node.getIsCustomCopy())) problems.add("当前节点需要人工选择抄送人"); JSONObject raw = JSON.parseObject(definition.getNodeJson()); JSONObject approve = raw.getJSONObject("approveFuncConfig"); if (approve != null && approve.getJSONObject("validateFuncConfig") != null && Boolean.TRUE.equals(approve.getJSONObject("validateFuncConfig").getBoolean("on"))) { problems.add("当前节点配置了前端自定义审批校验,请通过正常审核入口完成"); } Map saved = serviceUtil.infoData(definition.getFormId(), taskId); if (saved == null || saved.isEmpty()) { problems.add("未能读取已保存的表单数据"); return prepared; } String actorId = signature.getUserId(); String opinion = "开发测试一键审核;签名用户:" + actorId + ";电子签名:" + signature.getId() + ";原待办人:" + operator.getHandleId(); Map effective = JSON.parseObject(JSON.toJSONString(saved)); if (operator.getDraftData() != null && !operator.getDraftData().isBlank()) { Map draft = JSON.parseObject(operator.getDraftData()); Object draftVersion = draft.get("f_version"); if (draftVersion != null && saved.get("f_version") != null && !draftVersion.toString().equals(saved.get("f_version").toString())) { problems.add("节点暂存数据版本已变化,请先在正常表单中核对并保存"); return prepared; } draft.forEach((key, value) -> { if (!"f_version".equals(key)) effective.put(key, value); }); } UserEntity signer = serviceUtil.getUserInfo(actorId); if (signer == null || !Objects.equals(signer.getEnabledMark(), 1) || signer.getDeleteMark() != null) { problems.add("签名用户不存在或已停用"); return prepared; } VisualdevEntity released = serviceUtil.getReleaseInfo(definition.getFormId()); if (released == null || released.getFormData() == null) { problems.add("无法读取已发布表单控件配置"); return prepared; } String bizSign = signature.getId(); effective.put("biz_sign", bizSign); Map options = new HashMap<>(); problems.addAll(DevApprovalFill.fill(node, released.getFormData(), effective, signer, bizSign, type -> options.computeIfAbsent(type, key -> { var items = dictionaries.getList(key, "true"); return items == null ? new com.alibaba.fastjson.JSONArray() : (com.alibaba.fastjson.JSONArray) JSON.toJSON(items); }))); FormMappingConfig mapping = node.getFormMapping(); if (mapping != null && Boolean.TRUE.equals(mapping.getFillToForm())) { put(effective, mapping.getOpinionField(), opinion); put(effective, mapping.getOperatorField(), actorId); put(effective, mapping.getDateField(), System.currentTimeMillis()); put(effective, mapping.getDepartmentField(), signer.getOrganizeId()); put(effective, mapping.getFileField(), Collections.emptyList()); } if (!problems.isEmpty()) return prepared; CandidateCheckFo candidates = new CandidateCheckFo(); candidates.setId(taskId); candidates.setFlowId(prepared.model.getTaskEntity().getFlowId()); candidates.setFormData(effective); CandidateCheckVo next = taskService.checkCandidates(operator.getId(), candidates); if (next == null || (!Objects.equals(next.getType(), 3) && !Objects.equals(next.getType(), 2))) { problems.add("下一步需要人工选择分支或自由审批,请先处理此节点"); return prepared; } if (Objects.equals(next.getType(), 2)) { if (next.getList() == null || next.getList().isEmpty()) { problems.add("候选节点配置为空,无法确定下一步"); return prepared; } for (var choice : next.getList()) { if (Boolean.TRUE.equals(choice.getIsCandidates())) prepared.model.getCandidateList().put(choice.getNodeCode(), List.of(actorId)); } } prepared.model.setFormData(effective); prepared.model.setHandleStatus(FlowNature.AUDIT_COMPLETION); prepared.model.setHandleOpinion(opinion); operatorUtil.getGlobalParam(prepared.model.getTaskEntity(), node, prepared.model.getNodes().get(NodeEnum.GLOBAL.getType()), effective); prepared.state.setStatus("READY"); return prepared; } } static void clearContext() { FlowContextHolder.clearAll(); FlowOperatorHolder.clear(); FlowStatusHolder.clear(); FlowEventHolder.clear(); SystemAuditHolder.clear(); TriggerHolder.clear(); TriggerRecordHolder.clear(); } static boolean pending(OperatorEntity operator) { return operator.getHandleStatus() == null && !Objects.equals(operator.getCompletion(), FlowNature.ACTION) && Arrays.asList(OperatorStateEnum.RUNING.getCode(), OperatorStateEnum.ADD_SIGN.getCode(), OperatorStateEnum.TRANSFER.getCode(), OperatorStateEnum.ASSIGNED.getCode(), OperatorStateEnum.TRANSFER_PROCESSING.getCode()).contains(operator.getStatus()); } private static void put(Map data, String field, Object value) { if (field != null && !field.isBlank()) data.put(field, value); } }