build / build-push (push) Successful in 1m43s
- 详情出参新增 bizUrlAbs:biz_url 裸相对路径按 applyAppCode(by-code)→applyAppKey(by-key) 查应用注册表 pc_url 拼 base#path(口径同 UserHome 消息中心 jumpUrl),查不到为 null - 缓存 5min/未命中 60s,不阻断详情接口;biz_url 原值保留给 bare 态父页面接管
572 lines
28 KiB
Java
572 lines
28 KiB
Java
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.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.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;
|
||
import com.yunhai.platform.utils.model.BasePageResult;
|
||
import lombok.RequiredArgsConstructor;
|
||
import org.springframework.data.domain.Page;
|
||
import org.springframework.data.domain.PageRequest;
|
||
import org.springframework.stereotype.Service;
|
||
|
||
import java.time.LocalDateTime;
|
||
import java.util.ArrayList;
|
||
import java.util.Comparator;
|
||
import java.util.HashMap;
|
||
import java.util.List;
|
||
import java.util.Map;
|
||
import java.util.stream.Collectors;
|
||
|
||
/**
|
||
* 用户面查询(我的办件/待办/已办/办件详情)。
|
||
*/
|
||
@Service
|
||
@RequiredArgsConstructor
|
||
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 FlowDeptAdminService deptAdminService;
|
||
private final FlowEngine flowEngine;
|
||
private final BizUrlResolver bizUrlResolver;
|
||
|
||
public BasePageResult<FlowModels.InstanceItem> pageMy(Long schId, Integer submitterId,
|
||
Integer status, String appKey, Integer page, Integer size) {
|
||
Page<FlwInstance> p = instanceRepository.pageMy(schId, submitterId, status, emptyToNull(appKey),
|
||
PageRequest.of(normPage(page), normSize(size)));
|
||
return toResult(p.getTotalElements(), p.getContent().stream().map(this::toInstanceItem)
|
||
.collect(Collectors.toList()), page, size);
|
||
}
|
||
|
||
/** 抄送我的(NOTIFY 落库行,sent_time 倒序;知会不占待办,独立于 todo/done 口径) */
|
||
public BasePageResult<FlowModels.CcItem> pageCc(Long schId, Integer userId,
|
||
String appKey, Integer page, Integer size) {
|
||
Page<FlwCc> p = ccRepository.pageCc(schId, userId, emptyToNull(appKey),
|
||
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, String appKey,
|
||
Integer page, Integer size) {
|
||
Page<FlwTask> p = taskRepository.pageTodo(schId, assigneeId, emptyToNull(appKey),
|
||
PageRequest.of(normPage(page), normSize(size)));
|
||
List<FlowModels.TaskItem> items = p.getContent().stream()
|
||
.map(this::toTaskItem).collect(Collectors.toList());
|
||
fillInstanceInfo(items);
|
||
return toResult(p.getTotalElements(), items, page, size);
|
||
}
|
||
|
||
/** 已办:一个办件一行(保留本人最后办理的那条),并回填累计办理次数 */
|
||
public BasePageResult<FlowModels.TaskItem> pageDone(Long schId, Integer assigneeId, String appKey,
|
||
Integer page, Integer size) {
|
||
List<Integer> handled = FlwEnums.TaskStatus.handledCodes();
|
||
Page<FlwTask> p = taskRepository.pageDone(schId, assigneeId, handled, emptyToNull(appKey),
|
||
PageRequest.of(normPage(page), normSize(size)));
|
||
List<FlowModels.TaskItem> items = p.getContent().stream()
|
||
.map(this::toTaskItem).collect(Collectors.toList());
|
||
fillInstanceInfo(items);
|
||
fillDoneCount(items, assigneeId, handled);
|
||
return toResult(p.getTotalElements(), items, page, size);
|
||
}
|
||
|
||
/** 已办行的累计办理次数(同一办件多节点办理/退回重办时的"共办理 N 次") */
|
||
private void fillDoneCount(List<FlowModels.TaskItem> items, Integer assigneeId,
|
||
List<Integer> handled) {
|
||
if (items.isEmpty()) {
|
||
return;
|
||
}
|
||
List<Long> ids = items.stream().map(FlowModels.TaskItem::getInstanceId)
|
||
.distinct().collect(Collectors.toList());
|
||
Map<Long, Integer> counts = new HashMap<>();
|
||
for (Object[] row : taskRepository.countDoneByInstance(ids, assigneeId, handled)) {
|
||
counts.put((Long) row[0], ((Number) row[1]).intValue());
|
||
}
|
||
for (FlowModels.TaskItem item : items) {
|
||
item.setDoneCount(counts.getOrDefault(item.getInstanceId(), 1));
|
||
}
|
||
}
|
||
|
||
/**
|
||
* 办件详情:快照 + 任务轨迹 + 分支进度 + 本人待办标记(办理页/详情页共用数据源)。
|
||
* viewerId 必须由控制器从 BoaUser.getId() 传入(CAS 会话 LoginUser.userId 是登录名字符串,
|
||
* 不是数字主键,不能直接转 Integer——2026-09-12 全链测试修复)。
|
||
*/
|
||
public FlowModels.InstanceDetail detail(Long schId, Long instanceId, Integer viewerId) {
|
||
FlwInstance instance = instanceRepository.findById(instanceId)
|
||
.orElseThrow(() -> new BizException(ErrorCode.PARAM_ERROR, "办件不存在"));
|
||
if (!instance.getSchId().equals(schId)) {
|
||
throw new BizException(ErrorCode.FORBIDDEN, "办件不属于该学校");
|
||
}
|
||
FlowModels.InstanceDetail detail = new FlowModels.InstanceDetail();
|
||
detail.setInstanceId(instance.getId());
|
||
detail.setSchId(instance.getSchId());
|
||
detail.setDefCode(instance.getDefCode());
|
||
detail.setDefVersion(instance.getDefVersion());
|
||
detail.setBizType(instance.getBizType());
|
||
detail.setBizId(instance.getBizId());
|
||
detail.setBizTitle(instance.getBizTitle());
|
||
detail.setBizUrl(instance.getBizUrl());
|
||
detail.setBizUrlAbs(bizUrlResolver.resolve(
|
||
instance.getApplyAppCode(), instance.getApplyAppKey(), instance.getBizUrl()));
|
||
detail.setBizSnapshot(instance.getBizSnapshot());
|
||
detail.setFormDataJson(instance.getFormDataJson());
|
||
detail.setStatus(instance.getStatus());
|
||
detail.setCurrentNodes(instance.getCurrentNodes());
|
||
detail.setSubmitterId(instance.getSubmitterId());
|
||
detail.setSubmitterName(instance.getSubmitterName());
|
||
detail.setSubmitTime(str(instance.getSubmitTime()));
|
||
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, ccs, routeLogs, model));
|
||
List<Map<String, Object>> branches = new ArrayList<>();
|
||
for (FlwBranch b : branchRepository.findByInstanceId(instanceId)) {
|
||
Map<String, Object> row = new HashMap<>();
|
||
row.put("branchKey", b.getBranchKey());
|
||
row.put("status", b.getStatus());
|
||
branches.add(row);
|
||
}
|
||
detail.setBranches(branches);
|
||
List<Long> myPending = new ArrayList<>();
|
||
if (viewerId != null) {
|
||
for (FlwTask t : tasks) {
|
||
if (viewerId.equals(t.getAssigneeId())
|
||
&& t.getStatus() == FlwEnums.TaskStatus.PENDING.getCode()) {
|
||
myPending.add(t.getId());
|
||
}
|
||
}
|
||
detail.setMyPendingTask(!myPending.isEmpty());
|
||
detail.setMyPendingTaskIds(myPending);
|
||
}
|
||
detail.setNodeSummary(def == null ? "" : FlowEngine.nodeSummary(model));
|
||
// 办理面板数据(出口/退回目标/移交标记):仅对持本人待办的查看者填充
|
||
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) {
|
||
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;
|
||
}
|
||
|
||
/** 任务定位办件详情(办理页 /task/{taskId} 数据源:UserMsg jumpPath 只带 taskId) */
|
||
public FlowModels.InstanceDetail taskDetail(Long schId, Long taskId, Integer viewerId) {
|
||
FlwTask task = taskRepository.findById(taskId)
|
||
.orElseThrow(() -> new BizException(ErrorCode.PARAM_ERROR, "任务不存在:" + taskId));
|
||
if (!task.getSchId().equals(schId)) {
|
||
throw new BizException(ErrorCode.FORBIDDEN, "任务不属于该学校");
|
||
}
|
||
return detail(schId, task.getInstanceId(), viewerId);
|
||
}
|
||
|
||
private FlowModels.InstanceItem toInstanceItem(FlwInstance instance) {
|
||
FlowModels.InstanceItem item = new FlowModels.InstanceItem();
|
||
item.setInstanceId(instance.getId());
|
||
item.setBizType(instance.getBizType());
|
||
item.setBizId(instance.getBizId());
|
||
item.setBizTitle(instance.getBizTitle());
|
||
item.setStatus(instance.getStatus());
|
||
item.setCurrentNodes(instance.getCurrentNodes());
|
||
item.setSubmitterName(instance.getSubmitterName());
|
||
item.setSubmitTime(str(instance.getSubmitTime()));
|
||
item.setFinishTime(str(instance.getFinishTime()));
|
||
item.setDefVersion(instance.getDefVersion());
|
||
return item;
|
||
}
|
||
|
||
/**
|
||
* 流程轨迹(2026-09-18 重排为"时间流"):审批任务行 + 留痕事件行(6 发起/重新提交、7 撤回)
|
||
* 合成一条按时间升序的轨迹,替掉原先"按 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,抄送=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}、抄送行 cc{id}、合成行 submit —— taskId 可能为 null 不能当 key)。</li>
|
||
* </ul>
|
||
* 科室口径(视图两档:dept/user)另需 assigneeType/deptKey/deptName —— 只有 DEPT_KEY 节点有
|
||
* 科室概念,故仅对该类节点回填;节点在定义里找不到(重提后老节点、留痕事件行)不回填,
|
||
* 前端回落人名显示。
|
||
*
|
||
* <p>老办件(改造前发起)没有"发起"留痕行,按办件快照的 submitTime/submitterName 合成一条
|
||
* 兜底插入到时间流对应位置,保证轨迹首条仍是"发起申请"。
|
||
*/
|
||
private List<Map<String, Object>> buildTrace(FlwInstance instance, List<FlwTask> tasks,
|
||
List<FlwCc> ccs, List<FlwRouteLog> routeLogs,
|
||
FlowDefModel model) {
|
||
// 任务行、判断行与抄送行同场按发生时间排序:时间只到秒(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, model, instance, deptNames)));
|
||
}
|
||
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 (TimedRow r : rows) {
|
||
trace.add(r.row);
|
||
// 老办件兜底行的插入位:时间早于提交时间的行数(老办件里恒为 0;重提过的老办件按时间放在中段)
|
||
if (submitTime != null && r.time != null && r.time.isBefore(submitTime)) {
|
||
headIndex = trace.size();
|
||
}
|
||
}
|
||
if (!hasSubmitRow(tasks)) {
|
||
trace.add(headIndex, syntheticSubmitRow(instance));
|
||
}
|
||
int round = 1;
|
||
for (Map<String, Object> row : trace) {
|
||
if ("RESUBMIT".equals(row.get("kind"))) {
|
||
round++;
|
||
}
|
||
row.put("round", round);
|
||
}
|
||
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* 字段(前端复用人列)。
|
||
* 2026-10-12 补科室口径:抄送节点用 receivers,走 {@link #fillCcScope} 富化 assigneeType,
|
||
* 前端 dept 档据此显科室名 / "抄送对象",不再回落接收人真实姓名。 */
|
||
private Map<String, Object> ccTraceRow(FlwCc c, FlowDefModel model, FlwInstance instance,
|
||
Map<String, String> deptNames) {
|
||
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()));
|
||
fillCcScope(row, model, c.getNodeId(), instance.getSchId(), deptNames);
|
||
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()));
|
||
// 议题三②:调试弹窗「从路由日志导入」需要看失败摘要(>500 截断)
|
||
row.put("rawResponse", r.getRawResponse() == null ? "" :
|
||
(r.getRawResponse().length() > 500 ? r.getRawResponse().substring(0, 500) + "…" : r.getRawResponse()));
|
||
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())) {
|
||
return true;
|
||
}
|
||
}
|
||
return false;
|
||
}
|
||
|
||
/** 老办件兜底:改造前发起不留痕,用快照补一条"发起申请"(taskId 为空,前端按 rowKey 去重) */
|
||
private static Map<String, Object> syntheticSubmitRow(FlwInstance instance) {
|
||
Map<String, Object> row = new HashMap<>();
|
||
row.put("rowKey", "submit");
|
||
row.put("taskId", null);
|
||
row.put("kind", "SUBMIT");
|
||
row.put("nodeId", FlwEnums.TaskStatus.NODE_SUBMIT);
|
||
row.put("nodeName", "发起申请");
|
||
row.put("assigneeId", instance.getSubmitterId());
|
||
row.put("assigneeName", instance.getSubmitterName());
|
||
row.put("status", FlwEnums.TaskStatus.SUBMITTED.getCode());
|
||
row.put("time", str(instance.getSubmitTime()));
|
||
row.put("synthetic", true);
|
||
return row;
|
||
}
|
||
|
||
/** 该行实际发生的时间:办结过看 handleTime,否则看 arriveTime */
|
||
private static LocalDateTime happenedAt(FlwTask t) {
|
||
return t.getHandleTime() != null ? t.getHandleTime() : t.getArriveTime();
|
||
}
|
||
|
||
private static String kindOf(FlwTask t) {
|
||
int s = t.getStatus() == null ? FlwEnums.TaskStatus.PENDING.getCode() : t.getStatus();
|
||
if (s == FlwEnums.TaskStatus.SUBMITTED.getCode()) {
|
||
return FlwEnums.TaskStatus.NODE_RESUBMIT.equals(t.getNodeId()) ? "RESUBMIT" : "SUBMIT";
|
||
}
|
||
if (s == FlwEnums.TaskStatus.WITHDRAWN.getCode()) {
|
||
return "WITHDRAW";
|
||
}
|
||
if (s == FlwEnums.TaskStatus.APPROVED.getCode()) {
|
||
return "APPROVE";
|
||
}
|
||
if (s == FlwEnums.TaskStatus.REJECTED.getCode()) {
|
||
return "REJECT";
|
||
}
|
||
if (s == FlwEnums.TaskStatus.INVALIDATED.getCode()) {
|
||
return "INVALID";
|
||
}
|
||
if (s == FlwEnums.TaskStatus.RETURNED.getCode()) {
|
||
return "RETURN";
|
||
}
|
||
if (s == FlwEnums.TaskStatus.TRANSFERRED.getCode()) {
|
||
return "TRANSFER";
|
||
}
|
||
return "PENDING";
|
||
}
|
||
|
||
/**
|
||
* 科室口径富化:节点是 DEPT_KEY(科室办理)时带上 deptKey + 映射里的科室名,
|
||
* 前端"按科室显示"据此把人名换成科室名;USERS(指定人员)节点只带 assigneeType,
|
||
* 由前端显示"指定办理人"(口径同示意图的科室视图)。
|
||
*/
|
||
private void fillAssigneeScope(Map<String, Object> row, FlowDefModel model, String nodeId,
|
||
Long schId, Map<String, String> deptNames) {
|
||
FlowDefModel.Node node = model == null ? null : model.node(nodeId);
|
||
FlowDefModel.Assignee assignee = node == null ? null : node.assignee;
|
||
applyAssigneeScope(row, assignee, schId, deptNames);
|
||
}
|
||
|
||
/**
|
||
* 抄送行(kind=CC)科室口径富化(2026-10-12):NOTIFY 节点的接收人存在 {@code receivers}
|
||
* 字段而非 {@code assignee},故不能直接复用 {@link #fillAssigneeScope}——否则取到的 assignee
|
||
* 恒为 null,dept 档下抄送行只能回落接收人真实姓名(原先的泄露点)。
|
||
* 节点在定义里找不到(重提后的老节点、已删节点)不填,前端统一显示"抄送对象"。
|
||
*/
|
||
private void fillCcScope(Map<String, Object> row, FlowDefModel model, String nodeId,
|
||
Long schId, Map<String, String> deptNames) {
|
||
FlowDefModel.Node node = model == null ? null : model.node(nodeId);
|
||
FlowDefModel.Assignee receivers = node == null ? null : node.receivers;
|
||
applyAssigneeScope(row, receivers, schId, deptNames);
|
||
}
|
||
|
||
/** 两种口径的公共落值:assigneeType 必填;DEPT_KEY 另填 deptKey + 映射里的科室名 */
|
||
private void applyAssigneeScope(Map<String, Object> row, FlowDefModel.Assignee assignee,
|
||
Long schId, Map<String, String> deptNames) {
|
||
if (assignee == null || assignee.type == null) {
|
||
return;
|
||
}
|
||
row.put("assigneeType", assignee.type);
|
||
if (!FlowDefModel.ASSIGNEE_DEPT_KEY.equals(assignee.type) || assignee.deptKey == null) {
|
||
return;
|
||
}
|
||
row.put("deptKey", assignee.deptKey);
|
||
String deptName = deptNames.computeIfAbsent(assignee.deptKey,
|
||
k -> DeptLabelSupport.deptName(deptAdminService, schId, k));
|
||
if (deptName != null) {
|
||
row.put("deptName", deptName);
|
||
}
|
||
}
|
||
|
||
private FlowModels.TaskItem toTaskItem(FlwTask t) {
|
||
FlowModels.TaskItem item = new FlowModels.TaskItem();
|
||
item.setTaskId(t.getId());
|
||
item.setInstanceId(t.getInstanceId());
|
||
item.setNodeId(t.getNodeId());
|
||
item.setNodeName(t.getNodeName());
|
||
item.setAssigneeName(t.getAssigneeName());
|
||
item.setStatus(t.getStatus());
|
||
item.setOpinion(t.getOpinion());
|
||
item.setArriveTime(str(t.getArriveTime()));
|
||
item.setHandleTime(str(t.getHandleTime()));
|
||
item.setDeadline(str(t.getDeadline()));
|
||
item.setUrgeCount(t.getUrgeCount());
|
||
return item;
|
||
}
|
||
|
||
private void fillInstanceInfo(List<FlowModels.TaskItem> items) {
|
||
for (FlowModels.TaskItem 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());
|
||
item.setSubmitterName(instance.getSubmitterName());
|
||
}
|
||
}
|
||
}
|
||
|
||
/**
|
||
* 展示时间统一截断到秒:时间列已升毫秒精度(DATETIME(3),轨迹交织排序用),
|
||
* 对外展示口径保持秒级不变(存量前端零适配);排序用未截断的原值。
|
||
*/
|
||
private static String str(LocalDateTime t) {
|
||
return t == null ? null : t.truncatedTo(java.time.temporal.ChronoUnit.SECONDS).toString();
|
||
}
|
||
|
||
private static int normPage(Integer page) {
|
||
return page == null || page < 1 ? 0 : page - 1;
|
||
}
|
||
|
||
/** 空串按「不过滤」处理(前端清筛选可能传空值,避免误筛成空列表) */
|
||
private static String emptyToNull(String s) {
|
||
return s == null || s.trim().isEmpty() ? null : s.trim();
|
||
}
|
||
|
||
private static int normSize(Integer size) {
|
||
return size == null || size < 1 || size > 100 ? 10 : size;
|
||
}
|
||
|
||
private static <T> BasePageResult<T> toResult(long total, List<T> content, Integer page, Integer size) {
|
||
int pageIndex = page == null || page < 1 ? 1 : page;
|
||
int pageSize = size == null || size < 1 || size > 100 ? 10 : size;
|
||
int totalPages = (int) Math.ceil(total / (double) pageSize);
|
||
return new BasePageResult<>(content, (int) total, pageIndex, pageSize, totalPages,
|
||
pageIndex < totalPages);
|
||
}
|
||
}
|