- 多出口:FlwTask 增 edge_name 列(sql/20260916),FlwTaskRepository.claimAndFinish
同步落列;FlowTaskService.handle 出度>1 时 edgeName 必填;FlowEngine.continueAdvance
按 edge name 选路(单出口原路径逐字节兼容,未命中不推进);FlowAdvanceService/
FlowNodeAdvanceListener 透传;BranchScope 从 FlowEngine 提取
- DefValidator:多出口出口命名必填且同节点唯一;跨分支连线发布期明确拒绝
- 退回 POST /task/{id}/returnBack:默认退主管线最近已批节点,精确失效当前节点待办,
split 幂等 upsert 使退回重走并行段不撞 flw_branch uk
- 移交 POST /task/{id}/transfer:TRANSFERRED(5)+新建待办,AssigneeResolver.requireUserName
- 或签 ANY 补齐:首个同意失效同节点兄弟待办
- 已办口径/撤回守卫:status in (1,2,4,5);新事件 flow.returned/flow.transferred
- FlowDiagramService:EdgeVo.name/NodeVo.state/DiagramVo.layout,办件图状态计算
- 新 IT:多出口×3、ANY 或签、退回重走并行段、退回/移交/撤回
mvn test 84/84 绿
This commit is contained in:
@@ -5,12 +5,15 @@ import com.yunhai.flowcenter.engine.FlowEngine;
|
||||
import com.yunhai.flowcenter.entity.FlwAssigneeMap;
|
||||
import com.yunhai.flowcenter.entity.FlwDef;
|
||||
import com.yunhai.flowcenter.entity.FlwInstance;
|
||||
import com.yunhai.flowcenter.entity.FlwTask;
|
||||
import com.yunhai.flowcenter.enums.FlwEnums;
|
||||
import com.yunhai.flowcenter.model.FlowModels;
|
||||
import com.yunhai.flowcenter.repository.FlwAssigneeMapRepository;
|
||||
import com.yunhai.flowcenter.repository.FlwDefRepository;
|
||||
import com.yunhai.flowcenter.repository.FlwInstanceRepository;
|
||||
import com.yunhai.flowcenter.repository.FlwTaskRepository;
|
||||
import com.yunhai.platform.apistd.BizException;
|
||||
import com.yunhai.platform.openapi.client.OpenApiUserClient;
|
||||
import com.yunhai.platform.openapi.base.OpenApiUserClient;
|
||||
import com.yunhai.platform.utils.exception.ErrorCode;
|
||||
import com.yunhai.platform.utils.model.BoaUser;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
@@ -18,7 +21,11 @@ import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.stereotype.Service;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Set;
|
||||
import java.util.HashSet;
|
||||
|
||||
/**
|
||||
* 流程示意图出图(方案 §3.2):后端按视图解析好节点 label/subLabel,前端只画图不解析。
|
||||
@@ -42,6 +49,8 @@ public class FlowDiagramService {
|
||||
|
||||
private final FlwDefRepository defRepository;
|
||||
private final FlwAssigneeMapRepository mapRepository;
|
||||
private final FlwInstanceRepository instanceRepository;
|
||||
private final FlwTaskRepository taskRepository;
|
||||
private final FlowEngine flowEngine;
|
||||
private final OpenApiUserClient userClient;
|
||||
|
||||
@@ -87,12 +96,65 @@ public class FlowDiagramService {
|
||||
FlowModels.EdgeVo ev = new FlowModels.EdgeVo();
|
||||
ev.setFrom(e.from);
|
||||
ev.setTo(e.to);
|
||||
ev.setName(e.name);
|
||||
edges.add(ev);
|
||||
}
|
||||
vo.setEdges(edges);
|
||||
// 画布坐标透传(设计器保存;缺失时前端自行 Kahn 布局)
|
||||
if (model.layout != null && !model.layout.isEmpty()) {
|
||||
Map<String, Map<String, Integer>> layout = new HashMap<>();
|
||||
for (Map.Entry<String, FlowDefModel.Layout> en : model.layout.entrySet()) {
|
||||
if (en.getValue() == null) {
|
||||
continue;
|
||||
}
|
||||
Map<String, Integer> p = new HashMap<>();
|
||||
p.put("x", en.getValue().x);
|
||||
p.put("y", en.getValue().y);
|
||||
layout.put(en.getKey(), p);
|
||||
}
|
||||
vo.setLayout(layout);
|
||||
}
|
||||
// instance 口径:按任务轨迹给节点上运行时状态(前端高亮在办节点)
|
||||
if (instanceId != null) {
|
||||
Map<String, String> states = nodeStates(instanceId, model);
|
||||
for (FlowModels.NodeVo nv : nodes) {
|
||||
nv.setState(states.get(nv.getId()));
|
||||
}
|
||||
}
|
||||
return vo;
|
||||
}
|
||||
|
||||
/**
|
||||
* 办件节点运行时状态:done=已有已同意任务 / rejected=有拒绝任务(终态前)/
|
||||
* active=在 currentNodes / pending=其余。active 优先于 done(退回后重到同节点算在办)。
|
||||
*/
|
||||
private Map<String, String> nodeStates(Long instanceId, FlowDefModel model) {
|
||||
Map<String, String> states = new HashMap<>();
|
||||
Set<String> activeNodes = new HashSet<>();
|
||||
FlwInstance instance = instanceRepository.findById(instanceId).orElse(null);
|
||||
if (instance != null && instance.getCurrentNodes() != null && !instance.getCurrentNodes().isEmpty()) {
|
||||
activeNodes.addAll(java.util.Arrays.asList(instance.getCurrentNodes().split(",")));
|
||||
}
|
||||
for (FlowDefModel.Node n : model.nodes) {
|
||||
states.put(n.id, null);
|
||||
}
|
||||
for (FlwTask t : taskRepository.findByInstanceIdOrderByIdAsc(instanceId)) {
|
||||
String nodeState = states.get(t.getNodeId());
|
||||
if (t.getStatus() == FlwEnums.TaskStatus.REJECTED.getCode()) {
|
||||
states.put(t.getNodeId(), "rejected");
|
||||
} else if (t.getStatus() == FlwEnums.TaskStatus.APPROVED.getCode()
|
||||
&& nodeState == null && !activeNodes.contains(t.getNodeId())) {
|
||||
states.put(t.getNodeId(), "done");
|
||||
}
|
||||
}
|
||||
for (String nodeId : activeNodes) {
|
||||
if (states.containsKey(nodeId)) {
|
||||
states.put(nodeId, "active");
|
||||
}
|
||||
}
|
||||
return states;
|
||||
}
|
||||
|
||||
private FlowModels.NodeVo toNodeVo(Long schId, FlowDefModel.Node n, String mode) {
|
||||
FlowModels.NodeVo vo = new FlowModels.NodeVo();
|
||||
vo.setId(n.id);
|
||||
|
||||
@@ -126,8 +126,7 @@ public class FlowInstanceService {
|
||||
throw new BizException(ErrorCode.PARAM_ERROR, "仅审批中的办件可撤回");
|
||||
}
|
||||
if (!taskRepository.findByInstanceIdAndStatusIn(instanceId,
|
||||
java.util.Arrays.asList(FlwEnums.TaskStatus.APPROVED.getCode(),
|
||||
FlwEnums.TaskStatus.REJECTED.getCode())).isEmpty()) {
|
||||
FlwEnums.TaskStatus.handledCodes()).isEmpty()) {
|
||||
throw new BizException(ErrorCode.PARAM_ERROR, "已有任务被办理,不能撤回");
|
||||
}
|
||||
instance.setStatus(FlwEnums.InstanceStatus.WITHDRAWN.getCode());
|
||||
|
||||
@@ -5,8 +5,8 @@ import com.yunhai.flowcenter.repository.FlwAssigneeMapRepository;
|
||||
import com.yunhai.platform.apistd.BizException;
|
||||
import com.yunhai.platform.utils.exception.ErrorCode;
|
||||
import com.yunhai.platform.utils.model.BoaUser;
|
||||
import com.yunhai.platform.openapi.client.OpenApiGroupClient;
|
||||
import com.yunhai.platform.openapi.client.OpenApiUserClient;
|
||||
import com.yunhai.platform.openapi.base.OpenApiGroupClient;
|
||||
import com.yunhai.platform.openapi.base.OpenApiUserClient;
|
||||
import com.yunhai.platform.utils.model.BoaGroup;
|
||||
import lombok.Getter;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
package com.yunhai.flowcenter.service;
|
||||
|
||||
import com.yunhai.flowcenter.engine.BranchScope;
|
||||
import com.yunhai.flowcenter.engine.FlowDefModel;
|
||||
import com.yunhai.flowcenter.engine.FlowEngine;
|
||||
import com.yunhai.flowcenter.entity.FlwBranch;
|
||||
import com.yunhai.flowcenter.entity.FlwDef;
|
||||
@@ -111,6 +113,7 @@ public class FlowQueryService {
|
||||
row.put("handleTime", str(t.getHandleTime()));
|
||||
row.put("deadline", str(t.getDeadline()));
|
||||
row.put("urgeCount", t.getUrgeCount());
|
||||
row.put("edgeName", t.getEdgeName());
|
||||
trace.add(row);
|
||||
}
|
||||
detail.setTrace(trace);
|
||||
@@ -135,6 +138,53 @@ public class FlowQueryService {
|
||||
}
|
||||
FlwDef def = defRepository.findById(instance.getDefId()).orElse(null);
|
||||
detail.setNodeSummary(def == null ? "" : FlowEngine.nodeSummary(flowEngine.parseModel(def)));
|
||||
// 办理面板数据(出口/退回目标/移交标记):仅对持本人待办的查看者填充
|
||||
FlwTask myPend = viewerId == null ? null : tasks.stream()
|
||||
.filter(t -> viewerId.equals(t.getAssigneeId())
|
||||
&& t.getStatus() == FlwEnums.TaskStatus.PENDING.getCode())
|
||||
.findFirst().orElse(null);
|
||||
if (myPend != null && def != null) {
|
||||
FlowDefModel model = flowEngine.parseModel(def);
|
||||
String curNodeId = myPend.getNodeId();
|
||||
List<FlowDefModel.Edge> outs = model.outEdges(curNodeId);
|
||||
if (outs.size() > 1) {
|
||||
List<Map<String, Object>> outlets = new ArrayList<>();
|
||||
for (FlowDefModel.Edge e : outs) {
|
||||
Map<String, Object> o = new HashMap<>();
|
||||
o.put("name", e.name);
|
||||
o.put("toNodeId", e.to);
|
||||
FlowDefModel.Node to = model.node(e.to);
|
||||
o.put("toNodeName", to == null ? e.to
|
||||
: (to.name == null || to.name.isEmpty() ? e.to : to.name));
|
||||
outlets.add(o);
|
||||
}
|
||||
detail.setOutlets(outlets);
|
||||
} else {
|
||||
detail.setOutlets(new ArrayList<>());
|
||||
}
|
||||
// 可退回目标:办件已办过的主线审批节点(去重,排除当前节点)
|
||||
Map<String, Map<String, Object>> targets = new java.util.LinkedHashMap<>();
|
||||
for (FlwTask t : tasks) {
|
||||
if (t.getStatus() != FlwEnums.TaskStatus.APPROVED.getCode()
|
||||
|| t.getNodeId().equals(myPend.getNodeId())
|
||||
|| BranchScope.keyOf(model, t.getNodeId()) != null) {
|
||||
continue;
|
||||
}
|
||||
FlowDefModel.Node node = model.node(t.getNodeId());
|
||||
if (node != null && FlowDefModel.TYPE_APPROVAL.equals(node.type)
|
||||
&& !targets.containsKey(t.getNodeId())) {
|
||||
Map<String, Object> r = new HashMap<>();
|
||||
r.put("nodeId", node.id);
|
||||
r.put("nodeName", node.name == null || node.name.isEmpty() ? node.id : node.name);
|
||||
targets.put(node.id, r);
|
||||
}
|
||||
}
|
||||
detail.setReturnTargets(new ArrayList<>(targets.values()));
|
||||
detail.setCanTransfer(true);
|
||||
} else {
|
||||
detail.setOutlets(new ArrayList<>());
|
||||
detail.setReturnTargets(new ArrayList<>());
|
||||
}
|
||||
return detail;
|
||||
}
|
||||
|
||||
|
||||
@@ -1,13 +1,17 @@
|
||||
package com.yunhai.flowcenter.service;
|
||||
|
||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
import com.yunhai.flowcenter.engine.AssigneeResolver;
|
||||
import com.yunhai.flowcenter.engine.BranchScope;
|
||||
import com.yunhai.flowcenter.engine.FlowDefModel;
|
||||
import com.yunhai.flowcenter.engine.FlowEvent;
|
||||
import com.yunhai.flowcenter.engine.FlowEngine;
|
||||
import com.yunhai.flowcenter.entity.FlwDef;
|
||||
import com.yunhai.flowcenter.entity.FlwInstance;
|
||||
import com.yunhai.flowcenter.entity.FlwTask;
|
||||
import com.yunhai.flowcenter.enums.FlwEnums;
|
||||
import com.yunhai.flowcenter.repository.FlwBranchRepository;
|
||||
import com.yunhai.flowcenter.repository.FlwDefRepository;
|
||||
import com.yunhai.flowcenter.repository.FlwInstanceRepository;
|
||||
import com.yunhai.flowcenter.repository.FlwTaskRepository;
|
||||
import com.yunhai.platform.apistd.BizException;
|
||||
@@ -38,15 +42,20 @@ public class FlowTaskService {
|
||||
private final FlwTaskRepository taskRepository;
|
||||
private final FlwInstanceRepository instanceRepository;
|
||||
private final FlwBranchRepository branchRepository;
|
||||
private final FlwDefRepository defRepository;
|
||||
private final AssigneeResolver assigneeResolver;
|
||||
private final FlowEngine flowEngine;
|
||||
private final ApplicationEventPublisher eventPublisher;
|
||||
|
||||
/**
|
||||
* 办理(同意/拒绝收敛点,概览 §8.5 不变式 2)。
|
||||
*
|
||||
* @param edgeName 多出口节点的所选出口(办理按钮文案);单出结节忽略,出度>1 时必填
|
||||
* @return 办理后的任务(含最新状态)
|
||||
*/
|
||||
@Transactional
|
||||
public FlwTask handle(Long schId, Long taskId, boolean approved, String opinion, Integer operatorId) {
|
||||
public FlwTask handle(Long schId, Long taskId, boolean approved, String opinion,
|
||||
Integer operatorId, String edgeName) {
|
||||
FlwTask task = taskRepository.findById(taskId)
|
||||
.orElseThrow(() -> new BizException(ErrorCode.PARAM_ERROR, "任务不存在:" + taskId));
|
||||
if (!task.getSchId().equals(schId)) {
|
||||
@@ -58,10 +67,12 @@ public class FlowTaskService {
|
||||
if (!approved && (opinion == null || opinion.trim().isEmpty())) {
|
||||
throw new BizException(ErrorCode.PARAM_ERROR, "拒绝必须填写意见");
|
||||
}
|
||||
// 多出口选路校验(发布期已保证出口命名唯一):出度>1 必须带上出口名且能命中
|
||||
String chosenEdge = normalizeEdgeName(task, approved, edgeName);
|
||||
// 原子抢占:affected=1 才获得办理权(防双击/并发重复推进)
|
||||
int target = approved ? FlwEnums.TaskStatus.APPROVED.getCode() : FlwEnums.TaskStatus.REJECTED.getCode();
|
||||
LocalDateTime now = LocalDateTime.now();
|
||||
if (taskRepository.claimAndFinish(taskId, target, now, opinion) != 1) {
|
||||
if (taskRepository.claimAndFinish(taskId, target, now, opinion, chosenEdge) != 1) {
|
||||
throw new BizException(ErrorCode.PARAM_ERROR, "任务已被办理或已失效,请刷新");
|
||||
}
|
||||
FlwTask handled = taskRepository.findById(taskId).orElse(null);
|
||||
@@ -82,14 +93,63 @@ public class FlowTaskService {
|
||||
if (opinion != null && !opinion.isEmpty()) {
|
||||
data.put("opinion", opinion);
|
||||
}
|
||||
if (chosenEdge != null) {
|
||||
data.put("edgeName", chosenEdge);
|
||||
}
|
||||
eventPublisher.publishEvent(FlowEvent.of(FlowEvent.Type.TASK_HANDLED,
|
||||
instance.getId(), taskId, data));
|
||||
if (!approved) {
|
||||
rejectFlow(instance, task, opinion);
|
||||
} else if (FlowDefModel.COUNTERSIGN_ANY.equals(countersignOf(task))) {
|
||||
// 或签(ANY):任一人同意即放行,失效同节点兄弟待办——
|
||||
// 推进监听器只认 pending==0,不失效会卡到所有人办完(行为等同 ALL 的历史缺陷)
|
||||
taskRepository.invalidatePendingByNodeExcept(instance.getId(), task.getNodeId(), taskId);
|
||||
}
|
||||
return handled;
|
||||
}
|
||||
|
||||
/** 旧签名兼容(单出口调用方) */
|
||||
@Transactional
|
||||
public FlwTask handle(Long schId, Long taskId, boolean approved, String opinion, Integer operatorId) {
|
||||
return handle(schId, taskId, approved, opinion, operatorId, null);
|
||||
}
|
||||
|
||||
/** 出度>1 的节点必须选有效出口;单出口一律置 null(旧前端零改动) */
|
||||
private String normalizeEdgeName(FlwTask task, boolean approved, String edgeName) {
|
||||
if (!approved) {
|
||||
return null;
|
||||
}
|
||||
FlwDef def = defRepository.findById(instanceRepository.findById(task.getInstanceId())
|
||||
.map(FlwInstance::getDefId).orElse(null)).orElse(null);
|
||||
if (def == null) {
|
||||
return null;
|
||||
}
|
||||
FlowDefModel model = FlowDefModel.parse(objectMapper, def.getContentJson());
|
||||
java.util.List<FlowDefModel.Edge> outs = model.outEdges(task.getNodeId());
|
||||
if (outs.size() <= 1) {
|
||||
return null;
|
||||
}
|
||||
if (edgeName == null || edgeName.trim().isEmpty()) {
|
||||
throw new BizException(ErrorCode.PARAM_ERROR, "该节点有多个出口,请选择办理去向");
|
||||
}
|
||||
FlowDefModel.Edge hit = model.edgeByName(task.getNodeId(), edgeName.trim());
|
||||
if (hit == null) {
|
||||
throw new BizException(ErrorCode.PARAM_ERROR, "无效的办理去向:" + edgeName);
|
||||
}
|
||||
return hit.name;
|
||||
}
|
||||
|
||||
/** 该任务所属节点的会签模式(定义缺失/未配置按 ALL) */
|
||||
private String countersignOf(FlwTask task) {
|
||||
FlwDef def = defRepository.findById(instanceRepository.findById(task.getInstanceId())
|
||||
.map(FlwInstance::getDefId).orElse(null)).orElse(null);
|
||||
if (def == null) {
|
||||
return FlowDefModel.COUNTERSIGN_ALL;
|
||||
}
|
||||
FlowDefModel.Node node = FlowDefModel.parse(objectMapper, def.getContentJson()).node(task.getNodeId());
|
||||
return node == null || node.countersign == null ? FlowDefModel.COUNTERSIGN_ALL : node.countersign;
|
||||
}
|
||||
|
||||
/** END_REJECTED 拒绝终态(概览 §5.3):同批待办失效→分支失效→办件=30→拒绝事件 */
|
||||
private void rejectFlow(FlwInstance instance, FlwTask task, String opinion) {
|
||||
instance.setStatus(FlwEnums.InstanceStatus.REJECTED.getCode());
|
||||
@@ -106,6 +166,150 @@ public class FlowTaskService {
|
||||
instance.getId(), task.getId(), data));
|
||||
}
|
||||
|
||||
/**
|
||||
* 退回(B3):把流程退回上游已办节点重新办理。当前任务置 RETURNED、当前节点待办全部失效、
|
||||
* 目标节点重新激活建新任务;办件保持审批中(区别于拒绝=终态)。
|
||||
* v1 限定主线节点(BranchScope.keyOf(target)==null),分支内退回登记二期。
|
||||
*
|
||||
* @param targetNodeId 退回目标节点;空=最近一个已办节点(任务轨迹 handleTime 降序)
|
||||
*/
|
||||
@Transactional
|
||||
public void returnBack(Long schId, Long taskId, String targetNodeId, String opinion, Integer operatorId) {
|
||||
FlwTask task = requirePendingTask(schId, taskId, operatorId);
|
||||
FlwInstance instance = instanceRepository.findById(task.getInstanceId())
|
||||
.orElseThrow(() -> new BizException(ErrorCode.PARAM_ERROR, "办件不存在"));
|
||||
FlwDef def = defRepository.findById(instance.getDefId())
|
||||
.orElseThrow(() -> new BizException(ErrorCode.PARAM_ERROR, "流程定义缺失"));
|
||||
FlowDefModel model = FlowDefModel.parse(objectMapper, def.getContentJson());
|
||||
|
||||
String target = resolveReturnTarget(model, instance, task, targetNodeId);
|
||||
FlowDefModel.Node targetNode = model.node(target);
|
||||
if (!FlowDefModel.TYPE_APPROVAL.equals(targetNode.type)) {
|
||||
throw new BizException(ErrorCode.PARAM_ERROR, "退回目标必须是审批节点:" + target);
|
||||
}
|
||||
if (BranchScope.keyOf(model, target) != null) {
|
||||
throw new BizException(ErrorCode.PARAM_ERROR, "暂不支持退回到并行分支内的节点");
|
||||
}
|
||||
|
||||
LocalDateTime now = LocalDateTime.now();
|
||||
String fromLabel = task.getNodeName() == null ? task.getNodeId() : task.getNodeName();
|
||||
String targetLabel = targetNode.name == null ? target : targetNode.name;
|
||||
if (taskRepository.claimAndFinish(taskId, FlwEnums.TaskStatus.RETURNED.getCode(), now,
|
||||
"退回至 " + targetLabel + (opinion == null || opinion.isEmpty() ? "" : ":" + opinion), null) != 1) {
|
||||
throw new BizException(ErrorCode.PARAM_ERROR, "任务已被办理或已失效,请刷新");
|
||||
}
|
||||
taskRepository.invalidatePendingByNode(instance.getId(), task.getNodeId());
|
||||
flowEngine.returnTo(instance, def, task.getNodeId(), target);
|
||||
instanceRepository.save(instance);
|
||||
|
||||
Map<String, Object> data = new HashMap<>();
|
||||
data.put("fromNodeId", task.getNodeId());
|
||||
data.put("fromNodeName", fromLabel);
|
||||
data.put("targetNodeId", target);
|
||||
data.put("targetNodeName", targetLabel);
|
||||
if (opinion != null && !opinion.isEmpty()) {
|
||||
data.put("opinion", opinion);
|
||||
}
|
||||
data.put("targetUserIds", taskRepository
|
||||
.findByInstanceIdAndStatus(instance.getId(), FlwEnums.TaskStatus.PENDING.getCode()).stream()
|
||||
.filter(t -> target.equals(t.getNodeId()))
|
||||
.map(FlwTask::getAssigneeId)
|
||||
.collect(java.util.stream.Collectors.toList()));
|
||||
eventPublisher.publishEvent(FlowEvent.of(FlowEvent.Type.RETURNED, instance.getId(), taskId, data));
|
||||
}
|
||||
|
||||
/** 退回目标解析:显式指定须合法;缺省取最近一个已办节点 */
|
||||
private String resolveReturnTarget(FlowDefModel model, FlwInstance instance, FlwTask task,
|
||||
String targetNodeId) {
|
||||
if (targetNodeId != null && !targetNodeId.trim().isEmpty()) {
|
||||
FlowDefModel.Node node = model.node(targetNodeId.trim());
|
||||
if (node == null) {
|
||||
throw new BizException(ErrorCode.PARAM_ERROR, "退回目标节点不存在:" + targetNodeId);
|
||||
}
|
||||
if (node.id.equals(task.getNodeId())) {
|
||||
throw new BizException(ErrorCode.PARAM_ERROR, "不能退回当前节点");
|
||||
}
|
||||
return node.id;
|
||||
}
|
||||
return taskRepository.findByInstanceIdOrderByIdAsc(instance.getId()).stream()
|
||||
.filter(t -> FlwEnums.TaskStatus.APPROVED.getCode() == t.getStatus())
|
||||
.filter(t -> !task.getNodeId().equals(t.getNodeId()))
|
||||
.filter(t -> BranchScope.keyOf(model, t.getNodeId()) == null)
|
||||
.reduce((first, second) -> second)
|
||||
.map(FlwTask::getNodeId)
|
||||
.orElseThrow(() -> new BizException(ErrorCode.PARAM_ERROR,
|
||||
"无可退回的上游节点,请使用拒绝"));
|
||||
}
|
||||
|
||||
/**
|
||||
* 移交(B4):办理权转给他人。当前任务置 TRANSFERRED 留痕,同节点新建一条待办给目标人
|
||||
* (deadline 沿用原任务);推进监听器因新待办 pending>0 自然不放行,无需改推进逻辑。
|
||||
* 目标人存在性由 AssigneeResolver 严格校验(查不到必须拒绝,否则产生僵尸待办)。
|
||||
*/
|
||||
@Transactional
|
||||
public void transfer(Long schId, Long taskId, Integer toUserId, String reason, Integer operatorId) {
|
||||
if (toUserId == null) {
|
||||
throw new BizException(ErrorCode.PARAM_ERROR, "缺少移交对象");
|
||||
}
|
||||
FlwTask task = requirePendingTask(schId, taskId, operatorId);
|
||||
if (toUserId.equals(task.getAssigneeId())) {
|
||||
throw new BizException(ErrorCode.PARAM_ERROR, "不能移交给当前办理人本人");
|
||||
}
|
||||
FlwInstance instance = instanceRepository.findById(task.getInstanceId())
|
||||
.orElseThrow(() -> new BizException(ErrorCode.PARAM_ERROR, "办件不存在"));
|
||||
String toUserName = assigneeResolver.requireUserName(toUserId);
|
||||
|
||||
LocalDateTime now = LocalDateTime.now();
|
||||
String fromLabel = task.getNodeName() == null ? task.getNodeId() : task.getNodeName();
|
||||
if (taskRepository.claimAndFinish(taskId, FlwEnums.TaskStatus.TRANSFERRED.getCode(), now,
|
||||
"移交给 " + (toUserName.isEmpty() ? toUserId : toUserName)
|
||||
+ (reason == null || reason.isEmpty() ? "" : ":" + reason), null) != 1) {
|
||||
throw new BizException(ErrorCode.PARAM_ERROR, "任务已被办理或已失效,请刷新");
|
||||
}
|
||||
FlwTask newTask = new FlwTask();
|
||||
newTask.setInstanceId(task.getInstanceId());
|
||||
newTask.setSchId(task.getSchId());
|
||||
newTask.setNodeId(task.getNodeId());
|
||||
newTask.setNodeName(task.getNodeName());
|
||||
newTask.setAssigneeId(toUserId);
|
||||
newTask.setAssigneeName(toUserName);
|
||||
newTask.setStatus(FlwEnums.TaskStatus.PENDING.getCode());
|
||||
newTask.setArriveTime(now);
|
||||
newTask.setDeadline(task.getDeadline());
|
||||
newTask.setUrgeCount(0);
|
||||
taskRepository.save(newTask);
|
||||
|
||||
Map<String, Object> data = new HashMap<>();
|
||||
data.put("fromNodeId", task.getNodeId());
|
||||
data.put("fromNodeName", fromLabel);
|
||||
data.put("fromUserId", task.getAssigneeId());
|
||||
data.put("fromUserName", task.getAssigneeName());
|
||||
data.put("toUserId", toUserId);
|
||||
data.put("toUserName", toUserName);
|
||||
data.put("taskId", newTask.getId());
|
||||
if (reason != null && !reason.isEmpty()) {
|
||||
data.put("opinion", reason);
|
||||
}
|
||||
eventPublisher.publishEvent(FlowEvent.of(FlowEvent.Type.TRANSFERRED,
|
||||
instance.getId(), newTask.getId(), data));
|
||||
}
|
||||
|
||||
/** 待办任务守卫:存在、本校、本人办理、仍待办 */
|
||||
private FlwTask requirePendingTask(Long schId, Long taskId, Integer operatorId) {
|
||||
FlwTask task = taskRepository.findById(taskId)
|
||||
.orElseThrow(() -> new BizException(ErrorCode.PARAM_ERROR, "任务不存在:" + taskId));
|
||||
if (!task.getSchId().equals(schId)) {
|
||||
throw new BizException(ErrorCode.FORBIDDEN, "任务不属于该学校");
|
||||
}
|
||||
if (!task.getAssigneeId().equals(operatorId)) {
|
||||
throw new BizException(ErrorCode.FORBIDDEN, "仅任务办理人本人可操作");
|
||||
}
|
||||
if (task.getStatus() != FlwEnums.TaskStatus.PENDING.getCode()) {
|
||||
throw new BizException(ErrorCode.PARAM_ERROR, "仅待办任务可操作");
|
||||
}
|
||||
return task;
|
||||
}
|
||||
|
||||
/** 手工催办(提交人/管理员):urge_count+1 + URGE 事件(UserMsg 通道) */
|
||||
@Transactional
|
||||
public void urge(Long schId, Long taskId, int maxTimes) {
|
||||
|
||||
@@ -15,7 +15,7 @@ import com.yunhai.flowcenter.repository.FlwTaskRepository;
|
||||
import com.yunhai.platform.apistd.BizException;
|
||||
import com.yunhai.platform.utils.exception.ErrorCode;
|
||||
import com.yunhai.platform.utils.model.BoaUser;
|
||||
import com.yunhai.platform.openapi.client.OpenApiUserClient;
|
||||
import com.yunhai.platform.openapi.base.OpenApiUserClient;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.beans.factory.annotation.Value;
|
||||
|
||||
Reference in New Issue
Block a user