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:
@@ -51,7 +51,7 @@ public class DiagramImageService {
|
||||
private static final int LEGEND_H = 48;
|
||||
private static final int ARROW = 9;
|
||||
|
||||
/** 各类型盒型(w,h):开始/结束胶囊、审批圆角卡、并行菱形(取外接矩形) */
|
||||
/** 各类型盒型(w,h):开始/结束胶囊、审批圆角卡、并行/条件菱形(取外接矩形) */
|
||||
private static int[] boxOf(String type) {
|
||||
switch (type == null ? "" : type) {
|
||||
case FlowDefModel.TYPE_START:
|
||||
@@ -59,6 +59,7 @@ public class DiagramImageService {
|
||||
return new int[]{116, 44};
|
||||
case FlowDefModel.TYPE_PARALLEL_SPLIT:
|
||||
case FlowDefModel.TYPE_PARALLEL_JOIN:
|
||||
case FlowDefModel.TYPE_CONDITION:
|
||||
return new int[]{132, 78};
|
||||
default:
|
||||
return new int[]{200, 64};
|
||||
@@ -77,6 +78,11 @@ public class DiagramImageService {
|
||||
private static final Color C_PARALLEL_FILL = new Color(0xFF, 0xF8, 0xF0);
|
||||
private static final Color C_PARALLEL_BORDER = new Color(0xFF, 0x99, 0x00);
|
||||
private static final Color C_PARALLEL_TEXT = new Color(0xE4, 0x6A, 0x0A);
|
||||
private static final Color C_NOTIFY_FILL = new Color(0xF0, 0xF9, 0xFF);
|
||||
private static final Color C_NOTIFY_BORDER = new Color(0x13, 0xA8, 0xA8);
|
||||
private static final Color C_CONDITION_FILL = new Color(0xF9, 0xF0, 0xFF);
|
||||
private static final Color C_CONDITION_BORDER = new Color(0x92, 0x54, 0xDE);
|
||||
private static final Color C_CONDITION_TEXT = new Color(0x72, 0x2E, 0xD1);
|
||||
private static final Color C_DONE_BORDER = new Color(0x19, 0xBE, 0x6B);
|
||||
private static final Color C_ACTIVE_BORDER = new Color(0xED, 0x40, 0x14);
|
||||
private static final Color C_REJECT_FILL = new Color(0xFF, 0xF1, 0xF0);
|
||||
@@ -433,6 +439,49 @@ public class DiagramImageService {
|
||||
g.drawString(label, cx - fm.stringWidth(label) / 2f, cy + fm.getAscent() / 2f - 1);
|
||||
return;
|
||||
}
|
||||
if (FlowDefModel.TYPE_CONDITION.equals(type)) {
|
||||
// 条件分支:紫菱形(挂起=active 红框加粗,与前端 FlowDiagram.vue 同配色)
|
||||
int cx = p[0] + p[2] / 2;
|
||||
int cy = p[1] + p[3] / 2;
|
||||
Path2D d = new Path2D.Float();
|
||||
d.moveTo(cx, p[1]);
|
||||
d.lineTo(p[0] + p[2], cy);
|
||||
d.lineTo(cx, p[1] + p[3]);
|
||||
d.lineTo(p[0], cy);
|
||||
d.closePath();
|
||||
g.setColor(C_CONDITION_FILL);
|
||||
g.fill(d);
|
||||
boolean hung = "active".equals(n.getState());
|
||||
g.setColor(hung ? C_ACTIVE_BORDER : C_CONDITION_BORDER);
|
||||
g.setStroke(new BasicStroke(hung ? 2.2f : 1.6f));
|
||||
g.draw(d);
|
||||
g.setFont(subF);
|
||||
g.setColor(C_CONDITION_TEXT);
|
||||
FontMetrics fm = g.getFontMetrics();
|
||||
String label = truncate(g, n.getLabel(), p[2] - 36);
|
||||
g.drawString(label, cx - fm.stringWidth(label) / 2f, cy + fm.getAscent() / 2f - 1);
|
||||
return;
|
||||
}
|
||||
if (FlowDefModel.TYPE_NOTIFY.equals(type)) {
|
||||
// 抄送:圆角卡(青框白底,label + 单行 subLabel)——瞬时节点不占 active 红,
|
||||
// 状态着色与审批一致(done 绿框表示本办件已抄送过)
|
||||
g.setColor(C_NOTIFY_FILL);
|
||||
g.fillRoundRect(p[0], p[1], p[2], p[3], 12, 12);
|
||||
g.setColor(borderOf(n));
|
||||
g.setStroke(new BasicStroke("active".equals(n.getState()) ? 2.4f : 1.6f));
|
||||
g.drawRoundRect(p[0], p[1], p[2], p[3], 12, 12);
|
||||
g.setFont(labelF);
|
||||
g.setColor(C_NOTIFY_BORDER);
|
||||
String label = truncate(g, n.getLabel(), p[2] - 20);
|
||||
g.drawString(label, p[0] + (p[2] - g.getFontMetrics().stringWidth(label)) / 2f, p[1] + 26);
|
||||
if (n.getSubLabel() != null && !n.getSubLabel().isEmpty()) {
|
||||
g.setFont(subF);
|
||||
g.setColor(C_SUB);
|
||||
String sub = truncate(g, n.getSubLabel(), p[2] - 20);
|
||||
g.drawString(sub, p[0] + (p[2] - g.getFontMetrics().stringWidth(sub)) / 2f, p[1] + 46);
|
||||
}
|
||||
return;
|
||||
}
|
||||
// 审批:圆角卡(默认蓝框;done 绿/active 红/pending 灰/rejected 红底),label + 单行 subLabel
|
||||
g.setColor("rejected".equals(n.getState()) ? C_REJECT_FILL : Color.WHITE);
|
||||
g.fillRoundRect(p[0], p[1], p[2], p[3], 12, 12);
|
||||
@@ -491,6 +540,9 @@ public class DiagramImageService {
|
||||
if (t.equals(FlowDefModel.TYPE_PARALLEL_SPLIT) || t.equals(FlowDefModel.TYPE_PARALLEL_JOIN)) {
|
||||
return C_PARALLEL_BORDER;
|
||||
}
|
||||
if (t.equals(FlowDefModel.TYPE_NOTIFY)) {
|
||||
return C_NOTIFY_BORDER;
|
||||
}
|
||||
return C_APPROVAL_BORDER;
|
||||
}
|
||||
|
||||
|
||||
@@ -31,6 +31,7 @@ public class FlowDefAdminService {
|
||||
|
||||
private final FlwDefRepository defRepository;
|
||||
private final FlwAssigneeMapRepository mapRepository;
|
||||
private final com.yunhai.flowcenter.repository.FlwRouterRepository routerRepository;
|
||||
private final FlowEngine flowEngine;
|
||||
private final ObjectMapper objectMapper;
|
||||
|
||||
@@ -128,9 +129,7 @@ public class FlowDefAdminService {
|
||||
throw new BizException(ErrorCode.PARAM_ERROR, "已是发布状态");
|
||||
}
|
||||
FlowDefModel model = FlowDefModel.parse(objectMapper, def.getContentJson());
|
||||
Set<String> enabledDeptKeys = mapRepository.findEnabledBySchId(schId).stream()
|
||||
.map(FlwAssigneeMap::getDeptKey).collect(Collectors.toSet());
|
||||
DefValidator.validate(model, enabledDeptKeys);
|
||||
DefValidator.validate(model, enabledDeptKeys(schId), enabledRouterKeys());
|
||||
def.setStatus(FlwEnums.DefStatus.PUBLISHED.getCode());
|
||||
def.setPublishTime(LocalDateTime.now());
|
||||
return defRepository.save(def);
|
||||
@@ -159,9 +158,7 @@ public class FlowDefAdminService {
|
||||
throw new BizException(ErrorCode.PARAM_ERROR, "仅停用的流程可启用;草稿请走发布");
|
||||
}
|
||||
FlowDefModel model = FlowDefModel.parse(objectMapper, def.getContentJson());
|
||||
Set<String> enabledDeptKeys = mapRepository.findEnabledBySchId(schId).stream()
|
||||
.map(FlwAssigneeMap::getDeptKey).collect(Collectors.toSet());
|
||||
DefValidator.validate(model, enabledDeptKeys);
|
||||
DefValidator.validate(model, enabledDeptKeys(schId), enabledRouterKeys());
|
||||
def.setStatus(FlwEnums.DefStatus.PUBLISHED.getCode());
|
||||
def.setPublishTime(LocalDateTime.now());
|
||||
return defRepository.save(def);
|
||||
@@ -172,6 +169,18 @@ public class FlowDefAdminService {
|
||||
return defRepository.findBySchIdAndFlowGuidOrderByVersionDesc(schId, flowGuid);
|
||||
}
|
||||
|
||||
private Set<String> enabledDeptKeys(Long schId) {
|
||||
return mapRepository.findEnabledBySchId(schId).stream()
|
||||
.map(FlwAssigneeMap::getDeptKey).collect(Collectors.toSet());
|
||||
}
|
||||
|
||||
/** CONDITION routerKey 发布校验白名单(flw_router 启用键全集,全局不分校) */
|
||||
private Set<String> enabledRouterKeys() {
|
||||
return routerRepository.findByStatusOrderByIdAsc(1).stream()
|
||||
.map(com.yunhai.flowcenter.entity.FlwRouter::getRouterKey)
|
||||
.collect(Collectors.toSet());
|
||||
}
|
||||
|
||||
private FlwDef requireDef(Long schId, Long id) {
|
||||
FlwDef def = defRepository.findById(id)
|
||||
.orElseThrow(() -> new BizException(ErrorCode.PARAM_ERROR, "流程定义不存在:" + id));
|
||||
|
||||
@@ -3,14 +3,18 @@ package com.yunhai.flowcenter.service;
|
||||
import com.yunhai.flowcenter.engine.FlowDefModel;
|
||||
import com.yunhai.flowcenter.engine.FlowEngine;
|
||||
import com.yunhai.flowcenter.entity.FlwAssigneeMap;
|
||||
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.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.openapi.base.OpenApiUserClient;
|
||||
@@ -51,6 +55,8 @@ public class FlowDiagramService {
|
||||
private final FlwAssigneeMapRepository mapRepository;
|
||||
private final FlwInstanceRepository instanceRepository;
|
||||
private final FlwTaskRepository taskRepository;
|
||||
private final FlwCcRepository ccRepository;
|
||||
private final FlwRouteLogRepository routeLogRepository;
|
||||
private final FlowEngine flowEngine;
|
||||
private final OpenApiUserClient userClient;
|
||||
|
||||
@@ -186,6 +192,18 @@ public class FlowDiagramService {
|
||||
states.put(nodeId, "active");
|
||||
}
|
||||
}
|
||||
// 抄送节点(NOTIFY 不建任务,上面的任务循环覆盖不到它):有 flw_cc 落库行即视为已过
|
||||
// (瞬时完成,永不为 active——它不在 currentNodes 里)
|
||||
for (FlwCc c : ccRepository.findByInstanceIdOrderByIdAsc(instanceId)) {
|
||||
states.put(c.getNodeId(), "done");
|
||||
}
|
||||
// 条件分支节点(不建任务):有命中留痕(outlet 非空)即视为已过;
|
||||
// 挂起在 currentNodes → active 优先(退回重走已重新占位,不能被上一轮的命中留痕盖掉)
|
||||
for (FlwRouteLog r : routeLogRepository.findByInstanceIdOrderByIdAsc(instanceId)) {
|
||||
if (r.getOutlet() != null && !activeNodes.contains(r.getNodeId())) {
|
||||
states.put(r.getNodeId(), "done");
|
||||
}
|
||||
}
|
||||
return states;
|
||||
}
|
||||
|
||||
@@ -195,6 +213,25 @@ public class FlowDiagramService {
|
||||
vo.setType(n.type);
|
||||
vo.setCountersign(n.countersign);
|
||||
vo.setDeadlineHours(n.deadlineHours);
|
||||
if (FlowDefModel.TYPE_NOTIFY.equals(n.type)) {
|
||||
// 抄送卡:label=节点名(缺省"抄送"),subLabel=接收人名单(dept 视图不列姓名,同 USERS 纪律)
|
||||
vo.setLabel(firstNonBlank(n.name, "抄送"));
|
||||
FlowDefModel.Assignee r = n.receivers;
|
||||
if (r == null) {
|
||||
return vo;
|
||||
}
|
||||
vo.setAssigneeType(r.type);
|
||||
vo.setDeptKey(r.deptKey);
|
||||
if (FlowDefModel.ASSIGNEE_DEPT_KEY.equals(r.type)) {
|
||||
List<FlwAssigneeMap> maps = mapRepository.findEnabled(schId, r.deptKey);
|
||||
vo.setSubLabel(MODE_DEPT.equals(mode) ? "抄送科室 · " + maps.size() + "人"
|
||||
: "抄送 " + namesWithSuffix(memberNames(maps), new ArrayList<>()));
|
||||
} else if (FlowDefModel.ASSIGNEE_USERS.equals(r.type)) {
|
||||
vo.setSubLabel(MODE_DEPT.equals(mode) ? "抄送指定人员"
|
||||
: "抄送 " + namesWithSuffix(userNames(r.userIds), new ArrayList<>()));
|
||||
}
|
||||
return vo;
|
||||
}
|
||||
if (!FlowDefModel.TYPE_APPROVAL.equals(n.type)) {
|
||||
vo.setLabel(fixedLabel(n.type));
|
||||
return vo;
|
||||
@@ -313,6 +350,10 @@ public class FlowDiagramService {
|
||||
return "并行分发";
|
||||
case FlowDefModel.TYPE_PARALLEL_JOIN:
|
||||
return "汇聚等待";
|
||||
case FlowDefModel.TYPE_NOTIFY:
|
||||
return "抄送";
|
||||
case FlowDefModel.TYPE_CONDITION:
|
||||
return "条件分支";
|
||||
default:
|
||||
return type;
|
||||
}
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -0,0 +1,130 @@
|
||||
package com.yunhai.flowcenter.service;
|
||||
|
||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
import com.yunhai.flowcenter.engine.FlowDefModel;
|
||||
import com.yunhai.flowcenter.engine.FlowEngine;
|
||||
import com.yunhai.flowcenter.entity.FlwDef;
|
||||
import com.yunhai.flowcenter.entity.FlwInstance;
|
||||
import com.yunhai.flowcenter.entity.FlwRouter;
|
||||
import com.yunhai.flowcenter.enums.FlwEnums;
|
||||
import com.yunhai.flowcenter.repository.FlwDefRepository;
|
||||
import com.yunhai.flowcenter.repository.FlwInstanceRepository;
|
||||
import com.yunhai.flowcenter.repository.FlwRouterRepository;
|
||||
import com.yunhai.platform.apistd.BizException;
|
||||
import com.yunhai.platform.utils.exception.ErrorCode;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import org.springframework.stereotype.Service;
|
||||
import org.springframework.transaction.annotation.Transactional;
|
||||
import org.springframework.util.StringUtils;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* 条件路由管理(2026-09-24 §4.2/§4.4):flw_router 注册 CRUD(全局表,管理员维护)
|
||||
* + 挂起办件的重跑路由入口。发布校验引用本表(routerKey 白名单),停用注册会拦住新发布。
|
||||
*/
|
||||
@Service
|
||||
@RequiredArgsConstructor
|
||||
public class FlowRouterAdminService {
|
||||
|
||||
private final FlwRouterRepository routerRepository;
|
||||
private final FlwInstanceRepository instanceRepository;
|
||||
private final FlwDefRepository defRepository;
|
||||
private final FlowEngine flowEngine;
|
||||
private final ObjectMapper objectMapper;
|
||||
|
||||
/** 全量注册(编辑器下拉与管理页共用;表小不分页) */
|
||||
public List<FlwRouter> list() {
|
||||
return routerRepository.findAll();
|
||||
}
|
||||
|
||||
/** 保存(id 空=新建;routerKey 唯一,改键时校验不撞已有注册) */
|
||||
@Transactional
|
||||
public FlwRouter save(Long id, String routerKey, String name, String serviceKey, String path,
|
||||
String basePath) {
|
||||
if (isBlank(routerKey) || isBlank(name) || isBlank(serviceKey) || isBlank(path)) {
|
||||
throw new BizException(ErrorCode.PARAM_ERROR, "routerKey/name/serviceKey/path 必填");
|
||||
}
|
||||
if (!path.trim().startsWith("/")) {
|
||||
throw new BizException(ErrorCode.PARAM_ERROR, "path 必须以 / 开头(如 /oapi/notice/flow/route/dept-judge)");
|
||||
}
|
||||
FlwRouter router;
|
||||
if (id == null) {
|
||||
router = new FlwRouter();
|
||||
} else {
|
||||
router = routerRepository.findById(id)
|
||||
.orElseThrow(() -> new BizException(ErrorCode.PARAM_ERROR, "注册不存在:" + id));
|
||||
}
|
||||
routerRepository.findByRouterKey(routerKey.trim()).ifPresent(other -> {
|
||||
if (id == null || !other.getId().equals(id)) {
|
||||
throw new BizException(ErrorCode.PARAM_ERROR, "routerKey 已被注册:" + routerKey.trim());
|
||||
}
|
||||
});
|
||||
router.setRouterKey(routerKey.trim());
|
||||
router.setName(name.trim());
|
||||
router.setServiceKey(serviceKey.trim());
|
||||
router.setPath(path.trim());
|
||||
router.setBasePath(basePath == null ? "" : basePath.trim());
|
||||
router.setStatus(1);
|
||||
return routerRepository.save(router);
|
||||
}
|
||||
|
||||
/** 启用/停用(停用拦新发布引用;存量在途办件重跑会失败挂起,注意时机) */
|
||||
@Transactional
|
||||
public FlwRouter setStatus(Long id, boolean enabled) {
|
||||
FlwRouter router = routerRepository.findById(id)
|
||||
.orElseThrow(() -> new BizException(ErrorCode.PARAM_ERROR, "注册不存在:" + id));
|
||||
router.setStatus(enabled ? 1 : 0);
|
||||
return routerRepository.save(router);
|
||||
}
|
||||
|
||||
/**
|
||||
* 重跑路由(挂起恢复唯一入口,§4.4):办件须在途且挂在 CONDITION 节点(currentNodes 含之、
|
||||
* 该节点无待办任务)。重新调判断,成功即续走推进;仍失败抛业务错(挂起不变,留痕已落新行)。
|
||||
*/
|
||||
@Transactional
|
||||
public void reroute(Long schId, Long instanceId) {
|
||||
FlwInstance instance = instanceRepository.findById(instanceId)
|
||||
.orElseThrow(() -> new BizException(ErrorCode.PARAM_ERROR, "办件不存在:" + instanceId));
|
||||
if (!instance.getSchId().equals(schId)) {
|
||||
throw new BizException(ErrorCode.FORBIDDEN, "办件不属于该学校");
|
||||
}
|
||||
if (instance.getStatus() != FlwEnums.InstanceStatus.APPROVING.getCode()) {
|
||||
throw new BizException(ErrorCode.PARAM_ERROR, "仅审批中的办件可重跑路由");
|
||||
}
|
||||
FlwDef def = defRepository.findById(instance.getDefId()).orElse(null);
|
||||
if (def == null) {
|
||||
throw new BizException(ErrorCode.PARAM_ERROR, "办件定义缺失");
|
||||
}
|
||||
String conditionNode = findParkedCondition(instance, def);
|
||||
if (conditionNode == null) {
|
||||
throw new BizException(ErrorCode.PARAM_ERROR,
|
||||
"办件未挂在条件分支节点(无挂起的路由判断,无需重跑)");
|
||||
}
|
||||
flowEngine.rerouteCondition(instance, def, conditionNode);
|
||||
instanceRepository.save(instance);
|
||||
}
|
||||
|
||||
/** 在途节点里找 CONDITION(多个时取第一个;正常挂起只会有一个) */
|
||||
private String findParkedCondition(FlwInstance instance, FlwDef def) {
|
||||
String cur = instance.getCurrentNodes();
|
||||
if (StringUtils.isEmpty(cur)) {
|
||||
return null;
|
||||
}
|
||||
FlowDefModel model = FlowDefModel.parse(objectMapper, def.getContentJson());
|
||||
for (String nodeId : cur.split(",")) {
|
||||
if (nodeId.isEmpty()) {
|
||||
continue;
|
||||
}
|
||||
FlowDefModel.Node node = model.node(nodeId);
|
||||
if (node != null && FlowDefModel.TYPE_CONDITION.equals(node.type)) {
|
||||
return node.id;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
private static boolean isBlank(String s) {
|
||||
return s == null || s.trim().isEmpty();
|
||||
}
|
||||
}
|
||||
@@ -3,6 +3,7 @@ package com.yunhai.flowcenter.service;
|
||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
import com.yunhai.flowcenter.callback.CallbackAppSigner;
|
||||
import com.yunhai.flowcenter.entity.FlwCallbackReg;
|
||||
import com.yunhai.flowcenter.entity.FlwCc;
|
||||
import com.yunhai.flowcenter.entity.FlwDef;
|
||||
import com.yunhai.flowcenter.entity.FlwTask;
|
||||
import com.yunhai.flowcenter.entity.FlwInstance;
|
||||
@@ -10,6 +11,7 @@ import com.yunhai.flowcenter.engine.FlowEngine;
|
||||
import com.yunhai.flowcenter.enums.FlwEnums;
|
||||
import com.yunhai.flowcenter.model.OapiModels;
|
||||
import com.yunhai.flowcenter.repository.FlwCallbackRegRepository;
|
||||
import com.yunhai.flowcenter.repository.FlwCcRepository;
|
||||
import com.yunhai.flowcenter.repository.FlwDefRepository;
|
||||
import com.yunhai.flowcenter.repository.FlwInstanceRepository;
|
||||
import com.yunhai.flowcenter.repository.FlwTaskRepository;
|
||||
@@ -62,6 +64,7 @@ public class OapiFlowService {
|
||||
private final FlwDefRepository defRepository;
|
||||
private final FlwInstanceRepository instanceRepository;
|
||||
private final FlwTaskRepository taskRepository;
|
||||
private final FlwCcRepository ccRepository;
|
||||
private final FlwCallbackRegRepository regRepository;
|
||||
private final FlowInstanceService instanceService;
|
||||
private final FlowTaskService taskService;
|
||||
@@ -352,11 +355,17 @@ public class OapiFlowService {
|
||||
boolean hasSubmitRow = rows.stream()
|
||||
.anyMatch(t -> FlwEnums.TaskStatus.NODE_SUBMIT.equals(t.getNodeId()));
|
||||
List<OapiModels.ProgressStepVo> steps = new ArrayList<>();
|
||||
// 步骤发生时间(排序键):任务步=办理人行最晚时间(handleTime?:arriveTime),submit/事件步=动作时间,
|
||||
// 抄送步=最晚 sentTime——抄送步与任务步按真实时间交织,人读时间线不出现"抄送悬在最后"。
|
||||
Map<OapiModels.ProgressStepVo, java.time.LocalDateTime> stepTimes =
|
||||
new java.util.IdentityHashMap<>();
|
||||
// 第 0 步"发起申请"(对外时间线图3/图4口径):2026-09-18 起发起会留痕成真实任务行,
|
||||
// 有真实行时由下面的循环出这一步(可见内容与这里的合成步一致);老办件没有真实行,
|
||||
// 仍按办件快照合成,存量数据对外表现不变。
|
||||
if (!hasSubmitRow) {
|
||||
steps.add(submitStep(instance));
|
||||
OapiModels.ProgressStepVo submit = submitStep(instance);
|
||||
steps.add(submit);
|
||||
stepTimes.put(submit, instance.getSubmitTime());
|
||||
}
|
||||
// 轨迹留痕事件行(6 发起/重新提交、7 撤回)单独成步;步骤 label 直接用事件语义,
|
||||
// 不参与下面的"任一拒绝>任一同意>全失效"汇总(否则会被判成"失效")。
|
||||
@@ -370,6 +379,7 @@ public class OapiFlowService {
|
||||
OapiModels.ProgressStepVo es = eventStep(t);
|
||||
eventSteps.add(es);
|
||||
steps.add(es);
|
||||
stepTimes.put(es, t.getHandleTime() != null ? t.getHandleTime() : t.getArriveTime());
|
||||
continue;
|
||||
}
|
||||
OapiModels.ProgressStepVo step = stepsByNode.computeIfAbsent(t.getNodeId(), k -> {
|
||||
@@ -389,11 +399,77 @@ public class OapiFlowService {
|
||||
}
|
||||
s.setActive(activeNodes.contains(s.getNodeId()));
|
||||
s.setLabel(s.isActive() ? "待审核" : stepLabel(s.getAssignees()));
|
||||
java.time.LocalDateTime latest = null;
|
||||
for (OapiModels.ProgressTaskVo r : s.getAssignees()) {
|
||||
java.time.LocalDateTime t = r.getHandleTime() != null
|
||||
? java.time.LocalDateTime.parse(r.getHandleTime())
|
||||
: (r.getArriveTime() == null ? null : java.time.LocalDateTime.parse(r.getArriveTime()));
|
||||
if (t != null && (latest == null || t.isAfter(latest))) {
|
||||
latest = t;
|
||||
}
|
||||
}
|
||||
stepTimes.put(s, latest);
|
||||
}
|
||||
// 抄送步(有才交织排序):无抄送时保持既有拼装顺序原样输出(存量办件出参零变化);
|
||||
// 有抄送时按 (发生时间, 秩) 排——既有步=0、抄送步=1(同秒 DATETIME 分不出先后,任务步
|
||||
// 恒在前;不给既有步分秩是因为驳回/重提步同秒靠 id 定序即真实先后,秩会颠倒)。
|
||||
List<OapiModels.ProgressStepVo> ccStepList = ccSteps(instance.getId(), stepTimes);
|
||||
if (!ccStepList.isEmpty()) {
|
||||
Map<OapiModels.ProgressStepVo, Integer> ranks = new java.util.IdentityHashMap<>();
|
||||
for (OapiModels.ProgressStepVo s : steps) {
|
||||
ranks.put(s, 0);
|
||||
}
|
||||
for (OapiModels.ProgressStepVo s : ccStepList) {
|
||||
ranks.put(s, 1);
|
||||
}
|
||||
steps.addAll(ccStepList);
|
||||
steps.sort(java.util.Comparator
|
||||
.comparing((OapiModels.ProgressStepVo s) -> stepTimes.get(s),
|
||||
java.util.Comparator.nullsLast(java.util.Comparator.naturalOrder()))
|
||||
.thenComparing(s -> ranks.get(s)));
|
||||
}
|
||||
vo.setSteps(steps);
|
||||
return vo;
|
||||
}
|
||||
|
||||
/**
|
||||
* 抄送步(2026-09-23 NOTIFY 节点):按 nodeId 聚合(多人并排),label="抄送"、
|
||||
* 参与行 status=1/已抄送(复用"已办过"取值,消费方无需适配新状态);时间取 sentTime,
|
||||
* 与任务步按真实发生时间交织排序。无抄送返回空列表(存量办件出参零变化)。
|
||||
*/
|
||||
private List<OapiModels.ProgressStepVo> ccSteps(Long instanceId,
|
||||
Map<OapiModels.ProgressStepVo, java.time.LocalDateTime> stepTimes) {
|
||||
List<FlwCc> ccs = ccRepository.findByInstanceIdOrderByIdAsc(instanceId);
|
||||
if (ccs.isEmpty()) {
|
||||
return new ArrayList<>();
|
||||
}
|
||||
Map<String, OapiModels.ProgressStepVo> byNode = new java.util.LinkedHashMap<>();
|
||||
for (FlwCc c : ccs) {
|
||||
OapiModels.ProgressStepVo step = byNode.computeIfAbsent(c.getNodeId(), k -> {
|
||||
OapiModels.ProgressStepVo s = new OapiModels.ProgressStepVo();
|
||||
s.setNodeId(c.getNodeId());
|
||||
s.setNodeName(c.getNodeName() == null || c.getNodeName().isEmpty() ? "抄送" : c.getNodeName());
|
||||
s.setActive(false);
|
||||
s.setLabel("抄送");
|
||||
s.setAssignees(new ArrayList<>());
|
||||
stepTimes.put(s, c.getSentTime());
|
||||
return s;
|
||||
});
|
||||
OapiModels.ProgressTaskVo row = new OapiModels.ProgressTaskVo();
|
||||
row.setUserId(c.getUserId());
|
||||
row.setUserName(c.getUserName());
|
||||
row.setStatus(1);
|
||||
row.setStatusLabel("已抄送");
|
||||
row.setArriveTime(c.getSentTime() == null ? null : c.getSentTime().toString());
|
||||
step.getAssignees().add(row);
|
||||
java.time.LocalDateTime t = stepTimes.get(step);
|
||||
if (c.getSentTime() != null && (t == null || c.getSentTime().isAfter(t))) {
|
||||
stepTimes.put(step, c.getSentTime());
|
||||
}
|
||||
}
|
||||
return new ArrayList<>(byNode.values());
|
||||
}
|
||||
|
||||
/** 合成"发起申请"步(老办件:无真实留痕行,取办件快照的提交人/提交时间) */
|
||||
private OapiModels.ProgressStepVo submitStep(FlwInstance instance) {
|
||||
OapiModels.ProgressStepVo submit = new OapiModels.ProgressStepVo();
|
||||
@@ -436,7 +512,7 @@ public class OapiFlowService {
|
||||
return step;
|
||||
}
|
||||
|
||||
/** 任务行 → 对外步骤里的办理人行 */
|
||||
/** 任务行 → 对外步骤里的办理人行(展示时间截断到秒:列已升毫秒精度,对外口径不变) */
|
||||
private OapiModels.ProgressTaskVo taskVo(FlwTask t) {
|
||||
OapiModels.ProgressTaskVo row = new OapiModels.ProgressTaskVo();
|
||||
row.setTaskId(t.getId());
|
||||
@@ -445,8 +521,10 @@ public class OapiFlowService {
|
||||
row.setStatus(t.getStatus());
|
||||
row.setStatusLabel(taskLabel(t.getStatus()));
|
||||
row.setOpinion(t.getOpinion());
|
||||
row.setArriveTime(t.getArriveTime() == null ? null : t.getArriveTime().toString());
|
||||
row.setHandleTime(t.getHandleTime() == null ? null : t.getHandleTime().toString());
|
||||
row.setArriveTime(t.getArriveTime() == null ? null
|
||||
: t.getArriveTime().truncatedTo(java.time.temporal.ChronoUnit.SECONDS).toString());
|
||||
row.setHandleTime(t.getHandleTime() == null ? null
|
||||
: t.getHandleTime().truncatedTo(java.time.temporal.ChronoUnit.SECONDS).toString());
|
||||
row.setEdgeName(t.getEdgeName());
|
||||
return row;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user