- 多出口: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:
@@ -1,6 +1,6 @@
|
||||
package com.yunhai.flowcenter.controller.api;
|
||||
|
||||
import com.yunhai.platform.openapi.client.YschBaseOpenApi;
|
||||
import com.yunhai.platform.openapi.base.YschBaseOpenApi;
|
||||
import com.yunhai.platform.utils.model.BoaModule;
|
||||
import com.yunhai.platform.utils.model.BoaUser;
|
||||
import org.springframework.web.bind.annotation.GetMapping;
|
||||
|
||||
@@ -81,12 +81,13 @@ public class FlowUserController {
|
||||
return queryService.detail(schId, instance.getId(), user.getId());
|
||||
}
|
||||
|
||||
/** 同意(opinion 可选)——一切审批入口的收敛点 */
|
||||
/** 同意(opinion 可选;多出口节点必须带 edgeName=所选出口名,单出口可省)——一切审批入口的收敛点 */
|
||||
@PostMapping("/task/{id}/approve")
|
||||
public void approve(BoaUser user, @PathVariable("id") Long id,
|
||||
@RequestBody(required = false) Map<String, String> body) {
|
||||
String opinion = body == null ? null : body.get("opinion");
|
||||
taskService.handle(requireSchId(user), id, true, opinion, user.getId());
|
||||
String edgeName = body == null ? null : body.get("edgeName");
|
||||
taskService.handle(requireSchId(user), id, true, opinion, user.getId(), edgeName);
|
||||
}
|
||||
|
||||
/** 拒绝(opinion 必填) */
|
||||
@@ -97,6 +98,26 @@ public class FlowUserController {
|
||||
taskService.handle(requireSchId(user), id, false, opinion, user.getId());
|
||||
}
|
||||
|
||||
/** 退回(targetNodeId 可选=缺省回最近已办节点;opinion 可选) */
|
||||
@PostMapping("/task/{id}/returnBack")
|
||||
public void returnBack(BoaUser user, @PathVariable("id") Long id,
|
||||
@RequestBody(required = false) Map<String, String> body) {
|
||||
String targetNodeId = body == null ? null : body.get("targetNodeId");
|
||||
String opinion = body == null ? null : body.get("opinion");
|
||||
taskService.returnBack(requireSchId(user), id, targetNodeId, opinion, user.getId());
|
||||
}
|
||||
|
||||
/** 移交(toUserId 必填;reason 可选) */
|
||||
@PostMapping("/task/{id}/transfer")
|
||||
public void transfer(BoaUser user, @PathVariable("id") Long id,
|
||||
@RequestBody Map<String, Object> body) {
|
||||
Integer toUserId = body.get("toUserId") == null ? null
|
||||
: Integer.valueOf(String.valueOf(body.get("toUserId")));
|
||||
Object reason = body.get("reason");
|
||||
taskService.transfer(requireSchId(user), id, toUserId,
|
||||
reason == null ? null : String.valueOf(reason), user.getId());
|
||||
}
|
||||
|
||||
/** 撤回(提交人;仅尚无任务被办理时) */
|
||||
@PostMapping("/instance/{id}/withdraw")
|
||||
public void withdraw(BoaUser user, @PathVariable("id") Long id) {
|
||||
|
||||
@@ -3,7 +3,7 @@ package com.yunhai.flowcenter.engine;
|
||||
import com.yunhai.flowcenter.entity.FlwAssigneeMap;
|
||||
import com.yunhai.flowcenter.repository.FlwAssigneeMapRepository;
|
||||
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;
|
||||
@@ -61,4 +61,26 @@ public class AssigneeResolver {
|
||||
return "";
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 移交目标校验(严格口径,与 {@link #fetchName} 的降级容错相反):
|
||||
* 查不到就必须拒绝——否则会建出一条谁在待办里都看不到的僵尸任务。
|
||||
* UserBase 不可达与"人不存在"分别报错,便于排查。
|
||||
*/
|
||||
public String requireUserName(Integer userId) {
|
||||
if (userId == null) {
|
||||
throw new BizException(ErrorCode.PARAM_ERROR, "缺少移交对象");
|
||||
}
|
||||
BoaUser user;
|
||||
try {
|
||||
user = userClient.getUserById(String.valueOf(userId));
|
||||
} catch (Exception e) {
|
||||
log.warn("[flow-center] 移交对象校验失败(UserBase 不可达)userId={}:{}", userId, e.getMessage());
|
||||
throw new BizException(ErrorCode.PARAM_ERROR, "移交对象校验失败,请稍后重试:" + userId);
|
||||
}
|
||||
if (user == null) {
|
||||
throw new BizException(ErrorCode.PARAM_ERROR, "移交对象不存在:" + userId);
|
||||
}
|
||||
return user.getName() == null ? "" : user.getName();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,57 @@
|
||||
package com.yunhai.flowcenter.engine;
|
||||
|
||||
import java.util.ArrayDeque;
|
||||
import java.util.Deque;
|
||||
import java.util.HashSet;
|
||||
import java.util.Set;
|
||||
|
||||
/**
|
||||
* 分支归属判定:节点属于哪个并行分支(branchKey=PARALLEL_SPLIT 出边目标节点 id),
|
||||
* 主线节点与汇聚点之后属 null。运行时用于推导分支上下文({@link FlowEngine}),
|
||||
* 发布期用于拒绝跨分支连线({@link DefValidator})——两处必须同口径,故提取为静态工具。
|
||||
*/
|
||||
public final class BranchScope {
|
||||
|
||||
private BranchScope() {
|
||||
}
|
||||
|
||||
/**
|
||||
* 沿入边反向 BFS 走到最近的上游 PARALLEL_SPLIT,返回该 split 指向本链的 outEdge.to(即分支键);
|
||||
* 遇 START/PARALLEL_JOIN 边界即停(join 之后属汇聚链,分支键已消耗,返回 null)。
|
||||
*/
|
||||
public static String keyOf(FlowDefModel model, String nodeId) {
|
||||
if (model == null || nodeId == null) {
|
||||
return null;
|
||||
}
|
||||
Set<String> visited = new HashSet<>();
|
||||
Deque<String> queue = new ArrayDeque<>();
|
||||
queue.add(nodeId);
|
||||
visited.add(nodeId);
|
||||
while (!queue.isEmpty()) {
|
||||
String cur = queue.poll();
|
||||
for (FlowDefModel.Edge e : model.inEdges(cur)) {
|
||||
FlowDefModel.Node from = model.node(e.from);
|
||||
if (from == null) {
|
||||
continue;
|
||||
}
|
||||
if (FlowDefModel.TYPE_PARALLEL_SPLIT.equals(from.type)) {
|
||||
return e.to;
|
||||
}
|
||||
if (FlowDefModel.TYPE_START.equals(from.type)
|
||||
|| FlowDefModel.TYPE_PARALLEL_JOIN.equals(from.type)
|
||||
|| !visited.add(e.from)) {
|
||||
continue;
|
||||
}
|
||||
queue.add(e.from);
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
/** 两节点是否同属一个分支上下文(含"都属主线"=都属于 null) */
|
||||
public static boolean sameScope(FlowDefModel model, String a, String b) {
|
||||
String ka = keyOf(model, a);
|
||||
String kb = keyOf(model, b);
|
||||
return ka == null ? kb == null : ka.equals(kb);
|
||||
}
|
||||
}
|
||||
@@ -87,12 +87,19 @@ public final class DefValidator {
|
||||
for (FlowDefModel.Node n : model.nodes) {
|
||||
int out = model.outEdges(n.id).size();
|
||||
int in = model.inEdges(n.id).size();
|
||||
if (FlowDefModel.TYPE_START.equals(n.type) || FlowDefModel.TYPE_APPROVAL.equals(n.type)
|
||||
if (FlowDefModel.TYPE_START.equals(n.type)
|
||||
|| FlowDefModel.TYPE_PARALLEL_JOIN.equals(n.type)) {
|
||||
if (out != 1) {
|
||||
problems.add(nodeLabel(n) + " 出边必须恰为 1 条(当前 " + out + ")");
|
||||
}
|
||||
}
|
||||
if (FlowDefModel.TYPE_APPROVAL.equals(n.type)) {
|
||||
if (out < 1) {
|
||||
problems.add(nodeLabel(n) + " 出边至少 1 条");
|
||||
} else if (out > 1) {
|
||||
validateOutlets(model, n, problems);
|
||||
}
|
||||
}
|
||||
if (FlowDefModel.TYPE_PARALLEL_SPLIT.equals(n.type) && out < 1) {
|
||||
problems.add(nodeLabel(n) + " 出边至少 1 条");
|
||||
}
|
||||
@@ -134,6 +141,29 @@ public final class DefValidator {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 多出口节点校验(办理人按出口名选路):每个出口必须命名且同节点内唯一(名即办理按钮文案);
|
||||
* 出口目标必须与本节点同属一个分支上下文——跨分支连线依赖运行时的跨分支移动,一期未实现,
|
||||
* 在发布期明确拒绝而非运行时静默走错边。
|
||||
*/
|
||||
private static void validateOutlets(FlowDefModel model, FlowDefModel.Node n, List<String> problems) {
|
||||
Set<String> names = new HashSet<>();
|
||||
for (FlowDefModel.Edge e : model.outEdges(n.id)) {
|
||||
if (e.name == null || e.name.trim().isEmpty()) {
|
||||
problems.add(nodeLabel(n) + " 有 " + model.outEdges(n.id).size()
|
||||
+ " 条出边,每个出口必须命名(名称即办理按钮文案)");
|
||||
continue;
|
||||
}
|
||||
if (!names.add(e.name.trim())) {
|
||||
problems.add(nodeLabel(n) + " 出口名称重复:" + e.name + "(同一节点的出口名必须唯一)");
|
||||
}
|
||||
if (!BranchScope.sameScope(model, n.id, e.to)) {
|
||||
problems.add(nodeLabel(n) + " 的出口「" + e.name + "」指向另一并行分支内的节点("
|
||||
+ e.to + "):暂不支持跨分支连线,可改用并行分支编排");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static void validateApproval(FlowDefModel.Node n, Set<String> enabledDeptKeys, List<String> problems) {
|
||||
FlowDefModel.Assignee a = n.assignee;
|
||||
if (a == null || a.type == null) {
|
||||
|
||||
@@ -2,6 +2,7 @@ package com.yunhai.flowcenter.engine;
|
||||
|
||||
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.FlwDefRepository;
|
||||
import com.yunhai.flowcenter.repository.FlwInstanceRepository;
|
||||
@@ -27,9 +28,15 @@ public class FlowAdvanceService {
|
||||
private final FlwTaskRepository taskRepository;
|
||||
private final FlowEngine flowEngine;
|
||||
|
||||
/** @return 是否实际推进了 */
|
||||
/**
|
||||
* @param nodeId 待放行节点
|
||||
* @param edgeName 多出口节点所选出口(办理事件携带);可空——兜底从该节点
|
||||
* 最近一条已同意任务的 edge_name 列重放(推进在 AFTER_COMMIT 新事务重查,
|
||||
* 事件传参只是快路径,落库的值才是权威)
|
||||
* @return 是否实际推进了
|
||||
*/
|
||||
@Transactional(propagation = Propagation.REQUIRES_NEW)
|
||||
public boolean tryAdvance(Long instanceId, String nodeId) {
|
||||
public boolean tryAdvance(Long instanceId, String nodeId, String edgeName) {
|
||||
FlwInstance instance = instanceRepository.findById(instanceId).orElse(null);
|
||||
if (instance == null
|
||||
|| instance.getStatus() != FlwEnums.InstanceStatus.APPROVING.getCode()) {
|
||||
@@ -55,7 +62,17 @@ public class FlowAdvanceService {
|
||||
instanceId, instance.getDefId());
|
||||
return false;
|
||||
}
|
||||
flowEngine.continueAdvance(instance, def, nodeId);
|
||||
String chosen = edgeName;
|
||||
if (chosen == null) {
|
||||
chosen = taskRepository.findByInstanceIdAndStatus(instanceId,
|
||||
FlwEnums.TaskStatus.APPROVED.getCode()).stream()
|
||||
.filter(t -> nodeId.equals(t.getNodeId()) && t.getEdgeName() != null)
|
||||
.max(java.util.Comparator.comparing(
|
||||
FlwTask::getHandleTime, java.util.Comparator.nullsFirst(java.util.Comparator.naturalOrder())))
|
||||
.map(FlwTask::getEdgeName)
|
||||
.orElse(null);
|
||||
}
|
||||
flowEngine.continueAdvance(instance, def, nodeId, chosen);
|
||||
instanceRepository.save(instance);
|
||||
return true;
|
||||
}
|
||||
|
||||
@@ -6,6 +6,7 @@ import com.yunhai.platform.apistd.BizException;
|
||||
import com.yunhai.platform.utils.exception.ErrorCode;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
/**
|
||||
* 流程定义 JSON 模型(概览 §5.1 规范,一期 5 节点)。
|
||||
@@ -38,6 +39,11 @@ public class FlowDefModel {
|
||||
public List<Node> nodes;
|
||||
public List<Edge> edges;
|
||||
public Settings settings;
|
||||
/**
|
||||
* 画布坐标(nodeId → 左上角),仅前端设计器/查看器复用;引擎不消费。
|
||||
* 缺失时前端自行做自动布局,故可选、可空。
|
||||
*/
|
||||
public Map<String, Layout> layout;
|
||||
|
||||
@JsonIgnoreProperties(ignoreUnknown = true)
|
||||
public static class Node {
|
||||
@@ -65,7 +71,20 @@ public class FlowDefModel {
|
||||
public static class Edge {
|
||||
public String from;
|
||||
public String to;
|
||||
/**
|
||||
* 出边名(按钮文案)。单出口可空;多出口节点必填且同节点内唯一,
|
||||
* 办理人在办理页选它决定走哪条边(见 {@link #edgeByName})。
|
||||
*/
|
||||
public String name;
|
||||
/** 出口条件(二期条件自动路由预留;一期只校验透传,引擎不消费) */
|
||||
public String condition;
|
||||
}
|
||||
|
||||
/** 画布坐标(前端设计器专用) */
|
||||
@JsonIgnoreProperties(ignoreUnknown = true)
|
||||
public static class Layout {
|
||||
public Integer x;
|
||||
public Integer y;
|
||||
}
|
||||
|
||||
@JsonIgnoreProperties(ignoreUnknown = true)
|
||||
@@ -135,4 +154,17 @@ public class FlowDefModel {
|
||||
}
|
||||
return in;
|
||||
}
|
||||
|
||||
/** 按出边名取边(多出口节点路由用);未命中返回 null */
|
||||
public Edge edgeByName(String fromNodeId, String name) {
|
||||
if (name == null) {
|
||||
return null;
|
||||
}
|
||||
for (Edge e : outEdges(fromNodeId)) {
|
||||
if (name.equals(e.name)) {
|
||||
return e;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -14,13 +14,9 @@ import org.springframework.context.ApplicationEventPublisher;
|
||||
import org.springframework.stereotype.Service;
|
||||
|
||||
import java.time.LocalDateTime;
|
||||
import java.util.ArrayDeque;
|
||||
import java.util.Deque;
|
||||
import java.util.HashMap;
|
||||
import java.util.HashSet;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Set;
|
||||
|
||||
/**
|
||||
* 流程引擎推进核心(概览 §5.2 激活规则):由调用方事务驱动——
|
||||
@@ -50,19 +46,30 @@ public class FlowEngine {
|
||||
}
|
||||
|
||||
/**
|
||||
* 某节点全部同意后的放行推进:移出活跃节点集并激活唯一出边目标。
|
||||
* 分支上下文由 {@link #branchKeyOf} 从图上回溯推导(分支内串行链的中间节点无任务级分支标记)。
|
||||
* 某节点全部同意后的放行推进:移出活跃节点集并激活出边目标。
|
||||
* 单出边走唯一出边(旧定义逐字节兼容);多出边按办理人选的出口名(edgeName,即办理按钮文案)
|
||||
* 选路——未命中时记录错误并保持节点在途,绝不静默走第一条边。
|
||||
* 分支上下文由 {@link BranchScope#keyOf} 从图上回溯推导(分支内串行链的中间节点无任务级分支标记)。
|
||||
*/
|
||||
public void continueAdvance(FlwInstance instance, FlwDef def, String fromNodeId) {
|
||||
public void continueAdvance(FlwInstance instance, FlwDef def, String fromNodeId, String edgeName) {
|
||||
FlowDefModel model = parseModel(def);
|
||||
FlowDefModel.Node node = model.nodeById(fromNodeId);
|
||||
List<FlowDefModel.Edge> outs = model.outEdges(fromNodeId);
|
||||
if (outs.isEmpty()) {
|
||||
return;
|
||||
}
|
||||
FlowDefModel.Edge chosen = outs.get(0);
|
||||
if (outs.size() > 1) {
|
||||
chosen = model.edgeByName(fromNodeId, edgeName);
|
||||
if (chosen == null) {
|
||||
log.error("[flow-center] 多出口节点未命中出口,推进中止(节点保持在途):instance={} node={} edgeName={}",
|
||||
instance.getId(), fromNodeId, edgeName);
|
||||
return;
|
||||
}
|
||||
}
|
||||
removeCurrentNode(instance, fromNodeId);
|
||||
String branchKey = branchKeyOf(model, fromNodeId);
|
||||
String next = outs.get(0).to;
|
||||
String branchKey = BranchScope.keyOf(model, fromNodeId);
|
||||
String next = chosen.to;
|
||||
FlowDefModel.Node nextNode = model.nodeById(next);
|
||||
if (FlowDefModel.TYPE_PARALLEL_JOIN.equals(nextNode.type)) {
|
||||
// 会签合并守门:标记本分支到达、全部到达才放行(直通无分支键时 JOIN 直通放行)
|
||||
@@ -73,34 +80,15 @@ public class FlowEngine {
|
||||
}
|
||||
|
||||
/**
|
||||
* 回溯 fromNodeId 所在分支的 branchKey(= PARALLEL_SPLIT 出边目标节点 id):
|
||||
* 沿入边反向走到 split,split→N 边的 N 即分支键;遇 START/PARALLEL_JOIN 边界即停
|
||||
* (join 之后属汇聚链,branchKey 已消耗,返回 null)。
|
||||
* 退回目标激活(FlowTaskService.returnBack 调用):从 currentNodes 摘除来源节点,
|
||||
* 重新激活目标审批节点建新任务。沿用统一激活入口,split 幂等化后重入并行段不撞分支唯一约束。
|
||||
*/
|
||||
private String branchKeyOf(FlowDefModel model, String fromNodeId) {
|
||||
Set<String> visited = new HashSet<>();
|
||||
Deque<String> queue = new ArrayDeque<>();
|
||||
queue.add(fromNodeId);
|
||||
visited.add(fromNodeId);
|
||||
while (!queue.isEmpty()) {
|
||||
String cur = queue.poll();
|
||||
for (FlowDefModel.Edge e : model.inEdges(cur)) {
|
||||
FlowDefModel.Node from = model.node(e.from);
|
||||
if (from == null) {
|
||||
continue;
|
||||
}
|
||||
if (FlowDefModel.TYPE_PARALLEL_SPLIT.equals(from.type)) {
|
||||
return e.to;
|
||||
}
|
||||
if (FlowDefModel.TYPE_START.equals(from.type)
|
||||
|| FlowDefModel.TYPE_PARALLEL_JOIN.equals(from.type)
|
||||
|| !visited.add(e.from)) {
|
||||
continue;
|
||||
}
|
||||
queue.add(e.from);
|
||||
}
|
||||
}
|
||||
return null;
|
||||
public void returnTo(FlwInstance instance, FlwDef def, String fromNodeId, String targetNodeId) {
|
||||
FlowDefModel model = parseModel(def);
|
||||
removeCurrentNode(instance, fromNodeId);
|
||||
FlowDefModel.Node target = model.nodeById(targetNodeId);
|
||||
String branchKey = BranchScope.keyOf(model, targetNodeId);
|
||||
advance(instance, model, target.id, branchKey);
|
||||
}
|
||||
|
||||
/** 统一激活入口:含 JOIN 的到达标记与放行判断 */
|
||||
@@ -148,10 +136,17 @@ public class FlowEngine {
|
||||
break;
|
||||
case FlowDefModel.TYPE_PARALLEL_SPLIT: {
|
||||
for (FlowDefModel.Edge edge : model.outEdges(node.id)) {
|
||||
FlwBranch branch = new FlwBranch();
|
||||
// 幂等 upsert:退回重入并行段时分支行已存在(uk instance+branchKey),重置进行中即可
|
||||
FlwBranch branch = branchRepository
|
||||
.findByInstanceIdAndBranchKey(instance.getId(), edge.to).orElse(null);
|
||||
if (branch == null) {
|
||||
branch = new FlwBranch();
|
||||
branch.setInstanceId(instance.getId());
|
||||
branch.setBranchKey(edge.to);
|
||||
branch.setStatus(FlwEnums.BranchStatus.RUNNING.getCode());
|
||||
} else {
|
||||
branch.setStatus(FlwEnums.BranchStatus.RUNNING.getCode());
|
||||
}
|
||||
branchRepository.save(branch);
|
||||
advance(instance, model, edge.to, edge.to);
|
||||
}
|
||||
|
||||
@@ -28,6 +28,10 @@ public class FlowEvent {
|
||||
REJECTED("flow.rejected"),
|
||||
/** 提交人撤回,instance=40 */
|
||||
WITHDRAWN("flow.withdrawn"),
|
||||
/** 办理人退回上游节点(办件仍在途,instance 仍=10) */
|
||||
RETURNED("flow.returned"),
|
||||
/** 办理人移交办理权给他人(任务换人,办件仍在途) */
|
||||
TRANSFERRED("flow.transferred"),
|
||||
/** 催办(仅 UserMsg 通道,非回调事件枚举) */
|
||||
URGE("flow.urge");
|
||||
|
||||
|
||||
@@ -30,9 +30,10 @@ public class FlowNodeAdvanceListener {
|
||||
if (nodeId == null) {
|
||||
return;
|
||||
}
|
||||
String edgeName = (String) event.getData().get("edgeName");
|
||||
for (int attempt = 1; attempt <= 3; attempt++) {
|
||||
try {
|
||||
advanceService.tryAdvance(event.getInstanceId(), nodeId);
|
||||
advanceService.tryAdvance(event.getInstanceId(), nodeId, edgeName);
|
||||
return;
|
||||
} catch (ObjectOptimisticLockingFailureException e) {
|
||||
log.warn("[flow-center] 推进冲突重试 {}/3:instance={} node={}",
|
||||
|
||||
@@ -47,7 +47,7 @@ public class FlwTask extends BaseEntity {
|
||||
@Column(name = "assignee_name", length = 128)
|
||||
private String assigneeName;
|
||||
|
||||
/** 0 待办 / 1 同意 / 2 拒绝 / 3 失效 */
|
||||
/** 0 待办 / 1 同意 / 2 拒绝 / 3 失效 / 4 已退回 / 5 已移交 */
|
||||
@Column(name = "status", nullable = false)
|
||||
private Integer status = FlwEnums.TaskStatus.PENDING.getCode();
|
||||
|
||||
@@ -55,6 +55,13 @@ public class FlwTask extends BaseEntity {
|
||||
@Column(name = "opinion", length = 500)
|
||||
private String opinion;
|
||||
|
||||
/**
|
||||
* 多出口节点办理时选中的出口名(对应 def 边 name);单出口节点恒为 null。
|
||||
* 节点放行推进时按它决定走哪条边。
|
||||
*/
|
||||
@Column(name = "edge_name", length = 64)
|
||||
private String edgeName;
|
||||
|
||||
@Column(name = "arrive_time", nullable = false)
|
||||
private LocalDateTime arriveTime;
|
||||
|
||||
|
||||
@@ -23,9 +23,13 @@ public final class FlwEnums {
|
||||
}
|
||||
}
|
||||
|
||||
/** 任务状态:0 待办 / 1 同意 / 2 拒绝 / 3 失效(拒绝/撤回/重启重提时的清理目标) */
|
||||
/**
|
||||
* 任务状态:0 待办 / 1 同意 / 2 拒绝 / 3 失效(拒绝/撤回/重启重提时的清理目标)
|
||||
* / 4 已退回(办理人把流程退回上游节点)/ 5 已移交(办理人把办理权转给他人)。
|
||||
* 4/5 都算"已被办理":既进我的已办,也阻止提交人撤回。
|
||||
*/
|
||||
public enum TaskStatus {
|
||||
PENDING(0), APPROVED(1), REJECTED(2), INVALIDATED(3);
|
||||
PENDING(0), APPROVED(1), REJECTED(2), INVALIDATED(3), RETURNED(4), TRANSFERRED(5);
|
||||
|
||||
private final int code;
|
||||
|
||||
@@ -36,6 +40,12 @@ public final class FlwEnums {
|
||||
public int getCode() {
|
||||
return code;
|
||||
}
|
||||
|
||||
/** "已被办理"状态集(本人已办列表、撤回守卫共用口径),勿在各处硬编码字面量 */
|
||||
public static java.util.List<Integer> handledCodes() {
|
||||
return java.util.Arrays.asList(APPROVED.getCode(), REJECTED.getCode(),
|
||||
RETURNED.getCode(), TRANSFERRED.getCode());
|
||||
}
|
||||
}
|
||||
|
||||
/** 并行分支状态:0 进行中 / 1 已到达汇聚点 / 2 已失效 */
|
||||
|
||||
@@ -76,6 +76,15 @@ public final class FlowModels {
|
||||
/** 当前用户可否办理(存在本人的待办任务) */
|
||||
private boolean myPendingTask;
|
||||
private List<Long> myPendingTaskIds;
|
||||
/**
|
||||
* 本人待办所在节点的出口列表(多出口办理按钮,[{name,toNodeId,toNodeName}]);
|
||||
* 单出口/无待办为空数组——前端据此决定是否显示去向选择器。
|
||||
*/
|
||||
private List<Map<String, Object>> outlets;
|
||||
/** 可退回目标(主线已办审批节点去重,排除当前节点)[{nodeId,nodeName}] */
|
||||
private List<Map<String, Object>> returnTargets;
|
||||
/** 本人待办可否移交(恒=有本人待办,展示用) */
|
||||
private boolean canTransfer;
|
||||
}
|
||||
|
||||
/** 流程示意图出参(用户面 /diagram 与管理面 /admin/def/diagram 共用,方案 §3.1) */
|
||||
@@ -91,9 +100,11 @@ public final class FlowModels {
|
||||
private String mode;
|
||||
private List<NodeVo> nodes;
|
||||
private List<EdgeVo> edges;
|
||||
/** 画布坐标(nodeId→{x,y},设计器保存时透传;空=查看器自行自动布局) */
|
||||
private Map<String, Map<String, Integer>> layout;
|
||||
}
|
||||
|
||||
/** 示意图节点(label/subLabel 已按视图解析好,前端只画不解析) */
|
||||
/** 示意图节点(label/subLabel 已按视图解析好,前端只画不解析;state 仅 instance 口径有值) */
|
||||
@Data
|
||||
public static class NodeVo {
|
||||
private String id;
|
||||
@@ -104,12 +115,18 @@ public final class FlowModels {
|
||||
private String assigneeType;
|
||||
private String countersign;
|
||||
private Integer deadlineHours;
|
||||
/**
|
||||
* 运行时状态(仅 instanceId 口径):done 已办过 / active 当前在办 / pending 未到 /
|
||||
* rejected 被拒绝 / 其他为 null。flowGuid 口径恒 null。
|
||||
*/
|
||||
private String state;
|
||||
}
|
||||
|
||||
/** 示意图连边 */
|
||||
/** 示意图连边(name=出口文案,多出口节点显示为连线标签) */
|
||||
@Data
|
||||
public static class EdgeVo {
|
||||
private String from;
|
||||
private String to;
|
||||
private String name;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,9 +1,12 @@
|
||||
package com.yunhai.flowcenter.notify;
|
||||
|
||||
import com.yunhai.platform.apistd.BizException;
|
||||
import com.yunhai.platform.openapi.core.PlatformService;
|
||||
import com.yunhai.platform.openapi.core.YschOpenCall;
|
||||
import com.yunhai.platform.openapi.msg.YschMsgOpenApi;
|
||||
import com.yunhai.platform.openapi.msg.model.SendMsgRequest;
|
||||
import com.yunhai.platform.openapi.msg.model.TodoCountRequest;
|
||||
import com.yunhai.platform.utils.http.IDHttp;
|
||||
import lombok.Data;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.beans.factory.annotation.Value;
|
||||
import org.springframework.stereotype.Service;
|
||||
@@ -11,30 +14,56 @@ import org.springframework.stereotype.Service;
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* UserMsg 站内信客户端(概览 §七 契约,M1 手工三件套路线的<b>直达版</b>):
|
||||
* 经共享 IDHttp(yh-openapi 装配的出站自动签名 RestTemplate,X-App-Key/X-Timestamp/X-Signature
|
||||
* = SignUtil.signRequest 请求级 canonical,与 svc-hall FlowCenterClient 同一套通道)直调
|
||||
* UserMsg /oapi/msg/**。前提:UserMsg 侧 trusted-apps 白名单收本应用 appKey(本地占位 100019);
|
||||
* SDK(yh-usermsg-spring-boot-starter)落地后整体切换为 SDK 调用,契约不变。
|
||||
* 发送方不阻断主业务:失败只记日志(对齐 PartyLearn 容错语义),由调用方在 AFTER_COMMIT 监听器里容错。
|
||||
* UserMsg 站内信客户端(概览 §七 契约):主路径走 SDK 类型化门面 {@link YschMsgOpenApi}
|
||||
* (服务目录 key={@code usermsg}:yh.call.targets > yh.open.services > 枚举兜底
|
||||
* base-host + /usermsg)——签名/信封/错误映射由 SDK 统一承载,本应用零专属地址配置
|
||||
* (《子应用配置优化方案》§4.2)。目录未显式配置时回落 legacy
|
||||
* {@code flow-center.usermsg.base-url}(ad-hoc,迁移期兼容)。<br><br>
|
||||
* 发送方不阻断主业务:失败只记日志(对齐 PartyLearn 容错语义),由调用方在 AFTER_COMMIT
|
||||
* 监听器里容错。{@code flow-center.usermsg.enabled=false} 时全部方法静默返回(UserMsg 未部署
|
||||
* 的部署形态;生产当前即此态,避免打 localhost 的空转告警)。
|
||||
*/
|
||||
@Slf4j
|
||||
@Service
|
||||
@RequiredArgsConstructor
|
||||
public class UserMsgClient {
|
||||
|
||||
/** 服务目录命名目标(UserMsg 已入 PlatformService 枚举:key=usermsg、ctx=/usermsg) */
|
||||
private static final String DIRECTORY_KEY = PlatformService.USERMSG.key();
|
||||
|
||||
private final YschOpenCall openCall;
|
||||
private final IDHttp http;
|
||||
private final boolean enabled;
|
||||
private final String legacyBaseUrl;
|
||||
private final boolean directoryResolved;
|
||||
|
||||
@Value("${flow-center.usermsg.enabled:false}")
|
||||
private boolean enabled;
|
||||
|
||||
@Value("${flow-center.usermsg.base-url:http://localhost:8080}")
|
||||
private String baseUrl;
|
||||
public UserMsgClient(YschOpenCall openCall,
|
||||
IDHttp http,
|
||||
@Value("${flow-center.usermsg.enabled:false}") boolean enabled,
|
||||
@Value("${flow-center.usermsg.base-url:}") String baseUrl) {
|
||||
this.openCall = openCall;
|
||||
this.http = http;
|
||||
this.enabled = enabled;
|
||||
this.legacyBaseUrl = baseUrl == null ? "" : trimTrailing(baseUrl.trim());
|
||||
this.directoryResolved = openCall != null && openCall.hasTarget(DIRECTORY_KEY);
|
||||
if (directoryResolved) {
|
||||
YschMsgOpenApi.setCall(openCall);
|
||||
}
|
||||
if (enabled && !directoryResolved && legacyBaseUrl.isEmpty()) {
|
||||
// enabled=true 表示确实要发;目录与 legacy 键都缺 → 构造期 fail-fast
|
||||
throw new IllegalStateException("UserMsg 调用地址未配置:请配置服务目录 yh.open.services."
|
||||
+ DIRECTORY_KEY + "(推荐,容器直连完整 URL)或回落键 flow-center.usermsg.base-url");
|
||||
}
|
||||
}
|
||||
|
||||
public boolean enabled() {
|
||||
return enabled;
|
||||
}
|
||||
|
||||
/** 是否走服务目录命名目标(配置自检用) */
|
||||
public boolean directoryResolved() {
|
||||
return directoryResolved;
|
||||
}
|
||||
|
||||
/** 发送站内信(content 必填;msgNo 幂等,重发安全) */
|
||||
public void sendUserMsg(Integer schId, List<Integer> userIds, Integer msgType, String bizType,
|
||||
String bizId, String title, String content, String jumpPath, String msgNo,
|
||||
@@ -57,7 +86,11 @@ public class UserMsgClient {
|
||||
body.setSchId(schId);
|
||||
body.setExpireDays(expireDays);
|
||||
try {
|
||||
http.post(trimTrailing(baseUrl) + "/oapi/msg/sendUserMsg", body, Object.class);
|
||||
if (directoryResolved) {
|
||||
YschMsgOpenApi.sendUserMsg(body);
|
||||
} else {
|
||||
http.post(legacyBaseUrl + "/oapi/msg/sendUserMsg", body, Object.class);
|
||||
}
|
||||
} catch (BizException e) {
|
||||
log.warn("[flow-center] UserMsg 发送被拒:msgNo={} msg={}", msgNo, e.getMessage());
|
||||
} catch (Exception e) {
|
||||
@@ -75,7 +108,11 @@ public class UserMsgClient {
|
||||
body.setCount(count);
|
||||
body.setSchId(schId);
|
||||
try {
|
||||
http.post(trimTrailing(baseUrl) + "/oapi/msg/todo/inc", body, Object.class);
|
||||
if (directoryResolved) {
|
||||
YschMsgOpenApi.todoInc(body);
|
||||
} else {
|
||||
http.post(legacyBaseUrl + "/oapi/msg/todo/inc", body, Object.class);
|
||||
}
|
||||
} catch (Exception e) {
|
||||
log.warn("[flow-center] UserMsg 角标增量失败(不阻断主业务):userIds={} count={} {}",
|
||||
userIds, count, e.toString());
|
||||
@@ -85,31 +122,4 @@ public class UserMsgClient {
|
||||
private static String trimTrailing(String url) {
|
||||
return url.endsWith("/") ? url.substring(0, url.length() - 1) : url;
|
||||
}
|
||||
|
||||
/** /oapi/msg/sendUserMsg 请求体(UserMsg SendMsgRequest 同构,字段逐字对齐) */
|
||||
@Data
|
||||
public static class SendMsgRequest {
|
||||
private List<Integer> userIds;
|
||||
private String title;
|
||||
/** 必填(UserMsg @NotBlank) */
|
||||
private String content;
|
||||
private String msgNo;
|
||||
private String bizType;
|
||||
private String bizId;
|
||||
/** 0系统通知/1业务提醒/2审批待办 */
|
||||
private Integer msgType;
|
||||
/** 0无跳转/1应用内路径/2外链 */
|
||||
private Integer jumpType;
|
||||
private String jumpPath;
|
||||
private Integer schId;
|
||||
private Integer expireDays;
|
||||
}
|
||||
|
||||
/** /oapi/msg/todo/inc 请求体(UserMsg TodoCountRequest 同构) */
|
||||
@Data
|
||||
public static class TodoCountRequest {
|
||||
private List<Integer> userIds;
|
||||
private Integer count;
|
||||
private Integer schId;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -40,6 +40,12 @@ public class UserMsgNotifier {
|
||||
case TASK_HANDLED:
|
||||
notifyTaskHandled(event);
|
||||
break;
|
||||
case RETURNED:
|
||||
notifyReturned(event);
|
||||
break;
|
||||
case TRANSFERRED:
|
||||
notifyTaskCreated(event);
|
||||
break;
|
||||
case COMPLETED:
|
||||
case REJECTED:
|
||||
case WITHDRAWN:
|
||||
@@ -80,6 +86,36 @@ public class UserMsgNotifier {
|
||||
Collections.singletonList(task.getAssigneeId()), -1);
|
||||
}
|
||||
|
||||
/**
|
||||
* 退回:通知被退回节点的全部新待办人(RETURNED 事件 data.targetNodeIds 由服务层带上——
|
||||
* 退回重建任务在事件发布前已提交,这里按节点查待办即可);todo 角标由 notifyTaskCreated
|
||||
* 通用链路在引擎重建任务时已发过,这里只补一条带退回意见的消息。
|
||||
*/
|
||||
private void notifyReturned(FlowEvent event) {
|
||||
FlwInstance instance = instanceOf(event.getInstanceId());
|
||||
if (instance == null || event.getData() == null) {
|
||||
return;
|
||||
}
|
||||
Object fromNode = event.getData().get("fromNodeName");
|
||||
Object opinion = event.getData().get("opinion");
|
||||
StringBuilder content = new StringBuilder("「").append(safe(instance.getBizTitle())).append("」")
|
||||
.append("被退回(").append(fromNode == null ? "" : fromNode).append(")");
|
||||
if (opinion != null && !String.valueOf(opinion).isEmpty()) {
|
||||
content.append(",意见:").append(opinion);
|
||||
}
|
||||
java.util.List<Integer> targets = event.getData().get("targetUserIds") == null
|
||||
? Collections.emptyList()
|
||||
: (java.util.List<Integer>) event.getData().get("targetUserIds");
|
||||
if (targets.isEmpty()) {
|
||||
return;
|
||||
}
|
||||
userMsgClient.sendUserMsg(instance.getSchId().intValue(), targets, 1,
|
||||
"flow.returned", String.valueOf(instance.getId()),
|
||||
"办件被退回:" + safe(instance.getBizTitle()), content.toString(),
|
||||
"/instance/" + instance.getId(),
|
||||
"returned:" + instance.getId() + ":" + instance.getVersion(), 30);
|
||||
}
|
||||
|
||||
private void notifyResult(FlowEvent event) {
|
||||
FlwInstance instance = instanceOf(event.getInstanceId());
|
||||
if (instance == null) {
|
||||
|
||||
@@ -21,15 +21,33 @@ public interface FlwTaskRepository extends JpaRepository<FlwTask, Long> {
|
||||
|
||||
/** 任务办理抢占(概览 §5.4):affected=1 才获得推进权,防双击/并发重复推进 */
|
||||
@Modifying
|
||||
@Query("update FlwTask t set t.status = :target, t.handleTime = :handleTime, t.opinion = :opinion "
|
||||
+ "where t.id = :id and t.status = 0")
|
||||
@Query("update FlwTask t set t.status = :target, t.handleTime = :handleTime, t.opinion = :opinion, "
|
||||
+ "t.edgeName = :edgeName where t.id = :id and t.status = 0")
|
||||
int claimAndFinish(@Param("id") Long id, @Param("target") Integer target,
|
||||
@Param("handleTime") LocalDateTime handleTime, @Param("opinion") String opinion);
|
||||
@Param("handleTime") LocalDateTime handleTime, @Param("opinion") String opinion,
|
||||
@Param("edgeName") String edgeName);
|
||||
|
||||
@Modifying
|
||||
@Query("update FlwTask t set t.status = 3 where t.instanceId = :instanceId and t.status = 0")
|
||||
int invalidatePendingByInstance(@Param("instanceId") Long instanceId);
|
||||
|
||||
/**
|
||||
* 失效指定节点的待办(退回专用):不能按办件整体失效——并行兄弟分支的待办必须保留。
|
||||
*/
|
||||
@Modifying
|
||||
@Query("update FlwTask t set t.status = 3 where t.instanceId = :instanceId and t.status = 0 "
|
||||
+ "and t.nodeId = :nodeId")
|
||||
int invalidatePendingByNode(@Param("instanceId") Long instanceId, @Param("nodeId") String nodeId);
|
||||
|
||||
/**
|
||||
* 失效同节点除指定任务外的待办(或签 ANY 专用):任一人同意后兄弟待办作废。
|
||||
*/
|
||||
@Modifying
|
||||
@Query("update FlwTask t set t.status = 3 where t.instanceId = :instanceId and t.status = 0 "
|
||||
+ "and t.nodeId = :nodeId and t.id <> :exceptTaskId")
|
||||
int invalidatePendingByNodeExcept(@Param("instanceId") Long instanceId, @Param("nodeId") String nodeId,
|
||||
@Param("exceptTaskId") Long exceptTaskId);
|
||||
|
||||
@Modifying
|
||||
@Query("update FlwTask t set t.status = 3 where t.instanceId = :instanceId and t.status = 0 and t.id <> :exceptTaskId")
|
||||
int invalidatePendingExcept(@Param("instanceId") Long instanceId, @Param("exceptTaskId") Long exceptTaskId);
|
||||
@@ -39,9 +57,9 @@ public interface FlwTaskRepository extends JpaRepository<FlwTask, Long> {
|
||||
+ "order by t.arriveTime asc, t.id asc")
|
||||
Page<FlwTask> pageTodo(@Param("schId") Long schId, @Param("assigneeId") Integer assigneeId, Pageable pageable);
|
||||
|
||||
/** 我的已办(办理过的历史任务) */
|
||||
@Query("select t from FlwTask t where t.schId = :schId and t.assigneeId = :assigneeId and t.status in (1, 2) "
|
||||
+ "order by t.handleTime desc, t.id desc")
|
||||
/** 我的已办(办理过的历史任务,含退回/移交) */
|
||||
@Query("select t from FlwTask t where t.schId = :schId and t.assigneeId = :assigneeId "
|
||||
+ "and t.status in (1, 2, 4, 5) order by t.handleTime desc, t.id desc")
|
||||
Page<FlwTask> pageDone(@Param("schId") Long schId, @Param("assigneeId") Integer assigneeId, Pageable pageable);
|
||||
|
||||
/** 超期待催任务(定时扫描:待办+超期+未达催办上限) */
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -66,12 +66,15 @@ yh:
|
||||
"100005": "2c7885de594742cc840b8762910b2d2e"
|
||||
"100617": "2a3b4c5d6e7f8091a2b3c4d5e6f70819"
|
||||
open:
|
||||
enabled: true
|
||||
# 身份密钥唯一来源 = yh.app-auth(本文件 app-auth 段),open.* 副本已删;
|
||||
# 本地全链测试:指向 local-debug 的 UserBase;生产改回 base.yunhaixingtong.com
|
||||
base-host: http://localhost:8060
|
||||
base-path: /userbase
|
||||
app-key: 100019
|
||||
app-secret: "flow-center-dev-secret-100019"
|
||||
# 服务目录(子应用配置优化方案 §4.2):usermsg 未部署时无需配置(enabled=false 即不解析);
|
||||
# 部署后配容器直连完整 URL(如 http://usermsg-service:100xx/usermsg),或走平台级
|
||||
# YH_OPEN_SERVICES_USERMSG 注入。
|
||||
# services:
|
||||
# usermsg: http://localhost:8080
|
||||
|
||||
# ===== 流程中心专属配置 =====
|
||||
flow-center:
|
||||
@@ -97,8 +100,11 @@ flow-center:
|
||||
# 挂起/死信告警接收人(本校管理员 userId;本地联调可留空)
|
||||
alert-user-ids: ""
|
||||
usermsg:
|
||||
# UserMsg 基址(IDHttp 出站自动签名直达 /oapi/msg/**;UserMsg 侧 trusted-apps 须收本应用占位 appKey=100019)
|
||||
# 站内信/待办推送:主路径 = SDK 门面 YschMsgOpenApi(服务目录 yh.open.services.usermsg);
|
||||
# legacy 回落键 base-url 已删(无默认值)——enabled=true 且两者皆缺时构造期 fail-fast。
|
||||
# 本地 8080 起 UserMsg 后置 true;生产 UserMsg 未部署 → false(显式关闭,消除空转告警)。
|
||||
enabled: true
|
||||
# base-url legacy 回落(仅迁移期兜底,正常不用配):见上方 open.services.usermsg
|
||||
base-url: http://localhost:8080
|
||||
|
||||
logging:
|
||||
|
||||
@@ -0,0 +1,9 @@
|
||||
-- flw_task 增加 edge_name(多出口节点办理时选中的出边名,可空)
|
||||
-- 用途:审批节点有多条出边时,办理人在办理页选一个出口(名称即按钮文案),
|
||||
-- 所选出口落库在该任务行上,节点放行推进时按它决定走哪条边。
|
||||
-- 落库而非走事件传参:推进发生在 AFTER_COMMIT 的新事务里且带乐观锁重试,
|
||||
-- 事件传参不可靠;落列后可重放、可审计,并发办理时以最后办理的任务为准。
|
||||
-- 单出口节点恒为 NULL(旧数据零影响)。
|
||||
-- 本地/测试 ddl-auto=update 会自动加列;生产执行本脚本。
|
||||
ALTER TABLE `flw_task`
|
||||
ADD COLUMN `edge_name` VARCHAR(64) NULL COMMENT '多出口节点所选出口(对应 def 边 name)' AFTER `opinion`;
|
||||
@@ -97,7 +97,8 @@ class CallbackDispatchIT extends FlowTestSupport {
|
||||
new java.util.concurrent.atomic.AtomicInteger();
|
||||
|
||||
CountingUserMsgClient() {
|
||||
super(null);
|
||||
// enabled=false:本用例只覆盖回调派发,站内信为哑实现(两个方法均覆写)
|
||||
super(null, null, false, "");
|
||||
}
|
||||
|
||||
@Override
|
||||
|
||||
@@ -34,6 +34,75 @@ class DefValidatorTest {
|
||||
DefValidator.validate(parse(json), new HashSet<>(Collections.singletonList("ybb")));
|
||||
}
|
||||
|
||||
@Test
|
||||
void multiOutlet_withoutNames_fails() {
|
||||
String json = "{"
|
||||
+ "\"code\":\"c1\",\"name\":\"流程\",\"start\":\"start\","
|
||||
+ "\"nodes\":["
|
||||
+ "{\"id\":\"start\",\"type\":\"START\"},"
|
||||
+ "{\"id\":\"n1\",\"type\":\"APPROVAL\",\"name\":\"审批\",\"assignee\":{\"type\":\"DEPT_KEY\",\"deptKey\":\"ybb\"}},"
|
||||
+ "{\"id\":\"n2\",\"type\":\"APPROVAL\",\"name\":\"复核\",\"assignee\":{\"type\":\"DEPT_KEY\",\"deptKey\":\"ybb\"}},"
|
||||
+ "{\"id\":\"end\",\"type\":\"END\"}],"
|
||||
+ "\"edges\":[{\"from\":\"start\",\"to\":\"n1\"},"
|
||||
+ "{\"from\":\"n1\",\"to\":\"n2\"},{\"from\":\"n1\",\"to\":\"end\"},"
|
||||
+ "{\"from\":\"n2\",\"to\":\"end\"}]}";
|
||||
BizException e = assertThrows(BizException.class,
|
||||
() -> DefValidator.validate(parse(json), new HashSet<>(Collections.singletonList("ybb"))));
|
||||
assertTrue(e.getMessage().contains("出口必须命名"));
|
||||
}
|
||||
|
||||
@Test
|
||||
void multiOutlet_duplicateNames_fails() {
|
||||
String json = "{"
|
||||
+ "\"code\":\"c1\",\"name\":\"流程\",\"start\":\"start\","
|
||||
+ "\"nodes\":["
|
||||
+ "{\"id\":\"start\",\"type\":\"START\"},"
|
||||
+ "{\"id\":\"n1\",\"type\":\"APPROVAL\",\"name\":\"审批\",\"assignee\":{\"type\":\"DEPT_KEY\",\"deptKey\":\"ybb\"}},"
|
||||
+ "{\"id\":\"end\",\"type\":\"END\"}],"
|
||||
+ "\"edges\":[{\"from\":\"start\",\"to\":\"n1\"},"
|
||||
+ "{\"from\":\"n1\",\"to\":\"end\",\"name\":\"通过\"},"
|
||||
+ "{\"from\":\"n1\",\"to\":\"end\",\"name\":\"通过\"}]}";
|
||||
BizException e = assertThrows(BizException.class,
|
||||
() -> DefValidator.validate(parse(json), new HashSet<>(Collections.singletonList("ybb"))));
|
||||
assertTrue(e.getMessage().contains("出口名称重复"));
|
||||
}
|
||||
|
||||
@Test
|
||||
void multiOutlet_namedUnique_passes() {
|
||||
String json = "{"
|
||||
+ "\"code\":\"c1\",\"name\":\"流程\",\"start\":\"start\","
|
||||
+ "\"nodes\":["
|
||||
+ "{\"id\":\"start\",\"type\":\"START\"},"
|
||||
+ "{\"id\":\"n1\",\"type\":\"APPROVAL\",\"name\":\"审批\",\"assignee\":{\"type\":\"DEPT_KEY\",\"deptKey\":\"ybb\"}},"
|
||||
+ "{\"id\":\"end\",\"type\":\"END\"}],"
|
||||
+ "\"edges\":[{\"from\":\"start\",\"to\":\"n1\"},"
|
||||
+ "{\"from\":\"n1\",\"to\":\"end\",\"name\":\"通过\"},"
|
||||
+ "{\"from\":\"n1\",\"to\":\"end\",\"name\":\"驳回结束\"}]}";
|
||||
DefValidator.validate(parse(json), new HashSet<>(Collections.singletonList("ybb")));
|
||||
}
|
||||
|
||||
@Test
|
||||
void multiOutlet_crossBranchTarget_fails() {
|
||||
// n1 在并行分支内,出口指向主线 end —— 跨分支连线一期拒绝
|
||||
String json = "{"
|
||||
+ "\"code\":\"c1\",\"name\":\"流程\",\"start\":\"start\","
|
||||
+ "\"nodes\":["
|
||||
+ "{\"id\":\"start\",\"type\":\"START\"},"
|
||||
+ "{\"id\":\"split1\",\"type\":\"PARALLEL_SPLIT\"},"
|
||||
+ "{\"id\":\"n1\",\"type\":\"APPROVAL\",\"name\":\"分支审批\",\"assignee\":{\"type\":\"DEPT_KEY\",\"deptKey\":\"ybb\"}},"
|
||||
+ "{\"id\":\"join1\",\"type\":\"PARALLEL_JOIN\",\"waitFor\":\"ALL\"},"
|
||||
+ "{\"id\":\"end\",\"type\":\"END\"}],"
|
||||
+ "\"edges\":[{\"from\":\"start\",\"to\":\"split1\"},"
|
||||
+ "{\"from\":\"split1\",\"to\":\"n1\"},"
|
||||
+ "{\"from\":\"split1\",\"to\":\"end\",\"name\":\"直通\"},"
|
||||
+ "{\"from\":\"n1\",\"to\":\"end\",\"name\":\"分支出口\"},"
|
||||
+ "{\"from\":\"n1\",\"to\":\"join1\",\"name\":\"到汇聚\"},"
|
||||
+ "{\"from\":\"join1\",\"to\":\"end\"}]}";
|
||||
BizException e = assertThrows(BizException.class,
|
||||
() -> DefValidator.validate(parse(json), new HashSet<>(Collections.singletonList("ybb"))));
|
||||
assertTrue(e.getMessage().contains("跨分支连线"));
|
||||
}
|
||||
|
||||
@Test
|
||||
void missingEnd_fails() {
|
||||
String json = "{"
|
||||
|
||||
@@ -243,6 +243,131 @@ class FlowEngineIT extends FlowTestSupport {
|
||||
assertEquals(2, taskRepository.findByInstanceIdOrderByIdAsc(outcome.getInstanceId()).size());
|
||||
}
|
||||
|
||||
@Test
|
||||
void multiOutlet_approveWithChosenEdge_routesToTarget() {
|
||||
mapDept("km", 101, "甲");
|
||||
mapDept("kf", 102, "乙");
|
||||
FlwDef def = saveAndPublishDef("t10", "多出口", multiOutletDef("km", "kf"));
|
||||
FlowInstanceService.ApplyOutcome outcome = apply(def.getFlowGuid(), "biz-10", 201, "张三");
|
||||
Long instanceId = outcome.getInstanceId();
|
||||
FlwTask task = taskRepository.findByInstanceIdAndStatus(instanceId, 0).get(0);
|
||||
// 选"通过"出口 → 直达 END 完成
|
||||
taskService.handle(SCH, task.getId(), true, "ok", 101, "通过");
|
||||
assertEquals(FlwEnums.InstanceStatus.COMPLETED.getCode(),
|
||||
instanceRepository.findById(instanceId).get().getStatus());
|
||||
assertEquals("通过", taskRepository.findById(task.getId()).get().getEdgeName());
|
||||
}
|
||||
|
||||
@Test
|
||||
void multiOutlet_chooseOtherEdge_routesToSecondNode() throws Exception {
|
||||
mapDept("km", 101, "甲");
|
||||
mapDept("kf", 102, "乙");
|
||||
FlwDef def = saveAndPublishDef("t11", "多出口转向", multiOutletDef("km", "kf"));
|
||||
FlowInstanceService.ApplyOutcome outcome = apply(def.getFlowGuid(), "biz-11", 201, "张三");
|
||||
Long instanceId = outcome.getInstanceId();
|
||||
FlwTask task = taskRepository.findByInstanceIdAndStatus(instanceId, 0).get(0);
|
||||
// 选"转乙科"出口 → 到 n2 建新待办
|
||||
taskService.handle(SCH, task.getId(), true, "转给乙科", 101, "转乙科");
|
||||
assertEquals(FlwEnums.InstanceStatus.APPROVING.getCode(),
|
||||
instanceRepository.findById(instanceId).get().getStatus());
|
||||
List<FlwTask> pending = taskRepository.findByInstanceIdAndStatus(instanceId, 0);
|
||||
assertEquals(1, pending.size());
|
||||
assertEquals("n2", pending.get(0).getNodeId());
|
||||
assertEquals(102, pending.get(0).getAssigneeId());
|
||||
// 乙科通过 → 完成
|
||||
taskService.handle(SCH, pending.get(0).getId(), true, "ok", 102);
|
||||
assertEquals(FlwEnums.InstanceStatus.COMPLETED.getCode(),
|
||||
instanceRepository.findById(instanceId).get().getStatus());
|
||||
}
|
||||
|
||||
@Test
|
||||
void multiOutlet_multiOutletNode_requiresEdgeName() {
|
||||
mapDept("km", 101, "甲");
|
||||
mapDept("kf", 102, "乙");
|
||||
FlwDef def = saveAndPublishDef("t12", "多出口必选", multiOutletDef("km", "kf"));
|
||||
FlowInstanceService.ApplyOutcome outcome = apply(def.getFlowGuid(), "biz-12", 201, "张三");
|
||||
FlwTask task = taskRepository.findByInstanceIdAndStatus(outcome.getInstanceId(), 0).get(0);
|
||||
assertThrows(BizException.class,
|
||||
() -> taskService.handle(SCH, task.getId(), true, "ok", 101, null));
|
||||
assertThrows(BizException.class,
|
||||
() -> taskService.handle(SCH, task.getId(), true, "ok", 101, "不存在的出口"));
|
||||
}
|
||||
|
||||
@Test
|
||||
void anyCountersign_firstApprove_invalidatesSiblings_thenAdvances() {
|
||||
mapDept("ky", 101, "甲");
|
||||
mapDept("ky", 102, "乙");
|
||||
FlwDef def = saveAndPublishDef("t13", "或签", anyCountersignDef("ky"));
|
||||
FlowInstanceService.ApplyOutcome outcome = apply(def.getFlowGuid(), "biz-13", 201, "张三");
|
||||
Long instanceId = outcome.getInstanceId();
|
||||
List<FlwTask> pending = taskRepository.findByInstanceIdAndStatus(instanceId, 0);
|
||||
assertEquals(2, pending.size());
|
||||
// 或签:任一人同意即放行,兄弟待办失效(修复前行为=卡到全员办完)
|
||||
taskService.handle(SCH, pending.get(0).getId(), true, "我先批", 101);
|
||||
FlwInstance instance = instanceRepository.findById(instanceId).get();
|
||||
assertEquals(FlwEnums.InstanceStatus.COMPLETED.getCode(), instance.getStatus());
|
||||
List<FlwTask> all = taskRepository.findByInstanceIdOrderByIdAsc(instanceId);
|
||||
assertEquals(FlwEnums.TaskStatus.APPROVED.getCode(), all.get(0).getStatus());
|
||||
assertEquals(FlwEnums.TaskStatus.INVALIDATED.getCode(), all.get(1).getStatus());
|
||||
}
|
||||
|
||||
@Test
|
||||
void returnBack_fromTailNode_backToHead_thenReTraverseParallel_noUkViolation() {
|
||||
mapDept("km", 103, "主");
|
||||
mapDept("ka", 101, "甲");
|
||||
mapDept("kb", 102, "乙");
|
||||
FlwDef def = saveAndPublishDef("t14", "退回重入", mainlineParallelTailDef("km", "km"));
|
||||
FlowInstanceService.ApplyOutcome outcome = apply(def.getFlowGuid(), "biz-14", 201, "张三");
|
||||
Long instanceId = outcome.getInstanceId();
|
||||
// 顺序推进:n1 → 并行双分支 → join → n3(在出口复核节点)
|
||||
taskService.handle(SCH, tasksOf(instanceId, 103).get(0).getId(), true, "ok", 103);
|
||||
taskService.handle(SCH, tasksOf(instanceId, 101).get(0).getId(), true, "ok", 101);
|
||||
taskService.handle(SCH, tasksOf(instanceId, 102).get(0).getId(), true, "ok", 102);
|
||||
FlwTask tail = tasksOf(instanceId, 103).get(0);
|
||||
assertEquals("n3", tail.getNodeId());
|
||||
// 退回至入口(缺省主线最近已办=n1;并行内节点被过滤,不会命中 ta/tb)
|
||||
taskService.returnBack(SCH, tail.getId(), null, "重新审一遍", 103);
|
||||
FlwInstance returned = instanceRepository.findById(instanceId).get();
|
||||
assertEquals("n1", returned.getCurrentNodes());
|
||||
assertEquals(FlwEnums.InstanceStatus.APPROVING.getCode(), returned.getStatus());
|
||||
// 重入并行段:split 幂等 upsert,不撞 uk(instance,branchKey)
|
||||
taskService.handle(SCH, tasksOf(instanceId, 103).get(0).getId(), true, "再入口", 103);
|
||||
taskService.handle(SCH, tasksOf(instanceId, 101).get(0).getId(), true, "ok", 101);
|
||||
taskService.handle(SCH, tasksOf(instanceId, 102).get(0).getId(), true, "ok", 102);
|
||||
FlwTask n3Again = tasksOf(instanceId, 103).get(0);
|
||||
assertEquals("n3", n3Again.getNodeId());
|
||||
taskService.handle(SCH, n3Again.getId(), true, "复核通过", 103);
|
||||
assertEquals(FlwEnums.InstanceStatus.COMPLETED.getCode(),
|
||||
instanceRepository.findById(instanceId).get().getStatus());
|
||||
// 轨迹含 RETURNED 行
|
||||
assertEquals(1, allTasks(instanceId).stream()
|
||||
.filter(t -> FlwEnums.TaskStatus.RETURNED.getCode() == t.getStatus()).count());
|
||||
}
|
||||
|
||||
@Test
|
||||
void returnBack_noMainlineTarget_throws_andTransfer_reassigns() {
|
||||
mapDept("km", 101, "甲");
|
||||
FlwDef def = saveAndPublishDef("t15", "退回目标", linearDef("km", "首审"));
|
||||
FlowInstanceService.ApplyOutcome outcome = apply(def.getFlowGuid(), "biz-15", 201, "张三");
|
||||
Long instanceId = outcome.getInstanceId();
|
||||
FlwTask task = taskRepository.findByInstanceIdAndStatus(instanceId, 0).get(0);
|
||||
// 首节点无上游已办:缺省退回应明确报错
|
||||
assertThrows(BizException.class, () ->
|
||||
taskService.returnBack(SCH, task.getId(), null, "试试", 101));
|
||||
// 移交:当前任务置 TRANSFERRED,同节点给新办理人建待办
|
||||
taskService.transfer(SCH, task.getId(), 202, "人不在岗", 101);
|
||||
List<FlwTask> handled = allTasks(instanceId);
|
||||
assertEquals(2, handled.size(), "移交后应产生新待办行");
|
||||
assertEquals(FlwEnums.TaskStatus.TRANSFERRED.getCode(), handled.get(0).getStatus());
|
||||
FlwTask reassigned = taskRepository.findByInstanceIdAndStatus(instanceId, 0).get(0);
|
||||
assertEquals(202, reassigned.getAssigneeId());
|
||||
assertEquals(101, handled.get(0).getAssigneeId()); // 原办理人留在轨迹
|
||||
// 新办理人通过 → 完成;已退回/移交后提交人不能撤回
|
||||
taskService.handle(SCH, reassigned.getId(), true, "接手批准", 202);
|
||||
assertEquals(FlwEnums.InstanceStatus.COMPLETED.getCode(),
|
||||
instanceRepository.findById(instanceId).get().getStatus());
|
||||
}
|
||||
|
||||
private List<FlwBranch> branchRows(Long instanceId) {
|
||||
return branchRepository.findByInstanceId(instanceId);
|
||||
}
|
||||
|
||||
@@ -17,7 +17,7 @@ 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 org.junit.jupiter.api.BeforeEach;
|
||||
@@ -102,7 +102,9 @@ class FlowDiagramServiceTest {
|
||||
FlowEngine engine = new FlowEngine(new ObjectMapper(),
|
||||
taskRepository, mock(FlwBranchRepository.class), mock(AssigneeResolver.class),
|
||||
mock(ApplicationEventPublisher.class));
|
||||
service = new FlowDiagramService(defRepository, mapRepository, engine, userClient);
|
||||
service = new FlowDiagramService(defRepository, mapRepository,
|
||||
mock(com.yunhai.flowcenter.repository.FlwInstanceRepository.class),
|
||||
taskRepository, engine, userClient);
|
||||
}
|
||||
|
||||
private FlwDef def(String json) {
|
||||
|
||||
@@ -2,8 +2,8 @@ package com.yunhai.flowcenter.service;
|
||||
|
||||
import com.yunhai.flowcenter.entity.FlwAssigneeMap;
|
||||
import com.yunhai.flowcenter.repository.FlwAssigneeMapRepository;
|
||||
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 com.yunhai.platform.utils.model.BoaUser;
|
||||
import org.junit.jupiter.api.BeforeEach;
|
||||
|
||||
@@ -1,11 +1,11 @@
|
||||
package com.yunhai.flowcenter.stub;
|
||||
|
||||
import com.yunhai.platform.openapi.client.OpenApiUserClient;
|
||||
import com.yunhai.platform.openapi.model.SysUserGroupDeleteItem;
|
||||
import com.yunhai.platform.openapi.model.SysUserGroupStateRequest;
|
||||
import com.yunhai.platform.openapi.model.SysUserMoveRequest;
|
||||
import com.yunhai.platform.openapi.model.SysUserRequest;
|
||||
import com.yunhai.platform.openapi.model.SysUserTransferSchoolRequest;
|
||||
import com.yunhai.platform.openapi.base.OpenApiUserClient;
|
||||
import com.yunhai.platform.openapi.base.model.SysUserGroupDeleteItem;
|
||||
import com.yunhai.platform.openapi.base.model.SysUserGroupStateRequest;
|
||||
import com.yunhai.platform.openapi.base.model.SysUserMoveRequest;
|
||||
import com.yunhai.platform.openapi.base.model.SysUserRequest;
|
||||
import com.yunhai.platform.openapi.base.model.SysUserTransferSchoolRequest;
|
||||
import com.yunhai.platform.utils.model.BoaGroup;
|
||||
import com.yunhai.platform.utils.model.BoaModule;
|
||||
import com.yunhai.platform.utils.model.BoaRoleInfo;
|
||||
|
||||
@@ -103,4 +103,55 @@ public abstract class FlowTestSupport {
|
||||
private static String codeOf() {
|
||||
return "tc_" + UUID.randomUUID().toString().replace("-", "").substring(0, 8);
|
||||
}
|
||||
|
||||
/** 多出口流程:START → n1(deptA,出口"转乙科"→n2 / "通过"→end) → end;n2(deptB) → end */
|
||||
protected String multiOutletDef(String deptA, String deptB) {
|
||||
return "{"
|
||||
+ "\"code\":\"" + codeOf() + "\",\"name\":\"多出口测试流程\",\"start\":\"start\","
|
||||
+ "\"nodes\":["
|
||||
+ "{\"id\":\"start\",\"type\":\"START\",\"name\":\"开始\"},"
|
||||
+ "{\"id\":\"n1\",\"type\":\"APPROVAL\",\"name\":\"入口审核\",\"assignee\":{\"type\":\"DEPT_KEY\",\"deptKey\":\"" + deptA + "\"}},"
|
||||
+ "{\"id\":\"n2\",\"type\":\"APPROVAL\",\"name\":\"复核\",\"assignee\":{\"type\":\"DEPT_KEY\",\"deptKey\":\"" + deptB + "\"}},"
|
||||
+ "{\"id\":\"end\",\"type\":\"END\"}"
|
||||
+ "],"
|
||||
+ "\"edges\":[{\"from\":\"start\",\"to\":\"n1\"},"
|
||||
+ "{\"from\":\"n1\",\"to\":\"n2\",\"name\":\"转乙科\"},"
|
||||
+ "{\"from\":\"n1\",\"to\":\"end\",\"name\":\"通过\"},"
|
||||
+ "{\"from\":\"n2\",\"to\":\"end\"}],"
|
||||
+ "\"settings\":{\"rejectPolicy\":\"END_REJECTED\"}}";
|
||||
}
|
||||
|
||||
/** 或签流程:START → n1(deptKey, ANY 或签) → END */
|
||||
protected String anyCountersignDef(String deptKey) {
|
||||
return "{"
|
||||
+ "\"code\":\"" + codeOf() + "\",\"name\":\"夡签测试流程\",\"start\":\"start\","
|
||||
+ "\"nodes\":["
|
||||
+ "{\"id\":\"start\",\"type\":\"START\",\"name\":\"开始\"},"
|
||||
+ "{\"id\":\"n1\",\"type\":\"APPROVAL\",\"name\":\"任一人审批\",\"assignee\":{\"type\":\"DEPT_KEY\",\"deptKey\":\"" + deptKey + "\"},\"countersign\":\"ANY\"},"
|
||||
+ "{\"id\":\"end\",\"type\":\"END\"}"
|
||||
+ "],"
|
||||
+ "\"edges\":[{\"from\":\"start\",\"to\":\"n1\"},{\"from\":\"n1\",\"to\":\"end\"}],"
|
||||
+ "\"settings\":{\"rejectPolicy\":\"END_REJECTED\"}}";
|
||||
}
|
||||
|
||||
/** 主线带并行段:START → n1(deptA) → split → {ta(ka), tb(kb)} → join → n3(deptA) → END(退回重入用) */
|
||||
protected String mainlineParallelTailDef(String deptA, String deptMain) {
|
||||
return "{"
|
||||
+ "\"code\":\"" + codeOf() + "\",\"name\":\"主线并行尾验流程\",\"start\":\"start\","
|
||||
+ "\"nodes\":["
|
||||
+ "{\"id\":\"start\",\"type\":\"START\",\"name\":\"开始\"},"
|
||||
+ "{\"id\":\"n1\",\"type\":\"APPROVAL\",\"name\":\"入口\",\"assignee\":{\"type\":\"DEPT_KEY\",\"deptKey\":\"" + deptMain + "\"}},"
|
||||
+ "{\"id\":\"split1\",\"type\":\"PARALLEL_SPLIT\"},"
|
||||
+ "{\"id\":\"ta\",\"type\":\"APPROVAL\",\"name\":\"甲科审核\",\"assignee\":{\"type\":\"DEPT_KEY\",\"deptKey\":\"ka\"}},"
|
||||
+ "{\"id\":\"tb\",\"type\":\"APPROVAL\",\"name\":\"乙科审核\",\"assignee\":{\"type\":\"DEPT_KEY\",\"deptKey\":\"kb\"}},"
|
||||
+ "{\"id\":\"join1\",\"type\":\"PARALLEL_JOIN\",\"waitFor\":\"ALL\"},"
|
||||
+ "{\"id\":\"n3\",\"type\":\"APPROVAL\",\"name\":\"出口复核\",\"assignee\":{\"type\":\"DEPT_KEY\",\"deptKey\":\"" + deptA + "\"}},"
|
||||
+ "{\"id\":\"end\",\"type\":\"END\"}"
|
||||
+ "],"
|
||||
+ "\"edges\":[{\"from\":\"start\",\"to\":\"n1\"},{\"from\":\"n1\",\"to\":\"split1\"},"
|
||||
+ "{\"from\":\"split1\",\"to\":\"ta\"},{\"from\":\"split1\",\"to\":\"tb\"},"
|
||||
+ "{\"from\":\"ta\",\"to\":\"join1\"},{\"from\":\"tb\",\"to\":\"join1\"},"
|
||||
+ "{\"from\":\"join1\",\"to\":\"n3\"},{\"from\":\"n3\",\"to\":\"end\"}],"
|
||||
+ "\"settings\":{\"rejectPolicy\":\"END_REJECTED\"}}";
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user