feat(engine): NOTIFY 抄送 + CONDITION 条件路由 + JOIN scope 修复(两批全栈)
build / build-push (push) Successful in 47s
build / build-push (push) Successful in 47s
- NOTIFY:TYPE_NOTIFY 瞬时抄送(flw_cc 逐人留痕 + CC_SENT 事件 + 站内通知不加待办角标)、/api/flow/cc/my、轨迹 kind=CC 与 oapi progress 抄送步、示意图青框卡片
- CONDITION:flw_router 判断服务注册 + flw_route_log 决策留痕(REQUIRES_NEW,reroute 回滚不丢失败行)+ 同步判断调用(服务目录寻址 + 平台签名,3s×3 退避重试)+ 失败挂起/管理端重跑(不配默认出口)+ 可选事件 flow.route.decided
- JOIN scope:branch_key 路径前缀编码 + scope 级到齐放行 + DefValidator SPLIT/JOIN 配对与键长守卫(存量无点号键零迁移,历史直连图回退实例级口径)
- 管理端:/api/flow/admin/router/* CRUD + POST /api/flow/admin/instance/{id}/reroute
- 生产 SQL:sql/20260923_create_flw_cc.sql(含 flw_task 时间列升 DATETIME(3))、20260924_create_flw_router_and_route_log.sql
- 测试:全量 125 绿(FlowEngineIT 24,含条件/嵌套并行/退回重走用例)
This commit is contained in:
@@ -4,15 +4,19 @@ 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.FlwCc;
|
||||
import com.yunhai.flowcenter.entity.FlwDef;
|
||||
import com.yunhai.flowcenter.entity.FlwInstance;
|
||||
import com.yunhai.flowcenter.entity.FlwRouteLog;
|
||||
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.FlwBranchRepository;
|
||||
import com.yunhai.flowcenter.repository.FlwCcRepository;
|
||||
import com.yunhai.flowcenter.repository.FlwDefRepository;
|
||||
import com.yunhai.flowcenter.repository.FlwInstanceRepository;
|
||||
import com.yunhai.flowcenter.repository.FlwRouteLogRepository;
|
||||
import com.yunhai.flowcenter.repository.FlwTaskRepository;
|
||||
import com.yunhai.platform.apistd.BizException;
|
||||
import com.yunhai.platform.utils.exception.ErrorCode;
|
||||
@@ -40,6 +44,8 @@ public class FlowQueryService {
|
||||
private final FlwInstanceRepository instanceRepository;
|
||||
private final FlwTaskRepository taskRepository;
|
||||
private final FlwBranchRepository branchRepository;
|
||||
private final FlwCcRepository ccRepository;
|
||||
private final FlwRouteLogRepository routeLogRepository;
|
||||
private final FlwDefRepository defRepository;
|
||||
private final FlwAssigneeMapRepository mapRepository;
|
||||
private final FlowEngine flowEngine;
|
||||
@@ -52,6 +58,41 @@ public class FlowQueryService {
|
||||
.collect(Collectors.toList()), page, size);
|
||||
}
|
||||
|
||||
/** 抄送我的(NOTIFY 落库行,sent_time 倒序;知会不占待办,独立于 todo/done 口径) */
|
||||
public BasePageResult<FlowModels.CcItem> pageCc(Long schId, Integer userId,
|
||||
Integer page, Integer size) {
|
||||
Page<FlwCc> p = ccRepository.findBySchIdAndUserIdOrderBySentTimeDesc(schId, userId,
|
||||
PageRequest.of(normPage(page), normSize(size)));
|
||||
List<FlowModels.CcItem> items = p.getContent().stream()
|
||||
.map(this::toCcItem).collect(Collectors.toList());
|
||||
fillCcInstanceInfo(items);
|
||||
return toResult(p.getTotalElements(), items, page, size);
|
||||
}
|
||||
|
||||
private FlowModels.CcItem toCcItem(FlwCc cc) {
|
||||
FlowModels.CcItem item = new FlowModels.CcItem();
|
||||
item.setCcId(cc.getId());
|
||||
item.setInstanceId(cc.getInstanceId());
|
||||
item.setNodeId(cc.getNodeId());
|
||||
item.setNodeName(cc.getNodeName());
|
||||
item.setUserId(cc.getUserId());
|
||||
item.setUserName(cc.getUserName());
|
||||
item.setSentTime(str(cc.getSentTime()));
|
||||
return item;
|
||||
}
|
||||
|
||||
private void fillCcInstanceInfo(List<FlowModels.CcItem> items) {
|
||||
for (FlowModels.CcItem item : items) {
|
||||
FlwInstance instance = instanceRepository.findById(item.getInstanceId()).orElse(null);
|
||||
if (instance != null) {
|
||||
item.setBizTitle(instance.getBizTitle());
|
||||
item.setBizType(instance.getBizType());
|
||||
item.setBizId(instance.getBizId());
|
||||
item.setInstanceStatus(instance.getStatus());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public BasePageResult<FlowModels.TaskItem> pageTodo(Long schId, Integer assigneeId,
|
||||
Integer page, Integer size) {
|
||||
Page<FlwTask> p = taskRepository.pageTodo(schId, assigneeId,
|
||||
@@ -122,9 +163,11 @@ public class FlowQueryService {
|
||||
detail.setFinishTime(str(instance.getFinishTime()));
|
||||
detail.setVersion(instance.getVersion());
|
||||
List<FlwTask> tasks = taskRepository.findByInstanceIdOrderByIdAsc(instanceId);
|
||||
List<FlwCc> ccs = ccRepository.findByInstanceIdOrderByIdAsc(instanceId);
|
||||
List<FlwRouteLog> routeLogs = routeLogRepository.findByInstanceIdOrderByIdAsc(instanceId);
|
||||
FlwDef def = defRepository.findById(instance.getDefId()).orElse(null);
|
||||
FlowDefModel model = def == null ? null : flowEngine.parseModel(def);
|
||||
detail.setTrace(buildTrace(instance, tasks, model));
|
||||
detail.setTrace(buildTrace(instance, tasks, ccs, routeLogs, model));
|
||||
List<Map<String, Object>> branches = new ArrayList<>();
|
||||
for (FlwBranch b : branchRepository.findByInstanceId(instanceId)) {
|
||||
Map<String, Object> row = new HashMap<>();
|
||||
@@ -224,13 +267,19 @@ public class FlowQueryService {
|
||||
* 合成一条按时间升序的轨迹,替掉原先"按 id 升序"的排法(id 序在撤回重走场景下与
|
||||
* 真实发生顺序不一致)。
|
||||
*
|
||||
* <p>2026-09-23 并入抄送行(flw_cc,kind=CC):NOTIFY 节点不建任务,若不并入则抄送在
|
||||
* 轨迹上不可见;按 sentTime 与任务行同场归位、参与轮次进位(退回重走的第 N 轮抄送)。
|
||||
*
|
||||
* <p>2026-09-24 并入路由判断行(flw_route_log,kind=ROUTE):CONDITION 判断留痕
|
||||
* "系统判断 → 出口"(outlet=NULL 渲染为挂起);按 decideTime 同场归位。
|
||||
*
|
||||
* <p>每条行补四个前端直接用、不再自行推断的字段:
|
||||
* <ul>
|
||||
* <li>{@code time} 该行实际发生的时间(已办/退回/移交/撤回=handleTime,在办/失效=arriveTime);</li>
|
||||
* <li>{@code kind} SUBMIT|RESUBMIT|WITHDRAW|APPROVE|REJECT|RETURN|TRANSFER|INVALID|PENDING
|
||||
* <li>{@code time} 该行实际发生的时间(已办/退回/移交/撤回=handleTime,在办/失效=arriveTime,抄送=sentTime,判断=decideTime);</li>
|
||||
* <li>{@code kind} SUBMIT|RESUBMIT|WITHDRAW|APPROVE|REJECT|RETURN|TRANSFER|INVALID|PENDING|CC|ROUTE
|
||||
* (前端配色与文案的唯一依据);</li>
|
||||
* <li>{@code round} 第几轮审批(遇"重新提交"进位,撤回属于它结束的那一轮);</li>
|
||||
* <li>{@code rowKey} 去重 key(任务行 t{id}、合成行 submit —— taskId 可能为 null 不能当 key)。</li>
|
||||
* <li>{@code rowKey} 去重 key(任务行 t{id}、抄送行 cc{id}、合成行 submit —— taskId 可能为 null 不能当 key)。</li>
|
||||
* </ul>
|
||||
* 科室口径(视图两档:dept/user)另需 assigneeType/deptKey/deptName —— 只有 DEPT_KEY 节点有
|
||||
* 科室概念,故仅对该类节点回填;节点在定义里找不到(重提后老节点、留痕事件行)不回填,
|
||||
@@ -240,40 +289,39 @@ public class FlowQueryService {
|
||||
* 兜底插入到时间流对应位置,保证轨迹首条仍是"发起申请"。
|
||||
*/
|
||||
private List<Map<String, Object>> buildTrace(FlwInstance instance, List<FlwTask> tasks,
|
||||
List<FlwCc> ccs, List<FlwRouteLog> routeLogs,
|
||||
FlowDefModel model) {
|
||||
List<FlwTask> sorted = new ArrayList<>(tasks);
|
||||
sorted.sort(Comparator.comparing(FlowQueryService::happenedAt,
|
||||
Comparator.nullsLast(Comparator.naturalOrder()))
|
||||
.thenComparing(FlwTask::getId));
|
||||
// 任务行、判断行与抄送行同场按发生时间排序:时间只到秒(DATETIME),同秒平局任务行恒在前、
|
||||
// 判断行次之(办理→判断→抄送的因果序)、抄送行最后(秩隔开跨表 id 空间;稳排保证同源行相对次序不变)。
|
||||
// 无判断/抄送的办件所有行 source 恒 0,排序与旧版 (time,id) 严格等价——
|
||||
// 不能给任务行再分因果秩:驳回行与重提行同秒是常态,旧序按 id(真实先后),秩会颠倒。
|
||||
List<TimedRow> rows = new ArrayList<>();
|
||||
Map<String, String> deptNames = new HashMap<>();
|
||||
for (FlwTask t : tasks) {
|
||||
rows.add(new TimedRow(happenedAt(t), 0, t.getId(),
|
||||
taskTraceRow(t, model, instance, deptNames)));
|
||||
}
|
||||
for (FlwRouteLog r : routeLogs) {
|
||||
rows.add(new TimedRow(r.getDecideTime(), 1, r.getId(), routeTraceRow(r)));
|
||||
}
|
||||
for (FlwCc c : ccs) {
|
||||
rows.add(new TimedRow(c.getSentTime(), 2, c.getId(), ccTraceRow(c)));
|
||||
}
|
||||
rows.sort(Comparator.comparing((TimedRow r) -> r.time,
|
||||
Comparator.nullsLast(Comparator.naturalOrder()))
|
||||
.thenComparing(r -> r.source)
|
||||
.thenComparing(r -> r.seq));
|
||||
List<Map<String, Object>> trace = new ArrayList<>();
|
||||
int headIndex = 0;
|
||||
LocalDateTime submitTime = instance.getSubmitTime();
|
||||
for (FlwTask t : sorted) {
|
||||
Map<String, Object> row = new HashMap<>();
|
||||
row.put("rowKey", "t" + t.getId());
|
||||
row.put("taskId", t.getId());
|
||||
row.put("kind", kindOf(t));
|
||||
row.put("nodeId", t.getNodeId());
|
||||
row.put("nodeName", t.getNodeName());
|
||||
row.put("assigneeId", t.getAssigneeId());
|
||||
row.put("assigneeName", t.getAssigneeName());
|
||||
row.put("status", t.getStatus());
|
||||
row.put("opinion", t.getOpinion());
|
||||
row.put("arriveTime", str(t.getArriveTime()));
|
||||
row.put("handleTime", str(t.getHandleTime()));
|
||||
row.put("time", str(happenedAt(t)));
|
||||
row.put("deadline", str(t.getDeadline()));
|
||||
row.put("urgeCount", t.getUrgeCount());
|
||||
row.put("edgeName", t.getEdgeName());
|
||||
fillAssigneeScope(row, model, t.getNodeId(), instance.getSchId(), deptNames);
|
||||
trace.add(row);
|
||||
for (TimedRow r : rows) {
|
||||
trace.add(r.row);
|
||||
// 老办件兜底行的插入位:时间早于提交时间的行数(老办件里恒为 0;重提过的老办件按时间放在中段)
|
||||
if (submitTime != null && happenedAt(t).isBefore(submitTime)) {
|
||||
if (submitTime != null && r.time != null && r.time.isBefore(submitTime)) {
|
||||
headIndex = trace.size();
|
||||
}
|
||||
}
|
||||
if (!hasSubmitRow(sorted)) {
|
||||
if (!hasSubmitRow(tasks)) {
|
||||
trace.add(headIndex, syntheticSubmitRow(instance));
|
||||
}
|
||||
int round = 1;
|
||||
@@ -286,6 +334,74 @@ public class FlowQueryService {
|
||||
return trace;
|
||||
}
|
||||
|
||||
private Map<String, Object> taskTraceRow(FlwTask t, FlowDefModel model, FlwInstance instance,
|
||||
Map<String, String> deptNames) {
|
||||
Map<String, Object> row = new HashMap<>();
|
||||
row.put("rowKey", "t" + t.getId());
|
||||
row.put("taskId", t.getId());
|
||||
row.put("kind", kindOf(t));
|
||||
row.put("nodeId", t.getNodeId());
|
||||
row.put("nodeName", t.getNodeName());
|
||||
row.put("assigneeId", t.getAssigneeId());
|
||||
row.put("assigneeName", t.getAssigneeName());
|
||||
row.put("status", t.getStatus());
|
||||
row.put("opinion", t.getOpinion());
|
||||
row.put("arriveTime", str(t.getArriveTime()));
|
||||
row.put("handleTime", str(t.getHandleTime()));
|
||||
row.put("time", str(happenedAt(t)));
|
||||
row.put("deadline", str(t.getDeadline()));
|
||||
row.put("urgeCount", t.getUrgeCount());
|
||||
row.put("edgeName", t.getEdgeName());
|
||||
fillAssigneeScope(row, model, t.getNodeId(), instance.getSchId(), deptNames);
|
||||
return row;
|
||||
}
|
||||
|
||||
/** 抄送轨迹行(kind=CC):no taskId/opinion,接收人记在 assignee* 字段(前端复用人列) */
|
||||
private static Map<String, Object> ccTraceRow(FlwCc c) {
|
||||
Map<String, Object> row = new HashMap<>();
|
||||
row.put("rowKey", "cc" + c.getId());
|
||||
row.put("ccId", c.getId());
|
||||
row.put("kind", "CC");
|
||||
row.put("nodeId", c.getNodeId());
|
||||
row.put("nodeName", c.getNodeName() == null || c.getNodeName().isEmpty() ? "抄送" : c.getNodeName());
|
||||
row.put("assigneeId", c.getUserId());
|
||||
row.put("assigneeName", c.getUserName());
|
||||
row.put("status", null);
|
||||
row.put("time", str(c.getSentTime()));
|
||||
return row;
|
||||
}
|
||||
|
||||
/** 路由判断轨迹行(kind=ROUTE,2026-09-24):"系统判断 → 出口",outlet=NULL 渲染为挂起 */
|
||||
private static Map<String, Object> routeTraceRow(FlwRouteLog r) {
|
||||
Map<String, Object> row = new HashMap<>();
|
||||
row.put("rowKey", "r" + r.getId());
|
||||
row.put("routeLogId", r.getId());
|
||||
row.put("kind", "ROUTE");
|
||||
row.put("nodeId", r.getNodeId());
|
||||
row.put("nodeName", r.getNodeName() == null || r.getNodeName().isEmpty() ? "条件判断" : r.getNodeName());
|
||||
row.put("routerKey", r.getRouterKey());
|
||||
row.put("outlet", r.getOutlet());
|
||||
row.put("attempt", r.getAttempt());
|
||||
row.put("status", null);
|
||||
row.put("time", str(r.getDecideTime()));
|
||||
return row;
|
||||
}
|
||||
|
||||
/** 排序载体:发生时间 + 来源秩(任务 0/判断 1/抄送 2)+ 同表内序(id),跨表 id 不比大小 */
|
||||
private static final class TimedRow {
|
||||
final LocalDateTime time;
|
||||
final int source;
|
||||
final long seq;
|
||||
final Map<String, Object> row;
|
||||
|
||||
TimedRow(LocalDateTime time, int source, long seq, Map<String, Object> row) {
|
||||
this.time = time;
|
||||
this.source = source;
|
||||
this.seq = seq;
|
||||
this.row = row;
|
||||
}
|
||||
}
|
||||
|
||||
private static boolean hasSubmitRow(List<FlwTask> tasks) {
|
||||
for (FlwTask t : tasks) {
|
||||
if (FlwEnums.TaskStatus.NODE_SUBMIT.equals(t.getNodeId())) {
|
||||
@@ -394,8 +510,12 @@ public class FlowQueryService {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 展示时间统一截断到秒:时间列已升毫秒精度(DATETIME(3),轨迹交织排序用),
|
||||
* 对外展示口径保持秒级不变(存量前端零适配);排序用未截断的原值。
|
||||
*/
|
||||
private static String str(LocalDateTime t) {
|
||||
return t == null ? null : t.toString();
|
||||
return t == null ? null : t.truncatedTo(java.time.temporal.ChronoUnit.SECONDS).toString();
|
||||
}
|
||||
|
||||
private static int normPage(Integer page) {
|
||||
|
||||
Reference in New Issue
Block a user