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<String> 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<String> 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<String, Object> 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<String, Object> effective = JSON.parseObject(JSON.toJSONString(saved));
|
if (operator.getDraftData() != null && !operator.getDraftData().isBlank()) {
|
Map<String, Object> 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<String, com.alibaba.fastjson.JSONArray> 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<String, Object> data, String field, Object value) {
|
if (field != null && !field.isBlank()) data.put(field, value);
|
}
|
}
|