- 已办去重: 同一办件一行 + doneCount 累计次数(FlwTaskRepository 查询去重)
- 发起/撤回/重提留痕事件行(状态码 6/7, 无需 DDL); /oapi/flow/progress steps 含撤回/重提步骤
- 管理端新增启用回路: POST /admin/map/enable/{id}、POST /admin/def/enable/{id}; GET /api/flow/def/source 源码查询
- 修复 findEnabledByOrdId 未过滤 status 的隐性缺陷; 新增 DeptLabelSupport 科室标签支撑
- 测试: FlowEngineIT 撤回留痕/已办去重断言、OapiFlowContractIT 撤回步骤断言(94 tests 全过)
- 本地验签修复: 本地 UserBase 无 context-path, yh.open.services 显式覆盖服务目录枚举兜底的 /userbase basePath, 否则出站签名与服务端剥离不对齐 → GetUserInfo 等全部「验签失败」; 生产经 Caddy 挂 /userbase 时删除 services 覆盖
This commit is contained in:
@@ -9,6 +9,7 @@ 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.FlwBranchRepository;
|
||||
import com.yunhai.flowcenter.repository.FlwDefRepository;
|
||||
import com.yunhai.flowcenter.repository.FlwInstanceRepository;
|
||||
@@ -23,6 +24,7 @@ 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;
|
||||
@@ -39,6 +41,7 @@ public class FlowQueryService {
|
||||
private final FlwTaskRepository taskRepository;
|
||||
private final FlwBranchRepository branchRepository;
|
||||
private final FlwDefRepository defRepository;
|
||||
private final FlwAssigneeMapRepository mapRepository;
|
||||
private final FlowEngine flowEngine;
|
||||
|
||||
public BasePageResult<FlowModels.InstanceItem> pageMy(Long schId, Integer submitterId,
|
||||
@@ -59,16 +62,36 @@ public class FlowQueryService {
|
||||
return toResult(p.getTotalElements(), items, page, size);
|
||||
}
|
||||
|
||||
/** 已办:一个办件一行(保留本人最后办理的那条),并回填累计办理次数 */
|
||||
public BasePageResult<FlowModels.TaskItem> pageDone(Long schId, Integer assigneeId,
|
||||
Integer page, Integer size) {
|
||||
Page<FlwTask> p = taskRepository.pageDone(schId, assigneeId,
|
||||
List<Integer> handled = FlwEnums.TaskStatus.handledCodes();
|
||||
Page<FlwTask> p = taskRepository.pageDone(schId, assigneeId, handled,
|
||||
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 是登录名字符串,
|
||||
@@ -99,24 +122,9 @@ public class FlowQueryService {
|
||||
detail.setFinishTime(str(instance.getFinishTime()));
|
||||
detail.setVersion(instance.getVersion());
|
||||
List<FlwTask> tasks = taskRepository.findByInstanceIdOrderByIdAsc(instanceId);
|
||||
List<Map<String, Object>> trace = new ArrayList<>();
|
||||
for (FlwTask t : tasks) {
|
||||
Map<String, Object> row = new HashMap<>();
|
||||
row.put("taskId", t.getId());
|
||||
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("deadline", str(t.getDeadline()));
|
||||
row.put("urgeCount", t.getUrgeCount());
|
||||
row.put("edgeName", t.getEdgeName());
|
||||
trace.add(row);
|
||||
}
|
||||
detail.setTrace(trace);
|
||||
FlwDef def = defRepository.findById(instance.getDefId()).orElse(null);
|
||||
FlowDefModel model = def == null ? null : flowEngine.parseModel(def);
|
||||
detail.setTrace(buildTrace(instance, tasks, model));
|
||||
List<Map<String, Object>> branches = new ArrayList<>();
|
||||
for (FlwBranch b : branchRepository.findByInstanceId(instanceId)) {
|
||||
Map<String, Object> row = new HashMap<>();
|
||||
@@ -136,15 +144,13 @@ public class FlowQueryService {
|
||||
detail.setMyPendingTask(!myPending.isEmpty());
|
||||
detail.setMyPendingTaskIds(myPending);
|
||||
}
|
||||
FlwDef def = defRepository.findById(instance.getDefId()).orElse(null);
|
||||
detail.setNodeSummary(def == null ? "" : FlowEngine.nodeSummary(flowEngine.parseModel(def)));
|
||||
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) {
|
||||
FlowDefModel model = flowEngine.parseModel(def);
|
||||
String curNodeId = myPend.getNodeId();
|
||||
List<FlowDefModel.Edge> outs = model.outEdges(curNodeId);
|
||||
if (outs.size() > 1) {
|
||||
@@ -213,6 +219,153 @@ public class FlowQueryService {
|
||||
return item;
|
||||
}
|
||||
|
||||
/**
|
||||
* 流程轨迹(2026-09-18 重排为"时间流"):审批任务行 + 留痕事件行(6 发起/重新提交、7 撤回)
|
||||
* 合成一条按时间升序的轨迹,替掉原先"按 id 升序"的排法(id 序在撤回重走场景下与
|
||||
* 真实发生顺序不一致)。
|
||||
*
|
||||
* <p>每条行补四个前端直接用、不再自行推断的字段:
|
||||
* <ul>
|
||||
* <li>{@code time} 该行实际发生的时间(已办/退回/移交/撤回=handleTime,在办/失效=arriveTime);</li>
|
||||
* <li>{@code kind} SUBMIT|RESUBMIT|WITHDRAW|APPROVE|REJECT|RETURN|TRANSFER|INVALID|PENDING
|
||||
* (前端配色与文案的唯一依据);</li>
|
||||
* <li>{@code round} 第几轮审批(遇"重新提交"进位,撤回属于它结束的那一轮);</li>
|
||||
* <li>{@code rowKey} 去重 key(任务行 t{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,
|
||||
FlowDefModel model) {
|
||||
List<FlwTask> sorted = new ArrayList<>(tasks);
|
||||
sorted.sort(Comparator.comparing(FlowQueryService::happenedAt,
|
||||
Comparator.nullsLast(Comparator.naturalOrder()))
|
||||
.thenComparing(FlwTask::getId));
|
||||
Map<String, String> deptNames = new HashMap<>();
|
||||
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);
|
||||
// 老办件兜底行的插入位:时间早于提交时间的行数(老办件里恒为 0;重提过的老办件按时间放在中段)
|
||||
if (submitTime != null && happenedAt(t).isBefore(submitTime)) {
|
||||
headIndex = trace.size();
|
||||
}
|
||||
}
|
||||
if (!hasSubmitRow(sorted)) {
|
||||
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 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;
|
||||
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.mappedDeptName(mapRepository.findEnabled(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());
|
||||
|
||||
Reference in New Issue
Block a user