- 已办去重: 同一办件一行 + 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:
@@ -41,6 +41,8 @@ class FlowEngineIT extends FlowTestSupport {
|
||||
private FlowInstanceService instanceService;
|
||||
@Autowired
|
||||
private FlowTaskService taskService;
|
||||
@Autowired
|
||||
private com.yunhai.flowcenter.service.FlowQueryService queryService;
|
||||
|
||||
@TestConfiguration
|
||||
static class StubConfig {
|
||||
@@ -107,7 +109,7 @@ class FlowEngineIT extends FlowTestSupport {
|
||||
assertEquals(FlwEnums.InstanceStatus.REJECTED.getCode(), instance.getStatus());
|
||||
assertNotNull(instance.getFinishTime());
|
||||
// 同批另一待办已失效
|
||||
List<FlwTask> all = taskRepository.findByInstanceIdOrderByIdAsc(instance.getId());
|
||||
List<FlwTask> all = allTasks(instance.getId());
|
||||
assertEquals(FlwEnums.TaskStatus.REJECTED.getCode(), all.get(0).getStatus());
|
||||
assertEquals(FlwEnums.TaskStatus.INVALIDATED.getCode(), all.get(1).getStatus());
|
||||
}
|
||||
@@ -129,6 +131,15 @@ class FlowEngineIT extends FlowTestSupport {
|
||||
instanceService.withdraw(SCH, outcome.getInstanceId());
|
||||
assertEquals(FlwEnums.InstanceStatus.WITHDRAWN.getCode(),
|
||||
instanceRepository.findById(outcome.getInstanceId()).get().getStatus());
|
||||
// 撤回留痕(2026-09-18):轨迹里必须有"撤回申请"行,否则撤回在时间线上是空白
|
||||
List<FlwTask> afterWithdraw = traceRows(outcome.getInstanceId());
|
||||
FlwTask withdrawRow = afterWithdraw.stream()
|
||||
.filter(t -> FlwEnums.TaskStatus.NODE_WITHDRAW.equals(t.getNodeId())).findFirst().orElse(null);
|
||||
assertNotNull(withdrawRow, "撤回必须留痕");
|
||||
assertEquals(FlwEnums.TaskStatus.WITHDRAWN.getCode(), withdrawRow.getStatus());
|
||||
assertEquals(201, withdrawRow.getAssigneeId()); // 操作人=提交人
|
||||
// 留痕行不算"已被办理":重提前的撤回守卫口径不能被自己写的事件行挡住
|
||||
assertFalse(FlwEnums.TaskStatus.handledCodes().contains(withdrawRow.getStatus()));
|
||||
|
||||
// 重提重启后再办理,随后撤回必须被拒(已有任务被办理)
|
||||
FlowInstanceService.ApplyOutcome reapplied = apply(def.getFlowGuid(), "biz-4", 201, "张三");
|
||||
@@ -137,6 +148,60 @@ class FlowEngineIT extends FlowTestSupport {
|
||||
assertThrows(BizException.class, () -> instanceService.withdraw(SCH, reapplied.getInstanceId()));
|
||||
}
|
||||
|
||||
/**
|
||||
* 轨迹时间流 + 已办去重(2026-09-18):
|
||||
* ① 发起/重新提交留痕成事件行,detail.trace 按发生时间升序且带 kind/round/科室富化;
|
||||
* ② 同一办件被本人办理两次(驳回 + 重提后再同意)时,已办列表只出一行、doneCount=2、
|
||||
* 结果取最后一次(同意),并保留"共办理 N 次"的提示依据。
|
||||
*/
|
||||
@Test
|
||||
void trace_timeline_kindsRounds_andDoneDedup() {
|
||||
mapDept("ybb", 101, "甲");
|
||||
FlwDef def = saveAndPublishDef("t20", "轨迹时间流", linearDef("ybb", "审核"));
|
||||
FlowInstanceService.ApplyOutcome first = apply(def.getFlowGuid(), "biz-20", 201, "张三");
|
||||
Long instanceId = first.getInstanceId();
|
||||
|
||||
// 一轮:办理人驳回;提交人重提;二轮:同一个人同意 → 同一办件上本人两条已办
|
||||
taskService.handle(SCH, taskRepository.findByInstanceIdAndStatus(instanceId, 0).get(0).getId(),
|
||||
false, "材料不全", 101);
|
||||
apply(def.getFlowGuid(), "biz-20", 201, "张三");
|
||||
taskService.handle(SCH, taskRepository.findByInstanceIdAndStatus(instanceId, 0).get(0).getId(),
|
||||
true, "补齐了", 101);
|
||||
assertEquals(FlwEnums.InstanceStatus.COMPLETED.getCode(),
|
||||
instanceRepository.findById(instanceId).get().getStatus());
|
||||
|
||||
// ① 轨迹时间流:发起 → 驳回 → 重新提交 → 同意
|
||||
java.util.List<java.util.Map<String, Object>> trace =
|
||||
queryService.detail(SCH, instanceId, null).getTrace();
|
||||
java.util.List<String> kinds = new java.util.ArrayList<>();
|
||||
for (java.util.Map<String, Object> row : trace) {
|
||||
kinds.add((String) row.get("kind"));
|
||||
}
|
||||
assertEquals(java.util.Arrays.asList("SUBMIT", "REJECT", "RESUBMIT", "APPROVE"), kinds);
|
||||
// 时间为升序(前端直接按后端顺序渲染)
|
||||
for (int i = 1; i < trace.size(); i++) {
|
||||
assertTrue(String.valueOf(trace.get(i).get("time"))
|
||||
.compareTo(String.valueOf(trace.get(i - 1).get("time"))) >= 0,
|
||||
"轨迹必须按发生时间升序:" + trace);
|
||||
}
|
||||
// 轮次:驳回属于第 1 轮,重新提交与二轮同意属于第 2 轮
|
||||
assertEquals(1, trace.get(1).get("round"));
|
||||
assertEquals(2, trace.get(2).get("round"));
|
||||
assertEquals(2, trace.get(3).get("round"));
|
||||
// 科室口径富化(DEPT_KEY 节点):deptName 取映射里的科室名,前端"按科室"据此换掉人名
|
||||
assertEquals("DEPT_KEY", trace.get(3).get("assigneeType"));
|
||||
assertEquals("ybb", trace.get(3).get("deptKey"));
|
||||
assertEquals("ybb科", trace.get(3).get("deptName"));
|
||||
|
||||
// ② 已办去重:同一办件一行,结果是最后一次(同意),次数=2
|
||||
com.yunhai.platform.utils.model.BasePageResult<com.yunhai.flowcenter.model.FlowModels.TaskItem>
|
||||
done = queryService.pageDone(SCH, 101, 1, 10);
|
||||
assertEquals(1, done.getContent().size(), "一个办件只能有一条已办记录");
|
||||
assertEquals(2, done.getContent().get(0).getDoneCount());
|
||||
assertEquals(FlwEnums.TaskStatus.APPROVED.getCode(), done.getContent().get(0).getStatus());
|
||||
assertEquals("审核", done.getContent().get(0).getNodeName());
|
||||
}
|
||||
|
||||
@Test
|
||||
void reapply_inFlight_duplicated_noNewInstance() {
|
||||
mapDept("ybb", 101, "甲");
|
||||
@@ -171,7 +236,7 @@ class FlowEngineIT extends FlowTestSupport {
|
||||
assertEquals(FlwEnums.InstanceStatus.APPROVING.getCode(), after.getStatus());
|
||||
assertTrue(after.getVersion() > versionBefore, "重启后 instanceVersion 必须单调递增");
|
||||
// 轨迹连续:历史拒绝任务保留 + 新一轮任务待办
|
||||
List<FlwTask> all = taskRepository.findByInstanceIdOrderByIdAsc(first.getInstanceId());
|
||||
List<FlwTask> all = allTasks(first.getInstanceId());
|
||||
assertEquals(FlwEnums.TaskStatus.REJECTED.getCode(), all.get(0).getStatus());
|
||||
assertEquals(1, taskRepository.findByInstanceIdAndStatus(first.getInstanceId(), 0).size());
|
||||
assertNull(after.getFinishTime());
|
||||
@@ -240,7 +305,7 @@ class FlowEngineIT extends FlowTestSupport {
|
||||
assertEquals(FlwEnums.InstanceStatus.COMPLETED.getCode(),
|
||||
instanceRepository.findById(outcome.getInstanceId()).get().getStatus());
|
||||
// 只应有一轮 END 完成(无重复推进导致的重复任务创建:总任务数=2)
|
||||
assertEquals(2, taskRepository.findByInstanceIdOrderByIdAsc(outcome.getInstanceId()).size());
|
||||
assertEquals(2, allTasks(outcome.getInstanceId()).size());
|
||||
}
|
||||
|
||||
@Test
|
||||
@@ -306,7 +371,7 @@ class FlowEngineIT extends FlowTestSupport {
|
||||
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);
|
||||
List<FlwTask> all = allTasks(instanceId);
|
||||
assertEquals(FlwEnums.TaskStatus.APPROVED.getCode(), all.get(0).getStatus());
|
||||
assertEquals(FlwEnums.TaskStatus.INVALIDATED.getCode(), all.get(1).getStatus());
|
||||
}
|
||||
@@ -375,7 +440,18 @@ class FlowEngineIT extends FlowTestSupport {
|
||||
@Autowired
|
||||
private com.yunhai.flowcenter.repository.FlwBranchRepository branchRepository;
|
||||
|
||||
/**
|
||||
* 审批任务行(剔除 6/7 留痕事件行,2026-09-18 起"发起/重新提交/撤回"也落 flw_task 行):
|
||||
* 只关心审批动作的断言(按序号取值、按数量计数)必须走这个口,否则会取到事件行。
|
||||
*/
|
||||
private List<FlwTask> allTasks(Long instanceId) {
|
||||
return taskRepository.findByInstanceIdOrderByIdAsc(instanceId).stream()
|
||||
.filter(t -> !FlwEnums.TaskStatus.eventCodes().contains(t.getStatus()))
|
||||
.collect(java.util.stream.Collectors.toList());
|
||||
}
|
||||
|
||||
/** 全量轨迹行(含 6/7 留痕事件行):留痕类断言用 */
|
||||
private List<FlwTask> traceRows(Long instanceId) {
|
||||
return taskRepository.findByInstanceIdOrderByIdAsc(instanceId);
|
||||
}
|
||||
|
||||
|
||||
@@ -199,6 +199,25 @@ class OapiFlowContractIT extends FlowTestSupport {
|
||||
assertEquals(40, instanceRepository.findById(instanceId).get().getStatus());
|
||||
assertEquals(0, taskRepository.findByInstanceIdAndStatus(instanceId, 0).size(), "待办任务须全部失效");
|
||||
|
||||
// 对外轨迹(progress steps)必须能看到"撤回"这一步(2026-09-18 撤回留痕)
|
||||
JsonNode progress = unwrap(client.get(
|
||||
"/oapi/flow/progress?schId=1&instanceId=" + instanceId, JsonNode.class).getBody());
|
||||
JsonNode steps = progress.get("steps");
|
||||
JsonNode withdrawStep = null;
|
||||
for (JsonNode s : steps) {
|
||||
if ("withdraw".equals(s.get("nodeId").asText())) {
|
||||
withdrawStep = s;
|
||||
}
|
||||
}
|
||||
assertNotNull(withdrawStep, "对外进度须含撤回步骤:" + steps);
|
||||
assertEquals("撤回申请", withdrawStep.get("nodeName").asText());
|
||||
assertEquals("已撤回", withdrawStep.get("label").asText());
|
||||
assertEquals("已撤回", withdrawStep.get("assignees").get(0).get("statusLabel").asText());
|
||||
assertEquals(201, withdrawStep.get("assignees").get(0).get("userId").asInt());
|
||||
// 发起步仍在首位且文案不变(新办件用真实留痕行,老办件走合成步——对外表现一致)
|
||||
assertEquals("submit", steps.get(0).get("nodeId").asText());
|
||||
assertEquals("发起申请", steps.get(0).get("label").asText());
|
||||
|
||||
// 重复撤回(已 40)→ 拒绝
|
||||
ResponseEntity<JsonNode> again = client.post(
|
||||
"/oapi/flow/instance/" + instanceId + "/withdraw?schId=1&submitterId=201", null, JsonNode.class);
|
||||
|
||||
@@ -210,10 +210,22 @@ class FlowDiagramServiceTest {
|
||||
void c1_autoView_deptKeyNode_rendersDeptStyle() {
|
||||
when(mapRepository.findEnabled(SCH, "rs")).thenReturn(Arrays.asList(
|
||||
mapRow("人事处", 11, "张三"), mapRow("人事处", 12, "李四")));
|
||||
FlowModels.NodeVo n1 = node(service.build(def(DEPT_DEF_JSON), null, null), "n1");
|
||||
FlowModels.NodeVo n1 = node(service.build(def(DEPT_DEF_JSON), null, "auto"), "n1");
|
||||
assertEquals("人事处", n1.getLabel());
|
||||
assertEquals("科室 · 2人 · 时限48h", n1.getSubLabel());
|
||||
assertEquals("auto", service.build(def(DEPT_DEF_JSON), null, null).getMode());
|
||||
assertEquals("auto", service.build(def(DEPT_DEF_JSON), null, "auto").getMode());
|
||||
}
|
||||
|
||||
/** 2026-09-18:mode 缺省改为人员视图(原 auto 自适应)——老深链显式带 auto 仍按自适应出图 */
|
||||
@Test
|
||||
void c1_defaultMode_isUserView() {
|
||||
when(mapRepository.findEnabled(SCH, "rs")).thenReturn(Arrays.asList(
|
||||
mapRow("人事处", 11, "张三"), mapRow("人事处", 12, "李四")));
|
||||
FlowModels.DiagramVo vo = service.build(def(DEPT_DEF_JSON), null, null);
|
||||
assertEquals("user", vo.getMode());
|
||||
FlowModels.NodeVo n1 = node(vo, "n1");
|
||||
assertEquals("人事处审批", n1.getLabel());
|
||||
assertEquals("张三、李四 · 时限48h", n1.getSubLabel());
|
||||
}
|
||||
|
||||
// ==================== C2:USERS 节点人员视图(UserBase 补名降级) ====================
|
||||
@@ -289,7 +301,8 @@ class FlowDiagramServiceTest {
|
||||
FlowModels.DiagramVo vo = service.byGuid(SCH, "g1", null);
|
||||
assertEquals("教师入职", vo.getName());
|
||||
assertEquals(Integer.valueOf(2), vo.getVersion());
|
||||
assertEquals("auto", vo.getMode());
|
||||
// 2026-09-18:缺省口径=人员视图
|
||||
assertEquals("user", vo.getMode());
|
||||
assertNull(vo.getInstanceId());
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user